@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.
- package/README.md +16 -9
- package/dist/cli.js +210 -29
- 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
|
-
| **操作系统** |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = {
|
|
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,
|
|
5325
|
+
await postModelCard(msg, ts.sessionKey);
|
|
5203
5326
|
return;
|
|
5204
5327
|
}
|
|
5205
|
-
handleTurn(msg, text,
|
|
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,
|
|
5338
|
+
await postModelCard(msg, ts.sessionKey);
|
|
5215
5339
|
return;
|
|
5216
5340
|
}
|
|
5217
|
-
handleTurn(msg, text,
|
|
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
|
-
|
|
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({
|
|
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(
|
|
5978
|
-
sessions.set(
|
|
5979
|
-
activeKey =
|
|
5980
|
-
topicThreadId =
|
|
5981
|
-
rc.threadId =
|
|
5982
|
-
await persist(
|
|
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;
|