diff --git a/apps/wiki/src/lib/api-documentation.ts b/apps/wiki/src/lib/api-documentation.ts index 1aabff9..9513326 100644 --- a/apps/wiki/src/lib/api-documentation.ts +++ b/apps/wiki/src/lib/api-documentation.ts @@ -22,7 +22,7 @@ export const apiDocuments:Record={ "读取与编辑笔记", "GET, PUT", "路径参数:id,笔记稳定 ID(publicId 或存储 UUID)。PUT 使用 application/json,以下字段必填:title(1–200 字符)、description(最多 1000 字符)、markdown(最多 100000 字符)、tags(最多 50 个标签 UUID)、revision(GET 返回的 entry.revision,64 位十六进制)。可选 assets(最多 100 项 {id:媒体 UUID,role:content|caption},替换媒体关联,原二进制不删除);coverAssetId(图片 UUID 或 null,必须为本笔记 content 图片;null 清除显式封面)。静态代码页须先通过创建接口保存为数据库笔记,才可更新媒体。", - "GET 返回 {entry,tags},entry.backlinks 为引用来源 {id,title} 数组,entry.backlinkCount 为其长度;只计算正文中本站 post 的 Markdown 链接(含引用式链接),排除代码块、图片、外站链接及自引用。entry.assets 为媒体关联、entry.coverAssetId 为显式封面;PUT 返回更新后的 {entry}。先 GET 获取 revision,再 PUT;冲突后重新读取。", + "GET 返回 {entry,tags},entry.backlinks 为引用来源 {id,title} 数组,entry.backlinkCount 为其长度;只计算正文中本站 post 的 Markdown 链接(含引用式链接和粘贴的完整网址),排除代码块、图片、外站链接及自引用。entry.assets 为媒体关联、entry.coverAssetId 为显式封面;PUT 返回更新后的 {entry}。先 GET 获取 revision,再 PUT;冲突后重新读取。", "GET /api/notes/{id}\nPUT /api/notes/{id}\n{\"title\":\"标题\",\"description\":\"简介\",\"markdown\":\"正文\",\"tags\":[],\"revision\":\"\"}", "400:输入错误;404:笔记不存在;409:版本冲突。" ], diff --git a/apps/wiki/src/lib/discovery.ts b/apps/wiki/src/lib/discovery.ts index aca45a9..f003897 100644 --- a/apps/wiki/src/lib/discovery.ts +++ b/apps/wiki/src/lib/discovery.ts @@ -7,8 +7,9 @@ export function documentLinks(markdown:string):string[]{ const tree=fromMarkdown(markdown),definitions=new Map(),links:string[]=[]; function visit(node:typeof tree|typeof tree.children[number],collect:boolean){ if(node.type==='definition'&&!collect)definitions.set(normalize(node.identifier),node.url); - if(collect&&node.type==='link')links.push(node.url); - if(collect&&node.type==='linkReference'){const url=definitions.get(normalize(node.identifier));if(url)links.push(url);} + if(collect&&node.type==='link'){links.push(node.url);return;} + if(collect&&node.type==='linkReference'){const url=definitions.get(normalize(node.identifier));if(url)links.push(url);return;} + if(collect&&node.type==='text')for(const match of node.value.matchAll(/https?:\/\/[^\s<>]+/g))links.push(match[0].replace(/[.,;:!?,。;:!?)\]}'"]+$/g,'')); if('children' in node)for(const child of node.children)visit(child as typeof tree.children[number],collect); } visit(tree,false);visit(tree,true);return links; diff --git a/apps/wiki/src/pages/api/index.ts b/apps/wiki/src/pages/api/index.ts index be0c96e..558beec 100644 --- a/apps/wiki/src/pages/api/index.ts +++ b/apps/wiki/src/pages/api/index.ts @@ -3,7 +3,7 @@ import {apiDocuments} from '@/lib/api-documentation'; export default function handler(req:NextApiRequest,res:NextApiResponse){ if(!['GET','HEAD'].includes(req.method||'')){res.setHeader('Allow','GET, HEAD');return res.status(405).end();} const entries=[...Object.entries(apiDocuments).map(([path,doc])=>({path,title:doc[0],methods:doc[1],documentation:path+'?agent_mode=True'})), - {path:'/studio/api/export/{slug}',title:'导出创作方案',methods:'GET',documentation:'/studio/api/export/{slug}?agent_mode=True'}, + {path:'/studio/api/export/{slug}',title:'封面渲染与方案导出',methods:'GET, POST',documentation:'/studio/api/export/{slug}?agent_mode=True'}, {path:'/studio/api/health',title:'创作站健康检查',methods:'GET, HEAD',documentation:'/studio/api/health?agent_mode=True'}]; res.setHeader('Cache-Control','no-store'); const plain=typeof req.query.agent_mode==='string'&&req.query.agent_mode.toLowerCase()==='true'; diff --git a/apps/wiki/tests/discovery.test.ts b/apps/wiki/tests/discovery.test.ts index 15f411e..9faed23 100644 --- a/apps/wiki/tests/discovery.test.ts +++ b/apps/wiki/tests/discovery.test.ts @@ -19,3 +19,8 @@ test('default order favors inbound links and filters the entire corpus before pa assert.equal(first.total,26);assert.equal(first.rows.length,24);assert.equal(second.rows.length,2);assert.equal(second.nextCursor,null);assert.equal(new Set([...first.rows,...second.rows].map(r=>r.doc.id)).size,26); assert.throws(()=>discoveryPage(filtered,'different','wanted',first.nextCursor!),InvalidDiscoveryCursor);assert.throws(()=>discoveryPage(filtered,'','wanted','bad'),InvalidDiscoveryCursor); }); + +test('pasted full URLs count but disguised link labels and inline code do not',()=>{ + const source=doc('b',{markdown:'http://8.211.154.134:18131/explore/a。\n\n[http://8.211.154.134:18131/explore/c](https://example.com)\n\n`http://8.211.154.134:18131/explore/c`'}); + const refs=backlinksFor([doc('a'),source,doc('c')]);assert.equal(refs.get('a')?.length,1);assert.equal(refs.get('c')?.length,0); +});