221 lines
6.5 KiB
JavaScript
221 lines
6.5 KiB
JavaScript
|
|
// 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<object>} 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<boolean>}
|
|||
|
|
*/
|
|||
|
|
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<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';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算调用成本(单位:元)
|
|||
|
|
* @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<string>}
|
|||
|
|
*/
|
|||
|
|
_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;
|