@modelzen/feishu-codex-bridge 0.3.1 → 0.3.3

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 +16 -9
  2. package/dist/cli.js +643 -96
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,7 +33,7 @@
33
33
  - **私聊控制台**:私聊机器人弹交互菜单 —— 新建项目、项目列表、设置、诊断、重连。
34
34
  - **稳定隔离**:每会话独立 app-server 进程;卡死有 watchdog(默认 120s)→ 终止 → 回收,异常不波及其他群。
35
35
  - **本地加密密钥库**:飞书应用密钥用 AES-256-GCM 存在 `~/.feishu-codex-bridge/`,不入仓库、不进环境变量。
36
- - **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。
36
+ - **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。注:跨平台指进程运行与后台自启;「项目内只读/读写」隐私沙箱仅 macOS / 原生 Windows 可强制(见[安全须知](#-安全须知))。
37
37
 
38
38
  ---
39
39
 
@@ -41,7 +41,7 @@
41
41
 
42
42
  | 依赖 | 说明 | 获取方式 |
43
43
  |------|------|----------|
44
- | **操作系统** | **macOS / Windows** 均支持;Linux·WSL 为 best-effort(已实现 systemd,未广泛实测) | — |
44
+ | **操作系统** | 运行/后台常驻:**macOS / Windows** 均支持,Linux·WSL 为 best-effort(已实现 systemd,未广泛实测)。注意:「项目内只读 / 读写」隐私档的沙箱强制仅 **macOS / 原生 Windows**,Linux·WSL 上这两档会 fail-closed 拒绝启动(见下方[安全须知](#-安全须知)) | — |
45
45
  | **Node.js ≥ 20** | 运行时 | <https://nodejs.org> 或 `nvm install 20` |
46
46
  | **Codex CLI** | 后端,bridge 会 spawn `codex app-server` | `npm i -g @openai/codex`,或装 Codex.app,或用 `CODEX_BIN` 指向已有二进制 |
47
47
  | **Codex 已登录** | app-server 需要 `~/.codex/auth.json` | `codex login` |
@@ -241,17 +241,24 @@ src/
241
241
 
242
242
  ## ⚠️ 安全须知
243
243
 
244
- 本机器人调用 Codex 时固定使用 **`approvalPolicy: "never"` + `sandbox: "danger-full-access"`** —— **无任何人工审批、对磁盘完全访问**。这意味着:
244
+ 机器人调用 Codex 始终是 **`approvalPolicy: "never"`**(无人工逐条审批),**沙箱就是唯一的安全闸门**。每个项目有一档**权限**,在私聊控制台「📁 项目列表 ⚙️ 设置 🔐 权限」里用下拉框选择后提交:
245
245
 
246
- > **任何能在项目群里给机器人发消息的人,都能在你这台机器上、以你的身份、在该项目目录里执行任意命令(读写文件、联网、运行脚本)。**
246
+ | 档位 | 能读 | 能写 | 适用 |
247
+ |------|------|------|------|
248
+ | 🔒 **项目内只读** | 仅项目文件夹 | ✗ | 外部群 / 不可信场景的问答机器人 |
249
+ | ✏️ **项目内读写** | 仅项目文件夹 | 仅项目文件夹 | 自己的编码项目,但禁止它碰机器其余部分 |
250
+ | ⚠️ **完全访问** | 整台电脑 | 整台电脑 | 完全信任、你自己掌控的机器 |
247
251
 
248
- 因此:
249
-
250
- - 只把**你信任的人**拉进项目群;
251
- - 在**你自己掌控的机器/账号**上运行,最好是隔离的开发机或容器;
252
- - 绑定的项目目录里不要放你不愿被读写的敏感数据;
252
+ - **管理员 / 普通用户可分设**:🔐 权限里有「管理员档」和「普通用户档」两个下拉。两档**不同**时,管理员与群里其他人**各用独立的 Codex 线程**(互不串沙箱、也互不串对话历史)——典型:外部群里管理员 `完全访问`、其他人 `项目内只读`。两档**相同** = 所有人一致(默认)。
253
+ - **默认值**:你自己新建的项目群 = `完全访问`(与历史行为一致);**别人把机器人拉进的存量/外部群** = `项目内只读`;普通用户档默认**同管理员档**(不分档)。**升级前没有 `mode` 字段的老项目按 `完全访问` 处理**,行为不变。
254
+ - 🔒/✏️ 靠 Codex 的自定义 permissions 档把读写都**锁死在项目文件夹内**(读不到 `~/.ssh`、`/etc` 等),由操作系统沙箱强制:**macOS(Seatbelt)与原生 Windows(restricted token)可强制**,其中 Windows 需 Codex 以 elevated 沙箱运行、否则它会**拒绝执行**(仍不泄漏)。**Linux / WSL 无法强制读限定**(沙箱只挡写、不限读,Landlock 读限制尚未实现,WSL 等同 Linux)——在这些平台选 🔒/✏️ 会被**直接拒绝启动(fail-closed),绝不静默降级为完全访问**;要在 Linux/WSL 用,请把 Codex 跑在容器/隔离环境里。
255
+ > Windows 上的强制是 Codex 自己做的,请先在真机自测一次(让机器人读项目文件夹外的文件,应被拒)再用于真实外部群。
256
+ - ⚠️ `完全访问` 档意味着:**任何能给机器人发消息的人,都能在你这台机器上、以你的身份执行任意命令(读写文件、联网、跑脚本)**。这一档只把**你信任的人**拉进群,在**你自己掌控的隔离机器**上跑,目录里别放不愿被读写的敏感数据。
257
+ - 「联网」是档位之外的独立开关,只影响它执行的 shell 命令能否上网,不影响模型本身和 Codex 自带的联网搜索。
253
258
  - 它不是多租户托管服务,是给你(和你信任的小团队)自用的桥。
254
259
 
260
+ > 把机器人拉进**外部群**做只读问答前,先在飞书开发者后台开启应用的「可被添加到外部群 / 外部可用范围」,再由群里的真人手动把机器人加进群(机器人无法自行加入)。
261
+
255
262
  ---
256
263
 
257
264
  ## ❓ 故障排查
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];
@@ -1421,7 +1442,28 @@ function mapItemComplete(item) {
1421
1442
 
1422
1443
  // src/agent/codex-appserver/backend.ts
1423
1444
  var APPROVAL_POLICY = "never";
1424
- var SANDBOX = "danger-full-access";
1445
+ function sandboxParams(mode, network) {
1446
+ if ((mode ?? "full") === "full") return { sandbox: "danger-full-access" };
1447
+ if (process.platform !== "darwin" && process.platform !== "win32") {
1448
+ throw new Error(
1449
+ "\u300C\u9879\u76EE\u5185\u53EA\u8BFB / \u9879\u76EE\u5185\u8BFB\u5199\u300D\u9760\u64CD\u4F5C\u7CFB\u7EDF\u6C99\u7BB1\u628A\u8BFB\u5199\u9501\u8FDB\u9879\u76EE\u6587\u4EF6\u5939\uFF0C\u76EE\u524D\u53EA\u6709 macOS \u4E0E\u539F\u751F Windows \u80FD\u5F3A\u5236\u6267\u884C\u3002\u5F53\u524D\u5E73\u53F0\uFF08Linux / WSL \u53EA\u6321\u5199\u3001\u4E0D\u9650\u5236\u8BFB\u53D6\uFF0C\u65E0\u6CD5\u4FDD\u8BC1\u4E0D\u6CC4\u9732\u9690\u79C1\uFF09\u5DF2\u62D2\u7EDD\u542F\u52A8\uFF08\u7EDD\u4E0D\u964D\u7EA7\u4E3A\u5B8C\u5168\u8BBF\u95EE\uFF09\u3002\u8BF7\u6539\u7528\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u3001\u628A Codex \u8DD1\u8FDB\u5BB9\u5668/\u9694\u79BB\u73AF\u5883\uFF0C\u6216\u5728 macOS / Windows \u4E0A\u8FD0\u884C\u3002"
1450
+ );
1451
+ }
1452
+ return {
1453
+ config: {
1454
+ default_permissions: "feishu",
1455
+ permissions: {
1456
+ feishu: {
1457
+ filesystem: {
1458
+ ":minimal": "read",
1459
+ ":workspace_roots": { ".": mode === "write" ? "write" : "read" }
1460
+ },
1461
+ network: { enabled: Boolean(network) }
1462
+ }
1463
+ }
1464
+ }
1465
+ };
1466
+ }
1425
1467
  var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1426
1468
  "\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",
1427
1469
  "",
@@ -1606,23 +1648,25 @@ var CodexAppServerBackend = class {
1606
1648
  }
1607
1649
  }
1608
1650
  async startThread(opts) {
1651
+ const sandbox = sandboxParams(opts.mode, opts.network);
1609
1652
  const client = await this.spawn(opts.cwd);
1610
1653
  const res = await client.request("thread/start", {
1611
1654
  cwd: opts.cwd,
1612
1655
  approvalPolicy: APPROVAL_POLICY,
1613
- sandbox: SANDBOX,
1656
+ ...sandbox,
1614
1657
  developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1615
1658
  ...opts.model ? { model: opts.model } : {}
1616
1659
  });
1617
1660
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1618
1661
  }
1619
1662
  async resumeThread(opts) {
1663
+ const sandbox = sandboxParams(opts.mode, opts.network);
1620
1664
  const client = await this.spawn(opts.cwd);
1621
1665
  const res = await client.request("thread/resume", {
1622
1666
  threadId: opts.codexThreadId,
1623
1667
  cwd: opts.cwd,
1624
1668
  approvalPolicy: APPROVAL_POLICY,
1625
- sandbox: SANDBOX,
1669
+ ...sandbox,
1626
1670
  developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1627
1671
  ...opts.model ? { model: opts.model } : {}
1628
1672
  });
@@ -2168,6 +2212,15 @@ function selectStatic(opts) {
2168
2212
  behaviors: [{ type: "callback", value: { a: opts.actionId } }]
2169
2213
  };
2170
2214
  }
2215
+ function selectMenu(opts) {
2216
+ return {
2217
+ tag: "select_static",
2218
+ name: opts.name,
2219
+ placeholder: { tag: "plain_text", content: opts.placeholder },
2220
+ ...opts.initial ? { initial_option: opts.initial } : {},
2221
+ options: opts.options.map((o) => ({ text: { tag: "plain_text", content: o.label }, value: o.value }))
2222
+ };
2223
+ }
2171
2224
 
2172
2225
  // src/card/command-cards.ts
2173
2226
  var MC = {
@@ -2275,19 +2328,13 @@ function pickerTime(unixSeconds) {
2275
2328
  function talkLine(noMention, tail) {
2276
2329
  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
2330
  }
2278
- function buildHelpCard(scope, noMention = true) {
2331
+ function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2279
2332
  const elements = [];
2280
2333
  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
- );
2334
+ const lines = [talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"), "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"];
2335
+ if (isAdmin2) lines.push("\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2336
+ lines.push("\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2337
+ 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
2338
  } else if (scope === "topic") {
2292
2339
  elements.push(
2293
2340
  md("\u{1F9F5} **\u8BDD\u9898\u5185** \u2014 \u6BCF\u4E2A\u8BDD\u9898\u662F\u4E00\u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
@@ -2300,13 +2347,10 @@ function buildHelpCard(scope, noMention = true) {
2300
2347
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
2301
2348
  );
2302
2349
  } 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
- );
2350
+ const lines = ["\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB"];
2351
+ if (isAdmin2) lines.push("\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD", "\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2352
+ lines.push("\xB7 `/model` \u2192 \u9700\u8981\u5728\u8BDD\u9898\u91CC\u7528", "\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2353
+ 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
2354
  }
2311
2355
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
2312
2356
  }
@@ -3113,10 +3157,26 @@ async function uploadBuffer(channel, buffer) {
3113
3157
 
3114
3158
  // src/project/registry.ts
3115
3159
  import { mkdir as mkdir4, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "fs/promises";
3160
+ import { randomUUID as randomUUID2 } from "crypto";
3116
3161
  import { dirname as dirname5 } from "path";
3117
3162
  function defaultNoMention(p) {
3118
3163
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
3119
3164
  }
3165
+ function effectiveMode(p) {
3166
+ return p.mode ?? "full";
3167
+ }
3168
+ function effectiveGuestMode(p) {
3169
+ return p.guestMode ?? effectiveMode(p);
3170
+ }
3171
+ function turnTier(p, isAdminSender) {
3172
+ const adminTier = effectiveMode(p);
3173
+ const guestTier = effectiveGuestMode(p);
3174
+ return {
3175
+ mode: isAdminSender ? adminTier : guestTier,
3176
+ role: isAdminSender ? "admin" : "guest",
3177
+ split: guestTier !== adminTier
3178
+ };
3179
+ }
3120
3180
  var FILE_VERSION2 = 1;
3121
3181
  async function read() {
3122
3182
  try {
@@ -3128,9 +3188,18 @@ async function read() {
3128
3188
  throw err;
3129
3189
  }
3130
3190
  }
3191
+ var opChain = Promise.resolve();
3192
+ function withLock(fn) {
3193
+ const run = opChain.then(fn, fn);
3194
+ opChain = run.then(
3195
+ () => void 0,
3196
+ () => void 0
3197
+ );
3198
+ return run;
3199
+ }
3131
3200
  async function write(projects) {
3132
3201
  await mkdir4(dirname5(paths.projectsFile), { recursive: true });
3133
- const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
3202
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}-${randomUUID2()}`;
3134
3203
  const body = { version: FILE_VERSION2, projects };
3135
3204
  await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
3136
3205
  `, "utf8");
@@ -3146,34 +3215,41 @@ async function getProjectByName(name) {
3146
3215
  return (await read()).find((p) => p.name === name);
3147
3216
  }
3148
3217
  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);
3218
+ return withLock(async () => {
3219
+ const projects = await read();
3220
+ if (projects.some((x) => x.name === p.name)) {
3221
+ throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3222
+ }
3223
+ if (p.chatId) {
3224
+ const bound = projects.find((x) => x.chatId === p.chatId);
3225
+ if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
3226
+ }
3227
+ projects.push(p);
3228
+ await write(projects);
3229
+ });
3159
3230
  }
3160
3231
  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);
3232
+ return withLock(async () => {
3233
+ const projects = await read();
3234
+ const p = projects.find((x) => x.name === name);
3235
+ if (!p) return;
3236
+ const actual = typeof patch === "function" ? patch(p) : patch;
3237
+ const target = p;
3238
+ for (const [k, v] of Object.entries(actual)) {
3239
+ if (v !== void 0) target[k] = v;
3240
+ }
3241
+ await write(projects);
3242
+ });
3169
3243
  }
3170
3244
  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;
3245
+ return withLock(async () => {
3246
+ const projects = await read();
3247
+ const idx = projects.findIndex((p) => p.name === name);
3248
+ if (idx === -1) return void 0;
3249
+ const [removed] = projects.splice(idx, 1);
3250
+ await write(projects);
3251
+ return removed;
3252
+ });
3177
3253
  }
3178
3254
 
3179
3255
  // src/card/dm-cards.ts
@@ -3198,7 +3274,22 @@ var DM = {
3198
3274
  setTools: "dm.set.tools",
3199
3275
  setWatchdog: "dm.set.watchdog",
3200
3276
  setPending: "dm.set.pending",
3201
- setConcurrency: "dm.set.concurrency"
3277
+ setConcurrency: "dm.set.concurrency",
3278
+ // 权限管理:全局 admins(settings 卡进入)+ 项目响应白名单(项目列表 / 建项目完成卡进入)
3279
+ admins: "dm.admins",
3280
+ addAdminForm: "dm.admin.addForm",
3281
+ addAdminSubmit: "dm.admin.addSubmit",
3282
+ rmAdmin: "dm.admin.rm",
3283
+ allowlist: "dm.allowlist",
3284
+ addAllowedForm: "dm.allow.addForm",
3285
+ addAllowedSubmit: "dm.allow.addSubmit",
3286
+ rmAllowed: "dm.allow.rm",
3287
+ // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3288
+ projectSettings: "dm.projectSettings",
3289
+ setNoMentionDm: "dm.proj.noMention",
3290
+ // 🔐 权限:codex 沙箱档位(管理员档 + 普通用户档)+ 联网,做成下拉表单(选+提交)
3291
+ permission: "dm.proj.perm",
3292
+ permissionSubmit: "dm.proj.perm.submit"
3202
3293
  };
3203
3294
  var GS = {
3204
3295
  setNoMention: "gs.noMention"
@@ -3464,7 +3555,13 @@ function buildNewProjectDoneCard(p) {
3464
3555
  note(`\u{1F4C2} \`${p.cwd}\` \xB7 ${kindLabel(p.kind)}`),
3465
3556
  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
3557
  ];
3467
- if (p.chatId) elements.push(actions([linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary")]));
3558
+ if (p.chatId)
3559
+ elements.push(
3560
+ actions([
3561
+ linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary"),
3562
+ button("\u2699\uFE0F \u9879\u76EE\u8BBE\u7F6E", { a: DM.projectSettings, n: p.name })
3563
+ ])
3564
+ );
3468
3565
  return card(elements, { header: { title, template: "green" } });
3469
3566
  }
3470
3567
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
@@ -3495,6 +3592,7 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3495
3592
  }
3496
3593
  const row = [];
3497
3594
  if (p.chatId) row.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId)));
3595
+ row.push(button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: p.name }));
3498
3596
  row.push(button("\u{1F5D1} \u5220\u9664", { a: DM.rmConfirm, n: p.name }, "danger"));
3499
3597
  elements.push(actions(row));
3500
3598
  elements.push(hr());
@@ -3549,7 +3647,8 @@ function buildSettingsCard(cfg) {
3549
3647
  { label: "20", value: "20" }
3550
3648
  ]),
3551
3649
  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 })])
