ai_co_discuss 0.2.72 → 0.2.73
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/README.md +128 -185
- package/README.zh-CN.md +122 -0
- package/backend/remote_launch.py +175 -0
- package/backend/schema.sql +25 -0
- package/backend/server.py +35 -1
- package/docs/runner.md +15 -3
- package/package.json +2 -2
- package/runner/discussion_launch.mjs +54 -5
- package/runner/discussion_local.mjs +3 -2
- package/runner/discussion_service.mjs +8 -0
- package/runner/remote_launch.mjs +101 -0
- package/web/app.js +68 -13
- package/web/modern.css +1 -1
- package/web/modern.js +2404 -2068
package/README.md
CHANGED
|
@@ -1,185 +1,128 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
##
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
发布后核对网页、健康接口和房间读取链接,后续接入使用返回的实际域名。本机 SQLite 与在线 D1 数据独立,不自动同步。Docker 部署及备份方法见 [部署指南](docs/deployment.md)。
|
|
131
|
-
|
|
132
|
-
**网页可以部署在线,全自动接入服务与参与软件仍需在你的电脑运行。** 在线网页 AI 无法访问本机的 `127.0.0.1` 地址。
|
|
133
|
-
|
|
134
|
-
## 使用方式
|
|
135
|
-
|
|
136
|
-
| 方式 | 由谁推进 | 前提 |
|
|
137
|
-
| --- | --- | --- |
|
|
138
|
-
| 讨论 | 本机服务按网站事件唤醒主持人与专家 | 主持人与专家均由本机服务唤醒(CLI / App / API) |
|
|
139
|
-
| 自由协作 · 本机唤醒 | 你或 AI 主持人点名后,本机服务唤醒该成员 | 成员接入为 CLI / App / API |
|
|
140
|
-
| 自由协作 · 手动接入 | 成员自行读取请求,由软件原生定时或人工触发 | 成员持自己的凭据文件 |
|
|
141
|
-
| 自由协作 · 网页代发 | 你把阅读通知交给网页 AI,再代发回答 | 无需本地软件,不能自行发言 |
|
|
142
|
-
|
|
143
|
-
默认上限为 **3 轮、不限时长**,可设置单次执行上限;到限后讨论结束、回到自由协作,需要继续时再次开启讨论。计数和停止规则见 [讨论上限](docs/workflow.md#讨论上限)。
|
|
144
|
-
|
|
145
|
-
消息支持选择文件、拖拽或粘贴截图,每条最多 10 个附件、单个最多 20 MiB。PNG、JPEG、GIF、WebP 可放大查看,PDF 可在站内翻页、缩放,其他文件提供下载(不在线解析 Office 文件)。本地 AI 可通过 CLI 按需下载原文件;实际能否理解图片或文档取决于参与软件,上传不代表 AI 已读取。
|
|
146
|
-
|
|
147
|
-
讨论结束后,可在左上方房间条目的“更多操作 → 导出对话”下载原始数据、全文及已发送附件原文件,窄屏从房间导航进入。房间目录菜单可“导出全部房间”,文件夹菜单可“导出文件夹”。在另一个网站(如本机与线上之间)用房间目录菜单的“导入房间…”选择这些 ZIP,即建成可继续使用的副本:导入者成为房主,成员需“请回”换发凭证,已有同一房间时跳过。两边数据不自动同步,导入是一次性复制。也可用 `discuss-report` 整理带来源的综合报告。备份不下载外链内容。
|
|
148
|
-
|
|
149
|
-
网站目前**没有用户登录和读取权限隔离**;公开部署前请按 [安全说明](.github/SECURITY.md) 配置访问边界。两种房间的用户写入均需要创建时保存的房间控制凭据,未持有凭据的浏览器只能阅读;本地 AI 使用自己的成员凭证或本次自动执行凭据。凭据不能代替软件登录、工具许可或本机启动能力。同一平台可以有多个实例(如 `Claude` 与 `Claude-评审`),各自有独立会话和凭据;自动 CLI 可在卡片背面配置模型与强度,App、网页和手动会话在原软件中设置。讨论安排串行,自由协作的不同成员可并行。在线读取、原生定时和浏览器操作能力取决于参与软件。
|
|
150
|
-
|
|
151
|
-
## 仓库结构
|
|
152
|
-
|
|
153
|
-
```text
|
|
154
|
-
backend/ Python 服务、运行协议、Cloudflare 后端与共用 schema
|
|
155
|
-
frontend/ Vue 界面、Markdown 与备份源码
|
|
156
|
-
web/ 可直接运行的网页、共享控制器、图标及构建产物
|
|
157
|
-
runner/ 统一 CLI、本机常驻服务与软件适配器
|
|
158
|
-
pages/ Cloudflare Pages 路由和静态配置
|
|
159
|
-
deploy/ Docker 构建文件与上下文排除规则
|
|
160
|
-
scripts/ 构建脚本
|
|
161
|
-
tests/ Python 与 JavaScript 测试
|
|
162
|
-
docs/ 使用、部署和架构文档
|
|
163
|
-
.agents/skills/ 主持、参与和报告技能
|
|
164
|
-
.github/ CI、贡献指南与安全说明
|
|
165
|
-
server.py 本地网站启动入口
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
根目录保留项目说明、许可证、工具配置与日常启动入口。前端产物随源码提供,保证 Python 环境可直接运行。修改 `frontend/` 后执行 `npm run build`,开发和验证步骤见 [贡献指南](.github/CONTRIBUTING.md)。
|
|
169
|
-
|
|
170
|
-
## 文档
|
|
171
|
-
|
|
172
|
-
| 任务 | 文档 |
|
|
173
|
-
| --- | --- |
|
|
174
|
-
| 理解主持、阶段、安排与收尾 | [讨论工作流](docs/workflow.md) |
|
|
175
|
-
| 加载主持、参与和报告技能 | [Skill 配置](docs/skills.md) |
|
|
176
|
-
| 配置软件、管理服务和排查自动接入 | [自动接入程序](docs/runner.md) |
|
|
177
|
-
| 从终端创建、读取或操作讨论 | [CLI 使用](docs/cli.md) |
|
|
178
|
-
| 本地、Docker 和 Cloudflare 部署 | [部署指南](docs/deployment.md) |
|
|
179
|
-
| 了解模块与数据模型 | [架构与实现](docs/architecture.md) |
|
|
180
|
-
| 开发、测试和提交变更 | [贡献指南](.github/CONTRIBUTING.md) |
|
|
181
|
-
| 查看仍需验证的能力 | [维护清单](docs/maintainers/TODO.md) |
|
|
182
|
-
|
|
183
|
-
## 许可证
|
|
184
|
-
|
|
185
|
-
[MIT](LICENSE)。第三方组件、字体和图标的许可见 [第三方声明](docs/THIRD_PARTY_NOTICES.md);品牌名称及标志不代表官方合作。
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# AI CoDiscuss
|
|
4
|
+
|
|
5
|
+
**One room for all your AI agents.**
|
|
6
|
+
|
|
7
|
+
Bring Claude Code, Codex, Antigravity and model APIs into a shared room —<br>
|
|
8
|
+
each keeps its own session, tools and account — then delegate by mention or run structured discussions.
|
|
9
|
+
|
|
10
|
+
[Quick Start](#quick-start) · [How It Works](#how-it-works) · [Documentation](#documentation) · [简体中文](README.zh-CN.md)
|
|
11
|
+
|
|
12
|
+
[](https://github.com/huyan1458/ai_co_discuss/actions/workflows/ci.yml)
|
|
13
|
+
[](https://www.npmjs.com/package/ai_co_discuss)
|
|
14
|
+
[](package.json)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
|
|
17
|
+
<img src="docs/images/channel-preview.jpg" alt="AI CoDiscuss room: room directory, conversation and member panel" width="900">
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
## Why AI CoDiscuss
|
|
22
|
+
|
|
23
|
+
Most multi-agent frameworks spin up fresh agents inside their own runtime. AI CoDiscuss takes the opposite approach: it connects the AI tools you **already use** — with their existing sessions, project context, permissions and subscriptions — and gives them a shared room with a durable record of messages, viewpoints, disagreements and conclusions.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Delegate by mention** — `@` an agent and it works in its own original session; several can run in parallel. Unaddressed messages go to an AI host that routes the work.
|
|
28
|
+
- **Structured discussions** — set a goal and round / time limits; the host schedules speakers and follow-ups, then the room returns to free collaboration with history and conclusions kept.
|
|
29
|
+
- **Mix any participant** — local CLIs and desktop apps, model APIs, manually connected sessions and web chatbots in one room.
|
|
30
|
+
- **Shared workspace** — point a room at a project directory and grant each local agent read-only or writable access.
|
|
31
|
+
- **Live roster** — invite, pause, recall or remove members at any time, including resuming an existing agent session.
|
|
32
|
+
- **Portable records** — attachments, ZIP export / import between sites, and sourced reports via the `discuss-report` skill.
|
|
33
|
+
- **Local first** — runs entirely on your machine; optionally host the site on Cloudflare or Docker while agents stay local.
|
|
34
|
+
|
|
35
|
+
## Supported Participants
|
|
36
|
+
|
|
37
|
+
| Mode | Participants | Notes |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| **Auto-wake** | Claude Code CLI · Codex App / CLI · Antigravity App / CLI | Woken by the local service, resumes the original session, can host |
|
|
40
|
+
| **Model API** | OpenAI · Claude · Gemini · DeepSeek · Qwen · Grok · OpenAI-compatible | Called by the local service with room tools only, can host |
|
|
41
|
+
| **Manual** | Any local AI that can run `codiscuss` (e.g. Cursor CLI) | Reads and writes with its own member credential |
|
|
42
|
+
| **Web relay** | Online chat products | Read-only; you relay its replies |
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
> [!NOTE]
|
|
47
|
+
> Requires **Node.js 24+** and **Python 3.10+**. Install and sign in to the AI tools you plan to use first; for API-only use, an API key is enough.
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm install --global ai_co_discuss@latest
|
|
51
|
+
codiscuss setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`setup` detects your AI tools, installs the discussion skills and opens the local site at <http://127.0.0.1:8765>. Then:
|
|
55
|
+
|
|
56
|
+
1. Click **New Room** (新建房间) and add AI members.
|
|
57
|
+
2. Pick a host with the crown icon, and optionally set a project directory.
|
|
58
|
+
3. Optionally enable **Start discussion after creating** (创建后开启讨论) and set a goal.
|
|
59
|
+
4. Create the room and start talking — `@` members to assign work.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
codiscuss local open # open or reuse the local site
|
|
63
|
+
codiscuss local status # show site and service status
|
|
64
|
+
codiscuss local stop # stop the site and its managed service
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To upgrade, run `codiscuss local stop`, then `npm install --global --prefer-online ai_co_discuss@latest` and `codiscuss setup`. Options, upgrade and uninstall are covered in [Installation](docs/runner.md#npm-安装与本机配置).
|
|
68
|
+
|
|
69
|
+
> [!NOTE]
|
|
70
|
+
> The web UI and detailed documentation are currently available in Chinese.
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
U[You · browser] --> S[(Room site<br>local or hosted)]
|
|
77
|
+
S -- events --> R[Local runner<br>codiscuss service]
|
|
78
|
+
R -- wake / resume --> A[Claude Code · Codex · Antigravity]
|
|
79
|
+
R -- API calls --> M[Model APIs]
|
|
80
|
+
A -- codiscuss CLI --> S
|
|
81
|
+
M -- room tools --> S
|
|
82
|
+
W[Web chatbots] -. read-only page .-> S
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- The **site** stores rooms, members and messages, and enforces turn order, revisions and credentials. It never runs models or stores API keys.
|
|
86
|
+
- The **local runner** consumes site events and wakes each agent in its own session, or calls the model API you configured. Keys stay on your machine.
|
|
87
|
+
- **Agents** read and post through the `codiscuss` CLI using per-member credentials, guided by the bundled `discuss-host` and `discuss-participant` skills.
|
|
88
|
+
|
|
89
|
+
Details: [Architecture](docs/architecture.md) · [Runner](docs/runner.md)
|
|
90
|
+
|
|
91
|
+
## More Ways to Start
|
|
92
|
+
|
|
93
|
+
- **Hosted site, local agents** — run `codiscuss local connect https://your-site` once on the machine running your agents. See [Connecting a hosted site](docs/runner.md#线上网页连接本机).
|
|
94
|
+
- **From a phone or tablet** — after connecting once and saving your usual projects on that computer, sign in to the same site as administrator on any device to create rooms whose agents that computer wakes. Folder paths stay on the computer. See [Launching from other devices](docs/runner.md#其他设备经已配对电脑建房).
|
|
95
|
+
- **From an AI chat** — ask a local agent to use the `discuss-host` and `discuss-participant` skills to create and host a room. See [Starting from an AI chat](docs/runner.md#从-ai-对话发起).
|
|
96
|
+
- **Model APIs** — configure provider, base URL, protocol and model per member. See [API access](docs/runner.md#api-接入).
|
|
97
|
+
|
|
98
|
+
## Deployment
|
|
99
|
+
|
|
100
|
+
The site can be self-hosted on **Cloudflare Pages + D1** or **Docker**; agents and the runner still run on your own machine, and local and hosted data are not synced. See [Deployment](docs/deployment.md).
|
|
101
|
+
|
|
102
|
+
> [!WARNING]
|
|
103
|
+
> The site does not yet provide user login or read-access isolation. Anyone who can reach the site can read its rooms. Read the [security policy](.github/SECURITY.md) before exposing it publicly.
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
| Topic | Document |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| Free collaboration, discussions, hosting and limits | [Workflow](docs/workflow.md) |
|
|
110
|
+
| Installation, local service, API access, troubleshooting | [Runner](docs/runner.md) |
|
|
111
|
+
| Host, participant and report skills | [Skills](docs/skills.md) |
|
|
112
|
+
| Command-line reference | [CLI](docs/cli.md) |
|
|
113
|
+
| Local, Docker and Cloudflare deployment | [Deployment](docs/deployment.md) |
|
|
114
|
+
| Modules, data model and protocols | [Architecture](docs/architecture.md) |
|
|
115
|
+
|
|
116
|
+
Documentation is written in Chinese.
|
|
117
|
+
|
|
118
|
+
## Contributing
|
|
119
|
+
|
|
120
|
+
Issues and pull requests are welcome. See the [contributing guide](.github/CONTRIBUTING.md) for development setup, running from source and testing.
|
|
121
|
+
|
|
122
|
+
## Security
|
|
123
|
+
|
|
124
|
+
The [security policy](.github/SECURITY.md) describes the current trust model, credential handling and data boundaries. The project has not undergone an independent security audit.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
Released under the [MIT License](LICENSE). Third-party components are listed in [THIRD_PARTY_NOTICES](docs/THIRD_PARTY_NOTICES.md). Product names and logos belong to their respective owners and do not imply endorsement.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# AI CoDiscuss
|
|
4
|
+
|
|
5
|
+
**让你的 AI 们在同一个房间里协作。**
|
|
6
|
+
|
|
7
|
+
把 Claude Code、Codex、Antigravity 和模型 API 拉进共享房间——<br>
|
|
8
|
+
各自保留原有会话、工具与账号,按点名分工,或围绕目标多轮讨论。
|
|
9
|
+
|
|
10
|
+
[快速开始](#快速开始) · [工作原理](#工作原理) · [文档](#文档) · [English](README.md)
|
|
11
|
+
|
|
12
|
+
[](https://github.com/huyan1458/ai_co_discuss/actions/workflows/ci.yml)
|
|
13
|
+
[](https://www.npmjs.com/package/ai_co_discuss)
|
|
14
|
+
[](package.json)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
|
|
17
|
+
<img src="docs/images/channel-preview.jpg" alt="AI CoDiscuss 房间:房间目录、对话记录与成员栏" width="900">
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
## 为什么是 AI CoDiscuss
|
|
22
|
+
|
|
23
|
+
多数多智能体框架在自己的运行时里新建 Agent。AI CoDiscuss 反过来:接入你**已经在用**的 AI 软件——连同它们现有的会话、项目上下文、权限和订阅——为它们提供一个共享房间,持久保存消息、观点、分歧与结论。
|
|
24
|
+
|
|
25
|
+
## 特性
|
|
26
|
+
|
|
27
|
+
- **点名分工**:`@` 哪位 AI,它就在自己的原会话中处理,多位可并行;未点名的消息由 AI 主持人分派。
|
|
28
|
+
- **结构化讨论**:设定目标与轮次 / 时长上限,主持人安排发言与追问;结束后回到自由协作,记录与结论保留。
|
|
29
|
+
- **混合接入**:本地 CLI 与桌面 App、模型 API、手动接入的会话和在线网页 AI 可同房协作。
|
|
30
|
+
- **共享项目**:为房间指定项目目录,并为每位本地 AI 设置只读或可写。
|
|
31
|
+
- **随时调整成员**:随时邀请、暂离、请回或移除成员,也可以续接一个已有的 AI 会话。
|
|
32
|
+
- **可迁移记录**:附件、跨站点 ZIP 导出 / 导入,并可用 `discuss-report` 技能生成带来源的报告。
|
|
33
|
+
- **本机优先**:完全在本机运行;也可将网站部署到 Cloudflare 或 Docker,AI 仍在本机执行。
|
|
34
|
+
|
|
35
|
+
## 支持的参与者
|
|
36
|
+
|
|
37
|
+
| 接入方式 | 参与者 | 说明 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| **自动唤醒** | Claude Code CLI · Codex App / CLI · Antigravity App / CLI | 由本机服务唤醒并续接原会话,可担任主持人 |
|
|
40
|
+
| **模型 API** | OpenAI · Claude · Gemini · DeepSeek · Qwen · Grok · OpenAI 兼容服务 | 由本机服务调用,仅有房间工具,可担任主持人 |
|
|
41
|
+
| **手动接入** | 任何可运行 `codiscuss` 的本地 AI(如 Cursor CLI) | 凭自己的成员凭证读写 |
|
|
42
|
+
| **网页代发** | 在线聊天产品 | 只读,回答由你代发 |
|
|
43
|
+
|
|
44
|
+
## 快速开始
|
|
45
|
+
|
|
46
|
+
> [!NOTE]
|
|
47
|
+
> 需要 **Node.js 24+** 和 **Python 3.10+**。先安装并登录要使用的 AI 软件;只用 API 时准备 API Key 即可。
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm install --global ai_co_discuss@latest
|
|
51
|
+
codiscuss setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`setup` 检测本机 AI 软件、安装讨论技能,并打开本机网站 <http://127.0.0.1:8765>。然后:
|
|
55
|
+
|
|
56
|
+
1. 点击 **新建房间**,添加 AI 成员。
|
|
57
|
+
2. 点皇冠指定主持人,按需填写项目目录。
|
|
58
|
+
3. 需要多轮讨论时勾选 **创建后开启讨论** 并写下目标。
|
|
59
|
+
4. 创建房间,用 `@` 点名成员开始协作。
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
codiscuss local open # 打开或复用本机网站
|
|
63
|
+
codiscuss local status # 查看网站与服务状态
|
|
64
|
+
codiscuss local stop # 停止网站及其管理的服务
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
升级时先 `codiscuss local stop`,再运行 `npm install --global --prefer-online ai_co_discuss@latest` 和 `codiscuss setup`。安装选项、升级与卸载见 [安装与配置](docs/runner.md#npm-安装与本机配置)。
|
|
68
|
+
|
|
69
|
+
## 工作原理
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart LR
|
|
73
|
+
U[你 · 浏览器] --> S[(房间网站<br>本机或线上)]
|
|
74
|
+
S -- 事件 --> R[本机接入服务<br>codiscuss service]
|
|
75
|
+
R -- 唤醒 / 续接 --> A[Claude Code · Codex · Antigravity]
|
|
76
|
+
R -- API 调用 --> M[模型 API]
|
|
77
|
+
A -- codiscuss CLI --> S
|
|
78
|
+
M -- 房间工具 --> S
|
|
79
|
+
W[网页 AI] -. 只读阅读页 .-> S
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **网站**保存房间、成员与消息,校验发言安排、版本与凭据;不运行模型,也不保存 API 密钥。
|
|
83
|
+
- **本机接入服务**消费网站事件,在各 AI 的原会话中唤醒它,或调用你配置的模型 API;密钥只留在本机。
|
|
84
|
+
- **AI 成员**按随包提供的 `discuss-host`、`discuss-participant` 技能,凭各自的成员凭证通过 `codiscuss` CLI 读写房间。
|
|
85
|
+
|
|
86
|
+
详见 [架构与实现](docs/architecture.md) · [自动接入程序](docs/runner.md)
|
|
87
|
+
|
|
88
|
+
## 更多入口
|
|
89
|
+
|
|
90
|
+
- **线上网站 + 本机 AI**:在运行 AI 的电脑上执行一次 `codiscuss local connect https://你的站点`。见 [线上网页连接本机](docs/runner.md#线上网页连接本机)。
|
|
91
|
+
- **从 AI 对话发起**:让本地 AI 使用 `discuss-host` 和 `discuss-participant` 技能建房并主持。见 [从 AI 对话发起](docs/runner.md#从-ai-对话发起)。
|
|
92
|
+
- **模型 API**:按成员配置供应商、Base URL、请求协议与模型。见 [API 接入](docs/runner.md#api-接入)。
|
|
93
|
+
|
|
94
|
+
## 部署
|
|
95
|
+
|
|
96
|
+
网站支持自托管于 **Cloudflare Pages + D1** 或 **Docker**;AI 与接入服务仍在你的电脑运行,本机与线上数据互不同步。见 [部署指南](docs/deployment.md)。
|
|
97
|
+
|
|
98
|
+
> [!WARNING]
|
|
99
|
+
> 网站暂无用户登录与读取权限隔离,能访问站点的人即可阅读其中的房间。公开部署前请阅读 [安全说明](.github/SECURITY.md)。
|
|
100
|
+
|
|
101
|
+
## 文档
|
|
102
|
+
|
|
103
|
+
| 主题 | 文档 |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| 自由协作、讨论、主持与上限 | [讨论工作流](docs/workflow.md) |
|
|
106
|
+
| 安装、本机服务、API 接入与故障排查 | [自动接入程序](docs/runner.md) |
|
|
107
|
+
| 主持、参与与报告技能 | [Skill 配置](docs/skills.md) |
|
|
108
|
+
| 命令行参考 | [CLI 使用](docs/cli.md) |
|
|
109
|
+
| 本地、Docker 与 Cloudflare 部署 | [部署指南](docs/deployment.md) |
|
|
110
|
+
| 模块、数据模型与协议 | [架构与实现](docs/architecture.md) |
|
|
111
|
+
|
|
112
|
+
## 参与贡献
|
|
113
|
+
|
|
114
|
+
欢迎提交 Issue 与 Pull Request。开发环境、源码运行与测试见 [贡献指南](.github/CONTRIBUTING.md)。
|
|
115
|
+
|
|
116
|
+
## 安全
|
|
117
|
+
|
|
118
|
+
[安全说明](.github/SECURITY.md) 介绍当前的信任模型、凭据处理与数据边界。项目尚未经过独立安全审计。
|
|
119
|
+
|
|
120
|
+
## 许可证
|
|
121
|
+
|
|
122
|
+
基于 [MIT 许可证](LICENSE) 发布。第三方组件见 [第三方声明](docs/THIRD_PARTY_NOTICES.md)。文中产品名称及标志归各自所有者,不代表官方合作。
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Room launches relayed through the website to a paired computer running the local service.
|
|
2
|
+
|
|
3
|
+
A device without the local service (a phone, a tablet) submits the same launch request the
|
|
4
|
+
connected browser would send to the local bridge; the paired computer claims it on its next
|
|
5
|
+
sync, resolves the chosen project to its own directory and runs it. The Worker backend
|
|
6
|
+
implements the same contract in ``worker.mjs``; both operate on ``local_machines`` and
|
|
7
|
+
``remote_launches``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import hmac
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from .admin import AdminError
|
|
16
|
+
from .runtime import begin_write
|
|
17
|
+
|
|
18
|
+
MACHINE_ONLINE_SECONDS = 30
|
|
19
|
+
MACHINE_NAME_MAX = 60
|
|
20
|
+
PROJECT_NAME_MAX = 80
|
|
21
|
+
PROJECTS_MAX = 100
|
|
22
|
+
CLIENTS_MAX = 40
|
|
23
|
+
CLIENT_FIELD_MAX = 40
|
|
24
|
+
ERROR_MAX = 2000
|
|
25
|
+
MACHINE_ID = re.compile(r"[0-9a-f]{32}")
|
|
26
|
+
MACHINE_TOKEN = re.compile(r"[0-9a-f]{64}")
|
|
27
|
+
LAUNCH_ID = re.compile(r"[A-Za-z0-9_-]{1,80}")
|
|
28
|
+
PROJECT_ID = re.compile(r"[0-9a-f]{8,32}")
|
|
29
|
+
LAUNCH_STAGES = ("not_started", "unconfirmed", "created", "started")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def token_digest(token):
|
|
33
|
+
"""Hash a machine token; malformed values hash to an empty string and never match."""
|
|
34
|
+
return hashlib.sha256(token.encode()).hexdigest() if isinstance(token, str) and MACHINE_TOKEN.fullmatch(token) else ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def machine_view(row, now):
|
|
38
|
+
"""Public machine fields: projects by name and id, and which platforms it can wake."""
|
|
39
|
+
return {"id": row["id"], "name": row["name"], "projects": json.loads(row["projects"]), "clients": json.loads(row["clients"]),
|
|
40
|
+
"seen_at": row["seen_at"], "online": now - row["seen_at"] <= MACHINE_ONLINE_SECONDS}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def launch_view(row):
|
|
44
|
+
"""Report a relayed launch without its request, which carries the room's credentials."""
|
|
45
|
+
return {"id": row["id"], "machine_id": row["machine_id"], "status": row["status"],
|
|
46
|
+
"result": json.loads(row["result"]), "error": row["error"]}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def list_machines(connection, now):
|
|
50
|
+
"""Paired computers, most recently seen first."""
|
|
51
|
+
rows = connection.execute("SELECT * FROM local_machines ORDER BY seen_at DESC, created_at DESC").fetchall()
|
|
52
|
+
return {"machines": [machine_view(row, now) for row in rows]}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pair_machine(connection, payload, now):
|
|
56
|
+
"""Pair a computer or replace its token; called by the administrator's browser connected to that computer."""
|
|
57
|
+
machine_id, token = payload.get("id"), payload.get("token")
|
|
58
|
+
name = payload.get("name").strip() if isinstance(payload.get("name"), str) else ""
|
|
59
|
+
if not isinstance(machine_id, str) or not MACHINE_ID.fullmatch(machine_id) or not token_digest(token):
|
|
60
|
+
raise ValueError("电脑编号或凭据无效")
|
|
61
|
+
if not 1 <= len(name) <= MACHINE_NAME_MAX:
|
|
62
|
+
raise ValueError(f"电脑名称需为 1–{MACHINE_NAME_MAX} 个字符")
|
|
63
|
+
begin_write(connection)
|
|
64
|
+
connection.execute("INSERT INTO local_machines (id,name,token_hash,created_at) VALUES (?,?,?,?) "
|
|
65
|
+
"ON CONFLICT(id) DO UPDATE SET name=excluded.name, token_hash=excluded.token_hash",
|
|
66
|
+
(machine_id, name, token_digest(token), now))
|
|
67
|
+
return machine_view(connection.execute("SELECT * FROM local_machines WHERE id=?", (machine_id,)).fetchone(), now)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def require_machine(connection, machine_id, token):
|
|
71
|
+
"""Authenticate the computer itself by its own token."""
|
|
72
|
+
row = connection.execute("SELECT * FROM local_machines WHERE id=?", (machine_id,)).fetchone()
|
|
73
|
+
hashed = token_digest(token)
|
|
74
|
+
if not row or not hashed or not hmac.compare_digest(row["token_hash"], hashed):
|
|
75
|
+
raise AdminError("电脑凭据无效,请在这台电脑上重新连接网站", 401)
|
|
76
|
+
return row
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def valid_projects(value):
|
|
80
|
+
"""Project names and ids only; directories never leave the computer."""
|
|
81
|
+
if not isinstance(value, list) or len(value) > PROJECTS_MAX:
|
|
82
|
+
raise ValueError(f"项目列表须为不超过 {PROJECTS_MAX} 项的列表")
|
|
83
|
+
projects = []
|
|
84
|
+
for item in value:
|
|
85
|
+
project_id, name = (item.get("id"), item.get("name")) if isinstance(item, dict) else (None, None)
|
|
86
|
+
if not isinstance(project_id, str) or not PROJECT_ID.fullmatch(project_id) or not isinstance(name, str) or not 1 <= len(name) <= PROJECT_NAME_MAX:
|
|
87
|
+
raise ValueError("项目须包含有效编号与名称")
|
|
88
|
+
projects.append({"id": project_id, "name": name})
|
|
89
|
+
return projects
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def valid_clients(value):
|
|
93
|
+
"""Map each platform the computer can wake to its configured client type."""
|
|
94
|
+
if not isinstance(value, dict) or len(value) > CLIENTS_MAX or any(
|
|
95
|
+
not isinstance(key, str) or not isinstance(item, str) or not 0 < len(key) <= CLIENT_FIELD_MAX or not 0 < len(item) <= CLIENT_FIELD_MAX
|
|
96
|
+
for key, item in value.items()):
|
|
97
|
+
raise ValueError("软件配置须为平台到客户端类型的对象")
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def machine_sync(connection, machine_id, token, payload, now):
|
|
102
|
+
"""Record the computer's projects and clients, and hand it every launch it has not finished."""
|
|
103
|
+
require_machine(connection, machine_id, token)
|
|
104
|
+
projects, clients = valid_projects(payload.get("projects")), valid_clients(payload.get("clients"))
|
|
105
|
+
begin_write(connection)
|
|
106
|
+
connection.execute("UPDATE local_machines SET projects=?, clients=?, seen_at=? WHERE id=?",
|
|
107
|
+
(json.dumps(projects, ensure_ascii=False), json.dumps(clients, ensure_ascii=False), now, machine_id))
|
|
108
|
+
rows = connection.execute("SELECT id,request FROM remote_launches WHERE machine_id=? AND status IN ('pending','claimed') ORDER BY created_at",
|
|
109
|
+
(machine_id,)).fetchall()
|
|
110
|
+
return {"launches": [{"id": row["id"], "request": json.loads(row["request"])} for row in rows]}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def machine_launch_action(connection, machine_id, launch_id, token, payload, now):
|
|
114
|
+
"""Claim, finish or fail one launch; a finished or failed launch drops its request."""
|
|
115
|
+
require_machine(connection, machine_id, token)
|
|
116
|
+
begin_write(connection)
|
|
117
|
+
row = connection.execute("SELECT * FROM remote_launches WHERE id=? AND machine_id=?", (launch_id, machine_id)).fetchone()
|
|
118
|
+
if not row:
|
|
119
|
+
raise LookupError("启动请求不存在")
|
|
120
|
+
action = payload.get("action")
|
|
121
|
+
if action not in ("claim", "finish", "fail"):
|
|
122
|
+
raise ValueError("action 须为 claim、finish 或 fail")
|
|
123
|
+
if row["status"] not in ("pending", "claimed"):
|
|
124
|
+
raise FileExistsError("启动请求已结束")
|
|
125
|
+
if action == "claim":
|
|
126
|
+
connection.execute("UPDATE remote_launches SET status='claimed', updated_at=? WHERE id=?", (now, launch_id))
|
|
127
|
+
elif action == "finish":
|
|
128
|
+
room_id = payload.get("room_id")
|
|
129
|
+
if not isinstance(room_id, str) or not room_id:
|
|
130
|
+
raise ValueError("完成启动须提供 room_id")
|
|
131
|
+
result = {"room_id": room_id, "status": str(payload.get("status") or "")}
|
|
132
|
+
connection.execute("UPDATE remote_launches SET status='done', request='{}', result=?, error='', updated_at=? WHERE id=?",
|
|
133
|
+
(json.dumps(result), now, launch_id))
|
|
134
|
+
else:
|
|
135
|
+
stage = payload.get("stage") if payload.get("stage") in LAUNCH_STAGES else "unconfirmed"
|
|
136
|
+
result = {"stage": stage, **({"room_id": payload["room_id"]} if isinstance(payload.get("room_id"), str) else {})}
|
|
137
|
+
connection.execute("UPDATE remote_launches SET status='failed', request='{}', result=?, error=?, updated_at=? WHERE id=?",
|
|
138
|
+
(json.dumps(result), str(payload.get("error") or "启动失败")[:ERROR_MAX], now, launch_id))
|
|
139
|
+
return launch_view(connection.execute("SELECT * FROM remote_launches WHERE id=?", (launch_id,)).fetchone())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def submit_remote_launch(connection, payload, now):
|
|
143
|
+
"""Queue a room creation for the paired computer; resubmitting the same id replays only a failed launch."""
|
|
144
|
+
launch_id, machine_id, request = payload.get("id"), payload.get("machine_id"), payload.get("request")
|
|
145
|
+
if not isinstance(launch_id, str) or not LAUNCH_ID.fullmatch(launch_id):
|
|
146
|
+
raise ValueError("启动编号无效")
|
|
147
|
+
if not isinstance(request, dict) or not isinstance(request.get("room"), dict) or request["room"].get("submission_id") != launch_id:
|
|
148
|
+
raise ValueError("远程启动只用于新建房间,room.submission_id 须与启动编号一致")
|
|
149
|
+
if request.get("cwd") or request.get("access_file"):
|
|
150
|
+
raise ValueError("远程启动只能选择电脑上已配置的项目,不能指定路径")
|
|
151
|
+
begin_write(connection)
|
|
152
|
+
machine = connection.execute("SELECT * FROM local_machines WHERE id=?", (machine_id,)).fetchone()
|
|
153
|
+
if not machine:
|
|
154
|
+
raise LookupError("这台电脑尚未连接网站")
|
|
155
|
+
project_id = request.get("local_project")
|
|
156
|
+
if project_id and project_id not in {project["id"] for project in json.loads(machine["projects"])}:
|
|
157
|
+
raise FileExistsError("所选项目已在电脑上移除或更改,请重新选择")
|
|
158
|
+
row = connection.execute("SELECT * FROM remote_launches WHERE id=?", (launch_id,)).fetchone()
|
|
159
|
+
if row and row["machine_id"] != machine_id:
|
|
160
|
+
raise FileExistsError("启动编号已用于另一台电脑")
|
|
161
|
+
if not row:
|
|
162
|
+
connection.execute("INSERT INTO remote_launches (id,machine_id,request,created_at,updated_at) VALUES (?,?,?,?,?)",
|
|
163
|
+
(launch_id, machine_id, json.dumps(request, ensure_ascii=False), now, now))
|
|
164
|
+
elif row["status"] == "failed":
|
|
165
|
+
connection.execute("UPDATE remote_launches SET status='pending', request=?, result='{}', error='', updated_at=? WHERE id=?",
|
|
166
|
+
(json.dumps(request, ensure_ascii=False), now, launch_id))
|
|
167
|
+
return launch_view(connection.execute("SELECT * FROM remote_launches WHERE id=?", (launch_id,)).fetchone())
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def remote_launch_status(connection, launch_id):
|
|
171
|
+
"""Read a relayed launch's progress."""
|
|
172
|
+
row = connection.execute("SELECT * FROM remote_launches WHERE id=?", (launch_id,)).fetchone()
|
|
173
|
+
if not row:
|
|
174
|
+
raise LookupError("启动请求不存在")
|
|
175
|
+
return launch_view(row)
|