diff --git a/deploy/app/Dockerfile b/deploy/app/Dockerfile index eafd7510..b582061d 100644 --- a/deploy/app/Dockerfile +++ b/deploy/app/Dockerfile @@ -30,6 +30,8 @@ WORKDIR /app COPY --from=go-builder /server /app/server COPY --from=web-builder /web/dist /app/web/dist COPY deploy/postgres/goose /app/deploy/postgres/goose +COPY templates /app/templates +COPY configs /app/configs ENV DATA_DIR=/app/data \ HTTP_HOST=0.0.0.0 \ diff --git a/internal/template/loader.go b/internal/template/loader.go index 0eed3ed8..2d825343 100644 --- a/internal/template/loader.go +++ b/internal/template/loader.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "strings" + + "chat-agent-system/templates" ) func findProjectRoot() string { @@ -25,6 +27,12 @@ func findProjectRoot() string { 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) + } + if b, err := templates.DocTemplates.ReadFile("docs/" + name + ".md"); err == nil && len(strings.TrimSpace(string(b))) > 0 { + return string(b), nil + } root := findProjectRoot() path := filepath.Join(root, "templates", "docs", name+".md") b, err := os.ReadFile(path) diff --git a/internal/template/loader_test.go b/internal/template/loader_test.go index 0b174c74..8f22e7ad 100644 --- a/internal/template/loader_test.go +++ b/internal/template/loader_test.go @@ -38,3 +38,32 @@ func TestLoadDocTemplatesAndAgents(t *testing.T) { t.Errorf("render failed to replace customer_name") } } + +func TestGetDocTemplate_PrelimDesignOfficialNotStub(t *testing.T) { + tmpl, err := template.GetDocTemplate("prelim_design") + if err != nil { + t.Fatalf("prelim_design must load from embed/disk: %v", err) + } + if !strings.Contains(tmpl, "系统初步设计方案") { + t.Fatalf("official prelim_design title missing") + } + if !strings.Contains(tmpl, "```mermaid") { + t.Fatalf("official prelim_design must keep mermaid context diagram") + } + if strings.Contains(tmpl, "## 1. 结论") && strings.Contains(tmpl, "待确认") && !strings.Contains(tmpl, "子系统划分") { + t.Fatalf("must not fall back to two-section stub") + } +} + +func TestGetDocTemplate_WorksWithoutDiskCheckout(t *testing.T) { + t.Chdir(t.TempDir()) + for _, key := range []string{"intake", "prelim_design", "research"} { + tmpl, err := template.GetDocTemplate(key) + if err != nil { + t.Fatalf("%s must resolve from embed when cwd has no templates/: %v", key, err) + } + if strings.TrimSpace(tmpl) == "" { + t.Fatalf("%s embed was empty", key) + } + } +} diff --git a/templates/embed.go b/templates/embed.go new file mode 100644 index 00000000..ce0204a7 --- /dev/null +++ b/templates/embed.go @@ -0,0 +1,9 @@ +package templates + +import "embed" + +// DocTemplates holds official workflow markdown so containers without a +// checkout of templates/docs still serve the real contract, not a stub. +// +//go:embed docs/*.md +var DocTemplates embed.FS