ai_co_discuss 0.2.35 → 0.2.36
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 +1 -1
- package/backend/runtime.py +10 -0
- package/backend/server.py +4 -0
- package/docs/architecture.md +4 -4
- package/docs/cli.md +1 -1
- package/docs/runner.md +1 -1
- package/docs/workflow.md +2 -2
- package/package.json +1 -1
- package/runner/api_participant.mjs +24 -2
- package/runner/discuss.py +8 -1
- package/runner/discussion_api.mjs +4 -2
- package/web/app.js +54 -5
- package/web/modern.css +1 -1
- package/web/modern.js +1504 -1415
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ codiscuss local connect https://你的站点
|
|
|
113
113
|
|
|
114
114
|
下次打开会保留原讨论,已暂停的房间需在进度看板明确重启。网页已连接或创建成功不代表 AI 已回答,应核对实际执行进展与讨论记录。独立服务管理、停止归属及故障排查见 [接入说明](docs/runner.md#常驻讨论服务);私有配置需保留在原位置。
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
软件失败时先查看暂停原因,登录和权限问题在对应软件中处理。仍保留当前发言安排时,可点“跳过”,取消这次发言并恢复讨论,由主持人安排后续;“重启”则重试原安排。本机网站的已登记房间,可从进度看板 Codex 旁的“…”菜单直接选择“切换 App / CLI”,在弹窗内暂停并保存;软件路径在“接入 → 修正本场软件连接”修改。原会话与讨论进度保留,待服务确认采用后重启;线上连接暂不提供此修正入口。切到 App 后模型和文件权限按 App 设置,切回 CLI 前须让 App 释放原聊天。本场修改不影响后续讨论的默认配置,详见 [配置修正](docs/runner.md#已启动后的配置修正)。
|
|
117
117
|
|
|
118
118
|
**手动接入与网页 AI:** 手动接入可用于“自由协作”和“讨论”的专家(讨论的主持人必须由本机唤醒),在成员卡片正面的“自动 / 手动 / 网页”切换里选择。手动成员从成员菜单下载自己的凭据文件,或复制同样内容的凭证文本(设为环境变量 `DISCUSS_ACCESS`),连同参与说明私下交给对应 AI 会话,由人工或软件原生定时触发;讨论里的手动专家只在被安排发言时参与,没有及时回答时消息区显示等待发言卡片,可在进度看板点“跳过”本次发言,交给主持人继续安排;网页 AI 仍只用于自由协作,网页 AI 只能阅读房间,回答由你代发;它从自己的服务器读取阅读链接,只在本机运行的网站(`127.0.0.1`)它读不到,需由你粘贴材料。只有手动和网页成员时建房无需本机接入服务。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
|
|
119
119
|
|
package/backend/runtime.py
CHANGED
|
@@ -106,10 +106,20 @@ def runtime_view(connection, room_id):
|
|
|
106
106
|
event = connection.execute("SELECT id,participant,role,status,result FROM discussion_events WHERE room_id=? ORDER BY id DESC LIMIT 1", (room_id,)).fetchone()
|
|
107
107
|
result["latest_execution"] = dict(event) if event else None
|
|
108
108
|
result["participants"] = participant_executions(connection, room_id)
|
|
109
|
+
result["failures"] = failed_executions(connection, room_id)
|
|
109
110
|
result["timings"] = execution_timings(connection, room_id, row["run_id"])
|
|
110
111
|
return result
|
|
111
112
|
|
|
112
113
|
|
|
114
|
+
def failed_executions(connection, room_id):
|
|
115
|
+
"""Keep failed attempts readable after a retry, without exposing execution credentials."""
|
|
116
|
+
return [dict(row) for row in connection.execute("""SELECT e.id,e.participant,e.role,e.turn_id,
|
|
117
|
+
e.result,e.created_at,CASE WHEN t.dispatched_at>0 AND t.finished_at>0
|
|
118
|
+
THEN MAX(0,t.finished_at-t.dispatched_at) END AS elapsed_seconds
|
|
119
|
+
FROM discussion_events AS e LEFT JOIN event_timings AS t ON t.event_id=e.id
|
|
120
|
+
WHERE e.room_id=? AND e.status='failed' ORDER BY e.id""", (room_id,))]
|
|
121
|
+
|
|
122
|
+
|
|
113
123
|
def execution_timings(connection, room_id, run_id):
|
|
114
124
|
"""Summarize stage durations of the current run so slow stages are visible."""
|
|
115
125
|
now = int(time.time())
|
package/backend/server.py
CHANGED
|
@@ -1610,6 +1610,10 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1610
1610
|
raise LookupError("地址不存在")
|
|
1611
1611
|
room_id = parts[2]
|
|
1612
1612
|
token = self.headers.get("X-Discussion-Token", "")
|
|
1613
|
+
if self.headers.get("X-Discussion-Session"):
|
|
1614
|
+
record_reported_session(connection, room_id, token, {"session_id": self.headers.get("X-Discussion-Session"),
|
|
1615
|
+
"session_client": self.headers.get("X-Discussion-Session-Client")})
|
|
1616
|
+
connection.commit()
|
|
1613
1617
|
free = room_mode(connection, room_id) == "free"
|
|
1614
1618
|
if free and parts[3:4] in (["inbox"], ["executions"], ["members"], ["suggestions"], ["recommendations"]):
|
|
1615
1619
|
return self.handle_free_api(connection, parts, method)
|
package/docs/architecture.md
CHANGED
|
@@ -181,11 +181,11 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
|
|
|
181
181
|
|
|
182
182
|
浮层(菜单、预览、消息工具条、抽屉与弹窗)在深色频道使用毛玻璃:半透明上亮下暗渐变底配合 24px 背景模糊与增艳,边缘只用顶部内侧亮线、底部内侧暗线和向下投影,不加外侧白色辉光。浅色频道的控件本体保持不透明白色,只学习材质边界:蓝灰细描边、内侧顶部高光、内侧底部浅蓝灰暗边和蓝灰多层投影,按控件、面板、浮层三级加强。外观设置的「视觉效果」可切换为精简,写入根节点 `data-effects="reduced"` 并随布局偏好保存在浏览器;精简模式全局关闭背景模糊,深色浮层改为实色,深浅阴影各退为单层,用于保证低配设备的流畅度。浅色频道的发言快速导航不加底板,刻度改为发言者实色、固定 2px 粗细(覆盖脚本动画的内联高度,长度与透明度仍由脚本驱动),轮次号与轮次分隔比辅助文字深一级。
|
|
183
183
|
|
|
184
|
-
选中状态用填色、文字和勾选表达,键盘焦点使用中性边缘,不增加蓝色外框;卡片、模型选择、强度滑杆、模式、时长及编辑区遵循同一规则。加载图标在各处统一为每 3
|
|
184
|
+
选中状态用填色、文字和勾选表达,键盘焦点使用中性边缘,不增加蓝色外框;卡片、模型选择、强度滑杆、模式、时长及编辑区遵循同一规则。加载图标在各处统一为每 3 秒匀速旋转一圈;系统开启“减少动态效果”时仍保留缓慢旋转,以表示请求正在进行,其余动效继续遵循减弱设置。
|
|
185
185
|
|
|
186
186
|
接入页分别交付本地成员的私有配置文件和不含 token 的参与说明,在线成员只给阅读说明;前端快照仅含成员接入方式与文件可用状态。手动创建、邀请、请回直接请求网站,自动成员才经本机启动器。创建与成员变更在请求前保存原编号和凭据,结果不明时沿用;复制说明不代表已送达或接入。项目、工作目录、讨论上限和错误按需展开。暂停后的本场软件连接修正使用独立卡片,页面不推断本机服务在线状态,选择模式或下载配置不会启动讨论。
|
|
187
187
|
|
|
188
|
-
消息区显示议题、原文和主持简报;议题卡的参与配置使用与头像一致的 20px 行高,姓名与主持 / CLI 标签保留字号层级并垂直居中。全自动执行期间,消息流末尾按有效执行显示等待状态:专家复用发言卡片的头像、姓名、轮次与样式,显示“正在唤醒…”或“正在准备回复…”;主持执行使用同宽、更矮、弱化底色和边框的“主持工作”卡,显示头像、姓名及右上角耗时,按交付与进入状态显示“正在唤醒主持人…”或“正在安排下一步…”,不推测内部操作。卡片类型按本次执行职责区分,主持人作为专家回答时仍使用专家等待卡;正式主持发言保留普通消息卡。手动接入专家轮到发言时,即使没有自动执行,也按当前安排显示“等待发言”卡片,提示到该 AI
|
|
188
|
+
消息区显示议题、原文和主持简报;议题卡的参与配置使用与头像一致的 20px 行高,姓名与主持 / CLI 标签保留字号层级并垂直居中。全自动执行期间,消息流末尾按有效执行显示等待状态:专家复用发言卡片的头像、姓名、轮次与样式,显示“正在唤醒…”或“正在准备回复…”;主持执行使用同宽、更矮、弱化底色和边框的“主持工作”卡,显示头像、姓名及右上角耗时,按交付与进入状态显示“正在唤醒主持人…”或“正在安排下一步…”,不推测内部操作。卡片类型按本次执行职责区分,主持人作为专家回答时仍使用专家等待卡;正式主持发言保留普通消息卡。手动接入专家轮到发言时,即使没有自动执行,也按当前安排显示“等待发言”卡片,提示到该 AI 会话发送启动指令;标注“手动接入”,不显示执行动画或耗时。输入区的“复制启动指令”使用无底色、无描边的复制图标文字按钮,复制成功后原位显示“已复制”。自动成员仅有待处理安排、执行过期、暂停或归档时不显示等待卡片。卡片不计入消息记录,没有消息编号、回复或引用操作;执行成功或取消、手动安排已回答或取消后移除;失败执行保留为时间顺序中的卡片,显示失败原因、执行编号及已记录耗时,刷新、重试和收束后仍可查看。拥有控制凭据的浏览器可在当前失败卡片上点“重试”,沿用保留的安排创建新的执行;旧失败卡不消失,也不会作为 AI 原文计数,历史安排已变化时不提供重试;下一位自动专家实际交付、手动专家轮到发言时显示对应卡片。右侧进度看板的“跳过”取消当前专家本次安排及有效执行,保留排队安排并交给主持人继续安排,不暂停讨论。操作按钮保持单行排列。因执行失败或用户操作而暂停、且仍保留当前安排时,仍可点“跳过”:先取消该安排,再按当前运行版本恢复讨论,唤醒主持人而不重试被跳过的专家;恢复失败时说明安排已跳过,可核对后用“重启”继续。搜索与提及筛选时隐藏,状态更新仅在用户原本位于底部时跟随滚动。标题栏、窄屏标签栏与主持简报合为同一块顶部面板,简报展开内容留在面板内,与收起状态共用背景、边线与投影,不再另起浮层。消息视图中该面板浮在消息列表之上,使用与深色浮层同款的毛玻璃(半透明渐变底、24px 背景模糊与增艳;浅色为白色半透明),滚动经过的卡片在其下模糊可见;消息列表顶部留白、导航与筛选条下移量、跳转与当前消息判定都按收起时的面板高度计算,展开简报只覆盖消息,不推动消息与输入区。其他视图中面板按普通流式布局排列。精简视觉效果下面板改为实色。已存档房间在独立弹窗管理,不改变当前房间和草稿。存档房间仍可阅读、恢复与导出;批量删除作用于当前搜索和筛选结果,执行前确认。
|
|
189
189
|
|
|
190
190
|
所有发言卡片右上角显示耗时。等待卡片使用同一执行的 `runtime.timings` 唤醒与执行秒数之和,从交付唤醒起累计,不计之前排队;本地每秒更新,服务器计时变化时校准,状态结束或切换房间后停止。正式发言的 `elapsed_seconds` 在保存消息的同一事务内按有效执行凭据与原作者关联计算,保存在 `message_timings`,此后翻页、重试和运行结束均不改变。主持发言同样计到各自保存时刻;不接受客户端提供的耗时。用户留言、手动或代发内容及缺少记录的旧消息显示“耗时 —”。升级仅为有精确 `message:ID` 完成结果与计时记录的历史专家发言回填,不按时间邻近推测关联,不覆盖已有耗时。
|
|
191
191
|
|
|
@@ -237,7 +237,7 @@ running 状态即使只有 pending 事件,也必须先暂停才能重新启动
|
|
|
237
237
|
|
|
238
238
|
两种房间的用户写入、运行控制及成员管理都必须携带建房时的房主控制凭据;`actor`、`author`、`submitted_by` 只是声明。`room_modes` 为所有新房间保存控制哈希,`room_members` 保存每个实例的独立成员哈希、能力和主持职责。讨论房间的手动本地成员用自己的持久成员凭证;在线成员不能自行写入,由房主或获授主持职责的本地成员代发。成员主持可维护资料、摘要、阶段和安排,不能改名单、授予主持权限、控制自动运行或删除房间。房主凭据不能冒充本地成员提交观点。自动讨论里本机唤醒的成员仍要求本次有效执行凭据,持久成员凭证不能替代执行租约;手动接入的专家(`access=manual`、`adapter=manual`,主持人除外)用自己的成员凭证回答安排,其待派发事件由接入程序跳过,回答保存后由触发器完成事件并唤醒主持人。
|
|
239
239
|
|
|
240
|
-
房间响应的 `runtime.timings` 汇总当前运行各执行的阶段耗时:排队(事件产生到 reserve)、唤醒(reserve 到 AI enter)、执行(enter 到完成、撤销或失败),未结束的阶段计到当前时间;返回最近 20 条与全部合计,进度看板据此展示。升级前的执行没有计时记录,不计入。
|
|
240
|
+
房间响应的 `runtime.timings` 汇总当前运行各执行的阶段耗时:排队(事件产生到 reserve)、唤醒(reserve 到 AI enter)、执行(enter 到完成、撤销或失败),未结束的阶段计到当前时间;返回最近 20 条与全部合计,进度看板据此展示。升级前的执行没有计时记录,不计入。 `runtime.failures` 单独返回本房间全部失败执行的公开字段(编号、参与者、职责、安排、原因、创建时间与交付到失败的固定耗时),不随新执行覆盖,也不受最近 20 条计时截断影响;旧执行没有计时数据时耗时为 null。前端重试前重读房间,核对最新失败执行与当前保留的安排,再使用现有 runtime start 和 revision 校验恢复;无需新增表或更改执行重试协议。
|
|
241
241
|
|
|
242
242
|
每房间最多一个 dispatched/started 执行;写入同时校验运行代次、期限、身份及职责。租约 120 秒,通知程序每 30 秒续租,单步采用 discussion_limits.execution_minutes,新建默认 0(不限),非零值为 1–60 分钟;设置了整场截止时取更早期限。不限单步时长仍要求有效的租约、运行代次和身份,续租不设隐含执行截止。schema 重建建房触发器并显式写入 0,使旧表默认值为 20 的数据库中新建房间也默认不限,已有房间的配置保持不变。通知受理未必开始,开始未必完成。运行和最新执行状态对网页可见,控制哈希与执行哈希不公开。备份保持原有讨论数据范围,不导出运行凭据或本机绑定。
|
|
243
243
|
|
|
@@ -248,7 +248,7 @@ running 状态即使只有 pending 事件,也必须先暂停才能重新启动
|
|
|
248
248
|
自由房间(`room_modes.mode='free'`)与讨论房间共用房间、成员登记、消息、附件与目录,没有发言权、安排、阶段推进和轮次。没有控制凭据登记的旧房间只供阅读,不支持通过首次运行配置认领;本次不迁移历史房间。
|
|
249
249
|
|
|
250
250
|
- **凭据**:建房时由客户端提供房间控制凭据和每位成员的成员凭证,服务端只存哈希。用户身份的房间写入(发言、附件、存档、恢复、删除、打断)都须控制凭据;房间排序和归入文件夹只改共享目录,与站点级文件夹操作一样不需要凭据;无凭据、错误凭据、成员凭证或他房凭据一律只读。成员凭证只能以该成员身份发言或上传附件。声明字段(`author`、`source`、localhost)不代替凭据。
|
|
251
|
-
- **成员**:`room_members` 以 `id` 为稳定主键,`access` 为 `runner`(接入程序投递)、`manual`(成员自行读写)或 `online`(只读、原文代发),`session_mode` 标记是否借用用户正在使用的会话。房主用控制凭据 `POST members` 运行中加入成员,`POST members/ID {action}` 让成员离开(`away`,作废其进行中执行与未投递请求)、请回(`active`,必须提供不同于原凭证的新凭证,旧凭证副本不再有效)或移除(`remove
|
|
251
|
+
- **成员**:`room_members` 以 `id` 为稳定主键,`access` 为 `runner`(接入程序投递)、`manual`(成员自行读写)或 `online`(只读、原文代发),`session_mode` 标记是否借用用户正在使用的会话。房主用控制凭据 `POST members` 运行中加入成员,`POST members/ID {action}` 让成员离开(`away`,作废其进行中执行与未投递请求)、请回(`active`,必须提供不同于原凭证的新凭证,旧凭证副本不再有效)或移除(`remove`,保留历史发言)。同一成员凭证不能复用于其他成员或房主;相同凭证与配置的邀请、请回重试返回已有结果。会话续接由本机接入程序负责,服务端不保存会话绑定;唯一例外是手动成员上报的会话:带凭据的请求可带请求头 `X-Discussion-Session` 与 `X-Discussion-Session-Client`,`message post` 也可在正文带 `session_id` 与 `session_client`(CLI 读取 `CLAUDE_CODE_SESSION_ID` 或 `CODEX_THREAD_ID`,取值 `claude` / `codex`),服务端在 `member_sessions` 保存该手动成员最近上报的会话,成员视图以 `reported_session` 暴露。房主用 `POST members/ID {action:"adopt", adapter:"claude", session_id}`(须与上报一致、仅全自动讨论中在场的手动成员)把成员改为 `runner`、`borrowed` 并清除上报,本机接入程序据此接管。两套后端行为一致。
|
|
252
252
|
- **请求**:用户消息按结构化接收者 `to`(成员名)为每位成员生成一条请求;省略且仅有一位在场成员时默认点名该成员,多位成员时交给主持人(`route`),只有在线成员而没有主持人时只作留言,不广播。网页只从正文中代码与引用之外的 `@成员` 生成 `to`,没有 @ 时省略 `to`。请求状态为 `pending`(待投递)→ `delivering`(处理中)→ `completed` / `cancelled` / `failed`。
|
|
253
253
|
- **执行**:`member_executions` 每位成员最多一个 `dispatched/started`(部分唯一索引),不同成员可并行。接入程序用控制凭据 `GET inbox`、`POST executions {member_id, client_id}`(同一 `client_id` 返回同一执行)把成员的全部待投递请求打包;`POST executions/ID {action}` 支持 `renew`(租约 120 秒,不超过单次执行时限,默认 20 分钟)、`fail`(请求标记失败)和 `release`(仅确认未送达时退回队列)。租约过期的执行在下次读取或领取时标记失败,不自动重发。
|
|
254
254
|
- **主持人与任务链**:用户是需求方,不担任主持人。只要有在场的非在线成员,房间就保持恰好一位主持人:成员变动时,离开或移出的主持人失去角色,由第一位在场的 runner 成员(没有则手动成员)接任;建房和每条房主消息前也做同样的补位,因此旧房间在下一条消息时自动获得主持人。房主用 `POST members/ID {action:"role", role:"host"}` 更换主持人(在线成员不能担任),把现任主持人改为 `member` 会被拒绝。多位成员时未点名的房主消息生成给主持人的 `route` 请求。主持人的回答可带 `to` 点名其他成员(不能点名自己),这些请求组成一个批次(`free_batches`);批次内全部进入终态后,只要有一个完成就给主持人生成一次 `handoff` 请求,全部失败则停下该任务链。每条房主消息开启一条任务链(`chain_id` 为该消息编号),链上 AI 发起的请求(派活与回交)最多 8 次,超出即停下(`free_chains`,原因 `budget` / `all_failed`),房主发新消息开启新链。Worker 用同一批次内的集合 SQL 完成回交,回交请求按(派活消息,主持人)唯一,重复执行不会重复唤醒。
|
package/docs/cli.md
CHANGED
|
@@ -35,7 +35,7 @@ Bash 对应 `export DISCUSS_URL=http://127.0.0.1:8765`,CMD 对应 `set DISCUSS
|
|
|
35
35
|
|
|
36
36
|
建房与更新资料支持 `--user-request` / `--user-request-file`,将用户原文与 `--background` 分开保存,各限 4000 字。`room brief` 返回两者,概要和通知不自动展开原文。
|
|
37
37
|
|
|
38
|
-
`room create` 必须指定 `--access-file 私有文件`,CLI 在请求前原子保存建房编号、房主和各成员凭据,确认房间后另写各本地成员的独立文件,回执只返回 `member_access_files` 路径。结果不明时沿用同一文件、编号和创建参数;不能换文件重建。后续每次写入使用对应身份的文件,不能拿房主文件冒充成员;自动讨论使用本次任务文件中的执行凭据。文件含 `site`、`room`、`token`,CLI 检查站点与房间匹配。同样内容的单行 JSON 文本可放入环境变量 `DISCUSS_ACCESS`(或 `--access-file -` 从标准输入读取),未传 `--access-file` 时使用;成员菜单的“复制凭证文本”复制的就是这段文本,含 token,只交给该成员,不要写进命令行参数或回显。`--dry-run`
|
|
38
|
+
`room create` 必须指定 `--access-file 私有文件`,CLI 在请求前原子保存建房编号、房主和各成员凭据,确认房间后另写各本地成员的独立文件,回执只返回 `member_access_files` 路径。结果不明时沿用同一文件、编号和创建参数;不能换文件重建。后续每次写入使用对应身份的文件,不能拿房主文件冒充成员;自动讨论使用本次任务文件中的执行凭据。文件含 `site`、`room`、`token`,CLI 检查站点与房间匹配。同样内容的单行 JSON 文本可放入环境变量 `DISCUSS_ACCESS`(或 `--access-file -` 从标准输入读取),未传 `--access-file` 时使用;成员菜单的“复制凭证文本”复制的就是这段文本,含 token,只交给该成员,不要写进命令行参数或回显。`--dry-run` 不生成或写入凭据,预览会隐藏凭据字段。CLI 在 Claude Code / Codex 会话中运行时,所有带凭据的请求自动附带请求头 `X-Discussion-Session` 与 `X-Discussion-Session-Client`,`message post` 另在正文带 `session_id` 与 `session_client`(`claude` 取自 `CLAUDE_CODE_SESSION_ID`,`codex` 取自 `CODEX_THREAD_ID`),服务端仅对手动成员保存,供本机服务转为托管;其他情况忽略。
|
|
39
39
|
|
|
40
40
|
接入程序新启动的 CLI 使用 `DISCUSS_ACCESS_FILE` 绑定本次凭据文件,日常读写可省略 `--access-file`;文件每次重新读取,保留会话绑定与房间、站点检查。优先级为显式 `--access-file` → `DISCUSS_ACCESS_FILE` 文件 → `DISCUSS_ACCESS` JSON;已指定文件损坏或缺失时直接报错,不回退其他身份。首次接入已有会话和 App 仍使用交付的文件路径。建房仍须显式指定新凭据的保存路径。
|
|
41
41
|
|
package/docs/runner.md
CHANGED
|
@@ -421,7 +421,7 @@ npm run runner -- watch --config PRIVATE.json
|
|
|
421
421
|
|
|
422
422
|
暂停原因直接展示给用户,所以文案要说明下一步,且“可稍后恢复”不等于“可以重放结果不明的任务”:
|
|
423
423
|
|
|
424
|
-
- API 的 HTTP 错误按状态分流(`api_status_message`):401 / 403 提示核对密钥与模型权限,404 提示核对地址、请求协议和模型 ID,429 与 5xx
|
|
424
|
+
- API 的 HTTP 错误按状态分流(`api_status_message`):401 / 403 提示核对密钥与模型权限,404 提示核对地址、请求协议和模型 ID,429 与 5xx 提示等待恢复后再试,并提醒先核对房间里是否已有本次内容;其他状态保持通用说明。网络超时与连接失败不区分“网络不通”和“地址有误”,两者都在提示里。执行内的模型请求遇到超时、连接失败、429、5xx 时自动重试最多 2 次(间隔 3 秒、6 秒),这类失败发生在房间写入之前;房间写入不重试。模型某轮只回纯文本、未调用工具时,先追加一次提醒让其重试,仍不调用才暂停。
|
|
425
425
|
- 进程退出但结果未保存(讨论为“会话已结束,但未保存本次结果”,自由房间为“未提交本次回答”)与自由房间的“超过单次执行时限”都附带“请核对原会话确认是否已有内容”。
|
|
426
426
|
- 不识别的软件原始错误保持原样,不猜额度恢复时间,不自动重发。
|
|
427
427
|
|
package/docs/workflow.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 选择推进方式
|
|
6
6
|
|
|
7
|
-
- **讨论(全自动)**:主持人与专家都由本机服务唤醒(CLI / App / API)。本机软件与权限配置完成后明确启动,可按轮次、总时长上限或手动停止结束。主持创建正式安排即结束本步;专家自行认领、回答,程序再续接原主持。自动任务不另建原生定时。项目及 cwd 可选,分别属于本机接入配置。本地专家可单独选“手动接入”:成员记录带 `adapter: manual`,不被唤醒,凭自己的成员凭证认领发言权并提交安排内的回答,接入程序不派发它的事件,回答保存后照常续接主持人;主持人必须由本机唤醒。手动专家若运行在 Claude Code 或 Codex
|
|
7
|
+
- **讨论(全自动)**:主持人与专家都由本机服务唤醒(CLI / App / API)。本机软件与权限配置完成后明确启动,可按轮次、总时长上限或手动停止结束。主持创建正式安排即结束本步;专家自行认领、回答,程序再续接原主持。自动任务不另建原生定时。项目及 cwd 可选,分别属于本机接入配置。本地专家可单独选“手动接入”:成员记录带 `adapter: manual`,不被唤醒,凭自己的成员凭证认领发言权并提交安排内的回答,接入程序不派发它的事件,回答保存后照常续接主持人;主持人必须由本机唤醒。手动专家若运行在 Claude Code 或 Codex 中,任一带凭据的 CLI 调用(首条通常是 `room view`)都会附带该会话的 `CLAUDE_CODE_SESSION_ID` / `CODEX_THREAD_ID`(Codex 桌面应用会话转为 `codex`,其余转为 `codex_cli`),本机服务核对本机存在该会话后自动把它转为托管(`access=runner`、借用会话),之后每轮照常唤醒;服务没有 Claude 配置、找不到会话或其他软件的手动专家仍停在手动。手动专家不回答时讨论停在该安排,由用户取消安排(主持人被唤醒重新安排),或受轮次与总时长上限约束,没有单独的自动超时。模式在手动与全自动间切换时,带 `adapter: manual` 的专家保持手动(转为托管后不再带此标记)。网页 AI 会打断自动推进,只进入自由协作。
|
|
8
8
|
- **自由协作**:你或 AI 主持人点名一位或多位成员,各自在原会话处理请求;没有讨论发言权、安排和轮次。每位成员单独选择本机唤醒、手动接入或网页代发,见下方 [自由协作房间](#自由协作房间)。
|
|
9
9
|
- **手动接入**:自由协作成员的一种接入方式。本地成员持自己的凭据文件自行读写,可由人工或软件原生定时触发;网页成员只能阅读房间,回答需转交代发;网页 AI 从其服务器读取阅读链接,本机网站对它不可达,此时由用户粘贴材料。只有这两种成员时建房无需本机接入服务。复制参与指令、下载凭据文件或复制凭证文本都不代表已经送达、接入或唤醒。
|
|
10
10
|
|
|
@@ -110,7 +110,7 @@ stateDiagram-v2
|
|
|
110
110
|
|
|
111
111
|
主持人可以一次排好本轮顺序(`turn create --plan-file`):第一项立即生效,其余为 `queued`。专家保存回答后,网站在同一事务中把发言权交给下一位排队专家;末位答完才交回主持人。接力不发通知,需要逐个转交的在线专家不放进整轮。
|
|
112
112
|
|
|
113
|
-
用户留言或修改目标、背景、边界、焦点会记为待主持人处理(`held_since`),不打断正在发言的专家;该专家保存回答后交回主持人而不接力。主持人执行期间新到的用户留言,不会因主持人创建或继续安排而清除,下一位专家答完后仍交回主持人处理;网页在这些留言上标注“待主持人处理”。要立即生效,用户在进度看板点「跳过」:当前安排取消、本次执行作废并交回主持人,排队安排保留。主持人处理后用 `turn continue` 按原计划继续,或直接创建新安排,剩余排队改为 `skipped
|
|
113
|
+
用户留言或修改目标、背景、边界、焦点会记为待主持人处理(`held_since`),不打断正在发言的专家;该专家保存回答后交回主持人而不接力。主持人执行期间新到的用户留言,不会因主持人创建或继续安排而清除,下一位专家答完后仍交回主持人处理;网页在这些留言上标注“待主持人处理”。要立即生效,用户在进度看板点「跳过」:当前安排取消、本次执行作废并交回主持人,排队安排保留。主持人处理后用 `turn continue` 按原计划继续,或直接创建新安排,剩余排队改为 `skipped`。建房时开启“智能调度”的讨论,留言由决策模型先判断:有把握是“仅补充信息”就按原计划继续,是在问某位专家就先请该专家回应,其余情况才交回主持人;每次判断都在对话中留一行“智能调度”记录。暂停保留当前与排队安排,点“重启”由原专家继续;若要跳过当前安排,可点“跳过”,取消该安排后恢复讨论并交回主持人,后续队列保留;邀请只在队尾追加,提前结束、到限处理、收束、更换主持人或专家暂离、请回、移除才会作废全部排队安排。
|
|
114
114
|
|
|
115
115
|
本地专家自行领取与归还;在线专家由主持人以自身操作者身份、指定原专家代操作。主持人不能代领另一位本地专家的发言权。主动归还后未完成安排仍存在,主持人需取消或处理。正常推进不超时夺取;配置的到限处理会取消未完成安排并撤销旧自动执行,再交回主持人。
|
|
116
116
|
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { setTimeout } from 'node:timers/promises';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { api_generate, api_history, api_tool_results, read_api_key } from './discussion_api.mjs';
|
|
8
9
|
|
|
@@ -10,6 +11,9 @@ const MAX_MODEL_TURNS = 24;
|
|
|
10
11
|
// API participants follow the same skills that local software loads, read fresh for every execution.
|
|
11
12
|
const SKILLS = fileURLToPath(new URL('../.agents/skills/', import.meta.url));
|
|
12
13
|
const SITE_TIMEOUT_MS = 10_000;
|
|
14
|
+
const MODEL_RETRIES = 2;
|
|
15
|
+
const MODEL_RETRY_DELAY_MS = 3000;
|
|
16
|
+
const NO_TOOL_REMINDER = '普通文本不会被保存。请调用写入工具提交真实结果;确实无法继续才不调用工具直接结束。';
|
|
13
17
|
const TEXT = { type: 'string' };
|
|
14
18
|
const NUMBER = { type: 'integer', minimum: 1 };
|
|
15
19
|
const ROOM_FIELDS = ['id', 'title', 'goal', 'host', 'user_request', 'background', 'boundaries', 'experts', 'phase', 'focus',
|
|
@@ -150,6 +154,17 @@ export function system_prompt(context) {
|
|
|
150
154
|
return [environment, skill_text('discuss-participant'), ...(host && context.mode !== 'free' ? [skill_text('discuss-host')] : [])].join('\n\n---\n\n');
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
/** Retry a model request that failed before any room write; history is only extended on success. */
|
|
158
|
+
async function generate_with_retry(profile, key, state, signal) {
|
|
159
|
+
for (let attempt = 0; ; attempt++) {
|
|
160
|
+
try { return await api_generate(profile, key, state, signal); }
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (!error.retryable || attempt >= MODEL_RETRIES) throw error;
|
|
163
|
+
await setTimeout(MODEL_RETRY_DELAY_MS * (attempt + 1), undefined, { signal });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
/** Run bounded tool turns while the parent dispatcher continues renewing leases and observing pause. */
|
|
154
169
|
async function run_participant(profile, key, context) {
|
|
155
170
|
const room = await read_room(context);
|
|
@@ -158,9 +173,16 @@ async function run_participant(profile, key, context) {
|
|
|
158
173
|
execution_id: context.execution_id, requests: context.requests, read_after: context.read_after,
|
|
159
174
|
...(context.scheduler_suggestions?.length ? { scheduler_suggestions: context.scheduler_suggestions } : {}) } });
|
|
160
175
|
const state = { system: system_prompt(context), history: api_history(profile, prompt), tools: definitions };
|
|
176
|
+
let reminded = false;
|
|
161
177
|
for (let index = 0; index < MAX_MODEL_TURNS; index++) {
|
|
162
|
-
const calls = await
|
|
163
|
-
if (!calls.length)
|
|
178
|
+
const calls = await generate_with_retry(profile, key, state, context.signal);
|
|
179
|
+
if (!calls.length) {
|
|
180
|
+
// Some models answer in plain text mid-run; one reminder avoids pausing on a recoverable slip.
|
|
181
|
+
if (reminded) throw new Error('模型未调用房间工具保存结果,请核对模型是否支持工具调用');
|
|
182
|
+
reminded = true;
|
|
183
|
+
state.history.push(profile.api_protocol === 'gemini' ? { role: 'user', parts: [{ text: NO_TOOL_REMINDER }] } : { role: 'user', content: NO_TOOL_REMINDER });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
164
186
|
const results = [];
|
|
165
187
|
for (const call of calls) {
|
|
166
188
|
context.signal.throwIfAborted();
|
package/runner/discuss.py
CHANGED
|
@@ -894,7 +894,14 @@ def access_headers(args, site, path):
|
|
|
894
894
|
token = access.get("token")
|
|
895
895
|
if not isinstance(token, str) or not 32 <= len(token) <= 256:
|
|
896
896
|
raise CliError("凭据文件中的 token 无效")
|
|
897
|
-
|
|
897
|
+
headers = {"X-Discussion-Token": token}
|
|
898
|
+
# A hosting Claude Code or Codex session reports itself on any call, so a manual member can be taken over before it first speaks.
|
|
899
|
+
for variable, client in (("CLAUDE_CODE_SESSION_ID", "claude"), ("CODEX_THREAD_ID", "codex")):
|
|
900
|
+
session_id = os.getenv(variable, "").strip()
|
|
901
|
+
if session_id:
|
|
902
|
+
headers.update({"X-Discussion-Session": session_id, "X-Discussion-Session-Client": client})
|
|
903
|
+
break
|
|
904
|
+
return headers
|
|
898
905
|
|
|
899
906
|
|
|
900
907
|
def preview_body(value):
|
|
@@ -116,8 +116,10 @@ async function provider_fetch(profile, key, request, signal) {
|
|
|
116
116
|
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
117
117
|
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
118
118
|
});
|
|
119
|
-
} catch {
|
|
120
|
-
|
|
119
|
+
} catch {
|
|
120
|
+
throw Object.assign(new Error(signal?.aborted ? 'API 执行已取消' : 'API 请求超时或连接失败:可能是网络暂时不通或 Base URL 有误,请核对'), { retryable: !signal?.aborted });
|
|
121
|
+
}
|
|
122
|
+
if (!response.ok) throw Object.assign(new Error(api_status_message(response.status)), { retryable: response.status === 429 || response.status >= 500 });
|
|
121
123
|
try { return await response.json(); } catch { throw new Error('模型 API 返回的内容不是有效 JSON'); }
|
|
122
124
|
}
|
|
123
125
|
|
package/web/app.js
CHANGED
|
@@ -1610,7 +1610,7 @@ function participant_model(name, room, saved) {
|
|
|
1610
1610
|
function participant_access(name, room) {
|
|
1611
1611
|
const member = (room?.members || []).find(item => item.name === name);
|
|
1612
1612
|
const adapter = member?.adapter;
|
|
1613
|
-
if (member?.access === "manual") return "
|
|
1613
|
+
if (member?.access === "manual") return "手动启动";
|
|
1614
1614
|
if (adapter === "api") return "API";
|
|
1615
1615
|
if (["codex", "antigravity"].includes(adapter)) return "App";
|
|
1616
1616
|
return room?.experts?.[name] === "cli" ? "CLI" : "在线";
|
|
@@ -2121,6 +2121,55 @@ async function restart_discussion() {
|
|
|
2121
2121
|
show_toast("已重启,原安排会重新交给对应 AI;需本机接入程序保持运行");
|
|
2122
2122
|
}
|
|
2123
2123
|
|
|
2124
|
+
/** Only the failed attempt for the still-retained task can restart this discussion. */
|
|
2125
|
+
function can_retry_execution(execution, room) {
|
|
2126
|
+
const runtime = room.runtime;
|
|
2127
|
+
if (!execution || room.archived || room.phase !== "paused" || runtime?.mode !== "automatic"
|
|
2128
|
+
|| runtime.status !== "paused" || runtime.latest_execution?.id !== execution.id
|
|
2129
|
+
|| runtime.latest_execution.status !== "failed") return false;
|
|
2130
|
+
const turn = room.current_turn;
|
|
2131
|
+
const active_turn = turn && ["pending", "notified"].includes(turn.status);
|
|
2132
|
+
return execution.role === "host" ? !active_turn && execution.participant === room.host
|
|
2133
|
+
: active_turn && turn.id === execution.turn_id && turn.expert === execution.participant;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
/** Recheck the selected failed attempt before resuming its retained task with a new execution. */
|
|
2137
|
+
async function retry_execution(event_id) {
|
|
2138
|
+
const room_id = state.room.id;
|
|
2139
|
+
const room = await request_json(`/api/rooms/${room_id}`);
|
|
2140
|
+
const execution = room.runtime?.failures?.find(item => item.id === event_id);
|
|
2141
|
+
if (!can_retry_execution(execution, room)) throw new Error("执行或安排已变化,请刷新后重试");
|
|
2142
|
+
await request_json(`/api/rooms/${room_id}/runtime`, {
|
|
2143
|
+
action: "start", mode: "automatic", revision: room.runtime.revision, deadline: 0,
|
|
2144
|
+
});
|
|
2145
|
+
if (state.room?.id !== room_id) return;
|
|
2146
|
+
await refresh_room();
|
|
2147
|
+
show_toast(`已重新安排 ${execution.participant} 执行,原失败记录保留`);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/** Skip the selected assignment; a paused automatic run resumes with the host instead of retrying it. */
|
|
2151
|
+
async function skip_turn() {
|
|
2152
|
+
const room = state.room;
|
|
2153
|
+
const turn = room.current_turn;
|
|
2154
|
+
if (!turn || !["pending", "notified"].includes(turn.status)) throw new Error("当前没有可跳过的发言安排,请刷新后核对");
|
|
2155
|
+
const room_id = room.id, path = `/api/rooms/${room_id}`;
|
|
2156
|
+
const runtime = room.runtime || {};
|
|
2157
|
+
const resume = runtime.mode === "automatic" && (runtime.status === "paused" || runtime.expired);
|
|
2158
|
+
const revision = runtime.revision;
|
|
2159
|
+
let cancelled = false;
|
|
2160
|
+
try {
|
|
2161
|
+
await request_json(`${path}/turns/${turn.id}`, { actor: "用户", status: "cancelled" });
|
|
2162
|
+
cancelled = true;
|
|
2163
|
+
if (resume) await request_json(`${path}/runtime`, { action: "start", mode: "automatic", revision, deadline: 0 });
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
if (cancelled && resume) throw new Error(`本次发言已跳过,但未能继续讨论:${error.message}。核对后可点“重启”继续`);
|
|
2166
|
+
throw error;
|
|
2167
|
+
} finally {
|
|
2168
|
+
if (state.room?.id === room_id) await refresh_room();
|
|
2169
|
+
}
|
|
2170
|
+
if (state.room?.id === room_id) show_toast(resume ? "已跳过本次发言并恢复讨论,等待主持人继续安排" : "已跳过本次发言,等待主持人继续安排");
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2124
2173
|
/** End early with a host summary limited to the configured closing reserve. */
|
|
2125
2174
|
async function force_close() {
|
|
2126
2175
|
await request_json(`/api/rooms/${state.room.id}/limits`, { actor: "用户", force: true });
|
|
@@ -2205,7 +2254,7 @@ ${instruction}
|
|
|
2205
2254
|
/** How a manual member learns it is its turn: free rooms are pulled by the member, discussions only on the host's assignment. */
|
|
2206
2255
|
function manual_trigger_instruction(room) {
|
|
2207
2256
|
return room.mode === "free" ? "首次复制后,可由人工或软件原生定时触发;自行提交,不必每轮复制回答。"
|
|
2208
|
-
: "
|
|
2257
|
+
: "这是全自动讨论。现在只先了解房间(议题、参与者、已有发言),不要发言,不要自建定时任务;轮到你时会被唤醒。唤醒后先重读最新内容(turn current、新增发言),确认轮到你再认领发言权并提交;没有安排就结束本次检查。";
|
|
2209
2258
|
}
|
|
2210
2259
|
|
|
2211
2260
|
/** Download a private per-room setup file for the independent local runner. */
|
|
@@ -3054,10 +3103,10 @@ async function execute_view_action(name, value) {
|
|
|
3054
3103
|
save_context: () => save_context(event), save_topic: () => save_topic(event),
|
|
3055
3104
|
schedule: () => schedule_turn(event), filter: filter_view, reply: set_view_reply,
|
|
3056
3105
|
cancel_reply, cancel_answer: cancel_turn_reply, prepare_answer: prepare_turn_answer,
|
|
3057
|
-
copy_turn, notify: () => change_turn_status("notified"), cancel_turn: () => change_turn_status("cancelled"),
|
|
3106
|
+
copy_turn, notify: () => change_turn_status("notified"), cancel_turn: () => change_turn_status("cancelled"), skip_turn,
|
|
3058
3107
|
take_floor: () => change_floor("take"), return_floor: () => change_floor("return"),
|
|
3059
3108
|
copy_online: () => copy_context(), copy_cli: () => copy_context("cli"), copy_background,
|
|
3060
|
-
choose_runtime, export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, force_close, conclude_discussion,
|
|
3109
|
+
choose_runtime, export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, retry_execution, force_close, conclude_discussion,
|
|
3061
3110
|
save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
|
|
3062
3111
|
load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member, set_proxy, copy_online_notice,
|
|
3063
3112
|
prepare_brainstorm, return_conclusion, convert_suggestion, adopt_recommendation,
|
|
@@ -3088,7 +3137,7 @@ window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
|
3088
3137
|
attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
|
|
3089
3138
|
write_expert: write_view_expert, add_instance: add_view_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
|
|
3090
3139
|
request: request_json, identities: IDENTITIES, display_name, sources: SOURCE_LABELS, phases: PHASE_LABELS,
|
|
3091
|
-
runtime_label, execution_label, limits_label, participant_model, participant_access,
|
|
3140
|
+
runtime_label, execution_label, limits_label, participant_model, participant_access, can_retry_execution,
|
|
3092
3141
|
turns: TURN_LABELS, turn_label, turn_guidance, floor_status, progress_round, progress_speaker, brief_meta, max_message_length: MAX_MESSAGE_LENGTH, provenance: message_provenance, format_time };
|
|
3093
3142
|
|
|
3094
3143
|
restore_theme();
|