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{ 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{ 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';}