137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
|
|
import Router from '@koa/router'
|
||
|
|
import axios from 'axios'
|
||
|
|
import { readFile, writeFile } from 'fs/promises'
|
||
|
|
import { chmod } from 'fs/promises'
|
||
|
|
import { resolve } from 'path'
|
||
|
|
import { homedir } from 'os'
|
||
|
|
import { restartGateway } from '../services/hermes-cli'
|
||
|
|
|
||
|
|
const envPath = resolve(homedir(), '.hermes/.env')
|
||
|
|
const ILINK_BASE = 'https://ilinkai.weixin.qq.com'
|
||
|
|
|
||
|
|
export const weixinRoutes = new Router()
|
||
|
|
|
||
|
|
// GET /api/weixin/qrcode — fetch QR code from Tencent iLink API
|
||
|
|
weixinRoutes.get('/api/weixin/qrcode', async (ctx) => {
|
||
|
|
try {
|
||
|
|
const res = await axios.get(`${ILINK_BASE}/ilink/bot/get_bot_qrcode`, {
|
||
|
|
params: { bot_type: 3 },
|
||
|
|
timeout: 15000,
|
||
|
|
})
|
||
|
|
const data = res.data
|
||
|
|
if (!data || !data.qrcode) {
|
||
|
|
ctx.status = 500
|
||
|
|
ctx.body = { error: 'Failed to get QR code' }
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ctx.body = {
|
||
|
|
qrcode: data.qrcode,
|
||
|
|
qrcode_url: data.qrcode_img_content,
|
||
|
|
}
|
||
|
|
} catch (err: any) {
|
||
|
|
ctx.status = 500
|
||
|
|
ctx.body = { error: err.message || 'Failed to connect to iLink API' }
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// GET /api/weixin/qrcode/status — poll QR scan status
|
||
|
|
weixinRoutes.get('/api/weixin/qrcode/status', async (ctx) => {
|
||
|
|
const qrcode = ctx.query.qrcode as string
|
||
|
|
if (!qrcode) {
|
||
|
|
ctx.status = 400
|
||
|
|
ctx.body = { error: 'Missing qrcode parameter' }
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await axios.get(`${ILINK_BASE}/ilink/bot/get_qrcode_status`, {
|
||
|
|
params: { qrcode },
|
||
|
|
timeout: 35000,
|
||
|
|
})
|
||
|
|
const data = res.data
|
||
|
|
const status = data?.status || 'wait'
|
||
|
|
ctx.body = { status }
|
||
|
|
|
||
|
|
// If confirmed, return credentials so frontend can save them
|
||
|
|
if (status === 'confirmed') {
|
||
|
|
ctx.body = {
|
||
|
|
status: 'confirmed',
|
||
|
|
account_id: data.ilink_bot_id,
|
||
|
|
token: data.bot_token,
|
||
|
|
base_url: data.baseurl,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err: any) {
|
||
|
|
ctx.status = 500
|
||
|
|
ctx.body = { error: err.message || 'Failed to poll QR status' }
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// POST /api/weixin/save — save weixin credentials to .env
|
||
|
|
weixinRoutes.post('/api/weixin/save', async (ctx) => {
|
||
|
|
const { account_id, token, base_url } = ctx.request.body as {
|
||
|
|
account_id: string
|
||
|
|
token: string
|
||
|
|
base_url?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!account_id || !token) {
|
||
|
|
ctx.status = 400
|
||
|
|
ctx.body = { error: 'Missing account_id or token' }
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
let raw: string
|
||
|
|
try {
|
||
|
|
raw = await readFile(envPath, 'utf-8')
|
||
|
|
} catch {
|
||
|
|
raw = ''
|
||
|
|
}
|
||
|
|
|
||
|
|
const entries: Record<string, string> = {
|
||
|
|
WEIXIN_ACCOUNT_ID: account_id,
|
||
|
|
WEIXIN_TOKEN: token,
|
||
|
|
}
|
||
|
|
if (base_url) entries.WEIXIN_BASE_URL = base_url
|
||
|
|
|
||
|
|
const lines = raw.split('\n')
|
||
|
|
const existingKeys = new Set<string>()
|
||
|
|
|
||
|
|
const result: string[] = []
|
||
|
|
for (const line of lines) {
|
||
|
|
const trimmed = line.trim()
|
||
|
|
if (trimmed.startsWith('#')) {
|
||
|
|
result.push(line)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
const eqIdx = trimmed.indexOf('=')
|
||
|
|
if (eqIdx !== -1) {
|
||
|
|
const key = trimmed.slice(0, eqIdx).trim()
|
||
|
|
if (key in entries) {
|
||
|
|
result.push(`${key}=${entries[key]}`)
|
||
|
|
existingKeys.add(key)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result.push(line)
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const [key, val] of Object.entries(entries)) {
|
||
|
|
if (!existingKeys.has(key)) {
|
||
|
|
result.push(`${key}=${val}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let output = result.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '') + '\n'
|
||
|
|
await writeFile(envPath, output, 'utf-8')
|
||
|
|
try { await chmod(envPath, 0o600) } catch { /* ignore */ }
|
||
|
|
await restartGateway()
|
||
|
|
|
||
|
|
ctx.body = { success: true }
|
||
|
|
} catch (err: any) {
|
||
|
|
ctx.status = 500
|
||
|
|
ctx.body = { error: err.message }
|
||
|
|
}
|
||
|
|
})
|