@yvhitxcel/opencode-remote 0.15.1 → 0.16.1

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.
@@ -172,6 +172,9 @@ export async function initFetchConfig() {
172
172
  }
173
173
  let opencodeInstance = null;
174
174
  let opencodeServer = null;
175
+ let lastStdoutTime = 0;
176
+ let lastStdoutLine = '';
177
+ let lastReportedStatus = '';
175
178
  const PORTS_TO_TRY = [4096, 4097, 4098];
176
179
 
177
180
  // TCP-level port probe: true = occupied, false = free
@@ -241,8 +244,9 @@ export async function initOpenCode() {
241
244
  windowsHide: isWindows,
242
245
  });
243
246
  opencodeServer.stdout.on('data', (d) => {
247
+ lastStdoutTime = Date.now();
244
248
  const msg = d.toString().trim();
245
- if (msg) console.log(`[opencode] ${msg}`);
249
+ if (msg) { lastStdoutLine = msg.slice(0, 120); console.log(`[opencode] ${msg}`); }
246
250
  });
247
251
  opencodeServer.stderr.on('data', (d) => {
248
252
  const msg = d.toString().trim();
@@ -389,13 +393,12 @@ export async function sendMessage(session, message, callbacks) {
389
393
  body: promptBody,
390
394
  });
391
395
 
392
- // Poll for new response - multi-turn: continue until truly idle
396
+ // Poll for new response - keep going as long as new content keeps arriving
393
397
  const startTime = Date.now();
394
398
  let responseText = '';
395
399
  let hasToolActivity = false;
400
+ let idleSince = 0; // 最后一次收到新内容的时间戳
396
401
  let lastStatus = '';
397
- let idleCycles = 0;
398
- const IDLE_THRESHOLD = 3; // Exit after 3 idle polls (no new content, not processing)
399
402
 
400
403
  while (Date.now() - startTime < TIMEOUT_MS) {
401
404
  await new Promise(r => setTimeout(r, POLL_INTERVAL));
@@ -405,93 +408,62 @@ export async function sendMessage(session, message, callbacks) {
405
408
  path: { id: session.sessionId }
406
409
  });
407
410
 
408
- if (msgsResult.error) {
409
- console.error('[sendMessage] Messages error:', msgsResult.error);
410
- break;
411
- }
412
-
413
- if (!msgsResult.data?.length) {
414
- continue;
415
- }
411
+ if (msgsResult.error) { console.error('[sendMessage] Messages error:', msgsResult.error); break; }
412
+ if (!msgsResult.data?.length) continue;
416
413
 
417
414
  const messages = msgsResult.data;
418
- const newMsgCount = messages.length;
419
-
420
- // Check session status
421
- const latestMsg = messages[messages.length - 1];
422
- const currentStatus = latestMsg?.info?.status;
423
- if (currentStatus !== lastStatus) {
424
- lastStatus = currentStatus;
425
- if (lastStatus) {
426
- console.log(`[sendMessage] Session status: ${lastStatus}`);
427
- }
428
- }
429
415
 
430
- // If actively processing, reset idle counter and wait
431
- if (lastStatus === 'pending_tool' || lastStatus === 'thinking') {
432
- idleCycles = 0;
433
- continue;
434
- }
435
-
436
- // Check if there was tool activity and notify via callback
437
- if (newMsgCount > msgCountBefore) {
438
- for (let i = msgCountBefore; i < newMsgCount; i++) {
439
- const msg = messages[i];
440
- if (msg.parts) {
441
- for (const part of msg.parts) {
442
- if (part.type === 'tool_use' || part.type === 'tool_result') {
443
- hasToolActivity = true;
444
- callbacks?.onEvent?.({
445
- type: 'tool.call',
446
- properties: {
447
- name: part.name || part.tool_name || 'unknown',
448
- input: part.input || {}
449
- }
450
- });
451
- break;
452
- }
453
- }
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;
454
424
  }
455
- if (hasToolActivity) break;
456
425
  }
426
+ if (hasToolActivity) break;
457
427
  }
458
428
 
459
- // Find messages after our last message
460
- let startIdx = 0;
429
+ // 收集所有新的 assistant 回复(累加,不丢内容)
461
430
  if (lastMsgId) {
462
431
  const idx = messages.findIndex(m => m.info?.id === lastMsgId);
463
- if (idx >= 0) startIdx = idx + 1;
464
- }
465
-
466
- // Collect the latest assistant response text
467
- let newText = '';
468
- for (let i = messages.length - 1; i >= startIdx; i--) {
469
- const msg = messages[i];
470
- if (msg.info?.role === 'assistant') {
471
- const textParts = msg.parts
472
- ?.filter(p => p.type === 'text' && p.text)
473
- .map(p => p.text) || [];
474
-
475
- if (textParts.length > 0) {
476
- newText = textParts.join('\n');
477
- break;
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);
439
+ }
478
440
  }
479
441
  }
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
+ }
480
451
  }
