@yvhitxcel/opencode-remote 0.15.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.
Files changed (102) hide show
  1. package/README.md +82 -0
  2. package/bin/opencode-remote.js +70 -0
  3. package/bin/opencode-weixin.js +10 -0
  4. package/dist/AGENTS.md +20 -0
  5. package/dist/MEMORY.md +21 -0
  6. package/dist/bot-runner.js +180 -0
  7. package/dist/cli.js +256 -0
  8. package/dist/core/approval.js +95 -0
  9. package/dist/core/auth.js +119 -0
  10. package/dist/core/config.js +61 -0
  11. package/dist/core/notifications.js +134 -0
  12. package/dist/core/qiniu.js +267 -0
  13. package/dist/core/registry.js +86 -0
  14. package/dist/core/router.js +344 -0
  15. package/dist/core/session.js +403 -0
  16. package/dist/core/setup.js +418 -0
  17. package/dist/core/types.js +16 -0
  18. package/dist/feishu/adapter.js +72 -0
  19. package/dist/feishu/bot.js +168 -0
  20. package/dist/feishu/commands.js +601 -0
  21. package/dist/feishu/handler.js +380 -0
  22. package/dist/index.js +60 -0
  23. package/dist/opencode/client.js +823 -0
  24. package/dist/package-lock.json +762 -0
  25. package/dist/patch_spawn.js +28 -0
  26. package/dist/plugins/agents/acp/acp-adapter.js +42 -0
  27. package/dist/plugins/agents/claude-code/index.js +69 -0
  28. package/dist/plugins/agents/codex/index.js +44 -0
  29. package/dist/plugins/agents/copilot/index.js +44 -0
  30. package/dist/plugins/agents/opencode/index.js +66 -0
  31. package/dist/telegram/bot.js +288 -0
  32. package/dist/utils/message-split.js +38 -0
  33. package/dist/web/code-viewer.js +266 -0
  34. package/dist/weixin/adapter.js +135 -0
  35. package/dist/weixin/api.js +179 -0
  36. package/dist/weixin/bot.js +183 -0
  37. package/dist/weixin/commands.js +758 -0
  38. package/dist/weixin/handler.js +577 -0
  39. package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
  40. package/dist/weixin/node_modules/encodeurl/README.md +109 -0
  41. package/dist/weixin/node_modules/encodeurl/index.js +60 -0
  42. package/dist/weixin/node_modules/encodeurl/package.json +40 -0
  43. package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
  44. package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
  45. package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
  46. package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
  47. package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
  48. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
  49. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
  50. package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
  51. package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
  52. package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
  53. package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
  54. package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
  55. package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
  56. package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
  57. package/dist/weixin/node_modules/qiniu/README.md +56 -0
  58. package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
  59. package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
  60. package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
  61. package/dist/weixin/node_modules/qiniu/index.js +32 -0
  62. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
  63. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
  64. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
  65. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
  66. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
  67. package/dist/weixin/node_modules/qiniu/package.json +80 -0
  68. package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
  69. package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
  70. package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
  71. package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
  72. package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
  73. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
  74. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
  75. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
  76. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
  77. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
  78. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
  79. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
  80. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
  81. package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
  82. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
  83. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
  84. package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
  85. package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
  86. package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
  87. package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
  88. package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
  89. package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
  90. package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
  91. package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
  92. package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
  93. package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
  94. package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
  95. package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
  96. package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
  97. package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
  98. package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
  99. package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
  100. package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
  101. package/dist/weixin/types.js +25 -0
  102. package/package.json +56 -0
