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
102 lines
3.9 KiB
Go
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
|
|
}
|