@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.
Files changed (2) hide show
  1. package/dist/main.mjs +510 -124
  2. 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$40 = larkLogger("preflight");
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$40.info("开始前置环境检查...");
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$40.info(` ${status(useCodex && codex ? codex : claude)}`);
88
- log$40.info(` ${status(larkCli)}`);
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$40.error(msg);
124
+ log$41.error(msg);
125
125
  throw new Error("前置环境检查未通过,缺少必要依赖");
126
126
  }
127
- log$40.info("前置环境检查通过 ✓");
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$39 = larkLogger("config/ensure-lark-cli");
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$39.info("环境变量凭证未设置,跳过 lark-cli 配置同步");
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$39.info("lark-cli 配置文件已存在,跳过同步", {
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$39.info("检测到环境变量凭证但 lark-cli 配置文件缺失,开始同步", {
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$39.info("已生成新版 lark-cli 配置文件", { path: NEW_CONFIG_PATH });
200
+ log$40.info("已生成新版 lark-cli 配置文件", { path: NEW_CONFIG_PATH });
201
201
  } catch (err) {
202
- log$39.warn("生成新版 lark-cli 配置文件失败", {
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$39.info("已生成旧版 lark-cli 配置文件", { path: LEGACY_CONFIG_PATH });
221
+ log$40.info("已生成旧版 lark-cli 配置文件", { path: LEGACY_CONFIG_PATH });
222
222
  } catch (err) {
223
- log$39.warn("生成旧版 lark-cli 配置文件失败", {
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
- const DEFAULT_SETTINGS = {
255
- permissions: {
256
- allow: [
257
- "Bash(*)",
258
- "Read(*)",
259
- "Write(*)",
260
- "Edit(*)",
261
- "MultiEdit(*)",
262
- "Glob(*)",
263
- "Grep(*)",
264
- "LS(*)",
265
- "TodoWrite(*)",
266
- "WebFetch(*)",
267
- "WebSearch(*)"
268
- ],
269
- deny: [
270
- "Write(//.claude/CLAUDE.md)",
271
- "Edit(//.claude/CLAUDE.md)",
272
- "Write(//.claude/settings.json)",
273
- "Edit(//.claude/settings.json)",
274
- "Read(//.lark-cli/**)",
275
- "Read(//.config/lark/**)",
276
- "Read(//.larkpal/credentials.json)",
277
- "Read(//.env)",
278
- "Read(//.env.*)",
279
- "Bash(cat ~/.lark-cli:*)",
280
- "Bash(cat ~/.config/lark:*)",
281
- "Bash(cat ~/.larkpal/credentials:*)",
282
- "Bash(head ~/.lark-cli:*)",
283
- "Bash(head ~/.config/lark:*)",
284
- "Bash(tail ~/.lark-cli:*)",
285
- "Bash(tail ~/.config/lark:*)",
286
- "Bash(less ~/.lark-cli:*)",
287
- "Bash(less ~/.config/lark:*)",
288
- "Bash(more ~/.lark-cli:*)",
289
- "Bash(more ~/.config/lark:*)",
290
- "Bash(env:*)",
291
- "Bash(printenv:*)",
292
- "Bash(export -p:*)",
293
- "Bash(echo $LARK_APP_SECRET:*)",
294
- "Bash(echo $ANTHROPIC_API_KEY:*)"
295
- ]
296
- },
297
- hooks: {
298
- SessionStart: [{ hooks: [{
299
- type: "http",
300
- url: "http://localhost:3000/hooks/session-start",
301
- timeout: 5
302
- }] }],
303
- Stop: [{ hooks: [{
304
- type: "http",
305
- url: "http://localhost:3000/hooks/stop",
306
- timeout: 5
307
- }] }],
308
- SessionEnd: [{ hooks: [{
309
- type: "http",
310
- url: "http://localhost:3000/hooks/session-end",
311
- timeout: 5
312
- }] }],
313
- Notification: [{ hooks: [{
314
- type: "http",
315
- url: "http://localhost:3000/hooks/notification",
316
- timeout: 5
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(DEFAULT_SETTINGS, null, 2), "utf-8");
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$38 = larkLogger("user/credential-vault");
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$38.error("LARKPAL_ENCRYPTION_KEY 长度不正确,需要 32 字节", { actualLength: this.encryptionKey.length });
482
+ log$39.error("LARKPAL_ENCRYPTION_KEY 长度不正确,需要 32 字节", { actualLength: this.encryptionKey.length });
457
483
  this.encryptionKey = null;
458
- } else log$38.info("凭证加密已启用");
484
+ } else log$39.info("凭证加密已启用");
459
485
  } else {
460
486
  this.encryptionKey = null;
461
- log$38.warn("LARKPAL_ENCRYPTION_KEY 未设置,凭证将以明文存储(仅限开发模式)");
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$38.info("凭证文件不存在,使用空凭证", { path: this.credentialPath });
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$38.info("凭证文件加载完成", {
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$38.error("凭证文件加载失败", {
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$38.info("凭证文件保存完成", {
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$38.error("凭证文件保存失败", {
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$38.info("设置凭证", {
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$38.info("凭证已删除", { name });
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$38.warn("凭证解密失败,跳过", {
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$37 = larkLogger("user/mcp-merge");
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$37.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
708
+ log$38.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
683
709
  } catch (err) {
684
- log$37.warn("全局 MCP 配置文件解析失败", {
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$37.info("用户 MCP 配置加载完成", {
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$37.warn("用户 MCP 配置文件解析失败", {
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$37.info("MCP 配置合并完成", {
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$37.info("MCP 合并配置已写入临时文件", {
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$37.debug("MCP 临时配置文件已清理", { path: tmpPath });
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$36 = larkLogger("cc-runtime/stream-parser");
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$36.warn("NDJSON 解析失败,跳过该行", {
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$36.debug("跳过 subagent 内部事件", {
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$36.info("收到 system 消息", {
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$36.debug("收到未知消息类型,已忽略", { type: msg.type });
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$36.debug("content_block_start 事件", {
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$36.info("工具调用开始", {
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$36.info("轮次结束", { stopReason });
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$36.info("收到完整 assistant 消息", {
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$36.info("从 assistant 消息提取工具调用", {
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$36.debug("工具结果返回", {
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
- log$36.warn("模型请求了未声明工具", {
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$36.debug("工具执行进度", {
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$36.info("CC 执行完成", {
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 isResuming = !isEphemeral && (opts?._forceResume || this.ccSessionExists(cwd, claudeSessionId));
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 cwdEncoded = this.encodeCwdForCC(cwd);
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",