@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
package/bin/run.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodePocket Connector — 一行命令连接小程序
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* 首次: npx codepocket --key YOUR_KEY
|
|
8
|
+
* 之后: npx codepocket
|
|
9
|
+
*
|
|
10
|
+
* 也可通过环境变量配置:
|
|
11
|
+
* RELAY_KEY=xxx codepocket
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { WebSocket } = require('ws')
|
|
15
|
+
const fs = require('fs')
|
|
16
|
+
const path = require('path')
|
|
17
|
+
const os = require('os')
|
|
18
|
+
const qrcode = require('qrcode-terminal')
|
|
19
|
+
|
|
20
|
+
// ── Config ──
|
|
21
|
+
|
|
22
|
+
const CONFIG_DIR = path.join(os.homedir(), '.codepocket', 'hub')
|
|
23
|
+
const CONFIG_FILE = path.join(path.join(os.homedir(), '.codepocket'), 'config.json')
|
|
24
|
+
const SESSION_FILE = path.join(CONFIG_DIR, 'wechat-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
|
+
// Parse args
|
|
36
|
+
const args = process.argv.slice(2)
|
|
37
|
+
let hubUrl = process.env.CPKT_HUB_URL || process.env.HAPI_HUB_URL || 'http://127.0.0.1:3006'
|
|
38
|
+
let relayUrl = process.env.RELAY_WS_URL || ''
|
|
39
|
+
let relayKey = process.env.RELAY_KEY || ''
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i]
|
|
43
|
+
if (arg.startsWith('--hub=')) hubUrl = arg.slice(6).replace(/\/+$/, '')
|
|
44
|
+
if (arg === '--hub' && args[i + 1]) { hubUrl = args[++i].replace(/\/+$/, '') }
|
|
45
|
+
if (arg.startsWith('--relay=')) relayUrl = arg.slice(8)
|
|
46
|
+
if (arg === '--relay' && args[i + 1]) { relayUrl = args[++i] }
|
|
47
|
+
if (arg.startsWith('--key=')) relayKey = arg.slice(6)
|
|
48
|
+
if (arg === '--key' && args[i + 1]) { relayKey = args[++i] }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Load saved config, merge with args
|
|
52
|
+
const saved = loadConfig()
|
|
53
|
+
if (relayKey) {
|
|
54
|
+
// New key provided — save it
|
|
55
|
+
saveConfig({ ...saved, relayKey, relayUrl: relayUrl || saved.relayUrl || 'wss://bkstory.cn/ws' })
|
|
56
|
+
} else {
|
|
57
|
+
relayKey = saved.relayKey || ''
|
|
58
|
+
}
|
|
59
|
+
if (!relayUrl) relayUrl = saved.relayUrl || 'wss://bkstory.cn/ws'
|
|
60
|
+
|
|
61
|
+
let jwt = ''
|
|
62
|
+
let relayWs = null
|
|
63
|
+
let relaySessionToken = ''
|
|
64
|
+
let sseResponse = null
|
|
65
|
+
|
|
66
|
+
// ── Find CLI token ──
|
|
67
|
+
|
|
68
|
+
async function findHubToken() {
|
|
69
|
+
const hubHome = process.env.HAPI_HOME || path.join(os.homedir(), '.codepocket', 'hub')
|
|
70
|
+
const settingsPath = path.join(hubHome, 'settings.json')
|
|
71
|
+
try {
|
|
72
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
|
|
73
|
+
if (settings.cliApiToken) return settings.cliApiToken
|
|
74
|
+
} catch {}
|
|
75
|
+
if (process.env.CLI_API_TOKEN) return process.env.CLI_API_TOKEN
|
|
76
|
+
console.error('[!] 找不到 Hub token,请先运行: cpkt')
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Authenticate ──
|
|
81
|
+
|
|
82
|
+
async function authenticateHub(token) {
|
|
83
|
+
const res = await fetch(`${hubUrl}/api/auth`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ accessToken: token }),
|
|
87
|
+
})
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const data = await res.json().catch(() => ({}))
|
|
90
|
+
throw new Error(`Hub auth failed: ${data.error || res.status}`)
|
|
91
|
+
}
|
|
92
|
+
return (await res.json()).token
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── SSE stream ──
|
|
96
|
+
|
|
97
|
+
async function startSSE() {
|
|
98
|
+
const url = `${hubUrl}/api/events?token=${encodeURIComponent(jwt)}&all=true`
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(url, { headers: { Accept: 'text/event-stream' } })
|
|
101
|
+
if (!res.ok || !res.body) {
|
|
102
|
+
console.error(`[SSE] failed: ${res.status}, retrying in 5s...`)
|
|
103
|
+
setTimeout(startSSE, 5000)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const reader = res.body.getReader()
|
|
107
|
+
const decoder = new TextDecoder()
|
|
108
|
+
let buffer = ''
|
|
109
|
+
async function pump() {
|
|
110
|
+
while (true) {
|
|
111
|
+
const { done, value } = await reader.read()
|
|
112
|
+
if (done) break
|
|
113
|
+
buffer += decoder.decode(value, { stream: true })
|
|
114
|
+
const lines = buffer.split('\n')
|
|
115
|
+
buffer = lines.pop() || ''
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
if (line.startsWith('data:')) {
|
|
118
|
+
const raw = line.slice(5).trim()
|
|
119
|
+
if (!raw) continue
|
|
120
|
+
try { sendToRelay({ type: 'sse-event', event: JSON.parse(raw) }) } catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.error('[SSE] stream ended, reconnecting in 5s...')
|
|
125
|
+
setTimeout(startSSE, 5000)
|
|
126
|
+
}
|
|
127
|
+
sseResponse = res
|
|
128
|
+
pump().catch(() => { setTimeout(startSSE, 5000) })
|
|
129
|
+
} catch {
|
|
130
|
+
console.error('[SSE] connection error, retrying in 5s...')
|
|
131
|
+
setTimeout(startSSE, 5000)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Relay connection ──
|
|
136
|
+
|
|
137
|
+
function connectRelay() {
|
|
138
|
+
let savedSession = null
|
|
139
|
+
try { savedSession = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8')) } catch {}
|
|
140
|
+
|
|
141
|
+
relayWs = new WebSocket(relayUrl)
|
|
142
|
+
|
|
143
|
+
relayWs.on('open', () => {
|
|
144
|
+
console.log('[Relay] connected')
|
|
145
|
+
relayWs.send(JSON.stringify({
|
|
146
|
+
type: 'register',
|
|
147
|
+
key: relayKey,
|
|
148
|
+
sessionToken: savedSession?.sessionToken || undefined,
|
|
149
|
+
}))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
relayWs.on('message', (raw) => {
|
|
153
|
+
let msg
|
|
154
|
+
try { msg = JSON.parse(raw.toString()) } catch { return }
|
|
155
|
+
handleRelayMessage(msg)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
relayWs.on('close', () => {
|
|
159
|
+
console.log('[Relay] disconnected, reconnecting in 3s...')
|
|
160
|
+
relayWs = null
|
|
161
|
+
setTimeout(connectRelay, 3000)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
relayWs.on('error', () => { relayWs?.close() })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleRelayMessage(msg) {
|
|
168
|
+
switch (msg.type) {
|
|
169
|
+
case 'registered': {
|
|
170
|
+
relaySessionToken = msg.sessionToken
|
|
171
|
+
try {
|
|
172
|
+
fs.mkdirSync(path.dirname(SESSION_FILE), { recursive: true })
|
|
173
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify({ sessionToken: relaySessionToken, hubUrl, relayUrl }))
|
|
174
|
+
} catch {}
|
|
175
|
+
if (msg.reconnected) {
|
|
176
|
+
console.log('\n [✓] 已重新连接\n')
|
|
177
|
+
} else {
|
|
178
|
+
const qrContent = `codepocket://pair?token=${msg.token}`
|
|
179
|
+
console.log()
|
|
180
|
+
qrcode.generate(qrContent, { small: true }, (qrStr) => {
|
|
181
|
+
console.log(qrStr)
|
|
182
|
+
})
|
|
183
|
+
console.log(' 打开小程序 → 扫码连接\n')
|
|
184
|
+
}
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
case 'client-connected': {
|
|
188
|
+
console.log(' [✓] 微信小程序已连接')
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
case 'rest-request': {
|
|
192
|
+
forwardToHub(msg.requestId, msg.method, msg.path, msg.body)
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
case 'auth-request': {
|
|
196
|
+
handleAuthRequest(msg.requestId, msg.accessToken)
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
case 'register-error': {
|
|
200
|
+
console.error(` [!] ${msg.error}`)
|
|
201
|
+
console.error(' 请检查 key 是否正确')
|
|
202
|
+
console.error(' 获取 key: curl -H "Authorization: Bearer ADMIN_KEY" http://127.0.0.1:3010/admin/keys')
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sendToRelay(msg) {
|
|
209
|
+
if (relayWs?.readyState === WebSocket.OPEN) {
|
|
210
|
+
relayWs.send(JSON.stringify(msg))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function forwardToHub(requestId, method, p, body) {
|
|
215
|
+
try {
|
|
216
|
+
const init = {
|
|
217
|
+
method,
|
|
218
|
+
headers: {
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
...(jwt ? { Authorization: `Bearer ${jwt}` } : {}),
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
if (body && method !== 'GET') init.body = JSON.stringify(body)
|
|
224
|
+
const res = await fetch(`${hubUrl}${p}`, init)
|
|
225
|
+
const data = await res.json().catch(() => ({}))
|
|
226
|
+
sendToRelay({ type: 'rest-response', requestId, status: res.status, data })
|
|
227
|
+
} catch (err) {
|
|
228
|
+
sendToRelay({ type: 'rest-response', requestId, status: 502, data: { error: err.message } })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleAuthRequest(requestId, accessToken) {
|
|
233
|
+
try {
|
|
234
|
+
const res = await fetch(`${hubUrl}/api/auth`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify({ accessToken }),
|
|
238
|
+
})
|
|
239
|
+
const data = await res.json()
|
|
240
|
+
sendToRelay({ type: 'auth-response', requestId, ok: res.ok, data })
|
|
241
|
+
} catch (err) {
|
|
242
|
+
sendToRelay({ type: 'auth-response', requestId, ok: false, data: { error: err.message } })
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Main ──
|
|
247
|
+
|
|
248
|
+
async function main() {
|
|
249
|
+
console.log('╔══════════════════════════════════════╗')
|
|
250
|
+
console.log('║ CodePocket Connect ║')
|
|
251
|
+
console.log('╚══════════════════════════════════════╝')
|
|
252
|
+
console.log()
|
|
253
|
+
|
|
254
|
+
if (!relayKey) {
|
|
255
|
+
console.error('[!] 需要 relay 密钥(首次使用只需设置一次)')
|
|
256
|
+
console.error()
|
|
257
|
+
console.error(' 用法:')
|
|
258
|
+
console.error(' codepocket --key YOUR_KEY')
|
|
259
|
+
console.error()
|
|
260
|
+
console.error(' 获取 key:')
|
|
261
|
+
console.error(' curl -H "Authorization: Bearer ADMIN_KEY" http://YOUR_SERVER:3010/admin/keys')
|
|
262
|
+
console.error()
|
|
263
|
+
console.error(' 或设置环境变量:')
|
|
264
|
+
console.error(' export RELAY_KEY=xxx')
|
|
265
|
+
console.error()
|
|
266
|
+
process.exit(1)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(` Hub: ${hubUrl}`)
|
|
270
|
+
console.log(` Relay: ${relayUrl}`)
|
|
271
|
+
console.log(` Key: ${relayKey.slice(0, 8)}...`)
|
|
272
|
+
console.log()
|
|
273
|
+
|
|
274
|
+
console.log('[1/3] 连接 Hub...')
|
|
275
|
+
const token = await findHubToken()
|
|
276
|
+
|
|
277
|
+
console.log('[2/3] 认证中...')
|
|
278
|
+
jwt = await authenticateHub(token)
|
|
279
|
+
console.log('[✓] Hub 认证成功')
|
|
280
|
+
|
|
281
|
+
console.log('[3/3] 连接中继服务器...')
|
|
282
|
+
startSSE()
|
|
283
|
+
connectRelay()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
main().catch((err) => {
|
|
287
|
+
console.error('[!] 启动失败:', err.message)
|
|
288
|
+
process.exit(1)
|
|
289
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tina_summer/codepocket",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CodePocket — 你的全能 AI 随身控制台",
|
|
5
|
+
"bin": {
|
|
6
|
+
"codepocket": "bin/codepocket.js",
|
|
7
|
+
"cpkt": "bin/codepocket.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"vendor"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node bin/codepocket.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"qrcode-terminal": "^0.12.0",
|
|
19
|
+
"ws": "^8.18.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"codepocket",
|
|
26
|
+
"claude",
|
|
27
|
+
"wechat",
|
|
28
|
+
"ai"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodePocket Connector — 在用户机器上运行,桥接本地 Hapi Hub 和远程 Relay
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* bun run src/index.ts # 自动发现本地 hub
|
|
6
|
+
* bun run src/index.ts --hub http://localhost:3006 --relay wss://relay.example.com/ws
|
|
7
|
+
*
|
|
8
|
+
* 工作流程:
|
|
9
|
+
* 1. 连接本地 Hapi Hub REST API,获取 CLI_API_TOKEN
|
|
10
|
+
* 2. 用 token 调用 /api/auth 获取 JWT
|
|
11
|
+
* 3. 开启 SSE 流,接收实时事件
|
|
12
|
+
* 4. 连接远程 Relay WebSocket
|
|
13
|
+
* 5. 注册并获取配对码,显示给用户
|
|
14
|
+
* 6. 双向转发: SSE→Relay, Relay REST→Hub REST
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const HUB_URL = (process.env.CPKT_HUB_URL || process.env.HAPI_HUB_URL || 'http://127.0.0.1:3006').replace(/\/+$/, '')
|
|
18
|
+
const RELAY_WS = process.env.RELAY_WS_URL || 'ws://127.0.0.1:3010/ws'
|
|
19
|
+
const HUB_HOME = process.env.HAPI_HOME || `${process.env.HOME}/.codepocket/hub`
|
|
20
|
+
const SESSION_FILE = `${HUB_HOME}/wechat-session.json`
|
|
21
|
+
|
|
22
|
+
let hubToken = '' // CLI_API_TOKEN
|
|
23
|
+
let jwt = '' // JWT from hub
|
|
24
|
+
let relayWs: any = null
|
|
25
|
+
let relaySessionToken = ''
|
|
26
|
+
let sseController: AbortController | null = null
|
|
27
|
+
|
|
28
|
+
// --- Parse args ---
|
|
29
|
+
for (const arg of process.argv.slice(2)) {
|
|
30
|
+
if (arg.startsWith('--hub=')) process.env.HAPI_HUB_URL = arg.slice(6)
|
|
31
|
+
if (arg.startsWith('--relay=')) process.env.RELAY_WS_URL = arg.slice(8)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hubUrl = (process.env.HAPI_HUB_URL || HUB_URL).replace(/\/+$/, '')
|
|
35
|
+
const relayUrl = process.env.RELAY_WS_URL || RELAY_WS
|
|
36
|
+
|
|
37
|
+
// --- Step 1: Find CLI token ---
|
|
38
|
+
|
|
39
|
+
async function findHubToken(): Promise<string> {
|
|
40
|
+
// Try settings file
|
|
41
|
+
const settingsPath = `${HUB_HOME}/settings.json`
|
|
42
|
+
try {
|
|
43
|
+
const settings = await Bun.file(settingsPath).json()
|
|
44
|
+
if (settings.cliApiToken) return settings.cliApiToken
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
// Try env
|
|
48
|
+
if (process.env.CLI_API_TOKEN) return process.env.CLI_API_TOKEN
|
|
49
|
+
|
|
50
|
+
console.error('[!] 无法找到 CLI_API_TOKEN')
|
|
51
|
+
console.error(' 请确保 Hub 正在运行,或设置 CLI_API_TOKEN 环境变量')
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Step 2: Authenticate with hub ---
|
|
56
|
+
|
|
57
|
+
async function authenticateHub(token: string): Promise<string> {
|
|
58
|
+
const res = await fetch(`${hubUrl}/api/auth`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ accessToken: token }),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const data = await res.json().catch(() => ({}))
|
|
66
|
+
throw new Error(`Hub auth failed: ${data.error || res.status}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = await res.json()
|
|
70
|
+
return data.token
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- Step 3: Start SSE from hub ---
|
|
74
|
+
|
|
75
|
+
function startSSE() {
|
|
76
|
+
sseController?.abort()
|
|
77
|
+
sseController = new AbortController()
|
|
78
|
+
|
|
79
|
+
const url = `${hubUrl}/api/events?token=${encodeURIComponent(jwt)}&all=true`
|
|
80
|
+
|
|
81
|
+
;(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(url, {
|
|
84
|
+
signal: sseController!.signal,
|
|
85
|
+
headers: { Accept: 'text/event-stream' },
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!res.ok || !res.body) {
|
|
89
|
+
console.error(`[SSE] connection failed: ${res.status}`)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const reader = res.body.getReader()
|
|
94
|
+
const decoder = new TextDecoder()
|
|
95
|
+
let buffer = ''
|
|
96
|
+
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read()
|
|
99
|
+
if (done) break
|
|
100
|
+
|
|
101
|
+
buffer += decoder.decode(value, { stream: true })
|
|
102
|
+
const lines = buffer.split('\n')
|
|
103
|
+
buffer = lines.pop() || ''
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (line.startsWith('data:')) {
|
|
107
|
+
const raw = line.slice(5).trim()
|
|
108
|
+
if (!raw) continue
|
|
109
|
+
try {
|
|
110
|
+
const event = JSON.parse(raw)
|
|
111
|
+
forwardToRelay({ type: 'sse-event', event })
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
if (err.name !== 'AbortError') {
|
|
118
|
+
console.error('[SSE] stream ended, reconnecting in 5s...')
|
|
119
|
+
setTimeout(startSSE, 5000)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Step 4: Connect to relay ---
|
|
126
|
+
|
|
127
|
+
function connectRelay() {
|
|
128
|
+
try {
|
|
129
|
+
relayWs = new WebSocket(relayUrl)
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`[Relay] connect failed, retrying in 5s...`)
|
|
132
|
+
setTimeout(connectRelay, 5000)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
relayWs.onopen = () => {
|
|
137
|
+
console.log('[Relay] connected')
|
|
138
|
+
|
|
139
|
+
// Try to reconnect with existing session
|
|
140
|
+
let savedSession: any = null
|
|
141
|
+
try { savedSession = JSON.parse(require('fs').readFileSync(SESSION_FILE, 'utf-8')) } catch {}
|
|
142
|
+
|
|
143
|
+
relayWs.send(JSON.stringify({
|
|
144
|
+
type: 'register',
|
|
145
|
+
sessionToken: savedSession?.sessionToken || undefined,
|
|
146
|
+
}))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
relayWs.onmessage = (event: MessageEvent) => {
|
|
150
|
+
let msg: any
|
|
151
|
+
try { msg = JSON.parse(event.data as string) } catch { return }
|
|
152
|
+
handleRelayMessage(msg)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
relayWs.onclose = () => {
|
|
156
|
+
console.log('[Relay] disconnected, reconnecting in 3s...')
|
|
157
|
+
relayWs = null
|
|
158
|
+
setTimeout(connectRelay, 3000)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
relayWs.onerror = () => {
|
|
162
|
+
relayWs?.close()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleRelayMessage(msg: any) {
|
|
167
|
+
switch (msg.type) {
|
|
168
|
+
case 'registered': {
|
|
169
|
+
relaySessionToken = msg.sessionToken
|
|
170
|
+
|
|
171
|
+
// Save session for reconnection
|
|
172
|
+
try {
|
|
173
|
+
require('fs').mkdirSync(require('path').dirname(SESSION_FILE), { recursive: true })
|
|
174
|
+
require('fs').writeFileSync(SESSION_FILE, JSON.stringify({
|
|
175
|
+
sessionToken: relaySessionToken,
|
|
176
|
+
hubUrl,
|
|
177
|
+
relayUrl,
|
|
178
|
+
}))
|
|
179
|
+
} catch {}
|
|
180
|
+
|
|
181
|
+
if (msg.reconnected) {
|
|
182
|
+
console.log(`\n[✓] 已重新连接\n`)
|
|
183
|
+
} else {
|
|
184
|
+
console.log(`\n${'═'.repeat(40)}`)
|
|
185
|
+
console.log(` 微信小程序配对码: ${msg.code}`)
|
|
186
|
+
console.log(`${'═'.repeat(40)}\n`)
|
|
187
|
+
console.log(' 打开小程序 → 输入配对码 → 即可连接')
|
|
188
|
+
console.log(' 配对码 10 分钟内有效\n')
|
|
189
|
+
}
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'client-connected': {
|
|
194
|
+
console.log('[✓] 微信小程序已连接')
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Client REST request → forward to local hub
|
|
199
|
+
case 'rest-request': {
|
|
200
|
+
forwardToHub(msg.requestId, msg.method, msg.path, msg.body)
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Client auth request → forward to local hub
|
|
205
|
+
case 'auth-request': {
|
|
206
|
+
handleAuthRequest(msg.requestId, msg.accessToken)
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Forwarding ---
|
|
213
|
+
|
|
214
|
+
function forwardToRelay(msg: any) {
|
|
215
|
+
if (relayWs && relayWs.readyState === WebSocket.OPEN) {
|
|
216
|
+
relayWs.send(JSON.stringify(msg))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function forwardToHub(requestId: string, method: string, path: string, body?: any) {
|
|
221
|
+
try {
|
|
222
|
+
const init: RequestInit = {
|
|
223
|
+
method,
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
...(jwt ? { Authorization: `Bearer ${jwt}` } : {}),
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
if (body && method !== 'GET') init.body = JSON.stringify(body)
|
|
230
|
+
|
|
231
|
+
const res = await fetch(`${hubUrl}${path}`, init)
|
|
232
|
+
const data = await res.json().catch(() => ({}))
|
|
233
|
+
|
|
234
|
+
forwardToRelay({
|
|
235
|
+
type: 'rest-response',
|
|
236
|
+
requestId,
|
|
237
|
+
status: res.status,
|
|
238
|
+
data,
|
|
239
|
+
})
|
|
240
|
+
} catch (err: any) {
|
|
241
|
+
console.error(`[REST] ${method} ${path} failed:`, err.message)
|
|
242
|
+
forwardToRelay({
|
|
243
|
+
type: 'rest-response',
|
|
244
|
+
requestId,
|
|
245
|
+
status: 502,
|
|
246
|
+
data: { error: err.message },
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function handleAuthRequest(requestId: string, accessToken: string) {
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(`${hubUrl}/api/auth`, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: { 'Content-Type': 'application/json' },
|
|
256
|
+
body: JSON.stringify({ accessToken }),
|
|
257
|
+
})
|
|
258
|
+
const data = await res.json()
|
|
259
|
+
forwardToRelay({
|
|
260
|
+
type: 'auth-response',
|
|
261
|
+
requestId,
|
|
262
|
+
ok: res.ok,
|
|
263
|
+
data,
|
|
264
|
+
})
|
|
265
|
+
} catch (err: any) {
|
|
266
|
+
forwardToRelay({
|
|
267
|
+
type: 'auth-response',
|
|
268
|
+
requestId,
|
|
269
|
+
ok: false,
|
|
270
|
+
data: { error: err.message },
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- Main ---
|
|
276
|
+
|
|
277
|
+
async function main() {
|
|
278
|
+
console.log('╔══════════════════════════════════════╗')
|
|
279
|
+
console.log('║ CodePocket Connector v1.0 ║')
|
|
280
|
+
console.log('╚══════════════════════════════════════╝')
|
|
281
|
+
console.log()
|
|
282
|
+
console.log(`Hub: ${hubUrl}`)
|
|
283
|
+
console.log(`Relay: ${relayUrl}`)
|
|
284
|
+
console.log()
|
|
285
|
+
|
|
286
|
+
// 1. Find token
|
|
287
|
+
console.log('[1/3] 连接 Hub...')
|
|
288
|
+
hubToken = await findHubToken()
|
|
289
|
+
|
|
290
|
+
// 2. Authenticate
|
|
291
|
+
console.log('[2/3] 认证中...')
|
|
292
|
+
jwt = await authenticateHub(hubToken)
|
|
293
|
+
console.log('[✓] Hub 认证成功')
|
|
294
|
+
|
|
295
|
+
// 3. Start SSE + connect relay
|
|
296
|
+
console.log('[3/3] 连接中继服务器...')
|
|
297
|
+
startSSE()
|
|
298
|
+
connectRelay()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
main().catch((err) => {
|
|
302
|
+
console.error('[!] 启动失败:', err.message)
|
|
303
|
+
process.exit(1)
|
|
304
|
+
})
|