@vibe-lark/larkpal 0.1.62 → 0.1.63
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/bin/larkpal.js +0 -0
- package/dist/main.mjs +491 -219
- package/package.json +16 -15
package/dist/main.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as LarkCliCredentialProvider } from "./lark-cli-provider-CdgwmqSz.mjs";
|
|
2
2
|
import { n as getLarkRuntime, r as setLarkRuntime, t as larkLogger } from "./lark-logger-D7_pEVQc.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import * as path$2 from "node:path";
|
|
6
6
|
import path, { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,7 +9,7 @@ import "dotenv/config";
|
|
|
9
9
|
import { access, lstat, mkdir, open, readFile, readdir, readlink, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { createInterface } from "node:readline";
|
|
14
14
|
import { v4, v5 } from "uuid";
|
|
15
15
|
import { mkdir as mkdir$1, readFile as readFile$1, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
|
|
@@ -19,6 +19,9 @@ import path$1 from "path";
|
|
|
19
19
|
import os from "os";
|
|
20
20
|
import { EventEmitter } from "node:events";
|
|
21
21
|
import express, { Router } from "express";
|
|
22
|
+
import Busboy from "busboy";
|
|
23
|
+
import { PassThrough, Readable, Transform } from "node:stream";
|
|
24
|
+
import { pipeline } from "node:stream/promises";
|
|
22
25
|
import { EventEmitter as EventEmitter$1 } from "events";
|
|
23
26
|
import cron from "node-cron";
|
|
24
27
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
@@ -27,7 +30,6 @@ import { SILENT_REPLY_TOKEN } from "openclaw/plugin-sdk/reply-runtime";
|
|
|
27
30
|
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
28
31
|
import * as dns from "node:dns/promises";
|
|
29
32
|
import * as net from "node:net";
|
|
30
|
-
import { Readable } from "node:stream";
|
|
31
33
|
//#region src/config/preflight.ts
|
|
32
34
|
/**
|
|
33
35
|
* 启动前置环境检查
|
|
@@ -38,7 +40,7 @@ import { Readable } from "node:stream";
|
|
|
38
40
|
*
|
|
39
41
|
* 本模块在启动时检测它们是否已安装,缺失时给出明确的安装指引。
|
|
40
42
|
*/
|
|
41
|
-
const log$
|
|
43
|
+
const log$40 = larkLogger("preflight");
|
|
42
44
|
function detectCli(name) {
|
|
43
45
|
try {
|
|
44
46
|
const cliPath = execFileSync("which", [name], {
|
|
@@ -76,14 +78,14 @@ function detectCli(name) {
|
|
|
76
78
|
* 缺失任一依赖时打印安装指引并抛出错误,阻止启动。
|
|
77
79
|
*/
|
|
78
80
|
function preflight() {
|
|
79
|
-
log$
|
|
81
|
+
log$40.info("开始前置环境检查...");
|
|
80
82
|
const useCodex = (process.env.LARKPAL_RUNTIME || "claude-code") === "codex-app-server";
|
|
81
83
|
const claude = detectCli("claude");
|
|
82
84
|
const codex = useCodex ? detectCli(process.env.LARKPAL_CODEX_BIN || "codex") : void 0;
|
|
83
85
|
const larkCli = detectCli("lark-cli");
|
|
84
86
|
const status = (dep) => dep.installed ? `✅ ${dep.name} (${dep.version ?? "unknown"}) → ${dep.path}` : `❌ ${dep.name} 未安装`;
|
|
85
|
-
log$
|
|
86
|
-
log$
|
|
87
|
+
log$40.info(` ${status(useCodex && codex ? codex : claude)}`);
|
|
88
|
+
log$40.info(` ${status(larkCli)}`);
|
|
87
89
|
const missing = [];
|
|
88
90
|
if (useCodex) {
|
|
89
91
|
if (!codex?.installed) missing.push([
|
|
@@ -119,10 +121,10 @@ function preflight() {
|
|
|
119
121
|
"",
|
|
120
122
|
"请安装缺失的依赖后重新启动 LarkPal。"
|
|
121
123
|
].join("\n");
|
|
122
|
-
log$
|
|
124
|
+
log$40.error(msg);
|
|
123
125
|
throw new Error("前置环境检查未通过,缺少必要依赖");
|
|
124
126
|
}
|
|
125
|
-
log$
|
|
127
|
+
log$40.info("前置环境检查通过 ✓");
|
|
126
128
|
return {
|
|
127
129
|
claude,
|
|
128
130
|
codex,
|
|
@@ -147,7 +149,7 @@ function preflight() {
|
|
|
147
149
|
* 同时生成新版 (~/.lark-cli/config.json) 和旧版 (~/.config/lark/config.json)
|
|
148
150
|
* 格式的配置文件,保证 lark-cli 无论通过哪种路径读取都能正常工作。
|
|
149
151
|
*/
|
|
150
|
-
const log$
|
|
152
|
+
const log$39 = larkLogger("config/ensure-lark-cli");
|
|
151
153
|
/** 新版 lark-cli 配置路径 */
|
|
152
154
|
const NEW_CONFIG_DIR = join(homedir(), ".lark-cli");
|
|
153
155
|
const NEW_CONFIG_PATH = join(NEW_CONFIG_DIR, "config.json");
|
|
@@ -164,19 +166,19 @@ function ensureLarkCliConfig() {
|
|
|
164
166
|
const appId = process.env.LARK_APP_ID;
|
|
165
167
|
const appSecret = process.env.LARK_APP_SECRET;
|
|
166
168
|
if (!appId || !appSecret) {
|
|
167
|
-
log$
|
|
169
|
+
log$39.info("环境变量凭证未设置,跳过 lark-cli 配置同步");
|
|
168
170
|
return;
|
|
169
171
|
}
|
|
170
172
|
const hasNewConfig = existsSync(NEW_CONFIG_PATH);
|
|
171
173
|
const hasLegacyConfig = existsSync(LEGACY_CONFIG_PATH);
|
|
172
174
|
if (hasNewConfig && hasLegacyConfig) {
|
|
173
|
-
log$
|
|
175
|
+
log$39.info("lark-cli 配置文件已存在,跳过同步", {
|
|
174
176
|
newConfig: NEW_CONFIG_PATH,
|
|
175
177
|
legacyConfig: LEGACY_CONFIG_PATH
|
|
176
178
|
});
|
|
177
179
|
return;
|
|
178
180
|
}
|
|
179
|
-
log$
|
|
181
|
+
log$39.info("检测到环境变量凭证但 lark-cli 配置文件缺失,开始同步", {
|
|
180
182
|
appId,
|
|
181
183
|
hasNewConfig,
|
|
182
184
|
hasLegacyConfig
|
|
@@ -195,9 +197,9 @@ function ensureLarkCliConfig() {
|
|
|
195
197
|
encoding: "utf-8",
|
|
196
198
|
mode: 384
|
|
197
199
|
});
|
|
198
|
-
log$
|
|
200
|
+
log$39.info("已生成新版 lark-cli 配置文件", { path: NEW_CONFIG_PATH });
|
|
199
201
|
} catch (err) {
|
|
200
|
-
log$
|
|
202
|
+
log$39.warn("生成新版 lark-cli 配置文件失败", {
|
|
201
203
|
path: NEW_CONFIG_PATH,
|
|
202
204
|
error: err instanceof Error ? err.message : String(err)
|
|
203
205
|
});
|
|
@@ -216,9 +218,9 @@ function ensureLarkCliConfig() {
|
|
|
216
218
|
encoding: "utf-8",
|
|
217
219
|
mode: 384
|
|
218
220
|
});
|
|
219
|
-
log$
|
|
221
|
+
log$39.info("已生成旧版 lark-cli 配置文件", { path: LEGACY_CONFIG_PATH });
|
|
220
222
|
} catch (err) {
|
|
221
|
-
log$
|
|
223
|
+
log$39.warn("生成旧版 lark-cli 配置文件失败", {
|
|
222
224
|
path: LEGACY_CONFIG_PATH,
|
|
223
225
|
error: err instanceof Error ? err.message : String(err)
|
|
224
226
|
});
|
|
@@ -439,7 +441,7 @@ async function ensureDefaults() {
|
|
|
439
441
|
* - github: { type: "api_key", token }
|
|
440
442
|
* - custom-api: { type: "http_bearer", base_url, token }
|
|
441
443
|
*/
|
|
442
|
-
const log$
|
|
444
|
+
const log$38 = larkLogger("user/credential-vault");
|
|
443
445
|
var CredentialVault = class {
|
|
444
446
|
credentialPath;
|
|
445
447
|
encryptionKey;
|
|
@@ -451,12 +453,12 @@ var CredentialVault = class {
|
|
|
451
453
|
if (keyEnv) {
|
|
452
454
|
this.encryptionKey = keyEnv.length === 64 ? Buffer.from(keyEnv, "hex") : Buffer.from(keyEnv, "base64");
|
|
453
455
|
if (this.encryptionKey.length !== 32) {
|
|
454
|
-
log$
|
|
456
|
+
log$38.error("LARKPAL_ENCRYPTION_KEY 长度不正确,需要 32 字节", { actualLength: this.encryptionKey.length });
|
|
455
457
|
this.encryptionKey = null;
|
|
456
|
-
} else log$
|
|
458
|
+
} else log$38.info("凭证加密已启用");
|
|
457
459
|
} else {
|
|
458
460
|
this.encryptionKey = null;
|
|
459
|
-
log$
|
|
461
|
+
log$38.warn("LARKPAL_ENCRYPTION_KEY 未设置,凭证将以明文存储(仅限开发模式)");
|
|
460
462
|
}
|
|
461
463
|
}
|
|
462
464
|
/**
|
|
@@ -464,7 +466,7 @@ var CredentialVault = class {
|
|
|
464
466
|
*/
|
|
465
467
|
async load() {
|
|
466
468
|
if (!existsSync$1(this.credentialPath)) {
|
|
467
|
-
log$
|
|
469
|
+
log$38.info("凭证文件不存在,使用空凭证", { path: this.credentialPath });
|
|
468
470
|
this.store = {};
|
|
469
471
|
this.loaded = true;
|
|
470
472
|
return;
|
|
@@ -473,12 +475,12 @@ var CredentialVault = class {
|
|
|
473
475
|
const raw = await readFile$1(this.credentialPath, "utf-8");
|
|
474
476
|
this.store = JSON.parse(raw);
|
|
475
477
|
this.loaded = true;
|
|
476
|
-
log$
|
|
478
|
+
log$38.info("凭证文件加载完成", {
|
|
477
479
|
path: this.credentialPath,
|
|
478
480
|
credentialCount: Object.keys(this.store).length
|
|
479
481
|
});
|
|
480
482
|
} catch (err) {
|
|
481
|
-
log$
|
|
483
|
+
log$38.error("凭证文件加载失败", {
|
|
482
484
|
path: this.credentialPath,
|
|
483
485
|
error: err instanceof Error ? err.message : String(err)
|
|
484
486
|
});
|
|
@@ -492,12 +494,12 @@ var CredentialVault = class {
|
|
|
492
494
|
async save() {
|
|
493
495
|
try {
|
|
494
496
|
await writeFile$1(this.credentialPath, JSON.stringify(this.store, null, 2), "utf-8");
|
|
495
|
-
log$
|
|
497
|
+
log$38.info("凭证文件保存完成", {
|
|
496
498
|
path: this.credentialPath,
|
|
497
499
|
credentialCount: Object.keys(this.store).length
|
|
498
500
|
});
|
|
499
501
|
} catch (err) {
|
|
500
|
-
log$
|
|
502
|
+
log$38.error("凭证文件保存失败", {
|
|
501
503
|
path: this.credentialPath,
|
|
502
504
|
error: err instanceof Error ? err.message : String(err)
|
|
503
505
|
});
|
|
@@ -519,7 +521,7 @@ var CredentialVault = class {
|
|
|
519
521
|
*/
|
|
520
522
|
async setCredential(name, credential) {
|
|
521
523
|
if (!this.loaded) await this.load();
|
|
522
|
-
log$
|
|
524
|
+
log$38.info("设置凭证", {
|
|
523
525
|
name,
|
|
524
526
|
fields: Object.keys(credential)
|
|
525
527
|
});
|
|
@@ -535,7 +537,7 @@ var CredentialVault = class {
|
|
|
535
537
|
if (!(name in this.store)) return false;
|
|
536
538
|
delete this.store[name];
|
|
537
539
|
await this.save();
|
|
538
|
-
log$
|
|
540
|
+
log$38.info("凭证已删除", { name });
|
|
539
541
|
return true;
|
|
540
542
|
}
|
|
541
543
|
/**
|
|
@@ -564,7 +566,7 @@ var CredentialVault = class {
|
|
|
564
566
|
envVars[envKey] = String(fieldValue);
|
|
565
567
|
}
|
|
566
568
|
} catch (err) {
|
|
567
|
-
log$
|
|
569
|
+
log$38.warn("凭证解密失败,跳过", {
|
|
568
570
|
name,
|
|
569
571
|
error: err instanceof Error ? err.message : String(err)
|
|
570
572
|
});
|
|
@@ -621,7 +623,7 @@ var CredentialVault = class {
|
|
|
621
623
|
* - B 用户的 CC 进程连接 B 的数据库 MCP Server
|
|
622
624
|
* - 全局工具(lark-cli、文件操作等)所有用户共享
|
|
623
625
|
*/
|
|
624
|
-
const log$
|
|
626
|
+
const log$37 = larkLogger("user/mcp-merge");
|
|
625
627
|
/** 全局 MCP 配置路径 */
|
|
626
628
|
const GLOBAL_MCP_CONFIG_PATH = path$1.join(os.homedir(), ".claude", "mcp-servers.json");
|
|
627
629
|
/**
|
|
@@ -677,9 +679,9 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
677
679
|
if (existsSync$1(GLOBAL_MCP_CONFIG_PATH)) try {
|
|
678
680
|
const raw = await readFile$1(GLOBAL_MCP_CONFIG_PATH, "utf-8");
|
|
679
681
|
globalConfig = JSON.parse(raw);
|
|
680
|
-
log$
|
|
682
|
+
log$37.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
|
|
681
683
|
} catch (err) {
|
|
682
|
-
log$
|
|
684
|
+
log$37.warn("全局 MCP 配置文件解析失败", {
|
|
683
685
|
path: GLOBAL_MCP_CONFIG_PATH,
|
|
684
686
|
error: err instanceof Error ? err.message : String(err)
|
|
685
687
|
});
|
|
@@ -690,13 +692,13 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
690
692
|
if (existsSync$1(userMcpPath)) try {
|
|
691
693
|
const raw = await readFile$1(userMcpPath, "utf-8");
|
|
692
694
|
userConfig = JSON.parse(raw);
|
|
693
|
-
log$
|
|
695
|
+
log$37.info("用户 MCP 配置加载完成", {
|
|
694
696
|
userId: userCtx.userId,
|
|
695
697
|
serverCount: Object.keys(userConfig).length,
|
|
696
698
|
servers: Object.keys(userConfig)
|
|
697
699
|
});
|
|
698
700
|
} catch (err) {
|
|
699
|
-
log$
|
|
701
|
+
log$37.warn("用户 MCP 配置文件解析失败", {
|
|
700
702
|
userId: userCtx?.userId,
|
|
701
703
|
path: userMcpPath,
|
|
702
704
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -708,7 +710,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
708
710
|
...globalConfig,
|
|
709
711
|
...userConfig
|
|
710
712
|
};
|
|
711
|
-
log$
|
|
713
|
+
log$37.info("MCP 配置合并完成", {
|
|
712
714
|
userId: userCtx?.userId,
|
|
713
715
|
builtinServers: Object.keys(builtinConfig),
|
|
714
716
|
globalServers: Object.keys(globalConfig),
|
|
@@ -717,7 +719,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
717
719
|
});
|
|
718
720
|
const tmpPath = path$1.join(os.tmpdir(), `mcp-${sessionId}.json`);
|
|
719
721
|
await writeFile$1(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
|
|
720
|
-
log$
|
|
722
|
+
log$37.info("MCP 合并配置已写入临时文件", {
|
|
721
723
|
path: tmpPath,
|
|
722
724
|
serverCount: Object.keys(merged).length
|
|
723
725
|
});
|
|
@@ -731,7 +733,7 @@ async function cleanupMcpConfig(sessionId) {
|
|
|
731
733
|
try {
|
|
732
734
|
if (existsSync$1(tmpPath)) {
|
|
733
735
|
await unlink$1(tmpPath);
|
|
734
|
-
log$
|
|
736
|
+
log$37.debug("MCP 临时配置文件已清理", { path: tmpPath });
|
|
735
737
|
}
|
|
736
738
|
} catch {}
|
|
737
739
|
}
|
|
@@ -779,7 +781,7 @@ function getToolDisplayName(toolName) {
|
|
|
779
781
|
* - result(data) — 最终结果
|
|
780
782
|
* - parseError(error, rawLine) — 解析错误(不中断流)
|
|
781
783
|
*/
|
|
782
|
-
const log$
|
|
784
|
+
const log$36 = larkLogger("cc-runtime/stream-parser");
|
|
783
785
|
var CCStreamParser = class extends EventEmitter {
|
|
784
786
|
/**
|
|
785
787
|
* 解析一行 NDJSON 文本
|
|
@@ -793,7 +795,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
793
795
|
try {
|
|
794
796
|
msg = JSON.parse(trimmed);
|
|
795
797
|
} catch (err) {
|
|
796
|
-
log$
|
|
798
|
+
log$36.warn("NDJSON 解析失败,跳过该行", {
|
|
797
799
|
error: String(err),
|
|
798
800
|
rawLine: trimmed.slice(0, 200)
|
|
799
801
|
});
|
|
@@ -801,7 +803,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
801
803
|
return;
|
|
802
804
|
}
|
|
803
805
|
if (msg.parent_tool_use_id != null && msg.type !== "result") {
|
|
804
|
-
log$
|
|
806
|
+
log$36.debug("跳过 subagent 内部事件", {
|
|
805
807
|
type: msg.type,
|
|
806
808
|
parentToolUseId: msg.parent_tool_use_id
|
|
807
809
|
});
|
|
@@ -824,14 +826,14 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
824
826
|
this.handleResult(msg);
|
|
825
827
|
break;
|
|
826
828
|
case "system":
|
|
827
|
-
log$
|
|
829
|
+
log$36.info("收到 system 消息", {
|
|
828
830
|
subtype: msg.subtype,
|
|
829
831
|
detail: JSON.stringify(msg).slice(0, 500)
|
|
830
832
|
});
|
|
831
833
|
this.emit("system", msg);
|
|
832
834
|
break;
|
|
833
835
|
default:
|
|
834
|
-
log$
|
|
836
|
+
log$36.debug("收到未知消息类型,已忽略", { type: msg.type });
|
|
835
837
|
break;
|
|
836
838
|
}
|
|
837
839
|
}
|
|
@@ -850,13 +852,13 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
850
852
|
switch (event.type) {
|
|
851
853
|
case "content_block_start": {
|
|
852
854
|
const block = event.content_block;
|
|
853
|
-
log$
|
|
855
|
+
log$36.debug("content_block_start 事件", {
|
|
854
856
|
blockType: block?.type,
|
|
855
857
|
toolName: block?.name,
|
|
856
858
|
toolUseId: block?.id
|
|
857
859
|
});
|
|
858
860
|
if (block?.type === "tool_use" && block.name) {
|
|
859
|
-
log$
|
|
861
|
+
log$36.info("工具调用开始", {
|
|
860
862
|
toolName: block.name,
|
|
861
863
|
displayName: getToolDisplayName(block.name),
|
|
862
864
|
toolUseId: block.id
|
|
@@ -875,7 +877,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
875
877
|
case "message_delta": {
|
|
876
878
|
const stopReason = event.delta?.stop_reason;
|
|
877
879
|
if (stopReason) {
|
|
878
|
-
log$
|
|
880
|
+
log$36.info("轮次结束", { stopReason });
|
|
879
881
|
this.emit("turnEnd", stopReason);
|
|
880
882
|
}
|
|
881
883
|
break;
|
|
@@ -895,7 +897,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
895
897
|
const content = msg.message?.content;
|
|
896
898
|
const stopReason = msg.message?.stop_reason;
|
|
897
899
|
const blockTypes = Array.isArray(content) ? content.map((b) => `${b.type}${b.name ? ":" + b.name : ""}`).join(", ") : "none";
|
|
898
|
-
log$
|
|
900
|
+
log$36.info("收到完整 assistant 消息", {
|
|
899
901
|
model: msg.message?.model,
|
|
900
902
|
stopReason,
|
|
901
903
|
contentBlocks: content?.length,
|
|
@@ -903,7 +905,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
903
905
|
});
|
|
904
906
|
if (Array.isArray(content)) {
|
|
905
907
|
for (const block of content) if (block.type === "tool_use" && block.name) {
|
|
906
|
-
log$
|
|
908
|
+
log$36.info("从 assistant 消息提取工具调用", {
|
|
907
909
|
toolName: block.name,
|
|
908
910
|
displayName: getToolDisplayName(block.name),
|
|
909
911
|
toolUseId: block.id
|
|
@@ -922,7 +924,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
922
924
|
const content = msg.message?.content;
|
|
923
925
|
if (!Array.isArray(content)) return;
|
|
924
926
|
for (const item of content) if (item.type === "tool_result" && item.tool_use_id) {
|
|
925
|
-
log$
|
|
927
|
+
log$36.debug("工具结果返回", { toolUseId: item.tool_use_id });
|
|
926
928
|
this.emit("toolResult", item.tool_use_id);
|
|
927
929
|
}
|
|
928
930
|
}
|
|
@@ -930,7 +932,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
930
932
|
* 处理 tool_progress 消息 — 工具执行进度
|
|
931
933
|
*/
|
|
932
934
|
handleToolProgress(msg) {
|
|
933
|
-
log$
|
|
935
|
+
log$36.debug("工具执行进度", {
|
|
934
936
|
toolName: msg.tool_name,
|
|
935
937
|
elapsed: msg.elapsed_time_seconds
|
|
936
938
|
});
|
|
@@ -940,7 +942,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
940
942
|
* 处理 result 消息 — CC 执行完成的最终结果
|
|
941
943
|
*/
|
|
942
944
|
handleResult(msg) {
|
|
943
|
-
log$
|
|
945
|
+
log$36.info("CC 执行完成", {
|
|
944
946
|
subtype: msg.subtype,
|
|
945
947
|
isError: msg.is_error,
|
|
946
948
|
durationMs: msg.duration_ms,
|
|
@@ -973,7 +975,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
973
975
|
* 多轮对话:进程内自动保持完整对话上下文(mutableMessages 数组累积),
|
|
974
976
|
* 无需外部维护历史。
|
|
975
977
|
*/
|
|
976
|
-
const log$
|
|
978
|
+
const log$35 = larkLogger("cc-runtime/process-manager");
|
|
977
979
|
/**
|
|
978
980
|
* LarkPal 会话 UUID v5 命名空间
|
|
979
981
|
*
|
|
@@ -1031,6 +1033,8 @@ var SessionProcessManager = class {
|
|
|
1031
1033
|
* 用于 rewind_files 定位回退点。每次 sendMessage 时更新。
|
|
1032
1034
|
*/
|
|
1033
1035
|
lastUserMessageIds = /* @__PURE__ */ new Map();
|
|
1036
|
+
/** 最近一条用户消息是否包含图片:sessionId → hasImages */
|
|
1037
|
+
lastMessageHadImages = /* @__PURE__ */ new Map();
|
|
1034
1038
|
/**
|
|
1035
1039
|
* 控制请求等待表:request_id → { resolve, reject, timer }
|
|
1036
1040
|
*
|
|
@@ -1061,6 +1065,10 @@ var SessionProcessManager = class {
|
|
|
1061
1065
|
}
|
|
1062
1066
|
return false;
|
|
1063
1067
|
}
|
|
1068
|
+
/** 最近一条发送给 Claude Code 的用户消息是否包含图片 */
|
|
1069
|
+
getLastMessageHadImages(sessionId) {
|
|
1070
|
+
return this.lastMessageHadImages.get(sessionId) === true;
|
|
1071
|
+
}
|
|
1064
1072
|
/**
|
|
1065
1073
|
* 创建消息完成 Promise,用于阻塞 executePrompt 直到 CC 返回 result
|
|
1066
1074
|
*/
|
|
@@ -1094,9 +1102,9 @@ var SessionProcessManager = class {
|
|
|
1094
1102
|
const t0 = Date.now();
|
|
1095
1103
|
const prevLock = this.sessionLocks.get(sessionId);
|
|
1096
1104
|
if (prevLock) {
|
|
1097
|
-
log$
|
|
1105
|
+
log$35.info("等待上一条消息处理完成", { sessionId });
|
|
1098
1106
|
await prevLock;
|
|
1099
|
-
log$
|
|
1107
|
+
log$35.info("[perf] 串行锁等待完成", {
|
|
1100
1108
|
sessionId,
|
|
1101
1109
|
waitMs: Date.now() - t0
|
|
1102
1110
|
});
|
|
@@ -1110,7 +1118,7 @@ var SessionProcessManager = class {
|
|
|
1110
1118
|
try {
|
|
1111
1119
|
const existing = this.processes.get(sessionId);
|
|
1112
1120
|
if (existing && existing.status === "running" && existing.useStreamInput) {
|
|
1113
|
-
log$
|
|
1121
|
+
log$35.info("向常驻进程发送消息", {
|
|
1114
1122
|
sessionId,
|
|
1115
1123
|
promptLength: config.prompt.length,
|
|
1116
1124
|
pid: existing.childProcess.pid,
|
|
@@ -1120,14 +1128,14 @@ var SessionProcessManager = class {
|
|
|
1120
1128
|
this.sendMessage(sessionId, config.prompt);
|
|
1121
1129
|
this.resetIdleTimer(sessionId);
|
|
1122
1130
|
await completionPromise;
|
|
1123
|
-
log$
|
|
1131
|
+
log$35.info("[perf] executePrompt 完成(热进程)", {
|
|
1124
1132
|
sessionId,
|
|
1125
1133
|
totalMs: Date.now() - t0
|
|
1126
1134
|
});
|
|
1127
1135
|
return;
|
|
1128
1136
|
}
|
|
1129
1137
|
if (existing && (existing.status === "stopped" || existing.status === "crashed")) {
|
|
1130
|
-
log$
|
|
1138
|
+
log$35.info("清理已终止的旧进程,准备重启", {
|
|
1131
1139
|
sessionId,
|
|
1132
1140
|
oldStatus: existing.status
|
|
1133
1141
|
});
|
|
@@ -1135,25 +1143,25 @@ var SessionProcessManager = class {
|
|
|
1135
1143
|
this.sessionLocks.set(sessionId, sessionLock);
|
|
1136
1144
|
}
|
|
1137
1145
|
if (existing && existing.status === "running" && !existing.useStreamInput) {
|
|
1138
|
-
log$
|
|
1146
|
+
log$35.warn("停止旧的单次进程后重新启动为常驻模式", { sessionId });
|
|
1139
1147
|
await this.stopProcess(sessionId);
|
|
1140
1148
|
this.sessionLocks.set(sessionId, sessionLock);
|
|
1141
1149
|
}
|
|
1142
1150
|
const completionPromise = this.createMessageCompletionPromise(sessionId);
|
|
1143
1151
|
const tStart = Date.now();
|
|
1144
1152
|
await this.startPersistentProcess(config);
|
|
1145
|
-
log$
|
|
1153
|
+
log$35.info("[perf] 冷启动进程完成", {
|
|
1146
1154
|
sessionId,
|
|
1147
1155
|
coldStartMs: Date.now() - tStart
|
|
1148
1156
|
});
|
|
1149
1157
|
await completionPromise;
|
|
1150
|
-
log$
|
|
1158
|
+
log$35.info("[perf] executePrompt 完成(冷启动)", {
|
|
1151
1159
|
sessionId,
|
|
1152
1160
|
totalMs: Date.now() - t0
|
|
1153
1161
|
});
|
|
1154
1162
|
} finally {
|
|
1155
1163
|
if (config.transcriptMode === "ephemeral") await this.stopProcess(sessionId).catch((err) => {
|
|
1156
|
-
log$
|
|
1164
|
+
log$35.warn("ephemeral CC 进程清理失败", {
|
|
1157
1165
|
sessionId,
|
|
1158
1166
|
error: err instanceof Error ? err.message : String(err)
|
|
1159
1167
|
});
|
|
@@ -1192,13 +1200,13 @@ var SessionProcessManager = class {
|
|
|
1192
1200
|
try {
|
|
1193
1201
|
const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId, cwd);
|
|
1194
1202
|
args.push("--mcp-config", mcpConfigPath);
|
|
1195
|
-
log$
|
|
1203
|
+
log$35.info("MCP 配置已合并并注入到 CC 进程", {
|
|
1196
1204
|
sessionId,
|
|
1197
1205
|
userId: config.userContext?.userId,
|
|
1198
1206
|
mcpConfigPath
|
|
1199
1207
|
});
|
|
1200
1208
|
} catch (err) {
|
|
1201
|
-
log$
|
|
1209
|
+
log$35.warn("MCP 配置合并失败,CC 进程将使用全局默认配置", {
|
|
1202
1210
|
sessionId,
|
|
1203
1211
|
error: err instanceof Error ? err.message : String(err)
|
|
1204
1212
|
});
|
|
@@ -1225,12 +1233,12 @@ var SessionProcessManager = class {
|
|
|
1225
1233
|
if (larkCred?.user_access_token && typeof larkCred.user_access_token === "string") processEnv.LARK_USER_ACCESS_TOKEN = larkCred.user_access_token;
|
|
1226
1234
|
} else {
|
|
1227
1235
|
delete processEnv.LARK_USER_ACCESS_TOKEN;
|
|
1228
|
-
log$
|
|
1236
|
+
log$35.info("LARKPAL_DISABLE_USER_AUTH 已启用,跳过飞书 user_access_token 注入", {
|
|
1229
1237
|
sessionId,
|
|
1230
1238
|
userId: userContext.userId
|
|
1231
1239
|
});
|
|
1232
1240
|
}
|
|
1233
|
-
log$
|
|
1241
|
+
log$35.info("用户凭证已注入到 CC 进程环境", {
|
|
1234
1242
|
sessionId,
|
|
1235
1243
|
userId: userContext.userId,
|
|
1236
1244
|
credentialCount: Object.keys(credentialEnvVars).length,
|
|
@@ -1238,14 +1246,14 @@ var SessionProcessManager = class {
|
|
|
1238
1246
|
disableUserAuth
|
|
1239
1247
|
});
|
|
1240
1248
|
} catch (err) {
|
|
1241
|
-
log$
|
|
1249
|
+
log$35.warn("用户凭证加载失败,CC 进程将使用默认环境", {
|
|
1242
1250
|
sessionId,
|
|
1243
1251
|
userId: userContext.userId,
|
|
1244
1252
|
error: err instanceof Error ? err.message : String(err)
|
|
1245
1253
|
});
|
|
1246
1254
|
}
|
|
1247
1255
|
}
|
|
1248
|
-
log$
|
|
1256
|
+
log$35.info("启动常驻 CC 进程", {
|
|
1249
1257
|
sessionId,
|
|
1250
1258
|
claudeSessionId,
|
|
1251
1259
|
isResuming,
|
|
@@ -1285,7 +1293,7 @@ var SessionProcessManager = class {
|
|
|
1285
1293
|
if (pending) {
|
|
1286
1294
|
clearTimeout(pending.timer);
|
|
1287
1295
|
this.pendingControlRequests.delete(msg.response.request_id);
|
|
1288
|
-
log$
|
|
1296
|
+
log$35.info("收到 control_response", {
|
|
1289
1297
|
sessionId,
|
|
1290
1298
|
requestId: msg.response.request_id,
|
|
1291
1299
|
subtype: msg.response.subtype,
|
|
@@ -1302,7 +1310,7 @@ var SessionProcessManager = class {
|
|
|
1302
1310
|
if (child.stderr) createInterface({ input: child.stderr }).on("line", (line) => {
|
|
1303
1311
|
if (line.includes("no stdin data received")) return;
|
|
1304
1312
|
if (line.toLowerCase().includes("already in use")) stderrSessionInUse = true;
|
|
1305
|
-
log$
|
|
1313
|
+
log$35.warn("CC 进程 stderr", {
|
|
1306
1314
|
sessionId,
|
|
1307
1315
|
line: line.slice(0, 500)
|
|
1308
1316
|
});
|
|
@@ -1311,7 +1319,7 @@ var SessionProcessManager = class {
|
|
|
1311
1319
|
const exitCode = code ?? -1;
|
|
1312
1320
|
const exitSignal = signal ?? "none";
|
|
1313
1321
|
if (exitCode === 1 && stderrSessionInUse && !isResuming) {
|
|
1314
|
-
log$
|
|
1322
|
+
log$35.info("Session ID 已被占用,准备使用 --resume 模式重试", {
|
|
1315
1323
|
sessionId,
|
|
1316
1324
|
claudeSessionId
|
|
1317
1325
|
});
|
|
@@ -1324,21 +1332,21 @@ var SessionProcessManager = class {
|
|
|
1324
1332
|
});
|
|
1325
1333
|
return;
|
|
1326
1334
|
} catch (retryErr) {
|
|
1327
|
-
log$
|
|
1335
|
+
log$35.error("Session ID 重试失败", {
|
|
1328
1336
|
sessionId,
|
|
1329
1337
|
error: retryErr instanceof Error ? retryErr.message : String(retryErr)
|
|
1330
1338
|
});
|
|
1331
1339
|
}
|
|
1332
1340
|
}
|
|
1333
1341
|
if (exitCode === 0) {
|
|
1334
|
-
log$
|
|
1342
|
+
log$35.info("CC 常驻进程正常退出", {
|
|
1335
1343
|
sessionId,
|
|
1336
1344
|
exitCode,
|
|
1337
1345
|
signal: exitSignal
|
|
1338
1346
|
});
|
|
1339
1347
|
ccProcess.status = "stopped";
|
|
1340
1348
|
} else {
|
|
1341
|
-
log$
|
|
1349
|
+
log$35.error("CC 常驻进程异常退出", {
|
|
1342
1350
|
sessionId,
|
|
1343
1351
|
exitCode,
|
|
1344
1352
|
signal: exitSignal
|
|
@@ -1350,7 +1358,7 @@ var SessionProcessManager = class {
|
|
|
1350
1358
|
this.cleanup(sessionId);
|
|
1351
1359
|
});
|
|
1352
1360
|
child.on("error", (err) => {
|
|
1353
|
-
log$
|
|
1361
|
+
log$35.error("CC 常驻进程启动失败", {
|
|
1354
1362
|
sessionId,
|
|
1355
1363
|
error: err.message
|
|
1356
1364
|
});
|
|
@@ -1360,7 +1368,7 @@ var SessionProcessManager = class {
|
|
|
1360
1368
|
this.cleanup(sessionId);
|
|
1361
1369
|
});
|
|
1362
1370
|
ccProcess.status = "running";
|
|
1363
|
-
log$
|
|
1371
|
+
log$35.info("CC 常驻进程已启动", {
|
|
1364
1372
|
sessionId,
|
|
1365
1373
|
pid: child.pid,
|
|
1366
1374
|
isResuming
|
|
@@ -1388,7 +1396,7 @@ var SessionProcessManager = class {
|
|
|
1388
1396
|
parser.on("textDelta", (text) => {
|
|
1389
1397
|
if (!firstTokenLogged) {
|
|
1390
1398
|
firstTokenLogged = true;
|
|
1391
|
-
log$
|
|
1399
|
+
log$35.info("[perf] 首 token (text)", {
|
|
1392
1400
|
sessionId,
|
|
1393
1401
|
ttftMs: Date.now() - messageSentAt
|
|
1394
1402
|
});
|
|
@@ -1398,7 +1406,7 @@ var SessionProcessManager = class {
|
|
|
1398
1406
|
parser.on("thinkingDelta", (text) => {
|
|
1399
1407
|
if (!firstTokenLogged) {
|
|
1400
1408
|
firstTokenLogged = true;
|
|
1401
|
-
log$
|
|
1409
|
+
log$35.info("[perf] 首 token (thinking)", {
|
|
1402
1410
|
sessionId,
|
|
1403
1411
|
ttftMs: Date.now() - messageSentAt
|
|
1404
1412
|
});
|
|
@@ -1415,7 +1423,7 @@ var SessionProcessManager = class {
|
|
|
1415
1423
|
getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds);
|
|
1416
1424
|
});
|
|
1417
1425
|
parser.on("turnEnd", (stopReason) => {
|
|
1418
|
-
log$
|
|
1426
|
+
log$35.info("[perf] turnEnd", {
|
|
1419
1427
|
sessionId,
|
|
1420
1428
|
stopReason,
|
|
1421
1429
|
sinceMessageMs: Date.now() - messageSentAt
|
|
@@ -1423,7 +1431,7 @@ var SessionProcessManager = class {
|
|
|
1423
1431
|
getCallbacks()?.onTurnEnd?.(stopReason);
|
|
1424
1432
|
});
|
|
1425
1433
|
parser.on("result", (result) => {
|
|
1426
|
-
log$
|
|
1434
|
+
log$35.info("[perf] result 事件", {
|
|
1427
1435
|
sessionId,
|
|
1428
1436
|
sinceMessageMs: Date.now() - messageSentAt
|
|
1429
1437
|
});
|
|
@@ -1431,7 +1439,7 @@ var SessionProcessManager = class {
|
|
|
1431
1439
|
this.resolveMessageCompletion(sessionId);
|
|
1432
1440
|
});
|
|
1433
1441
|
parser.on("parseError", (error, rawLine) => {
|
|
1434
|
-
log$
|
|
1442
|
+
log$35.warn("流解析错误", {
|
|
1435
1443
|
sessionId,
|
|
1436
1444
|
error: error.message,
|
|
1437
1445
|
rawLine: rawLine.slice(0, 200)
|
|
@@ -1447,7 +1455,7 @@ var SessionProcessManager = class {
|
|
|
1447
1455
|
sendMessage(sessionId, message) {
|
|
1448
1456
|
const proc = this.processes.get(sessionId);
|
|
1449
1457
|
if (!proc || !proc.childProcess.stdin) {
|
|
1450
|
-
log$
|
|
1458
|
+
log$35.error("无法发送消息:进程不存在或 stdin 不可用", { sessionId });
|
|
1451
1459
|
return;
|
|
1452
1460
|
}
|
|
1453
1461
|
proc._resetPerfTimer?.();
|
|
@@ -1463,16 +1471,19 @@ var SessionProcessManager = class {
|
|
|
1463
1471
|
uuid: userMessageId
|
|
1464
1472
|
});
|
|
1465
1473
|
const messageLength = typeof message === "string" ? message.length : message.length;
|
|
1466
|
-
|
|
1474
|
+
const hasImages = Array.isArray(message) && message.some((block) => block.type === "image");
|
|
1475
|
+
this.lastMessageHadImages.set(sessionId, hasImages);
|
|
1476
|
+
log$35.info("通过 stdin 发送用户消息", {
|
|
1467
1477
|
sessionId,
|
|
1468
1478
|
messageLength,
|
|
1469
1479
|
isMultimodal: Array.isArray(message),
|
|
1480
|
+
hasImages,
|
|
1470
1481
|
userMessageId,
|
|
1471
1482
|
pid: proc.childProcess.pid
|
|
1472
1483
|
});
|
|
1473
1484
|
proc.childProcess.stdin.write(payload + "\n", (err) => {
|
|
1474
1485
|
if (err) {
|
|
1475
|
-
log$
|
|
1486
|
+
log$35.error("stdin 写入失败", {
|
|
1476
1487
|
sessionId,
|
|
1477
1488
|
error: err.message
|
|
1478
1489
|
});
|
|
@@ -1502,7 +1513,7 @@ var SessionProcessManager = class {
|
|
|
1502
1513
|
request_id: requestId,
|
|
1503
1514
|
request
|
|
1504
1515
|
});
|
|
1505
|
-
log$
|
|
1516
|
+
log$35.info("发送控制请求", {
|
|
1506
1517
|
sessionId,
|
|
1507
1518
|
subtype,
|
|
1508
1519
|
requestId,
|
|
@@ -1558,16 +1569,16 @@ var SessionProcessManager = class {
|
|
|
1558
1569
|
async ensureProcessForRewind(sessionId) {
|
|
1559
1570
|
const existing = this.processes.get(sessionId);
|
|
1560
1571
|
if (existing && existing.status === "running") {
|
|
1561
|
-
log$
|
|
1572
|
+
log$35.info("ensureProcessForRewind: 进程已在运行中,直接使用", { sessionId });
|
|
1562
1573
|
return existing;
|
|
1563
1574
|
}
|
|
1564
1575
|
const cwd = this.sessionCwdCache.get(sessionId);
|
|
1565
1576
|
if (!cwd) {
|
|
1566
|
-
log$
|
|
1577
|
+
log$35.warn("ensureProcessForRewind: 无法获取会话 cwd", { sessionId });
|
|
1567
1578
|
return;
|
|
1568
1579
|
}
|
|
1569
1580
|
if (existing) this.cleanup(sessionId);
|
|
1570
|
-
log$
|
|
1581
|
+
log$35.info("ensureProcessForRewind: 启动临时进程用于 rewind", {
|
|
1571
1582
|
sessionId,
|
|
1572
1583
|
cwd
|
|
1573
1584
|
});
|
|
@@ -1578,7 +1589,7 @@ var SessionProcessManager = class {
|
|
|
1578
1589
|
}, { skipInitialMessage: true });
|
|
1579
1590
|
const proc = this.processes.get(sessionId);
|
|
1580
1591
|
if (!proc || proc.status !== "running") {
|
|
1581
|
-
log$
|
|
1592
|
+
log$35.warn("ensureProcessForRewind: 进程启动失败", {
|
|
1582
1593
|
sessionId,
|
|
1583
1594
|
status: proc?.status
|
|
1584
1595
|
});
|
|
@@ -1600,11 +1611,11 @@ var SessionProcessManager = class {
|
|
|
1600
1611
|
async stopProcess(sessionId) {
|
|
1601
1612
|
const proc = this.processes.get(sessionId);
|
|
1602
1613
|
if (!proc) {
|
|
1603
|
-
log$
|
|
1614
|
+
log$35.warn("尝试停止不存在的进程", { sessionId });
|
|
1604
1615
|
return;
|
|
1605
1616
|
}
|
|
1606
1617
|
if (proc.status === "stopped" || proc.status === "crashed") {
|
|
1607
|
-
log$
|
|
1618
|
+
log$35.info("进程已经停止,直接清理", {
|
|
1608
1619
|
sessionId,
|
|
1609
1620
|
status: proc.status
|
|
1610
1621
|
});
|
|
@@ -1612,7 +1623,7 @@ var SessionProcessManager = class {
|
|
|
1612
1623
|
return;
|
|
1613
1624
|
}
|
|
1614
1625
|
proc.status = "stopping";
|
|
1615
|
-
log$
|
|
1626
|
+
log$35.info("正在停止 CC 常驻进程", {
|
|
1616
1627
|
sessionId,
|
|
1617
1628
|
pid: proc.childProcess.pid
|
|
1618
1629
|
});
|
|
@@ -1628,7 +1639,7 @@ var SessionProcessManager = class {
|
|
|
1628
1639
|
const child = proc.childProcess;
|
|
1629
1640
|
const forceKillTimer = setTimeout(() => {
|
|
1630
1641
|
if (!child.killed) {
|
|
1631
|
-
log$
|
|
1642
|
+
log$35.warn("CC 常驻进程未在规定时间内退出,强制终止", { sessionId });
|
|
1632
1643
|
child.kill("SIGKILL");
|
|
1633
1644
|
}
|
|
1634
1645
|
}, GRACEFUL_SHUTDOWN_MS);
|
|
@@ -1648,9 +1659,9 @@ var SessionProcessManager = class {
|
|
|
1648
1659
|
async stopAll() {
|
|
1649
1660
|
const sessionIds = Array.from(this.processes.keys());
|
|
1650
1661
|
if (sessionIds.length === 0) return;
|
|
1651
|
-
log$
|
|
1662
|
+
log$35.info("正在停止所有 CC 常驻进程", { count: sessionIds.length });
|
|
1652
1663
|
await Promise.all(sessionIds.map((id) => this.stopProcess(id)));
|
|
1653
|
-
log$
|
|
1664
|
+
log$35.info("所有 CC 常驻进程已停止");
|
|
1654
1665
|
}
|
|
1655
1666
|
/**
|
|
1656
1667
|
* 重置空闲计时器
|
|
@@ -1660,7 +1671,7 @@ var SessionProcessManager = class {
|
|
|
1660
1671
|
if (existing) clearTimeout(existing);
|
|
1661
1672
|
if (IDLE_TIMEOUT_MS <= 0) return;
|
|
1662
1673
|
const timer = setTimeout(() => {
|
|
1663
|
-
log$
|
|
1674
|
+
log$35.info("CC 常驻进程空闲超时,自动停止", {
|
|
1664
1675
|
sessionId,
|
|
1665
1676
|
timeoutMs: IDLE_TIMEOUT_MS
|
|
1666
1677
|
});
|
|
@@ -1677,6 +1688,7 @@ var SessionProcessManager = class {
|
|
|
1677
1688
|
this.processes.delete(sessionId);
|
|
1678
1689
|
this.activeCallbacks.delete(sessionId);
|
|
1679
1690
|
this.lastUserMessageIds.delete(sessionId);
|
|
1691
|
+
this.lastMessageHadImages.delete(sessionId);
|
|
1680
1692
|
for (const [reqId, pending] of this.pendingControlRequests) if (pending.sessionId === sessionId) {
|
|
1681
1693
|
clearTimeout(pending.timer);
|
|
1682
1694
|
this.pendingControlRequests.delete(reqId);
|
|
@@ -1688,7 +1700,7 @@ var SessionProcessManager = class {
|
|
|
1688
1700
|
this.idleTimers.delete(sessionId);
|
|
1689
1701
|
}
|
|
1690
1702
|
cleanupMcpConfig(sessionId);
|
|
1691
|
-
log$
|
|
1703
|
+
log$35.debug("已清理常驻进程状态", { sessionId });
|
|
1692
1704
|
}
|
|
1693
1705
|
/**
|
|
1694
1706
|
* 将 cwd 编码为 CC 的 projects 子目录名
|
|
@@ -1717,7 +1729,7 @@ var SessionProcessManager = class {
|
|
|
1717
1729
|
const cwdEncoded = this.encodeCwdForCC(cwd);
|
|
1718
1730
|
const sessionFile = join(homedir(), ".claude", "projects", cwdEncoded, `${claudeSessionId}.jsonl`);
|
|
1719
1731
|
const exists = existsSync(sessionFile);
|
|
1720
|
-
log$
|
|
1732
|
+
log$35.debug("检查 CC session 文件", {
|
|
1721
1733
|
cwd,
|
|
1722
1734
|
cwdEncoded,
|
|
1723
1735
|
claudeSessionId,
|
|
@@ -1726,7 +1738,7 @@ var SessionProcessManager = class {
|
|
|
1726
1738
|
});
|
|
1727
1739
|
return exists;
|
|
1728
1740
|
} catch (err) {
|
|
1729
|
-
log$
|
|
1741
|
+
log$35.warn("检查 CC session 文件失败,默认为首次创建", { error: err instanceof Error ? err.message : String(err) });
|
|
1730
1742
|
return false;
|
|
1731
1743
|
}
|
|
1732
1744
|
}
|
|
@@ -1741,7 +1753,7 @@ var SessionProcessManager = class {
|
|
|
1741
1753
|
*
|
|
1742
1754
|
* 这是 LarkPal 的默认 AI 引擎适配器。
|
|
1743
1755
|
*/
|
|
1744
|
-
const log$
|
|
1756
|
+
const log$34 = larkLogger("runtime/claude-code-adapter");
|
|
1745
1757
|
var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
1746
1758
|
name = "claude-code";
|
|
1747
1759
|
processManager;
|
|
@@ -1757,7 +1769,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1757
1769
|
constructor() {
|
|
1758
1770
|
this.processManager = new SessionProcessManager();
|
|
1759
1771
|
ClaudeCodeAdapter._instance = this;
|
|
1760
|
-
log$
|
|
1772
|
+
log$34.info("ClaudeCodeAdapter 初始化完成");
|
|
1761
1773
|
}
|
|
1762
1774
|
/**
|
|
1763
1775
|
* 获取底层 SessionProcessManager 实例
|
|
@@ -1770,7 +1782,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1770
1782
|
}
|
|
1771
1783
|
async executePrompt(config, callbacks) {
|
|
1772
1784
|
this.lastRuntimeConfigs.set(config.sessionId, config);
|
|
1773
|
-
log$
|
|
1785
|
+
log$34.info("executePrompt via ClaudeCodeAdapter", {
|
|
1774
1786
|
sessionId: config.sessionId,
|
|
1775
1787
|
cwd: config.cwd,
|
|
1776
1788
|
promptLength: config.prompt.length,
|
|
@@ -1779,11 +1791,11 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1779
1791
|
await this.processManager.executePrompt(config, callbacks);
|
|
1780
1792
|
}
|
|
1781
1793
|
async stopProcess(sessionId) {
|
|
1782
|
-
log$
|
|
1794
|
+
log$34.info("stopProcess via ClaudeCodeAdapter", { sessionId });
|
|
1783
1795
|
await this.processManager.stopProcess(sessionId);
|
|
1784
1796
|
}
|
|
1785
1797
|
async stopAll() {
|
|
1786
|
-
log$
|
|
1798
|
+
log$34.info("stopAll via ClaudeCodeAdapter");
|
|
1787
1799
|
await this.processManager.stopAll();
|
|
1788
1800
|
}
|
|
1789
1801
|
getProcessInfo(sessionId) {
|
|
@@ -1819,7 +1831,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1819
1831
|
};
|
|
1820
1832
|
//#endregion
|
|
1821
1833
|
//#region src/codex-runtime/app-server-client.ts
|
|
1822
|
-
const log$
|
|
1834
|
+
const log$33 = larkLogger("codex-runtime/app-server-client");
|
|
1823
1835
|
const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
1824
1836
|
var CodexAppServerClient = class extends EventEmitter {
|
|
1825
1837
|
sessionId;
|
|
@@ -1850,7 +1862,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1850
1862
|
"pipe"
|
|
1851
1863
|
]
|
|
1852
1864
|
});
|
|
1853
|
-
log$
|
|
1865
|
+
log$33.info("Codex app-server 进程已启动", {
|
|
1854
1866
|
sessionId: this.sessionId,
|
|
1855
1867
|
cwd: this.cwd,
|
|
1856
1868
|
pid: this.childProcess.pid,
|
|
@@ -1929,14 +1941,14 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1929
1941
|
bindStreams() {
|
|
1930
1942
|
if (this.childProcess.stdout) createInterface({ input: this.childProcess.stdout }).on("line", (line) => this.handleLine(line));
|
|
1931
1943
|
if (this.childProcess.stderr) createInterface({ input: this.childProcess.stderr }).on("line", (line) => {
|
|
1932
|
-
log$
|
|
1944
|
+
log$33.warn("Codex app-server stderr", {
|
|
1933
1945
|
sessionId: this.sessionId,
|
|
1934
1946
|
line: line.slice(0, 500)
|
|
1935
1947
|
});
|
|
1936
1948
|
this.emit("stderr", line);
|
|
1937
1949
|
});
|
|
1938
1950
|
this.childProcess.on("error", (error) => {
|
|
1939
|
-
log$
|
|
1951
|
+
log$33.error("Codex app-server 进程错误", {
|
|
1940
1952
|
sessionId: this.sessionId,
|
|
1941
1953
|
error: error.message
|
|
1942
1954
|
});
|
|
@@ -1945,7 +1957,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1945
1957
|
});
|
|
1946
1958
|
this.childProcess.on("close", (code, signal) => {
|
|
1947
1959
|
this.closed = true;
|
|
1948
|
-
log$
|
|
1960
|
+
log$33.info("Codex app-server 进程退出", {
|
|
1949
1961
|
sessionId: this.sessionId,
|
|
1950
1962
|
code,
|
|
1951
1963
|
signal
|
|
@@ -1962,7 +1974,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1962
1974
|
message = JSON.parse(trimmed);
|
|
1963
1975
|
} catch (err) {
|
|
1964
1976
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1965
|
-
log$
|
|
1977
|
+
log$33.warn("Codex app-server JSONL 解析失败", {
|
|
1966
1978
|
sessionId: this.sessionId,
|
|
1967
1979
|
error: error.message,
|
|
1968
1980
|
line: trimmed.slice(0, 200)
|
|
@@ -1982,7 +1994,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1982
1994
|
}
|
|
1983
1995
|
handleResponse(message) {
|
|
1984
1996
|
if (message.id == null) {
|
|
1985
|
-
log$
|
|
1997
|
+
log$33.warn("Codex app-server 返回无 id 错误响应", {
|
|
1986
1998
|
sessionId: this.sessionId,
|
|
1987
1999
|
message
|
|
1988
2000
|
});
|
|
@@ -1991,7 +2003,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1991
2003
|
const idKey = String(message.id);
|
|
1992
2004
|
const pending = this.pending.get(idKey);
|
|
1993
2005
|
if (!pending) {
|
|
1994
|
-
log$
|
|
2006
|
+
log$33.warn("Codex app-server 返回未知请求响应", {
|
|
1995
2007
|
sessionId: this.sessionId,
|
|
1996
2008
|
id: message.id
|
|
1997
2009
|
});
|
|
@@ -2037,7 +2049,7 @@ function isNotification(message) {
|
|
|
2037
2049
|
}
|
|
2038
2050
|
//#endregion
|
|
2039
2051
|
//#region src/runtime/codex-app-server-adapter.ts
|
|
2040
|
-
const log$
|
|
2052
|
+
const log$32 = larkLogger("runtime/codex-app-server-adapter");
|
|
2041
2053
|
const STATE_PATH = join(homedir(), ".larkpal", "runtime", "codex-app-server-threads.json");
|
|
2042
2054
|
const REQUEST_TIMEOUT_MS = 12e4;
|
|
2043
2055
|
var CodexAppServerAdapter = class {
|
|
@@ -2063,14 +2075,14 @@ var CodexAppServerAdapter = class {
|
|
|
2063
2075
|
extraArgs: params.extraArgs,
|
|
2064
2076
|
requestTimeoutMs: REQUEST_TIMEOUT_MS
|
|
2065
2077
|
}));
|
|
2066
|
-
log$
|
|
2078
|
+
log$32.info("CodexAppServerAdapter 初始化完成", { statePath: this.statePath });
|
|
2067
2079
|
}
|
|
2068
2080
|
async executePrompt(config, callbacks) {
|
|
2069
2081
|
await this.loadState();
|
|
2070
2082
|
const { sessionId } = config;
|
|
2071
2083
|
const prevLock = this.sessionLocks.get(sessionId);
|
|
2072
2084
|
if (prevLock) {
|
|
2073
|
-
log$
|
|
2085
|
+
log$32.info("等待上一条 Codex 消息处理完成", { sessionId });
|
|
2074
2086
|
await prevLock;
|
|
2075
2087
|
}
|
|
2076
2088
|
const completion = this.createCompletionPromise(sessionId);
|
|
@@ -2101,7 +2113,7 @@ var CodexAppServerAdapter = class {
|
|
|
2101
2113
|
if (config.transcriptMode === "ephemeral") {
|
|
2102
2114
|
const record = this.processes.get(sessionId);
|
|
2103
2115
|
if (record) await this.closeRecord(sessionId, record).catch((err) => {
|
|
2104
|
-
log$
|
|
2116
|
+
log$32.warn("ephemeral Codex app-server session 清理失败", {
|
|
2105
2117
|
sessionId,
|
|
2106
2118
|
error: err instanceof Error ? err.message : String(err)
|
|
2107
2119
|
});
|
|
@@ -2116,12 +2128,12 @@ var CodexAppServerAdapter = class {
|
|
|
2116
2128
|
async stopProcess(sessionId) {
|
|
2117
2129
|
const record = this.processes.get(sessionId);
|
|
2118
2130
|
if (!record) {
|
|
2119
|
-
log$
|
|
2131
|
+
log$32.warn("尝试停止不存在的 Codex app-server session", { sessionId });
|
|
2120
2132
|
return;
|
|
2121
2133
|
}
|
|
2122
2134
|
if (this.busySessions.has(sessionId) && record.process.currentTurnId) {
|
|
2123
2135
|
this.abortedSessions.add(sessionId);
|
|
2124
|
-
log$
|
|
2136
|
+
log$32.info("发送 Codex turn/interrupt", {
|
|
2125
2137
|
sessionId,
|
|
2126
2138
|
threadId: record.process.threadId,
|
|
2127
2139
|
turnId: record.process.currentTurnId
|
|
@@ -2208,7 +2220,7 @@ var CodexAppServerAdapter = class {
|
|
|
2208
2220
|
if (!isEphemeral) await this.setThreadId(config.sessionId, resumed.thread.id, config.cwd);
|
|
2209
2221
|
return;
|
|
2210
2222
|
} catch (err) {
|
|
2211
|
-
log$
|
|
2223
|
+
log$32.warn("Codex thread/resume 失败,将新建 thread", {
|
|
2212
2224
|
sessionId: config.sessionId,
|
|
2213
2225
|
threadId: storedThreadId,
|
|
2214
2226
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2274,7 +2286,7 @@ var CodexAppServerAdapter = class {
|
|
|
2274
2286
|
this.handleErrorNotification(sessionId, message.params);
|
|
2275
2287
|
break;
|
|
2276
2288
|
default:
|
|
2277
|
-
log$
|
|
2289
|
+
log$32.debug("忽略 Codex app-server notification", {
|
|
2278
2290
|
sessionId,
|
|
2279
2291
|
method: message.method
|
|
2280
2292
|
});
|
|
@@ -2302,7 +2314,7 @@ var CodexAppServerAdapter = class {
|
|
|
2302
2314
|
handleMcpProgress(sessionId, params) {
|
|
2303
2315
|
const toolName = this.lookupToolName(sessionId, params.itemId) ?? "mcp";
|
|
2304
2316
|
this.activeCallbacks.get(sessionId)?.onToolProgress?.(toolName, 0);
|
|
2305
|
-
log$
|
|
2317
|
+
log$32.debug("Codex MCP tool progress", {
|
|
2306
2318
|
sessionId,
|
|
2307
2319
|
itemId: params.itemId,
|
|
2308
2320
|
message: params.message
|
|
@@ -2332,7 +2344,7 @@ var CodexAppServerAdapter = class {
|
|
|
2332
2344
|
this.resolveCompletion(sessionId);
|
|
2333
2345
|
}
|
|
2334
2346
|
async handleServerRequest(client, request) {
|
|
2335
|
-
log$
|
|
2347
|
+
log$32.warn("Codex app-server server request 默认拒绝", {
|
|
2336
2348
|
method: request.method,
|
|
2337
2349
|
id: request.id
|
|
2338
2350
|
});
|
|
@@ -2498,10 +2510,10 @@ var CodexAppServerAdapter = class {
|
|
|
2498
2510
|
];
|
|
2499
2511
|
}
|
|
2500
2512
|
logUnsupportedConfig(config) {
|
|
2501
|
-
if (config.llmConfig) log$
|
|
2502
|
-
if (config.maxBudgetUsd != null) log$
|
|
2503
|
-
if (config.maxTurns != null) log$
|
|
2504
|
-
if (config.policy?.allowedTools || config.policy?.disallowedTools) log$
|
|
2513
|
+
if (config.llmConfig) log$32.warn("Codex app-server v1 暂不支持 per-request llmConfig,已忽略", { sessionId: config.sessionId });
|
|
2514
|
+
if (config.maxBudgetUsd != null) log$32.warn("Codex app-server v1 暂不支持 maxBudgetUsd,已忽略", { sessionId: config.sessionId });
|
|
2515
|
+
if (config.maxTurns != null) log$32.warn("Codex app-server v1 暂不支持 maxTurns,已忽略", { sessionId: config.sessionId });
|
|
2516
|
+
if (config.policy?.allowedTools || config.policy?.disallowedTools) log$32.warn("Codex app-server v1 暂不强制执行 tool allow/deny policy,已忽略", { sessionId: config.sessionId });
|
|
2505
2517
|
}
|
|
2506
2518
|
createCompletionPromise(sessionId) {
|
|
2507
2519
|
return new Promise((resolve) => {
|
|
@@ -2517,7 +2529,7 @@ var CodexAppServerAdapter = class {
|
|
|
2517
2529
|
async closeRecord(sessionId, record) {
|
|
2518
2530
|
record.process.status = "stopping";
|
|
2519
2531
|
if (record.process.threadId) await record.client.request("thread/unsubscribe", { threadId: record.process.threadId }).catch((err) => {
|
|
2520
|
-
log$
|
|
2532
|
+
log$32.warn("Codex thread/unsubscribe 失败", {
|
|
2521
2533
|
sessionId,
|
|
2522
2534
|
threadId: record.process.threadId,
|
|
2523
2535
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2550,7 +2562,7 @@ var CodexAppServerAdapter = class {
|
|
|
2550
2562
|
const parsed = JSON.parse(raw);
|
|
2551
2563
|
for (const [sessionId, entry] of Object.entries(parsed.sessions ?? {})) if (entry.threadId) this.threadIds.set(sessionId, entry.threadId);
|
|
2552
2564
|
} catch (err) {
|
|
2553
|
-
log$
|
|
2565
|
+
log$32.warn("Codex thread state 加载失败,将从空状态继续", {
|
|
2554
2566
|
path: this.statePath,
|
|
2555
2567
|
error: err instanceof Error ? err.message : String(err)
|
|
2556
2568
|
});
|
|
@@ -2618,18 +2630,18 @@ function getLarkpalMcpServerCommand() {
|
|
|
2618
2630
|
*
|
|
2619
2631
|
* 仅当 LARKPAL_RUNTIME=larkpal-agent 时使用。
|
|
2620
2632
|
*/
|
|
2621
|
-
const log$
|
|
2633
|
+
const log$31 = larkLogger("runtime/larkpal-agent-factory");
|
|
2622
2634
|
const AGENT_MD_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
2623
2635
|
async function loadSystemPrompt() {
|
|
2624
2636
|
try {
|
|
2625
2637
|
const content = await readFile(AGENT_MD_PATH, "utf-8");
|
|
2626
|
-
log$
|
|
2638
|
+
log$31.info("systemPrompt 加载成功", {
|
|
2627
2639
|
path: AGENT_MD_PATH,
|
|
2628
2640
|
length: content.length
|
|
2629
2641
|
});
|
|
2630
2642
|
return content;
|
|
2631
2643
|
} catch {
|
|
2632
|
-
log$
|
|
2644
|
+
log$31.warn("systemPrompt 文件不存在,使用默认", { path: AGENT_MD_PATH });
|
|
2633
2645
|
return "你是一个 AI 助手,帮助用户完成飞书知识管理任务。";
|
|
2634
2646
|
}
|
|
2635
2647
|
}
|
|
@@ -2745,7 +2757,7 @@ async function createLarkpalAgentAdapter() {
|
|
|
2745
2757
|
const scenarioManifestDirs = loadScenarioManifestDirs();
|
|
2746
2758
|
const scenarioProviderConfig = loadScenarioProviderConfig(scenarioManifestDirs);
|
|
2747
2759
|
const mcpTimeoutMs = loadAgentMcpTimeoutMs();
|
|
2748
|
-
log$
|
|
2760
|
+
log$31.info("创建 LarkpalAgentAdapter", {
|
|
2749
2761
|
baseURL: llm.baseURL,
|
|
2750
2762
|
model: llm.defaultModel,
|
|
2751
2763
|
maxTurns: defaultMaxTurns,
|
|
@@ -2772,19 +2784,19 @@ async function createLarkpalAgentAdapter() {
|
|
|
2772
2784
|
const toolCount = await registry.connectMcpServer(mcpServerUrl, { timeoutMs: mcpTimeoutMs });
|
|
2773
2785
|
const tools = registry.getAll();
|
|
2774
2786
|
adapter.registerTools(tools);
|
|
2775
|
-
log$
|
|
2787
|
+
log$31.info("MCP 工具注册完成", {
|
|
2776
2788
|
serverUrl: mcpServerUrl,
|
|
2777
2789
|
timeoutMs: mcpTimeoutMs,
|
|
2778
2790
|
toolCount,
|
|
2779
2791
|
toolNames: tools.map((t) => t.name)
|
|
2780
2792
|
});
|
|
2781
2793
|
} catch (err) {
|
|
2782
|
-
log$
|
|
2794
|
+
log$31.error("MCP Server 连接失败,Agent 将无法使用远程工具", {
|
|
2783
2795
|
serverUrl: mcpServerUrl,
|
|
2784
2796
|
error: err?.message
|
|
2785
2797
|
});
|
|
2786
2798
|
}
|
|
2787
|
-
else log$
|
|
2799
|
+
else log$31.warn("未配置 LARKPAL_AGENT_MCP_SERVER_URL,Agent 无远程工具可用");
|
|
2788
2800
|
return wrapLarkpalAgentAdapter(adapter);
|
|
2789
2801
|
}
|
|
2790
2802
|
function wrapLarkpalAgentAdapter(adapter) {
|
|
@@ -3497,7 +3509,7 @@ function summarizeString(value) {
|
|
|
3497
3509
|
* POST /api/chat/sessions — 创建新会话
|
|
3498
3510
|
* DELETE /api/chat/sessions/:id — 删除会话
|
|
3499
3511
|
*/
|
|
3500
|
-
const log$
|
|
3512
|
+
const log$30 = larkLogger("chat/stream");
|
|
3501
3513
|
const chatRunsBySession = /* @__PURE__ */ new Map();
|
|
3502
3514
|
const DEFAULT_STALE_RUN_IDLE_MS = 1800 * 1e3;
|
|
3503
3515
|
/** 统一写 SSE 格式数据到响应流 */
|
|
@@ -3509,7 +3521,7 @@ function isChatSessionRunning(sessionId, processManager) {
|
|
|
3509
3521
|
return getActiveChatRun(sessionId) != null || (processManager.isSessionBusy?.(sessionId) ?? false);
|
|
3510
3522
|
}
|
|
3511
3523
|
function logChatAudit(event) {
|
|
3512
|
-
log$
|
|
3524
|
+
log$30.info("[stream] Agent audit event", { ...event });
|
|
3513
3525
|
}
|
|
3514
3526
|
function prepareSSE(res) {
|
|
3515
3527
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -3560,7 +3572,7 @@ function addRunClient(req, res, run) {
|
|
|
3560
3572
|
run.clients.add(res);
|
|
3561
3573
|
req.on("close", () => {
|
|
3562
3574
|
run.clients.delete(res);
|
|
3563
|
-
log$
|
|
3575
|
+
log$30.info("[stream] 客户端断开连接", {
|
|
3564
3576
|
sessionId: run.sessionId,
|
|
3565
3577
|
runId: run.runId
|
|
3566
3578
|
});
|
|
@@ -3599,7 +3611,7 @@ function broadcastRunEvent(run, event, data) {
|
|
|
3599
3611
|
sendSSE(client, event, data);
|
|
3600
3612
|
} catch (err) {
|
|
3601
3613
|
run.clients.delete(client);
|
|
3602
|
-
log$
|
|
3614
|
+
log$30.warn("[stream] SSE 写入失败,移除客户端", {
|
|
3603
3615
|
sessionId: run.sessionId,
|
|
3604
3616
|
runId: run.runId,
|
|
3605
3617
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3619,7 +3631,7 @@ function queueRunMessage(run, messageStore, message, label) {
|
|
|
3619
3631
|
run.persistQueue = run.persistQueue.catch(() => void 0).then(async () => {
|
|
3620
3632
|
await messageStore.appendMessage(message);
|
|
3621
3633
|
}).catch((err) => {
|
|
3622
|
-
log$
|
|
3634
|
+
log$30.error(label, {
|
|
3623
3635
|
sessionId: run.sessionId,
|
|
3624
3636
|
runId: run.runId,
|
|
3625
3637
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3672,7 +3684,7 @@ async function reconcileRunWithAdapter(run, messageStore, processManager) {
|
|
|
3672
3684
|
else if (idleMs >= getStaleRunIdleMs()) staleReason = `run idle for ${Math.floor(idleMs / 1e3)}s`;
|
|
3673
3685
|
if (!staleReason) return;
|
|
3674
3686
|
const error = `Chat run became stale: ${staleReason}`;
|
|
3675
|
-
log$
|
|
3687
|
+
log$30.warn("[stream] activeRun 与 runtime 状态不一致,标记为 stale", {
|
|
3676
3688
|
sessionId: run.sessionId,
|
|
3677
3689
|
runId: run.runId,
|
|
3678
3690
|
staleReason,
|
|
@@ -3750,7 +3762,7 @@ function createChatRouter(config) {
|
|
|
3750
3762
|
addRunClient(req, res, activeRun);
|
|
3751
3763
|
sendSSE(res, "session", { sessionId });
|
|
3752
3764
|
sendRunStatus(res, activeRun);
|
|
3753
|
-
log$
|
|
3765
|
+
log$30.info("[stream] 已重新连接到运行中的 run", {
|
|
3754
3766
|
sessionId,
|
|
3755
3767
|
runId: activeRun.runId
|
|
3756
3768
|
});
|
|
@@ -3763,7 +3775,7 @@ function createChatRouter(config) {
|
|
|
3763
3775
|
const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
|
|
3764
3776
|
const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
|
|
3765
3777
|
const sourceArtifacts = getRunInputValue(body.sourceArtifacts, body.source_artifacts, body.options?.sourceArtifacts, body.options?.source_artifacts, body.options?.metadata?.sourceArtifacts, body.options?.metadata?.source_artifacts);
|
|
3766
|
-
log$
|
|
3778
|
+
log$30.info("[stream] 收到流式对话请求", {
|
|
3767
3779
|
userId,
|
|
3768
3780
|
sessionId,
|
|
3769
3781
|
requestId,
|
|
@@ -3780,13 +3792,13 @@ function createChatRouter(config) {
|
|
|
3780
3792
|
tenantKey,
|
|
3781
3793
|
channel: "web"
|
|
3782
3794
|
})).id;
|
|
3783
|
-
log$
|
|
3795
|
+
log$30.info("[stream] 自动创建新会话", {
|
|
3784
3796
|
userId: openId,
|
|
3785
3797
|
sessionId
|
|
3786
3798
|
});
|
|
3787
3799
|
}
|
|
3788
3800
|
if (isChatSessionRunning(sessionId, processManager)) {
|
|
3789
|
-
log$
|
|
3801
|
+
log$30.warn("[stream] session 正在被非 Web Chat run 占用", {
|
|
3790
3802
|
tenantKey,
|
|
3791
3803
|
userId,
|
|
3792
3804
|
sessionId
|
|
@@ -3797,7 +3809,6 @@ function createChatRouter(config) {
|
|
|
3797
3809
|
res.end();
|
|
3798
3810
|
return;
|
|
3799
3811
|
}
|
|
3800
|
-
const runtimePrompt = enrichPromptWithSourceArtifacts(body.prompt, sourceArtifacts);
|
|
3801
3812
|
const runtimeConfig = buildAgentRuntimeConfig({
|
|
3802
3813
|
requestId,
|
|
3803
3814
|
traceId,
|
|
@@ -3805,7 +3816,7 @@ function createChatRouter(config) {
|
|
|
3805
3816
|
conversationId: sessionId,
|
|
3806
3817
|
entrypoint: "chat",
|
|
3807
3818
|
scenarioId,
|
|
3808
|
-
prompt:
|
|
3819
|
+
prompt: body.prompt,
|
|
3809
3820
|
maxTurns: body.options?.maxTurns,
|
|
3810
3821
|
maxBudgetUsd: body.options?.maxBudgetUsd,
|
|
3811
3822
|
model: process.env.CLAUDE_MODEL || void 0,
|
|
@@ -3858,7 +3869,7 @@ function createChatRouter(config) {
|
|
|
3858
3869
|
});
|
|
3859
3870
|
} catch (err) {
|
|
3860
3871
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3861
|
-
log$
|
|
3872
|
+
log$30.error("[stream] 保存用户消息失败", {
|
|
3862
3873
|
sessionId,
|
|
3863
3874
|
error: errorMessage
|
|
3864
3875
|
});
|
|
@@ -3881,7 +3892,7 @@ function createChatRouter(config) {
|
|
|
3881
3892
|
const handleBlockedEvent = (event) => {
|
|
3882
3893
|
const sanitized = sanitizeBlockedEvent(event);
|
|
3883
3894
|
recordRunEvent(run, "blocked", { toolName: sanitized.name });
|
|
3884
|
-
log$
|
|
3895
|
+
log$30.warn("[stream] Agent blocked event", { ...sanitized });
|
|
3885
3896
|
if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, sanitized.type, {
|
|
3886
3897
|
toolName: sanitized.name,
|
|
3887
3898
|
inputSummary: sanitized.inputSummary,
|
|
@@ -3914,7 +3925,7 @@ function createChatRouter(config) {
|
|
|
3914
3925
|
const dedupeKey = getRuntimeEventDedupeKey(event);
|
|
3915
3926
|
if (dedupeKey) {
|
|
3916
3927
|
if (run.seenRuntimeEventIds.has(dedupeKey)) {
|
|
3917
|
-
log$
|
|
3928
|
+
log$30.debug("[stream] 跳过重复 runtime event", {
|
|
3918
3929
|
sessionId,
|
|
3919
3930
|
runId: run.runId,
|
|
3920
3931
|
eventId: dedupeKey,
|
|
@@ -4064,7 +4075,7 @@ function createChatRouter(config) {
|
|
|
4064
4075
|
});
|
|
4065
4076
|
clearInterval(heartbeatTimer);
|
|
4066
4077
|
endRunClients(run);
|
|
4067
|
-
log$
|
|
4078
|
+
log$30.info("[stream] 对话完成", {
|
|
4068
4079
|
sessionId,
|
|
4069
4080
|
runId: run.runId,
|
|
4070
4081
|
result: result.subtype,
|
|
@@ -4087,7 +4098,7 @@ function createChatRouter(config) {
|
|
|
4087
4098
|
},
|
|
4088
4099
|
onError: async (error) => {
|
|
4089
4100
|
recordRunEvent(run, "error");
|
|
4090
|
-
log$
|
|
4101
|
+
log$30.error("[stream] Runtime 执行出错", {
|
|
4091
4102
|
sessionId,
|
|
4092
4103
|
runId: run.runId,
|
|
4093
4104
|
error: error.message
|
|
@@ -4175,7 +4186,7 @@ function createChatRouter(config) {
|
|
|
4175
4186
|
scenarioId: runtimeConfig.runContext?.scenarioId,
|
|
4176
4187
|
...buildRunInputAuditMetadata(runtimeConfig)
|
|
4177
4188
|
} }));
|
|
4178
|
-
log$
|
|
4189
|
+
log$30.info("[stream] 开始执行 prompt", {
|
|
4179
4190
|
sessionId,
|
|
4180
4191
|
runId: run.runId,
|
|
4181
4192
|
cwd: runtimeConfig.cwd
|
|
@@ -4183,7 +4194,7 @@ function createChatRouter(config) {
|
|
|
4183
4194
|
await processManager.executePrompt(runtimeConfig, callbacks);
|
|
4184
4195
|
if (run.status === "running") {
|
|
4185
4196
|
const errorMessage = "Runtime completed without a final result event";
|
|
4186
|
-
log$
|
|
4197
|
+
log$30.error("[stream] executePrompt 未返回终态回调", {
|
|
4187
4198
|
sessionId,
|
|
4188
4199
|
runId: run.runId
|
|
4189
4200
|
});
|
|
@@ -4200,7 +4211,7 @@ function createChatRouter(config) {
|
|
|
4200
4211
|
}
|
|
4201
4212
|
} catch (err) {
|
|
4202
4213
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4203
|
-
log$
|
|
4214
|
+
log$30.error("[stream] executePrompt 异常", {
|
|
4204
4215
|
sessionId,
|
|
4205
4216
|
runId: run.runId,
|
|
4206
4217
|
error: errorMessage
|
|
@@ -4217,7 +4228,7 @@ function createChatRouter(config) {
|
|
|
4217
4228
|
}
|
|
4218
4229
|
const cursor = req.query.cursor;
|
|
4219
4230
|
const limit = parseInt(req.query.limit, 10) || 50;
|
|
4220
|
-
log$
|
|
4231
|
+
log$30.info("[history] 查询会话历史", {
|
|
4221
4232
|
userId: chatUser.userId,
|
|
4222
4233
|
sessionId,
|
|
4223
4234
|
cursor,
|
|
@@ -4243,7 +4254,7 @@ function createChatRouter(config) {
|
|
|
4243
4254
|
});
|
|
4244
4255
|
} catch (err) {
|
|
4245
4256
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4246
|
-
log$
|
|
4257
|
+
log$30.error("[history] 查询历史消息失败", {
|
|
4247
4258
|
sessionId,
|
|
4248
4259
|
error: errorMessage
|
|
4249
4260
|
});
|
|
@@ -4279,13 +4290,13 @@ function createChatRouter(config) {
|
|
|
4279
4290
|
});
|
|
4280
4291
|
router.get("/api/chat/sessions", chatAuthMiddleware, async (_req, res) => {
|
|
4281
4292
|
const chatUser = res.locals.chatUser;
|
|
4282
|
-
log$
|
|
4293
|
+
log$30.info("[sessions] 列出用户会话", { userId: chatUser.userId });
|
|
4283
4294
|
try {
|
|
4284
4295
|
const sessions = await messageStore.listSessions(chatUser.openId);
|
|
4285
4296
|
res.json({ sessions });
|
|
4286
4297
|
} catch (err) {
|
|
4287
4298
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4288
|
-
log$
|
|
4299
|
+
log$30.error("[sessions] 列出会话失败", {
|
|
4289
4300
|
userId: chatUser.userId,
|
|
4290
4301
|
error: errorMessage
|
|
4291
4302
|
});
|
|
@@ -4295,7 +4306,7 @@ function createChatRouter(config) {
|
|
|
4295
4306
|
router.post("/api/chat/sessions", chatAuthMiddleware, async (req, res) => {
|
|
4296
4307
|
const chatUser = res.locals.chatUser;
|
|
4297
4308
|
const body = req.body;
|
|
4298
|
-
log$
|
|
4309
|
+
log$30.info("[sessions] 创建新会话", {
|
|
4299
4310
|
userId: chatUser.userId,
|
|
4300
4311
|
title: body.title
|
|
4301
4312
|
});
|
|
@@ -4310,7 +4321,7 @@ function createChatRouter(config) {
|
|
|
4310
4321
|
res.json({ session });
|
|
4311
4322
|
} catch (err) {
|
|
4312
4323
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4313
|
-
log$
|
|
4324
|
+
log$30.error("[sessions] 创建会话失败", {
|
|
4314
4325
|
userId: chatUser.userId,
|
|
4315
4326
|
error: errorMessage
|
|
4316
4327
|
});
|
|
@@ -4320,7 +4331,7 @@ function createChatRouter(config) {
|
|
|
4320
4331
|
router.delete("/api/chat/sessions/:id", chatAuthMiddleware, async (req, res) => {
|
|
4321
4332
|
const chatUser = res.locals.chatUser;
|
|
4322
4333
|
const sessionId = String(req.params.id);
|
|
4323
|
-
log$
|
|
4334
|
+
log$30.info("[sessions] 删除会话", {
|
|
4324
4335
|
userId: chatUser.userId,
|
|
4325
4336
|
sessionId
|
|
4326
4337
|
});
|
|
@@ -4330,7 +4341,7 @@ function createChatRouter(config) {
|
|
|
4330
4341
|
res.json({ success: true });
|
|
4331
4342
|
} catch (err) {
|
|
4332
4343
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4333
|
-
log$
|
|
4344
|
+
log$30.error("[sessions] 删除会话失败", {
|
|
4334
4345
|
sessionId,
|
|
4335
4346
|
error: errorMessage
|
|
4336
4347
|
});
|
|
@@ -4339,34 +4350,6 @@ function createChatRouter(config) {
|
|
|
4339
4350
|
});
|
|
4340
4351
|
return router;
|
|
4341
4352
|
}
|
|
4342
|
-
/**
|
|
4343
|
-
* 将 sourceArtifacts 中的文件信息(fileToken、fileName 等)注入到用户 prompt 中,
|
|
4344
|
-
* 使 LLM 能看到实际的 file token 值并直接用于下载。
|
|
4345
|
-
*
|
|
4346
|
-
* 如果没有 sourceArtifacts 或无法提取有效信息,返回原始 prompt。
|
|
4347
|
-
*/
|
|
4348
|
-
function enrichPromptWithSourceArtifacts(prompt, sourceArtifacts) {
|
|
4349
|
-
if (!sourceArtifacts) return prompt;
|
|
4350
|
-
const lines = [];
|
|
4351
|
-
const extractInfo = (artifact) => {
|
|
4352
|
-
if (!artifact || typeof artifact !== "object") return;
|
|
4353
|
-
const record = artifact;
|
|
4354
|
-
const fileToken = record.fileToken ?? record.file_token ?? record.driveFileToken ?? record.drive_file_token;
|
|
4355
|
-
const fileName = record.fileName ?? record.file_name ?? record.name ?? record.title;
|
|
4356
|
-
const fileType = record.fileType ?? record.file_type ?? record.type ?? record.mimeType;
|
|
4357
|
-
const fileUrl = record.fileUrl ?? record.file_url ?? record.url ?? record.uri;
|
|
4358
|
-
if (fileToken || fileName || fileUrl) {
|
|
4359
|
-
if (fileName) lines.push(`- fileName: ${String(fileName)}`);
|
|
4360
|
-
if (fileToken) lines.push(`- fileToken: ${String(fileToken)}`);
|
|
4361
|
-
if (fileType) lines.push(`- fileType: ${String(fileType)}`);
|
|
4362
|
-
if (fileUrl) lines.push(`- fileUrl: ${String(fileUrl)}`);
|
|
4363
|
-
}
|
|
4364
|
-
};
|
|
4365
|
-
if (Array.isArray(sourceArtifacts)) for (const item of sourceArtifacts) extractInfo(item);
|
|
4366
|
-
else extractInfo(sourceArtifacts);
|
|
4367
|
-
if (lines.length === 0) return prompt;
|
|
4368
|
-
return `${prompt}\n\n[Attached Source File]\n${lines.join("\n")}`;
|
|
4369
|
-
}
|
|
4370
4353
|
//#endregion
|
|
4371
4354
|
//#region src/core/app-info-sync.ts
|
|
4372
4355
|
/**
|
|
@@ -4384,7 +4367,7 @@ function enrichPromptWithSourceArtifacts(prompt, sourceArtifacts) {
|
|
|
4384
4367
|
* - GET /open-apis/application/v6/applications/me?lang=zh_cn(需要 application:application:self_manage 权限,返回完整信息)
|
|
4385
4368
|
* 如果后者无权限则 fallback 到前者
|
|
4386
4369
|
*/
|
|
4387
|
-
const log$
|
|
4370
|
+
const log$29 = larkLogger("core/app-info-sync");
|
|
4388
4371
|
/**
|
|
4389
4372
|
* 从飞书获取应用信息
|
|
4390
4373
|
*
|
|
@@ -4395,7 +4378,7 @@ async function fetchAppInfo(credentials) {
|
|
|
4395
4378
|
const { appId, appSecret } = credentials;
|
|
4396
4379
|
const token = await getTenantAccessToken(appId, appSecret);
|
|
4397
4380
|
if (!token) {
|
|
4398
|
-
log$
|
|
4381
|
+
log$29.error("获取 tenant_access_token 失败,无法同步应用信息");
|
|
4399
4382
|
return null;
|
|
4400
4383
|
}
|
|
4401
4384
|
const fullInfo = await fetchFromApplicationApi(token);
|
|
@@ -4406,13 +4389,13 @@ async function fetchAppInfo(credentials) {
|
|
|
4406
4389
|
async function fetchFromApplicationApi(token) {
|
|
4407
4390
|
try {
|
|
4408
4391
|
const data = await (await fetch("https://open.feishu.cn/open-apis/application/v6/applications/me?lang=zh_cn", { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
4409
|
-
log$
|
|
4392
|
+
log$29.info("application/v6 API 响应", {
|
|
4410
4393
|
code: data.code,
|
|
4411
4394
|
msg: data.msg,
|
|
4412
4395
|
hasApp: !!data.data?.app
|
|
4413
4396
|
});
|
|
4414
4397
|
if (data.code !== 0 || !data.data?.app) {
|
|
4415
|
-
log$
|
|
4398
|
+
log$29.warn("application/v6 API 返回非零或无数据,将 fallback", {
|
|
4416
4399
|
code: data.code,
|
|
4417
4400
|
msg: data.msg
|
|
4418
4401
|
});
|
|
@@ -4427,7 +4410,7 @@ async function fetchFromApplicationApi(token) {
|
|
|
4427
4410
|
helpDocUrl: zhInfo?.help_use
|
|
4428
4411
|
};
|
|
4429
4412
|
} catch (err) {
|
|
4430
|
-
log$
|
|
4413
|
+
log$29.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4431
4414
|
return null;
|
|
4432
4415
|
}
|
|
4433
4416
|
}
|
|
@@ -4435,13 +4418,13 @@ async function fetchFromApplicationApi(token) {
|
|
|
4435
4418
|
async function fetchFromBotApi(token) {
|
|
4436
4419
|
try {
|
|
4437
4420
|
const data = await (await fetch("https://open.feishu.cn/open-apis/bot/v3/info", { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
4438
|
-
log$
|
|
4421
|
+
log$29.info("bot/v3/info API 响应", {
|
|
4439
4422
|
code: data.code,
|
|
4440
4423
|
msg: data.msg,
|
|
4441
4424
|
hasBot: !!data.bot
|
|
4442
4425
|
});
|
|
4443
4426
|
if (data.code !== 0 || !data.bot) {
|
|
4444
|
-
log$
|
|
4427
|
+
log$29.error("bot/v3/info API 失败", {
|
|
4445
4428
|
code: data.code,
|
|
4446
4429
|
msg: data.msg
|
|
4447
4430
|
});
|
|
@@ -4452,7 +4435,7 @@ async function fetchFromBotApi(token) {
|
|
|
4452
4435
|
avatarUrl: data.bot.avatar_url
|
|
4453
4436
|
};
|
|
4454
4437
|
} catch (err) {
|
|
4455
|
-
log$
|
|
4438
|
+
log$29.error("bot/v3/info API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4456
4439
|
return null;
|
|
4457
4440
|
}
|
|
4458
4441
|
}
|
|
@@ -4468,7 +4451,7 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
4468
4451
|
})
|
|
4469
4452
|
})).json();
|
|
4470
4453
|
if (data.code !== 0 || !data.tenant_access_token) {
|
|
4471
|
-
log$
|
|
4454
|
+
log$29.error("获取 tenant_access_token 失败", {
|
|
4472
4455
|
code: data.code,
|
|
4473
4456
|
msg: data.msg
|
|
4474
4457
|
});
|
|
@@ -4476,7 +4459,7 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
4476
4459
|
}
|
|
4477
4460
|
return data.tenant_access_token;
|
|
4478
4461
|
} catch (err) {
|
|
4479
|
-
log$
|
|
4462
|
+
log$29.error("获取 tenant_access_token 异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4480
4463
|
return null;
|
|
4481
4464
|
}
|
|
4482
4465
|
}
|
|
@@ -4514,10 +4497,10 @@ function parseDocTokenFromUrl(url) {
|
|
|
4514
4497
|
async function fetchDocContent(docUrl, accessToken) {
|
|
4515
4498
|
const parsed = parseDocTokenFromUrl(docUrl);
|
|
4516
4499
|
if (!parsed) {
|
|
4517
|
-
log$
|
|
4500
|
+
log$29.warn("无法从 URL 中解析文档 token", { url: docUrl });
|
|
4518
4501
|
return null;
|
|
4519
4502
|
}
|
|
4520
|
-
log$
|
|
4503
|
+
log$29.info("开始读取人设文档", {
|
|
4521
4504
|
url: docUrl,
|
|
4522
4505
|
type: parsed.type,
|
|
4523
4506
|
token: parsed.token
|
|
@@ -4525,18 +4508,18 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4525
4508
|
let docToken = parsed.token;
|
|
4526
4509
|
if (parsed.type === "wiki") {
|
|
4527
4510
|
const realToken = await resolveWikiNodeToDocToken(parsed.token, accessToken);
|
|
4528
|
-
if (!realToken) log$
|
|
4511
|
+
if (!realToken) log$29.warn("wiki 节点解析失败,尝试直接使用 token 读取");
|
|
4529
4512
|
else docToken = realToken;
|
|
4530
4513
|
}
|
|
4531
4514
|
try {
|
|
4532
4515
|
const data = await (await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
4533
|
-
log$
|
|
4516
|
+
log$29.info("文档 raw_content API 响应", {
|
|
4534
4517
|
code: data.code,
|
|
4535
4518
|
msg: data.msg,
|
|
4536
4519
|
contentLength: data.data?.content?.length
|
|
4537
4520
|
});
|
|
4538
4521
|
if (data.code !== 0 || !data.data?.content) {
|
|
4539
|
-
log$
|
|
4522
|
+
log$29.warn("读取文档内容失败", {
|
|
4540
4523
|
code: data.code,
|
|
4541
4524
|
msg: data.msg,
|
|
4542
4525
|
docToken
|
|
@@ -4545,7 +4528,7 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4545
4528
|
}
|
|
4546
4529
|
return data.data.content.trim();
|
|
4547
4530
|
} catch (err) {
|
|
4548
|
-
log$
|
|
4531
|
+
log$29.error("读取文档内容异常", {
|
|
4549
4532
|
error: err instanceof Error ? err.message : String(err),
|
|
4550
4533
|
docToken
|
|
4551
4534
|
});
|
|
@@ -4556,7 +4539,7 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4556
4539
|
async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
4557
4540
|
try {
|
|
4558
4541
|
const data = await (await fetch(`https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${wikiToken}`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
4559
|
-
log$
|
|
4542
|
+
log$29.info("wiki get_node API 响应", {
|
|
4560
4543
|
code: data.code,
|
|
4561
4544
|
msg: data.msg,
|
|
4562
4545
|
objType: data.data?.node?.obj_type
|
|
@@ -4564,7 +4547,7 @@ async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
|
4564
4547
|
if (data.code !== 0 || !data.data?.node?.obj_token) return null;
|
|
4565
4548
|
return data.data.node.obj_token;
|
|
4566
4549
|
} catch (err) {
|
|
4567
|
-
log$
|
|
4550
|
+
log$29.warn("wiki get_node 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4568
4551
|
return null;
|
|
4569
4552
|
}
|
|
4570
4553
|
}
|
|
@@ -4586,7 +4569,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4586
4569
|
const infoBlock = buildAppInfoBlock(appInfo);
|
|
4587
4570
|
if (!existsSync(claudeMdPath)) {
|
|
4588
4571
|
await writeFile(claudeMdPath, infoBlock + "\n\n" + getDefaultClaudeMdBody(), "utf-8");
|
|
4589
|
-
log$
|
|
4572
|
+
log$29.info("CLAUDE.md 已创建(含应用信息)", { appName: appInfo.appName });
|
|
4590
4573
|
return;
|
|
4591
4574
|
}
|
|
4592
4575
|
let content = await readFile(claudeMdPath, "utf-8");
|
|
@@ -4598,7 +4581,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4598
4581
|
content = before + infoBlock + after;
|
|
4599
4582
|
} else content = infoBlock + "\n\n" + content;
|
|
4600
4583
|
await writeFile(claudeMdPath, content, "utf-8");
|
|
4601
|
-
log$
|
|
4584
|
+
log$29.info("CLAUDE.md 应用信息已同步", {
|
|
4602
4585
|
appName: appInfo.appName,
|
|
4603
4586
|
hasDescription: !!appInfo.description,
|
|
4604
4587
|
hasAvatar: !!appInfo.avatarUrl
|
|
@@ -4613,7 +4596,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4613
4596
|
async function syncPersonaDocToClaudeMd(personaContent) {
|
|
4614
4597
|
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
4615
4598
|
if (!existsSync(claudeMdPath)) {
|
|
4616
|
-
log$
|
|
4599
|
+
log$29.warn("CLAUDE.md 不存在,无法同步人设文档(需先同步应用信息)");
|
|
4617
4600
|
return;
|
|
4618
4601
|
}
|
|
4619
4602
|
let content = await readFile(claudeMdPath, "utf-8");
|
|
@@ -4634,7 +4617,7 @@ async function syncPersonaDocToClaudeMd(personaContent) {
|
|
|
4634
4617
|
content = before + personaBlock + after;
|
|
4635
4618
|
} else content = content.trimEnd() + "\n\n" + personaBlock + "\n";
|
|
4636
4619
|
await writeFile(claudeMdPath, content, "utf-8");
|
|
4637
|
-
log$
|
|
4620
|
+
log$29.info("CLAUDE.md 人设文档已同步", { contentLength: personaContent.length });
|
|
4638
4621
|
}
|
|
4639
4622
|
/** 构建应用信息标记区块 */
|
|
4640
4623
|
function buildAppInfoBlock(appInfo) {
|
|
@@ -4726,24 +4709,24 @@ function getDefaultClaudeMdBody() {
|
|
|
4726
4709
|
* @returns 同步后的应用信息,如果失败返回 null
|
|
4727
4710
|
*/
|
|
4728
4711
|
async function syncAppInfo(credentials) {
|
|
4729
|
-
log$
|
|
4712
|
+
log$29.info("开始同步应用信息", { appId: credentials.appId });
|
|
4730
4713
|
const appInfo = await fetchAppInfo(credentials);
|
|
4731
4714
|
if (!appInfo) {
|
|
4732
|
-
log$
|
|
4715
|
+
log$29.warn("获取应用信息失败,跳过同步");
|
|
4733
4716
|
return null;
|
|
4734
4717
|
}
|
|
4735
4718
|
await syncAppInfoToClaudeMd(appInfo);
|
|
4736
4719
|
if (appInfo.helpDocUrl) {
|
|
4737
|
-
log$
|
|
4720
|
+
log$29.info("检测到帮助文档 URL,尝试同步人设文档", { helpDocUrl: appInfo.helpDocUrl });
|
|
4738
4721
|
const token = await getTenantAccessToken(credentials.appId, credentials.appSecret);
|
|
4739
4722
|
if (token) {
|
|
4740
4723
|
const personaContent = await fetchDocContent(appInfo.helpDocUrl, token);
|
|
4741
4724
|
if (personaContent) await syncPersonaDocToClaudeMd(personaContent);
|
|
4742
|
-
else log$
|
|
4725
|
+
else log$29.warn("人设文档内容为空或读取失败,跳过同步");
|
|
4743
4726
|
}
|
|
4744
4727
|
}
|
|
4745
4728
|
await installSyncSkill();
|
|
4746
|
-
log$
|
|
4729
|
+
log$29.info("应用信息同步完成", {
|
|
4747
4730
|
appName: appInfo.appName,
|
|
4748
4731
|
description: appInfo.description?.substring(0, 50),
|
|
4749
4732
|
hasPersonaDoc: !!appInfo.helpDocUrl
|
|
@@ -4761,7 +4744,7 @@ async function installSyncSkill() {
|
|
|
4761
4744
|
if ((await readFile(SYNC_SKILL_PATH, "utf-8")).includes(`skill-version: ${SYNC_SKILL_VERSION}`)) return;
|
|
4762
4745
|
}
|
|
4763
4746
|
await writeFile(SYNC_SKILL_PATH, SYNC_SKILL_CONTENT, "utf-8");
|
|
4764
|
-
log$
|
|
4747
|
+
log$29.info("sync-app-info 技能已安装", { path: SYNC_SKILL_PATH });
|
|
4765
4748
|
}
|
|
4766
4749
|
const SYNC_SKILL_CONTENT = `---
|
|
4767
4750
|
skill-version: ${SYNC_SKILL_VERSION}
|
|
@@ -5016,6 +4999,269 @@ async function sendCallback(callbackUrl, taskId, status, result, error) {
|
|
|
5016
4999
|
}
|
|
5017
5000
|
}
|
|
5018
5001
|
//#endregion
|
|
5002
|
+
//#region src/gateway/artifact-store.ts
|
|
5003
|
+
const DEFAULT_ARTIFACT_TTL_MS = 4320 * 60 * 1e3;
|
|
5004
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
5005
|
+
var ArtifactUploadError = class extends Error {
|
|
5006
|
+
constructor(message, statusCode = 400) {
|
|
5007
|
+
super(message);
|
|
5008
|
+
this.statusCode = statusCode;
|
|
5009
|
+
this.name = "ArtifactUploadError";
|
|
5010
|
+
}
|
|
5011
|
+
};
|
|
5012
|
+
var GatewayArtifactStore = class {
|
|
5013
|
+
rootDir;
|
|
5014
|
+
maxUploadBytes;
|
|
5015
|
+
ttlMs;
|
|
5016
|
+
constructor(params) {
|
|
5017
|
+
const workspaceRoot = params?.workspaceRoot || resolveWorkspaceRoot();
|
|
5018
|
+
this.rootDir = join(workspaceRoot, ".artifacts");
|
|
5019
|
+
this.maxUploadBytes = params?.maxUploadBytes ?? readPositiveIntEnv("LARKPAL_ARTIFACT_MAX_UPLOAD_BYTES", DEFAULT_MAX_UPLOAD_BYTES);
|
|
5020
|
+
this.ttlMs = params?.ttlMs ?? readPositiveIntEnv("LARKPAL_ARTIFACT_TTL_MS", DEFAULT_ARTIFACT_TTL_MS);
|
|
5021
|
+
}
|
|
5022
|
+
async create(input) {
|
|
5023
|
+
validateContextId(input.sessionId, "sessionId");
|
|
5024
|
+
validateContextId(input.owner.tenantKey, "tenantKey");
|
|
5025
|
+
const artifactId = `art_${v4().replace(/-/g, "")}`;
|
|
5026
|
+
const createdAtMs = Date.now();
|
|
5027
|
+
const createdAt = new Date(createdAtMs).toISOString();
|
|
5028
|
+
const expiresAt = new Date(createdAtMs + this.ttlMs).toISOString();
|
|
5029
|
+
const artifactDir = this.getArtifactDir(input.owner.tenantKey, input.sessionId, artifactId);
|
|
5030
|
+
const blobPath = join(artifactDir, "blob");
|
|
5031
|
+
const tmpBlobPath = join(artifactDir, `.blob.${process.pid}.${Date.now()}.tmp`);
|
|
5032
|
+
await mkdir(artifactDir, { recursive: true });
|
|
5033
|
+
const hash = createHash("sha256");
|
|
5034
|
+
let size = 0;
|
|
5035
|
+
const metering = new Transform({ transform: (chunk, _encoding, callback) => {
|
|
5036
|
+
size += chunk.length;
|
|
5037
|
+
if (size > this.maxUploadBytes) {
|
|
5038
|
+
callback(new ArtifactUploadError(`Artifact exceeds max upload size ${this.maxUploadBytes} bytes`, 413));
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
hash.update(chunk);
|
|
5042
|
+
callback(null, chunk);
|
|
5043
|
+
} });
|
|
5044
|
+
try {
|
|
5045
|
+
await pipeline(input.stream, metering, createWriteStream(tmpBlobPath));
|
|
5046
|
+
await rename(tmpBlobPath, blobPath);
|
|
5047
|
+
} catch (err) {
|
|
5048
|
+
await rm(artifactDir, {
|
|
5049
|
+
recursive: true,
|
|
5050
|
+
force: true
|
|
5051
|
+
}).catch(() => void 0);
|
|
5052
|
+
throw err;
|
|
5053
|
+
}
|
|
5054
|
+
const metadata = {
|
|
5055
|
+
artifactId,
|
|
5056
|
+
sessionId: input.sessionId,
|
|
5057
|
+
tenantKey: input.owner.tenantKey,
|
|
5058
|
+
userId: input.owner.userId,
|
|
5059
|
+
openId: input.owner.openId,
|
|
5060
|
+
kind: "workspace-file",
|
|
5061
|
+
uri: `artifact://${artifactId}`,
|
|
5062
|
+
fileName: sanitizeFileName(input.fileName),
|
|
5063
|
+
mimeType: normalizeMimeType(input.mimeType),
|
|
5064
|
+
size,
|
|
5065
|
+
contentHash: `sha256:${hash.digest("hex")}`,
|
|
5066
|
+
createdAt,
|
|
5067
|
+
expiresAt,
|
|
5068
|
+
metadata: input.metadata,
|
|
5069
|
+
blobPath
|
|
5070
|
+
};
|
|
5071
|
+
await writeJson(join(artifactDir, "meta.json"), metadata);
|
|
5072
|
+
return toPublicRef(metadata);
|
|
5073
|
+
}
|
|
5074
|
+
async list(params) {
|
|
5075
|
+
validateContextId(params.sessionId, "sessionId");
|
|
5076
|
+
validateContextId(params.owner.tenantKey, "tenantKey");
|
|
5077
|
+
const sessionDir = this.getSessionDir(params.owner.tenantKey, params.sessionId);
|
|
5078
|
+
let entries;
|
|
5079
|
+
try {
|
|
5080
|
+
entries = await readdir(sessionDir);
|
|
5081
|
+
} catch {
|
|
5082
|
+
return [];
|
|
5083
|
+
}
|
|
5084
|
+
const results = [];
|
|
5085
|
+
for (const entry of entries) {
|
|
5086
|
+
if (!entry.startsWith("art_")) continue;
|
|
5087
|
+
const metadata = await readArtifactMetadata(join(sessionDir, entry, "meta.json"));
|
|
5088
|
+
if (!metadata) continue;
|
|
5089
|
+
if (!isOwnedBy(metadata, params.owner, params.sessionId)) continue;
|
|
5090
|
+
if (Date.parse(metadata.expiresAt) <= Date.now()) continue;
|
|
5091
|
+
results.push(toPublicRef(metadata));
|
|
5092
|
+
}
|
|
5093
|
+
return results.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
5094
|
+
}
|
|
5095
|
+
async statBlob(ref, owner) {
|
|
5096
|
+
const metadata = await readArtifactMetadata(this.getMetadataPath(owner.tenantKey, ref.sessionId, ref.artifactId));
|
|
5097
|
+
if (!metadata || !isOwnedBy(metadata, owner, ref.sessionId)) return null;
|
|
5098
|
+
const blobStat = await stat(metadata.blobPath).catch(() => null);
|
|
5099
|
+
return blobStat ? {
|
|
5100
|
+
path: metadata.blobPath,
|
|
5101
|
+
size: blobStat.size
|
|
5102
|
+
} : null;
|
|
5103
|
+
}
|
|
5104
|
+
getSessionDir(tenantKey, sessionId) {
|
|
5105
|
+
return join(this.rootDir, tenantKey, sessionId);
|
|
5106
|
+
}
|
|
5107
|
+
getArtifactDir(tenantKey, sessionId, artifactId) {
|
|
5108
|
+
return join(this.getSessionDir(tenantKey, sessionId), artifactId);
|
|
5109
|
+
}
|
|
5110
|
+
getMetadataPath(tenantKey, sessionId, artifactId) {
|
|
5111
|
+
return join(this.getArtifactDir(tenantKey, sessionId, artifactId), "meta.json");
|
|
5112
|
+
}
|
|
5113
|
+
};
|
|
5114
|
+
function readPositiveIntEnv(name, fallback) {
|
|
5115
|
+
const raw = process.env[name];
|
|
5116
|
+
if (!raw) return fallback;
|
|
5117
|
+
const parsed = Number.parseInt(raw, 10);
|
|
5118
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
5119
|
+
}
|
|
5120
|
+
function validateContextId(value, fieldName) {
|
|
5121
|
+
if (!/^[A-Za-z0-9_.:-]{1,200}$/.test(value)) throw new ArtifactUploadError(`Invalid ${fieldName}`, 400);
|
|
5122
|
+
}
|
|
5123
|
+
function sanitizeFileName(fileName) {
|
|
5124
|
+
return fileName.replace(/[\\/\0\r\n]/g, "_").trim() || "upload.bin";
|
|
5125
|
+
}
|
|
5126
|
+
function normalizeMimeType(mimeType) {
|
|
5127
|
+
return mimeType?.trim() || "application/octet-stream";
|
|
5128
|
+
}
|
|
5129
|
+
function toPublicRef(metadata) {
|
|
5130
|
+
const { tenantKey: _tenantKey, userId: _userId, openId: _openId, blobPath: _blobPath, ...ref } = metadata;
|
|
5131
|
+
return ref;
|
|
5132
|
+
}
|
|
5133
|
+
function isOwnedBy(metadata, owner, sessionId) {
|
|
5134
|
+
return metadata.tenantKey === owner.tenantKey && metadata.userId === owner.userId && metadata.openId === owner.openId && metadata.sessionId === sessionId;
|
|
5135
|
+
}
|
|
5136
|
+
async function readArtifactMetadata(filePath) {
|
|
5137
|
+
try {
|
|
5138
|
+
return JSON.parse(await readFile(filePath, "utf-8"));
|
|
5139
|
+
} catch {
|
|
5140
|
+
return null;
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
async function writeJson(filePath, data) {
|
|
5144
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
5145
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
5146
|
+
}
|
|
5147
|
+
//#endregion
|
|
5148
|
+
//#region src/gateway/artifact-handler.ts
|
|
5149
|
+
const log$28 = larkLogger("gateway/artifacts");
|
|
5150
|
+
function createArtifactRouter(config) {
|
|
5151
|
+
const router = Router();
|
|
5152
|
+
const store = config?.store || new GatewayArtifactStore({ workspaceRoot: config?.workspaceRoot });
|
|
5153
|
+
router.post("/api/chat/artifacts", chatAuthMiddleware, (req, res) => {
|
|
5154
|
+
handleUpload(req, res, store);
|
|
5155
|
+
});
|
|
5156
|
+
router.get("/api/chat/artifacts", chatAuthMiddleware, (req, res) => {
|
|
5157
|
+
handleList(req, res, store);
|
|
5158
|
+
});
|
|
5159
|
+
return router;
|
|
5160
|
+
}
|
|
5161
|
+
async function handleList(req, res, store) {
|
|
5162
|
+
const sessionId = readString(req.query.session_id || req.query.sessionId);
|
|
5163
|
+
if (!sessionId) {
|
|
5164
|
+
res.status(400).json({ error: "session_id is required" });
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
const artifacts = await store.list({
|
|
5168
|
+
sessionId,
|
|
5169
|
+
owner: toOwner(res.locals.chatUser)
|
|
5170
|
+
});
|
|
5171
|
+
res.json({ artifacts });
|
|
5172
|
+
}
|
|
5173
|
+
async function handleUpload(req, res, store) {
|
|
5174
|
+
if (!req.headers["content-type"]?.includes("multipart/form-data")) {
|
|
5175
|
+
res.status(415).json({ error: "Content-Type must be multipart/form-data" });
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
const chatUser = res.locals.chatUser;
|
|
5179
|
+
try {
|
|
5180
|
+
const artifact = await parseMultipartUpload(req, store, chatUser);
|
|
5181
|
+
res.status(201).json({
|
|
5182
|
+
artifact,
|
|
5183
|
+
sourceArtifacts: [artifact]
|
|
5184
|
+
});
|
|
5185
|
+
} catch (err) {
|
|
5186
|
+
const statusCode = err instanceof ArtifactUploadError ? err.statusCode : 500;
|
|
5187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5188
|
+
log$28.warn("artifact upload failed", {
|
|
5189
|
+
statusCode,
|
|
5190
|
+
message
|
|
5191
|
+
});
|
|
5192
|
+
res.status(statusCode).json({ error: message });
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
function parseMultipartUpload(req, store, chatUser) {
|
|
5196
|
+
return new Promise((resolve, reject) => {
|
|
5197
|
+
const busboy = Busboy({ headers: req.headers });
|
|
5198
|
+
const owner = toOwner(chatUser);
|
|
5199
|
+
let sessionId = readString(req.query.session_id || req.query.sessionId) ?? "";
|
|
5200
|
+
let metadata;
|
|
5201
|
+
let uploadPromise;
|
|
5202
|
+
let fileSeen = false;
|
|
5203
|
+
let settled = false;
|
|
5204
|
+
const fail = (error) => {
|
|
5205
|
+
if (settled) return;
|
|
5206
|
+
settled = true;
|
|
5207
|
+
reject(error);
|
|
5208
|
+
};
|
|
5209
|
+
busboy.on("field", (name, value) => {
|
|
5210
|
+
if (name === "session_id" || name === "sessionId") sessionId = value.trim();
|
|
5211
|
+
else if (name === "metadata" && value.trim()) try {
|
|
5212
|
+
const parsed = JSON.parse(value);
|
|
5213
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) metadata = parsed;
|
|
5214
|
+
else fail(new ArtifactUploadError("metadata must be a JSON object"));
|
|
5215
|
+
} catch (err) {
|
|
5216
|
+
fail(new ArtifactUploadError(`metadata is not valid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
5217
|
+
}
|
|
5218
|
+
});
|
|
5219
|
+
busboy.on("file", (_name, file, info) => {
|
|
5220
|
+
if (fileSeen) {
|
|
5221
|
+
file.resume();
|
|
5222
|
+
fail(new ArtifactUploadError("Only one file is supported per upload"));
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
fileSeen = true;
|
|
5226
|
+
if (!sessionId) {
|
|
5227
|
+
file.resume();
|
|
5228
|
+
fail(new ArtifactUploadError("session_id is required before file field"));
|
|
5229
|
+
return;
|
|
5230
|
+
}
|
|
5231
|
+
const stream = new PassThrough();
|
|
5232
|
+
uploadPromise = store.create({
|
|
5233
|
+
sessionId,
|
|
5234
|
+
owner,
|
|
5235
|
+
fileName: info.filename || "upload.bin",
|
|
5236
|
+
mimeType: info.mimeType,
|
|
5237
|
+
metadata,
|
|
5238
|
+
stream
|
|
5239
|
+
});
|
|
5240
|
+
file.pipe(stream);
|
|
5241
|
+
});
|
|
5242
|
+
busboy.on("error", fail);
|
|
5243
|
+
busboy.on("finish", () => {
|
|
5244
|
+
if (settled) return;
|
|
5245
|
+
if (!fileSeen || !uploadPromise) {
|
|
5246
|
+
fail(new ArtifactUploadError("file is required"));
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
5249
|
+
uploadPromise.then(resolve, reject);
|
|
5250
|
+
});
|
|
5251
|
+
req.pipe(busboy);
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
function toOwner(chatUser) {
|
|
5255
|
+
return {
|
|
5256
|
+
tenantKey: chatUser.tenantKey,
|
|
5257
|
+
userId: chatUser.userId,
|
|
5258
|
+
openId: chatUser.openId
|
|
5259
|
+
};
|
|
5260
|
+
}
|
|
5261
|
+
function readString(value) {
|
|
5262
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
5263
|
+
}
|
|
5264
|
+
//#endregion
|
|
5019
5265
|
//#region src/gateway/execute-handler.ts
|
|
5020
5266
|
/**
|
|
5021
5267
|
* 执行接口路由 — 提供 CC 任务的创建、查询、取消和批量执行能力
|
|
@@ -6973,6 +7219,7 @@ function registerRoutes(app, processManager, scheduledTaskManager, appCredential
|
|
|
6973
7219
|
app.use("/hooks", createHooksRouter());
|
|
6974
7220
|
app.use("/api/mcp", createMcpCallbackRouter());
|
|
6975
7221
|
app.use(createChatAuthRouter());
|
|
7222
|
+
app.use(createArtifactRouter({ workspaceRoot }));
|
|
6976
7223
|
if (processManager && messageStore) app.use(createChatRouter({
|
|
6977
7224
|
processManager,
|
|
6978
7225
|
messageStore
|
|
@@ -8576,9 +8823,31 @@ function sortTraceValue(value) {
|
|
|
8576
8823
|
return value;
|
|
8577
8824
|
}
|
|
8578
8825
|
//#endregion
|
|
8826
|
+
//#region src/card/user-facing-error.ts
|
|
8827
|
+
function createUserFacingError(message, causeMessage) {
|
|
8828
|
+
const error = new Error(causeMessage || message);
|
|
8829
|
+
error.userFacingMessage = message;
|
|
8830
|
+
return error;
|
|
8831
|
+
}
|
|
8832
|
+
function getUserFacingErrorMessage(error) {
|
|
8833
|
+
if (!error || typeof error !== "object") return void 0;
|
|
8834
|
+
const message = error.userFacingMessage;
|
|
8835
|
+
return typeof message === "string" && message.trim() ? message : void 0;
|
|
8836
|
+
}
|
|
8837
|
+
//#endregion
|
|
8579
8838
|
//#region src/card/cc-stream-bridge.ts
|
|
8580
8839
|
const log$21 = larkLogger("card/cc-stream-bridge");
|
|
8581
8840
|
const CC_INTERNAL_PLACEHOLDER = "No response requested.";
|
|
8841
|
+
const CLAUDE_INVALID_PARAMETER_PATTERN = /API Error:\s*400\b.*parameter specified in the request is not valid/i;
|
|
8842
|
+
function buildClaudeCodeUserFacingError(errorMessage, hasImages) {
|
|
8843
|
+
if (hasImages && CLAUDE_INVALID_PARAMETER_PATTERN.test(errorMessage)) return createUserFacingError([
|
|
8844
|
+
"当前运行时无法处理这条图片消息。",
|
|
8845
|
+
"",
|
|
8846
|
+
"图片已经下载成功,但 Claude Code 当前模型接口返回了无效参数错误,通常表示当前模型或方舟 coding endpoint 不支持这类图片输入。",
|
|
8847
|
+
"请改用支持图片的 agent 引擎,或先用文字描述图片内容后再试。"
|
|
8848
|
+
].join("\n"), errorMessage);
|
|
8849
|
+
return new Error(errorMessage);
|
|
8850
|
+
}
|
|
8582
8851
|
/**
|
|
8583
8852
|
* CCStreamBridge — 将 CC 流事件桥接到 StreamingCardController
|
|
8584
8853
|
*
|
|
@@ -8745,11 +9014,13 @@ var CCStreamBridge = class {
|
|
|
8745
9014
|
this.controller.onIdle();
|
|
8746
9015
|
} else {
|
|
8747
9016
|
const errorMessage = data.result ?? `CC 执行失败: ${data.subtype}`;
|
|
9017
|
+
const hasImages = sessionId && pm ? pm.getLastMessageHadImages(sessionId) : false;
|
|
8748
9018
|
log$21.error("CC 执行返回错误", {
|
|
8749
9019
|
subtype: data.subtype,
|
|
9020
|
+
hasImages,
|
|
8750
9021
|
errorMessage: errorMessage.slice(0, 200)
|
|
8751
9022
|
});
|
|
8752
|
-
this.controller.onError(
|
|
9023
|
+
this.controller.onError(buildClaudeCodeUserFacingError(errorMessage, !!hasImages), { kind: "cc-execution" });
|
|
8753
9024
|
}
|
|
8754
9025
|
} else if (!this.accumulatedText.trim()) if (data.result) {
|
|
8755
9026
|
log$21.info("result 兜底:使用 result.result 作为最终回复", {
|
|
@@ -11923,8 +12194,9 @@ var StreamingCardController = class StreamingCardController {
|
|
|
11923
12194
|
const toolUseDisplay = this.computeToolUseDisplay();
|
|
11924
12195
|
try {
|
|
11925
12196
|
if (this.cardKit.cardMessageId) {
|
|
12197
|
+
const visibleError = getUserFacingErrorMessage(err) || "An error occurred while generating the response.";
|
|
11926
12198
|
const terminalContent = prepareTerminalCardContent({
|
|
11927
|
-
text: this.text.accumulatedText ? `${this.text.accumulatedText}\n\n---\n**Error**:
|
|
12199
|
+
text: this.text.accumulatedText ? `${this.text.accumulatedText}\n\n---\n**Error**: ${visibleError}` : `**Error**: ${visibleError}`,
|
|
11928
12200
|
reasoningText: this.reasoning.accumulatedReasoningText || void 0
|
|
11929
12201
|
}, this.imageResolver);
|
|
11930
12202
|
const errorCard = buildCardContent("complete", {
|