@vibe-lark/larkpal 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.mjs +510 -124
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import * as net from "node:net";
|
|
|
40
40
|
*
|
|
41
41
|
* 本模块在启动时检测它们是否已安装,缺失时给出明确的安装指引。
|
|
42
42
|
*/
|
|
43
|
-
const log$
|
|
43
|
+
const log$41 = larkLogger("preflight");
|
|
44
44
|
function detectCli(name) {
|
|
45
45
|
try {
|
|
46
46
|
const cliPath = execFileSync("which", [name], {
|
|
@@ -78,14 +78,14 @@ function detectCli(name) {
|
|
|
78
78
|
* 缺失任一依赖时打印安装指引并抛出错误,阻止启动。
|
|
79
79
|
*/
|
|
80
80
|
function preflight() {
|
|
81
|
-
log$
|
|
81
|
+
log$41.info("开始前置环境检查...");
|
|
82
82
|
const useCodex = (process.env.LARKPAL_RUNTIME || "claude-code") === "codex-app-server";
|
|
83
83
|
const claude = detectCli("claude");
|
|
84
84
|
const codex = useCodex ? detectCli(process.env.LARKPAL_CODEX_BIN || "codex") : void 0;
|
|
85
85
|
const larkCli = detectCli("lark-cli");
|
|
86
86
|
const status = (dep) => dep.installed ? `✅ ${dep.name} (${dep.version ?? "unknown"}) → ${dep.path}` : `❌ ${dep.name} 未安装`;
|
|
87
|
-
log$
|
|
88
|
-
log$
|
|
87
|
+
log$41.info(` ${status(useCodex && codex ? codex : claude)}`);
|
|
88
|
+
log$41.info(` ${status(larkCli)}`);
|
|
89
89
|
const missing = [];
|
|
90
90
|
if (useCodex) {
|
|
91
91
|
if (!codex?.installed) missing.push([
|
|
@@ -121,10 +121,10 @@ function preflight() {
|
|
|
121
121
|
"",
|
|
122
122
|
"请安装缺失的依赖后重新启动 LarkPal。"
|
|
123
123
|
].join("\n");
|
|
124
|
-
log$
|
|
124
|
+
log$41.error(msg);
|
|
125
125
|
throw new Error("前置环境检查未通过,缺少必要依赖");
|
|
126
126
|
}
|
|
127
|
-
log$
|
|
127
|
+
log$41.info("前置环境检查通过 ✓");
|
|
128
128
|
return {
|
|
129
129
|
claude,
|
|
130
130
|
codex,
|
|
@@ -149,7 +149,7 @@ function preflight() {
|
|
|
149
149
|
* 同时生成新版 (~/.lark-cli/config.json) 和旧版 (~/.config/lark/config.json)
|
|
150
150
|
* 格式的配置文件,保证 lark-cli 无论通过哪种路径读取都能正常工作。
|
|
151
151
|
*/
|
|
152
|
-
const log$
|
|
152
|
+
const log$40 = larkLogger("config/ensure-lark-cli");
|
|
153
153
|
/** 新版 lark-cli 配置路径 */
|
|
154
154
|
const NEW_CONFIG_DIR = join(homedir(), ".lark-cli");
|
|
155
155
|
const NEW_CONFIG_PATH = join(NEW_CONFIG_DIR, "config.json");
|
|
@@ -166,19 +166,19 @@ function ensureLarkCliConfig() {
|
|
|
166
166
|
const appId = process.env.LARK_APP_ID;
|
|
167
167
|
const appSecret = process.env.LARK_APP_SECRET;
|
|
168
168
|
if (!appId || !appSecret) {
|
|
169
|
-
log$
|
|
169
|
+
log$40.info("环境变量凭证未设置,跳过 lark-cli 配置同步");
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
const hasNewConfig = existsSync(NEW_CONFIG_PATH);
|
|
173
173
|
const hasLegacyConfig = existsSync(LEGACY_CONFIG_PATH);
|
|
174
174
|
if (hasNewConfig && hasLegacyConfig) {
|
|
175
|
-
log$
|
|
175
|
+
log$40.info("lark-cli 配置文件已存在,跳过同步", {
|
|
176
176
|
newConfig: NEW_CONFIG_PATH,
|
|
177
177
|
legacyConfig: LEGACY_CONFIG_PATH
|
|
178
178
|
});
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
|
-
log$
|
|
181
|
+
log$40.info("检测到环境变量凭证但 lark-cli 配置文件缺失,开始同步", {
|
|
182
182
|
appId,
|
|
183
183
|
hasNewConfig,
|
|
184
184
|
hasLegacyConfig
|
|
@@ -197,9 +197,9 @@ function ensureLarkCliConfig() {
|
|
|
197
197
|
encoding: "utf-8",
|
|
198
198
|
mode: 384
|
|
199
199
|
});
|
|
200
|
-
log$
|
|
200
|
+
log$40.info("已生成新版 lark-cli 配置文件", { path: NEW_CONFIG_PATH });
|
|
201
201
|
} catch (err) {
|
|
202
|
-
log$
|
|
202
|
+
log$40.warn("生成新版 lark-cli 配置文件失败", {
|
|
203
203
|
path: NEW_CONFIG_PATH,
|
|
204
204
|
error: err instanceof Error ? err.message : String(err)
|
|
205
205
|
});
|
|
@@ -218,9 +218,9 @@ function ensureLarkCliConfig() {
|
|
|
218
218
|
encoding: "utf-8",
|
|
219
219
|
mode: 384
|
|
220
220
|
});
|
|
221
|
-
log$
|
|
221
|
+
log$40.info("已生成旧版 lark-cli 配置文件", { path: LEGACY_CONFIG_PATH });
|
|
222
222
|
} catch (err) {
|
|
223
|
-
log$
|
|
223
|
+
log$40.warn("生成旧版 lark-cli 配置文件失败", {
|
|
224
224
|
path: LEGACY_CONFIG_PATH,
|
|
225
225
|
error: err instanceof Error ? err.message : String(err)
|
|
226
226
|
});
|
|
@@ -251,72 +251,98 @@ function getWorkspaceChatHistoryRoot(workspaceRoot = resolveWorkspaceRoot()) {
|
|
|
251
251
|
* 以及确保会话工作目录结构完整。
|
|
252
252
|
*/
|
|
253
253
|
const logger$10 = larkLogger("config/defaults");
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
254
|
+
function createDefaultSettings() {
|
|
255
|
+
const hookBaseUrl = getClaudeHookBaseUrl();
|
|
256
|
+
return {
|
|
257
|
+
permissions: {
|
|
258
|
+
allow: [
|
|
259
|
+
"Bash(*)",
|
|
260
|
+
"Read(*)",
|
|
261
|
+
"Write(*)",
|
|
262
|
+
"Edit(*)",
|
|
263
|
+
"MultiEdit(*)",
|
|
264
|
+
"Glob(*)",
|
|
265
|
+
"Grep(*)",
|
|
266
|
+
"LS(*)",
|
|
267
|
+
"TodoWrite(*)",
|
|
268
|
+
"WebFetch(*)",
|
|
269
|
+
"WebSearch(*)"
|
|
270
|
+
],
|
|
271
|
+
deny: [
|
|
272
|
+
"Write(//.claude/CLAUDE.md)",
|
|
273
|
+
"Edit(//.claude/CLAUDE.md)",
|
|
274
|
+
"Write(//.claude/settings.json)",
|
|
275
|
+
"Edit(//.claude/settings.json)",
|
|
276
|
+
"Read(//.lark-cli/**)",
|
|
277
|
+
"Read(//.config/lark/**)",
|
|
278
|
+
"Read(//.larkpal/credentials.json)",
|
|
279
|
+
"Read(//.env)",
|
|
280
|
+
"Read(//.env.*)",
|
|
281
|
+
"Bash(cat ~/.lark-cli:*)",
|
|
282
|
+
"Bash(cat ~/.config/lark:*)",
|
|
283
|
+
"Bash(cat ~/.larkpal/credentials:*)",
|
|
284
|
+
"Bash(head ~/.lark-cli:*)",
|
|
285
|
+
"Bash(head ~/.config/lark:*)",
|
|
286
|
+
"Bash(tail ~/.lark-cli:*)",
|
|
287
|
+
"Bash(tail ~/.config/lark:*)",
|
|
288
|
+
"Bash(less ~/.lark-cli:*)",
|
|
289
|
+
"Bash(less ~/.config/lark:*)",
|
|
290
|
+
"Bash(more ~/.lark-cli:*)",
|
|
291
|
+
"Bash(more ~/.config/lark:*)",
|
|
292
|
+
"Bash(env:*)",
|
|
293
|
+
"Bash(printenv:*)",
|
|
294
|
+
"Bash(export -p:*)",
|
|
295
|
+
"Bash(echo $LARK_APP_SECRET:*)",
|
|
296
|
+
"Bash(echo $ANTHROPIC_API_KEY:*)"
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
hooks: {
|
|
300
|
+
SessionStart: [{ hooks: [{
|
|
301
|
+
type: "http",
|
|
302
|
+
url: `${hookBaseUrl}/hooks/session-start`,
|
|
303
|
+
timeout: 5
|
|
304
|
+
}] }],
|
|
305
|
+
Stop: [{ hooks: [{
|
|
306
|
+
type: "http",
|
|
307
|
+
url: `${hookBaseUrl}/hooks/stop`,
|
|
308
|
+
timeout: 5
|
|
309
|
+
}] }],
|
|
310
|
+
PreToolUse: [{
|
|
311
|
+
matcher: "Read",
|
|
312
|
+
hooks: [{
|
|
313
|
+
type: "http",
|
|
314
|
+
url: `${hookBaseUrl}/hooks/pre-tool-use`,
|
|
315
|
+
timeout: 5
|
|
316
|
+
}]
|
|
317
|
+
}],
|
|
318
|
+
PostToolUse: [{
|
|
319
|
+
matcher: "Read",
|
|
320
|
+
hooks: [{
|
|
321
|
+
type: "http",
|
|
322
|
+
url: `${hookBaseUrl}/hooks/post-tool-use`,
|
|
323
|
+
timeout: 5
|
|
324
|
+
}]
|
|
325
|
+
}],
|
|
326
|
+
SessionEnd: [{ hooks: [{
|
|
327
|
+
type: "http",
|
|
328
|
+
url: `${hookBaseUrl}/hooks/session-end`,
|
|
329
|
+
timeout: 5
|
|
330
|
+
}] }],
|
|
331
|
+
Notification: [{ hooks: [{
|
|
332
|
+
type: "http",
|
|
333
|
+
url: `${hookBaseUrl}/hooks/notification`,
|
|
334
|
+
timeout: 5
|
|
335
|
+
}] }]
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function getClaudeHookBaseUrl() {
|
|
340
|
+
const explicit = process.env.LARKPAL_CC_HOOK_BASE_URL?.trim();
|
|
341
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
342
|
+
const port = process.env.LARKPAL_GATEWAY_PORT || "3000";
|
|
343
|
+
const rawHost = process.env.LARKPAL_GATEWAY_HOST || "localhost";
|
|
344
|
+
return `http://${rawHost === "0.0.0.0" || rawHost === "::" ? "127.0.0.1" : rawHost}:${port}`;
|
|
345
|
+
}
|
|
320
346
|
const DEFAULT_CLAUDE_MD = `# LarkPal
|
|
321
347
|
|
|
322
348
|
你是一个运行在飞书中的 AI 协作伙伴。这是你的初始状态。
|
|
@@ -414,7 +440,7 @@ async function ensureDefaults() {
|
|
|
414
440
|
const claudeMdPath = join(claudeDir, "CLAUDE.md");
|
|
415
441
|
await mkdir(claudeDir, { recursive: true });
|
|
416
442
|
logger$10.info("确保 ~/.claude 目录存在", { path: claudeDir });
|
|
417
|
-
await writeFile(settingsPath, JSON.stringify(
|
|
443
|
+
await writeFile(settingsPath, JSON.stringify(createDefaultSettings(), null, 2), "utf-8");
|
|
418
444
|
logger$10.info("settings.json 已同步(强制覆盖)", { path: settingsPath });
|
|
419
445
|
if (await fileExists(claudeMdPath)) logger$10.info("CLAUDE.md 已存在,跳过生成", { path: claudeMdPath });
|
|
420
446
|
else {
|
|
@@ -441,7 +467,7 @@ async function ensureDefaults() {
|
|
|
441
467
|
* - github: { type: "api_key", token }
|
|
442
468
|
* - custom-api: { type: "http_bearer", base_url, token }
|
|
443
469
|
*/
|
|
444
|
-
const log$
|
|
470
|
+
const log$39 = larkLogger("user/credential-vault");
|
|
445
471
|
var CredentialVault = class {
|
|
446
472
|
credentialPath;
|
|
447
473
|
encryptionKey;
|
|
@@ -453,12 +479,12 @@ var CredentialVault = class {
|
|
|
453
479
|
if (keyEnv) {
|
|
454
480
|
this.encryptionKey = keyEnv.length === 64 ? Buffer.from(keyEnv, "hex") : Buffer.from(keyEnv, "base64");
|
|
455
481
|
if (this.encryptionKey.length !== 32) {
|
|
456
|
-
log$
|
|
482
|
+
log$39.error("LARKPAL_ENCRYPTION_KEY 长度不正确,需要 32 字节", { actualLength: this.encryptionKey.length });
|
|
457
483
|
this.encryptionKey = null;
|
|
458
|
-
} else log$
|
|
484
|
+
} else log$39.info("凭证加密已启用");
|
|
459
485
|
} else {
|
|
460
486
|
this.encryptionKey = null;
|
|
461
|
-
log$
|
|
487
|
+
log$39.warn("LARKPAL_ENCRYPTION_KEY 未设置,凭证将以明文存储(仅限开发模式)");
|
|
462
488
|
}
|
|
463
489
|
}
|
|
464
490
|
/**
|
|
@@ -466,7 +492,7 @@ var CredentialVault = class {
|
|
|
466
492
|
*/
|
|
467
493
|
async load() {
|
|
468
494
|
if (!existsSync$1(this.credentialPath)) {
|
|
469
|
-
log$
|
|
495
|
+
log$39.info("凭证文件不存在,使用空凭证", { path: this.credentialPath });
|
|
470
496
|
this.store = {};
|
|
471
497
|
this.loaded = true;
|
|
472
498
|
return;
|
|
@@ -475,12 +501,12 @@ var CredentialVault = class {
|
|
|
475
501
|
const raw = await readFile$1(this.credentialPath, "utf-8");
|
|
476
502
|
this.store = JSON.parse(raw);
|
|
477
503
|
this.loaded = true;
|
|
478
|
-
log$
|
|
504
|
+
log$39.info("凭证文件加载完成", {
|
|
479
505
|
path: this.credentialPath,
|
|
480
506
|
credentialCount: Object.keys(this.store).length
|
|
481
507
|
});
|
|
482
508
|
} catch (err) {
|
|
483
|
-
log$
|
|
509
|
+
log$39.error("凭证文件加载失败", {
|
|
484
510
|
path: this.credentialPath,
|
|
485
511
|
error: err instanceof Error ? err.message : String(err)
|
|
486
512
|
});
|
|
@@ -494,12 +520,12 @@ var CredentialVault = class {
|
|
|
494
520
|
async save() {
|
|
495
521
|
try {
|
|
496
522
|
await writeFile$1(this.credentialPath, JSON.stringify(this.store, null, 2), "utf-8");
|
|
497
|
-
log$
|
|
523
|
+
log$39.info("凭证文件保存完成", {
|
|
498
524
|
path: this.credentialPath,
|
|
499
525
|
credentialCount: Object.keys(this.store).length
|
|
500
526
|
});
|
|
501
527
|
} catch (err) {
|
|
502
|
-
log$
|
|
528
|
+
log$39.error("凭证文件保存失败", {
|
|
503
529
|
path: this.credentialPath,
|
|
504
530
|
error: err instanceof Error ? err.message : String(err)
|
|
505
531
|
});
|
|
@@ -521,7 +547,7 @@ var CredentialVault = class {
|
|
|
521
547
|
*/
|
|
522
548
|
async setCredential(name, credential) {
|
|
523
549
|
if (!this.loaded) await this.load();
|
|
524
|
-
log$
|
|
550
|
+
log$39.info("设置凭证", {
|
|
525
551
|
name,
|
|
526
552
|
fields: Object.keys(credential)
|
|
527
553
|
});
|
|
@@ -537,7 +563,7 @@ var CredentialVault = class {
|
|
|
537
563
|
if (!(name in this.store)) return false;
|
|
538
564
|
delete this.store[name];
|
|
539
565
|
await this.save();
|
|
540
|
-
log$
|
|
566
|
+
log$39.info("凭证已删除", { name });
|
|
541
567
|
return true;
|
|
542
568
|
}
|
|
543
569
|
/**
|
|
@@ -566,7 +592,7 @@ var CredentialVault = class {
|
|
|
566
592
|
envVars[envKey] = String(fieldValue);
|
|
567
593
|
}
|
|
568
594
|
} catch (err) {
|
|
569
|
-
log$
|
|
595
|
+
log$39.warn("凭证解密失败,跳过", {
|
|
570
596
|
name,
|
|
571
597
|
error: err instanceof Error ? err.message : String(err)
|
|
572
598
|
});
|
|
@@ -623,7 +649,7 @@ var CredentialVault = class {
|
|
|
623
649
|
* - B 用户的 CC 进程连接 B 的数据库 MCP Server
|
|
624
650
|
* - 全局工具(lark-cli、文件操作等)所有用户共享
|
|
625
651
|
*/
|
|
626
|
-
const log$
|
|
652
|
+
const log$38 = larkLogger("user/mcp-merge");
|
|
627
653
|
/** 全局 MCP 配置路径 */
|
|
628
654
|
const GLOBAL_MCP_CONFIG_PATH = path$1.join(os.homedir(), ".claude", "mcp-servers.json");
|
|
629
655
|
/**
|
|
@@ -679,9 +705,9 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
679
705
|
if (existsSync$1(GLOBAL_MCP_CONFIG_PATH)) try {
|
|
680
706
|
const raw = await readFile$1(GLOBAL_MCP_CONFIG_PATH, "utf-8");
|
|
681
707
|
globalConfig = JSON.parse(raw);
|
|
682
|
-
log$
|
|
708
|
+
log$38.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
|
|
683
709
|
} catch (err) {
|
|
684
|
-
log$
|
|
710
|
+
log$38.warn("全局 MCP 配置文件解析失败", {
|
|
685
711
|
path: GLOBAL_MCP_CONFIG_PATH,
|
|
686
712
|
error: err instanceof Error ? err.message : String(err)
|
|
687
713
|
});
|
|
@@ -692,13 +718,13 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
692
718
|
if (existsSync$1(userMcpPath)) try {
|
|
693
719
|
const raw = await readFile$1(userMcpPath, "utf-8");
|
|
694
720
|
userConfig = JSON.parse(raw);
|
|
695
|
-
log$
|
|
721
|
+
log$38.info("用户 MCP 配置加载完成", {
|
|
696
722
|
userId: userCtx.userId,
|
|
697
723
|
serverCount: Object.keys(userConfig).length,
|
|
698
724
|
servers: Object.keys(userConfig)
|
|
699
725
|
});
|
|
700
726
|
} catch (err) {
|
|
701
|
-
log$
|
|
727
|
+
log$38.warn("用户 MCP 配置文件解析失败", {
|
|
702
728
|
userId: userCtx?.userId,
|
|
703
729
|
path: userMcpPath,
|
|
704
730
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -710,7 +736,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
710
736
|
...globalConfig,
|
|
711
737
|
...userConfig
|
|
712
738
|
};
|
|
713
|
-
log$
|
|
739
|
+
log$38.info("MCP 配置合并完成", {
|
|
714
740
|
userId: userCtx?.userId,
|
|
715
741
|
builtinServers: Object.keys(builtinConfig),
|
|
716
742
|
globalServers: Object.keys(globalConfig),
|
|
@@ -719,7 +745,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
719
745
|
});
|
|
720
746
|
const tmpPath = path$1.join(os.tmpdir(), `mcp-${sessionId}.json`);
|
|
721
747
|
await writeFile$1(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
|
|
722
|
-
log$
|
|
748
|
+
log$38.info("MCP 合并配置已写入临时文件", {
|
|
723
749
|
path: tmpPath,
|
|
724
750
|
serverCount: Object.keys(merged).length
|
|
725
751
|
});
|
|
@@ -733,7 +759,7 @@ async function cleanupMcpConfig(sessionId) {
|
|
|
733
759
|
try {
|
|
734
760
|
if (existsSync$1(tmpPath)) {
|
|
735
761
|
await unlink$1(tmpPath);
|
|
736
|
-
log$
|
|
762
|
+
log$38.debug("MCP 临时配置文件已清理", { path: tmpPath });
|
|
737
763
|
}
|
|
738
764
|
} catch {}
|
|
739
765
|
}
|
|
@@ -781,7 +807,7 @@ function getToolDisplayName(toolName) {
|
|
|
781
807
|
* - result(data) — 最终结果
|
|
782
808
|
* - parseError(error, rawLine) — 解析错误(不中断流)
|
|
783
809
|
*/
|
|
784
|
-
const log$
|
|
810
|
+
const log$37 = larkLogger("cc-runtime/stream-parser");
|
|
785
811
|
const MAX_TOOL_RESULT_CONTENT_CHARS = 8e3;
|
|
786
812
|
var CCStreamParser = class extends EventEmitter {
|
|
787
813
|
toolNamesById = /* @__PURE__ */ new Map();
|
|
@@ -798,7 +824,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
798
824
|
try {
|
|
799
825
|
msg = JSON.parse(trimmed);
|
|
800
826
|
} catch (err) {
|
|
801
|
-
log$
|
|
827
|
+
log$37.warn("NDJSON 解析失败,跳过该行", {
|
|
802
828
|
error: String(err),
|
|
803
829
|
rawLine: trimmed.slice(0, 200)
|
|
804
830
|
});
|
|
@@ -806,7 +832,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
806
832
|
return;
|
|
807
833
|
}
|
|
808
834
|
if (msg.parent_tool_use_id != null && msg.type !== "result") {
|
|
809
|
-
log$
|
|
835
|
+
log$37.debug("跳过 subagent 内部事件", {
|
|
810
836
|
type: msg.type,
|
|
811
837
|
parentToolUseId: msg.parent_tool_use_id
|
|
812
838
|
});
|
|
@@ -829,7 +855,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
829
855
|
this.handleResult(msg);
|
|
830
856
|
break;
|
|
831
857
|
case "system":
|
|
832
|
-
log$
|
|
858
|
+
log$37.info("收到 system 消息", {
|
|
833
859
|
subtype: msg.subtype,
|
|
834
860
|
detail: JSON.stringify(msg).slice(0, 500)
|
|
835
861
|
});
|
|
@@ -837,7 +863,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
837
863
|
this.emit("system", msg);
|
|
838
864
|
break;
|
|
839
865
|
default:
|
|
840
|
-
log$
|
|
866
|
+
log$37.debug("收到未知消息类型,已忽略", { type: msg.type });
|
|
841
867
|
break;
|
|
842
868
|
}
|
|
843
869
|
}
|
|
@@ -856,14 +882,14 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
856
882
|
switch (event.type) {
|
|
857
883
|
case "content_block_start": {
|
|
858
884
|
const block = event.content_block;
|
|
859
|
-
log$
|
|
885
|
+
log$37.debug("content_block_start 事件", {
|
|
860
886
|
blockType: block?.type,
|
|
861
887
|
toolName: block?.name,
|
|
862
888
|
toolUseId: block?.id
|
|
863
889
|
});
|
|
864
890
|
if (block?.type === "tool_use" && block.name) {
|
|
865
|
-
this.recordToolUse(block.name, block.id);
|
|
866
|
-
log$
|
|
891
|
+
if (!this.recordToolUse(block.name, block.id, block.input)) break;
|
|
892
|
+
log$37.info("工具调用开始", {
|
|
867
893
|
toolName: block.name,
|
|
868
894
|
displayName: getToolDisplayName(block.name),
|
|
869
895
|
toolUseId: block.id
|
|
@@ -882,7 +908,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
882
908
|
case "message_delta": {
|
|
883
909
|
const stopReason = event.delta?.stop_reason;
|
|
884
910
|
if (stopReason) {
|
|
885
|
-
log$
|
|
911
|
+
log$37.info("轮次结束", { stopReason });
|
|
886
912
|
this.emit("turnEnd", stopReason);
|
|
887
913
|
}
|
|
888
914
|
break;
|
|
@@ -902,7 +928,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
902
928
|
const content = msg.message?.content;
|
|
903
929
|
const stopReason = msg.message?.stop_reason;
|
|
904
930
|
const blockTypes = Array.isArray(content) ? content.map((b) => `${b.type}${b.name ? ":" + b.name : ""}`).join(", ") : "none";
|
|
905
|
-
log$
|
|
931
|
+
log$37.info("收到完整 assistant 消息", {
|
|
906
932
|
model: msg.message?.model,
|
|
907
933
|
stopReason,
|
|
908
934
|
contentBlocks: content?.length,
|
|
@@ -910,8 +936,8 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
910
936
|
});
|
|
911
937
|
if (Array.isArray(content)) {
|
|
912
938
|
for (const block of content) if (block.type === "tool_use" && block.name) {
|
|
913
|
-
this.recordToolUse(block.name, block.id);
|
|
914
|
-
log$
|
|
939
|
+
if (!this.recordToolUse(block.name, block.id, block.input)) continue;
|
|
940
|
+
log$37.info("从 assistant 消息提取工具调用", {
|
|
915
941
|
toolName: block.name,
|
|
916
942
|
displayName: getToolDisplayName(block.name),
|
|
917
943
|
toolUseId: block.id
|
|
@@ -931,7 +957,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
931
957
|
if (!Array.isArray(content)) return;
|
|
932
958
|
for (const item of content) if (item.type === "tool_result" && item.tool_use_id) {
|
|
933
959
|
const result = this.buildToolResultSummary(item, msg.toolUseResult);
|
|
934
|
-
log$
|
|
960
|
+
log$37.debug("工具结果返回", {
|
|
935
961
|
toolUseId: item.tool_use_id,
|
|
936
962
|
toolName: result.toolName,
|
|
937
963
|
isError: result.isError,
|
|
@@ -944,15 +970,19 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
944
970
|
if (msg.subtype !== "init" || !Array.isArray(msg.tools)) return;
|
|
945
971
|
this.declaredTools = new Set(msg.tools);
|
|
946
972
|
}
|
|
947
|
-
recordToolUse(toolName, toolUseId) {
|
|
973
|
+
recordToolUse(toolName, toolUseId, toolInput) {
|
|
948
974
|
if (toolUseId) this.toolNamesById.set(toolUseId, toolName);
|
|
949
975
|
if (this.declaredTools && !this.declaredTools.has(toolName)) {
|
|
950
|
-
|
|
976
|
+
const availableTools = [...this.declaredTools];
|
|
977
|
+
log$37.warn("模型请求了未声明工具,已抑制正常 toolUseStart", {
|
|
951
978
|
toolName,
|
|
952
|
-
toolUseId
|
|
979
|
+
toolUseId,
|
|
980
|
+
availableTools
|
|
953
981
|
});
|
|
954
|
-
this.emit("unknownToolUse", toolName, toolUseId);
|
|
982
|
+
this.emit("unknownToolUse", toolName, toolUseId, toolInput, availableTools);
|
|
983
|
+
return false;
|
|
955
984
|
}
|
|
985
|
+
return true;
|
|
956
986
|
}
|
|
957
987
|
buildToolResultSummary(item, toolUseResult) {
|
|
958
988
|
const serializedContent = serializeToolResultContent(item.content);
|
|
@@ -974,7 +1004,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
974
1004
|
* 处理 tool_progress 消息 — 工具执行进度
|
|
975
1005
|
*/
|
|
976
1006
|
handleToolProgress(msg) {
|
|
977
|
-
log$
|
|
1007
|
+
log$37.debug("工具执行进度", {
|
|
978
1008
|
toolName: msg.tool_name,
|
|
979
1009
|
elapsed: msg.elapsed_time_seconds
|
|
980
1010
|
});
|
|
@@ -984,7 +1014,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
984
1014
|
* 处理 result 消息 — CC 执行完成的最终结果
|
|
985
1015
|
*/
|
|
986
1016
|
handleResult(msg) {
|
|
987
|
-
log$
|
|
1017
|
+
log$37.info("CC 执行完成", {
|
|
988
1018
|
subtype: msg.subtype,
|
|
989
1019
|
isError: msg.is_error,
|
|
990
1020
|
durationMs: msg.duration_ms,
|
|
@@ -1048,6 +1078,223 @@ function tryParseJson$1(value) {
|
|
|
1048
1078
|
}
|
|
1049
1079
|
}
|
|
1050
1080
|
//#endregion
|
|
1081
|
+
//#region src/cc-runtime/transcript-sanitizer.ts
|
|
1082
|
+
const log$36 = larkLogger("cc-runtime/transcript-sanitizer");
|
|
1083
|
+
const ARTIFACT_DIR = "larkpal-artifacts";
|
|
1084
|
+
const MAX_READ_BYTES = readPositiveIntEnv$1("LARKPAL_TRANSCRIPT_SANITIZER_MAX_BYTES", 50 * 1024 * 1024);
|
|
1085
|
+
async function sanitizeClaudeTranscriptFile(filePath) {
|
|
1086
|
+
const result = {
|
|
1087
|
+
changed: false,
|
|
1088
|
+
lines: 0,
|
|
1089
|
+
sanitizedImageBlocks: 0,
|
|
1090
|
+
artifacts: 0
|
|
1091
|
+
};
|
|
1092
|
+
const fileStat = await stat(filePath);
|
|
1093
|
+
if (fileStat.size > MAX_READ_BYTES) {
|
|
1094
|
+
log$36.warn("跳过过大的 Claude transcript sanitizer", {
|
|
1095
|
+
filePath,
|
|
1096
|
+
size: fileStat.size
|
|
1097
|
+
});
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
const raw = await readFile(filePath, "utf-8");
|
|
1101
|
+
const hasTrailingNewline = raw.endsWith("\n");
|
|
1102
|
+
const lines = raw.split("\n");
|
|
1103
|
+
if (hasTrailingNewline) lines.pop();
|
|
1104
|
+
result.lines = lines.length;
|
|
1105
|
+
const out = [];
|
|
1106
|
+
const artifactRoot = join(dirname(filePath), ARTIFACT_DIR);
|
|
1107
|
+
for (const line of lines) {
|
|
1108
|
+
if (!line.trim()) {
|
|
1109
|
+
out.push(line);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (!mightContainImagePayload(line)) {
|
|
1113
|
+
out.push(line);
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
let entry;
|
|
1117
|
+
try {
|
|
1118
|
+
entry = JSON.parse(line);
|
|
1119
|
+
} catch {
|
|
1120
|
+
out.push(line);
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const lineStats = await sanitizeTranscriptEntry(entry, artifactRoot);
|
|
1124
|
+
if (lineStats.sanitizedImageBlocks > 0) {
|
|
1125
|
+
result.changed = true;
|
|
1126
|
+
result.sanitizedImageBlocks += lineStats.sanitizedImageBlocks;
|
|
1127
|
+
result.artifacts += lineStats.artifacts;
|
|
1128
|
+
out.push(JSON.stringify(entry));
|
|
1129
|
+
} else out.push(line);
|
|
1130
|
+
}
|
|
1131
|
+
if (!result.changed) return result;
|
|
1132
|
+
const tempPath = `${filePath}.larkpal-sanitized-${process.pid}-${Date.now()}`;
|
|
1133
|
+
await writeFile(tempPath, out.join("\n") + (hasTrailingNewline ? "\n" : ""), "utf-8");
|
|
1134
|
+
await rename(tempPath, filePath);
|
|
1135
|
+
log$36.info("Claude transcript 图片结果已清理", {
|
|
1136
|
+
filePath,
|
|
1137
|
+
...result
|
|
1138
|
+
});
|
|
1139
|
+
return result;
|
|
1140
|
+
}
|
|
1141
|
+
function isTranscriptSanitizerEnabled() {
|
|
1142
|
+
const value = process.env.LARKPAL_TRANSCRIPT_SANITIZER;
|
|
1143
|
+
return value !== "0" && value !== "false";
|
|
1144
|
+
}
|
|
1145
|
+
async function sanitizeTranscriptEntry(entry, artifactRoot) {
|
|
1146
|
+
let sanitizedImageBlocks = 0;
|
|
1147
|
+
let artifacts = 0;
|
|
1148
|
+
const message = asObject$1(entry.message);
|
|
1149
|
+
const content = message ? message.content : void 0;
|
|
1150
|
+
const toolUseResult = asObject$1(entry.toolUseResult);
|
|
1151
|
+
let firstArtifactUri;
|
|
1152
|
+
if (Array.isArray(content)) for (let i = 0; i < content.length; i++) {
|
|
1153
|
+
const block = asObject$1(content[i]);
|
|
1154
|
+
if (!block) continue;
|
|
1155
|
+
if (block.type === "tool_result") {
|
|
1156
|
+
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : void 0;
|
|
1157
|
+
const replacement = await sanitizeToolResultContent(block.content, artifactRoot, toolUseId);
|
|
1158
|
+
if (!replacement) continue;
|
|
1159
|
+
block.content = replacement.content;
|
|
1160
|
+
sanitizedImageBlocks += replacement.imageBlocks;
|
|
1161
|
+
artifacts += replacement.artifacts;
|
|
1162
|
+
firstArtifactUri ??= replacement.metadata[0]?.artifactUri;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (isBase64ImageBlock(block)) {
|
|
1166
|
+
const artifact = await writeImageArtifact(artifactRoot, block, void 0, i);
|
|
1167
|
+
content[i] = {
|
|
1168
|
+
type: "text",
|
|
1169
|
+
text: buildReplacementText([artifact])
|
|
1170
|
+
};
|
|
1171
|
+
sanitizedImageBlocks++;
|
|
1172
|
+
artifacts++;
|
|
1173
|
+
firstArtifactUri ??= artifact.artifactUri;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const toolUseResultReplacement = await sanitizeToolUseResult(toolUseResult, artifactRoot, firstArtifactUri);
|
|
1177
|
+
if (toolUseResultReplacement) {
|
|
1178
|
+
sanitizedImageBlocks += toolUseResultReplacement.sanitizedImageBlocks;
|
|
1179
|
+
artifacts += toolUseResultReplacement.artifacts;
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
sanitizedImageBlocks,
|
|
1183
|
+
artifacts
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
async function sanitizeToolUseResult(toolUseResult, artifactRoot, existingArtifactUri) {
|
|
1187
|
+
if (toolUseResult?.type !== "image") return null;
|
|
1188
|
+
const file = asObject$1(toolUseResult.file);
|
|
1189
|
+
if (!file || typeof file.base64 !== "string") return null;
|
|
1190
|
+
let artifactUri = existingArtifactUri;
|
|
1191
|
+
let artifacts = 0;
|
|
1192
|
+
if (!artifactUri) {
|
|
1193
|
+
artifactUri = (await writeImageArtifact(artifactRoot, {
|
|
1194
|
+
type: "image",
|
|
1195
|
+
source: {
|
|
1196
|
+
type: "base64",
|
|
1197
|
+
media_type: typeof file.type === "string" ? file.type : void 0,
|
|
1198
|
+
data: file.base64
|
|
1199
|
+
},
|
|
1200
|
+
filePath: typeof file.filePath === "string" ? file.filePath : void 0
|
|
1201
|
+
}, void 0, 0)).artifactUri;
|
|
1202
|
+
artifacts = 1;
|
|
1203
|
+
}
|
|
1204
|
+
file.artifactUri = artifactUri;
|
|
1205
|
+
file.base64 = "[redacted by LarkPal: image artifact externalized]";
|
|
1206
|
+
return {
|
|
1207
|
+
sanitizedImageBlocks: 1,
|
|
1208
|
+
artifacts
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
async function sanitizeToolResultContent(content, artifactRoot, toolUseId) {
|
|
1212
|
+
if (!Array.isArray(content)) return null;
|
|
1213
|
+
const metadata = [];
|
|
1214
|
+
let imageBlocks = 0;
|
|
1215
|
+
let artifacts = 0;
|
|
1216
|
+
for (let i = 0; i < content.length; i++) {
|
|
1217
|
+
const block = asObject$1(content[i]);
|
|
1218
|
+
if (!isBase64ImageBlock(block)) continue;
|
|
1219
|
+
imageBlocks++;
|
|
1220
|
+
const artifact = await writeImageArtifact(artifactRoot, block, toolUseId, i);
|
|
1221
|
+
metadata.push(artifact);
|
|
1222
|
+
artifacts++;
|
|
1223
|
+
}
|
|
1224
|
+
if (imageBlocks === 0) return null;
|
|
1225
|
+
return {
|
|
1226
|
+
content: buildReplacementText(metadata),
|
|
1227
|
+
imageBlocks,
|
|
1228
|
+
artifacts,
|
|
1229
|
+
metadata
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async function writeImageArtifact(artifactRoot, block, toolUseId, index) {
|
|
1233
|
+
const source = asObject$1(block.source);
|
|
1234
|
+
const data = typeof source?.data === "string" ? source.data : "";
|
|
1235
|
+
const mimeType = typeof source?.media_type === "string" ? source.media_type : void 0;
|
|
1236
|
+
const artifactId = `${toolUseId ?? randomUUID()}-${index}`;
|
|
1237
|
+
const artifactPath = join(artifactRoot, `${artifactId}.json`);
|
|
1238
|
+
const artifactUri = `artifact://claude-transcript/${artifactId}`;
|
|
1239
|
+
const dimensions = formatDimensions(block);
|
|
1240
|
+
await mkdir(artifactRoot, { recursive: true });
|
|
1241
|
+
await writeFile(artifactPath, JSON.stringify({
|
|
1242
|
+
artifactUri,
|
|
1243
|
+
originalToolName: "Read",
|
|
1244
|
+
toolUseId,
|
|
1245
|
+
mimeType,
|
|
1246
|
+
dimensions,
|
|
1247
|
+
originalSize: data.length,
|
|
1248
|
+
sourcePath: typeof block.filePath === "string" ? block.filePath : void 0,
|
|
1249
|
+
base64: data
|
|
1250
|
+
}, null, 2), {
|
|
1251
|
+
encoding: "utf-8",
|
|
1252
|
+
flag: "w"
|
|
1253
|
+
});
|
|
1254
|
+
return {
|
|
1255
|
+
artifactUri,
|
|
1256
|
+
originalToolName: "Read",
|
|
1257
|
+
toolUseId,
|
|
1258
|
+
mimeType,
|
|
1259
|
+
dimensions,
|
|
1260
|
+
originalSize: data.length,
|
|
1261
|
+
sourcePath: typeof block.filePath === "string" ? block.filePath : void 0,
|
|
1262
|
+
summary: "Rendered image/PDF page preview externalized by LarkPal transcript sanitizer."
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function buildReplacementText(metadata) {
|
|
1266
|
+
return JSON.stringify({
|
|
1267
|
+
larkpalSanitized: true,
|
|
1268
|
+
reason: "Previous image tool result was externalized to avoid replaying base64 image payloads into provider requests.",
|
|
1269
|
+
artifacts: metadata
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
function isBase64ImageBlock(value) {
|
|
1273
|
+
if (!value || value.type !== "image") return false;
|
|
1274
|
+
const source = asObject$1(value.source);
|
|
1275
|
+
return source?.type === "base64" && typeof source.data === "string";
|
|
1276
|
+
}
|
|
1277
|
+
function formatDimensions(block) {
|
|
1278
|
+
const dimensions = asObject$1(block.dimensions);
|
|
1279
|
+
if (!dimensions) return void 0;
|
|
1280
|
+
const width = dimensions.displayWidth ?? dimensions.originalWidth;
|
|
1281
|
+
const height = dimensions.displayHeight ?? dimensions.originalHeight;
|
|
1282
|
+
if (typeof width !== "number" || typeof height !== "number") return void 0;
|
|
1283
|
+
return `${width}x${height}`;
|
|
1284
|
+
}
|
|
1285
|
+
function mightContainImagePayload(line) {
|
|
1286
|
+
return line.includes("\"base64\"") && (line.includes("\"image\"") || line.includes("\"source\""));
|
|
1287
|
+
}
|
|
1288
|
+
function asObject$1(value) {
|
|
1289
|
+
return typeof value === "object" && value != null ? value : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
function readPositiveIntEnv$1(name, fallback) {
|
|
1292
|
+
const raw = process.env[name];
|
|
1293
|
+
if (!raw) return fallback;
|
|
1294
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1295
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1051
1298
|
//#region src/cc-runtime/process-manager.ts
|
|
1052
1299
|
/**
|
|
1053
1300
|
* CC 进程管理器 — 按 session_id 管理 Claude Code 常驻进程的生命周期
|
|
@@ -1268,7 +1515,16 @@ var SessionProcessManager = class {
|
|
|
1268
1515
|
const { sessionId, cwd, prompt } = config;
|
|
1269
1516
|
const isEphemeral = config.transcriptMode === "ephemeral";
|
|
1270
1517
|
const claudeSessionId = isEphemeral ? v4() : v5(sessionId, LARKPAL_SESSION_NAMESPACE);
|
|
1271
|
-
const
|
|
1518
|
+
const transcriptPath = this.getCCSessionFilePath(cwd, claudeSessionId);
|
|
1519
|
+
const transcriptExists = existsSync(transcriptPath);
|
|
1520
|
+
const isResuming = !isEphemeral && (opts?._forceResume || transcriptExists);
|
|
1521
|
+
if (isResuming && transcriptExists && isTranscriptSanitizerEnabled()) await sanitizeClaudeTranscriptFile(transcriptPath).catch((err) => {
|
|
1522
|
+
log$35.warn("Claude transcript sanitizer 失败,继续启动进程", {
|
|
1523
|
+
sessionId,
|
|
1524
|
+
transcriptPath,
|
|
1525
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1272
1528
|
const args = [
|
|
1273
1529
|
"-p",
|
|
1274
1530
|
"--input-format",
|
|
@@ -1817,12 +2073,10 @@ var SessionProcessManager = class {
|
|
|
1817
2073
|
*/
|
|
1818
2074
|
ccSessionExists(cwd, claudeSessionId) {
|
|
1819
2075
|
try {
|
|
1820
|
-
const
|
|
1821
|
-
const sessionFile = join(homedir(), ".claude", "projects", cwdEncoded, `${claudeSessionId}.jsonl`);
|
|
2076
|
+
const sessionFile = this.getCCSessionFilePath(cwd, claudeSessionId);
|
|
1822
2077
|
const exists = existsSync(sessionFile);
|
|
1823
2078
|
log$35.debug("检查 CC session 文件", {
|
|
1824
2079
|
cwd,
|
|
1825
|
-
cwdEncoded,
|
|
1826
2080
|
claudeSessionId,
|
|
1827
2081
|
sessionFile,
|
|
1828
2082
|
exists
|
|
@@ -1833,6 +2087,10 @@ var SessionProcessManager = class {
|
|
|
1833
2087
|
return false;
|
|
1834
2088
|
}
|
|
1835
2089
|
}
|
|
2090
|
+
getCCSessionFilePath(cwd, claudeSessionId) {
|
|
2091
|
+
const cwdEncoded = this.encodeCwdForCC(cwd);
|
|
2092
|
+
return join(homedir(), ".claude", "projects", cwdEncoded, `${claudeSessionId}.jsonl`);
|
|
2093
|
+
}
|
|
1836
2094
|
};
|
|
1837
2095
|
//#endregion
|
|
1838
2096
|
//#region src/runtime/claude-code-adapter.ts
|
|
@@ -6996,6 +7254,76 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
|
|
|
6996
7254
|
logger$6.info("批量任务全部完成", { totalTasks: taskConfigs.length });
|
|
6997
7255
|
}
|
|
6998
7256
|
//#endregion
|
|
7257
|
+
//#region src/cc-runtime/image-read-guard.ts
|
|
7258
|
+
const DEFAULT_MAX_IMAGE_READS_PER_RUN = 4;
|
|
7259
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
7260
|
+
".png",
|
|
7261
|
+
".jpg",
|
|
7262
|
+
".jpeg",
|
|
7263
|
+
".gif",
|
|
7264
|
+
".webp",
|
|
7265
|
+
".bmp",
|
|
7266
|
+
".tif",
|
|
7267
|
+
".tiff"
|
|
7268
|
+
]);
|
|
7269
|
+
const imageReadCounts = /* @__PURE__ */ new Map();
|
|
7270
|
+
function evaluateImageReadGuard(body) {
|
|
7271
|
+
const limit = getImageReadLimit();
|
|
7272
|
+
const filePath = getReadFilePath(body);
|
|
7273
|
+
if (limit <= 0 || !filePath || !isImagePath(filePath)) return {
|
|
7274
|
+
allowed: true,
|
|
7275
|
+
count: 0,
|
|
7276
|
+
limit,
|
|
7277
|
+
filePath
|
|
7278
|
+
};
|
|
7279
|
+
const key = getRunKey(body);
|
|
7280
|
+
const count = (imageReadCounts.get(key) ?? 0) + 1;
|
|
7281
|
+
imageReadCounts.set(key, count);
|
|
7282
|
+
if (count <= limit) return {
|
|
7283
|
+
allowed: true,
|
|
7284
|
+
count,
|
|
7285
|
+
limit,
|
|
7286
|
+
filePath
|
|
7287
|
+
};
|
|
7288
|
+
return {
|
|
7289
|
+
allowed: false,
|
|
7290
|
+
count,
|
|
7291
|
+
limit,
|
|
7292
|
+
filePath,
|
|
7293
|
+
reason: `LarkPal blocked image Read #${count}; max image reads per run is ${limit}. Use text extraction or start a fresh run/session before reading more rendered page images.`
|
|
7294
|
+
};
|
|
7295
|
+
}
|
|
7296
|
+
function resetImageReadGuard(key) {
|
|
7297
|
+
if (key) {
|
|
7298
|
+
imageReadCounts.delete(key);
|
|
7299
|
+
return;
|
|
7300
|
+
}
|
|
7301
|
+
imageReadCounts.clear();
|
|
7302
|
+
}
|
|
7303
|
+
function getImageReadLimit() {
|
|
7304
|
+
const raw = process.env.LARKPAL_CC_MAX_IMAGE_READS_PER_RUN;
|
|
7305
|
+
if (!raw) return DEFAULT_MAX_IMAGE_READS_PER_RUN;
|
|
7306
|
+
const parsed = Number.parseInt(raw, 10);
|
|
7307
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_MAX_IMAGE_READS_PER_RUN;
|
|
7308
|
+
}
|
|
7309
|
+
function getReadFilePath(body) {
|
|
7310
|
+
const input = asObject(body.tool_input);
|
|
7311
|
+
const value = input?.file_path ?? input?.path;
|
|
7312
|
+
return typeof value === "string" ? value : void 0;
|
|
7313
|
+
}
|
|
7314
|
+
function getRunKey(body) {
|
|
7315
|
+
const transcriptPath = typeof body.transcript_path === "string" ? body.transcript_path : void 0;
|
|
7316
|
+
const sessionId = typeof body.session_id === "string" ? body.session_id : void 0;
|
|
7317
|
+
const cwd = typeof body.cwd === "string" ? body.cwd : void 0;
|
|
7318
|
+
return transcriptPath ?? sessionId ?? cwd ?? "global";
|
|
7319
|
+
}
|
|
7320
|
+
function isImagePath(filePath) {
|
|
7321
|
+
return IMAGE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
7322
|
+
}
|
|
7323
|
+
function asObject(value) {
|
|
7324
|
+
return typeof value === "object" && value != null ? value : void 0;
|
|
7325
|
+
}
|
|
7326
|
+
//#endregion
|
|
6999
7327
|
//#region src/gateway/hooks-handler.ts
|
|
7000
7328
|
/**
|
|
7001
7329
|
* HTTP Hooks 接收路由
|
|
@@ -7028,6 +7356,63 @@ function createHooksRouter() {
|
|
|
7028
7356
|
});
|
|
7029
7357
|
res.json({ received: true });
|
|
7030
7358
|
});
|
|
7359
|
+
router.post("/pre-tool-use", (req, res) => {
|
|
7360
|
+
const body = req.body;
|
|
7361
|
+
const decision = evaluateImageReadGuard(body);
|
|
7362
|
+
logger$5.info("hook received: pre-tool-use", {
|
|
7363
|
+
method: req.method,
|
|
7364
|
+
url: req.originalUrl,
|
|
7365
|
+
sessionId: body.session_id,
|
|
7366
|
+
toolName: body.tool_name,
|
|
7367
|
+
filePath: decision.filePath,
|
|
7368
|
+
count: decision.count,
|
|
7369
|
+
limit: decision.limit,
|
|
7370
|
+
allowed: decision.allowed
|
|
7371
|
+
});
|
|
7372
|
+
if (decision.allowed) {
|
|
7373
|
+
res.json({ continue: true });
|
|
7374
|
+
return;
|
|
7375
|
+
}
|
|
7376
|
+
res.json({
|
|
7377
|
+
continue: true,
|
|
7378
|
+
hookSpecificOutput: {
|
|
7379
|
+
hookEventName: "PreToolUse",
|
|
7380
|
+
permissionDecision: "deny",
|
|
7381
|
+
permissionDecisionReason: decision.reason
|
|
7382
|
+
}
|
|
7383
|
+
});
|
|
7384
|
+
});
|
|
7385
|
+
router.post("/post-tool-use", async (req, res) => {
|
|
7386
|
+
const body = req.body;
|
|
7387
|
+
const transcriptPath = typeof body.transcript_path === "string" ? body.transcript_path : typeof body.transcriptPath === "string" ? body.transcriptPath : void 0;
|
|
7388
|
+
logger$5.info("hook received: post-tool-use", {
|
|
7389
|
+
method: req.method,
|
|
7390
|
+
url: req.originalUrl,
|
|
7391
|
+
sessionId: body.session_id,
|
|
7392
|
+
toolName: body.tool_name,
|
|
7393
|
+
transcriptPath
|
|
7394
|
+
});
|
|
7395
|
+
if (!transcriptPath || !isTranscriptSanitizerEnabled()) {
|
|
7396
|
+
res.json({ continue: true });
|
|
7397
|
+
return;
|
|
7398
|
+
}
|
|
7399
|
+
try {
|
|
7400
|
+
const result = await sanitizeClaudeTranscriptFile(transcriptPath);
|
|
7401
|
+
res.json({
|
|
7402
|
+
continue: true,
|
|
7403
|
+
hookSpecificOutput: result.changed ? {
|
|
7404
|
+
hookEventName: "PostToolUse",
|
|
7405
|
+
additionalContext: `LarkPal externalized ${result.sanitizedImageBlocks} image tool result(s) from the persisted Claude transcript to reduce future replay payload.`
|
|
7406
|
+
} : void 0
|
|
7407
|
+
});
|
|
7408
|
+
} catch (err) {
|
|
7409
|
+
logger$5.warn("post-tool-use transcript sanitizer failed", {
|
|
7410
|
+
transcriptPath,
|
|
7411
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7412
|
+
});
|
|
7413
|
+
res.json({ continue: true });
|
|
7414
|
+
}
|
|
7415
|
+
});
|
|
7031
7416
|
router.post("/session-end", (req, res) => {
|
|
7032
7417
|
const body = req.body;
|
|
7033
7418
|
logger$5.info("hook received: session-end", {
|
|
@@ -7036,6 +7421,7 @@ function createHooksRouter() {
|
|
|
7036
7421
|
sessionId: body.session_id,
|
|
7037
7422
|
body
|
|
7038
7423
|
});
|
|
7424
|
+
resetImageReadGuard(typeof body.transcript_path === "string" ? body.transcript_path : void 0);
|
|
7039
7425
|
res.json({ received: true });
|
|
7040
7426
|
});
|
|
7041
7427
|
router.post("/notification", (req, res) => {
|