ai_co_discuss 0.2.34 → 0.2.35
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 +3 -3
- package/.agents/skills/discuss-participant/SKILL.md +6 -4
- package/README.md +1 -1
- package/backend/free_room.py +23 -3
- package/backend/schema.sql +9 -3
- package/backend/server.py +3 -2
- package/docs/architecture.md +8 -6
- package/docs/cli.md +5 -3
- package/docs/runner.md +9 -7
- package/docs/workflow.md +9 -5
- package/package.json +1 -1
- package/runner/discuss.py +973 -970
- package/runner/discussion_adapters.mjs +3 -1
- package/runner/discussion_launch.mjs +38 -17
- package/runner/discussion_models.mjs +19 -4
- package/runner/discussion_runner.mjs +8 -4
- package/runner/discussion_service.mjs +3 -3
- package/runner/free_runner.mjs +5 -2
- package/web/app.js +13 -14
- package/web/modern.css +1 -1
- package/web/modern.js +1251 -1239
|
@@ -263,7 +263,7 @@ async function claude_delivery(config, binding, task, spawn_process = spawn) {
|
|
|
263
263
|
try {
|
|
264
264
|
child = spawn_process(config.executable, [...args, '--', task.prompt], {
|
|
265
265
|
cwd: launch_directory(config, task), windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'],
|
|
266
|
-
env: standalone_claude_env(),
|
|
266
|
+
env: { ...standalone_claude_env(), DISCUSS_ACCESS_FILE: task.access_path },
|
|
267
267
|
});
|
|
268
268
|
} catch (error) { throw not_sent(error); }
|
|
269
269
|
if (typeof child.kill === 'function') {
|
|
@@ -371,6 +371,7 @@ function cli_invocation(config, binding, task, user_home) {
|
|
|
371
371
|
args.push('--mode', 'accept-edits', '--project', project.project_id);
|
|
372
372
|
if (binding.session_id) args.push('--conversation', binding.session_id);
|
|
373
373
|
if (config.model) args.push('--model', config.model);
|
|
374
|
+
if (config.effort) args.push('--effort', config.effort);
|
|
374
375
|
args.push('-p', task.prompt);
|
|
375
376
|
return { args, project };
|
|
376
377
|
}
|
|
@@ -492,6 +493,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
|
|
|
492
493
|
try {
|
|
493
494
|
child = spawn_process(config.executable, invocation.args, {
|
|
494
495
|
cwd: launch_directory(config, task), windowsHide: true, shell: false, stdio: ['pipe', 'pipe', 'pipe'],
|
|
496
|
+
env: { ...process.env, DISCUSS_ACCESS_FILE: task.access_path },
|
|
495
497
|
});
|
|
496
498
|
} catch (error) { throw not_sent(error); }
|
|
497
499
|
const state = monitor_cli(child, config, binding, task);
|
|
@@ -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_models } from './discussion_models.mjs';
|
|
12
|
+
import { 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 { acquire_lock, api, CODEX_ADAPTERS, DiscussionStore, service_drafts, task_files, validate_config } from './discussion_runner.mjs';
|
|
@@ -30,8 +30,9 @@ export function next_instance(platform, taken) {
|
|
|
30
30
|
|
|
31
31
|
/** Supported local clients of an instance's platform. */
|
|
32
32
|
function clients_of(name) { return CLIENTS[platform_of(name)]; }
|
|
33
|
-
//
|
|
34
|
-
const EFFORT_LEVELS = { codex_cli: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], claude: ['low', 'medium', 'high', 'xhigh', 'max']
|
|
33
|
+
// The UI discovers each model's supported subset; omitted values keep the CLI's own default.
|
|
34
|
+
const EFFORT_LEVELS = { codex_cli: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
35
|
+
antigravity_cli: ['low', 'medium', 'high', 'xhigh', 'max'] };
|
|
35
36
|
const PROFILE_FIELDS = ['adapter', 'executable', 'model', 'effort', 'source_thread_id', 'ipc_directory', 'allowed_tools', 'cwd', 'project_name', 'project_id', 'base_url', 'api_protocol', 'api_key_ref'];
|
|
36
37
|
const MAX_MINUTES = 1440;
|
|
37
38
|
const SESSION_ID_PATTERN = /^[0-9A-Za-z_-]{8,128}$/;
|
|
@@ -106,22 +107,31 @@ export function pick_directory({ platform = process.platform, spawn_process = sp
|
|
|
106
107
|
const MODEL_CACHE_MS = 24 * 60 * 60 * 1000;
|
|
107
108
|
|
|
108
109
|
/** Return the software catalog, served from the private service home when still valid. */
|
|
109
|
-
export async function cached_software_models(home, profile, { refresh = false, query = software_models } = {}) {
|
|
110
|
+
export async function cached_software_models(home, profile, { refresh = false, query = software_models, query_effort = software_effort } = {}) {
|
|
110
111
|
const path = join(home, 'model-catalogs.json');
|
|
111
112
|
let stamp = '';
|
|
112
113
|
try { const file = statSync(profile.executable); stamp = `${file.size}:${file.mtimeMs}`; } catch {}
|
|
113
114
|
const key = JSON.stringify([profile.adapter, profile.executable || '', stamp]);
|
|
115
|
+
const effort_key = JSON.stringify([profile.adapter, profile.executable || '', stamp, 'effort', profile.model || '']);
|
|
114
116
|
let cache = {};
|
|
115
117
|
try { cache = JSON.parse(readFileSync(path, 'utf8')); } catch {}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const cached = entry => !refresh && entry && Date.now() - entry.at < MODEL_CACHE_MS ? entry.result : null;
|
|
119
|
+
const [result, effort] = await Promise.all([cached(cache[key]) || query(profile),
|
|
120
|
+
profile.adapter === 'antigravity_cli' ? cached(cache[effort_key]) || query_effort(profile) : null]);
|
|
121
|
+
const updates = {};
|
|
122
|
+
if (!cached(cache[key]) && !result.error && result.models.length) {
|
|
123
|
+
updates[key] = { at: Date.now(), result };
|
|
124
|
+
}
|
|
125
|
+
if (effort && !effort.error && !cached(cache[effort_key])) {
|
|
126
|
+
updates[effort_key] = { at: Date.now(), result: effort };
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(updates).length) {
|
|
129
|
+
try { cache = JSON.parse(readFileSync(path, 'utf8')); } catch {}
|
|
120
130
|
const prefix = JSON.stringify([profile.adapter, profile.executable || '']).slice(0, -1);
|
|
121
|
-
for (const old of Object.keys(cache)) if (old.startsWith(prefix)) delete cache[old];
|
|
122
|
-
atomic_json(path, { ...cache,
|
|
131
|
+
for (const old of Object.keys(cache)) if (old.startsWith(prefix) && !old.startsWith(key.slice(0, -1))) delete cache[old];
|
|
132
|
+
atomic_json(path, { ...cache, ...updates });
|
|
123
133
|
}
|
|
124
|
-
return result;
|
|
134
|
+
return effort ? { ...result, efforts: effort.levels, ...(effort.error ? { effort_error: effort.error } : {}) } : result;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/** Read private defaults without selecting a global room or identity. */
|
|
@@ -180,7 +190,7 @@ function validate_profile(name, profile, binding = false) {
|
|
|
180
190
|
if (profile.adapter === 'api') validate_api_profile(profile);
|
|
181
191
|
if (profile.effort && profile.adapter !== 'api' && !EFFORT_LEVELS[profile.adapter]?.includes(profile.effort)) {
|
|
182
192
|
throw new Error(EFFORT_LEVELS[profile.adapter] ? `${name}.effort 只能是 ${EFFORT_LEVELS[profile.adapter].join(' / ')}`
|
|
183
|
-
: `${name}
|
|
193
|
+
: `${name} 的思考档位在原软件中选择`);
|
|
184
194
|
}
|
|
185
195
|
}
|
|
186
196
|
|
|
@@ -411,19 +421,29 @@ export async function room_member(home, raw, locked = false) {
|
|
|
411
421
|
} finally { release(); }
|
|
412
422
|
}
|
|
413
423
|
|
|
424
|
+
/** A Codex desktop chat is resumed through the app, any other Codex session through the CLI. */
|
|
425
|
+
function codex_session_adapter(session_id, env = process.env) {
|
|
426
|
+
const sessions = join(env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
|
|
427
|
+
const file = session_files(sessions).find(path => path.endsWith(`-${session_id}.jsonl`));
|
|
428
|
+
const meta = file ? readFileSync(file, 'utf8').split('\n', 1)[0] : '';
|
|
429
|
+
return /"originator":"Codex Desktop"/.test(meta) ? 'codex' : 'codex_cli';
|
|
430
|
+
}
|
|
431
|
+
|
|
414
432
|
/**
|
|
415
|
-
* Register a manual Claude expert's own session as a hosted participant, so the service resumes it for later turns.
|
|
416
|
-
* Throws when the local
|
|
433
|
+
* Register a manual Claude or Codex expert's own session as a hosted participant, so the service resumes it for later turns.
|
|
434
|
+
* Returns the adapter used. Throws when the local software is not configured or the session is not found on this machine; the member then stays manual.
|
|
417
435
|
*/
|
|
418
|
-
export function adopt_member_profile(home, path, name, session_id) {
|
|
436
|
+
export function adopt_member_profile(home, path, name, session_id, client = 'claude') {
|
|
419
437
|
const release = acquire_lock(join(dirname(path), 'configuration-edit'));
|
|
420
438
|
try {
|
|
421
439
|
const config = configuration_snapshot(path).config;
|
|
422
|
-
const
|
|
440
|
+
const adapter = client === 'codex' ? codex_session_adapter(session_id) : 'claude';
|
|
441
|
+
const input = { cwd: config.cwd, participants: { [name]: { adapter, session_id } } };
|
|
423
442
|
const profile = borrowed_session(name, participant_profile(name, launch_settings(home).participants, input, false), config.cwd);
|
|
424
443
|
const next = { ...config, participants: { ...config.participants, [name]: profile } };
|
|
425
444
|
validate_config(next, dirname(path));
|
|
426
445
|
atomic_json(path, next);
|
|
446
|
+
return adapter;
|
|
427
447
|
} finally { release(); }
|
|
428
448
|
}
|
|
429
449
|
|
|
@@ -741,7 +761,8 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
741
761
|
else if (positionals[0] === 'models' && input?.adapter === 'api') result = await api_models(home, input);
|
|
742
762
|
else if (positionals[0] === 'models' && input) {
|
|
743
763
|
const saved = Object.values(launch_settings(home).participants).find(profile => profile.adapter === input.adapter);
|
|
744
|
-
result = await cached_software_models(home, { adapter: input.adapter, executable: input.executable || saved?.executable
|
|
764
|
+
result = await cached_software_models(home, { adapter: input.adapter, executable: input.executable || saved?.executable,
|
|
765
|
+
...(input.model ? { model: input.model } : {}) }, { refresh: input.refresh === true });
|
|
745
766
|
}
|
|
746
767
|
else if (positionals[0] === 'room-member' && input) result = await room_member(home, input);
|
|
747
768
|
else if (positionals[0] === 'pick-directory') result = await pick_directory();
|
|
@@ -17,7 +17,8 @@ function model_entry(id, label, efforts) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Keep all query processes noninteractive and avoid user/project startup hooks. */
|
|
20
|
-
function query_arguments(adapter) {
|
|
20
|
+
function query_arguments({ adapter, model }, effort = false) {
|
|
21
|
+
if (effort) return ['-p', '/effort', '--output-format', 'json', ...(model ? ['--model', model] : [])];
|
|
21
22
|
if (adapter === 'codex_cli') return ['app-server', '--listen', 'stdio://'];
|
|
22
23
|
if (adapter === 'antigravity_cli') return ['models'];
|
|
23
24
|
return ['--safe-mode', '-p', '--input-format', 'stream-json', '--output-format', 'stream-json',
|
|
@@ -29,7 +30,7 @@ function query_catalog(profile, options) {
|
|
|
29
30
|
return new Promise((resolve_result, reject) => {
|
|
30
31
|
const env = { ...process.env };
|
|
31
32
|
delete env.CLAUDECODE;
|
|
32
|
-
const child = (options.spawn || spawn)(profile.executable, query_arguments(profile.
|
|
33
|
+
const child = (options.spawn || spawn)(profile.executable, query_arguments(profile, options.effort), {
|
|
33
34
|
cwd: tmpdir(), env, shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
35
|
});
|
|
35
36
|
let finished = false, bytes = 0;
|
|
@@ -41,7 +42,7 @@ function query_catalog(profile, options) {
|
|
|
41
42
|
const timer = setTimeout(() => finish(new Error('读取模型列表超时,请检查软件登录和网络后重试')), options.timeout_ms ?? QUERY_TIMEOUT_MS);
|
|
42
43
|
const lines = createInterface({ input: child.stdout });
|
|
43
44
|
const send = value => child.stdin.write(`${JSON.stringify(value)}\n`);
|
|
44
|
-
const receive = catalog_reader(profile.adapter, send, finish);
|
|
45
|
+
const receive = catalog_reader(profile.adapter, send, finish, options.effort);
|
|
45
46
|
child.once('error', () => finish(new Error('无法启动模型查询,请检查 CLI 路径')));
|
|
46
47
|
child.stdin.on('error', () => finish(new Error('模型查询连接已关闭')));
|
|
47
48
|
child.stderr.on('data', () => {}); // CLI diagnostics can contain private account or configuration data.
|
|
@@ -93,7 +94,13 @@ function json_line(line) {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/** Claude initialization exposes its picker; agy emits tab-separated ids and labels. */
|
|
96
|
-
function catalog_reader(adapter, send, finish) {
|
|
97
|
+
function catalog_reader(adapter, send, finish, effort = false) {
|
|
98
|
+
if (effort) return { start: () => {}, line: line => {
|
|
99
|
+
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
|
+
}
|
|
103
|
+
} };
|
|
97
104
|
if (adapter === 'codex_cli') return codex_reader(send, finish);
|
|
98
105
|
if (adapter === 'claude') return {
|
|
99
106
|
start: () => send({ type: 'control_request', request_id: 'catalog', request: { subtype: 'initialize' } }),
|
|
@@ -125,3 +132,11 @@ export async function software_models(profile, options = {}) {
|
|
|
125
132
|
} catch (error) { result.error = error.message; }
|
|
126
133
|
return result;
|
|
127
134
|
}
|
|
135
|
+
|
|
136
|
+
/** Read native effort capabilities for the selected agy model, without an agent turn or saved setting changes. */
|
|
137
|
+
export async function software_effort(profile, options = {}) {
|
|
138
|
+
if (profile.adapter !== 'antigravity_cli' || !isAbsolute(profile.executable || '')
|
|
139
|
+
|| (profile.model && !MODEL_PATTERN.test(profile.model))) return { error: '无法读取所选模型的思考档位' };
|
|
140
|
+
try { return { levels: await query_catalog(profile, { ...options, effort: true }) }; }
|
|
141
|
+
catch { return { error: '无法读取原生思考档位;保留 CLI 默认值,或检查软件版本后重试' }; }
|
|
142
|
+
}
|
|
@@ -197,7 +197,7 @@ export function acquire_lock(directory) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/** Write a non-secret task separately from its CLI-only credential file; suggestions are smart-scheduling hints for the host. */
|
|
200
|
-
export function task_files(config, event, token, binding, suggestions = [], entered = false) {
|
|
200
|
+
export function task_files(config, event, token, binding, { suggestions = [], entered = false, process_access = false } = {}) {
|
|
201
201
|
const id = `event-${event.id}`;
|
|
202
202
|
const draft_directory = config.draft_directory || `${config.directory}-drafts`;
|
|
203
203
|
mkdirSync(draft_directory, { recursive: true });
|
|
@@ -208,6 +208,9 @@ export function task_files(config, event, token, binding, suggestions = [], ente
|
|
|
208
208
|
if (event.role === 'host') skills.push(join(config.project, '.agents/skills/discuss-host/SKILL.md'));
|
|
209
209
|
const adapter = config.participants[event.participant];
|
|
210
210
|
const auto_enter = entered || AUTO_ENTER_ADAPTERS.has(adapter.adapter);
|
|
211
|
+
const bound_access = process_access && AUTO_ENTER_ADAPTERS.has(adapter.adapter);
|
|
212
|
+
const access_rule = bound_access ? '本次凭据文件已通过进程环境 DISCUSS_ACCESS_FILE 绑定,codiscuss 自动读取,命令无需 --access-file。'
|
|
213
|
+
: '每次业务写入都带 --access-file。';
|
|
211
214
|
const skills_directory = join(config.project, '.agents', 'skills');
|
|
212
215
|
// Codex and agy run commands through PowerShell on Windows, whose execution policy blocks npm's codiscuss.ps1 shim.
|
|
213
216
|
const cli = process.platform === 'win32' && adapter.adapter !== 'claude' ? 'codiscuss.cmd' : 'codiscuss';
|
|
@@ -217,8 +220,9 @@ export function task_files(config, event, token, binding, suggestions = [], ente
|
|
|
217
220
|
atomic_json(task_path, { site: config.site, room: config.room, event_id: event.id, participant: event.participant,
|
|
218
221
|
project_name: adapter.project_name || null, cwd: adapter.cwd || null, project_access: adapter.project_access,
|
|
219
222
|
draft_directory, ...(suggestions.length ? { scheduler_suggestions: suggestions } : {}),
|
|
220
|
-
role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS,
|
|
221
|
-
|
|
223
|
+
role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS,
|
|
224
|
+
...(bound_access ? { access_env: 'DISCUSS_ACCESS_FILE' } : { access_file: access_path }),
|
|
225
|
+
instructions: `${adapter.adapter === 'antigravity_cli' ? `无人值守下 run_command 只能运行 ${cli} 的 subcommands 子命令,其他命令会被自动拒绝并使会话直接结束;浏览项目用 list_dir、find_by_name、grep_search、view_file。` : ''}读取指定技能;网站命令只用 ${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}` });
|
|
222
226
|
const read_rule = windows_rg() ? '读取含中文的技能或文本文件时用 rg --encoding utf-8 . 文件路径,不用 Get-Content(Windows PowerShell 会按系统编码读出乱码,需要反复重试)。' : '';
|
|
223
227
|
const scope = `本次只放行任务内 subcommands 列出的 ${cli} 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口。任务内 cwd 是讨论项目目录;${project_rule}项目读写仍受软件实际权限和议题授权约束。不读取凭据内容。${read_rule}`;
|
|
224
228
|
const plan_rule = event.role === 'host' ? '安排专家回应时先写发言计划,用 turn create --plan-file 一次排满本轮每位专家,不要每次只排一位。' : '';
|
|
@@ -275,7 +279,7 @@ export class DiscussionRunner {
|
|
|
275
279
|
let invoked = false;
|
|
276
280
|
let entered = pre_entered;
|
|
277
281
|
try {
|
|
278
|
-
const task = task_files(this.config, event, row.token, binding, suggestions, pre_entered);
|
|
282
|
+
const task = task_files(this.config, event, row.token, binding, { suggestions, entered: pre_entered, process_access: true });
|
|
279
283
|
invoked = true;
|
|
280
284
|
const result = await this.deliver(adapter, binding, { ...task, room_title: this.room_title,
|
|
281
285
|
api_context: { site: this.config.site, room: this.config.room, token: row.token, home: this.config.api_home,
|
|
@@ -220,7 +220,7 @@ class RoomWorker {
|
|
|
220
220
|
}
|
|
221
221
|
this.state.config_version = snapshot.version;
|
|
222
222
|
}
|
|
223
|
-
/** A manual Claude expert whose CLI reported its session is handed to the service, which resumes that session from the next turn on. */
|
|
223
|
+
/** A manual Claude or Codex expert whose CLI reported its session is handed to the service, which resumes that session from the next turn on. */
|
|
224
224
|
async adopt_manual_members() {
|
|
225
225
|
if (this.config.mode === 'free') return;
|
|
226
226
|
for (const member of this.runner.members || []) {
|
|
@@ -228,8 +228,8 @@ class RoomWorker {
|
|
|
228
228
|
if (member.access !== 'manual' || member.status !== 'active' || !member.reported_session || this.adopt_failed.has(key)) continue;
|
|
229
229
|
try {
|
|
230
230
|
const { adopt_member_profile } = await import('./discussion_launch.mjs');
|
|
231
|
-
adopt_member_profile(this.home, this.path, member.name, member.reported_session);
|
|
232
|
-
await api(this.config, `/members/${member.id}`, { action: 'adopt', adapter
|
|
231
|
+
const adapter = adopt_member_profile(this.home, this.path, member.name, member.reported_session, member.reported_client);
|
|
232
|
+
await api(this.config, `/members/${member.id}`, { action: 'adopt', adapter, session_id: member.reported_session });
|
|
233
233
|
this.log(`${this.config.room}: ${member.name} 已转为本机托管`);
|
|
234
234
|
} catch (error) {
|
|
235
235
|
this.adopt_failed.add(key);
|
package/runner/free_runner.mjs
CHANGED
|
@@ -62,6 +62,9 @@ export function free_task_files(config, member, execution, binding) {
|
|
|
62
62
|
atomic_json(access_path, { site: config.site, room: config.room, token: config.members[member.name].token,
|
|
63
63
|
session_id: binding.session_id || '' });
|
|
64
64
|
const cli = process.platform === 'win32' && profile.adapter !== 'claude' ? 'codiscuss.cmd' : 'codiscuss';
|
|
65
|
+
const bound_access = ['claude', 'codex_cli', 'antigravity_cli'].includes(profile.adapter);
|
|
66
|
+
const access_arg = bound_access ? '' : ' --access-file 凭据路径';
|
|
67
|
+
const access_rule = bound_access ? '本次凭据文件已通过进程环境 DISCUSS_ACCESS_FILE 绑定,codiscuss 自动读取,命令无需 --access-file。' : '';
|
|
65
68
|
const project_rule = !profile.cwd ? '未指定项目目录。'
|
|
66
69
|
: profile.project_access === 'write' ? '用户允许按请求修改 cwd 中的项目文件。' : 'cwd 中的项目文件只读,不修改。';
|
|
67
70
|
const messages = execution.requests.map(item => `#${item.message_id}`).join('、');
|
|
@@ -74,8 +77,8 @@ export function free_task_files(config, member, execution, binding) {
|
|
|
74
77
|
request_purposes: execution.requests.map(item => item.purpose || 'ask'), role: member.role || 'member',
|
|
75
78
|
read_after: execution.read_until, cwd: profile.cwd || null, project_access: profile.project_access, draft_directory,
|
|
76
79
|
skills: [join(config.project, '.agents/skills/discuss-participant/SKILL.md')], cli, subcommands: TASK_SUBCOMMANDS,
|
|
77
|
-
access_file: access_path,
|
|
78
|
-
instructions: `自由协作房间:用户点名了你(${messages}
|
|
80
|
+
...(bound_access ? { access_env: 'DISCUSS_ACCESS_FILE' } : { access_file: access_path }),
|
|
81
|
+
instructions: `自由协作房间:用户点名了你(${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}不读取或打印凭据内容,不建定时任务。` });
|
|
79
82
|
const prompt = `用户在自由协作房间点名了你(${member.name}),对应消息 ${messages}。请先读取任务文件:${task_path},按其中说明读取新消息、完成请求,并用 ${cli} message post 带 --execution ${execution.id} 提交一次回答后结束。命令只用任务内 subcommands 列出的 ${cli} 子命令;${project_rule}不读取凭据内容。`;
|
|
80
83
|
return { id, participant: member.name, directory: config.directory, draft_directory,
|
|
81
84
|
skills_directory: join(config.project, '.agents', 'skills'), access_path, prompt };
|
package/web/app.js
CHANGED
|
@@ -914,13 +914,6 @@ async function change_member({ member_id, name, action, role }) {
|
|
|
914
914
|
} catch (error) { if (state.room?.id === room_id) state.free_error = error.message; }
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
-
/** Composer stop in a free room: interrupt every member that is working or queued. */
|
|
918
|
-
async function interrupt_free() {
|
|
919
|
-
const busy = (state.room.free?.members || []).filter(member => member.pending
|
|
920
|
-
|| ["dispatched", "started"].includes(member.execution?.status));
|
|
921
|
-
for (const member of busy) await change_member({ member_id: member.id, action: "interrupt" });
|
|
922
|
-
}
|
|
923
|
-
|
|
924
917
|
/** Invite an independent member, retaining its credential before either delivery path runs. */
|
|
925
918
|
async function invite_member({ name, access, project_access = "read", session_id = "" }) {
|
|
926
919
|
const room_id = state.room.id;
|
|
@@ -1609,7 +1602,7 @@ function participant_model(name, room, saved) {
|
|
|
1609
1602
|
if (!profile) return "";
|
|
1610
1603
|
if (["codex", "antigravity"].includes(profile.adapter)) return "模型与强度由 App 设置";
|
|
1611
1604
|
const model = profile.model || "默认模型";
|
|
1612
|
-
const effort = profile.adapter === "antigravity_cli" ? "
|
|
1605
|
+
const effort = profile.effort || (profile.adapter === "antigravity_cli" ? "模型默认强度" : "默认强度");
|
|
1613
1606
|
return `${model} · ${effort}`;
|
|
1614
1607
|
}
|
|
1615
1608
|
|
|
@@ -1630,6 +1623,7 @@ const MODEL_SOURCE_LABELS = { "codex-app-server": "来自 Codex CLI 模型列表
|
|
|
1630
1623
|
/** Keep discoveries separate for each software executable, shared by its participant instances. */
|
|
1631
1624
|
function model_catalog_key(profile) {
|
|
1632
1625
|
if (profile.adapter === "api") return JSON.stringify([profile.adapter, profile.base_url, profile.api_protocol, profile.api_key_ref]);
|
|
1626
|
+
if (profile.adapter === "antigravity_cli") return JSON.stringify([profile.adapter, profile.executable || "", profile.model || ""]);
|
|
1633
1627
|
return JSON.stringify([profile.adapter, profile.executable || ""]);
|
|
1634
1628
|
}
|
|
1635
1629
|
|
|
@@ -1667,8 +1661,8 @@ function launch_model_state(name, launch = state.launch) {
|
|
|
1667
1661
|
const items = [{ label: "软件默认模型", value: "__default__" }, ...models.map(model => ({ label: model.label || model.id, value: model.id }))];
|
|
1668
1662
|
if (profile.model && !selected) items.push({ label: `${profile.model}(已保存,列表未确认)`, value: profile.model, disabled: true });
|
|
1669
1663
|
return { supported: MODEL_CLIENTS.includes(profile.adapter), items, loading: Boolean(catalog?.loading),
|
|
1670
|
-
efforts: selected?.efforts, source: MODEL_SOURCE_LABELS[catalog?.source] || catalog?.source || "",
|
|
1671
|
-
effort_note: profile.adapter === "antigravity_cli" ? "
|
|
1664
|
+
efforts: profile.adapter === "antigravity_cli" ? catalog?.efforts : selected?.efforts, source: MODEL_SOURCE_LABELS[catalog?.source] || catalog?.source || "",
|
|
1665
|
+
effort_note: profile.adapter === "antigravity_cli" ? catalog?.effort_error || (catalog?.loading ? "正在读取原生思考档位…" : "默认强度由 CLI 决定。") : !selected
|
|
1672
1666
|
? "选择模型后显示可用思考强度;默认值由软件决定。" : "所选模型不提供单独思考强度。",
|
|
1673
1667
|
error: catalog?.error || (!launch.available ? "连接本机服务后读取模型列表" : !catalog ? "尚未读取此软件的模型列表" : "") };
|
|
1674
1668
|
}
|
|
@@ -1680,9 +1674,11 @@ async function load_launch_models(name, force = false) {
|
|
|
1680
1674
|
if (profile.adapter === "api" && (!force || !profile.api_key_ref)) return;
|
|
1681
1675
|
const catalogs = state.launch.model_catalogs ||= {}, key = model_catalog_key(profile);
|
|
1682
1676
|
if (catalogs[key]?.loading || (!force && catalogs[key])) return;
|
|
1683
|
-
const
|
|
1677
|
+
const fallback = profile.adapter === "antigravity_cli" ? catalogs[model_catalog_key({ ...profile, model: "" })] : null;
|
|
1678
|
+
const current = { models: fallback?.models || [], loading: true, error: "", source: "" };
|
|
1684
1679
|
const input = profile.adapter === "api" ? { adapter: "api", base_url: profile.base_url, api_protocol: profile.api_protocol, model: profile.model || "discovery", api_key_ref: profile.api_key_ref }
|
|
1685
|
-
: { adapter: profile.adapter, ...(profile.executable ? { executable: profile.executable } : {}),
|
|
1680
|
+
: { adapter: profile.adapter, ...(profile.executable ? { executable: profile.executable } : {}),
|
|
1681
|
+
...(profile.adapter === "antigravity_cli" && profile.model ? { model: profile.model } : {}), ...(force ? { refresh: true } : {}) };
|
|
1686
1682
|
catalogs[key] = current;
|
|
1687
1683
|
publish_view();
|
|
1688
1684
|
try {
|
|
@@ -1690,6 +1686,8 @@ async function load_launch_models(name, force = false) {
|
|
|
1690
1686
|
if (result.adapter !== input.adapter) throw new Error("模型列表与所选软件不一致");
|
|
1691
1687
|
current.models = result.models || [];
|
|
1692
1688
|
current.source = result.source || "";
|
|
1689
|
+
current.efforts = result.efforts;
|
|
1690
|
+
current.effort_error = result.effort_error || "";
|
|
1693
1691
|
current.error = result.error || (!current.models.length ? "软件未返回可用模型,可使用默认模型" : "");
|
|
1694
1692
|
} catch (error) { current.error = error.message; }
|
|
1695
1693
|
finally {
|
|
@@ -1704,6 +1702,7 @@ function write_launch_model(name, value) {
|
|
|
1704
1702
|
write_launch_field(name, "model", model);
|
|
1705
1703
|
const levels = launch_model_state(name).efforts;
|
|
1706
1704
|
if (!levels?.includes(state.launch.profiles[name].effort)) write_launch_field(name, "effort", "");
|
|
1705
|
+
if (state.launch.profiles[name].adapter === "antigravity_cli") load_launch_models(name);
|
|
1707
1706
|
}
|
|
1708
1707
|
|
|
1709
1708
|
/** Reuse the key last saved on this machine for the same API root instead of asking again. */
|
|
@@ -2206,7 +2205,7 @@ ${instruction}
|
|
|
2206
2205
|
/** How a manual member learns it is its turn: free rooms are pulled by the member, discussions only on the host's assignment. */
|
|
2207
2206
|
function manual_trigger_instruction(room) {
|
|
2208
2207
|
return room.mode === "free" ? "首次复制后,可由人工或软件原生定时触发;自行提交,不必每轮复制回答。"
|
|
2209
|
-
: "这是全自动讨论:只在主持人给你安排发言时才参与,先核对 turn current 是否轮到你,再按参与技能认领发言权并提交;没有安排就结束本次检查。首次提交后,若你运行在 Claude Code 中,本机服务会自动接管你的会话并在后续轮次唤醒你;未被接管时才需要你或软件原生定时触发。";
|
|
2208
|
+
: "这是全自动讨论:只在主持人给你安排发言时才参与,先核对 turn current 是否轮到你,再按参与技能认领发言权并提交;没有安排就结束本次检查。首次提交后,若你运行在 Claude Code 或 Codex 中,本机服务会自动接管你的会话并在后续轮次唤醒你;未被接管时才需要你或软件原生定时触发。";
|
|
2210
2209
|
}
|
|
2211
2210
|
|
|
2212
2211
|
/** Download a private per-room setup file for the independent local runner. */
|
|
@@ -3060,7 +3059,7 @@ async function execute_view_action(name, value) {
|
|
|
3060
3059
|
copy_online: () => copy_context(), copy_cli: () => copy_context("cli"), copy_background,
|
|
3061
3060
|
choose_runtime, export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, force_close, conclude_discussion,
|
|
3062
3061
|
save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
|
|
3063
|
-
load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member,
|
|
3062
|
+
load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member, set_proxy, copy_online_notice,
|
|
3064
3063
|
prepare_brainstorm, return_conclusion, convert_suggestion, adopt_recommendation,
|
|
3065
3064
|
busy: () => { throw new Error("上一个操作仍在处理中,请稍候;长时间无响应请刷新页面"); },
|
|
3066
3065
|
copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };
|