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/entity-recognizer.js
CHANGED
|
@@ -96,7 +96,7 @@ export class EntityAliasAutomaton {
|
|
|
96
96
|
// 单字别名在普通句子中误报率过高;仅当整个输入就是该字时保留。
|
|
97
97
|
if (output.length === 1 && characters.length > 1) continue
|
|
98
98
|
found.push({ canonical: output.canonical, alias: output.alias, games: output.games,
|
|
99
|
-
start, end: index + 1 })
|
|
99
|
+
curated: Boolean(this.curated?.has(normalized(output.alias))), start, end: index + 1 })
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
found.sort((left, right) => left.start - right.start
|
|
@@ -130,6 +130,7 @@ export async function prepareEntityRecognition(store, { signal } = {}) {
|
|
|
130
130
|
}
|
|
131
131
|
store._aliasGroups ||= groups
|
|
132
132
|
state.automaton = new EntityAliasAutomaton(groups)
|
|
133
|
+
state.automaton.curated = store._curatedAliases || new Set()
|
|
133
134
|
return state.automaton
|
|
134
135
|
})
|
|
135
136
|
.catch((error) => {
|
|
@@ -159,7 +160,7 @@ export function createEntityRecognizer(store) {
|
|
|
159
160
|
if (signal?.aborted) throw cancelledError()
|
|
160
161
|
const catalog = await loadEntityRelationCatalog(store)
|
|
161
162
|
if (signal?.aborted) throw cancelledError()
|
|
162
|
-
return detectEntities(automaton, catalog, text)
|
|
163
|
+
return withAliasConflicts(store, detectEntities(automaton, catalog, text), text)
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
}
|
|
@@ -186,13 +187,26 @@ function detectEntities(automaton, catalog, text) {
|
|
|
186
187
|
relation_hints: relationHints }
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/** 本轮问题里出现的冲突别名字面:不做实体归属,只在上下文里点出数据缺陷。 */
|
|
191
|
+
function aliasConflictNotes(store, text) {
|
|
192
|
+
const conflicts = store?._aliasConflicts
|
|
193
|
+
if (!conflicts?.length) return []
|
|
194
|
+
const normalizedText = normalized(text)
|
|
195
|
+
return conflicts.filter((item) => normalizedText.includes(normalized(item.alias)))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function withAliasConflicts(store, result, text) {
|
|
199
|
+
const conflicts = aliasConflictNotes(store, text)
|
|
200
|
+
return conflicts.length ? { ...result, alias_conflicts: conflicts } : result
|
|
201
|
+
}
|
|
202
|
+
|
|
189
203
|
/** 仅在后台预热已经完成时同步识别;prompt assembly 不能等待异步 I/O。 */
|
|
190
204
|
function detectPreparedEntities(store, text) {
|
|
191
205
|
const state = preparedRecognizers.get(store)
|
|
192
206
|
const catalog = store?._endfieldRelationCatalog
|
|
193
207
|
if (!store?.loaded || !state?.automaton || state.dataVersion !== store.dataVersion
|
|
194
208
|
|| catalog?.dataVersion !== store.dataVersion) return null
|
|
195
|
-
return detectEntities(state.automaton, catalog.value, text)
|
|
209
|
+
return withAliasConflicts(store, detectEntities(state.automaton, catalog.value, text), text)
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
function messageText(message) {
|
|
@@ -204,8 +218,9 @@ function messageText(message) {
|
|
|
204
218
|
function recognitionContext(result, enabledGames) {
|
|
205
219
|
const aliases = new Map()
|
|
206
220
|
for (const match of result.matches) {
|
|
207
|
-
const value = aliases.get(match.canonical) || { aliases: new Set(), games: new Set() }
|
|
221
|
+
const value = aliases.get(match.canonical) || { aliases: new Set(), games: new Set(), curated: false }
|
|
208
222
|
value.aliases.add(match.alias)
|
|
223
|
+
if (match.curated) value.curated = true
|
|
209
224
|
for (const game of match.games || []) value.games.add(game)
|
|
210
225
|
aliases.set(match.canonical, value)
|
|
211
226
|
}
|
|
@@ -226,19 +241,23 @@ function recognitionContext(result, enabledGames) {
|
|
|
226
241
|
}
|
|
227
242
|
const lines = [...aliases].map(([canonical, value]) => {
|
|
228
243
|
const ownership = [...value.games].map((game) => GAME_LABELS[game]).filter(Boolean)
|
|
244
|
+
const basis = value.curated ? ' · 依据:人工别名表' : ''
|
|
229
245
|
return `- ${promptData(canonical)} — ${ownership.length ? ownership.join(' + ') : '归属未确定'}` +
|
|
230
|
-
`(问题中命中:${[...value.aliases].map((alias) => promptData(alias)).join('、')})`
|
|
246
|
+
`(问题中命中:${[...value.aliases].map((alias) => promptData(alias)).join('、')}${basis})`
|
|
231
247
|
})
|
|
232
248
|
const enabled = enabledGames.map((game) => GAME_LABELS[game]).filter(Boolean)
|
|
233
249
|
const relationLines = (result.relation_hints || []).map((hint) =>
|
|
234
250
|
hint.kind === 'retraveler_memory_prototype'
|
|
235
251
|
? `- ${promptData(hint.endfield_name)}:再旅者;泰拉记忆原型=${promptData(hint.terra_memory_prototype || '未登记')}。检索词:${(Array.isArray(hint.query_terms) ? hint.query_terms : []).map((term) => promptData(term)).join('、')}。两者不是别名。`
|
|
236
252
|
: `- ${promptData(hint.endfield_name)} / ${promptData(hint.arknights_name)}:仅登记外观相似;现有剧情没有关系证据,不得推断为再旅者或记忆原型。`)
|
|
253
|
+
const conflictLines = (result.alias_conflicts || []).map((item) =>
|
|
254
|
+
`- 别名冲突:${promptData(item.alias)} 同时被 ${item.canonicals.map((name) => promptData(name)).join('、')} 声明为别名。本轮已忽略该别名、不归属任何实体;需要这些实体时请直呼规范名或分别检索。`)
|
|
237
255
|
return [
|
|
238
256
|
'<prts:retrieval-context>',
|
|
239
257
|
`当前搭载资料:${enabled.join('、') || '无'}。`,
|
|
240
258
|
...(lines.length ? ['用户问题中识别到的规范实体与游戏归属:', ...lines] : []),
|
|
241
259
|
...relationLines,
|
|
260
|
+
...conflictLines,
|
|
242
261
|
'</prts:retrieval-context>',
|
|
243
262
|
].join('\n')
|
|
244
263
|
}
|
|
@@ -302,6 +321,8 @@ export function applyEntityRecognition(ctx, store, shared = null) {
|
|
|
302
321
|
ctx.systemPrompt.context({
|
|
303
322
|
name: 'prts-terrarchive:retrieval-entities',
|
|
304
323
|
order: 1000,
|
|
324
|
+
// Recognized entity names are user content: never resolve {{name}} in them.
|
|
325
|
+
interpolate: false,
|
|
305
326
|
text: ({ scope }) => {
|
|
306
327
|
const state = turns.get(scope)
|
|
307
328
|
return state?.context && hasVisibleRetrievalSkill(scope, []) ? state.context : ''
|
package/src/entity-routing.js
CHANGED
|
@@ -30,8 +30,11 @@ function mergeRelationCatalog(fallback, installed) {
|
|
|
30
30
|
/** 资料包内关系表优先,插件内置表只为旧包提供最小兼容路由。 */
|
|
31
31
|
export async function loadEntityRelationCatalog(store) {
|
|
32
32
|
const dataVersion = store?.dataVersion ?? 'no-store'
|
|
33
|
+
const scopeFingerprint = store?.scopeFingerprint ?? null
|
|
34
|
+
const installationId = store?.installationId ?? null
|
|
33
35
|
const cached = store?._endfieldRelationCatalog
|
|
34
|
-
if (cached?.dataVersion === dataVersion
|
|
36
|
+
if (cached?.dataVersion === dataVersion && cached.scopeFingerprint === scopeFingerprint
|
|
37
|
+
&& cached.installationId === installationId) return cached.value
|
|
35
38
|
let installed = {}
|
|
36
39
|
try {
|
|
37
40
|
const loaded = await store?.getDocumentByPath?.('config/retravelers.json')
|
|
@@ -39,7 +42,7 @@ export async function loadEntityRelationCatalog(store) {
|
|
|
39
42
|
installed = JSON.parse((record?.lines || []).map((line) => line.text).join('\n') || '{}')
|
|
40
43
|
} catch { installed = {} }
|
|
41
44
|
const value = mergeRelationCatalog(await bundledRelations(), installed)
|
|
42
|
-
if (store) store._endfieldRelationCatalog = { dataVersion, value }
|
|
45
|
+
if (store) store._endfieldRelationCatalog = { dataVersion, scopeFingerprint, installationId, value }
|
|
43
46
|
return value
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -72,11 +75,22 @@ export function relevantRetravelerRelations(catalog, ...values) {
|
|
|
72
75
|
}))
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* 仅双模块启用、且本次请求本身覆盖两款游戏时,把人工审校关系作为独立附属字段挂到
|
|
80
|
+
* 检索结果。只按请求参数与命中文档标题判断相关性:命中正文里顺带出现某个名字
|
|
81
|
+
* (如语音里提到“艾雅法拉”)不代表用户在问跨游戏关系,挂上只会制造噪声。
|
|
82
|
+
*/
|
|
76
83
|
export async function attachRetravelerRelations(store, response, request, enabledGames) {
|
|
77
84
|
if (!enabledGames?.includes('arknights') || !enabledGames?.includes('endfield')
|
|
78
85
|
|| response?.status === 'error' || response?.error) return response
|
|
86
|
+
const requestedGames = Array.isArray(request?.games) && request.games.length ? request.games : null
|
|
87
|
+
if (requestedGames && !(requestedGames.includes('arknights') && requestedGames.includes('endfield'))) {
|
|
88
|
+
return response
|
|
89
|
+
}
|
|
79
90
|
const catalog = await loadEntityRelationCatalog(store)
|
|
80
|
-
const
|
|
91
|
+
const { after: _after, cursor: _cursor, ...requestFields } = request || {}
|
|
92
|
+
const titles = (Array.isArray(response?.documents) ? response.documents : [])
|
|
93
|
+
.map((document) => document?.title).filter(Boolean)
|
|
94
|
+
const relations = relevantRetravelerRelations(catalog, requestFields, titles)
|
|
81
95
|
return relations.length ? { ...response, retraveler_relations: relations } : response
|
|
82
96
|
}
|
package/src/evidence-state.js
CHANGED
|
@@ -293,7 +293,8 @@ export function replayCoveredRead(state, requested, contract) {
|
|
|
293
293
|
stats: { elapsed_ms: 0, scanned_documents: 0, scanned_lines: 0, returned_chars: 0,
|
|
294
294
|
estimated_input_tokens: 0, truncated: hasMore, reused_local_evidence: true },
|
|
295
295
|
warnings: [],
|
|
296
|
-
guidance: '该范围已在当前模型可见的上文工具结果中完整出现;这里复用已核验的本地原文,不要再次读取相同范围。'
|
|
296
|
+
guidance: '该范围已在当前模型可见的上文工具结果中完整出现;这里复用已核验的本地原文,不要再次读取相同范围。'
|
|
297
|
+
+ '若上文其实已经看不到这些行(被压缩或折叠),传 reuse:false 重发本页全文。',
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -327,6 +328,7 @@ export function reuseReadCoverage(response, plan) {
|
|
|
327
328
|
},
|
|
328
329
|
warnings: [...(response.warnings || []), { code: 'PARTIAL_READ_REUSED',
|
|
329
330
|
message: `本页已有 ${plan.reusedRanges.length} 段经过核验,本次只返回新读取行。` }],
|
|
330
|
-
guidance: '本页与当前模型可见的上文原文部分重叠;这里只返回新读取行,coverage.reused_ranges 对应旧行就在上方可见工具结果中。'
|
|
331
|
+
guidance: '本页与当前模型可见的上文原文部分重叠;这里只返回新读取行,coverage.reused_ranges 对应旧行就在上方可见工具结果中。'
|
|
332
|
+
+ '若上文其实已经看不到这些行(被压缩或折叠),传 reuse:false 重发本页全文。',
|
|
331
333
|
}
|
|
332
334
|
}
|
package/src/i18n.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Official localizations: exact source identities, independent of reading coverage. */
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { assertCorpusVersion, corpusVersionSnapshot, documentGame, documentUid,
|
|
4
|
-
naturalDocumentTitle } from './store.js'
|
|
4
|
+
naturalDocumentTitle, DATA_VERSION_REF_PATTERN, dataVersionMatches, shortDataVersion } from './store.js'
|
|
5
5
|
import { LOCALIZATION_LANGUAGES } from './localization-format.js'
|
|
6
6
|
|
|
7
7
|
const PACK = 'endfield_official_game'
|
|
@@ -23,35 +23,29 @@ for (const [locale, code] of Object.entries({ 'en-us': 'EN', 'ja-jp': 'JP', 'ko-
|
|
|
23
23
|
const fault = (code, message) => Object.assign(new Error(message), { code,
|
|
24
24
|
retryable: code === 'PACKAGE_VERSION_MISMATCH' })
|
|
25
25
|
|
|
26
|
-
export const I18N_DESCRIPTION =
|
|
27
|
-
'查询终末地官方本地化文本,返回其他语言原文;不生成翻译。',
|
|
28
|
-
'已有资料时优先使用完整 title 或 document_uid,可加 line;只有原句/名称时用 query 反查,source_language 默认 CN。',
|
|
29
|
-
'languages 指定目标语言(EN 英语、JP 日语、KR 韩语、TC 繁中等)。query 默认 exact,片段用 match_mode=literal。',
|
|
30
|
-
'相同原文可能对应多个不同译文,按来源消歧。角色档案按整条记录、档案库按整篇内容块对齐,中文行号不是目标语言行号。',
|
|
31
|
-
'默认不显示内部文本 ID;仅核验时 include_ids=true,后续可用 text_ids 精确查询。续页原样提交 page.continuation。',
|
|
32
|
-
].join(' ')
|
|
26
|
+
export const I18N_DESCRIPTION = '查询终末地官方本地化原文。query/title/document_uid/text_ids 四选一;同文异译按来源消歧,中文行号不是译文行号。续页原样提交返回的“续页”调用。原文与各语言译文完全相同的条目会合并为一组并汇总出处。'
|
|
33
27
|
|
|
34
28
|
export const I18N_PARAMETERS = {
|
|
35
29
|
type: 'object', additionalProperties: false, required: ['languages'],
|
|
36
30
|
properties: {
|
|
37
31
|
game: { type: 'string', enum: ['endfield'], description: '当前支持终末地官方资料' },
|
|
38
|
-
query: { type: 'string', description: '
|
|
39
|
-
title: { type: 'string', description: '
|
|
40
|
-
document_uid: { type: 'string', description: '
|
|
41
|
-
line: { type: 'integer', description: '
|
|
42
|
-
text_ids: { type: 'array', items: { type: 'string' }, description: '
|
|
43
|
-
source_language: { type: 'string', description: '
|
|
32
|
+
query: { type: 'string', description: '名称或原句' },
|
|
33
|
+
title: { type: 'string', description: '已有结果的完整篇章标题' },
|
|
34
|
+
document_uid: { type: 'string', description: '已有结果的文档定位' },
|
|
35
|
+
line: { type: 'integer', description: '已有中文结果中的行号' },
|
|
36
|
+
text_ids: { type: 'array', items: { type: 'string' }, description: '返回的文本 ID 字符串,用于精确复查' },
|
|
37
|
+
source_language: { type: 'string', description: '原句语言,默认 CN;支持 en/ja/ko/zh-CN 等代码' },
|
|
44
38
|
languages: { type: 'array', items: { type: 'string' }, description: '1–4 种目标语言:CN/EN/JP/KR/TC/MX/BR/FR/DE/RU/IT/ID/TH/VN' },
|
|
45
|
-
match_mode: { type: 'string', enum: ['exact', 'literal'], description: '
|
|
46
|
-
include_ids: { type: 'boolean', description: '
|
|
39
|
+
match_mode: { type: 'string', enum: ['exact', 'literal'], description: '默认 exact 精确匹配;片段用 literal' },
|
|
40
|
+
include_ids: { type: 'boolean', description: '核验时返回内部 ID,默认 false' },
|
|
47
41
|
max_matches: { type: 'integer', description: '每页候选数,默认 5,最多 20' },
|
|
48
42
|
max_chars: { type: 'integer', description: '每页原文和译文的字符预算,默认 16000,范围 1000–48000' },
|
|
49
|
-
data_version: { type: 'string', description: '
|
|
43
|
+
data_version: { type: 'string', description: '已有结果的资料版本(可省略;续页给出的短版本号即可)' },
|
|
50
44
|
after: { type: 'object', additionalProperties: false,
|
|
51
|
-
required: ['
|
|
45
|
+
required: ['position', 'text_offset'],
|
|
52
46
|
properties: { data_version: { type: 'string' }, request_hash: { type: 'string' },
|
|
53
47
|
position: { type: 'integer' }, text_offset: { type: 'integer' } },
|
|
54
|
-
description: '
|
|
48
|
+
description: '原样复制 page.continuation.after' },
|
|
55
49
|
},
|
|
56
50
|
}
|
|
57
51
|
|
|
@@ -125,14 +119,15 @@ export function normalizeI18nRequest(args) {
|
|
|
125
119
|
request.max_matches = integer(args.max_matches ?? 5, 1, 20, 'max_matches')
|
|
126
120
|
request.max_chars = integer(args.max_chars ?? 16000, 1000, 48000, 'max_chars')
|
|
127
121
|
if (args.data_version !== undefined) {
|
|
128
|
-
if (typeof args.data_version !== 'string' ||
|
|
122
|
+
if (typeof args.data_version !== 'string' || !DATA_VERSION_REF_PATTERN.test(args.data_version)) throw fault('INVALID_REQUEST', 'data_version 必须是续页给出的资料版本(至少 8 位十六进制)')
|
|
129
123
|
request.data_version = args.data_version
|
|
130
124
|
}
|
|
131
125
|
if (args.after !== undefined) {
|
|
132
126
|
const after = args.after
|
|
133
127
|
if (!after || typeof after !== 'object' || Array.isArray(after)
|
|
134
|
-
|| Object.keys(after).
|
|
135
|
-
||
|
|
128
|
+
|| Object.keys(after).some((key) => !['data_version', 'position', 'request_hash', 'text_offset'].includes(key))
|
|
129
|
+
|| (after.data_version !== undefined && !DATA_VERSION_REF_PATTERN.test(String(after.data_version)))
|
|
130
|
+
|| (after.request_hash !== undefined && !/^[0-9a-f]{8,64}$/u.test(String(after.request_hash)))) {
|
|
136
131
|
throw fault('INVALID_REQUEST', 'after 必须原样使用工具返回的续页位置')
|
|
137
132
|
}
|
|
138
133
|
request.after = { ...after,
|
|
@@ -260,13 +255,13 @@ export async function executeI18n(store, raw, { signal, enabledGames = ['endfiel
|
|
|
260
255
|
await store.ready()
|
|
261
256
|
const snapshot = corpusVersionSnapshot(store)
|
|
262
257
|
checkpoint(store, snapshot, signal)
|
|
263
|
-
if ((request.data_version && request.data_version
|
|
264
|
-
|| (request.after && request.after.data_version
|
|
258
|
+
if ((request.data_version && !dataVersionMatches(request.data_version, snapshot.dataVersion))
|
|
259
|
+
|| (request.after?.data_version && !dataVersionMatches(request.after.data_version, snapshot.dataVersion))) {
|
|
265
260
|
throw fault('PACKAGE_VERSION_MISMATCH', '本地化查询所属资料版本已变化,请重新查询')
|
|
266
261
|
}
|
|
267
262
|
const { after, data_version: _version, ...identity } = request
|
|
268
263
|
const requestHash = sha256(JSON.stringify(identity))
|
|
269
|
-
if (after && after.request_hash
|
|
264
|
+
if (after?.request_hash && !requestHash.startsWith(after.request_hash)) throw fault('INVALID_REQUEST', '续页参数与原查询不一致')
|
|
270
265
|
const base = { game: 'endfield', data_version: snapshot.dataVersion,
|
|
271
266
|
source_language: request.source_language, languages: request.languages }
|
|
272
267
|
const pack = store.packs.get(PACK), meta = pack?.localization
|
|
@@ -325,6 +320,19 @@ export async function executeI18n(store, raw, { signal, enabledGames = ['endfiel
|
|
|
325
320
|
return a[0] - b[0] || a[1] - b[1] || a[2].localeCompare(b[2], 'en', { numeric: true })
|
|
326
321
|
})
|
|
327
322
|
}
|
|
323
|
+
// 按名称查询时同一译名常对应上千个文本 ID(每段对话的说话人字段各一份);原文与所有
|
|
324
|
+
// 请求语言完全一致的条目合并为一组,出处汇总到代表条目,避免逐页翻同一个译名。
|
|
325
|
+
const groupMembers = new Map()
|
|
326
|
+
if (request.query && ids.length > 1) {
|
|
327
|
+
const bySignature = new Map()
|
|
328
|
+
for (const id of ids) {
|
|
329
|
+
const signature = JSON.stringify(requestedLanguages.map((lang) => dictionaries.get(lang)?.byId.get(id) ?? null))
|
|
330
|
+
const representative = bySignature.get(signature)
|
|
331
|
+
if (representative === undefined) { bySignature.set(signature, id); groupMembers.set(id, [id]) }
|
|
332
|
+
else groupMembers.get(representative).push(id)
|
|
333
|
+
}
|
|
334
|
+
ids = [...groupMembers.keys()]
|
|
335
|
+
}
|
|
328
336
|
// Preserve source/catalog order for document reads and deterministic query pages.
|
|
329
337
|
let position = after?.position ?? 0, offset = after?.text_offset ?? 0
|
|
330
338
|
if (position > ids.length || (position === ids.length && offset)) throw fault('INVALID_REQUEST', '续页位置超出查询结果')
|
|
@@ -340,8 +348,11 @@ export async function executeI18n(store, raw, { signal, enabledGames = ['endfiel
|
|
|
340
348
|
const values = [sourceValue, ...Object.values(translations)]
|
|
341
349
|
const maximumLength = Math.max(...requestedLanguages.map((lang) => [...(dictionaries.get(lang)?.byId.get(id) || '')].length))
|
|
342
350
|
if (offset && offset >= maximumLength) throw fault('INVALID_REQUEST', '记录续页偏移超出正文')
|
|
343
|
-
const
|
|
351
|
+
const members = groupMembers.get(id) || [id]
|
|
352
|
+
const refs = members.flatMap((member) => catalog.byId.get(member).references)
|
|
353
|
+
.filter((ref) => !documentId || ref.document_id === documentId)
|
|
344
354
|
matches.push({ source: { language: request.source_language, ...sourceValue }, translations,
|
|
355
|
+
...(members.length > 1 ? { merged_text_count: members.length } : {}),
|
|
345
356
|
references: refs.slice(0, 8).map((ref) => publicReference(store, ref)), reference_count: refs.length,
|
|
346
357
|
...(request.include_ids ? { text_id: id, source_fields: entry.sources.slice(0, 8),
|
|
347
358
|
source_field_count: entry.sources.length } : {}) })
|
|
@@ -355,11 +366,49 @@ export async function executeI18n(store, raw, { signal, enabledGames = ['endfiel
|
|
|
355
366
|
matches, total_matches: ids.length,
|
|
356
367
|
...(request.text_ids ? { missing_text_ids: request.text_ids.filter((id) => !catalog.byId.has(id)) } : {}),
|
|
357
368
|
note: '返回官方本地化原文;引用请注明语言。中文行号仅用于定位,record/document 对齐不表示跨语言逐行对应。',
|
|
358
|
-
page: { has_more: hasMore, continuation: hasMore ? { ...identity,
|
|
359
|
-
|
|
360
|
-
request_hash: requestHash, position, text_offset: offset } } : null } }
|
|
369
|
+
page: { has_more: hasMore, continuation: hasMore ? { ...compactIdentity(identity),
|
|
370
|
+
after: { data_version: shortDataVersion(snapshot.dataVersion),
|
|
371
|
+
request_hash: requestHash.slice(0, 12), position, text_offset: offset } } : null } }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 续页调用只保留用户实际给出的参数,默认值不重复回显。 */
|
|
375
|
+
function compactIdentity(identity) {
|
|
376
|
+
const defaults = { source_language: 'CN', match_mode: 'exact', include_ids: false, max_matches: 5, max_chars: 16000 }
|
|
377
|
+
return Object.fromEntries(Object.entries(identity).filter(([key, value]) =>
|
|
378
|
+
value !== undefined && value !== null && defaults[key] !== value))
|
|
361
379
|
}
|
|
362
380
|
|
|
381
|
+
function renderValue(value) {
|
|
382
|
+
if (!value) return '(无)'
|
|
383
|
+
if (value.status === 'unavailable') return '(未安装该语言)'
|
|
384
|
+
if (value.status === 'missing_localization') return '(该语言无对应译文)'
|
|
385
|
+
const range = value.truncated || value.character_start
|
|
386
|
+
? ` 〔第 ${value.character_start + 1}–${value.character_end} 字 / 共 ${value.total_characters} 字〕` : ''
|
|
387
|
+
return `${value.text}${range}`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 文本投影:与其他检索工具一致,按条目分块展示原文、译文与出处,不直接回显 JSON。 */
|
|
363
391
|
export function renderI18n(_args, value) {
|
|
364
|
-
return [{ type: 'text', text:
|
|
392
|
+
if (!value || typeof value !== 'object') return [{ type: 'text', text: '[corpus_i18n:error] 无法投影本地化结果' }]
|
|
393
|
+
const lines = [`# 终末地官方本地化(${value.game_version || '版本未知'}):${value.source_language} → ${(value.languages || []).join('/')}`]
|
|
394
|
+
if (value.status !== 'ok') {
|
|
395
|
+
lines.push(value.message || (value.status === 'not_found'
|
|
396
|
+
? '未找到匹配原文。名称默认 exact 精确匹配整条文本;片段请设 match_mode="literal"。本工具只收录终末地官方本地化,明日方舟文本不在范围内。'
|
|
397
|
+
: value.status === 'unmapped_source' ? '该定位没有可对齐的本地化文本。' : `状态:${value.status}`))
|
|
398
|
+
}
|
|
399
|
+
if (value.total_matches) {
|
|
400
|
+
const raw = value.matches.reduce((sum, match) => sum + (match.merged_text_count || 1), 0)
|
|
401
|
+
lines.push(`共 ${value.total_matches} 组不同原文/译文${raw > value.matches.length ? `;本页 ${value.matches.length} 组合并自 ${raw} 条同文文本` : ''}。`)
|
|
402
|
+
}
|
|
403
|
+
value.matches.forEach((match, index) => {
|
|
404
|
+
lines.push('', `${index + 1}. ${match.source.language}:${renderValue(match.source)}`)
|
|
405
|
+
for (const [lang, translation] of Object.entries(match.translations || {})) lines.push(` ${lang}:${renderValue(translation)}`)
|
|
406
|
+
const refs = (match.references || []).slice(0, 3).map((ref) => ref.citation)
|
|
407
|
+
if (refs.length) lines.push(` 出处${match.reference_count > refs.length ? `(共 ${match.reference_count} 处,示例)` : ''}:${refs.join(';')}`)
|
|
408
|
+
if (match.text_id) lines.push(` text_id=${match.text_id}`)
|
|
409
|
+
})
|
|
410
|
+
if (value.missing_text_ids?.length) lines.push(`未找到的 text_ids:${value.missing_text_ids.join('、')}`)
|
|
411
|
+
if (value.note) lines.push('', value.note)
|
|
412
|
+
if (value.page?.has_more && value.page.continuation) lines.push(`续页:corpus_i18n(${JSON.stringify(value.page.continuation)})`)
|
|
413
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
365
414
|
}
|