Files
arcrun-collector/cmd/arcrun-app/connect.go
T
Leo bd0cead00e t193:daemon UI 換 Wails——CIS 是硬要求,fyne 做不到
leo 2026-08-04 看過 v0.16.0 畫面後:
「功能都有了,但美感非常糟糕⋯⋯跟 CIS 完全無關,每個功能都開一個小小的 popup 視窗,
 非常缺乏整體感,這要理解的是原始的技術選擇是否出錯?」
「我的要求是符合 CIS,在風格上跟 portal 一樣」
「CIS 已經提供規範,你做的連 Logo 都沒放上去,這跟美不美有關係嗎?
 要求放進 CIS 是硬要求,你做為檢查有嗎?沒有怎麼交貨?」

## 我的兩個錯
1. **動工前沒提醒做不到**——decisions-summary.md:314 的 D-daemon-UI
   是我 07-27 自己寫的調研(結論:fyne 全自繪、CSS 套不進去),寫完就沒再看。
   今天動工前該查它卻直接開寫,浪費 leo 的時間與 token。
2. **沒做 CIS 檢查就交貨**——連 logo 都沒放。

## 換 Wails(WebView 殼)
前端就是 HTML/CSS ⇒ 可以直接用 portal 那份色票與 lockup。
- style.css 的色票/字體/紙張紋理**逐字取自** portal 的 :root
  (matrix/arcrun:console-ui/public/portal/index.html),不自創任何顏色
- CIS lockup 官方 PNG,淺色 ink 版/深色 paper 版,切換規則同 portal
  (避開 08-01 作廢的自產 SVG——字腔缺失)
- 對話框改**內嵌覆蓋層**,不再每個功能開一個小 popup
- 原生資料夾選擇器(macOS powerbox 會自動授予該資料夾存取權,
  fyne 自繪 picker 拿不到;未來上 Mac App Store 是硬需求)
- 同步中的圓點會呼吸 ⇒ 看得出來在動(issue #17)
- 無帳號時是 onboarding 引導,不是空白面板

## 連線邏輯逐字沿用,不重寫
connect.go 的 normalizePortalURL/fetchConfigByLogin 取自 arcrun-tray/main.go,
含 t86 個資外洩事故的防線(不同實例=新增帳號,絕不覆蓋舊帳號的資料夾)。
今天已犯過一次「重寫別人修好的東西還修得更差」,不再犯第二次。

## 新增 check-cis.sh(交貨前必跑)
機械檢查六類:官方色票逐個到齊/**不准出現非 CIS 色**/logo 真的放進去/
不可用作廢 SVG/深色模式/字體與紙張紋理同 portal/app icon。
實跑 14 項全過。以後「沒跑過就不准交」。

⚠️ 未驗:實際畫面要 leo 開來看(我開不了視窗)。
2026-08-04 22:19:41 +08:00

129 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// connect.go — 連線精靈的後端(t193)
//
// ⚠️ 本檔的 normalizePortalURL / fetchConfigByLogin / instanceChanged
// **逐字取自 arcrun-tray/main.go**,不是重寫。理由:
// - 那些是踩過坑才長出來的(例:instanceChanged 是 t86 個資外洩事故的防線——
// leo 2026-07-28 實測發現 youlin 時代的資料夾被同步進 geek6688 新實例)
// - 今天已經犯過一次「重寫別人修好的東西還修得更差」,不再犯第二次
// 兩邊哪天要改,**兩邊一起改**(與 trayMinCloud* 常數同一套規矩)。
import (
"bytes"
"encoding/json"
"errors"
"net/http"
neturl "net/url"
"strings"
"time"
)
type daemonConfig struct {
CypherURL string `json:"cypher_url"`
Namespace string `json:"namespace"`
Library string `json:"library"`
Email string `json:"email"`
InstanceName string `json:"instance_name"`
}
type daemonConfigResp struct {
Success bool `json:"success"`
Config daemonConfig `json:"config"`
Error string `json:"error"`
}
// normalizePortalURL 把用戶貼的東西變成可打的 origin。
// portalGUI)與 cypherAPI)是不同子域:用戶手上的是 portal,這裡換算成 API 位址。
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("網址看起來不太對,請從信裡或瀏覽器網址列複製整段")
}
host := u.Host
if strings.HasPrefix(host, "arcrun-rag-ui.") {
host = "arcrun-cypher-executor." + strings.TrimPrefix(host, "arcrun-rag-ui.")
}
return "https://" + host, nil
}
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")
res, err := (&http.Client{Timeout: 20 * time.Second}).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
}
// Connect 是前端「連線」按鈕的入口:帳密換設定 → append 成新帳號(或更新既有同實例帳號)。
//
// 🔴 密碼零落地(刻意的安全設計):只在這一刻用來換設定,**不寫進 config.json**。
func (a *App) Connect(portalURL, email, password string) error {
r, err := fetchConfigByLogin(portalURL, email, password)
if err != nil {
return err
}
cfg, _ := loadCfg()
newHost := hostOf(r.Config.CypherURL)
for i := range cfg.Accounts {
if hostOf(cfg.Accounts[i].CypherURL) == newHost {
// 同一個實例=更新連線資訊,**保留既有 watch_folders**(改密碼不該清掉資料夾)
cfg.Accounts[i].CypherURL = r.Config.CypherURL
cfg.Accounts[i].Namespace = r.Config.Namespace
cfg.Accounts[i].APIKey = r.Config.Namespace
cfg.Accounts[i].Email = r.Config.Email
cfg.Accounts[i].InstanceName = r.Config.InstanceName
return saveCfg(cfg)
}
}
// 不同實例=新增一個帳號。**不覆蓋舊帳號**——t86 事故的教訓:
// 舊實例的資料夾若被帶進新實例,等於把別人的檔案同步到另一個知識庫(個資外洩)。
cfg.Accounts = append(cfg.Accounts, accountCfg{
InstanceName: r.Config.InstanceName,
Email: r.Config.Email,
CypherURL: r.Config.CypherURL,
Namespace: r.Config.Namespace,
APIKey: r.Config.Namespace,
})
return saveCfg(cfg)
}
func hostOf(s string) string {
u, err := neturl.Parse(strings.TrimSpace(s))
if err != nil || u.Host == "" {
return strings.TrimSpace(s)
}
return u.Host
}