@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.
- package/README.md +78 -8
- package/dist/core/auth.js +41 -108
- package/dist/core/notifications.js +11 -0
- package/dist/core/router.js +291 -61
- package/dist/feishu/commands.js +29 -35
- package/dist/feishu/handler.js +17 -26
- package/dist/opencode/client.js +48 -77
- package/dist/plugins/agents/claude-code/index.js +46 -4
- package/dist/telegram/adapter.js +75 -0
- package/dist/telegram/bot.js +66 -208
- package/dist/weixin/bot.js +12 -2
- package/dist/weixin/commands.js +29 -42
- package/dist/weixin/handler.js +80 -107
- package/package.json +2 -3
package/dist/opencode/client.js
CHANGED
|
@@ -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 -
|
|
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
|
-
|
|
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
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
//
|
|
460
|
-
let startIdx = 0;
|
|
429
|
+
// 收集所有新的 assistant 回复(累加,不丢内容)
|
|
461
430
|
if (lastMsgId) {
|
|
462
431
|
const idx = messages.findIndex(m => m.info?.id === lastMsgId);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
//
|
|
490
|
-
if (responseText &&
|
|
491
|
-
|
|
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
|
-
|
|
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();
|