@vibe-lark/larkpal 0.1.34 → 0.1.37
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/bin/larkpal.js +0 -0
- package/dist/main.mjs +271 -19
- package/package.json +13 -14
package/bin/larkpal.js
CHANGED
|
File without changes
|
package/dist/main.mjs
CHANGED
|
@@ -375,7 +375,8 @@ const TOOL_DISPLAY_NAMES = {
|
|
|
375
375
|
WebSearch: "正在搜索网页",
|
|
376
376
|
WebFetch: "正在获取网页内容",
|
|
377
377
|
TodoWrite: "正在更新任务列表",
|
|
378
|
-
Task: "正在执行子任务"
|
|
378
|
+
Task: "正在执行子任务",
|
|
379
|
+
_AutoCompact: "正在压缩对话历史"
|
|
379
380
|
};
|
|
380
381
|
/** 获取工具的中文显示名称,未知工具返回通用描述 */
|
|
381
382
|
function getToolDisplayName(toolName) {
|
|
@@ -420,6 +421,13 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
420
421
|
this.emit("parseError", err, trimmed);
|
|
421
422
|
return;
|
|
422
423
|
}
|
|
424
|
+
if (msg.parent_tool_use_id != null && msg.type !== "result") {
|
|
425
|
+
log$32.debug("跳过 subagent 内部事件", {
|
|
426
|
+
type: msg.type,
|
|
427
|
+
parentToolUseId: msg.parent_tool_use_id
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
423
431
|
switch (msg.type) {
|
|
424
432
|
case "stream_event":
|
|
425
433
|
this.handleStreamEvent(msg);
|
|
@@ -524,6 +532,11 @@ var CCStreamParser = class extends EventEmitter {
|
|
|
524
532
|
this.emit("toolUseStart", block.name, block.input ?? {});
|
|
525
533
|
}
|
|
526
534
|
}
|
|
535
|
+
const usage = msg.message?.usage;
|
|
536
|
+
if (usage && typeof usage.input_tokens === "number") this.emit("usage", {
|
|
537
|
+
input_tokens: usage.input_tokens,
|
|
538
|
+
output_tokens: usage.output_tokens
|
|
539
|
+
});
|
|
527
540
|
this.emit("assistant", msg);
|
|
528
541
|
}
|
|
529
542
|
/**
|
|
@@ -876,6 +889,37 @@ const IDLE_TIMEOUT_MS = (() => {
|
|
|
876
889
|
if (isNaN(seconds) || seconds < 0) return 0;
|
|
877
890
|
return seconds * 1e3;
|
|
878
891
|
})();
|
|
892
|
+
/**
|
|
893
|
+
* 自动压缩阈值(input_tokens 数)。
|
|
894
|
+
*
|
|
895
|
+
* 当 CC 返回的 input_tokens 超过此阈值时,在下一次用户消息发送前
|
|
896
|
+
* 自动执行 /compact 命令压缩对话历史。
|
|
897
|
+
*
|
|
898
|
+
* 通过 LARKPAL_CC_AUTO_COMPACT_THRESHOLD 环境变量配置,默认 50000(50K tokens)。
|
|
899
|
+
* 设为 0 表示禁用自动压缩。
|
|
900
|
+
*
|
|
901
|
+
* 参考数据:200K context 模型在 50K tokens 时 TTFT ≈ 7-10s(线性区间),
|
|
902
|
+
* 超过后延迟快速增长。压缩后目标约 20-30K tokens。
|
|
903
|
+
*/
|
|
904
|
+
const AUTO_COMPACT_THRESHOLD = (() => {
|
|
905
|
+
const envVal = process.env.LARKPAL_CC_AUTO_COMPACT_THRESHOLD;
|
|
906
|
+
if (envVal === void 0 || envVal === "") return 5e4;
|
|
907
|
+
const tokens = parseInt(envVal, 10);
|
|
908
|
+
if (isNaN(tokens) || tokens < 0) return 5e4;
|
|
909
|
+
return tokens;
|
|
910
|
+
})();
|
|
911
|
+
/**
|
|
912
|
+
* CC 推理努力级别。
|
|
913
|
+
*
|
|
914
|
+
* 控制模型的 thinking budget(思维深度),影响延迟和成本:
|
|
915
|
+
* low — 最少思考,快速响应(简单问答)
|
|
916
|
+
* medium — 适度思考(推荐:平衡延迟与质量)
|
|
917
|
+
* high — 较深思考(默认 CC 行为)
|
|
918
|
+
* xhigh/max — 最大思考(复杂推理)
|
|
919
|
+
*
|
|
920
|
+
* 通过 LARKPAL_CC_EFFORT 环境变量配置,默认 medium。
|
|
921
|
+
*/
|
|
922
|
+
const CC_EFFORT = process.env.LARKPAL_CC_EFFORT || "medium";
|
|
879
923
|
/** 优雅关闭等待时间(5 秒) */
|
|
880
924
|
const GRACEFUL_SHUTDOWN_MS = 5e3;
|
|
881
925
|
var SessionProcessManager = class {
|
|
@@ -920,6 +964,13 @@ var SessionProcessManager = class {
|
|
|
920
964
|
messageCompletionResolvers = /* @__PURE__ */ new Map();
|
|
921
965
|
/** 消息级串行锁:确保同一 session 的消息按顺序处理完成 */
|
|
922
966
|
sessionLocks = /* @__PURE__ */ new Map();
|
|
967
|
+
/**
|
|
968
|
+
* 最近一次 CC 响应的 input_tokens:sessionId → token 数
|
|
969
|
+
*
|
|
970
|
+
* 由 parser 的 usage 事件更新。用于在下一次 executePrompt 前判断
|
|
971
|
+
* 是否需要自动触发 /compact 压缩对话历史。
|
|
972
|
+
*/
|
|
973
|
+
lastInputTokens = /* @__PURE__ */ new Map();
|
|
923
974
|
/** 标记 session 为用户主动中断 */
|
|
924
975
|
markAborted(sessionId) {
|
|
925
976
|
this.abortedSessions.add(sessionId);
|
|
@@ -983,6 +1034,7 @@ var SessionProcessManager = class {
|
|
|
983
1034
|
pid: existing.childProcess.pid,
|
|
984
1035
|
prepareMs: Date.now() - t0
|
|
985
1036
|
});
|
|
1037
|
+
await this.autoCompactIfNeeded(sessionId);
|
|
986
1038
|
this.sendMessage(sessionId, config.prompt);
|
|
987
1039
|
this.resetIdleTimer(sessionId);
|
|
988
1040
|
await completionPromise;
|
|
@@ -1042,6 +1094,7 @@ var SessionProcessManager = class {
|
|
|
1042
1094
|
if (config.maxTurns) args.push("--max-turns", String(config.maxTurns));
|
|
1043
1095
|
if (config.maxBudgetUsd) args.push("--max-budget-usd", String(config.maxBudgetUsd));
|
|
1044
1096
|
if (config.model) args.push("--model", config.model);
|
|
1097
|
+
args.push("--effort", CC_EFFORT);
|
|
1045
1098
|
if (config.userContext && !config.userContext.isTenantIdentity) try {
|
|
1046
1099
|
const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext, sessionId);
|
|
1047
1100
|
if (mcpConfigPath) {
|
|
@@ -1283,6 +1336,16 @@ var SessionProcessManager = class {
|
|
|
1283
1336
|
getCallbacks()?.onResult?.(result);
|
|
1284
1337
|
this.resolveMessageCompletion(sessionId);
|
|
1285
1338
|
});
|
|
1339
|
+
parser.on("usage", (usage) => {
|
|
1340
|
+
this.lastInputTokens.set(sessionId, usage.input_tokens);
|
|
1341
|
+
log$29.info("[auto-compact] 更新 input_tokens", {
|
|
1342
|
+
sessionId,
|
|
1343
|
+
inputTokens: usage.input_tokens,
|
|
1344
|
+
outputTokens: usage.output_tokens,
|
|
1345
|
+
threshold: AUTO_COMPACT_THRESHOLD,
|
|
1346
|
+
needsCompact: AUTO_COMPACT_THRESHOLD > 0 && usage.input_tokens > AUTO_COMPACT_THRESHOLD
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
1286
1349
|
parser.on("parseError", (error, rawLine) => {
|
|
1287
1350
|
log$29.warn("流解析错误", {
|
|
1288
1351
|
sessionId,
|
|
@@ -1382,6 +1445,111 @@ var SessionProcessManager = class {
|
|
|
1382
1445
|
});
|
|
1383
1446
|
}
|
|
1384
1447
|
/**
|
|
1448
|
+
* 检查是否需要自动压缩,如需要则执行 /compact 命令
|
|
1449
|
+
*
|
|
1450
|
+
* 判断逻辑:
|
|
1451
|
+
* 1. AUTO_COMPACT_THRESHOLD > 0(未禁用)
|
|
1452
|
+
* 2. lastInputTokens[sessionId] > threshold
|
|
1453
|
+
*
|
|
1454
|
+
* 执行方式:通过 stdin 发送 "/compact" 作为用户消息,等待 CC 返回 result。
|
|
1455
|
+
* compact 期间:
|
|
1456
|
+
* - 通过 callbacks.onToolUseStart 通知 UI 显示"正在压缩对话历史"
|
|
1457
|
+
* - compact 产生的文本/result 被静默回调吞掉,不泄露到用户卡片
|
|
1458
|
+
* - compact 完成后通过 callbacks.onToolResult 通知 UI 压缩完成
|
|
1459
|
+
*/
|
|
1460
|
+
async autoCompactIfNeeded(sessionId) {
|
|
1461
|
+
if (AUTO_COMPACT_THRESHOLD <= 0) return;
|
|
1462
|
+
const lastTokens = this.lastInputTokens.get(sessionId);
|
|
1463
|
+
if (!lastTokens || lastTokens <= AUTO_COMPACT_THRESHOLD) return;
|
|
1464
|
+
log$29.info("[auto-compact] 触发自动压缩", {
|
|
1465
|
+
sessionId,
|
|
1466
|
+
lastInputTokens: lastTokens,
|
|
1467
|
+
threshold: AUTO_COMPACT_THRESHOLD
|
|
1468
|
+
});
|
|
1469
|
+
const tCompact = Date.now();
|
|
1470
|
+
const realCallbacks = this.activeCallbacks.get(sessionId);
|
|
1471
|
+
const compactToolUseId = `compact-${Date.now()}`;
|
|
1472
|
+
realCallbacks?.onToolUseStart?.("_AutoCompact", {
|
|
1473
|
+
inputTokens: lastTokens,
|
|
1474
|
+
threshold: AUTO_COMPACT_THRESHOLD
|
|
1475
|
+
});
|
|
1476
|
+
this.activeCallbacks.set(sessionId, {
|
|
1477
|
+
onTextDelta: () => {},
|
|
1478
|
+
onThinkingDelta: () => {},
|
|
1479
|
+
onToolUseStart: () => {},
|
|
1480
|
+
onToolResult: () => {},
|
|
1481
|
+
onToolProgress: () => {},
|
|
1482
|
+
onTurnEnd: () => {},
|
|
1483
|
+
onResult: () => {},
|
|
1484
|
+
onError: (err) => {
|
|
1485
|
+
log$29.warn("[auto-compact] compact 期间收到错误", {
|
|
1486
|
+
sessionId,
|
|
1487
|
+
error: err.message
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
try {
|
|
1492
|
+
await this.sendCompactCommand(sessionId);
|
|
1493
|
+
this.lastInputTokens.delete(sessionId);
|
|
1494
|
+
log$29.info("[auto-compact] 压缩完成", {
|
|
1495
|
+
sessionId,
|
|
1496
|
+
durationMs: Date.now() - tCompact
|
|
1497
|
+
});
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
log$29.warn("[auto-compact] 压缩失败,继续发送原始消息", {
|
|
1500
|
+
sessionId,
|
|
1501
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1502
|
+
durationMs: Date.now() - tCompact
|
|
1503
|
+
});
|
|
1504
|
+
} finally {
|
|
1505
|
+
if (realCallbacks) this.activeCallbacks.set(sessionId, realCallbacks);
|
|
1506
|
+
realCallbacks?.onToolResult?.(compactToolUseId);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* 向 CC 进程发送 /compact 命令并等待完成
|
|
1511
|
+
*
|
|
1512
|
+
* /compact 是 CC 的内置 slash command,通过 stdin 作为用户消息发送。
|
|
1513
|
+
* CC 会压缩对话历史并返回 result 事件表示完成。
|
|
1514
|
+
*
|
|
1515
|
+
* 超时 120 秒(compact 可能需要处理大量历史)。
|
|
1516
|
+
*/
|
|
1517
|
+
sendCompactCommand(sessionId) {
|
|
1518
|
+
const proc = this.processes.get(sessionId);
|
|
1519
|
+
if (!proc || proc.status !== "running") return Promise.reject(/* @__PURE__ */ new Error(`会话 ${sessionId} 无运行中的进程,无法执行 compact`));
|
|
1520
|
+
if (!proc.childProcess.stdin || proc.childProcess.stdin.destroyed) return Promise.reject(/* @__PURE__ */ new Error(`会话 ${sessionId} 的 stdin 不可用,无法执行 compact`));
|
|
1521
|
+
const COMPACT_TIMEOUT_MS = 12e4;
|
|
1522
|
+
return new Promise((resolve, reject) => {
|
|
1523
|
+
const timer = setTimeout(() => {
|
|
1524
|
+
reject(/* @__PURE__ */ new Error(`/compact 超时 (${COMPACT_TIMEOUT_MS}ms)`));
|
|
1525
|
+
}, COMPACT_TIMEOUT_MS);
|
|
1526
|
+
const completionPromise = this.createMessageCompletionPromise(sessionId);
|
|
1527
|
+
const compactPayload = JSON.stringify({
|
|
1528
|
+
type: "user",
|
|
1529
|
+
message: {
|
|
1530
|
+
role: "user",
|
|
1531
|
+
content: "/compact"
|
|
1532
|
+
},
|
|
1533
|
+
parent_tool_use_id: null,
|
|
1534
|
+
uuid: v4()
|
|
1535
|
+
});
|
|
1536
|
+
log$29.info("[auto-compact] 发送 /compact 命令", {
|
|
1537
|
+
sessionId,
|
|
1538
|
+
pid: proc.childProcess.pid
|
|
1539
|
+
});
|
|
1540
|
+
proc.childProcess.stdin.write(compactPayload + "\n", (err) => {
|
|
1541
|
+
if (err) {
|
|
1542
|
+
clearTimeout(timer);
|
|
1543
|
+
reject(/* @__PURE__ */ new Error(`/compact stdin 写入失败: ${err.message}`));
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
completionPromise.then(() => {
|
|
1547
|
+
clearTimeout(timer);
|
|
1548
|
+
resolve();
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1385
1553
|
* 获取指定会话的最近一条用户消息 ID
|
|
1386
1554
|
*
|
|
1387
1555
|
* 用于构建回退按钮的 action value。
|
|
@@ -1397,6 +1565,10 @@ var SessionProcessManager = class {
|
|
|
1397
1565
|
getSessionCwd(sessionId) {
|
|
1398
1566
|
return this.sessionCwdCache.get(sessionId);
|
|
1399
1567
|
}
|
|
1568
|
+
/** 检查指定 session 是否正在执行消息(CC 尚未返回 result) */
|
|
1569
|
+
isSessionBusy(sessionId) {
|
|
1570
|
+
return this.messageCompletionResolvers.has(sessionId);
|
|
1571
|
+
}
|
|
1400
1572
|
/**
|
|
1401
1573
|
* 为 rewind 操作临时启动一个 CC 进程
|
|
1402
1574
|
*
|
|
@@ -1526,6 +1698,7 @@ var SessionProcessManager = class {
|
|
|
1526
1698
|
this.processes.delete(sessionId);
|
|
1527
1699
|
this.activeCallbacks.delete(sessionId);
|
|
1528
1700
|
this.lastUserMessageIds.delete(sessionId);
|
|
1701
|
+
this.lastInputTokens.delete(sessionId);
|
|
1529
1702
|
for (const [reqId, pending] of this.pendingControlRequests) if (pending.sessionId === sessionId) {
|
|
1530
1703
|
clearTimeout(pending.timer);
|
|
1531
1704
|
this.pendingControlRequests.delete(reqId);
|
|
@@ -1645,6 +1818,9 @@ var ClaudeCodeAdapter = class ClaudeCodeAdapter {
|
|
|
1645
1818
|
lastActiveAt: proc.lastActiveAt
|
|
1646
1819
|
}));
|
|
1647
1820
|
}
|
|
1821
|
+
isSessionBusy(sessionId) {
|
|
1822
|
+
return this.processManager.isSessionBusy(sessionId);
|
|
1823
|
+
}
|
|
1648
1824
|
};
|
|
1649
1825
|
//#endregion
|
|
1650
1826
|
//#region src/runtime/larkpal-agent-factory.ts
|
|
@@ -5819,7 +5995,7 @@ var CCStreamBridge = class {
|
|
|
5819
5995
|
});
|
|
5820
5996
|
if (this.options.autoCompleteOnTurnEnd && stopReason === "end_turn") {
|
|
5821
5997
|
const trimmedText = this.accumulatedText.trim();
|
|
5822
|
-
if (trimmedText === CC_INTERNAL_PLACEHOLDER
|
|
5998
|
+
if (trimmedText === CC_INTERNAL_PLACEHOLDER) {
|
|
5823
5999
|
log$19.info("检测到 CC 内部占位消息,静默丢弃", {
|
|
5824
6000
|
text: trimmedText.slice(0, 50),
|
|
5825
6001
|
sessionKey: this.options.sessionKey
|
|
@@ -5827,6 +6003,10 @@ var CCStreamBridge = class {
|
|
|
5827
6003
|
this.controller.abortCard();
|
|
5828
6004
|
return;
|
|
5829
6005
|
}
|
|
6006
|
+
if (trimmedText === "") {
|
|
6007
|
+
log$19.info("turnEnd 时文本为空,延迟到 onResult 兜底处理", { sessionKey: this.options.sessionKey });
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
5830
6010
|
this.controller.markFullyComplete();
|
|
5831
6011
|
this.controller.onIdle();
|
|
5832
6012
|
}
|
|
@@ -5860,7 +6040,19 @@ var CCStreamBridge = class {
|
|
|
5860
6040
|
});
|
|
5861
6041
|
this.controller.onError(new Error(errorMessage), { kind: "cc-execution" });
|
|
5862
6042
|
}
|
|
6043
|
+
} else if (!this.accumulatedText.trim()) if (data.result) {
|
|
6044
|
+
log$19.info("result 兜底:使用 result.result 作为最终回复", {
|
|
6045
|
+
resultLen: data.result.length,
|
|
6046
|
+
sessionKey: this.options.sessionKey
|
|
6047
|
+
});
|
|
6048
|
+
this.controller.onPartialReply({ text: data.result });
|
|
6049
|
+
this.controller.markFullyComplete();
|
|
6050
|
+
this.controller.onIdle();
|
|
5863
6051
|
} else {
|
|
6052
|
+
log$19.info("result 兜底:无文本且无 result,abort 卡片", { sessionKey: this.options.sessionKey });
|
|
6053
|
+
this.controller.abortCard();
|
|
6054
|
+
}
|
|
6055
|
+
else {
|
|
5864
6056
|
this.controller.markFullyComplete();
|
|
5865
6057
|
this.controller.onIdle();
|
|
5866
6058
|
}
|
|
@@ -9731,7 +9923,7 @@ function formatContentByType(ctx) {
|
|
|
9731
9923
|
}
|
|
9732
9924
|
return content;
|
|
9733
9925
|
case "merge_forward": return content;
|
|
9734
|
-
case "audio": return "[语音消息
|
|
9926
|
+
case "audio": return content || "[语音消息]";
|
|
9735
9927
|
case "sticker": return content || "[表情贴纸]";
|
|
9736
9928
|
case "video":
|
|
9737
9929
|
case "media": return content || "[视频消息]";
|
|
@@ -9856,7 +10048,7 @@ async function dispatchToCC(params) {
|
|
|
9856
10048
|
const replyInThread = !!threadId;
|
|
9857
10049
|
const replyToMessageId = threadId ? void 0 : ctx.messageId;
|
|
9858
10050
|
let textPrompt = formatForCC(ctx, isGroup);
|
|
9859
|
-
if (isGroup) textPrompt = `[
|
|
10051
|
+
if (isGroup) textPrompt = `[有人@你] ${textPrompt}`;
|
|
9860
10052
|
const imageResources = ctx.resources.filter((r) => r.type === "image");
|
|
9861
10053
|
let prompt = textPrompt;
|
|
9862
10054
|
if (imageResources.length > 0) {
|
|
@@ -9942,6 +10134,28 @@ async function dispatchToCC(params) {
|
|
|
9942
10134
|
textLength: textPrompt.length,
|
|
9943
10135
|
imageCount: imageBlocks.length
|
|
9944
10136
|
});
|
|
10137
|
+
} else {
|
|
10138
|
+
const textWithoutImageRefs = textPrompt.replace(/!\[image\]\([^)]*\)/g, "").replace(/\[图片\]\s*/g, "").trim();
|
|
10139
|
+
if (textWithoutImageRefs.length > 0) {
|
|
10140
|
+
prompt = textPrompt.replace(/!\[image\]\([^)]*\)/g, "[图片加载失败]");
|
|
10141
|
+
log$12.info("图片下载失败但保留文字内容继续调度", {
|
|
10142
|
+
messageId: ctx.messageId,
|
|
10143
|
+
textLength: textWithoutImageRefs.length
|
|
10144
|
+
});
|
|
10145
|
+
} else {
|
|
10146
|
+
log$12.warn("纯图片消息且图片全部下载失败,中止调度", {
|
|
10147
|
+
messageId: ctx.messageId,
|
|
10148
|
+
expectedImages: imageResources.length
|
|
10149
|
+
});
|
|
10150
|
+
await LarkClient.fromAccount(account).sdk.im.message.reply({
|
|
10151
|
+
path: { message_id: ctx.messageId },
|
|
10152
|
+
data: {
|
|
10153
|
+
content: JSON.stringify({ text: "图片下载失败,请稍后重新发送。" }),
|
|
10154
|
+
msg_type: "text"
|
|
10155
|
+
}
|
|
10156
|
+
});
|
|
10157
|
+
return;
|
|
10158
|
+
}
|
|
9945
10159
|
}
|
|
9946
10160
|
}
|
|
9947
10161
|
const attachmentResources = ctx.resources.filter((r) => r.type === "file" || r.type === "audio" || r.type === "video" || r.type === "sticker");
|
|
@@ -10049,11 +10263,19 @@ async function dispatchToCC(params) {
|
|
|
10049
10263
|
}
|
|
10050
10264
|
}
|
|
10051
10265
|
} catch (err) {
|
|
10052
|
-
log$12.warn("
|
|
10266
|
+
log$12.warn("非图片附件下载失败,替换为降级描述", {
|
|
10053
10267
|
type: res.type,
|
|
10054
10268
|
fileKey: res.fileKey,
|
|
10055
10269
|
error: err instanceof Error ? err.message : String(err)
|
|
10056
10270
|
});
|
|
10271
|
+
const fallbackRegex = new RegExp(`<${res.type}\\s+key="${res.fileKey}"[^/]*/>`, "g");
|
|
10272
|
+
const fallbackMap = {
|
|
10273
|
+
file: `[文件: ${res.fileName || "未知"} - 下载失败]`,
|
|
10274
|
+
audio: "[语音消息 - 下载失败]",
|
|
10275
|
+
video: `[视频: ${res.fileName || "未知"} - 下载失败]`,
|
|
10276
|
+
sticker: "[表情贴纸 - 加载失败]"
|
|
10277
|
+
};
|
|
10278
|
+
updatedTextPrompt = updatedTextPrompt.replace(fallbackRegex, fallbackMap[res.type] || `[${res.type} - 下载失败]`);
|
|
10057
10279
|
}
|
|
10058
10280
|
if (typeof prompt === "string") prompt = updatedTextPrompt;
|
|
10059
10281
|
else {
|
|
@@ -10263,6 +10485,13 @@ async function dispatchTeammateEval(params) {
|
|
|
10263
10485
|
userId: void 0,
|
|
10264
10486
|
userName: void 0
|
|
10265
10487
|
});
|
|
10488
|
+
if (processManager.isSessionBusy?.(route.sessionId)) {
|
|
10489
|
+
log$12.info("dispatchTeammateEval 跳过:主 session 正在执行", {
|
|
10490
|
+
chatId,
|
|
10491
|
+
sessionId: route.sessionId
|
|
10492
|
+
});
|
|
10493
|
+
return { replied: false };
|
|
10494
|
+
}
|
|
10266
10495
|
await sessionRouter.ensureSessionDirectory(route);
|
|
10267
10496
|
const teammateSessionKey = `teammate_${route.sessionId}`;
|
|
10268
10497
|
startToolUseTraceRun(teammateSessionKey);
|
|
@@ -10331,11 +10560,12 @@ async function dispatchTeammateEval(params) {
|
|
|
10331
10560
|
pendingToolEvents.length = 0;
|
|
10332
10561
|
};
|
|
10333
10562
|
/**
|
|
10334
|
-
* 处理 text delta —
|
|
10563
|
+
* 处理 text delta — 前缀检测 + 缓冲
|
|
10335
10564
|
*
|
|
10336
|
-
* 策略:CC
|
|
10337
|
-
*
|
|
10338
|
-
*
|
|
10565
|
+
* 策略:prompt 要求 CC 如果决定不参与,必须首先输出 NO_REPLY。
|
|
10566
|
+
* 因此在缓冲阶段做前缀检测:如果 fullTextBuffer 以 NO_REPLY 开头则立即静默。
|
|
10567
|
+
* 这比等 end_turn 快得多(可提前 10-30s 结束评估等待)。
|
|
10568
|
+
* 兜底:end_turn 时再做最终判断(应对 CC 不遵守 prompt 约束的情况)。
|
|
10339
10569
|
*/
|
|
10340
10570
|
const handleTextDelta = (text) => {
|
|
10341
10571
|
if (silenced) return;
|
|
@@ -10344,6 +10574,13 @@ async function dispatchTeammateEval(params) {
|
|
|
10344
10574
|
return;
|
|
10345
10575
|
}
|
|
10346
10576
|
fullTextBuffer += text;
|
|
10577
|
+
if (fullTextBuffer.trimStart().startsWith(NO_REPLY_TOKEN)) {
|
|
10578
|
+
silenced = true;
|
|
10579
|
+
log$12.info("teammate 前缀检测: 检测到 NO_REPLY 首输出,立即静默", {
|
|
10580
|
+
chatId,
|
|
10581
|
+
bufferLen: fullTextBuffer.length
|
|
10582
|
+
});
|
|
10583
|
+
}
|
|
10347
10584
|
};
|
|
10348
10585
|
try {
|
|
10349
10586
|
await new Promise((resolve) => {
|
|
@@ -12379,13 +12616,13 @@ async function parseMessageEvent(event, botOpenId, expandCtx) {
|
|
|
12379
12616
|
const mentionsByOpenId = /* @__PURE__ */ new Map();
|
|
12380
12617
|
for (const info of mentionList) mentionsByOpenId.set(info.openId, info);
|
|
12381
12618
|
const acctId = expandCtx?.accountId;
|
|
12382
|
-
const larkClient = expandCtx ? LarkClient.fromCfg(expandCtx.cfg, acctId) : void 0;
|
|
12619
|
+
const larkClient = expandCtx ? expandCtx.larkClient ?? (expandCtx.cfg ? LarkClient.fromCfg(expandCtx.cfg, acctId) : void 0) : void 0;
|
|
12383
12620
|
let fetchSubMessages;
|
|
12384
12621
|
let batchResolveNames;
|
|
12385
|
-
if (expandCtx) {
|
|
12386
|
-
const account = getLarkAccount(expandCtx.cfg, acctId);
|
|
12622
|
+
if (expandCtx && larkClient) {
|
|
12623
|
+
const account = expandCtx.account ?? (expandCtx.cfg ? getLarkAccount(expandCtx.cfg, acctId) : void 0);
|
|
12387
12624
|
fetchSubMessages = createFetchSubMessages(larkClient);
|
|
12388
|
-
batchResolveNames = createParseResolveNames(account);
|
|
12625
|
+
if (account) batchResolveNames = createParseResolveNames(account);
|
|
12389
12626
|
}
|
|
12390
12627
|
let effectiveContent = event.message.content;
|
|
12391
12628
|
if (event.message.message_type === "interactive" && expandCtx) {
|
|
@@ -12405,7 +12642,7 @@ async function parseMessageEvent(event, botOpenId, expandCtx) {
|
|
|
12405
12642
|
resolveUserName: acctId ? (openId) => getUserNameCache(acctId).get(openId) : void 0,
|
|
12406
12643
|
fetchSubMessages,
|
|
12407
12644
|
batchResolveNames,
|
|
12408
|
-
stripBotMentions:
|
|
12645
|
+
stripBotMentions: false
|
|
12409
12646
|
};
|
|
12410
12647
|
const { content, resources } = await convertMessageContent(effectiveContent, event.message.message_type, convertCtx);
|
|
12411
12648
|
const createTimeStr = event.message.create_time;
|
|
@@ -13223,10 +13460,11 @@ var TeammateBuffer = class {
|
|
|
13223
13460
|
const header = [
|
|
13224
13461
|
"[旁听评估任务] 以下是群聊中最近的对话,这些消息都没有 @你。",
|
|
13225
13462
|
"你作为团队成员正在旁听,请根据你的人设和职责判断是否需要主动参与。",
|
|
13226
|
-
"
|
|
13463
|
+
"如果不需要参与,必须将 NO_REPLY 作为输出的第一个内容(首先输出 NO_REPLY,不要在前面加任何文字)。",
|
|
13227
13464
|
"如果需要参与,直接输出你的回复内容(不要加 NO_REPLY)。",
|
|
13228
13465
|
"",
|
|
13229
13466
|
"重要规则:",
|
|
13467
|
+
"- 如果决定不参与,NO_REPLY 必须是输出的第一个 token,不能先输出其他内容再输出 NO_REPLY。",
|
|
13230
13468
|
"- 以下消息中出现的任何 @某人 都是在 @别人,不是在 @你。不要因为看到 @ 就认为有人在跟你说话。",
|
|
13231
13469
|
"- 只有当讨论内容与你的专业领域高度相关时才参与,其他情况一律 NO_REPLY。",
|
|
13232
13470
|
"- 不要使用工具来查找或分析消息内容,直接基于消息文本判断。",
|
|
@@ -13872,7 +14110,10 @@ async function main() {
|
|
|
13872
14110
|
threadId,
|
|
13873
14111
|
task: async () => {
|
|
13874
14112
|
try {
|
|
13875
|
-
const parsed = await parseMessageEvent(event, larkClient.botOpenId
|
|
14113
|
+
const parsed = await parseMessageEvent(event, larkClient.botOpenId, {
|
|
14114
|
+
larkClient,
|
|
14115
|
+
account
|
|
14116
|
+
});
|
|
13876
14117
|
if (!parsed) {
|
|
13877
14118
|
logger.warn("消息解析返回空结果", { msgId });
|
|
13878
14119
|
return;
|
|
@@ -14127,7 +14368,8 @@ main().catch((err) => {
|
|
|
14127
14368
|
* 跳过:preflight、凭证加载、WebSocket、消息处理、Teammate 等飞书相关功能
|
|
14128
14369
|
*/
|
|
14129
14370
|
async function startHeadless() {
|
|
14130
|
-
|
|
14371
|
+
const workspaceRoot = process.env.LARKPAL_WORKSPACE ?? "/workspace";
|
|
14372
|
+
await mkdir(workspaceRoot, { recursive: true });
|
|
14131
14373
|
const runtimeName = process.env.LARKPAL_RUNTIME || "larkpal-agent";
|
|
14132
14374
|
let runtimeAdapter;
|
|
14133
14375
|
if (runtimeName === "larkpal-agent") runtimeAdapter = await createLarkpalAgentAdapter();
|
|
@@ -14140,22 +14382,32 @@ async function startHeadless() {
|
|
|
14140
14382
|
await scheduledTaskManager.loadFromDisk();
|
|
14141
14383
|
const gatewayPort = parseInt(process.env.LARKPAL_GATEWAY_PORT || "3000", 10);
|
|
14142
14384
|
const gatewayHost = process.env.LARKPAL_GATEWAY_HOST || "0.0.0.0";
|
|
14385
|
+
const enableWebChannel = process.env.LARKPAL_ENABLE_WEB_CHANNEL === "1" || process.env.LARKPAL_ENABLE_WEB_CHANNEL === "true";
|
|
14386
|
+
const messageStore = enableWebChannel ? new SessionMessageStore(workspaceRoot) : void 0;
|
|
14387
|
+
if (enableWebChannel) logger.info("Headless Web Channel 已启用,Chat API 路由将注册", { workspaceRoot });
|
|
14143
14388
|
const gateway = createGatewayServer({
|
|
14144
14389
|
port: gatewayPort,
|
|
14145
14390
|
host: gatewayHost,
|
|
14146
14391
|
processManager: runtimeAdapter,
|
|
14147
14392
|
scheduledTaskManager,
|
|
14148
14393
|
appCredentials: void 0,
|
|
14149
|
-
messageStore
|
|
14394
|
+
messageStore
|
|
14150
14395
|
});
|
|
14151
14396
|
await gateway.start();
|
|
14152
14397
|
logger.info("🚀 LarkPal Headless Gateway 启动完成", {
|
|
14153
14398
|
gatewayUrl: `http://${gatewayHost}:${gatewayPort}`,
|
|
14154
14399
|
runtime: runtimeName,
|
|
14400
|
+
webChannel: enableWebChannel,
|
|
14155
14401
|
endpoints: [
|
|
14156
14402
|
"/api/execute",
|
|
14157
14403
|
"/api/status",
|
|
14158
|
-
"/api/skills"
|
|
14404
|
+
"/api/skills",
|
|
14405
|
+
...enableWebChannel ? [
|
|
14406
|
+
"/api/chat/auth",
|
|
14407
|
+
"/api/chat/stream",
|
|
14408
|
+
"/api/chat/sessions",
|
|
14409
|
+
"/api/chat/history"
|
|
14410
|
+
] : []
|
|
14159
14411
|
]
|
|
14160
14412
|
});
|
|
14161
14413
|
const shutdown = async (signal) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-lark/larkpal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "LarkPal - Lark/Feishu bot service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/main.mjs",
|
|
@@ -19,21 +19,9 @@
|
|
|
19
19
|
"registry": "https://registry.npmjs.org",
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
|
-
"packageManager": "pnpm@10.32.1",
|
|
23
22
|
"engines": {
|
|
24
23
|
"node": ">=22"
|
|
25
24
|
},
|
|
26
|
-
"scripts": {
|
|
27
|
-
"start": "node --env-file=.env bin/larkpal.js",
|
|
28
|
-
"build": "tsdown",
|
|
29
|
-
"test": "vitest run",
|
|
30
|
-
"test:watch": "vitest",
|
|
31
|
-
"lint": "eslint src/",
|
|
32
|
-
"lint:fix": "eslint src/ --fix",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
-
"format": "prettier --write src/**/*.ts",
|
|
35
|
-
"format:check": "prettier --check src/**/*.ts"
|
|
36
|
-
},
|
|
37
25
|
"dependencies": {
|
|
38
26
|
"@larksuiteoapi/node-sdk": "^1.60.0",
|
|
39
27
|
"@sinclair/typebox": "0.34.48",
|
|
@@ -63,5 +51,16 @@
|
|
|
63
51
|
"typescript": "^5.9.3",
|
|
64
52
|
"typescript-eslint": "^8.32.1",
|
|
65
53
|
"vitest": "^4.1.1"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"start": "node --env-file=.env bin/larkpal.js",
|
|
57
|
+
"build": "tsdown",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:watch": "vitest",
|
|
60
|
+
"lint": "eslint src/",
|
|
61
|
+
"lint:fix": "eslint src/ --fix",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"format": "prettier --write src/**/*.ts",
|
|
64
|
+
"format:check": "prettier --check src/**/*.ts"
|
|
66
65
|
}
|
|
67
|
-
}
|
|
66
|
+
}
|