@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.
@@ -1,189 +1,62 @@
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(' Telegram Bot Token not configured');
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.on('callback_query:data', async (ctx) => {
48
+ if (!ctx.callbackQuery.data.startsWith('cmd:')) return;
49
+ const cmd = ctx.callbackQuery.data.slice(4);
50
+ try {
51
+ await ctx.answerCallbackQuery({ text: `执行: /${cmd}` });
52
+ const msg = await telegramAdapter.bot.api.sendMessage(ctx.chat.id, `/${cmd}`);
53
+ } catch (e) { console.error('[Telegram] callback error:', e.message); }
54
+ });
55
+
56
+ telegramAdapter.bot.start().catch((err) => {
57
+ if (telegramAdapter.isRunning) console.error('[Telegram] Polling error:', err);
58
+ });
59
+
187
60
  telegramAdapter.onMessage(async (ctx) => {
188
61
  const { message, platform, channelId } = ctx;
189
62
 
@@ -198,23 +71,22 @@ export async function startBot() {
198
71
  try {
199
72
  const session = await sessionManager.getExistingSession(platform, channelId, message.threadId);
200
73
  if (session) await sessionManager.resetConversation(platform, channelId, message.threadId);
201
- } catch (e) { console.warn('[Telegram] Error resetting session:', e.message); }
74
+ } catch (e) { console.warn('[Telegram] Reset error:', e.message); }
202
75
  }
203
76
 
204
77
  if (parsed.type === 'default') {
205
78
  const connected = await checkConnection();
206
79
  if (!connected) {
207
80
  await telegramAdapter.sendTyping(message.threadId, false);
208
- await telegramAdapter.sendMessage(message.threadId, '❌ OpenCode 离线,请检查服务是否运行');
81
+ await telegramAdapter.sendMessage(message.threadId, '❌ OpenCode 离线');
209
82
  return;
210
83
  }
211
-
212
84
  let session = openCodeSessions.get(message.threadId);
213
85
  if (!session) {
214
86
  const newSession = await createSession(message.threadId, `Telegram ${message.threadId}`);
215
87
  if (!newSession) {
216
88
  await telegramAdapter.sendTyping(message.threadId, false);
217
- await telegramAdapter.sendMessage(message.threadId, '❌ 无法创建 OpenCode 会话');
89
+ await telegramAdapter.sendMessage(message.threadId, '❌ 无法创建会话');
218
90
  return;
219
91
  }
220
92
  session = newSession;
@@ -222,67 +94,53 @@ export async function startBot() {
222
94
  }
223
95
  opencodeSessionId = session.sessionId;
224
96
 
97
+ const taskStart = Date.now();
225
98
  const response = await sendToOpenCode(session, parsed.prompt, {
226
99
  onTextDelta: () => {},
227
100
  onEvent: (event) => {
228
101
  if (event.type === 'tool.call') {
229
- const props = event.properties || {};
230
- const toolName = props.name || 'unknown';
231
- telegramAdapter.sendMessage(message.threadId, `🔧 ${toolName}`).catch(() => {});
102
+ const n = event.properties?.name || 'unknown';
103
+ telegramAdapter.sendMessage(message.threadId, `🔧 ${n}`).catch(() => {});
232
104
  }
233
105
  },
234
106
  });
235
107
 
236
108
  await telegramAdapter.sendTyping(message.threadId, false);
237
-
238
109
  if (response) {
239
- const chunks = splitMessage(response);
240
- for (const chunk of chunks) {
110
+ for (const chunk of splitMessage(response)) {
241
111
  if (chunk.trim()) await telegramAdapter.sendMessage(message.threadId, chunk);
242
112
  }
243
113
  }
114
+ const { formatTaskCompletion } = await import('../core/notifications.js');
115
+ await telegramAdapter.sendMessage(message.threadId, formatTaskCompletion('AI 任务', taskStart));
244
116
  return;
245
117
  }
246
118
 
247
119
  const result = await routeMessage(parsed, {
248
- threadId: message.threadId,
249
- channelId,
250
- platform,
251
- defaultAgent: 'opencode',
252
- opencodeSessionId,
120
+ threadId: message.threadId, channelId, platform,
121
+ defaultAgent: 'opencode', opencodeSessionId,
253
122
  });
254
123
 
255
124
  await telegramAdapter.sendTyping(message.threadId, false);
256
-
257
125
  if (typeof result === 'string') {
258
- await telegramAdapter.sendMessage(message.threadId, result);
259
- } else if (result) {
260
- let fullResponse = '';
261
- for await (const chunk of result) {
262
- fullResponse += chunk;
126
+ if (parsed.type === 'command' && (parsed.command === 'help' || parsed.command === 'start')) {
127
+ await telegramAdapter.sendMessage(message.threadId, result);
128
+ await telegramAdapter.sendCommandMenu(message.threadId, '📱 快速选择指令:');
129
+ } else {
130
+ await telegramAdapter.sendMessage(message.threadId, result);
263
131
  }
264
- if (fullResponse) await telegramAdapter.sendMessage(message.threadId, fullResponse);
132
+ } else if (result) {
133
+ let full = '';
134
+ for await (const chunk of result) full += chunk;
135
+ if (full) await telegramAdapter.sendMessage(message.threadId, full);
265
136
  }
266
137
  } catch (error) {
267
- console.error('Error handling message:', error);
138
+ console.error('[Telegram] Error:', error);
268
139
  await telegramAdapter.sendTyping(message.threadId, false);
269
- await telegramAdapter.sendMessage(message.threadId, '❌ 处理失败,请重试。');
140
+ await telegramAdapter.sendMessage(message.threadId, '❌ 处理失败');
270
141
  }
271
142
  });
272
-
273
- console.log('🚀 Starting Telegram bot...');
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
143
+
144
+ console.log('🚀 Telegram bot ready');
287
145
  await new Promise(() => {});
288
146
  }
