@vibe-lark/larkpal 0.1.41 → 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 -795
- 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,45 +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
|
-
/** 检查文本末尾是否可能是标记的不完整前缀 */
|
|
6309
|
-
static getPartialMarkerCutoff(text) {
|
|
6310
|
-
for (const marker of CCStreamBridge.INTERACTIVE_MARKER_PREFIXES) for (let len = marker.length - 1; len >= 2; len--) {
|
|
6311
|
-
const partial = marker.substring(0, len);
|
|
6312
|
-
if (text.endsWith(partial)) return text.length - len;
|
|
6313
|
-
}
|
|
6314
|
-
return -1;
|
|
6315
|
-
}
|
|
6316
|
-
/** 文本增量 → 累积后调用 controller.onPartialReply(实时过滤交互式标记) */
|
|
6207
|
+
/** 文本增量 → 累积后调用 controller.onPartialReply */
|
|
6317
6208
|
onTextDelta(text) {
|
|
6318
6209
|
this.accumulatedText += text;
|
|
6319
|
-
log$
|
|
6210
|
+
log$19.debug("textDelta 事件", {
|
|
6320
6211
|
deltaLen: text.length,
|
|
6321
6212
|
totalLen: this.accumulatedText.length
|
|
6322
6213
|
});
|
|
6323
|
-
|
|
6324
|
-
for (const prefix of CCStreamBridge.INTERACTIVE_MARKER_PREFIXES) {
|
|
6325
|
-
const idx = displayText.indexOf(prefix);
|
|
6326
|
-
if (idx !== -1) {
|
|
6327
|
-
displayText = displayText.substring(0, idx).trimEnd();
|
|
6328
|
-
this.controller.onPartialReply({ text: displayText });
|
|
6329
|
-
return;
|
|
6330
|
-
}
|
|
6331
|
-
}
|
|
6332
|
-
const cutoff = CCStreamBridge.getPartialMarkerCutoff(displayText);
|
|
6333
|
-
if (cutoff !== -1) displayText = displayText.substring(0, cutoff);
|
|
6334
|
-
this.controller.onPartialReply({ text: displayText });
|
|
6214
|
+
this.controller.onPartialReply({ text: this.accumulatedText });
|
|
6335
6215
|
}
|
|
6336
6216
|
/** 思考增量 → 累积后调用 controller.onReasoningStream */
|
|
6337
6217
|
onThinkingDelta(text) {
|
|
6338
6218
|
this.accumulatedThinkingText += text;
|
|
6339
|
-
log$
|
|
6219
|
+
log$19.debug("thinkingDelta 事件", {
|
|
6340
6220
|
deltaLen: text.length,
|
|
6341
6221
|
totalLen: this.accumulatedThinkingText.length
|
|
6342
6222
|
});
|
|
@@ -6348,7 +6228,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6348
6228
|
const displayName = getToolDisplayName(toolName);
|
|
6349
6229
|
const toolParams = typeof _toolInput === "object" && _toolInput !== null ? _toolInput : void 0;
|
|
6350
6230
|
const hasParams = toolParams && Object.keys(toolParams).length > 0;
|
|
6351
|
-
log$
|
|
6231
|
+
log$19.info("toolUseStart 事件", {
|
|
6352
6232
|
toolName,
|
|
6353
6233
|
displayName,
|
|
6354
6234
|
activeToolsCount: this.activeTools.size,
|
|
@@ -6361,7 +6241,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6361
6241
|
toolName,
|
|
6362
6242
|
toolParams
|
|
6363
6243
|
})) {
|
|
6364
|
-
log$
|
|
6244
|
+
log$19.info("toolUseStart: 更新已有步骤的 params", { toolName });
|
|
6365
6245
|
this.controller.onToolStart({
|
|
6366
6246
|
name: toolName,
|
|
6367
6247
|
phase: "start"
|
|
@@ -6385,7 +6265,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6385
6265
|
const toolName = this.activeTools.get(toolUseId) ?? this.lastToolName ?? "unknown";
|
|
6386
6266
|
const displayName = getToolDisplayName(toolName);
|
|
6387
6267
|
this.activeTools.delete(toolUseId);
|
|
6388
|
-
log$
|
|
6268
|
+
log$19.info("toolResult 事件", {
|
|
6389
6269
|
toolUseId,
|
|
6390
6270
|
toolName,
|
|
6391
6271
|
displayName,
|
|
@@ -6400,7 +6280,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6400
6280
|
}
|
|
6401
6281
|
/** 工具执行进度 → 记录日志 */
|
|
6402
6282
|
onToolProgress(toolName, elapsedSeconds) {
|
|
6403
|
-
log$
|
|
6283
|
+
log$19.debug("toolProgress 事件", {
|
|
6404
6284
|
toolName,
|
|
6405
6285
|
displayName: getToolDisplayName(toolName),
|
|
6406
6286
|
elapsedSeconds
|
|
@@ -6408,7 +6288,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6408
6288
|
}
|
|
6409
6289
|
/** 轮次结束 → 仅在 end_turn 时标记完成 + 触发 onIdle */
|
|
6410
6290
|
onTurnEnd(stopReason) {
|
|
6411
|
-
log$
|
|
6291
|
+
log$19.info("turnEnd 事件", {
|
|
6412
6292
|
stopReason,
|
|
6413
6293
|
accumulatedTextLen: this.accumulatedText.length,
|
|
6414
6294
|
accumulatedThinkingTextLen: this.accumulatedThinkingText.length
|
|
@@ -6416,7 +6296,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6416
6296
|
if (this.options.autoCompleteOnTurnEnd && stopReason === "end_turn") {
|
|
6417
6297
|
const trimmedText = this.accumulatedText.trim();
|
|
6418
6298
|
if (trimmedText === CC_INTERNAL_PLACEHOLDER) {
|
|
6419
|
-
log$
|
|
6299
|
+
log$19.info("检测到 CC 内部占位消息,静默丢弃", {
|
|
6420
6300
|
text: trimmedText.slice(0, 50),
|
|
6421
6301
|
sessionKey: this.options.sessionKey
|
|
6422
6302
|
});
|
|
@@ -6424,26 +6304,16 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6424
6304
|
return;
|
|
6425
6305
|
}
|
|
6426
6306
|
if (trimmedText === "") {
|
|
6427
|
-
log$
|
|
6307
|
+
log$19.info("turnEnd 时文本为空,延迟到 onResult 兜底处理", { sessionKey: this.options.sessionKey });
|
|
6428
6308
|
return;
|
|
6429
6309
|
}
|
|
6430
|
-
const strippedText = stripInteractiveMarkers(trimmedText);
|
|
6431
|
-
if (strippedText !== trimmedText) {
|
|
6432
|
-
log$18.info("turnEnd: 检测到交互式卡片标记,修剪卡片文本", {
|
|
6433
|
-
originalLen: trimmedText.length,
|
|
6434
|
-
strippedLen: strippedText.length,
|
|
6435
|
-
sessionKey: this.options.sessionKey
|
|
6436
|
-
});
|
|
6437
|
-
this.accumulatedText = strippedText;
|
|
6438
|
-
if (strippedText) this.controller.onPartialReply({ text: strippedText });
|
|
6439
|
-
}
|
|
6440
6310
|
this.controller.markFullyComplete();
|
|
6441
6311
|
this.controller.onIdle();
|
|
6442
6312
|
}
|
|
6443
6313
|
}
|
|
6444
6314
|
/** 最终结果 → 兜底最终化 + 错误处理 */
|
|
6445
6315
|
onResult(data) {
|
|
6446
|
-
log$
|
|
6316
|
+
log$19.info("result 事件", {
|
|
6447
6317
|
subtype: data.subtype,
|
|
6448
6318
|
isError: data.isError,
|
|
6449
6319
|
durationMs: data.durationMs,
|
|
@@ -6455,7 +6325,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6455
6325
|
const pm = ClaudeCodeAdapter.getInstance()?.getProcessManager();
|
|
6456
6326
|
const sessionId = this.options.sessionKey;
|
|
6457
6327
|
if (sessionId && pm ? pm.consumeAborted(sessionId) : false) {
|
|
6458
|
-
log$
|
|
6328
|
+
log$19.info("用户主动中断,按正常完成处理", {
|
|
6459
6329
|
subtype: data.subtype,
|
|
6460
6330
|
sessionId
|
|
6461
6331
|
});
|
|
@@ -6464,14 +6334,14 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6464
6334
|
this.controller.onIdle();
|
|
6465
6335
|
} else {
|
|
6466
6336
|
const errorMessage = data.result ?? `CC 执行失败: ${data.subtype}`;
|
|
6467
|
-
log$
|
|
6337
|
+
log$19.error("CC 执行返回错误", {
|
|
6468
6338
|
subtype: data.subtype,
|
|
6469
6339
|
errorMessage: errorMessage.slice(0, 200)
|
|
6470
6340
|
});
|
|
6471
6341
|
this.controller.onError(new Error(errorMessage), { kind: "cc-execution" });
|
|
6472
6342
|
}
|
|
6473
6343
|
} else if (!this.accumulatedText.trim()) if (data.result) {
|
|
6474
|
-
log$
|
|
6344
|
+
log$19.info("result 兜底:使用 result.result 作为最终回复", {
|
|
6475
6345
|
resultLen: data.result.length,
|
|
6476
6346
|
sessionKey: this.options.sessionKey
|
|
6477
6347
|
});
|
|
@@ -6479,7 +6349,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6479
6349
|
this.controller.markFullyComplete();
|
|
6480
6350
|
this.controller.onIdle();
|
|
6481
6351
|
} else {
|
|
6482
|
-
log$
|
|
6352
|
+
log$19.info("result 兜底:无文本且无 result,abort 卡片", { sessionKey: this.options.sessionKey });
|
|
6483
6353
|
this.controller.abortCard();
|
|
6484
6354
|
}
|
|
6485
6355
|
else {
|
|
@@ -6493,7 +6363,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6493
6363
|
* 内部复用上面的直接调用方法。
|
|
6494
6364
|
*/
|
|
6495
6365
|
bindParser(parser) {
|
|
6496
|
-
log$
|
|
6366
|
+
log$19.info("绑定 CCStreamParser 事件到卡片控制器");
|
|
6497
6367
|
parser.on("textDelta", (text) => this.onTextDelta(text));
|
|
6498
6368
|
parser.on("thinkingDelta", (text) => this.onThinkingDelta(text));
|
|
6499
6369
|
parser.on("toolUseStart", (toolName, toolInput) => this.onToolUseStart(toolName, toolInput));
|
|
@@ -6501,7 +6371,7 @@ var CCStreamBridge = class CCStreamBridge {
|
|
|
6501
6371
|
parser.on("toolProgress", (toolName, elapsedSeconds) => this.onToolProgress(toolName, elapsedSeconds));
|
|
6502
6372
|
parser.on("turnEnd", (stopReason) => this.onTurnEnd(stopReason));
|
|
6503
6373
|
parser.on("result", (data) => this.onResult(data));
|
|
6504
|
-
log$
|
|
6374
|
+
log$19.info("CCStreamParser 事件绑定完成");
|
|
6505
6375
|
}
|
|
6506
6376
|
};
|
|
6507
6377
|
//#endregion
|
|
@@ -8517,7 +8387,7 @@ function resolveLarkSdk(cfg, accountId) {
|
|
|
8517
8387
|
if (cached) return cached.sdk;
|
|
8518
8388
|
return LarkClient.fromCfg(cfg, accountId).sdk;
|
|
8519
8389
|
}
|
|
8520
|
-
const log$
|
|
8390
|
+
const log$18 = larkLogger("card/cardkit");
|
|
8521
8391
|
/**
|
|
8522
8392
|
* 记录 CardKit API 响应日志,检测错误码并抛出异常。
|
|
8523
8393
|
*
|
|
@@ -8527,13 +8397,13 @@ const log$17 = larkLogger("card/cardkit");
|
|
|
8527
8397
|
function logCardKitResponse(params) {
|
|
8528
8398
|
const { resp, api, context } = params;
|
|
8529
8399
|
const { code, msg } = resp;
|
|
8530
|
-
log$
|
|
8400
|
+
log$18.info(`cardkit ${api} response`, {
|
|
8531
8401
|
code,
|
|
8532
8402
|
msg,
|
|
8533
8403
|
context
|
|
8534
8404
|
});
|
|
8535
8405
|
if (code && code !== 0) {
|
|
8536
|
-
log$
|
|
8406
|
+
log$18.warn(`cardkit ${api} FAILED`, {
|
|
8537
8407
|
code,
|
|
8538
8408
|
msg,
|
|
8539
8409
|
context,
|
|
@@ -8944,7 +8814,7 @@ function validateLocalMediaRoots(filePath, localRoots) {
|
|
|
8944
8814
|
* Feishu messages, uploading media to the Feishu IM storage, and
|
|
8945
8815
|
* sending image / file messages to chats.
|
|
8946
8816
|
*/
|
|
8947
|
-
const log$
|
|
8817
|
+
const log$17 = larkLogger("outbound/media");
|
|
8948
8818
|
/**
|
|
8949
8819
|
* Upload an image to Feishu IM storage.
|
|
8950
8820
|
*
|
|
@@ -9012,7 +8882,7 @@ async function validateRemoteUrl(raw) {
|
|
|
9012
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}"`);
|
|
9013
8883
|
} catch (err) {
|
|
9014
8884
|
if (err instanceof Error && err.message.includes("SSRF protection")) throw err;
|
|
9015
|
-
log$
|
|
8885
|
+
log$17.warn(`[feishu-media] DNS resolution failed for "${hostname}": ${err}`);
|
|
9016
8886
|
}
|
|
9017
8887
|
}
|
|
9018
8888
|
/**
|
|
@@ -9030,21 +8900,21 @@ async function fetchMediaBuffer(urlOrPath, localRoots) {
|
|
|
9030
8900
|
if (localRoots !== void 0) validateLocalMediaRoots(filePath, localRoots);
|
|
9031
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.`);
|
|
9032
8902
|
const buf = fs.readFileSync(filePath);
|
|
9033
|
-
log$
|
|
8903
|
+
log$17.debug(`local file read: "${filePath}", ${buf.length} bytes`);
|
|
9034
8904
|
return buf;
|
|
9035
8905
|
}
|
|
9036
8906
|
await validateRemoteUrl(raw);
|
|
9037
8907
|
const FETCH_TIMEOUT_MS = 3e4;
|
|
9038
|
-
log$
|
|
8908
|
+
log$17.info(`fetching remote media: ${raw}`);
|
|
9039
8909
|
const response = await fetch(raw, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
9040
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.`);
|
|
9041
8911
|
const arrayBuffer = await response.arrayBuffer();
|
|
9042
|
-
log$
|
|
8912
|
+
log$17.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);
|
|
9043
8913
|
return Buffer.from(arrayBuffer);
|
|
9044
8914
|
}
|
|
9045
8915
|
//#endregion
|
|
9046
8916
|
//#region src/card/image-resolver.ts
|
|
9047
|
-
const log$
|
|
8917
|
+
const log$16 = larkLogger("card/image-resolver");
|
|
9048
8918
|
/** Matches complete markdown image syntax: `` */
|
|
9049
8919
|
const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
9050
8920
|
var ImageResolver = class {
|
|
@@ -9090,14 +8960,14 @@ var ImageResolver = class {
|
|
|
9090
8960
|
async resolveImagesAwait(text, timeoutMs) {
|
|
9091
8961
|
this.resolveImages(text);
|
|
9092
8962
|
if (this.pending.size > 0) {
|
|
9093
|
-
log$
|
|
8963
|
+
log$16.info("resolveImagesAwait: waiting for uploads", {
|
|
9094
8964
|
count: this.pending.size,
|
|
9095
8965
|
timeoutMs
|
|
9096
8966
|
});
|
|
9097
8967
|
const allUploads = Promise.all(this.pending.values());
|
|
9098
8968
|
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
9099
8969
|
await Promise.race([allUploads, timeout]);
|
|
9100
|
-
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 });
|
|
9101
8971
|
}
|
|
9102
8972
|
return this.resolveImages(text);
|
|
9103
8973
|
}
|
|
@@ -9107,7 +8977,7 @@ var ImageResolver = class {
|
|
|
9107
8977
|
}
|
|
9108
8978
|
async doUpload(url) {
|
|
9109
8979
|
try {
|
|
9110
|
-
log$
|
|
8980
|
+
log$16.info("uploading image", { url });
|
|
9111
8981
|
const buffer = await fetchRemoteImageBuffer(url);
|
|
9112
8982
|
const { imageKey } = await uploadImageLark({
|
|
9113
8983
|
cfg: this.cfg,
|
|
@@ -9115,7 +8985,7 @@ var ImageResolver = class {
|
|
|
9115
8985
|
imageType: "message",
|
|
9116
8986
|
accountId: this.accountId
|
|
9117
8987
|
});
|
|
9118
|
-
log$
|
|
8988
|
+
log$16.info("image uploaded", {
|
|
9119
8989
|
url,
|
|
9120
8990
|
imageKey
|
|
9121
8991
|
});
|
|
@@ -9124,7 +8994,7 @@ var ImageResolver = class {
|
|
|
9124
8994
|
this.onImageResolved();
|
|
9125
8995
|
return imageKey;
|
|
9126
8996
|
} catch (err) {
|
|
9127
|
-
log$
|
|
8997
|
+
log$16.warn("image upload failed", {
|
|
9128
8998
|
url,
|
|
9129
8999
|
error: String(err)
|
|
9130
9000
|
});
|
|
@@ -9145,7 +9015,7 @@ var ImageResolver = class {
|
|
|
9145
9015
|
* Encapsulates the terminateDueToUnavailable / shouldSkipForUnavailable
|
|
9146
9016
|
* logic previously scattered as closures in reply-dispatcher.ts.
|
|
9147
9017
|
*/
|
|
9148
|
-
const log$
|
|
9018
|
+
const log$15 = larkLogger("card/unavailable-guard");
|
|
9149
9019
|
var UnavailableGuard = class {
|
|
9150
9020
|
terminated = false;
|
|
9151
9021
|
replyToMessageId;
|
|
@@ -9198,7 +9068,7 @@ var UnavailableGuard = class {
|
|
|
9198
9068
|
this.terminated = true;
|
|
9199
9069
|
this.onTerminate();
|
|
9200
9070
|
const affectedMessageId = fromError?.messageId ?? this.replyToMessageId ?? cardMessageId ?? "unknown";
|
|
9201
|
-
log$
|
|
9071
|
+
log$15.warn("reply pipeline terminated by unavailable message", {
|
|
9202
9072
|
source,
|
|
9203
9073
|
apiCode,
|
|
9204
9074
|
messageId: affectedMessageId
|
|
@@ -9235,7 +9105,7 @@ function getActiveCard(sessionId) {
|
|
|
9235
9105
|
* Delegates throttling to FlushController and message-unavailable
|
|
9236
9106
|
* detection to UnavailableGuard.
|
|
9237
9107
|
*/
|
|
9238
|
-
const log$
|
|
9108
|
+
const log$14 = larkLogger("card/streaming");
|
|
9239
9109
|
var StreamingCardController = class StreamingCardController {
|
|
9240
9110
|
phase = "idle";
|
|
9241
9111
|
cardKit = {
|
|
@@ -9323,7 +9193,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9323
9193
|
}
|
|
9324
9194
|
}
|
|
9325
9195
|
if (!entry) {
|
|
9326
|
-
log$
|
|
9196
|
+
log$14.debug("footer metrics lookup: session entry missing", {
|
|
9327
9197
|
sessionKey: this.deps.sessionKey,
|
|
9328
9198
|
candidateKeys,
|
|
9329
9199
|
storePath,
|
|
@@ -9341,7 +9211,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9341
9211
|
contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : void 0,
|
|
9342
9212
|
model: typeof entry.model === "string" ? entry.model : void 0
|
|
9343
9213
|
};
|
|
9344
|
-
log$
|
|
9214
|
+
log$14.debug("footer metrics lookup: session entry found", {
|
|
9345
9215
|
sessionKey: this.deps.sessionKey,
|
|
9346
9216
|
matchedKey,
|
|
9347
9217
|
storePath,
|
|
@@ -9366,7 +9236,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9366
9236
|
}
|
|
9367
9237
|
}
|
|
9368
9238
|
if (!entry) {
|
|
9369
|
-
log$
|
|
9239
|
+
log$14.debug("footer metrics lookup: session entry missing", {
|
|
9370
9240
|
sessionKey: this.deps.sessionKey,
|
|
9371
9241
|
candidateKeys,
|
|
9372
9242
|
storePath,
|
|
@@ -9384,7 +9254,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9384
9254
|
contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : void 0,
|
|
9385
9255
|
model: typeof entry.model === "string" ? entry.model : void 0
|
|
9386
9256
|
};
|
|
9387
|
-
log$
|
|
9257
|
+
log$14.debug("footer metrics lookup: session entry found", {
|
|
9388
9258
|
sessionKey: this.deps.sessionKey,
|
|
9389
9259
|
matchedKey,
|
|
9390
9260
|
storePath,
|
|
@@ -9392,7 +9262,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9392
9262
|
});
|
|
9393
9263
|
return metrics;
|
|
9394
9264
|
} catch (err) {
|
|
9395
|
-
log$
|
|
9265
|
+
log$14.warn("footer metrics lookup failed", {
|
|
9396
9266
|
error: String(err),
|
|
9397
9267
|
sessionKey: this.deps.sessionKey
|
|
9398
9268
|
});
|
|
@@ -9499,7 +9369,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9499
9369
|
const from = this.phase;
|
|
9500
9370
|
if (from === to) return false;
|
|
9501
9371
|
if (!PHASE_TRANSITIONS[from].has(to)) {
|
|
9502
|
-
log$
|
|
9372
|
+
log$14.warn("phase transition rejected", {
|
|
9503
9373
|
from,
|
|
9504
9374
|
to,
|
|
9505
9375
|
source
|
|
@@ -9507,7 +9377,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9507
9377
|
return false;
|
|
9508
9378
|
}
|
|
9509
9379
|
this.phase = to;
|
|
9510
|
-
log$
|
|
9380
|
+
log$14.info("phase transition", {
|
|
9511
9381
|
from,
|
|
9512
9382
|
to,
|
|
9513
9383
|
source,
|
|
@@ -9627,7 +9497,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9627
9497
|
this.reasoning.dirty = true;
|
|
9628
9498
|
}
|
|
9629
9499
|
const text = split.answerText ?? stripReasoningTags(rawText);
|
|
9630
|
-
log$
|
|
9500
|
+
log$14.debug("onPartialReply", { len: text.length });
|
|
9631
9501
|
if (!text) return;
|
|
9632
9502
|
this.captureToolUseElapsed();
|
|
9633
9503
|
if (!this.reasoning.reasoningStartTime) this.reasoning.reasoningStartTime = Date.now();
|
|
@@ -9639,7 +9509,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9639
9509
|
this.text.lastPartialText = text;
|
|
9640
9510
|
this.text.accumulatedText = this.text.streamingPrefix ? this.text.streamingPrefix + "\n\n" + text : text;
|
|
9641
9511
|
if (!this.text.streamingPrefix && SILENT_REPLY_TOKEN.startsWith(this.text.accumulatedText.trim())) {
|
|
9642
|
-
log$
|
|
9512
|
+
log$14.debug("onPartialReply: buffering NO_REPLY prefix");
|
|
9643
9513
|
return;
|
|
9644
9514
|
}
|
|
9645
9515
|
await this.ensureCardCreated();
|
|
@@ -9649,7 +9519,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9649
9519
|
}
|
|
9650
9520
|
async onError(err, info) {
|
|
9651
9521
|
if (this.guard.terminate("onError", err)) return;
|
|
9652
|
-
log$
|
|
9522
|
+
log$14.error(`${info.kind} reply failed`, { error: String(err) });
|
|
9653
9523
|
this.captureToolUseElapsed();
|
|
9654
9524
|
this.finalizeCard("onError", "error");
|
|
9655
9525
|
await this.flush.waitForFlush();
|
|
@@ -9705,7 +9575,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9705
9575
|
if (this.cardKit.cardMessageId) {
|
|
9706
9576
|
const isNoReplyLeak = !this.text.completedText && SILENT_REPLY_TOKEN.startsWith(this.text.accumulatedText.trim());
|
|
9707
9577
|
const displayText = this.text.completedText || (isNoReplyLeak ? "" : this.text.accumulatedText) || "Done.";
|
|
9708
|
-
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");
|
|
9709
9579
|
const resolvedDisplayText = await this.imageResolver.resolveImagesAwait(displayText, 15e3);
|
|
9710
9580
|
const idleToolUseDisplay = this.computeToolUseDisplay();
|
|
9711
9581
|
const terminalContent = prepareTerminalCardContent({
|
|
@@ -9733,7 +9603,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9733
9603
|
const rewindSessionId = this.deps.sessionKey;
|
|
9734
9604
|
const rewindMessageId = ClaudeCodeAdapter.getInstance()?.getProcessManager()?.getLastUserMessageId(this.deps.sessionKey);
|
|
9735
9605
|
const rewindHasFileChanges = this.hasFileChangingTools(idleToolUseDisplay?.steps);
|
|
9736
|
-
log$
|
|
9606
|
+
log$14.info("onIdle: rewind button conditions", {
|
|
9737
9607
|
sessionId: rewindSessionId,
|
|
9738
9608
|
messageId: rewindMessageId,
|
|
9739
9609
|
hasFileChanges: rewindHasFileChanges,
|
|
@@ -9746,7 +9616,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9746
9616
|
if (idleEffectiveCardId) {
|
|
9747
9617
|
const seqBeforeClose = this.cardKit.cardKitSequence;
|
|
9748
9618
|
this.cardKit.cardKitSequence += 1;
|
|
9749
|
-
log$
|
|
9619
|
+
log$14.info("onIdle: closing streaming mode", {
|
|
9750
9620
|
attempt,
|
|
9751
9621
|
seqBefore: seqBeforeClose,
|
|
9752
9622
|
seqAfter: this.cardKit.cardKitSequence
|
|
@@ -9760,7 +9630,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9760
9630
|
});
|
|
9761
9631
|
const seqBeforeUpdate = this.cardKit.cardKitSequence;
|
|
9762
9632
|
this.cardKit.cardKitSequence += 1;
|
|
9763
|
-
log$
|
|
9633
|
+
log$14.info("onIdle: updating final card", {
|
|
9764
9634
|
attempt,
|
|
9765
9635
|
seqBefore: seqBeforeUpdate,
|
|
9766
9636
|
seqAfter: this.cardKit.cardKitSequence
|
|
@@ -9778,33 +9648,33 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9778
9648
|
card: completeCard,
|
|
9779
9649
|
accountId: this.deps.accountId
|
|
9780
9650
|
});
|
|
9781
|
-
log$
|
|
9651
|
+
log$14.info("reply completed, card finalized", {
|
|
9782
9652
|
elapsedMs: this.elapsed(),
|
|
9783
9653
|
isCardKit: !!idleEffectiveCardId,
|
|
9784
9654
|
attempt
|
|
9785
9655
|
});
|
|
9786
9656
|
break;
|
|
9787
9657
|
} catch (retryErr) {
|
|
9788
|
-
log$
|
|
9658
|
+
log$14.warn("final card update attempt failed", {
|
|
9789
9659
|
attempt,
|
|
9790
9660
|
maxRetries: MAX_FINAL_UPDATE_RETRIES,
|
|
9791
9661
|
error: String(retryErr)
|
|
9792
9662
|
});
|
|
9793
9663
|
if (attempt < MAX_FINAL_UPDATE_RETRIES) await new Promise((resolve) => setTimeout(resolve, FINAL_UPDATE_RETRY_DELAY_MS * attempt));
|
|
9794
|
-
else log$
|
|
9664
|
+
else log$14.error("final card update exhausted all retries, card may remain in streaming state", {
|
|
9795
9665
|
cardId: idleEffectiveCardId,
|
|
9796
9666
|
messageId: this.cardKit.cardMessageId
|
|
9797
9667
|
});
|
|
9798
9668
|
}
|
|
9799
9669
|
}
|
|
9800
9670
|
} catch (err) {
|
|
9801
|
-
log$
|
|
9671
|
+
log$14.error("final card update failed (outer)", { error: String(err) });
|
|
9802
9672
|
} finally {
|
|
9803
9673
|
clearToolUseTraceRun(this.deps.sessionKey);
|
|
9804
9674
|
}
|
|
9805
9675
|
}
|
|
9806
9676
|
markFullyComplete() {
|
|
9807
|
-
log$
|
|
9677
|
+
log$14.debug("markFullyComplete", {
|
|
9808
9678
|
completedTextLen: this.text.completedText.length,
|
|
9809
9679
|
accumulatedTextLen: this.text.accumulatedText.length
|
|
9810
9680
|
});
|
|
@@ -9843,7 +9713,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9843
9713
|
footerMetrics
|
|
9844
9714
|
});
|
|
9845
9715
|
await this.closeStreamingAndUpdate(effectiveCardId, abortCardContent, "abortCard");
|
|
9846
|
-
log$
|
|
9716
|
+
log$14.info("abortCard completed", { effectiveCardId });
|
|
9847
9717
|
} else if (this.cardKit.cardMessageId) {
|
|
9848
9718
|
const abortCard = buildCardContent("complete", {
|
|
9849
9719
|
text: terminalContent.text,
|
|
@@ -9864,10 +9734,10 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9864
9734
|
card: abortCard,
|
|
9865
9735
|
accountId: this.deps.accountId
|
|
9866
9736
|
});
|
|
9867
|
-
log$
|
|
9737
|
+
log$14.info("abortCard completed (IM fallback)", { messageId: this.cardKit.cardMessageId });
|
|
9868
9738
|
}
|
|
9869
9739
|
} catch (err) {
|
|
9870
|
-
log$
|
|
9740
|
+
log$14.warn("abortCard failed", { error: String(err) });
|
|
9871
9741
|
} finally {
|
|
9872
9742
|
clearToolUseTraceRun(this.deps.sessionKey);
|
|
9873
9743
|
}
|
|
@@ -9882,10 +9752,10 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9882
9752
|
async forceCloseStreaming() {
|
|
9883
9753
|
const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
|
|
9884
9754
|
if (!effectiveCardId && !this.cardKit.cardMessageId) {
|
|
9885
|
-
log$
|
|
9755
|
+
log$14.warn("forceCloseStreaming: no card to close");
|
|
9886
9756
|
return;
|
|
9887
9757
|
}
|
|
9888
|
-
log$
|
|
9758
|
+
log$14.info("forceCloseStreaming: 强制终态化卡片", {
|
|
9889
9759
|
cardId: effectiveCardId,
|
|
9890
9760
|
messageId: this.cardKit.cardMessageId,
|
|
9891
9761
|
phase: this.phase
|
|
@@ -9935,10 +9805,10 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9935
9805
|
card: completeCard,
|
|
9936
9806
|
accountId: this.deps.accountId
|
|
9937
9807
|
});
|
|
9938
|
-
log$
|
|
9808
|
+
log$14.info("forceCloseStreaming: 卡片已强制终态化");
|
|
9939
9809
|
if (!this.isTerminalPhase) this.finalizeCard("forceCloseStreaming", "abort");
|
|
9940
9810
|
} catch (err) {
|
|
9941
|
-
log$
|
|
9811
|
+
log$14.error("forceCloseStreaming failed", { error: String(err) });
|
|
9942
9812
|
} finally {
|
|
9943
9813
|
unregisterActiveCard(this.deps.sessionKey);
|
|
9944
9814
|
clearToolUseTraceRun(this.deps.sessionKey);
|
|
@@ -9962,7 +9832,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9962
9832
|
this.disposeShutdownHook = null;
|
|
9963
9833
|
return;
|
|
9964
9834
|
}
|
|
9965
|
-
log$
|
|
9835
|
+
log$14.info("reusing placeholder card", {
|
|
9966
9836
|
cardId: this.deps.placeholderCardId,
|
|
9967
9837
|
messageId: this.deps.placeholderMessageId
|
|
9968
9838
|
});
|
|
@@ -9986,7 +9856,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9986
9856
|
accountId: this.deps.accountId
|
|
9987
9857
|
});
|
|
9988
9858
|
if (this.isStaleCreate(epoch)) {
|
|
9989
|
-
log$
|
|
9859
|
+
log$14.info("ensureCardCreated: stale epoch after createCardEntity, bailing out", {
|
|
9990
9860
|
epoch,
|
|
9991
9861
|
phase: this.phase
|
|
9992
9862
|
});
|
|
@@ -9997,7 +9867,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
9997
9867
|
this.cardKit.originalCardKitCardId = cId;
|
|
9998
9868
|
this.cardKit.cardKitSequence = 1;
|
|
9999
9869
|
this.disposeShutdownHook = registerShutdownHook(`streaming-card:${cId}`, () => this.abortCard());
|
|
10000
|
-
log$
|
|
9870
|
+
log$14.info("created CardKit entity", {
|
|
10001
9871
|
cardId: cId,
|
|
10002
9872
|
initialSequence: this.cardKit.cardKitSequence
|
|
10003
9873
|
});
|
|
@@ -10010,7 +9880,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10010
9880
|
accountId: this.deps.accountId
|
|
10011
9881
|
});
|
|
10012
9882
|
if (this.isStaleCreate(epoch)) {
|
|
10013
|
-
log$
|
|
9883
|
+
log$14.info("ensureCardCreated: stale epoch after sendCardByCardId, bailing out", {
|
|
10014
9884
|
epoch,
|
|
10015
9885
|
phase: this.phase
|
|
10016
9886
|
});
|
|
@@ -10025,13 +9895,13 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10025
9895
|
this.disposeShutdownHook = null;
|
|
10026
9896
|
return;
|
|
10027
9897
|
}
|
|
10028
|
-
log$
|
|
9898
|
+
log$14.info("sent CardKit card", { messageId: result.messageId });
|
|
10029
9899
|
} else throw new Error("card.create returned empty card_id");
|
|
10030
9900
|
} catch (cardKitErr) {
|
|
10031
9901
|
if (this.isStaleCreate(epoch)) return;
|
|
10032
9902
|
if (this.guard.terminate("ensureCardCreated.cardkitFlow", cardKitErr)) return;
|
|
10033
9903
|
const apiDetail = extractApiDetail(cardKitErr);
|
|
10034
|
-
log$
|
|
9904
|
+
log$14.warn("CardKit flow failed, falling back to IM", { apiDetail });
|
|
10035
9905
|
this.cardKit.cardKitCardId = null;
|
|
10036
9906
|
this.cardKit.originalCardKitCardId = null;
|
|
10037
9907
|
const fallbackCard = buildCardContent("streaming", { showToolUse: this.deps.toolUseDisplay.showToolUse });
|
|
@@ -10044,7 +9914,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10044
9914
|
accountId: this.deps.accountId
|
|
10045
9915
|
});
|
|
10046
9916
|
if (this.isStaleCreate(epoch)) {
|
|
10047
|
-
log$
|
|
9917
|
+
log$14.info("ensureCardCreated: stale epoch after IM fallback send, bailing out", {
|
|
10048
9918
|
epoch,
|
|
10049
9919
|
phase: this.phase
|
|
10050
9920
|
});
|
|
@@ -10053,12 +9923,12 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10053
9923
|
this.cardKit.cardMessageId = result.messageId;
|
|
10054
9924
|
this.flush.setCardMessageReady(true);
|
|
10055
9925
|
if (!this.transition("streaming", "ensureCardCreated.imFallback")) return;
|
|
10056
|
-
log$
|
|
9926
|
+
log$14.info("sent fallback IM card", { messageId: result.messageId });
|
|
10057
9927
|
}
|
|
10058
9928
|
} catch (err) {
|
|
10059
9929
|
if (this.isStaleCreate(epoch)) return;
|
|
10060
9930
|
if (this.guard.terminate("ensureCardCreated.outer", err)) return;
|
|
10061
|
-
log$
|
|
9931
|
+
log$14.warn("thinking card failed, falling back to static", { error: String(err) });
|
|
10062
9932
|
this.transition("creation_failed", "ensureCardCreated.outer", "creation_failed");
|
|
10063
9933
|
}
|
|
10064
9934
|
})();
|
|
@@ -10067,10 +9937,10 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10067
9937
|
async performFlush() {
|
|
10068
9938
|
if (!this.cardKit.cardMessageId || this.isTerminalPhase) return;
|
|
10069
9939
|
if (!this.cardKit.cardKitCardId && this.cardKit.originalCardKitCardId) {
|
|
10070
|
-
log$
|
|
9940
|
+
log$14.debug("performFlush: skipping (CardKit streaming disabled, awaiting final update)");
|
|
10071
9941
|
return;
|
|
10072
9942
|
}
|
|
10073
|
-
log$
|
|
9943
|
+
log$14.debug("flushCardUpdate: enter", {
|
|
10074
9944
|
seq: this.cardKit.cardKitSequence,
|
|
10075
9945
|
isCardKit: !!this.cardKit.cardKitCardId
|
|
10076
9946
|
});
|
|
@@ -10092,7 +9962,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10092
9962
|
reasoningText: this.reasoning.accumulatedReasoningText || void 0
|
|
10093
9963
|
});
|
|
10094
9964
|
this.cardKit.cardKitSequence += 1;
|
|
10095
|
-
log$
|
|
9965
|
+
log$14.debug("flushCardUpdate: full card update (dirty)", {
|
|
10096
9966
|
seq: this.cardKit.cardKitSequence,
|
|
10097
9967
|
stepCount: display?.stepCount,
|
|
10098
9968
|
hasReasoning: !!this.reasoning.accumulatedReasoningText
|
|
@@ -10108,7 +9978,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10108
9978
|
} else if (resolvedText !== this.text.lastFlushedText) {
|
|
10109
9979
|
const prevSeq = this.cardKit.cardKitSequence;
|
|
10110
9980
|
this.cardKit.cardKitSequence += 1;
|
|
10111
|
-
log$
|
|
9981
|
+
log$14.debug("flushCardUpdate: answer seq bump", {
|
|
10112
9982
|
seqBefore: prevSeq,
|
|
10113
9983
|
seqAfter: this.cardKit.cardKitSequence
|
|
10114
9984
|
});
|
|
@@ -10123,7 +9993,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10123
9993
|
this.text.lastFlushedText = resolvedText;
|
|
10124
9994
|
}
|
|
10125
9995
|
} else {
|
|
10126
|
-
log$
|
|
9996
|
+
log$14.debug("flushCardUpdate: IM patch fallback");
|
|
10127
9997
|
const flushDisplay = this.computeToolUseDisplay();
|
|
10128
9998
|
const card = buildCardContent("streaming", {
|
|
10129
9999
|
text: this.reasoning.isReasoningPhase ? "" : resolvedText,
|
|
@@ -10143,31 +10013,31 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10143
10013
|
if (this.guard.terminate("flushCardUpdate", err)) return;
|
|
10144
10014
|
const apiCode = extractLarkApiCode(err);
|
|
10145
10015
|
if (isCardRateLimitError(err)) {
|
|
10146
|
-
log$
|
|
10016
|
+
log$14.info("flushCardUpdate: rate limited (230020), skipping", { seq: this.cardKit.cardKitSequence });
|
|
10147
10017
|
return;
|
|
10148
10018
|
}
|
|
10149
10019
|
if (isCardTableLimitError(err)) {
|
|
10150
|
-
log$
|
|
10020
|
+
log$14.warn("flushCardUpdate: card table limit exceeded (230099/11310), disabling CardKit streaming", { seq: this.cardKit.cardKitSequence });
|
|
10151
10021
|
this.cardKit.cardKitCardId = null;
|
|
10152
10022
|
return;
|
|
10153
10023
|
}
|
|
10154
10024
|
if (apiCode === 300317 && this.cardKit.cardKitCardId) {
|
|
10155
10025
|
const oldSeq = this.cardKit.cardKitSequence;
|
|
10156
10026
|
this.cardKit.cardKitSequence += 100;
|
|
10157
|
-
log$
|
|
10027
|
+
log$14.warn("flushCardUpdate: sequence conflict (300317), jumping sequence", {
|
|
10158
10028
|
oldSeq,
|
|
10159
10029
|
newSeq: this.cardKit.cardKitSequence
|
|
10160
10030
|
});
|
|
10161
10031
|
return;
|
|
10162
10032
|
}
|
|
10163
10033
|
const apiDetail = extractApiDetail(err);
|
|
10164
|
-
log$
|
|
10034
|
+
log$14.error("card stream update failed", {
|
|
10165
10035
|
apiCode,
|
|
10166
10036
|
seq: this.cardKit.cardKitSequence,
|
|
10167
10037
|
apiDetail
|
|
10168
10038
|
});
|
|
10169
10039
|
if (this.cardKit.cardKitCardId) {
|
|
10170
|
-
log$
|
|
10040
|
+
log$14.warn("disabling CardKit streaming, falling back to im.message.patch");
|
|
10171
10041
|
this.cardKit.cardKitCardId = null;
|
|
10172
10042
|
}
|
|
10173
10043
|
}
|
|
@@ -10192,7 +10062,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10192
10062
|
if (!this.cardKit.cardKitCardId || this.isTerminalPhase) return;
|
|
10193
10063
|
try {
|
|
10194
10064
|
const display = this.computeToolUseDisplay();
|
|
10195
|
-
log$
|
|
10065
|
+
log$14.info("updateToolUseStatus", {
|
|
10196
10066
|
hasDisplay: !!display,
|
|
10197
10067
|
stepCount: display?.stepCount,
|
|
10198
10068
|
steps: display?.steps?.map((s) => `${s.title}:${s.status}`).join(", ")
|
|
@@ -10214,7 +10084,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10214
10084
|
accountId: this.deps.accountId
|
|
10215
10085
|
});
|
|
10216
10086
|
} catch (err) {
|
|
10217
|
-
log$
|
|
10087
|
+
log$14.debug("updateToolUseStatus failed", { error: String(err) });
|
|
10218
10088
|
}
|
|
10219
10089
|
}
|
|
10220
10090
|
finalizeCard(source, reason) {
|
|
@@ -10226,7 +10096,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10226
10096
|
async closeStreamingAndUpdate(cardId, card, label) {
|
|
10227
10097
|
const seqBeforeClose = this.cardKit.cardKitSequence;
|
|
10228
10098
|
this.cardKit.cardKitSequence += 1;
|
|
10229
|
-
log$
|
|
10099
|
+
log$14.info(`${label}: closing streaming mode`, {
|
|
10230
10100
|
seqBefore: seqBeforeClose,
|
|
10231
10101
|
seqAfter: this.cardKit.cardKitSequence
|
|
10232
10102
|
});
|
|
@@ -10239,7 +10109,7 @@ var StreamingCardController = class StreamingCardController {
|
|
|
10239
10109
|
});
|
|
10240
10110
|
const seqBeforeUpdate = this.cardKit.cardKitSequence;
|
|
10241
10111
|
this.cardKit.cardKitSequence += 1;
|
|
10242
|
-
log$
|
|
10112
|
+
log$14.info(`${label}: updating card`, {
|
|
10243
10113
|
seqBefore: seqBeforeUpdate,
|
|
10244
10114
|
seqAfter: this.cardKit.cardKitSequence
|
|
10245
10115
|
});
|
|
@@ -10273,7 +10143,7 @@ function extractApiDetail(err) {
|
|
|
10273
10143
|
}
|
|
10274
10144
|
//#endregion
|
|
10275
10145
|
//#region src/messaging/format-for-cc.ts
|
|
10276
|
-
const log$
|
|
10146
|
+
const log$13 = larkLogger("messaging/format-for-cc");
|
|
10277
10147
|
function safeParseJSON(raw) {
|
|
10278
10148
|
try {
|
|
10279
10149
|
return JSON.parse(raw);
|
|
@@ -10329,7 +10199,7 @@ function extractPostText(rawContent) {
|
|
|
10329
10199
|
*/
|
|
10330
10200
|
function formatContentByType(ctx) {
|
|
10331
10201
|
const { contentType, content, resources } = ctx;
|
|
10332
|
-
log$
|
|
10202
|
+
log$13.debug("formatContentByType 开始格式化", {
|
|
10333
10203
|
contentType,
|
|
10334
10204
|
contentLength: content?.length ?? 0,
|
|
10335
10205
|
resourceCount: resources?.length ?? 0
|
|
@@ -10351,92 +10221,343 @@ function formatContentByType(ctx) {
|
|
|
10351
10221
|
const extracted = extractPostText(content);
|
|
10352
10222
|
if (extracted !== content) return extracted;
|
|
10353
10223
|
}
|
|
10354
|
-
return content;
|
|
10355
|
-
case "merge_forward": return content;
|
|
10356
|
-
case "audio": return content || "[语音消息]";
|
|
10357
|
-
case "sticker": return content || "[表情贴纸]";
|
|
10358
|
-
case "video":
|
|
10359
|
-
case "media": return content || "[视频消息]";
|
|
10360
|
-
case "interactive": return content;
|
|
10361
|
-
case "share_chat":
|
|
10362
|
-
case "share_user":
|
|
10363
|
-
case "share_calendar_event": return content;
|
|
10364
|
-
case "location": return content;
|
|
10365
|
-
case "system":
|
|
10366
|
-
case "folder":
|
|
10367
|
-
case "hongbao":
|
|
10368
|
-
case "calendar":
|
|
10369
|
-
case "general_calendar":
|
|
10370
|
-
case "video_chat":
|
|
10371
|
-
case "todo":
|
|
10372
|
-
case "vote": return content;
|
|
10373
|
-
default:
|
|
10374
|
-
log$
|
|
10375
|
-
return `[不支持的消息类型: ${contentType}]`;
|
|
10376
|
-
}
|
|
10377
|
-
}
|
|
10378
|
-
/**
|
|
10379
|
-
* 将消息创建时间格式化为 AI 可理解的时间描述。
|
|
10380
|
-
* 包含两部分:绝对时间(ISO 格式)+ 相对时间(如"5分钟前")。
|
|
10381
|
-
* 这让 AI 能判断消息的时效性,避免将很久前的讨论当作当前上下文。
|
|
10382
|
-
*/
|
|
10383
|
-
function formatMessageTime(createTime) {
|
|
10384
|
-
if (!createTime) return "";
|
|
10385
|
-
const msgDate = new Date(createTime);
|
|
10386
|
-
const diffMs = Date.now() - createTime;
|
|
10387
|
-
const absTime = msgDate.toLocaleString("zh-CN", {
|
|
10388
|
-
month: "2-digit",
|
|
10389
|
-
day: "2-digit",
|
|
10390
|
-
hour: "2-digit",
|
|
10391
|
-
minute: "2-digit",
|
|
10392
|
-
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
|
+
}
|
|
10393
10420
|
});
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10421
|
+
log$12.info("已创建授权卡片操作", {
|
|
10422
|
+
operationId,
|
|
10423
|
+
scopes: ctx.scopes,
|
|
10424
|
+
sessionId: ctx.sessionId
|
|
10425
|
+
});
|
|
10426
|
+
return {
|
|
10427
|
+
card,
|
|
10428
|
+
operationId
|
|
10429
|
+
};
|
|
10401
10430
|
}
|
|
10402
10431
|
/**
|
|
10403
|
-
*
|
|
10404
|
-
*
|
|
10405
|
-
* 格式说明(提供 context,不 control):
|
|
10406
|
-
* - 私聊:[04/25 14:30 (5分钟前)] 消息内容
|
|
10407
|
-
* - 群聊:[04/25 14:30 (5分钟前)] [张三] 消息内容
|
|
10408
|
-
* - 话题:[04/25 14:30 (5分钟前)] [张三] [话题回复] 消息内容
|
|
10409
|
-
*
|
|
10410
|
-
* 时间信息让 AI 能自主判断消息的时效性和上下文连贯性。
|
|
10432
|
+
* 构建多 phase 提问卡片(CardKit v2 格式)
|
|
10411
10433
|
*
|
|
10412
|
-
*
|
|
10413
|
-
*
|
|
10414
|
-
*
|
|
10434
|
+
* 每次显示一个 phase:
|
|
10435
|
+
* - 有预设选项时显示按钮组
|
|
10436
|
+
* - 允许自由文本时显示输入提示
|
|
10437
|
+
* - 底部有"跳过"和"提交"按钮
|
|
10415
10438
|
*/
|
|
10416
|
-
function
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
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
|
|
10425
10447
|
});
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
if (timeStr) parts.push(`[${timeStr}]`);
|
|
10430
|
-
if (isGroup && ctx.senderName) parts.push(`[${ctx.senderName}]`);
|
|
10431
|
-
if (ctx.threadId) parts.push("[话题回复]");
|
|
10432
|
-
const result = (parts.length > 0 ? parts.join(" ") + " " : "") + formattedContent;
|
|
10433
|
-
log$12.info("formatForCC 完成", {
|
|
10434
|
-
messageId: ctx.messageId,
|
|
10435
|
-
resultLength: result.length,
|
|
10436
|
-
hasTime: !!timeStr,
|
|
10437
|
-
hasThread: !!ctx.threadId
|
|
10448
|
+
if (phase.description) elements.push({
|
|
10449
|
+
tag: "markdown",
|
|
10450
|
+
content: phase.description
|
|
10438
10451
|
});
|
|
10439
|
-
|
|
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
|
+
};
|
|
10440
10561
|
}
|
|
10441
10562
|
//#endregion
|
|
10442
10563
|
//#region src/messaging/inbound/dispatch-cc.ts
|
|
@@ -10854,16 +10975,6 @@ async function dispatchToCC(params) {
|
|
|
10854
10975
|
}).catch((err) => {
|
|
10855
10976
|
log$11.warn("记录飞书 assistant 消息到 store 失败", { error: String(err) });
|
|
10856
10977
|
});
|
|
10857
|
-
if (feishuAccumText) handleInteractiveCardTriggers({
|
|
10858
|
-
text: feishuAccumText,
|
|
10859
|
-
chatId,
|
|
10860
|
-
sessionId: route.sessionId,
|
|
10861
|
-
appId: account.configured ? account.appId : "",
|
|
10862
|
-
brand: account.brand,
|
|
10863
|
-
account,
|
|
10864
|
-
replyInThread,
|
|
10865
|
-
threadId: ctx.threadId
|
|
10866
|
-
});
|
|
10867
10978
|
},
|
|
10868
10979
|
onError: (error) => {
|
|
10869
10980
|
log$11.error("CC 进程错误", {
|
|
@@ -10880,6 +10991,19 @@ async function dispatchToCC(params) {
|
|
|
10880
10991
|
maxTurns,
|
|
10881
10992
|
maxBudgetUsd
|
|
10882
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);
|
|
10883
11007
|
try {
|
|
10884
11008
|
await processManager.executePrompt({
|
|
10885
11009
|
sessionId: route.sessionId,
|
|
@@ -10898,6 +11022,8 @@ async function dispatchToCC(params) {
|
|
|
10898
11022
|
error: errorMessage
|
|
10899
11023
|
});
|
|
10900
11024
|
await cardController.onError(err instanceof Error ? err : new Error(errorMessage), { kind: "cc-dispatch" });
|
|
11025
|
+
} finally {
|
|
11026
|
+
onMcpCallback.removeListener(`callback:${route.sessionId}`, mcpCallbackListener);
|
|
10901
11027
|
}
|
|
10902
11028
|
}
|
|
10903
11029
|
/**
|
|
@@ -11125,28 +11251,29 @@ async function dispatchTeammateEval(params) {
|
|
|
11125
11251
|
}
|
|
11126
11252
|
}
|
|
11127
11253
|
/**
|
|
11128
|
-
*
|
|
11254
|
+
* 处理 MCP Server 回调 — 根据 tool 类型发送对应的飞书交互卡片
|
|
11129
11255
|
*
|
|
11130
|
-
*
|
|
11131
|
-
*
|
|
11132
|
-
*
|
|
11256
|
+
* 与旧的文本标记方案不同,这里直接从 MCP tool 参数中获取结构化数据,
|
|
11257
|
+
* 不需要从流式文本中解析 JSON。卡片回调后通过 resolveMcpRequest 将答案
|
|
11258
|
+
* 返回给 MCP Server,CC 自动继续执行(无需重新注入消息)。
|
|
11133
11259
|
*/
|
|
11134
|
-
async function
|
|
11135
|
-
const {
|
|
11260
|
+
async function handleMcpToolCallback(req, ctx) {
|
|
11261
|
+
const { chatId, account, replyInThread, threadId } = ctx;
|
|
11136
11262
|
try {
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
log$11.info("
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
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({
|
|
11144
11271
|
chatId,
|
|
11145
|
-
sessionId,
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
brand
|
|
11272
|
+
sessionId: req.sessionId,
|
|
11273
|
+
question: params.question,
|
|
11274
|
+
phases: params.phases
|
|
11149
11275
|
});
|
|
11276
|
+
mcpOperationMap.set(operationId, req.requestId);
|
|
11150
11277
|
await LarkClient.fromAccount(account).sdk.im.message.create({
|
|
11151
11278
|
params: { receive_id_type: "chat_id" },
|
|
11152
11279
|
data: {
|
|
@@ -11156,23 +11283,25 @@ async function handleInteractiveCardTriggers(params) {
|
|
|
11156
11283
|
...replyInThread && threadId ? { reply_in_thread: true } : {}
|
|
11157
11284
|
}
|
|
11158
11285
|
});
|
|
11159
|
-
log$11.info("
|
|
11160
|
-
|
|
11161
|
-
|
|
11286
|
+
log$11.info("[MCP] 提问卡片已发送", {
|
|
11287
|
+
requestId: req.requestId,
|
|
11288
|
+
operationId,
|
|
11289
|
+
chatId
|
|
11162
11290
|
});
|
|
11163
|
-
}
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
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
|
|
11169
11296
|
});
|
|
11170
|
-
const { card } =
|
|
11297
|
+
const { card, operationId } = buildAuthCard({
|
|
11171
11298
|
chatId,
|
|
11172
|
-
sessionId,
|
|
11173
|
-
|
|
11174
|
-
|
|
11299
|
+
sessionId: req.sessionId,
|
|
11300
|
+
appId: ctx.appId,
|
|
11301
|
+
scopes: params.scopes,
|
|
11302
|
+
brand: ctx.brand
|
|
11175
11303
|
});
|
|
11304
|
+
mcpOperationMap.set(operationId, req.requestId);
|
|
11176
11305
|
await LarkClient.fromAccount(account).sdk.im.message.create({
|
|
11177
11306
|
params: { receive_id_type: "chat_id" },
|
|
11178
11307
|
data: {
|
|
@@ -11182,18 +11311,30 @@ async function handleInteractiveCardTriggers(params) {
|
|
|
11182
11311
|
...replyInThread && threadId ? { reply_in_thread: true } : {}
|
|
11183
11312
|
}
|
|
11184
11313
|
});
|
|
11185
|
-
log$11.info("
|
|
11186
|
-
|
|
11187
|
-
|
|
11314
|
+
log$11.info("[MCP] 授权卡片已发送", {
|
|
11315
|
+
requestId: req.requestId,
|
|
11316
|
+
operationId,
|
|
11317
|
+
chatId
|
|
11188
11318
|
});
|
|
11189
11319
|
}
|
|
11190
11320
|
} catch (err) {
|
|
11191
|
-
log$11.error("
|
|
11192
|
-
|
|
11321
|
+
log$11.error("[MCP] 交互卡片发送失败", {
|
|
11322
|
+
requestId: req.requestId,
|
|
11323
|
+
tool: req.tool,
|
|
11193
11324
|
error: err instanceof Error ? err.message : String(err)
|
|
11194
11325
|
});
|
|
11326
|
+
resolveMcpRequest(req.requestId, {
|
|
11327
|
+
error: "卡片发送失败",
|
|
11328
|
+
details: String(err)
|
|
11329
|
+
});
|
|
11195
11330
|
}
|
|
11196
11331
|
}
|
|
11332
|
+
/**
|
|
11333
|
+
* 将飞书卡片 operationId 映射到 MCP requestId。
|
|
11334
|
+
* 当用户点击飞书卡片回调时,通过 operationId 找到对应的 MCP requestId,
|
|
11335
|
+
* 然后 resolve MCP request。
|
|
11336
|
+
*/
|
|
11337
|
+
const mcpOperationMap = /* @__PURE__ */ new Map();
|
|
11197
11338
|
//#endregion
|
|
11198
11339
|
//#region src/messaging/inbound/permission.ts
|
|
11199
11340
|
/**
|