@vibe-lark/larkpal 0.1.59 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +146 -39
  2. package/package.json +2 -2
package/dist/main.mjs CHANGED
@@ -3421,6 +3421,7 @@ function summarizeString(value) {
3421
3421
  */
3422
3422
  const log$29 = larkLogger("chat/stream");
3423
3423
  const chatRunsBySession = /* @__PURE__ */ new Map();
3424
+ const DEFAULT_STALE_RUN_IDLE_MS = 1800 * 1e3;
3424
3425
  /** 统一写 SSE 格式数据到响应流 */
3425
3426
  function sendSSE(res, event, data) {
3426
3427
  const payload = typeof data === "string" ? data : JSON.stringify(data);
@@ -3444,6 +3445,7 @@ function getActiveChatRun(sessionId) {
3444
3445
  return run?.status === "running" ? run : void 0;
3445
3446
  }
3446
3447
  function serializeChatRun(run) {
3448
+ const idleSeconds = Math.max(0, Math.floor((Date.now() - run.lastEventAt) / 1e3));
3447
3449
  return {
3448
3450
  runId: run.runId,
3449
3451
  sessionId: run.sessionId,
@@ -3456,9 +3458,23 @@ function serializeChatRun(run) {
3456
3458
  finalStatus: run.finalStatus,
3457
3459
  error: run.error,
3458
3460
  textLength: run.text.length,
3459
- finalText: run.status === "running" ? void 0 : run.text || void 0
3461
+ finalText: run.status === "running" ? void 0 : run.text || void 0,
3462
+ lastEventAt: run.lastEventAt,
3463
+ lastEventType: run.lastEventType,
3464
+ lastToolName: run.lastToolName,
3465
+ idleSeconds,
3466
+ adapterBusy: run.adapterBusy,
3467
+ staleReason: run.staleReason
3460
3468
  };
3461
3469
  }
3470
+ function getRuntimeEventDedupeKey(event) {
3471
+ const record = event;
3472
+ const eventId = record.eventId;
3473
+ if (typeof eventId === "string" && eventId.length > 0) return eventId;
3474
+ const runId = typeof record.runId === "string" ? record.runId : void 0;
3475
+ const seq = typeof record.seq === "number" || typeof record.seq === "string" ? String(record.seq) : void 0;
3476
+ return runId && seq ? `${runId}:${seq}` : void 0;
3477
+ }
3462
3478
  function isRunVisibleToUser(run, params) {
3463
3479
  return run.tenantKey === params.tenantKey && run.openId === params.openId;
3464
3480
  }
@@ -3475,6 +3491,17 @@ function addRunClient(req, res, run) {
3475
3491
  function sendRunStatus(res, run) {
3476
3492
  sendSSE(res, "run-status", serializeChatRun(run));
3477
3493
  }
3494
+ function getStaleRunIdleMs() {
3495
+ const raw = process.env.LARKPAL_CHAT_RUN_STALE_IDLE_MS;
3496
+ if (!raw) return DEFAULT_STALE_RUN_IDLE_MS;
3497
+ const parsed = Number.parseInt(raw, 10);
3498
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_STALE_RUN_IDLE_MS;
3499
+ }
3500
+ function recordRunEvent(run, eventType, params) {
3501
+ run.lastEventAt = Date.now();
3502
+ run.lastEventType = eventType;
3503
+ if (params?.toolName) run.lastToolName = params.toolName;
3504
+ }
3478
3505
  function extractRuntimeEventFinalText(event) {
3479
3506
  const direct = event.finalText;
3480
3507
  if (typeof direct === "string" && direct.trim()) return direct;
@@ -3522,6 +3549,71 @@ function queueRunMessage(run, messageStore, message, label) {
3522
3549
  });
3523
3550
  return run.persistQueue;
3524
3551
  }
3552
+ function appendRunStatusMessage(run, messageStore) {
3553
+ return queueRunMessage(run, messageStore, {
3554
+ sessionId: run.sessionId,
3555
+ role: "tool",
3556
+ content: JSON.stringify({
3557
+ runtimeEventType: "run-status",
3558
+ run: serializeChatRun(run)
3559
+ }),
3560
+ channel: "web",
3561
+ metadata: {
3562
+ runId: run.runId,
3563
+ requestId: run.requestId,
3564
+ traceId: run.traceId,
3565
+ scenarioId: run.scenarioId,
3566
+ runtimeEventType: "run-status",
3567
+ finalStatus: run.finalStatus,
3568
+ errorMessage: run.error,
3569
+ staleReason: run.staleReason
3570
+ }
3571
+ }, "[stream] 保存 run status 消息失败");
3572
+ }
3573
+ async function markRunTerminal(run, messageStore, status, params) {
3574
+ if (run.status !== "running") return false;
3575
+ run.status = status;
3576
+ run.completedAt = Date.now();
3577
+ run.finalStatus = params.finalStatus;
3578
+ run.error = params.error;
3579
+ run.staleReason = params.staleReason;
3580
+ recordRunEvent(run, `run-${status}`);
3581
+ broadcastRunStatus(run);
3582
+ await appendRunStatusMessage(run, messageStore);
3583
+ return true;
3584
+ }
3585
+ async function reconcileRunWithAdapter(run, messageStore, processManager) {
3586
+ if (run.status !== "running") return;
3587
+ const processInfo = processManager.getProcessInfo(run.sessionId);
3588
+ const adapterBusy = processManager.isSessionBusy?.(run.sessionId) ?? processInfo?.status === "running";
3589
+ run.adapterBusy = adapterBusy;
3590
+ const idleMs = Date.now() - run.lastEventAt;
3591
+ let staleReason;
3592
+ if (!adapterBusy) staleReason = processInfo ? `runtime process is ${processInfo.status}` : "runtime adapter reports session is not busy";
3593
+ else if (idleMs >= getStaleRunIdleMs()) staleReason = `run idle for ${Math.floor(idleMs / 1e3)}s`;
3594
+ if (!staleReason) return;
3595
+ const error = `Chat run became stale: ${staleReason}`;
3596
+ log$29.warn("[stream] activeRun 与 runtime 状态不一致,标记为 stale", {
3597
+ sessionId: run.sessionId,
3598
+ runId: run.runId,
3599
+ staleReason,
3600
+ idleMs,
3601
+ adapterBusy,
3602
+ processStatus: processInfo?.status
3603
+ });
3604
+ if (await markRunTerminal(run, messageStore, "failed", {
3605
+ finalStatus: "stale",
3606
+ error,
3607
+ staleReason
3608
+ })) {
3609
+ broadcastRunEvent(run, "error", {
3610
+ runId: run.runId,
3611
+ message: error,
3612
+ staleReason
3613
+ });
3614
+ endRunClients(run);
3615
+ }
3616
+ }
3525
3617
  function buildRuntimeEventMessageMetadata(event, runtimeConfig) {
3526
3618
  return {
3527
3619
  runtimeEventType: event.type,
@@ -3663,37 +3755,10 @@ function createChatRouter(config) {
3663
3755
  text: "",
3664
3756
  clients: /* @__PURE__ */ new Set(),
3665
3757
  runtimeConfig,
3666
- persistQueue: Promise.resolve()
3667
- };
3668
- const appendRunStatusMessage = () => {
3669
- return queueRunMessage(run, messageStore, {
3670
- sessionId,
3671
- role: "tool",
3672
- content: JSON.stringify({
3673
- runtimeEventType: "run-status",
3674
- run: serializeChatRun(run)
3675
- }),
3676
- channel: "web",
3677
- metadata: {
3678
- runId: run.runId,
3679
- requestId,
3680
- traceId,
3681
- scenarioId,
3682
- runtimeEventType: "run-status",
3683
- finalStatus: run.finalStatus,
3684
- errorMessage: run.error
3685
- }
3686
- }, "[stream] 保存 run status 消息失败");
3687
- };
3688
- const markRunTerminal = async (status, params) => {
3689
- if (run.status !== "running") return false;
3690
- run.status = status;
3691
- run.completedAt = Date.now();
3692
- run.finalStatus = params.finalStatus;
3693
- run.error = params.error;
3694
- broadcastRunStatus(run);
3695
- await appendRunStatusMessage();
3696
- return true;
3758
+ persistQueue: Promise.resolve(),
3759
+ seenRuntimeEventIds: /* @__PURE__ */ new Set(),
3760
+ lastEventAt: Date.now(),
3761
+ lastEventType: "run-started"
3697
3762
  };
3698
3763
  try {
3699
3764
  await messageStore.appendMessage({
@@ -3722,7 +3787,7 @@ function createChatRouter(config) {
3722
3787
  return;
3723
3788
  }
3724
3789
  chatRunsBySession.set(sessionId, run);
3725
- appendRunStatusMessage();
3790
+ appendRunStatusMessage(run, messageStore);
3726
3791
  prepareSSE(res);
3727
3792
  addRunClient(req, res, run);
3728
3793
  sendSSE(res, "session", { sessionId });
@@ -3736,6 +3801,7 @@ function createChatRouter(config) {
3736
3801
  }, 15e3);
3737
3802
  const handleBlockedEvent = (event) => {
3738
3803
  const sanitized = sanitizeBlockedEvent(event);
3804
+ recordRunEvent(run, "blocked", { toolName: sanitized.name });
3739
3805
  log$29.warn("[stream] Agent blocked event", { ...sanitized });
3740
3806
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, sanitized.type, {
3741
3807
  toolName: sanitized.name,
@@ -3766,7 +3832,21 @@ function createChatRouter(config) {
3766
3832
  broadcastRunEvent(run, "blocked", { event: sanitized });
3767
3833
  };
3768
3834
  const handleRuntimeEvent = (event) => {
3835
+ const dedupeKey = getRuntimeEventDedupeKey(event);
3836
+ if (dedupeKey) {
3837
+ if (run.seenRuntimeEventIds.has(dedupeKey)) {
3838
+ log$29.debug("[stream] 跳过重复 runtime event", {
3839
+ sessionId,
3840
+ runId: run.runId,
3841
+ eventId: dedupeKey,
3842
+ eventType: event.type
3843
+ });
3844
+ return;
3845
+ }
3846
+ run.seenRuntimeEventIds.add(dedupeKey);
3847
+ }
3769
3848
  const eventSummary = summarizeForAudit(event);
3849
+ recordRunEvent(run, event.type, { toolName: event.capabilityId });
3770
3850
  const runtimeFinalText = extractRuntimeEventFinalText(event);
3771
3851
  if (runtimeFinalText && !run.text.trim()) run.text = runtimeFinalText;
3772
3852
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "runtime_event", {
@@ -3793,13 +3873,16 @@ function createChatRouter(config) {
3793
3873
  };
3794
3874
  const callbacks = {
3795
3875
  onTextDelta: (text) => {
3876
+ recordRunEvent(run, "text-delta");
3796
3877
  run.text += text;
3797
3878
  broadcastRunEvent(run, "text-delta", { text });
3798
3879
  },
3799
3880
  onThinkingDelta: (text) => {
3881
+ recordRunEvent(run, "thinking-delta");
3800
3882
  broadcastRunEvent(run, "thinking-delta", { text });
3801
3883
  },
3802
3884
  onToolUseStart: (toolName, toolInput) => {
3885
+ recordRunEvent(run, "tool-use-start", { toolName });
3803
3886
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "tool_call_started", {
3804
3887
  toolName,
3805
3888
  inputSummary: summarizeForAudit(toolInput)
@@ -3827,6 +3910,7 @@ function createChatRouter(config) {
3827
3910
  });
3828
3911
  },
3829
3912
  onToolResult: (toolUseId, result) => {
3913
+ recordRunEvent(run, "tool-result");
3830
3914
  const resultSummary = summarizeForAudit(result);
3831
3915
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "tool_call_finished", {
3832
3916
  toolCallId: toolUseId,
@@ -3857,19 +3941,22 @@ function createChatRouter(config) {
3857
3941
  });
3858
3942
  },
3859
3943
  onToolProgress: (toolName, elapsedSeconds) => {
3944
+ recordRunEvent(run, "tool-progress", { toolName });
3860
3945
  broadcastRunEvent(run, "tool-progress", {
3861
3946
  toolName,
3862
3947
  elapsedSeconds
3863
3948
  });
3864
3949
  },
3865
3950
  onTurnEnd: (stopReason) => {
3951
+ recordRunEvent(run, "turn-end");
3866
3952
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "turn_finished", { metadata: { stopReason } }));
3867
3953
  broadcastRunEvent(run, "turn-end", { stopReason });
3868
3954
  },
3869
3955
  onResult: async (result) => {
3956
+ recordRunEvent(run, "result");
3870
3957
  const finalText = run.text.trim() ? run.text : result.result || "";
3871
3958
  run.text = finalText;
3872
- await markRunTerminal(result.isError ? "failed" : "completed", { finalStatus: result.subtype });
3959
+ await markRunTerminal(run, messageStore, result.isError ? "failed" : "completed", { finalStatus: result.subtype });
3873
3960
  await queueRunMessage(run, messageStore, {
3874
3961
  sessionId,
3875
3962
  role: "assistant",
@@ -3920,6 +4007,7 @@ function createChatRouter(config) {
3920
4007
  }));
3921
4008
  },
3922
4009
  onError: async (error) => {
4010
+ recordRunEvent(run, "error");
3923
4011
  log$29.error("[stream] Runtime 执行出错", {
3924
4012
  sessionId,
3925
4013
  runId: run.runId,
@@ -3949,7 +4037,7 @@ function createChatRouter(config) {
3949
4037
  requestId
3950
4038
  }
3951
4039
  }));
3952
- await markRunTerminal("failed", {
4040
+ await markRunTerminal(run, messageStore, "failed", {
3953
4041
  finalStatus: "error",
3954
4042
  error: error.message
3955
4043
  });
@@ -3968,6 +4056,7 @@ function createChatRouter(config) {
3968
4056
  });
3969
4057
  },
3970
4058
  onVerifierResult: (result) => {
4059
+ recordRunEvent(run, "verifier-result");
3971
4060
  const resultSummary = summarizeForAudit(result);
3972
4061
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "verifier_result", {
3973
4062
  resultSummary,
@@ -4019,7 +4108,7 @@ function createChatRouter(config) {
4019
4108
  sessionId,
4020
4109
  runId: run.runId
4021
4110
  });
4022
- await markRunTerminal("failed", {
4111
+ await markRunTerminal(run, messageStore, "failed", {
4023
4112
  finalStatus: "error",
4024
4113
  error: errorMessage
4025
4114
  });
@@ -4065,6 +4154,7 @@ function createChatRouter(config) {
4065
4154
  tenantKey: chatUser.tenantKey,
4066
4155
  openId: chatUser.openId
4067
4156
  }) ? run : void 0;
4157
+ if (visibleRun) await reconcileRunWithAdapter(visibleRun, messageStore, processManager);
4068
4158
  res.json({
4069
4159
  messages: result.messages,
4070
4160
  nextCursor: result.nextCursor,
@@ -4100,6 +4190,7 @@ function createChatRouter(config) {
4100
4190
  });
4101
4191
  return;
4102
4192
  }
4193
+ await reconcileRunWithAdapter(run, messageStore, processManager);
4103
4194
  const serialized = serializeChatRun(run);
4104
4195
  res.json({
4105
4196
  sessionId,
@@ -6569,6 +6660,7 @@ const PERSONA_FILE = join(homedir(), ".claude", "CLAUDE.md");
6569
6660
  function createStatusRouter(config) {
6570
6661
  const router = express.Router();
6571
6662
  const sessionsDir = getWorkspaceChatsRoot(config?.workspaceRoot ?? resolveWorkspaceRoot("/workspace"));
6663
+ const processManager = config?.processManager;
6572
6664
  router.get("/sessions", async (req, res) => {
6573
6665
  logger$2.info("list sessions request", {
6574
6666
  method: req.method,
@@ -6669,14 +6761,18 @@ function createStatusRouter(config) {
6669
6761
  try {
6670
6762
  const uptimeSeconds = process.uptime();
6671
6763
  const memory = process.memoryUsage();
6764
+ const processes = processManager?.getAllProcessInfo().map((processInfo) => {
6765
+ return serializeRuntimeProcess(processInfo, processManager);
6766
+ }) ?? [];
6672
6767
  const statusData = {
6673
6768
  uptime: uptimeSeconds,
6674
- processes: [],
6769
+ processes,
6675
6770
  memory
6676
6771
  };
6677
6772
  logger$2.info("get status response", {
6678
6773
  uptime: uptimeSeconds,
6679
- memoryRss: memory.rss
6774
+ memoryRss: memory.rss,
6775
+ processCount: processes.length
6680
6776
  });
6681
6777
  res.json(statusData);
6682
6778
  } catch (err) {
@@ -6720,6 +6816,14 @@ function createStatusRouter(config) {
6720
6816
  });
6721
6817
  return router;
6722
6818
  }
6819
+ function serializeRuntimeProcess(processInfo, processManager) {
6820
+ return {
6821
+ ...processInfo,
6822
+ startedAt: processInfo.startedAt.toISOString(),
6823
+ lastActiveAt: processInfo.lastActiveAt.toISOString(),
6824
+ busy: processManager.isSessionBusy?.(processInfo.sessionId) ?? processInfo.status === "running"
6825
+ };
6826
+ }
6723
6827
  //#endregion
6724
6828
  //#region src/gateway/server.ts
6725
6829
  const logger$1 = larkLogger("gateway");
@@ -6783,7 +6887,10 @@ function registerRoutes(app, processManager, scheduledTaskManager, appCredential
6783
6887
  app.put("/api/scheduled-tasks/:id", notImplemented);
6784
6888
  app.delete("/api/scheduled-tasks/:id", notImplemented);
6785
6889
  }
6786
- app.use("/api", createStatusRouter({ workspaceRoot }));
6890
+ app.use("/api", createStatusRouter({
6891
+ workspaceRoot,
6892
+ processManager
6893
+ }));
6787
6894
  app.use("/hooks", createHooksRouter());
6788
6895
  app.use("/api/mcp", createMcpCallbackRouter());
6789
6896
  app.use(createChatAuthRouter());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",
@@ -50,7 +50,7 @@
50
50
  "zod": "^4.3.6"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@vibe-lark/larkpal-agent": "^0.2.8"
53
+ "@vibe-lark/larkpal-agent": "^0.2.12"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@eslint/js": "^10.0.1",