buildSystemPrompt 不再綁死太陽能行業,角色與公司資料改由 後台 ai_context_prompt / ai_business_context 提供;預設供應商 改為 https://api.deepseek.com 與 deepseek-flash。callChat 只在 baseUrl 含 deepseek.com 時加 thinking: { type: "disabled" }, 並將 max_tokens 提升至 4000,避免思考模式令 temperature 失效 同正文被截斷。 同步更新 schemas、seed、/admin/ai 預設值同 placeholder, 以及 AGENTS.md、spec、plan 文件。
10 KiB
10 KiB
盈豐太陽能 — 結構優化 + AI Blog + 部署便利 (Design Spec)
- 日期:2026-09-11
- 狀態:已與客戶確認方向
- 背景:現有網站(見
2026-09-11-ying-fung-solar-design.md)功能完整,但想改善三樣嘢:- 資料/表單處理散落手砌,欠型別化驗證層
- 想有 AI 生成 Blog 文章功能(參考 webtemplate,但簡化、唔要 Google keyword search)
- 部署到 Cloudflare 嘅流程手續多,想更方便
- 本 spec 取代先前 spec 嘅部署一節;其餘架構決策不變。
- 2026-09-12 更新:AI Blog 提示詞已通用化、預設供應商轉 DeepSeek 官方,詳見
2026-09-12-ai-blog-template-deepseek-design.md;下表ai_base_url/ai_chat_model已同步為新預設。
1. 目標與非目標
目標
- 輕量 schema-first 驗證層:用 Zod 做「admin 表單輸入 + DB 寫入」嘅單一真相來源,得到 typed contract 好處,但零 codegen、零 API layer。
- AI Blog 生成:後台一鍵生成完整 Markdown 草稿(DeepSeek via OpenAI 相容端點),可選 Tavily 上網研究,支援關鍵字佇列。
- 部署便利:一鍵本機 setup script + Cloudflare Workers Builds(Git push 自動 deploy)。
非目標 (Out of scope)
- 唔引入真 OpenAPI spec / codegen / 前後端分離 SPA(已評估唔啱一個一頁式官網)。
- AI 封面圖生成(唔做)。
- 定時/排程自動生成(唔做;只做後台手動觸發)。
- Google Ads keyword discovery(唔做)。
- 圖片上傳(維持以 URL 貼圖)。
- 雙語、聯絡表單(維持現狀)。
2. Thread 1 — 輕量 schema-first 驗證層
2.1 依賴
新增 zod(v4,^4)。
2.2 結構
src/schemas/
├─ post.ts postInput
├─ content.ts contentItemInput(kind enum 用 CONTENT_KINDS)
├─ case.ts caseInput
├─ settings.ts 由 SETTINGS_GROUPS 動態砌出可選 key 驗證
├─ ai.ts aiSettingsInput + AI 輸出格式 schema
├─ keyword.ts keywordInput
└─ index.ts barrel(集中 re-export schema 同 z.infer 型別)
src/lib/form.ts parseForm(formData, schema)
- Schema 係「表單輸入 + DB 寫入」嘅唯一真相來源;型別一律
z.infer。 settings-fields.ts繼續做 UI metadata(標籤、分組),Zod 只負責 驗證,唔重複定義。parseForm介面:錯誤以type ParseResult<T> = | { ok: true; data: T } | { ok: false; errors: Record<string, string> }; function parseForm<T>(form: FormData, schema: ZodType<T>): ParseResult<T>;field -> 訊息形式回傳,admin 表單喺對應欄位下顯示;未對應欄位嘅錯誤顯示喺表單頂。
2.3 改動
pages/admin/post/[id].astro、content/[kind].astro、cases.astro、settings.astro:POST handler 改用parseForm取代手砌String(form.get(...))同手寫if (!title)。data/content.ts:改用src/schemas匯出嘅型別(如NewPost形狀),與 schema 對齊。- 排序(
up/down)、delete等非表單欄位動作維持現狀(唔屬 schema 範圍)。
2.4 唔做
- 唔改前台元件介面、唔改 DB schema(純驗證層)。
- 唔引入
openapi.json、唔引入 client codegen。
3. Thread 2 — AI Blog 生成
3.1 資料模型(改 src/db/schema.ts → npm run db:generate)
posts 加欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
focusKeyword |
text nullable | 生成時帶入嘅焦點關鍵字(SEO 用) |
新表 blog_keywords:
| 欄位 | 型別 | 說明 |
|---|---|---|
id |
text PK | UUID |
keyword |
text notNull | 關鍵字 / 主題 |
status |
text enum pending | generated | skipped,預設 pending |
狀態 |
createdAt |
integer (timestamp_ms) | |
usedAt |
integer (timestamp_ms) nullable | 生成時間 |
postId |
text nullable | 生成出嚟嘅文章 id(可選連結) |
索引:idx_keywords_status(status)。
比 webtemplate 減去 Google Ads 專屬欄位(
avg_monthly_searches、competition、source)。
3.2 Secrets 與設定
Secrets(wrangler secret / .dev.vars,加入 AppEnv):
AI_API_KEYTAVILY_API_KEY
設定(D1 site_settings,後台可改):
| Key | 預設 | 說明 |
|---|---|---|
ai_enabled |
1 |
總開關 |
ai_base_url |
https://api.deepseek.com |
OpenAI 相容端點(DeepSeek 官方) |
ai_chat_model |
deepseek-flash |
模型 |
ai_context_prompt |
"" |
寫作風格 / 語氣 |
ai_business_context |
seed 由公司資料砌 | 公司背景(grounding) |
ai_web_search_enabled |
0 |
Tavily 研究開關 |
ai_web_search_max_results |
5 |
Tavily 結果數 |
3.3 生成流程(src/lib/ai.ts)
POST /admin/ai
action = "generate-topic"(自由輸入 topic)
| "generate-next"(攞下一個 pending keyword)
│
▼
generateBlogPost({ db, env, topic })
1. 讀 ai_* 設定;檢查 ai_enabled + AI_API_KEY
2. 若係 generate-next → 攞最舊一個 pending keyword,mark 佢 "generated" + usedAt + postId;
generate-topic(自由輸入)唔會寫入佇列
3. 若 ai_web_search_enabled + TAVILY_API_KEY → Tavily search → research_brief
4. 組 system prompt:
ai_context_prompt + ai_business_context + research_brief
+ SEO/GEO 硬規則 + 輸出格式合約
5. 呼叫 DeepSeek chat completions(OpenAI 相容 /chat/completions)
6. 解析輸出(TITLE / CONTENT / META_DESC),用 Zod 驗證格式;失敗回錯誤
7. slugify + uniqueSlug;autoExcerpt 補 excerpt
8. 插入 posts(status = "draft"、focusKeyword = topic)→ 回 post id
│
▼
redirect 去 /admin/post/<id> 覆核
執行方式:同步 await。 POST handler 直接等生成完成(約 10–40 秒)再 redirect。原因:最簡單、admin 流程直觀,符合「唔使太複雜」;Workers 對 outbound fetch 等待唔計 CPU limit。若日後遇到超時,先改 Astro.locals.cfContext.waitUntil + 狀態欄(已知 adapter 有注入 locals.cfContext)。
提示合約:
- 輸出用固定分隔格式(TITLE / CONTENT / META_DESC),比純 JSON 容忍度更高。
- 內建規則:繁體中文(廣東話書面)、800–1200 字、H2/H3 結構、關鍵字放標題及首段、meta ≤ 160 字、GEO(summary-first + Q&A)。
3.4 Admin UI(新頁 /admin/ai)
一頁過包含:
- AI 設定表單(
ai_*keys,密碼類 secret 只顯示「已設定 / 未設定」,唔顯示值) - 關鍵字佇列:新增、刪除、標記 skipped;顯示狀態
- 生成掣:
generate-topic(自由輸入)+generate-next(下一個 pending)
- 生成掣用 POST 表單 + 提交時 JS 顯示「生成中…」(純漸進增強,唔引入框架)。
/admin首頁加入口連結去/admin/ai。
3.5 唔做
- AI 圖、排程、Google Ads、多語。
4. Thread 3 — 部署便利
4.1 一鍵本機 setup — scripts/setup.mjs(npm run setup)
互動式 Node script,逐步做:
- 檢查
wrangler whoami(未登入提示npm run login)。 wrangler d1 create yingfung-solar-db(若已存在/已填 id 則略過)→ 解析 output 拎database_id。wrangler kv namespace create CACHE→ 解析 id。- 將 id 寫入
wrangler.jsonc:以字串替換PASTE_D1_DATABASE_ID_HERE/PASTE_KV_NAMESPACE_ID_HERE(保留 JSONC 註解同行距,唔用 JSON.parse/stringify)。 - 提示輸入後台密碼 →
wrangler secret put ADMIN_PASSWORD。 - 套 migration + seed(remote)。
- 可選:
npm run build+npx wrangler deploy。
- 每一步 idempotent(重跑唔會整爛已設定嘅嘢)。
- 唔會自動改
astro.config.mjs嘅site(提示用戶手動改)。
4.2 Cloudflare Workers Builds(Git push 自動 deploy)
- 喺 Cloudflare Dashboard → Workers & Pages → 連接 Git repo。
- Build command:
npm run build - Deploy command:
npx wrangler deploy - Secrets(
ADMIN_PASSWORD、AI_API_KEY、TAVILY_API_KEY)喺 Dashboard 設定。 - Migration 唔入 CI:改 schema 時手動跑
npm run db:migrate(避免每次 push 亂郁 DB)。README 寫明呢個分工。
4.3 唔做
- 唔加 GitHub Actions。
- 唔自動化 Cloudflare Dashboard 連 repo(需要人手授權)。
5. 分階段實作
| 階段 | 內容 | 完成標準 |
|---|---|---|
| Phase 1 | Zod schema-first 驗證層 | npm run build 過;admin 表單驗證錯誤正常顯示 |
| Phase 2 | AI Blog + 關鍵字佇列 | npm run db:generate 出 migration;npm run dev 實測生成草稿 |
| Phase 3 | setup script + Workers Builds 文件 | 新 clone 行 npm run setup 可完成 provision;README 更新 |
三階段相對獨立,逐階段完成並 npm run build。
6. 風險與對策
| 風險 | 對策 |
|---|---|
| Zod 加入 admin POST 改動面大 | 逐個 admin POST handler 遷移;parseForm 提供一致 fallback,build 驗證 |
| LLM 輸出格式唔穩定 | 用容錯分隔格式 + Zod 驗證,失敗回明確錯誤、唔插入壞資料 |
| 同步生成請求時間長 | Workers I/O 等待唔計 CPU;若超時先改 cfContext.waitUntil + 狀態欄 |
wrangler.jsonc 有註解,程式改寫易壞 |
只做精準字串替換,唔 parse 整個檔 |
| Workers Builds 跑 migration 風險 | 明確唔放 CI,migration 手動執行 |
| 外部 AI 供應商停机 | 生成失敗只回錯誤,唔影響網站;設定可改 ai_base_url/model |
7. 驗證
- 每階段:
npm run build(唯一 build/type 檢查)。 - Phase 2:
npm run db:generate;npm run dev手動生成一篇草稿並覆核。 - Phase 3:
npm run setup喺未 provision 環境 dry-run(或已有環境重跑,確認 idempotent)。
8. 文件同步
AGENTS.md(root):指令、部署流程、AI secret。src/AGENTS.md:新schemas/、/admin/ai路由、AI 執行方式。src/db/AGENTS.md:新blog_keywords表、posts.focusKeyword。src/data/AGENTS.md:關鍵字查詢、型別來源改為 schemas。src/lib/AGENTS.md:ai.ts、form.ts、env.ts新 secret。README.md:setup script、Workers Builds、AI 設定步驟。migrations/AGENTS.md:新 migration。
9. 待客戶提供
AI_API_KEY(DeepInfra 或同類 OpenAI 相容供應商)。TAVILY_API_KEY(若用上網研究)。