mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ci): restore main lint/typecheck after direct merges
This commit is contained in:
@@ -3,23 +3,25 @@
|
|||||||
在你的项目中导入:
|
在你的项目中导入:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const translations = require('./translations/zh-CN.json');
|
const translations = require("./translations/zh-CN.json");
|
||||||
console.log(translations['Save']); // 输出:保存
|
console.log(translations["Save"]); // 输出:保存
|
||||||
```
|
```
|
||||||
|
|
||||||
## 继续翻译工作
|
## 继续翻译工作
|
||||||
|
|
||||||
1. **提取 OpenClaw 界面字符串**
|
1. **提取 OpenClaw 界面字符串**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/extract-strings.js
|
node scripts/extract-strings.js
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **过滤真正的界面文本**
|
2. **过滤真正的界面文本**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/filter-real-ui.js
|
node scripts/filter-real-ui.js
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **翻译剩余的字符串**
|
3. **翻译剩余的字符串**
|
||||||
- 编辑 `translations/ui-only.json`
|
- 编辑 `translations/ui-only.json`
|
||||||
|
|
||||||
## 🛠️ 工具说明
|
## 🛠️ 工具说明
|
||||||
@@ -64,10 +66,12 @@ extensions/openclaw-zh-cn-ui/
|
|||||||
## 📈 路线图
|
## 📈 路线图
|
||||||
|
|
||||||
### 短期目标
|
### 短期目标
|
||||||
|
|
||||||
- 完成剩余翻译
|
- 完成剩余翻译
|
||||||
- 提交 Pull Request
|
- 提交 Pull Request
|
||||||
|
|
||||||
### 长期目标
|
### 长期目标
|
||||||
|
|
||||||
- 支持更多语言
|
- 支持更多语言
|
||||||
- 创建翻译平台
|
- 创建翻译平台
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||||||
import { whatsappPlugin } from "./channel.js";
|
import { whatsappPlugin } from "./channel.js";
|
||||||
|
|
||||||
// Mock runtime
|
// Mock runtime
|
||||||
const mockSendMessageWhatsApp = vi.fn().mockResolvedValue({ messageId: "123", toJid: "123@s.whatsapp.net" });
|
const mockSendMessageWhatsApp = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ messageId: "123", toJid: "123@s.whatsapp.net" });
|
||||||
|
|
||||||
vi.mock("./runtime.js", () => ({
|
vi.mock("./runtime.js", () => ({
|
||||||
getWhatsAppRuntime: () => ({
|
getWhatsAppRuntime: () => ({
|
||||||
@@ -35,7 +37,7 @@ describe("whatsappPlugin.outbound.sendText", () => {
|
|||||||
"http://example.com",
|
"http://example.com",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
linkPreview: false,
|
linkPreview: false,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ describe("whatsappPlugin.outbound.sendText", () => {
|
|||||||
"hello",
|
"hello",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
linkPreview: undefined,
|
linkPreview: undefined,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,10 +73,18 @@ async function listJsonlFiles(dir: string): Promise<string[]> {
|
|||||||
function safeParseLine(line: string): CronRunLogEntry | null {
|
function safeParseLine(line: string): CronRunLogEntry | null {
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(line) as Partial<CronRunLogEntry> | null;
|
const obj = JSON.parse(line) as Partial<CronRunLogEntry> | null;
|
||||||
if (!obj || typeof obj !== "object") return null;
|
if (!obj || typeof obj !== "object") {
|
||||||
if (obj.action !== "finished") return null;
|
return null;
|
||||||
if (typeof obj.ts !== "number" || !Number.isFinite(obj.ts)) return null;
|
}
|
||||||
if (typeof obj.jobId !== "string" || !obj.jobId.trim()) return null;
|
if (obj.action !== "finished") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof obj.ts !== "number" || !Number.isFinite(obj.ts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof obj.jobId !== "string" || !obj.jobId.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return obj as CronRunLogEntry;
|
return obj as CronRunLogEntry;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -91,7 +99,8 @@ export async function main() {
|
|||||||
const args = parseArgs(process.argv);
|
const args = parseArgs(process.argv);
|
||||||
const store = typeof args.store === "string" ? args.store : undefined;
|
const store = typeof args.store === "string" ? args.store : undefined;
|
||||||
const runsDirArg = typeof args.runsDir === "string" ? args.runsDir : undefined;
|
const runsDirArg = typeof args.runsDir === "string" ? args.runsDir : undefined;
|
||||||
const runsDir = runsDirArg ?? (store ? path.join(path.dirname(path.resolve(store)), "runs") : null);
|
const runsDir =
|
||||||
|
runsDirArg ?? (store ? path.join(path.dirname(path.resolve(store)), "runs") : null);
|
||||||
if (!runsDir) {
|
if (!runsDir) {
|
||||||
usageAndExit(2);
|
usageAndExit(2);
|
||||||
}
|
}
|
||||||
@@ -138,19 +147,31 @@ export async function main() {
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const raw = await fs.readFile(file, "utf-8").catch(() => "");
|
const raw = await fs.readFile(file, "utf-8").catch(() => "");
|
||||||
if (!raw.trim()) continue;
|
if (!raw.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const lines = raw.split("\n");
|
const lines = raw.split("\n");
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const entry = safeParseLine(line.trim());
|
const entry = safeParseLine(line.trim());
|
||||||
if (!entry) continue;
|
if (!entry) {
|
||||||
if (entry.ts < fromMs || entry.ts > toMs) continue;
|
continue;
|
||||||
if (filterJobId && entry.jobId !== filterJobId) continue;
|
}
|
||||||
|
if (entry.ts < fromMs || entry.ts > toMs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (filterJobId && entry.jobId !== filterJobId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const model = (entry.model ?? "<unknown>").trim() || "<unknown>";
|
const model = (entry.model ?? "<unknown>").trim() || "<unknown>";
|
||||||
if (filterModel && model !== filterModel) continue;
|
if (filterModel && model !== filterModel) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const jobId = entry.jobId;
|
const jobId = entry.jobId;
|
||||||
const usage = entry.usage;
|
const usage = entry.usage;
|
||||||
const hasUsage = Boolean(usage && (usage.total_tokens ?? usage.input_tokens ?? usage.output_tokens) !== undefined);
|
const hasUsage = Boolean(
|
||||||
|
usage && (usage.total_tokens ?? usage.input_tokens ?? usage.output_tokens) !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const jobAgg = (totalsByJob[jobId] ??= {
|
const jobAgg = (totalsByJob[jobId] ??= {
|
||||||
jobId,
|
jobId,
|
||||||
@@ -219,8 +240,12 @@ export async function main() {
|
|||||||
console.log(`Cron usage report`);
|
console.log(`Cron usage report`);
|
||||||
console.log(` runsDir: ${runsDir}`);
|
console.log(` runsDir: ${runsDir}`);
|
||||||
console.log(` window: ${new Date(fromMs).toISOString()} → ${new Date(toMs).toISOString()}`);
|
console.log(` window: ${new Date(fromMs).toISOString()} → ${new Date(toMs).toISOString()}`);
|
||||||
if (filterJobId) console.log(` filter jobId: ${filterJobId}`);
|
if (filterJobId) {
|
||||||
if (filterModel) console.log(` filter model: ${filterModel}`);
|
console.log(` filter jobId: ${filterJobId}`);
|
||||||
|
}
|
||||||
|
if (filterModel) {
|
||||||
|
console.log(` filter model: ${filterModel}`);
|
||||||
|
}
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||||
import type { Context, Model } from "@mariozechner/pi-ai";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { applyExtraParamsToAgent } from "./extra-params.js";
|
import { applyExtraParamsToAgent } from "./extra-params.js";
|
||||||
|
|
||||||
// Mock streamSimple for testing
|
// Mock streamSimple for testing
|
||||||
@@ -13,7 +12,6 @@ vi.mock("@mariozechner/pi-ai", () => ({
|
|||||||
|
|
||||||
describe("extra-params: Z.AI tool_stream support", () => {
|
describe("extra-params: Z.AI tool_stream support", () => {
|
||||||
it("should inject tool_stream=true for zai provider by default", () => {
|
it("should inject tool_stream=true for zai provider by default", () => {
|
||||||
const capturedPayloads: unknown[] = [];
|
|
||||||
const mockStreamFn: StreamFn = vi.fn((model, context, options) => {
|
const mockStreamFn: StreamFn = vi.fn((model, context, options) => {
|
||||||
// Capture the payload that would be sent
|
// Capture the payload that would be sent
|
||||||
options?.onPayload?.({ model: model.id, messages: [] });
|
options?.onPayload?.({ model: model.id, messages: [] });
|
||||||
@@ -24,7 +22,7 @@ describe("extra-params: Z.AI tool_stream support", () => {
|
|||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
stopReason: "stop",
|
stopReason: "stop",
|
||||||
}),
|
}),
|
||||||
} as any;
|
} as unknown as ReturnType<StreamFn>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const agent = { streamFn: mockStreamFn };
|
const agent = { streamFn: mockStreamFn };
|
||||||
@@ -34,7 +32,12 @@ describe("extra-params: Z.AI tool_stream support", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
applyExtraParamsToAgent(agent, cfg as any, "zai", "glm-5");
|
applyExtraParamsToAgent(
|
||||||
|
agent,
|
||||||
|
cfg as unknown as Parameters<typeof applyExtraParamsToAgent>[1],
|
||||||
|
"zai",
|
||||||
|
"glm-5",
|
||||||
|
);
|
||||||
|
|
||||||
// The streamFn should be wrapped
|
// The streamFn should be wrapped
|
||||||
expect(agent.streamFn).toBeDefined();
|
expect(agent.streamFn).toBeDefined();
|
||||||
@@ -42,33 +45,44 @@ describe("extra-params: Z.AI tool_stream support", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not inject tool_stream for non-zai providers", () => {
|
it("should not inject tool_stream for non-zai providers", () => {
|
||||||
const mockStreamFn: StreamFn = vi.fn(() => ({
|
const mockStreamFn: StreamFn = vi.fn(
|
||||||
push: vi.fn(),
|
() =>
|
||||||
result: vi.fn().mockResolvedValue({
|
({
|
||||||
role: "assistant",
|
push: vi.fn(),
|
||||||
content: [{ type: "text", text: "ok" }],
|
result: vi.fn().mockResolvedValue({
|
||||||
stopReason: "stop",
|
role: "assistant",
|
||||||
}),
|
content: [{ type: "text", text: "ok" }],
|
||||||
} as any));
|
stopReason: "stop",
|
||||||
|
}),
|
||||||
|
}) as unknown as ReturnType<StreamFn>,
|
||||||
|
);
|
||||||
|
|
||||||
const agent = { streamFn: mockStreamFn };
|
const agent = { streamFn: mockStreamFn };
|
||||||
const cfg = {};
|
const cfg = {};
|
||||||
|
|
||||||
applyExtraParamsToAgent(agent, cfg as any, "anthropic", "claude-opus-4-6");
|
applyExtraParamsToAgent(
|
||||||
|
agent,
|
||||||
|
cfg as unknown as Parameters<typeof applyExtraParamsToAgent>[1],
|
||||||
|
"anthropic",
|
||||||
|
"claude-opus-4-6",
|
||||||
|
);
|
||||||
|
|
||||||
// Should remain unchanged (except for OpenAI wrapper)
|
// Should remain unchanged (except for OpenAI wrapper)
|
||||||
expect(agent.streamFn).toBeDefined();
|
expect(agent.streamFn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow disabling tool_stream via params", () => {
|
it("should allow disabling tool_stream via params", () => {
|
||||||
const mockStreamFn: StreamFn = vi.fn(() => ({
|
const mockStreamFn: StreamFn = vi.fn(
|
||||||
push: vi.fn(),
|
() =>
|
||||||
result: vi.fn().mockResolvedValue({
|
({
|
||||||
role: "assistant",
|
push: vi.fn(),
|
||||||
content: [{ type: "text", text: "ok" }],
|
result: vi.fn().mockResolvedValue({
|
||||||
stopReason: "stop",
|
role: "assistant",
|
||||||
}),
|
content: [{ type: "text", text: "ok" }],
|
||||||
} as any));
|
stopReason: "stop",
|
||||||
|
}),
|
||||||
|
}) as unknown as ReturnType<StreamFn>,
|
||||||
|
);
|
||||||
|
|
||||||
const agent = { streamFn: mockStreamFn };
|
const agent = { streamFn: mockStreamFn };
|
||||||
const cfg = {
|
const cfg = {
|
||||||
@@ -85,7 +99,12 @@ describe("extra-params: Z.AI tool_stream support", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
applyExtraParamsToAgent(agent, cfg as any, "zai", "glm-5");
|
applyExtraParamsToAgent(
|
||||||
|
agent,
|
||||||
|
cfg as unknown as Parameters<typeof applyExtraParamsToAgent>[1],
|
||||||
|
"zai",
|
||||||
|
"glm-5",
|
||||||
|
);
|
||||||
|
|
||||||
// The tool_stream wrapper should be applied but with enabled=false
|
// The tool_stream wrapper should be applied but with enabled=false
|
||||||
// In this case, it should just return the underlying streamFn
|
// In this case, it should just return the underlying streamFn
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
|
import type { TextContent } from "@mariozechner/pi-ai";
|
||||||
|
import type { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||||
import type {
|
import type {
|
||||||
PluginHookBeforeMessageWriteEvent,
|
PluginHookBeforeMessageWriteEvent,
|
||||||
PluginHookBeforeMessageWriteResult,
|
PluginHookBeforeMessageWriteResult,
|
||||||
} from "../plugins/types.js";
|
} from "../plugins/types.js";
|
||||||
import type { TextContent } from "@mariozechner/pi-ai";
|
|
||||||
import type { SessionManager } from "@mariozechner/pi-coding-agent";
|
|
||||||
import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||||
import { HARD_MAX_TOOL_RESULT_CHARS } from "./pi-embedded-runner/tool-result-truncation.js";
|
import { HARD_MAX_TOOL_RESULT_CHARS } from "./pi-embedded-runner/tool-result-truncation.js";
|
||||||
import { makeMissingToolResult, sanitizeToolCallInputs } from "./session-transcript-repair.js";
|
import { makeMissingToolResult, sanitizeToolCallInputs } from "./session-transcript-repair.js";
|
||||||
@@ -132,10 +132,16 @@ export function installSessionToolResultGuard(
|
|||||||
* or null if the message should be blocked.
|
* or null if the message should be blocked.
|
||||||
*/
|
*/
|
||||||
const applyBeforeWriteHook = (msg: AgentMessage): AgentMessage | null => {
|
const applyBeforeWriteHook = (msg: AgentMessage): AgentMessage | null => {
|
||||||
if (!beforeWrite) return msg;
|
if (!beforeWrite) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
const result = beforeWrite({ message: msg });
|
const result = beforeWrite({ message: msg });
|
||||||
if (result?.block) return null;
|
if (result?.block) {
|
||||||
if (result?.message) return result.message;
|
return null;
|
||||||
|
}
|
||||||
|
if (result?.message) {
|
||||||
|
return result.message;
|
||||||
|
}
|
||||||
return msg;
|
return msg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +198,9 @@ export function installSessionToolResultGuard(
|
|||||||
isSynthetic: false,
|
isSynthetic: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (!persisted) return undefined;
|
if (!persisted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return originalAppend(persisted as never);
|
return originalAppend(persisted as never);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +221,9 @@ export function installSessionToolResultGuard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const finalMessage = applyBeforeWriteHook(persistMessage(nextMessage));
|
const finalMessage = applyBeforeWriteHook(persistMessage(nextMessage));
|
||||||
if (!finalMessage) return undefined;
|
if (!finalMessage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const result = originalAppend(finalMessage as never);
|
const result = originalAppend(finalMessage as never);
|
||||||
|
|
||||||
const sessionFile = (
|
const sessionFile = (
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ function generateHtml(sessionData: SessionData): string {
|
|||||||
|
|
||||||
// Build CSS with theme variables
|
// Build CSS with theme variables
|
||||||
const css = templateCss
|
const css = templateCss
|
||||||
.replace("{{THEME_VARS}}", themeVars)
|
.replace("/* {{THEME_VARS}} */", themeVars.trim())
|
||||||
.replace("{{BODY_BG}}", bodyBg)
|
.replace("/* {{BODY_BG_DECL}} */", `--body-bg: ${bodyBg};`)
|
||||||
.replace("{{CONTAINER_BG}}", containerBg)
|
.replace("/* {{CONTAINER_BG_DECL}} */", `--container-bg: ${containerBg};`)
|
||||||
.replace("{{INFO_BG}}", infoBg);
|
.replace("/* {{INFO_BG_DECL}} */", `--info-bg: ${infoBg};`);
|
||||||
|
|
||||||
return template
|
return template
|
||||||
.replace("{{CSS}}", css)
|
.replace("{{CSS}}", css)
|
||||||
@@ -234,7 +234,7 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro
|
|||||||
const args = parseExportArgs(params.command.commandBodyNormalized);
|
const args = parseExportArgs(params.command.commandBodyNormalized);
|
||||||
|
|
||||||
// 1. Resolve session file
|
// 1. Resolve session file
|
||||||
const sessionEntry = params.sessionEntry as SessionEntry | undefined;
|
const sessionEntry = params.sessionEntry;
|
||||||
if (!sessionEntry?.sessionId) {
|
if (!sessionEntry?.sessionId) {
|
||||||
return { text: "❌ No active session found." };
|
return { text: "❌ No active session found." };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function trimMeshPlanCache() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oldest = [...meshPlanCache.entries()]
|
const oldest = [...meshPlanCache.entries()]
|
||||||
.sort((a, b) => a[1].createdAt - b[1].createdAt)
|
.toSorted((a, b) => a[1].createdAt - b[1].createdAt)
|
||||||
.slice(0, meshPlanCache.size - MAX_CACHED_MESH_PLANS);
|
.slice(0, meshPlanCache.size - MAX_CACHED_MESH_PLANS);
|
||||||
for (const [key] of oldest) {
|
for (const [key] of oldest) {
|
||||||
meshPlanCache.delete(key);
|
meshPlanCache.delete(key);
|
||||||
@@ -110,7 +110,10 @@ function putCachedPlan(params: Parameters<CommandHandler>[0], plan: MeshPlanShap
|
|||||||
trimMeshPlanCache();
|
trimMeshPlanCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCachedPlan(params: Parameters<CommandHandler>[0], planId: string): MeshPlanShape | null {
|
function getCachedPlan(
|
||||||
|
params: Parameters<CommandHandler>[0],
|
||||||
|
planId: string,
|
||||||
|
): MeshPlanShape | null {
|
||||||
return meshPlanCache.get(cacheKeyForPlan(params, planId))?.plan ?? null;
|
return meshPlanCache.get(cacheKeyForPlan(params, planId))?.plan ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +193,9 @@ export const handleMeshCommand: CommandHandler = async (params, allowTextCommand
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!params.command.isAuthorizedSender) {
|
if (!params.command.isAuthorizedSender) {
|
||||||
logVerbose(`Ignoring /mesh from unauthorized sender: ${params.command.senderId || "<unknown>"}`);
|
logVerbose(
|
||||||
|
`Ignoring /mesh from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
|
||||||
|
);
|
||||||
return { shouldContinue: false };
|
return { shouldContinue: false };
|
||||||
}
|
}
|
||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,88 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Session Export</title>
|
<title>Session Export</title>
|
||||||
<style>
|
<style>
|
||||||
{{CSS}}
|
{{CSS}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button id="hamburger" title="Open sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><rect x="5" y="6" width="2" height="12"/><path d="M6 12h10c1 0 2 0 2-2V8"/></svg></button>
|
<button id="hamburger" title="Open sidebar">
|
||||||
<div id="sidebar-overlay"></div>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||||
<div id="app">
|
<circle cx="6" cy="6" r="2.5" />
|
||||||
<aside id="sidebar">
|
<circle cx="6" cy="18" r="2.5" />
|
||||||
<div class="sidebar-header">
|
<circle cx="18" cy="12" r="2.5" />
|
||||||
<div class="sidebar-controls">
|
<rect x="5" y="6" width="2" height="12" />
|
||||||
<input type="text" class="sidebar-search" id="tree-search" placeholder="Search...">
|
<path d="M6 12h10c1 0 2 0 2-2V8" />
|
||||||
</div>
|
</svg>
|
||||||
<div class="sidebar-filters">
|
</button>
|
||||||
<button class="filter-btn active" data-filter="default" title="Hide settings entries">Default</button>
|
<div id="sidebar-overlay"></div>
|
||||||
<button class="filter-btn" data-filter="no-tools" title="Default minus tool results">No-tools</button>
|
<div id="app">
|
||||||
<button class="filter-btn" data-filter="user-only" title="Only user messages">User</button>
|
<aside id="sidebar">
|
||||||
<button class="filter-btn" data-filter="labeled-only" title="Only labeled entries">Labeled</button>
|
<div class="sidebar-header">
|
||||||
<button class="filter-btn" data-filter="all" title="Show everything">All</button>
|
<div class="sidebar-controls">
|
||||||
<button class="sidebar-close" id="sidebar-close" title="Close">✕</button>
|
<input type="text" class="sidebar-search" id="tree-search" placeholder="Search..." />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-filters">
|
||||||
|
<button class="filter-btn active" data-filter="default" title="Hide settings entries">
|
||||||
|
Default
|
||||||
|
</button>
|
||||||
|
<button class="filter-btn" data-filter="no-tools" title="Default minus tool results">
|
||||||
|
No-tools
|
||||||
|
</button>
|
||||||
|
<button class="filter-btn" data-filter="user-only" title="Only user messages">
|
||||||
|
User
|
||||||
|
</button>
|
||||||
|
<button class="filter-btn" data-filter="labeled-only" title="Only labeled entries">
|
||||||
|
Labeled
|
||||||
|
</button>
|
||||||
|
<button class="filter-btn" data-filter="all" title="Show everything">All</button>
|
||||||
|
<button class="sidebar-close" id="sidebar-close" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tree-container" id="tree-container"></div>
|
||||||
|
<div class="tree-status" id="tree-status"></div>
|
||||||
|
</aside>
|
||||||
|
<main id="content">
|
||||||
|
<div id="header-container"></div>
|
||||||
|
<div id="messages"></div>
|
||||||
|
</main>
|
||||||
|
<div id="image-modal" class="image-modal">
|
||||||
|
<img id="modal-image" src="" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-container" id="tree-container"></div>
|
|
||||||
<div class="tree-status" id="tree-status"></div>
|
|
||||||
</aside>
|
|
||||||
<main id="content">
|
|
||||||
<div id="header-container"></div>
|
|
||||||
<div id="messages"></div>
|
|
||||||
</main>
|
|
||||||
<div id="image-modal" class="image-modal">
|
|
||||||
<img id="modal-image" src="" alt="">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script id="session-data" type="application/json">{{SESSION_DATA}}</script>
|
<script id="session-data" type="application/json">
|
||||||
|
{{SESSION_DATA}}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Vendored libraries -->
|
<!-- Vendored libraries -->
|
||||||
<script>{{MARKED_JS}}</script>
|
<script>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
MARKED_JS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- highlight.js -->
|
<!-- highlight.js -->
|
||||||
<script>{{HIGHLIGHT_JS}}</script>
|
<script>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
HIGHLIGHT_JS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Main application code -->
|
<!-- Main application code -->
|
||||||
<script>
|
<script>
|
||||||
{{JS}}
|
{
|
||||||
</script>
|
{
|
||||||
</body>
|
JS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -130,7 +130,9 @@ export async function initSessionState(params: {
|
|||||||
// Stale cache (especially with multiple gateway processes or on Windows where
|
// Stale cache (especially with multiple gateway processes or on Windows where
|
||||||
// mtime granularity may miss rapid writes) can cause incorrect sessionId
|
// mtime granularity may miss rapid writes) can cause incorrect sessionId
|
||||||
// generation, leading to orphaned transcript files. See #17971.
|
// generation, leading to orphaned transcript files. See #17971.
|
||||||
const sessionStore: Record<string, SessionEntry> = loadSessionStore(storePath, { skipCache: true });
|
const sessionStore: Record<string, SessionEntry> = loadSessionStore(storePath, {
|
||||||
|
skipCache: true,
|
||||||
|
});
|
||||||
let sessionKey: string | undefined;
|
let sessionKey: string | undefined;
|
||||||
let sessionEntry: SessionEntry;
|
let sessionEntry: SessionEntry;
|
||||||
|
|
||||||
|
|||||||
@@ -235,31 +235,6 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "poll") {
|
|
||||||
const to = readStringParam(params, "to", { required: true });
|
|
||||||
const question =
|
|
||||||
readStringParam(params, "pollQuestion") ??
|
|
||||||
readStringParam(params, "question", { required: true });
|
|
||||||
const options =
|
|
||||||
readStringArrayParam(params, "pollOption") ?? readStringArrayParam(params, "options");
|
|
||||||
const threadId = readStringParam(params, "threadId");
|
|
||||||
const replyTo = readStringParam(params, "replyTo");
|
|
||||||
const silent = typeof params.silent === "boolean" ? params.silent : undefined;
|
|
||||||
return await handleTelegramAction(
|
|
||||||
{
|
|
||||||
action: "sendPoll",
|
|
||||||
to,
|
|
||||||
question,
|
|
||||||
options,
|
|
||||||
replyTo: replyTo != null ? Number(replyTo) : undefined,
|
|
||||||
threadId: threadId != null ? Number(threadId) : undefined,
|
|
||||||
silent,
|
|
||||||
accountId: accountId ?? undefined,
|
|
||||||
},
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function createActionIO(params: { action: DaemonAction; json: boolean }) {
|
|||||||
message?: string;
|
message?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
hints?: string[];
|
hints?: string[];
|
||||||
|
warnings?: string[];
|
||||||
service?: {
|
service?: {
|
||||||
label: string;
|
label: string;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
|
|||||||
@@ -171,10 +171,7 @@ export function loadSessionStore(
|
|||||||
let store: Record<string, SessionEntry> = {};
|
let store: Record<string, SessionEntry> = {};
|
||||||
let mtimeMs = getFileMtimeMs(storePath);
|
let mtimeMs = getFileMtimeMs(storePath);
|
||||||
const maxReadAttempts = process.platform === "win32" ? 3 : 1;
|
const maxReadAttempts = process.platform === "win32" ? 3 : 1;
|
||||||
const retryBuf =
|
const retryBuf = maxReadAttempts > 1 ? new Int32Array(new SharedArrayBuffer(4)) : undefined;
|
||||||
maxReadAttempts > 1
|
|
||||||
? new Int32Array(new SharedArrayBuffer(4))
|
|
||||||
: undefined;
|
|
||||||
for (let attempt = 0; attempt < maxReadAttempts; attempt++) {
|
for (let attempt = 0; attempt < maxReadAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(storePath, "utf-8");
|
const raw = fs.readFileSync(storePath, "utf-8");
|
||||||
@@ -587,9 +584,7 @@ async function saveSessionStoreUnlocked(
|
|||||||
// Final attempt failed — skip this save. The write lock ensures
|
// Final attempt failed — skip this save. The write lock ensures
|
||||||
// the next save will retry with fresh data. Log for diagnostics.
|
// the next save will retry with fresh data. Log for diagnostics.
|
||||||
if (i === 4) {
|
if (i === 4) {
|
||||||
console.warn(
|
console.warn(`[session-store] rename failed after 5 attempts: ${storePath}`);
|
||||||
`[session-store] rename failed after 5 attempts: ${storePath}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,7 +641,13 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!deliveryBestEffort) {
|
if (!deliveryBestEffort) {
|
||||||
return withRunSession({ status: "error", summary, outputText, error: String(err), ...telemetry });
|
return withRunSession({
|
||||||
|
status: "error",
|
||||||
|
summary,
|
||||||
|
outputText,
|
||||||
|
error: String(err),
|
||||||
|
...telemetry,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (synthesizedText) {
|
} else if (synthesizedText) {
|
||||||
@@ -739,7 +745,13 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!deliveryBestEffort) {
|
if (!deliveryBestEffort) {
|
||||||
return withRunSession({ status: "error", summary, outputText, error: String(err), ...telemetry });
|
return withRunSession({
|
||||||
|
status: "error",
|
||||||
|
summary,
|
||||||
|
outputText,
|
||||||
|
error: String(err),
|
||||||
|
...telemetry,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
logWarn(`[cron:${params.job.id}] ${String(err)}`);
|
logWarn(`[cron:${params.job.id}] ${String(err)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ vi.mock("../../config/sessions.js", () => ({
|
|||||||
resolveSessionResetPolicy: vi.fn().mockReturnValue({ mode: "idle", idleMinutes: 60 }),
|
resolveSessionResetPolicy: vi.fn().mockReturnValue({ mode: "idle", idleMinutes: 60 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import { loadSessionStore, evaluateSessionFreshness } from "../../config/sessions.js";
|
||||||
loadSessionStore,
|
|
||||||
evaluateSessionFreshness,
|
|
||||||
} from "../../config/sessions.js";
|
|
||||||
import { resolveCronSession } from "./session.js";
|
import { resolveCronSession } from "./session.js";
|
||||||
|
|
||||||
describe("resolveCronSession", () => {
|
describe("resolveCronSession", () => {
|
||||||
@@ -153,7 +150,11 @@ describe("resolveCronSession", () => {
|
|||||||
"webhook:stable-key": {
|
"webhook:stable-key": {
|
||||||
updatedAt: Date.now() - 1000,
|
updatedAt: Date.now() - 1000,
|
||||||
modelOverride: "some-model",
|
modelOverride: "some-model",
|
||||||
} as any,
|
} as unknown as {
|
||||||
|
sessionId: string;
|
||||||
|
updatedAt: number;
|
||||||
|
modelOverride?: string;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
|
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ export async function readCronRunLogEntries(
|
|||||||
if (jobId && obj.jobId !== jobId) {
|
if (jobId && obj.jobId !== jobId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const usage =
|
||||||
|
obj.usage && typeof obj.usage === "object"
|
||||||
|
? (obj.usage as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
const entry: CronRunLogEntry = {
|
const entry: CronRunLogEntry = {
|
||||||
ts: obj.ts,
|
ts: obj.ts,
|
||||||
jobId: obj.jobId,
|
jobId: obj.jobId,
|
||||||
@@ -117,26 +121,20 @@ export async function readCronRunLogEntries(
|
|||||||
durationMs: obj.durationMs,
|
durationMs: obj.durationMs,
|
||||||
nextRunAtMs: obj.nextRunAtMs,
|
nextRunAtMs: obj.nextRunAtMs,
|
||||||
model: typeof obj.model === "string" && obj.model.trim() ? obj.model : undefined,
|
model: typeof obj.model === "string" && obj.model.trim() ? obj.model : undefined,
|
||||||
provider: typeof obj.provider === "string" && obj.provider.trim() ? obj.provider : undefined,
|
provider:
|
||||||
usage:
|
typeof obj.provider === "string" && obj.provider.trim() ? obj.provider : undefined,
|
||||||
obj.usage && typeof obj.usage === "object"
|
usage: usage
|
||||||
? {
|
? {
|
||||||
input_tokens:
|
input_tokens: typeof usage.input_tokens === "number" ? usage.input_tokens : undefined,
|
||||||
typeof (obj.usage as any).input_tokens === "number" ? (obj.usage as any).input_tokens : undefined,
|
output_tokens:
|
||||||
output_tokens:
|
typeof usage.output_tokens === "number" ? usage.output_tokens : undefined,
|
||||||
typeof (obj.usage as any).output_tokens === "number" ? (obj.usage as any).output_tokens : undefined,
|
total_tokens: typeof usage.total_tokens === "number" ? usage.total_tokens : undefined,
|
||||||
total_tokens:
|
cache_read_tokens:
|
||||||
typeof (obj.usage as any).total_tokens === "number" ? (obj.usage as any).total_tokens : undefined,
|
typeof usage.cache_read_tokens === "number" ? usage.cache_read_tokens : undefined,
|
||||||
cache_read_tokens:
|
cache_write_tokens:
|
||||||
typeof (obj.usage as any).cache_read_tokens === "number"
|
typeof usage.cache_write_tokens === "number" ? usage.cache_write_tokens : undefined,
|
||||||
? (obj.usage as any).cache_read_tokens
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
cache_write_tokens:
|
|
||||||
typeof (obj.usage as any).cache_write_tokens === "number"
|
|
||||||
? (obj.usage as any).cache_write_tokens
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
if (typeof obj.sessionId === "string" && obj.sessionId.trim().length > 0) {
|
if (typeof obj.sessionId === "string" && obj.sessionId.trim().length > 0) {
|
||||||
entry.sessionId = obj.sessionId;
|
entry.sessionId = obj.sessionId;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { auditGatewayServiceConfig, checkTokenDrift, SERVICE_AUDIT_CODES } from "./service-audit.js";
|
import {
|
||||||
|
auditGatewayServiceConfig,
|
||||||
|
checkTokenDrift,
|
||||||
|
SERVICE_AUDIT_CODES,
|
||||||
|
} from "./service-audit.js";
|
||||||
import { buildMinimalServicePath } from "./service-env.js";
|
import { buildMinimalServicePath } from "./service-env.js";
|
||||||
|
|
||||||
describe("auditGatewayServiceConfig", () => {
|
describe("auditGatewayServiceConfig", () => {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { ChannelType } from "@buape/carbon";
|
import { ChannelType } from "@buape/carbon";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { maybeCreateDiscordAutoThread } from "./threading.js";
|
import { maybeCreateDiscordAutoThread } from "./threading.js";
|
||||||
|
|
||||||
describe("maybeCreateDiscordAutoThread", () => {
|
describe("maybeCreateDiscordAutoThread", () => {
|
||||||
const mockClient = { rest: { post: vi.fn(), get: vi.fn() } } as any;
|
const postMock = vi.fn();
|
||||||
const mockMessage = { id: "msg1", timestamp: "123" } as any;
|
const getMock = vi.fn();
|
||||||
|
const mockClient = {
|
||||||
|
rest: { post: postMock, get: getMock },
|
||||||
|
} as unknown as Parameters<typeof maybeCreateDiscordAutoThread>[0]["client"];
|
||||||
|
const mockMessage = {
|
||||||
|
id: "msg1",
|
||||||
|
timestamp: "123",
|
||||||
|
} as unknown as Parameters<typeof maybeCreateDiscordAutoThread>[0]["message"];
|
||||||
|
|
||||||
it("skips auto-thread if channelType is GuildForum", async () => {
|
it("skips auto-thread if channelType is GuildForum", async () => {
|
||||||
const result = await maybeCreateDiscordAutoThread({
|
const result = await maybeCreateDiscordAutoThread({
|
||||||
@@ -18,7 +25,7 @@ describe("maybeCreateDiscordAutoThread", () => {
|
|||||||
combinedBody: "test",
|
combinedBody: "test",
|
||||||
});
|
});
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
expect(mockClient.rest.post).not.toHaveBeenCalled();
|
expect(postMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips auto-thread if channelType is GuildMedia", async () => {
|
it("skips auto-thread if channelType is GuildMedia", async () => {
|
||||||
@@ -33,11 +40,11 @@ describe("maybeCreateDiscordAutoThread", () => {
|
|||||||
combinedBody: "test",
|
combinedBody: "test",
|
||||||
});
|
});
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
expect(mockClient.rest.post).not.toHaveBeenCalled();
|
expect(postMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates auto-thread if channelType is GuildText", async () => {
|
it("creates auto-thread if channelType is GuildText", async () => {
|
||||||
mockClient.rest.post.mockResolvedValueOnce({ id: "thread1" });
|
postMock.mockResolvedValueOnce({ id: "thread1" });
|
||||||
const result = await maybeCreateDiscordAutoThread({
|
const result = await maybeCreateDiscordAutoThread({
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
message: mockMessage,
|
message: mockMessage,
|
||||||
@@ -49,6 +56,6 @@ describe("maybeCreateDiscordAutoThread", () => {
|
|||||||
combinedBody: "test",
|
combinedBody: "test",
|
||||||
});
|
});
|
||||||
expect(result).toBe("thread1");
|
expect(result).toBe("thread1");
|
||||||
expect(mockClient.rest.post).toHaveBeenCalled();
|
expect(postMock).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ describe("mesh handlers", () => {
|
|||||||
it("runs steps in DAG order and supports retrying failed steps", async () => {
|
it("runs steps in DAG order and supports retrying failed steps", async () => {
|
||||||
const runState = new Map<string, "ok" | "error">();
|
const runState = new Map<string, "ok" | "error">();
|
||||||
mocks.agent.mockImplementation(
|
mocks.agent.mockImplementation(
|
||||||
(opts: { params: { idempotencyKey: string }; respond: (ok: boolean, payload?: unknown) => void }) => {
|
(opts: {
|
||||||
|
params: { idempotencyKey: string };
|
||||||
|
respond: (ok: boolean, payload?: unknown) => void;
|
||||||
|
}) => {
|
||||||
const agentRunId = `agent-${opts.params.idempotencyKey}`;
|
const agentRunId = `agent-${opts.params.idempotencyKey}`;
|
||||||
runState.set(agentRunId, "ok");
|
runState.set(agentRunId, "ok");
|
||||||
if (opts.params.idempotencyKey.includes(":review:1")) {
|
if (opts.params.idempotencyKey.includes(":review:1")) {
|
||||||
@@ -120,7 +123,10 @@ describe("mesh handlers", () => {
|
|||||||
|
|
||||||
// Make subsequent retries succeed
|
// Make subsequent retries succeed
|
||||||
mocks.agent.mockImplementation(
|
mocks.agent.mockImplementation(
|
||||||
(opts: { params: { idempotencyKey: string }; respond: (ok: boolean, payload?: unknown) => void }) => {
|
(opts: {
|
||||||
|
params: { idempotencyKey: string };
|
||||||
|
respond: (ok: boolean, payload?: unknown) => void;
|
||||||
|
}) => {
|
||||||
const agentRunId = `agent-${opts.params.idempotencyKey}`;
|
const agentRunId = `agent-${opts.params.idempotencyKey}`;
|
||||||
runState.set(agentRunId, "ok");
|
runState.set(agentRunId, "ok");
|
||||||
opts.respond(true, { runId: agentRunId, status: "accepted" });
|
opts.respond(true, { runId: agentRunId, status: "accepted" });
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import type { GatewayRequestHandlerOptions, GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||||
import { agentCommand } from "../../commands/agent.js";
|
import { agentCommand } from "../../commands/agent.js";
|
||||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import type { GatewayRequestHandlerOptions, GatewayRequestHandlers, RespondFn } from "./types.js";
|
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
validateMeshRetryParams,
|
validateMeshRetryParams,
|
||||||
validateMeshRunParams,
|
validateMeshRunParams,
|
||||||
validateMeshStatusParams,
|
validateMeshStatusParams,
|
||||||
type MeshPlanAutoParams,
|
|
||||||
type MeshRunParams,
|
|
||||||
type MeshWorkflowPlan,
|
type MeshWorkflowPlan,
|
||||||
} from "../protocol/index.js";
|
} from "../protocol/index.js";
|
||||||
import { agentHandlers } from "./agent.js";
|
import { agentHandlers } from "./agent.js";
|
||||||
@@ -77,13 +75,27 @@ function trimMap() {
|
|||||||
if (meshRuns.size <= MAX_KEEP_RUNS) {
|
if (meshRuns.size <= MAX_KEEP_RUNS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sorted = [...meshRuns.values()].sort((a, b) => a.startedAt - b.startedAt);
|
const sorted = [...meshRuns.values()].toSorted((a, b) => a.startedAt - b.startedAt);
|
||||||
const overflow = meshRuns.size - MAX_KEEP_RUNS;
|
const overflow = meshRuns.size - MAX_KEEP_RUNS;
|
||||||
for (const stale of sorted.slice(0, overflow)) {
|
for (const stale of sorted.slice(0, overflow)) {
|
||||||
meshRuns.delete(stale.runId);
|
meshRuns.delete(stale.runId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyUnknown(value: unknown): string {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value instanceof Error) {
|
||||||
|
return value.message;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDependsOn(dependsOn: string[] | undefined): string[] {
|
function normalizeDependsOn(dependsOn: string[] | undefined): string[] {
|
||||||
if (!Array.isArray(dependsOn)) {
|
if (!Array.isArray(dependsOn)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -123,10 +135,7 @@ function normalizePlan(plan: MeshWorkflowPlan): MeshWorkflowPlan {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPlanFromParams(params: {
|
function createPlanFromParams(params: { goal: string; steps?: MeshAutoStep[] }): MeshWorkflowPlan {
|
||||||
goal: string;
|
|
||||||
steps?: MeshAutoStep[];
|
|
||||||
}): MeshWorkflowPlan {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const goal = params.goal.trim();
|
const goal = params.goal.trim();
|
||||||
const sourceSteps = params.steps?.length
|
const sourceSteps = params.steps?.length
|
||||||
@@ -164,7 +173,9 @@ function createPlanFromParams(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePlanGraph(plan: MeshWorkflowPlan): { ok: true; order: string[] } | { ok: false; error: string } {
|
function validatePlanGraph(
|
||||||
|
plan: MeshWorkflowPlan,
|
||||||
|
): { ok: true; order: string[] } | { ok: false; error: string } {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
for (const step of plan.steps) {
|
for (const step of plan.steps) {
|
||||||
if (ids.has(step.id)) {
|
if (ids.has(step.id)) {
|
||||||
@@ -231,7 +242,12 @@ async function callGatewayHandler(
|
|||||||
): Promise<{ ok: boolean; payload?: unknown; error?: unknown; meta?: Record<string, unknown> }> {
|
): Promise<{ ok: boolean; payload?: unknown; error?: unknown; meta?: Record<string, unknown> }> {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
const settle = (result: { ok: boolean; payload?: unknown; error?: unknown; meta?: Record<string, unknown> }) => {
|
const settle = (result: {
|
||||||
|
ok: boolean;
|
||||||
|
payload?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
if (settled) {
|
if (settled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -312,7 +328,7 @@ async function executeStep(params: {
|
|||||||
if (!accepted.ok) {
|
if (!accepted.ok) {
|
||||||
step.status = "failed";
|
step.status = "failed";
|
||||||
step.endedAt = Date.now();
|
step.endedAt = Date.now();
|
||||||
step.error = String(accepted.error ?? "agent request failed");
|
step.error = stringifyUnknown(accepted.error ?? "agent request failed");
|
||||||
run.history.push({
|
run.history.push({
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
type: "step.error",
|
type: "step.error",
|
||||||
@@ -369,7 +385,7 @@ async function executeStep(params: {
|
|||||||
step.error =
|
step.error =
|
||||||
typeof waitPayload?.error === "string"
|
typeof waitPayload?.error === "string"
|
||||||
? waitPayload.error
|
? waitPayload.error
|
||||||
: String(waited.error ?? `agent.wait returned status ${waitStatus}`);
|
: stringifyUnknown(waited.error ?? `agent.wait returned status ${waitStatus}`);
|
||||||
run.history.push({
|
run.history.push({
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
type: "step.error",
|
type: "step.error",
|
||||||
@@ -647,7 +663,8 @@ async function generateAutoPlan(params: {
|
|||||||
const prompt = buildAutoPlannerPrompt({ goal: params.goal, maxSteps: params.maxSteps });
|
const prompt = buildAutoPlannerPrompt({ goal: params.goal, maxSteps: params.maxSteps });
|
||||||
const timeoutSeconds = Math.ceil((params.timeoutMs ?? AUTO_PLAN_TIMEOUT_MS) / 1000);
|
const timeoutSeconds = Math.ceil((params.timeoutMs ?? AUTO_PLAN_TIMEOUT_MS) / 1000);
|
||||||
const resolvedAgentId = normalizeAgentId(params.agentId ?? "main");
|
const resolvedAgentId = normalizeAgentId(params.agentId ?? "main");
|
||||||
const plannerSessionKey = params.sessionKey?.trim() || `agent:${resolvedAgentId}:${PLANNER_MAIN_KEY}`;
|
const plannerSessionKey =
|
||||||
|
params.sessionKey?.trim() || `agent:${resolvedAgentId}:${PLANNER_MAIN_KEY}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const runResult = await agentCommand(
|
const runResult = await agentCommand(
|
||||||
@@ -732,7 +749,7 @@ export const meshHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = params as MeshPlanAutoParams;
|
const p = params;
|
||||||
const maxSteps =
|
const maxSteps =
|
||||||
typeof p.maxSteps === "number" && Number.isFinite(p.maxSteps)
|
typeof p.maxSteps === "number" && Number.isFinite(p.maxSteps)
|
||||||
? Math.max(1, Math.min(16, Math.floor(p.maxSteps)))
|
? Math.max(1, Math.min(16, Math.floor(p.maxSteps)))
|
||||||
@@ -782,7 +799,7 @@ export const meshHandlers: GatewayRequestHandlers = {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const p = params as MeshRunParams;
|
const p = params;
|
||||||
const plan = normalizePlan(p.plan);
|
const plan = normalizePlan(p.plan);
|
||||||
const graph = validatePlanGraph(plan);
|
const graph = validatePlanGraph(plan);
|
||||||
if (!graph.ok) {
|
if (!graph.ok) {
|
||||||
@@ -853,7 +870,11 @@ export const meshHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (run.status === "running") {
|
if (run.status === "running") {
|
||||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "mesh run is currently running"));
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.UNAVAILABLE, "mesh run is currently running"),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const stepIds = resolveStepIdsForRetry(run, params.stepIds);
|
const stepIds = resolveStepIdsForRetry(run, params.stepIds);
|
||||||
|
|||||||
@@ -526,8 +526,9 @@ describe("gateway mesh.plan.auto scope handling", () => {
|
|||||||
it("allows operator.write clients for mesh.plan.auto", async () => {
|
it("allows operator.write clients for mesh.plan.auto", async () => {
|
||||||
const { handleGatewayRequest } = await import("../server-methods.js");
|
const { handleGatewayRequest } = await import("../server-methods.js");
|
||||||
const respond = vi.fn();
|
const respond = vi.fn();
|
||||||
const handler = vi.fn(({ respond: send }: { respond: (ok: boolean, payload?: unknown) => void }) =>
|
const handler = vi.fn(
|
||||||
send(true, { ok: true }),
|
({ respond: send }: { respond: (ok: boolean, payload?: unknown) => void }) =>
|
||||||
|
send(true, { ok: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await handleGatewayRequest({
|
await handleGatewayRequest({
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ beforeEach(() => {
|
|||||||
const runtime = createPluginRuntime();
|
const runtime = createPluginRuntime();
|
||||||
setTelegramRuntime(runtime);
|
setTelegramRuntime(runtime);
|
||||||
setActivePluginRegistry(
|
setActivePluginRegistry(
|
||||||
createTestRegistry([
|
createTestRegistry([{ pluginId: "telegram", plugin: telegramPlugin, source: "test" }]),
|
||||||
{ pluginId: "telegram", plugin: telegramPlugin, source: "test" },
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ describe("heartbeat transcript pruning", () => {
|
|||||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||||
|
|
||||||
// Create a transcript with some existing content
|
// Create a transcript with some existing content
|
||||||
const originalContent = await createTranscriptWithContent(transcriptPath, sessionId);
|
await createTranscriptWithContent(transcriptPath, sessionId);
|
||||||
const originalSize = (await fs.stat(transcriptPath)).size;
|
const originalSize = (await fs.stat(transcriptPath)).size;
|
||||||
|
|
||||||
// Seed session store
|
// Seed session store
|
||||||
@@ -147,7 +145,7 @@ describe("heartbeat transcript pruning", () => {
|
|||||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||||
|
|
||||||
// Create a transcript with some existing content
|
// Create a transcript with some existing content
|
||||||
const originalContent = await createTranscriptWithContent(transcriptPath, sessionId);
|
await createTranscriptWithContent(transcriptPath, sessionId);
|
||||||
const originalSize = (await fs.stat(transcriptPath)).size;
|
const originalSize = (await fs.stat(transcriptPath)).size;
|
||||||
|
|
||||||
// Seed session store
|
// Seed session store
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ function resolvePrimaryIPv4(): string | undefined {
|
|||||||
function initSelfPresence() {
|
function initSelfPresence() {
|
||||||
const host = os.hostname();
|
const host = os.hostname();
|
||||||
const ip = resolvePrimaryIPv4() ?? undefined;
|
const ip = resolvePrimaryIPv4() ?? undefined;
|
||||||
const version = process.env.OPENCLAW_VERSION ?? process.env.OPENCLAW_SERVICE_VERSION ?? process.env.npm_package_version ?? "unknown";
|
const version =
|
||||||
|
process.env.OPENCLAW_VERSION ??
|
||||||
|
process.env.OPENCLAW_SERVICE_VERSION ??
|
||||||
|
process.env.npm_package_version ??
|
||||||
|
"unknown";
|
||||||
const modelIdentifier = (() => {
|
const modelIdentifier = (() => {
|
||||||
const p = os.platform();
|
const p = os.platform();
|
||||||
if (p === "darwin") {
|
if (p === "darwin") {
|
||||||
|
|||||||
@@ -414,7 +414,6 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
|
|||||||
return { message: current };
|
return { message: current };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Message Write Hooks
|
// Message Write Hooks
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ export type PluginHookBeforeMessageWriteEvent = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PluginHookBeforeMessageWriteResult = {
|
export type PluginHookBeforeMessageWriteResult = {
|
||||||
block?: boolean; // If true, message is NOT written to JSONL
|
block?: boolean; // If true, message is NOT written to JSONL
|
||||||
message?: AgentMessage; // Optional: modified message to write instead
|
message?: AgentMessage; // Optional: modified message to write instead
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe("Discord Session Key Continuity", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(missingIdKey).toContain("unknown");
|
expect(missingIdKey).toContain("unknown");
|
||||||
|
|
||||||
// Should still be distinct from main
|
// Should still be distinct from main
|
||||||
expect(missingIdKey).not.toBe("agent:main:main");
|
expect(missingIdKey).not.toBe("agent:main:main");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { AnyMessageContent, MiscMessageGenerationOptions, WAPresence } from "@whiskeysockets/baileys";
|
import type {
|
||||||
|
AnyMessageContent,
|
||||||
|
MiscMessageGenerationOptions,
|
||||||
|
WAPresence,
|
||||||
|
} from "@whiskeysockets/baileys";
|
||||||
import type { ActiveWebSendOptions } from "../active-listener.js";
|
import type { ActiveWebSendOptions } from "../active-listener.js";
|
||||||
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
||||||
import { toWhatsappJid } from "../../utils.js";
|
import { toWhatsappJid } from "../../utils.js";
|
||||||
@@ -67,9 +71,11 @@ export function createWebSendApi(params: {
|
|||||||
} else {
|
} else {
|
||||||
payload = { text };
|
payload = { text };
|
||||||
}
|
}
|
||||||
const miscOptions: MiscMessageGenerationOptions = {
|
const miscOptions: MiscMessageGenerationOptions | undefined =
|
||||||
linkPreview: sendOptions?.linkPreview === false ? null : undefined,
|
sendOptions?.linkPreview === false
|
||||||
};
|
? // Baileys typing removed linkPreview from public options, but runtime still accepts it.
|
||||||
|
({ linkPreview: null } as unknown as MiscMessageGenerationOptions)
|
||||||
|
: undefined;
|
||||||
const result = await params.sock.sendMessage(jid, payload, miscOptions);
|
const result = await params.sock.sendMessage(jid, payload, miscOptions);
|
||||||
const accountId = sendOptions?.accountId ?? params.defaultAccountId;
|
const accountId = sendOptions?.accountId ?? params.defaultAccountId;
|
||||||
recordWhatsAppOutbound(accountId);
|
recordWhatsAppOutbound(accountId);
|
||||||
|
|||||||
@@ -83,9 +83,7 @@ export async function sendMessageWhatsApp(
|
|||||||
? {
|
? {
|
||||||
...(options.gifPlayback ? { gifPlayback: true } : {}),
|
...(options.gifPlayback ? { gifPlayback: true } : {}),
|
||||||
...(documentFileName ? { fileName: documentFileName } : {}),
|
...(documentFileName ? { fileName: documentFileName } : {}),
|
||||||
...(options.linkPreview !== undefined
|
...(options.linkPreview !== undefined ? { linkPreview: options.linkPreview } : {}),
|
||||||
? { linkPreview: options.linkPreview }
|
|
||||||
: {}),
|
|
||||||
accountId,
|
accountId,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user