2026-04-30 21:18:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Pipeline 共享工具函数与路径常量
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所有 phase/command 模块共用:配置加载、文件操作、路径计算、日志
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs')
|
|
|
|
|
|
const path = require('path')
|
|
|
|
|
|
|
|
|
|
|
|
// 路径常量(基于 lib/ 的父目录 scripts/)
|
|
|
|
|
|
const SCRIPTS_DIR = path.join(__dirname, '..')
|
|
|
|
|
|
const SKILLS_DIR = path.join(SCRIPTS_DIR, '..')
|
|
|
|
|
|
const PROJECT_ROOT = path.join(SKILLS_DIR, '..', '..')
|
|
|
|
|
|
const CONFIG_PATH = path.join(SKILLS_DIR, 'config.json')
|
2026-05-01 00:44:18 +08:00
|
|
|
|
const ACCOUNTS_DIR = path.join(PROJECT_ROOT, '..', 'accounts')
|
2026-04-30 21:18:31 +08:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 配置 & Manifest
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function loadConfig() {
|
|
|
|
|
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadManifest(manifestPath) {
|
|
|
|
|
|
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveManifest(manifestPath, manifest) {
|
|
|
|
|
|
const tmp = manifestPath + '.tmp'
|
|
|
|
|
|
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2), 'utf-8')
|
|
|
|
|
|
fs.renameSync(tmp, manifestPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadAccountConfig(accountId) {
|
|
|
|
|
|
const accountPath = path.join(ACCOUNTS_DIR, accountId, 'account.json')
|
|
|
|
|
|
if (!fs.existsSync(accountPath)) throw new Error(`账号不存在: ${accountPath}`)
|
|
|
|
|
|
return JSON.parse(fs.readFileSync(accountPath, 'utf-8'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 参考图解析
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function getReferences(manifest, accountConfig) {
|
|
|
|
|
|
const result = { localPaths: [], urls: [] }
|
|
|
|
|
|
const accountId = accountConfig.id || manifest.account || ''
|
|
|
|
|
|
|
|
|
|
|
|
// 优先读 manifest.references(agent 创建时写入)
|
|
|
|
|
|
const refs = manifest.references || []
|
|
|
|
|
|
if (refs.length > 0) {
|
|
|
|
|
|
for (const ref of refs) {
|
|
|
|
|
|
if (ref.url) result.urls.push(ref.url)
|
|
|
|
|
|
if (ref.file) {
|
|
|
|
|
|
const localPath = path.isAbsolute(ref.file) ? ref.file : path.resolve(ref.file)
|
|
|
|
|
|
if (fs.existsSync(localPath)) {
|
|
|
|
|
|
result.localPaths.push(localPath)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log('images', `参考图不存在: ${ref.file}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result.localPaths.length > 0 || result.urls.length > 0) return result
|
|
|
|
|
|
log('images', 'manifest.references 全部无效,尝试 account fallback')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 00:44:18 +08:00
|
|
|
|
// Fallback 1: 从 account.json 的顶层 references 读取
|
|
|
|
|
|
const topRefs = accountConfig.references || []
|
|
|
|
|
|
for (const ref of topRefs) {
|
|
|
|
|
|
if (ref.url) result.urls.push(ref.url)
|
|
|
|
|
|
if (ref.file && accountId) {
|
|
|
|
|
|
const localPath = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file)
|
|
|
|
|
|
if (fs.existsSync(localPath)) {
|
|
|
|
|
|
result.localPaths.push(localPath)
|
2026-04-30 21:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result.localPaths.length > 0 || result.urls.length > 0) return result
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback 2: 扫描 account 的 references 目录
|
|
|
|
|
|
if (accountId) {
|
|
|
|
|
|
const refDir = path.join(ACCOUNTS_DIR, accountId, 'references')
|
|
|
|
|
|
if (fs.existsSync(refDir)) {
|
|
|
|
|
|
const files = fs.readdirSync(refDir).filter(f =>
|
|
|
|
|
|
/\.(png|jpg|jpeg|webp)$/i.test(f)
|
|
|
|
|
|
)
|
|
|
|
|
|
for (const f of files) {
|
|
|
|
|
|
result.localPaths.push(path.join(refDir, f))
|
|
|
|
|
|
}
|
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
|
log('images', `从 references 目录兜底扫描到 ${files.length} 个参考图`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (result.localPaths.length === 0 && result.urls.length === 0) {
|
|
|
|
|
|
log('images', '无参考图,将使用纯文生图模式')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 文件操作
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function ensureDir(dir) {
|
|
|
|
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function slugify(text) {
|
2026-05-01 00:44:18 +08:00
|
|
|
|
// 限制中文字符最多5个,其他字符(英文数字)最多10个
|
|
|
|
|
|
let chineseChars = []
|
|
|
|
|
|
let otherChars = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const char of text) {
|
|
|
|
|
|
if (/\p{Script=Han}/u.test(char)) {
|
|
|
|
|
|
// 中文字符
|
|
|
|
|
|
if (chineseChars.length < 5) {
|
|
|
|
|
|
chineseChars.push(char)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (/\w/u.test(char)) {
|
|
|
|
|
|
// 英文、数字
|
|
|
|
|
|
if (otherChars.length < 10) {
|
|
|
|
|
|
otherChars.push(char)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = chineseChars.concat(otherChars).join('')
|
|
|
|
|
|
return result || 'untitled'
|
2026-04-30 21:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renameGeneratedFile(oldRelPath, dir, seq, nameHint, suffix) {
|
|
|
|
|
|
if (!oldRelPath) return oldRelPath
|
|
|
|
|
|
const oldAbs = path.resolve(dir, oldRelPath)
|
|
|
|
|
|
if (!fs.existsSync(oldAbs)) return oldRelPath
|
|
|
|
|
|
const ext = path.extname(oldAbs)
|
|
|
|
|
|
const slug = nameHint ? slugify(nameHint) : ''
|
|
|
|
|
|
const tag = suffix ? `_${suffix}` : ''
|
|
|
|
|
|
const newName = slug
|
|
|
|
|
|
? `scene_${String(seq).padStart(2, '0')}_${slug}${tag}${ext}`
|
|
|
|
|
|
: `scene_${String(seq).padStart(2, '0')}${tag}${ext}`
|
|
|
|
|
|
const newAbs = path.join(path.dirname(oldAbs), newName)
|
|
|
|
|
|
if (oldAbs !== newAbs) {
|
|
|
|
|
|
try { fs.renameSync(oldAbs, newAbs) } catch (_) { return oldRelPath }
|
|
|
|
|
|
}
|
|
|
|
|
|
return path.relative(dir, newAbs).replace(/\\/g, '/')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 日志 & 路径
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function log(phase, msg) {
|
|
|
|
|
|
console.log(`[${phase}] ${msg}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getManifestDir(manifestPath) {
|
|
|
|
|
|
return path.dirname(path.resolve(manifestPath))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 14:41:28 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 文本切分
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function splitTextIntoSentences(text) {
|
2026-05-02 02:31:55 +08:00
|
|
|
|
// 在句号、感叹号、分号、逗号处断句——它们是口播语音的天然呼吸点。
|
|
|
|
|
|
const sentenceEnders = /[。!;,]/
|
2026-05-01 14:41:28 +08:00
|
|
|
|
|
|
|
|
|
|
const sentences = []
|
|
|
|
|
|
let current = ''
|
|
|
|
|
|
|
|
|
|
|
|
for (const char of text) {
|
|
|
|
|
|
current += char
|
|
|
|
|
|
|
|
|
|
|
|
if (sentenceEnders.test(char)) {
|
2026-05-02 02:31:55 +08:00
|
|
|
|
sentences.push(current.trim().replace(/[。!;,:?、——…]/g, ''))
|
2026-05-01 14:41:28 +08:00
|
|
|
|
current = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (current.trim()) {
|
2026-05-02 02:31:55 +08:00
|
|
|
|
sentences.push(current.trim().replace(/[。!;,:?、——…]/g, ''))
|
2026-05-01 14:41:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return sentences
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 21:18:31 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Exports
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
SCRIPTS_DIR,
|
|
|
|
|
|
SKILLS_DIR,
|
|
|
|
|
|
PROJECT_ROOT,
|
|
|
|
|
|
CONFIG_PATH,
|
|
|
|
|
|
ACCOUNTS_DIR,
|
|
|
|
|
|
loadConfig,
|
|
|
|
|
|
loadManifest,
|
|
|
|
|
|
saveManifest,
|
|
|
|
|
|
loadAccountConfig,
|
|
|
|
|
|
getReferences,
|
|
|
|
|
|
ensureDir,
|
|
|
|
|
|
slugify,
|
|
|
|
|
|
renameGeneratedFile,
|
2026-05-01 14:41:28 +08:00
|
|
|
|
splitTextIntoSentences,
|
2026-04-30 21:18:31 +08:00
|
|
|
|
log,
|
|
|
|
|
|
getManifestDir,
|
|
|
|
|
|
}
|