@vibe-lark/larkpal 0.1.24 → 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 +373 -75
  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
@@ -1657,7 +1657,7 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
1657
1657
  * 群聊 → /workspace/chats/group_{chat_id}/
1658
1658
  * 话题 → /workspace/chats/group_{chat_id}/topics/{thread_id}/
1659
1659
  */
1660
- const logger$6 = larkLogger("routing/session-router");
1660
+ const logger$7 = larkLogger("routing/session-router");
1661
1661
  const SESSION_OUTPUT_RULES = [
1662
1662
  `## 内容输出优先级`,
1663
1663
  "",
@@ -1696,7 +1696,7 @@ var SessionRouter = class {
1696
1696
  cwd: path.join(this.workspaceRoot, "chats", `p2p_${safeId}`),
1697
1697
  type: "p2p"
1698
1698
  };
1699
- logger$6.debug("解析私聊路由", {
1699
+ logger$7.debug("解析私聊路由", {
1700
1700
  chatId,
1701
1701
  userId: safeId,
1702
1702
  sessionId: route.sessionId,
@@ -1712,7 +1712,7 @@ var SessionRouter = class {
1712
1712
  cwd: path.join(groupCwd, "topics", threadId),
1713
1713
  type: "topic"
1714
1714
  };
1715
- logger$6.debug("解析话题路由", {
1715
+ logger$7.debug("解析话题路由", {
1716
1716
  chatId,
1717
1717
  threadId,
1718
1718
  sessionId: route.sessionId,
@@ -1725,7 +1725,7 @@ var SessionRouter = class {
1725
1725
  cwd: groupCwd,
1726
1726
  type: "group"
1727
1727
  };
1728
- logger$6.debug("解析群聊路由", {
1728
+ logger$7.debug("解析群聊路由", {
1729
1729
  chatId,
1730
1730
  sessionId: route.sessionId,
1731
1731
  cwd: route.cwd
@@ -1739,7 +1739,7 @@ var SessionRouter = class {
1739
1739
  * 此方法是幂等的,多次调用不会报错也不会覆盖已有文件。
1740
1740
  */
1741
1741
  async ensureSessionDirectory(route) {
1742
- logger$6.info("确保会话目录存在", {
1742
+ logger$7.info("确保会话目录存在", {
1743
1743
  sessionId: route.sessionId,
1744
1744
  cwd: route.cwd
1745
1745
  });
@@ -1749,7 +1749,7 @@ var SessionRouter = class {
1749
1749
  const claudeMdPath = path.join(route.cwd, "CLAUDE.md");
1750
1750
  if (!existsSync$1(claudeMdPath)) {
1751
1751
  await writeFile$1(claudeMdPath, this.generateClaudeMd(route), "utf-8");
1752
- logger$6.info("创建会话级 CLAUDE.md", {
1752
+ logger$7.info("创建会话级 CLAUDE.md", {
1753
1753
  path: claudeMdPath,
1754
1754
  type: route.type
1755
1755
  });
@@ -1813,7 +1813,7 @@ var SessionRouter = class {
1813
1813
  *
1814
1814
  * 内部通过 taskStore 维护所有任务的生命周期状态。
1815
1815
  */
1816
- const logger$5 = larkLogger("gateway/execute");
1816
+ const logger$6 = larkLogger("gateway/execute");
1817
1817
  /** 全局任务存储:taskId → TaskInfo */
1818
1818
  const taskStore = /* @__PURE__ */ new Map();
1819
1819
  /**
@@ -1847,7 +1847,7 @@ function createExecuteRouter(processManager) {
1847
1847
  */
1848
1848
  async function handleExecute(req, res, processManager) {
1849
1849
  const body = req.body;
1850
- logger$5.info("收到执行请求", {
1850
+ logger$6.info("收到执行请求", {
1851
1851
  session_id: body.session_id,
1852
1852
  cwd: body.cwd,
1853
1853
  prompt: body.prompt?.slice(0, 200),
@@ -1856,7 +1856,7 @@ async function handleExecute(req, res, processManager) {
1856
1856
  max_budget_usd: body.max_budget_usd
1857
1857
  });
1858
1858
  if (!body.session_id || !body.cwd || !body.prompt) {
1859
- logger$5.warn("执行请求参数缺失", {
1859
+ logger$6.warn("执行请求参数缺失", {
1860
1860
  hasSessionId: !!body.session_id,
1861
1861
  hasCwd: !!body.cwd,
1862
1862
  hasPrompt: !!body.prompt
@@ -1876,7 +1876,7 @@ async function handleExecute(req, res, processManager) {
1876
1876
  createdAt: /* @__PURE__ */ new Date()
1877
1877
  };
1878
1878
  taskStore.set(taskId, taskInfo);
1879
- logger$5.info("任务已创建", {
1879
+ logger$6.info("任务已创建", {
1880
1880
  taskId,
1881
1881
  sessionId: body.session_id,
1882
1882
  mode
@@ -1892,7 +1892,7 @@ async function handleExecute(req, res, processManager) {
1892
1892
  const callbacks = createTaskCallbacks(taskId);
1893
1893
  processManager.executePrompt(config, callbacks).catch((err) => {
1894
1894
  const errorMsg = err instanceof Error ? err.message : String(err);
1895
- logger$5.error("异步任务执行异常", {
1895
+ logger$6.error("异步任务执行异常", {
1896
1896
  taskId,
1897
1897
  error: errorMsg
1898
1898
  });
@@ -1900,7 +1900,7 @@ async function handleExecute(req, res, processManager) {
1900
1900
  taskInfo.error = errorMsg;
1901
1901
  taskInfo.completedAt = /* @__PURE__ */ new Date();
1902
1902
  });
1903
- logger$5.info("异步任务已启动,立即返回", { taskId });
1903
+ logger$6.info("异步任务已启动,立即返回", { taskId });
1904
1904
  res.status(202).json({
1905
1905
  task_id: taskId,
1906
1906
  status: "running"
@@ -1908,8 +1908,8 @@ async function handleExecute(req, res, processManager) {
1908
1908
  return;
1909
1909
  }
1910
1910
  try {
1911
- const result = await executeAndWaitResult(taskId, config, processManager);
1912
- logger$5.info("同步任务执行完成", {
1911
+ const result = await executeAndWaitResult$1(taskId, config, processManager);
1912
+ logger$6.info("同步任务执行完成", {
1913
1913
  taskId,
1914
1914
  resultSubtype: result?.subtype
1915
1915
  });
@@ -1920,7 +1920,7 @@ async function handleExecute(req, res, processManager) {
1920
1920
  });
1921
1921
  } catch (err) {
1922
1922
  const errorMsg = err instanceof Error ? err.message : String(err);
1923
- logger$5.error("同步任务执行失败", {
1923
+ logger$6.error("同步任务执行失败", {
1924
1924
  taskId,
1925
1925
  error: errorMsg
1926
1926
  });
@@ -1938,12 +1938,12 @@ async function handleExecute(req, res, processManager) {
1938
1938
  */
1939
1939
  async function handleBatchExecute(req, res, processManager) {
1940
1940
  const body = req.body;
1941
- logger$5.info("收到批量执行请求", {
1941
+ logger$6.info("收到批量执行请求", {
1942
1942
  taskCount: body.tasks?.length,
1943
1943
  concurrency: body.concurrency
1944
1944
  });
1945
1945
  if (!body.tasks || !Array.isArray(body.tasks) || body.tasks.length === 0) {
1946
- logger$5.warn("批量执行请求参数缺失: tasks 为空或格式错误");
1946
+ logger$6.warn("批量执行请求参数缺失: tasks 为空或格式错误");
1947
1947
  res.status(400).json({
1948
1948
  error: "Bad Request",
1949
1949
  message: "Missing or empty required field: tasks"
@@ -1953,7 +1953,7 @@ async function handleBatchExecute(req, res, processManager) {
1953
1953
  for (let i = 0; i < body.tasks.length; i++) {
1954
1954
  const task = body.tasks[i];
1955
1955
  if (!task?.session_id || !task?.cwd || !task?.prompt) {
1956
- logger$5.warn("批量执行子任务参数缺失", { index: i });
1956
+ logger$6.warn("批量执行子任务参数缺失", { index: i });
1957
1957
  res.status(400).json({
1958
1958
  error: "Bad Request",
1959
1959
  message: `Task at index ${i} is missing required fields: session_id, cwd, prompt`
@@ -1984,7 +1984,7 @@ async function handleBatchExecute(req, res, processManager) {
1984
1984
  }
1985
1985
  };
1986
1986
  });
1987
- logger$5.info("批量任务已创建", {
1987
+ logger$6.info("批量任务已创建", {
1988
1988
  batchId,
1989
1989
  taskIds,
1990
1990
  concurrency
@@ -2001,17 +2001,17 @@ async function handleBatchExecute(req, res, processManager) {
2001
2001
  */
2002
2002
  function handleGetTask(req, res) {
2003
2003
  const taskId = req.params.taskId;
2004
- logger$5.info("查询任务状态", { taskId });
2004
+ logger$6.info("查询任务状态", { taskId });
2005
2005
  const taskInfo = taskStore.get(taskId);
2006
2006
  if (!taskInfo) {
2007
- logger$5.warn("任务不存在", { taskId });
2007
+ logger$6.warn("任务不存在", { taskId });
2008
2008
  res.status(404).json({
2009
2009
  error: "Not Found",
2010
2010
  message: `Task ${taskId} not found`
2011
2011
  });
2012
2012
  return;
2013
2013
  }
2014
- logger$5.info("返回任务状态", {
2014
+ logger$6.info("返回任务状态", {
2015
2015
  taskId,
2016
2016
  status: taskInfo.status
2017
2017
  });
@@ -2028,10 +2028,10 @@ function handleGetTask(req, res) {
2028
2028
  */
2029
2029
  async function handleCancelTask(req, res, processManager) {
2030
2030
  const taskId = req.params.taskId;
2031
- logger$5.info("收到取消任务请求", { taskId });
2031
+ logger$6.info("收到取消任务请求", { taskId });
2032
2032
  const taskInfo = taskStore.get(taskId);
2033
2033
  if (!taskInfo) {
2034
- logger$5.warn("取消失败:任务不存在", { taskId });
2034
+ logger$6.warn("取消失败:任务不存在", { taskId });
2035
2035
  res.status(404).json({
2036
2036
  error: "Not Found",
2037
2037
  message: `Task ${taskId} not found`
@@ -2039,7 +2039,7 @@ async function handleCancelTask(req, res, processManager) {
2039
2039
  return;
2040
2040
  }
2041
2041
  if (taskInfo.status !== "running") {
2042
- logger$5.warn("取消失败:任务不在运行状态", {
2042
+ logger$6.warn("取消失败:任务不在运行状态", {
2043
2043
  taskId,
2044
2044
  status: taskInfo.status
2045
2045
  });
@@ -2053,7 +2053,7 @@ async function handleCancelTask(req, res, processManager) {
2053
2053
  await processManager.stopProcess(taskInfo.sessionId);
2054
2054
  taskInfo.status = "cancelled";
2055
2055
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2056
- logger$5.info("任务已取消", {
2056
+ logger$6.info("任务已取消", {
2057
2057
  taskId,
2058
2058
  sessionId: taskInfo.sessionId
2059
2059
  });
@@ -2063,7 +2063,7 @@ async function handleCancelTask(req, res, processManager) {
2063
2063
  });
2064
2064
  } catch (err) {
2065
2065
  const errorMsg = err instanceof Error ? err.message : String(err);
2066
- logger$5.error("取消任务时发生错误", {
2066
+ logger$6.error("取消任务时发生错误", {
2067
2067
  taskId,
2068
2068
  error: errorMsg
2069
2069
  });
@@ -2086,7 +2086,7 @@ function createTaskCallbacks(taskId) {
2086
2086
  taskInfo.result = result;
2087
2087
  taskInfo.status = result.isError ? "failed" : "completed";
2088
2088
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2089
- logger$5.info("任务收到结果", {
2089
+ logger$6.info("任务收到结果", {
2090
2090
  taskId,
2091
2091
  status: taskInfo.status,
2092
2092
  subtype: result.subtype,
@@ -2100,7 +2100,7 @@ function createTaskCallbacks(taskId) {
2100
2100
  taskInfo.status = "failed";
2101
2101
  taskInfo.error = error.message;
2102
2102
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2103
- logger$5.error("任务执行出错", {
2103
+ logger$6.error("任务执行出错", {
2104
2104
  taskId,
2105
2105
  error: error.message
2106
2106
  });
@@ -2112,7 +2112,7 @@ function createTaskCallbacks(taskId) {
2112
2112
  *
2113
2113
  * 通过 Promise 包装回调机制,在收到 onResult 或 onError 时 resolve/reject。
2114
2114
  */
2115
- function executeAndWaitResult(taskId, config, processManager) {
2115
+ function executeAndWaitResult$1(taskId, config, processManager) {
2116
2116
  return new Promise((resolve, reject) => {
2117
2117
  processManager.executePrompt(config, {
2118
2118
  onResult: (result) => {
@@ -2122,7 +2122,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2122
2122
  taskInfo.status = result.isError ? "failed" : "completed";
2123
2123
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2124
2124
  }
2125
- logger$5.info("同步任务收到结果", {
2125
+ logger$6.info("同步任务收到结果", {
2126
2126
  taskId,
2127
2127
  subtype: result.subtype,
2128
2128
  durationMs: result.durationMs,
@@ -2137,7 +2137,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2137
2137
  taskInfo.error = error.message;
2138
2138
  taskInfo.completedAt = /* @__PURE__ */ new Date();
2139
2139
  }
2140
- logger$5.error("同步任务执行出错", {
2140
+ logger$6.error("同步任务执行出错", {
2141
2141
  taskId,
2142
2142
  error: error.message
2143
2143
  });
@@ -2145,7 +2145,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2145
2145
  }
2146
2146
  }).catch((err) => {
2147
2147
  const errorMsg = err instanceof Error ? err.message : String(err);
2148
- logger$5.error("同步任务 executePrompt 调用失败", {
2148
+ logger$6.error("同步任务 executePrompt 调用失败", {
2149
2149
  taskId,
2150
2150
  error: errorMsg
2151
2151
  });
@@ -2165,7 +2165,7 @@ function executeAndWaitResult(taskId, config, processManager) {
2165
2165
  * 使用简单的信号量模式控制并发数,逐个启动任务直到所有任务完成。
2166
2166
  */
2167
2167
  async function runBatchTasks(taskConfigs, concurrency, processManager) {
2168
- logger$5.info("开始批量任务执行", {
2168
+ logger$6.info("开始批量任务执行", {
2169
2169
  totalTasks: taskConfigs.length,
2170
2170
  concurrency
2171
2171
  });
@@ -2177,7 +2177,7 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2177
2177
  await processManager.executePrompt(config, callbacks);
2178
2178
  } catch (err) {
2179
2179
  const errorMsg = err instanceof Error ? err.message : String(err);
2180
- logger$5.error("批量子任务执行异常", {
2180
+ logger$6.error("批量子任务执行异常", {
2181
2181
  taskId,
2182
2182
  error: errorMsg
2183
2183
  });
@@ -2194,7 +2194,311 @@ async function runBatchTasks(taskConfigs, concurrency, processManager) {
2194
2194
  if (executing.size >= concurrency) await Promise.race(executing);
2195
2195
  }
2196
2196
  await Promise.all(executing);
2197
- 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
+ }
2198
2502
  }
2199
2503
  //#endregion
2200
2504
  //#region src/gateway/skills-handler.ts
@@ -3299,8 +3603,10 @@ function notImplemented(_req, res) {
3299
3603
  * 其余路由当前仍为占位 handler。
3300
3604
  */
3301
3605
  function registerRoutes(app, processManager, scheduledTaskManager, appCredentials) {
3302
- if (processManager) app.use(createExecuteRouter(processManager));
3303
- else {
3606
+ if (processManager) {
3607
+ app.use(createExecuteRouter(processManager));
3608
+ app.use(createAiExtractRouter(processManager));
3609
+ } else {
3304
3610
  logger$1.warn("processManager 未注入,执行路由使用占位 handler");
3305
3611
  app.post("/api/execute", notImplemented);
3306
3612
  app.post("/api/execute/batch", notImplemented);
@@ -9151,12 +9457,11 @@ async function dispatchTeammateEval(params) {
9151
9457
  startToolUseTraceRun(teammateSessionKey);
9152
9458
  const NO_REPLY_TOKEN = SILENT_REPLY_TOKEN;
9153
9459
  const CC_INTERNAL_PLACEHOLDER = "No response requested.";
9154
- const PREFIX_LEN = 22;
9155
9460
  /** 缓冲的 thinking 内容 */
9156
9461
  let thinkingBuffer = "";
9157
- /** text 前缀缓冲(用于判断是否为 NO_REPLY) */
9158
- let textPrefixBuffer = "";
9159
- /** 是否已确认 CC 决定回复(前缀检测通过) */
9462
+ /** 全部 text 输出缓冲(end_turn 时统一判断是否含 NO_REPLY) */
9463
+ let fullTextBuffer = "";
9464
+ /** 是否已确认 CC 决定回复(仅在 end_turn 时才确认) */
9160
9465
  let confirmed = false;
9161
9466
  /** 是否已确认 CC 决定静默 */
9162
9467
  let silenced = false;
@@ -9169,7 +9474,7 @@ async function dispatchTeammateEval(params) {
9169
9474
  const pendingToolEvents = [];
9170
9475
  let finalStopReason = "";
9171
9476
  /**
9172
- * 确认 CC 要回复 — 创建卡片 + bridge,灌入缓冲的 thinking + text 前缀
9477
+ * 确认 CC 要回复 — 创建卡片 + bridge,灌入缓冲的 thinking + text
9173
9478
  */
9174
9479
  const confirmReply = () => {
9175
9480
  if (confirmed) return;
@@ -9177,7 +9482,7 @@ async function dispatchTeammateEval(params) {
9177
9482
  log$11.info("teammate 确认回复,创建流式卡片", {
9178
9483
  chatId,
9179
9484
  thinkingLen: thinkingBuffer.length,
9180
- textPrefixLen: textPrefixBuffer.length
9485
+ textLen: fullTextBuffer.length
9181
9486
  });
9182
9487
  cardController = new StreamingCardController({
9183
9488
  cfg: {},
@@ -9208,13 +9513,18 @@ async function dispatchTeammateEval(params) {
9208
9513
  sessionKey: teammateSessionKey
9209
9514
  });
9210
9515
  if (thinkingBuffer) bridge.onThinkingDelta(thinkingBuffer);
9211
- if (textPrefixBuffer) bridge.onTextDelta(textPrefixBuffer);
9516
+ const cleanedText = fullTextBuffer.replace(/\s*NO_REPLY\s*$/, "").trim();
9517
+ if (cleanedText) bridge.onTextDelta(cleanedText);
9212
9518
  for (const evt of pendingToolEvents) if (evt.type === "start") bridge.onToolUseStart(evt.name, evt.input);
9213
9519
  else bridge.onToolResult(evt.id);
9214
9520
  pendingToolEvents.length = 0;
9215
9521
  };
9216
9522
  /**
9217
- * 处理 text delta — 前缀检测 + 流式转发
9523
+ * 处理 text delta — 全量缓冲,直到 end_turn 时统一判断
9524
+ *
9525
+ * 策略:CC 在 teammate 模式下可能先执行工具再输出文本,且文本中可能正文在前、
9526
+ * NO_REPLY 在末尾。因此不做前缀检测,而是缓冲所有 text,在 onTurnEnd(end_turn)
9527
+ * 时统一判断完整输出是否包含 NO_REPLY。
9218
9528
  */
9219
9529
  const handleTextDelta = (text) => {
9220
9530
  if (silenced) return;
@@ -9222,24 +9532,7 @@ async function dispatchTeammateEval(params) {
9222
9532
  bridge.onTextDelta(text);
9223
9533
  return;
9224
9534
  }
9225
- textPrefixBuffer += text;
9226
- if (textPrefixBuffer.length >= PREFIX_LEN) {
9227
- const trimmed = textPrefixBuffer.trim();
9228
- if (trimmed === NO_REPLY_TOKEN || trimmed === CC_INTERNAL_PLACEHOLDER) {
9229
- silenced = true;
9230
- log$11.info("teammate 前缀检测: 静默", {
9231
- chatId,
9232
- trimmed: trimmed.slice(0, 30)
9233
- });
9234
- return;
9235
- }
9236
- confirmReply();
9237
- return;
9238
- }
9239
- const currentTrimmed = textPrefixBuffer.trim();
9240
- const couldBeNoReply = NO_REPLY_TOKEN.startsWith(currentTrimmed);
9241
- const couldBePlaceholder = CC_INTERNAL_PLACEHOLDER.startsWith(currentTrimmed);
9242
- if (!couldBeNoReply && !couldBePlaceholder) confirmReply();
9535
+ fullTextBuffer += text;
9243
9536
  };
9244
9537
  try {
9245
9538
  await new Promise((resolve) => {
@@ -9285,12 +9578,12 @@ async function dispatchTeammateEval(params) {
9285
9578
  return;
9286
9579
  }
9287
9580
  if (!confirmed && !silenced) {
9288
- const trimmed = textPrefixBuffer.trim();
9289
- if (trimmed === NO_REPLY_TOKEN || trimmed === CC_INTERNAL_PLACEHOLDER || trimmed === "") {
9581
+ const trimmed = fullTextBuffer.trim();
9582
+ if (trimmed === NO_REPLY_TOKEN || trimmed === CC_INTERNAL_PLACEHOLDER || trimmed.endsWith(NO_REPLY_TOKEN) || trimmed === "") {
9290
9583
  silenced = true;
9291
9584
  log$11.info("teammate turnEnd 最终判断: 静默", {
9292
9585
  chatId,
9293
- trimmed: trimmed.slice(0, 30)
9586
+ trimmed: trimmed.slice(0, 60)
9294
9587
  });
9295
9588
  } else {
9296
9589
  confirmReply();
@@ -9327,7 +9620,7 @@ async function dispatchTeammateEval(params) {
9327
9620
  chatId,
9328
9621
  confirmed,
9329
9622
  silenced,
9330
- textPrefixBufferLen: textPrefixBuffer.length,
9623
+ fullTextBufferLen: fullTextBuffer.length,
9331
9624
  thinkingBufferLen: thinkingBuffer.length,
9332
9625
  stopReason: finalStopReason
9333
9626
  });
@@ -12003,8 +12296,13 @@ var TeammateBuffer = class {
12003
12296
  const header = [
12004
12297
  "[旁听评估任务] 以下是群聊中最近的对话,你作为团队成员正在旁听。",
12005
12298
  "请判断是否需要主动参与讨论。如果不需要,只输出 NO_REPLY。",
12006
- "如果需要参与,直接输出你的回复内容。",
12007
- "注意:这是一次旁听评估,仅在本次评估中适用 NO_REPLY 规则。后续如果用户直接 @你 则必须正常回复。",
12299
+ "如果需要参与,直接输出你的回复内容(不要加 NO_REPLY)。",
12300
+ "",
12301
+ "重要规则:",
12302
+ "- 这是一次旁听评估,仅在本次评估中适用 NO_REPLY 规则。后续如果用户直接 @你 则必须正常回复。",
12303
+ "- 你无法查看图片/文件的实际内容。如果消息只包含图片或文件且没有文字上下文,直接输出 NO_REPLY。",
12304
+ "- 不要使用任何工具(Bash/Glob/Read 等)来尝试查找或分析图片文件。",
12305
+ "- 只基于文字内容判断是否参与。",
12008
12306
  "",
12009
12307
  "---"
12010
12308
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",