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
96 lines
3.1 KiB
Go
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)
|
|
}
|