Files
beige/test/aiClient.js
yangqianqian 6a2632da70 baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:30:22 +08:00

110 lines
4.1 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.
'use strict';
// Clover 图片质量测试 · codeproxy 调用核心
// 复用自 xhs-tool/backend/infrastructure/aiClient.js已实测跑通的对的路简化去降级、去sharp
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') });
const FormData = require('form-data');
const https = require('https');
const fs = require('fs');
const KEY = process.env.AI_API_KEY;
const BASE_URL = process.env.AI_BASE_URL;
const MODEL_IMAGE = process.env.MODEL_IMAGE || 'gpt-image-2';
const IMAGE_TIMEOUT = 300000;
if (!KEY) throw new Error('AI_API_KEY 未配置(见 test/.env');
const sleep = ms => new Promise(r => setTimeout(r, ms));
function toBuffer(img) {
if (Buffer.isBuffer(img)) return img;
if (typeof img === 'string' && img.startsWith('data:'))
return Buffer.from(img.replace(/^data:[^;]+;base64,/, ''), 'base64');
return fs.readFileSync(img); // 本地文件路径
}
// 单次请求POST /images/edits (multipart)
function postEditOnce({ imgList, prompt, m, sz }) {
const form = new FormData();
form.append('model', m);
form.append('prompt', prompt);
form.append('size', sz);
form.append('n', '1');
for (let i = 0; i < imgList.length; i++) {
const buf = toBuffer(imgList[i]);
form.append('image[]', buf, { filename: `img_${i}.jpg`, contentType: 'image/jpeg' });
}
return new Promise((resolve, reject) => {
const ac = new AbortController();
const timer = setTimeout(() => { ac.abort(); req.destroy(new Error('300s timeout')); }, IMAGE_TIMEOUT);
const req = https.request(`${BASE_URL}/images/edits`, {
method: 'POST',
headers: { ...form.getHeaders(), Authorization: `Bearer ${KEY}` },
signal: ac.signal,
}, (r) => {
let body = '';
r.on('data', d => body += d);
r.on('end', () => { clearTimeout(timer); resolve({ status: r.statusCode, body }); });
r.on('error', reject);
});
req.on('error', (e) => { clearTimeout(timer); reject(e); });
form.pipe(req);
});
}
// 可重试错误:上游/网关临时故障 + 限流 + 超时
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
// 带参考图出图:失败自动退避重试(复用 xhs-tool 的重试思路)
async function imageEdit({ images, prompt, traceId = 't', model, size, maxRetry = 3 }) {
const m = model || MODEL_IMAGE;
const sz = size || process.env.IMAGE_SIZE || '1024x1536';
const imgList = Array.isArray(images) ? images : [images];
let lastErr;
for (let attempt = 1; attempt <= maxRetry; attempt++) {
const t0 = Date.now();
try {
const res = await postEditOnce({ imgList, prompt, m, sz });
console.log(`[imageEdit][${traceId}] try=${attempt}/${maxRetry} model=${m} size=${sz} imgs=${imgList.length} status=${res.status} ms=${Date.now()-t0}`);
if (res.status === 200) {
const d = JSON.parse(res.body).data[0];
if (d.url) return { type: 'url', value: d.url };
if (d.b64_json) return { type: 'b64', value: d.b64_json };
throw new Error('返回既无 url 也无 b64_json');
}
lastErr = new Error(`HTTP ${res.status}: ${res.body.slice(0, 300)}`);
if (!RETRYABLE.has(res.status)) throw lastErr; // 401/404等不重试直接抛
} catch (e) {
lastErr = e;
const code = (e.message.match(/HTTP (\d+)/) || [])[1];
if (code && !RETRYABLE.has(Number(code))) throw e;
}
if (attempt < maxRetry) {
const wait = 3000 * attempt; // 3s,6s 退避
console.log(` ↻ 上游临时故障,${wait/1000}s 后重试…`);
await sleep(wait);
}
}
throw lastErr;
}
// 保存结果到文件url则下载b64则解码
async function saveResult(result, outPath) {
if (result.type === 'b64') {
fs.writeFileSync(outPath, Buffer.from(result.value, 'base64'));
return outPath;
}
// url下载
await new Promise((resolve, reject) => {
https.get(result.value, (r) => {
const chunks = [];
r.on('data', c => chunks.push(c));
r.on('end', () => { fs.writeFileSync(outPath, Buffer.concat(chunks)); resolve(); });
r.on('error', reject);
}).on('error', reject);
});
return outPath;
}
module.exports = { imageEdit, saveResult, toBuffer, sleep };