feat(daemon/t54): 連線精靈——首次啟動輸入知識庫網址+帳密,自動換取設定並開始看守(不再需要下載 config.json 丟隱藏資料夾);portal→cypher 子域自動換算、三種輸入格式實測可用;選單加「連上知識庫…」供換帳號

This commit is contained in:
2026-07-26 00:16:58 +08:00
parent e548c57efa
commit 3174340c4c
+163 -5
View File
@@ -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
}
// ── t54leo 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("網址看起來不太對,請從信裡或瀏覽器網址列複製整段")
}
// portalGUI)與 cypherAPI)是不同子域:用戶手上的是 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() })