opencode-remote-control 0.2.5 → 0.2.8
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 +65 -10
- package/dist/cli.js +84 -36
- package/dist/core/auth.js +95 -0
- package/dist/core/types.js +0 -3
- package/dist/feishu/bot.js +217 -89
- package/dist/opencode/client.js +32 -4
- package/dist/telegram/bot.js +73 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# OpenCode Remote Control
|
|
2
2
|
|
|
3
|
+
> **Lightning-fast AI coding assistant powered by OpenCode — no Claude Code subscription required!**
|
|
4
|
+
|
|
3
5
|
<p align="center">
|
|
4
6
|
<a href="https://github.com/ceociocto/opencode-remote-control/actions/workflows/publish.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/ceociocto/opencode-remote-control/publish.yml?branch=main&style=for-the-badge" alt="CI status"></a>
|
|
5
7
|
<a href="https://www.npmjs.com/package/opencode-remote-control"><img src="https://img.shields.io/npm/v/opencode-remote-control?style=for-the-badge" alt="npm version"></a>
|
|
@@ -11,9 +13,13 @@
|
|
|
11
13
|
<a href="./README_CN.md">中文文档</a>
|
|
12
14
|
</p>
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
Control OpenCode from anywhere via Telegram or Feishu.
|
|
17
|
+
|
|
18
|
+
## What's New in v0.2
|
|
19
|
+
|
|
20
|
+
- **Feishu Channel** — Now supports Feishu/Lark bot with WebSocket long connection (no tunnel required!)
|
|
21
|
+
- **Faster Responses** — Optimized timeout handling for long AI responses
|
|
22
|
+
- **Better CLI** — Improved configuration experience with existing value display
|
|
17
23
|
|
|
18
24
|
## Requirements
|
|
19
25
|
|
|
@@ -21,7 +27,7 @@
|
|
|
21
27
|
- [OpenCode](https://github.com/opencode-ai/opencode) installed and accessible in PATH
|
|
22
28
|
- Telegram account (for Telegram bot)
|
|
23
29
|
- Feishu account (for Feishu bot)
|
|
24
|
-
- ngrok or cloudflared (
|
|
30
|
+
- **No** ngrok or cloudflared required (Feishu uses WebSocket long connection)
|
|
25
31
|
|
|
26
32
|
### Verify OpenCode Installation
|
|
27
33
|
|
|
@@ -68,7 +74,56 @@ Token is saved to `~/.opencode-remote/.env`
|
|
|
68
74
|
|
|
69
75
|
### Feishu Setup
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
Feishu uses **WebSocket Long Connection Mode** - no public IP or tunnel required!
|
|
78
|
+
|
|
79
|
+
#### Step 1: Create Feishu App
|
|
80
|
+
|
|
81
|
+
1. Visit [Feishu Open Platform](https://open.feishu.cn/) and login
|
|
82
|
+
2. Go to "Developer Console" → "Create Enterprise App"
|
|
83
|
+
3. Get **App ID** and **App Secret** from "Credentials & Basic Info"
|
|
84
|
+
|
|
85
|
+
#### Step 2: Configure Permissions
|
|
86
|
+
|
|
87
|
+
Go to "Permission Management" → "API Permissions" and enable:
|
|
88
|
+
|
|
89
|
+
| Permission | ID |
|
|
90
|
+
|------------|---|
|
|
91
|
+
| Get and send messages | `im:message` |
|
|
92
|
+
| Send messages as bot | `im:message:send_as_bot` |
|
|
93
|
+
| Receive bot messages | `im:message:receive_as_bot` |
|
|
94
|
+
|
|
95
|
+
#### Step 3: Enable Bot
|
|
96
|
+
|
|
97
|
+
1. Go to "App Capabilities" → "Bot"
|
|
98
|
+
2. Enable "Enable Bot"
|
|
99
|
+
3. Enable "Bot can proactively send messages to users"
|
|
100
|
+
4. Enable "Users can have private chats with bot"
|
|
101
|
+
|
|
102
|
+
#### Step 4: Configure Event Subscription (Critical!)
|
|
103
|
+
|
|
104
|
+
1. Go to "Event Subscription" page
|
|
105
|
+
2. **Subscription Method**: Select "**Use long connection to receive events**"
|
|
106
|
+
> ⚠️ Important: Choose "Use long connection to receive events", NOT "Send events to developer server"
|
|
107
|
+
3. Click "Add Event" and select:
|
|
108
|
+
- `im.message.receive_v1` - Receive messages
|
|
109
|
+
4. Save configuration
|
|
110
|
+
|
|
111
|
+
#### Step 5: Run Config Command
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
opencode-remote config
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Select "Feishu" and enter your App ID and App Secret.
|
|
118
|
+
|
|
119
|
+
#### Step 6: Publish App
|
|
120
|
+
|
|
121
|
+
1. Go to "Version Management & Publishing"
|
|
122
|
+
2. Create version → Request publishing → Publish
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
For detailed setup instructions, see [Feishu Setup Guide](./docs/FEISHU_SETUP_EN.md) or [飞书配置指南](./docs/FEISHU_SETUP.md).
|
|
72
127
|
|
|
73
128
|
## Start Service
|
|
74
129
|
|
|
@@ -145,18 +200,18 @@ Both Telegram and Feishu support the same commands:
|
|
|
145
200
|
|
|
146
201
|
The Telegram bot uses **Polling Mode** to fetch messages from Telegram servers, requiring no tunnel or public IP configuration.
|
|
147
202
|
|
|
148
|
-
### Feishu (
|
|
203
|
+
### Feishu (Long Connection Mode)
|
|
149
204
|
|
|
150
205
|
```
|
|
151
206
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
152
|
-
│ Feishu │───▶│ Feishu │───▶│
|
|
153
|
-
│ Client │ │ Server │ │
|
|
207
|
+
│ Feishu │───▶│ Feishu │───▶│ WebSocket │
|
|
208
|
+
│ Client │ │ Server │ │ (Long Conn) │
|
|
154
209
|
└─────────────┘ └─────────────┘ └──────┬──────┘
|
|
155
210
|
│
|
|
156
211
|
▼
|
|
157
212
|
┌─────────────┐
|
|
158
213
|
│ Feishu Bot │
|
|
159
|
-
│ (
|
|
214
|
+
│ (Local) │
|
|
160
215
|
└──────┬──────┘
|
|
161
216
|
│
|
|
162
217
|
▼
|
|
@@ -166,7 +221,7 @@ The Telegram bot uses **Polling Mode** to fetch messages from Telegram servers,
|
|
|
166
221
|
└─────────────┘
|
|
167
222
|
```
|
|
168
223
|
|
|
169
|
-
The Feishu bot uses **
|
|
224
|
+
The Feishu bot uses **WebSocket Long Connection Mode** - no public IP or tunnel (ngrok/cloudflared) required!
|
|
170
225
|
|
|
171
226
|
## Contributing
|
|
172
227
|
|
package/dist/cli.js
CHANGED
|
@@ -103,7 +103,24 @@ async function promptToken() {
|
|
|
103
103
|
return token;
|
|
104
104
|
}
|
|
105
105
|
async function promptFeishuConfig() {
|
|
106
|
-
|
|
106
|
+
// Read existing config to show as defaults
|
|
107
|
+
let existingAppId = '';
|
|
108
|
+
let existingAppSecret = '';
|
|
109
|
+
if (existsSync(CONFIG_FILE)) {
|
|
110
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
111
|
+
const appIdMatch = content.match(/FEISHU_APP_ID=(.+)/);
|
|
112
|
+
if (appIdMatch)
|
|
113
|
+
existingAppId = appIdMatch[1].trim();
|
|
114
|
+
const appSecretMatch = content.match(/FEISHU_APP_SECRET=(.+)/);
|
|
115
|
+
if (appSecretMatch)
|
|
116
|
+
existingAppSecret = appSecretMatch[1].trim();
|
|
117
|
+
}
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
120
|
+
console.log(' 📁 Config file: ' + CONFIG_FILE);
|
|
121
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log('📝 Step 1: Create Feishu App');
|
|
107
124
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
108
125
|
console.log('');
|
|
109
126
|
console.log(' 1. Go to https://open.feishu.cn/app');
|
|
@@ -111,8 +128,20 @@ async function promptFeishuConfig() {
|
|
|
111
128
|
console.log(' 3. Fill in app name and description');
|
|
112
129
|
console.log(' 4. Go to "凭证与基础信息" (Credentials) page');
|
|
113
130
|
console.log('');
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
console.log('⚠️ Important: In "事件订阅" (Event Subscription),');
|
|
132
|
+
console.log(' select "使用长连接接收事件" (Use long connection to receive events)');
|
|
133
|
+
console.log(' This allows the bot to work without ngrok/cloudflared!');
|
|
134
|
+
console.log('');
|
|
135
|
+
const promptInput = async (promptText, defaultValue) => {
|
|
136
|
+
if (defaultValue) {
|
|
137
|
+
const masked = defaultValue.length > 8
|
|
138
|
+
? defaultValue.slice(0, 4) + '****' + defaultValue.slice(-4)
|
|
139
|
+
: '****';
|
|
140
|
+
process.stdout.write(`${promptText} [current: ${masked}]: `);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
process.stdout.write(promptText);
|
|
144
|
+
}
|
|
116
145
|
return new Promise((resolve) => {
|
|
117
146
|
process.stdin.resume();
|
|
118
147
|
process.stdin.setEncoding('utf8');
|
|
@@ -128,18 +157,20 @@ async function promptFeishuConfig() {
|
|
|
128
157
|
process.once('SIGINT', onSigint);
|
|
129
158
|
process.stdin.once('data', (chunk) => {
|
|
130
159
|
cleanup();
|
|
131
|
-
|
|
160
|
+
const input = chunk.toString().trim();
|
|
161
|
+
// If user presses enter without input, use default value
|
|
162
|
+
resolve(input || defaultValue);
|
|
132
163
|
});
|
|
133
164
|
});
|
|
134
165
|
};
|
|
135
|
-
const appId = await promptInput('Enter your App ID
|
|
166
|
+
const appId = await promptInput('Enter your App ID', existingAppId);
|
|
136
167
|
if (!appId) {
|
|
137
|
-
console.log('\
|
|
168
|
+
console.log('\n❌ App ID is required. Press Ctrl+C to cancel.');
|
|
138
169
|
process.exit(0);
|
|
139
170
|
}
|
|
140
|
-
const appSecret = await promptInput('Enter your App Secret
|
|
171
|
+
const appSecret = await promptInput('Enter your App Secret', existingAppSecret);
|
|
141
172
|
if (!appSecret) {
|
|
142
|
-
console.log('\
|
|
173
|
+
console.log('\n❌ App Secret is required. Press Ctrl+C to cancel.');
|
|
143
174
|
process.exit(0);
|
|
144
175
|
}
|
|
145
176
|
return { appId, appSecret };
|
|
@@ -150,51 +181,68 @@ function showFeishuSetupGuide() {
|
|
|
150
181
|
console.log(' 📋 Step 2: Configure App Permissions');
|
|
151
182
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
152
183
|
console.log('');
|
|
153
|
-
console.log(' Go to
|
|
154
|
-
console.log(' Search and enable these permissions:');
|
|
184
|
+
console.log(' Go to: 权限管理 → API权限');
|
|
155
185
|
console.log('');
|
|
156
|
-
console.log('
|
|
157
|
-
console.log(' │ Permission │ Scope │');
|
|
158
|
-
console.log(' ├────────────────────────────────────────────────────┤');
|
|
159
|
-
console.log(' │ im:message 获取与发送消息 │');
|
|
160
|
-
console.log(' │ im:message:send_as_bot 以应用身份发消息 │');
|
|
161
|
-
console.log(' │ im:message:receive_as_bot 接收机器人消息 │');
|
|
162
|
-
console.log(' └────────────────────────────────────────────────────┘');
|
|
163
|
-
console.log('');
|
|
164
|
-
console.log(' 💡 TIP: Copy the JSON below and use "批量添加" feature:');
|
|
186
|
+
console.log(' Click "批量添加" (Batch Add) and paste this JSON:');
|
|
165
187
|
console.log('');
|
|
166
188
|
console.log(' ┌────────────────────────────────────────────────────┐');
|
|
167
|
-
console.log(' │
|
|
168
|
-
console.log(' │
|
|
169
|
-
console.log(' │
|
|
170
|
-
console.log(' │
|
|
171
|
-
console.log(' │
|
|
189
|
+
console.log(' │ │');
|
|
190
|
+
console.log(' │ { │');
|
|
191
|
+
console.log(' │ "im:message", │');
|
|
192
|
+
console.log(' │ "im:message:send_as_bot", │');
|
|
193
|
+
console.log(' │ "im:message:receive_as_bot" │');
|
|
194
|
+
console.log(' │ } │');
|
|
195
|
+
console.log(' │ │');
|
|
172
196
|
console.log(' └────────────────────────────────────────────────────┘');
|
|
173
197
|
console.log('');
|
|
198
|
+
console.log(' 📋 Or add manually:');
|
|
199
|
+
console.log(' • im:message - 获取与发送消息');
|
|
200
|
+
console.log(' • im:message:send_as_bot - 以应用身份发消息');
|
|
201
|
+
console.log(' • im:message:receive_as_bot - 接收机器人消息');
|
|
202
|
+
console.log('');
|
|
174
203
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
175
204
|
console.log(' 🤖 Step 3: Enable Robot Capability');
|
|
176
205
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
177
206
|
console.log('');
|
|
178
|
-
console.log('
|
|
179
|
-
console.log('
|
|
180
|
-
console.log('
|
|
207
|
+
console.log(' Go to: 应用能力 → 机器人');
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(' Enable these options:');
|
|
210
|
+
console.log(' ☑️ 启用机器人');
|
|
211
|
+
console.log(' ☑️ 机器人可主动发送消息给用户');
|
|
212
|
+
console.log(' ☑️ 用户可与机器人进行单聊');
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
215
|
+
console.log(' 🚀 Step 4: Start the Bot FIRST! (CRITICAL)');
|
|
216
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(' ⚠️ You MUST start the bot BEFORE configuring event subscription!');
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(' Run in another terminal:');
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(' opencode-remote feishu');
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(' Wait for: "ws client ready" (WebSocket connected)');
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log(' ✨ Long Connection Mode = NO ngrok/cloudflared needed!');
|
|
181
227
|
console.log('');
|
|
182
228
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
183
|
-
console.log(' 🔗 Step
|
|
229
|
+
console.log(' 🔗 Step 5: Configure Event Subscription');
|
|
184
230
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
185
231
|
console.log('');
|
|
186
|
-
console.log('
|
|
187
|
-
console.log(' $ opencode-remote feishu');
|
|
232
|
+
console.log(' (Make sure the bot is running! If not, start it first)');
|
|
188
233
|
console.log('');
|
|
189
|
-
console.log('
|
|
190
|
-
console.log('
|
|
234
|
+
console.log(' 1. Go to "事件订阅" (Event Subscription) page');
|
|
235
|
+
console.log(' 2. 订阅方式: 选择 "使用长连接接收事件"');
|
|
236
|
+
console.log(' (NOT "将事件发送至开发者服务器"!)');
|
|
237
|
+
console.log(' 3. Click "添加事件" and select:');
|
|
238
|
+
console.log(' - im.message.receive_v1 (接收消息)');
|
|
239
|
+
console.log(' 4. Save configuration');
|
|
191
240
|
console.log('');
|
|
192
|
-
console.log('
|
|
193
|
-
console.log('
|
|
194
|
-
console.log(' 5. Add event: im.message.receive_v1');
|
|
241
|
+
console.log(' ❌ If you see "未检测到应用连接信息":');
|
|
242
|
+
console.log(' → The bot is not running. Start it first!');
|
|
195
243
|
console.log('');
|
|
196
244
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
197
|
-
console.log(' 📤 Step
|
|
245
|
+
console.log(' 📤 Step 6: Publish App');
|
|
198
246
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
199
247
|
console.log('');
|
|
200
248
|
console.log(' 1. Go to "版本管理与发布" (Version & Publish)');
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Authorization management for OpenCode Remote Control
|
|
2
|
+
// First user to send /start becomes the owner automatically
|
|
3
|
+
const authState = {
|
|
4
|
+
telegramOwner: null,
|
|
5
|
+
feishuOwner: null,
|
|
6
|
+
};
|
|
7
|
+
// Auth file path for persistence
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
const AUTH_DIR = join(homedir(), '.opencode-remote');
|
|
12
|
+
const AUTH_FILE = join(AUTH_DIR, 'auth.json');
|
|
13
|
+
function ensureAuthDir() {
|
|
14
|
+
if (!existsSync(AUTH_DIR)) {
|
|
15
|
+
mkdirSync(AUTH_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function loadAuth() {
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(AUTH_FILE)) {
|
|
21
|
+
const data = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
|
|
22
|
+
authState.telegramOwner = data.telegramOwner || null;
|
|
23
|
+
authState.feishuOwner = data.feishuOwner || null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.warn('Failed to load auth state, starting fresh:', error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function saveAuth() {
|
|
31
|
+
try {
|
|
32
|
+
ensureAuthDir();
|
|
33
|
+
writeFileSync(AUTH_FILE, JSON.stringify(authState, null, 2));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('Failed to save auth state:', error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Initialize on module load
|
|
40
|
+
loadAuth();
|
|
41
|
+
export function isAuthorized(platform, userId) {
|
|
42
|
+
if (platform === 'telegram') {
|
|
43
|
+
return authState.telegramOwner === userId;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return authState.feishuOwner === userId;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function hasOwner(platform) {
|
|
50
|
+
if (platform === 'telegram') {
|
|
51
|
+
return authState.telegramOwner !== null;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return authState.feishuOwner !== null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function claimOwnership(platform, userId) {
|
|
58
|
+
if (platform === 'telegram') {
|
|
59
|
+
if (authState.telegramOwner) {
|
|
60
|
+
if (authState.telegramOwner === userId) {
|
|
61
|
+
return { success: true, message: 'already_owner' };
|
|
62
|
+
}
|
|
63
|
+
return { success: false, message: 'already_claimed' };
|
|
64
|
+
}
|
|
65
|
+
authState.telegramOwner = userId;
|
|
66
|
+
saveAuth();
|
|
67
|
+
return { success: true, message: 'claimed' };
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (authState.feishuOwner) {
|
|
71
|
+
if (authState.feishuOwner === userId) {
|
|
72
|
+
return { success: true, message: 'already_owner' };
|
|
73
|
+
}
|
|
74
|
+
return { success: false, message: 'already_claimed' };
|
|
75
|
+
}
|
|
76
|
+
authState.feishuOwner = userId;
|
|
77
|
+
saveAuth();
|
|
78
|
+
return { success: true, message: 'claimed' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function getOwner(platform) {
|
|
82
|
+
if (platform === 'telegram') {
|
|
83
|
+
return authState.telegramOwner;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return authState.feishuOwner;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// For debugging/display
|
|
90
|
+
export function getAuthStatus() {
|
|
91
|
+
return {
|
|
92
|
+
telegram: authState.telegramOwner !== null,
|
|
93
|
+
feishu: authState.feishuOwner !== null,
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -18,9 +18,6 @@ export function loadConfig() {
|
|
|
18
18
|
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || undefined,
|
|
19
19
|
feishuAppId: process.env.FEISHU_APP_ID || undefined,
|
|
20
20
|
feishuAppSecret: process.env.FEISHU_APP_SECRET || undefined,
|
|
21
|
-
feishuEncryptKey: process.env.FEISHU_ENCRYPT_KEY,
|
|
22
|
-
feishuVerificationToken: process.env.FEISHU_VERIFICATION_TOKEN,
|
|
23
|
-
feishuWebhookPort: parseInt(process.env.FEISHU_WEBHOOK_PORT || '3001', 10),
|
|
24
21
|
opencodeServerUrl: process.env.OPENCODE_SERVER_URL || 'http://localhost:3000',
|
|
25
22
|
tunnelUrl: process.env.TUNNEL_URL || '',
|
|
26
23
|
sessionIdleTimeoutMs: parseInt(process.env.SESSION_IDLE_TIMEOUT_MS || '1800000', 10),
|
package/dist/feishu/bot.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Feishu bot implementation for OpenCode Remote Control
|
|
2
|
-
|
|
2
|
+
// Uses WebSocket long connection mode - no tunnel/ngrok required!
|
|
3
3
|
import * as lark from '@larksuiteoapi/node-sdk';
|
|
4
4
|
import { initSessionManager, getOrCreateSession } from '../core/session.js';
|
|
5
5
|
import { splitMessage } from '../core/notifications.js';
|
|
6
6
|
import { EMOJI } from '../core/types.js';
|
|
7
7
|
import { initOpenCode, createSession, sendMessage, checkConnection } from '../opencode/client.js';
|
|
8
|
+
import { isAuthorized, hasOwner, claimOwnership, getAuthStatus } from '../core/auth.js';
|
|
8
9
|
let feishuClient = null;
|
|
10
|
+
let wsClient = null;
|
|
9
11
|
let config = null;
|
|
10
12
|
let openCodeSessions = null;
|
|
11
13
|
// Map Feishu event to shared MessageContext
|
|
@@ -41,8 +43,24 @@ function createFeishuAdapter(client) {
|
|
|
41
43
|
}
|
|
42
44
|
},
|
|
43
45
|
async sendTypingIndicator(threadId) {
|
|
44
|
-
// Feishu doesn't have typing indicator
|
|
45
|
-
//
|
|
46
|
+
// Feishu doesn't have native typing indicator API
|
|
47
|
+
// We send a "thinking" message that will be deleted later
|
|
48
|
+
const chatId = threadId.replace('feishu:', '');
|
|
49
|
+
try {
|
|
50
|
+
const result = await client.im.message.create({
|
|
51
|
+
params: { receive_id_type: 'chat_id' },
|
|
52
|
+
data: {
|
|
53
|
+
receive_id: chatId,
|
|
54
|
+
msg_type: 'text',
|
|
55
|
+
content: JSON.stringify({ text: '⏳ 思考中...' }),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return result.data?.message_id || '';
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('Failed to send typing indicator:', error);
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
46
64
|
},
|
|
47
65
|
async deleteMessage(threadId, messageId) {
|
|
48
66
|
if (!messageId)
|
|
@@ -65,9 +83,33 @@ async function handleCommand(adapter, ctx, text) {
|
|
|
65
83
|
const parts = text.split(/\s+/);
|
|
66
84
|
const command = parts[0].toLowerCase();
|
|
67
85
|
switch (command) {
|
|
68
|
-
case '/start':
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
case '/start': {
|
|
87
|
+
const result = claimOwnership('feishu', ctx.userId);
|
|
88
|
+
if (result.success) {
|
|
89
|
+
if (result.message === 'claimed') {
|
|
90
|
+
await adapter.reply(ctx.threadId, `🔐 **Security Setup Complete!**
|
|
91
|
+
|
|
92
|
+
✅ You are now the authorized owner of this bot.
|
|
93
|
+
|
|
94
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
95
|
+
⚠️ **IMPORTANT SECURITY NOTICE**
|
|
96
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
|
+
|
|
98
|
+
Only YOU can control OpenCode through this bot.
|
|
99
|
+
Other users will be blocked automatically.
|
|
100
|
+
|
|
101
|
+
Your Feishu ID: \`${ctx.userId}\`
|
|
102
|
+
|
|
103
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
104
|
+
|
|
105
|
+
🚀 **Ready to use!**
|
|
106
|
+
💬 Send me a prompt to start coding
|
|
107
|
+
/help — see all commands
|
|
108
|
+
/status — check OpenCode connection`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Already owner
|
|
112
|
+
await adapter.reply(ctx.threadId, `🚀 OpenCode Remote Control ready
|
|
71
113
|
|
|
72
114
|
💬 Send me a prompt to start coding
|
|
73
115
|
/help — see all commands
|
|
@@ -83,6 +125,31 @@ Commands:
|
|
|
83
125
|
/files — List changed files
|
|
84
126
|
/retry — Retry connection
|
|
85
127
|
|
|
128
|
+
💬 Anything else is treated as a prompt for OpenCode!`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Already claimed by someone else
|
|
133
|
+
await adapter.reply(ctx.threadId, `🚫 **Access Denied**
|
|
134
|
+
|
|
135
|
+
This bot is already secured by another user.
|
|
136
|
+
|
|
137
|
+
If you are the owner, check your configuration.`);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case '/help':
|
|
142
|
+
await adapter.reply(ctx.threadId, `📖 Commands
|
|
143
|
+
|
|
144
|
+
/start — Claim ownership & Start bot
|
|
145
|
+
/status — Check connection
|
|
146
|
+
/reset — Reset session
|
|
147
|
+
/approve — Approve pending changes
|
|
148
|
+
/reject — Reject pending changes
|
|
149
|
+
/diff — See full diff
|
|
150
|
+
/files — List changed files
|
|
151
|
+
/retry — Retry connection
|
|
152
|
+
|
|
86
153
|
💬 Anything else is treated as a prompt for OpenCode!`);
|
|
87
154
|
break;
|
|
88
155
|
case '/approve': {
|
|
@@ -163,9 +230,26 @@ async function handleMessage(adapter, ctx, text) {
|
|
|
163
230
|
const session = getOrCreateSession(ctx.threadId, 'feishu');
|
|
164
231
|
// Check if it's a command
|
|
165
232
|
if (text.startsWith('/')) {
|
|
233
|
+
// Commands are handled by handleCommand which has its own auth logic for /start
|
|
166
234
|
await handleCommand(adapter, ctx, text);
|
|
167
235
|
return;
|
|
168
236
|
}
|
|
237
|
+
// Authorization check for non-command messages
|
|
238
|
+
if (!isAuthorized('feishu', ctx.userId)) {
|
|
239
|
+
if (!hasOwner('feishu')) {
|
|
240
|
+
await adapter.reply(ctx.threadId, `🔐 **Authorization Required**
|
|
241
|
+
|
|
242
|
+
This bot is not yet secured.
|
|
243
|
+
|
|
244
|
+
Please send /start to claim ownership first.`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
await adapter.reply(ctx.threadId, `🚫 **Access Denied**
|
|
248
|
+
|
|
249
|
+
You are not authorized to use this bot.`);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
169
253
|
// Check OpenCode connection
|
|
170
254
|
const connected = await checkConnection();
|
|
171
255
|
if (!connected) {
|
|
@@ -176,13 +260,18 @@ Cannot connect to OpenCode server.
|
|
|
176
260
|
🔄 /retry — check again`);
|
|
177
261
|
return;
|
|
178
262
|
}
|
|
179
|
-
// Send typing indicator
|
|
180
|
-
|
|
263
|
+
// Send typing indicator
|
|
264
|
+
console.log('⏳ Sending typing indicator...');
|
|
265
|
+
const typingMsgId = await adapter.sendTypingIndicator(ctx.threadId);
|
|
181
266
|
// Get or create OpenCode session
|
|
182
267
|
let openCodeSession = openCodeSessions?.get(ctx.threadId);
|
|
183
268
|
if (!openCodeSession) {
|
|
184
269
|
const newSession = await createSession(ctx.threadId, `Feishu chat ${ctx.threadId}`);
|
|
185
270
|
if (!newSession) {
|
|
271
|
+
// Delete typing indicator before error message
|
|
272
|
+
if (typingMsgId && adapter.deleteMessage) {
|
|
273
|
+
await adapter.deleteMessage(ctx.threadId, typingMsgId);
|
|
274
|
+
}
|
|
186
275
|
await adapter.reply(ctx.threadId, '❌ Failed to create OpenCode session');
|
|
187
276
|
return;
|
|
188
277
|
}
|
|
@@ -195,10 +284,12 @@ Cannot connect to OpenCode server.
|
|
|
195
284
|
}
|
|
196
285
|
}
|
|
197
286
|
try {
|
|
287
|
+
console.log('🤖 Sending to OpenCode...');
|
|
198
288
|
const response = await sendMessage(openCodeSession, text);
|
|
289
|
+
console.log('✅ Got response from OpenCode');
|
|
199
290
|
// Delete typing indicator
|
|
200
|
-
if (adapter.deleteMessage
|
|
201
|
-
await adapter.deleteMessage(ctx.threadId,
|
|
291
|
+
if (typingMsgId && adapter.deleteMessage) {
|
|
292
|
+
await adapter.deleteMessage(ctx.threadId, typingMsgId);
|
|
202
293
|
}
|
|
203
294
|
// Split long messages
|
|
204
295
|
const messages = splitMessage(response);
|
|
@@ -208,6 +299,10 @@ Cannot connect to OpenCode server.
|
|
|
208
299
|
}
|
|
209
300
|
catch (error) {
|
|
210
301
|
console.error('Error sending message:', error);
|
|
302
|
+
// Delete typing indicator on error too
|
|
303
|
+
if (typingMsgId && adapter.deleteMessage) {
|
|
304
|
+
await adapter.deleteMessage(ctx.threadId, typingMsgId);
|
|
305
|
+
}
|
|
211
306
|
await adapter.reply(ctx.threadId, `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
212
307
|
}
|
|
213
308
|
}
|
|
@@ -228,13 +323,14 @@ function checkRateLimit(chatId) {
|
|
|
228
323
|
entry.count++;
|
|
229
324
|
return true;
|
|
230
325
|
}
|
|
231
|
-
// Start Feishu bot
|
|
326
|
+
// Start Feishu bot using WebSocket long connection mode
|
|
327
|
+
// No tunnel/ngrok required - just needs internet access!
|
|
232
328
|
export async function startFeishuBot(botConfig) {
|
|
233
329
|
config = botConfig;
|
|
234
330
|
if (!config.feishuAppId || !config.feishuAppSecret) {
|
|
235
|
-
throw new Error('Feishu credentials not configured');
|
|
331
|
+
throw new Error('Feishu credentials not configured. Run: opencode-remote config');
|
|
236
332
|
}
|
|
237
|
-
// Initialize Feishu client
|
|
333
|
+
// Initialize Feishu client for sending messages
|
|
238
334
|
feishuClient = new lark.Client({
|
|
239
335
|
appId: config.feishuAppId,
|
|
240
336
|
appSecret: config.feishuAppSecret,
|
|
@@ -255,103 +351,135 @@ export async function startFeishuBot(botConfig) {
|
|
|
255
351
|
console.error('❌ Failed to initialize OpenCode:', error);
|
|
256
352
|
console.log('Make sure OpenCode is running');
|
|
257
353
|
}
|
|
258
|
-
// Create adapter
|
|
354
|
+
// Create adapter for sending messages
|
|
259
355
|
const adapter = createFeishuAdapter(feishuClient);
|
|
260
|
-
// Create
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// Handle
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
// Validate event structure
|
|
274
|
-
if (!event.header?.event_type) {
|
|
275
|
-
res.status(400).json({ code: -1, msg: 'Invalid event structure' });
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
// Handle message events
|
|
279
|
-
if (event.header.event_type === 'im.message.receive_v1') {
|
|
356
|
+
// Create WebSocket client for long connection
|
|
357
|
+
wsClient = new lark.WSClient({
|
|
358
|
+
appId: config.feishuAppId,
|
|
359
|
+
appSecret: config.feishuAppSecret,
|
|
360
|
+
// For international Lark, add: domain: lark.Domain.Lark
|
|
361
|
+
});
|
|
362
|
+
// Create event dispatcher for handling incoming messages
|
|
363
|
+
const eventDispatcher = new lark.EventDispatcher({}).register({
|
|
364
|
+
// Handle incoming messages
|
|
365
|
+
'im.message.receive_v1': async (data) => {
|
|
366
|
+
console.log('📩 Received message event:', JSON.stringify(data, null, 2));
|
|
280
367
|
try {
|
|
281
368
|
// Validate event data
|
|
282
|
-
if (!
|
|
283
|
-
|
|
284
|
-
return;
|
|
369
|
+
if (!data?.message) {
|
|
370
|
+
console.warn('Received message event without message data');
|
|
371
|
+
return { code: 0 };
|
|
285
372
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
373
|
+
const chatId = data.message.chat_id;
|
|
374
|
+
console.log(`💬 Message from chat: ${chatId}`);
|
|
288
375
|
// Rate limiting
|
|
289
376
|
if (!checkRateLimit(chatId)) {
|
|
290
377
|
console.warn(`Rate limit exceeded for chat: ${chatId}`);
|
|
291
|
-
|
|
292
|
-
return;
|
|
378
|
+
return { code: 0 };
|
|
293
379
|
}
|
|
294
380
|
// Parse message content
|
|
295
381
|
let text = '';
|
|
296
382
|
try {
|
|
297
|
-
const content = JSON.parse(
|
|
383
|
+
const content = JSON.parse(data.message.content);
|
|
298
384
|
text = content.text || '';
|
|
385
|
+
console.log(`📝 Message text: ${text}`);
|
|
299
386
|
}
|
|
300
387
|
catch {
|
|
301
388
|
// If not JSON, try to use raw content
|
|
302
|
-
text =
|
|
389
|
+
text = data.message.content || '';
|
|
390
|
+
console.log(`📝 Raw message content: ${text}`);
|
|
303
391
|
}
|
|
304
392
|
// Skip empty messages
|
|
305
393
|
if (!text.trim()) {
|
|
306
|
-
|
|
307
|
-
return;
|
|
394
|
+
console.log('⏭️ Skipping empty message');
|
|
395
|
+
return { code: 0 };
|
|
308
396
|
}
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
397
|
+
// Create message context
|
|
398
|
+
const ctx = feishuEventToContext(data);
|
|
399
|
+
// Handle the message (async, don't wait)
|
|
400
|
+
handleMessage(adapter, ctx, text).catch(error => {
|
|
401
|
+
console.error('Error handling Feishu message:', error);
|
|
402
|
+
});
|
|
403
|
+
return { code: 0 };
|
|
312
404
|
}
|
|
313
405
|
catch (error) {
|
|
314
|
-
console.error('Feishu
|
|
315
|
-
|
|
406
|
+
console.error('Feishu event handler error:', error);
|
|
407
|
+
return { code: 0 };
|
|
316
408
|
}
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
// Unknown event type - acknowledge but ignore
|
|
320
|
-
console.log(`Received Feishu event: ${event.header.event_type}`);
|
|
321
|
-
res.json({ code: 0 });
|
|
322
|
-
});
|
|
323
|
-
// Health check endpoint
|
|
324
|
-
app.get('/health', (_req, res) => {
|
|
325
|
-
res.json({ status: 'ok', platform: 'feishu' });
|
|
326
|
-
});
|
|
327
|
-
// Start server
|
|
328
|
-
return new Promise((resolve, reject) => {
|
|
329
|
-
const server = app.listen(port, () => {
|
|
330
|
-
console.log(`🚀 Feishu webhook listening on port ${port}`);
|
|
331
|
-
console.log(`📡 Webhook URL: http://localhost:${port}${webhookPath}`);
|
|
332
|
-
console.log('\n📝 Setup instructions:');
|
|
333
|
-
console.log(' 1. Use ngrok or cloudflared to expose this endpoint:');
|
|
334
|
-
console.log(' ngrok http ' + port);
|
|
335
|
-
console.log(' 2. Configure the webhook URL in Feishu admin console');
|
|
336
|
-
console.log(' 3. Subscribe to "im.message.receive_v1" event');
|
|
337
|
-
});
|
|
338
|
-
server.on('error', (err) => {
|
|
339
|
-
reject(err);
|
|
340
|
-
});
|
|
341
|
-
// Handle graceful shutdown
|
|
342
|
-
process.once('SIGINT', () => {
|
|
343
|
-
console.log('\n🛑 Shutting down Feishu bot...');
|
|
344
|
-
server.close(() => {
|
|
345
|
-
console.log('Feishu bot stopped');
|
|
346
|
-
resolve();
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
process.once('SIGTERM', () => {
|
|
350
|
-
console.log('\n🛑 Shutting down Feishu bot...');
|
|
351
|
-
server.close(() => {
|
|
352
|
-
console.log('Feishu bot stopped');
|
|
353
|
-
resolve();
|
|
354
|
-
});
|
|
355
|
-
});
|
|
409
|
+
},
|
|
356
410
|
});
|
|
411
|
+
// Start WebSocket long connection
|
|
412
|
+
console.log('🔗 Starting Feishu WebSocket long connection...');
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log('✨ Long connection mode - NO tunnel/ngrok required!');
|
|
415
|
+
console.log(' Just make sure your computer can access the internet.');
|
|
416
|
+
console.log('');
|
|
417
|
+
// Show security status
|
|
418
|
+
const authStatus = getAuthStatus();
|
|
419
|
+
if (!authStatus.feishu) {
|
|
420
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
421
|
+
console.log(' 🔐 SECURITY NOTICE');
|
|
422
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
423
|
+
console.log('');
|
|
424
|
+
console.log(' Bot is NOT yet secured!');
|
|
425
|
+
console.log(' The FIRST user to send /start will become the owner.');
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log(' 👉 Open Feishu and send /start to YOUR bot NOW!');
|
|
428
|
+
console.log('');
|
|
429
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
430
|
+
console.log('');
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
console.log('🔒 Bot is secured (owner authorized)');
|
|
434
|
+
console.log('');
|
|
435
|
+
}
|
|
436
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
437
|
+
console.log(' 📋 Configuration Checklist');
|
|
438
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log(' Step 1: Add Permissions (权限管理 → API权限)');
|
|
441
|
+
console.log(' ────────────────────────────────────────');
|
|
442
|
+
console.log(' Click "批量添加" (Batch Add) and paste this JSON:');
|
|
443
|
+
console.log('');
|
|
444
|
+
console.log(' ┌────────────────────────────────────────────────────┐');
|
|
445
|
+
console.log(' │ { │');
|
|
446
|
+
console.log(' │ "im:message", │');
|
|
447
|
+
console.log(' │ "im:message:send_as_bot", │');
|
|
448
|
+
console.log(' │ "im:message:receive_as_bot" │');
|
|
449
|
+
console.log(' │ } │');
|
|
450
|
+
console.log(' └────────────────────────────────────────────────────┘');
|
|
451
|
+
console.log('');
|
|
452
|
+
console.log(' Step 2: Enable Robot (应用能力 → 机器人)');
|
|
453
|
+
console.log(' ────────────────────────────────────────');
|
|
454
|
+
console.log(' - Enable "启用机器人"');
|
|
455
|
+
console.log(' - Enable "机器人可主动发送消息给用户"');
|
|
456
|
+
console.log(' - Enable "用户可与机器人进行单聊"');
|
|
457
|
+
console.log('');
|
|
458
|
+
console.log(' Step 3: Event Subscription (事件订阅)');
|
|
459
|
+
console.log(' ────────────────────────────────────────');
|
|
460
|
+
console.log(' ⚠️ MUST start this bot BEFORE saving event config!');
|
|
461
|
+
console.log(' - Select "使用长连接接收事件"');
|
|
462
|
+
console.log(' - Add event: im.message.receive_v1');
|
|
463
|
+
console.log(' - Then click Save');
|
|
464
|
+
console.log('');
|
|
465
|
+
console.log(' Step 4: Publish App (版本管理与发布)');
|
|
466
|
+
console.log(' ────────────────────────────────────────');
|
|
467
|
+
console.log(' - Create version → Request publishing → Publish');
|
|
468
|
+
console.log('');
|
|
469
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
470
|
+
console.log(' 🔍 Debug: Send a message to your bot in Feishu!');
|
|
471
|
+
console.log(' You should see: 📩 Received message event');
|
|
472
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
473
|
+
console.log('');
|
|
474
|
+
// The start() method will block the main thread
|
|
475
|
+
// Handle graceful shutdown
|
|
476
|
+
const shutdown = () => {
|
|
477
|
+
console.log('\n🛑 Shutting down Feishu bot...');
|
|
478
|
+
// WSClient doesn't have a stop method, just let the process exit
|
|
479
|
+
process.exit(0);
|
|
480
|
+
};
|
|
481
|
+
process.once('SIGINT', shutdown);
|
|
482
|
+
process.once('SIGTERM', shutdown);
|
|
483
|
+
// Start the WebSocket client - this will block until process is killed
|
|
484
|
+
await wsClient.start({ eventDispatcher });
|
|
357
485
|
}
|
package/dist/opencode/client.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
// OpenCode SDK client for remote control
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
import { createOpencode } from '@opencode-ai/sdk';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const childProcess = require('node:child_process');
|
|
7
|
+
// Patch undici's default HeadersTimeout (30s) to 30 minutes for long AI responses.
|
|
8
|
+
// undici is Node.js's default fetch implementation and enforces headersTimeout on
|
|
9
|
+
// every request. Without this, streaming/processing large AI responses timeout.
|
|
10
|
+
let fetchPatched = false;
|
|
11
|
+
function patchFetchForUnlimitedTimeout() {
|
|
12
|
+
if (fetchPatched || typeof globalThis.fetch !== 'function')
|
|
13
|
+
return;
|
|
14
|
+
fetchPatched = true;
|
|
15
|
+
const originalFetch = globalThis.fetch;
|
|
16
|
+
// @ts-ignore - internal API
|
|
17
|
+
const originalDispatcher = originalFetch[Symbol.for('undici.globalDispatcher.1')];
|
|
18
|
+
if (originalDispatcher) {
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
originalDispatcher.headersTimeout = 30 * 60 * 1000;
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
originalDispatcher.bodyTimeout = 30 * 60 * 1000;
|
|
23
|
+
}
|
|
24
|
+
globalThis.fetch = function (input, init) {
|
|
25
|
+
if (init?.signal) {
|
|
26
|
+
const { signal: _signal, ...rest } = init;
|
|
27
|
+
return originalFetch(input, rest);
|
|
28
|
+
}
|
|
29
|
+
return originalFetch(input, init);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
5
32
|
// Windows compatibility: patch child_process.spawn to use shell for 'opencode' command
|
|
6
33
|
// This is needed because Windows requires shell: true to execute .cmd files
|
|
7
34
|
if (platform() === 'win32') {
|
|
8
|
-
const originalSpawn = spawn;
|
|
35
|
+
const originalSpawn = childProcess.spawn;
|
|
9
36
|
// @ts-ignore - monkey patching for Windows compatibility
|
|
10
|
-
|
|
37
|
+
childProcess.spawn = function (command, args, options = {}) {
|
|
11
38
|
if (command === 'opencode' && !options.shell) {
|
|
12
39
|
options.shell = true;
|
|
13
40
|
}
|
|
@@ -22,7 +49,7 @@ export async function verifyOpenCodeInstalled() {
|
|
|
22
49
|
return new Promise((resolve) => {
|
|
23
50
|
const isWindows = platform() === 'win32';
|
|
24
51
|
const command = isWindows ? 'where' : 'which';
|
|
25
|
-
const proc = spawn(command, ['opencode'], { shell: isWindows });
|
|
52
|
+
const proc = childProcess.spawn(command, ['opencode'], { shell: isWindows });
|
|
26
53
|
let output = '';
|
|
27
54
|
let errorOutput = '';
|
|
28
55
|
proc.stdout?.on('data', (chunk) => {
|
|
@@ -53,6 +80,7 @@ export async function verifyOpenCodeInstalled() {
|
|
|
53
80
|
let opencodeInstance = null;
|
|
54
81
|
let verificationDone = false;
|
|
55
82
|
export async function initOpenCode() {
|
|
83
|
+
patchFetchForUnlimitedTimeout();
|
|
56
84
|
if (opencodeInstance) {
|
|
57
85
|
return opencodeInstance;
|
|
58
86
|
}
|
package/dist/telegram/bot.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from '../core/types.js';
|
|
|
4
4
|
import { initSessionManager, getOrCreateSession } from '../core/session.js';
|
|
5
5
|
import { splitMessage } from '../core/notifications.js';
|
|
6
6
|
import { initOpenCode, createSession, sendMessage, checkConnection } from '../opencode/client.js';
|
|
7
|
+
import { isAuthorized, hasOwner, claimOwnership, getAuthStatus } from '../core/auth.js';
|
|
7
8
|
// Lazy initialization - bot is only created when startBot() is called
|
|
8
9
|
let config = null;
|
|
9
10
|
let bot = null;
|
|
@@ -18,11 +19,47 @@ function getThreadId(ctx) {
|
|
|
18
19
|
function setupBotCommands(bot, openCodeSessions) {
|
|
19
20
|
// Start command
|
|
20
21
|
bot.command('start', async (ctx) => {
|
|
21
|
-
|
|
22
|
+
const userId = String(ctx.from?.id);
|
|
23
|
+
const result = claimOwnership('telegram', userId);
|
|
24
|
+
if (result.success) {
|
|
25
|
+
if (result.message === 'claimed') {
|
|
26
|
+
await ctx.reply(`🔐 **Security Setup Complete!**
|
|
27
|
+
|
|
28
|
+
✅ You are now the authorized owner of this bot.
|
|
29
|
+
|
|
30
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
31
|
+
⚠️ **IMPORTANT SECURITY NOTICE**
|
|
32
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
33
|
+
|
|
34
|
+
Only YOU can control OpenCode through this bot.
|
|
35
|
+
Other users will be blocked automatically.
|
|
36
|
+
|
|
37
|
+
Your Telegram ID: \`${userId}\`
|
|
38
|
+
|
|
39
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
40
|
+
|
|
41
|
+
🚀 **Ready to use!**
|
|
42
|
+
💬 Send me a prompt to start coding
|
|
43
|
+
/help — see all commands
|
|
44
|
+
/status — check OpenCode connection`, { parse_mode: 'Markdown' });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Already owner
|
|
48
|
+
await ctx.reply(`🚀 OpenCode Remote Control ready
|
|
22
49
|
|
|
23
50
|
💬 Send me a prompt to start coding
|
|
24
51
|
/help — see all commands
|
|
25
52
|
/status — check OpenCode connection`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Already claimed by someone else
|
|
57
|
+
await ctx.reply(`🚫 **Access Denied**
|
|
58
|
+
|
|
59
|
+
This bot is already secured by another user.
|
|
60
|
+
|
|
61
|
+
If you are the owner, check your configuration.`, { parse_mode: 'Markdown' });
|
|
62
|
+
}
|
|
26
63
|
});
|
|
27
64
|
// Help command
|
|
28
65
|
bot.command('help', async (ctx) => {
|
|
@@ -132,9 +169,26 @@ Cannot connect to OpenCode server.
|
|
|
132
169
|
// Handle all other messages as prompts
|
|
133
170
|
bot.on('message:text', async (ctx) => {
|
|
134
171
|
const text = ctx.message.text;
|
|
172
|
+
const userId = String(ctx.from?.id);
|
|
135
173
|
// Skip if it's a command (already handled)
|
|
136
174
|
if (text.startsWith('/'))
|
|
137
175
|
return;
|
|
176
|
+
// Authorization check
|
|
177
|
+
if (!isAuthorized('telegram', userId)) {
|
|
178
|
+
if (!hasOwner('telegram')) {
|
|
179
|
+
await ctx.reply(`🔐 **Authorization Required**
|
|
180
|
+
|
|
181
|
+
This bot is not yet secured.
|
|
182
|
+
|
|
183
|
+
Please send /start to claim ownership first.`, { parse_mode: 'Markdown' });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
await ctx.reply(`🚫 **Access Denied**
|
|
187
|
+
|
|
188
|
+
You are not authorized to use this bot.`, { parse_mode: 'Markdown' });
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
138
192
|
const threadId = getThreadId(ctx);
|
|
139
193
|
// Send typing indicator
|
|
140
194
|
await ctx.api.sendChatAction(ctx.chat.id, 'typing');
|
|
@@ -236,6 +290,24 @@ export async function startBot() {
|
|
|
236
290
|
console.log('Make sure OpenCode is running');
|
|
237
291
|
}
|
|
238
292
|
console.log('🚀 Starting Telegram bot...');
|
|
293
|
+
// Show security status
|
|
294
|
+
const authStatus = getAuthStatus();
|
|
295
|
+
if (!authStatus.telegram) {
|
|
296
|
+
console.log('');
|
|
297
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
298
|
+
console.log(' 🔐 SECURITY NOTICE');
|
|
299
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log(' Bot is NOT yet secured!');
|
|
302
|
+
console.log(' The FIRST user to send /start will become the owner.');
|
|
303
|
+
console.log('');
|
|
304
|
+
console.log(' 👉 Open Telegram and send /start to YOUR bot NOW!');
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log('🔒 Bot is secured (owner authorized)');
|
|
310
|
+
}
|
|
239
311
|
// Handle graceful shutdown
|
|
240
312
|
const shutdown = async () => {
|
|
241
313
|
console.log('\n🛑 Shutting down Telegram bot...');
|