diff --git a/exe-engine/src/adapters/qwen-adapter.js b/exe-engine/src/adapters/qwen-adapter.js index ceadd27f..76fb8fb1 100644 --- a/exe-engine/src/adapters/qwen-adapter.js +++ b/exe-engine/src/adapters/qwen-adapter.js @@ -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} 异步生成器,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'; } diff --git a/exe-engine/src/context/context-manager.js b/exe-engine/src/context/context-manager.js new file mode 100644 index 00000000..3f13d32f --- /dev/null +++ b/exe-engine/src/context/context-manager.js @@ -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; diff --git a/exe-engine/src/context/session.js b/exe-engine/src/context/session.js new file mode 100644 index 00000000..895a1238 --- /dev/null +++ b/exe-engine/src/context/session.js @@ -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; diff --git a/exe-engine/src/controller/scheduler-v2.js b/exe-engine/src/controller/scheduler-v2.js new file mode 100644 index 00000000..52c44c77 --- /dev/null +++ b/exe-engine/src/controller/scheduler-v2.js @@ -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; diff --git a/exe-engine/src/executor/multi-model.js b/exe-engine/src/executor/multi-model.js new file mode 100644 index 00000000..a055965e --- /dev/null +++ b/exe-engine/src/executor/multi-model.js @@ -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} 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} 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; diff --git a/exe-engine/src/index.js b/exe-engine/src/index.js index b9ecd18c..ccba4543 100644 --- a/exe-engine/src/index.js +++ b/exe-engine/src/index.js @@ -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 }; diff --git a/exe-engine/src/monitor/dashboard.js b/exe-engine/src/monitor/dashboard.js new file mode 100644 index 00000000..66022a52 --- /dev/null +++ b/exe-engine/src/monitor/dashboard.js @@ -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; diff --git a/exe-engine/src/router/stubs/dialogue-stub.js b/exe-engine/src/router/stubs/dialogue-stub.js index 10098546..3254e29f 100644 --- a/exe-engine/src/router/stubs/dialogue-stub.js +++ b/exe-engine/src/router/stubs/dialogue-stub.js @@ -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 }; diff --git a/exe-engine/src/router/stubs/persona-stub.js b/exe-engine/src/router/stubs/persona-stub.js index 48be08cb..71445a43 100644 --- a/exe-engine/src/router/stubs/persona-stub.js +++ b/exe-engine/src/router/stubs/persona-stub.js @@ -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 }; diff --git a/exe-engine/src/router/stubs/user-stub.js b/exe-engine/src/router/stubs/user-stub.js index 7ed98240..952066d7 100644 --- a/exe-engine/src/router/stubs/user-stub.js +++ b/exe-engine/src/router/stubs/user-stub.js @@ -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 }; diff --git a/exe-engine/tests/smoke/exe-engine-p1.test.js b/exe-engine/tests/smoke/exe-engine-p1.test.js new file mode 100644 index 00000000..9fa2ec8e --- /dev/null +++ b/exe-engine/tests/smoke/exe-engine-p1.test.js @@ -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); + } +})();