@vibe-lark/larkpal 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +396 -72
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -221,7 +221,7 @@ function ensureLarkCliConfig() {
221
221
  * 首次启动时检测并生成 ~/.claude/settings.json 和 ~/.claude/CLAUDE.md,
222
222
  * 以及确保会话工作目录结构完整。
223
223
  */
224
- const logger$7 = larkLogger("config/defaults");
224
+ const logger$8 = larkLogger("config/defaults");
225
225
  const DEFAULT_SETTINGS = {
226
226
  permissions: {
227
227
  allow: [
@@ -344,13 +344,13 @@ async function ensureDefaults() {
344
344
  const settingsPath = join(claudeDir, "settings.json");
345
345
  const claudeMdPath = join(claudeDir, "CLAUDE.md");
346
346
  await mkdir(claudeDir, { recursive: true });
347
- logger$7.info("确保 ~/.claude 目录存在", { path: claudeDir });
347
+ logger$8.info("确保 ~/.claude 目录存在", { path: claudeDir });
348
348
  await writeFile(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2), "utf-8");
349
- logger$7.info("settings.json 已同步(强制覆盖)", { path: settingsPath });
350
- if (await fileExists(claudeMdPath)) logger$7.info("CLAUDE.md 已存在,跳过生成", { path: claudeMdPath });
349
+ logger$8.info("settings.json 已同步(强制覆盖)", { path: settingsPath });
350
+ if (await fileExists(claudeMdPath)) logger$8.info("CLAUDE.md 已存在,跳过生成", { path: claudeMdPath });
351
351
  else {
352
352
  await writeFile(claudeMdPath, DEFAULT_CLAUDE_MD, "utf-8");
353
- logger$7.info("已生成默认 CLAUDE.md", { path: claudeMdPath });
353
+ logger$8.info("已生成默认 CLAUDE.md", { path: claudeMdPath });
354
354
  }
355
355
  }
356
356
  //#endregion
@@ -435,7 +435,10 @@ var CCStreamParser = class extends EventEmitter {
435
435
  this.handleResult(msg);
436
436
  break;
437
437
  case "system":
438
- log$28.info("收到 system 消息", { subtype: msg.subtype });
438
+ log$28.info("收到 system 消息", {
439
+ subtype: msg.subtype,
440
+ detail: JSON.stringify(msg).slice(0, 500)
441
+ });
439
442
  this.emit("system", msg);
440
443
  break;
441
444
  default:
@@ -1654,7 +1657,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
1654
1657
  * 群聊 → /workspace/chats/group_{chat_id}/
1655
1658
  * 话题 → /workspace/chats/group_{chat_id}/topics/{thread_id}/
1656
1659
  */
1657
- const logger$6 = larkLogger("routing/session-router");
1660
+ const logger$7 = larkLogger("routing/session-router");
1658
1661
  const SESSION_OUTPUT_RULES = [
1659
1662
  `## 内容输出优先级`,
1660
1663
  "",
@@ -1693,7 +1696,7 @@ var SessionRouter = class {
1693
1696
  cwd: path.join(this.workspaceRoot, "chats", `p2p_${safeId}`),
1694
1697
  type: "p2p"
1695
1698
  };
1696
- logger$6.debug("解析私聊路由", {
1699
+ logger$7.debug("解析私聊路由", {
1697
1700
  chatId,
1698
1701
  userId: safeId,
1699
1702
  sessionId: route.sessionId,
@@ -1709,7 +1712,7 @@ var SessionRouter = class {
1709
1712
  cwd: path.join(groupCwd, "topics", threadId),
1710
1713
  type: "topic"
1711
1714
  };
1712
- logger$6.debug("解析话题路由", {
1715
+ logger$7.debug("解析话题路由", {
1713
1716
  chatId,
1714
1717
  threadId,
1715
1718
  sessionId: route.sessionId,
@@ -1722,7 +1725,7 @@ var SessionRouter = class {
1722
1725
  cwd: groupCwd,
1723
1726
  type: "group"
1724
1727
  };
1725
- logger$6.debug("解析群聊路由", {
1728
+ logger$7.debug("解析群聊路由", {
1726
1729
  chatId,
1727
1730
  sessionId: route.sessionId,
1728
1731
  cwd: route.cwd
@@ -1736,7 +1739,7 @@ var SessionRouter = class {
1736
1739
  * 此方法是幂等的,多次调用不会报错也不会覆盖已有文件。
1737
1740
  */
1738
1741
  async ensureSessionDirectory(route) {
1739
- logger$6.info("确保会话目录存在", {
1742
+ logger$7.info("确保会话目录存在", {
1740
1743
  sessionId: route.sessionId,
1741
1744
  cwd: route.cwd
1742
1745
  });
@@ -1746,7 +1749,7 @@ var SessionRouter = class {
1746
1749
  const claudeMdPath = path.join(route.cwd, "CLAUDE.md");
1747
1750
  if (!existsSync$1(claudeMdPath)) {
1748
1751
  await writeFile$1(claudeMdPath, this.generateClaudeMd(route), "utf-8");
1749
- logger$6.info("创建会话级 CLAUDE.md", {
1752
+ logger$7.info("创建会话级 CLAUDE.md", {
1750
1753
  path: claudeMdPath,
1751
1754
  type: route.type
1752
1755
  });
@@ -1810,7 +1813,7 @@ var SessionRouter = class {
1810
1813
  *
1811
1814
  * 内部通过 taskStore 维护所有任务的生命周期状态。
1812
1815
  */
1813
- const logger$5 = larkLogger("gateway/execute");
1816
+ const logger$6 = larkLogger("gateway/execute");
1814
1817
  /** 全局任务存储:taskId → TaskInfo */
1815
1818
  const taskStore = /* @__PURE__ */ new Map();
1816
1819
  /**
@@ -1844,7 +1847,7 @@ function createExecuteRouter(processManager) {
1844
1847
  */
1845
1848
  async function handleExecute(req, res, processManager) {
1846
1849
  const body = req.body;
1847
- logger$5.info("收到执行请求", {
1850
+ logger$6.info("收到执行请求", {
1848
1851
  session_id: body.session_id,
1849
1852
  cwd: body.cwd,
1850
1853
  prompt: body.prompt?.slice(0, 200),
@@ -1853,7 +1856,7 @@ async function handleExecute(req, res, processManager) {
1853
1856
  max_budget_usd: body.max_budget_usd
1854
1857
  });
1855
1858
  if (!body.session_id || !body.cwd || !body.prompt) {
1856
- logger$5.warn("执行请求参数缺失", {
1859
+ logger$6.warn("执行请求参数缺失", {
1857
1860
  hasSessionId: !!body.session_id,
1858
1861
  hasCwd: !!body.cwd,
1859
1862
  hasPrompt: !!body.prompt
@@ -1873,7 +1876,7 @@ async function handleExecute(req, res, processManager) {
1873
1876
  createdAt: /* @__PURE__ */ new Date()
1874
1877
  };
1875
1878
  taskStore.set(taskId, taskInfo);
1876
- logger$5.info("任务已创建", {
1879
+ logger$6.info("任务已创建", {
1877
1880
  taskId,
1878
1881
  sessionId: body.session_id,
1879
1882
  mode
@@ -1889,7 +1892,7 @@ async function handleExecute(req, res, processManager) {
1889
1892
  const callbacks = createTaskCallbacks(taskId);
1890
1893
  processManager.executePrompt(config, callbacks).catch((err) => {
1891
1894
  const errorMsg = err instanceof Error ? err.message : String(err);
1892
- logger$5.error("异步任务执行异常", {
1895
+ logger$6.error("异步任务执行异常", {
1893
1896
  taskId,
1894
1897
  error: errorMsg
1895
1898
  });
@@ -1897,7 +1900,7 @@ async function handleExecute(req, res, processManager) {
1897
1900
  taskInfo.error = errorMsg;
1898
1901
  taskInfo.completedAt = /* @__PURE__ */ new Date();
1899
1902
  });
1900
- logger$5.info("异步任务已启动,立即返回", { taskId });
1903
+ logger$6.info("异步任务已启动,立即返回", { taskId });
1901
1904
  res.status(202).json({
1902
1905
  task_id: taskId,
1903
1906
  status: "running"
@@ -1905,8 +1908,8 @@ async function handleExecute(req, res, processManager) {
1905
1908
  return;
1906
1909
  }
1907
1910
  try {
1908
- const result = await executeAndWaitResult(taskId, config, processManager);
1909
- logger$5.info("同步任务执行完成", {
1911
+ const result = await executeAndWaitResult$1(taskId, config, processManager);
1912
+ logger$6.info("同步任务执行完成", {
1910
1913
  taskId,
1911
1914
  resultSubtype: result?.subtype
1912
1915
  });
@@ -1917,7 +1920,7 @@ async function handleExecute(req, res, processManager) {
1917
1920
  });
1918
1921
  } catch (err) {
1919
1922
  const errorMsg = err instanceof Error ? err.message : String(err);
1920
- logger$5.error("同步任务执行失败", {
1923
+ logger$6.error("同步任务执行失败", {
1921
1924
  taskId,
1922
1925
  error: errorMsg
1923
1926
  });
@@ -1935,12 +1938,12 @@ async function handleExecute(req, res, processManager) {
1935
1938
  */
1936
1939
  async function handleBatchExecute(req, res, processManager) {
1937
1940
  const body = req.body;
1938
- logger$5.info("收到批量执行请求", {
1941
+ logger$6.info("收到批量执行请求", {
1939
1942
  taskCount: body.tasks?.length,
1940
1943
  concurrency: body.concurrency
1941
1944
  });
1942
1945
  if (!body.tasks || !Array.isArray(body.tasks) || body.tasks.length === 0) {
1943
- logger$5.warn("批量执行请求参数缺失: tasks 为空或格式错误");
1946
+ logger$6.warn("批量执行请求参数缺失: tasks 为空或格式错误");
1944
1947
  res.status(400).json({
1945
1948
  error: "Bad Request",
1946
1949
  message: "Missing or empty required field: tasks"
@@ -1950,7 +1953,7 @@ async function handleBatchExecute(req, res, processManager) {
1950
1953
  for (let i = 0; i < body.tasks.length; i++) {
1951
1954
  const task = body.tasks[i];
1952
1955
  if (!task?.session_id || !task?.cwd || !task?.prompt) {
1953
- logger$5.warn("批量执行子任务参数缺失", { index: i });
1956
+ logger$6.warn("批量执行子任务参数缺失", { index: i });
1954
1957
  res.status(400).json({
1955
1958
  error: "Bad Request",
1956
1959
  message: `Task at index ${i} is missing required fields: session_id, cwd, prompt`
@@ -1981,7 +1984,7 @@ async function handleBatchExecute(req, res, processManager) {
1981
1984
  }
1982
1985
  };
1983
1986
  });
1984
- logger$5.info("批量任务已创建", {
1987
+ logger$6.info("批量任务已创建", {
1985
1988
  batchId,
1986
1989
  taskIds,
1987
1990
  concurrency
@@ -1998,17 +2001,17 @@ async function handleBatchExecute(req, res, processManager) {
1998
2001
  */
1999
2002
  function handleGetTask(req, res) {
2000
2003
  const taskId = req.params.taskId;
2001
- logger$5.info("查询任务状态", { taskId });
2004
+ logger$6.info("查询任务状态", { taskId });
2002
2005
  const taskInfo = taskStore.get(taskId);
2003
2006
  if (!taskInfo) {
2004
- logger$5.warn("任务不存在", { taskId });
2007
+ logger$6.warn("任务不存在", { taskId });
2005
2008
  res.status(404).json({
2006
2009
  error: "Not Found",
2007
2010
  message: `Task ${taskId} not found`
2008
2011
  });
2009
2012
  return;
2010
2013
  }
2011
- logger$5.info("返回任务状态", {
2014
+ logger$6.info("返回任务状态", {
2012
2015
  taskId,
2013
2016
  status: taskInfo.status
2014
2017
  });
@@ -2025,10 +2028,10 @@ function handleGetTask(req, res) {
2025
2028
  */
2026
2029
  async function handleCancelTask(req, res, processManager) {
2027
2030
  const taskId = req.params.taskId;
2028
- logger$5.info("收到取消任务请求", { taskId });
2031
+ logger$6.info("收到取消任务请求", { taskId });
2029
2032
  const taskInfo = taskStore.get(taskId);
2030
2033
  if (!taskInfo) {
2031
- logger$5.warn("取消失败:任务不存在", { taskId });
2034
+ logger$6.warn("取消失败:任务不存在", { taskId });
2032
2035
  res.status(404).json({
2033
2036
  error: "Not Found",
2034
2037
  message: `Task ${taskId} not found`
@@ -2036,7 +2039,7 @@ async function handleCancelTask(req, res, processManager) {
2036
2039
  return;
2037
2040
  }
2038
2041
  if (taskInfo.status !== "running") {
2039
- logger$5.warn("取消失败:任务不在运行状态", {
2042
+ logger$6.warn("取消失败:任务不在运行状态", {
2040
2043
  taskId,
2041
2044
  status: taskInfo.status
2042
2045
  });
@@ -2050,7 +2053,7 @@ async function handleCancelTask(req, res, processManager) {
2050
2053
  await processManager.stopProcess(taskInfo.sessionId);
2051
2054
  taskInfo.status = "cancelled";
2052
2055
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2053
- logger$5.info("任务已取消", {
2056
+ logger$6.info("任务已取消", {
2054
2057
  taskId,
2055
2058
  sessionId: taskInfo.sessionId
2056
2059
  });
@@ -2060,7 +2063,7 @@ async function handleCancelTask(req, res, processManager) {
2060
2063
  });
2061
2064
  } catch (err) {
2062
2065
  const errorMsg = err instanceof Error ? err.message : String(err);
2063
- logger$5.error("取消任务时发生错误", {
2066
+ logger$6.error("取消任务时发生错误", {
2064
2067
  taskId,
2065
2068
  error: errorMsg
2066
2069
  });
@@ -2083,7 +2086,7 @@ function createTaskCallbacks(taskId) {
2083
2086
  taskInfo.result = result;
2084
2087
  taskInfo.status = result.isError ? "failed" : "completed";
2085
2088
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2086
- logger$5.info("任务收到结果", {
2089
+ logger$6.info("任务收到结果", {
2087
2090
  taskId,
2088
2091
  status: taskInfo.status,
2089
2092
  subtype: result.subtype,
@@ -2097,7 +2100,7 @@ function createTaskCallbacks(taskId) {
2097
2100
  taskInfo.status = "failed";
2098
2101
  taskInfo.error = error.message;
2099
2102
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2100
- logger$5.error("任务执行出错", {
2103
+ logger$6.error("任务执行出错", {
2101
2104
  taskId,
2102
2105
  error: error.message
2103
2106
  });
@@ -2109,7 +2112,7 @@ function createTaskCallbacks(taskId) {
2109
2112
  *
2110
2113
  * 通过 Promise 包装回调机制,在收到 onResult 或 onError 时 resolve/reject。
2111
2114
  */
2112
- function executeAndWaitResult(taskId, config, processManager) {
2115
+ function executeAndWaitResult$1(taskId, config, processManager) {
2113
2116
  return new Promise((resolve, reject) => {
2114
2117
  processManager.executePrompt(config, {
2115
2118
  onResult: (result) => {
@@ -2119,7 +2122,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2119
2122
  taskInfo.status = result.isError ? "failed" : "completed";
2120
2123
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2121
2124
  }
2122
- logger$5.info("同步任务收到结果", {
2125
+ logger$6.info("同步任务收到结果", {
2123
2126
  taskId,
2124
2127
  subtype: result.subtype,
2125
2128
  durationMs: result.durationMs,
@@ -2134,7 +2137,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2134
2137
  taskInfo.error = error.message;
2135
2138
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2136
2139
  }
2137
- logger$5.error("同步任务执行出错", {
2140
+ logger$6.error("同步任务执行出错", {
2138
2141
  taskId,
2139
2142
  error: error.message
2140
2143
  });
@@ -2142,7 +2145,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2142
2145
  }
2143
2146
  }).catch((err) => {
2144
2147
  const errorMsg = err instanceof Error ? err.message : String(err);
2145
- logger$5.error("同步任务 executePrompt 调用失败", {
2148
+ logger$6.error("同步任务 executePrompt 调用失败", {
2146
2149
  taskId,
2147
2150
  error: errorMsg
2148
2151
  });
@@ -2162,7 +2165,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2162
2165
  * 使用简单的信号量模式控制并发数,逐个启动任务直到所有任务完成。
2163
2166
  */
2164
2167
  async function runBatchTasks(taskConfigs, concurrency, processManager) {
2165
- logger$5.info("开始批量任务执行", {
2168
+ logger$6.info("开始批量任务执行", {
2166
2169
  totalTasks: taskConfigs.length,
2167
2170
  concurrency
2168
2171
  });
@@ -2174,7 +2177,7 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2174
2177
  await processManager.executePrompt(config, callbacks);
2175
2178
  } catch (err) {
2176
2179
  const errorMsg = err instanceof Error ? err.message : String(err);
2177
- logger$5.error("批量子任务执行异常", {
2180
+ logger$6.error("批量子任务执行异常", {
2178
2181
  taskId,
2179
2182
  error: errorMsg
2180
2183
  });
@@ -2191,7 +2194,311 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2191
2194
  if (executing.size >= concurrency) await Promise.race(executing);
2192
2195
  }
2193
2196
  await Promise.all(executing);
2194
- logger$5.info("批量任务全部完成", { totalTasks: taskConfigs.length });
2197
+ logger$6.info("批量任务全部完成", { totalTasks: taskConfigs.length });
2198
+ }
2199
+ //#endregion
2200
+ //#region src/gateway/ai-extract-handler.ts
2201
+ /**
2202
+ * AI 数据提取路由 — 统一 AI 能力网关
2203
+ *
2204
+ * 提供 POST /api/ai/extract 接口,接收外部服务(如 rd-assistant-api)的结构化数据提取请求。
2205
+ * 内部通过 CC(Claude Code)RuntimeAdapter 执行 AI 提取任务。
2206
+ *
2207
+ * 支持两种模式:
2208
+ * - 同步模式(默认):等待 CC 完成后直接返回提取结果
2209
+ * - 异步模式:立即返回,完成后通过 X-Callback-Url 回调通知
2210
+ *
2211
+ * 鉴权:通过 X-Internal-Secret 头验证服务间调用身份
2212
+ */
2213
+ const logger$5 = larkLogger("gateway/ai-extract");
2214
+ /** 内部通信密钥,从环境变量读取 */
2215
+ const INTERNAL_SECRET = process.env.LARKPAL_API_SECRET || "dev-internal-secret";
2216
+ /** CC 执行时的工作目录(使用临时目录,不需要真正的项目上下文) */
2217
+ const AI_EXTRACT_CWD = process.env.LARKPAL_AI_EXTRACT_CWD || "/tmp/larkpal-ai-extract";
2218
+ /**
2219
+ * 创建 AI 提取路由
2220
+ *
2221
+ * @param runtimeAdapter - CC 运行时适配器实例,用于执行 AI 提取 prompt
2222
+ */
2223
+ function createAiExtractRouter(runtimeAdapter) {
2224
+ const router = Router();
2225
+ router.post("/api/ai/extract", (req, res) => {
2226
+ handleExtract(req, res, runtimeAdapter);
2227
+ });
2228
+ return router;
2229
+ }
2230
+ async function handleExtract(req, res, runtimeAdapter) {
2231
+ const secret = req.headers["x-internal-secret"];
2232
+ if (secret !== INTERNAL_SECRET) {
2233
+ logger$5.warn("AI 提取请求鉴权失败", { providedSecret: secret?.slice(0, 8) + "..." });
2234
+ res.status(401).json({
2235
+ code: 1,
2236
+ message: "鉴权失败"
2237
+ });
2238
+ return;
2239
+ }
2240
+ const callbackUrl = req.headers["x-callback-url"];
2241
+ const body = req.body;
2242
+ if (!body.taskId || !body.fileName || !body.fileBase64 || !body.moduleName) {
2243
+ logger$5.warn("AI 提取请求参数不完整", {
2244
+ hasTaskId: !!body.taskId,
2245
+ hasFileName: !!body.fileName,
2246
+ hasFileBase64: !!body.fileBase64,
2247
+ hasModuleName: !!body.moduleName
2248
+ });
2249
+ res.status(400).json({
2250
+ code: 1,
2251
+ message: "缺少必要参数: taskId, fileName, fileBase64, moduleName"
2252
+ });
2253
+ return;
2254
+ }
2255
+ logger$5.info("收到 AI 提取请求", {
2256
+ taskId: body.taskId,
2257
+ fileName: body.fileName,
2258
+ moduleName: body.moduleName,
2259
+ extractionType: body.extractionType,
2260
+ indicatorCount: body.indicators?.length || 0,
2261
+ fileSize: body.fileBase64.length,
2262
+ hasCallback: !!callbackUrl
2263
+ });
2264
+ const prompt = buildExtractionPrompt(body);
2265
+ try {
2266
+ const { mkdir, writeFile } = await import("node:fs/promises");
2267
+ const { join } = await import("node:path");
2268
+ await mkdir(AI_EXTRACT_CWD, { recursive: true });
2269
+ const fileExt = body.fileName.split(".").pop() || "bin";
2270
+ const tempFilePath = join(AI_EXTRACT_CWD, `${body.taskId}.${fileExt}`);
2271
+ const fileBuffer = Buffer.from(body.fileBase64, "base64");
2272
+ await writeFile(tempFilePath, fileBuffer);
2273
+ logger$5.info("临时文件已写入", {
2274
+ tempFilePath,
2275
+ sizeBytes: fileBuffer.length
2276
+ });
2277
+ const sessionId = `ai-extract-${body.taskId}`;
2278
+ const outputFilePath = join(AI_EXTRACT_CWD, `${body.taskId}-result.json`);
2279
+ const result = await executeAndWaitResult(runtimeAdapter, {
2280
+ sessionId,
2281
+ cwd: AI_EXTRACT_CWD,
2282
+ prompt,
2283
+ maxTurns: 10
2284
+ });
2285
+ let extractedData = [];
2286
+ try {
2287
+ const { readFile } = await import("node:fs/promises");
2288
+ const fileContent = await readFile(outputFilePath, "utf-8");
2289
+ logger$5.info("从结果文件读取提取数据", {
2290
+ outputFilePath,
2291
+ contentLength: fileContent.length
2292
+ });
2293
+ extractedData = parseExtractionResult(fileContent);
2294
+ } catch {
2295
+ logger$5.info("结果文件不存在,从 CC 文本输出解析");
2296
+ extractedData = parseExtractionResult(result);
2297
+ }
2298
+ try {
2299
+ const { unlink } = await import("node:fs/promises");
2300
+ await unlink(tempFilePath);
2301
+ await unlink(outputFilePath).catch(() => {});
2302
+ } catch {}
2303
+ logger$5.info("AI 提取完成", {
2304
+ taskId: body.taskId,
2305
+ recordCount: extractedData.length
2306
+ });
2307
+ if (callbackUrl) {
2308
+ res.status(200).json({
2309
+ code: 0,
2310
+ message: "accepted",
2311
+ mode: "async"
2312
+ });
2313
+ await sendCallback(callbackUrl, body.taskId, "success", extractedData);
2314
+ } else res.status(200).json({
2315
+ code: 0,
2316
+ message: "ok",
2317
+ mode: "sync",
2318
+ data: extractedData
2319
+ });
2320
+ } catch (err) {
2321
+ const errorMsg = err instanceof Error ? err.message : String(err);
2322
+ logger$5.error("AI 提取执行失败", {
2323
+ taskId: body.taskId,
2324
+ error: errorMsg
2325
+ });
2326
+ if (callbackUrl) {
2327
+ res.status(200).json({
2328
+ code: 0,
2329
+ message: "accepted",
2330
+ mode: "async"
2331
+ });
2332
+ await sendCallback(callbackUrl, body.taskId, "failed", void 0, errorMsg);
2333
+ } else res.status(500).json({
2334
+ code: 1,
2335
+ message: `提取失败: ${errorMsg}`
2336
+ });
2337
+ }
2338
+ }
2339
+ /**
2340
+ * 构造用于数据提取的结构化 Prompt
2341
+ */
2342
+ function buildExtractionPrompt(body) {
2343
+ const indicatorsList = (body.indicators || []).map((ind, i) => ` ${i + 1}. ${ind.name}${ind.unit ? ` (单位: ${ind.unit})` : ""}${ind.format ? ` [格式: ${ind.format}]` : ""}`).join("\n");
2344
+ const fileExt = body.fileName.split(".").pop() || "";
2345
+ return `你是一个精确的数据提取助手。请从提供的文件中提取结构化性能测试数据。
2346
+
2347
+ ## 任务说明
2348
+ - 文件名: ${body.fileName}
2349
+ - 性能模块: ${body.moduleName}
2350
+ - 提取方式: ${body.extractionType}
2351
+
2352
+ ## 需要提取的指标
2353
+ ${indicatorsList}
2354
+
2355
+ ## 文件路径
2356
+ 文件已保存在当前目录: ./${body.taskId}.${fileExt}
2357
+
2358
+ ## 输出要求
2359
+ 请读取文件内容,提取出所有样品/配方的性能数据。
2360
+
2361
+ **必须以严格 JSON 格式输出**,不要包含任何其他文字。输出格式如下:
2362
+ \`\`\`json
2363
+ [
2364
+ {
2365
+ "formulaNo": "样品/配方编号(如果文件中有)",
2366
+ "indicators": {
2367
+ "指标名称1": 数值或字符串,
2368
+ "指标名称2": 数值或字符串
2369
+ },
2370
+ "testConditions": {
2371
+ "测试温度": "23°C",
2372
+ "测试标准": "相关标准号"
2373
+ },
2374
+ "confidence": 0.95,
2375
+ "notes": "备注信息(如有异常值或不确定项)"
2376
+ }
2377
+ ]
2378
+ \`\`\`
2379
+
2380
+ ## 注意事项
2381
+ 1. confidence 为 0-1 之间的浮点数,表示对该条数据提取准确性的置信度
2382
+ 2. 如果文件中某个指标的值模糊或无法确定,将 confidence 降低并在 notes 中说明
2383
+ 3. 如果文件是图片(OCR),先描述图片内容再提取数据
2384
+ 4. 每个独立的样品/配方/实验组应作为数组中的一个元素
2385
+ 5. 数值类指标请直接输出数字类型(不要带单位),单位已在指标定义中给出
2386
+
2387
+ 请直接读取文件并输出 JSON 结果。
2388
+
2389
+ **重要:你必须将最终的 JSON 结果写入文件 \`./${body.taskId}-result.json\`(只写纯 JSON 数组,不要包含 markdown 代码块标记)。**`;
2390
+ }
2391
+ /**
2392
+ * 调用 CC 执行 prompt 并等待完成
2393
+ */
2394
+ async function executeAndWaitResult(runtimeAdapter, config) {
2395
+ return new Promise((resolve, reject) => {
2396
+ let resultText = "";
2397
+ runtimeAdapter.executePrompt(config, {
2398
+ onTextDelta(text) {
2399
+ resultText += text;
2400
+ },
2401
+ onResult(result) {
2402
+ logger$5.info("CC 执行返回结果", {
2403
+ sessionId: config.sessionId,
2404
+ subtype: result.subtype,
2405
+ resultLength: resultText.length
2406
+ });
2407
+ resolve(resultText || result.result || "");
2408
+ },
2409
+ onError(error) {
2410
+ logger$5.error("CC 执行出错", {
2411
+ sessionId: config.sessionId,
2412
+ error: error.message
2413
+ });
2414
+ reject(error);
2415
+ }
2416
+ }).catch(reject);
2417
+ });
2418
+ }
2419
+ /**
2420
+ * 从 CC 输出中解析结构化数据
2421
+ */
2422
+ function parseExtractionResult(rawOutput) {
2423
+ logger$5.info("解析 CC 输出", {
2424
+ outputLength: rawOutput.length,
2425
+ preview: rawOutput.slice(0, 200)
2426
+ });
2427
+ const codeBlockMatch = rawOutput.match(/```json\s*\n?([\s\S]*?)```/);
2428
+ if (codeBlockMatch) try {
2429
+ return parseAndValidateJson(codeBlockMatch[1].trim());
2430
+ } catch {}
2431
+ const arrayMatch = rawOutput.match(/\[[\s\S]*\]/);
2432
+ if (arrayMatch) try {
2433
+ return parseAndValidateJson(arrayMatch[0]);
2434
+ } catch {}
2435
+ const objMatch = rawOutput.match(/\{[\s\S]*\}/);
2436
+ if (objMatch) try {
2437
+ return parseAndValidateJson(`[${objMatch[0]}]`);
2438
+ } catch {}
2439
+ try {
2440
+ return parseAndValidateJson(rawOutput.trim());
2441
+ } catch {
2442
+ logger$5.warn("无法从 CC 输出中解析 JSON,返回空结果", { output: rawOutput.slice(0, 500) });
2443
+ return [];
2444
+ }
2445
+ }
2446
+ /**
2447
+ * 解析并校验 JSON 格式的提取结果
2448
+ */
2449
+ function parseAndValidateJson(jsonStr) {
2450
+ const parsed = JSON.parse(jsonStr);
2451
+ return (Array.isArray(parsed) ? parsed : [parsed]).map((item) => ({
2452
+ formulaNo: item.formulaNo || item.formula_no || item.sampleId || item.sample_id,
2453
+ indicators: item.indicators || item.values || {},
2454
+ testConditions: item.testConditions || item.test_conditions || item.conditions,
2455
+ confidence: typeof item.confidence === "number" ? Math.max(0, Math.min(1, item.confidence)) : .8,
2456
+ notes: item.notes || item.remarks
2457
+ }));
2458
+ }
2459
+ /**
2460
+ * 向调用方发送回调通知
2461
+ */
2462
+ async function sendCallback(callbackUrl, taskId, status, data, error) {
2463
+ try {
2464
+ logger$5.info("发送回调通知", {
2465
+ callbackUrl,
2466
+ taskId,
2467
+ status,
2468
+ recordCount: data?.length
2469
+ });
2470
+ const response = await fetch(callbackUrl, {
2471
+ method: "POST",
2472
+ headers: {
2473
+ "Content-Type": "application/json",
2474
+ "X-Internal-Secret": INTERNAL_SECRET
2475
+ },
2476
+ body: JSON.stringify({
2477
+ taskId,
2478
+ status,
2479
+ data,
2480
+ error
2481
+ })
2482
+ });
2483
+ if (!response.ok) {
2484
+ const errText = await response.text();
2485
+ logger$5.warn("回调通知失败", {
2486
+ callbackUrl,
2487
+ status: response.status,
2488
+ body: errText
2489
+ });
2490
+ } else logger$5.info("回调通知成功", {
2491
+ callbackUrl,
2492
+ taskId
2493
+ });
2494
+ } catch (err) {
2495
+ const errorMsg = err instanceof Error ? err.message : String(err);
2496
+ logger$5.error("回调通知异常", {
2497
+ callbackUrl,
2498
+ taskId,
2499
+ error: errorMsg
2500
+ });
2501
+ }
2195
2502
  }
2196
2503
  //#endregion
2197
2504
  //#region src/gateway/skills-handler.ts
@@ -3296,8 +3603,10 @@ function notImplemented(_req, res) {
3296
3603
  * 其余路由当前仍为占位 handler。
3297
3604
  */
3298
3605
  function registerRoutes(app, processManager, scheduledTaskManager, appCredentials) {
3299
- if (processManager) app.use(createExecuteRouter(processManager));
3300
- else {
3606
+ if (processManager) {
3607
+ app.use(createExecuteRouter(processManager));
3608
+ app.use(createAiExtractRouter(processManager));
3609
+ } else {
3301
3610
  logger$1.warn("processManager 未注入,执行路由使用占位 handler");
3302
3611
  app.post("/api/execute", notImplemented);
3303
3612
  app.post("/api/execute/batch", notImplemented);
@@ -4615,6 +4924,7 @@ function sortTraceValue(value) {
4615
4924
  //#endregion
4616
4925
  //#region src/card/cc-stream-bridge.ts
4617
4926
  const log$18 = larkLogger("card/cc-stream-bridge");
4927
+ const CC_INTERNAL_PLACEHOLDER = "No response requested.";
4618
4928
  /**
4619
4929
  * CCStreamBridge — 将 CC 流事件桥接到 StreamingCardController
4620
4930
  *
@@ -4733,6 +5043,15 @@ var CCStreamBridge = class {
4733
5043
  accumulatedThinkingTextLen: this.accumulatedThinkingText.length
4734
5044
  });
4735
5045
  if (this.options.autoCompleteOnTurnEnd && stopReason === "end_turn") {
5046
+ const trimmedText = this.accumulatedText.trim();
5047
+ if (trimmedText === CC_INTERNAL_PLACEHOLDER || trimmedText === "") {
5048
+ log$18.info("检测到 CC 内部占位消息,静默丢弃", {
5049
+ text: trimmedText.slice(0, 50),
5050
+ sessionKey: this.options.sessionKey
5051
+ });
5052
+ this.controller.abortCard();
5053
+ return;
5054
+ }
4736
5055
  this.controller.markFullyComplete();
4737
5056
  this.controller.onIdle();
4738
5057
  }
@@ -8761,7 +9080,8 @@ async function dispatchToCC(params) {
8761
9080
  const isGroup = chatType === "group";
8762
9081
  const replyInThread = !!threadId;
8763
9082
  const replyToMessageId = threadId ? void 0 : ctx.messageId;
8764
- const textPrompt = formatForCC(ctx, isGroup);
9083
+ let textPrompt = formatForCC(ctx, isGroup);
9084
+ if (isGroup) textPrompt = `[直接@你的消息,请正常回复] ${textPrompt}`;
8765
9085
  const imageResources = ctx.resources.filter((r) => r.type === "image");
8766
9086
  let prompt = textPrompt;
8767
9087
  if (imageResources.length > 0) {
@@ -9007,6 +9327,9 @@ async function dispatchToCC(params) {
9007
9327
  chatId,
9008
9328
  replyToMessageId: cardDeps.replyToMessageId
9009
9329
  });
9330
+ cardController.ensureCardCreated().catch((err) => {
9331
+ log$11.warn("提前创建卡片失败(streaming 回调会重试)", { error: String(err) });
9332
+ });
9010
9333
  const bridge = new CCStreamBridge(cardController, {
9011
9334
  autoCompleteOnTurnEnd: true,
9012
9335
  sessionKey: route.sessionId
@@ -9133,12 +9456,12 @@ async function dispatchTeammateEval(params) {
9133
9456
  const teammateSessionKey = `teammate_${route.sessionId}`;
9134
9457
  startToolUseTraceRun(teammateSessionKey);
9135
9458
  const NO_REPLY_TOKEN = SILENT_REPLY_TOKEN;
9136
- const PREFIX_LEN = NO_REPLY_TOKEN.length;
9459
+ const CC_INTERNAL_PLACEHOLDER = "No response requested.";
9137
9460
  /** 缓冲的 thinking 内容 */
9138
9461
  let thinkingBuffer = "";
9139
- /** text 前缀缓冲(用于判断是否为 NO_REPLY) */
9140
- let textPrefixBuffer = "";
9141
- /** 是否已确认 CC 决定回复(前缀检测通过) */
9462
+ /** 全部 text 输出缓冲(end_turn 时统一判断是否含 NO_REPLY) */
9463
+ let fullTextBuffer = "";
9464
+ /** 是否已确认 CC 决定回复(仅在 end_turn 时才确认) */
9142
9465
  let confirmed = false;
9143
9466
  /** 是否已确认 CC 决定静默 */
9144
9467
  let silenced = false;
@@ -9151,7 +9474,7 @@ async function dispatchTeammateEval(params) {
9151
9474
  const pendingToolEvents = [];
9152
9475
  let finalStopReason = "";
9153
9476
  /**
9154
- * 确认 CC 要回复 — 创建卡片 + bridge,灌入缓冲的 thinking + text 前缀
9477
+ * 确认 CC 要回复 — 创建卡片 + bridge,灌入缓冲的 thinking + text
9155
9478
  */
9156
9479
  const confirmReply = () => {
9157
9480
  if (confirmed) return;
@@ -9159,7 +9482,7 @@ async function dispatchTeammateEval(params) {
9159
9482
  log$11.info("teammate 确认回复,创建流式卡片", {
9160
9483
  chatId,
9161
9484
  thinkingLen: thinkingBuffer.length,
9162
- textPrefixLen: textPrefixBuffer.length
9485
+ textLen: fullTextBuffer.length
9163
9486
  });
9164
9487
  cardController = new StreamingCardController({
9165
9488
  cfg: {},
@@ -9190,13 +9513,18 @@ async function dispatchTeammateEval(params) {
9190
9513
  sessionKey: teammateSessionKey
9191
9514
  });
9192
9515
  if (thinkingBuffer) bridge.onThinkingDelta(thinkingBuffer);
9193
- if (textPrefixBuffer) bridge.onTextDelta(textPrefixBuffer);
9516
+ const cleanedText = fullTextBuffer.replace(/\s*NO_REPLY\s*$/, "").trim();
9517
+ if (cleanedText) bridge.onTextDelta(cleanedText);
9194
9518
  for (const evt of pendingToolEvents) if (evt.type === "start") bridge.onToolUseStart(evt.name, evt.input);
9195
9519
  else bridge.onToolResult(evt.id);
9196
9520
  pendingToolEvents.length = 0;
9197
9521
  };
9198
9522
  /**
9199
- * 处理 text delta — 前缀检测 + 流式转发
9523
+ * 处理 text delta — 全量缓冲,直到 end_turn 时统一判断
9524
+ *
9525
+ * 策略:CC 在 teammate 模式下可能先执行工具再输出文本,且文本中可能正文在前、
9526
+ * NO_REPLY 在末尾。因此不做前缀检测,而是缓冲所有 text,在 onTurnEnd(end_turn)
9527
+ * 时统一判断完整输出是否包含 NO_REPLY。
9200
9528
  */
9201
9529
  const handleTextDelta = (text) => {
9202
9530
  if (silenced) return;
@@ -9204,17 +9532,7 @@ async function dispatchTeammateEval(params) {
9204
9532
  bridge.onTextDelta(text);
9205
9533
  return;
9206
9534
  }
9207
- textPrefixBuffer += text;
9208
- if (textPrefixBuffer.length >= PREFIX_LEN) {
9209
- if (textPrefixBuffer.trim() === NO_REPLY_TOKEN) {
9210
- silenced = true;
9211
- log$11.info("teammate 前缀检测: NO_REPLY", { chatId });
9212
- return;
9213
- }
9214
- confirmReply();
9215
- return;
9216
- }
9217
- if (!NO_REPLY_TOKEN.startsWith(textPrefixBuffer.trim())) confirmReply();
9535
+ fullTextBuffer += text;
9218
9536
  };
9219
9537
  try {
9220
9538
  await new Promise((resolve) => {
@@ -9260,12 +9578,12 @@ async function dispatchTeammateEval(params) {
9260
9578
  return;
9261
9579
  }
9262
9580
  if (!confirmed && !silenced) {
9263
- const trimmed = textPrefixBuffer.trim();
9264
- if (trimmed === NO_REPLY_TOKEN || trimmed === "") {
9581
+ const trimmed = fullTextBuffer.trim();
9582
+ if (trimmed === NO_REPLY_TOKEN || trimmed === CC_INTERNAL_PLACEHOLDER || trimmed.endsWith(NO_REPLY_TOKEN) || trimmed === "") {
9265
9583
  silenced = true;
9266
- log$11.info("teammate turnEnd 最终判断: NO_REPLY", {
9584
+ log$11.info("teammate turnEnd 最终判断: 静默", {
9267
9585
  chatId,
9268
- trimmed
9586
+ trimmed: trimmed.slice(0, 60)
9269
9587
  });
9270
9588
  } else {
9271
9589
  confirmReply();
@@ -9302,7 +9620,7 @@ async function dispatchTeammateEval(params) {
9302
9620
  chatId,
9303
9621
  confirmed,
9304
9622
  silenced,
9305
- textPrefixBufferLen: textPrefixBuffer.length,
9623
+ fullTextBufferLen: fullTextBuffer.length,
9306
9624
  thinkingBufferLen: thinkingBuffer.length,
9307
9625
  stopReason: finalStopReason
9308
9626
  });
@@ -11976,9 +12294,15 @@ var TeammateBuffer = class {
11976
12294
  */
11977
12295
  buildEvalPrompt(messages) {
11978
12296
  const header = [
11979
- "[系统提示] 以下是群聊中最近的对话,你作为团队成员正在旁听。",
11980
- "请判断是否需要参与讨论。如果不需要,只输出 NO_REPLY。",
11981
- "如果需要参与,直接输出你的回复内容。",
12297
+ "[旁听评估任务] 以下是群聊中最近的对话,你作为团队成员正在旁听。",
12298
+ "请判断是否需要主动参与讨论。如果不需要,只输出 NO_REPLY。",
12299
+ "如果需要参与,直接输出你的回复内容(不要加 NO_REPLY)。",
12300
+ "",
12301
+ "重要规则:",
12302
+ "- 这是一次旁听评估,仅在本次评估中适用 NO_REPLY 规则。后续如果用户直接 @你 则必须正常回复。",
12303
+ "- 你无法查看图片/文件的实际内容。如果消息只包含图片或文件且没有文字上下文,直接输出 NO_REPLY。",
12304
+ "- 不要使用任何工具(Bash/Glob/Read 等)来尝试查找或分析图片文件。",
12305
+ "- 只基于文字内容判断是否参与。",
11982
12306
  "",
11983
12307
  "---"
11984
12308
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",