ai_co_discuss 0.2.79 → 0.2.81

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.
@@ -65,6 +65,7 @@ description: "AI CoDiscuss 的主持技能,仅主持人调用,且须同时
65
65
  - 计划须包含 `waiting` 中的每位专家;`unregistered` 中尚未注册的手动专家不能排入,它注册后会自动加入本轮队列。名单变化后 `revision` 失效需重取,缺人或未带 `revision` 都会被拒绝;本轮不需要某人时先让其暂离。
66
66
  - 计划只覆盖当前一轮,跨轮追问要看本轮回答后再定;只有各轮问题不依赖回答的固定流程(如依次报数)才可排满多轮。后一位的问题取决于前一位回答时也不必等前一位答完:把问题写成“先读取最新回答,再按……回应”,由专家自己读取。
67
67
  - 网站在专家间自动接力,末位答完才交回主持;接力不发通知。用户留言或打断时服务端会自动把控制权交回主持。只有在线专家须逐个转交、或必须等用户先决定时,才只排一位,并在 state 里写明原因。
68
+ - `members` 中 `adapter` 为 `api` 的专家读不到本地文件,只能从对话了解项目。议题涉及本地文件时:开场的主持说明多交代项目情况(相关文件、结构、要点,必要时贴关键片段);`initial` 中它不读他人观点,靠这份介绍作答;进入交锋后把它排在能读文件的专家之后,问题里请它先读前面的回答;它在回答里索要的信息,安排能读文件的专家补充。
68
69
 
69
70
  ### 名单变化
70
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.2.79",
3
+ "version": "0.2.81",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -18,6 +18,11 @@ const CODEX_PERMISSIONS_MIN_MINOR = 163;
18
18
  /** Website subcommands an automatic task may run; service, launch, run, api and setup stay unapproved. */
19
19
  export const TASK_SUBCOMMANDS = ['room', 'message', 'turn', 'state', 'execution', 'health', 'schema'];
20
20
 
21
+ /** The agy project allows only codiscuss commands and headless agy ends the turn on a denied one, so every agy task states it; no member prompt can drop it. */
22
+ export function agy_command_rule(adapter, cli) {
23
+ return adapter === 'antigravity_cli' ? `无人值守下 run_command 只能运行 ${cli} 的 subcommands 子命令,其他命令会被自动拒绝并使会话直接结束;列目录、搜索或读取文件用 list_dir、find_by_name、grep_search、view_file。` : '';
24
+ }
25
+
21
26
  /** Only failures before a software invocation are safe for automatic redelivery. */
22
27
  function not_sent(error) { return Object.assign(error, { delivery_status: 'not_sent' }); }
23
28
 
@@ -9,7 +9,7 @@ import { pathToFileURL } from 'node:url';
9
9
  import { parseArgs } from 'node:util';
10
10
 
11
11
  import { atomic_json } from './discussion_adapters.mjs';
12
- import { software_effort, software_models } from './discussion_models.mjs';
12
+ import { software_defaults, software_effort, software_models } from './discussion_models.mjs';
13
13
  import { API_PROVIDERS, api_check, api_models, save_api_key, saved_api_keys, validate_api_profile } from './discussion_api.mjs';
14
14
  import { SCHEDULER_PROVIDERS, scheduler_check } from './scheduler.mjs';
15
15
  import { MAX_EXPERT_PROMPT } from './expert_prompts.mjs';
