4b3aaff321
治理侧保存输入观测版本、自动复检及人工复核历史,输入变化失效、版本冲突和幂等重放不覆盖人工记录。批量质量数量使用完整筛选元数据,未采集原文不伪造快照,延迟旧扫描不回退新输入。 验证:全仓 make verify 退出码0,Go656及前端253通过;五包竞态、源/组织/质量隔离PG、真实只读源和隔离网页读回复核通过。默认91个可选集成跳过不计通过。未接AI模型,不写DobotAI源库,未推送、未合并、未部署。
96 lines
3.0 KiB
Go
96 lines
3.0 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"knowledge-governance/internal/qaopsorg"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type QAOrgSyncStore interface {
|
|
GetSyncStatus(context.Context) (qaopsorg.SyncStatus, error)
|
|
EnqueueSync(context.Context, string, string) (qaopsorg.SyncReceipt, bool, error)
|
|
}
|
|
|
|
func authorizeQAOrganizationWrite(w http.ResponseWriter, r *http.Request) bool {
|
|
return authorizeQAIntent(w, r, "qa-organization")
|
|
}
|
|
func authorizeQAIntent(w http.ResponseWriter, r *http.Request, intent string) bool {
|
|
if r.Header.Get("X-Governance-Action") != intent || r.Header.Get("Sec-Fetch-Site") == "cross-site" {
|
|
writeJSON(w, 403, map[string]string{"error": "此操作需要同源操作意图"})
|
|
return false
|
|
}
|
|
// Fetch Metadata survives same-origin proxies that rewrite Host; untrusted
|
|
// forwarded headers must not grant cross-origin write authority.
|
|
if raw := r.Header.Get("Origin"); raw != "" && r.Header.Get("Sec-Fetch-Site") != "same-origin" {
|
|
origin, err := url.Parse(raw)
|
|
if err != nil || (origin.Scheme != "http" && origin.Scheme != "https") || origin.Host != r.Host {
|
|
writeJSON(w, 403, map[string]string{"error": "拒绝跨站配置请求"})
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func qaOrganizationSync(w http.ResponseWriter, r *http.Request, c Config) {
|
|
if !authorizeQAOps(w, r) {
|
|
return
|
|
}
|
|
if r.URL.RawQuery != "" {
|
|
writeOrgError(w, qaopsorg.ErrInvalid)
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
if r.Method == http.MethodGet {
|
|
if c.QAOrgSync == nil {
|
|
writeJSON(w, 200, map[string]any{"data": qaopsorg.SyncStatus{}})
|
|
return
|
|
}
|
|
result, err := c.QAOrgSync.GetSyncStatus(r.Context())
|
|
if err != nil {
|
|
writeOrgError(w, err)
|
|
return
|
|
}
|
|
result.Configured = c.QAOrgSyncEnabled
|
|
writeJSON(w, 200, map[string]any{"data": result})
|
|
return
|
|
}
|
|
if !authorizeQAOrganizationWrite(w, r) {
|
|
return
|
|
}
|
|
key := r.Header.Get("Idempotency-Key")
|
|
if key == "" || len(key) > 128 || strings.TrimSpace(key) != key {
|
|
writeJSON(w, 400, map[string]string{"error": "同步请求需要有效 Idempotency-Key,重试须复用原标识"})
|
|
return
|
|
}
|
|
var body *struct{}
|
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1024))
|
|
decoder.DisallowUnknownFields()
|
|
if decoder.Decode(&body) != nil || body == nil || decoder.Decode(&struct{}{}) != io.EOF {
|
|
writeOrgError(w, qaopsorg.ErrInvalid)
|
|
return
|
|
}
|
|
if !c.QAOrgSyncEnabled || c.QAOrgSync == nil {
|
|
writeJSON(w, 503, map[string]string{"error": "通讯录后台同步未接线,未创建任务"})
|
|
return
|
|
}
|
|
identity, _ := sessionFrom(r.Context())
|
|
receipt, replayed, err := c.QAOrgSync.EnqueueSync(r.Context(), key, identity.Username)
|
|
if err != nil {
|
|
if errors.Is(err, qaopsorg.ErrSyncWorkerUnavailable) {
|
|
writeJSON(w, 503, map[string]string{"error": "通讯录执行器未在线,未创建任务;请检查后台同步配置"})
|
|
return
|
|
}
|
|
writeOrgError(w, err)
|
|
return
|
|
}
|
|
operation := "created"
|
|
if replayed {
|
|
operation = "replayed"
|
|
}
|
|
writeJSON(w, 202, map[string]any{"data": receipt, "operation": operation})
|
|
}
|