Files
yingfungsolar/docs/superpowers/specs/2026-09-11-structure-ai-deploy-design.md
T
philip-cat 81fc515d6e AI Blog 改用 DeepSeek 官方並通用化提示詞
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 文件。
2026-09-12 00:31:26 +08:00

251 lines
10 KiB
Markdown
Raw 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.
# 盈豐太陽能 — 結構優化 + AI Blog + 部署便利 (Design Spec)
- **日期**2026-09-11
- **狀態**:已與客戶確認方向
- **背景**:現有網站(見 `2026-09-11-ying-fung-solar-design.md`)功能完整,但想改善三樣嘢:
1. 資料/表單處理散落手砌,欠型別化驗證層
2. 想有 AI 生成 Blog 文章功能(參考 webtemplate,但簡化、唔要 Google keyword search
3. 部署到 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. 目標與非目標
### 目標
1. **輕量 schema-first 驗證層**:用 Zod 做「admin 表單輸入 + DB 寫入」嘅單一真相來源,得到 typed contract 好處,但零 codegen、零 API layer。
2. **AI Blog 生成**:後台一鍵生成完整 Markdown 草稿(DeepSeek via OpenAI 相容端點),可選 Tavily 上網研究,支援關鍵字佇列。
3. **部署便利**:一鍵本機 setup script + Cloudflare Workers BuildsGit 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 contentItemInputkind 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` 介面:
```ts
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_KEY`
- `TAVILY_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 keywordmark 佢 "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 completionsOpenAI 相容 /chat/completions
6. 解析輸出(TITLE / CONTENT / META_DESC),用 Zod 驗證格式;失敗回錯誤
7. slugify + uniqueSlugautoExcerpt 補 excerpt
8. 插入 postsstatus = "draft"、focusKeyword = topic)→ 回 post id
redirect 去 /admin/post/<id> 覆核
```
**執行方式:同步 await。** POST handler 直接等生成完成(約 1040 秒)再 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 字、GEOsummary-first + Q&A)。
### 3.4 Admin UI(新頁 `/admin/ai`
一頁過包含:
1. **AI 設定表單**`ai_*` keys,密碼類 secret 只顯示「已設定 / 未設定」,唔顯示值)
2. **關鍵字佇列**:新增、刪除、標記 skipped;顯示狀態
3. **生成掣**`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,逐步做:
1. 檢查 `wrangler whoami`(未登入提示 `npm run login`)。
2. `wrangler d1 create yingfung-solar-db`(若已存在/已填 id 則略過)→ 解析 output 拎 `database_id`。
3. `wrangler kv namespace create CACHE` → 解析 id。
4. 將 id 寫入 `wrangler.jsonc`:以字串替換 `PASTE_D1_DATABASE_ID_HERE` / `PASTE_KV_NAMESPACE_ID_HERE`**保留 JSONC 註解同行距**,唔用 JSON.parse/stringify)。
5. 提示輸入後台密碼 → `wrangler secret put ADMIN_PASSWORD`。
6. 套 migration + seedremote)。
7. 可選:`npm run build` + `npx wrangler deploy`。
- 每一步 idempotent(重跑唔會整爛已設定嘅嘢)。
- 唔會自動改 `astro.config.mjs` 嘅 `site`(提示用戶手動改)。
### 4.2 Cloudflare Workers BuildsGit 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` 可完成 provisionREADME 更新 |
三階段相對獨立,逐階段完成並 `npm run build`。
---
## 6. 風險與對策
| 風險 | 對策 |
|---|---|
| Zod 加入 admin POST 改動面大 | 逐個 admin POST handler 遷移;`parseForm` 提供一致 fallbackbuild 驗證 |
| LLM 輸出格式唔穩定 | 用容錯分隔格式 + Zod 驗證,失敗回明確錯誤、唔插入壞資料 |
| 同步生成請求時間長 | Workers I/O 等待唔計 CPU;若超時先改 `cfContext.waitUntil` + 狀態欄 |
| `wrangler.jsonc` 有註解,程式改寫易壞 | 只做精準字串替換,唔 parse 整個檔 |
| Workers Builds 跑 migration 風險 | 明確唔放 CImigration 手動執行 |
| 外部 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`(若用上網研究)。