bd0cead00e
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 開來看(我開不了視窗)。
129 lines
4.5 KiB
Go
129 lines
4.5 KiB
Go
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。
|
||
// portal(GUI)與 cypher(API)是不同子域:用戶手上的是 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
|
||
}
|