diff --git a/cmd/arcrun-tray/main.go b/cmd/arcrun-tray/main.go index 5703d17..9707aaa 100644 --- a/cmd/arcrun-tray/main.go +++ b/cmd/arcrun-tray/main.go @@ -16,13 +16,16 @@ package main import ( + "bytes" "encoding/json" "errors" "fmt" + "net/http" neturl "net/url" "os" "path/filepath" "strings" + "time" "arcrun-rag/collector/supervisor" @@ -47,6 +50,7 @@ type directConfig struct { Email string `json:"email,omitempty"` // 實例主身分(t26;人人記得自己的 email,CF 全程隱形) InstanceName string `json:"instance_name,omitempty"` // 暱稱(t26 選配;不取就顯示 email) Library string `json:"library,omitempty"` + Extractor string `json:"extractor,omitempty"` // t54:連線精靈帶入(claude/gemma) IngestWF string `json:"ingest_workflow,omitempty"` RemovedWF string `json:"removed_workflow,omitempty"` PollSec int `json:"poll_interval_sec,omitempty"` @@ -191,6 +195,105 @@ func shortCypherHost(cypherURL string) string { return u } +// ── t54(leo 2026-07-25:「最好的就是把它的帳密直接輸入」)───────────────────── +// 首次啟動不再需要用戶去網站下載 config.json 丟進隱藏資料夾: +// 輸入「知識庫網址 + 帳號密碼」→ 打 /portal/daemon/config → 設定自動寫好。 +// 用戶只需要記得他剛在網站設的那組帳密(他本來就記得)。 + +// daemonConfigResp 是 /portal/daemon/config 的回應(只含連線設定,不含知識內容)。 +type daemonConfigResp struct { + Success bool `json:"success"` + Config struct { + CypherURL string `json:"cypher_url"` + Namespace string `json:"namespace"` + Library string `json:"library"` + Extractor string `json:"extractor"` + Email string `json:"email"` + InstanceName string `json:"instance_name"` + } `json:"config"` + Error string `json:"error"` +} + +// normalizePortalURL 把用戶貼的東西變成可打的 origin: +// 允許貼完整 portal 網址(.../portal/)、只貼主機名、或大小寫/尾斜線不一致。 +func normalizePortalURL(raw string) (string, error) { + s := strings.TrimSpace(raw) + if s == "" { + return "", errors.New("請貼上你的知識庫網址") + } + if !strings.Contains(s, "://") { + s = "https://" + s + } + u, err := neturl.Parse(s) + if err != nil || u.Host == "" { + return "", errors.New("網址看起來不太對,請從信裡或瀏覽器網址列複製整段") + } + // portal(GUI)與 cypher(API)是不同子域:用戶手上的是 portal,這裡換算成 API 位址。 + host := u.Host + if strings.HasPrefix(host, "arcrun-rag-ui.") { + host = "arcrun-cypher-executor." + strings.TrimPrefix(host, "arcrun-rag-ui.") + } + return "https://" + host, nil +} + +// fetchConfigByLogin 用帳密向實例換取這台機器該用的設定。 +func fetchConfigByLogin(portalURL, email, password string) (*daemonConfigResp, error) { + base, err := normalizePortalURL(portalURL) + if err != nil { + return nil, err + } + body, _ := json.Marshal(map[string]string{"email": strings.TrimSpace(email), "password": password}) + req, err := http.NewRequest(http.MethodPost, base+"/portal/daemon/config", bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{Timeout: 20 * time.Second} + res, err := client.Do(req) + if err != nil { + return nil, errors.New("連不上這個網址——請確認網址正確、網路正常") + } + defer res.Body.Close() + var out daemonConfigResp + if err := json.NewDecoder(res.Body).Decode(&out); err != nil { + return nil, errors.New("這個網址不像是 Arcrun RAG 知識庫,請再確認一次") + } + if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden { + return nil, errors.New("帳號或密碼不對——用你在知識庫網站設定的那組") + } + if !out.Success { + if out.Error != "" { + return nil, errors.New(out.Error) + } + return nil, errors.New("連線失敗,請稍後再試一次") + } + return &out, nil +} + +// applyRemoteConfig 把換到的設定寫進本地 config(保留既有看守資料夾)。 +func applyRemoteConfig(cfg *directConfig, r *daemonConfigResp) { + cfg.CypherURL = r.Config.CypherURL + cfg.Namespace = r.Config.Namespace + if r.Config.Library != "" { + cfg.Library = r.Config.Library + } + if r.Config.Extractor != "" { + cfg.Extractor = r.Config.Extractor + } + cfg.Email = r.Config.Email + if r.Config.InstanceName != "" { + cfg.InstanceName = r.Config.InstanceName + } + if cfg.Manifest == "" { + cfg.Manifest = filepath.Join(appDir(), "manifest.json") + } +} + +// isConnected 判斷「這台機器已經連上某個知識庫」——沒有就該跳連線精靈。 +func isConnected(cfg *directConfig) bool { + return strings.TrimSpace(cfg.CypherURL) != "" && strings.TrimSpace(cfg.Namespace) != "" +} + func main() { a := app.NewWithID("dev.arcrun.rag.tray") cfg := loadConfig() @@ -213,11 +316,65 @@ func main() { // 重設選單=刷新(比 (*Menu).Refresh() 跨版本更穩)。 var rebuildTray func() + restartWatch := func() { sup.Stop() sup.Start() } + // t54 連線精靈:輸入知識庫網址+帳密 → 換設定 → 寫檔 → 開始看守。 + // 沒連線過的機器一開 app 就自動跳;之後也能從選單「重新連線…」再開。 + var showConnectWizard func() + showConnectWizard = func() { + urlEntry := widget.NewEntry() + urlEntry.SetPlaceHolder("https://…workers.dev/portal/") + if cfg.CypherURL != "" { + urlEntry.SetText(cfg.CypherURL) + } + emailEntry := widget.NewEntry() + emailEntry.SetPlaceHolder("你的 Email") + if cfg.Email != "" { + emailEntry.SetText(cfg.Email) + } + pwEntry := widget.NewPasswordEntry() + pwEntry.SetPlaceHolder("你在知識庫設定的密碼") + hint := widget.NewLabel("") + hint.Wrapping = fyne.TextWrapWord + + form := container.NewVBox( + widget.NewLabel("連上你的知識庫"), + widget.NewLabel("貼上網址,再輸入你在網站上設定的帳號密碼:"), + urlEntry, emailEntry, pwEntry, hint, + ) + d := dialog.NewCustomConfirm("Arcrun RAG", "連線", "取消", form, func(ok bool) { + if !ok { + return + } + hint.SetText("連線中…") + go func() { + r, err := fetchConfigByLogin(urlEntry.Text, emailEntry.Text, pwEntry.Text) + if err != nil { + // fyne 的 UI 更新須回主執行緒;用 dialog 顯示即可(它內部處理) + dialog.ShowError(err, win) + showConnectWizard() // 讓用戶改完再試,不要把他丟回空白畫面 + return + } + applyRemoteConfig(cfg, r) + if err := saveConfig(cfg); err != nil { + dialog.ShowError(err, win) + return + } + restartWatch() + rebuildTray() + dialog.ShowInformation("連上了", "已連上「"+connectionStatusLabel(cfg.InstanceName, cfg.Email)+"」。\n接下來用選單「+ 新增知識資料夾…」挑要同步的資料夾就好。", win) + }() + }, win) + d.Resize(fyne.NewSize(460, 320)) + win.Show() + win.RequestFocus() + d.Show() + } + addAction := func() { win.Show() win.RequestFocus() @@ -332,6 +489,7 @@ func main() { items = append(items, fyne.NewMenuItem("+ 新增知識資料夾…", addAction), fyne.NewMenuItem("+ 建立新資料夾並看守…", newFolderAction), // t17 + fyne.NewMenuItem("🔗 連上知識庫…(換帳號/重新連線)", func() { showConnectWizard() }), // t54 fyne.NewMenuItemSeparator(), pauseItem, ) @@ -363,13 +521,13 @@ func main() { widget.NewButton("建立新資料夾並看守…", func() { newFolderAction() }), // t17 )) - // 開機即看守(若設定完整) - if cfg.CypherURL != "" && cfg.Namespace != "" { + // 開機即看守(若已連上知識庫) + if isConnected(cfg) { sup.Start() } else { - // 缺安裝器帶入的 cypher_url/namespace:提示用戶先完成安裝 - statusItem.Label = "狀態:尚未連結(請先完成安裝)" - win.Show() + // t54:沒連過就直接跳連線精靈(輸入網址+帳密),不再要用戶去下載 config.json。 + statusItem.Label = "狀態:尚未連線" + showConnectWizard() } a.Lifecycle().SetOnStopped(func() { sup.Stop() })