@vibe-lark/larkpal 0.1.56 → 0.1.58

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 +367 -135
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -2987,23 +2987,6 @@ async function createLarkpalAgentAdapter() {
2987
2987
  });
2988
2988
  }
2989
2989
  else log$29.warn("未配置 LARKPAL_AGENT_MCP_SERVER_URL,Agent 无远程工具可用");
2990
- try {
2991
- const { loadSkillsFromDir, registerSkills } = await import("@vibe-lark/larkpal-agent");
2992
- const { fileURLToPath } = await import("node:url");
2993
- const skillsDir = join(fileURLToPath(import.meta.resolve("@vibe-lark/larkpal-agent")).replace(/\/dist\/index\.js$/, ""), "src", "skills", "definitions");
2994
- const skills = await loadSkillsFromDir(skillsDir);
2995
- if (skills.length > 0) {
2996
- registerSkills(registry, skills, llm);
2997
- const skillTools = registry.getAll().filter((t) => t.name.startsWith("skill:"));
2998
- adapter.registerTools(skillTools);
2999
- log$29.info("Skill Meta-Tools 注册完成", {
3000
- skillCount: skills.length,
3001
- skillNames: skills.map((s) => s.name)
3002
- });
3003
- } else log$29.info("未找到 Skill 定义文件", { skillsDir });
3004
- } catch (err) {
3005
- log$29.warn("Skill 加载失败(非阻塞)", { error: err?.message });
3006
- }
3007
2990
  return adapter;
3008
2991
  }
3009
2992
  //#endregion
