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

96 lines
3.1 KiB
Go

package service
import (
"crypto/sha256"
"fmt"
"sort"
"strings"
"unicode/utf8"
"github.com/Tencent/WeKnora/internal/types"
)
// Normalize only fresh proposals before citation routing. Existing references
// and published pages must continue to use the strict identity validators.
func normalizeWikiFreshCandidates(input combinedExtraction) (combinedExtraction, error) {
if err := validateWikiCandidates(input); err != nil {
return combinedExtraction{}, err
}
items := make(map[string]extractedItem)
kinds := make(map[string]string)
reserved := make(map[string]bool)
preferText := func(left, right string) string {
if utf8.RuneCountInString(right) > utf8.RuneCountInString(left) ||
(utf8.RuneCountInString(right) == utf8.RuneCountInString(left) && right < left) {
return right
}
return left
}
for _, group := range []struct {
kind string
items []extractedItem
}{{types.WikiPageTypeEntity, input.Entities}, {types.WikiPageTypeConcept, input.Concepts}} {
for i, original := range group.items {
if len(original.Aliases) != 0 {
return combinedExtraction{}, wikiIdentityFailure("wiki_identity_alias_unsupported", group.kind, i, nil)
}
item := original
item.Name = strings.TrimSpace(item.Name)
item.Aliases = []string{}
item.SourceChunks = append([]string(nil), item.SourceChunks...)
identity := wikiSourceIdentityKey(group.kind, item.Name)
reserved[item.Slug] = true
if previous, exists := items[identity]; exists {
if previous.Slug < item.Slug {
item.Slug = previous.Slug
}
item.Description = preferText(previous.Description, item.Description)
item.Details = preferText(previous.Details, item.Details)
item.SourceChunks = append(item.SourceChunks, previous.SourceChunks...)
}
sort.Strings(item.SourceChunks)
unique := item.SourceChunks[:0]
for _, chunk := range item.SourceChunks {
if len(unique) == 0 || unique[len(unique)-1] != chunk {
unique = append(unique, chunk)
}
}
item.SourceChunks = unique
items[identity], kinds[identity] = item, group.kind
}
}
identities := make([]string, 0, len(items))
slugCount := make(map[string]int)
for identity, item := range items {
identities = append(identities, identity)
slugCount[item.Slug]++
}
sort.Strings(identities)
result := combinedExtraction{Entities: []extractedItem{}, Concepts: []extractedItem{}}
for _, identity := range identities {
item := items[identity]
if slugCount[item.Slug] > 1 {
base := item.Slug
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(identity)))
for length := 12; ; length += 4 {
candidate := ""
if length <= len(digest) {
candidate = base + "-" + digest[:length]
} else {
candidate = fmt.Sprintf("%s-%s-%d", base, digest, (length-len(digest))/4)
}
if !reserved[candidate] {
item.Slug, reserved[candidate] = candidate, true
break
}
}
}
if kinds[identity] == types.WikiPageTypeEntity {
result.Entities = append(result.Entities, item)
} else {
result.Concepts = append(result.Concepts, item)
}
}
return result, validateWikiIdentityCollisions(result)
}