@whispering233/ai-editor-shared 0.0.33 → 0.0.38

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.
Files changed (54) hide show
  1. package/dist/constants/backup.d.ts +14 -0
  2. package/dist/constants/backup.d.ts.map +1 -1
  3. package/dist/constants/backup.js +5 -0
  4. package/dist/constants/backup.js.map +1 -1
  5. package/dist/constants/delta.d.ts +25 -0
  6. package/dist/constants/delta.d.ts.map +1 -0
  7. package/dist/constants/delta.js +37 -0
  8. package/dist/constants/delta.js.map +1 -0
  9. package/dist/constants/entity.d.ts +17 -0
  10. package/dist/constants/entity.d.ts.map +1 -1
  11. package/dist/constants/entity.js +26 -0
  12. package/dist/constants/entity.js.map +1 -1
  13. package/dist/constants/index.d.ts +1 -0
  14. package/dist/constants/index.d.ts.map +1 -1
  15. package/dist/constants/index.js +2 -1
  16. package/dist/constants/index.js.map +1 -1
  17. package/dist/constants/tool.d.ts +2 -2
  18. package/dist/constants/tool.d.ts.map +1 -1
  19. package/dist/constants/tool.js +2 -2
  20. package/dist/constants/tool.js.map +1 -1
  21. package/dist/types/ability-panel.d.ts +16 -0
  22. package/dist/types/ability-panel.d.ts.map +1 -0
  23. package/dist/types/ability-panel.js +9 -0
  24. package/dist/types/ability-panel.js.map +1 -0
  25. package/dist/types/api.d.ts +34 -24
  26. package/dist/types/api.d.ts.map +1 -1
  27. package/dist/types/api.js +65 -9
  28. package/dist/types/api.js.map +1 -1
  29. package/dist/types/cloud.d.ts +200 -0
  30. package/dist/types/cloud.d.ts.map +1 -0
  31. package/dist/types/cloud.js +7 -0
  32. package/dist/types/cloud.js.map +1 -0
  33. package/dist/types/entity.d.ts +1 -1
  34. package/dist/types/entity.d.ts.map +1 -1
  35. package/dist/types/index.d.ts +2 -0
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/project.d.ts +1 -1
  38. package/dist/utils/ability-panel.d.ts +67 -0
  39. package/dist/utils/ability-panel.d.ts.map +1 -0
  40. package/dist/utils/ability-panel.js +159 -0
  41. package/dist/utils/ability-panel.js.map +1 -0
  42. package/dist/utils/backup.d.ts +50 -16
  43. package/dist/utils/backup.d.ts.map +1 -1
  44. package/dist/utils/backup.js +92 -59
  45. package/dist/utils/backup.js.map +1 -1
  46. package/dist/utils/index.d.ts +2 -0
  47. package/dist/utils/index.d.ts.map +1 -1
  48. package/dist/utils/index.js +2 -0
  49. package/dist/utils/index.js.map +1 -1
  50. package/dist/utils/relation-type.d.ts +10 -0
  51. package/dist/utils/relation-type.d.ts.map +1 -0
  52. package/dist/utils/relation-type.js +33 -0
  53. package/dist/utils/relation-type.js.map +1 -0
  54. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ // 能力面板(character.data.ability_panel)纯函数库(卡片 2.5)
