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 文件。
This commit is contained in:
+3
-1
@@ -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`。
|
||||
|
||||
+6
-5
@@ -20,8 +20,8 @@ export async function getAiSettings(db: Db): Promise<AiSettings> {
|
||||
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),
|
||||
});
|
||||
|
||||
@@ -127,11 +127,11 @@ const secrets = { ai: Boolean(env.AI_API_KEY), tavily: Boolean(env.TAVILY_API_KE
|
||||
</div>
|
||||
<div>
|
||||
<label for="ai_chat_model">模型</label>
|
||||
<input id="ai_chat_model" name="ai_chat_model" type="text" value={settings.ai_chat_model ?? "deepseek-ai/DeepSeek-V3-0324"} />
|
||||
<input id="ai_chat_model" name="ai_chat_model" type="text" value={settings.ai_chat_model ?? "deepseek-flash"} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="ai_base_url">OpenAI 相容 Base URL</label>
|
||||
<input id="ai_base_url" name="ai_base_url" type="text" value={settings.ai_base_url ?? "https://api.deepinfra.com/v1/openai"} />
|
||||
<input id="ai_base_url" name="ai_base_url" type="text" value={settings.ai_base_url ?? "https://api.deepseek.com"} />
|
||||
<p class="muted" style="margin:6px 0 0">⚠️ 只填信任嘅供應商;AI_API_KEY 會傳送去呢個網址。</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -153,11 +153,21 @@ const secrets = { ai: Boolean(env.AI_API_KEY), tavily: Boolean(env.TAVILY_API_KE
|
||||
</div>
|
||||
<div style="grid-column:1/-1">
|
||||
<label for="ai_context_prompt">寫作風格 / 語氣</label>
|
||||
<textarea id="ai_context_prompt" name="ai_context_prompt">{settings.ai_context_prompt ?? ""}</textarea>
|
||||
<textarea
|
||||
id="ai_context_prompt"
|
||||
name="ai_context_prompt"
|
||||
placeholder="例:用香港繁體中文書寫,語氣專業、親切、務實,避免誇大。目標讀者係⋯⋯"
|
||||
>{settings.ai_context_prompt ?? ""}</textarea>
|
||||
<p class="muted" style="margin:6px 0 0">可寫入角色、行業、語氣同目標受眾;留空會用通用寫手設定。</p>
|
||||
</div>
|
||||
<div style="grid-column:1/-1">
|
||||
<label for="ai_business_context">公司背景資料(grounding)</label>
|
||||
<textarea id="ai_business_context" name="ai_business_context">{settings.ai_business_context ?? ""}</textarea>
|
||||
<textarea
|
||||
id="ai_business_context"
|
||||
name="ai_business_context"
|
||||
placeholder={"公司名稱:\n行業:\n業務範圍:\n主要地區:\n主要產品服務:\n目標客群:\n特色定位:"}
|
||||
>{settings.ai_business_context ?? ""}</textarea>
|
||||
<p class="muted" style="margin:6px 0 0">逐項填寫公司事實,AI 會自然融入文章而唔會照抄原文。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions"><button type="submit">儲存 AI 設定</button></div>
|
||||
|
||||
+2
-2
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user