481
452
 
482
- if (newText && newText !== responseText) {
483
- const delta = newText.slice(responseText.length);
484
- responseText = newText;
485
- callbacks?.onTextDelta?.(delta);
486
- idleCycles = 0;
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}`);
487
462
  }
488
463
 
489
- // Exit only when truly idle: have response, not processing, no new text for N cycles
490
- if (responseText && lastStatus !== 'pending_tool' && lastStatus !== 'thinking') {
491
- idleCycles++;
492
- if (idleCycles >= IDLE_THRESHOLD) {
493
- break;
494
- }
464
+ // 有回复后:等 30 秒无新内容且 AI 不忙才退出
465
+ if (responseText && Date.now() - idleSince > 30000) {
466
+ break;
495
467
  }
496
468
  } catch (e) {
497
469
  console.warn('Poll error:', e.message);
@@ -523,7 +495,6 @@ export async function sendMessage(session, message, callbacks) {
523
495
  }
524
496
 
525
497
  callbacks?.onStatusChange?.({ type: 'idle', hasToolActivity });
526
- console.log(`💬 Response: ${responseText.slice(0, 100)}...`);
527
498
  return responseText;
528
499
  }
529
500
  catch (error) {
@@ -2,6 +2,13 @@
2
2
  import { spawn } from 'child_process';
3
3
  import { platform } from 'os';
4
4
 
5
+ const LIUV_CRASH_PATTERNS = [
6
+ 'Assertion failed',
7
+ 'UV_HANDLE_CLOSING',
8
+ 'src\\win\\async.c',
9
+ 'libuv',
10
+ ];
11
+
5
12
  export class ClaudeCodeAgentAdapter {
6
13
  name = 'claude-code';
7
14
  aliases = ['cc', 'claude'];
@@ -19,9 +26,7 @@ export class ClaudeCodeAgentAdapter {
19
26
  const projectDir = options.projectDir;
20
27
  const contextualPrompt = this.buildContextualPrompt(prompt, history);
21
28
 
22
- // 构建命令参数
23
29
  const args = ['--print', contextualPrompt];
24
- // `cwd` is set via spawn opts below, no need for --project flag
25
30
 
26
31
  return this.callClaude(args, projectDir);
27
32
  }
@@ -32,13 +37,40 @@ export class ClaudeCodeAgentAdapter {
32
37
  return `Previous:\n${historyText}\n\n${prompt}`;
33
38
  }
34
39
 
40
+ isCrashNoise(line) {
41
+ return LIUV_CRASH_PATTERNS.some(p => line.includes(p));
42
+ }
43
+
44
+ extractErrorMessage(stdout, stderr, code) {
45
+ // 先检查 stdout:--print 模式把错误也输出到 stdout
46
+ const stdoutLines = stdout.trim().split('\n').map(l => l.trim()).filter(Boolean);
47
+ const stdoutErrors = stdoutLines.filter(l => !this.isCrashNoise(l));
48
+ if (stdoutErrors.length > 0) {
49
+ return stdoutErrors.join('\n');
50
+ }
51
+
52
+ // 再检查 stderr
53
+ const stderrLines = stderr.trim().split('\n').map(l => l.trim()).filter(Boolean);
54
+ const stderrReal = stderrLines.filter(l => !this.isCrashNoise(l));
55
+ if (stderrReal.length > 0) {
56
+ return stderrReal.join('\n');
57
+ }
58
+
59
+ // 如果全是崩溃噪音,尝试从任一流中找 Error 关键词
60
+ const all = [...stdoutLines, ...stderrLines];
61
+ const firstRelevant = all.find(l => /Error|error|ERROR|^\d{3}/.test(l));
62
+ if (firstRelevant) return firstRelevant;
63
+
64
+ // 兜底
65
+ return `进程异常退出 (code: ${code})`;
66
+ }
67
+
35
68
  callClaude(args, projectDir) {
36
69
  return new Promise((resolve) => {
37
70
  const opts = {
38
71
  stdio: ['ignore', 'pipe', 'pipe'],
39
72
  shell: true,
40
73
  };
41
- // 如果指定了项目目录,在该目录下执行
42
74
  if (projectDir) {
43
75
  opts.cwd = projectDir;
44
76
  }
@@ -57,7 +89,17 @@ export class ClaudeCodeAgentAdapter {
57
89
  proc.on('close', (code) => {
58
90
  clearTimeout(timeout);
59
91
  console.log(`[claude-code] Process exited with code ${code}, stdout=${stdout.length} bytes, stderr=${stderr.length} bytes`);
60
- resolve(code === 0 ? stdout.trim() : `❌ Claude Code 错误: ${stderr}`);
92
+
93
+ if (code === 0) {
94
+ resolve(stdout.trim());
95
+ return;
96
+ }
97
+
98
+ const errorMsg = this.extractErrorMessage(stdout, stderr, code);
99
+ console.log(`[claude-code] Process failed, raw stderr:\n${stderr.trim().slice(-1000)}`);
100
+ console.log(`[claude-code] Error detail:\n${errorMsg}`);
101
+
102
+ resolve(`❌ Claude Code 错误 (exit code ${code}): ${errorMsg}`);
61
103
  });
62
104
  proc.on('error', (err) => {
63
105
  clearTimeout(timeout);
@@ -0,0 +1,75 @@
1
+ import { Bot } from 'grammy';
2
+ import { splitMessage } from '../utils/message-split.js';
3
+
4
+ export class TelegramAdapter {
5
+ name = 'telegram';
6
+ bot = null;
7
+ config = null;
8
+ messageHandler = null;
9
+ isRunning = false;
10
+ typingIntervals = new Map();
11
+
12
+ async start(config) {
13
+ this.config = config;
14
+ if (!config.telegramBotToken || config.telegramBotToken === 'your_bot_token_here') {
15
+ throw new Error('Telegram bot token not configured');
16
+ }
17
+ this.bot = new Bot(config.telegramBotToken);
18
+ this.isRunning = true;
19
+ console.log('🚀 Telegram adapter started');
20
+ }
21
+
22
+ async stop() {
23
+ this.isRunning = false;
24
+ for (const interval of this.typingIntervals.values()) clearInterval(interval);
25
+ this.typingIntervals.clear();
26
+ if (this.bot) { await this.bot.stop(); this.bot = null; }
27
+ console.log('👋 Telegram adapter stopped');
28
+ }
29
+
30
+ onMessage(handler) { this.messageHandler = handler; }
31
+
32
+ async sendMessage(threadId, text, opts = {}) {
33
+ if (!this.bot) throw new Error('Telegram adapter not started');
34
+ const chunks = splitMessage(text, { maxLength: 4000, addContinuationMarker: false });
35
+ for (const chunk of chunks) await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'Markdown', ...opts });
36
+ }
37
+
38
+ async sendCommandMenu(threadId, title) {
39
+ if (!this.bot) return;
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']],
47
+ ];
48
+ const keyboard = [];
49
+ for (const [, cmds] of groups) {
50
+ const row = cmds.map(cmd => ({ text: cmd, callback_data: `cmd:${cmd.slice(1)}` }));
51
+ keyboard.push(row);
52
+ }
53
+ await this.bot.api.sendMessage(threadId, title || '📱 选择指令:', {
54
+ reply_markup: { inline_keyboard: keyboard },
55
+ });
56
+ }
57
+
58
+ async sendTyping(threadId, isTyping) {
59
+ if (!this.bot) return;
60
+ if (isTyping) {
61
+ try { await this.bot.api.sendChatAction(threadId, 'typing'); } catch {}
62
+ const existing = this.typingIntervals.get(threadId);
63
+ if (existing) clearInterval(existing);
64
+ const interval = setInterval(async () => {
65
+ try { await this.bot.api.sendChatAction(threadId, 'typing'); } catch {}
66
+ }, 4000);
67
+ this.typingIntervals.set(threadId, interval);
68
+ } else {
69
+ const interval = this.typingIntervals.get(threadId);
70
+ if (interval) { clearInterval(interval); this.typingIntervals.delete(threadId); }
71
+ }
72
+ }
73
+ }
74
+
75
+ export const telegramAdapter = new TelegramAdapter();