ai_co_discuss 0.2.0 → 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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
1. 从任务确定目标、背景、边界、成功标准、必要专家和决策者,写入话题资料。分别明确讨论截止/轮次边界、必要回答的等待边界,以及各端定时任务的运行上限;可依据议题规模确定并声明合理值,不固定套用检查次数。沿用已有共识,只询问真正阻碍启动的缺项;默认交付简洁结论,有明确要求再写完整报告。
|
|
12
12
|
2. 当前窗口就是主持会话,从可信运行环境取得当前会话 ID;工具能隐式绑定当前聊天时不必索要 UUID。不得用最近活跃聊天或其他测试会话代替,也不另建调度聊天。
|
|
13
|
-
3. 指定房间则读取并继续,否则通过 CLI
|
|
13
|
+
3. 指定房间则读取并继续,否则通过 CLI `room create --access-file 私有房主文件` 创建;先保存建房编号,重试沿用该文件和原编号。创建回执给出各本地成员的独立文件路径,主持人后续使用自己的成员文件。主持人配置为 `cli`,专家按实际读写能力配置为 `cli` 或 `online`;不凭模型名称判断,不因浏览器碰巧打开某房间就接管它。
|
|
14
14
|
4. 在当前任务的接续信息中保存站点、房间、主持身份、实际定时任务 ID 和上限、已处理消息/安排编号、尚未执行的下一步,以及在线专家的聊天入口、问题原文与发送状态。未确认写入还需保留提交编号和原文位置。恢复时先对照网站和工具现状,不能只凭旧记录继续;交接主持时核对旧任务,避免两套主持同时推进。
|
|
15
15
|
|
|
16
16
|
接手活动话题时沿用有效安排。暂停恢复需明确继续指令,先核对目标、原阶段和安排,再恢复阶段与各自任务;已收束话题重开时追加原因,保留旧结论。新约束只重审受影响的判断并取消失效安排,目标已变成另一问题时另开关联话题。中途加入者按需补资料,已看过其他观点就如实说明,不补称独立观点。
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
**实际接入专家前,先建好房间并核验,再一次性交付简短、可执行的接入文本,不等用户催促。** 本次检查周期、运行截止和等待边界写入房间资料的边界字段,供参与者通过 `room brief` 获取。主持人回读 `room view` / `room brief`,确认议题、背景、边界和参与配置已保存。建房失败或结果不明时先处理阻碍,不让专家自行寻找或创建房间。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
需要用户首次转交时,每位本地参与者的文本通常只需“调用 discuss-participant,以实际身份通过 CLI 持续参与”、房间入口及私下交付的本人凭据文件路径;不包含 token 或其他成员文件。仅补充尚未保存在房间中的本次特殊要求。首个立即回答的专家先创建正式安排,可附真实安排编号,问题原文从安排读取;后续本地专家可先接入等待。不要再列技能/项目/CLI 路径、协议地址、阅读顺序、提交参数或定时与回执流程,这些由参与技能提供;工具入口确实缺失时再补。已运行者不重复启动,后续由其读取当前安排。正式启动前核对运行上限,不沿用已过期约定。
|
|
23
23
|
|
|
24
24
|
主持人已负责在线专家浏览器接入时,在该专家轮到发言后直接发送正式问题,保存送达记录;向用户报告结果即可,不再输出一份让用户重复转交的启动文本。用户明确要查看或复制文本时仍完整提供。首启交付、实际送达和任务启动分别记录,不能混为一个状态。
|
|
25
25
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
接入前核实本地专家已能读取 `discuss-participant`;缺失时提供仓库内 `.agents/skills/discuss-participant/SKILL.md` 的实际路径及 `docs/skills.md` 安装说明。启动文本给任务与入口,不打包背景、主持摘要或他人观点;专家按需阅读。展示文本不等于送达,网页复制按钮也不代替主持人的交付。
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
专家确实读不到入口时,主持人提供本轮判断必需的原始材料和出处,保留原文的条件、例外与反例;不得用倾向性节选替代需要复核的完整观点。记录实际交付的材料编号及仍缺少的内容;关键材料未补齐时,回答只能记为基于有限材料的暂定判断。独立阶段不夹带他人意见或主持结论。没有定时能力的可写入专家仍自行提交,只改为人工触发。同一平台可以有多个具名实例,各自使用会话和凭据;讨论房间安排仍串行,不能声称技术盲评。
|
|
34
34
|
|
|
35
35
|
## 在线专家:协助方式与操作恢复
|
|
36
36
|
|
|
@@ -34,7 +34,7 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
|
|
|
34
34
|
|
|
35
35
|
**自由协作任务(任务文件含 `mode: "free"`):** 没有发言权、安排和阶段,不执行 `execution enter`。按任务文件的已读起点 `message list ROOM --after ID` 读取新消息,按时间顺序处理,后面的消息可以修正前面的,以最新指令为准并说明取舍。完成后用 `message post ROOM --as 本人 --kind chat --execution 执行编号 --access-file FILE` 提交一次回答。提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 `--execution` 改作普通发言。手动接入的自由成员没有执行编号:从 `room view` 的 `free.requests` 找到自己的待处理请求,回答时用 `--request 请求编号` 关闭。任务文件 `role` 为 `host` 时你是本房间主持人:`route` 请求是用户未点名的消息,`handoff` 表示你派出的成员都已结束;先 `room view` 看成员,需要他人时在回答中加 `--to 成员名` 派活并说明各自负责的文件或模块,不需要时不加,本次即结束;不点名自己,不扩大用户给的权限。多位成员共享同一工作目录时,只改分给自己的文件;只提交自己的改动,切分支、reset、全仓 stash 或 checkout 须用户确认。
|
|
36
36
|
|
|
37
|
-
**手动接入:** 用户已选手动时沿用人工触发或原生定时;需要持续参与才读 [手动持续参与](references/manual.md)
|
|
37
|
+
**手动接入:** 用户已选手动时沿用人工触发或原生定时;需要持续参与才读 [手动持续参与](references/manual.md)。没有任务文件本身不代表手动授权:若房间正在自动运行,等待本次交付或核对缺项,不从旧文件接管。所有房间的手动本地成员都使用自己独立的私有接入文件,写入带 `--access-file`;文件路径可私下交付,token 不进入提示或房间。房主控制文件不能代替成员文件署名,在线成员没有自行写入资格。
|
|
38
38
|
|
|
39
39
|
## 先读什么
|
|
40
40
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
## 可靠写入
|
|
12
12
|
|
|
13
|
+
每次业务写入都带对应的 `--access-file`:手动成员使用本人的持久凭据,自动讨论使用本次执行文件。房主文件仅用于用户身份和房主管理。CLI 建房须显式指定私有房主文件,先保存建房编号;回执中的 `member_access_files` 给出各本地成员文件路径。讨论名单或主持职责变更仅房主可做,新增或改变能力的成员须提供独立的新凭据;保持未变成员原凭据,不能由成员自行扩权。
|
|
14
|
+
|
|
13
15
|
本地提交用 `message post --summary` 或 `--summary-file` 保存摘要,`--body` / `--body-file` 保存完整正文;摘要上限 1000 个 Unicode 字符。自己的摘要默认归自己。代发原作者提供的摘要时用 `--summary-by 原作者`;由主持人或用户另行整理时注明实际撰写者,默认是操作者,不能冒充原作者。摘要与正文一起使用同一提交编号,回读核对两者和归属。在线专家在原聊天分开摘要与完整正文,交由主持人留档;缺少摘要的旧记录不自动生成摘要。
|
|
14
16
|
|
|
15
17
|
一次写入先确定目标、身份、正文与关联关系,再选择对应命令:
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
|
|
32
34
|
长正文优先 UTF-8 文件,`--body-file -` 可读 stdin。当前消息正文上限为 32000 个 Unicode 字符,详细讨论状态上限为 4000 字;以目标站点协议为准。完整回答一次提交并完成安排,不能截断或用第一段冒充完整回答。超过上限时保留完整原文并说明限制,先明确完整材料的留档方式。
|
|
33
35
|
|
|
34
|
-
成功后核对服务端返回的持久记录,包括作者、正文及安排关联;响应已给出这些证据时不重复全文查询,缺少的证据再按编号补读。`duplicate=true`
|
|
36
|
+
成功后核对服务端返回的持久记录,包括作者、正文及安排关联;响应已给出这些证据时不重复全文查询,缺少的证据再按编号补读。`duplicate=true` 表示服务确认已有同一提交,不是新发言。讨论暂停或收束后不再提交新观点/安排,保留原文。手动成员凭据可能仍允许必要的 receipt 或主持说明,但自动执行失效后不能借此继续写入;不能改消息职责绕过阶段限制。
|
|
35
37
|
|
|
36
38
|
保存摘要后核对服务端返回的正文和截止编号,响应不足时用 `state view` 补读。`--until` 表示实际已读材料范围,不能证明下一安排、外部通知或清理已完成;这些事项在摘要正文中保留各自真实状态。保存结果不明时先比较现存摘要与待保存内容,不直接用旧草稿覆盖更新后的进度。
|
|
37
39
|
|
package/.github/SECURITY.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## 当前支持范围
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
项目没有用户登录、读取权限隔离或租户隔离,默认站点只由信任的使用者访问。所有新房间的用户写入、运行控制和成员管理都要求房主控制凭据;`--as`、作者、主持人只是声明,不能代替认证。本地成员只能使用自己的凭据和获授职责,房主凭据也不能冒充本地 AI 发言。服务端仅保存凭据哈希,写入与资格校验在同一事务完成。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
手动本地成员使用独立的持久成员凭证;在线成员不能凭自己的凭证写入,只能由房主或讨论主持人代发真实原文。自动讨论的 AI 写入须使用本次执行凭据,校验身份、职责、安排、运行代次和租约;自由房间的自动回答须同时携带成员凭证和有效执行编号。失效执行不能改作普通发言,持久成员凭证不能绕过自动讨论的执行检查。自由成员离开后凭证立即失效,请回时换发新凭证;讨论名单删去的成员同样停用。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
所有新房间在同一事务中登记房主及成员凭据。创建者先保存凭据再提交,同一建房编号的重试必须沿用原配置及全部凭据;服务端不返回明文凭据。浏览器在本地保存本房间的私有接入资料,下载成员文件时只导出该成员的 `site`、`room`、`token`。本机启动器另保存自己的私有配置;CLI 建房要求指定私有文件。不要提交这些文件到 Git、上传到房间或把 token 复制进指令。没有凭据登记的历史房间只读,本次不提供自动认领或迁移。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
能访问站点的人仍可以读取房间、列表、附件和原文备份;备份不含凭据或哈希。新建房间和站点级文件夹操作也没有账号鉴权,房间写入保护不是整站访问控制。公开站点不得存放敏感材料;需要私有使用时在服务外配置身份与网络访问控制,并实际验证 CLI 和阅读入口的访问行为。
|
|
12
12
|
|
|
13
13
|
Python 服务默认绑定回环地址;Cloudflare 部署通常对公网可见。根配置为示例资源 ID,部署者负责云账号、HTTPS、访问限制和备份。项目未经过独立安全审计。
|
|
14
14
|
|
package/README.md
CHANGED
|
@@ -63,11 +63,11 @@ codiscuss setup
|
|
|
63
63
|
|
|
64
64
|
1. 安装完成后进入自动打开的网站;以后运行 `codiscuss local open` 打开或复用本机网站,默认地址为 `http://127.0.0.1:8765`。
|
|
65
65
|
2. 点击“新建讨论”,填写**话题名称**和**用户原文**。把问题、背景与顾虑写在原文中,讨论目标、背景和边界由 AI 主持人据此补全。
|
|
66
|
-
3.
|
|
66
|
+
3. 房间用法选择“头脑风暴”,接入方式选择“程序唤醒”,从角色库添加本场专家,并从中指定 AI 主持人;参与区只显示已加入的实例,点击“移出”调整名单。自动 CLI 卡片背面从真实软件目录下拉选择模型,并按模型支持的档位设置思考强度;留空使用 CLI 默认值,App 在原软件设置。分别设置轮次和时长上限,点击数值展开滚轮或直接输入。新建默认 **3 轮 / 不限时长**,按轮次上限或手动停止结束;单次 AI 执行也默认不限时,可按需设置 1–60 分钟上限。
|
|
67
67
|
4. 勾选“创建后立即启动 AI”,复核软件配置。需要修改时展开“调整启动配置”;未配置的软件需选择 CLI / App 类型并补齐路径。项目目录、读写权限和已有会话按本场需要填写。
|
|
68
68
|
5. 点击“创建并启动讨论”。程序完成建房、登记与首次任务交付;在进度看板核对 AI 是否实际进入,并查看保存的发言。
|
|
69
69
|
|
|
70
|
-
只想让 AI 干活时,在第 3
|
|
70
|
+
只想让 AI 干活时,在第 3 步将房间用法改为“自由协作”:选择一位或多位本地 AI,默认由你主持,也可指定 AI 主持人;使用“程序唤醒”时点击“创建并连接”,之后在频道里直接发消息。多位时点名或在正文中 `@` 它们,处理中可随时停止。运行中可在右侧成员栏邀请、请离或请回 AI,指定主持人,或发起关联的头脑风暴。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
|
|
71
71
|
|
|
72
72
|
也可不勾选立即启动,先建房,再从“接入”页点击“启动讨论”。本机网页正常启动无需下载 JSON、手动登记房间或另执行首次启动命令。
|
|
73
73
|
|
|
@@ -109,7 +109,7 @@ codiscuss local connect https://你的站点
|
|
|
109
109
|
|
|
110
110
|
软件失败时先查看暂停原因,登录和权限问题在对应软件中处理。本机网站的已登记房间,可从进度看板 Codex 旁的“…”菜单直接选择“切换 App / CLI”,在弹窗内暂停并保存;软件路径在“接入 → 修正本场软件连接”修改。原会话与讨论进度保留,待服务确认采用后重启;线上连接暂不提供此修正入口。切到 App 后模型和文件权限按 App 设置,切回 CLI 前须让 App 释放原聊天。本场修改不影响后续讨论的默认配置,详见 [配置修正](docs/runner.md#已启动后的配置修正)。
|
|
111
111
|
|
|
112
|
-
**手动接入:**
|
|
112
|
+
**手动接入:** 先选“自由协作”或“头脑风暴”,再选手动接入(手动复制冷启动)。从“接入”页下载各本地成员自己的凭据文件,连同参与指令私下交给对应 AI 会话;建房无需本机接入服务,后续可由软件原生定时或人工触发,无需每轮复制本地 AI 的回答。在线专家只读,由主持人操作浏览器或由用户转交回答,首次确定分工后沿用。详见 [讨论工作流](docs/workflow.md)。
|
|
113
113
|
|
|
114
114
|
## 在线部署
|
|
115
115
|
|
|
@@ -131,7 +131,7 @@ codiscuss local connect https://你的站点
|
|
|
131
131
|
|
|
132
132
|
| 方式 | 由谁推进 | 前提 |
|
|
133
133
|
| --- | --- | --- |
|
|
134
|
-
| 自由协作 | 你或 AI
|
|
134
|
+
| 自由协作 | 你或 AI 主持人点名成员,按成员接入方式处理 | 默认由你主持,可增设一位本地 AI 主持人;自动成员由本机服务唤醒,手动成员自行读取请求,在线成员回答由你代发 |
|
|
135
135
|
| 事件驱动 | 本机服务按网站事件唤醒对应 AI 会话 | 选择“全自动讨论”,主持人与专家均为支持读写的本地 AI(CLI / App) |
|
|
136
136
|
| 软件原生定时 | AI 软件定时检查当前安排 | 选择“手动复制冷启动”,软件具备原生定时能力 |
|
|
137
137
|
| 人工触发 | 用户要求对应会话继续 | 选择“手动复制冷启动”;在线回答需代发 |
|
|
@@ -142,7 +142,7 @@ codiscuss local connect https://你的站点
|
|
|
142
142
|
|
|
143
143
|
讨论结束后,可在左上方频道条目的“更多操作 → 导出对话”下载原始数据、全文及已发送附件原文件,窄屏从频道导航进入。也可用 `discuss-report` 整理带来源的综合报告。备份目前不支持导入恢复,也不下载外链内容。
|
|
144
144
|
|
|
145
|
-
网站目前**没有用户登录和读取权限隔离**;公开部署前请按 [安全说明](.github/SECURITY.md)
|
|
145
|
+
网站目前**没有用户登录和读取权限隔离**;公开部署前请按 [安全说明](.github/SECURITY.md) 配置访问边界。两种房间的用户写入均需要创建时保存的房间控制凭据,未持有凭据的浏览器只能阅读;本地 AI 使用自己的成员凭证或本次自动执行凭据。凭据不能代替软件登录、工具许可或本机启动能力。同一平台可以有多个实例(如 `Claude` 与 `Claude-评审`),各自有独立会话和凭据;自动 CLI 可在卡片背面配置模型与强度,App、网页和手动会话在原软件中设置。讨论安排串行,自由协作的不同成员可并行。在线读取、原生定时和浏览器操作能力取决于参与软件。
|
|
146
146
|
|
|
147
147
|
## 仓库结构
|
|
148
148
|
|
package/backend/free_room.py
CHANGED
|
@@ -14,7 +14,7 @@ import json
|
|
|
14
14
|
import time
|
|
15
15
|
|
|
16
16
|
from .identity import MAX_ROOM_AGENTS, is_ai
|
|
17
|
-
from .runtime import LEASE_SECONDS, begin_write, token_hash
|
|
17
|
+
from .runtime import LEASE_SECONDS, begin_write, require_owner, token_hash
|
|
18
18
|
|
|
19
19
|
FREE_EXECUTION_MINUTES = 20
|
|
20
20
|
RECENT_REQUESTS = 100
|
|
@@ -68,12 +68,14 @@ def member_value(member):
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def member_values(members):
|
|
71
|
-
"""Validate
|
|
71
|
+
"""Validate independent member credentials for a room."""
|
|
72
72
|
if not isinstance(members, list) or not 1 <= len(members) <= MAX_ROOM_AGENTS:
|
|
73
|
-
raise ValueError("
|
|
73
|
+
raise ValueError("房间须有至少 1 位 AI 成员,且不超过成员上限")
|
|
74
74
|
result = [member_value(member) for member in members]
|
|
75
75
|
if len({member["name"] for member in result}) != len(result):
|
|
76
76
|
raise ValueError("成员名称不能重复")
|
|
77
|
+
if len({member["token"] for member in result}) != len(result):
|
|
78
|
+
raise ValueError("每位成员须使用独立凭证")
|
|
77
79
|
return result
|
|
78
80
|
|
|
79
81
|
|
|
@@ -84,21 +86,64 @@ def insert_member(connection, room_id, member, now):
|
|
|
84
86
|
member["access"], member["session_mode"], now))
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
def
|
|
89
|
+
def save_room_access(connection, room_id, control_token, members, settings):
|
|
88
90
|
"""Persist the mode, the owner's control hash and the members in the creating transaction."""
|
|
89
91
|
control = token_hash(control_token)
|
|
90
92
|
if not control:
|
|
91
|
-
raise ValueError("
|
|
92
|
-
|
|
93
|
+
raise ValueError("建房须提供 32–256 字符的房间控制凭据")
|
|
94
|
+
if any(token_hash(member["token"]) == control for member in members):
|
|
95
|
+
raise ValueError("成员凭证不能与房主控制凭据相同")
|
|
96
|
+
connection.execute("INSERT INTO room_modes (room_id,mode,control_hash) VALUES (?,?,?)", (room_id, settings["mode"], control))
|
|
93
97
|
for member in members:
|
|
94
|
-
insert_member(connection, room_id, member,
|
|
98
|
+
insert_member(connection, room_id, member, settings["created_at"])
|
|
99
|
+
connection.execute("UPDATE room_members SET role='host' WHERE room_id=? AND name=?", (room_id, settings["host"]))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def discussion_members(payload, settings):
|
|
103
|
+
"""Validate credentials against the room's declared participants and delivery mode."""
|
|
104
|
+
members = member_values(payload.get("members")) if settings["experts"] else []
|
|
105
|
+
if {member["name"] for member in members} != set(settings["experts"]):
|
|
106
|
+
raise ValueError("成员凭证名单须与参与者名单一致")
|
|
107
|
+
for member in members:
|
|
108
|
+
expected = "online" if settings["experts"][member["name"]] == "online" else (
|
|
109
|
+
"runner" if payload.get("runtime_mode") == "automatic" else "manual")
|
|
110
|
+
if member["access"] != expected:
|
|
111
|
+
raise ValueError("成员接入方式与房间参与能力、任务交付方式不符")
|
|
112
|
+
return members
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sync_discussion_members(connection, room_id, payload, settings):
|
|
116
|
+
"""Keep existing credentials; require fresh credentials for added or changed members."""
|
|
117
|
+
current = {row["name"]: row for row in connection.execute("SELECT * FROM room_members WHERE room_id=? AND status='active'", (room_id,))}
|
|
118
|
+
supplied = {member["name"]: member_value(member) for member in payload.get("members", [])}
|
|
119
|
+
run = connection.execute("SELECT mode FROM discussion_runtime WHERE room_id=?", (room_id,)).fetchone()
|
|
120
|
+
for name, capability in settings["experts"].items():
|
|
121
|
+
access = "online" if capability == "online" else ("runner" if run and run[0] == "automatic" else "manual")
|
|
122
|
+
old = current.get(name)
|
|
123
|
+
if old and (old["access"] == "online") == (access == "online"):
|
|
124
|
+
if name in supplied and token_hash(supplied[name]["token"]) != old["token_hash"]:
|
|
125
|
+
raise FileExistsError("成员凭证与已登记身份不符;请核对原接入文件")
|
|
126
|
+
continue
|
|
127
|
+
if name not in supplied or supplied[name]["access"] != access:
|
|
128
|
+
raise ValueError(f"新增或改变接入方式的成员 {name} 须提供独立的新凭证")
|
|
129
|
+
member = supplied[name]
|
|
130
|
+
hashed = token_hash(member["token"])
|
|
131
|
+
if connection.execute("SELECT 1 FROM room_members WHERE room_id=? AND token_hash=? UNION ALL SELECT 1 FROM room_modes WHERE room_id=? AND control_hash=?", (room_id, hashed, room_id, hashed)).fetchone():
|
|
132
|
+
raise ValueError("新成员凭证不能复用已有凭证")
|
|
133
|
+
connection.execute("UPDATE room_members SET status='removed' WHERE room_id=? AND name=?", (room_id, name))
|
|
134
|
+
insert_member(connection, room_id, member, int(time.time()))
|
|
135
|
+
for name in current.keys() - settings["experts"].keys():
|
|
136
|
+
connection.execute("UPDATE room_members SET status='removed' WHERE room_id=? AND name=?", (room_id, name))
|
|
137
|
+
connection.execute("UPDATE room_members SET role=CASE WHEN name=? THEN 'host' ELSE 'member' END WHERE room_id=?", (settings["host"], room_id))
|
|
95
138
|
|
|
96
139
|
|
|
97
140
|
def sync_participants(connection, room_id):
|
|
98
|
-
"""Mirror
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
"""Mirror member capabilities and the active host into shared room metadata."""
|
|
142
|
+
members = connection.execute("SELECT name,access,role,status FROM room_members WHERE room_id=? AND status<>'removed' ORDER BY id", (room_id,)).fetchall()
|
|
143
|
+
experts = {row["name"]: "online" if row["access"] == "online" else "cli" for row in members}
|
|
144
|
+
host = next((row["name"] for row in members if row["role"] == "host" and row["status"] == "active"), "用户")
|
|
145
|
+
connection.execute("UPDATE topic_settings SET experts=?,host=? WHERE room_id=?",
|
|
146
|
+
(json.dumps(experts, ensure_ascii=False), host, room_id))
|
|
102
147
|
|
|
103
148
|
|
|
104
149
|
def execution_minutes(connection, room_id):
|
|
@@ -163,14 +208,6 @@ def free_view(connection, room_id):
|
|
|
163
208
|
"conflicts": possible_conflicts(connection, room_id, names), "halted_chains": chains, "max_auto_hops": MAX_AUTO_HOPS}
|
|
164
209
|
|
|
165
210
|
|
|
166
|
-
def require_owner(connection, room_id, token):
|
|
167
|
-
"""Accept only the room control credential; declared names never grant ownership."""
|
|
168
|
-
row = connection.execute("SELECT control_hash FROM room_modes WHERE room_id=? AND mode='free'", (room_id,)).fetchone()
|
|
169
|
-
hashed = token_hash(token)
|
|
170
|
-
if not row or not hashed or not hmac.compare_digest(hashed, row[0]):
|
|
171
|
-
raise PermissionError("需要本房间的控制凭据;没有凭据只能阅读")
|
|
172
|
-
|
|
173
|
-
|
|
174
211
|
def require_member(connection, room_id, token, name=None):
|
|
175
212
|
"""Resolve an active member strictly from its own credential."""
|
|
176
213
|
hashed = token_hash(token)
|
|
@@ -533,13 +570,26 @@ def add_member(connection, room_id, token, payload, now):
|
|
|
533
570
|
require_owner(connection, room_id, token)
|
|
534
571
|
begin_write(connection)
|
|
535
572
|
member = member_value(payload)
|
|
536
|
-
|
|
573
|
+
existing = connection.execute("SELECT * FROM room_members WHERE room_id=? AND name=? AND status<>'removed'", (room_id, member["name"])).fetchone()
|
|
574
|
+
if existing and existing["status"] == "active" and existing["token_hash"] == token_hash(member["token"]) and all(
|
|
575
|
+
existing[key] == member[key] for key in ("adapter", "access", "session_mode")):
|
|
576
|
+
return free_view(connection, room_id)
|
|
577
|
+
if existing:
|
|
537
578
|
raise FileExistsError(f"{member['name']} 已是本房间成员;离开的成员请用请回")
|
|
579
|
+
require_fresh_member_token(connection, room_id, member["token"])
|
|
538
580
|
insert_member(connection, room_id, member, now)
|
|
539
581
|
sync_participants(connection, room_id)
|
|
540
582
|
return free_view(connection, room_id)
|
|
541
583
|
|
|
542
584
|
|
|
585
|
+
def require_fresh_member_token(connection, room_id, token):
|
|
586
|
+
"""Prevent owner or another member's credential from being reused for a new identity."""
|
|
587
|
+
hashed = token_hash(token)
|
|
588
|
+
if not hashed or connection.execute("SELECT 1 FROM room_members WHERE room_id=? AND token_hash=? UNION ALL SELECT 1 FROM room_modes WHERE room_id=? AND control_hash=?",
|
|
589
|
+
(room_id, hashed, room_id, hashed)).fetchone():
|
|
590
|
+
raise ValueError("须提供独立的新成员凭证")
|
|
591
|
+
|
|
592
|
+
|
|
543
593
|
def member_action(connection, room_id, member_id, token, payload):
|
|
544
594
|
"""Owner sends a member away, brings it back with a new credential, or removes it.
|
|
545
595
|
|
|
@@ -554,8 +604,11 @@ def member_action(connection, room_id, member_id, token, payload):
|
|
|
554
604
|
connection.execute("UPDATE room_members SET status=? WHERE id=?", ("away" if action == "away" else "removed", member_id))
|
|
555
605
|
elif action == "active":
|
|
556
606
|
hashed = token_hash(payload.get("token"))
|
|
607
|
+
if member["status"] == "active" and hashed and hmac.compare_digest(hashed, member["token_hash"]):
|
|
608
|
+
return free_view(connection, room_id)
|
|
557
609
|
if member["status"] != "away" or not hashed or hmac.compare_digest(hashed, member["token_hash"]):
|
|
558
610
|
raise ValueError("只有离开的成员可以请回,且须提供新的成员凭证")
|
|
611
|
+
require_fresh_member_token(connection, room_id, payload.get("token"))
|
|
559
612
|
connection.execute("UPDATE room_members SET status='active',token_hash=? WHERE id=?", (hashed, member_id))
|
|
560
613
|
elif action == "role":
|
|
561
614
|
role = payload.get("role")
|
package/backend/runtime.py
CHANGED
|
@@ -144,18 +144,65 @@ def require_control(connection, room_id, token):
|
|
|
144
144
|
return row
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
def require_owner(connection, room_id, token):
|
|
148
|
+
"""Require the persistent room owner credential, independent of execution mode."""
|
|
149
|
+
hashed = token_hash(token)
|
|
150
|
+
row = connection.execute("SELECT * FROM room_modes WHERE room_id=? AND control_hash=?",
|
|
151
|
+
(room_id, hashed)).fetchone() if hashed else None
|
|
152
|
+
if not row:
|
|
153
|
+
raise PermissionError("需要本房间的控制凭据;没有凭据只能阅读")
|
|
154
|
+
return row
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def write_actor(operation, payload):
|
|
158
|
+
"""Use the same message attribution defaults as persisted message details."""
|
|
159
|
+
if operation == "messages":
|
|
160
|
+
return payload.get("submitted_by", payload.get("author") if payload.get("source") == "cli" else "用户")
|
|
161
|
+
return payload.get("actor", "用户")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def authorize_participant(connection, room_id, identity, operation, payload):
|
|
165
|
+
"""Limit authenticated participants to their identity and assigned host duties."""
|
|
166
|
+
actor = write_actor(operation, payload)
|
|
167
|
+
if actor != identity["name"]:
|
|
168
|
+
raise PermissionError("凭据与实际参与身份不符")
|
|
169
|
+
if operation == "messages":
|
|
170
|
+
author = payload.get("author")
|
|
171
|
+
online = connection.execute("SELECT 1 FROM room_members WHERE room_id=? AND name=? AND access='online' AND status='active'",
|
|
172
|
+
(room_id, author)).fetchone()
|
|
173
|
+
if author != actor and not (online and identity["role"] in ("owner", "host")):
|
|
174
|
+
raise PermissionError("只能提交本人观点,或由房主、主持人代发在线成员原文")
|
|
175
|
+
elif operation == "floor":
|
|
176
|
+
speaker = payload.get("speaker", actor)
|
|
177
|
+
online = connection.execute("SELECT 1 FROM room_members WHERE room_id=? AND name=? AND access='online' AND status='active'",
|
|
178
|
+
(room_id, speaker)).fetchone()
|
|
179
|
+
if identity["role"] != "owner" and speaker != actor and not (identity["role"] == "host" and online):
|
|
180
|
+
raise PermissionError("只能操作本人发言权")
|
|
181
|
+
elif operation != "attachments" and identity["role"] != "owner":
|
|
182
|
+
if identity["role"] != "host" or operation not in ("topic", "state", "turns", "limits"):
|
|
183
|
+
raise PermissionError("本成员没有此管理权限")
|
|
184
|
+
if operation == "topic":
|
|
185
|
+
if any(field in payload for field in ("host", "experts", "members")):
|
|
186
|
+
raise PermissionError("只有房主可以修改成员和主持人")
|
|
187
|
+
|
|
188
|
+
|
|
147
189
|
def authorize_write(connection, room_id, token, operation, payload):
|
|
148
190
|
"""Check a capability under the same writer lock as the following mutation."""
|
|
149
191
|
begin_write(connection)
|
|
150
192
|
run = connection.execute("SELECT * FROM discussion_runtime WHERE room_id=?", (room_id,)).fetchone()
|
|
151
|
-
if not run:
|
|
152
|
-
return
|
|
153
193
|
hashed = token_hash(token)
|
|
154
|
-
|
|
194
|
+
owner = connection.execute("SELECT 1 FROM room_modes WHERE room_id=? AND control_hash=?", (room_id, hashed)).fetchone() if hashed else None
|
|
195
|
+
if owner:
|
|
196
|
+
authorize_participant(connection, room_id, {"name": "用户", "role": "owner"}, operation, payload)
|
|
155
197
|
return
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
member = connection.execute("SELECT * FROM room_members WHERE room_id=? AND token_hash=? AND status='active' AND access<>'online'",
|
|
199
|
+
(room_id, hashed)).fetchone() if hashed else None
|
|
200
|
+
if member and (not run or run["mode"] == "manual"):
|
|
201
|
+
authorize_participant(connection, room_id, member, operation, payload)
|
|
158
202
|
return
|
|
203
|
+
if not run:
|
|
204
|
+
raise PermissionError("需要房主或本成员的有效凭据;没有凭据只能阅读")
|
|
205
|
+
actor = write_actor(operation, payload)
|
|
159
206
|
work_seconds = execution_seconds(connection, room_id)
|
|
160
207
|
event = connection.execute("""SELECT * FROM discussion_events WHERE room_id=? AND token_hash=?
|
|
161
208
|
AND run_id=? AND generation=? AND status='started' AND lease_until>?
|
|
@@ -165,6 +212,7 @@ def authorize_write(connection, room_id, token, operation, payload):
|
|
|
165
212
|
raise PermissionError("执行资格无效、已结束或已到期;请重新读取运行状态")
|
|
166
213
|
if actor != event["participant"]:
|
|
167
214
|
raise PermissionError("执行凭据与实际参与身份不符")
|
|
215
|
+
authorize_participant(connection, room_id, {"name": event["participant"], "role": event["role"]}, operation, payload)
|
|
168
216
|
if operation == "messages":
|
|
169
217
|
if payload.get("author") != actor:
|
|
170
218
|
raise PermissionError("自动执行不能代发其他参与者")
|
|
@@ -182,6 +230,7 @@ def authorize_write(connection, room_id, token, operation, payload):
|
|
|
182
230
|
def configure_runtime(connection, room_id, token, payload):
|
|
183
231
|
"""Apply one versioned, explicit mode or lifecycle action."""
|
|
184
232
|
begin_write(connection)
|
|
233
|
+
require_owner(connection, room_id, token)
|
|
185
234
|
current = runtime_view(connection, room_id)
|
|
186
235
|
if payload.get("revision") != current["revision"]:
|
|
187
236
|
raise FileExistsError("运行状态已变化,请重新读取 revision")
|
|
@@ -210,6 +259,8 @@ def configure_runtime(connection, room_id, token, payload):
|
|
|
210
259
|
if action in ("pause", "stop"):
|
|
211
260
|
connection.execute("UPDATE topic_settings SET phase='paused' WHERE room_id=? AND phase<>'concluded'", (room_id,))
|
|
212
261
|
connection.execute("UPDATE discussion_runtime SET status=? WHERE room_id=?", (status, room_id))
|
|
262
|
+
connection.execute("UPDATE room_members SET access=? WHERE room_id=? AND access<>'online'",
|
|
263
|
+
("runner" if mode == "automatic" else "manual", room_id))
|
|
213
264
|
return runtime_view(connection, room_id)
|
|
214
265
|
|
|
215
266
|
|
package/backend/schema.sql
CHANGED
|
@@ -498,7 +498,7 @@ BEGIN
|
|
|
498
498
|
UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
|
|
499
499
|
WHERE room_id=NEW.room_id AND paused_at>0;
|
|
500
500
|
END;
|
|
501
|
-
--
|
|
501
|
+
-- Both room kinds register members; free rooms additionally persist requests and executions.
|
|
502
502
|
CREATE TABLE IF NOT EXISTS room_members (
|
|
503
503
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
504
504
|
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
@@ -513,6 +513,7 @@ CREATE TABLE IF NOT EXISTS room_members (
|
|
|
513
513
|
session_mode TEXT NOT NULL DEFAULT 'new'
|
|
514
514
|
);
|
|
515
515
|
CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
|
|
516
|
+
CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
|
|
516
517
|
CREATE TABLE IF NOT EXISTS member_executions (
|
|
517
518
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
518
519
|
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
package/backend/server.py
CHANGED
|
@@ -21,7 +21,7 @@ from urllib.parse import parse_qs, quote, urlsplit
|
|
|
21
21
|
|
|
22
22
|
from .free_room import (add_member, after_free_message, authorize_free_write, check_answer, check_proxy, execution_action, free_view,
|
|
23
23
|
inbox, interrupt_member, member_action, member_values, migrate_free_tables, proxy_member, reserve_execution,
|
|
24
|
-
room_mode,
|
|
24
|
+
discussion_members, room_mode, save_room_access, sync_discussion_members, view_execution)
|
|
25
25
|
from .identity import MAX_ROOM_AGENTS, PARTICIPANTS, is_ai, is_participant
|
|
26
26
|
from .runtime import (authorize_write, begin_write, configure_runtime, event_action,
|
|
27
27
|
LIMIT_DEFAULTS, LIMITS_SQL, limit_values, limits_view, require_within_limits,
|
|
@@ -76,7 +76,8 @@ ROOM_BACKUP_QUERIES = {
|
|
|
76
76
|
**{table: f"SELECT * FROM {table} WHERE message_id IN (SELECT id FROM messages WHERE room_id=?) ORDER BY message_id"
|
|
77
77
|
for table in ("message_details", "message_summaries", "message_timings")},
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
ROOM_BACKUP_QUERIES["room_creations"] = "SELECT submission_id,room_id FROM room_creations WHERE room_id=?"
|
|
80
|
+
# Shared membership plus free-room requests; credential hashes are never exported.
|
|
80
81
|
FREE_BACKUP_QUERIES = {
|
|
81
82
|
"room_modes": "SELECT room_id,mode FROM room_modes WHERE room_id=?",
|
|
82
83
|
"room_members": "SELECT id,room_id,name,adapter,role,status,read_until,created_at FROM room_members WHERE room_id=? ORDER BY id",
|
|
@@ -102,7 +103,7 @@ def room_backup(connection, room_id):
|
|
|
102
103
|
A versioned backup containing original rows and their relationships.
|
|
103
104
|
"""
|
|
104
105
|
connection.execute("BEGIN")
|
|
105
|
-
queries = {**ROOM_BACKUP_QUERIES, **
|
|
106
|
+
queries = {**ROOM_BACKUP_QUERIES, **FREE_BACKUP_QUERIES}
|
|
106
107
|
tables = {table: [dict(row) for row in connection.execute(query, (room_id,))]
|
|
107
108
|
for table, query in queries.items()}
|
|
108
109
|
if not tables["rooms"]:
|
|
@@ -195,6 +196,8 @@ def require_room(connection, room_id):
|
|
|
195
196
|
room["runtime"] = runtime_view(connection, room_id)
|
|
196
197
|
room["limits"] = limits_view(connection.execute(LIMITS_SQL, (room_id,)).fetchone(), room)
|
|
197
198
|
room["mode"] = room_mode(connection, room_id)
|
|
199
|
+
room["members"] = [dict(row) for row in connection.execute(
|
|
200
|
+
"SELECT id,name,adapter,role,status,access,session_mode FROM room_members WHERE room_id=? AND status<>'removed' ORDER BY id", (room_id,))]
|
|
198
201
|
parent = connection.execute("""SELECT rooms.id,rooms.title FROM room_links JOIN rooms ON rooms.id=room_links.parent_id
|
|
199
202
|
WHERE room_links.room_id=?""", (room_id,)).fetchone()
|
|
200
203
|
room["parent_room"] = dict(parent) if parent else None
|
|
@@ -380,6 +383,7 @@ def update_topic(connection, room_id, payload):
|
|
|
380
383
|
if settings["phase"] == "concluded" or turn["expert"] not in settings["experts"]:
|
|
381
384
|
raise ValueError("请先完成或取消当前发言安排")
|
|
382
385
|
connection.execute("UPDATE rooms SET title=?,goal=? WHERE id=?", (settings["title"], settings["goal"], room_id))
|
|
386
|
+
sync_discussion_members(connection, room_id, payload, settings)
|
|
383
387
|
save_topic_settings(connection, room_id, settings)
|
|
384
388
|
return require_room(connection, room_id)
|
|
385
389
|
|
|
@@ -547,17 +551,19 @@ def creation_request(settings):
|
|
|
547
551
|
|
|
548
552
|
|
|
549
553
|
def create_room(connection, payload, control_token=""):
|
|
550
|
-
"""Create
|
|
554
|
+
"""Create a credential-protected room, binding retries to the original credentials."""
|
|
551
555
|
if payload.get("mode", "brainstorm") not in ("free", "brainstorm"):
|
|
552
556
|
raise ValueError("房间模式须为 free 或 brainstorm")
|
|
553
557
|
free = payload.get("mode") == "free"
|
|
554
558
|
members = member_values(payload.get("members")) if free else []
|
|
555
559
|
if free:
|
|
556
|
-
payload = {**payload, "
|
|
560
|
+
payload = {**payload, "phase": "discussing", "experts": {member["name"]: "online" if member["access"] == "online" else "cli" for member in members}}
|
|
557
561
|
settings = topic_values(payload)
|
|
562
|
+
if not free:
|
|
563
|
+
members = discussion_members(payload, settings)
|
|
558
564
|
automatic = payload.get("runtime_mode") == "automatic"
|
|
559
|
-
if payload.get("runtime_mode", "manual") not in ("manual", "automatic") or
|
|
560
|
-
raise ValueError("
|
|
565
|
+
if payload.get("runtime_mode", "manual") not in ("manual", "automatic") or not token_hash(control_token):
|
|
566
|
+
raise ValueError("建房须提供有效的运行模式和房主控制凭据")
|
|
561
567
|
if free and (automatic or not token_hash(control_token)):
|
|
562
568
|
raise ValueError("自由房间须提供房间控制凭据,且不使用讨论运行模式")
|
|
563
569
|
parent_id = payload.get("parent_room_id")
|
|
@@ -568,8 +574,10 @@ def create_room(connection, payload, control_token=""):
|
|
|
568
574
|
if folder_id is not None and not isinstance(folder_id, str):
|
|
569
575
|
raise ValueError("文件夹编号必须是文本或 null")
|
|
570
576
|
request_text = creation_request(settings) + (":automatic" if automatic else "")
|
|
577
|
+
request_text += ":access:" + token_hash(control_token) + ":" + json.dumps(
|
|
578
|
+
[[member["name"], member["adapter"], member["access"], member["session_mode"], token_hash(member["token"])] for member in members], ensure_ascii=False)
|
|
571
579
|
if free:
|
|
572
|
-
request_text += ":free
|
|
580
|
+
request_text += ":free"
|
|
573
581
|
if parent_id is not None:
|
|
574
582
|
request_text += ":parent:" + parent_id
|
|
575
583
|
if folder_id is not None:
|
|
@@ -589,8 +597,8 @@ def create_room(connection, payload, control_token=""):
|
|
|
589
597
|
if identifier:
|
|
590
598
|
connection.execute("INSERT INTO room_creations (submission_id,room_id,request_text) VALUES (?,?,?)", (identifier, room_id, request_text))
|
|
591
599
|
save_topic_settings(connection, room_id, settings)
|
|
592
|
-
|
|
593
|
-
|
|
600
|
+
save_room_access(connection, room_id, control_token, members,
|
|
601
|
+
{"mode": "free" if free else "brainstorm", "host": settings["host"], "created_at": utc_now()})
|
|
594
602
|
if parent_id is not None:
|
|
595
603
|
connection.execute("INSERT INTO room_links (room_id,parent_id,created_at) VALUES (?,?,?)", (room_id, parent_id, utc_now()))
|
|
596
604
|
if automatic:
|
|
@@ -1169,15 +1177,27 @@ def turn_text(turn, room):
|
|
|
1169
1177
|
def protocol_text(room):
|
|
1170
1178
|
"""Describe explicit CLI and host operations separately from topic content."""
|
|
1171
1179
|
base = f"/api/rooms/{room['id']}"
|
|
1180
|
+
if room["mode"] == "free":
|
|
1181
|
+
return "\n\n".join([
|
|
1182
|
+
"# 自由协作参与协议", f"当前主持人:{room['host']}。没有讨论发言权、正式安排或轮次。",
|
|
1183
|
+
"先用 room view 读取成员、职责和 free.requests,再按需读取 message list / message view 原文;通知只给对象、问题和入口。",
|
|
1184
|
+
"用户写入和成员管理须房主控制凭据;本地成员写入须本人的 --access-file,不能借用用户或其他成员身份。凭据文件私下交付,不公开 token。",
|
|
1185
|
+
"runner 成员等待本机接入程序交付有效任务,不另建定时。用 message post --as 本人 --kind chat --execution 本次编号 --access-file FILE 回答;不执行 execution enter。执行失效后保留草稿,不能去掉编号改作普通发言。",
|
|
1186
|
+
"manual 成员可由人工或软件原生定时触发,用本人成员文件自行提交。回答带 --request 请求编号(可重复)关闭自己的请求,不带则为普通发言。不得编造 execution 编号。",
|
|
1187
|
+
"online 成员只读,在原聊天回答,由用户以房主凭据代发原文;作者仍是在线成员、提交者为用户,不能用成员凭证自行写入。复制说明不表示已送达或接入。",
|
|
1188
|
+
"获授 host 职责的本地成员可在回答中用 --to 点名其他成员,不能点名自己或扩大权限;route 表示分配任务,handoff 表示派出的成员已结束。链上 AI 请求受房间预算限制,到限等待用户。",
|
|
1189
|
+
f"附件使用 attachment upload/download;上传使用本人成员文件,消息带 attachment_ids。原始接口为 {base}/attachments 与 {base}/messages,自动成员在回答完成前上传。",
|
|
1190
|
+
"每次新内容用新 submission_id,同次重试沿用编号、原文、凭据和执行/请求关联;失败先核对服务端结果,不换身份或编号绕过。用户打断、成员离开或移除后停止本次工作。",
|
|
1191
|
+
])
|
|
1172
1192
|
return "\n\n".join([
|
|
1173
1193
|
"# 本地 CLI 读写与主持协议", f"当前主持人:{room['host']}。以下为留言板接口,不是模型服务 API。",
|
|
1174
1194
|
"写入使用 POST,通常为 Content-Type: application/json;附件上传使用 application/octet-stream 原始字节。主持操作填写 actor 为当前主持人或用户;这些是声明身份,不是认证凭据。",
|
|
1175
1195
|
f"附件:POST {base}/attachments?actor=身份&file_name=文件名&submission_id=新编号,原文件最多 20 MiB;GET 同路径默认列已发送附件,加 submission_id 精确核对上传。GET {base}/attachments/ID 读元数据,/ID/content 下载原文件。消息可带 attachment_ids(最多 10 个有序编号);只关联同房间、同提交者的未使用附件,可只有附件无正文。自动任务须在回答完成前上传;消息查询包含附件,按需下载后由当前软件读取,不自动解析。",
|
|
1176
1196
|
"讨论上限:room.limits 返回轮次、总时长、单次执行、到限方式、收束预留和实际进度。准备/暂停时通过 topic 的 limits 对象修改;0 表示不预设轮次/总时长/单次执行时限。POST /api/rooms/ROOM/limits(actor)或 room limits ROOM --check 检查到限处理;附 force:true 可在进行中提前进入收束,由主持人在收束预留时间内总结。到限停止新专家安排与观点;总结由真实主持人完成,预留计入总时长;暂停期间不计时也不消耗总时长。",
|
|
1177
|
-
"运行方式:room.runtime 显示 manual/automatic
|
|
1197
|
+
"运行方式:room.runtime 显示 manual/automatic、状态及真实执行回执。所有房间写入须带 X-Discussion-Token,CLI 用 --access-file 读取私有文件。用户及运行控制使用房主凭据,手动本地成员使用自己的成员凭证,不能相互冒充;在线成员只读,由房主或讨论主持人代发。自动讨论先 execution enter,创建安排或保存关联回答即完成本步,不创建原生定时任务;持久成员凭证不能替代自动执行,暂停、停止、换代、租约到期后的旧执行失效。run view/start/pause/stop/configure 字段查 CLI schema。",
|
|
1178
1198
|
f"发言权:GET {base}/floor 返回 holder、next_speaker、revision;POST 同路径,字段 actor、speaker(默认 actor)、action=take/return、revision(刚读到的整数版本)。仅无人持有且 next_speaker 为本人时可领取;持有者可主动归还,下一位回到主持人。版本变化返回 409,先读回核对。主持人仅可代操作 online 专家,cli 专家自行操作;用户可协助。",
|
|
1179
1199
|
"新房间由主持人持有发言权。创建安排须主持人持有,保存成功即归还并点名该专家;AI 的 opinion、moderation、receipt 都须实际作者持有,代发不豁免,opinion 必须关联有效 turn_id。消息保存成功自动归还,下一位为主持人。失败不归还,同编号重试不再次交接。用户本人发言不受发言权限制,也不改变发言权;@ 不改变顺序。维护状态、阶段及通知回执不占用发言权。取消安排须主持人持有,用户可直接取消失效安排;取消后归还给主持人领取。",
|
|
1180
|
-
'创建话题:POST /api/rooms
|
|
1200
|
+
'创建话题:POST /api/rooms,携带房主控制凭据;字段 title、goal、user_request、background、boundaries、host、experts、members、submission_id(新 UUID)。members 与 experts 名单一致,各项含 name、独立 token、access(手动本地 manual、自动 runner、在线 online)。AI 主持人必须为本地可写成员。CLI room create --access-file 私有房主文件 在请求前保存凭据,确认后返回各本地成员文件路径;同编号重试沿用原资料和全部凭据。名单、主持职责变更仅房主可做。',
|
|
1181
1201
|
f"查询提交:GET /api/rooms?submission_id=编号 查建房结果;GET {base}/messages?submission_id=编号 或 {base}/turns?submission_id=编号 查消息或安排。查询成功且列表为空表示尚未查到,读取失败不表示未写入。",
|
|
1182
1202
|
f"增量读取:GET {base}/messages?after=已读消息编号,每次最多 20 条,按 next_after 续读,has_more 表示还有一批;可加 kind=opinion/moderation/receipt、author、mention、q。after 不与翻页混用,过滤后的游标只代表该查询范围。",
|
|
1183
1203
|
f"维护话题:POST {base}/topic;actor 加需要修改的上述字段,另可更新 phase(preparing/initial/discussing/reviewing/paused/concluded)、focus。收束前先完成或取消当前安排。暂停保留安排;恢复阶段后再继续。",
|
|
@@ -1400,7 +1420,7 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1400
1420
|
"""Forward authenticated browser requests to the shared Node entrypoint and pin their website."""
|
|
1401
1421
|
site = self.local_request_site(method)
|
|
1402
1422
|
route = parts[2:]
|
|
1403
|
-
if method == "OPTIONS" and route in (["settings"], ["launch"], ["room-models"], ["room-member"]):
|
|
1423
|
+
if method == "OPTIONS" and route in (["settings"], ["launch"], ["room-models"], ["room-member"], ["models"]):
|
|
1404
1424
|
return self.respond({"ok": True})
|
|
1405
1425
|
if route == ["settings"] and method in ("GET", "POST"):
|
|
1406
1426
|
command = "settings"
|
|
@@ -1410,6 +1430,8 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1410
1430
|
command = "room-config"
|
|
1411
1431
|
elif route == ["room-models"] and method == "POST":
|
|
1412
1432
|
command = "room-models"
|
|
1433
|
+
elif route == ["models"] and method == "POST":
|
|
1434
|
+
command = "models"
|
|
1413
1435
|
elif route == ["room-member"] and method == "POST":
|
|
1414
1436
|
command = "room-member"
|
|
1415
1437
|
else:
|