@modelzen/feishu-codex-bridge 0.3.2 → 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 +210 -29
  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
@@ -1442,7 +1442,28 @@ function mapItemComplete(item) {
1442
1442
 
1443
1443
  // src/agent/codex-appserver/backend.ts
1444
1444
  var APPROVAL_POLICY = "never";
1445
- 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
+ }
1446
1467
  var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1447
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",
1448
1469
  "",
@@ -1627,23 +1648,25 @@ var CodexAppServerBackend = class {
1627
1648
  }
1628
1649
  }
1629
1650
  async startThread(opts) {
1651
+ const sandbox = sandboxParams(opts.mode, opts.network);
1630
1652
  const client = await this.spawn(opts.cwd);
1631
1653
  const res = await client.request("thread/start", {
1632
1654
  cwd: opts.cwd,
1633
1655
  approvalPolicy: APPROVAL_POLICY,
1634
- sandbox: SANDBOX,
1656
+ ...sandbox,
1635
1657
  developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1636
1658
  ...opts.model ? { model: opts.model } : {}
1637
1659
  });
1638
1660
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1639
1661
  }
1640
1662
  async resumeThread(opts) {
1663
+ const sandbox = sandboxParams(opts.mode, opts.network);
1641
1664
  const client = await this.spawn(opts.cwd);
1642
1665
  const res = await client.request("thread/resume", {
1643
1666
  threadId: opts.codexThreadId,
1644
1667
  cwd: opts.cwd,
1645
1668
  approvalPolicy: APPROVAL_POLICY,
1646
- sandbox: SANDBOX,
1669
+ ...sandbox,
1647
1670
  developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1648
1671
  ...opts.model ? { model: opts.model } : {}
1649
1672
  });
@@ -2194,6 +2217,7 @@ function selectMenu(opts) {
2194
2217
  tag: "select_static",
2195
2218
  name: opts.name,
2196
2219
  placeholder: { tag: "plain_text", content: opts.placeholder },
2220
+ ...opts.initial ? { initial_option: opts.initial } : {},
2197
2221
  options: opts.options.map((o) => ({ text: { tag: "plain_text", content: o.label }, value: o.value }))
2198
2222
  };
2199
2223
  }
@@ -3138,6 +3162,21 @@ import { dirname as dirname5 } from "path";
3138
3162
  function defaultNoMention(p) {
3139
3163
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
3140
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
+ }
3141
3180
  var FILE_VERSION2 = 1;
