@whispering233/ai-editor-shared 0.0.21 → 0.0.29

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 (69) hide show
  1. package/dist/constants/backup.d.ts +5 -5
  2. package/dist/constants/backup.d.ts.map +1 -1
  3. package/dist/constants/backup.js +5 -7
  4. package/dist/constants/backup.js.map +1 -1
  5. package/dist/constants/entity.d.ts +7 -7
  6. package/dist/constants/entity.d.ts.map +1 -1
  7. package/dist/constants/entity.js +7 -8
  8. package/dist/constants/entity.js.map +1 -1
  9. package/dist/constants/hook.d.ts +4 -4
  10. package/dist/constants/hook.d.ts.map +1 -1
  11. package/dist/constants/hook.js +4 -5
  12. package/dist/constants/hook.js.map +1 -1
  13. package/dist/constants/index.js +1 -1
  14. package/dist/constants/index.js.map +1 -1
  15. package/dist/constants/outline.d.ts +1 -1
  16. package/dist/constants/outline.d.ts.map +1 -1
  17. package/dist/constants/outline.js +2 -3
  18. package/dist/constants/outline.js.map +1 -1
  19. package/dist/constants/tool.d.ts +6 -6
  20. package/dist/constants/tool.d.ts.map +1 -1
  21. package/dist/constants/tool.js +9 -10
  22. package/dist/constants/tool.js.map +1 -1
  23. package/dist/types/api.d.ts +69 -45
  24. package/dist/types/api.d.ts.map +1 -1
  25. package/dist/types/api.js +173 -162
  26. package/dist/types/api.js.map +1 -1
  27. package/dist/types/chat.d.ts +7 -7
  28. package/dist/types/chat.d.ts.map +1 -1
  29. package/dist/types/chat.js +0 -1
  30. package/dist/types/chat.js.map +1 -1
  31. package/dist/types/entity.d.ts +31 -31
  32. package/dist/types/entity.d.ts.map +1 -1
  33. package/dist/types/entity.js +1 -2
  34. package/dist/types/entity.js.map +1 -1
  35. package/dist/types/outline.d.ts +25 -25
  36. package/dist/types/outline.d.ts.map +1 -1
  37. package/dist/types/outline.js +2 -3
  38. package/dist/types/outline.js.map +1 -1
  39. package/dist/types/project.d.ts +18 -18
  40. package/dist/types/project.d.ts.map +1 -1
  41. package/dist/types/project.js +0 -1
  42. package/dist/types/project.js.map +1 -1
  43. package/dist/types/tool.d.ts +15 -15
  44. package/dist/types/tool.d.ts.map +1 -1
  45. package/dist/types/tool.js +35 -39
  46. package/dist/types/tool.js.map +1 -1
  47. package/dist/utils/backup.d.ts +14 -14
  48. package/dist/utils/backup.d.ts.map +1 -1
  49. package/dist/utils/backup.js +28 -31
  50. package/dist/utils/backup.js.map +1 -1
  51. package/dist/utils/format.d.ts +3 -3
  52. package/dist/utils/format.d.ts.map +1 -1
  53. package/dist/utils/format.js +3 -4
  54. package/dist/utils/format.js.map +1 -1
  55. package/dist/utils/id.d.ts +6 -6
  56. package/dist/utils/id.d.ts.map +1 -1
  57. package/dist/utils/id.js +7 -9
  58. package/dist/utils/id.js.map +1 -1
  59. package/dist/utils/index.js +2 -2
  60. package/dist/utils/index.js.map +1 -1
  61. package/dist/utils/mapping.d.ts +4 -4
  62. package/dist/utils/mapping.d.ts.map +1 -1
  63. package/dist/utils/mapping.js +20 -20
  64. package/dist/utils/mapping.js.map +1 -1
  65. package/dist/utils/reference-file.d.ts +4 -4
  66. package/dist/utils/reference-file.d.ts.map +1 -1
  67. package/dist/utils/reference-file.js +14 -16
  68. package/dist/utils/reference-file.js.map +1 -1
  69. package/package.json +1 -1
package/dist/types/api.js CHANGED
@@ -1,10 +1,8 @@
1
- // API 契约 Zod schema(@whispering233/ai-editor-shared/types/api.ts,单一事实来源)
2
- // 契约来源:doc/api/endpoints.md(全部端点 Req/Res 与错误码)、doc/api/tools.md(决策 15 agent 终止语义)、
3
- // doc/database/schema.md(entity data 字段)、doc/database/hooks.md(hook data 字段)
4
- // 命名约定(endpoints.md):请求体/查询参数 snake_case,响应体 camelCase;
5
- // 嵌套 data 对象内部字段原样透传(snake_case,如 expected_payoff)。
1
+ // API Zod schema(@whispering233/ai-editor-shared/types/api.ts,单一事实来源)
2
+ // 命名约定():请求体/查询参数 snake_case,响应体 camelCase;
3
+ // 嵌套 data 对象内部字段原样透传(snake_case,如 expected_payoff)。
6
4
  // **校验执行边界(2026-08 修订)**:schema 定义于此,但**校验仅在服务端执行**——
7
- // client 只消费推断出的类型与常量,不打包校验函数(避免 50KB 级依赖进浏览器包)。
5
+ // client 只消费推断出的类型与常量,不打包校验函数(避免 50KB 级依赖进浏览器包)。
8
6
  // zod 版本:^4(注意 v4 API:z.record 必须两参、z.enum 接受 readonly 数组)
9
7
  import { z } from "zod";
10
8
  import { ENTITY_TYPES, RELATION_TYPES } from "../constants/entity.js";
@@ -12,47 +10,47 @@ import { HOOK_STATUSES, PAYOFF_TIMING } from "../constants/hook.js";
12
10
  import { CONFLICT_LEVELS } from "../constants/outline.js";
13
11
  import { BACKUP_FREQUENCIES } from "../constants/backup.js";
14
12
  // ============ 基础 schema ============
15
- /** 实体类型(schema.md entities 表 CHECK 约束;与 ENTITY_TYPES 常量对齐) */
13
+ /** 实体类型( entities 表 CHECK 约束;与 ENTITY_TYPES 常量对齐) */
16
14
  export const entityTypeSchema = z.enum(ENTITY_TYPES);
17
- /** 项目语言(schema.md project.json 契约) */
15
+ /** 项目语言( project.json */
18
16
  export const projectLanguageSchema = z.enum(["zh", "en"]);
19
- // ============ ErrorCode(单一来源:REST / SSE / 工具共用,endpoints.md「错误码」) ============
17
+ // ============ ErrorCode(单一来源:REST / SSE / 工具共用,「错误码」) ============
20
18
  /**
21
19
  * 错误码全量枚举
22
- * 文档出处:endpoints.md 各端点错误响应;DELTA_CONFLICT 为 2026-08 修订废弃码
20
+ * 文档出处: 各端点错误响应;DELTA_CONFLICT 为 2026-08 修订废弃码
23
21
  * (computeState 改为 skipped/conflicts 字段呈现,不再返回 409——保留枚举兼容历史引用);
24
- * 末尾四个为 tools.md 决策 15/16 补充命名(文档未给具体码名,按语义命名,供 SSE error 事件使用)
22
+ * 末尾四个为 命名(文档未给具体码名,按语义命名,供 SSE error 事件使用)
25
23
  */
