ai_co_discuss 0.2.80 → 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.80",
3
+ "version": "0.2.81",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -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
+ }
@@ -9,6 +9,8 @@ export const MAX_EXPERT_PROMPT = 4000;
9
9
  export const EXPERT_PROMPT_PRESETS = {
10
10
  // agy streams no thinking text, so without a word between tools the page cannot show how far it got.
11
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: '你读不到本地文件,相关内容以其他成员贴出的为准,不凭文件名猜测。缺信息时在回答里写明缺什么。',
12
14
  };
13
15
 
14
16
  /** The prompt this member receives: its own text when set, otherwise its software's preset. */
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; }