@modelzen/feishu-codex-bridge 0.1.8 → 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 +345 -118
  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
@@ -679,7 +679,11 @@ 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];
683
687
  var SCOPE_LABELS = {
684
688
  "im:message.group_at_msg:readonly": "\u63A5\u6536\u7FA4\u91CC @\u673A\u5668\u4EBA \u7684\u6D88\u606F",
685
689
  "im:message.group_msg": "\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\uFF08\u514D@\uFF09",
@@ -695,6 +699,8 @@ var SCOPE_LABELS = {
695
699
  "im:chat.announcement:write_only": "\u7F16\u8F91\u7FA4\u516C\u544A",
696
700
  "im:chat.top_notice:write_only": "\u7F6E\u9876\u7FA4\u516C\u544A\u6A2A\u5E45",
697
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",
698
704
  "cardkit:card:write": "\u4EA4\u4E92\u6309\u94AE\u5361\u7247",
699
705
  "docs:document.comment:read": "\u8BFB\u53D6\u6587\u6863\u8BC4\u8BBA",
700
706
  "docs:document.comment:create": "\u53D1\u8868\u6587\u6863\u8BC4\u8BBA\u56DE\u590D",
@@ -746,8 +752,15 @@ async function validateAppCredentials(appId, appSecret, tenant) {
746
752
  }
747
753
  const token = data.tenant_access_token;
748
754
  const info = await fetchBotInfo(base, token).catch(() => void 0);
749
- const missingScopes = await fetchMissingScopes(base, token).catch(() => void 0);
750
- 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
+ };
751
764
  }
