From 28b9fb146482810e74bd14f51568b46c664fef3e Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 17:33:22 +0800 Subject: [PATCH 1/4] Align homepage data route checks with public knowledge access --- apps/waterfall-wiki/scripts/auth-cli.ts | 5 ++++- apps/waterfall-wiki/tests/auth.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/waterfall-wiki/scripts/auth-cli.ts b/apps/waterfall-wiki/scripts/auth-cli.ts index 4e0d141..e79ae89 100644 --- a/apps/waterfall-wiki/scripts/auth-cli.ts +++ b/apps/waterfall-wiki/scripts/auth-cli.ts @@ -49,8 +49,11 @@ async function main() { const buildId = readFileSync('.next/BUILD_ID','utf8').trim(); assert.equal((await request('/')).status,200,'Public knowledge homepage'); assert.equal((await request('/account')).status,200,'Public account page'); + const homeData = await request(`/_next/data/${buildId}/index.json`); + assert.ok([200,404].includes(homeData.status),'Public homepage data is readable or absent for static pages'); + assert.ok(!isLoginRedirect(homeData.headers.get('location') || homeData.headers.get('x-nextjs-redirect'),base)); assert.equal((await request('/api/posts',{method:'POST',headers:{Cookie:'wiki_account='+ 'a'.repeat(64)},body:'{}'})).status,401,'Account cookies never grant admin rights'); - for (const path of ['/library/import','/skills/shadow-puppet/egret-example.png','/studio',`/_next/data/${buildId}/index.json`]) { + for (const path of ['/library/import','/skills/shadow-puppet/egret-example.png','/studio',`/_next/data/${buildId}/library/import.json`]) { const response = await request(path); assert.equal(response.status,307,path); const destination = response.headers.get('location') || response.headers.get('x-nextjs-redirect'); assert.ok(isLoginRedirect(destination,base),path+' must redirect to login: '+destination); } for (const path of ['/api/feed','/api/upload','/studio/api/export/test','/api/media/missing']) assert.equal((await request(path)).status,401,path); diff --git a/apps/waterfall-wiki/tests/auth.test.ts b/apps/waterfall-wiki/tests/auth.test.ts index ef5e33d..a2ed75e 100644 --- a/apps/waterfall-wiki/tests/auth.test.ts +++ b/apps/waterfall-wiki/tests/auth.test.ts @@ -34,9 +34,9 @@ test('proxy protects APIs, data routes, static content and Studio; fails closed' const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE; try{ const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config)); - for(const path of ['/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path); + for(const path of ['/library/import','/skills/example.png','/_next/data/build/library/import.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path); for(const path of ['/api/upload','/studio/api/export','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path); - for(const path of ['/','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path); + for(const path of ['/','/_next/data/build/index.json','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path); const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'}; assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403); From 68155fc0f79b1f8410752a8aa55fe1a5620df34e Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 17:46:22 +0800 Subject: [PATCH 2/4] Fix guidance directory counts and preserve submitted filters --- apps/waterfall-wiki/src/pages/index.tsx | 72 ++++++++++++++++++- .../src/styles/guidance.module.css | 1 + .../tests/guidance-integration.ts | 16 +++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/apps/waterfall-wiki/src/pages/index.tsx b/apps/waterfall-wiki/src/pages/index.tsx index 200d64f..b3f7981 100644 --- a/apps/waterfall-wiki/src/pages/index.tsx +++ b/apps/waterfall-wiki/src/pages/index.tsx @@ -1,7 +1,75 @@ import Head from 'next/head'; import Link from 'next/link'; -import {useEffect,useState} from 'react'; +import {useRouter} from 'next/router'; +import {useEffect,useState,type FormEvent} from 'react'; import GuidanceShell from '@/components/GuidanceShell'; import {guides} from '@/content/guides/catalog'; import s from '@/styles/guidance.module.css'; -export default function Home(){const [published,setPublished]=useState<{id:string;revision:number;title:string;summary:string;kind:string}[]>([]),[loadError,setLoadError]=useState('');useEffect(()=>{fetch('/api/documents').then(async r=>{const b=await r.json();if(!r.ok)throw Error(b.error);setPublished(b.documents);}).catch(e=>setLoadError(e.message));},[]);const[q,setQ]=useState(''),[kind,setKind]=useState('all');const items=guides.filter(g=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary} ${g.tags.join(' ')}`.toLowerCase().includes(q.toLowerCase()));return 织页 · 为 Agent 找到下一页
A WEB OF USEFUL KNOWLEDGE

一个起点,
连接下一种可能。

每一篇文档,都能独立帮助你的 Agent。
从当前问题出发,沿着有用的链接继续探索。

{['代码审查','规划','验证'].map(t=>)}

用户发布

发布一篇文档 ↗
{loadError&&

{loadError}

}
{published.filter(g=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary}`.toLowerCase().includes(q.toLowerCase())).map(g=>
↗
{g.id} · {g.kind} · v{g.revision}

{g.title}

{g.summary}

)}

从这里开始 {items.length} 篇

协议样例 · v0.1
{items.map(g=>
{g.kind==='guide'?'↗':'≋'}
织页 / protocol-examples · r{g.revision}

{g.title}

{g.summary}

{g.tags.map(t=>{t})}↗ {g.links.length} 个关联
)}{!items.length&&

没有匹配文档,请尝试其他关键词或分类。

}
} + +type PublishedDocument={id:string;revision:number;title:string;summary:string;kind:string}; +const categories=[['all','全部文档'],['guide','操作指导'],['context','背景知识'],['evidence','事实依据']]; +export default function Home(){ + const router=useRouter(); + const [published,setPublished]=useState([]); + const [loadError,setLoadError]=useState(''); + const [loading,setLoading]=useState(true); + const [q,setQ]=useState(''); + const [kind,setKind]=useState('all'); + useEffect(()=>{ + let active=true; + fetch('/api/documents').then(async r=>{ + const b=await r.json();if(!r.ok)throw Error(b.error); + if(active)setPublished(b.documents); + }).catch(e=>{if(active)setLoadError(e.message);}).finally(()=>{if(active)setLoading(false);}); + return()=>{active=false;}; + },[]); + useEffect(()=>{ + if(!router.isReady)return; + setQ(typeof router.query.q==='string'?router.query.q:''); + setKind(typeof router.query.kind==='string'&&categories.some(([key])=>key===router.query.kind)?router.query.kind:'all'); + },[router.isReady,router.query.q,router.query.kind]); + function rememberFilters(query:string,category:string){ + const next:{q?:string;kind?:string}={}; + if(query)next.q=query;if(category!=='all')next.kind=category; + void router.push({pathname:'/',query:next},undefined,{shallow:true,scroll:false}); + } + function search(e:FormEvent){e.preventDefault();rememberFilters(q,kind);} + function chooseKind(value:string){setKind(value);rememberFilters(q,value);} + function clearFilters(){setQ('');setKind('all');rememberFilters('','all');} + const matches=(g:{title:string;summary:string;kind:string;tags?:string[]})=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary} ${g.tags?.join(' ')||''}`.toLowerCase().includes(q.trim().toLowerCase()); + const items=guides.filter(matches),userItems=published.filter(matches); + const total=items.length+userItems.length; + return + 织页 · 为 Agent 找到下一页 +
+ A WEB OF USEFUL KNOWLEDGE +

一个起点,
连接下一种可能。

+

每一篇文档,都能独立帮助你的 Agent。
从当前问题出发,沿着有用的链接继续探索。

+
+ setQ(e.target.value)}/> +
+
{['代码审查','规划','验证'].map(t=>)}
+
+
+ +
+

{loading?'正在加载用户文档…':loadError?`协议样例 ${items.length} 篇;用户文档加载失败`:`找到 ${total} 篇文档`}

{(q||kind!=='all')&&}
+ {loadError&&

用户文档暂未加载:{loadError}。下方仍可阅读协议样例。

} + {!loading&&!loadError&&total===0&&

没有匹配文档,请尝试其他关键词或清除筛选。

} +

用户发布 {userItems.length} 篇

发布一篇文档 ↗
+
{userItems.map(g=>
↗
{g.id} · {g.kind} · v{g.revision}

{g.title}

{g.summary}

)}
+ {!loading&&!loadError&&!published.length&&

还没有用户文档。你可以发布第一篇,或先阅读下方样例。

} +

从这里开始 {items.length} 篇

协议样例 · v0.1
+
{items.map(g=>
{g.kind==='guide'?'↗':'≋'}
织页 / protocol-examples · r{g.revision}

{g.title}

{g.summary}

{g.tags.map(t=>{t})}↗ {g.links.length} 个关联
)}
+
+ +
+
; +} diff --git a/apps/waterfall-wiki/src/styles/guidance.module.css b/apps/waterfall-wiki/src/styles/guidance.module.css index 29bd39e..684cafd 100644 --- a/apps/waterfall-wiki/src/styles/guidance.module.css +++ b/apps/waterfall-wiki/src/styles/guidance.module.css @@ -1 +1,2 @@ .site{background:#fff;color:#233026;min-height:100vh;font:14px Arial,"PingFang SC",sans-serif}.site a{color:inherit;text-decoration:none}.site button,.site input{font:inherit}.header{height:80px;display:flex;align-items:center;gap:60px;border-bottom:1px solid #e7ebe5;padding:0 5%}.brand{font-size:28px;font-weight:700}.brand small{font-size:10px;border:1px solid #d5e3d1;padding:3px 5px;border-radius:4px;color:#71856c}.header nav{display:flex;gap:30px}.account{margin-left:auto;border:1px solid #dce2d8;border-radius:6px;padding:10px 14px}.hero{padding:65px 6% 45px;background:radial-gradient(ellipse at 90% 10%,#eaf3e4,transparent 55%),#f8faf6;border-bottom:1px solid #e2e8dd}.eyebrow{font-size:10px;letter-spacing:2px;font-weight:700;color:#748b70}.hero h1{font-size:52px;letter-spacing:-2px;line-height:1.25;font-weight:600;margin:22px 0}.hero p{font-size:16px;color:#788675;line-height:1.9}.search{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid #d6e0cf;border-radius:8px;max-width:620px;padding:15px 18px;margin-top:26px;box-shadow:0 5px 16px #35442b09}.search input{border:0;outline:0;flex:1;min-width:0;background:transparent}.search:focus-within{outline:2px solid #7b9b71}.search kbd{font-size:10px;color:#929e89;border:1px solid #e0e6da;padding:3px}.topics{display:flex;gap:12px;margin-top:15px}.topics button{border:0;background:transparent;color:#7b8b73;font-size:12px;cursor:pointer}.layout{display:grid;grid-template-columns:170px minmax(0,1fr) 250px;gap:35px;padding:40px 5% 80px;max-width:1500px;margin:auto}.sidebar h2{font-size:12px;color:#85917f;margin-bottom:18px}.sidebar button{display:flex;justify-content:space-between;width:100%;padding:12px;border:0;border-radius:5px;background:transparent;color:#74816e;cursor:pointer;margin-bottom:5px}.sidebar button[aria-pressed=true]{background:#edf3e8;color:#44673c;font-weight:600}.sidebar button span{font-size:11px}.sidebar p{font-size:12px;line-height:1.9;color:#919d88;border-top:1px solid #e7ebe2;padding-top:25px;margin-top:30px}.sectionTitle{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.sectionTitle h2{font-size:18px;margin:0}.sectionTitle small{font-size:11px;color:#91a086;font-weight:400;margin-left:10px}.sectionTitle>span{font-size:10px;color:#949e8d}.list{border-top:1px solid #e4eadd}.row{display:flex;gap:16px;padding:25px 0;border-bottom:1px solid #e4eadd}.icon{display:grid;place-items:center;flex-shrink:0;width:36px;height:42px;background:#f0f4eb;border:1px solid #e0e7d7;border-radius:5px;color:#7c9671;font-size:24px}.meta{font-size:10px;color:#949e8e;line-height:1.9}.row h3{font-size:17px;margin:8px 0;font-weight:600}.row p{font-size:12px;line-height:1.8;color:#84917b;margin:8px 0 13px}.tags{display:flex;gap:7px;flex-wrap:wrap}.tags span{font-size:10px;border:1px solid #e1e8d8;border-radius:4px;padding:3px 7px;color:#86977b}.manifest{padding:25px;background:#f0f4e7;border:1px solid #e1e8d5;border-radius:8px}.manifest h2{font-size:29px;line-height:1.5;font-weight:500;margin:20px 0}.manifest p{font-size:12px;line-height:1.9;color:#8c9c7c}.manifest a{display:block;border-top:1px solid #dce4d0;padding-top:16px;margin-top:25px;font-size:12px}.note{padding:24px 0;font-size:12px;line-height:1.9;color:#87957c}.note h3{font-size:13px;color:#53684a;margin:20px 0 8px}.footer{display:flex;justify-content:space-between;gap:20px;border-top:1px solid #e5ebdf;padding:25px 5%;font-size:11px;color:#95a08c}.detail{max-width:1120px;margin:auto;padding:40px 5% 80px}.detail>h1{font-size:38px;font-weight:600}.detail>.eyebrow{display:block;margin-top:30px}.detail>p{color:#84967a}.tabs{display:flex;gap:24px;align-items:center;border-bottom:1px solid #dfe6d8;margin-top:32px}.tabs button{border:0;border-bottom:2px solid transparent;padding:15px 0;background:transparent;cursor:pointer;color:#84967a}.tabs button[aria-pressed=true]{border-color:#79976b;color:#456b3a}.tabs a{margin-left:auto;font-size:12px}.detailGrid{display:grid;grid-template-columns:minmax(0,1fr) 230px;gap:50px}.prose{font-size:15px;line-height:2;color:#617656}.prose h2{font-size:20px;color:#344a2c;margin:30px 0 12px}.prose li{padding:5px 0}.prose pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;background:#f4f7ef;border:1px solid #e2e9d7;border-radius:6px;padding:22px;line-height:1.9;color:#6c865f}.related{display:block;border:1px solid #dfe8d4;border-radius:6px;padding:15px 18px;margin:12px 0}.related strong{font-size:14px}.related span{display:block;font-size:11px;color:#96a48b}.site a:hover{color:#71975c}.site button:focus-visible,.site a:focus-visible{outline:2px solid #7c9b6c;outline-offset:4px}@media(max-width:1050px){.layout{grid-template-columns:140px minmax(0,1fr);gap:25px}.right{display:none}.hero h1{font-size:44px}.header{gap:30px}}@media(max-width:650px){.header{height:70px;padding:0 20px;gap:18px}.header nav{gap:12px;font-size:12px}.brand{font-size:22px}.brand small{display:none}.account{font-size:11px;padding:8px}.hero{padding:42px 23px 32px}.hero h1{font-size:38px}.hero p{font-size:14px}.layout{display:block;padding:25px 22px 50px}.sidebar{display:flex;gap:4px;overflow:auto;margin-bottom:25px}.sidebar h2,.sidebar p{display:none}.sidebar button{white-space:nowrap;width:auto;gap:10px;font-size:12px}.sectionTitle>span{display:none}.footer{flex-direction:column}.detail{padding:30px 22px}.detail>h1{font-size:30px}.detailGrid{display:block}.tabs{gap:16px}.tabs a{font-size:10px}.prose pre{padding:15px}.icon{display:none}} +.search button,.sectionTitle>button{border:1px solid #d5e1cc;border-radius:5px;background:#f0f5eb;color:#456b3a;padding:6px 10px;cursor:pointer;white-space:nowrap}.sectionTitle>p{font-size:12px;color:#687d5d}.sectionTitle{gap:12px}.list+.sectionTitle{margin-top:30px} diff --git a/apps/waterfall-wiki/tests/guidance-integration.ts b/apps/waterfall-wiki/tests/guidance-integration.ts index b7bf9e6..66b5091 100644 --- a/apps/waterfall-wiki/tests/guidance-integration.ts +++ b/apps/waterfall-wiki/tests/guidance-integration.ts @@ -36,6 +36,22 @@ export async function verifyGuidancePublishing(base:string){ const link=`a[href="/documents/${httpDoc.id}?revision=1"]`; await page.goto(base+'/',{waitUntil:'domcontentloaded'}); await page.locator(link).waitFor({state:'visible'});assert.equal(await page.locator(link).textContent(),httpDoc.title); + // Published matches must not inherit the examples' empty state; submitted filters survive reload. + const search=page.getByRole('textbox',{name:'搜索文档'}); + await search.fill(httpDoc.title);await search.press('Enter'); + await page.waitForURL(url=>url.searchParams.get('q')===httpDoc.title); + await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); + assert.equal(await page.getByText('没有匹配文档',{exact:false}).count(),0); + await page.reload({waitUntil:'domcontentloaded'}); + await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); + assert.equal(await search.inputValue(),httpDoc.title); + await page.getByRole('button',{name:'事实依据'}).click(); + await page.waitForURL(url=>url.searchParams.get('kind')==='evidence'); + await page.getByText('没有匹配文档,请尝试其他关键词或清除筛选。',{exact:true}).waitFor(); + await page.getByRole('button',{name:'清除所有筛选'}).click(); + await page.waitForURL(url=>url.search===''); + await page.locator(link).waitFor({state:'visible'}); + assert.equal(await search.inputValue(),''); await page.locator('header a[href="/documents"]').click();await page.locator(link).waitFor({state:'visible'}); await page.locator(link).click();await page.locator('h1').waitFor();assert.equal(await page.locator('h1').textContent(),httpDoc.title); await page.goto(base+'/write',{waitUntil:'domcontentloaded'});assert.equal(await page.locator('h1').textContent(),'发布独立文档'); From b1f1a454e76b2a4fd8965aaa1a8dfeb0909672db Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 18:00:43 +0800 Subject: [PATCH 3/4] Run CI and deployment only after main updates --- .gitea/workflows/python.yml | 9 ++------- .gitea/workflows/studio.yml | 18 +++--------------- .gitea/workflows/waterfall-wiki.yml | 15 ++------------- docs/ci-and-entrypoints.md | 6 ++++-- docs/linked-guidance/publishing.md | 2 +- 5 files changed, 12 insertions(+), 38 deletions(-) diff --git a/.gitea/workflows/python.yml b/.gitea/workflows/python.yml index 82eac63..bb5b5d6 100644 --- a/.gitea/workflows/python.yml +++ b/.gitea/workflows/python.yml @@ -6,15 +6,10 @@ on: - 'apps/python/**' - 'scripts/deploy-python.sh' - '.gitea/workflows/python.yml' - pull_request: - paths: - - 'apps/python/**' - - 'scripts/deploy-python.sh' - - '.gitea/workflows/python.yml' - workflow_dispatch: jobs: verify-and-deploy: + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' runs-on: ces-control timeout-minutes: 45 steps: @@ -24,5 +19,5 @@ jobs: - name: Build and verify python run: docker build -f apps/python/Dockerfile -t "prompt-python:${{ gitea.sha }}" . - name: Release python - if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch') + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' run: bash scripts/deploy-python.sh "${{ gitea.sha }}" diff --git a/.gitea/workflows/studio.yml b/.gitea/workflows/studio.yml index 75ebd5c..b7bc0a3 100644 --- a/.gitea/workflows/studio.yml +++ b/.gitea/workflows/studio.yml @@ -13,22 +13,10 @@ on: - 'content/studio-examples/**' - 'content/studio-tools/layouts-api.md' - 'pnpm-workspace.yaml' - pull_request: - paths: - - 'apps/studio/**' - - 'scripts/deploy-studio.sh' - - 'scripts/verify-studio-production.sh' - - 'scripts/verify-studio-library.mjs' - - 'scripts/verify-studio-visual.cjs' - - '.gitea/workflows/studio.yml' - - 'scripts/*studio*' - - 'content/studio-examples/**' - - 'content/studio-tools/layouts-api.md' - - 'pnpm-workspace.yaml' - workflow_dispatch: jobs: verify-and-deploy: + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' runs-on: ces-control timeout-minutes: 45 steps: @@ -44,10 +32,10 @@ jobs: - name: Build and verify studio run: docker build -f apps/studio/Dockerfile -t "prompt-studio:${{ gitea.sha }}" . - name: Release studio - if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch') + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' run: bash scripts/deploy-studio.sh "${{ gitea.sha }}" - name: Verify the deployed Studio API and native pages - if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch') + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' run: bash scripts/verify-studio-production.sh - name: Save server acceptance evidence id: acceptance_evidence diff --git a/.gitea/workflows/waterfall-wiki.yml b/.gitea/workflows/waterfall-wiki.yml index 43fa65c..b2cb01b 100644 --- a/.gitea/workflows/waterfall-wiki.yml +++ b/.gitea/workflows/waterfall-wiki.yml @@ -12,21 +12,10 @@ on: - 'scripts/auth-admin.sh' - 'scripts/integration.sh' - 'pnpm-workspace.yaml' - pull_request: - paths: - - 'apps/waterfall-wiki/**' - - '.gitea/workflows/waterfall-wiki.yml' - - 'Dockerfile' - - 'compose.yml' - - 'scripts/deploy.sh' - - 'scripts/infra.sh' - - 'scripts/auth-admin.sh' - - 'scripts/integration.sh' - - 'pnpm-workspace.yaml' - workflow_dispatch: jobs: verify-and-deploy: + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' runs-on: ces-control timeout-minutes: 45 steps: @@ -47,5 +36,5 @@ jobs: WATERFALL_WIKI_TEST_IMAGE: waterfall-wiki-tests:${{ gitea.sha }} run: bash scripts/integration.sh - name: Deploy waterfall-wiki - if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch') + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' run: bash scripts/deploy.sh "${{ gitea.sha }}" diff --git a/docs/ci-and-entrypoints.md b/docs/ci-and-entrypoints.md index 99aab35..c39edfd 100644 --- a/docs/ci-and-entrypoints.md +++ b/docs/ci-and-entrypoints.md @@ -4,16 +4,18 @@ `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 不再使应用构建层失效。只有 main 的 push 事件触发 CI/CD(对应 PR 合并后的主线更新),工作分支 push、PR 创建/更新和手动 dispatch 均不触发;三个应用均有同样的作业级守卫。main 对纯文档改动使用 paths,但 public 下的 Markdown 属于产品资源,必须构建部署。public 下的技能 Markdown 继续纳入构建。旧 builder 每条 COPY 实测约 47 秒,避免拆成多条 COPY 增加快照成本。完整路径保留类型检查、单元测试、smoke 与完整隔离栈集成测试。 当前页面为 Next.js SSR 与编译组件,改 TSX 仍需 next build。Web 快速路径只编译生产 Web,并以不可变的版本目录挂载完整 standalone 产物,保持版本与回滚一致;不重新编译 worker 或导出运行镜像。 ## Web 快速发布 -main 的 push 与当前已部署 SHA 比较;只有页面、组件、样式、技能文档数据和 public 改动才走 Web 路径,API、共享库、依赖、配置、worker、脚本及数据库变化均回退完整流水线。PR 和手动运行始终完整验证;依赖构建镜像缺失也回退完整流水线。 +main 的 push 与当前已部署 SHA 比较;只有页面、组件、样式、技能文档数据和 public 改动才走 Web 路径,API、共享库、依赖、配置、worker、脚本及数据库变化均回退完整流水线。PR 阶段只做不会触发编译的阅读、差异审查与轻量配置检查,完整远程验证在用户手动合并到 main 后执行;依赖构建镜像缺失也回退完整流水线。 Web 路径复用当前 runtime 对应的已验证 integration 镜像作为工具环境,在临时容器构建生产 Web,不运行 docker build。编译缓存按基础镜像隔离持久保存。类型检查、单元测试和 Web smoke 仍同步执行,完整隔离栈测试只在全量路径执行。 完整 standalone 产物存入 `/opt/prompt/web-artifacts/.`,只读挂载到 Web 容器;仅重建 Web,保留 runtime 镜像、worker、Postgres 和 RustFS。切换前检查 main 与当前发布,部署锁与全量流程共用;失败自动恢复旧 Web Compose 配置。发布文件保留变量表达式,不写入生产秘密。健康检查通过后切换 current;原完整发布也能回滚到 Web 快速发布。 首次接入流水线变更仍走完整验证。2026-10-23 workflow 291 实测:Web 构建与发布91秒,拉取代码44秒,合计2分15秒,不包含共享 Runner 排队。运行镜像复用,worker、Postgres、RustFS 容器 ID 均不变;编译缓存已持久化。不能承诺 HMR 的毫秒级反馈。每次发布完整 Web 产物,不把个别 chunk 混入正在运行的 Next.js 版本。 + +工作流变更不会自动取消旧配置已创建的运行。历史 PR 排队任务须单独取消,不能将其当作新触发规则仍然生效的证据。 diff --git a/docs/linked-guidance/publishing.md b/docs/linked-guidance/publishing.md index e45632d..8b790b4 100644 --- a/docs/linked-guidance/publishing.md +++ b/docs/linked-guidance/publishing.md @@ -9,7 +9,7 @@ - 邮箱会话仅由新接口校验,不授予管理员 API 权限。发布是公开行为;目前没有草稿服务、私有文档、恢复撤下或账户删除功能。tags 是发现元数据,通过 XML 可重复的 tag 元素往返保存。 - 数据库通过行锁分配下一版本并拒绝过时编辑,触发器禁止修改/删除已发布内容。数据库所有者仍可执行数据库维护,触发器不是对数据库管理员的安全隔离。 -验证由 ces-control PR CI 执行:严格 XML 单元测试;PostgreSQL 真实发布、所有权、并发版本冲突、不可变内容和撤下保留历史的集成测试。PR 不自动部署,服务器视觉验收须主 chat 人工合并后的部署成功再执行。 +用户手动合并到 main 后,由 ces-control CI 执行验证:严格 XML 单元测试;PostgreSQL 真实发布、所有权、并发版本冲突、不可变内容和撤下保留历史的集成测试。PR 不自动部署,服务器视觉验收须用户手动合并且部署成功后再执行。 首页现已通过 `/api/documents` 加载真实发布摘要,并在 GuidanceShell 提供用户文档与发布入口。`/guide` 继续只读取静态协议样例,不覆盖主 chat 的阅读实现。 From 24beb66484f0d233899e9734dba88d584ce008d3 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 29 Oct 2026 18:03:17 +0800 Subject: [PATCH 4/4] Type guidance integration fixtures against the protocol --- apps/waterfall-wiki/tests/guidance-integration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/waterfall-wiki/tests/guidance-integration.ts b/apps/waterfall-wiki/tests/guidance-integration.ts index 66b5091..c74c0c0 100644 --- a/apps/waterfall-wiki/tests/guidance-integration.ts +++ b/apps/waterfall-wiki/tests/guidance-integration.ts @@ -2,13 +2,13 @@ import assert from 'node:assert/strict'; import {chromium} from 'playwright-core'; import {randomUUID} from 'node:crypto'; import {newToken,tokenDigest,ACCOUNT_COOKIE} from '../src/lib/account-crypto'; -import {guidanceXml} from '../src/content/guides/catalog'; +import {guidanceXml,type Guidance} from '../src/content/guides/catalog'; import {db} from '../src/lib/db'; import {publishDocument,resolveDocument,withdrawDocument,discoverDocuments} from '../src/lib/guidance-store'; export async function verifyGuidancePublishing(base:string){ const owner=randomUUID(),other=randomUUID(),id='user-'+owner+'-document'; for(const uid of [owner,other])await db().query("INSERT INTO account_users(id,email,password_hash,verified_at) VALUES($1,$2,'test',now())",[uid,uid+'@example.com']); - const doc={id,revision:1,kind:'context',title:'实际数据库文档',summary:'摘要',applies:'条件',body:'正文',steps:[],checks:[],links:[],tags:[]}; + const doc:Guidance={id,revision:1,kind:'context',title:'实际数据库文档',summary:'摘要',applies:'条件',body:'正文',steps:[],checks:[],links:[],tags:[]}; await assert.rejects(()=>publishDocument(other,doc),/INVALID_ID/); await publishDocument(other,{...doc,id:'user-'+other+'-document'}); await publishDocument(owner,doc);assert.equal((await resolveDocument(id,1))?.title,doc.title);