Files
arcrun-collector/template_install.go
T
Leo c7675a10a0 不再把開發用 template 塞進使用者資料夾;上游錯誤不再被退避訊息吞掉
## leo 三條裁決,逐條落實
① 「**別人的錯誤一律要顯示給用戶看,不然就會變成我的錯誤,導致客服**」
   → manifest entry 新增 `LastError`(存**原文**),退避訊息改成
     「上次失敗(第 N 次),58m 後重試|**原因:**<上游原文>」。
     成功後清空,不留舊錯誤嚇人。
     測試 `TestUpstreamErrorSurvivesBackoff` 從上游錯誤字串出發,
     驗它活到給使用者看的那句話;反向驗證拿掉即紅。

② 「**拿來開發一般人用不到的根本別安裝**」
   → daemon 不再代裝 system-dev template(`CLAUDE.md`/`scripts/`/`system-dev/`,37 檔)。
     兩層傷害:把人家資料夾弄亂 + 那些檔被當知識吃進去
     ⇒ 知識庫長出 `kb`/`t195-watch` 這種不是使用者內容的庫(leo 實撞)。
     `template-install` 子命令保留,開發者情境不受影響。

③ 「**它也不能只看隱藏檔內,因為 template 在我所有的 repo 裡不是隱藏的**」
   → 排除規則用**路徑身分**(`TemplateOwns()`,來源是內嵌 templatefs 的實際路徑
     + `system-dev/`・`scripts/` 目錄前綴),**不是**「有沒有以點開頭」。
     測試刻意把 template 檔放成**不隱藏**(就像 leo 的 repo),驗它仍被排除;反向驗證即紅。
     這些檔也不計進「有 N 個檔案沒有被整理」——那欄是給使用者看他自己的檔案的。

## 順帶修 leo 截圖上的重複
「有 2 個檔案沒有被整理」底下 `scripts/sdd-active-check.sh` 出現兩次
——多帳號時同一個資料夾被掃多輪、每輪都 append。已去重。

## 殘項(誠實)
App 端失敗卡還沒接上這條線 ⇒ **畫面尚未真的顯示原因**。未打包、未送達。
2026-08-06 21:33:37 +08:00

161 lines
5.0 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.
// template_install.go — daemon 代裝 system-dev-templatedaemon-beta task 2)。
//
// leo 四步定稿第 1 步:「幫你在指定資料夾安裝我們現有的 template,這個測過很多次,
// 但要讓 daemon 可用」——即**不走 git clone**template 快照(templatefs/vendored
// 打包進二進位,daemon 直接鋪檔。
//
// 冪等鐵律:**已存在的檔案一律不覆寫**(用戶的 status.md/wiki 是他的資產;
// 升級 template 版本=另案 update 流程,不在本函式)。
package collector
import (
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
// templateFS 是 system-dev-template 的 vendored 快照(版本見 templatefs/system-dev/VERSION)。
//
//go:embed all:templatefs
var templateFS embed.FS
const templateFSRoot = "templatefs"
// TemplateInstallResult 是一次代裝的結果帳目。
type TemplateInstallResult struct {
Installed []string `json:"installed"` // 本次新鋪的檔(相對路徑)
Skipped []string `json:"skipped"` // 已存在故跳過的檔
Version string `json:"version"` // 快照版本(templatefs/system-dev/VERSION
}
// TemplateVersion 讀出內嵌快照的版本號。
func TemplateVersion() string {
data, err := templateFS.ReadFile(templateFSRoot + "/system-dev/VERSION")
if err != nil {
return "unknown"
}
v := string(data)
for len(v) > 0 && (v[len(v)-1] == '\n' || v[len(v)-1] == '\r') {
v = v[:len(v)-1]
}
return v
}
// templateOwnedPaths 回傳「這份 template 擁有的相對路徑」集合(斜線分隔)。
//
// 🔴 leo 2026-08-06:「**它也不能只看隱藏檔內,因為 template 在我所有的 repo 裡不是隱藏的**」
//
// ⇒ 判準必須是**路徑身分**(這些路徑本來就是 template 的),不是「有沒有以點開頭」。
// 用途:掃描時把它們排除在知識之外——不管它是 daemon 舊版鋪的、
// 還是使用者自己的開發 repo 本來就有的,那都不是他想搜尋的內容。
func templateOwnedPaths() map[string]bool {
owned := map[string]bool{}
_ = fs.WalkDir(templateFS, templateFSRoot, func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
if rel, rerr := filepath.Rel(templateFSRoot, p); rerr == nil {
owned[filepath.ToSlash(rel)] = true
}
return nil
})
return owned
}
// TemplateOwns 回答「這個相對路徑是不是 template 的東西」。
// 也認**目錄前綴**template 的 `system-dev/` 底下使用者後來自己加的檔
// (例如他寫的 wiki)同樣是開發用的,不該進知識庫。
func TemplateOwns(rel string) bool {
rel = filepath.ToSlash(rel)
if templateOwnedCache[rel] {
return true
}
for _, prefix := range templateOwnedDirs {
if strings.HasPrefix(rel, prefix) {
return true
}
}
return false
}
var (
templateOwnedCache = templateOwnedPaths()
// 這些目錄整棵都是開發用的(template 鋪的、或使用者自己長出來的都一樣)。
templateOwnedDirs = []string{"system-dev/", "scripts/"}
)
// InstallTemplate 把內嵌 template 鋪進 root。冪等:既有檔案跳過不覆寫。
func InstallTemplate(root string) (*TemplateInstallResult, error) {
res := &TemplateInstallResult{Version: TemplateVersion()}
absRoot, err := filepath.Abs(root)
if err != nil {
return nil, err
}
if err := os.MkdirAll(absRoot, 0o755); err != nil {
return nil, fmt.Errorf("建目標資料夾失敗:%w", err)
}
err = fs.WalkDir(templateFS, templateFSRoot, func(p string, d fs.DirEntry, werr error) error {
if werr != nil {
return werr
}
if d.IsDir() {
return nil
}
rel, rerr := filepath.Rel(templateFSRoot, p)
if rerr != nil {
return rerr
}
dest := filepath.Join(absRoot, rel)
if _, serr := os.Stat(dest); serr == nil {
res.Skipped = append(res.Skipped, rel)
return nil // 冪等:不覆寫用戶既有檔
}
if merr := os.MkdirAll(filepath.Dir(dest), 0o755); merr != nil {
return merr
}
data, derr := templateFS.ReadFile(p)
if derr != nil {
return derr
}
if werr2 := os.WriteFile(dest, data, 0o644); werr2 != nil {
return werr2
}
res.Installed = append(res.Installed, rel)
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
// TemplateInstalled 判斷 root 是否已鋪過(以 wiki 標記檔存在為準)。
func TemplateInstalled(root string) bool {
_, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "status.md"))
return err == nil
}
// runTemplateInstall 是 `collector template-install` 子命令主體。
func runTemplateInstall(args []string) int {
fs2 := newFlagSet()
folder := fs2.String("folder", "", "要鋪 template 的資料夾(必填)")
if err := fs2.Parse(args); err != nil {
return 2
}
if *folder == "" {
fmt.Fprintln(os.Stderr, "錯誤:--folder 為必填")
return 2
}
res, err := InstallTemplate(*folder)
if err != nil {
fmt.Fprintln(os.Stderr, "collector template-install:", err)
return 1
}
fmt.Printf("template v%s:新鋪 %d 檔、跳過既有 %d 檔 → %s\n",
res.Version, len(res.Installed), len(res.Skipped), *folder)
return 0
}