@@ -134,6 +134,7 @@ export async function startWeixinBot(botConfig, restartFn) {
134
134
  globalThis.__weixinBotRunning = () => running;
135
135
 
136
136
  let buf = '';
137
+ let retryCount = 0;
137
138
  console.log('Polling for messages...');
138
139
 
139
140
  if (process.env.OPENCODE_RESTART === '1') {
@@ -170,8 +171,17 @@ export async function startWeixinBot(botConfig, restartFn) {
170
171
  }
171
172
  } catch (e) {
172
173
  if (!running) break;
173
- console.error('Polling error:', e);
174
- await new Promise(r => setTimeout(r, 2000));
174
+ const errMsg = e.message || '';
175
+ const isConnReset = errMsg.includes('ECONNRESET') || errMsg.includes('fetch failed');
176
+ if (isConnReset) {
177
+ retryCount++;
178
+ const delay = Math.min(2000 * retryCount, 15000);
179
+ console.error(`[bot] Connection error (${retryCount}), retry in ${delay}ms...`);
180
+ await new Promise(r => setTimeout(r, delay));
181
+ } else {
182
+ console.error('Polling error:', e);
183
+ await new Promise(r => setTimeout(r, 2000));
184
+ }
175
185
  }
176
186
  }
177
187
 
@@ -1,4 +1,4 @@
1
- import { detectCommand, COMMAND_ALIASES } from '../core/router.js';
1
+ import { detectCommand, COMMAND_ALIASES, getHelpText, DEMO_RESPONSES, setDemoMode, isDemoMode } from '../core/router.js';
2
2
  import { getOrCreateSession, saveSessionMapping, sessionManager } from '../core/session.js';
3
3
  import { splitMessage } from '../core/notifications.js';
4
4
  import { initOpenCode, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
@@ -91,37 +91,20 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
91
91
  return true;
92
92
  }
