@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 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 || trimmedText === "") {
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 = `[直接@你的消息,请正常回复] ${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 — 全量缓冲,直到 end_turn 时统一判断
10563
+ * 处理 text delta — 前缀检测 + 缓冲
10335
10564
  *
10336
- * 策略:CC teammate 模式下可能先执行工具再输出文本,且文本中可能正文在前、
10337
- * NO_REPLY 在末尾。因此不做前缀检测,而是缓冲所有 text,在 onTurnEnd(end_turn)
10338
- * 时统一判断完整输出是否包含 NO_REPLY。
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: true
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
- "如果不需要参与,只输出 NO_REPLY",
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
- await mkdir(process.env.LARKPAL_WORKSPACE ?? "/workspace", { recursive: true });
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: void 0
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.34",
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
+ }