@yvhitxcel/opencode-remote 0.16.2 → 0.17.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.
@@ -11,6 +11,57 @@ import { homedir } from 'os';
11
11
  const CONFIG_DIR = join(homedir(), '.opencode-remote');
12
12
  const CONFIG_FILE = join(CONFIG_DIR, '.env');
13
13
 
14
+ const threadModels = new Map();
15
+ const recentModels = [];
16
+ let rawDebugEnabled = false;
17
+ let thinkVisibleEnabled = false;
18
+
19
+ export function setRawDebug(enabled) {
20
+ rawDebugEnabled = enabled;
21
+ console.log(`[rawDebug] ${enabled ? 'ON' : 'OFF'}`);
22
+ }
23
+ export function isRawDebug() {
24
+ return rawDebugEnabled || process.env.DEBUG_RAW === '1';
25
+ }
26
+ export function setThinkVisible(enabled) {
27
+ thinkVisibleEnabled = enabled;
28
+ console.log(`[think] ${enabled ? 'ON' : 'OFF'}`);
29
+ }
30
+ export function isThinkVisible() {
31
+ return thinkVisibleEnabled;
32
+ }
33
+ export function setThreadModel(threadId, modelStr) {
34
+ if (!modelStr || !modelStr.includes('/')) {
35
+ threadModels.delete(threadId);
36
+ return null;
37
+ }
38
+ const parts = modelStr.split('/');
39
+ const entry = { providerID: parts[0], modelID: parts.slice(1).join('/') };
40
+ threadModels.set(threadId, entry);
41
+ pushRecent(entry);
42
+ return entry;
43
+ }
44
+
45
+ export function getThreadModel(threadId) {
46
+ return threadModels.get(threadId);
47
+ }
48
+
49
+ export function getRecentModels() {
50
+ return [...recentModels];
51
+ }
52
+
53
+ export function pushRecentModel(entry) {
54
+ pushRecent(entry);
55
+ }
56
+
57
+ function pushRecent(entry) {
58
+ const key = `${entry.providerID}/${entry.modelID}`;
59
+ const idx = recentModels.findIndex(e => `${e.providerID}/${e.modelID}` === key);
60
+ if (idx !== -1) recentModels.splice(idx, 1);
61
+ recentModels.unshift(entry);
62
+ if (recentModels.length > 5) recentModels.length = 5;
63
+ }
64
+
14
65
  // Find opencode.exe binary