@@ -3637,18 +3620,93 @@ function summarizeString(value) {
3637
3620
  * DELETE /api/chat/sessions/:id — 删除会话
3638
3621
  */
3639
3622
  const log$28 = larkLogger("chat/stream");
3640
- const activeStreamSessions = /* @__PURE__ */ new Set();
3623
+ const chatRunsBySession = /* @__PURE__ */ new Map();
3641
3624
  /** 统一写 SSE 格式数据到响应流 */
3642
3625
  function sendSSE(res, event, data) {
3643
3626
  const payload = typeof data === "string" ? data : JSON.stringify(data);
3644
3627
  res.write(`event: ${event}\ndata: ${payload}\n\n`);
3645
3628
  }
3646
3629
  function isChatSessionRunning(sessionId, processManager) {
3647
- return activeStreamSessions.has(sessionId) || (processManager.isSessionBusy?.(sessionId) ?? false);
3630
+ return getActiveChatRun(sessionId) != null || (processManager.isSessionBusy?.(sessionId) ?? false);
3648
3631
  }
3649
3632
  function logChatAudit(event) {
3650
3633
  log$28.info("[stream] Agent audit event", { ...event });
3651
3634
  }
3635
+ function prepareSSE(res) {
3636
+ res.setHeader("Content-Type", "text/event-stream");
3637
+ res.setHeader("Cache-Control", "no-cache");
3638
+ res.setHeader("Connection", "keep-alive");
3639
+ res.setHeader("X-Accel-Buffering", "no");
3640
+ res.flushHeaders();
3641
+ }
3642
+ function getActiveChatRun(sessionId) {
3643
+ const run = chatRunsBySession.get(sessionId);
3644
+ return run?.status === "running" ? run : void 0;
3645
+ }
3646
+ function serializeChatRun(run) {
3647
+ return {
3648
+ runId: run.runId,
3649
+ sessionId: run.sessionId,
3650
+ requestId: run.requestId,
3651
+ traceId: run.traceId,
3652
+ scenarioId: run.scenarioId,
3653
+ status: run.status,
3654
+ startedAt: run.startedAt,
3655
+ completedAt: run.completedAt,
3656
+ finalStatus: run.finalStatus,
3657
+ error: run.error,
3658
+ textLength: run.text.length
3659
+ };
3660
+ }
3661
+ function isRunVisibleToUser(run, params) {
3662
+ return run.tenantKey === params.tenantKey && run.openId === params.openId;
3663
+ }
3664
+ function addRunClient(req, res, run) {
3665
+ run.clients.add(res);
3666
+ req.on("close", () => {
3667
+ run.clients.delete(res);
3668
+ log$28.info("[stream] 客户端断开连接", {
3669
+ sessionId: run.sessionId,
3670
+ runId: run.runId
3671
+ });
3672
+ });
3673
+ }
3674
+ function sendRunStatus(res, run) {
3675
+ sendSSE(res, "run-status", serializeChatRun(run));
3676
+ }
3677
+ function broadcastRunEvent(run, event, data) {
3678
+ for (const client of Array.from(run.clients)) try {
3679
+ sendSSE(client, event, data);
3680
+ } catch (err) {
3681
+ run.clients.delete(client);
3682
+ log$28.warn("[stream] SSE 写入失败,移除客户端", {
3683
+ sessionId: run.sessionId,
3684
+ runId: run.runId,
3685
+ error: err instanceof Error ? err.message : String(err)
3686
+ });
3687
+ }
3688
+ }
3689
+ function broadcastRunStatus(run) {
3690
+ broadcastRunEvent(run, "run-status", serializeChatRun(run));
3691
+ }
3692
+ function endRunClients(run) {
3693
+ for (const client of Array.from(run.clients)) try {
3694
+ client.end();
3695
+ } catch {}
3696
+ run.clients.clear();
3697
+ }
3698
+ function queueRunMessage(run, messageStore, message, label) {
3699
+ run.persistQueue = run.persistQueue.catch(() => void 0).then(async () => {
3700
+ await messageStore.appendMessage(message);
3701
+ }).catch((err) => {
3702
+ log$28.error(label, {
3703
+ sessionId: run.sessionId,
3704
+ runId: run.runId,
3705
+ error: err instanceof Error ? err.message : String(err)
3706
+ });
3707
+ });
3708
+ return run.persistQueue;
3709
+ }
3652
3710
  function buildRuntimeEventMessageMetadata(event, runtimeConfig) {
3653
3711
  return {
3654
3712
  runtimeEventType: event.type,
@@ -3686,22 +3744,42 @@ function createChatRouter(config) {
3686
3744
  router.post("/api/chat/stream", chatAuthMiddleware, async (req, res) => {
3687
3745
  const { userId, openId, tenantKey } = res.locals.chatUser;
3688
3746
  const body = req.body;
3689
- if (!body.prompt || typeof body.prompt !== "string") {
3690
- res.status(400).json({ error: "prompt is required and must be a string" });
3691
- return;
3692
- }
3693
3747
  if (!tenantKey || !userId || !openId) {
3694
3748
  res.status(401).json({ error: "Missing required chat identity: tenantKey, userId, openId" });
3695
3749
  return;
3696
3750
  }
3697
3751
  const requestId = getGatewayRequestId(req.headers);
3698
3752
  const traceId = getGatewayTraceId(req.headers, requestId);
3753
+ let sessionId = body.session_id;
3754
+ const activeRun = sessionId ? getActiveChatRun(sessionId) : void 0;
3755
+ if (activeRun) {
3756
+ if (!isRunVisibleToUser(activeRun, {
3757
+ tenantKey,
3758
+ openId
3759
+ })) {
3760
+ res.status(403).json({ error: "Forbidden" });
3761
+ return;
3762
+ }
3763
+ prepareSSE(res);
3764
+ addRunClient(req, res, activeRun);
3765
+ sendSSE(res, "session", { sessionId });
3766
+ sendRunStatus(res, activeRun);
3767
+ log$28.info("[stream] 已重新连接到运行中的 run", {
3768
+ sessionId,
3769
+ runId: activeRun.runId
3770
+ });
3771
+ return;
3772
+ }
3773
+ if (!body.prompt || typeof body.prompt !== "string") {
3774
+ res.status(400).json({ error: "prompt is required and must be a string" });
3775
+ return;
3776
+ }
3699
3777
  const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
3700
3778
  const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
3701
3779
  const sourceArtifacts = getRunInputValue(body.sourceArtifacts, body.source_artifacts, body.options?.sourceArtifacts, body.options?.source_artifacts, body.options?.metadata?.sourceArtifacts, body.options?.metadata?.source_artifacts);
3702
3780
  log$28.info("[stream] 收到流式对话请求", {
3703
3781
  userId,
3704
- sessionId: body.session_id,
3782
+ sessionId,
3705
3783
  requestId,
3706
3784
  traceId,
3707
3785
  scenarioId,
@@ -3709,7 +3787,6 @@ function createChatRouter(config) {
3709
3787
  hasSourceArtifacts: sourceArtifacts !== void 0,
3710
3788
  promptLength: body.prompt.length
3711
3789
  });
3712
- let sessionId = body.session_id;
3713
3790
  if (!sessionId) {
3714
3791
  sessionId = (await messageStore.createSession({
3715
3792
  id: `client_${openId}_${Date.now().toString(36)}`,
@@ -3723,22 +3800,85 @@ function createChatRouter(config) {
3723
3800
  });
3724
3801
  }
3725
3802
  if (isChatSessionRunning(sessionId, processManager)) {
3726
- log$28.warn("[stream] session 正在运行,拒绝并发请求", {
3803
+ log$28.warn("[stream] session 正在被非 Web Chat run 占用", {
3727
3804
  tenantKey,
3728
3805
  userId,
3729
3806
  sessionId
3730
3807
  });
3731
- res.setHeader("Content-Type", "text/event-stream");
3732
- res.setHeader("Cache-Control", "no-cache");
3733
- res.setHeader("Connection", "keep-alive");
3734
- res.setHeader("X-Accel-Buffering", "no");
3735
- res.flushHeaders();
3808
+ prepareSSE(res);
3736
3809
  sendSSE(res, "session", { sessionId });
3737
3810
  sendSSE(res, "error", { message: `Session ${sessionId} is already running` });
3738
3811
  res.end();
3739
3812
  return;
3740
3813
  }
3741
- activeStreamSessions.add(sessionId);
3814
+ const runtimeConfig = buildAgentRuntimeConfig({
3815
+ requestId,
3816
+ traceId,
3817
+ sessionId,
3818
+ conversationId: sessionId,
3819
+ entrypoint: "chat",
3820
+ scenarioId,
3821
+ prompt: body.prompt,
3822
+ maxTurns: body.options?.maxTurns,
3823
+ maxBudgetUsd: body.options?.maxBudgetUsd,
3824
+ model: process.env.CLAUDE_MODEL || void 0,
3825
+ attachments,
3826
+ sourceArtifacts,
3827
+ policy: body.options?.policy,
3828
+ metadata: body.options?.metadata,
3829
+ identity: {
3830
+ tenantKey,
3831
+ userId,
3832
+ openId,
3833
+ userName: userId
3834
+ }
3835
+ });
3836
+ const run = {
3837
+ runId: requestId,
3838
+ sessionId,
3839
+ requestId,
3840
+ traceId,
3841
+ scenarioId,
3842
+ tenantKey,
3843
+ userId,
3844
+ openId,
3845
+ status: "running",
3846
+ startedAt: Date.now(),
3847
+ text: "",
3848
+ clients: /* @__PURE__ */ new Set(),
3849
+ runtimeConfig,
3850
+ persistQueue: Promise.resolve()
3851
+ };
3852
+ const appendRunStatusMessage = () => {
3853
+ return queueRunMessage(run, messageStore, {
3854
+ sessionId,
3855
+ role: "tool",
3856
+ content: JSON.stringify({
3857
+ runtimeEventType: "run-status",
3858
+ run: serializeChatRun(run)
3859
+ }),
3860
+ channel: "web",
3861
+ metadata: {
3862
+ runId: run.runId,
3863
+ requestId,
3864
+ traceId,
3865
+ scenarioId,
3866
+ runtimeEventType: "run-status",
3867
+ finalStatus: run.finalStatus,
3868
+ errorMessage: run.error
3869
+ }
3870
+ }, "[stream] 保存 run status 消息失败");
3871
+ };
3872
+ const markRunTerminal = async (status, params) => {
3873
+ if (run.status !== "running") return false;
3874
+ run.status = status;
3875
+ run.completedAt = Date.now();
3876
+ run.finalStatus = params.finalStatus;
3877
+ run.error = params.error;
3878
+ broadcastRunStatus(run);
3879
+ await appendRunStatusMessage();
3880
+ return true;
3881
+ };
3742
3882
  try {
3743
3883
  await messageStore.appendMessage({
3744
3884
  sessionId,
@@ -3746,6 +3886,7 @@ function createChatRouter(config) {
3746
3886
  content: body.prompt,
3747
3887
  channel: "web",
3748
3888
  metadata: {
3889
+ runId: run.runId,
3749
3890
  requestId,
3750
3891
  traceId,
3751
3892
  scenarioId,
@@ -3756,7 +3897,6 @@ function createChatRouter(config) {
3756
3897
  }
3757
3898
  });
3758
3899
  } catch (err) {
3759
- activeStreamSessions.delete(sessionId);
3760
3900
  const errorMessage = err instanceof Error ? err.message : String(err);
3761
3901
  log$28.error("[stream] 保存用户消息失败", {
3762
3902
  sessionId,
@@ -3765,34 +3905,12 @@ function createChatRouter(config) {
3765
3905
  res.status(500).json({ error: "Failed to append user message" });
3766
3906
  return;
3767
3907
  }
3768
- res.setHeader("Content-Type", "text/event-stream");
3769
- res.setHeader("Cache-Control", "no-cache");
3770
- res.setHeader("Connection", "keep-alive");
3771
- res.setHeader("X-Accel-Buffering", "no");
3772
- res.flushHeaders();
3908
+ chatRunsBySession.set(sessionId, run);
3909
+ appendRunStatusMessage();
3910
+ prepareSSE(res);
3911
+ addRunClient(req, res, run);
3773
3912
  sendSSE(res, "session", { sessionId });
3774
- const runtimeConfig = buildAgentRuntimeConfig({
3775
- requestId,
3776
- traceId,
3777
- sessionId,
3778
- conversationId: sessionId,
3779
- entrypoint: "chat",
3780
- scenarioId,
3781
- prompt: body.prompt,
3782
- maxTurns: body.options?.maxTurns,
3783
- maxBudgetUsd: body.options?.maxBudgetUsd,
3784
- model: process.env.CLAUDE_MODEL || void 0,
3785
- attachments,
3786
- sourceArtifacts,
3787
- policy: body.options?.policy,
3788
- metadata: body.options?.metadata,
3789
- identity: {
3790
- tenantKey,
3791
- userId,
3792
- openId,
3793
- userName: userId
3794
- }
3795
- });
3913
+ sendRunStatus(res, run);
3796
3914
  const handleBlockedEvent = (event) => {
3797
3915
  const sanitized = sanitizeBlockedEvent(event);
3798
3916
  log$28.warn("[stream] Agent blocked event", { ...sanitized });
@@ -3802,24 +3920,35 @@ function createChatRouter(config) {
3802
3920
  errorCode: sanitized.reasonCode,
3803
3921
  errorMessage: sanitized.reason,
3804
3922
  metadata: {
3923
+ runId: run.runId,
3805
3924
  traceId,
3806
3925
  reasonCode: sanitized.reasonCode
3807
3926
  }
3808
3927
  }));
3928
+ queueRunMessage(run, messageStore, {
3929
+ sessionId,
3930
+ role: "tool",
3931
+ content: JSON.stringify({ blocked: sanitized }),
3932
+ channel: "web",
3933
+ metadata: {
3934
+ runId: run.runId,
3935
+ requestId,
3936
+ traceId,
3937
+ scenarioId,
3938
+ runtimeEventType: "blocked",
3939
+ toolName: sanitized.name,
3940
+ reasonCode: sanitized.reasonCode
3941
+ }
3942
+ }, "[stream] 保存 blocked 消息失败");
3943
+ broadcastRunEvent(run, "blocked", { event: sanitized });
3809
3944
  };
3810
- let accumText = "";
3811
- let clientDisconnected = false;
3812
- req.on("close", () => {
3813
- clientDisconnected = true;
3814
- log$28.info("[stream] 客户端断开连接", { sessionId });
3815
- });
3816
3945
  const handleRuntimeEvent = (event) => {
3817
3946
  const eventSummary = summarizeForAudit(event);
3818
3947
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "runtime_event", {
3819
3948
  resultSummary: eventSummary,
3820
3949
  metadata: buildRuntimeEventAuditMetadata(event)
3821
3950
  }));
3822
- messageStore.appendMessage({
3951
+ queueRunMessage(run, messageStore, {
3823
3952
  sessionId,
3824
3953
  role: "tool",
3825
3954
  content: JSON.stringify({
@@ -3827,47 +3956,47 @@ function createChatRouter(config) {
3827
3956
  event: eventSummary
3828
3957
  }),
3829
3958
  channel: "web",
3830
- metadata: buildRuntimeEventMessageMetadata(event, runtimeConfig)
3831
- }).catch((err) => {
3832
- log$28.error("[stream] 保存 runtime event 消息失败", {
3833
- sessionId,
3834
- error: err
3835
- });
3836
- });
3837
- if (!clientDisconnected) sendSSE(res, "runtime-event", {
3959
+ metadata: {
3960
+ ...buildRuntimeEventMessageMetadata(event, runtimeConfig),
3961
+ runId: run.runId
3962
+ }
3963
+ }, "[stream] 保存 runtime event 消息失败");
3964
+ broadcastRunEvent(run, "runtime-event", {
3838
3965
  type: event.type,
3839
3966
  event: eventSummary
3840
3967
  });
3841
3968
  };
3842
3969
  const callbacks = {
3843
3970
  onTextDelta: (text) => {
3844
- accumText += text;
3845
- if (!clientDisconnected) sendSSE(res, "text-delta", { text });
3971
+ run.text += text;
3972
+ broadcastRunEvent(run, "text-delta", { text });
3846
3973
  },
3847
3974
  onThinkingDelta: (text) => {
3848
- if (!clientDisconnected) sendSSE(res, "thinking-delta", { text });
3975
+ broadcastRunEvent(run, "thinking-delta", { text });
3849
3976
  },
3850
3977
  onToolUseStart: (toolName, toolInput) => {
3851
3978
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "tool_call_started", {
3852
3979
  toolName,
3853
3980
  inputSummary: summarizeForAudit(toolInput)
3854
3981
  }));
3855
- messageStore.appendMessage({
3982
+ queueRunMessage(run, messageStore, {
3856
3983
  sessionId,
3857
3984
  role: "tool",
3858
3985
  content: JSON.stringify({
3859
3986
  toolName,
3860
- input: toolInput
3987
+ input: summarizeForAudit(toolInput)
3861
3988
  }),
3862
3989
  channel: "web",
3863
- metadata: { toolName }
3864
- }).catch((err) => {
3865
- log$28.error("[stream] 保存 tool 消息失败", {
3866
- sessionId,
3867
- error: err
3868
- });
3869
- });
3870
- if (!clientDisconnected) sendSSE(res, "tool-use-start", {
3990
+ metadata: {
3991
+ runId: run.runId,
3992
+ requestId,
3993
+ traceId,
3994
+ scenarioId,
3995
+ runtimeEventType: "tool-use-start",
3996
+ toolName
3997
+ }
3998
+ }, "[stream] 保存 tool 消息失败");
3999
+ broadcastRunEvent(run, "tool-use-start", {
3871
4000
  toolName,
3872
4001
  input: toolInput
3873
4002
  });
@@ -3879,62 +4008,80 @@ function createChatRouter(config) {
3879
4008
  resultSummary,
3880
4009
  metadata: buildToolResultAuditMetadata(result)
3881
4010
  }));
3882
- if (!clientDisconnected) sendSSE(res, "tool-result", {
4011
+ queueRunMessage(run, messageStore, {
4012
+ sessionId,
4013
+ role: "tool",
4014
+ content: JSON.stringify({
4015
+ toolUseId,
4016
+ result: resultSummary
4017
+ }),
4018
+ channel: "web",
4019
+ metadata: {
4020
+ runId: run.runId,
4021
+ requestId,
4022
+ traceId,
4023
+ scenarioId,
4024
+ runtimeEventType: "tool-result",
4025
+ toolCallId: toolUseId,
4026
+ toolResult: resultSummary
4027
+ }
4028
+ }, "[stream] 保存 tool result 消息失败");
4029
+ broadcastRunEvent(run, "tool-result", {
3883
4030
  toolUseId,
3884
4031
  result: resultSummary
3885
4032
  });
3886
4033
  },
3887
4034
  onToolProgress: (toolName, elapsedSeconds) => {
3888
- if (!clientDisconnected) sendSSE(res, "tool-progress", {
4035
+ broadcastRunEvent(run, "tool-progress", {
3889
4036
  toolName,
3890
4037
  elapsedSeconds
3891
4038
  });
3892
4039
  },
3893
4040
  onTurnEnd: (stopReason) => {
3894
4041
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "turn_finished", { metadata: { stopReason } }));
3895
- if (!clientDisconnected) sendSSE(res, "turn-end", { stopReason });
4042
+ broadcastRunEvent(run, "turn-end", { stopReason });
3896
4043
  },
3897
4044
  onResult: async (result) => {
3898
- try {
3899
- await messageStore.appendMessage({
3900
- sessionId,
3901
- role: "assistant",
3902
- content: accumText,
3903
- channel: "web",
3904
- metadata: {
3905
- triggeredBy: "web",
3906
- durationMs: result.durationMs,
3907
- numTurns: result.numTurns,
3908
- traceId,
3909
- requestId
3910
- }
3911
- });
3912
- } catch (err) {
3913
- log$28.error("[stream] 保存 assistant 消息失败", {
3914
- sessionId,
3915
- error: err
3916
- });
3917
- }
3918
- if (!clientDisconnected) {
3919
- sendSSE(res, "done", {
3920
- sessionId,
3921
- result: result.subtype,
3922
- text: accumText,
4045
+ const finalText = run.text || result.result || "";
4046
+ await markRunTerminal(result.isError ? "failed" : "completed", { finalStatus: result.subtype });
4047
+ await queueRunMessage(run, messageStore, {
4048
+ sessionId,
4049
+ role: "assistant",
4050
+ content: finalText,
4051
+ channel: "web",
4052
+ metadata: {
4053
+ runId: run.runId,
4054
+ triggeredBy: "web",
3923
4055
  durationMs: result.durationMs,
3924
- totalCostUsd: result.totalCostUsd,
3925
- numTurns: result.numTurns
3926
- });
3927
- res.end();
3928
- }
4056
+ numTurns: result.numTurns,
4057
+ traceId,
4058
+ requestId,
4059
+ scenarioId,
4060
+ finalStatus: result.subtype,
4061
+ isError: result.isError
4062
+ }
4063
+ }, "[stream] 保存 assistant 消息失败");
4064
+ broadcastRunEvent(run, "done", {
4065
+ sessionId,
4066
+ runId: run.runId,
4067
+ result: result.subtype,
4068
+ text: finalText,
4069
+ durationMs: result.durationMs,
4070
+ totalCostUsd: result.totalCostUsd,
4071
+ numTurns: result.numTurns
4072
+ });
4073
+ endRunClients(run);
3929
4074
  log$28.info("[stream] 对话完成", {
3930
4075
  sessionId,
4076
+ runId: run.runId,
3931
4077
  result: result.subtype,
3932
- textLength: accumText.length,
4078
+ textLength: finalText.length,
3933
4079
  durationMs: result.durationMs
3934
4080
  });
3935
4081
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "run_finished", {
3936
4082
  durationMs: result.durationMs,
3937
4083
  metadata: {
4084
+ runId: run.runId,
3938
4085
  subtype: result.subtype,
3939
4086
  totalCostUsd: result.totalCostUsd,
3940
4087
  numTurns: result.numTurns,
@@ -3945,22 +4092,45 @@ function createChatRouter(config) {
3945
4092
  }
3946
4093
  }));
3947
4094
  },
3948
- onError: (error) => {
4095
+ onError: async (error) => {
3949
4096
  log$28.error("[stream] Runtime 执行出错", {
3950
4097
  sessionId,
4098
+ runId: run.runId,
3951
4099
  error: error.message
3952
4100
  });
4101
+ await queueRunMessage(run, messageStore, {
4102
+ sessionId,
4103
+ role: "assistant",
4104
+ content: error.message,
4105
+ channel: "web",
4106
+ metadata: {
4107
+ runId: run.runId,
4108
+ triggeredBy: "web",
4109
+ traceId,
4110
+ requestId,
4111
+ scenarioId,
4112
+ finalStatus: "error",
4113
+ isError: true,
4114
+ errorMessage: error.message
4115
+ }
4116
+ }, "[stream] 保存 error 消息失败");
3953
4117
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "error", {
3954
4118
  errorMessage: error.message,
3955
4119
  metadata: {
4120
+ runId: run.runId,
3956
4121
  traceId,
3957
4122
  requestId
3958
4123
  }
3959
4124
  }));
3960
- if (!clientDisconnected) {
3961
- sendSSE(res, "error", { message: error.message });
3962
- res.end();
3963
- }
4125
+ await markRunTerminal("failed", {
4126
+ finalStatus: "error",
4127
+ error: error.message
4128
+ });
4129
+ broadcastRunEvent(run, "error", {
4130
+ runId: run.runId,
4131
+ message: error.message
4132
+ });
4133
+ endRunClients(run);
3964
4134
  },
3965
4135
  onAuditEvent: (event) => {
3966
4136
  logChatAudit({
@@ -3975,7 +4145,21 @@ function createChatRouter(config) {
3975
4145
  resultSummary,
3976
4146
  metadata: buildVerifierAuditMetadata(result)
3977
4147
  }));
3978
- if (!clientDisconnected) sendSSE(res, "verifier-result", { result: resultSummary });
4148
+ queueRunMessage(run, messageStore, {
4149
+ sessionId,
4150
+ role: "tool",
4151
+ content: JSON.stringify({ verifierResult: resultSummary }),
4152
+ channel: "web",
4153
+ metadata: {
4154
+ runId: run.runId,
4155
+ requestId,
4156
+ traceId,
4157
+ scenarioId,
4158
+ runtimeEventType: "verifier-result",
4159
+ verifierResult: resultSummary
4160
+ }
4161
+ }, "[stream] 保存 verifier result 消息失败");
4162
+ broadcastRunEvent(run, "verifier-result", { result: resultSummary });
3979
4163
  },
3980
4164
  onRuntimeEvent: handleRuntimeEvent,
3981
4165
  onScenarioEvent: handleRuntimeEvent,
@@ -3984,6 +4168,7 @@ function createChatRouter(config) {
3984
4168
  };
3985
4169
  try {
3986
4170
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "run_started", { metadata: {
4171
+ runId: run.runId,
3987
4172
  cwd: runtimeConfig.cwd,
3988
4173
  maxTurns: runtimeConfig.maxTurns,
3989
4174
  maxBudgetUsd: runtimeConfig.maxBudgetUsd,
@@ -3996,21 +4181,34 @@ function createChatRouter(config) {
3996
4181
  } }));
3997
4182
  log$28.info("[stream] 开始执行 prompt", {
3998
4183
  sessionId,
4184
+ runId: run.runId,
3999
4185
  cwd: runtimeConfig.cwd
4000
4186
  });
4001
4187
  await processManager.executePrompt(runtimeConfig, callbacks);
4188
+ if (run.status === "running") {
4189
+ const errorMessage = "Runtime completed without a final result event";
4190
+ log$28.error("[stream] executePrompt 未返回终态回调", {
4191
+ sessionId,
4192
+ runId: run.runId
4193
+ });
4194
+ await markRunTerminal("failed", {
4195
+ finalStatus: "error",
4196
+ error: errorMessage
4197
+ });
4198
+ broadcastRunEvent(run, "error", {
4199
+ runId: run.runId,
4200
+ message: errorMessage
4201
+ });
4202
+ endRunClients(run);
4203
+ }
4002
4204
  } catch (err) {
4003
4205
  const errorMessage = err instanceof Error ? err.message : String(err);
4004
4206
  log$28.error("[stream] executePrompt 异常", {
4005
4207
  sessionId,
4208
+ runId: run.runId,
4006
4209
  error: errorMessage
4007
4210
  });
4008
- if (!clientDisconnected) {
4009
- sendSSE(res, "error", { message: errorMessage });
4010
- res.end();
4011
- }
4012
- } finally {
4013
- activeStreamSessions.delete(sessionId);
4211
+ if (run.status === "running") await callbacks.onError?.(err instanceof Error ? err : new Error(errorMessage));
4014
4212
  }
4015
4213
  });
4016
4214
  router.get("/api/chat/history", chatAuthMiddleware, async (req, res) => {
@@ -4033,10 +4231,17 @@ function createChatRouter(config) {
4033
4231
  cursor,
4034
4232
  limit
4035
4233
  });
4234
+ const run = chatRunsBySession.get(sessionId);
4235
+ const visibleRun = run && isRunVisibleToUser(run, {
4236
+ tenantKey: chatUser.tenantKey,
4237
+ openId: chatUser.openId
4238
+ }) ? run : void 0;
4036
4239
  res.json({
4037
4240
  messages: result.messages,
4038
4241
  nextCursor: result.nextCursor,
4039
- sessionId
4242
+ sessionId,
4243
+ activeRun: visibleRun?.status === "running" ? serializeChatRun(visibleRun) : null,
4244
+ latestRun: visibleRun ? serializeChatRun(visibleRun) : null
4040
4245
  });
4041
4246
  } catch (err) {
4042
4247
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -4047,6 +4252,32 @@ function createChatRouter(config) {
4047
4252
  res.status(500).json({ error: "Failed to fetch history" });
4048
4253
  }
4049
4254
  });
4255
+ router.get("/api/chat/runs", chatAuthMiddleware, async (req, res) => {
4256
+ const chatUser = res.locals.chatUser;
4257
+ const sessionId = req.query.session_id;
4258
+ if (!sessionId) {
4259
+ res.status(400).json({ error: "session_id is required" });
4260
+ return;
4261
+ }
4262
+ const run = chatRunsBySession.get(sessionId);
4263
+ if (!run || !isRunVisibleToUser(run, {
4264
+ tenantKey: chatUser.tenantKey,
4265
+ openId: chatUser.openId
4266
+ })) {
4267
+ res.json({
4268
+ sessionId,
4269
+ run: null,
4270
+ activeRun: null
4271
+ });
4272
+ return;
4273
+ }
4274
+ const serialized = serializeChatRun(run);
4275
+ res.json({
4276
+ sessionId,
4277
+ run: serialized,
4278
+ activeRun: run.status === "running" ? serialized : null
4279
+ });
4280
+ });
4050
4281
  router.get("/api/chat/sessions", chatAuthMiddleware, async (_req, res) => {
4051
4282
  const chatUser = res.locals.chatUser;
4052
4283
  log$28.info("[sessions] 列出用户会话", { userId: chatUser.userId });
@@ -4096,6 +4327,7 @@ function createChatRouter(config) {
4096
4327
  });
4097
4328
  try {
4098
4329
  await messageStore.deleteSession(sessionId);
4330
+ chatRunsBySession.delete(sessionId);
4099
4331
  res.json({ success: true });
4100
4332
  } catch (err) {
4101
4333
  const errorMessage = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",