@vibe-lark/larkpal 0.1.91-issue70.0 → 0.1.91-issue71.0
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/README.md +0 -51
- package/dist/cli.mjs +3 -3
- package/dist/{live-smoke-DsR13pI9.mjs → live-smoke-CukAuAsd.mjs} +218 -39
- package/dist/main.mjs +194 -307
- package/dist/mcp-http-proxy.mjs +183 -0
- package/dist/mcp-server.mjs +11 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,60 +93,9 @@ lark-cli auth login
|
|
|
93
93
|
| `LARKPAL_ENCRYPTION_KEY` | 用户凭证加密密钥(32字节 hex) | 未设置(明文,仅限开发) |
|
|
94
94
|
| `LARKPAL_PLUGINS` | 声明式加载的插件列表 | 空 |
|
|
95
95
|
| `LARKPAL_CHAT_JWT_SECRET` | Chat API JWT 签名密钥 | fallback 到 `LARKPAL_API_SECRET` |
|
|
96
|
-
| `LARKPAL_BUSINESS_EVENT_CALLBACK_URL` | 飞书长连接业务事件回调地址,用于固定菜单和业务卡片 action | 空 |
|
|
97
96
|
|
|
98
97
|
会话工作目录默认写入 `LARKPAL_WORKSPACE` 下的隐藏目录:飞书会话为 `.chats/`,通用 Agent 会话为 `.sessions/`,Chat API 历史为 `.chat-history/`。
|
|
99
98
|
|
|
100
|
-
### Business Event Callback
|
|
101
|
-
|
|
102
|
-
当配置 `LARKPAL_BUSINESS_EVENT_CALLBACK_URL` 时,LarkPal 会把内置处理器未消费的业务事件 POST 到该地址。典型来源包括飞书固定机器人菜单 `application.bot.menu_v6` 和业务自定义 `card.action.trigger`。回调响应可以让 LarkPal 在触发会话中发送可见反馈;业务系统拥有 workflow 语义和卡片内容,LarkPal 只负责 Feishu transport。
|
|
103
|
-
|
|
104
|
-
**文本消息响应**
|
|
105
|
-
|
|
106
|
-
```json
|
|
107
|
-
{ "kind": "message", "text": "配方草稿已创建,请继续填写。" }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
兼容格式:
|
|
111
|
-
|
|
112
|
-
```json
|
|
113
|
-
{ "message": { "text": "配方草稿已创建,请继续填写。" } }
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
**互动卡片响应**
|
|
117
|
-
|
|
118
|
-
LarkPal 原样发送 `card` payload,不重写业务 action id。
|
|
119
|
-
|
|
120
|
-
```json
|
|
121
|
-
{
|
|
122
|
-
"kind": "card",
|
|
123
|
-
"card": {
|
|
124
|
-
"schema": "2.0",
|
|
125
|
-
"body": { "elements": [] }
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
兼容格式:
|
|
131
|
-
|
|
132
|
-
```json
|
|
133
|
-
{ "card": { "schema": "2.0", "body": { "elements": [] } } }
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**轻量提示响应**
|
|
137
|
-
|
|
138
|
-
对于不支持直接 toast 的事件(如固定菜单),LarkPal 会降级发送一条可见文本消息,避免用户点击后无反馈。
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{ "kind": "toast", "toast": { "type": "warning", "content": "请先上传实验文档。" } }
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
兼容格式:
|
|
145
|
-
|
|
146
|
-
```json
|
|
147
|
-
{ "toast": { "type": "warning", "content": "请先上传实验文档。" } }
|
|
148
|
-
```
|
|
149
|
-
|
|
150
99
|
### larkpal-agent Runtime 专用(LARKPAL_RUNTIME=larkpal-agent 时必填)
|
|
151
100
|
|
|
152
101
|
| 变量名 | 用途 | 示例 |
|
package/dist/cli.mjs
CHANGED
|
@@ -73,17 +73,17 @@ async function cli() {
|
|
|
73
73
|
process.exit(exitCode);
|
|
74
74
|
}
|
|
75
75
|
if (command === "meeting-live-smoke") {
|
|
76
|
-
const { runMeetingLiveSmokeCli } = await import("./live-smoke-
|
|
76
|
+
const { runMeetingLiveSmokeCli } = await import("./live-smoke-CukAuAsd.mjs").then((n) => n.t);
|
|
77
77
|
await runMeetingLiveSmokeCli(args.slice(1));
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
if (command === "meeting-live-teammate") {
|
|
81
|
-
const { runMeetingLiveTeammateCli } = await import("./live-smoke-
|
|
81
|
+
const { runMeetingLiveTeammateCli } = await import("./live-smoke-CukAuAsd.mjs").then((n) => n.t);
|
|
82
82
|
await runMeetingLiveTeammateCli(args.slice(1));
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
if (command === "meeting-live-check") {
|
|
86
|
-
const { runMeetingLiveCheckCli } = await import("./live-smoke-
|
|
86
|
+
const { runMeetingLiveCheckCli } = await import("./live-smoke-CukAuAsd.mjs").then((n) => n.t);
|
|
87
87
|
const exitCode = await runMeetingLiveCheckCli(args.slice(1));
|
|
88
88
|
process.exit(exitCode);
|
|
89
89
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
2
|
import { t as larkLogger } from "./lark-logger-D7_pEVQc.mjs";
|
|
3
3
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import path, { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
-
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { copyFile, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import os, { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { execFileSync, spawn } from "node:child_process";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { createInterface } from "node:readline";
|
|
11
11
|
import { v4, v5 } from "uuid";
|
|
12
|
-
import { readFile as readFile$1,
|
|
12
|
+
import { readFile as readFile$1, writeFile as writeFile$1 } from "fs/promises";
|
|
13
13
|
import { existsSync as existsSync$1 } from "fs";
|
|
14
14
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
15
|
-
import path$1 from "path";
|
|
16
|
-
import os from "os";
|
|
17
15
|
import { EventEmitter } from "node:events";
|
|
18
16
|
import { pipeline } from "node:stream/promises";
|
|
19
17
|
import { Transform } from "node:stream";
|
|
@@ -216,6 +214,29 @@ var CredentialVault = class {
|
|
|
216
214
|
}
|
|
217
215
|
};
|
|
218
216
|
//#endregion
|
|
217
|
+
//#region src/runtime/tool-policy.ts
|
|
218
|
+
const SIGNAL_NO_REPLY_TOOL = "signal_no_reply";
|
|
219
|
+
function appendDisallowedTool(policy, tool) {
|
|
220
|
+
const disallowedTools = policy?.disallowedTools ?? [];
|
|
221
|
+
if (disallowedTools.some((rule) => rule.tool === tool)) return {
|
|
222
|
+
...policy,
|
|
223
|
+
allowedTools: policy?.allowedTools ?? [],
|
|
224
|
+
allowedSkills: policy?.allowedSkills ?? [],
|
|
225
|
+
disallowedTools
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
...policy,
|
|
229
|
+
allowedTools: policy?.allowedTools ?? [],
|
|
230
|
+
allowedSkills: policy?.allowedSkills ?? [],
|
|
231
|
+
disallowedTools: [...disallowedTools, { tool }]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function serializeDisabledMcpTools(policy) {
|
|
235
|
+
const tools = policy?.disallowedTools?.map((rule) => rule.tool.trim()).filter(Boolean);
|
|
236
|
+
if (!tools?.length) return void 0;
|
|
237
|
+
return [...new Set(tools)].join(",");
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
219
240
|
//#region src/user/mcp-merge.ts
|
|
220
241
|
/**
|
|
221
242
|
* per-user MCP Server 配置合并
|
|
@@ -235,28 +256,53 @@ var CredentialVault = class {
|
|
|
235
256
|
* - 全局工具(lark-cli、文件操作等)所有用户共享
|
|
236
257
|
*/
|
|
237
258
|
const log$9 = larkLogger("user/mcp-merge");
|
|
259
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
238
260
|
/** 全局 MCP 配置路径 */
|
|
239
|
-
const GLOBAL_MCP_CONFIG_PATH = path
|
|
261
|
+
const GLOBAL_MCP_CONFIG_PATH = path.join(os.homedir(), ".claude", "mcp-servers.json");
|
|
240
262
|
/**
|
|
241
263
|
* 获取 larkpal MCP Server 的可执行路径
|
|
242
264
|
*
|
|
243
265
|
* 优先使用 dist/mcp-server.mjs(生产构建),回退到 tsx 直接运行源码(开发模式)
|
|
244
266
|
*/
|
|
245
267
|
function getLarkpalMcpServerCommand$1() {
|
|
246
|
-
const
|
|
247
|
-
|
|
268
|
+
const distCandidates = [
|
|
269
|
+
path.resolve(currentDir, "mcp-server.mjs"),
|
|
270
|
+
path.resolve(currentDir, "..", "dist", "mcp-server.mjs"),
|
|
271
|
+
path.resolve(currentDir, "..", "..", "dist", "mcp-server.mjs")
|
|
272
|
+
];
|
|
273
|
+
for (const distPath of distCandidates) if (existsSync(distPath)) return {
|
|
248
274
|
command: "node",
|
|
249
275
|
args: [distPath]
|
|
250
276
|
};
|
|
251
|
-
const srcPath = path
|
|
252
|
-
if (existsSync
|
|
277
|
+
const srcPath = path.resolve(currentDir, "..", "mcp-server", "index.ts");
|
|
278
|
+
if (existsSync(srcPath)) return {
|
|
253
279
|
command: "npx",
|
|
254
280
|
args: ["tsx", srcPath]
|
|
255
281
|
};
|
|
256
282
|
return {
|
|
283
|
+
command: "node",
|
|
284
|
+
args: [distCandidates[0] ?? path.resolve(currentDir, "mcp-server.mjs")]
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function getExternalMcpProxyCommand() {
|
|
288
|
+
const distCandidates = [
|
|
289
|
+
path.resolve(currentDir, "mcp-http-proxy.mjs"),
|
|
290
|
+
path.resolve(currentDir, "..", "dist", "mcp-http-proxy.mjs"),
|
|
291
|
+
path.resolve(currentDir, "..", "..", "dist", "mcp-http-proxy.mjs")
|
|
292
|
+
];
|
|
293
|
+
for (const distPath of distCandidates) if (existsSync(distPath)) return {
|
|
257
294
|
command: "node",
|
|
258
295
|
args: [distPath]
|
|
259
296
|
};
|
|
297
|
+
const srcPath = path.resolve(currentDir, "..", "mcp-http-proxy", "index.ts");
|
|
298
|
+
if (existsSync(srcPath)) return {
|
|
299
|
+
command: "npx",
|
|
300
|
+
args: ["tsx", srcPath]
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
command: "node",
|
|
304
|
+
args: [distCandidates[0] ?? path.resolve(currentDir, "mcp-http-proxy.mjs")]
|
|
305
|
+
};
|
|
260
306
|
}
|
|
261
307
|
/**
|
|
262
308
|
* 构建内置 larkpal MCP Server 配置
|
|
@@ -264,8 +310,9 @@ function getLarkpalMcpServerCommand$1() {
|
|
|
264
310
|
* 重要: CC 启动 MCP Server 子进程时,env 字段可能是「覆盖」语义(不继承父进程环境变量)。
|
|
265
311
|
* 因此必须将所有关键变量显式传递,特别是 PATH 和 HOME。
|
|
266
312
|
*/
|
|
267
|
-
function getBuiltinLarkpalConfig(sessionId, cwd) {
|
|
313
|
+
function getBuiltinLarkpalConfig(sessionId, cwd, policy) {
|
|
268
314
|
const { command, args } = getLarkpalMcpServerCommand$1();
|
|
315
|
+
const disabledTools = serializeDisabledMcpTools(policy);
|
|
269
316
|
return {
|
|
270
317
|
command,
|
|
271
318
|
args,
|
|
@@ -275,20 +322,59 @@ function getBuiltinLarkpalConfig(sessionId, cwd) {
|
|
|
275
322
|
NODE_PATH: process.env["NODE_PATH"] || "",
|
|
276
323
|
LARKPAL_MCP_SESSION_ID: sessionId,
|
|
277
324
|
...cwd ? { LARKPAL_MCP_CWD: cwd } : {},
|
|
325
|
+
...disabledTools ? { LARKPAL_MCP_DISABLED_TOOLS: disabledTools } : {},
|
|
278
326
|
LARKPAL_GATEWAY_PORT: process.env["LARKPAL_GATEWAY_PORT"] || "3000"
|
|
279
327
|
}
|
|
280
328
|
};
|
|
281
329
|
}
|
|
330
|
+
function getExternalMcpProviderConfig(provider) {
|
|
331
|
+
const { command, args } = getExternalMcpProxyCommand();
|
|
332
|
+
return {
|
|
333
|
+
command,
|
|
334
|
+
args,
|
|
335
|
+
env: {
|
|
336
|
+
PATH: process.env["PATH"] || "/usr/local/bin:/usr/bin:/bin",
|
|
337
|
+
HOME: process.env["HOME"] || "/home/larkpal",
|
|
338
|
+
NODE_PATH: process.env["NODE_PATH"] || "",
|
|
339
|
+
LARKPAL_EXTERNAL_MCP_NAME: provider.name,
|
|
340
|
+
LARKPAL_EXTERNAL_MCP_URL: provider.url,
|
|
341
|
+
...provider.headers ? { LARKPAL_EXTERNAL_MCP_HEADERS_JSON: JSON.stringify(provider.headers) } : {},
|
|
342
|
+
...provider.headerEnv ? { LARKPAL_EXTERNAL_MCP_HEADER_ENV_JSON: JSON.stringify(provider.headerEnv) } : {},
|
|
343
|
+
...provider.headersJsonEnv ? { LARKPAL_EXTERNAL_MCP_HEADERS_JSON_ENV: provider.headersJsonEnv } : {},
|
|
344
|
+
...provider.toolAllowlist?.length ? { LARKPAL_EXTERNAL_MCP_TOOL_ALLOWLIST: provider.toolAllowlist.join(",") } : {}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function buildExternalMcpConfig(providers) {
|
|
349
|
+
const result = {};
|
|
350
|
+
for (const provider of providers ?? []) {
|
|
351
|
+
const name = provider.name.trim();
|
|
352
|
+
const url = provider.url.trim();
|
|
353
|
+
if (!name || !url) {
|
|
354
|
+
log$9.warn("外部 MCP Provider 配置缺少 name 或 url,已跳过", {
|
|
355
|
+
name: provider.name,
|
|
356
|
+
url: provider.url
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
result[name] = getExternalMcpProviderConfig({
|
|
361
|
+
...provider,
|
|
362
|
+
name,
|
|
363
|
+
url
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
282
368
|
/**
|
|
283
369
|
* 合并全局、内置和用户 MCP 配置,写入临时文件
|
|
284
370
|
*
|
|
285
371
|
* @returns 临时文件路径,用于 --mcp-config 参数(总是返回有效路径)
|
|
286
372
|
*/
|
|
287
|
-
async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
288
|
-
const builtinConfig = { larkpal: getBuiltinLarkpalConfig(sessionId, cwd) };
|
|
373
|
+
async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd, policy, externalProviders) {
|
|
374
|
+
const builtinConfig = { larkpal: getBuiltinLarkpalConfig(sessionId, cwd, policy) };
|
|
289
375
|
let globalConfig = {};
|
|
290
|
-
if (existsSync
|
|
291
|
-
const raw = await readFile
|
|
376
|
+
if (existsSync(GLOBAL_MCP_CONFIG_PATH)) try {
|
|
377
|
+
const raw = await readFile(GLOBAL_MCP_CONFIG_PATH, "utf-8");
|
|
292
378
|
globalConfig = JSON.parse(raw);
|
|
293
379
|
log$9.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
|
|
294
380
|
} catch (err) {
|
|
@@ -299,9 +385,9 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
299
385
|
}
|
|
300
386
|
let userConfig = {};
|
|
301
387
|
if (userCtx) {
|
|
302
|
-
const userMcpPath = path
|
|
303
|
-
if (existsSync
|
|
304
|
-
const raw = await readFile
|
|
388
|
+
const userMcpPath = path.join(userCtx.credentialDir, "mcp-servers.json");
|
|
389
|
+
if (existsSync(userMcpPath)) try {
|
|
390
|
+
const raw = await readFile(userMcpPath, "utf-8");
|
|
305
391
|
userConfig = JSON.parse(raw);
|
|
306
392
|
log$9.info("用户 MCP 配置加载完成", {
|
|
307
393
|
userId: userCtx.userId,
|
|
@@ -316,20 +402,23 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
316
402
|
});
|
|
317
403
|
}
|
|
318
404
|
}
|
|
405
|
+
const externalConfig = buildExternalMcpConfig(externalProviders);
|
|
319
406
|
const merged = {
|
|
320
407
|
...builtinConfig,
|
|
321
408
|
...globalConfig,
|
|
322
|
-
...userConfig
|
|
409
|
+
...userConfig,
|
|
410
|
+
...externalConfig
|
|
323
411
|
};
|
|
324
412
|
log$9.info("MCP 配置合并完成", {
|
|
325
413
|
userId: userCtx?.userId,
|
|
326
414
|
builtinServers: Object.keys(builtinConfig),
|
|
327
415
|
globalServers: Object.keys(globalConfig),
|
|
328
416
|
userServers: Object.keys(userConfig),
|
|
417
|
+
externalServers: Object.keys(externalConfig),
|
|
329
418
|
mergedServers: Object.keys(merged)
|
|
330
419
|
});
|
|
331
|
-
const tmpPath = path
|
|
332
|
-
await writeFile
|
|
420
|
+
const tmpPath = path.join(os.tmpdir(), `mcp-${sessionId}.json`);
|
|
421
|
+
await writeFile(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
|
|
333
422
|
log$9.info("MCP 合并配置已写入临时文件", {
|
|
334
423
|
path: tmpPath,
|
|
335
424
|
serverCount: Object.keys(merged).length
|
|
@@ -340,10 +429,10 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
|
|
|
340
429
|
* 清理 MCP 临时配置文件
|
|
341
430
|
*/
|
|
342
431
|
async function cleanupMcpConfig(sessionId) {
|
|
343
|
-
const tmpPath = path
|
|
432
|
+
const tmpPath = path.join(os.tmpdir(), `mcp-${sessionId}.json`);
|
|
344
433
|
try {
|
|
345
|
-
if (existsSync
|
|
346
|
-
await unlink
|
|
434
|
+
if (existsSync(tmpPath)) {
|
|
435
|
+
await unlink(tmpPath);
|
|
347
436
|
log$9.debug("MCP 临时配置文件已清理", { path: tmpPath });
|
|
348
437
|
}
|
|
349
438
|
} catch {}
|
|
@@ -478,6 +567,8 @@ const log$8 = larkLogger("cc-runtime/stream-parser");
|
|
|
478
567
|
const MAX_TOOL_RESULT_CONTENT_CHARS = 8e3;
|
|
479
568
|
var CCStreamParser = class extends EventEmitter {
|
|
480
569
|
toolNamesById = /* @__PURE__ */ new Map();
|
|
570
|
+
emittedToolUseIds = /* @__PURE__ */ new Set();
|
|
571
|
+
emittedToolUseFingerprints = /* @__PURE__ */ new Set();
|
|
481
572
|
declaredTools = null;
|
|
482
573
|
/**
|
|
483
574
|
* 解析一行 NDJSON 文本
|
|
@@ -556,12 +647,21 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
556
647
|
});
|
|
557
648
|
if (block?.type === "tool_use" && block.name) {
|
|
558
649
|
if (!this.recordToolUse(block.name, block.id, block.input)) break;
|
|
650
|
+
if (!hasDurableToolInput(block.input)) {
|
|
651
|
+
log$8.debug("工具调用 start 仅含草稿参数,等待完整 assistant 消息", {
|
|
652
|
+
toolName: block.name,
|
|
653
|
+
toolUseId: block.id
|
|
654
|
+
});
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
if (this.hasEmittedToolUseStart(block.name, block.input, block.id)) break;
|
|
559
658
|
log$8.info("工具调用开始", {
|
|
560
659
|
toolName: block.name,
|
|
561
660
|
displayName: getToolDisplayName(block.name),
|
|
562
661
|
toolUseId: block.id
|
|
563
662
|
});
|
|
564
|
-
this.
|
|
663
|
+
this.markToolUseStartEmitted(block.name, block.input, block.id);
|
|
664
|
+
this.emit("toolUseStart", block.name, block.input, block.id);
|
|
565
665
|
}
|
|
566
666
|
break;
|
|
567
667
|
}
|
|
@@ -604,11 +704,13 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
604
704
|
if (Array.isArray(content)) {
|
|
605
705
|
for (const block of content) if (block.type === "tool_use" && block.name) {
|
|
606
706
|
if (!this.recordToolUse(block.name, block.id, block.input)) continue;
|
|
707
|
+
if (this.hasEmittedToolUseStart(block.name, block.input, block.id)) continue;
|
|
607
708
|
log$8.info("从 assistant 消息提取工具调用", {
|
|
608
709
|
toolName: block.name,
|
|
609
710
|
displayName: getToolDisplayName(block.name),
|
|
610
711
|
toolUseId: block.id
|
|
611
712
|
});
|
|
713
|
+
this.markToolUseStartEmitted(block.name, block.input, block.id);
|
|
612
714
|
this.emit("toolUseStart", block.name, block.input ?? {}, block.id);
|
|
613
715
|
}
|
|
614
716
|
}
|
|
@@ -651,6 +753,14 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
651
753
|
}
|
|
652
754
|
return true;
|
|
653
755
|
}
|
|
756
|
+
hasEmittedToolUseStart(toolName, toolInput, toolUseId) {
|
|
757
|
+
if (toolUseId && this.emittedToolUseIds.has(toolUseId)) return true;
|
|
758
|
+
return this.emittedToolUseFingerprints.has(fingerprintToolUse(toolName, toolInput));
|
|
759
|
+
}
|
|
760
|
+
markToolUseStartEmitted(toolName, toolInput, toolUseId) {
|
|
761
|
+
if (toolUseId) this.emittedToolUseIds.add(toolUseId);
|
|
762
|
+
this.emittedToolUseFingerprints.add(fingerprintToolUse(toolName, toolInput));
|
|
763
|
+
}
|
|
654
764
|
buildToolResultSummary(item, toolUseResult) {
|
|
655
765
|
const serializedContent = serializeToolResultContent(item.content);
|
|
656
766
|
const truncatedContent = truncateToolResultContent(serializedContent);
|
|
@@ -673,9 +783,10 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
673
783
|
handleToolProgress(msg) {
|
|
674
784
|
log$8.debug("工具执行进度", {
|
|
675
785
|
toolName: msg.tool_name,
|
|
786
|
+
toolUseId: msg.tool_use_id,
|
|
676
787
|
elapsed: msg.elapsed_time_seconds
|
|
677
788
|
});
|
|
678
|
-
this.emit("toolProgress", msg.tool_name, msg.elapsed_time_seconds);
|
|
789
|
+
this.emit("toolProgress", msg.tool_name, msg.elapsed_time_seconds, msg.tool_use_id);
|
|
679
790
|
}
|
|
680
791
|
/**
|
|
681
792
|
* 处理 result 消息 — CC 执行完成的最终结果
|
|
@@ -698,6 +809,19 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
698
809
|
});
|
|
699
810
|
}
|
|
700
811
|
};
|
|
812
|
+
function hasDurableToolInput(toolInput) {
|
|
813
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) return false;
|
|
814
|
+
return Object.keys(toolInput).length > 0;
|
|
815
|
+
}
|
|
816
|
+
function fingerprintToolUse(toolName, toolInput) {
|
|
817
|
+
return `${toolName}:${stableJsonStringify(toolInput)}`;
|
|
818
|
+
}
|
|
819
|
+
function stableJsonStringify(value) {
|
|
820
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? String(value);
|
|
821
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableJsonStringify(item)).join(",")}]`;
|
|
822
|
+
const input = value;
|
|
823
|
+
return `{${Object.keys(input).sort().map((key) => `${JSON.stringify(key)}:${stableJsonStringify(input[key])}`).join(",")}}`;
|
|
824
|
+
}
|
|
701
825
|
function serializeToolResultContent(content) {
|
|
702
826
|
if (typeof content === "string") return content;
|
|
703
827
|
return safeJsonStringify(content);
|
|
@@ -1211,7 +1335,7 @@ var SessionProcessManager = class {
|
|
|
1211
1335
|
if (config.model) args.push("--model", config.model);
|
|
1212
1336
|
args.push("--effort", CC_EFFORT);
|
|
1213
1337
|
try {
|
|
1214
|
-
const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId, cwd);
|
|
1338
|
+
const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId, cwd, config.policy, config.externalMcpProviders);
|
|
1215
1339
|
args.push("--mcp-config", mcpConfigPath);
|
|
1216
1340
|
log$6.info("MCP 配置已合并并注入到 CC 进程", {
|
|
1217
1341
|
sessionId,
|
|
@@ -1442,8 +1566,8 @@ var SessionProcessManager = class {
|
|
|
1442
1566
|
summary: this.lastMultimodalSummaries.get(sessionId)
|
|
1443
1567
|
}));
|
|
1444
1568
|
});
|
|
1445
|
-
parser.on("toolProgress", (toolName, elapsedSeconds) => {
|
|
1446
|
-
getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds);
|
|
1569
|
+
parser.on("toolProgress", (toolName, elapsedSeconds, toolUseId) => {
|
|
1570
|
+
getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds, toolUseId);
|
|
1447
1571
|
});
|
|
1448
1572
|
parser.on("turnEnd", (stopReason) => {
|
|
1449
1573
|
log$6.info("[perf] turnEnd", {
|
|
@@ -2420,7 +2544,7 @@ var CodexAppServerAdapter = class {
|
|
|
2420
2544
|
return existing;
|
|
2421
2545
|
}
|
|
2422
2546
|
const env = this.buildProcessEnv(config);
|
|
2423
|
-
const extraArgs = this.buildCodexArgs(config
|
|
2547
|
+
const extraArgs = this.buildCodexArgs(config);
|
|
2424
2548
|
const client = this.clientFactory({
|
|
2425
2549
|
sessionId: config.sessionId,
|
|
2426
2550
|
cwd: config.cwd,
|
|
@@ -2574,7 +2698,7 @@ var CodexAppServerAdapter = class {
|
|
|
2574
2698
|
const { toolName, toolInput } = this.describeToolItem(params.item);
|
|
2575
2699
|
if (!toolName) return;
|
|
2576
2700
|
this.rememberToolName(sessionId, params.item.id, toolName);
|
|
2577
|
-
callbacks.onToolUseStart?.(toolName, toolInput);
|
|
2701
|
+
callbacks.onToolUseStart?.(toolName, toolInput, params.item.id);
|
|
2578
2702
|
}
|
|
2579
2703
|
handleItemCompleted(sessionId, params) {
|
|
2580
2704
|
if (params.item.type === "contextCompaction") {
|
|
@@ -2605,7 +2729,7 @@ var CodexAppServerAdapter = class {
|
|
|
2605
2729
|
}
|
|
2606
2730
|
handleMcpProgress(sessionId, params) {
|
|
2607
2731
|
const toolName = this.lookupToolName(sessionId, params.itemId) ?? "mcp";
|
|
2608
|
-
this.activeCallbacks.get(sessionId)?.onToolProgress?.(toolName, 0);
|
|
2732
|
+
this.activeCallbacks.get(sessionId)?.onToolProgress?.(toolName, 0, params.itemId);
|
|
2609
2733
|
log$3.debug("Codex MCP tool progress", {
|
|
2610
2734
|
sessionId,
|
|
2611
2735
|
itemId: params.itemId,
|
|
@@ -2971,10 +3095,12 @@ var CodexAppServerAdapter = class {
|
|
|
2971
3095
|
}).catch(() => {})));
|
|
2972
3096
|
}
|
|
2973
3097
|
buildProcessEnv(config) {
|
|
3098
|
+
const disabledTools = serializeDisabledMcpTools(config.policy);
|
|
2974
3099
|
return {
|
|
2975
3100
|
...process.env,
|
|
2976
3101
|
LARKPAL_MCP_SESSION_ID: config.sessionId,
|
|
2977
3102
|
LARKPAL_MCP_CWD: config.cwd,
|
|
3103
|
+
...disabledTools ? { LARKPAL_MCP_DISABLED_TOOLS: disabledTools } : {},
|
|
2978
3104
|
LARKPAL_USER_ID: config.userContext?.userId ?? process.env.LARKPAL_USER_ID,
|
|
2979
3105
|
LARKPAL_USER_OPEN_ID: config.userContext?.openId ?? process.env.LARKPAL_USER_OPEN_ID,
|
|
2980
3106
|
LARKPAL_USER_NAME: config.userContext?.userName ?? process.env.LARKPAL_USER_NAME,
|
|
@@ -2982,14 +3108,16 @@ var CodexAppServerAdapter = class {
|
|
|
2982
3108
|
LARKPAL_IS_TENANT: config.userContext?.isTenantIdentity ? "1" : process.env.LARKPAL_IS_TENANT
|
|
2983
3109
|
};
|
|
2984
3110
|
}
|
|
2985
|
-
buildCodexArgs(
|
|
3111
|
+
buildCodexArgs(config) {
|
|
2986
3112
|
const { command, args } = getLarkpalMcpServerCommand();
|
|
3113
|
+
const disabledTools = serializeDisabledMcpTools(config.policy);
|
|
2987
3114
|
const env = {
|
|
2988
3115
|
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
2989
3116
|
HOME: process.env.HOME || homedir(),
|
|
2990
3117
|
NODE_PATH: process.env.NODE_PATH || "",
|
|
2991
|
-
LARKPAL_MCP_SESSION_ID: sessionId,
|
|
2992
|
-
...cwd ? { LARKPAL_MCP_CWD: cwd } : {},
|
|
3118
|
+
LARKPAL_MCP_SESSION_ID: config.sessionId,
|
|
3119
|
+
...config.cwd ? { LARKPAL_MCP_CWD: config.cwd } : {},
|
|
3120
|
+
...disabledTools ? { LARKPAL_MCP_DISABLED_TOOLS: disabledTools } : {},
|
|
2993
3121
|
LARKPAL_GATEWAY_PORT: process.env.LARKPAL_GATEWAY_PORT || "3000"
|
|
2994
3122
|
};
|
|
2995
3123
|
return [
|
|
@@ -3005,7 +3133,7 @@ var CodexAppServerAdapter = class {
|
|
|
3005
3133
|
if (config.llmConfig) log$3.warn("Codex app-server v1 暂不支持 per-request llmConfig,已忽略", { sessionId: config.sessionId });
|
|
3006
3134
|
if (config.maxBudgetUsd != null) log$3.warn("Codex app-server v1 暂不支持 maxBudgetUsd,已忽略", { sessionId: config.sessionId });
|
|
3007
3135
|
if (config.maxTurns != null) log$3.warn("Codex app-server v1 暂不支持 maxTurns,已忽略", { sessionId: config.sessionId });
|
|
3008
|
-
if (config.policy?.allowedTools
|
|
3136
|
+
if (config.policy?.allowedTools?.length) log$3.warn("Codex app-server v1 暂不强制执行 tool allow policy,已忽略", { sessionId: config.sessionId });
|
|
3009
3137
|
}
|
|
3010
3138
|
createCompletionPromise(sessionId) {
|
|
3011
3139
|
return new Promise((resolve) => {
|
|
@@ -4127,9 +4255,11 @@ function buildAgentRuntimeConfig(input) {
|
|
|
4127
4255
|
});
|
|
4128
4256
|
const policy = buildExecutionPolicy(input.policy, {
|
|
4129
4257
|
maxTurns: input.maxTurns,
|
|
4130
|
-
maxBudgetUsd: input.maxBudgetUsd
|
|
4258
|
+
maxBudgetUsd: input.maxBudgetUsd,
|
|
4259
|
+
allowSignalNoReplyTool: input.allowSignalNoReplyTool
|
|
4131
4260
|
});
|
|
4132
4261
|
const userContext = buildUserContext(input.identity);
|
|
4262
|
+
const externalMcpProviders = input.externalMcpProviders ?? loadExternalMcpProvidersFromEnv();
|
|
4133
4263
|
const requestContext = {
|
|
4134
4264
|
requestId,
|
|
4135
4265
|
sessionId,
|
|
@@ -4180,9 +4310,56 @@ function buildAgentRuntimeConfig(input) {
|
|
|
4180
4310
|
authHeaders,
|
|
4181
4311
|
metadata
|
|
4182
4312
|
},
|
|
4313
|
+
externalMcpProviders,
|
|
4183
4314
|
metadata
|
|
4184
4315
|
};
|
|
4185
4316
|
}
|
|
4317
|
+
function loadExternalMcpProvidersFromEnv(env = process.env) {
|
|
4318
|
+
const raw = env.LARKPAL_EXTERNAL_MCP_PROVIDERS;
|
|
4319
|
+
if (!raw?.trim()) return void 0;
|
|
4320
|
+
const parsed = JSON.parse(raw);
|
|
4321
|
+
if (!Array.isArray(parsed)) throw new Error("LARKPAL_EXTERNAL_MCP_PROVIDERS must be a JSON array");
|
|
4322
|
+
return parsed.map((entry, index) => normalizeExternalMcpProvider(entry, index));
|
|
4323
|
+
}
|
|
4324
|
+
function normalizeExternalMcpProvider(entry, index) {
|
|
4325
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}] must be an object`);
|
|
4326
|
+
const record = entry;
|
|
4327
|
+
return {
|
|
4328
|
+
name: readRequiredString(record.name, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].name`),
|
|
4329
|
+
url: readRequiredString(record.url, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].url`),
|
|
4330
|
+
headers: readStringRecord(record.headers, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].headers`),
|
|
4331
|
+
headerEnv: readStringRecord(record.headerEnv, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].headerEnv`),
|
|
4332
|
+
headersJsonEnv: readOptionalString(record.headersJsonEnv),
|
|
4333
|
+
toolAllowlist: readStringArray(record.toolAllowlist, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].toolAllowlist`)
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
function readRequiredString(value, field) {
|
|
4337
|
+
const text = readOptionalString(value);
|
|
4338
|
+
if (!text) throw new Error(`${field} is required`);
|
|
4339
|
+
return text;
|
|
4340
|
+
}
|
|
4341
|
+
function readOptionalString(value) {
|
|
4342
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
4343
|
+
}
|
|
4344
|
+
function readStringRecord(value, field) {
|
|
4345
|
+
if (value === void 0) return void 0;
|
|
4346
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object`);
|
|
4347
|
+
const result = {};
|
|
4348
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4349
|
+
if (typeof entry !== "string") throw new Error(`${field}.${key} must be a string`);
|
|
4350
|
+
result[key] = entry;
|
|
4351
|
+
}
|
|
4352
|
+
return result;
|
|
4353
|
+
}
|
|
4354
|
+
function readStringArray(value, field) {
|
|
4355
|
+
if (value === void 0) return void 0;
|
|
4356
|
+
if (!Array.isArray(value)) throw new Error(`${field} must be an array`);
|
|
4357
|
+
const result = value.map((entry, index) => {
|
|
4358
|
+
if (typeof entry !== "string" || !entry.trim()) throw new Error(`${field}[${index}] must be a non-empty string`);
|
|
4359
|
+
return entry.trim();
|
|
4360
|
+
});
|
|
4361
|
+
return result.length ? result : void 0;
|
|
4362
|
+
}
|
|
4186
4363
|
function getRunInputValue(...values) {
|
|
4187
4364
|
return values.find((value) => value !== void 0);
|
|
4188
4365
|
}
|
|
@@ -4298,7 +4475,7 @@ function buildAuthHeaders(params) {
|
|
|
4298
4475
|
return headers;
|
|
4299
4476
|
}
|
|
4300
4477
|
function buildExecutionPolicy(policy, limits) {
|
|
4301
|
-
|
|
4478
|
+
const base = {
|
|
4302
4479
|
allowedTools: policy?.allowedTools ?? [],
|
|
4303
4480
|
disallowedTools: policy?.disallowedTools,
|
|
4304
4481
|
allowedSkills: policy?.allowedSkills ?? [],
|
|
@@ -4309,6 +4486,7 @@ function buildExecutionPolicy(policy, limits) {
|
|
|
4309
4486
|
maxBudgetUsd: limits.maxBudgetUsd ?? policy?.maxBudgetUsd,
|
|
4310
4487
|
turnPolicy: policy?.turnPolicy
|
|
4311
4488
|
};
|
|
4489
|
+
return limits.allowSignalNoReplyTool ? base : appendDisallowedTool(base, SIGNAL_NO_REPLY_TOOL);
|
|
4312
4490
|
}
|
|
4313
4491
|
function buildUserContext(identity) {
|
|
4314
4492
|
if (!identity?.userId && !identity?.openId) return void 0;
|
|
@@ -5492,6 +5670,7 @@ async function runFastDecision(params) {
|
|
|
5492
5670
|
model: params.model,
|
|
5493
5671
|
transcriptMode: "ephemeral",
|
|
5494
5672
|
maxTurns: 1,
|
|
5673
|
+
allowSignalNoReplyTool: true,
|
|
5495
5674
|
policy: {
|
|
5496
5675
|
disallowedTools: [{ tool: "ask_user" }, { tool: "request_permission" }],
|
|
5497
5676
|
maxTurns: 1,
|