@modelzen/feishu-codex-bridge 0.1.7 → 0.2.0

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 +5 -3
  2. package/dist/cli.js +656 -144
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,6 +121,8 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
121
121
 
122
122
  > 「**文档评论回复**」功能另需 `docs:document.comment:read`、`docs:document.comment:create`、`wiki:wiki:readonly` 三项(见 `COMMENT_SCOPES`)。它们**已预勾选进同一个开通链接**,但**不属于** `REQUIRED_SCOPES` —— 不开通也不会卡住后台服务安装,只是该功能静默关闭。
123
123
 
124
+ > 「**把我加进已有群**」功能另需 `im:chat:readonly`(读群名)、`im:chat.members:write_only`(解绑时机器人退群)两项(见 `JOIN_GROUP_SCOPES`)。同样**已预勾选进同一个开通链接**、**不属于** `REQUIRED_SCOPES`,不开通只是该功能静默关闭。
125
+
124
126
  ### 2)订阅事件 + 回调(长连接模式)
125
127
 
126
128
  `run` / `start` 初始化到这步会**自动打开**「**事件与回调**」页(`https://open.feishu.cn/app/<app_id>/event`)。这页顶部有「**事件配置**」「**回调配置**」两个独立标签,要分别配(飞书对事件/回调**既无开通 API、也无预选深链、连查询订阅状态的接口都没有**,只能手点):
@@ -130,6 +132,8 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
130
132
  - `im.message.receive_v1` —— 收群/私聊消息
131
133
  - `application.bot.menu_v6` —— 机器人菜单点击
132
134
  - `drive.notice.comment_add_v1` —— 云文档新增评论(**仅「文档评论回复」功能需要**;不加则该功能静默关闭,其余照常)
135
+ - `im.chat.member.bot.added_v1` —— 机器人被加入群(**仅「把我加进已有群」功能需要**;触发私聊推送绑定卡,不加则拉我进群没反应)
136
+ - `im.chat.member.bot.deleted_v1` —— 机器人被移出群(同上;触发自动解绑项目,不加则被踢后项目不会自动清理)
133
137
 
134
138
  **「回调配置」标签** → 「订阅方式」改**长连接** → 点「添加回调」:
135
139
 
@@ -267,9 +271,7 @@ src/
267
271
  - 🐛 **反馈 / 贡献**:<https://github.com/modelzen/feishu-codex-bridge/issues>
268
272
  - 👥 **交流群**:扫码加入「Vonvon 灵感研究所」👇
269
273
 
270
- <p align="center"><img src="docs/assets/vonvon-group-qr.png" alt="Vonvon 灵感研究所 群二维码" width="260"></p>
271
-
272
- > 该群二维码永久有效,扫码即可加入。
274
+ <p align="center"><img src="docs/assets/vonvon-group-qr.png" alt="Vonvon 灵感研究所 群二维码" width="300"></p>
273
275
 
274
276
  ---
275
277
 