2
+ //
3
+ // 职责:结构防御解析 / 顶层分组名 / 叶子路径枚举 / 模板派生深拷贝 / 值类型判定。
4
+ // 契约:docs/db/schema.md「人物 data 分层」(结构不变式)、docs/design/10-data-model.md §14
5
+ //(结构人工编辑、只有已存在叶子可被 Delta 改、模板派生 = 结构快照深拷贝)。
6
+ //
7
+ // 消费方:db 列表摘要与统计(2.1/2.2)、变更记录的叶子字段路径、人物页 panel-tree(批次 3)、
8
+ // 面板模板与「从角色复制」(结构快照)。
9
+ //
10
+ // 宽校验契约:**任何非法结构都不得抛错**——读取端按「空面板 / 跳过坏元素」处理,
11
+ // 坏 JSON 不能打挂 computeState 或列表接口(同 custom_fields 的宽松先例)。
12
+ // **读端自动规范化(幂等)**:`panelLeafPaths` / `panelTopLevelNames` / `cloneAbilityPanel`
13
+ // 接受 `unknown`(raw JSON 可直接传入)并在内部先过 `parseAbilityPanel`——消费方不得假定
14
+ // `data.ability_panel` 是规范形状(见 docs/db/schema.md「人物 data 分层」宽校验段)。
15
+ //
16
+ // 路径解析口径(docs/db/schema.md「面板路径解析口径」):点分路径的**数组段按同层 `name` 匹配、
17
+ // 取先序第一个**;名字含 `.` 或同层重名属**已知歧义**(解析侧不拒绝,UI 结构编辑给防呆提示);
18
+ // 路径前缀由 `abilityPanelFieldPath` 统一拼接,消费方禁止手拼。
19
+ /** 面板在 character.data 中的键名(**唯一拼写**;字段路径拼接必须走 `abilityPanelFieldPath`) */
20
+ export const ABILITY_PANEL_DATA_KEY = "ability_panel";
21
+ /**
22
+ * Delta 字段路径拼接:`ability_panel.<叶子点分路径>`(如 `ability_panel.火系.等级`)。
23
+ *
24
+ * 接受 `panelLeafPaths` 的产物(叶子对象)或其 `path` 字符串——**消费方不得手拼 `ability_panel.` 前缀**
25
+ * (前缀拼写漂移会让 Delta 路径静默失效,见 docs/db/schema.md「面板路径解析口径」)。
26
+ */
27
+ export function abilityPanelFieldPath(pathOrLeaf) {
28
+ const path = typeof pathOrLeaf === "string" ? pathOrLeaf : pathOrLeaf.path;
29
+ return `${ABILITY_PANEL_DATA_KEY}.${path}`;
30
+ }
31
+ /** 数值字面量:整数/小数 + 可选负号(不支持指数、千分位、前导 +) */
32
+ const ABILITY_NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/;
33
+ /**
34
+ * 判定分支节点(`children` 为**非空数组**)——**分支/叶子的唯一拼写**,
35
+ * UI 与工具不得各自内联 `node.children?.length` 判断(不变式:分支 ⇔ 非空 children 数组)。
36
+ *
37
+ * 防御:先做 `Array.isArray`——宽校验 JSON 里 `children` 可能是字符串/对象
38
+ *(`{ children:"s" }` 在只比 `length > 0` 时会误判为分支)。
39
+ */
40
+ export function isAbilityBranch(node) {
41
+ return Array.isArray(node.children) && node.children.length > 0;
42
+ }
43
+ /**
44
+ * 防御解析能力面板:任意 JSON 值 → **规范形状**节点数组。
45
+ *
46
+ * 口径:
47
+ * - 非数组输入(null/对象/字符串/number)→ `[]`(空面板)
48
+ * - 坏元素**跳过**、保留其余合法元素:非对象、数组元素、`name` 非字符串或空/纯空白
49
+ * - `children` 非数组、或为空数组 → **归一为叶子**(与「删掉最后一个子节点即降级为叶子」一致)
50
+ * - 分支节点上的 `value` **丢弃**(分支不可赋值);叶子 `value` 仅接受 `string` / 有限 `number`
51
+ * (`null` / `NaN` / `Infinity` → 视为缺省)
52
+ * - 未知键丢弃(返回规范形状);**不修改入参**(不改写 `name` 原文——仅用于空名校验时为只读)
53
+ */
54
+ export function parseAbilityPanel(value) {
55
+ if (!Array.isArray(value))
56
+ return [];
57
+ return parseAbilityNodes(value);
58
+ }
59
+ /**
60
+ * 叶子枚举(**先序遍历序**;供变更记录字段下拉与 Delta 字段路径使用)。
61
+ *
62
+ * `path` = 祖先名与自身名逐字以 `.` 拼接(如 `火系.等级`)、**不做转义**——
63
+ * 名字本身含 `.` 时路径有歧义(`a.b` 既可能是 `a` → `b`,也可能是单节点名 `a.b`);
64
+ * 已知边界:面板 UI 侧对含 `.` 的名字给防呆提示,解析侧不拒绝(宽校验)。
65
+ *
66
+ * **输入可 raw**(自动过 `parseAbilityPanel`)——直接传 `entity.data.ability_panel` 安全。
67
+ */
68
+ export function panelLeafPaths(panel) {
69
+ const out = [];
70
+ collectLeaves(parseAbilityPanel(panel), "", out);
71
+ return out;
72
+ }
73
+ /**
74
+ * 顶层分组名(顺序 = 数组顺序;**不去重**——消费者按需去重/计数,见 2.2 统计口径)。
75
+ * **输入可 raw**(自动过 `parseAbilityPanel`)。
76
+ */
77
+ export function panelTopLevelNames(panel) {
78
+ return parseAbilityPanel(panel).map((node) => node.name);
79
+ }
80
+ /**
81
+ * 结构快照深拷贝(面板模板 / 从已有角色复制结构)。
82
+ *
83
+ * 返回**规范形状副本**:不共享任何引用(数组与嵌套对象全部新建)、丢弃分支上的 `value`、
84
+ * 空 `children` 数组归一为叶子(与 `parseAbilityPanel` 同口径);叶子 `value` 原样保留
85
+ * (模板里的值 = 派生后的默认值,见 `10-data-model.md` §14 不变式 6)。
86
+ * **输入可 raw**(自动过 `parseAbilityPanel`)——非规范输入先规范化再拷贝。
87
+ */
88
+ export function cloneAbilityPanel(panel) {
89
+ return parseAbilityPanel(panel).map(cloneAbilityNode);
90
+ }
91
+ /**
92
+ * UI 值输入 → 存储值:
93
+ * - `trim` 后为空串/纯空白 → `undefined`(空值,叶子允许)
94
+ * - 纯数字字面量 → `number`(`"12"` → 12);超出双精度可表示范围(非有限数)→ 退回 `string`
95
+ * - 其余非空文本 → `string`(原样去首尾空白)
96
+ *
97
+ * 已知瑕疵:`"007"` 存为 `7`(丢前导零——数值语义);需保留前导零时写非纯数字文本(如 `"007级"`)。
98
+ */
99
+ export function coerceAbilityValue(raw) {
100
+ const trimmed = raw.trim();
101
+ if (trimmed === "")
102
+ return undefined;
103
+ if (!ABILITY_NUMBER_PATTERN.test(trimmed))
104
+ return trimmed;
105
+ const parsed = Number(trimmed);
106
+ return Number.isFinite(parsed) ? parsed : trimmed;
107
+ }
108
+ /** 递归收集叶子(`prefix` = 祖先名点分前缀;顶层传空串) */
109
+ function collectLeaves(nodes, prefix, out) {
110
+ for (const node of nodes) {
111
+ const path = prefix === "" ? node.name : `${prefix}.${node.name}`;
112
+ if (isAbilityBranch(node)) {
113
+ collectLeaves(node.children ?? [], path, out);
114
+ continue;
115
+ }
116
+ out.push(node.value === undefined ? { path, name: node.name } : { path, name: node.name, value: node.value });
117
+ }
118
+ }
119
+ /** 单节点深拷贝(分支丢弃 value、空 children 归一为叶子——规范形状) */
120
+ function cloneAbilityNode(node) {
121
+ const children = node.children;
122
+ if (Array.isArray(children) && children.length > 0) {
123
+ return { name: node.name, children: cloneAbilityPanel(children) };
124
+ }
125
+ return node.value === undefined ? { name: node.name } : { name: node.name, value: node.value };
126
+ }
127
+ /** 单节点防御解析(坏元素 → null 由调用方跳过) */
128
+ function parseAbilityNode(raw) {
129
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
130
+ return null;
131
+ const record = raw;
132
+ const name = record.name;
133
+ if (typeof name !== "string" || name.trim() === "")
134
+ return null; // 空名/纯空白名 → 节点无意义
135
+ const children = Array.isArray(record.children) ? parseAbilityNodes(record.children) : [];
136
+ if (children.length > 0)
137
+ return { name, children }; // 分支:丢弃 value(不可赋值)
138
+ const value = normalizeAbilityValue(record.value);
139
+ return value === undefined ? { name } : { name, value };
140
+ }
141
+ /** 节点数组防御解析(跳过坏元素) */
142
+ function parseAbilityNodes(raw) {
143
+ const out = [];
144
+ for (const item of raw) {
145
+ const node = parseAbilityNode(item);
146
+ if (node !== null)
147
+ out.push(node);
148
+ }
149
+ return out;
150
+ }
151
+ /** 叶子值归一:仅接受 `string` / 有限 `number`;其余(null/NaN/Infinity/对象/布尔)→ 缺省 */
152
+ function normalizeAbilityValue(raw) {
153
+ if (typeof raw === "string")
154
+ return raw;
155
+ if (typeof raw === "number" && Number.isFinite(raw))
156
+ return raw;
157
+ return undefined;
158
+ }
159
+ //# sourceMappingURL=ability-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability-panel.js","sourceRoot":"","sources":["../../src/utils/ability-panel.ts"],"names":[],"mappings":"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"}
@@ -1,38 +1,48 @@
1
- import { type BackupKind } from "../constants/backup.js";
2
- /** 备份文件名解析结果(time + 类型 + 可选自定义名称) */
1
+ import { type BackupKind, type BackupStats } from "../constants/backup.js";
2
+ /** 备份文件名解析结果(time + 类型 + 可选标签 + 设备 + 统计) */
3
3
  export interface ParsedBackupFileName {
4
- /** 本地时区时间(文件名时间戳;旧秒级格式 → 毫秒为 0) */
4
+ /** 本地时区时间(文件名时间戳,毫秒精度) */
5
5
  time: Date;
6
- /** 备份类型(由文件名 kind 段解析推断:auto / manual) */
6
+ /** 备份类型(由类型段解析:自动/手动) */
7
7
  kind: BackupKind;
8
- /** 手动备份自定义名称(自动备份/快照/旧秒级备份无此字段) */
8
+ /** 用户自定义标签(自动备份/覆盖前快照无此字段) */
9
9
  name?: string;
10
+ /** 来源设备(唯一格式恒有此段) */
11
+ device: string;
12
+ /** 尾部三段统计(唯一格式恒有此段) */
13
+ stats: BackupStats;
10
14
  }
