ai_co_discuss 0.2.9 → 0.2.11
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-participant/SKILL.md +2 -0
- package/README.md +2 -0
- package/backend/free_room.py +99 -8
- package/backend/schema.sql +6 -0
- package/backend/server.py +8 -1
- package/docs/architecture.md +7 -4
- package/docs/cli.md +1 -1
- package/docs/runner.md +6 -2
- package/docs/skills.md +4 -0
- package/docs/workflow.md +6 -2
- package/package.json +4 -2
- package/runner/discuss.py +1 -1
- package/runner/discussion_local.mjs +12 -1
- package/runner/discussion_setup.mjs +17 -2
- package/runner/free_runner.mjs +2 -2
- package/web/app.js +30 -7
- package/web/index.html +126 -126
- package/web/modern.css +1 -1
- package/web/modern.js +1241 -1020
|
@@ -34,6 +34,8 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
|
|
|
34
34
|
|
|
35
35
|
**自由协作任务(任务文件含 `mode: "free"`):** 没有发言权、安排和阶段,不执行 `execution enter`。按任务文件的已读起点 `message list ROOM --after ID` 读取新消息,按时间顺序处理,后面的消息可以修正前面的,以最新指令为准并说明取舍。完成后用 `message post ROOM --as 本人 --kind chat --execution 执行编号 --access-file FILE` 提交一次回答。提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 `--execution` 改作普通发言。手动接入的自由成员没有执行编号:从 `room view` 的 `free.requests` 找到自己的待处理请求,回答时用 `--request 请求编号` 关闭。任务文件 `role` 为 `host` 时你是本房间主持人:`route` 请求是用户未点名的消息,`handoff` 表示你派出的成员都已结束;先 `room view` 看成员,需要他人时在回答中加 `--to 成员名` 派活并说明各自负责的文件或模块,不需要时不加,本次即结束;不点名自己,不扩大用户给的权限。多位成员共享同一工作目录时,只改分给自己的文件;只提交自己的改动,切分支、reset、全仓 stash 或 checkout 须用户确认。
|
|
36
36
|
|
|
37
|
+
自由房间的普通成员可在回答中用 `--to 成员名` 或正文 `@成员` 提出回应建议,不能点名自己;这不会派发任务。网页显示“建议请 X 回应”,只有房主点击“转为请求”后才建立关联原回答的新用户任务链。任务的 `request_messages` 可能指向已读游标之前的原回答,须用 `message view ROOM ID` 补读;用户确认消息不代替原建议。不要把建议显示或复制入口当作已经通知;代码与引用里的正文 @ 不生成建议。
|
|
38
|
+
|
|
37
39
|
**手动接入:** 用户已选手动时沿用人工触发或原生定时;需要持续参与才读 [手动持续参与](references/manual.md)。没有任务文件本身不代表手动授权:若房间正在自动运行,等待本次交付或核对缺项,不从旧文件接管。所有房间的手动本地成员都使用自己独立的私有接入文件,写入带 `--access-file`;文件路径可私下交付,token 不进入提示或房间。房主控制文件不能代替成员文件署名,在线成员没有自行写入资格。
|
|
38
40
|
|
|
39
41
|
## 先读什么
|
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ npm link
|
|
|
53
53
|
codiscuss setup
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
也可以在仓库中打开本地 AI,调用 [codiscuss-setup](.agents/skills/codiscuss-setup/SKILL.md)(Codex 为 `$codiscuss-setup`,其他 AI 明确读取该文件),由 AI 完成安装、配置、升级、诊断,以及可选的 Cloudflare 在线部署。
|
|
57
|
+
|
|
56
58
|
`setup` 使用同一安装配置流程并打开本机网站;命令链接到当前仓库,移动仓库后需重新安装。只需手动接入时,可用 `python server.py` 并保持终端打开,访问 <http://127.0.0.1:8765>,网站命令使用 `python runner/discuss.py …`。直接运行 Python 的数据位于 `data/discuss.sqlite3`,与 `codiscuss local` 的包外数据独立;两种启动方式不要交替操作同一讨论。
|
|
57
59
|
|
|
58
60
|
### 发起第一场讨论
|
package/backend/free_room.py
CHANGED
|
@@ -11,13 +11,16 @@ launched here.
|
|
|
11
11
|
|
|
12
12
|
import hmac
|
|
13
13
|
import json
|
|
14
|
+
import re
|
|
14
15
|
import time
|
|
16
|
+
import uuid
|
|
15
17
|
|
|
16
18
|
from .identity import MAX_ROOM_AGENTS, is_ai
|
|
17
19
|
from .runtime import LEASE_SECONDS, begin_write, require_owner, token_hash
|
|
18
20
|
|
|
19
21
|
FREE_EXECUTION_MINUTES = 20
|
|
20
22
|
RECENT_REQUESTS = 100
|
|
23
|
+
RECENT_EXECUTIONS = 100
|
|
21
24
|
RECENT_CHANGES = 50
|
|
22
25
|
MAX_CHANGED_FILES = 200
|
|
23
26
|
MAX_PATH_LENGTH = 500
|
|
@@ -157,6 +160,18 @@ def public_execution(row):
|
|
|
157
160
|
return {key: row[key] for key in EXECUTION_FIELDS} if row else None
|
|
158
161
|
|
|
159
162
|
|
|
163
|
+
def recent_executions(connection, room_id):
|
|
164
|
+
"""Return bounded public reports, including requests no longer in the recent request list."""
|
|
165
|
+
rows = connection.execute("""SELECT e.*,
|
|
166
|
+
(SELECT json_group_array(id) FROM member_requests WHERE execution_id=e.id) AS request_ids,
|
|
167
|
+
(SELECT MAX(answer_id) FROM member_requests WHERE execution_id=e.id) AS answer_id,
|
|
168
|
+
(SELECT MIN(message_id) FROM member_requests WHERE execution_id=e.id) AS request_message_id
|
|
169
|
+
FROM member_executions e WHERE room_id=? ORDER BY id DESC LIMIT ?""", (room_id, RECENT_EXECUTIONS))
|
|
170
|
+
return [{**public_execution(row), "cwd": row["cwd"], "changes": json.loads(row["changes"] or "[]"),
|
|
171
|
+
"changes_recorded": bool(row["changes"]), "request_ids": json.loads(row["request_ids"]),
|
|
172
|
+
"answer_id": row["answer_id"], "request_message_id": row["request_message_id"]} for row in rows]
|
|
173
|
+
|
|
174
|
+
|
|
160
175
|
def possible_conflicts(connection, room_id, names):
|
|
161
176
|
"""Report files changed by executions of different members whose time windows overlapped.
|
|
162
177
|
|
|
@@ -204,7 +219,7 @@ def free_view(connection, room_id):
|
|
|
204
219
|
WHERE room_id=? ORDER BY id DESC LIMIT ?""", (room_id, RECENT_REQUESTS))]
|
|
205
220
|
chains = [dict(row) for row in connection.execute(
|
|
206
221
|
"SELECT chain_id,reason FROM free_chains WHERE room_id=? ORDER BY chain_id DESC LIMIT 10", (room_id,))]
|
|
207
|
-
return {"members": members, "requests": requests[::-1], "execution_minutes": execution_minutes(connection, room_id),
|
|
222
|
+
return {"members": members, "requests": requests[::-1], "executions": recent_executions(connection, room_id), "execution_minutes": execution_minutes(connection, room_id),
|
|
208
223
|
"conflicts": possible_conflicts(connection, room_id, names), "halted_chains": chains, "max_auto_hops": MAX_AUTO_HOPS}
|
|
209
224
|
|
|
210
225
|
|
|
@@ -278,11 +293,9 @@ def proxy_member(connection, room_id, author):
|
|
|
278
293
|
def check_answer(connection, room_id, member, payload):
|
|
279
294
|
"""A runner member completes a live execution; a manual member may close its own open requests.
|
|
280
295
|
|
|
281
|
-
Only the AI host
|
|
296
|
+
Only the AI host dispatches work; other members' targets remain suggestions.
|
|
282
297
|
"""
|
|
283
298
|
if payload.get("to") is not None:
|
|
284
|
-
if member["role"] != "host":
|
|
285
|
-
raise PermissionError("只有主持人可以在回答中点名成员派活")
|
|
286
299
|
named_members(active_members(connection, room_id), payload["to"], exclude=member["name"])
|
|
287
300
|
if member["access"] == "online":
|
|
288
301
|
raise PermissionError("在线成员只读,回答由用户代发")
|
|
@@ -323,6 +336,82 @@ def insert_request(connection, room_id, member_id, message_id, chain_id, purpose
|
|
|
323
336
|
VALUES (?,?,?,?,?,?,?,?)""", (room_id, member_id, message_id, chain_id, batch_id, purpose, created_by, int(time.time())))
|
|
324
337
|
|
|
325
338
|
|
|
339
|
+
def mention_prose(text):
|
|
340
|
+
"""Remove Markdown code and quote blocks before interpreting response suggestions."""
|
|
341
|
+
lines, fence, quoted = [], "", False
|
|
342
|
+
for line in text.splitlines():
|
|
343
|
+
marker = re.match(r"^ {0,3}(`{3,}|~{3,})", line)
|
|
344
|
+
if marker:
|
|
345
|
+
value = marker[1]
|
|
346
|
+
if not fence:
|
|
347
|
+
fence = value
|
|
348
|
+
elif value[0] == fence[0] and len(value) >= len(fence):
|
|
349
|
+
fence = ""
|
|
350
|
+
continue
|
|
351
|
+
if fence or re.match(r"^( {4}|\t)", line):
|
|
352
|
+
continue
|
|
353
|
+
if re.match(r"^ {0,3}>", line):
|
|
354
|
+
quoted = True
|
|
355
|
+
continue
|
|
356
|
+
if not line.strip():
|
|
357
|
+
quoted = False
|
|
358
|
+
if not quoted:
|
|
359
|
+
lines.append(line)
|
|
360
|
+
return re.sub(r"(`+)[\s\S]*?\1(?!`)", "", "\n".join(lines))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def save_suggestions(connection, room_id, message_id, member, payload):
|
|
364
|
+
"""Save ordinary members' explicit targets and prose mentions without waking anyone."""
|
|
365
|
+
if member == "用户" or member["role"] == "host":
|
|
366
|
+
return
|
|
367
|
+
active = active_members(connection, room_id)
|
|
368
|
+
targets = set(named_members(active, payload["to"] if payload.get("to") is not None else [], exclude=member["name"]))
|
|
369
|
+
prose = mention_prose(str(payload.get("text", "")))
|
|
370
|
+
for name, target in active.items():
|
|
371
|
+
if name != member["name"] and re.search(r"(^|[\s((,,。;;::])@" + re.escape(name) + r"(?![A-Za-z0-9_\-\u4e00-\u9fff])", prose):
|
|
372
|
+
targets.add(target["id"])
|
|
373
|
+
for target_id in targets:
|
|
374
|
+
connection.execute("INSERT INTO member_suggestions (message_id,member_id) VALUES (?,?)", (message_id, target_id))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def message_suggestions(connection, message_id):
|
|
378
|
+
"""Expose suggested members and the durable request, including terminal conversions."""
|
|
379
|
+
return [dict(row) for row in connection.execute("""SELECT s.member_id,m.name AS member,m.status,
|
|
380
|
+
r.id AS request_id,r.status AS request_status FROM member_suggestions s
|
|
381
|
+
JOIN room_members m ON m.id=s.member_id
|
|
382
|
+
LEFT JOIN member_requests r ON r.message_id=s.message_id AND r.member_id=s.member_id
|
|
383
|
+
WHERE s.message_id=? ORDER BY s.member_id""", (message_id,))]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def convert_suggestion(connection, room_id, token, payload):
|
|
387
|
+
"""Let the owner turn a saved suggestion into one request and a fresh user chain."""
|
|
388
|
+
begin_write(connection)
|
|
389
|
+
require_owner(connection, room_id, token)
|
|
390
|
+
message_id, member_id = payload.get("message_id"), payload.get("member_id")
|
|
391
|
+
if type(message_id) is not int or type(member_id) is not int:
|
|
392
|
+
raise ValueError("须提供原消息编号 message_id 与成员编号 member_id")
|
|
393
|
+
target = connection.execute("""SELECT m.name,m.status FROM member_suggestions s
|
|
394
|
+
JOIN messages source ON source.id=s.message_id JOIN room_members m ON m.id=s.member_id
|
|
395
|
+
WHERE source.room_id=? AND m.room_id=? AND s.message_id=? AND s.member_id=?""",
|
|
396
|
+
(room_id, room_id, message_id, member_id)).fetchone()
|
|
397
|
+
if not target:
|
|
398
|
+
raise LookupError("这条消息没有该成员的回应建议")
|
|
399
|
+
if target["status"] != "active":
|
|
400
|
+
raise FileExistsError("该成员已离开,不能转为请求")
|
|
401
|
+
if connection.execute("SELECT 1 FROM room_directory WHERE room_id=? AND archived=1", (room_id,)).fetchone():
|
|
402
|
+
raise ValueError("频道已存档;恢复后才能继续协作")
|
|
403
|
+
previous = connection.execute("SELECT * FROM member_requests WHERE message_id=? AND member_id=?", (message_id, member_id)).fetchone()
|
|
404
|
+
if previous:
|
|
405
|
+
return {"request": dict(previous), "duplicate": True}
|
|
406
|
+
confirmation = connection.execute("""INSERT INTO messages (room_id,author,text,reply_to,source,submission_id,created_at)
|
|
407
|
+
VALUES (?,'用户',?,?,'user',?,strftime('%Y-%m-%dT%H:%M:%fZ','now'))""",
|
|
408
|
+
(room_id, f"请 @{target['name']} 回应 #{message_id} 的建议。", message_id, uuid.uuid4().hex))
|
|
409
|
+
connection.execute("INSERT INTO message_details (message_id,kind,submitted_by) VALUES (?,'chat','用户')", (confirmation.lastrowid,))
|
|
410
|
+
insert_request(connection, room_id, member_id, message_id, confirmation.lastrowid, "ask", "user")
|
|
411
|
+
request = connection.execute("SELECT * FROM member_requests WHERE message_id=? AND member_id=?", (message_id, member_id)).fetchone()
|
|
412
|
+
return {"request": dict(request), "duplicate": False}
|
|
413
|
+
|
|
414
|
+
|
|
326
415
|
def halt_chain(connection, room_id, chain_id, reason):
|
|
327
416
|
"""Stop automatic progress on a chain; the user's next message starts a new one."""
|
|
328
417
|
connection.execute("INSERT OR IGNORE INTO free_chains (room_id,chain_id,reason) VALUES (?,?,?)", (room_id, chain_id, reason))
|
|
@@ -412,10 +501,12 @@ def after_free_message(connection, room_id, message_id, actor, payload):
|
|
|
412
501
|
def expire_executions(connection, room_id):
|
|
413
502
|
"""Fail executions whose lease lapsed; their requests stay visible as failed, never silently retried."""
|
|
414
503
|
now = int(time.time())
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
504
|
+
limit_seconds = execution_minutes(connection, room_id) * 60
|
|
505
|
+
stale = connection.execute(
|
|
506
|
+
"SELECT id,created_at,lease_until FROM member_executions WHERE room_id=? AND status IN ('dispatched','started') AND lease_until<=?", (room_id, now)).fetchall()
|
|
507
|
+
for row in stale:
|
|
508
|
+
reason = "已达到单次执行时限" if row["lease_until"] >= row["created_at"] + limit_seconds else "执行租约到期,接入程序未续租"
|
|
509
|
+
finish_execution(connection, row["id"], "failed", reason, "failed")
|
|
419
510
|
if stale:
|
|
420
511
|
settle_batches(connection, room_id)
|
|
421
512
|
|
package/backend/schema.sql
CHANGED
|
@@ -550,6 +550,12 @@ CREATE TABLE IF NOT EXISTS member_requests (
|
|
|
550
550
|
UNIQUE(message_id,member_id)
|
|
551
551
|
);
|
|
552
552
|
CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
|
|
553
|
+
-- Ordinary members can suggest a response without creating work or spending a chain budget.
|
|
554
|
+
CREATE TABLE IF NOT EXISTS member_suggestions (
|
|
555
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
556
|
+
member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
|
|
557
|
+
PRIMARY KEY (message_id,member_id)
|
|
558
|
+
);
|
|
553
559
|
-- A host's dispatch: its named requests form one batch that hands back to the host once.
|
|
554
560
|
CREATE TABLE IF NOT EXISTS free_batches (
|
|
555
561
|
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
package/backend/server.py
CHANGED
|
@@ -22,6 +22,7 @@ from urllib.parse import parse_qs, quote, urlsplit
|
|
|
22
22
|
from .free_room import (add_member, after_free_message, authorize_free_write, check_answer, check_proxy, execution_action, free_view,
|
|
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
|
+
from .free_room import convert_suggestion, message_suggestions, save_suggestions
|
|
25
26
|
from .identity import MAX_ROOM_AGENTS, PARTICIPANTS, is_ai, is_participant
|
|
26
27
|
from .runtime import (authorize_write, begin_write, configure_runtime, event_action,
|
|
27
28
|
LIMIT_DEFAULTS, LIMITS_SQL, limit_values, limits_view, require_within_limits,
|
|
@@ -85,6 +86,7 @@ FREE_BACKUP_QUERIES = {
|
|
|
85
86
|
"room_modes": "SELECT room_id,mode FROM room_modes WHERE room_id=?",
|
|
86
87
|
"room_members": "SELECT id,room_id,name,adapter,role,status,read_until,created_at FROM room_members WHERE room_id=? ORDER BY id",
|
|
87
88
|
"member_requests": "SELECT * FROM member_requests WHERE room_id=? ORDER BY id",
|
|
89
|
+
"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",
|
|
88
90
|
}
|
|
89
91
|
MESSAGE_QUERY = """SELECT messages.*, COALESCE(details.kind,'opinion') AS kind,
|
|
90
92
|
COALESCE(details.submitted_by, CASE WHEN messages.source IN ('cli','url','user')
|
|
@@ -667,6 +669,7 @@ def attachment_view(row):
|
|
|
667
669
|
def message_with_attachments(connection, row):
|
|
668
670
|
"""Attach the published file list to an original message."""
|
|
669
671
|
message = dict(row)
|
|
672
|
+
message["suggestions"] = message_suggestions(connection, message["id"])
|
|
670
673
|
message["attachments"] = [attachment_view(item) for item in connection.execute(
|
|
671
674
|
"SELECT * FROM attachments WHERE message_id=? ORDER BY position,id", (message["id"],))]
|
|
672
675
|
return message
|
|
@@ -838,6 +841,7 @@ def append_free_message(connection, room, payload, actor):
|
|
|
838
841
|
(cursor.lastrowid, expected["summary"], expected["summary_by"]))
|
|
839
842
|
for position, identifier in enumerate(identifiers):
|
|
840
843
|
connection.execute("UPDATE attachments SET message_id=?,position=? WHERE id=?", (cursor.lastrowid, position, identifier))
|
|
844
|
+
save_suggestions(connection, room_id, cursor.lastrowid, proxy or actor, payload)
|
|
841
845
|
after_free_message(connection, room_id, cursor.lastrowid, proxy or actor, payload)
|
|
842
846
|
return {"message": get_message(connection, room_id, cursor.lastrowid), "duplicate": False}
|
|
843
847
|
|
|
@@ -1189,6 +1193,7 @@ def protocol_text(room):
|
|
|
1189
1193
|
"manual 成员可由人工或软件原生定时触发,用本人成员文件自行提交。回答带 --request 请求编号(可重复)关闭自己的请求,不带则为普通发言。不得编造 execution 编号。",
|
|
1190
1194
|
"online 成员只读,在原聊天回答,由用户以房主凭据代发原文;作者仍是在线成员、提交者为用户,不能用成员凭证自行写入。复制说明不表示已送达或接入。",
|
|
1191
1195
|
"获授 host 职责的本地成员可在回答中用 --to 点名其他成员,不能点名自己或扩大权限;route 表示分配任务,handoff 表示派出的成员已结束。链上 AI 请求受房间预算限制,到限等待用户。",
|
|
1196
|
+
"普通成员的 --to 或正文 @ 只保存为回应建议,不唤醒;代码与引用中的正文 @ 不算。房主在网页转为请求后建立关联原消息的新用户任务链,重复点击不重复派发。",
|
|
1192
1197
|
f"附件使用 attachment upload/download;上传使用本人成员文件,消息带 attachment_ids。原始接口为 {base}/attachments 与 {base}/messages,自动成员在回答完成前上传。",
|
|
1193
1198
|
"每次新内容用新 submission_id,同次重试沿用编号、原文、凭据和执行/请求关联;失败先核对服务端结果,不换身份或编号绕过。用户打断、成员离开或移除后停止本次工作。",
|
|
1194
1199
|
])
|
|
@@ -1528,7 +1533,7 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1528
1533
|
room_id = parts[2]
|
|
1529
1534
|
token = self.headers.get("X-Discussion-Token", "")
|
|
1530
1535
|
free = room_mode(connection, room_id) == "free"
|
|
1531
|
-
if free and parts[3:4] in (["inbox"], ["executions"], ["members"]):
|
|
1536
|
+
if free and parts[3:4] in (["inbox"], ["executions"], ["members"], ["suggestions"]):
|
|
1532
1537
|
return self.handle_free_api(connection, parts, method)
|
|
1533
1538
|
if parts[3:4] == ["attachments"]:
|
|
1534
1539
|
return self.handle_attachments(connection, parts, params, method)
|
|
@@ -1659,6 +1664,8 @@ class DiscussionHandler(BaseHTTPRequestHandler):
|
|
|
1659
1664
|
token = self.headers.get("X-Discussion-Token", "")
|
|
1660
1665
|
if parts[3:] == ["inbox"] and method == "GET":
|
|
1661
1666
|
result = inbox(connection, room_id, token)
|
|
1667
|
+
elif parts[3:] == ["suggestions"] and method == "POST":
|
|
1668
|
+
result = convert_suggestion(connection, room_id, token, self.read_payload())
|
|
1662
1669
|
elif parts[3:] == ["executions"] and method == "POST":
|
|
1663
1670
|
result = reserve_execution(connection, room_id, token, self.read_payload())
|
|
1664
1671
|
elif len(parts) == 5 and parts[3] == "executions":
|
package/docs/architecture.md
CHANGED
|
@@ -134,7 +134,7 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
|
|
|
134
134
|
| --- | --- |
|
|
135
135
|
| `main.js`、`App.vue`、`ChannelWorkspace.vue` | 协作频道入口、主题与整体布局 |
|
|
136
136
|
| `ChannelDirectory.vue` | 频道、文件夹和存档管理 |
|
|
137
|
-
| `ChannelDiscussion.vue` |
|
|
137
|
+
| `ChannelDiscussion.vue` | 议题卡、消息流、原文导航和输入区;正文 `@` 在输入框上方弹出成员补全,按光标位置筛选与替换,支持鼠标、方向键和 Enter / Tab,中文输入期间不拦截候选键;自由房间只列在场成员,讨论房间列主持人与专家,选择不自动发送 |
|
|
138
138
|
| `TopicPanel.vue`、`WorkspaceDialogs.vue` | 话题资料、主持操作、接入步骤及编辑对话框 |
|
|
139
139
|
| `CreateDiscussion.vue`、`LimitPicker.vue`、`LaunchSettings.vue` | 新建讨论表单及本机启动配置;推进方式与参与专家分组,单选方式使用强调色卡片,参与者通过“邀请 AI”加入并独立移出;轮次和时长独立配置,以“不限”按钮切换各自上限,按需展开滚轮,选择即时生效,并预览到限条件;鼠标滚轮逐格切换且不产生惯性,鼠标及触摸拖动松手后保留惯性,先阻尼滑行再吸附预计停点,轻点中间数字精确输入;键盘聚焦和直接输入时仅高亮中间选中行,输入与显示共用行高,避免整列描边与原生输入框叠加;时长分小时/分钟两列,收起与摘要共用时长格式;支持键盘,减少动态效果时保留拖动惯性并去掉三维翻转 |
|
|
140
140
|
| `ProgressBoard.vue`、`ProgressPanel.vue` | 共享进度看板及抽屉容器;Codex 参与者菜单内直接打开 App / CLI 切换弹窗,显式暂停后复用本场配置的读取与保存,不跳转接入页或自动重启 |
|
|
@@ -160,7 +160,7 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
|
|
|
160
160
|
|
|
161
161
|
左右侧栏仅用朝向正文的柔和阴影分界,避免竖向描边与内侧高光线重复叠加。分栏拖柄覆盖接缝,层级高于主持简报、低于浮动预览,不占用布局宽度,仅在悬停、聚焦或拖动时显色。新建入口与导航选中项具有独立边界,右栏状态为轻浮起面板,焦点、参与者及耗时以留白和分隔线分组。消息、共识与主持面板使用较轻阴影,按钮使用短投影,菜单和抽屉使用浮层投影,搜索和表单使用内阴影;按下和禁用时降低或移除投影。输入区聚焦保留面板阴影并叠加焦点边界。材质调整保留三栏结构和阅读宽度。
|
|
162
162
|
|
|
163
|
-
浮层(菜单、预览、消息工具条、抽屉与弹窗)在深色频道使用毛玻璃:半透明上亮下暗渐变底配合 24px 背景模糊与增艳,边缘只用顶部内侧亮线、底部内侧暗线和向下投影,不加外侧白色辉光。浅色频道的控件本体保持不透明白色,只学习材质边界:蓝灰细描边、内侧顶部高光、内侧底部浅蓝灰暗边和蓝灰多层投影,按控件、面板、浮层三级加强。外观设置的「视觉效果」可切换为精简,写入根节点 `data-effects="reduced"`
|
|
163
|
+
浮层(菜单、预览、消息工具条、抽屉与弹窗)在深色频道使用毛玻璃:半透明上亮下暗渐变底配合 24px 背景模糊与增艳,边缘只用顶部内侧亮线、底部内侧暗线和向下投影,不加外侧白色辉光。浅色频道的控件本体保持不透明白色,只学习材质边界:蓝灰细描边、内侧顶部高光、内侧底部浅蓝灰暗边和蓝灰多层投影,按控件、面板、浮层三级加强。外观设置的「视觉效果」可切换为精简,写入根节点 `data-effects="reduced"` 并随布局偏好保存在浏览器;精简模式全局关闭背景模糊,深色浮层改为实色,深浅阴影各退为单层,用于保证低配设备的流畅度。浅色频道的发言快速导航不加底板,刻度改为发言者实色、固定 2px 粗细(覆盖脚本动画的内联高度,长度与透明度仍由脚本驱动),轮次号与轮次分隔比辅助文字深一级。
|
|
164
164
|
|
|
165
165
|
选中状态用填色、文字和勾选表达,键盘焦点使用中性边缘,不增加蓝色外框;卡片、模型选择、强度滑杆、模式、时长及编辑区遵循同一规则。
|
|
166
166
|
|
|
@@ -218,17 +218,20 @@ running 状态即使只有 pending 事件,也必须先暂停才能重新启动
|
|
|
218
218
|
|
|
219
219
|
## 自由协作协议
|
|
220
220
|
|
|
221
|
+
执行报告:`free.executions` 返回最近 100 次执行的白名单字段,不含凭据或 `client_id`;包含 `cwd`、数组 `changes`、`changes_recorded`、完整 `request_ids`、`answer_id` 与首条 `request_message_id`。报告不依赖最近请求列表是否截断:完成的报告挂在回答下,失败/取消的报告挂在原请求下。`changes_recorded=false` 表示非 Git 或未取得报告,不能当成无变化;已上报的 `[]` 才表示未观察到文件变化。单次时限与续租中断使用不同失败原因。
|
|
222
|
+
|
|
221
223
|
自由房间(`room_modes.mode='free'`)与讨论房间共用房间、成员登记、消息、附件与目录,没有发言权、安排、阶段推进和轮次。没有控制凭据登记的旧房间只供阅读,不支持通过首次运行配置认领;本次不迁移历史房间。
|
|
222
224
|
|
|
223
225
|
- **凭据**:建房时由客户端提供房间控制凭据和每位成员的成员凭证,服务端只存哈希。用户身份的房间写入(发言、附件、存档、恢复、删除、打断)都须控制凭据;频道排序和归入文件夹只改共享目录,与站点级文件夹操作一样不需要凭据;无凭据、错误凭据、成员凭证或他房凭据一律只读。成员凭证只能以该成员身份发言或上传附件。声明字段(`author`、`source`、localhost)不代替凭据。
|
|
224
226
|
- **成员**:`room_members` 以 `id` 为稳定主键,`access` 为 `runner`(接入程序投递)、`manual`(成员自行读写)或 `online`(只读、原文代发),`session_mode` 标记是否借用用户正在使用的会话。房主用控制凭据 `POST members` 运行中加入成员,`POST members/ID {action}` 让成员离开(`away`,作废其进行中执行与未投递请求)、请回(`active`,必须提供不同于原凭证的新凭证,旧凭证副本不再有效)或移除(`remove`,保留历史发言)。同一成员凭证不能复用于其他成员或房主;相同凭证与配置的邀请、请回重试返回已有结果。会话续接由本机接入程序负责,服务端不保存会话绑定。
|
|
225
227
|
- **请求**:用户消息按结构化接收者 `to`(成员名)为每位成员生成一条请求;省略且仅有一位在场成员时默认点名该成员,多位成员时不点名只作留言,不广播。网页从点名按钮和正文中代码与引用之外的 `@成员` 生成 `to`。请求状态为 `pending`(待投递)→ `delivering`(处理中)→ `completed` / `cancelled` / `failed`。
|
|
226
228
|
- **执行**:`member_executions` 每位成员最多一个 `dispatched/started`(部分唯一索引),不同成员可并行。接入程序用控制凭据 `GET inbox`、`POST executions {member_id, client_id}`(同一 `client_id` 返回同一执行)把成员的全部待投递请求打包;`POST executions/ID {action}` 支持 `renew`(租约 120 秒,不超过单次执行时限,默认 20 分钟)、`fail`(请求标记失败)和 `release`(仅确认未送达时退回队列)。租约过期的执行在下次读取或领取时标记失败,不自动重发。
|
|
227
|
-
- **主持人与任务链**:房主用 `POST members/ID {action:"role", role:"host"}` 指定至多一位主持人(在线成员不能担任)。多位成员时未点名的房主消息生成给主持人的 `route` 请求。主持人的回答可带 `to`
|
|
229
|
+
- **主持人与任务链**:房主用 `POST members/ID {action:"role", role:"host"}` 指定至多一位主持人(在线成员不能担任)。多位成员时未点名的房主消息生成给主持人的 `route` 请求。主持人的回答可带 `to` 点名其他成员(不能点名自己),这些请求组成一个批次(`free_batches`);批次内全部进入终态后,只要有一个完成就给主持人生成一次 `handoff` 请求,全部失败则停下该任务链。每条房主消息开启一条任务链(`chain_id` 为该消息编号),链上 AI 发起的请求(派活与回交)最多 8 次,超出即停下(`free_chains`,原因 `budget` / `all_failed`),房主发新消息开启新链。Worker 用同一批次内的集合 SQL 完成回交,回交请求按(派活消息,主持人)唯一,重复执行不会重复唤醒。
|
|
230
|
+
- **回应建议**:普通成员新发言的结构化 `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 发布前显式执行。已有历史消息不补写建议。
|
|
228
231
|
- **在线成员**:`access=online` 的成员只读,不进入 `inbox`,不能用自己的凭证发言;房主以控制凭据代发其回答(`author` 为该成员、`submitted_by` 为用户、`source=proxy`),可带 `request_ids` 关闭它的待处理请求。
|
|
229
232
|
- **关联房间**:从自由房间发起的讨论房间在建房时带 `parent_room_id`,记录在 `room_links`;讨论房间视图有 `parent_room`,自由房间视图列出 `linked_rooms`。头脑风暴的轮次、时长与名单因此天然独立于自由房间。
|
|
230
233
|
- **手动成员**:手动成员不进入 `inbox`,也不能被领取执行;它用成员凭证读写,回答时可带 `request_ids` 关闭自己的待处理请求,不带则是普通发言、不关闭任何请求。
|
|
231
|
-
- **改动记录**:接入程序在执行前后对成员 cwd 做 Git 工作区快照(状态与内容哈希),执行结束且进程退出后用 `report {changes}` 记录一次。房间视图的 `free.conflicts` 列出不同成员在同一 cwd
|
|
234
|
+
- **改动记录**:接入程序在执行前后对成员 cwd 做 Git 工作区快照(状态与内容哈希),执行结束且进程退出后用 `report {changes}` 记录一次。房间视图的 `free.conflicts` 列出不同成员在同一 cwd、时段重叠的执行均观察到变化的文件,只作“可能冲突”提示;非 Git 目录不记录。快照也会包含其他进程在同一目录的写入,不能确定修改者;分工修改不同文件仍可能出现提示,不代表已发生覆盖。
|
|
232
235
|
- **回答**:接入程序投递的成员回答须带 `execution_id` 并使用成员凭证;消息保存、执行完成、请求完成和成员已读游标在同一事务中写入。执行结束、被打断或过期后的回答返回 409;缺少执行编号返回 400,不能改作普通发言。消息类型为 `chat`,不计入讨论轮次。
|
|
233
236
|
- **打断**:`POST members/ID/interrupt`(控制凭据)作废该成员的进行中执行及所有未投递请求,不会自动重新开始;成员的已读游标不前移,下一次请求从同一位置读起。执行失败只影响该成员的请求,房间不暂停。
|
|
234
237
|
- **讨论成员变更**:房主修改讨论名单或读写能力时,为新增或改变能力的成员提供新凭证,未变成员保留原凭证;删去的成员立即停用。成员主持不能自行扩大名单或更改主持职责。
|
package/docs/cli.md
CHANGED
|
@@ -153,7 +153,7 @@ codiscuss message post ROOM --as Claude --kind chat --execution 执行编号 --b
|
|
|
153
153
|
|
|
154
154
|
手动接入的成员没有执行编号:`room view` 的 `free.requests` 列出它的待处理请求,回答时用 `--request 请求编号`(可重复)关闭对应请求;不带 `--request` 的回答是普通发言,不关闭任何请求。`--request` 与 `--execution` 不能同用。
|
|
155
155
|
|
|
156
|
-
主持人在回答中用 `--to 成员名`(可重复)点名派活,例如 `--to Codex --to Gemini
|
|
156
|
+
主持人在回答中用 `--to 成员名`(可重复)点名派活,例如 `--to Codex --to Gemini`;不能点名自己。被点名的成员都结束后,主持人会再收到一次 `handoff` 请求。普通成员也可用 `--to` 或正文 `@成员` 提出回应建议,但不会派发;房主在网页点击“转为请求”后才建立关联原消息的新用户任务链。代码与引用里的正文 @ 不生成建议。
|
|
157
157
|
|
|
158
158
|
## 写入核对与错误处理
|
|
159
159
|
|
package/docs/runner.md
CHANGED
|
@@ -36,7 +36,7 @@ codiscuss local stop
|
|
|
36
36
|
|
|
37
37
|
安装实现保持固定软件表和本地文件复制。路径查找参考 [npm/node-which](https://github.com/npm/node-which/blob/main/lib/index.js) 的 PATH 顺序与执行权限检查,只覆盖所需命令和后缀;技能复制参考 [OpenAI 单文件安装脚本](https://github.com/openai/skills/blob/main/skills/.system/skill-installer/scripts/install-skill-from-github.py) 的整目录复制与同名保护思路,保留本项目的内容哈希更新判断。不引入这些项目作为依赖,也不增加在线技能下载、插件注册或后台更新。
|
|
38
38
|
|
|
39
|
-
默认 `setup` 最后执行 `local open`:后台启动 Python 回环网站(`--local-launch --no-demo`)与讨论服务,再打开浏览器;首次为空库。网站地址默认为 `http://127.0.0.1:8765`。`local start` 具有相同启动行为但不打开浏览器,重复启动复用原进程;真正绑定成功并通过健康检查后才返回就绪。端口占用会失败,不接管或停止占用者。网站管理进程脱离启动它的终端在后台运行,关闭终端不会停止网站或讨论服务;本机网站不设登录自启,重启电脑后运行 `local open` 重新启动。安装摘要会同时提示这些状态与停止命令。`local open/start/status/stop` 在终端显示中文结果与网站地址;输出被重定向或加 `--json` 时在 stdout 输出 JSON。改端口或升级程序前先 `local stop`,再重新启动。
|
|
39
|
+
默认 `setup` 最后执行 `local open`:后台启动 Python 回环网站(`--local-launch --no-demo`)与讨论服务,再打开浏览器;首次为空库。网站地址默认为 `http://127.0.0.1:8765`。`local start` 具有相同启动行为但不打开浏览器,重复启动复用原进程;真正绑定成功并通过健康检查后才返回就绪。端口占用会失败,不接管或停止占用者。网站管理进程脱离启动它的终端在后台运行,关闭终端不会停止网站或讨论服务;本机网站不设登录自启,重启电脑后运行 `local open` 重新启动。安装摘要会同时提示这些状态与停止命令。`setup` 启动前若发现网站已在运行但版本(记录在 `local/status.json`)或端口与当前安装不同,交互终端询问是否重启(默认重启,会暂停托管中的讨论);`--yes` 或非交互运行不停止服务,报告待处理及 `local stop` / `local open` 命令。版本与端口一致时直接复用。`local open/start/status/stop` 在终端显示中文结果与网站地址;输出被重定向或加 `--json` 时在 stdout 输出 JSON。改端口或升级程序前先 `local stop`,再重新启动。
|
|
40
40
|
|
|
41
41
|
网站数据库为服务目录下 `local/discuss.sqlite3`,配置、登记表和讨论执行数据也位于包外;服务目录位置及 `DISCUSS_SERVICE_HOME` 见下文。源码运行的 `data/discuss.sqlite3` 不自动迁移。网站仍在而讨论服务已停止时,显式 `local open/start` 会要求网站管理进程恢复服务;不会自动恢复已暂停的房间。`local status` 的 `running` 要求网站和讨论服务同时运行,`website_running` 单独表示网站状态。`local stop` 关闭它创建的网站,并停止它启动或恢复且仍由相同 PID 持有的讨论服务;预先运行或后来独立启动的服务会保留。停止服务会暂停其托管中的活动讨论,数据库保留。日志在 `local/website.log`、`local/supervisor.log` 与原 `service.log`。
|
|
42
42
|
|
|
@@ -273,12 +273,16 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
|
|
|
273
273
|
|
|
274
274
|
## 自由协作房间
|
|
275
275
|
|
|
276
|
+
执行结束后,Git 快照成功时即使没有变化也上报 `changes: []`,供报告卡显示“未观察到文件变化”;非 Git 目录或快照失败不伪造空报告。上报记录是工作区观察,可能包含其他并行进程写入;报告卡不归因修改者。服务重启导致未上报的历史记录仍显示“未提供自动改动检测”。
|
|
277
|
+
|
|
276
278
|
网页选择“自由协作”及自动接入后,启动器沿用浏览器生成的房间控制凭据和各成员凭证,保存到本机私有配置 `members` 并登记常驻服务,不启用讨论运行。私有配置带 `mode: "free"`,服务据此使用 `free_runner.mjs`。手动建房直接调用网站,不启动或要求本机服务;浏览器保存私有接入资料,按成员分别下载凭据文件。
|
|
277
279
|
|
|
278
|
-
`codiscuss launch room-member` 在运行中的自由房间邀请(`add`)或请回(`rejoin`)自动成员:先用房间控制凭据核对房主身份,在登记配置中写入成员的软件配置与新凭证,再通知网站;网页通过 `member_token`
|
|
280
|
+
`codiscuss launch room-member` 在运行中的自由房间邀请(`add`)或请回(`rejoin`)自动成员:先用房间控制凭据核对房主身份,在登记配置中写入成员的软件配置与新凭证,再通知网站;网页通过 `member_token` 提供其已保存的新凭证,结果不明时沿用同一凭证重试。网页邀请默认项目只读,显式选择可写后通过该成员的 `participants[name].project_access` 传递,不改变其他成员;重试保留最初选择。该命令也可为手动成员生成配置旁的 `manual-成员名.access.json`,回执只含文件路径;网页手动邀请和请回则直接在网站完成。常驻服务读取更新后的配置时逐个成员核对:新成员直接采用,已绑定会话的成员若项目、cwd 或接入方式变化,只阻止该成员并报告原因,不影响其他成员。
|
|
279
281
|
|
|
280
282
|
每次检查读取 `inbox`:成员没有进行中的执行且有待投递消息时,先持久化本地 `client_id` 再领取执行,然后用原适配器续接该成员的绑定会话;任务文件列出执行编号、待处理消息和已读起点,凭据文件只含成员凭证。进程退出而网站未收到回答、软件报错或超过单次执行时限时报告失败;只有确认未送达才退回队列重试,最多 3 次。领取后、调用前中断的投递不自动重发。主持人的任务文件带 `role: "host"` 和每条请求的 `request_purposes`(`ask` / `route` / `handoff`),说明里写明如何用 `--to` 派活与何时结束。网站上执行被打断、成员离开或被移除后,只停止该成员的进程;不同成员各自投递,可以并行。成员配置了 cwd 且位于 Git 工作区时,执行前后各做一次快照,执行结束且进程退出后报告改动的文件,供网站提示可能冲突;服务重启会丢失尚未报告的快照。只能在网页创建自由房间,不支持对话入口。
|
|
281
283
|
|
|
284
|
+
普通成员的回应建议不会出现在待投递队列。房主转换后沿用同一请求和执行路径:任务文件的 `request_messages` 指向原成员回答,`chain_id` 属于这次用户确认;重复转换不会再次投递,手动与在线目标仍不进入自动投递。
|
|
285
|
+
|
|
282
286
|
Antigravity App 的 Sidecar 按房间用法校验任务:讨论读取对应事件,自由房间读取对应 `executions/ID` 并核对有效租约。自由执行的受理回执由服务核对并绑定真实会话,即使回答已快速完成也保留绑定;取消、过期或结果不明的投递不重新发送。本次使用替身 Sidecar 与临时服务验证,未重新调用真实 App。
|
|
283
287
|
|
|
284
288
|
## 常驻讨论服务
|
package/docs/skills.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
本项目的可分发技能保存在 `.agents/skills/`,不需要维护者个人技能目录或专用服务器。安装技能不会启动讨论或发送消息。
|
|
4
4
|
|
|
5
|
+
该目录按 `.gitignore` 白名单入库:未列入白名单的本地技能不会提交。新增需分发的技能时同步白名单;需要进入 npm 包的还要加入 `package.json` 的 `files`。
|
|
6
|
+
|
|
5
7
|
主持开始新讨论时确认全自动或手动复制冷启动。自动任务明确引用仓库内的当前技能版本,并提供任务文件与 CLI 凭据文件路径;参与者先 `execution enter`,再按阶段读取材料。自动执行不建原生定时。手动方式沿用本文的原有接入;受保护房间另由用户提供私有接入文件。讨论项目和 cwd 可选,不能把 CLI 仓库路径当作讨论资料目录或擅自增加目录权限。
|
|
6
8
|
|
|
7
9
|
两份主技能把讨论质量与判断放在前面,仅保留当前任务必需的执行边界。主持人的 `references/manual.md` 只在手动接入、在线转交或原生任务场景读取;参与者的 `references/manual.md` 只管手动持续参与,`references/writing.md` 在首次使用某类写入、代发或失败恢复时按需查阅。自动任务不加载手动流程,也不逐轮复读帮助、全量历史和故障清单。
|
|
@@ -16,6 +18,8 @@
|
|
|
16
18
|
| [discuss-host](../.agents/skills/discuss-host/SKILL.md) | 主持人 | 同时调用参与技能;独立保存用户原文、理解用户意图、接入专家、安排后续发言、保存进度和收束 |
|
|
17
19
|
| [discuss-report](../.agents/skills/discuss-report/SKILL.md) | 报告整理者 | 只读既有讨论,按主题综合分析并保存原文、来源与未决事项;不启动讨论或改写正式决策 |
|
|
18
20
|
|
|
21
|
+
另有 [codiscuss-setup](../.agents/skills/codiscuss-setup/SKILL.md) 供已克隆仓库的用户在仓库内调用,由 AI 完成源码安装、配置、升级、诊断,并在用户确认后部署 Cloudflare 在线网站。它依赖仓库文件,不进入 npm 包,安装器也不复制;Codex 可在仓库内自动发现,其他 AI 明确读取该 `SKILL.md`。
|
|
22
|
+
|
|
19
23
|
技能与网站角色都按实际能力使用。本地可执行 Python 并联网的 AI 可设为 `cli`;只在网页原聊天回答的 AI 设为 `online`。不能因名称是 Codex、Claude 或 Gemini 就推定它有 CLI 或定时能力。
|
|
20
24
|
|
|
21
25
|
## 自动安装完整技能
|
package/docs/workflow.md
CHANGED
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
## 自由协作房间
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
成员回答下的执行报告列出请求编号、耗时、终态与工作区观察文件;失败或取消的报告保留在原请求下。报告展示最近 100 次执行;“未提供自动改动检测”表示非 Git 目录或未上报,不能据此判断没有文件变化。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
自由协作房间从同一角色库选择一位或多位成员,默认由用户主持,也可在创建时指定一位本地 AI 主持人。只有一位成员时,每条消息都交给它;有多位时,在输入框上方点选成员,或在正文中 `@成员`(代码和引用中的 @ 不算),被点名的成员各自处理;未点名且没有 AI 主持人时只作留言。输入 `@` 后,输入框上方弹出在场成员菜单,继续输入可筛选;用方向键与 Enter / Tab 或点击补全,Esc 关闭。选择只修改草稿,Ctrl / Cmd + Enter 仍发送消息。自动成员空闲时,服务把它名下所有未处理的消息打包成一次执行交给原会话;执行期间再发的消息排队,下一次一起投递。手动成员自行读取自己的请求,在线成员由用户转交。成员按时间顺序处理,后面的消息可以修正前面的。
|
|
18
|
+
|
|
19
|
+
运行中可在右侧成员栏邀请 AI(沿用本机软件配置和本房间工作目录),也可让成员离开、请回或移除。自动成员的项目文件权限默认只读;需要它修改项目时,在邀请前选择“项目文件可写”,权限只应用于这位成员。离开会立即停止它的工作;请回时换发新凭证并续接它原来的会话。邀请时可选“手动接入”:成员不会被自动唤醒,而是得到一份私有凭据文件,由你在它的会话中按参与技能读写。绑定你正在使用的会话的成员会标注“借用会话”:它工作时不要在原窗口继续操作。
|
|
18
20
|
|
|
19
21
|
可以把一位本机成员设为主持人。之后没有点名的消息交给主持人分配;主持人在回答中点名成员派活,被点名的成员都结束后主持人再被唤醒一次,继续汇总或安排下一步。派出的成员全部失败时不再唤醒主持人;同一条消息引出的 AI 请求最多 8 次,用完即停,成员栏会提示,发一条新消息即重新开始。
|
|
20
22
|
|
|
23
|
+
普通成员回答中的 `@成员` 或结构化 `to` 只保存为“建议请 X 回应”,不自动唤醒;正文里的代码与引用不算建议。房主可在原回答下点击“转为请求”,网站记录一条用户确认,建立新任务链,并把请求关联到原回答。重复点击或重试返回同一条请求,已完成、失败或取消的请求也不会重新派发;目标成员离开后不能转换。自动、手动和在线成员沿用各自的接入方式。
|
|
24
|
+
|
|
21
25
|
在线 AI 以“在线只读”成员加入:点名后,在成员栏复制阅读通知交给它;拿到回答后,在输入框选择“代发”该成员并粘贴原文,记录保留它为作者、你为提交者,并关闭它的待处理请求。
|
|
22
26
|
|
|
23
27
|
需要深入讨论时,在成员栏点“发起头脑风暴”:用当前成员新建一场讨论房间(有在线成员时为手动推进),轮次和时长独立。讨论收束后,在议题卡片点“把结论发回”,结论会作为留言回到自由房间,不唤醒任何 AI。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai_co_discuss",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"bin": {
|
|
5
5
|
"codiscuss": "runner/codiscuss.mjs"
|
|
6
6
|
},
|
|
@@ -72,7 +72,9 @@
|
|
|
72
72
|
"backend/schema.sql",
|
|
73
73
|
"server.py",
|
|
74
74
|
"web/**",
|
|
75
|
-
".agents/skills/**",
|
|
75
|
+
".agents/skills/discuss-host/**",
|
|
76
|
+
".agents/skills/discuss-participant/**",
|
|
77
|
+
".agents/skills/discuss-report/**",
|
|
76
78
|
".github/SECURITY.md",
|
|
77
79
|
"docs/**",
|
|
78
80
|
"!**/*.log",
|
package/runner/discuss.py
CHANGED
|
@@ -227,7 +227,7 @@ def message_commands(subcommands, registry):
|
|
|
227
227
|
post.add_argument("--turn", type=positive_int, help="所回答的安排编号,仅 opinion 可用")
|
|
228
228
|
post.add_argument("--execution", type=positive_int, help="自由房间:本次执行编号,仅 chat 可用;执行失效后不能改作普通发言")
|
|
229
229
|
post.add_argument("--to", type=ai_name, action="append", default=[],
|
|
230
|
-
help="
|
|
230
|
+
help="自由房间:主持人点名派活,普通成员提出回应建议(须房主确认);可重复")
|
|
231
231
|
post.add_argument("--request", type=positive_int, action="append", default=[],
|
|
232
232
|
help="自由房间手动接入成员:本次回答完成的请求编号,可重复;不能与 --execution 同用")
|
|
233
233
|
post.add_argument("--reply-to", type=positive_int, help="被引用的原文编号")
|
|
@@ -15,6 +15,7 @@ import { tone } from './discussion_tui.mjs';
|
|
|
15
15
|
|
|
16
16
|
const SCRIPT = fileURLToPath(import.meta.url);
|
|
17
17
|
const SERVER = fileURLToPath(new URL('../server.py', import.meta.url));
|
|
18
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
18
19
|
const START_WAIT_MS = 15000;
|
|
19
20
|
const STOP_WAIT_MS = 25000;
|
|
20
21
|
const POLL_MS = 200;
|
|
@@ -149,7 +150,7 @@ async function supervise(home) {
|
|
|
149
150
|
const stop_path = join(directory, 'stop.request');
|
|
150
151
|
const settings = local_settings(home);
|
|
151
152
|
const url = `http://127.0.0.1:${settings.port || 8765}`;
|
|
152
|
-
const status = { pid: process.pid, ready: false, url };
|
|
153
|
+
const status = { pid: process.pid, ready: false, url, version: VERSION };
|
|
153
154
|
if (existsSync(stop_path)) unlinkSync(stop_path);
|
|
154
155
|
atomic_json(status_path, status);
|
|
155
156
|
let website, owned_service, stopped = false;
|
|
@@ -208,6 +209,16 @@ async function start_local(home) {
|
|
|
208
209
|
throw new Error(`本机启动未完成,请查看 ${join(directory, 'supervisor.log')}`);
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/** Explain why a running website must restart before this installation's version or port takes effect. */
|
|
213
|
+
export function restart_reason(home = service_home()) {
|
|
214
|
+
const current = local_status(home);
|
|
215
|
+
if (!current.website_running) return null;
|
|
216
|
+
const url = `http://127.0.0.1:${local_settings(home).port || 8765}`;
|
|
217
|
+
if (current.url !== url) return `本机网站正在 ${current.url} 运行,端口配置已改为 ${url}`;
|
|
218
|
+
if (current.version !== VERSION) return `本机网站正在运行${current.version ? `版本 ${current.version}` : '未记录版本的旧程序'},当前安装为 ${VERSION}`;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
211
222
|
/** Ask the owned supervisor to stop, retaining its database and user configuration. */
|
|
212
223
|
async function stop_local(home) {
|
|
213
224
|
if (!local_status(home).pid) return local_status(home);
|
|
@@ -280,6 +280,13 @@ function setup_issues(result) {
|
|
|
280
280
|
return issues;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/** Ask before restarting a running website; non-interactive runs never stop it and report the pending restart instead. */
|
|
284
|
+
async function confirm_restart(reason, options) {
|
|
285
|
+
if (options.yes || !process.stdin.isTTY) return false;
|
|
286
|
+
const choice = await ask(`${reason}\n重启后生效,重启会暂停托管中的讨论。现在重启?`, '[重启/暂不,默认重启]');
|
|
287
|
+
return !choice.trim() || ['重启', 'restart', 'r', 'y', 'yes'].includes(choice.trim().toLowerCase());
|
|
288
|
+
}
|
|
289
|
+
|
|
283
290
|
/** Configure Python, selected clients and skills, returning partial results on startup failure. */
|
|
284
291
|
async function setup(options) {
|
|
285
292
|
intro('配置 AI CoDiscuss', PACKAGE.version, PACKAGE.description);
|
|
@@ -301,9 +308,17 @@ async function setup(options) {
|
|
|
301
308
|
result.configured = result.issues.length === 0;
|
|
302
309
|
result.status = result.configured ? 'configured' : 'needs_attention';
|
|
303
310
|
if (!options['no-start']) {
|
|
304
|
-
const { local_main } = await import('./discussion_local.mjs');
|
|
305
|
-
const
|
|
311
|
+
const { local_main, restart_reason } = await import('./discussion_local.mjs');
|
|
312
|
+
const reason = restart_reason(home);
|
|
313
|
+
if (reason && !await confirm_restart(reason, options)) {
|
|
314
|
+
result.issues.push(`${reason};重启后才生效,重启会暂停托管中的讨论。确认后运行 codiscuss local stop,再运行 codiscuss local open。`);
|
|
315
|
+
result.configured = false;
|
|
316
|
+
result.status = 'needs_attention';
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
const launch = spinner(reason ? '重启本机网站与讨论服务' : '启动本机网站与讨论服务');
|
|
306
320
|
try {
|
|
321
|
+
if (reason) await local_main(['stop'], () => {});
|
|
307
322
|
result.local = await local_main([options['no-open'] ? 'start' : 'open'], () => {});
|
|
308
323
|
launch.done('网站与讨论服务已在后台运行', [`${tone.cyan(result.local.url)}${options['no-open'] ? tone.dim(' 未打开浏览器,可直接访问此地址') : tone.dim(' 已在浏览器打开')}`]);
|
|
309
324
|
if (result.configured) result.status = 'ready';
|
package/runner/free_runner.mjs
CHANGED
|
@@ -227,8 +227,8 @@ export class FreeRoomRunner {
|
|
|
227
227
|
if (active.has(execution_id) || this.members.get(snapshot.member)?.size) continue;
|
|
228
228
|
this.snapshots.delete(execution_id);
|
|
229
229
|
const after = workspace_snapshot(snapshot.directory);
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
if (!after) continue;
|
|
231
|
+
const changes = changed_between(snapshot.before, after);
|
|
232
232
|
try { await api(this.config, `/executions/${execution_id}`, { action: 'report', changes: changes.slice(0, 200) }); }
|
|
233
233
|
catch (error) { if (!/HTTP (404|409)/.test(error.message)) throw error; }
|
|
234
234
|
}
|
package/web/app.js
CHANGED
|
@@ -636,7 +636,17 @@ function message_markup(message) {
|
|
|
636
636
|
/** Render continuous history while retaining expanded answers and reading position. */
|
|
637
637
|
function render_messages(result) {
|
|
638
638
|
const previous = new Map(state.messages.map((message) => [message.id, message]));
|
|
639
|
-
|
|
639
|
+
const requests = new Map((state.room?.free?.requests || []).map(request => [`${request.message_id}:${request.member_id}`, request]));
|
|
640
|
+
state.messages = result.messages.map((message) => {
|
|
641
|
+
const saved = previous.get(message.id);
|
|
642
|
+
const suggestions = (saved?.suggestions || message.suggestions || []).map(suggestion => {
|
|
643
|
+
const request = requests.get(`${message.id}:${suggestion.member_id}`);
|
|
644
|
+
return request && suggestion.request_id !== request.id ? { ...suggestion, request_id: request.id } : suggestion;
|
|
645
|
+
});
|
|
646
|
+
if (saved) return suggestions.some((item, index) => item !== saved.suggestions?.[index]) ? { ...saved, suggestions } : saved;
|
|
647
|
+
return { ...message, suggestions, rendered_text: window.discuss_markdown.render_markdown(message.text),
|
|
648
|
+
rendered_summary: window.discuss_markdown.render_markdown(message.summary || "") };
|
|
649
|
+
});
|
|
640
650
|
state.message_total = result.total;
|
|
641
651
|
const container = element("messages");
|
|
642
652
|
const open_receipts = [...container.querySelectorAll(".message-receipt[open]")].map((receipt) => receipt.dataset.receiptId);
|
|
@@ -864,10 +874,10 @@ async function return_conclusion() {
|
|
|
864
874
|
}
|
|
865
875
|
|
|
866
876
|
/** Persist a member mutation before transmission and reuse its credential after an uncertain response. */
|
|
867
|
-
function prepare_member_request(room_id, operation, member) {
|
|
877
|
+
function prepare_member_request(room_id, operation, member, options = {}) {
|
|
868
878
|
const key = MEMBER_PENDING_PREFIX + room_id + ":" + operation;
|
|
869
879
|
const previous = localStorage.getItem(key);
|
|
870
|
-
const record = previous ? JSON.parse(previous) : { key, member: { ...member, token: crypto.randomUUID() + crypto.randomUUID() } };
|
|
880
|
+
const record = previous ? JSON.parse(previous) : { key, member: { ...member, token: crypto.randomUUID() + crypto.randomUUID() }, options };
|
|
871
881
|
localStorage.setItem(key, JSON.stringify(record));
|
|
872
882
|
return record;
|
|
873
883
|
}
|
|
@@ -909,7 +919,7 @@ async function interrupt_free() {
|
|
|
909
919
|
}
|
|
910
920
|
|
|
911
921
|
/** Invite an independent member, retaining its credential before either delivery path runs. */
|
|
912
|
-
async function invite_member({ name, access }) {
|
|
922
|
+
async function invite_member({ name, access, project_access = "read" }) {
|
|
913
923
|
const room_id = state.room.id;
|
|
914
924
|
state.free_error = ""; state.free_notice = "";
|
|
915
925
|
try {
|
|
@@ -918,12 +928,14 @@ async function invite_member({ name, access }) {
|
|
|
918
928
|
const taken = new Set((state.room.free?.members || []).map(member => member.name));
|
|
919
929
|
let instance = name;
|
|
920
930
|
for (let number = 2; taken.has(instance); number++) instance = `${name}-${number}`;
|
|
921
|
-
const record = prepare_member_request(room_id, `add:${name}:${access}`, { name: instance, access, adapter: "", session_mode: "new" });
|
|
931
|
+
const record = prepare_member_request(room_id, `add:${name}:${access}`, { name: instance, access, adapter: "", session_mode: "new" }, { project_access });
|
|
922
932
|
let result;
|
|
923
933
|
if (access === "runner") {
|
|
924
934
|
const draft = launch_options();
|
|
935
|
+
const participants = { ...draft.participants, [record.member.name]: { ...draft.participants?.[name],
|
|
936
|
+
...draft.participants?.[record.member.name], project_access: record.options?.project_access || "read" } };
|
|
925
937
|
result = await local_launch_request("room-member", { room_id, token, action: "add", name: record.member.name, access,
|
|
926
|
-
member_token: record.member.token, participants
|
|
938
|
+
member_token: record.member.token, participants, cwd: draft.cwd });
|
|
927
939
|
} else result = await request_json(`/api/rooms/${room_id}/members`, record.member);
|
|
928
940
|
finish_member_request(room_id, token, record, result);
|
|
929
941
|
if (state.room?.id === room_id) {
|
|
@@ -2089,6 +2101,17 @@ async function interrupt_member(member_id) {
|
|
|
2089
2101
|
if (state.room?.id === room_id) await refresh_room();
|
|
2090
2102
|
}
|
|
2091
2103
|
|
|
2104
|
+
/** Confirm one ordinary member's suggestion, keeping retries bound to its source and target. */
|
|
2105
|
+
async function convert_suggestion({ message_id, member_id }) {
|
|
2106
|
+
const room_id = state.room.id;
|
|
2107
|
+
const result = await request_json(`/api/rooms/${room_id}/suggestions`, { message_id, member_id });
|
|
2108
|
+
if (state.room?.id !== room_id) return;
|
|
2109
|
+
state.messages = state.messages.map(message => message.id === message_id ? { ...message,
|
|
2110
|
+
suggestions: message.suggestions.map(suggestion => suggestion.member_id === member_id
|
|
2111
|
+
? { ...suggestion, request_id: result.request.id } : suggestion) } : message);
|
|
2112
|
+
await refresh_room();
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2092
2115
|
/** A completed creation starts the next form with fresh mode defaults. */
|
|
2093
2116
|
function reset_creation_mode() {
|
|
2094
2117
|
state.creation_mode = "brainstorm:automatic";
|
|
@@ -2803,7 +2826,7 @@ async function execute_view_action(name, value) {
|
|
|
2803
2826
|
choose_runtime, export_runtime, download_member_access, copy_member_join, pause_runtime, check_limits, pause_discussion, restart_discussion, force_close, conclude_discussion,
|
|
2804
2827
|
save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection: load_launch_settings,
|
|
2805
2828
|
load_room_configuration, save_room_configuration, interrupt_member, toggle_recipient, change_member, invite_member, interrupt_free, set_proxy, copy_online_notice,
|
|
2806
|
-
prepare_brainstorm, return_conclusion,
|
|
2829
|
+
prepare_brainstorm, return_conclusion, convert_suggestion,
|
|
2807
2830
|
copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };
|
|
2808
2831
|
try {
|
|
2809
2832
|
await actions[name](value);
|