267 lines
7.7 KiB
JavaScript
267 lines
7.7 KiB
JavaScript
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} 生成请求的Body(JSON字符串)
|
||
*/
|
||
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
|
||
};
|