26
24
  export const ERROR_CODES = [
27
- // ---- endpoints.md 提取(现行)----
25
+ // ---- 提取(现行)----
28
26
  "VALIDATION_ERROR", // 400 参数校验失败(entity/delta/outline 创建等)
29
27
  "ENTITY_NOT_FOUND", // 404 实体不存在(详情/更新/删除/restore)
30
28
  "RELATION_EXISTS", // 409 关系已存在
31
- "EVENT_ALREADY_MOUNTED", // 409 事件已挂载时间点,occurs_at 1:n 重复挂载拒绝(G2,决策 26 修订)
29
+ "EVENT_ALREADY_MOUNTED", // 409 事件已挂载时间点,occurs_at 1:n 重复挂载拒绝(G2
32
30
  "RELATION_NOT_FOUND", // 404 关系不存在
33
31
  "OUTLINE_NODE_NOT_FOUND", // 404 大纲节点不存在(compute / path / restore / purge)
34
- "OUTLINE_ANCESTOR_DELETED", // 409 restore 时存在软删祖先(决策 12 修订)
35
- "INVALID_PROJECT_PATH", // 400 create/open 路径校验失败(决策 17)
36
- "PROPOSAL_STALE", // 409 确认时引用快照不一致(决策 14)
37
- "PROPOSAL_NOT_FOUND", // 404 proposal_id 不存在(决策 14)
38
- "PROPOSAL_PROJECT_MISMATCH", // 409 提案所属项目 ≠ 当前项目(决策 14 修订)
39
- "SCHEMA_VERSION_MISMATCH", // 409 导入 zip 的 data.db user_version 与当前程序版本不匹配(E2;拒绝导入,不静默重建,release-review §二)
40
- "PROJECT_VERSION_NEWER", // 409 open 时项目 data.db user_version 高于当前程序版本(E4;拒绝打开并提示升级程序,堵降级数据丢失,release-review §一)
41
- "BACKUP_TARGET_EXISTS", // 409 重命名备份目标文件名已存在(决策 29,B2.6:renameSync 目标存在会静默覆盖——显式拒绝防数据丢失)
42
- "REFERENCE_FILE_MISSING", // 409 参考资料 file 类文件缺失(决策 43:PUT 更新时读原文件失败——外部删除,提示先扫描同步)
32
+ "OUTLINE_ANCESTOR_DELETED", // 409 restore 时存在软删祖先
33
+ "INVALID_PROJECT_PATH", // 400 create/open 路径校验失败
34
+ "PROPOSAL_STALE", // 409 确认时引用快照不一致
35
+ "PROPOSAL_NOT_FOUND", // 404 proposal_id 不存在
36
+ "PROPOSAL_PROJECT_MISMATCH", // 409 提案所属项目 ≠ 当前项目
37
+ "SCHEMA_VERSION_MISMATCH", // 409 导入 zip 的 data.db user_version 与当前程序版本不匹配(拒绝导入,不静默重建)
38
+ "PROJECT_VERSION_NEWER", // 409 open 时项目 data.db user_version 高于当前程序版本(拒绝打开并提示升级程序,堵降级数据丢失)
39
+ "BACKUP_TARGET_EXISTS", // 409 重命名备份目标文件名已存在(B2.6:renameSync 目标存在会静默覆盖——显式拒绝防数据丢失)
40
+ "REFERENCE_FILE_MISSING", // 409 参考资料 file 类文件缺失(PUT 更新时读原文件失败——外部删除,提示先扫描同步)
43
41
  // ---- 废弃(保留兼容)----
44
42
  "DELTA_CONFLICT", // 已废弃(2026-08 修订:computeState 以 conflicts 字段替代 409)
45
- // ---- tools.md 决策 15/16 补充命名(SSE error 事件用)----
46
- "TOOL_RESULT_TOO_LARGE", // 工具结果 token 预算超限:截断/拒绝该工具结果(决策 15)
47
- "AGENT_DISPATCH_ERROR", // 工具调度器缺陷(S7.3 防御:结果条数不符 / id 错位 / 调度器抛错),终止循环(决策 15)
48
- "AGENT_INTERNAL_ERROR", // agent 循环内部未知异常(S7.3 防御路径——chatStream 契约不 throw,理论不可达)
49
- "AGENT_MAX_ITERATIONS", // agent 循环超 8 轮上限,发 error 事件终止(决策 15)
50
- "AGENT_TIMEOUT", // 单轮 120s 超时终止(决策 15)
51
- "AGENT_TOKEN_BUDGET", // 上下文 token 预算超限终止(决策 15)
43
+ // ---- 命名(SSE error 事件用)----
44
+ "TOOL_RESULT_TOO_LARGE", // 工具结果 token 预算超限:截断/拒绝该工具结果
45
+ "AGENT_DISPATCH_ERROR", // 工具调度器缺陷(S7.3 防御:结果条数不符 / id 错位 / 调度器抛错),终止循环
46
+ "AGENT_INTERNAL_ERROR", // agent 循环内部未知异常(S7.3 防御路径——chatStream throw,理论不可达)
47
+ "AGENT_MAX_ITERATIONS", // agent 循环超 8 轮上限,发 error 事件终止
48
+ "AGENT_TIMEOUT", // 单轮 120s 超时终止
49
+ "AGENT_TOKEN_BUDGET", // 上下文 token 预算超限终止
52
50
  ];
53
51
  /** ErrorCode 枚举 schema */
54
52
  export const errorCodeSchema = z.enum(ERROR_CODES);
55
- // ============ 通用响应包裹(endpoints.md「通用约定」) ============
53
+ // ============ 通用响应包裹(「通用约定」) ============
56
54
  /** 成功响应包裹:{ success: true, data: T } */
