@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 +936 -784
- package/dist/mcp-server.mjs +367 -0
- package/package.json +2 -1
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$
|
|
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$
|
|
399
|
+
logger$10.info("确保 ~/.claude 目录存在", { path: claudeDir });
|
|
399
400
|
await writeFile(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2), "utf-8");
|
|
400
|
-
logger$
|
|
401
|
-
if (await fileExists(claudeMdPath)) logger$
|
|
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$
|
|
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
|
|
823
|
+
* CC 进程启动时,将全局 MCP、内置 MCP(larkpal)和用户私有 MCP 合并后传入:
|
|
823
824
|
*
|
|
824
|
-
*
|
|
825
|
-
* /
|
|
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
|
-
*
|
|
841
|
+
* 获取 larkpal MCP Server 的可执行路径
|
|
839
842
|
*
|
|
840
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
1148
|
-
const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext, sessionId);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2380
|
+
logger$8.info("查询任务状态", { taskId });
|
|
2346
2381
|
const taskInfo = taskStore.get(taskId);
|
|
2347
2382
|
if (!taskInfo) {
|
|
2348
|
-
logger$
|
|
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$
|
|
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$
|
|
2407
|
+
logger$8.info("收到取消任务请求", { taskId });
|
|
2373
2408
|
const taskInfo = taskStore.get(taskId);
|
|
2374
2409
|
if (!taskInfo) {
|
|
2375
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2668
|
+
logger$7.info("从结果文件读取输出", {
|
|
2634
2669
|
resultFilePath,
|
|
2635
2670
|
contentLength: fileContent.length
|
|
2636
2671
|
});
|
|
2637
2672
|
finalResult = fileContent;
|
|
2638
2673
|
} catch {
|
|
2639
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2773
|
+
logger$7.warn("回调通知失败", {
|
|
2739
2774
|
callbackUrl,
|
|
2740
2775
|
status: response.status,
|
|
2741
2776
|
body: errText
|
|
2742
2777
|
});
|
|
2743
|
-
} else logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2864
|
+
logger$6.info("list skills response", { skillCount: skills.length });
|
|
2830
2865
|
res.json({ skills });
|
|
2831
2866
|
} catch (err) {
|
|
2832
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3060
|
+
logger$5.info("list sessions response", { sessionCount: sessions.length });
|
|
3026
3061
|
res.json({ sessions });
|
|
3027
3062
|
} catch (err) {
|
|
3028
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3165
|
+
logger$5.info("get persona response", { contentLength: content.length });
|
|
3131
3166
|
res.json({ content });
|
|
3132
3167
|
} catch (err) {
|
|
3133
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
3761
|
-
|
|
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
|
-
|
|
3966
|
+
参数说明:
|
|
3967
|
+
- scopes: 需要的权限列表(如 "contact:user.base:readonly")
|
|
3968
|
+
- reason: 申请原因说明
|
|
3784
3969
|
|
|
3785
|
-
|
|
3970
|
+
### signal_no_reply — 标记本次不回复
|
|
3971
|
+
在 teammate 评估等场景中,如果你决定不参与当前对话,调用此工具标记后即可正常结束。
|
|
3972
|
+
此工具即时返回,不阻塞。
|
|
3786
3973
|
|
|
3787
|
-
###
|
|
3788
|
-
- **需要用户回答、确认、选择** 的场景 → 必须使用
|
|
3974
|
+
### 何时使用 MCP 工具 vs lark-cli
|
|
3975
|
+
- **需要用户回答、确认、选择** 的场景 → 必须使用 ask_user 或 request_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/
|
|
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
|
-
|
|
5736
|
-
|
|
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
|
-
*
|
|
5740
|
-
*
|
|
5741
|
-
*
|
|
5742
|
-
*
|
|
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
|
|
5745
|
-
const
|
|
5746
|
-
const
|
|
5747
|
-
|
|
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
|
-
|
|
5750
|
-
|
|
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
|
-
*
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
6210
|
+
log$19.debug("textDelta 事件", {
|
|
6312
6211
|
deltaLen: text.length,
|
|
6313
6212
|
totalLen: this.accumulatedText.length
|
|
6314
6213
|
});
|
|
6315
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
8917
|
+
const log$16 = larkLogger("card/image-resolver");
|
|
9037
8918
|
/** Matches complete markdown image syntax: `` */
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
9737
|
+
log$14.info("abortCard completed (IM fallback)", { messageId: this.cardKit.cardMessageId });
|
|
9857
9738
|
}
|
|
9858
9739
|
} catch (err) {
|
|
9859
|
-
log$
|
|
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$
|
|
9755
|
+
log$14.warn("forceCloseStreaming: no card to close");
|
|
9875
9756
|
return;
|
|
9876
9757
|
}
|
|
9877
|
-
log$
|
|
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$
|
|
9808
|
+
log$14.info("forceCloseStreaming: 卡片已强制终态化");
|
|
9928
9809
|
if (!this.isTerminalPhase) this.finalizeCard("forceCloseStreaming", "abort");
|
|
9929
9810
|
} catch (err) {
|
|
9930
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
9940
|
+
log$14.debug("performFlush: skipping (CardKit streaming disabled, awaiting final update)");
|
|
10060
9941
|
return;
|
|
10061
9942
|
}
|
|
10062
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10402
|
-
*
|
|
10403
|
-
*
|
|
10434
|
+
* 每次显示一个 phase:
|
|
10435
|
+
* - 有预设选项时显示按钮组
|
|
10436
|
+
* - 允许自由文本时显示输入提示
|
|
10437
|
+
* - 底部有"跳过"和"提交"按钮
|
|
10404
10438
|
*/
|
|
10405
|
-
function
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
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
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
11254
|
+
* 处理 MCP Server 回调 — 根据 tool 类型发送对应的飞书交互卡片
|
|
11118
11255
|
*
|
|
11119
|
-
*
|
|
11120
|
-
*
|
|
11121
|
-
*
|
|
11256
|
+
* 与旧的文本标记方案不同,这里直接从 MCP tool 参数中获取结构化数据,
|
|
11257
|
+
* 不需要从流式文本中解析 JSON。卡片回调后通过 resolveMcpRequest 将答案
|
|
11258
|
+
* 返回给 MCP Server,CC 自动继续执行(无需重新注入消息)。
|
|
11122
11259
|
*/
|
|
11123
|
-
async function
|
|
11124
|
-
const {
|
|
11260
|
+
async function handleMcpToolCallback(req, ctx) {
|
|
11261
|
+
const { chatId, account, replyInThread, threadId } = ctx;
|
|
11125
11262
|
try {
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
log$11.info("
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
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
|
-
|
|
11136
|
-
|
|
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
|
-
|
|
11150
|
-
|
|
11286
|
+
log$11.info("[MCP] 提问卡片已发送", {
|
|
11287
|
+
requestId: req.requestId,
|
|
11288
|
+
operationId,
|
|
11289
|
+
chatId
|
|
11151
11290
|
});
|
|
11152
|
-
}
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
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 } =
|
|
11297
|
+
const { card, operationId } = buildAuthCard({
|
|
11160
11298
|
chatId,
|
|
11161
|
-
sessionId,
|
|
11162
|
-
|
|
11163
|
-
|
|
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
|
-
|
|
11176
|
-
|
|
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
|
-
|
|
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
|
/**
|