ai_co_discuss 0.2.13 → 0.2.15

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.
@@ -12,6 +12,8 @@ const CALL_TIMEOUT_MS = 30_000;
12
12
  const CLI_DIAGNOSTIC_CHARS = 1000;
13
13
  /** Codex exec may linger after its final event; the finished turn is already saved, so stop it to free the session. */
14
14
  const CLI_EXIT_GRACE_MS = 1500;
15
+ // A hung CLI keeps its lease renewed forever when the room sets no execution limit, so cap every process.
16
+ const CLI_MAX_RUN_MS = 2 * 60 * 60_000;
15
17
  const CODEX_PERMISSIONS_MIN_MINOR = 163;
16
18
  /** Website subcommands an automatic task may run; service, launch, run, api and setup stay unapproved. */
17
19
  export const TASK_SUBCOMMANDS = ['room', 'message', 'turn', 'state', 'floor', 'execution', 'health', 'schema'];
@@ -416,6 +418,8 @@ function monitor_cli(child, config, binding, task) {
416
418
  const state = { child, session_id: binding.session_id || '', initialized: false, completed: false, error: '', diagnostic: '' };
417
419
  const log_path = resolve(task.directory, `${config.adapter}-${task.id}.log`);
418
420
  const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
421
+ const max_run_ms = Number(process.env.CODISCUSS_CLI_MAX_MS) || CLI_MAX_RUN_MS;
422
+ state.limit_timer = setTimeout(() => { state.error = `CLI 运行超过 ${Math.round(max_run_ms / 1000)} 秒仍未结束,已终止`; child.kill(); }, max_run_ms);
419
423
  child.stdout.on('data', chunk => task.on_output(log_path, chunk));
420
424
  child.stderr.setEncoding('utf8');
421
425
  child.stderr.on('data', chunk => {
@@ -434,6 +438,7 @@ function monitor_cli(child, config, binding, task) {
434
438
  });
435
439
  child.once('close', (code, signal) => {
436
440
  clearTimeout(state.finish_timer);
441
+ clearTimeout(state.limit_timer);
437
442
  task.on_trace?.('cli_closed', { code, signal, killed_after_completed: Boolean(state.finished) });
438
443
  if (!state.spawned) return;
439
444
  const error = cli_exit_error(state, code, signal);
@@ -537,6 +542,7 @@ export function setup_sidecar(config, root = join(homedir(), '.gemini', 'config'
537
542
 
538
543
  /** Select only allowlisted adapter types from local configuration. */
539
544
  export async function deliver_task(config, binding, task, spawn_process = spawn, user_home = homedir()) {
545
+ if (config.adapter === 'api') return (await import('./api_participant.mjs')).api_delivery(config, binding, task);
540
546
  const adapters = { codex: codex_delivery, claude: claude_delivery, antigravity: antigravity_delivery,
541
547
  codex_cli: cli_delivery, antigravity_cli: cli_delivery };
542
548
  if (!adapters[config.adapter]) throw new Error('不支持的软件适配器');
@@ -0,0 +1,146 @@
1
+ /** Provider presets and independent wire protocols; model IDs are never rewritten. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ export const API_PROVIDERS = {
7
+ Codex: { label: 'OpenAI', base_url: 'https://api.openai.com/v1', api_protocol: 'responses', models: ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna'] },
8
+ Claude: { label: 'Anthropic', base_url: 'https://api.anthropic.com/v1', api_protocol: 'anthropic', models: ['claude-sonnet-5-5', 'claude-opus-5-5', 'claude-haiku-5-5', 'claude-fable-5-1'] },
9
+ Gemini: { label: 'Google', base_url: 'https://generativelanguage.googleapis.com/v1beta', api_protocol: 'gemini', models: ['gemini-4-argon', 'gemini-3.8-flash'] },
10
+ DeepSeek: { label: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', api_protocol: 'chat', models: ['deepseek-flash', 'deepseek-v4-pro'] },
11
+ Qwen: { label: '阿里云百炼', base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', api_protocol: 'chat', models: ['qwen3.8-flash', 'qwen3.7-plus', 'qwen3.8-max'] },
12
+ Grok: { label: 'xAI', base_url: 'https://api.x.ai/v1', api_protocol: 'responses', models: ['grok-4.7'] },
13
+ };
14
+ const PROTOCOLS = ['responses', 'chat', 'anthropic', 'gemini'];
15
+ const REQUEST_TIMEOUT_MS = 180_000;
16
+ const MAX_OUTPUT_TOKENS = 8192;
17
+
18
+ /** Validate an explicit API root, keeping custom gateway paths intact. */
19
+ export function api_base_url(value) {
20
+ const url = new URL(value);
21
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('Base URL 须为不含凭据、查询或片段的 HTTP(S) 地址');
22
+ return url.href.replace(/\/+$/, '');
23
+ }
24
+
25
+ /** Check protocol/model independently of provider identity. */
26
+ export function validate_api_profile(profile) {
27
+ api_base_url(profile.base_url);
28
+ if (!PROTOCOLS.includes(profile.api_protocol)) throw new Error('请选择 API 请求协议');
29
+ if (!profile.model || typeof profile.model !== 'string') throw new Error('请填写 API 模型 ID');
30
+ if (!/^[a-f0-9-]{36}$/i.test(profile.api_key_ref || '')) throw new Error('请保存此 Base URL 对应的 API Key');
31
+ }
32
+
33
+ /** Save a key privately; browsers and room configuration receive only an opaque reference. */
34
+ export function save_api_key(home, input) {
35
+ const base_url = api_base_url(input.base_url);
36
+ const api_key = typeof input.api_key === 'string' ? input.api_key.trim() : '';
37
+ if (!api_key || /[\r\n]/.test(api_key)) throw new Error('API Key 不能为空或包含换行');
38
+ const api_key_ref = randomUUID();
39
+ const directory = join(home, 'api-keys');
40
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
41
+ writeFileSync(join(directory, `${api_key_ref}.json`), JSON.stringify({ base_url, api_key }), { flag: 'wx', mode: 0o600 });
42
+ return { api_key_ref };
43
+ }
44
+
45
+ /** Resolve only a validated reference bound to this exact API root. */
46
+ export function read_api_key(home, profile) {
47
+ validate_api_profile(profile);
48
+ let saved;
49
+ try { saved = JSON.parse(readFileSync(join(home, 'api-keys', `${profile.api_key_ref}.json`), 'utf8')); }
50
+ catch { throw new Error('本机 API Key 不存在,请重新保存'); }
51
+ if (saved.base_url !== api_base_url(profile.base_url)) throw new Error('Base URL 已变化,请为新地址重新填写 API Key');
52
+ return saved.api_key;
53
+ }
54
+
55
+ /** Convert neutral tool definitions to one provider's request body. */
56
+ export function api_request(profile, context) {
57
+ const { system, history, tools } = context;
58
+ const model = profile.model;
59
+ const functions = tools.map(tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
60
+ if (profile.api_protocol === 'responses') return { path: '/responses', body: { model, instructions: system, input: history,
61
+ tools: functions.map(tool => ({ type: 'function', ...tool, strict: false })), store: false,
62
+ include: ['reasoning.encrypted_content'], max_output_tokens: MAX_OUTPUT_TOKENS } };
63
+ if (profile.api_protocol === 'anthropic') return { path: '/messages', body: { model, system, messages: history,
64
+ tools: functions.map(({ parameters, ...tool }) => ({ ...tool, input_schema: parameters })), max_tokens: MAX_OUTPUT_TOKENS } };
65
+ if (profile.api_protocol === 'gemini') return { path: `/models/${encodeURIComponent(model.replace(/^models\//, ''))}:generateContent`, body: {
66
+ systemInstruction: { parts: [{ text: system }] }, contents: history,
67
+ tools: [{ functionDeclarations: functions }], generationConfig: { maxOutputTokens: MAX_OUTPUT_TOKENS } } };
68
+ return { path: '/chat/completions', body: { model, messages: [{ role: 'system', content: system }, ...history],
69
+ tools: functions.map(tool => ({ type: 'function', function: tool })) } };
70
+ }
71
+
72
+ /** Build authentication without encoding keys into URLs. */
73
+ function api_headers(protocol, key) {
74
+ if (protocol === 'anthropic') return { 'x-api-key': key, 'anthropic-version': '2023-06-01' };
75
+ if (protocol === 'gemini') return { 'x-goog-api-key': key };
76
+ return { Authorization: `Bearer ${key}` };
77
+ }
78
+
79
+ /** A bounded request; errors expose only status, never provider bodies or authorization. */
80
+ async function provider_fetch(profile, key, request, signal) {
81
+ let response;
82
+ try {
83
+ response = await fetch(`${api_base_url(profile.base_url)}${request.path}`, {
84
+ method: request.body === undefined ? 'GET' : 'POST', redirect: 'error',
85
+ headers: { 'Content-Type': 'application/json', ...api_headers(profile.api_protocol, key) },
86
+ body: request.body === undefined ? undefined : JSON.stringify(request.body),
87
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS),
88
+ });
89
+ } catch { throw new Error(signal?.aborted ? 'API 执行已取消' : 'API 请求超时或连接失败,请核对 Base URL'); }
90
+ if (!response.ok) throw new Error(`模型 API 返回 HTTP ${response.status},请核对地址、密钥、模型和请求协议`);
91
+ try { return await response.json(); } catch { throw new Error('模型 API 返回的内容不是有效 JSON'); }
92
+ }
93
+
94
+ /** Fetch model IDs as returned by the provider; manual entry remains available. */
95
+ export async function api_models(home, profile) {
96
+ const key = read_api_key(home, profile);
97
+ const data = await provider_fetch(profile, key, { path: '/models' });
98
+ const models = (data.data || data.models || []).map(model => ({ id: model.id || model.name?.replace(/^models\//, ''), label: model.displayName || model.name || model.id }))
99
+ .filter(model => typeof model.id === 'string');
100
+ return { adapter: 'api', models, error: '', source: 'api' };
101
+ }
102
+
103
+ /** Start protocol-specific history with a user request. */
104
+ export function api_history(profile, prompt) {
105
+ return profile.api_protocol === 'gemini' ? [{ role: 'user', parts: [{ text: prompt }] }] : [{ role: 'user', content: prompt }];
106
+ }
107
+
108
+ /** Preserve raw reasoning/signature blocks for subsequent tool turns, without displaying them. */
109
+ export function api_response(profile, data, history) {
110
+ if (profile.api_protocol === 'responses') {
111
+ if (data.status === 'incomplete' || data.error) throw new Error('模型 API 未完成回答');
112
+ const output = data.output || [];
113
+ history.push(...output);
114
+ return output.filter(item => item.type === 'function_call').map(item => ({ id: item.call_id, name: item.name, arguments: item.arguments }));
115
+ }
116
+ if (profile.api_protocol === 'anthropic') {
117
+ if (data.stop_reason === 'max_tokens') throw new Error('模型 API 输出超过单次长度限制');
118
+ history.push({ role: 'assistant', content: data.content });
119
+ return (data.content || []).filter(item => item.type === 'tool_use').map(item => ({ id: item.id, name: item.name, arguments: item.input }));
120
+ }
121
+ if (profile.api_protocol === 'gemini') {
122
+ const candidate = data.candidates?.[0];
123
+ if (!candidate?.content || candidate.finishReason === 'MAX_TOKENS') throw new Error('模型 API 未完成回答');
124
+ history.push(candidate.content);
125
+ return (candidate.content.parts || []).filter(item => item.functionCall).map(item => ({ name: item.functionCall.name, arguments: item.functionCall.args }));
126
+ }
127
+ const choice = data.choices?.[0];
128
+ if (!choice?.message || choice.finish_reason === 'length') throw new Error('模型 API 未完成回答');
129
+ history.push(choice.message);
130
+ return (choice.message.tool_calls || []).map(item => ({ id: item.id, name: item.function.name, arguments: item.function.arguments }));
131
+ }
132
+
133
+ /** Append all results together so parallel tool calls retain their protocol's pairing. */
134
+ export function api_tool_results(profile, history, results) {
135
+ if (profile.api_protocol === 'anthropic') return history.push({ role: 'user', content: results.map(item => ({ type: 'tool_result', tool_use_id: item.id, content: JSON.stringify(item.result) })) });
136
+ if (profile.api_protocol === 'gemini') return history.push({ role: 'user', parts: results.map(item => ({ functionResponse: { name: item.name, response: { result: item.result } } })) });
137
+ history.push(...results.map(item => profile.api_protocol === 'responses'
138
+ ? { type: 'function_call_output', call_id: item.id, output: JSON.stringify(item.result) }
139
+ : { role: 'tool', tool_call_id: item.id, content: JSON.stringify(item.result) }));
140
+ }
141
+
142
+ /** Generate one tool-call turn with the selected wire protocol. */
143
+ export async function api_generate(profile, key, context, signal) {
144
+ const data = await provider_fetch(profile, key, api_request(profile, context), signal);
145
+ return api_response(profile, data, context.history);
146
+ }