ai_co_discuss 0.2.13 → 0.2.15

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.
@@ -59,13 +59,14 @@ description: "AI CoDiscuss 的主持技能,仅主持人调用,且须同时
59
59
 
60
60
  | 判断 | 保存与结束边界 |
61
61
  | --- | --- |
62
- | 还需专家回应 | 先保存本次判断与下一步意图,再持有发言权创建正式安排;问题互不依赖时用 `turn create --plan-file` 一次排好本轮顺序,网站在专家间自动接力,末位答完才交回主持;接力不发通知,需要逐个转交的在线专家不放进整轮。创建成功即结束本次执行。 |
62
+ | 还需专家回应 | 先保存本次判断与下一步意图,再持有发言权提交下文的发言计划(`turn create --plan-file`);网站在专家间自动接力,末位答完才交回主持;接力不发通知,需要逐个转交的在线专家不放进计划。创建成功即结束本次执行。 |
63
63
  | 本轮仍有排队安排(`queued_turns` 非空) | 说明用户留言、改资料(`held_since` 非 0)或打断让本轮提前交回。先处理这些新内容;原计划仍适用就 `turn continue`,否则直接创建新安排,剩余排队随之作废。 |
64
+ | 专家名单有变化(用户邀请、请回、暂离或移除专家) | 以 `room view` 中最新的 `experts` 为准重排本轮:暂离或移除的专家不再安排,新加入或请回的专家先补独立观点,再进入交锋。`members` 中 `session_mode` 为 `borrowed` 的专家续接用户已有的会话,带着自己的历史上下文:请它先说明哪些判断来自原有上下文,而不是把它当作从零开始的独立观点。排队安排已随名单变化作废,不必取消。 |
64
65
  | 主持人需要以专家身份发言 | 给自己创建正式安排后结束;同一会话收到新的专家任务,再凭新资格发表 `opinion`。 |
65
66
  | 已可收束,或正在限时收束 | 先发表有原文依据的最终 `moderation`、保存 state 与简报,最后设为 `concluded`。 |
66
67
  | 缺少必要用户决定或无法继续 | 保留已有判断与具体阻碍,用 `execution fail` 暂停,向用户说明需要什么。 |
67
68
 
68
- 后续若干次安排的专家与问题可预先确定时(如固定轮流报数,不是只排“本轮三位成员”),一次用 `turn create --plan-file` 排满整个可预知序列(最多 10 项),不要每答完一位就唤醒主持再排下一位;只有后一位必须读前一位回答才能决定时才逐个安排。创建安排或收束成功后立即结束,只回一句话,不再输出总结。
69
+ **发言计划(默认做法)**:每次需要专家回应时,先写好发言计划再一次提交,不逐位安排。计划是 `[{"expert":"名字","question":"…"}]`,按顺序列出本轮每位应发言的专家各一次,最多 10 项。计划只覆盖当前一轮,跨轮追问要看本轮回答后再定;只有各轮问题不依赖回答的固定流程(如依次报数)才可排满多轮。后一位的问题取决于前一位回答时也不必等前一位答完:把问题写成“先读取最新回答,再按……回应”,由专家自己读取。用户留言或打断时服务端会自动把控制权交回主持,不需要主持为此逐位把关。只有在线专家须逐个转交、或必须等用户先决定时,才只排一位,并在 state 里写明原因。创建安排或收束成功后立即结束,只回一句话,不再输出总结。
69
70
 
70
71
  创建安排、完成收束或执行失败会结束本次写入资格;此后只允许回读核验,不再补 state、回执或归还已交接的发言权。是否完成以网站业务结果为准;结果不明按参与技能核对原提交,不另创安排。
71
72
 
@@ -7,7 +7,7 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
7
7
 
8
8
  **主要任务是参与讨论:理解问题,检验证据与假设,回应其他人的论证,形成或修正自己的判断。** 网站操作负责保存这份工作,不应代替思考或成为发言主题。
9
9
 