package/dist/cli.js CHANGED
@@ -538,7 +538,7 @@ async function spawnExecProvider(pc, ref) {
538
538
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
539
539
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
540
540
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
541
- return new Promise((resolve5, reject) => {
541
+ return new Promise((resolve6, reject) => {
542
542
  const env = {};
543
543
  if (pc.passEnv) for (const k of pc.passEnv) {
544
544
  const v = process.env[k];
@@ -583,7 +583,7 @@ async function spawnExecProvider(pc, ref) {
583
583
  try {
584
584
  const parsed = JSON.parse(stdout);
585
585
  const value = parsed.values?.[ref.id];
586
- if (typeof value === "string") return resolve5(value);
586
+ if (typeof value === "string") return resolve6(value);
587
587
  const err = parsed.errors?.[ref.id]?.message;
588
588
  reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
589
589
  } catch (err) {
@@ -679,7 +679,37 @@ var COMMENT_SCOPES = [
679
679
  "docs:document.comment:create",
680
680
  "wiki:wiki:readonly"
681
681
  ];
682
- var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES];
682
+ var JOIN_GROUP_SCOPES = [
683
+ "im:chat:readonly",
684
+ "im:chat.members:write_only"
685
+ ];
686
+ var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES, ...JOIN_GROUP_SCOPES];
687
+ var SCOPE_LABELS = {
688
+ "im:message.group_at_msg:readonly": "\u63A5\u6536\u7FA4\u91CC @\u673A\u5668\u4EBA \u7684\u6D88\u606F",
689
+ "im:message.group_msg": "\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\uFF08\u514D@\uFF09",
690
+ "im:message.p2p_msg:readonly": "\u63A5\u6536\u79C1\u804A\u6D88\u606F\uFF08\u7BA1\u7406\u53F0\uFF09",
691
+ "im:message:send_as_bot": "\u53D1\u9001\u6D88\u606F / \u5361\u7247",
692
+ "im:message.pins:write_only": "\u7F6E\u9876\u6D88\u606F\u5230\u7FA4 Pin",
693
+ "im:message.reactions:write_only": "\u6D88\u606F\u8868\u60C5\u56DE\u590D\uFF08\u8FD0\u884C\u72B6\u6001\uFF09",
694
+ "im:resource": "\u56FE\u7247 / \u6587\u4EF6\u4E0A\u4F20\u4E0E\u4E0B\u8F7D",
695
+ "im:chat:create": "\u521B\u5EFA\u9879\u76EE\u7FA4",
696
+ "im:chat:update": "\u8F6C\u79FB\u7FA4\u4E3B\uFF08\u89E3\u7ED1\u65F6\uFF09",
697
+ "im:chat.managers:write_only": "\u8BBE\u7F6E\u7FA4\u7BA1\u7406\u5458",
698
+ "im:chat.announcement:read": "\u8BFB\u53D6\u7FA4\u516C\u544A",
699
+ "im:chat.announcement:write_only": "\u7F16\u8F91\u7FA4\u516C\u544A",
700
+ "im:chat.top_notice:write_only": "\u7F6E\u9876\u7FA4\u516C\u544A\u6A2A\u5E45",
701
+ "im:chat.tabs:write_only": "\u6DFB\u52A0\u7FA4\u6807\u7B7E\u9875",
702
+ "im:chat:readonly": "\u8BFB\u53D6\u7FA4\u4FE1\u606F\uFF08\u7FA4\u540D/\u7FA4\u4E3B\uFF0C\u52A0\u5165\u5B58\u91CF\u7FA4\u7528\uFF09",
703
+ "im:chat.members:write_only": "\u7FA4\u6210\u5458\u589E\u51CF\uFF08\u7ED1\u5B9A\u7684\u5B58\u91CF\u7FA4\u89E3\u7ED1\u65F6\u673A\u5668\u4EBA\u9000\u7FA4\uFF09",
704
+ "cardkit:card:write": "\u4EA4\u4E92\u6309\u94AE\u5361\u7247",
705
+ "docs:document.comment:read": "\u8BFB\u53D6\u6587\u6863\u8BC4\u8BBA",
706
+ "docs:document.comment:create": "\u53D1\u8868\u6587\u6863\u8BC4\u8BBA\u56DE\u590D",
707
+ "wiki:wiki:readonly": "\u8BFB\u53D6\u77E5\u8BC6\u5E93\u8282\u70B9"
708
+ };
709
+ function labelScope(scope) {
710
+ const label = SCOPE_LABELS[scope];
711
+ return label ? `${label}\uFF08${scope}\uFF09` : scope;
712
+ }
683
713
  var HOSTS = {
684
714
  feishu: "open.feishu.cn",
685
715
  lark: "open.larksuite.com"
@@ -722,8 +752,15 @@ async function validateAppCredentials(appId, appSecret, tenant) {
722
752
  }
723
753
  const token = data.tenant_access_token;
724
754
  const info = await fetchBotInfo(base, token).catch(() => void 0);
725
- const missingScopes = await fetchMissingScopes(base, token).catch(() => void 0);
726
- return { ok: true, botName: info?.bot?.app_name, botOpenId: info?.bot?.open_id, missingScopes };
755
+ const granted = await fetchGrantedScopes(base, token).catch(() => void 0);
756
+ const missing = (list) => granted ? list.filter((s) => !granted.has(s)) : void 0;
757
+ return {
758
+ ok: true,
759
+ botName: info?.bot?.app_name,
760
+ botOpenId: info?.bot?.open_id,
761
+ missingScopes: missing(REQUIRED_SCOPES),
762
+ missingJoinScopes: missing(JOIN_GROUP_SCOPES)
763
+ };
727
764
  }
728
765
  async function fetchBotInfo(base, token) {
729
766
  const resp = await fetch(`${base}/open-apis/bot/v3/info`, {
@@ -732,15 +769,14 @@ async function fetchBotInfo(base, token) {
732
769
  if (!resp.ok) return void 0;
733
770
  return await resp.json();
734
771
  }
735
- async function fetchMissingScopes(base, token) {
772
+ async function fetchGrantedScopes(base, token) {
736
773
  const resp = await fetch(`${base}/open-apis/application/v6/scopes`, {
737
774
  headers: { Authorization: `Bearer ${token}` }
738
775
  });
739
776
  if (!resp.ok) return void 0;
740
777
  const body = await resp.json();
741
778
  if (!body.data?.scopes) return void 0;
742
- const granted = new Set(body.data.scopes.filter((s) => s.grant_status === 1).map((s) => s.scope_name));
743
- return REQUIRED_SCOPES.filter((s) => !granted.has(s));
779
+ return new Set(body.data.scopes.filter((s) => s.grant_status === 1).map((s) => s.scope_name));
744
780
  }
745
781
 
746
782
  // src/utils/open-url.ts
@@ -1047,6 +1083,10 @@ async function confirmReadyForDaemon(result) {
1047
1083
  console.log(" \u2022 \uFF08\u53EF\u9009\uFF09\u60F3\u8981\u300C\u5728\u98DE\u4E66\u6587\u6863\u8BC4\u8BBA\u91CC @\u673A\u5668\u4EBA\u5C31\u81EA\u52A8\u56DE\u590D\u300D\uFF0C\u518D\u52A0\u8FD9\u4E00\u4E2A\u4E8B\u4EF6\uFF1A");
1048
1084
  console.log(" drive.notice.comment_add_v1\uFF08\u4E91\u6587\u6863\u65B0\u589E\u8BC4\u8BBA\uFF09");
1049
1085
  console.log(" \u5B83\u4F9D\u8D56\u300C\u6587\u6863\u8BC4\u8BBA\u300D\u6743\u9650\uFF08docs:document.comment:read / :create\uFF0C\u6388\u6743\u94FE\u63A5\u5DF2\u9884\u52FE\u9009\uFF09\uFF1B\u4E0D\u52A0\u5219\u8BE5\u529F\u80FD\u9759\u9ED8\u5173\u95ED\u3002");
1086
+ console.log(" \u2022 \uFF08\u53EF\u9009\uFF09\u60F3\u8981\u300C\u628A\u6211\u52A0\u8FDB\u5DF2\u6709\u7FA4\u5C31\u80FD\u7ED1\u5B9A\u6210\u9879\u76EE\u300D\uFF0C\u518D\u52A0\u8FD9\u4E24\u4E2A\u4E8B\u4EF6\uFF1A");
1087
+ console.log(" im.chat.member.bot.added_v1\uFF08\u673A\u5668\u4EBA\u88AB\u52A0\u5165\u7FA4 \u2192 \u79C1\u804A\u63A8\u9001\u7ED1\u5B9A\u5361\uFF09");
1088
+ console.log(" im.chat.member.bot.deleted_v1\uFF08\u673A\u5668\u4EBA\u88AB\u79FB\u51FA\u7FA4 \u2192 \u81EA\u52A8\u89E3\u7ED1\u9879\u76EE\uFF09");
1089
+ console.log(" \u5B83\u4EEC\u4F9D\u8D56\u300C\u7FA4\u4FE1\u606F/\u7FA4\u6210\u5458\u300D\u6743\u9650\uFF08im:chat:readonly / im:chat.members:write_only\uFF0C\u5DF2\u9884\u52FE\u9009\uFF09\uFF1B\u4E0D\u52A0\u5219\u8BE5\u529F\u80FD\u9759\u9ED8\u5173\u95ED\u3002");
1050
1090
  console.log(" \u2022 \u5207\u5230\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u6807\u7B7E \u2192 \u300C\u8BA2\u9605\u65B9\u5F0F\u300D\u6539\u300C\u957F\u8FDE\u63A5\u300D\u2192 \u70B9\u300C\u6DFB\u52A0\u56DE\u8C03\u300D\u52FE\u9009\uFF1A");
1051
1091
  console.log(" card.action.trigger\uFF08\u5361\u7247\u56DE\u4F20\u4EA4\u4E92\uFF09");
1052
1092
  console.log(" \u26A0\uFE0F \u5B83\u662F\u300C\u56DE\u8C03\u300D\u4E0D\u662F\u300C\u4E8B\u4EF6\u300D\u2014\u2014\u5728\u4E0A\u9762\u300C\u6DFB\u52A0\u4E8B\u4EF6\u300D\u91CC\u641C\u4E0D\u5230\uFF0C\u5FC5\u987B\u5207\u5230\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u8FD9\u4E2A\u6807\u7B7E\u3002");
@@ -1112,7 +1152,7 @@ var AsyncQueue = class {
1112
1152
  continue;
1113
1153
  }
1114
1154
  if (this.closed) return;
1115
- const next = await new Promise((resolve5) => this.waiters.push(resolve5));
1155
+ const next = await new Promise((resolve6) => this.waiters.push(resolve6));
1116
1156
  if (next.done) return;
1117
1157
  yield next.value;
1118
1158
  }
@@ -1163,8 +1203,8 @@ var AppServerClient = class {
1163
1203
  const id = ++this.nextId;
1164
1204
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1165
1205
  `;
1166
- return new Promise((resolve5, reject) => {
1167
- this.pending.set(id, { resolve: resolve5, reject });
1206
+ return new Promise((resolve6, reject) => {
1207
+ this.pending.set(id, { resolve: resolve6, reject });
1168
1208
  this.child.stdin.write(payload, (err) => {
1169
1209
  if (err) {
1170
1210
  this.pending.delete(id);
@@ -1188,14 +1228,14 @@ var AppServerClient = class {
1188
1228
  const child = this.child;
1189
1229
  if (!child || child.exitCode !== null) return;
1190
1230
  child.kill("SIGTERM");
1191
- await new Promise((resolve5) => {
1231
+ await new Promise((resolve6) => {
1192
1232
  const t = setTimeout(() => {
1193
1233
  if (child.exitCode === null) child.kill("SIGKILL");
1194
- resolve5();
1234
+ resolve6();
1195
1235
  }, graceMs);
1196
1236
  child.once("exit", () => {
1197
1237
  clearTimeout(t);
1198
- resolve5();
1238
+ resolve6();
1199
1239
  });
1200
1240
  });
1201
1241
  }
@@ -1312,14 +1352,28 @@ function mapItemComplete(item) {
1312
1352
  // src/agent/codex-appserver/backend.ts
1313
1353
  var APPROVAL_POLICY = "never";
1314
1354
  var SANDBOX = "danger-full-access";
1355
+ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1356
+ "\u4F60\u73B0\u5728\u901A\u8FC7\u300C\u98DE\u4E66\u6865\u300D\u4E0E\u7528\u6237\u5BF9\u8BDD\uFF1A\u4F60\u7684\u56DE\u590D\u4F1A\u88AB\u6E32\u67D3\u6210\u98DE\u4E66\u6D88\u606F\u3002\u8BF7\u9075\u5B88\u4E24\u6761\u8F93\u51FA\u7EA6\u5B9A\u3002",
1357
+ "",
1358
+ "1) \u56FE\u7247\uFF1A\u8981\u914D\u56FE\u65F6\uFF0C\u7528\u6807\u51C6 Markdown \u56FE\u7247\u8BED\u6CD5 ![\u8BF4\u660E](\u8DEF\u5F84) \u5F15\u7528\u4E00\u4E2A\u3010\u771F\u5B9E\u5B58\u5728\u3011\u7684\u56FE\u7247\uFF0C",
1359
+ "\u98DE\u4E66\u6865\u4F1A\u81EA\u52A8\u4E0A\u4F20\u5E76\u5728\u98DE\u4E66\u91CC\u6E32\u67D3\u3002\u8DEF\u5F84\u53EF\u4EE5\u662F\u76F8\u5BF9\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u7684\u76F8\u5BF9\u8DEF\u5F84\u3001\u5DE5\u4F5C\u76EE\u5F55\u5185\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C",
1360
+ "\u6216\u4E00\u4E2A http(s) \u56FE\u7247 URL\u3002\u7EDD\u4E0D\u8981\u7F16\u9020\u4E0D\u5B58\u5728\u7684\u56FE\u7247\u5360\u4F4D\uFF08\u4F8B\u5982\u5199 ![\u7BA1\u7406\u53F0\u622A\u56FE] \u5374\u6CA1\u6709\u5BF9\u5E94\u6587\u4EF6\uFF09\u2014\u2014",
1361
+ "\u6CA1\u6709\u771F\u5B9E\u56FE\u7247\u5C31\u4E0D\u8981\u5199\u56FE\u7247\u8BED\u6CD5\u3002",
1362
+ "",
1363
+ "2) \u5361\u7247\uFF1A\u4EC5\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u300C\u7528\u5361\u7247\u56DE\u590D / \u505A\u6210\u98DE\u4E66\u5361\u7247 / \u5361\u7247\u5F62\u5F0F\u5C55\u793A / changelog \u5361\u7247\u300D\u4E4B\u7C7B\u65F6\uFF0C",
1364
+ "\u628A\u8981\u5C55\u793A\u7684\u5185\u5BB9\u5305\u8FDB\u4E00\u4E2A ```feishu-card \u4EE3\u7801\u5757\uFF0C\u5757\u5185\u7528 Markdown \u4E66\u5199\uFF1A",
1365
+ "\u9996\u884C\u7528 `# \u6807\u9898` \u4F5C\u4E3A\u5361\u7247\u6807\u9898\u680F\uFF1B\u7528 `---` \u4F5C\u5206\u9694\u7EBF\uFF1B\u7528 `> \u6587\u5B57` \u4F5C\u7070\u8272\u6CE8\u811A\uFF1B",
1366
+ "`**\u7C97\u4F53**`\u3001\u5217\u8868\u3001\u94FE\u63A5\u7167\u5E38\u4F7F\u7528\uFF1B\u914D\u56FE\u540C\u6837\u7528 ![\u8BF4\u660E](\u771F\u5B9E\u8DEF\u5F84)\u3002",
1367
+ "\u4E0D\u8981\u624B\u5199\u98DE\u4E66\u5361\u7247\u7684 JSON\u3002\u666E\u901A\u95EE\u7B54\u6B63\u5E38\u56DE\u590D\u5373\u53EF\uFF0C\u53EA\u6709\u7528\u6237\u8981\u5361\u7247\u65F6\u624D\u7528 ```feishu-card \u4EE3\u7801\u5757\u3002"
1368
+ ].join("\n");
1315
1369
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1316
1370
  function withDeadline(p, ms, label) {
1317
- return new Promise((resolve5, reject) => {
1371
+ return new Promise((resolve6, reject) => {
1318
1372
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1319
1373
  p.then(
1320
1374
  (v) => {
1321
1375
  clearTimeout(t);
1322
- resolve5(v);
1376
+ resolve6(v);
1323
1377
  },
1324
1378
  (e) => {
1325
1379
  clearTimeout(t);
@@ -1359,11 +1413,11 @@ var CodexThread = class {
1359
1413
  if (self.model) params.model = self.model;
1360
1414
  if (self.effort) params.effort = self.effort;
1361
1415
  let startError;
1362
- const startFailed = new Promise((resolve5) => {
1416
+ const startFailed = new Promise((resolve6) => {
1363
1417
  self.client.request("turn/start", params).then(void 0, (err) => {
1364
1418
  startError = err instanceof Error ? err : new Error(String(err));
1365
1419
  log.fail("agent", startError, { phase: "turn/start" });
1366
- resolve5("start-failed");
1420
+ resolve6("start-failed");
1367
1421
  });
1368
1422
  });
1369
1423
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -1487,6 +1541,7 @@ var CodexAppServerBackend = class {
1487
1541
  cwd: opts.cwd,
1488
1542
  approvalPolicy: APPROVAL_POLICY,
1489
1543
  sandbox: SANDBOX,
1544
+ developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1490
1545
  ...opts.model ? { model: opts.model } : {}
1491
1546
  });
1492
1547
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
@@ -1498,6 +1553,7 @@ var CodexAppServerBackend = class {
1498
1553
  cwd: opts.cwd,
1499
1554
  approvalPolicy: APPROVAL_POLICY,
1500
1555
  sandbox: SANDBOX,
1556
+ developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1501
1557
  ...opts.model ? { model: opts.model } : {}
1502
1558
  });
1503
1559
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
@@ -1667,7 +1723,7 @@ function stampRenderToken(card2) {
1667
1723
  };
1668
1724
  visit(card2);
1669
1725
  }
1670
- async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread = false) {
1726
+ async function sendManagedCard(channel, to, card2, replyTo, replyInThread = false, receiveIdType = "chat_id") {
1671
1727
  stampRenderToken(card2);
1672
1728
  const created = await channel.rawClient.cardkit.v1.card.create({
1673
1729
  data: { type: "card_json", data: JSON.stringify(card2) }
@@ -1686,8 +1742,8 @@ async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread =
1686
1742
  messageId = sent.data?.message_id;
1687
1743
  } else {
1688
1744
  const sent = await channel.rawClient.im.v1.message.create({
1689
- params: { receive_id_type: "chat_id" },
1690
- data: { receive_id: chatId, msg_type: "interactive", content }
1745
+ params: { receive_id_type: receiveIdType },
1746
+ data: { receive_id: to, msg_type: "interactive", content }
1691
1747
  });
1692
1748
  messageId = sent.data?.message_id;
1693
1749
  }
@@ -1928,6 +1984,15 @@ function card(elements, opts = {}) {
1928
1984
  function md(content) {
1929
1985
  return { tag: "markdown", content };
1930
1986
  }
1987
+ function image(imgKey, alt = "") {
1988
+ return {
1989
+ tag: "img",
1990
+ img_key: imgKey,
1991
+ alt: { tag: "plain_text", content: alt },
1992
+ mode: "fit_horizontal",
1993
+ preview: true
1994
+ };
1995
+ }
1931
1996
  function note(content) {
1932
1997
  return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: "grey" } };
1933
1998
  }
@@ -2134,14 +2199,20 @@ function pickerTime(unixSeconds) {
2134
2199
  const md2 = `${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
2135
2200
  return d.getFullYear() === now.getFullYear() ? `${md2} ${hm}` : `${d.getFullYear()}-${md2} ${hm}`;
2136
2201
  }
2137
- function buildHelpCard(scope) {
2202
+ function talkLine(noMention, tail) {
2203
+ return noMention ? `\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 ${tail}` : `\xB7 **@\u6211 + \u5185\u5BB9** \u2192 ${tail}\uFF08\u672C\u7FA4\u9ED8\u8BA4\u9700 @\uFF1B\`/settings\` \u53EF\u5F00\u542F\u514D@\uFF09`;
2204
+ }
2205
+ function buildHelpCard(scope, noMention = true) {
2138
2206
  const elements = [];
2139
2207
  if (scope === "single") {
2140
2208
  elements.push(
2141
2209
  md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"),
2142
2210
  hr(),
2143
2211
  md(
2144
- "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u4EA4\u7ED9\u6211\u5904\u7406\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
2212
+ `${talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406")}
2213
+ \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2214
+ \xB7 \`/settings\` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09
2215
+ \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2145
2216
  )
2146
2217
  );
2147
2218
  } else if (scope === "topic") {
@@ -2149,7 +2220,9 @@ function buildHelpCard(scope) {
2149
2220
  md("\u{1F9F5} **\u8BDD\u9898\u5185** \u2014 \u6BCF\u4E2A\u8BDD\u9898\u662F\u4E00\u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
2150
2221
  hr(),
2151
2222
  md(
2152
- "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
2223
+ `${talkLine(noMention, "\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD")}
2224
+ \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2225
+ \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2153
2226
  ),
2154
2227
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
2155
2228
  );
@@ -2164,7 +2237,7 @@ function buildHelpCard(scope) {
2164
2237
  }
2165
2238
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
2166
2239
  }
2167
- function buildWelcomeCard(kind, docUrl) {
2240
+ function buildWelcomeCard(kind, docUrl, noMention = true) {
2168
2241
  const elements = [
2169
2242
  md("\u{1F44B} **\u6B22\u8FCE\u4F7F\u7528 Codex Bridge** \u2014 \u672C\u7FA4\u5DF2\u7ED1\u5B9A\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5728\u7FA4\u91CC\u5C31\u80FD\u9A71\u52A8\u672C\u673A Codex \u5E72\u6D3B\u3002"),
2170
2243
  hr()
@@ -2173,7 +2246,10 @@ function buildWelcomeCard(kind, docUrl) {
2173
2246
  elements.push(
2174
2247
  md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4**\uFF08\u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\uFF09"),
2175
2248
  md(
2176
- "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u4EA4\u7ED9\u6211\u5904\u7406\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/help` \u2192 \u547D\u4EE4\u901F\u67E5\u5361"
2249
+ `${talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406")}
2250
+ \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2251
+ \xB7 \`/settings\` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09
2252
+ \xB7 \`/help\` \u2192 \u547D\u4EE4\u901F\u67E5\u5361`
2177
2253
  )
2178
2254
  );
2179
2255
  } else {
@@ -2337,6 +2413,92 @@ function truncateTail(s, n) {
2337
2413
  return t.length > n ? `\u2026${t.slice(t.length - n)}` : t;
2338
2414
  }
2339
2415
 
2416
+ // src/card/markdown-render.ts
2417
+ var NO_IMAGES = /* @__PURE__ */ new Map();
2418
+ var IMG_RE = /!\[([^\]]*)\]\(\s*(<[^>]+>|[^)\s]+)(?:\s+(?:"[^"]*"|'[^']*'))?\s*\)/g;
2419
+ var FENCE_RE = /```feishu-card[^\n]*\n([\s\S]*?)```/g;
2420
+ function cleanSrc(raw) {
2421
+ let s = raw.trim();
2422
+ if (s.startsWith("<") && s.endsWith(">")) s = s.slice(1, -1).trim();
2423
+ return s;
2424
+ }
2425
+ function extractCardFences(text) {
2426
+ const fences = [];
2427
+ const re = new RegExp(FENCE_RE.source, "g");
2428
+ const stripped = text.replace(re, (_full, inner) => {
2429
+ fences.push(inner.trim());
2430
+ return "";
2431
+ });
2432
+ return { fences, stripped };
2433
+ }
2434
+ function renderRichText(text, images = NO_IMAGES) {
2435
+ const body = extractCardFences(text).stripped;
2436
+ if (!body.includes("![")) {
2437
+ const t = body.trim();
2438
+ return t ? [md(t)] : [];
2439
+ }
2440
+ const els = [];
2441
+ let buf = "";
2442
+ const flush = () => {
2443
+ const t = buf.trim();
2444
+ if (t) els.push(md(t));
2445
+ buf = "";
2446
+ };
2447
+ const re = new RegExp(IMG_RE.source, "g");
2448
+ let last = 0;
2449
+ let m;
2450
+ while ((m = re.exec(body)) !== null) {
2451
+ buf += body.slice(last, m.index);
2452
+ const alt = m[1] ?? "";
2453
+ const src = cleanSrc(m[2] ?? "");
2454
+ const key = images.get(src);
2455
+ if (key) {
2456
+ flush();
2457
+ els.push(image(key, alt));
2458
+ } else {
2459
+ buf += m[0];
2460
+ }
2461
+ last = m.index + m[0].length;
2462
+ }
2463
+ buf += body.slice(last);
2464
+ flush();
2465
+ return els;
2466
+ }
2467
+ function buildCleanCard(fenceMarkdown, images = NO_IMAGES, template = "blue") {
2468
+ const lines = fenceMarkdown.split("\n");
2469
+ let start = 0;
2470
+ while (start < lines.length && lines[start]?.trim() === "") start++;
2471
+ const headingMatch = lines[start]?.match(/^#{1,6}\s+(.+?)\s*$/);
2472
+ const title = headingMatch ? headingMatch[1] : "";
2473
+ if (headingMatch) start++;
2474
+ const bodyMarkdown = lines.slice(start).join("\n").trim();
2475
+ const elements = renderCleanBody(bodyMarkdown, images);
2476
+ const body = elements.length > 0 ? elements : [md(title || "\xAD")];
2477
+ return card(body, {
2478
+ ...title ? { header: { title, template } } : {},
2479
+ summary: title || "\u5361\u7247"
2480
+ });
2481
+ }
2482
+ function renderCleanBody(bodyMarkdown, images) {
2483
+ const out = [];
2484
+ for (const raw of bodyMarkdown.split(/\n{2,}/)) {
2485
+ const block = raw.trim();
2486
+ if (!block) continue;
2487
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(block)) {
2488
+ out.push(hr());
2489
+ continue;
2490
+ }
2491
+ const blockLines = block.split("\n");
2492
+ if (blockLines.every((l) => l.trim() === "" || /^\s*>\s?/.test(l))) {
2493
+ const noteText = blockLines.map((l) => l.replace(/^\s*>\s?/, "")).join("\n").trim();
2494
+ if (noteText) out.push(note(noteText));
2495
+ continue;
2496
+ }
2497
+ out.push(...renderRichText(block, images));
2498
+ }
2499
+ return out;
2500
+ }
2501
+
2340
2502
  // src/card/tool-render.ts
2341
2503
  var HEADER_TITLE_MAX = 80;
2342
2504
  var OUTPUT_MAX = 1200;
@@ -2415,7 +2577,7 @@ function renderTerminal(state, rc) {
2415
2577
  })
2416
2578
  );
2417
2579
  }
2418
- if (answer) elements.push(md(answer));
2580
+ if (answer) elements.push(...renderRichText(answer, rc.images));
2419
2581
  if (state.terminal === "interrupted") {
2420
2582
  elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
2421
2583
  } else if (state.terminal === "idle_timeout") {
@@ -2624,6 +2786,207 @@ var RunCardStream = class {
2624
2786
  }
2625
2787
  };
2626
2788
 
2789
+ // src/card/outbound-images.ts
2790
+ import { readFile as readFile5, stat as stat2 } from "fs/promises";
2791
+ import { extname, isAbsolute, resolve as resolve2, sep } from "path";
2792
+ var MAX_IMAGES = 9;
2793
+ var MAX_BYTES = 10 * 1024 * 1024;
2794
+ var DOWNLOAD_TIMEOUT_MS = 1e4;
2795
+ var ALLOWED_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif", "tif", "tiff", "bmp", "ico"]);
2796
+ var cache = /* @__PURE__ */ new Map();
2797
+ var IMG_RE2 = /!\[([^\]]*)\]\(\s*(<[^>]+>|[^)\s]+)(?:\s+(?:"[^"]*"|'[^']*'))?\s*\)/g;
2798
+ function cleanSrc2(raw) {
2799
+ let s = raw.trim();
2800
+ if (s.startsWith("<") && s.endsWith(">")) s = s.slice(1, -1).trim();
2801
+ return s;
2802
+ }
2803
+ function imageSources(text) {
2804
+ const out = [];
2805
+ const seen = /* @__PURE__ */ new Set();
2806
+ const re = new RegExp(IMG_RE2.source, "g");
2807
+ let m;
2808
+ while ((m = re.exec(text)) !== null) {
2809
+ const src = cleanSrc2(m[2] ?? "");
2810
+ if (src && !seen.has(src)) {
2811
+ seen.add(src);
2812
+ out.push(src);
2813
+ }
2814
+ }
2815
+ return out;
2816
+ }
2817
+ async function uploadOutboundImages(channel, sources, cwd) {
2818
+ const picked = sources.slice(0, MAX_IMAGES);
2819
+ if (sources.length > picked.length) {
2820
+ log.warn("outbound", "image-cap", { skipped: sources.length - picked.length });
2821
+ }
2822
+ const results = await Promise.all(
2823
+ picked.map(async (src) => {
2824
+ try {
2825
+ return [src, await resolveAndUpload(channel, src, cwd)];
2826
+ } catch (err) {
2827
+ log.warn("outbound", "image-failed", { src: src.slice(0, 80), err: String(err) });
2828
+ return [src, void 0];
2829
+ }
2830
+ })
2831
+ );
2832
+ const out = /* @__PURE__ */ new Map();
2833
+ for (const [src, key] of results) if (key) out.set(src, key);
2834
+ if (out.size > 0) log.info("outbound", "images", { want: sources.length, uploaded: out.size });
2835
+ return out;
2836
+ }
2837
+ async function resolveAndUpload(channel, src, cwd) {
2838
+ const { buffer, cacheKey } = await loadSource(src, cwd);
2839
+ if (!buffer) return void 0;
2840
+ const hit = cache.get(cacheKey);
2841
+ if (hit) return hit;
2842
+ const key = await uploadBuffer(channel, buffer);
2843
+ if (key) cache.set(cacheKey, key);
2844
+ return key;
2845
+ }
2846
+ async function loadSource(src, cwd) {
2847
+ if (/^https?:\/\//i.test(src)) return loadRemote(src);
2848
+ return loadLocal(src, cwd);
2849
+ }
2850
+ async function loadLocal(src, cwd) {
2851
+ const cwdAbs = resolve2(cwd);
2852
+ const abs = isAbsolute(src) ? resolve2(src) : resolve2(cwdAbs, src);
2853
+ if (abs !== cwdAbs && !abs.startsWith(cwdAbs + sep)) {
2854
+ log.warn("outbound", "image-outside-cwd", { src: src.slice(0, 80) });
2855
+ return { cacheKey: `local:${abs}` };
2856
+ }
2857
+ const ext = extname(abs).slice(1).toLowerCase();
2858
+ if (!ALLOWED_EXT.has(ext)) {
2859
+ log.warn("outbound", "image-ext", { ext, src: src.slice(0, 80) });
2860
+ return { cacheKey: `local:${abs}` };
2861
+ }
2862
+ let size;
2863
+ let mtimeMs;
2864
+ try {
2865
+ const st = await stat2(abs);
2866
+ if (!st.isFile()) throw new Error("not a file");
2867
+ size = st.size;
2868
+ mtimeMs = st.mtimeMs;
2869
+ } catch {
2870
+ log.warn("outbound", "image-missing", { src: src.slice(0, 80) });
2871
+ return { cacheKey: `local:${abs}` };
2872
+ }
2873
+ if (size === 0 || size > MAX_BYTES) {
2874
+ log.warn("outbound", "image-size", { size, src: src.slice(0, 80) });
2875
+ return { cacheKey: `local:${abs}:${size}` };
2876
+ }
2877
+ const buffer = await readFile5(abs);
2878
+ return { buffer, cacheKey: `local:${abs}:${mtimeMs}:${size}` };
2879
+ }
2880
+ async function loadRemote(url) {
2881
+ const cacheKey = `url:${url}`;
2882
+ const ctrl = new AbortController();
2883
+ const timer = setTimeout(() => ctrl.abort(), DOWNLOAD_TIMEOUT_MS);
2884
+ try {
2885
+ const res = await fetch(url, { signal: ctrl.signal, redirect: "follow" });
2886
+ if (!res.ok) {
2887
+ log.warn("outbound", "image-http", { url: url.slice(0, 80), status: res.status });
2888
+ return { cacheKey };
2889
+ }
2890
+ const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
2891
+ if (ct && !ct.startsWith("image/")) {
2892
+ log.warn("outbound", "image-ctype", { ct, url: url.slice(0, 80) });
2893
+ return { cacheKey };
2894
+ }
2895
+ const declared = Number(res.headers.get("content-length") ?? 0);
2896
+ if (declared > MAX_BYTES) {
2897
+ log.warn("outbound", "image-size", { declared, url: url.slice(0, 80) });
2898
+ return { cacheKey };
2899
+ }
2900
+ const ab = await res.arrayBuffer();
2901
+ if (ab.byteLength === 0 || ab.byteLength > MAX_BYTES) {
2902
+ log.warn("outbound", "image-size", { size: ab.byteLength, url: url.slice(0, 80) });
2903
+ return { cacheKey };
2904
+ }
2905
+ return { buffer: Buffer.from(ab), cacheKey };
2906
+ } catch (err) {
2907
+ log.warn("outbound", "image-fetch", { url: url.slice(0, 80), err: String(err) });
2908
+ return { cacheKey };
2909
+ } finally {
2910
+ clearTimeout(timer);
2911
+ }
2912
+ }
2913
+ async function uploadBuffer(channel, buffer) {
2914
+ const res = await channel.rawClient.im.v1.image.create({
2915
+ data: { image_type: "message", image: buffer }
2916
+ });
2917
+ const key = res?.image_key ?? res?.data?.image_key;
2918
+ if (!key) {
2919
+ log.warn("outbound", "image-no-key", { res: JSON.stringify(res).slice(0, 120) });
2920
+ return void 0;
2921
+ }
2922
+ return key;
2923
+ }
2924
+
2925
+ // src/project/registry.ts
2926
+ import { mkdir as mkdir4, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2927
+ import { dirname as dirname5 } from "path";
2928
+ function defaultNoMention(p) {
2929
+ return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
2930
+ }
2931
+ var FILE_VERSION2 = 1;
2932
+ async function read() {
2933
+ try {
2934
+ const text = await readFile6(paths.projectsFile, "utf8");
2935
+ const parsed = JSON.parse(text);
2936
+ return Array.isArray(parsed.projects) ? parsed.projects : [];
2937
+ } catch (err) {
2938
+ if (err.code === "ENOENT") return [];
2939
+ throw err;
2940
+ }
2941
+ }
2942
+ async function write(projects) {
2943
+ await mkdir4(dirname5(paths.projectsFile), { recursive: true });
2944
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
2945
+ const body = { version: FILE_VERSION2, projects };
2946
+ await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
2947
+ `, "utf8");
2948
+ await rename4(tmp, paths.projectsFile);
2949
+ }
2950
+ async function listProjects() {
2951
+ return read();
2952
+ }
2953
+ async function getProjectByChatId(chatId) {
2954
+ return (await read()).find((p) => p.chatId === chatId);
2955
+ }
2956
+ async function getProjectByName(name) {
2957
+ return (await read()).find((p) => p.name === name);
2958
+ }
2959
+ async function addProject(p) {
2960
+ const projects = await read();
2961
+ if (projects.some((x) => x.name === p.name)) {
2962
+ throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
2963
+ }
2964
+ if (p.chatId) {
2965
+ const bound = projects.find((x) => x.chatId === p.chatId);
2966
+ if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
2967
+ }
2968
+ projects.push(p);
2969
+ await write(projects);
2970
+ }
2971
+ async function updateProject(name, patch) {
2972
+ const projects = await read();
2973
+ const p = projects.find((x) => x.name === name);
2974
+ if (!p) return;
2975
+ const target = p;
2976
+ for (const [k, v] of Object.entries(patch)) {
2977
+ if (v !== void 0) target[k] = v;
2978
+ }
2979
+ await write(projects);
2980
+ }
2981
+ async function removeProject(name) {
2982
+ const projects = await read();
2983
+ const idx = projects.findIndex((p) => p.name === name);
2984
+ if (idx === -1) return void 0;
2985
+ const [removed] = projects.splice(idx, 1);
2986
+ await write(projects);
2987
+ return removed;
2988
+ }
2989
+
2627
2990
  // src/card/dm-cards.ts
2628
2991
  function openChatUrl(chatId) {
2629
2992
  return `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(chatId)}`;
@@ -2633,6 +2996,7 @@ var DM = {
2633
2996
  menu: "dm.menu",
2634
2997
  newProject: "dm.newProject",
2635
2998
  newProjectSubmit: "dm.newProject.submit",
2999
+ joinGroupSubmit: "dm.joinGroup.submit",
2636
3000
  projects: "dm.projects",
2637
3001
  settings: "dm.settings",
2638
3002
  doctor: "dm.doctor",
@@ -2780,11 +3144,33 @@ function scopeDiagnosis(i) {
2780
3144
  return [md("- \u98DE\u4E66\u6743\u9650\uFF1A\u2705 \u5FC5\u9700\u6743\u9650\u5DF2\u5168\u90E8\u5F00\u901A")];
2781
3145
  }
2782
3146
  return [
2783
- md(`- \u98DE\u4E66\u6743\u9650\uFF1A\u274C \u7F3A ${i.missingScopes.length} \u9879 \u2014\u2014 \u5F00\u901A\u524D\u76F8\u5173\u529F\u80FD\uFF08\u6536\u53D1\u6D88\u606F / \u5361\u7247 / \u5EFA\u7FA4\u7B49\uFF09\u4E0D\u53EF\u7528`),
2784
- note(`\u5F85\u5F00\u901A\uFF1A${i.missingScopes.join("\u3000")}`),
3147
+ md(`- \u98DE\u4E66\u6743\u9650\uFF1A\u274C \u7F3A ${i.missingScopes.length} \u9879 \u2014\u2014 \u5F00\u901A\u524D\u76F8\u5173\u529F\u80FD\uFF08\u6536\u53D1\u6D88\u606F / \u5361\u7247 / \u56FE\u7247 / \u5EFA\u7FA4\u7B49\uFF09\u4E0D\u53EF\u7528`),
3148
+ note(`\u5F85\u5F00\u901A\uFF1A
3149
+ ${i.missingScopes.map((s) => `\xB7 ${labelScope(s)}`).join("\n")}`),
2785
3150
  actions([linkButton("\u{1F511} \u4E00\u952E\u53BB\u5F00\u901A\u8FD9\u4E9B\u6743\u9650", i.scopeGrantUrl)])
2786
3151
  ];
2787
3152
  }
3153
+ function joinFeatureDiagnosis(i) {
3154
+ const out = [md("**\u52A0\u5165\u5B58\u91CF\u7FA4\uFF08\u53EF\u9009\uFF09**")];
3155
+ if (i.missingJoinScopes === void 0) {
3156
+ out.push(md("- \u6743\u9650\uFF1A\u26A0\uFE0F \u672A\u80FD\u81EA\u52A8\u68C0\u67E5\uFF08\u51ED\u636E\u5931\u6548\u6216\u7F51\u7EDC\u4E0D\u901A\uFF09"), actions([linkButton("\u{1F511} \u53BB\u5F00\u901A", i.joinScopeGrantUrl)]));
3157
+ } else if (i.missingJoinScopes.length === 0) {
3158
+ out.push(md("- \u6743\u9650\uFF1A\u2705 \u5DF2\u5F00\u901A\uFF08`im:chat:readonly` / `im:chat.members:write_only`\uFF09"));
3159
+ } else {
3160
+ out.push(
3161
+ md(`- \u6743\u9650\uFF1A\u274C \u7F3A ${i.missingJoinScopes.length} \u9879 \u2014\u2014 \u5F00\u901A\u540E\u624D\u80FD\u628A\u6211\u52A0\u8FDB\u5DF2\u6709\u7FA4\uFF08\u7ED1\u5B9A / \u9000\u7FA4\uFF09`),
3162
+ note(`\u5F85\u5F00\u901A\uFF1A
3163
+ ${i.missingJoinScopes.map((s) => `\xB7 ${labelScope(s)}`).join("\n")}`),
3164
+ actions([linkButton("\u{1F511} \u4E00\u952E\u5F00\u901A\u8FD9\u4E24\u9879\u6743\u9650", i.joinScopeGrantUrl)])
3165
+ );
3166
+ }
3167
+ out.push(
3168
+ note(
3169
+ "\u26A0\uFE0F \u8FD8\u9700\u5728\u540E\u53F0\u300C\u4E8B\u4EF6\u4E0E\u56DE\u8C03\u300D\u624B\u52A8\u8BA2\u9605 `im.chat.member.bot.added_v1`\uFF08\u88AB\u62C9\u8FDB\u7FA4\u2192\u63A8\u9001\u7ED1\u5B9A\u5361\uFF09\u548C `im.chat.member.bot.deleted_v1`\uFF08\u88AB\u79FB\u51FA\u7FA4\u2192\u81EA\u52A8\u89E3\u7ED1\uFF09\u2014\u2014 \u98DE\u4E66\u65E0\u67E5\u8BE2\u63A5\u53E3\uFF0C\u8FD9\u91CC\u65E0\u6CD5\u81EA\u52A8\u68C0\u6D4B\u3002"
3170
+ )
3171
+ );
3172
+ return out;
3173
+ }
2788
3174
  function codexDiagnosePrompt(i) {
2789
3175
  return [
2790
3176
  "\u6211\u5728\u7528 feishu-codex-bridge\uFF08\u98DE\u4E66 \u2194 \u672C\u5730 Codex \u6865\u63A5\uFF09\u9047\u5230\u95EE\u9898\uFF0C\u8BF7\u5E2E\u6211\u5B9A\u4F4D\u539F\u56E0\u5E76\u7ED9\u51FA\u4FEE\u590D\u6B65\u9AA4\u3002",
@@ -2827,6 +3213,8 @@ function buildDoctorCard(i) {
2827
3213
  ...scopeDiagnosis(i),
2828
3214
  note(`bridge v${i.bridgeVer}\u3000\xB7\u3000Node ${i.node}\u3000\xB7\u3000${i.platform}`),
2829
3215
  hr(),
3216
+ ...joinFeatureDiagnosis(i),
3217
+ hr(),
2830
3218
  md("**\u65E5\u5FD7\u8DEF\u5F84**"),
2831
3219
  note(`\u540E\u53F0\u5B88\u62A4\u8F93\u51FA\uFF1A\`${i.logStdout}\``),
2832
3220
  note(`\u540E\u53F0\u5B88\u62A4\u9519\u8BEF\uFF1A\`${i.logStderr}\``),
@@ -2860,14 +3248,35 @@ function buildNewProjectFormCard(opts = {}) {
2860
3248
  );
2861
3249
  return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "turquoise" } });
2862
3250
  }
3251
+ function buildJoinGroupFormCard(opts) {
3252
+ const elements = [];
3253
+ if (opts.error) elements.push(md(`\u274C **\u7ED1\u5B9A\u5931\u8D25**\uFF1A${opts.error}`));
3254
+ elements.push(
3255
+ md("\u6211\u5DF2\u88AB\u52A0\u5165\u8FD9\u4E2A\u7FA4\u3002\u586B\u4E00\u4E0B\u8981\u7ED1\u5B9A\u7684\u9879\u76EE\u4FE1\u606F\u5373\u53EF\u5F00\u59CB\u7528\u3002"),
3256
+ md("\u9879\u76EE\u540D\u9ED8\u8BA4\u7528\u7FA4\u540D\uFF0C\u53EF\u6539\u3002**\u6587\u4EF6\u5939\u8DEF\u5F84\u7559\u7A7A** = \u81EA\u52A8\u65B0\u5EFA\u7A7A\u767D\u9879\u76EE\uFF1B**\u586B\u7EDD\u5BF9\u8DEF\u5F84** = \u7528\u7535\u8111\u4E0A\u5DF2\u6709\u7684\u6587\u4EF6\u5939\u3002"),
3257
+ form("join_group", [
3258
+ input({ name: "name", label: "\u9879\u76EE\u540D", placeholder: "my-app", value: opts.name, required: true }),
3259
+ input({ name: "cwd", label: "\u6587\u4EF6\u5939\u8DEF\u5F84\uFF08\u9009\u586B\uFF0C\u7559\u7A7A\u81EA\u52A8\u65B0\u5EFA\uFF09", placeholder: "/Users/you/code/my-app", value: opts.cwd }),
3260
+ note("\u9009\u7FA4\u7C7B\u578B(\u76F4\u63A5\u70B9\u5BF9\u5E94\u6309\u94AE\u521B\u5EFA)\uFF1A\u{1F465} \u591A\u8BDD\u9898\u7FA4 = @\u6211\u5F00\u8BDD\u9898\u3001\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF1B\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4 = \u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\u3001\u8FDE\u7EED\u4E0A\u4E0B\u6587\uFF08\u9ED8\u8BA4\u4E0D\u514D@\uFF09\u3002"),
3261
+ actions([
3262
+ submitButton("\u{1F465} \u7ED1\u5B9A\xB7\u591A\u8BDD\u9898\u7FA4", { a: DM.joinGroupSubmit, kind: "multi", chatId: opts.chatId }, "primary", "submit_multi"),
3263
+ submitButton("\u{1F4AC} \u7ED1\u5B9A\xB7\u5355\u4F1A\u8BDD\u7FA4", { a: DM.joinGroupSubmit, kind: "single", chatId: opts.chatId }, "primary", "submit_single")
3264
+ ])
3265
+ ])
3266
+ );
3267
+ return card(elements, { header: { title: "\u{1F517} \u7ED1\u5B9A\u5DF2\u6709\u7FA4", template: "turquoise" } });
3268
+ }
2863
3269
  function buildNewProjectDoneCard(p) {
3270
+ const joined = (p.origin ?? "created") === "joined";
3271
+ const verb = joined ? "\u5DF2\u7ED1\u5B9A\u7FA4" : "\u5DF2\u521B\u5EFA\u9879\u76EE";
3272
+ const title = joined ? "\u{1F517} \u7ED1\u5B9A\u5DF2\u6709\u7FA4" : "\u2795 \u65B0\u5EFA\u9879\u76EE";
2864
3273
  const elements = [
2865
- md(`\u2705 \u5DF2\u521B\u5EFA\u9879\u76EE **${p.name}**${p.blank ? " _(\u7A7A\u767D\u9879\u76EE)_" : ""}`),
3274
+ md(`\u2705 ${verb} **${p.name}**${p.blank ? " _(\u7A7A\u767D\u9879\u76EE)_" : ""}`),
2866
3275
  note(`\u{1F4C2} \`${p.cwd}\` \xB7 ${kindLabel(p.kind)}`),
2867
- md(p.chatId ? "\u7FA4\u5DF2\u5EFA\u597D \u{1F449} \u53BB\u9879\u76EE\u7FA4\u91CC **@\u6211** \u5E72\u6D3B\u3002" : "\u53D1\u6211\u4EFB\u610F\u6D88\u606F\u53EF\u518D\u6B21\u6253\u5F00\u7BA1\u7406\u53F0\u3002")
3276
+ md(p.chatId ? "\u{1F449} \u53BB\u7FA4\u91CC **@\u6211** \u5E72\u6D3B\u3002" : "\u53D1\u6211\u4EFB\u610F\u6D88\u606F\u53EF\u518D\u6B21\u6253\u5F00\u7BA1\u7406\u53F0\u3002")
2868
3277
  ];
2869
3278
  if (p.chatId) elements.push(actions([linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary")]));
2870
- return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "green" } });
3279
+ return card(elements, { header: { title, template: "green" } });
2871
3280
  }
2872
3281
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
2873
3282
  if (projects.length === 0) {
@@ -2882,7 +3291,7 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
2882
3291
  elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
2883
3292
  elements.push(
2884
3293
  note(
2885
- p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)} \xB7 \u514D@\uFF1A${p.noMention ?? true ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
3294
+ p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)}${(p.origin ?? "created") === "joined" ? " \xB7 \u{1F517}\u5DF2\u52A0\u5165" : ""} \xB7 \u514D@\uFF1A${p.noMention ?? defaultNoMention(p) ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
2886
3295
  )
2887
3296
  );
2888
3297
  const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
@@ -2905,11 +3314,12 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
2905
3314
  elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
2906
3315
  return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
2907
3316
  }
2908
- function buildRmConfirmCard(name) {
3317
+ function buildRmConfirmCard(name, origin) {
3318
+ const note_ = (origin ?? "created") === "joined" ? "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u786E\u8BA4\u540E**\u6211\u4F1A\u9000\u51FA\u8BE5\u7FA4**\uFF08\u7FA4\u662F\u4F60\u4EEC\u7684\uFF0C\u4E0D\u4F1A\u89E3\u6563\uFF09\u3002" : "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002";
2909
3319
  return card(
2910
3320
  [
2911
3321
  md(`\u786E\u5B9A\u5220\u9664\u9879\u76EE **${name}**\uFF1F`),
2912
- note("\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002"),
3322
+ note(note_),
2913
3323
  actions([
2914
3324
  button("\u2705 \u786E\u8BA4\u5220\u9664", { a: DM.rmDo, n: name }, "danger"),
2915
3325
  button("\u53D6\u6D88", { a: DM.rmCancel })
@@ -2957,7 +3367,7 @@ function buildSettingsCard(cfg) {
2957
3367
  }
2958
3368
  function buildGroupSettingsCard(project) {
2959
3369
  const kind = project.kind ?? "multi";
2960
- const noMention = project.noMention ?? true;
3370
+ const noMention = project.noMention ?? defaultNoMention(project);
2961
3371
  const scopeNote = kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u7684\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002";
2962
3372
  return card(
2963
3373
  [
@@ -2977,16 +3387,16 @@ function buildGroupSettingsCard(project) {
2977
3387
  // src/service/update.ts
2978
3388
  import { execFile, spawn as spawn5 } from "child_process";
2979
3389
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
2980
- import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
3390
+ import { dirname as dirname7, join as join8, resolve as resolve4 } from "path";
2981
3391
  import { fileURLToPath as fileURLToPath3 } from "url";
2982
3392
  import { promisify } from "util";
2983
3393
 
2984
3394
  // src/service/launchd.ts
2985
3395
  import { spawn as spawn4, spawnSync } from "child_process";
2986
3396
  import { existsSync as existsSync4 } from "fs";
2987
- import { appendFile, mkdir as mkdir4, rm as rm2, writeFile as writeFile4 } from "fs/promises";
3397
+ import { appendFile, mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
2988
3398
  import { homedir as homedir3, userInfo as userInfo2 } from "os";
2989
- import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
3399
+ import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
2990
3400
  import { fileURLToPath as fileURLToPath2 } from "url";
2991
3401
  var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
2992
3402
  function launchAgentPlistPath() {
@@ -2999,8 +3409,8 @@ function serviceStderrPath() {
2999
3409
  return join7(paths.appDir, "service.err.log");
3000
3410
  }
3001
3411
  function resolveCliBinPath() {
3002
- const distDir = dirname5(fileURLToPath2(import.meta.url));
3003
- return resolve2(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3412
+ const distDir = dirname6(fileURLToPath2(import.meta.url));
3413
+ return resolve3(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3004
3414
  }
3005
3415
  function escapeXml(value) {
3006
3416
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -3040,9 +3450,9 @@ function buildPlist() {
3040
3450
  }
3041
3451
  async function installLaunchd() {
3042
3452
  const plistPath = launchAgentPlistPath();
3043
- await mkdir4(dirname5(plistPath), { recursive: true });
3453
+ await mkdir5(dirname6(plistPath), { recursive: true });
3044
3454
  await ensureLogFiles();
3045
- await writeFile4(plistPath, buildPlist(), "utf8");
3455
+ await writeFile5(plistPath, buildPlist(), "utf8");
3046
3456
  if (isLoaded()) {
3047
3457
  const bootout = runLaunchctl(["bootout", serviceTarget()]);
3048
3458
  if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
@@ -3124,7 +3534,7 @@ async function waitUntilUnloaded(timeoutMs = 5e3) {
3124
3534
  throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
3125
3535
  }
3126
3536
  async function ensureLogFiles() {
3127
- await mkdir4(paths.appDir, { recursive: true });
3537
+ await mkdir5(paths.appDir, { recursive: true });
3128
3538
  await appendFile(serviceStdoutPath(), "");
3129
3539
  await appendFile(serviceStderrPath(), "");
3130
3540
  }
@@ -3166,7 +3576,7 @@ function getServiceAdapter() {
3166
3576
  var execFileP = promisify(execFile);
3167
3577
  var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
3168
3578
  function pkgRoot() {
3169
- return resolve3(dirname6(fileURLToPath3(import.meta.url)), "..");
3579
+ return resolve4(dirname7(fileURLToPath3(import.meta.url)), "..");
3170
3580
  }
3171
3581
  function pkgJson() {
3172
3582
  try {
@@ -3231,68 +3641,10 @@ async function restartDaemon() {
3231
3641
  await getServiceAdapter().restart();
3232
3642
  }
3233
3643
 
3234
- // src/project/registry.ts
3235
- import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3236
- import { dirname as dirname7 } from "path";
3237
- var FILE_VERSION2 = 1;
3238
- async function read() {
3239
- try {
3240
- const text = await readFile5(paths.projectsFile, "utf8");
3241
- const parsed = JSON.parse(text);
3242
- return Array.isArray(parsed.projects) ? parsed.projects : [];
3243
- } catch (err) {
3244
- if (err.code === "ENOENT") return [];
3245
- throw err;
3246
- }
3247
- }
3248
- async function write(projects) {
3249
- await mkdir5(dirname7(paths.projectsFile), { recursive: true });
3250
- const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
3251
- const body = { version: FILE_VERSION2, projects };
3252
- await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
3253
- `, "utf8");
3254
- await rename4(tmp, paths.projectsFile);
3255
- }
3256
- async function listProjects() {
3257
- return read();
3258
- }
3259
- async function getProjectByChatId(chatId) {
3260
- return (await read()).find((p) => p.chatId === chatId);
3261
- }
3262
- async function getProjectByName(name) {
3263
- return (await read()).find((p) => p.name === name);
3264
- }
3265
- async function addProject(p) {
3266
- const projects = await read();
3267
- if (projects.some((x) => x.name === p.name)) {
3268
- throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3269
- }
3270
- projects.push(p);
3271
- await write(projects);
3272
- }
3273
- async function updateProject(name, patch) {
3274
- const projects = await read();
3275
- const p = projects.find((x) => x.name === name);
3276
- if (!p) return;
3277
- const target = p;
3278
- for (const [k, v] of Object.entries(patch)) {
3279
- if (v !== void 0) target[k] = v;
3280
- }
3281
- await write(projects);
3282
- }
3283
- async function removeProject(name) {
3284
- const projects = await read();
3285
- const idx = projects.findIndex((p) => p.name === name);
3286
- if (idx === -1) return void 0;
3287
- const [removed] = projects.splice(idx, 1);
3288
- await write(projects);
3289
- return removed;
3290
- }
3291
-
3292
3644
  // src/project/lifecycle.ts
3293
3645
  import { mkdir as mkdir6 } from "fs/promises";
3294
3646
  import { existsSync as existsSync6 } from "fs";
3295
- import { isAbsolute, join as join9, resolve as resolve4 } from "path";
3647
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
3296
3648
 
3297
3649
  // src/project/git-info.ts
3298
3650
  import { execFile as execFile2 } from "child_process";
@@ -3393,21 +3745,23 @@ var HELP_DOC_URL = "https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g";
3393
3745
  async function onboardGroup(channel, project) {
3394
3746
  const kind = project.kind ?? "multi";
3395
3747
  const chatId = project.chatId;
3748
+ const decorate = (project.origin ?? "created") !== "joined";
3396
3749
  try {
3397
- const content = JSON.stringify(buildWelcomeCard(kind, HELP_DOC_URL || void 0));
3750
+ const noMention = project.noMention ?? defaultNoMention(project);
3751
+ const content = JSON.stringify(buildWelcomeCard(kind, HELP_DOC_URL || void 0, noMention));
3398
3752
  const sent = await channel.rawClient.im.v1.message.create({
3399
3753
  params: { receive_id_type: "chat_id" },
3400
3754
  data: { receive_id: chatId, msg_type: "interactive", content }
3401
3755
  });
3402
3756
  const messageId = sent.data?.message_id;
3403
- if (messageId) {
3757
+ if (messageId && decorate) {
3404
3758
  await channel.rawClient.im.v1.pin.create({ data: { message_id: messageId } });
3405
3759
  log.info("project", "onboard-pin", { name: project.name });
3406
3760
  }
3407
3761
  } catch (err) {
3408
3762
  log.fail("project", err, { phase: "onboard-welcome" });
3409
3763
  }
3410
- if (HELP_DOC_URL) {
3764
+ if (decorate && HELP_DOC_URL) {
3411
3765
  try {
3412
3766
  await channel.rawClient.im.v1.chatTab.create({
3413
3767
  path: { chat_id: chatId },
@@ -3423,21 +3777,21 @@ async function onboardGroup(channel, project) {
3423
3777
  }
3424
3778
 
3425
3779
  // src/project/lifecycle.ts
3780
+ async function resolveCwd(name, existingPath) {
3781
+ if (existingPath) {
3782
+ const cwd2 = isAbsolute2(existingPath) ? existingPath : resolve5(existingPath);
3783
+ if (!existsSync6(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
3784
+ return { cwd: cwd2, blank: false };
3785
+ }
3786
+ const cwd = join9(paths.projectsRootDir, name);
3787
+ await mkdir6(cwd, { recursive: true });
3788
+ return { cwd, blank: true };
3789
+ }
3426
3790
  async function createProject(channel, input2) {
3427
3791
  const name = input2.name.trim();
3428
3792
  if (!name) throw new Error("\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A");
3429
3793
  if (await getProjectByName(name)) throw new Error(`\u9879\u76EE\u540D\u300C${name}\u300D\u5DF2\u5B58\u5728\uFF0C\u6362\u4E2A\u540D\u6216\u7528 /projects \u770B\u5DF2\u6709\u7684`);
3430
- let cwd;
3431
- let blank;
3432
- if (input2.existingPath) {
3433
- cwd = isAbsolute(input2.existingPath) ? input2.existingPath : resolve4(input2.existingPath);
3434
- if (!existsSync6(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
3435
- blank = false;
3436
- } else {
3437
- cwd = join9(paths.projectsRootDir, name);
3438
- await mkdir6(cwd, { recursive: true });
3439
- blank = true;
3440
- }
3794
+ const { cwd, blank } = await resolveCwd(name, input2.existingPath);
3441
3795
  const res = await channel.rawClient.im.v1.chat.create({
3442
3796
  params: { user_id_type: "open_id" },
3443
3797
  data: { name, user_id_list: [input2.ownerOpenId] }
@@ -3449,13 +3803,35 @@ async function createProject(channel, input2) {
3449
3803
  params: { member_id_type: "open_id" },
3450
3804
  data: { manager_ids: [input2.ownerOpenId] }
3451
3805
  }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
3452
- const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi" };
3806
+ const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi", origin: "created" };
3453
3807
  await addProject(project);
3454
3808
  log.info("project", "create", { name, chatId, cwd, blank });
3455
3809
  await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
3456
3810
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
3457
3811
  return project;
3458
3812
  }
3813
+ async function joinExistingGroup(channel, input2) {
3814
+ const name = input2.name.trim();
3815
+ if (!name) throw new Error("\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A");
3816
+ if (await getProjectByName(name)) throw new Error(`\u9879\u76EE\u540D\u300C${name}\u300D\u5DF2\u5B58\u5728\uFF0C\u6362\u4E2A\u540D\u6216\u7528 /projects \u770B\u5DF2\u6709\u7684`);
3817
+ const bound = await getProjectByChatId(input2.chatId);
3818
+ if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
3819
+ const { cwd, blank } = await resolveCwd(name, input2.existingPath);
3820
+ const project = {
3821
+ name,
3822
+ chatId: input2.chatId,
3823
+ cwd,
3824
+ blank,
3825
+ createdAt: Date.now(),
3826
+ kind: input2.kind ?? "multi",
3827
+ origin: "joined",
3828
+ addedBy: input2.addedBy
3829
+ };
3830
+ await addProject(project);
3831
+ log.info("project", "join", { name, chatId: input2.chatId, cwd, blank, kind: project.kind });
3832
+ await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard-join" }));
3833
+ return project;
3834
+ }
3459
3835
 
3460
3836
  // src/project/group-ops.ts
3461
3837
  async function transferOwnership(channel, chatId, toOpenId) {
@@ -3466,14 +3842,21 @@ async function transferOwnership(channel, chatId, toOpenId) {
3466
3842
  });
3467
3843
  log.info("project", "owner-transfer", { chatId: chatId.slice(-6), to: toOpenId.slice(-6) });
3468
3844
  }
3845
+ async function leaveChat(channel, chatId) {
3846
+ await channel.rawClient.request({
3847
+ method: "PATCH",
3848
+ url: `/open-apis/im/v1/chats/${encodeURIComponent(chatId)}/members/me_leave`
3849
+ });
3850
+ log.info("project", "leave-chat", { chatId: chatId.slice(-6) });
3851
+ }
3469
3852
 
3470
3853
  // src/bot/session-store.ts
3471
- import { mkdir as mkdir7, readFile as readFile6, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3854
+ import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3472
3855
  import { dirname as dirname8 } from "path";
3473
3856
  var FILE_VERSION3 = 1;
3474
3857
  async function read2() {
3475
3858
  try {
3476
- const text = await readFile6(paths.sessionsFile, "utf8");
3859
+ const text = await readFile7(paths.sessionsFile, "utf8");
3477
3860
  const parsed = JSON.parse(text);
3478
3861
  return Array.isArray(parsed.sessions) ? parsed.sessions : [];
3479
3862
  } catch (err) {
@@ -3529,9 +3912,9 @@ async function handleDmConsole(channel, cfg, msg) {
3529
3912
  }
3530
3913
 
3531
3914
  // src/bot/media.ts
3532
- import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat2 } from "fs/promises";
3915
+ import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat3 } from "fs/promises";
3533
3916
  import { join as join10 } from "path";
3534
- var MAX_IMAGES = 9;
3917
+ var MAX_IMAGES2 = 9;
3535
3918
  var MEDIA_TTL_MS = 60 * 6e4;
3536
3919
  var EXT_BY_CONTENT_TYPE = {
3537
3920
  "image/png": "png",
@@ -3564,7 +3947,7 @@ async function collectInboundImages(channel, msg) {
3564
3947
  }
3565
3948
  const out = [];
3566
3949
  let index = 0;
3567
- for (const ref of refs.slice(0, MAX_IMAGES)) {
3950
+ for (const ref of refs.slice(0, MAX_IMAGES2)) {
3568
3951
  const path = await downloadOne(channel, ref, index++);
3569
3952
  if (path) out.push(path);
3570
3953
  }
@@ -3671,7 +4054,7 @@ async function pruneOldMedia() {
3671
4054
  for (const name of entries) {
3672
4055
  const file = join10(paths.mediaDir, name);
3673
4056
  try {
3674
- const st = await stat2(file);
4057
+ const st = await stat3(file);
3675
4058
  if (st.mtimeMs < cutoff) await rm3(file, { force: true });
3676
4059
  } catch {
3677
4060
  }
@@ -3996,11 +4379,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3996
4379
  log.info("intake", "reject", { reason: "not_allowed", chatId: msg.chatId.slice(-6) });
3997
4380
  return;
3998
4381
  }
4382
+ if (!project) {
4383
+ log.info("intake", "unbound-group", { chatId: msg.chatId.slice(-6), atBot: msg.mentionedBot });
4384
+ if (msg.mentionedBot) {
4385
+ await channel.send(
4386
+ msg.chatId,
4387
+ { markdown: "\u672C\u7FA4\u8FD8\u6CA1\u7ED1\u5B9A\u4E3A\u9879\u76EE\u3002\u8BF7**\u628A\u6211\u62C9\u8FDB\u7FA4\u7684\u7BA1\u7406\u5458**\u5728\u4E0E\u6211\u7684\u79C1\u804A\u91CC\u5B8C\u6210\u7ED1\u5B9A\u540E\u518D @\u6211\u3002" },
4388
+ { replyTo: msg.messageId }
4389
+ ).catch(() => void 0);
4390
+ }
4391
+ return;
4392
+ }
3999
4393
  const text = msg.content.trim();
4000
4394
  const cmd = parseCommand(text);
4001
4395
  if ((project?.kind ?? "multi") === "single") {
4002
4396
  if (cmd === "help") {
4003
- await postHelpCard(msg, "single");
4397
+ await postHelpCard(msg, "single", false, project);
4004
4398
  return;
4005
4399
  }
4006
4400
  if (cmd === "settings") {
@@ -4016,7 +4410,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4016
4410
  }
4017
4411
  if (msg.threadId) {
4018
4412
  if (cmd === "help") {
4019
- await postHelpCard(msg, "topic", true);
4413
+ await postHelpCard(msg, "topic", true, project);
4020
4414
  return;
4021
4415
  }
4022
4416
  if (cmd === "model") {
@@ -4027,7 +4421,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4027
4421
  return;
4028
4422
  }
4029
4423
  if (cmd === "help") {
4030
- await postHelpCard(msg, "main");
4424
+ await postHelpCard(msg, "main", false, project);
4031
4425
  return;
4032
4426
  }
4033
4427
  if (cmd === "resume") {
@@ -4050,7 +4444,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4050
4444
  return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
4051
4445
  }
4052
4446
  function shouldRespondWithoutMention(project, msg) {
4053
- if (!(project.noMention ?? true)) return false;
4447
+ if (!(project.noMention ?? defaultNoMention(project))) return false;
4054
4448
  if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
4055
4449
  if ((project.kind ?? "multi") === "single") return true;
4056
4450
  return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
@@ -4247,9 +4641,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4247
4641
  log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
4248
4642
  });
4249
4643
  }
4250
- async function postHelpCard(msg, scope, inThread = false) {
4644
+ async function postHelpCard(msg, scope, inThread = false, project) {
4645
+ const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
4251
4646
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
4252
- await sendManagedCard(channel, msg.chatId, buildHelpCard(scope), msg.messageId, inThread).catch(
4647
+ await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention), msg.messageId, inThread).catch(
4253
4648
  (err) => log.fail("card", err, { cmd: "help", scope })
4254
4649
  );
4255
4650
  log.info("card", "help", { scope });
@@ -4387,6 +4782,32 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4387
4782
  (e) => log.fail("console", e, { phase: "new-project-result" })
4388
4783
  );
4389
4784
  })();
4785
+ }).on(DM.joinGroupSubmit, ({ evt, formValue, value }) => {
4786
+ const op = evt.operator?.openId;
4787
+ if (!dmAdmin(op)) return;
4788
+ const name = String(formValue?.name ?? "").trim();
4789
+ const cwdIn = String(formValue?.cwd ?? "").trim();
4790
+ const chatId = typeof value.chatId === "string" ? value.chatId : "";
4791
+ const kind = value.kind === "single" ? "single" : "multi";
4792
+ void (async () => {
4793
+ let result;
4794
+ if (!chatId)
4795
+ result = buildJoinGroupFormCard({ chatId: "", name, cwd: cwdIn, error: "\u7F3A\u5C11\u7FA4\u6807\u8BC6\uFF0C\u8BF7\u91CD\u65B0\u4ECE\u8FDB\u7FA4\u901A\u77E5\u91CC\u6253\u5F00\u7ED1\u5B9A\u5361" });
4796
+ else if (!name) result = buildJoinGroupFormCard({ chatId, cwd: cwdIn, error: "\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A" });
4797
+ else if (!op) result = buildJoinGroupFormCard({ chatId, name, cwd: cwdIn, error: "\u65E0\u6CD5\u8BC6\u522B\u64CD\u4F5C\u8005\u8EAB\u4EFD" });
4798
+ else {
4799
+ try {
4800
+ const p = await joinExistingGroup(channel, { name, chatId, addedBy: op, existingPath: cwdIn || void 0, kind });
4801
+ log.info("console", "join-group", { name: p.name, blank: p.blank });
4802
+ result = buildNewProjectDoneCard(p);
4803
+ } catch (err) {
4804
+ result = buildJoinGroupFormCard({ chatId, name, cwd: cwdIn, error: err instanceof Error ? err.message : String(err) });
4805
+ }
4806
+ }
4807
+ await sendManagedCard(channel, evt.chatId, result).catch(
4808
+ (e) => log.fail("console", e, { phase: "join-group-result" })
4809
+ );
4810
+ })();
4390
4811
  }).on(DM.projects, ({ evt }) => {
4391
4812
  if (!dmAdmin(evt.operator?.openId)) return;
4392
4813
  patch(evt, renderProjectList);
@@ -4399,6 +4820,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4399
4820
  const secret = await getSecret(secretKeyForApp(app.id)).catch(() => void 0);
4400
4821
  const scopeCheck = secret ? await validateAppCredentials(app.id, secret, app.tenant).catch(() => void 0) : void 0;
4401
4822
  const missingScopes = scopeCheck?.missingScopes;
4823
+ const missingJoinScopes = scopeCheck?.missingJoinScopes;
4402
4824
  const info = {
4403
4825
  codexOk: await backend.isAvailable().catch(() => false),
4404
4826
  codexVer: codexBin ? codexVersion(codexBin) : null,
@@ -4415,7 +4837,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4415
4837
  app.id,
4416
4838
  app.tenant,
4417
4839
  missingScopes && missingScopes.length ? missingScopes : void 0
4418
- )
4840
+ ),
4841
+ missingJoinScopes,
4842
+ // 「加入存量群」按钮恒预选这两项 opt-in scope(它们不在必需清单里)。
4843
+ joinScopeGrantUrl: buildScopeGrantUrl(app.id, app.tenant, JOIN_GROUP_SCOPES)
4419
4844
  };
4420
4845
  await sendManagedCard(channel, evt.chatId, buildDoctorCard(info), evt.messageId).catch(
4421
4846
  (err) => log.fail("console", err, { cmd: "doctor" })
@@ -4476,7 +4901,8 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
4476
4901
  }).on(DM.rmConfirm, async ({ evt, value }) => {
4477
4902
  const name = typeof value.n === "string" ? value.n : void 0;
4478
4903
  if (!dmAdmin(evt.operator?.openId) || !name) return;
4479
- await patch(evt, buildRmConfirmCard(name));
4904
+ const proj = (await listProjects()).find((p) => p.name === name);
4905
+ await patch(evt, buildRmConfirmCard(name, proj?.origin));
4480
4906
  }).on(DM.rmCancel, ({ evt }) => {
4481
4907
  if (!dmAdmin(evt.operator?.openId)) return;
4482
4908
  patch(evt, renderProjectList);
@@ -4486,15 +4912,25 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
4486
4912
  if (!dmAdmin(op) || !name) return;
4487
4913
  patch(evt, async () => {
4488
4914
  const removed = await removeProject(name);
4489
- let transferred = false;
4490
- if (removed?.chatId && op) {
4491
- transferred = await transferOwnership(channel, removed.chatId, op).then(() => true).catch((err) => {
4492
- log.fail("console", err, { phase: "owner-transfer" });
4915
+ let tail;
4916
+ if (removed && (removed.origin ?? "created") === "joined") {
4917
+ const left = removed.chatId ? await leaveChat(channel, removed.chatId).then(() => true).catch((err) => {
4918
+ log.fail("console", err, { phase: "leave-chat" });
4493
4919
  return false;
4494
- });
4920
+ }) : false;
4921
+ log.info("console", "rm", { name, origin: "joined", left });
4922
+ tail = left ? "\u6211\u5DF2\u9000\u51FA\u8BE5\u7FA4\uFF08\u7FA4\u662F\u4F60\u4EEC\u7684\uFF0C\u4E0D\u4F1A\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u6211\u9000\u7FA4\u5931\u8D25\uFF08\u53EF\u80FD\u6743\u9650\u4E0D\u8DB3\uFF09\uFF0C\u53EF\u5728\u7FA4\u91CC\u624B\u52A8\u628A\u6211\u79FB\u9664\u3002";
4923
+ } else {
4924
+ let transferred = false;
4925
+ if (removed?.chatId && op) {
4926
+ transferred = await transferOwnership(channel, removed.chatId, op).then(() => true).catch((err) => {
4927
+ log.fail("console", err, { phase: "owner-transfer" });
4928
+ return false;
4929
+ });
4930
+ }
4931
+ log.info("console", "rm", { name, origin: "created", transferred });
4932
+ tail = transferred ? "\u7FA4\u4E3B\u5DF2\u8F6C\u7ED9\u4F60 \u2192 \u8BF7\u5728\u98DE\u4E66\u91CC**\u81EA\u884C\u89E3\u6563\u8BE5\u7FA4**\uFF08\u673A\u5668\u4EBA\u4E0D\u4E3B\u52A8\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u7FA4\u4E3B\u8F6C\u8BA9\u5931\u8D25\uFF08\u53EF\u80FD bot \u975E\u7FA4\u4E3B\uFF09\uFF0C\u8BF7\u7528\u300C\u{1F6AA} \u7FA4\u7BA1\u7406\u300D\u624B\u52A8\u8F6C\u8BA9\u540E\u89E3\u6563\u3002";
4495
4933
  }
4496
- log.info("console", "rm", { name, transferred });
4497
- const tail = transferred ? "\u7FA4\u4E3B\u5DF2\u8F6C\u7ED9\u4F60 \u2192 \u8BF7\u5728\u98DE\u4E66\u91CC**\u81EA\u884C\u89E3\u6563\u8BE5\u7FA4**\uFF08\u673A\u5668\u4EBA\u4E0D\u4E3B\u52A8\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u7FA4\u4E3B\u8F6C\u8BA9\u5931\u8D25\uFF08\u53EF\u80FD bot \u975E\u7FA4\u4E3B\uFF09\uFF0C\u8BF7\u7528\u300C\u{1F6AA} \u7FA4\u7BA1\u7406\u300D\u624B\u52A8\u8F6C\u8BA9\u540E\u89E3\u6563\u3002";
4498
4934
  await channel.send(evt.chatId, { markdown: `\u2705 \u5DF2\u5220\u9664\u9879\u76EE\u300C${name}\u300D\uFF08\u89E3\u7ED1\uFF0C\u672A\u5220\u4EE3\u7801\u76EE\u5F55\uFF09\u3002
4499
4935
  ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4500
4936
  return renderProjectList();
@@ -4686,9 +5122,22 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4686
5122
  const finalMsgId = cardMsgId;
4687
5123
  await adoptThreadId(finalMsgId);
4688
5124
  rc.cardKey = finalMsgId;
5125
+ const answerText = finalMessageText(rc.rs);
5126
+ const { fences } = extractCardFences(answerText);
5127
+ const imgSources = imageSources(answerText);
5128
+ if (imgSources.length > 0) {
5129
+ rc.images = await uploadOutboundImages(channel, imgSources, opts.cwd ?? fallbackCwd);
5130
+ }
4689
5131
  await stream2.updateCard(channel, buildRunCard(rc));
4690
5132
  runsByCard.delete(cardMsgId);
4691
5133
  promoteCard(finalMsgId, rc);
5134
+ for (const fence of fences) {
5135
+ try {
5136
+ await sendManagedCard(channel, opts.chatId, buildCleanCard(fence, rc.images), finalMsgId, !opts.flat);
5137
+ } catch (err) {
5138
+ log.fail("card", err, { phase: "clean-card" });
5139
+ }
5140
+ }
4692
5141
  if (topicThreadId) await patchSession(topicThreadId, { updatedAt: Date.now() });
4693
5142
  replyTo = finalMsgId;
4694
5143
  replyInThread = !opts.flat;
@@ -4817,13 +5266,57 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4817
5266
  });
4818
5267
  return fresh;
4819
5268
  }
5269
+ async function onBotAddedToChat(evt) {
5270
+ await withTrace({ chatId: evt.chatId }, async () => {
5271
+ const op = evt.operator?.openId;
5272
+ if (await getProjectByChatId(evt.chatId)) {
5273
+ log.info("intake", "bot-added-bound", { chatId: evt.chatId.slice(-6) });
5274
+ return;
5275
+ }
5276
+ if (!op || !isAdmin(cfg, op)) {
5277
+ log.info("intake", "bot-added-nonadmin", { chatId: evt.chatId.slice(-6), op: op?.slice(-6) });
5278
+ return;
5279
+ }
5280
+ const info = await channel.getChatInfo(evt.chatId).catch((err) => {
5281
+ log.fail("intake", err, { phase: "bot-added-chatinfo" });
5282
+ return void 0;
5283
+ });
5284
+ const name = (info?.name ?? "").trim();
5285
+ await sendManagedCard(
5286
+ channel,
5287
+ op,
5288
+ buildJoinGroupFormCard({ chatId: evt.chatId, name }),
5289
+ void 0,
5290
+ false,
5291
+ "open_id"
5292
+ ).catch((err) => log.fail("intake", err, { phase: "bot-added-bindcard" }));
5293
+ log.info("intake", "bot-added", { chatId: evt.chatId.slice(-6), op: op.slice(-6), named: Boolean(name) });
5294
+ }).catch((err) => log.fail("intake", err, { phase: "bot-added" }));
5295
+ }
5296
+ async function onBotRemovedFromChat(chatId) {
5297
+ const project = await getProjectByChatId(chatId);
5298
+ if (!project) return;
5299
+ const removed = await removeProject(project.name);
5300
+ if (!removed) return;
5301
+ log.info("intake", "bot-removed-unbind", { name: removed.name, chatId: chatId.slice(-6) });
5302
+ if (removed.addedBy) {
5303
+ await channel.rawClient.im.v1.message.create({
5304
+ params: { receive_id_type: "open_id" },
5305
+ data: {
5306
+ receive_id: removed.addedBy,
5307
+ msg_type: "text",
5308
+ content: JSON.stringify({ text: `\u2139\uFE0F \u6211\u5DF2\u88AB\u79FB\u51FA\u7FA4\u300C${removed.name}\u300D\uFF0C\u5BF9\u5E94\u9879\u76EE\u5DF2\u81EA\u52A8\u89E3\u7ED1\u3002` })
5309
+ }
5310
+ }).catch(() => void 0);
5311
+ }
5312
+ }
4820
5313
  async function shutdown() {
4821
5314
  const live = [...sessions.values()];
4822
5315
  sessions.clear();
4823
5316
  await Promise.allSettled(live.map((t) => t.close()));
4824
5317
  log.info("bridge", "shutdown", { closed: live.length });
4825
5318
  }
4826
- return { onMessage, onComment, dispatcher, shutdown };
5319
+ return { onMessage, onComment, onBotAddedToChat, onBotRemovedFromChat, dispatcher, shutdown };
4827
5320
  }
4828
5321
  async function getThreadId(channel, messageId) {
4829
5322
  try {
@@ -4861,6 +5354,24 @@ async function startBridge(opts) {
4861
5354
  channel.on("message", orchestrator.onMessage);
4862
5355
  channel.on("cardAction", orchestrator.dispatcher.handle);
4863
5356
  channel.on("comment", orchestrator.onComment);
5357
+ channel.on("botAdded", orchestrator.onBotAddedToChat);
5358
+ try {
5359
+ const tap = channel.dispatcher;
5360
+ if (tap?.register) {
5361
+ tap.register({
5362
+ "im.chat.member.bot.deleted_v1": (raw) => {
5363
+ const ev = raw;
5364
+ const chatId = ev?.chat_id ?? ev?.event?.chat_id;
5365
+ if (chatId) void orchestrator.onBotRemovedFromChat(chatId);
5366
+ }
5367
+ });
5368
+ log.info("ws", "bot-removed-tap");
5369
+ } else {
5370
+ log.info("ws", "bot-removed-tap-unavailable");
5371
+ }
5372
+ } catch (err) {
5373
+ log.fail("ws", err, { phase: "bot-removed-tap" });
5374
+ }
4864
5375
  channel.on("reject", (evt) => log.info("intake", "reject", { reason: evt.reason, msgId: evt.messageId }));
4865
5376
  channel.on("error", (err) => log.fail("ws", err));
4866
5377
  channel.on("reconnecting", () => log.info("ws", "reconnecting"));
@@ -5072,6 +5583,7 @@ async function runBotInit(name) {
5072
5583
  }
5073
5584
  console.log("\n\u4E0B\u4E00\u6B65\uFF08\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u540E\u53F0\uFF0C\u9700\u624B\u52A8\u4E00\u6B21 https://open.feishu.cn/app \uFF09\uFF1A");
5074
5585
  console.log(" 1) \u4E8B\u4EF6\u4E0E\u56DE\u8C03 \u2192 \u957F\u8FDE\u63A5 \u2192 \u8BA2\u9605\uFF1Aim.message.receive_v1 / card.action.trigger / application.bot.menu_v6");
5586
+ console.log(" \uFF08\u53EF\u9009\uFF09\u300C\u52A0\u8FDB\u5DF2\u6709\u7FA4\u300D\u529F\u80FD\u518D\u8BA2\u9605\uFF1Aim.chat.member.bot.added_v1 / im.chat.member.bot.deleted_v1");
5075
5587
  console.log(" 2) \u521B\u5EFA\u5E76\u53D1\u5E03\u5E94\u7528\u7248\u672C");
5076
5588
  console.log("\n`bot list` \u67E5\u770B\u5168\u90E8\uFF1B`bot use <\u540D>` \u5207\u6362\u5F53\u524D\uFF1B`run` \u524D\u53F0\u8DD1 / `start` \u540E\u53F0\u5E38\u9A7B\u3002\n");
5077
5589
  }
@@ -5171,15 +5683,15 @@ async function secretsRemove(id) {
5171
5683
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
5172
5684
  }
5173
5685
  function readStdin() {
5174
- return new Promise((resolve5) => {
5686
+ return new Promise((resolve6) => {
5175
5687
  let data = "";
5176
5688
  if (process.stdin.isTTY) {
5177
- resolve5("");
5689
+ resolve6("");
5178
5690
  return;
5179
5691
  }
5180
5692
  process.stdin.setEncoding("utf8");
5181
5693
  process.stdin.on("data", (c) => data += c);
5182
- process.stdin.on("end", () => resolve5(data));
5694
+ process.stdin.on("end", () => resolve6(data));
5183
5695
  });
5184
5696
  }
5185
5697
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {