ai_co_discuss 0.2.20 → 0.2.21
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 +3 -3
- package/backend/free_room.py +2 -2
- package/backend/server.py +5 -5
- package/docs/architecture.md +14 -14
- package/package.json +1 -1
- package/web/backup.js +5 -5
- package/web/modern.css +1 -1
- package/web/modern.js +41 -41
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
自由协作房间里,你点名哪个 AI,它就在自己的原会话中处理并回复,多位可以同时进行;也可以指定一位 AI 主持人替你分配任务。讨论房间由主持人安排发言与收束。本地 AI 通过 CLI 读写,在线网页 AI 在原聊天回答,由你或主持人代发。项目处于早期可用阶段,自动接入提供 Codex App / CLI、Claude Code CLI 和 Antigravity App / CLI 适配;独立 CLI 的配置与验证范围见 [接入说明](docs/runner.md#独立-cli-接入)。
|
|
9
9
|
|
|
10
|
-

|
|
11
11
|
|
|
12
12
|
[便捷安装](#快速开始) · [本机网页](#从本机网页新建讨论) · [线上网页](#从线上网页新建讨论) · [AI 对话新建讨论](#从-ai-对话新建讨论) · [在线部署](#在线部署) · [文档](#文档)
|
|
13
13
|
|
|
@@ -71,7 +71,7 @@ codiscuss setup
|
|
|
71
71
|
|
|
72
72
|
讨论进行中可在右侧进度看板邀请新专家(沿用本机软件配置);Codex、Claude 可填写已有会话 ID,请一个带着自己上下文的原会话加入,它工作时不要在原窗口继续操作。也可从成员的“…”菜单让其暂离、请回或移除。暂离保留位置与会话,不参与安排;这些操作不暂停讨论,主持人会被唤醒重新安排。主持人不能暂离,需先更换。
|
|
73
73
|
|
|
74
|
-
只想让 AI 干活时,在第 3 步将房间用法改为“自由协作”,只需填写话题名称,任务在对话中随时补充:选择一位或多位 AI,并在每张卡片背面选择接入方式:本机唤醒(App / CLI / API)、手动接入(凭据交给你自己的会话)或网页代发(回答由你转发)。你是需求方,由一位 AI 主持(默认第一位,可点皇冠更换;网页 AI
|
|
74
|
+
只想让 AI 干活时,在第 3 步将房间用法改为“自由协作”,只需填写话题名称,任务在对话中随时补充:选择一位或多位 AI,并在每张卡片背面选择接入方式:本机唤醒(App / CLI / API)、手动接入(凭据交给你自己的会话)或网页代发(回答由你转发)。你是需求方,由一位 AI 主持(默认第一位,可点皇冠更换;网页 AI 不能主持);有本机唤醒的成员时点击“创建并连接”,之后在房间里直接发消息。`@` 到的成员必须回应;没 `@` 的消息交给主持人分派,其他成员只在被你或主持人点名时发言。处理中可随时停止。运行中可在右侧成员栏邀请、请离或请回 AI,指定主持人,或发起关联的头脑风暴。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
|
|
75
75
|
|
|
76
76
|
**通过 API 接入**:添加 Codex(OpenAI)、Claude、Gemini、DeepSeek、Qwen 或 Grok,翻开卡片选择 API,填写并保存 API Key,选择或输入模型 ID。默认提供官方 Base URL,也可填写 OpenRouter 等兼容服务的地址并选择对应请求协议。Grok 始终显示,无需安装 Cursor 或 Grok Bot。API 可以担任主持人,与 CLI / App 专家混用,也可进入自由协作房间。API 与网页专家不访问项目文件,只有 CLI 专家可选择只读或写入;API 仍可通过房间工具保存讨论发言和主持安排。调用由本机服务发出,费用按所选 API 账号计费;密钥仅保存于本机。模型与配置说明见 [API 接入](docs/runner.md#api-接入)。
|
|
77
77
|
|
|
@@ -146,7 +146,7 @@ codiscuss local connect https://你的站点
|
|
|
146
146
|
|
|
147
147
|
消息支持选择文件、拖拽或粘贴截图,每条最多 10 个附件、单个最多 20 MiB。PNG、JPEG、GIF、WebP 可放大查看,PDF 可在站内翻页、缩放,其他文件提供下载(不在线解析 Office 文件)。本地 AI 可通过 CLI 按需下载原文件;实际能否理解图片或文档取决于参与软件,上传不代表 AI 已读取。
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
讨论结束后,可在左上方房间条目的“更多操作 → 导出对话”下载原始数据、全文及已发送附件原文件,窄屏从房间导航进入。房间目录菜单可“导出全部房间”,文件夹菜单可“导出文件夹”。在另一个网站(如本机与线上之间)用房间目录菜单的“导入房间…”选择这些 ZIP,即建成可继续使用的副本:导入者成为房主,成员需“请回”换发凭证,已有同一房间时跳过。两边数据不自动同步,导入是一次性复制。也可用 `discuss-report` 整理带来源的综合报告。备份不下载外链内容。
|
|
150
150
|
|
|
151
151
|
网站目前**没有用户登录和读取权限隔离**;公开部署前请按 [安全说明](.github/SECURITY.md) 配置访问边界。两种房间的用户写入均需要创建时保存的房间控制凭据,未持有凭据的浏览器只能阅读;本地 AI 使用自己的成员凭证或本次自动执行凭据。凭据不能代替软件登录、工具许可或本机启动能力。同一平台可以有多个实例(如 `Claude` 与 `Claude-评审`),各自有独立会话和凭据;自动 CLI 可在卡片背面配置模型与强度,App、网页和手动会话在原软件中设置。讨论安排串行,自由协作的不同成员可并行。在线读取、原生定时和浏览器操作能力取决于参与软件。
|
|
152
152
|
|
package/backend/free_room.py
CHANGED
|
@@ -408,7 +408,7 @@ def convert_suggestion(connection, room_id, token, payload):
|
|
|
408
408
|
if target["status"] != "active":
|
|
409
409
|
raise FileExistsError("该成员已离开,不能转为请求")
|
|
410
410
|
if connection.execute("SELECT 1 FROM room_directory WHERE room_id=? AND archived=1", (room_id,)).fetchone():
|
|
411
|
-
raise ValueError("
|
|
411
|
+
raise ValueError("房间已存档;恢复后才能继续协作")
|
|
412
412
|
previous = connection.execute("SELECT * FROM member_requests WHERE message_id=? AND member_id=?", (message_id, member_id)).fetchone()
|
|
413
413
|
if previous:
|
|
414
414
|
return {"request": dict(previous), "duplicate": True}
|
|
@@ -566,7 +566,7 @@ def reserve_execution(connection, room_id, token, payload):
|
|
|
566
566
|
return execution_detail(connection, previous)
|
|
567
567
|
expire_executions(connection, room_id)
|
|
568
568
|
if connection.execute("SELECT archived FROM room_directory WHERE room_id=?", (room_id,)).fetchone()[0]:
|
|
569
|
-
raise FileExistsError("
|
|
569
|
+
raise FileExistsError("房间已存档,不再投递")
|
|
570
570
|
member = connection.execute("SELECT * FROM room_members WHERE room_id=? AND id=? AND status='active' AND access='runner'",
|
|
571
571
|
(room_id, payload.get("member_id"))).fetchone()
|
|
572
572
|
if not member:
|
package/backend/server.py
CHANGED
|
@@ -384,7 +384,7 @@ def update_topic(connection, room_id, payload):
|
|
|
384
384
|
require_host(room, payload)
|
|
385
385
|
settings = topic_values(payload, room)
|
|
386
386
|
if room["archived"] and settings["phase"] not in ("paused", "concluded"):
|
|
387
|
-
raise ValueError("
|
|
387
|
+
raise ValueError("请先恢复已存档房间,再调整讨论阶段")
|
|
388
388
|
turn = room["current_turn"]
|
|
389
389
|
if turn and turn["status"] in ("pending", "notified"):
|
|
390
390
|
if settings["phase"] == "concluded" or turn["expert"] not in settings["experts"]:
|
|
@@ -510,7 +510,7 @@ def manage_room(connection, room_id, action, payload):
|
|
|
510
510
|
rooms = [item["id"] for item in list_rooms(connection)["rooms"] if item["id"] != room_id]
|
|
511
511
|
before = payload.get("before_id")
|
|
512
512
|
if before is not None and before not in rooms:
|
|
513
|
-
raise ValueError("
|
|
513
|
+
raise ValueError("目标房间不存在或与当前房间相同")
|
|
514
514
|
rooms.insert(rooms.index(before) if before else len(rooms), room_id)
|
|
515
515
|
connection.executemany("UPDATE room_directory SET position=? WHERE room_id=?", enumerate(rooms, 1))
|
|
516
516
|
elif action == "folder":
|
|
@@ -526,7 +526,7 @@ def manage_room(connection, room_id, action, payload):
|
|
|
526
526
|
connection.execute("UPDATE room_directory SET archived=? WHERE room_id=?", (int(action == "archive"), room_id))
|
|
527
527
|
elif action == "delete":
|
|
528
528
|
if payload.get("confirm_title") != room["title"]:
|
|
529
|
-
raise ValueError("
|
|
529
|
+
raise ValueError("请输入完整房间名称确认删除")
|
|
530
530
|
delete_room_records(connection, room_id)
|
|
531
531
|
return {"deleted": True, "id": room_id}
|
|
532
532
|
else:
|
|
@@ -833,7 +833,7 @@ def append_free_message(connection, room, payload, actor):
|
|
|
833
833
|
raise FileExistsError("这个提交编号已被另一条内容使用,请为新发言生成新编号")
|
|
834
834
|
return {"message": previous, "duplicate": True}
|
|
835
835
|
if room["archived"]:
|
|
836
|
-
raise ValueError("
|
|
836
|
+
raise ValueError("房间已存档;恢复后才能继续协作")
|
|
837
837
|
check_message_references(connection, room_id, expected)
|
|
838
838
|
check_message_attachments(connection, room_id, submitted_by, identifiers)
|
|
839
839
|
if proxy:
|
|
@@ -1146,7 +1146,7 @@ def room_text(connection, room_id, params):
|
|
|
1146
1146
|
if room["phase"] == "initial":
|
|
1147
1147
|
content.append("本阶段先独立形成详细观点:仅阅读目标、背景、边界和自己的已有讨论;暂不阅读他人观点或主持总结。提交后再交叉阅读。这是参与约定,不是权限隔离。")
|
|
1148
1148
|
if room["archived"]:
|
|
1149
|
-
content.append("
|
|
1149
|
+
content.append("房间已存档,记录仍可查阅;先恢复房间,再由用户明确恢复讨论阶段。")
|
|
1150
1150
|
if room["phase"] in ("paused", "concluded"):
|
|
1151
1151
|
content.append("停止处理发言安排,取消本讨论自己的后台任务并核对实际状态;非必要不生成回执,正常停止无需回执。不要因旧通知恢复讨论。")
|
|
1152
1152
|
if turn:
|
package/docs/architecture.md
CHANGED
|
@@ -83,7 +83,7 @@ Python 服务独占监听端口:Windows 使用 `SO_EXCLUSIVEADDRUSE`,关闭
|
|
|
83
83
|
|
|
84
84
|
旧库初始化会补齐缺失表和记录,保留已有消息与安排。迁移不能重置当前 `room_floor`;新增迁移需要验证旧库和重复执行行为。Python 服务启动会应用 schema;远端 D1 更新由部署者明确执行。
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
房间资料将“用户原文”与“背景”作为同级章节,新建和编辑资料分别填写;两套后端、CLI 的 `room brief`、纯文本资料页与可读备份均提供独立原文。`user_request` 限 4000 字并保留首尾空白,由增量表 `room_user_requests` 保存;D1 升级须先应用 schema。旧房间没有独立记录时,仅对背景开头“用户原文:”及后续独占一行的“项目背景与入口:”或“背景:”明确分隔进行兼容读取,读取不改原始数据;下次保存资料时分开落库。未明确分隔的旧背景保持原样。
|
|
87
87
|
|
|
88
88
|
## 读写协议
|
|
89
89
|
|
|
@@ -112,11 +112,11 @@ GET / HEAD 不发布消息;旧 `/r/ROOM_ID/publish` 返回 410。新增内容
|
|
|
112
112
|
|
|
113
113
|
### 目录写入
|
|
114
114
|
|
|
115
|
-
`POST /api/folders/FOLDER_ID/move` 接收 `before_id`(null 表示整个列表末尾),两处编号均可使用保留值 `__uncategorized__` 表示内部排序用的虚拟分组。两套后端在同一事务更新 `topic_folders.position` 和 `directory_settings.uncategorized_position`,返回真实 `folders`
|
|
115
|
+
`POST /api/folders/FOLDER_ID/move` 接收 `before_id`(null 表示整个列表末尾),两处编号均可使用保留值 `__uncategorized__` 表示内部排序用的虚拟分组。两套后端在同一事务更新 `topic_folders.position` 和 `directory_settings.uncategorized_position`,返回真实 `folders` 与未归类房间区域的零基位置;目录读取也返回这两个字段。位置为 null 时该区域保持在末尾,新建文件夹排在它前面;设置为具体位置后,新建文件夹追加到真实文件夹末尾,不挤动该区域。旧库应用 schema 后默认保持原顺序,重复初始化不覆盖已保存位置;D1 部署前须应用 schema。虚拟分组不写入 `topic_folders`,未归类对话的 `folder_id` 仍为 null。
|
|
116
116
|
|
|
117
117
|
`POST /api/folders/FOLDER_ID/rename` 接收 1–40 字符的 `name`,保留文件夹编号、顺序与对话归属。建房请求可带 `folder_id`,省略或 null 表示未归类;两套后端均将建房与归类放在同一事务,并将指定文件夹纳入创建请求的幂等校验。重试返回已创建的对话,不再次变更后来调整的归属;不存在的文件夹会使创建失败,不留下孤立对话。
|
|
118
118
|
|
|
119
|
-
`POST /api/folders/FOLDER_ID/delete` 仅移除真实文件夹,使用既有 `ON DELETE SET NULL`
|
|
119
|
+
`POST /api/folders/FOLDER_ID/delete` 仅移除真实文件夹,使用既有 `ON DELETE SET NULL` 外键将普通和存档房间的归属清空,并在同一事务中调整未归类的位置,其他分组保持相对顺序。它不删除房间或消息,不改变讨论阶段与运行状态;不存在的文件夹及虚拟分组返回 404。复用现有表和外键,无需 schema 迁移。
|
|
120
120
|
|
|
121
121
|
## 聊天室本地备份
|
|
122
122
|
|
|
@@ -124,18 +124,18 @@ GET / HEAD 不发布消息;旧 `/r/ROOM_ID/publish` 返回 410。新增内容
|
|
|
124
124
|
|
|
125
125
|
Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读取,避免导出中途新增消息导致正文与关联元数据不一致。浏览器随后下载快照中不可变的附件原文件;外部链接只保留地址,未发送上传、未保存草稿与其他部署配置不在备份内。旧版快照缺少 `attachments` 时仍可导出。
|
|
126
126
|
|
|
127
|
-
`frontend/backup.mjs` 接收完整快照,通过 fflate 在浏览器内生成 ZIP:`完整数据.json` 保存可核对的原始字段,`讨论记录.md` 提供离线原文及消息/安排锚点,`备份说明.txt`
|
|
127
|
+
`frontend/backup.mjs` 接收完整快照,通过 fflate 在浏览器内生成 ZIP:`完整数据.json` 保存可核对的原始字段,`讨论记录.md` 提供离线原文及消息/安排锚点,`备份说明.txt` 记录范围。三个主题通过同一控制器触发,下载中切换房间不会改变本次备份对象。请求或打包失败不生成部分成功包,用户可再次操作重试。房间目录菜单的“导出全部房间”和文件夹菜单的“导出文件夹”把多个房间打成一个批量 ZIP,每个房间一个 `<标题>_<房间编号>/` 子目录,结构同单个备份;存档房间一并导出。备份需要将完整房间载入内存,当前未实现流式导出;很大的房间受浏览器及部署资源限制。
|
|
128
128
|
|
|
129
|
-
##
|
|
129
|
+
## 房间导入
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
房间目录菜单“导入房间…”接受单个或批量备份 ZIP(也接受单独的 `完整数据.json`),可多选。浏览器逐个房间为其生成新的房间控制凭据并保存在本浏览器,再调用 `POST /api/rooms/import`(请求体为 `完整数据.json` 对象,上限 32 MiB,`X-Discussion-Token` 为新凭据)。两套后端规则一致(`backend/room_import.py` 与 `worker.mjs` 中同名流程):
|
|
132
132
|
|
|
133
133
|
- **身份与去重**:保留备份中的房间编号;本站已有该编号时返回 200 `{skipped:true}`,不覆盖、不重复,重复导入安全。导入成功返回 201 与附件编号映射。
|
|
134
134
|
- **重新编号**:消息、发言安排、成员和自由协作请求在本站当前最大编号之后按原顺序重排;回复、回答、安排关联、摘要、耗时、附件、请求与建议随之映射;房间目标、共识、简报、议题字段、安排问题和消息正文中的 `#编号` 若指向本房间消息则改写为新编号,其他数字原样保留。共识与简报的“覆盖至”取所覆盖的最后一条新编号。附件获得新编号。
|
|
135
135
|
- **继续使用**:导入者成为房主;成员保留名称、职责与接入方式,状态改为离开(已移除者保持移除),凭据不导出也不可用,需在成员卡片“请回”换发新凭证。讨论房间没有运行记录,进行中的阶段改为已暂停,由房主重新启动;自由协作保持可用,未完成的请求改为已取消。所属文件夹按编号复用,本站没有时新建;存档状态保留;建房提交编号与来源房间关联不导入。
|
|
136
136
|
- **写入顺序**:消息先以自由协作模式、安排先以排队状态、消息职责先以占位类型写入,再恢复真实值,避免发言权、安排与运行触发器把历史当作新动作;全部写入在一个事务(D1 为带编号断言的单个 batch)中完成,失败整体回滚。D1 写入按 100 个参数分块合并多行。
|
|
137
137
|
- **附件原文件**:导入后浏览器把 ZIP 中的原文件逐个 `POST /api/rooms/ROOM_ID/attachments/新编号/import`,需房主凭据,大小与 SHA-256 须与记录一致;已有文件不覆盖。缺少原文件时消息仍导入,附件显示缺失。
|
|
138
|
-
-
|
|
138
|
+
- **结果**:房间目录下方列出导入、跳过与失败数量;失败项写明文件或房间及原因。
|
|
139
139
|
|
|
140
140
|
## 前端与素材
|
|
141
141
|
|
|
@@ -146,7 +146,7 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
|
|
|
146
146
|
| 源码 | 职责 |
|
|
147
147
|
| --- | --- |
|
|
148
148
|
| `main.js`、`App.vue`、`ChannelWorkspace.vue` | 协作频道入口、主题与整体布局 |
|
|
149
|
-
| `ChannelDirectory.vue` |
|
|
149
|
+
| `ChannelDirectory.vue` | 房间、文件夹和存档管理 |
|
|
150
150
|
| `ChannelDiscussion.vue` | 议题卡、消息流、原文导航和输入区;正文 `@` 在输入框上方弹出成员补全,按光标位置筛选与替换,支持鼠标、方向键和 Enter / Tab,中文输入期间不拦截候选键;自由房间只列在场成员,讨论房间列主持人与专家,选择不自动发送 |
|
|
151
151
|
| `TopicPanel.vue`、`WorkspaceDialogs.vue` | 话题资料、主持操作、接入步骤及编辑对话框 |
|
|
152
152
|
| `CreateDiscussion.vue`、`LimitPicker.vue`、`LaunchSettings.vue` | 新建讨论表单及本机启动配置;推进方式与参与专家分组,单选方式使用强调色卡片,参与者通过“邀请 AI”加入并独立移出;轮次和时长独立配置,以“不限”按钮切换各自上限,按需展开滚轮,选择即时生效,并预览到限条件;鼠标滚轮逐格切换且不产生惯性,鼠标及触摸拖动松手后保留惯性,先阻尼滑行再吸附预计停点,轻点中间数字精确输入;键盘聚焦和直接输入时仅高亮中间选中行,输入与显示共用行高,避免整列描边与原生输入框叠加;时长分小时/分钟两列,收起与摘要共用时长格式;支持键盘,减少动态效果时保留拖动惯性并去掉三维翻转 |
|
|
@@ -167,9 +167,9 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
|
|
|
167
167
|
|
|
168
168
|
正反面占用同一网格单元,避免翻转引起布局跳动。角度由现有 Motion 弹簧驱动:质量 1、频率 2.615 Hz、阻尼比 0.96(刚度 270.032、阻尼 31.551);目标在 0° / 180° 之间切换,中途反向保留速度,无固定时长。隐藏面设为 inert 并退出辅助技术读取,转过 90° 后移交焦点;Escape 返回,减少动态效果时直接到位。滑块采用单位质量、4.5 Hz 临界阻尼弹簧,拖动由 6 Hz 手部弹簧牵引、8 Hz 端点力限制;松手依据真实位置与速度的投影选择档位,点击直接选档,方向键和 Home / End 覆盖全部档位,取消拖动恢复原选项。模型与强度仅在创建前配置,讨论进度中保持只读。
|
|
169
169
|
|
|
170
|
-
界面只提供默认深色「协作频道」与浅色协作频道。深浅频道共用组件,浅色覆盖同时作用于正文、菜单和弹窗;只有深色频道启用根节点的 `dark`
|
|
170
|
+
界面只提供默认深色「协作频道」与浅色协作频道。深浅频道共用组件,浅色覆盖同时作用于正文、菜单和弹窗;只有深色频道启用根节点的 `dark` 类。外观设置集中在左下角,窄屏可从房间导航抽屉进入。主题保存在浏览器,已移除或未知主题值回退并保存为深色频道;切换保留房间、筛选与未发送草稿。原独立主题样式和切换入口已删除;HTML 中的隐藏控件仍作为共享控制器的表单适配层,不是可显示的第二套界面。
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
房间管理集中在左上方房间条目自带的「更多操作」菜单,右键条目使用同一菜单,窄屏从房间导航抽屉进入。「新建对话」与「讨论房间」的折叠箭头保持图标中心线及文字起点对齐。重命名、复制阅读入口、导出、移动、存档和删除对应被点击的房间,不切换当前讨论;当前房间菜单另提供编辑议题和刷新消息。房间条目不重复显示消息总数,统计保留在主区域顶栏。顶栏保留搜索、筛选、进度和侧栏开关。
|
|
173
173
|
|
|
174
174
|
深浅频道采用统一的低饱和蓝灰色系,由 `channel.css` 的背景、表面、强调色与分级阴影变量维护。左右侧栏共用底色,三栏标题与主持简报共用顶部表面色;正文底色、卡片与控件用明度差区分,避免各区域使用不同冷暖的灰色。深色卡片保留顶部亮边、底部暗边和局部渐变;浅色使用白色卡片、蓝灰细描边与柔和投影。交互状态共用蓝色强调色,参与者和运行状态保留各自的语义颜色。
|
|
175
175
|
|
|
@@ -181,11 +181,11 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
|
|
|
181
181
|
|
|
182
182
|
接入页分别交付本地成员的私有配置文件和不含 token 的参与说明,在线成员只给阅读说明;前端快照仅含成员接入方式与文件可用状态。手动创建、邀请、请回直接请求网站,自动成员才经本机启动器。创建与成员变更在请求前保存原编号和凭据,结果不明时沿用;复制说明不代表已送达或接入。项目、工作目录、讨论上限和错误按需展开。暂停后的本场软件连接修正使用独立卡片,页面不推断本机服务在线状态,选择模式或下载配置不会启动讨论。
|
|
183
183
|
|
|
184
|
-
消息区显示议题、原文和主持简报;议题卡的参与配置使用与头像一致的 20px 行高,姓名与主持 / CLI 标签保留字号层级并垂直居中。全自动执行期间,消息流末尾按有效执行显示等待状态:专家复用发言卡片的头像、姓名、轮次与样式,显示“正在唤醒…”或“正在准备回复…”;主持执行使用同宽、更矮、弱化底色和边框的“主持工作”卡,显示头像、姓名及右上角耗时,按交付与进入状态显示“正在唤醒主持人…”或“正在安排下一步…”,不推测内部操作。卡片类型按本次执行职责区分,主持人作为专家回答时仍使用专家等待卡;正式主持发言保留普通消息卡。安排待处理、手动接入、执行过期、暂停或归档不显示等待卡片。卡片不计入消息记录,没有消息编号、回复或引用操作;执行结束后移除,下一位专家实际交付后才显示其等待卡。搜索与提及筛选时隐藏,状态更新仅在用户原本位于底部时跟随滚动。标题栏、窄屏标签栏与主持简报合为同一块顶部面板,简报展开内容留在面板内,与收起状态共用背景、边线与投影,不再另起浮层。消息视图中该面板浮在消息列表之上,使用与深色浮层同款的毛玻璃(半透明渐变底、24px
|
|
184
|
+
消息区显示议题、原文和主持简报;议题卡的参与配置使用与头像一致的 20px 行高,姓名与主持 / CLI 标签保留字号层级并垂直居中。全自动执行期间,消息流末尾按有效执行显示等待状态:专家复用发言卡片的头像、姓名、轮次与样式,显示“正在唤醒…”或“正在准备回复…”;主持执行使用同宽、更矮、弱化底色和边框的“主持工作”卡,显示头像、姓名及右上角耗时,按交付与进入状态显示“正在唤醒主持人…”或“正在安排下一步…”,不推测内部操作。卡片类型按本次执行职责区分,主持人作为专家回答时仍使用专家等待卡;正式主持发言保留普通消息卡。安排待处理、手动接入、执行过期、暂停或归档不显示等待卡片。卡片不计入消息记录,没有消息编号、回复或引用操作;执行结束后移除,下一位专家实际交付后才显示其等待卡。搜索与提及筛选时隐藏,状态更新仅在用户原本位于底部时跟随滚动。标题栏、窄屏标签栏与主持简报合为同一块顶部面板,简报展开内容留在面板内,与收起状态共用背景、边线与投影,不再另起浮层。消息视图中该面板浮在消息列表之上,使用与深色浮层同款的毛玻璃(半透明渐变底、24px 背景模糊与增艳;浅色为白色半透明),滚动经过的卡片在其下模糊可见;消息列表顶部留白、导航与筛选条下移量、跳转与当前消息判定都按收起时的面板高度计算,展开简报只覆盖消息,不推动消息与输入区。其他视图中面板按普通流式布局排列。精简视觉效果下面板改为实色。已存档房间在独立弹窗管理,不改变当前房间和草稿。存档房间仍可阅读、恢复与导出;批量删除作用于当前搜索和筛选结果,执行前确认。
|
|
185
185
|
|
|
186
186
|
所有发言卡片右上角显示耗时。等待卡片使用同一执行的 `runtime.timings` 唤醒与执行秒数之和,从交付唤醒起累计,不计之前排队;本地每秒更新,服务器计时变化时校准,状态结束或切换房间后停止。正式发言的 `elapsed_seconds` 在保存消息的同一事务内按有效执行凭据与原作者关联计算,保存在 `message_timings`,此后翻页、重试和运行结束均不改变。主持发言同样计到各自保存时刻;不接受客户端提供的耗时。用户留言、手动或代发内容及缺少记录的旧消息显示“耗时 —”。升级仅为有精确 `message:ID` 完成结果与计时记录的历史专家发言回填,不按时间邻近推测关联,不覆盖已有耗时。
|
|
187
187
|
|
|
188
|
-
进度看板在宽屏消息页的右侧栏显示(视口 ≥1200px
|
|
188
|
+
进度看板在宽屏消息页的右侧栏显示(视口 ≥1200px),其他布局用同一组件的抽屉。仅当右侧进度栏不可见时,顶栏显示状态按钮(空间充足时附轮次),点击打开独立进度抽屉,关闭后保留原侧栏布局;右侧栏的展开与收起仍由独立的房间概览按钮控制。状态来自当前安排、本轮发言与 `runtime.participants`;会话编号来自最近一次实际进入的执行,`stale` 表示执行租约过期。模型与思考强度另从本机 `/api/local/room-models` 只读获取本场登记配置,仅返回适配器、模型与强度,继承本机桥接的同源或站点连接鉴权;请求站点固定为已验证的来源,不经过公开网站或改变运行状态。读取结果按房间及运行版本缓存,切换房间丢弃迟到响应;未获取时不使用默认配置猜测。手动模式仅按安排和发言展示进度,不推断离线或伪造执行回执。
|
|
189
189
|
|
|
190
190
|
运行操作按服务器状态开放:暂停、重启、取消安排与提前结束均调用既有接口,业务成功以服务器确认为准。重启只用于已暂停或到期的全自动运行,需要本机接入程序运行;服务器按话题上限截短新截止时间。准备、收束和存档状态不显示不可用操作。界面尺寸与动效参数以组件和样式为准,变更通过 [贡献指南](../.github/CONTRIBUTING.md) 的界面验收要求核对。
|
|
191
191
|
|
|
@@ -205,7 +205,7 @@ Python 通过进程内条件变量唤醒订阅者,不占用数据库连接,
|
|
|
205
205
|
|
|
206
206
|
新建文件夹在编号返回前不可作为归类目标;创建对话在服务器确认前不展示可用房间入口,完成后不抢占后来发生的导航。删除确认后立即从目录移除,失败恢复;存档保留当前原文和草稿。
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
未归类房间直接平铺,文件夹内房间缩进显示。手动顺序与最近发言排序分开保存;最近发言模式可跨文件夹移动,组内排序需切回手动模式。排序偏好保存在浏览器,不改写服务器保存的手动顺序。
|
|
209
209
|
|
|
210
210
|
## 测试与已知边界
|
|
211
211
|
|
|
@@ -237,7 +237,7 @@ running 状态即使只有 pending 事件,也必须先暂停才能重新启动
|
|
|
237
237
|
|
|
238
238
|
自由房间(`room_modes.mode='free'`)与讨论房间共用房间、成员登记、消息、附件与目录,没有发言权、安排、阶段推进和轮次。没有控制凭据登记的旧房间只供阅读,不支持通过首次运行配置认领;本次不迁移历史房间。
|
|
239
239
|
|
|
240
|
-
-
|
|
240
|
+
- **凭据**:建房时由客户端提供房间控制凭据和每位成员的成员凭证,服务端只存哈希。用户身份的房间写入(发言、附件、存档、恢复、删除、打断)都须控制凭据;房间排序和归入文件夹只改共享目录,与站点级文件夹操作一样不需要凭据;无凭据、错误凭据、成员凭证或他房凭据一律只读。成员凭证只能以该成员身份发言或上传附件。声明字段(`author`、`source`、localhost)不代替凭据。
|
|
241
241
|
- **成员**:`room_members` 以 `id` 为稳定主键,`access` 为 `runner`(接入程序投递)、`manual`(成员自行读写)或 `online`(只读、原文代发),`session_mode` 标记是否借用用户正在使用的会话。房主用控制凭据 `POST members` 运行中加入成员,`POST members/ID {action}` 让成员离开(`away`,作废其进行中执行与未投递请求)、请回(`active`,必须提供不同于原凭证的新凭证,旧凭证副本不再有效)或移除(`remove`,保留历史发言)。同一成员凭证不能复用于其他成员或房主;相同凭证与配置的邀请、请回重试返回已有结果。会话续接由本机接入程序负责,服务端不保存会话绑定。
|
|
242
242
|
- **请求**:用户消息按结构化接收者 `to`(成员名)为每位成员生成一条请求;省略且仅有一位在场成员时默认点名该成员,多位成员时交给主持人(`route`),只有在线成员而没有主持人时只作留言,不广播。网页从点名按钮和正文中代码与引用之外的 `@成员` 生成 `to`。请求状态为 `pending`(待投递)→ `delivering`(处理中)→ `completed` / `cancelled` / `failed`。
|
|
243
243
|
- **执行**:`member_executions` 每位成员最多一个 `dispatched/started`(部分唯一索引),不同成员可并行。接入程序用控制凭据 `GET inbox`、`POST executions {member_id, client_id}`(同一 `client_id` 返回同一执行)把成员的全部待投递请求打包;`POST executions/ID {action}` 支持 `renew`(租约 120 秒,不超过单次执行时限,默认 20 分钟)、`fail`(请求标记失败)和 `release`(仅确认未送达时退回队列)。租约过期的执行在下次读取或领取时标记失败,不自动重发。
|
package/package.json
CHANGED
package/web/backup.js
CHANGED
|
@@ -8,15 +8,15 @@ var discuss_backup=(function(e){Object.defineProperty(e,Symbol.toStringTag,{valu
|
|
|
8
8
|
附件/:本聊天室已发送附件的原文件,与完整数据中的附件编号对应。
|
|
9
9
|
外部网页、外链图片和外部附件仅保留原链接,不下载其内容;未发送的上传不在备份内。
|
|
10
10
|
这是导出时刻的快照,之后的新消息不在包内。
|
|
11
|
-
|
|
12
|
-
`,je=`AI CoDiscuss
|
|
11
|
+
导入:在任一共议网站的房间目录选“导入房间”并选择本 ZIP,即建成可继续使用的副本;已有同一房间时跳过。
|
|
12
|
+
`,je=`AI CoDiscuss 房间批量备份
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
每个子文件夹是一个房间的完整备份,结构与单个房间备份相同(完整数据.json、讨论记录.md、附件/)。
|
|
15
|
+
在任一共议网站的房间目录选“导入房间”并选择本 ZIP,可一次导入全部房间;已有同一房间时跳过。
|
|
16
16
|
`;function $(e){let t=e??``,n=Math.max(2,...(t.match(/`+/g)||[]).map(e=>e.length)),r="`".repeat(n+1);return`${r}text\n${t}\n${r}`}function Me(e,t,n,r){let i=t?.submitted_by||([`cli`,`api`,`url`,`user`].includes(e.source)?e.author:`用户`),a=[`<a id="message-${e.id}"></a>`,`### #${e.id} · ${e.author}`,`时间:${e.created_at};来源:${e.source};职责:${t?.kind||`opinion`};代发/提交者:${i}`];e.reply_to&&a.push(`回复:[#${e.reply_to}](#message-${e.reply_to})`),t?.turn_id&&a.push(`对应安排:[#${t.turn_id}](#turn-${t.turn_id})`),n?.text&&a.push(`简述(作者:${n.author}):`,$(n.text)),a.push(`原文:`,$(e.text));for(let e of r){let t=Pe(e).split(`/`).map(e=>encodeURIComponent(e).replace(/[()]/g,e=>`%${e.charCodeAt(0).toString(16)}`)).join(`/`);a.push(`附件:${$(e.file_name)}\n\n[下载原文件](${t}) · ${e.size} 字节 · SHA-256: ${e.sha256}`)}return a.join(`
|
|
17
17
|
|
|
18
18
|
`)}function Ne(e){let t=e.tables,n=t.rooms[0],r=t.topic_settings[0],i=t.room_user_requests?.[0],a=!i&&r?.background.match(/^用户原文[::][ \t]*(?:\r?\n)?([\s\S]*?)(?:\r?\n){2,}((?:项目背景与入口|背景)[::][\s\S]*)$/),o=i?.text??(a?a[1]:``),s=a?a[2]:r?.background,c=t.room_briefs[0],l=new Map(t.message_details.map(e=>[e.message_id,e])),u=new Map(t.message_summaries.map(e=>[e.message_id,e])),d=[`# 讨论记录`,$(n.title),`房间:${n.id};创建:${n.created_at};导出:${e.exported_at}`,`来源:${e.source_url||`未记录`}`,`消息:${t.messages.length} 条;发言安排:${t.discussion_turns.length} 项`,`目录状态:${t.room_directory[0]?.archived?`已存档`:`使用中`};文件夹:${t.topic_folders[0]?.name||`未分组`}`,`## 目标`,$(n.goal)];r&&d.push(`主持人:${r.host};阶段:${r.phase}`,`参与配置:`,$(r.experts),`## 用户原文`,$(o),`## 背景`,$(s),`## 讨论边界`,$(r.boundaries),`## 当前焦点`,$(r.focus)),c&&d.push(`## 主持简报`,`覆盖至 #${c.until};更新:${c.updated_at}`,$(c.text)),t.discussion_limits?.[0]&&d.push(`## 讨论上限`,$(JSON.stringify(t.discussion_limits[0],null,2))),d.push(`## 共识与分歧`,`覆盖至 #${n.state_until}`,$(n.state_text),`## 发言权`,$(JSON.stringify(t.room_floor[0]||{},null,2)),`## 全部消息`);for(let e of t.messages)d.push(Me(e,l.get(e.id),u.get(e.id),(t.attachments||[]).filter(t=>t.message_id===e.id)));t.messages.length||d.push(`暂无消息。`),d.push(`## 全部发言安排`);for(let e of t.discussion_turns)d.push(`<a id="turn-${e.id}"></a>`,`### 安排 #${e.id} · ${e.expert}`,`状态:${e.status};创建:${e.created_at};通知:${e.notified_at||`未记录`}`,$(e.question)),e.answer_id&&d.push(`回答:[#${e.answer_id}](#message-${e.answer_id})`);return t.discussion_turns.length||d.push(`暂无发言安排。`),d.join(`
|
|
19
19
|
|
|
20
20
|
`)+`
|
|
21
21
|
`}function Pe(e){let t=e.file_name.replace(/[<>:"/\\|?*\u0000-\u001f]/g,`_`).replace(/[. ]+$/,``)||`附件`;return`附件/${e.id}_${t}`}async function Fe(e){let t=await fetch(`/api/rooms/${encodeURIComponent(e.room_id)}/attachments/${encodeURIComponent(e.id)}/content?download=1`);if(!t.ok)throw Error(`附件下载失败:${e.file_name},未生成备份`);let n=new Uint8Array(await t.arrayBuffer());if(n.length!==e.size)throw Error(`附件不完整:${e.file_name},未生成备份`);if(globalThis.crypto?.subtle&&[...new Uint8Array(await crypto.subtle.digest(`SHA-256`,n))].map(e=>e.toString(16).padStart(2,`0`)).join(``)!==e.sha256)throw Error(`附件校验失败:${e.file_name},未生成备份`);return n}async function Ie(e,t,n,r){e[`${n}完整数据.json`]=_e(JSON.stringify(t,null,2)+`
|
|
22
|
-
`),e[`${n}讨论记录.md`]=_e(Ne(t));for(let i of t.tables.attachments||[])e[n+Pe(i)]=[await r(i),{level:0}]}function Le(e){return new Promise((t,n)=>{Te(e,{level:6},(e,r)=>e?n(e):t(r))})}function Re(e){return[...e.replace(/[<>:"/\\|?*\u0000-\u001f]/g,`_`)].slice(0,ke).join(``).replace(/[. ]+$/,``)||`讨论`}async function ze(e,t=Fe){let n={"备份说明.txt":_e(Ae)};return await Ie(n,e,``,t),Le(n)}async function Be(e,t=Fe){let n={"备份说明.txt":_e(je)};for(let r of e){let e=r.tables.rooms[0];await Ie(n,r,`${Re(e.title)}_${e.id}/`,t)}return Le(n)}async function Ve(e){let t=new Uint8Array(await e.arrayBuffer());if(e.name.toLowerCase().endsWith(`.json`))return[{snapshot:JSON.parse(ve(t)),files:new Map}];let n=await new Promise((n,r)=>De(t,(t,i)=>t?r(Error(`${e.name} 不是有效的 ZIP 备份`)):n(i))),r=Object.keys(n).filter(e=>/^(?:[^/]+\/)?完整数据\.json$/.test(e)).map(e=>{let t=e.slice(0,-9),r=JSON.parse(ve(n[e]));return{snapshot:r,files:new Map((r.tables?.attachments||[]).map(e=>[e.id,n[t+Pe(e)]]).filter(([,e])=>e))}});if(!r.length)throw Error(`${e.name}
|
|
22
|
+
`),e[`${n}讨论记录.md`]=_e(Ne(t));for(let i of t.tables.attachments||[])e[n+Pe(i)]=[await r(i),{level:0}]}function Le(e){return new Promise((t,n)=>{Te(e,{level:6},(e,r)=>e?n(e):t(r))})}function Re(e){return[...e.replace(/[<>:"/\\|?*\u0000-\u001f]/g,`_`)].slice(0,ke).join(``).replace(/[. ]+$/,``)||`讨论`}async function ze(e,t=Fe){let n={"备份说明.txt":_e(Ae)};return await Ie(n,e,``,t),Le(n)}async function Be(e,t=Fe){let n={"备份说明.txt":_e(je)};for(let r of e){let e=r.tables.rooms[0];await Ie(n,r,`${Re(e.title)}_${e.id}/`,t)}return Le(n)}async function Ve(e){let t=new Uint8Array(await e.arrayBuffer());if(e.name.toLowerCase().endsWith(`.json`))return[{snapshot:JSON.parse(ve(t)),files:new Map}];let n=await new Promise((n,r)=>De(t,(t,i)=>t?r(Error(`${e.name} 不是有效的 ZIP 备份`)):n(i))),r=Object.keys(n).filter(e=>/^(?:[^/]+\/)?完整数据\.json$/.test(e)).map(e=>{let t=e.slice(0,-9),r=JSON.parse(ve(n[e]));return{snapshot:r,files:new Map((r.tables?.attachments||[]).map(e=>[e.id,n[t+Pe(e)]]).filter(([,e])=>e))}});if(!r.length)throw Error(`${e.name} 中没有房间备份(完整数据.json)`);return r}function He(e,t){let n=URL.createObjectURL(new Blob([e],{type:`application/zip`})),r=document.createElement(`a`);r.href=n,r.download=t,document.body.append(r);try{r.click()}finally{r.remove(),setTimeout(()=>URL.revokeObjectURL(n),Oe)}}async function Ue(e){He(await ze(e),`${Re(e.tables.rooms[0].title)}_${e.exported_at.replace(/[:.]/g,`-`)}.zip`)}async function We(e,t){He(await Be(e),`${Re(t)}_${new Date().toISOString().replace(/[:.]/g,`-`)}.zip`)}return e.attachment_archive_path=Pe,e.create_room_archive=ze,e.create_rooms_archive=Be,e.download_room_backup=Ue,e.download_rooms_backup=We,e.read_room_archives=Ve,e.room_markdown=Ne,e})({});
|