3650
+ hr(),
3651
+ actions([button("\u{1F46E} \u7BA1\u7406\u5458", { a: DM.admins }), button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
3553
3652
  ],
3554
3653
  { header: { title: "\u2699\uFE0F \u8BBE\u7F6E", template: "blue" } }
3555
3654
  );
@@ -3572,6 +3671,199 @@ function buildGroupSettingsCard(project) {
3572
3671
  { header: { title: "\u2699\uFE0F \u7FA4\u8BBE\u7F6E", template: "blue" } }
3573
3672
  );
3574
3673
  }
3674
+ function memberName(names, id) {
3675
+ return names.get(id) ?? `\u2026${id.slice(-6)}`;
3676
+ }
3677
+ function buildAdminsCard(cfg, names) {
3678
+ const owner = resolveOwner(cfg);
3679
+ const admins = cfg.preferences?.access?.admins ?? [];
3680
+ 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()];
3681
+ const seen = /* @__PURE__ */ new Set();
3682
+ if (owner) {
3683
+ seen.add(owner);
3684
+ elements.push(actions([md(`\u{1F451} **${memberName(names, owner)}** \xB7 Bot \u62E5\u6709\u8005\uFF08\u6CE8\u518C\u8005\uFF09`)]));
3685
+ }
3686
+ let extra = 0;
3687
+ for (const id of admins) {
3688
+ if (seen.has(id)) continue;
3689
+ seen.add(id);
3690
+ extra++;
3691
+ elements.push(actions([md(memberName(names, id)), button("\u{1F5D1} \u79FB\u9664", { a: DM.rmAdmin, u: id }, "danger")]));
3692
+ }
3693
+ if (extra === 0) elements.push(note("\u6682\u65E0\u989D\u5916\u7BA1\u7406\u5458\u3002"));
3694
+ elements.push(
3695
+ hr(),
3696
+ actions([button("\u2795 \u6DFB\u52A0\u7BA1\u7406\u5458", { a: DM.addAdminForm }, "primary"), button("\u2B05\uFE0F \u8BBE\u7F6E", { a: DM.settings })]),
3697
+ 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")
3698
+ );
3699
+ return card(elements, { header: { title: "\u{1F46E} \u7BA1\u7406\u5458", template: "blue" } });
3700
+ }
3701
+ function buildAddAdminCard(members) {
3702
+ const MAX = 50;
3703
+ const shown = members.slice(0, MAX);
3704
+ const formEls = [];
3705
+ if (shown.length > 0) {
3706
+ formEls.push(
3707
+ selectMenu({
3708
+ name: "pick",
3709
+ placeholder: "\u4ECE\u9879\u76EE\u7FA4\u6210\u5458\u9009\u62E9",
3710
+ options: shown.map((m) => ({ label: m.name, value: m.openId }))
3711
+ })
3712
+ );
3713
+ }
3714
+ formEls.push(
3715
+ input({
3716
+ name: "open_id",
3717
+ 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",
3718
+ placeholder: "ou_xxx"
3719
+ }),
3720
+ actions([submitButton("\u2705 \u786E\u8BA4\u6DFB\u52A0", { a: DM.addAdminSubmit }, "primary", "submit_admin")])
3721
+ );
3722
+ const tail = [];
3723
+ 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`));
3724
+ return card(
3725
+ [
3726
+ md("**\u6DFB\u52A0\u7BA1\u7406\u5458** \xB7 \u4ECE\u9879\u76EE\u7FA4\u6210\u5458\u9009\uFF0C\u6216\u8F93\u5165 open_id"),
3727
+ form("add_admin", formEls),
3728
+ ...tail,
3729
+ actions([button("\u2B05\uFE0F \u53D6\u6D88", { a: DM.admins })])
3730
+ ],
3731
+ { header: { title: "\u2795 \u6DFB\u52A0\u7BA1\u7406\u5458", template: "blue" } }
3732
+ );
3733
+ }
3734
+ var MODE_OPTS = [
3735
+ { value: "qa", label: "\u{1F512} \u9879\u76EE\u5185\u53EA\u8BFB", desc: "\u53EA\u80FD\u67E5\u770B\u9879\u76EE\u6587\u4EF6\u5939\u91CC\u7684\u5185\u5BB9\uFF0C\u4E0D\u4F1A\u6539\u4EFB\u4F55\u6587\u4EF6" },
3736
+ { value: "write", label: "\u270F\uFE0F \u9879\u76EE\u5185\u8BFB\u5199", desc: "\u80FD\u67E5\u770B\u5E76\u4FEE\u6539\u9879\u76EE\u6587\u4EF6\u5939\u91CC\u7684\u6587\u4EF6\uFF0C\u4F46\u78B0\u4E0D\u5230\u6587\u4EF6\u5939\u5916" },
3737
+ { value: "full", label: "\u26A0\uFE0F \u5B8C\u5168\u8BBF\u95EE", desc: "\u80FD\u8BFB\u5199\u6574\u53F0\u7535\u8111\u4E0A\u7684\u4EFB\u4F55\u6587\u4EF6" }
3738
+ ];
3739
+ function tierLabel(m) {
3740
+ return MODE_OPTS.find((o) => o.value === m)?.label ?? m;
3741
+ }
3742
+ var TIER_SELECT_OPTS = MODE_OPTS.map((o) => ({ label: `${o.label} \u2014 ${o.desc}`, value: o.value }));
3743
+ function permissionSummary(p) {
3744
+ const admin = effectiveMode(p);
3745
+ const guest = effectiveGuestMode(p);
3746
+ return admin === guest ? `\u6240\u6709\u4EBA\uFF1A${tierLabel(admin)}` : `\u7BA1\u7406\u5458\uFF1A${tierLabel(admin)}\u3000\xB7\u3000\u5176\u4ED6\u4EBA\uFF1A${tierLabel(guest)}`;
3747
+ }
3748
+ function buildPermissionCard(p) {
3749
+ const network = p.network ?? false;
3750
+ return card(
3751
+ [
3752
+ md(`**\u{1F510} \u6743\u9650** \xB7 ${p.name}`),
3753
+ note(
3754
+ "codex \u6C99\u7BB1\u7684\u8BBF\u95EE\u8303\u56F4\u3002\u300C\u7BA1\u7406\u5458\u6863\u300D\u7ED9 owner / \u7BA1\u7406\u5458\uFF0C\u300C\u666E\u901A\u7528\u6237\u6863\u300D\u7ED9\u7FA4\u91CC\u5176\u4ED6\u4EBA\u3002\u4E24\u6863**\u4E0D\u540C**\u65F6\uFF0C\u4E24\u7C7B\u4EBA\u5404\u7528\u72EC\u7ACB\u7EBF\u7A0B\uFF08\u4E92\u4E0D\u4E32\u6C99\u7BB1\u4E0E\u5BF9\u8BDD\u5386\u53F2\uFF09\uFF1B**\u76F8\u540C**\u5219\u6240\u6709\u4EBA\u4E00\u81F4\u3002"
3755
+ ),
3756
+ form("perm", [
3757
+ md("\u{1F451} **\u7BA1\u7406\u5458\u6863**"),
3758
+ selectMenu({ name: "mode", placeholder: "\u9009\u62E9\u7BA1\u7406\u5458\u6743\u9650\u6863", options: TIER_SELECT_OPTS, initial: effectiveMode(p) }),
3759
+ md("\u{1F465} **\u666E\u901A\u7528\u6237\u6863**"),
3760
+ selectMenu({
3761
+ name: "guestMode",
3762
+ placeholder: "\u9009\u62E9\u666E\u901A\u7528\u6237\u6743\u9650\u6863",
3763
+ options: TIER_SELECT_OPTS,
3764
+ initial: effectiveGuestMode(p)
3765
+ }),
3766
+ md("\u{1F310} **\u8054\u7F51**\uFF08\u53EA\u5BF9\u53EA\u8BFB / \u8BFB\u5199\u6863\u6709\u610F\u4E49\uFF1B\u5B8C\u5168\u8BBF\u95EE\u6052\u8054\u7F51\uFF09"),
3767
+ selectMenu({
3768
+ name: "network",
3769
+ placeholder: "\u8054\u7F51\u5F00\u5173",
3770
+ options: [
3771
+ { label: "\u5173\uFF08\u9ED8\u8BA4\uFF0C\u66F4\u5B89\u5168\uFF09", value: "off" },
3772
+ { label: "\u5F00", value: "on" }
3773
+ ],
3774
+ initial: network ? "on" : "off"
3775
+ }),
3776
+ actions([submitButton("\u2705 \u4FDD\u5B58\u6743\u9650", { a: DM.permissionSubmit, n: p.name }, "primary", "submit_perm")])
3777
+ ]),
3778
+ note("\u4FDD\u5B58\u4F1A\u65AD\u5F00\u672C\u9879\u76EE\u6B63\u5728\u8FDB\u884C\u7684\u4F1A\u8BDD\uFF0C\u8BA9\u65B0\u6863\u4F4D\u7ACB\u5373\u751F\u6548\u3002"),
3779
+ actions([button("\u2B05\uFE0F \u8FD4\u56DE\u8BBE\u7F6E", { a: DM.projectSettings, n: p.name })])
3780
+ ],
3781
+ { header: { title: "\u{1F510} \u6743\u9650", template: "blue" } }
3782
+ );
3783
+ }
3784
+ function buildProjectSettingsCard(project) {
3785
+ const kind = project.kind ?? "multi";
3786
+ const noMention = project.noMention ?? defaultNoMention(project);
3787
+ return card(
3788
+ [
3789
+ md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
3790
+ note(`${kindLabel(kind)}${project.cwd ? ` \xB7 \u{1F4C2} \`${project.cwd}\`` : ""}`),
3791
+ hr(),
3792
+ actions([button("\u{1F510} \u6743\u9650", { a: DM.permission, n: project.name }, "primary")]),
3793
+ note(`\u5F53\u524D ${permissionSummary(project)}\u3000\xB7\u3000codex \u6C99\u7BB1\u53EF\u8BBF\u95EE\u7684\u8303\u56F4\uFF08\u7BA1\u7406\u5458 / \u666E\u901A\u7528\u6237\u53EF\u5206\u8BBE\uFF09\u3002`),
3794
+ hr(),
3795
+ md("\u270B \u514D@\uFF08\u4E0D\u7528 @ \u4E5F\u56DE\u590D\uFF09"),
3796
+ actions([
3797
+ button("\u5F00", { a: DM.setNoMentionDm, v: "on", n: project.name }, noMention ? "primary" : "default"),
3798
+ button("\u5173", { a: DM.setNoMentionDm, v: "off", n: project.name }, noMention ? "default" : "primary")
3799
+ ]),
3800
+ note(
3801
+ 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"
3802
+ ),
3803
+ hr(),
3804
+ actions([button("\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", { a: DM.allowlist, n: project.name }, "primary")]),
3805
+ note("\u8BBE\u7F6E\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex\uFF08\u7A7A = \u6240\u6709\u4EBA\uFF09\u3002"),
3806
+ hr(),
3807
+ actions([button("\u2B05\uFE0F \u9879\u76EE\u5217\u8868", { a: DM.projects })])
3808
+ ],
3809
+ { header: { title: "\u2699\uFE0F \u9879\u76EE\u8BBE\u7F6E", template: "blue" } }
3810
+ );
3811
+ }
3812
+ function buildAllowlistCard(project, names) {
3813
+ const list = project.allowedUsers ?? [];
3814
+ const elements = [md(`**\u54CD\u5E94\u767D\u540D\u5355** \xB7 ${project.name}`), note("\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex"), hr()];
3815
+ if (list.length === 0) {
3816
+ elements.push(note("\u5F53\u524D**\u6240\u6709\u4EBA**\u53EF\u7528\uFF08\u7BA1\u7406\u5458\u59CB\u7EC8\u53EF\u7528\uFF09\u3002"));
3817
+ } else {
3818
+ for (const id of list) {
3819
+ elements.push(
3820
+ actions([md(memberName(names, id)), button("\u{1F5D1} \u79FB\u9664", { a: DM.rmAllowed, u: id, n: project.name }, "danger")])
3821
+ );
3822
+ }
3823
+ }
3824
+ elements.push(
3825
+ hr(),
3826
+ actions([
3827
+ button("\u2795 \u6DFB\u52A0", { a: DM.addAllowedForm, n: project.name }, "primary"),
3828
+ button("\u2B05\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: project.name })
3829
+ ]),
3830
+ 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")
3831
+ );
3832
+ return card(elements, { header: { title: "\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", template: "blue" } });
3833
+ }
3834
+ function buildAddAllowedCard(projectName, members) {
3835
+ const MAX = 50;
3836
+ const shown = members.slice(0, MAX);
3837
+ const formEls = [];
3838
+ if (shown.length > 0) {
3839
+ formEls.push(
3840
+ selectMenu({
3841
+ name: "pick",
3842
+ placeholder: "\u4ECE\u7FA4\u6210\u5458\u9009\u62E9",
3843
+ options: shown.map((m) => ({ label: m.name, value: m.openId }))
3844
+ })
3845
+ );
3846
+ }
3847
+ formEls.push(
3848
+ input({
3849
+ name: "open_id",
3850
+ label: shown.length ? "\u6216\u76F4\u63A5\u8F93\u5165 open_id" : "\u8F93\u5165 open_id\uFF08\u672A\u8BFB\u53D6\u5230\u7FA4\u6210\u5458\uFF09",
3851
+ placeholder: "ou_xxx"
3852
+ }),
3853
+ actions([submitButton("\u2705 \u786E\u8BA4\u6DFB\u52A0", { a: DM.addAllowedSubmit, n: projectName }, "primary", "submit_allowed")])
3854
+ );
3855
+ const tail = [];
3856
+ 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`));
3857
+ return card(
3858
+ [
3859
+ md(`**\u6DFB\u52A0\u53EF\u4F7F\u7528\u300C${projectName}\u300D\u7684\u4EBA**`),
3860
+ form("add_allowed", formEls),
3861
+ ...tail,
3862
+ actions([button("\u2B05\uFE0F \u53D6\u6D88", { a: DM.allowlist, n: projectName })])
3863
+ ],
3864
+ { header: { title: "\u2795 \u6DFB\u52A0\u767D\u540D\u5355\u6210\u5458", template: "blue" } }
3865
+ );
3866
+ }
3575
3867
 
3576
3868
  // src/service/update.ts
3577
3869
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
@@ -4350,9 +4642,19 @@ async function createProject(channel, input2) {
4350
4642
  params: { member_id_type: "open_id" },
4351
4643
  data: { manager_ids: [input2.ownerOpenId] }
4352
4644
  }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
4353
- const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi", origin: "created" };
4645
+ const project = {
4646
+ name,
4647
+ chatId,
4648
+ cwd,
4649
+ blank,
4650
+ createdAt: Date.now(),
4651
+ kind: input2.kind ?? "multi",
4652
+ origin: "created",
4653
+ mode: input2.mode ?? "full",
4654
+ network: input2.network ?? false
4655
+ };
4354
4656
  await addProject(project);
4355
- log.info("project", "create", { name, chatId, cwd, blank });
4657
+ log.info("project", "create", { name, chatId, cwd, blank, mode: project.mode });
4356
4658
  await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
4357
4659
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
4358
4660
  return project;
@@ -4372,10 +4674,12 @@ async function joinExistingGroup(channel, input2) {
4372
4674
  createdAt: Date.now(),
4373
4675
  kind: input2.kind ?? "multi",
4374
4676
  origin: "joined",
4375
- addedBy: input2.addedBy
4677
+ addedBy: input2.addedBy,
4678
+ mode: input2.mode ?? "qa",
4679
+ network: input2.network ?? false
4376
4680
  };
4377
4681
  await addProject(project);
4378
- log.info("project", "join", { name, chatId: input2.chatId, cwd, blank, kind: project.kind });
4682
+ log.info("project", "join", { name, chatId: input2.chatId, cwd, blank, kind: project.kind, mode: project.mode });
4379
4683
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard-join" }));
4380
4684
  return project;
4381
4685
  }
@@ -4845,6 +5149,74 @@ var Semaphore = class {
4845
5149
  };
4846
5150
 
4847
5151
  // src/bot/handle-message.ts
5152
+ async function resolveNames(channel, ids) {
5153
+ const uniq = [...new Set(ids.filter((x) => Boolean(x)))];
5154
+ const out = /* @__PURE__ */ new Map();
5155
+ if (uniq.length === 0) return out;
5156
+ try {
5157
+ const r = await channel.rawClient.contact.v3.user.batch({
5158
+ params: { user_ids: uniq, user_id_type: "open_id" }
5159
+ });
5160
+ for (const it of r.data?.items ?? []) {
5161
+ if (it.open_id && it.name) out.set(it.open_id, it.name);
5162
+ }
5163
+ } catch (err) {
5164
+ log.info("console", "resolve-names-fail", { n: uniq.length, err: String(err) });
5165
+ }
5166
+ return out;
5167
+ }
5168
+ async function fetchChatMembers(channel, chatId) {
5169
+ try {
5170
+ const r = await channel.rawClient.im.v1.chatMembers.get({
5171
+ path: { chat_id: chatId },
5172
+ params: { member_id_type: "open_id", page_size: 100 }
5173
+ });
5174
+ const out = [];
5175
+ for (const it of r.data?.items ?? []) {
5176
+ if (it.member_id) out.push({ openId: it.member_id, name: it.name || `\u2026${it.member_id.slice(-6)}` });
5177
+ }
5178
+ return out;
5179
+ } catch (err) {
5180
+ log.info("console", "fetch-members-fail", { chatId: chatId.slice(-6), err: String(err) });
5181
+ return [];
5182
+ }
5183
+ }
5184
+ async function fetchAllProjectMembers(channel) {
5185
+ const projects = await listProjects();
5186
+ const lists = await Promise.all(projects.filter((p) => p.chatId).map((p) => fetchChatMembers(channel, p.chatId)));
5187
+ const seen = /* @__PURE__ */ new Map();
5188
+ for (const members of lists) {
5189
+ for (const m of members) if (!seen.has(m.openId)) seen.set(m.openId, m.name);
5190
+ }
5191
+ return [...seen].map(([openId, name]) => ({ openId, name }));
5192
+ }
5193
+ function pickOpenId(formValue) {
5194
+ const raw = formValue?.pick;
5195
+ const cands = Array.isArray(raw) ? raw : [raw];
5196
+ for (const c of cands) {
5197
+ if (typeof c === "string" && c.startsWith("ou_")) return c;
5198
+ if (c && typeof c === "object") {
5199
+ const o = c;
5200
+ for (const v of [o.open_id, o.id, o.value]) if (typeof v === "string" && v.startsWith("ou_")) return v;
5201
+ }
5202
+ }
5203
+ return void 0;
5204
+ }
5205
+ function selectValue(formValue, name) {
5206
+ const c = (() => {
5207
+ const raw = formValue?.[name];
5208
+ return Array.isArray(raw) ? raw[0] : raw;
5209
+ })();
5210
+ if (typeof c === "string") return c;
5211
+ if (c && typeof c === "object") {
5212
+ const o = c;
5213
+ for (const v of [o.value, o.id]) if (typeof v === "string") return v;
5214
+ }
5215
+ return void 0;
5216
+ }
5217
+ function asTier(v) {
5218
+ return v === "qa" || v === "write" || v === "full" ? v : void 0;
5219
+ }
4848
5220
  function createOrchestrator(channel, cfg, fallbackCwd) {
4849
5221
  const backend = createBackend();
4850
5222
  const sessions = /* @__PURE__ */ new Map();
@@ -4922,7 +5294,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4922
5294
  }
