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;
|