ai_co_discuss 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/discuss-host/references/manual.md +3 -3
- package/.agents/skills/discuss-participant/SKILL.md +1 -1
- package/.agents/skills/discuss-participant/references/writing.md +3 -1
- package/.github/SECURITY.md +4 -4
- package/README.md +5 -5
- package/backend/free_room.py +73 -20
- package/backend/runtime.py +56 -5
- package/backend/schema.sql +2 -1
- package/backend/server.py +35 -13
- package/docs/architecture.md +238 -233
- package/docs/cli.md +23 -15
- package/docs/deployment.md +2 -2
- package/docs/runner.md +17 -5
- package/docs/workflow.md +5 -5
- package/package.json +2 -2
- package/runner/discuss.py +75 -3
- package/runner/discussion_adapters.mjs +3 -1
- package/runner/discussion_launch.mjs +33 -9
- package/runner/discussion_models.mjs +127 -0
- package/runner/discussion_sidecar.mjs +7 -3
- package/runner/free_runner.mjs +10 -3
- package/web/app.js +2797 -2616
- package/web/index.html +126 -126
- package/web/modern.css +1 -1
- package/web/modern.js +1054 -962
package/docs/cli.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
`codiscuss launch room-models --file FILE` 输入 `site`、`room_id`,只读返回本机已登记房间中各参与者的 `adapter/model/effort`;未登记时返回空参与者对象。命令不启动 AI、不修改配置,不用本机默认模型填补缺失值。
|
|
14
14
|
|
|
15
|
+
`codiscuss launch models --file FILE` 输入 `adapter` 和可选的完整 `executable`,读取对应 CLI 的模型目录与支持的档位,不发送提示词。详情与失败边界见 [本机模型目录](runner.md#本机模型目录)。
|
|
16
|
+
|
|
15
17
|
## 连接与发现
|
|
16
18
|
|
|
17
19
|
```sh
|
|
@@ -31,25 +33,27 @@ Bash 对应 `export DISCUSS_URL=http://127.0.0.1:8765`,CMD 对应 `set DISCUSS
|
|
|
31
33
|
|
|
32
34
|
建房与更新资料支持 `--user-request` / `--user-request-file`,将用户原文与 `--background` 分开保存,各限 4000 字。`room brief` 返回两者,概要和通知不自动展开原文。
|
|
33
35
|
|
|
36
|
+
`room create` 必须指定 `--access-file 私有文件`,CLI 在请求前原子保存建房编号、房主和各成员凭据,确认房间后另写各本地成员的独立文件,回执只返回 `member_access_files` 路径。结果不明时沿用同一文件、编号和创建参数;不能换文件重建。后续每次写入使用对应身份的文件,不能拿房主文件冒充成员;自动讨论使用本次任务文件中的执行凭据。文件含 `site`、`room`、`token`,CLI 检查站点与房间匹配。`--dry-run` 不生成或写入凭据,预览会隐藏凭据字段。
|
|
37
|
+
|
|
34
38
|
消息 JSON 中的 `elapsed_seconds` 为只读耗时:自动执行从交付唤醒到保存该消息的秒数,不含排队,保存后固定。用户留言、手动或代发内容及没有计时记录的旧消息返回 `null`;发布命令无需提供耗时。
|
|
35
39
|
|
|
36
40
|
## 附件
|
|
37
41
|
|
|
38
42
|
```sh
|
|
39
|
-
codiscuss attachment upload ROOM --as 用户 --file ./资料.pdf --submission-id UPLOAD_UUID
|
|
43
|
+
codiscuss attachment upload ROOM --as 用户 --file ./资料.pdf --submission-id UPLOAD_UUID --access-file OWNER.json
|
|
40
44
|
codiscuss attachment list ROOM --submission-id UPLOAD_UUID
|
|
41
45
|
codiscuss attachment view ROOM ATTACHMENT_ID
|
|
42
46
|
codiscuss attachment download ROOM ATTACHMENT_ID --dest ./下载资料.pdf
|
|
43
|
-
codiscuss message post ROOM --as 用户 --body "请参考附件" --attachment ATTACHMENT_ID --submission-id MESSAGE_UUID
|
|
47
|
+
codiscuss message post ROOM --as 用户 --body "请参考附件" --attachment ATTACHMENT_ID --submission-id MESSAGE_UUID --access-file OWNER.json
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
上传回执中的 `attachment.id` 是后续引用的编号;上传和消息分别使用自己的提交编号。单个文件最多 20 MiB,消息最多重复指定 10 次 `--attachment`;仅附件消息可省略正文。普通 `attachment list ROOM` 只列已发送附件,可加 `--message ID`、`--page N`;精确提交查询用于核对未关联的上传。下载路径必须明确、父目录已存在且目标文件不存在;客户端验证长度和 SHA-256,不覆盖已有文件。
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
所有写入带 `--access-file`。自动任务先上传,再提交关联回答;回答完成后执行凭据不能继续上传。AI 发言仍需有效安排、发言权和摘要;下载后由参与软件自身工具读取,不默认把全部附件加入通知。`--dry-run` 预览路径与元数据,不上传字节或创建下载文件。结果不明先查原上传编号;重试沿用原文件、身份和编号。
|
|
49
53
|
|
|
50
54
|
## 讨论上限参数
|
|
51
55
|
|
|
52
|
-
`room create` / `room update` 支持 `--max-rounds`、`--max-minutes`、`--execution-minutes`(三者均以 0 表示不限;单次执行默认 0,可设 1–60 分钟)、`--on-limit pause|summarize`、`--summary-minutes`。未传字段沿用旧值;活动阶段须先暂停才能改上限。`room view` 包含 `limits` 与 `completed_rounds`,`room limits ROOM` 单独读取,`room limits ROOM --check --as
|
|
56
|
+
`room create` / `room update` 支持 `--max-rounds`、`--max-minutes`、`--execution-minutes`(三者均以 0 表示不限;单次执行默认 0,可设 1–60 分钟)、`--on-limit pause|summarize`、`--summary-minutes`。未传字段沿用旧值;活动阶段须先暂停才能改上限。`room view` 包含 `limits` 与 `completed_rounds`,`room limits ROOM` 单独读取,`room limits ROOM --check --as 身份 --access-file FILE` 检查并执行到限处理。检查不会启动 AI,也不代写总结。
|
|
53
57
|
|
|
54
58
|
`run start` 的 `--deadline` 可省略,此时采用房间总时长;房间总时长为 0 时不限总时长。显式传 0 也表示不另设运行期限,仍受话题上限约束;非零值与话题截止取更早值。不限总时长仍受轮次上限、主动设置的单次执行超时及手动停止约束。暂停或重新启动不会重置话题计时;继续前需调整已耗尽的上限。
|
|
55
59
|
|
|
@@ -63,38 +67,40 @@ codiscuss message post ROOM --as 用户 --body "请参考附件" --attachment AT
|
|
|
63
67
|
New-Item -ItemType Directory -Force data | Out-Null
|
|
64
68
|
[IO.File]::WriteAllText((Join-Path (Get-Location) 'data/experts.json'), '{"Codex":"cli","Gemini":"cli"}', [Text.UTF8Encoding]::new($false))
|
|
65
69
|
$room_submission = [guid]::NewGuid().ToString()
|
|
66
|
-
$created = codiscuss room create --as Codex --submission-id $room_submission --title '方案讨论' --goal '选择可验证的实现方案' --background '在此填写实际背景' --boundaries '在此填写约束、检查周期和有限截止条件' --experts-file data/experts.json --phase initial | ConvertFrom-Json
|
|
70
|
+
$created = codiscuss room create --as Codex --submission-id $room_submission --title '方案讨论' --goal '选择可验证的实现方案' --background '在此填写实际背景' --boundaries '在此填写约束、检查周期和有限截止条件' --experts-file data/experts.json --phase initial --access-file data/room-owner.json | ConvertFrom-Json
|
|
67
71
|
$room = $created.data.id
|
|
72
|
+
$host_access = $created.data.member_access_files.Codex
|
|
73
|
+
$expert_access = $created.data.member_access_files.Gemini
|
|
68
74
|
codiscuss room view $room
|
|
69
75
|
codiscuss room brief $room
|
|
70
76
|
codiscuss room protocol $room
|
|
71
77
|
```
|
|
72
78
|
|
|
73
|
-
`--experts-file` 替换完整参与配置,更新时不可只传单个增量。`--host`
|
|
79
|
+
`--experts-file` 替换完整参与配置,更新时不可只传单个增量。`--host` 可显式指定主持人,默认采用创建操作者。新房间发言权由主持人持有,之后每次需读取最新状态。名单与主持职责只能由房主变更;新增或改变读写能力时,使用网页或 `api` 同时提供对应 `members` 及新凭据,保留未变成员凭据。
|
|
74
80
|
|
|
75
81
|
主持人创建安排并回读:
|
|
76
82
|
|
|
77
83
|
```powershell
|
|
78
84
|
$turn_submission = [guid]::NewGuid().ToString()
|
|
79
|
-
$planned = codiscuss turn create $room --as Codex --expert Gemini --question '基于议题资料提出你的初始观点、依据与风险。' --submission-id $turn_submission | ConvertFrom-Json
|
|
85
|
+
$planned = codiscuss turn create $room --as Codex --expert Gemini --question '基于议题资料提出你的初始观点、依据与风险。' --submission-id $turn_submission --access-file $host_access | ConvertFrom-Json
|
|
80
86
|
$turn = $planned.data.turn.id
|
|
81
87
|
codiscuss turn view $room $turn
|
|
82
88
|
codiscuss floor view $room
|
|
83
89
|
codiscuss turn prompt $room $turn -o text
|
|
84
90
|
```
|
|
85
91
|
|
|
86
|
-
只有实际通过约定渠道送达后,才执行 `turn mark-notified ROOM TURN --as Codex`。本地专家也可自行发现 `pending` 安排,不要求补通知状态。主持人应将房间、安排及提交编号保存到接续信息。
|
|
92
|
+
只有实际通过约定渠道送达后,才执行 `turn mark-notified ROOM TURN --as Codex --access-file FILE`。本地专家也可自行发现 `pending` 安排,不要求补通知状态。主持人应将房间、安排及提交编号保存到接续信息。
|
|
87
93
|
|
|
88
|
-
Gemini 在自己的会话中读取实际房间与安排,并将完整回答保存为 UTF-8 `data/answer.txt` 后执行(`$room`、`$turn`
|
|
94
|
+
Gemini 在自己的会话中读取实际房间与安排,并将完整回答保存为 UTF-8 `data/answer.txt` 后执行(`$room`、`$turn` 在该终端设为刚核验的真实值,`$expert_access` 为私下交付的 Gemini 文件路径):
|
|
89
95
|
|
|
90
96
|
```powershell
|
|
91
97
|
codiscuss room view $room
|
|
92
98
|
codiscuss turn current $room
|
|
93
99
|
$floor = (codiscuss floor view $room | ConvertFrom-Json).data
|
|
94
|
-
codiscuss floor take $room --as Gemini --revision $floor.revision
|
|
100
|
+
codiscuss floor take $room --as Gemini --revision $floor.revision --access-file $expert_access
|
|
95
101
|
codiscuss floor view $room
|
|
96
102
|
$answer_submission = [guid]::NewGuid().ToString()
|
|
97
|
-
$posted = codiscuss message post $room --as Gemini --turn $turn --summary '在此填写回答的简述' --body-file data/answer.txt --submission-id $answer_submission | ConvertFrom-Json
|
|
103
|
+
$posted = codiscuss message post $room --as Gemini --turn $turn --summary '在此填写回答的简述' --body-file data/answer.txt --submission-id $answer_submission --access-file $expert_access | ConvertFrom-Json
|
|
98
104
|
codiscuss message view $room $posted.data.message.id
|
|
99
105
|
codiscuss turn current $room
|
|
100
106
|
codiscuss floor view $room
|
|
@@ -103,10 +109,10 @@ codiscuss floor view $room
|
|
|
103
109
|
确认 `holder` 为自己才能提交;否则保留草稿等待。完整原文保存成功后安排为 `answered`,发言权自动归还,下一位为主持人。主持人读过回答后更新状态,并在下一次安排或主持发言前重新领取:
|
|
104
110
|
|
|
105
111
|
```powershell
|
|
106
|
-
codiscuss state set $room --as Codex --body-file data/state.txt --brief-file data/brief.txt --until $posted.data.message.id
|
|
112
|
+
codiscuss state set $room --as Codex --body-file data/state.txt --brief-file data/brief.txt --until $posted.data.message.id --access-file $host_access
|
|
107
113
|
codiscuss state view $room
|
|
108
114
|
$floor = (codiscuss floor view $room | ConvertFrom-Json).data
|
|
109
|
-
codiscuss floor take $room --as Codex --revision $floor.revision
|
|
115
|
+
codiscuss floor take $room --as Codex --revision $floor.revision --access-file $host_access
|
|
110
116
|
codiscuss floor view $room
|
|
111
117
|
```
|
|
112
118
|
|
|
@@ -114,6 +120,8 @@ codiscuss floor view $room
|
|
|
114
120
|
|
|
115
121
|
## 常用命令
|
|
116
122
|
|
|
123
|
+
下表省略每次写入必须附带的 `--access-file FILE`,实际执行时使用本人文件。
|
|
124
|
+
|
|
117
125
|
| 目的 | 命令示意 |
|
|
118
126
|
| --- | --- |
|
|
119
127
|
| 查找原文 | `message list ROOM --query 关键词 --author Gemini --page 1` |
|
|
@@ -173,11 +181,11 @@ codiscuss message list $room --submission-id $answer_submission
|
|
|
173
181
|
| 3 | 网络失败或结果不能确认;POST 可能已保存 |
|
|
174
182
|
| 130 | 中断,写入结果需核对 |
|
|
175
183
|
|
|
176
|
-
`--as`
|
|
184
|
+
`--as` 声明业务身份,不是身份认证;所有房间写入都必须带有效的 `--access-file`,不能仅凭发言权写入。
|
|
177
185
|
|
|
178
186
|
## 运行控制与一次执行
|
|
179
187
|
|
|
180
|
-
|
|
188
|
+
CLI `room create` 默认手动。运行控制使用房主私有文件(下例 `PRIVATE.json`),成员文件不能启动或切换运行。CLI 校验站点和房间一致,不把 token 放进预览或回执。不要读取并拼接认证头,也不要将文件上传到共享记录。
|
|
181
189
|
|
|
182
190
|
```sh
|
|
183
191
|
codiscuss run view ROOM --site SITE
|
package/docs/deployment.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
本地 Python / SQLite 与 Cloudflare Pages / D1 提供同样的讨论接口。二选一即可,数据库彼此独立,不自动同步。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
当前服务没有用户登录和读取权限隔离;所有新房间都要求房主或成员写入凭据。先阅读 [安全说明](../.github/SECURITY.md),再决定是否暴露到公网。外部访问控制也可能阻止在线 AI 读取;此时按工作流转交必要材料。
|
|
6
6
|
|
|
7
7
|
## Python 服务
|
|
8
8
|
|
|
@@ -218,4 +218,4 @@ codiscuss run start ROOM --site SITE --revision REV --access-file "PRIVATE.json"
|
|
|
218
218
|
|
|
219
219
|
自动模式可连接运行中的 Codex App、Antigravity App,或按需启动 Claude Code CLI、Codex CLI、Antigravity CLI(`agy`)。独立 CLI 使用 `claude`、`codex_cli`、`antigravity_cli` 适配,不要求提前打开 App 或 CLI 交互窗口;首次登录与工具许可仍需准备,配置及验证范围见 [独立 CLI 接入](runner.md#独立-cli-接入)。App 关闭、账号失效或权限等待不能算作已开始;已交付任务等待实际进入,不因聊天忙碌重复新建,等待仍受整场截止约束。接入程序退出、断网或崩溃后,旧执行最多在剩余租约内写入;恢复前核对原会话,过期资格不会重新激活。重试和停止规则见 [自动接入程序](runner.md)。
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
手动兜底:由房主暂停并核对原执行停止后,切换手动接入;复制原接入指令,在原会话继续。每位本地成员使用自己的私有文件作为 `--access-file`,不能使用房主文件署名,也不把 token 放进复制文本。公开房间不保存本机目录、软件账号或配置文件。
|
package/docs/runner.md
CHANGED
|
@@ -69,12 +69,22 @@ Windows 的 MSIX 应用(如 Claude 桌面版)可能把派生进程对 AppDat
|
|
|
69
69
|
python server.py --host 127.0.0.1 --local-launch
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
需要 Node.js 24+ 与已安装、已登录的 AI 客户端;讨论所需的最小工具权限按 [单次执行权限](#单次执行权限)
|
|
72
|
+
需要 Node.js 24+ 与已安装、已登录的 AI 客户端;讨论所需的最小工具权限按 [单次执行权限](#单次执行权限) 提供。新建时分别选择房间用法与“程序唤醒”,从角色库添加本地参与实例并指定主持人;自由协作默认用户主持,头脑风暴须选择本地 AI 主持。参与区只显示已加入的实例,用独立“移出”按钮调整名单。自动 CLI 卡片背面通过真实软件目录下拉选择模型,并按该模型支持的档位显示强度滑杆;首档使用 CLI 默认,agy 的强度包含在模型 ID 中。恢复默认只清除该实例的模型与强度覆盖,App、网页和手动会话在原软件设置。配置入口不依赖勾选立即启动,移出后重新加入保留本次填写。头脑风暴可勾选“创建后立即启动 AI”,按需填写项目目录与项目文件权限(默认只读),再点击“创建并启动讨论”;不勾选则只建房,之后在“接入”页启动。自由协作的“创建并连接”登记自动成员,后续点名产生任务时才唤醒。配置可“保存为本机软件默认配置”,保存本身不启动任何 AI。卡片输入沿用当前页面的启动草稿,不随只建房请求保存到网站。正常首次自动接入无需下载 JSON、手动登记房间或另执行 `run start`。
|
|
73
73
|
|
|
74
74
|
网页在发送前把完整已提交输入、原提交号和控制凭据保存在当前站点的浏览器本地存储。服务启动失败或响应丢失后,刷新仍会显示“继续未完成的启动”;点击“继续本次启动”沿用原请求和本机已固定配置,无需重新填写,也不随新的默认配置变化。刷新只恢复界面,不自动运行;已暂停、结束、过期或在启动期间改变的房间不会因此重新开始。部分失败明确显示已核实的阶段,已建房不等于回滚;结果不明时保留原请求待核对。
|
|
75
75
|
|
|
76
76
|
“移除待办”仅取消此浏览器的重试意图,不删除房间或停止讨论。首次请求预检确认尚未执行时可直接修正后提交;已有部分结果或结果不明时,应先处理原请求,不悄悄换编号建另一场。请求完成后先保存房间凭据关联,再移除待办;不同标签的迟到失败不能删除新一次重试的记录。重试自身的预检失败也不删除原待办,因为另一次在途请求可能已产生结果。浏览器本地存储不可写时,网页会在发送前报错;存储被清除后,使用原私有配置恢复,不提供仅凭公开房间编号的自动认领。
|
|
77
77
|
|
|
78
|
+
### 本机模型目录
|
|
79
|
+
|
|
80
|
+
网页的自动 CLI 模型选择通过 `POST /api/local/models` 读取本机软件目录。输入为 `adapter`、可选完整 `executable`;未传路径时沿用该适配器的本机配置。CLI 对应 `codiscuss launch models --file FILE`。返回 `{adapter, models:[{id,label,efforts?}], source, error?}`,只含模型元数据;没有列表时显示原因并允许使用软件默认,不要求手填 ID。页面按适配器和路径缓存,模型支持的档位随目录返回;App、纯网页和手动会话在原软件设置。
|
|
81
|
+
|
|
82
|
+
- Codex:临时运行 `app-server`,依次 `initialize`、`initialized`、分页 `model/list`,不创建 thread 或 turn。见 [官方协议](https://learn.chatgpt.com/docs/app-server#list-models-modellist)。
|
|
83
|
+
- Claude:临时以 `--safe-mode --no-session-persistence` 开启流式控制通道,仅发送 initialize,读取软件返回的 models 和 supportedEffortLevels;禁用自定义启动扩展,不发送用户消息。协议依据 [官方客户端初始化](https://github.com/anthropics/claude-agent-sdk-python/blob/main/src/claude_agent_sdk/_internal/query.py)。旧版本不支持时显示查询失败。
|
|
84
|
+
- Antigravity:执行只读 `agy models`,解析软件返回的模型 ID 与显示名;档位随模型。见 [官方命令说明](https://antigravity.google/docs/cli/headless/#select-a-model-effort-or-agent)。
|
|
85
|
+
|
|
86
|
+
查询仅使用 Node 内置模块,进程最长 30 秒、响应最多 1 MiB,结束或失败均关闭查询进程。原始软件诊断和账号字段不返回网页;本机入口沿用 Origin 与站点连接鉴权,不需要房间凭据。目录可能含客户端预置项目,不证明账号额度或实际推理可用。本次以本机三个 CLI 完成只读列表核对,没有发送模型提示词或启动真实讨论。
|
|
87
|
+
|
|
78
88
|
### 已启动后的配置修正
|
|
79
89
|
|
|
80
90
|
软件启动失败后,先查看进度看板的失败摘要,必要时展开完整错误。登录或工具权限由对应软件处理。切换 Codex 时,可直接打开进度看板参与者旁的“…”菜单,选择“切换 App / CLI”,在弹窗内暂停、读取配置并保存;已经暂停时直接读取。软件路径仍在“接入 → 修正本场软件连接”修改。两个入口共用本场配置,不修改本机默认配置、不自动重启,保留原房间、发言进度和会话。服务确认采用后,可在进度看板明确重启;未确认时先核对本机服务。
|
|
@@ -134,7 +144,7 @@ codiscuss launch start --file 本次启动.json
|
|
|
134
144
|
|
|
135
145
|
预览核对软件字段、路径、参与方式和讨论上限,不创建房间、服务或 AI;它不证明账号登录和工具权限已就绪。不限总时长时,运行的 `deadline` 为 0,按轮次上限或手动停止结束;执行租约与单次超时继续有效,进入收束后按收束预留生成有限截止。启动回执只含房间、状态与文件路径,不输出令牌。`current_task` 返回当前主持须读取的任务文件和本次执行凭据路径,随后按参与技能实际 `execution enter`。
|
|
136
146
|
|
|
137
|
-
|
|
147
|
+
建房提交号、房间控制凭据、各实例的成员凭证、本次解析后的软件配置、启动期望版本和首个主持执行先保存在本机,再进行后续操作。网页已生成的成员凭证由启动器保留,不能重新生成造成交付文件失效。网络结果不明时沿用原文件重试,不换提交号;重试使用本次已保存配置,后来修改本机默认值只影响新请求。改变单场显式配置仍会被拒绝作为同次重试。启动后的房间仍需从原进度看板明确继续,重复启动不会自动恢复暂停或结束的讨论。已使用其他配置登记的房间拒绝重复接管,沿用原配置。
|
|
138
148
|
|
|
139
149
|
接入已有受保护房间时,启动器先验证事件读取所需的控制凭据,再保存日志、启动服务和改变房间状态;错误凭据不留下失败绑定。旧日志曾保存错误凭据时,沿用原请求并提供正确的私有凭据文件或网页凭据,验证成功后会修正该房间配置;运行中的服务读取修正值,保留原会话、运行代次和暂停状态。启动回执不代表 AI 已进入或已完成任务,仍以网站实际执行记录为准。
|
|
140
150
|
|
|
@@ -214,7 +224,7 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
|
|
|
214
224
|
| `claude` | 现有 `claude -p --output-format json`;后续 `--resume ID` | 沿用上表中的 Claude 规则 |
|
|
215
225
|
| `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd 时使用本场独立草稿目录。只在讨论服务新建的会话中执行:每位参与者一个独立原生讨论项目,续接复用该项目;不绑定外部已有会话 |
|
|
216
226
|
|
|
217
|
-
三种 CLI 适配器可选 `model`:分别作为 `codex -m`、`claude --model`、`agy --model` 的参数传入,留空使用各 CLI
|
|
227
|
+
三种 CLI 适配器可选 `model`:分别作为 `codex -m`、`claude --model`、`agy --model` 的参数传入,留空使用各 CLI 的默认模型;可存入本机默认配置,也可在新建讨论时按实例从软件目录中选择。配置文件的模型名不能以 `-` 开头或含空格。可选 `effort` 设置思考档位:Codex 接受 low / medium / high / xhigh / max / ultra,以 `-c model_reasoning_effort="档位"` 传入;Claude 接受 low / medium / high / xhigh / max,以 `--effort` 传入;网页按所选模型实际支持的档位收窄。agy 的档位写在模型名中(如 `gemini-4-argon-high`),不单独设置。App 适配器使用 App 内选择的模型与档位。
|
|
218
228
|
|
|
219
229
|
进度看板的参与者姓名下显示本场已登记的模型与思考强度,不使用新建草稿或本机默认配置代替。由本机桥接的只读 `room-models` 返回 `adapter/model/effort`,按当前站点与房间匹配;本机页面和已授权连接的线上页面均可读取,不要求暂停或浏览器持有房间控制凭据,不返回路径、令牌或会话编号。CLI 未显式指定时显示“默认模型 / 默认强度”,agy 显示“强度随模型”,App 和手动参与提示在原软件内设置;未连接本机、未登记或旧版服务不支持时显示未获取。它展示启动配置,不能证明软件最终采用的默认值,也不提供讨论中改模型的入口。
|
|
220
230
|
|
|
@@ -263,12 +273,14 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
|
|
|
263
273
|
|
|
264
274
|
## 自由协作房间
|
|
265
275
|
|
|
266
|
-
|
|
276
|
+
网页选择“自由协作”及自动接入后,启动器沿用浏览器生成的房间控制凭据和各成员凭证,保存到本机私有配置 `members` 并登记常驻服务,不启用讨论运行。私有配置带 `mode: "free"`,服务据此使用 `free_runner.mjs`。手动建房直接调用网站,不启动或要求本机服务;浏览器保存私有接入资料,按成员分别下载凭据文件。
|
|
267
277
|
|
|
268
|
-
`codiscuss launch room-member` 在运行中的自由房间邀请(`add`)或请回(`rejoin
|
|
278
|
+
`codiscuss launch room-member` 在运行中的自由房间邀请(`add`)或请回(`rejoin`)自动成员:先用房间控制凭据核对房主身份,在登记配置中写入成员的软件配置与新凭证,再通知网站;网页通过 `member_token` 提供其已保存的新凭证,结果不明时沿用同一凭证重试。该命令也可为手动成员生成配置旁的 `manual-成员名.access.json`,回执只含文件路径;网页手动邀请和请回则直接在网站完成。常驻服务读取更新后的配置时逐个成员核对:新成员直接采用,已绑定会话的成员若项目、cwd 或接入方式变化,只阻止该成员并报告原因,不影响其他成员。
|
|
269
279
|
|
|
270
280
|
每次检查读取 `inbox`:成员没有进行中的执行且有待投递消息时,先持久化本地 `client_id` 再领取执行,然后用原适配器续接该成员的绑定会话;任务文件列出执行编号、待处理消息和已读起点,凭据文件只含成员凭证。进程退出而网站未收到回答、软件报错或超过单次执行时限时报告失败;只有确认未送达才退回队列重试,最多 3 次。领取后、调用前中断的投递不自动重发。主持人的任务文件带 `role: "host"` 和每条请求的 `request_purposes`(`ask` / `route` / `handoff`),说明里写明如何用 `--to` 派活与何时结束。网站上执行被打断、成员离开或被移除后,只停止该成员的进程;不同成员各自投递,可以并行。成员配置了 cwd 且位于 Git 工作区时,执行前后各做一次快照,执行结束且进程退出后报告改动的文件,供网站提示可能冲突;服务重启会丢失尚未报告的快照。只能在网页创建自由房间,不支持对话入口。
|
|
271
281
|
|
|
282
|
+
Antigravity App 的 Sidecar 按房间用法校验任务:讨论读取对应事件,自由房间读取对应 `executions/ID` 并核对有效租约。自由执行的受理回执由服务核对并绑定真实会话,即使回答已快速完成也保留绑定;取消、过期或结果不明的投递不重新发送。本次使用替身 Sidecar 与临时服务验证,未重新调用真实 App。
|
|
283
|
+
|
|
272
284
|
## 常驻讨论服务
|
|
273
285
|
|
|
274
286
|
`discussion_service.mjs` 把上述事件消费封装为本机常驻服务,`codiscuss.mjs` 提供面向用户的 CLI:一个进程托管所有已登记房间,网页看板点「重启」后无需再回终端启动程序。服务每轮检查后等待 2 秒,重读登记表并对每个房间执行同一 `DiscussionRunner` 检查;房间暂停、到期、停止或收束时只继续等待,不退出。单个房间的配置错误、数据目录被 `watch` 占用或网站请求失败只记录在该房间状态中,其他房间照常处理。
|
package/docs/workflow.md
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
## 选择推进方式
|
|
6
6
|
|
|
7
7
|
- **全自动讨论**:本机软件与权限配置完成后明确启动,可按轮次、总时长上限或手动停止结束。主持创建正式安排即结束本步;专家自行认领、回答,程序再续接原主持。自动任务不另建原生定时。项目及 cwd 可选,分别属于本机接入配置。
|
|
8
|
-
-
|
|
9
|
-
-
|
|
8
|
+
- **自由协作**:你或 AI 主持人点名一位或多位成员,各自在原会话处理请求;没有讨论发言权、安排和轮次。可自动或手动接入,见下方 [自由协作房间](#自由协作房间)。
|
|
9
|
+
- **手动接入(手动复制冷启动)**:是自由协作或头脑风暴的接入方式,不是第三种房间。建房无需本机接入服务;本地成员持自己的凭据文件自行读写,可由人工或软件原生定时触发。在线成员只读,回答需转交代发。复制参与指令或下载凭据不代表已经送达、接入或唤醒。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
切换前先暂停,核对原会话与原生任务停止,再由房主按最新版本修改模式。切到手动保留历史与未完成安排,本地成员使用自己的 `--access-file`,不能借用房主凭据署名。切回自动需明确启动新代次并沿用所选上限,成员持久凭证不能替代本次执行凭据。故障只暂停并保留原因,不静默换软件、身份或创建第二个会话。
|
|
12
12
|
|
|
13
13
|
## 自由协作房间
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
自由协作房间从同一角色库选择一位或多位成员,默认由用户主持,也可在创建时指定一位本地 AI 主持人。只有一位成员时,每条消息都交给它;有多位时,在输入框上方点选成员,或在正文中 `@成员`(代码和引用中的 @ 不算),被点名的成员各自处理;未点名且没有 AI 主持人时只作留言。自动成员空闲时,服务把它名下所有未处理的消息打包成一次执行交给原会话;执行期间再发的消息排队,下一次一起投递。手动成员自行读取自己的请求,在线成员由用户转交。成员按时间顺序处理,后面的消息可以修正前面的。
|
|
16
16
|
|
|
17
17
|
运行中可在右侧成员栏邀请 AI(沿用本机软件配置和本房间工作目录),也可让成员离开、请回或移除。离开会立即停止它的工作;请回时换发新凭证并续接它原来的会话。邀请时可选“手动接入”:成员不会被自动唤醒,而是得到一份私有凭据文件,由你在它的会话中按参与技能读写。绑定你正在使用的会话的成员会标注“借用会话”:它工作时不要在原窗口继续操作。
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
页面在每条消息上标出请求状态(待投递、处理中、已回复、已停止、失败)。本机服务离线时请求停在“待投递”,不显示为已唤醒。点「停止」会作废当前执行和尚未投递的消息,迟到的回答会被拒绝;之后重新发消息即可。一次执行失败只影响这次请求,房间继续可用。
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
两种房间的用户写入均须房间控制凭据,未持有凭据的浏览器只能阅读。手动成员的创建、邀请和请回直接在网站完成,并交付各自私有凭据文件;在线成员只交付阅读指令。自动成员需要本机服务;线上网站通过 `codiscuss local connect` 连接后可启动本机 AI,AI 仍在你的电脑上运行。
|
|
30
30
|
|
|
31
31
|
## 讨论上限
|
|
32
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai_co_discuss",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"bin": {
|
|
5
5
|
"codiscuss": "runner/codiscuss.mjs"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dev": "npm run build && wrangler pages dev --ip 127.0.0.1 --port 8787",
|
|
9
9
|
"db:local": "wrangler d1 execute DB --local --file backend/schema.sql",
|
|
10
10
|
"db:remote": "wrangler d1 execute DB --remote --file backend/schema.sql",
|
|
11
|
-
"check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs",
|
|
11
|
+
"check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_models.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs",
|
|
12
12
|
"runner": "node runner/discussion_runner.mjs",
|
|
13
13
|
"service": "node runner/discussion_service.mjs",
|
|
14
14
|
"build:ui": "vite build --config frontend/vite.config.mjs --mode markdown && vite build --config frontend/vite.config.mjs --mode backup && vite build --config frontend/vite.config.mjs",
|
package/runner/discuss.py
CHANGED
|
@@ -7,7 +7,9 @@ import json
|
|
|
7
7
|
import math
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
import secrets
|
|
10
11
|
import sys
|
|
12
|
+
import tempfile
|
|
11
13
|
from dataclasses import dataclass
|
|
12
14
|
from http.client import HTTPException
|
|
13
15
|
from pathlib import Path
|
|
@@ -29,7 +31,8 @@ INSTANCE_PATTERN = re.compile(r"(Codex|Claude|Gemini|DeepSeek|Qwen)(?:-[0-9A-Za-
|
|
|
29
31
|
PHASES = ("preparing", "initial", "discussing", "reviewing", "paused", "concluded")
|
|
30
32
|
KINDS = ("opinion", "moderation", "receipt", "chat")
|
|
31
33
|
OVERVIEW_FIELDS = ("id", "title", "goal", "host", "phase", "focus", "message_count",
|
|
32
|
-
"latest_id", "state_until", "current_turn", "queued_turns", "held_since", "floor", "runtime", "limits", "completed_rounds", "latest_receipts", "answered_turns", "duplicate", "submission_id"
|
|
34
|
+
"latest_id", "state_until", "current_turn", "queued_turns", "held_since", "floor", "runtime", "limits", "completed_rounds", "latest_receipts", "answered_turns", "duplicate", "submission_id",
|
|
35
|
+
"mode", "members", "free", "parent_room", "linked_rooms")
|
|
33
36
|
BRIEF_FIELDS = ("id", "title", "goal", "host", "user_request", "background", "boundaries", "experts")
|
|
34
37
|
ROOM_LIST_FIELDS = ("id", "title", "goal", "message_count", "latest_id", "created_at")
|
|
35
38
|
GUIDANCE = {
|
|
@@ -796,6 +799,60 @@ def emit(value, output="json", error=False):
|
|
|
796
799
|
stream.flush()
|
|
797
800
|
|
|
798
801
|
|
|
802
|
+
def save_access(path, value):
|
|
803
|
+
"""Persist private capabilities before sending requests, without printing their content."""
|
|
804
|
+
target = Path(path)
|
|
805
|
+
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=target.parent, delete=False) as stream:
|
|
806
|
+
temporary = Path(stream.name)
|
|
807
|
+
os.chmod(temporary, 0o600)
|
|
808
|
+
json.dump(value, stream, ensure_ascii=False, indent=2)
|
|
809
|
+
stream.flush()
|
|
810
|
+
os.fsync(stream.fileno())
|
|
811
|
+
try:
|
|
812
|
+
os.replace(temporary, target)
|
|
813
|
+
finally:
|
|
814
|
+
temporary.unlink(missing_ok=True)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def prepare_creation_access(args, site, plan):
|
|
818
|
+
"""Create or reuse one explicit private credential bundle for a stable creation id."""
|
|
819
|
+
if getattr(args, "operation", "") != "room create":
|
|
820
|
+
return
|
|
821
|
+
if not getattr(args, "access_file", None):
|
|
822
|
+
raise CliError("建房须用 --access-file 指定私有凭据文件路径;首次自动生成,重试沿用同一文件")
|
|
823
|
+
path = Path(args.access_file)
|
|
824
|
+
payload = plan.body
|
|
825
|
+
access = read_object(path) if path.exists() else {"site": site, "room": "", "token": secrets.token_urlsafe(48),
|
|
826
|
+
"submission_id": payload.get("submission_id"), "members": {}}
|
|
827
|
+
if access.get("site") != site or access.get("submission_id") != payload.get("submission_id"):
|
|
828
|
+
raise CliError("建房凭据文件与本次站点或 submission_id 不符")
|
|
829
|
+
experts = payload.setdefault("experts", {name: "cli" if name == "Codex" else "online" for name in PARTICIPANTS[1:]})
|
|
830
|
+
members = []
|
|
831
|
+
for name, capability in experts.items():
|
|
832
|
+
member = access["members"].setdefault(name, {"token": secrets.token_urlsafe(48)})
|
|
833
|
+
members.append({"name": name, "token": member["token"], "access": "online" if capability == "online" else "manual"})
|
|
834
|
+
payload["members"] = members
|
|
835
|
+
save_access(path, access)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def finish_creation_access(args, site, plan, value):
|
|
839
|
+
"""Save the confirmed room and separate member files; return paths without secrets."""
|
|
840
|
+
if getattr(args, "operation", "") != "room create":
|
|
841
|
+
return {}
|
|
842
|
+
path = Path(args.access_file).resolve()
|
|
843
|
+
access = read_object(path)
|
|
844
|
+
access["room"] = value["id"]
|
|
845
|
+
save_access(path, access)
|
|
846
|
+
paths = {}
|
|
847
|
+
for member in plan.body["members"]:
|
|
848
|
+
if member["access"] == "online":
|
|
849
|
+
continue
|
|
850
|
+
member_path = path.with_name(f"{path.stem}.{member['name']}.json")
|
|
851
|
+
save_access(member_path, {"site": site, "room": value["id"], "token": member["token"]})
|
|
852
|
+
paths[member["name"]] = str(member_path)
|
|
853
|
+
return {"owner_access_file": str(path), "member_access_files": paths}
|
|
854
|
+
|
|
855
|
+
|
|
799
856
|
def access_headers(args, site, path):
|
|
800
857
|
"""Load a room-scoped secret only for its exact site and room."""
|
|
801
858
|
if not getattr(args, "access_file", None):
|
|
@@ -803,7 +860,9 @@ def access_headers(args, site, path):
|
|
|
803
860
|
access = read_object(args.access_file)
|
|
804
861
|
expected = f"/api/rooms/{access.get('room', '')}"
|
|
805
862
|
target = urlsplit(path).path
|
|
806
|
-
|
|
863
|
+
creation = target == "/api/rooms" and bool(access.get("submission_id"))
|
|
864
|
+
reading = f"/r/{access.get('room', '')}"
|
|
865
|
+
if access.get("site", "").rstrip("/") != site or not (creation or target == expected or target.startswith(expected + "/") or target == reading or target.startswith(reading + "/")):
|
|
807
866
|
raise CliError("凭据文件不属于本次站点和房间")
|
|
808
867
|
token = access.get("token")
|
|
809
868
|
if not isinstance(token, str) or not 32 <= len(token) <= 256:
|
|
@@ -811,6 +870,16 @@ def access_headers(args, site, path):
|
|
|
811
870
|
return {"X-Discussion-Token": token}
|
|
812
871
|
|
|
813
872
|
|
|
873
|
+
def preview_body(value):
|
|
874
|
+
"""Keep private capability material out of offline request previews."""
|
|
875
|
+
if isinstance(value, dict):
|
|
876
|
+
return {key: "[redacted]" if key in ("token", "execution_token", "control_hash", "token_hash") else preview_body(item)
|
|
877
|
+
for key, item in value.items()}
|
|
878
|
+
if isinstance(value, list):
|
|
879
|
+
return [preview_body(item) for item in value]
|
|
880
|
+
return value
|
|
881
|
+
|
|
882
|
+
|
|
814
883
|
def run(args, registry):
|
|
815
884
|
"""Execute a parsed command, with schema and previews remaining offline."""
|
|
816
885
|
if args.resource == "schema":
|
|
@@ -825,14 +894,17 @@ def run(args, registry):
|
|
|
825
894
|
url += "?" + urlencode(plan.params)
|
|
826
895
|
meta = {"site": site, "method": plan.method, "path": plan.path}
|
|
827
896
|
if getattr(args, "dry_run", False):
|
|
828
|
-
return {"ok": True, "data": {"method": plan.method, "url": url, "body": plan.body,
|
|
897
|
+
return {"ok": True, "data": {"method": plan.method, "url": url, "body": preview_body(plan.body),
|
|
829
898
|
**({"file": plan.upload_file} if plan.upload_file else {}),
|
|
830
899
|
**({"destination": plan.download_file} if plan.download_file else {})},
|
|
831
900
|
"meta": {**meta, "dry_run": True, "validation": "仅本地参数;未验证房间、身份或服务端状态"}}
|
|
832
901
|
try:
|
|
902
|
+
prepare_creation_access(args, site, plan)
|
|
833
903
|
headers = access_headers(args, site, plan.path)
|
|
834
904
|
value = send_request(plan, url, timeout, headers) if headers else send_request(plan, url, timeout)
|
|
835
905
|
result = project_result(value, plan, site)
|
|
906
|
+
if getattr(args, "operation", "") == "room create":
|
|
907
|
+
result.update(finish_creation_access(args, site, plan, value))
|
|
836
908
|
except CliError as error:
|
|
837
909
|
error.details.update(meta)
|
|
838
910
|
if plan.method == "POST":
|
|
@@ -474,7 +474,9 @@ async function antigravity_delivery(config, binding, task) {
|
|
|
474
474
|
} catch (error) { throw not_sent(error); }
|
|
475
475
|
const path = join(directory, `${task.id}.request.json`);
|
|
476
476
|
if (existsSync(path)) throw new Error('Sidecar 请求已存在;必须核对回执,不自动重复发送');
|
|
477
|
-
atomic_json(path, { id: task.id,
|
|
477
|
+
atomic_json(path, { id: task.id, mode: task.id.startsWith('free-') ? 'free' : 'brainstorm',
|
|
478
|
+
event_id: Number(task.id.replace(/^(event|free)-/, '')), prompt: task.prompt,
|
|
479
|
+
session_id: binding.session_id || '', access_path: task.access_path });
|
|
478
480
|
return { session_id: binding.session_id || '', status: 'sidecar_pending' };
|
|
479
481
|
}
|
|
480
482
|
|
|
@@ -7,6 +7,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
|
|
9
9
|
import { atomic_json } from './discussion_adapters.mjs';
|
|
10
|
+
import { software_models } from './discussion_models.mjs';
|
|
10
11
|
import { acquire_lock, api, CODEX_ADAPTERS, DiscussionStore, service_drafts, task_files, validate_config } from './discussion_runner.mjs';
|
|
11
12
|
import { assert_shared_home, CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
|
|
12
13
|
|
|
@@ -35,12 +36,14 @@ const HELP = `用法:codiscuss launch settings [--file FILE]
|
|
|
35
36
|
codiscuss launch start --file FILE [--dry-run]
|
|
36
37
|
codiscuss launch room-config --file FILE
|
|
37
38
|
codiscuss launch room-models --file FILE
|
|
39
|
+
codiscuss launch models --file FILE
|
|
38
40
|
codiscuss launch room-member --file FILE
|
|
39
41
|
|
|
40
42
|
settings 读取本机软件默认配置;指定文件时保存,不启动 AI。
|
|
41
43
|
start 使用启动 JSON 创建或接入房间并启动;--dry-run 只校验和预览。
|
|
42
44
|
room-config 修正已暂停房间的软件路径或切换 Codex App / CLI,不启动讨论;须提供 site、room_id、token。
|
|
43
45
|
room-models 只读指定 site、room_id 的本机登记模型与思考强度,不启动讨论。
|
|
46
|
+
models 按 adapter、executable 读取本机 CLI 的模型目录,不启动讨论或生成回答。
|
|
44
47
|
room-member 为运行中的自由房间邀请或请回一位成员;须提供 site、room_id、token、action(add/rejoin)、name。
|
|
45
48
|
JSON 指定 site、entry(conversation/web)、room(建房内容,含 submission_id)
|
|
46
49
|
或 room_id(已有房间)、participants(本次软件覆盖配置)。
|
|
@@ -134,7 +137,8 @@ function launch_input(input) {
|
|
|
134
137
|
// P1: a free room is launched once, at creation, from the web page.
|
|
135
138
|
if (value.entry !== 'web' || value.room_id) throw new Error('自由协作房间目前只能在网页创建时启动');
|
|
136
139
|
const members = value.room.members;
|
|
137
|
-
|
|
140
|
+
const names = members?.map(member => typeof member === 'string' ? member : member.name);
|
|
141
|
+
if (!Array.isArray(members) || !members.length || new Set(names).size !== names.length || names.some(name => !clients_of(name))) {
|
|
138
142
|
throw new Error('自由协作房间须选择至少 1 位支持自动接入的 AI 成员,且不能重复');
|
|
139
143
|
}
|
|
140
144
|
}
|
|
@@ -158,7 +162,8 @@ function launch_config(home, input, room, directory, options) {
|
|
|
158
162
|
for (const name of new Set([room.host, ...Object.keys(room.experts)])) {
|
|
159
163
|
participants[name] = participant_profile(name, defaults, input, name === room.host);
|
|
160
164
|
}
|
|
161
|
-
|
|
165
|
+
const members = launch_members(input, Object.keys(participants), options);
|
|
166
|
+
return validate_config({ site: input.site, room: room.id || 'preview', token: options.token, participants, members,
|
|
162
167
|
cwd: input.cwd, discussion_project: input.discussion_project, project_access: input.project_access || undefined,
|
|
163
168
|
data_directory: join(directory, 'runtime'), draft_directory: service_drafts(home, join(directory, 'runtime')) }, directory);
|
|
164
169
|
}
|
|
@@ -166,13 +171,22 @@ function launch_config(home, input, room, directory, options) {
|
|
|
166
171
|
/** Free rooms add one private credential per member, kept with the room configuration for retries. */
|
|
167
172
|
function free_launch_config(home, input, room, directory, options) {
|
|
168
173
|
const defaults = options.defaults || launch_settings(home).participants;
|
|
169
|
-
const
|
|
170
|
-
const
|
|
174
|
+
const names = room.members.map(member => typeof member === 'string' ? member : member.name);
|
|
175
|
+
const participants = Object.fromEntries(names.map(name => [name, participant_profile(name, defaults, input, false)]));
|
|
176
|
+
const members = launch_members(input, names, options);
|
|
171
177
|
return validate_config({ site: input.site, room: room.id || 'preview', token: options.token, mode: 'free', participants, members,
|
|
172
178
|
cwd: input.cwd, project_access: input.project_access || undefined,
|
|
173
179
|
data_directory: join(directory, 'runtime'), draft_directory: service_drafts(home, join(directory, 'runtime')) }, directory);
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
/** Preserve caller-generated member credentials; journal newly generated ones before creation. */
|
|
183
|
+
function launch_members(input, names, options) {
|
|
184
|
+
return Object.fromEntries(names.map(name => {
|
|
185
|
+
const supplied = input.room?.members?.find(member => member?.name === name);
|
|
186
|
+
return [name, { token: supplied?.token || options.members?.[name]?.token || `${randomUUID()}${randomUUID()}` }];
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
/** Merge one participant's machine defaults with this launch's overrides and check its software. */
|
|
177
191
|
function participant_profile(name, defaults, input, current_host) {
|
|
178
192
|
const platform = platform_of(name);
|
|
@@ -235,12 +249,17 @@ export async function room_member(home, raw, locked = false) {
|
|
|
235
249
|
return room_member(home, { ...raw, name: next_instance(platform_of(name), taken), new_instance: false }, true);
|
|
236
250
|
}
|
|
237
251
|
const current = room.free.members.find(member => member.name === name);
|
|
252
|
+
if (input.member_token !== undefined && (typeof input.member_token !== 'string' || input.member_token.length < 32 || input.member_token.length > 256)) throw new Error('member_token 须为 32–256 字符');
|
|
253
|
+
if (current?.status === 'active' && input.member_token && config.members?.[name]?.token === input.member_token) {
|
|
254
|
+
const view = await site_request(input.site, `/api/rooms/${input.room_id}/members/${current.id}`, input.token, { action: 'active', token: input.member_token });
|
|
255
|
+
return { room_id: input.room_id, member: name, access: current.access, free: view, access_file: null };
|
|
256
|
+
}
|
|
238
257
|
if (action === 'add' && !current && config.participants[name] && !locked) throw new Error(`${name} 曾在本房间使用过并绑定了会话;请换一个名称`);
|
|
239
258
|
if (action === 'rejoin' && current?.status !== 'away') throw new Error(`${name} 不是离开状态,无需请回`);
|
|
240
259
|
if (action === 'add' && current) throw new Error(`${name} 已是本房间成员;离开的成员请用请回`);
|
|
241
260
|
const manual = action === 'rejoin' ? current.access === 'manual' : access === 'manual';
|
|
242
261
|
const pending = input.retry_token && config.members?.[name]?.pending === input.retry_token;
|
|
243
|
-
const token = pending ? config.members[name].token : `${randomUUID()}${randomUUID()}
|
|
262
|
+
const token = input.member_token || (pending ? config.members[name].token : `${randomUUID()}${randomUUID()}`);
|
|
244
263
|
let profile = config.participants[name];
|
|
245
264
|
if (!manual) {
|
|
246
265
|
if (action === 'add') profile = participant_profile(name, launch_settings(home).participants, { ...input, cwd: input.cwd || config.cwd }, false);
|
|
@@ -519,10 +538,11 @@ async function perform_launch(home, context, options) {
|
|
|
519
538
|
try {
|
|
520
539
|
let room = context.room;
|
|
521
540
|
const free = config.mode === 'free';
|
|
522
|
-
if (!input.room_id) room = await site_request(input.site, '/api/rooms', config.token,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
541
|
+
if (!input.room_id) room = await site_request(input.site, '/api/rooms', config.token, {
|
|
542
|
+
...input.room, ...(free ? {} : { runtime_mode: 'automatic' }),
|
|
543
|
+
members: Object.keys(config.participants).map(name => ({ name, adapter: config.participants[name].adapter,
|
|
544
|
+
token: config.members[name].token, access: 'runner', session_mode: config.participants[name].session_id ? 'borrowed' : 'new' })),
|
|
545
|
+
});
|
|
526
546
|
config.room = room.id;
|
|
527
547
|
await verify_control(config, room);
|
|
528
548
|
Object.assign(progress, { stage: 'created', room_id: room.id });
|
|
@@ -563,6 +583,10 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
563
583
|
let result;
|
|
564
584
|
if (positionals[0] === 'settings') result = input ? save_launch_settings(home, input) : launch_settings(home);
|
|
565
585
|
else if (positionals[0] === 'room-models' && input) result = room_models(home, input);
|
|
586
|
+
else if (positionals[0] === 'models' && input) {
|
|
587
|
+
const saved = Object.values(launch_settings(home).participants).find(profile => profile.adapter === input.adapter);
|
|
588
|
+
result = await software_models({ adapter: input.adapter, executable: input.executable || saved?.executable });
|
|
589
|
+
}
|
|
566
590
|
else if (positionals[0] === 'room-member' && input) result = await room_member(home, input);
|
|
567
591
|
else if (positionals[0] === 'start' && input) result = await launch_discussion(home, input, { dry_run: values['dry-run'], browser_request: values.browser });
|
|
568
592
|
else if (positionals[0] === 'room-config' && input) result = await room_configuration(home, input, { local_guard: values['configuration-locked'] });
|