@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.
- package/README.md +82 -0
- package/bin/opencode-remote.js +70 -0
- package/bin/opencode-weixin.js +10 -0
- package/dist/AGENTS.md +20 -0
- package/dist/MEMORY.md +21 -0
- package/dist/bot-runner.js +180 -0
- package/dist/cli.js +256 -0
- package/dist/core/approval.js +95 -0
- package/dist/core/auth.js +119 -0
- package/dist/core/config.js +61 -0
- package/dist/core/notifications.js +134 -0
- package/dist/core/qiniu.js +267 -0
- package/dist/core/registry.js +86 -0
- package/dist/core/router.js +344 -0
- package/dist/core/session.js +403 -0
- package/dist/core/setup.js +418 -0
- package/dist/core/types.js +16 -0
- package/dist/feishu/adapter.js +72 -0
- package/dist/feishu/bot.js +168 -0
- package/dist/feishu/commands.js +601 -0
- package/dist/feishu/handler.js +380 -0
- package/dist/index.js +60 -0
- package/dist/opencode/client.js +823 -0
- package/dist/package-lock.json +762 -0
- package/dist/patch_spawn.js +28 -0
- package/dist/plugins/agents/acp/acp-adapter.js +42 -0
- package/dist/plugins/agents/claude-code/index.js +69 -0
- package/dist/plugins/agents/codex/index.js +44 -0
- package/dist/plugins/agents/copilot/index.js +44 -0
- package/dist/plugins/agents/opencode/index.js +66 -0
- package/dist/telegram/bot.js +288 -0
- package/dist/utils/message-split.js +38 -0
- package/dist/web/code-viewer.js +266 -0
- package/dist/weixin/adapter.js +135 -0
- package/dist/weixin/api.js +179 -0
- package/dist/weixin/bot.js +183 -0
- package/dist/weixin/commands.js +758 -0
- package/dist/weixin/handler.js +577 -0
- package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
- package/dist/weixin/node_modules/encodeurl/README.md +109 -0
- package/dist/weixin/node_modules/encodeurl/index.js +60 -0
- package/dist/weixin/node_modules/encodeurl/package.json +40 -0
- package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
- package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
- package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
- package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
- package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
- package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
- package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
- package/dist/weixin/node_modules/qiniu/README.md +56 -0
- package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
- package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
- package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
- package/dist/weixin/node_modules/qiniu/index.js +32 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
- package/dist/weixin/node_modules/qiniu/package.json +80 -0
- package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
- package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
- package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
- package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
- package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
- package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
- package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
- package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
- package/dist/weixin/types.js +25 -0
- package/package.json +56 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// OpenCode Remote Control - Setup/Config module
|
|
2
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { loadWeixinCredentials } from '../weixin/bot.js';
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = join(homedir(), '.opencode-remote');
|
|
9
|
+
const CONFIG_FILE = join(CONFIG_DIR, '.env');
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
14
|
+
const VERSION = packageJson.version;
|
|
15
|
+
|
|
16
|
+
export { VERSION };
|
|
17
|
+
|
|
18
|
+
export function printBanner() {
|
|
19
|
+
console.log(`
|
|
20
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
21
|
+
OpenCode Remote Control v${VERSION}
|
|
22
|
+
Control OpenCode from Telegram, Feishu, or WeChat
|
|
23
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function promptChannel() {
|
|
28
|
+
console.log('\n📝 Select a channel to configure:');
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(' 1. Telegram');
|
|
31
|
+
console.log(' 2. Feishu (飞书)');
|
|
32
|
+
console.log(' 3. Weixin (微信)');
|
|
33
|
+
console.log('');
|
|
34
|
+
process.stdout.write('Enter your choice (1, 2 or 3): ');
|
|
35
|
+
const choice = await new Promise((resolve) => {
|
|
36
|
+
process.stdin.setEncoding('utf8');
|
|
37
|
+
const cleanup = () => {
|
|
38
|
+
process.stdin.pause();
|
|
39
|
+
process.removeListener('SIGINT', onSigint);
|
|
40
|
+
};
|
|
41
|
+
const onSigint = () => {
|
|
42
|
+
cleanup();
|
|
43
|
+
console.log('\nCancelled');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
};
|
|
46
|
+
process.once('SIGINT', onSigint);
|
|
47
|
+
process.stdin.once('data', (chunk) => {
|
|
48
|
+
cleanup();
|
|
49
|
+
resolve(chunk.toString().trim());
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
if (choice === '1' || choice.toLowerCase() === 'telegram') {
|
|
53
|
+
return 'telegram';
|
|
54
|
+
}
|
|
55
|
+
else if (choice === '2' || choice.toLowerCase() === 'feishu') {
|
|
56
|
+
return 'feishu';
|
|
57
|
+
}
|
|
58
|
+
else if (choice === '3' || choice.toLowerCase() === 'weixin' || choice.toLowerCase() === '微信') {
|
|
59
|
+
return 'weixin';
|
|
60
|
+
}
|
|
61
|
+
console.log('Invalid choice, defaulting to Telegram');
|
|
62
|
+
return 'telegram';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function promptToken() {
|
|
66
|
+
console.log('\n📝 Setup required: Telegram Bot Token');
|
|
67
|
+
console.log('\nHow to get a token:');
|
|
68
|
+
console.log(' 1. Open Telegram');
|
|
69
|
+
console.log(' 2. Search @BotFather');
|
|
70
|
+
console.log(' 3. Send /newbot and follow instructions');
|
|
71
|
+
console.log(' 4. Copy the token you receive');
|
|
72
|
+
console.log('');
|
|
73
|
+
process.stdout.write('Enter your bot token: ');
|
|
74
|
+
const token = await new Promise((resolve) => {
|
|
75
|
+
process.stdin.resume();
|
|
76
|
+
process.stdin.setEncoding('utf8');
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
process.stdin.pause();
|
|
79
|
+
process.removeListener('SIGINT', onSigint);
|
|
80
|
+
};
|
|
81
|
+
const onSigint = () => {
|
|
82
|
+
cleanup();
|
|
83
|
+
console.log('\nCancelled');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
process.once('SIGINT', onSigint);
|
|
87
|
+
process.stdin.once('data', (chunk) => {
|
|
88
|
+
cleanup();
|
|
89
|
+
resolve(chunk.toString().trim());
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
if (!token) {
|
|
93
|
+
console.log('\nCancelled');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
return token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function promptFeishuConfig() {
|
|
100
|
+
let existingAppId = '';
|
|
101
|
+
let existingAppSecret = '';
|
|
102
|
+
if (existsSync(CONFIG_FILE)) {
|
|
103
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
104
|
+
const appIdMatch = content.match(/FEISHU_APP_ID=(.+)/);
|
|
105
|
+
if (appIdMatch)
|
|
106
|
+
existingAppId = appIdMatch[1].trim();
|
|
107
|
+
const appSecretMatch = content.match(/FEISHU_APP_SECRET=(.+)/);
|
|
108
|
+
if (appSecretMatch)
|
|
109
|
+
existingAppSecret = appSecretMatch[1].trim();
|
|
110
|
+
}
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
113
|
+
console.log(' 📁 Config file: ' + CONFIG_FILE);
|
|
114
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('📝 Step 1: Create Feishu App');
|
|
117
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(' 1. Go to https://open.feishu.cn/app');
|
|
120
|
+
console.log(' 2. Click "创建企业自建应用" (Create enterprise app)');
|
|
121
|
+
console.log(' 3. Fill in app name and description');
|
|
122
|
+
console.log(' 4. Go to "凭证与基础信息" (Credentials) page');
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('⚠️ Important: In "事件订阅" (Event Subscription),');
|
|
125
|
+
console.log(' select "使用长连接接收事件" (Use long connection to receive events)');
|
|
126
|
+
console.log(' This allows the bot to work without ngrok/cloudflared!');
|
|
127
|
+
console.log('');
|
|
128
|
+
const promptInput = async (promptText, defaultValue) => {
|
|
129
|
+
if (defaultValue) {
|
|
130
|
+
const masked = defaultValue.length > 8
|
|
131
|
+
? defaultValue.slice(0, 4) + '****' + defaultValue.slice(-4)
|
|
132
|
+
: '****';
|
|
133
|
+
process.stdout.write(`${promptText} [current: ${masked}]: `);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
process.stdout.write(promptText);
|
|
137
|
+
}
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
process.stdin.resume();
|
|
140
|
+
process.stdin.setEncoding('utf8');
|
|
141
|
+
const cleanup = () => {
|
|
142
|
+
process.stdin.pause();
|
|
143
|
+
process.removeListener('SIGINT', onSigint);
|
|
144
|
+
};
|
|
145
|
+
const onSigint = () => {
|
|
146
|
+
cleanup();
|
|
147
|
+
console.log('\nCancelled');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
};
|
|
150
|
+
process.once('SIGINT', onSigint);
|
|
151
|
+
process.stdin.once('data', (chunk) => {
|
|
152
|
+
cleanup();
|
|
153
|
+
const input = chunk.toString().trim();
|
|
154
|
+
resolve(input || defaultValue);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
const appId = await promptInput('Enter your App ID', existingAppId);
|
|
159
|
+
if (!appId) {
|
|
160
|
+
console.log('\n❌ App ID is required. Press Ctrl+C to cancel.');
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
const appSecret = await promptInput('Enter your App Secret', existingAppSecret);
|
|
164
|
+
if (!appSecret) {
|
|
165
|
+
console.log('\n❌ App Secret is required. Press Ctrl+C to cancel.');
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
return { appId, appSecret };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function showFeishuSetupGuide() {
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
174
|
+
console.log(' 📋 Step 2: Configure App Permissions');
|
|
175
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(' Go to: 权限管理 → API权限');
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log(' Click "批量添加" (Batch Add) and paste this JSON:');
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(' ┌────────────────────────────────────────────────────┐');
|
|
182
|
+
console.log(' │ │');
|
|
183
|
+
console.log(' │ { │');
|
|
184
|
+
console.log(' │ "im:message", │');
|
|
185
|
+
console.log(' │ "im:message:send_as_bot", │');
|
|
186
|
+
console.log(' │ "im:message:receive_as_bot" │');
|
|
187
|
+
console.log(' │ } │');
|
|
188
|
+
console.log(' │ │');
|
|
189
|
+
console.log(' └────────────────────────────────────────────────────┘');
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log(' 📋 Or add manually:');
|
|
192
|
+
console.log(' • im:message - 获取与发送消息');
|
|
193
|
+
console.log(' • im:message:send_as_bot - 以应用身份发消息');
|
|
194
|
+
console.log(' • im:message:receive_as_bot - 接收机器人消息');
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
197
|
+
console.log(' 🤖 Step 3: Enable Robot Capability');
|
|
198
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(' Go to: 应用能力 → 机器人');
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(' Enable these options:');
|
|
203
|
+
console.log(' ☑️ 启用机器人');
|
|
204
|
+
console.log(' ☑️ 机器人可主动发送消息给用户');
|
|
205
|
+
console.log(' ☑️ 用户可与机器人进行单聊');
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
208
|
+
console.log(' 🚀 Step 4: Start the Bot FIRST! (CRITICAL)');
|
|
209
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(' ⚠️ You MUST start the bot BEFORE configuring event subscription!');
|
|
212
|
+
console.log('');
|
|
213
|
+
console.log(' Run in another terminal:');
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(' opencode-remote feishu');
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log(' Wait for: "ws client ready" (WebSocket connected)');
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(' ✨ Long Connection Mode = NO ngrok/cloudflared needed!');
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
222
|
+
console.log(' 🔗 Step 5: Configure Event Subscription');
|
|
223
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
224
|
+
console.log('');
|
|
225
|
+
console.log(' (Make sure the bot is running! If not, start it first)');
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(' 1. Go to "事件订阅" (Event Subscription) page');
|
|
228
|
+
console.log(' 2. 订阅方式: 选择 "使用长连接接收事件"');
|
|
229
|
+
console.log(' (NOT "将事件发送至开发者服务器"!)');
|
|
230
|
+
console.log(' 3. Click "添加事件" and select:');
|
|
231
|
+
console.log(' - im.message.receive_v1 (接收消息)');
|
|
232
|
+
console.log(' 4. Save configuration');
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(' ❌ If you see "未检测到应用连接信息":');
|
|
235
|
+
console.log(' → The bot is not running. Start it first!');
|
|
236
|
+
console.log('');
|
|
237
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
238
|
+
console.log(' 📤 Step 6: Publish App');
|
|
239
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(' 1. Go to "版本管理与发布" (Version & Publish)');
|
|
242
|
+
console.log(' 2. Click "创建版本" (Create Version)');
|
|
243
|
+
console.log(' 3. Fill in version info and submit for review');
|
|
244
|
+
console.log(' 4. After approval, click "发布" (Publish)');
|
|
245
|
+
console.log(' 5. Search your bot in Feishu and start chatting!');
|
|
246
|
+
console.log('');
|
|
247
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function saveConfig(token) {
|
|
251
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
252
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
let existing = '';
|
|
255
|
+
if (existsSync(CONFIG_FILE)) {
|
|
256
|
+
existing = readFileSync(CONFIG_FILE, 'utf-8');
|
|
257
|
+
}
|
|
258
|
+
const lines = existing.split('\n').filter(line => !line.startsWith('TELEGRAM_BOT_TOKEN='));
|
|
259
|
+
lines.push(`TELEGRAM_BOT_TOKEN=${token}`);
|
|
260
|
+
writeFileSync(CONFIG_FILE, lines.join('\n'));
|
|
261
|
+
console.log(`\n✅ Token saved to ${CONFIG_FILE}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function saveFeishuConfig(appId, appSecret) {
|
|
265
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
266
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
267
|
+
}
|
|
268
|
+
let existing = '';
|
|
269
|
+
if (existsSync(CONFIG_FILE)) {
|
|
270
|
+
existing = readFileSync(CONFIG_FILE, 'utf-8');
|
|
271
|
+
}
|
|
272
|
+
const lines = existing.split('\n').filter(line => !line.startsWith('FEISHU_APP_ID=') &&
|
|
273
|
+
!line.startsWith('FEISHU_APP_SECRET='));
|
|
274
|
+
lines.push(`FEISHU_APP_ID=${appId}`);
|
|
275
|
+
lines.push(`FEISHU_APP_SECRET=${appSecret}`);
|
|
276
|
+
writeFileSync(CONFIG_FILE, lines.join('\n'));
|
|
277
|
+
console.log(`\n✅ Feishu config saved to ${CONFIG_FILE}`);
|
|
278
|
+
showFeishuSetupGuide();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function getCurrentTimeout() {
|
|
282
|
+
if (existsSync(CONFIG_FILE)) {
|
|
283
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
284
|
+
const match = content.match(/OPENCODE_REQUEST_TIMEOUT_MINUTES=(\d+)/);
|
|
285
|
+
if (match) {
|
|
286
|
+
return parseInt(match[1], 10);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return 30;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function saveTimeoutConfig(timeoutMinutes) {
|
|
293
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
294
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
let existing = '';
|
|
297
|
+
if (existsSync(CONFIG_FILE)) {
|
|
298
|
+
existing = readFileSync(CONFIG_FILE, 'utf-8');
|
|
299
|
+
}
|
|
300
|
+
const lines = existing.split('\n').filter(line => !line.startsWith('OPENCODE_REQUEST_TIMEOUT_MINUTES='));
|
|
301
|
+
lines.push(`OPENCODE_REQUEST_TIMEOUT_MINUTES=${timeoutMinutes}`);
|
|
302
|
+
writeFileSync(CONFIG_FILE, lines.join('\n'));
|
|
303
|
+
console.log(`\n✅ Timeout saved: ${timeoutMinutes} minutes`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function runConfigTimeout() {
|
|
307
|
+
printBanner();
|
|
308
|
+
const currentTimeout = getCurrentTimeout();
|
|
309
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
310
|
+
console.log(' ⏱️ Request Timeout Configuration');
|
|
311
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(' This sets how long to wait for OpenCode responses.');
|
|
314
|
+
console.log(' Increase this if you get "fetch failed" errors for long tasks.');
|
|
315
|
+
console.log('');
|
|
316
|
+
console.log(` Current timeout: ${currentTimeout} minutes`);
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log(' Recommended values:');
|
|
319
|
+
console.log(' • 30 minutes (default, good for most tasks)');
|
|
320
|
+
console.log(' • 60 minutes (for complex refactoring)');
|
|
321
|
+
console.log(' • 120 minutes (for large projects)');
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
324
|
+
console.log('');
|
|
325
|
+
process.stdout.write('Enter timeout in minutes (or press Enter to keep current): ');
|
|
326
|
+
const input = await new Promise((resolve) => {
|
|
327
|
+
process.stdin.resume();
|
|
328
|
+
process.stdin.setEncoding('utf8');
|
|
329
|
+
const cleanup = () => {
|
|
330
|
+
process.stdin.pause();
|
|
331
|
+
process.removeListener('SIGINT', onSigint);
|
|
332
|
+
};
|
|
333
|
+
const onSigint = () => {
|
|
334
|
+
cleanup();
|
|
335
|
+
console.log('\nCancelled');
|
|
336
|
+
process.exit(0);
|
|
337
|
+
};
|
|
338
|
+
process.once('SIGINT', onSigint);
|
|
339
|
+
process.stdin.once('data', (chunk) => {
|
|
340
|
+
cleanup();
|
|
341
|
+
resolve(chunk.toString().trim());
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
if (!input) {
|
|
345
|
+
console.log('\n⏭️ No change made.');
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
const timeout = parseInt(input, 10);
|
|
349
|
+
if (isNaN(timeout) || timeout < 1) {
|
|
350
|
+
console.log('\n❌ Invalid number. Please enter a positive number.');
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
if (timeout > 480) {
|
|
354
|
+
console.log('\n⚠️ Warning: Timeout exceeds 8 hours. Are you sure?');
|
|
355
|
+
}
|
|
356
|
+
await saveTimeoutConfig(timeout);
|
|
357
|
+
console.log('\n🚀 Restart opencode-remote for the change to take effect.');
|
|
358
|
+
process.exit(0);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function runConfig() {
|
|
362
|
+
printBanner();
|
|
363
|
+
const channel = await promptChannel();
|
|
364
|
+
if (channel === 'telegram') {
|
|
365
|
+
const token = await promptToken();
|
|
366
|
+
if (!token || token === 'your_bot_token_here') {
|
|
367
|
+
console.log('\n❌ Invalid token. Please try again.');
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
await saveConfig(token);
|
|
371
|
+
console.log('\n🚀 Ready! Run `opencode-remote` to start the bot.');
|
|
372
|
+
}
|
|
373
|
+
else if (channel === 'feishu') {
|
|
374
|
+
const { appId, appSecret } = await promptFeishuConfig();
|
|
375
|
+
if (!appId || !appSecret) {
|
|
376
|
+
console.log('\n❌ Invalid credentials. Please try again.');
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
await saveFeishuConfig(appId, appSecret);
|
|
380
|
+
console.log('\n🚀 Ready! Run `opencode-remote feishu` to start the Feishu bot.');
|
|
381
|
+
}
|
|
382
|
+
else if (channel === 'weixin') {
|
|
383
|
+
console.log('');
|
|
384
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
385
|
+
console.log(' 📱 Weixin (微信) Login');
|
|
386
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log(' Weixin bot uses QR code login.');
|
|
389
|
+
console.log(' No manual configuration needed!');
|
|
390
|
+
console.log('');
|
|
391
|
+
console.log(' To set up Weixin:');
|
|
392
|
+
console.log('');
|
|
393
|
+
console.log(' 1. Run: opencode-remote weixin');
|
|
394
|
+
console.log(' 2. Scan the QR code with your WeChat app');
|
|
395
|
+
console.log(' 3. Confirm login on your phone');
|
|
396
|
+
console.log('');
|
|
397
|
+
console.log(' Credentials will be saved automatically.');
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
400
|
+
console.log('');
|
|
401
|
+
console.log('🚀 Run `opencode-remote weixin` to start the login process.');
|
|
402
|
+
}
|
|
403
|
+
process.exit(0);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function hasTelegramConfig(config) {
|
|
407
|
+
return !!(config.telegramBotToken?.trim());
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function hasFeishuConfig(config) {
|
|
411
|
+
return !!(config.feishuAppId?.trim() &&
|
|
412
|
+
config.feishuAppSecret?.trim());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function hasWeixinConfig() {
|
|
416
|
+
const creds = loadWeixinCredentials();
|
|
417
|
+
return !!(creds?.token && creds?.accountId);
|
|
418
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Core types for OpenCode Remote Control
|
|
2
|
+
// Message templates - emoji vocabulary
|
|
3
|
+
export const EMOJI = {
|
|
4
|
+
SUCCESS: '✅',
|
|
5
|
+
ERROR: '❌',
|
|
6
|
+
LOADING: '⏳',
|
|
7
|
+
THINKING: '🤔',
|
|
8
|
+
APPROVAL: '📝',
|
|
9
|
+
FILES: '📄',
|
|
10
|
+
CODE: '🔧',
|
|
11
|
+
START: '🚀',
|
|
12
|
+
EXPIRED: '💤',
|
|
13
|
+
WARNING: '⚠️',
|
|
14
|
+
QUESTION: '💬',
|
|
15
|
+
};
|
|
16
|
+
export { loadConfig } from './config.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
|
|
3
|
+
function createFeishuAdapter(client) {
|
|
4
|
+
return {
|
|
5
|
+
async reply(threadId, text) {
|
|
6
|
+
const chatId = threadId.replace('feishu:', '');
|
|
7
|
+
try {
|
|
8
|
+
const result = await client.im.message.create({
|
|
9
|
+
params: { receive_id_type: 'chat_id' },
|
|
10
|
+
data: {
|
|
11
|
+
receive_id: chatId,
|
|
12
|
+
msg_type: 'text',
|
|
13
|
+
content: JSON.stringify({ text }),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
return result.data?.message_id || '';
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error('发送飞书消息失败:', error);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
async sendTypingIndicator(threadId) {
|
|
24
|
+
const chatId = threadId.replace('feishu:', '');
|
|
25
|
+
try {
|
|
26
|
+
const result = await client.im.message.create({
|
|
27
|
+
params: { receive_id_type: 'chat_id' },
|
|
28
|
+
data: {
|
|
29
|
+
receive_id: chatId,
|
|
30
|
+
msg_type: 'text',
|
|
31
|
+
content: JSON.stringify({ text: '⏳ 思考中...' }),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return result.data?.message_id || '';
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error('发送思考指示失败:', error);
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async updateMessage(threadId, messageId, text) {
|
|
42
|
+
if (!messageId)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
await client.im.message.patch({
|
|
46
|
+
path: { message_id: messageId },
|
|
47
|
+
data: {
|
|
48
|
+
content: JSON.stringify({ text }),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn('更新消息失败:', error);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async deleteMessage(threadId, messageId) {
|
|
57
|
+
if (!messageId)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
await client.im.message.delete({
|
|
61
|
+
path: { message_id: messageId },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.warn('删除消息失败:', error);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { createFeishuAdapter };
|
|
72
|
+
export default createFeishuAdapter;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import { initSessionManager } from '../core/session.js';
|
|
3
|
+
import { initOpenCode } from '../opencode/client.js';
|
|
4
|
+
import { getAuthStatus } from '../core/auth.js';
|
|
5
|
+
import { createFeishuAdapter } from './adapter.js';
|
|
6
|
+
import { handleMessage } from './handler.js';
|
|
7
|
+
|
|
8
|
+
let feishuClient = null;
|
|
9
|
+
let wsClient = null;
|
|
10
|
+
let config = null;
|
|
11
|
+
let openCodeSessions = null;
|
|
12
|
+
|
|
13
|
+
function feishuEventToContext(event) {
|
|
14
|
+
return {
|
|
15
|
+
platform: 'feishu',
|
|
16
|
+
threadId: `feishu:${event.message.chat_id}`,
|
|
17
|
+
userId: event.sender?.sender_id?.user_id || 'unknown',
|
|
18
|
+
messageId: event.message.message_id,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const rateLimitMap = new Map();
|
|
23
|
+
const RATE_LIMIT = 100;
|
|
24
|
+
const RATE_WINDOW = 60000;
|
|
25
|
+
|
|
26
|
+
function checkRateLimit(chatId) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const entry = rateLimitMap.get(chatId);
|
|
29
|
+
if (!entry || now > entry.resetTime) {
|
|
30
|
+
rateLimitMap.set(chatId, { count: 1, resetTime: now + RATE_WINDOW });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (entry.count >= RATE_LIMIT) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
entry.count++;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function startFeishuBot(botConfig) {
|
|
41
|
+
config = botConfig;
|
|
42
|
+
if (!config.feishuAppId || !config.feishuAppSecret) {
|
|
43
|
+
throw new Error('飞书未配置。运行: opencode-remote config');
|
|
44
|
+
}
|
|
45
|
+
feishuClient = new lark.Client({
|
|
46
|
+
appId: config.feishuAppId,
|
|
47
|
+
appSecret: config.feishuAppSecret,
|
|
48
|
+
});
|
|
49
|
+
initSessionManager(config);
|
|
50
|
+
openCodeSessions = new Map();
|
|
51
|
+
console.log('🔧 正在初始化 OpenCode...');
|
|
52
|
+
try {
|
|
53
|
+
await initOpenCode();
|
|
54
|
+
console.log('✅ OpenCode 就绪');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('❌ OpenCode 初始化失败:', error);
|
|
58
|
+
}
|
|
59
|
+
const adapter = createFeishuAdapter(feishuClient);
|
|
60
|
+
wsClient = new lark.WSClient({
|
|
61
|
+
appId: config.feishuAppId,
|
|
62
|
+
appSecret: config.feishuAppSecret,
|
|
63
|
+
});
|
|
64
|
+
const eventDispatcher = new lark.EventDispatcher({}).register({
|
|
65
|
+
'im.message.receive_v1': async (data) => {
|
|
66
|
+
console.log('📩 收到消息事件');
|
|
67
|
+
try {
|
|
68
|
+
if (!data?.message) {
|
|
69
|
+
console.warn('收到无消息内容的事件');
|
|
70
|
+
return { code: 0 };
|
|
71
|
+
}
|
|
72
|
+
const chatId = data.message.chat_id;
|
|
73
|
+
if (!checkRateLimit(chatId)) {
|
|
74
|
+
console.warn(`频率超限: ${chatId}`);
|
|
75
|
+
return { code: 0 };
|
|
76
|
+
}
|
|
77
|
+
let text = '';
|
|
78
|
+
try {
|
|
79
|
+
const content = JSON.parse(data.message.content);
|
|
80
|
+
text = content.text || '';
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
text = data.message.content || '';
|
|
84
|
+
}
|
|
85
|
+
if (!text.trim()) {
|
|
86
|
+
return { code: 0 };
|
|
87
|
+
}
|
|
88
|
+
const ctx = feishuEventToContext(data);
|
|
89
|
+
handleMessage(adapter, ctx, text, openCodeSessions).catch(error => {
|
|
90
|
+
console.error('处理飞书消息失败:', error);
|
|
91
|
+
});
|
|
92
|
+
return { code: 0 };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('飞书事件处理器错误:', error);
|
|
96
|
+
return { code: 0 };
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
console.log('🔗 正在启动飞书 WebSocket 长连接...');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('✨ 长连接模式 - 无需隧道/ngrok!');
|
|
103
|
+
console.log(' 确保你的电脑可以访问互联网。');
|
|
104
|
+
console.log('');
|
|
105
|
+
const authStatus = getAuthStatus();
|
|
106
|
+
if (!authStatus.feishu) {
|
|
107
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
108
|
+
console.log(' 🔐 安全提示');
|
|
109
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' Bot 尚未绑定安全所有者!');
|
|
112
|
+
console.log(' FIRST 发送 /start 的人将成为所有者。');
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(' 👉 打开飞书向 Bot 发送 /start!');
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log('🔒 Bot 已绑定(所有者已认证)');
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
|
123
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
124
|
+
console.log(' 📋 配置检查清单');
|
|
125
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log(' Step 1: 添加权限 (权限管理 → API权限)');
|
|
128
|
+
console.log(' ────────────────────────────────────────');
|
|
129
|
+
console.log(' 点击"批量添加"(Batch Add) 并粘贴以下 JSON:');
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(' ┌────────────────────────────────────────────────────┐');
|
|
132
|
+
console.log(' │ { │');
|
|
133
|
+
console.log(' │ "im:message", │');
|
|
134
|
+
console.log(' │ "im:message:send_as_bot", │');
|
|
135
|
+
console.log(' │ "im:message:receive_as_bot" │');
|
|
136
|
+
console.log(' │ } │');
|
|
137
|
+
console.log(' └────────────────────────────────────────────────────┘');
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(' Step 2: 启用机器人 (应用能力 → 机器人)');
|
|
140
|
+
console.log(' ────────────────────────────────────────');
|
|
141
|
+
console.log(' - 启用"启用机器人"');
|
|
142
|
+
console.log(' - 启用"机器人可主动发送消息给用户"');
|
|
143
|
+
console.log(' - 启用"用户可与机器人进行单聊"');
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(' Step 3: 事件订阅 (事件订阅)');
|
|
146
|
+
console.log(' ────────────────────────────────────────');
|
|
147
|
+
console.log(' ⚠️ 必须先启动此 Bot 再保存事件配置!');
|
|
148
|
+
console.log(' - 选择"使用长连接接收事件"');
|
|
149
|
+
console.log(' - 添加事件: im.message.receive_v1');
|
|
150
|
+
console.log(' - 点击保存');
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(' Step 4: 发布应用 (版本管理与发布)');
|
|
153
|
+
console.log(' ────────────────────────────────────────');
|
|
154
|
+
console.log(' - 创建版本 → 申请发布 → 发布');
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
157
|
+
console.log(' 🔍 调试: 在飞书中向你的 Bot 发送消息!');
|
|
158
|
+
console.log(' 你将看到: 📩 收到消息事件');
|
|
159
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
160
|
+
console.log('');
|
|
161
|
+
const shutdown = () => {
|
|
162
|
+
console.log('\n🛑 关闭飞书 Bot...');
|
|
163
|
+
process.exit(0);
|
|
164
|
+
};
|
|
165
|
+
process.once('SIGINT', shutdown);
|
|
166
|
+
process.once('SIGTERM', shutdown);
|
|
167
|
+
await wsClient.start({ eventDispatcher });
|
|
168
|
+
}
|