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} 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} 包含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} 有效的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} 生成请求的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} 查询请求的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 };