@modelzen/feishu-codex-bridge 0.3.1 → 0.3.2

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 (2) hide show
  1. package/dist/cli.js +434 -68
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -78,6 +78,7 @@ import { dirname as dirname3, join as join2 } from "path";
78
78
 
79
79
  // src/config/store.ts
80
80
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
81
+ import { randomUUID } from "crypto";
81
82
  import { dirname as dirname2 } from "path";
82
83
 
83
84
  // src/config/schema.ts
@@ -114,18 +115,23 @@ function getRunIdleTimeoutMs(cfg) {
114
115
  const clamped = Math.min(Math.max(Math.floor(raw), 10), 1800);
115
116
  return clamped * 1e3;
116
117
  }
117
- function isUserAllowed(cfg, senderId) {
118
- const list = cfg.preferences?.access?.allowedUsers;
119
- if (!list || list.length === 0) return true;
120
- return list.includes(senderId);
121
- }
122
118
  function isChatAllowed(cfg, chatId) {
123
119
  const list = cfg.preferences?.access?.allowedChats;
124
120
  if (!list || list.length === 0) return true;
125
121
  return list.includes(chatId);
126
122
  }
123
+ function resolveOwner(cfg) {
124
+ const access = cfg.preferences?.access;
125
+ return access?.ownerOpenId ?? access?.admins?.[0];
126
+ }
127
127
  function isAdmin(cfg, senderId) {
128
- const list = cfg.preferences?.access?.admins;
128
+ if (!senderId) return false;
129
+ if (senderId === resolveOwner(cfg)) return true;
130
+ return Boolean(cfg.preferences?.access?.admins?.includes(senderId));
131
+ }
132
+ function isUserAllowedInProject(cfg, project, senderId) {
133
+ if (isAdmin(cfg, senderId)) return true;
134
+ const list = project?.allowedUsers;
129
135
  if (!list || list.length === 0) return true;
130
136
  return list.includes(senderId);
131
137
  }
@@ -174,13 +180,21 @@ exec ${sq(node)} ${sq(bridgeEntry)} secrets get "$@"
174
180
  await rename(tmp, wrapperPath);
175
181
  return wrapperPath;
176
182
  }
