@vibe-lark/larkpal 0.1.68 → 0.1.70

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
@@ -3011,7 +3011,7 @@ function wrapLarkpalAgentAdapter(adapter) {
3011
3011
  return adapter.name;
3012
3012
  },
3013
3013
  executePrompt(config, callbacks) {
3014
- return adapter.executePrompt(normalizeLarkpalAgentConfig(config), callbacks);
3014
+ return adapter.executePrompt(config, callbacks);
3015
3015
  },
3016
3016
  stopProcess(sessionId) {
3017
3017
  return adapter.stopProcess(sessionId);
@@ -3030,59 +3030,6 @@ function wrapLarkpalAgentAdapter(adapter) {
3030
3030
  }
3031
3031
  };
3032
3032
  }
3033
- function normalizeLarkpalAgentConfig(config) {
3034
- if (!Array.isArray(config.prompt)) return config;
3035
- const textParts = [];
3036
- const imageArtifacts = [];
3037
- const imageLines = [];
3038
- config.prompt.forEach((block, index) => {
3039
- if (block.type === "text") {
3040
- if (block.text.trim()) textParts.push(block.text);
3041
- return;
3042
- }
3043
- if (block.type !== "image") return;
3044
- const artifact = toAgentImageArtifact(block, index);
3045
- const label = artifact?.assetId ?? block.fileKey ?? `image_${index + 1}`;
3046
- if (artifact) imageArtifacts.push(artifact);
3047
- imageLines.push(artifact ? `- ${label}: ${artifact.path} (${artifact.mimeType})` : `- ${label}: inline image omitted from text prompt (${block.source.media_type})`);
3048
- });
3049
- if (imageLines.length === 0) return config;
3050
- const prompt = [
3051
- textParts.join("\n\n").trim(),
3052
- "[Images]",
3053
- ...imageLines,
3054
- "The user is asking about these image attachments. Use visual or file tools to inspect the image pixels when needed; do not infer image content from the file name alone."
3055
- ].filter(Boolean).join("\n");
3056
- return {
3057
- ...config,
3058
- prompt,
3059
- sourceArtifacts: mergeSourceArtifacts(config.sourceArtifacts, imageArtifacts),
3060
- metadata: {
3061
- ...config.metadata,
3062
- inboundImageCount: imageLines.length,
3063
- visualAllowedDirs: [config.cwd]
3064
- }
3065
- };
3066
- }
3067
- function toAgentImageArtifact(block, index) {
3068
- if (!block.filePath) return null;
3069
- return {
3070
- type: "image_asset",
3071
- assetId: block.fileKey ?? `inbound_image_${index + 1}`,
3072
- path: block.filePath,
3073
- mimeType: block.source.media_type.split(";", 1)[0] || "image/png",
3074
- metadata: {
3075
- source: "feishu-message",
3076
- fileKey: block.fileKey
3077
- }
3078
- };
3079
- }
3080
- function mergeSourceArtifacts(existing, imageArtifacts) {
3081
- if (imageArtifacts.length === 0) return existing;
3082
- if (existing === void 0) return imageArtifacts;
3083
- if (Array.isArray(existing)) return [...existing, ...imageArtifacts];
3084
- return [existing, ...imageArtifacts];
3085
- }
3086
3033
  function getArtifactId(artifact) {
3087
3034
  const artifactId = typeof artifact.artifactId === "string" ? artifact.artifactId.trim() : "";
3088
3035
  if (artifactId.startsWith("art_")) return artifactId;
@@ -3724,6 +3671,8 @@ function summarizeString(value) {
3724
3671
  *
3725
3672
  * 路由:
3726
3673
  * POST /api/chat/stream — SSE 流式对话
3674
+ * POST /api/chat/runs/:runId/cancel — 取消运行中的 Web Chat run
3675
+ * POST /api/chat/sessions/:id/cancel-active-run — 取消会话中的 active run
3727
3676
  * GET /api/chat/history — 分页获取会话历史
3728
3677
  * GET /api/chat/sessions — 列出当前用户的所有会话
3729
3678
  * POST /api/chat/sessions — 创建新会话
@@ -3754,6 +3703,9 @@ function getActiveChatRun(sessionId) {
3754
3703
  const run = chatRunsBySession.get(sessionId);
3755
3704
  return run?.status === "running" ? run : void 0;
3756
3705
  }
3706
+ function findChatRunByRunId(runId) {
3707
+ for (const run of chatRunsBySession.values()) if (run.runId === runId) return run;
3708
+ }
3757
3709
  function serializeChatRun(run) {
3758
3710
  const idleSeconds = Math.max(0, Math.floor((Date.now() - run.lastEventAt) / 1e3));
3759
3711
  return {
@@ -3944,6 +3896,34 @@ async function cleanupTerminalRunProcess(run, processManager, reason) {
3944
3896
  });
3945
3897
  }
3946
3898
  }
3899
+ async function cancelChatRun(run, messageStore, processManager) {
3900
+ if (run.status !== "running") return {
3901
+ cancelled: false,
3902
+ run: serializeChatRun(run)
3903
+ };
3904
+ const processInfo = processManager.getProcessInfo(run.sessionId);
3905
+ run.adapterBusy = processManager.isSessionBusy?.(run.sessionId) ?? processInfo?.status === "running";
3906
+ run.processStatus = processInfo?.status;
3907
+ await processManager.stopProcess(run.sessionId);
3908
+ const stoppedInfo = processManager.getProcessInfo(run.sessionId);
3909
+ run.adapterBusy = processManager.isSessionBusy?.(run.sessionId) ?? false;
3910
+ run.processStatus = stoppedInfo?.status ?? "stopped";
3911
+ const marked = await markRunTerminal(run, messageStore, "cancelled", {
3912
+ finalStatus: "user_cancelled",
3913
+ error: "Chat run cancelled by user"
3914
+ });
3915
+ if (marked) {
3916
+ broadcastRunEvent(run, "cancelled", {
3917
+ runId: run.runId,
3918
+ sessionId: run.sessionId
3919
+ });
3920
+ endRunClients(run);
3921
+ }
3922
+ return {
3923
+ cancelled: marked,
3924
+ run: serializeChatRun(run)
3925
+ };
3926
+ }
3947
3927
  async function reconcileRunWithAdapter(run, messageStore, processManager) {
3948
3928
  if (run.status !== "running") return;
3949
3929
  const processInfo = processManager.getProcessInfo(run.sessionId);
@@ -4028,6 +4008,8 @@ function buildRuntimeEventMessageMetadata(event, runtimeConfig) {
4028
4008
  *
4029
4009
  * 路由:
4030
4010
  * POST /api/chat/stream — SSE 流式对话
4011
+ * POST /api/chat/runs/:runId/cancel — 取消运行中的 Web Chat run
4012
+ * POST /api/chat/sessions/:id/cancel-active-run — 取消会话中的 active run
4031
4013
  * GET /api/chat/history — 分页获取会话历史
4032
4014
  * GET /api/chat/sessions — 列出当前用户的所有会话
4033
4015
  * POST /api/chat/sessions — 创建新会话
@@ -4188,6 +4170,7 @@ function createChatRouter(config) {
4188
4170
  }, 15e3);
4189
4171
  const handleBlockedEvent = (event) => {
4190
4172
  const sanitized = sanitizeBlockedEvent(event);
4173
+ if (run.status !== "running") return;
4191
4174
  recordRunEvent(run, "blocked", { toolName: sanitized.name });
4192
4175
  log$30.warn("[stream] Agent blocked event", { ...sanitized });
4193
4176
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, sanitized.type, {
@@ -4219,6 +4202,7 @@ function createChatRouter(config) {
4219
4202
  broadcastRunEvent(run, "blocked", { event: sanitized });
4220
4203
  };
4221
4204
  const handleRuntimeEvent = (event) => {
4205
+ if (run.status !== "running") return;
4222
4206
  const dedupeKey = getRuntimeEventDedupeKey(event);
4223
4207
  if (dedupeKey) {
4224
4208
  if (run.seenRuntimeEventIds.has(dedupeKey)) {
@@ -4260,15 +4244,18 @@ function createChatRouter(config) {
4260
4244
  };
4261
4245
  const callbacks = {
4262
4246
  onTextDelta: (text) => {
4247
+ if (run.status !== "running") return;
4263
4248
  recordRunEvent(run, "text-delta");
4264
4249
  run.text += text;
4265
4250
  broadcastRunEvent(run, "text-delta", { text });
4266
4251
  },
4267
4252
  onThinkingDelta: (text) => {
4253
+ if (run.status !== "running") return;
4268
4254
  recordRunEvent(run, "thinking-delta");
4269
4255
  broadcastRunEvent(run, "thinking-delta", { text });
4270
4256
  },
4271
4257
  onToolUseStart: (toolName, toolInput) => {
4258
+ if (run.status !== "running") return;
4272
4259
  recordRunEvent(run, "tool-use-start", { toolName });
4273
4260
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "tool_call_started", {
4274
4261
  toolName,
@@ -4297,6 +4284,7 @@ function createChatRouter(config) {
4297
4284
  });
4298
4285
  },
4299
4286
  onToolResult: (toolUseId, result) => {
4287
+ if (run.status !== "running") return;
4300
4288
  recordRunEvent(run, "tool-result");
4301
4289
  const resultSummary = summarizeForAudit(result);
4302
4290
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "tool_call_finished", {
@@ -4328,6 +4316,7 @@ function createChatRouter(config) {
4328
4316
  });
4329
4317
  },
4330
4318
  onToolProgress: (toolName, elapsedSeconds) => {
4319
+ if (run.status !== "running") return;
4331
4320
  recordRunEvent(run, "tool-progress", { toolName });
4332
4321
  broadcastRunEvent(run, "tool-progress", {
4333
4322
  toolName,
@@ -4335,11 +4324,13 @@ function createChatRouter(config) {
4335
4324
  });
4336
4325
  },
4337
4326
  onTurnEnd: (stopReason) => {
4327
+ if (run.status !== "running") return;
4338
4328
  recordRunEvent(run, "turn-end");
4339
4329
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "turn_finished", { metadata: { stopReason } }));
4340
4330
  broadcastRunEvent(run, "turn-end", { stopReason });
4341
4331
  },
4342
4332
  onResult: async (result) => {
4333
+ if (run.status !== "running") return;
4343
4334
  recordRunEvent(run, "result");
4344
4335
  const finalText = run.text.trim() ? run.text : result.result || "";
4345
4336
  run.text = finalText;
@@ -4395,6 +4386,7 @@ function createChatRouter(config) {
4395
4386
  }));
4396
4387
  },
4397
4388
  onError: async (error) => {
4389
+ if (run.status !== "running") return;
4398
4390
  recordRunEvent(run, "error");
4399
4391
  log$30.error("[stream] Runtime 执行出错", {
4400
4392
  sessionId,
@@ -4445,6 +4437,7 @@ function createChatRouter(config) {
4445
4437
  });
4446
4438
  },
4447
4439
  onVerifierResult: (result) => {
4440
+ if (run.status !== "running") return;
4448
4441
  recordRunEvent(run, "verifier-result");
4449
4442
  const resultSummary = summarizeForAudit(result);
4450
4443
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "verifier_result", {
@@ -4519,6 +4512,84 @@ function createChatRouter(config) {
4519
4512
  if (run.status === "running") await callbacks.onError?.(err instanceof Error ? err : new Error(errorMessage));
4520
4513
  }
4521
4514
  });
4515
+ router.post("/api/chat/runs/:runId/cancel", chatAuthMiddleware, async (req, res) => {
4516
+ const chatUser = res.locals.chatUser;
4517
+ const runId = String(req.params.runId);
4518
+ const run = findChatRunByRunId(runId);
4519
+ if (!run) {
4520
+ res.json({
4521
+ success: true,
4522
+ cancelled: false,
4523
+ run: null
4524
+ });
4525
+ return;
4526
+ }
4527
+ if (!isRunVisibleToUser(run, {
4528
+ tenantKey: chatUser.tenantKey,
4529
+ openId: chatUser.openId
4530
+ })) {
4531
+ res.status(403).json({ error: "Forbidden" });
4532
+ return;
4533
+ }
4534
+ try {
4535
+ const result = await cancelChatRun(run, messageStore, processManager);
4536
+ res.json({
4537
+ success: true,
4538
+ ...result
4539
+ });
4540
+ } catch (err) {
4541
+ const errorMessage = err instanceof Error ? err.message : String(err);
4542
+ log$30.error("[runs] 取消 run 失败", {
4543
+ runId,
4544
+ sessionId: run.sessionId,
4545
+ error: errorMessage
4546
+ });
4547
+ res.status(500).json({
4548
+ error: "Failed to cancel chat run",
4549
+ run: serializeChatRun(run)
4550
+ });
4551
+ }
4552
+ });
4553
+ router.post("/api/chat/sessions/:id/cancel-active-run", chatAuthMiddleware, async (req, res) => {
4554
+ const chatUser = res.locals.chatUser;
4555
+ const sessionId = String(req.params.id);
4556
+ const run = chatRunsBySession.get(sessionId);
4557
+ if (!run) {
4558
+ res.json({
4559
+ success: true,
4560
+ cancelled: false,
4561
+ sessionId,
4562
+ run: null
4563
+ });
4564
+ return;
4565
+ }
4566
+ if (!isRunVisibleToUser(run, {
4567
+ tenantKey: chatUser.tenantKey,
4568
+ openId: chatUser.openId
4569
+ })) {
4570
+ res.status(403).json({ error: "Forbidden" });
4571
+ return;
4572
+ }
4573
+ try {
4574
+ const result = await cancelChatRun(run, messageStore, processManager);
4575
+ res.json({
4576
+ success: true,
4577
+ sessionId,
4578
+ ...result
4579
+ });
4580
+ } catch (err) {
4581
+ const errorMessage = err instanceof Error ? err.message : String(err);
4582
+ log$30.error("[sessions] 取消 active run 失败", {
4583
+ sessionId,
4584
+ runId: run.runId,
4585
+ error: errorMessage
4586
+ });
4587
+ res.status(500).json({
4588
+ error: "Failed to cancel active chat run",
4589
+ run: serializeChatRun(run)
4590
+ });
4591
+ }
4592
+ });
4522
4593
  router.get("/api/chat/history", chatAuthMiddleware, async (req, res) => {
4523
4594
  const chatUser = res.locals.chatUser;
4524
4595
  const sessionId = req.query.session_id;
@@ -8997,6 +9068,7 @@ function getUserFacingErrorMessage(error) {
8997
9068
  const log$21 = larkLogger("card/cc-stream-bridge");
8998
9069
  const CC_INTERNAL_PLACEHOLDER = "No response requested.";
8999
9070
  const CLAUDE_INVALID_PARAMETER_PATTERN = /API Error:\s*400\b.*parameter specified in the request is not valid/i;
9071
+ const TOOL_PROGRESS_CARD_REFRESH_INTERVAL_MS = 15e3;
9000
9072
  function buildClaudeCodeUserFacingError(errorMessage, hasImages) {
9001
9073
  if (hasImages && CLAUDE_INVALID_PARAMETER_PATTERN.test(errorMessage)) return createUserFacingError([
9002
9074
  "当前运行时无法处理这条图片消息。",
@@ -9020,6 +9092,8 @@ var CCStreamBridge = class {
9020
9092
  activeTools = /* @__PURE__ */ new Map();
9021
9093
  /** 最近一次 toolUseStart 的工具名(用于 toolResult 时查找) */
9022
9094
  lastToolName = null;
9095
+ /** 最近一次由 tool_progress 推动卡片刷新的时间 */
9096
+ lastToolProgressCardRefreshAt = 0;
9023
9097
  controller;
9024
9098
  options;
9025
9099
  constructor(controller, options) {
@@ -9108,13 +9182,18 @@ var CCStreamBridge = class {
9108
9182
  });
9109
9183
  this.controller.onToolPayload({ text: `✅ ${displayName} 完成` });
9110
9184
  }
9111
- /** 工具执行进度 → 记录日志 */
9185
+ /** 工具执行进度 → 节流刷新卡片,避免长工具执行期间卡片长时间无可见更新 */
9112
9186
  onToolProgress(toolName, elapsedSeconds) {
9187
+ const displayName = getToolDisplayName(toolName);
9113
9188
  log$21.debug("toolProgress 事件", {
9114
9189
  toolName,
9115
- displayName: getToolDisplayName(toolName),
9190
+ displayName,
9116
9191
  elapsedSeconds
9117
9192
  });
9193
+ const now = Date.now();
9194
+ if (now - this.lastToolProgressCardRefreshAt < TOOL_PROGRESS_CARD_REFRESH_INTERVAL_MS) return;
9195
+ this.lastToolProgressCardRefreshAt = now;
9196
+ this.controller.onToolPayload({ text: `${displayName} running for ${Math.max(0, Math.floor(elapsedSeconds))}s` });
9118
9197
  }
9119
9198
  /** 轮次结束 → 仅在 end_turn 时标记完成 + 触发 onIdle */
9120
9199
  onTurnEnd(stopReason) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",
@@ -21,22 +21,9 @@
21
21
  "registry": "https://registry.npmjs.org",
22
22
  "access": "public"
23
23
  },
24
- "packageManager": "pnpm@10.32.1",
25
24
  "engines": {
26
25
  "node": ">=22"
27
26
  },
28
- "scripts": {
29
- "start": "node --env-file=.env bin/larkpal.js",
30
- "build": "tsdown",
31
- "test": "vitest run",
32
- "test:watch": "vitest",
33
- "lint": "eslint src/",
34
- "lint:fix": "eslint src/ --fix",
35
- "sit:larkpal-agent": "node scripts/sit/larkpal-agent-0.1.9-sit.mjs",
36
- "typecheck": "tsc --noEmit",
37
- "format": "prettier --write src/**/*.ts",
38
- "format:check": "prettier --check src/**/*.ts"
39
- },
40
27
  "dependencies": {
41
28
  "@larksuiteoapi/node-sdk": "^1.60.0",
42
29
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -69,5 +56,17 @@
69
56
  "typescript": "^5.9.3",
70
57
  "typescript-eslint": "^8.32.1",
71
58
  "vitest": "^4.1.1"
59
+ },
60
+ "scripts": {
61
+ "start": "node --env-file=.env bin/larkpal.js",
62
+ "build": "tsdown",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "lint": "eslint src/",
66
+ "lint:fix": "eslint src/ --fix",
67
+ "sit:larkpal-agent": "node scripts/sit/larkpal-agent-0.1.9-sit.mjs",
68
+ "typecheck": "tsc --noEmit",
69
+ "format": "prettier --write src/**/*.ts",
70
+ "format:check": "prettier --check src/**/*.ts"
72
71
  }
73
- }
72
+ }