Files
Agent-System-Backend/internal/tools/document_renderer.go
T
weichen fec2d16cc6 Close Task 10 renderer delivery gaps
Why:
- Independent review found trusted-boundary, semantic parity, replay-budget,
  comparison, persistence, cancellation, provenance, and routing gaps.
- Storage migration must include the new durable delivery table.

Evidence:
- go test -count=1 ./...
- go test -race ./internal/docgen ./internal/tools ./internal/runmgr
- go test ./... (deps/agent-document-renderer)
- go vet ./...
- go build ./...
- CGO_ENABLED=0 go build -trimpath ./cmd/agent-system
- Python house-style unittest suite and git diff --check

Impact:
- Word delivery now uses host-owned freezes, Markdown-first resumable persistence,
  closed registration boundaries, semantic shared rendering, and real comparison.
- Controlled/project documents still fail closed to RDM and non-Word flows remain
  unchanged.

Refs:
- Unified Document Renderer implementation plan Task 10
- /tmp/task10-backend-review-last.md
- Renderer d5cd8c126045a27d5a5cc5da23f64d3c737e116a
2026-10-15 21:44:03 +00:00

102 lines
3.9 KiB
Go

package tools
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"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","additionalProperties":false,"properties":{` +
`"operationId":{"type":"string"},"name":{"type":"string"},"documentId":{"type":"string"},"documentType":{"type":"string"},` +
`"language":{"type":"string"},"markdown":{"type":"string"},` +
`"mode":{"type":"string","enum":["working","preview"]},` +
`"controlled":{"type":"boolean"},"projectDocument":{"type":"boolean"}` +
`},"required":["operationId","name","documentId","markdown","controlled","projectDocument"]}`),
}, executeDocumentRenderer)
}
type documentRendererArgs struct {
OperationID string `json:"operationId"`
Name string `json:"name"`
DocumentID string `json:"documentId"`
DocumentType string `json:"documentType"`
Language string `json:"language"`
Markdown string `json:"markdown"`
Mode string `json:"mode"`
Controlled *bool `json:"controlled"`
ProjectDocument *bool `json:"projectDocument"`
}
type documentDeliveryHost interface {
ExecuteDocumentDelivery(context.Context, docgen.OperationRequest) (docgen.OperationReceipt, error)
}
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.OperationID) == "" || strings.TrimSpace(args.Name) == "" || strings.TrimSpace(args.DocumentID) == "" || strings.TrimSpace(args.Markdown) == "" {
return "", errors.New("render_document: operationId, 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 *args.Controlled || *args.ProjectDocument {
return "", docgen.ErrRDMRequired
}
if strings.TrimSpace(rc.OwnerID()) == "" {
return "", errors.New("render_document: trusted requester identity is required")
}
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"
}
host, ok := rc.(documentDeliveryHost)
if !ok {
return "", errors.New("render_document: trusted delivery host is unavailable")
}
receipt, err := host.ExecuteDocumentDelivery(execParentCtx(rc), docgen.OperationRequest{
OperationID: args.OperationID, DocumentID: args.DocumentID, Title: args.Name,
DocumentType: documentType, Language: language, Markdown: []byte(args.Markdown), Mode: mode,
Controlled: *args.Controlled, ProjectDocument: *args.ProjectDocument,
})
encoded, marshalErr := json.Marshal(receipt)
if marshalErr != nil {
return "", marshalErr
}
if err != nil {
return "", fmt.Errorf("render_document: %s", encoded)
}
return string(encoded), nil
}