ai_co_discuss 0.3.19 → 0.3.22

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.
@@ -26,16 +26,16 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
26
26
 
27
27
  1. **不读取或打印凭据内容**,token 不进入提示或房间。每个成员只用自己的凭据,AI 不借用用户身份发言,房主控制凭据不能代替成员署名。(身份与权限由网站按凭据校验。)
28
28
  2. **执行失效后不再写入**:暂停、到期或被打断的旧任务保留草稿并结束,不换身份、职责或编号绕过拒绝,也不去掉 `--execution` 改作普通发言。任务文件的 `stopped_executions` 列出你此前被用户停止或跳过的执行:会话里如有它们未完成的工作,不接着做、不为它们补交,只处理本次请求。(失效的执行若还能产生可见结果,用户的打断与暂停就失去意义。)
29
- 3. **写入前定好 `--submission-id`**:同次重试沿用编号和原内容,新内容换新编号;结果不明时先按原编号查询,再按 [写入与恢复](references/writing.md) 处理。(网站靠它去重,换号会产生重复发言。)
29
+ 3. **写入前定好 `--submission-id`**:同次重试沿用编号和原内容,新内容换新编号;`message post` 可省略,CLI 按房间、身份、关联与内容生成,用同一份草稿重试即得同一编号;结果不明时先按原编号查询,再按 [写入与恢复](references/writing.md) 处理。(网站靠它去重,换号会产生重复发言。)
30
30
  4. **不创建定时任务、不操作其他软件、不发状态回执**(就绪、开始、完成、停止等说明),除非用户要求或出现需要记录的异常;回执不能作为观点、完成或共识的证据。(这些动作有副作用,回执只会淹没真正的回答。)
31
31
 
32
32
  ## 命令入口与身份
33
33
 
34
34
  用统一命令 `codiscuss room …`、`codiscuss message …` 等;任务或用户显式指定 CLI 路径时沿用指定入口,未安装时用已核实的 `python 路径/runner/discuss.py …`(统一入口需 Node.js 24+,网站读写需 Python 3.10+)。裸 `codiscuss` 会启动服务,不要单独运行;用法查 `codiscuss --help`,字段以本技能为准,这里没写的才查 `codiscuss schema 具体命令`。下文命令均省略入口前缀。
35
35
 
36
- 从实际任务或用户指定值核实站点、房间和身份,不从当前网页或最近聊天猜测:站点用 `--site`、进程 `DISCUSS_URL` 或凭据绑定的站点,房间始终显式指定,写入显式 `--as 本人`。任务标明 `access_env: "DISCUSS_ACCESS_FILE"` 时凭据已由进程绑定,`--access-file FILE` 可省略;否则用任务交付的 `access_file`。项目与 cwd 有值时用于本次获准资料;CLI 路径只定位工具,不把 CLI 仓库当作讨论项目或扩大文件权限。
36
+ 从实际任务或用户指定值核实站点、房间和身份,不从当前网页或最近聊天猜测。任务标明 `access_env: "DISCUSS_ACCESS_FILE"` 时凭据已由进程绑定,`--access-file FILE` 可省略;否则用任务交付的 `access_file`。任务凭据已绑定站点、房间、身份与本次执行,命令省略房间、`--as`、`--execution` 和 `--submission-id`,由 CLI 从凭据和内容补齐,例如 `message post --body-file 草稿.md`;下文命令里的 `ROOM`、`--as`、执行编号只在没有绑定凭据(如手动接入未带凭据)时需要填写。项目与 cwd 有值时用于本次获准资料;CLI 路径只定位工具,不把 CLI 仓库当作讨论项目或扩大文件权限。
37
37
 
