@yvhitxcel/opencode-remote 0.15.0 → 0.16.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.
- package/README.md +95 -8
- package/dist/core/auth.js +41 -108
- package/dist/core/router.js +152 -60
- package/dist/feishu/commands.js +2 -35
- package/dist/feishu/handler.js +17 -26
- package/dist/opencode/client.js +47 -78
- package/dist/telegram/adapter.js +55 -0
- package/dist/telegram/bot.js +48 -207
- package/dist/weixin/bot.js +12 -2
- package/dist/weixin/commands.js +2 -42
- package/dist/weixin/handler.js +80 -107
- package/package.json +2 -3
package/dist/feishu/handler.js
CHANGED
|
@@ -2,46 +2,36 @@ import { getOrCreateSession } from '../core/session.js';
|
|
|
2
2
|
import { splitMessage } from '../core/notifications.js';
|
|
3
3
|
import { initOpenCode, createSession, sendMessage, checkConnection, resumeSession, shareSession } from '../opencode/client.js';
|
|
4
4
|
import { isAuthorized, hasOwner } from '../core/auth.js';
|
|
5
|
-
import { detectCommand } from '../core/router.js';
|
|
5
|
+
import { detectCommand, EXPERT_SYSTEM_PROMPT } from '../core/router.js';
|
|
6
6
|
import { handleCommand, formatTimeAgo } from './commands.js';
|
|
7
7
|
import { existsSync, readFileSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
|
|
10
|
-
const EXPERT_SYSTEM_PROMPT = `你是一个专家角色扮演系统,严格按照 AGENTS.md 中的"专家点评系统"流程执行。
|
|
11
|
-
|
|
12
|
-
当用户输入包含触发词(z / 叫全部专家 / 叫所有专家 / 呼叫专家点评 / 专家点评 / 专家意见 / call all experts / expert review)时,启动专家评审。
|
|
13
|
-
|
|
14
|
-
## 规则
|
|
15
|
-
- 严格遵循 AGENTS.md 中定义的 13 位角色和点评流程
|
|
16
|
-
- 言辞必须苛刻犀利,不讨好不委婉
|
|
17
|
-
- 不说客套话
|
|
18
|
-
- 直接指出问题`;
|
|
19
|
-
|
|
20
10
|
async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
21
11
|
const session = await getOrCreateSession(ctx.threadId, 'feishu');
|
|
22
12
|
|
|
13
|
+
const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
|
|
14
|
+
let expertPrompt = null;
|
|
15
|
+
|
|
23
16
|
if (text.startsWith('/z')) {
|
|
24
17
|
const arg = text.slice(2).trim();
|
|
25
18
|
if (arg === 'off' || arg === 'reset' || arg === '关闭') {
|
|
26
|
-
|
|
27
|
-
session.systemPrompt = null;
|
|
28
|
-
await adapter.reply(ctx.threadId, '⏹️ 专家模式已关闭');
|
|
19
|
+
await adapter.reply(ctx.threadId, '⏹️ 自定义 prompt 已清除');
|
|
29
20
|
return;
|
|
30
21
|
}
|
|
31
22
|
if (arg) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (!session.expertMode) {
|
|
38
|
-
session.expertMode = true;
|
|
39
|
-
session.systemPrompt = EXPERT_SYSTEM_PROMPT;
|
|
23
|
+
expertPrompt = arg;
|
|
24
|
+
} else {
|
|
25
|
+
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
40
26
|
}
|
|
41
|
-
await adapter
|
|
27
|
+
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt);
|
|
42
28
|
return;
|
|
43
29
|
}
|
|
44
30
|
|
|
31
|
+
if (expertTriggers.some(t => text.trim().toLowerCase().includes(t))) {
|
|
32
|
+
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
33
|
+
}
|
|
34
|
+
|
|
45
35
|
const parsed = detectCommand(text);
|
|
46
36
|
if (parsed) {
|
|
47
37
|
await handleCommand(adapter, ctx, parsed.name, parsed.arg, openCodeSessions);
|
|
@@ -222,7 +212,7 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
|
222
212
|
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
|
|
223
213
|
}
|
|
224
214
|
|
|
225
|
-
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session) {
|
|
215
|
+
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt) {
|
|
226
216
|
await adapter.sendTypingIndicator(ctx.threadId);
|
|
227
217
|
let openCodeSession = openCodeSessions.get(ctx.threadId);
|
|
228
218
|
if (!openCodeSession) {
|
|
@@ -258,8 +248,8 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
|
|
|
258
248
|
}
|
|
259
249
|
}
|
|
260
250
|
|
|
261
|
-
if (
|
|
262
|
-
scopedText = `${
|
|
251
|
+
if (expertPrompt) {
|
|
252
|
+
scopedText = `${expertPrompt}\n\n${scopedText}`;
|
|
263
253
|
}
|
|
264
254
|
|
|
265
255
|
console.log(`📤 Forwarding to OpenCode: ${text.substring(0, 80)}...`);
|
|
@@ -268,6 +258,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
|
|
|
268
258
|
let hasToolActivity = false;
|
|
269
259
|
let toolCallCount = 0;
|
|
270
260
|
let response = await sendMessage(openCodeSession, scopedText, {
|
|
261
|
+
idleThreshold: expertPrompt ? 30 : 10,
|
|
271
262
|
onEvent: (event) => {
|
|
272
263
|
if (event.type === 'tool.call') {
|
|
273
264
|
const props = event.properties || {};
|
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,11 @@ 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;
|
|
396
|
-
let
|
|
397
|
-
let idleCycles = 0;
|
|
398
|
-
const IDLE_THRESHOLD = 3; // Exit after 3 idle polls (no new content, not processing)
|
|
400
|
+
let idleSince = 0; // 最后一次收到新内容的时间戳
|
|
399
401
|
|
|
400
402
|
while (Date.now() - startTime < TIMEOUT_MS) {
|
|
401
403
|
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
@@ -405,93 +407,61 @@ export async function sendMessage(session, message, callbacks) {
|
|
|
405
407
|
path: { id: session.sessionId }
|
|
406
408
|
});
|
|
407
409
|
|
|
408
|
-
if (msgsResult.error) {
|
|
409
|
-
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (!msgsResult.data?.length) {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
410
|
+
if (msgsResult.error) { console.error('[sendMessage] Messages error:', msgsResult.error); break; }
|
|
411
|
+
if (!msgsResult.data?.length) continue;
|
|
416
412
|
|
|
417
413
|
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
414
|
|
|
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
|
-
}
|
|
415
|
+
// 工具活动
|
|
416
|
+
for (let i = msgCountBefore; i < messages.length; i++) {
|
|
417
|
+
const msg = messages[i];
|
|
418
|
+
if (msg.parts) for (const part of msg.parts) {
|
|
419
|
+
if (part.type === 'tool_use' || part.type === 'tool_result') {
|
|
420
|
+
hasToolActivity = true;
|
|
421
|
+
callbacks?.onEvent?.({ type: 'tool.call', properties: { name: part.name || part.tool_name || 'unknown', input: part.input || {} } });
|
|
422
|
+
break;
|
|
454
423
|
}
|
|
455
|
-
if (hasToolActivity) break;
|
|
456
424
|
}
|
|
425
|
+
if (hasToolActivity) break;
|
|
457
426
|
}
|
|
458
427
|
|
|
459
|
-
//
|
|
460
|
-
let startIdx = 0;
|
|
428
|
+
// 收集所有新的 assistant 回复(累加,不丢内容)
|
|
461
429
|
if (lastMsgId) {
|
|
462
430
|
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;
|
|
431
|
+
const startIdx = idx >= 0 ? idx + 1 : 0;
|
|
432
|
+
const newParts = [];
|
|
433
|
+
for (let i = startIdx; i < messages.length; i++) {
|
|
434
|
+
const msg = messages[i];
|
|
435
|
+
if (msg.info?.role === 'assistant' && msg.parts) {
|
|
436
|
+
for (const p of msg.parts) {
|
|
437
|
+
if (p.type === 'text' && p.text) newParts.push(p.text);
|
|
438
|
+
}
|
|
478
439
|
}
|
|
479
440
|
}
|
|
441
|
+
const fullText = newParts.join('\n');
|
|
442
|
+
if (fullText && fullText !== responseText) {
|
|
443
|
+
const delta = fullText.slice(responseText.length);
|
|
444
|
+
responseText = fullText;
|
|
445
|
+
callbacks?.onTextDelta?.(delta);
|
|
446
|
+
callbacks?.onNewContent?.(delta);
|
|
447
|
+
idleSince = Date.now();
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
480
450
|
}
|
|
481
451
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
452
|
+
// 检查 AI 是否还在忙(thinking/pending_tool 说明还没干完)
|
|
453
|
+
const latestStatus = msgsResult.data?.length ? msgsResult.data[msgsResult.data.length - 1]?.info?.status : '';
|
|
454
|
+
if (latestStatus === 'thinking' || latestStatus === 'pending_tool') {
|
|
455
|
+
idleSince = Date.now();
|
|
456
|
+
}
|
|
457
|
+
if (latestStatus && latestStatus !== lastReportedStatus) {
|
|
458
|
+
lastReportedStatus = latestStatus;
|
|
459
|
+
console.log(`[AI状态] ${latestStatus}`);
|
|
487
460
|
}
|
|
488
461
|
|
|
489
|
-
//
|
|
490
|
-
if (responseText &&
|
|
491
|
-
|
|
492
|
-
if (idleCycles >= IDLE_THRESHOLD) {
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
462
|
+
// 有回复后:等 30 秒无新内容且 AI 不忙才退出
|
|
463
|
+
if (responseText && Date.now() - idleSince > 30000) {
|
|
464
|
+
break;
|
|
495
465
|
}
|
|
496
466
|
} catch (e) {
|
|
497
467
|
console.warn('Poll error:', e.message);
|
|
@@ -523,7 +493,6 @@ export async function sendMessage(session, message, callbacks) {
|
|
|
523
493
|
}
|
|
524
494
|
|
|
525
495
|
callbacks?.onStatusChange?.({ type: 'idle', hasToolActivity });
|
|
526
|
-
console.log(`💬 Response: ${responseText.slice(0, 100)}...`);
|
|
527
496
|
return responseText;
|
|
528
497
|
}
|
|
529
498
|
catch (error) {
|
|
@@ -0,0 +1,55 @@
|
|
|
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) {
|
|
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' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async sendTyping(threadId, isTyping) {
|
|
39
|
+
if (!this.bot) return;
|
|
40
|
+
if (isTyping) {
|
|
41
|
+
try { await this.bot.api.sendChatAction(threadId, 'typing'); } catch {}
|
|
42
|
+
const existing = this.typingIntervals.get(threadId);
|
|
43
|
+
if (existing) clearInterval(existing);
|
|
44
|
+
const interval = setInterval(async () => {
|
|
45
|
+
try { await this.bot.api.sendChatAction(threadId, 'typing'); } catch {}
|
|
46
|
+
}, 4000);
|
|
47
|
+
this.typingIntervals.set(threadId, interval);
|
|
48
|
+
} else {
|
|
49
|
+
const interval = this.typingIntervals.get(threadId);
|
|
50
|
+
if (interval) { clearInterval(interval); this.typingIntervals.delete(threadId); }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const telegramAdapter = new TelegramAdapter();
|
package/dist/telegram/bot.js
CHANGED
|
@@ -1,189 +1,53 @@
|
|
|
1
|
-
// Telegram Bot adapter with multi-agent support
|
|
2
|
-
import { Bot } from 'grammy';
|
|
3
|
-
import { splitMessage } from '../utils/message-split.js';
|
|
4
1
|
import { registry } from '../core/registry.js';
|
|
5
2
|
import { sessionManager } from '../core/session.js';
|
|
6
3
|
import { initOpenCode, createSession, sendMessage as sendToOpenCode, checkConnection } from '../opencode/client.js';
|
|
7
4
|
import { parseMessage, routeMessage } from '../core/router.js';
|
|
5
|
+
import { telegramAdapter } from './adapter.js';
|
|
8
6
|
|
|
9
|
-
export class TelegramAdapter {
|
|
10
|
-
name = 'telegram';
|
|
11
|
-
bot = null;
|
|
12
|
-
config = null;
|
|
13
|
-
messageHandler = null;
|
|
14
|
-
isRunning = false;
|
|
15
|
-
typingIntervals = new Map();
|
|
16
|
-
|
|
17
|
-
async start(config) {
|
|
18
|
-
this.config = config;
|
|
19
|
-
|
|
20
|
-
if (!config.telegramBotToken || config.telegramBotToken === 'your_bot_token_here') {
|
|
21
|
-
throw new Error('Telegram bot token not configured. Run "opencode-remote config" first.');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
this.bot = new Bot(config.telegramBotToken);
|
|
25
|
-
|
|
26
|
-
this.bot.on('message:text', async (ctx) => {
|
|
27
|
-
console.log('[Telegram] Received message:', ctx.message.text);
|
|
28
|
-
|
|
29
|
-
if (ctx.message.from.is_bot) {
|
|
30
|
-
console.log('[Telegram] Ignoring bot message');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!this.messageHandler) {
|
|
35
|
-
console.log('[Telegram] No message handler registered');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const message = {
|
|
41
|
-
id: ctx.message.message_id.toString(),
|
|
42
|
-
threadId: ctx.chat.id.toString(),
|
|
43
|
-
userId: ctx.message.from?.id?.toString() || 'unknown',
|
|
44
|
-
text: ctx.message.text || '',
|
|
45
|
-
timestamp: new Date(ctx.message.date * 1000),
|
|
46
|
-
channelId: 'default',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const msgCtx = {
|
|
50
|
-
message,
|
|
51
|
-
platform: 'telegram',
|
|
52
|
-
channelId: 'default',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
await this.messageHandler(msgCtx);
|
|
56
|
-
} catch (err) {
|
|
57
|
-
console.error('[Telegram] Error in message handler:', err);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
this.bot.start().then(() => {
|
|
62
|
-
console.log('[Telegram] Bot stopped gracefully');
|
|
63
|
-
}).catch((err) => {
|
|
64
|
-
if (this.isRunning) {
|
|
65
|
-
console.error('[Telegram] Bot polling error:', err);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
this.isRunning = true;
|
|
70
|
-
console.log('🚀 Telegram adapter started');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async stop() {
|
|
74
|
-
this.isRunning = false;
|
|
75
|
-
|
|
76
|
-
for (const interval of this.typingIntervals.values()) {
|
|
77
|
-
clearInterval(interval);
|
|
78
|
-
}
|
|
79
|
-
this.typingIntervals.clear();
|
|
80
|
-
|
|
81
|
-
if (this.bot) {
|
|
82
|
-
await this.bot.stop();
|
|
83
|
-
this.bot = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.log('👋 Telegram adapter stopped');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
onMessage(handler) {
|
|
90
|
-
this.messageHandler = handler;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async sendMessage(threadId, text) {
|
|
94
|
-
if (!this.bot) {
|
|
95
|
-
throw new Error('Telegram adapter not started');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const chunks = splitMessage(text, { maxLength: 4000, addContinuationMarker: false });
|
|
99
|
-
|
|
100
|
-
for (const chunk of chunks) {
|
|
101
|
-
await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'Markdown' });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async sendTyping(threadId, isTyping) {
|
|
106
|
-
if (!this.bot) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (isTyping) {
|
|
111
|
-
try {
|
|
112
|
-
await this.bot.api.sendChatAction(threadId, 'typing');
|
|
113
|
-
} catch {
|
|
114
|
-
// Ignore errors
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const existing = this.typingIntervals.get(threadId);
|
|
118
|
-
if (existing) {
|
|
119
|
-
clearInterval(existing);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const interval = setInterval(async () => {
|
|
123
|
-
try {
|
|
124
|
-
await this.bot.api.sendChatAction(threadId, 'typing');
|
|
125
|
-
} catch {
|
|
126
|
-
// Ignore errors
|
|
127
|
-
}
|
|
128
|
-
}, 4000);
|
|
129
|
-
|
|
130
|
-
this.typingIntervals.set(threadId, interval);
|
|
131
|
-
} else {
|
|
132
|
-
const interval = this.typingIntervals.get(threadId);
|
|
133
|
-
if (interval) {
|
|
134
|
-
clearInterval(interval);
|
|
135
|
-
this.typingIntervals.delete(threadId);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export const telegramAdapter = new TelegramAdapter();
|
|
142
|
-
|
|
143
|
-
// Legacy startBot function for backward compatibility
|
|
144
7
|
export async function startBot() {
|
|
145
8
|
const { loadConfig } = await import('../core/config.js');
|
|
146
9
|
const config = loadConfig();
|
|
147
|
-
|
|
10
|
+
|
|
148
11
|
if (!config.telegramBotToken || config.telegramBotToken === 'your_bot_token_here') {
|
|
149
|
-
console.log('');
|
|
150
|
-
console.log('
|
|
151
|
-
console.log('
|
|
152
|
-
console.log('
|
|
153
|
-
console.log('');
|
|
154
|
-
console.log(' To get your bot token:');
|
|
155
|
-
console.log(' 1. Open Telegram app');
|
|
156
|
-
console.log(' 2. Search for @BotFather');
|
|
157
|
-
console.log(' 3. Send: /newbot');
|
|
158
|
-
console.log(' 4. Follow the instructions to create your bot');
|
|
159
|
-
console.log(' 5. Copy the token');
|
|
160
|
-
console.log('');
|
|
161
|
-
console.log(' Then run: opencode-remote config');
|
|
162
|
-
console.log('');
|
|
163
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
12
|
+
console.log('\n❌ Telegram Bot Token not configured\n');
|
|
13
|
+
console.log('To get your bot token:');
|
|
14
|
+
console.log(' 1. Open Telegram app, search @BotFather');
|
|
15
|
+
console.log(' 2. Send /newbot and follow instructions');
|
|
16
|
+
console.log(' 3. Then run: opencode-remote config\n');
|
|
164
17
|
process.exit(1);
|
|
165
18
|
}
|
|
166
|
-
|
|
167
|
-
console.log('');
|
|
168
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
169
|
-
console.log(' OpenCode Remote Control');
|
|
170
|
-
console.log(' Control OpenCode from Telegram');
|
|
171
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
172
|
-
console.log('');
|
|
173
|
-
|
|
174
|
-
// Initialize session manager
|
|
19
|
+
|
|
175
20
|
await sessionManager.start();
|
|
176
|
-
|
|
177
|
-
// Load plugins
|
|
178
21
|
await registry.loadBuiltInPlugins();
|
|
179
|
-
|
|
180
|
-
// Initialize Telegram adapter
|
|
181
22
|
await telegramAdapter.start(config);
|
|
182
|
-
|
|
183
|
-
// Set up message handler
|
|
23
|
+
|
|
184
24
|
let openCodeSessions = new Map();
|
|
185
25
|
let opencodeSessionId = null;
|
|
186
26
|
|
|
27
|
+
telegramAdapter.bot.on('message:text', async (ctx) => {
|
|
28
|
+
if (ctx.message.from.is_bot) return;
|
|
29
|
+
if (!telegramAdapter.messageHandler) return;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const message = {
|
|
33
|
+
id: ctx.message.message_id.toString(),
|
|
34
|
+
threadId: ctx.chat.id.toString(),
|
|
35
|
+
userId: ctx.message.from?.id?.toString() || 'unknown',
|
|
36
|
+
text: ctx.message.text || '',
|
|
37
|
+
timestamp: new Date(ctx.message.date * 1000),
|
|
38
|
+
channelId: 'default',
|
|
39
|
+
};
|
|
40
|
+
const msgCtx = { message, platform: 'telegram', channelId: 'default' };
|
|
41
|
+
await telegramAdapter.messageHandler(msgCtx);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('[Telegram] Error:', err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
telegramAdapter.bot.start().catch((err) => {
|
|
48
|
+
if (telegramAdapter.isRunning) console.error('[Telegram] Polling error:', err);
|
|
49
|
+
});
|
|
50
|
+
|
|
187
51
|
telegramAdapter.onMessage(async (ctx) => {
|
|
188
52
|
const { message, platform, channelId } = ctx;
|
|
189
53
|
|
|
@@ -198,23 +62,22 @@ export async function startBot() {
|
|
|
198
62
|
try {
|
|
199
63
|
const session = await sessionManager.getExistingSession(platform, channelId, message.threadId);
|
|
200
64
|
if (session) await sessionManager.resetConversation(platform, channelId, message.threadId);
|
|
201
|
-
} catch (e) { console.warn('[Telegram]
|
|
65
|
+
} catch (e) { console.warn('[Telegram] Reset error:', e.message); }
|
|
202
66
|
}
|
|
203
67
|
|
|
204
68
|
if (parsed.type === 'default') {
|
|
205
69
|
const connected = await checkConnection();
|
|
206
70
|
if (!connected) {
|
|
207
71
|
await telegramAdapter.sendTyping(message.threadId, false);
|
|
208
|
-
await telegramAdapter.sendMessage(message.threadId, '❌ OpenCode
|
|
72
|
+
await telegramAdapter.sendMessage(message.threadId, '❌ OpenCode 离线');
|
|
209
73
|
return;
|
|
210
74
|
}
|
|
211
|
-
|
|
212
75
|
let session = openCodeSessions.get(message.threadId);
|
|
213
76
|
if (!session) {
|
|
214
77
|
const newSession = await createSession(message.threadId, `Telegram ${message.threadId}`);
|
|
215
78
|
if (!newSession) {
|
|
216
79
|
await telegramAdapter.sendTyping(message.threadId, false);
|
|
217
|
-
await telegramAdapter.sendMessage(message.threadId, '❌
|
|
80
|
+
await telegramAdapter.sendMessage(message.threadId, '❌ 无法创建会话');
|
|
218
81
|
return;
|
|
219
82
|
}
|
|
220
83
|
session = newSession;
|
|
@@ -226,18 +89,15 @@ export async function startBot() {
|
|
|
226
89
|
onTextDelta: () => {},
|
|
227
90
|
onEvent: (event) => {
|
|
228
91
|
if (event.type === 'tool.call') {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
telegramAdapter.sendMessage(message.threadId, `🔧 ${toolName}`).catch(() => {});
|
|
92
|
+
const n = event.properties?.name || 'unknown';
|
|
93
|
+
telegramAdapter.sendMessage(message.threadId, `🔧 ${n}`).catch(() => {});
|
|
232
94
|
}
|
|
233
95
|
},
|
|
234
96
|
});
|
|
235
97
|
|
|
236
98
|
await telegramAdapter.sendTyping(message.threadId, false);
|
|
237
|
-
|
|
238
99
|
if (response) {
|
|
239
|
-
const
|
|
240
|
-
for (const chunk of chunks) {
|
|
100
|
+
for (const chunk of splitMessage(response)) {
|
|
241
101
|
if (chunk.trim()) await telegramAdapter.sendMessage(message.threadId, chunk);
|
|
242
102
|
}
|
|
243
103
|
}
|
|
@@ -245,44 +105,25 @@ export async function startBot() {
|
|
|
245
105
|
}
|
|
246
106
|
|
|
247
107
|
const result = await routeMessage(parsed, {
|
|
248
|
-
threadId: message.threadId,
|
|
249
|
-
|
|
250
|
-
platform,
|
|
251
|
-
defaultAgent: 'opencode',
|
|
252
|
-
opencodeSessionId,
|
|
108
|
+
threadId: message.threadId, channelId, platform,
|
|
109
|
+
defaultAgent: 'opencode', opencodeSessionId,
|
|
253
110
|
});
|
|
254
111
|
|
|
255
112
|
await telegramAdapter.sendTyping(message.threadId, false);
|
|
256
|
-
|
|
257
113
|
if (typeof result === 'string') {
|
|
258
114
|
await telegramAdapter.sendMessage(message.threadId, result);
|
|
259
115
|
} else if (result) {
|
|
260
|
-
let
|
|
261
|
-
for await (const chunk of result)
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
if (fullResponse) await telegramAdapter.sendMessage(message.threadId, fullResponse);
|
|
116
|
+
let full = '';
|
|
117
|
+
for await (const chunk of result) full += chunk;
|
|
118
|
+
if (full) await telegramAdapter.sendMessage(message.threadId, full);
|
|
265
119
|
}
|
|
266
120
|
} catch (error) {
|
|
267
|
-
console.error('Error
|
|
121
|
+
console.error('[Telegram] Error:', error);
|
|
268
122
|
await telegramAdapter.sendTyping(message.threadId, false);
|
|
269
|
-
await telegramAdapter.sendMessage(message.threadId, '❌
|
|
123
|
+
await telegramAdapter.sendMessage(message.threadId, '❌ 处理失败');
|
|
270
124
|
}
|
|
271
125
|
});
|
|
272
|
-
|
|
273
|
-
console.log('🚀
|
|
274
|
-
console.log('');
|
|
275
|
-
console.log('Available commands:');
|
|
276
|
-
console.log(' /start - Start bot');
|
|
277
|
-
console.log(' /help - Show all commands');
|
|
278
|
-
console.log(' /agents - List available agents');
|
|
279
|
-
console.log(' /new - Start new conversation');
|
|
280
|
-
console.log(' /reset - Reset session');
|
|
281
|
-
console.log(' /oc <prompt> - Use OpenCode');
|
|
282
|
-
console.log(' /cc <prompt> - Use Claude Code');
|
|
283
|
-
console.log(' /cx <prompt> - Use Codex');
|
|
284
|
-
console.log('');
|
|
285
|
-
|
|
286
|
-
// Keep process alive
|
|
126
|
+
|
|
127
|
+
console.log('🚀 Telegram bot ready');
|
|
287
128
|
await new Promise(() => {});
|
|
288
129
|
}
|