// exe-engine/src/adapters/qwen-adapter.js // EXE-Engine · Qwen (通义千问) 模型适配器 // 适配 Qwen-Max / Qwen-Coder(DashScope OpenAI 兼容模式) // PRJ-EXE-001 · Phase 1 准备 · ZY-EXE-P1-001 // 版权:国作登字-2026-A-00037559 'use strict'; const https = require('https'); const http = require('http'); const BaseAdapter = require('./base-adapter'); /** * Qwen 模型适配器 * * 本体论锚定:Qwen 是一瓶中文优化的墨水。 * 中文生态优势突出,阿里云原生,适合文本处理任务。 */ class QwenAdapter extends BaseAdapter { constructor(config) { super({ ...config, provider: config.provider || 'dashscope' }); } /** * 执行推理请求 * * @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 {Promise} EXE 标准响应 */ async execute(request) { if (this.isInCooldown()) { throw new Error(`[EXE] ${this.name} 处于冷却期,暂不可用`); } const startTime = Date.now(); const messages = []; if (request.systemPrompt) { messages.push({ role: 'system', content: request.systemPrompt }); } messages.push({ role: 'user', content: request.prompt }); const body = JSON.stringify({ model: this._resolveModel(request.taskType), messages, max_tokens: request.maxTokens || this.defaultMaxTokens, temperature: request.temperature ?? this.defaultTemperature, stream: false }); try { const response = await this._httpPost(this.endpoint, body, { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }); const data = JSON.parse(response); if (!data.choices || !data.choices[0]) { throw new Error('Qwen API 返回格式异常: 缺少 choices'); } const latency = Date.now() - startTime; const usage = data.usage || {}; this.resetFailures(); return { model: data.model || this.name, output: data.choices[0].message.content, usage: { inputTokens: usage.prompt_tokens || 0, outputTokens: usage.completion_tokens || 0, cost: this._calculateCost(usage.prompt_tokens || 0, usage.completion_tokens || 0), unit: this.costPerToken.currency || 'CNY' }, latency, status: 'success', finishReason: data.choices[0].finish_reason }; } catch (err) { this.recordFailure(err.message); throw err; } } /** * 健康检查 * @returns {Promise} */ async healthCheck() { try { const body = JSON.stringify({ model: 'qwen-turbo', messages: [{ role: 'user', content: 'ping' }], max_tokens: 5 }); await this._httpPost(this.endpoint, body, { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }); this._healthy = true; } catch (err) { this._healthy = false; this._lastHealthError = err.message; } this._lastHealthCheck = new Date().toISOString(); 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'; } /** * 计算调用成本(单位:元) * @param {number} inputTokens * @param {number} outputTokens * @returns {number} */ _calculateCost(inputTokens, outputTokens) { const inputCost = (inputTokens / 1_000_000) * this.costPerToken.input; const outputCost = (outputTokens / 1_000_000) * this.costPerToken.output; return Math.round((inputCost + outputCost) * 10000) / 10000; } /** * HTTP POST 请求 * @param {string} url * @param {string} body * @param {object} headers * @returns {Promise} */ _httpPost(url, body, headers) { return new Promise((resolve, reject) => { const parsed = new URL(url); const transport = parsed.protocol === 'https:' ? https : http; const req = transport.request({ hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search, method: 'POST', headers: { ...headers, 'Content-Length': Buffer.byteLength(body) }, timeout: 120000 }, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { if (res.statusCode >= 400) { reject(new Error(`Qwen API 错误 (${res.statusCode}): ${data}`)); return; } resolve(data); }); }); req.on('error', err => reject(new Error(`Qwen 网络错误: ${err.message}`))); req.on('timeout', () => { req.destroy(); reject(new Error('Qwen API 请求超时')); }); req.write(body); req.end(); }); } } module.exports = QwenAdapter;