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.
- package/.agents/skills/discuss-host/SKILL.md +3 -2
- package/.agents/skills/discuss-participant/SKILL.md +1 -1
- package/README.md +13 -9
- package/backend/free_room.py +64 -0
- package/backend/identity.py +31 -31
- package/backend/runtime.py +364 -364
- package/backend/schema.sql +599 -580
- package/backend/server.py +1782 -1760
- package/docs/architecture.md +8 -4
- package/docs/cli.md +2 -0
- package/docs/runner.md +37 -4
- package/docs/workflow.md +148 -148
- package/package.json +2 -2
- package/runner/api_participant.mjs +186 -0
- package/runner/discuss.py +946 -946
- package/runner/discussion_adapters.mjs +6 -0
- package/runner/discussion_api.mjs +146 -0
- package/runner/discussion_launch.mjs +728 -664
- package/runner/discussion_runner.mjs +36 -7
- package/runner/discussion_service.mjs +395 -394
- package/runner/free_runner.mjs +6 -2
- package/web/app.js +2913 -2859
- package/web/backup.js +1 -1
- package/web/index.html +1 -1
- package/web/markdown.js +1 -1
- package/web/modern.css +1 -1
- package/web/modern.js +7362 -5077
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/** Durable website-event delivery to
|
|
1
|
+
/** Durable website-event delivery to AI software and explicitly configured API participants. */
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -7,13 +8,22 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
7
8
|
import { parseArgs } from 'node:util';
|
|
8
9
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
10
|
import { atomic_json, deliver_task, remove_agy_projects, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
|
|
11
|
+
import { read_api_key } from './discussion_api.mjs';
|
|
10
12
|
|
|
11
13
|
const POLL_MS = 2000;
|
|
12
14
|
/** Headless CLIs know their session at start, so the runner enters the execution instead of spending model turns on it. */
|
|
15
|
+
let rg_available;
|
|
16
|
+
/** Windows PowerShell mis-decodes UTF-8 files, so point the AI at rg only when it is installed. */
|
|
17
|
+
function windows_rg() {
|
|
18
|
+
if (process.platform !== 'win32') return false;
|
|
19
|
+
rg_available ??= spawnSync('rg', ['--version'], { stdio: 'ignore', windowsHide: true }).status === 0;
|
|
20
|
+
return rg_available;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
const AUTO_ENTER_ADAPTERS = new Set(['codex_cli', 'claude', 'antigravity_cli']);
|
|
14
24
|
const DELIVERY_RETRY_DELAYS_MS = [2000, 4000];
|
|
15
25
|
const RENEW_MS = 30_000;
|
|
16
|
-
const ADAPTER_NAMES = ['codex', 'claude', 'antigravity', 'codex_cli', 'antigravity_cli'];
|
|
26
|
+
const ADAPTER_NAMES = ['codex', 'claude', 'antigravity', 'codex_cli', 'antigravity_cli', 'api'];
|
|
17
27
|
export const CODEX_ADAPTERS = ['codex', 'codex_cli'];
|
|
18
28
|
const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
29
|
/** Project files in cwd are read-only unless the launch or participant explicitly grants write. */
|
|
@@ -40,6 +50,12 @@ export function validate_config(input, base_directory) {
|
|
|
40
50
|
if (!config.project || !existsSync(join(config.project, 'runner', 'discuss.py'))) throw new Error('cli_project 必须指向 AI CoDiscuss 项目;省略时默认使用本仓库');
|
|
41
51
|
if (!config.participants || !Object.keys(config.participants).length) throw new Error('请配置本机参与软件');
|
|
42
52
|
for (const participant of Object.values(config.participants)) {
|
|
53
|
+
if (participant.adapter === 'api') {
|
|
54
|
+
read_api_key(config.api_home, participant);
|
|
55
|
+
for (const field of ['cwd', 'project_name', 'project_id', 'executable', 'effort']) delete participant[field];
|
|
56
|
+
participant.project_access = 'read';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
43
59
|
participant.cwd = participant.cwd ?? config.cwd;
|
|
44
60
|
participant.project_name = participant.project_name ?? config.discussion_project;
|
|
45
61
|
participant.project_access = participant.project_access || config.project_access || 'read';
|
|
@@ -121,7 +137,9 @@ export class DiscussionStore {
|
|
|
121
137
|
&& CODEX_ADAPTERS.includes(next[1]) && session && next[6] === session && (!item[6] || item[6] === session)
|
|
122
138
|
&& JSON.stringify(item.slice(2, 6)) === JSON.stringify(next.slice(2, 6));
|
|
123
139
|
});
|
|
124
|
-
|
|
140
|
+
// Experts invited during a run only add entries; every existing binding must stay as it was.
|
|
141
|
+
const added = before.every(item => entries.some(next => JSON.stringify(next) === JSON.stringify(item)));
|
|
142
|
+
if (!allowed && !added) throw new Error('已绑定会话的项目、cwd 或身份配置发生变化;请暂停并使用独立运行目录重新接入');
|
|
125
143
|
}
|
|
126
144
|
this.db.prepare("INSERT INTO metadata VALUES ('context',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(context);
|
|
127
145
|
}
|
|
@@ -180,8 +198,10 @@ export function task_files(config, event, token, binding) {
|
|
|
180
198
|
draft_directory,
|
|
181
199
|
role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS, access_file: access_path,
|
|
182
200
|
instructions: `读取指定技能;网站命令只用 ${cli} 的 subcommands 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口;${auto_enter ? '接入程序已进入本次执行,不要再执行 execution enter。' : `使用 ${cli} execution enter 房间 执行编号 --access-file 凭据路径 进入。进入成功后才处理本次安排;`}凭据文件由 CLI 内部读取,不要打印或读取其 token。每次业务写入都带 --access-file。答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;${project_rule}只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。` });
|
|
183
|
-
const
|
|
184
|
-
const
|
|
201
|
+
const read_rule = windows_rg() ? '读取含中文的技能或文本文件时用 rg --encoding utf-8 . 文件路径,不用 Get-Content(Windows PowerShell 会按系统编码读出乱码,需要反复重试)。' : '';
|
|
202
|
+
const scope = `本次只放行任务内 subcommands 列出的 ${cli} 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口。任务内 cwd 是讨论项目目录;${project_rule}项目读写仍受软件实际权限和议题授权约束。不读取凭据内容。${read_rule}`;
|
|
203
|
+
const plan_rule = event.role === 'host' ? '安排专家回应时先写发言计划,用 turn create --plan-file 一次排满本轮每位专家,不要每次只排一位。' : '';
|
|
204
|
+
const intro = `用户已启动此房间的全自动讨论。请以 ${event.participant} 的${event.role === 'host' ? '主持' : '专家'}职责处理本次工作。${plan_rule}`;
|
|
185
205
|
const prompt = auto_enter
|
|
186
206
|
? `${intro}接入程序已为本次执行完成 execution enter,不要再执行。任务内容如下(同一份保存在 ${task_path},无需再读):
|
|
187
207
|
${readFileSync(task_path, 'utf8')}
|
|
@@ -202,8 +222,9 @@ export class DiscussionRunner {
|
|
|
202
222
|
this.seen = new Set();
|
|
203
223
|
this.blocked = new Set();
|
|
204
224
|
}
|
|
205
|
-
/** Append one timestamped stage to the event's trace file; tracing must never affect delivery. */
|
|
225
|
+
/** Append one timestamped stage to the event's trace file when CODISCUSS_TRACE=1; tracing must never affect delivery. */
|
|
206
226
|
trace(id, stage, detail = {}) {
|
|
227
|
+
if (process.env.CODISCUSS_TRACE !== '1') return;
|
|
207
228
|
try { appendFileSync(join(this.config.directory, `event-${id}.trace.jsonl`), JSON.stringify({ t: new Date().toISOString(), stage, ...detail }) + '\n', { mode: 0o600 }); }
|
|
208
229
|
catch { /* Diagnostics only. */ }
|
|
209
230
|
}
|
|
@@ -230,6 +251,8 @@ export class DiscussionRunner {
|
|
|
230
251
|
const task = task_files(this.config, event, row.token, binding);
|
|
231
252
|
invoked = true;
|
|
232
253
|
const result = await this.deliver(adapter, binding, { ...task, room_title: this.room_title,
|
|
254
|
+
api_context: { site: this.config.site, room: this.config.room, token: row.token, home: this.config.api_home,
|
|
255
|
+
event_id: row.id, role: event.role, turn_id: event.turn_id },
|
|
233
256
|
on_trace: (stage, detail) => this.trace(row.id, stage, detail),
|
|
234
257
|
on_binding: session => {
|
|
235
258
|
this.store.bind(event.participant, session);
|
|
@@ -303,7 +326,13 @@ export class DiscussionRunner {
|
|
|
303
326
|
}
|
|
304
327
|
/** Preserve the cause on the website and revoke work before asking for intervention. */
|
|
305
328
|
async fail(row, reason) {
|
|
306
|
-
await api(this.config, `/events/${row.id}`, { action: 'fail', reason });
|
|
329
|
+
try { await api(this.config, `/events/${row.id}`, { action: 'fail', reason }); }
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!/HTTP 409/.test(error.message)) throw error;
|
|
332
|
+
const latest = await api(this.config, `/events/${row.id}`, { action: 'view' });
|
|
333
|
+
if (['completed', 'cancelled', 'failed'].includes(latest.status)) return this.reconcile(row, latest);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
307
336
|
this.store.record(row.id, { status: 'failed', error: reason });
|
|
308
337
|
this.stop_children();
|
|
309
338
|
}
|