@vibe-lark/larkpal 0.1.62 → 0.1.64
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 +567 -221
- package/package.json +4 -2
package/dist/main.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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";
|
|
8
8
|
import "dotenv/config";
|
|
9
|
-
import { access, lstat, mkdir, open, readFile, readdir, readlink, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
9
|
+
import { access, copyFile, 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";
|
|
@@ -18,7 +18,10 @@ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypt
|
|
|
18
18
|
import path$1 from "path";
|
|
19
19
|
import os from "os";
|
|
20
20
|
import { EventEmitter } from "node:events";
|
|
21
|
+
import { pipeline } from "node:stream/promises";
|
|
22
|
+
import { PassThrough, Readable, Transform } from "node:stream";
|
|
21
23
|
import express, { Router } from "express";
|
|
24
|
+
import Busboy from "busboy";
|
|
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
|
});
|
|
@@ -2607,6 +2619,165 @@ function getLarkpalMcpServerCommand() {
|
|
|
2607
2619
|
};
|
|
2608
2620
|
}
|
|
2609
2621
|
//#endregion
|
|
2622
|
+
//#region src/gateway/artifact-store.ts
|
|
2623
|
+
const DEFAULT_ARTIFACT_TTL_MS = 4320 * 60 * 1e3;
|
|
2624
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
2625
|
+
var ArtifactUploadError = class extends Error {
|
|
2626
|
+
constructor(message, statusCode = 400) {
|
|
2627
|
+
super(message);
|
|
2628
|
+
this.statusCode = statusCode;
|
|
2629
|
+
this.name = "ArtifactUploadError";
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
var GatewayArtifactStore = class {
|
|
2633
|
+
rootDir;
|
|
2634
|
+
maxUploadBytes;
|
|
2635
|
+
ttlMs;
|
|
2636
|
+
constructor(params) {
|
|
2637
|
+
const workspaceRoot = params?.workspaceRoot || resolveWorkspaceRoot();
|
|
2638
|
+
this.rootDir = join(workspaceRoot, ".artifacts");
|
|
2639
|
+
this.maxUploadBytes = params?.maxUploadBytes ?? readPositiveIntEnv("LARKPAL_ARTIFACT_MAX_UPLOAD_BYTES", DEFAULT_MAX_UPLOAD_BYTES);
|
|
2640
|
+
this.ttlMs = params?.ttlMs ?? readPositiveIntEnv("LARKPAL_ARTIFACT_TTL_MS", DEFAULT_ARTIFACT_TTL_MS);
|
|
2641
|
+
}
|
|
2642
|
+
async create(input) {
|
|
2643
|
+
validateContextId(input.sessionId, "sessionId");
|
|
2644
|
+
validateContextId(input.owner.tenantKey, "tenantKey");
|
|
2645
|
+
const artifactId = `art_${v4().replace(/-/g, "")}`;
|
|
2646
|
+
const createdAtMs = Date.now();
|
|
2647
|
+
const createdAt = new Date(createdAtMs).toISOString();
|
|
2648
|
+
const expiresAt = new Date(createdAtMs + this.ttlMs).toISOString();
|
|
2649
|
+
const artifactDir = this.getArtifactDir(input.owner.tenantKey, input.sessionId, artifactId);
|
|
2650
|
+
const blobPath = join(artifactDir, "blob");
|
|
2651
|
+
const tmpBlobPath = join(artifactDir, `.blob.${process.pid}.${Date.now()}.tmp`);
|
|
2652
|
+
await mkdir(artifactDir, { recursive: true });
|
|
2653
|
+
const hash = createHash("sha256");
|
|
2654
|
+
let size = 0;
|
|
2655
|
+
const metering = new Transform({ transform: (chunk, _encoding, callback) => {
|
|
2656
|
+
size += chunk.length;
|
|
2657
|
+
if (size > this.maxUploadBytes) {
|
|
2658
|
+
callback(new ArtifactUploadError(`Artifact exceeds max upload size ${this.maxUploadBytes} bytes`, 413));
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
hash.update(chunk);
|
|
2662
|
+
callback(null, chunk);
|
|
2663
|
+
} });
|
|
2664
|
+
try {
|
|
2665
|
+
await pipeline(input.stream, metering, createWriteStream(tmpBlobPath));
|
|
2666
|
+
await rename(tmpBlobPath, blobPath);
|
|
2667
|
+
} catch (err) {
|
|
2668
|
+
await rm(artifactDir, {
|
|
2669
|
+
recursive: true,
|
|
2670
|
+
force: true
|
|
2671
|
+
}).catch(() => void 0);
|
|
2672
|
+
throw err;
|
|
2673
|
+
}
|
|
2674
|
+
const metadata = {
|
|
2675
|
+
artifactId,
|
|
2676
|
+
sessionId: input.sessionId,
|
|
2677
|
+
tenantKey: input.owner.tenantKey,
|
|
2678
|
+
userId: input.owner.userId,
|
|
2679
|
+
openId: input.owner.openId,
|
|
2680
|
+
kind: "workspace-file",
|
|
2681
|
+
uri: `artifact://${artifactId}`,
|
|
2682
|
+
fileName: sanitizeFileName(input.fileName),
|
|
2683
|
+
mimeType: normalizeMimeType(input.mimeType),
|
|
2684
|
+
size,
|
|
2685
|
+
contentHash: `sha256:${hash.digest("hex")}`,
|
|
2686
|
+
createdAt,
|
|
2687
|
+
expiresAt,
|
|
2688
|
+
metadata: input.metadata,
|
|
2689
|
+
blobPath
|
|
2690
|
+
};
|
|
2691
|
+
await writeJson(join(artifactDir, "meta.json"), metadata);
|
|
2692
|
+
return toPublicRef(metadata);
|
|
2693
|
+
}
|
|
2694
|
+
async list(params) {
|
|
2695
|
+
validateContextId(params.sessionId, "sessionId");
|
|
2696
|
+
validateContextId(params.owner.tenantKey, "tenantKey");
|
|
2697
|
+
const sessionDir = this.getSessionDir(params.owner.tenantKey, params.sessionId);
|
|
2698
|
+
let entries;
|
|
2699
|
+
try {
|
|
2700
|
+
entries = await readdir(sessionDir);
|
|
2701
|
+
} catch {
|
|
2702
|
+
return [];
|
|
2703
|
+
}
|
|
2704
|
+
const results = [];
|
|
2705
|
+
for (const entry of entries) {
|
|
2706
|
+
if (!entry.startsWith("art_")) continue;
|
|
2707
|
+
const metadata = await readArtifactMetadata(join(sessionDir, entry, "meta.json"));
|
|
2708
|
+
if (!metadata) continue;
|
|
2709
|
+
if (!isOwnedBy(metadata, params.owner, params.sessionId)) continue;
|
|
2710
|
+
if (Date.parse(metadata.expiresAt) <= Date.now()) continue;
|
|
2711
|
+
results.push(toPublicRef(metadata));
|
|
2712
|
+
}
|
|
2713
|
+
return results.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2714
|
+
}
|
|
2715
|
+
async statBlob(ref, owner) {
|
|
2716
|
+
const metadata = await readArtifactMetadata(this.getMetadataPath(owner.tenantKey, ref.sessionId, ref.artifactId));
|
|
2717
|
+
if (!metadata || !isOwnedBy(metadata, owner, ref.sessionId)) return null;
|
|
2718
|
+
const blobStat = await stat(metadata.blobPath).catch(() => null);
|
|
2719
|
+
return blobStat ? {
|
|
2720
|
+
path: metadata.blobPath,
|
|
2721
|
+
size: blobStat.size
|
|
2722
|
+
} : null;
|
|
2723
|
+
}
|
|
2724
|
+
async resolveBlob(params) {
|
|
2725
|
+
validateContextId(params.sessionId, "sessionId");
|
|
2726
|
+
validateContextId(params.owner.tenantKey, "tenantKey");
|
|
2727
|
+
validateContextId(params.artifactId, "artifactId");
|
|
2728
|
+
const metadata = await readArtifactMetadata(this.getMetadataPath(params.owner.tenantKey, params.sessionId, params.artifactId));
|
|
2729
|
+
if (!metadata || !isOwnedBy(metadata, params.owner, params.sessionId)) return null;
|
|
2730
|
+
if (Date.parse(metadata.expiresAt) <= Date.now()) return null;
|
|
2731
|
+
if (!await stat(metadata.blobPath).catch(() => null)) return null;
|
|
2732
|
+
return {
|
|
2733
|
+
ref: toPublicRef(metadata),
|
|
2734
|
+
blobPath: metadata.blobPath
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
getSessionDir(tenantKey, sessionId) {
|
|
2738
|
+
return join(this.rootDir, tenantKey, sessionId);
|
|
2739
|
+
}
|
|
2740
|
+
getArtifactDir(tenantKey, sessionId, artifactId) {
|
|
2741
|
+
return join(this.getSessionDir(tenantKey, sessionId), artifactId);
|
|
2742
|
+
}
|
|
2743
|
+
getMetadataPath(tenantKey, sessionId, artifactId) {
|
|
2744
|
+
return join(this.getArtifactDir(tenantKey, sessionId, artifactId), "meta.json");
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
function readPositiveIntEnv(name, fallback) {
|
|
2748
|
+
const raw = process.env[name];
|
|
2749
|
+
if (!raw) return fallback;
|
|
2750
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2751
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
2752
|
+
}
|
|
2753
|
+
function validateContextId(value, fieldName) {
|
|
2754
|
+
if (!/^[A-Za-z0-9_.:-]{1,200}$/.test(value)) throw new ArtifactUploadError(`Invalid ${fieldName}`, 400);
|
|
2755
|
+
}
|
|
2756
|
+
function sanitizeFileName(fileName) {
|
|
2757
|
+
return fileName.replace(/[\\/\0\r\n]/g, "_").trim() || "upload.bin";
|
|
2758
|
+
}
|
|
2759
|
+
function normalizeMimeType(mimeType) {
|
|
2760
|
+
return mimeType?.trim() || "application/octet-stream";
|
|
2761
|
+
}
|
|
2762
|
+
function toPublicRef(metadata) {
|
|
2763
|
+
const { tenantKey: _tenantKey, userId: _userId, openId: _openId, blobPath: _blobPath, ...ref } = metadata;
|
|
2764
|
+
return ref;
|
|
2765
|
+
}
|
|
2766
|
+
function isOwnedBy(metadata, owner, sessionId) {
|
|
2767
|
+
return metadata.tenantKey === owner.tenantKey && metadata.userId === owner.userId && metadata.openId === owner.openId && metadata.sessionId === sessionId;
|
|
2768
|
+
}
|
|
2769
|
+
async function readArtifactMetadata(filePath) {
|
|
2770
|
+
try {
|
|
2771
|
+
return JSON.parse(await readFile(filePath, "utf-8"));
|
|
2772
|
+
} catch {
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
async function writeJson(filePath, data) {
|
|
2777
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
2778
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
2779
|
+
}
|
|
2780
|
+
//#endregion
|
|
2610
2781
|
//#region src/runtime/larkpal-agent-factory.ts
|
|
2611
2782
|
/**
|
|
2612
2783
|
* LarkpalAgent RuntimeAdapter 工厂
|
|
@@ -2618,18 +2789,18 @@ function getLarkpalMcpServerCommand() {
|
|
|
2618
2789
|
*
|
|
2619
2790
|
* 仅当 LARKPAL_RUNTIME=larkpal-agent 时使用。
|
|
2620
2791
|
*/
|
|
2621
|
-
const log$
|
|
2792
|
+
const log$31 = larkLogger("runtime/larkpal-agent-factory");
|
|
2622
2793
|
const AGENT_MD_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
2623
2794
|
async function loadSystemPrompt() {
|
|
2624
2795
|
try {
|
|
2625
2796
|
const content = await readFile(AGENT_MD_PATH, "utf-8");
|
|
2626
|
-
log$
|
|
2797
|
+
log$31.info("systemPrompt 加载成功", {
|
|
2627
2798
|
path: AGENT_MD_PATH,
|
|
2628
2799
|
length: content.length
|
|
2629
2800
|
});
|
|
2630
2801
|
return content;
|
|
2631
2802
|
} catch {
|
|
2632
|
-
log$
|
|
2803
|
+
log$31.warn("systemPrompt 文件不存在,使用默认", { path: AGENT_MD_PATH });
|
|
2633
2804
|
return "你是一个 AI 助手,帮助用户完成飞书知识管理任务。";
|
|
2634
2805
|
}
|
|
2635
2806
|
}
|
|
@@ -2745,7 +2916,7 @@ async function createLarkpalAgentAdapter() {
|
|
|
2745
2916
|
const scenarioManifestDirs = loadScenarioManifestDirs();
|
|
2746
2917
|
const scenarioProviderConfig = loadScenarioProviderConfig(scenarioManifestDirs);
|
|
2747
2918
|
const mcpTimeoutMs = loadAgentMcpTimeoutMs();
|
|
2748
|
-
log$
|
|
2919
|
+
log$31.info("创建 LarkpalAgentAdapter", {
|
|
2749
2920
|
baseURL: llm.baseURL,
|
|
2750
2921
|
model: llm.defaultModel,
|
|
2751
2922
|
maxTurns: defaultMaxTurns,
|
|
@@ -2754,6 +2925,7 @@ async function createLarkpalAgentAdapter() {
|
|
|
2754
2925
|
});
|
|
2755
2926
|
const { LarkpalAgentAdapter, ToolRegistry } = await import("@vibe-lark/larkpal-agent");
|
|
2756
2927
|
const registry = new ToolRegistry();
|
|
2928
|
+
const visualMaxBatchSize = parseInt(process.env.LARKPAL_AGENT_VISUAL_MAX_BATCH_SIZE || "3", 10);
|
|
2757
2929
|
const adapter = new LarkpalAgentAdapter({
|
|
2758
2930
|
llm,
|
|
2759
2931
|
systemPrompt,
|
|
@@ -2762,8 +2934,9 @@ async function createLarkpalAgentAdapter() {
|
|
|
2762
2934
|
registry,
|
|
2763
2935
|
scenarioManifestDirs,
|
|
2764
2936
|
scenarioProviderConfig,
|
|
2937
|
+
artifactResolvers: [createGatewayArtifactResolver()],
|
|
2765
2938
|
visualRuntime: {
|
|
2766
|
-
maxBatchSize:
|
|
2939
|
+
maxBatchSize: visualMaxBatchSize,
|
|
2767
2940
|
allowedDirs: [process.env.LARK_CLI_WORKDIR || "/tmp/ai-knowledge-lark-cli", "/tmp"].filter(Boolean)
|
|
2768
2941
|
}
|
|
2769
2942
|
});
|
|
@@ -2772,21 +2945,66 @@ async function createLarkpalAgentAdapter() {
|
|
|
2772
2945
|
const toolCount = await registry.connectMcpServer(mcpServerUrl, { timeoutMs: mcpTimeoutMs });
|
|
2773
2946
|
const tools = registry.getAll();
|
|
2774
2947
|
adapter.registerTools(tools);
|
|
2775
|
-
log$
|
|
2948
|
+
log$31.info("MCP 工具注册完成", {
|
|
2776
2949
|
serverUrl: mcpServerUrl,
|
|
2777
2950
|
timeoutMs: mcpTimeoutMs,
|
|
2778
2951
|
toolCount,
|
|
2779
2952
|
toolNames: tools.map((t) => t.name)
|
|
2780
2953
|
});
|
|
2781
2954
|
} catch (err) {
|
|
2782
|
-
log$
|
|
2955
|
+
log$31.error("MCP Server 连接失败,Agent 将无法使用远程工具", {
|
|
2783
2956
|
serverUrl: mcpServerUrl,
|
|
2784
2957
|
error: err?.message
|
|
2785
2958
|
});
|
|
2786
2959
|
}
|
|
2787
|
-
else log$
|
|
2960
|
+
else log$31.warn("未配置 LARKPAL_AGENT_MCP_SERVER_URL,Agent 无远程工具可用");
|
|
2788
2961
|
return wrapLarkpalAgentAdapter(adapter);
|
|
2789
2962
|
}
|
|
2963
|
+
function createGatewayArtifactResolver(params) {
|
|
2964
|
+
const store = params?.store || new GatewayArtifactStore();
|
|
2965
|
+
return {
|
|
2966
|
+
name: "larkpal-gateway-artifact-store",
|
|
2967
|
+
canResolve(artifact) {
|
|
2968
|
+
return getArtifactId(artifact) !== void 0;
|
|
2969
|
+
},
|
|
2970
|
+
async resolve(artifact, context) {
|
|
2971
|
+
const artifactId = getArtifactId(artifact);
|
|
2972
|
+
const tenantKey = context.tenantKey || context.authHeaders?.["x-tenant-key"];
|
|
2973
|
+
const userId = context.userId || context.authHeaders?.["x-user-id"];
|
|
2974
|
+
const openId = context.authHeaders?.["x-open-id"] || userId;
|
|
2975
|
+
if (!artifactId || !tenantKey || !userId || !openId) return void 0;
|
|
2976
|
+
const resolved = await store.resolveBlob({
|
|
2977
|
+
sessionId: context.sessionId,
|
|
2978
|
+
artifactId,
|
|
2979
|
+
owner: {
|
|
2980
|
+
tenantKey,
|
|
2981
|
+
userId,
|
|
2982
|
+
openId
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
if (!resolved) return void 0;
|
|
2986
|
+
const targetDir = join(context.cwd, "source-artifacts", artifactId);
|
|
2987
|
+
const fileName = sanitizeWorkspaceFileName(resolved.ref.fileName || artifact.fileName || artifactId);
|
|
2988
|
+
const targetPath = join(targetDir, fileName);
|
|
2989
|
+
await mkdir(targetDir, { recursive: true });
|
|
2990
|
+
await copyFile(resolved.blobPath, targetPath);
|
|
2991
|
+
return {
|
|
2992
|
+
artifactId,
|
|
2993
|
+
kind: "workspace-file",
|
|
2994
|
+
path: targetPath,
|
|
2995
|
+
fileName,
|
|
2996
|
+
mimeType: resolved.ref.mimeType ?? artifact.mimeType,
|
|
2997
|
+
size: resolved.ref.size ?? artifact.size,
|
|
2998
|
+
contentHash: resolved.ref.contentHash ?? artifact.contentHash,
|
|
2999
|
+
source: artifact,
|
|
3000
|
+
refs: {
|
|
3001
|
+
uri: resolved.ref.uri,
|
|
3002
|
+
gatewayArtifactId: resolved.ref.artifactId
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
2790
3008
|
function wrapLarkpalAgentAdapter(adapter) {
|
|
2791
3009
|
return {
|
|
2792
3010
|
get name() {
|
|
@@ -2865,6 +3083,17 @@ function mergeSourceArtifacts(existing, imageArtifacts) {
|
|
|
2865
3083
|
if (Array.isArray(existing)) return [...existing, ...imageArtifacts];
|
|
2866
3084
|
return [existing, ...imageArtifacts];
|
|
2867
3085
|
}
|
|
3086
|
+
function getArtifactId(artifact) {
|
|
3087
|
+
const artifactId = typeof artifact.artifactId === "string" ? artifact.artifactId.trim() : "";
|
|
3088
|
+
if (artifactId.startsWith("art_")) return artifactId;
|
|
3089
|
+
const uri = typeof artifact.uri === "string" ? artifact.uri.trim() : "";
|
|
3090
|
+
if (!uri.startsWith("artifact://")) return void 0;
|
|
3091
|
+
const idFromUri = uri.slice(11).split(/[/?#]/, 1)[0]?.trim();
|
|
3092
|
+
return idFromUri?.startsWith("art_") ? idFromUri : void 0;
|
|
3093
|
+
}
|
|
3094
|
+
function sanitizeWorkspaceFileName(fileName) {
|
|
3095
|
+
return basename(fileName).replace(/[\\/\0\r\n]/g, "_").trim() || "artifact.bin";
|
|
3096
|
+
}
|
|
2868
3097
|
//#endregion
|
|
2869
3098
|
//#region src/routing/session-router.ts
|
|
2870
3099
|
/**
|
|
@@ -3204,6 +3433,7 @@ const MAX_SUMMARY_STRING_LENGTH = 200;
|
|
|
3204
3433
|
const PROTECTED_CONTEXT_HEADERS = new Set([
|
|
3205
3434
|
"x-tenant-key",
|
|
3206
3435
|
"x-user-id",
|
|
3436
|
+
"x-open-id",
|
|
3207
3437
|
"x-session-id",
|
|
3208
3438
|
"x-conversation-id",
|
|
3209
3439
|
"x-trace-id",
|
|
@@ -3246,6 +3476,7 @@ function buildAgentRuntimeConfig(input) {
|
|
|
3246
3476
|
const authHeaders = buildAuthHeaders({
|
|
3247
3477
|
tenantKey,
|
|
3248
3478
|
userId,
|
|
3479
|
+
openId: input.identity?.openId,
|
|
3249
3480
|
sessionId,
|
|
3250
3481
|
conversationId,
|
|
3251
3482
|
traceId,
|
|
@@ -3408,6 +3639,7 @@ function buildAuthHeaders(params) {
|
|
|
3408
3639
|
const headers = {
|
|
3409
3640
|
"x-tenant-key": params.tenantKey,
|
|
3410
3641
|
"x-user-id": params.userId,
|
|
3642
|
+
...params.openId ? { "x-open-id": params.openId } : {},
|
|
3411
3643
|
"x-session-id": params.sessionId,
|
|
3412
3644
|
"x-conversation-id": params.conversationId,
|
|
3413
3645
|
"x-trace-id": params.traceId,
|
|
@@ -3497,7 +3729,7 @@ function summarizeString(value) {
|
|
|
3497
3729
|
* POST /api/chat/sessions — 创建新会话
|
|
3498
3730
|
* DELETE /api/chat/sessions/:id — 删除会话
|
|
3499
3731
|
*/
|
|
3500
|
-
const log$
|
|
3732
|
+
const log$30 = larkLogger("chat/stream");
|
|
3501
3733
|
const chatRunsBySession = /* @__PURE__ */ new Map();
|
|
3502
3734
|
const DEFAULT_STALE_RUN_IDLE_MS = 1800 * 1e3;
|
|
3503
3735
|
/** 统一写 SSE 格式数据到响应流 */
|
|
@@ -3509,7 +3741,7 @@ function isChatSessionRunning(sessionId, processManager) {
|
|
|
3509
3741
|
return getActiveChatRun(sessionId) != null || (processManager.isSessionBusy?.(sessionId) ?? false);
|
|
3510
3742
|
}
|
|
3511
3743
|
function logChatAudit(event) {
|
|
3512
|
-
log$
|
|
3744
|
+
log$30.info("[stream] Agent audit event", { ...event });
|
|
3513
3745
|
}
|
|
3514
3746
|
function prepareSSE(res) {
|
|
3515
3747
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -3560,7 +3792,7 @@ function addRunClient(req, res, run) {
|
|
|
3560
3792
|
run.clients.add(res);
|
|
3561
3793
|
req.on("close", () => {
|
|
3562
3794
|
run.clients.delete(res);
|
|
3563
|
-
log$
|
|
3795
|
+
log$30.info("[stream] 客户端断开连接", {
|
|
3564
3796
|
sessionId: run.sessionId,
|
|
3565
3797
|
runId: run.runId
|
|
3566
3798
|
});
|
|
@@ -3599,7 +3831,7 @@ function broadcastRunEvent(run, event, data) {
|
|
|
3599
3831
|
sendSSE(client, event, data);
|
|
3600
3832
|
} catch (err) {
|
|
3601
3833
|
run.clients.delete(client);
|
|
3602
|
-
log$
|
|
3834
|
+
log$30.warn("[stream] SSE 写入失败,移除客户端", {
|
|
3603
3835
|
sessionId: run.sessionId,
|
|
3604
3836
|
runId: run.runId,
|
|
3605
3837
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3619,7 +3851,7 @@ function queueRunMessage(run, messageStore, message, label) {
|
|
|
3619
3851
|
run.persistQueue = run.persistQueue.catch(() => void 0).then(async () => {
|
|
3620
3852
|
await messageStore.appendMessage(message);
|
|
3621
3853
|
}).catch((err) => {
|
|
3622
|
-
log$
|
|
3854
|
+
log$30.error(label, {
|
|
3623
3855
|
sessionId: run.sessionId,
|
|
3624
3856
|
runId: run.runId,
|
|
3625
3857
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3672,7 +3904,7 @@ async function reconcileRunWithAdapter(run, messageStore, processManager) {
|
|
|
3672
3904
|
else if (idleMs >= getStaleRunIdleMs()) staleReason = `run idle for ${Math.floor(idleMs / 1e3)}s`;
|
|
3673
3905
|
if (!staleReason) return;
|
|
3674
3906
|
const error = `Chat run became stale: ${staleReason}`;
|
|
3675
|
-
log$
|
|
3907
|
+
log$30.warn("[stream] activeRun 与 runtime 状态不一致,标记为 stale", {
|
|
3676
3908
|
sessionId: run.sessionId,
|
|
3677
3909
|
runId: run.runId,
|
|
3678
3910
|
staleReason,
|
|
@@ -3750,7 +3982,7 @@ function createChatRouter(config) {
|
|
|
3750
3982
|
addRunClient(req, res, activeRun);
|
|
3751
3983
|
sendSSE(res, "session", { sessionId });
|
|
3752
3984
|
sendRunStatus(res, activeRun);
|
|
3753
|
-
log$
|
|
3985
|
+
log$30.info("[stream] 已重新连接到运行中的 run", {
|
|
3754
3986
|
sessionId,
|
|
3755
3987
|
runId: activeRun.runId
|
|
3756
3988
|
});
|
|
@@ -3763,7 +3995,7 @@ function createChatRouter(config) {
|
|
|
3763
3995
|
const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
|
|
3764
3996
|
const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
|
|
3765
3997
|
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$
|
|
3998
|
+
log$30.info("[stream] 收到流式对话请求", {
|
|
3767
3999
|
userId,
|
|
3768
4000
|
sessionId,
|
|
3769
4001
|
requestId,
|
|
@@ -3780,13 +4012,13 @@ function createChatRouter(config) {
|
|
|
3780
4012
|
tenantKey,
|
|
3781
4013
|
channel: "web"
|
|
3782
4014
|
})).id;
|
|
3783
|
-
log$
|
|
4015
|
+
log$30.info("[stream] 自动创建新会话", {
|
|
3784
4016
|
userId: openId,
|
|
3785
4017
|
sessionId
|
|
3786
4018
|
});
|
|
3787
4019
|
}
|
|
3788
4020
|
if (isChatSessionRunning(sessionId, processManager)) {
|
|
3789
|
-
log$
|
|
4021
|
+
log$30.warn("[stream] session 正在被非 Web Chat run 占用", {
|
|
3790
4022
|
tenantKey,
|
|
3791
4023
|
userId,
|
|
3792
4024
|
sessionId
|
|
@@ -3797,7 +4029,6 @@ function createChatRouter(config) {
|
|
|
3797
4029
|
res.end();
|
|
3798
4030
|
return;
|
|
3799
4031
|
}
|
|
3800
|
-
const runtimePrompt = enrichPromptWithSourceArtifacts(body.prompt, sourceArtifacts);
|
|
3801
4032
|
const runtimeConfig = buildAgentRuntimeConfig({
|
|
3802
4033
|
requestId,
|
|
3803
4034
|
traceId,
|
|
@@ -3805,7 +4036,7 @@ function createChatRouter(config) {
|
|
|
3805
4036
|
conversationId: sessionId,
|
|
3806
4037
|
entrypoint: "chat",
|
|
3807
4038
|
scenarioId,
|
|
3808
|
-
prompt:
|
|
4039
|
+
prompt: body.prompt,
|
|
3809
4040
|
maxTurns: body.options?.maxTurns,
|
|
3810
4041
|
maxBudgetUsd: body.options?.maxBudgetUsd,
|
|
3811
4042
|
model: process.env.CLAUDE_MODEL || void 0,
|
|
@@ -3858,7 +4089,7 @@ function createChatRouter(config) {
|
|
|
3858
4089
|
});
|
|
3859
4090
|
} catch (err) {
|
|
3860
4091
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3861
|
-
log$
|
|
4092
|
+
log$30.error("[stream] 保存用户消息失败", {
|
|
3862
4093
|
sessionId,
|
|
3863
4094
|
error: errorMessage
|
|
3864
4095
|
});
|
|
@@ -3881,7 +4112,7 @@ function createChatRouter(config) {
|
|
|
3881
4112
|
const handleBlockedEvent = (event) => {
|
|
3882
4113
|
const sanitized = sanitizeBlockedEvent(event);
|
|
3883
4114
|
recordRunEvent(run, "blocked", { toolName: sanitized.name });
|
|
3884
|
-
log$
|
|
4115
|
+
log$30.warn("[stream] Agent blocked event", { ...sanitized });
|
|
3885
4116
|
if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, sanitized.type, {
|
|
3886
4117
|
toolName: sanitized.name,
|
|
3887
4118
|
inputSummary: sanitized.inputSummary,
|
|
@@ -3914,7 +4145,7 @@ function createChatRouter(config) {
|
|
|
3914
4145
|
const dedupeKey = getRuntimeEventDedupeKey(event);
|
|
3915
4146
|
if (dedupeKey) {
|
|
3916
4147
|
if (run.seenRuntimeEventIds.has(dedupeKey)) {
|
|
3917
|
-
log$
|
|
4148
|
+
log$30.debug("[stream] 跳过重复 runtime event", {
|
|
3918
4149
|
sessionId,
|
|
3919
4150
|
runId: run.runId,
|
|
3920
4151
|
eventId: dedupeKey,
|
|
@@ -4064,7 +4295,7 @@ function createChatRouter(config) {
|
|
|
4064
4295
|
});
|
|
4065
4296
|
clearInterval(heartbeatTimer);
|
|
4066
4297
|
endRunClients(run);
|
|
4067
|
-
log$
|
|
4298
|
+
log$30.info("[stream] 对话完成", {
|
|
4068
4299
|
sessionId,
|
|
4069
4300
|
runId: run.runId,
|
|
4070
4301
|
result: result.subtype,
|
|
@@ -4087,7 +4318,7 @@ function createChatRouter(config) {
|
|
|
4087
4318
|
},
|
|
4088
4319
|
onError: async (error) => {
|
|
4089
4320
|
recordRunEvent(run, "error");
|
|
4090
|
-
log$
|
|
4321
|
+
log$30.error("[stream] Runtime 执行出错", {
|
|
4091
4322
|
sessionId,
|
|
4092
4323
|
runId: run.runId,
|
|
4093
4324
|
error: error.message
|
|
@@ -4175,7 +4406,7 @@ function createChatRouter(config) {
|
|
|
4175
4406
|
scenarioId: runtimeConfig.runContext?.scenarioId,
|
|
4176
4407
|
...buildRunInputAuditMetadata(runtimeConfig)
|
|
4177
4408
|
} }));
|
|
4178
|
-
log$
|
|
4409
|
+
log$30.info("[stream] 开始执行 prompt", {
|
|
4179
4410
|
sessionId,
|
|
4180
4411
|
runId: run.runId,
|
|
4181
4412
|
cwd: runtimeConfig.cwd
|
|
@@ -4183,7 +4414,7 @@ function createChatRouter(config) {
|
|
|
4183
4414
|
await processManager.executePrompt(runtimeConfig, callbacks);
|
|
4184
4415
|
if (run.status === "running") {
|
|
4185
4416
|
const errorMessage = "Runtime completed without a final result event";
|
|
4186
|
-
log$
|
|
4417
|
+
log$30.error("[stream] executePrompt 未返回终态回调", {
|
|
4187
4418
|
sessionId,
|
|
4188
4419
|
runId: run.runId
|
|
4189
4420
|
});
|
|
@@ -4200,7 +4431,7 @@ function createChatRouter(config) {
|
|
|
4200
4431
|
}
|
|
4201
4432
|
} catch (err) {
|
|
4202
4433
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4203
|
-
log$
|
|
4434
|
+
log$30.error("[stream] executePrompt 异常", {
|
|
4204
4435
|
sessionId,
|
|
4205
4436
|
runId: run.runId,
|
|
4206
4437
|
error: errorMessage
|
|
@@ -4217,7 +4448,7 @@ function createChatRouter(config) {
|
|
|
4217
4448
|
}
|
|
4218
4449
|
const cursor = req.query.cursor;
|
|
4219
4450
|
const limit = parseInt(req.query.limit, 10) || 50;
|
|
4220
|
-
log$
|
|
4451
|
+
log$30.info("[history] 查询会话历史", {
|
|
4221
4452
|
userId: chatUser.userId,
|
|
4222
4453
|
sessionId,
|
|
4223
4454
|
cursor,
|
|
@@ -4243,7 +4474,7 @@ function createChatRouter(config) {
|
|
|
4243
4474
|
});
|
|
4244
4475
|
} catch (err) {
|
|
4245
4476
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4246
|
-
log$
|
|
4477
|
+
log$30.error("[history] 查询历史消息失败", {
|
|
4247
4478
|
sessionId,
|
|
4248
4479
|
error: errorMessage
|
|
4249
4480
|
});
|
|
@@ -4279,13 +4510,13 @@ function createChatRouter(config) {
|
|
|
4279
4510
|
});
|
|
4280
4511
|
router.get("/api/chat/sessions", chatAuthMiddleware, async (_req, res) => {
|
|
4281
4512
|
const chatUser = res.locals.chatUser;
|
|
4282
|
-
log$
|
|
4513
|
+
log$30.info("[sessions] 列出用户会话", { userId: chatUser.userId });
|
|
4283
4514
|
try {
|
|
4284
4515
|
const sessions = await messageStore.listSessions(chatUser.openId);
|
|
4285
4516
|
res.json({ sessions });
|
|
4286
4517
|
} catch (err) {
|
|
4287
4518
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4288
|
-
log$
|
|
4519
|
+
log$30.error("[sessions] 列出会话失败", {
|
|
4289
4520
|
userId: chatUser.userId,
|
|
4290
4521
|
error: errorMessage
|
|
4291
4522
|
});
|
|
@@ -4295,7 +4526,7 @@ function createChatRouter(config) {
|
|
|
4295
4526
|
router.post("/api/chat/sessions", chatAuthMiddleware, async (req, res) => {
|
|
4296
4527
|
const chatUser = res.locals.chatUser;
|
|
4297
4528
|
const body = req.body;
|
|
4298
|
-
log$
|
|
4529
|
+
log$30.info("[sessions] 创建新会话", {
|
|
4299
4530
|
userId: chatUser.userId,
|
|
4300
4531
|
title: body.title
|
|
4301
4532
|
});
|
|
@@ -4310,7 +4541,7 @@ function createChatRouter(config) {
|
|
|
4310
4541
|
res.json({ session });
|
|
4311
4542
|
} catch (err) {
|
|
4312
4543
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4313
|
-
log$
|
|
4544
|
+
log$30.error("[sessions] 创建会话失败", {
|
|
4314
4545
|
userId: chatUser.userId,
|
|
4315
4546
|
error: errorMessage
|
|
4316
4547
|
});
|
|
@@ -4320,7 +4551,7 @@ function createChatRouter(config) {
|
|
|
4320
4551
|
router.delete("/api/chat/sessions/:id", chatAuthMiddleware, async (req, res) => {
|
|
4321
4552
|
const chatUser = res.locals.chatUser;
|
|
4322
4553
|
const sessionId = String(req.params.id);
|
|
4323
|
-
log$
|
|
4554
|
+
log$30.info("[sessions] 删除会话", {
|
|
4324
4555
|
userId: chatUser.userId,
|
|
4325
4556
|
sessionId
|
|
4326
4557
|
});
|
|
@@ -4330,7 +4561,7 @@ function createChatRouter(config) {
|
|
|
4330
4561
|
res.json({ success: true });
|
|
4331
4562
|
} catch (err) {
|
|
4332
4563
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4333
|
-
log$
|
|
4564
|
+
log$30.error("[sessions] 删除会话失败", {
|
|
4334
4565
|
sessionId,
|
|
4335
4566
|
error: errorMessage
|
|
4336
4567
|
});
|
|
@@ -4339,34 +4570,6 @@ function createChatRouter(config) {
|
|
|
4339
4570
|
});
|
|
4340
4571
|
return router;
|
|
4341
4572
|
}
|
|
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
4573
|
//#endregion
|
|
4371
4574
|
//#region src/core/app-info-sync.ts
|
|
4372
4575
|
/**
|
|
@@ -4384,7 +4587,7 @@ function enrichPromptWithSourceArtifacts(prompt, sourceArtifacts) {
|
|
|
4384
4587
|
* - GET /open-apis/application/v6/applications/me?lang=zh_cn(需要 application:application:self_manage 权限,返回完整信息)
|
|
4385
4588
|
* 如果后者无权限则 fallback 到前者
|
|
4386
4589
|
*/
|
|
4387
|
-
const log$
|
|
4590
|
+
const log$29 = larkLogger("core/app-info-sync");
|
|
4388
4591
|
/**
|
|
4389
4592
|
* 从飞书获取应用信息
|
|
4390
4593
|
*
|
|
@@ -4395,7 +4598,7 @@ async function fetchAppInfo(credentials) {
|
|
|
4395
4598
|
const { appId, appSecret } = credentials;
|
|
4396
4599
|
const token = await getTenantAccessToken(appId, appSecret);
|
|
4397
4600
|
if (!token) {
|
|
4398
|
-
log$
|
|
4601
|
+
log$29.error("获取 tenant_access_token 失败,无法同步应用信息");
|
|
4399
4602
|
return null;
|
|
4400
4603
|
}
|
|
4401
4604
|
const fullInfo = await fetchFromApplicationApi(token);
|
|
@@ -4406,13 +4609,13 @@ async function fetchAppInfo(credentials) {
|
|
|
4406
4609
|
async function fetchFromApplicationApi(token) {
|
|
4407
4610
|
try {
|
|
4408
4611
|
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$
|
|
4612
|
+
log$29.info("application/v6 API 响应", {
|
|
4410
4613
|
code: data.code,
|
|
4411
4614
|
msg: data.msg,
|
|
4412
4615
|
hasApp: !!data.data?.app
|
|
4413
4616
|
});
|
|
4414
4617
|
if (data.code !== 0 || !data.data?.app) {
|
|
4415
|
-
log$
|
|
4618
|
+
log$29.warn("application/v6 API 返回非零或无数据,将 fallback", {
|
|
4416
4619
|
code: data.code,
|
|
4417
4620
|
msg: data.msg
|
|
4418
4621
|
});
|
|
@@ -4427,7 +4630,7 @@ async function fetchFromApplicationApi(token) {
|
|
|
4427
4630
|
helpDocUrl: zhInfo?.help_use
|
|
4428
4631
|
};
|
|
4429
4632
|
} catch (err) {
|
|
4430
|
-
log$
|
|
4633
|
+
log$29.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4431
4634
|
return null;
|
|
4432
4635
|
}
|
|
4433
4636
|
}
|
|
@@ -4435,13 +4638,13 @@ async function fetchFromApplicationApi(token) {
|
|
|
4435
4638
|
async function fetchFromBotApi(token) {
|
|
4436
4639
|
try {
|
|
4437
4640
|
const data = await (await fetch("https://open.feishu.cn/open-apis/bot/v3/info", { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
4438
|
-
log$
|
|
4641
|
+
log$29.info("bot/v3/info API 响应", {
|
|
4439
4642
|
code: data.code,
|
|
4440
4643
|
msg: data.msg,
|
|
4441
4644
|
hasBot: !!data.bot
|
|
4442
4645
|
});
|
|
4443
4646
|
if (data.code !== 0 || !data.bot) {
|
|
4444
|
-
log$
|
|
4647
|
+
log$29.error("bot/v3/info API 失败", {
|
|
4445
4648
|
code: data.code,
|
|
4446
4649
|
msg: data.msg
|
|
4447
4650
|
});
|
|
@@ -4452,7 +4655,7 @@ async function fetchFromBotApi(token) {
|
|
|
4452
4655
|
avatarUrl: data.bot.avatar_url
|
|
4453
4656
|
};
|
|
4454
4657
|
} catch (err) {
|
|
4455
|
-
log$
|
|
4658
|
+
log$29.error("bot/v3/info API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4456
4659
|
return null;
|
|
4457
4660
|
}
|
|
4458
4661
|
}
|
|
@@ -4468,7 +4671,7 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
4468
4671
|
})
|
|
4469
4672
|
})).json();
|
|
4470
4673
|
if (data.code !== 0 || !data.tenant_access_token) {
|
|
4471
|
-
log$
|
|
4674
|
+
log$29.error("获取 tenant_access_token 失败", {
|
|
4472
4675
|
code: data.code,
|
|
4473
4676
|
msg: data.msg
|
|
4474
4677
|
});
|
|
@@ -4476,7 +4679,7 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
4476
4679
|
}
|
|
4477
4680
|
return data.tenant_access_token;
|
|
4478
4681
|
} catch (err) {
|
|
4479
|
-
log$
|
|
4682
|
+
log$29.error("获取 tenant_access_token 异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4480
4683
|
return null;
|
|
4481
4684
|
}
|
|
4482
4685
|
}
|
|
@@ -4514,10 +4717,10 @@ function parseDocTokenFromUrl(url) {
|
|
|
4514
4717
|
async function fetchDocContent(docUrl, accessToken) {
|
|
4515
4718
|
const parsed = parseDocTokenFromUrl(docUrl);
|
|
4516
4719
|
if (!parsed) {
|
|
4517
|
-
log$
|
|
4720
|
+
log$29.warn("无法从 URL 中解析文档 token", { url: docUrl });
|
|
4518
4721
|
return null;
|
|
4519
4722
|
}
|
|
4520
|
-
log$
|
|
4723
|
+
log$29.info("开始读取人设文档", {
|
|
4521
4724
|
url: docUrl,
|
|
4522
4725
|
type: parsed.type,
|
|
4523
4726
|
token: parsed.token
|
|
@@ -4525,18 +4728,18 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4525
4728
|
let docToken = parsed.token;
|
|
4526
4729
|
if (parsed.type === "wiki") {
|
|
4527
4730
|
const realToken = await resolveWikiNodeToDocToken(parsed.token, accessToken);
|
|
4528
|
-
if (!realToken) log$
|
|
4731
|
+
if (!realToken) log$29.warn("wiki 节点解析失败,尝试直接使用 token 读取");
|
|
4529
4732
|
else docToken = realToken;
|
|
4530
4733
|
}
|
|
4531
4734
|
try {
|
|
4532
4735
|
const data = await (await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
4533
|
-
log$
|
|
4736
|
+
log$29.info("文档 raw_content API 响应", {
|
|
4534
4737
|
code: data.code,
|
|
4535
4738
|
msg: data.msg,
|
|
4536
4739
|
contentLength: data.data?.content?.length
|
|
4537
4740
|
});
|
|
4538
4741
|
if (data.code !== 0 || !data.data?.content) {
|
|
4539
|
-
log$
|
|
4742
|
+
log$29.warn("读取文档内容失败", {
|
|
4540
4743
|
code: data.code,
|
|
4541
4744
|
msg: data.msg,
|
|
4542
4745
|
docToken
|
|
@@ -4545,7 +4748,7 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4545
4748
|
}
|
|
4546
4749
|
return data.data.content.trim();
|
|
4547
4750
|
} catch (err) {
|
|
4548
|
-
log$
|
|
4751
|
+
log$29.error("读取文档内容异常", {
|
|
4549
4752
|
error: err instanceof Error ? err.message : String(err),
|
|
4550
4753
|
docToken
|
|
4551
4754
|
});
|
|
@@ -4556,7 +4759,7 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4556
4759
|
async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
4557
4760
|
try {
|
|
4558
4761
|
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$
|
|
4762
|
+
log$29.info("wiki get_node API 响应", {
|
|
4560
4763
|
code: data.code,
|
|
4561
4764
|
msg: data.msg,
|
|
4562
4765
|
objType: data.data?.node?.obj_type
|
|
@@ -4564,7 +4767,7 @@ async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
|
4564
4767
|
if (data.code !== 0 || !data.data?.node?.obj_token) return null;
|
|
4565
4768
|
return data.data.node.obj_token;
|
|
4566
4769
|
} catch (err) {
|
|
4567
|
-
log$
|
|
4770
|
+
log$29.warn("wiki get_node 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4568
4771
|
return null;
|
|
4569
4772
|
}
|
|
4570
4773
|
}
|
|
@@ -4586,7 +4789,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4586
4789
|
const infoBlock = buildAppInfoBlock(appInfo);
|
|
4587
4790
|
if (!existsSync(claudeMdPath)) {
|
|
4588
4791
|
await writeFile(claudeMdPath, infoBlock + "\n\n" + getDefaultClaudeMdBody(), "utf-8");
|
|
4589
|
-
log$
|
|
4792
|
+
log$29.info("CLAUDE.md 已创建(含应用信息)", { appName: appInfo.appName });
|
|
4590
4793
|
return;
|
|
4591
4794
|
}
|
|
4592
4795
|
let content = await readFile(claudeMdPath, "utf-8");
|
|
@@ -4598,7 +4801,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4598
4801
|
content = before + infoBlock + after;
|
|
4599
4802
|
} else content = infoBlock + "\n\n" + content;
|
|
4600
4803
|
await writeFile(claudeMdPath, content, "utf-8");
|
|
4601
|
-
log$
|
|
4804
|
+
log$29.info("CLAUDE.md 应用信息已同步", {
|
|
4602
4805
|
appName: appInfo.appName,
|
|
4603
4806
|
hasDescription: !!appInfo.description,
|
|
4604
4807
|
hasAvatar: !!appInfo.avatarUrl
|
|
@@ -4613,7 +4816,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4613
4816
|
async function syncPersonaDocToClaudeMd(personaContent) {
|
|
4614
4817
|
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
4615
4818
|
if (!existsSync(claudeMdPath)) {
|
|
4616
|
-
log$
|
|
4819
|
+
log$29.warn("CLAUDE.md 不存在,无法同步人设文档(需先同步应用信息)");
|
|
4617
4820
|
return;
|
|
4618
4821
|
}
|
|
4619
4822
|
let content = await readFile(claudeMdPath, "utf-8");
|
|
@@ -4634,7 +4837,7 @@ async function syncPersonaDocToClaudeMd(personaContent) {
|
|
|
4634
4837
|
content = before + personaBlock + after;
|
|
4635
4838
|
} else content = content.trimEnd() + "\n\n" + personaBlock + "\n";
|
|
4636
4839
|
await writeFile(claudeMdPath, content, "utf-8");
|
|
4637
|
-
log$
|
|
4840
|
+
log$29.info("CLAUDE.md 人设文档已同步", { contentLength: personaContent.length });
|
|
4638
4841
|
}
|
|
4639
4842
|
/** 构建应用信息标记区块 */
|
|
4640
4843
|
function buildAppInfoBlock(appInfo) {
|
|
@@ -4726,24 +4929,24 @@ function getDefaultClaudeMdBody() {
|
|
|
4726
4929
|
* @returns 同步后的应用信息,如果失败返回 null
|
|
4727
4930
|
*/
|
|
4728
4931
|
async function syncAppInfo(credentials) {
|
|
4729
|
-
log$
|
|
4932
|
+
log$29.info("开始同步应用信息", { appId: credentials.appId });
|
|
4730
4933
|
const appInfo = await fetchAppInfo(credentials);
|
|
4731
4934
|
if (!appInfo) {
|
|
4732
|
-
log$
|
|
4935
|
+
log$29.warn("获取应用信息失败,跳过同步");
|
|
4733
4936
|
return null;
|
|
4734
4937
|
}
|
|
4735
4938
|
await syncAppInfoToClaudeMd(appInfo);
|
|
4736
4939
|
if (appInfo.helpDocUrl) {
|
|
4737
|
-
log$
|
|
4940
|
+
log$29.info("检测到帮助文档 URL,尝试同步人设文档", { helpDocUrl: appInfo.helpDocUrl });
|
|
4738
4941
|
const token = await getTenantAccessToken(credentials.appId, credentials.appSecret);
|
|
4739
4942
|
if (token) {
|
|
4740
4943
|
const personaContent = await fetchDocContent(appInfo.helpDocUrl, token);
|
|
4741
4944
|
if (personaContent) await syncPersonaDocToClaudeMd(personaContent);
|
|
4742
|
-
else log$
|
|
4945
|
+
else log$29.warn("人设文档内容为空或读取失败,跳过同步");
|
|
4743
4946
|
}
|
|
4744
4947
|
}
|
|
4745
4948
|
await installSyncSkill();
|
|
4746
|
-
log$
|
|
4949
|
+
log$29.info("应用信息同步完成", {
|
|
4747
4950
|
appName: appInfo.appName,
|
|
4748
4951
|
description: appInfo.description?.substring(0, 50),
|
|
4749
4952
|
hasPersonaDoc: !!appInfo.helpDocUrl
|
|
@@ -4761,7 +4964,7 @@ async function installSyncSkill() {
|
|
|
4761
4964
|
if ((await readFile(SYNC_SKILL_PATH, "utf-8")).includes(`skill-version: ${SYNC_SKILL_VERSION}`)) return;
|
|
4762
4965
|
}
|
|
4763
4966
|
await writeFile(SYNC_SKILL_PATH, SYNC_SKILL_CONTENT, "utf-8");
|
|
4764
|
-
log$
|
|
4967
|
+
log$29.info("sync-app-info 技能已安装", { path: SYNC_SKILL_PATH });
|
|
4765
4968
|
}
|
|
4766
4969
|
const SYNC_SKILL_CONTENT = `---
|
|
4767
4970
|
skill-version: ${SYNC_SKILL_VERSION}
|
|
@@ -5016,6 +5219,123 @@ async function sendCallback(callbackUrl, taskId, status, result, error) {
|
|
|
5016
5219
|
}
|
|
5017
5220
|
}
|
|
5018
5221
|
//#endregion
|
|
5222
|
+
//#region src/gateway/artifact-handler.ts
|
|
5223
|
+
const log$28 = larkLogger("gateway/artifacts");
|
|
5224
|
+
function createArtifactRouter(config) {
|
|
5225
|
+
const router = Router();
|
|
5226
|
+
const store = config?.store || new GatewayArtifactStore({ workspaceRoot: config?.workspaceRoot });
|
|
5227
|
+
router.post("/api/chat/artifacts", chatAuthMiddleware, (req, res) => {
|
|
5228
|
+
handleUpload(req, res, store);
|
|
5229
|
+
});
|
|
5230
|
+
router.get("/api/chat/artifacts", chatAuthMiddleware, (req, res) => {
|
|
5231
|
+
handleList(req, res, store);
|
|
5232
|
+
});
|
|
5233
|
+
return router;
|
|
5234
|
+
}
|
|
5235
|
+
async function handleList(req, res, store) {
|
|
5236
|
+
const sessionId = readString(req.query.session_id || req.query.sessionId);
|
|
5237
|
+
if (!sessionId) {
|
|
5238
|
+
res.status(400).json({ error: "session_id is required" });
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
5241
|
+
const artifacts = await store.list({
|
|
5242
|
+
sessionId,
|
|
5243
|
+
owner: toOwner(res.locals.chatUser)
|
|
5244
|
+
});
|
|
5245
|
+
res.json({ artifacts });
|
|
5246
|
+
}
|
|
5247
|
+
async function handleUpload(req, res, store) {
|
|
5248
|
+
if (!req.headers["content-type"]?.includes("multipart/form-data")) {
|
|
5249
|
+
res.status(415).json({ error: "Content-Type must be multipart/form-data" });
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
const chatUser = res.locals.chatUser;
|
|
5253
|
+
try {
|
|
5254
|
+
const artifact = await parseMultipartUpload(req, store, chatUser);
|
|
5255
|
+
res.status(201).json({
|
|
5256
|
+
artifact,
|
|
5257
|
+
sourceArtifacts: [artifact]
|
|
5258
|
+
});
|
|
5259
|
+
} catch (err) {
|
|
5260
|
+
const statusCode = err instanceof ArtifactUploadError ? err.statusCode : 500;
|
|
5261
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5262
|
+
log$28.warn("artifact upload failed", {
|
|
5263
|
+
statusCode,
|
|
5264
|
+
message
|
|
5265
|
+
});
|
|
5266
|
+
res.status(statusCode).json({ error: message });
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
function parseMultipartUpload(req, store, chatUser) {
|
|
5270
|
+
return new Promise((resolve, reject) => {
|
|
5271
|
+
const busboy = Busboy({ headers: req.headers });
|
|
5272
|
+
const owner = toOwner(chatUser);
|
|
5273
|
+
let sessionId = readString(req.query.session_id || req.query.sessionId) ?? "";
|
|
5274
|
+
let metadata;
|
|
5275
|
+
let uploadPromise;
|
|
5276
|
+
let fileSeen = false;
|
|
5277
|
+
let settled = false;
|
|
5278
|
+
const fail = (error) => {
|
|
5279
|
+
if (settled) return;
|
|
5280
|
+
settled = true;
|
|
5281
|
+
reject(error);
|
|
5282
|
+
};
|
|
5283
|
+
busboy.on("field", (name, value) => {
|
|
5284
|
+
if (name === "session_id" || name === "sessionId") sessionId = value.trim();
|
|
5285
|
+
else if (name === "metadata" && value.trim()) try {
|
|
5286
|
+
const parsed = JSON.parse(value);
|
|
5287
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) metadata = parsed;
|
|
5288
|
+
else fail(new ArtifactUploadError("metadata must be a JSON object"));
|
|
5289
|
+
} catch (err) {
|
|
5290
|
+
fail(new ArtifactUploadError(`metadata is not valid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
5291
|
+
}
|
|
5292
|
+
});
|
|
5293
|
+
busboy.on("file", (_name, file, info) => {
|
|
5294
|
+
if (fileSeen) {
|
|
5295
|
+
file.resume();
|
|
5296
|
+
fail(new ArtifactUploadError("Only one file is supported per upload"));
|
|
5297
|
+
return;
|
|
5298
|
+
}
|
|
5299
|
+
fileSeen = true;
|
|
5300
|
+
if (!sessionId) {
|
|
5301
|
+
file.resume();
|
|
5302
|
+
fail(new ArtifactUploadError("session_id is required before file field"));
|
|
5303
|
+
return;
|
|
5304
|
+
}
|
|
5305
|
+
const stream = new PassThrough();
|
|
5306
|
+
uploadPromise = store.create({
|
|
5307
|
+
sessionId,
|
|
5308
|
+
owner,
|
|
5309
|
+
fileName: info.filename || "upload.bin",
|
|
5310
|
+
mimeType: info.mimeType,
|
|
5311
|
+
metadata,
|
|
5312
|
+
stream
|
|
5313
|
+
});
|
|
5314
|
+
file.pipe(stream);
|
|
5315
|
+
});
|
|
5316
|
+
busboy.on("error", fail);
|
|
5317
|
+
busboy.on("finish", () => {
|
|
5318
|
+
if (settled) return;
|
|
5319
|
+
if (!fileSeen || !uploadPromise) {
|
|
5320
|
+
fail(new ArtifactUploadError("file is required"));
|
|
5321
|
+
return;
|
|
5322
|
+
}
|
|
5323
|
+
uploadPromise.then(resolve, reject);
|
|
5324
|
+
});
|
|
5325
|
+
req.pipe(busboy);
|
|
5326
|
+
});
|
|
5327
|
+
}
|
|
5328
|
+
function toOwner(chatUser) {
|
|
5329
|
+
return {
|
|
5330
|
+
tenantKey: chatUser.tenantKey,
|
|
5331
|
+
userId: chatUser.userId,
|
|
5332
|
+
openId: chatUser.openId
|
|
5333
|
+
};
|
|
5334
|
+
}
|
|
5335
|
+
function readString(value) {
|
|
5336
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
5337
|
+
}
|
|
5338
|
+
//#endregion
|
|
5019
5339
|
//#region src/gateway/execute-handler.ts
|
|
5020
5340
|
/**
|
|
5021
5341
|
* 执行接口路由 — 提供 CC 任务的创建、查询、取消和批量执行能力
|
|
@@ -6973,6 +7293,7 @@ function registerRoutes(app, processManager, scheduledTaskManager, appCredential
|
|
|
6973
7293
|
app.use("/hooks", createHooksRouter());
|
|
6974
7294
|
app.use("/api/mcp", createMcpCallbackRouter());
|
|
6975
7295
|
app.use(createChatAuthRouter());
|
|
7296
|
+
app.use(createArtifactRouter({ workspaceRoot }));
|
|
6976
7297
|
if (processManager && messageStore) app.use(createChatRouter({
|
|
6977
7298
|
processManager,
|
|
6978
7299
|
messageStore
|
|
@@ -8576,9 +8897,31 @@ function sortTraceValue(value) {
|
|
|
8576
8897
|
return value;
|
|
8577
8898
|
}
|
|
8578
8899
|
//#endregion
|
|
8900
|
+
//#region src/card/user-facing-error.ts
|
|
8901
|
+
function createUserFacingError(message, causeMessage) {
|
|
8902
|
+
const error = new Error(causeMessage || message);
|
|
8903
|
+
error.userFacingMessage = message;
|
|
8904
|
+
return error;
|
|
8905
|
+
}
|
|
8906
|
+
function getUserFacingErrorMessage(error) {
|
|
8907
|
+
if (!error || typeof error !== "object") return void 0;
|
|
8908
|
+
const message = error.userFacingMessage;
|
|
8909
|
+
return typeof message === "string" && message.trim() ? message : void 0;
|
|
8910
|
+
}
|
|
8911
|
+
//#endregion
|
|
8579
8912
|
//#region src/card/cc-stream-bridge.ts
|
|
8580
8913
|
const log$21 = larkLogger("card/cc-stream-bridge");
|
|
8581
8914
|
const CC_INTERNAL_PLACEHOLDER = "No response requested.";
|
|
8915
|
+
const CLAUDE_INVALID_PARAMETER_PATTERN = /API Error:\s*400\b.*parameter specified in the request is not valid/i;
|
|
8916
|
+
function buildClaudeCodeUserFacingError(errorMessage, hasImages) {
|
|
8917
|
+
if (hasImages && CLAUDE_INVALID_PARAMETER_PATTERN.test(errorMessage)) return createUserFacingError([
|
|
8918
|
+
"当前运行时无法处理这条图片消息。",
|
|
8919
|
+
"",
|
|
8920
|
+
"图片已经下载成功,但 Claude Code 当前模型接口返回了无效参数错误,通常表示当前模型或方舟 coding endpoint 不支持这类图片输入。",
|
|
8921
|
+
"请改用支持图片的 agent 引擎,或先用文字描述图片内容后再试。"
|
|
8922
|
+
].join("\n"), errorMessage);
|
|
8923
|
+
return new Error(errorMessage);
|
|
8924
|
+
}
|
|
8582
8925
|
/**
|
|
8583
8926
|
* CCStreamBridge — 将 CC 流事件桥接到 StreamingCardController
|
|
8584
8927
|
*
|
|
@@ -8745,11 +9088,13 @@ var CCStreamBridge = class {
|
|
|
8745
9088
|
this.controller.onIdle();
|
|
8746
9089
|
} else {
|
|
8747
9090
|
const errorMessage = data.result ?? `CC 执行失败: ${data.subtype}`;
|
|
9091
|
+
const hasImages = sessionId && pm ? pm.getLastMessageHadImages(sessionId) : false;
|
|
8748
9092
|
log$21.error("CC 执行返回错误", {
|
|
8749
9093
|
subtype: data.subtype,
|
|
9094
|
+
hasImages,
|
|
8750
9095
|
errorMessage: errorMessage.slice(0, 200)
|
|
8751
9096
|
});
|
|
8752
|
-
this.controller.onError(
|
|
9097
|
+
this.controller.onError(buildClaudeCodeUserFacingError(errorMessage, !!hasImages), { kind: "cc-execution" });
|
|
8753
9098
|
}
|
|
8754
9099
|
} else if (!this.accumulatedText.trim()) if (data.result) {
|
|
8755
9100
|
log$21.info("result 兜底:使用 result.result 作为最终回复", {
|
|
@@ -11923,8 +12268,9 @@ var StreamingCardController = class StreamingCardController {
|
|
|
11923
12268
|
const toolUseDisplay = this.computeToolUseDisplay();
|
|
11924
12269
|
try {
|
|
11925
12270
|
if (this.cardKit.cardMessageId) {
|
|
12271
|
+
const visibleError = getUserFacingErrorMessage(err) || "An error occurred while generating the response.";
|
|
11926
12272
|
const terminalContent = prepareTerminalCardContent({
|
|
11927
|
-
text: this.text.accumulatedText ? `${this.text.accumulatedText}\n\n---\n**Error**:
|
|
12273
|
+
text: this.text.accumulatedText ? `${this.text.accumulatedText}\n\n---\n**Error**: ${visibleError}` : `**Error**: ${visibleError}`,
|
|
11928
12274
|
reasoningText: this.reasoning.accumulatedReasoningText || void 0
|
|
11929
12275
|
}, this.imageResolver);
|
|
11930
12276
|
const errorCard = buildCardContent("complete", {
|