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:
2026-10-19 01:32:51 +08:00
parent 3c963bb1d9
commit 77b1858163
22 changed files with 49 additions and 1405 deletions
@@ -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
}
-23
View File
@@ -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,
})
-2
View File
@@ -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())
-11
View File
@@ -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) {