15
66
  function findOpenCodeExe() {
16
67
  const isWindows = platform() === 'win32';
@@ -174,7 +225,6 @@ let opencodeInstance = null;
174
225
  let opencodeServer = null;
175
226
  let lastStdoutTime = 0;
176
227
  let lastStdoutLine = '';
177
- let lastReportedStatus = '';
178
228
  const PORTS_TO_TRY = [4096, 4097, 4098];
179
229
 
180
230
  // TCP-level port probe: true = occupied, false = free
@@ -190,7 +240,7 @@ function probeTCP(port, timeoutMs = 2000) {
190
240
  }
191
241
 
192
242
  async function tryConnectPort(port, timeoutMs = 5000) {
193
- const { createOpencodeClient } = await import('@opencode-ai/sdk');
243
+ const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
194
244
  const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
195
245
  const result = await Promise.race([
196
246
  client.session.list(),
@@ -255,7 +305,7 @@ export async function initOpenCode() {
255
305
  opencodeServer.on('exit', (code) => console.log(`[opencode] exited with code ${code}`));
256
306
 
257
307
  // Wait for server to be ready
258
- const { createOpencodeClient } = await import('@opencode-ai/sdk');
308
+ const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
259
309
  for (let i = 0; i < 15; i++) {
260
310
  await new Promise(r => setTimeout(r, 1000));
261
311
  try {
@@ -318,7 +368,7 @@ export async function createSession(_threadId, title = `Remote control session`)
318
368
  const opencode = await initOpenCode();
319
369
  try {
320
370
  const createResult = await opencode.client.session.create({
321
- body: { title },
371
+ title,
322
372
  });
323
373
  if (createResult.error) {
324
374
  console.error('Failed to create session:', createResult.error);
@@ -329,7 +379,7 @@ export async function createSession(_threadId, title = `Remote control session`)
329
379
  let shareUrl;
330
380
  if (process.env.SHARE_SESSIONS === 'true') {
331
381
  const shareResult = await opencode.client.session.share({
332
- path: { id: sessionId }
382
+ sessionID: sessionId,
333
383
  });
334
384
  if (!shareResult.error && shareResult.data?.share?.url) {
335
385
  shareUrl = shareResult.data.share.url;
@@ -349,14 +399,13 @@ export async function createSession(_threadId, title = `Remote control session`)
349
399
  }
350
400
  }
351
401
  // Send message - use promptAsync then poll for response
352
- export async function sendMessage(session, message, callbacks) {
353
- const TIMEOUT_MS = 5 * 60 * 1000; // 5 minute timeout
354
- const POLL_INTERVAL = 2000; // 2 seconds between polls
355
-
402
+ export async function sendMessage(session, message, callbacks, threadId) {
403
+ const TIMEOUT_MS = 5 * 60 * 1000;
404
+
356
405
  try {
357
406
  // Verify session is valid first
358
407
  try {
359
- const sessionCheck = await session.client.session.get({ path: { id: session.sessionId } });
408
+ const sessionCheck = await session.client.session.get({ sessionID: session.sessionId });
360
409
  if (sessionCheck.error) {
361
410
  console.error('[sendMessage] Session error:', sessionCheck.error);
362
411
  return '❌ 会话无效,请发送 /restart 重启';
@@ -365,22 +414,16 @@ export async function sendMessage(session, message, callbacks) {
365
414
  console.error('[sendMessage] Session check failed:', e.message);
366
415
  return '❌ 会话连接失败,请发送 /restart 重启';
367
416
  }
368
-
369
- // Get last message ID and count before sending
370
- let lastMsgId = null;
371
- let msgCountBefore = 0;
372
- try {
373
- const msgsBefore = await session.client.session.messages({ path: { id: session.sessionId } });
374
- if (msgsBefore.data?.length > 0) {
375
- lastMsgId = msgsBefore.data[msgsBefore.data.length - 1].info?.id;
376
- msgCountBefore = msgsBefore.data.length;
377
- }
378
- } catch { /* ignore */ }
379
-
380
- // Send message using promptAsync (non-blocking)
417
+
418
+ // Build prompt body
381
419
  const promptBody = {
382
420
  parts: [{ type: 'text', text: message }]
383
421
  };
422
+ // Inject local model preference if set
423
+ if (threadId && threadModels.has(threadId)) {
424
+ session.model = threadModels.get(threadId);
425
+ pushRecent(session.model);
426
+ }
384
427
  // Per-message model override if set on session
385
428
  if (session.model?.providerID && session.model?.modelID) {
386
429
  promptBody.model = {
@@ -388,124 +431,101 @@ export async function sendMessage(session, message, callbacks) {
388
431
  modelID: session.model.modelID,
389
432
  };
390
433
  }
391
- const sendResult = await session.client.session.promptAsync({
392
- path: { id: session.sessionId },
393
- body: promptBody,
394
- });
395
434
 
396
- // Poll for new response - keep going as long as new content keeps arriving
397
- const startTime = Date.now();
398
- let responseText = '';
399
- let hasToolActivity = false;
400
- let idleSince = 0; // 最后一次收到新内容的时间戳
401
- let lastStatus = '';
435
+ // Stream the response via session.prompt (POST /session/{sessionID}/message)
436
+ const abortController = new AbortController();
437
+ const timeoutId = setTimeout(() => abortController.abort(), TIMEOUT_MS);
402
438
 
403
- while (Date.now() - startTime < TIMEOUT_MS) {
404
- await new Promise(r => setTimeout(r, POLL_INTERVAL));
439
+ try {
440
+ const response = await session.client.session.prompt({
441
+ sessionID: session.sessionId,
442
+ parts: promptBody.parts,
443
+ ...(promptBody.model ? { model: promptBody.model } : {}),
444
+ }, {
445
+ parseAs: 'stream',
446
+ signal: abortController.signal,
447
+ });
405
448
 
406
- try {
407
- const msgsResult = await session.client.session.messages({
408
- path: { id: session.sessionId }
409
- });
449
+ if (response.error) {
450
+ return `❌ 发送失败: ${response.error}`;
451
+ }
410
452
 
411
- if (msgsResult.error) { console.error('[sendMessage] Messages error:', msgsResult.error); break; }
412
- if (!msgsResult.data?.length) continue;
453
+ const stream = response.data;
454
+ if (!stream) {
455
+ return '❌ 未收到响应流';
456
+ }
413
457
 
414
- const messages = msgsResult.data;
458
+ const reader = stream.getReader();
459
+ const decoder = new TextDecoder();
460
+ let rawJson = '';
461
+ let responseText = '';
415
462
 
416
- // 工具活动
417
- for (let i = msgCountBefore; i < messages.length; i++) {
418
- const msg = messages[i];
419
- if (msg.parts) for (const part of msg.parts) {
420
- if (part.type === 'tool_use' || part.type === 'tool_result') {
421
- hasToolActivity = true;
422
- callbacks?.onEvent?.({ type: 'tool.call', properties: { name: part.name || part.tool_name || 'unknown', input: part.input || {} } });
423
- break;
424
- }
425
- }
426
- if (hasToolActivity) break;
427
- }
463
+ while (true) {
464
+ const { done, value } = await reader.read();
465
+ if (done) break;
466
+ const chunk = decoder.decode(value, { stream: true });
467
+ if (!chunk) continue;
468
+ rawJson += chunk;
469
+ }
428
470
 
429
- // 收集所有新的 assistant 回复(累加,不丢内容)
430
- if (lastMsgId) {
431
- const idx = messages.findIndex(m => m.info?.id === lastMsgId);
432
- const startIdx = idx >= 0 ? idx + 1 : 0;
433
- const newParts = [];
434
- for (let i = startIdx; i < messages.length; i++) {
435
- const msg = messages[i];
436
- if (msg.info?.role === 'assistant' && msg.parts) {
437
- for (const p of msg.parts) {
438
- if (p.type === 'text' && p.text) newParts.push(p.text);
471
+ // Parse the full response JSON
472
+ if (isRawDebug()) console.log('[RAW]', rawJson);
473
+ try {
474
+ const parsed = JSON.parse(rawJson);
475
+ const t = parsed.info?.tokens || {};
476
+ const time = parsed.info?.time || {};
477
+ const elapsed = time.completed && time.created ? `${(time.completed - time.created) / 1000}s` : '?';
478
+ const cacheRead = t.cache?.read || 0;
479
+ const cacheWrite = t.cache?.write || 0;
480
+ const cacheRate = cacheRead + cacheWrite > 0 ? `${(cacheRead / (cacheRead + cacheWrite) * 100).toFixed(0)}%` : '-';
481
+ console.log(`[RESPONSE] ${parsed.info?.providerID}/${parsed.info?.modelID} │ ${elapsed} │ tokens=${t.total || '?'} (in=${t.input} out=${t.output} rsn=${t.reasoning}) │ cache ${cacheRate} │ finish=${parsed.info?.finish || '?'}`);
482
+ const meta = { modelID: parsed.info?.modelID, providerID: parsed.info?.providerID, tokens: t, parts: parsed.parts };
483
+ callbacks?.onResponseMeta?.(meta);
484
+ if (parsed.parts) {
485
+ for (const part of parsed.parts) {
486
+ if (part.type === 'text' && part.text) {
487
+ responseText += part.text;
488
+ callbacks?.onNewContent?.(part.text);
489
+ callbacks?.onTextDelta?.(part.text);
490
+ }
491
+ if (part.type === 'reasoning' && part.text) {
492
+ const cleaned = part.text.replace(/\n/g, ' ').trim();
493
+ console.log(`[REASONING] ${cleaned.slice(0, 300)}`);
494
+ if (thinkVisibleEnabled) {
495
+ responseText += `\n🤔 思考: ${cleaned}\n━━━━━━━━━━━━━━━━━━\n`;
496
+ callbacks?.onNewContent?.(`\n🤔 思考: ${cleaned}\n━━━━━━━━━━━━━━━━━━\n`);
439
497
  }
440
498
  }
441
499
  }
442
- const fullText = newParts.join('\n');
443
- if (fullText && fullText !== responseText) {
444
- const delta = fullText.slice(responseText.length);
445
- responseText = fullText;
446
- callbacks?.onTextDelta?.(delta);
447
- callbacks?.onNewContent?.(delta);
448
- idleSince = Date.now();
449
- continue;
450
- }
451
500
  }
452
-
453
- // 检查 AI 是否还在忙(thinking/pending_tool 说明还没干完)
454
- const latestStatus = msgsResult.data?.length ? msgsResult.data[msgsResult.data.length - 1]?.info?.status : '';
455
- if (latestStatus === 'thinking' || latestStatus === 'pending_tool') {
456
- idleSince = Date.now();
457
- }
458
- if (latestStatus) lastStatus = latestStatus;
459
- if (latestStatus && latestStatus !== lastReportedStatus) {
460
- lastReportedStatus = latestStatus;
461
- console.log(`[AI状态] ${latestStatus}`);
462
- }
463
-
464
- // 有回复后:等 30 秒无新内容且 AI 不忙才退出
465
- if (responseText && Date.now() - idleSince > 30000) {
466
- break;
501
+ if (!responseText && parsed.info?.finish) {
502
+ responseText = '[empty response]';
467
503
  }
468
504
  } catch (e) {
469
- console.warn('Poll error:', e.message);
505
+ console.error('[sendMessage] Failed to parse response:', e.message);
506
+ console.log('[RAW]', rawJson.slice(0, 1000));
507
+ responseText = rawJson;
470
508
  }
509
+
510
+ callbacks?.onStatusChange?.({ type: 'idle' });
511
+ return responseText;
512
+
513
+ } finally {
514
+ clearTimeout(timeoutId);
471
515
  }
472
-
473
- if (!responseText) {
474
- console.warn(' Timeout waiting for response, status:', lastStatus);
475
- // Try one more time with a fresh message query
476
- try {
477
- const finalMsgs = await session.client.session.messages({ path: { id: session.sessionId }, query: { limit: 50 } });
478
- if (finalMsgs.data?.length) {
479
- for (let i = finalMsgs.data.length - 1; i >= 0; i--) {
480
- const msg = finalMsgs.data[i];
481
- if (msg.info?.role === 'assistant' && msg.parts) {
482
- const textParts = msg.parts.filter(p => p.type === 'text' && p.text).map(p => p.text);
483
- if (textParts.length > 0) {
484
- responseText = textParts.join('\n');
485
- break;
486
- }
487
- }
488
- }
489
- }
490
- } catch { /* ignore */ }
491
-
492
- if (!responseText) {
493
- return '⏰ 请求超时,请重试';
494
- }
516
+ } catch (error) {
517
+ if (error.name === 'AbortError') {
518
+ console.warn('[sendMessage] 5min timeout, aborting stream');
519
+ return '⏰ 请求超时,请重试';
495
520
  }
496
-
497
- callbacks?.onStatusChange?.({ type: 'idle', hasToolActivity });
498
- return responseText;
499
- }
500
- catch (error) {
501
- console.error('Error sending message:', error);
521
+ console.error('[sendMessage] Error:', error);
502
522
  return `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
503
523
  }
504
524
  }
505
525
  export async function getSession(session) {
506
526
  try {
507
527
  const result = await session.client.session.get({
508
- path: { id: session.sessionId }
528
+ sessionID: session.sessionId
509
529
  });
510
530
  if (result.error) {
511
531
  return null;
@@ -519,7 +539,7 @@ export async function getSession(session) {
519
539
  export async function shareSession(session) {
520
540
  try {
521
541
  const result = await session.client.session.share({
522
- path: { id: session.sessionId }
542
+ sessionID: session.sessionId
523
543
  });
524
544
  if (result.error || !result.data?.share?.url) {
525
545
  return null;
@@ -545,7 +565,7 @@ export async function checkConnection() {
545
565
  export async function abortSession(session) {
546
566
  try {
547
567
  await session.client.session.abort({
548
- path: { id: session.sessionId }
568
+ sessionID: session.sessionId
549
569
  });
550
570
  console.log(`🛑 Aborted session: ${session.sessionId}`);
551
571
  return true;
@@ -558,7 +578,7 @@ export async function abortSession(session) {
558
578
  export async function getSessionMessages(session, limit = 20) {
559
579
  try {
560
580
  const result = await session.client.session.messages({
561
- path: { id: session.sessionId }
581
+ sessionID: session.sessionId
562
582
  });
563
583
  if (result.error) {
564
584
  return null;
@@ -574,7 +594,7 @@ export async function resumeSession(sessionId, title = 'Resumed session') {
574
594
  try {
575
595
  const opencode = await initOpenCode();
576
596
  if (!opencode) return null;
577
- const getResult = await opencode.client.session.get({ path: { id: sessionId } });
597
+ const getResult = await opencode.client.session.get({ sessionID: sessionId });
578
598
  if (getResult.error) {
579
599
  console.warn(`Session ${sessionId} not found`);
580
600
  return null;
@@ -599,10 +619,9 @@ export async function listOpenCodeSessions() {
599
619
  return sessions.map(s => ({
600
620
  id: s.id,
601
621
  title: s.title || 'Untitled',
602
- status: s.status?.type || 'unknown',
603
622
  directory: s.directory || '',
604
- createdAt: s.created_at || 0,
605
- lastActivity: s.updated_at || 0,
623
+ createdAt: s.created_at || s.time?.created || 0,
624
+ lastActivity: s.updated_at || s.time?.updated || 0,
606
625
  }));
607
626
  }
608
627
  catch (error) {
@@ -612,7 +631,7 @@ export async function listOpenCodeSessions() {
612
631
  }
613
632
  export async function listOpenCodeSessionsFromServer(baseUrl) {
614
633
  try {
615
- const { createOpencodeClient } = await import('@opencode-ai/sdk');
634
+ const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
616
635
  const client = createOpencodeClient({
617
636
  baseUrl: baseUrl || 'http://localhost:4096',
618
637
  });
@@ -624,10 +643,9 @@ export async function listOpenCodeSessionsFromServer(baseUrl) {
624
643
  return sessions.map(s => ({
625
644
  id: s.id,
626
645
  title: s.title || 'Untitled',
627
- status: s.status?.type || 'unknown',
628
646
  directory: s.directory || '',
629
- createdAt: s.created_at || 0,
630
- lastActivity: s.updated_at || 0,
647
+ createdAt: s.created_at || s.time?.created || 0,
648
+ lastActivity: s.updated_at || s.time?.updated || 0,
631
649
  }));
632
650
  }
633
651
  catch (error) {
@@ -640,7 +658,7 @@ export async function createOpenCodeSession(title = 'New session') {
640
658
  const opencode = await initOpenCode();
641
659
  if (!opencode) return null;
642
660
  const result = await opencode.client.session.create({
643
- body: { title }
661
+ title
644
662
  });
645
663
  if (result.error) {
646
664
  return null;
@@ -664,7 +682,7 @@ export async function deleteOpenCodeSession(sessionId) {
664
682
  const opencode = await initOpenCode();
665
683
  if (!opencode) return false;
666
684
  const result = await opencode.client.session.delete({
667
- path: { id: sessionId }
685
+ sessionID: sessionId
668
686
  });
669
687
  if (result.error) {
670
688
  return false;
@@ -679,9 +697,9 @@ export async function deleteOpenCodeSession(sessionId) {
679
697
  }
680
698
  export async function renameOpenCodeSession(session, title) {
681
699
  try {
682
- const result = await session.client.session.patch({
683
- path: { id: session.sessionId },
684
- body: { title }
700
+ const result = await session.client.session.update({
701
+ sessionID: session.sessionId,
702
+ title,
685
703
  });
686
704
  if (result.error) {
687
705
  return false;
@@ -699,9 +717,9 @@ export async function forkSession(sessionId, messageID, directory) {
699
717
  const opencode = await initOpenCode();
700
718
  if (!opencode) return null;
701
719
  const result = await opencode.client.session.fork({
702
- path: { id: sessionId },
703
- body: { messageID },
704
- query: directory ? { directory } : {}
720
+ sessionID: sessionId,
721
+ messageID,
722
+ ...(directory ? { directory } : {}),
705
723
  });
706
724
  if (result.error) {
707
725
  console.warn(`Fork failed: ${result.error}`);
@@ -726,8 +744,9 @@ export async function revertSessionMessage(sessionId, messageID, partID) {
726
744
  const opencode = await initOpenCode();
727
745
  if (!opencode) return false;
728
746
  const result = await opencode.client.session.revert({
729
- path: { id: sessionId },
730
- body: { messageID, partID }
747
+ sessionID: sessionId,
748
+ messageID,
749
+ partID,
731
750
  });
732
751
  if (result.error) {
733
752
  console.warn(`Revert failed: ${result.error}`);
@@ -746,7 +765,7 @@ export async function unrevertSession(sessionId) {
746
765
  const opencode = await initOpenCode();
747
766
  if (!opencode) return false;
748
767
  const result = await opencode.client.session.unrevert({
749
- path: { id: sessionId }
768
+ sessionID: sessionId
750
769
  });
751
770
  if (result.error) {
752
771
  console.warn(`Unrevert failed: ${result.error}`);
@@ -765,9 +784,12 @@ export async function listProviders() {
765
784
  try {
766
785
  const opencode = await initOpenCode();
767
786
  if (!opencode) return null;
768
- const result = await opencode.client.provider.list();
769
- if (result.error || !result.data?.all) return null;
770
- return result.data.all;
787
+ // Config2.providers() GET /config/providers (same as v1)
788
+ const result = await opencode.client.config.providers();
789
+ if (result.error) return null;
790
+ // v2 returns { providers: [...] }, v1 returns { all: [...] }
791
+ const data = result.data?.providers || result.data?.all || result.data || [];
792
+ return Array.isArray(data) ? data : null;
771
793
  } catch (error) {
772
794
  console.error('Failed to list providers:', error.message);
773
795
  return null;
@@ -779,7 +801,7 @@ export async function updateGlobalModel(modelStr) {
779
801
  const opencode = await initOpenCode();
780
802
  if (!opencode) return false;
781
803
  const result = await opencode.client.config.update({
782
- body: { model: modelStr },
804
+ config: { model: modelStr },
783
805
  });
784
806
  if (result.error) {
785
807
  console.error('Failed to update model:', result.error);
@@ -24,9 +24,13 @@ export class ClaudeCodeAgentAdapter {
24
24
 
25
25
  async sendPrompt(_sessionId, prompt, history, options = {}) {
26
26
  const projectDir = options.projectDir;
27
- const contextualPrompt = this.buildContextualPrompt(prompt, history);
27
+ let cleanPrompt = prompt;
28
+ if (prompt.startsWith('-c')) {
29
+ cleanPrompt = prompt.slice(2).trim();
30
+ }
31
+ const contextualPrompt = this.buildContextualPrompt(cleanPrompt, history);
28
32
 
29
- const args = ['--print', contextualPrompt];
33
+ const args = ['--print', '-c', contextualPrompt];
30
34
 
31
35
  return this.callClaude(args, projectDir);
32
36
  }
@@ -22,11 +22,15 @@ export class OpenCodeAgentAdapter {
22
22
  });
23
23
  }
24
24
 
25
- async sendPrompt(_sessionId, prompt, history) {
26
- const contextualPrompt = this.buildContextualPrompt(prompt, history);
25
+ async sendPrompt(_sessionId, prompt, history, options = {}) {
26
+ let cleanPrompt = prompt;
27
+ if (prompt.startsWith('-c')) {
28
+ cleanPrompt = prompt.slice(2).trim();
29
+ }
30
+ const contextualPrompt = this.buildContextualPrompt(cleanPrompt, history);
27
31
  return this.callOpenCode(contextualPrompt);
28
32
  }
29
-
33
+
30
34
  buildContextualPrompt(prompt, history) {
31
35
  if (!history || history.length === 0) return prompt;
32
36
  const historyText = history
@@ -34,7 +38,7 @@ export class OpenCodeAgentAdapter {
34
38
  .join('\n\n');
35
39
  return `Previous conversation:\n${historyText}\n\nCurrent request: ${prompt}`;
36
40
  }
37
-
41
+
38
42
  extractErrorMessage(stdout, stderr) {
39
43
  const lines = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
40
44
  .map(l => l.trim()).filter(Boolean)
@@ -48,7 +52,7 @@ export class OpenCodeAgentAdapter {
48
52
 
49
53
  callOpenCode(prompt) {
50
54
  return new Promise((resolve) => {
51
- const proc = spawn('opencode', ['run', '--format', 'json', prompt], {
55
+ const proc = spawn('opencode', ['run', '--format', 'json', '-c', prompt], {
52
56
  stdio: ['ignore', 'pipe', 'pipe'],
53
57
  shell: true,
54
58
  });
@@ -56,11 +60,33 @@ export class OpenCodeAgentAdapter {
56
60
  let stdout = '';
57
61
  let stderr = '';
58
62
  let fullText = '';
63
+ let resigned = false;
64
+
65
+ const STUCK_PATTERNS = [
66
+ 'Free usage exceeded', 'quota exceeded', 'rate limit',
67
+ 'retrying in', 'retry attempt',
68
+ '429', '401', '403', '402', 'Payment Required',
69
+ 'subscription required', 'insufficient_quota',
70
+ ];
71
+
72
+ const checkStuck = (stderrText) => {
73
+ if (resigned) return;
74
+ for (const pattern of STUCK_PATTERNS) {
75
+ if (stderrText.toLowerCase().includes(pattern.toLowerCase())) {
76
+ resigned = true;
77
+ proc.kill();
78
+ const detail = this.extractErrorMessage('', stderrText);
79
+ resolve(`❌ OpenCode 无法继续: ${detail || pattern}`);
80
+ return true;
81
+ }
82
+ }
83
+ return false;
84
+ };
59
85
 
60
86
  proc.stdout?.on('data', (data) => {
61
- stdout += data.toString();
62
- const lines = stdout.split('\n');
63
- stdout = lines.pop() || '';
87
+ const chunk = data.toString();
88
+ stdout += chunk;
89
+ const lines = chunk.split('\n');
64
90
  for (const line of lines) {
65
91
  if (!line.trim()) continue;
66
92
  try {
@@ -70,15 +96,19 @@ export class OpenCodeAgentAdapter {
70
96
  }
71
97
  });
72
98
 
73
- proc.stderr?.on('data', (data) => { stderr += data.toString(); });
99
+ proc.stderr?.on('data', (data) => {
100
+ stderr += data.toString();
101
+ checkStuck(stderr);
102
+ });
74
103
 
75
104
  proc.on('close', (code) => {
105
+ if (resigned) return;
76
106
  if (code !== 0) {
77
107
  const detail = this.extractErrorMessage(stdout, stderr);
78
108
  const hint = detail
79
109
  ? `: ${detail}`
80
110
  : '。请运行 `opencode auth login` 配置认证。';
81
- resolve(`❌ OpenCode 错误${hint}`);
111
+ resolve(`❌ OpenCode 错误 (exit code ${code})${hint}`);
82
112
  } else {
83
113
  resolve(fullText || '完成');
84
114
  }
@@ -38,12 +38,9 @@ export class TelegramAdapter {
38
38
  async sendCommandMenu(threadId, title) {
39
39
  if (!this.bot) return;
40
40
  const groups = [
41
- ['🟢 常用', ['/help', '/status', '/start', '/reset']],
42
- ['🔄 任务', ['/loop', '/refresh', '/restart']],
43
- ['🤖 AI', ['/model', '/agents', '/oc', '/cc']],
44
- ['🧠 专家', ['/tutorial', '/z', '/diagnose']],
45
- ['📂 会话', ['/sessions', '/delsessions', '/copy', '/revert']],
46
- ['⬆️ 文件', ['/upload', '/delete']],
41
+ ['/help', '/start', '/reset', '/diagnose'],
42
+ ['/restart', '/model', '/oc', '/cc'],
43
+ ['/cx', '/copilot'],
47
44
  ];
48
45
  const keyboard = [];
49
46
  for (const [, cmds] of groups) {
@@ -1,8 +1,8 @@
1
1
  import { registry } from '../core/registry.js';
2
- import { sessionManager } from '../core/session.js';
3
2
  import { initOpenCode, createSession, sendMessage as sendToOpenCode, checkConnection } from '../opencode/client.js';
4
3
  import { parseMessage, routeMessage } from '../core/router.js';
5
4
  import { telegramAdapter } from './adapter.js';
5
+ import { splitMessage } from '../utils/message-split.js';
6
6
 
7
7
  export async function startBot() {
8
8
  const { loadConfig } = await import('../core/config.js');
@@ -17,7 +17,6 @@ export async function startBot() {
17
17
  process.exit(1);
18
18
  }
19
19
 
20
- await sessionManager.start();
21
20
  await registry.loadBuiltInPlugins();
22
21
  await telegramAdapter.start(config);
23
22
 
@@ -68,10 +67,6 @@ export async function startBot() {
68
67
  if (parsed.type === 'command' && parsed.command === 'reset') {
69
68
  openCodeSessions.delete(message.threadId);
70
69
  opencodeSessionId = null;
71
- try {
72
- const session = await sessionManager.getExistingSession(platform, channelId, message.threadId);
73
- if (session) await sessionManager.resetConversation(platform, channelId, message.threadId);
74
- } catch (e) { console.warn('[Telegram] Reset error:', e.message); }
75
70
  }
76
71
 
77
72
  if (parsed.type === 'default') {