服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
537 lines
17 KiB
Go
537 lines
17 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
|
||
ann "pvfine/internal/annotations"
|
||
"pvfine/internal/mcpserver"
|
||
"pvfine/internal/ruleconfig"
|
||
)
|
||
|
||
type annotationArgs struct {
|
||
Query string `json:"query"`
|
||
FilePath string `json:"filePath"`
|
||
PathKind string `json:"pathKind"`
|
||
Extensions []string `json:"extensions"`
|
||
Section string `json:"section"`
|
||
Kinds []string `json:"kinds"`
|
||
TargetKinds []string `json:"targetKinds"`
|
||
AnnotationTypes []string `json:"annotationTypes"`
|
||
Relation string `json:"relation"`
|
||
PVFVersion string `json:"pvfVersion"`
|
||
WorkspaceID string `json:"workspaceId"`
|
||
Status string `json:"status"`
|
||
Cursor string `json:"cursor"`
|
||
Limit int `json:"limit"`
|
||
DocumentID string `json:"documentId"`
|
||
RulesRevision string `json:"rulesRevision"`
|
||
Value *string `json:"value"`
|
||
ValuesCursor string `json:"valuesCursor"`
|
||
ValuesLimit int `json:"valuesLimit"`
|
||
ExpectedRevision uint64 `json:"expectedRevision"`
|
||
Occurrence *int `json:"occurrence"`
|
||
TokenIndex *int `json:"tokenIndex"`
|
||
StartLine *int `json:"startLine"`
|
||
EndLine *int `json:"endLine"`
|
||
}
|
||
|
||
func (s *MCPService) annotationTools() []mcpserver.Tool {
|
||
props := map[string]any{}
|
||
for _, k := range strings.Fields("query filePath pathKind section relation pvfVersion workspaceId status cursor documentId rulesRevision value valuesCursor") {
|
||
props[k] = map[string]any{"type": "string", "maxLength": 4096}
|
||
}
|
||
for _, k := range strings.Fields("extensions kinds targetKinds annotationTypes") {
|
||
props[k] = map[string]any{"type": "array", "maxItems": 100, "items": map[string]any{"type": "string", "maxLength": 256}}
|
||
}
|
||
for _, k := range strings.Fields("expectedRevision occurrence tokenIndex startLine endLine") {
|
||
min := 0
|
||
if k == "startLine" || k == "endLine" {
|
||
min = 1
|
||
}
|
||
props[k] = map[string]any{"type": "integer", "minimum": min}
|
||
}
|
||
for _, k := range []string{"limit", "valuesLimit"} {
|
||
props[k] = map[string]any{"type": "integer", "minimum": 1, "maximum": 100}
|
||
}
|
||
for k, values := range map[string][]string{"pathKind": {"file", "directory"}, "status": {"effective", "all"}} {
|
||
props[k] = map[string]any{"type": "string", "enum": values}
|
||
}
|
||
result := []mcpserver.Tool{}
|
||
for _, d := range []struct{ name, description, fields, required string }{
|
||
{"search", "搜索已激活的 PVF 标注规则与共享字段说明;section 留空检索全部类别。版本默认 current(需 workspaceId),all 可离线浏览。返回文档身份与分页摘要,不扫描归档。", "query filePath pathKind extensions section kinds targetKinds annotationTypes relation pvfVersion workspaceId status cursor limit", ""},
|
||
{"get", "读取标注文档完整布局、整块继承后的定义、分页枚举和关联定义。需搜索返回的 documentId/rulesRevision;value 按字符串精确查找。", "documentId rulesRevision value valuesCursor valuesLimit", "documentId rulesRevision"},
|
||
{"explain", "解释当前工作区已提交脚本的实际标注。位置为 UTF-16;occurrence/tokenIndex 从 0 开始。与 read_file 同源,不包含前端草稿,不冻结界面。", "workspaceId expectedRevision filePath rulesRevision section occurrence tokenIndex startLine endLine cursor limit", "workspaceId expectedRevision filePath"},
|
||
} {
|
||
d := d
|
||
properties := map[string]any{}
|
||
for _, k := range strings.Fields(d.fields) {
|
||
properties[k] = props[k]
|
||
}
|
||
result = append(result, mcpserver.Tool{Name: "pvf_annotation_" + d.name, Description: d.description, ReadOnly: true, Schema: map[string]any{"type": "object", "properties": properties, "required": strings.Fields(d.required), "additionalProperties": false}, Call: func(ctx context.Context, raw json.RawMessage) (any, error) {
|
||
var a annotationArgs
|
||
if err := json.Unmarshal(raw, &a); err != nil {
|
||
return nil, fmt.Errorf("INVALID_ARGUMENT")
|
||
}
|
||
s.c.mu.Lock()
|
||
defer s.c.mu.Unlock()
|
||
switch d.name {
|
||
case "search":
|
||
return s.annotationSearch(a)
|
||
case "get":
|
||
return s.annotationGet(a)
|
||
default:
|
||
return s.annotationExplain(a)
|
||
}
|
||
}})
|
||
}
|
||
return result
|
||
}
|
||
|
||
type annotationDocument struct {
|
||
Entry ruleconfig.Entry
|
||
Rule ann.Rule
|
||
Original any
|
||
Preview *ann.PreviewSpec
|
||
DocumentID string
|
||
ResolutionError string
|
||
}
|
||
|
||
type annotationCatalog struct {
|
||
Revision string
|
||
Document ann.Document
|
||
Items []annotationDocument
|
||
}
|
||
|
||
func annotationHash(v any) string {
|
||
b, _ := json.Marshal(v)
|
||
h := sha256.Sum256(b)
|
||
return hex.EncodeToString(h[:])
|
||
}
|
||
func (c *core) annotationCatalogLocked() (annotationCatalog, error) {
|
||
if c.annotationErr != nil {
|
||
return annotationCatalog{}, c.annotationErr
|
||
}
|
||
if c.annotationEngine == nil {
|
||
return annotationCatalog{}, fmt.Errorf("RULES_UNAVAILABLE")
|
||
}
|
||
if c.annotationDocumentation != nil && c.annotationDocumentationEngine == c.annotationEngine && c.annotationDocumentationSnapshot == c.annotationSnapshot {
|
||
return *c.annotationDocumentation, nil
|
||
}
|
||
doc := c.annotationEngine.Document()
|
||
entries := []ruleconfig.Entry{}
|
||
revision := annotationHash(doc)
|
||
if c.annotationSnapshot != nil {
|
||
revision = c.annotationSnapshot.Revision
|
||
entries = c.annotationSnapshot.Entries
|
||
}
|
||
if len(entries) == 0 {
|
||
for _, r := range doc.Rules {
|
||
b, _ := json.Marshal(r)
|
||
entries = append(entries, ruleconfig.Entry{ID: r.ID, Kind: "rule", JSON: string(b), Source: "unknown", Effective: true})
|
||
}
|
||
for _, f := range doc.Fields {
|
||
b, _ := json.Marshal(f)
|
||
entries = append(entries, ruleconfig.Entry{ID: f.ID, Kind: "field", JSON: string(b), Source: "unknown", Effective: true})
|
||
}
|
||
}
|
||
out := annotationCatalog{Revision: revision, Document: doc, Items: []annotationDocument{}}
|
||
for i, e := range entries {
|
||
d := annotationDocument{Entry: e, DocumentID: annotationHash([]any{revision, e.Kind, e.ID, e.Source, i})}
|
||
if e.Kind == "field" {
|
||
var f ann.FieldDefinition
|
||
if json.Unmarshal([]byte(e.JSON), &f) != nil {
|
||
continue
|
||
}
|
||
d.Original = f
|
||
d.Preview = f.Preview
|
||
d.Rule = ann.Rule{ID: f.ID, PVFVersions: f.PVFVersions, Match: f.Match, Target: f.Target, Annotation: f.Annotation}
|
||
} else if e.Kind == "rule" {
|
||
var r ann.Rule
|
||
if json.Unmarshal([]byte(e.JSON), &r) != nil {
|
||
continue
|
||
}
|
||
d.Original = r
|
||
d.Rule = r
|
||
if resolved, err := ann.ResolveDocumentationRule(r, doc.Fields); err == nil {
|
||
d.Rule = resolved
|
||
} else {
|
||
d.ResolutionError = err.Error()
|
||
}
|
||
for _, f := range doc.Fields {
|
||
if f.ID == r.Field {
|
||
d.Preview = f.Preview
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
continue
|
||
}
|
||
out.Items = append(out.Items, d)
|
||
}
|
||
c.annotationDocumentation = &out
|
||
c.annotationDocumentationEngine = c.annotationEngine
|
||
c.annotationDocumentationSnapshot = c.annotationSnapshot
|
||
return out, nil
|
||
}
|
||
|
||
// Read-only cursors bind all filters and the activated snapshots; no filesystem state is read.
|
||
type annotationCursor struct {
|
||
Revision string `json:"r"`
|
||
Binding string `json:"b"`
|
||
Offset int `json:"o"`
|
||
Workspace string `json:"w,omitempty"`
|
||
Text string `json:"t,omitempty"`
|
||
}
|
||
|
||
func annotationPage(cursor, revision, binding, workspace, text string, n, limit int) (int, int, error) {
|
||
start := 0
|
||
if cursor != "" {
|
||
b, err := base64.RawURLEncoding.DecodeString(cursor)
|
||
var c annotationCursor
|
||
if err != nil || json.Unmarshal(b, &c) != nil {
|
||
return 0, 0, fmt.Errorf("INVALID_CURSOR")
|
||
}
|
||
if c.Revision != revision {
|
||
return 0, 0, fmt.Errorf("STALE_RULES")
|
||
}
|
||
if c.Workspace != workspace {
|
||
return 0, 0, fmt.Errorf("STALE_WORKSPACE")
|
||
}
|
||
if c.Text != text {
|
||
return 0, 0, fmt.Errorf("STALE_TEXT")
|
||
}
|
||
if c.Binding != binding || c.Offset < 0 || c.Offset > n {
|
||
return 0, 0, fmt.Errorf("INVALID_CURSOR")
|
||
}
|
||
start = c.Offset
|
||
}
|
||
return start, min(start+limit, n), nil
|
||
}
|
||
func annotationNext(end, n int, revision, binding, workspace, text string) any {
|
||
if end >= n {
|
||
return nil
|
||
}
|
||
b, _ := json.Marshal(annotationCursor{revision, binding, end, workspace, text})
|
||
return base64.RawURLEncoding.EncodeToString(b)
|
||
}
|
||
func annotationSection(s string) string {
|
||
return strings.ToLower(strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(s), "["), "]")))
|
||
}
|
||
func annotationIn(xs []string, s string) bool {
|
||
if len(xs) == 0 {
|
||
return true
|
||
}
|
||
for _, x := range xs {
|
||
if strings.EqualFold(x, s) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
func annotationSource(source string) string {
|
||
switch source {
|
||
case "annotations.user.json", "user":
|
||
return "user"
|
||
case "annotations.json", "builtin":
|
||
return "builtin"
|
||
case "paths.json", "sections.json", "generated":
|
||
return "generated"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
func annotationTerms(values []string) []string {
|
||
unique := map[string]bool{}
|
||
out := []string{}
|
||
for _, v := range values {
|
||
v = strings.ToLower(strings.TrimSpace(v))
|
||
if !unique[v] {
|
||
unique[v] = true
|
||
out = append(out, v)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
func annotationShort(s string) (string, bool) {
|
||
r := []rune(s)
|
||
if len(r) > 512 {
|
||
return string(r[:512]), true
|
||
}
|
||
return s, false
|
||
}
|
||
func annotationVersion(d annotationDocument, doc ann.Document, v string) bool {
|
||
if v == "all" {
|
||
return true
|
||
}
|
||
if !annotationIn(d.Rule.PVFVersions, v) {
|
||
return false
|
||
}
|
||
if d.Rule.Field != "" {
|
||
for _, f := range doc.Fields {
|
||
if f.ID == d.Rule.Field {
|
||
return annotationIn(f.PVFVersions, v)
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
func (s *MCPService) annotationSearch(a annotationArgs) (any, error) {
|
||
cat, err := s.c.annotationCatalogLocked()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if a.PVFVersion == "" {
|
||
a.PVFVersion = "current"
|
||
}
|
||
if a.Status == "" {
|
||
a.Status = "effective"
|
||
}
|
||
if a.PathKind == "" {
|
||
a.PathKind = "file"
|
||
}
|
||
if a.Limit == 0 {
|
||
a.Limit = 20
|
||
}
|
||
if a.PVFVersion == "current" {
|
||
if s.c.archive == nil {
|
||
return nil, fmt.Errorf("NO_ARCHIVE: 请指定 pvfVersion 或 all")
|
||
}
|
||
if a.WorkspaceID == "" || a.WorkspaceID != s.c.mcpWorkspaceID {
|
||
return nil, fmt.Errorf("STALE_WORKSPACE")
|
||
}
|
||
a.PVFVersion = s.c.archive.ClientVersion()
|
||
} else if a.PVFVersion != "all" && !ann.ValidPVFVersion(a.PVFVersion) {
|
||
return nil, fmt.Errorf("INVALID_VERSION: 支持 90US,90CN,110US,115US,115CN,100CN,TW")
|
||
}
|
||
cursor := a.Cursor
|
||
a.Cursor = ""
|
||
a.Limit = max(1, min(a.Limit, 100))
|
||
a.Section = annotationSection(a.Section)
|
||
for _, p := range []*[]string{&a.Extensions, &a.Kinds, &a.TargetKinds, &a.AnnotationTypes} {
|
||
*p = annotationTerms(*p)
|
||
}
|
||
a.Query = strings.Join(annotationTerms(strings.Fields(a.Query)), " ")
|
||
a.FilePath = strings.ToLower(strings.Trim(strings.ReplaceAll(a.FilePath, "\\", "/"), "/"))
|
||
binding := annotationHash(a)
|
||
type hit struct {
|
||
item map[string]any
|
||
score int
|
||
key string
|
||
}
|
||
hits := []hit{}
|
||
terms := strings.Fields(strings.ToLower(a.Query))
|
||
for _, d := range cat.Items {
|
||
r := d.Rule
|
||
effective := d.Entry.Effective && annotationVersion(d, cat.Document, a.PVFVersion)
|
||
if a.Status != "all" && !effective {
|
||
continue
|
||
}
|
||
if !annotationIn(a.Kinds, d.Entry.Kind) || !annotationIn(a.TargetKinds, r.Target.Kind) || !annotationIn(a.AnnotationTypes, r.Annotation.Type) {
|
||
continue
|
||
}
|
||
if a.Section != "" && a.Section != annotationSection(r.Target.Section) {
|
||
continue
|
||
}
|
||
if a.Relation != "" && r.Annotation.Relation != a.Relation {
|
||
continue
|
||
}
|
||
applicability := "unknown"
|
||
reasons := []string{}
|
||
if a.FilePath != "" {
|
||
if !ann.DocumentationMatches(r.Match, a.FilePath, a.PathKind == "directory") {
|
||
continue
|
||
}
|
||
applicability = "matched"
|
||
reasons = append(reasons, "filePath")
|
||
}
|
||
if a.PathKind == "directory" && r.Target.Kind != "path" {
|
||
continue
|
||
}
|
||
if len(a.Extensions) > 0 && len(r.Match.Extensions) > 0 {
|
||
matched := false
|
||
for _, ext := range a.Extensions {
|
||
if annotationIn(r.Match.Extensions, ext) {
|
||
matched = true
|
||
}
|
||
}
|
||
if !matched {
|
||
continue
|
||
}
|
||
}
|
||
if a.PVFVersion == "all" || !effective {
|
||
applicability = "unknown"
|
||
}
|
||
raw, _ := json.Marshal([]any{r.ID, r.Description, r.Field, r.Target.Section, r.Annotation.Relation, r.Annotation})
|
||
hay := strings.ToLower(string(raw))
|
||
matched := true
|
||
score := 0
|
||
for _, term := range terms {
|
||
if !strings.Contains(hay, term) {
|
||
matched = false
|
||
break
|
||
}
|
||
score++
|
||
for _, v := range []string{r.ID, r.Target.Section, r.Annotation.Title} {
|
||
if strings.EqualFold(v, term) {
|
||
score += 10
|
||
}
|
||
}
|
||
}
|
||
if !matched {
|
||
continue
|
||
}
|
||
summary, truncated := annotationShort(strings.TrimSpace(r.Description + "\n" + r.Annotation.Content))
|
||
why := []string{}
|
||
if d.Entry.Disabled {
|
||
why = append(why, "disabled")
|
||
}
|
||
if d.Entry.Overridden {
|
||
why = append(why, "overridden")
|
||
}
|
||
if !annotationVersion(d, cat.Document, a.PVFVersion) {
|
||
why = append(why, "version_mismatch")
|
||
}
|
||
if len(terms) > 0 {
|
||
reasons = append(reasons, "query")
|
||
}
|
||
if a.Section != "" {
|
||
reasons = append(reasons, "section")
|
||
}
|
||
values := []map[string]string{}
|
||
keys := []string{}
|
||
for k := range r.Annotation.Values {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
if len(terms) > 0 {
|
||
for _, k := range keys {
|
||
label := r.Annotation.Values[k]
|
||
for _, term := range terms {
|
||
if strings.Contains(strings.ToLower(k+" "+label), term) {
|
||
short, _ := annotationShort(label)
|
||
values = append(values, map[string]string{"value": k, "label": short})
|
||
break
|
||
}
|
||
}
|
||
if len(values) >= 5 {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
item := map[string]any{"documentId": d.DocumentID, "kind": d.Entry.Kind, "ruleId": r.ID, "fieldId": r.Field, "title": r.Annotation.Title, "summary": summary, "summaryTruncated": truncated, "section": r.Target.Section, "targetKind": r.Target.Kind, "annotationType": r.Annotation.Type, "pvfVersions": r.PVFVersions, "effective": effective, "applicability": applicability, "matchReasons": reasons, "inactiveReasons": why, "source": annotationSource(d.Entry.Source), "valueCount": len(r.Annotation.Values), "matchedValues": values, "relation": r.Annotation.Relation}
|
||
hits = append(hits, hit{item, score, d.Entry.Kind + "\x00" + r.ID + "\x00" + d.Entry.Source + d.DocumentID})
|
||
}
|
||
sort.SliceStable(hits, func(i, j int) bool {
|
||
if hits[i].score != hits[j].score {
|
||
return hits[i].score > hits[j].score
|
||
}
|
||
return hits[i].key < hits[j].key
|
||
})
|
||
start, end, err := annotationPage(cursor, cat.Revision, binding, a.WorkspaceID, "", len(hits), a.Limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
items := []map[string]any{}
|
||
for _, h := range hits[start:end] {
|
||
items = append(items, h.item)
|
||
}
|
||
return map[string]any{"schemaVersion": 1, "rulesRevision": cat.Revision, "context": map[string]any{"workspaceId": a.WorkspaceID, "pvfVersion": a.PVFVersion, "filePath": a.FilePath}, "items": items, "totalMatches": len(hits), "nextCursor": annotationNext(end, len(hits), cat.Revision, binding, a.WorkspaceID, "")}, nil
|
||
}
|
||
|
||
func (s *MCPService) annotationGet(a annotationArgs) (any, error) {
|
||
cat, err := s.c.annotationCatalogLocked()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if a.RulesRevision != cat.Revision {
|
||
return nil, fmt.Errorf("STALE_RULES")
|
||
}
|
||
var selected *annotationDocument
|
||
for i := range cat.Items {
|
||
if cat.Items[i].DocumentID == a.DocumentID {
|
||
selected = &cat.Items[i]
|
||
break
|
||
}
|
||
}
|
||
if selected == nil {
|
||
return nil, fmt.Errorf("DOCUMENT_NOT_FOUND")
|
||
}
|
||
d := *selected
|
||
r := d.Rule
|
||
if a.ValuesLimit == 0 {
|
||
a.ValuesLimit = 50
|
||
}
|
||
a.ValuesLimit = max(1, min(a.ValuesLimit, 100))
|
||
cursor := a.ValuesCursor
|
||
a.ValuesCursor = ""
|
||
binding := annotationHash(a)
|
||
keys := []string{}
|
||
for k := range r.Annotation.Values {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
start, end, err := annotationPage(cursor, cat.Revision, binding, "", "", len(keys), a.ValuesLimit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
values := []map[string]string{}
|
||
for _, k := range keys[start:end] {
|
||
values = append(values, map[string]string{"value": k, "label": r.Annotation.Values[k]})
|
||
}
|
||
// Enum maps are paged separately, never duplicated inside raw definitions.
|
||
raw, _ := json.Marshal(d.Original)
|
||
var original map[string]any
|
||
_ = json.Unmarshal(raw, &original)
|
||
if annotation, ok := original["annotation"].(map[string]any); ok {
|
||
delete(annotation, "values")
|
||
}
|
||
resolved := r
|
||
resolved.Annotation.Values = nil
|
||
relations := map[string]ann.RelationSpec{}
|
||
warnings := []string{}
|
||
if d.ResolutionError != "" {
|
||
warnings = append(warnings, d.ResolutionError)
|
||
}
|
||
visiting := map[string]bool{}
|
||
var visit func(string)
|
||
visit = func(name string) {
|
||
if name == "" {
|
||
return
|
||
}
|
||
if visiting[name] {
|
||
warnings = append(warnings, "relation_cycle:"+name)
|
||
return
|
||
}
|
||
if _, ok := relations[name]; ok {
|
||
return
|
||
}
|
||
spec, ok := cat.Document.Relations[name]
|
||
if !ok {
|
||
warnings = append(warnings, "missing_relation:"+name)
|
||
return
|
||
}
|
||
visiting[name] = true
|
||
relations[name] = spec
|
||
for _, child := range spec.Relations {
|
||
visit(child)
|
||
}
|
||
visiting[name] = false
|
||
}
|
||
visit(r.Annotation.Relation)
|
||
result := map[string]any{"schemaVersion": 1, "rulesRevision": cat.Revision, "documentId": d.DocumentID, "kind": d.Entry.Kind, "ruleId": r.ID, "source": annotationSource(d.Entry.Source), "effective": d.Entry.Effective, "disabled": d.Entry.Disabled, "overridden": d.Entry.Overridden, "originalDefinition": original, "resolvedDefinition": resolved, "preview": d.Preview, "valueCount": len(keys), "values": values, "definitionsOmitValues": true, "nextValuesCursor": annotationNext(end, len(keys), cat.Revision, binding, "", ""), "relations": relations, "warnings": warnings}
|
||
if a.Value != nil {
|
||
label, found := r.Annotation.Values[*a.Value]
|
||
result["value"] = map[string]any{"value": *a.Value, "found": found, "label": label}
|
||
}
|
||
return result, nil
|
||
}
|