@vibe-lark/larkpal 0.1.40 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.mjs CHANGED
@@ -22,6 +22,7 @@ import path, { join as join$1 } from "path";
22
22
  import os, { homedir as homedir$1 } from "os";
23
23
  import express, { Router } from "express";
24
24
  import cron from "node-cron";
25
+ import { EventEmitter as EventEmitter$1 } from "events";
25
26
  import * as Lark from "@larksuiteoapi/node-sdk";
26
27
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
27
28
  import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
@@ -224,7 +225,7 @@ function ensureLarkCliConfig() {
224
225
  * 首次启动时检测并生成 ~/.claude/settings.json 和 ~/.claude/CLAUDE.md,
225
226
  * 以及确保会话工作目录结构完整。
226
227
  */
227
- const logger$9 = larkLogger("config/defaults");
228
+ const logger$10 = larkLogger("config/defaults");
228
229
  const DEFAULT_SETTINGS = {
229
230
  permissions: {
230
231
  allow: [
@@ -395,13 +396,13 @@ async function ensureDefaults() {
395
396
  const settingsPath = join(claudeDir, "settings.json");
396
397
  const claudeMdPath = join(claudeDir, "CLAUDE.md");
397
398
  await mkdir(claudeDir, { recursive: true });
398
- logger$9.info("确保 ~/.claude 目录存在", { path: claudeDir });
399
+ logger$10.info("确保 ~/.claude 目录存在", { path: claudeDir });
399
400
  await writeFile(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2), "utf-8");
400
- logger$9.info("settings.json 已同步(强制覆盖)", { path: settingsPath });
401
- if (await fileExists(claudeMdPath)) logger$9.info("CLAUDE.md 已存在,跳过生成", { path: claudeMdPath });
401
+ logger$10.info("settings.json 已同步(强制覆盖)", { path: settingsPath });
402
+ if (await fileExists(claudeMdPath)) logger$10.info("CLAUDE.md 已存在,跳过生成", { path: claudeMdPath });
402
403
  else {
403
404
  await writeFile(claudeMdPath, DEFAULT_CLAUDE_MD, "utf-8");
404
- logger$9.info("已生成默认 CLAUDE.md", { path: claudeMdPath });
405
+ logger$10.info("已生成默认 CLAUDE.md", { path: claudeMdPath });
405
406
  }
406
407
  }
407
408
  //#endregion
@@ -819,14 +820,16 @@ var CredentialVault = class {
819
820
  /**
820
821
  * per-user MCP Server 配置合并
821
822
  *
822
- * CC 进程启动时,将全局 MCP 和用户私有 MCP 合并后传入:
823
+ * CC 进程启动时,将全局 MCP、内置 MCP(larkpal)和用户私有 MCP 合并后传入:
823
824
  *
824
- * ~/.claude/mcp-servers.json ← 全局 MCP(lark-cli 等共享工具)
825
- * /workspace/users/{userId}/mcp-servers.json 用户私有 MCP
826
- * 合并(用户优先级高于全局,同名覆盖)
825
+ * 内置 larkpal MCP Server ← 必定注入(交互式 tool)
826
+ * ~/.claude/mcp-servers.json 全局 MCP(lark-cli 等共享工具)
827
+ * /workspace/users/{userId}/mcp-servers.json ← 用户私有 MCP
828
+ * ↓ 合并(用户优先级最高 > 全局 > 内置)
827
829
  * 临时文件 /tmp/mcp-{sessionId}.json → 通过 --mcp-config 传入 CC
828
830
  *
829
831
  * 效果:
832
+ * - 所有 CC 进程都有 larkpal MCP Server(ask_user、request_permission、signal_no_reply)
830
833
  * - A 用户的 CC 进程连接 A 的数据库 MCP Server
831
834
  * - B 用户的 CC 进程连接 B 的数据库 MCP Server
832
835
  * - 全局工具(lark-cli、文件操作等)所有用户共享
@@ -835,32 +838,44 @@ const log$30 = larkLogger("user/mcp-merge");
835
838
  /** 全局 MCP 配置路径 */
836
839
  const GLOBAL_MCP_CONFIG_PATH = path.join(os.homedir(), ".claude", "mcp-servers.json");
837
840
  /**
838
- * 合并全局和用户 MCP 配置,写入临时文件
841
+ * 获取 larkpal MCP Server 的可执行路径
839
842
  *
840
- * @returns 临时文件路径,用于 --mcp-config 参数;如果没有用户配置则返回 null
843
+ * 优先使用 dist/mcp-server.mjs(生产构建),回退到 tsx 直接运行源码(开发模式)
844
+ */
845
+ function getLarkpalMcpServerCommand() {
846
+ const distPath = path.resolve(import.meta.dirname ?? __dirname, "..", "dist", "mcp-server.mjs");
847
+ if (existsSync$1(distPath)) return {
848
+ command: "node",
849
+ args: [distPath]
850
+ };
851
+ const srcPath = path.resolve(import.meta.dirname ?? __dirname, "..", "src", "mcp-server", "index.ts");
852
+ if (existsSync$1(srcPath)) return {
853
+ command: "npx",
854
+ args: ["tsx", srcPath]
855
+ };
856
+ return {
857
+ command: "node",
858
+ args: [distPath]
859
+ };
860
+ }
861
+ /**
862
+ * 构建内置 larkpal MCP Server 配置
863
+ */
864
+ function getBuiltinLarkpalConfig() {
865
+ const { command, args } = getLarkpalMcpServerCommand();
866
+ return {
867
+ command,
868
+ args,
869
+ env: {}
870
+ };
871
+ }
872
+ /**
873
+ * 合并全局、内置和用户 MCP 配置,写入临时文件
874
+ *
875
+ * @returns 临时文件路径,用于 --mcp-config 参数(总是返回有效路径)
841
876
  */
842
877
  async function mergeAndWriteMcpConfig(userCtx, sessionId) {
843
- const userMcpPath = path.join(userCtx.credentialDir, "mcp-servers.json");
844
- let userConfig = {};
845
- if (existsSync$1(userMcpPath)) try {
846
- const raw = await readFile$1(userMcpPath, "utf-8");
847
- userConfig = JSON.parse(raw);
848
- log$30.info("用户 MCP 配置加载完成", {
849
- userId: userCtx.userId,
850
- serverCount: Object.keys(userConfig).length,
851
- servers: Object.keys(userConfig)
852
- });
853
- } catch (err) {
854
- log$30.warn("用户 MCP 配置文件解析失败", {
855
- userId: userCtx.userId,
856
- path: userMcpPath,
857
- error: err instanceof Error ? err.message : String(err)
858
- });
859
- }
860
- if (Object.keys(userConfig).length === 0) {
861
- log$30.info("用户无自定义 MCP 配置,使用全局默认", { userId: userCtx.userId });
862
- return null;
863
- }
878
+ const builtinConfig = { larkpal: getBuiltinLarkpalConfig() };
864
879
  let globalConfig = {};
865
880
  if (existsSync$1(GLOBAL_MCP_CONFIG_PATH)) try {
866
881
  const raw = await readFile$1(GLOBAL_MCP_CONFIG_PATH, "utf-8");
@@ -872,12 +887,33 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId) {
872
887
  error: err instanceof Error ? err.message : String(err)
873
888
  });
874
889
  }
890
+ let userConfig = {};
891
+ if (userCtx) {
892
+ const userMcpPath = path.join(userCtx.credentialDir, "mcp-servers.json");
893
+ if (existsSync$1(userMcpPath)) try {
894
+ const raw = await readFile$1(userMcpPath, "utf-8");
895
+ userConfig = JSON.parse(raw);
896
+ log$30.info("用户 MCP 配置加载完成", {
897
+ userId: userCtx.userId,
898
+ serverCount: Object.keys(userConfig).length,
899
+ servers: Object.keys(userConfig)
900
+ });
901
+ } catch (err) {
902
+ log$30.warn("用户 MCP 配置文件解析失败", {
903
+ userId: userCtx?.userId,
904
+ path: userMcpPath,
905
+ error: err instanceof Error ? err.message : String(err)
906
+ });
907
+ }
908
+ }
875
909
  const merged = {
910
+ ...builtinConfig,
876
911
  ...globalConfig,
877
912
  ...userConfig
878
913
  };
879
914
  log$30.info("MCP 配置合并完成", {
880
- userId: userCtx.userId,
915
+ userId: userCtx?.userId,
916
+ builtinServers: Object.keys(builtinConfig),
881
917
  globalServers: Object.keys(globalConfig),
882
918
  userServers: Object.keys(userConfig),
883
919
  mergedServers: Object.keys(merged)
@@ -1144,16 +1180,14 @@ var SessionProcessManager = class {
1144
1180
  if (config.maxBudgetUsd) args.push("--max-budget-usd", String(config.maxBudgetUsd));
1145
1181
  if (config.model) args.push("--model", config.model);
1146
1182
  args.push("--effort", CC_EFFORT);
1147
- if (config.userContext && !config.userContext.isTenantIdentity) try {
1148
- const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext, sessionId);
1149
- if (mcpConfigPath) {
1150
- args.push("--mcp-config", mcpConfigPath);
1151
- log$29.info("MCP 配置已合并并注入到 CC 进程", {
1152
- sessionId,
1153
- userId: config.userContext.userId,
1154
- mcpConfigPath
1155
- });
1156
- }
1183
+ try {
1184
+ const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId);
1185
+ args.push("--mcp-config", mcpConfigPath);
1186
+ log$29.info("MCP 配置已合并并注入到 CC 进程", {
1187
+ sessionId,
1188
+ userId: config.userContext?.userId,
1189
+ mcpConfigPath
1190
+ });
1157
1191
  } catch (err) {
1158
1192
  log$29.warn("MCP 配置合并失败,CC 进程将使用全局默认配置", {
1159
1193
  sessionId,
@@ -1162,6 +1196,7 @@ var SessionProcessManager = class {
1162
1196
  }
1163
1197
  const processEnv = { ...process.env };
1164
1198
  processEnv.CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING = "1";
1199
+ processEnv.LARKPAL_MCP_SESSION_ID = sessionId;
1165
1200
  if (config.userContext) {
1166
1201
  const { userContext } = config;
1167
1202
  processEnv.LARKPAL_USER_ID = userContext.userId;
@@ -1982,7 +2017,7 @@ async function createLarkpalAgentAdapter() {
1982
2017
  * 群聊 → /workspace/chats/group_{chat_id}/
1983
2018
  * 话题 → /workspace/chats/group_{chat_id}/topics/{thread_id}/
1984
2019
  */
1985
- const logger$8 = larkLogger("routing/session-router");
2020
+ const logger$9 = larkLogger("routing/session-router");
1986
2021
  const SESSION_OUTPUT_RULES = [
1987
2022
  `## 内容输出优先级`,
1988
2023
  "",
@@ -2021,7 +2056,7 @@ var SessionRouter = class {
2021
2056
  cwd: path.join(this.workspaceRoot, "chats", `p2p_${safeId}`),
2022
2057
  type: "p2p"
2023
2058
  };
2024
- logger$8.debug("解析私聊路由", {
2059
+ logger$9.debug("解析私聊路由", {
2025
2060
  chatId,
2026
2061
  userId: safeId,
2027
2062
  sessionId: route.sessionId,
@@ -2037,7 +2072,7 @@ var SessionRouter = class {
2037
2072
  cwd: path.join(groupCwd, "topics", threadId),
2038
2073
  type: "topic"
2039
2074
  };
2040
- logger$8.debug("解析话题路由", {
2075
+ logger$9.debug("解析话题路由", {
2041
2076
  chatId,
2042
2077
  threadId,
2043
2078
  sessionId: route.sessionId,
@@ -2050,7 +2085,7 @@ var SessionRouter = class {
2050
2085
  cwd: groupCwd,
2051
2086
  type: "group"
2052
2087
  };
2053
- logger$8.debug("解析群聊路由", {
2088
+ logger$9.debug("解析群聊路由", {
2054
2089
  chatId,
2055
2090
  sessionId: route.sessionId,
2056
2091
  cwd: route.cwd
@@ -2064,7 +2099,7 @@ var SessionRouter = class {
2064
2099
  * 此方法是幂等的,多次调用不会报错也不会覆盖已有文件。
2065
2100
  */
2066
2101
  async ensureSessionDirectory(route) {
2067
- logger$8.info("确保会话目录存在", {
2102
+ logger$9.info("确保会话目录存在", {
2068
2103
  sessionId: route.sessionId,
2069
2104
  cwd: route.cwd
2070
2105
  });
@@ -2074,7 +2109,7 @@ var SessionRouter = class {
2074
2109
  const claudeMdPath = path.join(route.cwd, "CLAUDE.md");
2075
2110
  if (!existsSync$1(claudeMdPath)) {
2076
2111
  await writeFile$1(claudeMdPath, this.generateClaudeMd(route), "utf-8");
2077
- logger$8.info("创建会话级 CLAUDE.md", {
2112
+ logger$9.info("创建会话级 CLAUDE.md", {
2078
2113
  path: claudeMdPath,
2079
2114
  type: route.type
2080
2115
  });
@@ -2138,7 +2173,7 @@ var SessionRouter = class {
2138
2173
  *
2139
2174
  * 内部通过 taskStore 维护所有任务的生命周期状态。
2140
2175
  */
2141
- const logger$7 = larkLogger("gateway/execute");
2176
+ const logger$8 = larkLogger("gateway/execute");
2142
2177
  /** 全局任务存储:taskId → TaskInfo */
2143
2178
  const taskStore = /* @__PURE__ */ new Map();
2144
2179
  /**
@@ -2172,7 +2207,7 @@ function createExecuteRouter(processManager) {
2172
2207
  */
2173
2208
  async function handleExecute(req, res, processManager) {
2174
2209
  const body = req.body;
2175
- logger$7.info("收到执行请求", {
2210
+ logger$8.info("收到执行请求", {
2176
2211
  session_id: body.session_id,
2177
2212
  cwd: body.cwd,
2178
2213
  prompt: body.prompt?.slice(0, 200),
@@ -2182,7 +2217,7 @@ async function handleExecute(req, res, processManager) {
2182
2217
  has_llm_config: !!body.llm_config
2183
2218
  });
2184
2219
  if (!body.session_id || !body.cwd || !body.prompt) {
2185
- logger$7.warn("执行请求参数缺失", {
2220
+ logger$8.warn("执行请求参数缺失", {
2186
2221
  hasSessionId: !!body.session_id,
2187
2222
  hasCwd: !!body.cwd,
2188
2223
  hasPrompt: !!body.prompt
@@ -2202,7 +2237,7 @@ async function handleExecute(req, res, processManager) {
2202
2237
  createdAt: /* @__PURE__ */ new Date()
2203
2238
  };
2204
2239
  taskStore.set(taskId, taskInfo);
2205
- logger$7.info("任务已创建", {
2240
+ logger$8.info("任务已创建", {
2206
2241
  taskId,
2207
2242
  sessionId: body.session_id,
2208
2243
  mode
@@ -2233,7 +2268,7 @@ async function handleExecute(req, res, processManager) {
2233
2268
  const callbacks = createTaskCallbacks(taskId);
2234
2269
  processManager.executePrompt(config, callbacks).catch((err) => {
2235
2270
  const errorMsg = err instanceof Error ? err.message : String(err);
2236
- logger$7.error("异步任务执行异常", {
2271
+ logger$8.error("异步任务执行异常", {
2237
2272
  taskId,
2238
2273
  error: errorMsg
2239
2274
  });
@@ -2241,7 +2276,7 @@ async function handleExecute(req, res, processManager) {
2241
2276
  taskInfo.error = errorMsg;
2242
2277
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2243
2278
  });
2244
- logger$7.info("异步任务已启动,立即返回", { taskId });
2279
+ logger$8.info("异步任务已启动,立即返回", { taskId });
2245
2280
  res.status(202).json({
2246
2281
  task_id: taskId,
2247
2282
  status: "running"
@@ -2250,7 +2285,7 @@ async function handleExecute(req, res, processManager) {
2250
2285
  }
2251
2286
  try {
2252
2287
  const result = await executeAndWaitResult$1(taskId, config, processManager);
2253
- logger$7.info("同步任务执行完成", {
2288
+ logger$8.info("同步任务执行完成", {
2254
2289
  taskId,
2255
2290
  resultSubtype: result?.subtype
2256
2291
  });
@@ -2261,7 +2296,7 @@ async function handleExecute(req, res, processManager) {
2261
2296
  });
2262
2297
  } catch (err) {
2263
2298
  const errorMsg = err instanceof Error ? err.message : String(err);
2264
- logger$7.error("同步任务执行失败", {
2299
+ logger$8.error("同步任务执行失败", {
2265
2300
  taskId,
2266
2301
  error: errorMsg
2267
2302
  });
@@ -2279,12 +2314,12 @@ async function handleExecute(req, res, processManager) {
2279
2314
  */
2280
2315
  async function handleBatchExecute(req, res, processManager) {
2281
2316
  const body = req.body;
2282
- logger$7.info("收到批量执行请求", {
2317
+ logger$8.info("收到批量执行请求", {
2283
2318
  taskCount: body.tasks?.length,
2284
2319
  concurrency: body.concurrency
2285
2320
  });
2286
2321
  if (!body.tasks || !Array.isArray(body.tasks) || body.tasks.length === 0) {
2287
- logger$7.warn("批量执行请求参数缺失: tasks 为空或格式错误");
2322
+ logger$8.warn("批量执行请求参数缺失: tasks 为空或格式错误");
2288
2323
  res.status(400).json({
2289
2324
  error: "Bad Request",
2290
2325
  message: "Missing or empty required field: tasks"
@@ -2294,7 +2329,7 @@ async function handleBatchExecute(req, res, processManager) {
2294
2329
  for (let i = 0; i < body.tasks.length; i++) {
2295
2330
  const task = body.tasks[i];
2296
2331
  if (!task?.session_id || !task?.cwd || !task?.prompt) {
2297
- logger$7.warn("批量执行子任务参数缺失", { index: i });
2332
+ logger$8.warn("批量执行子任务参数缺失", { index: i });
2298
2333
  res.status(400).json({
2299
2334
  error: "Bad Request",
2300
2335
  message: `Task at index ${i} is missing required fields: session_id, cwd, prompt`
@@ -2325,7 +2360,7 @@ async function handleBatchExecute(req, res, processManager) {
2325
2360
  }
2326
2361
  };
2327
2362
  });
2328
- logger$7.info("批量任务已创建", {
2363
+ logger$8.info("批量任务已创建", {
2329
2364
  batchId,
2330
2365
  taskIds,
2331
2366
  concurrency
@@ -2342,17 +2377,17 @@ async function handleBatchExecute(req, res, processManager) {
2342
2377
  */
2343
2378
  function handleGetTask(req, res) {
2344
2379
  const taskId = req.params.taskId;
2345
- logger$7.info("查询任务状态", { taskId });
2380
+ logger$8.info("查询任务状态", { taskId });
2346
2381
  const taskInfo = taskStore.get(taskId);
2347
2382
  if (!taskInfo) {
2348
- logger$7.warn("任务不存在", { taskId });
2383
+ logger$8.warn("任务不存在", { taskId });
2349
2384
  res.status(404).json({
2350
2385
  error: "Not Found",
2351
2386
  message: `Task ${taskId} not found`
2352
2387
  });
2353
2388
  return;
2354
2389
  }
2355
- logger$7.info("返回任务状态", {
2390
+ logger$8.info("返回任务状态", {
2356
2391
  taskId,
2357
2392
  status: taskInfo.status
2358
2393
  });
@@ -2369,10 +2404,10 @@ function handleGetTask(req, res) {
2369
2404
  */
2370
2405
  async function handleCancelTask(req, res, processManager) {
2371
2406
  const taskId = req.params.taskId;
2372
- logger$7.info("收到取消任务请求", { taskId });
2407
+ logger$8.info("收到取消任务请求", { taskId });
2373
2408
  const taskInfo = taskStore.get(taskId);
2374
2409
  if (!taskInfo) {
2375
- logger$7.warn("取消失败:任务不存在", { taskId });
2410
+ logger$8.warn("取消失败:任务不存在", { taskId });
2376
2411
  res.status(404).json({
2377
2412
  error: "Not Found",
2378
2413
  message: `Task ${taskId} not found`
@@ -2380,7 +2415,7 @@ async function handleCancelTask(req, res, processManager) {
2380
2415
  return;
2381
2416
  }
2382
2417
  if (taskInfo.status !== "running") {
2383
- logger$7.warn("取消失败:任务不在运行状态", {
2418
+ logger$8.warn("取消失败:任务不在运行状态", {
2384
2419
  taskId,
2385
2420
  status: taskInfo.status
2386
2421
  });
@@ -2394,7 +2429,7 @@ async function handleCancelTask(req, res, processManager) {
2394
2429
  await processManager.stopProcess(taskInfo.sessionId);
2395
2430
  taskInfo.status = "cancelled";
2396
2431
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2397
- logger$7.info("任务已取消", {
2432
+ logger$8.info("任务已取消", {
2398
2433
  taskId,
2399
2434
  sessionId: taskInfo.sessionId
2400
2435
  });
@@ -2404,7 +2439,7 @@ async function handleCancelTask(req, res, processManager) {
2404
2439
  });
2405
2440
  } catch (err) {
2406
2441
  const errorMsg = err instanceof Error ? err.message : String(err);
2407
- logger$7.error("取消任务时发生错误", {
2442
+ logger$8.error("取消任务时发生错误", {
2408
2443
  taskId,
2409
2444
  error: errorMsg
2410
2445
  });
@@ -2427,7 +2462,7 @@ function createTaskCallbacks(taskId) {
2427
2462
  taskInfo.result = result;
2428
2463
  taskInfo.status = result.isError ? "failed" : "completed";
2429
2464
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2430
- logger$7.info("任务收到结果", {
2465
+ logger$8.info("任务收到结果", {
2431
2466
  taskId,
2432
2467
  status: taskInfo.status,
2433
2468
  subtype: result.subtype,
@@ -2441,7 +2476,7 @@ function createTaskCallbacks(taskId) {
2441
2476
  taskInfo.status = "failed";
2442
2477
  taskInfo.error = error.message;
2443
2478
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2444
- logger$7.error("任务执行出错", {
2479
+ logger$8.error("任务执行出错", {
2445
2480
  taskId,
2446
2481
  error: error.message
2447
2482
  });
@@ -2463,7 +2498,7 @@ function executeAndWaitResult$1(taskId, config, processManager) {
2463
2498
  taskInfo.status = result.isError ? "failed" : "completed";
2464
2499
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2465
2500
  }
2466
- logger$7.info("同步任务收到结果", {
2501
+ logger$8.info("同步任务收到结果", {
2467
2502
  taskId,
2468
2503
  subtype: result.subtype,
2469
2504
  durationMs: result.durationMs,
@@ -2478,7 +2513,7 @@ function executeAndWaitResult$1(taskId, config, processManager) {
2478
2513
  taskInfo.error = error.message;
2479
2514
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2480
2515
  }
2481
- logger$7.error("同步任务执行出错", {
2516
+ logger$8.error("同步任务执行出错", {
2482
2517
  taskId,
2483
2518
  error: error.message
2484
2519
  });
@@ -2486,7 +2521,7 @@ function executeAndWaitResult$1(taskId, config, processManager) {
2486
2521
  }
2487
2522
  }).catch((err) => {
2488
2523
  const errorMsg = err instanceof Error ? err.message : String(err);
2489
- logger$7.error("同步任务 executePrompt 调用失败", {
2524
+ logger$8.error("同步任务 executePrompt 调用失败", {
2490
2525
  taskId,
2491
2526
  error: errorMsg
2492
2527
  });
@@ -2506,7 +2541,7 @@ function executeAndWaitResult$1(taskId, config, processManager) {
2506
2541
  * 使用简单的信号量模式控制并发数,逐个启动任务直到所有任务完成。
2507
2542
  */
2508
2543
  async function runBatchTasks(taskConfigs, concurrency, processManager) {
2509
- logger$7.info("开始批量任务执行", {
2544
+ logger$8.info("开始批量任务执行", {
2510
2545
  totalTasks: taskConfigs.length,
2511
2546
  concurrency
2512
2547
  });
@@ -2518,7 +2553,7 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2518
2553
  await processManager.executePrompt(config, callbacks);
2519
2554
  } catch (err) {
2520
2555
  const errorMsg = err instanceof Error ? err.message : String(err);
2521
- logger$7.error("批量子任务执行异常", {
2556
+ logger$8.error("批量子任务执行异常", {
2522
2557
  taskId,
2523
2558
  error: errorMsg
2524
2559
  });
@@ -2535,7 +2570,7 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2535
2570
  if (executing.size >= concurrency) await Promise.race(executing);
2536
2571
  }
2537
2572
  await Promise.all(executing);
2538
- logger$7.info("批量任务全部完成", { totalTasks: taskConfigs.length });
2573
+ logger$8.info("批量任务全部完成", { totalTasks: taskConfigs.length });
2539
2574
  }
2540
2575
  //#endregion
2541
2576
  //#region src/gateway/agent-completion-handler.ts
@@ -2554,7 +2589,7 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2554
2589
  *
2555
2590
  * 鉴权:通过 X-Internal-Secret 头验证服务间调用身份
2556
2591
  */
2557
- const logger$6 = larkLogger("gateway/agent-completion");
2592
+ const logger$7 = larkLogger("gateway/agent-completion");
2558
2593
  /** 内部通信密钥,从环境变量读取 */
2559
2594
  const INTERNAL_SECRET = process.env.LARKPAL_API_SECRET || "dev-internal-secret";
2560
2595
  /** CC 执行时的工作目录(使用临时目录) */
@@ -2574,7 +2609,7 @@ function createAgentCompletionRouter(runtimeAdapter) {
2574
2609
  async function handleCompletion(req, res, runtimeAdapter) {
2575
2610
  const secret = req.headers["x-internal-secret"];
2576
2611
  if (secret !== INTERNAL_SECRET) {
2577
- logger$6.warn("Agent completion 鉴权失败", { providedSecret: secret?.slice(0, 8) + "..." });
2612
+ logger$7.warn("Agent completion 鉴权失败", { providedSecret: secret?.slice(0, 8) + "..." });
2578
2613
  res.status(401).json({
2579
2614
  code: 1,
2580
2615
  message: "鉴权失败"
@@ -2584,7 +2619,7 @@ async function handleCompletion(req, res, runtimeAdapter) {
2584
2619
  const callbackUrl = req.headers["x-callback-url"];
2585
2620
  const body = req.body;
2586
2621
  if (!body.taskId || !body.prompt) {
2587
- logger$6.warn("Agent completion 参数不完整", {
2622
+ logger$7.warn("Agent completion 参数不完整", {
2588
2623
  hasTaskId: !!body.taskId,
2589
2624
  hasPrompt: !!body.prompt
2590
2625
  });
@@ -2594,7 +2629,7 @@ async function handleCompletion(req, res, runtimeAdapter) {
2594
2629
  });
2595
2630
  return;
2596
2631
  }
2597
- logger$6.info("收到 Agent completion 请求", {
2632
+ logger$7.info("收到 Agent completion 请求", {
2598
2633
  taskId: body.taskId,
2599
2634
  task: body.task || "unknown",
2600
2635
  promptLength: body.prompt.length,
@@ -2612,7 +2647,7 @@ async function handleCompletion(req, res, runtimeAdapter) {
2612
2647
  const filePath = join(taskDir, file.name);
2613
2648
  const fileBuffer = Buffer.from(file.contentBase64, "base64");
2614
2649
  await writeFile(filePath, fileBuffer);
2615
- logger$6.info("附件文件已写入", {
2650
+ logger$7.info("附件文件已写入", {
2616
2651
  filePath,
2617
2652
  sizeBytes: fileBuffer.length
2618
2653
  });
@@ -2630,13 +2665,13 @@ async function handleCompletion(req, res, runtimeAdapter) {
2630
2665
  const { readFile } = await import("node:fs/promises");
2631
2666
  const resultFilePath = join(taskDir, resultFileName);
2632
2667
  const fileContent = await readFile(resultFilePath, "utf-8");
2633
- logger$6.info("从结果文件读取输出", {
2668
+ logger$7.info("从结果文件读取输出", {
2634
2669
  resultFilePath,
2635
2670
  contentLength: fileContent.length
2636
2671
  });
2637
2672
  finalResult = fileContent;
2638
2673
  } catch {
2639
- logger$6.info("结果文件不存在,使用 CC 文本输出");
2674
+ logger$7.info("结果文件不存在,使用 CC 文本输出");
2640
2675
  }
2641
2676
  try {
2642
2677
  const { rm } = await import("node:fs/promises");
@@ -2645,7 +2680,7 @@ async function handleCompletion(req, res, runtimeAdapter) {
2645
2680
  force: true
2646
2681
  });
2647
2682
  } catch {}
2648
- logger$6.info("Agent completion 完成", {
2683
+ logger$7.info("Agent completion 完成", {
2649
2684
  taskId: body.taskId,
2650
2685
  resultLength: finalResult.length
2651
2686
  });
@@ -2664,7 +2699,7 @@ async function handleCompletion(req, res, runtimeAdapter) {
2664
2699
  });
2665
2700
  } catch (err) {
2666
2701
  const errorMsg = err instanceof Error ? err.message : String(err);
2667
- logger$6.error("Agent completion 执行失败", {
2702
+ logger$7.error("Agent completion 执行失败", {
2668
2703
  taskId: body.taskId,
2669
2704
  error: errorMsg
2670
2705
  });
@@ -2692,7 +2727,7 @@ async function executeAndWaitResult(runtimeAdapter, config) {
2692
2727
  resultText += text;
2693
2728
  },
2694
2729
  onResult(result) {
2695
- logger$6.info("CC 执行返回结果", {
2730
+ logger$7.info("CC 执行返回结果", {
2696
2731
  sessionId: config.sessionId,
2697
2732
  subtype: result.subtype,
2698
2733
  resultLength: resultText.length
@@ -2700,7 +2735,7 @@ async function executeAndWaitResult(runtimeAdapter, config) {
2700
2735
  resolve(resultText || result.result || "");
2701
2736
  },
2702
2737
  onError(error) {
2703
- logger$6.error("CC 执行出错", {
2738
+ logger$7.error("CC 执行出错", {
2704
2739
  sessionId: config.sessionId,
2705
2740
  error: error.message
2706
2741
  });
@@ -2714,7 +2749,7 @@ async function executeAndWaitResult(runtimeAdapter, config) {
2714
2749
  */
2715
2750
  async function sendCallback(callbackUrl, taskId, status, result, error) {
2716
2751
  try {
2717
- logger$6.info("发送回调通知", {
2752
+ logger$7.info("发送回调通知", {
2718
2753
  callbackUrl,
2719
2754
  taskId,
2720
2755
  status,
@@ -2735,18 +2770,18 @@ async function sendCallback(callbackUrl, taskId, status, result, error) {
2735
2770
  });
2736
2771
  if (!response.ok) {
2737
2772
  const errText = await response.text();
2738
- logger$6.warn("回调通知失败", {
2773
+ logger$7.warn("回调通知失败", {
2739
2774
  callbackUrl,
2740
2775
  status: response.status,
2741
2776
  body: errText
2742
2777
  });
2743
- } else logger$6.info("回调通知成功", {
2778
+ } else logger$7.info("回调通知成功", {
2744
2779
  callbackUrl,
2745
2780
  taskId
2746
2781
  });
2747
2782
  } catch (err) {
2748
2783
  const errorMsg = err instanceof Error ? err.message : String(err);
2749
- logger$6.error("回调通知异常", {
2784
+ logger$7.error("回调通知异常", {
2750
2785
  callbackUrl,
2751
2786
  taskId,
2752
2787
  error: errorMsg
@@ -2764,7 +2799,7 @@ async function sendCallback(callbackUrl, taskId, status, result, error) {
2764
2799
  *
2765
2800
  * 提供 CRUD 接口来管理技能文件。
2766
2801
  */
2767
- const logger$5 = larkLogger("gateway/skills");
2802
+ const logger$6 = larkLogger("gateway/skills");
2768
2803
  /** 全局技能目录 */
2769
2804
  const GLOBAL_COMMANDS_DIR = join(homedir(), ".claude", "commands");
2770
2805
  /** 会话级技能基础目录 */
@@ -2807,7 +2842,7 @@ async function scanSkills(dir, scope) {
2807
2842
  });
2808
2843
  }
2809
2844
  } catch (err) {
2810
- if (err.code !== "ENOENT") logger$5.warn("failed to scan skills directory", {
2845
+ if (err.code !== "ENOENT") logger$6.warn("failed to scan skills directory", {
2811
2846
  dir,
2812
2847
  error: String(err)
2813
2848
  });
@@ -2820,16 +2855,16 @@ async function scanSkills(dir, scope) {
2820
2855
  function createSkillsRouter() {
2821
2856
  const router = express.Router();
2822
2857
  router.get("/", async (req, res) => {
2823
- logger$5.info("list skills request", {
2858
+ logger$6.info("list skills request", {
2824
2859
  method: req.method,
2825
2860
  url: req.originalUrl
2826
2861
  });
2827
2862
  try {
2828
2863
  const skills = await scanSkills(GLOBAL_COMMANDS_DIR, "global");
2829
- logger$5.info("list skills response", { skillCount: skills.length });
2864
+ logger$6.info("list skills response", { skillCount: skills.length });
2830
2865
  res.json({ skills });
2831
2866
  } catch (err) {
2832
- logger$5.error("list skills failed", { error: String(err) });
2867
+ logger$6.error("list skills failed", { error: String(err) });
2833
2868
  res.status(500).json({
2834
2869
  error: "Failed to list skills",
2835
2870
  detail: String(err)
@@ -2838,7 +2873,7 @@ function createSkillsRouter() {
2838
2873
  });
2839
2874
  router.post("/", async (req, res) => {
2840
2875
  const { name, content, scope, session_dir } = req.body;
2841
- logger$5.info("create skill request", {
2876
+ logger$6.info("create skill request", {
2842
2877
  method: req.method,
2843
2878
  url: req.originalUrl,
2844
2879
  body: {
@@ -2849,7 +2884,7 @@ function createSkillsRouter() {
2849
2884
  }
2850
2885
  });
2851
2886
  if (!name || !content || !scope) {
2852
- logger$5.warn("create skill bad request", {
2887
+ logger$6.warn("create skill bad request", {
2853
2888
  name,
2854
2889
  scope
2855
2890
  });
@@ -2857,12 +2892,12 @@ function createSkillsRouter() {
2857
2892
  return;
2858
2893
  }
2859
2894
  if (scope !== "global" && scope !== "session") {
2860
- logger$5.warn("create skill invalid scope", { scope });
2895
+ logger$6.warn("create skill invalid scope", { scope });
2861
2896
  res.status(400).json({ error: "Invalid scope, must be \"global\" or \"session\"" });
2862
2897
  return;
2863
2898
  }
2864
2899
  if (scope === "session" && !session_dir) {
2865
- logger$5.warn("create skill missing session_dir for session scope");
2900
+ logger$6.warn("create skill missing session_dir for session scope");
2866
2901
  res.status(400).json({ error: "session_dir is required for session scope" });
2867
2902
  return;
2868
2903
  }
@@ -2872,7 +2907,7 @@ function createSkillsRouter() {
2872
2907
  const filePath = join(targetDir, `${name}.md`);
2873
2908
  await writeFile(filePath, content, "utf-8");
2874
2909
  const id = encodeId(filePath);
2875
- logger$5.info("create skill response", {
2910
+ logger$6.info("create skill response", {
2876
2911
  id,
2877
2912
  name,
2878
2913
  scope,
@@ -2885,7 +2920,7 @@ function createSkillsRouter() {
2885
2920
  path: filePath
2886
2921
  });
2887
2922
  } catch (err) {
2888
- logger$5.error("create skill failed", { error: String(err) });
2923
+ logger$6.error("create skill failed", { error: String(err) });
2889
2924
  res.status(500).json({
2890
2925
  error: "Failed to create skill",
2891
2926
  detail: String(err)
@@ -2895,14 +2930,14 @@ function createSkillsRouter() {
2895
2930
  router.put("/:id", async (req, res) => {
2896
2931
  const id = String(req.params.id);
2897
2932
  const { content } = req.body;
2898
- logger$5.info("update skill request", {
2933
+ logger$6.info("update skill request", {
2899
2934
  method: req.method,
2900
2935
  url: req.originalUrl,
2901
2936
  id,
2902
2937
  contentLength: content?.length
2903
2938
  });
2904
2939
  if (!content) {
2905
- logger$5.warn("update skill bad request: missing content", { id });
2940
+ logger$6.warn("update skill bad request: missing content", { id });
2906
2941
  res.status(400).json({ error: "Missing required field: content" });
2907
2942
  return;
2908
2943
  }
@@ -2910,7 +2945,7 @@ function createSkillsRouter() {
2910
2945
  const filePath = decodeId(id);
2911
2946
  await stat(filePath);
2912
2947
  await writeFile(filePath, content, "utf-8");
2913
- logger$5.info("update skill response", {
2948
+ logger$6.info("update skill response", {
2914
2949
  id,
2915
2950
  updated: true,
2916
2951
  filePath
@@ -2921,11 +2956,11 @@ function createSkillsRouter() {
2921
2956
  });
2922
2957
  } catch (err) {
2923
2958
  if (err.code === "ENOENT") {
2924
- logger$5.warn("update skill not found", { id });
2959
+ logger$6.warn("update skill not found", { id });
2925
2960
  res.status(404).json({ error: "Skill not found" });
2926
2961
  return;
2927
2962
  }
2928
- logger$5.error("update skill failed", {
2963
+ logger$6.error("update skill failed", {
2929
2964
  id,
2930
2965
  error: String(err)
2931
2966
  });
@@ -2937,7 +2972,7 @@ function createSkillsRouter() {
2937
2972
  });
2938
2973
  router.delete("/:id", async (req, res) => {
2939
2974
  const id = String(req.params.id);
2940
- logger$5.info("delete skill request", {
2975
+ logger$6.info("delete skill request", {
2941
2976
  method: req.method,
2942
2977
  url: req.originalUrl,
2943
2978
  id
@@ -2946,7 +2981,7 @@ function createSkillsRouter() {
2946
2981
  const filePath = decodeId(id);
2947
2982
  await stat(filePath);
2948
2983
  await unlink(filePath);
2949
- logger$5.info("delete skill response", {
2984
+ logger$6.info("delete skill response", {
2950
2985
  id,
2951
2986
  deleted: true,
2952
2987
  filePath
@@ -2957,11 +2992,11 @@ function createSkillsRouter() {
2957
2992
  });
2958
2993
  } catch (err) {
2959
2994
  if (err.code === "ENOENT") {
2960
- logger$5.warn("delete skill not found", { id });
2995
+ logger$6.warn("delete skill not found", { id });
2961
2996
  res.status(404).json({ error: "Skill not found" });
2962
2997
  return;
2963
2998
  }
2964
- logger$5.error("delete skill failed", {
2999
+ logger$6.error("delete skill failed", {
2965
3000
  id,
2966
3001
  error: String(err)
2967
3002
  });
@@ -2982,7 +3017,7 @@ function createSkillsRouter() {
2982
3017
  * - 会话目录: /workspace/chats/
2983
3018
  * - 人设文件: ~/.claude/CLAUDE.md
2984
3019
  */
2985
- const logger$4 = larkLogger("gateway/status");
3020
+ const logger$5 = larkLogger("gateway/status");
2986
3021
  /** 会话目录 */
2987
3022
  const SESSIONS_DIR = "/workspace/chats";
2988
3023
  /** 人设文件路径 */
@@ -2993,7 +3028,7 @@ const PERSONA_FILE = join(homedir(), ".claude", "CLAUDE.md");
2993
3028
  function createStatusRouter() {
2994
3029
  const router = express.Router();
2995
3030
  router.get("/sessions", async (req, res) => {
2996
- logger$4.info("list sessions request", {
3031
+ logger$5.info("list sessions request", {
2997
3032
  method: req.method,
2998
3033
  url: req.originalUrl
2999
3034
  });
@@ -3004,7 +3039,7 @@ function createStatusRouter() {
3004
3039
  entries = await readdir(SESSIONS_DIR);
3005
3040
  } catch (err) {
3006
3041
  if (err.code === "ENOENT") {
3007
- logger$4.info("list sessions response: sessions directory not found, returning empty", { dir: SESSIONS_DIR });
3042
+ logger$5.info("list sessions response: sessions directory not found, returning empty", { dir: SESSIONS_DIR });
3008
3043
  res.json({ sessions: [] });
3009
3044
  return;
3010
3045
  }
@@ -3022,10 +3057,10 @@ function createStatusRouter() {
3022
3057
  });
3023
3058
  } catch {}
3024
3059
  }
3025
- logger$4.info("list sessions response", { sessionCount: sessions.length });
3060
+ logger$5.info("list sessions response", { sessionCount: sessions.length });
3026
3061
  res.json({ sessions });
3027
3062
  } catch (err) {
3028
- logger$4.error("list sessions failed", { error: String(err) });
3063
+ logger$5.error("list sessions failed", { error: String(err) });
3029
3064
  res.status(500).json({
3030
3065
  error: "Failed to list sessions",
3031
3066
  detail: String(err)
@@ -3034,7 +3069,7 @@ function createStatusRouter() {
3034
3069
  });
3035
3070
  router.get("/sessions/:id", async (req, res) => {
3036
3071
  const id = String(req.params.id);
3037
- logger$4.info("get session detail request", {
3072
+ logger$5.info("get session detail request", {
3038
3073
  method: req.method,
3039
3074
  url: req.originalUrl,
3040
3075
  sessionId: id
@@ -3046,14 +3081,14 @@ function createStatusRouter() {
3046
3081
  s = await stat(sessionPath);
3047
3082
  } catch (err) {
3048
3083
  if (err.code === "ENOENT") {
3049
- logger$4.warn("get session detail: session not found", { id });
3084
+ logger$5.warn("get session detail: session not found", { id });
3050
3085
  res.status(404).json({ error: "Session not found" });
3051
3086
  return;
3052
3087
  }
3053
3088
  throw err;
3054
3089
  }
3055
3090
  if (!s.isDirectory()) {
3056
- logger$4.warn("get session detail: path is not a directory", { id });
3091
+ logger$5.warn("get session detail: path is not a directory", { id });
3057
3092
  res.status(404).json({ error: "Session not found" });
3058
3093
  return;
3059
3094
  }
@@ -3061,7 +3096,7 @@ function createStatusRouter() {
3061
3096
  const files = [];
3062
3097
  for (const entry of entries) files.push(entry);
3063
3098
  const lastModified = s.mtime.toISOString();
3064
- logger$4.info("get session detail response", {
3099
+ logger$5.info("get session detail response", {
3065
3100
  id,
3066
3101
  path: sessionPath,
3067
3102
  fileCount: files.length,
@@ -3074,7 +3109,7 @@ function createStatusRouter() {
3074
3109
  lastModified
3075
3110
  });
3076
3111
  } catch (err) {
3077
- logger$4.error("get session detail failed", {
3112
+ logger$5.error("get session detail failed", {
3078
3113
  id,
3079
3114
  error: String(err)
3080
3115
  });
@@ -3085,7 +3120,7 @@ function createStatusRouter() {
3085
3120
  }
3086
3121
  });
3087
3122
  router.get("/status", async (req, res) => {
3088
- logger$4.info("get status request", {
3123
+ logger$5.info("get status request", {
3089
3124
  method: req.method,
3090
3125
  url: req.originalUrl
3091
3126
  });
@@ -3097,13 +3132,13 @@ function createStatusRouter() {
3097
3132
  processes: [],
3098
3133
  memory
3099
3134
  };
3100
- logger$4.info("get status response", {
3135
+ logger$5.info("get status response", {
3101
3136
  uptime: uptimeSeconds,
3102
3137
  memoryRss: memory.rss
3103
3138
  });
3104
3139
  res.json(statusData);
3105
3140
  } catch (err) {
3106
- logger$4.error("get status failed", { error: String(err) });
3141
+ logger$5.error("get status failed", { error: String(err) });
3107
3142
  res.status(500).json({
3108
3143
  error: "Failed to get status",
3109
3144
  detail: String(err)
@@ -3111,7 +3146,7 @@ function createStatusRouter() {
3111
3146
  }
3112
3147
  });
3113
3148
  router.get("/persona", async (req, res) => {
3114
- logger$4.info("get persona request", {
3149
+ logger$5.info("get persona request", {
3115
3150
  method: req.method,
3116
3151
  url: req.originalUrl
3117
3152
  });
@@ -3121,16 +3156,16 @@ function createStatusRouter() {
3121
3156
  content = await readFile(PERSONA_FILE, "utf-8");
3122
3157
  } catch (err) {
3123
3158
  if (err.code === "ENOENT") {
3124
- logger$4.info("get persona: CLAUDE.md not found, returning empty");
3159
+ logger$5.info("get persona: CLAUDE.md not found, returning empty");
3125
3160
  res.json({ content: "" });
3126
3161
  return;
3127
3162
  }
3128
3163
  throw err;
3129
3164
  }
3130
- logger$4.info("get persona response", { contentLength: content.length });
3165
+ logger$5.info("get persona response", { contentLength: content.length });
3131
3166
  res.json({ content });
3132
3167
  } catch (err) {
3133
- logger$4.error("get persona failed", { error: String(err) });
3168
+ logger$5.error("get persona failed", { error: String(err) });
3134
3169
  res.status(500).json({
3135
3170
  error: "Failed to get persona",
3136
3171
  detail: String(err)
@@ -3138,7 +3173,7 @@ function createStatusRouter() {
3138
3173
  }
3139
3174
  });
3140
3175
  router.put("/persona", async (_req, res) => {
3141
- logger$4.warn("PUT /persona 已废弃,人设内容由飞书云文档统一管理,请使用 POST /api/sync-app-info 触发同步");
3176
+ logger$5.warn("PUT /persona 已废弃,人设内容由飞书云文档统一管理,请使用 POST /api/sync-app-info 触发同步");
3142
3177
  res.status(403).json({ error: "人设内容由飞书云文档统一管理,不支持直接修改。请在云文档中编辑后调用 POST /api/sync-app-info 同步。" });
3143
3178
  });
3144
3179
  return router;
@@ -3151,7 +3186,7 @@ function createStatusRouter() {
3151
3186
  * 接收来自外部系统的 hook 通知,记录详细日志。
3152
3187
  * 所有 hook 当前只做日志记录,不做复杂业务处理。
3153
3188
  */
3154
- const logger$3 = larkLogger("gateway/hooks");
3189
+ const logger$4 = larkLogger("gateway/hooks");
3155
3190
  /**
3156
3191
  * 创建 HTTP Hooks 路由
3157
3192
  */
@@ -3159,7 +3194,7 @@ function createHooksRouter() {
3159
3194
  const router = express.Router();
3160
3195
  router.post("/session-start", (req, res) => {
3161
3196
  const body = req.body;
3162
- logger$3.info("hook received: session-start", {
3197
+ logger$4.info("hook received: session-start", {
3163
3198
  method: req.method,
3164
3199
  url: req.originalUrl,
3165
3200
  sessionId: body.session_id,
@@ -3169,7 +3204,7 @@ function createHooksRouter() {
3169
3204
  });
3170
3205
  router.post("/stop", (req, res) => {
3171
3206
  const body = req.body;
3172
- logger$3.info("hook received: stop", {
3207
+ logger$4.info("hook received: stop", {
3173
3208
  method: req.method,
3174
3209
  url: req.originalUrl,
3175
3210
  body
@@ -3178,7 +3213,7 @@ function createHooksRouter() {
3178
3213
  });
3179
3214
  router.post("/session-end", (req, res) => {
3180
3215
  const body = req.body;
3181
- logger$3.info("hook received: session-end", {
3216
+ logger$4.info("hook received: session-end", {
3182
3217
  method: req.method,
3183
3218
  url: req.originalUrl,
3184
3219
  sessionId: body.session_id,
@@ -3188,7 +3223,7 @@ function createHooksRouter() {
3188
3223
  });
3189
3224
  router.post("/notification", (req, res) => {
3190
3225
  const body = req.body;
3191
- logger$3.info("hook received: notification", {
3226
+ logger$4.info("hook received: notification", {
3192
3227
  method: req.method,
3193
3228
  url: req.originalUrl,
3194
3229
  body
@@ -3429,6 +3464,178 @@ function createSchedulerRouter(taskManager) {
3429
3464
  return router;
3430
3465
  }
3431
3466
  //#endregion
3467
+ //#region src/gateway/mcp-callback-handler.ts
3468
+ /**
3469
+ * MCP Server 回调路由 Handler
3470
+ *
3471
+ * 接收 MCP Server(作为 CC tool provider)发起的回调请求,管理 pending requests 的生命周期。
3472
+ *
3473
+ * 架构:
3474
+ * MCP Server → POST /api/mcp/callback → 宿主层接收(存储 pending,发出事件)
3475
+ * 用户操作卡片 → 飞书回调 → PUT /api/mcp/resolve/:requestId → 将答案存入
3476
+ * MCP Server → GET /api/mcp/resolve/:requestId → 轮询获取用户回答
3477
+ */
3478
+ const logger$3 = larkLogger("gateway/mcp-callback");
3479
+ /** pending requests 内存 Map */
3480
+ const pendingRequests = /* @__PURE__ */ new Map();
3481
+ /** session 静默标记 Map */
3482
+ const sessionNoReplyFlags = /* @__PURE__ */ new Map();
3483
+ /** TTL: 30 分钟 */
3484
+ const TTL_MS = 1800 * 1e3;
3485
+ /** 定期清理过期的 pending requests(每 5 分钟检查一次) */
3486
+ setInterval(() => {
3487
+ const now = Date.now();
3488
+ let cleaned = 0;
3489
+ for (const [id, req] of pendingRequests) if (now - req.createdAt > TTL_MS) {
3490
+ pendingRequests.delete(id);
3491
+ cleaned++;
3492
+ }
3493
+ if (cleaned > 0) logger$3.info("TTL cleanup completed", {
3494
+ cleaned,
3495
+ remaining: pendingRequests.size
3496
+ });
3497
+ }, 300 * 1e3).unref();
3498
+ /**
3499
+ * 当收到新的 MCP callback 时发出事件。
3500
+ * 事件名: `callback:${sessionId}`,payload 为 PendingMcpRequest。
3501
+ * 用于 dispatch-cc 订阅并在收到 ask_user/request_permission 回调时发送飞书卡片。
3502
+ */
3503
+ const onMcpCallback = new EventEmitter$1();
3504
+ onMcpCallback.setMaxListeners(100);
3505
+ /**
3506
+ * 将 result 存入对应的 pending request
3507
+ * @returns true 表示成功 resolve,false 表示 requestId 不存在
3508
+ */
3509
+ function resolveMcpRequest(requestId, result) {
3510
+ const pending = pendingRequests.get(requestId);
3511
+ if (!pending) {
3512
+ logger$3.warn("resolveMcpRequest: requestId not found", { requestId });
3513
+ return false;
3514
+ }
3515
+ pending.result = result;
3516
+ logger$3.info("resolveMcpRequest: request resolved", {
3517
+ requestId,
3518
+ sessionId: pending.sessionId,
3519
+ tool: pending.tool,
3520
+ elapsed: Date.now() - pending.createdAt
3521
+ });
3522
+ return true;
3523
+ }
3524
+ /**
3525
+ * 清除 session 的静默标记(每个 turn 开始时调用)
3526
+ */
3527
+ function clearSessionNoReplyFlag(sessionId) {
3528
+ if (sessionNoReplyFlags.has(sessionId)) {
3529
+ sessionNoReplyFlags.delete(sessionId);
3530
+ logger$3.debug("clearSessionNoReplyFlag: cleared", { sessionId });
3531
+ }
3532
+ }
3533
+ /**
3534
+ * 创建 MCP Callback 路由
3535
+ */
3536
+ function createMcpCallbackRouter() {
3537
+ const router = express.Router();
3538
+ router.post("/callback", (req, res) => {
3539
+ const startTime = Date.now();
3540
+ const { requestId, tool, params, sessionId } = req.body;
3541
+ logger$3.info("callback received", {
3542
+ requestId,
3543
+ tool,
3544
+ sessionId,
3545
+ params
3546
+ });
3547
+ if (!requestId || !tool || !sessionId) {
3548
+ logger$3.warn("callback rejected: missing required fields", {
3549
+ requestId,
3550
+ tool,
3551
+ sessionId
3552
+ });
3553
+ res.status(400).json({ error: "Missing required fields: requestId, tool, sessionId" });
3554
+ return;
3555
+ }
3556
+ if (tool === "signal_no_reply") {
3557
+ sessionNoReplyFlags.set(sessionId, true);
3558
+ logger$3.info("signal_no_reply: session marked as no-reply", {
3559
+ requestId,
3560
+ sessionId,
3561
+ elapsed: Date.now() - startTime
3562
+ });
3563
+ res.json({ acknowledged: true });
3564
+ return;
3565
+ }
3566
+ const pending = {
3567
+ requestId,
3568
+ tool,
3569
+ params,
3570
+ sessionId,
3571
+ createdAt: Date.now()
3572
+ };
3573
+ pendingRequests.set(requestId, pending);
3574
+ logger$3.info("pending request created", {
3575
+ requestId,
3576
+ tool,
3577
+ sessionId,
3578
+ totalPending: pendingRequests.size,
3579
+ elapsed: Date.now() - startTime
3580
+ });
3581
+ onMcpCallback.emit(`callback:${sessionId}`, pending);
3582
+ res.json({
3583
+ status: "pending",
3584
+ requestId
3585
+ });
3586
+ });
3587
+ router.get("/resolve/:requestId", (req, res) => {
3588
+ const requestId = req.params.requestId;
3589
+ logger$3.debug("resolve poll", { requestId });
3590
+ const pending = pendingRequests.get(requestId);
3591
+ if (!pending) {
3592
+ logger$3.warn("resolve poll: requestId not found", { requestId });
3593
+ res.status(404).json({ status: "not_found" });
3594
+ return;
3595
+ }
3596
+ if (pending.result !== void 0) {
3597
+ const result = pending.result;
3598
+ pendingRequests.delete(requestId);
3599
+ logger$3.info("resolve poll: returning resolved result", {
3600
+ requestId,
3601
+ sessionId: pending.sessionId,
3602
+ tool: pending.tool,
3603
+ elapsed: Date.now() - pending.createdAt
3604
+ });
3605
+ res.json({
3606
+ status: "resolved",
3607
+ result
3608
+ });
3609
+ return;
3610
+ }
3611
+ logger$3.debug("resolve poll: still pending", {
3612
+ requestId,
3613
+ sessionId: pending.sessionId,
3614
+ waitingMs: Date.now() - pending.createdAt
3615
+ });
3616
+ res.json({ status: "pending" });
3617
+ });
3618
+ router.put("/resolve/:requestId", (req, res) => {
3619
+ const requestId = req.params.requestId;
3620
+ const body = req.body;
3621
+ logger$3.info("resolve put", {
3622
+ requestId,
3623
+ result: body.result
3624
+ });
3625
+ if (body.result === void 0) {
3626
+ logger$3.warn("resolve put rejected: missing result field", { requestId });
3627
+ res.status(400).json({ error: "Missing required field: result" });
3628
+ return;
3629
+ }
3630
+ if (!resolveMcpRequest(requestId, body.result)) {
3631
+ res.status(404).json({ error: "requestId not found" });
3632
+ return;
3633
+ }
3634
+ res.json({ success: true });
3635
+ });
3636
+ return router;
3637
+ }
3638
+ //#endregion
3432
3639
  //#region src/core/app-info-sync.ts
3433
3640
  /**
3434
3641
  * 应用信息同步
@@ -3740,53 +3947,34 @@ function getDefaultClaudeMdBody() {
3740
3947
  - 你的可用技能在 ~/.claude/commands/ 和当前目录的 .claude/commands/ 中
3741
3948
  - 使用 /help 查看所有可用技能
3742
3949
 
3743
- ## 交互式卡片(重要)
3744
-
3745
- 当你需要向用户发送交互式卡片时,在回复中嵌入以下标记。系统会自动检测并发送对应的飞书卡片。
3746
-
3747
- ### 权限申请卡片
3748
- 当 lark-cli 调用返回权限不足错误(如 99991672、scope missing)时,使用此标记引导用户申请权限:
3950
+ ## 交互式工具(重要)
3749
3951
 
3750
- [PERMISSION_REQUEST]
3751
- \`\`\`json
3752
- {"scopes": ["calendar:calendar", "vc:meeting.meetingevent:read"]}
3753
- \`\`\`
3952
+ 你的 MCP 工具列表中有以下 LarkPal 系统级工具,它们由宿主层自动注册,用于与用户进行结构化交互:
3754
3953
 
3755
- 系统会发送一张独立的权限申请卡片,包含"去申请"和"已完成"按钮。用户完成配置后会自动通知你重试。
3954
+ ### ask_user — 向用户提问
3955
+ 当你需要收集用户的多维信息(不确定且需要用户决策的场景),使用此工具。
3956
+ 工具会阻塞等待用户在飞书/Web 端回答后返回结构化答案。
3756
3957
 
3757
- ### 向用户提问卡片
3758
- 当你需要收集用户的多维信息时(不确定且需要用户决策的场景),使用此标记发送结构化提问卡片:
3958
+ 参数说明:
3959
+ - question: 主问题描述
3960
+ - phases: 提问阶段列表,每个阶段可包含选项(options)或允许自由文本(allowFreeText)
3759
3961
 
3760
- [ASK_USER]
3761
- \`\`\`json
3762
- {
3763
- "question": "为了帮你完成这个任务,需要确认以下信息:",
3764
- "phases": [
3765
- {
3766
- "id": "target",
3767
- "title": "目标选择",
3768
- "description": "你想在哪个环境操作?",
3769
- "options": [{"label": "测试环境", "value": "sit"}, {"label": "生产环境", "value": "prod"}],
3770
- "allowFreeText": true,
3771
- "required": true
3772
- },
3773
- {
3774
- "id": "scope",
3775
- "title": "操作范围",
3776
- "options": [{"label": "仅当前模块", "value": "current"}, {"label": "全量", "value": "all"}],
3777
- "required": false
3778
- }
3779
- ]
3780
- }
3781
- \`\`\`
3962
+ ### request_permission — 请求飞书 API 权限
3963
+ 当 lark-cli 调用返回权限不足错误(如 99991672、scope missing)时,使用此工具引导用户申请权限。
3964
+ 工具会阻塞等待用户在飞书端完成权限配置后返回结果。
3782
3965
 
3783
- 卡片会逐 phase 展示选项,用户可选择预设项或跳过。至少需要回答一个 phase。回答完成后系统会将结果注入对话继续。
3966
+ 参数说明:
3967
+ - scopes: 需要的权限列表(如 "contact:user.base:readonly")
3968
+ - reason: 申请原因说明
3784
3969
 
3785
- **注意**:标记文本会从最终展示给用户的回复中自动移除,用户只会看到卡片本身。
3970
+ ### signal_no_reply — 标记本次不回复
3971
+ 在 teammate 评估等场景中,如果你决定不参与当前对话,调用此工具标记后即可正常结束。
3972
+ 此工具即时返回,不阻塞。
3786
3973
 
3787
- ### 何时使用标记 vs lark-cli
3788
- - **需要用户回答、确认、选择** 的场景 → 必须使用 [ASK_USER][PERMISSION_REQUEST] 标记,系统会统一样式并处理回调
3974
+ ### 何时使用 MCP 工具 vs lark-cli
3975
+ - **需要用户回答、确认、选择** 的场景 → 必须使用 ask_userrequest_permission 工具,系统会统一发送飞书卡片并处理回调
3789
3976
  - **纯通知、展示信息** 的卡片(不需要用户交互反馈) → 可以用 lark-cli 自行发送
3977
+ - **决定静默不回复** → 使用 signal_no_reply 工具
3790
3978
  `;
3791
3979
  }
3792
3980
  /**
@@ -4408,6 +4596,7 @@ function registerRoutes(app, processManager, scheduledTaskManager, appCredential
4408
4596
  }
4409
4597
  app.use("/api", createStatusRouter());
4410
4598
  app.use("/hooks", createHooksRouter());
4599
+ app.use("/api/mcp", createMcpCallbackRouter());
4411
4600
  app.use(createChatAuthRouter());
4412
4601
  if (processManager && messageStore) app.use(createChatRouter({
4413
4602
  processManager,
@@ -5727,389 +5916,101 @@ var LarkClient = class LarkClient {
5727
5916
  };
5728
5917
  injectLarkClient(LarkClient);
5729
5918
  //#endregion
5730
- //#region src/card/interactive-cards.ts
5919
+ //#region src/card/reasoning-utils.ts
5731
5920
  /**
5732
- * 交互式卡片模块 授权卡片 & 提问卡片
5921
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
5922
+ * SPDX-License-Identifier: MIT
5733
5923
  *
5734
- * 基于"后处理检测 + 下轮注入"的异步模式:
5735
- * CC 无法暂停等待用户输入,因此通过卡片交互收集用户反馈后,
5736
- * 将结果作为新消息注入 CC 的下一轮对话。
5924
+ * Shared utilities for the reasoning display subsystem.
5925
+ */
5926
+ function normalizeToolName(name) {
5927
+ return name?.trim().toLowerCase() ?? "";
5928
+ }
5929
+ function truncateText(value, maxLength) {
5930
+ if (value.length <= maxLength) return value;
5931
+ return `${value.slice(0, maxLength - 3)}...`;
5932
+ }
5933
+ const INLINE_ASSIGNMENT_RE = /(^|[\s"'`])([A-Za-z_][A-Za-z0-9_]*)(=(?:"[^"]*"|'[^']*'|[^\s"'`]+))/g;
5934
+ const AUTH_HEADER_SECRET_RE = /(Authorization\s*:\s*(?:Bearer|Basic|Token)\s+)([^'"\s]+)/gi;
5935
+ const QUOTED_HEADER_ARG_RE = /((?:^|[\s"'`])(?:-H|--header)\s+)(['"])([A-Za-z0-9_-]+)(\s*:\s*)([^'"]*)(\2)/gi;
5936
+ const UNQUOTED_HEADER_ARG_RE = /((?:^|[\s"'`])(?:-H|--header)\s+)([A-Za-z0-9_-]+)(\s*:\s*)([^\s"'`]+)/gi;
5937
+ const SECRET_FLAG_RE = /((?:^|[\s"'`]))(--?[A-Za-z0-9][A-Za-z0-9-]*)(=|\s+)(?:"([^"]*)"|'([^']*)'|([^\s"'`]+))/g;
5938
+ const SENSITIVE_NAME_RE = /token|secret|password|api[_-]?key|authorization|cookie|credential|bearer|session[_-]?id|client[_-]?secret|access[_-]?key/i;
5939
+ function redactInlineSecrets(value) {
5940
+ return value.replace(INLINE_ASSIGNMENT_RE, (match, prefix, key) => isSensitiveName(key) ? `${prefix}${key}=[redacted]` : match).replace(AUTH_HEADER_SECRET_RE, "$1[redacted]").replace(QUOTED_HEADER_ARG_RE, (match, prefix, quote, name, separator) => shouldRedactHeaderValue(name) ? `${prefix}${quote}${name}${separator}[redacted]${quote}` : match).replace(UNQUOTED_HEADER_ARG_RE, (match, prefix, name, separator) => shouldRedactHeaderValue(name) ? `${prefix}${name}${separator}[redacted]` : match).replace(SECRET_FLAG_RE, (match, prefix, flag, separator, doubleQuoted, singleQuoted, bare) => {
5941
+ if (!isSensitiveName(flag.replace(/^-+/, ""))) return match;
5942
+ return `${prefix}${flag}${separator}${doubleQuoted !== void 0 ? "\"[redacted]\"" : singleQuoted !== void 0 ? "'[redacted]'" : bare !== void 0 ? "[redacted]" : "[redacted]"}`;
5943
+ });
5944
+ }
5945
+ function isSensitiveName(value) {
5946
+ return SENSITIVE_NAME_RE.test(value);
5947
+ }
5948
+ function shouldRedactHeaderValue(name) {
5949
+ return !/^authorization$/i.test(name) && isSensitiveName(name);
5950
+ }
5951
+ //#endregion
5952
+ //#region src/card/tool-use-trace-store.ts
5953
+ /**
5954
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
5955
+ * SPDX-License-Identifier: MIT
5737
5956
  *
5738
- * 包含:
5739
- * - 授权申请卡片(Permission Auth Card)
5740
- * - Phase 提问卡片(Ask Card)
5741
- * - CC 输出文本后处理(检测特殊标记)
5742
- * - 操作 ID 生命周期管理
5957
+ * Runtime store for structured tool-use steps.
5958
+ *
5959
+ * The Feishu card renderer reads from this store by session key so it can
5960
+ * render observable, replayable tool execution without relying purely on
5961
+ * reply payload text.
5743
5962
  */
5744
- const log$19 = larkLogger("card/interactive-cards");
5745
- const pendingOperations = /* @__PURE__ */ new Map();
5746
- const OPERATION_TTL_MS = 1800 * 1e3;
5747
- setInterval(() => {
5963
+ const TRACE_TTL_MS = 1800 * 1e3;
5964
+ const MAX_SESSION_TRACES = 128;
5965
+ const MAX_STEPS_PER_SESSION = 256;
5966
+ const STEP_RUNNING_TIMEOUT_MS = 300 * 1e3;
5967
+ const GENERIC_STRING_LIMIT = 512;
5968
+ const RESULT_STRING_LIMIT = 1024;
5969
+ const COMMAND_STRING_LIMIT = 4096;
5970
+ const PATH_STRING_LIMIT = 2048;
5971
+ const sessionTraces = /* @__PURE__ */ new Map();
5972
+ function startToolUseTraceRun(sessionKey) {
5973
+ if (!sessionKey) return;
5974
+ pruneTraceStore();
5975
+ sessionTraces.set(sessionKey, {
5976
+ nextSeq: 1,
5977
+ updatedAt: Date.now(),
5978
+ steps: [],
5979
+ currentRunId: void 0
5980
+ });
5981
+ }
5982
+ function clearToolUseTraceRun(sessionKey) {
5983
+ if (!sessionKey) return;
5984
+ sessionTraces.delete(sessionKey);
5985
+ }
5986
+ function recordToolUseStart(params) {
5987
+ const { sessionKey, toolName, toolParams, toolCallId, runId } = params;
5988
+ if (!sessionKey || !toolName) return;
5989
+ const state = sessionTraces.get(sessionKey);
5990
+ if (!state) return;
5991
+ if (runId) {
5992
+ if (state.currentRunId === void 0) state.currentRunId = runId;
5993
+ else if (state.currentRunId !== runId) return;
5994
+ }
5748
5995
  const now = Date.now();
5749
- for (const [id, op] of pendingOperations) if (now - op.createdAt > OPERATION_TTL_MS) pendingOperations.delete(id);
5750
- }, 6e4);
5996
+ if (state.steps.length >= MAX_STEPS_PER_SESSION) state.steps.splice(0, state.steps.length - MAX_STEPS_PER_SESSION + 1);
5997
+ state.steps.push({
5998
+ id: `${state.nextSeq}`,
5999
+ seq: state.nextSeq,
6000
+ toolName,
6001
+ toolCallId: toolCallId || void 0,
6002
+ runId: runId || void 0,
6003
+ params: sanitizeTraceValue(toolParams, 0, { source: "params" }),
6004
+ status: "running",
6005
+ startedAt: now
6006
+ });
6007
+ state.nextSeq += 1;
6008
+ state.updatedAt = now;
6009
+ }
5751
6010
  /**
5752
- * 构建授权申请卡片(CardKit v2 格式)
5753
- *
5754
- * 包含:
5755
- * - 缺失权限列表
5756
- * - "去申请" 按钮(跳转到开放平台)
5757
- * - "已完成" 按钮(回调通知宿主层)
5758
- */
5759
- function buildAuthCard(ctx) {
5760
- const operationId = randomUUID();
5761
- const openPlatformHost = ctx.brand === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn";
5762
- const scopeQuery = ctx.scopes.join(",");
5763
- const authUrl = `${openPlatformHost}/app/${ctx.appId}/auth?q=${encodeURIComponent(scopeQuery)}`;
5764
- const card = {
5765
- schema: "2.0",
5766
- config: {
5767
- wide_screen_mode: true,
5768
- update_multi: true
5769
- },
5770
- header: {
5771
- template: "orange",
5772
- title: {
5773
- tag: "plain_text",
5774
- content: "🔐 需要申请权限才能继续"
5775
- }
5776
- },
5777
- body: { elements: [
5778
- {
5779
- tag: "markdown",
5780
- content: `当前操作需要以下飞书权限,请应用管理员前往开放平台申请:\n\n${ctx.scopes.map((s) => `• \`${s}\``).join("\n")}`
5781
- },
5782
- { tag: "hr" },
5783
- {
5784
- tag: "markdown",
5785
- content: "**第一步:** 前往开放平台申请上述权限并发布版本"
5786
- },
5787
- {
5788
- tag: "action",
5789
- actions: [{
5790
- tag: "button",
5791
- text: {
5792
- tag: "plain_text",
5793
- content: "去申请权限 ↗"
5794
- },
5795
- type: "primary",
5796
- multi_url: {
5797
- url: authUrl,
5798
- pc_url: authUrl,
5799
- android_url: authUrl,
5800
- ios_url: authUrl
5801
- }
5802
- }]
5803
- },
5804
- {
5805
- tag: "markdown",
5806
- content: "**第二步:** 权限申请通过后,点击下方按钮通知我继续"
5807
- },
5808
- {
5809
- tag: "action",
5810
- actions: [{
5811
- tag: "button",
5812
- text: {
5813
- tag: "plain_text",
5814
- content: "✓ 已完成权限配置"
5815
- },
5816
- type: "default",
5817
- behaviors: [{
5818
- type: "callback",
5819
- value: {
5820
- action: "auth_complete",
5821
- operationId
5822
- }
5823
- }]
5824
- }]
5825
- }
5826
- ] }
5827
- };
5828
- pendingOperations.set(operationId, {
5829
- type: "auth",
5830
- operationId,
5831
- chatId: ctx.chatId,
5832
- sessionId: ctx.sessionId,
5833
- createdAt: Date.now(),
5834
- authContext: {
5835
- appId: ctx.appId,
5836
- scopes: ctx.scopes
5837
- }
5838
- });
5839
- log$19.info("已创建授权卡片操作", {
5840
- operationId,
5841
- scopes: ctx.scopes,
5842
- sessionId: ctx.sessionId
5843
- });
5844
- return {
5845
- card,
5846
- operationId
5847
- };
5848
- }
5849
- /**
5850
- * 构建多 phase 提问卡片(CardKit v2 格式)
5851
- *
5852
- * 每次显示一个 phase:
5853
- * - 有预设选项时显示按钮组
5854
- * - 允许自由文本时显示输入提示
5855
- * - 底部有"跳过"和"提交"按钮
5856
- */
5857
- function buildAskCard(ctx, phaseIndex = 0) {
5858
- const operationId = randomUUID();
5859
- const phase = ctx.phases[phaseIndex];
5860
- const totalPhases = ctx.phases.length;
5861
- const elements = [];
5862
- elements.push({
5863
- tag: "markdown",
5864
- content: ctx.question
5865
- });
5866
- if (phase.description) elements.push({
5867
- tag: "markdown",
5868
- content: phase.description
5869
- });
5870
- elements.push({ tag: "hr" });
5871
- elements.push({
5872
- tag: "markdown",
5873
- content: `**${phase.title}** (${phaseIndex + 1}/${totalPhases})`
5874
- });
5875
- if (phase.options && phase.options.length > 0) {
5876
- const optionButtons = phase.options.map((opt) => ({
5877
- tag: "button",
5878
- text: {
5879
- tag: "plain_text",
5880
- content: opt.label
5881
- },
5882
- type: "default",
5883
- behaviors: [{
5884
- type: "callback",
5885
- value: {
5886
- action: "ask_select",
5887
- operationId,
5888
- phaseId: phase.id,
5889
- value: opt.value
5890
- }
5891
- }]
5892
- }));
5893
- elements.push({
5894
- tag: "action",
5895
- actions: optionButtons
5896
- });
5897
- }
5898
- if (phase.allowFreeText !== false) elements.push({
5899
- tag: "markdown",
5900
- content: "_💡 你也可以直接回复消息补充更多信息_"
5901
- });
5902
- const bottomActions = [];
5903
- if (totalPhases > 1 && phaseIndex < totalPhases - 1) bottomActions.push({
5904
- tag: "button",
5905
- text: {
5906
- tag: "plain_text",
5907
- content: "跳过 →"
5908
- },
5909
- type: "default",
5910
- behaviors: [{
5911
- type: "callback",
5912
- value: {
5913
- action: "ask_skip",
5914
- operationId,
5915
- phaseId: phase.id
5916
- }
5917
- }]
5918
- });
5919
- bottomActions.push({
5920
- tag: "button",
5921
- text: {
5922
- tag: "plain_text",
5923
- content: "✓ 完成提交"
5924
- },
5925
- type: "primary",
5926
- behaviors: [{
5927
- type: "callback",
5928
- value: {
5929
- action: "ask_submit",
5930
- operationId
5931
- }
5932
- }]
5933
- });
5934
- if (bottomActions.length > 0) {
5935
- elements.push({ tag: "hr" });
5936
- elements.push({
5937
- tag: "action",
5938
- actions: bottomActions
5939
- });
5940
- }
5941
- const card = {
5942
- schema: "2.0",
5943
- config: {
5944
- wide_screen_mode: true,
5945
- update_multi: true
5946
- },
5947
- header: {
5948
- template: "blue",
5949
- title: {
5950
- tag: "plain_text",
5951
- content: "💬 需要补充信息"
5952
- }
5953
- },
5954
- body: { elements }
5955
- };
5956
- pendingOperations.set(operationId, {
5957
- type: "ask",
5958
- operationId,
5959
- chatId: ctx.chatId,
5960
- sessionId: ctx.sessionId,
5961
- createdAt: Date.now(),
5962
- askContext: {
5963
- phases: ctx.phases,
5964
- currentPhase: phaseIndex,
5965
- answers: {}
5966
- }
5967
- });
5968
- log$19.info("已创建提问卡片操作", {
5969
- operationId,
5970
- phaseId: phase.id,
5971
- phaseIndex,
5972
- totalPhases,
5973
- sessionId: ctx.sessionId
5974
- });
5975
- return {
5976
- card,
5977
- operationId
5978
- };
5979
- }
5980
- /** CC 输出中的授权请求标记格式 */
5981
- const AUTH_REQUEST_PATTERN = /\[PERMISSION_REQUEST\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
5982
- /** CC 输出中的提问请求标记格式 */
5983
- const ASK_USER_PATTERN = /\[ASK_USER\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
5984
- /**
5985
- * 从 CC 回复文本中检测是否包含授权请求标记
5986
- */
5987
- function detectAuthRequest(text) {
5988
- const match = text.match(AUTH_REQUEST_PATTERN);
5989
- if (!match) return null;
5990
- try {
5991
- const data = JSON.parse(match[1]);
5992
- if (Array.isArray(data.scopes) && data.scopes.length > 0) return { scopes: data.scopes };
5993
- } catch {}
5994
- return null;
5995
- }
5996
- /**
5997
- * 从 CC 回复文本中检测是否包含提问请求标记
5998
- */
5999
- function detectAskRequest(text) {
6000
- const match = text.match(ASK_USER_PATTERN);
6001
- if (!match) return null;
6002
- try {
6003
- const data = JSON.parse(match[1]);
6004
- if (typeof data.question === "string" && Array.isArray(data.phases) && data.phases.length > 0) return {
6005
- question: data.question,
6006
- phases: data.phases
6007
- };
6008
- } catch {}
6009
- return null;
6010
- }
6011
- /**
6012
- * 从 CC 回复文本中移除标记部分(避免展示给用户)
6013
- */
6014
- function stripInteractiveMarkers(text) {
6015
- return text.replace(AUTH_REQUEST_PATTERN, "").replace(ASK_USER_PATTERN, "").trim();
6016
- }
6017
- //#endregion
6018
- //#region src/card/reasoning-utils.ts
6019
- /**
6020
- * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
6021
- * SPDX-License-Identifier: MIT
6022
- *
6023
- * Shared utilities for the reasoning display subsystem.
6024
- */
6025
- function normalizeToolName(name) {
6026
- return name?.trim().toLowerCase() ?? "";
6027
- }
6028
- function truncateText(value, maxLength) {
6029
- if (value.length <= maxLength) return value;
6030
- return `${value.slice(0, maxLength - 3)}...`;
6031
- }
6032
- const INLINE_ASSIGNMENT_RE = /(^|[\s"'`])([A-Za-z_][A-Za-z0-9_]*)(=(?:"[^"]*"|'[^']*'|[^\s"'`]+))/g;
6033
- const AUTH_HEADER_SECRET_RE = /(Authorization\s*:\s*(?:Bearer|Basic|Token)\s+)([^'"\s]+)/gi;
6034
- const QUOTED_HEADER_ARG_RE = /((?:^|[\s"'`])(?:-H|--header)\s+)(['"])([A-Za-z0-9_-]+)(\s*:\s*)([^'"]*)(\2)/gi;
6035
- const UNQUOTED_HEADER_ARG_RE = /((?:^|[\s"'`])(?:-H|--header)\s+)([A-Za-z0-9_-]+)(\s*:\s*)([^\s"'`]+)/gi;
6036
- const SECRET_FLAG_RE = /((?:^|[\s"'`]))(--?[A-Za-z0-9][A-Za-z0-9-]*)(=|\s+)(?:"([^"]*)"|'([^']*)'|([^\s"'`]+))/g;
6037
- const SENSITIVE_NAME_RE = /token|secret|password|api[_-]?key|authorization|cookie|credential|bearer|session[_-]?id|client[_-]?secret|access[_-]?key/i;
6038
- function redactInlineSecrets(value) {
6039
- return value.replace(INLINE_ASSIGNMENT_RE, (match, prefix, key) => isSensitiveName(key) ? `${prefix}${key}=[redacted]` : match).replace(AUTH_HEADER_SECRET_RE, "$1[redacted]").replace(QUOTED_HEADER_ARG_RE, (match, prefix, quote, name, separator) => shouldRedactHeaderValue(name) ? `${prefix}${quote}${name}${separator}[redacted]${quote}` : match).replace(UNQUOTED_HEADER_ARG_RE, (match, prefix, name, separator) => shouldRedactHeaderValue(name) ? `${prefix}${name}${separator}[redacted]` : match).replace(SECRET_FLAG_RE, (match, prefix, flag, separator, doubleQuoted, singleQuoted, bare) => {
6040
- if (!isSensitiveName(flag.replace(/^-+/, ""))) return match;
6041
- return `${prefix}${flag}${separator}${doubleQuoted !== void 0 ? "\"[redacted]\"" : singleQuoted !== void 0 ? "'[redacted]'" : bare !== void 0 ? "[redacted]" : "[redacted]"}`;
6042
- });
6043
- }
6044
- function isSensitiveName(value) {
6045
- return SENSITIVE_NAME_RE.test(value);
6046
- }
6047
- function shouldRedactHeaderValue(name) {
6048
- return !/^authorization$/i.test(name) && isSensitiveName(name);
6049
- }
6050
- //#endregion
6051
- //#region src/card/tool-use-trace-store.ts
6052
- /**
6053
- * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
6054
- * SPDX-License-Identifier: MIT
6055
- *
6056
- * Runtime store for structured tool-use steps.
6057
- *
6058
- * The Feishu card renderer reads from this store by session key so it can
6059
- * render observable, replayable tool execution without relying purely on
6060
- * reply payload text.
6061
- */
6062
- const TRACE_TTL_MS = 1800 * 1e3;
6063
- const MAX_SESSION_TRACES = 128;
6064
- const MAX_STEPS_PER_SESSION = 256;
6065
- const STEP_RUNNING_TIMEOUT_MS = 300 * 1e3;
6066
- const GENERIC_STRING_LIMIT = 512;
6067
- const RESULT_STRING_LIMIT = 1024;
6068
- const COMMAND_STRING_LIMIT = 4096;
6069
- const PATH_STRING_LIMIT = 2048;
6070
- const sessionTraces = /* @__PURE__ */ new Map();
6071
- function startToolUseTraceRun(sessionKey) {
6072
- if (!sessionKey) return;
6073
- pruneTraceStore();
6074
- sessionTraces.set(sessionKey, {
6075
- nextSeq: 1,
6076
- updatedAt: Date.now(),
6077
- steps: [],
6078
- currentRunId: void 0
6079
- });
6080
- }
6081
- function clearToolUseTraceRun(sessionKey) {
6082
- if (!sessionKey) return;
6083
- sessionTraces.delete(sessionKey);
6084
- }
6085
- function recordToolUseStart(params) {
6086
- const { sessionKey, toolName, toolParams, toolCallId, runId } = params;
6087
- if (!sessionKey || !toolName) return;
6088
- const state = sessionTraces.get(sessionKey);
6089
- if (!state) return;
6090
- if (runId) {
6091
- if (state.currentRunId === void 0) state.currentRunId = runId;
6092
- else if (state.currentRunId !== runId) return;
6093
- }
6094
- const now = Date.now();
6095
- if (state.steps.length >= MAX_STEPS_PER_SESSION) state.steps.splice(0, state.steps.length - MAX_STEPS_PER_SESSION + 1);
6096
- state.steps.push({
6097
- id: `${state.nextSeq}`,
6098
- seq: state.nextSeq,
6099
- toolName,
6100
- toolCallId: toolCallId || void 0,
6101
- runId: runId || void 0,
6102
- params: sanitizeTraceValue(toolParams, 0, { source: "params" }),
6103
- status: "running",
6104
- startedAt: now
6105
- });
6106
- state.nextSeq += 1;
6107
- state.updatedAt = now;
6108
- }
6109
- /**
6110
- * 更新已有 running 步骤的参数(当 assistant 完整消息到来后补全参数)。
6111
- * 如果找到匹配的 running 步骤且当前 params 为空则更新,返回 true。
6112
- * 找不到或无需更新返回 false。
6011
+ * 更新已有 running 步骤的参数(当 assistant 完整消息到来后补全参数)。
6012
+ * 如果找到匹配的 running 步骤且当前 params 为空则更新,返回 true。
6013
+ * 找不到或无需更新返回 false。
6113
6014
  */
6114
6015
  function updateToolUseParams(params) {
6115
6016
  const { sessionKey, toolName, toolParams } = params;
@@ -6273,14 +6174,14 @@ function sortTraceValue(value) {
6273
6174
  }
6274
6175
  //#endregion
6275
6176
  //#region src/card/cc-stream-bridge.ts
6276
- const log$18 = larkLogger("card/cc-stream-bridge");
6177
+ const log$19 = larkLogger("card/cc-stream-bridge");
6277
6178
  const CC_INTERNAL_PLACEHOLDER = "No response requested.";
6278
6179
  /**
6279
6180
  * CCStreamBridge — 将 CC 流事件桥接到 StreamingCardController
6280
6181
  *
6281
6182
  * 内部维护文本累积状态,将增量事件转换为控制器需要的累积文本回调。
6282
6183
  */
6283
- var CCStreamBridge = class CCStreamBridge {
6184
+ var CCStreamBridge = class {
6284
6185
  /** 累积的文本输出(textDelta 逐步拼接) */
6285
6186
  accumulatedText = "";
6286
6187
  /** 累积的思考输出(thinkingDelta 逐步拼接) */
@@ -6298,34 +6199,24 @@ var CCStreamBridge = class CCStreamBridge {
6298
6199
  sessionKey: options?.sessionKey
6299
6200
  };
6300
6201
  if (this.options.sessionKey) startToolUseTraceRun(this.options.sessionKey);
6301
- log$18.info("CCStreamBridge 初始化", {
6202
+ log$19.info("CCStreamBridge 初始化", {
6302
6203
  autoCompleteOnTurnEnd: this.options.autoCompleteOnTurnEnd,
6303
6204
  sessionKey: this.options.sessionKey ?? "(none)"
6304
6205
  });
6305
6206
  }
6306
- /** 交互式标记前缀 用于流式过滤 */
6307
- static INTERACTIVE_MARKER_PREFIXES = ["[ASK_USER]", "[PERMISSION_REQUEST]"];
6308
- /** 文本增量 → 累积后调用 controller.onPartialReply(实时过滤交互式标记) */
6207
+ /** 文本增量 累积后调用 controller.onPartialReply */
6309
6208
  onTextDelta(text) {
6310
6209
  this.accumulatedText += text;
6311
- log$18.debug("textDelta 事件", {
6210
+ log$19.debug("textDelta 事件", {
6312
6211
  deltaLen: text.length,
6313
6212
  totalLen: this.accumulatedText.length
6314
6213
  });
6315
- let displayText = this.accumulatedText;
6316
- for (const prefix of CCStreamBridge.INTERACTIVE_MARKER_PREFIXES) {
6317
- const idx = displayText.indexOf(prefix);
6318
- if (idx !== -1) {
6319
- displayText = displayText.substring(0, idx).trimEnd();
6320
- break;
6321
- }
6322
- }
6323
- this.controller.onPartialReply({ text: displayText });
6214
+ this.controller.onPartialReply({ text: this.accumulatedText });
6324
6215
  }
6325
6216
  /** 思考增量 → 累积后调用 controller.onReasoningStream */
6326
6217
  onThinkingDelta(text) {
6327
6218
  this.accumulatedThinkingText += text;
6328
- log$18.debug("thinkingDelta 事件", {
6219
+ log$19.debug("thinkingDelta 事件", {
6329
6220
  deltaLen: text.length,
6330
6221
  totalLen: this.accumulatedThinkingText.length
6331
6222
  });
@@ -6337,7 +6228,7 @@ var CCStreamBridge = class CCStreamBridge {
6337
6228
  const displayName = getToolDisplayName(toolName);
6338
6229
  const toolParams = typeof _toolInput === "object" && _toolInput !== null ? _toolInput : void 0;
6339
6230
  const hasParams = toolParams && Object.keys(toolParams).length > 0;
6340
- log$18.info("toolUseStart 事件", {
6231
+ log$19.info("toolUseStart 事件", {
6341
6232
  toolName,
6342
6233
  displayName,
6343
6234
  activeToolsCount: this.activeTools.size,
@@ -6350,7 +6241,7 @@ var CCStreamBridge = class CCStreamBridge {
6350
6241
  toolName,
6351
6242
  toolParams
6352
6243
  })) {
6353
- log$18.info("toolUseStart: 更新已有步骤的 params", { toolName });
6244
+ log$19.info("toolUseStart: 更新已有步骤的 params", { toolName });
6354
6245
  this.controller.onToolStart({
6355
6246
  name: toolName,
6356
6247
  phase: "start"
@@ -6374,7 +6265,7 @@ var CCStreamBridge = class CCStreamBridge {
6374
6265
  const toolName = this.activeTools.get(toolUseId) ?? this.lastToolName ?? "unknown";
6375
6266
  const displayName = getToolDisplayName(toolName);
6376
6267
  this.activeTools.delete(toolUseId);
6377
- log$18.info("toolResult 事件", {
6268
+ log$19.info("toolResult 事件", {
6378
6269
  toolUseId,
6379
6270
  toolName,
6380
6271
  displayName,
@@ -6389,7 +6280,7 @@ var CCStreamBridge = class CCStreamBridge {
6389
6280
  }
6390
6281
  /** 工具执行进度 → 记录日志 */
6391
6282
  onToolProgress(toolName, elapsedSeconds) {
6392
- log$18.debug("toolProgress 事件", {
6283
+ log$19.debug("toolProgress 事件", {
6393
6284
  toolName,
6394
6285
  displayName: getToolDisplayName(toolName),
6395
6286
  elapsedSeconds
@@ -6397,7 +6288,7 @@ var CCStreamBridge = class CCStreamBridge {
6397
6288
  }
6398
6289
  /** 轮次结束 → 仅在 end_turn 时标记完成 + 触发 onIdle */
6399
6290
  onTurnEnd(stopReason) {
6400
- log$18.info("turnEnd 事件", {
6291
+ log$19.info("turnEnd 事件", {
6401
6292
  stopReason,
6402
6293
  accumulatedTextLen: this.accumulatedText.length,
6403
6294
  accumulatedThinkingTextLen: this.accumulatedThinkingText.length
@@ -6405,7 +6296,7 @@ var CCStreamBridge = class CCStreamBridge {
6405
6296
  if (this.options.autoCompleteOnTurnEnd && stopReason === "end_turn") {
6406
6297
  const trimmedText = this.accumulatedText.trim();
6407
6298
  if (trimmedText === CC_INTERNAL_PLACEHOLDER) {
6408
- log$18.info("检测到 CC 内部占位消息,静默丢弃", {
6299
+ log$19.info("检测到 CC 内部占位消息,静默丢弃", {
6409
6300
  text: trimmedText.slice(0, 50),
6410
6301
  sessionKey: this.options.sessionKey
6411
6302
  });
@@ -6413,26 +6304,16 @@ var CCStreamBridge = class CCStreamBridge {
6413
6304
  return;
6414
6305
  }
6415
6306
  if (trimmedText === "") {
6416
- log$18.info("turnEnd 时文本为空,延迟到 onResult 兜底处理", { sessionKey: this.options.sessionKey });
6307
+ log$19.info("turnEnd 时文本为空,延迟到 onResult 兜底处理", { sessionKey: this.options.sessionKey });
6417
6308
  return;
6418
6309
  }
6419
- const strippedText = stripInteractiveMarkers(trimmedText);
6420
- if (strippedText !== trimmedText) {
6421
- log$18.info("turnEnd: 检测到交互式卡片标记,修剪卡片文本", {
6422
- originalLen: trimmedText.length,
6423
- strippedLen: strippedText.length,
6424
- sessionKey: this.options.sessionKey
6425
- });
6426
- this.accumulatedText = strippedText;
6427
- if (strippedText) this.controller.onPartialReply({ text: strippedText });
6428
- }
6429
6310
  this.controller.markFullyComplete();
6430
6311
  this.controller.onIdle();
6431
6312
  }
6432
6313
  }
6433
6314
  /** 最终结果 → 兜底最终化 + 错误处理 */
6434
6315
  onResult(data) {
6435
- log$18.info("result 事件", {
6316
+ log$19.info("result 事件", {
6436
6317
  subtype: data.subtype,
6437
6318
  isError: data.isError,
6438
6319
  durationMs: data.durationMs,
@@ -6444,7 +6325,7 @@ var CCStreamBridge = class CCStreamBridge {
6444
6325
  const pm = ClaudeCodeAdapter.getInstance()?.getProcessManager();
6445
6326
  const sessionId = this.options.sessionKey;
6446
6327
  if (sessionId && pm ? pm.consumeAborted(sessionId) : false) {
6447
- log$18.info("用户主动中断,按正常完成处理", {
6328
+ log$19.info("用户主动中断,按正常完成处理", {
6448
6329
  subtype: data.subtype,
6449
6330
  sessionId
6450
6331
  });
@@ -6453,14 +6334,14 @@ var CCStreamBridge = class CCStreamBridge {
6453
6334
  this.controller.onIdle();
6454
6335
  } else {
6455
6336
  const errorMessage = data.result ?? `CC 执行失败: ${data.subtype}`;
6456
- log$18.error("CC 执行返回错误", {
6337
+ log$19.error("CC 执行返回错误", {
6457
6338
  subtype: data.subtype,
6458
6339
  errorMessage: errorMessage.slice(0, 200)
6459
6340
  });
6460
6341
  this.controller.onError(new Error(errorMessage), { kind: "cc-execution" });
6461
6342
  }
6462
6343
  } else if (!this.accumulatedText.trim()) if (data.result) {
6463
- log$18.info("result 兜底:使用 result.result 作为最终回复", {
6344
+ log$19.info("result 兜底:使用 result.result 作为最终回复", {
6464
6345
  resultLen: data.result.length,
6465
6346
  sessionKey: this.options.sessionKey
6466
6347
  });
@@ -6468,7 +6349,7 @@ var CCStreamBridge = class CCStreamBridge {
6468
6349
  this.controller.markFullyComplete();
6469
6350
  this.controller.onIdle();
6470
6351
  } else {
6471
- log$18.info("result 兜底:无文本且无 result,abort 卡片", { sessionKey: this.options.sessionKey });
6352
+ log$19.info("result 兜底:无文本且无 result,abort 卡片", { sessionKey: this.options.sessionKey });
6472
6353
  this.controller.abortCard();
6473
6354
  }
6474
6355
  else {
@@ -6482,7 +6363,7 @@ var CCStreamBridge = class CCStreamBridge {
6482
6363
  * 内部复用上面的直接调用方法。
6483
6364
  */
6484
6365
  bindParser(parser) {
6485
- log$18.info("绑定 CCStreamParser 事件到卡片控制器");
6366
+ log$19.info("绑定 CCStreamParser 事件到卡片控制器");
6486
6367
  parser.on("textDelta", (text) => this.onTextDelta(text));
6487
6368
  parser.on("thinkingDelta", (text) => this.onThinkingDelta(text));
6488
6369
  parser.on("toolUseStart", (toolName, toolInput) => this.onToolUseStart(toolName, toolInput));
@@ -6490,7 +6371,7 @@ var CCStreamBridge = class CCStreamBridge {
6490
6371
  parser.on("toolProgress", (toolName, elapsedSeconds) => this.onToolProgress(toolName, elapsedSeconds));
6491
6372
  parser.on("turnEnd", (stopReason) => this.onTurnEnd(stopReason));
6492
6373
  parser.on("result", (data) => this.onResult(data));
6493
- log$18.info("CCStreamParser 事件绑定完成");
6374
+ log$19.info("CCStreamParser 事件绑定完成");
6494
6375
  }
6495
6376
  };
6496
6377
  //#endregion
@@ -8506,7 +8387,7 @@ function resolveLarkSdk(cfg, accountId) {
8506
8387
  if (cached) return cached.sdk;
8507
8388
  return LarkClient.fromCfg(cfg, accountId).sdk;
8508
8389
  }
8509
- const log$17 = larkLogger("card/cardkit");
8390
+ const log$18 = larkLogger("card/cardkit");
8510
8391
  /**
8511
8392
  * 记录 CardKit API 响应日志,检测错误码并抛出异常。
8512
8393
  *
@@ -8516,13 +8397,13 @@ const log$17 = larkLogger("card/cardkit");
8516
8397
  function logCardKitResponse(params) {
8517
8398
  const { resp, api, context } = params;
8518
8399
  const { code, msg } = resp;
8519
- log$17.info(`cardkit ${api} response`, {
8400
+ log$18.info(`cardkit ${api} response`, {
8520
8401
  code,
8521
8402
  msg,
8522
8403
  context
8523
8404
  });
8524
8405
  if (code && code !== 0) {
8525
- log$17.warn(`cardkit ${api} FAILED`, {
8406
+ log$18.warn(`cardkit ${api} FAILED`, {
8526
8407
  code,
8527
8408
  msg,
8528
8409
  context,
@@ -8933,7 +8814,7 @@ function validateLocalMediaRoots(filePath, localRoots) {
8933
8814
  * Feishu messages, uploading media to the Feishu IM storage, and
8934
8815
  * sending image / file messages to chats.
8935
8816
  */
8936
- const log$16 = larkLogger("outbound/media");
8817
+ const log$17 = larkLogger("outbound/media");
8937
8818
  /**
8938
8819
  * Upload an image to Feishu IM storage.
8939
8820
  *
@@ -9001,7 +8882,7 @@ async function validateRemoteUrl(raw) {
9001
8882
  for (const addr of addresses) if (isPrivateIP(addr)) throw new Error(`[feishu-media] Domain "${hostname}" resolves to private/reserved IP "${addr}" (SSRF protection). URL: "${raw}"`);
9002
8883
  } catch (err) {
9003
8884
  if (err instanceof Error && err.message.includes("SSRF protection")) throw err;
9004
- log$16.warn(`[feishu-media] DNS resolution failed for "${hostname}": ${err}`);
8885
+ log$17.warn(`[feishu-media] DNS resolution failed for "${hostname}": ${err}`);
9005
8886
  }
9006
8887
  }
9007
8888
  /**
@@ -9019,21 +8900,21 @@ async function fetchMediaBuffer(urlOrPath, localRoots) {
9019
8900
  if (localRoots !== void 0) validateLocalMediaRoots(filePath, localRoots);
9020
8901
  else throw new Error(`[feishu-media] Local file access denied for "${filePath}": mediaLocalRoots is not configured. Configure mediaLocalRoots to explicitly allow local file access.`);
9021
8902
  const buf = fs.readFileSync(filePath);
9022
- log$16.debug(`local file read: "${filePath}", ${buf.length} bytes`);
8903
+ log$17.debug(`local file read: "${filePath}", ${buf.length} bytes`);
9023
8904
  return buf;
9024
8905
  }
9025
8906
  await validateRemoteUrl(raw);
9026
8907
  const FETCH_TIMEOUT_MS = 3e4;
9027
- log$16.info(`fetching remote media: ${raw}`);
8908
+ log$17.info(`fetching remote media: ${raw}`);
9028
8909
  const response = await fetch(raw, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
9029
8910
  if (!response.ok) throw new Error(`[feishu-media] Failed to fetch media from "${raw}": HTTP ${response.status} ${response.statusText}. Verify the URL is accessible and returns a valid media resource.`);
9030
8911
  const arrayBuffer = await response.arrayBuffer();
9031
- log$16.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);
8912
+ log$17.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);
9032
8913
  return Buffer.from(arrayBuffer);
9033
8914
  }
9034
8915
  //#endregion
9035
8916
  //#region src/card/image-resolver.ts
9036
- const log$15 = larkLogger("card/image-resolver");
8917
+ const log$16 = larkLogger("card/image-resolver");
9037
8918
  /** Matches complete markdown image syntax: `![alt](value)` */
9038
8919
  const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
9039
8920
  var ImageResolver = class {
@@ -9079,14 +8960,14 @@ var ImageResolver = class {
9079
8960
  async resolveImagesAwait(text, timeoutMs) {
9080
8961
  this.resolveImages(text);
9081
8962
  if (this.pending.size > 0) {
9082
- log$15.info("resolveImagesAwait: waiting for uploads", {
8963
+ log$16.info("resolveImagesAwait: waiting for uploads", {
9083
8964
  count: this.pending.size,
9084
8965
  timeoutMs
9085
8966
  });
9086
8967
  const allUploads = Promise.all(this.pending.values());
9087
8968
  const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
9088
8969
  await Promise.race([allUploads, timeout]);
9089
- if (this.pending.size > 0) log$15.warn("resolveImagesAwait: timed out with pending uploads", { remaining: this.pending.size });
8970
+ if (this.pending.size > 0) log$16.warn("resolveImagesAwait: timed out with pending uploads", { remaining: this.pending.size });
9090
8971
  }
9091
8972
  return this.resolveImages(text);
9092
8973
  }
@@ -9096,7 +8977,7 @@ var ImageResolver = class {
9096
8977
  }
9097
8978
  async doUpload(url) {
9098
8979
  try {
9099
- log$15.info("uploading image", { url });
8980
+ log$16.info("uploading image", { url });
9100
8981
  const buffer = await fetchRemoteImageBuffer(url);
9101
8982
  const { imageKey } = await uploadImageLark({
9102
8983
  cfg: this.cfg,
@@ -9104,7 +8985,7 @@ var ImageResolver = class {
9104
8985
  imageType: "message",
9105
8986
  accountId: this.accountId
9106
8987
  });
9107
- log$15.info("image uploaded", {
8988
+ log$16.info("image uploaded", {
9108
8989
  url,
9109
8990
  imageKey
9110
8991
  });
@@ -9113,7 +8994,7 @@ var ImageResolver = class {
9113
8994
  this.onImageResolved();
9114
8995
  return imageKey;
9115
8996
  } catch (err) {
9116
- log$15.warn("image upload failed", {
8997
+ log$16.warn("image upload failed", {
9117
8998
  url,
9118
8999
  error: String(err)
9119
9000
  });
@@ -9134,7 +9015,7 @@ var ImageResolver = class {
9134
9015
  * Encapsulates the terminateDueToUnavailable / shouldSkipForUnavailable
9135
9016
  * logic previously scattered as closures in reply-dispatcher.ts.
9136
9017
  */
9137
- const log$14 = larkLogger("card/unavailable-guard");
9018
+ const log$15 = larkLogger("card/unavailable-guard");
9138
9019
  var UnavailableGuard = class {
9139
9020
  terminated = false;
9140
9021
  replyToMessageId;
@@ -9187,7 +9068,7 @@ var UnavailableGuard = class {
9187
9068
  this.terminated = true;
9188
9069
  this.onTerminate();
9189
9070
  const affectedMessageId = fromError?.messageId ?? this.replyToMessageId ?? cardMessageId ?? "unknown";
9190
- log$14.warn("reply pipeline terminated by unavailable message", {
9071
+ log$15.warn("reply pipeline terminated by unavailable message", {
9191
9072
  source,
9192
9073
  apiCode,
9193
9074
  messageId: affectedMessageId
@@ -9224,7 +9105,7 @@ function getActiveCard(sessionId) {
9224
9105
  * Delegates throttling to FlushController and message-unavailable
9225
9106
  * detection to UnavailableGuard.
9226
9107
  */
9227
- const log$13 = larkLogger("card/streaming");
9108
+ const log$14 = larkLogger("card/streaming");
9228
9109
  var StreamingCardController = class StreamingCardController {
9229
9110
  phase = "idle";
9230
9111
  cardKit = {
@@ -9312,7 +9193,7 @@ var StreamingCardController = class StreamingCardController {
9312
9193
  }
9313
9194
  }
9314
9195
  if (!entry) {
9315
- log$13.debug("footer metrics lookup: session entry missing", {
9196
+ log$14.debug("footer metrics lookup: session entry missing", {
9316
9197
  sessionKey: this.deps.sessionKey,
9317
9198
  candidateKeys,
9318
9199
  storePath,
@@ -9330,7 +9211,7 @@ var StreamingCardController = class StreamingCardController {
9330
9211
  contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : void 0,
9331
9212
  model: typeof entry.model === "string" ? entry.model : void 0
9332
9213
  };
9333
- log$13.debug("footer metrics lookup: session entry found", {
9214
+ log$14.debug("footer metrics lookup: session entry found", {
9334
9215
  sessionKey: this.deps.sessionKey,
9335
9216
  matchedKey,
9336
9217
  storePath,
@@ -9355,7 +9236,7 @@ var StreamingCardController = class StreamingCardController {
9355
9236
  }
9356
9237
  }
9357
9238
  if (!entry) {
9358
- log$13.debug("footer metrics lookup: session entry missing", {
9239
+ log$14.debug("footer metrics lookup: session entry missing", {
9359
9240
  sessionKey: this.deps.sessionKey,
9360
9241
  candidateKeys,
9361
9242
  storePath,
@@ -9373,7 +9254,7 @@ var StreamingCardController = class StreamingCardController {
9373
9254
  contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : void 0,
9374
9255
  model: typeof entry.model === "string" ? entry.model : void 0
9375
9256
  };
9376
- log$13.debug("footer metrics lookup: session entry found", {
9257
+ log$14.debug("footer metrics lookup: session entry found", {
9377
9258
  sessionKey: this.deps.sessionKey,
9378
9259
  matchedKey,
9379
9260
  storePath,
@@ -9381,7 +9262,7 @@ var StreamingCardController = class StreamingCardController {
9381
9262
  });
9382
9263
  return metrics;
9383
9264
  } catch (err) {
9384
- log$13.warn("footer metrics lookup failed", {
9265
+ log$14.warn("footer metrics lookup failed", {
9385
9266
  error: String(err),
9386
9267
  sessionKey: this.deps.sessionKey
9387
9268
  });
@@ -9488,7 +9369,7 @@ var StreamingCardController = class StreamingCardController {
9488
9369
  const from = this.phase;
9489
9370
  if (from === to) return false;
9490
9371
  if (!PHASE_TRANSITIONS[from].has(to)) {
9491
- log$13.warn("phase transition rejected", {
9372
+ log$14.warn("phase transition rejected", {
9492
9373
  from,
9493
9374
  to,
9494
9375
  source
@@ -9496,7 +9377,7 @@ var StreamingCardController = class StreamingCardController {
9496
9377
  return false;
9497
9378
  }
9498
9379
  this.phase = to;
9499
- log$13.info("phase transition", {
9380
+ log$14.info("phase transition", {
9500
9381
  from,
9501
9382
  to,
9502
9383
  source,
@@ -9616,7 +9497,7 @@ var StreamingCardController = class StreamingCardController {
9616
9497
  this.reasoning.dirty = true;
9617
9498
  }
9618
9499
  const text = split.answerText ?? stripReasoningTags(rawText);
9619
- log$13.debug("onPartialReply", { len: text.length });
9500
+ log$14.debug("onPartialReply", { len: text.length });
9620
9501
  if (!text) return;
9621
9502
  this.captureToolUseElapsed();
9622
9503
  if (!this.reasoning.reasoningStartTime) this.reasoning.reasoningStartTime = Date.now();
@@ -9628,7 +9509,7 @@ var StreamingCardController = class StreamingCardController {
9628
9509
  this.text.lastPartialText = text;
9629
9510
  this.text.accumulatedText = this.text.streamingPrefix ? this.text.streamingPrefix + "\n\n" + text : text;
9630
9511
  if (!this.text.streamingPrefix && SILENT_REPLY_TOKEN.startsWith(this.text.accumulatedText.trim())) {
9631
- log$13.debug("onPartialReply: buffering NO_REPLY prefix");
9512
+ log$14.debug("onPartialReply: buffering NO_REPLY prefix");
9632
9513
  return;
9633
9514
  }
9634
9515
  await this.ensureCardCreated();
@@ -9638,7 +9519,7 @@ var StreamingCardController = class StreamingCardController {
9638
9519
  }
9639
9520
  async onError(err, info) {
9640
9521
  if (this.guard.terminate("onError", err)) return;
9641
- log$13.error(`${info.kind} reply failed`, { error: String(err) });
9522
+ log$14.error(`${info.kind} reply failed`, { error: String(err) });
9642
9523
  this.captureToolUseElapsed();
9643
9524
  this.finalizeCard("onError", "error");
9644
9525
  await this.flush.waitForFlush();
@@ -9694,7 +9575,7 @@ var StreamingCardController = class StreamingCardController {
9694
9575
  if (this.cardKit.cardMessageId) {
9695
9576
  const isNoReplyLeak = !this.text.completedText && SILENT_REPLY_TOKEN.startsWith(this.text.accumulatedText.trim());
9696
9577
  const displayText = this.text.completedText || (isNoReplyLeak ? "" : this.text.accumulatedText) || "Done.";
9697
- if (!this.text.completedText && !this.text.accumulatedText) log$13.warn("reply completed without visible text, using empty-reply fallback");
9578
+ if (!this.text.completedText && !this.text.accumulatedText) log$14.warn("reply completed without visible text, using empty-reply fallback");
9698
9579
  const resolvedDisplayText = await this.imageResolver.resolveImagesAwait(displayText, 15e3);
9699
9580
  const idleToolUseDisplay = this.computeToolUseDisplay();
9700
9581
  const terminalContent = prepareTerminalCardContent({
@@ -9722,7 +9603,7 @@ var StreamingCardController = class StreamingCardController {
9722
9603
  const rewindSessionId = this.deps.sessionKey;
9723
9604
  const rewindMessageId = ClaudeCodeAdapter.getInstance()?.getProcessManager()?.getLastUserMessageId(this.deps.sessionKey);
9724
9605
  const rewindHasFileChanges = this.hasFileChangingTools(idleToolUseDisplay?.steps);
9725
- log$13.info("onIdle: rewind button conditions", {
9606
+ log$14.info("onIdle: rewind button conditions", {
9726
9607
  sessionId: rewindSessionId,
9727
9608
  messageId: rewindMessageId,
9728
9609
  hasFileChanges: rewindHasFileChanges,
@@ -9735,7 +9616,7 @@ var StreamingCardController = class StreamingCardController {
9735
9616
  if (idleEffectiveCardId) {
9736
9617
  const seqBeforeClose = this.cardKit.cardKitSequence;
9737
9618
  this.cardKit.cardKitSequence += 1;
9738
- log$13.info("onIdle: closing streaming mode", {
9619
+ log$14.info("onIdle: closing streaming mode", {
9739
9620
  attempt,
9740
9621
  seqBefore: seqBeforeClose,
9741
9622
  seqAfter: this.cardKit.cardKitSequence
@@ -9749,7 +9630,7 @@ var StreamingCardController = class StreamingCardController {
9749
9630
  });
9750
9631
  const seqBeforeUpdate = this.cardKit.cardKitSequence;
9751
9632
  this.cardKit.cardKitSequence += 1;
9752
- log$13.info("onIdle: updating final card", {
9633
+ log$14.info("onIdle: updating final card", {
9753
9634
  attempt,
9754
9635
  seqBefore: seqBeforeUpdate,
9755
9636
  seqAfter: this.cardKit.cardKitSequence
@@ -9767,33 +9648,33 @@ var StreamingCardController = class StreamingCardController {
9767
9648
  card: completeCard,
9768
9649
  accountId: this.deps.accountId
9769
9650
  });
9770
- log$13.info("reply completed, card finalized", {
9651
+ log$14.info("reply completed, card finalized", {
9771
9652
  elapsedMs: this.elapsed(),
9772
9653
  isCardKit: !!idleEffectiveCardId,
9773
9654
  attempt
9774
9655
  });
9775
9656
  break;
9776
9657
  } catch (retryErr) {
9777
- log$13.warn("final card update attempt failed", {
9658
+ log$14.warn("final card update attempt failed", {
9778
9659
  attempt,
9779
9660
  maxRetries: MAX_FINAL_UPDATE_RETRIES,
9780
9661
  error: String(retryErr)
9781
9662
  });
9782
9663
  if (attempt < MAX_FINAL_UPDATE_RETRIES) await new Promise((resolve) => setTimeout(resolve, FINAL_UPDATE_RETRY_DELAY_MS * attempt));
9783
- else log$13.error("final card update exhausted all retries, card may remain in streaming state", {
9664
+ else log$14.error("final card update exhausted all retries, card may remain in streaming state", {
9784
9665
  cardId: idleEffectiveCardId,
9785
9666
  messageId: this.cardKit.cardMessageId
9786
9667
  });
9787
9668
  }
9788
9669
  }
9789
9670
  } catch (err) {
9790
- log$13.error("final card update failed (outer)", { error: String(err) });
9671
+ log$14.error("final card update failed (outer)", { error: String(err) });
9791
9672
  } finally {
9792
9673
  clearToolUseTraceRun(this.deps.sessionKey);
9793
9674
  }
9794
9675
  }
9795
9676
  markFullyComplete() {
9796
- log$13.debug("markFullyComplete", {
9677
+ log$14.debug("markFullyComplete", {
9797
9678
  completedTextLen: this.text.completedText.length,
9798
9679
  accumulatedTextLen: this.text.accumulatedText.length
9799
9680
  });
@@ -9832,7 +9713,7 @@ var StreamingCardController = class StreamingCardController {
9832
9713
  footerMetrics
9833
9714
  });
9834
9715
  await this.closeStreamingAndUpdate(effectiveCardId, abortCardContent, "abortCard");
9835
- log$13.info("abortCard completed", { effectiveCardId });
9716
+ log$14.info("abortCard completed", { effectiveCardId });
9836
9717
  } else if (this.cardKit.cardMessageId) {
9837
9718
  const abortCard = buildCardContent("complete", {
9838
9719
  text: terminalContent.text,
@@ -9853,10 +9734,10 @@ var StreamingCardController = class StreamingCardController {
9853
9734
  card: abortCard,
9854
9735
  accountId: this.deps.accountId
9855
9736
  });
9856
- log$13.info("abortCard completed (IM fallback)", { messageId: this.cardKit.cardMessageId });
9737
+ log$14.info("abortCard completed (IM fallback)", { messageId: this.cardKit.cardMessageId });
9857
9738
  }
9858
9739
  } catch (err) {
9859
- log$13.warn("abortCard failed", { error: String(err) });
9740
+ log$14.warn("abortCard failed", { error: String(err) });
9860
9741
  } finally {
9861
9742
  clearToolUseTraceRun(this.deps.sessionKey);
9862
9743
  }
@@ -9871,10 +9752,10 @@ var StreamingCardController = class StreamingCardController {
9871
9752
  async forceCloseStreaming() {
9872
9753
  const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
9873
9754
  if (!effectiveCardId && !this.cardKit.cardMessageId) {
9874
- log$13.warn("forceCloseStreaming: no card to close");
9755
+ log$14.warn("forceCloseStreaming: no card to close");
9875
9756
  return;
9876
9757
  }
9877
- log$13.info("forceCloseStreaming: 强制终态化卡片", {
9758
+ log$14.info("forceCloseStreaming: 强制终态化卡片", {
9878
9759
  cardId: effectiveCardId,
9879
9760
  messageId: this.cardKit.cardMessageId,
9880
9761
  phase: this.phase
@@ -9924,10 +9805,10 @@ var StreamingCardController = class StreamingCardController {
9924
9805
  card: completeCard,
9925
9806
  accountId: this.deps.accountId
9926
9807
  });
9927
- log$13.info("forceCloseStreaming: 卡片已强制终态化");
9808
+ log$14.info("forceCloseStreaming: 卡片已强制终态化");
9928
9809
  if (!this.isTerminalPhase) this.finalizeCard("forceCloseStreaming", "abort");
9929
9810
  } catch (err) {
9930
- log$13.error("forceCloseStreaming failed", { error: String(err) });
9811
+ log$14.error("forceCloseStreaming failed", { error: String(err) });
9931
9812
  } finally {
9932
9813
  unregisterActiveCard(this.deps.sessionKey);
9933
9814
  clearToolUseTraceRun(this.deps.sessionKey);
@@ -9951,7 +9832,7 @@ var StreamingCardController = class StreamingCardController {
9951
9832
  this.disposeShutdownHook = null;
9952
9833
  return;
9953
9834
  }
9954
- log$13.info("reusing placeholder card", {
9835
+ log$14.info("reusing placeholder card", {
9955
9836
  cardId: this.deps.placeholderCardId,
9956
9837
  messageId: this.deps.placeholderMessageId
9957
9838
  });
@@ -9975,7 +9856,7 @@ var StreamingCardController = class StreamingCardController {
9975
9856
  accountId: this.deps.accountId
9976
9857
  });
9977
9858
  if (this.isStaleCreate(epoch)) {
9978
- log$13.info("ensureCardCreated: stale epoch after createCardEntity, bailing out", {
9859
+ log$14.info("ensureCardCreated: stale epoch after createCardEntity, bailing out", {
9979
9860
  epoch,
9980
9861
  phase: this.phase
9981
9862
  });
@@ -9986,7 +9867,7 @@ var StreamingCardController = class StreamingCardController {
9986
9867
  this.cardKit.originalCardKitCardId = cId;
9987
9868
  this.cardKit.cardKitSequence = 1;
9988
9869
  this.disposeShutdownHook = registerShutdownHook(`streaming-card:${cId}`, () => this.abortCard());
9989
- log$13.info("created CardKit entity", {
9870
+ log$14.info("created CardKit entity", {
9990
9871
  cardId: cId,
9991
9872
  initialSequence: this.cardKit.cardKitSequence
9992
9873
  });
@@ -9999,7 +9880,7 @@ var StreamingCardController = class StreamingCardController {
9999
9880
  accountId: this.deps.accountId
10000
9881
  });
10001
9882
  if (this.isStaleCreate(epoch)) {
10002
- log$13.info("ensureCardCreated: stale epoch after sendCardByCardId, bailing out", {
9883
+ log$14.info("ensureCardCreated: stale epoch after sendCardByCardId, bailing out", {
10003
9884
  epoch,
10004
9885
  phase: this.phase
10005
9886
  });
@@ -10014,13 +9895,13 @@ var StreamingCardController = class StreamingCardController {
10014
9895
  this.disposeShutdownHook = null;
10015
9896
  return;
10016
9897
  }
10017
- log$13.info("sent CardKit card", { messageId: result.messageId });
9898
+ log$14.info("sent CardKit card", { messageId: result.messageId });
10018
9899
  } else throw new Error("card.create returned empty card_id");
10019
9900
  } catch (cardKitErr) {
10020
9901
  if (this.isStaleCreate(epoch)) return;
10021
9902
  if (this.guard.terminate("ensureCardCreated.cardkitFlow", cardKitErr)) return;
10022
9903
  const apiDetail = extractApiDetail(cardKitErr);
10023
- log$13.warn("CardKit flow failed, falling back to IM", { apiDetail });
9904
+ log$14.warn("CardKit flow failed, falling back to IM", { apiDetail });
10024
9905
  this.cardKit.cardKitCardId = null;
10025
9906
  this.cardKit.originalCardKitCardId = null;
10026
9907
  const fallbackCard = buildCardContent("streaming", { showToolUse: this.deps.toolUseDisplay.showToolUse });
@@ -10033,7 +9914,7 @@ var StreamingCardController = class StreamingCardController {
10033
9914
  accountId: this.deps.accountId
10034
9915
  });
10035
9916
  if (this.isStaleCreate(epoch)) {
10036
- log$13.info("ensureCardCreated: stale epoch after IM fallback send, bailing out", {
9917
+ log$14.info("ensureCardCreated: stale epoch after IM fallback send, bailing out", {
10037
9918
  epoch,
10038
9919
  phase: this.phase
10039
9920
  });
@@ -10042,12 +9923,12 @@ var StreamingCardController = class StreamingCardController {
10042
9923
  this.cardKit.cardMessageId = result.messageId;
10043
9924
  this.flush.setCardMessageReady(true);
10044
9925
  if (!this.transition("streaming", "ensureCardCreated.imFallback")) return;
10045
- log$13.info("sent fallback IM card", { messageId: result.messageId });
9926
+ log$14.info("sent fallback IM card", { messageId: result.messageId });
10046
9927
  }
10047
9928
  } catch (err) {
10048
9929
  if (this.isStaleCreate(epoch)) return;
10049
9930
  if (this.guard.terminate("ensureCardCreated.outer", err)) return;
10050
- log$13.warn("thinking card failed, falling back to static", { error: String(err) });
9931
+ log$14.warn("thinking card failed, falling back to static", { error: String(err) });
10051
9932
  this.transition("creation_failed", "ensureCardCreated.outer", "creation_failed");
10052
9933
  }
10053
9934
  })();
@@ -10056,10 +9937,10 @@ var StreamingCardController = class StreamingCardController {
10056
9937
  async performFlush() {
10057
9938
  if (!this.cardKit.cardMessageId || this.isTerminalPhase) return;
10058
9939
  if (!this.cardKit.cardKitCardId && this.cardKit.originalCardKitCardId) {
10059
- log$13.debug("performFlush: skipping (CardKit streaming disabled, awaiting final update)");
9940
+ log$14.debug("performFlush: skipping (CardKit streaming disabled, awaiting final update)");
10060
9941
  return;
10061
9942
  }
10062
- log$13.debug("flushCardUpdate: enter", {
9943
+ log$14.debug("flushCardUpdate: enter", {
10063
9944
  seq: this.cardKit.cardKitSequence,
10064
9945
  isCardKit: !!this.cardKit.cardKitCardId
10065
9946
  });
@@ -10081,7 +9962,7 @@ var StreamingCardController = class StreamingCardController {
10081
9962
  reasoningText: this.reasoning.accumulatedReasoningText || void 0
10082
9963
  });
10083
9964
  this.cardKit.cardKitSequence += 1;
10084
- log$13.debug("flushCardUpdate: full card update (dirty)", {
9965
+ log$14.debug("flushCardUpdate: full card update (dirty)", {
10085
9966
  seq: this.cardKit.cardKitSequence,
10086
9967
  stepCount: display?.stepCount,
10087
9968
  hasReasoning: !!this.reasoning.accumulatedReasoningText
@@ -10097,7 +9978,7 @@ var StreamingCardController = class StreamingCardController {
10097
9978
  } else if (resolvedText !== this.text.lastFlushedText) {
10098
9979
  const prevSeq = this.cardKit.cardKitSequence;
10099
9980
  this.cardKit.cardKitSequence += 1;
10100
- log$13.debug("flushCardUpdate: answer seq bump", {
9981
+ log$14.debug("flushCardUpdate: answer seq bump", {
10101
9982
  seqBefore: prevSeq,
10102
9983
  seqAfter: this.cardKit.cardKitSequence
10103
9984
  });
@@ -10112,7 +9993,7 @@ var StreamingCardController = class StreamingCardController {
10112
9993
  this.text.lastFlushedText = resolvedText;
10113
9994
  }
10114
9995
  } else {
10115
- log$13.debug("flushCardUpdate: IM patch fallback");
9996
+ log$14.debug("flushCardUpdate: IM patch fallback");
10116
9997
  const flushDisplay = this.computeToolUseDisplay();
10117
9998
  const card = buildCardContent("streaming", {
10118
9999
  text: this.reasoning.isReasoningPhase ? "" : resolvedText,
@@ -10132,31 +10013,31 @@ var StreamingCardController = class StreamingCardController {
10132
10013
  if (this.guard.terminate("flushCardUpdate", err)) return;
10133
10014
  const apiCode = extractLarkApiCode(err);
10134
10015
  if (isCardRateLimitError(err)) {
10135
- log$13.info("flushCardUpdate: rate limited (230020), skipping", { seq: this.cardKit.cardKitSequence });
10016
+ log$14.info("flushCardUpdate: rate limited (230020), skipping", { seq: this.cardKit.cardKitSequence });
10136
10017
  return;
10137
10018
  }
10138
10019
  if (isCardTableLimitError(err)) {
10139
- log$13.warn("flushCardUpdate: card table limit exceeded (230099/11310), disabling CardKit streaming", { seq: this.cardKit.cardKitSequence });
10020
+ log$14.warn("flushCardUpdate: card table limit exceeded (230099/11310), disabling CardKit streaming", { seq: this.cardKit.cardKitSequence });
10140
10021
  this.cardKit.cardKitCardId = null;
10141
10022
  return;
10142
10023
  }
10143
10024
  if (apiCode === 300317 && this.cardKit.cardKitCardId) {
10144
10025
  const oldSeq = this.cardKit.cardKitSequence;
10145
10026
  this.cardKit.cardKitSequence += 100;
10146
- log$13.warn("flushCardUpdate: sequence conflict (300317), jumping sequence", {
10027
+ log$14.warn("flushCardUpdate: sequence conflict (300317), jumping sequence", {
10147
10028
  oldSeq,
10148
10029
  newSeq: this.cardKit.cardKitSequence
10149
10030
  });
10150
10031
  return;
10151
10032
  }
10152
10033
  const apiDetail = extractApiDetail(err);
10153
- log$13.error("card stream update failed", {
10034
+ log$14.error("card stream update failed", {
10154
10035
  apiCode,
10155
10036
  seq: this.cardKit.cardKitSequence,
10156
10037
  apiDetail
10157
10038
  });
10158
10039
  if (this.cardKit.cardKitCardId) {
10159
- log$13.warn("disabling CardKit streaming, falling back to im.message.patch");
10040
+ log$14.warn("disabling CardKit streaming, falling back to im.message.patch");
10160
10041
  this.cardKit.cardKitCardId = null;
10161
10042
  }
10162
10043
  }
@@ -10181,7 +10062,7 @@ var StreamingCardController = class StreamingCardController {
10181
10062
  if (!this.cardKit.cardKitCardId || this.isTerminalPhase) return;
10182
10063
  try {
10183
10064
  const display = this.computeToolUseDisplay();
10184
- log$13.info("updateToolUseStatus", {
10065
+ log$14.info("updateToolUseStatus", {
10185
10066
  hasDisplay: !!display,
10186
10067
  stepCount: display?.stepCount,
10187
10068
  steps: display?.steps?.map((s) => `${s.title}:${s.status}`).join(", ")
@@ -10203,7 +10084,7 @@ var StreamingCardController = class StreamingCardController {
10203
10084
  accountId: this.deps.accountId
10204
10085
  });
10205
10086
  } catch (err) {
10206
- log$13.debug("updateToolUseStatus failed", { error: String(err) });
10087
+ log$14.debug("updateToolUseStatus failed", { error: String(err) });
10207
10088
  }
10208
10089
  }
10209
10090
  finalizeCard(source, reason) {
@@ -10215,7 +10096,7 @@ var StreamingCardController = class StreamingCardController {
10215
10096
  async closeStreamingAndUpdate(cardId, card, label) {
10216
10097
  const seqBeforeClose = this.cardKit.cardKitSequence;
10217
10098
  this.cardKit.cardKitSequence += 1;
10218
- log$13.info(`${label}: closing streaming mode`, {
10099
+ log$14.info(`${label}: closing streaming mode`, {
10219
10100
  seqBefore: seqBeforeClose,
10220
10101
  seqAfter: this.cardKit.cardKitSequence
10221
10102
  });
@@ -10228,7 +10109,7 @@ var StreamingCardController = class StreamingCardController {
10228
10109
  });
10229
10110
  const seqBeforeUpdate = this.cardKit.cardKitSequence;
10230
10111
  this.cardKit.cardKitSequence += 1;
10231
- log$13.info(`${label}: updating card`, {
10112
+ log$14.info(`${label}: updating card`, {
10232
10113
  seqBefore: seqBeforeUpdate,
10233
10114
  seqAfter: this.cardKit.cardKitSequence
10234
10115
  });
@@ -10262,7 +10143,7 @@ function extractApiDetail(err) {
10262
10143
  }
10263
10144
  //#endregion
10264
10145
  //#region src/messaging/format-for-cc.ts
10265
- const log$12 = larkLogger("messaging/format-for-cc");
10146
+ const log$13 = larkLogger("messaging/format-for-cc");
10266
10147
  function safeParseJSON(raw) {
10267
10148
  try {
10268
10149
  return JSON.parse(raw);
@@ -10318,7 +10199,7 @@ function extractPostText(rawContent) {
10318
10199
  */
10319
10200
  function formatContentByType(ctx) {
10320
10201
  const { contentType, content, resources } = ctx;
10321
- log$12.debug("formatContentByType 开始格式化", {
10202
+ log$13.debug("formatContentByType 开始格式化", {
10322
10203
  contentType,
10323
10204
  contentLength: content?.length ?? 0,
10324
10205
  resourceCount: resources?.length ?? 0
@@ -10340,92 +10221,343 @@ function formatContentByType(ctx) {
10340
10221
  const extracted = extractPostText(content);
10341
10222
  if (extracted !== content) return extracted;
10342
10223
  }
10343
- return content;
10344
- case "merge_forward": return content;
10345
- case "audio": return content || "[语音消息]";
10346
- case "sticker": return content || "[表情贴纸]";
10347
- case "video":
10348
- case "media": return content || "[视频消息]";
10349
- case "interactive": return content;
10350
- case "share_chat":
10351
- case "share_user":
10352
- case "share_calendar_event": return content;
10353
- case "location": return content;
10354
- case "system":
10355
- case "folder":
10356
- case "hongbao":
10357
- case "calendar":
10358
- case "general_calendar":
10359
- case "video_chat":
10360
- case "todo":
10361
- case "vote": return content;
10362
- default:
10363
- log$12.warn("遇到不支持的消息类型", { contentType });
10364
- return `[不支持的消息类型: ${contentType}]`;
10365
- }
10366
- }
10367
- /**
10368
- * 将消息创建时间格式化为 AI 可理解的时间描述。
10369
- * 包含两部分:绝对时间(ISO 格式)+ 相对时间(如"5分钟前")。
10370
- * 这让 AI 能判断消息的时效性,避免将很久前的讨论当作当前上下文。
10371
- */
10372
- function formatMessageTime(createTime) {
10373
- if (!createTime) return "";
10374
- const msgDate = new Date(createTime);
10375
- const diffMs = Date.now() - createTime;
10376
- const absTime = msgDate.toLocaleString("zh-CN", {
10377
- month: "2-digit",
10378
- day: "2-digit",
10379
- hour: "2-digit",
10380
- minute: "2-digit",
10381
- hour12: false
10224
+ return content;
10225
+ case "merge_forward": return content;
10226
+ case "audio": return content || "[语音消息]";
10227
+ case "sticker": return content || "[表情贴纸]";
10228
+ case "video":
10229
+ case "media": return content || "[视频消息]";
10230
+ case "interactive": return content;
10231
+ case "share_chat":
10232
+ case "share_user":
10233
+ case "share_calendar_event": return content;
10234
+ case "location": return content;
10235
+ case "system":
10236
+ case "folder":
10237
+ case "hongbao":
10238
+ case "calendar":
10239
+ case "general_calendar":
10240
+ case "video_chat":
10241
+ case "todo":
10242
+ case "vote": return content;
10243
+ default:
10244
+ log$13.warn("遇到不支持的消息类型", { contentType });
10245
+ return `[不支持的消息类型: ${contentType}]`;
10246
+ }
10247
+ }
10248
+ /**
10249
+ * 将消息创建时间格式化为 AI 可理解的时间描述。
10250
+ * 包含两部分:绝对时间(ISO 格式)+ 相对时间(如"5分钟前")。
10251
+ * 这让 AI 能判断消息的时效性,避免将很久前的讨论当作当前上下文。
10252
+ */
10253
+ function formatMessageTime(createTime) {
10254
+ if (!createTime) return "";
10255
+ const msgDate = new Date(createTime);
10256
+ const diffMs = Date.now() - createTime;
10257
+ const absTime = msgDate.toLocaleString("zh-CN", {
10258
+ month: "2-digit",
10259
+ day: "2-digit",
10260
+ hour: "2-digit",
10261
+ minute: "2-digit",
10262
+ hour12: false
10263
+ });
10264
+ const diffSec = Math.floor(diffMs / 1e3);
10265
+ let relTime;
10266
+ if (diffSec < 60) relTime = "刚刚";
10267
+ else if (diffSec < 3600) relTime = `${Math.floor(diffSec / 60)}分钟前`;
10268
+ else if (diffSec < 86400) relTime = `${Math.floor(diffSec / 3600)}小时前`;
10269
+ else relTime = `${Math.floor(diffSec / 86400)}天前`;
10270
+ return `${absTime} (${relTime})`;
10271
+ }
10272
+ /**
10273
+ * 将飞书 MessageContext 格式化为 Claude Code 可理解的 AI Friendly 纯文本。
10274
+ *
10275
+ * 格式说明(提供 context,不 control):
10276
+ * - 私聊:[04/25 14:30 (5分钟前)] 消息内容
10277
+ * - 群聊:[04/25 14:30 (5分钟前)] [张三] 消息内容
10278
+ * - 话题:[04/25 14:30 (5分钟前)] [张三] [话题回复] 消息内容
10279
+ *
10280
+ * 时间信息让 AI 能自主判断消息的时效性和上下文连贯性。
10281
+ *
10282
+ * @param ctx 已解析的飞书消息上下文
10283
+ * @param isGroup 是否为群聊上下文,群聊时在消息前添加发送者前缀
10284
+ * @returns 格式化后的纯文本字符串
10285
+ */
10286
+ function formatForCC(ctx, isGroup) {
10287
+ log$13.info("formatForCC 开始处理", {
10288
+ messageId: ctx.messageId,
10289
+ contentType: ctx.contentType,
10290
+ chatType: ctx.chatType,
10291
+ isGroup: isGroup ?? false,
10292
+ senderName: ctx.senderName,
10293
+ createTime: ctx.createTime,
10294
+ threadId: ctx.threadId
10295
+ });
10296
+ const formattedContent = formatContentByType(ctx);
10297
+ const parts = [];
10298
+ const timeStr = formatMessageTime(ctx.createTime);
10299
+ if (timeStr) parts.push(`[${timeStr}]`);
10300
+ if (isGroup && ctx.senderName) parts.push(`[${ctx.senderName}]`);
10301
+ if (ctx.threadId) parts.push("[话题回复]");
10302
+ const result = (parts.length > 0 ? parts.join(" ") + " " : "") + formattedContent;
10303
+ log$13.info("formatForCC 完成", {
10304
+ messageId: ctx.messageId,
10305
+ resultLength: result.length,
10306
+ hasTime: !!timeStr,
10307
+ hasThread: !!ctx.threadId
10308
+ });
10309
+ return result;
10310
+ }
10311
+ //#endregion
10312
+ //#region src/card/interactive-cards.ts
10313
+ /**
10314
+ * 交互式卡片模块 — 授权卡片 & 提问卡片
10315
+ *
10316
+ * 基于"后处理检测 + 下轮注入"的异步模式:
10317
+ * CC 无法暂停等待用户输入,因此通过卡片交互收集用户反馈后,
10318
+ * 将结果作为新消息注入 CC 的下一轮对话。
10319
+ *
10320
+ * 包含:
10321
+ * - 授权申请卡片(Permission Auth Card)
10322
+ * - 多 Phase 提问卡片(Ask Card)
10323
+ * - CC 输出文本后处理(检测特殊标记)
10324
+ * - 操作 ID 生命周期管理
10325
+ */
10326
+ const log$12 = larkLogger("card/interactive-cards");
10327
+ const pendingOperations = /* @__PURE__ */ new Map();
10328
+ const OPERATION_TTL_MS = 1800 * 1e3;
10329
+ setInterval(() => {
10330
+ const now = Date.now();
10331
+ for (const [id, op] of pendingOperations) if (now - op.createdAt > OPERATION_TTL_MS) pendingOperations.delete(id);
10332
+ }, 6e4);
10333
+ /**
10334
+ * 构建授权申请卡片(CardKit v2 格式)
10335
+ *
10336
+ * 包含:
10337
+ * - 缺失权限列表
10338
+ * - "去申请" 按钮(跳转到开放平台)
10339
+ * - "已完成" 按钮(回调通知宿主层)
10340
+ */
10341
+ function buildAuthCard(ctx) {
10342
+ const operationId = randomUUID();
10343
+ const openPlatformHost = ctx.brand === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn";
10344
+ const scopeQuery = ctx.scopes.join(",");
10345
+ const authUrl = `${openPlatformHost}/app/${ctx.appId}/auth?q=${encodeURIComponent(scopeQuery)}`;
10346
+ const card = {
10347
+ schema: "2.0",
10348
+ config: {
10349
+ wide_screen_mode: true,
10350
+ update_multi: true
10351
+ },
10352
+ header: {
10353
+ template: "orange",
10354
+ title: {
10355
+ tag: "plain_text",
10356
+ content: "🔐 需要申请权限才能继续"
10357
+ }
10358
+ },
10359
+ body: { elements: [
10360
+ {
10361
+ tag: "markdown",
10362
+ content: `当前操作需要以下飞书权限,请应用管理员前往开放平台申请:\n\n${ctx.scopes.map((s) => `• \`${s}\``).join("\n")}`
10363
+ },
10364
+ { tag: "hr" },
10365
+ {
10366
+ tag: "markdown",
10367
+ content: "**第一步:** 前往开放平台申请上述权限并发布版本"
10368
+ },
10369
+ {
10370
+ tag: "action",
10371
+ actions: [{
10372
+ tag: "button",
10373
+ text: {
10374
+ tag: "plain_text",
10375
+ content: "去申请权限 ↗"
10376
+ },
10377
+ type: "primary",
10378
+ multi_url: {
10379
+ url: authUrl,
10380
+ pc_url: authUrl,
10381
+ android_url: authUrl,
10382
+ ios_url: authUrl
10383
+ }
10384
+ }]
10385
+ },
10386
+ {
10387
+ tag: "markdown",
10388
+ content: "**第二步:** 权限申请通过后,点击下方按钮通知我继续"
10389
+ },
10390
+ {
10391
+ tag: "action",
10392
+ actions: [{
10393
+ tag: "button",
10394
+ text: {
10395
+ tag: "plain_text",
10396
+ content: "✓ 已完成权限配置"
10397
+ },
10398
+ type: "default",
10399
+ behaviors: [{
10400
+ type: "callback",
10401
+ value: {
10402
+ action: "auth_complete",
10403
+ operationId
10404
+ }
10405
+ }]
10406
+ }]
10407
+ }
10408
+ ] }
10409
+ };
10410
+ pendingOperations.set(operationId, {
10411
+ type: "auth",
10412
+ operationId,
10413
+ chatId: ctx.chatId,
10414
+ sessionId: ctx.sessionId,
10415
+ createdAt: Date.now(),
10416
+ authContext: {
10417
+ appId: ctx.appId,
10418
+ scopes: ctx.scopes
10419
+ }
10382
10420
  });
10383
- const diffSec = Math.floor(diffMs / 1e3);
10384
- let relTime;
10385
- if (diffSec < 60) relTime = "刚刚";
10386
- else if (diffSec < 3600) relTime = `${Math.floor(diffSec / 60)}分钟前`;
10387
- else if (diffSec < 86400) relTime = `${Math.floor(diffSec / 3600)}小时前`;
10388
- else relTime = `${Math.floor(diffSec / 86400)}天前`;
10389
- return `${absTime} (${relTime})`;
10421
+ log$12.info("已创建授权卡片操作", {
10422
+ operationId,
10423
+ scopes: ctx.scopes,
10424
+ sessionId: ctx.sessionId
10425
+ });
10426
+ return {
10427
+ card,
10428
+ operationId
10429
+ };
10390
10430
  }
10391
10431
  /**
10392
- * 将飞书 MessageContext 格式化为 Claude Code 可理解的 AI Friendly 纯文本。
10393
- *
10394
- * 格式说明(提供 context,不 control):
10395
- * - 私聊:[04/25 14:30 (5分钟前)] 消息内容
10396
- * - 群聊:[04/25 14:30 (5分钟前)] [张三] 消息内容
10397
- * - 话题:[04/25 14:30 (5分钟前)] [张三] [话题回复] 消息内容
10398
- *
10399
- * 时间信息让 AI 能自主判断消息的时效性和上下文连贯性。
10432
+ * 构建多 phase 提问卡片(CardKit v2 格式)
10400
10433
  *
10401
- * @param ctx 已解析的飞书消息上下文
10402
- * @param isGroup 是否为群聊上下文,群聊时在消息前添加发送者前缀
10403
- * @returns 格式化后的纯文本字符串
10434
+ * 每次显示一个 phase:
10435
+ * - 有预设选项时显示按钮组
10436
+ * - 允许自由文本时显示输入提示
10437
+ * - 底部有"跳过"和"提交"按钮
10404
10438
  */
10405
- function formatForCC(ctx, isGroup) {
10406
- log$12.info("formatForCC 开始处理", {
10407
- messageId: ctx.messageId,
10408
- contentType: ctx.contentType,
10409
- chatType: ctx.chatType,
10410
- isGroup: isGroup ?? false,
10411
- senderName: ctx.senderName,
10412
- createTime: ctx.createTime,
10413
- threadId: ctx.threadId
10439
+ function buildAskCard(ctx, phaseIndex = 0) {
10440
+ const operationId = randomUUID();
10441
+ const phase = ctx.phases[phaseIndex];
10442
+ const totalPhases = ctx.phases.length;
10443
+ const elements = [];
10444
+ elements.push({
10445
+ tag: "markdown",
10446
+ content: ctx.question
10414
10447
  });
10415
- const formattedContent = formatContentByType(ctx);
10416
- const parts = [];
10417
- const timeStr = formatMessageTime(ctx.createTime);
10418
- if (timeStr) parts.push(`[${timeStr}]`);
10419
- if (isGroup && ctx.senderName) parts.push(`[${ctx.senderName}]`);
10420
- if (ctx.threadId) parts.push("[话题回复]");
10421
- const result = (parts.length > 0 ? parts.join(" ") + " " : "") + formattedContent;
10422
- log$12.info("formatForCC 完成", {
10423
- messageId: ctx.messageId,
10424
- resultLength: result.length,
10425
- hasTime: !!timeStr,
10426
- hasThread: !!ctx.threadId
10448
+ if (phase.description) elements.push({
10449
+ tag: "markdown",
10450
+ content: phase.description
10427
10451
  });
10428
- return result;
10452
+ elements.push({ tag: "hr" });
10453
+ elements.push({
10454
+ tag: "markdown",
10455
+ content: `**${phase.title}** (${phaseIndex + 1}/${totalPhases})`
10456
+ });
10457
+ if (phase.options && phase.options.length > 0) {
10458
+ const optionButtons = phase.options.map((opt) => ({
10459
+ tag: "button",
10460
+ text: {
10461
+ tag: "plain_text",
10462
+ content: opt.label
10463
+ },
10464
+ type: "default",
10465
+ behaviors: [{
10466
+ type: "callback",
10467
+ value: {
10468
+ action: "ask_select",
10469
+ operationId,
10470
+ phaseId: phase.id,
10471
+ value: opt.value
10472
+ }
10473
+ }]
10474
+ }));
10475
+ elements.push({
10476
+ tag: "action",
10477
+ actions: optionButtons
10478
+ });
10479
+ }
10480
+ if (phase.allowFreeText !== false) elements.push({
10481
+ tag: "markdown",
10482
+ content: "_💡 你也可以直接回复消息补充更多信息_"
10483
+ });
10484
+ const bottomActions = [];
10485
+ if (totalPhases > 1 && phaseIndex < totalPhases - 1) bottomActions.push({
10486
+ tag: "button",
10487
+ text: {
10488
+ tag: "plain_text",
10489
+ content: "跳过 →"
10490
+ },
10491
+ type: "default",
10492
+ behaviors: [{
10493
+ type: "callback",
10494
+ value: {
10495
+ action: "ask_skip",
10496
+ operationId,
10497
+ phaseId: phase.id
10498
+ }
10499
+ }]
10500
+ });
10501
+ bottomActions.push({
10502
+ tag: "button",
10503
+ text: {
10504
+ tag: "plain_text",
10505
+ content: "✓ 完成提交"
10506
+ },
10507
+ type: "primary",
10508
+ behaviors: [{
10509
+ type: "callback",
10510
+ value: {
10511
+ action: "ask_submit",
10512
+ operationId
10513
+ }
10514
+ }]
10515
+ });
10516
+ if (bottomActions.length > 0) {
10517
+ elements.push({ tag: "hr" });
10518
+ elements.push({
10519
+ tag: "action",
10520
+ actions: bottomActions
10521
+ });
10522
+ }
10523
+ const card = {
10524
+ schema: "2.0",
10525
+ config: {
10526
+ wide_screen_mode: true,
10527
+ update_multi: true
10528
+ },
10529
+ header: {
10530
+ template: "blue",
10531
+ title: {
10532
+ tag: "plain_text",
10533
+ content: "💬 需要补充信息"
10534
+ }
10535
+ },
10536
+ body: { elements }
10537
+ };
10538
+ pendingOperations.set(operationId, {
10539
+ type: "ask",
10540
+ operationId,
10541
+ chatId: ctx.chatId,
10542
+ sessionId: ctx.sessionId,
10543
+ createdAt: Date.now(),
10544
+ askContext: {
10545
+ phases: ctx.phases,
10546
+ currentPhase: phaseIndex,
10547
+ answers: {}
10548
+ }
10549
+ });
10550
+ log$12.info("已创建提问卡片操作", {
10551
+ operationId,
10552
+ phaseId: phase.id,
10553
+ phaseIndex,
10554
+ totalPhases,
10555
+ sessionId: ctx.sessionId
10556
+ });
10557
+ return {
10558
+ card,
10559
+ operationId
10560
+ };
10429
10561
  }
10430
10562
  //#endregion
10431
10563
  //#region src/messaging/inbound/dispatch-cc.ts
@@ -10843,16 +10975,6 @@ async function dispatchToCC(params) {
10843
10975
  }).catch((err) => {
10844
10976
  log$11.warn("记录飞书 assistant 消息到 store 失败", { error: String(err) });
10845
10977
  });
10846
- if (feishuAccumText) handleInteractiveCardTriggers({
10847
- text: feishuAccumText,
10848
- chatId,
10849
- sessionId: route.sessionId,
10850
- appId: account.configured ? account.appId : "",
10851
- brand: account.brand,
10852
- account,
10853
- replyInThread,
10854
- threadId: ctx.threadId
10855
- });
10856
10978
  },
10857
10979
  onError: (error) => {
10858
10980
  log$11.error("CC 进程错误", {
@@ -10869,6 +10991,19 @@ async function dispatchToCC(params) {
10869
10991
  maxTurns,
10870
10992
  maxBudgetUsd
10871
10993
  });
10994
+ clearSessionNoReplyFlag(route.sessionId);
10995
+ const mcpCallbackListener = (req) => {
10996
+ handleMcpToolCallback(req, {
10997
+ chatId,
10998
+ sessionId: route.sessionId,
10999
+ account,
11000
+ replyInThread,
11001
+ threadId: ctx.threadId,
11002
+ appId: account.configured ? account.appId : "",
11003
+ brand: account.brand
11004
+ });
11005
+ };
11006
+ onMcpCallback.on(`callback:${route.sessionId}`, mcpCallbackListener);
10872
11007
  try {
10873
11008
  await processManager.executePrompt({
10874
11009
  sessionId: route.sessionId,
@@ -10887,6 +11022,8 @@ async function dispatchToCC(params) {
10887
11022
  error: errorMessage
10888
11023
  });
10889
11024
  await cardController.onError(err instanceof Error ? err : new Error(errorMessage), { kind: "cc-dispatch" });
11025
+ } finally {
11026
+ onMcpCallback.removeListener(`callback:${route.sessionId}`, mcpCallbackListener);
10890
11027
  }
10891
11028
  }
10892
11029
  /**
@@ -11114,28 +11251,29 @@ async function dispatchTeammateEval(params) {
11114
11251
  }
11115
11252
  }
11116
11253
  /**
11117
- * 检测 CC 回复文本中的交互标记,并发送对应的独立卡片。
11254
+ * 处理 MCP Server 回调 — 根据 tool 类型发送对应的飞书交互卡片
11118
11255
  *
11119
- * 标记格式:
11120
- * - [PERMISSION_REQUEST] ```json { "scopes": ["scope1", "scope2"] } ```
11121
- * - [ASK_USER] ```json { "question": "...", "phases": [...] } ```
11256
+ * 与旧的文本标记方案不同,这里直接从 MCP tool 参数中获取结构化数据,
11257
+ * 不需要从流式文本中解析 JSON。卡片回调后通过 resolveMcpRequest 将答案
11258
+ * 返回给 MCP Server,CC 自动继续执行(无需重新注入消息)。
11122
11259
  */
11123
- async function handleInteractiveCardTriggers(params) {
11124
- const { text, chatId, sessionId, appId, brand, account, replyInThread, threadId } = params;
11260
+ async function handleMcpToolCallback(req, ctx) {
11261
+ const { chatId, account, replyInThread, threadId } = ctx;
11125
11262
  try {
11126
- const authReq = detectAuthRequest(text);
11127
- if (authReq && appId) {
11128
- log$11.info("检测到 CC 输出中的权限申请标记", {
11129
- sessionId,
11130
- scopes: authReq.scopes
11131
- });
11132
- const { card } = buildAuthCard({
11263
+ if (req.tool === "ask_user") {
11264
+ const params = req.params;
11265
+ log$11.info("[MCP] 收到 ask_user 回调,准备发送提问卡片", {
11266
+ requestId: req.requestId,
11267
+ sessionId: req.sessionId,
11268
+ question: params.question?.slice(0, 50)
11269
+ });
11270
+ const { card, operationId } = buildAskCard({
11133
11271
  chatId,
11134
- sessionId,
11135
- appId,
11136
- scopes: authReq.scopes,
11137
- brand
11272
+ sessionId: req.sessionId,
11273
+ question: params.question,
11274
+ phases: params.phases
11138
11275
  });
11276
+ mcpOperationMap.set(operationId, req.requestId);
11139
11277
  await LarkClient.fromAccount(account).sdk.im.message.create({
11140
11278
  params: { receive_id_type: "chat_id" },
11141
11279
  data: {
@@ -11145,23 +11283,25 @@ async function handleInteractiveCardTriggers(params) {
11145
11283
  ...replyInThread && threadId ? { reply_in_thread: true } : {}
11146
11284
  }
11147
11285
  });
11148
- log$11.info("权限申请卡片已发送", {
11149
- chatId,
11150
- sessionId
11286
+ log$11.info("[MCP] 提问卡片已发送", {
11287
+ requestId: req.requestId,
11288
+ operationId,
11289
+ chatId
11151
11290
  });
11152
- }
11153
- const askReq = detectAskRequest(text);
11154
- if (askReq) {
11155
- log$11.info("检测到 CC 输出中的提问标记", {
11156
- sessionId,
11157
- question: askReq.question.slice(0, 50)
11291
+ } else if (req.tool === "request_permission") {
11292
+ const params = req.params;
11293
+ log$11.info("[MCP] 收到 request_permission 回调,准备发送授权卡片", {
11294
+ requestId: req.requestId,
11295
+ scopes: params.scopes
11158
11296
  });
11159
- const { card } = buildAskCard({
11297
+ const { card, operationId } = buildAuthCard({
11160
11298
  chatId,
11161
- sessionId,
11162
- question: askReq.question,
11163
- phases: askReq.phases
11299
+ sessionId: req.sessionId,
11300
+ appId: ctx.appId,
11301
+ scopes: params.scopes,
11302
+ brand: ctx.brand
11164
11303
  });
11304
+ mcpOperationMap.set(operationId, req.requestId);
11165
11305
  await LarkClient.fromAccount(account).sdk.im.message.create({
11166
11306
  params: { receive_id_type: "chat_id" },
11167
11307
  data: {
@@ -11171,18 +11311,30 @@ async function handleInteractiveCardTriggers(params) {
11171
11311
  ...replyInThread && threadId ? { reply_in_thread: true } : {}
11172
11312
  }
11173
11313
  });
11174
- log$11.info("提问卡片已发送", {
11175
- chatId,
11176
- sessionId
11314
+ log$11.info("[MCP] 授权卡片已发送", {
11315
+ requestId: req.requestId,
11316
+ operationId,
11317
+ chatId
11177
11318
  });
11178
11319
  }
11179
11320
  } catch (err) {
11180
- log$11.error("交互式卡片后处理失败", {
11181
- sessionId,
11321
+ log$11.error("[MCP] 交互卡片发送失败", {
11322
+ requestId: req.requestId,
11323
+ tool: req.tool,
11182
11324
  error: err instanceof Error ? err.message : String(err)
11183
11325
  });
11326
+ resolveMcpRequest(req.requestId, {
11327
+ error: "卡片发送失败",
11328
+ details: String(err)
11329
+ });
11184
11330
  }
11185
11331
  }
11332
+ /**
11333
+ * 将飞书卡片 operationId 映射到 MCP requestId。
11334
+ * 当用户点击飞书卡片回调时,通过 operationId 找到对应的 MCP requestId,
11335
+ * 然后 resolve MCP request。
11336
+ */
11337
+ const mcpOperationMap = /* @__PURE__ */ new Map();
11186
11338
  //#endregion
11187
11339
  //#region src/messaging/inbound/permission.ts
11188
11340
  /**