317 lines
9.4 KiB
JavaScript
317 lines
9.4 KiB
JavaScript
|
|
// exe-engine/src/router/age-router.js
|
|||
|
|
// EXE-Engine · AGE-Router 路由网关
|
|||
|
|
// 接收所有 AI 执行请求,进行鉴权、分类、路由
|
|||
|
|
// PRJ-EXE-001 · Phase 0 · ZY-EXE-P0-001
|
|||
|
|
// 版权:国作登字-2026-A-00037559
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const { randomUUID } = require('crypto');
|
|||
|
|
|
|||
|
|
// HLI 501 存根路由(Phase 0 占位,后续 Phase 实现)
|
|||
|
|
const { personaStubs } = require('./stubs/persona-stub');
|
|||
|
|
const { userStubs } = require('./stubs/user-stub');
|
|||
|
|
const { ticketStubs } = require('./stubs/ticket-stub');
|
|||
|
|
const { dialogueStubs } = require('./stubs/dialogue-stub');
|
|||
|
|
const { storageStubs } = require('./stubs/storage-stub');
|
|||
|
|
const { dashboardStubs } = require('./stubs/dashboard-stub');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* AGE-Router 路由网关
|
|||
|
|
*
|
|||
|
|
* 本体论锚定:路由网关 = 笔的分流阀。
|
|||
|
|
* 所有墨水需求都从这里进入,网关决定用哪种墨水、多少量。
|
|||
|
|
*
|
|||
|
|
* 职责:
|
|||
|
|
* 1. 请求鉴权(验证用户身份 + 资源池额度)
|
|||
|
|
* 2. 任务分类(代码生成 / 文本处理 / 数据分析 / Agent 指令)
|
|||
|
|
* 3. 模型选择(通过 LoadBalancer 选择最优模型)
|
|||
|
|
* 4. 限流降级(资源池耗尽时的优雅降级策略)
|
|||
|
|
*/
|
|||
|
|
class AGERouter {
|
|||
|
|
/**
|
|||
|
|
* @param {object} deps
|
|||
|
|
* @param {LoadBalancer} deps.loadBalancer 负载均衡器
|
|||
|
|
* @param {ResourceMeter} deps.resourceMeter 资源计量器
|
|||
|
|
* @param {ContextCache} deps.contextCache 上下文缓存
|
|||
|
|
* @param {AgentController} deps.agentController Agent 调度器
|
|||
|
|
* @param {object} [deps.poolConfig] 资源池配置
|
|||
|
|
*/
|
|||
|
|
constructor(deps) {
|
|||
|
|
this._loadBalancer = deps.loadBalancer;
|
|||
|
|
this._resourceMeter = deps.resourceMeter;
|
|||
|
|
this._contextCache = deps.contextCache;
|
|||
|
|
this._agentController = deps.agentController;
|
|||
|
|
this._poolConfig = deps.poolConfig || {};
|
|||
|
|
|
|||
|
|
// 限流计数器 { agentId: { count, resetAt } }
|
|||
|
|
this._rateLimits = new Map();
|
|||
|
|
|
|||
|
|
// HLI 存根路由注册表(天眼路由扫描识别用)
|
|||
|
|
this._hliStubs = [
|
|||
|
|
...personaStubs,
|
|||
|
|
...userStubs,
|
|||
|
|
...ticketStubs,
|
|||
|
|
...dialogueStubs,
|
|||
|
|
...storageStubs,
|
|||
|
|
...dashboardStubs
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行 AI 请求(统一入口)
|
|||
|
|
*
|
|||
|
|
* 对应 API: POST /api/exe/v1/execute
|
|||
|
|
*
|
|||
|
|
* @param {object} request
|
|||
|
|
* @param {string} request.agentId Agent ID
|
|||
|
|
* @param {string} request.taskType 任务类型
|
|||
|
|
* @param {string} request.prompt Prompt
|
|||
|
|
* @param {string} [request.systemPrompt] 系统 Prompt
|
|||
|
|
* @param {object} [request.context] 上下文
|
|||
|
|
* @param {object} [request.preferences] 偏好 { model, priority, maxTokens }
|
|||
|
|
* @param {string} [request.resourcePool] 资源池 ID
|
|||
|
|
* @param {string} [request.apiKey] BYOK 模式的用户 Key
|
|||
|
|
* @returns {Promise<object>} EXE 标准响应
|
|||
|
|
*/
|
|||
|
|
async execute(request) {
|
|||
|
|
const requestId = `exe-${randomUUID().slice(0, 12)}`;
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 1. 限流检查
|
|||
|
|
this._checkRateLimit(request.agentId, request.resourcePool);
|
|||
|
|
|
|||
|
|
// 2. 尝试加载缓存上下文
|
|||
|
|
const cacheKey = `${request.agentId}:${request.taskType}`;
|
|||
|
|
const cachedContext = this._contextCache.get(cacheKey);
|
|||
|
|
if (cachedContext && !request.context) {
|
|||
|
|
request.context = cachedContext;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 通过 LoadBalancer 选择适配器
|
|||
|
|
const adapter = this._loadBalancer.select({
|
|||
|
|
taskType: request.taskType,
|
|||
|
|
model: request.preferences?.model || 'auto',
|
|||
|
|
priority: request.preferences?.priority || 'balanced'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!adapter) {
|
|||
|
|
return this._buildErrorResponse(requestId, startTime, 'NO_AVAILABLE_MODEL',
|
|||
|
|
'没有可用的模型适配器,所有模型可能处于冷却期');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 执行推理
|
|||
|
|
const result = await adapter.execute({
|
|||
|
|
prompt: request.prompt,
|
|||
|
|
systemPrompt: request.systemPrompt,
|
|||
|
|
taskType: request.taskType,
|
|||
|
|
maxTokens: request.preferences?.maxTokens || 4096,
|
|||
|
|
temperature: request.preferences?.temperature
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 5. 记录计量数据
|
|||
|
|
const mode = request.apiKey ? 'byok' : 'pool';
|
|||
|
|
this._resourceMeter.record({
|
|||
|
|
requestId,
|
|||
|
|
agentId: request.agentId,
|
|||
|
|
model: result.model,
|
|||
|
|
taskType: request.taskType,
|
|||
|
|
resourcePool: request.resourcePool || 'default',
|
|||
|
|
mode,
|
|||
|
|
inputTokens: result.usage.inputTokens,
|
|||
|
|
outputTokens: result.usage.outputTokens,
|
|||
|
|
cost: result.usage.cost,
|
|||
|
|
costUnit: result.usage.unit,
|
|||
|
|
latency: result.latency,
|
|||
|
|
status: 'success'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 6. 更新上下文缓存
|
|||
|
|
if (request.context) {
|
|||
|
|
this._contextCache.set(cacheKey, request.context);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 7. 返回标准响应
|
|||
|
|
return {
|
|||
|
|
requestId,
|
|||
|
|
model: result.model,
|
|||
|
|
output: result.output,
|
|||
|
|
usage: result.usage,
|
|||
|
|
latency: result.latency,
|
|||
|
|
status: 'success'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} catch (err) {
|
|||
|
|
// 尝试故障转移
|
|||
|
|
const failoverResult = await this._tryFailover(request, requestId, startTime, err);
|
|||
|
|
if (failoverResult) return failoverResult;
|
|||
|
|
|
|||
|
|
// 记录失败
|
|||
|
|
this._resourceMeter.record({
|
|||
|
|
requestId,
|
|||
|
|
agentId: request.agentId,
|
|||
|
|
model: 'unknown',
|
|||
|
|
taskType: request.taskType,
|
|||
|
|
resourcePool: request.resourcePool || 'default',
|
|||
|
|
mode: request.apiKey ? 'byok' : 'pool',
|
|||
|
|
inputTokens: 0,
|
|||
|
|
outputTokens: 0,
|
|||
|
|
cost: 0,
|
|||
|
|
costUnit: 'CNY',
|
|||
|
|
latency: Date.now() - startTime,
|
|||
|
|
status: 'error'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return this._buildErrorResponse(requestId, startTime, 'EXECUTION_FAILED', err.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取系统状态
|
|||
|
|
* @returns {object}
|
|||
|
|
*/
|
|||
|
|
getStatus() {
|
|||
|
|
return {
|
|||
|
|
router: 'AGE-Router v1.0.0',
|
|||
|
|
project: 'PRJ-EXE-001',
|
|||
|
|
phase: 'Phase 0',
|
|||
|
|
adapters: this._loadBalancer.getStatus(),
|
|||
|
|
meter: this._resourceMeter.getSummary(),
|
|||
|
|
cache: this._contextCache.getStatus(),
|
|||
|
|
agents: this._agentController.getRegisteredAgents(),
|
|||
|
|
hliStubs: this._hliStubs.map(s => ({
|
|||
|
|
routeId: s.routeId,
|
|||
|
|
path: s.path,
|
|||
|
|
phase: s.phase,
|
|||
|
|
status: 'stub_501'
|
|||
|
|
}))
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理 HLI 存根路由请求
|
|||
|
|
* @param {string} routePath 路由路径(如 /hli/persona/load)
|
|||
|
|
* @returns {object|null} 501 响应,不匹配返回 null
|
|||
|
|
*/
|
|||
|
|
async handleStubRoute(routePath) {
|
|||
|
|
const stub = this._hliStubs.find(s => s.path === routePath);
|
|||
|
|
if (!stub) return null;
|
|||
|
|
return stub.handler({});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有已注册的 HLI 存根路由
|
|||
|
|
* @returns {Array}
|
|||
|
|
*/
|
|||
|
|
getHLIStubs() {
|
|||
|
|
return this._hliStubs;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 内部方法 ──
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 限流检查
|
|||
|
|
* @param {string} agentId
|
|||
|
|
* @param {string} poolId
|
|||
|
|
*/
|
|||
|
|
_checkRateLimit(agentId, poolId) {
|
|||
|
|
const key = agentId || 'anonymous';
|
|||
|
|
const now = Date.now();
|
|||
|
|
let limiter = this._rateLimits.get(key);
|
|||
|
|
|
|||
|
|
if (!limiter || now >= limiter.resetAt) {
|
|||
|
|
limiter = { count: 0, resetAt: now + 60000 }; // 每分钟窗口
|
|||
|
|
this._rateLimits.set(key, limiter);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
limiter.count++;
|
|||
|
|
|
|||
|
|
// 获取资源池的速率限制
|
|||
|
|
const pool = this._poolConfig.pools?.[poolId || 'default'];
|
|||
|
|
const limit = pool?.rateLimitPerMinute || 30;
|
|||
|
|
|
|||
|
|
if (limiter.count > limit) {
|
|||
|
|
throw new Error(`[限流] Agent ${key} 每分钟请求数超限 (${limiter.count}/${limit})`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 故障转移
|
|||
|
|
* @param {object} request
|
|||
|
|
* @param {string} requestId
|
|||
|
|
* @param {number} startTime
|
|||
|
|
* @param {Error} originalError
|
|||
|
|
* @returns {Promise<object|null>}
|
|||
|
|
*/
|
|||
|
|
async _tryFailover(request, requestId, startTime, originalError) {
|
|||
|
|
try {
|
|||
|
|
const failoverAdapter = this._loadBalancer.failover(
|
|||
|
|
request.preferences?.model || 'unknown',
|
|||
|
|
request.taskType
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!failoverAdapter) return null;
|
|||
|
|
|
|||
|
|
const result = await failoverAdapter.execute({
|
|||
|
|
prompt: request.prompt,
|
|||
|
|
systemPrompt: request.systemPrompt,
|
|||
|
|
taskType: request.taskType,
|
|||
|
|
maxTokens: request.preferences?.maxTokens || 4096
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this._resourceMeter.record({
|
|||
|
|
requestId,
|
|||
|
|
agentId: request.agentId,
|
|||
|
|
model: result.model,
|
|||
|
|
taskType: request.taskType,
|
|||
|
|
resourcePool: request.resourcePool || 'default',
|
|||
|
|
mode: request.apiKey ? 'byok' : 'pool',
|
|||
|
|
inputTokens: result.usage.inputTokens,
|
|||
|
|
outputTokens: result.usage.outputTokens,
|
|||
|
|
cost: result.usage.cost,
|
|||
|
|
costUnit: result.usage.unit,
|
|||
|
|
latency: result.latency,
|
|||
|
|
status: 'success'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
requestId,
|
|||
|
|
model: result.model,
|
|||
|
|
output: result.output,
|
|||
|
|
usage: result.usage,
|
|||
|
|
latency: result.latency,
|
|||
|
|
status: 'success',
|
|||
|
|
_failover: true,
|
|||
|
|
_originalError: originalError.message
|
|||
|
|
};
|
|||
|
|
} catch (failoverErr) {
|
|||
|
|
// Failover also failed - return null to trigger error response
|
|||
|
|
this._lastFailoverError = failoverErr.message;
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构建错误响应
|
|||
|
|
* @param {string} requestId
|
|||
|
|
* @param {number} startTime
|
|||
|
|
* @param {string} code
|
|||
|
|
* @param {string} message
|
|||
|
|
* @returns {object}
|
|||
|
|
*/
|
|||
|
|
_buildErrorResponse(requestId, startTime, code, message) {
|
|||
|
|
return {
|
|||
|
|
requestId,
|
|||
|
|
model: null,
|
|||
|
|
output: null,
|
|||
|
|
usage: { inputTokens: 0, outputTokens: 0, cost: 0, unit: 'CNY' },
|
|||
|
|
latency: Date.now() - startTime,
|
|||
|
|
status: 'error',
|
|||
|
|
error: { code, message }
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = AGERouter;
|