From 81fc515d6ee03bed0b45455079c4921002a232ef Mon Sep 17 00:00:00 2001 From: philipcheung Date: Sat, 12 Sep 2026 00:31:26 +0800 Subject: [PATCH] =?UTF-8?q?AI=20Blog=20=E6=94=B9=E7=94=A8=20DeepSeek=20?= =?UTF-8?q?=E5=AE=98=E6=96=B9=E4=B8=A6=E9=80=9A=E7=94=A8=E5=8C=96=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=A9=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 文件。 --- .astro/content.d.ts | 159 ++++++++++++++++++ .astro/types.d.ts | 3 +- AGENTS.md | 2 +- .../plans/2026-09-11-structure-ai-deploy.md | 2 + .../2026-09-11-structure-ai-deploy-design.md | 5 +- ...-09-12-ai-blog-template-deepseek-design.md | 120 +++++++++++++ migrations/seed.sql | 12 +- src/lib/AGENTS.md | 4 +- src/lib/ai.ts | 11 +- src/pages/admin/ai.astro | 18 +- src/schemas/ai.ts | 4 +- 11 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 .astro/content.d.ts create mode 100644 docs/superpowers/specs/2026-09-12-ai-blog-template-deepseek-design.md diff --git a/.astro/content.d.ts b/.astro/content.d.ts new file mode 100644 index 0000000..2bf13df --- /dev/null +++ b/.astro/content.d.ts @@ -0,0 +1,159 @@ +declare module 'astro:content' { + export interface RenderResult { + Content: import('astro/runtime/server/index.js').AstroComponentFactory; + headings: import('astro').MarkdownHeading[]; + remarkPluginFrontmatter: Record; + } + interface Render { + '.md': Promise; + } + + export interface RenderedContent { + html: string; + metadata?: { + imagePaths: Array; + [key: string]: unknown; + }; + } + + type Flatten = T extends { [K: string]: infer U } ? U : never; + + export type CollectionKey = keyof DataEntryMap; + export type CollectionEntry = Flatten; + + type AllValuesOf = T extends any ? T[keyof T] : never; + + export type ReferenceDataEntry< + C extends CollectionKey, + E extends keyof DataEntryMap[C] = string, + > = { + collection: C; + id: E; + }; + + export type ReferenceLiveEntry = { + collection: C; + id: string; + }; + + export function getCollection>( + collection: C, + filter?: (entry: CollectionEntry) => entry is E, + ): Promise; + export function getCollection( + collection: C, + filter?: (entry: CollectionEntry) => unknown, + ): Promise[]>; + + export function getLiveCollection( + collection: C, + filter?: LiveLoaderCollectionFilterType, + ): Promise< + import('astro').LiveDataCollectionResult, LiveLoaderErrorType> + >; + + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + entry: ReferenceDataEntry, + ): E extends keyof DataEntryMap[C] + ? Promise + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + collection: C, + id: E, + ): E extends keyof DataEntryMap[C] + ? string extends keyof DataEntryMap[C] + ? Promise | undefined + : Promise + : Promise | undefined>; + export function getLiveEntry( + collection: C, + filter: string | LiveLoaderEntryFilterType, + ): Promise, LiveLoaderErrorType>>; + + /** Resolve an array of entry references from the same collection */ + export function getEntries( + entries: ReferenceDataEntry[], + ): Promise[]>; + + export function render( + entry: DataEntryMap[C][string], + ): Promise; + + export function render( + entry: import('astro').LiveDataEntry>, + ): Promise; + + export function reference< + C extends + | keyof DataEntryMap + // Allow generic `string` to avoid excessive type errors in the config + // if `dev` is not running to update as you edit. + // Invalid collection names will be caught at build time. + | (string & {}), + >( + collection: C, + ): import('astro/zod').ZodPipe< + import('astro/zod').ZodString, + import('astro/zod').ZodTransform< + C extends keyof DataEntryMap + ? { + collection: C; + id: string; + } + : never, + string + > + >; + + type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; + type InferEntrySchema = import('astro/zod').infer< + ReturnTypeOrOriginal['schema']> + >; + type ExtractLoaderConfig = T extends { loader: infer L } ? L : never; + type InferLoaderSchema< + C extends keyof DataEntryMap, + L = ExtractLoaderConfig, + > = L extends { schema: import('astro/zod').ZodSchema } + ? import('astro/zod').infer + : any; + + type DataEntryMap = { + + }; + + type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< + infer TData, + infer TEntryFilter, + infer TCollectionFilter, + infer TError + > + ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } + : { data: never; entryFilter: never; collectionFilter: never; error: never }; + type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; + type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; + type ExtractErrorType = ExtractLoaderTypes['error']; + type ExtractDataType = ExtractLoaderTypes['data']; + + type LiveLoaderDataType = + LiveContentConfig['collections'][C]['schema'] extends undefined + ? ExtractDataType + : import('astro/zod').infer< + Exclude + >; + type LiveLoaderEntryFilterType = + ExtractEntryFilterType; + type LiveLoaderCollectionFilterType = + ExtractCollectionFilterType; + type LiveLoaderErrorType = ExtractErrorType< + LiveContentConfig['collections'][C]['loader'] + >; + + export type ContentConfig = never; + export type LiveContentConfig = never; +} diff --git a/.astro/types.d.ts b/.astro/types.d.ts index f752936..d95e66d 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1,3 @@ /// -/// \ No newline at end of file +/// +/// \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 5045d3f..2b604dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Astro 7 + React 19 + Chakra UI v3,部署在 Cloudflare Workers(單一 Worker - **資料層**:`getDb(getEnv().DB)` → Drizzle;查詢集中在 `src/data/content.ts`,schema 在 `src/db/schema.ts`。改 schema 後跑 `npm run db:generate` 產生 migration。 - **Seed 與 migration 是分開的**:`db:migrate:local` / `db:migrate` 只套 migrations,`migrations/seed.sql` 要用 `db:seed:local` / `db:seed` 另外執行;seed 可重複跑。 - **表單驗證集中在 `src/schemas/`**(Zod)+ `src/lib/form.ts` 的 `parseForm(form, schema)`:admin 頁面唔好再手寫逐欄驗證。實體輸入型別用 `z.infer` 由 schemas 匯出。 -- **AI Blog**:`src/lib/ai.ts`(OpenAI 相容 chat completions + Tavily)+ `/admin/ai`;同步生成、一律存草稿並標記關鍵字 `generated`。需要 secrets `AI_API_KEY`(必需)/`TAVILY_API_KEY`(可選);未設時回明確錯誤,唔會 throw。 +- **AI Blog**:`src/lib/ai.ts`(OpenAI 相容 chat completions + Tavily)+ `/admin/ai`;**預設供應商 DeepSeek 官方(`https://api.deepseek.com`、`deepseek-flash`)**,提示詞通用唔綁行業(角色/公司資料由後台 `ai_context_prompt`/`ai_business_context` 提供)。同步生成、一律存草稿並標記關鍵字 `generated`。需要 secrets `AI_API_KEY`(必需)/`TAVILY_API_KEY`(可選);未設時回明確錯誤,唔會 throw。已部署 DB 轉供應商要喺 `/admin/ai` 改,唔好重跑 `db:seed`(會覆蓋後台設定)。 - **部署用 Cloudflare Workers Builds**(Git push 自動):build `npm run build`、deploy `npx wrangler deploy`。**Migration 唔會喺 CI 執行**,改 schema 要手動 `npm run db:migrate`(雲端)。 - **`wrangler.jsonc` 有佔位 id**:`database_id` / KV `id` 要先用 `db:create` / `kv:create` 產生再貼上,未貼前無法部署。 - **上線前要改 `astro.config.mjs` 的 `site`**:sitemap / canonical / OG 全部靠它(現為 `https://example.com`)。 diff --git a/docs/superpowers/plans/2026-09-11-structure-ai-deploy.md b/docs/superpowers/plans/2026-09-11-structure-ai-deploy.md index c0e5587..0a8e4ae 100644 --- a/docs/superpowers/plans/2026-09-11-structure-ai-deploy.md +++ b/docs/superpowers/plans/2026-09-11-structure-ai-deploy.md @@ -12,6 +12,8 @@ **Spec:** `docs/superpowers/specs/2026-09-11-structure-ai-deploy-design.md` +> **2026-09-12 更新:** AI Blog 提示詞已通用化、預設供應商轉 DeepSeek 官方(`https://api.deepseek.com` / `deepseek-flash`),詳見 `docs/superpowers/specs/2026-09-12-ai-blog-template-deepseek-design.md`。本計劃內舊嘅 `api.deepinfra.com` / `deepseek-ai/DeepSeek-V3-0324` 值已過時。 + --- ## 檔案結構 diff --git a/docs/superpowers/specs/2026-09-11-structure-ai-deploy-design.md b/docs/superpowers/specs/2026-09-11-structure-ai-deploy-design.md index 40c3172..5d33e1e 100644 --- a/docs/superpowers/specs/2026-09-11-structure-ai-deploy-design.md +++ b/docs/superpowers/specs/2026-09-11-structure-ai-deploy-design.md @@ -7,6 +7,7 @@ 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` 已同步為新預設。 --- @@ -110,8 +111,8 @@ src/lib/form.ts parseForm(formData, schema) | Key | 預設 | 說明 | |---|---|---| | `ai_enabled` | `1` | 總開關 | -| `ai_base_url` | `https://api.deepinfra.com/v1/openai` | OpenAI 相容端點 | -| `ai_chat_model` | `deepseek-ai/DeepSeek-V3-0324` | 模型 | +| `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 研究開關 | diff --git a/docs/superpowers/specs/2026-09-12-ai-blog-template-deepseek-design.md b/docs/superpowers/specs/2026-09-12-ai-blog-template-deepseek-design.md new file mode 100644 index 0000000..d66eceb --- /dev/null +++ b/docs/superpowers/specs/2026-09-12-ai-blog-template-deepseek-design.md @@ -0,0 +1,120 @@ +# AI Blog 通用化(Template-ready)+ 轉用 DeepSeek 官方 (Design Spec) + +- **日期**:2026-09-12 +- **狀態**:已與客戶確認方向 +- **背景**:本網站打算做成可重用 template,套用到唔同公司。但現時 AI Blog 嘅 system prompt 硬編碼咗「香港村屋太陽能公司『盈豐太陽能』」,只啱太陽能公司用。同時客戶想由 DeepInfra 轉用 DeepSeek 官方供應商。 +- **本 spec 只涵蓋 AI Blog 生成**;其他太陽能硬編碼內容(`config.ts`、其他 seed、前台文案)唔喺今次範圍。 +- **相關**:`2026-09-11-structure-ai-deploy-design.md`(AI Blog 原始設計)依然有效,本 spec 係其「通用化 + 供應商」修訂。 + +--- + +## 1. 目標與非目標 + +### 目標 + +1. **提示詞通用化**:`src/lib/ai.ts` 嘅 `buildSystemPrompt` 唔再綁死任何行業/公司;角色同公司資料一律由後台設定提供,令同一套 code 可套用到任何行業。 +2. **轉用 DeepSeek 官方**:預設供應商改成 `https://api.deepseek.com`+`deepseek-flash`,並處理 DeepSeek「思考模式預設開啟」對生成嘅影響。 +3. **後台易輸入**:`/admin/ai` 為公司背景同寫作風格加格式指引,令新公司開箱即用。 + +### 非目標 (Out of scope) + +- 輸出格式維持 `TITLE / CONTENT / META_DESC`;**唔加** SECTION、**唔加** AI 封面圖(同原 spec 一致)。 +- 關鍵字佇列、`generate-next`、Tavily 研究、供應商設定 UI 行為不變。 +- 唔改關鍵字 discovery/排程生成(維持唔做)。 +- `config.ts`、其他太陽能 seed 內容唔郁。 + +--- + +## 2. 提示詞模型(結構化欄位) + +沿用現有「結構化欄位」做法(同 webtemplate 一致),唔引入完整可編輯 prompt 模板: + +| 欄位 | 來源 | 角色 | +|---|---|---| +| `ai_context_prompt` | 後台 | 角色/寫作風格/語氣/目標受眾(可寫行業) | +| `ai_business_context` | 後台 | 公司事實(grounding):公司名稱/行業/業務範圍/主要地區/主要產品服務/目標客群/特色定位 | +| (SEO/GEO 規則 + 輸出格式) | `ai.ts` code | 通用、唔綁行業,維持穩定輸出 | + +`buildSystemPrompt` 組合順序維持:`contextPrompt → businessContext → research → 通用任務指示`。 + +通用任務指示(取代原硬編碼角色): + +``` +你係一位專業嘅 SEO 內容寫手。請根據上面嘅公司背景同寫作風格, +就以下主題寫一篇 800–1200 字嘅繁體中文(香港廣東話書面語)博客文章。 +主題: +要求: +- 標題要放焦點關鍵字,首 100 字內再出現一次,關鍵字密度約 1–2%。 +- 用 ## / ### 分段,段落清晰易讀。 +- 開頭先畀一段總結(summary-first),再展開。 +- 內容要實用、準確,符合公司業務;唔好作出未經證實嘅承諾或數字。 +- 適合 SEO 同 AI 搜尋(GEO):用問答式小標題、必要時用列表。 +輸出格式(必須嚴格跟隨,唔要加額外說明): +TITLE: <文章標題> +CONTENT: + +META_DESC: <160 字以內 SEO 描述> +``` + +語言維持「繁體中文(香港)」(本網站只做中文);如需調整語言/語氣,經 `ai_context_prompt` 指定。 + +--- + +## 3. DeepSeek 供應商 + +### 3.1 預設值 + +| Key | 舊值 | 新值 | +|---|---|---| +| `ai_base_url` | `https://api.deepinfra.com/v1/openai` | `https://api.deepseek.com` | +| `ai_chat_model` | `deepseek-ai/DeepSeek-V3-0324` | `deepseek-flash` | + +DeepSeek base URL 係 OpenAI 相容,現有 `callChat` 會自動接 `/chat/completions`,無需改 endpoint 邏輯。 + +模型選擇:`deepseek-flash`(DeepSeek-V4.1-Flash)——快、平、支援繁體中文,適合每日 SEO blog。後台仍可自行改為 `deepseek-v4-pro` 或其他供應商。 + +### 3.2 思考模式 + +DeepSeek 思考模式**預設開啟**(effort `high`),會令:`temperature` 失效、reasoning token 佔用 `max_tokens`(有機會正文被截斷)。 + +決策:**生成時關閉思考模式**。 +- `callChat` 只在 `baseUrl` 含 `deepseek.com` 時加 `thinking: { type: "disabled" }`,保留 `temperature: 0.7`。 +- 其他 OpenAI 相容供應商**唔會**收到非標準 `thinking` 參數,維持兼容。 +- `max_tokens` 由 `2200` 提升到 `4000`(中文 800–1200 字連 Markdown 需要)。 + +### 3.3 現行已部署 D1 + +`site_settings` 由 DB 讀取,code 預設只在 key 缺失時生效。已部署 DB 仍有舊 deepinfra 值,處理方式: + +- **部署後手動**去 `/admin/ai` 改 Base URL+模型;`AI_API_KEY` secret 換成 DeepSeek key(`wrangler secret put AI_API_KEY`)。 +- **唔重跑 `db:seed`**:seed 用 `INSERT OR REPLACE`,會覆蓋所有後台改過嘅設定。 + +--- + +## 4. 檔案改動 + +| 檔案 | 改動 | +|---|---| +| `src/lib/ai.ts` | 通用化 `buildSystemPrompt`;預設 baseUrl/model 改 DeepSeek;`callChat` 加 conditional thinking disabled、`max_tokens: 4000` | +| `src/schemas/ai.ts` | `ai_base_url` / `ai_chat_model` 預設值同步 | +| `migrations/seed.sql` | `ai_base_url` / `ai_chat_model` 改預設;`ai_business_context` 改通用格式範本 | +| `src/pages/admin/ai.astro` | 模型/URL 預設值同步;公司背景+寫作風格加 placeholder/格式說明 | +| `src/lib/AGENTS.md`、根 `AGENTS.md` | 更新 AI Blog 合約描述(DeepSeek 預設、通用提示詞) | + +--- + +## 5. 驗證 + +- `npm run build`(唯一 build 驗證)。 +- `npm run dev`(:4321):`.dev.vars` 填 DeepSeek `AI_API_KEY` → 登入 `/admin/ai` → 用通用公司背景 + 一個主題生成,確認: + 1. 草稿成功寫入並 redirect 去編輯頁; + 2. 唔再出現太陽能字眼(除非公司背景自己寫); + 3. 無 timeout、正文完整(未被 max_tokens 截斷)。 + +--- + +## 6. 決策記錄 + +- 提示詞採「結構化欄位」而唔係完整可編輯 prompt 模板:保輸出格式穩定、最貼近 webtemplate、改動最少(YAGNI)。 +- 模型預設 `deepseek-flash`:成本/速度平衡;`deepseek-v4-pro` 貴約 3–4 倍。 +- 關閉思考模式:格式最穩定、最快、最便宜;temperature 生效。 diff --git a/migrations/seed.sql b/migrations/seed.sql index 8f251a6..291b833 100644 --- a/migrations/seed.sql +++ b/migrations/seed.sql @@ -23,10 +23,16 @@ INSERT OR REPLACE INTO site_settings (key, value, updated_at) VALUES INSERT OR REPLACE INTO site_settings (key, value, updated_at) VALUES ('ai_enabled', '1', strftime('%s','now')*1000), - ('ai_base_url', 'https://api.deepinfra.com/v1/openai', strftime('%s','now')*1000), - ('ai_chat_model', 'deepseek-ai/DeepSeek-V3-0324', strftime('%s','now')*1000), + ('ai_base_url', 'https://api.deepseek.com', strftime('%s','now')*1000), + ('ai_chat_model', 'deepseek-flash', strftime('%s','now')*1000), ('ai_context_prompt', '用香港繁體中文(廣東話書面語)書寫,語氣專業、親切、務實,避免誇大。', strftime('%s','now')*1000), - ('ai_business_context', '盈豐太陽能工程有限公司專營香港村屋太陽能系統:現場評估、合法鋁合金支架、代辦中電/港燈上網電價(FiT)申請、專業安裝及驗收認證。着重透明報價、專人跟進、安全合規。', strftime('%s','now')*1000), + ('ai_business_context', '公司名稱: +行業: +業務範圍: +主要地區: +主要產品服務: +目標客群: +特色定位:', strftime('%s','now')*1000), ('ai_web_search_enabled', '0', strftime('%s','now')*1000), ('ai_web_search_max_results', '5', strftime('%s','now')*1000); diff --git a/src/lib/AGENTS.md b/src/lib/AGENTS.md index 33fe3c5..cff8ccc 100644 --- a/src/lib/AGENTS.md +++ b/src/lib/AGENTS.md @@ -19,8 +19,10 @@ - 密碼同 session 比對用 `safeEqual`(constant-time),改動要保留防 timing attack 嘅做法。 - **`form.ts`**:`parseForm(formData, schema)` 將 `FormData` 抽成物件交畀 Zod 驗證;回 `{ ok: true, data }` 或 `{ ok: false, errors }`(field → 訊息)。admin 表單一律經呢度,schema 喺 `src/schemas/`。 - **`ai.ts`**:AI 生成 Blog 流程。 - - `getAiSettings(db)` 由 `site_settings` 砌出供應商設定(enabled / baseUrl / model / context / web search)。 + - `getAiSettings(db)` 由 `site_settings` 砌出供應商設定(enabled / baseUrl / model / context / web search)。**預設供應商係 DeepSeek 官方**(`https://api.deepseek.com`、模型 `deepseek-flash`)。 + - `buildSystemPrompt` **通用、唔綁行業**:角色/語氣來自 `ai_context_prompt`,公司事實來自 `ai_business_context`;硬編碼只剩通用 SEO/GEO 規則同 `TITLE` / `CONTENT` / `META_DESC` 輸出格式。新增行業內容一律經後台設定,唔好再寫死公司名。 - Tavily 上網研究(失敗回空字串,唔中斷)+ OpenAI 相容 `chat/completions`(有 timeout)。 + - `callChat` 固定 `max_tokens: 4000`;**只在 baseUrl 含 `deepseek.com` 時**加 `thinking: { type: "disabled" }`(DeepSeek 思考模式預設開啟,會令 temperature 失效同佔用 token),其他供應商唔會收到非標準參數。 - `parseGeneratedPost(text)` 解析 `TITLE` / `CONTENT` / `META_DESC` 並用 `aiGeneratedPost` schema 驗證。 - `generateBlogPost(db, env, input)` 同步生成:`{ mode: "topic" }` 或 `{ mode: "next" }`(攞佇列下一個 pending);一律存 **draft** 並將關鍵字標記 `generated`。缺 `AI_API_KEY` / 未啟用時回 `{ ok: false, error }`,唔會 throw。 - **`markdown.ts`**:`renderMarkdown`(`marked`,內容受信任所以**唔 sanitize**)、`slugify`(保留中文)、`autoExcerpt`。 diff --git a/src/lib/ai.ts b/src/lib/ai.ts index 6d0b657..15fc84f 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -20,8 +20,8 @@ export async function getAiSettings(db: Db): Promise { const s = await getSettings(db); return { enabled: s.ai_enabled !== "0", - baseUrl: s.ai_base_url || "https://api.deepinfra.com/v1/openai", - model: s.ai_chat_model || "deepseek-ai/DeepSeek-V3-0324", + baseUrl: s.ai_base_url || "https://api.deepseek.com", + model: s.ai_chat_model || "deepseek-flash", contextPrompt: s.ai_context_prompt ?? "", businessContext: s.ai_business_context ?? "", webSearchEnabled: s.ai_web_search_enabled === "1", @@ -61,7 +61,7 @@ function buildSystemPrompt(opts: { if (opts.contextPrompt) parts.push(opts.contextPrompt); if (opts.businessContext) parts.push(`## 公司背景資料\n${opts.businessContext}`); if (opts.research) parts.push(`## 網上研究資料(只作參考,唔好照抄)\n${opts.research}`); - parts.push(`你係香港村屋太陽能公司「盈豐太陽能」嘅 SEO 內容寫手。請就以下主題寫一篇 800–1200 字嘅繁體中文(香港廣東話書面語)博客文章。 + parts.push(`你係一位專業嘅 SEO 內容寫手。請根據上面嘅公司背景同寫作風格,就以下主題寫一篇 800–1200 字嘅繁體中文(香港廣東話書面語)博客文章。 主題:${opts.topic} @@ -69,7 +69,7 @@ function buildSystemPrompt(opts: { - 標題要放焦點關鍵字,首 100 字內再出現一次,關鍵字密度約 1–2%。 - 用 ## / ### 分段,段落清晰易讀。 - 開頭先畀一段總結(summary-first),再展開。 -- 內容要對香港村屋太陽能實用、準確,唔好作出未經證實嘅承諾或數字。 +- 內容要實用、準確,符合公司業務;唔好作出未經證實嘅承諾或數字。 - 適合 SEO 同 AI 搜尋(GEO):用問答式小標題、必要時用列表。 輸出格式(必須嚴格跟隨,唔要加額外說明): @@ -87,8 +87,9 @@ async function callChat(settings: AiSettings, apiKey: string, systemPrompt: stri body: JSON.stringify({ model: settings.model, messages: [{ role: "user", content: systemPrompt }], - max_tokens: 2200, + max_tokens: 4000, temperature: 0.7, + ...(settings.baseUrl.includes("deepseek.com") ? { thinking: { type: "disabled" } } : {}), }), signal: AbortSignal.timeout(90_000), }); diff --git a/src/pages/admin/ai.astro b/src/pages/admin/ai.astro index 9690308..4c11750 100644 --- a/src/pages/admin/ai.astro +++ b/src/pages/admin/ai.astro @@ -127,11 +127,11 @@ const secrets = { ai: Boolean(env.AI_API_KEY), tavily: Boolean(env.TAVILY_API_KE
- +
- +

⚠️ 只填信任嘅供應商;AI_API_KEY 會傳送去呢個網址。

@@ -153,11 +153,21 @@ const secrets = { ai: Boolean(env.AI_API_KEY), tavily: Boolean(env.TAVILY_API_KE
- + +

可寫入角色、行業、語氣同目標受眾;留空會用通用寫手設定。

- + +

逐項填寫公司事實,AI 會自然融入文章而唔會照抄原文。

diff --git a/src/schemas/ai.ts b/src/schemas/ai.ts index fcb215f..a7f948b 100644 --- a/src/schemas/ai.ts +++ b/src/schemas/ai.ts @@ -6,10 +6,10 @@ export const aiSettingsInput = z.object({ (v) => (v === "" ? undefined : v), z .url("Base URL 要係有效網址。") - .default("https://api.deepinfra.com/v1/openai") + .default("https://api.deepseek.com") .refine((u) => /^https?:\/\//i.test(u), "Base URL 只支援 http/https。"), ), - ai_chat_model: z.string().trim().min(1, "請填模型名稱。").default("deepseek-ai/DeepSeek-V3-0324"), + ai_chat_model: z.string().trim().min(1, "請填模型名稱。").default("deepseek-flash"), ai_context_prompt: z.string().default(""), ai_business_context: z.string().default(""), ai_web_search_enabled: z.enum(["1", "0"]).default("0"),