Pair every guidance page with Markdown and enable shared document references
PROMPT Docker CI and deployment / verify-and-deploy (push) Successful in 16m58s
PROMPT Docker CI and deployment / verify-and-deploy (push) Successful in 16m58s
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# 构建与入口
|
||||
|
||||
人类默认从 `/` 进入;Agent 从 `/agent` 或 `/agent/index.md` 读取 `text/markdown` 索引,根目录 `/llms.txt` 提供发现链接。索引复用技能目录,技能继续使用 `/skills/<id>/SKILL.md` 和现有子文档 URL,避免复制文档。
|
||||
人类默认从 `/` 进入;对应 Markdown 根入口为 `/index.md`,指导文档目录为 `/skills` 与 `/skills/index.md`。每篇网页地址后加 `/index.md` 即为同内容的 Markdown,允许跨分组引用。`/agent`、`/agent/index.md` 和旧技能文档 URL 兼容保留;`/llms.txt` 提供发现链接。详见 `docs/guidance.md`。
|
||||
|
||||
`ces-control` 必须预装 Docker、Compose,并允许 Runner 访问持久 Docker daemon。CI 只检查环境,不安装系统包、不启停主机服务。当前服务器没有 buildx,先继续使用 daemon 自带层缓存;不要直接粘贴依赖 GitHub cache 服务的 `type=gha` 参数。后续若换 Docker daemon 或清理缓存,会重新冷构建。
|
||||
|
||||
Dockerfile 保持单次 COPY,`.dockerignore` 排除非构建输入;根 Markdown 和 docs 不再使应用构建层失效。push 和 PR 对纯文档改动使用 paths,但 public 下的 Markdown 属于产品资源,必须构建部署。public 下的技能 Markdown 继续纳入构建。旧 builder 每条 COPY 实测约 47 秒,避免拆成多条 COPY 增加快照成本。仍保留类型检查、单元测试、smoke 与完整隔离栈集成测试。
|
||||
Dockerfile 保持单次 COPY,`.dockerignore` 排除非构建输入;根 Markdown 和 docs 不再使应用构建层失效。push 和 PR 对纯文档改动使用 paths,但 public 下的 Markdown 属于产品资源,必须构建部署。public 下的技能 Markdown 继续纳入构建。旧 builder 每条 COPY 实测约 47 秒,避免拆成多条 COPY 增加快照成本。完整路径保留类型检查、单元测试、smoke 与完整隔离栈集成测试。
|
||||
|
||||
当前页面为 Next.js SSR 与编译组件,改 TSX 仍需 next build。把 HTML 的 COPY 移到最后不能避免编译。独立挂载静态文件会破坏镜像版本与回滚的一致性;只有真正独立、无需编译的内容才适合后续拆成单独发布渠道。
|
||||
当前页面为 Next.js SSR 与编译组件,改 TSX 仍需 next build。Web 快速路径只编译生产 Web,并以不可变的版本目录挂载完整 standalone 产物,保持版本与回滚一致;不重新编译 worker 或导出运行镜像。
|
||||
|
||||
## Web 快速发布
|
||||
|
||||
@@ -16,4 +16,4 @@ Web 路径复用当前 runtime 对应的已验证 integration 镜像作为工具
|
||||
|
||||
完整 standalone 产物存入 `/opt/prompt/web-artifacts/<sha>.<run>`,只读挂载到 Web 容器;仅重建 Web,保留 runtime 镜像、worker、Postgres 和 RustFS。切换前检查 main 与当前发布,部署锁与全量流程共用;失败自动恢复旧 Web Compose 配置。发布文件保留变量表达式,不写入生产秘密。健康检查通过后切换 current;原完整发布也能回滚到 Web 快速发布。
|
||||
|
||||
首次接入流水线变更仍走完整验证;后续 Web 更新速度需要实测,不能承诺 HMR 的毫秒级反馈。每次发布完整 Web 产物,不把个别 chunk 混入正在运行的 Next.js 版本。
|
||||
首次接入流水线变更仍走完整验证。2026-10-23 workflow 291 实测:Web 构建与发布91秒,拉取代码44秒,合计2分15秒,不包含共享 Runner 排队。运行镜像复用,worker、Postgres、RustFS 容器 ID 均不变;编译缓存已持久化。不能承诺 HMR 的毫秒级反馈。每次发布完整 Web 产物,不把个别 chunk 混入正在运行的 Next.js 版本。
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 指导文档的两种呈现
|
||||
|
||||
指导文档共用一份内容。每篇都有独立网页 URL,后面加 `/index.md` 就是 Markdown 版本:
|
||||
|
||||
| 网页 | Markdown |
|
||||
| --- | --- |
|
||||
| `/` | `/index.md` |
|
||||
| `/skills` | `/skills/index.md` |
|
||||
| `/skills/shadow-puppet` | `/skills/shadow-puppet/index.md` |
|
||||
| `/skills/shadow-puppet/prompt` | `/skills/shadow-puppet/prompt/index.md` |
|
||||
| `/skills/image-delivery` | `/skills/image-delivery/index.md` |
|
||||
|
||||
这里的 skills 指指导文档,不要求使用外部技能包规范,也不要求先从某个 SKILL.md 进入。旧 `/agent`、`/agent/index.md`、`/tools/skills/...`、`/skills/<name>/SKILL.md` 和 `<doc>.md` 地址兼容保留。
|
||||
|
||||
`src/content/skills/guidance.ts` 是全局平面文档注册表。路径是文档的稳定标识;分组只用于浏览,不隔离引用。已有两套文档继续从 catalog 读取内容,注册表为它们生成成对 URL、共享资源引用和反向引用;新增独立文档直接注册唯一 path,无需归属某个技能。
|
||||
|
||||
网页正文的文档链接进入网页,Markdown 正文的文档链接进入 Markdown。图片和 API 继续使用原始资源地址。复制按钮复制当前这一篇的 Markdown 地址,每页 HTML head 也声明对应地址。反向引用从正文链接计算,两种呈现显示相同关系。
|
||||
|
||||
共用的 image-delivery 文档由 street-people/checks 和 shadow-puppet/delivery 引用,同时链接回各自的具体验收协议。它证明引用可以跨分组,不复制内容。
|
||||
+8
-1
@@ -2,7 +2,14 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ["playwright-core"],
|
||||
async rewrites(){return [{source:"/agent",destination:"/api/agent-index"},{source:"/agent/index.md",destination:"/api/agent-index"},{source:"/skills/:skill/:doc.md",destination:"/api/skill-docs/:skill/:doc"}]},
|
||||
async rewrites(){return [
|
||||
{source:"/index.md",destination:"/api/guidance"},
|
||||
{source:"/skills/index.md",destination:"/api/guidance/skills"},
|
||||
{source:"/skills/:path+/index.md",destination:"/api/guidance/skills/:path+"},
|
||||
{source:"/agent",destination:"/api/agent-index"},
|
||||
{source:"/agent/index.md",destination:"/api/agent-index"},
|
||||
{source:"/skills/:skill/:doc.md",destination:"/api/skill-docs/:skill/:doc"},
|
||||
]},
|
||||
output: "standalone",
|
||||
outputFileTracingIncludes: {"/*":["./node_modules/playwright-core/**/*"]},
|
||||
poweredByHeader: false,
|
||||
|
||||
+6
-3
@@ -1,6 +1,9 @@
|
||||
# PROMPT
|
||||
|
||||
> 素材与创作技能网站。
|
||||
> 创作素材与可相互引用的指导文档。
|
||||
|
||||
- [Agent 默认入口](/agent/index.md): Markdown 技能索引与任务导航。
|
||||
- [人类入口](/): 网页浏览与素材管理。
|
||||
- [网站 Markdown 入口](/index.md)
|
||||
- [全部指导文档](/skills/index.md)
|
||||
- [人类入口](/)
|
||||
|
||||
每篇指导文档的网页地址加 /index.md,即为一一对应的 Markdown。沿文档链接继续阅读,可直接进入任意一篇,不必经过某个技能入口。
|
||||
|
||||
@@ -31,7 +31,7 @@ export function Feed({initial,section='',tagPath=''}:{initial:FeedData;section?:
|
||||
useEffect(()=>{generation.current++;setPosts(initial.posts);setCursor(initial.nextCursor);setError('');setBusy(false);},[initial,router.query.q]);
|
||||
const current=sections.find(s=>s.path===section);const target=initial.tags.find(t=>t.path===tagPath);const children=initial.tags.filter(t=>target?t.parent_id===target.id:!t.parent_id);
|
||||
async function load(){const gen=generation.current;setBusy(true);setError('');try{const q=new URLSearchParams({section,path:tagPath,cursor:cursor||'',q:String(router.query.q||'')});const res=await fetch('/api/feed?'+q);const d=await res.json();if(!res.ok)throw new Error(d.error);if(gen===generation.current){setPosts(old=>[...old,...d.posts.filter((p:Post)=>!old.some(a=>a.id===p.id))]);setCursor(d.nextCursor);}}catch{if(gen===generation.current)setError('加载没有完成,点击重试。');}finally{if(gen===generation.current)setBusy(false);}}
|
||||
return <Shell><Head><title>{target?.name||current?.name||'灵感首页'} · PROMPT</title><meta name="description" content="自己的图文、视频与声音创作空间"/><link rel="alternate" type="text/markdown" href="/agent/index.md" title="Agent 入口"/></Head>
|
||||
return <Shell><Head><title>{target?.name||current?.name||'灵感首页'} · PROMPT</title><meta name="description" content="自己的图文、视频与声音创作空间"/><link rel="alternate" type="text/markdown" href="/index.md" title="Agent 文档入口"/></Head>
|
||||
{current&&<header className="page-header feed-heading"><div><h1>{target?.name||current.name}</h1><p>{current.hint}</p></div></header>}
|
||||
{!section&&<Heatmap data={initial.contributions}/>}
|
||||
{tagPath&&<div className="breadcrumbs"><Link href={`/${section}`}>全部{current?.name}</Link>{tagPath.split('/').map((_,i)=>{const path=tagPath.split('/').slice(0,i+1).join('/');return <span key={path}> / <Link href={`/${section}/${path}`}>{initial.tags.find(t=>t.path===path)?.name}</Link></span>;})}</div>}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import {Shell} from './Shell';
|
||||
import {SkillCard} from './SkillCard';
|
||||
import {skillCatalog} from '@/content/skills/catalog';
|
||||
import {guidanceDocuments,humanDocumentUrl} from '@/content/skills/guidance';
|
||||
export function GuidanceIndex(){return <Shell><Head><title>指导文档 · PROMPT</title><link rel="canonical" href="/skills"/><link rel="alternate" type="text/markdown" href="/skills/index.md" title="指导文档"/></Head>
|
||||
<header className="page-header"><div><h1>指导文档</h1><p>每篇都有独立地址,网页与 Markdown 一一对应。沿着引用,找到需要的方法。</p></div></header>
|
||||
<article className="docs-body"><h2>全部文档</h2><p>这些分组只帮助浏览。任何一篇都可以独立进入,也可以引用其他文档。</p><div className="guidance-directory">{guidanceDocuments.map(doc=><div key={doc.path}><Link href={humanDocumentUrl(doc.path)}>{doc.title}</Link><small>{doc.collection}</small><a href={`${humanDocumentUrl(doc.path)}/index.md`}>Markdown ↗</a></div>)}</div></article>
|
||||
<h2>按创作主题浏览</h2>
|
||||
<div className="skills-masonry">{skillCatalog.map(skill=><SkillCard key={skill.id} skill={skill}/>)}</div>
|
||||
</Shell>}
|
||||
@@ -6,7 +6,7 @@ const groups=[
|
||||
{label:'',items:[{href:'/',label:'首页',icon:Grid2X2}]},
|
||||
{label:'素材',items:[{href:'/images',label:'图片',icon:Image},{href:'/videos',label:'视频',icon:Film},{href:'/audio',label:'声音',icon:AudioLines}]},
|
||||
{label:'作品',items:[{href:'/articles',label:'图文',icon:BookOpen},{href:'/motion',label:'动态',icon:Clapperboard}]},
|
||||
{label:'工具',items:[{href:'/tools/skills',label:'技能',icon:BookOpen},{href:'/tools/doodle',label:'线条工具',icon:Sparkles}]},
|
||||
{label:'工具',items:[{href:'/skills',label:'指导文档',icon:BookOpen},{href:'/tools/doodle',label:'线条工具',icon:Sparkles}]},
|
||||
];
|
||||
export function Shell({children}:{children:ReactNode}){
|
||||
const router=useRouter();const [open,setOpen]=useState(false);const [query,setQuery]=useState(String(router.query.q||''));
|
||||
|
||||
@@ -3,12 +3,46 @@ import Link from 'next/link';
|
||||
import {useState,useEffect,useRef} from 'react';
|
||||
import {X,PanelRight,Copy,ExternalLink} from 'lucide-react';
|
||||
import {Shell} from './Shell';
|
||||
import {getSkill,skillDocumentMarkdown,type Resource} from '@/content/skills/catalog';
|
||||
export function SkillDocs({documentId='index',skillId='street-people'}:{documentId?:string;skillId?:string}){
|
||||
const skill=getSkill(skillId)!;const skillResources=skill.resources;const doc=skill.documents.find(d=>d.id===documentId)!;const [open,setOpen]=useState(false);const[selected,setSelected]=useState<Resource|null>(null);const[copied,setCopied]=useState(false);const panel=useRef<HTMLElement>(null);
|
||||
import {getGuidance,guidanceMarkdown,guidanceBacklinks,humanDocumentUrl,markdownDocumentUrl,legacyDocumentPath,type GuidanceResource} from '@/content/skills/guidance';
|
||||
|
||||
export function SkillDocs({documentPath,documentId='index',skillId='street-people'}:{documentPath?:string;documentId?:string;skillId?:string}){
|
||||
const path=documentPath??legacyDocumentPath(skillId,documentId);
|
||||
const doc=getGuidance(path)!;
|
||||
const [open,setOpen]=useState(false);
|
||||
const [selected,setSelected]=useState<GuidanceResource|null>(null);
|
||||
const [copied,setCopied]=useState(false);
|
||||
const panel=useRef<HTMLElement>(null);
|
||||
useEffect(()=>{setOpen(window.innerWidth>760)},[]);
|
||||
useEffect(()=>{setSelected(null);setCopied(false)},[skillId,documentId]);
|
||||
useEffect(()=>{setSelected(null);setCopied(false)},[path]);
|
||||
useEffect(()=>{if(!open)return;const handler=(e:KeyboardEvent)=>{if(e.key==='Escape')setOpen(false)};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler)},[open]);
|
||||
const resource=(id:string)=>skillResources.find(r=>r.id===id)!;
|
||||
return <Shell><Head><title>{`${doc.title} · 技能文档 · PROMPT`}</title></Head><div className="docs-top"><Link href="/tools/skills">← 全部技能</Link><button onClick={()=>setOpen(v=>!v)} aria-expanded={open}><PanelRight size={16}/>文档与来源 · {skillResources.length}</button></div><div className={`docs-layout ${open?'with-sources':''}`}><article className="docs-body"><div className="docs-eyebrow">{skill.title} / SKILL DOCUMENTS</div><h1>{doc.title}</h1><div className="docs-actions"><button onClick={async()=>{try{const value=new URL(skillResources.find(r=>r.id==='index')!.href,location.origin).href;if(navigator.clipboard)await navigator.clipboard.writeText(value);else{const input=document.createElement('textarea');input.value=value;document.body.appendChild(input);input.select();const ok=document.execCommand('copy');input.remove();if(!ok)throw Error('Copy unavailable');}setCopied(true)}catch{setCopied(false)}}}><Copy size={14}/>{copied?'已复制入口文档':'复制入口文档给 Agent'}</button><a href={resource(documentId).href} target="_blank" rel="noreferrer">纯文本版本 ↗</a></div><p className="docs-hint">网页与 Agent 纯文本版本来自同一份文档。参考图、子文档与接口均通过链接连接。</p>{doc.blocks.map((b,i)=>b.type==='heading'?<h2 key={i}>{b.text}</h2>:b.type==='code'?<pre key={i}>{b.text}</pre>:b.type==='links'?<div className="docs-inline-links" key={i}>{b.refs!.map(id=>{const r=resource(id);return <a href={r.human||r.href} key={id} onClick={e=>{e.preventDefault();setSelected(r);setOpen(true);panel.current?.focus()}}>{r.kind==='document'?'文档':r.kind==='image'?'图片':r.method} · {r.title}<ExternalLink size={12}/></a>})}</div>:<p key={i}>{b.text}</p>)}</article>{open&&<aside className="docs-sources" ref={panel} tabIndex={-1} aria-label="技能所有文档与来源"><header><div><b>文档与来源</b><small>当前技能的完整资源索引</small></div><button aria-label="关闭文档与来源" onClick={()=>setOpen(false)}><X size={18}/></button></header>{(['document','image','api'] as const).map(kind=><section key={kind}><h3>{kind==='document'?'文档':kind==='image'?'参考与示意图':'API 接口'}</h3>{skillResources.filter(r=>r.kind===kind).map(r=><button key={r.id} className={selected?.id===r.id?'selected':''} onClick={()=>setSelected(r)}><span>{r.method|| (r.kind==='image'?'IMG':'MD')}</span><div>{r.title}<small>{r.href}</small></div></button>)}</section>)}{selected&&<div className="docs-resource-preview"><h3>{selected.title}</h3>{selected.kind==='image'?<img src={selected.href} alt={selected.title}/>:selected.kind==='document'?<pre>{skillDocumentMarkdown(skillId,selected.id)}</pre>:<><code>{selected.method} {selected.href}</code><pre>{JSON.stringify(selected.detail||{method:selected.method,path:selected.href},null,2)}</pre><p>{selected.description}</p></>}<div className="docs-resource-actions">{selected.human&&<Link href={selected.human}>打开文档网页 ↗</Link>}{selected.method!=='POST'&&!selected.template&&<a href={selected.href} target="_blank" rel="noreferrer">{selected.kind==='document'?'打开纯文本':'打开资源'} ↗</a>}</div></div>}</aside>}</div></Shell>;
|
||||
const resource=(id:string)=>doc.resources.find(r=>r.id===id)!;
|
||||
const incoming=guidanceBacklinks(path);
|
||||
async function copy(){
|
||||
try{
|
||||
const value=new URL(markdownDocumentUrl(path),location.origin).href;
|
||||
if(navigator.clipboard)await navigator.clipboard.writeText(value);
|
||||
else{const input=document.createElement('textarea');input.value=value;document.body.appendChild(input);input.select();const ok=document.execCommand('copy');input.remove();if(!ok)throw Error('Copy unavailable');}
|
||||
setCopied(true);
|
||||
}catch{setCopied(false)}
|
||||
}
|
||||
return <Shell><Head><title>{doc.title} · 指导文档 · PROMPT</title><link rel="canonical" href={humanDocumentUrl(path)}/><link rel="alternate" type="text/markdown" href={markdownDocumentUrl(path)} title={doc.title}/></Head>
|
||||
<div className="docs-top"><Link href="/skills">← 全部指导文档</Link><button onClick={()=>setOpen(v=>!v)} aria-expanded={open}><PanelRight size={16}/>文档与来源 · {doc.resources.length}</button></div>
|
||||
<div className={`docs-layout ${open?'with-sources':''}`}><article className="docs-body">
|
||||
<div className="docs-eyebrow">{doc.collection} / 指导文档</div><h1>{doc.title}</h1>
|
||||
<div className="docs-actions"><button onClick={copy}><Copy size={14}/>{copied?'已复制本文 Markdown 地址':'复制本文给 Agent'}</button><a href={markdownDocumentUrl(path)} target="_blank" rel="noreferrer">本文 Markdown ↗</a></div>
|
||||
<p className="docs-hint">网页与 Markdown 来自同一份文档。每篇都有独立地址,可以直接进入,也可以沿引用继续阅读。</p>
|
||||
{doc.blocks.map((b,i)=>b.type==='heading'?<h2 key={i}>{b.text}</h2>:b.type==='code'?<pre key={i}>{b.text}</pre>:b.type==='links'?<div className="docs-inline-links" key={i}>{b.refs!.map(id=>{
|
||||
const r=resource(id);
|
||||
return r.kind==='document'?<Link href={r.human!} key={id}>文档 · {r.title}<ExternalLink size={12}/></Link>:<a href={r.href} key={id} onClick={e=>{e.preventDefault();setSelected(r);setOpen(true);panel.current?.focus()}}>{r.kind==='image'?'图片':r.method} · {r.title}<ExternalLink size={12}/></a>;
|
||||
})}</div>:<p key={i}>{b.text}</p>)}
|
||||
{incoming.length>0&&<><h2>引用本文的文档</h2><div className="docs-inline-links">{incoming.map(d=><Link key={d.path} href={humanDocumentUrl(d.path)}>{d.title}<ExternalLink size={12}/></Link>)}</div></>}
|
||||
</article>
|
||||
{open&&<aside className="docs-sources" ref={panel} tabIndex={-1} aria-label="文档与资源"><header><div><b>文档与来源</b><small>本文的文档、参考图与接口</small></div><button aria-label="关闭文档与来源" onClick={()=>setOpen(false)}><X size={18}/></button></header>
|
||||
{(['document','image','api'] as const).map(kind=><section key={kind}><h3>{kind==='document'?'指导文档':kind==='image'?'参考与示意图':'API 接口'}</h3>{doc.resources.filter(r=>r.kind===kind).map(r=><button key={r.id} className={selected?.id===r.id?'selected':''} onClick={()=>setSelected(r)}><span>{r.method||(r.kind==='image'?'IMG':'MD')}</span><div>{r.title}<small>{r.human||r.href}</small></div></button>)}</section>)}
|
||||
{selected&&<div className="docs-resource-preview"><h3>{selected.title}</h3>{selected.kind==='image'?<img src={selected.href} alt={selected.title}/>:selected.kind==='document'?<pre>{guidanceMarkdown(selected.documentPath!)}</pre>:<><code>{selected.method} {selected.href}</code><pre>{JSON.stringify(selected.detail||{method:selected.method,path:selected.href},null,2)}</pre><p>{selected.description}</p></>}
|
||||
<div className="docs-resource-actions">{selected.human&&<Link href={selected.human}>打开文档网页 ↗</Link>}{selected.method!=='POST'&&!selected.template&&<a href={selected.href} target="_blank" rel="noreferrer">{selected.kind==='document'?'打开 Markdown':'打开资源'} ↗</a>}</div>
|
||||
</div>}
|
||||
</aside>}
|
||||
</div>
|
||||
</Shell>;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const skillDocuments:SkillDocument[]=[
|
||||
{id:'upload',title:'上传与返回结果',blocks:[{type:'paragraph',text:'先由 agent 生成 PNG,再以 multipart/form-data 提交。API 不负责绘图。'}, {type:'links',refs:['submit','spec']},{type:'code',text:`POST /api/skills/street-people\nContent-Type: multipart/form-data\n\n${Object.entries(streetPeople.upload.fields).map(([k,v])=>k+': '+v).join('\n')}\n\n201: { id, slug, url, assets }\n400: 文件或表单不符合规范\n409: slug 已存在\n413: 超出上传限制\n500: 服务器或存储错误`},{type:'heading',text:'调用示例'},{type:'code',text:`curl -X POST "$BASE/api/skills/street-people" \\\n -F 'title=街头小人 · 冬日片段' \\\n -F 'slug=winter-street-people' \\\n -F 'description=人物动作、提示词与生成来源' \\\n -F 'files=@person.png;type=image/png'`},{type:'paragraph',text:'最多 16 张,单文件 250 MB,总计 1 GB,每张最多 4000 万像素。重复 slug 返回 409,不会覆盖已有笔记。使用返回的 url 查看结果。'},{type:'links',refs:['checks','index']}]},
|
||||
{id:'checks',title:'交付检查清单',blocks:[{type:'paragraph',text:'逐张检查完整人物、纸片边缘、衣着与动作。外部需要真实透明像素 alpha=0,内部需要接近不透明像素 alpha≥250。仅增加 alpha 通道而没有透明背景的 PNG 会被拒绝。'}, {type:'paragraph',text:'检查无截肢、无额外人物、无文字、无背景和外部阴影。记录生成来源与提示词;来源不同的图包分开提交。服务器的像素检查不能代替 agent 对画风和内容的检查。'}, {type:'links',refs:['style','sample','upload','index']}]},
|
||||
];
|
||||
export const skillCatalog=[{id:'street-people',title:'街头小人',category:'图片生成 · 透明 PNG',description:'松散的线条,冬天的衣着。把街上的一个瞬间,剪成一张小贴纸。',image:'/skills/street-people/sample.png',alt:'手绘街头小人,透明剪纸素材',href:'/tools/skills/street-people',created_at:'2026-10-22T17:29:47Z'},{id:'shadow-puppet',title:'照片入皮影',category:'照片转译 · 3:4 海报',description:'把照片里的主角请上幕布,让它用一个姿态演完一整出戏。',image:'/skills/shadow-puppet/egret-example.png',alt:'白鹭照片与暖灯宣纸皮影的上下等分海报示例',href:'/tools/skills/shadow-puppet',created_at:'2026-10-23T03:45:00Z'}];
|
||||
export const skillCatalog=[{id:'street-people',title:'街头小人',category:'图片生成 · 透明 PNG',description:'松散的线条,冬天的衣着。把街上的一个瞬间,剪成一张小贴纸。',image:'/skills/street-people/sample.png',alt:'手绘街头小人,透明剪纸素材',href:'/skills/street-people',created_at:'2026-10-22T17:29:47Z'},{id:'shadow-puppet',title:'照片入皮影',category:'照片转译 · 3:4 海报',description:'把照片里的主角请上幕布,让它用一个姿态演完一整出戏。',image:'/skills/shadow-puppet/egret-example.png',alt:'白鹭照片与暖灯宣纸皮影的上下等分海报示例',href:'/skills/shadow-puppet',created_at:'2026-10-23T03:45:00Z'}];
|
||||
export type SkillDefinition={id:string;title:string;documents:SkillDocument[];resources:Resource[]};
|
||||
const definitions:Record<string,SkillDefinition>={
|
||||
'street-people':{id:'street-people',title:'街头小人',documents:skillDocuments,resources:skillResources.map(r=>r.kind==='api'?{...r,detail:r.method==='POST'?streetPeople.upload:streetPeople,description:r.method==='POST'?'上传接口。阅读上传文档后,由 Agent 使用 multipart/form-data 提交文件。此处仅展示说明,不会发送请求。':'返回 JSON 规范。入口文档负责引导流程,JSON 用于查阅接口结构。'}:r)},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {getSkill,skillCatalog,type Block,type Resource} from './catalog';
|
||||
|
||||
export type GuidanceResource=Resource&{documentPath?:string};
|
||||
export type GuidanceDocument={path:string;title:string;collection:string;blocks:Block[];resources:GuidanceResource[]};
|
||||
const origin='http://8.211.154.134:18131';
|
||||
export const humanDocumentUrl=(path:string)=>`/skills/${path}`;
|
||||
export const markdownDocumentUrl=(path:string)=>`${humanDocumentUrl(path)}/index.md`;
|
||||
export const legacyDocumentPath=(skill:string,doc:string)=>skill+(['index','SKILL'].includes(doc)?'':`/${doc}`);
|
||||
export const documentResource=(path:string,title:string,id=`doc:${path}`):GuidanceResource=>({id,kind:'document',title,documentPath:path,href:markdownDocumentUrl(path),human:humanDocumentUrl(path)});
|
||||
const shared=documentResource('image-delivery','图像交付与来源记录');
|
||||
|
||||
// A flat document registry. Collection names organize browsing, never restrict references.
|
||||
export const guidanceDocuments:GuidanceDocument[]=skillCatalog.flatMap(card=>{
|
||||
const skill=getSkill(card.id)!;
|
||||
const resources:GuidanceResource[]=skill.resources.map(r=>{
|
||||
if(r.kind!=='document')return {...r,id:`${card.id}:${r.id}`};
|
||||
const legacy=r.href.match(/^\/skills\/([^/]+)\/([^/]+)\.md$/);
|
||||
const path=r.href.startsWith('/skills/')&&r.href.endsWith('/index.md')?r.href.slice(8,-9)
|
||||
:legacy?legacyDocumentPath(legacy[1],legacy[2])
|
||||
:r.human?.startsWith('/skills/')?r.human.slice(8):legacyDocumentPath(card.id,r.id);
|
||||
const title=skill.documents.find(d=>legacyDocumentPath(card.id,d.id)===path)?.title??r.title;
|
||||
return documentResource(path,title,`${card.id}:${r.id}`);
|
||||
});
|
||||
return skill.documents.map(doc=>({
|
||||
path:legacyDocumentPath(card.id,doc.id),title:doc.title,collection:skill.title,
|
||||
resources:[...resources,shared],
|
||||
blocks:[...doc.blocks.map(b=>b.type==='links'?{...b,refs:b.refs!.map(id=>`${card.id}:${id}`)}:{...b}),
|
||||
...(['checks','delivery'].includes(doc.id)?[{type:'heading' as const,text:'共用指导文档'},{type:'links' as const,refs:[shared.id]}]:[])],
|
||||
}));
|
||||
});
|
||||
guidanceDocuments.push({
|
||||
path:'image-delivery',title:'图像交付与来源记录',collection:'共用指导',
|
||||
resources:[documentResource('street-people/checks','透明人物验收'),documentResource('shadow-puppet/delivery','皮影海报验收与上传')],
|
||||
blocks:[
|
||||
{type:'paragraph',text:'这是一份可被不同创作方法共同引用的指导文档。每篇文档都有独立网址,引用不会复制或隔离内容。'},
|
||||
{type:'heading',text:'交付前确认'},
|
||||
{type:'paragraph',text:'按照具体任务确认尺寸、比例、格式、透明背景与成品数量。保留原始生成文件;透明 PNG 与有背景的海报使用各自的验收规则。'},
|
||||
{type:'heading',text:'记录来源'},
|
||||
{type:'paragraph',text:'记录原片或参考图、使用的提示词、生成工具及必要的修改说明。来源不同的作品分开说明,便于复用时追溯。'},
|
||||
{type:'heading',text:'选择对应的交付协议'},
|
||||
{type:'paragraph',text:'阅读目标文档的上传说明再提交。不同接口的字段和校验条件可能不同,以被引用文档为准。'},
|
||||
{type:'links',refs:['doc:street-people/checks','doc:shadow-puppet/delivery']},
|
||||
],
|
||||
});
|
||||
const registry=new Map(guidanceDocuments.map(doc=>[doc.path,doc]));
|
||||
export function getGuidance(path:string){return registry.get(path);}
|
||||
export function guidanceBacklinks(path:string){return guidanceDocuments.filter(doc=>doc.path!==path&&doc.blocks.some(b=>b.type==='links'&&b.refs!.some(id=>doc.resources.find(r=>r.id===id)?.documentPath===path)));}
|
||||
export function guidanceMarkdown(path:string,base=origin){
|
||||
const doc=getGuidance(path);if(!doc)return null;
|
||||
const url=(href:string)=>new URL(href,base).href;
|
||||
const blocks=doc.blocks.map(b=>b.type==='heading'?`## ${b.text}`:b.type==='code'?`\x60\x60\x60\n${b.text}\n\x60\x60\x60`:b.type==='links'?b.refs!.map(id=>{
|
||||
const r=doc.resources.find(r=>r.id===id)!;return `- [${r.title}](${url(r.href)})${r.method?' — '+r.method:''}`;
|
||||
}).join('\n'):b.text).join('\n\n');
|
||||
const incoming=guidanceBacklinks(path);
|
||||
return `# ${doc.title}\n\n[网页版本](${url(humanDocumentUrl(path))}) · [全部指导文档](${url('/skills/index.md')})\n\n${blocks}\n`+
|
||||
(incoming.length?`\n## 引用本文的文档\n\n${incoming.map(d=>`- [${d.title}](${url(markdownDocumentUrl(d.path))})`).join('\n')}\n`:'');
|
||||
}
|
||||
export function guidanceIndexMarkdown(root=false,base=origin){
|
||||
const url=(href:string)=>new URL(href,base).href;
|
||||
return `# ${root?'PROMPT':'指导文档'}\n\n[网页版本](${url(root?'/':'/skills')}) · [指导文档目录](${url('/skills/index.md')})\n\n每篇指导文档都有独立网址,网页地址加 /index.md 即为对应的 Markdown。文档可以相互引用,不要求从某个技能入口逐级进入。\n\n## 全部指导文档\n\n`+
|
||||
guidanceDocuments.map(d=>`- [${d.title}](${url(markdownDocumentUrl(d.path))}) — ${d.collection}`).join('\n')+'\n';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
export function sendMarkdown(req:NextApiRequest,res:NextApiResponse,text:string|null){
|
||||
res.setHeader('Allow','GET, HEAD');
|
||||
if(req.method!=='GET'&&req.method!=='HEAD')return res.status(405).end();
|
||||
if(text===null)return res.status(404).end();
|
||||
res.setHeader('Content-Type','text/markdown; charset=utf-8');
|
||||
res.setHeader('Cache-Control','no-cache');
|
||||
return req.method==='HEAD'?res.status(200).end():res.status(200).send(text);
|
||||
}
|
||||
@@ -1,28 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { skillCatalog } from "@/content/skills/catalog";
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
res.setHeader("Allow", "GET, HEAD");
|
||||
return res.status(405).end();
|
||||
}
|
||||
const markdown = [
|
||||
"# PROMPT — Agent 入口",
|
||||
"",
|
||||
"人类入口:[/](/)。Agent 默认入口:[/agent/index.md](/agent/index.md)。",
|
||||
"",
|
||||
"按任务选择技能,先阅读 SKILL.md,再按其中链接读取子文档、参考图片与上传协议。所有相对链接均以当前网站根地址解析。",
|
||||
"不要从 HTML 页面猜测上传协议;生成由你自己的工具完成,网站提供文档、校验与素材存储。执行上传前遵循用户授权和对应技能的要求。",
|
||||
"",
|
||||
"## 技能目录",
|
||||
...skillCatalog.map(skill => `- [${skill.title}](/skills/${skill.id}/SKILL.md) — ${skill.description}`),
|
||||
"",
|
||||
"## 网站导航",
|
||||
"- [人类首页](/)",
|
||||
"- [技能浏览](/tools/skills)",
|
||||
"",
|
||||
].join("\n");
|
||||
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
return req.method === "HEAD" ? res.status(200).end() : res.status(200).send(markdown);
|
||||
}
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {guidanceIndexMarkdown} from '@/content/skills/guidance';
|
||||
import {sendMarkdown} from '@/lib/markdown-response';
|
||||
export default function handler(req:NextApiRequest,res:NextApiResponse){return sendMarkdown(req,res,guidanceIndexMarkdown(true));}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {guidanceIndexMarkdown,guidanceMarkdown} from '@/content/skills/guidance';
|
||||
import {sendMarkdown} from '@/lib/markdown-response';
|
||||
export default function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
const parts=Array.isArray(req.query.path)?req.query.path:[];
|
||||
const text=parts.length===0?guidanceIndexMarkdown(true):parts[0]!=='skills'?null:parts.length===1?guidanceIndexMarkdown():guidanceMarkdown(parts.slice(1).join('/'));
|
||||
return sendMarkdown(req,res,text);
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {skillDocumentMarkdown} from '@/content/skills/catalog';
|
||||
import {guidanceMarkdown,legacyDocumentPath} from '@/content/skills/guidance';
|
||||
import {sendMarkdown} from '@/lib/markdown-response';
|
||||
export default function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
res.setHeader('Allow','GET, HEAD');
|
||||
if(!['GET','HEAD'].includes(req.method||''))return res.status(405).end();
|
||||
const name=String(req.query.doc).replace(/\.md$/,'');
|
||||
const text=skillDocumentMarkdown(String(req.query.skill),name==='SKILL'?'index':name);
|
||||
if(!text)return res.status(404).end();
|
||||
res.setHeader('Content-Type','text/markdown; charset=utf-8');res.setHeader('Cache-Control','no-cache');
|
||||
return req.method==='HEAD'?res.status(200).end():res.status(200).send(text);
|
||||
return sendMarkdown(req,res,guidanceMarkdown(legacyDocumentPath(String(req.query.skill),name)));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {documentMarkdown} from '@/content/skills/catalog';
|
||||
export default function handler(req:NextApiRequest,res:NextApiResponse){if(!['GET','HEAD'].includes(req.method||''))return res.status(405).end();const name=String(req.query.doc).replace(/\.md$/,'');const text=documentMarkdown(name==='SKILL'?'index':name);if(!text)return res.status(404).end();res.setHeader('Content-Type','text/markdown; charset=utf-8');res.setHeader('Cache-Control','no-cache');return req.method==='HEAD'?res.end():res.send(text)}
|
||||
import {guidanceMarkdown,legacyDocumentPath} from '@/content/skills/guidance';
|
||||
import {sendMarkdown} from '@/lib/markdown-response';
|
||||
export default function handler(req:NextApiRequest,res:NextApiResponse){return sendMarkdown(req,res,guidanceMarkdown(legacyDocumentPath('street-people',String(req.query.doc).replace(/\.md$/,''))));}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type {NextApiRequest,NextApiResponse} from "next";
|
||||
import {upload} from "../upload";
|
||||
import {streetPeople} from "@/lib/street-people";
|
||||
import {documentMarkdown} from "@/content/skills/catalog";
|
||||
import {guidanceMarkdown} from "@/content/skills/guidance";
|
||||
import {validateCharacter} from "@/lib/character-image";
|
||||
export const config={api:{bodyParser:false}};
|
||||
export default async function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
res.setHeader("Allow","GET, POST");
|
||||
if(req.method==="GET"){
|
||||
res.setHeader("Cache-Control","public, max-age=300");
|
||||
if(req.query.format==="md")return res.setHeader("Content-Type","text/markdown; charset=utf-8").status(200).send(documentMarkdown("index"));
|
||||
if(req.query.format==="md")return res.setHeader("Content-Type","text/markdown; charset=utf-8").status(200).send(guidanceMarkdown("street-people"));
|
||||
return res.status(200).json(streetPeople);
|
||||
}
|
||||
if(req.method==="POST")return upload(req,res,{validateImage:validateCharacter,defaultTitle:"街头小人 · 新生成"});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type {GetServerSideProps} from 'next';
|
||||
import {SkillDocs} from '@/components/SkillDocs';
|
||||
import {GuidanceIndex} from '@/components/GuidanceIndex';
|
||||
import {getGuidance} from '@/content/skills/guidance';
|
||||
export default function GuidancePage({documentPath}:{documentPath:string|null}){return documentPath===null?<GuidanceIndex/>:<SkillDocs documentPath={documentPath}/>;}
|
||||
export const getServerSideProps:GetServerSideProps=async({params})=>{
|
||||
const parts=params?.path;if(parts===undefined)return {props:{documentPath:null}};
|
||||
if(!Array.isArray(parts))return {notFound:true};
|
||||
const documentPath=parts.join('/');return getGuidance(documentPath)?{props:{documentPath}}:{notFound:true};
|
||||
};
|
||||
@@ -1,5 +1 @@
|
||||
import Head from 'next/head';
|
||||
import {Shell} from '@/components/Shell';
|
||||
import {SkillCard} from '@/components/SkillCard';
|
||||
import {skillCatalog} from '@/content/skills/catalog';
|
||||
export default function Skills(){return <Shell><Head><title>技能 · PROMPT</title></Head><header className="page-header"><div><h1>技能</h1><p>把一种喜欢的画风,变成可以反复调用的创作方法。</p></div></header><div className="skills-masonry">{skillCatalog.map(skill=><SkillCard key={skill.id} skill={skill}/>)}</div></Shell>}
|
||||
export {GuidanceIndex as default} from '@/components/GuidanceIndex';
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
.skills-masonry{columns:4 230px;column-gap:24px}.skill-pin{display:block;break-inside:avoid;margin:0 0 28px}.skill-pin-cover{background:#f1eee8!important;aspect-ratio:4/5;display:grid;place-items:center;max-height:440px}.skill-pin-cover img{width:100%;height:100%;object-fit:contain!important;padding:24px}.skill-pin-category{display:block;font-size:11px;color:#a08873;margin:14px 4px 8px}.skill-pin p{font-size:13px;line-height:1.8;color:#777;margin:8px 4px 12px}.skill-pin-link{font-size:12px;margin:0 4px;color:#5e6b56}.docs-top{display:flex;justify-content:space-between;font-size:13px;color:#777;gap:15px;margin-bottom:28px}.docs-top button{display:flex;align-items:center;gap:8px}.docs-layout{max-width:1200px;margin:auto;display:grid;gap:35px;grid-template-columns:minmax(0,800px);justify-content:center}.docs-layout.with-sources{grid-template-columns:minmax(0,1fr) 340px}.docs-body{min-width:0;padding:18px 12px 70px}.docs-eyebrow{font-size:11px;color:#9b968b;letter-spacing:1.5px}.docs-body h1{font-size:38px;font-weight:700;margin:18px 0 24px;letter-spacing:-1px}.docs-body h2{font-size:23px;margin:40px 0 20px;font-weight:650}.docs-body>p{font-size:15px;color:#555;line-height:2;margin:22px 0}.docs-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:12px}.docs-actions button{display:flex;align-items:center;gap:8px;background:#252525;color:white;padding:12px 16px;border-radius:24px}.docs-actions a{color:#888}.docs-body>.docs-hint{font-size:12px;color:#999}.docs-body pre,.docs-resource-preview pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#f5f5f2;padding:22px;border-radius:15px;font-size:12px;line-height:1.9;color:#666}.docs-inline-links{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0}.docs-inline-links a{display:flex;align-items:center;gap:7px;border:1px solid #e4e4df;border-radius:20px;padding:9px 12px;font-size:12px;color:#64705b}.docs-sources{position:sticky;top:105px;align-self:start;max-height:calc(100vh - 125px);overflow:auto;border:1px solid #e6e6e1;border-radius:18px;background:white;box-shadow:0 8px 35px #00000008;padding:20px}.docs-sources>header{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;border-bottom:1px solid #eee;padding-bottom:18px}.docs-sources header b{font-size:16px}.docs-sources header small{display:block;font-size:11px;color:#aaa;margin-top:7px}.docs-sources h3{font-size:12px;color:#999;margin:21px 0 10px}.docs-sources section>button{display:flex;text-align:left;align-items:flex-start;gap:9px;width:100%;padding:12px 8px;border-radius:10px}.docs-sources section>button.selected,.docs-sources section>button:hover{background:#f2f3ed}.docs-sources section>button>span{font-size:9px;border:1px solid #ddd;padding:3px 4px;border-radius:5px;color:#92987f;flex-shrink:0}.docs-sources section>button>div{font-size:12px;color:#555;min-width:0}.docs-sources section small{display:block;font-size:10px;overflow-wrap:anywhere;color:#aaa;margin-top:5px}.docs-resource-preview{border-top:1px solid #ddd;margin-top:20px}.docs-resource-preview img{width:100%;max-height:350px;object-fit:contain;background:repeating-conic-gradient(#eee 0 25%,white 0 50%) 0/16px 16px}.docs-resource-preview pre{font-size:10px;max-height:330px;overflow:auto;padding:12px}.docs-resource-preview p,.docs-resource-preview code{font-size:11px;line-height:1.8;overflow-wrap:anywhere}.docs-resource-actions{display:flex;flex-wrap:wrap;gap:12px;font-size:12px;color:#60724e;margin:18px 0}.docs-body a:focus-visible,.docs-sources button:focus-visible{outline:2px solid #81956d;outline-offset:3px}
|
||||
@media(max-width:1050px){.docs-layout.with-sources{grid-template-columns:minmax(0,1fr) 285px;gap:18px}}@media(max-width:760px){.docs-layout.with-sources{display:block}.docs-sources{position:fixed;right:12px;left:12px;top:95px;bottom:18px;max-height:none;z-index:45;box-shadow:0 0 0 100vmax #0005}.docs-body h1{font-size:30px}.docs-body{padding:10px 0 40px}.docs-body>p{font-size:14px}.skills-masonry{columns:2;column-gap:15px}.skill-pin-cover img{padding:12px}.skill-pin p{font-size:12px}.snapshot-reader{padding:14px 8px}.snapshot-reader>header{align-items:flex-start;flex-direction:column}.snapshot-reader nav{gap:10px}.snapshot-reader nav a{font-size:11px}}
|
||||
.guidance-directory{border-top:1px solid #e4e4df;margin-top:24px}.guidance-directory>div{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:20px;padding:18px 0;border-bottom:1px solid #e4e4df}.guidance-directory>div>a:first-child{font-size:15px;color:#3e4938}.guidance-directory small,.guidance-directory>div>a:last-child{font-size:12px;color:#888}@media(max-width:760px){.guidance-directory>div{grid-template-columns:minmax(0,1fr) auto;gap:8px}.guidance-directory small{grid-column:1;grid-row:2}.guidance-directory>div>a:last-child{grid-column:2;grid-row:1/3}}
|
||||
|
||||
@@ -7,7 +7,7 @@ test('human and agent documents share a complete linked resource registry',()=>{
|
||||
for(const doc of skillDocuments)assert.ok(skillResources.some(r=>r.id===doc.id&&r.human&&r.href.endsWith('.md')));
|
||||
});
|
||||
|
||||
test('each registered skill resolves its own entry, documents and API resources without leaking another skill',()=>{
|
||||
test('original collections retain their authored documents and API resources',()=>{
|
||||
for(const card of skillCatalog){
|
||||
const skill=getSkill(card.id)!;assert.ok(skill);assert.equal(skill.id,card.id);
|
||||
assert.equal(new Set(skill.resources.map(r=>r.id)).size,skill.resources.length);
|
||||
@@ -23,6 +23,37 @@ test('each registered skill resolves its own entry, documents and API resources
|
||||
assert.equal(shadow.resources.filter(r=>r.kind==='image').length,2);
|
||||
assert.equal(shadow.resources.find(r=>r.id==='upload-api')?.href,'/api/upload');
|
||||
assert.equal(shadow.resources.find(r=>r.id==='media-api')?.template,true);
|
||||
assert.ok(!skillDocumentMarkdown('shadow-puppet','index')!.includes('/skills/street-people/'));
|
||||
assert.ok(!documentMarkdown('index')!.includes('/skills/shadow-puppet/'));
|
||||
});
|
||||
|
||||
test('every guidance page has a matching Markdown URL and all document links resolve globally',async()=>{
|
||||
const {guidanceDocuments,getGuidance,guidanceMarkdown,guidanceIndexMarkdown,humanDocumentUrl,markdownDocumentUrl,guidanceBacklinks}=await import('../src/content/skills/guidance');
|
||||
assert.equal(new Set(guidanceDocuments.map(d=>d.path)).size,guidanceDocuments.length);
|
||||
for(const doc of guidanceDocuments){
|
||||
for(const r of doc.resources)if(r.documentPath)assert.ok(getGuidance(r.documentPath),r.documentPath);
|
||||
const text=guidanceMarkdown(doc.path)!;
|
||||
assert.ok(text.startsWith('# '+doc.title));
|
||||
assert.equal(markdownDocumentUrl(doc.path),humanDocumentUrl(doc.path)+'/index.md');
|
||||
assert.ok(text.includes(humanDocumentUrl(doc.path)));
|
||||
for(const block of doc.blocks){
|
||||
if(block.text)assert.ok(text.includes(block.text));
|
||||
for(const id of block.refs||[]){const r=doc.resources.find(r=>r.id===id);assert.ok(r,doc.path+': '+id);if(r.documentPath){assert.ok(getGuidance(r.documentPath));assert.ok(text.includes(markdownDocumentUrl(r.documentPath)));}}
|
||||
}
|
||||
assert.ok(guidanceIndexMarkdown().includes(markdownDocumentUrl(doc.path)));
|
||||
}
|
||||
assert.ok(guidanceIndexMarkdown(true).includes('/skills/index.md'));
|
||||
assert.equal(getGuidance('__proto__'),undefined);assert.equal(guidanceMarkdown('missing'),null);
|
||||
const shared=guidanceBacklinks('image-delivery').map(d=>d.path);
|
||||
assert.ok(shared.includes('street-people/checks'));assert.ok(shared.includes('shadow-puppet/delivery'));
|
||||
assert.ok(guidanceMarkdown('image-delivery')!.includes('/skills/street-people/checks/index.md'));
|
||||
assert.ok(guidanceMarkdown('image-delivery')!.includes('/skills/shadow-puppet/delivery/index.md'));
|
||||
});
|
||||
|
||||
test('Markdown counterpart responses support GET and HEAD and reject writes or missing documents',async()=>{
|
||||
const {sendMarkdown}=await import('../src/lib/markdown-response');
|
||||
const {guidanceMarkdown}=await import('../src/content/skills/guidance');
|
||||
for(const [method,text,expected] of [['GET',guidanceMarkdown('shadow-puppet/prompt'),200],['HEAD',guidanceMarkdown('shadow-puppet/prompt'),200],['POST',guidanceMarkdown('shadow-puppet/prompt'),405],['GET',guidanceMarkdown('missing'),404]] as const){
|
||||
const result={code:0,body:null as string|null,headers:{} as Record<string,string>,setHeader(k:string,v:string){this.headers[k]=v;return this},status(code:number){this.code=code;return this},send(body:string){this.body=body;return this},end(){return this}};
|
||||
sendMarkdown({method} as import('next').NextApiRequest,result as unknown as import('next').NextApiResponse,text);
|
||||
assert.equal(result.code,expected);if(method==='GET'&&expected===200){assert.match(result.headers['Content-Type'],/text\/markdown/);assert.ok(result.body?.startsWith('# '));}else assert.equal(result.body,null);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user