@modelzen/feishu-codex-bridge 0.6.4 → 0.6.6

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/README.md +32 -8
  2. package/dist/cli.js +280 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,6 +4,9 @@
4
4
  [![total downloads](https://badgen.net/npm/dt/@modelzen/feishu-codex-bridge)](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
5
5
  [![downloads/month](https://badgen.net/npm/dm/@modelzen/feishu-codex-bridge)](https://www.npmjs.com/package/@modelzen/feishu-codex-bridge)
6
6
  [![license](https://badgen.net/npm/license/@modelzen/feishu-codex-bridge)](https://github.com/modelzen/feishu-codex-bridge/blob/main/LICENSE)
7
+ [![官网](https://badgen.net/badge/%E5%AE%98%E7%BD%91/bridge.vonvon.cc/blue)](https://bridge.vonvon.cc)
8
+
9
+ 🌐 **官网:<https://bridge.vonvon.cc>**
7
10
 
8
11
  > 把飞书 / Lark 桥接到你本机的 [Codex](https://github.com/openai/codex) 或 [Claude Code](https://www.anthropic.com/claude-code),在群里 @ 机器人就能让它在指定项目目录里干活,结果以流式 Markdown 卡片实时回到群里。
9
12
  >
@@ -23,22 +26,42 @@
23
26
 
24
27
  ## ⚡ 安装
25
28
 
26
- 两条命令装好、打开网页控制台,**剩下全在网页里点**(扫码加机器人 / 开权限 / 启停):
29
+ 两步打开本机网页控制台。装好 Bridge、打开控制台,再在页面里启动后台服务、添加第一个 Bridge 机器人。
27
30
 
28
31
  ```bash
29
- npm i -g @modelzen/feishu-codex-bridge # 1. 装命令
30
- feishu-codex-bridge web # 2. 打开本机网页控制台
32
+ # 1. 全局安装 Bridge
33
+ npm i -g @modelzen/feishu-codex-bridge
34
+
35
+ # 2. 打开本机网页控制台
36
+ # 启动服务、添加好机器人后,这条前台命令即可 Ctrl+C 关闭
37
+ feishu-codex-bridge web
31
38
  ```
32
39
 
33
- **Codex / Claude** 替你装?把下面这段发给它:
40
+ 不想自己敲?把下面这段发给 **Codex / Claude** 等 AI agent,让它替你装好并跑起来:
34
41
 
35
42
  ```text
36
- 如果本地没有 feishu-codex-bridge 命令,就先用 npm i -g @modelzen/feishu-codex-bridge 安装。
37
- 然后运行 feishu-codex-bridge web 打开网页控制台,把输出的网址给我打开。
38
- 装好、开完网页就停,别的不用做。
43
+ 帮我在这台电脑上安装并跑起来 feishu-codex-bridge
44
+
45
+ 1) 先 node -v 确认有 Node.js(≥18),没有就先装好;
46
+ 2) 再 codex --version 确认已经安装了 codex CLI,没有就先装好;
47
+ 3) 全局安装:
48
+ npm i -g @modelzen/feishu-codex-bridge
49
+ 4) 前台运行,打开本机 Web 控制台:
50
+ feishu-codex-bridge web
51
+ 它一启动会打印一个 http://127.0.0.1:xxxx/?token=... 的链接。
52
+ 5) 把那个链接发给我;
53
+ 6) 然后用文字告诉我接下来怎么做:
54
+ - 我在浏览器打开这个链接;
55
+ - 点页面上的「启动」按钮,把后台服务跑起来;
56
+ - 启动后页面会自动进入可写控制台,在里面扫码添加我的第一个飞书机器人;
57
+ - 成功创建机器人后,前台那条 feishu-codex-bridge web 就可以 Ctrl+C 关掉,不影响后台服务。
39
58
  ```
40
59
 
41
- > 前置:**Node ≥ 20**,外加一个登录好的后端 —— **Codex**(`npm i -g @openai/codex && codex login`)或 **Claude Code**(SDK 随桥内置、复用本机 `claude` 登录态,首次按需下载约 265MB)。打开网页后扫码加机器人、按 checklist 开权限 / 订阅事件,全程点点点,不用碰命令行。
60
+ ### 三分钟,看它入职你的飞书
61
+
62
+ 从全局装包、打开本机网页控制台,到扫码建机器人、私聊报到,一整段带解说录屏。点击下面封面去 B 站观看高清版。
63
+
64
+ [![3 分钟跑通:飞书里直接发任务,Codex 在你本机真干活](docs/assets/install-demo-cover.jpg)](https://www.bilibili.com/video/BV1xP7V6fESb)
42
65
 
43
66
  ---
44
67
 
@@ -140,6 +163,7 @@ npm test # vitest
140
163
 
141
164
  ## 💬 文档 & 交流
142
165
 
166
+ - 🌐 **官网**:<https://bridge.vonvon.cc>
143
167
  - 🎀 **图文介绍**:<https://my.feishu.cn/docx/AFKNdf4QaooL5OxSR8bc5H7vn7b> —— 配大量截图,讲清它在飞书里长什么样、怎么用。
144
168
  - 📖 **命令手册**:<https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g> —— 各场景可用命令速查。
145
169
  - 🐛 **反馈 / 贡献**:<https://github.com/modelzen/feishu-codex-bridge/issues>
package/dist/cli.js CHANGED
@@ -1106,7 +1106,7 @@ ${lines || "\uFF08\u65E0\uFF09"}`));
1106
1106
  return card(elements, { forward: false });
1107
1107
  }
1108
1108
  function buildCliBridgeTaskCompletionCard(input2) {
1109
- const verb = input2.replyDoneAt ? "\u2705 \u5DF2\u786E\u8BA4\u5B8C\u6210" : input2.status === "failed" ? "\u274C \u4EFB\u52A1\u5931\u8D25" : pickCopy(COPY.completion, input2.id || input2.cwd);
1109
+ const verb = input2.replyDoneAt ? "\u2705 \u5DF2\u6536\u5DE5" : input2.status === "failed" ? "\u274C \u4EFB\u52A1\u5931\u8D25" : pickCopy(COPY.completion, input2.id || input2.cwd);
1110
1110
  const elements = [
1111
1111
  titleEl(verb),
1112
1112
  metaLine(input2.source, input2.cwd)
@@ -1123,7 +1123,7 @@ ${codeBlock(chunk, "text")}`));
1123
1123
  }
1124
1124
  if (input2.replyEnabled) {
1125
1125
  const expiresAt = input2.replyExpiresAt ? `\uFF08\u6709\u6548\u671F\u81F3 ${new Date(input2.replyExpiresAt).toLocaleString("zh-CN")}\uFF09` : "";
1126
- elements.push(actions([button("\u23F3 \u7B49\u5F85\u786E\u8BA4", { a: CLI.taskCompletionDone, id: input2.id }, "primary")]));
1126
+ elements.push(actions([button("\u2705 \u6536\u5DE5", { a: CLI.taskCompletionDone, id: input2.id }, "primary")]));
1127
1127
  elements.push(note(pickCopy(COPY.footerReply, input2.id || input2.cwd) + expiresAt));
1128
1128
  } else if (input2.replyDoneAt) {
1129
1129
  elements.push(actions([disabledButton("\u2705 \u5DF2\u5B8C\u6210", "primary")]));
@@ -1190,8 +1190,8 @@ var init_cards2 = __esm({
1190
1190
  "\u4EBA\u5728\u684C\u524D\u6211\u5C31\u95ED\u5634,\u4E00\u5207\u56DE\u5F52\u7EC8\u7AEF\u3002"
1191
1191
  ],
1192
1192
  footerReply: [
1193
- "\u{1F4AC} \u76F4\u63A5\u56DE\u590D\u8FD9\u6761\u6D88\u606F,\u5C31\u80FD\u63A5\u7740\u652F\u4F7F\u5B83\u5E72\u6D3B;\u6216\u70B9\u300C\u7B49\u5F85\u786E\u8BA4\u300D\u8BA9\u5B83\u6536\u5DE5\u3002",
1194
- "\u{1F4AC} \u56DE\u6211\u4E00\u53E5\u8BDD,\u5B83\u7ACB\u523B\u63A5\u7740\u8DD1;\u4E0D\u60F3\u7EE7\u7EED\u5C31\u70B9\u300C\u7B49\u5F85\u786E\u8BA4\u300D\u3002"
1193
+ "\u{1F4AC} \u76F4\u63A5\u56DE\u590D\u8FD9\u6761\u6D88\u606F,\u5C31\u80FD\u63A5\u7740\u652F\u4F7F\u5B83\u5E72\u6D3B;\u6216\u70B9\u300C\u6536\u5DE5\u300D\u8BA9\u5B83\u9000\u51FA\u3002",
1194
+ "\u{1F4AC} \u56DE\u6211\u4E00\u53E5\u8BDD,\u5B83\u7ACB\u523B\u63A5\u7740\u8DD1;\u4E0D\u60F3\u7EE7\u7EED\u5C31\u70B9\u300C\u6536\u5DE5\u300D\u3002"
1195
1195
  ]
1196
1196
  };
1197
1197
  }
@@ -3126,14 +3126,19 @@ function mapNotification(n, ctx) {
3126
3126
  function mapItemStart(item, ctx) {
3127
3127
  switch (item.type) {
3128
3128
  case "commandExecution":
3129
- return { type: "tool_use", itemId: item.id, title: item.command, detail: String(item.cwd) };
3129
+ return { type: "tool_use", itemId: item.id, title: item.command, detail: String(item.cwd), kind: "command" };
3130
3130
  case "fileChange":
3131
- return { type: "tool_use", itemId: item.id, title: fileChangeTitle(item.changes, ctx?.cwd) };
3131
+ return { type: "tool_use", itemId: item.id, title: fileChangeTitle(item.changes, ctx?.cwd), kind: "file" };
3132
3132
  case "webSearch":
3133
- return { type: "tool_use", itemId: item.id, title: "\u8054\u7F51\u641C\u7D22" };
3133
+ return {
3134
+ type: "tool_use",
3135
+ itemId: item.id,
3136
+ title: item.query ? `\u8054\u7F51\u641C\u7D22\uFF1A${item.query}` : "\u8054\u7F51\u641C\u7D22",
3137
+ kind: "search"
3138
+ };
3134
3139
  case "mcpToolCall":
3135
3140
  case "dynamicToolCall":
3136
- return { type: "tool_use", itemId: item.id, title: "\u5DE5\u5177\u8C03\u7528" };
3141
+ return { type: "tool_use", itemId: item.id, title: "\u5DE5\u5177\u8C03\u7528", kind: "tool" };
3137
3142
  default:
3138
3143
  return null;
3139
3144
  }
@@ -3803,7 +3808,8 @@ function createTurnMapper(ctx = {}) {
3803
3808
  type: "tool_use",
3804
3809
  itemId: String(b.id ?? `tool${++blockSeq}`),
3805
3810
  title: toolTitle(b.name ?? "\u5DE5\u5177", b.input ?? {}, ctx.cwd),
3806
- detail: toolDetail(b.name ?? "", b.input ?? {})
3811
+ detail: toolDetail(b.name ?? "", b.input ?? {}),
3812
+ kind: toolKind(b.name ?? "")
3807
3813
  });
3808
3814
  }
3809
3815
  }
@@ -3922,6 +3928,26 @@ function toolTitle(name, input2, cwd) {
3922
3928
  return name || "\u5DE5\u5177\u8C03\u7528";
3923
3929
  }
3924
3930
  }
3931
+ function toolKind(name) {
3932
+ switch (name) {
3933
+ case "Bash":
3934
+ case "BashOutput":
3935
+ return "command";
3936
+ case "Read":
3937
+ case "Write":
3938
+ case "Edit":
3939
+ case "MultiEdit":
3940
+ case "NotebookEdit":
3941
+ return "file";
3942
+ case "Glob":
3943
+ case "Grep":
3944
+ case "WebFetch":
3945
+ case "WebSearch":
3946
+ return "search";
3947
+ default:
3948
+ return "tool";
3949
+ }
3950
+ }
3925
3951
  function toolDetail(name, input2) {
3926
3952
  if (name === "Bash" && typeof input2.description === "string" && input2.description) {
3927
3953
  return input2.description;
@@ -5512,7 +5538,11 @@ function buildResumeCard(state) {
5512
5538
  if (state.threads.length === 0) {
5513
5539
  elements.push(md("_\u8BE5\u76EE\u5F55\u4E0B\u8FD8\u6CA1\u6709\u5386\u53F2\u4F1A\u8BDD\u3002\u76F4\u63A5 @\u6211 \u5373\u53EF\u65B0\u5EFA\u3002_"));
5514
5540
  } else {
5515
- elements.push(note("\u70B9\u4E00\u6761\u5373\u6062\u590D \u2014\u2014 \u5728\u65B0\u8BDD\u9898\u91CC\u6253\u5F00\u5386\u53F2\u3001\u53EF\u76F4\u63A5\u7EE7\u7EED\u3002"));
5541
+ elements.push(
5542
+ note(
5543
+ state.flat ? "\u70B9\u4E00\u6761\u5373\u5207\u56DE \u2014\u2014 \u5C31\u5730\u7EE7\u7EED\uFF0C\u4E0D\u53E6\u8D77\u8BDD\u9898\u3002" : "\u70B9\u4E00\u6761\u5373\u6062\u590D \u2014\u2014 \u5728\u65B0\u8BDD\u9898\u91CC\u6253\u5F00\u5386\u53F2\u3001\u53EF\u76F4\u63A5\u7EE7\u7EED\u3002"
5544
+ )
5545
+ );
5516
5546
  for (const t of state.threads) {
5517
5547
  const title = (t.name?.trim() || t.preview.trim() || "(\u65E0\u6458\u8981)").replace(/\s+/g, " ");
5518
5548
  const label = `\u21A9\uFE0F ${pickerTime(t.updatedAt || t.createdAt)} \xB7 ${truncate(title, RESUME_TITLE_MAX)}`;
@@ -5525,7 +5555,19 @@ function buildResumeLaunchingCard(state) {
5525
5555
  return card([md("\u23F3 \u6B63\u5728\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u2026"), note(metaNote(state))], { summary: "\u6062\u590D\u4E2D" });
5526
5556
  }
5527
5557
  function buildResumeDoneCard(state) {
5528
- return card([md("\u2705 \u5DF2\u6062\u590D \u2014\u2014 \u5DF2\u5728\u4E0A\u65B9\u65B0\u8BDD\u9898\u6253\u5F00\uFF0C\u53EF\u76F4\u63A5\u7EE7\u7EED\u3002"), note(metaNote(state))], { summary: "\u5DF2\u6062\u590D" });
5558
+ const line = state.flat ? "\u21A9\uFE0F \u5DF2\u5207\u56DE \u2014\u2014 \u5C31\u5730\u7EE7\u7EED\uFF0C\u4E0A\u9762\u7684\u5386\u53F2\u6D88\u606F\u4FDD\u7559\u3002" : "\u2705 \u5DF2\u6062\u590D \u2014\u2014 \u5DF2\u5728\u4E0A\u65B9\u65B0\u8BDD\u9898\u6253\u5F00\uFF0C\u53EF\u76F4\u63A5\u7EE7\u7EED\u3002";
5559
+ return card([md(line), note(metaNote(state))], { summary: "\u5DF2\u6062\u590D" });
5560
+ }
5561
+ function buildClearedCard() {
5562
+ return card(
5563
+ [
5564
+ md("\u{1F9F9} **\u5DF2\u5F00\u542F\u5168\u65B0\u4F1A\u8BDD**"),
5565
+ md("\u2705 \u4E0A\u4E0B\u6587\u5DF2\u6E05\u7A7A \u2014\u2014 \u6211\u4ECE\u8FD9\u91CC\u91CD\u65B0\u5F00\u59CB\uFF0C\u4E0D\u518D\u53C2\u8003\u4E0A\u9762\u7684\u5BF9\u8BDD\u3002"),
5566
+ md("\u{1F4AC} \u98DE\u4E66\u804A\u5929\u8BB0\u5F55\u4E0D\u53D7\u5F71\u54CD \u2014\u2014 \u4E0A\u9762\u7684\u6D88\u606F\u90FD\u8FD8\u5728\uFF0C\u53EA\u662F\u6211\u4E0D\u518D\u8BFB\u53D6\u5B83\u4EEC\u3002"),
5567
+ md("\u{1F5C2}\uFE0F \u521A\u624D\u90A3\u6BB5\u5DF2\u5F52\u6863 \u2014\u2014 \u53D1 `/resume` \u968F\u65F6\u5207\u56DE\u7EE7\u7EED\u3002")
5568
+ ],
5569
+ { summary: "\u5DF2\u6E05\u7A7A\u4E0A\u4E0B\u6587" }
5570
+ );
5529
5571
  }
5530
5572
  function buildResumeErrorCard(state, message) {
5531
5573
  return card([md(`\u274C \u6062\u590D\u5931\u8D25\uFF1A${truncate(message, 200)}`), note(metaNote(state))], { summary: "\u6062\u590D\u5931\u8D25" });
@@ -5582,6 +5624,8 @@ function buildHelpCard(scope, noMention = true, isAdmin2 = false, caps) {
5582
5624
  if (showGoal) lines.push(goalLine);
5583
5625
  lines.push("\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6", "\xB7 `/context` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4");
5584
5626
  if (showCompact) lines.push(compactLine);
5627
+ if (isAdmin2) lines.push("\xB7 `/clear` \u2192 \u6E05\u7A7A\u4E0A\u4E0B\u6587\uFF0C\u5F00\u4E00\u6BB5\u5168\u65B0\u4F1A\u8BDD\uFF08\u98DE\u4E66\u6D88\u606F\u4FDD\u7559\uFF09");
5628
+ if (isAdmin2 && showResume) lines.push("\xB7 `/resume` \u2192 \u5207\u56DE\u5386\u53F2\u4F1A\u8BDD");
5585
5629
  if (isAdmin2) lines.push("\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
5586
5630
  lines.push("\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
5587
5631
  elements.push(md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"), hr(), md(lines.join("\n")));
@@ -5625,6 +5669,8 @@ function buildWelcomeCard(kind, docUrl, noMention = true, caps, agentName = "Cod
5625
5669
  talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"),
5626
5670
  ...showGoal ? [goalLine] : [],
5627
5671
  "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6",
5672
+ "\xB7 `/clear` \u2192 \u6E05\u7A7A\u4E0A\u4E0B\u6587\uFF0C\u5F00\u65B0\u4F1A\u8BDD\uFF08\u7BA1\u7406\u5458\uFF0C\u98DE\u4E66\u6D88\u606F\u4FDD\u7559\uFF09",
5673
+ ...showResume ? ["\xB7 `/resume` \u2192 \u5207\u56DE\u5386\u53F2\u4F1A\u8BDD\uFF08\u7BA1\u7406\u5458\uFF09"] : [],
5628
5674
  "\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09",
5629
5675
  "\xB7 `/help` \u2192 \u547D\u4EE4\u901F\u67E5\u5361"
5630
5676
  ].join("\n")
@@ -7007,6 +7053,7 @@ function reduce(state, evt) {
7007
7053
  id: evt.itemId,
7008
7054
  title: evt.title,
7009
7055
  detail: evt.detail,
7056
+ kind: evt.kind,
7010
7057
  status: "running"
7011
7058
  };
7012
7059
  return {
@@ -7349,25 +7396,61 @@ function renderCleanBody(bodyMarkdown, images) {
7349
7396
  }
7350
7397
 
7351
7398
  // src/card/tool-render.ts
7352
- var HEADER_TITLE_MAX = 80;
7399
+ var HEADER_TITLE_MAX = 120;
7353
7400
  var OUTPUT_MAX = 1200;
7401
+ var CMD_BLOCK_MAX = 1e3;
7402
+ var SUMMARY_LINE_MAX = 400;
7354
7403
  var BODY_TOTAL_MAX = 2500;
7404
+ function statusIcon(status) {
7405
+ return status === "done" ? "\u2705" : status === "error" ? "\u274C" : "\u23F3";
7406
+ }
7407
+ function kindGlyph(kind) {
7408
+ switch (kind) {
7409
+ case "command":
7410
+ return "\u{1F527}";
7411
+ case "file":
7412
+ return "\u{1F4C4}";
7413
+ case "search":
7414
+ return "\u{1F50D}";
7415
+ default:
7416
+ return "";
7417
+ }
7418
+ }
7419
+ function leadGlyphs(tool) {
7420
+ const glyph = kindGlyph(tool.kind);
7421
+ return glyph ? `${statusIcon(tool.status)} ${glyph}` : statusIcon(tool.status);
7422
+ }
7355
7423
  function toolHeaderText(tool) {
7356
- const icon = tool.status === "done" ? "\u2705" : tool.status === "error" ? "\u274C" : "\u23F3";
7357
- return `${icon} **${escapeInline2(truncate3(tool.title, HEADER_TITLE_MAX))}**`;
7424
+ return `${leadGlyphs(tool)} **${escapeInline2(truncate3(tool.title, HEADER_TITLE_MAX))}**`;
7425
+ }
7426
+ function toolSummaryLine(tool) {
7427
+ return `- ${leadGlyphs(tool)} **${escapeInline2(truncate3(tool.title, SUMMARY_LINE_MAX))}**`;
7358
7428
  }
7359
7429
  function toolBodyMd(tool) {
7360
- if (!tool.output) {
7361
- return tool.status === "running" ? "_\u8FD0\u884C\u4E2D\u2026_" : "";
7362
- }
7363
- const label = tool.status === "error" ? "Error" : "Output";
7364
- const block = tool.output.startsWith("```") ? tool.output : bashBlock(tool.output);
7365
- const body = `**${label}**
7366
- ${block}`;
7430
+ const lead = invocationMd(tool);
7431
+ const outputPart = tool.output ? outputBlock(tool) : tool.status === "running" ? "_\u8FD0\u884C\u4E2D\u2026_" : tool.kind === "search" ? "_\uFF08\u641C\u7D22\u7ED3\u679C\u5DF2\u7528\u4E8E\u4F5C\u7B54\uFF0C\u4E0D\u5355\u72EC\u56DE\u4F20\uFF09_" : "";
7432
+ const body = [lead, outputPart].filter(Boolean).join("\n\n");
7367
7433
  if (body.length <= BODY_TOTAL_MAX) return body;
7368
7434
  return `${body.slice(0, BODY_TOTAL_MAX)}\u2026
7369
7435
 
7370
- _\uFF08\u5DF2\u622A\u65AD\uFF0C\u5B8C\u6574\u8F93\u51FA ${tool.output.length} \u5B57\u7B26\uFF09_`;
7436
+ _\uFF08\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09_`;
7437
+ }
7438
+ function invocationMd(tool) {
7439
+ if (tool.kind !== "command") return "";
7440
+ const cmd = tool.title.trim();
7441
+ if (!cmd) return "";
7442
+ const detail = tool.detail ? `
7443
+ \`${escapeInline2(tool.detail)}\`` : "";
7444
+ return `**\u547D\u4EE4**
7445
+ \`\`\`bash
7446
+ ${truncate3(cmd, CMD_BLOCK_MAX)}
7447
+ \`\`\`${detail}`;
7448
+ }
7449
+ function outputBlock(tool) {
7450
+ const label = tool.status === "error" ? "Error" : "Output";
7451
+ const block = tool.output.startsWith("```") ? tool.output : bashBlock(tool.output);
7452
+ return `**${label}**
7453
+ ${block}`;
7371
7454
  }
7372
7455
  function bashBlock(output) {
7373
7456
  const note2 = output.length > OUTPUT_MAX ? `
@@ -7468,6 +7551,8 @@ var CONTROLS_EID = "controls";
7468
7551
  var REASONING_MAX = 1500;
7469
7552
  var COLLAPSE_TOOL_THRESHOLD = 3;
7470
7553
  var PROCESS_BODY_BUDGET = 22e3;
7554
+ var PROCESS_COMPONENT_BUDGET = 120;
7555
+ var SUMMARY_BODY_MAX = 6e3;
7471
7556
  function buildRunCard(rc) {
7472
7557
  const state = rc.rs;
7473
7558
  const running = state.terminal === "running";
@@ -7529,7 +7614,7 @@ function renderTerminal(state, rc) {
7529
7614
  const toolCount = blocks.reduce((n, b) => b.kind === "tool" ? n + 1 : n, 0);
7530
7615
  elements.push(
7531
7616
  collapsiblePanelEl({
7532
- title: processTitle(Boolean(reasoning), toolCount),
7617
+ title: processTitle(Boolean(reasoning), toolCount, state.terminal),
7533
7618
  expanded: false,
7534
7619
  border: "grey",
7535
7620
  elements: processEls
@@ -7577,7 +7662,9 @@ function lastTextIndex(blocks) {
7577
7662
  }
7578
7663
  function buildProcessBody(reasoning, blocks) {
7579
7664
  const rich = processElements(reasoning, blocks, false);
7580
- if (estimateSize2(rich) <= PROCESS_BODY_BUDGET) return rich;
7665
+ if (estimateSize2(rich) <= PROCESS_BODY_BUDGET && estimateComponents(rich) <= PROCESS_COMPONENT_BUDGET) {
7666
+ return rich;
7667
+ }
7581
7668
  return processElements(reasoning, blocks, true);
7582
7669
  }
7583
7670
  function processElements(reasoning, blocks, compactTools) {
@@ -7592,18 +7679,25 @@ function processElements(reasoning, blocks, compactTools) {
7592
7679
  }
7593
7680
  return out;
7594
7681
  }
7595
- function processTitle(hasReasoning, toolCount) {
7682
+ function processTitle(hasReasoning, toolCount, terminal) {
7683
+ const icon = terminal === "interrupted" ? "\u23F9" : terminal === "idle_timeout" ? "\u23F1" : terminal === "error" ? "\u26A0\uFE0F" : "\u2705";
7684
+ const noun = terminal === "interrupted" ? "\u4E2D\u65AD\u524D\u7684\u8FC7\u7A0B" : terminal === "idle_timeout" ? "\u8D85\u65F6\u524D\u7684\u8FC7\u7A0B" : terminal === "error" ? "\u51FA\u9519\u524D\u7684\u8FC7\u7A0B" : "\u672C\u8F6E\u8FC7\u7A0B";
7596
7685
  const parts = [];
7597
7686
  if (hasReasoning) parts.push("\u{1F9E0} \u601D\u8003");
7598
- if (toolCount > 0) parts.push(`\u{1F9F0} ${toolCount} \u4E2A\u5DE5\u5177\u8C03\u7528`);
7599
- const detail = parts.length > 0 ? `\uFF1A${parts.join(" \xB7 ")}` : "";
7600
- return `\u{1F5C2} **\u8FC7\u7A0B${detail}**\uFF08\u70B9\u51FB\u5C55\u5F00\uFF09`;
7687
+ if (toolCount > 0) parts.push(`\u{1F9F0} ${toolCount} \u4E2A\u5DE5\u5177`);
7688
+ const detail = parts.length > 0 ? ` \xB7 ${parts.join(" \xB7 ")}` : "";
7689
+ return `${icon} **${noun}**${detail}\uFF08\u70B9\u51FB\u5C55\u5F00\uFF09`;
7601
7690
  }
7602
7691
  function estimateSize2(els) {
7603
7692
  let n = 0;
7604
7693
  for (const el of els) n += JSON.stringify(el).length;
7605
7694
  return n;
7606
7695
  }
7696
+ function estimateComponents(els) {
7697
+ let n = 0;
7698
+ for (const el of els) n += el.tag === "collapsible_panel" ? 3 : 1;
7699
+ return n;
7700
+ }
7607
7701
  function buildRunCardPlain(rc) {
7608
7702
  return buildRunCard({ ...rc, cardKey: void 0 });
7609
7703
  }
@@ -7638,11 +7732,11 @@ function* groupBlocks(blocks) {
7638
7732
  }
7639
7733
  function renderToolGroup(tools, finalized, compact = false) {
7640
7734
  if (tools.length === 0) return [];
7641
- if (compact) return [collapsedToolSummary(tools, true)];
7735
+ if (compact) return [collapsedToolSummary(tools, finalized)];
7736
+ if (finalized) return tools.map((t) => toolPanel(t, false));
7642
7737
  if (tools.length < COLLAPSE_TOOL_THRESHOLD) {
7643
7738
  return tools.map((t) => toolPanel(t, false));
7644
7739
  }
7645
- if (finalized) return [collapsedToolSummary(tools, true)];
7646
7740
  const prior = tools.slice(0, -1);
7647
7741
  const latest = tools[tools.length - 1];
7648
7742
  const out = [];
@@ -7652,7 +7746,7 @@ function renderToolGroup(tools, finalized, compact = false) {
7652
7746
  }
7653
7747
  function reasoningPanel(content, active) {
7654
7748
  return collapsiblePanel({
7655
- title: active ? "\u{1F9E0} **\u601D\u8003\u4E2D**" : "\u{1F9E0} **\u601D\u8003\u5B8C\u6210\uFF0C\u70B9\u51FB\u67E5\u770B**",
7749
+ title: active ? "\u{1F9E0} **\u6B63\u5728\u601D\u8003\u2026**" : "\u{1F9E0} **\u601D\u8003\u8FC7\u7A0B**\uFF08\u70B9\u51FB\u5C55\u5F00\uFF09",
7656
7750
  expanded: active,
7657
7751
  border: "grey",
7658
7752
  body: truncate4(content, REASONING_MAX)
@@ -7668,11 +7762,24 @@ function toolPanel(tool, expanded) {
7668
7762
  }
7669
7763
  function collapsedToolSummary(tools, finalized) {
7670
7764
  const suffix = finalized ? "\uFF08\u5DF2\u7ED3\u675F\uFF09" : "";
7765
+ const lines = tools.map(toolSummaryLine);
7766
+ let body = lines.join("\n");
7767
+ if (body.length > SUMMARY_BODY_MAX) {
7768
+ let kept = 0;
7769
+ let acc = 0;
7770
+ for (const line of lines) {
7771
+ if (acc + line.length + 1 > SUMMARY_BODY_MAX) break;
7772
+ acc += line.length + 1;
7773
+ kept++;
7774
+ }
7775
+ body = `${lines.slice(0, kept).join("\n")}
7776
+ - _\u2026\u8FD8\u6709 ${tools.length - kept} \u4E2A\u547D\u4EE4\u672A\u663E\u793A_`;
7777
+ }
7671
7778
  return collapsiblePanel({
7672
- title: `\u2615 **${tools.length} \u4E2A\u5DE5\u5177\u8C03\u7528${suffix}**`,
7779
+ title: `\u{1F9F0} **${tools.length} \u4E2A\u5DE5\u5177\u8C03\u7528${suffix}**`,
7673
7780
  expanded: false,
7674
7781
  border: "blue",
7675
- body: tools.map((t) => `- ${toolHeaderText(t)}`).join("\n")
7782
+ body
7676
7783
  });
7677
7784
  }
7678
7785
  function footerStatusText(status) {
@@ -11282,6 +11389,14 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11282
11389
  return;
11283
11390
  }
11284
11391
  const ts = turnSession(msg.chatId, project, msg.senderId);
11392
+ if (cmd === "resume") {
11393
+ postResumeCard(msg, ts.sessionKey);
11394
+ return;
11395
+ }
11396
+ if (cmd === "clear") {
11397
+ runClear(msg, ts.sessionKey, ts);
11398
+ return;
11399
+ }
11285
11400
  if (cmd === "model") {
11286
11401
  postModelCard(msg, ts.sessionKey, false);
11287
11402
  return;
@@ -11315,6 +11430,14 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11315
11430
  await channel.send(msg.chatId, { markdown: `\`/${cmd}\` \u8BF7\u5230\u4E3B\u7FA4\u533A\u4F7F\u7528\uFF08\u8BDD\u9898\u5916\u53D1\uFF09\u3002` }, { replyTo: msg.messageId, replyInThread: true }).catch(() => void 0);
11316
11431
  return;
11317
11432
  }
11433
+ if (cmd === "clear") {
11434
+ await channel.send(
11435
+ msg.chatId,
11436
+ { markdown: "`/clear` \u4EC5\u5355\u4F1A\u8BDD\u7FA4\u53EF\u7528\u3002\u591A\u8BDD\u9898\u7FA4\u91CC\uFF0C\u56DE\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9 \u5373\u5F00\u65B0\u8BDD\u9898\uFF1B`/resume` \u53EF\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u3002" },
11437
+ { replyTo: msg.messageId, replyInThread: true }
11438
+ ).catch(() => void 0);
11439
+ return;
11440
+ }
11318
11441
  const ts = turnSession(msg.threadId, project, msg.senderId);
11319
11442
  if (cmd === "model") {
11320
11443
  postModelCard(msg, ts.sessionKey, true);
@@ -11352,6 +11475,14 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11352
11475
  await postGroupSettings(msg, project);
11353
11476
  return;
11354
11477
  }
11478
+ if (cmd === "clear") {
11479
+ await channel.send(
11480
+ msg.chatId,
11481
+ { markdown: "`/clear` \u4EC5\u5355\u4F1A\u8BDD\u7FA4\u53EF\u7528\u3002\u591A\u8BDD\u9898\u7FA4\u91CC\uFF0C@\u6211 + \u5185\u5BB9 \u5373\u5F00\u65B0\u8BDD\u9898\uFF1B`/resume` \u53EF\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u3002" },
11482
+ { replyTo: msg.messageId }
11483
+ ).catch(() => void 0);
11484
+ return;
11485
+ }
11355
11486
  if (cmd === "model" || cmd === "compact" || cmd === "context") {
11356
11487
  await channel.send(msg.chatId, { markdown: `\`/${cmd}\` \u9700\u8981\u5728\u8BDD\u9898\u91CC\u4F7F\u7528\uFF08\u5148 @\u6211 \u5F00\u4E2A\u8BDD\u9898\uFF09\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
11357
11488
  return;
@@ -11380,7 +11511,7 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11380
11511
  function parseCommand(text) {
11381
11512
  const m = /^\/(\w+)/.exec(text);
11382
11513
  const name = m?.[1]?.toLowerCase();
11383
- return name === "resume" || name === "model" || name === "settings" || name === "help" || name === "compact" || name === "context" ? name : null;
11514
+ return name === "resume" || name === "model" || name === "settings" || name === "help" || name === "compact" || name === "context" || name === "clear" ? name : null;
11384
11515
  }
11385
11516
  function shouldRespondWithoutMention(project, msg) {
11386
11517
  if (!(project.noMention ?? defaultNoMention(project))) return false;
@@ -11696,7 +11827,7 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11696
11827
  );
11697
11828
  }).catch((err) => log.fail("intake", err));
11698
11829
  }
11699
- function postResumeCard(msg) {
11830
+ function postResumeCard(msg, singleSessionKey) {
11700
11831
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
11701
11832
  if (!isAdmin(cfg, msg.senderId)) {
11702
11833
  await denyAdminCommand(msg, "resume");
@@ -11715,7 +11846,9 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11715
11846
  projectName: project?.name,
11716
11847
  backend: be.id,
11717
11848
  threads,
11718
- createdAt: Date.now()
11849
+ createdAt: Date.now(),
11850
+ // Single-session group: pick rebinds this key flat (no new topic).
11851
+ ...singleSessionKey ? { flat: true, sessionKey: singleSessionKey } : {}
11719
11852
  };
11720
11853
  const res = await sendManagedCard(channel, msg.chatId, buildResumeCard(state), msg.messageId);
11721
11854
  pruneResumePending();
@@ -11839,6 +11972,68 @@ function createOrchestrator(channel, cfg, fallbackCwd, cliBridge) {
11839
11972
  }
11840
11973
  });
11841
11974
  }
11975
+ function runClear(msg, sessionKey, perm) {
11976
+ const reply = (markdown) => channel.send(msg.chatId, { markdown }, { replyTo: msg.messageId }).then(
11977
+ () => void 0,
11978
+ () => void 0
11979
+ );
11980
+ if (!isAdmin(cfg, msg.senderId)) {
11981
+ void denyAdminCommand(msg, "clear");
11982
+ return;
11983
+ }
11984
+ if (active.get(sessionKey)) {
11985
+ void reply("\u23F3 \u8FD9\u4E00\u8F6E\u8FD8\u5728\u8DD1\uFF0C\u7ED3\u675F\u540E\u518D `/clear`\u3002");
11986
+ return;
11987
+ }
11988
+ const reserved = { queue: [], requesterOpenId: msg.senderId };
11989
+ active.set(sessionKey, reserved);
11990
+ void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
11991
+ try {
11992
+ const [rec, project] = await Promise.all([getSession(sessionKey), getProjectByChatId(msg.chatId)]);
11993
+ const be = backendFor(rec?.backend ?? project?.backend);
11994
+ const cwd = rec?.cwd ?? project?.cwd ?? fallbackCwd;
11995
+ const fresh = await be.startThread({
11996
+ cwd,
11997
+ model: rec?.model,
11998
+ effort: rec?.effort,
11999
+ mode: perm.mode,
12000
+ network: perm.network,
12001
+ autoCompact: perm.autoCompact
12002
+ });
12003
+ const old = sessions.get(sessionKey);
12004
+ if (old && old !== fresh) {
12005
+ sessions.delete(sessionKey);
12006
+ void old.close().catch(() => void 0);
12007
+ }
12008
+ trackSession(sessionKey, fresh);
12009
+ lastUsage.delete(sessionKey);
12010
+ await upsertSession({
12011
+ threadId: sessionKey,
12012
+ chatId: msg.chatId,
12013
+ cwd,
12014
+ sessionId: fresh.sessionId,
12015
+ backend: be.id,
12016
+ model: rec?.model,
12017
+ effort: rec?.effort,
12018
+ summary: "(\u65B0\u4F1A\u8BDD)",
12019
+ createdAt: Date.now(),
12020
+ updatedAt: Date.now()
12021
+ });
12022
+ await sendManagedCard(channel, msg.chatId, buildClearedCard(), msg.messageId, false).catch(
12023
+ (err) => log.fail("card", err, { phase: "clear" })
12024
+ );
12025
+ log.info("intake", "clear", { sessionKey, sessionId: fresh.sessionId, backend: be.id });
12026
+ } catch (err) {
12027
+ log.fail("intake", err, { phase: "clear" });
12028
+ await reply(`\u274C ${err instanceof Error ? err.message : String(err)}`);
12029
+ } finally {
12030
+ if (active.get(sessionKey) === reserved) active.delete(sessionKey);
12031
+ if (reserved.queue.length > 0) {
12032
+ await reply(`\u26A0\uFE0F \u6E05\u7A7A\u671F\u95F4\u6536\u5230\u7684 ${reserved.queue.length} \u6761\u6D88\u606F\u672A\u8FDB\u5165\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u53D1\u3002`);
12033
+ }
12034
+ }
12035
+ });
12036
+ }
11842
12037
  async function postHelpCard(msg, scope, inThread = false, project) {
11843
12038
  const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
11844
12039
  const caps = backendFor(project?.backend).capabilities;
@@ -12707,6 +12902,55 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
12707
12902
  async function resumeFromCard(evt, state, sessionId, backendId) {
12708
12903
  const be = backendFor(backendId);
12709
12904
  try {
12905
+ if (state.flat && state.sessionKey) {
12906
+ const sessionKey = state.sessionKey;
12907
+ if (active.get(sessionKey)) {
12908
+ resumePending.delete(evt.messageId);
12909
+ settleUpdate(evt.messageId, buildResumeErrorCard(state, "\u5F53\u524D\u6709\u4EFB\u52A1\u5728\u8DD1\uFF0C\u7ED3\u675F\u540E\u518D /resume"));
12910
+ return;
12911
+ }
12912
+ const reserved = { queue: [], requesterOpenId: state.requesterOpenId };
12913
+ active.set(sessionKey, reserved);
12914
+ try {
12915
+ const history2 = await be.readHistory(state.cwd, sessionId);
12916
+ resumePending.delete(evt.messageId);
12917
+ await withTrace({ chatId: state.chatId, msgId: state.originalMsgId }, async () => {
12918
+ const cardState = { cwd: state.cwd, projectName: state.projectName, history: history2 };
12919
+ await sendManagedCard(channel, state.chatId, buildHistoryCard(cardState), state.originalMsgId, false).catch(
12920
+ (err) => log.fail("card", err, { phase: "resume-history-single" })
12921
+ );
12922
+ const old = sessions.get(sessionKey);
12923
+ if (old) {
12924
+ sessions.delete(sessionKey);
12925
+ void old.close().catch(() => void 0);
12926
+ }
12927
+ lastUsage.delete(sessionKey);
12928
+ const now = Date.now();
12929
+ await upsertSession({
12930
+ threadId: sessionKey,
12931
+ chatId: state.chatId,
12932
+ cwd: state.cwd,
12933
+ sessionId,
12934
+ backend: be.id,
12935
+ summary: history2.name || history2.preview || "(\u6062\u590D\u4F1A\u8BDD)",
12936
+ createdAt: now,
12937
+ updatedAt: now
12938
+ });
12939
+ log.info("card", "resume-done-single", { sessionKey, sessionId, turns: history2.totalTurns });
12940
+ });
12941
+ settleUpdate(evt.messageId, buildResumeDoneCard(state));
12942
+ } finally {
12943
+ if (active.get(sessionKey) === reserved) active.delete(sessionKey);
12944
+ if (reserved.queue.length > 0) {
12945
+ await channel.send(
12946
+ state.chatId,
12947
+ { markdown: `\u26A0\uFE0F \u5207\u56DE\u671F\u95F4\u6536\u5230\u7684 ${reserved.queue.length} \u6761\u6D88\u606F\u672A\u8FDB\u5165\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u53D1\u3002` },
12948
+ { replyTo: state.originalMsgId }
12949
+ ).catch(() => void 0);
12950
+ }
12951
+ }
12952
+ return;
12953
+ }
12710
12954
  const history = await be.readHistory(state.cwd, sessionId);
12711
12955
  resumePending.delete(evt.messageId);
12712
12956
  let bound = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {