Remove the API relay link and the ESP32 device feature
Two pieces of upstream promotion came out of this deployment. The sidebar "apiRelay" tab only opened a registration page on a third-party site, so it goes with no replacement. The circuit badge next to Settings was not what it looked like. It opened an ESP32 device manager: add a device, show LAN status, remote connect/disconnect, rename, delete, backed by six REST routes and an mcu_devices table. The purchase button inside that dialog was the promotional part; the rest was real device management. Since the hardware is out of scope here, the entire feature is removed rather than just the link that pointed upstream - UI entry, client API, routes, controller, store, schema, locale strings, and the one GlobalAgentServer method that only existed to answer its status column. Table creation is dropped but no DROP TABLE was added, so an already-provisioned database keeps its rows as harmless orphans instead of losing data on upgrade. DevicesView and the hermes.devices route are untouched: that is LAN device discovery, a separate feature. AGENTS.md now records the removal so the badge does not come back.
This commit is contained in:
@@ -1,234 +0,0 @@
|
||||
import type { Context } from 'koa'
|
||||
import { config } from '../config'
|
||||
import { getLanEndpointKind } from '../services/lan-discovery'
|
||||
import { getPublicSystemInfo } from '../services/system-info'
|
||||
import { getOutboundRelayClient, startOutboundRelayClient, stopOutboundRelayClient } from '../services/global-agent/outbound-relay-client'
|
||||
import { getGlobalAgentServer } from '../services/global-agent/server'
|
||||
import { createMcuDevice, deleteMcuDevice, getMcuDevice, listMcuDevices, updateMcuDeviceName, type McuDeviceRecord } from '../db/hermes/mcu-devices-store'
|
||||
|
||||
function normalizeDeviceCode(value: unknown): string {
|
||||
const normalized = String(value || '').trim()
|
||||
return normalized.length <= 255 ? normalized : ''
|
||||
}
|
||||
|
||||
function normalizeName(value: unknown, deviceCode: string): string {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized.length > 80) return normalized.slice(0, 80)
|
||||
return normalized || deviceCode
|
||||
}
|
||||
|
||||
async function verifyOfficialDeviceCode(deviceCode: string): Promise<boolean> {
|
||||
try {
|
||||
const url = `${config.remoteRelay.url.replace(/\/$/, '')}/global-agent/device/${encodeURIComponent(deviceCode)}`
|
||||
const response = await fetch(url, { method: 'GET' })
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function remoteConnectionId(deviceCode: string): string {
|
||||
return `mcu-device:${deviceCode}`
|
||||
}
|
||||
|
||||
function requestToken(ctx: Context): string {
|
||||
const auth = ctx.headers.authorization || ''
|
||||
if (typeof auth === 'string' && auth.startsWith('Bearer ')) return auth.slice(7).trim()
|
||||
return typeof ctx.query.token === 'string' ? ctx.query.token.trim() : ''
|
||||
}
|
||||
|
||||
function requestBaseUrl(ctx: Context): string | undefined {
|
||||
const host = ctx.get('host').trim()
|
||||
if (!host) return undefined
|
||||
return `${ctx.protocol || 'http'}://${host}`
|
||||
}
|
||||
|
||||
function localBaseUrl(): string {
|
||||
return `http://127.0.0.1:${config.port}`
|
||||
}
|
||||
|
||||
async function localRelayMachineInfo(url: string) {
|
||||
const info = await getPublicSystemInfo()
|
||||
return {
|
||||
...info,
|
||||
http_port: config.port,
|
||||
endpoint_kind: getLanEndpointKind(config.port),
|
||||
url,
|
||||
relay_url: config.remoteRelay.url,
|
||||
}
|
||||
}
|
||||
|
||||
function withMcuDeviceStatus(device: McuDeviceRecord) {
|
||||
const remoteClient = getOutboundRelayClient(remoteConnectionId(device.device_code))
|
||||
return {
|
||||
...device,
|
||||
lan_connected: Boolean(getGlobalAgentServer()?.hasMcuDeviceCode(device.device_code)),
|
||||
remote_connected: Boolean(remoteClient?.isConnected()),
|
||||
}
|
||||
}
|
||||
|
||||
function listMcuDevicesWithStatus() {
|
||||
return listMcuDevices().map(withMcuDeviceStatus)
|
||||
}
|
||||
|
||||
export async function listMcuDevicesController(ctx: Context) {
|
||||
ctx.body = { devices: listMcuDevicesWithStatus() }
|
||||
}
|
||||
|
||||
export async function createMcuDeviceController(ctx: Context) {
|
||||
const body = ctx.request.body as { name?: unknown; device_code?: unknown; deviceCode?: unknown } | undefined
|
||||
const deviceCode = normalizeDeviceCode(body?.device_code ?? body?.deviceCode)
|
||||
if (!deviceCode) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'device_code is required' }
|
||||
return
|
||||
}
|
||||
|
||||
const isOfficial = await verifyOfficialDeviceCode(deviceCode)
|
||||
|
||||
try {
|
||||
const device = createMcuDevice({
|
||||
name: normalizeName(body?.name, deviceCode),
|
||||
deviceCode,
|
||||
isOfficial,
|
||||
})
|
||||
ctx.status = 201
|
||||
ctx.body = {
|
||||
device: withMcuDeviceStatus(device),
|
||||
devices: listMcuDevicesWithStatus(),
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'mcu_device_exists') {
|
||||
ctx.status = 409
|
||||
ctx.body = { error: 'MCU device already exists' }
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMcuDeviceController(ctx: Context) {
|
||||
const id = Number(ctx.params.id)
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'invalid mcu device id' }
|
||||
return
|
||||
}
|
||||
|
||||
const body = ctx.request.body as { name?: unknown } | undefined
|
||||
try {
|
||||
const device = updateMcuDeviceName(id, String(body?.name || ''))
|
||||
ctx.body = {
|
||||
device: withMcuDeviceStatus(device),
|
||||
devices: listMcuDevicesWithStatus(),
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'mcu_device_not_found') {
|
||||
ctx.status = 404
|
||||
ctx.body = { error: 'MCU device not found' }
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMcuDeviceController(ctx: Context) {
|
||||
const id = Number(ctx.params.id)
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'invalid mcu device id' }
|
||||
return
|
||||
}
|
||||
|
||||
const deleted = deleteMcuDevice(id)
|
||||
if (!deleted) {
|
||||
ctx.status = 404
|
||||
ctx.body = { error: 'MCU device not found' }
|
||||
return
|
||||
}
|
||||
|
||||
ctx.body = { devices: listMcuDevicesWithStatus() }
|
||||
}
|
||||
|
||||
export async function connectMcuDeviceRemoteController(ctx: Context) {
|
||||
const id = Number(ctx.params.id)
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'invalid mcu device id' }
|
||||
return
|
||||
}
|
||||
|
||||
const device = getMcuDevice(id)
|
||||
if (!device) {
|
||||
ctx.status = 404
|
||||
ctx.body = { error: 'MCU device not found' }
|
||||
return
|
||||
}
|
||||
|
||||
if (!await verifyOfficialDeviceCode(device.device_code)) {
|
||||
ctx.status = 403
|
||||
ctx.body = { error: '非官方设备码' }
|
||||
return
|
||||
}
|
||||
|
||||
const userToken = requestToken(ctx)
|
||||
if (!userToken) {
|
||||
ctx.status = 401
|
||||
ctx.body = { error: 'Unauthorized' }
|
||||
return
|
||||
}
|
||||
|
||||
const connectionId = remoteConnectionId(device.device_code)
|
||||
stopOutboundRelayClient(connectionId)
|
||||
const baseUrl = localBaseUrl()
|
||||
const publicBaseUrl = requestBaseUrl(ctx) || baseUrl
|
||||
const machineInfo = await localRelayMachineInfo(publicBaseUrl)
|
||||
const client = startOutboundRelayClient({
|
||||
connectionId,
|
||||
relayUrl: config.remoteRelay.url,
|
||||
userToken,
|
||||
instanceId: connectionId,
|
||||
deviceCode: device.device_code,
|
||||
localBaseUrl: baseUrl,
|
||||
machineInfo,
|
||||
relayProtocol: 'mcu-socket.io',
|
||||
})
|
||||
if (!client) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'Failed to start relay client' }
|
||||
return
|
||||
}
|
||||
if (!await client.waitForConnected(8000)) {
|
||||
stopOutboundRelayClient(connectionId)
|
||||
ctx.status = 502
|
||||
ctx.body = { error: 'Failed to connect remote relay' }
|
||||
return
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
device: withMcuDeviceStatus(device),
|
||||
devices: listMcuDevicesWithStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectMcuDeviceRemoteController(ctx: Context) {
|
||||
const id = Number(ctx.params.id)
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'invalid mcu device id' }
|
||||
return
|
||||
}
|
||||
|
||||
const device = getMcuDevice(id)
|
||||
if (!device) {
|
||||
ctx.status = 404
|
||||
ctx.body = { error: 'MCU device not found' }
|
||||
return
|
||||
}
|
||||
|
||||
stopOutboundRelayClient(remoteConnectionId(device.device_code))
|
||||
ctx.body = {
|
||||
device: withMcuDeviceStatus(device),
|
||||
devices: listMcuDevicesWithStatus(),
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { getDb, jsonDelete, jsonGetAll, jsonSet } from '../index'
|
||||
import { MCU_DEVICES_TABLE } from './schemas'
|
||||
|
||||
export interface McuDeviceRecord {
|
||||
id: number
|
||||
name: string
|
||||
device_code: string
|
||||
is_official: boolean
|
||||
created_at: number
|
||||
}
|
||||
|
||||
interface StoredMcuDeviceRow {
|
||||
id: number
|
||||
name: string
|
||||
device_code: string
|
||||
is_official: number
|
||||
created_at: number
|
||||
}
|
||||
|
||||
function rowToRecord(row: StoredMcuDeviceRow | Record<string, any>): McuDeviceRecord {
|
||||
return {
|
||||
id: Number(row.id),
|
||||
name: String(row.name || ''),
|
||||
device_code: String(row.device_code || ''),
|
||||
is_official: Number(row.is_official || 0) === 1,
|
||||
created_at: Number(row.created_at || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export function listMcuDevices(): McuDeviceRecord[] {
|
||||
const db = getDb()
|
||||
if (!db) {
|
||||
return Object.values(jsonGetAll(MCU_DEVICES_TABLE))
|
||||
.map(rowToRecord)
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
}
|
||||
|
||||
const rows = db.prepare(`SELECT * FROM ${MCU_DEVICES_TABLE} ORDER BY created_at DESC, id DESC`).all() as unknown as StoredMcuDeviceRow[]
|
||||
return rows.map(rowToRecord)
|
||||
}
|
||||
|
||||
export function getMcuDevice(id: number): McuDeviceRecord | null {
|
||||
const db = getDb()
|
||||
if (!db) {
|
||||
const row = jsonGetAll(MCU_DEVICES_TABLE)[String(id)]
|
||||
return row ? rowToRecord(row) : null
|
||||
}
|
||||
|
||||
const row = db.prepare(`SELECT * FROM ${MCU_DEVICES_TABLE} WHERE id = ?`).get(id) as unknown as StoredMcuDeviceRow | undefined
|
||||
return row ? rowToRecord(row) : null
|
||||
}
|
||||
|
||||
export function createMcuDevice(input: {
|
||||
name: string
|
||||
deviceCode: string
|
||||
isOfficial: boolean
|
||||
}): McuDeviceRecord {
|
||||
const db = getDb()
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const name = input.name.trim()
|
||||
const deviceCode = input.deviceCode.trim()
|
||||
const isOfficial = input.isOfficial ? 1 : 0
|
||||
|
||||
if (!db) {
|
||||
const existing = Object.values(jsonGetAll(MCU_DEVICES_TABLE))
|
||||
.map(rowToRecord)
|
||||
.find(record => record.device_code === deviceCode)
|
||||
if (existing) throw new Error('mcu_device_exists')
|
||||
const id = Date.now()
|
||||
const row = { id, name, device_code: deviceCode, is_official: isOfficial, created_at: now }
|
||||
jsonSet(MCU_DEVICES_TABLE, String(id), row as any)
|
||||
return rowToRecord(row)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = db.prepare(`
|
||||
INSERT INTO ${MCU_DEVICES_TABLE} (name, device_code, is_official, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(name, deviceCode, isOfficial, now)
|
||||
|
||||
const id = Number(result.lastInsertRowid)
|
||||
const row = db.prepare(`SELECT * FROM ${MCU_DEVICES_TABLE} WHERE id = ?`).get(id) as unknown as StoredMcuDeviceRow
|
||||
return rowToRecord(row)
|
||||
} catch (error: any) {
|
||||
if (String(error?.message || '').toLowerCase().includes('unique')) {
|
||||
throw new Error('mcu_device_exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function updateMcuDeviceName(id: number, name: string): McuDeviceRecord {
|
||||
const db = getDb()
|
||||
const normalizedName = name.trim().slice(0, 80)
|
||||
|
||||
if (!db) {
|
||||
const rows = jsonGetAll(MCU_DEVICES_TABLE)
|
||||
const row = rows[String(id)]
|
||||
if (!row) throw new Error('mcu_device_not_found')
|
||||
const next = { ...row, name: normalizedName || String(row.device_code || '') }
|
||||
jsonSet(MCU_DEVICES_TABLE, String(id), next as any)
|
||||
return rowToRecord(next)
|
||||
}
|
||||
|
||||
const existing = db.prepare(`SELECT * FROM ${MCU_DEVICES_TABLE} WHERE id = ?`).get(id) as unknown as StoredMcuDeviceRow | undefined
|
||||
if (!existing) throw new Error('mcu_device_not_found')
|
||||
|
||||
db.prepare(`UPDATE ${MCU_DEVICES_TABLE} SET name = ? WHERE id = ?`).run(normalizedName || existing.device_code, id)
|
||||
const row = db.prepare(`SELECT * FROM ${MCU_DEVICES_TABLE} WHERE id = ?`).get(id) as unknown as StoredMcuDeviceRow
|
||||
return rowToRecord(row)
|
||||
}
|
||||
|
||||
export function deleteMcuDevice(id: number): boolean {
|
||||
const db = getDb()
|
||||
if (!db) {
|
||||
const rows = jsonGetAll(MCU_DEVICES_TABLE)
|
||||
if (!rows[String(id)]) return false
|
||||
jsonDelete(MCU_DEVICES_TABLE, String(id))
|
||||
return true
|
||||
}
|
||||
|
||||
const result = db.prepare(`DELETE FROM ${MCU_DEVICES_TABLE} WHERE id = ?`).run(id)
|
||||
return Number(result.changes) > 0
|
||||
}
|
||||
@@ -324,24 +324,6 @@ export const DEVICES_INDEXES = {
|
||||
idx_devices_last_seen: 'CREATE INDEX IF NOT EXISTS idx_devices_last_seen ON devices(last_seen_at)',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCU Devices
|
||||
// ============================================================================
|
||||
|
||||
export const MCU_DEVICES_TABLE = 'mcu_devices'
|
||||
|
||||
export const MCU_DEVICES_SCHEMA: Record<string, string> = {
|
||||
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
||||
name: "TEXT NOT NULL DEFAULT ''",
|
||||
device_code: 'TEXT NOT NULL UNIQUE',
|
||||
is_official: 'INTEGER NOT NULL DEFAULT 0',
|
||||
created_at: `INTEGER NOT NULL DEFAULT (strftime('%s','now'))`,
|
||||
}
|
||||
|
||||
export const MCU_DEVICES_INDEXES = {
|
||||
idx_mcu_devices_created_at: 'CREATE INDEX IF NOT EXISTS idx_mcu_devices_created_at ON mcu_devices(created_at)',
|
||||
}
|
||||
|
||||
export const STT_PROVIDER_SETTINGS_TABLE = 'stt_provider_settings'
|
||||
|
||||
export const STT_PROVIDER_SETTINGS_SCHEMA: Record<string, string> = {
|
||||
@@ -850,11 +832,6 @@ export function initAllHermesTables(): void {
|
||||
indexes: DEVICES_INDEXES,
|
||||
})
|
||||
|
||||
// MCU devices
|
||||
syncTable(MCU_DEVICES_TABLE, MCU_DEVICES_SCHEMA, {
|
||||
indexes: MCU_DEVICES_INDEXES,
|
||||
})
|
||||
|
||||
syncTable(STT_PROVIDER_SETTINGS_TABLE, STT_PROVIDER_SETTINGS_SCHEMA, {
|
||||
indexes: STT_PROVIDER_SETTINGS_INDEXES,
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import { uploadRoutes } from './upload'
|
||||
import { updateRoutes } from './update'
|
||||
import { authPublicRoutes, authProtectedRoutes } from './auth'
|
||||
import { devicePublicRoutes, deviceRoutes } from './devices'
|
||||
import { mcuDeviceRoutes } from './mcu-devices'
|
||||
import { apiDocsRoutes } from './api-docs'
|
||||
|
||||
// Hermes route modules
|
||||
@@ -68,7 +67,6 @@ export function registerRoutes(app: any, authMiddleware: Array<(ctx: Context, ne
|
||||
// --- Protected routes (auth required) ---
|
||||
app.use(authProtectedRoutes.routes())
|
||||
app.use(deviceRoutes.routes())
|
||||
app.use(mcuDeviceRoutes.routes())
|
||||
app.use(uploadRoutes.routes())
|
||||
app.use(updateRoutes.routes()) // Must be before proxy (proxy catch-all matches everything)
|
||||
app.use(sessionRoutes.routes())
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import Router from '@koa/router'
|
||||
import * as ctrl from '../controllers/mcu-devices'
|
||||
|
||||
export const mcuDeviceRoutes = new Router()
|
||||
|
||||
mcuDeviceRoutes.get('/api/mcu-devices', ctrl.listMcuDevicesController)
|
||||
mcuDeviceRoutes.post('/api/mcu-devices', ctrl.createMcuDeviceController)
|
||||
mcuDeviceRoutes.post('/api/mcu-devices/:id/remote-connect', ctrl.connectMcuDeviceRemoteController)
|
||||
mcuDeviceRoutes.post('/api/mcu-devices/:id/remote-disconnect', ctrl.disconnectMcuDeviceRemoteController)
|
||||
mcuDeviceRoutes.patch('/api/mcu-devices/:id', ctrl.updateMcuDeviceController)
|
||||
mcuDeviceRoutes.delete('/api/mcu-devices/:id', ctrl.deleteMcuDeviceController)
|
||||
@@ -475,21 +475,6 @@ export class GlobalAgentServer {
|
||||
return Array.from(this.clients.keys())
|
||||
}
|
||||
|
||||
hasMcuDeviceCode(deviceCode: string): boolean {
|
||||
const expected = deviceCode.trim()
|
||||
if (!expected) return false
|
||||
for (const socket of this.clients.values()) {
|
||||
const auth = socket.handshake.auth || {}
|
||||
const raw = typeof auth.deviceCode === 'string'
|
||||
? auth.deviceCode
|
||||
: typeof auth.device_code === 'string'
|
||||
? auth.device_code
|
||||
: ''
|
||||
if (raw.trim() === expected) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async httpRequest(request: RelayHttpRequest, options: GlobalAgentRequestOptions = {}): Promise<RelayHttpResponse> {
|
||||
const socket = this.resolveClient(options.clientId)
|
||||
if (!socket) {
|
||||
|
||||
Reference in New Issue
Block a user