Files
CyberStrikeAI/internal/workflow/package/exporter.go
T
chenchen acfacfe1b3 feat(工作流):支持本地图编排策略包导入导出 (#195)
* docs: define local workflow package mvp

* docs: fix workflow package api contract

* feat: add local workflow package mvp

* feat(workflow): add package API client

* feat(workflow): add package import interface

* feat(workflow): connect package import flow

* fix(workflow): map package validation errors

* fix(workflow): handle import key generation errors

* feat(workflow): localize package import states

* fix(workflow): reset package import modal on open

---------

Co-authored-by: ruanmingchen <“ruanm@chenchen”>
2026-07-14 11:28:37 +08:00

99 lines
3.2 KiB
Go

package workflowpackage
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"path"
"strings"
"time"
)
// Export builds a deterministic, human-readable single-workflow package.
func Export(source Document) ([]byte, ExportMetadata, error) {
source.ID = strings.TrimSpace(source.ID)
source.Name = strings.TrimSpace(source.Name)
if source.ID == "" || source.Name == "" || source.Version <= 0 || !safePackageWorkflowID(source.ID) {
return nil, ExportMetadata{}, fmt.Errorf("workflow id, name and version are required")
}
if strings.TrimSpace(source.GraphJSON) == "" {
return nil, ExportMetadata{}, fmt.Errorf("workflow graph_json is required")
}
contentHash, graphHash, payload, err := DocumentHashes(source)
if err != nil {
return nil, ExportMetadata{}, err
}
workflowPath := path.Join("workflows", source.ID+".json")
createdAt := source.UpdatedAt.UTC()
if createdAt.IsZero() {
createdAt = time.Unix(0, 0).UTC()
}
manifest := Manifest{
PackageFormat: PackageFormat,
FormatVersion: FormatVersion,
PackageID: "pkg_" + strings.TrimPrefix(contentHash, "sha256:")[:16],
CreatedAt: createdAt.Format(time.RFC3339),
Items: []ManifestItem{{
Type: "workflow",
Path: workflowPath,
SourceID: source.ID,
SourceRevision: source.Version,
ContentHash: contentHash,
GraphHash: graphHash,
}},
}
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return nil, ExportMetadata{}, fmt.Errorf("marshal manifest: %w", err)
}
checksums := fmt.Sprintf("%s manifest.json\n%s %s\n", strings.TrimPrefix(sha256Prefixed(manifestBytes), "sha256:"), strings.TrimPrefix(contentHash, "sha256:"), workflowPath)
var out bytes.Buffer
zw := zip.NewWriter(&out)
for _, entry := range []struct {
name string
data []byte
}{
{name: "checksums.sha256", data: []byte(checksums)},
{name: "manifest.json", data: manifestBytes},
{name: workflowPath, data: payload},
} {
header := &zip.FileHeader{Name: entry.name, Method: zip.Store}
header.SetModTime(time.Unix(0, 0).UTC())
writer, err := zw.CreateHeader(header)
if err != nil {
return nil, ExportMetadata{}, fmt.Errorf("write %s: %w", entry.name, err)
}
if _, err := writer.Write(entry.data); err != nil {
return nil, ExportMetadata{}, fmt.Errorf("write %s: %w", entry.name, err)
}
}
if err := zw.Close(); err != nil {
return nil, ExportMetadata{}, fmt.Errorf("close package: %w", err)
}
pkg := out.Bytes()
return pkg, ExportMetadata{
PackageHash: sha256Prefixed(pkg),
ContentHash: contentHash,
GraphHash: graphHash,
SourceRevision: source.Version,
FileName: source.ID + ".csapkg.zip",
}, nil
}
// DocumentHashes returns the canonical package item and graph hashes together
// with the canonical item bytes used by export and inspection persistence.
func DocumentHashes(source Document) (string, string, []byte, error) {
graph, err := canonicalJSON([]byte(source.GraphJSON))
if err != nil {
return "", "", nil, fmt.Errorf("canonicalize graph_json: %w", err)
}
source.GraphJSON = string(graph)
payload, err := json.Marshal(source)
if err != nil {
return "", "", nil, fmt.Errorf("marshal workflow payload: %w", err)
}
return sha256Prefixed(payload), sha256Prefixed(graph), payload, nil
}