38
- 每次 shell 调用都是一次模型往返,手动会话里还可能要用户逐条确认,所以互不依赖的只读命令放进同一条 shell 命令依次执行。正文想好后,生成提交编号、写草稿、`message post` 放进同一条命令,草稿写成功才提交(失败重试仍用这份草稿和编号);不同的网站写入各自单独执行并核对结果。
38
+ 每次 shell 调用都是一次模型往返,手动会话里还可能要用户逐条确认,所以互不依赖的只读命令放进同一条 shell 命令依次执行。正文想好后,先用当前软件的文件写入工具把正文(和需要的摘要)写成草稿文件,再用一条只含 `codiscuss` 的命令以 `--body-file`(`--summary-file`)提交;失败重试仍用这份草稿。不要用 heredoc、`echo` 重定向或命令行字面量传长文本,也不要在命令里调用函数生成编号(如 `[guid]::NewGuid()`、`$(uuidgen)`):自动运行时这类组合命令会被权限检查拒绝,文本中的引号(含中文弯引号)还会截断参数。其他写入命令需要编号时直接写一个新的 UUID 字面值。不同的网站写入各自单独执行并核对结果。
39
39
 
40
40
  ## 阅读
41
41
 
@@ -58,6 +58,6 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
58
58
 
59
59
  ## 保存与核验
60
60
 
61
- 正文优先 UTF-8 `--body-file`。需要附件时先 `attachment upload ROOM --file FILE --submission-id 上传编号`,再用 `message post --attachment ID` 关联(最多 10 个,单个 20 MiB,须在执行资格结束前上传)。
61
+ 正文用 UTF-8 `--body-file`,摘要含引号或较长时用 `--summary-file`。需要附件时先 `attachment upload ROOM --file FILE --submission-id 上传编号`,再用 `message post --attachment ID` 关联(最多 10 个,单个 20 MiB,须在执行资格结束前上传)。
62
62
 
63
63
  成功须核对服务端返回的持久记录、作者和关联;返回已是持久记录时不再读一遍,缺少所需证据才按编号补读。答案落库后即可结束,不用旧资格继续写入。代发保留真实作者、完整原文及引用关系,摘要归实际撰写者,不把自己的改写冒充对方回答。执行结束不等于整个房间的工作结束。
@@ -29,13 +29,13 @@
29
29
  自动任务用本次执行编号保存一次完整回答,例如:
30
30
 
31
31
  ```sh
32
- codiscuss message post ROOM --as 本人 --kind chat --execution 执行编号 --body-file draft.md --submission-id 新UUID --access-file FILE
32
+ codiscuss message post ROOM --as 本人 --kind chat --execution 执行编号 --body-file draft.md --access-file FILE
33
33
  ```
34
34
 
35
35
  手动成员用自己的成员凭据和服务端给出的请求编号:
36
36
 
37
37
  ```sh
38
- codiscuss message post ROOM --as 本人 --kind chat --request 请求编号 --body-file draft.md --submission-id 新UUID --access-file FILE
38
+ codiscuss message post ROOM --as 本人 --kind chat --request 请求编号 --body-file draft.md --access-file FILE
39
39
  ```
40
40
 
41
41
  需要摘要时一并使用 `--summary` 或 `--summary-file`。成功提交后当前请求即完成;不要再补一条普通 chat 或状态回执。
@@ -28,7 +28,7 @@
28
28
 
29
29
  主持类写入需要本次任务授予主持职责。网页成员代发须保留真实作者、完整原文、关联请求、原答复的链接/引用对应关系及同房间关联,不把自己的改写冒充他人原文。主持人补充的引用附录需明确标注来源与归属,无法取得的出处说明缺失;不把自己的核验结论混进专家原文。
30
30
 
31
- 房间、消息和安排创建使用显式 `--submission-id`:新内容生成新编号,同一次提交保留编号、原始资料、身份和关联。**先保存编号与原文,再发请求**,避免中断后无法核对。编号为 8–100 位字母、数字、下划线或连字符,可用 UUID。建房重试对照首次创建资料,不使用后来修改的话题内容替换请求。
31
+ 房间、消息和安排创建使用显式 `--submission-id`:新内容生成新编号,同一次提交保留编号、原始资料、身份和关联。**先保存编号与原文,再发请求**,避免中断后无法核对。编号为 8–100 位字母、数字、下划线或连字符,可用 UUID。`message post` 省略编号时由 CLI 按房间、身份、关联与内容生成(`auto-` 开头),草稿文件即原文记录,重试只需用同一份草稿。建房重试对照首次创建资料,不使用后来修改的话题内容替换请求。
32
32
 
