t181 daemon 端:萃取預設走 Workers AI(免金鑰)+托盤可切換 AI 來源

【leo 08-04 列為最優先】「daemon 的 AI 改用 workers AI」
——「這是我的用戶**最大障礙**,造成首輪測試用戶的**好評或惡評**」。

【新增】collector/extract_workersai.go:打自己雲端實例的 /portal/daemon/extract,
那端用 env.AI binding ⇒ **完全不需要任何金鑰**。與 gemma 路架構完全對稱
(leo 的判斷:「不論用 Claude/Gemini/Workers AI…應該是同一件事」——是,
只有「送到哪」不同,讀檔/轉檔/淨化/落卡全共用)。
舊實例回 404 時講人話:「你的知識庫還是舊版 ⇒ 請到 portal 按『立即更新』」。

【預設改為一律 Workers AI】leo 特別交代:
  「default 用 Workers AI,你要用 Gemini 要**特別去選取**,**不管你現在是否有填金鑰**」
  「只要更新版本,就已經 default workers AI 了,除非去一個地方切換」
  「不然我會有很多質疑,**花在解釋為什麼 Gemini 不管用上**」
⇒ 判準是新欄位 ExtractorExplicit(使用者主動選過),**不是「有沒有金鑰」**。
  舊 config 沒這欄=false ⇒ 更新版本後自動走 Workers AI;金鑰留著不動,改選 Gemini 立刻可用。

【托盤=唯一切換處】「AI 設定…」改成引擎選擇:雲端 AI(預設)/Gemini(選配)。
選 Gemini 才顯示金鑰欄,並附「Billing Tier: Unavailable ⇒ 換 Google 帳號」的排難提示
(今天 oscar 撞的那題)。Gemini 不推廣(leo:「特定人告訴他怎麼做就好」),文案只說明不慫恿。

【標籤】leo:「用 workers AI 就**不顯示**,用 Gemini 會顯示 Gemini」
⇒ 預設路徑不佔版面;只有主動選了別的才標出來(延續 t178「標籤必須與實際行為一致」)。

【測試】新增 TestT181DefaultsToWorkersAI 六則,含最關鍵的
**「有金鑰但沒主動選 ⇒ 仍走 workers-ai」**;既有測試補 ExtractorExplicit 以隔離變因
(它們測的是萃取管線,不是預設邏輯)。兩模組全綠。

