Files
one_divine_lot/src/api/qmt-connections.js
T

152 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 服务端 APIQMT 连接配置域(从 api.js 拆分,2026-08-31
*
* 端点:
* qmt-connections / add / update / remove / activate / set-default / test
* active-host
*
* 热切换编排:激活/更新/删除连接导致生效地址变化时,同步 dataSource.setBaseUrl + marketFeed.setBaseUrl
* (技术约束-008:按请求读取 baseUrl,改字段即生效)
*
* 2026-08-31:移除 subscribe-ws / unsubscribe-ws / tick-snapshot(架构演进后无人调用,
* 订阅与快照统一由 QuoteSync/QuoteHub 内部处理,不再暴露为 HTTP 端点(R-015 迭代 13
*/
import {
getQmtConnections, getActiveQmtConnection,
addQmtConnection, updateQmtConnection, removeQmtConnection,
activateQmtConnection, setDefaultQmtConnection,
} from '../settings.js';
/** QMT 连接端点方法表 */
export const QMT_METHODS = new Set([
'qmt-connections',
'qmt-connections/add',
'qmt-connections/update',
'qmt-connections/remove',
'qmt-connections/activate',
'qmt-connections/set-default',
'qmt-connections/test',
'qmt-connections/active-host',
'qmt-connections/mcp-status', // R-020/迭代18QMT MCP 状态(probe / 缓存)
]);
/** 服务端代理测试连接:GET {baseUrl}/health(浏览器跨域规避,Q3 手动测试) */
async function testQmtConnection(ctx, args) {
const baseUrl = String(args?.baseUrl ?? '').trim().replace(/\/+$/, '');
if (!/^https?:\/\//i.test(baseUrl)) {
throw Object.assign(new Error('HTTP 地址格式无效(需以 http:// 或 https:// 开头)'), { code: 'qmt-invalid-url' });
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
const startedAt = Date.now();
try {
const res = await fetch(baseUrl + '/health', { signal: controller.signal });
const latencyMs = Date.now() - startedAt;
let status = null;
try { status = (await res.json())?.status ?? null; } catch { /* 非 JSON 响应按 status=null */ }
return { ok: res.ok, latencyMs, httpStatus: res.status, status, reachable: res.ok };
} catch (err) {
return {
ok: false,
latencyMs: Date.now() - startedAt,
reachable: false,
error: err.name === 'AbortError' ? '请求超时(15s' : (err?.message ?? String(err)),
};
} finally {
clearTimeout(timer);
}
}
/**
* 处理 QMT 连接端点
* @param {string} method
* @param {object} args
* @param {object} runtime { manager, settings, dataSource, marketCache }
*/
export async function handleQmt(method, args, { ctx, settings, dataSource, marketFeed, qmtHealthMonitor, mcpManager }) {
switch (method) {
case 'qmt-connections': {
const state = getQmtConnections(settings);
return { ...state, active: getActiveQmtConnection(state) };
}
case 'qmt-connections/add':
return await addQmtConnection(settings, { name: args.name, baseUrl: args.baseUrl });
case 'qmt-connections/update': {
const before = getQmtConnections(settings);
const value = await updateQmtConnection(settings, args.id, { name: args.name, baseUrl: args.baseUrl });
// 编辑的是激活配置且地址变化 → 立即热切换(技术约束-008)
if (before.activeId === args.id && value.baseUrl !== dataSource.baseUrl) {
dataSource.setBaseUrl(value.baseUrl);
marketFeed?.setBaseUrl(value.baseUrl);
qmtHealthMonitor?.probe(); // 切换后立即补探健康
ctx?.logger?.info('[one-divine-lot] 激活配置地址已变更,数据源热切换 → ' + value.baseUrl);
// R-020/迭代18:MCP 目标地址随激活地址变更重挂
mcpManager?.resync(value.baseUrl).catch(() => {});
}
return value;
}
case 'qmt-connections/remove': {
const result = await removeQmtConnection(settings, args.id);
// 生效连接因删除发生变化 → 立即热切换(产品约束-005 删除边界)
const afterState = getQmtConnections(settings);
const active = getActiveQmtConnection(afterState);
if (active && active.baseUrl !== dataSource.baseUrl) {
dataSource.setBaseUrl(active.baseUrl);
marketFeed?.setBaseUrl(active.baseUrl);
qmtHealthMonitor?.probe(); // 切换后立即补探健康
ctx?.logger?.info('[one-divine-lot] 激活配置已删除,自动切换 → ' + active.name + ' (' + active.baseUrl + ')');
}
// R-020/迭代18:激活已变(或列表清空)→ MCP 跟随新激活 / 卸载
mcpManager?.resync(active?.baseUrl).catch(() => {});
return { ...afterState, active, removedWasActive: result.removedWasActive, removedWasDefault: result.removedWasDefault };
}
case 'qmt-connections/activate': {
const state = await activateQmtConnection(settings, args.id);
const active = getActiveQmtConnection(state);
if (active && active.baseUrl !== dataSource.baseUrl) {
dataSource.setBaseUrl(active.baseUrl);
marketFeed?.setBaseUrl(active.baseUrl);
qmtHealthMonitor?.probe(); // 切换后立即补探健康
ctx?.logger?.info('[one-divine-lot] QMT 连接激活: ' + active.name + ' (' + active.baseUrl + ')');
}
// R-020/迭代18:激活切换 → MCP 实例跟随(卸旧挂新,url=新激活 baseUrl+/mcp
mcpManager?.resync(active?.baseUrl).catch(() => {});
return { ...state, active };
}
case 'qmt-connections/set-default':
return await setDefaultQmtConnection(settings, args.id);
case 'qmt-connections/test': {
// 支持传未保存的 baseUrl(表单先测后存),或传 id 用已存配置地址
let url = args.baseUrl;
if (!url && args.id) {
const state = getQmtConnections(settings);
url = state.list.find((c) => c.id === args.id)?.baseUrl;
}
return await testQmtConnection(ctx, { baseUrl: url });
}
case 'qmt-connections/active-host': {
// R-005:返回激活配置的 host 与 ws 地址
const state = getQmtConnections(settings);
const active = getActiveQmtConnection(state) ?? { baseUrl: dataSource.baseUrl, name: '(fallback)' };
const baseUrl = String(active.baseUrl ?? '').replace(/\/+$/, '');
let host = '';
let wsUrl = '';
try {
const u = new URL(baseUrl);
host = u.host; // 含端口,如 100.110.38.78:8610
wsUrl = 'ws://' + u.host + '/ws';
} catch { /* baseUrl 非法时返回空 */ }
return { name: active.name, baseUrl, host, wsUrl, activeId: state.activeId ?? '' };
}
case 'qmt-connections/mcp-status': {
// R-020/迭代18MCP 状态(缓存秒回;refresh=true 即时探测)
if (!mcpManager) return { state: 'disabled', error: 'mcpManager 不可用', checkedAt: 0 };
if (args.refresh) return await mcpManager.probe();
return mcpManager.getStatus();
}
default:
throw Object.assign(new Error('unknown qmt method: ' + method), { code: 'not-found' });
}
}