shuzhiren-comfyui/任务队列后端/outside/outPlatforms/coze/coze.js

267 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from 'axios';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as path from 'path';
import { fileURLToPath } from 'url';
import jwt from 'jsonwebtoken';
import redis from '../../../redis/index.js';
// 获取当前文件的目录ES模块方式
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Coze配置
const COZE_OAUTH_CONFIG = {
appId: '1172420148562',
kid: 'noU76VVvKw679eiyjwHUZLcU2zDwKtSD6N-rOsPIwe0',
privateKeyPath: path.join(__dirname, 'private_key.pem')
};
// Redis中存储API密钥的键
const REDIS_COZE_TOKEN_KEY = 'coze:api:token';
const REDIS_COZE_EXPIRE_KEY = 'coze:api:expireTime';
/**
* 生成符合Coze规范的JWT
* @param {number} durationSeconds - 有效期(秒)
* @returns {Promise<string>} JWT token
*/
async function generateCozeJWT(durationSeconds = 900) {
const privateKey = fs.readFileSync(COZE_OAUTH_CONFIG.privateKeyPath, 'utf8');
const now = Math.floor(Date.now() / 1000);
const header = {
alg: 'RS256',
typ: 'JWT',
kid: COZE_OAUTH_CONFIG.kid
};
const payload = {
iss: COZE_OAUTH_CONFIG.appId,
aud: 'api.coze.cn',
iat: now,
exp: now + durationSeconds,
jti: crypto.randomBytes(32).toString('hex')
};
return jwt.sign(payload, privateKey, {
algorithm: 'RS256',
header: header
});
}
/**
* 获取OAuth Access Token
* @param {string} jwtToken - 生成的JWT
* @param {number} durationSeconds - Token有效期
* @returns {Promise<Object>} 包含access_token的对象
*/
async function getOAuthToken(jwtToken, durationSeconds = 3600) {
try {
const response = await axios.post(
'https://api.coze.cn/api/permission/oauth2/token',
{
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
duration_seconds: durationSeconds
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
}
}
);
return {
access_token: response.data.access_token
};
} catch (error) {
console.error('获取Coze Token失败:', error.response?.data || error.message);
throw error;
}
}
/**
* 获取有效的API密钥过期自动刷新
* @returns {Promise<string>} 有效的API密钥
*/
async function getValidApiKey() {
const now = Date.now();
try {
// 1. 从Redis获取当前token和过期时间
const [storedToken, storedExpireTime] = await Promise.all([
redis.get(REDIS_COZE_TOKEN_KEY),
redis.get(REDIS_COZE_EXPIRE_KEY)
]);
// 2. 如果token存在且未过期5分钟内则直接返回
if (storedToken && storedExpireTime && now < parseInt(storedExpireTime) - 5 * 60 * 1000) {
return storedToken;
}
// 3. 否则重新生成token
console.log('Coze API密钥已过期或不存在重新生成...');
const jwtToken = await generateCozeJWT();
const tokenResponse = await getOAuthToken(jwtToken);
const newToken = tokenResponse.access_token;
// 设置过期时间为1小时后
const newExpireTime = now + 3600 * 1000;
// 4. 将新token和过期时间存储到Redis使用Promise.all并行执行
await Promise.all([
redis.set(REDIS_COZE_TOKEN_KEY, newToken),
redis.set(REDIS_COZE_EXPIRE_KEY, newExpireTime.toString()),
redis.expire(REDIS_COZE_TOKEN_KEY, 3660),
redis.expire(REDIS_COZE_EXPIRE_KEY, 3660)
]);
console.log('Coze API密钥生成成功有效期1小时');
return newToken;
} catch (error) {
console.error('获取或生成Coze API密钥失败:', error);
throw error;
}
}
/**
* 获取生成请求的URL
* @returns {string} 生成请求的URL
*/
function getGenerateUrl() {
return 'https://api.coze.cn/v1/workflow/run';
}
/**
* 获取生成请求的Headers
* @param {string} apikey - API密钥可选自动生成时忽略
* @returns {Promise<Object>} 生成请求的Headers
*/
async function getGenerateHeader(apikey = null) {
const validApiKey = apikey || await getValidApiKey();
return {
"Authorization": `Bearer ${validApiKey}`,
"Content-Type": "application/json"
};
}
/**
* 获取生成请求的Body
* @param {Object} params - 包含payload和apikey的参数对象
* @returns {string} 生成请求的BodyJSON字符串
*/
function getGenerateBody(params) {
try {
// 前端发送的payload已经是完整的请求体对象包含is_async、parameters和workflow_id
// 只需要将其转换为JSON字符串即可
return params.payload;
} catch (error) {
console.error('构建Coze请求体失败:', error);
// 返回与成功例子一致的基本格式作为备用
return JSON.stringify({ is_async: true, parameters: {}, workflow_id: '' });
}
}
/**
* 获取查询请求的URL
* @param {string} remoteTaskId - 外部任务ID
* @param {string} workflowId - 工作流ID可选
* @returns {string} 查询请求的URL
*/
function getQueryUrl(remoteTaskId, workflowId = null) {
if (!workflowId) {
// 如果没有workflowId使用通用的运行历史查询接口
return `https://api.coze.cn/v1/workflow/run_histories/${remoteTaskId}`;
}
return `https://api.coze.cn/v1/workflows/${workflowId}/run_histories/${remoteTaskId}`;
}
/**
* 获取查询请求的Headers
* @param {string} apikey - API密钥可选自动生成时忽略
* @returns {Promise<Object>} 查询请求的Headers
*/
async function getQueryHeader(apikey = null) {
const validApiKey = apikey || await getValidApiKey();
return {
"Authorization": `Bearer ${validApiKey}`,
"Content-Type": "application/json"
};
}
/**
* 处理查询响应,判断任务状态
* @param {Object} response - 外部平台返回的响应数据
* @returns {boolean|Object} 任务完成返回结果否则返回false
*/
function getTaskStatus(response) {
if (!response) {
return false;
}
console.log('Coze API响应:', response);
// 处理Coze API的响应格式
// Coze API可能直接返回data对象或者在response.data中
const data = response.data;
// 首先检查Coze API的调用状态码
// code: 0 表示调用成功,其他值表示调用失败
if (response.code !== undefined) {
if (response.code !== 0) {
// 调用失败,返回错误信息
return {
result: JSON.stringify({ error: data.msg || 'API调用失败', code: data.code }),
status: 'failed'
};
} else {
// 当code为0时取data列表里的第一个值的execute_status
const taskData = data[0];
// 检查任务状态
// Coze API使用execute_status字段表示任务状态
if (taskData && taskData.execute_status) {
switch (taskData.execute_status) {
case 'Success':
// 任务完成,返回结果
console.log('任务成功完成,返回结果');
return {
result: taskData.output,
status: 'success'
};
case 'Running':
// 任务执行中返回false表示继续轮询
return false;
case 'Fail':
// 任务失败
return {
result: JSON.stringify({ error: taskData.error_message || taskData.error || taskData.msg || '任务失败' }),
status: 'failed'
};
default:
// 其他状态,视为任务仍在处理中
return false;
}
} else {
console.log('taskData 或 execute_status 不存在,继续轮询');
}
}
} else {
console.log('data.code 不存在,继续轮询');
}
// 其他情况,视为任务仍在处理中
return false;
}
export default {
getGenerateUrl,
getGenerateHeader,
getGenerateBody,
getQueryUrl,
getQueryHeader,
getTaskStatus
};