@@ -200,7 +200,7 @@ export function pick_directory({ platform = process.platform, spawn_process = sp
200
200
  const MODEL_CACHE_MS = 24 * 60 * 60 * 1000;
201
201
 
202
202
  /** Return the software catalog, served from the private service home when still valid. */
203
- export async function cached_software_models(home, profile, { refresh = false, query = software_models, query_effort = software_effort } = {}) {
203
+ export async function cached_software_models(home, profile, { refresh = false, query = software_models, query_effort = software_effort, query_defaults = software_defaults } = {}) {
204
204
  const path = join(home, 'model-catalogs.json');
205
205
  let stamp = '';
206
206
  try { const file = statSync(profile.executable); stamp = `${file.size}:${file.mtimeMs}`; } catch {}
@@ -209,8 +209,9 @@ export async function cached_software_models(home, profile, { refresh = false, q
209
209
  let cache = {};
210
210
  try { cache = JSON.parse(readFileSync(path, 'utf8')); } catch {}
211
211
  const cached = entry => !refresh && entry && Date.now() - entry.at < MODEL_CACHE_MS ? entry.result : null;
212
- const [result, effort] = await Promise.all([cached(cache[key]) || query(profile),
213
- profile.adapter === 'antigravity_cli' ? cached(cache[effort_key]) || query_effort(profile) : null]);
212
+ // Defaults are read on every request, so a changed software setting shows without waiting out the cache.
213
+ const [result, effort, defaults] = await Promise.all([cached(cache[key]) || query(profile),
214
+ profile.adapter === 'antigravity_cli' ? cached(cache[effort_key]) || query_effort(profile) : null, query_defaults(profile)]);
214
215
  const updates = {};
215
216
  if (!cached(cache[key]) && !result.error && result.models.length) {
216
217
  updates[key] = { at: Date.now(), result };
@@ -224,7 +225,7 @@ export async function cached_software_models(home, profile, { refresh = false, q
224
225
  for (const old of Object.keys(cache)) if (old.startsWith(prefix) && !old.startsWith(key.slice(0, -1))) delete cache[old];
225
226
  atomic_json(path, { ...cache, ...updates });
226
227
  }
227
- return effort ? { ...result, efforts: effort.levels, ...(effort.error ? { effort_error: effort.error } : {}) } : result;
228
+ return { ...result, defaults, ...(effort ? { efforts: effort.levels, ...(effort.error ? { effort_error: effort.error } : {}) } : {}) };
228
229
  }
229
230
 
230
231
  /** Read private defaults without selecting a global room or identity. */
@@ -233,8 +234,8 @@ export function launch_settings(home) {
233
234
  return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : { participants: {} };
234
235
  }
235
236
 
236
- /** Read only the registered room's model choices, never machine defaults or connection secrets. */
237
- export function room_models(home, raw) {
237
+ /** Read the registered room's model choices and what each software selects for empty ones, never connection secrets. */
238
+ export async function room_models(home, raw, { read_defaults = software_defaults } = {}) {
238
239
  const input = launch_input({ ...raw, entry: 'web' });
239
240
  if (!input.room_id) throw new Error('读取本场模型须提供 room_id');
240
241
  const paths = read_registry(home).filter(path => {
@@ -243,11 +244,12 @@ export function room_models(home, raw) {
243
244
  });
244
245
  if (paths.length > 1) throw new Error('本场存在多份登记配置,无法确定模型设置');
245
246
  const profiles = paths.length ? configuration_snapshot(paths[0]).config.participants : {};
246
- const participants = Object.fromEntries(Object.entries(profiles).map(([name, profile]) => {
247
+ const participants = Object.fromEntries(await Promise.all(Object.entries(profiles).map(async ([name, profile]) => {
247
248
  const cli = ['codex_cli', 'claude', 'antigravity_cli', 'api'].includes(profile.adapter);
249
+ const defaults = await read_defaults(profile);
248
250
  return [name, { adapter: profile.adapter, model: cli ? profile.model || '' : '',
249
- effort: EFFORT_LEVELS[profile.adapter] ? profile.effort || '' : '' }];
250
- }));
251
+ effort: EFFORT_LEVELS[profile.adapter] ? profile.effort || '' : '', default_model: defaults.model, default_effort: defaults.effort }];
252
+ })));
251
253
  return { room_id: input.room_id, participants };
252
254
  }
253
255
 
@@ -886,7 +888,7 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
886
888
  else if (positionals[0] === 'api-key' && input) result = save_api_key(home, input);
887
889
  else if (positionals[0] === 'api-check' && input) result = await api_check(home, input);
888
890
  else if (positionals[0] === 'scheduler-check' && input) result = await scheduler_check(home, input);
889
- else if (positionals[0] === 'room-models' && input) result = room_models(home, input);
891
+ else if (positionals[0] === 'room-models' && input) result = await room_models(home, input);
890
892
  else if (positionals[0] === 'models' && input?.adapter === 'api') result = await api_models(home, input);
891
893
  else if (positionals[0] === 'models' && input) {
892
894
  const saved = Object.values(launch_settings(home).participants).find(profile => profile.adapter === input.adapter);
@@ -1,7 +1,8 @@
1
1
  /** Discover model selectors from the installed software without starting an AI turn. */
2
2
  import { spawn } from 'node:child_process';
3
- import { isAbsolute } from 'node:path';
4
- import { tmpdir } from 'node:os';
3
+ import { readFileSync } from 'node:fs';
4
+ import { isAbsolute, join } from 'node:path';
5
+ import { homedir, tmpdir } from 'node:os';
5
6
  import { createInterface } from 'node:readline';
6
7
 
7
8
  const QUERY_TIMEOUT_MS = 30000;
@@ -9,6 +10,38 @@ const MAX_OUTPUT_BYTES = 1024 * 1024;
9
10
  const SOURCES = { codex_cli: 'codex-app-server', claude: 'claude-cli', antigravity_cli: 'antigravity-cli' };
10
11
  const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
11
12
 
13
+ /** Read the string keys of a TOML file by table; Codex settings need nothing richer. */
14
+ function toml_strings(text) {
15
+ const tables = { '': {} };
16
+ let table = '';
17
+ for (const line of text.split(/\r?\n/)) {
18
+ const header = line.match(/^\s*\[\s*([^\]]+?)\s*\]\s*(?:#.*)?$/);
19
+ if (header) { table = header[1].replaceAll('"', ''); tables[table] ||= {}; continue; }
20
+ const pair = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"/);
21
+ if (pair) tables[table][pair[1]] = pair[2];
22
+ }
23
+ return tables;
24
+ }
25
+
26
+ /**
27
+ * What Codex or Claude Code runs with when the card leaves model or effort empty, read from the software's own user settings.
28
+ * Empty where the settings say nothing.
29
+ */
30
+ export function cli_defaults(adapter, { env = process.env, home = homedir() } = {}) {
31
+ try {
32
+ if (adapter === 'codex_cli') {
33
+ const tables = toml_strings(readFileSync(join(env.CODEX_HOME || join(home, '.codex'), 'config.toml'), 'utf8'));
34
+ const top = tables[''], chosen = top.profile ? tables[`profiles.${top.profile}`] || {} : {};
35
+ return { model: chosen.model || top.model || '', effort: chosen.model_reasoning_effort || top.model_reasoning_effort || '' };
36
+ }
37
+ if (adapter === 'claude') {
38
+ const settings = JSON.parse(readFileSync(join(env.CLAUDE_CONFIG_DIR || join(home, '.claude'), 'settings.json'), 'utf8'));
39
+ return { model: typeof settings.model === 'string' ? settings.model : '', effort: typeof settings.effortLevel === 'string' ? settings.effortLevel : '' };
40
+ }
41
+ } catch { /* Missing or unreadable settings mean the software's built-in default. */ }
42
+ return { model: '', effort: '' };
43
+ }
44
+
12
45
  /** Return only model identifiers, display names and supported effort values. */
13
46
  function model_entry(id, label, efforts) {
14
47
  if (typeof id !== 'string' || !MODEL_PATTERN.test(id)) return null;
@@ -16,9 +49,9 @@ function model_entry(id, label, efforts) {
16
49
  ...(Array.isArray(efforts) ? { efforts: [...new Set(efforts.filter(item => typeof item === 'string'))] } : {}) };
17
50
  }
18
51
 
19
- /** Keep all query processes noninteractive and avoid user/project startup hooks. */
20
- function query_arguments({ adapter, model }, effort = false) {
21
- if (effort) return ['-p', '/effort', '--output-format', 'json', ...(model ? ['--model', model] : [])];
52
+ /** Keep all query processes noninteractive and avoid user/project startup hooks; agy's native slash commands answer without an agent turn. */
53
+ function query_arguments({ adapter, model }, command = '') {
54
+ if (command) return ['-p', `/${command}`, '--output-format', 'json', ...(command === 'effort' && model ? ['--model', model] : [])];
22
55
  if (adapter === 'codex_cli') return ['app-server', '--listen', 'stdio://'];
23
56
  if (adapter === 'antigravity_cli') return ['models'];
24
57
  return ['--safe-mode', '-p', '--input-format', 'stream-json', '--output-format', 'stream-json',
@@ -30,7 +63,7 @@ function query_catalog(profile, options) {
30
63
  return new Promise((resolve_result, reject) => {
31
64
  const env = { ...process.env };
32
65
  delete env.CLAUDECODE;
33
- const child = (options.spawn || spawn)(profile.executable, query_arguments(profile, options.effort), {
66
+ const child = (options.spawn || spawn)(profile.executable, query_arguments(profile, options.command), {
34
67
  cwd: tmpdir(), env, shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
35
68
  });
36
69
  let finished = false, bytes = 0;
@@ -42,7 +75,7 @@ function query_catalog(profile, options) {
42
75
  const timer = setTimeout(() => finish(new Error('读取模型列表超时,请检查软件登录和网络后重试')), options.timeout_ms ?? QUERY_TIMEOUT_MS);
43
76
  const lines = createInterface({ input: child.stdout });
44
77
  const send = value => child.stdin.write(`${JSON.stringify(value)}\n`);
45
- const receive = catalog_reader(profile.adapter, send, finish, options.effort);
78
+ const receive = catalog_reader(profile.adapter, send, finish, options.command);
46
79
  child.once('error', () => finish(new Error('无法启动模型查询,请检查 CLI 路径')));
47
80
  child.stdin.on('error', () => finish(new Error('模型查询连接已关闭')));
48
81
  child.stderr.on('data', () => {}); // CLI diagnostics can contain private account or configuration data.
@@ -94,12 +127,12 @@ function json_line(line) {
94
127
  }
95
128
 
96
129
  /** Claude initialization exposes its picker; agy emits tab-separated ids and labels. */
97
- function catalog_reader(adapter, send, finish, effort = false) {
98
- if (effort) return { start: () => {}, line: line => {
130
+ function catalog_reader(adapter, send, finish, command = '') {
131
+ if (command) return { start: () => {}, line: line => {
99
132
  const reply = json_line(line), data = reply?.command?.data;
100
- if (reply?.status === 'SUCCESS' && reply.command?.name === 'effort' && Array.isArray(data?.available)) {
101
- finish(null, data.adjustable ? data.available.filter(level => typeof level === 'string') : []);
102
- }
133
+ if (reply?.status !== 'SUCCESS' || reply.command?.name !== command || !data) return;
134
+ if (command === 'effort' && Array.isArray(data.available)) finish(null, data.adjustable ? data.available.filter(level => typeof level === 'string') : []);
135
+ if (command === 'model') finish(null, { model: MODEL_PATTERN.test(data.id || '') ? data.id : '', effort: typeof data.effort === 'string' ? data.effort : '' });
103
136
  } };
104
137
  if (adapter === 'codex_cli') return codex_reader(send, finish);
105
138
  if (adapter === 'claude') return {
@@ -137,6 +170,14 @@ export async function software_models(profile, options = {}) {
137
170
  export async function software_effort(profile, options = {}) {
138
171
  if (profile.adapter !== 'antigravity_cli' || !isAbsolute(profile.executable || '')
139
172
  || (profile.model && !MODEL_PATTERN.test(profile.model))) return { error: '无法读取所选模型的思考档位' };
140
- try { return { levels: await query_catalog(profile, { ...options, effort: true }) }; }
173
+ try { return { levels: await query_catalog(profile, { ...options, command: 'effort' }) }; }
141
174
  catch { return { error: '无法读取原生思考档位;保留 CLI 默认值,或检查软件版本后重试' }; }
142
175
  }
176
+
177
+ /** What a CLI runs with when the card leaves model or effort empty: agy answers its native /model, the others keep it in settings files. */
178
+ export async function software_defaults(profile, options = {}) {
179
+ if (profile.adapter !== 'antigravity_cli') return cli_defaults(profile.adapter);
180
+ if (!isAbsolute(profile.executable || '')) return { model: '', effort: '' };
181
+ try { return await query_catalog(profile, { ...options, command: 'model' }); }
182
+ catch { return { model: '', effort: '' }; }
183
+ }
@@ -7,7 +7,7 @@ import { DatabaseSync } from 'node:sqlite';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { parseArgs } from 'node:util';
9
9
  import { setTimeout as sleep } from 'node:timers/promises';
10
- import { atomic_json, deliver_task, remove_agy_projects, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
10
+ import { agy_command_rule, atomic_json, deliver_task, remove_agy_projects, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
11
11
  import { read_api_key } from './discussion_api.mjs';
12
12
  import { with_expert_prompt } from './expert_prompts.mjs';
13
13
  import { decide_interjection, decide_next_speaker, decide_round, validate_scheduler } from './scheduler.mjs';
@@ -231,7 +231,7 @@ export function task_files(config, event, token, binding, { suggestions = [], en
231
231
  draft_directory, ...(suggestions.length ? { scheduler_suggestions: suggestions } : {}),
232
232
  role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS,
233
233
  ...(bound_access ? { access_env: 'DISCUSS_ACCESS_FILE' } : { access_file: access_path }),
234
- instructions: `读取指定技能;网站命令只用 ${cli} 的 subcommands 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口;${auto_enter ? '接入程序已进入本次执行,不要再执行 execution enter。' : `使用 ${cli} execution enter 房间 执行编号 --access-file 凭据路径 进入。进入成功后才处理本次安排;`}凭据文件由 CLI 内部读取,不要打印或读取其 token。${access_rule}答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;${project_rule}只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。${suggestion_rule}` });
234
+ instructions: `${agy_command_rule(adapter.adapter, cli)}读取指定技能;网站命令只用 ${cli} 的 subcommands 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口;${auto_enter ? '接入程序已进入本次执行,不要再执行 execution enter。' : `使用 ${cli} execution enter 房间 执行编号 --access-file 凭据路径 进入。进入成功后才处理本次安排;`}凭据文件由 CLI 内部读取,不要打印或读取其 token。${access_rule}答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;${project_rule}只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。${suggestion_rule}` });
235
235
  const read_rule = windows_rg() ? '读取含中文的技能或文本文件时用 rg --encoding utf-8 . 文件路径,不用 Get-Content(Windows PowerShell 会按系统编码读出乱码,需要反复重试)。' : '';
236
236
  const scope = `本次只放行任务内 subcommands 列出的 ${cli} 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口。任务内 cwd 是讨论项目目录;${project_rule}项目读写仍受软件实际权限和议题授权约束。不读取凭据内容。${read_rule}`;
237
237
  const plan_rule = event.role === 'host' ? '安排专家回应时先写发言计划,先用 turn roster 取得最新专家状态,再用 turn create --plan-file 加 --roster 一次排满 waiting 中的每位专家,不要每次只排一位。' : '';
@@ -7,9 +7,10 @@
7
7
  export const MAX_EXPERT_PROMPT = 4000;
8
8
 
9
9
  export const EXPERT_PROMPT_PRESETS = {
10
- // Headless agy ends the turn when a command needs approval, and tends to work without a word between tools.
11
- antigravity_cli: `无人值守下 run_command 只能运行 codiscuss 的子命令,其他命令会被自动拒绝并使会话直接结束;列目录、搜索或读取文件用 list_dir、find_by_name、grep_search、view_file。
12
- 任务需要多步操作时,在读完材料、改定方案、验证结果这类关键转折处,用一两句中文写出进展再继续。`,
10
+ // agy streams no thinking text, so without a word between tools the page cannot show how far it got.
11
+ antigravity_cli: '多步任务中,在关键转折处用一句话写出进展再继续。',
12
+ // An API expert sees only the room, so what it knows of local files comes from other members and it must ask for the rest.
13
+ api: '你读不到本地文件,相关内容以其他成员贴出的为准,不凭文件名猜测。缺信息时在回答里写明缺什么。',
13
14
  };
14
15
 
15
16
  /** The prompt this member receives: its own text when set, otherwise its software's preset. */
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { appendFileSync, mkdirSync, readFileSync, statSync } from 'node:fs';
5
5
  import { basename, join, resolve } from 'node:path';
6
- import { atomic_json, deliver_task, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
6
+ import { agy_command_rule, atomic_json, deliver_task, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
7
7
  import { api } from './discussion_runner.mjs';
8
8
  import { with_expert_prompt } from './expert_prompts.mjs';
9
9
  import { ROUTE_CONFIDENCE, ROUTE_CONTEXT_MESSAGES, ROUTE_PROMPT_VERSION, ROUTE_WIDE_CONFIDENCE, decide_group_order, route_free_message } from './scheduler.mjs';
@@ -87,7 +87,7 @@ export function free_task_files(config, member, execution, binding) {
87
87
  read_after: execution.read_until, cwd: profile.cwd || null, project_access: profile.project_access, draft_directory,
88
88
  skills: [join(config.project, '.agents/skills/discuss-participant/SKILL.md')], cli, subcommands: TASK_SUBCOMMANDS,
89
89
  ...(bound_access ? { access_env: 'DISCUSS_ACCESS_FILE' } : { access_file: access_path }),
90
- instructions: `${discussion ? '讨论房间:用户直接向你提问(' + messages + ')。这不是你的发言安排,不要领取发言权、不要安排他人,也不改变讨论进度,只回答这个提问。' : '自由协作房间:用户点名了你(' + messages + ')。'}${access_rule}先用 ${cli} message list ${config.room} --after ${execution.read_until}${access_arg} 读取新消息,按时间顺序处理,后面的消息可能修正前面的,以最新指令为准。${host_rule}完成后用 ${cli} message post ${config.room} --as ${member.name} --kind chat --execution ${execution.id} --body-file 草稿 --submission-id 新UUID${access_arg} 提交一次回答。提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 --execution 改作普通发言。草稿只写入 draft_directory;${project_rule}不读取或打印凭据内容,不建定时任务。` });
90
+ instructions: `${agy_command_rule(profile.adapter, cli)}${discussion ? '讨论房间:用户直接向你提问(' + messages + ')。这不是你的发言安排,不要领取发言权、不要安排他人,也不改变讨论进度,只回答这个提问。' : '自由协作房间:用户点名了你(' + messages + ')。'}${access_rule}先用 ${cli} message list ${config.room} --after ${execution.read_until}${access_arg} 读取新消息,按时间顺序处理,后面的消息可能修正前面的,以最新指令为准。${host_rule}完成后用 ${cli} message post ${config.room} --as ${member.name} --kind chat --execution ${execution.id} --body-file 草稿 --submission-id 新UUID${access_arg} 提交一次回答。提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 --execution 改作普通发言。草稿只写入 draft_directory;${project_rule}不读取或打印凭据内容,不建定时任务。` });
91
91
  const prompt = `用户在${discussion ? '讨论' : '自由协作'}房间点名了你(${member.name}),对应消息 ${messages}。请先读取任务文件:${task_path},按其中说明读取新消息、完成请求,并用 ${cli} message post 带 --execution ${execution.id} 提交一次回答后结束。命令只用任务内 subcommands 列出的 ${cli} 子命令;${project_rule}不读取凭据内容。`;
92
92
  return { id, participant: member.name, directory: config.directory, draft_directory,
93
93
  skills_directory: join(config.project, '.agents', 'skills'), access_path, prompt: with_expert_prompt(prompt, profile) };
package/web/app.js CHANGED
@@ -1889,15 +1889,17 @@ async function load_room_models() {
1889
1889
  }
1890
1890
  }
1891
1891
 
1892
- /** Describe registered launch choices without claiming the software's effective defaults are known. */
1892
+ /** Describe registered launch choices; an empty choice shows what the software's own settings select, or says the software decides. */
1893
1893
  function participant_model(name, room, saved) {
1894
1894
  // Nothing to say beats a placeholder: a manual member's settings live in its own session and unread ones are not guessed.
1895
1895
  if (room?.runtime?.mode !== "automatic" || (room.members || []).find(member => member.name === name)?.access === "manual") return "";
1896
1896
  const profile = saved?.room_id === room.id ? saved.participants?.[name] : null;
1897
1897
  if (!profile) return "";
1898
1898
  if (["codex", "antigravity"].includes(profile.adapter)) return "模型与强度由 App 设置";
1899
- const model = profile.model || "默认模型";
1900
- const effort = profile.effort || (profile.adapter === "antigravity_cli" ? "模型默认强度" : "默认强度");
1899
+ const model = profile.model || profile.default_model || "软件默认模型";
1900
+ // agy's default effort belongs to its default model, so a chosen model keeps its own default instead.
1901
+ const default_effort = profile.adapter === "antigravity_cli" && profile.model ? "" : profile.default_effort;
1902
+ const effort = profile.effort || default_effort || (profile.adapter === "antigravity_cli" ? "模型默认强度" : "软件默认强度");
1901
1903
  return `${model} · ${effort}`;
1902
1904
  }
1903
1905
 
@@ -1953,9 +1955,10 @@ function launch_model_state(name, launch = state.launch) {
1953
1955
  const catalog = launch.model_catalogs?.[model_catalog_key(profile)];
1954
1956
  const models = catalog?.models || [];
1955
1957
  const selected = models.find(model => model.id === profile.model);
1956
- const items = [{ label: "软件默认模型", value: "__default__" }, ...models.map(model => ({ label: model.label || model.id, value: model.id }))];
1958
+ const defaults = catalog?.defaults || {};
1959
+ const items = [{ label: defaults.model ? `软件默认(${defaults.model})` : "软件默认模型", value: "__default__" }, ...models.map(model => ({ label: model.label || model.id, value: model.id }))];
1957
1960
  if (profile.model && !selected) items.push({ label: `${profile.model}(已保存,列表未确认)`, value: profile.model, disabled: true });
1958
- return { supported: MODEL_CLIENTS.includes(profile.adapter), items, loading: Boolean(catalog?.loading),
1961
+ return { supported: MODEL_CLIENTS.includes(profile.adapter), items, defaults, loading: Boolean(catalog?.loading),
1959
1962
  efforts: profile.adapter === "antigravity_cli" ? catalog?.efforts : selected?.efforts, source: MODEL_SOURCE_LABELS[catalog?.source] || catalog?.source || "",
1960
1963
  effort_note: profile.adapter === "antigravity_cli" ? catalog?.effort_error || (catalog?.loading ? "正在读取原生思考档位…" : "默认强度由 CLI 决定。") : !selected
1961
1964
  ? "选择模型后显示可用思考强度;默认值由软件决定。" : "所选模型不提供单独思考强度。",
@@ -1982,6 +1985,7 @@ async function load_launch_models(name, force = false) {
1982
1985
  current.models = result.models || [];
1983
1986
  current.source = result.source || "";
1984
1987
  current.efforts = result.efforts;
1988
+ current.defaults = result.defaults || {};
1985
1989
  current.effort_error = result.effort_error || "";
1986
1990
  current.error = result.error || (!current.models.length ? "软件未返回可用模型,可使用默认模型" : "");
1987
1991
  } catch (error) { current.error = error.message; }