ai_co_discuss 0.2.29 → 0.2.31
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/SKILL.md +1 -1
- package/README.md +3 -3
- package/backend/free_room.py +52 -12
- package/backend/room_import.py +2 -0
- package/backend/runtime.py +5 -3
- package/backend/schema.sql +7 -0
- package/backend/server.py +7 -3
- package/docs/architecture.md +3 -1
- package/docs/cli.md +2 -2
- package/docs/runner.md +34 -0
- package/docs/workflow.md +5 -3
- package/package.json +2 -2
- package/runner/api_participant.mjs +2 -1
- package/runner/discuss.py +26 -10
- package/runner/discussion_api.mjs +20 -4
- package/runner/discussion_launch.mjs +17 -8
- package/runner/discussion_runner.mjs +43 -7
- package/runner/discussion_service.mjs +1 -1
- package/runner/free_runner.mjs +54 -5
- package/runner/scheduler.mjs +99 -0
- package/web/app.js +84 -21
- package/web/modern.css +1 -1
- package/web/modern.js +1227 -997
|
@@ -55,7 +55,7 @@ description: "AI CoDiscuss 的主持技能,仅主持人调用,且须同时
|
|
|
55
55
|
|
|
56
56
|
收到自动任务后,按参与技能进入本次执行;若当前调用已成功进入,不重复认领。不创建原生定时,也不手动通知已配置的本地专家,通知程序负责后续交付与续租。
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
阅读新增的相关回答、用户约束及必要原文,判断下一步是追问、补证、复核还是收束。任务带 `scheduler_suggestions` 时,那是用户开启的智能调度对未处理留言给出的分流建议(请某位专家回应、主持人处理或仅补充信息),附置信度;读原文后自行决定是否采纳,不当作用户指令或已核实的判断。不要只回复“已就绪”或只写一段摘要就退出;本次执行需要以以下一种结果交接:
|
|
59
59
|
|
|
60
60
|
| 判断 | 保存与结束边界 |
|
|
61
61
|
| --- | --- |
|
package/README.md
CHANGED
|
@@ -65,13 +65,13 @@ codiscuss setup
|
|
|
65
65
|
|
|
66
66
|
1. 安装完成后进入自动打开的网站;以后运行 `codiscuss local open` 打开或复用本机网站,默认地址为 `http://127.0.0.1:8765`。
|
|
67
67
|
2. 点击“新建讨论”,填写**话题名称**和**用户原文**。把问题、背景与顾虑写在原文中,讨论目标、背景和边界由 AI 主持人据此补全。
|
|
68
|
-
3. 房间用法选择“讨论”,从卡片下方的“添加”行加入本场专家,点卡片上的皇冠指定 AI 主持人,点击“移出”调整名单。讨论全程由本机服务唤醒 AI,每张卡片可选 App、CLI 或 API
|
|
68
|
+
3. 房间用法选择“讨论”,从卡片下方的“添加”行加入本场专家,点卡片上的皇冠指定 AI 主持人,点击“移出”调整名单。讨论全程由本机服务唤醒 AI,每张卡片可选 App、CLI 或 API;本地专家也可选“手动接入”(主持人不可),网页 AI 只用于自由协作。自动 CLI 卡片背面从真实软件目录下拉选择模型,并按模型支持的档位设置思考强度;留空使用 CLI 默认值,App 在原软件设置。项目文件只读 / 可写也在每张自动 CLI 卡片上分别设置(默认只读);App 的文件权限在原软件设置,API 专家不访问项目文件;软件路径缺失时卡片背面直接提示填写。分别设置轮次和时长上限,点击数值展开滚轮或直接输入。新建默认 **3 轮 / 不限时长**,按轮次上限或手动停止结束;单次 AI 执行也默认不限时,可按需设置 1–60 分钟上限。
|
|
69
69
|
4. “创建后立即启动 AI”默认已勾选。需要一起查看项目时填写项目目录,可直接输入完整路径,也可点输入框右侧的文件夹图标在本机弹窗选取。复用已有会话等高级覆盖在建房后的“接入”页设置。
|
|
70
70
|
5. 点击“创建并启动讨论”。程序完成建房、登记与首次任务交付;在进度看板核对 AI 是否实际进入,并查看保存的发言。
|
|
71
71
|
|
|
72
72
|
讨论进行中可在右侧进度看板邀请新专家(沿用本机软件配置);Codex、Claude 可填写已有会话 ID,请一个带着自己上下文的原会话加入,它工作时不要在原窗口继续操作。也可从成员的“…”菜单让其暂离、请回或移除。暂离保留位置与会话,不参与安排;这些操作不暂停讨论,主持人会被唤醒重新安排。主持人不能暂离,需先更换。
|
|
73
73
|
|
|
74
|
-
只想让 AI 干活时,在第 3 步将房间用法改为“自由协作”,只需填写话题名称,任务在对话中随时补充:选择一位或多位 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
|
|
|
@@ -115,7 +115,7 @@ codiscuss local connect https://你的站点
|
|
|
115
115
|
|
|
116
116
|
软件失败时先查看暂停原因,登录和权限问题在对应软件中处理。本机网站的已登记房间,可从进度看板 Codex 旁的“…”菜单直接选择“切换 App / CLI”,在弹窗内暂停并保存;软件路径在“接入 → 修正本场软件连接”修改。原会话与讨论进度保留,待服务确认采用后重启;线上连接暂不提供此修正入口。切到 App 后模型和文件权限按 App 设置,切回 CLI 前须让 App 释放原聊天。本场修改不影响后续讨论的默认配置,详见 [配置修正](docs/runner.md#已启动后的配置修正)。
|
|
117
117
|
|
|
118
|
-
**手动接入与网页 AI:**
|
|
118
|
+
**手动接入与网页 AI:** 手动接入可用于“自由协作”和“讨论”的专家(讨论的主持人必须由本机唤醒),在成员卡片正面的“自动 / 手动 / 网页”切换里选择。手动成员从成员菜单下载自己的凭据文件,或复制同样内容的凭证文本(设为环境变量 `DISCUSS_ACCESS`),连同参与说明私下交给对应 AI 会话,由人工或软件原生定时触发;讨论里的手动专家只在被安排发言时参与,没有及时回答时讨论会等待,可在进度看板取消安排;网页 AI 仍只用于自由协作,网页 AI 只能阅读房间,回答由你代发;它从自己的服务器读取阅读链接,只在本机运行的网站(`127.0.0.1`)它读不到,需由你粘贴材料。只有手动和网页成员时建房无需本机接入服务。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
|
|
119
119
|
|
|
120
120
|
## 在线部署
|
|
121
121
|
|
package/backend/free_room.py
CHANGED
|
@@ -26,6 +26,7 @@ MAX_CHANGED_FILES = 200
|
|
|
26
26
|
MAX_PATH_LENGTH = 500
|
|
27
27
|
ACTIVE = ("dispatched", "started")
|
|
28
28
|
ACCESS_MODES = ("runner", "manual", "online")
|
|
29
|
+
SCHEDULER_PROVIDERS = ("jev", "decisions")
|
|
29
30
|
# AI-created requests (host dispatches and hand-offs) allowed per chain; each owner message starts a chain.
|
|
30
31
|
MAX_AUTO_HOPS = 8
|
|
31
32
|
# A host that left gives up the role; the first active runner member (else manual) then takes it.
|
|
@@ -107,17 +108,30 @@ def save_room_access(connection, room_id, control_token, members, settings):
|
|
|
107
108
|
connection.execute("UPDATE room_members SET role='host' WHERE room_id=? AND name=?", (room_id, settings["host"]))
|
|
108
109
|
|
|
109
110
|
|
|
111
|
+
def discussion_access_ok(access, capability, automatic, is_host):
|
|
112
|
+
"""Online experts stay online; a woken discussion also takes manual experts, but never a manual host."""
|
|
113
|
+
if capability == "online":
|
|
114
|
+
return access == "online"
|
|
115
|
+
if not automatic:
|
|
116
|
+
return access == "manual"
|
|
117
|
+
return access == "runner" or (access == "manual" and not is_host)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def mark_manual(member, automatic):
|
|
121
|
+
"""A manual expert of a woken discussion carries adapter 'manual', so mode switches never turn it into a woken member."""
|
|
122
|
+
return {**member, "adapter": "manual"} if automatic and member["access"] == "manual" else member
|
|
123
|
+
|
|
124
|
+
|
|
110
125
|
def discussion_members(payload, settings):
|
|
111
126
|
"""Validate credentials against the room's declared participants and delivery mode."""
|
|
112
127
|
members = member_values(payload.get("members")) if settings["experts"] else []
|
|
113
128
|
if {member["name"] for member in members} != set(settings["experts"]):
|
|
114
129
|
raise ValueError("成员凭证名单须与参与者名单一致")
|
|
115
130
|
for member in members:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if member["access"] != expected:
|
|
131
|
+
if not discussion_access_ok(member["access"], settings["experts"][member["name"]],
|
|
132
|
+
payload.get("runtime_mode") == "automatic", member["name"] == settings["host"]):
|
|
119
133
|
raise ValueError("成员接入方式与房间参与能力、任务交付方式不符")
|
|
120
|
-
return members
|
|
134
|
+
return [mark_manual(member, payload.get("runtime_mode") == "automatic") for member in members]
|
|
121
135
|
|
|
122
136
|
|
|
123
137
|
def sync_discussion_members(connection, room_id, payload, settings):
|
|
@@ -126,15 +140,15 @@ def sync_discussion_members(connection, room_id, payload, settings):
|
|
|
126
140
|
supplied = {member["name"]: member_value(member) for member in payload.get("members", [])}
|
|
127
141
|
run = connection.execute("SELECT mode FROM discussion_runtime WHERE room_id=?", (room_id,)).fetchone()
|
|
128
142
|
for name, capability in settings["experts"].items():
|
|
129
|
-
|
|
143
|
+
automatic = bool(run and run[0] == "automatic")
|
|
130
144
|
old = current.get(name)
|
|
131
|
-
if old and (old["access"] == "online") == (
|
|
145
|
+
if old and (old["access"] == "online") == (capability == "online"):
|
|
132
146
|
if name in supplied and token_hash(supplied[name]["token"]) != old["token_hash"]:
|
|
133
147
|
raise FileExistsError("成员凭证与已登记身份不符;请核对原接入文件")
|
|
134
148
|
continue
|
|
135
|
-
if name not in supplied or supplied[name]["access"]
|
|
149
|
+
if name not in supplied or not discussion_access_ok(supplied[name]["access"], capability, automatic, name == settings["host"]):
|
|
136
150
|
raise ValueError(f"新增或改变接入方式的成员 {name} 须提供独立的新凭证")
|
|
137
|
-
member = supplied[name]
|
|
151
|
+
member = mark_manual(supplied[name], automatic)
|
|
138
152
|
hashed = token_hash(member["token"])
|
|
139
153
|
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():
|
|
140
154
|
raise ValueError("新成员凭证不能复用已有凭证")
|
|
@@ -222,7 +236,9 @@ def free_view(connection, room_id):
|
|
|
222
236
|
members.append(member)
|
|
223
237
|
names = {row["id"]: row["name"] for row in connection.execute("SELECT id,name FROM room_members WHERE room_id=?", (room_id,))}
|
|
224
238
|
requests = [{**dict(row), "member": names.get(row["member_id"], "")} for row in connection.execute(
|
|
225
|
-
"""SELECT id,member_id,message_id,status,execution_id,answer_id,chain_id,batch_id,purpose,created_by
|
|
239
|
+
"""SELECT id,member_id,message_id,status,execution_id,answer_id,chain_id,batch_id,purpose,created_by,
|
|
240
|
+
(SELECT provider FROM request_schedules WHERE request_id=member_requests.id) AS schedule_provider,
|
|
241
|
+
(SELECT confidence FROM request_schedules WHERE request_id=member_requests.id) AS schedule_confidence FROM member_requests
|
|
226
242
|
WHERE room_id=? ORDER BY id DESC LIMIT ?""", (room_id, RECENT_REQUESTS))]
|
|
227
243
|
chains = [dict(row) for row in connection.execute(
|
|
228
244
|
"SELECT chain_id,reason FROM free_chains WHERE room_id=? ORDER BY chain_id DESC LIMIT 10", (room_id,))]
|
|
@@ -545,7 +561,7 @@ def inbox(connection, room_id, token):
|
|
|
545
561
|
members = []
|
|
546
562
|
for row in connection.execute("SELECT * FROM room_members WHERE room_id=? AND status='active' AND access='runner' ORDER BY id", (room_id,)):
|
|
547
563
|
pending = [dict(item) for item in connection.execute(
|
|
548
|
-
"SELECT id,message_id FROM member_requests WHERE member_id=? AND status='pending' ORDER BY id", (row["id"],))]
|
|
564
|
+
"SELECT id,message_id,purpose FROM member_requests WHERE member_id=? AND status='pending' ORDER BY id", (row["id"],))]
|
|
549
565
|
members.append({**{key: row[key] for key in MEMBER_FIELDS}, "pending": pending})
|
|
550
566
|
executions = [{**public_execution(row), "member": row["name"]} for row in connection.execute(
|
|
551
567
|
"""SELECT member_executions.*,room_members.name FROM member_executions JOIN room_members ON room_members.id=member_id
|
|
@@ -554,6 +570,30 @@ def inbox(connection, room_id, token):
|
|
|
554
570
|
"execution_minutes": execution_minutes(connection, room_id)}
|
|
555
571
|
|
|
556
572
|
|
|
573
|
+
def schedule_request(connection, room_id, token, request_id, payload):
|
|
574
|
+
"""Smart scheduling moves one unnamed message from the host to a woken member before the host takes it."""
|
|
575
|
+
require_owner(connection, room_id, token)
|
|
576
|
+
begin_write(connection)
|
|
577
|
+
if payload.get("action") != "schedule":
|
|
578
|
+
raise ValueError("只支持 action=schedule")
|
|
579
|
+
confidence = payload.get("confidence")
|
|
580
|
+
if payload.get("provider") not in SCHEDULER_PROVIDERS or type(confidence) not in (int, float) or not 0 <= confidence <= 1:
|
|
581
|
+
raise ValueError("须提供智能调度供应商 provider 与 0–1 的置信度 confidence")
|
|
582
|
+
request = connection.execute("SELECT * FROM member_requests WHERE room_id=? AND id=?", (room_id, request_id)).fetchone()
|
|
583
|
+
if not request:
|
|
584
|
+
raise LookupError("请求不存在")
|
|
585
|
+
if request["purpose"] != "route" or request["status"] != "pending":
|
|
586
|
+
raise FileExistsError("请求已由主持人接手或已处理,不能改派")
|
|
587
|
+
target = connection.execute("""SELECT * FROM room_members WHERE room_id=? AND id=? AND status='active'
|
|
588
|
+
AND access='runner' AND role<>'host'""", (room_id, payload.get("member_id"))).fetchone()
|
|
589
|
+
if not target:
|
|
590
|
+
raise LookupError("改派对象须为在场、由本机唤醒的非主持成员")
|
|
591
|
+
connection.execute("UPDATE member_requests SET member_id=?,purpose='scheduled' WHERE id=?", (target["id"], request_id))
|
|
592
|
+
connection.execute("INSERT OR REPLACE INTO request_schedules (request_id,room_id,provider,confidence) VALUES (?,?,?,?)",
|
|
593
|
+
(request_id, room_id, payload["provider"], float(confidence)))
|
|
594
|
+
return dict(connection.execute("SELECT * FROM member_requests WHERE id=?", (request_id,)).fetchone())
|
|
595
|
+
|
|
596
|
+
|
|
557
597
|
def reserve_execution(connection, room_id, token, payload):
|
|
558
598
|
"""Bundle a member's open requests into one execution; a retried client_id returns the same one."""
|
|
559
599
|
require_owner(connection, room_id, token)
|
|
@@ -700,7 +740,7 @@ def add_discussion_member(connection, room_id, token, payload, now):
|
|
|
700
740
|
member = member_value(payload)
|
|
701
741
|
_, experts, phase = discussion_roster(connection, room_id)
|
|
702
742
|
run = connection.execute("SELECT mode FROM discussion_runtime WHERE room_id=?", (room_id,)).fetchone()
|
|
703
|
-
if member["access"]
|
|
743
|
+
if not discussion_access_ok(member["access"], "cli", bool(run and run["mode"] == "automatic"), False):
|
|
704
744
|
raise ValueError("新专家的接入方式须与讨论的推进方式一致")
|
|
705
745
|
existing = connection.execute("SELECT * FROM room_members WHERE room_id=? AND name=? AND status<>'removed'", (room_id, member["name"])).fetchone()
|
|
706
746
|
if existing and existing["status"] == "active" and existing["token_hash"] == token_hash(member["token"]):
|
|
@@ -712,7 +752,7 @@ def add_discussion_member(connection, room_id, token, payload, now):
|
|
|
712
752
|
if len(experts) >= MAX_ROOM_AGENTS:
|
|
713
753
|
raise ValueError("房间成员已达上限")
|
|
714
754
|
require_fresh_member_token(connection, room_id, member["token"])
|
|
715
|
-
insert_member(connection, room_id, member, now)
|
|
755
|
+
insert_member(connection, room_id, mark_manual(member, bool(run and run["mode"] == "automatic")), now)
|
|
716
756
|
save_discussion_experts(connection, room_id, {**experts, member["name"]: "cli"})
|
|
717
757
|
|
|
718
758
|
|
package/backend/room_import.py
CHANGED
|
@@ -159,6 +159,8 @@ def record_statements(tables, room_id, maps):
|
|
|
159
159
|
for row in tables.get("member_requests", [])]
|
|
160
160
|
statements += [("INSERT INTO member_suggestions (message_id,member_id) VALUES (?,?)", (messages[row["message_id"]], members[row["member_id"]]))
|
|
161
161
|
for row in tables.get("member_suggestions", [])]
|
|
162
|
+
statements += [("INSERT INTO request_schedules (request_id,room_id,provider,confidence) VALUES (?,?,?,?)",
|
|
163
|
+
(requests[row["request_id"]], room_id, row["provider"], row["confidence"])) for row in tables.get("request_schedules", [])]
|
|
162
164
|
return statements
|
|
163
165
|
|
|
164
166
|
|
package/backend/runtime.py
CHANGED
|
@@ -197,7 +197,7 @@ def authorize_write(connection, room_id, token, operation, payload):
|
|
|
197
197
|
return
|
|
198
198
|
member = connection.execute("SELECT * FROM room_members WHERE room_id=? AND token_hash=? AND status='active' AND access<>'online'",
|
|
199
199
|
(room_id, hashed)).fetchone() if hashed else None
|
|
200
|
-
if member and (not run or run["mode"] == "manual"):
|
|
200
|
+
if member and (not run or run["mode"] == "manual" or member["access"] == "manual"):
|
|
201
201
|
authorize_participant(connection, room_id, member, operation, payload)
|
|
202
202
|
return
|
|
203
203
|
if not run or not hashed:
|
|
@@ -259,8 +259,10 @@ def configure_runtime(connection, room_id, token, payload):
|
|
|
259
259
|
if action in ("pause", "stop"):
|
|
260
260
|
connection.execute("UPDATE topic_settings SET phase='paused' WHERE room_id=? AND phase<>'concluded'", (room_id,))
|
|
261
261
|
connection.execute("UPDATE discussion_runtime SET status=? WHERE room_id=?", (status, room_id))
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
# Only a real mode change converts members; a manual expert of a woken discussion (adapter 'manual') stays manual.
|
|
263
|
+
if mode != current["mode"]:
|
|
264
|
+
connection.execute("UPDATE room_members SET access='manual' WHERE room_id=? AND access='runner'" if mode == "manual" else
|
|
265
|
+
"UPDATE room_members SET access='runner' WHERE room_id=? AND access='manual' AND adapter<>'manual'", (room_id,))
|
|
264
266
|
return runtime_view(connection, room_id)
|
|
265
267
|
|
|
266
268
|
|
package/backend/schema.sql
CHANGED
|
@@ -569,6 +569,13 @@ CREATE TABLE IF NOT EXISTS member_requests (
|
|
|
569
569
|
UNIQUE(message_id,member_id)
|
|
570
570
|
);
|
|
571
571
|
CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
|
|
572
|
+
-- Smart scheduling's decision for a request it moved from the host to a member; a separate table so D1 needs no column change.
|
|
573
|
+
CREATE TABLE IF NOT EXISTS request_schedules (
|
|
574
|
+
request_id INTEGER PRIMARY KEY REFERENCES member_requests(id) ON DELETE CASCADE,
|
|
575
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
576
|
+
provider TEXT NOT NULL,
|
|
577
|
+
confidence REAL NOT NULL
|
|
578
|
+
);
|
|
572
579
|
-- Ordinary members can suggest a response without creating work or spending a chain budget.
|
|
573
580
|
CREATE TABLE IF NOT EXISTS member_suggestions (
|
|
574
581
|
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
package/backend/server.py
CHANGED
|
@@ -23,6 +23,7 @@ from .free_room import (add_member, after_free_message, authorize_free_write, ch
|
|
|
23
23
|
inbox, interrupt_member, member_action, member_values, migrate_free_tables, proxy_member, reserve_execution,
|
|
24
24
|
discussion_members, room_mode, save_room_access, sync_discussion_members, view_execution)
|
|
25
25
|
from .free_room import add_discussion_member, convert_suggestion, discussion_member_action, message_suggestions, save_suggestions, sync_participants
|
|
26
|
+
from .free_room import schedule_request
|
|
26
27
|
from .identity import MAX_ROOM_AGENTS, PARTICIPANTS, is_ai, is_participant
|
|
27
28
|
from .room_import import import_room, imported_attachment
|
|
28
29
|
from .runtime import (authorize_write, begin_write, configure_runtime, event_action,
|
|
@@ -89,6 +90,7 @@ FREE_BACKUP_QUERIES = {
|
|
|
89
90
|
"room_members": "SELECT id,room_id,name,adapter,role,status,read_until,created_at,access,session_mode FROM room_members WHERE room_id=? ORDER BY id",
|
|
90
91
|
"member_requests": "SELECT * FROM member_requests WHERE room_id=? ORDER BY id",
|
|
91
92
|
"member_suggestions": "SELECT s.* FROM member_suggestions s JOIN messages m ON m.id=s.message_id WHERE m.room_id=? ORDER BY s.message_id,s.member_id",
|
|
93
|
+
"request_schedules": "SELECT * FROM request_schedules WHERE room_id=? ORDER BY request_id",
|
|
92
94
|
}
|
|
93
95
|
MESSAGE_QUERY = """SELECT messages.*, COALESCE(details.kind,'opinion') AS kind,
|
|
94
96
|
COALESCE(details.submitted_by, CASE WHEN messages.source IN ('cli','api','url','user')
|
|
@@ -536,7 +538,7 @@ def manage_room(connection, room_id, action, payload):
|
|
|
536
538
|
|
|
537
539
|
def delete_room_records(connection, room_id):
|
|
538
540
|
"""Remove a room's dependent records in the caller's transaction."""
|
|
539
|
-
for table in ("member_requests", "member_executions", "room_members", "room_modes", "free_batches", "free_chains", "room_links"):
|
|
541
|
+
for table in ("request_schedules", "member_requests", "member_executions", "room_members", "room_modes", "free_batches", "free_chains", "room_links"):
|
|
540
542
|
connection.execute(f"DELETE FROM {table} WHERE room_id=?", (room_id,))
|
|
541
543
|
connection.execute("DELETE FROM message_timings WHERE message_id IN (SELECT id FROM messages WHERE room_id=?)", (room_id,))
|
|
542
544
|
connection.execute("DELETE FROM message_summaries WHERE message_id IN (SELECT id FROM messages WHERE room_id=?)", (room_id,))
|
|
@@ -1218,7 +1220,7 @@ def protocol_text(room):
|
|
|
1218
1220
|
"写入使用 POST,通常为 Content-Type: application/json;附件上传使用 application/octet-stream 原始字节。主持操作填写 actor 为当前主持人或用户;这些是声明身份,不是认证凭据。",
|
|
1219
1221
|
f"附件:POST {base}/attachments?actor=身份&file_name=文件名&submission_id=新编号,原文件最多 20 MiB;GET 同路径默认列已发送附件,加 submission_id 精确核对上传。GET {base}/attachments/ID 读元数据,/ID/content 下载原文件。消息可带 attachment_ids(最多 10 个有序编号);只关联同房间、同提交者的未使用附件,可只有附件无正文。自动任务须在回答完成前上传;消息查询包含附件,按需下载后由当前软件读取,不自动解析。",
|
|
1220
1222
|
"讨论上限:room.limits 返回轮次、总时长、单次执行、到限方式、收束预留和实际进度。准备/暂停时通过 topic 的 limits 对象修改;0 表示不预设轮次/总时长/单次执行时限。POST /api/rooms/ROOM/limits(actor)或 room limits ROOM --check 检查到限处理;附 force:true 可在进行中提前进入收束,由主持人在收束预留时间内总结。到限停止新专家安排与观点;总结由真实主持人完成,预留计入总时长;暂停期间不计时也不消耗总时长。",
|
|
1221
|
-
"运行方式:room.runtime 显示 manual/automatic、状态及真实执行回执。所有房间写入须带 X-Discussion-Token,CLI 用 --access-file 读取私有文件。用户及运行控制使用房主凭据,手动本地成员使用自己的成员凭证,不能相互冒充;在线成员只读,由房主或讨论主持人代发。自动讨论先 execution enter
|
|
1223
|
+
"运行方式:room.runtime 显示 manual/automatic、状态及真实执行回执。所有房间写入须带 X-Discussion-Token,CLI 用 --access-file 读取私有文件。用户及运行控制使用房主凭据,手动本地成员使用自己的成员凭证,不能相互冒充;在线成员只读,由房主或讨论主持人代发。自动讨论先 execution enter,创建安排或保存关联回答即完成本步,不创建原生定时任务;本机唤醒成员的持久凭证不能替代自动执行,手动接入的专家用自己的成员凭证回答安排,暂停、停止、换代、租约到期后的旧执行失效。run view/start/pause/stop/configure 字段查 CLI schema。",
|
|
1222
1224
|
f"发言权:GET {base}/floor 返回 holder、next_speaker、revision;POST 同路径,字段 actor、speaker(默认 actor)、action=take/return、revision(刚读到的整数版本)。仅无人持有且 next_speaker 为本人时可领取;持有者可主动归还,下一位回到主持人。版本变化返回 409,先读回核对。主持人仅可代操作 online 专家,cli 专家自行操作;用户可协助。",
|
|
1223
1225
|
"新房间由主持人持有发言权。创建安排须主持人持有,保存成功即归还并点名该专家;AI 的 opinion、moderation、receipt 都须实际作者持有,代发不豁免,opinion 必须关联有效 turn_id。消息保存成功自动归还,下一位为主持人。失败不归还,同编号重试不再次交接。用户本人发言不受发言权限制,也不改变发言权;@ 不改变顺序。维护状态、阶段及通知回执不占用发言权。取消安排须主持人持有,用户可直接取消失效安排;取消后归还给主持人领取。",
|
|
1224
1226
|
'创建话题: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 私有房主文件 在请求前保存凭据,确认后返回各本地成员文件路径;同编号重试沿用原资料和全部凭据。名单、主持职责变更仅房主可做。',
|
|
@@ -1557,7 +1559,7 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1557
1559
|
room_id = parts[2]
|
|
1558
1560
|
token = self.headers.get("X-Discussion-Token", "")
|
|
1559
1561
|
free = room_mode(connection, room_id) == "free"
|
|
1560
|
-
if free and parts[3:4] in (["inbox"], ["executions"], ["members"], ["suggestions"]):
|
|
1562
|
+
if free and parts[3:4] in (["inbox"], ["executions"], ["members"], ["suggestions"], ["requests"]):
|
|
1561
1563
|
return self.handle_free_api(connection, parts, method)
|
|
1562
1564
|
if parts[3:4] == ["members"] and method == "POST" and len(parts) in (4, 5):
|
|
1563
1565
|
if len(parts) == 4:
|
|
@@ -1715,6 +1717,8 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1715
1717
|
result = convert_suggestion(connection, room_id, token, self.read_payload())
|
|
1716
1718
|
elif parts[3:] == ["executions"] and method == "POST":
|
|
1717
1719
|
result = reserve_execution(connection, room_id, token, self.read_payload())
|
|
1720
|
+
elif len(parts) == 5 and parts[3] == "requests" and method == "POST":
|
|
1721
|
+
result = schedule_request(connection, room_id, token, int(parts[4]), self.read_payload())
|
|
1718
1722
|
elif len(parts) == 5 and parts[3] == "executions":
|
|
1719
1723
|
execution_id = int(parts[4])
|
|
1720
1724
|
result = (view_execution(connection, room_id, execution_id, token) if method == "GET"
|
package/docs/architecture.md
CHANGED
|
@@ -42,6 +42,7 @@ Python 与 Cloudflare 是两种部署实现,运行时选其一,不是请求
|
|
|
42
42
|
| `frontend/markdown.mjs` | Markdown 原文 | 三个主题共用的安全 HTML 与公式渲染;原始 HTML 关闭 |
|
|
43
43
|
| `runner/` | 私有房间配置、网站事件 | 接入 Codex App / CLI、Claude Code CLI、Antigravity App / CLI 的真实会话,续租与有限重试;`discussion_service.mjs` 常驻托管多个已登记房间;不生成回答,详见 [自动接入程序](runner.md) |
|
|
44
44
|
| `runner/free_runner.mjs` | 自由房间的 inbox、私有房间配置 | 把成员名下的全部待投递请求打包为一次执行,复用同一组软件适配器;只停止该成员自己的进程 |
|
|
45
|
+
| `runner/scheduler.mjs` | 房间配置中的 `scheduler`、本机私有密钥 | 可选智能调度:把中立的选择/判断题转换为 TypeSafe Jev 或 OpenAI Decisions 请求,返回带置信度的类型化答案;不生成文本,不保存密钥 |
|
|
45
46
|
| `scripts/build_pages.mjs` | 后端源码、已构建 `web/`、Pages 静态文件 | 清理固定输出并生成 `dist/pages/`;不部署 |
|
|
46
47
|
|
|
47
48
|
两套后端、Python 运行协议与共用 schema 位于 `backend/`;前端源码及 Vite 配置在 `frontend/`,静态运行文件在 `web/`,测试和构建脚本分别在 `tests/`、`scripts/`。`deploy/` 保存 Docker 文件,`.github/` 保存 CI、贡献指南和安全说明,第三方声明位于 `docs/`。根目录保留用户启动入口和工具约定的配置文件;`python server.py` 仍以项目根目录定位网页与默认数据库,不受终端工作目录影响。
|
|
@@ -226,7 +227,7 @@ Python 通过进程内条件变量唤醒订阅者,不占用数据库连接,
|
|
|
226
227
|
|
|
227
228
|
running 状态即使只有 pending 事件,也必须先暂停才能重新启动或配置模式。网页模式选择先读取最新状态,重试复用既有控制凭据;操作始终绑定点击时的房间,避免慢响应影响后来选中的房间。
|
|
228
229
|
|
|
229
|
-
两种房间的用户写入、运行控制及成员管理都必须携带建房时的房主控制凭据;`actor`、`author`、`submitted_by` 只是声明。`room_modes` 为所有新房间保存控制哈希,`room_members`
|
|
230
|
+
两种房间的用户写入、运行控制及成员管理都必须携带建房时的房主控制凭据;`actor`、`author`、`submitted_by` 只是声明。`room_modes` 为所有新房间保存控制哈希,`room_members` 保存每个实例的独立成员哈希、能力和主持职责。讨论房间的手动本地成员用自己的持久成员凭证;在线成员不能自行写入,由房主或获授主持职责的本地成员代发。成员主持可维护资料、摘要、阶段和安排,不能改名单、授予主持权限、控制自动运行或删除房间。房主凭据不能冒充本地成员提交观点。自动讨论里本机唤醒的成员仍要求本次有效执行凭据,持久成员凭证不能替代执行租约;手动接入的专家(`access=manual`、`adapter=manual`,主持人除外)用自己的成员凭证回答安排,其待派发事件由接入程序跳过,回答保存后由触发器完成事件并唤醒主持人。
|
|
230
231
|
|
|
231
232
|
房间响应的 `runtime.timings` 汇总当前运行各执行的阶段耗时:排队(事件产生到 reserve)、唤醒(reserve 到 AI enter)、执行(enter 到完成、撤销或失败),未结束的阶段计到当前时间;返回最近 20 条与全部合计,进度看板据此展示。升级前的执行没有计时记录,不计入。
|
|
232
233
|
|
|
@@ -243,6 +244,7 @@ running 状态即使只有 pending 事件,也必须先暂停才能重新启动
|
|
|
243
244
|
- **请求**:用户消息按结构化接收者 `to`(成员名)为每位成员生成一条请求;省略且仅有一位在场成员时默认点名该成员,多位成员时交给主持人(`route`),只有在线成员而没有主持人时只作留言,不广播。网页只从正文中代码与引用之外的 `@成员` 生成 `to`,没有 @ 时省略 `to`。请求状态为 `pending`(待投递)→ `delivering`(处理中)→ `completed` / `cancelled` / `failed`。
|
|
244
245
|
- **执行**:`member_executions` 每位成员最多一个 `dispatched/started`(部分唯一索引),不同成员可并行。接入程序用控制凭据 `GET inbox`、`POST executions {member_id, client_id}`(同一 `client_id` 返回同一执行)把成员的全部待投递请求打包;`POST executions/ID {action}` 支持 `renew`(租约 120 秒,不超过单次执行时限,默认 20 分钟)、`fail`(请求标记失败)和 `release`(仅确认未送达时退回队列)。租约过期的执行在下次读取或领取时标记失败,不自动重发。
|
|
245
246
|
- **主持人与任务链**:用户是需求方,不担任主持人。只要有在场的非在线成员,房间就保持恰好一位主持人:成员变动时,离开或移出的主持人失去角色,由第一位在场的 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 完成回交,回交请求按(派活消息,主持人)唯一,重复执行不会重复唤醒。
|
|
247
|
+
- **智能调度改派**:接入程序用控制凭据 `POST requests/ID {action:"schedule", member_id, provider, confidence}` 把一条仍为 `pending` 的 `route` 请求改给一位在场、由本机唤醒的非主持成员,`purpose` 改为 `scheduled`,`created_by` 与 `chain_id` 不变,不消耗任务链预算;同一事务把供应商(`jev` / `decisions`)与 0–1 置信度写入 `request_schedules`(独立表,D1 重新应用 schema 即可,无需改旧表列),房间视图的请求带 `schedule_provider`、`schedule_confidence`,备份、导入与删除一并处理。请求已被主持人领取或已处理返回 409,目标不合格返回 404,供应商或置信度无效返回 400;Worker 用带条件的 `UPDATE` 与同批次 `INSERT … SELECT` 防止与主持人领取并发时丢失请求或留下孤立记录。`inbox` 的待投递项带 `purpose` 供接入程序识别。
|
|
246
248
|
- **回应建议**:普通成员新发言的结构化 `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 发布前显式执行。已有历史消息不补写建议。
|
|
247
249
|
- **在线成员**:`access=online` 的成员只读,不进入 `inbox`,不能用自己的凭证发言;房主以控制凭据代发其回答(`author` 为该成员、`submitted_by` 为用户、`source=proxy`),可带 `request_ids` 关闭它的待处理请求。
|
|
248
250
|
- **关联房间**:从自由房间发起的讨论房间在建房时带 `parent_room_id`,记录在 `room_links`;讨论房间视图有 `parent_room`,自由房间视图列出 `linked_rooms`。头脑风暴的轮次、时长与名单因此天然独立于自由房间。
|
package/docs/cli.md
CHANGED
|
@@ -31,11 +31,11 @@ $env:DISCUSS_URL = 'http://127.0.0.1:8765'
|
|
|
31
31
|
codiscuss room list
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Bash 对应 `export DISCUSS_URL=http://127.0.0.1:8765`,CMD 对应 `set DISCUSS_URL=http://127.0.0.1:8765`。`--site`
|
|
34
|
+
Bash 对应 `export DISCUSS_URL=http://127.0.0.1:8765`,CMD 对应 `set DISCUSS_URL=http://127.0.0.1:8765`。`--site` 优先于环境变量;两者都没有时,使用凭据中绑定的站点(凭据只会发回该站点),因此自动任务只带凭据文件即可;没有任何配置则报错,不默认写入线上。站点须为根地址,不带路径、查询、用户名或密码。客户端使用标准代理设置、校验证书且不跟随重定向。
|
|
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
|
|
38
|
+
`room create` 必须指定 `--access-file 私有文件`,CLI 在请求前原子保存建房编号、房主和各成员凭据,确认房间后另写各本地成员的独立文件,回执只返回 `member_access_files` 路径。结果不明时沿用同一文件、编号和创建参数;不能换文件重建。后续每次写入使用对应身份的文件,不能拿房主文件冒充成员;自动讨论使用本次任务文件中的执行凭据。文件含 `site`、`room`、`token`,CLI 检查站点与房间匹配。同样内容的单行 JSON 文本可放入环境变量 `DISCUSS_ACCESS`(或 `--access-file -` 从标准输入读取),未传 `--access-file` 时使用;成员菜单的“复制凭证文本”复制的就是这段文本,含 token,只交给该成员,不要写进命令行参数或回显。`--dry-run` 不生成或写入凭据,预览会隐藏凭据字段。
|
|
39
39
|
|
|
40
40
|
消息 JSON 中的 `elapsed_seconds` 为只读耗时:自动执行从交付唤醒到保存该消息的秒数,不含排队,保存后固定。用户留言、手动或代发内容及没有计时记录的旧消息返回 `null`;发布命令无需提供耗时。
|
|
41
41
|
|
package/docs/runner.md
CHANGED
|
@@ -318,6 +318,17 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
|
|
|
318
318
|
|
|
319
319
|
每次检查读取 `inbox`:成员没有进行中的执行且有待投递消息时,先持久化本地 `client_id` 再领取执行,然后用原适配器续接该成员的绑定会话;任务文件列出执行编号、待处理消息和已读起点,凭据文件只含成员凭证。进程退出而网站未收到回答、软件报错或超过单次执行时限时报告失败;只有确认未送达才退回队列重试,最多 3 次。领取后、调用前中断的投递不自动重发。主持人的任务文件带 `role: "host"` 和每条请求的 `request_purposes`(`ask` / `route` / `handoff`),说明里写明如何用 `--to` 派活与何时结束。网站上执行被打断、成员离开或被移除后,只停止该成员的进程;不同成员各自投递,可以并行。成员配置了 cwd 且位于 Git 工作区时,执行前后各做一次快照,执行结束且进程退出后报告改动的文件,供网站提示可能冲突;服务重启会丢失尚未报告的快照。只能在网页创建自由房间,不支持对话入口。
|
|
320
320
|
|
|
321
|
+
### 智能调度(可选)
|
|
322
|
+
|
|
323
|
+
新建房间面板在选好 AI 后可勾选“智能调度”,选择供应商(TypeSafe Jev 的 `POST /v1/systemone`,或 OpenAI Decisions 的 `POST /v1/decisions`)、模型(预设 `jev-latest` / `gpt-6-luna`)并填写密钥。密钥经 `api-key` 保存到服务目录,绑定供应商官方根地址;启动请求只带 `scheduler: {provider, model, api_key_ref}`,`validate_config` 核对密钥存在后写入私有房间配置。只在本次由本机服务启动时提供:自由房间至少有一位本机唤醒成员,讨论须勾选“创建后立即启动”。目前只能在建房时开启,已建房间不能中途开启或关闭。
|
|
324
|
+
|
|
325
|
+
`runner/scheduler.mjs` 用中立的题目(`choice` 带选项,`predicate` 为是非题)组装两家的请求,并把答案统一为 `{choice, confidence, probabilities}` 或 `{probability}`。上下文是房间标题、最近 10 条消息(每条截断到 600 字)和待处理的那条用户消息。请求 15 秒超时,错误只报 HTTP 状态,不含响应正文或密钥。
|
|
326
|
+
|
|
327
|
+
- **自由房间**:每次检查先看主持人名下的 `route` 请求,每条只判断一次。选项为各位本机唤醒的非主持成员(描述为软件与项目读写权限)和“交回主持人”;只有选中成员且 `confidence ≥ 0.8` 时,才用 `POST requests/ID {action:"schedule"}` 改派(带供应商与置信度,网站记入请求),随后重读 `inbox`,本轮不再唤醒主持人。判断失败、超时、置信度不足、选择交回主持人或改派返回 409 时,请求留给主持人,行为与未开启相同。成员任务文件对 `scheduled` 请求说明“用户没有点名,由智能调度交给你”。网页在消息上显示“智能调度 → 成员”,悬停显示供应商与置信度;主持人读 `room view` 时也能看到这些字段。
|
|
328
|
+
- **讨论房间**:只给建议,不改变房间。投递主持事件前,若 `held_since` 非 0,对其后的每条用户留言判断一次(请某位专家回应 / 主持人处理 / 仅补充信息),结果以 `scheduler_suggestions` 写入主持任务文件(API 主持人写入首个提示),说明仅供参考;建议在记录投递前完成,失败只少一项建议。
|
|
329
|
+
- 每次判断与结果写入 `service.log`;阈值 0.8 未经实测校准,两家的置信度不可互相套用。面板提示:开启后房间标题与最近消息会发给所选供应商。
|
|
330
|
+
- 2026-10-23 用真实 Jev 密钥核对:`/v1/systemone` 的请求与答案格式与本模块一致;4 条自由房间分派全部合理(直接派出的置信度 0.91),3 条讨论建议中 2 条符合预期,单次 0.5–1.1 秒(首次连接约 5 秒)。该密钥的 `GET /v1/models` 只列出 `jev-latest` 与 `jev-preview`,没有可锁定的版本,默认仍用 `jev-latest`;Jev 升级可能改变判断结果。OpenAI Decisions 仅按文档实现,未用真实密钥调用。
|
|
331
|
+
|
|
321
332
|
普通成员的回应建议不会出现在待投递队列。房主转换后沿用同一请求和执行路径:任务文件的 `request_messages` 指向原成员回答,`chain_id` 属于这次用户确认;重复转换不会再次投递,手动与在线目标仍不进入自动投递。
|
|
322
333
|
|
|
323
334
|
Antigravity App 的 Sidecar 按房间用法校验任务:讨论读取对应事件,自由房间读取对应 `executions/ID` 并核对有效租约。自由执行的受理回执由服务核对并绑定真实会话,即使回答已快速完成也保留绑定;取消、过期或结果不明的投递不重新发送。本次使用替身 Sidecar 与临时服务验证,未重新调用真实 App。
|
|
@@ -398,3 +409,26 @@ npm run runner -- watch --config PRIVATE.json
|
|
|
398
409
|
只有确认软件尚未被调用的失败(如进程未能启动、App 连接尚未建立或 Sidecar 未就绪)才自动重试:首次失败后等 2 秒,再次失败后等 4 秒,最多尝试 3 次;重试复用原事件、执行令牌与任务正文。每次重试或报告失败前先查网站:已经进入则续租,业务已保存则完成,不重发。软件调用已发生后的非零退出、超时或创建回执丢失不能证明未送达,保留 `uncertain` 并继续核对网站业务状态及可用的 Sidecar 回执,受整场截止约束。Codex queue 的原始受理回执保存在本机;不能把缺少执行回执当成投递失败。3 次确认未送出、已启动的 CLI 进程报告失败或实际执行租约到期时按失败暂停。Claude 进程结束时立即判断:非零退出码,或输出里 `is_error` 为真(例如用量上限 429 时退出码仍为 0),都马上以原始错误暂停,不再等单次执行时限;新增独立 CLI 按上节的结构化事件核对。正常退出但网站上本次执行仍未完成时,先重新查看事件,确认仍未保存结果再以“会话已结束,但未保存本次结果”暂停。
|
|
399
410
|
|
|
400
411
|
数据目录固定站点、房间、运行及参与者项目/cwd 配置,修改后拒绝继续复用;需先暂停,明确重新接入。完成以网站业务记录为准,网页中的 dispatched、started、completed 分别表示交付、真实进入、业务保存;收束退出前核对本地最后一项状态。排障须核对原会话与对应事件,不能删库或换编号冒充重试。手动兜底由用户明确选择,通知程序不会自动降级。
|
|
412
|
+
|
|
413
|
+
### 失败提示与下一步
|
|
414
|
+
|
|
415
|
+
暂停原因直接展示给用户,所以文案要说明下一步,且“可稍后恢复”不等于“可以重放结果不明的任务”:
|
|
416
|
+
|
|
417
|
+
- API 的 HTTP 错误按状态分流(`api_status_message`):401 / 403 提示核对密钥与模型权限,404 提示核对地址、请求协议和模型 ID,429 与 5xx 提示等待恢复后再试,并提醒先核对房间里是否已有本次内容;其他状态保持通用说明。网络超时与连接失败不区分“网络不通”和“地址有误”,两者都在提示里。
|
|
418
|
+
- 进程退出但结果未保存(讨论为“会话已结束,但未保存本次结果”,自由房间为“未提交本次回答”)与自由房间的“超过单次执行时限”都附带“请核对原会话确认是否已有内容”。
|
|
419
|
+
- 不识别的软件原始错误保持原样,不猜额度恢复时间,不自动重发。
|
|
420
|
+
|
|
421
|
+
### 适配器能力与已知限制
|
|
422
|
+
|
|
423
|
+
各入口对“进入、成功、停止、结果不明”的含义不同,业务层不能把它们当作等价信号。成功只看网站是否保存了本次业务结果;会话 ID 出现、进程退出或受理回执都不等于完成。
|
|
424
|
+
|
|
425
|
+
| 入口 | “已进入”的依据 | 停止时 | 已验证范围 |
|
|
426
|
+
| --- | --- | --- | --- |
|
|
427
|
+
| Codex App、Antigravity App(Sidecar) | AI 自己执行 `execution enter`(自由房间无此步) | runner 没有进程可停,只能在服务端撤销执行,迟到回答被拒;自由房间的执行失联会显示“停止未确认” | 迟到回答被拒有测试 |
|
|
428
|
+
| `codex_cli`、`antigravity_cli` | runner 在会话 ID 首次上报时代为进入(`thread.started` / `init`),此时模型不一定已读到任务 | `child.kill()`,只结束直接子进程 | Codex 在 Windows 上 1 次实测未见工具进程残留;agy 未验证 |
|
|
429
|
+
| `claude` | runner 在进程启动成功后代为进入,此时模型不一定已读到任务 | Windows 用 `taskkill /T /F` 结束整个进程树,其他系统普通结束 | Windows 真实孙进程测试 |
|
|
430
|
+
| `api` | runner 在调用模型前自己进入 | 中止请求 | — |
|
|
431
|
+
|
|
432
|
+
所有入口:确认未送出才有界重试;已调用而送达未知时不自动重发,讨论房间记为 `uncertain` 并继续核对网站,自由房间直接按“投递失败”结束这次执行;进程正常退出但结果未保存时先核对网站,仍未保存则以失败收尾(讨论房间暂停,自由房间只影响该成员的这次执行);已绑定会话的项目、cwd、接入方式或绑定 ID 变化时阻止并要求重新接入(模型与强度在进行中的讨论里只读)。
|
|
433
|
+
|
|
434
|
+
新增适配器上线前须过四项:停止后无工具进程残留(在真实进程上验证)、迟到回答被拒、重启后不重发、结果未保存时核对后以失败收尾。
|
package/docs/workflow.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## 选择推进方式
|
|
6
6
|
|
|
7
|
-
- **讨论(全自动)**:主持人与专家都由本机服务唤醒(CLI / App / API)。本机软件与权限配置完成后明确启动,可按轮次、总时长上限或手动停止结束。主持创建正式安排即结束本步;专家自行认领、回答,程序再续接原主持。自动任务不另建原生定时。项目及 cwd
|
|
7
|
+
- **讨论(全自动)**:主持人与专家都由本机服务唤醒(CLI / App / API)。本机软件与权限配置完成后明确启动,可按轮次、总时长上限或手动停止结束。主持创建正式安排即结束本步;专家自行认领、回答,程序再续接原主持。自动任务不另建原生定时。项目及 cwd 可选,分别属于本机接入配置。本地专家可单独选“手动接入”:成员记录带 `adapter: manual`,不被唤醒,凭自己的成员凭证认领发言权并提交安排内的回答,接入程序不派发它的事件,回答保存后照常续接主持人;主持人必须由本机唤醒。手动专家不回答时讨论停在该安排,由用户取消安排(主持人被唤醒重新安排),或受轮次与总时长上限约束,没有单独的自动超时。模式在手动与全自动间切换时,带 `adapter: manual` 的专家保持手动。网页 AI 会打断自动推进,只进入自由协作。
|
|
8
8
|
- **自由协作**:你或 AI 主持人点名一位或多位成员,各自在原会话处理请求;没有讨论发言权、安排和轮次。每位成员单独选择本机唤醒、手动接入或网页代发,见下方 [自由协作房间](#自由协作房间)。
|
|
9
|
-
- **手动接入**:自由协作成员的一种接入方式。本地成员持自己的凭据文件自行读写,可由人工或软件原生定时触发;网页成员只能阅读房间,回答需转交代发;网页 AI
|
|
9
|
+
- **手动接入**:自由协作成员的一种接入方式。本地成员持自己的凭据文件自行读写,可由人工或软件原生定时触发;网页成员只能阅读房间,回答需转交代发;网页 AI 从其服务器读取阅读链接,本机网站对它不可达,此时由用户粘贴材料。只有这两种成员时建房无需本机接入服务。复制参与指令、下载凭据文件或复制凭证文本都不代表已经送达、接入或唤醒。
|
|
10
10
|
|
|
11
11
|
讨论运行中可在进度看板邀请专家(经本机服务配置后加入;Codex CLI / App、Claude 可填写已有会话 ID,续接这个带有自己上下文的原会话,标注为借用会话),或从成员菜单让专家暂离、请回或移除。暂离会取消它当前与排队中的安排,保留位置与会话,不计入之后的轮次;请回换发新凭证、续接原会话。名单变化不暂停讨论,排队中的安排作废,主持人被唤醒重新安排;主持人本身不能暂离或移除,更换主持人仍会暂停;正在发言(有进行中安排)的专家须先暂停讨论才能暂离或移除。暂停后可对本轮已发言的专家选择“作废本轮发言”:彻底删除其最近一条观点,该专家重新计为本轮未发言(不新增轮次),恢复后由主持人重新安排。故障只暂停并保留原因,不静默换软件、身份或创建第二个会话。
|
|
12
12
|
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
在线 AI 以“网页代发”成员加入:点名后,在成员栏复制阅读通知交给它;拿到回答后,在输入框选择“代发”该成员并粘贴原文,记录保留它为作者、你为提交者,并关闭它的待处理请求。
|
|
26
26
|
|
|
27
|
+
新建房间时可勾选“智能调度”(可选,需本机服务,供应商为 TypeSafe Jev 或 OpenAI Decisions,按次计费)。开启后,没点名的消息先由调度模型判断:有把握能直接交给某位本机唤醒的成员时立即派给它,消息上显示“智能调度 → 成员”,省去一次主持人分派;拿不准、需要拆分协调或调用失败时,仍交给主持人。目前只能在建房时开启。
|
|
28
|
+
|
|
27
29
|
需要深入讨论时,在成员栏点“发起头脑风暴”:用当前由本机唤醒的成员新建一场全自动讨论房间,手动和网页成员不随行,轮次和时长独立。讨论收束后,在议题卡片点“把结论发回”,结论会作为留言回到自由房间,不唤醒任何 AI。
|
|
28
30
|
|
|
29
31
|
多位成员共享同一个工作目录时不加锁,由你划分各自负责的文件。成员栏会提示“可能的改动冲突”:两位成员在重叠时段都改动了同一文件时列出文件名。提示依据执行前后的 Git 工作区对比,无法确定修改者,未出现提示也不代表没有覆盖;非 Git 目录不提供提示。
|
|
@@ -97,7 +99,7 @@ stateDiagram-v2
|
|
|
97
99
|
|
|
98
100
|
主持人可以一次排好本轮顺序(`turn create --plan-file`):第一项立即生效,其余为 `queued`。专家保存回答后,网站在同一事务中把发言权交给下一位排队专家;末位答完才交回主持人。接力不发通知,需要逐个转交的在线专家不放进整轮。
|
|
99
101
|
|
|
100
|
-
用户留言或修改目标、背景、边界、焦点会记为待主持人处理(`held_since`),不打断正在发言的专家;该专家保存回答后交回主持人而不接力。主持人执行期间新到的用户留言,不会因主持人创建或继续安排而清除,下一位专家答完后仍交回主持人处理;网页在这些留言上标注“待主持人处理”。要立即生效,用户在输入框点「停止」或在看板点打断:当前安排取消、本次执行作废并交回主持人,排队安排保留。主持人处理后用 `turn continue` 按原计划继续,或直接创建新安排,剩余排队改为 `skipped
|
|
102
|
+
用户留言或修改目标、背景、边界、焦点会记为待主持人处理(`held_since`),不打断正在发言的专家;该专家保存回答后交回主持人而不接力。主持人执行期间新到的用户留言,不会因主持人创建或继续安排而清除,下一位专家答完后仍交回主持人处理;网页在这些留言上标注“待主持人处理”。要立即生效,用户在输入框点「停止」或在看板点打断:当前安排取消、本次执行作废并交回主持人,排队安排保留。主持人处理后用 `turn continue` 按原计划继续,或直接创建新安排,剩余排队改为 `skipped`。建房时开启“智能调度”的讨论,主持人被唤醒时会附带对这些留言的分流建议(请某位专家回应、主持人处理或仅补充信息),只供主持人参考,不自动执行。暂停保留当前与排队安排,恢复后由原专家继续;提前结束、到限处理、收束或换人会取消全部排队安排。
|
|
101
103
|
|
|
102
104
|
本地专家自行领取与归还;在线专家由主持人以自身操作者身份、指定原专家代操作。主持人不能代领另一位本地专家的发言权。主动归还后未完成安排仍存在,主持人需取消或处理。正常推进不超时夺取;配置的到限处理会取消未完成安排并撤销旧自动执行,再交回主持人。
|
|
103
105
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai_co_discuss",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"bin": {
|
|
5
5
|
"codiscuss": "runner/codiscuss.mjs"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dev": "npm run build && wrangler pages dev --ip 127.0.0.1 --port 8787",
|
|
9
9
|
"db:local": "wrangler d1 execute DB --local --file backend/schema.sql",
|
|
10
10
|
"db:remote": "wrangler d1 execute DB --remote --file backend/schema.sql",
|
|
11
|
-
"check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_models.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs && node --check runner/discussion_api.mjs && node --check runner/api_participant.mjs",
|
|
11
|
+
"check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_models.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs && node --check runner/discussion_api.mjs && node --check runner/api_participant.mjs && node --check runner/scheduler.mjs",
|
|
12
12
|
"runner": "node runner/discussion_runner.mjs",
|
|
13
13
|
"service": "node runner/discussion_service.mjs",
|
|
14
14
|
"build:ui": "vite build --config frontend/vite.config.mjs --mode markdown && vite build --config frontend/vite.config.mjs --mode backup && vite build --config frontend/vite.config.mjs",
|
|
@@ -154,7 +154,8 @@ async function run_participant(profile, key, context) {
|
|
|
154
154
|
const room = await read_room(context);
|
|
155
155
|
const definitions = participant_tools(context);
|
|
156
156
|
const prompt = JSON.stringify({ room, task: { role: context.role, turn_id: context.turn_id,
|
|
157
|
-
execution_id: context.execution_id, requests: context.requests, read_after: context.read_after
|
|
157
|
+
execution_id: context.execution_id, requests: context.requests, read_after: context.read_after,
|
|
158
|
+
...(context.scheduler_suggestions?.length ? { scheduler_suggestions: context.scheduler_suggestions } : {}) } });
|
|
158
159
|
const state = { system: system_prompt(context), history: api_history(profile, prompt), tools: definitions };
|
|
159
160
|
for (let index = 0; index < MAX_MODEL_TURNS; index++) {
|
|
160
161
|
const calls = await api_generate(profile, key, state, context.signal);
|
package/runner/discuss.py
CHANGED
|
@@ -140,14 +140,14 @@ def attachment_id(value):
|
|
|
140
140
|
def add_common(parser):
|
|
141
141
|
"""Allow common flags before or after subcommands without shared state."""
|
|
142
142
|
options = {"default": argparse.SUPPRESS}
|
|
143
|
-
parser.add_argument("--site", help="HTTP 服务地址;未指定时读 DISCUSS_URL
|
|
143
|
+
parser.add_argument("--site", help="HTTP 服务地址;未指定时读 DISCUSS_URL,再读凭据中的 site", **options)
|
|
144
144
|
parser.add_argument("--as", dest="actor", type=participant,
|
|
145
145
|
help="业务写入的声明操作者;不是登录认证;api 使用原始请求体", **options)
|
|
146
146
|
parser.add_argument("-o", "--output", choices=("json", "text"),
|
|
147
147
|
help="默认 json;成功 stdout,错误 stderr", **options)
|
|
148
148
|
parser.add_argument("--timeout", type=float, help="请求超时秒数,默认 20", **options)
|
|
149
149
|
parser.add_argument("--dry-run", action="store_true", help="只预览写入请求,不联网", **options)
|
|
150
|
-
parser.add_argument("--access-file", help="本房间的受限凭据 JSON
|
|
150
|
+
parser.add_argument("--access-file", help="本房间的受限凭据 JSON 文件(- 读标准输入);未指定时读环境变量 DISCUSS_ACCESS 中的同样 JSON 文本;不输出其 token", **options)
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def add_text(parser, name, required=False):
|
|
@@ -362,6 +362,22 @@ def read_object(path):
|
|
|
362
362
|
return value
|
|
363
363
|
|
|
364
364
|
|
|
365
|
+
def load_access(args):
|
|
366
|
+
"""Read the room credential from --access-file, else from the DISCUSS_ACCESS JSON text; empty when neither is given."""
|
|
367
|
+
if getattr(args, "access_file", None):
|
|
368
|
+
return read_object(args.access_file)
|
|
369
|
+
text = os.getenv("DISCUSS_ACCESS", "").strip()
|
|
370
|
+
if not text:
|
|
371
|
+
return {}
|
|
372
|
+
try:
|
|
373
|
+
value = json.loads(text)
|
|
374
|
+
except ValueError as error:
|
|
375
|
+
raise CliError(f"DISCUSS_ACCESS 不是有效 JSON: {error}") from error
|
|
376
|
+
if not isinstance(value, dict):
|
|
377
|
+
raise CliError("DISCUSS_ACCESS 必须是 JSON 对象")
|
|
378
|
+
return value
|
|
379
|
+
|
|
380
|
+
|
|
365
381
|
def text_field(args, field):
|
|
366
382
|
"""Read one literal or file-backed field without altering its content."""
|
|
367
383
|
values = vars(args)
|
|
@@ -588,7 +604,7 @@ def plan_execution(args):
|
|
|
588
604
|
"""Use the bound actual session when acknowledging a delivered execution."""
|
|
589
605
|
body = {"action": args.action}
|
|
590
606
|
if args.action == "enter":
|
|
591
|
-
access =
|
|
607
|
+
access = load_access(args)
|
|
592
608
|
body["session_id"] = args.session_id or access.get("session_id")
|
|
593
609
|
if not body["session_id"]:
|
|
594
610
|
raise CliError("尚未取得实际 session_id,请先核对接入回执")
|
|
@@ -618,13 +634,13 @@ def build_plan(args):
|
|
|
618
634
|
def resolve_site(args):
|
|
619
635
|
"""Resolve the flag, then the environment, then the site the access file is bound to; never a built-in default."""
|
|
620
636
|
site = getattr(args, "site", None) or os.getenv("DISCUSS_URL", "")
|
|
621
|
-
if not site
|
|
622
|
-
# The
|
|
623
|
-
site = str(
|
|
637
|
+
if not site:
|
|
638
|
+
# The credential's own site is the only place it is ever sent back to.
|
|
639
|
+
site = str(load_access(args).get("site", ""))
|
|
624
640
|
parsed = urlsplit(site)
|
|
625
641
|
if (parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username
|
|
626
642
|
or parsed.password or parsed.query or parsed.fragment or parsed.path not in ("", "/")):
|
|
627
|
-
raise CliError("请用 --site、DISCUSS_URL
|
|
643
|
+
raise CliError("请用 --site、DISCUSS_URL 或凭据(--access-file / DISCUSS_ACCESS)指定 http(s)://主机[:端口],不带路径、凭据或查询")
|
|
628
644
|
try:
|
|
629
645
|
parsed.port
|
|
630
646
|
except ValueError as error:
|
|
@@ -749,7 +765,7 @@ def command_schema(args, registry):
|
|
|
749
765
|
name = " ".join(args.command)
|
|
750
766
|
if not name:
|
|
751
767
|
return {"version": VERSION, "commands": {key: item.description for key, item in registry.items()},
|
|
752
|
-
"exit_codes": EXIT_CODES, "site": "--site > DISCUSS_URL >
|
|
768
|
+
"exit_codes": EXIT_CODES, "site": "--site > DISCUSS_URL > 凭据(--access-file 或 DISCUSS_ACCESS)中的 site;无内置默认站点",
|
|
753
769
|
"output": "默认 JSON;成功 stdout,错误 stderr;无交互提示、无自动重试"}
|
|
754
770
|
if name not in registry:
|
|
755
771
|
raise CliError(f"未知命令 {name};运行 discuss schema 查看可用命令")
|
|
@@ -858,9 +874,9 @@ def finish_creation_access(args, site, plan, value):
|
|
|
858
874
|
|
|
859
875
|
def access_headers(args, site, path):
|
|
860
876
|
"""Load a room-scoped secret only for its exact site and room."""
|
|
861
|
-
|
|
877
|
+
access = load_access(args)
|
|
878
|
+
if not access:
|
|
862
879
|
return {}
|
|
863
|
-
access = read_object(args.access_file)
|
|
864
880
|
expected = f"/api/rooms/{access.get('room', '')}"
|
|
865
881
|
target = urlsplit(path).path
|
|
866
882
|
creation = target == "/api/rooms" and bool(access.get("submission_id"))
|