ai_co_discuss 0.1.3 → 0.1.5

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
@@ -135,7 +135,7 @@ codiscuss local connect https://你的站点
135
135
 
136
136
  默认上限为 **3 轮、60 分钟、到限暂停**,可改为请主持人总结,并设置单次执行上限与收束时间。计数和停止规则见 [讨论上限](docs/workflow.md#讨论上限)。
137
137
 
138
- 讨论结束后,可在“更多操作 → 导出对话”下载原始数据和全文,或用 `discuss-report` 整理带来源的综合报告。备份目前不支持导入恢复,也不下载外部附件。
138
+ 讨论结束后,可在左上方频道条目的“更多操作 → 导出对话”下载原始数据和全文,窄屏从频道导航进入;经典主题使用“备份当前讨论”按钮。也可用 `discuss-report` 整理带来源的综合报告。备份目前不支持导入恢复,也不下载外部附件。
139
139
 
140
140
  网站目前**没有用户登录和读取权限隔离**;公开部署前请按 [安全说明](.github/SECURITY.md) 配置访问边界。同一模型名称只能配置一个参与者,安排串行进行;在线读取、原生定时和浏览器操作能力取决于参与软件。
141
141
 
@@ -138,6 +138,8 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
138
138
 
139
139
  界面提供默认深色「协作频道」、浅色协作频道和经典「千禧聊天室」。深浅频道共用组件,浅色覆盖同时作用于正文、菜单和弹窗;只有深色频道启用根节点的 `dark` 类。频道界面的外观设置集中在左下角,窄屏可从频道导航抽屉进入。主题保存在浏览器,切换保留房间、筛选与未发送草稿。
140
140
 
141
+ 频道管理集中在左上方频道条目自带的「更多操作」菜单,右键条目使用同一菜单,窄屏从频道导航抽屉进入。重命名、复制阅读入口、导出、移动、存档和删除对应被点击的频道,不切换当前讨论;当前频道菜单另提供编辑议题和刷新消息。频道条目不重复显示消息总数,统计保留在主区域顶栏。顶栏保留搜索、筛选、进度和侧栏开关。
142
+
141
143
  深浅频道采用统一的低饱和蓝灰色系,由 `channel.css` 的背景、表面、强调色与分级阴影变量维护。左右侧栏共用底色,三栏标题与主持简报共用顶部表面色;正文底色、卡片与控件用明度差区分,避免各区域使用不同冷暖的灰色。深色卡片保留顶部亮边、底部暗边和局部渐变;浅色使用白色卡片、蓝灰细描边与柔和投影。交互状态共用蓝色强调色,参与者和运行状态保留各自的语义颜色。
142
144
 
143
145
  左右侧栏仅用朝向正文的柔和阴影分界,避免竖向描边与内侧高光线重复叠加。分栏拖柄覆盖接缝,层级高于主持简报、低于浮动预览,不占用布局宽度,仅在悬停、聚焦或拖动时显色。新建入口与导航选中项具有独立边界,右栏状态为轻浮起面板,焦点、参与者及耗时以留白和分隔线分组。消息、共识与主持面板使用较轻阴影,按钮使用短投影,菜单和抽屉使用浮层投影,搜索和表单使用内阴影;按下和禁用时降低或移除投影。输入区聚焦保留面板阴影并叠加焦点边界。材质调整保留三栏结构、阅读宽度和经典主题。
@@ -146,7 +148,7 @@ Python 在一个只读事务快照中查询;D1 使用同一个 batch 事务读
146
148
 
147
149
  消息区显示议题、原文和主持简报;简报收起时以局限在消息列内的底部渐隐阴影区分正文,不向侧栏溢出;展开后由浮层承接投影,避免叠加阴影或推动消息与输入区。已存档频道在独立弹窗管理,不改变当前频道和草稿。存档频道仍可阅读、恢复与导出;批量删除作用于当前搜索和筛选结果,执行前确认。
148
150
 
149
- 进度看板在宽屏消息页的右侧栏显示(视口 ≥1200px),其他布局用同一组件的抽屉。状态来自当前安排、本轮发言与 `runtime.participants`;会话编号来自最近一次实际进入的执行,`stale` 表示执行租约过期。手动模式仅按安排和发言展示进度,不推断离线或伪造执行回执。
151
+ 进度看板在宽屏消息页的右侧栏显示(视口 ≥1200px),其他布局用同一组件的抽屉。仅当右侧进度栏不可见时,顶栏显示状态按钮(空间充足时附轮次),点击打开独立进度抽屉,关闭后保留原侧栏布局;右侧栏的展开与收起仍由独立的频道概览按钮控制。状态来自当前安排、本轮发言与 `runtime.participants`;会话编号来自最近一次实际进入的执行,`stale` 表示执行租约过期。手动模式仅按安排和发言展示进度,不推断离线或伪造执行回执。
150
152
 
151
153
  运行操作按服务器状态开放:暂停、重启、取消安排与提前结束均调用既有接口,业务成功以服务器确认为准。重启只用于已暂停或到期的全自动运行,需要本机接入程序运行;服务器按话题上限截短新截止时间。准备、收束和存档状态不显示不可用操作。界面尺寸与动效参数以组件和样式为准,变更通过 [贡献指南](../.github/CONTRIBUTING.md) 的界面验收要求核对。
152
154
 
package/docs/runner.md CHANGED
@@ -34,7 +34,7 @@ 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
 
@@ -206,11 +206,13 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
206
206
  | `claude` | 现有 `claude -p --output-format json`;后续 `--resume ID` | 沿用上表中的 Claude 规则 |
207
207
  | `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd 时使用本场独立草稿目录。只在讨论服务新建的会话中执行:每位参与者一个独立原生讨论项目,续接复用该项目;不绑定外部已有会话 |
208
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
+
209
211
  两种新适配器都不需要 `source_thread_id` 或 `ipc_directory`。实际 ID 在输出流到达时写入本机绑定和本次执行凭据,随后 AI 按任务文件自行 `execution enter` 并通过讨论 CLI 保存业务结果;程序不把终端回答直接代发。已有 App 绑定不能通过改适配器直接复用,须先暂停并使用独立 `data_directory` 明确重新接入。此路径不承诺 CLI 会话同步到桌面 App。
210
212
 
211
213
  ### 单次执行权限
212
214
 
213
- 每次执行只开放本场讨论所需的最小权限,不使用全部放行或绕过审批参数。任务文件的 `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 的持久命令检查会核对。
214
216
 
215
217
  项目文件权限 `project_access` 为 `read`(默认)或 `write`:启动文件顶层设本场值,`participants.NAME.project_access` 覆盖单个 AI,空字符串表示沿用本场值。它只随本次启动保存,不能存入本机默认配置;网页新建讨论在“启动”区直接提供项目目录与项目文件选项,单个 AI 的覆盖在“调整启动配置”中。三种 CLI 都在用户指定的 cwd 启动;省略 cwd 时才使用独立草稿目录。agy 的权限全部由其独立讨论项目执行,见下文。App 适配器的写权限由 App 自身决定,任务文件始终写明本场要求。
216
218
 
@@ -235,7 +237,7 @@ Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地
235
237
 
236
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 默认时限提前截断。
237
239
 
238
- 结构化失败(Codex `turn.failed`、Antigravity 非 `SUCCESS` 结果)、非零退出码、缺少会话 ID 或成功结束事件均记录为失败;软件已启动后不自动重发。正常退出后若网站尚未保存本次结果,复查后暂停。暂停或停止接入服务会终止本服务创建的 CLI 进程,保留会话绑定和本机日志。
240
+ 结构化失败(Codex `turn.failed`、Antigravity 非 `SUCCESS` 结果)、非零退出码、缺少会话 ID 或成功结束事件均记录为失败;软件已启动后不自动重发。正常退出后若网站尚未保存本次结果,复查后暂停。同一参与者兼任主持与专家时,主持保存安排后进程可能仍在退出;该参与者由本服务创建的进程全部退出前不交付下一次执行,避免续接同一会话时出现两个写入者。暂停或停止接入服务会终止本服务创建的 CLI 进程,保留会话绑定和本机日志。
239
241
 
240
242
  2026-10-19 权限调整验证:Windows / Codex CLI 0.163.0 的本地配置检查采用了本次 profile;以原生 `sandbox`(`unelevated`)运行无模型的文件操作,确认项目 cwd 保持、中文文件可读、只读模式拒绝项目与运行目录写入、可写模式允许项目写入但仍拒绝运行目录写入,两种模式均可写草稿并访问本机 HTTP。此证据不覆盖 elevated 模式、其他平台或本轮调整后的真实模型讨论。
241
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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,