10
- 所有参与者包括主持人使用本技能;主持人另读 discuss-host。只承担本次授予的职责,安装、读取或编辑技能不启动讨论。各 AI 在自己的软件中形成观点,不调用模型服务 API 替代参与者。
10
+ 软件参与者包括主持人使用本技能;主持人另读 discuss-host。只承担本次授予的职责,安装、读取或编辑技能不启动讨论。各 AI 使用自己的软件或用户明确配置的 API 形成观点;软件参与者不能自行改用模型 API 替代本次身份或其他专家。API 参与者由本机接入程序提供房间工具与相同的参与边界。
11
11
 
12
12
  ## 讨论质量
13
13
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/huyan1458/ai_co_discuss/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/huyan1458/ai_co_discuss/actions/workflows/ci.yml)
4
4
  [MIT License](LICENSE)
5
5
 
6
- 让已有的 AI 软件在同一个房间里协作:在网页遥控本地 AI 干活,随时邀请其他 AI 加入或请离,也可以围绕议题发起多轮讨论。共享记录保存消息、观点、分歧和结论。每个 AI 使用自己的会话、账号与工具,项目不调用模型服务 API 生成回答。
6
+ 让 AI 软件和 API 专家在同一个房间里协作:在网页遥控本地 AI 干活,随时邀请其他 AI 加入或请离,也可以围绕议题发起多轮讨论。共享记录保存消息、观点、分歧和结论。软件参与者使用自己的会话、账号与工具;API 参与者使用你配置的模型服务和房间上下文。
7
7
 
8
8
  自由协作房间里,你点名哪个 AI,它就在自己的原会话中处理并回复,多位可以同时进行;也可以指定一位 AI 主持人替你分配任务。讨论房间由主持人安排发言与收束。本地 AI 通过 CLI 读写,在线网页 AI 在原聊天回答,由你或主持人代发。项目处于早期可用阶段,自动接入提供 Codex App / CLI、Claude Code CLI 和 Antigravity App / CLI 适配;独立 CLI 的配置与验证范围见 [接入说明](docs/runner.md#独立-cli-接入)。
9
9
 
@@ -17,7 +17,7 @@
17
17
 
18
18
  ### 安装包用户
19
19
 
20
- 需要 **Node.js 24+、npm、Python 3.10+**,所选 AI CLI 须预先安装并登录;安装器不会代装这些运行环境或 AI 软件。安装包已发布到 npm registry,在任意目录运行:
20
+ 需要 **Node.js 24+、npm、Python 3.10+**。使用 CLI 时须预先安装并登录;仅使用 API 时无需安装 AI 软件,准备对应服务的 API Key 即可。安装器不会代装运行环境或 AI 软件。安装包已发布到 npm registry,在任意目录运行:
21
21
 
22
22
  ```sh
23
23
  npx --yes --package ai_co_discuss codiscuss install
@@ -65,11 +65,15 @@ codiscuss setup
65
65
 
66
66
  1. 安装完成后进入自动打开的网站;以后运行 `codiscuss local open` 打开或复用本机网站,默认地址为 `http://127.0.0.1:8765`。
67
67
  2. 点击“新建讨论”,填写**话题名称**和**用户原文**。把问题、背景与顾虑写在原文中,讨论目标、背景和边界由 AI 主持人据此补全。
68
- 3. 房间用法选择“头脑风暴”,接入方式默认“程序唤醒”,从卡片下方的“添加”行加入本场专家,点卡片上的皇冠指定 AI 主持人,点击“移出”调整名单。自动 CLI 卡片背面从真实软件目录下拉选择模型,并按模型支持的档位设置思考强度;留空使用 CLI 默认值,App 在原软件设置。项目文件只读 / 可写也在每张自动 CLI 卡片上分别设置(默认只读),在线专家始终只读;软件路径缺失时卡片背面直接提示填写。分别设置轮次和时长上限,点击数值展开滚轮或直接输入。新建默认 **3 轮 / 不限时长**,按轮次上限或手动停止结束;单次 AI 执行也默认不限时,可按需设置 1–60 分钟上限。
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
- 只想让 AI 干活时,在第 3 步将房间用法改为“自由协作”,只需填写话题名称,任务在对话中随时补充:选择一位或多位本地 AI,默认由你主持,也可指定 AI 主持人;使用“程序唤醒”时点击“创建并连接”,之后在频道里直接发消息。多位时点名或在正文中 `@` 它们,处理中可随时停止。运行中可在右侧成员栏邀请、请离或请回 AI,指定主持人,或发起关联的头脑风暴。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
72
+ 讨论进行中可在右侧进度看板邀请新专家(沿用本机软件配置);Codex、Claude 可填写已有会话 ID,请一个带着自己上下文的原会话加入,它工作时不要在原窗口继续操作。也可从成员的“…”菜单让其暂离、请回或移除。暂离保留位置与会话,不参与安排;这些操作不暂停讨论,主持人会被唤醒重新安排。主持人不能暂离,需先更换。
73
+
74
+ 只想让 AI 干活时,在第 3 步将房间用法改为“自由协作”,只需填写话题名称,任务在对话中随时补充:选择一位或多位 AI,并在每张卡片背面选择接入方式:本机唤醒(App / CLI / API)、手动接入(凭据交给你自己的会话)或网页代发(回答由你转发)。默认由你主持,也可指定 AI 主持人;有本机唤醒的成员时点击“创建并连接”,之后在频道里直接发消息。多位时点名或在正文中 `@` 它们,处理中可随时停止。运行中可在右侧成员栏邀请、请离或请回 AI,指定主持人,或发起关联的头脑风暴。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
75
+
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-接入)。
73
77
 
74
78
  也可不勾选立即启动,先建房,再从“接入”页点击“启动讨论”。本机网页正常启动无需下载 JSON、手动登记房间或另执行首次启动命令。
75
79
 
@@ -111,7 +115,7 @@ codiscuss local connect https://你的站点
111
115
 
112
116
  软件失败时先查看暂停原因,登录和权限问题在对应软件中处理。本机网站的已登记房间,可从进度看板 Codex 旁的“…”菜单直接选择“切换 App / CLI”,在弹窗内暂停并保存;软件路径在“接入 → 修正本场软件连接”修改。原会话与讨论进度保留,待服务确认采用后重启;线上连接暂不提供此修正入口。切到 App 后模型和文件权限按 App 设置,切回 CLI 前须让 App 释放原聊天。本场修改不影响后续讨论的默认配置,详见 [配置修正](docs/runner.md#已启动后的配置修正)。
113
117
 
114
- **手动接入:** 先选“自由协作”或“头脑风暴”,再选手动接入(手动复制冷启动)。从“接入”页下载各本地成员自己的凭据文件,连同参与指令私下交给对应 AI 会话;建房无需本机接入服务,后续可由软件原生定时或人工触发,无需每轮复制本地 AI 的回答。在线专家只读,由主持人操作浏览器或由用户转交回答,首次确定分工后沿用。详见 [讨论工作流](docs/workflow.md)。
118
+ **手动接入与网页 AI:** 只用于“自由协作”,在成员卡片背面选择。手动成员从成员菜单下载自己的凭据文件,连同参与说明私下交给对应 AI 会话,由人工或软件原生定时触发;网页 AI 只能阅读房间,回答由你代发;它从自己的服务器读取阅读链接,只在本机运行的网站(`127.0.0.1`)它读不到,需由你粘贴材料。只有手动和网页成员时建房无需本机接入服务。讨论房间需要全程自动推进,暂不接受这两种成员。详见 [自由协作房间](docs/workflow.md#自由协作房间)。
115
119
 
116
120
  ## 在线部署
117
121
 
@@ -133,10 +137,10 @@ codiscuss local connect https://你的站点
133
137
 
134
138
  | 方式 | 由谁推进 | 前提 |
135
139
  | --- | --- | --- |
136
- | 自由协作 | 你或 AI 主持人点名成员,按成员接入方式处理 | 默认由你主持,可增设一位本地 AI 主持人;自动成员由本机服务唤醒,手动成员自行读取请求,在线成员回答由你代发 |
137
- | 事件驱动 | 本机服务按网站事件唤醒对应 AI 会话 | 选择“全自动讨论”,主持人与专家均为支持读写的本地 AI(CLI / App) |
138
- | 软件原生定时 | AI 软件定时检查当前安排 | 选择“手动复制冷启动”,软件具备原生定时能力 |
139
- | 人工触发 | 用户要求对应会话继续 | 选择“手动复制冷启动”;在线回答需代发 |
140
+ | 讨论 | 本机服务按网站事件唤醒主持人与专家 | 主持人与专家均由本机服务唤醒(CLI / App / API) |
141
+ | 自由协作 · 本机唤醒 | 你或 AI 主持人点名后,本机服务唤醒该成员 | 成员接入为 CLI / App / API |
142
+ | 自由协作 · 手动接入 | 成员自行读取请求,由软件原生定时或人工触发 | 成员持自己的凭据文件 |
143
+ | 自由协作 · 网页代发 | 你把阅读通知交给网页 AI,再代发回答 | 无需本地软件,不能自行发言 |
140
144
 
141
145
  默认上限为 **3 轮、不限时长、到限暂停**,可改为请主持人总结,并设置单次执行上限与收束时间。计数和停止规则见 [讨论上限](docs/workflow.md#讨论上限)。
142
146
 
@@ -673,6 +673,70 @@ def add_member(connection, room_id, token, payload, now):
673
673
  return free_view(connection, room_id)
674
674
 
675
675
 
676
+ def discussion_roster(connection, room_id):
677
+ """Return a discussion's host, active experts and phase; away experts keep only their member row."""
678
+ row = connection.execute("SELECT host,experts,phase FROM topic_settings WHERE room_id=?", (room_id,)).fetchone()
679
+ return row["host"], json.loads(row["experts"]), row["phase"]
680
+
681
+
682
+ def save_discussion_experts(connection, room_id, experts):
683
+ """Write the active roster; a running discussion keeps going and its host is asked to re-plan."""
684
+ connection.execute("UPDATE topic_settings SET experts=? WHERE room_id=?", (json.dumps(experts, ensure_ascii=False), room_id))
685
+
686
+
687
+ def add_discussion_member(connection, room_id, token, payload, now):
688
+ """Owner invites one expert into a discussion using the room's delivery method."""
689
+ require_owner(connection, room_id, token)
690
+ begin_write(connection)
691
+ member = member_value(payload)
692
+ _, experts, phase = discussion_roster(connection, room_id)
693
+ run = connection.execute("SELECT mode FROM discussion_runtime WHERE room_id=?", (room_id,)).fetchone()
694
+ if member["access"] != ("runner" if run and run["mode"] == "automatic" else "manual"):
695
+ raise ValueError("新专家的接入方式须与讨论的推进方式一致")
696
+ existing = connection.execute("SELECT * FROM room_members WHERE room_id=? AND name=? AND status<>'removed'", (room_id, member["name"])).fetchone()
697
+ if existing and existing["status"] == "active" and existing["token_hash"] == token_hash(member["token"]):
698
+ return
699
+ if existing:
700
+ raise FileExistsError(f"{member['name']} 已是本房间成员;离开的成员请用请回")
701
+ if phase == "concluded":
702
+ raise ValueError("讨论已收束,不能再邀请专家")
703
+ if len(experts) >= MAX_ROOM_AGENTS:
704
+ raise ValueError("房间成员已达上限")
705
+ require_fresh_member_token(connection, room_id, member["token"])
706
+ insert_member(connection, room_id, member, now)
707
+ save_discussion_experts(connection, room_id, {**experts, member["name"]: "cli"})
708
+
709
+
710
+ def discussion_member_action(connection, room_id, member_id, token, payload):
711
+ """Owner sends an expert away (keeping its seat), brings it back with a new credential, or removes it.
712
+
713
+ Leaving cancels the expert's open assignments; the host keeps its role and cannot leave.
714
+ """
715
+ member = owned_member(connection, room_id, member_id, token)
716
+ host, experts, _ = discussion_roster(connection, room_id)
717
+ action = payload.get("action")
718
+ if action in ("away", "remove"):
719
+ if member["name"] == host:
720
+ raise ValueError("主持人不能离开或移除;请先更换主持人")
721
+ connection.execute("UPDATE discussion_turns SET status='cancelled' WHERE room_id=? AND expert=? AND status IN ('pending','notified')",
722
+ (room_id, member["name"]))
723
+ connection.execute("UPDATE discussion_turns SET status='skipped' WHERE room_id=? AND expert=? AND status='queued'", (room_id, member["name"]))
724
+ connection.execute("UPDATE room_members SET status=? WHERE id=?", ("away" if action == "away" else "removed", member_id))
725
+ experts.pop(member["name"], None)
726
+ elif action == "active":
727
+ hashed = token_hash(payload.get("token"))
728
+ if member["status"] == "active" and hashed and hmac.compare_digest(hashed, member["token_hash"]):
729
+ return
730
+ if member["status"] != "away" or not hashed or hmac.compare_digest(hashed, member["token_hash"]):
731
+ raise ValueError("只有离开的成员可以请回,且须提供新的成员凭证")
732
+ require_fresh_member_token(connection, room_id, payload.get("token"))
733
+ connection.execute("UPDATE room_members SET status='active',token_hash=? WHERE id=?", (hashed, member_id))
734
+ experts[member["name"]] = "online" if member["access"] == "online" else "cli"
735
+ else:
736
+ raise ValueError("action 须为 away、active 或 remove")
737
+ save_discussion_experts(connection, room_id, experts)
738
+
739
+
676
740
  def require_fresh_member_token(connection, room_id, token):
677
741
  """Prevent owner or another member's credential from being reused for a new identity."""
678
742
  hashed = token_hash(token)
@@ -1,31 +1,31 @@
1
- """Participant identities: an AI platform is a type, a room participant is one named instance of it.
2
-
3
- `Claude` and `Claude-评审` are two instances of the Claude platform in the same room; each keeps its
4
- own session, model and thinking effort. Platform names alone keep their historical meaning.
5
- """
6
-
7
- import re
8
-
9
- USER = "用户"
10
- AI_PLATFORMS = ("Codex", "Claude", "Gemini", "DeepSeek", "Qwen")
11
- PARTICIPANTS = [USER, *AI_PLATFORMS]
12
- MAX_INSTANCE_SUFFIX = 16
13
- # AI instances one room may hold, across platforms.
14
- MAX_ROOM_AGENTS = 12
15
- INSTANCE_PATTERN = re.compile(rf"({'|'.join(AI_PLATFORMS)})(?:-[0-9A-Za-z_一-鿿]{{1,{MAX_INSTANCE_SUFFIX}}})?")
16
-
17
-
18
- def is_ai(name):
19
- """Whether a name is an AI platform or a named instance of one."""
20
- return isinstance(name, str) and INSTANCE_PATTERN.fullmatch(name) is not None
21
-
22
-
23
- def is_participant(name):
24
- """Whether a name may author, submit or be scheduled: the user or any AI instance."""
25
- return name == USER or is_ai(name)
26
-
27
-
28
- def platform_of(name):
29
- """Return the platform an AI instance belongs to, or None for the user and invalid names."""
30
- match = INSTANCE_PATTERN.fullmatch(name) if isinstance(name, str) else None
31
- return match.group(1) if match else None
1
+ """Participant identities: an AI platform is a type, a room participant is one named instance of it.
2
+
3
+ `Claude` and `Claude-评审` are two instances of the Claude platform in the same room; each keeps its
4
+ own session, model and thinking effort. Platform names alone keep their historical meaning.
5
+ """
6
+
7
+ import re
8
+
9
+ USER = "用户"
10
+ AI_PLATFORMS = ("Codex", "Claude", "Gemini", "DeepSeek", "Qwen", "Grok")
11
+ PARTICIPANTS = [USER, *AI_PLATFORMS]
12
+ MAX_INSTANCE_SUFFIX = 16
13
+ # AI instances one room may hold, across platforms.
14
+ MAX_ROOM_AGENTS = 12
15
+ INSTANCE_PATTERN = re.compile(rf"({'|'.join(AI_PLATFORMS)})(?:-[0-9A-Za-z_一-鿿]{{1,{MAX_INSTANCE_SUFFIX}}})?")
16
+
17
+
18
+ def is_ai(name):
19
+ """Whether a name is an AI platform or a named instance of one."""
20
+ return isinstance(name, str) and INSTANCE_PATTERN.fullmatch(name) is not None
21
+
22
+
23
+ def is_participant(name):
24
+ """Whether a name may author, submit or be scheduled: the user or any AI instance."""
25
+ return name == USER or is_ai(name)
26
+
27
+
28
+ def platform_of(name):
29
+ """Return the platform an AI instance belongs to, or None for the user and invalid names."""
30
+ match = INSTANCE_PATTERN.fullmatch(name) if isinstance(name, str) else None
31
+ return match.group(1) if match else None