4923
5295
  const project = await getProjectByChatId(msg.chatId);
4924
5296
  if (!msg.mentionedBot && !(project && shouldRespondWithoutMention(project, msg))) return;
4925
- if (!isChatAllowed(cfg, msg.chatId) || !isUserAllowed(cfg, msg.senderId)) {
5297
+ if (!isChatAllowed(cfg, msg.chatId) || !isUserAllowedInProject(cfg, project, msg.senderId)) {
4926
5298
  log.info("intake", "reject", { reason: "not_allowed", chatId: msg.chatId.slice(-6) });
4927
5299
  return;
4928
5300
  }
@@ -4948,11 +5320,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4948
5320
  await postGroupSettings(msg, project);
4949
5321
  return;
4950
5322
  }
5323
+ const ts = turnSession(msg.chatId, project, msg.senderId);
4951
5324
  if (cmd === "model") {
4952
- await postModelCard(msg, msg.chatId);
5325
+ await postModelCard(msg, ts.sessionKey);
4953
5326
  return;
4954
5327
  }
4955
- handleTurn(msg, text, msg.chatId, true, project);
5328
+ handleTurn(msg, text, ts.sessionKey, true, project, ts);
4956
5329
  return;
4957
5330
  }
4958
5331
  if (msg.threadId) {
@@ -4960,11 +5333,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4960
5333
  await postHelpCard(msg, "topic", true, project);
4961
5334
  return;
4962
5335
  }
