@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.
- package/dist/main.mjs +373 -75
- 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$
|
|
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$
|
|
347
|
+
logger$8.info("确保 ~/.claude 目录存在", { path: claudeDir });
|
|
348
348
|
await writeFile(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2), "utf-8");
|
|
349
|
-
logger$
|
|
350
|
-
if (await fileExists(claudeMdPath)) logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2004
|
+
logger$6.info("查询任务状态", { taskId });
|
|
2005
2005
|
const taskInfo = taskStore.get(taskId);
|
|
2006
2006
|
if (!taskInfo) {
|
|
2007
|
-
logger$
|
|
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$
|
|
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$
|
|
2031
|
+
logger$6.info("收到取消任务请求", { taskId });
|
|
2032
2032
|
const taskInfo = taskStore.get(taskId);
|
|
2033
2033
|
if (!taskInfo) {
|
|
2034
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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)
|
|
3303
|
-
|
|
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
|
|
9158
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
"
|
|
12299
|
+
"如果需要参与,直接输出你的回复内容(不要加 NO_REPLY)。",
|
|
12300
|
+
"",
|
|
12301
|
+
"重要规则:",
|
|
12302
|
+
"- 这是一次旁听评估,仅在本次评估中适用 NO_REPLY 规则。后续如果用户直接 @你 则必须正常回复。",
|
|
12303
|
+
"- 你无法查看图片/文件的实际内容。如果消息只包含图片或文件且没有文字上下文,直接输出 NO_REPLY。",
|
|
12304
|
+
"- 不要使用任何工具(Bash/Glob/Read 等)来尝试查找或分析图片文件。",
|
|
12305
|
+
"- 只基于文字内容判断是否参与。",
|
|
12008
12306
|
"",
|
|
12009
12307
|
"---"
|
|
12010
12308
|
].join("\n");
|