ai_co_discuss 0.2.50 → 0.2.58

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 CHANGED
@@ -140,7 +140,7 @@ codiscuss local connect https://你的站点
140
140
  | 自由协作 · 手动接入 | 成员自行读取请求,由软件原生定时或人工触发 | 成员持自己的凭据文件 |
141
141
  | 自由协作 · 网页代发 | 你把阅读通知交给网页 AI,再代发回答 | 无需本地软件,不能自行发言 |
142
142
 
143
- 默认上限为 **3 轮、不限时长、到限暂停**,可改为请主持人总结,并设置单次执行上限与收束时间。计数和停止规则见 [讨论上限](docs/workflow.md#讨论上限)。
143
+ 默认上限为 **3 轮、不限时长、到限暂停**,可设置单次执行上限;到限后由你决定追加轮次或结束。计数和停止规则见 [讨论上限](docs/workflow.md#讨论上限)。
144
144
 
145
145
  消息支持选择文件、拖拽或粘贴截图,每条最多 10 个附件、单个最多 20 MiB。PNG、JPEG、GIF、WebP 可放大查看,PDF 可在站内翻页、缩放,其他文件提供下载(不在线解析 Office 文件)。本地 AI 可通过 CLI 按需下载原文件;实际能否理解图片或文档取决于参与软件,上传不代表 AI 已读取。
146
146
 
@@ -18,6 +18,7 @@ from .runtime import begin_write, require_owner, token_hash
18
18
  BACKUP_FORMAT = "ai-codiscuss-room-backup"
19
19
  ROOM_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
20
20
  REFERENCE = re.compile(r"#(\d+)")
21
+ # "reviewing" only appears in backups made before that phase was retired; it imports like discussing.
21
22
  ACTIVE_PHASES = ("initial", "discussing", "reviewing")
22
23
  OPEN_REQUESTS = ("pending", "delivering")
23
24
  LIMIT_FIELDS = ("max_rounds", "max_minutes", "execution_minutes", "on_limit", "summary_minutes",
@@ -289,8 +289,8 @@ def validate_start(connection, room_id, payload):
289
289
  if not experts or any(mode != "cli" for mode in experts.values()):
290
290
  raise ValueError("自动运行的参与者须均具有本地 CLI 读写能力")
291
291
  phase = payload.get("phase", "initial" if row["phase"] == "preparing" else "discussing")
292
- if phase not in ("initial", "discussing", "reviewing"):
293
- raise ValueError("恢复阶段必须为 initial/discussing/reviewing")
292
+ if phase not in ("initial", "discussing"):
293
+ raise ValueError("恢复阶段必须为 initial/discussing")
294
294
  if row["phase"] in ("preparing", "paused"):
295
295
  connection.execute("UPDATE topic_settings SET phase=? WHERE room_id=?", (phase, room_id))
296
296
  connection.execute("UPDATE discussion_limits SET started_at=? WHERE room_id=? AND started_at=0",
@@ -202,12 +202,12 @@ BEGIN
202
202
  INSERT INTO discussion_limits(room_id,execution_minutes) VALUES (NEW.id,0);
203
203
  END;
204
204
  CREATE TRIGGER IF NOT EXISTS limits_topic_started AFTER INSERT ON topic_settings
205
- WHEN NEW.phase IN ('initial','discussing','reviewing')
205
+ WHEN NEW.phase IN ('initial','discussing')
206
206
  BEGIN
207
207
  UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
208
208
  END;
209
209
  CREATE TRIGGER IF NOT EXISTS limits_phase_started AFTER UPDATE OF phase ON topic_settings
210
- WHEN NEW.phase IN ('initial','discussing','reviewing') AND NEW.phase<>OLD.phase
210
+ WHEN NEW.phase IN ('initial','discussing') AND NEW.phase<>OLD.phase
211
211
  BEGIN
212
212
  UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
213
213
  END;
@@ -400,7 +400,7 @@ BEGIN
400
400
  UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
401
401
  AND role='host' AND status='started' AND NEW.phase='concluded';
402
402
  UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
403
- resume_phase=CASE WHEN OLD.phase IN ('initial','discussing','reviewing') THEN OLD.phase ELSE resume_phase END,
403
+ resume_phase=CASE WHEN OLD.phase IN ('initial','discussing') THEN OLD.phase ELSE resume_phase END,
404
404
  updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
405
405
  END;
406
406
  CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
@@ -476,14 +476,14 @@ INSERT INTO discussion_pauses(room_id,paused_at)
476
476
  CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
477
477
  FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
478
478
  CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
479
- WHEN OLD.phase IN ('initial','discussing','reviewing') AND NEW.phase NOT IN ('initial','discussing','reviewing')
479
+ WHEN OLD.phase IN ('initial','discussing') AND NEW.phase NOT IN ('initial','discussing')
480
480
  BEGIN
481
481
  INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
482
482
  WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
483
483
  UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
484
484
  END;
485
485
  CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
486
- WHEN NEW.phase IN ('initial','discussing','reviewing') AND OLD.phase NOT IN ('initial','discussing','reviewing')
486
+ WHEN NEW.phase IN ('initial','discussing') AND OLD.phase NOT IN ('initial','discussing')
487
487
  BEGIN
488
488
  UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
489
489
  WHERE room_id=NEW.room_id AND paused_at>0;
@@ -671,3 +671,7 @@ BEGIN
671
671
  ended_message_id=CASE WHEN NEW.phase='concluded' THEN COALESCE((SELECT MAX(id) FROM messages WHERE room_id=NEW.room_id),0) ELSE 0 END
672
672
  WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
673
673
  END;
674
+
675
+ -- The review phase behaved exactly like discussing and was retired; move rows left in it.
676
+ UPDATE topic_settings SET phase='discussing' WHERE phase='reviewing';
677
+ UPDATE discussion_runtime SET resume_phase='discussing' WHERE resume_phase='reviewing';
package/backend/server.py CHANGED
@@ -69,7 +69,7 @@ ASSETS.update({"/pdfjs/" + file.relative_to(WEB_ROOT / "pdfjs").as_posix():
69
69
  for file in (WEB_ROOT / "pdfjs").rglob("*") if file.is_file()})
70
70
  SCHEMA = (Path(__file__).resolve().parent / "schema.sql").read_text(encoding="utf-8")
71
71
  PHASES = {"preparing": "准备中", "initial": "独立观点", "discussing": "深入讨论",
72
- "reviewing": "结论复核", "paused": "已暂停", "concluded": "已收束"}
72
+ "paused": "已暂停", "concluded": "已收束"}
73
73
  TURN_STATUSES = {"pending": "待处理", "notified": "已通知,待回答", "answered": "已回答", "cancelled": "已取消",
74
74
  "queued": "排队中", "skipped": "未开始即取消"}
75
75
  MAX_TOPIC_DETAIL = 4000
@@ -153,7 +153,7 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
153
153
  | `TopicPanel.vue`、`WorkspaceDialogs.vue` | 话题资料、主持操作、接入步骤及编辑对话框 |
154
154
  | `CreateDiscussion.vue`、`LimitPicker.vue`、`LaunchSettings.vue` | 新建讨论表单及本机启动配置;推进方式与参与专家分组,单选方式使用强调色卡片,参与者通过“邀请 AI”加入并独立移出;轮次和时长独立配置,以“不限”按钮切换各自上限,按需展开滚轮,选择即时生效,并预览到限条件;鼠标滚轮逐格切换且不产生惯性,鼠标及触摸拖动松手后保留惯性,先阻尼滑行再吸附预计停点,轻点中间数字精确输入;键盘聚焦和直接输入时仅高亮中间选中行,输入与显示共用行高,避免整列描边与原生输入框叠加;时长分小时/分钟两列,收起与摘要共用时长格式;支持键盘,减少动态效果时保留拖动惯性并去掉三维翻转 |
155
155
  | `ProgressBoard.vue`、`ProgressPanel.vue` | 共享进度看板及抽屉容器;Codex 参与者菜单内直接打开 App / CLI 切换弹窗,显式暂停后复用本场配置的读取与保存,不跳转接入页或自动重启 |
156
- | `FreeBoard.vue` | 自由协作房间的右侧看板,版式与讨论进度看板一致:房间状态与请求/执行计数、当前任务、成员(主持人居首,显示接入方式、模型与状态)、邀请、发起头脑风暴和每次执行用时 |
156
+ | `FreeBoard.vue` | 自由协作房间的右侧看板,版式与讨论进度看板一致:房间状态与请求/执行计数、当前任务、成员(主持人居首,显示接入方式、模型与状态)、邀请(弹出 `InviteDialog.vue`)、开启讨论(弹出 `StartDiscussionDialog.vue`)和每次执行用时 |
157
157
  | `AccountSettings.vue` | 设置弹窗的管理员账号分区(注册、登录、改名与头像、退出登录) |
158
158
  | `use-discussion.js` | 将 `web/app.js` 的共享控制器状态接入 Vue |
159
159
  | `theme.css`、`channel.css` | 组件主题、深浅配色与响应式布局 |
@@ -165,7 +165,7 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
165
165
 
166
166
  ### 主题与交互边界
167
167
 
168
- 新建表单独立选择房间用法(头脑风暴/自由协作)与接入方式(程序唤醒/手动),共用内联角色库。接入方式以分段切换放在参与区标题旁;每次添加生成独立实例,已加入成员以 `ExpertCard.vue` 渲染为翻面卡片:正面是身份与只读配置(状态、软件、模型、强度、项目权限),背面是设置,项目只读 / 可写按专家分别设置,App 与手动会话显示“由软件设置”,API 与网页专家显示“不访问”,缺少的软件路径在背面直接填写;卡片上的皇冠指定主持人,移出按钮同步修复主持人选择,角色库为卡片下方的“添加”标签行。自由协作的用户是需求方,默认由第一位能自行发言的成员主持,可点皇冠更换。支持本地参与的卡片翻面设置参与能力和软件入口,纯网页角色不翻面。自动 CLI 从本机软件目录下拉选择模型,不要求手填 ID;模型与强度同层显示,恢复默认只清除当前实例覆盖。App、网页和手动会话在原软件内设置。`EffortSlider.vue` 按所选模型的实际档位显示,首档为空值的 CLI 默认,显示标签与提交值分离(Extra High 对应 xhigh);标题处的档位名随拖动实时预览、松手才提交,悬停、拖动与键盘聚焦分别有光晕反馈,减少动态效果时不做过渡;agy 的强度随模型,不另显示滑杆。目录不可用时显示原因并保留软件默认,不虚构列表。
168
+ 新建表单只建协作房间(可先不加成员;项目目录在第 1 步,勾选“创建后开启讨论”才展开目标与上限,上限控件为共用的 `DiscussionLimits.vue`),并为每位成员选择接入方式(程序唤醒/手动),共用内联角色库。接入方式以分段切换放在参与区标题旁;每次添加生成独立实例,已加入成员以 `ExpertCard.vue` 渲染为翻面卡片:正面是身份与只读配置(状态、软件、模型、强度、项目权限),背面是设置,项目只读 / 可写按专家分别设置,App 与手动会话显示“由软件设置”,API 与网页专家显示“不访问”,缺少的软件路径在背面直接填写;卡片上的皇冠指定主持人,移出按钮同步修复主持人选择,角色库为卡片下方的“添加”标签行。自由协作的用户是需求方,默认由第一位能自行发言的成员主持,可点皇冠更换。支持本地参与的卡片翻面设置参与能力和软件入口,纯网页角色不翻面。自动 CLI 从本机软件目录下拉选择模型,不要求手填 ID;模型与强度同层显示,恢复默认只清除当前实例覆盖。App、网页和手动会话在原软件内设置。`EffortSlider.vue` 按所选模型的实际档位显示,首档为空值的 CLI 默认,显示标签与提交值分离(Extra High 对应 xhigh);标题处的档位名随拖动实时预览、松手才提交,悬停、拖动与键盘聚焦分别有光晕反馈,减少动态效果时不做过渡;agy 的强度随模型,不另显示滑杆。目录不可用时显示原因并保留软件默认,不虚构列表。
169
169
 
170
170
  项目权限选择仅对 CLI 入口开放;API 与网页专家不访问项目文件,界面显示“不访问”。创建卡片、接入设置、经典主题和自由房间邀请沿用同一 CLI 入口判断,API 不继承本场可写配置,执行器强制归一为只读。App 的实际文件权限在原软件中配置。
171
171
 
@@ -262,7 +262,7 @@ running 状态即使只有 pending 事件,也必须先暂停才能重新启动
262
262
  - **回应建议**:普通成员新发言的结构化 `to` 与正文在场成员 @ 合并保存到 `member_suggestions`(原消息编号、稳定成员编号),排除自己、代码与引用;不会创建请求。消息读取返回 `suggestions`,包含目标在场状态及已转换的请求编号。房主用 `POST suggestions {message_id,member_id}` 转换:同一事务记录用户确认消息,调用现有建请求路径,`message_id` 保留原回答、`chain_id` 使用用户确认消息编号、`created_by=user`。`UNIQUE(message_id,member_id)` 使重复或并发点击只生成一个请求及一条确认,终态不重新派发。权限、在场状态和未存档条件在写入事务内校验;Worker 使用批次断言。升级须应用包含新表的 schema;Python 重启时自动建表,D1 发布前显式执行。已有历史消息不补写建议。
263
263
  - **讨论中的直接提问**:讨论房间的用户消息带 `to`(点名成员名列表)时,除保存消息外为每位被点名的在场成员建一条 `ask` 请求(`created_by=user`,不入 `free_batches`,不占安排);讨论运行中点名主持人不重复建请求。`GET inbox`、`POST executions` 与 `executions/ID` 对讨论房间同样可用,成员以 `execution_id`(接入程序交付)或 `request_ids`(手动 / 代发)提交 `chat` 回答,走自由房间的写入校验。同一成员的 `member_executions` 与 `discussion_events` 互斥(任一方领取时另一方有进行中则返回 409)。房间视图 `asks` 给出最近请求与执行,供消息旁的状态徽标。本机服务以 `DiscussionWithQuestions`(`runner/discussion_service.mjs`)并行运行讨论 runner 与 `FreeRoomRunner`,共用同一成员会话绑定。
264
264
  - **在线成员**:`access=online` 的成员只读,不进入 `inbox`,不能用自己的凭证发言;房主以控制凭据代发其回答(`author` 为该成员、`submitted_by` 为用户、`source=proxy`),可带 `request_ids` 关闭它的待处理请求。
265
- - **关联房间**:从自由房间发起的讨论房间在建房时带 `parent_room_id`,记录在 `room_links`;讨论房间视图有 `parent_room`,自由房间视图列出 `linked_rooms`。头脑风暴的轮次、时长与名单因此天然独立于自由房间。
265
+ - **关联房间(旧)**:早期从自由房间发起的讨论子房间带 `parent_room_id`,记录在 `room_links`,视图仍列出 `linked_rooms` 供查看历史;新房间不再创建关联子房间,讨论在房间内开启(`discussion` 接口)。
266
266
  - **手动成员**:手动成员不进入 `inbox`,也不能被领取执行;它用成员凭证读写,回答时可带 `request_ids` 关闭自己的待处理请求,不带则是普通发言、不关闭任何请求。
267
267
  - **改动记录**:接入程序在执行前后对成员 cwd 做 Git 工作区快照(状态与内容哈希),执行结束且进程退出后用 `report {changes}` 记录一次。房间视图的 `free.conflicts` 列出不同成员在同一 cwd、时段重叠的执行均观察到变化的文件,只作“可能冲突”提示;非 Git 目录不记录。快照也会包含其他进程在同一目录的写入,不能确定修改者;分工修改不同文件仍可能出现提示,不代表已发生覆盖。
268
268
  - **回答**:接入程序投递的成员回答须带 `execution_id` 并使用成员凭证;消息保存、执行完成、请求完成和成员已读游标在同一事务中写入。执行结束、被打断或过期后的回答返回 409;缺少执行编号返回 400,不能改作普通发言。消息类型为 `chat`,不计入讨论轮次。
package/docs/runner.md CHANGED
@@ -102,7 +102,7 @@ codiscuss local stop
102
102
  python server.py --host 127.0.0.1 --local-launch
103
103
  ```
104
104
 
105
- 需要 Node.js 24+ 与已安装、已登录的 AI 客户端;讨论所需的最小工具权限按 [单次执行权限](#单次执行权限) 提供。新建时分别选择房间用法与“程序唤醒”,从卡片下方的“添加”行加入本地参与实例,点卡片上的皇冠指定主持人;自由协作默认由第一位能自行发言的成员主持(用户是需求方),头脑风暴须选择本地 AI 主持。参与区只显示已加入的实例,用独立“移出”按钮调整名单。自动 CLI 卡片翻到背面后通过真实软件目录下拉选择模型,并按该模型支持的档位显示强度滑杆;首档使用 CLI 默认,agy 的思考档位通过原生只读命令查询。恢复默认只清除该实例的模型与强度覆盖,App、网页和手动会话在原软件设置。配置入口不依赖勾选立即启动,移出后重新加入保留本次填写。接入方式默认“程序唤醒”,“创建后立即启动 AI”默认勾选;本机服务未连接时只建房。项目文件权限在各自动 CLI 卡片上按专家设置(默认只读,在线专家固定只读),卡片背面“取消”或 Esc 恢复翻面前的设置,“完成”保留修改;卡片只在缺少软件路径、Sidecar IPC 目录或 Codex App 来源聊天时显示对应输入。项目目录可直接输入,也可点输入框右侧的文件夹图标经 `POST /api/local/pick-directory` 在运行本机服务的电脑上弹出系统目录选择窗口(Windows 用 PowerShell WinForms,macOS 用 osascript,Linux 用 zenity),取消时不返回路径,窗口最长等待 10 分钟。再点击“创建并启动讨论”;不勾选则只建房,之后在“接入”页启动。自由协作的“创建并连接”登记自动成员,后续点名产生任务时才唤醒。复用会话、单个 AI 的工作目录与项目等高级覆盖,以及“保存为本机软件默认配置”,在建房后的“接入”页设置,保存本身不启动任何 AI。卡片输入沿用当前页面的启动草稿,不随只建房请求保存到网站。正常首次自动接入无需下载 JSON、手动登记房间或另执行 `run start`。
105
+ 需要 Node.js 24+ 与已安装、已登录的 AI 客户端;讨论所需的最小工具权限按 [单次执行权限](#单次执行权限) 提供。新建时分别选择房间用法与“程序唤醒”,从卡片下方的“添加”行加入本地参与实例,点卡片上的皇冠指定主持人;自由协作默认由第一位能自行发言的成员主持(用户是需求方),开启讨论须有一位由本机唤醒的本地 AI 主持。参与区只显示已加入的实例,用独立“移出”按钮调整名单。自动 CLI 卡片翻到背面后通过真实软件目录下拉选择模型,并按该模型支持的档位显示强度滑杆;首档使用 CLI 默认,agy 的思考档位通过原生只读命令查询。恢复默认只清除该实例的模型与强度覆盖,App、网页和手动会话在原软件设置。配置入口不依赖勾选立即启动,移出后重新加入保留本次填写。接入方式默认“程序唤醒”,“创建后立即启动 AI”默认勾选;本机服务未连接时只建房。项目文件权限在各自动 CLI 卡片上按专家设置(默认只读,在线专家固定只读),卡片背面“取消”或 Esc 恢复翻面前的设置,“完成”保留修改;卡片只在缺少软件路径、Sidecar IPC 目录或 Codex App 来源聊天时显示对应输入。项目目录可直接输入,也可点输入框右侧的文件夹图标经 `POST /api/local/pick-directory` 在运行本机服务的电脑上弹出系统目录选择窗口(Windows 用 PowerShell WinForms,macOS 用 osascript,Linux 用 zenity),取消时不返回路径,窗口最长等待 10 分钟。再点击“创建并启动讨论”;不勾选则只建房,之后在“接入”页启动。自由协作的“创建并连接”登记自动成员,后续点名产生任务时才唤醒。复用会话、单个 AI 的工作目录与项目等高级覆盖,以及“保存为本机软件默认配置”,在建房后的“接入”页设置,保存本身不启动任何 AI。卡片输入沿用当前页面的启动草稿,不随只建房请求保存到网站。正常首次自动接入无需下载 JSON、手动登记房间或另执行 `run start`。
106
106
 
107
107
  网页在发送前把完整已提交输入、原提交号和控制凭据保存在当前站点的浏览器本地存储。服务启动失败或响应丢失后,刷新仍会显示“继续未完成的启动”;点击“继续本次启动”沿用原请求和本机已固定配置,无需重新填写,也不随新的默认配置变化。刷新只恢复界面,不自动运行;已暂停、结束、过期或在启动期间改变的房间不会因此重新开始。部分失败明确显示已核实的阶段,已建房不等于回滚;结果不明时保留原请求待核对。
108
108
 
@@ -318,6 +318,8 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
318
318
 
319
319
  运行中的讨论同样可用 `room-member` 邀请或请回专家,但只接受由本机唤醒的成员(不接受 `access: manual`)。`participants[name].session_id` 指定已有会话时,成员标记为 `borrowed`,首次投递即续接该会话;网页仅对 Codex CLI / App 与 Claude 提供此项,agy 照旧拒绝。邀请前在本机核对会话确实存在(Codex 查 `CODEX_HOME/sessions`,Claude 查 `CLAUDE_CONFIG_DIR/projects`),填错时当场报错,不会让讨论在轮到它时暂停;Claude 会话只能在原目录续接,未指定项目时沿用原目录,项目目录不同则拒绝。Codex 会话若正被 App 持有,CLI 续接报 active writer,进度看板在暂停原因下提供“切换到 Codex App”,保存采用后可在弹窗内重启。常驻服务每轮检查成员视图:已注册(`registered_at` 非空)且带 `reported_session` 的手动 Claude 专家,`adopt_member_profile` 在本机配置登记其借用会话(会话须存在于 `CLAUDE_CONFIG_DIR/projects` 且 Claude 软件已配置),随后调用 `adopt` 把成员转为托管;任一步失败则保持手动并记录一次日志,同一会话 ID 不重试。讨论配置只新增参与者时常驻服务直接采用,讨论不暂停;已有参与者的项目、cwd 或身份变化仍须暂停后重新接入。
320
320
 
321
+ 建房时可先指定项目目录(cwd):没有本机唤醒的成员也会登记该房间,之后加入的成员沿用此目录。
322
+
321
323
  讨论房间同样登记一个 `FreeRoomRunner`(`DiscussionWithQuestions`):只投递用户的直接提问(`ask`),任务文件说明“这不是发言安排,不改变讨论进度”;与讨论执行互斥由网站保证。
322
324
 
323
325
  每次检查读取 `inbox`:成员没有进行中的执行且有待投递消息时,先持久化本地 `client_id` 再领取执行,然后用原适配器续接该成员的绑定会话;任务文件列出执行编号、待处理消息和已读起点,凭据文件只含成员凭证。进程退出而网站未收到回答、软件报错或超过单次执行时限时报告失败;只有确认未送达才退回队列重试,最多 3 次。领取后、调用前中断的投递不自动重发。主持人的任务文件带 `role: "host"` 和每条请求的 `request_purposes`(`ask` / `route` / `handoff`),说明里写明如何用 `--to` 派活与何时结束。网站上执行被打断、成员离开或被移除后,只停止该成员的进程;不同成员各自投递,可以并行。成员配置了 cwd 且位于 Git 工作区时,执行前后各做一次快照,执行结束且进程退出后报告改动的文件,供网站提示可能冲突;服务重启会丢失尚未报告的快照。只能在网页创建自由房间,不支持对话入口。
package/docs/workflow.md CHANGED
@@ -35,7 +35,7 @@
35
35
 
36
36
  新建房间时可勾选“智能调度”(可选,需本机服务,供应商为 TypeSafe Jev 或 OpenAI Decisions,按次计费)。开启后,没点名的消息仍照常交给主持人;决策模型同时在后台判断,若有更适合回答的成员,就在消息旁提示“智能调度建议让 B 回答”,60 秒内可采纳:打断主持人改由 B 回答,或等主持人答完再请 B 回答。不采纳则提示自动消失,一切照旧。目前只能在建房时开启。开启智能调度只需连接本机服务,成员可以全是手动接入或网页成员:本机服务仍登记该房间并运行决策;被推荐的手动或网页成员与其他成员一样,采纳后在成员栏出现待处理请求,由你或其自行处理(网页成员需你复制通知并代发)。
37
37
 
38
- 需要深入讨论时,在成员栏点“发起头脑风暴”:用当前由本机唤醒的成员新建一场全自动讨论房间,手动和网页成员不随行,轮次和时长独立。讨论收束后,在议题卡片点“把结论发回”,结论会作为留言回到自由房间,不唤醒任何 AI。
38
+ 需要深入讨论时,在成员栏点“开启讨论”(或建房时勾选“创建后开启讨论”):在弹窗里填目标、轮次和时长等上限,由本机唤醒的主持人安排成员轮流发言。到限、结束或用户点“回到自由协作”后,房间继续自由协作,成员、历史与结论保留,可再次开启;不再另建讨论房间。
39
39
 
40
40
  多位成员共享同一个工作目录时不加锁,由你划分各自负责的文件。成员栏会提示“可能的改动冲突”:两位成员在重叠时段都改动了同一文件时列出文件名。提示依据执行前后的 Git 工作区对比,无法确定修改者,未出现提示也不代表没有覆盖;非 Git 目录不提供提示。
41
41
 
@@ -100,7 +100,7 @@ AI 不借用用户身份发言;用户本人留言不受安排限制,话题
100
100
 
101
101
  主持人可以一次排好本轮顺序:先 `turn roster` 申请最新专家状态(在场专家、本轮待发言者 `waiting` 与 `revision`),再 `turn create --plan-file … --roster <revision>`。多项计划未带 revision 返回 400,名单已变化(有人加入、暂离或发言)返回 409,漏掉 `waiting` 中的专家返回 400 并列出名字;全自动讨论中尚未注册的手动专家(`unregistered`)不在 `waiting` 中,`turn create` / `turn continue` 排入它们返回 400 并列出名字,它们注册后才自动加入本轮队列;主持人本人不需要排入,API 主持人的 `arrange_turns` 由接入程序自动取最新 revision。第一项立即生效,其余为 `queued`。专家保存回答后,网站在同一事务中把下一位排队专家的安排转为当前;末位答完才交回主持人。接力不发通知,需要逐个转交的在线专家不放进整轮。
102
102
 
103
- 用户留言或修改目标、背景、边界、焦点会记为待主持人处理(`held_since`),不打断正在发言的专家;该专家保存回答后交回主持人而不接力。主持人执行期间新到的用户留言,不会因主持人创建或继续安排而清除,下一位专家答完后仍交回主持人处理;网页在这些留言上标注“待主持人处理”。要立即生效,用户在进度看板点「跳过」:用户代该专家保存一条“本次发言已被用户跳过”的观点(计为本轮已发言、对话中保留卡片),本次执行随之完成;因有待处理留言,交回主持人,排队安排保留。主持人处理后用 `turn continue` 按原计划继续,或直接创建新安排,剩余排队改为 `skipped`。建房时开启“智能调度”的讨论,留言由决策模型先判断:有把握是“仅补充信息”就按原计划继续,是在问某位专家就先请该专家回应,其余情况才交回主持人;每次判断都在对话中留一行“智能调度”记录。暂停保留当前与排队安排,点“重启”由原专家继续;若要跳过当前安排,可点“跳过”,保存跳过发言后恢复讨论,按原计划交给下一位(无排队则交回主持人);邀请只插入补齐位置,提前结束、到限处理、收束、更换主持人或专家暂离、请回、移除才会作废全部排队安排。
103
+ 用户留言或修改目标、背景、边界、焦点会记为待主持人处理(`held_since`),不打断正在发言的专家;该专家保存回答后交回主持人而不接力。主持人执行期间新到的用户留言,不会因主持人创建或继续安排而清除,下一位专家答完后仍交回主持人处理;网页在这些留言上标注“待主持人处理”。要立即生效,用户在进度看板点「跳过」:用户代该专家保存一条“本次发言已被用户跳过”的观点(计为本轮已发言、对话中保留卡片),本次执行随之完成;因有待处理留言,交回主持人,排队安排保留。主持人处理后用 `turn continue` 按原计划继续,或直接创建新安排,剩余排队改为 `skipped`。建房时开启“智能调度”的讨论,留言由决策模型先判断:有把握是“仅补充信息”就按原计划继续,是在问某位专家就先请该专家回应,其余情况才交回主持人;每次判断都在对话中留一行“智能调度”记录。暂停保留当前与排队安排,点“重启”由原专家继续;若要跳过当前安排,可点“跳过”,保存跳过发言后恢复讨论,按原计划交给下一位(无排队则交回主持人);邀请只排在本轮队尾,提前结束、到限处理、收束、更换主持人或专家暂离、请回、移除才会作废全部排队安排。
104
104
 
105
105
  在线专家由主持人以自身操作者身份、指定原专家代发。正常推进不超时夺取;到限会暂停,取消未完成安排并撤销旧自动执行。
106
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.2.50",
3
+ "version": "0.2.58",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -222,8 +222,8 @@ function launch_input(input) {
222
222
  const members = value.room.members;
223
223
  const names = members?.map(member => typeof member === 'string' ? member : member.name);
224
224
  const woken = Array.isArray(members) ? members.filter(woken_member).map(member => typeof member === 'string' ? member : member.name) : [];
225
- if ((!woken.length && !value.scheduler) || !names?.length || new Set(names).size !== names.length || woken.some(name => !clients_of(name))) {
226
- throw new Error('自由协作房间须选择至少 1 位支持自动接入的 AI 成员(或开启智能调度),且不能重复');
225
+ if ((!woken.length && !value.scheduler && !value.cwd) || !names?.length || new Set(names).size !== names.length || woken.some(name => !clients_of(name))) {
226
+ throw new Error('自由协作房间须选择至少 1 位支持自动接入的 AI 成员(或开启智能调度、指定项目目录),且不能重复');
227
227
  }
228
228
  }
229
229
  if (value.access_file) {
@@ -29,7 +29,7 @@ export const CODEX_ADAPTERS = ['codex', 'codex_cli'];
29
29
  const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
30
30
  /** Project files in cwd are read-only unless the launch or participant explicitly grants write. */
31
31
  const PROJECT_ACCESS = ['read', 'write'];
32
- const ACTIVE_PHASES = ['initial', 'discussing', 'reviewing'];
32
+ const ACTIVE_PHASES = ['initial', 'discussing'];
33
33
  const MAX_PLAN_TURNS = 10;
34
34
  const CONTEXT_MESSAGES = 12;
35
35
  /** Session recorded when smart scheduling performs the host's writes without waking its software. */
@@ -71,8 +71,8 @@ export function validate_config(input, base_directory) {
71
71
  if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw new Error('站点必须是无路径和凭据的 HTTP(S) 根地址');
72
72
  if (!/^[A-Za-z0-9_-]+$/.test(config.room ?? '') || typeof config.token !== 'string' || config.token.length < 32) throw new Error('缺少房间及本房间凭据');
73
73
  if (!config.project || !existsSync(join(config.project, 'runner', 'discuss.py'))) throw new Error('cli_project 必须指向 AI CoDiscuss 项目;省略时默认使用本仓库');
74
- // A free room of manual members runs here only for smart scheduling, which needs no local software.
75
- if (!config.participants || (!Object.keys(config.participants).length && !(config.mode === 'free' && config.scheduler))) throw new Error('请配置本机参与软件');
74
+ // A free room of manual members runs here only for smart scheduling or a preset project directory, which need no local software.
75
+ if (!config.participants || (!Object.keys(config.participants).length && !(config.mode === 'free' && (config.scheduler || config.cwd)))) throw new Error('请配置本机参与软件');
76
76
  for (const participant of Object.values(config.participants)) {
77
77
  if (participant.adapter === 'api') {
78
78
  read_api_key(config.api_home, participant);
@@ -181,6 +181,8 @@ export class DiscussionStore {
181
181
  binding(participant) { return this.db.prepare('SELECT session_id FROM bindings WHERE participant=?').get(participant) || {}; }
182
182
  /** Commit a returned session before later notifications can use it. */
183
183
  bind(participant, session_id) { this.db.prepare('INSERT INTO bindings VALUES (?,?) ON CONFLICT(participant) DO UPDATE SET session_id=excluded.session_id').run(participant, session_id); }
184
+ /** The failure recorded for one task now; a row read earlier in the tick may predate it. */
185
+ recorded_error(id) { return this.db.prepare('SELECT error FROM inbox WHERE id=?').get(id)?.error || ''; }
184
186
  /** List work that still needs dispatch or reconciliation. */
185
187
  pending() { return this.db.prepare("SELECT * FROM inbox WHERE status NOT IN ('completed','cancelled','failed') ORDER BY id").all(); }
186
188
  /** Release this instance's database connection. */
@@ -502,6 +504,9 @@ export class DiscussionRunner {
502
504
  if (this.exited.has(row.id) && ['dispatched', 'started'].includes(event.status)) {
503
505
  // A business write may have just completed the task; recheck before declaring a silent exit.
504
506
  const latest = await api(this.config, `/events/${row.id}`, { action: 'view' });
507
+ // The child records its own failure just before exiting; prefer that cause over the generic one.
508
+ const cause = row.error || this.store.recorded_error(row.id);
509
+ if (['dispatched', 'started'].includes(latest.status) && cause) return this.fail(row, cause);
505
510
  if (['dispatched', 'started'].includes(latest.status)) return this.fail(row, `${event.participant} 会话已结束,但未保存本次结果${this.exited.get(row.id) ? `:${this.exited.get(row.id)}` : ''};请核对原会话确认是否已有内容`);
506
511
  return this.reconcile(row, latest);
507
512
  }
package/web/app.js CHANGED
@@ -35,7 +35,7 @@ const DEFAULT_USER_ICON = "/icons/user.svg";
35
35
  const MANAGEMENT_LOGIN_HINT = "需要登录管理员账号或使用创建该房间的浏览器";
36
36
  const THEMES = ["channel", "channel-light"];
37
37
  const SOURCE_LABELS = { demo: "示例", user: "本人发言", proxy: "用户代发", cli: "CLI 发言", api: "API 发言", url: "历史 URL 发言", host_proxy: "主持人代发" };
38
- const PHASE_LABELS = { preparing: "准备中", initial: "独立观点", discussing: "深入讨论", reviewing: "结论复核", paused: "已暂停", concluded: "已收束" };
38
+ const PHASE_LABELS = { preparing: "准备中", initial: "独立观点", discussing: "深入讨论", paused: "已暂停", concluded: "已收束" };
39
39
  const TURN_LABELS = { pending: "待处理", notified: "已通知,待回答", answered: "已回答", cancelled: "已取消" };
40
40
  const PLATFORM_IDENTITIES = {
41
41
  "用户": { label: "User", initials: "U", style: "user", icon: "/icons/user.svg", description: "发起与协助" },
@@ -1524,7 +1524,7 @@ async function submit_room(event, folder_id = null) {
1524
1524
  const free = payload.mode === "free";
1525
1525
  // A discussion can still be created without the local service and started later from the room;
1526
1526
  // a free room needs the service only for members it wakes or for smart scheduling.
1527
- const launching = free ? Object.values(payload.experts).includes("cli") || (state.launch.available && Boolean(creation_launch_options().scheduler)) : state.launch.create && state.launch.available;
1527
+ const launching = free ? Object.values(payload.experts).includes("cli") || (state.launch.available && Boolean(creation_launch_options().scheduler || state.launch.workspace.cwd)) : state.launch.create && state.launch.available;
1528
1528
  if (launching && !state.launch.available) throw new Error("本机唤醒的成员需要连接本机服务;也可改为手动接入或网页代发");
1529
1529
  const launch_draft = launching ? creation_launch_options() : null;
1530
1530
  // A discussion is only a way to push the room forward: the room is created as a free room, then opened with these limits.
@@ -2396,10 +2396,10 @@ async function conclude_discussion() {
2396
2396
  }
2397
2397
 
2398
2398
  /** Turn this free room into a discussion within the given limits and start the run; the local service takes it over at its next check. */
2399
- async function start_room_discussion({ goal, max_rounds, max_minutes }) {
2399
+ async function start_room_discussion({ goal, max_rounds, max_minutes, execution_minutes = 0, named_followup = false }) {
2400
2400
  const room_id = state.room.id, path = `/api/rooms/${room_id}`;
2401
2401
  if (!localStorage.getItem(`discuss-access:${room_id}`)) throw new Error("此浏览器缺少房间控制凭据,请使用创建房间的浏览器");
2402
- await request_json(`${path}/discussion`, { action: "start", ...(goal ? { goal } : {}), limits: { max_rounds, max_minutes } });
2402
+ await request_json(`${path}/discussion`, { action: "start", ...(goal ? { goal } : {}), limits: { max_rounds, max_minutes, execution_minutes }, named_followup });
2403
2403
  const runtime = await request_json(`${path}/runtime`);
2404
2404
  await request_json(`${path}/runtime`, { action: "start", mode: "automatic", revision: runtime.revision, deadline: 0 });
2405
2405
  await refresh_room();
package/web/index.html CHANGED
@@ -56,7 +56,7 @@
56
56
  <details class="topic-details" id="host-management"><summary>主持管理 · 修改话题与安排发言</summary><div class="topic-detail-content">
57
57
  <p class="local-note" id="host-operator"></p>
58
58
  <form id="turn-form" class="topic-form"><h3>安排下一位专家</h3><label>发言专家<select id="turn-expert-select"></select></label><label>需要回应的问题<textarea id="turn-question-input" maxlength="1000" rows="3" required placeholder="只写本轮需要回答的问题,不粘贴上下文。"></textarea></label><button class="button primary" id="schedule-turn" type="submit">创建发言安排</button><p id="schedule-note"></p></form>
59
- <section class="info-card"><h3>讨论上限与进度</h3><p id="discussion-limits-status"></p><button id="check-discussion-limits" class="button secondary" type="button">检查并执行到限处理</button></section><form id="topic-form" class="topic-form"><h3>话题资料与进度</h3><div class="topic-form-grid"><label>话题名称<input id="topic-title-input" maxlength="80" required></label><label>主持人<select id="topic-host-select"></select></label><label>阶段<select id="topic-phase-select"><option value="preparing">准备中</option><option value="initial">独立观点</option><option value="discussing">深入讨论</option><option value="reviewing">结论复核</option><option value="paused">已暂停</option><option value="concluded">已收束</option></select></label><label>当前焦点<input id="topic-focus-input" maxlength="1000"></label></div><label>议题目标<textarea id="topic-goal-input" maxlength="1000" rows="2"></textarea></label><label>用户原文<textarea id="topic-user_request-input" maxlength="4000" rows="3"></textarea></label><label>背景<textarea id="topic-background-input" maxlength="4000" rows="3"></textarea></label><label>讨论边界<textarea id="topic-boundaries-input" maxlength="4000" rows="3"></textarea></label><fieldset><legend>讨论上限</legend><div class="topic-form-grid"><label>讨论轮次上限<input id="topic-limit-max_rounds-input" type="number" min="0" max="100" step="1" value="3" placeholder="留空不限"></label><label>讨论总时长(分钟)<input id="topic-limit-max_minutes-input" type="number" min="0" max="1440" step="1" value="60" placeholder="留空不限"></label><label>单次 AI 执行上限(分钟,仅自动)<input id="topic-limit-execution_minutes-input" type="number" min="0" max="60" step="1" value="0" placeholder="留空不限"></label></div><p class="local-note">轮次、总时长、单次执行留空或 0 表示不限;首次进入讨论阶段或创建安排后开始计时,暂停期间不计时;到限后暂停,由用户决定追加轮次或结束。一轮指每位参与专家各完成一次观点。</p></fieldset><fieldset><legend>参与专家</legend><div id="topic-expert-modes" class="expert-mode-grid"></div></fieldset><p class="local-note">AI 主持人须配置为本地 CLI,可同时作为专家。主持发言与个人观点分别记录。</p><button class="button primary" type="submit">保存话题设置</button></form>
59
+ <section class="info-card"><h3>讨论上限与进度</h3><p id="discussion-limits-status"></p><button id="check-discussion-limits" class="button secondary" type="button">检查并执行到限处理</button></section><form id="topic-form" class="topic-form"><h3>话题资料与进度</h3><div class="topic-form-grid"><label>话题名称<input id="topic-title-input" maxlength="80" required></label><label>主持人<select id="topic-host-select"></select></label><label>阶段<select id="topic-phase-select"><option value="preparing">准备中</option><option value="initial">独立观点</option><option value="discussing">深入讨论</option><option value="paused">已暂停</option><option value="concluded">已收束</option></select></label><label>当前焦点<input id="topic-focus-input" maxlength="1000"></label></div><label>议题目标<textarea id="topic-goal-input" maxlength="1000" rows="2"></textarea></label><label>用户原文<textarea id="topic-user_request-input" maxlength="4000" rows="3"></textarea></label><label>背景<textarea id="topic-background-input" maxlength="4000" rows="3"></textarea></label><label>讨论边界<textarea id="topic-boundaries-input" maxlength="4000" rows="3"></textarea></label><fieldset><legend>讨论上限</legend><div class="topic-form-grid"><label>讨论轮次上限<input id="topic-limit-max_rounds-input" type="number" min="0" max="100" step="1" value="3" placeholder="留空不限"></label><label>讨论总时长(分钟)<input id="topic-limit-max_minutes-input" type="number" min="0" max="1440" step="1" value="60" placeholder="留空不限"></label><label>单次 AI 执行上限(分钟,仅自动)<input id="topic-limit-execution_minutes-input" type="number" min="0" max="60" step="1" value="0" placeholder="留空不限"></label></div><p class="local-note">轮次、总时长、单次执行留空或 0 表示不限;首次进入讨论阶段或创建安排后开始计时,暂停期间不计时;到限后暂停,由用户决定追加轮次或结束。一轮指每位参与专家各完成一次观点。</p></fieldset><fieldset><legend>参与专家</legend><div id="topic-expert-modes" class="expert-mode-grid"></div></fieldset><p class="local-note">AI 主持人须配置为本地 CLI,可同时作为专家。主持发言与个人观点分别记录。</p><button class="button primary" type="submit">保存话题设置</button></form>
60
60
  </div></details>
61
61
  </section>
62
62
  <section class="discussion-layout panel" data-panel="discussion" hidden>