5336
+ const ts = turnSession(msg.threadId, project, msg.senderId);
4963
5337
  if (cmd === "model") {
4964
- await postModelCard(msg, msg.threadId);
5338
+ await postModelCard(msg, ts.sessionKey);
4965
5339
  return;
4966
5340
  }
4967
- handleTurn(msg, text, msg.threadId, false, project);
5341
+ handleTurn(msg, text, ts.sessionKey, false, project, ts);
4968
5342
  return;
4969
5343
  }
4970
5344
  if (cmd === "help") {
@@ -4996,9 +5370,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4996
5370
  if ((project.kind ?? "multi") === "single") return true;
4997
5371
  return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
4998
5372
  }
5373
+ async function denyAdminCommand(msg, cmd) {
5374
+ await channel.send(msg.chatId, { markdown: `\u26A0\uFE0F \`/${cmd}\` \u4EC5 bot \u7BA1\u7406\u5458\u53EF\u7528\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
5375
+ log.info("intake", "cmd-denied", { cmd });
5376
+ }
4999
5377
  async function postGroupSettings(msg, project) {
5000
5378
  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);
5379
+ await denyAdminCommand(msg, "settings");
5002
5380
  return;
5003
5381
  }
5004
5382
  if (!project) {
@@ -5010,13 +5388,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5010
5388
  log.info("card", "group-settings", { project: project.name });
5011
5389
  });
5012
5390
  }
5013
- async function handleTurn(msg, text, sessionKey, flat, project) {
5391
+ function turnPerm(project, senderId) {
5392
+ if (!project) return {};
5393
+ const t = turnTier(project, isAdmin(cfg, senderId));
5394
+ return { mode: t.mode, network: project.network, roleSuffix: t.split ? t.role : void 0 };
5395
+ }
5396
+ function turnSession(baseKey, project, senderId) {
5397
+ const perm = turnPerm(project, senderId);
5398
+ return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
5399
+ }
5400
+ async function handleTurn(msg, text, sessionKey, flat, project, perm) {
5014
5401
  const existing = active.get(sessionKey);
5015
5402
  if (existing) {
5016
5403
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
5017
5404
  const cur = active.get(sessionKey);
5018
5405
  if (!cur) {
5019
- startReservedRun(msg, text, sessionKey, flat, project, images);
5406
+ startReservedRun(msg, text, sessionKey, flat, project, perm, images);
5020
5407
  return;
5021
5408
  }
5022
5409
  if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
@@ -5035,9 +5422,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5035
5422
  log.info("intake", "queued", { depth: cur.queue.length });
5036
5423
  return;
5037
5424
  }
5038
- startReservedRun(msg, text, sessionKey, flat, project);
5425
+ startReservedRun(msg, text, sessionKey, flat, project, perm);
5039
5426
  }
5040
- function startReservedRun(msg, text, sessionKey, flat, project, preloadedImages) {
5427
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages) {
5041
5428
  const existing = active.get(sessionKey);
5042
5429
  if (existing) {
5043
5430
  existing.queue.push({ text, images: preloadedImages });
@@ -5050,10 +5437,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5050
5437
  const reaction = runReaction(msg.messageId, !sema.hasFree());
5051
5438
  try {
5052
5439
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
5053
- let thread = await resolveThread(sessionKey, msg.chatId);
5440
+ let thread = await resolveThread(sessionKey, msg.chatId, { mode: perm.mode, network: perm.network });
5054
5441
  if (!thread) {
5055
5442
  const cwd = project?.cwd ?? fallbackCwd;
5056
- thread = await backend.startThread({ cwd });
5443
+ thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
5057
5444
  sessions.set(sessionKey, thread);
5058
5445
  await upsertSession({
5059
5446
  threadId: sessionKey,
@@ -5088,7 +5475,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5088
5475
  }
5089
5476
  });
5090
5477
  }
5091
- async function resolveThread(threadId, chatId) {
5478
+ async function resolveThread(threadId, chatId, perm) {
5092
5479
  const live = sessions.get(threadId);
5093
5480
  if (live) return live;
5094
5481
  const rec = await getSession(threadId);
@@ -5098,7 +5485,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5098
5485
  cwd: rec.cwd,
5099
5486
  codexThreadId: rec.codexThreadId,
5100
5487
  model: rec.model,
5101
- effort: rec.effort
5488
+ effort: rec.effort,
5489
+ mode: perm?.mode,
5490
+ network: perm?.network
5102
5491
  });
5103
5492
  sessions.set(threadId, resumed);
5104
5493
  return resumed;
@@ -5106,20 +5495,39 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5106
5495
  log.fail("agent", err, { phase: "resume-on-turn", threadId });
5107
5496
  const project = await getProjectByChatId(chatId);
5108
5497
  const cwd = project?.cwd ?? rec.cwd ?? fallbackCwd;
5109
- const fresh = await backend.startThread({ cwd, model: rec.model, effort: rec.effort });
5498
+ const fresh = await backend.startThread({
5499
+ cwd,
5500
+ model: rec.model,
5501
+ effort: rec.effort,
5502
+ mode: perm?.mode ?? project?.mode,
5503
+ network: perm?.network ?? project?.network
5504
+ });
5110
5505
  sessions.set(threadId, fresh);
5111
5506
  return fresh;
5112
5507
  }
5113
5508
  }
5509
+ async function evictLiveSessionsForChat(chatId) {
5510
+ let closed = 0;
5511
+ for (const rec of await listSessions()) {
5512
+ if (rec.chatId !== chatId) continue;
5513
+ const live = sessions.get(rec.threadId);
5514
+ if (!live) continue;
5515
+ sessions.delete(rec.threadId);
5516
+ void live.close().catch(() => void 0);
5517
+ closed++;
5518
+ }
5519
+ if (closed) log.info("console", "tier-evict", { chatId, closed });
5520
+ }
5114
5521
  function startTopicDirectly(msg, text, project) {
5115
5522
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5116
5523
  const reaction = runReaction(msg.messageId, !sema.hasFree());
5117
5524
  const cwd = project?.cwd ?? fallbackCwd;
5525
+ const perm = turnPerm(project, msg.senderId);
5118
5526
  if (project) void refreshBranch(channel, project).catch(() => void 0);
5119
5527
  const { model, effort } = pickDefault(await listModels());
5120
5528
  let thread;
5121
5529
  try {
5122
- thread = await backend.startThread({ cwd, model, effort });
5530
+ thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network });
5123
5531
  } catch (err) {
5124
5532
  reaction.done();
5125
5533
  log.fail("card", err, { phase: "start-topic" });
@@ -5141,7 +5549,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5141
5549
  effort,
5142
5550
  cwd,
5143
5551
  summary: text.slice(0, 80) || "(\u7A7A)",
5144
- requesterOpenId: msg.senderId
5552
+ requesterOpenId: msg.senderId,
5553
+ roleSuffix: perm.roleSuffix
5145
5554
  },
5146
5555
  reaction,
5147
5556
  () => reaction.done()
@@ -5150,6 +5559,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5150
5559
  }).catch((err) => log.fail("intake", err));
5151
5560
  }
5152
5561
  async function postResumeCard(msg) {
5562
+ if (!isAdmin(cfg, msg.senderId)) {
5563
+ await denyAdminCommand(msg, "resume");
5564
+ return;
5565
+ }
5153
5566
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5154
5567
  const project = await getProjectByChatId(msg.chatId);
5155
5568
  const cwd = project?.cwd ?? fallbackCwd;
@@ -5191,7 +5604,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5191
5604
  async function postHelpCard(msg, scope, inThread = false, project) {
5192
5605
  const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
5193
5606
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5194
- await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention), msg.messageId, inThread).catch(
5607
+ await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention, isAdmin(cfg, msg.senderId)), msg.messageId, inThread).catch(
5195
5608
  (err) => log.fail("card", err, { cmd: "help", scope })
5196
5609
  );
5197
5610
  log.info("card", "help", { scope });
@@ -5230,7 +5643,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5230
5643
  return void 0;
5231
5644
  }
5232
5645
  const op = evt.operator?.openId ?? "";
5233
- if (op !== state.requesterOpenId || !isChatAllowed(cfg, state.chatId) || !isUserAllowed(cfg, op)) {
5646
+ if (op !== state.requesterOpenId || !isChatAllowed(cfg, state.chatId)) {
5234
5647
  log.info("card", "action-denied", { reason: "not-allowed" });
5235
5648
  return void 0;
5236
5649
  }
@@ -5266,7 +5679,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5266
5679
  settleUpdate(evt.messageId, buildResumeLaunchingCard(state));
5267
5680
  void resumeFromCard(evt, state, codexThreadId);
5268
5681
  });
5269
- const runAllowed = (evt) => isChatAllowed(cfg, evt.chatId) && isUserAllowed(cfg, evt.operator?.openId ?? "");
5682
+ const runAllowed = (evt) => isChatAllowed(cfg, evt.chatId);
5270
5683
  const runOwnerOrAdmin = (evt, ownerOpenId) => {
5271
5684
  if (!runAllowed(evt)) return false;
5272
5685
  const op = evt.operator?.openId ?? "";
@@ -5281,6 +5694,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5281
5694
  });
5282
5695
  const dmAdmin = (openId) => isAdmin(cfg, openId ?? "");
5283
5696
  const patch = (evt, c) => settleUpdate(evt.messageId, c, evt.chatId);
5697
+ const namesWithOperator = async (evt, ids) => {
5698
+ const m = await resolveNames(channel, ids);
5699
+ if (ids.some((id) => id && !m.has(id))) {
5700
+ for (const mem of await fetchAllProjectMembers(channel)) if (!m.has(mem.openId)) m.set(mem.openId, mem.name);
5701
+ }
5702
+ const op = evt.operator;
5703
+ if (op?.openId && op.name && !m.has(op.openId)) m.set(op.openId, op.name);
5704
+ return m;
5705
+ };
5284
5706
  function applyPref(evt, mut) {
5285
5707
  if (!dmAdmin(evt.operator?.openId)) return;
5286
5708
  const prefs = { ...cfg.preferences ?? {} };
@@ -5504,6 +5926,132 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5504
5926
  }
5505
5927
  return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", noMention: on });
5506
5928
  });
5929
+ }).on(DM.admins, ({ evt }) => {
5930
+ if (!dmAdmin(evt.operator?.openId)) return;
5931
+ patch(
5932
+ evt,
5933
+ async () => buildAdminsCard(cfg, await namesWithOperator(evt, [resolveOwner(cfg), ...cfg.preferences?.access?.admins ?? []]))
5934
+ );
5935
+ }).on(DM.addAdminForm, ({ evt }) => {
5936
+ if (!dmAdmin(evt.operator?.openId)) return;
5937
+ patch(evt, async () => {
5938
+ const all = await fetchAllProjectMembers(channel);
5939
+ const members = all.filter((m) => !isAdmin(cfg, m.openId));
5940
+ return buildAddAdminCard(members);
5941
+ });
5942
+ }).on(DM.addAdminSubmit, ({ evt, formValue }) => {
5943
+ if (!dmAdmin(evt.operator?.openId)) return;
5944
+ const manual = String(formValue?.open_id ?? "").trim();
5945
+ const id = manual.startsWith("ou_") ? manual : pickOpenId(formValue);
5946
+ log.info("console", "admin-add", { picked: id?.slice(-6) ?? null });
5947
+ void (async () => {
5948
+ if (id) {
5949
+ const access = { ...cfg.preferences?.access ?? {} };
5950
+ access.ownerOpenId ??= resolveOwner(cfg);
5951
+ access.admins = Array.from(/* @__PURE__ */ new Set([...access.admins ?? [], id]));
5952
+ cfg.preferences = { ...cfg.preferences ?? {}, access };
5953
+ await saveConfig(cfg).catch((e) => log.fail("console", e, { phase: "save-config" }));
5954
+ }
5955
+ const ids = [resolveOwner(cfg), ...cfg.preferences?.access?.admins ?? []];
5956
+ const next = buildAdminsCard(cfg, await namesWithOperator(evt, ids));
5957
+ await sendManagedCard(channel, evt.chatId, next).catch((e) => log.fail("console", e, { phase: "admin-add-result" }));
5958
+ })();
5959
+ }).on(DM.rmAdmin, ({ evt, value }) => {
5960
+ if (!dmAdmin(evt.operator?.openId)) return;
5961
+ const id = typeof value.u === "string" ? value.u : "";
5962
+ patch(evt, async () => {
5963
+ if (id && id !== resolveOwner(cfg)) {
5964
+ const access = { ...cfg.preferences?.access ?? {} };
5965
+ access.ownerOpenId ??= resolveOwner(cfg);
5966
+ access.admins = (access.admins ?? []).filter((x) => x !== id);
5967
+ cfg.preferences = { ...cfg.preferences ?? {}, access };
5968
+ await saveConfig(cfg).catch((e) => log.fail("console", e, { phase: "save-config" }));
5969
+ }
5970
+ const ids = [resolveOwner(cfg), ...cfg.preferences?.access?.admins ?? []];
5971
+ return buildAdminsCard(cfg, await namesWithOperator(evt, ids));
5972
+ });
5973
+ }).on(DM.allowlist, ({ evt, value }) => {
5974
+ if (!dmAdmin(evt.operator?.openId)) return;
5975
+ const name = typeof value.n === "string" ? value.n : "";
5976
+ patch(evt, async () => {
5977
+ const p = await getProjectByName(name);
5978
+ if (!p) return buildDmMenuCard();
5979
+ return buildAllowlistCard(p, await namesWithOperator(evt, p.allowedUsers ?? []));
5980
+ });
5981
+ }).on(DM.addAllowedForm, ({ evt, value }) => {
5982
+ if (!dmAdmin(evt.operator?.openId)) return;
5983
+ const name = typeof value.n === "string" ? value.n : "";
5984
+ if (!name) return;
5985
+ patch(evt, async () => {
5986
+ const p = await getProjectByName(name);
5987
+ const members = p?.chatId ? await fetchChatMembers(channel, p.chatId) : [];
5988
+ return buildAddAllowedCard(name, members);
5989
+ });
5990
+ }).on(DM.addAllowedSubmit, ({ evt, value, formValue }) => {
5991
+ if (!dmAdmin(evt.operator?.openId)) return;
5992
+ const name = typeof value.n === "string" ? value.n : "";
5993
+ const manual = String(formValue?.open_id ?? "").trim();
5994
+ const id = manual.startsWith("ou_") ? manual : pickOpenId(formValue);
5995
+ log.info("console", "allow-add", { project: name, picked: id?.slice(-6) ?? null });
5996
+ void (async () => {
5997
+ if (id) await updateProject(name, (p) => ({ allowedUsers: Array.from(/* @__PURE__ */ new Set([...p.allowedUsers ?? [], id])) }));
5998
+ const fresh = await getProjectByName(name);
5999
+ if (!fresh) return;
6000
+ const card2 = buildAllowlistCard(fresh, await namesWithOperator(evt, fresh.allowedUsers ?? []));
6001
+ await sendManagedCard(channel, evt.chatId, card2).catch((e) => log.fail("console", e, { phase: "allow-add-result" }));
6002
+ })();
6003
+ }).on(DM.rmAllowed, ({ evt, value }) => {
6004
+ if (!dmAdmin(evt.operator?.openId)) return;
6005
+ const id = typeof value.u === "string" ? value.u : "";
6006
+ const name = typeof value.n === "string" ? value.n : "";
6007
+ patch(evt, async () => {
6008
+ await updateProject(name, (p) => ({ allowedUsers: (p.allowedUsers ?? []).filter((x) => x !== id) }));
6009
+ const fresh = await getProjectByName(name);
6010
+ if (!fresh) return buildDmMenuCard();
6011
+ return buildAllowlistCard(fresh, await namesWithOperator(evt, fresh.allowedUsers ?? []));
6012
+ });
6013
+ }).on(DM.projectSettings, ({ evt, value }) => {
6014
+ if (!dmAdmin(evt.operator?.openId)) return;
6015
+ const name = typeof value.n === "string" ? value.n : "";
6016
+ patch(evt, async () => {
6017
+ const p = await getProjectByName(name);
6018
+ return p ? buildProjectSettingsCard(p) : buildDmMenuCard();
6019
+ });
6020
+ }).on(DM.setNoMentionDm, ({ evt, value }) => {
6021
+ if (!dmAdmin(evt.operator?.openId)) return;
6022
+ const name = typeof value.n === "string" ? value.n : "";
6023
+ const on = value.v === "on";
6024
+ patch(evt, async () => {
6025
+ const p = await getProjectByName(name);
6026
+ if (!p) return buildDmMenuCard();
6027
+ await updateProject(name, { noMention: on });
6028
+ return buildProjectSettingsCard({ ...p, noMention: on });
6029
+ });
6030
+ }).on(DM.permission, ({ evt, value }) => {
6031
+ if (!dmAdmin(evt.operator?.openId)) return;
6032
+ const name = typeof value.n === "string" ? value.n : "";
6033
+ patch(evt, async () => {
6034
+ const p = await getProjectByName(name);
6035
+ return p ? buildPermissionCard(p) : buildDmMenuCard();
6036
+ });
6037
+ }).on(DM.permissionSubmit, ({ evt, value, formValue }) => {
6038
+ if (!dmAdmin(evt.operator?.openId)) return;
6039
+ const name = typeof value.n === "string" ? value.n : "";
6040
+ const mode = asTier(selectValue(formValue, "mode"));
6041
+ const guestMode = asTier(selectValue(formValue, "guestMode"));
6042
+ const network = selectValue(formValue, "network") === "on";
6043
+ void (async () => {
6044
+ const p = await getProjectByName(name);
6045
+ if (!p) return;
6046
+ await updateProject(name, { ...mode ? { mode } : {}, ...guestMode ? { guestMode } : {}, network });
6047
+ await evictLiveSessionsForChat(p.chatId);
6048
+ log.info("console", "permission", { project: name, mode, guestMode, network });
6049
+ const fresh = await getProjectByName(name);
6050
+ if (!fresh) return;
6051
+ await sendManagedCard(channel, evt.chatId, buildProjectSettingsCard(fresh)).catch(
6052
+ (e) => log.fail("console", e, { phase: "permission-result" })
6053
+ );
6054
+ })();
5507
6055
  });
5508
6056
  async function resumeFromCard(evt, state, codexThreadId) {
5509
6057
  try {
@@ -5605,13 +6153,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5605
6153
  if (activeKey.startsWith("pending:")) {
5606
6154
  const tid = await getThreadId(channel, messageId);
5607
6155
  if (tid) {
6156
+ const key = opts.roleSuffix ? `${tid}#${opts.roleSuffix}` : tid;
5608
6157
  active.delete(activeKey);
5609
- active.set(tid, state);
5610
- sessions.set(tid, opts.thread);
5611
- activeKey = tid;
5612
- topicThreadId = tid;
5613
- rc.threadId = tid;
5614
- await persist(tid);
6158
+ active.set(key, state);
6159
+ sessions.set(key, opts.thread);
6160
+ activeKey = key;
6161
+ topicThreadId = key;
6162
+ rc.threadId = key;
6163
+ await persist(key);
5615
6164
  }
5616
6165
  } else {
5617
6166
  topicThreadId = activeKey;
@@ -5753,8 +6302,6 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5753
6302
  if (!evt.mentionedBot) return log.info("comment", "skip", { reason: "not-mentioned" });
5754
6303
  if (!SUPPORTED_FILE_TYPES.has(evt.fileType))
5755
6304
  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
6305
  const resolved = await resolveComment(channel, evt);
5759
6306
  if (!resolved) return log.info("comment", "skip", { reason: "no-target-or-empty" });
5760
6307
  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.3",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {