feat(exe-engine): implement Phase 1 — context, session, multi-model, scheduler, monitor, HLI routes
ZY-EXE-P1-001: ContextManager with Grid-DB persistence stub ZY-EXE-P1-002: Session management for multi-turn dialogues ZY-EXE-P1-003: MultiModelExecutor with fastest/consensus/fallback strategies ZY-EXE-P1-004: QwenAdapter upgrade — executeStream + enhanced _resolveModel ZY-EXE-P1-005: SchedulerV2 with priority queue and dependency graph ZY-EXE-P1-006: MonitorDashboard for real-time metrics ZY-EXE-P1-007: HLI Persona routes (load/switch) — 501→200 ZY-EXE-P1-008: HLI Dialogue routes (send/stream/history) — 501→200 ZY-EXE-P1-009: HLI User routes (profile/update) — 501→200 All P0 tests pass (78/78), P1 tests pass (92/92). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
0060a33d69
commit
c9d18ea0ea
|
|
@ -118,13 +118,45 @@ class QwenAdapter extends BaseAdapter {
|
|||
return this._healthy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式执行推理请求
|
||||
*
|
||||
* Phase 1 占位:内部仍调用非流式接口,将完整结果作为单个 chunk 返回。
|
||||
* Phase 2 将对接真正的 SSE 流式传输。
|
||||
*
|
||||
* @param {object} request
|
||||
* @param {string} request.prompt 用户 prompt
|
||||
* @param {string} [request.systemPrompt] 系统 prompt
|
||||
* @param {string} [request.taskType] 任务类型
|
||||
* @param {number} [request.maxTokens] 最大输出 token
|
||||
* @param {number} [request.temperature] 温度
|
||||
* @returns {AsyncGenerator<object>} 异步生成器,yield chunk 对象
|
||||
*/
|
||||
async *executeStream(request) {
|
||||
// Phase 1:回退到非流式执行,yield 完整结果作为单个 chunk
|
||||
const result = await this.execute(request);
|
||||
yield {
|
||||
type: 'chunk',
|
||||
content: result.output,
|
||||
model: result.model,
|
||||
usage: result.usage,
|
||||
finishReason: result.finishReason || 'stop',
|
||||
done: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务类型解析具体模型名
|
||||
* - code_generation → qwen-coder-plus-latest(代码专用模型)
|
||||
* - text / text_processing → qwen-max-latest(文本最新版本)
|
||||
* - reasoning / 默认 → qwen-max(通用稳定版本)
|
||||
* @param {string} taskType
|
||||
* @returns {string}
|
||||
*/
|
||||
_resolveModel(taskType) {
|
||||
if (taskType === 'code_generation') return 'qwen-coder-plus-latest';
|
||||
if (taskType === 'reasoning') return 'qwen-max';
|
||||
if (taskType === 'text' || taskType === 'text_processing') return 'qwen-max-latest';
|
||||
return 'qwen-max';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
// exe-engine/src/context/context-manager.js
|
||||
// EXE-Engine · 上下文管理器
|
||||
// 支持持久化的升级上下文系统(Grid-DB 接口预留)
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-001
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
/**
|
||||
* 上下文管理器
|
||||
*
|
||||
* 本体论锚定:上下文 = 笔的记忆链。
|
||||
* 每次对话都有连续的记忆,管理器负责维护这条记忆链。
|
||||
* Phase 1 使用内存 + ContextCache,Phase 2 对接 Grid-DB 持久化。
|
||||
*/
|
||||
class ContextManager {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {ContextCache} deps.cache 上下文缓存实例
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._cache = deps.cache || null;
|
||||
this._contexts = new Map();
|
||||
// Grid-DB 持久化接口预留
|
||||
this._gridDB = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新上下文
|
||||
* @param {string} agentId Agent ID
|
||||
* @param {string} sessionId 会话 ID
|
||||
* @param {object} [initialData] 初始数据
|
||||
* @returns {string} contextId
|
||||
*/
|
||||
createContext(agentId, sessionId, initialData = {}) {
|
||||
const contextId = `ctx-${randomUUID().slice(0, 12)}`;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const context = {
|
||||
contextId,
|
||||
agentId,
|
||||
sessionId,
|
||||
data: { ...initialData },
|
||||
messages: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this._contexts.set(contextId, context);
|
||||
|
||||
// 同步到缓存
|
||||
if (this._cache) {
|
||||
this._cache.set(contextId, context);
|
||||
}
|
||||
|
||||
return contextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文
|
||||
* @param {string} contextId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getContext(contextId) {
|
||||
// 先查内存
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (ctx) return ctx;
|
||||
|
||||
// 再查缓存
|
||||
if (this._cache) {
|
||||
const cached = this._cache.get(contextId);
|
||||
if (cached) {
|
||||
this._contexts.set(contextId, cached);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新上下文数据
|
||||
* @param {string} contextId
|
||||
* @param {object} updates 要合并的更新
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
updateContext(contextId, updates) {
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (!ctx) return false;
|
||||
|
||||
Object.assign(ctx.data, updates);
|
||||
ctx.updatedAt = new Date().toISOString();
|
||||
|
||||
if (this._cache) {
|
||||
this._cache.set(contextId, ctx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记上下文为过期
|
||||
* @param {string} contextId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
expireContext(contextId) {
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (!ctx) return false;
|
||||
|
||||
ctx.status = 'expired';
|
||||
ctx.updatedAt = new Date().toISOString();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定 Agent 的所有上下文
|
||||
* @param {string} agentId
|
||||
* @returns {object[]}
|
||||
*/
|
||||
listContexts(agentId) {
|
||||
const results = [];
|
||||
for (const [, ctx] of this._contexts) {
|
||||
if (ctx.agentId === agentId) {
|
||||
results.push(ctx);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上下文追加消息
|
||||
* @param {string} contextId
|
||||
* @param {string} role 角色 (user | assistant | system)
|
||||
* @param {string} content 消息内容
|
||||
* @returns {boolean}
|
||||
*/
|
||||
addMessage(contextId, role, content) {
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (!ctx) return false;
|
||||
|
||||
ctx.messages.push({
|
||||
role,
|
||||
content,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
ctx.updatedAt = new Date().toISOString();
|
||||
|
||||
if (this._cache) {
|
||||
this._cache.set(contextId, ctx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Grid-DB 持久化实例(P2 对接预留)
|
||||
* @param {object} gridDBInstance
|
||||
*/
|
||||
setGridDBPersistence(gridDBInstance) {
|
||||
this._gridDB = gridDBInstance;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContextManager;
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// exe-engine/src/context/session.js
|
||||
// EXE-Engine · 会话管理
|
||||
// 多轮对话的会话生命周期管理
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-002
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
/**
|
||||
* 会话管理器
|
||||
*
|
||||
* 本体论锚定:会话 = 笔与纸之间的一段连续书写。
|
||||
* 每段书写有开始和结束,中间的每一笔都被记录。
|
||||
*/
|
||||
class Session {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {ContextManager} deps.contextManager 上下文管理器
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._contextManager = deps.contextManager || null;
|
||||
this._sessions = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
* @param {string} agentId Agent ID
|
||||
* @param {object} [metadata] 会话元数据
|
||||
* @returns {{ sessionId: string, contextId: string }}
|
||||
*/
|
||||
createSession(agentId, metadata = {}) {
|
||||
const sessionId = `sess-${randomUUID().slice(0, 12)}`;
|
||||
let contextId = null;
|
||||
|
||||
// 如果有上下文管理器,同步创建上下文
|
||||
if (this._contextManager) {
|
||||
contextId = this._contextManager.createContext(agentId, sessionId);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const session = {
|
||||
sessionId,
|
||||
agentId,
|
||||
contextId,
|
||||
turns: [],
|
||||
metadata: { ...metadata },
|
||||
createdAt: now,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this._sessions.set(sessionId, session);
|
||||
|
||||
return { sessionId, contextId };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话
|
||||
* @param {string} sessionId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getSession(sessionId) {
|
||||
return this._sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加对话轮次
|
||||
* @param {string} sessionId
|
||||
* @param {string} userMessage 用户消息
|
||||
* @param {string} assistantMessage 助手回复
|
||||
* @returns {boolean}
|
||||
*/
|
||||
addTurn(sessionId, userMessage, assistantMessage) {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session || session.status !== 'active') return false;
|
||||
|
||||
const turn = {
|
||||
turnId: session.turns.length + 1,
|
||||
user: userMessage,
|
||||
assistant: assistantMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
session.turns.push(turn);
|
||||
|
||||
// 同步到上下文管理器
|
||||
if (this._contextManager && session.contextId) {
|
||||
this._contextManager.addMessage(session.contextId, 'user', userMessage);
|
||||
this._contextManager.addMessage(session.contextId, 'assistant', assistantMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话历史
|
||||
* @param {string} sessionId
|
||||
* @param {number} [limit] 返回最近 N 轮
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getHistory(sessionId, limit) {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session) return [];
|
||||
|
||||
if (limit && limit > 0) {
|
||||
return session.turns.slice(-limit);
|
||||
}
|
||||
return [...session.turns];
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
* @param {string} sessionId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
endSession(sessionId) {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session) return false;
|
||||
|
||||
session.status = 'ended';
|
||||
|
||||
// 同步过期上下文
|
||||
if (this._contextManager && session.contextId) {
|
||||
this._contextManager.expireContext(session.contextId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定 Agent 的所有会话
|
||||
* @param {string} agentId
|
||||
* @returns {object[]}
|
||||
*/
|
||||
listSessions(agentId) {
|
||||
const results = [];
|
||||
for (const [, session] of this._sessions) {
|
||||
if (session.agentId === agentId) {
|
||||
results.push(session);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Session;
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
// exe-engine/src/controller/scheduler-v2.js
|
||||
// EXE-Engine · 调度器 v2
|
||||
// 优先队列 + 依赖图的增强调度
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-005
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
// 优先级数值映射:数值越小优先级越高(high=0 最先出队)
|
||||
const PRIORITY_MAP = { high: 0, normal: 1, low: 2 };
|
||||
|
||||
/**
|
||||
* 调度器 v2
|
||||
*
|
||||
* 本体论锚定:调度器 = 笔的时间管理器。
|
||||
* 哪些字先写、哪些字后写、哪些字要等前面的字写完才能写,
|
||||
* 调度器用优先队列和依赖图来决定。
|
||||
*/
|
||||
class SchedulerV2 {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {AgentController} deps.agentController Agent 调度器
|
||||
* @param {number} [deps.maxConcurrency=3] 最大并发数
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._agentController = deps.agentController || null;
|
||||
this._maxConcurrency = deps.maxConcurrency || 3;
|
||||
|
||||
// 待处理队列
|
||||
this._queue = [];
|
||||
// 已完成任务 { taskId: result }
|
||||
this._completed = new Map();
|
||||
// 失败任务 { taskId: error }
|
||||
this._failed = new Map();
|
||||
// 正在执行的任务
|
||||
this._running = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* 入队任务
|
||||
* @param {object} task
|
||||
* @param {string} [task.taskId] 任务 ID(自动生成)
|
||||
* @param {string} task.agentId Agent ID
|
||||
* @param {string} [task.priority='normal'] 优先级
|
||||
* @param {string[]} [task.dependsOn=[]] 依赖任务 ID 列表
|
||||
* @param {object} task.payload 任务载荷
|
||||
* @returns {string} taskId
|
||||
*/
|
||||
enqueue(task) {
|
||||
const taskId = task.taskId || `task-${randomUUID().slice(0, 8)}`;
|
||||
|
||||
const entry = {
|
||||
taskId,
|
||||
agentId: task.agentId,
|
||||
priority: task.priority || 'normal',
|
||||
dependsOn: task.dependsOn || [],
|
||||
payload: task.payload || {},
|
||||
status: 'pending',
|
||||
enqueuedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this._queue.push(entry);
|
||||
|
||||
// 按优先级排序(同优先级保持 FIFO)
|
||||
this._queue.sort((a, b) => {
|
||||
const pa = PRIORITY_MAP[a.priority] ?? 1;
|
||||
const pb = PRIORITY_MAP[b.priority] ?? 1;
|
||||
return pa - pb;
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出队:返回优先级最高且依赖已满足的任务
|
||||
* @returns {object|null}
|
||||
*/
|
||||
dequeue() {
|
||||
for (let i = 0; i < this._queue.length; i++) {
|
||||
const task = this._queue[i];
|
||||
if (this._areDependenciesMet(task)) {
|
||||
this._queue.splice(i, 1);
|
||||
task.status = 'running';
|
||||
this._running.add(task.taskId);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务完成
|
||||
* @param {string} taskId
|
||||
* @param {*} result 执行结果
|
||||
*/
|
||||
complete(taskId, result) {
|
||||
this._completed.set(taskId, {
|
||||
result,
|
||||
completedAt: new Date().toISOString()
|
||||
});
|
||||
this._running.delete(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务失败
|
||||
* @param {string} taskId
|
||||
* @param {string} error 错误信息
|
||||
*/
|
||||
fail(taskId, error) {
|
||||
this._failed.set(taskId, {
|
||||
error,
|
||||
failedAt: new Date().toISOString()
|
||||
});
|
||||
this._running.delete(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前队列状态
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getQueue() {
|
||||
return [...this._queue];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调度器指标
|
||||
* @returns {object}
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
pending: this._queue.length,
|
||||
running: this._running.size,
|
||||
completed: this._completed.size,
|
||||
failed: this._failed.size,
|
||||
maxConcurrency: this._maxConcurrency
|
||||
};
|
||||
}
|
||||
|
||||
// ── 内部方法 ──
|
||||
|
||||
/**
|
||||
* 检查任务依赖是否全部满足
|
||||
* @param {object} task
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_areDependenciesMet(task) {
|
||||
if (!task.dependsOn || task.dependsOn.length === 0) return true;
|
||||
return task.dependsOn.every(depId => this._completed.has(depId));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SchedulerV2;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
// exe-engine/src/executor/multi-model.js
|
||||
// EXE-Engine · 多模型执行器
|
||||
// 支持 fastest / consensus / fallback 多策略执行
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-003
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
// consensus 策略:输出长度差异在此比例内视为一致
|
||||
const CONSENSUS_LENGTH_THRESHOLD = 0.2;
|
||||
|
||||
/**
|
||||
* 多模型执行器
|
||||
*
|
||||
* 本体论锚定:多模型 = 同时用多瓶墨水写字。
|
||||
* fastest 赛跑取最快,consensus 共识取多数,fallback 逐瓶尝试。
|
||||
*/
|
||||
class MultiModelExecutor {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {Map<string, BaseAdapter>} deps.adapters 适配器 Map
|
||||
* @param {LoadBalancer} deps.loadBalancer 负载均衡器
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._adapters = deps.adapters || new Map();
|
||||
this._loadBalancer = deps.loadBalancer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行多模型策略
|
||||
* @param {object} request EXE 标准请求
|
||||
* @param {string} [strategy='fastest'] 执行策略
|
||||
* @returns {Promise<object>} EXE 标准响应 + _strategy + _modelsUsed
|
||||
*/
|
||||
async execute(request, strategy = 'fastest') {
|
||||
const requestId = `exe-mm-${randomUUID().slice(0, 8)}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
const availableAdapters = this._getHealthyAdapters();
|
||||
if (availableAdapters.length === 0) {
|
||||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||||
'NO_AVAILABLE_MODEL', '没有可用的模型适配器');
|
||||
}
|
||||
|
||||
try {
|
||||
switch (strategy) {
|
||||
case 'fastest':
|
||||
return await this._executeFastest(request, availableAdapters, requestId, startTime);
|
||||
case 'consensus':
|
||||
return await this._executeConsensus(request, availableAdapters, requestId, startTime);
|
||||
case 'fallback':
|
||||
return await this._executeFallback(request, availableAdapters, requestId, startTime);
|
||||
default:
|
||||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||||
'INVALID_STRATEGY', `未知策略: ${strategy}`);
|
||||
}
|
||||
} catch (err) {
|
||||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||||
'EXECUTION_FAILED', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用模型名列表
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getAvailableModels() {
|
||||
const models = [];
|
||||
for (const [name, adapter] of this._adapters) {
|
||||
if (!adapter.isInCooldown()) {
|
||||
models.push(name);
|
||||
}
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
// ── 策略实现 ──
|
||||
|
||||
/**
|
||||
* fastest 策略:赛跑,返回最快结果
|
||||
*/
|
||||
async _executeFastest(request, adapters, requestId, startTime) {
|
||||
const promises = adapters.map(adapter =>
|
||||
adapter.execute(request).then(result => ({ adapter, result }))
|
||||
);
|
||||
|
||||
const { adapter, result } = await Promise.any(promises);
|
||||
|
||||
return {
|
||||
requestId,
|
||||
model: result.model || adapter.name,
|
||||
output: result.output,
|
||||
usage: result.usage,
|
||||
latency: Date.now() - startTime,
|
||||
status: 'success',
|
||||
_strategy: 'fastest',
|
||||
_modelsUsed: [adapter.name]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* consensus 策略:全部执行,多数同意则返回
|
||||
* 简化判断:输出长度差异在 ±20% 内视为一致
|
||||
*/
|
||||
async _executeConsensus(request, adapters, requestId, startTime) {
|
||||
const results = await Promise.allSettled(
|
||||
adapters.map(adapter =>
|
||||
adapter.execute(request).then(result => ({ adapter, result }))
|
||||
)
|
||||
);
|
||||
|
||||
const successful = results
|
||||
.filter(r => r.status === 'fulfilled')
|
||||
.map(r => r.value);
|
||||
|
||||
if (successful.length === 0) {
|
||||
return this._buildErrorResponse(requestId, startTime, 'consensus',
|
||||
'ALL_MODELS_FAILED', '所有模型执行失败');
|
||||
}
|
||||
|
||||
// 用第一个成功结果作为基准
|
||||
const baseline = successful[0];
|
||||
const baseLen = (baseline.result.output || '').length || 1;
|
||||
let agreements = 0;
|
||||
|
||||
for (const s of successful) {
|
||||
const len = (s.result.output || '').length;
|
||||
if (Math.abs(len - baseLen) / baseLen <= CONSENSUS_LENGTH_THRESHOLD) {
|
||||
agreements++;
|
||||
}
|
||||
}
|
||||
|
||||
const consensus = agreements > successful.length / 2;
|
||||
const chosen = baseline;
|
||||
|
||||
return {
|
||||
requestId,
|
||||
model: chosen.result.model || chosen.adapter.name,
|
||||
output: chosen.result.output,
|
||||
usage: chosen.result.usage,
|
||||
latency: Date.now() - startTime,
|
||||
status: 'success',
|
||||
_strategy: 'consensus',
|
||||
_consensus: consensus,
|
||||
_modelsUsed: successful.map(s => s.adapter.name)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* fallback 策略:按优先级逐个尝试,返回第一个成功
|
||||
*/
|
||||
async _executeFallback(request, adapters, requestId, startTime) {
|
||||
const errors = [];
|
||||
|
||||
for (const adapter of adapters) {
|
||||
try {
|
||||
const result = await adapter.execute(request);
|
||||
return {
|
||||
requestId,
|
||||
model: result.model || adapter.name,
|
||||
output: result.output,
|
||||
usage: result.usage,
|
||||
latency: Date.now() - startTime,
|
||||
status: 'success',
|
||||
_strategy: 'fallback',
|
||||
_modelsUsed: [adapter.name],
|
||||
_attemptsCount: errors.length + 1
|
||||
};
|
||||
} catch (err) {
|
||||
errors.push({ model: adapter.name, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
return this._buildErrorResponse(requestId, startTime, 'fallback',
|
||||
'ALL_MODELS_FAILED', `所有模型执行失败: ${errors.map(e => e.model).join(', ')}`);
|
||||
}
|
||||
|
||||
// ── 辅助方法 ──
|
||||
|
||||
_getHealthyAdapters() {
|
||||
const healthy = [];
|
||||
for (const [, adapter] of this._adapters) {
|
||||
if (!adapter.isInCooldown()) {
|
||||
healthy.push(adapter);
|
||||
}
|
||||
}
|
||||
return healthy;
|
||||
}
|
||||
|
||||
_buildErrorResponse(requestId, startTime, strategy, 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 },
|
||||
_strategy: strategy,
|
||||
_modelsUsed: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MultiModelExecutor;
|
||||
|
|
@ -17,6 +17,13 @@ const BaseAdapter = require('./adapters/base-adapter');
|
|||
const DeepSeekAdapter = require('./adapters/deepseek-adapter');
|
||||
const QwenAdapter = require('./adapters/qwen-adapter');
|
||||
|
||||
// Phase 1 新增模块
|
||||
const ContextManager = require('./context/context-manager');
|
||||
const Session = require('./context/session');
|
||||
const MultiModelExecutor = require('./executor/multi-model');
|
||||
const SchedulerV2 = require('./controller/scheduler-v2');
|
||||
const MonitorDashboard = require('./monitor/dashboard');
|
||||
|
||||
const CONFIG_DIR = path.resolve(__dirname, '../config');
|
||||
|
||||
/**
|
||||
|
|
@ -157,6 +164,13 @@ function createEngine(overrides = {}) {
|
|||
poolConfig
|
||||
});
|
||||
|
||||
// 7. Phase 1 组件
|
||||
const contextManager = new ContextManager({ cache });
|
||||
const session = new Session({ contextManager });
|
||||
const multiModelExecutor = new MultiModelExecutor({ adapters, loadBalancer: balancer });
|
||||
const scheduler = new SchedulerV2({ agentController: controller });
|
||||
const monitor = new MonitorDashboard({ resourceMeter: meter });
|
||||
|
||||
return {
|
||||
router,
|
||||
meter,
|
||||
|
|
@ -164,6 +178,11 @@ function createEngine(overrides = {}) {
|
|||
controller,
|
||||
balancer,
|
||||
adapters,
|
||||
contextManager,
|
||||
session,
|
||||
multiModelExecutor,
|
||||
scheduler,
|
||||
monitor,
|
||||
config: { modelConfig, poolConfig }
|
||||
};
|
||||
}
|
||||
|
|
@ -209,5 +228,10 @@ module.exports = {
|
|||
AgentController,
|
||||
BaseAdapter,
|
||||
DeepSeekAdapter,
|
||||
QwenAdapter
|
||||
QwenAdapter,
|
||||
ContextManager,
|
||||
Session,
|
||||
MultiModelExecutor,
|
||||
SchedulerV2,
|
||||
MonitorDashboard
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
// exe-engine/src/monitor/dashboard.js
|
||||
// EXE-Engine · 监控仪表盘
|
||||
// 实时指标采集与快照
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-006
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 监控仪表盘
|
||||
*
|
||||
* 本体论锚定:仪表盘 = 笔的健康面板。
|
||||
* 每秒写了多少字、响应多快、用了多少墨水、出了几次错,
|
||||
* 一切尽在仪表盘。
|
||||
*/
|
||||
class MonitorDashboard {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {ResourceMeter} deps.resourceMeter 资源计量器
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._resourceMeter = deps.resourceMeter || null;
|
||||
this._metrics = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一个指标数据点
|
||||
* @param {string} name 指标名称
|
||||
* @param {number} value 指标值
|
||||
* @param {object} [tags] 标签
|
||||
*/
|
||||
recordMetric(name, value, tags = {}) {
|
||||
this._metrics.push({
|
||||
name,
|
||||
value,
|
||||
tags,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间范围内的指标
|
||||
* @param {string} name 指标名称
|
||||
* @param {number} [timeRangeMs=60000] 时间范围(毫秒)
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getMetrics(name, timeRangeMs = 60000) {
|
||||
const cutoff = Date.now() - timeRangeMs;
|
||||
return this._metrics.filter(m =>
|
||||
m.name === name && m.timestamp >= cutoff
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统快照
|
||||
* @returns {object}
|
||||
*/
|
||||
getSnapshot() {
|
||||
const now = Date.now();
|
||||
const oneMinuteAgo = now - 60000;
|
||||
|
||||
// 最近一分钟的请求计数
|
||||
const recentRequests = this._metrics.filter(m =>
|
||||
m.name === 'request_count' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 最近一分钟的延迟
|
||||
const recentLatency = this._metrics.filter(m =>
|
||||
m.name === 'latency_ms' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 最近一分钟的 token 消耗
|
||||
const recentTokens = this._metrics.filter(m =>
|
||||
m.name === 'tokens_used' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 最近一分钟的错误
|
||||
const recentErrors = this._metrics.filter(m =>
|
||||
m.name === 'error_count' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 计算 QPS
|
||||
const qps = recentRequests.length > 0
|
||||
? recentRequests.reduce((sum, m) => sum + m.value, 0) / 60
|
||||
: 0;
|
||||
|
||||
// 计算平均延迟
|
||||
const avgLatency = recentLatency.length > 0
|
||||
? recentLatency.reduce((sum, m) => sum + m.value, 0) / recentLatency.length
|
||||
: 0;
|
||||
|
||||
// 总 token 数
|
||||
const totalTokens = recentTokens.reduce((sum, m) => sum + m.value, 0);
|
||||
|
||||
// 错误率
|
||||
const totalRequests = recentRequests.reduce((sum, m) => sum + m.value, 0);
|
||||
const totalErrors = recentErrors.reduce((sum, m) => sum + m.value, 0);
|
||||
const errorRate = totalRequests > 0 ? totalErrors / totalRequests : 0;
|
||||
|
||||
return {
|
||||
qps: Math.round(qps * 1000) / 1000,
|
||||
avgLatency: Math.round(avgLatency * 100) / 100,
|
||||
totalTokens,
|
||||
errorRate: Math.round(errorRate * 10000) / 10000,
|
||||
metricsCount: this._metrics.length,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有已采集的指标
|
||||
*/
|
||||
resetMetrics() {
|
||||
this._metrics = [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonitorDashboard;
|
||||
|
|
@ -1,44 +1,103 @@
|
|||
// exe-engine/src/router/stubs/dialogue-stub.js
|
||||
// HLI 存根路由 · DIALOGUE 域
|
||||
// 501 占位 — Phase 0 骨架,P1 实现
|
||||
// HLI 路由 · DIALOGUE 域
|
||||
// Phase 1 实装 — 对话发送、流式、历史
|
||||
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
function createStubHandler(routeId, routePath, targetPhase) {
|
||||
return async (req) => {
|
||||
return {
|
||||
statusCode: 501,
|
||||
body: {
|
||||
status: 'not_implemented',
|
||||
routeId,
|
||||
path: routePath,
|
||||
phase: targetPhase,
|
||||
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
// 对话存储(session_id → turns[])
|
||||
const _dialogueStore = new Map();
|
||||
|
||||
const dialogueStubs = [
|
||||
{
|
||||
routeId: 'HLI-DIALOGUE-001',
|
||||
path: '/hli/dialogue/send',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-DIALOGUE-001', '/hli/dialogue/send', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { session_id, message, persona_id } = body;
|
||||
|
||||
if (!session_id || !message) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_PARAMS', message: '缺少 session_id 或 message' }
|
||||
};
|
||||
}
|
||||
|
||||
// 获取或创建会话轮次列表
|
||||
if (!_dialogueStore.has(session_id)) {
|
||||
_dialogueStore.set(session_id, []);
|
||||
}
|
||||
|
||||
const turns = _dialogueStore.get(session_id);
|
||||
const turnId = `turn-${randomUUID().slice(0, 12)}`;
|
||||
|
||||
// 模拟回复(Phase 1 无真实 AI,返回确认回复)
|
||||
const reply = `[${persona_id || 'system'}] 已收到: ${message}`;
|
||||
|
||||
turns.push({
|
||||
turnId,
|
||||
user: message,
|
||||
assistant: reply,
|
||||
persona_id: persona_id || null,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { reply, session_id, turn_id: turnId }
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-DIALOGUE-002',
|
||||
path: '/hli/dialogue/stream',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-DIALOGUE-002', '/hli/dialogue/stream', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { session_id } = body;
|
||||
|
||||
// SSE 流式占位 — Phase 1 返回 ready 状态
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
type: 'stream',
|
||||
session_id: session_id || null,
|
||||
status: 'ready'
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-DIALOGUE-003',
|
||||
path: '/hli/dialogue/history',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-DIALOGUE-003', '/hli/dialogue/history', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { session_id, limit } = body;
|
||||
|
||||
if (!session_id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_SESSION_ID', message: '缺少 session_id' }
|
||||
};
|
||||
}
|
||||
|
||||
const allTurns = _dialogueStore.get(session_id) || [];
|
||||
const returnTurns = limit && limit > 0 ? allTurns.slice(-limit) : allTurns;
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
session_id,
|
||||
turns: returnTurns,
|
||||
total: allTurns.length
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { dialogueStubs, createStubHandler };
|
||||
module.exports = { dialogueStubs };
|
||||
|
|
|
|||
|
|
@ -1,45 +1,113 @@
|
|||
// exe-engine/src/router/stubs/persona-stub.js
|
||||
// HLI 存根路由 · PERSONA 域
|
||||
// 501 占位 — Phase 0 骨架,P1 实现
|
||||
// HLI 路由 · PERSONA 域
|
||||
// Phase 1 实装 — 人格体加载与切换
|
||||
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 创建 501 存根处理器
|
||||
* @param {string} routeId HLI 路由 ID
|
||||
* @param {string} routePath 路由路径
|
||||
* @param {string} targetPhase 目标实现阶段
|
||||
* @returns {Function} 处理器函数
|
||||
*/
|
||||
function createStubHandler(routeId, routePath, targetPhase) {
|
||||
return async (req) => {
|
||||
return {
|
||||
statusCode: 501,
|
||||
body: {
|
||||
status: 'not_implemented',
|
||||
routeId,
|
||||
path: routePath,
|
||||
phase: targetPhase,
|
||||
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
// 人格体内存存储(Phase 1 用内存,Phase 2 对接 Grid-DB)
|
||||
const _personaStore = new Map();
|
||||
|
||||
// 预加载默认人格体
|
||||
_personaStore.set('PER-ZY-01', {
|
||||
persona_id: 'PER-ZY-01',
|
||||
name: '铸渊',
|
||||
traits: ['guardian', 'code', 'precise'],
|
||||
status: 'idle'
|
||||
});
|
||||
_personaStore.set('PER-SY-01', {
|
||||
persona_id: 'PER-SY-01',
|
||||
name: '霜砚',
|
||||
traits: ['writing', 'aesthetic', 'literary'],
|
||||
status: 'idle'
|
||||
});
|
||||
_personaStore.set('PER-QQ-01', {
|
||||
persona_id: 'PER-QQ-01',
|
||||
name: '秋秋',
|
||||
traits: ['social', 'emotional', 'empathetic'],
|
||||
status: 'idle'
|
||||
});
|
||||
|
||||
// 当前活跃人格体
|
||||
let _activePersonaId = null;
|
||||
|
||||
const personaStubs = [
|
||||
{
|
||||
routeId: 'HLI-PERSONA-001',
|
||||
path: '/hli/persona/load',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-PERSONA-001', '/hli/persona/load', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const personaId = body.persona_id;
|
||||
|
||||
if (!personaId) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_PERSONA_ID', message: '缺少 persona_id' }
|
||||
};
|
||||
}
|
||||
|
||||
const persona = _personaStore.get(personaId);
|
||||
if (!persona) {
|
||||
// 未知人格体:创建默认占位
|
||||
const newPersona = {
|
||||
persona_id: personaId,
|
||||
name: `Persona-${personaId}`,
|
||||
traits: [],
|
||||
status: 'loaded'
|
||||
};
|
||||
_personaStore.set(personaId, newPersona);
|
||||
return { statusCode: 200, body: newPersona };
|
||||
}
|
||||
|
||||
persona.status = 'loaded';
|
||||
return { statusCode: 200, body: { ...persona } };
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-PERSONA-002',
|
||||
path: '/hli/persona/switch',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-PERSONA-002', '/hli/persona/switch', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { from_persona_id, to_persona_id } = body;
|
||||
|
||||
if (!to_persona_id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_TARGET', message: '缺少 to_persona_id' }
|
||||
};
|
||||
}
|
||||
|
||||
// 标记旧人格体为 idle
|
||||
if (from_persona_id && _personaStore.has(from_persona_id)) {
|
||||
_personaStore.get(from_persona_id).status = 'idle';
|
||||
}
|
||||
|
||||
// 确保目标人格体存在
|
||||
if (!_personaStore.has(to_persona_id)) {
|
||||
_personaStore.set(to_persona_id, {
|
||||
persona_id: to_persona_id,
|
||||
name: `Persona-${to_persona_id}`,
|
||||
traits: [],
|
||||
status: 'active'
|
||||
});
|
||||
} else {
|
||||
_personaStore.get(to_persona_id).status = 'active';
|
||||
}
|
||||
|
||||
_activePersonaId = to_persona_id;
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
active_persona_id: to_persona_id,
|
||||
previous_persona_id: from_persona_id || null,
|
||||
status: 'switched'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { personaStubs, createStubHandler };
|
||||
module.exports = { personaStubs };
|
||||
|
|
|
|||
|
|
@ -1,38 +1,90 @@
|
|||
// exe-engine/src/router/stubs/user-stub.js
|
||||
// HLI 存根路由 · USER 域
|
||||
// 501 占位 — Phase 0 骨架,P1 实现
|
||||
// HLI 路由 · USER 域
|
||||
// Phase 1 实装 — 用户资料查询与更新
|
||||
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
function createStubHandler(routeId, routePath, targetPhase) {
|
||||
return async (req) => {
|
||||
return {
|
||||
statusCode: 501,
|
||||
body: {
|
||||
status: 'not_implemented',
|
||||
routeId,
|
||||
path: routePath,
|
||||
phase: targetPhase,
|
||||
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
// 用户内存存储
|
||||
const _userStore = new Map();
|
||||
|
||||
// 预加载默认用户
|
||||
_userStore.set('USR-001', {
|
||||
user_id: 'USR-001',
|
||||
name: '冰朔',
|
||||
role: 'admin',
|
||||
email: 'ice@guanghulab.com',
|
||||
createdAt: '2026-01-01T00:00:00.000Z'
|
||||
});
|
||||
_userStore.set('USR-002', {
|
||||
user_id: 'USR-002',
|
||||
name: '测试用户',
|
||||
role: 'user',
|
||||
email: 'test@guanghulab.com',
|
||||
createdAt: '2026-03-01T00:00:00.000Z'
|
||||
});
|
||||
|
||||
const userStubs = [
|
||||
{
|
||||
routeId: 'HLI-USER-001',
|
||||
path: '/hli/user/profile',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-USER-001', '/hli/user/profile', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const userId = body.user_id;
|
||||
|
||||
if (!userId) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_USER_ID', message: '缺少 user_id' }
|
||||
};
|
||||
}
|
||||
|
||||
const user = _userStore.get(userId);
|
||||
if (!user) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
body: { error: true, code: 'USER_NOT_FOUND', message: `用户 ${userId} 不存在` }
|
||||
};
|
||||
}
|
||||
|
||||
return { statusCode: 200, body: { ...user } };
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-USER-002',
|
||||
path: '/hli/user/profile/update',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-USER-002', '/hli/user/profile/update', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { user_id, updates } = body;
|
||||
|
||||
if (!user_id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_USER_ID', message: '缺少 user_id' }
|
||||
};
|
||||
}
|
||||
|
||||
// 不存在则创建
|
||||
if (!_userStore.has(user_id)) {
|
||||
_userStore.set(user_id, {
|
||||
user_id,
|
||||
name: 'Unknown',
|
||||
role: 'user',
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const user = _userStore.get(user_id);
|
||||
if (updates && typeof updates === 'object') {
|
||||
Object.assign(user, updates);
|
||||
}
|
||||
user.updatedAt = new Date().toISOString();
|
||||
|
||||
return { statusCode: 200, body: { ...user } };
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { userStubs, createStubHandler };
|
||||
module.exports = { userStubs };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
// exe-engine/tests/smoke/exe-engine-p1.test.js
|
||||
// EXE-Engine · Phase 1 冒烟测试
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
createEngine,
|
||||
ContextManager,
|
||||
Session,
|
||||
MultiModelExecutor,
|
||||
SchedulerV2,
|
||||
MonitorDashboard,
|
||||
QwenAdapter
|
||||
} = require('../../src/index');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
passed++;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` ✅ ${message}`);
|
||||
} else {
|
||||
failed++;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(` ❌ ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🔧 EXE-Engine Phase 1 冒烟测试\n');
|
||||
|
||||
const engine = createEngine();
|
||||
|
||||
// ── 测试 1: Phase 1 模块导出 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('── 测试 1: Phase 1 模块导出 ──');
|
||||
assert(typeof ContextManager === 'function', 'ContextManager 是构造函数');
|
||||
assert(typeof Session === 'function', 'Session 是构造函数');
|
||||
assert(typeof MultiModelExecutor === 'function', 'MultiModelExecutor 是构造函数');
|
||||
assert(typeof SchedulerV2 === 'function', 'SchedulerV2 是构造函数');
|
||||
assert(typeof MonitorDashboard === 'function', 'MonitorDashboard 是构造函数');
|
||||
assert(engine.contextManager instanceof ContextManager, 'engine.contextManager 已创建');
|
||||
assert(engine.session instanceof Session, 'engine.session 已创建');
|
||||
assert(engine.multiModelExecutor instanceof MultiModelExecutor, 'engine.multiModelExecutor 已创建');
|
||||
assert(engine.scheduler instanceof SchedulerV2, 'engine.scheduler 已创建');
|
||||
assert(engine.monitor instanceof MonitorDashboard, 'engine.monitor 已创建');
|
||||
|
||||
// ── 测试 2: ContextManager ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 2: ContextManager 上下文管理 ──');
|
||||
const cm = engine.contextManager;
|
||||
const ctxId = cm.createContext('AG-ZY-01', 'sess-test-001', { repo: 'guanghulab' });
|
||||
assert(typeof ctxId === 'string' && ctxId.startsWith('ctx-'), `创建上下文返回 ID: ${ctxId}`);
|
||||
|
||||
const ctx = cm.getContext(ctxId);
|
||||
assert(ctx !== null, '获取上下文成功');
|
||||
assert(ctx.agentId === 'AG-ZY-01', '上下文 agentId 正确');
|
||||
assert(ctx.sessionId === 'sess-test-001', '上下文 sessionId 正确');
|
||||
assert(ctx.data.repo === 'guanghulab', '上下文初始数据正确');
|
||||
assert(ctx.status === 'active', '上下文初始状态为 active');
|
||||
assert(Array.isArray(ctx.messages), '上下文 messages 是数组');
|
||||
|
||||
const updateOk = cm.updateContext(ctxId, { branch: 'main' });
|
||||
assert(updateOk === true, '更新上下文成功');
|
||||
assert(cm.getContext(ctxId).data.branch === 'main', '更新后数据正确');
|
||||
|
||||
const addMsgOk = cm.addMessage(ctxId, 'user', '你好');
|
||||
assert(addMsgOk === true, '添加消息成功');
|
||||
assert(cm.getContext(ctxId).messages.length === 1, '消息数量正确');
|
||||
assert(cm.getContext(ctxId).messages[0].role === 'user', '消息角色正确');
|
||||
|
||||
const expireOk = cm.expireContext(ctxId);
|
||||
assert(expireOk === true, '过期上下文成功');
|
||||
assert(cm.getContext(ctxId).status === 'expired', '过期后状态正确');
|
||||
|
||||
const contexts = cm.listContexts('AG-ZY-01');
|
||||
assert(contexts.length >= 1, 'listContexts 返回结果');
|
||||
|
||||
cm.setGridDBPersistence({ mock: true });
|
||||
assert(cm._gridDB !== null, 'Grid-DB 预留接口设置成功');
|
||||
|
||||
const notFoundCtx = cm.getContext('ctx-nonexistent');
|
||||
assert(notFoundCtx === null, '不存在的上下文返回 null');
|
||||
|
||||
// ── 测试 3: Session 会话管理 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 3: Session 会话管理 ──');
|
||||
const sess = engine.session;
|
||||
const { sessionId, contextId } = sess.createSession('AG-ZY-01', { topic: '代码审查' });
|
||||
assert(typeof sessionId === 'string' && sessionId.startsWith('sess-'), `创建会话: ${sessionId}`);
|
||||
assert(typeof contextId === 'string' && contextId.startsWith('ctx-'), `关联上下文: ${contextId}`);
|
||||
|
||||
const sessObj = sess.getSession(sessionId);
|
||||
assert(sessObj !== null, '获取会话成功');
|
||||
assert(sessObj.agentId === 'AG-ZY-01', '会话 agentId 正确');
|
||||
assert(sessObj.status === 'active', '会话初始状态 active');
|
||||
assert(sessObj.metadata.topic === '代码审查', '会话元数据正确');
|
||||
|
||||
const turnOk = sess.addTurn(sessionId, '帮我看看这段代码', '代码看起来没问题');
|
||||
assert(turnOk === true, '添加轮次成功');
|
||||
|
||||
sess.addTurn(sessionId, '还有优化建议吗', '建议使用 const 替代 let');
|
||||
const history = sess.getHistory(sessionId, 1);
|
||||
assert(history.length === 1, 'getHistory limit=1 返回 1 条');
|
||||
assert(history[0].user === '还有优化建议吗', '历史最后一条消息正确');
|
||||
|
||||
const fullHistory = sess.getHistory(sessionId);
|
||||
assert(fullHistory.length === 2, '完整历史 2 条');
|
||||
|
||||
const endOk = sess.endSession(sessionId);
|
||||
assert(endOk === true, '结束会话成功');
|
||||
assert(sess.getSession(sessionId).status === 'ended', '结束后状态为 ended');
|
||||
|
||||
const addAfterEnd = sess.addTurn(sessionId, 'test', 'test');
|
||||
assert(addAfterEnd === false, '结束后不能添加轮次');
|
||||
|
||||
const sessions = sess.listSessions('AG-ZY-01');
|
||||
assert(sessions.length >= 1, 'listSessions 返回结果');
|
||||
|
||||
// ── 测试 4: MultiModelExecutor ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 4: MultiModelExecutor 多模型执行器 ──');
|
||||
const mme = engine.multiModelExecutor;
|
||||
const availModels = mme.getAvailableModels();
|
||||
assert(Array.isArray(availModels), 'getAvailableModels 返回数组');
|
||||
assert(availModels.length === 4, `可用模型 4 个 (实际: ${availModels.length})`);
|
||||
assert(availModels.includes('deepseek-v3'), '包含 deepseek-v3');
|
||||
assert(availModels.includes('qwen-max'), '包含 qwen-max');
|
||||
|
||||
// ── 测试 5: SchedulerV2 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 5: SchedulerV2 调度器 ──');
|
||||
const sched = engine.scheduler;
|
||||
|
||||
const t1 = sched.enqueue({ agentId: 'AG-ZY-01', priority: 'high', payload: { action: 'build' } });
|
||||
assert(typeof t1 === 'string', `入队高优先级任务: ${t1}`);
|
||||
|
||||
const t2 = sched.enqueue({ agentId: 'AG-ZY-01', priority: 'low', payload: { action: 'lint' } });
|
||||
const t3 = sched.enqueue({ agentId: 'AG-SY-01', priority: 'normal', payload: { action: 'review' } });
|
||||
|
||||
const queue = sched.getQueue();
|
||||
assert(queue.length === 3, '队列中 3 个任务');
|
||||
assert(queue[0].priority === 'high', '高优先级排在前面');
|
||||
|
||||
const dequeued = sched.dequeue();
|
||||
assert(dequeued !== null, '出队成功');
|
||||
assert(dequeued.priority === 'high', '出队的是高优先级任务');
|
||||
|
||||
sched.complete(dequeued.taskId, { success: true });
|
||||
const status = sched.getStatus();
|
||||
assert(status.completed === 1, '已完成 1 个');
|
||||
assert(status.pending === 2, '待处理 2 个');
|
||||
|
||||
// 依赖测试
|
||||
const t4 = sched.enqueue({
|
||||
taskId: 'task-dep-1',
|
||||
agentId: 'AG-ZY-01',
|
||||
priority: 'high',
|
||||
dependsOn: ['task-not-done'],
|
||||
payload: { action: 'deploy' }
|
||||
});
|
||||
const depDequeue = sched.dequeue();
|
||||
assert(depDequeue?.taskId !== 'task-dep-1', '依赖未满足的任务不出队');
|
||||
|
||||
// 标记失败测试
|
||||
sched.fail('task-fail-test', '测试失败');
|
||||
assert(sched.getStatus().failed === 1, 'fail 记录成功');
|
||||
|
||||
// ── 测试 6: MonitorDashboard ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 6: MonitorDashboard 监控仪表盘 ──');
|
||||
const mon = engine.monitor;
|
||||
|
||||
mon.recordMetric('request_count', 1, { model: 'deepseek-v3' });
|
||||
mon.recordMetric('request_count', 1, { model: 'qwen-max' });
|
||||
mon.recordMetric('latency_ms', 150, { model: 'deepseek-v3' });
|
||||
mon.recordMetric('latency_ms', 200, { model: 'qwen-max' });
|
||||
mon.recordMetric('tokens_used', 1500, {});
|
||||
mon.recordMetric('error_count', 0, {});
|
||||
|
||||
const reqMetrics = mon.getMetrics('request_count');
|
||||
assert(reqMetrics.length === 2, '获取 request_count 指标 2 条');
|
||||
|
||||
const latMetrics = mon.getMetrics('latency_ms');
|
||||
assert(latMetrics.length === 2, '获取 latency_ms 指标 2 条');
|
||||
|
||||
const snapshot = mon.getSnapshot();
|
||||
assert(typeof snapshot.qps === 'number', 'snapshot 包含 qps');
|
||||
assert(typeof snapshot.avgLatency === 'number', 'snapshot 包含 avgLatency');
|
||||
assert(typeof snapshot.totalTokens === 'number', 'snapshot 包含 totalTokens');
|
||||
assert(typeof snapshot.errorRate === 'number', 'snapshot 包含 errorRate');
|
||||
assert(snapshot.totalTokens === 1500, `totalTokens = 1500 (实际: ${snapshot.totalTokens})`);
|
||||
assert(snapshot.metricsCount === 6, `metricsCount = 6 (实际: ${snapshot.metricsCount})`);
|
||||
|
||||
mon.resetMetrics();
|
||||
assert(mon.getMetrics('request_count').length === 0, 'resetMetrics 清除成功');
|
||||
|
||||
// ── 测试 7: HLI Persona 路由 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 7: HLI Persona 路由(Phase 1 实装) ──');
|
||||
(async () => {
|
||||
const loadResult = await engine.router.handleStubRoute('/hli/persona/load');
|
||||
assert(loadResult.statusCode === 200 || loadResult.statusCode === 400,
|
||||
`persona/load 返回 ${loadResult.statusCode}`);
|
||||
|
||||
// 带参数调用
|
||||
const personaStubs = engine.router.getHLIStubs().filter(s => s.path === '/hli/persona/load');
|
||||
const loadHandler = personaStubs[0].handler;
|
||||
const loadRes = await loadHandler({ body: { persona_id: 'PER-ZY-01' } });
|
||||
assert(loadRes.statusCode === 200, 'persona/load 加载铸渊成功');
|
||||
assert(loadRes.body.persona_id === 'PER-ZY-01', '返回正确的 persona_id');
|
||||
assert(loadRes.body.status === 'loaded', '状态为 loaded');
|
||||
assert(loadRes.body.name === '铸渊', '人格体名称正确');
|
||||
|
||||
const switchStubs = engine.router.getHLIStubs().filter(s => s.path === '/hli/persona/switch');
|
||||
const switchHandler = switchStubs[0].handler;
|
||||
const switchRes = await switchHandler({ body: { from_persona_id: 'PER-ZY-01', to_persona_id: 'PER-SY-01' } });
|
||||
assert(switchRes.statusCode === 200, 'persona/switch 切换成功');
|
||||
assert(switchRes.body.active_persona_id === 'PER-SY-01', '活跃人格体切换正确');
|
||||
assert(switchRes.body.status === 'switched', '状态为 switched');
|
||||
|
||||
// ── 测试 8: HLI Dialogue 路由 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 8: HLI Dialogue 路由(Phase 1 实装) ──');
|
||||
|
||||
const dialogueStubs = engine.router.getHLIStubs();
|
||||
const sendHandler = dialogueStubs.find(s => s.path === '/hli/dialogue/send').handler;
|
||||
const sendRes = await sendHandler({ body: { session_id: 'test-sess', message: '你好', persona_id: 'PER-ZY-01' } });
|
||||
assert(sendRes.statusCode === 200, 'dialogue/send 发送成功');
|
||||
assert(typeof sendRes.body.reply === 'string', '返回 reply 字符串');
|
||||
assert(sendRes.body.session_id === 'test-sess', '返回正确的 session_id');
|
||||
assert(typeof sendRes.body.turn_id === 'string', '返回 turn_id');
|
||||
|
||||
const histHandler = dialogueStubs.find(s => s.path === '/hli/dialogue/history').handler;
|
||||
const histRes = await histHandler({ body: { session_id: 'test-sess' } });
|
||||
assert(histRes.statusCode === 200, 'dialogue/history 查询成功');
|
||||
assert(Array.isArray(histRes.body.turns), '返回 turns 数组');
|
||||
assert(histRes.body.total >= 1, `历史记录 total >= 1 (实际: ${histRes.body.total})`);
|
||||
|
||||
const streamHandler = dialogueStubs.find(s => s.path === '/hli/dialogue/stream').handler;
|
||||
const streamRes = await streamHandler({ body: { session_id: 'test-sess' } });
|
||||
assert(streamRes.statusCode === 200, 'dialogue/stream 返回 200');
|
||||
assert(streamRes.body.type === 'stream', 'stream 类型正确');
|
||||
|
||||
// ── 测试 9: HLI User 路由 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 9: HLI User 路由(Phase 1 实装) ──');
|
||||
|
||||
const profileHandler = dialogueStubs.find(s => s.path === '/hli/user/profile').handler;
|
||||
const profileRes = await profileHandler({ body: { user_id: 'USR-001' } });
|
||||
assert(profileRes.statusCode === 200, 'user/profile 查询成功');
|
||||
assert(profileRes.body.user_id === 'USR-001', '返回正确的 user_id');
|
||||
assert(profileRes.body.name === '冰朔', '用户名正确');
|
||||
|
||||
const updateHandler = dialogueStubs.find(s => s.path === '/hli/user/profile/update').handler;
|
||||
const updateRes = await updateHandler({ body: { user_id: 'USR-001', updates: { role: 'superadmin' } } });
|
||||
assert(updateRes.statusCode === 200, 'user/profile/update 更新成功');
|
||||
assert(updateRes.body.role === 'superadmin', '角色更新正确');
|
||||
|
||||
const notFoundRes = await profileHandler({ body: { user_id: 'USR-NONEXIST' } });
|
||||
assert(notFoundRes.statusCode === 404, '不存在的用户返回 404');
|
||||
|
||||
const newUserRes = await updateHandler({ body: { user_id: 'USR-NEW', updates: { name: '新用户' } } });
|
||||
assert(newUserRes.statusCode === 200, '更新不存在的用户自动创建');
|
||||
assert(newUserRes.body.name === '新用户', '新用户名称正确');
|
||||
|
||||
// ── 测试 10: Qwen Adapter executeStream ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 10: QwenAdapter executeStream ──');
|
||||
const qwAdapter = engine.adapters.get('qwen-max');
|
||||
assert(typeof qwAdapter.executeStream === 'function', 'executeStream 方法存在');
|
||||
|
||||
// 验证 _resolveModel 增强
|
||||
assert(qwAdapter._resolveModel('code_generation') === 'qwen-coder-plus-latest', '_resolveModel code_generation 正确');
|
||||
assert(qwAdapter._resolveModel('text_processing') === 'qwen-max-latest', '_resolveModel text_processing 正确');
|
||||
assert(qwAdapter._resolveModel('reasoning') === 'qwen-max', '_resolveModel reasoning 正确');
|
||||
|
||||
// ── 测试结果汇总 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n══════════════════════════════════════');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🔧 EXE-Engine Phase 1 冒烟测试完成`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` ✅ 通过: ${passed}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` ❌ 失败: ${failed}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` 📊 总计: ${passed + failed}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('══════════════════════════════════════\n');
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in New Issue