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.
- package/.agents/skills/discuss-host/SKILL.md +1 -0
- package/package.json +1 -1
- package/runner/discussion_launch.mjs +13 -11
- package/runner/discussion_models.mjs +54 -13
- package/runner/expert_prompts.mjs +2 -0
- package/web/app.js +9 -5
- package/web/modern.css +1 -1
- package/web/modern.js +106 -112
|
@@ -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
|
@@ -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
|
-
|
|
213
|
-
|
|
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 ? {
|
|
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
|
|
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 {
|
|
4
|
-
import {
|
|
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 },
|
|
21
|
-
if (
|
|
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.
|
|
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.
|
|
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,
|
|
98
|
-
if (
|
|
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
|
|
101
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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; }
|