@@ -0,0 +1,28 @@
1
+ import { createRequire } from 'node:module';
2
+ import Module from 'node:module';
3
+ import { platform } from 'os';
4
+ const require = createRequire(import.meta.url);
5
+ const childProcess = require('node:child_process');
6
+ // Suppress only DEP0190 warning (shell args concatenation) from SDK internals
7
+ const originalEmit = process.emit;
8
+ process.emit = function (name, warning) {
9
+ if (name === 'warning' && warning && warning.code === 'DEP0190') return;
10
+ return originalEmit.apply(this, arguments);
11
+ };
12
+ const originalSpawn = childProcess.spawn;
13
+ childProcess.spawn = function patchedSpawn(command, args, options = {}) {
14
+ // On Windows, use shell for commands that need PATH resolution
15
+ const isWindows = platform() === 'win32';
16
+ if (isWindows && !options.shell && typeof command === 'string' && !command.includes('\\') && !command.includes('/')) {
17
+ options = { ...options, shell: true };
18
+ }
19
+ return originalSpawn.call(this, command, args, options);
20
+ };
21
+ const originalLoad = Module._load;
22
+ Module._load = function (request, parent, isMain) {
23
+ const result = originalLoad.apply(this, arguments);
24
+ if (request === 'child_process' || request === 'node:child_process') {
25
+ result.spawn = childProcess.spawn;
26
+ }
27
+ return result;
28
+ };
@@ -0,0 +1,42 @@
1
+ // ACP (Agent Communication Protocol) remote agent adapter
2
+ export class ACPAdapter {
3
+ name;
4
+ aliases;
5
+ config;
6
+
7
+ constructor(config) {
8
+ this.name = config.name;
9
+ this.aliases = config.aliases || [];
10
+ this.config = config;
11
+ }
12
+
13
+ async isAvailable() {
14
+ try {
15
+ const response = await fetch(`${this.config.endpoint}/health`, {
16
+ method: 'GET',
17
+ signal: AbortSignal.timeout(5000),
18
+ });
19
+ return response.ok;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ async sendPrompt(_sessionId, prompt, history) {
26
+ try {
27
+ const response = await fetch(`${this.config.endpoint}/prompt`, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ prompt, history: history || [] }),
31
+ signal: AbortSignal.timeout(120000),
32
+ });
33
+ if (!response.ok) {
34
+ return `❌ ACP agent error: ${response.statusText}`;
35
+ }
36
+ const data = await response.json();
37
+ return data.text || data.content || JSON.stringify(data);
38
+ } catch (error) {
39
+ return `❌ ACP agent error: ${error.message}`;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,69 @@
1
+ // Claude Code CLI agent adapter
2
+ import { spawn } from 'child_process';
3
+ import { platform } from 'os';
4
+
5
+ export class ClaudeCodeAgentAdapter {
6
+ name = 'claude-code';
7
+ aliases = ['cc', 'claude'];
8
+
9
+ async isAvailable() {
10
+ return new Promise((resolve) => {
11
+ const command = platform() === 'win32' ? 'where' : 'which';
12
+ const proc = spawn(command, ['claude'], { stdio: 'ignore', shell: true });
13
+ proc.on('close', (code) => resolve(code === 0));
14
+ proc.on('error', () => resolve(false));
15
+ });
16
+ }
17
+
18
+ async sendPrompt(_sessionId, prompt, history, options = {}) {
19
+ const projectDir = options.projectDir;
20
+ const contextualPrompt = this.buildContextualPrompt(prompt, history);
21
+
22
+ // 构建命令参数
23
+ const args = ['--print', contextualPrompt];
24
+ // `cwd` is set via spawn opts below, no need for --project flag
25
+
26
+ return this.callClaude(args, projectDir);
27
+ }
28
+
29
+ buildContextualPrompt(prompt, history) {
30
+ if (!history || history.length === 0) return prompt;
31
+ const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
32
+ return `Previous:\n${historyText}\n\n${prompt}`;
33
+ }
34
+
35
+ callClaude(args, projectDir) {
36
+ return new Promise((resolve) => {
37
+ const opts = {
38
+ stdio: ['ignore', 'pipe', 'pipe'],
39
+ shell: true,
40
+ };
41
+ // 如果指定了项目目录,在该目录下执行
42
+ if (projectDir) {
43
+ opts.cwd = projectDir;
44
+ }
45
+
46
+ console.log(`[claude-code] Spawning: claude ${args.join(' ')} in ${opts.cwd || process.cwd()}`);
47
+ const proc = spawn('claude', args, opts);
48
+ let stdout = '';
49
+ let stderr = '';
50
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
51
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
52
+
53
+ const timeout = setTimeout(() => {
54
+ console.log(`[claude-code] Process still running after 30s, stderr so far: ${stderr.slice(-200)}`);
55
+ }, 30000);
56
+
57
+ proc.on('close', (code) => {
58
+ clearTimeout(timeout);
59
+ 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}`);
61
+ });
62
+ proc.on('error', (err) => {
63
+ clearTimeout(timeout);
64
+ console.log(`[claude-code] Spawn error: ${err.message}`);
65
+ resolve(`❌ Claude Code 启动失败: ${err.message}`);
66
+ });
67
+ });
68
+ }
69
+ }
@@ -0,0 +1,44 @@
1
+ // OpenAI Codex CLI agent adapter
2
+ import { spawn } from 'child_process';
3
+ import { platform } from 'os';
4
+
5
+ export class CodexAgentAdapter {
6
+ name = 'codex';
7
+ aliases = ['cx'];
8
+
9
+ async isAvailable() {
10
+ return new Promise((resolve) => {
11
+ const command = platform() === 'win32' ? 'where' : 'which';
12
+ const proc = spawn(command, ['codex'], { stdio: 'ignore', shell: true });
13
+ proc.on('close', (code) => resolve(code === 0));
14
+ proc.on('error', () => resolve(false));
15
+ });
16
+ }
17
+
18
+ async sendPrompt(_sessionId, prompt, history) {
19
+ const contextualPrompt = this.buildContextualPrompt(prompt, history);
20
+ return this.callCodex(contextualPrompt);
21
+ }
22
+
23
+ buildContextualPrompt(prompt, history) {
24
+ if (!history || history.length === 0) return prompt;
25
+ const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
26
+ return `Context:\n${historyText}\n\n${prompt}`;
27
+ }
28
+
29
+ callCodex(prompt) {
30
+ return new Promise((resolve) => {
31
+ const proc = spawn('codex', ['--prompt', prompt], {
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ shell: true,
34
+ });
35
+ let stdout = '';
36
+ let stderr = '';
37
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
38
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
39
+ proc.on('close', (code) => {
40
+ resolve(code === 0 ? stdout.trim() : `❌ Codex 错误: ${stderr}`);
41
+ });
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,44 @@
1
+ // GitHub Copilot CLI agent adapter
2
+ import { spawn } from 'child_process';
3
+ import { platform } from 'os';
4
+
5
+ export class CopilotAgentAdapter {
6
+ name = 'copilot';
7
+ aliases = ['copilot-cli', 'copilot'];
8
+
9
+ async isAvailable() {
10
+ return new Promise((resolve) => {
11
+ const command = platform() === 'win32' ? 'where' : 'which';
12
+ const proc = spawn(command, ['copilot'], { stdio: 'ignore', shell: true });
13
+ proc.on('close', (code) => resolve(code === 0));
14
+ proc.on('error', () => resolve(false));
15
+ });
16
+ }
17
+
18
+ async sendPrompt(_sessionId, prompt, history) {
19
+ const contextualPrompt = this.buildContextualPrompt(prompt, history);
20
+ return this.callCopilot(contextualPrompt);
21
+ }
22
+
23
+ buildContextualPrompt(prompt, history) {
24
+ if (!history || history.length === 0) return prompt;
25
+ const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
26
+ return `Context:\n${historyText}\n\n${prompt}`;
27
+ }
28
+
29
+ callCopilot(prompt) {
30
+ return new Promise((resolve) => {
31
+ const proc = spawn('copilot', ['suggest', '--prompt', prompt], {
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ shell: true,
34
+ });
35
+ let stdout = '';
36
+ let stderr = '';
37
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
38
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
39
+ proc.on('close', (code) => {
40
+ resolve(code === 0 ? stdout.trim() : `❌ Copilot 错误: ${stderr}`);
41
+ });
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,66 @@
1
+ // OpenCode CLI agent adapter
2
+ import { spawn } from 'child_process';
3
+ import { platform } from 'os';
4
+
5
+ export class OpenCodeAgentAdapter {
6
+ name = 'opencode';
7
+ aliases = ['oc', 'opencodeai'];
8
+
9
+ async isAvailable() {
10
+ return new Promise((resolve) => {
11
+ const command = platform() === 'win32' ? 'where' : 'which';
12
+ const proc = spawn(command, ['opencode'], { stdio: 'ignore', shell: true });
13
+ proc.on('close', (code) => resolve(code === 0));
14
+ proc.on('error', () => resolve(false));
15
+ });
16
+ }
17
+
18
+ async sendPrompt(_sessionId, prompt, history) {
19
+ const contextualPrompt = this.buildContextualPrompt(prompt, history);
20
+ return this.callOpenCode(contextualPrompt);
21
+ }
22
+
23
+ buildContextualPrompt(prompt, history) {
24
+ if (!history || history.length === 0) return prompt;
25
+ const historyText = history
26
+ .map(msg => `[${msg.role === 'user' ? 'User' : 'Assistant'}]: ${msg.content}`)
27
+ .join('\n\n');
28
+ return `Previous conversation:\n${historyText}\n\nCurrent request: ${prompt}`;
29
+ }
30
+
31
+ callOpenCode(prompt) {
32
+ return new Promise((resolve) => {
33
+ const proc = spawn('opencode', ['run', '--format', 'json', prompt], {
34
+ stdio: ['ignore', 'pipe', 'pipe'],
35
+ shell: true,
36
+ });
37
+
38
+ let stdout = '';
39
+ let stderr = '';
40
+ let fullText = '';
41
+
42
+ proc.stdout?.on('data', (data) => {
43
+ stdout += data.toString();
44
+ const lines = stdout.split('\n');
45
+ stdout = lines.pop() || '';
46
+ for (const line of lines) {
47
+ if (!line.trim()) continue;
48
+ try {
49
+ const event = JSON.parse(line);
50
+ if (event.text) fullText += event.text;
51
+ } catch {}
52
+ }
53
+ });
54
+
55
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
56
+
57
+ proc.on('close', (code) => {
58
+ if (code !== 0) {
59
+ resolve(`❌ OpenCode 错误。请运行 \`opencode auth login\` 配置认证。`);
60
+ } else {
61
+ resolve(fullText || '完成');
62
+ }
63
+ });
64
+ });
65
+ }
66
+ }
@@ -0,0 +1,288 @@
1
+ // Telegram Bot adapter with multi-agent support
2
+ import { Bot } from 'grammy';
3
+ import { splitMessage } from '../utils/message-split.js';
4
+ import { registry } from '../core/registry.js';
5
+ import { sessionManager } from '../core/session.js';
6
+ import { initOpenCode, createSession, sendMessage as sendToOpenCode, checkConnection } from '../opencode/client.js';
7
+ import { parseMessage, routeMessage } from '../core/router.js';
8
+
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
+ export async function startBot() {
145
+ const { loadConfig } = await import('../core/config.js');
146
+ const config = loadConfig();
147
+
148
+ 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('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
164
+ process.exit(1);
165
+ }
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
175
+ await sessionManager.start();
176
+
177
+ // Load plugins
178
+ await registry.loadBuiltInPlugins();
179
+
180
+ // Initialize Telegram adapter
181
+ await telegramAdapter.start(config);
182
+
183
+ // Set up message handler
184
+ let openCodeSessions = new Map();
185
+ let opencodeSessionId = null;
186
+
187
+ telegramAdapter.onMessage(async (ctx) => {
188
+ const { message, platform, channelId } = ctx;
189
+
190
+ telegramAdapter.sendTyping(message.threadId, true).catch(() => {});
191
+
192
+ try {
193
+ const parsed = parseMessage(message.text);
194
+
195
+ if (parsed.type === 'command' && parsed.command === 'reset') {
196
+ openCodeSessions.delete(message.threadId);
197
+ opencodeSessionId = null;
198
+ try {
199
+ const session = await sessionManager.getExistingSession(platform, channelId, message.threadId);
200
+ if (session) await sessionManager.resetConversation(platform, channelId, message.threadId);
201
+ } catch (e) { console.warn('[Telegram] Error resetting session:', e.message); }
202
+ }
203
+
204
+ if (parsed.type === 'default') {
205
+ const connected = await checkConnection();
206
+ if (!connected) {
207
+ await telegramAdapter.sendTyping(message.threadId, false);
208
+ await telegramAdapter.sendMessage(message.threadId, '❌ OpenCode 离线,请检查服务是否运行');
209
+ return;
210
+ }
211
+
212
+ let session = openCodeSessions.get(message.threadId);
213
+ if (!session) {
214
+ const newSession = await createSession(message.threadId, `Telegram ${message.threadId}`);
215
+ if (!newSession) {
216
+ await telegramAdapter.sendTyping(message.threadId, false);
217
+ await telegramAdapter.sendMessage(message.threadId, '❌ 无法创建 OpenCode 会话');
218
+ return;
219
+ }
220
+ session = newSession;
221
+ openCodeSessions.set(message.threadId, session);
222
+ }
223
+ opencodeSessionId = session.sessionId;
224
+
225
+ const response = await sendToOpenCode(session, parsed.prompt, {
226
+ onTextDelta: () => {},
227
+ onEvent: (event) => {
228
+ if (event.type === 'tool.call') {
229
+ const props = event.properties || {};
230
+ const toolName = props.name || 'unknown';
231
+ telegramAdapter.sendMessage(message.threadId, `🔧 ${toolName}`).catch(() => {});
232
+ }
233
+ },
234
+ });
235
+
236
+ await telegramAdapter.sendTyping(message.threadId, false);
237
+
238
+ if (response) {
239
+ const chunks = splitMessage(response);
240
+ for (const chunk of chunks) {
241
+ if (chunk.trim()) await telegramAdapter.sendMessage(message.threadId, chunk);
242
+ }
243
+ }
244
+ return;
245
+ }
246
+
247
+ const result = await routeMessage(parsed, {
248
+ threadId: message.threadId,
249
+ channelId,
250
+ platform,
251
+ defaultAgent: 'opencode',
252
+ opencodeSessionId,
253
+ });
254
+
255
+ await telegramAdapter.sendTyping(message.threadId, false);
256
+
257
+ 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;
263
+ }
264
+ if (fullResponse) await telegramAdapter.sendMessage(message.threadId, fullResponse);
265
+ }
266
+ } catch (error) {
267
+ console.error('Error handling message:', error);
268
+ await telegramAdapter.sendTyping(message.threadId, false);
269
+ await telegramAdapter.sendMessage(message.threadId, '❌ 处理失败,请重试。');
270
+ }
271
+ });
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
287
+ await new Promise(() => {});
288
+ }
@@ -0,0 +1,38 @@
1
+ // Shared message splitting utility
2
+ export function splitMessage(text, options = {}) {
3
+ const { maxLength = 2000, addContinuationMarker = true, continuationMarker = '\n\n[continued...]' } = options;
4
+
5
+ if (text.length <= maxLength) {
6
+ return [text];
7
+ }
8
+
9
+ const chunks = [];
10
+ let remaining = text;
11
+
12
+ while (remaining.length > maxLength) {
13
+ let splitPoint = remaining.lastIndexOf('\n```\n', maxLength);
14
+ if (splitPoint < maxLength / 2) {
15
+ splitPoint = remaining.lastIndexOf('\n\n', maxLength);
16
+ }
17
+ if (splitPoint < maxLength / 2) {
18
+ splitPoint = remaining.lastIndexOf('\n', maxLength);
19
+ }
20
+ if (splitPoint < maxLength / 2) {
21
+ splitPoint = maxLength;
22
+ }
23
+ chunks.push(remaining.slice(0, splitPoint).trim());
24
+ remaining = remaining.slice(splitPoint).trim();
25
+ }
26
+
27
+ if (remaining) {
28
+ chunks.push(remaining);
29
+ }
30
+
31
+ if (addContinuationMarker && chunks.length > 1) {
32
+ for (let i = 0; i < chunks.length - 1; i++) {
33
+ chunks[i] += continuationMarker;
34
+ }
35
+ }
36
+
37
+ return chunks;
38
+ }