177
- async function saveConfig(cfg, path = paths.configFile) {
178
- await mkdir(dirname2(path), { recursive: true });
179
- const tmp = `${path}.tmp-${process.pid}`;
180
- await writeFile(tmp, `${JSON.stringify(cfg, null, 2)}
183
+ var saveChain = Promise.resolve();
184
+ function saveConfig(cfg, path = paths.configFile) {
185
+ const run = saveChain.then(async () => {
186
+ await mkdir(dirname2(path), { recursive: true });
187
+ const tmp = `${path}.tmp-${process.pid}-${randomUUID()}`;
188
+ await writeFile(tmp, `${JSON.stringify(cfg, null, 2)}
181
189
  `, "utf8");
182
- await chmod(tmp, 384);
183
- await rename(tmp, path);
190
+ await chmod(tmp, 384);
191
+ await rename(tmp, path);
192
+ });
193
+ saveChain = run.then(
194
+ () => void 0,
195
+ () => void 0
196
+ );
197
+ return run;
184
198
  }
185
199
 
186
200
  // src/config/bots.ts
@@ -670,7 +684,7 @@ async function runRegistrationWizard() {
670
684
  accounts: { app: { id: result.client_id, secret: result.client_secret, tenant } }
671
685
  };
672
686
  if (operatorOpenId) {
673
- cfg.preferences = { access: { admins: [operatorOpenId] } };
687
+ cfg.preferences = { access: { ownerOpenId: operatorOpenId, admins: [operatorOpenId] } };
674
688
  console.log(` Admin: ${operatorOpenId} (\u4F60\u81EA\u5DF1\uFF0C\u5DF2\u81EA\u52A8\u52A0\u5165\u7BA1\u7406\u5458\u540D\u5355)`);
675
689
  } else {
676
690
  console.log(
@@ -727,7 +741,13 @@ var JOIN_GROUP_SCOPES = [
727
741
  "im:chat:readonly",
728
742
  "im:chat.members:write_only"
729
743
  ];
730
- var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES, ...JOIN_GROUP_SCOPES];
744
+ var CONTACT_SCOPES = ["contact:user.base:readonly"];
745
+ var GRANT_SCOPES = [
746
+ ...REQUIRED_SCOPES,
747
+ ...COMMENT_SCOPES,
748
+ ...JOIN_GROUP_SCOPES,
749
+ ...CONTACT_SCOPES
750
+ ];
731
751
  var SCOPE_LABELS = {
732
752
  "im:message.group_at_msg:readonly": "\u63A5\u6536\u7FA4\u91CC @\u673A\u5668\u4EBA \u7684\u6D88\u606F",
733
753
  "im:message.group_msg": "\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\uFF08\u514D@\uFF09",
@@ -748,7 +768,8 @@ var SCOPE_LABELS = {
748
768
  "cardkit:card:write": "\u4EA4\u4E92\u6309\u94AE\u5361\u7247",
749
769
  "docs:document.comment:read": "\u8BFB\u53D6\u6587\u6863\u8BC4\u8BBA",
750
770
  "docs:document.comment:create": "\u53D1\u8868\u6587\u6863\u8BC4\u8BBA\u56DE\u590D",
751
- "wiki:wiki:readonly": "\u8BFB\u53D6\u77E5\u8BC6\u5E93\u8282\u70B9"
771
+ "wiki:wiki:readonly": "\u8BFB\u53D6\u77E5\u8BC6\u5E93\u8282\u70B9",
772
+ "contact:user.base:readonly": "\u8BFB\u53D6\u6210\u5458\u59D3\u540D\uFF08\u7BA1\u7406\u5458 / \u767D\u540D\u5355\u5C55\u793A\uFF09"
752
773
  };
753
774
  function labelScope(scope) {
754
775
  const label = SCOPE_LABELS[scope];
@@ -2168,6 +2189,14 @@ function selectStatic(opts) {
2168
2189
  behaviors: [{ type: "callback", value: { a: opts.actionId } }]
2169
2190
  };
2170
2191
  }
2192
+ function selectMenu(opts) {
2193
+ return {
2194
+ tag: "select_static",
2195
+ name: opts.name,
2196
+ placeholder: { tag: "plain_text", content: opts.placeholder },
2197
+ options: opts.options.map((o) => ({ text: { tag: "plain_text", content: o.label }, value: o.value }))
2198
+ };
2199
+ }
2171
2200
 
2172
2201
  // src/card/command-cards.ts
2173
2202
  var MC = {
@@ -2275,19 +2304,13 @@ function pickerTime(unixSeconds) {
2275
2304
  function talkLine(noMention, tail) {
2276
2305
  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`;
2277
2306
  }
2278
- function buildHelpCard(scope, noMention = true) {
2307
+ function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2279
2308
  const elements = [];
2280
2309
  if (scope === "single") {
2281
- elements.push(
2282
- md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"),
2283
- hr(),
2284
- md(
2285
- `${talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406")}
2286
- \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2287
- \xB7 \`/settings\` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09
2288
- \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2289
- )
2290
- );
2310
+ const lines = [talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"), "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"];
2311
+ if (isAdmin2) lines.push("\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2312
+ lines.push("\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2313
+ elements.push(md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"), hr(), md(lines.join("\n")));
2291
2314
  } else if (scope === "topic") {
2292
2315
  elements.push(
2293
2316
  md("\u{1F9F5} **\u8BDD\u9898\u5185** \u2014 \u6BCF\u4E2A\u8BDD\u9898\u662F\u4E00\u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
@@ -2300,13 +2323,10 @@ function buildHelpCard(scope, noMention = true) {
2300
2323
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
2301
2324
  );
2302
2325
  } else {
2303
- elements.push(
2304
- md("\u{1F465} **\u4E3B\u7FA4\u533A** \u2014 @\u6211\u5F00\u8BDD\u9898\uFF0C\u6BCF\u4E2A\u8BDD\u9898\u662F\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
2305
- hr(),
2306
- md(
2307
- "\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB\n\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/model` \u2192 \u9700\u8981\u5728\u8BDD\u9898\u91CC\u7528\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
2308
- )
2309
- );
2326
+ const lines = ["\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB"];
2327
+ if (isAdmin2) lines.push("\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD", "\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2328
+ lines.push("\xB7 `/model` \u2192 \u9700\u8981\u5728\u8BDD\u9898\u91CC\u7528", "\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2329
+ elements.push(md("\u{1F465} **\u4E3B\u7FA4\u533A** \u2014 @\u6211\u5F00\u8BDD\u9898\uFF0C\u6BCF\u4E2A\u8BDD\u9898\u662F\u72EC\u7ACB\u4F1A\u8BDD\u3002"), hr(), md(lines.join("\n")));
2310
2330
  }
2311
2331
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
2312
2332
  }
@@ -3113,6 +3133,7 @@ async function uploadBuffer(channel, buffer) {
3113
3133
 
3114
3134
  // src/project/registry.ts
3115
3135
  import { mkdir as mkdir4, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "fs/promises";
3136
+ import { randomUUID as randomUUID2 } from "crypto";
3116
3137
  import { dirname as dirname5 } from "path";
3117
3138
  function defaultNoMention(p) {
3118
3139
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
@@ -3128,9 +3149,18 @@ async function read() {
3128
3149
  throw err;
3129
3150
  }
3130
3151
  }
3152
+ var opChain = Promise.resolve();
3153
+ function withLock(fn) {
3154
+ const run = opChain.then(fn, fn);
3155
+ opChain = run.then(
3156
+ () => void 0,
3157
+ () => void 0
3158
+ );
3159
+ return run;
3160
+ }
3131
3161
  async function write(projects) {
3132
3162
  await mkdir4(dirname5(paths.projectsFile), { recursive: true });
3133
- const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
3163
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID2()}`;
3134
3164
  const body = { version: FILE_VERSION2, projects };
3135
3165
  await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
3136
3166
  `, "utf8");
@@ -3146,34 +3176,41 @@ async function getProjectByName(name) {
3146
3176
  return (await read()).find((p) => p.name === name);
3147
3177
  }
3148
3178
  async function addProject(p) {
3149
- const projects = await read();
3150
- if (projects.some((x) => x.name === p.name)) {
3151
- throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3152
- }
3153
- if (p.chatId) {
3154
- const bound = projects.find((x) => x.chatId === p.chatId);
3155
- if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
3156
- }
3157
- projects.push(p);
3158
- await write(projects);
3179
+ return withLock(async () => {
3180
+ const projects = await read();
3181
+ if (projects.some((x) => x.name === p.name)) {
3182
+ throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3183
+ }
3184
+ if (p.chatId) {
3185
+ const bound = projects.find((x) => x.chatId === p.chatId);
3186
+ if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
3187
+ }
3188
+ projects.push(p);
3189
+ await write(projects);
3190
+ });
3159
3191
  }
3160
3192
  async function updateProject(name, patch) {
3161
- const projects = await read();
3162
- const p = projects.find((x) => x.name === name);
3163
- if (!p) return;
3164
- const target = p;
3165
- for (const [k, v] of Object.entries(patch)) {
3166
- if (v !== void 0) target[k] = v;
3167
- }
3168
- await write(projects);
3193
+ return withLock(async () => {
3194
+ const projects = await read();
3195
+ const p = projects.find((x) => x.name === name);
3196
+ if (!p) return;
3197
+ const actual = typeof patch === "function" ? patch(p) : patch;
3198
+ const target = p;
3199
+ for (const [k, v] of Object.entries(actual)) {
3200
+ if (v !== void 0) target[k] = v;
3201
+ }
3202
+ await write(projects);
3203
+ });
3169
3204
  }
3170
3205
  async function removeProject(name) {
3171
- const projects = await read();
3172
- const idx = projects.findIndex((p) => p.name === name);
3173
- if (idx === -1) return void 0;
3174
- const [removed] = projects.splice(idx, 1);
3175
- await write(projects);
3176
- return removed;
3206
+ return withLock(async () => {
3207
+ const projects = await read();
3208
+ const idx = projects.findIndex((p) => p.name === name);
3209
+ if (idx === -1) return void 0;
3210
+ const [removed] = projects.splice(idx, 1);
3211
+ await write(projects);
3212
+ return removed;
3213
+ });
3177
3214
  }
3178
3215
 
3179
3216
  // src/card/dm-cards.ts
@@ -3198,7 +3235,19 @@ var DM = {
3198
3235
  setTools: "dm.set.tools",
3199
3236
  setWatchdog: "dm.set.watchdog",
3200
3237
  setPending: "dm.set.pending",
3201
- setConcurrency: "dm.set.concurrency"
3238
+ setConcurrency: "dm.set.concurrency",
3239
+ // 权限管理:全局 admins(settings 卡进入)+ 项目响应白名单(项目列表 / 建项目完成卡进入)
3240
+ admins: "dm.admins",
3241
+ addAdminForm: "dm.admin.addForm",
3242
+ addAdminSubmit: "dm.admin.addSubmit",
3243
+ rmAdmin: "dm.admin.rm",
3244
+ allowlist: "dm.allowlist",
3245
+ addAllowedForm: "dm.allow.addForm",
3246
+ addAllowedSubmit: "dm.allow.addSubmit",
3247
+ rmAllowed: "dm.allow.rm",
3248
+ // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3249
+ projectSettings: "dm.projectSettings",
3250
+ setNoMentionDm: "dm.proj.noMention"
3202
3251
  };
3203
3252
  var GS = {
3204
3253
  setNoMention: "gs.noMention"
@@ -3464,7 +3513,13 @@ function buildNewProjectDoneCard(p) {
3464
3513
  note(`\u{1F4C2} \`${p.cwd}\` \xB7 ${kindLabel(p.kind)}`),
3465
3514
  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")
3466
3515
  ];
3467
- if (p.chatId) elements.push(actions([linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary")]));
3516
+ if (p.chatId)
3517
+ elements.push(
3518
+ actions([
3519
+ linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary"),
3520
+ button("\u2699\uFE0F \u9879\u76EE\u8BBE\u7F6E", { a: DM.projectSettings, n: p.name })
3521
+ ])
3522
+ );
3468
3523
  return card(elements, { header: { title, template: "green" } });
3469
3524
  }
3470
3525
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
@@ -3495,6 +3550,7 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3495
3550
  }
3496
3551
  const row = [];
3497
3552
  if (p.chatId) row.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId)));
3553
+ row.push(button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: p.name }));
3498
3554
  row.push(button("\u{1F5D1} \u5220\u9664", { a: DM.rmConfirm, n: p.name }, "danger"));
3499
3555
  elements.push(actions(row));
3500
3556
  elements.push(hr());
@@ -3549,7 +3605,8 @@ function buildSettingsCard(cfg) {
3549
3605
  { label: "20", value: "20" }
3550
3606
  ]),