33
33
  长正文优先 UTF-8 文件,`--body-file -` 可读 stdin。当前消息正文上限为 32000 个 Unicode 字符,详细讨论状态上限为 4000 字;以目标站点协议为准。完整回答一次提交并完成安排,不能截断或用第一段冒充完整回答。超过上限时保留完整原文并说明限制,先明确完整材料的留档方式。
34
34
 
@@ -1107,11 +1107,11 @@ def advance_queues(connection, room_id):
1107
1107
 
1108
1108
  Asked rows stay, so the speaking order can still show who already answered.
1109
1109
  """
1110
- connection.execute(f"""DELETE FROM free_queue WHERE room_id=? AND NOT {QUEUE_ASKED.format(row="free_queue")}
1111
- AND NOT EXISTS(SELECT 1 FROM room_members m WHERE m.id=free_queue.member_id AND m.status='active')""", (room_id,))
1112
- connection.execute(f"""INSERT OR IGNORE INTO member_requests (room_id,member_id,message_id,chain_id,batch_id,purpose,created_by,created_at)
1113
- SELECT q.room_id,q.member_id,q.message_id,q.message_id,0,'ask','user',? FROM free_queue q
1114
- WHERE q.room_id=? AND q.id=(SELECT MIN(x.id) FROM free_queue x WHERE x.message_id=q.message_id AND NOT {QUEUE_ASKED.format(row="x")})
1110
+ connection.execute("""DELETE FROM free_queue WHERE id IN (SELECT q.id FROM free_queue_waiting q WHERE q.room_id=?
1111
+ AND NOT EXISTS(SELECT 1 FROM room_members m WHERE m.id=q.member_id AND m.status='active'))""", (room_id,))
1112
+ connection.execute("""INSERT OR IGNORE INTO member_requests (room_id,member_id,message_id,chain_id,batch_id,purpose,created_by,created_at)
1113
+ SELECT q.room_id,q.member_id,q.message_id,q.message_id,0,'ask','user',? FROM free_queue_waiting q
1114
+ WHERE q.id IN (SELECT MIN(id) FROM free_queue_waiting WHERE room_id=? GROUP BY message_id)
1115
1115
  AND NOT EXISTS(SELECT 1 FROM member_requests r WHERE r.message_id=q.message_id AND r.status IN ('pending','delivering'))""",
1116
1116
  (int(time.time()), room_id))
1117
1117
 
@@ -1142,8 +1142,11 @@ def speaking_order(connection, room):
1142
1142
  target = session["id"]
1143
1143
  items, later, session_plan_state = discussion.session_speaking_order(connection, session)
1144
1144
  else:
1145
- target = connection.execute("""SELECT MAX(q.message_id) FROM free_queue q LEFT JOIN member_requests r ON r.message_id=q.message_id
1146
- AND r.member_id=q.member_id WHERE q.room_id=? AND (r.id IS NULL OR r.status IN ('pending','delivering'))""", (room["id"],)).fetchone()[0] or 0
1145
+ target = connection.execute("""SELECT MAX(message_id) FROM (
1146
+ SELECT MAX(message_id) AS message_id FROM free_queue_waiting WHERE room_id=?
1147
+ UNION ALL SELECT MAX(r.message_id) FROM member_requests r WHERE r.room_id=? AND r.status IN ('pending','delivering')
1148
+ AND EXISTS(SELECT 1 FROM free_queue q WHERE q.message_id=r.message_id AND q.member_id=r.member_id))""",
1149
+ (room["id"], room["id"])).fetchone()[0] or 0
1147
1150
  if target:
1148
1151
  kind = "queue"