11
15
  /**
12
- * 解析备份文件名 → { time, kind, name? }。
16
+ * 解析备份文件名 → { time, kind, name?, device, stats };不符唯一格式一律 null
13
17
  *
14
18
  * 格式不符返回 null:非时间戳形状(含路径分隔符、`..`、空串、非数字、多后缀等)一律
15
- * 拒绝——白名单校验语义(restore 流程第 1 步)。兼容三类:
16
- * 新格式(带/不带 kind 段)/ 旧带名称无 kind 段 / 旧秒级格式(历史备份不迁移)。
19
+ * 拒绝——白名单校验语义(restore 流程第 1 步)。**只认唯一格式**:三类旧命名(秒级 /
20
+ * 带名称无类型段 / 单字母 `-m`/`-a` 段)同样返回 null——文件留盘但不识别。
17
21
  * 数字合法但日期不存在(如 20261301、2 月 30 日)同样返回 null:Date 构造会对
18
22
  * 越界值滚动进位(20261301 → 2027-01-01),回读比对不一致即拒绝。
19
23
  *
20
- * @param fileName 备份文件名(如 "20260813-101530123.zip" / "20260813-101530123-m-定稿.zip"
21
- * @returns 本地时区时间 + 类型 + 可选名称;格式不符返回 null
24
+ * @param fileName 备份文件名(如 "20260813-101530123-自动-苹果本-人物32-设定58-章120.zip")
25
+ * @returns 本地时区时间 + 类型 + 可选标签 + 设备 + 统计;格式不符返回 null
22
26
  */
