feat(daemon/t54): 連線精靈——首次啟動輸入知識庫網址+帳密,自動換取設定並開始看守(不再需要下載 config.json 丟隱藏資料夾);portal→cypher 子域自動換算、三種輸入格式實測可用;選單加「連上知識庫…」供換帳號
This commit is contained in:
+163
-5
@@ -16,13 +16,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
neturl "net/url"
|
neturl "net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"arcrun-rag/collector/supervisor"
|
"arcrun-rag/collector/supervisor"
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ type directConfig struct {
|
|||||||
Email string `json:"email,omitempty"` // 實例主身分(t26;人人記得自己的 email,CF 全程隱形)
|
Email string `json:"email,omitempty"` // 實例主身分(t26;人人記得自己的 email,CF 全程隱形)
|
||||||
InstanceName string `json:"instance_name,omitempty"` // 暱稱(t26 選配;不取就顯示 email)
|
InstanceName string `json:"instance_name,omitempty"` // 暱稱(t26 選配;不取就顯示 email)
|
||||||
Library string `json:"library,omitempty"`
|
Library string `json:"library,omitempty"`
|
||||||
|
Extractor string `json:"extractor,omitempty"` // t54:連線精靈帶入(claude/gemma)
|
||||||
IngestWF string `json:"ingest_workflow,omitempty"`
|
IngestWF string `json:"ingest_workflow,omitempty"`
|
||||||
RemovedWF string `json:"removed_workflow,omitempty"`
|
RemovedWF string `json:"removed_workflow,omitempty"`
|
||||||
PollSec int `json:"poll_interval_sec,omitempty"`
|
PollSec int `json:"poll_interval_sec,omitempty"`
|
||||||
@@ -191,6 +195,105 @@ func shortCypherHost(cypherURL string) string {
|
|||||||
return u
|
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() {
|
func main() {
|
||||||
a := app.NewWithID("dev.arcrun.rag.tray")
|
a := app.NewWithID("dev.arcrun.rag.tray")
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
@@ -213,11 +316,65 @@ func main() {
|
|||||||
// 重設選單=刷新(比 (*Menu).Refresh() 跨版本更穩)。
|
// 重設選單=刷新(比 (*Menu).Refresh() 跨版本更穩)。
|
||||||
var rebuildTray func()
|
var rebuildTray func()
|
||||||
|
|
||||||
|
|
||||||
restartWatch := func() {
|
restartWatch := func() {
|
||||||
sup.Stop()
|
sup.Stop()
|
||||||
sup.Start()
|
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() {
|
addAction := func() {
|
||||||
win.Show()
|
win.Show()
|
||||||
win.RequestFocus()
|
win.RequestFocus()
|
||||||
@@ -332,6 +489,7 @@ func main() {
|
|||||||
items = append(items,
|
items = append(items,
|
||||||
fyne.NewMenuItem("+ 新增知識資料夾…", addAction),
|
fyne.NewMenuItem("+ 新增知識資料夾…", addAction),
|
||||||
fyne.NewMenuItem("+ 建立新資料夾並看守…", newFolderAction), // t17
|
fyne.NewMenuItem("+ 建立新資料夾並看守…", newFolderAction), // t17
|
||||||
|
fyne.NewMenuItem("🔗 連上知識庫…(換帳號/重新連線)", func() { showConnectWizard() }), // t54
|
||||||
fyne.NewMenuItemSeparator(),
|
fyne.NewMenuItemSeparator(),
|
||||||
pauseItem,
|
pauseItem,
|
||||||
)
|
)
|
||||||
@@ -363,13 +521,13 @@ func main() {
|
|||||||
widget.NewButton("建立新資料夾並看守…", func() { newFolderAction() }), // t17
|
widget.NewButton("建立新資料夾並看守…", func() { newFolderAction() }), // t17
|
||||||
))
|
))
|
||||||
|
|
||||||
// 開機即看守(若設定完整)
|
// 開機即看守(若已連上知識庫)
|
||||||
if cfg.CypherURL != "" && cfg.Namespace != "" {
|
if isConnected(cfg) {
|
||||||
sup.Start()
|
sup.Start()
|
||||||
} else {
|
} else {
|
||||||
// 缺安裝器帶入的 cypher_url/namespace:提示用戶先完成安裝
|
// t54:沒連過就直接跳連線精靈(輸入網址+帳密),不再要用戶去下載 config.json。
|
||||||
statusItem.Label = "狀態:尚未連結(請先完成安裝)"
|
statusItem.Label = "狀態:尚未連線"
|
||||||
win.Show()
|
showConnectWizard()
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Lifecycle().SetOnStopped(func() { sup.Stop() })
|
a.Lifecycle().SetOnStopped(func() { sup.Stop() })
|
||||||
|
|||||||
Reference in New Issue
Block a user