prts-terrarchive 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +100 -23
- package/README.md +82 -18
- package/bin/build-mainline-catalog.py +195 -0
- package/contracts/corpus_i18n_v1.schema.json +10 -12
- package/contracts/corpus_tools_v1.schema.json +30 -1
- package/docs/endfield-mainline-navigation.md +45 -0
- package/lib/client.js +157 -27
- package/lib/skins/common.css +12 -2
- package/lib/skins/endfield-aic.css +1 -1
- package/lib/skins/prts-agent.css +9 -7
- package/package.json +7 -3
- package/presets/definition.js +1 -1
- package/presets/prts/preset.yml +1 -1
- package/resources/endfield-mainline.json +4632 -0
- package/skills/prts-composition/SKILL.md +3 -1
- package/skills/prts-investigation/SKILL.md +19 -113
- package/skills/prts-investigation/references/board-guide.md +131 -0
- package/skills/prts-retrieval/SKILL.md +4 -2
- package/skills/prts-retrieval/references/cloud-tools.md +3 -1
- package/skills/prts-retrieval/references/local-tools.md +3 -3
- package/skills/prts-retrieval/references/module-arknights.md +1 -1
- package/skills/prts-retrieval/references/module-dual.md +1 -1
- package/skills/prts-retrieval/references/module-endfield.md +3 -0
- package/skills/prts-retrieval/references/tools-runtime.md +22 -10
- package/src/cloud-projection.js +23 -2
- package/src/cloud.js +24 -0
- package/src/entity-recognizer.js +26 -5
- package/src/entity-routing.js +18 -4
- package/src/evidence-state.js +4 -2
- package/src/i18n.js +79 -30
- package/src/index.js +346 -162
- package/src/installer.js +86 -14
- package/src/investigation-tools.js +40 -17
- package/src/navigation.js +342 -0
- package/src/read-args.js +86 -0
- package/src/read.js +66 -15
- package/src/search-projection.js +154 -19
- package/src/search.js +177 -21
- package/src/skill.js +25 -16
- package/src/skin-capabilities.js +28 -0
- package/src/store.js +127 -3
- package/src/timeline.js +86 -2
- package/src/tool-args.js +53 -0
- package/src/ui.js +158 -34
- package/src/web-guidance.js +14 -2
package/src/search-projection.js
CHANGED
|
@@ -18,10 +18,15 @@ const EVIDENCE_LABELS = Object.freeze({
|
|
|
18
18
|
derived_timeline: '整理性时间线', entity_projection: '实体关联入口', catalog: '资料目录',
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
+
// 命中高亮用 ⟦⟧:明日方舟档案正文自身大量使用【代号】【性别】,不能再用【】。
|
|
22
|
+
const HIT_OPEN = '⟦'
|
|
23
|
+
const HIT_CLOSE = '⟧'
|
|
24
|
+
|
|
21
25
|
function renderLine(line) {
|
|
22
26
|
const marker = line.role === 'match' ? '>' : line.role === 'constraint' ? '+' : ' '
|
|
23
27
|
const number = line.line == null ? '' : String(line.line).padStart(4, ' ')
|
|
24
|
-
|
|
28
|
+
// 与 corpus_read 的行格式一致:「说话人: 台词」。
|
|
29
|
+
const speaker = line.speaker ? `${line.speaker}: ` : ''
|
|
25
30
|
return `${marker} ${number} ${speaker}${line.rendered_text ?? line.text}${line.truncated ? '(本行已截断)' : ''}`.trimEnd()
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -29,18 +34,47 @@ function normalizeText(value) {
|
|
|
29
34
|
return String(value ?? '').normalize('NFKC').replace(/\s+/gu, ' ').trim()
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 在原文上高亮:匹配时按 NFKC + 小写比较(与检索一致),输出保留原文字符。
|
|
39
|
+
* 逐码点建立「归一化偏移 → 原文偏移」映射,避免全角标点被改写。
|
|
40
|
+
*/
|
|
41
|
+
function normalizedIndex(text) {
|
|
42
|
+
let normalized = ''
|
|
43
|
+
const starts = []
|
|
44
|
+
const ends = []
|
|
45
|
+
let offset = 0
|
|
46
|
+
let previousSpace = true
|
|
47
|
+
for (const char of text) {
|
|
48
|
+
let piece = char.normalize('NFKC').toLocaleLowerCase()
|
|
49
|
+
if (/^\s+$/u.test(piece)) {
|
|
50
|
+
piece = previousSpace ? '' : ' '
|
|
51
|
+
}
|
|
52
|
+
for (let index = 0; index < piece.length; index += 1) {
|
|
53
|
+
starts.push(offset)
|
|
54
|
+
ends.push(offset + char.length)
|
|
55
|
+
}
|
|
56
|
+
if (piece) previousSpace = piece === ' '
|
|
57
|
+
normalized += piece
|
|
58
|
+
offset += char.length
|
|
59
|
+
}
|
|
60
|
+
return { normalized, starts, ends }
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
function highlightLiteral(text, query) {
|
|
33
|
-
const needle = normalizeText(query)
|
|
64
|
+
const needle = normalizeText(query).toLocaleLowerCase()
|
|
34
65
|
if (!needle) return text
|
|
35
|
-
const
|
|
36
|
-
const loweredNeedle = needle.toLocaleLowerCase()
|
|
66
|
+
const { normalized, starts, ends } = normalizedIndex(text)
|
|
37
67
|
const parts = []
|
|
38
68
|
let cursor = 0
|
|
39
|
-
let offset =
|
|
69
|
+
let offset = normalized.indexOf(needle)
|
|
40
70
|
while (offset >= 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
const from = starts[offset]
|
|
72
|
+
const to = ends[offset + needle.length - 1]
|
|
73
|
+
if (from >= cursor) {
|
|
74
|
+
parts.push(text.slice(cursor, from), `${HIT_OPEN}${text.slice(from, to)}${HIT_CLOSE}`)
|
|
75
|
+
cursor = to
|
|
76
|
+
}
|
|
77
|
+
offset = normalized.indexOf(needle, offset + needle.length)
|
|
44
78
|
}
|
|
45
79
|
return parts.length ? `${parts.join('')}${text.slice(cursor)}` : text
|
|
46
80
|
}
|
|
@@ -48,26 +82,47 @@ function highlightLiteral(text, query) {
|
|
|
48
82
|
function highlightRegex(text, query) {
|
|
49
83
|
try {
|
|
50
84
|
const regex = new RegExp(query, 'giu')
|
|
51
|
-
|
|
85
|
+
// 检索按 NFKC 文本匹配;展示仍在原文上尽力高亮,匹配不上时保持原文不加标记。
|
|
86
|
+
return text.replace(regex, (matched) => matched ? `${HIT_OPEN}${matched}${HIT_CLOSE}` : matched)
|
|
52
87
|
} catch {
|
|
53
88
|
return text
|
|
54
89
|
}
|
|
55
90
|
}
|
|
56
91
|
|
|
92
|
+
function speakerlessQuery(line, query) {
|
|
93
|
+
const speaker = normalizeText(line.speaker)
|
|
94
|
+
const needle = normalizeText(query)
|
|
95
|
+
if (!speaker || !needle.toLocaleLowerCase().startsWith(`${speaker.toLocaleLowerCase()}:`)) return query
|
|
96
|
+
return needle.slice(speaker.length + 1).trim() || query
|
|
97
|
+
}
|
|
98
|
+
|
|
57
99
|
function renderedExcerptLine(line, match, options) {
|
|
58
100
|
if (line.role !== 'match' || !options.query) return line
|
|
59
101
|
const renderedText = match.match_kind === 'regex'
|
|
60
|
-
? highlightRegex(line.text, options.query)
|
|
102
|
+
? highlightRegex(line.text, options.query)
|
|
103
|
+
: highlightLiteral(line.text, speakerlessQuery(line, options.query))
|
|
61
104
|
return renderedText === line.text ? line : { ...line, rendered_text: renderedText }
|
|
62
105
|
}
|
|
63
106
|
|
|
64
107
|
function renderMatch(match, options) {
|
|
108
|
+
// 目录/元数据命中没有正文行:直说"为什么命中",别让调用方以为漏了内容。
|
|
109
|
+
if (match.line_start == null && !(match.excerpt || []).length) {
|
|
110
|
+
const locator = options.documentUid
|
|
111
|
+
? { document_uid: options.documentUid, mode: 'document' }
|
|
112
|
+
: { title: String(options.documentTitle || ''), mode: 'document' }
|
|
113
|
+
return [`命中方式:标题/元数据命中本篇(该篇没有正文命中行)`,
|
|
114
|
+
`读取:corpus_read(${JSON.stringify(locator)})`].join('\n')
|
|
115
|
+
}
|
|
65
116
|
const range = match.line_start == null ? ''
|
|
66
117
|
: `命中:第 ${match.line_start === match.line_end ? match.line_start
|
|
67
118
|
: `${match.line_start}-${match.line_end}`} 行`
|
|
119
|
+
// 句柄:检索结果直接给出可回读的凭据,Agent 不必先查 document_uid 再读。
|
|
120
|
+
const handle = match.citation_handle
|
|
121
|
+
? `句柄:${match.citation_handle} · 回到这几行:corpus_read({"citation":"${match.citation_handle}"})`
|
|
122
|
+
: ''
|
|
68
123
|
return [range, `证据:${EVIDENCE_LABELS[match.evidence_kind] || match.evidence_kind}`,
|
|
69
124
|
...(match.excerpt || []).map((line) => renderLine(renderedExcerptLine(line, match, options))),
|
|
70
|
-
`引用:${match.citation}
|
|
125
|
+
`引用:${match.citation}`, handle].filter(Boolean).join('\n')
|
|
71
126
|
}
|
|
72
127
|
|
|
73
128
|
function renderSection(section) {
|
|
@@ -84,14 +139,41 @@ function renderEntitySummary(summary) {
|
|
|
84
139
|
`引用:${summary.citation}`].filter(Boolean).join('\n')
|
|
85
140
|
}
|
|
86
141
|
|
|
142
|
+
const WIKI_TYPES = ['character_wiki', 'story_wiki', 'character_activity_wiki']
|
|
143
|
+
const WIKI_NOTICE = '引文状态:Wiki 为整理性资料;其中引号内容未核验为当前资料包官方原文,逐字引用前请回查原文。'
|
|
144
|
+
|
|
145
|
+
/** 引用资料(泰拉年表、活动时间线)里存的是原始 wikitext / JSON,展开进检索结果只会烧 token。 */
|
|
146
|
+
function isSerializedDataLine(line) {
|
|
147
|
+
const text = String(line.rendered_text ?? line.text ?? '')
|
|
148
|
+
if (text.length > 240) return true
|
|
149
|
+
if (/^\s*[{[]/u.test(text)) return true
|
|
150
|
+
return /"(?:schema_version|source_location|wikitext_line|raw_wikitext)"\s*:/u.test(text)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function serializedReferenceBody(document, matches) {
|
|
154
|
+
const lines = matches.flatMap((match) => match.excerpt || [])
|
|
155
|
+
if (!lines.length || !lines.some(isSerializedDataLine)) return null
|
|
156
|
+
if (lines.filter(isSerializedDataLine).length < Math.ceil(lines.length / 2)) return null
|
|
157
|
+
return ['本篇为引用资料,正文是原始 wikitext / JSON,不在检索结果里展开。',
|
|
158
|
+
'查事件年代与顺序请用 timeline_search;确需原文时按已知行号读取:',
|
|
159
|
+
`corpus_read(${JSON.stringify(document.document_uid
|
|
160
|
+
? { document_uid: document.document_uid, mode: 'document' }
|
|
161
|
+
: { title: document.title, mode: 'document' })})`].join('\n')
|
|
162
|
+
}
|
|
163
|
+
|
|
87
164
|
function renderDocument(document, options) {
|
|
165
|
+
const serialized = document.resource_type === 'reference'
|
|
166
|
+
? serializedReferenceBody(document, document.matches || []) : null
|
|
88
167
|
const body = document.entity_summary ? renderEntitySummary(document.entity_summary)
|
|
89
168
|
: document.section_content ? renderSection(document.section_content)
|
|
90
|
-
:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
169
|
+
: serialized ? serialized
|
|
170
|
+
: document.matches?.length ? document.matches.map((match) => renderMatch(match, {
|
|
171
|
+
...options, documentUid: document.document_uid, documentTitle: document.title })).join('\n\n')
|
|
172
|
+
: [document.available_sections?.length ? `可用字段:${document.available_sections.join('、')}` : '',
|
|
173
|
+
`读取:corpus_read(${JSON.stringify(document.document_uid
|
|
174
|
+
? { document_uid: document.document_uid, mode: 'document' }
|
|
175
|
+
: { title: document.title })})`].filter(Boolean).join('\n')
|
|
176
|
+
const wikiNotice = !options.sharedWikiNotice && WIKI_TYPES.includes(document.resource_type)
|
|
95
177
|
? '引文状态:Wiki 为整理性资料;其中引号内容未核验为当前资料包官方原文,逐字引用前请回查原文。'
|
|
96
178
|
: ''
|
|
97
179
|
const game = document.game === 'endfield' ? '终末地'
|
|
@@ -104,6 +186,46 @@ function renderDocument(document, options) {
|
|
|
104
186
|
.filter(Boolean).join('\n')
|
|
105
187
|
}
|
|
106
188
|
|
|
189
|
+
/** 零命中时按实际使用的过滤条件给出具体放宽方向。 */
|
|
190
|
+
export function zeroHitHints(args = {}) {
|
|
191
|
+
const has = (field) => Array.isArray(args?.[field])
|
|
192
|
+
&& args[field].some((item) => typeof item === 'string' && item.trim())
|
|
193
|
+
const hints = []
|
|
194
|
+
if (has('context_terms')) {
|
|
195
|
+
hints.push('context_terms 是强约束:每个词都必须出现在命中行前后 3 行内,不是整篇共现;请先去掉 context_terms 重试。')
|
|
196
|
+
}
|
|
197
|
+
if (has('story_names')) {
|
|
198
|
+
hints.push('story_names 只匹配单关/单篇剧情名(如“鳞的记忆”);活动或章节名(如“昨日海”)请改用 activity_names。')
|
|
199
|
+
}
|
|
200
|
+
if (has('activity_names') || has('collection_names')) {
|
|
201
|
+
hints.push('activity_names / collection_names 需与展示名完全一致;不确定时先省略 query 只按名称列目录核对。')
|
|
202
|
+
}
|
|
203
|
+
if (has('speakers')) {
|
|
204
|
+
hints.push('speakers 只匹配本人亲口台词(已自动展开已登记别名,如 银灰→恩希欧迪斯);提及该角色的旁白或他人台词请改用 entity_names 或 query。')
|
|
205
|
+
const query = normalizeText(args?.query)
|
|
206
|
+
if (query && args.speakers.some((name) => query.includes(normalizeText(name)))) {
|
|
207
|
+
hints.push('query 只匹配台词正文,不含说话人名;按说话人找台词时请省略 query,或只写台词片段。')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (has('resource_types') || has('content_types') || has('wiki_sections')) {
|
|
211
|
+
hints.push('resource_types / content_types / wiki_sections 会缩小资料类型;可先移除再看命中分布。')
|
|
212
|
+
}
|
|
213
|
+
if (args?.match_mode === 'regex') hints.push('regex 模式按受限正则匹配;可改回默认 literal 并缩短为关键短语。')
|
|
214
|
+
return hints
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 本页命中的游戏分布。跨游戏命中时提醒收窄,避免同名实体(如「博士」)互相污染。 */
|
|
218
|
+
function gameDistributionLine(byGame) {
|
|
219
|
+
const labels = { arknights: '明日方舟', endfield: '终末地' }
|
|
220
|
+
const parts = Object.entries(byGame || {})
|
|
221
|
+
.filter(([game, count]) => labels[game] && Number.isInteger(count) && count > 0)
|
|
222
|
+
.map(([game, count]) => `${labels[game]} ${count} 篇`)
|
|
223
|
+
if (!parts.length) return ''
|
|
224
|
+
const cross = Object.keys(byGame || {}).filter((game) => labels[game]).length > 1
|
|
225
|
+
return `命中分布(按游戏,本页):${parts.join(' · ')}`
|
|
226
|
+
+ (cross ? '。跨游戏命中,做人物向研究时请用 games 限定' : '')
|
|
227
|
+
}
|
|
228
|
+
|
|
107
229
|
export function projectSearch(value, options = {}) {
|
|
108
230
|
const documents = value?.documents || []
|
|
109
231
|
const matches = documents.reduce((total, document) => total
|
|
@@ -118,14 +240,21 @@ export function projectSearch(value, options = {}) {
|
|
|
118
240
|
? `扫描尚未穷尽。继续时保留本次搜索词和过滤条件,并设置 after: ${JSON.stringify(value.page.next_after)}。`
|
|
119
241
|
: value?.page?.next_cursor
|
|
120
242
|
? `旧版分页尚未结束。继续时仅提交 cursor: ${JSON.stringify(value.page.next_cursor)}。` : ''
|
|
243
|
+
const distribution = gameDistributionLine(value?.by_game)
|
|
244
|
+
const hints = zeroHitHints(options.args)
|
|
245
|
+
// 覆盖率:让"没有找到"带上检查范围,成为可引用的否定证据。
|
|
246
|
+
const scopeText = Number.isInteger(value?.scope?.documents)
|
|
247
|
+
? `本地 ${value.scope.documents} 篇 / ${Number(value.scope.lines || 0)} 行`
|
|
248
|
+
: ''
|
|
121
249
|
const zero = documents.length ? '' : exhausted
|
|
122
|
-
? '
|
|
250
|
+
? [`已检查完整检索范围${scopeText ? `(${scopeText})` : ''},没有找到。请检查展示名、缩短连续字面串或移除冲突过滤条件。`,
|
|
251
|
+
...hints.map((hint) => `- ${hint}`)].join('\n')
|
|
123
252
|
: value?.page?.next_after
|
|
124
253
|
? '本页没有发现命中文档,但扫描位置已经推进;这不是全库零命中。'
|
|
125
254
|
: '该旧版分页链已经结束,但不能证明资料范围已经穷尽;需要完整性时请重新搜索。'
|
|
126
255
|
const complete = exhausted && documents.length
|
|
127
256
|
? Number.isInteger(value.page.total_documents)
|
|
128
|
-
?
|
|
257
|
+
? `已检查完整检索范围${scopeText ? `(${scopeText})` : ''},共匹配 ${value.page.total_documents} 篇资料。`
|
|
129
258
|
: '已扫描至当前检索范围末尾。'
|
|
130
259
|
: ''
|
|
131
260
|
const warnings = (value?.warnings || []).length
|
|
@@ -134,6 +263,12 @@ export function projectSearch(value, options = {}) {
|
|
|
134
263
|
? `## 再旅者对应关系(人工审校附属字段)\n${value.retraveler_relations.map((item) =>
|
|
135
264
|
`- 终末地角色:${item.endfield_name};泰拉记忆原型:${item.terra_memory_prototype || '未登记'};` +
|
|
136
265
|
`状态:${item.relation_status}。这是跨游戏关系,不是人物别名。`).join('\n')}` : ''
|
|
137
|
-
|
|
266
|
+
// 多篇 Wiki 同屏时引文提示只说一次,避免每篇重复同一句话。
|
|
267
|
+
const wikiCount = documents.filter((document) => WIKI_TYPES.includes(document.resource_type)).length
|
|
268
|
+
const sharedWikiNotice = wikiCount > 1
|
|
269
|
+
const documentOptions = sharedWikiNotice ? { ...options, sharedWikiNotice } : options
|
|
270
|
+
return [heading, distribution, sharedWikiNotice ? `${WIKI_NOTICE}(适用于下列所有 Wiki 条目)` : '',
|
|
271
|
+
warnings, relations,
|
|
272
|
+
...documents.map((document) => renderDocument(document, documentOptions)), zero, complete, next]
|
|
138
273
|
.filter(Boolean).join('\n\n')
|
|
139
274
|
}
|
package/src/search.js
CHANGED
|
@@ -3,7 +3,8 @@ import { createHash, createHmac, timingSafeEqual } from 'node:crypto'
|
|
|
3
3
|
import { deflateRawSync, inflateRawSync } from 'node:zlib'
|
|
4
4
|
import { activityMatches, aliasesFor } from './timeline.js'
|
|
5
5
|
import { assertCorpusVersion, corpusVersionSnapshot,
|
|
6
|
-
DOCUMENT_ORDERING_VERSION, documentGame, documentUid, naturalDocumentTitle
|
|
6
|
+
DOCUMENT_ORDERING_VERSION, documentGame, documentUid, naturalDocumentTitle,
|
|
7
|
+
DATA_VERSION_REF_PATTERN, dataVersionMatches, shortDataVersion } from './store.js'
|
|
7
8
|
import { projectSearch } from './search-projection.js'
|
|
8
9
|
import { wikiActivityName, wikiActivityRanges, wikiCharacterName, wikiDocumentRole,
|
|
9
10
|
wikiSectionAt, wikiSectionRanges } from './wiki.js'
|
|
@@ -51,7 +52,14 @@ const SEARCH_POLICY_FINGERPRINT = createHash('sha256').update(JSON.stringify({
|
|
|
51
52
|
matchingPolicyVersion: MATCHING_POLICY_VERSION,
|
|
52
53
|
})).digest('base64url').slice(0, 16)
|
|
53
54
|
const FILTER_LIMIT = 16
|
|
54
|
-
|
|
55
|
+
// The high word identifies the installed pack set; the low word retains the
|
|
56
|
+
// candidate ordinal. Keep these bit assignments fixed across plugin releases.
|
|
57
|
+
const PAGE_SCOPE_PACKS = Object.freeze(['official_game', 'endfield_official_game',
|
|
58
|
+
'reviewed_wiki', 'terra_journey', 'endfield_reviewed_knowledge', 'entities', 'references'])
|
|
59
|
+
const PAGE_POSITION_STRIDE = 2 ** 32
|
|
60
|
+
const MAX_CANDIDATE_POSITION = 10_000_000
|
|
61
|
+
const MAX_PAGE_POSITION = (2 ** PAGE_SCOPE_PACKS.length) * PAGE_POSITION_STRIDE
|
|
62
|
+
+ MAX_CANDIDATE_POSITION
|
|
55
63
|
/** 过滤单项最长 512 码点:与上述项数上限共同保证压缩游标可被重新解码。 */
|
|
56
64
|
const FILTER_ITEM_LIMIT = 512
|
|
57
65
|
/**
|
|
@@ -231,14 +239,14 @@ function normalizedRequest(raw = {}) {
|
|
|
231
239
|
const resourceType = normalizeText(raw.after.resource_type)
|
|
232
240
|
const title = normalizeText(raw.after.title)
|
|
233
241
|
const position = raw.after.position
|
|
234
|
-
if (typeof dataVersion !== 'string' || !
|
|
242
|
+
if (typeof dataVersion !== 'string' || !DATA_VERSION_REF_PATTERN.test(dataVersion)
|
|
235
243
|
|| !resourceType || !title || !Number.isSafeInteger(position) || position < 0
|
|
236
|
-
|| position >
|
|
244
|
+
|| position > MAX_PAGE_POSITION || position % PAGE_POSITION_STRIDE > MAX_CANDIDATE_POSITION
|
|
237
245
|
|| Object.keys(raw.after).some((key) => ![
|
|
238
246
|
'data_version', 'resource_type', 'title', 'position',
|
|
239
247
|
].includes(key))) {
|
|
240
248
|
throw Object.assign(new Error(
|
|
241
|
-
'after 必须包含且只能包含
|
|
249
|
+
'after 必须包含且只能包含 data_version(续页给出的版本,至少 8 位)、resource_type、title 和续页给出的安全整数 position'),
|
|
242
250
|
{ code: 'INVALID_REQUEST' })
|
|
243
251
|
}
|
|
244
252
|
after = { data_version: dataVersion, resource_type: resourceType, title, position }
|
|
@@ -307,17 +315,46 @@ function withoutAfter(request) {
|
|
|
307
315
|
return searchRequest
|
|
308
316
|
}
|
|
309
317
|
|
|
318
|
+
function pageScopeMask(store) {
|
|
319
|
+
if (!store.scopeFingerprint) return null
|
|
320
|
+
if (!(store.packs instanceof Map)
|
|
321
|
+
|| [...store.packs.keys()].some((id) => !PAGE_SCOPE_PACKS.includes(id))) {
|
|
322
|
+
throw Object.assign(new Error('无法识别当前安装范围,请重新安装资料包'),
|
|
323
|
+
{ code: 'PAGE_ANCHOR_INVALID' })
|
|
324
|
+
}
|
|
325
|
+
return PAGE_SCOPE_PACKS.reduce((mask, id, index) =>
|
|
326
|
+
mask + (store.packs.has(id) ? 2 ** index : 0), 0)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function encodePagePosition(store, ordinal) {
|
|
330
|
+
const mask = pageScopeMask(store)
|
|
331
|
+
return mask === null ? ordinal : (mask + 1) * PAGE_POSITION_STRIDE + ordinal
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function decodePagePosition(store, position) {
|
|
335
|
+
const mask = pageScopeMask(store)
|
|
336
|
+
const encodedMask = Math.floor(position / PAGE_POSITION_STRIDE) - 1
|
|
337
|
+
const ordinal = position % PAGE_POSITION_STRIDE
|
|
338
|
+
if ((mask === null ? encodedMask !== -1 : encodedMask !== mask)
|
|
339
|
+
|| ordinal > MAX_CANDIDATE_POSITION) {
|
|
340
|
+
throw Object.assign(new Error('分页锚点绑定的安装范围已经变化,请重新搜索'),
|
|
341
|
+
{ code: 'PAGE_ANCHOR_MISMATCH' })
|
|
342
|
+
}
|
|
343
|
+
return ordinal
|
|
344
|
+
}
|
|
345
|
+
|
|
310
346
|
async function checkpointAfterTitle(store, after, request, { signal, deadline } = {}) {
|
|
311
347
|
assertSearchActive(signal, deadline, '分页锚点校验')
|
|
312
|
-
if (after.data_version
|
|
348
|
+
if (!dataVersionMatches(after.data_version, store.dataVersion)) {
|
|
313
349
|
throw Object.assign(new Error('分页锚点绑定到另一个资料版本,请重新搜索'),
|
|
314
350
|
{ code: 'PAGE_ANCHOR_VERSION_MISMATCH', retryable: false })
|
|
315
351
|
}
|
|
352
|
+
const position = decodePagePosition(store, after.position)
|
|
316
353
|
const regex = request.match_mode === 'regex'
|
|
317
354
|
? new RegExp(safeRegex(request.query).source, 'iu') : null
|
|
318
355
|
const candidates = await candidateDocumentIds(store, request, regex, { signal, deadline })
|
|
319
356
|
assertSearchActive(signal, deadline, '分页锚点校验')
|
|
320
|
-
const documentId = candidates[
|
|
357
|
+
const documentId = candidates[position]
|
|
321
358
|
const location = documentId ? store.documents.get(documentId) : null
|
|
322
359
|
if (!location) throw Object.assign(new Error('找不到分页锚点,请重新搜索'),
|
|
323
360
|
{ code: 'PAGE_ANCHOR_NOT_FOUND' })
|
|
@@ -326,7 +363,7 @@ async function checkpointAfterTitle(store, after, request, { signal, deadline }
|
|
|
326
363
|
throw Object.assign(new Error('分页锚点与当前资料版本不匹配,请重新搜索'),
|
|
327
364
|
{ code: 'PAGE_ANCHOR_MISMATCH' })
|
|
328
365
|
}
|
|
329
|
-
return { nextCandidateIndex:
|
|
366
|
+
return { nextCandidateIndex: position + 1, matchedDocumentsSoFar: 0,
|
|
330
367
|
matchedCountKnown: false }
|
|
331
368
|
}
|
|
332
369
|
|
|
@@ -352,8 +389,8 @@ async function exposeTitleContinuation(store, result, { signal, deadline } = {})
|
|
|
352
389
|
const document = documentId ? store.documents.get(documentId)?.document : null
|
|
353
390
|
if (!document) throw Object.assign(new Error('无法生成下一页的标题锚点'),
|
|
354
391
|
{ code: 'PAGE_ANCHOR_INVALID' })
|
|
355
|
-
nextAfter = { data_version: store.dataVersion, resource_type: publicResourceType(document),
|
|
356
|
-
title: naturalDocumentTitle(document), position: anchorOrdinal }
|
|
392
|
+
nextAfter = { data_version: shortDataVersion(store.dataVersion), resource_type: publicResourceType(document),
|
|
393
|
+
title: naturalDocumentTitle(document), position: encodePagePosition(store, anchorOrdinal) }
|
|
357
394
|
}
|
|
358
395
|
return { ...result, page: { ...page, next_after: nextAfter } }
|
|
359
396
|
}
|
|
@@ -401,7 +438,8 @@ function validCursorSignature(body, received, secret, bytes = 32) {
|
|
|
401
438
|
}
|
|
402
439
|
|
|
403
440
|
async function encodeOffsetCursor(store, request, offset) {
|
|
404
|
-
const payload = [3, cursorVersionTag(store.dataVersion), offset,
|
|
441
|
+
const payload = [3, cursorVersionTag(store.dataVersion), offset,
|
|
442
|
+
compactCursorRequest(request), store.scopeFingerprint ?? null]
|
|
405
443
|
const compressed = deflateRawSync(Buffer.from(JSON.stringify(payload)), { level: 9 })
|
|
406
444
|
const encoded = compressed.toString('base64url')
|
|
407
445
|
const body = `s3.${encoded}`
|
|
@@ -413,7 +451,7 @@ async function encodeOffsetCursor(store, request, offset) {
|
|
|
413
451
|
async function encodeScanCursor(store, request, nextCandidateIndex, matchedDocumentsSoFar) {
|
|
414
452
|
const payload = [4, cursorVersionTag(store.dataVersion), DOCUMENT_ORDERING_VERSION,
|
|
415
453
|
SEARCH_POLICY_FINGERPRINT, nextCandidateIndex, matchedDocumentsSoFar,
|
|
416
|
-
compactCursorRequest(request)]
|
|
454
|
+
compactCursorRequest(request), store.scopeFingerprint ?? null]
|
|
417
455
|
const compressed = deflateRawSync(Buffer.from(JSON.stringify(payload)), { level: 9 })
|
|
418
456
|
const encoded = compressed.toString('base64url')
|
|
419
457
|
const body = `s4.${encoded}`
|
|
@@ -423,6 +461,13 @@ async function encodeScanCursor(store, request, nextCandidateIndex, matchedDocum
|
|
|
423
461
|
return `${body}.${signature}`
|
|
424
462
|
}
|
|
425
463
|
|
|
464
|
+
function assertCursorScope(store, scope) {
|
|
465
|
+
if ((scope ?? null) !== (store.scopeFingerprint ?? null)) {
|
|
466
|
+
throw Object.assign(new Error('cursor 绑定的安装范围已经变化,请重新搜索'),
|
|
467
|
+
{ code: 'CURSOR_VERSION_MISMATCH' })
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
426
471
|
async function decodeCursor(store, cursor) {
|
|
427
472
|
if (String(cursor).length > CURSOR_MAX_LENGTH) {
|
|
428
473
|
throw Object.assign(new Error('cursor 超过长度上限'), { code: 'CURSOR_INVALID' })
|
|
@@ -450,6 +495,7 @@ async function decodeCursor(store, cursor) {
|
|
|
450
495
|
throw Object.assign(new Error('cursor 绑定到另一个资料版本'),
|
|
451
496
|
{ code: 'CURSOR_VERSION_MISMATCH' })
|
|
452
497
|
}
|
|
498
|
+
assertCursorScope(store, value[7])
|
|
453
499
|
if (value[2] !== DOCUMENT_ORDERING_VERSION || value[3] !== SEARCH_POLICY_FINGERPRINT) {
|
|
454
500
|
throw Object.assign(new Error('cursor 绑定的排序或搜索策略已经变化,请重新搜索'),
|
|
455
501
|
{ code: 'CURSOR_POLICY_MISMATCH' })
|
|
@@ -477,6 +523,7 @@ async function decodeCursor(store, cursor) {
|
|
|
477
523
|
{ code: value?.[1] !== cursorVersionTag(store.dataVersion)
|
|
478
524
|
? 'CURSOR_VERSION_MISMATCH' : 'CURSOR_INVALID' })
|
|
479
525
|
}
|
|
526
|
+
assertCursorScope(store, value[4])
|
|
480
527
|
return { kind: 'legacy', request, offset: value[2] }
|
|
481
528
|
}
|
|
482
529
|
|
|
@@ -496,6 +543,7 @@ async function decodeCursor(store, cursor) {
|
|
|
496
543
|
|| !Number.isInteger(value.offset) || value.offset < 0) {
|
|
497
544
|
throw Object.assign(new Error('cursor 内容无效'), { code: 'CURSOR_INVALID' })
|
|
498
545
|
}
|
|
546
|
+
assertCursorScope(store, value.scope_fingerprint)
|
|
499
547
|
return { kind: 'legacy', request: value.request, offset: value.offset }
|
|
500
548
|
}
|
|
501
549
|
|
|
@@ -520,7 +568,53 @@ function documentMatches(document, speakers, filters) {
|
|
|
520
568
|
if (!filters.activity_names.some((name) => activityMatches(activity, name, { exact }))) return false
|
|
521
569
|
}
|
|
522
570
|
if (filters.wiki_sections.length && !wikiRole) return false
|
|
523
|
-
return !filters.speakers.length ||
|
|
571
|
+
return !filters.speakers.length || speakers.some((speaker) => speakerAllowed(filters, speaker))
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* speakers 过滤按别名组匹配:请求「银灰」也命中原文说话人「恩希欧迪斯」。
|
|
576
|
+
* 展开结果挂在不可枚举属性上,游标编码仍只保存用户原始输入。
|
|
577
|
+
*/
|
|
578
|
+
const SPEAKER_ALIASES = Symbol('speakerAliases')
|
|
579
|
+
const SPEAKER_CANONICALS = Symbol('speakerCanonicals')
|
|
580
|
+
|
|
581
|
+
async function expandSpeakerAliases(store, request) {
|
|
582
|
+
const speakers = request?.filters?.speakers || []
|
|
583
|
+
if (!speakers.length) return request
|
|
584
|
+
let groups = []
|
|
585
|
+
try { groups = await aliasesFor(store, speakers) } catch { groups = [] }
|
|
586
|
+
const names = new Set(speakers)
|
|
587
|
+
const canonicals = new Set()
|
|
588
|
+
for (const group of groups) {
|
|
589
|
+
if (group.aliases.length > 1 || group.games.length) canonicals.add(normalizeText(group.canonical))
|
|
590
|
+
for (const alias of group.aliases) {
|
|
591
|
+
const value = normalizeText(alias)
|
|
592
|
+
if (!value) continue
|
|
593
|
+
names.add(value)
|
|
594
|
+
// 原文说话人常只写名不写姓:「恩希欧迪斯·希瓦艾什」→「恩希欧迪斯」。
|
|
595
|
+
const given = value.split(/[·・•]/u)[0].trim()
|
|
596
|
+
if (given && given !== value && [...given].length >= 2) names.add(given)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
Object.defineProperty(request.filters, SPEAKER_ALIASES, { value: names, enumerable: false })
|
|
600
|
+
Object.defineProperty(request.filters, SPEAKER_CANONICALS, { value: canonicals, enumerable: false })
|
|
601
|
+
return request
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** 行级兜底:资料包已把说话人链接到实体时,按实体规范名匹配。 */
|
|
605
|
+
function speakerOccurrenceAllowed(filters, line) {
|
|
606
|
+
const canonicals = filters[SPEAKER_CANONICALS]
|
|
607
|
+
if (!canonicals?.size) return false
|
|
608
|
+
return (line.entity_occurrences ?? []).some((item) => item.evidence_kind === 'speaker'
|
|
609
|
+
&& canonicals.has(normalizeText(item.canonical_name)))
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function speakerAllowed(filters, speaker) {
|
|
613
|
+
if (!filters.speakers.length) return true
|
|
614
|
+
const value = normalizeText(speaker)
|
|
615
|
+
if (!value) return false
|
|
616
|
+
return (filters[SPEAKER_ALIASES] || filters.speakers).has?.(value)
|
|
617
|
+
?? filters.speakers.includes(value)
|
|
524
618
|
}
|
|
525
619
|
|
|
526
620
|
function needsWikiActivityHydration(document) {
|
|
@@ -572,6 +666,29 @@ function lineContent(line) {
|
|
|
572
666
|
? text.slice(prefix.length).trimStart() : text
|
|
573
667
|
}
|
|
574
668
|
|
|
669
|
+
/** 展示用正文:只去掉说话人前缀,保留原文全角标点与空白,便于逐字复制引用。 */
|
|
670
|
+
function displayLineContent(line) {
|
|
671
|
+
const text = String(line.text ?? '').trim()
|
|
672
|
+
const speaker = String(line.speaker_raw ?? '').trim()
|
|
673
|
+
if (line.line_type !== 'dialogue' || !speaker) return text
|
|
674
|
+
const escaped = speaker.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
|
|
675
|
+
return text.replace(new RegExp(`^${escaped}\\s*[::]\\s*`, 'u'), '')
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* 用户常把搜索结果中的「说话人: 台词」整行复制回来搜索;台词正文不含说话人,
|
|
680
|
+
* 因此当 query 以本行说话人加冒号开头时,剥掉前缀再匹配正文。
|
|
681
|
+
*/
|
|
682
|
+
function speakerlessQuery(line, query) {
|
|
683
|
+
const speaker = normalizeText(line.speaker_raw)
|
|
684
|
+
if (!query || !speaker || line.line_type !== 'dialogue') return null
|
|
685
|
+
const lowered = query.toLowerCase()
|
|
686
|
+
const prefix = `${speaker.toLowerCase()}:`
|
|
687
|
+
if (!lowered.startsWith(prefix)) return null
|
|
688
|
+
const rest = query.slice(prefix.length).trim()
|
|
689
|
+
return rest || null
|
|
690
|
+
}
|
|
691
|
+
|
|
575
692
|
function matchesText(text, query, mode, regex) {
|
|
576
693
|
if (!query) return true
|
|
577
694
|
if (mode === 'regex') return regex.test(normalizeText(text))
|
|
@@ -638,18 +755,24 @@ function entityOccurrence(record, line, entityGroups, queryPresent, queryRange =
|
|
|
638
755
|
|
|
639
756
|
function lineMatch(record, index, request, regex, entityGroups, bounds = null) {
|
|
640
757
|
const line = record.lines[index]
|
|
641
|
-
if (request.filters
|
|
758
|
+
if (!speakerAllowed(request.filters, line.speaker_raw)
|
|
759
|
+
&& !speakerOccurrenceAllowed(request.filters, line)) return null
|
|
642
760
|
const content = lineContent(line)
|
|
643
|
-
|
|
761
|
+
let query = request.query
|
|
762
|
+
if (!matchesText(content, query, request.match_mode, regex)) {
|
|
763
|
+
const stripped = request.match_mode === 'regex' ? null : speakerlessQuery(line, query)
|
|
764
|
+
if (!stripped || !matchesText(content, stripped, request.match_mode, regex)) return null
|
|
765
|
+
query = stripped
|
|
766
|
+
}
|
|
644
767
|
let start = null
|
|
645
768
|
let end = null
|
|
646
|
-
if (
|
|
769
|
+
if (query) {
|
|
647
770
|
if (regex) {
|
|
648
771
|
const match = new RegExp(regex.source, regex.flags).exec(normalizeText(content))
|
|
649
772
|
if (match) { start = match.index; end = match.index + match[0].length }
|
|
650
773
|
} else {
|
|
651
774
|
const haystack = normalizeText(content).toLowerCase()
|
|
652
|
-
const needle =
|
|
775
|
+
const needle = query.toLowerCase()
|
|
653
776
|
start = haystack.indexOf(needle)
|
|
654
777
|
// 区间终点必须用归一化后 needle 的长度:toLocaleLowerCase 可能改变长度
|
|
655
778
|
// (如土耳其语 İ → i̇),用原 query 长度会让实体邻近度判定错位。
|
|
@@ -742,7 +865,7 @@ function isExactOfficialArchiveTitle(document, request) {
|
|
|
742
865
|
}
|
|
743
866
|
|
|
744
867
|
function lineAllowed(line, filters) {
|
|
745
|
-
return
|
|
868
|
+
return speakerAllowed(filters, line.speaker_raw) || speakerOccurrenceAllowed(filters, line)
|
|
746
869
|
}
|
|
747
870
|
|
|
748
871
|
function readableAnchor(record, filters, ranges = null) {
|
|
@@ -845,7 +968,7 @@ function excerpt(record, item, bounds = null) {
|
|
|
845
968
|
item.passage_end + PREVIEW_OPTIONS.after_lines)
|
|
846
969
|
let truncated = false
|
|
847
970
|
const lines = record.lines.slice(start - 1, end).map((line) => {
|
|
848
|
-
const raw =
|
|
971
|
+
const raw = displayLineContent(line)
|
|
849
972
|
const text = [...raw].length > PREVIEW_OPTIONS.max_chars_per_line
|
|
850
973
|
? `${[...raw].slice(0, PREVIEW_OPTIONS.max_chars_per_line).join('')}…` : raw
|
|
851
974
|
if (text !== raw) truncated = true
|
|
@@ -1003,6 +1126,8 @@ async function executeLegacySearch(store, request, offset,
|
|
|
1003
1126
|
citation: titleOnly ? `《${title}》`
|
|
1004
1127
|
: sectionName ? `《${title}》Wiki·${sectionName}`
|
|
1005
1128
|
: `《${title}》第 ${lineStart === lineEnd ? lineStart : `${lineStart}-${lineEnd}`} 行`,
|
|
1129
|
+
...(titleOnly || sectionName ? {}
|
|
1130
|
+
: { citation_handle: `${documentUid(metadata.document_id)}#L${lineStart}` }),
|
|
1006
1131
|
})
|
|
1007
1132
|
returnedChars += shown.characters
|
|
1008
1133
|
}
|
|
@@ -1017,6 +1142,7 @@ async function executeLegacySearch(store, request, offset,
|
|
|
1017
1142
|
assertSearchActive(signal, deadline, '分页游标生成')
|
|
1018
1143
|
return {
|
|
1019
1144
|
result_kind: resultKind, documents,
|
|
1145
|
+
by_game: gameDistribution(documents),
|
|
1020
1146
|
page: { returned_documents: documents.length,
|
|
1021
1147
|
total_relation: 'unknown', has_more: hasMore, exhausted: false,
|
|
1022
1148
|
next_cursor: nextCursor },
|
|
@@ -1095,6 +1221,19 @@ async function candidateDocumentIds(store, request, regex, { signal, deadline }
|
|
|
1095
1221
|
indexed = await store.findDocumentsByShortLiteral?.(request.query,
|
|
1096
1222
|
{ signal, deadline, packIds: scopedPackIds }) ?? null
|
|
1097
1223
|
}
|
|
1224
|
+
// 「说话人: 台词」整行复制搜索:倒排里没有说话人前缀,另按冒号后的台词补一次候选。
|
|
1225
|
+
const colon = request.query.indexOf(':')
|
|
1226
|
+
const spoken = colon > 0 ? request.query.slice(colon + 1).trim() : ''
|
|
1227
|
+
if (indexed !== null && spoken && [...request.query.slice(0, colon)].length <= FILTER_ITEM_LIMIT) {
|
|
1228
|
+
const spokenNgrams = ngramsFor(spoken)
|
|
1229
|
+
let extra = spokenNgrams.length && indexedLookup
|
|
1230
|
+
? await indexedLookup.call(store, spokenNgrams, { signal, deadline, packIds: scopedPackIds }) : null
|
|
1231
|
+
if (extra === null && spokenNgrams.length && [...spokenNgrams[0]].length < 3) {
|
|
1232
|
+
extra = await store.findDocumentsByShortLiteral?.(spoken,
|
|
1233
|
+
{ signal, deadline, packIds: scopedPackIds }) ?? null
|
|
1234
|
+
}
|
|
1235
|
+
indexed = extra === null ? null : [...new Set([...indexed, ...extra])]
|
|
1236
|
+
}
|
|
1098
1237
|
assertSearchActive(signal, deadline, '候选文档发现')
|
|
1099
1238
|
if (indexed === null) return prioritizeOfficialTitle(scopedIds)
|
|
1100
1239
|
return prioritizeOfficialTitle(
|
|
@@ -1352,6 +1491,8 @@ function buildPublicDocument(store, group, resultKind, request, budget = Infinit
|
|
|
1352
1491
|
citation: titleOnly ? `《${title}》`
|
|
1353
1492
|
: sectionName ? `《${title}》Wiki·${sectionName}`
|
|
1354
1493
|
: `《${title}》第 ${lineStart === lineEnd ? lineStart : `${lineStart}-${lineEnd}`} 行`,
|
|
1494
|
+
...(titleOnly || sectionName ? {}
|
|
1495
|
+
: { citation_handle: `${documentUid(metadata.document_id)}#L${lineStart}` }),
|
|
1355
1496
|
})
|
|
1356
1497
|
characters += shownCharacters
|
|
1357
1498
|
}
|
|
@@ -1359,6 +1500,17 @@ function buildPublicDocument(store, group, resultKind, request, budget = Infinit
|
|
|
1359
1500
|
return { document: result, characters, reasons }
|
|
1360
1501
|
}
|
|
1361
1502
|
|
|
1503
|
+
/** 本页命中的游戏分布:同名实体跨游戏时,让调用方一眼看出该收窄 games。 */
|
|
1504
|
+
function gameDistribution(documents) {
|
|
1505
|
+
const counts = {}
|
|
1506
|
+
for (const document of documents) {
|
|
1507
|
+
const game = document?.game
|
|
1508
|
+
if (game !== 'arknights' && game !== 'endfield') continue
|
|
1509
|
+
counts[game] = (counts[game] || 0) + 1
|
|
1510
|
+
}
|
|
1511
|
+
return counts
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1362
1514
|
async function executeScanSearch(store, request, checkpoint,
|
|
1363
1515
|
{ signal, requestId = null, deadline = Date.now() + SEARCH_TIMEOUT_MS } = {}) {
|
|
1364
1516
|
const started = Date.now()
|
|
@@ -1433,6 +1585,9 @@ async function executeScanSearch(store, request, checkpoint,
|
|
|
1433
1585
|
return {
|
|
1434
1586
|
result_kind: resultKind,
|
|
1435
1587
|
documents,
|
|
1588
|
+
by_game: gameDistribution(documents),
|
|
1589
|
+
// 本次检索覆盖的本地资料规模:零命中时据此说明"检查了多少篇/行"。
|
|
1590
|
+
...(typeof store.corpusTotals === 'function' ? { scope: store.corpusTotals() } : {}),
|
|
1436
1591
|
page: {
|
|
1437
1592
|
returned_documents: documents.length,
|
|
1438
1593
|
...(exhausted && countKnown ? { total_documents: matchedDocuments } : {}),
|
|
@@ -1464,7 +1619,7 @@ export async function executeSearch(store, raw,
|
|
|
1464
1619
|
snapshot = corpusVersionSnapshot(store)
|
|
1465
1620
|
deadline = Date.now() + SEARCH_TIMEOUT_MS
|
|
1466
1621
|
assertSearchActive(signal, deadline)
|
|
1467
|
-
const normalized = normalizedRequest(raw)
|
|
1622
|
+
const normalized = await expandSpeakerAliases(store, normalizedRequest(raw))
|
|
1468
1623
|
const attachCorpusWarnings = (value, request) => {
|
|
1469
1624
|
assertCorpusVersion(store, snapshot)
|
|
1470
1625
|
if (value?.status === 'error' || !(store.packs instanceof Map)) return value
|
|
@@ -1480,6 +1635,7 @@ export async function executeSearch(store, raw,
|
|
|
1480
1635
|
if (normalized.cursor) {
|
|
1481
1636
|
assertSearchActive(signal, deadline, '分页游标校验')
|
|
1482
1637
|
const decoded = await decodeCursor(store, normalized.cursor)
|
|
1638
|
+
await expandSpeakerAliases(store, decoded.request)
|
|
1483
1639
|
assertSearchActive(signal, deadline, '分页游标校验')
|
|
1484
1640
|
const requestedGames = decoded.request.filters?.games?.length
|
|
1485
1641
|
? decoded.request.filters.games : ['arknights', 'endfield']
|
|
@@ -1517,6 +1673,6 @@ export async function executeSearch(store, raw,
|
|
|
1517
1673
|
export function renderSearch(args, value) {
|
|
1518
1674
|
if (value?.error) return [{ type: 'text', text: `[corpus_search:error] ${value.error.code}: ${value.error.message}` }]
|
|
1519
1675
|
return [{ type: 'text', text: projectSearch(value, {
|
|
1520
|
-
query: normalizeText(args?.query),
|
|
1676
|
+
query: normalizeText(args?.query), args,
|
|
1521
1677
|
}) }]
|
|
1522
1678
|
}
|