23
27
  export declare function parseBackupFileName(fileName: string): ParsedBackupFileName | null;
24
28
  /**
25
- * 生成备份文件名:Date → `<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip`(本地时区,与 parse 对称)。
26
- * 四类输出:
27
- * auto 无名称 → 纯时间戳;auto 带名称 → `-a-<名称>`;manual 无名称 → `-m`;manual 带名称 → `-m-<名称>`。
28
- * 毫秒精度;文件名时间戳命名,`.backups/` 内按文件名排序即时间序(保留策略依赖)。
29
+ * 生成备份文件名(**唯一格式**):`<时间戳>-<自动|手动>-<设备>[-<标签>]-人物N-设定N-章N.zip`。
29
30
  *
31
+ * `device` 与 `stats` 必填(写入侧只有这一种形态,调用方保证:device 已 sanitize 且非空、
32
+ * stats 描述该备份的内容)——与 parseBackupFileName 互为逆运算;类型系统即约束,无运行时兜底分支。
33
+ *
34
+ * 毫秒精度;本地时区;`.backups/` 内按文件名排序即时间序(保留策略依赖)。
35
+ *
36
+ * @param opts.device 来源设备名(已 sanitize;禁 `-`)
37
+ * @param opts.stats 备份内容的未软删存量快照
30
38
  * @param opts.kind 备份类型(缺省 "auto")
31
- * @param opts.name 自定义名称——调用方保证已 sanitize(见 sanitizeBackupName);本函数纯拼接
39
+ * @param opts.name 用户标签——调用方保证已 sanitize(见 sanitizeBackupName);本函数纯拼接
32
40
  */
