feat: 新增高铁游与车宿徒步游技能及相关基础设施

This commit is contained in:
yangxiangyuan
2026-09-30 13:47:51 +08:00
parent bbc7ca6523
commit ac475b6659
66 changed files with 14572 additions and 0 deletions
@@ -0,0 +1,233 @@
/* ============================================================
* 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))