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 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
- 供开发者和未取得安装包的用户使用;已完成包安装可直接跳到下一节。需要 Git、Python 3.10+,使用自动接入还需 Node.js 24+ 和 npm。仓库包含构建好的网页。
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 类型必须与配置对应;其他接入方式与配置细节见 [两个入口的启动配置](docs/runner.md#两种讨论入口与启动配置)。
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` 重新启动。安装摘要会同时提示这些状态与停止命令。改端口或升级程序前先 `local stop`,再重新启动。
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`,`subcommands` 列出唯一放行的网站子命令:`room`、`message`、`turn`、`state`、`floor`、`execution`、`health`、`schema`;`service`、`launch`、`run`、`api`、`setup`、`install`、`local` 不放行。提示词要求命令以 `codiscuss` 开头,不用 `python discuss.py`、`cd` 或管道。规则不含路径,重装或更换 Python 无需更新;子进程 PATH 中的 `codiscuss` 须指向本次安装,setup 的持久命令检查会核对。
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 或成功结束事件均记录为失败;软件已启动后不自动重发。正常退出后若网站尚未保存本次结果,复查后暂停。暂停或停止接入服务会终止本服务创建的 CLI 进程,保留会话绑定和本机日志。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -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
- const PROFILE_FIELDS = ['adapter', 'executable', 'source_thread_id', 'ipc_directory', 'allowed_tools', 'cwd', 'project_name', 'project_id'];
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
- export async function local_main(args, output = value => process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)) {
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 output(LOCAL_HELP);
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
- output(result);
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
- output(result);
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: 'codiscuss', subcommands: TASK_SUBCOMMANDS, access_file: access_path,
159
- instructions: '读取指定技能;网站命令只用 codiscuss 的 subcommands 子命令,命令以 codiscuss 开头,不用 python discuss.py、cd、管道或其他入口;使用 codiscuss execution enter 房间 执行编号 --access-file 凭据路径 进入。凭据文件由 CLI 内部读取,不要打印或读取其 token。进入成功后才处理本次安排;每次业务写入都带 --access-file。答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;' + project_rule + '只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。' });
160
- const prompt = `用户已启动此房间的全自动讨论。请以 ${event.participant} 的${event.role === 'host' ? '主持' : '专家'}职责处理本次工作。先读取任务文件:${task_path},立即用 codiscuss execution enter 进入,再读取指定版本的技能和网站现状。本次只放行任务内 subcommands 列出的 codiscuss 子命令,命令以 codiscuss 开头,不用 python discuss.py、cd、管道或其他入口。任务内 cwd 是讨论项目目录;${project_rule}项目读写仍受软件实际权限和议题授权约束。不读取凭据内容。实际会话 ID 由接入程序写入凭据文件,尚未就绪只短暂重查,不编造。`;
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 => { this.children.add(child); child.once('close', () => this.children.delete(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
- if (next) {
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
- return { client: LAUNCH_CLIENTS[name]?.find(item => item.value === profile.adapter)?.label || "尚未选择客户端",
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,