@vibe-lark/larkpal 0.1.62 → 0.1.64

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