feat(t213): 匯出診斷檔搬進 arcrun-app 本機端(phase 2,接手 t210 落地後的 Progress/FailureBreakdown)
t210(commit 5effcb2)已把「總量進度」「失敗分類」算好並寫進 status.json(現況快照,
不隨閒置歸零)——本次直接讀那兩個欄位,不另算一套(首頁與診斷檔同一組數字)。
新增:
- collector/cmd/arcrun-app/diagnostics_export.go
- App.ExportDiagnostics():合併本機(daemon 版本/自我更新狀態/Progress/
FailureBreakdown/略過檔案樣本)與雲端(每帳號打新端點 GET /portal/daemon/diagnostics,
X-Arcrun-API-Key 認證)成一份 JSON,彈系統存檔對話框存下來
- mergeDiagnostics 抽成無 IO 純函式,方便測試
- collector/cmd/arcrun-app/diagnostics_export_test.go:5 個離線測試,涵蓋
同首頁數字/分類名稱原樣照抄不自己判斷/失敗檔名 basename-only/略過清單省略欄位/
帳號層 cloud 與 cloud_error 互斥
- frontend/src/main.js:「版本與更新」頁加「疑難排解」卡片+匯出按鈕
守住的規則:
- Progress/FailureBreakdown 原樣接住 status.json,不重掃 manifest
- 分類名稱字串只認 collector/progress.go 的 ClassifyFailure,本檔不判斷任何分類
- 失敗檔名 basename-only:借用既有 buildSkipped() 輸出(本來就是 basename+白話標籤)
驗證(實跑,非設計稿):go build/vet/gofmt 全乾淨;go test ./...(collector+
arcrun-app)全過;check-cis.sh/check-render.sh 視覺機械閘全過;wails build 成功
產生含 ExportDiagnostics 的 bindings;拿本機真實 config.json(2 個真帳號)+真網路
跑 buildDiagnosticsPayload():真拿到 update_check.latest=v0.18.23、真列出本機
略過檔案(basename)、兩個真帳號誠實回 cloud_error(線上新端點還沒部署,與總管
實測結果一致)。未在真實 GUI 點過按鈕(本機正跑 production Arcrun.app,避免干擾)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
// diagnostics_export.go — 檢修孔的 daemon 端(t213 phase 2,InkStoneCo 總管交辦,2026-08-08)。
|
||||
//
|
||||
// 為什麼要在這裡做,不在雲端 portal 網頁做(phase 1 調查結論):
|
||||
// 舊的「匯出診斷檔給我們看」按鈕住在雲端 RAG Portal 網頁(matrix/arcrun 的
|
||||
// GET /portal/data/diagnostics),在封測者的**瀏覽器**裡執行;而封測者電腦上跑的
|
||||
// daemon(本檔所在的 arcrun-app)是完全獨立的另一個行程,瀏覽器對本機檔案系統零
|
||||
// 存取權——那顆按鈕不管加多少雲端欄位都構不到本機資料。本檔把按鈕搬到 daemon
|
||||
// 行程本體,本機這半直接讀 status.json,雲端那半改打新增的 X-Arcrun-API-Key 版
|
||||
// 端點(GET /portal/daemon/diagnostics,免帳密,daemon 背景行程沒有 portal session)。
|
||||
//
|
||||
// 🔴 leo 08-08 三條追加規則:
|
||||
// ① 首頁與診斷檔必須是同一組數字——本檔**只讀** status.json 裡 t210 已經算好的
|
||||
// Progress/FailureBreakdown,不重新掃 manifest、不另算一套。
|
||||
// ② 分類名稱字串只准住在 collector/progress.go 的 ClassifyFailure——本檔原樣照抄
|
||||
// FailureBreakdown.Groups,不認得任何一個分類名(呼應 app.go buildProgress 的規矩)。
|
||||
// ③ 失敗檔名只出 basename,不出完整路徑(完整路徑會洩漏使用者的資料夾結構)——
|
||||
// 沿用首頁既有的 buildSkipped():它的 Files 欄位本來就是 filepath.Base()+白話標籤,
|
||||
// 不是原始路徑,這裡直接借用,不重寫第二套。
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
collector "arcrun-rag/collector"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// diagnosticsHTTP:雲端這半只是一次 GET(只讀查詢,不佔 KV/D1 寫入額度),
|
||||
// 15s 逾時給雲端冷啟動/LLM 相關端點的餘裕(比照 extract_workersai.go 的精神,
|
||||
// 但這支端點不碰 AI,通常遠快於此,逾時值只是保底)。
|
||||
var diagnosticsHTTP = &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
// localDiagnostics=地端這半:daemon 版本/自我更新狀態+t210 已算好的總量進度/
|
||||
// 失敗分類+讀不了的檔案樣本(basename)。
|
||||
type localDiagnostics struct {
|
||||
DaemonVersion string `json:"daemon_version"`
|
||||
UpdateCheck UpdateInfo `json:"update_check"` // 現查現答(Q4:Mac 卡在舊版)
|
||||
Progress collector.SyncProgress `json:"progress"` // 同首頁(Q2 的分母:Total)
|
||||
FailureBreakdown collector.FailureBreakdown `json:"failure_breakdown"` // 同首頁(Q3:分類統計)
|
||||
// SkippedSample/SkippedMore=格式讀不了、根本沒進 manifest 的檔案樣本,
|
||||
// 沿用 buildSkipped() 既有邏輯(basename+白話格式標籤,如「舊版報告.doc(舊版 Word)」);
|
||||
// 沒有東西被略過時兩者都是零值,JSON 省略。
|
||||
SkippedSample []string `json:"skipped_sample,omitempty"`
|
||||
SkippedMore int `json:"skipped_more,omitempty"`
|
||||
}
|
||||
|
||||
// accountDiagnostics=一個雲端帳號(知識庫實例)的雲端那半。
|
||||
// Cloud 拿不到時填 CloudError(誠實回報,不假裝有數字)——常見原因:舊版雲端沒有
|
||||
// /portal/daemon/diagnostics(部署還沒到)、網路不通、api key 尚未設定。
|
||||
type accountDiagnostics struct {
|
||||
InstanceName string `json:"instance_name"`
|
||||
Host string `json:"host"`
|
||||
Cloud map[string]any `json:"cloud,omitempty"`
|
||||
CloudError string `json:"cloud_error,omitempty"`
|
||||
}
|
||||
|
||||
// exportedDiagnostics=按鈕按下去存的那一份完整檔案。
|
||||
type exportedDiagnostics struct {
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
Local localDiagnostics `json:"local"`
|
||||
Accounts []accountDiagnostics `json:"accounts"`
|
||||
}
|
||||
|
||||
// fetchCloudDiagnosticsFn 是可在測試中替換的間接呼叫(比照 cloud_version.go 的
|
||||
// fetchCloudVersion 慣例),讓 mergeDiagnostics 以外的 IO 邊界也能被單測頂替。
|
||||
var fetchCloudDiagnosticsFn = fetchCloudDiagnostics
|
||||
|
||||
// fetchCloudDiagnostics 打雲端新端點 GET {cypherURL}/portal/daemon/diagnostics
|
||||
// (X-Arcrun-API-Key 認證,matrix/arcrun commit 93b1140)。回應原樣轉存(本身已守住
|
||||
// 兩條紅線:不含內部概念/不含卡片內容,見 buildDiagnostics 註解),本函式不重新挑欄位。
|
||||
func fetchCloudDiagnostics(cypherURL, apiKey string) (map[string]any, error) {
|
||||
base := strings.TrimSpace(cypherURL)
|
||||
if base == "" {
|
||||
return nil, fmt.Errorf("這個帳號還沒設定知識庫網址")
|
||||
}
|
||||
if strings.TrimSpace(apiKey) == "" {
|
||||
return nil, fmt.Errorf("這個帳號還沒有連線金鑰")
|
||||
}
|
||||
url := strings.TrimSuffix(base, "/") + "/portal/daemon/diagnostics"
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Arcrun-API-Key", apiKey)
|
||||
|
||||
resp, err := diagnosticsHTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("連不上你的知識庫:%w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
// 舊實例還沒有這條 route ⇒ 講人話,別讓診斷檔裡出現裸 404(比照 extract_workersai.go 慣例)
|
||||
return nil, fmt.Errorf("你的知識庫還是舊版(沒有雲端診斷功能)⇒ 請到 portal 按「立即更新」重裝一次")
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("雲端診斷查詢失敗(HTTP %d):%.300s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return nil, fmt.Errorf("雲端回應解析失敗:%w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// buildDiagnosticsPayload 只管 IO(讀 status.json/讀 config.json/打網路);
|
||||
// 純合併邏輯抽進 mergeDiagnostics(無 IO),方便測試不必真的連網/彈存檔對話框
|
||||
// 就能涵蓋「同一組數字」「分類原樣照抄」「basename-only」這幾條規則。
|
||||
func (a *App) buildDiagnosticsPayload() exportedDiagnostics {
|
||||
sync := loadSyncStatus()
|
||||
skipped := buildSkipped(sync) // 首頁既有邏輯:Files 已是 basename+白話標籤,直接借用
|
||||
update := a.CheckUpdate() // 現查現答,不用可能過期的背景檢查快取(Q4)
|
||||
|
||||
cfg, _ := loadCfg()
|
||||
accounts := make([]accountDiagnostics, 0, len(cfg.Accounts))
|
||||
for _, acc := range cfg.Accounts {
|
||||
ad := accountDiagnostics{InstanceName: accountName(acc), Host: shortHost(acc.CypherURL)}
|
||||
key := acc.APIKey
|
||||
if strings.TrimSpace(key) == "" {
|
||||
key = acc.Namespace // 同 collector/direct.go 的既有 fallback(api_key 空值時退回 namespace)
|
||||
}
|
||||
cloud, err := fetchCloudDiagnosticsFn(acc.CypherURL, key)
|
||||
if err != nil {
|
||||
ad.CloudError = err.Error()
|
||||
} else {
|
||||
ad.Cloud = cloud
|
||||
}
|
||||
accounts = append(accounts, ad)
|
||||
}
|
||||
|
||||
return mergeDiagnostics(sync, skipped, version, update, accounts)
|
||||
}
|
||||
|
||||
// mergeDiagnostics 純函式:把已經各自拿到的本機/雲端資料組成最終輸出形狀,不做任何 IO。
|
||||
//
|
||||
// leo 08-08 規則對照:
|
||||
// ① 同首頁數字——Progress/FailureBreakdown 原樣接住 sync 裡 t210 已算好的值,不重算。
|
||||
// ② 分類名稱只認得 collector/progress.go 的 ClassifyFailure——這裡原樣照抄
|
||||
// sync.FailureBreakdown,本函式不比對/不認得任何一個分類字串。
|
||||
// ③ 失敗檔名只出 basename——skipped.Files 沿用 buildSkipped() 既有輸出(本來就是
|
||||
// filepath.Base()+白話標籤),本函式不重新處理路徑。
|
||||
func mergeDiagnostics(sync syncStatus, skipped *UISkipped, daemonVersion string, update UpdateInfo, accounts []accountDiagnostics) exportedDiagnostics {
|
||||
local := localDiagnostics{
|
||||
DaemonVersion: daemonVersion,
|
||||
UpdateCheck: update,
|
||||
Progress: sync.Progress,
|
||||
FailureBreakdown: sync.FailureBreakdown,
|
||||
}
|
||||
if skipped != nil {
|
||||
local.SkippedSample = skipped.Files
|
||||
local.SkippedMore = skipped.More
|
||||
}
|
||||
return exportedDiagnostics{
|
||||
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Local: local,
|
||||
Accounts: accounts,
|
||||
}
|
||||
}
|
||||
|
||||
// ExportDiagnostics(前端「疑難排解」按鈕呼叫):組出診斷內容 → 彈系統存檔對話框 →
|
||||
// 使用者選好位置就寫檔。回傳實際寫入的路徑(前端顯示「已存到 xxx」);
|
||||
// 使用者取消對話框時回傳空字串+nil(不算錯誤,不彈紅字嚇人)。
|
||||
func (a *App) ExportDiagnostics() (string, error) {
|
||||
payload := a.buildDiagnosticsPayload()
|
||||
data, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "匯出診斷檔",
|
||||
DefaultFilename: fmt.Sprintf("arcrun-diagnostics-%s.json", time.Now().Format("2006-01-02-15-04-05")),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if path == "" {
|
||||
return "", nil // 使用者按了取消
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
// diagnostics_export_test.go — t213 phase 2:mergeDiagnostics 純函式測試(無網路/無磁碟),
|
||||
// 涵蓋 leo 08-08 三條規則:①同首頁數字 ②分類名稱只認 ClassifyFailure(本檔不自己判斷)
|
||||
// ③失敗檔名 basename-only。
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
collector "arcrun-rag/collector"
|
||||
)
|
||||
|
||||
func TestMergeDiagnostics_SameNumbersAsHomeScreen(t *testing.T) {
|
||||
// leo 規則①:Progress/FailureBreakdown 必須原樣接住 status.json 裡 t210 已算好的值,
|
||||
// 不是本檔重新掃 manifest 算出來的——這裡直接餵一組跟首頁會看到的一模一樣的 syncStatus,
|
||||
// 斷言輸出的 local.progress/local.failure_breakdown 逐欄位相等。
|
||||
sync := syncStatus{
|
||||
Progress: collector.SyncProgress{Total: 9000, Done: 8879, Pending: 101, Stuck: 15, Unreadable: 5},
|
||||
FailureBreakdown: collector.FailureBreakdown{
|
||||
Total: 20,
|
||||
Groups: []collector.FailureGroup{
|
||||
{Category: collector.FailQuotaExhausted, Count: 12},
|
||||
{Category: collector.FailNoTextInFile, Count: 5},
|
||||
{Category: collector.FailOther, Count: 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
out := mergeDiagnostics(sync, nil, "0.18.23", UpdateInfo{Current: "0.18.23"}, nil)
|
||||
|
||||
if out.Local.Progress != sync.Progress {
|
||||
t.Fatalf("progress 沒有原樣接住:got %+v want %+v", out.Local.Progress, sync.Progress)
|
||||
}
|
||||
if out.Local.FailureBreakdown.Total != 20 {
|
||||
t.Fatalf("failure_breakdown.total = %d, want 20", out.Local.FailureBreakdown.Total)
|
||||
}
|
||||
if len(out.Local.FailureBreakdown.Groups) != 3 {
|
||||
t.Fatalf("failure_breakdown.groups 數量跑掉:got %d want 3", len(out.Local.FailureBreakdown.Groups))
|
||||
}
|
||||
// 分母(Q2:9000 檔 vs 雲端 101 張卡)就是 Progress.Total,必須是真的總量,不是本輪計數。
|
||||
if out.Local.Progress.Total != 9000 {
|
||||
t.Fatalf("Q2 的分母跑掉:Total = %d, want 9000", out.Local.Progress.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDiagnostics_CategoryNamesPassThroughVerbatim(t *testing.T) {
|
||||
// leo 規則②:分類名稱字串只准住在 collector/progress.go 的 ClassifyFailure。
|
||||
// 這裡故意餵一個「本檔完全沒見過」的假分類名,驗證 mergeDiagnostics 原樣照抄、
|
||||
// 不會因為認不得而過濾掉或改寫——它不准對分類名稱做任何判斷。
|
||||
sync := syncStatus{
|
||||
FailureBreakdown: collector.FailureBreakdown{
|
||||
Total: 1,
|
||||
Groups: []collector.FailureGroup{{Category: "未來才會新增的假分類", Count: 1}},
|
||||
},
|
||||
}
|
||||
out := mergeDiagnostics(sync, nil, "dev", UpdateInfo{}, nil)
|
||||
if len(out.Local.FailureBreakdown.Groups) != 1 || out.Local.FailureBreakdown.Groups[0].Category != "未來才會新增的假分類" {
|
||||
t.Fatalf("分類名稱沒有原樣照抄:%+v", out.Local.FailureBreakdown.Groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDiagnostics_SkippedNamesAreBasenameOnly(t *testing.T) {
|
||||
// leo 08-08 補的紅線:失敗檔名只出 basename,不出完整路徑。
|
||||
// buildSkipped() 本來就只輸出 filepath.Base()+白話標籤(見 app.go),這裡驗證
|
||||
// mergeDiagnostics 原樣帶出這個既有保證、且序列化後的 JSON 真的看不到路徑分隔符/使用者名稱。
|
||||
skipped := &UISkipped{
|
||||
Files: []string{"教材授權書-Leov2.pages(Pages)", "舊版報告.doc(舊版 Word)"},
|
||||
More: 2,
|
||||
}
|
||||
out := mergeDiagnostics(syncStatus{}, skipped, "dev", UpdateInfo{}, nil)
|
||||
|
||||
if len(out.Local.SkippedSample) != 2 {
|
||||
t.Fatalf("skipped_sample 數量跑掉:%v", out.Local.SkippedSample)
|
||||
}
|
||||
for _, name := range out.Local.SkippedSample {
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
t.Fatalf("skipped_sample 洩漏了路徑分隔符(疑似夾帶完整路徑):%q", name)
|
||||
}
|
||||
}
|
||||
if out.Local.SkippedMore != 2 {
|
||||
t.Fatalf("skipped_more = %d, want 2", out.Local.SkippedMore)
|
||||
}
|
||||
|
||||
// 序列化後再檢查一次(防禦:就算欄位邏輯對,JSON tag 打錯字也可能悄悄漏東西進去)。
|
||||
raw, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "/Users/") || strings.Contains(string(raw), `C:\`) {
|
||||
t.Fatalf("整份 JSON 不該出現絕對路徑:%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDiagnostics_NoSkipped_OmitsSampleFields(t *testing.T) {
|
||||
// 沒有任何檔案被略過(skipped == nil,同 buildSkipped() 的既有語意)→ 不該生出空陣列佔畫面。
|
||||
out := mergeDiagnostics(syncStatus{}, nil, "dev", UpdateInfo{}, nil)
|
||||
if out.Local.SkippedSample != nil {
|
||||
t.Fatalf("沒有略過任何檔案時 SkippedSample 應為 nil,得到 %v", out.Local.SkippedSample)
|
||||
}
|
||||
raw, _ := json.Marshal(out)
|
||||
if strings.Contains(string(raw), "skipped_sample") {
|
||||
t.Fatalf("omitempty 沒生效,空狀態不該出現 skipped_sample 欄位:%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDiagnostics_AccountsCarryCloudOrError(t *testing.T) {
|
||||
// 帳號層:雲端查得到 → cloud 有值;查不到(如目前線上 404,見 t213 部署備註)→ cloud_error
|
||||
// 誠實帶出原因,兩者互斥,且都要附上是哪個帳號/實例(不然多帳號時分不出是誰的狀態)。
|
||||
accounts := []accountDiagnostics{
|
||||
{InstanceName: "youlin.hsieh.dev", Host: "arcrun-cypher-executor.youlin-hsieh-dev.workers.dev",
|
||||
Cloud: map[string]any{"library_count": float64(3), "triplet_count": float64(191)}},
|
||||
{InstanceName: "geek6688", Host: "arcrun-cypher-executor.arcrun-fc9490d5.workers.dev",
|
||||
CloudError: "你的知識庫還是舊版(沒有雲端診斷功能)⇒ 請到 portal 按「立即更新」重裝一次"},
|
||||
}
|
||||
out := mergeDiagnostics(syncStatus{}, nil, "dev", UpdateInfo{}, accounts)
|
||||
if len(out.Accounts) != 2 {
|
||||
t.Fatalf("accounts 數量跑掉:%d", len(out.Accounts))
|
||||
}
|
||||
if out.Accounts[0].Cloud == nil || out.Accounts[0].CloudError != "" {
|
||||
t.Fatalf("第一個帳號應該只有 cloud、沒有 cloud_error:%+v", out.Accounts[0])
|
||||
}
|
||||
if out.Accounts[1].Cloud != nil || out.Accounts[1].CloudError == "" {
|
||||
t.Fatalf("第二個帳號應該只有 cloud_error、沒有 cloud:%+v", out.Accounts[1])
|
||||
}
|
||||
}
|
||||
@@ -232,6 +232,21 @@ function pageUpdate(s) {
|
||||
<h3>需要協助?</h3>
|
||||
<div class="d">安裝、更新、把知識庫接到你的 AI,說明文件都寫在這裡。</div>
|
||||
<div class="acts"><button id="uDocs">開啟使用說明</button></div>
|
||||
</div>
|
||||
${cardDiagnostics()}`;
|
||||
}
|
||||
|
||||
// 疑難排解/匯出診斷檔(t213,2026-08-08):leo 直接指令「一顆按鈕下載一個檔案,
|
||||
// 把檔案發給我」——這裡是那顆按鈕真正住的地方(不是雲端 portal 網頁,那邊碰不到
|
||||
// 這台電腦上的資料)。按下去在**這台電腦**上合併本機統計+雲端統計成一份 JSON,
|
||||
// 彈系統存檔對話框讓你選位置存。只有數字/狀態,不含你的任何文件內容。
|
||||
function cardDiagnostics() {
|
||||
return `
|
||||
<div class="card">
|
||||
<h3>疑難排解</h3>
|
||||
<div class="d">搜尋或同步有問題時,可以匯出一份診斷檔給我們,幫你更快找到問題(只有統計數字,不含你的任何文件內容)。</div>
|
||||
<div class="acts"><button id="uDiag">匯出診斷檔</button></div>
|
||||
<div class="d" id="uDiagStatus" style="margin-top:8px"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -273,6 +288,7 @@ function wire() {
|
||||
on('obInstall', () => go.OpenURL('https://install.arcrun.dev/'));
|
||||
on('aiSave', saveAI);
|
||||
on('uCheck', checkUpdate); on('uDownload', downloadUpdate); on('uApply', applyUpdate);
|
||||
on('uDiag', exportDiagnostics);
|
||||
document.querySelectorAll('[data-portal]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.portal); });
|
||||
document.querySelectorAll('[data-openurl]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.openurl); });
|
||||
document.querySelectorAll('[data-addto]').forEach((b) => { b.onclick = () => addFolder(Number(b.dataset.addto)); });
|
||||
@@ -364,5 +380,18 @@ async function applyUpdate() {
|
||||
try { await go.ApplyUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; renderPage(); }
|
||||
}
|
||||
|
||||
// 匯出診斷檔(t213):後端 ExportDiagnostics 自己彈系統存檔對話框;回傳空字串=
|
||||
// 使用者按了取消,不算錯誤、不顯示紅字。
|
||||
async function exportDiagnostics() {
|
||||
const el = $('uDiagStatus');
|
||||
if (el) el.textContent = '匯出中…';
|
||||
try {
|
||||
const path = await go.ExportDiagnostics();
|
||||
if (el) el.textContent = path ? `已存到:${path}` : '已取消。';
|
||||
} catch (ex) {
|
||||
if (el) el.textContent = '匯出失敗:' + String(ex);
|
||||
}
|
||||
}
|
||||
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
|
||||
Reference in New Issue
Block a user