package template import ( "fmt" "os" "path/filepath" "strings" "chat-agent-system/internal/workflowdef" "chat-agent-system/templates" ) func findProjectRoot() string { wd, _ := os.Getwd() candidates := []string{wd} dir := wd for i := 0; i < 10; i++ { candidates = append(candidates, dir) parent := filepath.Dir(dir) if parent == dir { break } dir = parent } if env := strings.TrimSpace(os.Getenv("APP_ROOT")); env != "" { candidates = append(candidates, env) } // Image WORKDIR is /app; Dockerfile copies templates/configs here. candidates = append(candidates, "/app") for _, root := range candidates { if root == "" { continue } if _, err := os.Stat(filepath.Join(root, "templates", "docs")); err == nil { return root } } return wd } func GetDocTemplate(name string) (string, error) { name = strings.TrimSuffix(name, ".md") if name == "" || strings.Contains(name, "..") || strings.ContainsAny(name, `/\`) { return "", fmt.Errorf("read doc template: invalid name %q", name) } for _, candidate := range templateNames(name) { if b, err := templates.DocTemplates.ReadFile("docs/" + candidate + ".md"); err == nil && len(strings.TrimSpace(string(b))) > 0 { return string(b), nil } root := findProjectRoot() path := filepath.Join(root, "templates", "docs", candidate+".md") b, err := os.ReadFile(path) if err == nil && len(strings.TrimSpace(string(b))) > 0 { return string(b), nil } } return "", fmt.Errorf("read doc template %s: file not found", name) } func templateNames(name string) []string { names := []string{name} if canonical := workflowdef.CanonicalTaskKey(name); canonical != name { names = append(names, canonical) } if legacy := workflowdef.ImplementationTaskKey(name); legacy != name { names = append(names, legacy) } return names } func GetAgentInstruction(role string) (string, error) { role = strings.TrimSuffix(role, ".md") if role == "" || strings.Contains(role, "..") || strings.ContainsAny(role, `/\`) { return "", fmt.Errorf("read agent instruction: invalid role %q", role) } return GetAgentInstructionFile(filepath.Join("configs", "agents", role+".md")) } // GetAgentInstructionFile 读取 configs/agents/ 下指定相对路径的指令文件, // 并自动追加 configs/shared/writing.md。rel 必须落在 configs/agents/ 内,拒绝 ".."。 func GetAgentInstructionFile(rel string) (string, error) { rel = filepath.ToSlash(strings.TrimSpace(rel)) if rel == "" || strings.Contains(rel, "..") { return "", fmt.Errorf("read agent instruction file: invalid path %q", rel) } if !strings.HasPrefix(rel, "configs/agents/") { return "", fmt.Errorf("read agent instruction file: must be under configs/agents/: %q", rel) } root := findProjectRoot() path := filepath.Join(root, filepath.FromSlash(rel)) b, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("read agent instruction file %s: %w", rel, err) } inst := string(b) writingPath := filepath.Join(root, "configs", "shared", "writing.md") if wb, err := os.ReadFile(writingPath); err == nil { inst += "\n\n" + string(wb) } return inst, nil }