prts-terrarchive 0.2.2 → 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 +52 -18
- package/README.md +49 -16
- package/bin/build-mainline-catalog.py +195 -0
- package/contracts/corpus_i18n_v1.schema.json +1 -3
- package/contracts/corpus_tools_v1.schema.json +30 -1
- package/docs/endfield-mainline-navigation.md +45 -0
- package/lib/client.js +154 -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/resources/endfield-mainline.json +4632 -0
- package/skills/prts-investigation/SKILL.md +1 -1
- package/skills/prts-investigation/references/board-guide.md +1 -1
- package/skills/prts-retrieval/SKILL.md +2 -0
- 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 +70 -15
- package/src/index.js +304 -98
- package/src/installer.js +86 -14
- package/src/investigation-tools.js +7 -4
- 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/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,9 +113,9 @@ async function requireLocalCorpus(store) {
|
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
const READ_DESCRIPTION = '读取 PRTS.chat 本地资料,支持行号上下文、Wiki 字段和单篇/合集连续阅读。只选一种主定位方式;续页原样提交 page.continuation
|
|
116
|
+
const READ_DESCRIPTION = '读取 PRTS.chat 本地资料,支持行号上下文、Wiki 字段和单篇/合集连续阅读。只选一种主定位方式;续页原样提交 page.continuation。最小 UID 全文调用:{"document_uid":"检索返回的 doc_ 定位","mode":"document"};不要填写不适用的附属参数。'
|
|
114
117
|
|
|
115
|
-
const SEARCH_DESCRIPTION = '搜索 PRTS.chat 本地语料,返回按文档归并的命中行及上下各一行;省略 query 可列目录。过滤数组内 OR、字段间 AND
|
|
118
|
+
const SEARCH_DESCRIPTION = '搜索 PRTS.chat 本地语料,返回按文档归并的命中行及上下各一行;省略 query 可列目录。过滤数组内 OR、字段间 AND。终末地版本、最新主线与章节顺序先用 corpus_navigate 定位。'
|
|
116
119
|
|
|
117
120
|
const TIMELINE_DESCRIPTION = '检索明日方舟活动时间线(PRTS Wiki《泰拉年表》),返回年份、事件与可反查的出处标记。'
|
|
118
121
|
|
|
@@ -177,16 +180,16 @@ const SEARCH_PARAMETERS = {
|
|
|
177
180
|
description: '内容形式' },
|
|
178
181
|
collection_names: stringList('活动或任务等上级集合展示名'),
|
|
179
182
|
character_names: stringList('资料归属角色的展示名'),
|
|
180
|
-
story_names: stringList('
|
|
181
|
-
activity_names: stringList('
|
|
183
|
+
story_names: stringList('单关/单篇剧情名,如“鳞的记忆”;活动名不要填这里,改用 activity_names'),
|
|
184
|
+
activity_names: stringList('活动或主线章节名,如“昨日海”'),
|
|
182
185
|
wiki_sections: { type: 'array', items: { type: 'string', enum: WIKI_SECTION_VALUES },
|
|
183
186
|
description: '限定 Wiki 标签字段' },
|
|
184
187
|
entity_names: stringList('只返回出现指定实体的行'),
|
|
185
|
-
speakers: stringList('
|
|
188
|
+
speakers: stringList('说话人展示名,仅匹配亲口台词;自动展开已登记别名'),
|
|
186
189
|
match_mode: { type: 'string', enum: ['literal', 'regex'],
|
|
187
190
|
description: '默认 literal 连续字面匹配;regex 为受限正则' },
|
|
188
191
|
context_terms: { type: 'array', items: { type: 'string' },
|
|
189
|
-
description: '
|
|
192
|
+
description: '强约束:每个词都须出现在命中行前后 3 行内(非整篇共现),最多 8 个;不确定时省略' },
|
|
190
193
|
after: { type: 'object', additionalProperties: false,
|
|
191
194
|
description: '原样复制 page.next_after,保留原 query 和过滤条件;切版后重新搜索',
|
|
192
195
|
required: ['data_version', 'resource_type', 'title', 'position'], properties: {
|
|
@@ -224,6 +227,7 @@ const SEARCH_OUTPUT_SCHEMA = {
|
|
|
224
227
|
line_start: { type: 'integer' }, line_end: { type: 'integer' },
|
|
225
228
|
match_kind: { type: 'string' }, evidence_kind: { type: 'string' },
|
|
226
229
|
citation: { type: 'string' },
|
|
230
|
+
citation_handle: { type: 'string' },
|
|
227
231
|
excerpt: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
228
232
|
required: ['line', 'role', 'line_type', 'speaker', 'text', 'truncated'],
|
|
229
233
|
properties: { line: { type: 'integer' },
|
|
@@ -259,6 +263,13 @@ const SEARCH_OUTPUT_SCHEMA = {
|
|
|
259
263
|
} }, { type: 'null' }] } } },
|
|
260
264
|
truncated: { type: 'boolean' },
|
|
261
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
|
+
} },
|
|
262
273
|
warnings: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
263
274
|
required: ['code', 'game', 'message'], properties: {
|
|
264
275
|
code: { type: 'string' }, game: { type: 'string', enum: ['arknights', 'endfield'] },
|
|
@@ -276,34 +287,39 @@ const SEARCH_OUTPUT_SCHEMA = {
|
|
|
276
287
|
const READ_PARAMETERS = {
|
|
277
288
|
type: 'object',
|
|
278
289
|
properties: {
|
|
279
|
-
title: { type: 'string', description: '
|
|
290
|
+
title: { type: 'string', description: '展示标题(可省略分隔符与 Wiki 序号后缀,找不到会列出候选);与 document_uid 互斥;可配 line、section 或 mode=document,单独提供时从第 1 行读全文' },
|
|
280
291
|
document_uid: { type: 'string',
|
|
281
|
-
description: '
|
|
292
|
+
description: '仅复制搜索返回的 doc_ 开头同名消歧定位,替代 title;不能填展示标题,标题请用 title;可配合集 mode' },
|
|
282
293
|
stage_code: { type: 'string', description: '明日方舟关卡代号,如 15-17、TW-ST-1' },
|
|
283
|
-
story_part: { type: 'string', enum: ['before', 'after', 'story'],
|
|
284
|
-
description: '
|
|
294
|
+
story_part: { type: 'string', enum: ['', 'before', 'after', 'story'],
|
|
295
|
+
description: '空或省略=不适用;依赖 stage_code,多篇时消歧:before=行动前,after=行动后,story=幕间;单篇可省略' },
|
|
285
296
|
character_name: { type: 'string', description: '角色展示名;与 record_name 或 material 配合' },
|
|
286
|
-
record_name: { type: 'string', description: '
|
|
287
|
-
segment: { type: 'integer', description: '
|
|
288
|
-
material: { type: 'string', enum: ['profile', 'module', 'voice', 'skin', 'recruitment', 'potential'],
|
|
289
|
-
description: '
|
|
290
|
-
game: { type: 'string', enum: ['arknights', 'endfield'],
|
|
291
|
-
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,仅角色资料同名时限定游戏' },
|
|
292
303
|
activity_name: { type: 'string', description: '明日方舟活动展示名,配 mode=activity' },
|
|
293
304
|
collection_name: { type: 'string', description: '终末地任务集合展示名,配 mode=collection' },
|
|
294
305
|
content_types: { type: 'array', items: { type: 'string', enum: END_FIELD_STORY_CONTENT_TYPES },
|
|
295
306
|
description: '终末地集合的内容形式过滤;续页保留' },
|
|
296
|
-
line: { type: 'integer', description: '
|
|
307
|
+
line: { type: 'integer', description: '上下文中心行号(around 模式,默认前后各 3 行);mode=document 不接受 line,连续读取请用 start_line' },
|
|
308
|
+
start_line: { type: 'integer', description: '连续读取的起始行号,仅配 mode=document;page.continuation 里的续页参数就是它' },
|
|
297
309
|
position: { type: 'integer', description: '合集续读位置,从 page.continuation 复制' },
|
|
298
|
-
mode: { type: 'string', enum: ['document', 'activity', 'collection'],
|
|
299
|
-
description: 'document=单篇全文,activity=活动,collection=任务集合;document_uid
|
|
300
|
-
section: { type: 'string', enum: WIKI_SECTION_VALUES, description: '读取 Wiki
|
|
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,并返回提示' },
|
|
301
313
|
before: { type: 'integer', description: '前文行数,默认 3,上限 100' },
|
|
302
314
|
after: { type: 'integer', description: '后文行数,默认 3,上限 100' },
|
|
303
315
|
cursor: { type: 'string', description: '仅兼容旧会话游标' },
|
|
304
|
-
data_version: { type: 'string', description: '
|
|
316
|
+
data_version: { type: 'string', description: '续页调用给出的短版本号;可省略' },
|
|
305
317
|
max_lines: { type: 'integer', description: '输出行数上限,默认 100,最大 500' },
|
|
306
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,强制重发本页全文' },
|
|
307
323
|
},
|
|
308
324
|
additionalProperties: false,
|
|
309
325
|
}
|
|
@@ -369,7 +385,8 @@ const READ_OUTPUT_SCHEMA = {
|
|
|
369
385
|
activity_name: { type: 'string' }, collection_name: { type: 'string' },
|
|
370
386
|
content_types: { type: 'array', items: { type: 'string', enum: END_FIELD_STORY_CONTENT_TYPES } },
|
|
371
387
|
mode: { type: 'string', enum: ['document', 'activity', 'collection'] },
|
|
372
|
-
|
|
388
|
+
start_line: { type: 'integer' }, position: { type: 'integer' },
|
|
389
|
+
data_version: { type: 'string' },
|
|
373
390
|
} }, { type: 'null' }] } } },
|
|
374
391
|
},
|
|
375
392
|
}
|
|
@@ -383,7 +400,7 @@ function projectReadToolValue(response, store) {
|
|
|
383
400
|
&& store.requiresDocumentUid?.(response.document?.document_id)) {
|
|
384
401
|
projected.page.continuation = {
|
|
385
402
|
document_uid: documentUid(response.document.document_id), mode: 'document',
|
|
386
|
-
|
|
403
|
+
start_line: continuation.start_line, data_version: continuation.data_version,
|
|
387
404
|
}
|
|
388
405
|
}
|
|
389
406
|
const uniqueStage = continuation.stage_code && continuation.story_part
|
|
@@ -396,7 +413,7 @@ function projectReadToolValue(response, store) {
|
|
|
396
413
|
&& (continuation.stage_code || continuation.record_name || continuation.material)
|
|
397
414
|
&& !uniqueStage && !uniqueRecord && !uniqueMaterial) {
|
|
398
415
|
projected.page.continuation = {
|
|
399
|
-
title: projected.primary.title, mode: 'document',
|
|
416
|
+
title: projected.primary.title, mode: 'document', start_line: continuation.start_line,
|
|
400
417
|
data_version: continuation.data_version,
|
|
401
418
|
}
|
|
402
419
|
}
|
|
@@ -473,19 +490,48 @@ const CLOUD_SEARCH_PARAMETERS = {
|
|
|
473
490
|
depth: { type: 'string', enum: ['fast', 'standard', 'deep'],
|
|
474
491
|
description: '兼容字段,不改变检索路线;通常省略' },
|
|
475
492
|
evidence_policy: { type: 'string', enum: ['mixed', 'original_only'],
|
|
476
|
-
description: '默认 mixed
|
|
493
|
+
description: '默认 mixed 混合检索,通常省略;original_only 仅检索官方剧情原文' },
|
|
477
494
|
options: {
|
|
478
495
|
type: 'object', additionalProperties: false,
|
|
479
496
|
properties: {
|
|
480
497
|
search_intent: {
|
|
481
498
|
type: 'string', enum: ['single_sentence_search'],
|
|
482
|
-
description: '
|
|
499
|
+
description: '仅用于凭记忆找某一句官方原文出处;会关闭档案/Wiki/图谱/年表,只检索原文。综合分析、人物关系等问题不要填写',
|
|
483
500
|
},
|
|
484
501
|
},
|
|
485
502
|
},
|
|
486
503
|
},
|
|
487
504
|
}
|
|
488
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
|
+
|
|
489
535
|
const CLOUD_INSPECT_PARAMETERS = {
|
|
490
536
|
type: 'object', additionalProperties: false,
|
|
491
537
|
properties: {
|
|
@@ -493,38 +539,76 @@ const CLOUD_INSPECT_PARAMETERS = {
|
|
|
493
539
|
description: '游戏过滤;省略时查看全部启用游戏' },
|
|
494
540
|
section: { type: 'string', enum: ['summary', 'candidates', 'selected_sources', 'events', 'trace_steps', 'answer_context'],
|
|
495
541
|
description: '默认 summary' },
|
|
496
|
-
cursor: { type: 'integer', description: '
|
|
497
|
-
limit: { type: 'integer', description: '本页条数' },
|
|
498
|
-
channels: stringList('按渠道过滤(candidates/events
|
|
499
|
-
query_variants: stringList('
|
|
500
|
-
retrievers: stringList('
|
|
501
|
-
source_types: { type: 'array', items: { type: 'string'
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
event_sequence_from: { type: 'integer', description: '
|
|
507
|
-
event_sequence_to: { type: 'integer', description: '
|
|
508
|
-
event_time_from: { type: 'string', description: '
|
|
509
|
-
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 时间' },
|
|
510
556
|
content_mode: { type: 'string', enum: ['none', 'preview', 'full'],
|
|
511
|
-
description: 'none=无正文,preview
|
|
512
|
-
content_max_chars: { type: 'integer', description: '正文字符上限' },
|
|
557
|
+
description: 'none=无正文,preview=摘要(默认),full=本页全文' },
|
|
558
|
+
content_max_chars: { type: 'integer', description: '正文字符上限 100–100000;通常省略' },
|
|
513
559
|
},
|
|
514
560
|
}
|
|
515
561
|
|
|
516
562
|
/** ---- 模型扁平参数 → 版本化 wire contract ---- */
|
|
517
563
|
|
|
518
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
|
+
|
|
519
602
|
async function modelReadToContract(args = {}, store, enabledGames = ['arknights', 'endfield']) {
|
|
603
|
+
const notes = []
|
|
520
604
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
521
605
|
throw Object.assign(new Error('读取参数必须是对象'), { code: 'INVALID_REQUEST' })
|
|
522
606
|
}
|
|
523
607
|
let expectedDataVersion
|
|
524
608
|
if (args.data_version !== undefined) {
|
|
525
609
|
expectedDataVersion = String(args.data_version)
|
|
526
|
-
if (!
|
|
527
|
-
throw Object.assign(new Error('data_version
|
|
610
|
+
if (!DATA_VERSION_REF_PATTERN.test(expectedDataVersion)) {
|
|
611
|
+
throw Object.assign(new Error('data_version 必须是续页结果给出的资料版本(至少 8 位小写十六进制,可省略)'),
|
|
528
612
|
{ code: 'INVALID_REQUEST' })
|
|
529
613
|
}
|
|
530
614
|
}
|
|
@@ -590,24 +674,66 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
590
674
|
}
|
|
591
675
|
const allowed = new Set(['title', 'document_uid', 'stage_code', 'story_part', 'character_name', 'record_name',
|
|
592
676
|
'segment', 'material', 'game', 'activity_name', 'collection_name', 'content_types',
|
|
593
|
-
'line', 'position', 'mode', 'section', 'before', 'after', 'max_lines', 'max_chars',
|
|
594
|
-
'data_version'])
|
|
677
|
+
'line', 'start_line', 'position', 'mode', 'section', 'before', 'after', 'max_lines', 'max_chars',
|
|
678
|
+
'reuse', 'citation', 'data_version'])
|
|
595
679
|
if (Object.keys(args).some((key) => !allowed.has(key))) {
|
|
596
680
|
throw Object.assign(new Error('corpus_read 包含不支持的参数'), { code: 'INVALID_REQUEST' })
|
|
597
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
|
+
}
|
|
598
707
|
|
|
599
708
|
const hasTitle = args.title !== undefined
|
|
600
709
|
const hasDocumentUid = args.document_uid !== undefined
|
|
601
|
-
const hasStageLocator = args.stage_code !== undefined
|
|
602
|
-
const hasRecordLocator = args.record_name !== undefined
|
|
603
|
-
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
|
|
604
713
|
const hasActivityLocator = args.activity_name !== undefined
|
|
605
714
|
const hasCollectionLocator = args.collection_name !== undefined
|
|
606
|
-
const locatorCount =
|
|
607
|
-
hasActivityLocator, hasCollectionLocator].filter(Boolean).length
|
|
715
|
+
const locatorCount = readTargets(args).length
|
|
608
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()))) {
|
|
609
735
|
throw Object.assign(new Error(
|
|
610
|
-
'
|
|
736
|
+
'document_uid 必须是检索结果返回的 doc_ 定位(doc_ 后为 16 位字母、数字、_ 或 -),不能填写展示标题;展示标题请改填 title,例如 {"title":"完整展示标题","mode":"document"}。不要同时提交 title 和 document_uid。',
|
|
611
737
|
), { code: 'INVALID_REQUEST' })
|
|
612
738
|
}
|
|
613
739
|
if (args.character_name !== undefined && !hasRecordLocator && !hasMaterialLocator) {
|
|
@@ -638,8 +764,11 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
638
764
|
const expectedMode = hasActivityLocator ? 'activity'
|
|
639
765
|
: hasCollectionLocator ? 'collection' : args.mode
|
|
640
766
|
if (args.mode !== undefined && args.mode !== expectedMode) {
|
|
641
|
-
|
|
642
|
-
{
|
|
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}",已自动改用。`)
|
|
643
772
|
}
|
|
644
773
|
if (args.line !== undefined || args.section !== undefined || args.before !== undefined
|
|
645
774
|
|| args.after !== undefined) {
|
|
@@ -680,7 +809,7 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
680
809
|
} catch (error) {
|
|
681
810
|
throw Object.assign(new Error(error.message), { code: error.code || 'DOCUMENT_AMBIGUOUS' })
|
|
682
811
|
}
|
|
683
|
-
return {
|
|
812
|
+
return withContractNote({
|
|
684
813
|
locator: hasDocumentUid ? { document_uid: String(args.document_uid).trim() }
|
|
685
814
|
: { [hasActivityLocator ? 'activity_name' : 'collection_name']: name },
|
|
686
815
|
selection: { mode: expectedMode, cursor: null,
|
|
@@ -689,7 +818,7 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
689
818
|
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}),
|
|
690
819
|
limits: { ...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
691
820
|
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}) },
|
|
692
|
-
}
|
|
821
|
+
}, notes)
|
|
693
822
|
}
|
|
694
823
|
|
|
695
824
|
let locator
|
|
@@ -716,8 +845,35 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
716
845
|
try {
|
|
717
846
|
record = await store.getDocumentByStoryStage(stageCode, storyPart)
|
|
718
847
|
} catch (error) {
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
721
877
|
}
|
|
722
878
|
if (!record) {
|
|
723
879
|
const partLabel = storyPart
|
|
@@ -783,15 +939,24 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
783
939
|
if (!title) throw Object.assign(new Error('title 不能为空'), { code: 'INVALID_REQUEST' })
|
|
784
940
|
const record = await store.getDocumentByTitle(title)
|
|
785
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
|
+
}
|
|
786
949
|
locator = { display_title: title }
|
|
787
950
|
}
|
|
788
951
|
const section = String(args.section ?? '').trim()
|
|
789
|
-
if (section && !hasTitle) {
|
|
790
|
-
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' })
|
|
791
954
|
}
|
|
792
955
|
const naturalDocumentLocator = hasDocumentUid || hasStageLocator || hasRecordLocator || hasMaterialLocator
|
|
956
|
+
// 只给 title 时默认从头读全文(受 max_lines/max_chars 限制并给出续页),
|
|
957
|
+
// 与关卡/密录/角色资料等其他直达定位保持一致。
|
|
793
958
|
const mode = args.mode || (section ? 'section' : Number.isInteger(args.line) ? 'around'
|
|
794
|
-
: naturalDocumentLocator ? 'document' : '')
|
|
959
|
+
: naturalDocumentLocator || hasTitle ? 'document' : '')
|
|
795
960
|
if (!mode) throw Object.assign(new Error(
|
|
796
961
|
'请提供 line、section 或 mode="document";max_lines/max_chars 只限制输出量,不能代替读取方式',
|
|
797
962
|
), { code: 'INVALID_REQUEST' })
|
|
@@ -808,34 +973,42 @@ async function modelReadToContract(args = {}, store, enabledGames = ['arknights'
|
|
|
808
973
|
if (section && mode !== 'section') {
|
|
809
974
|
throw Object.assign(new Error('section 只能与 mode=section 一起使用'), { code: 'INVALID_REQUEST' })
|
|
810
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
|
+
}
|
|
811
990
|
const selection = mode === 'document'
|
|
812
|
-
? { mode, cursor: null, ...(args.
|
|
991
|
+
? { mode, cursor: null, ...(args.start_line !== undefined ? { start_line: args.start_line } : {}) }
|
|
813
992
|
: mode === 'section'
|
|
814
993
|
? { mode, section }
|
|
815
994
|
: { mode: 'around', center_line: args.line,
|
|
816
995
|
...(args.before !== undefined ? { before_lines: args.before } : {}),
|
|
817
996
|
...(args.after !== undefined ? { after_lines: args.after } : {}) }
|
|
818
|
-
return {
|
|
997
|
+
return withContractNote({
|
|
819
998
|
locator, selection,
|
|
820
999
|
...(expectedDataVersion ? { expected_data_version: expectedDataVersion } : {}),
|
|
821
1000
|
limits: { ...(args.max_lines !== undefined ? { max_lines: args.max_lines } : {}),
|
|
822
1001
|
...(args.max_chars !== undefined ? { max_chars: args.max_chars } : {}) },
|
|
823
|
-
}
|
|
1002
|
+
}, notes)
|
|
824
1003
|
}
|
|
825
1004
|
|
|
826
1005
|
/** ---- 云端渲染(output.render 用,压缩为模型可读文本) ---- */
|
|
827
1006
|
|
|
828
|
-
function renderCloudSearch(
|
|
829
|
-
|
|
830
|
-
return [{ type: 'text', text: `[cloud_search:error] ${value.error.code}: ${value.error.message}` }]
|
|
831
|
-
}
|
|
832
|
-
return [{ type: 'text', text: projectCloudSearch(value) }]
|
|
1007
|
+
function renderCloudSearch(args, value) {
|
|
1008
|
+
return [{ type: 'text', text: projectCloudSearch(value, { args }) }]
|
|
833
1009
|
}
|
|
834
1010
|
|
|
835
1011
|
function renderCloudInspect(_args, value) {
|
|
836
|
-
if (value.status === 'error') {
|
|
837
|
-
return [{ type: 'text', text: `[cloud_inspect:error] ${value.error.code}: ${value.error.message}` }]
|
|
838
|
-
}
|
|
839
1012
|
return [{ type: 'text', text: JSON.stringify(projectCloudInspect(value)) }]
|
|
840
1013
|
}
|
|
841
1014
|
|
|
@@ -936,6 +1109,25 @@ export async function apply(ctx, config = {}) {
|
|
|
936
1109
|
installArchivePtcReceipts(toolCtx, readPresentationMeta)
|
|
937
1110
|
// Host 的 preset 准入已经完成初始化;工具挂载不再触发下载或全量扫描。
|
|
938
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
|
+
|
|
939
1131
|
tools.register({
|
|
940
1132
|
name: 'corpus_search',
|
|
941
1133
|
description: SEARCH_DESCRIPTION,
|
|
@@ -945,10 +1137,11 @@ export async function apply(ctx, config = {}) {
|
|
|
945
1137
|
isConcurrencySafe: () => true,
|
|
946
1138
|
presentCall: (args) => searchCallView('PRTS 本地资料', args,
|
|
947
1139
|
shared.effective().enabledGames),
|
|
948
|
-
execute: async (
|
|
1140
|
+
execute: async (rawArgs, exec) => {
|
|
949
1141
|
await requireLocalCorpus(store)
|
|
1142
|
+
const args = dropEmptyPlaceholders(rawArgs)
|
|
950
1143
|
const snapshot = corpusVersionSnapshot(store)
|
|
951
|
-
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1144
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
952
1145
|
const completedSearchCalls = evidenceState.completedSearchCalls
|
|
953
1146
|
const callId = String(exec?.callId || '')
|
|
954
1147
|
// 先做对象校验再做幂等哈希:JSON.stringify(undefined) 返回 undefined,
|
|
@@ -971,10 +1164,9 @@ export async function apply(ctx, config = {}) {
|
|
|
971
1164
|
const requestHash = createHash('sha256').update(JSON.stringify(scopedArgs)).digest('hex')
|
|
972
1165
|
if (callId && completedSearchCalls.has(callId)) {
|
|
973
1166
|
const cached = completedSearchCalls.get(callId)
|
|
974
|
-
if (cached.requestHash !== requestHash)
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
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 })
|
|
978
1170
|
}
|
|
979
1171
|
assertCorpusVersion(store, snapshot)
|
|
980
1172
|
const replay = structuredClone(cached.response)
|
|
@@ -1010,23 +1202,29 @@ export async function apply(ctx, config = {}) {
|
|
|
1010
1202
|
// 模型调用顺序独占执行并逐个提交结果。
|
|
1011
1203
|
isConcurrencySafe: () => false,
|
|
1012
1204
|
presentCall: (args) => readCallView(args, shared.effective().enabledGames),
|
|
1013
|
-
execute: async (
|
|
1205
|
+
execute: async (rawArgs, exec) => {
|
|
1014
1206
|
await requireLocalCorpus(store)
|
|
1207
|
+
let { args, guidance: argumentGuidance } = normalizeReadArguments(rawArgs,
|
|
1208
|
+
{ sections: WIKI_SECTION_VALUES })
|
|
1015
1209
|
const snapshot = corpusVersionSnapshot(store)
|
|
1016
|
-
const evidenceState = evidenceStates.forExecution(exec, store.dataVersion)
|
|
1210
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
1017
1211
|
let contract
|
|
1018
1212
|
try {
|
|
1019
1213
|
contract = await modelReadToContract(args, store, shared.effective().enabledGames)
|
|
1020
1214
|
assertCorpusVersion(store, snapshot)
|
|
1215
|
+
const inferred = contractNotes.get(contract)
|
|
1216
|
+
if (inferred) argumentGuidance = [argumentGuidance, inferred].filter(Boolean).join('\n')
|
|
1021
1217
|
} catch (error) {
|
|
1022
1218
|
assertCorpusVersion(store, snapshot)
|
|
1023
1219
|
throw Object.assign(new Error(error.message),
|
|
1024
1220
|
{ code: error.code || 'INVALID_REQUEST', retryable: error.retryable ?? false })
|
|
1025
1221
|
}
|
|
1026
|
-
if (contract.expected_data_version !== undefined
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
|
1030
1228
|
}
|
|
1031
1229
|
// 证据覆盖按 Harness Agent 隔离;模型不感知 intent_id。
|
|
1032
1230
|
contract.intent_id = evidenceState.intentId
|
|
@@ -1034,12 +1232,14 @@ export async function apply(ctx, config = {}) {
|
|
|
1034
1232
|
normalizeReadRequest(contract)
|
|
1035
1233
|
const requested = await resolveReadWindow(store, contract)
|
|
1036
1234
|
assertCorpusVersion(store, snapshot)
|
|
1037
|
-
|
|
1235
|
+
// reuse:false 时不参与"上文已可见"的判定:宿主 surface 与模型实际收到的内容
|
|
1236
|
+
// 一旦不一致(外部压缩、跨请求折叠),去重就会静默少给行,宁可重发。
|
|
1237
|
+
const visibleResults = args.reuse === false ? new Map() : visibleToolResults(exec?.agent)
|
|
1038
1238
|
if (coveredRead(evidenceState, requested, visibleResults)) {
|
|
1039
1239
|
const replay = replayCoveredRead(evidenceState, requested, contract)
|
|
1040
1240
|
if (replay) {
|
|
1041
1241
|
assertCorpusVersion(store, snapshot)
|
|
1042
|
-
return projectReadToolValue(replay, store)
|
|
1242
|
+
return withReadGuidance(projectReadToolValue(replay, store), argumentGuidance)
|
|
1043
1243
|
}
|
|
1044
1244
|
}
|
|
1045
1245
|
const response = await executeRead(store, contract, { signal: exec?.signal })
|
|
@@ -1051,7 +1251,7 @@ export async function apply(ctx, config = {}) {
|
|
|
1051
1251
|
}, visibleResults) : null
|
|
1052
1252
|
const delivered = reuseReadCoverage(response, coveragePlan)
|
|
1053
1253
|
rememberRead(evidenceState, delivered, { callId: String(exec?.callId || ''), store })
|
|
1054
|
-
return projectReadToolValue(delivered, store)
|
|
1254
|
+
return withReadGuidance(projectReadToolValue(delivered, store), argumentGuidance)
|
|
1055
1255
|
},
|
|
1056
1256
|
})
|
|
1057
1257
|
|
|
@@ -1090,7 +1290,8 @@ export async function apply(ctx, config = {}) {
|
|
|
1090
1290
|
throw Object.assign(new Error('明日方舟资料当前未启用'), { code: 'INVALID_REQUEST' })
|
|
1091
1291
|
}
|
|
1092
1292
|
const snapshot = corpusVersionSnapshot(store)
|
|
1093
|
-
const response = await executeTimelineSearch(store,
|
|
1293
|
+
const response = await executeTimelineSearch(store,
|
|
1294
|
+
dropEmptyPlaceholders(args, { zeroFields: TIMELINE_ZERO_FIELDS }), { signal: exec?.signal })
|
|
1094
1295
|
assertCorpusVersion(store, snapshot)
|
|
1095
1296
|
return rememberArchivePresentation(response, timelineArchivePresentation(response))
|
|
1096
1297
|
},
|
|
@@ -1124,9 +1325,10 @@ export async function apply(ctx, config = {}) {
|
|
|
1124
1325
|
timeoutMs: 180_000,
|
|
1125
1326
|
isConcurrencySafe: () => true,
|
|
1126
1327
|
presentCall: (args) => searchCallView('PRTS 云端资料', args, c.enabledGames),
|
|
1127
|
-
execute: async (
|
|
1328
|
+
execute: async (rawArgs, exec) => {
|
|
1128
1329
|
try {
|
|
1129
|
-
const
|
|
1330
|
+
const args = dropEmptyPlaceholders(rawArgs)
|
|
1331
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
1130
1332
|
const cloud = cloudClients.forExecution(exec)
|
|
1131
1333
|
await cloud.capabilities({ signal: exec?.signal })
|
|
1132
1334
|
const requestedGames = Array.isArray(args.games) ? args.games : c.enabledGames
|
|
@@ -1142,7 +1344,8 @@ export async function apply(ctx, config = {}) {
|
|
|
1142
1344
|
rememberCloudMappings(evidenceState, enriched)
|
|
1143
1345
|
return rememberArchivePresentation(enriched, cloudArchivePresentation(enriched, { dataVersion }))
|
|
1144
1346
|
} catch (error) {
|
|
1145
|
-
|
|
1347
|
+
// 抛出交给宿主标记 isError,让 Agent 明确知道本次调用失败。
|
|
1348
|
+
throw cloudFaultError(error, 'cloud_search')
|
|
1146
1349
|
}
|
|
1147
1350
|
},
|
|
1148
1351
|
})
|
|
@@ -1158,9 +1361,11 @@ export async function apply(ctx, config = {}) {
|
|
|
1158
1361
|
presentCall: (args) => ({ card: 'generic',
|
|
1159
1362
|
title: `查看 PRTS 云端检索详情 · ${presentedGames(args, c.enabledGames)}`,
|
|
1160
1363
|
kind: 'read', rawInput: compactToolTitle(args?.section || 'summary') }),
|
|
1161
|
-
execute: async (
|
|
1364
|
+
execute: async (rawArgs, exec) => {
|
|
1162
1365
|
try {
|
|
1163
|
-
const
|
|
1366
|
+
const args = dropEmptyPlaceholders(rawArgs, { zeroFields: CLOUD_INSPECT_ZERO_FIELDS })
|
|
1367
|
+
validateCloudInspectArgs(args)
|
|
1368
|
+
const evidenceState = evidenceStates.forExecution(exec, store.scopeFingerprint ?? store.dataVersion)
|
|
1164
1369
|
const cloud = cloudClients.forExecution(exec)
|
|
1165
1370
|
await cloud.capabilities({ signal: exec?.signal })
|
|
1166
1371
|
const payload = { ...args,
|
|
@@ -1173,7 +1378,7 @@ export async function apply(ctx, config = {}) {
|
|
|
1173
1378
|
inspect: true, dataVersion: store.dataVersion,
|
|
1174
1379
|
}))
|
|
1175
1380
|
} catch (error) {
|
|
1176
|
-
|
|
1381
|
+
throw cloudFaultError(error, 'cloud_inspect')
|
|
1177
1382
|
}
|
|
1178
1383
|
},
|
|
1179
1384
|
})
|
|
@@ -1211,7 +1416,8 @@ export async function apply(ctx, config = {}) {
|
|
|
1211
1416
|
storeEntry.releaseTimer = null
|
|
1212
1417
|
try {
|
|
1213
1418
|
const pointer = await readCurrentReleasePointer(releasesDir)
|
|
1214
|
-
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
|
|
1215
1421
|
} catch { /* 让 ready() 输出真实指针错误 */ }
|
|
1216
1422
|
store.reset()
|
|
1217
1423
|
prepareEntityRecognition(store).catch((error) => {
|