@vibe-lark/larkpal 0.1.61 → 0.1.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/larkpal.js +0 -0
- package/dist/main.mjs +580 -297
- package/package.json +16 -15
package/dist/main.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as LarkCliCredentialProvider } from "./lark-cli-provider-CdgwmqSz.mjs";
|
|
2
2
|
import { n as getLarkRuntime, r as setLarkRuntime, t as larkLogger } from "./lark-logger-D7_pEVQc.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import * as path$2 from "node:path";
|
|
6
6
|
import path, { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,7 +9,7 @@ import "dotenv/config";
|
|
|
9
9
|
import { access, lstat, mkdir, open, readFile, readdir, readlink, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { createInterface } from "node:readline";
|
|
14
14
|
import { v4, v5 } from "uuid";
|
|
15
15
|
import { mkdir as mkdir$1, readFile as readFile$1, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
|
|
@@ -19,6 +19,9 @@ import path$1 from "path";
|
|
|
19
19
|
import os from "os";
|
|
20
20
|
import { EventEmitter } from "node:events";
|
|
21
21
|
import express, { Router } from "express";
|
|
22
|
+
import Busboy from "busboy";
|
|
23
|
+
import { PassThrough, Readable, Transform } from "node:stream";
|
|
24
|
+
import { pipeline } from "node:stream/promises";
|
|
22
25
|
import { EventEmitter as EventEmitter$1 } from "events";
|
|
23
26
|
import cron from "node-cron";
|
|
24
27
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
@@ -27,7 +30,6 @@ import { SILENT_REPLY_TOKEN } from "openclaw/plugin-sdk/reply-runtime";
|
|
|
27
30
|
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
28
31
|
import * as dns from "node:dns/promises";
|
|
29
32
|
import * as net from "node:net";
|
|
30
|
-
import { Readable } from "node:stream";
|
|
31
33
|
//#region src/config/preflight.ts
|
|
32
34
|
/**
|
|
33
35
|
* 启动前置环境检查
|
|
@@ -38,7 +40,7 @@ import { Readable } from "node:stream";
|
|
|
38
40
|
*
|
|
39
41
|
* 本模块在启动时检测它们是否已安装,缺失时给出明确的安装指引。
|
|
40
42
|
*/
|
|
41
|
-
const log$
|
|
43
|
+
const log$40 = larkLogger("preflight");
|
|
42
44
|
function detectCli(name) {
|
|
43
45
|
try {
|
|
44
46
|
const cliPath = execFileSync("which", [name], {
|
|
@@ -76,14 +78,14 @@ function detectCli(name) {
|
|
|
76
78
|
* 缺失任一依赖时打印安装指引并抛出错误,阻止启动。
|
|
77
79
|
*/
|
|
78
80
|
function preflight() {
|
|
79
|
-
log$
|
|
81
|
+
log$40.info("开始前置环境检查...");
|
|
80
82
|
const useCodex = (process.env.LARKPAL_RUNTIME || "claude-code") === "codex-app-server";
|
|
81
83
|
const claude = detectCli("claude");
|
|
82
84
|
const codex = useCodex ? detectCli(process.env.LARKPAL_CODEX_BIN || "codex") : void 0;
|
|
83
85
|
const larkCli = detectCli("lark-cli");
|
|
84
86
|
const status = (dep) => dep.installed ? `✅ ${dep.name} (${dep.version ?? "unknown"}) → ${dep.path}` : `❌ ${dep.name} 未安装`;
|
|
85
|
-
log$
|
|
86
|
-
log$
|
|
87
|
+
log$40.info(` ${status(useCodex && codex ? codex : claude)}`);
|
|
88
|
+
log$40.info(` ${status(larkCli)}`);
|
|
87
89
|
const missing = [];
|
|
88
90
|
if (useCodex) {
|
|
89
91
|
if (!codex?.installed) missing.push([
|
|
@@ -119,10 +121,10 @@ function preflight() {
|
|
|
119
121
|
"",
|
|
120
122
|
"请安装缺失的依赖后重新启动 LarkPal。"
|
|
121
123
|
].join("\n");
|
|
122
|
-
log$
|
|
124
|
+
log$40.error(msg);
|
|
123
125
|
throw new Error("前置环境检查未通过,缺少必要依赖");
|
|
124
126
|
}
|
|
125
|
-
log$
|
|
127
|
+
log$40.info("前置环境检查通过 ✓");
|
|
126
128
|
return {
|
|
127
129
|
claude,
|
|
128
130
|
codex,
|
|
@@ -147,7 +149,7 @@ function preflight() {
|
|
|
147
149
|
* 同时生成新版 (~/.lark-cli/config.json) 和旧版 (~/.config/lark/config.json)
|
|
148
150
|
* 格式的配置文件,保证 lark-cli 无论通过哪种路径读取都能正常工作。
|
|
149
151
|
*/
|
|
150
|
-
const log$
|
|
152
|
+
const log$39 = larkLogger("config/ensure-lark-cli");
|
|
151
153
|
/** 新版 lark-cli 配置路径 */
|
|
152
154
|
const NEW_CONFIG_DIR = join(homedir(), ".lark-cli");
|
|
153
155
|
const NEW_CONFIG_PATH = join(NEW_CONFIG_DIR, "config.json");
|
|
@@ -164,19 +166,19 @@ function ensureLarkCliConfig() {
|
|
|
164
166
|
const appId = process.env.LARK_APP_ID;
|
|
165
167
|
const appSecret = process.env.LARK_APP_SECRET;
|
|
166
168
|
if (!appId || !appSecret) {
|
|
167
|
-
log$
|
|
169
|
+
log$39.info("环境变量凭证未设置,跳过 lark-cli 配置同步");
|
|
168
170
|
return;
|
|
169
171
|
}
|
|
170
172
|
const hasNewConfig = existsSync(NEW_CONFIG_PATH);
|
|
171
173
|
const hasLegacyConfig = existsSync(LEGACY_CONFIG_PATH);
|
|
172
174
|
if (hasNewConfig && hasLegacyConfig) {
|
|
173
|
-
log$
|
|
175
|
+
log$39.info("lark-cli 配置文件已存在,跳过同步", {
|
|
174
176
|
newConfig: NEW_CONFIG_PATH,
|
|
175
177
|
legacyConfig: LEGACY_CONFIG_PATH
|
|
176
178
|
});
|
|
177
179
|
return;
|
|
178
180
|
}
|
|
179
|
-
log$
|
|
181
|
+
log$39.info("检测到环境变量凭证但 lark-cli 配置文件缺失,开始同步", {
|
|
180
182
|
appId,
|
|
181
183
|
hasNewConfig,
|
|
182
184
|
hasLegacyConfig
|
|
@@ -195,9 +197,9 @@ function ensureLarkCliConfig() {
|
|
|
195
197
|
encoding: "utf-8",
|
|
196
198
|
mode: 384
|
|
197
199
|
});
|
|
198
|
-
log$
|
|
200
|
+
log$39.info("已生成新版 lark-cli 配置文件", { path: NEW_CONFIG_PATH });
|
|
199
201
|
} catch (err) {
|
|
200
|
-
log$
|
|
202
|
+
log$39.warn("生成新版 lark-cli 配置文件失败", {
|
|
201
203
|
path: NEW_CONFIG_PATH,
|
|
202
204
|
error: err instanceof Error ? err.message : String(err)
|
|
203
205
|
});
|
|
@@ -216,9 +218,9 @@ function ensureLarkCliConfig() {
|
|
|
216
218
|
encoding: "utf-8",
|
|
217
219
|
mode: 384
|
|
218
220
|
});
|
|
219
|
-
log$
|
|
221
|
+
log$39.info("已生成旧版 lark-cli 配置文件", { path: LEGACY_CONFIG_PATH });
|
|
220
222
|
} catch (err) {
|
|
221
|
-
log$
|
|
223
|
+
log$39.warn("生成旧版 lark-cli 配置文件失败", {
|
|
222
224
|
path: LEGACY_CONFIG_PATH,
|
|
223
225
|
error: err instanceof Error ? err.message : String(err)
|
|
224
226
|
});
|
|
@@ -439,7 +441,7 @@ async function ensureDefaults() {
|
|
|
439
441
|
* - github: { type: "api_key", token }
|
|
440
442
|
* - custom-api: { type: "http_bearer", base_url, token }
|
|
441
443
|
*/
|
|
442
|
-
const log$
|
|
444
|
+
const log$38 = larkLogger("user/credential-vault");
|
|
443
445
|
var CredentialVault = class {
|
|
444
446
|
credentialPath;
|
|
445
447
|
encryptionKey;
|
|
@@ -451,12 +453,12 @@ var CredentialVault = class {
|
|
|
451
453
|
if (keyEnv) {
|
|
452
454
|
this.encryptionKey = keyEnv.length === 64 ? Buffer.from(keyEnv, "hex") : Buffer.from(keyEnv, "base64");
|
|
453
455
|
if (this.encryptionKey.length !== 32) {
|
|
454
|
-
log$
|
|
456
|
+
log$38.error("LARKPAL_ENCRYPTION_KEY 长度不正确,需要 32 字节", { actualLength: this.encryptionKey.length });
|
|
455
457
|
this.encryptionKey = null;
|
|
456
|
-
} else log$
|
|
458
|
+
} else log$38.info("凭证加密已启用");
|
|
457
459
|
} else {
|
|
458
460
|
this.encryptionKey = null;
|
|
459
|
-
log$
|
|
461
|
+
log$38.warn("LARKPAL_ENCRYPTION_KEY 未设置,凭证将以明文存储(仅限开发模式)");
|
|
460
462
|
}
|
|
461
463
|
}
|
|
462
464
|
/**
|
|
@@ -464,7 +466,7 @@ var CredentialVault = class {
|
|
|
464
466
|
*/
|
|
465
467
|
async load() {
|
|
466
468
|
if (!existsSync$1(this.credentialPath)) {
|
|
467
|
-
log$
|
|
469
|
+
log$38.info("凭证文件不存在,使用空凭证", { path: this.credentialPath });
|
|
468
470
|
this.store = {};
|
|
469
471
|
this.loaded = true;
|
|
470
472
|
return;
|
|
@@ -473,12 +475,12 @@ var CredentialVault = class {
|
|
|
473
475
|
const raw = await readFile$1(this.credentialPath, "utf-8");
|
|
474
476
|
this.store = JSON.parse(raw);
|
|
475
477
|
this.loaded = true;
|
|
476
|
-
log$
|
|
478
|
+
log$38.info("凭证文件加载完成", {
|
|
477
479
|
path: this.credentialPath,
|
|
478
480
|
credentialCount: Object.keys(this.store).length
|
|
479
481
|
});
|
|
480
482
|
} catch (err) {
|
|
481
|
-
log$
|
|
483
|
+
log$38.error("凭证文件加载失败", {
|
|
482
484
|
path: this.credentialPath,
|
|
483
485
|
error: err instanceof Error ? err.message : String(err)
|
|
484
486
|
});
|
|
@@ -492,12 +494,12 @@ var CredentialVault = class {
|
|
|
492
494
|
async save() {
|
|
493
495
|
try {
|
|
494
496
|
await writeFile$1(this.credentialPath, JSON.stringify(this.store, null, 2), "utf-8");
|
|
495
|
-
log$
|
|
497
|
+
log$38.info("凭证文件保存完成", {
|
|
496
498
|
path: this.credentialPath,
|
|
497
499
|
credentialCount: Object.keys(this.store).length
|
|
498
500
|
});
|
|
499
501
|
} catch (err) {
|
|
500
|
-
log$
|
|
502
|
+
log$38.error("凭证文件保存失败", {
|
|
501
503
|
path: this.credentialPath,
|
|
502
504
|
error: err instanceof Error ? err.message : String(err)
|
|
503
505
|
});
|
|
@@ -519,7 +521,7 @@ var CredentialVault = class {
|
|
|
519
521
|
*/
|
|
520
522
|
async setCredential(name, credential) {
|
|
521
523
|
if (!this.loaded) await this.load();
|
|
522
|
-
log$
|
|
524
|
+
log$38.info("设置凭证", {
|
|
523
525
|
name,
|
|
524
526
|
fields: Object.keys(credential)
|
|
525
527
|
});
|
|
@@ -535,7 +537,7 @@ var CredentialVault = class {
|
|
|
535
537
|
if (!(name in this.store)) return false;
|
|
536
538
|
delete this.store[name];
|
|
537
539
|
await this.save();
|
|
538
|
-
log$
|
|
540
|
+
log$38.info("凭证已删除", { name });
|
|
539
541
|
return true;
|
|
540
542
|
}
|
|
541
543
|
/**
|
|
@@ -564,7 +566,7 @@ var CredentialVault = class {
|
|
|
564
566
|
envVars[envKey] = String(fieldValue);
|
|
565
567
|
}
|
|
566
568
|
} catch (err) {
|
|
567
|
-
log$
|
|
569
|
+
log$38.warn("凭证解密失败,跳过", {
|
|
568
570
|
name,
|
|
569
571
|
error: err instanceof Error ? err.message : String(err)
|
|
570
572
|
});
|
|
@@ -621,7 +623,7 @@ var CredentialVault = class {
|
|
|
621
623
|
* - B 用户的 CC 进程连接 B 的数据库 MCP Server
|
|
622
624
|
* - 全局工具(lark-cli、文件操作等)所有用户共享
|
|
623
625
|
*/
|
|
624
|
-
const log$
|
|
626
|
+
const log$37 = larkLogger("user/mcp-merge");
|
|
625
627
|
/** 全局 MCP 配置路径 */
|
|
626
628
|
const GLOBAL_MCP_CONFIG_PATH = path$1.join(os.homedir(), ".claude", "mcp-servers.json");
|
|
627
629
|
/**
|
|
@@ -677,9 +679,9 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
677
679
|
if (existsSync$1(GLOBAL_MCP_CONFIG_PATH)) try {
|
|
678
680
|
const raw = await readFile$1(GLOBAL_MCP_CONFIG_PATH, "utf-8");
|
|
679
681
|
globalConfig = JSON.parse(raw);
|
|
680
|
-
log$
|
|
682
|
+
log$37.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
|
|
681
683
|
} catch (err) {
|
|
682
|
-
log$
|
|
684
|
+
log$37.warn("全局 MCP 配置文件解析失败", {
|
|
683
685
|
path: GLOBAL_MCP_CONFIG_PATH,
|
|
684
686
|
error: err instanceof Error ? err.message : String(err)
|
|
685
687
|
});
|
|
@@ -690,13 +692,13 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
690
692
|
if (existsSync$1(userMcpPath)) try {
|
|
691
693
|
const raw = await readFile$1(userMcpPath, "utf-8");
|
|
692
694
|
userConfig = JSON.parse(raw);
|
|
693
|
-
log$
|
|
695
|
+
log$37.info("用户 MCP 配置加载完成", {
|
|
694
696
|
userId: userCtx.userId,
|
|
695
697
|
serverCount: Object.keys(userConfig).length,
|
|
696
698
|
servers: Object.keys(userConfig)
|
|
697
699
|
});
|
|
698
700
|
} catch (err) {
|
|
699
|
-
log$
|
|
701
|
+
log$37.warn("用户 MCP 配置文件解析失败", {
|
|
700
702
|
userId: userCtx?.userId,
|
|
701
703
|
path: userMcpPath,
|
|
702
704
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -708,7 +710,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
708
710
|
...globalConfig,
|
|
709
711
|
...userConfig
|
|
710
712
|
};
|
|
711
|
-
log$
|
|
713
|
+
log$37.info("MCP 配置合并完成", {
|
|
712
714
|
userId: userCtx?.userId,
|
|
713
715
|
builtinServers: Object.keys(builtinConfig),
|
|
714
716
|
globalServers: Object.keys(globalConfig),
|
|
@@ -717,7 +719,7 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
717
719
|
});
|
|
718
720
|
const tmpPath = path$1.join(os.tmpdir(), `mcp-${sessionId}.json`);
|
|
719
721
|
await writeFile$1(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
|
|
720
|
-
log$
|
|
722
|
+
log$37.info("MCP 合并配置已写入临时文件", {
|
|
721
723
|
path: tmpPath,
|
|
722
724
|
serverCount: Object.keys(merged).length
|
|
723
725
|
});
|
|
@@ -731,7 +733,7 @@ async function cleanupMcpConfig(sessionId) {
|
|
|
731
733
|
try {
|
|
732
734
|
if (existsSync$1(tmpPath)) {
|
|
733
735
|
await unlink$1(tmpPath);
|
|
734
|
-
log$
|
|
736
|
+
log$37.debug("MCP 临时配置文件已清理", { path: tmpPath });
|
|
735
737
|
}
|
|
736
738
|
} catch {}
|
|
737
739
|
}
|
|
@@ -779,7 +781,7 @@ function getToolDisplayName(toolName) {
|
|
|
779
781
|
* - result(data) — 最终结果
|
|
780
782
|
* - parseError(error, rawLine) — 解析错误(不中断流)
|
|
781
783
|
*/
|
|
782
|
-
const log$
|
|
784
|
+
const log$36 = larkLogger("cc-runtime/stream-parser");
|
|
783
785
|
var CCStreamParser = class extends EventEmitter {
|
|
784
786
|
/**
|
|
785
787
|
* 解析一行 NDJSON 文本
|
|
@@ -793,7 +795,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
793
795
|
try {
|
|
794
796
|
msg = JSON.parse(trimmed);
|
|
795
797
|
} catch (err) {
|
|
796
|
-
log$
|
|
798
|
+
log$36.warn("NDJSON 解析失败,跳过该行", {
|
|
797
799
|
error: String(err),
|
|
798
800
|
rawLine: trimmed.slice(0, 200)
|
|
799
801
|
});
|
|
@@ -801,7 +803,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
801
803
|
return;
|
|
802
804
|
}
|
|
803
805
|
if (msg.parent_tool_use_id != null && msg.type !== "result") {
|
|
804
|
-
log$
|
|
806
|
+
log$36.debug("跳过 subagent 内部事件", {
|
|
805
807
|
type: msg.type,
|
|
806
808
|
parentToolUseId: msg.parent_tool_use_id
|
|
807
809
|
});
|
|
@@ -824,14 +826,14 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
824
826
|
this.handleResult(msg);
|
|
825
827
|
break;
|
|
826
828
|
case "system":
|
|
827
|
-
log$
|
|
829
|
+
log$36.info("收到 system 消息", {
|
|
828
830
|
subtype: msg.subtype,
|
|
829
831
|
detail: JSON.stringify(msg).slice(0, 500)
|
|
830
832
|
});
|
|
831
833
|
this.emit("system", msg);
|
|
832
834
|
break;
|
|
833
835
|
default:
|
|
834
|
-
log$
|
|
836
|
+
log$36.debug("收到未知消息类型,已忽略", { type: msg.type });
|
|
835
837
|
break;
|
|
836
838
|
}
|
|
837
839
|
}
|
|
@@ -850,13 +852,13 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
850
852
|
switch (event.type) {
|
|
851
853
|
case "content_block_start": {
|
|
852
854
|
const block = event.content_block;
|
|
853
|
-
log$
|
|
855
|
+
log$36.debug("content_block_start 事件", {
|
|
854
856
|
blockType: block?.type,
|
|
855
857
|
toolName: block?.name,
|
|
856
858
|
toolUseId: block?.id
|
|
857
859
|
});
|
|
858
860
|
if (block?.type === "tool_use" && block.name) {
|
|
859
|
-
log$
|
|
861
|
+
log$36.info("工具调用开始", {
|
|
860
862
|
toolName: block.name,
|
|
861
863
|
displayName: getToolDisplayName(block.name),
|
|
862
864
|
toolUseId: block.id
|
|
@@ -875,7 +877,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
875
877
|
case "message_delta": {
|
|
876
878
|
const stopReason = event.delta?.stop_reason;
|
|
877
879
|
if (stopReason) {
|
|
878
|
-
log$
|
|
880
|
+
log$36.info("轮次结束", { stopReason });
|
|
879
881
|
this.emit("turnEnd", stopReason);
|
|
880
882
|
}
|
|
881
883
|
break;
|
|
@@ -895,7 +897,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
895
897
|
const content = msg.message?.content;
|
|
896
898
|
const stopReason = msg.message?.stop_reason;
|
|
897
899
|
const blockTypes = Array.isArray(content) ? content.map((b) => `${b.type}${b.name ? ":" + b.name : ""}`).join(", ") : "none";
|
|
898
|
-
log$
|
|
900
|
+
log$36.info("收到完整 assistant 消息", {
|
|
899
901
|
model: msg.message?.model,
|
|
900
902
|
stopReason,
|
|
901
903
|
contentBlocks: content?.length,
|
|
@@ -903,7 +905,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
903
905
|
});
|
|
904
906
|
if (Array.isArray(content)) {
|
|
905
907
|
for (const block of content) if (block.type === "tool_use" && block.name) {
|
|
906
|
-
log$
|
|
908
|
+
log$36.info("从 assistant 消息提取工具调用", {
|
|
907
909
|
toolName: block.name,
|
|
908
910
|
displayName: getToolDisplayName(block.name),
|
|
909
911
|
toolUseId: block.id
|
|
@@ -922,7 +924,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
922
924
|
const content = msg.message?.content;
|
|
923
925
|
if (!Array.isArray(content)) return;
|
|
924
926
|
for (const item of content) if (item.type === "tool_result" && item.tool_use_id) {
|
|
925
|
-
log$
|
|
927
|
+
log$36.debug("工具结果返回", { toolUseId: item.tool_use_id });
|
|
926
928
|
this.emit("toolResult", item.tool_use_id);
|
|
927
929
|
}
|
|
928
930
|
}
|
|
@@ -930,7 +932,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
930
932
|
* 处理 tool_progress 消息 — 工具执行进度
|
|
931
933
|
*/
|
|
932
934
|
handleToolProgress(msg) {
|
|
933
|
-
log$
|
|
935
|
+
log$36.debug("工具执行进度", {
|
|
934
936
|
toolName: msg.tool_name,
|
|
935
937
|
elapsed: msg.elapsed_time_seconds
|
|
936
938
|
});
|
|
@@ -940,7 +942,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
940
942
|
* 处理 result 消息 — CC 执行完成的最终结果
|
|
941
943
|
*/
|
|
942
944
|
handleResult(msg) {
|
|
943
|
-
log$
|
|
945
|
+
log$36.info("CC 执行完成", {
|
|
944
946
|
subtype: msg.subtype,
|
|
945
947
|
isError: msg.is_error,
|
|
946
948
|
durationMs: msg.duration_ms,
|
|
@@ -973,7 +975,7 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
973
975
|
* 多轮对话:进程内自动保持完整对话上下文(mutableMessages 数组累积),
|
|
974
976
|
* 无需外部维护历史。
|
|
975
977
|
*/
|
|
976
|
-
const log$
|
|
978
|
+
const log$35 = larkLogger("cc-runtime/process-manager");
|
|
977
979
|
/**
|
|
978
980
|
* LarkPal 会话 UUID v5 命名空间
|
|
979
981
|
*
|
|
@@ -1031,6 +1033,8 @@ var SessionProcessManager = class {
|
|
|
1031
1033
|
* 用于 rewind_files 定位回退点。每次 sendMessage 时更新。
|
|
1032
1034
|
*/
|
|
1033
1035
|
lastUserMessageIds = /* @__PURE__ */ new Map();
|
|
1036
|
+
/** 最近一条用户消息是否包含图片:sessionId → hasImages */
|
|
1037
|
+
lastMessageHadImages = /* @__PURE__ */ new Map();
|
|
1034
1038
|
/**
|
|
1035
1039
|
* 控制请求等待表:request_id → { resolve, reject, timer }
|
|
1036
1040
|
*
|
|
@@ -1061,6 +1065,10 @@ var SessionProcessManager = class {
|
|
|
1061
1065
|
}
|
|
1062
1066
|
return false;
|
|
1063
1067
|
}
|
|
1068
|
+
/** 最近一条发送给 Claude Code 的用户消息是否包含图片 */
|
|
1069
|
+
getLastMessageHadImages(sessionId) {
|
|
1070
|
+
return this.lastMessageHadImages.get(sessionId) === true;
|
|
1071
|
+
}
|
|
1064
1072
|
/**
|
|
1065
1073
|
* 创建消息完成 Promise,用于阻塞 executePrompt 直到 CC 返回 result
|
|
1066
1074
|
*/
|
|
@@ -1094,9 +1102,9 @@ var SessionProcessManager = class {
|
|
|
1094
1102
|
const t0 = Date.now();
|
|
1095
1103
|
const prevLock = this.sessionLocks.get(sessionId);
|
|
1096
1104
|
if (prevLock) {
|
|
1097
|
-
log$
|
|
1105
|
+
log$35.info("等待上一条消息处理完成", { sessionId });
|
|
1098
1106
|
await prevLock;
|
|
1099
|
-
log$
|
|
1107
|
+
log$35.info("[perf] 串行锁等待完成", {
|
|
1100
1108
|
sessionId,
|
|
1101
1109
|
waitMs: Date.now() - t0
|
|
1102
1110
|
});
|
|
@@ -1110,7 +1118,7 @@ var SessionProcessManager = class {
|
|
|
1110
1118
|
try {
|
|
1111
1119
|
const existing = this.processes.get(sessionId);
|
|
1112
1120
|
if (existing && existing.status === "running" && existing.useStreamInput) {
|
|
1113
|
-
log$
|
|
1121
|
+
log$35.info("向常驻进程发送消息", {
|
|
1114
1122
|
sessionId,
|
|
1115
1123
|
promptLength: config.prompt.length,
|
|
1116
1124
|
pid: existing.childProcess.pid,
|
|
@@ -1120,14 +1128,14 @@ var SessionProcessManager = class {
|
|
|
1120
1128
|
this.sendMessage(sessionId, config.prompt);
|
|
1121
1129
|
this.resetIdleTimer(sessionId);
|
|
1122
1130
|
await completionPromise;
|
|
1123
|
-
log$
|
|
1131
|
+
log$35.info("[perf] executePrompt 完成(热进程)", {
|
|
1124
1132
|
sessionId,
|
|
1125
1133
|
totalMs: Date.now() - t0
|
|
1126
1134
|
});
|
|
1127
1135
|
return;
|
|
1128
1136
|
}
|
|
1129
1137
|
if (existing && (existing.status === "stopped" || existing.status === "crashed")) {
|
|
1130
|
-
log$
|
|
1138
|
+
log$35.info("清理已终止的旧进程,准备重启", {
|
|
1131
1139
|
sessionId,
|
|
1132
1140
|
oldStatus: existing.status
|
|
1133
1141
|
});
|
|
@@ -1135,25 +1143,25 @@ var SessionProcessManager = class {
|
|
|
1135
1143
|
this.sessionLocks.set(sessionId, sessionLock);
|
|
1136
1144
|
}
|
|
1137
1145
|
if (existing && existing.status === "running" && !existing.useStreamInput) {
|
|
1138
|
-
log$
|
|
1146
|
+
log$35.warn("停止旧的单次进程后重新启动为常驻模式", { sessionId });
|
|
1139
1147
|
await this.stopProcess(sessionId);
|
|
1140
1148
|
this.sessionLocks.set(sessionId, sessionLock);
|
|
1141
1149
|
}
|
|
1142
1150
|
const completionPromise = this.createMessageCompletionPromise(sessionId);
|
|
1143
1151
|
const tStart = Date.now();
|
|
1144
1152
|
await this.startPersistentProcess(config);
|
|
1145
|
-
log$
|
|
1153
|
+
log$35.info("[perf] 冷启动进程完成", {
|
|
1146
1154
|
sessionId,
|
|
1147
1155
|
coldStartMs: Date.now() - tStart
|
|
1148
1156
|
});
|
|
1149
1157
|
await completionPromise;
|
|
1150
|
-
log$
|
|
1158
|
+
log$35.info("[perf] executePrompt 完成(冷启动)", {
|
|
1151
1159
|
sessionId,
|
|
1152
1160
|
totalMs: Date.now() - t0
|
|
1153
1161
|
});
|
|
1154
1162
|
} finally {
|
|
1155
1163
|
if (config.transcriptMode === "ephemeral") await this.stopProcess(sessionId).catch((err) => {
|
|
1156
|
-
log$
|
|
1164
|
+
log$35.warn("ephemeral CC 进程清理失败", {
|
|
1157
1165
|
sessionId,
|
|
1158
1166
|
error: err instanceof Error ? err.message : String(err)
|
|
1159
1167
|
});
|
|
@@ -1192,13 +1200,13 @@ var SessionProcessManager = class {
|
|
|
1192
1200
|
try {
|
|
1193
1201
|
const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId, cwd);
|
|
1194
1202
|
args.push("--mcp-config", mcpConfigPath);
|
|
1195
|
-
log$
|
|
1203
|
+
log$35.info("MCP 配置已合并并注入到 CC 进程", {
|
|
1196
1204
|
sessionId,
|
|
1197
1205
|
userId: config.userContext?.userId,
|
|
1198
1206
|
mcpConfigPath
|
|
1199
1207
|
});
|
|
1200
1208
|
} catch (err) {
|
|
1201
|
-
log$
|
|
1209
|
+
log$35.warn("MCP 配置合并失败,CC 进程将使用全局默认配置", {
|
|
1202
1210
|
sessionId,
|
|
1203
1211
|
error: err instanceof Error ? err.message : String(err)
|
|
1204
1212
|
});
|
|
@@ -1225,12 +1233,12 @@ var SessionProcessManager = class {
|
|
|
1225
1233
|
if (larkCred?.user_access_token && typeof larkCred.user_access_token === "string") processEnv.LARK_USER_ACCESS_TOKEN = larkCred.user_access_token;
|
|
1226
1234
|
} else {
|
|
1227
1235
|
delete processEnv.LARK_USER_ACCESS_TOKEN;
|
|
1228
|
-
log$
|
|
1236
|
+
log$35.info("LARKPAL_DISABLE_USER_AUTH 已启用,跳过飞书 user_access_token 注入", {
|
|
1229
1237
|
sessionId,
|
|
1230
1238
|
userId: userContext.userId
|
|
1231
1239
|
});
|
|
1232
1240
|
}
|
|
1233
|
-
log$
|
|
1241
|
+
log$35.info("用户凭证已注入到 CC 进程环境", {
|
|
1234
1242
|
sessionId,
|
|
1235
1243
|
userId: userContext.userId,
|
|
1236
1244
|
credentialCount: Object.keys(credentialEnvVars).length,
|
|
@@ -1238,14 +1246,14 @@ var SessionProcessManager = class {
|
|
|
1238
1246
|
disableUserAuth
|
|
1239
1247
|
});
|
|
1240
1248
|
} catch (err) {
|
|
1241
|
-
log$
|
|
1249
|
+
log$35.warn("用户凭证加载失败,CC 进程将使用默认环境", {
|
|
1242
1250
|
sessionId,
|
|
1243
1251
|
userId: userContext.userId,
|
|
1244
1252
|
error: err instanceof Error ? err.message : String(err)
|
|
1245
1253
|
});
|
|
1246
1254
|
}
|
|
1247
1255
|
}
|
|
1248
|
-
log$
|
|
1256
|
+
log$35.info("启动常驻 CC 进程", {
|
|
1249
1257
|
sessionId,
|
|
1250
1258
|
claudeSessionId,
|
|
1251
1259
|
isResuming,
|
|
@@ -1285,7 +1293,7 @@ var SessionProcessManager = class {
|
|
|
1285
1293
|
if (pending) {
|
|
1286
1294
|
clearTimeout(pending.timer);
|
|
1287
1295
|
this.pendingControlRequests.delete(msg.response.request_id);
|
|
1288
|
-
log$
|
|
1296
|
+
log$35.info("收到 control_response", {
|
|
1289
1297
|
sessionId,
|
|
1290
1298
|
requestId: msg.response.request_id,
|
|
1291
1299
|
subtype: msg.response.subtype,
|
|
@@ -1302,7 +1310,7 @@ var SessionProcessManager = class {
|
|
|
1302
1310
|
if (child.stderr) createInterface({ input: child.stderr }).on("line", (line) => {
|
|
1303
1311
|
if (line.includes("no stdin data received")) return;
|
|
1304
1312
|
if (line.toLowerCase().includes("already in use")) stderrSessionInUse = true;
|
|
1305
|
-
log$
|
|
1313
|
+
log$35.warn("CC 进程 stderr", {
|
|
1306
1314
|
sessionId,
|
|
1307
1315
|
line: line.slice(0, 500)
|
|
1308
1316
|
});
|
|
@@ -1311,7 +1319,7 @@ var SessionProcessManager = class {
|
|
|
1311
1319
|
const exitCode = code ?? -1;
|
|
1312
1320
|
const exitSignal = signal ?? "none";
|
|
1313
1321
|
if (exitCode === 1 && stderrSessionInUse && !isResuming) {
|
|
1314
|
-
log$
|
|
1322
|
+
log$35.info("Session ID 已被占用,准备使用 --resume 模式重试", {
|
|
1315
1323
|
sessionId,
|
|
1316
1324
|
claudeSessionId
|
|
1317
1325
|
});
|
|
@@ -1324,21 +1332,21 @@ var SessionProcessManager = class {
|
|
|
1324
1332
|
});
|
|
1325
1333
|
return;
|
|
1326
1334
|
} catch (retryErr) {
|
|
1327
|
-
log$
|
|
1335
|
+
log$35.error("Session ID 重试失败", {
|
|
1328
1336
|
sessionId,
|
|
1329
1337
|
error: retryErr instanceof Error ? retryErr.message : String(retryErr)
|
|
1330
1338
|
});
|
|
1331
1339
|
}
|
|
1332
1340
|
}
|
|
1333
1341
|
if (exitCode === 0) {
|
|
1334
|
-
log$
|
|
1342
|
+
log$35.info("CC 常驻进程正常退出", {
|
|
1335
1343
|
sessionId,
|
|
1336
1344
|
exitCode,
|
|
1337
1345
|
signal: exitSignal
|
|
1338
1346
|
});
|
|
1339
1347
|
ccProcess.status = "stopped";
|
|
1340
1348
|
} else {
|
|
1341
|
-
log$
|
|
1349
|
+
log$35.error("CC 常驻进程异常退出", {
|
|
1342
1350
|
sessionId,
|
|
1343
1351
|
exitCode,
|
|
1344
1352
|
signal: exitSignal
|
|
@@ -1350,7 +1358,7 @@ var SessionProcessManager = class {
|
|
|
1350
1358
|
this.cleanup(sessionId);
|
|
1351
1359
|
});
|
|
1352
1360
|
child.on("error", (err) => {
|
|
1353
|
-
log$
|
|
1361
|
+
log$35.error("CC 常驻进程启动失败", {
|
|
1354
1362
|
sessionId,
|
|
1355
1363
|
error: err.message
|
|
1356
1364
|
});
|
|
@@ -1360,7 +1368,7 @@ var SessionProcessManager = class {
|
|
|
1360
1368
|
this.cleanup(sessionId);
|
|
1361
1369
|
});
|
|
1362
1370
|
ccProcess.status = "running";
|
|
1363
|
-
log$
|
|
1371
|
+
log$35.info("CC 常驻进程已启动", {
|
|
1364
1372
|
sessionId,
|
|
1365
1373
|
pid: child.pid,
|
|
1366
1374
|
isResuming
|
|
@@ -1388,7 +1396,7 @@ var SessionProcessManager = class {
|
|
|
1388
1396
|
parser.on("textDelta", (text) => {
|
|
1389
1397
|
if (!firstTokenLogged) {
|
|
1390
1398
|
firstTokenLogged = true;
|
|
1391
|
-
log$
|
|
1399
|
+
log$35.info("[perf] 首 token (text)", {
|
|
1392
1400
|
sessionId,
|
|
1393
1401
|
ttftMs: Date.now() - messageSentAt
|
|
1394
1402
|
});
|
|
@@ -1398,7 +1406,7 @@ var SessionProcessManager = class {
|
|
|
1398
1406
|
parser.on("thinkingDelta", (text) => {
|
|
1399
1407
|
if (!firstTokenLogged) {
|
|
1400
1408
|
firstTokenLogged = true;
|
|
1401
|
-
log$
|
|
1409
|
+
log$35.info("[perf] 首 token (thinking)", {
|
|
1402
1410
|
sessionId,
|
|
1403
1411
|
ttftMs: Date.now() - messageSentAt
|
|
1404
1412
|
});
|
|
@@ -1415,7 +1423,7 @@ var SessionProcessManager = class {
|
|
|
1415
1423
|
getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds);
|
|
1416
1424
|
});
|
|
1417
1425
|
parser.on("turnEnd", (stopReason) => {
|
|
1418
|
-
log$
|
|
1426
|
+
log$35.info("[perf] turnEnd", {
|
|
1419
1427
|
sessionId,
|
|
1420
1428
|
stopReason,
|
|
1421
1429
|
sinceMessageMs: Date.now() - messageSentAt
|
|
@@ -1423,7 +1431,7 @@ var SessionProcessManager = class {
|
|
|
1423
1431
|
getCallbacks()?.onTurnEnd?.(stopReason);
|
|
1424
1432
|
});
|
|
1425
1433
|
parser.on("result", (result) => {
|
|
1426
|
-
log$
|
|
1434
|
+
log$35.info("[perf] result 事件", {
|
|
1427
1435
|
sessionId,
|
|
1428
1436
|
sinceMessageMs: Date.now() - messageSentAt
|
|
1429
1437
|
});
|
|
@@ -1431,7 +1439,7 @@ var SessionProcessManager = class {
|
|
|
1431
1439
|
this.resolveMessageCompletion(sessionId);
|
|
1432
1440
|
});
|
|
1433
1441
|
parser.on("parseError", (error, rawLine) => {
|
|
1434
|
-
log$
|
|
1442
|
+
log$35.warn("流解析错误", {
|
|
1435
1443
|
sessionId,
|
|
1436
1444
|
error: error.message,
|
|
1437
1445
|
rawLine: rawLine.slice(0, 200)
|
|
@@ -1447,7 +1455,7 @@ var SessionProcessManager = class {
|
|
|
1447
1455
|
sendMessage(sessionId, message) {
|
|
1448
1456
|
const proc = this.processes.get(sessionId);
|
|
1449
1457
|
if (!proc || !proc.childProcess.stdin) {
|
|
1450
|
-
log$
|
|
1458
|
+
log$35.error("无法发送消息:进程不存在或 stdin 不可用", { sessionId });
|
|
1451
1459
|
return;
|
|
1452
1460
|
}
|
|
1453
1461
|
proc._resetPerfTimer?.();
|
|
@@ -1463,16 +1471,19 @@ var SessionProcessManager = class {
|
|
|
1463
1471
|
uuid: userMessageId
|
|
1464
1472
|
});
|
|
1465
1473
|
const messageLength = typeof message === "string" ? message.length : message.length;
|
|
1466
|
-
|
|
1474
|
+
const hasImages = Array.isArray(message) && message.some((block) => block.type === "image");
|
|
1475
|
+
this.lastMessageHadImages.set(sessionId, hasImages);
|
|
1476
|
+
log$35.info("通过 stdin 发送用户消息", {
|
|
1467
1477
|
sessionId,
|
|
1468
1478
|
messageLength,
|
|
1469
1479
|
isMultimodal: Array.isArray(message),
|
|
1480
|
+
hasImages,
|
|
1470
1481
|
userMessageId,
|
|
1471
1482
|
pid: proc.childProcess.pid
|
|
1472
1483
|
});
|
|
1473
1484
|
proc.childProcess.stdin.write(payload + "\n", (err) => {
|
|
1474
1485
|
if (err) {
|
|
1475
|
-
log$
|
|
1486
|
+
log$35.error("stdin 写入失败", {
|
|
1476
1487
|
sessionId,
|
|
1477
1488
|
error: err.message
|
|
1478
1489
|
});
|
|
@@ -1502,7 +1513,7 @@ var SessionProcessManager = class {
|
|
|
1502
1513
|
request_id: requestId,
|
|
1503
1514
|
request
|
|
1504
1515
|
});
|
|
1505
|
-
log$
|
|
1516
|
+
log$35.info("发送控制请求", {
|
|
1506
1517
|
sessionId,
|
|
1507
1518
|
subtype,
|
|
1508
1519
|
requestId,
|
|
@@ -1558,16 +1569,16 @@ var SessionProcessManager = class {
|
|
|
1558
1569
|
async ensureProcessForRewind(sessionId) {
|
|
1559
1570
|
const existing = this.processes.get(sessionId);
|
|
1560
1571
|
if (existing && existing.status === "running") {
|
|
1561
|
-
log$
|
|
1572
|
+
log$35.info("ensureProcessForRewind: 进程已在运行中,直接使用", { sessionId });
|
|
1562
1573
|
return existing;
|
|
1563
1574
|
}
|
|
1564
1575
|
const cwd = this.sessionCwdCache.get(sessionId);
|
|
1565
1576
|
if (!cwd) {
|
|
1566
|
-
log$
|
|
1577
|
+
log$35.warn("ensureProcessForRewind: 无法获取会话 cwd", { sessionId });
|
|
1567
1578
|
return;
|
|
1568
1579
|
}
|
|
1569
1580
|
if (existing) this.cleanup(sessionId);
|
|
1570
|
-
log$
|
|
1581
|
+
log$35.info("ensureProcessForRewind: 启动临时进程用于 rewind", {
|
|
1571
1582
|
sessionId,
|
|
1572
1583
|
cwd
|
|
1573
1584
|
});
|
|
@@ -1578,7 +1589,7 @@ var SessionProcessManager = class {
|
|
|
1578
1589
|
}, { skipInitialMessage: true });
|
|
1579
1590
|
const proc = this.processes.get(sessionId);
|
|
1580
1591
|
if (!proc || proc.status !== "running") {
|
|
1581
|
-
log$
|
|
1592
|
+
log$35.warn("ensureProcessForRewind: 进程启动失败", {
|
|
1582
1593
|
sessionId,
|
|
1583
1594
|
status: proc?.status
|
|
1584
1595
|
});
|
|
@@ -1600,11 +1611,11 @@ var SessionProcessManager = class {
|
|
|
1600
1611
|
async stopProcess(sessionId) {
|
|
1601
1612
|
const proc = this.processes.get(sessionId);
|
|
1602
1613
|
if (!proc) {
|
|
1603
|
-
log$
|
|
1614
|
+
log$35.warn("尝试停止不存在的进程", { sessionId });
|
|
1604
1615
|
return;
|
|
1605
1616
|
}
|
|
1606
1617
|
if (proc.status === "stopped" || proc.status === "crashed") {
|
|
1607
|
-
log$
|
|
1618
|
+
log$35.info("进程已经停止,直接清理", {
|
|
1608
1619
|
sessionId,
|
|
1609
1620
|
status: proc.status
|
|
1610
1621
|
});
|
|
@@ -1612,7 +1623,7 @@ var SessionProcessManager = class {
|
|
|
1612
1623
|
return;
|
|
1613
1624
|
}
|
|
1614
1625
|
proc.status = "stopping";
|
|
1615
|
-
log$
|
|
1626
|
+
log$35.info("正在停止 CC 常驻进程", {
|
|
1616
1627
|
sessionId,
|
|
1617
1628
|
pid: proc.childProcess.pid
|
|
1618
1629
|
});
|
|
@@ -1628,7 +1639,7 @@ var SessionProcessManager = class {
|
|
|
1628
1639
|
const child = proc.childProcess;
|
|
1629
1640
|
const forceKillTimer = setTimeout(() => {
|
|
1630
1641
|
if (!child.killed) {
|
|
1631
|
-
log$
|
|
1642
|
+
log$35.warn("CC 常驻进程未在规定时间内退出,强制终止", { sessionId });
|
|
1632
1643
|
child.kill("SIGKILL");
|
|
1633
1644
|
}
|
|
1634
1645
|
}, GRACEFUL_SHUTDOWN_MS);
|
|
@@ -1648,9 +1659,9 @@ var SessionProcessManager = class {
|
|
|
1648
1659
|
async stopAll() {
|
|
1649
1660
|
const sessionIds = Array.from(this.processes.keys());
|
|
1650
1661
|
if (sessionIds.length === 0) return;
|
|
1651
|
-
log$
|
|
1662
|
+
log$35.info("正在停止所有 CC 常驻进程", { count: sessionIds.length });
|
|
1652
1663
|
await Promise.all(sessionIds.map((id) => this.stopProcess(id)));
|
|
1653
|
-
log$
|
|
1664
|
+
log$35.info("所有 CC 常驻进程已停止");
|
|
1654
1665
|
}
|
|
1655
1666
|
/**
|
|
1656
1667
|
* 重置空闲计时器
|
|
@@ -1660,7 +1671,7 @@ var SessionProcessManager = class {
|
|
|
1660
1671
|
if (existing) clearTimeout(existing);
|
|
1661
1672
|
if (IDLE_TIMEOUT_MS <= 0) return;
|
|
1662
1673
|
const timer = setTimeout(() => {
|
|
1663
|
-
log$
|
|
1674
|
+
log$35.info("CC 常驻进程空闲超时,自动停止", {
|
|
1664
1675
|
sessionId,
|
|
1665
1676
|
timeoutMs: IDLE_TIMEOUT_MS
|
|
1666
1677
|
});
|
|
@@ -1677,6 +1688,7 @@ var SessionProcessManager = class {
|
|
|
1677
1688
|
this.processes.delete(sessionId);
|
|
1678
1689
|
this.activeCallbacks.delete(sessionId);
|
|
1679
1690
|
this.lastUserMessageIds.delete(sessionId);
|
|
1691
|
+
this.lastMessageHadImages.delete(sessionId);
|
|
1680
1692
|
for (const [reqId, pending] of this.pendingControlRequests) if (pending.sessionId === sessionId) {
|
|
1681
1693
|
clearTimeout(pending.timer);
|
|
1682
1694
|
this.pendingControlRequests.delete(reqId);
|
|
@@ -1688,7 +1700,7 @@ var SessionProcessManager = class {
|
|
|
1688
1700
|
this.idleTimers.delete(sessionId);
|
|
1689
1701
|
}
|
|
1690
1702
|
cleanupMcpConfig(sessionId);
|
|
1691
|
-
log$
|
|
1703
|
+
log$35.debug("已清理常驻进程状态", { sessionId });
|
|
1692
1704
|
}
|
|
1693
1705
|
/**
|
|
1694
1706
|
* 将 cwd 编码为 CC 的 projects 子目录名
|
|
@@ -1717,7 +1729,7 @@ var SessionProcessManager = class {
|
|
|
1717
1729
|
const cwdEncoded = this.encodeCwdForCC(cwd);
|
|
1718
1730
|
const sessionFile = join(homedir(), ".claude", "projects", cwdEncoded, `${claudeSessionId}.jsonl`);
|
|
1719
1731
|
const exists = existsSync(sessionFile);
|
|
1720
|
-
log$
|
|
1732
|
+
log$35.debug("检查 CC session 文件", {
|
|
1721
1733
|
cwd,
|
|
1722
1734
|
cwdEncoded,
|
|
1723
1735
|
claudeSessionId,
|
|
@@ -1726,7 +1738,7 @@ var SessionProcessManager = class {
|
|
|
1726
1738
|
});
|
|
1727
1739
|
return exists;
|
|
1728
1740
|
} catch (err) {
|
|
1729
|
-
log$
|
|
1741
|
+
log$35.warn("检查 CC session 文件失败,默认为首次创建", { error: err instanceof Error ? err.message : String(err) });
|
|
1730
1742
|
return false;
|
|
1731
1743
|
}
|
|
1732
1744
|
}
|
|
@@ -1741,7 +1753,7 @@ var SessionProcessManager = class {
|
|
|
1741
1753
|
*
|
|
1742
1754
|
* 这是 LarkPal 的默认 AI 引擎适配器。
|
|
1743
1755
|
*/
|
|
1744
|
-
const log$
|
|
1756
|
+
const log$34 = larkLogger("runtime/claude-code-adapter");
|
|
1745
1757
|
var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
1746
1758
|
name = "claude-code";
|
|
1747
1759
|
processManager;
|
|
@@ -1757,7 +1769,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1757
1769
|
constructor() {
|
|
1758
1770
|
this.processManager = new SessionProcessManager();
|
|
1759
1771
|
ClaudeCodeAdapter._instance = this;
|
|
1760
|
-
log$
|
|
1772
|
+
log$34.info("ClaudeCodeAdapter 初始化完成");
|
|
1761
1773
|
}
|
|
1762
1774
|
/**
|
|
1763
1775
|
* 获取底层 SessionProcessManager 实例
|
|
@@ -1770,7 +1782,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1770
1782
|
}
|
|
1771
1783
|
async executePrompt(config, callbacks) {
|
|
1772
1784
|
this.lastRuntimeConfigs.set(config.sessionId, config);
|
|
1773
|
-
log$
|
|
1785
|
+
log$34.info("executePrompt via ClaudeCodeAdapter", {
|
|
1774
1786
|
sessionId: config.sessionId,
|
|
1775
1787
|
cwd: config.cwd,
|
|
1776
1788
|
promptLength: config.prompt.length,
|
|
@@ -1779,11 +1791,11 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1779
1791
|
await this.processManager.executePrompt(config, callbacks);
|
|
1780
1792
|
}
|
|
1781
1793
|
async stopProcess(sessionId) {
|
|
1782
|
-
log$
|
|
1794
|
+
log$34.info("stopProcess via ClaudeCodeAdapter", { sessionId });
|
|
1783
1795
|
await this.processManager.stopProcess(sessionId);
|
|
1784
1796
|
}
|
|
1785
1797
|
async stopAll() {
|
|
1786
|
-
log$
|
|
1798
|
+
log$34.info("stopAll via ClaudeCodeAdapter");
|
|
1787
1799
|
await this.processManager.stopAll();
|
|
1788
1800
|
}
|
|
1789
1801
|
getProcessInfo(sessionId) {
|
|
@@ -1819,7 +1831,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1819
1831
|
};
|
|
1820
1832
|
//#endregion
|
|
1821
1833
|
//#region src/codex-runtime/app-server-client.ts
|
|
1822
|
-
const log$
|
|
1834
|
+
const log$33 = larkLogger("codex-runtime/app-server-client");
|
|
1823
1835
|
const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
1824
1836
|
var CodexAppServerClient = class extends EventEmitter {
|
|
1825
1837
|
sessionId;
|
|
@@ -1850,7 +1862,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1850
1862
|
"pipe"
|
|
1851
1863
|
]
|
|
1852
1864
|
});
|
|
1853
|
-
log$
|
|
1865
|
+
log$33.info("Codex app-server 进程已启动", {
|
|
1854
1866
|
sessionId: this.sessionId,
|
|
1855
1867
|
cwd: this.cwd,
|
|
1856
1868
|
pid: this.childProcess.pid,
|
|
@@ -1929,14 +1941,14 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1929
1941
|
bindStreams() {
|
|
1930
1942
|
if (this.childProcess.stdout) createInterface({ input: this.childProcess.stdout }).on("line", (line) => this.handleLine(line));
|
|
1931
1943
|
if (this.childProcess.stderr) createInterface({ input: this.childProcess.stderr }).on("line", (line) => {
|
|
1932
|
-
log$
|
|
1944
|
+
log$33.warn("Codex app-server stderr", {
|
|
1933
1945
|
sessionId: this.sessionId,
|
|
1934
1946
|
line: line.slice(0, 500)
|
|
1935
1947
|
});
|
|
1936
1948
|
this.emit("stderr", line);
|
|
1937
1949
|
});
|
|
1938
1950
|
this.childProcess.on("error", (error) => {
|
|
1939
|
-
log$
|
|
1951
|
+
log$33.error("Codex app-server 进程错误", {
|
|
1940
1952
|
sessionId: this.sessionId,
|
|
1941
1953
|
error: error.message
|
|
1942
1954
|
});
|
|
@@ -1945,7 +1957,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1945
1957
|
});
|
|
1946
1958
|
this.childProcess.on("close", (code, signal) => {
|
|
1947
1959
|
this.closed = true;
|
|
1948
|
-
log$
|
|
1960
|
+
log$33.info("Codex app-server 进程退出", {
|
|
1949
1961
|
sessionId: this.sessionId,
|
|
1950
1962
|
code,
|
|
1951
1963
|
signal
|
|
@@ -1962,7 +1974,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1962
1974
|
message = JSON.parse(trimmed);
|
|
1963
1975
|
} catch (err) {
|
|
1964
1976
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1965
|
-
log$
|
|
1977
|
+
log$33.warn("Codex app-server JSONL 解析失败", {
|
|
1966
1978
|
sessionId: this.sessionId,
|
|
1967
1979
|
error: error.message,
|
|
1968
1980
|
line: trimmed.slice(0, 200)
|
|
@@ -1982,7 +1994,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1982
1994
|
}
|
|
1983
1995
|
handleResponse(message) {
|
|
1984
1996
|
if (message.id == null) {
|
|
1985
|
-
log$
|
|
1997
|
+
log$33.warn("Codex app-server 返回无 id 错误响应", {
|
|
1986
1998
|
sessionId: this.sessionId,
|
|
1987
1999
|
message
|
|
1988
2000
|
});
|
|
@@ -1991,7 +2003,7 @@ var CodexAppServerClient = class extends EventEmitter {
|
|
|
1991
2003
|
const idKey = String(message.id);
|
|
1992
2004
|
const pending = this.pending.get(idKey);
|
|
1993
2005
|
if (!pending) {
|
|
1994
|
-
log$
|
|
2006
|
+
log$33.warn("Codex app-server 返回未知请求响应", {
|
|
1995
2007
|
sessionId: this.sessionId,
|
|
1996
2008
|
id: message.id
|
|
1997
2009
|
});
|
|
@@ -2037,7 +2049,7 @@ function isNotification(message) {
|
|
|
2037
2049
|
}
|
|
2038
2050
|
//#endregion
|
|
2039
2051
|
//#region src/runtime/codex-app-server-adapter.ts
|
|
2040
|
-
const log$
|
|
2052
|
+
const log$32 = larkLogger("runtime/codex-app-server-adapter");
|
|
2041
2053
|
const STATE_PATH = join(homedir(), ".larkpal", "runtime", "codex-app-server-threads.json");
|
|
2042
2054
|
const REQUEST_TIMEOUT_MS = 12e4;
|
|
2043
2055
|
var CodexAppServerAdapter = class {
|
|
@@ -2063,14 +2075,14 @@ var CodexAppServerAdapter = class {
|
|
|
2063
2075
|
extraArgs: params.extraArgs,
|
|
2064
2076
|
requestTimeoutMs: REQUEST_TIMEOUT_MS
|
|
2065
2077
|
}));
|
|
2066
|
-
log$
|
|
2078
|
+
log$32.info("CodexAppServerAdapter 初始化完成", { statePath: this.statePath });
|
|
2067
2079
|
}
|
|
2068
2080
|
async executePrompt(config, callbacks) {
|
|
2069
2081
|
await this.loadState();
|
|
2070
2082
|
const { sessionId } = config;
|
|
2071
2083
|
const prevLock = this.sessionLocks.get(sessionId);
|
|
2072
2084
|
if (prevLock) {
|
|
2073
|
-
log$
|
|
2085
|
+
log$32.info("等待上一条 Codex 消息处理完成", { sessionId });
|
|
2074
2086
|
await prevLock;
|
|
2075
2087
|
}
|
|
2076
2088
|
const completion = this.createCompletionPromise(sessionId);
|
|
@@ -2101,7 +2113,7 @@ var CodexAppServerAdapter = class {
|
|
|
2101
2113
|
if (config.transcriptMode === "ephemeral") {
|
|
2102
2114
|
const record = this.processes.get(sessionId);
|
|
2103
2115
|
if (record) await this.closeRecord(sessionId, record).catch((err) => {
|
|
2104
|
-
log$
|
|
2116
|
+
log$32.warn("ephemeral Codex app-server session 清理失败", {
|
|
2105
2117
|
sessionId,
|
|
2106
2118
|
error: err instanceof Error ? err.message : String(err)
|
|
2107
2119
|
});
|
|
@@ -2116,12 +2128,12 @@ var CodexAppServerAdapter = class {
|
|
|
2116
2128
|
async stopProcess(sessionId) {
|
|
2117
2129
|
const record = this.processes.get(sessionId);
|
|
2118
2130
|
if (!record) {
|
|
2119
|
-
log$
|
|
2131
|
+
log$32.warn("尝试停止不存在的 Codex app-server session", { sessionId });
|
|
2120
2132
|
return;
|
|
2121
2133
|
}
|
|
2122
2134
|
if (this.busySessions.has(sessionId) && record.process.currentTurnId) {
|
|
2123
2135
|
this.abortedSessions.add(sessionId);
|
|
2124
|
-
log$
|
|
2136
|
+
log$32.info("发送 Codex turn/interrupt", {
|
|
2125
2137
|
sessionId,
|
|
2126
2138
|
threadId: record.process.threadId,
|
|
2127
2139
|
turnId: record.process.currentTurnId
|
|
@@ -2208,7 +2220,7 @@ var CodexAppServerAdapter = class {
|
|
|
2208
2220
|
if (!isEphemeral) await this.setThreadId(config.sessionId, resumed.thread.id, config.cwd);
|
|
2209
2221
|
return;
|
|
2210
2222
|
} catch (err) {
|
|
2211
|
-
log$
|
|
2223
|
+
log$32.warn("Codex thread/resume 失败,将新建 thread", {
|
|
2212
2224
|
sessionId: config.sessionId,
|
|
2213
2225
|
threadId: storedThreadId,
|
|
2214
2226
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2274,7 +2286,7 @@ var CodexAppServerAdapter = class {
|
|
|
2274
2286
|
this.handleErrorNotification(sessionId, message.params);
|
|
2275
2287
|
break;
|
|
2276
2288
|
default:
|
|
2277
|
-
log$
|
|
2289
|
+
log$32.debug("忽略 Codex app-server notification", {
|
|
2278
2290
|
sessionId,
|
|
2279
2291
|
method: message.method
|
|
2280
2292
|
});
|
|
@@ -2302,7 +2314,7 @@ var CodexAppServerAdapter = class {
|
|
|
2302
2314
|
handleMcpProgress(sessionId, params) {
|
|
2303
2315
|
const toolName = this.lookupToolName(sessionId, params.itemId) ?? "mcp";
|
|
2304
2316
|
this.activeCallbacks.get(sessionId)?.onToolProgress?.(toolName, 0);
|
|
2305
|
-
log$
|
|
2317
|
+
log$32.debug("Codex MCP tool progress", {
|
|
2306
2318
|
sessionId,
|
|
2307
2319
|
itemId: params.itemId,
|
|
2308
2320
|
message: params.message
|
|
@@ -2332,7 +2344,7 @@ var CodexAppServerAdapter = class {
|
|
|
2332
2344
|
this.resolveCompletion(sessionId);
|
|
2333
2345
|
}
|
|
2334
2346
|
async handleServerRequest(client, request) {
|
|
2335
|
-
log$
|
|
2347
|
+
log$32.warn("Codex app-server server request 默认拒绝", {
|
|
2336
2348
|
method: request.method,
|
|
2337
2349
|
id: request.id
|
|
2338
2350
|
});
|
|
@@ -2498,10 +2510,10 @@ var CodexAppServerAdapter = class {
|
|
|
2498
2510
|
];
|
|
2499
2511
|
}
|
|
2500
2512
|
logUnsupportedConfig(config) {
|
|
2501
|
-
if (config.llmConfig) log$
|
|
2502
|
-
if (config.maxBudgetUsd != null) log$
|
|
2503
|
-
if (config.maxTurns != null) log$
|
|
2504
|
-
if (config.policy?.allowedTools || config.policy?.disallowedTools) log$
|
|
2513
|
+
if (config.llmConfig) log$32.warn("Codex app-server v1 暂不支持 per-request llmConfig,已忽略", { sessionId: config.sessionId });
|
|
2514
|
+
if (config.maxBudgetUsd != null) log$32.warn("Codex app-server v1 暂不支持 maxBudgetUsd,已忽略", { sessionId: config.sessionId });
|
|
2515
|
+
if (config.maxTurns != null) log$32.warn("Codex app-server v1 暂不支持 maxTurns,已忽略", { sessionId: config.sessionId });
|
|
2516
|
+
if (config.policy?.allowedTools || config.policy?.disallowedTools) log$32.warn("Codex app-server v1 暂不强制执行 tool allow/deny policy,已忽略", { sessionId: config.sessionId });
|
|
2505
2517
|
}
|
|
2506
2518
|
createCompletionPromise(sessionId) {
|
|
2507
2519
|
return new Promise((resolve) => {
|
|
@@ -2517,7 +2529,7 @@ var CodexAppServerAdapter = class {
|
|
|
2517
2529
|
async closeRecord(sessionId, record) {
|
|
2518
2530
|
record.process.status = "stopping";
|
|
2519
2531
|
if (record.process.threadId) await record.client.request("thread/unsubscribe", { threadId: record.process.threadId }).catch((err) => {
|
|
2520
|
-
log$
|
|
2532
|
+
log$32.warn("Codex thread/unsubscribe 失败", {
|
|
2521
2533
|
sessionId,
|
|
2522
2534
|
threadId: record.process.threadId,
|
|
2523
2535
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2550,7 +2562,7 @@ var CodexAppServerAdapter = class {
|
|
|
2550
2562
|
const parsed = JSON.parse(raw);
|
|
2551
2563
|
for (const [sessionId, entry] of Object.entries(parsed.sessions ?? {})) if (entry.threadId) this.threadIds.set(sessionId, entry.threadId);
|
|
2552
2564
|
} catch (err) {
|
|
2553
|
-
log$
|
|
2565
|
+
log$32.warn("Codex thread state 加载失败,将从空状态继续", {
|
|
2554
2566
|
path: this.statePath,
|
|
2555
2567
|
error: err instanceof Error ? err.message : String(err)
|
|
2556
2568
|
});
|
|
@@ -2618,18 +2630,18 @@ function getLarkpalMcpServerCommand() {
|
|
|
2618
2630
|
*
|
|
2619
2631
|
* 仅当 LARKPAL_RUNTIME=larkpal-agent 时使用。
|
|
2620
2632
|
*/
|
|
2621
|
-
const log$
|
|
2633
|
+
const log$31 = larkLogger("runtime/larkpal-agent-factory");
|
|
2622
2634
|
const AGENT_MD_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
2623
2635
|
async function loadSystemPrompt() {
|
|
2624
2636
|
try {
|
|
2625
2637
|
const content = await readFile(AGENT_MD_PATH, "utf-8");
|
|
2626
|
-
log$
|
|
2638
|
+
log$31.info("systemPrompt 加载成功", {
|
|
2627
2639
|
path: AGENT_MD_PATH,
|
|
2628
2640
|
length: content.length
|
|
2629
2641
|
});
|
|
2630
2642
|
return content;
|
|
2631
2643
|
} catch {
|
|
2632
|
-
log$
|
|
2644
|
+
log$31.warn("systemPrompt 文件不存在,使用默认", { path: AGENT_MD_PATH });
|
|
2633
2645
|
return "你是一个 AI 助手,帮助用户完成飞书知识管理任务。";
|
|
2634
2646
|
}
|
|
2635
2647
|
}
|
|
@@ -2745,7 +2757,7 @@ async function createLarkpalAgentAdapter() {
|
|
|
2745
2757
|
const scenarioManifestDirs = loadScenarioManifestDirs();
|
|
2746
2758
|
const scenarioProviderConfig = loadScenarioProviderConfig(scenarioManifestDirs);
|
|
2747
2759
|
const mcpTimeoutMs = loadAgentMcpTimeoutMs();
|
|
2748
|
-
log$
|
|
2760
|
+
log$31.info("创建 LarkpalAgentAdapter", {
|
|
2749
2761
|
baseURL: llm.baseURL,
|
|
2750
2762
|
model: llm.defaultModel,
|
|
2751
2763
|
maxTurns: defaultMaxTurns,
|
|
@@ -2772,20 +2784,98 @@ async function createLarkpalAgentAdapter() {
|
|
|
2772
2784
|
const toolCount = await registry.connectMcpServer(mcpServerUrl, { timeoutMs: mcpTimeoutMs });
|
|
2773
2785
|
const tools = registry.getAll();
|
|
2774
2786
|
adapter.registerTools(tools);
|
|
2775
|
-
log$
|
|
2787
|
+
log$31.info("MCP 工具注册完成", {
|
|
2776
2788
|
serverUrl: mcpServerUrl,
|
|
2777
2789
|
timeoutMs: mcpTimeoutMs,
|
|
2778
2790
|
toolCount,
|
|
2779
2791
|
toolNames: tools.map((t) => t.name)
|
|
2780
2792
|
});
|
|
2781
2793
|
} catch (err) {
|
|
2782
|
-
log$
|
|
2794
|
+
log$31.error("MCP Server 连接失败,Agent 将无法使用远程工具", {
|
|
2783
2795
|
serverUrl: mcpServerUrl,
|
|
2784
2796
|
error: err?.message
|
|
2785
2797
|
});
|
|
2786
2798
|
}
|
|
2787
|
-
else log$
|
|
2788
|
-
return adapter;
|
|
2799
|
+
else log$31.warn("未配置 LARKPAL_AGENT_MCP_SERVER_URL,Agent 无远程工具可用");
|
|
2800
|
+
return wrapLarkpalAgentAdapter(adapter);
|
|
2801
|
+
}
|
|
2802
|
+
function wrapLarkpalAgentAdapter(adapter) {
|
|
2803
|
+
return {
|
|
2804
|
+
get name() {
|
|
2805
|
+
return adapter.name;
|
|
2806
|
+
},
|
|
2807
|
+
executePrompt(config, callbacks) {
|
|
2808
|
+
return adapter.executePrompt(normalizeLarkpalAgentConfig(config), callbacks);
|
|
2809
|
+
},
|
|
2810
|
+
stopProcess(sessionId) {
|
|
2811
|
+
return adapter.stopProcess(sessionId);
|
|
2812
|
+
},
|
|
2813
|
+
stopAll() {
|
|
2814
|
+
return adapter.stopAll();
|
|
2815
|
+
},
|
|
2816
|
+
getProcessInfo(sessionId) {
|
|
2817
|
+
return adapter.getProcessInfo(sessionId);
|
|
2818
|
+
},
|
|
2819
|
+
getAllProcessInfo() {
|
|
2820
|
+
return adapter.getAllProcessInfo();
|
|
2821
|
+
},
|
|
2822
|
+
isSessionBusy(sessionId) {
|
|
2823
|
+
return adapter.isSessionBusy?.(sessionId) ?? false;
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
function normalizeLarkpalAgentConfig(config) {
|
|
2828
|
+
if (!Array.isArray(config.prompt)) return config;
|
|
2829
|
+
const textParts = [];
|
|
2830
|
+
const imageArtifacts = [];
|
|
2831
|
+
const imageLines = [];
|
|
2832
|
+
config.prompt.forEach((block, index) => {
|
|
2833
|
+
if (block.type === "text") {
|
|
2834
|
+
if (block.text.trim()) textParts.push(block.text);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
if (block.type !== "image") return;
|
|
2838
|
+
const artifact = toAgentImageArtifact(block, index);
|
|
2839
|
+
const label = artifact?.assetId ?? block.fileKey ?? `image_${index + 1}`;
|
|
2840
|
+
if (artifact) imageArtifacts.push(artifact);
|
|
2841
|
+
imageLines.push(artifact ? `- ${label}: ${artifact.path} (${artifact.mimeType})` : `- ${label}: inline image omitted from text prompt (${block.source.media_type})`);
|
|
2842
|
+
});
|
|
2843
|
+
if (imageLines.length === 0) return config;
|
|
2844
|
+
const prompt = [
|
|
2845
|
+
textParts.join("\n\n").trim(),
|
|
2846
|
+
"[Images]",
|
|
2847
|
+
...imageLines,
|
|
2848
|
+
"The user is asking about these image attachments. Use visual or file tools to inspect the image pixels when needed; do not infer image content from the file name alone."
|
|
2849
|
+
].filter(Boolean).join("\n");
|
|
2850
|
+
return {
|
|
2851
|
+
...config,
|
|
2852
|
+
prompt,
|
|
2853
|
+
sourceArtifacts: mergeSourceArtifacts(config.sourceArtifacts, imageArtifacts),
|
|
2854
|
+
metadata: {
|
|
2855
|
+
...config.metadata,
|
|
2856
|
+
inboundImageCount: imageLines.length,
|
|
2857
|
+
visualAllowedDirs: [config.cwd]
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
function toAgentImageArtifact(block, index) {
|
|
2862
|
+
if (!block.filePath) return null;
|
|
2863
|
+
return {
|
|
2864
|
+
type: "image_asset",
|
|
2865
|
+
assetId: block.fileKey ?? `inbound_image_${index + 1}`,
|
|
2866
|
+
path: block.filePath,
|
|
2867
|
+
mimeType: block.source.media_type.split(";", 1)[0] || "image/png",
|
|
2868
|
+
metadata: {
|
|
2869
|
+
source: "feishu-message",
|
|
2870
|
+
fileKey: block.fileKey
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function mergeSourceArtifacts(existing, imageArtifacts) {
|
|
2875
|
+
if (imageArtifacts.length === 0) return existing;
|
|
2876
|
+
if (existing === void 0) return imageArtifacts;
|
|
2877
|
+
if (Array.isArray(existing)) return [...existing, ...imageArtifacts];
|
|
2878
|
+
return [existing, ...imageArtifacts];
|
|
2789
2879
|
}
|
|
2790
2880
|
//#endregion
|
|
2791
2881
|
//#region src/routing/session-router.ts
|
|
@@ -3419,7 +3509,7 @@ function summarizeString(value) {
|
|
|
3419
3509
|
* POST /api/chat/sessions — 创建新会话
|
|
3420
3510
|
* DELETE /api/chat/sessions/:id — 删除会话
|
|
3421
3511
|
*/
|
|
3422
|
-
const log$
|
|
3512
|
+
const log$30 = larkLogger("chat/stream");
|
|
3423
3513
|
const chatRunsBySession = /* @__PURE__ */ new Map();
|
|
3424
3514
|
const DEFAULT_STALE_RUN_IDLE_MS = 1800 * 1e3;
|
|
3425
3515
|
/** 统一写 SSE 格式数据到响应流 */
|
|
@@ -3431,7 +3521,7 @@ function isChatSessionRunning(sessionId, processManager) {
|
|
|
3431
3521
|
return getActiveChatRun(sessionId) != null || (processManager.isSessionBusy?.(sessionId) ?? false);
|
|
3432
3522
|
}
|
|
3433
3523
|
function logChatAudit(event) {
|
|
3434
|
-
log$
|
|
3524
|
+
log$30.info("[stream] Agent audit event", { ...event });
|
|
3435
3525
|
}
|
|
3436
3526
|
function prepareSSE(res) {
|
|
3437
3527
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -3482,7 +3572,7 @@ function addRunClient(req, res, run) {
|
|
|
3482
3572
|
run.clients.add(res);
|
|
3483
3573
|
req.on("close", () => {
|
|
3484
3574
|
run.clients.delete(res);
|
|
3485
|
-
log$
|
|
3575
|
+
log$30.info("[stream] 客户端断开连接", {
|
|
3486
3576
|
sessionId: run.sessionId,
|
|
3487
3577
|
runId: run.runId
|
|
3488
3578
|
});
|
|
@@ -3521,7 +3611,7 @@ function broadcastRunEvent(run, event, data) {
|
|
|
3521
3611
|
sendSSE(client, event, data);
|
|
3522
3612
|
} catch (err) {
|
|
3523
3613
|
run.clients.delete(client);
|
|
3524
|
-
log$
|
|
3614
|
+
log$30.warn("[stream] SSE 写入失败,移除客户端", {
|
|
3525
3615
|
sessionId: run.sessionId,
|
|
3526
3616
|
runId: run.runId,
|
|
3527
3617
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3541,7 +3631,7 @@ function queueRunMessage(run, messageStore, message, label) {
|
|
|
3541
3631
|
run.persistQueue = run.persistQueue.catch(() => void 0).then(async () => {
|
|
3542
3632
|
await messageStore.appendMessage(message);
|
|
3543
3633
|
}).catch((err) => {
|
|
3544
|
-
log$
|
|
3634
|
+
log$30.error(label, {
|
|
3545
3635
|
sessionId: run.sessionId,
|
|
3546
3636
|
runId: run.runId,
|
|
3547
3637
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3594,7 +3684,7 @@ async function reconcileRunWithAdapter(run, messageStore, processManager) {
|
|
|
3594
3684
|
else if (idleMs >= getStaleRunIdleMs()) staleReason = `run idle for ${Math.floor(idleMs / 1e3)}s`;
|
|
3595
3685
|
if (!staleReason) return;
|
|
3596
3686
|
const error = `Chat run became stale: ${staleReason}`;
|
|
3597
|
-
log$
|
|
3687
|
+
log$30.warn("[stream] activeRun 与 runtime 状态不一致,标记为 stale", {
|
|
3598
3688
|
sessionId: run.sessionId,
|
|
3599
3689
|
runId: run.runId,
|
|
3600
3690
|
staleReason,
|
|
@@ -3672,7 +3762,7 @@ function createChatRouter(config) {
|
|
|
3672
3762
|
addRunClient(req, res, activeRun);
|
|
3673
3763
|
sendSSE(res, "session", { sessionId });
|
|
3674
3764
|
sendRunStatus(res, activeRun);
|
|
3675
|
-
log$
|
|
3765
|
+
log$30.info("[stream] 已重新连接到运行中的 run", {
|
|
3676
3766
|
sessionId,
|
|
3677
3767
|
runId: activeRun.runId
|
|
3678
3768
|
});
|
|
@@ -3685,7 +3775,7 @@ function createChatRouter(config) {
|
|
|
3685
3775
|
const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
|
|
3686
3776
|
const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
|
|
3687
3777
|
const sourceArtifacts = getRunInputValue(body.sourceArtifacts, body.source_artifacts, body.options?.sourceArtifacts, body.options?.source_artifacts, body.options?.metadata?.sourceArtifacts, body.options?.metadata?.source_artifacts);
|
|
3688
|
-
log$
|
|
3778
|
+
log$30.info("[stream] 收到流式对话请求", {
|
|
3689
3779
|
userId,
|
|
3690
3780
|
sessionId,
|
|
3691
3781
|
requestId,
|
|
@@ -3702,13 +3792,13 @@ function createChatRouter(config) {
|
|
|
3702
3792
|
tenantKey,
|
|
3703
3793
|
channel: "web"
|
|
3704
3794
|
})).id;
|
|
3705
|
-
log$
|
|
3795
|
+
log$30.info("[stream] 自动创建新会话", {
|
|
3706
3796
|
userId: openId,
|
|
3707
3797
|
sessionId
|
|
3708
3798
|
});
|
|
3709
3799
|
}
|
|
3710
3800
|
if (isChatSessionRunning(sessionId, processManager)) {
|
|
3711
|
-
log$
|
|
3801
|
+
log$30.warn("[stream] session 正在被非 Web Chat run 占用", {
|
|
3712
3802
|
tenantKey,
|
|
3713
3803
|
userId,
|
|
3714
3804
|
sessionId
|
|
@@ -3719,7 +3809,6 @@ function createChatRouter(config) {
|
|
|
3719
3809
|
res.end();
|
|
3720
3810
|
return;
|
|
3721
3811
|
}
|
|
3722
|
-
const runtimePrompt = enrichPromptWithSourceArtifacts(body.prompt, sourceArtifacts);
|
|
3723
3812
|
const runtimeConfig = buildAgentRuntimeConfig({
|
|
3724
3813
|
requestId,
|
|
3725
3814
|
traceId,
|
|
@@ -3727,7 +3816,7 @@ function createChatRouter(config) {
|
|
|
3727
3816
|
conversationId: sessionId,
|
|
3728
3817
|
entrypoint: "chat",
|
|
3729
3818
|
scenarioId,
|
|
3730
|
-
prompt:
|
|
3819
|
+
prompt: body.prompt,
|
|
3731
3820
|
maxTurns: body.options?.maxTurns,
|
|
3732
3821
|
maxBudgetUsd: body.options?.maxBudgetUsd,
|
|
3733
3822
|
model: process.env.CLAUDE_MODEL || void 0,
|
|
@@ -3780,7 +3869,7 @@ function createChatRouter(config) {
|
|
|
3780
3869
|
});
|
|
3781
3870
|
} catch (err) {
|
|
3782
3871
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3783
|
-
log$
|
|
3872
|
+
log$30.error("[stream] 保存用户消息失败", {
|
|
3784
3873
|
sessionId,
|
|
3785
3874
|
error: errorMessage
|
|
3786
3875
|
});
|
|
@@ -3803,7 +3892,7 @@ function createChatRouter(config) {
|
|
|
3803
3892
|
const handleBlockedEvent = (event) => {
|
|
3804
3893
|
const sanitized = sanitizeBlockedEvent(event);
|
|
3805
3894
|
recordRunEvent(run, "blocked", { toolName: sanitized.name });
|
|
3806
|
-
log$
|
|
3895
|
+
log$30.warn("[stream] Agent blocked event", { ...sanitized });
|
|
3807
3896
|
if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, sanitized.type, {
|
|
3808
3897
|
toolName: sanitized.name,
|
|
3809
3898
|
inputSummary: sanitized.inputSummary,
|
|
@@ -3836,7 +3925,7 @@ function createChatRouter(config) {
|
|
|
3836
3925
|
const dedupeKey = getRuntimeEventDedupeKey(event);
|
|
3837
3926
|
if (dedupeKey) {
|
|
3838
3927
|
if (run.seenRuntimeEventIds.has(dedupeKey)) {
|
|
3839
|
-
log$
|
|
3928
|
+
log$30.debug("[stream] 跳过重复 runtime event", {
|
|
3840
3929
|
sessionId,
|
|
3841
3930
|
runId: run.runId,
|
|
3842
3931
|
eventId: dedupeKey,
|
|
@@ -3986,7 +4075,7 @@ function createChatRouter(config) {
|
|
|
3986
4075
|
});
|
|
3987
4076
|
clearInterval(heartbeatTimer);
|
|
3988
4077
|
endRunClients(run);
|
|
3989
|
-
log$
|
|
4078
|
+
log$30.info("[stream] 对话完成", {
|
|
3990
4079
|
sessionId,
|
|
3991
4080
|
runId: run.runId,
|
|
3992
4081
|
result: result.subtype,
|
|
@@ -4009,7 +4098,7 @@ function createChatRouter(config) {
|
|
|
4009
4098
|
},
|
|
4010
4099
|
onError: async (error) => {
|
|
4011
4100
|
recordRunEvent(run, "error");
|
|
4012
|
-
log$
|
|
4101
|
+
log$30.error("[stream] Runtime 执行出错", {
|
|
4013
4102
|
sessionId,
|
|
4014
4103
|
runId: run.runId,
|
|
4015
4104
|
error: error.message
|
|
@@ -4097,7 +4186,7 @@ function createChatRouter(config) {
|
|
|
4097
4186
|
scenarioId: runtimeConfig.runContext?.scenarioId,
|
|
4098
4187
|
...buildRunInputAuditMetadata(runtimeConfig)
|
|
4099
4188
|
} }));
|
|
4100
|
-
log$
|
|
4189
|
+
log$30.info("[stream] 开始执行 prompt", {
|
|
4101
4190
|
sessionId,
|
|
4102
4191
|
runId: run.runId,
|
|
4103
4192
|
cwd: runtimeConfig.cwd
|
|
@@ -4105,7 +4194,7 @@ function createChatRouter(config) {
|
|
|
4105
4194
|
await processManager.executePrompt(runtimeConfig, callbacks);
|
|
4106
4195
|
if (run.status === "running") {
|
|
4107
4196
|
const errorMessage = "Runtime completed without a final result event";
|
|
4108
|
-
log$
|
|
4197
|
+
log$30.error("[stream] executePrompt 未返回终态回调", {
|
|
4109
4198
|
sessionId,
|
|
4110
4199
|
runId: run.runId
|
|
4111
4200
|
});
|
|
@@ -4122,7 +4211,7 @@ function createChatRouter(config) {
|
|
|
4122
4211
|
}
|
|
4123
4212
|
} catch (err) {
|
|
4124
4213
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4125
|
-
log$
|
|
4214
|
+
log$30.error("[stream] executePrompt 异常", {
|
|
4126
4215
|
sessionId,
|
|
4127
4216
|
runId: run.runId,
|
|
4128
4217
|
error: errorMessage
|
|
@@ -4139,7 +4228,7 @@ function createChatRouter(config) {
|
|
|
4139
4228
|
}
|
|
4140
4229
|
const cursor = req.query.cursor;
|
|
4141
4230
|
const limit = parseInt(req.query.limit, 10) || 50;
|
|
4142
|
-
log$
|
|
4231
|
+
log$30.info("[history] 查询会话历史", {
|
|
4143
4232
|
userId: chatUser.userId,
|
|
4144
4233
|
sessionId,
|
|
4145
4234
|
cursor,
|
|
@@ -4165,7 +4254,7 @@ function createChatRouter(config) {
|
|
|
4165
4254
|
});
|
|
4166
4255
|
} catch (err) {
|
|
4167
4256
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4168
|
-
log$
|
|
4257
|
+
log$30.error("[history] 查询历史消息失败", {
|
|
4169
4258
|
sessionId,
|
|
4170
4259
|
error: errorMessage
|
|
4171
4260
|
});
|
|
@@ -4201,13 +4290,13 @@ function createChatRouter(config) {
|
|
|
4201
4290
|
});
|
|
4202
4291
|
router.get("/api/chat/sessions", chatAuthMiddleware, async (_req, res) => {
|
|
4203
4292
|
const chatUser = res.locals.chatUser;
|
|
4204
|
-
log$
|
|
4293
|
+
log$30.info("[sessions] 列出用户会话", { userId: chatUser.userId });
|
|
4205
4294
|
try {
|
|
4206
4295
|
const sessions = await messageStore.listSessions(chatUser.openId);
|
|
4207
4296
|
res.json({ sessions });
|
|
4208
4297
|
} catch (err) {
|
|
4209
4298
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4210
|
-
log$
|
|
4299
|
+
log$30.error("[sessions] 列出会话失败", {
|
|
4211
4300
|
userId: chatUser.userId,
|
|
4212
4301
|
error: errorMessage
|
|
4213
4302
|
});
|
|
@@ -4217,7 +4306,7 @@ function createChatRouter(config) {
|
|
|
4217
4306
|
router.post("/api/chat/sessions", chatAuthMiddleware, async (req, res) => {
|
|
4218
4307
|
const chatUser = res.locals.chatUser;
|
|
4219
4308
|
const body = req.body;
|
|
4220
|
-
log$
|
|
4309
|
+
log$30.info("[sessions] 创建新会话", {
|
|
4221
4310
|
userId: chatUser.userId,
|
|
4222
4311
|
title: body.title
|
|
4223
4312
|
});
|
|
@@ -4232,7 +4321,7 @@ function createChatRouter(config) {
|
|
|
4232
4321
|
res.json({ session });
|
|
4233
4322
|
} catch (err) {
|
|
4234
4323
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4235
|
-
log$
|
|
4324
|
+
log$30.error("[sessions] 创建会话失败", {
|
|
4236
4325
|
userId: chatUser.userId,
|
|
4237
4326
|
error: errorMessage
|
|
4238
4327
|
});
|
|
@@ -4242,7 +4331,7 @@ function createChatRouter(config) {
|
|
|
4242
4331
|
router.delete("/api/chat/sessions/:id", chatAuthMiddleware, async (req, res) => {
|
|
4243
4332
|
const chatUser = res.locals.chatUser;
|
|
4244
4333
|
const sessionId = String(req.params.id);
|
|
4245
|
-
log$
|
|
4334
|
+
log$30.info("[sessions] 删除会话", {
|
|
4246
4335
|
userId: chatUser.userId,
|
|
4247
4336
|
sessionId
|
|
4248
4337
|
});
|
|
@@ -4252,7 +4341,7 @@ function createChatRouter(config) {
|
|
|
4252
4341
|
res.json({ success: true });
|
|
4253
4342
|
} catch (err) {
|
|
4254
4343
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4255
|
-
log$
|
|
4344
|
+
log$30.error("[sessions] 删除会话失败", {
|
|
4256
4345
|
sessionId,
|
|
4257
4346
|
error: errorMessage
|
|
4258
4347
|
});
|
|
@@ -4261,34 +4350,6 @@ function createChatRouter(config) {
|
|
|
4261
4350
|
});
|
|
4262
4351
|
return router;
|
|
4263
4352
|
}
|
|
4264
|
-
/**
|
|
4265
|
-
* 将 sourceArtifacts 中的文件信息(fileToken、fileName 等)注入到用户 prompt 中,
|
|
4266
|
-
* 使 LLM 能看到实际的 file token 值并直接用于下载。
|
|
4267
|
-
*
|
|
4268
|
-
* 如果没有 sourceArtifacts 或无法提取有效信息,返回原始 prompt。
|
|
4269
|
-
*/
|
|
4270
|
-
function enrichPromptWithSourceArtifacts(prompt, sourceArtifacts) {
|
|
4271
|
-
if (!sourceArtifacts) return prompt;
|
|
4272
|
-
const lines = [];
|
|
4273
|
-
const extractInfo = (artifact) => {
|
|
4274
|
-
if (!artifact || typeof artifact !== "object") return;
|
|
4275
|
-
const record = artifact;
|
|
4276
|
-
const fileToken = record.fileToken ?? record.file_token ?? record.driveFileToken ?? record.drive_file_token;
|
|
4277
|
-
const fileName = record.fileName ?? record.file_name ?? record.name ?? record.title;
|
|
4278
|
-
const fileType = record.fileType ?? record.file_type ?? record.type ?? record.mimeType;
|
|
4279
|
-
const fileUrl = record.fileUrl ?? record.file_url ?? record.url ?? record.uri;
|
|
4280
|
-
if (fileToken || fileName || fileUrl) {
|
|
4281
|
-
if (fileName) lines.push(`- fileName: ${String(fileName)}`);
|
|
4282
|
-
if (fileToken) lines.push(`- fileToken: ${String(fileToken)}`);
|
|
4283
|
-
if (fileType) lines.push(`- fileType: ${String(fileType)}`);
|
|
4284
|
-
if (fileUrl) lines.push(`- fileUrl: ${String(fileUrl)}`);
|
|
4285
|
-
}
|
|
4286
|
-
};
|
|
4287
|
-
if (Array.isArray(sourceArtifacts)) for (const item of sourceArtifacts) extractInfo(item);
|
|
4288
|
-
else extractInfo(sourceArtifacts);
|
|
4289
|
-
if (lines.length === 0) return prompt;
|
|
4290
|
-
return `${prompt}\n\n[Attached Source File]\n${lines.join("\n")}`;
|
|
4291
|
-
}
|
|
4292
4353
|
//#endregion
|
|
4293
4354
|
//#region src/core/app-info-sync.ts
|
|
4294
4355
|
/**
|
|
@@ -4306,7 +4367,7 @@ function enrichPromptWithSourceArtifacts(prompt, sourceArtifacts) {
|
|
|
4306
4367
|
* - GET /open-apis/application/v6/applications/me?lang=zh_cn(需要 application:application:self_manage 权限,返回完整信息)
|
|
4307
4368
|
* 如果后者无权限则 fallback 到前者
|
|
4308
4369
|
*/
|
|
4309
|
-
const log$
|
|
4370
|
+
const log$29 = larkLogger("core/app-info-sync");
|
|
4310
4371
|
/**
|
|
4311
4372
|
* 从飞书获取应用信息
|
|
4312
4373
|
*
|
|
@@ -4317,7 +4378,7 @@ async function fetchAppInfo(credentials) {
|
|
|
4317
4378
|
const { appId, appSecret } = credentials;
|
|
4318
4379
|
const token = await getTenantAccessToken(appId, appSecret);
|
|
4319
4380
|
if (!token) {
|
|
4320
|
-
log$
|
|
4381
|
+
log$29.error("获取 tenant_access_token 失败,无法同步应用信息");
|
|
4321
4382
|
return null;
|
|
4322
4383
|
}
|
|
4323
4384
|
const fullInfo = await fetchFromApplicationApi(token);
|
|
@@ -4328,13 +4389,13 @@ async function fetchAppInfo(credentials) {
|
|
|
4328
4389
|
async function fetchFromApplicationApi(token) {
|
|
4329
4390
|
try {
|
|
4330
4391
|
const data = await (await fetch("https://open.feishu.cn/open-apis/application/v6/applications/me?lang=zh_cn", { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
4331
|
-
log$
|
|
4392
|
+
log$29.info("application/v6 API 响应", {
|
|
4332
4393
|
code: data.code,
|
|
4333
4394
|
msg: data.msg,
|
|
4334
4395
|
hasApp: !!data.data?.app
|
|
4335
4396
|
});
|
|
4336
4397
|
if (data.code !== 0 || !data.data?.app) {
|
|
4337
|
-
log$
|
|
4398
|
+
log$29.warn("application/v6 API 返回非零或无数据,将 fallback", {
|
|
4338
4399
|
code: data.code,
|
|
4339
4400
|
msg: data.msg
|
|
4340
4401
|
});
|
|
@@ -4349,7 +4410,7 @@ async function fetchFromApplicationApi(token) {
|
|
|
4349
4410
|
helpDocUrl: zhInfo?.help_use
|
|
4350
4411
|
};
|
|
4351
4412
|
} catch (err) {
|
|
4352
|
-
log$
|
|
4413
|
+
log$29.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4353
4414
|
return null;
|
|
4354
4415
|
}
|
|
4355
4416
|
}
|
|
@@ -4357,13 +4418,13 @@ async function fetchFromApplicationApi(token) {
|
|
|
4357
4418
|
async function fetchFromBotApi(token) {
|
|
4358
4419
|
try {
|
|
4359
4420
|
const data = await (await fetch("https://open.feishu.cn/open-apis/bot/v3/info", { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
4360
|
-
log$
|
|
4421
|
+
log$29.info("bot/v3/info API 响应", {
|
|
4361
4422
|
code: data.code,
|
|
4362
4423
|
msg: data.msg,
|
|
4363
4424
|
hasBot: !!data.bot
|
|
4364
4425
|
});
|
|
4365
4426
|
if (data.code !== 0 || !data.bot) {
|
|
4366
|
-
log$
|
|
4427
|
+
log$29.error("bot/v3/info API 失败", {
|
|
4367
4428
|
code: data.code,
|
|
4368
4429
|
msg: data.msg
|
|
4369
4430
|
});
|
|
@@ -4374,7 +4435,7 @@ async function fetchFromBotApi(token) {
|
|
|
4374
4435
|
avatarUrl: data.bot.avatar_url
|
|
4375
4436
|
};
|
|
4376
4437
|
} catch (err) {
|
|
4377
|
-
log$
|
|
4438
|
+
log$29.error("bot/v3/info API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4378
4439
|
return null;
|
|
4379
4440
|
}
|
|
4380
4441
|
}
|
|
@@ -4390,7 +4451,7 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
4390
4451
|
})
|
|
4391
4452
|
})).json();
|
|
4392
4453
|
if (data.code !== 0 || !data.tenant_access_token) {
|
|
4393
|
-
log$
|
|
4454
|
+
log$29.error("获取 tenant_access_token 失败", {
|
|
4394
4455
|
code: data.code,
|
|
4395
4456
|
msg: data.msg
|
|
4396
4457
|
});
|
|
@@ -4398,7 +4459,7 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
4398
4459
|
}
|
|
4399
4460
|
return data.tenant_access_token;
|
|
4400
4461
|
} catch (err) {
|
|
4401
|
-
log$
|
|
4462
|
+
log$29.error("获取 tenant_access_token 异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4402
4463
|
return null;
|
|
4403
4464
|
}
|
|
4404
4465
|
}
|
|
@@ -4436,10 +4497,10 @@ function parseDocTokenFromUrl(url) {
|
|
|
4436
4497
|
async function fetchDocContent(docUrl, accessToken) {
|
|
4437
4498
|
const parsed = parseDocTokenFromUrl(docUrl);
|
|
4438
4499
|
if (!parsed) {
|
|
4439
|
-
log$
|
|
4500
|
+
log$29.warn("无法从 URL 中解析文档 token", { url: docUrl });
|
|
4440
4501
|
return null;
|
|
4441
4502
|
}
|
|
4442
|
-
log$
|
|
4503
|
+
log$29.info("开始读取人设文档", {
|
|
4443
4504
|
url: docUrl,
|
|
4444
4505
|
type: parsed.type,
|
|
4445
4506
|
token: parsed.token
|
|
@@ -4447,18 +4508,18 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4447
4508
|
let docToken = parsed.token;
|
|
4448
4509
|
if (parsed.type === "wiki") {
|
|
4449
4510
|
const realToken = await resolveWikiNodeToDocToken(parsed.token, accessToken);
|
|
4450
|
-
if (!realToken) log$
|
|
4511
|
+
if (!realToken) log$29.warn("wiki 节点解析失败,尝试直接使用 token 读取");
|
|
4451
4512
|
else docToken = realToken;
|
|
4452
4513
|
}
|
|
4453
4514
|
try {
|
|
4454
4515
|
const data = await (await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
4455
|
-
log$
|
|
4516
|
+
log$29.info("文档 raw_content API 响应", {
|
|
4456
4517
|
code: data.code,
|
|
4457
4518
|
msg: data.msg,
|
|
4458
4519
|
contentLength: data.data?.content?.length
|
|
4459
4520
|
});
|
|
4460
4521
|
if (data.code !== 0 || !data.data?.content) {
|
|
4461
|
-
log$
|
|
4522
|
+
log$29.warn("读取文档内容失败", {
|
|
4462
4523
|
code: data.code,
|
|
4463
4524
|
msg: data.msg,
|
|
4464
4525
|
docToken
|
|
@@ -4467,7 +4528,7 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4467
4528
|
}
|
|
4468
4529
|
return data.data.content.trim();
|
|
4469
4530
|
} catch (err) {
|
|
4470
|
-
log$
|
|
4531
|
+
log$29.error("读取文档内容异常", {
|
|
4471
4532
|
error: err instanceof Error ? err.message : String(err),
|
|
4472
4533
|
docToken
|
|
4473
4534
|
});
|
|
@@ -4478,7 +4539,7 @@ async function fetchDocContent(docUrl, accessToken) {
|
|
|
4478
4539
|
async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
4479
4540
|
try {
|
|
4480
4541
|
const data = await (await fetch(`https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${wikiToken}`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
4481
|
-
log$
|
|
4542
|
+
log$29.info("wiki get_node API 响应", {
|
|
4482
4543
|
code: data.code,
|
|
4483
4544
|
msg: data.msg,
|
|
4484
4545
|
objType: data.data?.node?.obj_type
|
|
@@ -4486,7 +4547,7 @@ async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
|
4486
4547
|
if (data.code !== 0 || !data.data?.node?.obj_token) return null;
|
|
4487
4548
|
return data.data.node.obj_token;
|
|
4488
4549
|
} catch (err) {
|
|
4489
|
-
log$
|
|
4550
|
+
log$29.warn("wiki get_node 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
4490
4551
|
return null;
|
|
4491
4552
|
}
|
|
4492
4553
|
}
|
|
@@ -4508,7 +4569,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4508
4569
|
const infoBlock = buildAppInfoBlock(appInfo);
|
|
4509
4570
|
if (!existsSync(claudeMdPath)) {
|
|
4510
4571
|
await writeFile(claudeMdPath, infoBlock + "\n\n" + getDefaultClaudeMdBody(), "utf-8");
|
|
4511
|
-
log$
|
|
4572
|
+
log$29.info("CLAUDE.md 已创建(含应用信息)", { appName: appInfo.appName });
|
|
4512
4573
|
return;
|
|
4513
4574
|
}
|
|
4514
4575
|
let content = await readFile(claudeMdPath, "utf-8");
|
|
@@ -4520,7 +4581,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4520
4581
|
content = before + infoBlock + after;
|
|
4521
4582
|
} else content = infoBlock + "\n\n" + content;
|
|
4522
4583
|
await writeFile(claudeMdPath, content, "utf-8");
|
|
4523
|
-
log$
|
|
4584
|
+
log$29.info("CLAUDE.md 应用信息已同步", {
|
|
4524
4585
|
appName: appInfo.appName,
|
|
4525
4586
|
hasDescription: !!appInfo.description,
|
|
4526
4587
|
hasAvatar: !!appInfo.avatarUrl
|
|
@@ -4535,7 +4596,7 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
4535
4596
|
async function syncPersonaDocToClaudeMd(personaContent) {
|
|
4536
4597
|
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
4537
4598
|
if (!existsSync(claudeMdPath)) {
|
|
4538
|
-
log$
|
|
4599
|
+
log$29.warn("CLAUDE.md 不存在,无法同步人设文档(需先同步应用信息)");
|
|
4539
4600
|
return;
|
|
4540
4601
|
}
|
|
4541
4602
|
let content = await readFile(claudeMdPath, "utf-8");
|
|
@@ -4556,7 +4617,7 @@ async function syncPersonaDocToClaudeMd(personaContent) {
|
|
|
4556
4617
|
content = before + personaBlock + after;
|
|
4557
4618
|
} else content = content.trimEnd() + "\n\n" + personaBlock + "\n";
|
|
4558
4619
|
await writeFile(claudeMdPath, content, "utf-8");
|
|
4559
|
-
log$
|
|
4620
|
+
log$29.info("CLAUDE.md 人设文档已同步", { contentLength: personaContent.length });
|
|
4560
4621
|
}
|
|
4561
4622
|
/** 构建应用信息标记区块 */
|
|
4562
4623
|
function buildAppInfoBlock(appInfo) {
|
|
@@ -4648,24 +4709,24 @@ function getDefaultClaudeMdBody() {
|
|
|
4648
4709
|
* @returns 同步后的应用信息,如果失败返回 null
|
|
4649
4710
|
*/
|
|
4650
4711
|
async function syncAppInfo(credentials) {
|
|
4651
|
-
log$
|
|
4712
|
+
log$29.info("开始同步应用信息", { appId: credentials.appId });
|
|
4652
4713
|
const appInfo = await fetchAppInfo(credentials);
|
|
4653
4714
|
if (!appInfo) {
|
|
4654
|
-
log$
|
|
4715
|
+
log$29.warn("获取应用信息失败,跳过同步");
|
|
4655
4716
|
return null;
|
|
4656
4717
|
}
|
|
4657
4718
|
await syncAppInfoToClaudeMd(appInfo);
|
|
4658
4719
|
if (appInfo.helpDocUrl) {
|
|
4659
|
-
log$
|
|
4720
|
+
log$29.info("检测到帮助文档 URL,尝试同步人设文档", { helpDocUrl: appInfo.helpDocUrl });
|
|
4660
4721
|
const token = await getTenantAccessToken(credentials.appId, credentials.appSecret);
|
|
4661
4722
|
if (token) {
|
|
4662
4723
|
const personaContent = await fetchDocContent(appInfo.helpDocUrl, token);
|
|
4663
4724
|
if (personaContent) await syncPersonaDocToClaudeMd(personaContent);
|
|
4664
|
-
else log$
|
|
4725
|
+
else log$29.warn("人设文档内容为空或读取失败,跳过同步");
|
|
4665
4726
|
}
|
|
4666
4727
|
}
|
|
4667
4728
|
await installSyncSkill();
|
|
4668
|
-
log$
|
|
4729
|
+
log$29.info("应用信息同步完成", {
|
|
4669
4730
|
appName: appInfo.appName,
|
|
4670
4731
|
description: appInfo.description?.substring(0, 50),
|
|
4671
4732
|
hasPersonaDoc: !!appInfo.helpDocUrl
|
|
@@ -4683,7 +4744,7 @@ async function installSyncSkill() {
|
|
|
4683
4744
|
if ((await readFile(SYNC_SKILL_PATH, "utf-8")).includes(`skill-version: ${SYNC_SKILL_VERSION}`)) return;
|
|
4684
4745
|
}
|
|
4685
4746
|
await writeFile(SYNC_SKILL_PATH, SYNC_SKILL_CONTENT, "utf-8");
|
|
4686
|
-
log$
|
|
4747
|
+
log$29.info("sync-app-info 技能已安装", { path: SYNC_SKILL_PATH });
|
|
4687
4748
|
}
|
|
4688
4749
|
const SYNC_SKILL_CONTENT = `---
|
|
4689
4750
|
skill-version: ${SYNC_SKILL_VERSION}
|
|
@@ -4938,6 +4999,269 @@ async function sendCallback(callbackUrl, taskId, status, result, error) {
|
|
|
4938
4999
|
}
|
|
4939
5000
|
}
|
|
4940
5001
|
//#endregion
|
|
5002
|
+
//#region src/gateway/artifact-store.ts
|
|
5003
|
+
const DEFAULT_ARTIFACT_TTL_MS = 4320 * 60 * 1e3;
|
|
5004
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
5005
|
+
var ArtifactUploadError = class extends Error {
|
|
5006
|
+
constructor(message, statusCode = 400) {
|
|
5007
|
+
super(message);
|
|
5008
|
+
this.statusCode = statusCode;
|
|
5009
|
+
this.name = "ArtifactUploadError";
|
|
5010
|
+
}
|
|
5011
|
+
};
|
|
5012
|
+
var GatewayArtifactStore = class {
|
|
5013
|
+
rootDir;
|
|
5014
|
+
maxUploadBytes;
|
|
5015
|
+
ttlMs;
|
|
5016
|
+
constructor(params) {
|
|
5017
|
+
const workspaceRoot = params?.workspaceRoot || resolveWorkspaceRoot();
|
|
5018
|
+
this.rootDir = join(workspaceRoot, ".artifacts");
|
|
5019
|
+
this.maxUploadBytes = params?.maxUploadBytes ?? readPositiveIntEnv("LARKPAL_ARTIFACT_MAX_UPLOAD_BYTES", DEFAULT_MAX_UPLOAD_BYTES);
|
|
5020
|
+
this.ttlMs = params?.ttlMs ?? readPositiveIntEnv("LARKPAL_ARTIFACT_TTL_MS", DEFAULT_ARTIFACT_TTL_MS);
|
|
5021
|
+
}
|
|
5022
|
+
async create(input) {
|
|
5023
|
+
validateContextId(input.sessionId, "sessionId");
|
|
5024
|
+
validateContextId(input.owner.tenantKey, "tenantKey");
|
|
5025
|
+
const artifactId = `art_${v4().replace(/-/g, "")}`;
|
|
5026
|
+
const createdAtMs = Date.now();
|
|
5027
|
+
const createdAt = new Date(createdAtMs).toISOString();
|
|
5028
|
+
const expiresAt = new Date(createdAtMs + this.ttlMs).toISOString();
|
|
5029
|
+
const artifactDir = this.getArtifactDir(input.owner.tenantKey, input.sessionId, artifactId);
|
|
5030
|
+
const blobPath = join(artifactDir, "blob");
|
|
5031
|
+
const tmpBlobPath = join(artifactDir, `.blob.${process.pid}.${Date.now()}.tmp`);
|
|
5032
|
+
await mkdir(artifactDir, { recursive: true });
|
|
5033
|
+
const hash = createHash("sha256");
|
|
5034
|
+
let size = 0;
|
|
5035
|
+
const metering = new Transform({ transform: (chunk, _encoding, callback) => {
|
|
5036
|
+
size += chunk.length;
|
|
5037
|
+
if (size > this.maxUploadBytes) {
|
|
5038
|
+
callback(new ArtifactUploadError(`Artifact exceeds max upload size ${this.maxUploadBytes} bytes`, 413));
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
hash.update(chunk);
|
|
5042
|
+
callback(null, chunk);
|
|
5043
|
+
} });
|
|
5044
|
+
try {
|
|
5045
|
+
await pipeline(input.stream, metering, createWriteStream(tmpBlobPath));
|
|
5046
|
+
await rename(tmpBlobPath, blobPath);
|
|
5047
|
+
} catch (err) {
|
|
5048
|
+
await rm(artifactDir, {
|
|
5049
|
+
recursive: true,
|
|
5050
|
+
force: true
|
|
5051
|
+
}).catch(() => void 0);
|
|
5052
|
+
throw err;
|
|
5053
|
+
}
|
|
5054
|
+
const metadata = {
|
|
5055
|
+
artifactId,
|
|
5056
|
+
sessionId: input.sessionId,
|
|
5057
|
+
tenantKey: input.owner.tenantKey,
|
|
5058
|
+
userId: input.owner.userId,
|
|
5059
|
+
openId: input.owner.openId,
|
|
5060
|
+
kind: "workspace-file",
|
|
5061
|
+
uri: `artifact://${artifactId}`,
|
|
5062
|
+
fileName: sanitizeFileName(input.fileName),
|
|
5063
|
+
mimeType: normalizeMimeType(input.mimeType),
|
|
5064
|
+
size,
|
|
5065
|
+
contentHash: `sha256:${hash.digest("hex")}`,
|
|
5066
|
+
createdAt,
|
|
5067
|
+
expiresAt,
|
|
5068
|
+
metadata: input.metadata,
|
|
5069
|
+
blobPath
|
|
5070
|
+
};
|
|
5071
|
+
await writeJson(join(artifactDir, "meta.json"), metadata);
|
|
5072
|
+
return toPublicRef(metadata);
|
|
5073
|
+
}
|
|
5074
|
+
async list(params) {
|
|
5075
|
+
validateContextId(params.sessionId, "sessionId");
|
|
5076
|
+
validateContextId(params.owner.tenantKey, "tenantKey");
|
|
5077
|
+
const sessionDir = this.getSessionDir(params.owner.tenantKey, params.sessionId);
|
|
5078
|
+
let entries;
|
|
5079
|
+
try {
|
|
5080
|
+
entries = await readdir(sessionDir);
|
|
5081
|
+
} catch {
|
|
5082
|
+
return [];
|
|
5083
|
+
}
|
|
5084
|
+
const results = [];
|
|
5085
|
+
for (const entry of entries) {
|
|
5086
|
+
if (!entry.startsWith("art_")) continue;
|
|
5087
|
+
const metadata = await readArtifactMetadata(join(sessionDir, entry, "meta.json"));
|
|
5088
|
+
if (!metadata) continue;
|
|
5089
|
+
if (!isOwnedBy(metadata, params.owner, params.sessionId)) continue;
|
|
5090
|
+
if (Date.parse(metadata.expiresAt) <= Date.now()) continue;
|
|
5091
|
+
results.push(toPublicRef(metadata));
|
|
5092
|
+
}
|
|
5093
|
+
return results.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
5094
|
+
}
|
|
5095
|
+
async statBlob(ref, owner) {
|
|
5096
|
+
const metadata = await readArtifactMetadata(this.getMetadataPath(owner.tenantKey, ref.sessionId, ref.artifactId));
|
|
5097
|
+
if (!metadata || !isOwnedBy(metadata, owner, ref.sessionId)) return null;
|
|
5098
|
+
const blobStat = await stat(metadata.blobPath).catch(() => null);
|
|
5099
|
+
return blobStat ? {
|
|
5100
|
+
path: metadata.blobPath,
|
|
5101
|
+
size: blobStat.size
|
|
5102
|
+
} : null;
|
|
5103
|
+
}
|
|
5104
|
+
getSessionDir(tenantKey, sessionId) {
|
|
5105
|
+
return join(this.rootDir, tenantKey, sessionId);
|
|
5106
|
+
}
|
|
5107
|
+
getArtifactDir(tenantKey, sessionId, artifactId) {
|
|
5108
|
+
return join(this.getSessionDir(tenantKey, sessionId), artifactId);
|
|
5109
|
+
}
|
|
5110
|
+
getMetadataPath(tenantKey, sessionId, artifactId) {
|
|
5111
|
+
return join(this.getArtifactDir(tenantKey, sessionId, artifactId), "meta.json");
|
|
5112
|
+
}
|
|
5113
|
+
};
|
|
5114
|
+
function readPositiveIntEnv(name, fallback) {
|
|
5115
|
+
const raw = process.env[name];
|
|
5116
|
+
if (!raw) return fallback;
|
|
5117
|
+
const parsed = Number.parseInt(raw, 10);
|
|
5118
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
5119
|
+
}
|
|
5120
|
+
function validateContextId(value, fieldName) {
|
|
5121
|
+
if (!/^[A-Za-z0-9_.:-]{1,200}$/.test(value)) throw new ArtifactUploadError(`Invalid ${fieldName}`, 400);
|
|
5122
|
+
}
|
|
5123
|
+
function sanitizeFileName(fileName) {
|
|
5124
|
+
return fileName.replace(/[\\/\0\r\n]/g, "_").trim() || "upload.bin";
|
|
5125
|
+
}
|
|
5126
|
+
function normalizeMimeType(mimeType) {
|
|
5127
|
+
return mimeType?.trim() || "application/octet-stream";
|
|
5128
|
+
}
|
|
5129
|
+
function toPublicRef(metadata) {
|
|
5130
|
+
const { tenantKey: _tenantKey, userId: _userId, openId: _openId, blobPath: _blobPath, ...ref } = metadata;
|
|
5131
|
+
return ref;
|
|
5132
|
+
}
|
|
5133
|
+
function isOwnedBy(metadata, owner, sessionId) {
|
|
5134
|
+
return metadata.tenantKey === owner.tenantKey && metadata.userId === owner.userId && metadata.openId === owner.openId && metadata.sessionId === sessionId;
|
|
5135
|
+
}
|
|
5136
|
+
async function readArtifactMetadata(filePath) {
|
|
5137
|
+
try {
|
|
5138
|
+
return JSON.parse(await readFile(filePath, "utf-8"));
|
|
5139
|
+
} catch {
|
|
5140
|
+
return null;
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
async function writeJson(filePath, data) {
|
|
5144
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
5145
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
5146
|
+
}
|
|
5147
|
+
//#endregion
|
|
5148
|
+
//#region src/gateway/artifact-handler.ts
|
|
5149
|
+
const log$28 = larkLogger("gateway/artifacts");
|
|
5150
|
+
function createArtifactRouter(config) {
|
|
5151
|
+
const router = Router();
|
|
5152
|
+
const store = config?.store || new GatewayArtifactStore({ workspaceRoot: config?.workspaceRoot });
|
|
5153
|
+
router.post("/api/chat/artifacts", chatAuthMiddleware, (req, res) => {
|
|
5154
|
+
handleUpload(req, res, store);
|
|
5155
|
+
});
|
|
5156
|
+
router.get("/api/chat/artifacts", chatAuthMiddleware, (req, res) => {
|
|
5157
|
+
handleList(req, res, store);
|
|
5158
|
+
});
|
|
5159
|
+
return router;
|
|
5160
|
+
}
|
|
5161
|
+
async function handleList(req, res, store) {
|
|
5162
|
+
const sessionId = readString(req.query.session_id || req.query.sessionId);
|
|
5163
|
+
if (!sessionId) {
|
|
5164
|
+
res.status(400).json({ error: "session_id is required" });
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
const artifacts = await store.list({
|
|
5168
|
+
sessionId,
|
|
5169
|
+
owner: toOwner(res.locals.chatUser)
|
|
5170
|
+
});
|
|
5171
|
+
res.json({ artifacts });
|
|
5172
|
+
}
|
|
5173
|
+
async function handleUpload(req, res, store) {
|
|
5174
|
+
if (!req.headers["content-type"]?.includes("multipart/form-data")) {
|
|
5175
|
+
res.status(415).json({ error: "Content-Type must be multipart/form-data" });
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
const chatUser = res.locals.chatUser;
|
|
5179
|
+
try {
|
|
5180
|
+
const artifact = await parseMultipartUpload(req, store, chatUser);
|
|
5181
|
+
res.status(201).json({
|
|
5182
|
+
artifact,
|
|
5183
|
+
sourceArtifacts: [artifact]
|
|
5184
|
+
});
|
|
5185
|
+
} catch (err) {
|
|
5186
|
+
const statusCode = err instanceof ArtifactUploadError ? err.statusCode : 500;
|
|
5187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5188
|
+
log$28.warn("artifact upload failed", {
|
|
5189
|
+
statusCode,
|
|
5190
|
+
message
|
|
5191
|
+
});
|
|
5192
|
+
res.status(statusCode).json({ error: message });
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
function parseMultipartUpload(req, store, chatUser) {
|
|
5196
|
+
return new Promise((resolve, reject) => {
|
|
5197
|
+
const busboy = Busboy({ headers: req.headers });
|
|
5198
|
+
const owner = toOwner(chatUser);
|
|
5199
|
+
let sessionId = readString(req.query.session_id || req.query.sessionId) ?? "";
|
|
5200
|
+
let metadata;
|
|
5201
|
+
let uploadPromise;
|
|
5202
|
+
let fileSeen = false;
|
|
5203
|
+
let settled = false;
|
|
5204
|
+
const fail = (error) => {
|
|
5205
|
+
if (settled) return;
|
|
5206
|
+
settled = true;
|
|
5207
|
+
reject(error);
|
|
5208
|
+
};
|
|
5209
|
+
busboy.on("field", (name, value) => {
|
|
5210
|
+
if (name === "session_id" || name === "sessionId") sessionId = value.trim();
|
|
5211
|
+
else if (name === "metadata" && value.trim()) try {
|
|
5212
|
+
const parsed = JSON.parse(value);
|
|
5213
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) metadata = parsed;
|
|
5214
|
+
else fail(new ArtifactUploadError("metadata must be a JSON object"));
|
|
5215
|
+
} catch (err) {
|
|
5216
|
+
fail(new ArtifactUploadError(`metadata is not valid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
5217
|
+
}
|
|
5218
|
+
});
|
|
5219
|
+
busboy.on("file", (_name, file, info) => {
|
|
5220
|
+
if (fileSeen) {
|
|
5221
|
+
file.resume();
|
|
5222
|
+
fail(new ArtifactUploadError("Only one file is supported per upload"));
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
fileSeen = true;
|
|
5226
|
+
if (!sessionId) {
|
|
5227
|
+
file.resume();
|
|
5228
|
+
fail(new ArtifactUploadError("session_id is required before file field"));
|
|
5229
|
+
return;
|
|
5230
|
+
}
|
|
5231
|
+
const stream = new PassThrough();
|
|
5232
|
+
uploadPromise = store.create({
|
|
5233
|
+
sessionId,
|
|
5234
|
+
owner,
|
|
5235
|
+
fileName: info.filename || "upload.bin",
|
|
5236
|
+
mimeType: info.mimeType,
|
|
5237
|
+
metadata,
|
|
5238
|
+
stream
|
|
5239
|
+
});
|
|
5240
|
+
file.pipe(stream);
|
|
5241
|
+
});
|
|
5242
|
+
busboy.on("error", fail);
|
|
5243
|
+
busboy.on("finish", () => {
|
|
5244
|
+
if (settled) return;
|
|
5245
|
+
if (!fileSeen || !uploadPromise) {
|
|
5246
|
+
fail(new ArtifactUploadError("file is required"));
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
5249
|
+
uploadPromise.then(resolve, reject);
|
|
5250
|
+
});
|
|
5251
|
+
req.pipe(busboy);
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
function toOwner(chatUser) {
|
|
5255
|
+
return {
|
|
5256
|
+
tenantKey: chatUser.tenantKey,
|
|
5257
|
+
userId: chatUser.userId,
|
|
5258
|
+
openId: chatUser.openId
|
|
5259
|
+
};
|
|
5260
|
+
}
|
|
5261
|
+
function readString(value) {
|
|
5262
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
5263
|
+
}
|
|
5264
|
+
//#endregion
|
|
4941
5265
|
//#region src/gateway/execute-handler.ts
|
|
4942
5266
|
/**
|
|
4943
5267
|
* 执行接口路由 — 提供 CC 任务的创建、查询、取消和批量执行能力
|
|
@@ -6895,6 +7219,7 @@ function registerRoutes(app, processManager, scheduledTaskManager, appCredential
|
|
|
6895
7219
|
app.use("/hooks", createHooksRouter());
|
|
6896
7220
|
app.use("/api/mcp", createMcpCallbackRouter());
|
|
6897
7221
|
app.use(createChatAuthRouter());
|
|
7222
|
+
app.use(createArtifactRouter({ workspaceRoot }));
|
|
6898
7223
|
if (processManager && messageStore) app.use(createChatRouter({
|
|
6899
7224
|
processManager,
|
|
6900
7225
|
messageStore
|
|
@@ -8498,9 +8823,31 @@ function sortTraceValue(value) {
|
|
|
8498
8823
|
return value;
|
|
8499
8824
|
}
|
|
8500
8825
|
//#endregion
|
|
8826
|
+
//#region src/card/user-facing-error.ts
|
|
8827
|
+
function createUserFacingError(message, causeMessage) {
|
|
8828
|
+
const error = new Error(causeMessage || message);
|
|
8829
|
+
error.userFacingMessage = message;
|
|
8830
|
+
return error;
|
|
8831
|
+
}
|
|
8832
|
+
function getUserFacingErrorMessage(error) {
|
|
8833
|
+
if (!error || typeof error !== "object") return void 0;
|
|
8834
|
+
const message = error.userFacingMessage;
|
|
8835
|
+
return typeof message === "string" && message.trim() ? message : void 0;
|
|
8836
|
+
}
|
|
8837
|
+
//#endregion
|
|
8501
8838
|
//#region src/card/cc-stream-bridge.ts
|
|
8502
8839
|
const log$21 = larkLogger("card/cc-stream-bridge");
|
|
8503
8840
|
const CC_INTERNAL_PLACEHOLDER = "No response requested.";
|
|
8841
|
+
const CLAUDE_INVALID_PARAMETER_PATTERN = /API Error:\s*400\b.*parameter specified in the request is not valid/i;
|
|
8842
|
+
function buildClaudeCodeUserFacingError(errorMessage, hasImages) {
|
|
8843
|
+
if (hasImages && CLAUDE_INVALID_PARAMETER_PATTERN.test(errorMessage)) return createUserFacingError([
|
|
8844
|
+
"当前运行时无法处理这条图片消息。",
|
|
8845
|
+
"",
|
|
8846
|
+
"图片已经下载成功,但 Claude Code 当前模型接口返回了无效参数错误,通常表示当前模型或方舟 coding endpoint 不支持这类图片输入。",
|
|
8847
|
+
"请改用支持图片的 agent 引擎,或先用文字描述图片内容后再试。"
|
|
8848
|
+
].join("\n"), errorMessage);
|
|
8849
|
+
return new Error(errorMessage);
|
|
8850
|
+
}
|
|
8504
8851
|
/**
|
|
8505
8852
|
* CCStreamBridge — 将 CC 流事件桥接到 StreamingCardController
|
|
8506
8853
|
*
|
|
@@ -8667,11 +9014,13 @@ var CCStreamBridge = class {
|
|
|
8667
9014
|
this.controller.onIdle();
|
|
8668
9015
|
} else {
|
|
8669
9016
|
const errorMessage = data.result ?? `CC 执行失败: ${data.subtype}`;
|
|
9017
|
+
const hasImages = sessionId && pm ? pm.getLastMessageHadImages(sessionId) : false;
|
|
8670
9018
|
log$21.error("CC 执行返回错误", {
|
|
8671
9019
|
subtype: data.subtype,
|
|
9020
|
+
hasImages,
|
|
8672
9021
|
errorMessage: errorMessage.slice(0, 200)
|
|
8673
9022
|
});
|
|
8674
|
-
this.controller.onError(
|
|
9023
|
+
this.controller.onError(buildClaudeCodeUserFacingError(errorMessage, !!hasImages), { kind: "cc-execution" });
|
|
8675
9024
|
}
|
|
8676
9025
|
} else if (!this.accumulatedText.trim()) if (data.result) {
|
|
8677
9026
|
log$21.info("result 兜底:使用 result.result 作为最终回复", {
|
|
@@ -10877,26 +11226,6 @@ async function sendCardByCardId(params) {
|
|
|
10877
11226
|
chatId: response?.data?.chat_id ?? ""
|
|
10878
11227
|
};
|
|
10879
11228
|
}
|
|
10880
|
-
/**
|
|
10881
|
-
* Close (or open) the streaming mode on a CardKit card.
|
|
10882
|
-
*
|
|
10883
|
-
* Must be called after streaming is complete to restore normal card
|
|
10884
|
-
* behaviour (forwarding, interaction callbacks, etc.).
|
|
10885
|
-
*/
|
|
10886
|
-
async function setCardStreamingMode(params) {
|
|
10887
|
-
const { cfg, cardId, streamingMode, sequence, accountId } = params;
|
|
10888
|
-
logCardKitResponse({
|
|
10889
|
-
resp: await resolveLarkSdk(cfg, accountId).cardkit.v1.card.settings({
|
|
10890
|
-
data: {
|
|
10891
|
-
settings: JSON.stringify({ streaming_mode: streamingMode }),
|
|
10892
|
-
sequence
|
|
10893
|
-
},
|
|
10894
|
-
path: { card_id: cardId }
|
|
10895
|
-
}),
|
|
10896
|
-
api: "card.settings",
|
|
10897
|
-
context: `seq=${sequence}, streaming_mode=${streamingMode}`
|
|
10898
|
-
});
|
|
10899
|
-
}
|
|
10900
11229
|
//#endregion
|
|
10901
11230
|
//#region src/card/reply-dispatcher-types.ts
|
|
10902
11231
|
const TERMINAL_PHASES = new Set([
|
|
@@ -11865,8 +12194,9 @@ var StreamingCardController = class StreamingCardController {
|
|
|
11865
12194
|
const toolUseDisplay = this.computeToolUseDisplay();
|
|
11866
12195
|
try {
|
|
11867
12196
|
if (this.cardKit.cardMessageId) {
|
|
12197
|
+
const visibleError = getUserFacingErrorMessage(err) || "An error occurred while generating the response.";
|
|
11868
12198
|
const terminalContent = prepareTerminalCardContent({
|
|
11869
|
-
text: this.text.accumulatedText ? `${this.text.accumulatedText}\n\n---\n**Error**:
|
|
12199
|
+
text: this.text.accumulatedText ? `${this.text.accumulatedText}\n\n---\n**Error**: ${visibleError}` : `**Error**: ${visibleError}`,
|
|
11870
12200
|
reasoningText: this.reasoning.accumulatedReasoningText || void 0
|
|
11871
12201
|
}, this.imageResolver);
|
|
11872
12202
|
const errorCard = buildCardContent("complete", {
|
|
@@ -11882,7 +12212,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
11882
12212
|
footer: this.deps.resolvedFooter,
|
|
11883
12213
|
footerMetrics
|
|
11884
12214
|
});
|
|
11885
|
-
if (errorEffectiveCardId) await this.
|
|
12215
|
+
if (errorEffectiveCardId) await this.updateStreamingCard(errorEffectiveCardId, errorCard, "onError");
|
|
11886
12216
|
else await updateCardFeishu({
|
|
11887
12217
|
cfg: this.deps.cfg,
|
|
11888
12218
|
messageId: this.cardKit.cardMessageId,
|
|
@@ -11950,20 +12280,6 @@ var StreamingCardController = class StreamingCardController {
|
|
|
11950
12280
|
const FINAL_UPDATE_RETRY_DELAY_MS = 2e3;
|
|
11951
12281
|
for (let attempt = 1; attempt <= MAX_FINAL_UPDATE_RETRIES; attempt++) try {
|
|
11952
12282
|
if (idleEffectiveCardId) {
|
|
11953
|
-
const seqBeforeClose = this.cardKit.cardKitSequence;
|
|
11954
|
-
this.cardKit.cardKitSequence += 1;
|
|
11955
|
-
log$16.info("onIdle: closing streaming mode", {
|
|
11956
|
-
attempt,
|
|
11957
|
-
seqBefore: seqBeforeClose,
|
|
11958
|
-
seqAfter: this.cardKit.cardKitSequence
|
|
11959
|
-
});
|
|
11960
|
-
await setCardStreamingMode({
|
|
11961
|
-
cfg: this.deps.cfg,
|
|
11962
|
-
cardId: idleEffectiveCardId,
|
|
11963
|
-
streamingMode: false,
|
|
11964
|
-
sequence: this.cardKit.cardKitSequence,
|
|
11965
|
-
accountId: this.deps.accountId
|
|
11966
|
-
});
|
|
11967
12283
|
const seqBeforeUpdate = this.cardKit.cardKitSequence;
|
|
11968
12284
|
this.cardKit.cardKitSequence += 1;
|
|
11969
12285
|
log$16.info("onIdle: updating final card", {
|
|
@@ -12048,7 +12364,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
12048
12364
|
footer: this.deps.resolvedFooter,
|
|
12049
12365
|
footerMetrics
|
|
12050
12366
|
});
|
|
12051
|
-
await this.
|
|
12367
|
+
await this.updateStreamingCard(effectiveCardId, abortCardContent, "abortCard");
|
|
12052
12368
|
log$16.info("abortCard completed", { effectiveCardId });
|
|
12053
12369
|
} else if (this.cardKit.cardMessageId) {
|
|
12054
12370
|
const abortCard = buildCardContent("complete", {
|
|
@@ -12086,20 +12402,6 @@ var StreamingCardController = class StreamingCardController {
|
|
|
12086
12402
|
if (this.cardCreationPromise) await this.cardCreationPromise;
|
|
12087
12403
|
const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
|
|
12088
12404
|
if (effectiveCardId) {
|
|
12089
|
-
const seqBeforeClose = this.cardKit.cardKitSequence;
|
|
12090
|
-
this.cardKit.cardKitSequence += 1;
|
|
12091
|
-
log$16.info("replaceWithClarificationCard: closing streaming mode", {
|
|
12092
|
-
cardId: effectiveCardId,
|
|
12093
|
-
seqBefore: seqBeforeClose,
|
|
12094
|
-
seqAfter: this.cardKit.cardKitSequence
|
|
12095
|
-
});
|
|
12096
|
-
await setCardStreamingMode({
|
|
12097
|
-
cfg: this.deps.cfg,
|
|
12098
|
-
cardId: effectiveCardId,
|
|
12099
|
-
streamingMode: false,
|
|
12100
|
-
sequence: this.cardKit.cardKitSequence,
|
|
12101
|
-
accountId: this.deps.accountId
|
|
12102
|
-
});
|
|
12103
12405
|
const seqBeforeUpdate = this.cardKit.cardKitSequence;
|
|
12104
12406
|
this.cardKit.cardKitSequence += 1;
|
|
12105
12407
|
await updateCardKitCard({
|
|
@@ -12146,10 +12448,10 @@ var StreamingCardController = class StreamingCardController {
|
|
|
12146
12448
|
}
|
|
12147
12449
|
}
|
|
12148
12450
|
/**
|
|
12149
|
-
*
|
|
12451
|
+
* 强制终态化 streaming 卡片(兜底方法)。
|
|
12150
12452
|
*
|
|
12151
|
-
* 当进程已退出且卡片可能因 API
|
|
12152
|
-
* 由 session-control-handler
|
|
12453
|
+
* 当进程已退出且卡片可能因 API 调用失败仍停留在中间态时,
|
|
12454
|
+
* 由 session-control-handler 调用,直接更新卡片内容并保留 streaming mode。
|
|
12153
12455
|
* 即使已经是 terminal phase 也会尝试执行 API 调用。
|
|
12154
12456
|
*/
|
|
12155
12457
|
async forceCloseStreaming() {
|
|
@@ -12158,7 +12460,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
12158
12460
|
log$16.warn("forceCloseStreaming: no card to close");
|
|
12159
12461
|
return;
|
|
12160
12462
|
}
|
|
12161
|
-
log$16.info("forceCloseStreaming:
|
|
12463
|
+
log$16.info("forceCloseStreaming: 强制终态化 streaming 卡片", {
|
|
12162
12464
|
cardId: effectiveCardId,
|
|
12163
12465
|
messageId: this.cardKit.cardMessageId,
|
|
12164
12466
|
phase: this.phase
|
|
@@ -12186,14 +12488,6 @@ var StreamingCardController = class StreamingCardController {
|
|
|
12186
12488
|
showUndoButton: false
|
|
12187
12489
|
});
|
|
12188
12490
|
if (effectiveCardId) {
|
|
12189
|
-
this.cardKit.cardKitSequence += 1;
|
|
12190
|
-
await setCardStreamingMode({
|
|
12191
|
-
cfg: this.deps.cfg,
|
|
12192
|
-
cardId: effectiveCardId,
|
|
12193
|
-
streamingMode: false,
|
|
12194
|
-
sequence: this.cardKit.cardKitSequence,
|
|
12195
|
-
accountId: this.deps.accountId
|
|
12196
|
-
});
|
|
12197
12491
|
this.cardKit.cardKitSequence += 1;
|
|
12198
12492
|
await updateCardKitCard({
|
|
12199
12493
|
cfg: this.deps.cfg,
|
|
@@ -12507,22 +12801,9 @@ var StreamingCardController = class StreamingCardController {
|
|
|
12507
12801
|
this.onEnterTerminalPhase();
|
|
12508
12802
|
}
|
|
12509
12803
|
/**
|
|
12510
|
-
*
|
|
12804
|
+
* Update terminal card content while keeping CardKit streaming mode enabled.
|
|
12511
12805
|
*/
|
|
12512
|
-
async
|
|
12513
|
-
const seqBeforeClose = this.cardKit.cardKitSequence;
|
|
12514
|
-
this.cardKit.cardKitSequence += 1;
|
|
12515
|
-
log$16.info(`${label}: closing streaming mode`, {
|
|
12516
|
-
seqBefore: seqBeforeClose,
|
|
12517
|
-
seqAfter: this.cardKit.cardKitSequence
|
|
12518
|
-
});
|
|
12519
|
-
await setCardStreamingMode({
|
|
12520
|
-
cfg: this.deps.cfg,
|
|
12521
|
-
cardId,
|
|
12522
|
-
streamingMode: false,
|
|
12523
|
-
sequence: this.cardKit.cardKitSequence,
|
|
12524
|
-
accountId: this.deps.accountId
|
|
12525
|
-
});
|
|
12806
|
+
async updateStreamingCard(cardId, card, label) {
|
|
12526
12807
|
const seqBeforeUpdate = this.cardKit.cardKitSequence;
|
|
12527
12808
|
this.cardKit.cardKitSequence += 1;
|
|
12528
12809
|
log$16.info(`${label}: updating card`, {
|
|
@@ -13479,6 +13760,8 @@ async function dispatchToCC(params) {
|
|
|
13479
13760
|
const base64Data = buffer.toString("base64");
|
|
13480
13761
|
imageBlocks.push({
|
|
13481
13762
|
type: "image",
|
|
13763
|
+
filePath: imgFilePath,
|
|
13764
|
+
fileKey: imgRes.fileKey,
|
|
13482
13765
|
source: {
|
|
13483
13766
|
type: "base64",
|
|
13484
13767
|
media_type: mediaType,
|