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
+8 -4
View File
@@ -459,6 +459,10 @@ func TestRunDirectOnceMultiAccount(t *testing.T) {
{CypherURL: serverB.URL, Namespace: "nsB", WatchFolders: []string{rootB}},
},
MaxRemoved: DefaultMaxRemovedRatio,
// t181:本測驗的是「雙帳號各自送到自己的實例」,不是萃取。
// 預設已改成 workers-ai(會打雲端萃取端點),為隔離變因這裡明確走舊制直送
// Extractor 空+ExtractorExplicit=true ⇒ 不被預設覆蓋)。
ExtractorExplicit: true,
}
results, exit, _ := RunDirectOnce(cfg, false)
if exit != 0 {
@@ -551,7 +555,7 @@ func TestAccountDataStructureFunctions(t *testing.T) {
parent := &DirectConfig{
Manifest: "/tmp/m.json",
PollSec: 10,
Extractor: "claude",
Extractor: "claude", ExtractorExplicit: true,
Accounts: []AccountConfig{
{CypherURL: "https://a.example", Namespace: "nsA", WatchFolders: []string{"/folder1"}},
},
@@ -628,7 +632,7 @@ func TestRunDirectOnceAccountResultsTagged(t *testing.T) {
func TestMakeAccountSubConfigExtractorOverride(t *testing.T) {
parent := &DirectConfig{
Manifest: "/tmp/m.json",
Extractor: "claude",
Extractor: "claude", ExtractorExplicit: true,
GeminiAPIKey: "machine-key",
LLMModel: "model-machine",
PollSec: 5,
@@ -638,7 +642,7 @@ func TestMakeAccountSubConfigExtractorOverride(t *testing.T) {
CypherURL: "https://a.example",
Namespace: "ns",
WatchFolders: []string{"/tmp"},
Extractor: "gemma", // 帳號層覆蓋
Extractor: "gemma", // 帳號層覆蓋AccountConfig 無 ExtractorExplicit,那是機器層旗標)
GeminiAPIKey: "account-key", // 帳號層覆蓋
LLMModel: "model-acc", // 帳號層覆蓋
}
@@ -658,7 +662,7 @@ func TestMakeAccountSubConfigExtractorOverride(t *testing.T) {
func TestMakeAccountSubConfigExtractorFallback(t *testing.T) {
parent := &DirectConfig{
Manifest: "/tmp/m.json",
Extractor: "gemma",
Extractor: "gemma", ExtractorExplicit: true,
GeminiAPIKey: "machine-key",
LLMModel: "model-machine",
PollSec: 5,