57
55
  export function apiSuccessSchema(dataSchema) {
58
56
  return z.object({
@@ -69,16 +67,16 @@ export const apiErrorSchema = z.object({
69
67
  fields: z.array(z.string()).optional(), // 校验失败时指出具体字段(VALIDATION_ERROR)
70
68
  }),
71
69
  });
72
- // ============ 实体 data 字段 schema(endpoints.md 创建接口 + schema.md + hooks.md) ============
70
+ // ============ 实体 data 字段 schema( 创建接口 + + ) ============
73
71
  /**
74
- * character 专属字段(schema.md:role/gender/age/personality[]/motivation/abilities[]/status/custom_fields)
75
- * 注意:data 嵌套对象内部字段原样透传(snake_case,如 custom_fields),顶层契约字段才是 camelCase
72
+ * character 专属字段(:role/gender/age/personality[]/motivation/abilities[]/status/custom_fields)
73
+ * 注意:data 嵌套对象内部字段原样透传(snake_case,如 custom_fields),顶层字段才是 camelCase
76
74
  */
77
75
  export const characterDataSchema = z
78
76
  .object({
79
77
  role: z.string().optional(),
80
78
  gender: z.string().optional(),
81
- age: z.union([z.string(), z.number()]).optional(), // 年龄文本或数字皆可(schema.md 未定死类型)
79
+ age: z.union([z.string(), z.number()]).optional(), // 年龄文本或数字皆可( 未定死类型)
82
80
  personality: z.array(z.string()).optional(),
83
81
  motivation: z.string().optional(),
84
82
  abilities: z.array(z.string()).optional(),
@@ -86,10 +84,10 @@ export const characterDataSchema = z
86
84
  custom_fields: z.record(z.string(), z.unknown()).optional(),
87
85
  })
88
86
  .passthrough(); // 允许未知字段(创作工具,用户自定义字段自由)
89
- /** setting 专属字段(schema.md:description/tags/rules/custom_fields;
90
- * `tags` = 分类标签(决策 31 K2,2026-08:分类统一字段,前后端同名);
91
- * `rules` = 规则条款(恢复原始语义,仅设定详情页编辑);
92
- * `parent_id`(决策 30 层级 belongs_to)与 `category`(决策 31 废弃)不参与新字段,旧残留 passthrough 容错) */
87
+ /** setting 专属字段(:description/tags/rules/custom_fields;
88
+ * `tags` = 分类标签( K2,2026-08:分类统一字段,前后端同名);
89
+ * `rules` = 规则条款(恢复原始语义,仅设定详情页编辑);
90
+ * `parent_id`( 层级 belongs_to)与 `category`( 废弃)不参与新字段,旧残留 passthrough 容错) */
93
91
  export const settingDataSchema = z
94
92
  .object({
95
93
  description: z.string().optional(),
@@ -98,7 +96,7 @@ export const settingDataSchema = z
98
96
  custom_fields: z.record(z.string(), z.unknown()).optional(),
99
97
  })
100
98
  .passthrough();
101
- /** location 专属字段(schema.md:type/parent_id/description/custom_fields) */
99
+ /** location 专属字段(:type/parent_id/description/custom_fields) */
102
100
  export const locationDataSchema = z
103
101
  .object({
104
102
  type: z.string().optional(),
@@ -107,42 +105,42 @@ export const locationDataSchema = z
107
105
  custom_fields: z.record(z.string(), z.unknown()).optional(),
108
106
  })
109
107
  .passthrough();
110
- /** hook 专属字段(hooks.md 第 30-56 行:status/category/expected_payoff/payoff_timing/half_life/is_core/notes/expected_resolve_node_id) */
108
+ /** hook 专属字段(:status/category/expected_payoff/payoff_timing/half_life/is_core/notes/expected_resolve_node_id) */
111
109
  export const hookDataSchema = z
112
110
  .object({
113
111
  status: z.enum(HOOK_STATUSES).optional(),
114
112
  category: z.string().optional(), // 自由填(HOOK_CATEGORIES 仅为前端建议值)
115
113
  expected_payoff: z.string().optional(),
116
114
  payoff_timing: z.enum(PAYOFF_TIMING).optional(),
117
- half_life: z.number().int().positive().optional(), // 章数;缺省映射见决策 21
115
+ half_life: z.number().int().positive().optional(), // 章数;缺省映射见
118
116
  is_core: z.boolean().optional(),
119
117
  notes: z.string().optional(),
120
- expected_resolve_node_id: z.string().nullable().optional(), // 决策 21 ready_to_resolve 依据
118
+ expected_resolve_node_id: z.string().nullable().optional(), // ready_to_resolve 依据
121
119
  })
122
120
  .passthrough();
123
- /** event 专属字段(决策 26 时间轴事件:description/tags[];字段名 snake_case) */
121
+ /** event 专属字段( 时间轴事件:description/tags[];字段名 snake_case) */
124
122
  export const eventDataSchema = z
125
123
  .object({
126
124
  description: z.string().optional(),
127
125
  tags: z.array(z.string()).optional(),
128
126
  })
129
127
  .passthrough(); // 允许未知字段(创作工具,用户自定义字段自由)
130
- /** timepoint 专属字段(G2 时间标签点,决策 26 修订):data 空——时间标签文本 = name,可重命名,YAGNI 不加 data 字段 */
128
+ /** timepoint 专属字段(G2 时间标签点):data 空——时间标签文本 = name,可重命名,YAGNI 不加 data 字段 */
131
129
  export const timepointDataSchema = z.object({}).passthrough();
132
- /** reference 专属字段(决策 36 参考资料:type 分类 / content 全文长文本 / source 来源 / tags 标签数组;
133
- * 决策 44 修订:type 为自由文本分类(不再预置枚举,缺省 material 写入侧兜底);
134
- * 决策 43 修订:两类承载——kind = file(本地 md 文档:file_name 相对路径 + content 正文镜像 + file_mtime
135
- * 上次同步快照)/ link(外源链接:url 必填 + content 可选备注);kind 缺省视为 link(存量条目运行时兼容);
136
- * source 仅存量旧条目使用(新建不再写入)) */
130
+ /** reference 专属字段( 参考资料:type 分类 / content 全文长文本 / source 来源 / tags 标签数组;
131
+ * type 为自由文本分类(不再预置枚举,缺省 material 写入侧兜底);
132
+ * 两类承载——kind = file(本地 md 文档:file_name 相对路径 + content 正文镜像 + file_mtime
133
+ * 上次同步快照)/ link(外源链接:url 必填 + content 可选备注);kind 缺省视为 link(存量条目运行时兼容);
134
+ * source 仅存量旧条目使用(新建不再写入)) */
137
135
  export const referenceDataSchema = z
138
136
  .object({
139
- type: z.string().optional(), // 自由文本分类(决策 44:取消预置枚举;缺省 material 写入侧兜底)
140
- kind: z.enum(["file", "link"]).optional(), // 决策 43:缺省视为 link
137
+ type: z.string().optional(), // 自由文本分类(取消预置枚举;缺省 material 写入侧兜底)
138
+ kind: z.enum(["file", "link"]).optional(), // 缺省视为 link
141
139
  file_name: z.string().optional(), // file 类:references/ 下相对路径(服务端写入,客户端只读)
142
140
  file_mtime: z.string().optional(), // file 类:上次同步时文件 mtime(scan 比对基准,服务端写入)
143
141
  url: z.string().optional(), // link 类:外源链接 URL(创建时必填校验在服务端 route 层)
144
142
  content: z.string().optional(),
145
- source: z.string().nullable().optional(), // 存量旧条目兼容(决策 43:新建不再写入)
143
+ source: z.string().nullable().optional(), // 存量旧条目兼容(新建不再写入)
146
144
  tags: z.array(z.string()).optional(),
147
145
  })
148
146
  .passthrough(); // 允许未知字段(创作工具,用户自定义字段自由)
@@ -158,31 +156,31 @@ export const ENTITY_DATA_SCHEMAS = {
158
156
  hook: hookDataSchema,
159
157
  event: eventDataSchema,
160
158
  timepoint: timepointDataSchema, // G2 时间标签点:data 空
161
- reference: referenceDataSchema, // 参考资料(决策 36)
159
+ reference: referenceDataSchema, // 参考资料
162
160
  };
163
- // ============ project 端点(endpoints.md「项目管理」) ============
161
+ // ============ project 端点(「项目管理」) ============
164
162
  /** ProjectConfig 响应(GET /api/v1/project/config;与 types/project.ts 的 ProjectConfig 对齐;
165
- * `prompt` 已废弃(决策 41)不再返回——项目规则唯一事实源改为项目目录 AGENTS.md) */
163
+ * `prompt` 已废弃不再返回——项目规则唯一事实源改为项目目录 ) */
166
164
  export const projectConfigSchema = z.object({
167
165
  id: z.string(),
168
166
  name: z.string(),
169
167
  language: projectLanguageSchema,
170
- schemaVersion: z.number().int(), // 决策 13
171
- currentPosition: z.string().nullable(), // 「当前位置」节点 id;null = 未设置(决策 21)
172
- backupFrequencyMinutes: z.number().int().nullable(), // 自动备份频率(决策 27);null = 关闭;缺省 10 由读侧兜底
168
+ schemaVersion: z.number().int(), //
169
+ currentPosition: z.string().nullable(), // 「当前位置」节点 id;null = 未设置
170
+ backupFrequencyMinutes: z.number().int().nullable(), // 自动备份频率;null = 关闭;缺省 10 由读侧兜底
173
171
  createdAt: z.string(),
174
172
  updatedAt: z.string(),
175
173
  });
176
174
  // POST /api/v1/project/create
177
175
  export const projectCreateReqSchema = z
