Files
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

233 lines
9.0 KiB
Go

package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"unicode/utf8"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
)
const wikiFailureCaptureEnv = "WEKNORA_WIKI_FAILURE_CAPTURE"
const wikiFailureCaptureVersion = "wiki-identity-failure-v1"
type wikiFailureCaptureScope struct {
Directory string `json:"directory"`
TenantID uint64 `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
KnowledgeID string `json:"knowledge_id"`
PendingOpID int64 `json:"pending_op_id"`
Attempt int `json:"attempt"`
ModelID string `json:"model_id"`
Phase string `json:"phase"`
SourceHash string `json:"source_hash"`
Release string `json:"release"`
APIImage string `json:"api_image"`
ExpiresAt time.Time `json:"expires_at"`
}
type wikiIdentityFailureArtifact struct {
Version string `json:"version"`
CapturedAt time.Time `json:"captured_at"`
Status string `json:"status"`
TenantID uint64 `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
KnowledgeID string `json:"knowledge_id"`
PendingOpID int64 `json:"pending_op_id"`
Attempt int `json:"attempt"`
ModelID string `json:"model_id"`
Phase string `json:"phase"`
Release string `json:"release"`
APIImage string `json:"api_image"`
Validation *wikiSourceIdentityError `json:"validation"`
TemplateHash string `json:"template_sha256"`
InputHash string `json:"template_input_hash"`
RepairHash string `json:"repair_instruction_sha256"`
SourceHash string `json:"source_sha256"`
ResponseHash string `json:"response_sha256"`
SourceBytes int `json:"source_bytes"`
ResponseBytes int `json:"response_bytes"`
Source string `json:"source,omitempty"`
Response string `json:"response,omitempty"`
}
func wikiFailureTextHash(text string) string {
sum := sha256.Sum256([]byte(text))
return hex.EncodeToString(sum[:])
}
// Recovery diagnostics are opt-in for one exact operation and source leaf.
// They never enter checkpoints, exports or public errors, and cannot retry work.
func captureWikiIdentityFailure(ctx context.Context, modelID, template string, input map[string]string, repair, raw string, original error) {
outcome := retainWikiIdentityFailure(ctx, modelID, template, input, repair, raw, original)
if outcome != "" {
logger.Errorf(ctx, "wiki identity failure diagnostic: outcome=%s", outcome)
}
}
func retainWikiIdentityFailure(ctx context.Context, modelID, template string, input map[string]string, repair, raw string, original error) string {
setting := os.Getenv(wikiFailureCaptureEnv)
if setting == "" {
return ""
}
var scope wikiFailureCaptureScope
decoder := json.NewDecoder(strings.NewReader(setting))
decoder.DisallowUnknownFields()
if len(setting) > 4096 || decoder.Decode(&scope) != nil || decoder.Decode(new(any)) != io.EOF ||
scope.Directory == "" || !filepath.IsAbs(scope.Directory) || scope.TenantID == 0 ||
scope.KnowledgeBaseID == "" || scope.KnowledgeID == "" || scope.PendingOpID <= 0 || scope.Attempt <= 0 ||
scope.ModelID == "" || scope.Phase != "wiki_candidate_slug" || len(scope.SourceHash) != 64 ||
scope.Release == "" || scope.APIImage == "" || !time.Now().Before(scope.ExpiresAt) {
return "invalid_configuration"
}
guard := wikiRecoveryFromContext(ctx)
tenant, ok := types.TenantIDFromContext(ctx)
var validation *wikiSourceIdentityError
if guard == nil || !ok || !errors.As(original, &validation) {
return ""
}
row := guard.row
if tenant != scope.TenantID || row.TenantID != tenant || row.KnowledgeBaseID != scope.KnowledgeBaseID ||
row.KnowledgeID != scope.KnowledgeID || row.PendingOpID != scope.PendingOpID || row.AttemptNumber != scope.Attempt ||
modelID != scope.ModelID || row.ModelID != modelID || row.ReviewedRelease != scope.Release ||
row.ReviewedAPIImage != scope.APIImage || wikiPromptPurpose(template) != scope.Phase ||
validation.SourceHash != scope.SourceHash || wikiCheckpointHash([]string{input["Content"]}) != scope.SourceHash {
return ""
}
artifact := wikiIdentityFailureArtifact{
Version: wikiFailureCaptureVersion, CapturedAt: time.Now().UTC(), Status: "complete",
TenantID: tenant, KnowledgeBaseID: row.KnowledgeBaseID, KnowledgeID: row.KnowledgeID,
PendingOpID: row.PendingOpID, Attempt: row.AttemptNumber, ModelID: modelID, Phase: scope.Phase,
Release: row.ReviewedRelease, APIImage: row.ReviewedAPIImage, Validation: validation,
TemplateHash: wikiFailureTextHash(template), InputHash: wikiCheckpointHash(input), RepairHash: wikiFailureTextHash(repair),
SourceHash: wikiFailureTextHash(input["Content"]), ResponseHash: wikiFailureTextHash(raw),
SourceBytes: len(input["Content"]), ResponseBytes: len(raw),
}
if !utf8.ValidString(input["Content"]) || !utf8.ValidString(raw) {
artifact.Status = "invalid_utf8"
} else if artifact.SourceBytes <= 256*1024 && artifact.ResponseBytes <= 1024*1024 {
artifact.Source, artifact.Response = input["Content"], raw
} else {
artifact.Status = "size_limit"
}
encoded, err := json.Marshal(artifact)
if err != nil {
return "encoding_failed"
}
if len(encoded) > 2*1024*1024 {
artifact.Source, artifact.Response, artifact.Status = "", "", "size_limit"
encoded, _ = json.Marshal(artifact)
}
return writeWikiFailureArtifact(scope, encoded, artifact.Status)
}
func writeWikiFailureArtifact(scope wikiFailureCaptureScope, encoded []byte, status string) string {
return writeWikiFailureArtifactWithFile(scope, encoded, status, func(root *os.Root, name string) (wikiFailureFile, error) {
return root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
})
}
type wikiFailureFile interface {
io.Writer
Sync() error
Close() error
}
func writeWikiFailureArtifactWithFile(scope wikiFailureCaptureScope, encoded []byte, status string, open func(*os.Root, string) (wikiFailureFile, error)) string {
// The operator must provision a private, unserved mount. Resolve no symlinks;
// os.Root keeps all later operations inside that opened directory.
resolved, err := filepath.EvalSymlinks(scope.Directory)
if err != nil || resolved != filepath.Clean(scope.Directory) {
return "private_directory_unavailable"
}
if wikiFailureDirectoryIsServed(resolved) {
return "private_directory_is_served"
}
root, err := os.OpenRoot(resolved)
if err != nil {
return "private_directory_unavailable"
}
defer root.Close()
info, err := root.Stat(".")
if err != nil || !info.IsDir() || info.Mode().Perm()&0077 != 0 {
return "private_directory_permissions"
}
owner, ok := info.Sys().(*syscall.Stat_t)
if !ok || uint64(owner.Uid) != uint64(os.Geteuid()) {
return "private_directory_owner"
}
// Exclusive directory creation is a durable single-use claim across workers.
// A failed write leaves the claim consumed, with no automatic capture retry.
key := wikiCheckpointHash([]any{scope.TenantID, scope.KnowledgeBaseID, scope.KnowledgeID, scope.PendingOpID, scope.Attempt})
if err := root.Mkdir(key, 0700); err != nil {
if errors.Is(err, os.ErrExist) {
return "already_claimed"
}
return "claim_failed"
}
if !syncWikiFailureDirectory(root, ".") {
return "sync_failed"
}
part := filepath.Join(key, "failure.partial")
file, err := open(root, part)
if err != nil {
return "write_failed"
}
written, writeErr := file.Write(encoded)
syncErr := file.Sync()
closeErr := file.Close()
if written != len(encoded) || writeErr != nil || syncErr != nil || closeErr != nil {
_ = root.Remove(part)
return "write_failed"
}
if err := root.Rename(part, filepath.Join(key, "failure.json")); err != nil {
_ = root.Remove(part)
return "write_failed"
}
if !syncWikiFailureDirectory(root, key) {
return "sync_failed"
}
return status
}
func wikiFailureDirectoryIsServed(directory string) bool {
storage := strings.TrimSpace(os.Getenv("LOCAL_STORAGE_BASE_DIR"))
if storage == "" {
storage = "/data/files"
}
for _, base := range []string{storage, "/data/uploads", "/usr/share/nginx/html"} {
base, err := filepath.Abs(base)
if err != nil {
return true
}
if resolved, err := filepath.EvalSymlinks(base); err == nil {
base = resolved
}
relative, err := filepath.Rel(base, directory)
if err != nil || (relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))) {
return true
}
}
return false
}
func syncWikiFailureDirectory(root *os.Root, name string) bool {
directory, err := root.Open(name)
if err != nil {
return false
}
syncErr := directory.Sync()
closeErr := directory.Close()
return syncErr == nil && closeErr == nil
}