narraeon 0.7.2 → 0.7.5
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/dist/node/protocol/v1.js +8 -2
- package/dist/node/runtime/V1Runtime.js +12 -3
- package/dist/node/runtime/archive/WorldArchives.js +17 -5
- package/dist/node/runtime/content/ContentWorkspace.js +2 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +98 -165
- package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
- package/dist/node/runtime/model/FileNativeModelAdapters.js +106 -18
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +104 -21
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
- package/dist/node/runtime/play/PlayCallChain.js +200 -374
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +312 -167
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +5 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +151 -108
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -1
- package/dist/node/runtime/world/WorldDocumentStore.js +1 -1
- package/dist/node/runtime/world/WorldLedgerAuthority.js +37 -3
- package/dist/node/runtime/world/WorldTurnLedger.js +17 -4
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -4
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +19 -241
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +8 -27
- package/dist/node/server/createServer.js +44 -20
- package/dist/web/assets/index-9N9kO8kY.js +1408 -0
- package/dist/web/assets/index-BjxBYYQi.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BUCslLEK.css +0 -1
- package/dist/web/assets/index-DkamBH6c.js +0 -1408
|
@@ -18,6 +18,16 @@ export const settingImprovementToolNames = [
|
|
|
18
18
|
"setting_move",
|
|
19
19
|
"setting_delete",
|
|
20
20
|
];
|
|
21
|
+
const authoringOperations = [
|
|
22
|
+
"list",
|
|
23
|
+
"search",
|
|
24
|
+
"read",
|
|
25
|
+
"create",
|
|
26
|
+
"write_file",
|
|
27
|
+
"patch",
|
|
28
|
+
"move",
|
|
29
|
+
"delete",
|
|
30
|
+
];
|
|
21
31
|
const frameExample = `format: narraeon.world-frame/v1
|
|
22
32
|
bindings:
|
|
23
33
|
currentSituation: "@current-situation"
|
|
@@ -153,10 +163,13 @@ export function settingImprovementRuntimeContract(locale, gameplay) {
|
|
|
153
163
|
- Passing mechanical checks does not make content discoverable during play. Whenever important information is created or reorganized, decide its natural owner, initial injection or discovery path, future update location, and player-visible projection together.`;
|
|
154
164
|
}
|
|
155
165
|
export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
166
|
+
return authoringToolDefinitions(contentPackageAuthoringSurface, locale, gameplay);
|
|
167
|
+
}
|
|
168
|
+
/** Tool definitions a surface exposes; names, paths and descriptions come from the surface. */
|
|
169
|
+
export function authoringToolDefinitions(surface, locale, gameplay) {
|
|
156
170
|
const tabletop = gameplay.mode === "tabletop";
|
|
157
|
-
const descriptions = locale
|
|
158
|
-
|
|
159
|
-
: toolDescriptionsEn(tabletop);
|
|
171
|
+
const descriptions = surface.descriptions[locale](tabletop);
|
|
172
|
+
const name = (operation) => authoringToolName(surface, operation);
|
|
160
173
|
const schema = (properties, required) => ({
|
|
161
174
|
type: "object",
|
|
162
175
|
additionalProperties: false,
|
|
@@ -169,17 +182,20 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
|
169
182
|
const limit = { type: "integer", minimum: 1, maximum: 100 };
|
|
170
183
|
return [
|
|
171
184
|
{
|
|
172
|
-
name: "
|
|
173
|
-
description: descriptions.
|
|
185
|
+
name: name("list"),
|
|
186
|
+
description: descriptions.list,
|
|
174
187
|
inputSchema: schema({
|
|
175
|
-
directory: {
|
|
188
|
+
directory: {
|
|
189
|
+
type: "string",
|
|
190
|
+
pattern: `^${surface.root}(?:/[^\\\\]+)?$`,
|
|
191
|
+
},
|
|
176
192
|
limit,
|
|
177
193
|
cursor,
|
|
178
194
|
}, []),
|
|
179
195
|
},
|
|
180
196
|
{
|
|
181
|
-
name: "
|
|
182
|
-
description: descriptions.
|
|
197
|
+
name: name("search"),
|
|
198
|
+
description: descriptions.search,
|
|
183
199
|
inputSchema: schema({
|
|
184
200
|
query: { type: "string", minLength: 1, maxLength: 256 },
|
|
185
201
|
within: path,
|
|
@@ -189,17 +205,17 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
|
189
205
|
}, ["query"]),
|
|
190
206
|
},
|
|
191
207
|
{
|
|
192
|
-
name: "
|
|
193
|
-
description: descriptions.
|
|
208
|
+
name: name("read"),
|
|
209
|
+
description: descriptions.read,
|
|
194
210
|
inputSchema: schema({ path }, ["path"]),
|
|
195
211
|
},
|
|
196
212
|
{
|
|
197
|
-
name: "
|
|
198
|
-
description: descriptions.
|
|
213
|
+
name: name("create"),
|
|
214
|
+
description: descriptions.create,
|
|
199
215
|
inputSchema: schema({
|
|
200
216
|
path: {
|
|
201
217
|
type: "string",
|
|
202
|
-
pattern:
|
|
218
|
+
pattern: `^${surface.root}/.+\\.(?:ya?ml|md)$`,
|
|
203
219
|
},
|
|
204
220
|
ref: { type: "string", pattern: "^[a-z][a-z0-9-]{1,31}$" },
|
|
205
221
|
title: { type: "string", minLength: 1, maxLength: 120 },
|
|
@@ -213,8 +229,8 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
|
213
229
|
}, ["path", "ref", "title", "summary", "aliases", "body"]),
|
|
214
230
|
},
|
|
215
231
|
{
|
|
216
|
-
name: "
|
|
217
|
-
description: descriptions.
|
|
232
|
+
name: name("write_file"),
|
|
233
|
+
description: descriptions.write_file,
|
|
218
234
|
inputSchema: schema({
|
|
219
235
|
path,
|
|
220
236
|
contents: text,
|
|
@@ -229,8 +245,8 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
|
229
245
|
}, ["path", "contents"]),
|
|
230
246
|
},
|
|
231
247
|
{
|
|
232
|
-
name: "
|
|
233
|
-
description: descriptions.
|
|
248
|
+
name: name("patch"),
|
|
249
|
+
description: descriptions.patch,
|
|
234
250
|
inputSchema: schema({
|
|
235
251
|
document: text,
|
|
236
252
|
op: {
|
|
@@ -253,40 +269,130 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
|
253
269
|
}, ["document", "op"]),
|
|
254
270
|
},
|
|
255
271
|
{
|
|
256
|
-
name: "
|
|
257
|
-
description: descriptions.
|
|
272
|
+
name: name("move"),
|
|
273
|
+
description: descriptions.move,
|
|
258
274
|
inputSchema: schema({ from: text, to: path }, ["from", "to"]),
|
|
259
275
|
},
|
|
260
276
|
{
|
|
261
|
-
name: "
|
|
262
|
-
description: descriptions.
|
|
277
|
+
name: name("delete"),
|
|
278
|
+
description: descriptions.delete,
|
|
263
279
|
inputSchema: schema({ document: text }, ["document"]),
|
|
264
280
|
},
|
|
265
281
|
];
|
|
266
282
|
}
|
|
267
283
|
const toolDescriptionsEn = (tabletop) => ({
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
284
|
+
list: "List the world root or a world/ subdirectory from the content package's current tree. The root also lists opening and control files. A cursor is valid only for the same query and current-tree revision.",
|
|
285
|
+
search: "Search literal source text in world documents in the content package's current tree. within may be world, a world/ directory or path, or @ref.",
|
|
286
|
+
read: "Read a complete world document by world/ path or @ref, or read a complete opening/control file by exact path. Existing document selectors are projected to @refs. The result is never truncated or paginated.",
|
|
287
|
+
create: "Create one world/ .yaml or .md document. Supply a unique lowercase ASCII ref, title, summary, aliases, and body without a technical header; Runtime completes the storage envelope. The created document is already fully read.",
|
|
288
|
+
write_file: `Replace a completely read world/ .yaml or .md document body while preserving its identity and metadata, or create/replace opening.md, control/frame.yaml, control/player-views.yaml, ${tabletop ? "control/blocks/*.md, or control/gameplay.json (only a tabletop resolution scheme may change)" : "or control/blocks/*.md"}, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. To repair a damaged world document, also provide ref, title, summary, and aliases; Runtime preserves any recoverable storage identity. Create new world documents with setting_create.`,
|
|
289
|
+
patch: 'Update a completely read YAML world document without rewriting unrelated content. locator is a non-empty path whose segments are map keys or zero-based array indexes. Use add for a missing map key or array index, replace for an existing node, append for the end of an existing array, and remove for an existing map key or array item. When title, summary, or aliases are stale, update all three together with op "set_metadata". Rewrite Markdown bodies with setting_write_file.',
|
|
290
|
+
move: "Move a completely read world document to a new world/ .yaml or .md logical path while preserving its identity and contents.",
|
|
291
|
+
delete: "Permanently delete a completely read world document from the content-package current tree. Runtime rejects deletion while currentSituation, a frame slot, a player-view selector, or another document's $ref still points to it, and reports every blocking location. Delete or redirect those references first.",
|
|
276
292
|
});
|
|
277
293
|
const toolDescriptionsZhCN = (tabletop) => ({
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
294
|
+
list: "列出内容包当前树的 world 根目录或 world/ 子目录;根目录同时列出 opening 和 control 专用文件。cursor 只对同一查询和当前树 revision 有效。",
|
|
295
|
+
search: "在内容包当前树的 world 文档中按原文字面搜索;within 可为 world、world/ 目录或路径或 @ref。",
|
|
296
|
+
read: "按 world/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 opening/control;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
|
|
297
|
+
create: "创建一份 world/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
|
|
298
|
+
write_file: `整份替换已完整读取的 world/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md${tabletop ? "、control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)" : ""},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 setting_create。`,
|
|
299
|
+
patch: '细粒度更新已完整读取的 YAML 世界文档,不重写无关内容。locator 是非空路径,每段可以是 map key 或从 0 开始的数组下标;add 新建 map key 或数组位置,replace 更新既有节点,append 追加既有数组,remove 删除既有 map key 或数组项。title、summary 或 aliases 过时时用 op "set_metadata" 整组更新三项。Markdown 正文用 setting_write_file 整份重写。',
|
|
300
|
+
move: "把已完整读取的世界文档移动到新的 world/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
|
|
301
|
+
delete: "从内容包当前树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
|
|
286
302
|
});
|
|
303
|
+
const worldRevisionDescriptionsEn = (tabletop) => ({
|
|
304
|
+
list: "List the state root or a state/ subdirectory from the world revision worktree. The root also lists control files. A cursor is valid only for the same query and worktree revision.",
|
|
305
|
+
search: "Search literal source text in world documents in the world revision worktree. within may be state, a state/ directory or path, or @ref.",
|
|
306
|
+
read: "Read a complete world document by state/ path or @ref, or read a complete control file by exact path. Existing document selectors are projected to @refs. The result is never truncated or paginated.",
|
|
307
|
+
create: "Create one state/ .yaml or .md document. Supply a unique lowercase ASCII ref, title, summary, aliases, and body without a technical header; Runtime completes the storage envelope. The created document is already fully read.",
|
|
308
|
+
write_file: `Replace a completely read state/ .yaml or .md document body while preserving its identity and metadata, or create/replace control/frame.yaml, control/player-views.yaml, ${tabletop ? "control/blocks/*.md, or control/gameplay.json (only a tabletop resolution scheme may change)" : "or control/blocks/*.md"}, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. To repair a damaged world document, also provide ref, title, summary, and aliases; Runtime preserves any recoverable storage identity. Create new world documents with world_revision_create.`,
|
|
309
|
+
patch: 'Update a completely read YAML world document without rewriting unrelated content. locator is a non-empty path whose segments are map keys or zero-based array indexes. Use add for a missing map key or array index, replace for an existing node, append for the end of an existing array, and remove for an existing map key or array item. When title, summary, or aliases are stale, update all three together with op "set_metadata". Rewrite Markdown bodies with world_revision_write_file.',
|
|
310
|
+
move: "Move a completely read world document to a new state/ .yaml or .md logical path while preserving its identity and contents.",
|
|
311
|
+
delete: "Permanently delete a completely read world document from the world revision worktree. Runtime rejects deletion while currentSituation, a frame slot, a player-view selector, or another document's $ref still points to it, and reports every blocking location. Delete or redirect those references first.",
|
|
312
|
+
});
|
|
313
|
+
const worldRevisionDescriptionsZhCN = (tabletop) => ({
|
|
314
|
+
list: "列出世界修订工作树的 state 根目录或 state/ 子目录;根目录同时列出 control 文件。cursor 只对同一查询和工作树 revision 有效。",
|
|
315
|
+
search: "在世界修订工作树的 state 文档中按原文字面搜索;within 可为 state、state/ 目录或路径或 @ref。",
|
|
316
|
+
read: "按 state/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 control 文件;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
|
|
317
|
+
create: "创建一份 state/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
|
|
318
|
+
write_file: `整份替换已完整读取的 state/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 control/frame.yaml、control/player-views.yaml、control/blocks/*.md${tabletop ? "、control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)" : ""},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 world_revision_create。`,
|
|
319
|
+
patch: '细粒度更新已完整读取的 YAML 世界文档,不重写无关内容。locator 是非空路径,每段可以是 map key 或从 0 开始的数组下标;add 新建 map key 或数组位置,replace 更新既有节点,append 追加既有数组,remove 删除既有 map key 或数组项。title、summary 或 aliases 过时时用 op "set_metadata" 整组更新三项。Markdown 正文用 world_revision_write_file 整份重写。',
|
|
320
|
+
move: "把已完整读取的世界文档移动到新的 state/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
|
|
321
|
+
delete: "从世界修订工作树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
|
|
322
|
+
});
|
|
323
|
+
function worldRevisionUnavailablePath(path, locale) {
|
|
324
|
+
if (path === "opening.md")
|
|
325
|
+
return localized(locale, "opening.md is immutable after a world has been created. Edit state/* or control/* instead.", "世界创建后 opening.md 已成为不可变创世叙事。请改写 state/* 或 control/*。");
|
|
326
|
+
if (path === "world" || path.startsWith("world/"))
|
|
327
|
+
return localized(locale, "World revision exposes only state/* and control/* logical paths; use those instead.", "世界修订只公开 state/* 与 control/* 逻辑路径;请改用这两类路径。");
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
/** The content package's setting improvement: templates it edits, `world/` paths, and `opening.md`. */
|
|
331
|
+
export const contentPackageAuthoringSurface = {
|
|
332
|
+
layout: "content_package",
|
|
333
|
+
root: "world",
|
|
334
|
+
toolPrefix: "setting",
|
|
335
|
+
opening: true,
|
|
336
|
+
phrases: {
|
|
337
|
+
en: {
|
|
338
|
+
tree: "the content package's current tree",
|
|
339
|
+
bareTree: "the content package current tree",
|
|
340
|
+
noun: "the content package",
|
|
341
|
+
slug: "content-package",
|
|
342
|
+
directoryHeading: "Setting directory",
|
|
343
|
+
},
|
|
344
|
+
"zh-CN": {
|
|
345
|
+
tree: "内容包当前树",
|
|
346
|
+
bareTree: "内容包当前树",
|
|
347
|
+
noun: "内容包",
|
|
348
|
+
slug: "content-package",
|
|
349
|
+
directoryHeading: "设定目录",
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
descriptions: {
|
|
353
|
+
en: toolDescriptionsEn,
|
|
354
|
+
"zh-CN": toolDescriptionsZhCN,
|
|
355
|
+
},
|
|
356
|
+
unavailablePath: () => null,
|
|
357
|
+
};
|
|
358
|
+
/** A world revision: the state/control worktree of a world already in play. */
|
|
359
|
+
export const worldRevisionAuthoringSurface = {
|
|
360
|
+
layout: "world_revision",
|
|
361
|
+
root: "state",
|
|
362
|
+
toolPrefix: "world_revision",
|
|
363
|
+
opening: false,
|
|
364
|
+
phrases: {
|
|
365
|
+
en: {
|
|
366
|
+
tree: "the world revision worktree",
|
|
367
|
+
bareTree: "the world revision worktree",
|
|
368
|
+
noun: "the world revision worktree",
|
|
369
|
+
slug: "world-revision",
|
|
370
|
+
directoryHeading: "World revision directory",
|
|
371
|
+
},
|
|
372
|
+
"zh-CN": {
|
|
373
|
+
tree: "世界修订工作树",
|
|
374
|
+
bareTree: "世界修订工作树",
|
|
375
|
+
noun: "世界修订工作树",
|
|
376
|
+
slug: "world-revision",
|
|
377
|
+
directoryHeading: "世界修订目录",
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
descriptions: {
|
|
381
|
+
en: worldRevisionDescriptionsEn,
|
|
382
|
+
"zh-CN": worldRevisionDescriptionsZhCN,
|
|
383
|
+
},
|
|
384
|
+
unavailablePath: worldRevisionUnavailablePath,
|
|
385
|
+
};
|
|
386
|
+
function authoringToolName(surface, operation) {
|
|
387
|
+
return `${surface.toolPrefix}_${operation}`;
|
|
388
|
+
}
|
|
389
|
+
function authoringOperation(surface, name) {
|
|
390
|
+
return (authoringOperations.find((operation) => authoringToolName(surface, operation) === name) ?? null);
|
|
391
|
+
}
|
|
287
392
|
export class SettingAuthoringTransaction {
|
|
288
393
|
#baseFiles;
|
|
289
394
|
#locale;
|
|
395
|
+
#surface;
|
|
290
396
|
#preview;
|
|
291
397
|
#validateFiles;
|
|
292
398
|
#snapshot;
|
|
@@ -295,10 +401,11 @@ export class SettingAuthoringTransaction {
|
|
|
295
401
|
constructor(input) {
|
|
296
402
|
this.#baseFiles = cloneFiles(input.baseFiles);
|
|
297
403
|
this.#locale = input.locale ?? defaultAppLocale;
|
|
404
|
+
this.#surface = input.surface ?? contentPackageAuthoringSurface;
|
|
298
405
|
this.#preview = input.preview;
|
|
299
406
|
this.#validateFiles = input.validateFiles ?? (() => undefined);
|
|
300
407
|
this.#snapshot = WorldDocumentStore.open({
|
|
301
|
-
layout:
|
|
408
|
+
layout: this.#surface.layout,
|
|
302
409
|
files: cloneFiles(input.baseFiles),
|
|
303
410
|
});
|
|
304
411
|
const authorization = input.authorization?.revision === (input.revision ?? this.#snapshot.id)
|
|
@@ -327,13 +434,23 @@ export class SettingAuthoringTransaction {
|
|
|
327
434
|
};
|
|
328
435
|
}
|
|
329
436
|
execute(calls) {
|
|
330
|
-
const normalized = calls.map(normalizeCall);
|
|
437
|
+
const normalized = calls.map((call) => normalizeCall(call, this.#surface));
|
|
331
438
|
const results = [];
|
|
332
439
|
const responseBase = this.files();
|
|
333
440
|
let lastSuccessfulWrite = -1;
|
|
334
441
|
for (const call of normalized) {
|
|
335
442
|
const before = this.files();
|
|
336
|
-
|
|
443
|
+
const unavailable = unavailableArgument(this.#surface, call.arguments, this.#locale);
|
|
444
|
+
if (unavailable !== null) {
|
|
445
|
+
results.push({
|
|
446
|
+
toolCallId: call.id,
|
|
447
|
+
markdown: `# Runtime tool rejected\n\n${unavailable}`,
|
|
448
|
+
isError: true,
|
|
449
|
+
changes: [],
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (call.operation !== null && isReadOperation(call.operation)) {
|
|
337
454
|
results.push(toToolResult(call.id, this.#executeRead(call), []));
|
|
338
455
|
continue;
|
|
339
456
|
}
|
|
@@ -347,39 +464,41 @@ export class SettingAuthoringTransaction {
|
|
|
347
464
|
const settled = results[lastSuccessfulWrite];
|
|
348
465
|
// The stored review stays cumulative for the whole user request; the
|
|
349
466
|
// model only needs what this tool response changed.
|
|
350
|
-
settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#locale)}`;
|
|
467
|
+
settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#surface, this.#locale)}`;
|
|
351
468
|
}
|
|
352
469
|
return results;
|
|
353
470
|
}
|
|
354
471
|
#executeRead(call) {
|
|
355
|
-
if (call.
|
|
356
|
-
return listDocuments(this.#snapshot, call.arguments, this.#locale);
|
|
357
|
-
if (call.
|
|
358
|
-
return searchDocuments(this.#snapshot, call.arguments, this.#locale);
|
|
359
|
-
if (call.
|
|
360
|
-
return readDocument(this.#snapshot, call.arguments, this.#reads, this.#locale);
|
|
472
|
+
if (call.operation === "list")
|
|
473
|
+
return listDocuments(this.#snapshot, call.arguments, this.#surface, this.#locale);
|
|
474
|
+
if (call.operation === "search")
|
|
475
|
+
return searchDocuments(this.#snapshot, call.arguments, this.#surface, this.#locale);
|
|
476
|
+
if (call.operation === "read")
|
|
477
|
+
return readDocument(this.#snapshot, call.arguments, this.#reads, this.#surface, this.#locale);
|
|
361
478
|
return failure(`Unsupported read tool: ${call.name}`, this.#locale);
|
|
362
479
|
}
|
|
363
480
|
#executeWrite(call) {
|
|
364
481
|
const snapshot = this.#snapshot;
|
|
365
482
|
const reads = cloneReads(this.#reads);
|
|
366
483
|
try {
|
|
367
|
-
if (isWorldWriteCall(call)) {
|
|
368
|
-
if (call.
|
|
369
|
-
const deleted = deleteWorldDocument(snapshot, call, reads, this.#locale);
|
|
484
|
+
if (isWorldWriteCall(call, this.#surface)) {
|
|
485
|
+
if (call.operation === "delete") {
|
|
486
|
+
const deleted = deleteWorldDocument(snapshot, call, reads, this.#surface, this.#locale);
|
|
370
487
|
this.#validateFiles(deleted.snapshot.files);
|
|
371
488
|
this.#snapshot = deleted.snapshot;
|
|
372
489
|
this.#reads = reads;
|
|
373
490
|
return success(deleted.markdown);
|
|
374
491
|
}
|
|
375
|
-
const requestedRef = call.
|
|
492
|
+
const requestedRef = call.operation === "create"
|
|
376
493
|
? requiredSettingRef(call.arguments.ref, this.#locale)
|
|
377
494
|
: null;
|
|
378
495
|
const revised = snapshot.revise({
|
|
379
|
-
commands: [
|
|
496
|
+
commands: [
|
|
497
|
+
worldRevisionCommand(snapshot, call, this.#surface, this.#locale),
|
|
498
|
+
],
|
|
380
499
|
});
|
|
381
500
|
if (!revised.ok)
|
|
382
|
-
throw new SettingAuthoringError(renderRevisionFailure(revised.diagnostics, this.#locale));
|
|
501
|
+
throw new SettingAuthoringError(renderRevisionFailure(revised.diagnostics, this.#surface, this.#locale));
|
|
383
502
|
if (requestedRef !== null &&
|
|
384
503
|
revised.changes.some(({ shortRef }) => shortRef !== requestedRef))
|
|
385
504
|
throw new SettingAuthoringError(localized(this.#locale, `ref @${requestedRef} already exists; choose a different ref`, `ref @${requestedRef} 已存在;请换一个 ref`));
|
|
@@ -392,9 +511,9 @@ export class SettingAuthoringTransaction {
|
|
|
392
511
|
this.#reads = reads;
|
|
393
512
|
return success(renderWorldWriteSuccess(revised.changes, this.#locale));
|
|
394
513
|
}
|
|
395
|
-
if (call.
|
|
514
|
+
if (call.operation !== "write_file")
|
|
396
515
|
throw new SettingAuthoringError(`Unsupported write tool: ${call.name}`);
|
|
397
|
-
const changed = writeOpaque(snapshot, call, reads, this.#locale);
|
|
516
|
+
const changed = writeOpaque(snapshot, call, reads, this.#surface, this.#locale);
|
|
398
517
|
this.#validateFiles(changed.snapshot.files);
|
|
399
518
|
this.#snapshot = changed.snapshot;
|
|
400
519
|
this.#reads = reads;
|
|
@@ -403,12 +522,13 @@ export class SettingAuthoringTransaction {
|
|
|
403
522
|
catch (error) {
|
|
404
523
|
const message = error instanceof Error
|
|
405
524
|
? error.message
|
|
406
|
-
:
|
|
525
|
+
: `The ${this.#surface.phrases.en.slug} write failed`;
|
|
407
526
|
return failure(message, this.#locale);
|
|
408
527
|
}
|
|
409
528
|
}
|
|
410
529
|
#inspect() {
|
|
411
530
|
const inspection = inspectContentPackageCurrentTree(this.#snapshot.files, {
|
|
531
|
+
requireOpening: this.#surface.opening,
|
|
412
532
|
worldDocumentSnapshot: this.#snapshot,
|
|
413
533
|
});
|
|
414
534
|
const diff = fileDiff(this.#baseFiles, this.#snapshot.files);
|
|
@@ -419,7 +539,7 @@ export class SettingAuthoringTransaction {
|
|
|
419
539
|
diagnostics: inspection.issues.map(({ code, path, message }) => ({
|
|
420
540
|
code,
|
|
421
541
|
path,
|
|
422
|
-
message: publicSettingDiagnosticMessage(code, message, this.#locale),
|
|
542
|
+
message: publicSettingDiagnosticMessage(code, message, this.#surface, this.#locale),
|
|
423
543
|
})),
|
|
424
544
|
preview: null,
|
|
425
545
|
playCoverage: null,
|
|
@@ -431,7 +551,7 @@ export class SettingAuthoringTransaction {
|
|
|
431
551
|
diff,
|
|
432
552
|
diagnostics: [],
|
|
433
553
|
preview,
|
|
434
|
-
playCoverage: buildSettingAuthoringPlayCoverage(this.#snapshot, preview, diff),
|
|
554
|
+
playCoverage: buildSettingAuthoringPlayCoverage(this.#snapshot, preview, diff, this.#surface),
|
|
435
555
|
};
|
|
436
556
|
}
|
|
437
557
|
catch (error) {
|
|
@@ -453,7 +573,7 @@ export class SettingAuthoringTransaction {
|
|
|
453
573
|
}
|
|
454
574
|
}
|
|
455
575
|
}
|
|
456
|
-
function renderAutomaticReview(review, responseDiff, snapshot, locale) {
|
|
576
|
+
function renderAutomaticReview(review, responseDiff, snapshot, surface, locale) {
|
|
457
577
|
if (review.status === "usable" && review.preview !== null) {
|
|
458
578
|
const changed = responseDiff.length;
|
|
459
579
|
const cumulative = review.diff.length;
|
|
@@ -461,7 +581,7 @@ function renderAutomaticReview(review, responseDiff, snapshot, locale) {
|
|
|
461
581
|
const changedShortRefs = new Set(listQueryableWorldDocuments(snapshot)
|
|
462
582
|
.filter(({ logicalPath }) => changedPaths.has(logicalPath))
|
|
463
583
|
.map(({ shortRef }) => shortRef));
|
|
464
|
-
return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changed ${changed} file(s) in
|
|
584
|
+
return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changed ${changed} file(s) in ${surface.phrases.en.noun}${cumulative === changed ? "" : `; ${cumulative} file(s) changed so far in this user request`}. Use this review and the coverage below to decide whether the current user request is satisfied or further edits are needed.`, `# 当前树自动检查通过\n\n内容树校验和真实 Prompt Preview 均已通过;本次工具响应修改${surface.phrases["zh-CN"].noun}中的 ${changed} 个文件${cumulative === changed ? "" : `,本次用户请求累计改动 ${cumulative} 个文件`}。请根据本次检查与下方覆盖报告判断当前用户要求是否已经满足,或是否还需修改。`)}\n\n${renderPlayCoverage(buildSettingAuthoringPlayCoverage(snapshot, review.preview, responseDiff, surface), surface, locale)}\n\n${renderWorldMaintenanceReport(review.preview.compilation.maintenance, changedShortRefs, locale)}`;
|
|
465
585
|
}
|
|
466
586
|
return [
|
|
467
587
|
localized(locale, "# Current-tree review needs repair", "# 当前树自动检查需要修复"),
|
|
@@ -471,7 +591,7 @@ function renderAutomaticReview(review, responseDiff, snapshot, locale) {
|
|
|
471
591
|
localized(locale, "Repair these diagnostics with the ordinary read/write tools. Runtime will run the review again after the next successful write call.", "请用普通读写工具修复这些诊断;下一个写调用成功后 Runtime 会再次自动检查。"),
|
|
472
592
|
].join("\n");
|
|
473
593
|
}
|
|
474
|
-
function buildSettingAuthoringPlayCoverage(snapshot, preview, diff) {
|
|
594
|
+
function buildSettingAuthoringPlayCoverage(snapshot, preview, diff, surface) {
|
|
475
595
|
const documents = listQueryableWorldDocuments(snapshot);
|
|
476
596
|
const byShortRef = new Map(documents.map((document) => [document.shortRef, document]));
|
|
477
597
|
const { fullInjected, nodeInjected, catalogSummary, injectedSelections } = worldMaterialCoverage(preview.compilation.coverage);
|
|
@@ -524,7 +644,11 @@ function buildSettingAuthoringPlayCoverage(snapshot, preview, diff) {
|
|
|
524
644
|
totals,
|
|
525
645
|
changed: diff.map(({ path, after }) => {
|
|
526
646
|
const access = changedPathPlayAccess(path, after, accessByPath, enabledWorldInstructionPaths);
|
|
527
|
-
return {
|
|
647
|
+
return {
|
|
648
|
+
path,
|
|
649
|
+
access,
|
|
650
|
+
detail: playAccessDetail(access, surface),
|
|
651
|
+
};
|
|
528
652
|
}),
|
|
529
653
|
};
|
|
530
654
|
}
|
|
@@ -555,7 +679,7 @@ function incrementPlayCoverageTotal(totals, access) {
|
|
|
555
679
|
else if (access === "on_demand")
|
|
556
680
|
totals.onDemand += 1;
|
|
557
681
|
}
|
|
558
|
-
function playAccessDetail(access) {
|
|
682
|
+
function playAccessDetail(access, surface) {
|
|
559
683
|
const details = {
|
|
560
684
|
full_injected: "complete body is injected into every fresh play context",
|
|
561
685
|
node_injected: "only a selected node is injected into fresh play context",
|
|
@@ -566,11 +690,11 @@ function playAccessDetail(access) {
|
|
|
566
690
|
play_control: "compiled as immutable play-author control",
|
|
567
691
|
unused_control: "not enabled by control/frame.yaml and unavailable to ordinary play AI",
|
|
568
692
|
player_view: "projected through exact player-view selectors",
|
|
569
|
-
removed:
|
|
693
|
+
removed: `removed from ${surface.phrases.en.bareTree}`,
|
|
570
694
|
};
|
|
571
695
|
return details[access];
|
|
572
696
|
}
|
|
573
|
-
function renderPlayCoverage(coverage, locale) {
|
|
697
|
+
function renderPlayCoverage(coverage, surface, locale) {
|
|
574
698
|
const labels = locale === "zh-CN"
|
|
575
699
|
? {
|
|
576
700
|
full_injected: "全文注入",
|
|
@@ -582,7 +706,7 @@ function renderPlayCoverage(coverage, locale) {
|
|
|
582
706
|
play_control: "游玩控制",
|
|
583
707
|
unused_control: "未被 control/frame.yaml 启用",
|
|
584
708
|
player_view: "玩家视图精确投影",
|
|
585
|
-
removed: "
|
|
709
|
+
removed: `已从${surface.phrases["zh-CN"].tree}删除`,
|
|
586
710
|
}
|
|
587
711
|
: {
|
|
588
712
|
full_injected: "full text injected",
|
|
@@ -594,7 +718,7 @@ function renderPlayCoverage(coverage, locale) {
|
|
|
594
718
|
play_control: "play-author control",
|
|
595
719
|
unused_control: "not enabled by control/frame.yaml",
|
|
596
720
|
player_view: "exact player-view projection",
|
|
597
|
-
removed:
|
|
721
|
+
removed: `removed from ${surface.phrases.en.bareTree}`,
|
|
598
722
|
};
|
|
599
723
|
const maximumReportedChanges = 64;
|
|
600
724
|
const reportedChanges = coverage.changed.slice(0, maximumReportedChanges);
|
|
@@ -634,32 +758,42 @@ class SettingAuthoringError extends Error {
|
|
|
634
758
|
this.name = "SettingAuthoringError";
|
|
635
759
|
}
|
|
636
760
|
}
|
|
637
|
-
function normalizeCall(call) {
|
|
638
|
-
if (!isRecord(call.arguments))
|
|
639
|
-
return { id: call.id, name: call.name, arguments: {} };
|
|
761
|
+
function normalizeCall(call, surface) {
|
|
640
762
|
return {
|
|
641
763
|
id: call.id,
|
|
642
764
|
name: call.name,
|
|
643
|
-
|
|
765
|
+
operation: authoringOperation(surface, call.name),
|
|
766
|
+
arguments: isRecord(call.arguments) ? structuredClone(call.arguments) : {},
|
|
644
767
|
};
|
|
645
768
|
}
|
|
646
|
-
function
|
|
647
|
-
return
|
|
648
|
-
name === "setting_search" ||
|
|
649
|
-
name === "setting_read");
|
|
769
|
+
function isReadOperation(operation) {
|
|
770
|
+
return operation === "list" || operation === "search" || operation === "read";
|
|
650
771
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
772
|
+
/**
|
|
773
|
+
* Rejects a call that names a path outside the surface before any store work.
|
|
774
|
+
* The check covers every path-like argument, so a read and a write fail alike.
|
|
775
|
+
*/
|
|
776
|
+
function unavailableArgument(surface, args, locale) {
|
|
777
|
+
for (const field of ["path", "directory", "within", "from", "to", "document"])
|
|
778
|
+
if (typeof args[field] === "string") {
|
|
779
|
+
const message = surface.unavailablePath(args[field], locale);
|
|
780
|
+
if (message !== null)
|
|
781
|
+
return message;
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
function isWorldWriteCall(call, surface) {
|
|
786
|
+
if (call.operation === "create" ||
|
|
787
|
+
call.operation === "patch" ||
|
|
788
|
+
call.operation === "move" ||
|
|
789
|
+
call.operation === "delete")
|
|
656
790
|
return true;
|
|
657
|
-
return (call.
|
|
791
|
+
return (call.operation === "write_file" &&
|
|
658
792
|
typeof call.arguments.path === "string" &&
|
|
659
|
-
call.arguments.path.startsWith(
|
|
793
|
+
call.arguments.path.startsWith(`${surface.root}/`));
|
|
660
794
|
}
|
|
661
|
-
function worldRevisionCommand(snapshot, call, locale) {
|
|
662
|
-
if (call.
|
|
795
|
+
function worldRevisionCommand(snapshot, call, surface, locale) {
|
|
796
|
+
if (call.operation === "create") {
|
|
663
797
|
if (!hasOnly(call.arguments, [
|
|
664
798
|
"path",
|
|
665
799
|
"ref",
|
|
@@ -668,8 +802,8 @@ function worldRevisionCommand(snapshot, call, locale) {
|
|
|
668
802
|
"aliases",
|
|
669
803
|
"body",
|
|
670
804
|
]))
|
|
671
|
-
throw new SettingAuthoringError("
|
|
672
|
-
const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), locale);
|
|
805
|
+
throw new SettingAuthoringError(`${authoringToolName(surface, "create")} accepts only path, ref, title, summary, aliases, and body`);
|
|
806
|
+
const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), surface, locale);
|
|
673
807
|
const ref = requiredSettingRef(call.arguments.ref, locale);
|
|
674
808
|
const title = requiredString(call.arguments.title, "title");
|
|
675
809
|
const summary = requiredString(call.arguments.summary, "summary");
|
|
@@ -688,9 +822,9 @@ function worldRevisionCommand(snapshot, call, locale) {
|
|
|
688
822
|
body,
|
|
689
823
|
};
|
|
690
824
|
}
|
|
691
|
-
if (call.
|
|
692
|
-
const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), locale);
|
|
693
|
-
const contents = worldWriteContents(snapshot, logicalPath, call.arguments, locale);
|
|
825
|
+
if (call.operation === "write_file") {
|
|
826
|
+
const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), surface, locale);
|
|
827
|
+
const contents = worldWriteContents(snapshot, logicalPath, call.arguments, surface, locale);
|
|
694
828
|
assertNoLiteralUnicodeEscapes(contents, locale);
|
|
695
829
|
return {
|
|
696
830
|
kind: "write",
|
|
@@ -698,17 +832,17 @@ function worldRevisionCommand(snapshot, call, locale) {
|
|
|
698
832
|
contents,
|
|
699
833
|
};
|
|
700
834
|
}
|
|
701
|
-
if (call.
|
|
702
|
-
const toLogicalPath = safePath(requiredString(call.arguments.to, "to"));
|
|
703
|
-
if (
|
|
704
|
-
throw new SettingAuthoringError("
|
|
835
|
+
if (call.operation === "move") {
|
|
836
|
+
const toLogicalPath = safePath(requiredString(call.arguments.to, "to"), surface);
|
|
837
|
+
if (!documentPathPattern(surface).test(toLogicalPath))
|
|
838
|
+
throw new SettingAuthoringError(`${authoringToolName(surface, "move")}.to must be a ${surface.root}/ .yaml or .md path`);
|
|
705
839
|
return {
|
|
706
840
|
kind: "move",
|
|
707
|
-
document: revisionTarget(requiredString(call.arguments.from, "from")),
|
|
841
|
+
document: revisionTarget(requiredString(call.arguments.from, "from"), surface),
|
|
708
842
|
toLogicalPath,
|
|
709
843
|
};
|
|
710
844
|
}
|
|
711
|
-
if (call.
|
|
845
|
+
if (call.operation === "patch") {
|
|
712
846
|
const op = requiredString(call.arguments.op, "op");
|
|
713
847
|
if (op === "set_metadata") {
|
|
714
848
|
const title = requiredString(call.arguments.title, "title");
|
|
@@ -717,31 +851,31 @@ function worldRevisionCommand(snapshot, call, locale) {
|
|
|
717
851
|
assertNoLiteralUnicodeEscapes({ title, summary, aliases }, locale);
|
|
718
852
|
return {
|
|
719
853
|
kind: "patch",
|
|
720
|
-
document: revisionTarget(requiredString(call.arguments.document, "document")),
|
|
854
|
+
document: revisionTarget(requiredString(call.arguments.document, "document"), surface),
|
|
721
855
|
edits: [{ op: "set_metadata", title, summary, aliases }],
|
|
722
856
|
};
|
|
723
857
|
}
|
|
724
858
|
if (op !== "add" && op !== "replace" && op !== "append" && op !== "remove")
|
|
725
|
-
throw new SettingAuthoringError("
|
|
859
|
+
throw new SettingAuthoringError(`${authoringToolName(surface, "patch")}.op must be add, replace, append, remove, or set_metadata`);
|
|
726
860
|
const locator = requiredYamlLocatorPath(call.arguments.locator, "locator");
|
|
727
861
|
if (op === "remove") {
|
|
728
862
|
if (!hasOnly(call.arguments, ["document", "op", "locator"]))
|
|
729
|
-
throw new SettingAuthoringError("
|
|
863
|
+
throw new SettingAuthoringError(`${authoringToolName(surface, "patch")} remove accepts only document, op, and locator`);
|
|
730
864
|
assertNoLiteralUnicodeEscapes(locator, locale);
|
|
731
865
|
return {
|
|
732
866
|
kind: "patch",
|
|
733
|
-
document: revisionTarget(requiredString(call.arguments.document, "document")),
|
|
867
|
+
document: revisionTarget(requiredString(call.arguments.document, "document"), surface),
|
|
734
868
|
edits: [{ op: "remove", locator: { yaml: locator } }],
|
|
735
869
|
};
|
|
736
870
|
}
|
|
737
871
|
if (!hasOnly(call.arguments, ["document", "op", "locator", "value"]) ||
|
|
738
872
|
!Object.hasOwn(call.arguments, "value"))
|
|
739
|
-
throw new SettingAuthoringError(
|
|
873
|
+
throw new SettingAuthoringError(`${authoringToolName(surface, "patch")} ${op} requires document, op, locator, and value`);
|
|
740
874
|
const value = structuredClone(call.arguments.value);
|
|
741
875
|
assertNoLiteralUnicodeEscapes({ locator, value }, locale);
|
|
742
876
|
return {
|
|
743
877
|
kind: "patch",
|
|
744
|
-
document: revisionTarget(requiredString(call.arguments.document, "document")),
|
|
878
|
+
document: revisionTarget(requiredString(call.arguments.document, "document"), surface),
|
|
745
879
|
edits: [
|
|
746
880
|
{
|
|
747
881
|
op,
|
|
@@ -755,15 +889,19 @@ function worldRevisionCommand(snapshot, call, locale) {
|
|
|
755
889
|
}
|
|
756
890
|
throw new SettingAuthoringError(`Unsupported write tool: ${call.name}`);
|
|
757
891
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
892
|
+
/** The world-document path pattern for a surface, e.g. `world/…yaml` or `state/…md`. */
|
|
893
|
+
function documentPathPattern(surface) {
|
|
894
|
+
return new RegExp(`^${surface.root}/.+\\.(?:ya?ml|md)$`, "u");
|
|
895
|
+
}
|
|
896
|
+
function safeWorldDocumentPath(path, surface, locale) {
|
|
897
|
+
const logicalPath = safePath(path, surface);
|
|
898
|
+
if (!documentPathPattern(surface).test(logicalPath))
|
|
899
|
+
throw new SettingAuthoringError(localized(locale, `World files must be .yaml or .md documents below ${surface.root}/`, `world 文件必须是 ${surface.root}/ 下的 .yaml 或 .md 文档`));
|
|
762
900
|
return logicalPath;
|
|
763
901
|
}
|
|
764
|
-
function worldWriteContents(snapshot, logicalPath, args, locale) {
|
|
902
|
+
function worldWriteContents(snapshot, logicalPath, args, surface, locale) {
|
|
765
903
|
if (!hasOnly(args, ["path", "contents", "ref", "title", "summary", "aliases"]))
|
|
766
|
-
throw new SettingAuthoringError("
|
|
904
|
+
throw new SettingAuthoringError(`${authoringToolName(surface, "write_file")} accepts only its declared arguments`);
|
|
767
905
|
const body = requiredString(args.contents, "contents");
|
|
768
906
|
const metadataKeys = ["ref", "title", "summary", "aliases"];
|
|
769
907
|
const provided = metadataKeys.filter((key) => args[key] !== undefined);
|
|
@@ -771,7 +909,7 @@ function worldWriteContents(snapshot, logicalPath, args, locale) {
|
|
|
771
909
|
const existingDocument = listQueryableWorldDocuments(snapshot).some(({ logicalPath: existingPath }) => existingPath === logicalPath);
|
|
772
910
|
if (provided.length === 0) {
|
|
773
911
|
if (!existingFile && !containsTechnicalDocumentHeader(logicalPath, body))
|
|
774
|
-
throw new SettingAuthoringError(localized(locale,
|
|
912
|
+
throw new SettingAuthoringError(localized(locale, `Create a new world document with ${authoringToolName(surface, "create")}`, `新建世界文档请使用 ${authoringToolName(surface, "create")}`));
|
|
775
913
|
if (existingFile &&
|
|
776
914
|
!existingDocument &&
|
|
777
915
|
!containsTechnicalDocumentHeader(logicalPath, body))
|
|
@@ -781,9 +919,9 @@ function worldWriteContents(snapshot, logicalPath, args, locale) {
|
|
|
781
919
|
if (provided.length !== metadataKeys.length)
|
|
782
920
|
throw new SettingAuthoringError(localized(locale, "Repair metadata must provide ref, title, summary, and aliases together", "修复元信息时必须同时提供 ref、title、summary 和 aliases"));
|
|
783
921
|
if (!existingFile)
|
|
784
|
-
throw new SettingAuthoringError(localized(locale,
|
|
922
|
+
throw new SettingAuthoringError(localized(locale, `Create a new world document with ${authoringToolName(surface, "create")}`, `新建世界文档请使用 ${authoringToolName(surface, "create")}`));
|
|
785
923
|
if (existingDocument)
|
|
786
|
-
throw new SettingAuthoringError(localized(locale,
|
|
924
|
+
throw new SettingAuthoringError(localized(locale, `Repair metadata is only for a damaged document; update valid metadata with ${authoringToolName(surface, "patch")} set_metadata`, `修复元信息只用于损坏文档;有效文档请用 ${authoringToolName(surface, "patch")} set_metadata 更新元信息`));
|
|
787
925
|
const ref = requiredSettingRef(args.ref, locale);
|
|
788
926
|
const title = requiredString(args.title, "title");
|
|
789
927
|
const summary = requiredString(args.summary, "summary");
|
|
@@ -826,25 +964,26 @@ function recoverStoredDocumentId(snapshot, logicalPath) {
|
|
|
826
964
|
? id
|
|
827
965
|
: null;
|
|
828
966
|
}
|
|
829
|
-
function revisionTarget(value) {
|
|
830
|
-
const selector = documentSelector(value);
|
|
967
|
+
function revisionTarget(value, surface) {
|
|
968
|
+
const selector = documentSelector(value, surface);
|
|
831
969
|
if (selector === null)
|
|
832
970
|
throw new SettingAuthoringError(`Invalid world-document selector: ${value}`);
|
|
833
971
|
return selector;
|
|
834
972
|
}
|
|
835
|
-
function deleteWorldDocument(snapshot, call, reads, locale) {
|
|
973
|
+
function deleteWorldDocument(snapshot, call, reads, surface, locale) {
|
|
974
|
+
const tool = authoringToolName(surface, "delete");
|
|
836
975
|
if (!hasOnly(call.arguments, ["document"]))
|
|
837
|
-
throw new SettingAuthoringError(
|
|
976
|
+
throw new SettingAuthoringError(`${tool} accepts only the document argument`);
|
|
838
977
|
const supplied = requiredString(call.arguments.document, "document");
|
|
839
|
-
const selector = documentSelector(supplied);
|
|
978
|
+
const selector = documentSelector(supplied, surface);
|
|
840
979
|
if (selector === null)
|
|
841
|
-
throw new SettingAuthoringError(localized(locale,
|
|
980
|
+
throw new SettingAuthoringError(localized(locale, `${tool}.document must be an @ref or ${surface.root}/ document path`, `${tool}.document 必须是 @ref 或 ${surface.root}/ 文档路径`));
|
|
842
981
|
const resolved = snapshot.query({
|
|
843
982
|
kind: "read_document",
|
|
844
983
|
document: selector,
|
|
845
984
|
});
|
|
846
985
|
if (resolved.kind === "error")
|
|
847
|
-
throw new SettingAuthoringError(renderRevisionFailure(resolved.diagnostics, locale));
|
|
986
|
+
throw new SettingAuthoringError(renderRevisionFailure(resolved.diagnostics, surface, locale));
|
|
848
987
|
if (resolved.kind !== "read_document")
|
|
849
988
|
throw new Error("Unexpected world-document delete lookup result");
|
|
850
989
|
const target = resolved.document;
|
|
@@ -859,7 +998,7 @@ function deleteWorldDocument(snapshot, call, reads, locale) {
|
|
|
859
998
|
throw new SettingAuthoringError(localized(locale, `Cannot delete @${target.shortRef}; remove or redirect every reference first:\n${locations}`, `无法删除 @${target.shortRef};请先删除或改向全部引用:\n${locations}`));
|
|
860
999
|
}
|
|
861
1000
|
const revised = WorldDocumentStore.open({
|
|
862
|
-
layout:
|
|
1001
|
+
layout: surface.layout,
|
|
863
1002
|
files: sortFiles(snapshot.files.filter(({ path }) => path !== target.logicalPath)),
|
|
864
1003
|
});
|
|
865
1004
|
rebaseReads(reads, revised);
|
|
@@ -942,12 +1081,14 @@ function deletionLocator(locator) {
|
|
|
942
1081
|
? `yaml:${JSON.stringify(locator.yaml)}`
|
|
943
1082
|
: `markdown:${JSON.stringify(locator.markdown)}`;
|
|
944
1083
|
}
|
|
945
|
-
function writeOpaque(snapshot, call, reads, locale) {
|
|
1084
|
+
function writeOpaque(snapshot, call, reads, surface, locale) {
|
|
946
1085
|
if (!hasOnly(call.arguments, ["path", "contents"]))
|
|
947
1086
|
throw new SettingAuthoringError("Special-file writes accept only path and contents");
|
|
948
|
-
const path = safePath(requiredString(call.arguments.path, "path"));
|
|
949
|
-
if (!writableOpaquePath(path))
|
|
950
|
-
throw new SettingAuthoringError(
|
|
1087
|
+
const path = safePath(requiredString(call.arguments.path, "path"), surface);
|
|
1088
|
+
if (!writableOpaquePath(path, surface))
|
|
1089
|
+
throw new SettingAuthoringError(surface.opening
|
|
1090
|
+
? localized(locale, "Special-file writes accept only opening.md, control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or control/gameplay.json", "专用文件只允许写入 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md 或 control/gameplay.json")
|
|
1091
|
+
: localized(locale, "Special-file writes accept only control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or control/gameplay.json", "专用文件只允许写入 control/frame.yaml、control/player-views.yaml、control/blocks/*.md 或 control/gameplay.json"));
|
|
951
1092
|
const authoredContents = requiredString(call.arguments.contents, "contents");
|
|
952
1093
|
assertNoLiteralUnicodeEscapes(authoredContents, locale);
|
|
953
1094
|
const contents = projectOpaqueDocumentIds(snapshot, path, authoredContents);
|
|
@@ -964,7 +1105,7 @@ function writeOpaque(snapshot, call, reads, locale) {
|
|
|
964
1105
|
delete existing.encoding;
|
|
965
1106
|
}
|
|
966
1107
|
const revised = WorldDocumentStore.open({
|
|
967
|
-
layout:
|
|
1108
|
+
layout: surface.layout,
|
|
968
1109
|
files: sortFiles(next),
|
|
969
1110
|
});
|
|
970
1111
|
rebaseReads(reads, revised);
|
|
@@ -973,7 +1114,7 @@ function writeOpaque(snapshot, call, reads, locale) {
|
|
|
973
1114
|
return {
|
|
974
1115
|
snapshot: revised,
|
|
975
1116
|
markdown: [
|
|
976
|
-
localized(locale, `# Current-tree write accepted\n\n${existing === undefined ? "Created" : "Updated"} ${path} in
|
|
1117
|
+
localized(locale, `# Current-tree write accepted\n\n${existing === undefined ? "Created" : "Updated"} ${path} in ${surface.phrases.en.tree}. This file is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.`, `# 当前树写入已接受\n\n已在${surface.phrases["zh-CN"].tree}中${existing === undefined ? "创建" : "更新"} ${path}。该文件已视为完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。`),
|
|
977
1118
|
...(gameplay === null
|
|
978
1119
|
? []
|
|
979
1120
|
: [
|
|
@@ -993,9 +1134,9 @@ function assertGameplayWriteKeepsMode(files, contents, locale) {
|
|
|
993
1134
|
if (before.mode !== after.mode)
|
|
994
1135
|
throw new SettingAuthoringError(localized(locale, "Gameplay mode is fixed; control/gameplay.json may change only the resolution scheme of a tabletop package", "玩法模式固定;control/gameplay.json 只能修改跑团的判定方案,不能在互动式小说与跑团之间切换"));
|
|
995
1136
|
}
|
|
996
|
-
function writableOpaquePath(path) {
|
|
1137
|
+
function writableOpaquePath(path, surface) {
|
|
997
1138
|
return (isPackageFollowupControlPath(path) ||
|
|
998
|
-
path === "opening.md" ||
|
|
1139
|
+
(surface.opening && path === "opening.md") ||
|
|
999
1140
|
path === "control/frame.yaml" ||
|
|
1000
1141
|
path === "control/player-views.yaml" ||
|
|
1001
1142
|
path === gameplayPath ||
|
|
@@ -1029,25 +1170,27 @@ function renderWorldWriteSuccess(changes, locale) {
|
|
|
1029
1170
|
localized(locale, "This call was accepted independently. Every created or changed document above is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.", "本调用已独立接受。上方新建或修改的文档均视为已经完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。"),
|
|
1030
1171
|
].join("\n");
|
|
1031
1172
|
}
|
|
1032
|
-
function renderRevisionFailure(diagnostics, locale) {
|
|
1173
|
+
function renderRevisionFailure(diagnostics, surface, locale) {
|
|
1033
1174
|
return diagnostics
|
|
1034
|
-
.map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, locale)}`)
|
|
1175
|
+
.map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, surface, locale)}`)
|
|
1035
1176
|
.join("\n");
|
|
1036
1177
|
}
|
|
1037
|
-
function listDocuments(snapshot, args, locale) {
|
|
1178
|
+
function listDocuments(snapshot, args, surface, locale) {
|
|
1179
|
+
const tool = authoringToolName(surface, "list");
|
|
1180
|
+
const root = surface.root;
|
|
1038
1181
|
if (!hasOnly(args, ["directory", "limit", "cursor"]))
|
|
1039
|
-
return failure(
|
|
1040
|
-
const directory = args.directory ??
|
|
1182
|
+
return failure(`${tool} accepts only directory, limit, and cursor`, locale);
|
|
1183
|
+
const directory = args.directory ?? root;
|
|
1041
1184
|
const limit = args.limit ?? 100;
|
|
1042
1185
|
if (typeof directory !== "string" ||
|
|
1043
|
-
(directory !==
|
|
1186
|
+
(directory !== root && !directory.startsWith(`${root}/`)) ||
|
|
1044
1187
|
typeof limit !== "number" ||
|
|
1045
1188
|
!Number.isInteger(limit) ||
|
|
1046
1189
|
limit < 1 ||
|
|
1047
1190
|
limit > 100 ||
|
|
1048
1191
|
!validCursor(args.cursor))
|
|
1049
|
-
return failure(
|
|
1050
|
-
const relative = directory ===
|
|
1192
|
+
return failure(`Invalid ${tool} directory, limit, or cursor`, locale);
|
|
1193
|
+
const relative = directory === root ? "" : directory.slice(root.length + 1);
|
|
1051
1194
|
const result = snapshot.query({
|
|
1052
1195
|
kind: "catalog",
|
|
1053
1196
|
directory: relative,
|
|
@@ -1055,7 +1198,7 @@ function listDocuments(snapshot, args, locale) {
|
|
|
1055
1198
|
...(args.cursor === undefined ? {} : { cursor: args.cursor }),
|
|
1056
1199
|
});
|
|
1057
1200
|
if (result.kind === "error")
|
|
1058
|
-
return renderQueryFailure(result, locale);
|
|
1201
|
+
return renderQueryFailure(result, surface, locale);
|
|
1059
1202
|
if (result.kind !== "catalog")
|
|
1060
1203
|
throw new Error("Unexpected catalog result");
|
|
1061
1204
|
const entries = result.entries.map((entry) => {
|
|
@@ -1067,11 +1210,12 @@ function listDocuments(snapshot, args, locale) {
|
|
|
1067
1210
|
});
|
|
1068
1211
|
if (relative === "" && result.page.start === 0)
|
|
1069
1212
|
entries.unshift(...snapshot.files
|
|
1070
|
-
.filter(({ path }) => !path.startsWith(
|
|
1213
|
+
.filter(({ path }) => !path.startsWith(`${root}/`))
|
|
1071
1214
|
.map(({ path, encoding }) => encoding === undefined ? `- [special] ${path}` : `- [binary] ${path}`));
|
|
1072
|
-
return success(`# ${localized(locale,
|
|
1215
|
+
return success(`# ${localized(locale, surface.phrases.en.directoryHeading, surface.phrases["zh-CN"].directoryHeading)}\n\n${entries.join("\n") || localized(locale, "(empty)", "(空)")}\n\n${pageFooter(result.page, locale)}`);
|
|
1073
1216
|
}
|
|
1074
|
-
function searchDocuments(snapshot, args, locale) {
|
|
1217
|
+
function searchDocuments(snapshot, args, surface, locale) {
|
|
1218
|
+
const tool = authoringToolName(surface, "search");
|
|
1075
1219
|
const query = args.query;
|
|
1076
1220
|
const limit = args.limit ?? 20;
|
|
1077
1221
|
if (!hasOnly(args, ["query", "within", "caseSensitive", "limit", "cursor"]) ||
|
|
@@ -1085,10 +1229,10 @@ function searchDocuments(snapshot, args, locale) {
|
|
|
1085
1229
|
(args.caseSensitive !== undefined &&
|
|
1086
1230
|
typeof args.caseSensitive !== "boolean") ||
|
|
1087
1231
|
!validCursor(args.cursor))
|
|
1088
|
-
return failure(
|
|
1089
|
-
const within = searchScope(args.within);
|
|
1232
|
+
return failure(`Invalid ${tool} arguments`, locale);
|
|
1233
|
+
const within = searchScope(args.within, surface);
|
|
1090
1234
|
if (within === invalidScope)
|
|
1091
|
-
return failure(
|
|
1235
|
+
return failure(`Invalid ${tool} within scope`, locale);
|
|
1092
1236
|
const result = snapshot.query({
|
|
1093
1237
|
kind: "literal_search",
|
|
1094
1238
|
query,
|
|
@@ -1098,39 +1242,39 @@ function searchDocuments(snapshot, args, locale) {
|
|
|
1098
1242
|
...(args.cursor === undefined ? {} : { cursor: args.cursor }),
|
|
1099
1243
|
});
|
|
1100
1244
|
if (result.kind === "error")
|
|
1101
|
-
return renderQueryFailure(result, locale);
|
|
1245
|
+
return renderQueryFailure(result, surface, locale);
|
|
1102
1246
|
if (result.kind !== "literal_search")
|
|
1103
1247
|
throw new Error("Unexpected literal search result");
|
|
1104
1248
|
const lines = result.matches.map(({ document, referenceProjection, range }) => `- @${document.shortRef} · ${document.logicalPath} · ${range.start.line}:${range.start.column}\n ${JSON.stringify(referenceProjection.excerpt)}`);
|
|
1105
1249
|
return success(`# ${localized(locale, "Literal setting search", "设定字面搜索")}\n\n${lines.join("\n") || localized(locale, "Zero literal matches do not prove the fact is absent.", "0 个字面命中不证明设定中不存在该事实。")}\n\n${pageFooter(result.page, locale)}`);
|
|
1106
1250
|
}
|
|
1107
1251
|
const invalidScope = Symbol("invalid-scope");
|
|
1108
|
-
function searchScope(value) {
|
|
1252
|
+
function searchScope(value, surface) {
|
|
1109
1253
|
if (value === undefined)
|
|
1110
1254
|
return undefined;
|
|
1111
1255
|
if (typeof value !== "string" || value.length === 0)
|
|
1112
1256
|
return invalidScope;
|
|
1113
|
-
if (value ===
|
|
1257
|
+
if (value === surface.root)
|
|
1114
1258
|
return { directory: "" };
|
|
1115
1259
|
if (value.startsWith("@"))
|
|
1116
1260
|
return value.length > 1
|
|
1117
1261
|
? { document: { shortRef: value.slice(1) } }
|
|
1118
1262
|
: invalidScope;
|
|
1119
|
-
if (value.startsWith(
|
|
1120
|
-
const path = safePath(value);
|
|
1263
|
+
if (value.startsWith(`${surface.root}/`)) {
|
|
1264
|
+
const path = safePath(value, surface);
|
|
1121
1265
|
return /\.(?:ya?ml|md)$/u.test(path)
|
|
1122
1266
|
? { document: { logicalPath: path } }
|
|
1123
|
-
: { directory: path.slice(
|
|
1267
|
+
: { directory: path.slice(surface.root.length + 1) };
|
|
1124
1268
|
}
|
|
1125
1269
|
return invalidScope;
|
|
1126
1270
|
}
|
|
1127
|
-
function readDocument(snapshot, args, reads, locale) {
|
|
1271
|
+
function readDocument(snapshot, args, reads, surface, locale) {
|
|
1128
1272
|
const path = args.path;
|
|
1129
1273
|
if (!hasOnly(args, ["path", "maxBytes", "cursor"]) ||
|
|
1130
1274
|
typeof path !== "string" ||
|
|
1131
1275
|
path.length === 0)
|
|
1132
|
-
return failure(
|
|
1133
|
-
const opaque = snapshot.files.find((file) => file.path === path && !file.path.startsWith(
|
|
1276
|
+
return failure(`Invalid ${authoringToolName(surface, "read")} arguments`, locale);
|
|
1277
|
+
const opaque = snapshot.files.find((file) => file.path === path && !file.path.startsWith(`${surface.root}/`));
|
|
1134
1278
|
if (opaque !== undefined) {
|
|
1135
1279
|
if (opaque.encoding !== undefined)
|
|
1136
1280
|
return failure("Special files require one complete text read", locale);
|
|
@@ -1138,28 +1282,28 @@ function readDocument(snapshot, args, reads, locale) {
|
|
|
1138
1282
|
const projected = projectOpaqueDocumentIds(snapshot, path, opaque.contents);
|
|
1139
1283
|
return success(`# ${localized(locale, "Special-file source", "专用文件原文")} ${path}\n\n${projected}\n\n---\nComplete: yes`);
|
|
1140
1284
|
}
|
|
1141
|
-
const selector = documentSelector(path);
|
|
1285
|
+
const selector = documentSelector(path, surface);
|
|
1142
1286
|
if (selector === null)
|
|
1143
1287
|
return failure(selector === null && typeof path === "string" && !path.includes("/")
|
|
1144
|
-
?
|
|
1145
|
-
:
|
|
1288
|
+
? `Use @ref or a ${surface.root}/ logical path`
|
|
1289
|
+
: `Invalid ${authoringToolName(surface, "read")} path`, locale);
|
|
1146
1290
|
const result = snapshot.query({
|
|
1147
1291
|
kind: "read_document",
|
|
1148
1292
|
document: selector,
|
|
1149
1293
|
referenceProjection: "short_ref",
|
|
1150
1294
|
});
|
|
1151
1295
|
if (result.kind === "error") {
|
|
1152
|
-
if (repairableDamagedWorldRead(snapshot, path, result))
|
|
1296
|
+
if (repairableDamagedWorldRead(snapshot, path, surface, result))
|
|
1153
1297
|
reads.damagedWorldPaths.add(path);
|
|
1154
|
-
return renderQueryFailure(result, locale);
|
|
1298
|
+
return renderQueryFailure(result, surface, locale);
|
|
1155
1299
|
}
|
|
1156
1300
|
if (result.kind !== "read_document")
|
|
1157
1301
|
throw new Error("Unexpected read result");
|
|
1158
1302
|
reads.worldDocumentIds.add(result.document.documentId);
|
|
1159
1303
|
return success(`# ${localized(locale, "Exact read", "精确读取")} @${result.document.shortRef}\n\ntitle: ${result.document.title}\nsummary: ${result.document.summary}\ncodec: ${result.document.codec}\nlogicalPath: ${result.document.logicalPath}\n[body]\n${result.body.trimEnd()}\n[/body]\n\n---\nComplete: yes`);
|
|
1160
1304
|
}
|
|
1161
|
-
function repairableDamagedWorldRead(snapshot, path, result) {
|
|
1162
|
-
if (!path.startsWith(
|
|
1305
|
+
function repairableDamagedWorldRead(snapshot, path, surface, result) {
|
|
1306
|
+
if (!path.startsWith(`${surface.root}/`) ||
|
|
1163
1307
|
!snapshot.files.some(({ path: candidate }) => candidate === path))
|
|
1164
1308
|
return false;
|
|
1165
1309
|
// A failed exact read authorizes replacement only when the source itself is
|
|
@@ -1170,11 +1314,11 @@ function repairableDamagedWorldRead(snapshot, path, result) {
|
|
|
1170
1314
|
code !== "document_not_found" &&
|
|
1171
1315
|
code !== "document_ambiguous");
|
|
1172
1316
|
}
|
|
1173
|
-
function documentSelector(value) {
|
|
1317
|
+
function documentSelector(value, surface) {
|
|
1174
1318
|
if (value.startsWith("@"))
|
|
1175
1319
|
return value.length > 1 ? { shortRef: value.slice(1) } : null;
|
|
1176
|
-
if (value.startsWith(
|
|
1177
|
-
const path = safePath(value);
|
|
1320
|
+
if (value.startsWith(`${surface.root}/`)) {
|
|
1321
|
+
const path = safePath(value, surface);
|
|
1178
1322
|
return /\.(?:ya?ml|md)$/u.test(path) ? { logicalPath: path } : null;
|
|
1179
1323
|
}
|
|
1180
1324
|
return null;
|
|
@@ -1231,20 +1375,21 @@ function projectOpaqueDocumentIds(snapshot, path, source) {
|
|
|
1231
1375
|
}
|
|
1232
1376
|
return changed ? document.toString({ indent: 2, lineWidth: 0 }) : source;
|
|
1233
1377
|
}
|
|
1234
|
-
function renderQueryFailure(result, locale) {
|
|
1378
|
+
function renderQueryFailure(result, surface, locale) {
|
|
1235
1379
|
return failure(result.diagnostics
|
|
1236
|
-
.map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, locale)}`)
|
|
1380
|
+
.map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, surface, locale)}`)
|
|
1237
1381
|
.join("\n"), locale);
|
|
1238
1382
|
}
|
|
1239
|
-
function publicSettingDiagnosticMessage(code, message, locale) {
|
|
1383
|
+
function publicSettingDiagnosticMessage(code, message, surface, locale) {
|
|
1384
|
+
const writeFile = authoringToolName(surface, "write_file");
|
|
1240
1385
|
if (code === "document_header_invalid" ||
|
|
1241
1386
|
code === "document_identity_invalid" ||
|
|
1242
1387
|
code === "invalid_document_header")
|
|
1243
|
-
return localized(locale,
|
|
1388
|
+
return localized(locale, `Document storage metadata is missing or damaged. Repair this document with ${writeFile} using its ref, title, summary, aliases, and body`, `文档存储元信息缺失或损坏;请用 ${writeFile} 提供 ref、title、summary、aliases 和正文进行修复`);
|
|
1244
1389
|
if (code === "document_identity_duplicate" || code === "duplicate_id")
|
|
1245
1390
|
return localized(locale, "Runtime-owned storage identity is duplicated; repair the affected document without supplying an identity", "Runtime 维护的存储身份发生重复;请修复受影响文档,不要提供内部身份");
|
|
1246
1391
|
if (code === "document_short_ref_invalid")
|
|
1247
|
-
return localized(locale,
|
|
1392
|
+
return localized(locale, `Document ref must be a 2-to-32-character lowercase ASCII short handle; repair it with ${writeFile} and public metadata`, `文档 ref 必须是 2~32 位小写 ASCII 短句柄;请用 ${writeFile} 和公开元信息修复`);
|
|
1248
1393
|
if (code === "document_short_ref_duplicate" || code === "duplicate_ref")
|
|
1249
1394
|
return localized(locale, "Document refs must be unique; choose a different ref for the affected document", "文档 ref 必须唯一;请为受影响文档选择另一个 ref");
|
|
1250
1395
|
return message
|
|
@@ -1306,13 +1451,13 @@ function fileDiff(baseFiles, draftFiles) {
|
|
|
1306
1451
|
];
|
|
1307
1452
|
});
|
|
1308
1453
|
}
|
|
1309
|
-
function safePath(path) {
|
|
1454
|
+
function safePath(path, surface) {
|
|
1310
1455
|
if (path.startsWith("/") ||
|
|
1311
1456
|
path.includes("\\") ||
|
|
1312
1457
|
path
|
|
1313
1458
|
.split("/")
|
|
1314
1459
|
.some((part) => part === "." || part === ".." || part.length === 0))
|
|
1315
|
-
throw new SettingAuthoringError(`Unsafe
|
|
1460
|
+
throw new SettingAuthoringError(`Unsafe ${surface.phrases.en.slug} path: ${path}`);
|
|
1316
1461
|
return path;
|
|
1317
1462
|
}
|
|
1318
1463
|
function requiredString(value, name) {
|