@tina_summer/codepocket 2.0.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/bin/codepocket.js +453 -0
- package/bin/run.js +289 -0
- package/package.json +31 -0
- package/src/index.ts +304 -0
- package/src/mcp-client.js +348 -0
- package/src/mcp-server.js +331 -0
- package/vendor/cpkt-hub +0 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodePocket — 你的全能 AI 随身控制台
|
|
5
|
+
*
|
|
6
|
+
* npx codepocket
|
|
7
|
+
*
|
|
8
|
+
* Powered by Local Claude Code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { WebSocket } = require('ws')
|
|
12
|
+
const { spawn, execSync } = require('child_process')
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
const os = require('os')
|
|
16
|
+
const readline = require('readline')
|
|
17
|
+
const { startMcpServer, handlePhoneActionResponse: onPhoneActionResponse } = require('../src/mcp-server')
|
|
18
|
+
const { runTestCli } = require('../src/mcp-client')
|
|
19
|
+
|
|
20
|
+
// ── Config ──
|
|
21
|
+
|
|
22
|
+
const CONFIG_DIR = path.join(os.homedir(), '.codepocket')
|
|
23
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
24
|
+
const SESSION_FILE = path.join(CONFIG_DIR, 'session.json')
|
|
25
|
+
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) } catch { return {} }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveConfig(cfg) {
|
|
31
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
32
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function injectMcpConfig(port) {
|
|
36
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json')
|
|
37
|
+
let settings = {}
|
|
38
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) } catch {}
|
|
39
|
+
if (!settings.mcpServers) settings.mcpServers = {}
|
|
40
|
+
if (settings.mcpServers['codepocket-phone']) return // already configured
|
|
41
|
+
settings.mcpServers['codepocket-phone'] = {
|
|
42
|
+
type: 'sse',
|
|
43
|
+
url: `http://127.0.0.1:${port}/sse`,
|
|
44
|
+
}
|
|
45
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
46
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
47
|
+
console.log(` [✓] MCP 配置已写入 ${settingsPath}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Parse args ──
|
|
51
|
+
|
|
52
|
+
const args = process.argv.slice(2)
|
|
53
|
+
|
|
54
|
+
let hubUrl = process.env.CPKT_HUB_URL || process.env.HAPI_HUB_URL || 'http://127.0.0.1:3006'
|
|
55
|
+
let relayUrl = process.env.RELAY_WS_URL || ''
|
|
56
|
+
let relayKey = process.env.RELAY_KEY || ''
|
|
57
|
+
let noHub = false
|
|
58
|
+
|
|
59
|
+
// Handle 'connect' subcommand (connector mode: hub + relay + MCP)
|
|
60
|
+
const CONNECT_ARGS = ['connect', 'c']
|
|
61
|
+
|
|
62
|
+
// Handle 'test' subcommand (doesn't need hub/relay)
|
|
63
|
+
if (args[0] === 'test') {
|
|
64
|
+
runTestCli(args.slice(1))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!CONNECT_ARGS.includes(args[0])) {
|
|
69
|
+
// Default or any other subcommand → pass through to hub binary (runs Claude)
|
|
70
|
+
const hubBin = findHubBin()
|
|
71
|
+
if (hubBin) {
|
|
72
|
+
const child = spawn(hubBin, args, {
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
HAPI_HOME: path.join(os.homedir(), '.codepocket', 'hub'),
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
child.on('exit', (code) => process.exit(code || 0))
|
|
80
|
+
} else {
|
|
81
|
+
console.error('[!] 找不到 Hub 二进制')
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const connectArgs = args[0] === 'connect' || args[0] === 'c' ? args.slice(1) : args
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < connectArgs.length; i++) {
|
|
90
|
+
const arg = connectArgs[i]
|
|
91
|
+
if (arg.startsWith('--hub=')) hubUrl = arg.slice(6).replace(/\/+$/, '')
|
|
92
|
+
if (arg === '--hub' && connectArgs[i + 1]) { hubUrl = connectArgs[++i].replace(/\/+$/, '') }
|
|
93
|
+
if (arg.startsWith('--relay=')) relayUrl = arg.slice(8)
|
|
94
|
+
if (arg === '--relay' && connectArgs[i + 1]) relayUrl = connectArgs[++i]
|
|
95
|
+
if (arg.startsWith('--key=')) relayKey = arg.slice(6)
|
|
96
|
+
if (arg === '--key' && connectArgs[i + 1]) relayKey = connectArgs[++i]
|
|
97
|
+
if (arg === '--no-hub') noHub = true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const saved = loadConfig()
|
|
101
|
+
if (relayKey) {
|
|
102
|
+
saveConfig({ ...saved, relayKey, relayUrl: relayUrl || saved.relayUrl || 'wss://bkstory.cn/ws' })
|
|
103
|
+
} else {
|
|
104
|
+
relayKey = saved.relayKey || ''
|
|
105
|
+
}
|
|
106
|
+
if (!relayUrl) relayUrl = saved.relayUrl || 'wss://bkstory.cn/ws'
|
|
107
|
+
|
|
108
|
+
let jwt = ''
|
|
109
|
+
let relayWs = null
|
|
110
|
+
|
|
111
|
+
// ── Step 1: Start hub if needed ──
|
|
112
|
+
|
|
113
|
+
function startHub() {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
if (noHub) { resolve(); return }
|
|
116
|
+
|
|
117
|
+
// Check if hub already running
|
|
118
|
+
try {
|
|
119
|
+
const result = execSync(`curl -s -o /dev/null -w "%{http_code}" ${hubUrl}/ 2>/dev/null`).toString().trim()
|
|
120
|
+
if (result === '200') {
|
|
121
|
+
console.log(' [✓] Hub 已在运行')
|
|
122
|
+
resolve()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
|
|
127
|
+
console.log(' 启动 Hub...')
|
|
128
|
+
const hubBin = findHubBin()
|
|
129
|
+
if (!hubBin) {
|
|
130
|
+
console.error(' [!] 找不到 Hub 二进制')
|
|
131
|
+
reject(new Error('Hub binary not found'))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hub = spawn(hubBin, ['hub', 'start'], {
|
|
136
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
137
|
+
detached: false,
|
|
138
|
+
env: {
|
|
139
|
+
...process.env,
|
|
140
|
+
HAPI_HOME: path.join(os.homedir(), '.codepocket', 'hub'),
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
let started = false
|
|
145
|
+
hub.stdout.on('data', (data) => {
|
|
146
|
+
const text = data.toString()
|
|
147
|
+
if (!started) process.stdout.write(' ')
|
|
148
|
+
process.stdout.write(text)
|
|
149
|
+
})
|
|
150
|
+
hub.stderr.on('data', (data) => {
|
|
151
|
+
const text = data.toString()
|
|
152
|
+
if (text.includes('listening') || text.includes('ready') || text.includes('started')) {
|
|
153
|
+
if (!started) { started = true; resolve() }
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Wait up to 10s for hub
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
if (!started) {
|
|
160
|
+
// Try connecting
|
|
161
|
+
try {
|
|
162
|
+
execSync(`curl -s -o /dev/null -w "%{http_code}" ${hubUrl}/ 2>/dev/null`)
|
|
163
|
+
started = true
|
|
164
|
+
resolve()
|
|
165
|
+
} catch {
|
|
166
|
+
console.log(' [i] Hub 启动中,继续连接...')
|
|
167
|
+
resolve()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, 8000)
|
|
171
|
+
|
|
172
|
+
hub.on('exit', (code) => {
|
|
173
|
+
if (!started && code !== 0) {
|
|
174
|
+
reject(new Error(`Hub exited with code ${code}`))
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function findHubBin() {
|
|
181
|
+
// 1. Bundled binary in vendor/
|
|
182
|
+
const bundledBin = path.join(__dirname, '..', 'vendor', process.platform === 'win32' ? 'cpkt-hub.exe' : 'cpkt-hub')
|
|
183
|
+
if (fs.existsSync(bundledBin)) return bundledBin
|
|
184
|
+
|
|
185
|
+
// 2. Global install
|
|
186
|
+
try { const r = execSync('which cpkt-hub 2>/dev/null').toString().trim(); if (r) return r } catch {}
|
|
187
|
+
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Step 2: Authenticate ──
|
|
192
|
+
|
|
193
|
+
async function findHubToken() {
|
|
194
|
+
const hubHome = process.env.HAPI_HOME || path.join(os.homedir(), '.codepocket', 'hub')
|
|
195
|
+
const settingsPath = path.join(hubHome, 'settings.json')
|
|
196
|
+
try {
|
|
197
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
|
|
198
|
+
if (settings.cliApiToken) return settings.cliApiToken
|
|
199
|
+
} catch {}
|
|
200
|
+
if (process.env.CLI_API_TOKEN) return process.env.CLI_API_TOKEN
|
|
201
|
+
throw new Error('找不到 Hub token')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function authenticateHub(token) {
|
|
205
|
+
const res = await fetch(`${hubUrl}/api/auth`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({ accessToken: token }),
|
|
209
|
+
})
|
|
210
|
+
if (!res.ok) throw new Error(`Hub auth failed: ${res.status}`)
|
|
211
|
+
return (await res.json()).token
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Step 3: SSE stream ──
|
|
215
|
+
|
|
216
|
+
async function startSSE() {
|
|
217
|
+
const url = `${hubUrl}/api/events?token=${encodeURIComponent(jwt)}&all=true`
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetch(url, { headers: { Accept: 'text/event-stream' } })
|
|
220
|
+
if (!res.ok || !res.body) { setTimeout(startSSE, 5000); return }
|
|
221
|
+
console.log(' [✓] SSE 已连接,监听事件...')
|
|
222
|
+
const reader = res.body.getReader()
|
|
223
|
+
const decoder = new TextDecoder()
|
|
224
|
+
let buffer = ''
|
|
225
|
+
async function pump() {
|
|
226
|
+
while (true) {
|
|
227
|
+
const { done, value } = await reader.read()
|
|
228
|
+
if (done) break
|
|
229
|
+
buffer += decoder.decode(value, { stream: true })
|
|
230
|
+
const lines = buffer.split('\n')
|
|
231
|
+
buffer = lines.pop() || ''
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
if (line.startsWith('data:')) {
|
|
234
|
+
try {
|
|
235
|
+
const event = JSON.parse(line.slice(5).trim())
|
|
236
|
+
console.log(`[SSE] ${event.type || 'unknown'}`)
|
|
237
|
+
sendToRelay({ type: 'sse-event', event })
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
setTimeout(startSSE, 5000)
|
|
243
|
+
}
|
|
244
|
+
pump().catch(() => { setTimeout(startSSE, 5000) })
|
|
245
|
+
} catch { setTimeout(startSSE, 5000) }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Step 4: Relay connection ──
|
|
249
|
+
|
|
250
|
+
let relayPingInterval = null
|
|
251
|
+
|
|
252
|
+
function connectRelay() {
|
|
253
|
+
let savedSession = null
|
|
254
|
+
try { savedSession = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8')) } catch {}
|
|
255
|
+
|
|
256
|
+
// Clean up old connection
|
|
257
|
+
if (relayPingInterval) { clearInterval(relayPingInterval); relayPingInterval = null }
|
|
258
|
+
if (relayWs) {
|
|
259
|
+
try { relayWs.removeAllListeners() } catch {}
|
|
260
|
+
try { relayWs.close() } catch {}
|
|
261
|
+
relayWs = null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
relayWs = new WebSocket(relayUrl)
|
|
265
|
+
|
|
266
|
+
relayWs.on('open', () => {
|
|
267
|
+
relayWs.send(JSON.stringify({
|
|
268
|
+
type: 'register',
|
|
269
|
+
key: relayKey,
|
|
270
|
+
sessionToken: savedSession?.sessionToken || undefined,
|
|
271
|
+
}))
|
|
272
|
+
// Heartbeat every 30s to detect stale connections
|
|
273
|
+
relayPingInterval = setInterval(() => {
|
|
274
|
+
if (relayWs?.readyState === WebSocket.OPEN) {
|
|
275
|
+
relayWs.ping()
|
|
276
|
+
}
|
|
277
|
+
}, 30000)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
relayWs.on('message', (raw) => {
|
|
281
|
+
let msg
|
|
282
|
+
try { msg = JSON.parse(raw.toString()) } catch { return }
|
|
283
|
+
switch (msg.type) {
|
|
284
|
+
case 'registered': {
|
|
285
|
+
try {
|
|
286
|
+
fs.mkdirSync(path.dirname(SESSION_FILE), { recursive: true })
|
|
287
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify({ sessionToken: msg.sessionToken, hubUrl, relayUrl }))
|
|
288
|
+
} catch {}
|
|
289
|
+
if (msg.reconnected) {
|
|
290
|
+
console.log('\n [✓] 已重新连接\n')
|
|
291
|
+
} else {
|
|
292
|
+
console.log(' [✓] 已连接中继,等待小程序...\n')
|
|
293
|
+
}
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
case 'client-connected': {
|
|
297
|
+
console.log(' [✓] 小程序已连接')
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
case 'rest-request': {
|
|
301
|
+
forwardToHub(msg.requestId, msg.method, msg.path, msg.body)
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
case 'auth-request': {
|
|
305
|
+
handleAuthRequest(msg.requestId, msg.accessToken)
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
case 'register-error': {
|
|
309
|
+
console.error(` [!] ${msg.error}`)
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
case 'phone-action-response': {
|
|
313
|
+
onPhoneActionResponse(msg)
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
relayWs.on('close', () => {
|
|
320
|
+
if (relayPingInterval) { clearInterval(relayPingInterval); relayPingInterval = null }
|
|
321
|
+
relayWs = null
|
|
322
|
+
console.log('[relay] 连接断开,3秒后重连...')
|
|
323
|
+
setTimeout(connectRelay, 3000)
|
|
324
|
+
})
|
|
325
|
+
relayWs.on('error', (err) => {
|
|
326
|
+
console.error('[relay] 连接错误:', err.message)
|
|
327
|
+
relayWs?.close()
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function sendToRelay(msg) {
|
|
332
|
+
if (relayWs?.readyState === WebSocket.OPEN) relayWs.send(JSON.stringify(msg))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function doForwardToHub(requestId, method, p, body) {
|
|
336
|
+
const init = {
|
|
337
|
+
method,
|
|
338
|
+
headers: { 'Content-Type': 'application/json', ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}) },
|
|
339
|
+
}
|
|
340
|
+
if (body && method !== 'GET') init.body = JSON.stringify(body)
|
|
341
|
+
return fetch(`${hubUrl}${p}`, init)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function forwardToHub(requestId, method, p, body) {
|
|
345
|
+
try {
|
|
346
|
+
let res = await doForwardToHub(requestId, method, p, body)
|
|
347
|
+
// JWT expired — re-authenticate and retry
|
|
348
|
+
if (res.status === 401) {
|
|
349
|
+
console.log('[REST] JWT expired, re-authenticating...')
|
|
350
|
+
try {
|
|
351
|
+
const token = await findHubToken()
|
|
352
|
+
jwt = await authenticateHub(token)
|
|
353
|
+
res = await doForwardToHub(requestId, method, p, body)
|
|
354
|
+
} catch (authErr) {
|
|
355
|
+
console.error('[REST] Re-auth failed:', authErr.message)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const data = await res.json().catch(() => ({}))
|
|
359
|
+
sendToRelay({ type: 'rest-response', requestId, status: res.status, data })
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(`[REST] ${method} ${p} failed:`, err.message, err.cause?.message || '')
|
|
362
|
+
sendToRelay({ type: 'rest-response', requestId, status: 502, data: { error: err.message } })
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function handleAuthRequest(requestId, accessToken) {
|
|
367
|
+
try {
|
|
368
|
+
const res = await fetch(`${hubUrl}/api/auth`, {
|
|
369
|
+
method: 'POST',
|
|
370
|
+
headers: { 'Content-Type': 'application/json' },
|
|
371
|
+
body: JSON.stringify({ accessToken }),
|
|
372
|
+
})
|
|
373
|
+
sendToRelay({ type: 'auth-response', requestId, ok: res.ok, data: await res.json() })
|
|
374
|
+
} catch (err) {
|
|
375
|
+
sendToRelay({ type: 'auth-response', requestId, ok: false, data: { error: err.message } })
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Main ──
|
|
380
|
+
|
|
381
|
+
async function main() {
|
|
382
|
+
console.log()
|
|
383
|
+
console.log(' ╔══════════════════════════════════════╗')
|
|
384
|
+
console.log(' ║ CodePocket ║')
|
|
385
|
+
console.log(' ╚══════════════════════════════════════╝')
|
|
386
|
+
console.log()
|
|
387
|
+
|
|
388
|
+
if (!relayKey) {
|
|
389
|
+
console.log(' [!] 未找到配置,请输入 Key')
|
|
390
|
+
console.log(' 在 CodePocket 小程序中获取 Key →')
|
|
391
|
+
relayKey = await new Promise((resolve) => {
|
|
392
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
393
|
+
rl.question(' Key: ', (answer) => {
|
|
394
|
+
rl.close()
|
|
395
|
+
resolve(answer.trim())
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
if (!relayKey) {
|
|
399
|
+
console.error('')
|
|
400
|
+
console.error(' [!] Key 不能为空')
|
|
401
|
+
process.exit(1)
|
|
402
|
+
}
|
|
403
|
+
saveConfig({ ...loadConfig(), relayKey, relayUrl })
|
|
404
|
+
console.log('')
|
|
405
|
+
console.log(' [✓] 配置已保存')
|
|
406
|
+
console.log('')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log(` Relay: ${relayUrl}`)
|
|
410
|
+
console.log(` Key: ${relayKey.slice(0, 8)}...`)
|
|
411
|
+
console.log()
|
|
412
|
+
|
|
413
|
+
// Step 1: Start hub
|
|
414
|
+
console.log('[1/3] 检查 Hub...')
|
|
415
|
+
try {
|
|
416
|
+
await startHub()
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.error(` [!] ${e.message}`)
|
|
419
|
+
console.error(' 你可以用 --no-hub 跳过自动启动')
|
|
420
|
+
process.exit(1)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Step 1.5: Start MCP server
|
|
424
|
+
console.log('[1.5/3] 启动 MCP server...')
|
|
425
|
+
try {
|
|
426
|
+
const mcpPort = await startMcpServer(sendToRelay)
|
|
427
|
+
injectMcpConfig(mcpPort)
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.error(` [!] MCP server 启动失败: ${e.message}`)
|
|
430
|
+
console.error(' 手机工具功能将不可用')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Step 2: Authenticate
|
|
434
|
+
console.log('[2/3] 认证...')
|
|
435
|
+
try {
|
|
436
|
+
const token = await findHubToken()
|
|
437
|
+
jwt = await authenticateHub(token)
|
|
438
|
+
console.log(' [✓] 认证成功')
|
|
439
|
+
} catch (e) {
|
|
440
|
+
console.error(` [!] ${e.message}`)
|
|
441
|
+
process.exit(1)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Step 3: Connect relay
|
|
445
|
+
console.log('[3/3] 连接中继...')
|
|
446
|
+
startSSE()
|
|
447
|
+
connectRelay()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
main().catch((err) => {
|
|
451
|
+
console.error('[!] 启动失败:', err.message)
|
|
452
|
+
process.exit(1)
|
|
453
|
+
})
|