752
765
  async function fetchBotInfo(base, token) {
753
766
  const resp = await fetch(`${base}/open-apis/bot/v3/info`, {
@@ -756,15 +769,14 @@ async function fetchBotInfo(base, token) {
756
769
  if (!resp.ok) return void 0;
757
770
  return await resp.json();
758
771
  }
759
- async function fetchMissingScopes(base, token) {
772
+ async function fetchGrantedScopes(base, token) {
760
773
  const resp = await fetch(`${base}/open-apis/application/v6/scopes`, {
761
774
  headers: { Authorization: `Bearer ${token}` }
762
775
  });
763
776
  if (!resp.ok) return void 0;
764
777
  const body = await resp.json();
765
778
  if (!body.data?.scopes) return void 0;
766
- const granted = new Set(body.data.scopes.filter((s) => s.grant_status === 1).map((s) => s.scope_name));
767
- 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));
768
780
  }
769
781
 
770
782
  // src/utils/open-url.ts
@@ -1071,6 +1083,10 @@ async function confirmReadyForDaemon(result) {
1071
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");
1072
1084
  console.log(" drive.notice.comment_add_v1\uFF08\u4E91\u6587\u6863\u65B0\u589E\u8BC4\u8BBA\uFF09");
1073
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");
1074
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");
1075
1091
  console.log(" card.action.trigger\uFF08\u5361\u7247\u56DE\u4F20\u4EA4\u4E92\uFF09");
1076
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");
@@ -1707,7 +1723,7 @@ function stampRenderToken(card2) {
1707
1723
  };
1708
1724
  visit(card2);
1709
1725
  }
1710
- async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread = false) {
1726
+ async function sendManagedCard(channel, to, card2, replyTo, replyInThread = false, receiveIdType = "chat_id") {
1711
1727
  stampRenderToken(card2);
1712
1728
  const created = await channel.rawClient.cardkit.v1.card.create({
1713
1729
  data: { type: "card_json", data: JSON.stringify(card2) }
@@ -1726,8 +1742,8 @@ async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread =
1726
1742
  messageId = sent.data?.message_id;
1727
1743
  } else {
1728
1744
  const sent = await channel.rawClient.im.v1.message.create({
1729
- params: { receive_id_type: "chat_id" },
1730
- data: { receive_id: chatId, msg_type: "interactive", content }
1745
+ params: { receive_id_type: receiveIdType },
1746
+ data: { receive_id: to, msg_type: "interactive", content }
1731
1747
  });
1732
1748
  messageId = sent.data?.message_id;
1733
1749
  }
@@ -2183,14 +2199,20 @@ function pickerTime(unixSeconds) {
2183
2199
  const md2 = `${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
2184
2200
  return d.getFullYear() === now.getFullYear() ? `${md2} ${hm}` : `${d.getFullYear()}-${md2} ${hm}`;
2185
2201
  }
2186
- 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) {
2187
2206
  const elements = [];
2188
2207
  if (scope === "single") {
2189
2208
  elements.push(
2190
2209
  md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"),
2191
2210
  hr(),
2192
2211
  md(
2193
- "\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`
2194
2216
  )
2195
2217
  );
2196
2218
  } else if (scope === "topic") {
@@ -2198,7 +2220,9 @@ function buildHelpCard(scope) {
2198
2220
  md("\u{1F9F5} **\u8BDD\u9898\u5185** \u2014 \u6BCF\u4E2A\u8BDD\u9898\u662F\u4E00\u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
2199
2221
  hr(),
2200
2222
  md(
2201
- "\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`
2202
2226
  ),
2203
2227
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
2204
2228
  );
@@ -2213,7 +2237,7 @@ function buildHelpCard(scope) {
2213
2237
  }
2214
2238
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
2215
2239
  }
2216
- function buildWelcomeCard(kind, docUrl) {
2240
+ function buildWelcomeCard(kind, docUrl, noMention = true) {
2217
2241
  const elements = [
2218
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"),
2219
2243
  hr()
@@ -2222,7 +2246,10 @@ function buildWelcomeCard(kind, docUrl) {
2222
2246
  elements.push(
2223
2247
  md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4**\uFF08\u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\uFF09"),
2224
2248
  md(
2225
- "\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`
2226
2253
  )
2227
2254
  );
2228
2255
  } else {
@@ -2895,6 +2922,71 @@ async function uploadBuffer(channel, buffer) {
2895
2922
  return key;
2896
2923
  }
2897
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
+
2898
2990
  // src/card/dm-cards.ts
2899
2991
  function openChatUrl(chatId) {
2900
2992
  return `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(chatId)}`;
@@ -2904,6 +2996,7 @@ var DM = {
2904
2996
  menu: "dm.menu",
2905
2997
  newProject: "dm.newProject",
2906
2998
  newProjectSubmit: "dm.newProject.submit",
2999
+ joinGroupSubmit: "dm.joinGroup.submit",
2907
3000
  projects: "dm.projects",
2908
3001
  settings: "dm.settings",
2909
3002
  doctor: "dm.doctor",
@@ -3057,6 +3150,27 @@ ${i.missingScopes.map((s) => `\xB7 ${labelScope(s)}`).join("\n")}`),
3057
3150
  actions([linkButton("\u{1F511} \u4E00\u952E\u53BB\u5F00\u901A\u8FD9\u4E9B\u6743\u9650", i.scopeGrantUrl)])
3058
3151
  ];
3059
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
+ }
3060
3174
  function codexDiagnosePrompt(i) {
3061
3175
  return [
3062
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",
@@ -3099,6 +3213,8 @@ function buildDoctorCard(i) {
3099
3213
  ...scopeDiagnosis(i),
3100
3214
  note(`bridge v${i.bridgeVer}\u3000\xB7\u3000Node ${i.node}\u3000\xB7\u3000${i.platform}`),
3101
3215
  hr(),
3216
+ ...joinFeatureDiagnosis(i),
3217
+ hr(),
3102
3218
  md("**\u65E5\u5FD7\u8DEF\u5F84**"),
3103
3219
  note(`\u540E\u53F0\u5B88\u62A4\u8F93\u51FA\uFF1A\`${i.logStdout}\``),
3104
3220
  note(`\u540E\u53F0\u5B88\u62A4\u9519\u8BEF\uFF1A\`${i.logStderr}\``),
@@ -3132,14 +3248,35 @@ function buildNewProjectFormCard(opts = {}) {
3132
3248
  );
3133
3249
  return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "turquoise" } });
3134
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
+ }
3135
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";
3136
3273
  const elements = [
3137
- 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)_" : ""}`),
3138
3275
  note(`\u{1F4C2} \`${p.cwd}\` \xB7 ${kindLabel(p.kind)}`),
3139
- 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")
3140
3277
  ];
3141
3278
  if (p.chatId) elements.push(actions([linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary")]));
3142
- return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "green" } });
3279
+ return card(elements, { header: { title, template: "green" } });
3143
3280
  }
3144
3281
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
3145
3282
  if (projects.length === 0) {
@@ -3154,7 +3291,7 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3154
3291
  elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
3155
3292
  elements.push(
3156
3293
  note(
3157
- 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"
3158
3295
  )
3159
3296
  );
3160
3297
  const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
@@ -3177,11 +3314,12 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3177
3314
  elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
3178
3315
  return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
3179
3316
  }
3180
- 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";
3181
3319
  return card(
3182
3320
  [
3183
3321
  md(`\u786E\u5B9A\u5220\u9664\u9879\u76EE **${name}**\uFF1F`),
3184
- 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_),
3185
3323
  actions([
3186
3324
  button("\u2705 \u786E\u8BA4\u5220\u9664", { a: DM.rmDo, n: name }, "danger"),
3187
3325
  button("\u53D6\u6D88", { a: DM.rmCancel })
@@ -3229,7 +3367,7 @@ function buildSettingsCard(cfg) {
3229
3367
  }
3230
3368
  function buildGroupSettingsCard(project) {
3231
3369
  const kind = project.kind ?? "multi";
3232
- const noMention = project.noMention ?? true;
3370
+ const noMention = project.noMention ?? defaultNoMention(project);
3233
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";
3234
3372
  return card(
3235
3373
  [
@@ -3249,16 +3387,16 @@ function buildGroupSettingsCard(project) {
3249
3387
  // src/service/update.ts
3250
3388
  import { execFile, spawn as spawn5 } from "child_process";
3251
3389
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3252
- import { dirname as dirname6, join as join8, resolve as resolve4 } from "path";
3390
+ import { dirname as dirname7, join as join8, resolve as resolve4 } from "path";
3253
3391
  import { fileURLToPath as fileURLToPath3 } from "url";
3254
3392
  import { promisify } from "util";
3255
3393
 
3256
3394
  // src/service/launchd.ts
3257
3395
  import { spawn as spawn4, spawnSync } from "child_process";
3258
3396
  import { existsSync as existsSync4 } from "fs";
3259
- 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";
3260
3398
  import { homedir as homedir3, userInfo as userInfo2 } from "os";
3261
- import { dirname as dirname5, join as join7, resolve as resolve3 } from "path";
3399
+ import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
3262
3400
  import { fileURLToPath as fileURLToPath2 } from "url";
3263
3401
  var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
3264
3402
  function launchAgentPlistPath() {
@@ -3271,7 +3409,7 @@ function serviceStderrPath() {
3271
3409
  return join7(paths.appDir, "service.err.log");
3272
3410
  }
3273
3411
  function resolveCliBinPath() {
3274
- const distDir = dirname5(fileURLToPath2(import.meta.url));
3412
+ const distDir = dirname6(fileURLToPath2(import.meta.url));
3275
3413
  return resolve3(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3276
3414
  }
3277
3415
  function escapeXml(value) {
@@ -3312,9 +3450,9 @@ function buildPlist() {
3312
3450
  }
3313
3451
  async function installLaunchd() {
3314
3452
  const plistPath = launchAgentPlistPath();
3315
- await mkdir4(dirname5(plistPath), { recursive: true });
3453
+ await mkdir5(dirname6(plistPath), { recursive: true });
3316
3454
  await ensureLogFiles();
3317
- await writeFile4(plistPath, buildPlist(), "utf8");
3455
+ await writeFile5(plistPath, buildPlist(), "utf8");
3318
3456
  if (isLoaded()) {
3319
3457
  const bootout = runLaunchctl(["bootout", serviceTarget()]);
3320
3458
  if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
@@ -3396,7 +3534,7 @@ async function waitUntilUnloaded(timeoutMs = 5e3) {
3396
3534
  throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
3397
3535
  }
3398
3536
  async function ensureLogFiles() {
3399
- await mkdir4(paths.appDir, { recursive: true });
3537
+ await mkdir5(paths.appDir, { recursive: true });
3400
3538
  await appendFile(serviceStdoutPath(), "");
3401
3539
  await appendFile(serviceStderrPath(), "");
3402
3540
  }
@@ -3438,7 +3576,7 @@ function getServiceAdapter() {
3438
3576
  var execFileP = promisify(execFile);
3439
3577
  var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
3440
3578
  function pkgRoot() {
3441
- return resolve4(dirname6(fileURLToPath3(import.meta.url)), "..");
3579
+ return resolve4(dirname7(fileURLToPath3(import.meta.url)), "..");
3442
3580
  }
3443
3581
  function pkgJson() {
3444
3582
  try {
@@ -3503,64 +3641,6 @@ async function restartDaemon() {
3503
3641
  await getServiceAdapter().restart();
3504
3642
  }
3505
3643
 
3506
- // src/project/registry.ts
3507
- import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3508
- import { dirname as dirname7 } from "path";
3509
- var FILE_VERSION2 = 1;
3510
- async function read() {
3511
- try {
3512
- const text = await readFile6(paths.projectsFile, "utf8");
3513
- const parsed = JSON.parse(text);
3514
- return Array.isArray(parsed.projects) ? parsed.projects : [];
3515
- } catch (err) {
3516
- if (err.code === "ENOENT") return [];
3517
- throw err;
3518
- }
3519
- }
3520
- async function write(projects) {
3521
- await mkdir5(dirname7(paths.projectsFile), { recursive: true });
3522
- const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
3523
- const body = { version: FILE_VERSION2, projects };
3524
- await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
3525
- `, "utf8");
3526
- await rename4(tmp, paths.projectsFile);
3527
- }
3528
- async function listProjects() {
3529
- return read();
3530
- }
3531
- async function getProjectByChatId(chatId) {
3532
- return (await read()).find((p) => p.chatId === chatId);
3533
- }
3534
- async function getProjectByName(name) {
3535
- return (await read()).find((p) => p.name === name);
3536
- }
3537
- async function addProject(p) {
3538
- const projects = await read();
3539
- if (projects.some((x) => x.name === p.name)) {
3540
- throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3541
- }
3542
- projects.push(p);
3543
- await write(projects);
3544
- }
3545
- async function updateProject(name, patch) {
3546
- const projects = await read();
3547
- const p = projects.find((x) => x.name === name);
3548
- if (!p) return;
3549
- const target = p;
3550
- for (const [k, v] of Object.entries(patch)) {
3551
- if (v !== void 0) target[k] = v;
3552
- }
3553
- await write(projects);
3554
- }
3555
- async function removeProject(name) {
3556
- const projects = await read();
3557
- const idx = projects.findIndex((p) => p.name === name);
3558
- if (idx === -1) return void 0;
3559
- const [removed] = projects.splice(idx, 1);
3560
- await write(projects);
3561
- return removed;
3562
- }
3563
-
3564
3644
  // src/project/lifecycle.ts
3565
3645
  import { mkdir as mkdir6 } from "fs/promises";
3566
3646
  import { existsSync as existsSync6 } from "fs";
@@ -3665,21 +3745,23 @@ var HELP_DOC_URL = "https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g";
3665
3745
  async function onboardGroup(channel, project) {
3666
3746
  const kind = project.kind ?? "multi";
3667
3747
  const chatId = project.chatId;
3748
+ const decorate = (project.origin ?? "created") !== "joined";
3668
3749
  try {
3669
- 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));
3670
3752
  const sent = await channel.rawClient.im.v1.message.create({
3671
3753
  params: { receive_id_type: "chat_id" },
3672
3754
  data: { receive_id: chatId, msg_type: "interactive", content }
3673
3755
  });
3674
3756
  const messageId = sent.data?.message_id;
3675
- if (messageId) {
3757
+ if (messageId && decorate) {
3676
3758
  await channel.rawClient.im.v1.pin.create({ data: { message_id: messageId } });
3677
3759
  log.info("project", "onboard-pin", { name: project.name });
3678
3760
  }
3679
3761
  } catch (err) {
3680
3762
  log.fail("project", err, { phase: "onboard-welcome" });
3681
3763
  }
3682
- if (HELP_DOC_URL) {
3764
+ if (decorate && HELP_DOC_URL) {
3683
3765
  try {
3684
3766
  await channel.rawClient.im.v1.chatTab.create({
3685
3767
  path: { chat_id: chatId },
@@ -3695,21 +3777,21 @@ async function onboardGroup(channel, project) {
3695
3777
  }
3696
3778
 
3697
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
+ }
3698
3790
  async function createProject(channel, input2) {
3699
3791
  const name = input2.name.trim();
3700
3792
  if (!name) throw new Error("\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A");
3701
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`);
3702
- let cwd;
3703
- let blank;
3704
- if (input2.existingPath) {
3705
- cwd = isAbsolute2(input2.existingPath) ? input2.existingPath : resolve5(input2.existingPath);
3706
- if (!existsSync6(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
3707
- blank = false;
3708
- } else {
3709
- cwd = join9(paths.projectsRootDir, name);
3710
- await mkdir6(cwd, { recursive: true });
3711
- blank = true;
3712
- }
3794
+ const { cwd, blank } = await resolveCwd(name, input2.existingPath);
3713
3795
  const res = await channel.rawClient.im.v1.chat.create({
3714
3796
  params: { user_id_type: "open_id" },
3715
3797
  data: { name, user_id_list: [input2.ownerOpenId] }
@@ -3721,13 +3803,35 @@ async function createProject(channel, input2) {
3721
3803
  params: { member_id_type: "open_id" },
3722
3804
  data: { manager_ids: [input2.ownerOpenId] }
3723
3805
  }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
3724
- 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" };
3725
3807
  await addProject(project);
3726
3808
  log.info("project", "create", { name, chatId, cwd, blank });
3727
3809
  await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
3728
3810
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
3729
3811
  return project;
3730
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
+ }
3731
3835
 
3732
3836
  // src/project/group-ops.ts
3733
3837
  async function transferOwnership(channel, chatId, toOpenId) {
@@ -3738,6 +3842,13 @@ async function transferOwnership(channel, chatId, toOpenId) {
3738
3842
  });
3739
3843
  log.info("project", "owner-transfer", { chatId: chatId.slice(-6), to: toOpenId.slice(-6) });
3740
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
+ }
3741
3852
 
3742
3853
  // src/bot/session-store.ts
3743
3854
  import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "fs/promises";
@@ -4268,11 +4379,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4268
4379
  log.info("intake", "reject", { reason: "not_allowed", chatId: msg.chatId.slice(-6) });
4269
4380
  return;
4270
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
+ }
4271
4393
  const text = msg.content.trim();
4272
4394
  const cmd = parseCommand(text);
4273
4395
  if ((project?.kind ?? "multi") === "single") {
4274
4396
  if (cmd === "help") {
4275
- await postHelpCard(msg, "single");
4397
+ await postHelpCard(msg, "single", false, project);
4276
4398
  return;
4277
4399
  }
4278
4400
  if (cmd === "settings") {
@@ -4288,7 +4410,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4288
4410
  }
4289
4411
  if (msg.threadId) {
4290
4412
  if (cmd === "help") {
4291
- await postHelpCard(msg, "topic", true);
4413
+ await postHelpCard(msg, "topic", true, project);
4292
4414
  return;
4293
4415
  }
4294
4416
  if (cmd === "model") {
@@ -4299,7 +4421,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4299
4421
  return;
4300
4422
  }
4301
4423
  if (cmd === "help") {
4302
- await postHelpCard(msg, "main");
4424
+ await postHelpCard(msg, "main", false, project);
4303
4425
  return;
4304
4426
  }
4305
4427
  if (cmd === "resume") {
@@ -4322,7 +4444,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4322
4444
  return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
4323
4445
  }
4324
4446
  function shouldRespondWithoutMention(project, msg) {
4325
- if (!(project.noMention ?? true)) return false;
4447
+ if (!(project.noMention ?? defaultNoMention(project))) return false;
4326
4448
  if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
4327
4449
  if ((project.kind ?? "multi") === "single") return true;
4328
4450
  return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
@@ -4519,9 +4641,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4519
4641
  log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
4520
4642
  });
4521
4643
  }
4522
- 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;
4523
4646
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
4524
- await sendManagedCard(channel, msg.chatId, buildHelpCard(scope), msg.messageId, inThread).catch(
4647
+ await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention), msg.messageId, inThread).catch(
4525
4648
  (err) => log.fail("card", err, { cmd: "help", scope })
4526
4649
  );
4527
4650
  log.info("card", "help", { scope });
@@ -4659,6 +4782,32 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4659
4782
  (e) => log.fail("console", e, { phase: "new-project-result" })
4660
4783
  );
4661
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
+ })();
4662
4811
  }).on(DM.projects, ({ evt }) => {
4663
4812
  if (!dmAdmin(evt.operator?.openId)) return;
4664
4813
  patch(evt, renderProjectList);
@@ -4671,6 +4820,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4671
4820
  const secret = await getSecret(secretKeyForApp(app.id)).catch(() => void 0);
4672
4821
  const scopeCheck = secret ? await validateAppCredentials(app.id, secret, app.tenant).catch(() => void 0) : void 0;
4673
4822
  const missingScopes = scopeCheck?.missingScopes;
4823
+ const missingJoinScopes = scopeCheck?.missingJoinScopes;
4674
4824
  const info = {
4675
4825
  codexOk: await backend.isAvailable().catch(() => false),
4676
4826
  codexVer: codexBin ? codexVersion(codexBin) : null,
@@ -4687,7 +4837,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4687
4837
  app.id,
4688
4838
  app.tenant,
4689
4839
  missingScopes && missingScopes.length ? missingScopes : void 0
4690
- )
4840
+ ),
4841
+ missingJoinScopes,
4842
+ // 「加入存量群」按钮恒预选这两项 opt-in scope(它们不在必需清单里)。
4843
+ joinScopeGrantUrl: buildScopeGrantUrl(app.id, app.tenant, JOIN_GROUP_SCOPES)
4691
4844
  };
4692
4845
  await sendManagedCard(channel, evt.chatId, buildDoctorCard(info), evt.messageId).catch(
4693
4846
  (err) => log.fail("console", err, { cmd: "doctor" })
@@ -4748,7 +4901,8 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
4748
4901
  }).on(DM.rmConfirm, async ({ evt, value }) => {
4749
4902
  const name = typeof value.n === "string" ? value.n : void 0;
4750
4903
  if (!dmAdmin(evt.operator?.openId) || !name) return;
4751
- await patch(evt, buildRmConfirmCard(name));
4904
+ const proj = (await listProjects()).find((p) => p.name === name);
4905
+ await patch(evt, buildRmConfirmCard(name, proj?.origin));
4752
4906
  }).on(DM.rmCancel, ({ evt }) => {
4753
4907
  if (!dmAdmin(evt.operator?.openId)) return;
4754
4908
  patch(evt, renderProjectList);
@@ -4758,15 +4912,25 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
4758
4912
  if (!dmAdmin(op) || !name) return;
4759
4913
  patch(evt, async () => {
4760
4914
  const removed = await removeProject(name);
4761
- let transferred = false;
4762
- if (removed?.chatId && op) {
4763
- transferred = await transferOwnership(channel, removed.chatId, op).then(() => true).catch((err) => {
4764
- 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" });
4765
4919
  return false;
4766
- });
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";
4767
4933
  }
4768
- log.info("console", "rm", { name, transferred });
4769
- 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";
4770
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
4771
4935
  ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4772
4936
  return renderProjectList();
@@ -5102,13 +5266,57 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5102
5266
  });
5103
5267
  return fresh;
5104
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
+ }
5105
5313
  async function shutdown() {
5106
5314
  const live = [...sessions.values()];
5107
5315
  sessions.clear();
5108
5316
  await Promise.allSettled(live.map((t) => t.close()));
5109
5317
  log.info("bridge", "shutdown", { closed: live.length });
5110
5318
  }
5111
- return { onMessage, onComment, dispatcher, shutdown };
5319
+ return { onMessage, onComment, onBotAddedToChat, onBotRemovedFromChat, dispatcher, shutdown };
5112
5320
  }
5113
5321
  async function getThreadId(channel, messageId) {
5114
5322
  try {
@@ -5146,6 +5354,24 @@ async function startBridge(opts) {
5146
5354
  channel.on("message", orchestrator.onMessage);
5147
5355
  channel.on("cardAction", orchestrator.dispatcher.handle);
5148
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
+ }
5149
5375
  channel.on("reject", (evt) => log.info("intake", "reject", { reason: evt.reason, msgId: evt.messageId }));
5150
5376
  channel.on("error", (err) => log.fail("ws", err));
5151
5377
  channel.on("reconnecting", () => log.info("ws", "reconnecting"));
@@ -5357,6 +5583,7 @@ async function runBotInit(name) {
5357
5583
  }
5358
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");
5359
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");
5360
5587
  console.log(" 2) \u521B\u5EFA\u5E76\u53D1\u5E03\u5E94\u7528\u7248\u672C");
5361
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");
5362
5589
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.1.8",
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": {