@vibe-lark/larkpal 0.1.37 → 0.1.39

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 (3) hide show
  1. package/bin/larkpal.js +0 -0
  2. package/dist/main.mjs +690 -252
  3. package/package.json +14 -13
package/dist/main.mjs CHANGED
@@ -9,6 +9,7 @@ import "dotenv/config";
9
9
  import { homedir } from "node:os";
10
10
  import { access, lstat, mkdir, readFile, readdir, readlink, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
11
11
  import { execSync, spawn } from "node:child_process";
12
+ import { randomUUID } from "node:crypto";
12
13
  import { createInterface } from "node:readline";
13
14
  import { v4, v5 } from "uuid";
14
15
  import { EventEmitter } from "node:events";
@@ -326,6 +327,54 @@ const DEFAULT_CLAUDE_MD = `# LarkPal
326
327
  ## 技能
327
328
  - 你的可用技能在 ~/.claude/commands/ 和当前目录的 .claude/commands/ 中
328
329
  - 使用 /help 查看所有可用技能
330
+
331
+ ## 交互式卡片(重要)
332
+
333
+ 当你需要向用户发送交互式卡片时,在回复中嵌入以下标记。系统会自动检测并发送对应的飞书卡片。
334
+
335
+ ### 权限申请卡片
336
+ 当 lark-cli 调用返回权限不足错误(如 99991672、scope missing)时,使用此标记引导用户申请权限:
337
+
338
+ [PERMISSION_REQUEST]
339
+ \`\`\`json
340
+ {"scopes": ["calendar:calendar", "vc:meeting.meetingevent:read"]}
341
+ \`\`\`
342
+
343
+ 系统会发送一张独立的权限申请卡片,包含"去申请"和"已完成"按钮。用户完成配置后会自动通知你重试。
344
+
345
+ ### 向用户提问卡片
346
+ 当你需要收集用户的多维信息时(不确定且需要用户决策的场景),使用此标记发送结构化提问卡片:
347
+
348
+ [ASK_USER]
349
+ \`\`\`json
350
+ {
351
+ "question": "为了帮你完成这个任务,需要确认以下信息:",
352
+ "phases": [
353
+ {
354
+ "id": "target",
355
+ "title": "目标选择",
356
+ "description": "你想在哪个环境操作?",
357
+ "options": [{"label": "测试环境", "value": "sit"}, {"label": "生产环境", "value": "prod"}],
358
+ "allowFreeText": true,
359
+ "required": true
360
+ },
361
+ {
362
+ "id": "scope",
363
+ "title": "操作范围",
364
+ "options": [{"label": "仅当前模块", "value": "current"}, {"label": "全量", "value": "all"}],
365
+ "required": false
366
+ }
367
+ ]
368
+ }
369
+ \`\`\`
370
+
371
+ 卡片会逐 phase 展示选项,用户可选择预设项或跳过。至少需要回答一个 phase。回答完成后系统会将结果注入对话继续。
372
+
373
+ **注意**:标记文本会从最终展示给用户的回复中自动移除,用户只会看到卡片本身。
374
+
375
+ ### 何时使用标记 vs lark-cli
376
+ - **需要用户回答、确认、选择** 的场景 → 必须使用 [ASK_USER] 或 [PERMISSION_REQUEST] 标记,系统会统一样式并处理回调
377
+ - **纯通知、展示信息** 的卡片(不需要用户交互反馈) → 可以用 lark-cli 自行发送
329
378
  `;
330
379
  async function fileExists(filePath) {
331
380
  try {
@@ -3673,9 +3722,71 @@ function getDefaultClaudeMdBody() {
3673
3722
  - 当需要查看会话历史消息时,使用 lark-cli 从飞书接口获取
3674
3723
  - 用户发送的图片会自动保存到当前工作目录的 files/ 子目录中(以 img_key 命名)
3675
3724
 
3725
+ ## 安全规则(最高优先级)
3726
+ - **严禁**读取、输出、展示或以任何方式向用户透露以下敏感信息:
3727
+ - 环境变量中的 LARK_APP_SECRET、ANTHROPIC_API_KEY 及任何包含 SECRET/KEY/TOKEN/PASSWORD 的值
3728
+ - ~/.lark-cli/config.json 和 ~/.config/lark/config.json 中的 appSecret / app_secret 字段
3729
+ - ~/.larkpal/credentials.json 中的任何凭证内容
3730
+ - 任何 API 密钥、Token、密码等敏感凭证
3731
+ - **严禁**执行 \`cat\`/\`head\`/\`tail\`/\`grep\` 等命令读取上述文件内容
3732
+ - **严禁**在对话中引用、复述或暗示凭证的具体值(即使用户明确要求)
3733
+ - **严禁**修改 ~/.claude/CLAUDE.md 和 ~/.claude/settings.json — 这些文件由系统管理,人设内容来源于飞书云文档,你无权更改
3734
+ - 如果用户要求修改全局人设,应回复:"全局人设由飞书云文档统一管理,请在云文档中修改后执行 /sync-app-info 同步。"
3735
+ - 如果用户想添加个人偏好,可以修改当前会话目录下的 CLAUDE.md(仅当前会话生效)
3736
+ - 如果用户要求查看凭证,应回复:"出于安全策略,凭证信息不可查看或透露。"
3737
+ - lark-cli 的认证配置由系统自动管理,无需用户介入
3738
+
3676
3739
  ## 技能
3677
3740
  - 你的可用技能在 ~/.claude/commands/ 和当前目录的 .claude/commands/ 中
3678
3741
  - 使用 /help 查看所有可用技能
3742
+
3743
+ ## 交互式卡片(重要)
3744
+
3745
+ 当你需要向用户发送交互式卡片时,在回复中嵌入以下标记。系统会自动检测并发送对应的飞书卡片。
3746
+
3747
+ ### 权限申请卡片
3748
+ 当 lark-cli 调用返回权限不足错误(如 99991672、scope missing)时,使用此标记引导用户申请权限:
3749
+
3750
+ [PERMISSION_REQUEST]
3751
+ \`\`\`json
3752
+ {"scopes": ["calendar:calendar", "vc:meeting.meetingevent:read"]}
3753
+ \`\`\`
3754
+
3755
+ 系统会发送一张独立的权限申请卡片,包含"去申请"和"已完成"按钮。用户完成配置后会自动通知你重试。
3756
+
3757
+ ### 向用户提问卡片
3758
+ 当你需要收集用户的多维信息时(不确定且需要用户决策的场景),使用此标记发送结构化提问卡片:
3759
+
3760
+ [ASK_USER]
3761
+ \`\`\`json
3762
+ {
3763
+ "question": "为了帮你完成这个任务,需要确认以下信息:",
3764
+ "phases": [
3765
+ {
3766
+ "id": "target",
3767
+ "title": "目标选择",
3768
+ "description": "你想在哪个环境操作?",
3769
+ "options": [{"label": "测试环境", "value": "sit"}, {"label": "生产环境", "value": "prod"}],
3770
+ "allowFreeText": true,
3771
+ "required": true
3772
+ },
3773
+ {
3774
+ "id": "scope",
3775
+ "title": "操作范围",
3776
+ "options": [{"label": "仅当前模块", "value": "current"}, {"label": "全量", "value": "all"}],
3777
+ "required": false
3778
+ }
3779
+ ]
3780
+ }
3781
+ \`\`\`
3782
+
3783
+ 卡片会逐 phase 展示选项,用户可选择预设项或跳过。至少需要回答一个 phase。回答完成后系统会将结果注入对话继续。
3784
+
3785
+ **注意**:标记文本会从最终展示给用户的回复中自动移除,用户只会看到卡片本身。
3786
+
3787
+ ### 何时使用标记 vs lark-cli
3788
+ - **需要用户回答、确认、选择** 的场景 → 必须使用 [ASK_USER] 或 [PERMISSION_REQUEST] 标记,系统会统一样式并处理回调
3789
+ - **纯通知、展示信息** 的卡片(不需要用户交互反馈) → 可以用 lark-cli 自行发送
3679
3790
  `;
3680
3791
  }
3681
3792
  /**
@@ -5616,6 +5727,294 @@ var LarkClient = class LarkClient {
5616
5727
  };
5617
5728
  injectLarkClient(LarkClient);
5618
5729
  //#endregion
5730
+ //#region src/card/interactive-cards.ts
5731
+ /**
5732
+ * 交互式卡片模块 — 授权卡片 & 提问卡片
5733
+ *
5734
+ * 基于"后处理检测 + 下轮注入"的异步模式:
5735
+ * CC 无法暂停等待用户输入,因此通过卡片交互收集用户反馈后,
5736
+ * 将结果作为新消息注入 CC 的下一轮对话。
5737
+ *
5738
+ * 包含:
5739
+ * - 授权申请卡片(Permission Auth Card)
5740
+ * - 多 Phase 提问卡片(Ask Card)
5741
+ * - CC 输出文本后处理(检测特殊标记)
5742
+ * - 操作 ID 生命周期管理
5743
+ */
5744
+ const log$19 = larkLogger("card/interactive-cards");
5745
+ const pendingOperations = /* @__PURE__ */ new Map();
5746
+ const OPERATION_TTL_MS = 1800 * 1e3;
5747
+ setInterval(() => {
5748
+ const now = Date.now();
5749
+ for (const [id, op] of pendingOperations) if (now - op.createdAt > OPERATION_TTL_MS) pendingOperations.delete(id);
5750
+ }, 6e4);
5751
+ /**
5752
+ * 构建授权申请卡片(CardKit v2 格式)
5753
+ *
5754
+ * 包含:
5755
+ * - 缺失权限列表
5756
+ * - "去申请" 按钮(跳转到开放平台)
5757
+ * - "已完成" 按钮(回调通知宿主层)
5758
+ */
5759
+ function buildAuthCard(ctx) {
5760
+ const operationId = randomUUID();
5761
+ const openPlatformHost = ctx.brand === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn";
5762
+ const scopeQuery = ctx.scopes.join(",");
5763
+ const authUrl = `${openPlatformHost}/app/${ctx.appId}/auth?q=${encodeURIComponent(scopeQuery)}`;
5764
+ const card = {
5765
+ schema: "2.0",
5766
+ config: {
5767
+ wide_screen_mode: true,
5768
+ update_multi: true
5769
+ },
5770
+ header: {
5771
+ template: "orange",
5772
+ title: {
5773
+ tag: "plain_text",
5774
+ content: "🔐 需要申请权限才能继续"
5775
+ }
5776
+ },
5777
+ body: { elements: [
5778
+ {
5779
+ tag: "markdown",
5780
+ content: `当前操作需要以下飞书权限,请应用管理员前往开放平台申请:\n\n${ctx.scopes.map((s) => `• \`${s}\``).join("\n")}`
5781
+ },
5782
+ { tag: "hr" },
5783
+ {
5784
+ tag: "markdown",
5785
+ content: "**第一步:** 前往开放平台申请上述权限并发布版本"
5786
+ },
5787
+ {
5788
+ tag: "action",
5789
+ actions: [{
5790
+ tag: "button",
5791
+ text: {
5792
+ tag: "plain_text",
5793
+ content: "去申请权限 ↗"
5794
+ },
5795
+ type: "primary",
5796
+ multi_url: {
5797
+ url: authUrl,
5798
+ pc_url: authUrl,
5799
+ android_url: authUrl,
5800
+ ios_url: authUrl
5801
+ }
5802
+ }]
5803
+ },
5804
+ {
5805
+ tag: "markdown",
5806
+ content: "**第二步:** 权限申请通过后,点击下方按钮通知我继续"
5807
+ },
5808
+ {
5809
+ tag: "action",
5810
+ actions: [{
5811
+ tag: "button",
5812
+ text: {
5813
+ tag: "plain_text",
5814
+ content: "✓ 已完成权限配置"
5815
+ },
5816
+ type: "default",
5817
+ behaviors: [{
5818
+ type: "callback",
5819
+ value: {
5820
+ action: "auth_complete",
5821
+ operationId
5822
+ }
5823
+ }]
5824
+ }]
5825
+ }
5826
+ ] }
5827
+ };
5828
+ pendingOperations.set(operationId, {
5829
+ type: "auth",
5830
+ operationId,
5831
+ chatId: ctx.chatId,
5832
+ sessionId: ctx.sessionId,
5833
+ createdAt: Date.now(),
5834
+ authContext: {
5835
+ appId: ctx.appId,
5836
+ scopes: ctx.scopes
5837
+ }
5838
+ });
5839
+ log$19.info("已创建授权卡片操作", {
5840
+ operationId,
5841
+ scopes: ctx.scopes,
5842
+ sessionId: ctx.sessionId
5843
+ });
5844
+ return {
5845
+ card,
5846
+ operationId
5847
+ };
5848
+ }
5849
+ /**
5850
+ * 构建多 phase 提问卡片(CardKit v2 格式)
5851
+ *
5852
+ * 每次显示一个 phase:
5853
+ * - 有预设选项时显示按钮组
5854
+ * - 允许自由文本时显示输入提示
5855
+ * - 底部有"跳过"和"提交"按钮
5856
+ */
5857
+ function buildAskCard(ctx, phaseIndex = 0) {
5858
+ const operationId = randomUUID();
5859
+ const phase = ctx.phases[phaseIndex];
5860
+ const totalPhases = ctx.phases.length;
5861
+ const elements = [];
5862
+ elements.push({
5863
+ tag: "markdown",
5864
+ content: ctx.question
5865
+ });
5866
+ if (phase.description) elements.push({
5867
+ tag: "markdown",
5868
+ content: phase.description
5869
+ });
5870
+ elements.push({ tag: "hr" });
5871
+ elements.push({
5872
+ tag: "markdown",
5873
+ content: `**${phase.title}** (${phaseIndex + 1}/${totalPhases})`
5874
+ });
5875
+ if (phase.options && phase.options.length > 0) {
5876
+ const optionButtons = phase.options.map((opt) => ({
5877
+ tag: "button",
5878
+ text: {
5879
+ tag: "plain_text",
5880
+ content: opt.label
5881
+ },
5882
+ type: "default",
5883
+ behaviors: [{
5884
+ type: "callback",
5885
+ value: {
5886
+ action: "ask_select",
5887
+ operationId,
5888
+ phaseId: phase.id,
5889
+ value: opt.value
5890
+ }
5891
+ }]
5892
+ }));
5893
+ elements.push({
5894
+ tag: "action",
5895
+ actions: optionButtons
5896
+ });
5897
+ }
5898
+ if (phase.allowFreeText !== false) elements.push({
5899
+ tag: "markdown",
5900
+ content: "_💡 你也可以直接回复消息补充更多信息_"
5901
+ });
5902
+ const bottomActions = [];
5903
+ if (totalPhases > 1 && phaseIndex < totalPhases - 1) bottomActions.push({
5904
+ tag: "button",
5905
+ text: {
5906
+ tag: "plain_text",
5907
+ content: "跳过 →"
5908
+ },
5909
+ type: "default",
5910
+ behaviors: [{
5911
+ type: "callback",
5912
+ value: {
5913
+ action: "ask_skip",
5914
+ operationId,
5915
+ phaseId: phase.id
5916
+ }
5917
+ }]
5918
+ });
5919
+ bottomActions.push({
5920
+ tag: "button",
5921
+ text: {
5922
+ tag: "plain_text",
5923
+ content: "✓ 完成提交"
5924
+ },
5925
+ type: "primary",
5926
+ behaviors: [{
5927
+ type: "callback",
5928
+ value: {
5929
+ action: "ask_submit",
5930
+ operationId
5931
+ }
5932
+ }]
5933
+ });
5934
+ if (bottomActions.length > 0) {
5935
+ elements.push({ tag: "hr" });
5936
+ elements.push({
5937
+ tag: "action",
5938
+ actions: bottomActions
5939
+ });
5940
+ }
5941
+ const card = {
5942
+ schema: "2.0",
5943
+ config: {
5944
+ wide_screen_mode: true,
5945
+ update_multi: true
5946
+ },
5947
+ header: {
5948
+ template: "blue",
5949
+ title: {
5950
+ tag: "plain_text",
5951
+ content: "💬 需要补充信息"
5952
+ }
5953
+ },
5954
+ body: { elements }
5955
+ };
5956
+ pendingOperations.set(operationId, {
5957
+ type: "ask",
5958
+ operationId,
5959
+ chatId: ctx.chatId,
5960
+ sessionId: ctx.sessionId,
5961
+ createdAt: Date.now(),
5962
+ askContext: {
5963
+ phases: ctx.phases,
5964
+ currentPhase: phaseIndex,
5965
+ answers: {}
5966
+ }
5967
+ });
5968
+ log$19.info("已创建提问卡片操作", {
5969
+ operationId,
5970
+ phaseId: phase.id,
5971
+ phaseIndex,
5972
+ totalPhases,
5973
+ sessionId: ctx.sessionId
5974
+ });
5975
+ return {
5976
+ card,
5977
+ operationId
5978
+ };
5979
+ }
5980
+ /** CC 输出中的授权请求标记格式 */
5981
+ const AUTH_REQUEST_PATTERN = /\[PERMISSION_REQUEST\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
5982
+ /** CC 输出中的提问请求标记格式 */
5983
+ const ASK_USER_PATTERN = /\[ASK_USER\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
5984
+ /**
5985
+ * 从 CC 回复文本中检测是否包含授权请求标记
5986
+ */
5987
+ function detectAuthRequest(text) {
5988
+ const match = text.match(AUTH_REQUEST_PATTERN);
5989
+ if (!match) return null;
5990
+ try {
5991
+ const data = JSON.parse(match[1]);
5992
+ if (Array.isArray(data.scopes) && data.scopes.length > 0) return { scopes: data.scopes };
5993
+ } catch {}
5994
+ return null;
5995
+ }
5996
+ /**
5997
+ * 从 CC 回复文本中检测是否包含提问请求标记
5998
+ */
5999
+ function detectAskRequest(text) {
6000
+ const match = text.match(ASK_USER_PATTERN);
6001
+ if (!match) return null;
6002
+ try {
6003
+ const data = JSON.parse(match[1]);
6004
+ if (typeof data.question === "string" && Array.isArray(data.phases) && data.phases.length > 0) return {
6005
+ question: data.question,
6006
+ phases: data.phases
6007
+ };
6008
+ } catch {}
6009
+ return null;
6010
+ }
6011
+ /**
6012
+ * 从 CC 回复文本中移除标记部分(避免展示给用户)
6013
+ */
6014
+ function stripInteractiveMarkers(text) {
6015
+ return text.replace(AUTH_REQUEST_PATTERN, "").replace(ASK_USER_PATTERN, "").trim();
6016
+ }
6017
+ //#endregion
5619
6018
  //#region src/card/reasoning-utils.ts
5620
6019
  /**
5621
6020
  * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
@@ -5874,7 +6273,7 @@ function sortTraceValue(value) {
5874
6273
  }
5875
6274
  //#endregion
5876
6275
  //#region src/card/cc-stream-bridge.ts
5877
- const log$19 = larkLogger("card/cc-stream-bridge");
6276
+ const log$18 = larkLogger("card/cc-stream-bridge");
5878
6277
  const CC_INTERNAL_PLACEHOLDER = "No response requested.";
5879
6278
  /**
5880
6279
  * CCStreamBridge — 将 CC 流事件桥接到 StreamingCardController
@@ -5899,7 +6298,7 @@ var CCStreamBridge = class {
5899
6298
  sessionKey: options?.sessionKey
5900
6299
  };
5901
6300
  if (this.options.sessionKey) startToolUseTraceRun(this.options.sessionKey);
5902
- log$19.info("CCStreamBridge 初始化", {
6301
+ log$18.info("CCStreamBridge 初始化", {
5903
6302
  autoCompleteOnTurnEnd: this.options.autoCompleteOnTurnEnd,
5904
6303
  sessionKey: this.options.sessionKey ?? "(none)"
5905
6304
  });
@@ -5907,7 +6306,7 @@ var CCStreamBridge = class {
5907
6306
  /** 文本增量 → 累积后调用 controller.onPartialReply */
5908
6307
  onTextDelta(text) {
5909
6308
  this.accumulatedText += text;
5910
- log$19.debug("textDelta 事件", {
6309
+ log$18.debug("textDelta 事件", {
5911
6310
  deltaLen: text.length,
5912
6311
  totalLen: this.accumulatedText.length
5913
6312
  });
@@ -5916,7 +6315,7 @@ var CCStreamBridge = class {
5916
6315
  /** 思考增量 → 累积后调用 controller.onReasoningStream */
5917
6316
  onThinkingDelta(text) {
5918
6317
  this.accumulatedThinkingText += text;
5919
- log$19.debug("thinkingDelta 事件", {
6318
+ log$18.debug("thinkingDelta 事件", {
5920
6319
  deltaLen: text.length,
5921
6320
  totalLen: this.accumulatedThinkingText.length
5922
6321
  });
@@ -5928,7 +6327,7 @@ var CCStreamBridge = class {
5928
6327
  const displayName = getToolDisplayName(toolName);
5929
6328
  const toolParams = typeof _toolInput === "object" && _toolInput !== null ? _toolInput : void 0;
5930
6329
  const hasParams = toolParams && Object.keys(toolParams).length > 0;
5931
- log$19.info("toolUseStart 事件", {
6330
+ log$18.info("toolUseStart 事件", {
5932
6331
  toolName,
5933
6332
  displayName,
5934
6333
  activeToolsCount: this.activeTools.size,
@@ -5941,7 +6340,7 @@ var CCStreamBridge = class {
5941
6340
  toolName,
5942
6341
  toolParams
5943
6342
  })) {
5944
- log$19.info("toolUseStart: 更新已有步骤的 params", { toolName });
6343
+ log$18.info("toolUseStart: 更新已有步骤的 params", { toolName });
5945
6344
  this.controller.onToolStart({
5946
6345
  name: toolName,
5947
6346
  phase: "start"
@@ -5965,7 +6364,7 @@ var CCStreamBridge = class {
5965
6364
  const toolName = this.activeTools.get(toolUseId) ?? this.lastToolName ?? "unknown";
5966
6365
  const displayName = getToolDisplayName(toolName);
5967
6366
  this.activeTools.delete(toolUseId);
5968
- log$19.info("toolResult 事件", {
6367
+ log$18.info("toolResult 事件", {
5969
6368
  toolUseId,
5970
6369
  toolName,
5971
6370
  displayName,
@@ -5980,7 +6379,7 @@ var CCStreamBridge = class {
5980
6379
  }
5981
6380
  /** 工具执行进度 → 记录日志 */
5982
6381
  onToolProgress(toolName, elapsedSeconds) {
5983
- log$19.debug("toolProgress 事件", {
6382
+ log$18.debug("toolProgress 事件", {
5984
6383
  toolName,
5985
6384
  displayName: getToolDisplayName(toolName),
5986
6385
  elapsedSeconds
@@ -5988,7 +6387,7 @@ var CCStreamBridge = class {
5988
6387
  }
5989
6388
  /** 轮次结束 → 仅在 end_turn 时标记完成 + 触发 onIdle */
5990
6389
  onTurnEnd(stopReason) {
5991
- log$19.info("turnEnd 事件", {
6390
+ log$18.info("turnEnd 事件", {
5992
6391
  stopReason,
5993
6392
  accumulatedTextLen: this.accumulatedText.length,
5994
6393
  accumulatedThinkingTextLen: this.accumulatedThinkingText.length
@@ -5996,7 +6395,7 @@ var CCStreamBridge = class {
5996
6395
  if (this.options.autoCompleteOnTurnEnd && stopReason === "end_turn") {
5997
6396
  const trimmedText = this.accumulatedText.trim();
5998
6397
  if (trimmedText === CC_INTERNAL_PLACEHOLDER) {
5999
- log$19.info("检测到 CC 内部占位消息,静默丢弃", {
6398
+ log$18.info("检测到 CC 内部占位消息,静默丢弃", {
6000
6399
  text: trimmedText.slice(0, 50),
6001
6400
  sessionKey: this.options.sessionKey
6002
6401
  });
@@ -6004,16 +6403,26 @@ var CCStreamBridge = class {
6004
6403
  return;
6005
6404
  }
6006
6405
  if (trimmedText === "") {
6007
- log$19.info("turnEnd 时文本为空,延迟到 onResult 兜底处理", { sessionKey: this.options.sessionKey });
6406
+ log$18.info("turnEnd 时文本为空,延迟到 onResult 兜底处理", { sessionKey: this.options.sessionKey });
6008
6407
  return;
6009
6408
  }
6409
+ const strippedText = stripInteractiveMarkers(trimmedText);
6410
+ if (strippedText !== trimmedText) {
6411
+ log$18.info("turnEnd: 检测到交互式卡片标记,修剪卡片文本", {
6412
+ originalLen: trimmedText.length,
6413
+ strippedLen: strippedText.length,
6414
+ sessionKey: this.options.sessionKey
6415
+ });
6416
+ this.accumulatedText = strippedText;
6417
+ if (strippedText) this.controller.onPartialReply({ text: strippedText });
6418
+ }
6010
6419
  this.controller.markFullyComplete();
6011
6420
  this.controller.onIdle();
6012
6421
  }
6013
6422
  }
6014
6423
  /** 最终结果 → 兜底最终化 + 错误处理 */
6015
6424
  onResult(data) {
6016
- log$19.info("result 事件", {
6425
+ log$18.info("result 事件", {
6017
6426
  subtype: data.subtype,
6018
6427
  isError: data.isError,
6019
6428
  durationMs: data.durationMs,
@@ -6025,7 +6434,7 @@ var CCStreamBridge = class {
6025
6434
  const pm = ClaudeCodeAdapter.getInstance()?.getProcessManager();
6026
6435
  const sessionId = this.options.sessionKey;
6027
6436
  if (sessionId && pm ? pm.consumeAborted(sessionId) : false) {
6028
- log$19.info("用户主动中断,按正常完成处理", {
6437
+ log$18.info("用户主动中断,按正常完成处理", {
6029
6438
  subtype: data.subtype,
6030
6439
  sessionId
6031
6440
  });
@@ -6034,14 +6443,14 @@ var CCStreamBridge = class {
6034
6443
  this.controller.onIdle();
6035
6444
  } else {
6036
6445
  const errorMessage = data.result ?? `CC 执行失败: ${data.subtype}`;
6037
- log$19.error("CC 执行返回错误", {
6446
+ log$18.error("CC 执行返回错误", {
6038
6447
  subtype: data.subtype,
6039
6448
  errorMessage: errorMessage.slice(0, 200)
6040
6449
  });
6041
6450
  this.controller.onError(new Error(errorMessage), { kind: "cc-execution" });
6042
6451
  }
6043
6452
  } else if (!this.accumulatedText.trim()) if (data.result) {
6044
- log$19.info("result 兜底:使用 result.result 作为最终回复", {
6453
+ log$18.info("result 兜底:使用 result.result 作为最终回复", {
6045
6454
  resultLen: data.result.length,
6046
6455
  sessionKey: this.options.sessionKey
6047
6456
  });
@@ -6049,7 +6458,7 @@ var CCStreamBridge = class {
6049
6458
  this.controller.markFullyComplete();
6050
6459
  this.controller.onIdle();
6051
6460
  } else {
6052
- log$19.info("result 兜底:无文本且无 result,abort 卡片", { sessionKey: this.options.sessionKey });
6461
+ log$18.info("result 兜底:无文本且无 result,abort 卡片", { sessionKey: this.options.sessionKey });
6053
6462
  this.controller.abortCard();
6054
6463
  }
6055
6464
  else {
@@ -6063,7 +6472,7 @@ var CCStreamBridge = class {
6063
6472
  * 内部复用上面的直接调用方法。
6064
6473
  */
6065
6474
  bindParser(parser) {
6066
- log$19.info("绑定 CCStreamParser 事件到卡片控制器");
6475
+ log$18.info("绑定 CCStreamParser 事件到卡片控制器");
6067
6476
  parser.on("textDelta", (text) => this.onTextDelta(text));
6068
6477
  parser.on("thinkingDelta", (text) => this.onThinkingDelta(text));
6069
6478
  parser.on("toolUseStart", (toolName, toolInput) => this.onToolUseStart(toolName, toolInput));
@@ -6071,7 +6480,7 @@ var CCStreamBridge = class {
6071
6480
  parser.on("toolProgress", (toolName, elapsedSeconds) => this.onToolProgress(toolName, elapsedSeconds));
6072
6481
  parser.on("turnEnd", (stopReason) => this.onTurnEnd(stopReason));
6073
6482
  parser.on("result", (data) => this.onResult(data));
6074
- log$19.info("CCStreamParser 事件绑定完成");
6483
+ log$18.info("CCStreamParser 事件绑定完成");
6075
6484
  }
6076
6485
  };
6077
6486
  //#endregion
@@ -8087,7 +8496,7 @@ function resolveLarkSdk(cfg, accountId) {
8087
8496
  if (cached) return cached.sdk;
8088
8497
  return LarkClient.fromCfg(cfg, accountId).sdk;
8089
8498
  }
8090
- const log$18 = larkLogger("card/cardkit");
8499
+ const log$17 = larkLogger("card/cardkit");
8091
8500
  /**
8092
8501
  * 记录 CardKit API 响应日志,检测错误码并抛出异常。
8093
8502
  *
@@ -8097,13 +8506,13 @@ const log$18 = larkLogger("card/cardkit");
8097
8506
  function logCardKitResponse(params) {
8098
8507
  const { resp, api, context } = params;
8099
8508
  const { code, msg } = resp;
8100
- log$18.info(`cardkit ${api} response`, {
8509
+ log$17.info(`cardkit ${api} response`, {
8101
8510
  code,
8102
8511
  msg,
8103
8512
  context
8104
8513
  });
8105
8514
  if (code && code !== 0) {
8106
- log$18.warn(`cardkit ${api} FAILED`, {
8515
+ log$17.warn(`cardkit ${api} FAILED`, {
8107
8516
  code,
8108
8517
  msg,
8109
8518
  context,
@@ -8514,7 +8923,7 @@ function validateLocalMediaRoots(filePath, localRoots) {
8514
8923
  * Feishu messages, uploading media to the Feishu IM storage, and
8515
8924
  * sending image / file messages to chats.
8516
8925
  */
8517
- const log$17 = larkLogger("outbound/media");
8926
+ const log$16 = larkLogger("outbound/media");
8518
8927
  /**
8519
8928
  * Upload an image to Feishu IM storage.
8520
8929
  *
@@ -8582,7 +8991,7 @@ async function validateRemoteUrl(raw) {
8582
8991
  for (const addr of addresses) if (isPrivateIP(addr)) throw new Error(`[feishu-media] Domain "${hostname}" resolves to private/reserved IP "${addr}" (SSRF protection). URL: "${raw}"`);
8583
8992
  } catch (err) {
8584
8993
  if (err instanceof Error && err.message.includes("SSRF protection")) throw err;
8585
- log$17.warn(`[feishu-media] DNS resolution failed for "${hostname}": ${err}`);
8994
+ log$16.warn(`[feishu-media] DNS resolution failed for "${hostname}": ${err}`);
8586
8995
  }
8587
8996
  }
8588
8997
  /**
@@ -8600,21 +9009,21 @@ async function fetchMediaBuffer(urlOrPath, localRoots) {
8600
9009
  if (localRoots !== void 0) validateLocalMediaRoots(filePath, localRoots);
8601
9010
  else throw new Error(`[feishu-media] Local file access denied for "${filePath}": mediaLocalRoots is not configured. Configure mediaLocalRoots to explicitly allow local file access.`);
8602
9011
  const buf = fs.readFileSync(filePath);
8603
- log$17.debug(`local file read: "${filePath}", ${buf.length} bytes`);
9012
+ log$16.debug(`local file read: "${filePath}", ${buf.length} bytes`);
8604
9013
  return buf;
8605
9014
  }
8606
9015
  await validateRemoteUrl(raw);
8607
9016
  const FETCH_TIMEOUT_MS = 3e4;
8608
- log$17.info(`fetching remote media: ${raw}`);
9017
+ log$16.info(`fetching remote media: ${raw}`);
8609
9018
  const response = await fetch(raw, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
8610
9019
  if (!response.ok) throw new Error(`[feishu-media] Failed to fetch media from "${raw}": HTTP ${response.status} ${response.statusText}. Verify the URL is accessible and returns a valid media resource.`);
8611
9020
  const arrayBuffer = await response.arrayBuffer();
8612
- log$17.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);
9021
+ log$16.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);
8613
9022
  return Buffer.from(arrayBuffer);
8614
9023
  }
8615
9024
  //#endregion
8616
9025
  //#region src/card/image-resolver.ts
8617
- const log$16 = larkLogger("card/image-resolver");
9026
+ const log$15 = larkLogger("card/image-resolver");
8618
9027
  /** Matches complete markdown image syntax: `![alt](value)` */
8619
9028
  const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
8620
9029
  var ImageResolver = class {
@@ -8660,14 +9069,14 @@ var ImageResolver = class {
8660
9069
  async resolveImagesAwait(text, timeoutMs) {
8661
9070
  this.resolveImages(text);
8662
9071
  if (this.pending.size > 0) {
8663
- log$16.info("resolveImagesAwait: waiting for uploads", {
9072
+ log$15.info("resolveImagesAwait: waiting for uploads", {
8664
9073
  count: this.pending.size,
8665
9074
  timeoutMs
8666
9075
  });
8667
9076
  const allUploads = Promise.all(this.pending.values());
8668
9077
  const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
8669
9078
  await Promise.race([allUploads, timeout]);
8670
- if (this.pending.size > 0) log$16.warn("resolveImagesAwait: timed out with pending uploads", { remaining: this.pending.size });
9079
+ if (this.pending.size > 0) log$15.warn("resolveImagesAwait: timed out with pending uploads", { remaining: this.pending.size });
8671
9080
  }
8672
9081
  return this.resolveImages(text);
8673
9082
  }
@@ -8677,7 +9086,7 @@ var ImageResolver = class {
8677
9086
  }
8678
9087
  async doUpload(url) {
8679
9088
  try {
8680
- log$16.info("uploading image", { url });
9089
+ log$15.info("uploading image", { url });
8681
9090
  const buffer = await fetchRemoteImageBuffer(url);
8682
9091
  const { imageKey } = await uploadImageLark({
8683
9092
  cfg: this.cfg,
@@ -8685,7 +9094,7 @@ var ImageResolver = class {
8685
9094
  imageType: "message",
8686
9095
  accountId: this.accountId
8687
9096
  });
8688
- log$16.info("image uploaded", {
9097
+ log$15.info("image uploaded", {
8689
9098
  url,
8690
9099
  imageKey
8691
9100
  });
@@ -8694,7 +9103,7 @@ var ImageResolver = class {
8694
9103
  this.onImageResolved();
8695
9104
  return imageKey;
8696
9105
  } catch (err) {
8697
- log$16.warn("image upload failed", {
9106
+ log$15.warn("image upload failed", {
8698
9107
  url,
8699
9108
  error: String(err)
8700
9109
  });
@@ -8715,7 +9124,7 @@ var ImageResolver = class {
8715
9124
  * Encapsulates the terminateDueToUnavailable / shouldSkipForUnavailable
8716
9125
  * logic previously scattered as closures in reply-dispatcher.ts.
8717
9126
  */
8718
- const log$15 = larkLogger("card/unavailable-guard");
9127
+ const log$14 = larkLogger("card/unavailable-guard");
8719
9128
  var UnavailableGuard = class {
8720
9129
  terminated = false;
8721
9130
  replyToMessageId;
@@ -8768,7 +9177,7 @@ var UnavailableGuard = class {
8768
9177
  this.terminated = true;
8769
9178
  this.onTerminate();
8770
9179
  const affectedMessageId = fromError?.messageId ?? this.replyToMessageId ?? cardMessageId ?? "unknown";
8771
- log$15.warn("reply pipeline terminated by unavailable message", {
9180
+ log$14.warn("reply pipeline terminated by unavailable message", {
8772
9181
  source,
8773
9182
  apiCode,
8774
9183
  messageId: affectedMessageId
@@ -8805,7 +9214,7 @@ function getActiveCard(sessionId) {
8805
9214
  * Delegates throttling to FlushController and message-unavailable
8806
9215
  * detection to UnavailableGuard.
8807
9216
  */
8808
- const log$14 = larkLogger("card/streaming");
9217
+ const log$13 = larkLogger("card/streaming");
8809
9218
  var StreamingCardController = class StreamingCardController {
8810
9219
  phase = "idle";
8811
9220
  cardKit = {
@@ -8893,7 +9302,7 @@ var StreamingCardController = class StreamingCardController {
8893
9302
  }
8894
9303
  }
8895
9304
  if (!entry) {
8896
- log$14.debug("footer metrics lookup: session entry missing", {
9305
+ log$13.debug("footer metrics lookup: session entry missing", {
8897
9306
  sessionKey: this.deps.sessionKey,
8898
9307
  candidateKeys,
8899
9308
  storePath,
@@ -8911,7 +9320,7 @@ var StreamingCardController = class StreamingCardController {
8911
9320
  contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : void 0,
8912
9321
  model: typeof entry.model === "string" ? entry.model : void 0
8913
9322
  };
8914
- log$14.debug("footer metrics lookup: session entry found", {
9323
+ log$13.debug("footer metrics lookup: session entry found", {
8915
9324
  sessionKey: this.deps.sessionKey,
8916
9325
  matchedKey,
8917
9326
  storePath,
@@ -8936,7 +9345,7 @@ var StreamingCardController = class StreamingCardController {
8936
9345
  }
8937
9346
  }
8938
9347
  if (!entry) {
8939
- log$14.debug("footer metrics lookup: session entry missing", {
9348
+ log$13.debug("footer metrics lookup: session entry missing", {
8940
9349
  sessionKey: this.deps.sessionKey,
8941
9350
  candidateKeys,
8942
9351
  storePath,
@@ -8954,7 +9363,7 @@ var StreamingCardController = class StreamingCardController {
8954
9363
  contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : void 0,
8955
9364
  model: typeof entry.model === "string" ? entry.model : void 0
8956
9365
  };
8957
- log$14.debug("footer metrics lookup: session entry found", {
9366
+ log$13.debug("footer metrics lookup: session entry found", {
8958
9367
  sessionKey: this.deps.sessionKey,
8959
9368
  matchedKey,
8960
9369
  storePath,
@@ -8962,7 +9371,7 @@ var StreamingCardController = class StreamingCardController {
8962
9371
  });
8963
9372
  return metrics;
8964
9373
  } catch (err) {
8965
- log$14.warn("footer metrics lookup failed", {
9374
+ log$13.warn("footer metrics lookup failed", {
8966
9375
  error: String(err),
8967
9376
  sessionKey: this.deps.sessionKey
8968
9377
  });
@@ -9069,7 +9478,7 @@ var StreamingCardController = class StreamingCardController {
9069
9478
  const from = this.phase;
9070
9479
  if (from === to) return false;
9071
9480
  if (!PHASE_TRANSITIONS[from].has(to)) {
9072
- log$14.warn("phase transition rejected", {
9481
+ log$13.warn("phase transition rejected", {
9073
9482
  from,
9074
9483
  to,
9075
9484
  source
@@ -9077,7 +9486,7 @@ var StreamingCardController = class StreamingCardController {
9077
9486
  return false;
9078
9487
  }
9079
9488
  this.phase = to;
9080
- log$14.info("phase transition", {
9489
+ log$13.info("phase transition", {
9081
9490
  from,
9082
9491
  to,
9083
9492
  source,
@@ -9197,7 +9606,7 @@ var StreamingCardController = class StreamingCardController {
9197
9606
  this.reasoning.dirty = true;
9198
9607
  }
9199
9608
  const text = split.answerText ?? stripReasoningTags(rawText);
9200
- log$14.debug("onPartialReply", { len: text.length });
9609
+ log$13.debug("onPartialReply", { len: text.length });
9201
9610
  if (!text) return;
9202
9611
  this.captureToolUseElapsed();
9203
9612
  if (!this.reasoning.reasoningStartTime) this.reasoning.reasoningStartTime = Date.now();
@@ -9209,7 +9618,7 @@ var StreamingCardController = class StreamingCardController {
9209
9618
  this.text.lastPartialText = text;
9210
9619
  this.text.accumulatedText = this.text.streamingPrefix ? this.text.streamingPrefix + "\n\n" + text : text;
9211
9620
  if (!this.text.streamingPrefix && SILENT_REPLY_TOKEN.startsWith(this.text.accumulatedText.trim())) {
9212
- log$14.debug("onPartialReply: buffering NO_REPLY prefix");
9621
+ log$13.debug("onPartialReply: buffering NO_REPLY prefix");
9213
9622
  return;
9214
9623
  }
9215
9624
  await this.ensureCardCreated();
@@ -9219,7 +9628,7 @@ var StreamingCardController = class StreamingCardController {
9219
9628
  }
9220
9629
  async onError(err, info) {
9221
9630
  if (this.guard.terminate("onError", err)) return;
9222
- log$14.error(`${info.kind} reply failed`, { error: String(err) });
9631
+ log$13.error(`${info.kind} reply failed`, { error: String(err) });
9223
9632
  this.captureToolUseElapsed();
9224
9633
  this.finalizeCard("onError", "error");
9225
9634
  await this.flush.waitForFlush();
@@ -9275,7 +9684,7 @@ var StreamingCardController = class StreamingCardController {
9275
9684
  if (this.cardKit.cardMessageId) {
9276
9685
  const isNoReplyLeak = !this.text.completedText && SILENT_REPLY_TOKEN.startsWith(this.text.accumulatedText.trim());
9277
9686
  const displayText = this.text.completedText || (isNoReplyLeak ? "" : this.text.accumulatedText) || "Done.";
9278
- if (!this.text.completedText && !this.text.accumulatedText) log$14.warn("reply completed without visible text, using empty-reply fallback");
9687
+ if (!this.text.completedText && !this.text.accumulatedText) log$13.warn("reply completed without visible text, using empty-reply fallback");
9279
9688
  const resolvedDisplayText = await this.imageResolver.resolveImagesAwait(displayText, 15e3);
9280
9689
  const idleToolUseDisplay = this.computeToolUseDisplay();
9281
9690
  const terminalContent = prepareTerminalCardContent({
@@ -9303,7 +9712,7 @@ var StreamingCardController = class StreamingCardController {
9303
9712
  const rewindSessionId = this.deps.sessionKey;
9304
9713
  const rewindMessageId = ClaudeCodeAdapter.getInstance()?.getProcessManager()?.getLastUserMessageId(this.deps.sessionKey);
9305
9714
  const rewindHasFileChanges = this.hasFileChangingTools(idleToolUseDisplay?.steps);
9306
- log$14.info("onIdle: rewind button conditions", {
9715
+ log$13.info("onIdle: rewind button conditions", {
9307
9716
  sessionId: rewindSessionId,
9308
9717
  messageId: rewindMessageId,
9309
9718
  hasFileChanges: rewindHasFileChanges,
@@ -9316,7 +9725,7 @@ var StreamingCardController = class StreamingCardController {
9316
9725
  if (idleEffectiveCardId) {
9317
9726
  const seqBeforeClose = this.cardKit.cardKitSequence;
9318
9727
  this.cardKit.cardKitSequence += 1;
9319
- log$14.info("onIdle: closing streaming mode", {
9728
+ log$13.info("onIdle: closing streaming mode", {
9320
9729
  attempt,
9321
9730
  seqBefore: seqBeforeClose,
9322
9731
  seqAfter: this.cardKit.cardKitSequence
@@ -9330,7 +9739,7 @@ var StreamingCardController = class StreamingCardController {
9330
9739
  });
9331
9740
  const seqBeforeUpdate = this.cardKit.cardKitSequence;
9332
9741
  this.cardKit.cardKitSequence += 1;
9333
- log$14.info("onIdle: updating final card", {
9742
+ log$13.info("onIdle: updating final card", {
9334
9743
  attempt,
9335
9744
  seqBefore: seqBeforeUpdate,
9336
9745
  seqAfter: this.cardKit.cardKitSequence
@@ -9348,33 +9757,33 @@ var StreamingCardController = class StreamingCardController {
9348
9757
  card: completeCard,
9349
9758
  accountId: this.deps.accountId
9350
9759
  });
9351
- log$14.info("reply completed, card finalized", {
9760
+ log$13.info("reply completed, card finalized", {
9352
9761
  elapsedMs: this.elapsed(),
9353
9762
  isCardKit: !!idleEffectiveCardId,
9354
9763
  attempt
9355
9764
  });
9356
9765
  break;
9357
9766
  } catch (retryErr) {
9358
- log$14.warn("final card update attempt failed", {
9767
+ log$13.warn("final card update attempt failed", {
9359
9768
  attempt,
9360
9769
  maxRetries: MAX_FINAL_UPDATE_RETRIES,
9361
9770
  error: String(retryErr)
9362
9771
  });
9363
9772
  if (attempt < MAX_FINAL_UPDATE_RETRIES) await new Promise((resolve) => setTimeout(resolve, FINAL_UPDATE_RETRY_DELAY_MS * attempt));
9364
- else log$14.error("final card update exhausted all retries, card may remain in streaming state", {
9773
+ else log$13.error("final card update exhausted all retries, card may remain in streaming state", {
9365
9774
  cardId: idleEffectiveCardId,
9366
9775
  messageId: this.cardKit.cardMessageId
9367
9776
  });
9368
9777
  }
9369
9778
  }
9370
9779
  } catch (err) {
9371
- log$14.error("final card update failed (outer)", { error: String(err) });
9780
+ log$13.error("final card update failed (outer)", { error: String(err) });
9372
9781
  } finally {
9373
9782
  clearToolUseTraceRun(this.deps.sessionKey);
9374
9783
  }
9375
9784
  }
9376
9785
  markFullyComplete() {
9377
- log$14.debug("markFullyComplete", {
9786
+ log$13.debug("markFullyComplete", {
9378
9787
  completedTextLen: this.text.completedText.length,
9379
9788
  accumulatedTextLen: this.text.accumulatedText.length
9380
9789
  });
@@ -9413,7 +9822,7 @@ var StreamingCardController = class StreamingCardController {
9413
9822
  footerMetrics
9414
9823
  });
9415
9824
  await this.closeStreamingAndUpdate(effectiveCardId, abortCardContent, "abortCard");
9416
- log$14.info("abortCard completed", { effectiveCardId });
9825
+ log$13.info("abortCard completed", { effectiveCardId });
9417
9826
  } else if (this.cardKit.cardMessageId) {
9418
9827
  const abortCard = buildCardContent("complete", {
9419
9828
  text: terminalContent.text,
@@ -9434,10 +9843,10 @@ var StreamingCardController = class StreamingCardController {
9434
9843
  card: abortCard,
9435
9844
  accountId: this.deps.accountId
9436
9845
  });
9437
- log$14.info("abortCard completed (IM fallback)", { messageId: this.cardKit.cardMessageId });
9846
+ log$13.info("abortCard completed (IM fallback)", { messageId: this.cardKit.cardMessageId });
9438
9847
  }
9439
9848
  } catch (err) {
9440
- log$14.warn("abortCard failed", { error: String(err) });
9849
+ log$13.warn("abortCard failed", { error: String(err) });
9441
9850
  } finally {
9442
9851
  clearToolUseTraceRun(this.deps.sessionKey);
9443
9852
  }
@@ -9452,10 +9861,10 @@ var StreamingCardController = class StreamingCardController {
9452
9861
  async forceCloseStreaming() {
9453
9862
  const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
9454
9863
  if (!effectiveCardId && !this.cardKit.cardMessageId) {
9455
- log$14.warn("forceCloseStreaming: no card to close");
9864
+ log$13.warn("forceCloseStreaming: no card to close");
9456
9865
  return;
9457
9866
  }
9458
- log$14.info("forceCloseStreaming: 强制终态化卡片", {
9867
+ log$13.info("forceCloseStreaming: 强制终态化卡片", {
9459
9868
  cardId: effectiveCardId,
9460
9869
  messageId: this.cardKit.cardMessageId,
9461
9870
  phase: this.phase
@@ -9505,10 +9914,10 @@ var StreamingCardController = class StreamingCardController {
9505
9914
  card: completeCard,
9506
9915
  accountId: this.deps.accountId
9507
9916
  });
9508
- log$14.info("forceCloseStreaming: 卡片已强制终态化");
9917
+ log$13.info("forceCloseStreaming: 卡片已强制终态化");
9509
9918
  if (!this.isTerminalPhase) this.finalizeCard("forceCloseStreaming", "abort");
9510
9919
  } catch (err) {
9511
- log$14.error("forceCloseStreaming failed", { error: String(err) });
9920
+ log$13.error("forceCloseStreaming failed", { error: String(err) });
9512
9921
  } finally {
9513
9922
  unregisterActiveCard(this.deps.sessionKey);
9514
9923
  clearToolUseTraceRun(this.deps.sessionKey);
@@ -9532,7 +9941,7 @@ var StreamingCardController = class StreamingCardController {
9532
9941
  this.disposeShutdownHook = null;
9533
9942
  return;
9534
9943
  }
9535
- log$14.info("reusing placeholder card", {
9944
+ log$13.info("reusing placeholder card", {
9536
9945
  cardId: this.deps.placeholderCardId,
9537
9946
  messageId: this.deps.placeholderMessageId
9538
9947
  });
@@ -9556,7 +9965,7 @@ var StreamingCardController = class StreamingCardController {
9556
9965
  accountId: this.deps.accountId
9557
9966
  });
9558
9967
  if (this.isStaleCreate(epoch)) {
9559
- log$14.info("ensureCardCreated: stale epoch after createCardEntity, bailing out", {
9968
+ log$13.info("ensureCardCreated: stale epoch after createCardEntity, bailing out", {
9560
9969
  epoch,
9561
9970
  phase: this.phase
9562
9971
  });
@@ -9567,7 +9976,7 @@ var StreamingCardController = class StreamingCardController {
9567
9976
  this.cardKit.originalCardKitCardId = cId;
9568
9977
  this.cardKit.cardKitSequence = 1;
9569
9978
  this.disposeShutdownHook = registerShutdownHook(`streaming-card:${cId}`, () => this.abortCard());
9570
- log$14.info("created CardKit entity", {
9979
+ log$13.info("created CardKit entity", {
9571
9980
  cardId: cId,
9572
9981
  initialSequence: this.cardKit.cardKitSequence
9573
9982
  });
@@ -9580,7 +9989,7 @@ var StreamingCardController = class StreamingCardController {
9580
9989
  accountId: this.deps.accountId
9581
9990
  });
9582
9991
  if (this.isStaleCreate(epoch)) {
9583
- log$14.info("ensureCardCreated: stale epoch after sendCardByCardId, bailing out", {
9992
+ log$13.info("ensureCardCreated: stale epoch after sendCardByCardId, bailing out", {
9584
9993
  epoch,
9585
9994
  phase: this.phase
9586
9995
  });
@@ -9595,13 +10004,13 @@ var StreamingCardController = class StreamingCardController {
9595
10004
  this.disposeShutdownHook = null;
9596
10005
  return;
9597
10006
  }
9598
- log$14.info("sent CardKit card", { messageId: result.messageId });
10007
+ log$13.info("sent CardKit card", { messageId: result.messageId });
9599
10008
  } else throw new Error("card.create returned empty card_id");
9600
10009
  } catch (cardKitErr) {
9601
10010
  if (this.isStaleCreate(epoch)) return;
9602
10011
  if (this.guard.terminate("ensureCardCreated.cardkitFlow", cardKitErr)) return;
9603
10012
  const apiDetail = extractApiDetail(cardKitErr);
9604
- log$14.warn("CardKit flow failed, falling back to IM", { apiDetail });
10013
+ log$13.warn("CardKit flow failed, falling back to IM", { apiDetail });
9605
10014
  this.cardKit.cardKitCardId = null;
9606
10015
  this.cardKit.originalCardKitCardId = null;
9607
10016
  const fallbackCard = buildCardContent("streaming", { showToolUse: this.deps.toolUseDisplay.showToolUse });
@@ -9614,7 +10023,7 @@ var StreamingCardController = class StreamingCardController {
9614
10023
  accountId: this.deps.accountId
9615
10024
  });
9616
10025
  if (this.isStaleCreate(epoch)) {
9617
- log$14.info("ensureCardCreated: stale epoch after IM fallback send, bailing out", {
10026
+ log$13.info("ensureCardCreated: stale epoch after IM fallback send, bailing out", {
9618
10027
  epoch,
9619
10028
  phase: this.phase
9620
10029
  });
@@ -9623,12 +10032,12 @@ var StreamingCardController = class StreamingCardController {
9623
10032
  this.cardKit.cardMessageId = result.messageId;
9624
10033
  this.flush.setCardMessageReady(true);
9625
10034
  if (!this.transition("streaming", "ensureCardCreated.imFallback")) return;
9626
- log$14.info("sent fallback IM card", { messageId: result.messageId });
10035
+ log$13.info("sent fallback IM card", { messageId: result.messageId });
9627
10036
  }
9628
10037
  } catch (err) {
9629
10038
  if (this.isStaleCreate(epoch)) return;
9630
10039
  if (this.guard.terminate("ensureCardCreated.outer", err)) return;
9631
- log$14.warn("thinking card failed, falling back to static", { error: String(err) });
10040
+ log$13.warn("thinking card failed, falling back to static", { error: String(err) });
9632
10041
  this.transition("creation_failed", "ensureCardCreated.outer", "creation_failed");
9633
10042
  }
9634
10043
  })();
@@ -9637,10 +10046,10 @@ var StreamingCardController = class StreamingCardController {
9637
10046
  async performFlush() {
9638
10047
  if (!this.cardKit.cardMessageId || this.isTerminalPhase) return;
9639
10048
  if (!this.cardKit.cardKitCardId && this.cardKit.originalCardKitCardId) {
9640
- log$14.debug("performFlush: skipping (CardKit streaming disabled, awaiting final update)");
10049
+ log$13.debug("performFlush: skipping (CardKit streaming disabled, awaiting final update)");
9641
10050
  return;
9642
10051
  }
9643
- log$14.debug("flushCardUpdate: enter", {
10052
+ log$13.debug("flushCardUpdate: enter", {
9644
10053
  seq: this.cardKit.cardKitSequence,
9645
10054
  isCardKit: !!this.cardKit.cardKitCardId
9646
10055
  });
@@ -9662,7 +10071,7 @@ var StreamingCardController = class StreamingCardController {
9662
10071
  reasoningText: this.reasoning.accumulatedReasoningText || void 0
9663
10072
  });
9664
10073
  this.cardKit.cardKitSequence += 1;
9665
- log$14.debug("flushCardUpdate: full card update (dirty)", {
10074
+ log$13.debug("flushCardUpdate: full card update (dirty)", {
9666
10075
  seq: this.cardKit.cardKitSequence,
9667
10076
  stepCount: display?.stepCount,
9668
10077
  hasReasoning: !!this.reasoning.accumulatedReasoningText
@@ -9678,7 +10087,7 @@ var StreamingCardController = class StreamingCardController {
9678
10087
  } else if (resolvedText !== this.text.lastFlushedText) {
9679
10088
  const prevSeq = this.cardKit.cardKitSequence;
9680
10089
  this.cardKit.cardKitSequence += 1;
9681
- log$14.debug("flushCardUpdate: answer seq bump", {
10090
+ log$13.debug("flushCardUpdate: answer seq bump", {
9682
10091
  seqBefore: prevSeq,
9683
10092
  seqAfter: this.cardKit.cardKitSequence
9684
10093
  });
@@ -9693,7 +10102,7 @@ var StreamingCardController = class StreamingCardController {
9693
10102
  this.text.lastFlushedText = resolvedText;
9694
10103
  }
9695
10104
  } else {
9696
- log$14.debug("flushCardUpdate: IM patch fallback");
10105
+ log$13.debug("flushCardUpdate: IM patch fallback");
9697
10106
  const flushDisplay = this.computeToolUseDisplay();
9698
10107
  const card = buildCardContent("streaming", {
9699
10108
  text: this.reasoning.isReasoningPhase ? "" : resolvedText,
@@ -9713,31 +10122,31 @@ var StreamingCardController = class StreamingCardController {
9713
10122
  if (this.guard.terminate("flushCardUpdate", err)) return;
9714
10123
  const apiCode = extractLarkApiCode(err);
9715
10124
  if (isCardRateLimitError(err)) {
9716
- log$14.info("flushCardUpdate: rate limited (230020), skipping", { seq: this.cardKit.cardKitSequence });
10125
+ log$13.info("flushCardUpdate: rate limited (230020), skipping", { seq: this.cardKit.cardKitSequence });
9717
10126
  return;
9718
10127
  }
9719
10128
  if (isCardTableLimitError(err)) {
9720
- log$14.warn("flushCardUpdate: card table limit exceeded (230099/11310), disabling CardKit streaming", { seq: this.cardKit.cardKitSequence });
10129
+ log$13.warn("flushCardUpdate: card table limit exceeded (230099/11310), disabling CardKit streaming", { seq: this.cardKit.cardKitSequence });
9721
10130
  this.cardKit.cardKitCardId = null;
9722
10131
  return;
9723
10132
  }
9724
10133
  if (apiCode === 300317 && this.cardKit.cardKitCardId) {
9725
10134
  const oldSeq = this.cardKit.cardKitSequence;
9726
10135
  this.cardKit.cardKitSequence += 100;
9727
- log$14.warn("flushCardUpdate: sequence conflict (300317), jumping sequence", {
10136
+ log$13.warn("flushCardUpdate: sequence conflict (300317), jumping sequence", {
9728
10137
  oldSeq,
9729
10138
  newSeq: this.cardKit.cardKitSequence
9730
10139
  });
9731
10140
  return;
9732
10141
  }
9733
10142
  const apiDetail = extractApiDetail(err);
9734
- log$14.error("card stream update failed", {
10143
+ log$13.error("card stream update failed", {
9735
10144
  apiCode,
9736
10145
  seq: this.cardKit.cardKitSequence,
9737
10146
  apiDetail
9738
10147
  });
9739
10148
  if (this.cardKit.cardKitCardId) {
9740
- log$14.warn("disabling CardKit streaming, falling back to im.message.patch");
10149
+ log$13.warn("disabling CardKit streaming, falling back to im.message.patch");
9741
10150
  this.cardKit.cardKitCardId = null;
9742
10151
  }
9743
10152
  }
@@ -9762,7 +10171,7 @@ var StreamingCardController = class StreamingCardController {
9762
10171
  if (!this.cardKit.cardKitCardId || this.isTerminalPhase) return;
9763
10172
  try {
9764
10173
  const display = this.computeToolUseDisplay();
9765
- log$14.info("updateToolUseStatus", {
10174
+ log$13.info("updateToolUseStatus", {
9766
10175
  hasDisplay: !!display,
9767
10176
  stepCount: display?.stepCount,
9768
10177
  steps: display?.steps?.map((s) => `${s.title}:${s.status}`).join(", ")
@@ -9784,7 +10193,7 @@ var StreamingCardController = class StreamingCardController {
9784
10193
  accountId: this.deps.accountId
9785
10194
  });
9786
10195
  } catch (err) {
9787
- log$14.debug("updateToolUseStatus failed", { error: String(err) });
10196
+ log$13.debug("updateToolUseStatus failed", { error: String(err) });
9788
10197
  }
9789
10198
  }
9790
10199
  finalizeCard(source, reason) {
@@ -9796,7 +10205,7 @@ var StreamingCardController = class StreamingCardController {
9796
10205
  async closeStreamingAndUpdate(cardId, card, label) {
9797
10206
  const seqBeforeClose = this.cardKit.cardKitSequence;
9798
10207
  this.cardKit.cardKitSequence += 1;
9799
- log$14.info(`${label}: closing streaming mode`, {
10208
+ log$13.info(`${label}: closing streaming mode`, {
9800
10209
  seqBefore: seqBeforeClose,
9801
10210
  seqAfter: this.cardKit.cardKitSequence
9802
10211
  });
@@ -9809,7 +10218,7 @@ var StreamingCardController = class StreamingCardController {
9809
10218
  });
9810
10219
  const seqBeforeUpdate = this.cardKit.cardKitSequence;
9811
10220
  this.cardKit.cardKitSequence += 1;
9812
- log$14.info(`${label}: updating card`, {
10221
+ log$13.info(`${label}: updating card`, {
9813
10222
  seqBefore: seqBeforeUpdate,
9814
10223
  seqAfter: this.cardKit.cardKitSequence
9815
10224
  });
@@ -9843,7 +10252,7 @@ function extractApiDetail(err) {
9843
10252
  }
9844
10253
  //#endregion
9845
10254
  //#region src/messaging/format-for-cc.ts
9846
- const log$13 = larkLogger("messaging/format-for-cc");
10255
+ const log$12 = larkLogger("messaging/format-for-cc");
9847
10256
  function safeParseJSON(raw) {
9848
10257
  try {
9849
10258
  return JSON.parse(raw);
@@ -9899,7 +10308,7 @@ function extractPostText(rawContent) {
9899
10308
  */
9900
10309
  function formatContentByType(ctx) {
9901
10310
  const { contentType, content, resources } = ctx;
9902
- log$13.debug("formatContentByType 开始格式化", {
10311
+ log$12.debug("formatContentByType 开始格式化", {
9903
10312
  contentType,
9904
10313
  contentLength: content?.length ?? 0,
9905
10314
  resourceCount: resources?.length ?? 0
@@ -9941,7 +10350,7 @@ function formatContentByType(ctx) {
9941
10350
  case "todo":
9942
10351
  case "vote": return content;
9943
10352
  default:
9944
- log$13.warn("遇到不支持的消息类型", { contentType });
10353
+ log$12.warn("遇到不支持的消息类型", { contentType });
9945
10354
  return `[不支持的消息类型: ${contentType}]`;
9946
10355
  }
9947
10356
  }
@@ -9984,7 +10393,7 @@ function formatMessageTime(createTime) {
9984
10393
  * @returns 格式化后的纯文本字符串
9985
10394
  */
9986
10395
  function formatForCC(ctx, isGroup) {
9987
- log$13.info("formatForCC 开始处理", {
10396
+ log$12.info("formatForCC 开始处理", {
9988
10397
  messageId: ctx.messageId,
9989
10398
  contentType: ctx.contentType,
9990
10399
  chatType: ctx.chatType,
@@ -10000,7 +10409,7 @@ function formatForCC(ctx, isGroup) {
10000
10409
  if (isGroup && ctx.senderName) parts.push(`[${ctx.senderName}]`);
10001
10410
  if (ctx.threadId) parts.push("[话题回复]");
10002
10411
  const result = (parts.length > 0 ? parts.join(" ") + " " : "") + formattedContent;
10003
- log$13.info("formatForCC 完成", {
10412
+ log$12.info("formatForCC 完成", {
10004
10413
  messageId: ctx.messageId,
10005
10414
  resultLength: result.length,
10006
10415
  hasTime: !!timeStr,
@@ -10010,7 +10419,7 @@ function formatForCC(ctx, isGroup) {
10010
10419
  }
10011
10420
  //#endregion
10012
10421
  //#region src/messaging/inbound/dispatch-cc.ts
10013
- const log$12 = larkLogger("inbound/dispatch-cc");
10422
+ const log$11 = larkLogger("inbound/dispatch-cc");
10014
10423
  async function dispatchToCC(params) {
10015
10424
  const { ctx, account, sessionRouter, processManager } = params;
10016
10425
  const chatId = ctx.chatId;
@@ -10020,7 +10429,7 @@ async function dispatchToCC(params) {
10020
10429
  const ccModel = params.model;
10021
10430
  const maxTurns = params.maxTurns;
10022
10431
  const maxBudgetUsd = params.maxBudgetUsd;
10023
- log$12.info("dispatchToCC 开始", {
10432
+ log$11.info("dispatchToCC 开始", {
10024
10433
  msgId,
10025
10434
  chatId,
10026
10435
  chatType,
@@ -10033,12 +10442,12 @@ async function dispatchToCC(params) {
10033
10442
  threadId,
10034
10443
  userId: ctx.senderId
10035
10444
  });
10036
- log$12.info("会话路由解析完成", {
10445
+ log$11.info("会话路由解析完成", {
10037
10446
  sessionId: route.sessionId,
10038
10447
  cwd: route.cwd,
10039
10448
  type: route.type
10040
10449
  });
10041
- if (params.userContext) log$12.info("用户上下文已注入", {
10450
+ if (params.userContext) log$11.info("用户上下文已注入", {
10042
10451
  userId: params.userContext.userId,
10043
10452
  isTenantIdentity: params.userContext.isTenantIdentity,
10044
10453
  credentialDir: params.userContext.credentialDir
@@ -10048,7 +10457,9 @@ async function dispatchToCC(params) {
10048
10457
  const replyInThread = !!threadId;
10049
10458
  const replyToMessageId = threadId ? void 0 : ctx.messageId;
10050
10459
  let textPrompt = formatForCC(ctx, isGroup);
10051
- if (isGroup) textPrompt = `[有人@你] ${textPrompt}`;
10460
+ if (isGroup && replyInThread) textPrompt = `[群聊话题] ${textPrompt}`;
10461
+ else if (isGroup) textPrompt = `[群聊] ${textPrompt}`;
10462
+ else textPrompt = `[私聊] ${textPrompt}`;
10052
10463
  const imageResources = ctx.resources.filter((r) => r.type === "image");
10053
10464
  let prompt = textPrompt;
10054
10465
  if (imageResources.length > 0) {
@@ -10075,7 +10486,7 @@ async function dispatchToCC(params) {
10075
10486
  for await (const chunk of readable) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
10076
10487
  buffer = Buffer.concat(chunks);
10077
10488
  } else {
10078
- log$12.warn("图片资源下载返回未知格式,跳过", {
10489
+ log$11.warn("图片资源下载返回未知格式,跳过", {
10079
10490
  fileKey: imgRes.fileKey,
10080
10491
  responseType: typeof response
10081
10492
  });
@@ -10083,7 +10494,7 @@ async function dispatchToCC(params) {
10083
10494
  }
10084
10495
  if (resp.headers?.["content-type"]) mediaType = resp.headers["content-type"];
10085
10496
  } else {
10086
- log$12.warn("图片资源下载返回空,跳过", { fileKey: imgRes.fileKey });
10497
+ log$11.warn("图片资源下载返回空,跳过", { fileKey: imgRes.fileKey });
10087
10498
  continue;
10088
10499
  }
10089
10500
  const imgFileName = `${imgRes.fileKey}.png`;
@@ -10093,13 +10504,13 @@ async function dispatchToCC(params) {
10093
10504
  const { writeFile, mkdir } = await import("fs/promises");
10094
10505
  await mkdir(filesDir, { recursive: true });
10095
10506
  await writeFile(imgFilePath, buffer);
10096
- log$12.info("图片已保存到工作目录", {
10507
+ log$11.info("图片已保存到工作目录", {
10097
10508
  fileKey: imgRes.fileKey,
10098
10509
  path: imgFilePath,
10099
10510
  sizeBytes: buffer.length
10100
10511
  });
10101
10512
  } catch (saveErr) {
10102
- log$12.warn("图片保存到工作目录失败", {
10513
+ log$11.warn("图片保存到工作目录失败", {
10103
10514
  fileKey: imgRes.fileKey,
10104
10515
  path: imgFilePath,
10105
10516
  error: saveErr instanceof Error ? saveErr.message : String(saveErr)
@@ -10114,13 +10525,13 @@ async function dispatchToCC(params) {
10114
10525
  data: base64Data
10115
10526
  }
10116
10527
  });
10117
- log$12.info("图片资源下载成功", {
10528
+ log$11.info("图片资源下载成功", {
10118
10529
  fileKey: imgRes.fileKey,
10119
10530
  mediaType,
10120
10531
  sizeBytes: buffer.length
10121
10532
  });
10122
10533
  } catch (err) {
10123
- log$12.warn("图片资源下载失败,跳过", {
10534
+ log$11.warn("图片资源下载失败,跳过", {
10124
10535
  fileKey: imgRes.fileKey,
10125
10536
  error: err instanceof Error ? err.message : String(err)
10126
10537
  });
@@ -10130,7 +10541,7 @@ async function dispatchToCC(params) {
10130
10541
  type: "text",
10131
10542
  text: textPrompt
10132
10543
  }, ...imageBlocks];
10133
- log$12.info("已构建多模态 prompt", {
10544
+ log$11.info("已构建多模态 prompt", {
10134
10545
  textLength: textPrompt.length,
10135
10546
  imageCount: imageBlocks.length
10136
10547
  });
@@ -10138,12 +10549,12 @@ async function dispatchToCC(params) {
10138
10549
  const textWithoutImageRefs = textPrompt.replace(/!\[image\]\([^)]*\)/g, "").replace(/\[图片\]\s*/g, "").trim();
10139
10550
  if (textWithoutImageRefs.length > 0) {
10140
10551
  prompt = textPrompt.replace(/!\[image\]\([^)]*\)/g, "[图片加载失败]");
10141
- log$12.info("图片下载失败但保留文字内容继续调度", {
10552
+ log$11.info("图片下载失败但保留文字内容继续调度", {
10142
10553
  messageId: ctx.messageId,
10143
10554
  textLength: textWithoutImageRefs.length
10144
10555
  });
10145
10556
  } else {
10146
- log$12.warn("纯图片消息且图片全部下载失败,中止调度", {
10557
+ log$11.warn("纯图片消息且图片全部下载失败,中止调度", {
10147
10558
  messageId: ctx.messageId,
10148
10559
  expectedImages: imageResources.length
10149
10560
  });
@@ -10165,7 +10576,7 @@ async function dispatchToCC(params) {
10165
10576
  await fsMkdir(filesDir, { recursive: true });
10166
10577
  let updatedTextPrompt = typeof prompt === "string" ? prompt : prompt[0].text;
10167
10578
  for (const res of attachmentResources) try {
10168
- log$12.info("开始下载非图片附件", {
10579
+ log$11.info("开始下载非图片附件", {
10169
10580
  type: res.type,
10170
10581
  fileKey: res.fileKey,
10171
10582
  fileName: res.fileName,
@@ -10179,7 +10590,7 @@ async function dispatchToCC(params) {
10179
10590
  },
10180
10591
  params: { type: "file" }
10181
10592
  });
10182
- log$12.info("非图片附件 API 响应已收到", {
10593
+ log$11.info("非图片附件 API 响应已收到", {
10183
10594
  type: res.type,
10184
10595
  fileKey: res.fileKey,
10185
10596
  responseType: typeof response,
@@ -10198,7 +10609,7 @@ async function dispatchToCC(params) {
10198
10609
  for await (const chunk of readable) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
10199
10610
  buffer = Buffer.concat(chunks);
10200
10611
  } else {
10201
- log$12.warn("非图片附件下载返回未知格式,跳过", {
10612
+ log$11.warn("非图片附件下载返回未知格式,跳过", {
10202
10613
  type: res.type,
10203
10614
  fileKey: res.fileKey,
10204
10615
  responseType: typeof response
@@ -10206,7 +10617,7 @@ async function dispatchToCC(params) {
10206
10617
  continue;
10207
10618
  }
10208
10619
  } else {
10209
- log$12.warn("非图片附件下载返回空,跳过", {
10620
+ log$11.warn("非图片附件下载返回空,跳过", {
10210
10621
  type: res.type,
10211
10622
  fileKey: res.fileKey
10212
10623
  });
@@ -10229,7 +10640,7 @@ async function dispatchToCC(params) {
10229
10640
  }
10230
10641
  const filePath = path.join(filesDir, savedFileName);
10231
10642
  await fsWriteFile(filePath, buffer);
10232
- log$12.info("非图片附件已保存到工作目录", {
10643
+ log$11.info("非图片附件已保存到工作目录", {
10233
10644
  type: res.type,
10234
10645
  fileKey: res.fileKey,
10235
10646
  savedFileName,
@@ -10263,7 +10674,7 @@ async function dispatchToCC(params) {
10263
10674
  }
10264
10675
  }
10265
10676
  } catch (err) {
10266
- log$12.warn("非图片附件下载失败,替换为降级描述", {
10677
+ log$11.warn("非图片附件下载失败,替换为降级描述", {
10267
10678
  type: res.type,
10268
10679
  fileKey: res.fileKey,
10269
10680
  error: err instanceof Error ? err.message : String(err)
@@ -10282,12 +10693,12 @@ async function dispatchToCC(params) {
10282
10693
  const textBlock = prompt.find((b) => b.type === "text");
10283
10694
  if (textBlock) textBlock.text = updatedTextPrompt;
10284
10695
  }
10285
- log$12.info("非图片附件处理完成", {
10696
+ log$11.info("非图片附件处理完成", {
10286
10697
  totalAttachments: attachmentResources.length,
10287
10698
  promptLength: typeof prompt === "string" ? prompt.length : prompt.find((b) => b.type === "text")?.text?.length
10288
10699
  });
10289
10700
  }
10290
- log$12.info("消息格式化完成", {
10701
+ log$11.info("消息格式化完成", {
10291
10702
  promptLength: typeof prompt === "string" ? prompt.length : prompt.length,
10292
10703
  isMultimodal: Array.isArray(prompt),
10293
10704
  isGroup
@@ -10319,13 +10730,13 @@ async function dispatchToCC(params) {
10319
10730
  };
10320
10731
  const cardController = new StreamingCardController(cardDeps);
10321
10732
  registerActiveCard(route.sessionId, cardController);
10322
- log$12.info("StreamingCardController 已创建", {
10733
+ log$11.info("StreamingCardController 已创建", {
10323
10734
  sessionId: route.sessionId,
10324
10735
  chatId,
10325
10736
  replyToMessageId: cardDeps.replyToMessageId
10326
10737
  });
10327
10738
  cardController.ensureCardCreated().catch((err) => {
10328
- log$12.warn("提前创建卡片失败(streaming 回调会重试)", { error: String(err) });
10739
+ log$11.warn("提前创建卡片失败(streaming 回调会重试)", { error: String(err) });
10329
10740
  });
10330
10741
  if (params.messageStore) {
10331
10742
  const userContent = typeof prompt === "string" ? prompt : textPrompt;
@@ -10336,7 +10747,7 @@ async function dispatchToCC(params) {
10336
10747
  channel: "feishu-bot",
10337
10748
  title: userContent.slice(0, 30) + (userContent.length > 30 ? "..." : "")
10338
10749
  }).catch((err) => {
10339
- log$12.warn("创建飞书端会话记录失败", { error: String(err) });
10750
+ log$11.warn("创建飞书端会话记录失败", { error: String(err) });
10340
10751
  });
10341
10752
  params.messageStore.appendMessage({
10342
10753
  sessionId: storeSessionId,
@@ -10345,18 +10756,18 @@ async function dispatchToCC(params) {
10345
10756
  channel: "feishu-bot",
10346
10757
  metadata: { feishuMessageId: msgId }
10347
10758
  }).catch((err) => {
10348
- log$12.warn("记录飞书用户消息到 store 失败", { error: String(err) });
10759
+ log$11.warn("记录飞书用户消息到 store 失败", { error: String(err) });
10349
10760
  });
10350
10761
  }
10351
10762
  const bridge = new CCStreamBridge(cardController, {
10352
10763
  autoCompleteOnTurnEnd: true,
10353
10764
  sessionKey: route.sessionId
10354
10765
  });
10355
- log$12.info("CCStreamBridge 已创建", { sessionId: route.sessionId });
10766
+ log$11.info("CCStreamBridge 已创建", { sessionId: route.sessionId });
10356
10767
  let feishuAccumText = "";
10357
10768
  const callbacks = {
10358
10769
  onTextDelta: (text) => {
10359
- log$12.debug("CC onTextDelta", {
10770
+ log$11.debug("CC onTextDelta", {
10360
10771
  sessionId: route.sessionId,
10361
10772
  deltaLen: text.length
10362
10773
  });
@@ -10364,28 +10775,28 @@ async function dispatchToCC(params) {
10364
10775
  feishuAccumText += text;
10365
10776
  },
10366
10777
  onThinkingDelta: (text) => {
10367
- log$12.debug("CC onThinkingDelta", {
10778
+ log$11.debug("CC onThinkingDelta", {
10368
10779
  sessionId: route.sessionId,
10369
10780
  deltaLen: text.length
10370
10781
  });
10371
10782
  bridge.onThinkingDelta(text);
10372
10783
  },
10373
10784
  onToolUseStart: (toolName, toolInput) => {
10374
- log$12.info("CC onToolUseStart", {
10785
+ log$11.info("CC onToolUseStart", {
10375
10786
  sessionId: route.sessionId,
10376
10787
  toolName
10377
10788
  });
10378
10789
  bridge.onToolUseStart(toolName, toolInput);
10379
10790
  },
10380
10791
  onToolResult: (toolUseId) => {
10381
- log$12.info("CC onToolResult", {
10792
+ log$11.info("CC onToolResult", {
10382
10793
  sessionId: route.sessionId,
10383
10794
  toolUseId
10384
10795
  });
10385
10796
  bridge.onToolResult(toolUseId);
10386
10797
  },
10387
10798
  onToolProgress: (toolName, elapsedSeconds) => {
10388
- log$12.debug("CC onToolProgress", {
10799
+ log$11.debug("CC onToolProgress", {
10389
10800
  sessionId: route.sessionId,
10390
10801
  toolName,
10391
10802
  elapsedSeconds
@@ -10393,14 +10804,14 @@ async function dispatchToCC(params) {
10393
10804
  bridge.onToolProgress(toolName, elapsedSeconds);
10394
10805
  },
10395
10806
  onTurnEnd: (stopReason) => {
10396
- log$12.info("CC onTurnEnd", {
10807
+ log$11.info("CC onTurnEnd", {
10397
10808
  sessionId: route.sessionId,
10398
10809
  stopReason
10399
10810
  });
10400
10811
  bridge.onTurnEnd(stopReason);
10401
10812
  },
10402
10813
  onResult: (result) => {
10403
- log$12.info("CC onResult", {
10814
+ log$11.info("CC onResult", {
10404
10815
  sessionId: route.sessionId,
10405
10816
  subtype: result.subtype,
10406
10817
  isError: result.isError,
@@ -10420,18 +10831,28 @@ async function dispatchToCC(params) {
10420
10831
  numTurns: result.numTurns
10421
10832
  }
10422
10833
  }).catch((err) => {
10423
- log$12.warn("记录飞书 assistant 消息到 store 失败", { error: String(err) });
10834
+ log$11.warn("记录飞书 assistant 消息到 store 失败", { error: String(err) });
10835
+ });
10836
+ if (feishuAccumText) handleInteractiveCardTriggers({
10837
+ text: feishuAccumText,
10838
+ chatId,
10839
+ sessionId: route.sessionId,
10840
+ appId: account.configured ? account.appId : "",
10841
+ brand: account.brand,
10842
+ account,
10843
+ replyInThread,
10844
+ threadId: ctx.threadId
10424
10845
  });
10425
10846
  },
10426
10847
  onError: (error) => {
10427
- log$12.error("CC 进程错误", {
10848
+ log$11.error("CC 进程错误", {
10428
10849
  sessionId: route.sessionId,
10429
10850
  error: error.message
10430
10851
  });
10431
10852
  cardController.onError(error, { kind: "cc-process" });
10432
10853
  }
10433
10854
  };
10434
- log$12.info("开始执行 CC 进程", {
10855
+ log$11.info("开始执行 CC 进程", {
10435
10856
  sessionId: route.sessionId,
10436
10857
  cwd: route.cwd,
10437
10858
  promptLength: prompt.length,
@@ -10448,10 +10869,10 @@ async function dispatchToCC(params) {
10448
10869
  maxBudgetUsd,
10449
10870
  userContext: params.userContext
10450
10871
  }, callbacks);
10451
- log$12.info("CC 进程 executePrompt 调用完成", { sessionId: route.sessionId });
10872
+ log$11.info("CC 进程 executePrompt 调用完成", { sessionId: route.sessionId });
10452
10873
  } catch (err) {
10453
10874
  const errorMessage = err instanceof Error ? err.message : String(err);
10454
- log$12.error("CC 进程 executePrompt 调用异常", {
10875
+ log$11.error("CC 进程 executePrompt 调用异常", {
10455
10876
  sessionId: route.sessionId,
10456
10877
  error: errorMessage
10457
10878
  });
@@ -10473,7 +10894,7 @@ async function dispatchToCC(params) {
10473
10894
  */
10474
10895
  async function dispatchTeammateEval(params) {
10475
10896
  const { chatId, prompt, account, sessionRouter, processManager } = params;
10476
- log$12.info("dispatchTeammateEval 开始", {
10897
+ log$11.info("dispatchTeammateEval 开始", {
10477
10898
  chatId,
10478
10899
  promptLength: prompt.length
10479
10900
  });
@@ -10486,7 +10907,7 @@ async function dispatchTeammateEval(params) {
10486
10907
  userName: void 0
10487
10908
  });
10488
10909
  if (processManager.isSessionBusy?.(route.sessionId)) {
10489
- log$12.info("dispatchTeammateEval 跳过:主 session 正在执行", {
10910
+ log$11.info("dispatchTeammateEval 跳过:主 session 正在执行", {
10490
10911
  chatId,
10491
10912
  sessionId: route.sessionId
10492
10913
  });
@@ -10519,7 +10940,7 @@ async function dispatchTeammateEval(params) {
10519
10940
  const confirmReply = () => {
10520
10941
  if (confirmed) return;
10521
10942
  confirmed = true;
10522
- log$12.info("teammate 确认回复,创建流式卡片", {
10943
+ log$11.info("teammate 确认回复,创建流式卡片", {
10523
10944
  chatId,
10524
10945
  thinkingLen: thinkingBuffer.length,
10525
10946
  textLen: fullTextBuffer.length
@@ -10576,7 +10997,7 @@ async function dispatchTeammateEval(params) {
10576
10997
  fullTextBuffer += text;
10577
10998
  if (fullTextBuffer.trimStart().startsWith(NO_REPLY_TOKEN)) {
10578
10999
  silenced = true;
10579
- log$12.info("teammate 前缀检测: 检测到 NO_REPLY 首输出,立即静默", {
11000
+ log$11.info("teammate 前缀检测: 检测到 NO_REPLY 首输出,立即静默", {
10580
11001
  chatId,
10581
11002
  bufferLen: fullTextBuffer.length
10582
11003
  });
@@ -10621,7 +11042,7 @@ async function dispatchTeammateEval(params) {
10621
11042
  onTurnEnd: (stopReason) => {
10622
11043
  finalStopReason = stopReason;
10623
11044
  if (stopReason === "tool_use") {
10624
- log$12.debug("teammate turnEnd: tool_use, 跳过最终判断", { chatId });
11045
+ log$11.debug("teammate turnEnd: tool_use, 跳过最终判断", { chatId });
10625
11046
  if (confirmed && bridge) bridge.onTurnEnd(stopReason);
10626
11047
  return;
10627
11048
  }
@@ -10629,7 +11050,7 @@ async function dispatchTeammateEval(params) {
10629
11050
  const trimmed = fullTextBuffer.trim();
10630
11051
  if (trimmed === NO_REPLY_TOKEN || trimmed === CC_INTERNAL_PLACEHOLDER || trimmed.endsWith(NO_REPLY_TOKEN) || trimmed === "") {
10631
11052
  silenced = true;
10632
- log$12.info("teammate turnEnd 最终判断: 静默", {
11053
+ log$11.info("teammate turnEnd 最终判断: 静默", {
10633
11054
  chatId,
10634
11055
  trimmed: trimmed.slice(0, 60)
10635
11056
  });
@@ -10644,7 +11065,7 @@ async function dispatchTeammateEval(params) {
10644
11065
  resolve();
10645
11066
  },
10646
11067
  onError: (error) => {
10647
- log$12.error("teammate 评估 CC 进程错误", {
11068
+ log$11.error("teammate 评估 CC 进程错误", {
10648
11069
  chatId,
10649
11070
  error: error.message
10650
11071
  });
@@ -10657,14 +11078,14 @@ async function dispatchTeammateEval(params) {
10657
11078
  prompt,
10658
11079
  model: params.model
10659
11080
  }, callbacks).catch((err) => {
10660
- log$12.error("teammate executePrompt 异常", {
11081
+ log$11.error("teammate executePrompt 异常", {
10661
11082
  chatId,
10662
11083
  error: err instanceof Error ? err.message : String(err)
10663
11084
  });
10664
11085
  resolve();
10665
11086
  });
10666
11087
  });
10667
- log$12.info("dispatchTeammateEval 完成", {
11088
+ log$11.info("dispatchTeammateEval 完成", {
10668
11089
  chatId,
10669
11090
  confirmed,
10670
11091
  silenced,
@@ -10675,13 +11096,83 @@ async function dispatchTeammateEval(params) {
10675
11096
  return { replied: confirmed };
10676
11097
  } catch (err) {
10677
11098
  const errorMessage = err instanceof Error ? err.message : String(err);
10678
- log$12.error("dispatchTeammateEval 异常", {
11099
+ log$11.error("dispatchTeammateEval 异常", {
10679
11100
  chatId,
10680
11101
  error: errorMessage
10681
11102
  });
10682
11103
  return { replied: false };
10683
11104
  }
10684
11105
  }
11106
+ /**
11107
+ * 检测 CC 回复文本中的交互标记,并发送对应的独立卡片。
11108
+ *
11109
+ * 标记格式:
11110
+ * - [PERMISSION_REQUEST] ```json { "scopes": ["scope1", "scope2"] } ```
11111
+ * - [ASK_USER] ```json { "question": "...", "phases": [...] } ```
11112
+ */
11113
+ async function handleInteractiveCardTriggers(params) {
11114
+ const { text, chatId, sessionId, appId, brand, account, replyInThread, threadId } = params;
11115
+ try {
11116
+ const authReq = detectAuthRequest(text);
11117
+ if (authReq && appId) {
11118
+ log$11.info("检测到 CC 输出中的权限申请标记", {
11119
+ sessionId,
11120
+ scopes: authReq.scopes
11121
+ });
11122
+ const { card } = buildAuthCard({
11123
+ chatId,
11124
+ sessionId,
11125
+ appId,
11126
+ scopes: authReq.scopes,
11127
+ brand
11128
+ });
11129
+ await LarkClient.fromAccount(account).sdk.im.message.create({
11130
+ params: { receive_id_type: "chat_id" },
11131
+ data: {
11132
+ receive_id: chatId,
11133
+ msg_type: "interactive",
11134
+ content: JSON.stringify(card),
11135
+ ...replyInThread && threadId ? { reply_in_thread: true } : {}
11136
+ }
11137
+ });
11138
+ log$11.info("权限申请卡片已发送", {
11139
+ chatId,
11140
+ sessionId
11141
+ });
11142
+ }
11143
+ const askReq = detectAskRequest(text);
11144
+ if (askReq) {
11145
+ log$11.info("检测到 CC 输出中的提问标记", {
11146
+ sessionId,
11147
+ question: askReq.question.slice(0, 50)
11148
+ });
11149
+ const { card } = buildAskCard({
11150
+ chatId,
11151
+ sessionId,
11152
+ question: askReq.question,
11153
+ phases: askReq.phases
11154
+ });
11155
+ await LarkClient.fromAccount(account).sdk.im.message.create({
11156
+ params: { receive_id_type: "chat_id" },
11157
+ data: {
11158
+ receive_id: chatId,
11159
+ msg_type: "interactive",
11160
+ content: JSON.stringify(card),
11161
+ ...replyInThread && threadId ? { reply_in_thread: true } : {}
11162
+ }
11163
+ });
11164
+ log$11.info("提问卡片已发送", {
11165
+ chatId,
11166
+ sessionId
11167
+ });
11168
+ }
11169
+ } catch (err) {
11170
+ log$11.error("交互式卡片后处理失败", {
11171
+ sessionId,
11172
+ error: err instanceof Error ? err.message : String(err)
11173
+ });
11174
+ }
11175
+ }
10685
11176
  //#endregion
10686
11177
  //#region src/messaging/inbound/permission.ts
10687
11178
  /**
@@ -12187,7 +12678,7 @@ const convertLocation = (raw) => {
12187
12678
  * injected via callbacks in `ConvertContext`. Callers are responsible
12188
12679
  * for creating the appropriate callbacks (UAT / TAT / event push).
12189
12680
  */
12190
- const log$11 = larkLogger("converters/merge-forward");
12681
+ const log$10 = larkLogger("converters/merge-forward");
12191
12682
  /**
12192
12683
  * Recursively expand a merge_forward message.
12193
12684
  *
@@ -12201,10 +12692,16 @@ const log$11 = larkLogger("converters/merge-forward");
12201
12692
  */
12202
12693
  const convertMergeForward = async (_raw, ctx) => {
12203
12694
  const { accountId, messageId, resolveUserName, batchResolveNames, fetchSubMessages, convertMessageContent } = ctx;
12204
- if (!fetchSubMessages) return {
12205
- content: "<forwarded_messages/>",
12206
- resources: []
12207
- };
12695
+ if (!fetchSubMessages) {
12696
+ log$10.warn("fetchSubMessages 回调未注入,无法展开合并转发消息", {
12697
+ messageId,
12698
+ accountId
12699
+ });
12700
+ return {
12701
+ content: "<forwarded_messages/>",
12702
+ resources: []
12703
+ };
12704
+ }
12208
12705
  return {
12209
12706
  content: await expand(accountId, messageId, resolveUserName, batchResolveNames, fetchSubMessages, convertMessageContent),
12210
12707
  resources: []
@@ -12214,20 +12711,32 @@ async function expand(accountId, messageId, resolveUserName, batchResolveNames,
12214
12711
  let items;
12215
12712
  try {
12216
12713
  items = await fetchSubMessages(messageId);
12714
+ log$10.info("fetchSubMessages 成功", {
12715
+ messageId,
12716
+ itemCount: items.length
12717
+ });
12217
12718
  } catch (error) {
12218
- log$11.error("fetch sub-messages failed", {
12719
+ log$10.error("fetch sub-messages failed", {
12219
12720
  messageId,
12220
12721
  error: error instanceof Error ? error.message : String(error)
12221
12722
  });
12222
12723
  return "<forwarded_messages/>";
12223
12724
  }
12224
- if (items.length === 0) return "<forwarded_messages/>";
12725
+ if (items.length === 0) {
12726
+ log$10.warn("fetchSubMessages 返回空 items", { messageId });
12727
+ return "<forwarded_messages/>";
12728
+ }
12225
12729
  const childrenMap = buildChildrenMap(items, messageId);
12730
+ log$10.info("buildChildrenMap 完成", {
12731
+ messageId,
12732
+ parentKeys: [...childrenMap.keys()],
12733
+ childCounts: [...childrenMap.entries()].map(([k, v]) => `${k}:${v.length}`)
12734
+ });
12226
12735
  const senderIds = collectSenderIds(items, messageId);
12227
12736
  if (senderIds.length > 0 && batchResolveNames) try {
12228
12737
  await batchResolveNames(senderIds);
12229
12738
  } catch (err) {
12230
- log$11.debug("batchResolveNames failed (best-effort)", { error: err instanceof Error ? err.message : String(err) });
12739
+ log$10.debug("batchResolveNames failed (best-effort)", { error: err instanceof Error ? err.message : String(err) });
12231
12740
  }
12232
12741
  return formatSubTree(messageId, childrenMap, accountId, resolveUserName, convertContent);
12233
12742
  }
@@ -12307,7 +12816,7 @@ async function formatSubTree(parentId, childrenMap, accountId, resolveUserName,
12307
12816
  const indented = indentLines(content, " ");
12308
12817
  parts.push(`[${timestamp}] ${displayName}:\n${indented}`);
12309
12818
  } catch (err) {
12310
- log$11.warn("failed to convert sub-message", {
12819
+ log$10.warn("failed to convert sub-message", {
12311
12820
  messageId: item.message_id,
12312
12821
  msgType: item.msg_type ?? "unknown",
12313
12822
  error: err instanceof Error ? err.message : String(err)
@@ -12521,7 +13030,7 @@ async function convertMessageContent(raw, messageType, ctx) {
12521
13030
  }
12522
13031
  //#endregion
12523
13032
  //#region src/messaging/inbound/parse-io.ts
12524
- const log$10 = larkLogger("inbound/parse-io");
13033
+ const log$9 = larkLogger("inbound/parse-io");
12525
13034
  /**
12526
13035
  * 对 interactive 消息,通过 TAT 调用 API 获取完整 v2 卡片内容。
12527
13036
  * 事件推送的 content 可能不包含 json_card,API 调用可返回完整的 raw_card_content。
@@ -12541,7 +13050,7 @@ async function fetchCardContent(messageId, larkClient) {
12541
13050
  }
12542
13051
  }))?.data?.items?.[0]?.body?.content ?? void 0;
12543
13052
  } catch (err) {
12544
- log$10.warn(`fetchCardContent failed for ${messageId}: ${err instanceof Error ? err.message : String(err)}`);
13053
+ log$9.warn(`fetchCardContent failed for ${messageId}: ${err instanceof Error ? err.message : String(err)}`);
12545
13054
  return;
12546
13055
  }
12547
13056
  }
@@ -12556,6 +13065,10 @@ async function fetchCardContent(messageId, larkClient) {
12556
13065
  */
12557
13066
  function createFetchSubMessages(larkClient) {
12558
13067
  return async (msgId) => {
13068
+ log$9.info("fetchSubMessages 请求", {
13069
+ msgId,
13070
+ url: `/open-apis/im/v1/messages/${msgId}`
13071
+ });
12559
13072
  const response = await larkClient.sdk.request({
12560
13073
  method: "GET",
12561
13074
  url: `/open-apis/im/v1/messages/${msgId}`,
@@ -12564,6 +13077,12 @@ function createFetchSubMessages(larkClient) {
12564
13077
  card_msg_content_type: "raw_card_content"
12565
13078
  }
12566
13079
  });
13080
+ log$9.info("fetchSubMessages 响应", {
13081
+ msgId,
13082
+ code: response?.code,
13083
+ msg: response?.msg,
13084
+ itemCount: response?.data?.items?.length ?? 0
13085
+ });
12567
13086
  if (response?.code !== 0) throw new Error(`API error: code=${response?.code} msg=${response?.msg}`);
12568
13087
  return response?.data?.items ?? [];
12569
13088
  };
@@ -12575,11 +13094,11 @@ function createFetchSubMessages(larkClient) {
12575
13094
  * the account and log function.
12576
13095
  */
12577
13096
  function createParseResolveNames(account) {
12578
- return createBatchResolveNames(account, (...args) => log$10.info(args.map(String).join(" ")));
13097
+ return createBatchResolveNames(account, (...args) => log$9.info(args.map(String).join(" ")));
12579
13098
  }
12580
13099
  //#endregion
12581
13100
  //#region src/messaging/inbound/parse.ts
12582
- const log$9 = larkLogger("inbound/parse");
13101
+ const log$8 = larkLogger("inbound/parse");
12583
13102
  /**
12584
13103
  * Parse a raw Feishu message event into a normalised MessageContext.
12585
13104
  *
@@ -12629,7 +13148,7 @@ async function parseMessageEvent(event, botOpenId, expandCtx) {
12629
13148
  const fullContent = await fetchCardContent(event.message.message_id, larkClient);
12630
13149
  if (fullContent) {
12631
13150
  effectiveContent = fullContent;
12632
- log$9.info("replaced interactive content with full v2 card data");
13151
+ log$8.info("replaced interactive content with full v2 card data");
12633
13152
  }
12634
13153
  }
12635
13154
  const convertCtx = {
@@ -12750,7 +13269,7 @@ var MessageDedup = class {
12750
13269
  };
12751
13270
  //#endregion
12752
13271
  //#region src/messaging/inbound/media-download.ts
12753
- const log$8 = larkLogger("inbound/media-download");
13272
+ const log$7 = larkLogger("inbound/media-download");
12754
13273
  /**
12755
13274
  * 从飞书 API 下载单个图片资源,返回 Buffer + mediaType
12756
13275
  */
@@ -12778,7 +13297,7 @@ async function downloadLarkImage(params) {
12778
13297
  for await (const chunk of readable) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
12779
13298
  buffer = Buffer.concat(chunks);
12780
13299
  } else {
12781
- log$8.warn("图片资源下载返回未知格式", {
13300
+ log$7.warn("图片资源下载返回未知格式", {
12782
13301
  fileKey,
12783
13302
  responseType: typeof response
12784
13303
  });
@@ -12786,10 +13305,10 @@ async function downloadLarkImage(params) {
12786
13305
  }
12787
13306
  if (resp.headers?.["content-type"]) mediaType = resp.headers["content-type"];
12788
13307
  } else {
12789
- log$8.warn("图片资源下载返回空", { fileKey });
13308
+ log$7.warn("图片资源下载返回空", { fileKey });
12790
13309
  return null;
12791
13310
  }
12792
- log$8.info("图片资源下载成功", {
13311
+ log$7.info("图片资源下载成功", {
12793
13312
  fileKey,
12794
13313
  mediaType,
12795
13314
  sizeBytes: buffer.length
@@ -12799,7 +13318,7 @@ async function downloadLarkImage(params) {
12799
13318
  mediaType
12800
13319
  };
12801
13320
  } catch (err) {
12802
- log$8.warn("图片资源下载失败", {
13321
+ log$7.warn("图片资源下载失败", {
12803
13322
  fileKey,
12804
13323
  error: err instanceof Error ? err.message : String(err)
12805
13324
  });
@@ -12834,12 +13353,12 @@ async function buildMultimodalPrompt(params) {
12834
13353
  await mkdir(saveDir, { recursive: true });
12835
13354
  const imgFilePath = path.join(saveDir, `${imgRes.fileKey}.png`);
12836
13355
  await writeFile(imgFilePath, buffer);
12837
- log$8.info("图片已保存到工作目录", {
13356
+ log$7.info("图片已保存到工作目录", {
12838
13357
  fileKey: imgRes.fileKey,
12839
13358
  path: imgFilePath
12840
13359
  });
12841
13360
  } catch (saveErr) {
12842
- log$8.warn("图片保存失败", {
13361
+ log$7.warn("图片保存失败", {
12843
13362
  fileKey: imgRes.fileKey,
12844
13363
  error: saveErr instanceof Error ? saveErr.message : String(saveErr)
12845
13364
  });
@@ -12854,7 +13373,7 @@ async function buildMultimodalPrompt(params) {
12854
13373
  });
12855
13374
  }
12856
13375
  if (imageBlocks.length > 0) {
12857
- log$8.info("已构建多模态 prompt", {
13376
+ log$7.info("已构建多模态 prompt", {
12858
13377
  textLength: textPrompt.length,
12859
13378
  imageCount: imageBlocks.length
12860
13379
  });
@@ -12877,7 +13396,7 @@ const TENANT_USER_ID = "tenant";
12877
13396
  * 在消息入口处构建 UserContext,通过参数逐层传递到
12878
13397
  * SessionRouter、ProcessManager、CC 进程环境。
12879
13398
  */
12880
- const log$7 = larkLogger("user/context");
13399
+ const log$6 = larkLogger("user/context");
12881
13400
  /**
12882
13401
  * 构建用户上下文
12883
13402
  *
@@ -12887,7 +13406,7 @@ const log$7 = larkLogger("user/context");
12887
13406
  function buildUserContext(params) {
12888
13407
  const { openId, userId, userName, chatType, workspaceRoot } = params;
12889
13408
  if (chatType === "group") {
12890
- log$7.info("群聊场景,使用 tenant 身份", {
13409
+ log$6.info("群聊场景,使用 tenant 身份", {
12891
13410
  openId,
12892
13411
  chatType
12893
13412
  });
@@ -12900,7 +13419,7 @@ function buildUserContext(params) {
12900
13419
  };
12901
13420
  }
12902
13421
  const effectiveUserId = userId || openId;
12903
- log$7.info("私聊场景,使用用户个人身份", {
13422
+ log$6.info("私聊场景,使用用户个人身份", {
12904
13423
  effectiveUserId,
12905
13424
  openId,
12906
13425
  userId,
@@ -12925,7 +13444,7 @@ function buildUserContext(params) {
12925
13444
  * ├── CLAUDE.md # 用户级人设(跨所有会话共享)
12926
13445
  * └── settings.json # 用户级偏好
12927
13446
  */
12928
- const log$6 = larkLogger("user/user-dir");
13447
+ const log$5 = larkLogger("user/user-dir");
12929
13448
  /**
12930
13449
  * 确保用户目录结构存在
12931
13450
  *
@@ -12934,7 +13453,7 @@ const log$6 = larkLogger("user/user-dir");
12934
13453
  */
12935
13454
  async function ensureUserDirectory(userCtx) {
12936
13455
  const { userId, credentialDir } = userCtx;
12937
- log$6.info("确保用户目录存在", {
13456
+ log$5.info("确保用户目录存在", {
12938
13457
  userId,
12939
13458
  credentialDir
12940
13459
  });
@@ -12942,7 +13461,7 @@ async function ensureUserDirectory(userCtx) {
12942
13461
  const credentialsPath = path.join(credentialDir, "credentials.json");
12943
13462
  if (!existsSync$1(credentialsPath)) {
12944
13463
  await writeFile$1(credentialsPath, JSON.stringify({}, null, 2), "utf-8");
12945
- log$6.info("创建默认 credentials.json", {
13464
+ log$5.info("创建默认 credentials.json", {
12946
13465
  userId,
12947
13466
  path: credentialsPath
12948
13467
  });
@@ -12950,7 +13469,7 @@ async function ensureUserDirectory(userCtx) {
12950
13469
  const mcpServersPath = path.join(credentialDir, "mcp-servers.json");
12951
13470
  if (!existsSync$1(mcpServersPath)) {
12952
13471
  await writeFile$1(mcpServersPath, JSON.stringify({}, null, 2), "utf-8");
12953
- log$6.info("创建默认 mcp-servers.json", {
13472
+ log$5.info("创建默认 mcp-servers.json", {
12954
13473
  userId,
12955
13474
  path: mcpServersPath
12956
13475
  });
@@ -12963,7 +13482,7 @@ async function ensureUserDirectory(userCtx) {
12963
13482
  userName: userCtx.userName
12964
13483
  };
12965
13484
  await writeFile$1(settingsPath, JSON.stringify(defaultSettings, null, 2), "utf-8");
12966
- log$6.info("创建默认 settings.json", {
13485
+ log$5.info("创建默认 settings.json", {
12967
13486
  userId,
12968
13487
  path: settingsPath
12969
13488
  });
@@ -12981,7 +13500,7 @@ async function ensureUserDirectory(userCtx) {
12981
13500
  `这是你的个人配置文件,跨所有会话共享。`,
12982
13501
  `你可以在这里记录用户偏好、常用指令等信息。`
12983
13502
  ].join("\n"), "utf-8");
12984
- log$6.info("创建用户级 CLAUDE.md", {
13503
+ log$5.info("创建用户级 CLAUDE.md", {
12985
13504
  userId,
12986
13505
  path: claudeMdPath
12987
13506
  });
@@ -12990,79 +13509,6 @@ async function ensureUserDirectory(userCtx) {
12990
13509
  }
12991
13510
  larkLogger("user/oauth-proxy");
12992
13511
  //#endregion
12993
- //#region src/gateway/plugins/loader.ts
12994
- const PLUGIN_REGISTRY = {};
12995
- const log$4 = larkLogger("gateway/plugin-loader");
12996
- /** 已加载的插件实例列表 */
12997
- let loadedPlugins = [];
12998
- /**
12999
- * 加载并注册所有已声明的插件
13000
- *
13001
- * @returns 是否有插件需要外部网络访问(影响网关监听地址)
13002
- */
13003
- async function loadPlugins(app, context) {
13004
- const pluginNames = (process.env.LARKPAL_PLUGINS || "").split(",").map((s) => s.trim()).filter(Boolean);
13005
- if (pluginNames.length === 0) {
13006
- log$4.info("未声明任何插件,跳过插件加载");
13007
- return { requiresExternalAccess: false };
13008
- }
13009
- log$4.info("开始加载插件", { plugins: pluginNames });
13010
- let needsExternal = false;
13011
- for (const name of pluginNames) {
13012
- const factoryLoader = PLUGIN_REGISTRY[name];
13013
- if (!factoryLoader) {
13014
- log$4.warn("未知插件,跳过", {
13015
- name,
13016
- available: Object.keys(PLUGIN_REGISTRY)
13017
- });
13018
- continue;
13019
- }
13020
- try {
13021
- const plugin = (await factoryLoader())();
13022
- plugin.register(app, context);
13023
- log$4.info("插件路由已注册", {
13024
- name: plugin.name,
13025
- version: plugin.version,
13026
- description: plugin.description,
13027
- requiresExternalAccess: plugin.requiresExternalAccess
13028
- });
13029
- if (plugin.init) {
13030
- await plugin.init();
13031
- log$4.info("插件初始化完成", { name: plugin.name });
13032
- }
13033
- loadedPlugins.push(plugin);
13034
- if (plugin.requiresExternalAccess) needsExternal = true;
13035
- } catch (err) {
13036
- log$4.error("插件加载失败", {
13037
- name,
13038
- error: err instanceof Error ? err.message : String(err)
13039
- });
13040
- }
13041
- }
13042
- log$4.info("插件加载完成", {
13043
- loaded: loadedPlugins.map((p) => p.name),
13044
- requiresExternalAccess: needsExternal
13045
- });
13046
- return { requiresExternalAccess: needsExternal };
13047
- }
13048
- /**
13049
- * 关闭所有已加载的插件
13050
- */
13051
- async function disposePlugins() {
13052
- for (const plugin of loadedPlugins) try {
13053
- if (plugin.dispose) {
13054
- await plugin.dispose();
13055
- log$4.info("插件已清理", { name: plugin.name });
13056
- }
13057
- } catch (err) {
13058
- log$4.warn("插件清理失败", {
13059
- name: plugin.name,
13060
- error: err instanceof Error ? err.message : String(err)
13061
- });
13062
- }
13063
- loadedPlugins = [];
13064
- }
13065
- //#endregion
13066
13512
  //#region src/messaging/inbound/reply-policy.ts
13067
13513
  const log$3 = larkLogger("messaging/reply-policy");
13068
13514
  /**
@@ -13971,8 +14417,7 @@ async function main() {
13971
14417
  await teammateConfig.load();
13972
14418
  logger.info("Teammate 配置加载完成", { globalDefault: teammateConfig.getData().globalDefault });
13973
14419
  const gatewayPort = 3e3;
13974
- const hasPlugins = !!process.env.LARKPAL_PLUGINS?.trim();
13975
- const gatewayHost = process.env.LARKPAL_GATEWAY_HOST || (hasPlugins ? "0.0.0.0" : "localhost");
14420
+ const gatewayHost = process.env.LARKPAL_GATEWAY_HOST || "localhost";
13976
14421
  const messageStore = new SessionMessageStore(workspaceRoot);
13977
14422
  const gateway = createGatewayServer({
13978
14423
  port: gatewayPort,
@@ -13985,15 +14430,10 @@ async function main() {
13985
14430
  },
13986
14431
  messageStore
13987
14432
  });
13988
- await loadPlugins(gateway.app, {
13989
- workspaceRoot,
13990
- externalBaseUrl: process.env.LARKPAL_EXTERNAL_URL
13991
- });
13992
14433
  await gateway.start();
13993
14434
  logger.info("网关 HTTP 服务启动完成", {
13994
14435
  host: gatewayHost,
13995
- port: gatewayPort,
13996
- hasPlugins
14436
+ port: gatewayPort
13997
14437
  });
13998
14438
  const messageDedup = new MessageDedup({
13999
14439
  ttlMs: 6e4,
@@ -14320,8 +14760,6 @@ async function main() {
14320
14760
  wsAbortController.abort();
14321
14761
  logger.info("WebSocket 连接已关闭");
14322
14762
  messageDedup.dispose();
14323
- await disposePlugins();
14324
- logger.info("网关插件已清理");
14325
14763
  await gateway.stop();
14326
14764
  logger.info("网关 HTTP 服务已停止");
14327
14765
  logger.info("LarkPal 已关闭");