178
176
  .object({
179
- path: z.string(), // 项目目录绝对路径(决策 17 校验)
177
+ path: z.string(), // 项目目录绝对路径( 校验)
180
178
  config: z
181
179
  .object({
182
180
  name: z.string().optional(),
183
181
  language: projectLanguageSchema.optional(),
184
- // prompt 已废弃(决策 41):不再接受(strict schema 传入 → 400 VALIDATION_ERROR);
185
- // 项目规则改由 PUT /api/v1/project/agents 写入 AGENTS.md
182
+ // prompt 已废弃:不再接受(strict schema 传入 → 400 VALIDATION_ERROR);
183
+ // 项目规则改由 PUT /api/v1/project/agents 写入
186
184
  })
187
185
  .strict()
188
186
  .optional(),
@@ -230,51 +228,51 @@ export const projectConfigUpdateReqSchema = z
230
228
  .object({
231
229
  name: z.string().optional(),
232
230
  language: projectLanguageSchema.optional(),
233
- // prompt 已废弃(决策 41):不再接受(strict schema 传入 → 400 VALIDATION_ERROR);
234
- // 项目规则改由 PUT /api/v1/project/agents 写入 AGENTS.md
231
+ // prompt 已废弃:不再接受(strict schema 传入 → 400 VALIDATION_ERROR);
232
+ // 项目规则改由 PUT /api/v1/project/agents 写入
235
233
  current_position: z.string().nullable().optional(), // 须指向存在的非软删大纲节点(服务端校验)
236
234
  /**
237
- * 自动备份频率(决策 27 + 批次十四修订):仅接受枚举 1/5/10/15/30/60(BACKUP_FREQUENCIES),其他(含 0)→ 400
238
- * VALIDATION_ERROR;null = 关闭(写入 null)——0 仅读侧兼容旧数据语义,写侧一律用 null 表示关闭
239
- */
235
+ * 自动备份频率( + 批次十四修订):仅接受枚举 1/5/10/15/30/60(BACKUP_FREQUENCIES),其他(含 0)→ 400
236
+ * VALIDATION_ERROR;null = 关闭(写入 null)——0 仅读侧兼容旧数据语义,写侧一律用 null 表示关闭
237
+ */
240
238
  backup_frequency_minutes: z.union(BACKUP_FREQUENCIES.map((v) => z.literal(v))).nullable().optional(),
241
239
  })
242
240
  .strict();
243
241
  export const projectConfigUpdateResSchema = z.object({
244
242
  updated: z.literal(true),
245
243
  });
246
- // GET /api/v1/project/agents(决策 41:项目规则文件 AGENTS.md——唯一事实源,取代 project.json `prompt`)
244
+ // GET /api/v1/project/agents(项目规则文件 ——唯一事实源,取代 project.json `prompt`)
247
245
  // 语义:无当前项目 → 409 NO_PROJECT_OPEN;文件不存在不报错(exists:false + 空串);
248
- // updatedAt = 文件 mtime(ISO 8601,外部修改检测依据);读取每次实时读文件不缓存
246
+ // updatedAt = 文件 mtime(ISO 8601,外部修改检测依据);读取每次实时读文件不缓存
249
247
  export const projectAgentsGetResSchema = z.object({
250
- content: z.string(), // AGENTS.md 文件内容(文件不存在 → 空串)
248
+ content: z.string(), // 文件内容(文件不存在 → 空串)
251
249
  exists: z.boolean(), // 文件是否存在(false 时 content 为空串)
252
250
  updatedAt: z.string().nullable(), // 文件 mtime(ISO 8601;文件不存在 → null)
253
251
  });
254
- // PUT /api/v1/project/agents(决策 41:设置页直接编辑 AGENTS.md 文件内容)
252
+ // PUT /api/v1/project/agents(设置页直接编辑 文件内容)
255
253
  // 语义:整体替换(非追加);空串 = 清空规则(保留空文件不删除);文件不存在自动创建;
256
- // 写入走原子写(决策 11 同款);写入后返回新 mtime(前端更新本地比对基线)
254
+ // 写入走原子写( 同款);写入后返回新 mtime(前端更新本地比对基线)
257
255
  export const projectAgentsPutReqSchema = z
258
256
  .object({
259
- content: z.string(), // AGENTS.md 完整内容(整体替换;空串 = 清空规则文件,保留空文件不删除)
257
+ content: z.string(), // 完整内容(整体替换;空串 = 清空规则文件,保留空文件不删除)
260
258
  })
261
259
  .strict();
262
260
  export const projectAgentsPutResSchema = z.object({
263
261
  saved: z.literal(true),
264
262
  updatedAt: z.string(), // 写入后的文件 mtime(ISO 8601)——前端更新本地比对基线
265
263
  });
266
- // POST /api/v1/project/backup(决策 28 新增:手动备份可携带自定义名称)
264
+ // POST /api/v1/project/backup 新增:手动备份可携带自定义名称)
267
265
  // - 请求体可选 `name`(string);空串/缺省 → 无自定义名称(纯时间戳文件名)。
268
266
  // - **形状校验仅限类型**(oracle 审核 P2-1:zod 与 sanitize 的「.zip 剥离 + 长度」判定
269
- // 顺序曾在 schema 内重复实现导致误拒——如 29 字符 + ".zip" schema 判超长而 sanitize 判合法);
270
- // **名称规则(trim/.zip 剥离/长度/字符集)权威判定全部收敛在 shared sanitizeBackupName**——
271
- // writeBackup 为唯一执行点,非法 → 400 VALIDATION_ERROR(决策 28)。
267
+ // 顺序曾在 schema 内重复实现导致误拒——如 29 字符 + ".zip" schema 判超长而 sanitize 判合法);
268
+ // **名称规则(trim/.zip 剥离/长度/字符集)权威判定全部收敛在 shared sanitizeBackupName**——
269
+ // writeBackup 为唯一执行点,非法 → 400 VALIDATION_ERROR
272
270
  export const projectBackupReqSchema = z
273
271
  .object({
274
272
  name: z.string().optional(),
275
273
  })
276
274
  .strict();
277
- // POST /api/v1/project/backup/rename(决策 29):重命名备份(只改名称段,时间戳与 kind 保持)
275
+ // POST /api/v1/project/backup/rename:重命名备份(只改名称段,时间戳与 kind 保持)
278
276
  // - 请求体 fileName 必填;name 可选——非空 → sanitize(非法 400);空串/缺省 → 清除名称段
279
277
  export const projectBackupRenameReqSchema = z
280
278
  .object({
@@ -282,46 +280,46 @@ export const projectBackupRenameReqSchema = z
282
280
  name: z.string().optional(),
283
281
  })
284
282
  .strict();
285
- // ============ 导出/导入端点(E1/E2:release-review §二,产品承诺「数据主权归用户」) ============
283
+ // ============ 导出/导入端点(产品承诺「数据主权归用户」) ============
286
284
  /**
287
- * 导出 zip 内固定三文件名(E1:GET /api/v1/project/export 的 zip 条目名与数据文件
285
+ * 导出 zip 内固定三文件名(GET /api/v1/project/export 的 zip 条目名与数据文件
288
286
  * 原名一致——import 侧按此固定名校验,缺失即坏包)
289
287
  */
290
288
  export const PROJECT_EXPORT_FILE_NAMES = ["project.json", "outline.json", "data.db"];
291
289
  /**
292
- * GET /api/v1/project/export(E1 实现,E2 依赖):
293
- * - **响应为二进制 zip(application/zip),非 JSON 包裹**——endpoints.md「成功响应
294
- * {success,data}」通用约定的显式例外;Content-Disposition: attachment;
295
- * filename*=UTF-8''<书名>.zip(RFC 5987)
290
+ * GET /api/v1/project/export
291
+ * - **响应为二进制 zip(application/zip),非 JSON 包裹**——「成功响应
292
+ * {success,data}」通用约定的显式例外;Content-Disposition: attachment;
293
+ * filename*=UTF-8''<书名>.zip(RFC 5987)
296
294
  * - zip 内三文件:project.json + outline.json + data.db(导出前 wal_checkpoint(TRUNCATE)
297
- * 保证 data.db 主文件完整快照;决策 17 key 存用户级配置,天然不入包)
295
+ * 保证 data.db 主文件完整快照; key 存用户级配置,天然不入包)
298
296
  * - 错误:无当前项目 → 409 NO_PROJECT_OPEN(服务端补充码,与 /config 一致);
299
- * 三文件缺失任一 → 500 INTERNAL_ERROR(打开的项目三文件必然齐全,缺失即损坏)
300
- * - 二进制响应不走 Zod parse——契约以本注释 + PROJECT_EXPORT_FILE_NAMES 常量表达
297
+ * 三文件缺失任一 → 500 INTERNAL_ERROR(打开的项目三文件必然齐全,缺失即损坏)
298
+ * - 二进制响应不走 Zod parse——以本注释 + PROJECT_EXPORT_FILE_NAMES 常量表达
301
299
  */
302
- // POST /api/v1/project/import(E2 实现;E1 已落契约)
300
+ // POST /api/v1/project/import( 已落)
303
301
  // - 请求:multipart/form-data 文件上传——field "file"(zip 备份包)+ field "name"(书名,
304
- // 必填;禁路径分隔符/纯点/控制字符,与 client 新建项目同规则)——目标目录为
305
- // 服务端决定的 创作根/books/<name>/(客户端不可指定路径,防越权)
306
- // - 服务端流程(E2):解压到临时目录 → 校验(条目白名单 = PROJECT_EXPORT_FILE_NAMES
307
- // 三文件名 + project.json/outline.json 顶层契约 + data.db user_version 匹配)→
308
- // 原子搬入新书目录(新建,不覆盖现有项目)→ 返回 200
309
- // - 错误码:坏包/缺文件/未知条目/契约不符 → 400 VALIDATION_ERROR;data.db user_version
310
- // 与当前程序版本不匹配 → 409 SCHEMA_VERSION_MISMATCH(拒绝导入,不静默重建);
311
- // 目标书名已存在 → 409 PROJECT_ALREADY_EXISTS(服务端补充码,与 create 同语义)
302
+ // 必填;禁路径分隔符/纯点/控制字符,与 client 新建项目同规则)——目标目录为
303
+ // 服务端决定的 创作根/books/<name>/(客户端不可指定路径,防越权)
304
+ // - 服务端流程:解压到临时目录 → 校验(条目白名单 = PROJECT_EXPORT_FILE_NAMES
305
+ // 三文件名 + project.json/outline.json 顶层data.db user_version 匹配)→
306
+ // 原子搬入新书目录(新建,不覆盖现有项目)→ 返回 200
307
+ // - 错误码:坏包/缺文件/未知条目/不符 → 400 VALIDATION_ERROR;data.db user_version
308
+ // 与当前程序版本不匹配 → 409 SCHEMA_VERSION_MISMATCH(拒绝导入,不静默重建);
309
+ // 目标书名已存在 → 409 PROJECT_ALREADY_EXISTS(服务端补充码,与 create 同语义)
312
310
  export const projectImportResSchema = z.object({
313
311
  imported: z.literal(true),
314
312
  id: z.string(), // 项目 project_id(覆盖恢复 = 书架目标项目原 id;导入新书 = 沿用 zip 内 project.json 的 id)
315
313
  path: z.string(), // 书目录绝对路径(创作根/books/<name>/ 或去重名 books/<name> (N)/)
316
- name: z.string(), // 书名(新书目录名;project.json 内部 name 同此——「目录名 = 书名」不变式,决策 27)
317
- /** 决策 27 分流:restored = zip 内 id 匹配书架 → 覆盖恢复;new = 导入为新书(前端按此提示 toast) */
314
+ name: z.string(), // 书名(新书目录名;project.json 内部 name 同此——「目录名 = 书名」不变式)
315
+ /** 分流:restored = zip 内 id 匹配书架 → 覆盖恢复;new = 导入为新书(前端按此提示 toast) */
318
316
  mode: z.enum(["restored", "new"]),
319
317
  });
320
- // ============ entity 端点(endpoints.md「实体 CRUD」) ============
318
+ // ============ entity 端点(「实体 CRUD」) ============
321
319
  /**
322
320
  * 关系(GET /api/v1/relation depth=1 项;与 types/entity.ts RelationRecord 对齐)
323
321
  * 定义于此处供 entity 详情响应(relations: RelationSummary[],形状同 RelationRecord,
324
- * endpoints.md L187 未单独列字段)与 relation 查询共用,避免同结构两处定义漂移
322
+ * 未单独列字段)与 relation 查询共用,避免同结构两处定义漂移
325
323
  */
326
324
  export const relationRecordSchema = z.object({
327
325
  id: z.string(),
@@ -341,10 +339,10 @@ export const entitySummarySchema = z.object({
341
339
  type: entityTypeSchema,
342
340
  name: z.string(),
343
341
  summary: z.record(z.string(), z.unknown()), // 从 data 提取的关键摘要字段
344
- // M2(2026-08 批次六):仅 setting 列表填充(决策 30 层级 = belongs_to)
342
+ // M2(2026-08 批次六):仅 setting 列表填充( 层级 = belongs_to)
345
343
  parentId: z.string().optional(),
346
344
  parentName: z.string().optional(),
347
- // 手动排序位(决策 46,2026-08 批次十三):仅 setting 类型填充(entities.sort_order 列)
345
+ // 手动排序位(2026-08 批次十三):仅 setting 类型填充(entities.sort_order 列)
348
346
  sortOrder: z.number().int().optional(),
349
347
  createdAt: z.string(),
350
348
  updatedAt: z.string(),
@@ -356,10 +354,10 @@ export const entityListQuerySchema = z.object({
356
354
  limit: z.coerce.number().int().min(1).max(200).default(50),
357
355
  sort: z.enum(["name", "created_at", "updated_at"]).optional(),
358
356
  order: z.enum(["asc", "desc"]).optional(),
359
- // 标签包含筛选(决策 31,2026-08):data 数组字段(setting.rules / event.tags)包含该标签即命中
357
+ // 标签包含筛选(2026-08):data 数组字段(setting.rules / event.tags)包含该标签即命中
360
358
  tag: z.string().optional(),
361
- // 上级设定筛选(决策 32,2026-08,仅 setting 类型生效,其他类型路由层忽略):匹配 = 实体在设定层级树
362
- // (belongs_to,决策 30)中直接或间接属于该上级(递归子树,不含上级自身);复用 listSettingHierarchyEdges
359
+ // 上级设定筛选(2026-08,仅 setting 类型生效,其他类型路由层忽略):匹配 = 实体在设定层级树
360
+ // (belongs_to)中直接或间接属于该上级(递归子树,不含上级自身);复用 listSettingHierarchyEdges
363
361
  // 建邻接表 DFS 收集后代集合走 db JS 过滤路径(total = 过滤后总数);与 q/tag/排序/分页组合(AND);
364
362
  // 指向不存在的设定(含已软删)→ 空结果(宽松,同 tag 无匹配不 404);不传 = 不过滤
365
363
  parent_id: z.string().optional(),
@@ -376,7 +374,7 @@ export const entityDetailResSchema = z.object({
376
374
  type: entityTypeSchema,
377
375
  name: z.string(),
378
376
  data: z.record(z.string(), z.unknown()), // 完整字段(嵌套 snake_case 原样透传)
379
- relations: z.array(relationRecordSchema), // RelationSummary(形状同 RelationRecord,endpoints.md L187)
377
+ relations: z.array(relationRecordSchema), // RelationSummary(形状同 RelationRecord,)
380
378
  deltaCount: z.number().int(),
381
379
  createdAt: z.string(),
382
380
  updatedAt: z.string(),
@@ -406,7 +404,7 @@ export const entityUpdateResSchema = z.object({
406
404
  id: z.string(),
407
405
  updated: z.literal(true),
408
406
  });
409
- // DELETE /api/v1/entity/:type/:id(软删 + 级联计数,决策 12)
407
+ // DELETE /api/v1/entity/:type/:id(软删 + 级联计数)
410
408
  export const entityDeleteResSchema = z.object({
411
409
  deleted: z.literal(true),
412
410
  cascaded: z.object({
@@ -414,9 +412,9 @@ export const entityDeleteResSchema = z.object({
414
412
  deltas: z.number().int(),
415
413
  }),
416
414
  });
417
- // ============ relation 端点(endpoints.md「关系管理」) ============
415
+ // ============ relation 端点(「关系管理」) ============
418
416
  // relationRecordSchema 定义于 entity 区(entity 详情 relations 与 relation 查询共用,避免重复定义)
419
- /** 路径结构(depth>=2,endpoints.md) */
417
+ /** 路径结构(depth>=2,) */
420
418
  export const relationPathSchema = z.object({
421
419
  nodes: z.array(z.object({ type: z.string(), id: z.string(), name: z.string() })),
422
420
  edges: z.array(z.object({ from: z.string(), to: z.string(), relationType: z.string() })),
@@ -434,7 +432,7 @@ export const relationQueryResSchema = z.object({
434
432
  relations: z.array(relationRecordSchema),
435
433
  paths: z.array(relationPathSchema).optional(), // depth>=2 时返回
436
434
  });
437
- // POST /api/v1/relation(relation_type 限定 schema.md 预定义 16 种)
435
+ // POST /api/v1/relation(relation_type 限定 预定义 16 种)
438
436
  export const relationCreateReqSchema = z
439
437
  .object({
440
438
  source_type: z.string(),
@@ -455,18 +453,18 @@ export const relationCreateResSchema = z.object({
455
453
  relationType: z.string(),
456
454
  }),
457
455
  });
458
- // PUT /api/v1/relation/:id(endpoints.md「PUT /relation/:id」:metadata **整体替换**,含清空传 {})
456
+ // PUT /api/v1/relation/:id(「PUT /relation/:id」:metadata **整体替换**,含清空传 {})
459
457
  export const relationUpdateMetaReqSchema = z
460
458
  .object({
461
459
  metadata: z.record(z.string(), z.unknown()),
462
460
  })
463
461
  .strict();
464
- // DELETE /api/v1/relation/:id(物理删除,不进回收站,决策 12 修订)
462
+ // DELETE /api/v1/relation/:id(物理删除,不进回收站)
465
463
  export const relationDeleteResSchema = z.object({
466
464
  deleted: z.literal(true),
467
465
  });
468
- // ============ delta 端点(endpoints.md「Delta 变更追踪」) ============
469
- /** 变更操作类型(2026-08 修订语义:set/update/add/remove,见 endpoints.md) */
466
+ // ============ delta 端点(「Delta 变更追踪」) ============
467
+ /** 变更操作类型(2026-08 修订语义:set/update/add/remove,见 ) */
470
468
  export const deltaOpSchema = z.enum(["set", "update", "add", "remove"]);
471
469
  /** 单条属性变更 */
472
470
  export const deltaChangeSchema = z.object({
@@ -507,7 +505,7 @@ export const deltaByNodeResSchema = z.object({
507
505
  nodeId: z.string(),
508
506
  deltas: z.array(deltaRecordSchema),
509
507
  });
510
- // POST /api/v1/delta/compute(决策 9/19:只沿大纲树父链累积)
508
+ // POST /api/v1/delta/compute(只沿大纲树父链累积)
511
509
  export const deltaComputeReqSchema = z
512
510
  .object({
513
511
  target_type: z.string(),
@@ -531,7 +529,7 @@ export const deltaComputeResSchema = z.object({
531
529
  expected: z.unknown(),
532
530
  actual: z.unknown(),
533
531
  }))
534
- .optional(), // 决策 9 修订:op=update 且当前值 ≠ from 时跳过该 change
532
+ .optional(), // op=update 且当前值 ≠ from 时跳过该 change
535
533
  })),
536
534
  conflicts: z.array(z.object({
537
535
  deltaId: z.string(),
@@ -540,14 +538,14 @@ export const deltaComputeResSchema = z.object({
540
538
  actual: z.unknown(),
541
539
  })),
542
540
  });
543
- // ============ outline 端点(endpoints.md「大纲操作」,严格三层决策 19) ============
541
+ // ============ outline 端点(「大纲操作」,严格三层) ============
544
542
  /**
545
- * 大纲节点 data 字段 schema(决策 23,麦基《故事》字段集,schema.md outline.json「节点结构化信息」节):
543
+ * 大纲节点 data 字段 schema(麦基《故事》字段集, outline.json「节点结构化信息」节):
546
544
  * scene——goal/conflict_levels/value_from/value_to;chapter——reversal/climax_scene;
547
545
  * volume——climax_scene/inciting_scene。
548
546
  * 宽松语义与 ENTITY_DATA_SCHEMAS 一致:
549
- * - `.passthrough()` 允许未知字段(创作工具,用户自定义字段自由,未知字段原样保留透传)
550
- * - 引用字段(climax_scene/inciting_scene)仅类型校验(字符串),不校验存在性/范围(决策 23:MVP 宽松)
547
+ * - `.passthrough` 允许未知字段(创作工具,用户自定义字段自由,未知字段原样保留透传)
548
+ * - 引用字段(climax_scene/inciting_scene)仅类型校验(字符串),不校验存在性/范围(MVP 宽松)
551
549
  * 请求体 data 本体使用宽松 record(outlineCreateReqSchema),精确校验在服务端 route 层按层级选用
552
550
  */
553
551
  export const sceneDataSchema = z
@@ -588,9 +586,9 @@ export const outlineNodeSchema = z.lazy(() => z.object({
588
586
  type: z.enum(["volume", "chapter", "scene"]),
589
587
  title: z.string(),
590
588
  summary: z.string().optional(),
591
- data: z.record(z.string(), z.unknown()).optional(), // 节点结构化信息(决策 23;内部字段原样透传)
592
- updatedAt: z.string(), // 节点版本戳(决策 19)
593
- deleted: z.boolean().optional(), // 软删标记(决策 12,管理视图)
589
+ data: z.record(z.string(), z.unknown()).optional(), // 节点结构化信息(内部字段原样透传)
590
+ updatedAt: z.string(), // 节点版本戳
591
+ deleted: z.boolean().optional(), // 软删标记(管理视图)
594
592
  deletedAt: z.string().optional(),
595
593
  children: z.array(outlineNodeSchema).optional(),
596
594
  metadata: z
@@ -605,26 +603,26 @@ export const outlineNodeSchema = z.lazy(() => z.object({
605
603
  export const outlineTreeSchema = z.object({
606
604
  id: z.literal("root"),
607
605
  type: z.literal("root"),
608
- schemaVersion: z.number().int(), // outline.json 顶层 schema_version(决策 13)
606
+ schemaVersion: z.number().int(), // outline.json 顶层 schema_version
609
607
  children: z.array(outlineNodeSchema),
610
608
  });
611
609
  // GET /api/v1/outline(Query)
612
610
  export const outlineGetQuerySchema = z.object({
613
- // 显式字符串布尔:z.coerce.boolean() 会把 "false" 解析为 true(反向问题),
611
+ // 显式字符串布尔:z.coerce.boolean 会把 "false" 解析为 true(反向问题),
614
612
  // 改为枚举 + transform:显式传 false → false;不传 → undefined(默认关闭 metadata 统计)
615
613
  with_metadata: z
616
614
  .enum(["true", "false"])
617
615
  .transform((v) => v === "true")
618
616
  .optional(), // 跨 outline.json × data.db 联查统计
619
617
  });
620
- // POST /api/v1/outline(parent_id 必填,无默认值,决策 19)
618
+ // POST /api/v1/outline(parent_id 必填,无默认值)
621
619
  export const outlineCreateReqSchema = z
622
620
  .object({
623
621
  type: z.enum(["volume", "chapter", "scene"]),
624
622
  title: z.string().min(1).max(200),
625
623
  parent_id: z.string(), // volume→root;chapter→volume 或 root;scene→必须 chapter
626
624
  summary: z.string().optional(),
627
- data: z.record(z.string(), z.unknown()).optional(), // 节点结构化信息(决策 23,宽松 record,按层级 schema 精校验)
625
+ data: z.record(z.string(), z.unknown()).optional(), // 节点结构化信息(宽松 record,按层级 schema 精校验)
628
626
  })
629
627
  .strict();
630
628
  export const outlineCreateResSchema = z.object({
@@ -639,7 +637,7 @@ export const outlineUpdateReqSchema = z
639
637
  .object({
640
638
  title: z.string().min(1).max(200).optional(),
641
639
  summary: z.string().optional(),
642
- data: z.record(z.string(), z.unknown()).optional(), // 部分合并(决策 23;按层级 schema 精校验)
640
+ data: z.record(z.string(), z.unknown()).optional(), // 部分合并(按层级 schema 精校验)
643
641
  })
644
642
  .strict();
645
643
  export const outlineUpdateResSchema = z.object({
@@ -657,10 +655,10 @@ export const outlineMoveResSchema = z.object({
657
655
  previousParentId: z.string(),
658
656
  newParentId: z.string(),
659
657
  });
660
- // PUT /api/v1/entity/event/:id/move(时间轴事件重排,决策 26;命名风格同 outlineMoveReqSchema)
658
+ // PUT /api/v1/entity/event/:id/move(时间轴事件重排,;命名风格同 outlineMoveReqSchema)
661
659
  export const entityMoveReqSchema = z
662
660
  .object({
663
- // 0-based 全局事件线性序(endpoints.md):超过当前事件总数 → clamp 到末尾(不返回 4xx);
661
+ // 0-based 全局事件线性序():超过当前事件总数 → clamp 到末尾(不返回 4xx);
664
662
  // 负数由本 schema 拒绝(400 VALIDATION_ERROR)——db 层 moveEvent 对负数 clamp 至 0
665
663
  // 仅为内部防御语义(HTTP 路径不可达)
666
664
  order: z.number().int().min(0),
@@ -669,8 +667,8 @@ export const entityMoveReqSchema = z
669
667
  export const entityMoveResSchema = z.object({
670
668
  moved: z.literal(true),
671
669
  });
672
- // PUT /api/v1/entity/setting/:id/move(设定同级重排 / 改父 + 重排,决策 46,2026-08 批次十三;
673
- // 修订决策 42「设定无 sort_order 语义」约束——复用 entities.sort_order 列,无 DDL 迁移)
670
+ // PUT /api/v1/entity/setting/:id/move(设定同级重排 / 改父 + 重排,,2026-08 批次十三;
671
+ // 修订「设定无 sort_order 语义」约束——复用 entities.sort_order 列,无 DDL 迁移)
674
672
  export const settingMoveReqSchema = z
675
673
  .object({
676
674
  // 目标父设定 id;null = 移为顶层根(无上级)。与当前父相同(含同为根)→ 仅重排
@@ -680,8 +678,8 @@ export const settingMoveReqSchema = z
680
678
  order: z.number().int().min(0).optional(),
681
679
  })
682
680
  .strict();
683
- // POST /api/v1/entity/event/:id/move_to(跨组挂载复合写,G2 决策 26 修订:事件拖到另一时间点
684
- // 区块 = 改挂载 + 重排一次提交;服务端事务内原子完成——endpoints.md「G2 跨组拖拽」的复合端点实现)
681
+ // POST /api/v1/entity/event/:id/move_to(跨组挂载复合写,G2 事件拖到另一时间点
682
+ // 区块 = 改挂载 + 重排一次提交;服务端事务内原子完成——「G2 跨组拖拽」的复合端点实现)
685
683
  export const eventMoveToReqSchema = z
686
684
  .object({
687
685
  // 目标时间点 id;null = 移出挂载区(仅重排,归入时间轴「未挂载」兜底区)。
@@ -691,7 +689,7 @@ export const eventMoveToReqSchema = z
691
689
  order: z.number().int().min(0),
692
690
  })
693
691
  .strict();
694
- // DELETE /api/v1/outline/:nodeId(软删 + 递归级联,决策 12)
692
+ // DELETE /api/v1/outline/:nodeId(软删 + 递归级联)
695
693
  export const outlineDeleteResSchema = z.object({
696
694
  deleted: z.literal(true),
697
695
  cascaded: z.object({
@@ -705,13 +703,13 @@ export const outlinePathResSchema = z.object({
705
703
  nodeId: z.string(),
706
704
  path: z.array(z.string()), // 如 ["root", "vol-1", "ch-3", "sc-15"]
707
705
  });
708
- // ============ trash 端点(endpoints.md「回收站」,决策 12) ============
706
+ // ============ trash 端点(「回收站」) ============
709
707
  // GET /api/v1/trash(deletedAt 为 camelCase——响应体约定)
710
708
  export const trashListResSchema = z.object({
711
709
  entities: z.array(z.object({ id: z.string(), type: z.string(), name: z.string(), deletedAt: z.string() })),
712
710
  nodes: z.array(z.object({ id: z.string(), type: z.string(), title: z.string(), deletedAt: z.string() })),
713
711
  });
714
- // POST /api/v1/trash/entity/:type/:id/restore(级联还原,决策 12 修订)
712
+ // POST /api/v1/trash/entity/:type/:id/restore(级联还原)
715
713
  export const trashRestoreEntityResSchema = z.object({
716
714
  restored: z.literal(true),
717
715
  restoredRelations: z.number().int(),
@@ -728,7 +726,7 @@ export const trashRestoreNodeResSchema = z.object({
728
726
  export const trashPurgeResSchema = z.object({
729
727
  purged: z.literal(true),
730
728
  });
731
- // ============ chat 端点(endpoints.md「AI 对话」,决策 18 持久化) ============
729
+ // ============ chat 端点(「AI 对话」, 持久化) ============
732
730
  // POST /api/v1/chat(POST + SSE;消息落 chat_messages 表)
733
731
  export const chatSendReqSchema = z
734
732
  .object({
@@ -762,11 +760,11 @@ export const chatMessagesResSchema = z.object({
762
760
  role: z.enum(["user", "assistant", "tool"]),
763
761
  content: z.string().nullable().optional(),
764
762
  toolCalls: z.array(z.unknown()).optional(), // assistant 消息的工具调用数组
765
- toolCallId: z.string().nullable().optional(), // tool 消息关联的调用 id(决策 18 修订)
763
+ toolCallId: z.string().nullable().optional(), // tool 消息关联的调用 id
766
764
  createdAt: z.string(),
767
765
  })),
768
766
  });
769
- // ============ proposal 端点(endpoints.md「提案确认」,决策 14) ============
767
+ // ============ proposal 端点(「提案确认」) ============
770
768
  // POST /api/v1/proposal/:proposalId/confirm(409 PROPOSAL_STALE / 404 PROPOSAL_NOT_FOUND / 409 PROPOSAL_PROJECT_MISMATCH)
771
769
  export const proposalConfirmResSchema = z.object({
772
770
  confirmed: z.literal(true),
@@ -776,8 +774,8 @@ export const proposalConfirmResSchema = z.object({
776
774
  export const proposalRejectResSchema = z.object({
777
775
  rejected: z.literal(true),
778
776
  });
779
- // ============ settings 端点(endpoints.md「系统设置」,决策 17) ============
780
- /** 思考强度(决策 34:参考 pi 的 ThinkingLevel 档位——off / minimal / low / medium / high / xhigh / max;off = 不加 reasoning 参数) */
777
+ // ============ settings 端点(「系统设置」) ============
778
+ /** 思考强度(参考 pi 的 ThinkingLevel 档位——off / minimal / low / medium / high / xhigh / max;off = 不加 reasoning 参数) */
781
779
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
782
780
  /** 模型目录条目(GET /settings/llm 返回,供前端模型下拉与上下文占用分母) */
783
781
  export const modelInfoSchema = z.object({
@@ -788,42 +786,55 @@ export const modelInfoSchema = z.object({
788
786
  maxTokens: z.number(),
789
787
  reasoning: z.boolean(),
790
788
  });
791
- // GET /api/v1/settings/llm(key 不回传明文;models 为可用模型目录,当前 model 必在 list 内)
792
- export const settingsLlmGetResSchema = z.object({
793
- model: z.string(), // 默认 "deepseek-v4-flash"
794
- thinkingLevel: z.enum(THINKING_LEVELS), // 思考强度(决策 34;缺省 high
795
- apiKeySet: z.boolean(),
789
+ /** 单 provider 条目(批次十六:GET /settings/llm providers[]——目录 + 该家有效 key 状态) */
790
+ export const settingsProviderSchema = z.object({
791
+ id: z.string(), // provider 目录 id(deepseek / opencode-go)
792
+ displayName: z.string(), // 分组标题(如 "OpenCode Go"
793
+ apiKeySet: z.boolean(), // 该家解析链(env > config api_keys > pi-agent auth.json)是否有有效 key
796
794
  apiKeyMasked: z.string().optional(), // 掩码展示(utils/format.ts maskApiKey)
797
- models: z.array(modelInfoSchema), // 模型目录(决策 34 getAvailableModels)
795
+ models: z.array(modelInfoSchema), // 该家模型目录( getAvailableModels(provider)
798
796
  });
799
- // PUT /api/v1/settings/llm(写入 ~/.ai-editor/config.json,绝不入项目文件,决策 17)
797
+ // GET /api/v1/settings/llm(批次十六:多 provider——provider/model 激活一对 + 各家目录/key 状态;key 不回传明文)
798
+ export const settingsLlmGetResSchema = z.object({
799
+ provider: z.string(), // 激活 provider id(缺省 "deepseek")
800
+ model: z.string(), // 当前模型名(属于 provider 目录,缺省 "deepseek-v4-flash")
801
+ thinkingLevel: z.enum(THINKING_LEVELS), // 思考强度(全局,不分 provider)
802
+ providers: z.array(settingsProviderSchema), // 全量注册 provider(前端下拉分组/禁用依据)
803
+ });
804
+ // PUT /api/v1/settings/llm(写入 ~/.ai-editor/config.json,绝不入项目文件;批次十六 v2)
805
+ // 语义:provider + model 成对(跨 provider 激活);api_keys 写谁谁变、空串 = 清除该家;
806
+ // server 校验 model ∈ provider 目录(不符 → VALIDATION_ERROR);api_key 旧字段不再接受(前端已升 api_keys)
800
807
  export const settingsLlmPutReqSchema = z
801
808
  .object({
809
+ provider: z.string().optional(),
802
810
  model: z.string().optional(),
803
811
  thinking_level: z.enum(THINKING_LEVELS).optional(),
804
- api_key: z.string().optional(), // 空字符串 = 清除已保存 key
812
+ api_keys: z.record(z.string(), z.string()).optional(), // provider key;空字符串 = 清除该家
805
813
  })
806
814
  .strict();
807
815
  export const settingsLlmPutResSchema = z.object({
808
816
  saved: z.literal(true),
809
817
  });
810
- // ============ 用户级配置文件(决策 48,批次十四):~/.ai-editor/config.json schema v1 ============
811
- // 契约来源:doc/design/decisions.md 决策 48、doc/api/endpoints.md「用户级配置文件」节。
818
+ // ============ 用户级配置文件(批次十四):~/.ai-editor/config.json schema v2(批次十六多 provider) ============
812
819
  // 设计要点:
813
820
  // - 非 strict(宽松读取):config.json 是用户自有文件,未来版本追加字段不应使整份配置失效
814
- // (zod 默认 strip 未知字段,safeParse 仍成功)
815
- // - schema_version 可选:缺省 = v0 旧格式,与 v1 同结构(model/thinking_level/api_key)直接兼容,
816
- // 不迁移不写回(决策 48:读侧兼容;用户下次在设置页保存时自然落新格式)
821
+ // (zod 默认 strip 未知字段,safeParse 仍成功)
822
+ // - schema_version 可选:缺省 = v0 旧格式;v0/v1 与 v2 均**读侧兼容不迁移不写回**
823
+ // (用户下次在设置页保存时自然落新格式);schema_version 非 1/2 的版本 → 整份失效(空配置默认值)
824
+ // - provider/api_keys 为 v2 字段:缺省 provider=deepseek(服务端解析时校验目录);
825
+ // 旧 api_key 字段读侧视为 api_keys["deepseek"]
817
826
  // - 校验仅在服务端执行(settings.ts 消费);client 只消费推断类型
818
827
  export const userConfigFileSchema = z
819
828
  .object({
820
- schema_version: z.literal(1).optional(), // 格式版本;缺省 = v0 旧格式(同结构兼容)
829
+ schema_version: z.union([z.literal(1), z.literal(2)]).optional(), // 格式版本;缺省 = v0 旧格式(同结构兼容)
830
+ provider: z.string().optional(), // 激活 provider id(缺省 deepseek;未知 id 由服务端解析兜底)
821
831
  model: z.string().optional(), // 当前模型名(缺省 deepseek-v4-flash)
822
- thinking_level: z.enum(THINKING_LEVELS).optional(), // 思考强度(决策 34;缺省 high)
823
- api_key: z.string().optional(), // DeepSeek API key(不入项目文件,决策 17)
832
+ thinking_level: z.enum(THINKING_LEVELS).optional(), // 思考强度(缺省 high)
833
+ api_key: z.string().optional(), // v1 旧字段(读侧视为 api_keys["deepseek"],不写回)
834
+ api_keys: z.record(z.string(), z.string()).optional(), // v2:各 provider API key(不入项目文件)
824
835
  })
825
836
  .passthrough(); // 未知字段保留不校验(用户自有文件,未来版本追加字段不应使整份配置失效)
826
- // ============ names 端点(endpoints.md「POST /api/v1/names/resolve」,决策 47 工具调用人类可读化) ============
837
+ // ============ names 端点(「POST /api/v1/names/resolve」, 工具调用人类可读化) ============
827
838
  // 批量名称解析:把工具参数中的 id 解析为人类可读名称(label = 类型中文,name = 实体名/节点标题)
828
839
  // 前缀分流(char-/set-/loc-/hook-/ev-/tp-/ref- → 实体;vol-/ch-/sc- → 大纲节点;rel- → null;其余 → null)
829
840
  export const namesResolveReqSchema = z
@@ -834,14 +845,14 @@ export const namesResolveReqSchema = z
834
845
  export const namesResolveResSchema = z.object({
835
846
  names: z.record(z.string(), z.object({ label: z.string(), name: z.string() }).nullable()),
836
847
  });
837
- // ============ SSE 事件(endpoints.md chat 端点事件流,第 738-765 行) ============
838
- /** 心跳 ping(每 15-30s,决策 20):空 payload */
848
+ // ============ SSE 事件( chat 端点事件流,第 738-765 行) ============
849
+ /** 心跳 ping(每 15-30s):空 payload */
839
850
  export const ssePingEventSchema = z.object({});
840
851
  /** tool_call:AI 调用了工具 */
841
852
  export const sseToolCallEventSchema = z.object({
842
853
  tool: z.string(),
843
854
  args: z.record(z.string(), z.unknown()),
844
- id: z.string(), // call_ 前缀(决策 18 成对重组依据)
855
+ id: z.string(), // call_ 前缀( 成对重组依据)
845
856
  });
846
857
  /** tool_result:工具执行结果 */
847
858
  export const sseToolResultEventSchema = z.object({
@@ -855,7 +866,7 @@ export const sseTextEventSchema = z.object({
855
866
  });
856
867
  /** proposal:AI 发出提案(完整预览仅经此事件推送 GUI,tool_result 不含预览,2026-08 修订) */
857
868
  export const sseProposalEventSchema = z.object({
858
- proposal_id: z.string(), // prop_ 前缀(决策 14)
869
+ proposal_id: z.string(), // prop_ 前缀
859
870
  type: z.string(), // 提案对应工具名(如 "propose_create_entity")
860
871
  preview: z.unknown(),
861
872
  });