1149
1152
  for row in connection.execute("""SELECT q.member_id,r.status FROM free_queue q LEFT JOIN member_requests r
@@ -1817,8 +1820,8 @@ def interrupt_member(connection, room_id, member_id, token):
1817
1820
 
1818
1821
  def withdrawals(connection, room_id):
1819
1822
  """The room's taken-back messages by id, so the page keeps them in place while AI readers skip them."""
1820
- return [dict(row) for row in connection.execute("""SELECT w.message_id,w.kind FROM message_withdrawals w
1821
- JOIN messages m ON m.id=w.message_id WHERE m.room_id=? ORDER BY w.message_id""", (room_id,))]
1823
+ return [dict(row) for row in connection.execute("""SELECT w.message_id,w.kind FROM message_withdrawal_rooms r
1824
+ JOIN message_withdrawals w ON w.message_id=r.message_id WHERE r.room_id=? ORDER BY r.message_id""", (room_id,))]
1822
1825
 
1823
1826
 
1824
1827
  def owner_message(connection, room_id, message_id):
@@ -20,6 +20,8 @@ from .admin import AdminError
20
20
  from .runtime import begin_write
21
21
 
22
22
  MACHINE_ONLINE_SECONDS = 30
23
+ # Heartbeats refresh seen_at at most this often (well inside the online window) so frequent polls do not each write a row.
24
+ MACHINE_SEEN_REFRESH_SECONDS = 10
23
25
  MACHINE_NAME_MAX = 60
24
26
  PROJECT_NAME_MAX = 80
25
27
  PROJECTS_MAX = 100
@@ -135,11 +137,15 @@ def machine_sync(connection, machine_id, token, payload, now):
135
137
  projects, clients = valid_projects(payload.get("projects")), valid_clients(payload.get("clients"))
136
138
  public_key = valid_public_key(payload["public_key"]) if "public_key" in payload else None
137
139
  begin_write(connection)
138
- connection.execute("UPDATE local_machines SET projects=?, clients=?, seen_at=? WHERE id=?",
139
- (json.dumps(projects, ensure_ascii=False), json.dumps(clients, ensure_ascii=False), now, machine_id))
140
+ # Unchanged heartbeats write nothing until seen_at is due for a refresh.
141
+ connection.execute("UPDATE local_machines SET projects=?1, clients=?2, seen_at=?3 WHERE id=?4 "
142
+ "AND (projects IS NOT ?1 OR clients IS NOT ?2 OR seen_at<?3-?5)",
143
+ (json.dumps(projects, ensure_ascii=False), json.dumps(clients, ensure_ascii=False), now, machine_id,
144
+ MACHINE_SEEN_REFRESH_SECONDS))
140
145
  if public_key:
141
146
  connection.execute("INSERT INTO machine_keys (machine_id,public_key) VALUES (?,?) "
142
- "ON CONFLICT(machine_id) DO UPDATE SET public_key=excluded.public_key", (machine_id, json.dumps(public_key)))
147
+ "ON CONFLICT(machine_id) DO UPDATE SET public_key=excluded.public_key "
148
+ "WHERE machine_keys.public_key IS NOT excluded.public_key", (machine_id, json.dumps(public_key)))
143
149
  rows = connection.execute("SELECT id,request FROM remote_launches WHERE machine_id=? AND status IN ('pending','claimed') ORDER BY created_at",
144
150
  (machine_id,)).fetchall()
145
151
  return {"launches": [{"id": row["id"], "request": json.loads(row["request"])} for row in rows]}
@@ -245,7 +251,7 @@ def claim_machine_commands(connection, machine_id, token, now):
245
251
  require_machine(connection, machine_id, token)
246
252
  begin_write(connection)
247
253
  connection.execute("DELETE FROM machine_commands WHERE machine_id=? AND created_at<?", (machine_id, now - COMMAND_TTL_SECONDS))
248
- connection.execute("UPDATE local_machines SET seen_at=? WHERE id=?", (now, machine_id))
254
+ connection.execute("UPDATE local_machines SET seen_at=? WHERE id=? AND seen_at<?", (now, machine_id, now - MACHINE_SEEN_REFRESH_SECONDS))
249
255
  rows = connection.execute("SELECT id,request FROM machine_commands WHERE machine_id=? AND status='pending' ORDER BY created_at",
250
256
  (machine_id,)).fetchall()
251
257
  for row in rows: