/* ============================================================ * debug_dashscope_image_models.js —— 探测百炼(DashScope)可用的文生图模型 * * 目的:项目此前只用了 chat/completions 与 TTS,没有文生图调用。 * 本脚本枚举候选模型与候选端点,找出「本账号实际能用」的组合, * 并把生成的图落盘到临时目录做 MD5 比对,确认不同 prompt 出的是不同的图。 * * 运行:node dev_test_scripts/debug/debug_dashscope_image_models.js * 说明:只读探测,不写入仓库任何文件(产物落在系统临时目录)。 * ============================================================ */ const fs = require('fs') const path = require('path') const crypto = require('crypto') // 载入本机私有凭据(与项目其他模块同一份文件,不进仓库) const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local') if (fs.existsSync(envPath)) { const text = fs.readFileSync(envPath, 'utf8') for (const line of text.split(/\r?\n/)) { if (!line || /^\s*#/.test(line)) continue const i = line.indexOf('=') if (i < 0) continue const k = line.slice(0, i).trim() const v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '') if (k && !(k in process.env)) process.env[k] = v } } const KEY = process.env.DASHSCOPE_API_KEY || '' const MaaS = (process.env.DASHSCOPE_API_HOST_FOR_OPENAI || '').replace(/\/+$/, '') const PUBLIC_BASE = 'https://dashscope.aliyuncs.com' const OUT = path.join(process.env.TEMP || 'C:\\Windows\\Temp', 'tc_img_probe') fs.mkdirSync(OUT, { recursive: true }) const promptA = 'coastal embankment above tidal flat, wind turbines, autumn afternoon' const promptB = 'old Chinese town stone lane with grey brick houses and red lanterns' console.log('[env] KEY 已加载:', !!KEY, KEY ? '(len=' + KEY.length + ')' : '') console.log('[env] MaaS host:', MaaS ? MaaS.replace(/\/compatible-mode.*$/, '/…') : '(空)') console.log('[out]', OUT) const get = async (url, headers) => { const r = await fetch(url, { headers, signal: AbortSignal.timeout(25000) }) const t = await r.text() return { status: r.status, body: t } } /* ---------- 1. 先看这个 MaaS 端点声明了哪些模型 ---------- */ async function listModels() { console.log('\n======== 1. 列出 MaaS 端点的模型 ========') if (!MaaS) { console.log('无 MaaS host,跳过'); return [] } try { const r = await get(MaaS + '/models', { Authorization: 'Bearer ' + KEY }) console.log('status=', r.status) if (r.status !== 200) { console.log(r.body.slice(0, 400)); return [] } let j = null try { j = JSON.parse(r.body) } catch { console.log(r.body.slice(0, 400)); return [] } const ids = (j.data || []).map((m) => m.id) console.log('模型数 =', ids.length) const likely = ids.filter((id) => /image|t2i|wanx|wan2|flux|sd|diffus/i.test(id)) console.log('疑似文生图相关:', likely.length ? likely.join(', ') : '(无)') console.log('全部模型(前 60):\n ' + ids.slice(0, 60).join('\n ')) return ids } catch (e) { console.log('ERR', e.message) ; return [] } } /* ---------- 2. 探 text2image/image-synthesis 端点 ---------- */ const WANX_MODELS = [ 'wanx2.1-t2i-turbo', 'wanx2.1-t2i-plus', 'wanx2.0-t2i-turbo', 'wanx-v1', 'wan2.2-t2i-flash', 'wan2.2-t2i-plus', 'wan2.5-t2i-preview', 'wan2.7-t2i-plus' ] async function tryImageSynthesis(model) { const url = PUBLIC_BASE + '/api/v1/services/aigc/text2image/image-synthesis' try { const r = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json', 'X-DashScope-Async': 'enable' }, body: JSON.stringify({ model, input: { prompt: promptA }, parameters: { size: '1024*576', n: 1 } }), signal: AbortSignal.timeout(25000) }) const t = await r.text() let j = null try { j = JSON.parse(t) } catch {} if (r.status === 200 && j && j.output && j.output.task_id) { console.log(' ✓ ' + model + ' task=' + j.output.task_id) return j.output.task_id } const msg = (j && (j.message || (j.output && j.output.task_status))) || t.slice(0, 160) console.log(' ✗ ' + model + ' HTTP ' + r.status + ' ' + String(msg).slice(0, 160)) return null } catch (e) { console.log(' ✗ ' + model + ' ERR ' + e.message) return null } } async function poll(taskId, maxTry) { const url = PUBLIC_BASE + '/api/v1/tasks/' + taskId for (let i = 0; i < (maxTry || 20); i++) { await new Promise((r) => setTimeout(r, 3000)) const r = await get(url, { Authorization: 'Bearer ' + KEY }) let j = null try { j = JSON.parse(r.body) } catch {} const st = j && j.output && j.output.task_status if (st === 'SUCCEEDED') { const results = (j.output.results || []).map((x) => x.url).filter(Boolean) return { status: st, urls: results } } if (st === 'FAILED' || st === 'UNKNOWN') { return { status: st, msg: j.message || (j.output && j.output.message) } } } return { status: 'TIMEOUT' } } /* ---------- 3. 探多模态生成端点(qwen-image 走 messages) ---------- */ const QWEN_IMAGE_MODELS = ['qwen-image', 'qwen-image-plus', 'qwen-image-max'] async function tryMultimodal(model) { const url = PUBLIC_BASE + '/api/v1/services/aigc/multimodal-generation/generation' try { const r = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, input: { messages: [{ role: 'user', content: [{ text: promptA }] }] }, parameters: { size: '1024*576', n: 1 } }), signal: AbortSignal.timeout(40000) }) const t = await r.text() let j = null try { j = JSON.parse(t) } catch {} const content = j && j.output && j.output.choices && j.output.choices[0] && j.output.choices[0].message && j.output.choices[0].message.content const urls = Array.isArray(content) ? content.map((c) => c.image).filter(Boolean) : [] if (r.status === 200 && urls.length) { console.log(' ✓ ' + model + ' 直接返回图片'); return { status: 'SUCCEEDED', urls } } if (r.status === 200 && j && j.output && j.output.task_id) { console.log(' ✓ ' + model + ' task=' + j.output.task_id); return { status: 'TASK', task: j.output.task_id } } const msg = (j && j.message) || t.slice(0, 160) console.log(' ✗ ' + model + ' HTTP ' + r.status + ' ' + String(msg).slice(0, 160)) return null } catch (e) { console.log(' ✗ ' + model + ' ERR ' + e.message); return null } } /* ---------- 4. 下载两张图,比对 MD5 ---------- */ async function download(url, name) { const r = await fetch(url, { signal: AbortSignal.timeout(60000) }) if (!r.ok) throw new Error('HTTP ' + r.status) const buf = Buffer.from(await r.arrayBuffer()) const file = path.join(OUT, name) fs.writeFileSync(file, buf) return { file, size: buf.length, md5: crypto.createHash('md5').update(buf).digest('hex') } } async function main() { const available = await listModels() console.log('\n======== 2. text2image/image-synthesis 候选模型 ========') const hit = [] for (const m of WANX_MODELS) { const t = await tryImageSynthesis(m) if (t) hit.push({ model: m, task: t, kind: 'synthesis' }) } console.log('\n======== 3. multimodal-generation 候选模型 ========') for (const m of QWEN_IMAGE_MODELS) { if (available.length && available.indexOf(m) < 0) { console.log(' - ' + m + ' 不在 MaaS 模型列表,仍试一次'); } const r = await tryMultimodal(m) if (r && r.status === 'SUCCEEDED') hit.push({ model: m, urls: r.urls, kind: 'multimodal' }) else if (r && r.status === 'TASK') hit.push({ model: m, task: r.task, kind: 'multimodal' }) } if (!hit.length) { console.log('\n结论:没有可用的文生图模型组合。'); return } const first = hit[0] console.log('\n======== 4. 用可用模型 ' + first.model + ' 跑两张不同 prompt,比对 MD5 ========') let urls = first.urls if (!urls) { const p = await poll(first.task, 30) console.log('poll:', p.status, p.msg || '') urls = p.urls || [] } if (!urls.length) { console.log('未拿到图片 URL,无法比对'); return } console.log('URL 数 =', urls.length, urls[0].slice(0, 100)) const a1 = await download(urls[0], 'probe_A.jpg') console.log('A:', a1.size, a1.md5) // 换 prompt 再生成一张 let taskB = null if (first.kind === 'synthesis') taskB = await tryImageSynthesisWith(first.model, promptB) if (taskB) { const p = await poll(taskB, 30) if (p.urls && p.urls.length) { const b1 = await download(p.urls[0], 'probe_B.jpg') console.log('B:', b1.size, b1.md5) console.log('\n两张图 MD5 是否相同:', a1.md5 === b1.md5 ? '❌ 相同(模型没区分 prompt)' : '✓ 不同(模型正常按 prompt 出图)') } else console.log('B 图未生成:', p.status) } } async function tryImageSynthesisWith(model, prompt) { const url = PUBLIC_BASE + '/api/v1/services/aigc/text2image/image-synthesis' try { const r = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json', 'X-DashScope-Async': 'enable' }, body: JSON.stringify({ model, input: { prompt }, parameters: { size: '1024*576', n: 1 } }), signal: AbortSignal.timeout(25000) }) const j = JSON.parse(await r.text()) return j && j.output && j.output.task_id ? j.output.task_id : null } catch { return null } } main().catch((e) => console.error('FATAL', e && e.message))