Files
xinmi/internal/workbench/quality.go
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

138 lines
4.6 KiB
Go

package workbench
import (
"crypto/sha256"
"fmt"
"strings"
)
type QualityIssue struct {
SegmentIDs []string `json:"segment_ids"`
Severity string `json:"severity"`
Problem string `json:"problem"`
Suggestion string `json:"suggestion"`
}
type QualityReport struct {
DurationMinutes int `json:"duration_minutes,omitempty"`
Passed bool `json:"passed"`
Summary string `json:"summary"`
Issues []QualityIssue `json:"issues"`
ManuscriptHash string `json:"manuscript_hash"`
SettingsHash string `json:"settings_hash,omitempty"`
}
type QualitySettings struct {
Rules string `json:"rules"`
Style string `json:"style"`
}
func (settings QualitySettings) validate() error {
if len([]rune(settings.Rules)) > 6000 || len([]rune(settings.Style)) > 2000 {
return fmt.Errorf("%w: 质检规则最多 6000 字,风格要求最多 2000 字", ErrInvalid)
}
return nil
}
func (settings QualitySettings) hash() string {
if settings.Rules == "" && settings.Style == "" {
return ""
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(compact(settings))))
}
func (settings QualitySettings) instructions(advice string) string {
return "任务质检规则:\n" + settings.Rules + "\n任务表达风格:\n" + settings.Style + "\n本次补充意见:\n" + advice
}
func manuscriptHash(segments []Segment) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(compact(segments))))
}
func validateQuality(report *QualityReport, segments []Segment) error {
if report == nil || strings.TrimSpace(report.Summary) == "" {
return fmt.Errorf("%w: AI 质检结果缺少总结,请重试", ErrInvalid)
}
ids := map[string]bool{}
for _, seg := range segments {
ids[seg.ID] = true
}
blocking := false
for _, issue := range report.Issues {
if issue.Severity != "blocking" && issue.Severity != "suggestion" {
return fmt.Errorf("%w: AI 质检问题缺少有效等级", ErrInvalid)
}
if len(issue.SegmentIDs) == 0 || strings.TrimSpace(issue.Problem) == "" || strings.TrimSpace(issue.Suggestion) == "" {
return fmt.Errorf("%w: AI 质检问题缺少位置或修改建议", ErrInvalid)
}
for _, id := range issue.SegmentIDs {
if !ids[id] {
return fmt.Errorf("%w: AI 质检引用了不存在的正文", ErrInvalid)
}
}
blocking = blocking || issue.Severity == "blocking"
}
if report.Passed == blocking {
return fmt.Errorf("%w: AI 质检结论与问题等级冲突,请重新质检", ErrInvalid)
}
report.ManuscriptHash = manuscriptHash(segments)
return nil
}
func qualityMarkdown(report *QualityReport) string {
verdict := "通过"
if !report.Passed {
verdict = "待修改"
}
text := "## 跨稿质检:" + verdict + "\n\n" + report.Summary
for i, issue := range report.Issues {
severity := "建议"
if issue.Severity == "blocking" {
severity = "必须修改"
}
text += fmt.Sprintf("\n\n### %d. %s · %s\n\n%s\n\n修改建议:%s", i+1, severity, strings.Join(issue.SegmentIDs, "、"), issue.Problem, issue.Suggestion)
}
return text
}
func requireQuality(d TaskData, accepted []Segment) error {
if d.Quality == nil || !d.Quality.Passed || d.Quality.ManuscriptHash != manuscriptHash(accepted) || d.Quality.SettingsHash != d.QualitySettings.hash() || d.Quality.DurationMinutes != d.DurationMinutes {
return fmt.Errorf("%w: 请先完成当前正文的跨稿质检并处理必须修改的问题,再组合成稿", ErrInvalid)
}
return nil
}
// Nodes may repeat an Agent, so assignment includes the canvas node identity.
// Reject reversed dependencies instead of relying on a prompt-only ordering rule.
func validateSegmentOrder(segments []Segment, flow FlowData) error {
nodes := map[string]FlowNode{}
byAgent := map[string][]string{}
for _, node := range flow.Nodes {
nodes[node.ID] = node
byAgent[node.AgentID] = append(byAgent[node.AgentID], node.ID)
}
first, last := map[string]int{}, map[string]int{}
for i := range segments {
segment := &segments[i]
if segment.NodeID == "" && len(byAgent[segment.AgentID]) == 1 {
segment.NodeID = byAgent[segment.AgentID][0]
}
node, ok := nodes[segment.NodeID]
if !ok || node.AgentID != segment.AgentID {
return fmt.Errorf("%w: 内容段缺少对应画布节点或职责不匹配,请重新拆解", ErrInvalid)
}
if _, exists := first[node.ID]; !exists {
first[node.ID] = i
}
last[node.ID] = i
}
for _, edge := range flow.Edges {
end, sourcePresent := last[edge.Source]
start, targetPresent := first[edge.Target]
if targetPresent && (!sourcePresent || end >= start) {
return fmt.Errorf("%w: 内容段没有遵循画布前后依赖,请重新拆解", ErrInvalid)
}
}
return nil
}