ai_co_discuss 0.2.71 → 0.2.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/discuss-host/references/manual.md +1 -1
- package/.agents/skills/discuss-participant/SKILL.md +1 -1
- package/.agents/skills/discuss-participant/references/free.md +2 -0
- package/backend/free_room.py +54 -5
- package/backend/schema.sql +11 -0
- package/backend/server.py +25 -13
- package/docs/architecture.md +280 -280
- package/docs/deployment.md +225 -225
- package/docs/runner.md +455 -455
- package/docs/skills.md +1 -1
- package/docs/workflow.md +157 -155
- package/package.json +1 -1
- package/runner/api_participant.mjs +235 -230
- package/runner/discussion_adapters.mjs +70 -2
- package/runner/free_runner.mjs +39 -3
- package/web/app.js +3382 -3384
- package/web/icons/typesafe.png +0 -0
- package/web/modern.css +1 -1
- package/web/modern.js +2174 -2042
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
## 在线专家:协助方式与操作恢复
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
请专家回答时,回答较长的同时提供简述与完整正文;简述的写法、保存与署名遵循 discuss-participant。若长回答缺少简述,主持人可据完整原文另行整理并注明摘要归主持人,专家正文保持原样。
|
|
38
38
|
|
|
39
39
|
选择「用户手动复制协助」时,每轮在当前聊天给出完整、可复制的提问文本和对应安排编号,由用户发到专家原聊天并贴回完整回答;主持人不自行操作该专家网页或发送问题。用户确认送达后再记录通知,收到并核对对应回答后通过 CLI 原文代发。展示提问文本不算送达,等待用户期间不重复催促。
|
|
40
40
|
|
|
@@ -81,7 +81,7 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
|
|
|
81
81
|
|
|
82
82
|
可以坚持有依据的少数意见,也可以因新证据修改判断并解释变化。主动检查关键反例与遗漏,但不为反对而反对,不以赞同人数代替证据;材料不足时说明什么证据会改变结论。需要对方回应时直接 `@专家名` 并说明具体问题;@ 只是讨论线索,不会通知。
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
篇幅和结构由问题决定。保留关键数字、条件与失败情形;回应追问只补新论证或观点变化,旧内容用原文编号引用。回答摘要按需填写(讨论与自由协作相同):正文较长、读者需要先看结论时,用一两句写主要判断或变化及关键条件;简短回答不写,不把正文复述一遍。完整论证与出处放正文,不再复制一遍长清单。
|
|
85
85
|
|
|
86
86
|
示例(只示意密度与结构,内容与篇幅按问题而定):
|
|
87
87
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
没有安排和阶段,不执行 `execution enter`。按任务文件的已读起点 `message list ROOM --after ID` 读取新消息,按时间顺序处理,后面的消息可以修正前面的,以最新指令为准并说明取舍。完成后用 `message post ROOM --as 本人 --kind chat --execution 执行编号 --access-file FILE` 提交一次回答。
|
|
8
8
|
|
|
9
|
+
任务需要多步操作时,在关键转折处(读完材料、改定方案、验证结果)用一两句中文写出进展,不另调命令:本机接入程序会把这些中途说明显示在网页的处理中卡片,让用户知道你在做什么。只写进展与判断,不复述工具输出,不代替最终回答。
|
|
10
|
+
|
|
9
11
|
提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 `--execution` 改作普通发言,因为那会让已失效的执行继续产生可见结果。
|
|
10
12
|
|
|
11
13
|
手动接入的自由成员没有执行编号:从 `room view` 的 `free.requests` 找到自己的待处理请求,回答时用 `--request 请求编号` 关闭。
|
package/backend/free_room.py
CHANGED
|
@@ -24,6 +24,12 @@ RECENT_REQUESTS = 100
|
|
|
24
24
|
RECENT_EXECUTIONS = 100
|
|
25
25
|
RECENT_CHANGES = 50
|
|
26
26
|
MAX_CHANGED_FILES = 200
|
|
27
|
+
# What a member's software last showed it doing, as reported by the runner with each renewal.
|
|
28
|
+
ACTIVITY_PHASES = ("", "thinking", "command", "tool", "editing", "writing")
|
|
29
|
+
# Progress notes per execution, their length, and how many one renewal may carry.
|
|
30
|
+
MAX_EXECUTION_NOTES = 50
|
|
31
|
+
MAX_NOTE_LENGTH = 500
|
|
32
|
+
MAX_NOTES_PER_RENEW = 10
|
|
27
33
|
MAX_PATH_LENGTH = 500
|
|
28
34
|
ACTIVE = ("dispatched", "started")
|
|
29
35
|
ACCESS_MODES = ("runner", "manual", "online")
|
|
@@ -44,10 +50,12 @@ QUEUE_ASKED = "EXISTS(SELECT 1 FROM member_requests r WHERE r.message_id={row}.m
|
|
|
44
50
|
SESSION_MODES = ("new", "borrowed")
|
|
45
51
|
FREE_OPERATIONS = ("messages", "attachments", "move", "folder", "archive", "restore", "delete")
|
|
46
52
|
MEMBER_FIELDS = ("id", "name", "adapter", "role", "status", "access", "session_mode", "read_until")
|
|
47
|
-
EXECUTION_FIELDS = ("id", "member_id", "status", "session_id", "lease_until", "created_at", "finished_at", "result"
|
|
53
|
+
EXECUTION_FIELDS = ("id", "member_id", "status", "session_id", "lease_until", "created_at", "finished_at", "result",
|
|
54
|
+
"activity", "activity_at")
|
|
48
55
|
# Columns added after the first free-room tables; Python databases created by that build gain them here.
|
|
49
56
|
LATER_COLUMNS = {"room_members": {"access": "TEXT NOT NULL DEFAULT 'runner'", "session_mode": "TEXT NOT NULL DEFAULT 'new'"},
|
|
50
|
-
"member_executions": {"changes": "TEXT NOT NULL DEFAULT ''", "cwd": "TEXT NOT NULL DEFAULT ''"
|
|
57
|
+
"member_executions": {"changes": "TEXT NOT NULL DEFAULT ''", "cwd": "TEXT NOT NULL DEFAULT ''",
|
|
58
|
+
"activity": "TEXT NOT NULL DEFAULT ''", "activity_at": "INTEGER NOT NULL DEFAULT 0"},
|
|
51
59
|
"member_requests": {"chain_id": "INTEGER NOT NULL DEFAULT 0", "batch_id": "INTEGER NOT NULL DEFAULT 0",
|
|
52
60
|
"purpose": "TEXT NOT NULL DEFAULT 'ask'", "created_by": "TEXT NOT NULL DEFAULT 'user'"}}
|
|
53
61
|
|
|
@@ -187,16 +195,30 @@ def public_execution(row):
|
|
|
187
195
|
return {key: row[key] for key in EXECUTION_FIELDS} if row else None
|
|
188
196
|
|
|
189
197
|
|
|
198
|
+
def note_count(connection, execution_id):
|
|
199
|
+
"""How many progress notes an execution has; the texts are read separately so room views stay small."""
|
|
200
|
+
return connection.execute("SELECT COUNT(*) FROM execution_notes WHERE execution_id=?", (execution_id,)).fetchone()[0]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def execution_notes(connection, room_id, execution_id):
|
|
204
|
+
"""Read one execution's progress notes in order, with the same access as the room view."""
|
|
205
|
+
if not connection.execute("SELECT 1 FROM member_executions WHERE room_id=? AND id=?", (room_id, execution_id)).fetchone():
|
|
206
|
+
raise LookupError("执行不存在")
|
|
207
|
+
return {"notes": [dict(row) for row in connection.execute(
|
|
208
|
+
"SELECT seq,text,created_at FROM execution_notes WHERE execution_id=? ORDER BY seq", (execution_id,))]}
|
|
209
|
+
|
|
210
|
+
|
|
190
211
|
def recent_executions(connection, room_id):
|
|
191
212
|
"""Return bounded public reports, including requests no longer in the recent request list."""
|
|
192
213
|
rows = connection.execute("""SELECT e.*,
|
|
193
214
|
(SELECT json_group_array(id) FROM member_requests WHERE execution_id=e.id) AS request_ids,
|
|
194
215
|
(SELECT MAX(answer_id) FROM member_requests WHERE execution_id=e.id) AS answer_id,
|
|
195
|
-
(SELECT MIN(message_id) FROM member_requests WHERE execution_id=e.id) AS request_message_id
|
|
216
|
+
(SELECT MIN(message_id) FROM member_requests WHERE execution_id=e.id) AS request_message_id,
|
|
217
|
+
(SELECT COUNT(*) FROM execution_notes WHERE execution_id=e.id) AS note_count
|
|
196
218
|
FROM member_executions e WHERE room_id=? ORDER BY id DESC LIMIT ?""", (room_id, RECENT_EXECUTIONS))
|
|
197
219
|
return [{**public_execution(row), "cwd": row["cwd"], "changes": json.loads(row["changes"] or "[]"),
|
|
198
220
|
"changes_recorded": bool(row["changes"]), "request_ids": json.loads(row["request_ids"]),
|
|
199
|
-
"answer_id": row["answer_id"], "request_message_id": row["request_message_id"]} for row in rows]
|
|
221
|
+
"answer_id": row["answer_id"], "request_message_id": row["request_message_id"], "notes": row["note_count"]} for row in rows]
|
|
200
222
|
|
|
201
223
|
|
|
202
224
|
def possible_conflicts(connection, room_id, names):
|
|
@@ -245,6 +267,7 @@ def free_view(connection, room_id):
|
|
|
245
267
|
member["execution"] = public_execution(latest)
|
|
246
268
|
if latest and latest["status"] in ACTIVE:
|
|
247
269
|
member["execution"]["stale"] = latest["lease_until"] <= now
|
|
270
|
+
member["execution"]["notes"] = note_count(connection, latest["id"])
|
|
248
271
|
member["pending"] = connection.execute("SELECT COUNT(*) FROM member_requests WHERE member_id=? AND status='pending'",
|
|
249
272
|
(row["id"],)).fetchone()[0]
|
|
250
273
|
members.append(member)
|
|
@@ -1037,6 +1060,26 @@ def changed_files(payload):
|
|
|
1037
1060
|
return sorted(set(files))
|
|
1038
1061
|
|
|
1039
1062
|
|
|
1063
|
+
def renewed_activity(payload, row, now):
|
|
1064
|
+
"""Validate the runner's reported phase; its output time is kept within the execution, since the runner's clock may differ."""
|
|
1065
|
+
activity, activity_at = payload.get("activity"), payload.get("activity_at")
|
|
1066
|
+
if activity not in ACTIVITY_PHASES:
|
|
1067
|
+
raise ValueError(f"activity 须为 {'、'.join(item for item in ACTIVITY_PHASES if item)} 或空")
|
|
1068
|
+
if type(activity_at) is not int:
|
|
1069
|
+
raise ValueError("activity_at 须为秒级时间戳")
|
|
1070
|
+
return activity, min(max(activity_at, row["created_at"]), now)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def renewed_notes(payload):
|
|
1074
|
+
"""Validate progress notes carried by a renewal; a repeated sequence number is the same note resent."""
|
|
1075
|
+
notes = payload.get("notes", [])
|
|
1076
|
+
if (not isinstance(notes, list) or len(notes) > MAX_NOTES_PER_RENEW or any(
|
|
1077
|
+
not isinstance(note, dict) or type(note.get("seq")) is not int or not 1 <= note["seq"] <= MAX_EXECUTION_NOTES
|
|
1078
|
+
or not isinstance(note.get("text"), str) or not 0 < len(note["text"]) <= MAX_NOTE_LENGTH for note in notes)):
|
|
1079
|
+
raise ValueError(f"notes 须为最多 {MAX_NOTES_PER_RENEW} 条 {{seq: 1–{MAX_EXECUTION_NOTES}, text: 1–{MAX_NOTE_LENGTH} 字}}")
|
|
1080
|
+
return notes
|
|
1081
|
+
|
|
1082
|
+
|
|
1040
1083
|
def execution_action(connection, room_id, execution_id, token, payload):
|
|
1041
1084
|
"""Runner-side lifecycle: renew, fail, release undelivered work, or report observed file changes."""
|
|
1042
1085
|
require_owner(connection, room_id, token)
|
|
@@ -1057,7 +1100,13 @@ def execution_action(connection, room_id, execution_id, token, payload):
|
|
|
1057
1100
|
limit = row["created_at"] + execution_minutes(connection, room_id) * 60
|
|
1058
1101
|
if row["lease_until"] <= now or limit <= now:
|
|
1059
1102
|
raise FileExistsError("执行租约或单次时限已到,不能续租")
|
|
1060
|
-
|
|
1103
|
+
# Older runners renew without activity; the last reported one is kept.
|
|
1104
|
+
activity, activity_at = renewed_activity(payload, row, now) if "activity" in payload else (row["activity"], row["activity_at"])
|
|
1105
|
+
notes = renewed_notes(payload)
|
|
1106
|
+
connection.execute("UPDATE member_executions SET lease_until=?,activity=?,activity_at=? WHERE id=?",
|
|
1107
|
+
(min(now + LEASE_SECONDS, limit), activity, activity_at, execution_id))
|
|
1108
|
+
connection.executemany("INSERT OR IGNORE INTO execution_notes (execution_id,seq,room_id,text,created_at) VALUES (?,?,?,?,?)",
|
|
1109
|
+
[(execution_id, note["seq"], room_id, note["text"], now) for note in notes])
|
|
1061
1110
|
elif action == "fail":
|
|
1062
1111
|
finish_execution(connection, execution_id, "failed", str(payload.get("reason") or "执行失败"), "failed")
|
|
1063
1112
|
settle_batches(connection, room_id)
|
package/backend/schema.sql
CHANGED
|
@@ -532,8 +532,19 @@ CREATE TABLE IF NOT EXISTS member_executions (
|
|
|
532
532
|
result TEXT NOT NULL DEFAULT '',
|
|
533
533
|
changes TEXT NOT NULL DEFAULT '',
|
|
534
534
|
cwd TEXT NOT NULL DEFAULT '',
|
|
535
|
+
activity TEXT NOT NULL DEFAULT '',
|
|
536
|
+
activity_at INTEGER NOT NULL DEFAULT 0,
|
|
535
537
|
UNIQUE(room_id,client_id)
|
|
536
538
|
);
|
|
539
|
+
-- Progress notes the runner saw a member's software write between tool calls; numbered per execution so a retried renewal adds nothing.
|
|
540
|
+
CREATE TABLE IF NOT EXISTS execution_notes (
|
|
541
|
+
execution_id INTEGER NOT NULL REFERENCES member_executions(id) ON DELETE CASCADE,
|
|
542
|
+
seq INTEGER NOT NULL,
|
|
543
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
544
|
+
text TEXT NOT NULL,
|
|
545
|
+
created_at INTEGER NOT NULL,
|
|
546
|
+
PRIMARY KEY(execution_id,seq)
|
|
547
|
+
);
|
|
537
548
|
-- One live execution per member: different members may run in parallel, one session never twice.
|
|
538
549
|
CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
|
|
539
550
|
WHERE status IN ('dispatched','started');
|
package/backend/server.py
CHANGED
|
@@ -26,7 +26,7 @@ from .free_room import (add_member, after_free_message, authorize_free_write, ch
|
|
|
26
26
|
discussion_members, room_mode, save_room_access, sync_discussion_members, view_execution)
|
|
27
27
|
from .free_room import add_discussion_member, convert_suggestion, discussion_member_action, message_suggestions, save_suggestions, sync_participants
|
|
28
28
|
from .free_room import active_members, adopt_recommendation, ask_members, asks_view, checked_asks, recommend_member, register_member, route_held_messages
|
|
29
|
-
from .free_room import reorder_speakers, speaking_order, stop_room_work
|
|
29
|
+
from .free_room import execution_notes, reorder_speakers, speaking_order, stop_room_work
|
|
30
30
|
from .identity import MAX_ROOM_AGENTS, PARTICIPANTS, is_ai, is_participant
|
|
31
31
|
from .room_import import import_room, imported_attachment
|
|
32
32
|
from .runtime import (authorize_write, begin_write, configure_runtime, event_action,
|
|
@@ -61,7 +61,8 @@ ASSETS = {"/": ("index.html", "text/html"),
|
|
|
61
61
|
"/markdown.js": ("markdown.js", "text/javascript"),
|
|
62
62
|
"/backup.js": ("backup.js", "text/javascript"),
|
|
63
63
|
"/markdown.css": ("markdown.css", "text/css"),
|
|
64
|
-
"/icons/LICENSE.txt": ("icons/LICENSE.txt", "text/plain")
|
|
64
|
+
"/icons/LICENSE.txt": ("icons/LICENSE.txt", "text/plain"),
|
|
65
|
+
"/icons/typesafe.png": ("icons/typesafe.png", "image/png")}
|
|
65
66
|
ASSETS.update({f"/icons/{name}.svg": (f"icons/{name}.svg", "image/svg+xml")
|
|
66
67
|
for name in ("openai", "claude", "gemini", "deepseek", "qwen", "user")})
|
|
67
68
|
ASSETS.update({"/pdfjs/" + file.relative_to(WEB_ROOT / "pdfjs").as_posix():
|
|
@@ -223,12 +224,14 @@ def require_room(connection, room_id):
|
|
|
223
224
|
room["round_budget"] = dict(budget) if budget else None
|
|
224
225
|
if budget:
|
|
225
226
|
room["opinion_count"] = opinion_count(connection, room_id, room["experts"])
|
|
226
|
-
|
|
227
|
+
session = connection.execute("SELECT id,goal,status,started_message_id,ended_message_id,created_at FROM discussion_sessions WHERE room_id=? ORDER BY id DESC LIMIT 1", (room_id,)).fetchone()
|
|
228
|
+
room["session"] = dict(session) if session else None
|
|
229
|
+
room["round_groups"] = budget_round_groups(connection, room_id, room["experts"], budget["base"], room["session"]) if budget else room["rounds"]
|
|
227
230
|
room["runtime"] = runtime_view(connection, room_id)
|
|
228
231
|
room["limits"] = limits_view(connection.execute(LIMITS_SQL, (room_id,)).fetchone(), room)
|
|
229
232
|
room["mode"] = room_mode(connection, room_id)
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
if room["mode"] == "free" and not session:
|
|
234
|
+
room["round_groups"] = []
|
|
232
235
|
room["members"] = [dict(row) for row in connection.execute(
|
|
233
236
|
"""SELECT m.id,m.name,m.adapter,m.role,m.status,m.access,m.session_mode,COALESCE(s.session_id,'') AS reported_session,COALESCE(s.client,'') AS reported_client,
|
|
234
237
|
COALESCE(r.registered_at,'') AS registered_at
|
|
@@ -326,27 +329,33 @@ def discussion_rounds(connection, room_id, experts):
|
|
|
326
329
|
return rounds
|
|
327
330
|
|
|
328
331
|
|
|
329
|
-
def budget_round_groups(connection, room_id, experts, base):
|
|
332
|
+
def budget_round_groups(connection, room_id, experts, base, session):
|
|
330
333
|
"""Group history by the round budget's rule: every len(experts) opinions form a round, and a new segment (base) starts a new one.
|
|
331
334
|
|
|
335
|
+
Only messages of the latest discussion are grouped, numbered from 1; free collaboration before or after it has no rounds.
|
|
332
336
|
`rounds` stays speaker-based because scheduling needs who still owes a turn; this view only labels history.
|
|
333
337
|
"""
|
|
334
|
-
if not experts:
|
|
338
|
+
if not experts or not session:
|
|
335
339
|
return []
|
|
340
|
+
first, last = session["started_message_id"], session["ended_message_id"] or float("inf")
|
|
336
341
|
rows = connection.execute("""SELECT messages.id,messages.author,
|
|
337
342
|
COALESCE(details.kind,'opinion') AS kind FROM messages
|
|
338
343
|
LEFT JOIN message_details AS details ON details.message_id=messages.id
|
|
339
344
|
WHERE room_id=? ORDER BY messages.id""", (room_id,))
|
|
340
345
|
groups, next_start, seen = [], None, 0
|
|
341
346
|
for message in rows:
|
|
347
|
+
opinion = message["kind"] == "opinion" and message["author"] in experts
|
|
348
|
+
# base counts every opinion in the room, so opinions outside the discussion still advance seen.
|
|
349
|
+
seen += opinion
|
|
350
|
+
if not first < message["id"] <= last:
|
|
351
|
+
continue
|
|
342
352
|
if next_start is None:
|
|
343
353
|
next_start = message["id"]
|
|
344
|
-
if
|
|
354
|
+
if not opinion:
|
|
345
355
|
continue
|
|
346
356
|
if not groups or groups[-1]["completed"]:
|
|
347
357
|
groups.append({"number": len(groups) + 1, "start_id": next_start, "opinions": 0, "completed": False})
|
|
348
358
|
current = groups[-1]
|
|
349
|
-
seen += 1
|
|
350
359
|
current["opinions"] += 1
|
|
351
360
|
current["completed"] = current["opinions"] >= len(experts) or seen == base
|
|
352
361
|
if current["completed"]:
|
|
@@ -645,7 +654,7 @@ def manage_room(connection, room_id, action, payload):
|
|
|
645
654
|
|
|
646
655
|
def delete_room_records(connection, room_id):
|
|
647
656
|
"""Remove a room's dependent records in the caller's transaction."""
|
|
648
|
-
for table in ("member_recommendations", "free_queue", "speaking_orders", "member_requests", "member_executions", "member_sessions", "member_registrations", "room_members", "room_modes", "free_batches", "free_chains", "room_links",
|
|
657
|
+
for table in ("member_recommendations", "free_queue", "speaking_orders", "member_requests", "execution_notes", "member_executions", "member_sessions", "member_registrations", "room_members", "room_modes", "free_batches", "free_chains", "room_links",
|
|
649
658
|
"discussion_options", "round_budgets", "scheduler_decisions", "scheduler_routing", "discussion_sessions"):
|
|
650
659
|
if table == "scheduler_decisions":
|
|
651
660
|
for child in ("scheduler_feedback", "scheduler_decision_context", "scheduler_decision_inputs"):
|
|
@@ -766,7 +775,8 @@ def start_discussion(connection, room_id, token, payload):
|
|
|
766
775
|
raise ValueError("named_followup 须为布尔值")
|
|
767
776
|
# A fresh session first, so the phase mirror follows it and not a concluded earlier one.
|
|
768
777
|
if not connection.execute("SELECT 1 FROM discussion_sessions WHERE room_id=? AND status IN ('active','paused')", (room_id,)).fetchone():
|
|
769
|
-
connection.execute("INSERT INTO discussion_sessions (room_id,goal,status,created_at) VALUES (?,?,'paused'
|
|
778
|
+
connection.execute("INSERT INTO discussion_sessions (room_id,goal,status,started_message_id,created_at) VALUES (?,?,'paused',?,?)",
|
|
779
|
+
(room_id, settings["goal"], room["latest_id"], utc_now()))
|
|
770
780
|
connection.execute("UPDATE rooms SET goal=? WHERE id=?", (settings["goal"], room_id))
|
|
771
781
|
save_topic_settings(connection, room_id, settings)
|
|
772
782
|
connection.execute("UPDATE room_modes SET mode='brainstorm' WHERE room_id=?", (room_id,))
|
|
@@ -1556,7 +1566,7 @@ def participation_text(params, free=False):
|
|
|
1556
1566
|
"先回答真正的问题,给出理由、证据、适用条件与不确定性;区分事实、推断和建议,引用原文编号,不编造他人的回答。"
|
|
1557
1567
|
"讨论初始阶段先独立判断,暂不读他人观点;深入讨论先理解对方主张及其最有力的依据,再说明同意、分歧或需要补的证据。"
|
|
1558
1568
|
"可以坚持有依据的少数意见,也可因新证据修改判断并说明变化。",
|
|
1559
|
-
"
|
|
1569
|
+
"输出:完整正文;回答较长时先给 1–3 句摘要。",
|
|
1560
1570
|
"不要操作网页表单、调用写入接口或生成发布链接。读取失败时如实报告,由用户提供复制的材料。",
|
|
1561
1571
|
*(["自由协作:只回应点名给你的消息(编号见阅读通知),按时间顺序处理,后面的消息可以修正前面的。"] if free else []), footer]
|
|
1562
1572
|
|
|
@@ -1604,7 +1614,7 @@ def protocol_text(room):
|
|
|
1604
1614
|
f"维护话题:POST {base}/topic;actor 加需要修改的上述字段,另可更新 phase(preparing/initial/discussing/paused/concluded)、focus。收束前先完成或取消当前安排;主持人收束还须先 GET {base}/conclusion 取得 blockers 与 revision,并把 revision 作为 conclusion 一并提交(有阻碍项或状态已变会被拒绝)。暂停保留安排;恢复阶段后再继续。",
|
|
1605
1615
|
f"安排发言:POST {base}/turns;字段 actor、expert、question、submission_id(新 UUID)。同一话题同时只有一个未完成安排;重试沿用同一编号和问题。整轮安排改传 turns=[{{expert,question}},…](最多 {MAX_PLAN_TURNS} 项),并须先 GET {base}/turns/roster 取得最新专家状态,把返回的 revision 作为 roster 一并提交(名单变化后需重取;须包含 waiting 中的每位专家):第一项立即生效,其余排队(queued);每个回答保存后自动交给下一位,最后一位答完再交回主持人。用户留言或修改目标、背景、边界、焦点后(room.held_since 非 0),当前回答完成即交回主持人;主持人处理后用 POST {base}/turns/continue(actor)按原计划继续,或直接创建新安排替换剩余排队(原排队改为 skipped)。",
|
|
1606
1616
|
f"通知:仅把对象、问题、话题入口交给对应专家,不附背景、摘要或历史。使用已有且获准的会话通道;没有通道时交由用户协助。实际送达后 POST {base}/turns/安排编号,字段 actor、status=notified;取消用 status=cancelled。复制不算送达。",
|
|
1607
|
-
f"专家观点:POST {base}/messages;author、text、submission_id、source=cli,kind=opinion;turn_id(回答的安排编号)可省略,省略时回答你当前有效的安排;可选 reply_to
|
|
1617
|
+
f"专家观点:POST {base}/messages;author、text、submission_id、source=cli,kind=opinion;turn_id(回答的安排编号)可省略,省略时回答你当前有效的安排;可选 reply_to(本话题原文编号)。回答较长时可附 summary(最多 {MAX_SUMMARY_LENGTH} 字),1–3 句结论先行,保留关键理由、条件和不确定性,简短回答不写;summary_by 为原作者或提交者,默认提交者。text 保留完整正文,最多 {MAX_TEXT_LENGTH} 个 Unicode 字符,请求最多 {MAX_BODY_BYTES} 字节;不截断或用摘要完成安排。旧客户端缺少 summary 时不自动生成。",
|
|
1608
1618
|
f"主持发言:同一消息接口,author={room['host']}、submitted_by={room['host']}、kind=moderation、source=cli;个人观点使用 kind=opinion,不能自动视为共识。",
|
|
1609
1619
|
f"代发只读专家观点:author 保留原专家,submitted_by={room['host']}、kind=opinion、text 为原文;须关联 turn_id,可附 reply_to。服务保存代发来源;不要冒充专家直接提交。该专家须有当前有效的安排。",
|
|
1610
1620
|
f"整理状态:POST {base}/state;actor、text(详细状态,最多 {MAX_STATE_LENGTH} 字)、until(依据的消息截止编号);brief(最多 {MAX_BRIEF_LENGTH} 字)为面向人的主持简报,说明结论、共识、歧义和下一步意图。进度或安排变化后更新;省略 brief 保留旧简报,空字符串清除。所有参与专家各保存一次完整观点才完成一轮;同轮重复发言只计该专家一次,主持说明、用户留言及取消安排不推进轮次。房间 rounds 按当前配置专家和完整历史生成,completed_rounds 为完成轮数,answered_turns 仅为已回答安排数。状态不覆盖原文,也不代表全体一致同意。",
|
|
@@ -2170,6 +2180,8 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
2170
2180
|
result = recommend_member(connection, room_id, token, self.read_payload())
|
|
2171
2181
|
elif len(parts) == 5 and parts[3] == "recommendations" and method == "POST":
|
|
2172
2182
|
result = adopt_recommendation(connection, room_id, token, int(parts[4]), self.read_payload())
|
|
2183
|
+
elif len(parts) == 6 and parts[3] == "executions" and parts[5] == "notes" and method == "GET":
|
|
2184
|
+
result = execution_notes(connection, room_id, int(parts[4]))
|
|
2173
2185
|
elif len(parts) == 5 and parts[3] == "executions":
|
|
2174
2186
|
execution_id = int(parts[4])
|
|
2175
2187
|
result = (view_execution(connection, room_id, execution_id, token) if method == "GET"
|