33
- export declare function formatBackupFileName(date: Date, opts?: {
41
+ export declare function formatBackupFileName(date: Date, opts: {
34
42
  kind?: BackupKind;
35
43
  name?: string;
44
+ device: string;
45
+ stats: BackupStats;
36
46
  }): string;
37
47
  /**
38
48
  * 备份名称规则:
@@ -46,4 +56,28 @@ export declare function formatBackupFileName(date: Date, opts?: {
46
56
  * @returns 规范化后的名称;非法 → null(调用方转 400 VALIDATION_ERROR)
47
57
  */
48
58
  export declare function sanitizeBackupName(raw: string): string | null;
59
+ /**
60
+ * 设备名规则(文件名的第 3 段):
61
+ * - trim 后非空,长度 ≤ MAX_DEVICE_NAME_LENGTH(constants/backup.ts)
62
+ * - **禁 `-`**——段分隔符:设备段含 `-` 时与标签段不可区分(解析无法定位段边界)
63
+ * - 禁路径分隔符(/ \)与保留字符(: * ? " < > |)与控制字符
64
+ * - 禁纯点(. / ..)
65
+ *
66
+ * 服务端唯一执行点:云端配置写入(`PUT /api/v1/cloud/config`);缺省设备名派生自 hostname
67
+ * (见 deviceNameFromHostname,结果必然合法)。
68
+ * @returns 规范化后的设备名;非法 → null(调用方转 400 VALIDATION_ERROR)
69
+ */
70
+ export declare function sanitizeDeviceName(raw: string): string | null;
71
+ /**
72
+ * 主机名 → 缺省设备名(纯函数,os.hostname() 的读取在服务端):
73
+ * ① trim → 取第一个 `.` 之前的部分(`MacBook-Pro.local` → `MacBook-Pro`)→ ② 非法字符(含 `-`,
74
+ * Windows 主机名常见)替换为 `_` 并折叠连续 `_` → ③ **剥首尾空白与 `_`**(必须在截断前做,
75
+ * 否则 `"__abc__"` 类会残留)→ ④ 截到 16 字符 → ⑤ **再去尾空白与 `_`**(截断可能切出新的尾部)
76
+ * → ⑥ 空(含纯空白 / 纯点)兜底 `"unknown"`。
77
+ *
78
+ * **不变式**:对任意输入,`sanitizeDeviceName(deviceNameFromHostname(h)) === deviceNameFromHostname(h)`
79
+ * 且恒非 null(单测以样本矩阵钤住:`"_ _:>"`、`" ? 99|"`、`" "`、`"---"`、
80
+ * `"a".repeat(15) + " "`、`"aaaaaaaaaaaaaaa bcdef"` 等)。
81
+ */
82
+ export declare function deviceNameFromHostname(hostname: string): string;
49
83
  //# sourceMappingURL=backup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/utils/backup.ts"],"names":[],"mappings":"AAaA,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAajF,qCAAqC;AACrC,MAAM,WAAW,oBAAoB;IACpC,mCAAmC;IAClC,IAAI,EAAE,IAAI,CAAC;IACZ,0CAA0C;IACzC,IAAI,EAAE,UAAU,CAAC;IAClB,mCAAmC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CA0CjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAUpG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa7D"}
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/utils/backup.ts"],"names":[],"mappings":"AAeA,OAAO,EAAkD,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAW3H,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC,0BAA0B;IACzB,IAAI,EAAE,IAAI,CAAC;IACZ,yBAAyB;IACxB,IAAI,EAAE,UAAU,CAAC;IAClB,8BAA8B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACpB,MAAM,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACtB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAgCjF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAC7E,MAAM,CAOR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU/D"}
@@ -1,42 +1,39 @@
1
- // 备份文件名工具(.backups/ 时间戳命名 + 类型标签 + 重命名)
1
+ // 备份文件名工具(.backups/ 时间戳命名 + 类型/设备标签 + 尾部统计段 + 重命名)
2
2
  // 纯函数,零 Node 依赖(client 浏览器打包安全)
3
3
  // 时区约定:文件名时间戳为本地时区(无时区后缀),format/parse 对称使用本地时间
4
4
  //
5
- // 格式——<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip,kind 段单字母 m/a
6
- // - 自动备份/覆盖前快照(无名称):<YYYYMMDD-HHmmssSSS>.zip,如 20260813-101530123.zip
7
- // - 手动备份(无名称):<YYYYMMDD-HHmmssSSS>-m.zip
8
- // - 手动备份(带名称):<YYYYMMDD-HHmmssSSS>-m-<名称>.zip,如 20260813-101530123-m-定稿.zip
9
- // - 自动备份重命名后:<YYYYMMDD-HHmmssSSS>-a-<名称>.zip
10
- // 兼容解析(不迁移,历史备份不重命名):
11
- // - 旧秒级:<YYYYMMDD-HHmmss>.zip → kind=auto(毫秒为 0)
12
- // - 旧带名称无 kind 段:<YYYYMMDD-HHmmssSSS>-<名称>.zip → kind=manual
13
- import { MAX_BACKUP_NAME_LENGTH } from "../constants/backup.js";
14
- /** 新格式:<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip——组 8 = kind 段(a→auto、m→manual、
15
- * auto)、组 9 = 名称;名称部分 [^/\\]+ 拒绝路径分隔符(防路径穿越);写入侧
16
- * sanitizeBackupName 严格限制字符集。
17
- * 已知歧义(接受):旧「名称恰为单字母 a/m」的备份(如 <时间戳>-m.zip)按本正则
18
- * 解析为 kind 标记(auto/manual 无名称),不按旧带名称格式回退。*/
19
- const BACKUP_FILE_NAME_PATTERN_MS = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})(\d{3})(?:-(a|m)(?:-([^/\\]+?))?)?\.zip$/;
20
- /** 旧带名称(无 kind 段,格式):<YYYYMMDD-HHmmssSSS>-<名称>.zip → 兼容为 manual + 名称 */
21
- const BACKUP_FILE_NAME_PATTERN_LEGACY_NAMED = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})(\d{3})-([^/\\]+?)\.zip$/;
22
- /** 旧格式(秒精度):仅解析兼容 → kind=auto */
23
- const BACKUP_FILE_NAME_PATTERN_LEGACY = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})\.zip$/;
5
+ // **唯一格式(写入 = 解析)**——<YYYYMMDD-HHmmssSSS>-<自动|手动>-<设备>[-<标签>]-人物N-设定N-章N.zip:
6
+ // - 自动备份/覆盖前快照:<时间戳>-自动-苹果本-人物32-设定58-章120.zip
7
+ // - 手动备份(带标签):<时间戳>-手动-苹果本-定稿-人物32-设定58-章120.zip
8
+ // 段序固定:尾部三段统计可**从尾部倒切**,故设备/标签里出现类似字样也不歧义;
9
+ // 设备段禁 `-`(见 sanitizeDeviceName),标签为自由文本(1-30 字符)。
10
+ //
11
+ // 早期开发期的三类旧命名(秒级 <YYYYMMDD-HHmmss>.zip、带名称无类型段 <YYYYMMDD-HHmmssSSS>-<名称>.zip、
12
+ // 单字母 `-m`/`-a` 段)**不再解析**:文件留在磁盘但不识别(不出现在列表、不可恢复、不参与保留策略),
13
+ // 也不做重命名迁移(旧份没有设备/统计信息,硬补会谎报——统计必须描述该备份的内容)。
14
+ // 升级兜底(只剩旧命名时立即生成一份新格式备份)见 server `ensureParseableBackup`。
15
+ import { MAX_BACKUP_NAME_LENGTH, MAX_DEVICE_NAME_LENGTH } from "../constants/backup.js";
16
+ /**
17
+ * 唯一格式(设备段 + 尾部三段统计):<时间戳>-<自动|手动>-<设备>[-<标签>]-人物N-设定N-章N.zip
18
+ * 设备与标签两组均排除路径分隔符(`/` `\\`)——解析结果是 restore/rename 的文件名白名单,
19
+ * 含分隔符即路径穿越。设备段另禁 `-`(与写入侧 sanitizeDeviceName 同口径);标签自由文本
20
+ * (可含 `-`,故统计段从尾部固定倒切)。
21
+ */
22
+ const BACKUP_FILE_NAME_PATTERN = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})(\d{3})-(手动|自动)-([^-/\\]+?)(?:-([^/\\]+?))?-人物(\d+)-设定(\d+)-章(\d+)\.zip$/;
24
23
  /**
25
- * 解析备份文件名 → { time, kind, name? }。
24
+ * 解析备份文件名 → { time, kind, name?, device, stats };不符唯一格式一律 null
26
25
  *
27
26
  * 格式不符返回 null:非时间戳形状(含路径分隔符、`..`、空串、非数字、多后缀等)一律
28
- * 拒绝——白名单校验语义(restore 流程第 1 步)。兼容三类:
29
- * 新格式(带/不带 kind 段)/ 旧带名称无 kind 段 / 旧秒级格式(历史备份不迁移)。
27
+ * 拒绝——白名单校验语义(restore 流程第 1 步)。**只认唯一格式**:三类旧命名(秒级 /
28
+ * 带名称无类型段 / 单字母 `-m`/`-a` 段)同样返回 null——文件留盘但不识别。
30
29
  * 数字合法但日期不存在(如 20261301、2 月 30 日)同样返回 null:Date 构造会对
31
30
  * 越界值滚动进位(20261301 → 2027-01-01),回读比对不一致即拒绝。
32
31
  *
33
- * @param fileName 备份文件名(如 "20260813-101530123.zip" / "20260813-101530123-m-定稿.zip"
34
- * @returns 本地时区时间 + 类型 + 可选名称;格式不符返回 null
32
+ * @param fileName 备份文件名(如 "20260813-101530123-自动-苹果本-人物32-设定58-章120.zip")
33
+ * @returns 本地时区时间 + 类型 + 可选标签 + 设备 + 统计;格式不符返回 null
35
34
  */
