fix(ci): restore main lint/typecheck after direct merges

This commit is contained in:
Peter Steinberger
2026-02-16 23:26:02 +00:00
parent 076df941a3
commit eaa2f7a7bf
29 changed files with 3025 additions and 2575 deletions

View File

@@ -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
### 长期目标 ### 长期目标
- 支持更多语言 - 支持更多语言
- 创建翻译平台 - 创建翻译平台

View File

@@ -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,
}) }),
); );
}); });
}); });

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 = (

View File

@@ -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." };
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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}.`);
}, },
}; };

View File

@@ -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;

View File

@@ -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}`,
);
} }
} }
} }

View File

@@ -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)}`);
} }

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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", () => {

View File

@@ -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();
}); });
}); });

View File

@@ -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" });

View File

@@ -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);

View File

@@ -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({

View File

@@ -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

View File

@@ -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") {

View File

@@ -414,7 +414,6 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
return { message: current }; return { message: current };
} }
// ========================================================================= // =========================================================================
// Message Write Hooks // Message Write Hooks
// ========================================================================= // =========================================================================

View File

@@ -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
}; };

View File

@@ -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");
}); });

View File

@@ -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);

View File

@@ -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;