54 lines
8.4 KiB
TypeScript
54 lines
8.4 KiB
TypeScript
import type {Tag} from './model';
|
|
import {systemTags,ordinaryTags,selectedSystemTags} from './system-tags';
|
|
import type {PoolClient} from 'pg';
|
|
import {noteDescriptions} from '@/content/note-descriptions';
|
|
import {guidanceDocuments,guidanceMarkdown} from '@/content/skills/guidance';
|
|
import {wikiPages,wikiMarkdown} from '@/content/wiki';
|
|
import {skillCatalog} from '@/content/skills/catalog';
|
|
import {publicPostId} from './model';
|
|
import {pageId,exploreUrl,guidanceUrl} from '@/content/page-links';
|
|
import {getPostById,discoveryDocuments,postsByPublicIds,publishedPublicIds} from './library';
|
|
import {backlinksFor,rankDocuments,discoveryPage,type SearchDocument,type Backlink} from './discovery';
|
|
import {applyStaticEdit,staticEdits,withRevision} from './note-edits';
|
|
export type ExploreMedia={kind:'image'|'video'|'audio';url:string;title:string;poster?:string};
|
|
export type ExploreEntry={archived?:boolean;id:string;title:string;description:string;imageCount:number;namespace:string;categories:string[];systemTags?:Tag[];markdown:string;media:ExploreMedia[];color:string;postSlug?:string;editableMarkdown?:string;revision?:string;assets?:{id:string;kind:string;role:string}[];coverAssetId?:string|null;backlinkCount?:number;backlinks?:Backlink[];searchExcerpt?:string};
|
|
export const staticEntries:ExploreEntry[]=[...guidanceDocuments.map(doc=>{const card=skillCatalog.find(c=>doc.path===c.id);return {id:pageId('guidance:'+doc.path),title:doc.title,description:noteDescriptions[doc.path]||doc.blocks.find(b=>b.type==='paragraph')?.text||doc.title,imageCount:new Set([...(card?[card.image]:[]),...doc.resources.filter(r=>r.kind==='image').map(r=>r.href)]).size,namespace:'Main',categories:[doc.collection,...(doc.collection==='照片入皮影'?['中国风']:doc.collection==='街头小人'?['贴纸']:[])],systemTags:card?[systemTags[0]]:[],markdown:guidanceMarkdown(doc.path)!,media:Array.from(new Map([...(card?[{kind:'image' as const,url:card.image,title:card.alt}]:[]),...doc.resources.filter(r=>r.kind==='image').map(r=>({kind:'image' as const,url:r.href,title:r.title}))].map(m=>[m.url,m])).values()),color:'#eee5dc'};}),...wikiPages.map(page=>({id:pageId('wiki:'+page.namespace+':'+page.slug),title:page.title,description:page.summary,imageCount:0,namespace:page.namespace,categories:page.categories,markdown:wikiMarkdown(page),media:[],color:page.color}))];
|
|
// Canonicalize document references, including absolute links; pictures remain explicit links.
|
|
export function canonicalMarkdown(text:string){return text.replace(/\]\((?:http:\/\/8\.211\.154\.134:18131)?\/skills\/([^ )]+)\)/g,(whole,path:string)=>{const clean=path.replace(/\/index\.md$/,'').replace(/\.md$/,'');return pageId('guidance:'+clean)?']('+guidanceUrl(clean)+'?agent_mode=True)':whole;});}
|
|
async function readEntry(id:string,client?:PoolClient):Promise<ExploreEntry|null>{
|
|
const validId=/^(?:[0-9a-f]{24}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.test(id);
|
|
const post=validId&&(process.env.DATABASE_URL||client||id.length===36)?await getPostById(id,client):null;
|
|
if(!post){const page=staticEntries.find(e=>e.id===id);if(!page)return null;const edits=await staticEdits(client);return withRevision(applyStaticEdit({...page,markdown:canonicalMarkdown(page.markdown)},edits.find(e=>e.page_id===id)));}
|
|
const orderedAssets=[...post.assets].sort((a,b)=>Number(b.id===post.cover_asset_id)-Number(a.id===post.cover_asset_id));
|
|
const media:ExploreMedia[]=orderedAssets.filter(a=>a.kind!=='text'&&a.role==='content').map(a=>({kind:a.kind as ExploreMedia['kind'],url:'/api/media/'+a.id,title:a.filename||post.title,...(a.kind==='video'&&a.variants.includes('poster')?{poster:'/api/media/'+post.assets.find(p=>p.source_asset_id===a.id&&p.derivation==='poster')?.id}:{})}));
|
|
const editableMarkdown=post.assets.filter(a=>a.kind==='text').map(a=>a.body||'').join('\n\n');
|
|
const body=editableMarkdown+'\n\n'+post.assets.filter(a=>a.derivation).map(a=>'['+({poster:'视频封面',audio_track:'独立音轨',silent_video:'静音视频'}[a.derivation||'']||a.derivation)+'](/api/media/'+a.id+')').join(' · ');
|
|
return withRevision({archived:post.archived||false,id:publicPostId(post),assets:post.assets.filter(a=>a.kind!=='text').map(a=>({id:a.id,kind:a.kind,role:a.role})),coverAssetId:post.cover_asset_id,title:post.title,editableMarkdown,description:post.description||body.slice(0,180)||post.title,imageCount:media.filter(m=>m.kind==='image').length,namespace:['image','video','audio'].includes(post.kind)?'File':'Main',categories:ordinaryTags(post.tags).map(t=>t.name),systemTags:selectedSystemTags(post.tags),markdown:'# '+post.title+'\n\n'+body,media,color:'#eee5dc',postSlug:post.slug});
|
|
}
|
|
export async function discoveryCorpus(client?:PoolClient){
|
|
const [stored,edits,publishedIds]=await Promise.all([process.env.DATABASE_URL||client?discoveryDocuments(client):Promise.resolve([]),staticEdits(client),process.env.DATABASE_URL||client?publishedPublicIds(client):Promise.resolve([])]);
|
|
// Archived database notes must still suppress their file-backed predecessors.
|
|
const published=new Set(publishedIds);
|
|
const pages=staticEntries.filter(e=>!published.has(e.id)).map(e=>applyStaticEdit(e,edits.find(edit=>edit.page_id===e.id)));
|
|
const documents:SearchDocument[]=[...pages.map(e=>({id:e.id,title:e.title,description:e.description,markdown:canonicalMarkdown(e.markdown),categories:e.categories,systemTags:e.systemTags?.flatMap(t=>[t.name,t.system_key||'']),namespace:e.namespace,createdAt:''})),...stored.map(p=>({id:p.public_id||p.id,aliases:[p.id],title:p.title,description:p.description,markdown:canonicalMarkdown(p.markdown),categories:p.categories,systemTags:(p.system_tags||[]).flatMap((t:Tag)=>[t.name,t.system_key||'']),namespace:['image','video','audio'].includes(p.kind)?'File':'Main',createdAt:new Date(p.created_at).toISOString()}))];
|
|
return {documents,pages};
|
|
}
|
|
export async function getEntry(id:string,client?:PoolClient):Promise<ExploreEntry|null>{
|
|
const entry=await readEntry(id,client);if(!entry)return null;
|
|
const {documents}=await discoveryCorpus(client),refs=backlinksFor(documents).get(entry.id)||[];
|
|
return {...entry,backlinkCount:refs.length,backlinks:refs};
|
|
}
|
|
export async function exploreFeed(q='',category='',cursor?:string){
|
|
const {documents,pages}=await discoveryCorpus(),ranked=rankDocuments(documents,q,category),page=discoveryPage(ranked,q,category,cursor);
|
|
const staticIds=new Set(pages.map(e=>e.id)),posts=await postsByPublicIds(page.rows.filter(r=>!staticIds.has(r.doc.id)).map(r=>r.doc.id));
|
|
const entries:ExploreEntry[]=page.rows.map(({doc,backlinkCount})=>{
|
|
const original=pages.find(e=>e.id===doc.id);const post=posts.find(p=>publicPostId(p)===doc.id);
|
|
if(original)return {...original,backlinkCount,...(q?{searchExcerpt:excerpt(doc.markdown,q)}:{})};
|
|
if(!post)throw Error('Search result disappeared');
|
|
const cover=post.cover_asset_id||post.assets.find(a=>a.kind==='image'&&a.role==='content')?.id,video=post.assets.find(a=>a.kind==='video'),poster=post.assets.find(a=>a.source_asset_id===video?.id&&a.derivation==='poster')?.id;
|
|
return {id:doc.id,title:doc.title,description:doc.description||doc.markdown.slice(0,180)||doc.title,imageCount:post.assets.filter(a=>a.kind==='image'&&a.role==='content').length,namespace:doc.namespace,categories:doc.categories,systemTags:selectedSystemTags(post.tags),markdown:'',media:cover?[{kind:'image' as const,url:'/api/media/'+cover,title:doc.title}]:poster?[{kind:'image' as const,url:'/api/media/'+poster,title:doc.title}]:[],color:'#eee5dc',backlinkCount,...(q?{searchExcerpt:excerpt(doc.markdown,q)}:{})};
|
|
});return {entries,nextCursor:page.nextCursor,total:page.total,sort:q?'relevance,backlinks,newest':'backlinks,newest'};
|
|
}
|
|
function excerpt(markdown:string,q:string){const clean=markdown.replace(/[`#*_[\]<>]/g,'').replace(/\s+/g,' '),term=q.trim().replace(/^"|"$/g,'').split(/\s+/)[0]||'',at=clean.toLocaleLowerCase().indexOf(term.toLocaleLowerCase());return (at>50?'…':'')+clean.slice(Math.max(0,at-50),Math.max(0,at-50)+180);}
|
|
export function entryText(entry:ExploreEntry){return `${entry.markdown}\n\nArchived: ${entry.archived?'true':'false'}\nDescription: ${entry.description}\nBacklinks: ${entry.backlinkCount??0}\n${(entry.backlinks||[]).map(b=>'引用来源:['+b.title+']('+exploreUrl(b.id)+')').join('\n')}\nSystem tags: ${(entry.systemTags||[]).map(t=>t.name).join(', ')}\nNamespace: ${entry.namespace}\nCategories: ${entry.categories.join(', ')}\n\n`+entry.media.map(m=>`- ${m.kind}: [${m.title}](${m.url})`).join('\n')+'\n\n[网页]('+exploreUrl(entry.id)+')\n';}
|