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/index.js
CHANGED
|
@@ -30,17 +30,21 @@ import { createHash } from 'node:crypto'
|
|
|
30
30
|
import { mkdir, realpath } from 'node:fs/promises'
|
|
31
31
|
import { assertCorpusVersion, corpusVersionSnapshot,
|
|
32
32
|
CorpusStore, documentGame, documentUid, naturalDocumentTitle, publicStoryStageCode,
|
|
33
|
-
publicStoryPart } from './store.js'
|
|
33
|
+
publicStoryPart, DATA_VERSION_REF_PATTERN, dataVersionMatches } from './store.js'
|
|
34
34
|
import { readCurrentReleasePointer } from './installer.js'
|
|
35
35
|
import { watchCurrentRelease } from './release-watch.js'
|
|
36
36
|
import { END_FIELD_STORY_CONTENT_TYPES, executeRead, readContractFromCursor,
|
|
37
37
|
normalizeReadRequest, projectReadPublic, renderRead } from './read.js'
|
|
38
38
|
import { executeSearch, renderSearch } from './search.js'
|
|
39
|
+
import { executeNavigation, NAVIGATION_DESCRIPTION, NAVIGATION_PARAMETERS, renderNavigation } from './navigation.js'
|
|
39
40
|
import { executeI18n, I18N_DESCRIPTION, I18N_PARAMETERS, I18N_OUTPUT_SCHEMA, renderI18n } from './i18n.js'
|
|
40
41
|
import { executeTimelineSearch, renderTimeline } from './timeline.js'
|
|
41
42
|
import { AnonymousSessionProvider, CloudRetrievalClient, StaticTokenProvider,
|
|
42
43
|
createAgentCloudClientRegistry,
|
|
43
|
-
|
|
44
|
+
cloudFaultError, readOrCreateClientId } from './cloud.js'
|
|
45
|
+
import { dropEmptyPlaceholders } from './tool-args.js'
|
|
46
|
+
import { formatCitation, normalizeReadArguments, parseCitation, readTargets,
|
|
47
|
+
readTargetError, withReadGuidance } from './read-args.js'
|
|
44
48
|
import { createSharedState } from './state.js'
|
|
45
49
|
import { applyUi } from './ui.js'
|
|
46
50
|
import { acquireInvestigationStore } from './investigation-store.js'
|
|
@@ -66,7 +70,6 @@ const storesByDirectory = new Map()
|
|
|
66
70
|
|
|
67
71
|
const LOCAL_CORPUS_MISSING_MESSAGE =
|
|
68
72
|
'本地数据包暂未安装,请提醒用户前往“设置 → 插件 → PRTS 语料”安装。'
|
|
69
|
-
const DATA_VERSION_PATTERN = /^[0-9a-f]{64}$/
|
|
70
73
|
|
|
71
74
|
function sameFilesystemPath(left, right) {
|
|
72
75
|
return process.platform === 'win32'
|
|
@@ -110,35 +113,15 @@ async function requireLocalCorpus(store) {
|
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
const READ_DESCRIPTION =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
'像 grep 一样搜索 PRTS.chat 本地语料;命中立即返回原行及上下各一行,并按文档归并。',
|
|
123
|
-
'query 使用短实体名、篇章展示名或原句片段;也可省略 query,仅按过滤条件列出资料入口。',
|
|
124
|
-
'角色个人页用 character_wiki;活动/密录整理页用 story_wiki;角色在单个活动中的辅助整理用 character_activity_wiki。wiki_sections 可精确限定相关活动、相关角色、剧情总结、角色剧情概括等标签字段。',
|
|
125
|
-
'literal 是默认连续字面匹配;只有特殊模式才使用受限 regex。新查询的下一页保留原搜索条件,并把返回的 next_after 原样放入 after;锚点由完整资料版本、资料类型与自然标题组成,不再暴露内部 cursor。旧会话的 cursor 链仅按返回的 next_cursor 继续。',
|
|
126
|
-
].join(' ')
|
|
127
|
-
|
|
128
|
-
const TIMELINE_DESCRIPTION = [
|
|
129
|
-
'按活动名、年份及自动展开的实体别名检索活动时间线(PRTS Wiki《泰拉年表》本地投影)。',
|
|
130
|
-
'人物放 entity_names 以自动裂变别名;结果只给时间、事件正文和“年表出处”标记,把标记原样传回 source_marker 可反查完整来源。',
|
|
131
|
-
].join(' ')
|
|
132
|
-
|
|
133
|
-
const CLOUD_SEARCH_DESCRIPTION = [
|
|
134
|
-
'用一次自然语言请求同时查询 PRTS.chat 云端的明日方舟与终末地图谱、档案、原文、自建 Wiki 和时间线组合索引;只有用户明确限定游戏时才使用 games 收窄。',
|
|
135
|
-
'返回末尾的「## 可读取原文」列出已映射到本地篇章的完整自然语言标题与行号;有同名消歧标记时按 document_uid + line 调用,否则按 title + line。',
|
|
136
|
-
].join(' ')
|
|
137
|
-
|
|
138
|
-
const CLOUD_INSPECT_DESCRIPTION = [
|
|
139
|
-
'按 section 和过滤条件读取最近一次云端检索状态(request_id 由运行时自动注入)。',
|
|
140
|
-
'用于定点读取回答材料、候选状态或诊断记录,并支持按 next_cursor 分页。',
|
|
141
|
-
].join(' ')
|
|
116
|
+
const READ_DESCRIPTION = '读取 PRTS.chat 本地资料,支持行号上下文、Wiki 字段和单篇/合集连续阅读。只选一种主定位方式;续页原样提交 page.continuation。最小 UID 全文调用:{"document_uid":"检索返回的 doc_ 定位","mode":"document"};不要填写不适用的附属参数。'
|
|
117
|
+
|
|
118
|
+
const SEARCH_DESCRIPTION = '搜索 PRTS.chat 本地语料,返回按文档归并的命中行及上下各一行;省略 query 可列目录。过滤数组内 OR、字段间 AND。终末地版本、最新主线与章节顺序先用 corpus_navigate 定位。'
|
|
119
|
+
|
|
120
|
+
const TIMELINE_DESCRIPTION = '检索明日方舟活动时间线(PRTS Wiki《泰拉年表》),返回年份、事件与可反查的出处标记。'
|
|
121
|
+
|
|
122
|
+
const CLOUD_SEARCH_DESCRIPTION = '用自然语言检索 PRTS.chat 云端图谱、档案、原文、Wiki 和时间线。返回的「可读取原文」定位可交给 corpus_read 核验。'
|
|
123
|
+
|
|
124
|
+
const CLOUD_INSPECT_DESCRIPTION = '读取最近一次云端检索的回答材料、候选或诊断记录;request_id 由运行时注入。'
|
|
142
125
|
|
|
143
126
|
const TOOL_GAME_LABELS = Object.freeze({ arknights: '明日方舟', endfield: '终末地' })
|
|
144
127
|
|
|
@@ -187,34 +170,34 @@ const stringList = (description) => ({ type: 'array', items: { type: 'string' },
|
|
|
187
170
|
const SEARCH_PARAMETERS = {
|
|
188
171
|
type: 'object', additionalProperties: false,
|
|
189
172
|
properties: {
|
|
190
|
-
cursor: { type: 'string', description: '
|
|
191
|
-
query: { type: 'string', description: '
|
|
173
|
+
cursor: { type: 'string', description: '旧会话 next_cursor,单独提交;新查询用 after' },
|
|
174
|
+
query: { type: 'string', description: '短实体名、标题或原句片段;完整研究问题用 cloud_search' },
|
|
192
175
|
resource_types: { type: 'array', items: { type: 'string', enum: RESOURCE_TYPES },
|
|
193
|
-
description: '资料类型;character_bundle
|
|
176
|
+
description: '资料类型;character_bundle 聚合角色资料' },
|
|
194
177
|
games: { type: 'array', items: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
195
|
-
description: '
|
|
178
|
+
description: '游戏范围;省略时查询全部启用游戏' },
|
|
196
179
|
content_types: { type: 'array', items: { type: 'string', enum: CONTENT_TYPES },
|
|
197
|
-
description: '
|
|
198
|
-
collection_names: stringList('
|
|
199
|
-
character_names: stringList('
|
|
200
|
-
story_names: stringList('
|
|
201
|
-
activity_names: stringList('
|
|
180
|
+
description: '内容形式' },
|
|
181
|
+
collection_names: stringList('活动或任务等上级集合展示名'),
|
|
182
|
+
character_names: stringList('资料归属角色的展示名'),
|
|
183
|
+
story_names: stringList('单关/单篇剧情名,如“鳞的记忆”;活动名不要填这里,改用 activity_names'),
|
|
184
|
+
activity_names: stringList('活动或主线章节名,如“昨日海”'),
|
|
202
185
|
wiki_sections: { type: 'array', items: { type: 'string', enum: WIKI_SECTION_VALUES },
|
|
203
|
-
description: 'Wiki
|
|
186
|
+
description: '限定 Wiki 标签字段' },
|
|
204
187
|
entity_names: stringList('只返回出现指定实体的行'),
|
|
205
|
-
speakers: stringList('
|
|
188
|
+
speakers: stringList('说话人展示名,仅匹配亲口台词;自动展开已登记别名'),
|
|
206
189
|
match_mode: { type: 'string', enum: ['literal', 'regex'],
|
|
207
|
-
description: '默认 literal
|
|
190
|
+
description: '默认 literal 连续字面匹配;regex 为受限正则' },
|
|
208
191
|
context_terms: { type: 'array', items: { type: 'string' },
|
|
209
|
-
description: '
|
|
192
|
+
description: '强约束:每个词都须出现在命中行前后 3 行内(非整篇共现),最多 8 个;不确定时省略' },
|
|
210
193
|
after: { type: 'object', additionalProperties: false,
|
|
211
|
-
description: '
|
|
194
|
+
description: '原样复制 page.next_after,保留原 query 和过滤条件;切版后重新搜索',
|
|
212
195
|
required: ['data_version', 'resource_type', 'title', 'position'], properties: {
|
|
213
196
|
data_version: { type: 'string',
|
|
214
|
-
description: '
|
|
197
|
+
description: '上一页的完整 SHA-256 资料版本' },
|
|
215
198
|
resource_type: { type: 'string', enum: RESOURCE_TYPES },
|
|
216
|
-
title: { type: 'string'
|
|
217
|
-
position: { type: 'integer'
|
|
199
|
+
title: { type: 'string' },
|
|
200
|
+
position: { type: 'integer' },
|
|
218
201
|
} },
|
|
219
202
|
},
|
|
220
203
|
}
|
|
@@ -244,6 +227,7 @@ const SEARCH_OUTPUT_SCHEMA = {
|
|
|
244
227
|
line_start: { type: 'integer' }, line_end: { type: 'integer' },
|
|
245
228
|
match_kind: { type: 'string' }, evidence_kind: { type: 'string' },
|
|
246
229
|
citation: { type: 'string' },
|
|
230
|
+
citation_handle: { type: 'string' },
|
|
247
231
|
excerpt: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
248
232
|
required: ['line', 'role', 'line_type', 'speaker', 'text', 'truncated'],
|
|
249
233
|
properties: { line: { type: 'integer' },
|
|
@@ -279,6 +263,13 @@ const SEARCH_OUTPUT_SCHEMA = {
|
|
|
279
263
|
} }, { type: 'null' }] } } },
|
|
280
264
|
truncated: { type: 'boolean' },
|
|
281
265
|
truncation_reasons: { type: 'array', items: { type: 'string' } },
|
|
266
|
+
scope: { type: 'object', additionalProperties: false,
|
|
267
|
+
required: ['documents', 'lines'], properties: {
|
|
268
|
+
documents: { type: 'integer' }, lines: { type: 'integer' },
|
|
269
|
+
} },
|
|
270
|
+
by_game: { type: 'object', additionalProperties: false, properties: {
|
|
271
|
+
arknights: { type: 'integer' }, endfield: { type: 'integer' },
|
|
272
|
+
} },
|
|
282
273
|
warnings: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
283
274
|
required: ['code', 'game', 'message'], properties: {
|
|
284
275
|
code: { type: 'string' }, game: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
@@ -296,34 +287,39 @@ const SEARCH_OUTPUT_SCHEMA = {
|
|
|
296
287
|
const READ_PARAMETERS = {
|
|
297
288
|
type: 'object',
|
|
298
289
|
properties: {
|
|
299
|
-
title: { type: 'string', description: '
|
|
290
|
+
title: { type: 'string', description: '展示标题(可省略分隔符与 Wiki 序号后缀,找不到会列出候选);与 document_uid 互斥;可配 line、section 或 mode=document,单独提供时从第 1 行读全文' },
|
|
300
291
|
document_uid: { type: 'string',
|
|
301
|
-
description: '
|
|
302
|
-
stage_code: { type: 'string', description: '
|
|
303
|
-
story_part: { type: 'string', enum: ['before', 'after', 'story'],
|
|
304
|
-
description: '
|
|
292
|
+
description: '仅复制搜索返回的 doc_ 开头同名消歧定位,替代 title;不能填展示标题,标题请用 title;可配合集 mode' },
|
|
293
|
+
stage_code: { type: 'string', description: '明日方舟关卡代号,如 15-17、TW-ST-1' },
|
|
294
|
+
story_part: { type: 'string', enum: ['', 'before', 'after', 'story'],
|
|
295
|
+
description: '空或省略=不适用;依赖 stage_code,多篇时消歧:before=行动前,after=行动后,story=幕间;单篇可省略' },
|
|
305
296
|
character_name: { type: 'string', description: '角色展示名;与 record_name 或 material 配合' },
|
|
306
|
-
record_name: { type: 'string', description: '
|
|
307
|
-
segment: { type: 'integer', description: '
|
|
308
|
-
material: { type: 'string', enum: ['profile', 'module', 'voice', 'skin', 'recruitment', 'potential'],
|
|
309
|
-
description: '
|
|
310
|
-
game: { type: 'string', enum: ['arknights', 'endfield'],
|
|
311
|
-
description: '
|
|
312
|
-
activity_name: { type: 'string', description: '
|
|
313
|
-
collection_name: { type: 'string', description: '
|
|
297
|
+
record_name: { type: 'string', description: '干员密录名;须配 character_name,多段时加 segment' },
|
|
298
|
+
segment: { type: 'integer', description: '依赖 record_name 的密录段号,从 1 开始;其他定位请省略' },
|
|
299
|
+
material: { type: 'string', enum: ['', 'profile', 'module', 'voice', 'skin', 'recruitment', 'potential'],
|
|
300
|
+
description: '空或省略=不适用;依赖 character_name,读官方角色资料:档案/模组/语音/时装/招聘合同/信物。两款游戏通用(终末地如 {"character_name":"管理员","material":"profile"});活动、组织等实体资料用搜索返回的 title 读取' },
|
|
301
|
+
game: { type: 'string', enum: ['', 'arknights', 'endfield'],
|
|
302
|
+
description: '空或省略=不适用;依赖 character_name+material,仅角色资料同名时限定游戏' },
|
|
303
|
+
activity_name: { type: 'string', description: '明日方舟活动展示名,配 mode=activity' },
|
|
304
|
+
collection_name: { type: 'string', description: '终末地任务集合展示名,配 mode=collection' },
|
|
314
305
|
content_types: { type: 'array', items: { type: 'string', enum: END_FIELD_STORY_CONTENT_TYPES },
|
|
315
|
-
description: '
|
|
316
|
-
line: { type: 'integer', description: 'around
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
306
|
+
description: '终末地集合的内容形式过滤;续页保留' },
|
|
307
|
+
line: { type: 'integer', description: '上下文中心行号(around 模式,默认前后各 3 行);mode=document 不接受 line,连续读取请用 start_line' },
|
|
308
|
+
start_line: { type: 'integer', description: '连续读取的起始行号,仅配 mode=document;page.continuation 里的续页参数就是它' },
|
|
309
|
+
position: { type: 'integer', description: '合集续读位置,从 page.continuation 复制' },
|
|
310
|
+
mode: { type: 'string', enum: ['', 'document', 'activity', 'collection'],
|
|
311
|
+
description: '空或省略=不适用;document=单篇全文,activity=活动,collection=任务集合;document_uid、关卡、密录和角色资料默认单篇全文;显式 mode=document 全文优先于合法 section,并返回提示' },
|
|
312
|
+
section: { type: 'string', enum: ['', ...WIKI_SECTION_VALUES], description: '空或省略=不适用;配 title 或 document_uid 读取 Wiki 标签字段;并非每篇都有全部字段,不确定时用 mode=document;显式全文优先于合法 section,并返回提示' },
|
|
313
|
+
before: { type: 'integer', description: '前文行数,默认 3,上限 100' },
|
|
314
|
+
after: { type: 'integer', description: '后文行数,默认 3,上限 100' },
|
|
315
|
+
cursor: { type: 'string', description: '仅兼容旧会话游标' },
|
|
316
|
+
data_version: { type: 'string', description: '续页调用给出的短版本号;可省略' },
|
|
317
|
+
max_lines: { type: 'integer', description: '输出行数上限,默认 100,最大 500' },
|
|
318
|
+
max_chars: { type: 'integer', description: '输出字符上限,默认 12000,最大 100000' },
|
|
319
|
+
citation: { type: 'string',
|
|
320
|
+
description: '检索结果给出的「句柄」,形如 doc_xxxxxxxxxxxxxxxx#L123 或 #L120-135;与 title/document_uid/line 等定位参数互斥,用于回到原文某几行' },
|
|
321
|
+
reuse: { type: 'boolean',
|
|
322
|
+
description: '默认 true:跳过"上文已可见"的行,只返回新增行。长会话或上文被压缩/折叠后看不到原文时传 false,强制重发本页全文' },
|
|
327
323
|
},
|
|
328
324
|
additionalProperties: false,
|
|
329
325
|
}
|
|
@@ -389,7 +385,8 @@ const READ_OUTPUT_SCHEMA = {
|
|
|
389
385
|
activity_name: { type: 'string' }, collection_name: { type: 'string' },
|
|
390
386
|
content_types: { type: 'array', items: { type: 'string', enum: END_FIELD_STORY_CONTENT_TYPES } },
|
|
391
387
|
mode: { type: 'string', enum: ['document', 'activity', 'collection'] },
|
|
392
|
-
|
|
388
|
+
start_line: { type: 'integer' }, position: { type: 'integer' },
|
|
389
|
+
data_version: { type: 'string' },
|
|
393
390
|
} }, { type: 'null' }] } } },
|
|
394
391
|
},
|
|
395
392
|
}
|
|
@@ -403,7 +400,7 @@ function projectReadToolValue(response, store) {
|
|
|
403
400
|
&& store.requiresDocumentUid?.(response.document?.document_id)) {
|
|
404
401
|
projected.page.continuation = {
|
|
405
402
|
document_uid: documentUid(response.document.document_id), mode: 'document',
|
|
406
|
-
|
|
403
|
+
start_line: continuation.start_line, data_version: continuation.data_version,
|
|
407
404
|
}
|
|
408
405
|
}
|
|
409
406
|
const uniqueStage = continuation.stage_code && continuation.story_part
|
|
@@ -416,7 +413,7 @@ function projectReadToolValue(response, store) {
|
|
|
416
413
|
&& (continuation.stage_code || continuation.record_name || continuation.material)
|
|
417
414
|
&& !uniqueStage && !uniqueRecord && !uniqueMaterial) {
|
|
418
415
|
projected.page.continuation = {
|
|
419
|
-
title: projected.primary.title, mode: 'document',
|
|
416
|
+
title: projected.primary.title, mode: 'document', start_line: continuation.start_line,
|
|
420
417
|
data_version: continuation.data_version,
|
|
421
418
|
}
|
|
422
419
|
}
|
|
@@ -474,12 +471,12 @@ function readPresentationMeta(_args, value) {
|
|
|
474
471
|
const TIMELINE_PARAMETERS = {
|
|
475
472
|
type: 'object', additionalProperties: false,
|
|
476
473
|
properties: {
|
|
477
|
-
query: { type: 'string', description: '
|
|
478
|
-
activity_names: { type: 'array', items: { type: 'string' }, description: '
|
|
479
|
-
entity_names: { type: 'array', items: { type: 'string' }, description: '
|
|
480
|
-
year_start: { type: 'integer', description: '
|
|
481
|
-
year_end: { type: 'integer', description: '
|
|
482
|
-
source_marker: { type: 'string', description: '
|
|
474
|
+
query: { type: 'string', description: '事件正文短语,最多 200 字符' },
|
|
475
|
+
activity_names: { type: 'array', items: { type: 'string' }, description: '活动展示名,最多 20 项' },
|
|
476
|
+
entity_names: { type: 'array', items: { type: 'string' }, description: '实体展示名,自动展开别名,最多 20 项' },
|
|
477
|
+
year_start: { type: 'integer', description: '起始年份,含边界' },
|
|
478
|
+
year_end: { type: 'integer', description: '结束年份,含边界' },
|
|
479
|
+
source_marker: { type: 'string', description: '反查来源时复制返回的年表出处标记' },
|
|
483
480
|
max_results: { type: 'integer', description: '最多返回事件数,默认 20,上限 100' },
|
|
484
481
|
},
|
|
485
482
|
}
|
|
@@ -487,66 +484,131 @@ const TIMELINE_PARAMETERS = {
|
|
|
487
484
|
const CLOUD_SEARCH_PARAMETERS = {
|
|
488
485
|
type: 'object', additionalProperties: false, required: ['query'],
|
|
489
486
|
properties: {
|
|
490
|
-
query: { type: 'string', description: '
|
|
487
|
+
query: { type: 'string', description: '包含主体、事件和所求关系的完整问题' },
|
|
491
488
|
games: { type: 'array', items: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
492
|
-
description: '
|
|
489
|
+
description: '游戏范围;省略时查询全部启用游戏' },
|
|
493
490
|
depth: { type: 'string', enum: ['fast', 'standard', 'deep'],
|
|
494
|
-
description: '
|
|
491
|
+
description: '兼容字段,不改变检索路线;通常省略' },
|
|
495
492
|
evidence_policy: { type: 'string', enum: ['mixed', 'original_only'],
|
|
496
|
-
description: 'mixed
|
|
493
|
+
description: '默认 mixed 混合检索,通常省略;original_only 仅检索官方剧情原文' },
|
|
497
494
|
options: {
|
|
498
495
|
type: 'object', additionalProperties: false,
|
|
499
|
-
description: '通常省略。只有用户记得一句原文大意且措辞可能不准时,选择官方剧情单句向量路线',
|
|
500
496
|
properties: {
|
|
501
497
|
search_intent: {
|
|
502
498
|
type: 'string', enum: ['single_sentence_search'],
|
|
503
|
-
description: '
|
|
499
|
+
description: '仅用于凭记忆找某一句官方原文出处;会关闭档案/Wiki/图谱/年表,只检索原文。综合分析、人物关系等问题不要填写',
|
|
504
500
|
},
|
|
505
501
|
},
|
|
506
502
|
},
|
|
507
503
|
},
|
|
508
504
|
}
|
|
509
505
|
|
|
506
|
+
// 与后端 models/schemas.py SourceType、models/retrieval.py CandidateStatus 保持一致。
|
|
507
|
+
const CLOUD_SOURCE_TYPES = ['entity_profile', 'entity_attachment', 'graph_entity', 'graph_relation',
|
|
508
|
+
'graph_event', 'csv_archive', 'csv_activity', 'csv_record', 'vector_original', 'vector_scene',
|
|
509
|
+
'vector_wiki', 'entity_attribute', 'travel_notes', 'classic_hit', 'timeline']
|
|
510
|
+
const CLOUD_CANDIDATE_STATUSES = ['retrieved', 'kept', 'filtered', 'discarded', 'error']
|
|
511
|
+
|
|
512
|
+
// 从 1 开始计数的整数字段:0 只可能是占位,按“省略”处理。
|
|
513
|
+
const TIMELINE_ZERO_FIELDS = ['max_results']
|
|
514
|
+
const CLOUD_INSPECT_ZERO_FIELDS = ['limit', 'event_sequence_from', 'event_sequence_to', 'content_max_chars']
|
|
515
|
+
|
|
516
|
+
// 只接受最无歧义的形式(T 分隔、Z 或 ±HH:MM 时区),避免本地放行而后端 422。
|
|
517
|
+
const AWARE_ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/u
|
|
518
|
+
|
|
519
|
+
/** cloud_inspect 本地预检:把后端 422 会拒绝的常见误用提前转成可执行的提示。 */
|
|
520
|
+
function validateCloudInspectArgs(args) {
|
|
521
|
+
for (const field of ['event_time_from', 'event_time_to']) {
|
|
522
|
+
if (args[field] !== undefined && !AWARE_ISO_DATETIME.test(String(args[field]).trim())) {
|
|
523
|
+
throw Object.assign(new Error(`${field} 必须是带时区的 ISO 8601 时间,例如 2025-01-01T08:00:00+08:00;不需要按时间过滤时请省略该字段`),
|
|
524
|
+
{ code: 'INVALID_REQUEST', retryable: false })
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (const field of ['event_sequence_from', 'event_sequence_to', 'limit', 'cursor', 'content_max_chars']) {
|
|
528
|
+
if (args[field] !== undefined && (!Number.isInteger(args[field]) || args[field] < (field === 'cursor' ? 0 : 1))) {
|
|
529
|
+
throw Object.assign(new Error(`${field} 必须是${field === 'cursor' ? '非负' : '正'}整数;不需要时请省略该字段`),
|
|
530
|
+
{ code: 'INVALID_REQUEST', retryable: false })
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
510
535
|
const CLOUD_INSPECT_PARAMETERS = {
|
|
511
536
|
type: 'object', additionalProperties: false,
|
|
512
|
-
description: '查看最近一次云端检索的指定区段;request_id 由运行时自动注入',
|
|
513
537
|
properties: {
|
|
514
538
|
games: { type: 'array', items: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
515
|
-
description: '
|
|
539
|
+
description: '游戏过滤;省略时查看全部启用游戏' },
|
|
516
540
|
section: { type: 'string', enum: ['summary', 'candidates', 'selected_sources', 'events', 'trace_steps', 'answer_context'],
|
|
517
541
|
description: '默认 summary' },
|
|
518
|
-
cursor: { type: 'integer', description: '
|
|
519
|
-
limit: { type: 'integer', description: '本页条数' },
|
|
520
|
-
channels: stringList('按渠道过滤(candidates/events
|
|
521
|
-
query_variants: stringList('
|
|
522
|
-
retrievers: stringList('
|
|
523
|
-
source_types: { type: 'array', items: { type: 'string'
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
event_sequence_from: { type: 'integer', description: '
|
|
529
|
-
event_sequence_to: { type: 'integer', description: '
|
|
530
|
-
event_time_from: { type: 'string', description: '
|
|
531
|
-
event_time_to: { type: 'string', description: '
|
|
542
|
+
cursor: { type: 'integer', description: '续页时复制返回的 next_cursor;首页省略' },
|
|
543
|
+
limit: { type: 'integer', description: '本页条数 1–200;通常省略' },
|
|
544
|
+
channels: stringList('按渠道过滤(candidates/events);通常省略'),
|
|
545
|
+
query_variants: stringList('按查询变体过滤;通常省略'),
|
|
546
|
+
retrievers: stringList('按检索器过滤;通常省略'),
|
|
547
|
+
source_types: { type: 'array', items: { type: 'string', enum: CLOUD_SOURCE_TYPES },
|
|
548
|
+
description: '按来源类型过滤;通常省略' },
|
|
549
|
+
statuses: { type: 'array', items: { type: 'string', enum: CLOUD_CANDIDATE_STATUSES },
|
|
550
|
+
description: '按候选状态过滤;通常省略' },
|
|
551
|
+
stages: stringList('按处理阶段过滤;通常省略'),
|
|
552
|
+
event_sequence_from: { type: 'integer', description: '仅 section=events:事件序号下界(含),从 1 开始' },
|
|
553
|
+
event_sequence_to: { type: 'integer', description: '仅 section=events:事件序号上界(含),从 1 开始' },
|
|
554
|
+
event_time_from: { type: 'string', description: '仅 section=events:带时区的 ISO 8601 时间,如 2025-01-01T08:00:00+08:00' },
|
|
555
|
+
event_time_to: { type: 'string', description: '仅 section=events:带时区的 ISO 8601 时间' },
|
|
532
556
|
content_mode: { type: 'string', enum: ['none', 'preview', 'full'],
|
|
533
|
-
description: 'none
|
|
534
|
-
content_max_chars: { type: 'integer', description: '正文字符上限' },
|
|
557
|
+
description: 'none=无正文,preview=摘要(默认),full=本页全文' },
|
|
558
|
+
content_max_chars: { type: 'integer', description: '正文字符上限 100–100000;通常省略' },
|
|
535
559
|
},
|
|
536
560
|
}
|
|
537
561
|
|
|
538
562
|
/** ---- 模型扁平参数 → 版本化 wire contract ---- */
|
|
539
563
|
|
|
540
564
|
/** corpus_read:将模型使用的自然定位器转换为版本化读取契约。 */
|
|
565
|
+
/** modelReadToContract 推断出的默认值说明;契约对象本身不允许携带额外字段。 */
|
|
566
|
+
const contractNotes = new WeakMap()
|
|
567
|
+
|
|
568
|
+
function withContractNote(contract, notes) {
|
|
569
|
+
if (notes.length) contractNotes.set(contract, notes.join('\n'))
|
|
570
|
+
return contract
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 明日方舟“活动”(activity_name)与终末地“任务集合”(collection_name)是同一种连续
|
|
575
|
+
* 阅读入口,只是两款游戏的叫法不同。字段填反时,若名称只在另一款游戏中存在,就按
|
|
576
|
+
* 实际资料改道并说明,而不是报“找不到”。
|
|
577
|
+
*/
|
|
578
|
+
function streamExists(lookup) {
|
|
579
|
+
try { return lookup().length > 0 } catch { return true }
|
|
580
|
+
}
|
|
581
|
+
function rerouteStreamLocator(args, store, enabledGames, notes) {
|
|
582
|
+
const swap = (from, to, mode, game) => {
|
|
583
|
+
const name = String(args[from] ?? '').trim()
|
|
584
|
+
if (!name || args[to] !== undefined || args.document_uid !== undefined || !enabledGames.includes(game)) return null
|
|
585
|
+
const own = from === 'activity_name'
|
|
586
|
+
? enabledGames.includes('arknights') && streamExists(() => store.activityStoryDocuments({ activityName: name }))
|
|
587
|
+
: enabledGames.includes('endfield') && streamExists(() => store.endfieldCollectionDocuments({ collectionName: name }))
|
|
588
|
+
if (own) return null
|
|
589
|
+
const other = to === 'collection_name'
|
|
590
|
+
? streamExists(() => store.endfieldCollectionDocuments({ collectionName: name }))
|
|
591
|
+
: streamExists(() => store.activityStoryDocuments({ activityName: name }))
|
|
592
|
+
if (!other) return null
|
|
593
|
+
const { [from]: _drop, ...rest } = args
|
|
594
|
+
notes.push(`定位改道:“${name}”是${game === 'endfield' ? '终末地任务集合' : '明日方舟活动'},已按 ${to}+mode="${mode}" 读取。`)
|
|
595
|
+
return { ...rest, [to]: name, ...(args.mode !== undefined ? { mode } : {}) }
|
|
596
|
+
}
|
|
597
|
+
return swap('activity_name', 'collection_name', 'collection', 'endfield')
|
|
598
|
+
|| swap('collection_name', 'activity_name', 'activity', 'arknights')
|
|
599
|
+
|| args
|
|
600
|
+
}
|
|
601
|
+
|
|
541
602
|
async function modelReadToContract(args = {}, store, enabledGames = ['arknights', 'endfield']) {
|
|
603
|
+
const notes = []
|
|
542
604
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
543
605
|
throw Object.assign(new Error('读取参数必须是对象'), { code: 'INVALID_REQUEST' })
|
|
544
606
|
}
|
|
545
607
|
let expectedDataVersion
|
|
546
608
|
if (args.data_version !== undefined) {
|
|
547
609
|
expectedDataVersion = String(args.data_version)
|
|
548
|
-
if (!
|
|
549
|
-
throw Object.assign(new Error('data_version
|
|
610
|
+
if (!DATA_VERSION_REF_PATTERN.test(expectedDataVersion)) {
|
|
611
|
+
throw Object.assign(new Error('data_version 必须是续页结果给出的资料版本(至少 8 位小写十六进制,可省略)'),
|
|
550
612
|
{ code: 'INVALID_REQUEST' })
|
|
551
613
|
}
|
|
552
614
|
}
|
|
@@ -612,24 +674,66 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
612
674
|
}
|
|
613
675
|
const allowed = new Set(['title', 'document_uid', 'stage_code', 'story_part', 'character_name', 'record_name',
|
|
614
676
|
'segment', 'material', 'game', 'activity_name', 'collection_name', 'content_types',
|
|
615
|
-
'line', 'position', 'mode', 'section', 'before', 'after', 'max_lines', 'max_chars',
|
|
616
|
-
'data_version'])
|
|
677
|
+
'line', 'start_line', 'position', 'mode', 'section', 'before', 'after', 'max_lines', 'max_chars',
|
|
678
|
+
'reuse', 'citation', 'data_version'])
|
|
617
679
|
if (Object.keys(args).some((key) => !allowed.has(key))) {
|
|
618
680
|
throw Object.assign(new Error('corpus_read 包含不支持的参数'), { code: 'INVALID_REQUEST' })
|
|
619
681
|
}
|
|
682
|
+
args = rerouteStreamLocator(args, store, enabledGames, notes)
|
|
683
|
+
|
|
684
|
+
// 句柄是检索结果发出的重新取用凭据:展开成 document_uid + 起始行 + 行数,
|
|
685
|
+
// 不让 Agent 为了拿 uid 先跑一次搜索。
|
|
686
|
+
if (args.citation !== undefined) {
|
|
687
|
+
const parsed = parseCitation(args.citation)
|
|
688
|
+
if (!parsed) {
|
|
689
|
+
throw Object.assign(new Error(
|
|
690
|
+
'citation 格式应为 doc_<16位定位>#L<行号> 或 doc_<16位定位>#L<起始行>-<结束行>;请原样复制检索结果里的「句柄」。',
|
|
691
|
+
), { code: 'INVALID_REQUEST' })
|
|
692
|
+
}
|
|
693
|
+
const conflicts = ['title', 'document_uid', 'line', 'start_line', 'stage_code', 'story_part',
|
|
694
|
+
'activity_name', 'collection_name', 'character_name', 'record_name', 'material']
|
|
695
|
+
.filter((key) => args[key] !== undefined)
|
|
696
|
+
if (conflicts.length) {
|
|
697
|
+
throw Object.assign(new Error(
|
|
698
|
+
`citation 已包含篇章与行号,不能再与 ${conflicts.join('、')} 同时提交。`,
|
|
699
|
+
), { code: 'INVALID_REQUEST' })
|
|
700
|
+
}
|
|
701
|
+
delete args.citation
|
|
702
|
+
args.document_uid = parsed.documentUid
|
|
703
|
+
args.mode = 'document'
|
|
704
|
+
args.start_line = parsed.startLine
|
|
705
|
+
args.max_lines = parsed.endLine - parsed.startLine + 1
|
|
706
|
+
}
|
|
620
707
|
|
|
621
708
|
const hasTitle = args.title !== undefined
|
|
622
709
|
const hasDocumentUid = args.document_uid !== undefined
|
|
623
|
-
const hasStageLocator = args.stage_code !== undefined
|
|
624
|
-
const hasRecordLocator = args.record_name !== undefined
|
|
625
|
-
const hasMaterialLocator = args.material !== undefined
|
|
710
|
+
const hasStageLocator = args.stage_code !== undefined
|
|
711
|
+
const hasRecordLocator = args.record_name !== undefined
|
|
712
|
+
const hasMaterialLocator = args.character_name !== undefined && args.material !== undefined
|
|
626
713
|
const hasActivityLocator = args.activity_name !== undefined
|
|
627
714
|
const hasCollectionLocator = args.collection_name !== undefined
|
|
628
|
-
const locatorCount =
|
|
629
|
-
hasActivityLocator, hasCollectionLocator].filter(Boolean).length
|
|
715
|
+
const locatorCount = readTargets(args).length
|
|
630
716
|
if (locatorCount !== 1) {
|
|
717
|
+
throw Object.assign(new Error(readTargetError(args)), { code: 'INVALID_REQUEST' })
|
|
718
|
+
}
|
|
719
|
+
if (args.story_part !== undefined && !hasStageLocator) {
|
|
720
|
+
throw Object.assign(new Error('附属参数 story_part 依赖 stage_code;关卡读取请仅提交 {"stage_code":"15-17","story_part":"after"},其他定位请省略 story_part'),
|
|
721
|
+
{ code: 'INVALID_REQUEST' })
|
|
722
|
+
}
|
|
723
|
+
if (args.segment !== undefined && !hasRecordLocator) {
|
|
724
|
+
throw Object.assign(new Error('附属参数 segment 依赖 record_name;密录读取请仅提交 {"character_name":"角色名","record_name":"密录名","segment":1},其他定位请省略 segment'),
|
|
725
|
+
{ code: 'INVALID_REQUEST' })
|
|
726
|
+
}
|
|
727
|
+
if (args.material !== undefined && args.character_name === undefined) {
|
|
728
|
+
throw Object.assign(new Error('附属参数 material 依赖 character_name;角色资料读取请仅提交 {"character_name":"角色名","material":"profile"},其他定位请省略 material'),
|
|
729
|
+
{ code: 'INVALID_REQUEST' })
|
|
730
|
+
}
|
|
731
|
+
// Validate before either the single-document or collection lookup, so a
|
|
732
|
+
// display title in this field is not mistaken for missing corpus data.
|
|
733
|
+
if (hasDocumentUid && (typeof args.document_uid !== 'string'
|
|
734
|
+
|| !/^doc_[A-Za-z0-9_-]{16}$/u.test(args.document_uid.trim()))) {
|
|
631
735
|
throw Object.assign(new Error(
|
|
632
|
-
'
|
|
736
|
+
'document_uid 必须是检索结果返回的 doc_ 定位(doc_ 后为 16 位字母、数字、_ 或 -),不能填写展示标题;展示标题请改填 title,例如 {"title":"完整展示标题","mode":"document"}。不要同时提交 title 和 document_uid。',
|
|
633
737
|
), { code: 'INVALID_REQUEST' })
|
|
634
738
|
}
|
|
635
739
|
if (args.character_name !== undefined && !hasRecordLocator && !hasMaterialLocator) {
|
|
@@ -660,8 +764,11 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
660
764
|
const expectedMode = hasActivityLocator ? 'activity'
|
|
661
765
|
: hasCollectionLocator ? 'collection' : args.mode
|
|
662
766
|
if (args.mode !== undefined && args.mode !== expectedMode) {
|
|
663
|
-
|
|
664
|
-
{
|
|
767
|
+
if (uidStreamMode || args.mode !== (expectedMode === 'activity' ? 'collection' : 'activity')) {
|
|
768
|
+
throw Object.assign(new Error(`${hasActivityLocator ? 'activity_name' : 'collection_name'} 必须使用 mode="${expectedMode}"`),
|
|
769
|
+
{ code: 'INVALID_REQUEST' })
|
|
770
|
+
}
|
|
771
|
+
notes.push(`读取方式:${hasActivityLocator ? 'activity_name' : 'collection_name'} 对应 mode="${expectedMode}",已自动改用。`)
|
|
665
772
|
}
|
|
666
773
|
if (args.line !== undefined || args.section !== undefined || args.before !== undefined
|
|
667
774
|
|| args.after !== undefined) {
|
|
@@ -702,7 +809,7 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
702
809
|
} catch (error) {
|
|
703
810
|
throw Object.assign(new Error(error.message), { code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
704
811
|
}
|
|
705
|
-
return {
|
|
812
|
+
return withContractNote({
|
|
706
813
|
locator: hasDocumentUid ? { document_uid: String(args.document_uid).trim() }
|
|
707
814
|
: { [hasActivityLocator ? 'activity_name' : 'collection_name']: name },
|
|
708
815
|
selection: { mode: expectedMode, cursor: null,
|
|
@@ -711,7 +818,7 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
711
818
|
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}),
|
|
712
819
|
limits: { ...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
713
820
|
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}) },
|
|
714
|
-
}
|
|
821
|
+
}, notes)
|
|
715
822
|
}
|
|
716
823
|
|
|
717
824
|
let locator
|
|
@@ -738,8 +845,35 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
738
845
|
try {
|
|
739
846
|
record = await store.getDocumentByStoryStage(stageCode, storyPart)
|
|
740
847
|
} catch (error) {
|
|
741
|
-
|
|
742
|
-
|
|
848
|
+
// 同一关卡有行动前/行动后两篇时,未指定 story_part 默认先读行动前,
|
|
849
|
+
// 并提示另一篇的读法,而不是直接报错让调用方重试。
|
|
850
|
+
let fallback = null
|
|
851
|
+
if (!storyPart && error.code === 'DOCUMENT_AMBIGUOUS') {
|
|
852
|
+
for (const part of ['before', 'story', 'after']) {
|
|
853
|
+
try {
|
|
854
|
+
fallback = await store.getDocumentByStoryStage(stageCode, part)
|
|
855
|
+
} catch {
|
|
856
|
+
fallback = null
|
|
857
|
+
}
|
|
858
|
+
if (fallback) {
|
|
859
|
+
const others = []
|
|
860
|
+
for (const other of ['before', 'after', 'story'].filter((item) => item !== part)) {
|
|
861
|
+
try {
|
|
862
|
+
if (await store.getDocumentByStoryStage(stageCode, other)) others.push(other)
|
|
863
|
+
} catch { /* 其余部分仍有歧义时不提示 */ }
|
|
864
|
+
}
|
|
865
|
+
const labels = { before: '行动前', after: '行动后', story: '纯剧情/幕间' }
|
|
866
|
+
notes.push(`关卡定位:${stageCode} 有多篇剧情,未指定 story_part,已默认读取${labels[part]}。`
|
|
867
|
+
+ (others.length ? `另有${others.map((item) => `${labels[item]}(story_part="${item}")`).join('、')}。` : ''))
|
|
868
|
+
break
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (!fallback) {
|
|
873
|
+
throw Object.assign(new Error(error.message),
|
|
874
|
+
{ code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
875
|
+
}
|
|
876
|
+
record = fallback
|
|
743
877
|
}
|
|
744
878
|
if (!record) {
|
|
745
879
|
const partLabel = storyPart
|
|
@@ -805,15 +939,24 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
805
939
|
if (!title) throw Object.assign(new Error('title 不能为空'), { code: 'INVALID_REQUEST' })
|
|
806
940
|
const record = await store.getDocumentByTitle(title)
|
|
807
941
|
requireEnabled(record)
|
|
942
|
+
if (record) {
|
|
943
|
+
const resolvedTitle = naturalDocumentTitle(record.record.document)
|
|
944
|
+
const exact = (store.naturalTitleIndex?.get(title)?.length || store.titleIndex?.get(title)?.length)
|
|
945
|
+
if (!exact && resolvedTitle && resolvedTitle !== title) {
|
|
946
|
+
notes.push(`标题匹配:未找到完全一致的标题,已按宽松匹配读取《${resolvedTitle}》。`)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
808
949
|
locator = { display_title: title }
|
|
809
950
|
}
|
|
810
951
|
const section = String(args.section ?? '').trim()
|
|
811
|
-
if (section && !hasTitle) {
|
|
812
|
-
throw Object.assign(new Error('section 只能与 title 定位器一起使用'), { code: 'INVALID_REQUEST' })
|
|
952
|
+
if (section && !hasTitle && !hasDocumentUid) {
|
|
953
|
+
throw Object.assign(new Error('section 只能与 title 或 document_uid 定位器一起使用'), { code: 'INVALID_REQUEST' })
|
|
813
954
|
}
|
|
814
955
|
const naturalDocumentLocator = hasDocumentUid || hasStageLocator || hasRecordLocator || hasMaterialLocator
|
|
956
|
+
// 只给 title 时默认从头读全文(受 max_lines/max_chars 限制并给出续页),
|
|
957
|
+
// 与关卡/密录/角色资料等其他直达定位保持一致。
|
|
815
958
|
const mode = args.mode || (section ? 'section' : Number.isInteger(args.line) ? 'around'
|
|
816
|
-
: naturalDocumentLocator ? 'document' : '')
|
|
959
|
+
: naturalDocumentLocator || hasTitle ? 'document' : '')
|
|
817
960
|
if (!mode) throw Object.assign(new Error(
|
|
818
961
|
'请提供 line、section 或 mode="document";max_lines/max_chars 只限制输出量,不能代替读取方式',
|
|
819
962
|
), { code: 'INVALID_REQUEST' })
|
|
@@ -830,34 +973,42 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
830
973
|
if (section && mode !== 'section') {
|
|
831
974
|
throw Object.assign(new Error('section 只能与 mode=section 一起使用'), { code: 'INVALID_REQUEST' })
|
|
832
975
|
}
|
|
976
|
+
// line 只表示"中心行"(around ±3)。它与全文起点共用过同一字段,导致照着续读提示
|
|
977
|
+
// 传 line 时落回 ±3 窗口、永远前进不到下一页;这里把歧义变成显式失败。
|
|
978
|
+
// 例外:line=1 与"从第 1 行读起"同义,是客户端补齐默认值时常见的无害占位,放行。
|
|
979
|
+
if (mode === 'document' && args.line !== undefined && Number(args.line) > 1) {
|
|
980
|
+
throw Object.assign(new Error(
|
|
981
|
+
'mode="document" 不接受 line(line 是上下文中心行号,around 模式默认前后各 3 行)。'
|
|
982
|
+
+ `连续读取请改用 start_line,例如 ${JSON.stringify({ start_line: args.line, mode: 'document' })}。`,
|
|
983
|
+
), { code: 'INVALID_REQUEST' })
|
|
984
|
+
}
|
|
985
|
+
if (mode !== 'document' && args.start_line !== undefined) {
|
|
986
|
+
throw Object.assign(new Error(
|
|
987
|
+
'start_line 只用于 mode="document" 的连续读取;查看某一行上下文请改用 line(around 模式)。',
|
|
988
|
+
), { code: 'INVALID_REQUEST' })
|
|
989
|
+
}
|
|
833
990
|
const selection = mode === 'document'
|
|
834
|
-
? { mode, cursor: null, ...(args.
|
|
991
|
+
? { mode, cursor: null, ...(args.start_line !== undefined ? { start_line: args.start_line } : {}) }
|
|
835
992
|
: mode === 'section'
|
|
836
993
|
? { mode, section }
|
|
837
994
|
: { mode: 'around', center_line: args.line,
|
|
838
995
|
...(args.before !== undefined ? { before_lines: args.before } : {}),
|
|
839
996
|
...(args.after !== undefined ? { after_lines: args.after } : {}) }
|
|
840
|
-
return {
|
|
997
|
+
return withContractNote({
|
|
841
998
|
locator, selection,
|
|
842
999
|
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}),
|
|
843
1000
|
limits: { ...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
844
1001
|
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}) },
|
|
845
|
-
}
|
|
1002
|
+
}, notes)
|
|
846
1003
|
}
|
|
847
1004
|
|
|
848
1005
|
/** ---- 云端渲染(output.render 用,压缩为模型可读文本) ---- */
|
|
849
1006
|
|
|
850
|
-
function renderCloudSearch(
|
|
851
|
-
|
|
852
|
-
return [{ type: 'text', text: `[cloud_search:error] ${value.error.code}: ${value.error.message}` }]
|
|
853
|
-
}
|
|
854
|
-
return [{ type: 'text', text: projectCloudSearch(value) }]
|
|
1007
|
+
function renderCloudSearch(args, value) {
|
|
1008
|
+
return [{ type: 'text', text: projectCloudSearch(value, { args }) }]
|
|
855
1009
|
}
|
|
856
1010
|
|
|
857
1011
|
function renderCloudInspect(_args, value) {
|
|
858
|
-
if (value.status === 'error') {
|
|
859
|
-
return [{ type: 'text', text: `[cloud_inspect:error] ${value.error.code}: ${value.error.message}` }]
|
|
860
|
-
}
|
|
861
1012
|
return [{ type: 'text', text: JSON.stringify(projectCloudInspect(value)) }]
|
|
862
1013
|
}
|
|
863
1014
|
|
|
@@ -868,7 +1019,7 @@ function renderCloudInspect(_args, value) {
|
|
|
868
1019
|
* @param {{
|
|
869
1020
|
* releasesDir?: string,
|
|
870
1021
|
* cacheShards?: number,
|
|
871
|
-
* uiSkin?: 'harness' | 'prts-agent',
|
|
1022
|
+
* uiSkin?: 'harness' | 'prts-agent' | 'endfield-aic' | 'rhine-lab',
|
|
872
1023
|
* download?: { order?: ('modelscope'|'site')[], siteBaseUrl?: string },
|
|
873
1024
|
* cloud?: { baseUrl?: string, game?: 'arknights' | 'endfield', userId?: string, token?: string, timeoutMs?: number, maxResponseBytes?: number },
|
|
874
1025
|
* }} [config]
|
|
@@ -958,6 +1109,25 @@ export async function apply(ctx, config = {}) {
|
|
|
958
1109
|
installArchivePtcReceipts(toolCtx, readPresentationMeta)
|
|
959
1110
|
// Host 的 preset 准入已经完成初始化;工具挂载不再触发下载或全量扫描。
|
|
960
1111
|
applyEntityRecognition(toolCtx, store, shared)
|
|
1112
|
+
tools.register({
|
|
1113
|
+
name: 'corpus_navigate',
|
|
1114
|
+
description: NAVIGATION_DESCRIPTION,
|
|
1115
|
+
parameters: NAVIGATION_PARAMETERS,
|
|
1116
|
+
output: { schema: { type: 'object', additionalProperties: true }, render: renderNavigation },
|
|
1117
|
+
timeoutMs: 60_000,
|
|
1118
|
+
isConcurrencySafe: () => true,
|
|
1119
|
+
presentCall: (args) => ({ card: 'generic', kind: 'search',
|
|
1120
|
+
title: `终末地主线导航 · ${compactToolTitle(args?.query || '主线目录')}` }),
|
|
1121
|
+
execute: async (rawArgs, exec) => {
|
|
1122
|
+
await requireLocalCorpus(store)
|
|
1123
|
+
const response = await executeNavigation(store, dropEmptyPlaceholders(rawArgs), {
|
|
1124
|
+
signal: exec?.signal, allowedGames: shared.effective().enabledGames,
|
|
1125
|
+
})
|
|
1126
|
+
if (response.error) throw Object.assign(new Error(response.error.message), response.error)
|
|
1127
|
+
return response
|
|
1128
|
+
},
|
|
1129
|
+
})
|
|
1130
|
+
|
|
961
1131
|
tools.register({
|
|
962
1132
|
name: 'corpus_search',
|
|
963
1133
|
description: SEARCH_DESCRIPTION,
|
|
@@ -967,10 +1137,11 @@ export async function apply(ctx, config = {}) {
|
|
|
967
1137
|
isConcurrencySafe: () => true,
|
|
968
1138
|
presentCall: (args) => searchCallView('PRTS 本地资料', args,
|
|
969
1139
|
shared.effective().enabledGames),
|
|
970
|
-
execute: async (
|
|
1140
|
+
execute: async (rawArgs, exec) => {
|
|
971
1141
|
await requireLocalCorpus(store)
|
|
1142
|
+
const args = dropEmptyPlaceholders(rawArgs)
|
|
972
1143
|
const snapshot = corpusVersionSnapshot(store)
|
|
973
|
-
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1144
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
974
1145
|
const completedSearchCalls = evidenceState.completedSearchCalls
|
|
975
1146
|
const callId = String(exec?.callId || '')
|
|
976
1147
|
// 先做对象校验再做幂等哈希:JSON.stringify(undefined) 返回 undefined,
|
|
@@ -993,10 +1164,9 @@ export async function apply(ctx, config = {}) {
|
|
|
993
1164
|
const requestHash = createHash('sha256').update(JSON.stringify(scopedArgs)).digest('hex')
|
|
994
1165
|
if (callId && completedSearchCalls.has(callId)) {
|
|
995
1166
|
const cached = completedSearchCalls.get(callId)
|
|
996
|
-
if (cached.requestHash !== requestHash)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
error: { code: 'INVALID_REQUEST', message: 'callId 已绑定到另一个搜索请求', retryable: false },
|
|
1167
|
+
if (cached.requestHash !== requestHash) {
|
|
1168
|
+
throw Object.assign(new Error('callId 已绑定到另一个搜索请求'),
|
|
1169
|
+
{ code: 'INVALID_REQUEST', retryable: false })
|
|
1000
1170
|
}
|
|
1001
1171
|
assertCorpusVersion(store, snapshot)
|
|
1002
1172
|
const replay = structuredClone(cached.response)
|
|
@@ -1032,23 +1202,29 @@ export async function apply(ctx, config = {}) {
|
|
|
1032
1202
|
// 模型调用顺序独占执行并逐个提交结果。
|
|
1033
1203
|
isConcurrencySafe: () => false,
|
|
1034
1204
|
presentCall: (args) => readCallView(args, shared.effective().enabledGames),
|
|
1035
|
-
execute: async (
|
|
1205
|
+
execute: async (rawArgs, exec) => {
|
|
1036
1206
|
await requireLocalCorpus(store)
|
|
1207
|
+
let { args, guidance: argumentGuidance } = normalizeReadArguments(rawArgs,
|
|
1208
|
+
{ sections: WIKI_SECTION_VALUES })
|
|
1037
1209
|
const snapshot = corpusVersionSnapshot(store)
|
|
1038
|
-
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1210
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
1039
1211
|
let contract
|
|
1040
1212
|
try {
|
|
1041
1213
|
contract = await modelReadToContract(args, store, shared.effective().enabledGames)
|
|
1042
1214
|
assertCorpusVersion(store, snapshot)
|
|
1215
|
+
const inferred = contractNotes.get(contract)
|
|
1216
|
+
if (inferred) argumentGuidance = [argumentGuidance, inferred].filter(Boolean).join('\n')
|
|
1043
1217
|
} catch (error) {
|
|
1044
1218
|
assertCorpusVersion(store, snapshot)
|
|
1045
1219
|
throw Object.assign(new Error(error.message),
|
|
1046
1220
|
{ code: error.code || 'INVALID_REQUEST', retryable: error.retryable ?? false })
|
|
1047
1221
|
}
|
|
1048
|
-
if (contract.expected_data_version !== undefined
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1222
|
+
if (contract.expected_data_version !== undefined) {
|
|
1223
|
+
if (!dataVersionMatches(contract.expected_data_version, store.dataVersion)) {
|
|
1224
|
+
throw Object.assign(new Error('续页所属资料版本与当前激活版本不一致,请重新读取'),
|
|
1225
|
+
{ code: 'PACKAGE_VERSION_MISMATCH', retryable: true })
|
|
1226
|
+
}
|
|
1227
|
+
contract.expected_data_version = store.dataVersion
|
|
1052
1228
|
}
|
|
1053
1229
|
// 证据覆盖按 Harness Agent 隔离;模型不感知 intent_id。
|
|
1054
1230
|
contract.intent_id = evidenceState.intentId
|
|
@@ -1056,12 +1232,14 @@ export async function apply(ctx, config = {}) {
|
|
|
1056
1232
|
normalizeReadRequest(contract)
|
|
1057
1233
|
const requested = await resolveReadWindow(store, contract)
|
|
1058
1234
|
assertCorpusVersion(store, snapshot)
|
|
1059
|
-
|
|
1235
|
+
// reuse:false 时不参与"上文已可见"的判定:宿主 surface 与模型实际收到的内容
|
|
1236
|
+
// 一旦不一致(外部压缩、跨请求折叠),去重就会静默少给行,宁可重发。
|
|
1237
|
+
const visibleResults = args.reuse === false ? new Map() : visibleToolResults(exec?.agent)
|
|
1060
1238
|
if (coveredRead(evidenceState, requested, visibleResults)) {
|
|
1061
1239
|
const replay = replayCoveredRead(evidenceState, requested, contract)
|
|
1062
1240
|
if (replay) {
|
|
1063
1241
|
assertCorpusVersion(store, snapshot)
|
|
1064
|
-
return projectReadToolValue(replay, store)
|
|
1242
|
+
return withReadGuidance(projectReadToolValue(replay, store), argumentGuidance)
|
|
1065
1243
|
}
|
|
1066
1244
|
}
|
|
1067
1245
|
const response = await executeRead(store, contract, { signal: exec?.signal })
|
|
@@ -1073,7 +1251,7 @@ export async function apply(ctx, config = {}) {
|
|
|
1073
1251
|
}, visibleResults) : null
|
|
1074
1252
|
const delivered = reuseReadCoverage(response, coveragePlan)
|
|
1075
1253
|
rememberRead(evidenceState, delivered, { callId: String(exec?.callId || ''), store })
|
|
1076
|
-
return projectReadToolValue(delivered, store)
|
|
1254
|
+
return withReadGuidance(projectReadToolValue(delivered, store), argumentGuidance)
|
|
1077
1255
|
},
|
|
1078
1256
|
})
|
|
1079
1257
|
|
|
@@ -1112,7 +1290,8 @@ export async function apply(ctx, config = {}) {
|
|
|
1112
1290
|
throw Object.assign(new Error('明日方舟资料当前未启用'), { code: 'INVALID_REQUEST' })
|
|
1113
1291
|
}
|
|
1114
1292
|
const snapshot = corpusVersionSnapshot(store)
|
|
1115
|
-
const response = await executeTimelineSearch(store,
|
|
1293
|
+
const response = await executeTimelineSearch(store,
|
|
1294
|
+
dropEmptyPlaceholders(args, { zeroFields: TIMELINE_ZERO_FIELDS }), { signal: exec?.signal })
|
|
1116
1295
|
assertCorpusVersion(store, snapshot)
|
|
1117
1296
|
return rememberArchivePresentation(response, timelineArchivePresentation(response))
|
|
1118
1297
|
},
|
|
@@ -1146,9 +1325,10 @@ export async function apply(ctx, config = {}) {
|
|
|
1146
1325
|
timeoutMs: 180_000,
|
|
1147
1326
|
isConcurrencySafe: () => true,
|
|
1148
1327
|
presentCall: (args) => searchCallView('PRTS 云端资料', args, c.enabledGames),
|
|
1149
|
-
execute: async (
|
|
1328
|
+
execute: async (rawArgs, exec) => {
|
|
1150
1329
|
try {
|
|
1151
|
-
const
|
|
1330
|
+
const args = dropEmptyPlaceholders(rawArgs)
|
|
1331
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
1152
1332
|
const cloud = cloudClients.forExecution(exec)
|
|
1153
1333
|
await cloud.capabilities({ signal: exec?.signal })
|
|
1154
1334
|
const requestedGames = Array.isArray(args.games) ? args.games : c.enabledGames
|
|
@@ -1164,7 +1344,8 @@ export async function apply(ctx, config = {}) {
|
|
|
1164
1344
|
rememberCloudMappings(evidenceState, enriched)
|
|
1165
1345
|
return rememberArchivePresentation(enriched, cloudArchivePresentation(enriched, { dataVersion }))
|
|
1166
1346
|
} catch (error) {
|
|
1167
|
-
|
|
1347
|
+
// 抛出交给宿主标记 isError,让 Agent 明确知道本次调用失败。
|
|
1348
|
+
throw cloudFaultError(error, 'cloud_search')
|
|
1168
1349
|
}
|
|
1169
1350
|
},
|
|
1170
1351
|
})
|
|
@@ -1180,9 +1361,11 @@ export async function apply(ctx, config = {}) {
|
|
|
1180
1361
|
presentCall: (args) => ({ card: 'generic',
|
|
1181
1362
|
title: `查看 PRTS 云端检索详情 · ${presentedGames(args, c.enabledGames)}`,
|
|
1182
1363
|
kind: 'read', rawInput: compactToolTitle(args?.section || 'summary') }),
|
|
1183
|
-
execute: async (
|
|
1364
|
+
execute: async (rawArgs, exec) => {
|
|
1184
1365
|
try {
|
|
1185
|
-
const
|
|
1366
|
+
const args = dropEmptyPlaceholders(rawArgs, { zeroFields: CLOUD_INSPECT_ZERO_FIELDS })
|
|
1367
|
+
validateCloudInspectArgs(args)
|
|
1368
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
1186
1369
|
const cloud = cloudClients.forExecution(exec)
|
|
1187
1370
|
await cloud.capabilities({ signal: exec?.signal })
|
|
1188
1371
|
const payload = { ...args,
|
|
@@ -1195,7 +1378,7 @@ export async function apply(ctx, config = {}) {
|
|
|
1195
1378
|
inspect: true, dataVersion: store.dataVersion,
|
|
1196
1379
|
}))
|
|
1197
1380
|
} catch (error) {
|
|
1198
|
-
|
|
1381
|
+
throw cloudFaultError(error, 'cloud_inspect')
|
|
1199
1382
|
}
|
|
1200
1383
|
},
|
|
1201
1384
|
})
|
|
@@ -1217,7 +1400,7 @@ export async function apply(ctx, config = {}) {
|
|
|
1217
1400
|
const handle = acquireInvestigationStore(investigationCtx.storageDomain)
|
|
1218
1401
|
shared.investigations = handle.service
|
|
1219
1402
|
investigationCtx.effect(() => () => { if (shared.investigations === handle.service) shared.investigations = null; return handle.release() }, 'prts: investigations')
|
|
1220
|
-
if (enableTools) await investigationCtx.inject(['tools', 'systemPrompt'], toolCtx => mountInvestigationTools(toolCtx, handle.service))
|
|
1403
|
+
if (enableTools) await investigationCtx.inject(['tools', 'systemPrompt'], toolCtx => mountInvestigationTools(toolCtx, handle.service, shared))
|
|
1221
1404
|
})
|
|
1222
1405
|
if (enableTools) await ctx.inject(['tools', 'systemPrompt'], async toolCtx => {
|
|
1223
1406
|
mountWebGuidance(toolCtx)
|
|
@@ -1233,7 +1416,8 @@ export async function apply(ctx, config = {}) {
|
|
|
1233
1416
|
storeEntry.releaseTimer = null
|
|
1234
1417
|
try {
|
|
1235
1418
|
const pointer = await readCurrentReleasePointer(releasesDir)
|
|
1236
|
-
if (store.loaded && store.releaseId === pointer.release_id
|
|
1419
|
+
if (store.loaded && store.releaseId === pointer.release_id
|
|
1420
|
+
&& store.installationId === (pointer.installation_id ?? null)) return
|
|
1237
1421
|
} catch { /* 让 ready() 输出真实指针错误 */ }
|
|
1238
1422
|
store.reset()
|
|
1239
1423
|
prepareEntityRecognition(store).catch((error) => {
|