3142
3181
  async function read() {
3143
3182
  try {
@@ -3247,7 +3286,10 @@ var DM = {
3247
3286
  rmAllowed: "dm.allow.rm",
3248
3287
  // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3249
3288
  projectSettings: "dm.projectSettings",
3250
- setNoMentionDm: "dm.proj.noMention"
3289
+ setNoMentionDm: "dm.proj.noMention",
3290
+ // 🔐 权限:codex 沙箱档位(管理员档 + 普通用户档)+ 联网,做成下拉表单(选+提交)
3291
+ permission: "dm.proj.perm",
3292
+ permissionSubmit: "dm.proj.perm.submit"
3251
3293
  };
3252
3294
  var GS = {
3253
3295
  setNoMention: "gs.noMention"
@@ -3689,6 +3731,56 @@ function buildAddAdminCard(members) {
3689
3731
  { header: { title: "\u2795 \u6DFB\u52A0\u7BA1\u7406\u5458", template: "blue" } }
3690
3732
  );
3691
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
+ }
3692
3784
  function buildProjectSettingsCard(project) {
3693
3785
  const kind = project.kind ?? "multi";
3694
3786
  const noMention = project.noMention ?? defaultNoMention(project);
@@ -3697,6 +3789,9 @@ function buildProjectSettingsCard(project) {
3697
3789
  md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
3698
3790
  note(`${kindLabel(kind)}${project.cwd ? ` \xB7 \u{1F4C2} \`${project.cwd}\`` : ""}`),
3699
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(),
3700
3795
  md("\u270B \u514D@\uFF08\u4E0D\u7528 @ \u4E5F\u56DE\u590D\uFF09"),
3701
3796
  actions([
3702
3797
  button("\u5F00", { a: DM.setNoMentionDm, v: "on", n: project.name }, noMention ? "primary" : "default"),
@@ -4547,9 +4642,19 @@ async function createProject(channel, input2) {
4547
4642
  params: { member_id_type: "open_id" },
4548
4643
  data: { manager_ids: [input2.ownerOpenId] }
4549
4644
  }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
4550
- 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
+ };
4551
4656
  await addProject(project);
4552
- log.info("project", "create", { name, chatId, cwd, blank });
4657
+ log.info("project", "create", { name, chatId, cwd, blank, mode: project.mode });
4553
4658
  await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
4554
4659
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
4555
4660
  return project;
@@ -4569,10 +4674,12 @@ async function joinExistingGroup(channel, input2) {
4569
4674
  createdAt: Date.now(),
4570
4675
  kind: input2.kind ?? "multi",
4571
4676
  origin: "joined",
4572
- addedBy: input2.addedBy
4677
+ addedBy: input2.addedBy,
4678
+ mode: input2.mode ?? "qa",
4679
+ network: input2.network ?? false
4573
4680
  };
4574
4681
  await addProject(project);
4575
- 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 });
4576
4683
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard-join" }));
4577
4684
  return project;
4578
4685
  }
@@ -5095,6 +5202,21 @@ function pickOpenId(formValue) {
5095
5202
  }
5096
5203
  return void 0;
5097
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
+ }
5098
5220
  function createOrchestrator(channel, cfg, fallbackCwd) {
5099
5221
  const backend = createBackend();
5100
5222
  const sessions = /* @__PURE__ */ new Map();
@@ -5198,11 +5320,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5198
5320
  await postGroupSettings(msg, project);
5199
5321
  return;
5200
5322
  }
5323
+ const ts = turnSession(msg.chatId, project, msg.senderId);
5201
5324
  if (cmd === "model") {
5202
- await postModelCard(msg, msg.chatId);
5325
+ await postModelCard(msg, ts.sessionKey);
5203
5326
  return;
5204
5327
  }
5205
- handleTurn(msg, text, msg.chatId, true, project);
5328
+ handleTurn(msg, text, ts.sessionKey, true, project, ts);
5206
5329
  return;
5207
5330
  }
5208
5331
  if (msg.threadId) {
@@ -5210,11 +5333,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5210
5333
  await postHelpCard(msg, "topic", true, project);
5211
5334
  return;
5212
5335
  }
5336
+ const ts = turnSession(msg.threadId, project, msg.senderId);
5213
5337
  if (cmd === "model") {
5214
- await postModelCard(msg, msg.threadId);
5338
+ await postModelCard(msg, ts.sessionKey);
5215
5339
  return;
5216
5340
  }
5217
- handleTurn(msg, text, msg.threadId, false, project);
5341
+ handleTurn(msg, text, ts.sessionKey, false, project, ts);
5218
5342
  return;
5219
5343
  }
5220
5344
  if (cmd === "help") {
@@ -5264,13 +5388,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5264
5388
  log.info("card", "group-settings", { project: project.name });
5265
5389
  });
5266
5390
  }
5267
- 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) {
5268
5401
  const existing = active.get(sessionKey);
5269
5402
  if (existing) {
5270
5403
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
5271
5404
  const cur = active.get(sessionKey);
5272
5405
  if (!cur) {
5273
- startReservedRun(msg, text, sessionKey, flat, project, images);
5406
+ startReservedRun(msg, text, sessionKey, flat, project, perm, images);
5274
5407
  return;
5275
5408
  }
5276
5409
  if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
@@ -5289,9 +5422,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5289
5422
  log.info("intake", "queued", { depth: cur.queue.length });
5290
5423
  return;
5291
5424
  }
5292
- startReservedRun(msg, text, sessionKey, flat, project);
5425
+ startReservedRun(msg, text, sessionKey, flat, project, perm);
5293
5426
  }
5294
- function startReservedRun(msg, text, sessionKey, flat, project, preloadedImages) {
5427
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages) {
5295
5428
  const existing = active.get(sessionKey);
5296
5429
  if (existing) {
5297
5430
  existing.queue.push({ text, images: preloadedImages });
@@ -5304,10 +5437,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5304
5437
  const reaction = runReaction(msg.messageId, !sema.hasFree());
5305
5438
  try {
5306
5439
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
5307
- let thread = await resolveThread(sessionKey, msg.chatId);
5440
+ let thread = await resolveThread(sessionKey, msg.chatId, { mode: perm.mode, network: perm.network });
5308
5441
  if (!thread) {
5309
5442
  const cwd = project?.cwd ?? fallbackCwd;
5310
- thread = await backend.startThread({ cwd });
5443
+ thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
5311
5444
  sessions.set(sessionKey, thread);
5312
5445
  await upsertSession({
5313
5446
  threadId: sessionKey,
@@ -5342,7 +5475,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5342
5475
  }
5343
5476
  });
5344
5477
  }
5345
- async function resolveThread(threadId, chatId) {
5478
+ async function resolveThread(threadId, chatId, perm) {
5346
5479
  const live = sessions.get(threadId);
5347
5480
  if (live) return live;
5348
5481
  const rec = await getSession(threadId);
@@ -5352,7 +5485,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5352
5485
  cwd: rec.cwd,
5353
5486
  codexThreadId: rec.codexThreadId,
5354
5487
  model: rec.model,
5355
- effort: rec.effort
5488
+ effort: rec.effort,
5489
+ mode: perm?.mode,
5490
+ network: perm?.network
5356
5491
  });
5357
5492
  sessions.set(threadId, resumed);
5358
5493
  return resumed;
@@ -5360,20 +5495,39 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5360
5495
  log.fail("agent", err, { phase: "resume-on-turn", threadId });
5361
5496
  const project = await getProjectByChatId(chatId);
5362
5497
  const cwd = project?.cwd ?? rec.cwd ?? fallbackCwd;
5363
- 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
+ });
5364
5505
  sessions.set(threadId, fresh);
5365
5506
  return fresh;
5366
5507
  }
5367
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
+ }
5368
5521
  function startTopicDirectly(msg, text, project) {
5369
5522
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5370
5523
  const reaction = runReaction(msg.messageId, !sema.hasFree());
5371
5524
  const cwd = project?.cwd ?? fallbackCwd;
5525
+ const perm = turnPerm(project, msg.senderId);
5372
5526
  if (project) void refreshBranch(channel, project).catch(() => void 0);
5373
5527
  const { model, effort } = pickDefault(await listModels());
5374
5528
  let thread;
5375
5529
  try {
5376
- thread = await backend.startThread({ cwd, model, effort });
5530
+ thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network });
5377
5531
  } catch (err) {
5378
5532
  reaction.done();
5379
5533
  log.fail("card", err, { phase: "start-topic" });
@@ -5395,7 +5549,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5395
5549
  effort,
5396
5550
  cwd,
5397
5551
  summary: text.slice(0, 80) || "(\u7A7A)",
5398
- requesterOpenId: msg.senderId
5552
+ requesterOpenId: msg.senderId,
5553
+ roleSuffix: perm.roleSuffix
5399
5554
  },
5400
5555
  reaction,
5401
5556
  () => reaction.done()
@@ -5872,6 +6027,31 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5872
6027
  await updateProject(name, { noMention: on });
5873
6028
  return buildProjectSettingsCard({ ...p, noMention: on });
5874
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
+ })();
5875
6055
  });
5876
6056
  async function resumeFromCard(evt, state, codexThreadId) {
5877
6057
  try {
@@ -5973,13 +6153,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5973
6153
  if (activeKey.startsWith("pending:")) {
5974
6154
  const tid = await getThreadId(channel, messageId);
5975
6155
  if (tid) {
6156
+ const key = opts.roleSuffix ? `${tid}#${opts.roleSuffix}` : tid;
5976
6157
  active.delete(activeKey);
5977
- active.set(tid, state);
5978
- sessions.set(tid, opts.thread);
5979
- activeKey = tid;
5980
- topicThreadId = tid;
5981
- rc.threadId = tid;
5982
- 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);
5983
6164
  }
5984
6165
  } else {
5985
6166
  topicThreadId = activeKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.2",
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": {