⚠️ 未送達:要打包 v0.15.5 並部署雲端端點才會到用戶手上。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 15:00:48 +08:00
parent 1a3e974941
commit 1a8906c0b7
7 changed files with 302 additions and 31 deletions
+67 -15
View File
@@ -88,7 +88,11 @@ type directConfig struct {
Email string `json:"email,omitempty"`
InstanceName string `json:"instance_name,omitempty"`
Library string `json:"library,omitempty"`
Extractor string `json:"extractor,omitempty"` // 機器層級:claude/gemma
Extractor string `json:"extractor,omitempty"` // "workers-ai"(預設,免金鑰)|"gemma"
// t181:使用者主動在「AI 設定…」選過才 true。false/舊 config 沒這欄 ⇒ 一律走 workers-ai。
// leo 08-04:「只要更新版本,就已經 default workers AI 了,
// 除非去一個地方切換你指定的 AI 來源」。
ExtractorExplicit bool `json:"extractor_explicit,omitempty"`
ClaudeBin string `json:"claude_bin,omitempty"` // t92collector fallback 找到後回寫
GeminiAPIKey string `json:"gemini_api_key,omitempty"` // t108gemma 路 key
LLMModel string `json:"llm_model,omitempty"` // gemma 路模型
@@ -726,11 +730,22 @@ func accountEngineLabel(acc accountCfg, defaultExtractor string) string {
extractor = strings.TrimSpace(defaultExtractor)
}
switch extractor {
// t181:走雲端 Workers AI**免金鑰,新的預設**)。
// 空值也走這條——`direct.go` 的預設邏輯:沒設 extractor=新用戶=走 workers-ai
// 標籤跟著一致,才不會重演 t178「畫面說 A、實際跑 B」的矛盾。
// t181leo 08-04):「上方如果用 workers AI 就**不顯示**,如果用 Gemini 會顯示 Gemini」
// ⇒ 預設路徑不佔版面(那是常態,不需要標註);只有主動選了別的引擎才標出來。
// 空值也走這條——沒設 extractor=新用戶=走 workers-aidirect.go 預設邏輯)。
case "workers-ai", "":
return ""
// t178leo 08-04 實撞:朋友的托盤顯示「oscar · Claude」,但他根本沒有 Claude):
// `claude` 也顯示 Gemini——因為 **direct.go 早就把 claude 正規化成 gemma**
// t176:地端先只支援 Gemini)。標籤若照著 config 的舊字串念,就與實際行為不符
// ⇒ 用戶看到 Claude、卻收到「Gemini 金鑰是空的」,兩個訊息互相矛盾、更難懂。
// 舊值來源=雲端舊版下發後留在 config.json 的殘留(t176 擋了新寫入,沒洗掉舊的)。
//
// 📌 Gemini 仍是**選配**leo 08-04:「客戶說他要用 Gemini,但現在變成選配,
// 不需要推廣,特定人告訴他怎麼做就好」)⇒ 已填金鑰的用戶維持這條路、標籤照實顯示。
case "gemma", "claude":
return " · Gemini"
}
@@ -874,43 +889,80 @@ func main() {
// 之後要擴充成「任何用戶想用的模型」時,這裡加 provider 選單即可(欄位已預留 LLMModel)。
// ⚠️ 這是「緊急先加上輸入位置」的版本——leo 已指出托盤下拉選單塞不下未來的設定量
//(30+ 資料夾根本認不出),後續走本地 HTML 設定頁(像 Google Drive)。
// t181leo 08-04):這裡是「**唯一可以切換 AI 來源的地方**」。
// leo:「只要更新版本,就已經 default workers AI 了,除非**去一個地方切換**你指定的 AI 來源」
// 「default 用 Workers AI,你要用 Gemini 要**特別去選取**,不管你現在是否有填金鑰」
// ⇒ 預設選項固定在「雲端 AI」,除非使用者**主動**改選 Gemini(才寫 ExtractorExplicit=true)。
// Gemini 是選配、不推廣(leo:「特定人告訴他怎麼做就好」),故文案不慫恿、只說明。
showAISettings = func() {
const (
optCloud = "雲端 AI(推薦・不必申請任何金鑰)"
optGemini = "Google Gemini(需要自己申請金鑰)"
)
keyEntry := widget.NewPasswordEntry()
keyEntry.SetPlaceHolder("貼上你的 Gemini API Key")
keyEntry.SetText(cfg.GeminiAPIKey) // 帶入現值,讓他看得到「已經設過了」
hint := widget.NewLabel("")
hint.Wrapping = fyne.TextWrapWord
note := widget.NewLabel("")
note.Wrapping = fyne.TextWrapWord
// 只有「主動選過 Gemini」才預選 Gemini;其餘一律停在雲端 AI(含已填金鑰但沒選過的人)
selected := optCloud
if cfg.ExtractorExplicit && cfg.Extractor == "gemma" {
selected = optGemini
}
radio := widget.NewRadioGroup([]string{optCloud, optGemini}, func(s string) {
if s == optGemini {
keyEntry.Show()
note.SetText("到 aistudio.google.com/apikey 免費申請。\n" +
"若顯示 Billing Tier: Unavailable,代表那個 Google 帳號被 Google 限制了,換一個帳號申請。")
} else {
keyEntry.Hide()
note.SetText("用你自己 Cloudflare 帳號內建的 AI,不需要任何金鑰、不必去別的網站申請。")
}
})
radio.SetSelected(selected)
form := container.NewVBox(
widget.NewLabel("萃取用的 AI 設定"),
widget.NewLabel("目前使用 Google Gemini。到 aistudio.google.com 免費申請金鑰,貼在下面:"),
keyEntry, hint,
widget.NewLabel("用哪個 AI 幫你整理文件?"),
radio, keyEntry, note,
)
d := dialog.NewCustomConfirm("AI 設定", "儲存", "取消", form, func(ok bool) {
if !ok {
return
}
useGemini := radio.Selected == optGemini
key := strings.TrimSpace(keyEntry.Text)
if key == "" {
dialog.ShowError(errors.New("金鑰是空的——請貼上 Gemini API Key"), win)
if useGemini && key == "" {
dialog.ShowError(errors.New("選了 Gemini 就要貼上金鑰;不想申請的話請改選「雲端 AI」"), win)
return
}
engine := "workers-ai"
if useGemini {
engine = "gemma"
cfg.GeminiAPIKey = key
}
// 機器層寫一份當預設;各帳號層一併寫,避免舊 config 殘留的空欄位繼承到別的值。
cfg.GeminiAPIKey = key
cfg.Extractor = "gemma"
cfg.Extractor = engine
cfg.ExtractorExplicit = true // ← 使用者主動選過,之後不再被預設覆蓋
for i := range cfg.Accounts {
cfg.Accounts[i].GeminiAPIKey = key
cfg.Accounts[i].Extractor = "gemma"
cfg.Accounts[i].Extractor = engine
if useGemini {
cfg.Accounts[i].GeminiAPIKey = key
}
}
if err := saveConfig(cfg); err != nil {
dialog.ShowError(err, win)
return
}
restartWatch() // 讓新金鑰立刻生效,不必等下一輪
restartWatch() // 立刻生效,不必等下一輪
rebuildTray()
dialog.ShowInformation("已儲存", "AI 設定已更新,接下來的同步會用這把金鑰萃取。", win)
msg := "接下來的同步會用你自己 Cloudflare 帳號內建的 AI 整理文件(不需要金鑰)。"
if useGemini {
msg = "接下來的同步會用 Google Gemini 整理文件。"
}
dialog.ShowInformation("已儲存", msg, win)
}, win)
d.Resize(fyne.NewSize(460, 260))
d.Resize(fyne.NewSize(520, 320))
win.Show()
win.RequestFocus()
d.Show()
+4 -1
View File
@@ -502,7 +502,10 @@ func TestAccountEngineLabel(t *testing.T) {
{"帳號有claude→仍顯示 Gemini(實際就是走 gemma", accountCfg{Extractor: "claude"}, "", " · Gemini"},
{"帳號空退回default-gemma", accountCfg{}, "gemma", " · Gemini"},
{"帳號空退回default-claude→仍顯示 Gemini", accountCfg{}, "claude", " · Gemini"},
{"兩者皆空無標籤", accountCfg{}, "", ""},
// t181:兩者皆空=新用戶=走 workers-ai(免金鑰預設)⇒ **不顯示標籤**
// leo 08-04:「上方如果用 workers AI 就不顯示」——預設路徑是常態,不佔版面)
{"兩者皆空=預設雲端 AI=不顯示", accountCfg{}, "", ""},
{"t181 明確 workers-ai 也不顯示", accountCfg{Extractor: "workers-ai"}, "", ""},
{"帳號層優先於default", accountCfg{Extractor: "gemma"}, "claude", " · Gemini"},
}
for _, c := range cases {