3551
3607
  note("\u26A0\uFE0F \u5047\u6B7B\u8D85\u65F6 / \u5E76\u53D1\u4E0A\u9650 \u6539\u540E\u9700**\u91CD\u542F**\u751F\u6548\uFF1B\u5DE5\u5177\u663E\u793A / \u8FD0\u884C\u4E2D\u65B0\u6D88\u606F \u5373\u65F6\u751F\u6548\u3002"),
3552
- actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
3608
+ hr(),
3609
+ actions([button("\u{1F46E} \u7BA1\u7406\u5458", { a: DM.admins }), button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
3553
3610
  ],
3554
3611
  { header: { title: "\u2699\uFE0F \u8BBE\u7F6E", template: "blue" } }
3555
3612
  );
@@ -3572,6 +3629,146 @@ function buildGroupSettingsCard(project) {
3572
3629
  { header: { title: "\u2699\uFE0F \u7FA4\u8BBE\u7F6E", template: "blue" } }
3573
3630
  );
3574
3631
  }
3632
+ function memberName(names, id) {
3633
+ return names.get(id) ?? `\u2026${id.slice(-6)}`;
3634
+ }
3635
+ function buildAdminsCard(cfg, names) {
3636
+ const owner = resolveOwner(cfg);
3637
+ const admins = cfg.preferences?.access?.admins ?? [];
3638
+ const elements = [md("**\u7BA1\u7406\u5458\u540D\u5355** \xB7 \u672C bot \u5168\u5C40\uFF08\u53EF\u79C1\u804A\u7BA1\u7406 / \u5EFA\u9879\u76EE / \u9500\u6BC1\u64CD\u4F5C\uFF09"), hr()];
3639
+ const seen = /* @__PURE__ */ new Set();
3640
+ if (owner) {
3641
+ seen.add(owner);
3642
+ elements.push(actions([md(`\u{1F451} **${memberName(names, owner)}** \xB7 Bot \u62E5\u6709\u8005\uFF08\u6CE8\u518C\u8005\uFF09`)]));
3643
+ }
3644
+ let extra = 0;
3645
+ for (const id of admins) {
3646
+ if (seen.has(id)) continue;
3647
+ seen.add(id);
3648
+ extra++;
3649
+ elements.push(actions([md(memberName(names, id)), button("\u{1F5D1} \u79FB\u9664", { a: DM.rmAdmin, u: id }, "danger")]));
3650
+ }
3651
+ if (extra === 0) elements.push(note("\u6682\u65E0\u989D\u5916\u7BA1\u7406\u5458\u3002"));
3652
+ elements.push(
3653
+ hr(),
3654
+ actions([button("\u2795 \u6DFB\u52A0\u7BA1\u7406\u5458", { a: DM.addAdminForm }, "primary"), button("\u2B05\uFE0F \u8BBE\u7F6E", { a: DM.settings })]),
3655
+ note("\u{1F451} Bot \u62E5\u6709\u8005\uFF08\u6CE8\u518C\u6B64 bot \u7684\u4EBA\uFF09\u6052\u4E3A\u7BA1\u7406\u5458\uFF0C\u4E0D\u53EF\u79FB\u9664\uFF1B\u540D\u5355\u4E3A\u7A7A\u65F6\u4EC5\u62E5\u6709\u8005\u53EF\u7BA1\u7406\u3002")
3656
+ );
3657
+ return card(elements, { header: { title: "\u{1F46E} \u7BA1\u7406\u5458", template: "blue" } });
3658
+ }
3659
+ function buildAddAdminCard(members) {
3660
+ const MAX = 50;
3661
+ const shown = members.slice(0, MAX);
3662
+ const formEls = [];
3663
+ if (shown.length > 0) {
3664
+ formEls.push(
3665
+ selectMenu({
3666
+ name: "pick",
3667
+ placeholder: "\u4ECE\u9879\u76EE\u7FA4\u6210\u5458\u9009\u62E9",
3668
+ options: shown.map((m) => ({ label: m.name, value: m.openId }))
3669
+ })
3670
+ );
3671
+ }
3672
+ formEls.push(
3673
+ input({
3674
+ name: "open_id",
3675
+ label: shown.length ? "\u6216\u76F4\u63A5\u8F93\u5165 open_id" : "\u8F93\u5165 open_id\uFF08\u672A\u8BFB\u53D6\u5230\u9879\u76EE\u7FA4\u6210\u5458\uFF09",
3676
+ placeholder: "ou_xxx"
3677
+ }),
3678
+ actions([submitButton("\u2705 \u786E\u8BA4\u6DFB\u52A0", { a: DM.addAdminSubmit }, "primary", "submit_admin")])
3679
+ );
3680
+ const tail = [];
3681
+ if (members.length > MAX) tail.push(note(`\u5019\u9009\u8F83\u591A\uFF0C\u4EC5\u5217\u524D ${MAX} \u4E2A\uFF1B\u5176\u4F59\u8BF7\u76F4\u63A5\u8F93\u5165 open_id\u3002`));
3682
+ return card(
3683
+ [
3684
+ md("**\u6DFB\u52A0\u7BA1\u7406\u5458** \xB7 \u4ECE\u9879\u76EE\u7FA4\u6210\u5458\u9009\uFF0C\u6216\u8F93\u5165 open_id"),
3685
+ form("add_admin", formEls),
3686
+ ...tail,
3687
+ actions([button("\u2B05\uFE0F \u53D6\u6D88", { a: DM.admins })])
3688
+ ],
3689
+ { header: { title: "\u2795 \u6DFB\u52A0\u7BA1\u7406\u5458", template: "blue" } }
3690
+ );
3691
+ }
3692
+ function buildProjectSettingsCard(project) {
3693
+ const kind = project.kind ?? "multi";
3694
+ const noMention = project.noMention ?? defaultNoMention(project);
3695
+ return card(
3696
+ [
3697
+ md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
3698
+ note(`${kindLabel(kind)}${project.cwd ? ` \xB7 \u{1F4C2} \`${project.cwd}\`` : ""}`),
3699
+ hr(),
3700
+ md("\u270B \u514D@\uFF08\u4E0D\u7528 @ \u4E5F\u56DE\u590D\uFF09"),
3701
+ actions([
3702
+ button("\u5F00", { a: DM.setNoMentionDm, v: "on", n: project.name }, noMention ? "primary" : "default"),
3703
+ button("\u5173", { a: DM.setNoMentionDm, v: "off", n: project.name }, noMention ? "default" : "primary")
3704
+ ]),
3705
+ note(
3706
+ 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\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002"
3707
+ ),
3708
+ hr(),
3709
+ actions([button("\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", { a: DM.allowlist, n: project.name }, "primary")]),
3710
+ note("\u8BBE\u7F6E\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex\uFF08\u7A7A = \u6240\u6709\u4EBA\uFF09\u3002"),
3711
+ hr(),
3712
+ actions([button("\u2B05\uFE0F \u9879\u76EE\u5217\u8868", { a: DM.projects })])
3713
+ ],
3714
+ { header: { title: "\u2699\uFE0F \u9879\u76EE\u8BBE\u7F6E", template: "blue" } }
3715
+ );
3716
+ }
3717
+ function buildAllowlistCard(project, names) {
3718
+ const list = project.allowedUsers ?? [];
3719
+ const elements = [md(`**\u54CD\u5E94\u767D\u540D\u5355** \xB7 ${project.name}`), note("\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex"), hr()];
3720
+ if (list.length === 0) {
3721
+ elements.push(note("\u5F53\u524D**\u6240\u6709\u4EBA**\u53EF\u7528\uFF08\u7BA1\u7406\u5458\u59CB\u7EC8\u53EF\u7528\uFF09\u3002"));
3722
+ } else {
3723
+ for (const id of list) {
3724
+ elements.push(
3725
+ actions([md(memberName(names, id)), button("\u{1F5D1} \u79FB\u9664", { a: DM.rmAllowed, u: id, n: project.name }, "danger")])
3726
+ );
3727
+ }
3728
+ }
3729
+ elements.push(
3730
+ hr(),
3731
+ actions([
3732
+ button("\u2795 \u6DFB\u52A0", { a: DM.addAllowedForm, n: project.name }, "primary"),
3733
+ button("\u2B05\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: project.name })
3734
+ ]),
3735
+ note("\u7BA1\u7406\u5458\u59CB\u7EC8\u53EF\u7528\uFF0C\u4E0D\u53D7\u6B64\u540D\u5355\u9650\u5236\uFF1B\u540D\u5355\u4E3A\u7A7A = \u6240\u6709\u4EBA\u53EF\u7528\u3002")
3736
+ );
3737
+ return card(elements, { header: { title: "\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", template: "blue" } });
3738
+ }
3739
+ function buildAddAllowedCard(projectName, members) {
3740
+ const MAX = 50;
3741
+ const shown = members.slice(0, MAX);
3742
+ const formEls = [];
3743
+ if (shown.length > 0) {
3744
+ formEls.push(
3745
+ selectMenu({
3746
+ name: "pick",
3747
+ placeholder: "\u4ECE\u7FA4\u6210\u5458\u9009\u62E9",
3748
+ options: shown.map((m) => ({ label: m.name, value: m.openId }))
3749
+ })
3750
+ );
3751
+ }
3752
+ formEls.push(
3753
+ input({
3754
+ name: "open_id",
3755
+ label: shown.length ? "\u6216\u76F4\u63A5\u8F93\u5165 open_id" : "\u8F93\u5165 open_id\uFF08\u672A\u8BFB\u53D6\u5230\u7FA4\u6210\u5458\uFF09",
3756
+ placeholder: "ou_xxx"
3757
+ }),
3758
+ actions([submitButton("\u2705 \u786E\u8BA4\u6DFB\u52A0", { a: DM.addAllowedSubmit, n: projectName }, "primary", "submit_allowed")])
3759
+ );
3760
+ const tail = [];
3761
+ if (members.length > MAX) tail.push(note(`\u7FA4\u6210\u5458\u8F83\u591A\uFF0C\u4EC5\u5217\u524D ${MAX} \u4E2A\uFF1B\u5176\u4F59\u8BF7\u76F4\u63A5\u8F93\u5165 open_id\u3002`));
3762
+ return card(
3763
+ [
3764
+ md(`**\u6DFB\u52A0\u53EF\u4F7F\u7528\u300C${projectName}\u300D\u7684\u4EBA**`),
3765
+ form("add_allowed", formEls),
3766
+ ...tail,
3767
+ actions([button("\u2B05\uFE0F \u53D6\u6D88", { a: DM.allowlist, n: projectName })])
3768
+ ],
3769
+ { header: { title: "\u2795 \u6DFB\u52A0\u767D\u540D\u5355\u6210\u5458", template: "blue" } }
3770
+ );
3771
+ }
3575
3772
 
3576
3773
  // src/service/update.ts
3577
3774
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
@@ -4845,6 +5042,59 @@ var Semaphore = class {
4845
5042
  };
4846
5043
 
4847
5044
  // src/bot/handle-message.ts
5045
+ async function resolveNames(channel, ids) {
5046
+ const uniq = [...new Set(ids.filter((x) => Boolean(x)))];
5047
+ const out = /* @__PURE__ */ new Map();
5048
+ if (uniq.length === 0) return out;
5049
+ try {
5050
+ const r = await channel.rawClient.contact.v3.user.batch({
5051
+ params: { user_ids: uniq, user_id_type: "open_id" }
5052
+ });
5053
+ for (const it of r.data?.items ?? []) {
5054
+ if (it.open_id && it.name) out.set(it.open_id, it.name);
5055
+ }
5056
+ } catch (err) {
5057
+ log.info("console", "resolve-names-fail", { n: uniq.length, err: String(err) });
5058
+ }
5059
+ return out;
5060
+ }
5061
+ async function fetchChatMembers(channel, chatId) {
5062
+ try {
5063
+ const r = await channel.rawClient.im.v1.chatMembers.get({
5064
+ path: { chat_id: chatId },
5065
+ params: { member_id_type: "open_id", page_size: 100 }
5066
+ });
5067
+ const out = [];
5068
+ for (const it of r.data?.items ?? []) {
5069
+ if (it.member_id) out.push({ openId: it.member_id, name: it.name || `\u2026${it.member_id.slice(-6)}` });
5070
+ }
5071
+ return out;
5072
+ } catch (err) {
5073
+ log.info("console", "fetch-members-fail", { chatId: chatId.slice(-6), err: String(err) });
5074
+ return [];
5075
+ }
5076
+ }
5077
+ async function fetchAllProjectMembers(channel) {
5078
+ const projects = await listProjects();
5079
+ const lists = await Promise.all(projects.filter((p) => p.chatId).map((p) => fetchChatMembers(channel, p.chatId)));
5080
+ const seen = /* @__PURE__ */ new Map();
5081
+ for (const members of lists) {
5082
+ for (const m of members) if (!seen.has(m.openId)) seen.set(m.openId, m.name);
5083
+ }
5084
+ return [...seen].map(([openId, name]) => ({ openId, name }));
5085
+ }
5086
+ function pickOpenId(formValue) {
5087
+ const raw = formValue?.pick;
5088
+ const cands = Array.isArray(raw) ? raw : [raw];
5089
+ for (const c of cands) {
5090
+ if (typeof c === "string" && c.startsWith("ou_")) return c;
5091
+ if (c && typeof c === "object") {
5092
+ const o = c;
5093
+ for (const v of [o.open_id, o.id, o.value]) if (typeof v === "string" && v.startsWith("ou_")) return v;
5094
+ }
5095
+ }
5096
+ return void 0;
5097
+ }
4848
5098
  function createOrchestrator(channel, cfg, fallbackCwd) {
4849
5099
  const backend = createBackend();
4850
5100
  const sessions = /* @__PURE__ */ new Map();
@@ -4922,7 +5172,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4922
5172
  }
4923
5173
  const project = await getProjectByChatId(msg.chatId);
4924
5174
  if (!msg.mentionedBot && !(project && shouldRespondWithoutMention(project, msg))) return;
4925
- if (!isChatAllowed(cfg, msg.chatId) || !isUserAllowed(cfg, msg.senderId)) {
5175
+ if (!isChatAllowed(cfg, msg.chatId) || !isUserAllowedInProject(cfg, project, msg.senderId)) {
4926
5176
  log.info("intake", "reject", { reason: "not_allowed", chatId: msg.chatId.slice(-6) });
4927
5177
  return;
4928
5178
  }
@@ -4996,9 +5246,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4996
5246
  if ((project.kind ?? "multi") === "single") return true;
4997
5247
  return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
4998
5248
  }
5249
+ async function denyAdminCommand(msg, cmd) {
5250
+ await channel.send(msg.chatId, { markdown: `\u26A0\uFE0F \`/${cmd}\` \u4EC5 bot \u7BA1\u7406\u5458\u53EF\u7528\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
5251
+ log.info("intake", "cmd-denied", { cmd });
5252
+ }
4999
5253
  async function postGroupSettings(msg, project) {
5000
5254
  if (!isAdmin(cfg, msg.senderId)) {
5001
- await channel.send(msg.chatId, { markdown: "\u4EC5\u7BA1\u7406\u5458\u53EF\u6539\u7FA4\u8BBE\u7F6E\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
5255
+ await denyAdminCommand(msg, "settings");
5002
5256
  return;
5003
5257
  }
5004
5258
  if (!project) {
@@ -5150,6 +5404,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5150
5404
  }).catch((err) => log.fail("intake", err));
5151
5405
  }
5152
5406
  async function postResumeCard(msg) {
5407
+ if (!isAdmin(cfg, msg.senderId)) {
5408
+ await denyAdminCommand(msg, "resume");
5409
+ return;
5410
+ }
5153
5411
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5154
5412
  const project = await getProjectByChatId(msg.chatId);
5155
5413
  const cwd = project?.cwd ?? fallbackCwd;
@@ -5191,7 +5449,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5191
5449
  async function postHelpCard(msg, scope, inThread = false, project) {
5192
5450
  const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
5193
5451
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5194
- await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention), msg.messageId, inThread).catch(
5452
+ await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention, isAdmin(cfg, msg.senderId)), msg.messageId, inThread).catch(
5195
5453
  (err) => log.fail("card", err, { cmd: "help", scope })
5196
5454
  );
5197
5455
  log.info("card", "help", { scope });
@@ -5230,7 +5488,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5230
5488
  return void 0;
5231
5489
  }
5232
5490
  const op = evt.operator?.openId ?? "";
5233
- if (op !== state.requesterOpenId || !isChatAllowed(cfg, state.chatId) || !isUserAllowed(cfg, op)) {
5491
+ if (op !== state.requesterOpenId || !isChatAllowed(cfg, state.chatId)) {
5234
5492
  log.info("card", "action-denied", { reason: "not-allowed" });
5235
5493
  return void 0;
5236
5494
  }
@@ -5266,7 +5524,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5266
5524
  settleUpdate(evt.messageId, buildResumeLaunchingCard(state));
5267
5525
  void resumeFromCard(evt, state, codexThreadId);
5268
5526
  });
5269
- const runAllowed = (evt) => isChatAllowed(cfg, evt.chatId) && isUserAllowed(cfg, evt.operator?.openId ?? "");
5527
+ const runAllowed = (evt) => isChatAllowed(cfg, evt.chatId);
5270
5528
  const runOwnerOrAdmin = (evt, ownerOpenId) => {
5271
5529
  if (!runAllowed(evt)) return false;
5272
5530
  const op = evt.operator?.openId ?? "";
@@ -5281,6 +5539,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5281
5539
  });
5282
5540
  const dmAdmin = (openId) => isAdmin(cfg, openId ?? "");
5283
5541
  const patch = (evt, c) => settleUpdate(evt.messageId, c, evt.chatId);
5542
+ const namesWithOperator = async (evt, ids) => {
5543
+ const m = await resolveNames(channel, ids);
5544
+ if (ids.some((id) => id && !m.has(id))) {
5545
+ for (const mem of await fetchAllProjectMembers(channel)) if (!m.has(mem.openId)) m.set(mem.openId, mem.name);
5546
+ }
5547
+ const op = evt.operator;
5548
+ if (op?.openId && op.name && !m.has(op.openId)) m.set(op.openId, op.name);
5549
+ return m;
5550
+ };
5284
5551
  function applyPref(evt, mut) {
5285
5552
  if (!dmAdmin(evt.operator?.openId)) return;
5286
5553
  const prefs = { ...cfg.preferences ?? {} };
@@ -5504,6 +5771,107 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5504
5771
  }
5505
5772
  return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", noMention: on });
5506
5773
  });
5774
+ }).on(DM.admins, ({ evt }) => {
5775
+ if (!dmAdmin(evt.operator?.openId)) return;
5776
+ patch(
5777
+ evt,
5778
+ async () => buildAdminsCard(cfg, await namesWithOperator(evt, [resolveOwner(cfg), ...cfg.preferences?.access?.admins ?? []]))
5779
+ );
5780
+ }).on(DM.addAdminForm, ({ evt }) => {
5781
+ if (!dmAdmin(evt.operator?.openId)) return;
5782
+ patch(evt, async () => {
5783
+ const all = await fetchAllProjectMembers(channel);
5784
+ const members = all.filter((m) => !isAdmin(cfg, m.openId));
5785
+ return buildAddAdminCard(members);
5786
+ });
5787
+ }).on(DM.addAdminSubmit, ({ evt, formValue }) => {
5788
+ if (!dmAdmin(evt.operator?.openId)) return;
5789
+ const manual = String(formValue?.open_id ?? "").trim();
5790
+ const id = manual.startsWith("ou_") ? manual : pickOpenId(formValue);
5791
+ log.info("console", "admin-add", { picked: id?.slice(-6) ?? null });
5792
+ void (async () => {
5793
+ if (id) {
5794
+ const access = { ...cfg.preferences?.access ?? {} };
5795
+ access.ownerOpenId ??= resolveOwner(cfg);
5796
+ access.admins = Array.from(/* @__PURE__ */ new Set([...access.admins ?? [], id]));
5797
+ cfg.preferences = { ...cfg.preferences ?? {}, access };
5798
+ await saveConfig(cfg).catch((e) => log.fail("console", e, { phase: "save-config" }));
5799
+ }
5800
+ const ids = [resolveOwner(cfg), ...cfg.preferences?.access?.admins ?? []];
5801
+ const next = buildAdminsCard(cfg, await namesWithOperator(evt, ids));
5802
+ await sendManagedCard(channel, evt.chatId, next).catch((e) => log.fail("console", e, { phase: "admin-add-result" }));
5803
+ })();
5804
+ }).on(DM.rmAdmin, ({ evt, value }) => {
5805
+ if (!dmAdmin(evt.operator?.openId)) return;
5806
+ const id = typeof value.u === "string" ? value.u : "";
5807
+ patch(evt, async () => {
5808
+ if (id && id !== resolveOwner(cfg)) {
5809
+ const access = { ...cfg.preferences?.access ?? {} };
5810
+ access.ownerOpenId ??= resolveOwner(cfg);
5811
+ access.admins = (access.admins ?? []).filter((x) => x !== id);
5812
+ cfg.preferences = { ...cfg.preferences ?? {}, access };
5813
+ await saveConfig(cfg).catch((e) => log.fail("console", e, { phase: "save-config" }));
5814
+ }
5815
+ const ids = [resolveOwner(cfg), ...cfg.preferences?.access?.admins ?? []];
5816
+ return buildAdminsCard(cfg, await namesWithOperator(evt, ids));
5817
+ });
5818
+ }).on(DM.allowlist, ({ evt, value }) => {
5819
+ if (!dmAdmin(evt.operator?.openId)) return;
5820
+ const name = typeof value.n === "string" ? value.n : "";
5821
+ patch(evt, async () => {
5822
+ const p = await getProjectByName(name);
5823
+ if (!p) return buildDmMenuCard();
5824
+ return buildAllowlistCard(p, await namesWithOperator(evt, p.allowedUsers ?? []));
5825
+ });
5826
+ }).on(DM.addAllowedForm, ({ evt, value }) => {
5827
+ if (!dmAdmin(evt.operator?.openId)) return;
5828
+ const name = typeof value.n === "string" ? value.n : "";
5829
+ if (!name) return;
5830
+ patch(evt, async () => {
5831
+ const p = await getProjectByName(name);
5832
+ const members = p?.chatId ? await fetchChatMembers(channel, p.chatId) : [];
5833
+ return buildAddAllowedCard(name, members);
5834
+ });
5835
+ }).on(DM.addAllowedSubmit, ({ evt, value, formValue }) => {
5836
+ if (!dmAdmin(evt.operator?.openId)) return;
5837
+ const name = typeof value.n === "string" ? value.n : "";
5838
+ const manual = String(formValue?.open_id ?? "").trim();
5839
+ const id = manual.startsWith("ou_") ? manual : pickOpenId(formValue);
5840
+ log.info("console", "allow-add", { project: name, picked: id?.slice(-6) ?? null });
5841
+ void (async () => {
5842
+ if (id) await updateProject(name, (p) => ({ allowedUsers: Array.from(/* @__PURE__ */ new Set([...p.allowedUsers ?? [], id])) }));
5843
+ const fresh = await getProjectByName(name);
5844
+ if (!fresh) return;
5845
+ const card2 = buildAllowlistCard(fresh, await namesWithOperator(evt, fresh.allowedUsers ?? []));
5846
+ await sendManagedCard(channel, evt.chatId, card2).catch((e) => log.fail("console", e, { phase: "allow-add-result" }));
5847
+ })();
5848
+ }).on(DM.rmAllowed, ({ evt, value }) => {
5849
+ if (!dmAdmin(evt.operator?.openId)) return;
5850
+ const id = typeof value.u === "string" ? value.u : "";
5851
+ const name = typeof value.n === "string" ? value.n : "";
5852
+ patch(evt, async () => {
5853
+ await updateProject(name, (p) => ({ allowedUsers: (p.allowedUsers ?? []).filter((x) => x !== id) }));
5854
+ const fresh = await getProjectByName(name);
5855
+ if (!fresh) return buildDmMenuCard();
5856
+ return buildAllowlistCard(fresh, await namesWithOperator(evt, fresh.allowedUsers ?? []));
5857
+ });
5858
+ }).on(DM.projectSettings, ({ evt, value }) => {
5859
+ if (!dmAdmin(evt.operator?.openId)) return;
5860
+ const name = typeof value.n === "string" ? value.n : "";
5861
+ patch(evt, async () => {
5862
+ const p = await getProjectByName(name);
5863
+ return p ? buildProjectSettingsCard(p) : buildDmMenuCard();
5864
+ });
5865
+ }).on(DM.setNoMentionDm, ({ evt, value }) => {
5866
+ if (!dmAdmin(evt.operator?.openId)) return;
5867
+ const name = typeof value.n === "string" ? value.n : "";
5868
+ const on = value.v === "on";
5869
+ patch(evt, async () => {
5870
+ const p = await getProjectByName(name);
5871
+ if (!p) return buildDmMenuCard();
5872
+ await updateProject(name, { noMention: on });
5873
+ return buildProjectSettingsCard({ ...p, noMention: on });
5874
+ });
5507
5875
  });
5508
5876
  async function resumeFromCard(evt, state, codexThreadId) {
5509
5877
  try {
@@ -5753,8 +6121,6 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5753
6121
  if (!evt.mentionedBot) return log.info("comment", "skip", { reason: "not-mentioned" });
5754
6122
  if (!SUPPORTED_FILE_TYPES.has(evt.fileType))
5755
6123
  return log.info("comment", "skip", { reason: "unsupported-fileType", fileType: evt.fileType });
5756
- if (!isUserAllowed(cfg, evt.operator.openId))
5757
- return log.info("comment", "skip", { reason: "not-allowed" });
5758
6124
  const resolved = await resolveComment(channel, evt);
5759
6125
  if (!resolved) return log.info("comment", "skip", { reason: "no-target-or-empty" });
5760
6126
  const { target, ctx } = resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {