ai_co_discuss 0.1.2 → 0.1.4
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 +14 -2
- package/docs/runner.md +7 -3
- package/package.json +1 -1
- package/runner/discussion_adapters.mjs +4 -2
- package/runner/discussion_launch.mjs +10 -2
- package/runner/discussion_local.mjs +35 -6
- package/runner/discussion_runner.mjs +18 -5
- package/runner/discussion_setup.mjs +1 -1
- package/web/app.js +17 -2
- package/web/modern.css +1 -1
- package/web/modern.js +8373 -8242
package/README.md
CHANGED
|
@@ -29,9 +29,21 @@ npx --yes --package ai_co_discuss codiscuss install
|
|
|
29
29
|
|
|
30
30
|
安装摘要为“配置待处理”时,处理列出的路径、旧配置或技能冲突后重跑 `codiscuss setup`;就绪后直接进入[发起第一场讨论](#发起第一场讨论)。已有配置和自定义技能会保留,数据存放在包外,升级或卸载包不会删除。安装就绪不代表真实 AI 已执行过任务。详细选项见 [npm 安装与本机配置](docs/runner.md#npm-安装与本机配置)。
|
|
31
31
|
|
|
32
|
+
#### 升级与卸载
|
|
33
|
+
|
|
34
|
+
已安装过时不要再用 `npx` 升级:npx 会直接运行已安装的旧版本。先停止本机网站,再强制从线上取最新版并重新配置:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
codiscuss local stop
|
|
38
|
+
npm install --global --prefer-online ai_co_discuss@latest
|
|
39
|
+
codiscuss setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--prefer-online` 避免 npm 用本地缓存把 `latest` 解析成旧版本;完成后可用 `npm ls -g ai_co_discuss` 核对版本。卸载时先 `codiscuss local stop`,开启过登录自启的再运行 `codiscuss service autostart off`,最后 `npm uninstall --global ai_co_discuss`。讨论数据、软件配置和已复制的技能不会被删除,位置见 [卸载说明](docs/runner.md#npm-安装与本机配置)。
|
|
43
|
+
|
|
32
44
|
### 源码运行方式
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
供开发者使用;已完成包安装可直接跳到下一节。需要 Git、Python 3.10+,使用自动接入还需 Node.js 24+ 和 npm。仓库包含构建好的网页。
|
|
35
47
|
|
|
36
48
|
```sh
|
|
37
49
|
git clone https://github.com/huyan1458/ai_co_discuss.git
|
|
@@ -79,7 +91,7 @@ codiscuss local connect https://你的站点
|
|
|
79
91
|
|
|
80
92
|
> 请使用 discuss-host 和 discuss-participant,在 http://127.0.0.1:8765 新建一场全自动讨论,议题是“议题与目标”,我的问题和背景是“用户原文”。由当前 Codex 会话主持,参与者为 Codex 和 Claude,最多 3 轮、60 分钟,到限暂停;沿用本机软件配置。
|
|
81
93
|
|
|
82
|
-
将地址、议题和参与者替换为实际内容。AI 主持人使用已保存的软件配置完成建房与启动,**当前实际会话继续主持**;后续安排由本机服务交付,可在网页查看同一场讨论。无需用户手写启动 JSON。当前会话的 CLI / App
|
|
94
|
+
将地址、议题和参与者替换为实际内容。AI 主持人使用已保存的软件配置完成建房与启动,**当前实际会话继续主持**;后续安排由本机服务交付,可在网页查看同一场讨论。无需用户手写启动 JSON。当前会话的 CLI / App 类型必须与配置对应;agy 不能从自己的对话主持全自动讨论(只在讨论服务新建的会话中运行),请改用其他软件主持或从网页发起。其他接入方式与配置细节见 [两个入口的启动配置](docs/runner.md#两种讨论入口与启动配置)。
|
|
83
95
|
|
|
84
96
|
#### 项目与日常使用
|
|
85
97
|
|
package/docs/runner.md
CHANGED
|
@@ -34,10 +34,12 @@ codiscuss local stop
|
|
|
34
34
|
|
|
35
35
|
安装实现保持固定软件表和本地文件复制。路径查找参考 [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) 的整目录复制与同名保护思路,保留本项目的内容哈希更新判断。不引入这些项目作为依赖,也不增加在线技能下载、插件注册或后台更新。
|
|
36
36
|
|
|
37
|
-
默认 `setup` 最后执行 `local open`:后台启动 Python 回环网站(`--local-launch --no-demo`)与讨论服务,再打开浏览器;首次为空库。网站地址默认为 `http://127.0.0.1:8765`。`local start` 具有相同启动行为但不打开浏览器,重复启动复用原进程;真正绑定成功并通过健康检查后才返回就绪。端口占用会失败,不接管或停止占用者。网站管理进程脱离启动它的终端在后台运行,关闭终端不会停止网站或讨论服务;本机网站不设登录自启,重启电脑后运行 `local open`
|
|
37
|
+
默认 `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`,再重新启动。
|
|
38
38
|
|
|
39
39
|
网站数据库为服务目录下 `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`。
|
|
40
40
|
|
|
41
|
+
已安装后升级不要使用 `npx`(它会运行已安装的旧版本):先 `codiscuss local stop`,再 `npm install --global --prefer-online ai_co_discuss@latest`,然后重跑 `codiscuss setup`;`--prefer-online` 避免本地缓存把 `latest` 解析为旧版本。
|
|
42
|
+
|
|
41
43
|
卸载先 `codiscuss local stop`,按需要另用 `codiscuss service stop` 停止预先独立运行的服务,再执行 `npm uninstall --global ai_co_discuss`;自定义 prefix 时带回相同 `--prefix`。安装器不删除数据库、私有配置或已复制技能。
|
|
42
44
|
|
|
43
45
|
维护者从源码生成安装包时先完成相关测试与网页构建,再 `npm pack --pack-destination <已存在的输出目录>`。包采用运行文件白名单,不含数据库、凭据、`.dev`、前端源码或 node_modules;新增运行文件需同步清单。`npm run test:install` 在临时 prefix/home 中验证真实 npx 安装、命令、技能和本机启停,不触发真实 AI。Windows 已通过隔离测试;macOS/Linux 启动与默认浏览器仍需实机核验。
|
|
@@ -204,11 +206,13 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
|
|
|
204
206
|
| `claude` | 现有 `claude -p --output-format json`;后续 `--resume ID` | 沿用上表中的 Claude 规则 |
|
|
205
207
|
| `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd 时使用本场独立草稿目录。只在讨论服务新建的会话中执行:每位参与者一个独立原生讨论项目,续接复用该项目;不绑定外部已有会话 |
|
|
206
208
|
|
|
209
|
+
三种 CLI 适配器可选 `model`:分别作为 `codex -m`、`claude --model`、`agy --model` 的参数传入,留空使用各 CLI 的默认模型;可存入本机默认配置,也可在新建讨论时按参与者单独填写。模型名不能以 `-` 开头或含空格。可选 `effort` 设置思考档位:Codex 为 low / medium / high / xhigh / max / ultra,以 `-c model_reasoning_effort="档位"` 传入;Claude 为 low / medium / high / xhigh / max,以 `--effort` 传入;agy 的档位写在模型名中(如 `gemini-4-argon-high`),不单独设置。App 适配器使用 App 内选择的模型与档位。
|
|
210
|
+
|
|
207
211
|
两种新适配器都不需要 `source_thread_id` 或 `ipc_directory`。实际 ID 在输出流到达时写入本机绑定和本次执行凭据,随后 AI 按任务文件自行 `execution enter` 并通过讨论 CLI 保存业务结果;程序不把终端回答直接代发。已有 App 绑定不能通过改适配器直接复用,须先暂停并使用独立 `data_directory` 明确重新接入。此路径不承诺 CLI 会话同步到桌面 App。
|
|
208
212
|
|
|
209
213
|
### 单次执行权限
|
|
210
214
|
|
|
211
|
-
每次执行只开放本场讨论所需的最小权限,不使用全部放行或绕过审批参数。任务文件的 `cli` 为 `codiscuss
|
|
215
|
+
每次执行只开放本场讨论所需的最小权限,不使用全部放行或绕过审批参数。任务文件的 `cli` 为 `codiscuss`;Windows 下 Codex 与 agy 经 PowerShell 执行命令,npm 生成的 `codiscuss.ps1` 会被执行策略拒绝,因此改为 `codiscuss.cmd`,Claude 仍用 `codiscuss`。`subcommands` 列出唯一放行的网站子命令:`room`、`message`、`turn`、`state`、`floor`、`execution`、`health`、`schema`;`service`、`launch`、`run`、`api`、`setup`、`install`、`local` 不放行。提示词要求命令以该 `cli` 开头,不用 `python discuss.py`、`cd` 或管道。规则不含路径,重装或更换 Python 无需更新;子进程 PATH 中的 `codiscuss` 须指向本次安装,setup 的持久命令检查会核对。
|
|
212
216
|
|
|
213
217
|
项目文件权限 `project_access` 为 `read`(默认)或 `write`:启动文件顶层设本场值,`participants.NAME.project_access` 覆盖单个 AI,空字符串表示沿用本场值。它只随本次启动保存,不能存入本机默认配置;网页新建讨论在“启动”区直接提供项目目录与项目文件选项,单个 AI 的覆盖在“调整启动配置”中。三种 CLI 都在用户指定的 cwd 启动;省略 cwd 时才使用独立草稿目录。agy 的权限全部由其独立讨论项目执行,见下文。App 适配器的写权限由 App 自身决定,任务文件始终写明本场要求。
|
|
214
218
|
|
|
@@ -233,7 +237,7 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
|
|
|
233
237
|
|
|
234
238
|
2026-10-18 本机实测(本轮权限调整前):Claude 主持、Codex 与 Gemini 专家完成一场最小讨论。Windows 的 Codex / agy 经 PowerShell 调用时优先执行 npm 生成的 `codiscuss.ps1`;Codex elevated 沙箱内即使当前用户为 RemoteSigned 也拒绝 `.ps1`,Codex 自行改用 `cmd.exe` 调用 `codiscuss.cmd` 后成功。`agy` 非交互模式下被拒绝的工具也可能伴随退出码 0,最终仍以网站业务保存为准。`agy` 显式使用 `--print-timeout 0`,由房间的单次执行时限和整场截止控制停止,避免额外的 CLI 默认时限提前截断。
|
|
235
239
|
|
|
236
|
-
结构化失败(Codex `turn.failed`、Antigravity 非 `SUCCESS` 结果)、非零退出码、缺少会话 ID
|
|
240
|
+
结构化失败(Codex `turn.failed`、Antigravity 非 `SUCCESS` 结果)、非零退出码、缺少会话 ID 或成功结束事件均记录为失败;软件已启动后不自动重发。正常退出后若网站尚未保存本次结果,复查后暂停。同一参与者兼任主持与专家时,主持保存安排后进程可能仍在退出;该参与者由本服务创建的进程全部退出前不交付下一次执行,避免续接同一会话时出现两个写入者。暂停或停止接入服务会终止本服务创建的 CLI 进程,保留会话绑定和本机日志。
|
|
237
241
|
|
|
238
242
|
2026-10-19 权限调整验证:Windows / Codex CLI 0.163.0 的本地配置检查采用了本次 profile;以原生 `sandbox`(`unelevated`)运行无模型的文件操作,确认项目 cwd 保持、中文文件可读、只读模式拒绝项目与运行目录写入、可写模式允许项目写入但仍拒绝运行目录写入,两种模式均可写草稿并访问本机 HTTP。此证据不覆盖 elevated 模式、其他平台或本轮调整后的真实模型讨论。
|
|
239
243
|
|
package/package.json
CHANGED
|
@@ -242,7 +242,7 @@ async function claude_delivery(config, binding, task, spawn_process = spawn) {
|
|
|
242
242
|
try { bind_access(task.access_path, session_id); }
|
|
243
243
|
catch (error) { throw not_sent(error); }
|
|
244
244
|
const args = ['-p', '--output-format', 'json', binding.session_id ? '--resume' : '--session-id', session_id,
|
|
245
|
-
...claude_permissions(config, task)];
|
|
245
|
+
...(config.model ? ['--model', config.model] : []), ...(config.effort ? ['--effort', config.effort] : []), ...claude_permissions(config, task)];
|
|
246
246
|
let child;
|
|
247
247
|
try {
|
|
248
248
|
child = spawn_process(config.executable, [...args, '--', task.prompt], {
|
|
@@ -341,7 +341,8 @@ function cli_invocation(config, binding, task, user_home) {
|
|
|
341
341
|
const permissions = codex_permissions(config, task);
|
|
342
342
|
const args = ['exec', '--cd', launch_directory(config, task)];
|
|
343
343
|
if (binding.session_id) args.push('resume', binding.session_id);
|
|
344
|
-
args.push(...permissions.args, '--json', '--skip-git-repo-check', '-')
|
|
344
|
+
args.push(...permissions.args, '--json', '--skip-git-repo-check', ...(config.model ? ['-m', config.model] : []),
|
|
345
|
+
...(config.effort ? ['-c', `model_reasoning_effort="${config.effort}"`] : []), '-');
|
|
345
346
|
return { args, input: task.prompt, permissions };
|
|
346
347
|
}
|
|
347
348
|
const project = prepare_agy_project(config, binding, task, user_home);
|
|
@@ -349,6 +350,7 @@ function cli_invocation(config, binding, task, user_home) {
|
|
|
349
350
|
'--add-dir', task.draft_directory, '--add-dir', task.skills_directory];
|
|
350
351
|
args.push('--mode', 'accept-edits', '--project', project.project_id);
|
|
351
352
|
if (binding.session_id) args.push('--conversation', binding.session_id);
|
|
353
|
+
if (config.model) args.push('--model', config.model);
|
|
352
354
|
args.push('-p', task.prompt);
|
|
353
355
|
return { args, project };
|
|
354
356
|
}
|
|
@@ -11,7 +11,9 @@ import { acquire_lock, api, DiscussionStore, service_drafts, task_files, validat
|
|
|
11
11
|
import { CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
|
|
12
12
|
|
|
13
13
|
const CLIENTS = { Codex: ['codex_cli', 'codex'], Claude: ['claude'], Gemini: ['antigravity_cli', 'antigravity'] };
|
|
14
|
-
|
|
14
|
+
// Reasoning effort is a separate CLI argument only for Codex and Claude; agy encodes it in the model name.
|
|
15
|
+
const EFFORT_LEVELS = { codex_cli: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], claude: ['low', 'medium', 'high', 'xhigh', 'max'] };
|
|
16
|
+
const PROFILE_FIELDS = ['adapter', 'executable', 'model', 'effort', 'source_thread_id', 'ipc_directory', 'allowed_tools', 'cwd', 'project_name', 'project_id'];
|
|
15
17
|
const MAX_MINUTES = 1440;
|
|
16
18
|
const CONFIG_APPLY_TIMEOUT_MS = 6000;
|
|
17
19
|
const CONFIG_APPLY_POLL_MS = 100;
|
|
@@ -60,6 +62,12 @@ function validate_profile(name, profile, binding = false) {
|
|
|
60
62
|
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) throw new Error(`${name}.allowed_tools 必须为工具名称数组`);
|
|
61
63
|
} else if (typeof value !== 'string') throw new Error(`${name}.${key} 必须为文本`);
|
|
62
64
|
if (['executable', 'cwd', 'ipc_directory'].includes(key) && value && !isAbsolute(value)) throw new Error(`${name}.${key} 必须为完整绝对路径`);
|
|
65
|
+
// The model is passed as one CLI argument, so it must not look like a flag or contain spaces.
|
|
66
|
+
if (key === 'model' && value && !/^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/.test(value)) throw new Error(`${name}.model 只能是模型名称,例如 gpt-6-astra、opus 或 gemini-4-argon-high`);
|
|
67
|
+
}
|
|
68
|
+
if (profile.effort && !EFFORT_LEVELS[profile.adapter]?.includes(profile.effort)) {
|
|
69
|
+
throw new Error(EFFORT_LEVELS[profile.adapter] ? `${name}.effort 只能是 ${EFFORT_LEVELS[profile.adapter].join(' / ')}`
|
|
70
|
+
: `${name} 的思考档位不能单独设置;agy 请选择带档位的模型名,App 在软件内选择`);
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -103,7 +111,7 @@ function launch_config(home, input, room, directory, options) {
|
|
|
103
111
|
const participants = {};
|
|
104
112
|
for (const name of new Set([room.host, ...Object.keys(room.experts)])) {
|
|
105
113
|
const profile = { ...defaults[name], ...input.participants?.[name] };
|
|
106
|
-
for (const field of ['cwd', 'project_name', 'project_id', 'project_access']) if (profile[field] === '') delete profile[field];
|
|
114
|
+
for (const field of ['cwd', 'model', 'effort', 'project_name', 'project_id', 'project_access']) if (profile[field] === '') delete profile[field];
|
|
107
115
|
validate_profile(name, profile, true);
|
|
108
116
|
if (!CLIENTS[name]?.includes(profile.adapter)) throw new Error(`请配置 ${name} 的 CLI / App 客户端`);
|
|
109
117
|
if (profile.adapter !== 'antigravity' && (!existsSync(profile.executable || '') || !statSync(profile.executable).isFile()
|
|
@@ -11,6 +11,7 @@ import { atomic_json } from './discussion_adapters.mjs';
|
|
|
11
11
|
import { acquire_lock } from './discussion_runner.mjs';
|
|
12
12
|
import { main as service_main, service_home, service_pid, service_ready, start_service } from './discussion_service.mjs';
|
|
13
13
|
import { detect_python, local_settings } from './discussion_setup.mjs';
|
|
14
|
+
import { tone } from './discussion_tui.mjs';
|
|
14
15
|
|
|
15
16
|
const SCRIPT = fileURLToPath(import.meta.url);
|
|
16
17
|
const SERVER = fileURLToPath(new URL('../server.py', import.meta.url));
|
|
@@ -18,13 +19,14 @@ const START_WAIT_MS = 15000;
|
|
|
18
19
|
const STOP_WAIT_MS = 25000;
|
|
19
20
|
const POLL_MS = 200;
|
|
20
21
|
const CONNECTION_TOKEN_BYTES = 32;
|
|
21
|
-
const LOCAL_HELP = `用法:codiscuss local open|start|status|stop
|
|
22
|
+
const LOCAL_HELP = `用法:codiscuss local open|start|status|stop [--json]
|
|
22
23
|
codiscuss local connect SITE [--no-open]
|
|
23
24
|
codiscuss local disconnect SITE
|
|
24
25
|
|
|
25
26
|
connect 允许指定 HTTPS 站点通过此浏览器使用本机软件配置并启动 AI,打开连接链接完成接入。
|
|
26
27
|
disconnect 撤销该站点的新启动权限;已运行的讨论须另行暂停。
|
|
27
28
|
数据保留在服务目录中;重新 connect 会替换该站点的旧连接。
|
|
29
|
+
结果摘要写入终端;输出被重定向或加 --json 时 JSON 写入 stdout。
|
|
28
30
|
`;
|
|
29
31
|
|
|
30
32
|
/** Accept an explicit secure website origin; loopback HTTP is available for local deployments. */
|
|
@@ -227,25 +229,52 @@ function open_site(url) {
|
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
/** Manage local website lifecycle without changing discussion launch semantics. */
|
|
230
|
-
|
|
232
|
+
/** Print a readable result for people; machine output stays on stdout only when redirected or with --json. */
|
|
233
|
+
function local_summary(command, result, opened) {
|
|
234
|
+
const site = tone.cyan(result.url || result.local_url || '');
|
|
235
|
+
const background = tone.dim('网站与讨论服务在后台运行,关闭此终端不影响;停止运行 codiscuss local stop');
|
|
236
|
+
const lines = {
|
|
237
|
+
open: [`本机网站已就绪:${site}${opened ? '(已在浏览器打开)' : ''}`, background],
|
|
238
|
+
start: [`本机网站已就绪:${site}(未打开浏览器,可直接访问此地址)`, background],
|
|
239
|
+
status: result.running ? [`本机网站运行中:${site}`]
|
|
240
|
+
: result.website_running ? [`本机网站运行中:${site},但讨论服务未运行;运行 codiscuss local open 恢复`]
|
|
241
|
+
: ['本机网站未运行;运行 codiscuss local open 启动'],
|
|
242
|
+
stop: ['本机网站与讨论服务已停止;数据已保留,运行 codiscuss local open 重新启动'],
|
|
243
|
+
connect: [`已连接 ${result.connected};本机网站 ${tone.cyan(result.local_url || '')}`,
|
|
244
|
+
opened ? '已在浏览器打开连接链接,请在该页面完成接入;连接链接不要转发' : `请在浏览器打开连接链接完成接入,链接不要转发:${result.url}`],
|
|
245
|
+
disconnect: [`已撤销 ${result.disconnected} 的本机启动权限;正在运行的讨论需在进度看板暂停`],
|
|
246
|
+
}[command];
|
|
247
|
+
process.stderr.write(`${lines.join('\n')}\n`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Manage local website lifecycle without changing discussion launch semantics.
|
|
252
|
+
* Without an output callback, people get a summary on stderr and scripts get JSON when stdout is redirected or --json is given.
|
|
253
|
+
*/
|
|
254
|
+
export async function local_main(args, output) {
|
|
231
255
|
const { positionals, values } = parseArgs({ args, allowPositionals: true, options: {
|
|
232
|
-
'no-open': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
256
|
+
'no-open': { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
233
257
|
} });
|
|
234
258
|
const [command = 'open', site] = positionals;
|
|
235
259
|
const home = service_home();
|
|
236
|
-
if (values.help || command === 'help') return
|
|
260
|
+
if (values.help || command === 'help') return process.stdout.write(LOCAL_HELP);
|
|
261
|
+
const report = (result, opened) => {
|
|
262
|
+
if (output) return output(result);
|
|
263
|
+
local_summary(command, result, opened);
|
|
264
|
+
if (values.json || !process.stdout.isTTY) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
265
|
+
};
|
|
237
266
|
if (['connect', 'disconnect'].includes(command)) {
|
|
238
267
|
if (positionals.length !== 2 || (values['no-open'] && command !== 'connect')) throw new Error(LOCAL_HELP);
|
|
239
268
|
const origin = connection_site(site);
|
|
240
269
|
const result = command === 'connect' ? await connect_site(home, origin, values['no-open'])
|
|
241
270
|
: (save_site_connection(home, origin), { disconnected: origin });
|
|
242
|
-
|
|
271
|
+
report(result, command === 'connect' && !values['no-open']);
|
|
243
272
|
return result;
|
|
244
273
|
}
|
|
245
274
|
if (positionals.length > 1 || values['no-open'] || !['open', 'start', 'status', 'stop'].includes(command)) throw new Error(LOCAL_HELP);
|
|
246
275
|
const result = command === 'status' ? local_status(home) : command === 'stop' ? await stop_local(home) : await start_local(home);
|
|
247
276
|
if (command === 'open') open_site(result.url);
|
|
248
|
-
|
|
277
|
+
report(result, command === 'open');
|
|
249
278
|
return result;
|
|
250
279
|
}
|
|
251
280
|
|
|
@@ -150,14 +150,16 @@ export function task_files(config, event, token, binding) {
|
|
|
150
150
|
if (event.role === 'host') skills.push(join(config.project, '.agents/skills/discuss-host/SKILL.md'));
|
|
151
151
|
const adapter = config.participants[event.participant];
|
|
152
152
|
const skills_directory = join(config.project, '.agents', 'skills');
|
|
153
|
+
// Codex and agy run commands through PowerShell on Windows, whose execution policy blocks npm's codiscuss.ps1 shim.
|
|
154
|
+
const cli = process.platform === 'win32' && adapter.adapter !== 'claude' ? 'codiscuss.cmd' : 'codiscuss';
|
|
153
155
|
const project_rule = !adapter.cwd ? '未指定讨论项目目录。'
|
|
154
156
|
: adapter.project_access === 'write' ? '用户允许按议题需要修改 cwd 中的项目文件。' : 'cwd 中的项目文件只读,不修改。';
|
|
155
157
|
atomic_json(task_path, { site: config.site, room: config.room, event_id: event.id, participant: event.participant,
|
|
156
158
|
project_name: adapter.project_name || null, cwd: adapter.cwd || null, project_access: adapter.project_access,
|
|
157
159
|
draft_directory,
|
|
158
|
-
role: event.role, turn_id: event.turn_id, skills, cli
|
|
159
|
-
instructions:
|
|
160
|
-
const prompt = `用户已启动此房间的全自动讨论。请以 ${event.participant} 的${event.role === 'host' ? '主持' : '专家'}职责处理本次工作。先读取任务文件:${task_path},立即用
|
|
160
|
+
role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS, access_file: access_path,
|
|
161
|
+
instructions: `读取指定技能;网站命令只用 ${cli} 的 subcommands 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口;使用 ${cli} execution enter 房间 执行编号 --access-file 凭据路径 进入。凭据文件由 CLI 内部读取,不要打印或读取其 token。进入成功后才处理本次安排;每次业务写入都带 --access-file。答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;${project_rule}只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。` });
|
|
162
|
+
const prompt = `用户已启动此房间的全自动讨论。请以 ${event.participant} 的${event.role === 'host' ? '主持' : '专家'}职责处理本次工作。先读取任务文件:${task_path},立即用 ${cli} execution enter 进入,再读取指定版本的技能和网站现状。本次只放行任务内 subcommands 列出的 ${cli} 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口。任务内 cwd 是讨论项目目录;${project_rule}项目读写仍受软件实际权限和议题授权约束。不读取凭据内容。实际会话 ID 由接入程序写入凭据文件,尚未就绪只短暂重查,不编造。`;
|
|
161
163
|
return { id, participant: event.participant, directory: config.directory, draft_directory, skills_directory, access_path, prompt };
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -169,6 +171,7 @@ export class DiscussionRunner {
|
|
|
169
171
|
this.deliver = deliver;
|
|
170
172
|
this.children = new Set();
|
|
171
173
|
this.exited = new Set();
|
|
174
|
+
this.running = new Map();
|
|
172
175
|
}
|
|
173
176
|
/** Reserve with a durable token so a lost HTTP response can be reconciled. */
|
|
174
177
|
async dispatch(row, event) {
|
|
@@ -191,7 +194,7 @@ export class DiscussionRunner {
|
|
|
191
194
|
invoked = true;
|
|
192
195
|
const result = await this.deliver(adapter, binding, { ...task, room_title: this.room_title,
|
|
193
196
|
on_binding: session => this.store.bind(event.participant, session),
|
|
194
|
-
on_process: child =>
|
|
197
|
+
on_process: child => this.track(event.participant, child),
|
|
195
198
|
on_output: (path, chunk) => appendFileSync(path, chunk, { mode: 0o600 }),
|
|
196
199
|
on_failure: error => { try { this.store.record(row.id, { error }); } catch { /* Store may already be closed at shutdown. */ } },
|
|
197
200
|
on_exit: () => this.exited.add(row.id),
|
|
@@ -296,7 +299,8 @@ export class DiscussionRunner {
|
|
|
296
299
|
if (event.generation !== run.generation || event.run_id !== run.run_id) this.store.record(row.id, { status: 'cancelled' });
|
|
297
300
|
}
|
|
298
301
|
const next = eligible.at(-1);
|
|
299
|
-
|
|
302
|
+
// A host that just saved the next turn may still be exiting; the same session cannot accept a second writer yet.
|
|
303
|
+
if (next && !this.running.get(next.event.participant)) {
|
|
300
304
|
const fresh = await api(this.config, `/events/${next.row.id}`, { action: 'view' });
|
|
301
305
|
if (fresh.status === 'pending') {
|
|
302
306
|
await this.dispatch(next.row, fresh);
|
|
@@ -304,6 +308,15 @@ export class DiscussionRunner {
|
|
|
304
308
|
else this.store.record(next.row.id, { status: fresh.status });
|
|
305
309
|
}
|
|
306
310
|
}
|
|
311
|
+
/** Count live processes per participant so a resumed session never overlaps its previous writer. */
|
|
312
|
+
track(participant, child) {
|
|
313
|
+
this.children.add(child);
|
|
314
|
+
this.running.set(participant, (this.running.get(participant) || 0) + 1);
|
|
315
|
+
child.once('close', () => {
|
|
316
|
+
this.children.delete(child);
|
|
317
|
+
this.running.set(participant, this.running.get(participant) - 1);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
307
320
|
/** Stop only child processes created by this runner, never the desktop apps. */
|
|
308
321
|
stop_children() { for (const child of this.children) child.kill(); }
|
|
309
322
|
}
|
|
@@ -304,7 +304,7 @@ async function setup(options) {
|
|
|
304
304
|
const launch = spinner('启动本机网站与讨论服务');
|
|
305
305
|
try {
|
|
306
306
|
result.local = await local_main([options['no-open'] ? 'start' : 'open'], () => {});
|
|
307
|
-
launch.done('网站与讨论服务已在后台运行');
|
|
307
|
+
launch.done('网站与讨论服务已在后台运行', [`${tone.cyan(result.local.url)}${options['no-open'] ? tone.dim(' 未打开浏览器,可直接访问此地址') : tone.dim(' 已在浏览器打开')}`]);
|
|
308
308
|
if (result.configured) result.status = 'ready';
|
|
309
309
|
} catch (error) {
|
|
310
310
|
launch.fail(error.message);
|
package/web/app.js
CHANGED
|
@@ -1080,6 +1080,16 @@ async function load_launch_settings() {
|
|
|
1080
1080
|
finally { refresh_launch_recoveries(); render_launch_settings(); publish_view(); }
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
|
+
// Only CLI clients accept a model argument; App clients use the model chosen inside the App.
|
|
1084
|
+
const LAUNCH_MODEL_HINTS = {
|
|
1085
|
+
codex_cli: { example: "例如 gpt-6-astra", help: "传给 codex -m;留空用 CLI 默认模型" },
|
|
1086
|
+
claude: { example: "例如 opus、sonnet、fable", help: "传给 claude --model;留空用 CLI 默认模型" },
|
|
1087
|
+
antigravity_cli: { example: "例如 gemini-4-argon-high", help: "传给 agy --model;可用模型运行 agy models 查看,留空用 CLI 默认模型" },
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// Reasoning effort is a separate argument only for Codex and Claude; agy encodes it in the model name.
|
|
1091
|
+
const LAUNCH_EFFORT_LEVELS = { codex_cli: ["low", "medium", "high", "xhigh", "max", "ultra"], claude: ["low", "medium", "high", "xhigh", "max"] };
|
|
1092
|
+
|
|
1083
1093
|
/** Keep software drafts separate from shared room records and from discussion advancement mode. */
|
|
1084
1094
|
function write_launch_field(name, field, value) {
|
|
1085
1095
|
if (name === "workspace") state.launch.workspace[field] = value;
|
|
@@ -1087,6 +1097,7 @@ function write_launch_field(name, field, value) {
|
|
|
1087
1097
|
else {
|
|
1088
1098
|
state.launch.profiles[name] ||= {};
|
|
1089
1099
|
if (field === "allowed_tools") value = value.split("\n").map(item => item.trim()).filter(Boolean);
|
|
1100
|
+
if (["model", "effort"].includes(field)) value = value.trim();
|
|
1090
1101
|
state.launch.profiles[name][field] = value;
|
|
1091
1102
|
}
|
|
1092
1103
|
state.launch.notice = "";
|
|
@@ -1117,7 +1128,9 @@ function launch_summary(name) {
|
|
|
1117
1128
|
const directory = profile.cwd || state.launch.workspace.cwd;
|
|
1118
1129
|
const access = !cli_directory ? "按 App 权限" : !directory ? "无项目目录"
|
|
1119
1130
|
: launch_access(name) === "write" ? "项目可写" : "项目只读";
|
|
1120
|
-
|
|
1131
|
+
const label = LAUNCH_CLIENTS[name]?.find(item => item.value === profile.adapter)?.label || "尚未选择客户端";
|
|
1132
|
+
const choice = [profile.model, profile.effort && `${profile.effort} 档`].filter(Boolean).join(" · ");
|
|
1133
|
+
return { client: cli_directory && choice ? `${label} · ${choice}` : label,
|
|
1121
1134
|
session: profile.session_id ? "复用指定会话" : "新建会话",
|
|
1122
1135
|
cwd: !cli_directory && profile.session_id ? "沿用指定会话的目录"
|
|
1123
1136
|
: directory || (cli_directory ? "本次讨论的私有目录" : "由 App 项目确定"), project, access };
|
|
@@ -1273,6 +1286,8 @@ function render_launch_settings() {
|
|
|
1273
1286
|
const profile = state.launch.profiles[name] || {};
|
|
1274
1287
|
const fields = profile.adapter === "antigravity" ? [["ipc_directory", "Sidecar IPC 目录"]]
|
|
1275
1288
|
: [["executable", "原生可执行文件路径"]];
|
|
1289
|
+
if (["codex_cli", "claude", "antigravity_cli"].includes(profile.adapter)) fields.push(["model", "模型(可选,留空用 CLI 默认)"]);
|
|
1290
|
+
if (LAUNCH_EFFORT_LEVELS[profile.adapter]) fields.push(["effort", `思考档位(可选:${LAUNCH_EFFORT_LEVELS[profile.adapter].join(" / ")})`]);
|
|
1276
1291
|
if (profile.adapter === "codex") fields.push(["source_thread_id", "来源聊天 ID(新建会话时使用)"]);
|
|
1277
1292
|
if (profile.adapter === "claude") fields.push(["allowed_tools", "允许的工具(每行一项,可选)"]);
|
|
1278
1293
|
fields.push(["session_id", "复用会话 ID(仅本次,可选)"], ["cwd", "此 AI 的工作目录(可选)"], ["project_name", "此 AI 的项目(可选)"]);
|
|
@@ -2053,7 +2068,7 @@ function change_view_theme(theme) {
|
|
|
2053
2068
|
}
|
|
2054
2069
|
|
|
2055
2070
|
window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
|
|
2056
|
-
write_launch: write_launch_field, write_room_connection, launch_clients: LAUNCH_CLIENTS, launch_summary, empty_discussion_message,
|
|
2071
|
+
write_launch: write_launch_field, write_room_connection, launch_clients: LAUNCH_CLIENTS, launch_model_hints: LAUNCH_MODEL_HINTS, launch_effort_levels: LAUNCH_EFFORT_LEVELS, launch_summary, empty_discussion_message,
|
|
2057
2072
|
start_drag: start_directory_drag,
|
|
2058
2073
|
write_expert: write_view_expert, execute: execute_view_action, theme: change_view_theme,
|
|
2059
2074
|
request: request_json, identities: IDENTITIES, display_name, sources: SOURCE_LABELS, phases: PHASE_LABELS,
|