@vibe-lark/larkpal 0.1.76 → 0.1.77
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 +390 -52
- 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
|
});
|
|
@@ -305,6 +305,22 @@ const DEFAULT_SETTINGS = {
|
|
|
305
305
|
url: "http://localhost:3000/hooks/stop",
|
|
306
306
|
timeout: 5
|
|
307
307
|
}] }],
|
|
308
|
+
PreToolUse: [{
|
|
309
|
+
matcher: "Read",
|
|
310
|
+
hooks: [{
|
|
311
|
+
type: "http",
|
|
312
|
+
url: "http://localhost:3000/hooks/pre-tool-use",
|
|
313
|
+
timeout: 5
|
|
314
|
+
}]
|
|
315
|
+
}],
|
|
316
|
+
PostToolUse: [{
|
|
317
|
+
matcher: "Read",
|
|
318
|
+
hooks: [{
|
|
319
|
+
type: "http",
|
|
320
|
+
url: "http://localhost:3000/hooks/post-tool-use",
|
|
321
|
+
timeout: 5
|
|
322
|
+
}]
|
|
323
|
+
}],
|
|
308
324
|
SessionEnd: [{ hooks: [{
|
|
309
325
|
type: "http",
|
|
310
326
|
url: "http://localhost:3000/hooks/session-end",
|
|
@@ -441,7 +457,7 @@ async function ensureDefaults() {
|
|
|
441
457
|
* - github: { type: "api_key", token }
|
|
442
458
|
* - custom-api: { type: "http_bearer", base_url, token }
|
|
443
459
|
*/
|
|
444
|
-
const log$
|
|
460
|
+
const log$39 = larkLogger("user/credential-vault");
|
|
445
461
|
var CredentialVault = class {
|
|
446
462
|
credentialPath;
|
|
447
463
|
encryptionKey;
|
|
@@ -453,12 +469,12 @@ var CredentialVault = class {
|
|
|
453
469
|
if (keyEnv) {
|
|
454
470
|
this.encryptionKey = keyEnv.length === 64 ? Buffer.from(keyEnv, "hex") : Buffer.from(keyEnv, "base64");
|
|
455
471
|
if (this.encryptionKey.length !== 32) {
|
|
456
|
-
log$
|
|
472
|
+
log$39.error("LARKPAL_ENCRYPTION_KEY 长度不正确,需要 32 字节", { actualLength: this.encryptionKey.length });
|
|
457
473
|
this.encryptionKey = null;
|
|
458
|
-
} else log$
|
|
474
|
+
} else log$39.info("凭证加密已启用");
|
|
459
475
|
} else {
|
|
460
476
|
this.encryptionKey = null;
|
|
461
|
-
log$
|
|
477
|
+
log$39.warn("LARKPAL_ENCRYPTION_KEY 未设置,凭证将以明文存储(仅限开发模式)");
|
|
462
478
|
}
|
|
463
479
|
}
|
|
464
480
|
/**
|
|
@@ -466,7 +482,7 @@ var CredentialVault = class {
|
|
|
466
482
|
*/
|
|
467
483
|
async load() {
|
|
468
484
|
if (!existsSync$1(this.credentialPath)) {
|
|
469
|
-
log$
|
|
485
|
+
log$39.info("凭证文件不存在,使用空凭证", { path: this.credentialPath });
|
|
470
486
|
this.store = {};
|
|
471
487
|
this.loaded = true;
|
|
472
488
|
return;
|
|
@@ -475,12 +491,12 @@ var CredentialVault = class {
|
|
|
475
491
|
const raw = await readFile$1(this.credentialPath, "utf-8");
|
|
476
492
|
this.store = JSON.parse(raw);
|
|
477
493
|
this.loaded = true;
|
|
478
|
-
log$
|
|
494
|
+
log$39.info("凭证文件加载完成", {
|
|
479
495
|
path: this.credentialPath,
|
|
480
496
|
credentialCount: Object.keys(this.store).length
|
|
481
497
|
});
|
|
482
498
|
} catch (err) {
|
|
483
|
-
log$
|
|
499
|
+
log$39.error("凭证文件加载失败", {
|
|
484
500
|
path: this.credentialPath,
|
|
485
501
|
error: err instanceof Error ? err.message : String(err)
|
|
486
502
|
});
|
|
@@ -494,12 +510,12 @@ var CredentialVault = class {
|
|
|
494
510
|
async save() {
|
|
495
511
|
try {
|
|
496
512
|
await writeFile$1(this.credentialPath, JSON.stringify(this.store, null, 2), "utf-8");
|
|
497
|
-
log$
|
|
513
|
+
log$39.info("凭证文件保存完成", {
|
|
498
514
|
path: this.credentialPath,
|
|
499
515
|
credentialCount: Object.keys(this.store).length
|
|
500
516
|
});
|
|
501
517
|
} catch (err) {
|
|
502
|
-
log$
|
|
518
|
+
log$39.error("凭证文件保存失败", {
|
|
503
519
|
path: this.credentialPath,
|
|
504
520
|
error: err instanceof Error ? err.message : String(err)
|
|
505
521
|
});
|
|
@@ -521,7 +537,7 @@ var CredentialVault = class {
|
|
|
521
537
|
*/
|
|
522
538
|
async setCredential(name, credential) {
|
|
523
539
|
if (!this.loaded) await this.load();
|
|
524
|
-
log$
|
|
540
|
+
log$39.info("设置凭证", {
|
|
525
541
|
name,
|
|
526
542
|
fields: Object.keys(credential)
|
|
527
543
|
});
|
|
@@ -537,7 +553,7 @@ var CredentialVault = class {
|
|
|
537
553
|
if (!(name in this.store)) return false;
|
|
538
554
|
delete this.store[name];
|
|
539
555
|
await this.save();
|
|
540
|
-
log$
|
|
556
|
+
log$39.info("凭证已删除", { name });
|
|
541
557
|
return true;
|
|
542
558
|
}
|
|
543
559
|
/**
|
|
@@ -566,7 +582,7 @@ var CredentialVault = class {
|
|
|
566
582
|
envVars[envKey] = String(fieldValue);
|
|
567
583
|
}
|
|
568
584
|
} catch (err) {
|
|
569
|
-
log$
|
|
585
|
+
log$39.warn("凭证解密失败,跳过", {
|
|
570
586
|
name,
|
|
571
587
|
error: err instanceof Error ? err.message : String(err)
|
|
572
588
|
});
|
|
@@ -623,7 +639,7 @@ var CredentialVault = class {
|
|
|
623
639
|
* - B 用户的 CC 进程连接 B 的数据库 MCP Server
|
|
624
640
|
* - 全局工具(lark-cli、文件操作等)所有用户共享
|
|
625
641
|
*/
|
|
626
|
-
const log$
|
|
642
|
+
const log$38 = larkLogger("user/mcp-merge");
|
|
627
643
|
/** 全局 MCP 配置路径 */
|
|
628
644
|
const GLOBAL_MCP_CONFIG_PATH = path$1.join(os.homedir(), ".claude", "mcp-servers.json");
|
|
629
645
|
/**
|
|
@@ -679,9 +695,9 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
679
695
|
if (existsSync$1(GLOBAL_MCP_CONFIG_PATH)) try {
|
|
680
696
|
const raw = await readFile$1(GLOBAL_MCP_CONFIG_PATH, "utf-8");
|
|
681
697
|
globalConfig = JSON.parse(raw);
|
|
682
|
-
log$
|
|
698
|
+
log$38.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
|
|
683
699
|
} catch (err) {
|
|
684
|
-
log$
|
|
700
|
+
log$38.warn("全局 MCP 配置文件解析失败", {
|
|
685
701
|
path: GLOBAL_MCP_CONFIG_PATH,
|
|
686
702
|
error: err instanceof Error ? err.message : String(err)
|
|
687
703
|
});
|
|
@@ -692,13 +708,13 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
692
708
|
if (existsSync$1(userMcpPath)) try {
|
|
693
709
|
const raw = await readFile$1(userMcpPath, "utf-8");
|
|
694
710
|
userConfig = JSON.parse(raw);
|
|
695
|
-
log$
|
|
711
|
+
log$38.info("用户 MCP 配置加载完成", {
|
|
696
712
|
userId: userCtx.userId,
|
|
697
713
|
serverCount: Object.keys(userConfig).length,
|
|
698
714
|
servers: Object.keys(userConfig)
|
|
699
715
|
});
|
|
700
716
|
} catch (err) {
|
|
701
|
-
log$
|
|
717
|
+
log$38.warn("用户 MCP 配置文件解析失败", {
|
|
702
718
|
userId: userCtx?.userId,
|
|
703
719
|
path: userMcpPath,
|
|
704
720
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -710,7 +726,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
710
726
|
...globalConfig,
|
|
711
727
|
...userConfig
|
|
712
728
|
};
|
|
713
|
-
log$
|
|
729
|
+
log$38.info("MCP 配置合并完成", {
|
|
714
730
|
userId: userCtx?.userId,
|
|
715
731
|
builtinServers: Object.keys(builtinConfig),
|
|
716
732
|
globalServers: Object.keys(globalConfig),
|
|
@@ -719,7 +735,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
719
735
|
});
|
|
720
736
|
const tmpPath = path$1.join(os.tmpdir(), `mcp-${sessionId}.json`);
|
|
721
737
|
await writeFile$1(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
|
|
722
|
-
log$
|
|
738
|
+
log$38.info("MCP 合并配置已写入临时文件", {
|
|
723
739
|
path: tmpPath,
|
|
724
740
|
serverCount: Object.keys(merged).length
|
|
725
741
|
});
|
|
@@ -733,7 +749,7 @@ async function cleanupMcpConfig(sessionId) {
|
|
|
733
749
|
try {
|
|
734
750
|
if (existsSync$1(tmpPath)) {
|
|
735
751
|
await unlink$1(tmpPath);
|
|
736
|
-
log$
|
|
752
|
+
log$38.debug("MCP 临时配置文件已清理", { path: tmpPath });
|
|
737
753
|
}
|
|
738
754
|
} catch {}
|
|
739
755
|
}
|
|
@@ -781,7 +797,7 @@ function getToolDisplayName(toolName) {
|
|
|
781
797
|
* - result(data) — 最终结果
|
|
782
798
|
* - parseError(error, rawLine) — 解析错误(不中断流)
|
|
783
799
|
*/
|
|
784
|
-
const log$
|
|
800
|
+
const log$37 = larkLogger("cc-runtime/stream-parser");
|
|
785
801
|
const MAX_TOOL_RESULT_CONTENT_CHARS = 8e3;
|
|
786
802
|
var CCStreamParser = class extends EventEmitter {
|
|
787
803
|
toolNamesById = /* @__PURE__ */ new Map();
|
|
@@ -798,7 +814,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
798
814
|
try {
|
|
799
815
|
msg = JSON.parse(trimmed);
|
|
800
816
|
} catch (err) {
|
|
801
|
-
log$
|
|
817
|
+
log$37.warn("NDJSON 解析失败,跳过该行", {
|
|
802
818
|
error: String(err),
|
|
803
819
|
rawLine: trimmed.slice(0, 200)
|
|
804
820
|
});
|
|
@@ -806,7 +822,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
806
822
|
return;
|
|
807
823
|
}
|
|
808
824
|
if (msg.parent_tool_use_id != null && msg.type !== "result") {
|
|
809
|
-
log$
|
|
825
|
+
log$37.debug("跳过 subagent 内部事件", {
|
|
810
826
|
type: msg.type,
|
|
811
827
|
parentToolUseId: msg.parent_tool_use_id
|
|
812
828
|
});
|
|
@@ -829,7 +845,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
829
845
|
this.handleResult(msg);
|
|
830
846
|
break;
|
|
831
847
|
case "system":
|
|
832
|
-
log$
|
|
848
|
+
log$37.info("收到 system 消息", {
|
|
833
849
|
subtype: msg.subtype,
|
|
834
850
|
detail: JSON.stringify(msg).slice(0, 500)
|
|
835
851
|
});
|
|
@@ -837,7 +853,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
837
853
|
this.emit("system", msg);
|
|
838
854
|
break;
|
|
839
855
|
default:
|
|
840
|
-
log$
|
|
856
|
+
log$37.debug("收到未知消息类型,已忽略", { type: msg.type });
|
|
841
857
|
break;
|
|
842
858
|
}
|
|
843
859
|
}
|
|
@@ -856,14 +872,14 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
856
872
|
switch (event.type) {
|
|
857
873
|
case "content_block_start": {
|
|
858
874
|
const block = event.content_block;
|
|
859
|
-
log$
|
|
875
|
+
log$37.debug("content_block_start 事件", {
|
|
860
876
|
blockType: block?.type,
|
|
861
877
|
toolName: block?.name,
|
|
862
878
|
toolUseId: block?.id
|
|
863
879
|
});
|
|
864
880
|
if (block?.type === "tool_use" && block.name) {
|
|
865
881
|
this.recordToolUse(block.name, block.id);
|
|
866
|
-
log$
|
|
882
|
+
log$37.info("工具调用开始", {
|
|
867
883
|
toolName: block.name,
|
|
868
884
|
displayName: getToolDisplayName(block.name),
|
|
869
885
|
toolUseId: block.id
|
|
@@ -882,7 +898,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
882
898
|
case "message_delta": {
|
|
883
899
|
const stopReason = event.delta?.stop_reason;
|
|
884
900
|
if (stopReason) {
|
|
885
|
-
log$
|
|
901
|
+
log$37.info("轮次结束", { stopReason });
|
|
886
902
|
this.emit("turnEnd", stopReason);
|
|
887
903
|
}
|
|
888
904
|
break;
|
|
@@ -902,7 +918,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
902
918
|
const content = msg.message?.content;
|
|
903
919
|
const stopReason = msg.message?.stop_reason;
|
|
904
920
|
const blockTypes = Array.isArray(content) ? content.map((b) => `${b.type}${b.name ? ":" + b.name : ""}`).join(", ") : "none";
|
|
905
|
-
log$
|
|
921
|
+
log$37.info("收到完整 assistant 消息", {
|
|
906
922
|
model: msg.message?.model,
|
|
907
923
|
stopReason,
|
|
908
924
|
contentBlocks: content?.length,
|
|
@@ -911,7 +927,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
911
927
|
if (Array.isArray(content)) {
|
|
912
928
|
for (const block of content) if (block.type === "tool_use" && block.name) {
|
|
913
929
|
this.recordToolUse(block.name, block.id);
|
|
914
|
-
log$
|
|
930
|
+
log$37.info("从 assistant 消息提取工具调用", {
|
|
915
931
|
toolName: block.name,
|
|
916
932
|
displayName: getToolDisplayName(block.name),
|
|
917
933
|
toolUseId: block.id
|
|
@@ -931,7 +947,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
931
947
|
if (!Array.isArray(content)) return;
|
|
932
948
|
for (const item of content) if (item.type === "tool_result" && item.tool_use_id) {
|
|
933
949
|
const result = this.buildToolResultSummary(item, msg.toolUseResult);
|
|
934
|
-
log$
|
|
950
|
+
log$37.debug("工具结果返回", {
|
|
935
951
|
toolUseId: item.tool_use_id,
|
|
936
952
|
toolName: result.toolName,
|
|
937
953
|
isError: result.isError,
|
|
@@ -947,7 +963,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
947
963
|
recordToolUse(toolName, toolUseId) {
|
|
948
964
|
if (toolUseId) this.toolNamesById.set(toolUseId, toolName);
|
|
949
965
|
if (this.declaredTools && !this.declaredTools.has(toolName)) {
|
|
950
|
-
log$
|
|
966
|
+
log$37.warn("模型请求了未声明工具", {
|
|
951
967
|
toolName,
|
|
952
968
|
toolUseId
|
|
953
969
|
});
|
|
@@ -974,7 +990,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
974
990
|
* 处理 tool_progress 消息 — 工具执行进度
|
|
975
991
|
*/
|
|
976
992
|
handleToolProgress(msg) {
|
|
977
|
-
log$
|
|
993
|
+
log$37.debug("工具执行进度", {
|
|
978
994
|
toolName: msg.tool_name,
|
|
979
995
|
elapsed: msg.elapsed_time_seconds
|
|
980
996
|
});
|
|
@@ -984,7 +1000,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
984
1000
|
* 处理 result 消息 — CC 执行完成的最终结果
|
|
985
1001
|
*/
|
|
986
1002
|
handleResult(msg) {
|
|
987
|
-
log$
|
|
1003
|
+
log$37.info("CC 执行完成", {
|
|
988
1004
|
subtype: msg.subtype,
|
|
989
1005
|
isError: msg.is_error,
|
|
990
1006
|
durationMs: msg.duration_ms,
|
|
@@ -1048,6 +1064,189 @@ function tryParseJson$1(value) {
|
|
|
1048
1064
|
}
|
|
1049
1065
|
}
|
|
1050
1066
|
//#endregion
|
|
1067
|
+
//#region src/cc-runtime/transcript-sanitizer.ts
|
|
1068
|
+
const log$36 = larkLogger("cc-runtime/transcript-sanitizer");
|
|
1069
|
+
const ARTIFACT_DIR = "larkpal-artifacts";
|
|
1070
|
+
const MAX_READ_BYTES = readPositiveIntEnv$1("LARKPAL_TRANSCRIPT_SANITIZER_MAX_BYTES", 50 * 1024 * 1024);
|
|
1071
|
+
async function sanitizeClaudeTranscriptFile(filePath) {
|
|
1072
|
+
const result = {
|
|
1073
|
+
changed: false,
|
|
1074
|
+
lines: 0,
|
|
1075
|
+
sanitizedImageBlocks: 0,
|
|
1076
|
+
artifacts: 0
|
|
1077
|
+
};
|
|
1078
|
+
const fileStat = await stat(filePath);
|
|
1079
|
+
if (fileStat.size > MAX_READ_BYTES) {
|
|
1080
|
+
log$36.warn("跳过过大的 Claude transcript sanitizer", {
|
|
1081
|
+
filePath,
|
|
1082
|
+
size: fileStat.size
|
|
1083
|
+
});
|
|
1084
|
+
return result;
|
|
1085
|
+
}
|
|
1086
|
+
const raw = await readFile(filePath, "utf-8");
|
|
1087
|
+
const hasTrailingNewline = raw.endsWith("\n");
|
|
1088
|
+
const lines = raw.split("\n");
|
|
1089
|
+
if (hasTrailingNewline) lines.pop();
|
|
1090
|
+
result.lines = lines.length;
|
|
1091
|
+
const out = [];
|
|
1092
|
+
const artifactRoot = join(dirname(filePath), ARTIFACT_DIR);
|
|
1093
|
+
for (const line of lines) {
|
|
1094
|
+
if (!line.trim()) {
|
|
1095
|
+
out.push(line);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
if (!mightContainImagePayload(line)) {
|
|
1099
|
+
out.push(line);
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
let entry;
|
|
1103
|
+
try {
|
|
1104
|
+
entry = JSON.parse(line);
|
|
1105
|
+
} catch {
|
|
1106
|
+
out.push(line);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const lineStats = await sanitizeTranscriptEntry(entry, artifactRoot);
|
|
1110
|
+
if (lineStats.sanitizedImageBlocks > 0) {
|
|
1111
|
+
result.changed = true;
|
|
1112
|
+
result.sanitizedImageBlocks += lineStats.sanitizedImageBlocks;
|
|
1113
|
+
result.artifacts += lineStats.artifacts;
|
|
1114
|
+
out.push(JSON.stringify(entry));
|
|
1115
|
+
} else out.push(line);
|
|
1116
|
+
}
|
|
1117
|
+
if (!result.changed) return result;
|
|
1118
|
+
const tempPath = `${filePath}.larkpal-sanitized-${process.pid}-${Date.now()}`;
|
|
1119
|
+
await writeFile(tempPath, out.join("\n") + (hasTrailingNewline ? "\n" : ""), "utf-8");
|
|
1120
|
+
await rename(tempPath, filePath);
|
|
1121
|
+
log$36.info("Claude transcript 图片结果已清理", {
|
|
1122
|
+
filePath,
|
|
1123
|
+
...result
|
|
1124
|
+
});
|
|
1125
|
+
return result;
|
|
1126
|
+
}
|
|
1127
|
+
function isTranscriptSanitizerEnabled() {
|
|
1128
|
+
const value = process.env.LARKPAL_TRANSCRIPT_SANITIZER;
|
|
1129
|
+
return value !== "0" && value !== "false";
|
|
1130
|
+
}
|
|
1131
|
+
async function sanitizeTranscriptEntry(entry, artifactRoot) {
|
|
1132
|
+
let sanitizedImageBlocks = 0;
|
|
1133
|
+
let artifacts = 0;
|
|
1134
|
+
const message = asObject$1(entry.message);
|
|
1135
|
+
const content = message ? message.content : void 0;
|
|
1136
|
+
if (!Array.isArray(content)) return {
|
|
1137
|
+
sanitizedImageBlocks,
|
|
1138
|
+
artifacts
|
|
1139
|
+
};
|
|
1140
|
+
for (const block of content) {
|
|
1141
|
+
const toolResult = asObject$1(block);
|
|
1142
|
+
if (!toolResult || toolResult.type !== "tool_result") continue;
|
|
1143
|
+
const toolUseId = typeof toolResult.tool_use_id === "string" ? toolResult.tool_use_id : void 0;
|
|
1144
|
+
const replacement = await sanitizeToolResultContent(toolResult.content, artifactRoot, toolUseId);
|
|
1145
|
+
if (!replacement) continue;
|
|
1146
|
+
toolResult.content = replacement.content;
|
|
1147
|
+
sanitizedImageBlocks += replacement.imageBlocks;
|
|
1148
|
+
artifacts += replacement.artifacts;
|
|
1149
|
+
const toolUseResult = asObject$1(entry.toolUseResult);
|
|
1150
|
+
if (toolUseResult?.type === "image") {
|
|
1151
|
+
const file = asObject$1(toolUseResult.file);
|
|
1152
|
+
if (file && typeof file.base64 === "string") {
|
|
1153
|
+
file.artifactUri = replacement.metadata[0]?.artifactUri;
|
|
1154
|
+
file.base64 = "[redacted by LarkPal: image artifact externalized]";
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
sanitizedImageBlocks,
|
|
1160
|
+
artifacts
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
async function sanitizeToolResultContent(content, artifactRoot, toolUseId) {
|
|
1164
|
+
if (!Array.isArray(content)) return null;
|
|
1165
|
+
const metadata = [];
|
|
1166
|
+
let imageBlocks = 0;
|
|
1167
|
+
let artifacts = 0;
|
|
1168
|
+
for (let i = 0; i < content.length; i++) {
|
|
1169
|
+
const block = asObject$1(content[i]);
|
|
1170
|
+
if (!isBase64ImageBlock(block)) continue;
|
|
1171
|
+
imageBlocks++;
|
|
1172
|
+
const artifact = await writeImageArtifact(artifactRoot, block, toolUseId, i);
|
|
1173
|
+
metadata.push(artifact);
|
|
1174
|
+
artifacts++;
|
|
1175
|
+
}
|
|
1176
|
+
if (imageBlocks === 0) return null;
|
|
1177
|
+
return {
|
|
1178
|
+
content: buildReplacementText(metadata),
|
|
1179
|
+
imageBlocks,
|
|
1180
|
+
artifacts,
|
|
1181
|
+
metadata
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
async function writeImageArtifact(artifactRoot, block, toolUseId, index) {
|
|
1185
|
+
const source = asObject$1(block.source);
|
|
1186
|
+
const data = typeof source?.data === "string" ? source.data : "";
|
|
1187
|
+
const mimeType = typeof source?.media_type === "string" ? source.media_type : void 0;
|
|
1188
|
+
const artifactId = `${toolUseId ?? randomUUID()}-${index}`;
|
|
1189
|
+
const artifactPath = join(artifactRoot, `${artifactId}.json`);
|
|
1190
|
+
const artifactUri = `artifact://claude-transcript/${artifactId}`;
|
|
1191
|
+
const dimensions = formatDimensions(block);
|
|
1192
|
+
await mkdir(artifactRoot, { recursive: true });
|
|
1193
|
+
await writeFile(artifactPath, JSON.stringify({
|
|
1194
|
+
artifactUri,
|
|
1195
|
+
originalToolName: "Read",
|
|
1196
|
+
toolUseId,
|
|
1197
|
+
mimeType,
|
|
1198
|
+
dimensions,
|
|
1199
|
+
originalSize: data.length,
|
|
1200
|
+
sourcePath: typeof block.filePath === "string" ? block.filePath : void 0,
|
|
1201
|
+
base64: data
|
|
1202
|
+
}, null, 2), {
|
|
1203
|
+
encoding: "utf-8",
|
|
1204
|
+
flag: "w"
|
|
1205
|
+
});
|
|
1206
|
+
return {
|
|
1207
|
+
artifactUri,
|
|
1208
|
+
originalToolName: "Read",
|
|
1209
|
+
toolUseId,
|
|
1210
|
+
mimeType,
|
|
1211
|
+
dimensions,
|
|
1212
|
+
originalSize: data.length,
|
|
1213
|
+
sourcePath: typeof block.filePath === "string" ? block.filePath : void 0,
|
|
1214
|
+
summary: "Rendered image/PDF page preview externalized by LarkPal transcript sanitizer."
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function buildReplacementText(metadata) {
|
|
1218
|
+
return JSON.stringify({
|
|
1219
|
+
larkpalSanitized: true,
|
|
1220
|
+
reason: "Previous image tool result was externalized to avoid replaying base64 image payloads into provider requests.",
|
|
1221
|
+
artifacts: metadata
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function isBase64ImageBlock(value) {
|
|
1225
|
+
if (!value || value.type !== "image") return false;
|
|
1226
|
+
const source = asObject$1(value.source);
|
|
1227
|
+
return source?.type === "base64" && typeof source.data === "string";
|
|
1228
|
+
}
|
|
1229
|
+
function formatDimensions(block) {
|
|
1230
|
+
const dimensions = asObject$1(block.dimensions);
|
|
1231
|
+
if (!dimensions) return void 0;
|
|
1232
|
+
const width = dimensions.displayWidth ?? dimensions.originalWidth;
|
|
1233
|
+
const height = dimensions.displayHeight ?? dimensions.originalHeight;
|
|
1234
|
+
if (typeof width !== "number" || typeof height !== "number") return void 0;
|
|
1235
|
+
return `${width}x${height}`;
|
|
1236
|
+
}
|
|
1237
|
+
function mightContainImagePayload(line) {
|
|
1238
|
+
return line.includes("\"tool_result\"") && line.includes("\"type\":\"image\"") && line.includes("\"base64\"");
|
|
1239
|
+
}
|
|
1240
|
+
function asObject$1(value) {
|
|
1241
|
+
return typeof value === "object" && value != null ? value : void 0;
|
|
1242
|
+
}
|
|
1243
|
+
function readPositiveIntEnv$1(name, fallback) {
|
|
1244
|
+
const raw = process.env[name];
|
|
1245
|
+
if (!raw) return fallback;
|
|
1246
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1247
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1248
|
+
}
|
|
1249
|
+
//#endregion
|
|
1051
1250
|
//#region src/cc-runtime/process-manager.ts
|
|
1052
1251
|
/**
|
|
1053
1252
|
* CC 进程管理器 — 按 session_id 管理 Claude Code 常驻进程的生命周期
|
|
@@ -1268,7 +1467,16 @@ var SessionProcessManager = class {
|
|
|
1268
1467
|
const { sessionId, cwd, prompt } = config;
|
|
1269
1468
|
const isEphemeral = config.transcriptMode === "ephemeral";
|
|
1270
1469
|
const claudeSessionId = isEphemeral ? v4() : v5(sessionId, LARKPAL_SESSION_NAMESPACE);
|
|
1271
|
-
const
|
|
1470
|
+
const transcriptPath = this.getCCSessionFilePath(cwd, claudeSessionId);
|
|
1471
|
+
const transcriptExists = existsSync(transcriptPath);
|
|
1472
|
+
const isResuming = !isEphemeral && (opts?._forceResume || transcriptExists);
|
|
1473
|
+
if (isResuming && transcriptExists && isTranscriptSanitizerEnabled()) await sanitizeClaudeTranscriptFile(transcriptPath).catch((err) => {
|
|
1474
|
+
log$35.warn("Claude transcript sanitizer 失败,继续启动进程", {
|
|
1475
|
+
sessionId,
|
|
1476
|
+
transcriptPath,
|
|
1477
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1272
1480
|
const args = [
|
|
1273
1481
|
"-p",
|
|
1274
1482
|
"--input-format",
|
|
@@ -1817,12 +2025,10 @@ var SessionProcessManager = class {
|
|
|
1817
2025
|
*/
|
|
1818
2026
|
ccSessionExists(cwd, claudeSessionId) {
|
|
1819
2027
|
try {
|
|
1820
|
-
const
|
|
1821
|
-
const sessionFile = join(homedir(), ".claude", "projects", cwdEncoded, `${claudeSessionId}.jsonl`);
|
|
2028
|
+
const sessionFile = this.getCCSessionFilePath(cwd, claudeSessionId);
|
|
1822
2029
|
const exists = existsSync(sessionFile);
|
|
1823
2030
|
log$35.debug("检查 CC session 文件", {
|
|
1824
2031
|
cwd,
|
|
1825
|
-
cwdEncoded,
|
|
1826
2032
|
claudeSessionId,
|
|
1827
2033
|
sessionFile,
|
|
1828
2034
|
exists
|
|
@@ -1833,6 +2039,10 @@ var SessionProcessManager = class {
|
|
|
1833
2039
|
return false;
|
|
1834
2040
|
}
|
|
1835
2041
|
}
|
|
2042
|
+
getCCSessionFilePath(cwd, claudeSessionId) {
|
|
2043
|
+
const cwdEncoded = this.encodeCwdForCC(cwd);
|
|
2044
|
+
return join(homedir(), ".claude", "projects", cwdEncoded, `${claudeSessionId}.jsonl`);
|
|
2045
|
+
}
|
|
1836
2046
|
};
|
|
1837
2047
|
//#endregion
|
|
1838
2048
|
//#region src/runtime/claude-code-adapter.ts
|
|
@@ -6996,6 +7206,76 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
|
|
|
6996
7206
|
logger$6.info("批量任务全部完成", { totalTasks: taskConfigs.length });
|
|
6997
7207
|
}
|
|
6998
7208
|
//#endregion
|
|
7209
|
+
//#region src/cc-runtime/image-read-guard.ts
|
|
7210
|
+
const DEFAULT_MAX_IMAGE_READS_PER_RUN = 4;
|
|
7211
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
7212
|
+
".png",
|
|
7213
|
+
".jpg",
|
|
7214
|
+
".jpeg",
|
|
7215
|
+
".gif",
|
|
7216
|
+
".webp",
|
|
7217
|
+
".bmp",
|
|
7218
|
+
".tif",
|
|
7219
|
+
".tiff"
|
|
7220
|
+
]);
|
|
7221
|
+
const imageReadCounts = /* @__PURE__ */ new Map();
|
|
7222
|
+
function evaluateImageReadGuard(body) {
|
|
7223
|
+
const limit = getImageReadLimit();
|
|
7224
|
+
const filePath = getReadFilePath(body);
|
|
7225
|
+
if (limit <= 0 || !filePath || !isImagePath(filePath)) return {
|
|
7226
|
+
allowed: true,
|
|
7227
|
+
count: 0,
|
|
7228
|
+
limit,
|
|
7229
|
+
filePath
|
|
7230
|
+
};
|
|
7231
|
+
const key = getRunKey(body);
|
|
7232
|
+
const count = (imageReadCounts.get(key) ?? 0) + 1;
|
|
7233
|
+
imageReadCounts.set(key, count);
|
|
7234
|
+
if (count <= limit) return {
|
|
7235
|
+
allowed: true,
|
|
7236
|
+
count,
|
|
7237
|
+
limit,
|
|
7238
|
+
filePath
|
|
7239
|
+
};
|
|
7240
|
+
return {
|
|
7241
|
+
allowed: false,
|
|
7242
|
+
count,
|
|
7243
|
+
limit,
|
|
7244
|
+
filePath,
|
|
7245
|
+
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.`
|
|
7246
|
+
};
|
|
7247
|
+
}
|
|
7248
|
+
function resetImageReadGuard(key) {
|
|
7249
|
+
if (key) {
|
|
7250
|
+
imageReadCounts.delete(key);
|
|
7251
|
+
return;
|
|
7252
|
+
}
|
|
7253
|
+
imageReadCounts.clear();
|
|
7254
|
+
}
|
|
7255
|
+
function getImageReadLimit() {
|
|
7256
|
+
const raw = process.env.LARKPAL_CC_MAX_IMAGE_READS_PER_RUN;
|
|
7257
|
+
if (!raw) return DEFAULT_MAX_IMAGE_READS_PER_RUN;
|
|
7258
|
+
const parsed = Number.parseInt(raw, 10);
|
|
7259
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_MAX_IMAGE_READS_PER_RUN;
|
|
7260
|
+
}
|
|
7261
|
+
function getReadFilePath(body) {
|
|
7262
|
+
const input = asObject(body.tool_input);
|
|
7263
|
+
const value = input?.file_path ?? input?.path;
|
|
7264
|
+
return typeof value === "string" ? value : void 0;
|
|
7265
|
+
}
|
|
7266
|
+
function getRunKey(body) {
|
|
7267
|
+
const transcriptPath = typeof body.transcript_path === "string" ? body.transcript_path : void 0;
|
|
7268
|
+
const sessionId = typeof body.session_id === "string" ? body.session_id : void 0;
|
|
7269
|
+
const cwd = typeof body.cwd === "string" ? body.cwd : void 0;
|
|
7270
|
+
return transcriptPath ?? sessionId ?? cwd ?? "global";
|
|
7271
|
+
}
|
|
7272
|
+
function isImagePath(filePath) {
|
|
7273
|
+
return IMAGE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
7274
|
+
}
|
|
7275
|
+
function asObject(value) {
|
|
7276
|
+
return typeof value === "object" && value != null ? value : void 0;
|
|
7277
|
+
}
|
|
7278
|
+
//#endregion
|
|
6999
7279
|
//#region src/gateway/hooks-handler.ts
|
|
7000
7280
|
/**
|
|
7001
7281
|
* HTTP Hooks 接收路由
|
|
@@ -7028,6 +7308,63 @@ function createHooksRouter() {
|
|
|
7028
7308
|
});
|
|
7029
7309
|
res.json({ received: true });
|
|
7030
7310
|
});
|
|
7311
|
+
router.post("/pre-tool-use", (req, res) => {
|
|
7312
|
+
const body = req.body;
|
|
7313
|
+
const decision = evaluateImageReadGuard(body);
|
|
7314
|
+
logger$5.info("hook received: pre-tool-use", {
|
|
7315
|
+
method: req.method,
|
|
7316
|
+
url: req.originalUrl,
|
|
7317
|
+
sessionId: body.session_id,
|
|
7318
|
+
toolName: body.tool_name,
|
|
7319
|
+
filePath: decision.filePath,
|
|
7320
|
+
count: decision.count,
|
|
7321
|
+
limit: decision.limit,
|
|
7322
|
+
allowed: decision.allowed
|
|
7323
|
+
});
|
|
7324
|
+
if (decision.allowed) {
|
|
7325
|
+
res.json({ continue: true });
|
|
7326
|
+
return;
|
|
7327
|
+
}
|
|
7328
|
+
res.json({
|
|
7329
|
+
continue: true,
|
|
7330
|
+
hookSpecificOutput: {
|
|
7331
|
+
hookEventName: "PreToolUse",
|
|
7332
|
+
permissionDecision: "deny",
|
|
7333
|
+
permissionDecisionReason: decision.reason
|
|
7334
|
+
}
|
|
7335
|
+
});
|
|
7336
|
+
});
|
|
7337
|
+
router.post("/post-tool-use", async (req, res) => {
|
|
7338
|
+
const body = req.body;
|
|
7339
|
+
const transcriptPath = typeof body.transcript_path === "string" ? body.transcript_path : typeof body.transcriptPath === "string" ? body.transcriptPath : void 0;
|
|
7340
|
+
logger$5.info("hook received: post-tool-use", {
|
|
7341
|
+
method: req.method,
|
|
7342
|
+
url: req.originalUrl,
|
|
7343
|
+
sessionId: body.session_id,
|
|
7344
|
+
toolName: body.tool_name,
|
|
7345
|
+
transcriptPath
|
|
7346
|
+
});
|
|
7347
|
+
if (!transcriptPath || !isTranscriptSanitizerEnabled()) {
|
|
7348
|
+
res.json({ continue: true });
|
|
7349
|
+
return;
|
|
7350
|
+
}
|
|
7351
|
+
try {
|
|
7352
|
+
const result = await sanitizeClaudeTranscriptFile(transcriptPath);
|
|
7353
|
+
res.json({
|
|
7354
|
+
continue: true,
|
|
7355
|
+
hookSpecificOutput: result.changed ? {
|
|
7356
|
+
hookEventName: "PostToolUse",
|
|
7357
|
+
additionalContext: `LarkPal externalized ${result.sanitizedImageBlocks} image tool result(s) from the persisted Claude transcript to reduce future replay payload.`
|
|
7358
|
+
} : void 0
|
|
7359
|
+
});
|
|
7360
|
+
} catch (err) {
|
|
7361
|
+
logger$5.warn("post-tool-use transcript sanitizer failed", {
|
|
7362
|
+
transcriptPath,
|
|
7363
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7364
|
+
});
|
|
7365
|
+
res.json({ continue: true });
|
|
7366
|
+
}
|
|
7367
|
+
});
|
|
7031
7368
|
router.post("/session-end", (req, res) => {
|
|
7032
7369
|
const body = req.body;
|
|
7033
7370
|
logger$5.info("hook received: session-end", {
|
|
@@ -7036,6 +7373,7 @@ function createHooksRouter() {
|
|
|
7036
7373
|
sessionId: body.session_id,
|
|
7037
7374
|
body
|
|
7038
7375
|
});
|
|
7376
|
+
resetImageReadGuard(typeof body.transcript_path === "string" ? body.transcript_path : void 0);
|
|
7039
7377
|
res.json({ received: true });
|
|
7040
7378
|
});
|
|
7041
7379
|
router.post("/notification", (req, res) => {
|