@iamsamyiok/agents-chat 3.18.0

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.
@@ -0,0 +1,479 @@
1
+ // 事项管控(卡牌)核心:数据模型 + 自动注入执行器 + 过程/结果归档
2
+ // 复用 oc.chatSolo(opencode run --format json,支持 -s 续聊)作为单 Agent 执行内核
3
+ // 核心能力:
4
+ // 1) 卡牌增删改查(cards.json,零依赖),order 字段决定看板内顺序(可拖拽调整)
5
+ // 2) 依赖解析:dependsOn 全部完成后才可被调度
6
+ // 3) 双轨注入:mode='continue' 复用上一卡牌的 opencode 会话(同进程续聊);
7
+ // mode='new'/'parallel' 新开进程(各自独立会话)
8
+ // 4) 完成自动感知:oc.chatSolo 的 done 事件触发下一张卡牌注入(无需人工干预)
9
+ // 5) 过程与结果归档:过程事件落入 messages.json(taskId=cardId)+ 流转日志 flow.jsonl
10
+ // 6) 工作区(workspace):可选,指定后 Agent 在该目录读写文件,相关产出集中存放
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { resolveRunner, detectKernels, KERNEL_DEFS, stopScope } = require('./agent');
14
+ const oc = require('./oc');
15
+ const store = require('./store');
16
+
17
+ const ROOT = path.join(__dirname, '..', '..');
18
+ const DATA_DIR = process.env.AGENTS_CHAT_DATA || path.join(ROOT, '.data');
19
+ const CARDS_PATH = path.join(DATA_DIR, 'cards.json');
20
+ const CARDS_CFG_PATH = path.join(DATA_DIR, 'cards_config.json');
21
+ const TRASH_PATH = path.join(DATA_DIR, 'cards_trash.json');
22
+ const TRASH_TTL = 30 * 24 * 3600 * 1000; // 垃圾桶默认保留 30 天
23
+
24
+ function ensureDir() { if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); }
25
+ function readCards() {
26
+ try { return JSON.parse(fs.readFileSync(CARDS_PATH, 'utf8')); } catch { return []; }
27
+ }
28
+ function writeCards(list) {
29
+ ensureDir();
30
+ const tmp = CARDS_PATH + '.tmp';
31
+ fs.writeFileSync(tmp, JSON.stringify(list, null, 2), 'utf8');
32
+ fs.renameSync(tmp, CARDS_PATH);
33
+ }
34
+ function readConfig() {
35
+ try { return JSON.parse(fs.readFileSync(CARDS_CFG_PATH, 'utf8')); } catch { return { workspace: '' }; }
36
+ }
37
+ function writeConfig(cfg) {
38
+ ensureDir();
39
+ fs.writeFileSync(CARDS_CFG_PATH, JSON.stringify(cfg, null, 2), 'utf8');
40
+ }
41
+ function readTrash() {
42
+ try { return JSON.parse(fs.readFileSync(TRASH_PATH, 'utf8')); } catch { return []; }
43
+ }
44
+ function writeTrash(list) {
45
+ ensureDir();
46
+ fs.writeFileSync(TRASH_PATH, JSON.stringify(list, null, 2), 'utf8');
47
+ }
48
+ // 启动时清理超过 30 天的垃圾桶快照,并连带清除其日志,避免占用磁盘
49
+ (function purgeTrash() {
50
+ const list = readTrash();
51
+ if (!list.length) return;
52
+ const now = Date.now();
53
+ const keep = [];
54
+ let purged = 0;
55
+ for (const t of list) {
56
+ if (now - (t.deletedAt || 0) > TRASH_TTL) { try { store.deleteTask(t.card && t.card.id); } catch { /* ignore */ } purged++; }
57
+ else keep.push(t);
58
+ }
59
+ if (purged) writeTrash(keep);
60
+ })();
61
+
62
+ function newId() { return 'c-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6); }
63
+
64
+ // ---------- 卡牌仓储 ----------
65
+ const CardStore = {
66
+ // 看板顺序:order 升序(拖拽调整);无 order 的旧数据兜底按 createdAt
67
+ list() {
68
+ return readCards().sort((a, b) => ((a.order === undefined ? 1e9 : a.order) - (b.order === undefined ? 1e9 : b.order)) || (a.createdAt - b.createdAt));
69
+ },
70
+ get(id) { return readCards().find(c => c.id === id) || null; },
71
+ add(card) {
72
+ const list = readCards();
73
+ const order = list.length ? Math.max(...list.map(c => (c.order === undefined ? 0 : c.order))) + 1 : 1;
74
+ const rec = {
75
+ id: card.id || newId(),
76
+ title: String(card.title || '').slice(0, 500),
77
+ content: String(card.content || '').slice(0, 20000),
78
+ priority: Number(card.priority) || 999,
79
+ status: 'pending',
80
+ mode: ['new', 'continue', 'parallel'].includes(card.mode) ? card.mode : 'new',
81
+ chainId: card.mode === 'continue' ? String(card.chainId || '') : '',
82
+ dependsOn: Array.isArray(card.dependsOn) ? card.dependsOn.map(String) : [],
83
+ model: String(card.model || '').slice(0, 80),
84
+ ocSessionId: '',
85
+ result: '',
86
+ error: '',
87
+ order,
88
+ createdAt: Date.now(),
89
+ updatedAt: Date.now(),
90
+ startedAt: 0,
91
+ finishedAt: 0
92
+ };
93
+ // continue 卡牌隐式依赖其链首卡牌:必须等链首产出会话后再续聊
94
+ if (rec.mode === 'continue' && rec.chainId && !rec.dependsOn.includes(rec.chainId)) {
95
+ rec.dependsOn.push(rec.chainId);
96
+ }
97
+ list.push(rec);
98
+ writeCards(list);
99
+ return rec;
100
+ },
101
+ update(id, patch) {
102
+ const list = readCards();
103
+ const c = list.find(x => x.id === id);
104
+ if (!c) return null;
105
+ Object.assign(c, patch, { updatedAt: Date.now() });
106
+ writeCards(list);
107
+ return c;
108
+ },
109
+ // 拖拽重排:按给定 id 顺序重编 order(ids 为看板当前顺序的全量 id)
110
+ reorder(ids) {
111
+ const list = readCards();
112
+ const map = new Map(list.map(c => [c.id, c]));
113
+ ids.forEach((id, i) => { const c = map.get(id); if (c) c.order = i + 1; });
114
+ writeCards(list);
115
+ return true;
116
+ },
117
+ remove(id) {
118
+ const list = readCards();
119
+ const card = list.find(c => c.id === id);
120
+ if (!card) return false;
121
+ // 软删除:移入垃圾桶(保留过程日志快照),并从看板移除
122
+ const msgs = [];
123
+ try { for (const m of store.getMessages(id)) msgs.push(m); } catch { /* ignore */ }
124
+ const trash = readTrash();
125
+ trash.push({ card: { ...card }, deletedAt: Date.now(), messages: msgs });
126
+ writeTrash(trash);
127
+ const newList = list.filter(c => c.id !== id);
128
+ writeCards(newList);
129
+ // 其余任务若有引用该任务,清理其链/依赖引用
130
+ const others = newList.map(c => {
131
+ let changed = false;
132
+ if (c.chainId === id) { c.chainId = ''; c.mode = 'new'; changed = true; }
133
+ if (Array.isArray(c.dependsOn)) {
134
+ const before = c.dependsOn.length;
135
+ c.dependsOn = c.dependsOn.filter(d => d !== id);
136
+ if (c.dependsOn.length !== before) changed = true;
137
+ }
138
+ return changed ? c : null;
139
+ }).filter(Boolean);
140
+ if (others.length) { for (const o of others) { const t = newList.find(x => x.id === o.id); if (t) Object.assign(t, o); } writeCards(newList); }
141
+ return true;
142
+ },
143
+ getTrash() {
144
+ return readTrash().sort((a, b) => b.deletedAt - a.deletedAt);
145
+ },
146
+ emptyTrash() {
147
+ const trash = readTrash();
148
+ for (const t of trash) { try { store.deleteTask(t.card && t.card.id); } catch { /* ignore */ } }
149
+ writeTrash([]);
150
+ return trash.length;
151
+ },
152
+ restoreFromTrash(id) {
153
+ const trash = readTrash();
154
+ const idx = trash.findIndex(t => t.card && t.card.id === id);
155
+ if (idx < 0) return null;
156
+ const snap = trash[idx];
157
+ const card = { ...snap.card, status: 'pending', ocSessionId: '', result: '', error: '', startedAt: 0, finishedAt: 0 };
158
+ // 还原过程日志
159
+ if (Array.isArray(snap.messages)) for (const m of snap.messages) { try { store.addMessage({ ...m }); } catch { /* ignore */ } }
160
+ const list = readCards();
161
+ const maxOrder = list.length ? Math.max(...list.map(c => (c.order === undefined ? 0 : c.order))) : 0;
162
+ card.order = maxOrder + 1;
163
+ list.push(card);
164
+ writeCards(list);
165
+ trash.splice(idx, 1);
166
+ writeTrash(trash);
167
+ return card;
168
+ },
169
+ resetRunning() {
170
+ const list = readCards();
171
+ let n = 0;
172
+ for (const c of list) if (c.status === 'running') { c.status = 'pending'; c.error = '服务重启,已复位为待执行'; n++; }
173
+ if (n) writeCards(list);
174
+ return n;
175
+ },
176
+ getConfig() { return readConfig(); },
177
+ setConfig(patch) {
178
+ const c = readConfig();
179
+ Object.assign(c, patch);
180
+ writeConfig(c);
181
+ return c;
182
+ }
183
+ };
184
+
185
+ // ---------- 执行器(事件广播 + 自动注入) ----------
186
+ const MAX_PARALLEL = Number(process.env.AGENTS_CHAT_CARD_PARALLEL) > 0 ? Number(process.env.AGENTS_CHAT_CARD_PARALLEL) : 2;
187
+ const subscribers = new Set();
188
+
189
+ function broadcast(ev) {
190
+ for (const send of subscribers) { try { send(ev); } catch { /* closed */ } }
191
+ }
192
+ function sseSubscribe(send) { subscribers.add(send); return () => subscribers.delete(send); }
193
+
194
+ function buildCardPrompt(card, workspace) {
195
+ let p = `【多任务编排 · 任务 ${card.id}】\n标题:${card.title}\n\n任务内容:\n${card.content}\n\n请完成上述任务,并给出明确的结果与(如有)产出文件的完整路径。`;
196
+ if (workspace) p += `\n\n工作目录(请在以下目录读写相关文件):${workspace}`;
197
+ return p;
198
+ }
199
+
200
+ function isEligible(card, all) {
201
+ if (card.status !== 'pending') return false;
202
+ for (const dep of (card.dependsOn || [])) {
203
+ const d = all.find(c => c.id === dep);
204
+ if (!d || d.status !== 'done') return false;
205
+ }
206
+ return true;
207
+ }
208
+
209
+ function pickEligible(all, activeIds) {
210
+ const eligible = all.filter(c => isEligible(c, all) && !activeIds.has(c.id));
211
+ eligible.sort((a, b) => {
212
+ const pa = a.priority === undefined ? 999 : a.priority;
213
+ const pb = b.priority === undefined ? 999 : b.priority;
214
+ if (pa !== pb) return pa - pb;
215
+ return (a.order || 0) - (b.order || 0);
216
+ });
217
+ return eligible;
218
+ }
219
+
220
+ class CardRunner {
221
+ constructor() {
222
+ this.active = new Set();
223
+ this.token = 0;
224
+ this.running = false;
225
+ this.timer = null;
226
+ this.procs = new Map(); // cardId -> { pid, child, lastActive, status }
227
+ }
228
+ isRunning() { return this.running; }
229
+
230
+ // 终止单个任务对应的子进程
231
+ killCard(cardId) {
232
+ this.active.delete(cardId);
233
+ const p = this.procs.get(cardId);
234
+ if (p && p.child) { try { p.child.kill('SIGTERM'); } catch { /* ignore */ } }
235
+ this.procs.delete(cardId);
236
+ }
237
+
238
+ // 供前端展示:当前存活进程 + opencode 是否在工作中(近 5s 有活动即视为工作中)
239
+ getProcesses() {
240
+ const out = [];
241
+ const now = Date.now();
242
+ for (const [cardId, p] of this.procs) {
243
+ out.push({ cardId, pid: p.pid || null, working: (now - (p.lastActive || 0)) < 5000 });
244
+ }
245
+ return out;
246
+ }
247
+
248
+ stop() {
249
+ this.token++;
250
+ this.running = false;
251
+ if (this.timer) { try { this.timer.unref(); } catch { /* ignore */ } }
252
+ try { stopScope('solo'); } catch { /* ignore */ }
253
+ this.procs.clear();
254
+ broadcast({ type: 'notice', content: '⏹ 多任务编排执行已停止,未开始的任务保留待执行' });
255
+ broadcast({ type: 'runner_stopped' });
256
+ }
257
+
258
+ start() {
259
+ if (this.running) return;
260
+ this.running = true;
261
+ this.token++;
262
+ broadcast({ type: 'runner_started' });
263
+ this.tick();
264
+ }
265
+
266
+ tick() {
267
+ if (!this.running) return;
268
+ const myToken = this.token;
269
+ const all = CardStore.list();
270
+ const eligible = pickEligible(all, this.active);
271
+ if (!eligible.length) {
272
+ if (this.active.size === 0) { this.running = false; broadcast({ type: 'all_done' }); }
273
+ return;
274
+ }
275
+ while (this.active.size < MAX_PARALLEL && eligible.length > 0) {
276
+ const card = eligible.shift();
277
+ this.active.add(card.id);
278
+ const p = this.runCard(card, myToken);
279
+ p.finally(() => {
280
+ this.active.delete(card.id);
281
+ if (myToken === this.token) this.tick();
282
+ });
283
+ }
284
+ }
285
+
286
+ async runCard(card, myToken) {
287
+ const runner = resolveRunner();
288
+ if (runner.kind === 'missing') {
289
+ const hint = require('./agent').missingHint(runner);
290
+ CardStore.update(card.id, { status: 'failed', error: hint.slice(0, 2000), finishedAt: Date.now() });
291
+ broadcast({ type: 'task_done', cardId: card.id, status: 'failed', title: card.title });
292
+ return;
293
+ }
294
+ if (runner.kind === 'demo') {
295
+ CardStore.update(card.id, { status: 'failed', error: '演示模式下任务执行不可用,请安装 opencode/claude/codex/pi 内核', finishedAt: Date.now() });
296
+ broadcast({ type: 'task_done', cardId: card.id, status: 'failed', title: card.title });
297
+ return;
298
+ }
299
+
300
+ let ocSessionId = '';
301
+ if (card.mode === 'continue' && card.chainId) {
302
+ const prev = CardStore.get(card.chainId);
303
+ ocSessionId = prev && prev.ocSessionId ? prev.ocSessionId : '';
304
+ }
305
+
306
+ CardStore.update(card.id, { status: 'running', startedAt: Date.now(), error: '', result: '' });
307
+ broadcast({ type: 'task_start', cardId: card.id, title: card.title, mode: card.mode, ocSessionId });
308
+
309
+ const kind = runner.kind === 'opencode' ? 'opencode' : 'fallback';
310
+ const cfg = CardStore.getConfig();
311
+ const workspace = (cfg.workspace || '').trim();
312
+ const prompt = buildCardPrompt(card, workspace && isValidDir(workspace) ? workspace : '');
313
+ const texts = new Map();
314
+ const order = [];
315
+ let doneError = '';
316
+ let sesId = ocSessionId;
317
+
318
+ let child = null;
319
+ try {
320
+ await new Promise((resolve) => {
321
+ child = oc.chatSolo(kind, runner, {
322
+ prompt,
323
+ model: card.model || '',
324
+ ocSessionId: sesId,
325
+ behavior: 'card',
326
+ cwd: workspace && isValidDir(workspace) ? workspace : undefined
327
+ }, (ev) => {
328
+ const proc = this.procs.get(card.id);
329
+ if (proc) { proc.lastActive = Date.now(); }
330
+ if (ev.type === 'session') {
331
+ sesId = ev.ocSessionId;
332
+ CardStore.update(card.id, { ocSessionId: sesId });
333
+ broadcast({ type: 'session', cardId: card.id, ocSessionId: sesId });
334
+ } else if (ev.type === 'text') {
335
+ if (!texts.has(ev.partId)) order.push(ev.partId);
336
+ texts.set(ev.partId, ev.text);
337
+ broadcast({ type: 'text', cardId: card.id, partId: ev.partId, text: ev.text, agentName: 'Agent', phase: 'work' });
338
+ } else if (ev.type === 'reasoning') {
339
+ broadcast({ type: 'reasoning', cardId: card.id, partId: ev.partId, text: ev.text });
340
+ } else if (ev.type === 'tool') {
341
+ broadcast({ type: 'tool', cardId: card.id, name: ev.name, summary: ev.summary });
342
+ store.addMessage({ role: 'assistant', agentId: 'solo', agentName: 'Agent', actor: 'assistant', phase: 'work', taskId: card.id, content: `[工具] ${ev.name}${ev.summary ? '(' + ev.summary + ')' : ''} 执行完成` });
343
+ } else if (ev.type === 'done') {
344
+ doneError = ev.error || '';
345
+ resolve();
346
+ }
347
+ });
348
+ });
349
+ } catch (err) {
350
+ doneError = String((err && err.message) || err).slice(0, 2000);
351
+ }
352
+
353
+ // 登记进程信息(PID + 是否工作中),并广播给前端
354
+ this.procs.set(card.id, { pid: child ? child.pid : null, child, lastActive: Date.now() });
355
+ broadcast({ type: 'proc', cardId: card.id, pid: child ? child.pid : null });
356
+
357
+ const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
358
+ const stopped = myToken !== this.token;
359
+ if (finalText) {
360
+ store.addMessage({ role: 'assistant', agentId: 'solo', agentName: 'Agent', actor: 'assistant', phase: 'work', taskId: card.id, content: finalText.slice(0, 20000) });
361
+ }
362
+ const status = stopped ? 'pending' : (doneError ? 'failed' : 'done');
363
+ CardStore.update(card.id, {
364
+ status,
365
+ ocSessionId: sesId,
366
+ result: (doneError ? `执行出错:${doneError}` : finalText).slice(0, 20000),
367
+ error: doneError || '',
368
+ finishedAt: Date.now()
369
+ });
370
+ broadcast({ type: 'task_done', cardId: card.id, status, title: card.title });
371
+ // 任务结束,移除进程登记
372
+ this.procs.delete(card.id);
373
+ }
374
+
375
+ async runOne(cardId) {
376
+ const card = CardStore.get(cardId);
377
+ if (!card || card.status === 'running') return false;
378
+ this.active.add(cardId);
379
+ const myToken = this.token;
380
+ await this.runCard(card, myToken).catch(() => {});
381
+ this.active.delete(cardId);
382
+ return true;
383
+ }
384
+
385
+ // ---------- 任务完成后追加聊天:复用该卡牌的 opencode 会话(-s 续聊,等效同一进程第二轮输入) ----------
386
+ // 过程事件经 broadcast 推送(followup_start / text / tool / followup_done),回复归档进消息与 result
387
+ async chatFollowup(cardId, prompt) {
388
+ const card = CardStore.get(cardId);
389
+ if (!card) return { ok: false, error: '任务不存在' };
390
+ if (card.status === 'running' || card.status === 'pending') return { ok: false, error: '任务尚未执行完成,先运行任务再追加聊天' };
391
+ if (this.followups && this.followups.has(cardId)) return { ok: false, error: '该任务已有追加聊天进行中' };
392
+ if (!this.followups) this.followups = new Set();
393
+ this.followups.add(cardId);
394
+
395
+ const runner = resolveRunner();
396
+ try {
397
+ if (runner.kind === 'missing') {
398
+ const hint = require('./agent').missingHint(runner);
399
+ return { ok: false, error: hint.slice(0, 500) };
400
+ }
401
+ const kind = runner.kind === 'opencode' ? 'opencode' : 'fallback';
402
+ if (!card.ocSessionId && kind === 'opencode') {
403
+ return { ok: false, error: '该任务没有可续的会话(可能未通过 opencode 内核执行),无法追加聊天' };
404
+ }
405
+ const cfg = CardStore.getConfig();
406
+ const workspace = (cfg.workspace || '').trim();
407
+ const cwd = workspace && isValidDir(workspace) ? workspace : undefined;
408
+
409
+ const userText = String(prompt || '').trim();
410
+ if (!userText) return { ok: false, error: '请输入追加内容' };
411
+ store.addMessage({ role: 'user', agentId: 'solo', agentName: '我', actor: 'user', phase: 'followup', taskId: cardId, content: userText.slice(0, 20000) });
412
+ broadcast({ type: 'followup_start', cardId: cardId });
413
+
414
+ const texts = new Map();
415
+ const order = [];
416
+ let doneError = '';
417
+ let sesId = card.ocSessionId || '';
418
+ let child = null;
419
+ try {
420
+ await new Promise((resolve) => {
421
+ child = oc.chatSolo(kind, runner, {
422
+ prompt: userText,
423
+ model: card.model || '',
424
+ ocSessionId: sesId,
425
+ behavior: 'card',
426
+ cwd
427
+ }, (ev) => {
428
+ const proc = this.procs.get(cardId);
429
+ if (proc) proc.lastActive = Date.now();
430
+ if (ev.type === 'session') {
431
+ sesId = ev.ocSessionId;
432
+ CardStore.update(cardId, { ocSessionId: sesId });
433
+ } else if (ev.type === 'text') {
434
+ if (!texts.has(ev.partId)) order.push(ev.partId);
435
+ texts.set(ev.partId, ev.text);
436
+ broadcast({ type: 'text', cardId, partId: ev.partId, text: ev.text, agentName: 'Agent', phase: 'followup' });
437
+ } else if (ev.type === 'tool') {
438
+ broadcast({ type: 'tool', cardId, name: ev.name, summary: ev.summary, phase: 'followup' });
439
+ store.addMessage({ role: 'assistant', agentId: 'solo', agentName: 'Agent', actor: 'assistant', phase: 'followup', taskId: cardId, content: `[工具] ${ev.name}${ev.summary ? '(' + ev.summary + ')' : ''} 执行完成` });
440
+ } else if (ev.type === 'done') {
441
+ doneError = ev.error || '';
442
+ resolve();
443
+ }
444
+ });
445
+ });
446
+ } catch (err) {
447
+ doneError = String((err && err.message) || err).slice(0, 2000);
448
+ }
449
+
450
+ const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
451
+ if (finalText) {
452
+ store.addMessage({ role: 'assistant', agentId: 'solo', agentName: 'Agent', actor: 'assistant', phase: 'followup', taskId: cardId, content: finalText.slice(0, 20000) });
453
+ }
454
+ // 追加聊天完成后:结果滚动归档(保留此前结果,追加本轮回复),失败原因单独记录
455
+ const cur = CardStore.get(cardId) || {};
456
+ const merged = [cur.result, finalText].filter(Boolean).join('\n\n');
457
+ CardStore.update(cardId, {
458
+ ocSessionId: sesId,
459
+ result: merged.slice(-20000),
460
+ followupError: doneError || '',
461
+ finishedAt: Date.now()
462
+ });
463
+ broadcast({ type: 'followup_done', cardId, error: doneError || '' });
464
+ return { ok: !doneError, error: doneError || '' };
465
+ } finally {
466
+ this.followups.delete(cardId);
467
+ }
468
+ }
469
+
470
+ isFollowupRunning(cardId) { return !!this.followups && this.followups.has(cardId); }
471
+ }
472
+
473
+ function isValidDir(p) {
474
+ try { return fs.existsSync(p) && fs.statSync(p).isDirectory(); } catch { return false; }
475
+ }
476
+
477
+ const runner = new CardRunner();
478
+
479
+ module.exports = { CardStore, CardRunner, runner, sseSubscribe, buildCardPrompt, isEligible, pickEligible, MAX_PARALLEL, CARDS_PATH, CARDS_CFG_PATH };
package/app/lib/env.js ADDED
@@ -0,0 +1,47 @@
1
+ // 零依赖 .env 加载器:解析 KEY=VALUE,注入 process.env(不覆盖已有值)
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const PLACEHOLDER_PATTERNS = [/sk-在这里填入你的密钥/, /^your-api-key/i, /^changeme$/i, /^$/];
6
+
7
+ function parseEnv(text) {
8
+ const out = {};
9
+ for (const raw of String(text || '').split(/\r?\n/)) {
10
+ const line = raw.trim();
11
+ if (!line || line.startsWith('#')) continue;
12
+ const eq = line.indexOf('=');
13
+ if (eq <= 0) continue;
14
+ const key = line.slice(0, eq).trim();
15
+ let val = line.slice(eq + 1).trim();
16
+ // 剥离成对引号
17
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
18
+ val = val.slice(1, -1);
19
+ }
20
+ // 行内注释(仅对未加引号的值)
21
+ out[key] = val;
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function loadEnv(envPath) {
27
+ let raw = '';
28
+ try { raw = fs.readFileSync(envPath, 'utf8'); } catch { return {}; }
29
+ const parsed = parseEnv(raw);
30
+ for (const [k, v] of Object.entries(parsed)) {
31
+ if (process.env[k] === undefined && v !== '') process.env[k] = v;
32
+ }
33
+ return parsed;
34
+ }
35
+
36
+ // API 配置是否有效(旧版 pi 内核遗留:OpenCode 内核下认证由 opencode 自身管理,
37
+ // 此函数仅供兼容,内核判定不再依赖它)
38
+ function isApiConfigured(env) {
39
+ const e = env || {};
40
+ const base = e.AGENTS_CHAT_BASE_URL || process.env.AGENTS_CHAT_BASE_URL || '';
41
+ const model = e.AGENTS_CHAT_MODEL || process.env.AGENTS_CHAT_MODEL || '';
42
+ const key = e.AGENTS_CHAT_API_KEY || process.env.AGENTS_CHAT_API_KEY || '';
43
+ if (!base || !model || !key) return false;
44
+ return !PLACEHOLDER_PATTERNS.some(re => re.test(key));
45
+ }
46
+
47
+ module.exports = { loadEnv, parseEnv, isApiConfigured };