Files
official-site-base/docs/plans/2026-07-23-phase0-tech-validation.md

861 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 0 技术验证 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 本地跑通「客户在 Sveltia CMS 改内容 → 提交到自建 Gitea → Gitea Action 自动构建 Astro → ossutil 同步到阿里云 OSS」的完整链路,验证设计文档([../specs/2026-07-23-official-site-cms-design.md](../specs/2026-07-23-official-site-cms-design.md))的技术可行性。
**Architecture:** Astro 5SSG)生成纯静态站;Sveltia CMS(CDN SPA)作为客户可视化后台,经 Gitea OAuth 提交 Markdown 到仓库;本地 Docker Gitea + act_runner 提供 Git 后端 + CI;构建产物由 ossutil 推到阿里云 OSS。内容用 Astro Content Collections + Zod schema 强类型校验。
**Tech Stack:** Astro 5 · Sveltia CMS · Gitea+Gitea Actions / act_runner)· ossutil · 阿里云 OSS · Vitest
**前置条件:** 本机已装 Node.js 20+、Docker;后续接真实 OSS 时需阿里云账号 + AK/SK + 已建 bucket。
---
## Scope
本计划**只覆盖 Phase 0 技术验证**(设计文档 §十二 Phase 0)。Phase 1 基座化、Phase 2 首客户上线、Phase 3 Payload 适配器各自后续单独出计划。
**验证成功标准(端到端):**`http://localhost:4321/admin/` 用 Sveltia 新增一条新闻 → 提交 → 本地 Gitea 收到 commit → Gitea Action 触发并构建成功 → `dist/` 生成新页面 → ossutil 脚本可把 `dist/` 同步到 OSS(凭据就绪时实推)。
---
## File Structure
```
official-site-base/
├── package.json # 依赖:astro, vitest
├── astro.config.mjs # Astro 配置
├── tsconfig.json
├── vitest.config.ts
├── src/
│ ├── content.config.ts # Content Collectionsnews+ schema 引用
│ ├── content/
│ │ ├── schemas.ts # Zod schema(可单独单测)
│ │ └── news/
│ │ └── welcome.md # 示例新闻
│ ├── layouts/BaseLayout.astro
│ ├── pages/
│ │ ├── index.astro # 首页
│ │ └── news/
│ │ ├── index.astro # 新闻列表
│ │ └── [slug].astro # 新闻详情
│ └── styles/global.css # 设计 token(白标入口)
├── public/
│ ├── admin/
│ │ ├── index.html # Sveltia CMS 入口
│ │ └── config.yml # Sveltia 配置(gitea backend
│ └── favicon.svg
├── tests/content-schema.test.ts # Zod schema 单测
├── scripts/deploy-oss.sh # ossutil 发布脚本
├── .gitea/workflows/deploy.yml # Gitea Action
├── docker/gitea.docker-compose.yml # 本地 Gitea + runner
├── .env.example # OSS 凭据模板
└── docs/{specs,plans}/...
```
**职责边界:**
- `src/content/schemas.ts` —— 纯 Zod schema,与 Astro 解耦,可单测
- `src/content.config.ts` —— 把 schema 绑到 collection,定义 loader
- `public/admin/config.yml` —— Sveltia 字段配置,与 schema 字段保持一致(重复但必要:Sveltia 是 YAML、Astro 是 TS
- `scripts/deploy-oss.sh` —— 独立发布脚本,本地和 CI 共用(DRY)
---
## Task 1: 初始化 Astro 项目 + 工具链
**Files:**
- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `vitest.config.ts`
- Create: `src/styles/global.css`, `public/favicon.svg`, `.env.example`
- [ ] **Step 1: 初始化 Astro 项目(在 official-site-base 目录内)**
```bash
# 已在 official-site-base 目录,初始化最小 Astro(不加模板)
npm init -y
npm install astro@latest
npm install -D vitest@latest
```
- [ ] **Step 2: 写 package.json scripts**
`package.json``scripts` 改为:
```json
{
"name": "official-site-base",
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"test": "vitest run"
}
}
```
- [ ] **Step 3: 写 astro.config.mjs**
```js
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://example.com',
});
```
- [ ] **Step 4: 写 tsconfig.json**
```json
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
```
- [ ] **Step 5: 写 vitest.config.ts**
```ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['tests/**/*.test.ts'],
},
});
```
- [ ] **Step 6: 写设计 token 入口 src/styles/global.css**
```css
:root {
/* 白标入口:换客户时改这里 */
--color-primary: #007aff;
--color-text: #1a1a1a;
--color-bg: #ffffff;
--font-sans: system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
html { font-family: var(--font-sans); color: var(--color-text); background: var(--color-bg); }
body { margin: 0; max-width: 960px; margin-inline: auto; padding: 1.5rem; }
a { color: var(--color-primary); }
```
- [ ] **Step 7: 写 public/favicon.svg 与 .env.example**
`public/favicon.svg`:
```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#007aff"/><text x="16" y="22" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">O</text></svg>
```
`.env.example`:
```bash
# 阿里云 OSS 发布凭据(复制为 .env 后填真值,.env 已被 .gitignore 排除)
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
OSS_BUCKET=official-site-base-demo
OSS_REGION=cn-hangzhou
# 本地 Gitea
GITEA_BASE_URL=http://localhost:3000
```
- [ ] **Step 8: 验证 Astro 能起**
Run: `npx astro --version && npm run dev`,浏览器开 `http://localhost:4321`Ctrl+C 停止。
Expected: 版本号输出(5.x);dev server 起来无报错(此时还没有页面,404 正常)。
- [ ] **Step 9: Commit**
```bash
git add -A
git commit -m "feat: 初始化 Astro 项目与工具链(vitest/设计token/env"
```
---
## Task 2: news Content Collection + Zod schemaTDD
**Files:**
- Create: `src/content/schemas.ts`, `src/content.config.ts`, `src/content/news/welcome.md`
- Test: `tests/content-schema.test.ts`
- [ ] **Step 1: 写失败测试 tests/content-schema.test.ts**
```ts
import { describe, it, expect } from 'vitest';
import { newsSchema } from '../src/content/schemas';
describe('newsSchema', () => {
it('accepts valid frontmatter and coerces date', () => {
const parsed = newsSchema.parse({
title: '官网基座启动',
date: '2026-07-23',
category: '公告',
});
expect(parsed.title).toBe('官网基座启动');
expect(parsed.date).toEqual(new Date('2026-07-23'));
expect(parsed.category).toBe('公告');
});
it('defaults category to 未分类', () => {
const parsed = newsSchema.parse({ title: '测试', date: '2026-07-23' });
expect(parsed.category).toBe('未分类');
});
it('rejects missing title', () => {
expect(() => newsSchema.parse({ date: '2026-07-23' })).toThrow();
});
it('rejects missing date', () => {
expect(() => newsSchema.parse({ title: '测试' })).toThrow();
});
});
```
- [ ] **Step 2: 运行测试,确认失败**
Run: `npm test`
Expected: FAIL —— `Cannot find module '../src/content/schemas'`(文件还没建)。
- [ ] **Step 3: 写 schema src/content/schemas.ts**
```ts
import { z } from 'astro:content';
export const newsSchema = z.object({
title: z.string(),
date: z.coerce.date(),
category: z.string().default('未分类'),
});
```
> `z` 直接从 `astro:content` 导出(Astro 内置 zod,无需单独装)。
- [ ] **Step 4: 写 collection 配置 src/content.config.ts**
```ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { newsSchema } from './content/schemas';
const news = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/news' }),
schema: newsSchema,
});
export const collections = { news };
```
- [ ] **Step 5: 写示例内容 src/content/news/welcome.md**
```markdown
---
title: 官网基座启动
date: 2026-07-23
category: 公告
---
这是 Phase 0 技术验证的第一条新闻。内容编辑链路跑通后,客户可在 Sveltia CMS 自助新增。
```
- [ ] **Step 6: 运行测试,确认通过**
Run: `npm test`
Expected: PASS4 个测试全过)。
- [ ] **Step 7: Commit**
```bash
git add -A
git commit -m "feat: 新增 news 内容集合与 Zod schema(含单测)"
```
---
## Task 3: 首页 + 新闻列表/详情页面
**Files:**
- Create: `src/layouts/BaseLayout.astro`, `src/pages/index.astro`, `src/pages/news/index.astro`, `src/pages/news/[slug].astro`
- [ ] **Step 1: 写 BaseLayout src/layouts/BaseLayout.astro**
```astro
---
import '../styles/global.css';
interface Props { title: string; }
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<header><a href="/" style="font-weight:700;font-size:1.25rem;">官网基座</a></header>
<main><slot /></main>
<footer style="margin-top:3rem;font-size:.85rem;color:#888;">© 官网基座</footer>
</body>
</html>
```
- [ ] **Step 2: 写首页 src/pages/index.astro**
```astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
const news = (await getCollection('news')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
---
<BaseLayout title="官网基座 · 首页">
<h1>官网基座</h1>
<p>Phase 0 技术验证站点。</p>
<section>
<h2>最新动态</h2>
<ul>
{news.map((post) => (
<li><a href={`/news/${post.id}/`}>{post.data.title}</a> <small>{post.data.date.toLocaleDateString('zh-CN')}</small></li>
))}
</ul>
<p><a href="/news/">查看全部 →</a></p>
</section>
</BaseLayout>
```
- [ ] **Step 3: 写新闻列表 src/pages/news/index.astro**
```astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
const news = (await getCollection('news')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
---
<BaseLayout title="新闻动态">
<h1>新闻动态</h1>
<ul>
{news.map((post) => (
<li>
<a href={`/news/${post.id}/`}>{post.data.title}</a>
<small> · {post.data.category} · {post.data.date.toLocaleDateString('zh-CN')}</small>
</li>
))}
</ul>
</BaseLayout>
```
- [ ] **Step 4: 写新闻详情 src/pages/news/[slug].astro**
```astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const news = await getCollection('news');
return news.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<BaseLayout title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<p><small>{post.data.category} · {post.data.date.toLocaleDateString('zh-CN')}</small></p>
<Content />
</article>
<p><a href="/news/">← 返回列表</a></p>
</BaseLayout>
```
- [ ] **Step 5: 构建并验证产物**
Run: `npm run build`
Expected: `dist/index.html``dist/news/index.html``dist/news/welcome/index.html` 生成,无报错。
Run: `npm run preview`,浏览器开 `http://localhost:4321`,点进新闻详情。
Expected: 首页列出到「官网基座启动」一条;详情页正文渲染。
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: 首页与新闻列表/详情页面"
```
---
## Task 4: Sveltia CMS 接入(/admin
**Files:**
- Create: `public/admin/index.html`, `public/admin/config.yml`
- [ ] **Step 1: 写 CMS 入口 public/admin/index.html**
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex" />
<title>Sveltia CMS</title>
</head>
<body>
<!-- 注意:不要加 type="module",不要加 CSS linkSveltia 自带样式) -->
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
</body>
</html>
```
> 国内生产环境可把 `sveltia-cms.js` 自托管到 OSS 或换国内 CDNunpkg 国内可能慢),Phase 0 先用官方 CDN 验证。
- [ ] **Step 2: 写 Sveltia 配置 public/admin/config.yml**
```yaml
# yaml-language-server: $schema=https://unpkg.com/@sveltia/cms/schema/sveltia-cms.json
# 本地验证用本地 Gitea(Task 5 起的);生产换真实 Gitea 地址
backend:
name: gitea
repo: lzy/official-site-base
base_url: http://localhost:3000
api_root: http://localhost:3000/api/v1
media_folder: /public/media
public_folder: /media
collections:
- name: news
label: 新闻动态
label_singular: 新闻
folder: /src/content/news
create: true
slug: '{{slug}}'
fields:
- { label: 标题, name: title, widget: string }
- { label: 日期, name: date, widget: datetime, type: date }
- { label: 分类, name: category, widget: string, default: 未分类 }
- { label: 正文, name: body, widget: richtext }
```
> Sveltia 的 `folder` 路径要和 Astro `content.config.ts` 的 `base``./src/content/news`)一致;字段要和 `newsSchema` 一一对应(title/date/category)。
- [ ] **Step 3: 验证 CMS 界面能加载**
Run: `npm run dev`,浏览器开 `http://localhost:4321/admin/`
Expected: 看到 Sveltia CMS 登录/界面(此时还没连 Gitea,点登录会报错或重定向失败——属正常,Task 6 接 Gitea 后通)。
- [ ] **Step 4: Commit**
```bash
git add -A
git commit -m "feat: 接入 Sveltia CMS/admin + gitea backend 配置)"
```
---
## Task 5: 本地 Gitea + act_runnerDocker
**Files:**
- Create: `docker/gitea.docker-compose.yml`
- [ ] **Step 1: 写 docker-compose docker/gitea.docker-compose.yml**
```yaml
services:
gitea:
image: gitea/gitea:1
environment:
- USER_UID=1000
- USER_GID=1000
volumes:
- ./gitea-data:/data
ports:
- "3000:3000" # Web
- "2222:22" # SSH
restart: unless-stopped
runner:
image: gitea/act_runner:latest
environment:
- GITEA_INSTANCE_URL=http://gitea:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=phase0-local-token
volumes:
- ./runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- gitea
restart: unless-stopped
```
- [ ] **Step 2: 启动 Gitea + runner**
```bash
cd docker
docker compose -f gitea.docker-compose.yml up -d
docker compose -f gitea.docker-compose.yml ps
```
Expected: 两个容器 `Up`
- [ ] **Step 3: 完成 Gitea 首次安装(Web 向导)**
浏览器开 `http://localhost:3000`
- 数据库选 SQLite(默认)
- 站点 URL`http://localhost:3000`
- 创建管理员账户:用户名 `lzy`,密码自设(记牢)
- 点「立即安装」
Expected: 安装完成,跳转登录页,能登入。
- [ ] **Step 4: 注册 runner 到 Gitea**
在 Gitea Web`http://localhost:3000/admin/actions/runners`,确认 `runner` 容器已注册并 `idle`compose 里 token 写死为 `phase0-local-token`,runner 自动注册)。如未注册,到「Site Administration → Actions → Runners」用 `phase0-local-token` 手动添加。
Expected: runners 列表有一条 `idle` 的 runner。
- [ ] **Step 5: 把 official-site-base 推到本地 Gitea**
```bash
# 在 official-site-base 目录
git remote add local http://localhost:3000/lzy/official-site-base.git
# 先在 Gitea Web 「新建仓库」official-site-base(空仓库,不要勾 README
git push -u local main
```
Expected: 推送成功,Gitea 上能看到代码。
- [ ] **Step 6: Commit docker 配置**
```bash
# 回 official-site-base 目录
git add docker/gitea.docker-compose.yml
git commit -m "chore: 本地 Gitea + act_runner docker-compose"
```
> 注意:`docker/gitea-data/` 和 `docker/runner-data/` 不要提交(本地数据),后续加到 .gitignore。
- [ ] **Step 7: 补 .gitignore 排除本地数据**
`.gitignore` 末尾追加:
```
# 本地 Gitea/runner 数据
docker/gitea-data/
docker/runner-data/
```
```bash
git add .gitignore
git commit -m "chore: 忽略本地 Gitea/runner 数据目录"
```
---
## Task 6: Gitea OAuth App + Sveltia 提交链路验证
**Files:**
- Modify: `public/admin/config.yml`(确认 base_url
- [ ] **Step 1: 在 Gitea 创建 OAuth2 应用**
Gitea Web:头像 → Settings → Applications → **OAuth2 Applications** → 「Create Application」:
- Name: `Sveltia CMS`
- Redirect URI: `http://localhost:4321/admin/`
- 创建后记录 `Client ID``Client Secret`
Expected: 拿到一对 Client ID / Secret。
- [ ] **Step 2: 在 Sveltia config 补 auth 配置**
修改 `public/admin/config.yml` 的 backend 段(追加 `app_id` / `client_id`Sveltia gitea backend 用 OAuth):
```yaml
backend:
name: gitea
repo: lzy/official-site-base
base_url: http://localhost:3000
api_root: http://localhost:3000/api/v1
# Gitea OAuth2 AppStep 1 创建)
app_id: <填 Client ID>
```
> `client_secret` 不写进配置(Sveltia gitea backend 走 PKCE/标准 OAuth 流,仅用 client_id + redirect;如 Gitea 要求 secret,参考 Sveltia Backends 文档配 `auth_type`)。Phase 0 若 PKCE 流不通,降级用 `git-gateway` 或本地 token 方式,记录到 spec 风险项。
- [ ] **Step 3: 验证 Sveltia 登录 Gitea**
Run: `npm run dev`,浏览器开 `http://localhost:4321/admin/`,点登录。
Expected: 跳转到 Gitea 授权页 → 授权 → 回到 Sveltia 看到「新闻动态」集合,能看到 Task 2 的 welcome.md。
- [ ] **Step 4: 新增一条新闻并提交**
在 Sveltia:进入「新闻动态」→「新增新闻」→ 填标题/日期/分类/正文 → 保存 → 「Publish」。
Expected: 提交成功,本地 Gitea 仓库(`http://localhost:3000/lzy/official-site-base`)出现新 commit`src/content/news/` 多一个 md 文件。
- [ ] **Step 5: 拉取并验证内容进入构建**
```bash
git pull local main
ls src/content/news/
npm run build
```
Expected: 新文件出现;build 产物 `dist/news/<新slug>/index.html` 生成。
- [ ] **Step 6: Commit config 调整**
```bash
git add public/admin/config.yml
git commit -m "feat: Sveltia 接 Gitea OAuth,跑通提交链路"
```
---
## Task 7: Gitea Action 自动构建
**Files:**
- Create: `.gitea/workflows/deploy.yml`
- [ ] **Step 1: 写工作流 .gitea/workflows/deploy.yml**
```yaml
name: build-and-deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install deps
run: npm ci
- name: Build
run: npm run build
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
- name: Sync to OSS
env:
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
run: bash scripts/deploy-oss.sh
```
> Gitea Actions 兼容 GitHub Actions 语法;`secrets` 在 Gitea 仓库 Settings → Actions → Secrets 配。OSS 同步在 Task 8 写脚本;本地验证时若未配 OSS secrets,该 step 会跳过(脚本内判空)。
- [ ] **Step 2: 推送触发 Action**
```bash
git add .gitea/workflows/deploy.yml
git commit -m "ci: Gitea Action 构建并发布到 OSS"
git push local main
```
- [ ] **Step 3: 验证 Action 执行**
Gitea Web`http://localhost:3000/lzy/official-site-base/actions`
Expected: 看到一次 push 触发的运行,`build` 作业绿色,artifact `dist` 上传成功;`Sync to OSS` 步骤因无凭据显示跳过/失败(Task 8 配齐后转绿)。
---
## Task 8: ossutil 发布脚本
**Files:**
- Create: `scripts/deploy-oss.sh`
- [ ] **Step 1: 安装 ossutil(如未装)**
```bash
# 参考 https://help.aliyun.com/document_detail/120075.html
# macOS/Linux:
curl -o /usr/local/bin/ossutil https://gosspublic.alicdn.com/ossutil/install.sh && bash /usr/local/bin/ossutil
ossutil version
```
> Windows 下载 `ossutil64.exe` 放进 PATH。Phase 0 本地不强求装;CI 环境在 workflow 里装。
- [ ] **Step 2: 写脚本 scripts/deploy-oss.sh**
```bash
#!/usr/bin/env bash
set -euo pipefail
# 判空:没配凭据时跳过(Phase 0 本地/未配 OSS 时不阻塞)
if [ -z "${OSS_ACCESS_KEY_ID:-}" ] || [ -z "${OSS_BUCKET:-}" ]; then
echo "[deploy-oss] 未配置 OSS 凭据,跳过同步。"
exit 0
fi
: "${OSS_ACCESS_KEY_SECRET:?需 OSS_ACCESS_KEY_SECRET}"
: "${OSS_ENDPOINT:?需 OSS_ENDPOINT}"
: "${OSS_BUCKET:?需 OSS_BUCKET}"
DIST_DIR="${1:-dist}"
[ -d "$DIST_DIR" ] || { echo "[deploy-oss] $DIST_DIR 不存在,请先 build"; exit 1; }
echo "[deploy-oss] 同步 $DIST_DIR → oss://$OSS_BUCKET/"
ossutil sync "$DIST_DIR" "oss://$OSS_BUCKET/" \
-i "$OSS_ACCESS_KEY_ID" \
-k "$OSS_ACCESS_KEY_SECRET" \
-e "$OSS_ENDPOINT" \
--force --delete
echo "[deploy-oss] 刷新 CDN(如配了 CDN_DOMAIN"
if [ -n "${CDN_DOMAIN:-}" ]; then
ossutil cdn refresh --dirs "https://${CDN_DOMAIN}/" \
-i "$OSS_ACCESS_KEY_ID" -k "$OSS_ACCESS_KEY_SECRET" -e "$OSS_ENDPOINT" || true
fi
echo "[deploy-oss] 完成"
```
- [ ] **Step 3: 本地空跑验证(无凭据应跳过)**
```bash
chmod +x scripts/deploy-oss.sh
./scripts/deploy-oss.sh
```
Expected: 输出 `[deploy-oss] 未配置 OSS 凭据,跳过同步。`,退出码 0。
- [ ] **Step 4: 本地真实推送验证(需 OSS 凭据)**
在阿里云控制台建 bucket(如 `official-site-base-demo`,公共读),拿 AK/SK
```bash
cp .env.example .env
# 编辑 .env 填真值
set -a; source .env; set +a # bash 加载环境变量
npm run build
./scripts/deploy-oss.sh
```
Expected: 输出同步文件列表;浏览器开 `https://official-site-base-demo.oss-cn-hangzhou.aliyuncs.com/index.html` 能看到首页。
- [ ] **Step 5: 配 Gitea Secrets 让 CI 同步**
Gitea Web:仓库 Settings → Actions → Secrets,新增:
- `OSS_ACCESS_KEY_ID``OSS_ACCESS_KEY_SECRET``OSS_ENDPOINT``OSS_BUCKET`
再 push 一次:
```bash
git add scripts/deploy-oss.sh
git commit -m "feat: ossutil 发布脚本(本地与 CI 共用)"
git push local main
```
Expected: CI 的 `Sync to OSS` 步骤变绿,OSS 内容更新。
---
## Task 9: 端到端验证 + README 更新
**Files:**
- Modify: `README.md`
- [ ] **Step 1: 端到端验证清单**
按下列顺序操作并逐条勾选:
- [ ] `npm run dev`,开 `http://localhost:4321/admin/`,登录 Gitea
- [ ] Sveltia 新增一条新闻 → Publish
- [ ] 本地 Gitea 仓库出现新 commit
- [ ] `git pull local main`,本地 `src/content/news/` 出现新 md
- [ ] push 后 Gitea Action 触发并构建成功
- [ ] OSS 同步成功(如已配凭据)
- [ ] `npm run build` + preview,首页与详情页展示新内容
- [ ] **Step 2: 更新 README.md 状态段**
把 README 的「状态」段从:
```
📋 设计已确认,待出实施计划(Phase 0 技术验证)。
```
改为:
```
✅ Phase 0 技术验证已跑通:Sveltia CMS → Gitea → Action → OSS 全链路。
## 本地开发
npm install
npm run dev # 站点 http://localhost:4321CMS http://localhost:4321/admin/
docker compose -f docker/gitea.docker-compose.yml up -d # 起 Gitea
## 发布
npm run build
./scripts/deploy-oss.sh # 需配 .env 的 OSS 凭据
```
- [ ] **Step 3: 最终提交**
```bash
git add README.md
git commit -m "docs: Phase 0 验证完成,更新 README 与本地开发说明"
git push local main
```
---
## Self-Review
**1. Spec 覆盖检查**(对照 spec §五 内容模型 / §四架构 / §九部署):
- ✅ news 集合 + Zod schemaTask 2)—— 代表 Content Collections 机制,其余 7 个集合在 Phase 1 基座化时按同模式补
- ✅ Sveltia CMS /admin 可视化编辑(Task 4
- ✅ 自建 Gitea + ActionsTask 5/7
- ✅ 阿里云 OSS 发布(Task 8
- ✅ 端到端链路验证(Task 9
- ⏭ 留待后续计划:jobs/downloads/messages 集合、表单微服务、白标换 token 脚本、CMS 适配器接口(Phase 1-3
**2. 占位符扫描:** Task 6 Step 2 的 `<填 Client ID>` 是执行时填入的真实值(来自 Step 1 创建的 App),非 TODO 占位符;其余步骤代码完整。✅
**3. 类型/命名一致性:**
- `newsSchema` 字段(title/date/category)在 Task 2 schema、Task 4 config.yml、Task 3 页面三处一致 ✅
- collection 名 `news` 在 content.config.ts、config.yml、页面 getCollection('news') 一致 ✅
- OSS 环境变量名(OSS_ACCESS_KEY_ID 等)在 .env.example、deploy-oss.sh、deploy.yml secrets 三处一致 ✅
- Gitea 仓库路径 `lzy/official-site-base` 在 config.yml、push 命令一致 ✅
**已知风险(执行时留意,已写入 spec §十一):**
- Gitea OAuth PKCE 流是否开箱通(Task 6 Step 2 注明了降级方案)
- `act_runner` 注册 tokencompose 写死 `phase0-local-token`,仅本地)
- unpkg CDN 国内访问速度(生产换自托管)
---
## 执行记录(2026-07-23 实施偏差与现状)
代码侧 Task 1-4、7、8 + Task 5 文件部分已完成(`feat/phase0-tech-validation` 分支,7 commit)。
**实际依赖版本(偏离计划 `@latest`):**
| 包 | 计划 | 实际 | 原因 |
|----|------|------|------|
| astro | latest(7.1) | **5.18.2** | latest 要 node≥22.12,本机 node 22.11 被拒启动 |
| vitest | latest(4) | **3.2.7** | latest(4) 依赖 rolldownWindows native binding 装不上 |
| zod | latest(4) | **3.25.76** | astro 5 内置 zod 3zod 4 与 astro:content 不兼容 |
**其他调整:**
- Task 2 schema 改用独立 `zod` 包(`import { z } from 'zod'`)而非 `astro:content`,让 vitest 能直接测(`astro:content` 是虚拟模块);版本与 astro 内置 dedupe 为同一份 3.25.76。
- Sveltia CMS backends 已查实:原生支持 GitHub / GitLab / **Gitea****不支持 Gitee / Bitbucket**(故选自建 Gitea,见 spec §三)。
**验证已过:** schema 单测 4 个全过;`astro build` 出 3 页;`deploy-oss.sh` 空跑跳过退出 0。
**阻塞(待用户决策):** 本机无 DockerTask 5(起 Gitea/ 6OAuth/ 9(端到端)无法本地跑通。Gitea 部署方式三选一:① 本地装 Docker Desktop ② 本地 Gitea 二进制 ③ 上服务器(ops 机或新轻量云)。