46 lines
3.8 KiB
TypeScript
46 lines
3.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import { chapters } from "../content/learning-skills/data";
|
|
import { transaction } from "./db";
|
|
import { putFile } from "./storage";
|
|
|
|
function stableId(name: string) {
|
|
const bytes=createHash("sha256").update(`prompt:learning-series:${name}`).digest().subarray(0,16);
|
|
bytes[6]=(bytes[6]&15)|80;bytes[8]=(bytes[8]&63)|128;
|
|
const hex=bytes.toString("hex");return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
|
|
}
|
|
// Explicit editorial publication, never part of the generic storage bootstrap.
|
|
// Re-running updates these three authored posts without duplicating posts or media.
|
|
export async function publishLearningSeries() {
|
|
const path="public/learning-skills/cover.png";
|
|
const cover=await readFile(path);const digest=createHash("sha256").update(cover).digest("hex");
|
|
const storageKey=`editorial/learning-skills/${digest}.png`;
|
|
const coverId=stableId("cover");
|
|
await putFile(storageKey,path,"image/png");
|
|
return transaction(async client=>{
|
|
await client.query("SELECT pg_advisory_xact_lock(18131002)");
|
|
await client.query(`INSERT INTO assets(id,kind,filename,storage_key,mime_type,byte_size,sha256,width,height)
|
|
VALUES($1,'image','学习资料自己做-系列共用封面.png',$2,'image/png',$3,$4,1080,1440)
|
|
ON CONFLICT(id) DO UPDATE SET storage_key=excluded.storage_key,byte_size=excluded.byte_size,sha256=excluded.sha256`,[coverId,storageKey,cover.length,digest]);
|
|
const root=(await client.query(`INSERT INTO tags(id,parent_id,slug,name) VALUES($1,NULL,'learning-skills','学习工具')
|
|
ON CONFLICT(parent_id,slug) DO UPDATE SET name=excluded.name RETURNING id`,[stableId("tag-root")])).rows[0].id;
|
|
const published:string[]=[];
|
|
for(const chapter of chapters){
|
|
const child=(await client.query(`INSERT INTO tags(id,parent_id,slug,name) VALUES($1,$2,$3,$4)
|
|
ON CONFLICT(parent_id,slug) DO UPDATE SET name=excluded.name RETURNING id`,[stableId(`tag-${chapter.key}`),root,chapter.number==="01"?"notes":chapter.number==="02"?"teaching":"english",chapter.number==="01"?"笔记复习":chapter.number==="02"?"教学资料":"英语学习"])).rows[0].id;
|
|
const result=await client.query(`INSERT INTO posts(id,slug,kind,title,page_key,cover_asset_id) VALUES($1,$2,'article',$3,$4,$5)
|
|
ON CONFLICT(slug) DO UPDATE SET title=excluded.title,cover_asset_id=excluded.cover_asset_id,updated_at=now()
|
|
WHERE posts.page_key=excluded.page_key AND posts.kind='article' RETURNING id`,[stableId(chapter.key),chapter.slug,chapter.title,chapter.key,coverId]);
|
|
if(!result.rows[0])throw new Error(`Publication slug is already used by another post: ${chapter.slug}`);
|
|
const postId=result.rows[0].id;const captionId=stableId(`caption-${chapter.key}`);
|
|
const caption=`学习资料自己做 · 第 ${chapter.number} 辑\n${chapter.subtitle}。三篇共用系列封面,每篇 8 页,介绍 10 项工具。\n\n${chapter.skills.map((skill,i)=>`${String(i+1).padStart(2,"0")} ${skill.name} — ${skill.title}`).join("\n")}\n\n根据 GitHub 项目说明整理,尚未逐项安装实测。内页小样为排版示意;可在末页访问全部来源。`;
|
|
await client.query("INSERT INTO assets(id,kind,body) VALUES($1,'text',$2) ON CONFLICT(id) DO UPDATE SET body=excluded.body",[captionId,caption]);
|
|
await client.query(`INSERT INTO post_assets(post_id,asset_id,position,role) VALUES($1,$2,0,'content'),($1,$3,1,'caption')
|
|
ON CONFLICT(post_id,position) DO UPDATE SET asset_id=excluded.asset_id,role=excluded.role`,[postId,coverId,captionId]);
|
|
await client.query("INSERT INTO post_tags(post_id,tag_id) VALUES($1,$2) ON CONFLICT DO NOTHING",[postId,child]);
|
|
published.push(chapter.slug);
|
|
}
|
|
return {published,coverId};
|
|
});
|
|
}
|