Files
Agent-System-Backend/internal/tools/document_renderer.go
T
weichen a21a3b57b4 Route workspace Word delivery through the shared renderer
Why: Task 10 replaces Backend's independent Markdown-to-Word production path with the reviewed canonical parser, embedded organization-default candidate, and Word adapter.
Evidence: Canonical parity, deterministic package, routing, safety, cancellation, comparison, persistence, affected-package race, full test, vet, build, and static build checks passed locally.
Impact: Ordinary workspace renders now persist Markdown, deterministic DOCX, manifest, and archive with hashes; controlled/project/review/approved routes fail closed to RDM, and legacy Go rendering is comparison-only.
Refs: Unified Document Renderer Task 10; Renderer d449b9f779f2f968f8ac556b89940a6c9aaf3fdc
2026-10-15 19:41:14 +00:00

118 lines
4.7 KiB
Go

package tools
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"agent-document-renderer/render"
"agent-system-backend/internal/docgen"
core "agent-system-core"
)
func registerDocumentRenderer(r *Registry) error {
return r.Register(core.ToolDesc{
Name: "render_document", Source: core.SourceBuiltin, Edits: true, Risk: "medium",
Description: "把普通工作区 Markdown 通过统一文档渲染器生成 Markdown、DOCX、manifest 和可下载归档。只允许非受控工作区交付;项目受控文档必须交给 RDM。公司默认规则仍是待视觉评审候选,状态会写入 manifest。",
Parameters: json.RawMessage(`{"type":"object","properties":{` +
`"name":{"type":"string"},"documentId":{"type":"string"},"documentType":{"type":"string"},` +
`"language":{"type":"string"},"markdown":{"type":"string"},` +
`"mode":{"type":"string","enum":["working","preview"]},"frozenAt":{"type":"string"},` +
`"controlled":{"type":"boolean"},"projectDocument":{"type":"boolean"}` +
`},"required":["name","documentId","markdown","frozenAt","controlled","projectDocument"]}`),
}, executeDocumentRenderer)
}
type documentRendererArgs struct {
Name string `json:"name"`
DocumentID string `json:"documentId"`
DocumentType string `json:"documentType"`
Language string `json:"language"`
Markdown string `json:"markdown"`
Mode string `json:"mode"`
FrozenAt string `json:"frozenAt"`
Controlled *bool `json:"controlled"`
ProjectDocument *bool `json:"projectDocument"`
}
func executeDocumentRenderer(rc ToolExecCtx, raw json.RawMessage) (string, error) {
var args documentRendererArgs
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&args); err != nil {
return "", fmt.Errorf("render_document: bad args: %w", err)
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
return "", errors.New("render_document: arguments must contain one JSON object")
}
if strings.TrimSpace(args.Name) == "" || strings.TrimSpace(args.DocumentID) == "" || strings.TrimSpace(args.Markdown) == "" {
return "", errors.New("render_document: name, documentId, and markdown are required")
}
if args.Controlled == nil || args.ProjectDocument == nil {
return "", errors.New("render_document: controlled and projectDocument must be explicit")
}
if strings.TrimSpace(rc.OwnerID()) == "" {
return "", errors.New("render_document: trusted requester identity is required")
}
frozenAt, err := time.Parse(time.RFC3339Nano, args.FrozenAt)
if err != nil {
return "", fmt.Errorf("render_document: frozenAt must be RFC3339: %w", err)
}
mode := render.Mode(args.Mode)
if mode == "" {
mode = render.ModePreview
}
if mode != render.ModeWorking && mode != render.ModePreview {
return "", fmt.Errorf("render_document: mode %q is unavailable for an ordinary workspace document", mode)
}
documentType := strings.TrimSpace(args.DocumentType)
if documentType == "" {
documentType = "general"
}
language := strings.TrimSpace(args.Language)
if language == "" {
language = "zh-CN"
}
delivery, err := docgen.Generate(execParentCtx(rc), docgen.Request{
DocumentID: args.DocumentID, Title: args.Name, DocumentType: documentType, Language: language,
Markdown: []byte(args.Markdown), Mode: mode, FrozenAt: frozenAt.UTC(), RequestedBy: rc.OwnerID(),
Controlled: *args.Controlled, ProjectDocument: *args.ProjectDocument,
})
if err != nil {
return "", fmt.Errorf("render_document: %w", err)
}
artifacts := []docgen.Artifact{delivery.Markdown, delivery.DOCX, delivery.Manifest, delivery.Archive}
for _, item := range artifacts {
if err := checkToolExternalEffect(rc); err != nil {
return "", fmt.Errorf("render_document: permission recheck before %s: %w", item.Name, err)
}
if _, err := rc.WriteArtifact(item.Name, artifactKindOf(item.Name, item.MIME), item.MIME, item.Bytes); err != nil {
return "", fmt.Errorf("render_document: persist %s: %w", item.Name, err)
}
}
type outputArtifact struct {
Name string `json:"name"`
SHA256 string `json:"sha256"`
Size int `json:"size"`
}
output := struct {
ProfileStatus string `json:"profileStatus"`
PublicationAllowed bool `json:"publicationAllowed"`
Artifacts []outputArtifact `json:"artifacts"`
}{ProfileStatus: delivery.Profile.ReviewStatus, PublicationAllowed: delivery.Profile.PublicationAllowed}
for _, item := range artifacts {
output.Artifacts = append(output.Artifacts, outputArtifact{Name: item.Name, SHA256: item.SHA256, Size: len(item.Bytes)})
}
encoded, err := json.Marshal(output)
if err != nil {
return "", err
}
return string(encoded), nil
}