36
35
  export function parseBackupFileName(fileName) {
37
- const m = BACKUP_FILE_NAME_PATTERN_MS.exec(fileName) ??
38
- BACKUP_FILE_NAME_PATTERN_LEGACY_NAMED.exec(fileName) ??
39
- BACKUP_FILE_NAME_PATTERN_LEGACY.exec(fileName);
36
+ const m = BACKUP_FILE_NAME_PATTERN.exec(fileName);
40
37
  if (m === null)
41
38
  return null;
42
39
  const y = Number(m[1]);
@@ -45,8 +42,7 @@ export function parseBackupFileName(fileName) {
45
42
  const h = Number(m[4]);
46
43
  const mi = Number(m[5]);
47
44
  const s = Number(m[6]);
48
- // 毫秒位:新格式第 7 组捕获,旧格式无(= 0)
49
- const milli = m[7] !== undefined ? Number(m[7]) : 0;
45
+ const milli = Number(m[7]); // 唯一格式恒捕获毫秒段
50
46
  const date = new Date(y, mo - 1, d, h, mi, s, milli);
51
47
  // 越界日期回读比对(Date 滚动进位后各分量必然变化,一致性校验即拒绝)
52
48
  if (date.getFullYear() !== y ||
@@ -58,45 +54,37 @@ export function parseBackupFileName(fileName) {
58
54
  date.getMilliseconds() !== milli) {
59
55
  return null;
60
56
  }
61
- const result = { time: date, kind: "auto" };
62
- // kind/名称:新格式组 8 为 kind 段(a/m,缺省 auto)、组 9 为名称;旧带名称格式组 8 为名称
63
- // (无 kind 段 → 兼容为 manual)。歧义注:旧「名称恰为单字母 a/m」的备份按新格式
64
- // 解析为 kind 标记(无名称),接受——见 BACKUP_FILE_NAME_PATTERN_MS 注释。
65
- if (m[8] === "a" || m[8] === "m") {
66
- result.kind = m[8] === "a" ? "auto" : "manual";
67
- // 名称组 [^/\\]+ 的 + 已保证非空(组匹配即非空),仅需判 undefined
68
- if (m[9] !== undefined) {
69
- result.name = m[9];
70
- }
71
- }
72
- else if (m[8] !== undefined) {
73
- result.kind = "manual"; // 旧带名称(格式)→ 兼容为 manual
74
- result.name = m[8];
75
- }
57
+ // 组序:8 = 类型段(手动/自动)、9 = 设备、10 = 标签(可选)、11-13 = 尾部三段统计
58
+ const result = {
59
+ time: date,
60
+ kind: m[8] === "手动" ? "manual" : "auto",
61
+ device: m[9],
62
+ stats: { characters: Number(m[11]), settings: Number(m[12]), chapters: Number(m[13]) },
63
+ };
64
+ if (m[10] !== undefined)
65
+ result.name = m[10];
76
66
  return result;
77
67
  }
78
68
  /**
79
- * 生成备份文件名:Date → `<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip`(本地时区,与 parse 对称)。
80
- * 四类输出:
81
- * auto 无名称 纯时间戳;auto 带名称 `-a-<名称>`;manual 无名称 → `-m`;manual 带名称 → `-m-<名称>`。
82
- * 毫秒精度;文件名时间戳命名,`.backups/` 内按文件名排序即时间序(保留策略依赖)。
69
+ * 生成备份文件名(**唯一格式**):`<时间戳>-<自动|手动>-<设备>[-<标签>]-人物N-设定N-章N.zip`。
70
+ *
71
+ * `device` `stats` 必填(写入侧只有这一种形态,调用方保证:device sanitize 且非空、
72
+ * stats 描述该备份的内容)——与 parseBackupFileName 互为逆运算;类型系统即约束,无运行时兜底分支。
83
73
  *
74
+ * 毫秒精度;本地时区;`.backups/` 内按文件名排序即时间序(保留策略依赖)。
75
+ *
76
+ * @param opts.device 来源设备名(已 sanitize;禁 `-`)
77
+ * @param opts.stats 备份内容的未软删存量快照
84
78
  * @param opts.kind 备份类型(缺省 "auto")
85
- * @param opts.name 自定义名称——调用方保证已 sanitize(见 sanitizeBackupName);本函数纯拼接
79
+ * @param opts.name 用户标签——调用方保证已 sanitize(见 sanitizeBackupName);本函数纯拼接
86
80
  */
87
81
  export function formatBackupFileName(date, opts) {
88
82
  const pad = (n) => String(n).padStart(2, "0");
89
83
  const pad3 = (n) => String(n).padStart(3, "0");
90
84
  const base = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}${pad3(date.getMilliseconds())}`;
91
- const kind = opts?.kind ?? "auto";
92
- const name = opts?.name;
93
- if (kind === "auto" && (name === undefined || name.length === 0))
94
- return `${base}.zip`;
95
- if (kind === "auto")
96
- return `${base}-a-${name}.zip`;
97
- if (name === undefined || name.length === 0)
98
- return `${base}-m.zip`;
99
- return `${base}-m-${name}.zip`;
85
+ const typeSegment = (opts.kind ?? "auto") === "auto" ? "自动" : "手动";
86
+ const tag = opts.name !== undefined && opts.name.length > 0 ? `-${opts.name}` : "";
87
+ return `${base}-${typeSegment}-${opts.device}${tag}-人物${opts.stats.characters}-设定${opts.stats.settings}-章${opts.stats.chapters}.zip`;
100
88
  }
101
89
  /**
102
90
  * 备份名称规则:
@@ -128,4 +116,49 @@ export function sanitizeBackupName(raw) {
128
116
  return null;
129
117
  return name;
130
118
  }
119
+ /**
120
+ * 设备名规则(文件名的第 3 段):
121
+ * - trim 后非空,长度 ≤ MAX_DEVICE_NAME_LENGTH(constants/backup.ts)
122
+ * - **禁 `-`**——段分隔符:设备段含 `-` 时与标签段不可区分(解析无法定位段边界)
123
+ * - 禁路径分隔符(/ \)与保留字符(: * ? " < > |)与控制字符
124
+ * - 禁纯点(. / ..)
125
+ *
126
+ * 服务端唯一执行点:云端配置写入(`PUT /api/v1/cloud/config`);缺省设备名派生自 hostname
127
+ * (见 deviceNameFromHostname,结果必然合法)。
128
+ * @returns 规范化后的设备名;非法 → null(调用方转 400 VALIDATION_ERROR)
129
+ */
130
+ export function sanitizeDeviceName(raw) {
131
+ const name = raw.trim();
132
+ if (name === "")
133
+ return null;
134
+ if (name.length > MAX_DEVICE_NAME_LENGTH)
135
+ return null;
136
+ if (/[\\/:*?"<>|\x00-\x1f\x7f-]/.test(name))
137
+ return null;
138
+ if (/^\.+$/.test(name))
139
+ return null;
140
+ return name;
141
+ }
142
+ /**
143
+ * 主机名 → 缺省设备名(纯函数,os.hostname() 的读取在服务端):
144
+ * ① trim → 取第一个 `.` 之前的部分(`MacBook-Pro.local` → `MacBook-Pro`)→ ② 非法字符(含 `-`,
145
+ * Windows 主机名常见)替换为 `_` 并折叠连续 `_` → ③ **剥首尾空白与 `_`**(必须在截断前做,
146
+ * 否则 `"__abc__"` 类会残留)→ ④ 截到 16 字符 → ⑤ **再去尾空白与 `_`**(截断可能切出新的尾部)
147
+ * → ⑥ 空(含纯空白 / 纯点)兜底 `"unknown"`。
148
+ *
149
+ * **不变式**:对任意输入,`sanitizeDeviceName(deviceNameFromHostname(h)) === deviceNameFromHostname(h)`
150
+ * 且恒非 null(单测以样本矩阵钤住:`"_ _:>"`、`" ? 99|"`、`" "`、`"---"`、
151
+ * `"a".repeat(15) + " "`、`"aaaaaaaaaaaaaaa bcdef"` 等)。
152
+ */
153
+ export function deviceNameFromHostname(hostname) {
154
+ // ① 取第一个 `.` 之前的部分(域名后缀丢弃;`.local` 即在此步消失)
155
+ const first = hostname.trim().split(".")[0] ?? "";
156
+ // ② 非法字符 → `_`,并折叠连续 `_`
157
+ const mapped = first.replace(/[\\/:*?"<>|\x00-\x1f\x7f-]/g, "_").replace(/_+/g, "_");
158
+ // ③ 剥首尾空白与 `_`(截断前;空白同属非规范字符——否则会产出 sanitizeDeviceName 拒绝的值)
159
+ const trimmed = mapped.replace(/^[\s_]+|[\s_]+$/g, "");
160
+ // ④⑤ 截断后再去尾(截断可能把空白/`_` 切到末尾)
161
+ const cut = trimmed.slice(0, MAX_DEVICE_NAME_LENGTH).replace(/[\s_]+$/, "");
162
+ return cut === "" ? "unknown" : cut;
163
+ }
131
164
  //# sourceMappingURL=backup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/utils/backup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/utils/backup.ts"],"names":[],"mappings":"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"}
@@ -3,4 +3,6 @@ export * from "./format.js";
3
3
  export * from "./mapping.js";
4
4
  export * from "./backup.js";
5
5
  export * from "./reference-file.js";
6
+ export * from "./ability-panel.js";
7
+ export * from "./relation-type.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
@@ -5,4 +5,6 @@ export * from "./format.js";
5
5
  export * from "./mapping.js";
6
6
  export * from "./backup.js";
7
7
  export * from "./reference-file.js"; // 参考资料文件 frontmatter/文件名纯函数
8
+ export * from "./ability-panel.js"; // 能力面板(人物)结构纯函数
9
+ export * from "./relation-type.js"; // 关系类型自定义值语法校验(单一来源,卡片 8.2)
8
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,YAAY;AACZ,yFAAyF;AACzF,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC,CAAC,4BAA4B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,YAAY;AACZ,yFAAyF;AACzF,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC,CAAC,4BAA4B;AACjE,cAAc,oBAAoB,CAAC,CAAC,gBAAgB;AACpD,cAAc,oBAAoB,CAAC,CAAC,4BAA4B"}
@@ -0,0 +1,10 @@
1
+ /** 关系类型长度上限(`trim` 后计) */
2
+ export declare const RELATION_TYPE_MAX_LENGTH = 32;
3
+ /** 归一:去首尾空白(REST schema 落库前应用,避免「宿敌」与「宿敌 」并存为两个类型) */
4
+ export declare function normalizeRelationType(value: string): string;
5
+ /**
6
+ * 语法校验:合法 → null;非法 → 中文错误消息(直接可回显给作者)。
7
+ * 顺序:trim 后为空 → 控制字符 → 长度(按 trim 后计)。
8
+ */
9
+ export declare function relationTypeSyntaxError(value: unknown): string | null;
10
+ //# sourceMappingURL=relation-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relation-type.d.ts","sourceRoot":"","sources":["../../src/utils/relation-type.ts"],"names":[],"mappings":"AAQA,0BAA0B;AAC1B,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAK3C,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CASrE"}
@@ -0,0 +1,33 @@
1
+ // 关系类型(relation_type)自定义值语法校验的**单一来源**(卡片 8.2)
2
+ //
3
+ // 口径(docs/api/40-api-relation.md「关系类型自由化」,2026-09):relation_type 为自由字符串
4
+ // = 预定义 17 类 ∪ 自定义类型;语法 = `trim` 后非空、长度 ≤ 32、禁控制字符(U+0000–U+001F / U+007F)。
5
+ // 消费方三处共用本文件,禁止各自手抄:REST schema(shared/types/api.ts)、
6
+ // db `createRelation` 守卫(packages/db)、client 提交前预校验(packages/client)。
7
+ // **不 import zod**:纯函数,客户端可安全经 shared 根入口打包。
8
+ /** 关系类型长度上限(`trim` 后计) */
9
+ export const RELATION_TYPE_MAX_LENGTH = 32;
10
+ /** 控制字符(U+0000–U+001F 与 U+007F):落库即脏数据,禁 */
11
+ const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
12
+ /** 归一:去首尾空白(REST schema 落库前应用,避免「宿敌」与「宿敌 」并存为两个类型) */
13
+ export function normalizeRelationType(value) {
14
+ return value.trim();
15
+ }
16
+ /**
17
+ * 语法校验:合法 → null;非法 → 中文错误消息(直接可回显给作者)。
18
+ * 顺序:trim 后为空 → 控制字符 → 长度(按 trim 后计)。
19
+ */
20
+ export function relationTypeSyntaxError(value) {
21
+ if (typeof value !== "string")
22
+ return "关系类型必须是字符串";
23
+ const trimmed = value.trim();
24
+ if (trimmed === "")
25
+ return "关系类型不能为空";
26
+ if (CONTROL_CHAR_RE.test(value))
27
+ return "关系类型不能包含控制字符";
28
+ if (trimmed.length > RELATION_TYPE_MAX_LENGTH) {
29
+ return `关系类型不能超过 ${RELATION_TYPE_MAX_LENGTH} 个字符`;
30
+ }
31
+ return null;
32
+ }
33
+ //# sourceMappingURL=relation-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relation-type.js","sourceRoot":"","sources":["../../src/utils/relation-type.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,wEAAwE;AACxE,4EAA4E;AAC5E,sDAAsD;AACtD,sEAAsE;AACtE,6CAA6C;AAE7C,0BAA0B;AAC1B,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAE3C,4CAA4C;AAC5C,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD,sDAAsD;AACtD,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,YAAY,CAAC;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,UAAU,CAAC;IACtC,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,cAAc,CAAC;IACvD,IAAI,OAAO,CAAC,MAAM,GAAG,wBAAwB,EAAE,CAAC;QAC9C,OAAO,YAAY,wBAAwB,MAAM,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whispering233/ai-editor-shared",
3
- "version": "0.0.33",
3
+ "version": "0.0.38",
4
4
  "license": "MIT",
5
5
  "description": "前后端共享层:纯类型 + 常量 + 纯函数(零 Node 依赖)",
6
6
  "type": "module",