93
93
  case 'help':
94
- await adapter.reply(ctx.threadId, `📖 指令
95
-
96
- 🟢 常用:
97
- /start — 首次认证
98
- /help — 帮助
99
- /status — 查看状态
100
- /reset — 重置会话
101
- /copy — 复制回复
102
- /revert — 撤销消息
103
-
104
- 🔄 任务:
105
- /loop — 循环执行
106
- /refresh — 刷新上下文
107
- /restart — 重启 bot
108
- /stop — 停止 bot
109
-
110
- 📂 会话:
111
- /sessions — 浏览会话
112
- /delsessions — 删除会话
113
-
114
- 🤖 AI 模型:
115
- /model — 切换模型
116
- /agents — 查看可用 Agent
117
- /oc — 使用 OpenCode
118
- /cc — 使用 Claude Code
119
-
120
- ⬆️ 文件:
121
- /upload — 上传构建产物
122
-
123
- 💬 直接发消息给 AI!`);
94
+ await adapter.reply(ctx.threadId, getHelpText());
124
95
  return true;
96
+ case 'tutorial': {
97
+ const { TUTORIAL_STEPS } = await import('../core/router.js');
98
+ const stepNum = parseInt(arg, 10);
99
+ const step = !isNaN(stepNum) && stepNum >= 1 && stepNum <= TUTORIAL_STEPS.length ? stepNum : 1;
100
+ const s = TUTORIAL_STEPS[step - 1];
101
+ let msg = `📚 教程 · 第 ${s.step}/${TUTORIAL_STEPS.length} 步\n━━━━━━━━━━━━━━━━\n\n${s.title}\n\n${s.desc}\n\n`;
102
+ if (s.action) msg += `👉 ${s.action}`;
103
+ msg += `\n\n回复 /tutorial${step < TUTORIAL_STEPS.length ? ` 继续第${step + 1}步` : ''} 进入下一步`;
104
+ const msgs = splitMessage(msg);
105
+ for (const m of msgs) await adapter.reply(ctx.threadId, m);
106
+ return true;
107
+ }
125
108
  case 'status': {
126
109
  const connected = await checkConnection();
127
110
  const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
@@ -489,17 +472,6 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
489
472
  return true;
490
473
  }
491
474
 
492
- case 'stop': {
493
- await adapter.reply(ctx.threadId, '🛑 正在停止 bot...');
494
- setTimeout(() => {
495
- if (globalThis.__weixinBotShutdown) {
496
- globalThis.__weixinBotShutdown(false);
497
- }
498
- setTimeout(() => process.exit(0), 1000);
499
- }, 500);
500
- return true;
501
- }
502
-
503
475
  case 'upload': {
504
476
  const projectDir = session.projectDir || globalThis.__autoProjectDir;
505
477
 
@@ -738,6 +710,21 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
738
710
 
739
711
 
740
712
 
713
+ case 'demo': {
714
+ const argText = (arg || '').trim().toLowerCase();
715
+ if (argText === 'off' || argText === 'exit' || argText === 'stop') {
716
+ setDemoMode(ctx.threadId, false);
717
+ await adapter.reply(ctx.threadId, '⏹️ 已退出沙箱模式');
718
+ return true;
719
+ }
720
+ setDemoMode(ctx.threadId, true);
721
+ let msg = '🎮 沙箱模式已启动\n\n在此模式下所有命令返回模拟输出,无需连接 OpenCode。\n\n';
722
+ msg += '试试发送: /help /status /model /agents /loop /copy\n';
723
+ msg += '发送 /demo off 退出';
724
+ await adapter.reply(ctx.threadId, msg);
725
+ return true;
726
+ }
727
+
741
728
  case 'diagnose': {
742
729
  const { checkConnection } = await import('../opencode/client.js');
743
730
  const diag = ['🔍 诊断报告\n'];