Files
probably-rendering-one-more…/scripts/publish-document-covers.mjs

43 lines
6.3 KiB
JavaScript

/** Run INSIDE the server using auth-cli.cjs run; no content or credentials are exported. */
import {createRequire} from 'node:module';
import {createHash} from 'node:crypto';
import {readFile,writeFile,mkdir} from 'node:fs/promises';import path from 'node:path';
const base=process.env.PROMPT_LOCAL_API||'http://127.0.0.1:3000',dir=process.env.PROMPT_COVER_BUNDLE||'/tmp/prompt-document-covers';
const catalog=JSON.parse(await readFile(path.join(dir,'catalog.json'),'utf8')),stateFile=process.env.PROMPT_COVER_STATE||'/tmp/prompt-document-cover-state.json';
let state;try{state=JSON.parse(await readFile(stateFile,'utf8'))}catch{state={assets:{},snapshots:{},done:[]}};
const save=()=>writeFile(stateFile,JSON.stringify(state),{mode:0o600});
async function api(route,options={}){const r=await fetch(base+route,options);if(!r.ok)throw Error(route+': '+r.status);return r.json()}
const json=(method,body)=>({method,headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
let allTags=await api('/api/tags');if(allTags.filter(t=>t.kind==='system').length!==2)throw Error('System tag migration is not deployed');
async function tag(name){let t=allTags.find(t=>t.name===name);if(!t){t=await api('/api/tags',json('POST',{name:'#'+name}));allTags.push(t)}return t.id;}
const sys=key=>{const t=allTags.find(t=>t.system_key===key);if(!t)throw Error('Missing system tag');return t.id};
async function upload(key,bytes,name){if(state.assets[key])return state.assets[key];const f=new FormData();f.set('kind','image');f.set('assetOnly','true');f.append('files',new Blob([bytes],{type:'image/png'}),name);const d=await api('/api/upload',{method:'POST',body:f});state.assets[key]=d.assets[0];await save();return d.assets[0]}
async function oldMedia(entry){if(entry.assets)return entry.assets;const assets=[];for(const m of entry.media){if(m.kind!=='image')continue;const u=new URL(m.url,base);if(u.origin!==new URL(base).origin)throw Error('Unexpected media origin');const r=await fetch(u);if(!r.ok)throw Error('Source image unavailable');assets.push({id:await upload(m.url,await r.arrayBuffer(),'original-reference.png'),role:'content'});}return assets;}
async function read(id){return (await api('/api/notes/'+id)).entry}
async function snapshot(entry){if(!state.snapshots[entry.id]){state.snapshots[entry.id]=entry;await save()}}
async function labels(entry,extra=[],system=[]){const ids=[];for(const name of new Set([...entry.categories,...extra]))ids.push(await tag(name));for(const s of entry.systemTags||[])ids.push(s.id);for(const key of system)ids.push(sys(key));return [...new Set(ids)]}
await mkdir(dir,{recursive:true});
for(const n of catalog.notes){if(state.done.includes(n.id))continue;const entry=await read(n.id);await snapshot(entry);const cover=await upload('cover:'+n.id,await readFile(path.join(dir,n.file)),n.file),assets=[{id:cover,role:'content'},...await oldMedia(entry)].filter((a,i,arr)=>arr.findIndex(b=>b.id===a.id)===i);
const extra=n.collection==='照片入皮影'?['中国风']:n.collection==='街头小人'?['贴纸']:[],tagIds=await labels(entry,extra,n.rebrand?['document-entry']:[]),markdown=n.rebrand?entry.editableMarkdown.replaceAll('waterfall-wiki','PROMPT'):entry.editableMarkdown;
if(entry.postSlug)await api('/api/notes/'+n.id,json('PUT',{title:entry.title,description:entry.description,markdown,tags:tagIds,assets,coverAssetId:cover,revision:entry.revision}));
else await api('/api/posts',json('POST',{publicId:n.id,slug:n.slug,kind:'article',title:entry.title,description:entry.description,text:markdown,tags:tagIds,assets,coverAssetId:cover}));
state.done.push(n.id);await save();
}
for(const n of [...catalog.roots,...catalog.entries.map(id=>({id}))]){const entry=await read(n.id);await snapshot(entry);await api('/api/notes/'+n.id,json('PATCH',{tags:await labels(entry,n.topic?[n.topic]:[],['document-entry']),revision:entry.revision}));}
for(const slug of catalog.references){
const r=await fetch(base+'/p/'+slug,{redirect:'manual'}),location=r.headers.get('location');if(!location?.startsWith('/explore/'))throw Error('Reference post unavailable');
const entry=await read(location.split('/').at(-1));await snapshot(entry);
let cover=state.assets['reference-cover:'+slug];
if(!cover){
const sharp=createRequire('/app/package.json')('sharp'),source=state.snapshots[entry.id].media.filter(m=>m.kind==='image'),cols=source.length>6?3:2,rows=Math.ceil(source.length/cols),cellW=Math.floor(756/cols),cellH=Math.min(295,Math.floor(815/rows)),parts=[];
for(let i=0;i<source.length;i++){const url=new URL(source[i].url,base);if(url.origin!==new URL(base).origin)throw Error('Unexpected reference origin');const image=await fetch(url);if(!image.ok)throw Error('Reference image unavailable');parts.push({input:await sharp(Buffer.from(await image.arrayBuffer())).resize(cellW-18,cellH-18,{fit:'contain',background:'#f4f0e6'}).png().toBuffer(),left:72+i%cols*cellW,top:245+Math.floor(i/cols)*cellH});}
const title=slug.endsWith('-original')?'原始贴纸素材':'画风参考素材';
const label=`<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1200"><g fill="#31473c" font-family="Arial,'Noto Sans CJK SC',sans-serif"><text x="72" y="87" font-size="24" letter-spacing="3">PROMPT / REFERENCES</text><text x="68" y="175" font-size="62" font-weight="600">${title}</text><text x="72" y="218" font-size="22">${source.length} 张原始图片 · 本页为索引封面</text><path d="M72 1104H828" stroke="#9da991"/><text x="72" y="1152" font-size="21">参考图 / 贴纸 · 原件在图包中保留</text></g></svg>`;
const bytes=await sharp({create:{width:900,height:1200,channels:3,background:'#f4f0e6'}}).composite([...parts,{input:Buffer.from(label),left:0,top:0}]).png().toBuffer();
cover=await upload('reference-cover:'+slug,bytes,slug+'-index.png');state.referenceHashes||={};state.referenceHashes[entry.id]=createHash('sha256').update(bytes).digest('hex');await save();
}
const assets=[{id:cover,role:'content'},...entry.assets].filter((a,i,arr)=>arr.findIndex(b=>b.id===a.id)===i);
await api('/api/notes/'+entry.id,json('PUT',{title:entry.title,description:entry.description,markdown:entry.editableMarkdown,tags:await labels(entry,['贴纸'],['reference-image']),assets,coverAssetId:cover,revision:entry.revision}));
}
console.log(JSON.stringify({independentCovers:catalog.notes.length,rootEntries:catalog.roots.length+catalog.entries.length,referencePosts:catalog.references.length,contentExported:false}));