@whispering233/ai-editor-db 0.0.20 → 0.0.26
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/connection.js +2 -2
- package/dist/connection.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/002_event_timeline.js +1 -1
- package/dist/migrations/002_event_timeline.js.map +1 -1
- package/dist/migrations/003_timepoint.js +8 -8
- package/dist/migrations/003_timepoint.js.map +1 -1
- package/dist/migrations/004_setting_tags.js +2 -2
- package/dist/migrations/004_setting_tags.js.map +1 -1
- package/dist/migrations/005_reference.js +2 -2
- package/dist/migrations/005_reference.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +7 -7
- package/dist/migrations/index.js.map +1 -1
- package/dist/queries/chat.d.ts +20 -20
- package/dist/queries/chat.d.ts.map +1 -1
- package/dist/queries/chat.js +105 -59
- package/dist/queries/chat.js.map +1 -1
- package/dist/queries/compute-state.d.ts +9 -9
- package/dist/queries/compute-state.d.ts.map +1 -1
- package/dist/queries/compute-state.js +22 -28
- package/dist/queries/compute-state.js.map +1 -1
- package/dist/queries/delta.d.ts +21 -21
- package/dist/queries/delta.d.ts.map +1 -1
- package/dist/queries/delta.js +110 -63
- package/dist/queries/delta.js.map +1 -1
- package/dist/queries/entity.d.ts +50 -46
- package/dist/queries/entity.d.ts.map +1 -1
- package/dist/queries/entity.js +226 -137
- package/dist/queries/entity.js.map +1 -1
- package/dist/queries/migration.d.ts +31 -31
- package/dist/queries/migration.d.ts.map +1 -1
- package/dist/queries/migration.js +43 -44
- package/dist/queries/migration.js.map +1 -1
- package/dist/queries/outline-ops.d.ts +22 -22
- package/dist/queries/outline-ops.d.ts.map +1 -1
- package/dist/queries/outline-ops.js +35 -42
- package/dist/queries/outline-ops.js.map +1 -1
- package/dist/queries/relation.d.ts +18 -18
- package/dist/queries/relation.d.ts.map +1 -1
- package/dist/queries/relation.js +115 -84
- package/dist/queries/relation.js.map +1 -1
- package/dist/queries/trash.d.ts +10 -10
- package/dist/queries/trash.d.ts.map +1 -1
- package/dist/queries/trash.js +75 -48
- package/dist/queries/trash.js.map +1 -1
- package/dist/query-db.d.ts +5 -0
- package/dist/query-db.d.ts.map +1 -0
- package/dist/query-db.js +22 -0
- package/dist/query-db.js.map +1 -0
- package/dist/schema.d.ts +8 -20
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +13 -85
- package/dist/schema.js.map +1 -1
- package/dist/storage/atomic.d.ts +8 -8
- package/dist/storage/atomic.d.ts.map +1 -1
- package/dist/storage/atomic.js +10 -11
- package/dist/storage/atomic.js.map +1 -1
- package/dist/storage/outline.d.ts +14 -14
- package/dist/storage/outline.d.ts.map +1 -1
- package/dist/storage/outline.js +17 -18
- package/dist/storage/outline.js.map +1 -1
- package/dist/storage/project.d.ts +11 -11
- package/dist/storage/project.d.ts.map +1 -1
- package/dist/storage/project.js +17 -18
- package/dist/storage/project.js.map +1 -1
- package/dist/tables.d.ts +718 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +141 -0
- package/dist/tables.js.map +1 -0
- package/package.json +3 -2
package/dist/queries/entity.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
// @whispering233/ai-editor-db 实体 CRUD 查询(S3.1)
|
|
2
2
|
//
|
|
3
|
-
// 单一事实来源:doc/database/schema.md 第 18-39 行(entities 表)、doc/api/endpoints.md 第 150-278 行
|
|
4
3
|
// (实体端点:列表 q/分页/排序 + 摘要字段、详情 + deltaCount、创建/部分更新/软删级联)、
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// (软删:常规查询默认过滤、级联软删关系与 Delta)、(updated_at 提案快照比对)。
|
|
5
|
+
// 时间约定():ISO 8601 应用层写入(nowIso),模块内不生成时间。
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
7
|
+
// 摘要字段提取():**行内解析**(SELECT 整行 → JSON.parse → JS 提取)——
|
|
8
|
+
// character → role/status、setting → tags/description(M2 批次六)、location → type、hook → status/payoff_timing、
|
|
9
|
+
// event → description/tags;
|
|
10
|
+
// 取舍:json_extract 免全量 parse 但需按类型动态列,SQL 复杂化;MVP 数据量小,行内解析
|
|
11
|
+
// 与 better-sqlite3 字符串列一致(chat.ts 同款风格),数据量大后再优化。
|
|
13
12
|
// 级联软删边界:relations/deltas 的**查询**模块 S3.2 才建——本卡只做级联软删所需 UPDATE。
|
|
13
|
+
//
|
|
14
|
+
// 批次十五(15.6 卡 5):查询全部经 queryDb 走 drizzle 构建器(混合风格 4A)。
|
|
15
|
+
// 语义逐句对照改造(git show 52c7c13^:packages/db/src/queries/entity.ts):
|
|
16
|
+
// - deleted_at IS NULL ↔ isNull;软删过滤语义不变
|
|
17
|
+
// - name LIKE ? ↔ like(entities.name, `%${q}%`)——通配符 %/_ 原样透传(模糊搜索语义)
|
|
18
|
+
// - 排序白名单(name/created_at/updated_at × asc/desc)↔ 动态选列对象 desc/asc(无字符串拼接)
|
|
19
|
+
// - event/timepoint 固定排序 `sort_order IS NULL, sort_order ASC, id ASC` ↔ orderBy(sql 模板)(NULL 沉底)
|
|
20
|
+
// - COUNT(*) ↔ count;IN 占位符 ↔ inArray(空集生成恒假 SQL,原生 IN (NULL) 语义等价)
|
|
21
|
+
// - EXISTS 子查询(eventOccursAt)↔ sql 模板(跨表互引,builder 难表达,4A 允许)
|
|
22
|
+
// - 热循环(moveEvent/moveTimepoint/moveSetting/reorderTimepoints 批量重写 sort_order):
|
|
23
|
+
// builder 版循环内每次 .run 重新编译 SQL(prepare 不复用);数据量小可接受,
|
|
24
|
+
// 如需极致性能可改 sql 模板( 混合风格边界)——循环处注释明示
|
|
25
|
+
// - data 等 JSON 列保持 text 模式:drizzle 行读出 string,写入 JSON.stringify,
|
|
26
|
+
// parseDataColumn 防御解析不动(坏行返回 {},.2 验证)
|
|
27
|
+
// 事务沿用 withTransaction(native db.transaction),连接级共享已验证(15.2 验证记录①)。
|
|
14
28
|
import { ENTITY_TYPES, generateEntityId } from "@whispering233/ai-editor-shared";
|
|
29
|
+
import { and, asc, count, desc, eq, inArray, isNull, like, or, sql } from "drizzle-orm";
|
|
15
30
|
import { nowIso } from "../storage/atomic.js";
|
|
16
31
|
import { withTransaction } from "../connection.js";
|
|
32
|
+
import { deltaRecords, entities, relationRecords } from "../tables.js";
|
|
33
|
+
import { queryDb } from "../query-db.js";
|
|
17
34
|
// relation.ts ↔ entity.ts 循环引用(relation.ts import getEntity):仅函数调用期使用
|
|
18
35
|
// RelationError/rowToRelationRow,无模块顶层求值依赖,ESM 运行时安全。
|
|
19
36
|
import { createRelation, deleteRelation, listSettingHierarchyEdges, RelationError, rowToRelationRow, wouldCreateSettingCycle, } from "./relation.js";
|
|
@@ -27,8 +44,8 @@ function toSummary(row) {
|
|
|
27
44
|
summary.role = data.role;
|
|
28
45
|
if (data.status !== undefined)
|
|
29
46
|
summary.status = data.status;
|
|
30
|
-
//
|
|
31
|
-
// search_entities
|
|
47
|
+
// (2026-08 批次十三):两行式行布局字段——动机摘要截断 40 字符(防
|
|
48
|
+
// search_entities 工具上下文膨胀, 同款语义)+ 性格/能力标签各前 2 个(行 chips)
|
|
32
49
|
if (typeof data.motivation === "string" && data.motivation !== "") {
|
|
33
50
|
summary.motivation = data.motivation.slice(0, 40);
|
|
34
51
|
}
|
|
@@ -44,12 +61,12 @@ function toSummary(row) {
|
|
|
44
61
|
}
|
|
45
62
|
break;
|
|
46
63
|
case "setting":
|
|
47
|
-
//
|
|
64
|
+
// K2(2026-08):分类由 data.tags 承接(与 event 同字段语义)——摘要暴露 tags(前 3 个)
|
|
48
65
|
if (Array.isArray(data.tags)) {
|
|
49
66
|
summary.tags = data.tags.filter((t) => typeof t === "string" && t !== "").slice(0, 3);
|
|
50
67
|
}
|
|
51
68
|
// M2(2026-08 批次六):描述摘要截断 100 字符——列表行展示用;截断防 search_entities
|
|
52
|
-
// 工具上下文膨胀(limit 200 × 长文描述会打爆 token
|
|
69
|
+
// 工具上下文膨胀(limit 200 × 长文描述会打爆 token 预算),完整文本在详情页
|
|
53
70
|
if (typeof data.description === "string" && data.description !== "") {
|
|
54
71
|
summary.description = data.description.slice(0, 100);
|
|
55
72
|
}
|
|
@@ -64,7 +81,7 @@ function toSummary(row) {
|
|
|
64
81
|
if (data.payoff_timing !== undefined)
|
|
65
82
|
summary.payoff_timing = data.payoff_timing;
|
|
66
83
|
break;
|
|
67
|
-
// event
|
|
84
|
+
// event:description/tags 两字段摘要( ;
|
|
68
85
|
// tags 为数组原样返回——Record 稀疏语义,字段缺失即不出现)
|
|
69
86
|
case "event":
|
|
70
87
|
if (data.description !== undefined)
|
|
@@ -72,10 +89,10 @@ function toSummary(row) {
|
|
|
72
89
|
if (data.tags !== undefined)
|
|
73
90
|
summary.tags = data.tags;
|
|
74
91
|
break;
|
|
75
|
-
// reference
|
|
92
|
+
// reference( 参考资料 + type 分类 + content 摘要截断 120 字 + tags 前 3
|
|
76
93
|
// + 来源字段(11.1 补 source;11.4 起按 kind 区分:file → file_name、link → url、
|
|
77
94
|
// 存量无 kind 条目 → source 兼容)——全文长文本不随列表返回(防列表响应与
|
|
78
|
-
// search_entities
|
|
95
|
+
// search_entities 工具上下文膨胀),完整 content 在详情页(get_entity 全量 data)
|
|
79
96
|
case "reference":
|
|
80
97
|
if (data.type !== undefined)
|
|
81
98
|
summary.type = data.type;
|
|
@@ -102,7 +119,7 @@ function toSummary(row) {
|
|
|
102
119
|
type: row.type,
|
|
103
120
|
name: row.name,
|
|
104
121
|
summary,
|
|
105
|
-
//
|
|
122
|
+
// (2026-08 批次十三):仅 setting 暴露同级手动排序位(稀疏——NULL 不出现)
|
|
106
123
|
...(row.type === "setting" && row.sort_order !== null ? { sortOrder: row.sort_order } : {}),
|
|
107
124
|
createdAt: row.created_at,
|
|
108
125
|
updatedAt: row.updated_at,
|
|
@@ -115,7 +132,7 @@ export function rowToEntityRow(row) {
|
|
|
115
132
|
type: row.type,
|
|
116
133
|
name: row.name,
|
|
117
134
|
data: parseDataColumn(row.data),
|
|
118
|
-
//
|
|
135
|
+
// setting 复用 sort_order 列承载同级手动排序位(NULL = 未参与);event/timepoint 为全局线性序
|
|
119
136
|
sort_order: row.sort_order ?? null,
|
|
120
137
|
created_at: row.created_at,
|
|
121
138
|
updated_at: row.updated_at,
|
|
@@ -142,7 +159,7 @@ function parseDataColumn(value) {
|
|
|
142
159
|
/**
|
|
143
160
|
* data 字段过滤(S6.3 工具 search_entities 下沉,filters 语义见 EntityListQuery):
|
|
144
161
|
* status 字符串相等匹配;tags 要求 `data.tags` 为数组且包含全部指定 tags(AND)。
|
|
145
|
-
* **分类字段统一为 data.tags
|
|
162
|
+
* **分类字段统一为 data.tags( K2 修订)**:setting 与 event 同语义,不再按类型路由。
|
|
146
163
|
* 匹配失败(如非数组)一律视为不匹配——防御,不做宽松猜测。
|
|
147
164
|
*/
|
|
148
165
|
function matchDataFilters(data, filters) {
|
|
@@ -160,7 +177,7 @@ function matchDataFilters(data, filters) {
|
|
|
160
177
|
return true;
|
|
161
178
|
}
|
|
162
179
|
/**
|
|
163
|
-
* 收集设定层级树中某节点的全部后代 id
|
|
180
|
+
* 收集设定层级树中某节点的全部后代 id(2026-08):复用 listSettingHierarchyEdges 全量边
|
|
164
181
|
* (只读、O(N)、走关系表索引,两端未软删)构建 childOf 邻接表(parentId → 直接子 id 列表),
|
|
165
182
|
* DFS 收集**不含自身**的递归后代集合。防环守卫:结果 Set 去重——已收集节点不再入栈,
|
|
166
183
|
* 数据异常成环也不死循环。父不存在/已软删 → 空集合(边集合已做软删可见性过滤)。
|
|
@@ -186,43 +203,47 @@ function collectSettingDescendants(db, rootId) {
|
|
|
186
203
|
return result;
|
|
187
204
|
}
|
|
188
205
|
/**
|
|
189
|
-
* 实体列表(GET /api/v1/entity/:type
|
|
206
|
+
* 实体列表(GET /api/v1/entity/:type,):
|
|
190
207
|
* type 过滤 + q 模糊搜索(name LIKE)+ 排序(name/created_at/updated_at × asc/desc,
|
|
191
|
-
* 白名单防注入)+ 分页(limit clamp 1-200)+
|
|
208
|
+
* 白名单防注入)+ 分页(limit clamp 1-200)+ **默认过滤软删**。
|
|
192
209
|
* total 为过滤后总数(不含分页)。
|
|
193
210
|
* filters 语义(S6.3 下沉):data 字段 JS 过滤(列表摘要不含 data),此时 SQL 只做
|
|
194
211
|
* type/q/软删过滤,filters + 分页在 JS 层(MVP 数据量小,全行查询可接受);
|
|
195
|
-
* parentId
|
|
212
|
+
* parentId 语义( 上层筛选):同上——复用 listSettingHierarchyEdges 收集递归后代集合,
|
|
196
213
|
* 与 filters 同款 JS 过滤路径(两者可同时存在,AND 组合;SQL 路径保持 COUNT+LIMIT 行为不变)。
|
|
214
|
+
*
|
|
215
|
+
* drizzle 改造:where 动态条件 and 组合(顺序与旧 where.join 一致:
|
|
216
|
+
* 软删过滤恒为首条);排序白名单映射为**列对象**(asc/desc 包装,禁止字符串拼接;
|
|
217
|
+
* event/timepoint 固定 sql 模板排序 NULL 沉底)。
|
|
197
218
|
*/
|
|
198
219
|
export function listEntities(db, query) {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
220
|
+
const q = queryDb(db);
|
|
221
|
+
const conds = [isNull(entities.deleted_at)];
|
|
201
222
|
if (query.type !== undefined) {
|
|
202
|
-
|
|
203
|
-
params.push(query.type);
|
|
223
|
+
conds.push(eq(entities.type, query.type));
|
|
204
224
|
}
|
|
205
225
|
if (query.q !== undefined && query.q !== "") {
|
|
206
|
-
|
|
207
|
-
|
|
226
|
+
// LIKE 通配符 %/_ 原样透传——模糊搜索语义(注释明示,不转义)
|
|
227
|
+
conds.push(like(entities.name, `%${query.q}%`));
|
|
208
228
|
}
|
|
229
|
+
const whereExpr = and(...conds);
|
|
209
230
|
// 排序白名单(列名不可参数化,只允许枚举值;id 作次级排序保证稳定分页)
|
|
210
|
-
const sortCol = query.sort === "name" ?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const
|
|
231
|
+
const sortCol = query.sort === "name" ? entities.name : query.sort === "created_at" ? entities.created_at : entities.updated_at;
|
|
232
|
+
// 默认降序(query.order === "asc" 才升序,与旧 `"ASC" : "DESC"` 语义一致)
|
|
233
|
+
const orderAsc = query.order === "asc";
|
|
234
|
+
// event / timepoint(时间轴, + G2)固定按 sort_order 升序、NULL 沉底(
|
|
235
|
+
// 列表恒按 sort_order 升序,sort/order 参数不参与排序)——`sort_order IS NULL` 为 1 的排最后
|
|
236
|
+
//(SQLite 布尔序),实现 NULL 沉底;id 作稳定次序
|
|
237
|
+
const orderByExpr = query.type === "event" || query.type === "timepoint"
|
|
238
|
+
? [sql `${entities.sort_order} IS NULL`, asc(entities.sort_order), asc(entities.id)]
|
|
239
|
+
: [orderAsc ? asc(sortCol) : desc(sortCol), asc(entities.id)];
|
|
217
240
|
const offset = Math.max(0, Math.trunc(query.offset ?? 0));
|
|
218
241
|
const limit = Math.min(200, Math.max(1, Math.trunc(query.limit ?? 50)));
|
|
219
|
-
// JS
|
|
242
|
+
// JS 过滤路径:filters(S6.3 工具下沉)或 parentId( 上级设定筛选)存在时,
|
|
220
243
|
// SQL 取全量候选行(type/q/软删),JS 层执行 data/层级过滤 + 分页(MVP 数据量小,全行查询可接受);
|
|
221
244
|
// 两者皆无时保持 COUNT + LIMIT SQL 原路径(行为不变)。
|
|
222
245
|
if (query.filters !== undefined || query.parentId !== undefined) {
|
|
223
|
-
const all =
|
|
224
|
-
.prepare(`SELECT * FROM entities WHERE ${where.join(" AND ")} ORDER BY ${orderSql}`)
|
|
225
|
-
.all(...params);
|
|
246
|
+
const all = q.select().from(entities).where(whereExpr).orderBy(...orderByExpr).all();
|
|
226
247
|
const descendants = query.parentId !== undefined ? collectSettingDescendants(db, query.parentId) : null;
|
|
227
248
|
const filtered = all.filter((r) => {
|
|
228
249
|
const row = rowToEntityRow(r);
|
|
@@ -237,28 +258,36 @@ export function listEntities(db, query) {
|
|
|
237
258
|
total: filtered.length,
|
|
238
259
|
};
|
|
239
260
|
}
|
|
240
|
-
const total =
|
|
241
|
-
const rows =
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
.
|
|
261
|
+
const total = q.select({ c: count() }).from(entities).where(whereExpr).get().c;
|
|
262
|
+
const rows = q
|
|
263
|
+
.select()
|
|
264
|
+
.from(entities)
|
|
265
|
+
.where(whereExpr)
|
|
266
|
+
.orderBy(...orderByExpr)
|
|
267
|
+
.limit(limit)
|
|
268
|
+
.offset(offset)
|
|
269
|
+
.all();
|
|
245
270
|
return { items: rows.map((r) => toSummary(rowToEntityRow(r))), total };
|
|
246
271
|
}
|
|
247
|
-
/** 按 id 取实体详情(GET /api/v1/entity/:type/:id);不存在或已软删返回 null
|
|
272
|
+
/** 按 id 取实体详情(GET /api/v1/entity/:type/:id);不存在或已软删返回 null */
|
|
248
273
|
export function getEntity(db, id) {
|
|
249
|
-
const row = db
|
|
250
|
-
.
|
|
251
|
-
.
|
|
274
|
+
const row = queryDb(db)
|
|
275
|
+
.select()
|
|
276
|
+
.from(entities)
|
|
277
|
+
.where(and(eq(entities.id, id), isNull(entities.deleted_at)))
|
|
278
|
+
.get();
|
|
252
279
|
return row === undefined ? null : rowToEntityRow(row);
|
|
253
280
|
}
|
|
254
|
-
/** 实体的 Delta 计数(详情响应 deltaCount
|
|
281
|
+
/** 实体的 Delta 计数(详情响应 deltaCount;目标实体软删的 Delta 不可见) */
|
|
255
282
|
export function countDeltasForEntity(db, id) {
|
|
256
|
-
return db
|
|
257
|
-
.
|
|
258
|
-
.
|
|
283
|
+
return queryDb(db)
|
|
284
|
+
.select({ c: count() })
|
|
285
|
+
.from(deltaRecords)
|
|
286
|
+
.where(and(eq(deltaRecords.target_id, id), isNull(deltaRecords.deleted_at)))
|
|
287
|
+
.get().c;
|
|
259
288
|
}
|
|
260
289
|
/**
|
|
261
|
-
* 创建实体(POST /api/v1/entity/:type
|
|
290
|
+
* 创建实体(POST /api/v1/entity/:type,):
|
|
262
291
|
* id = shared generateEntityId(char-/set-/loc-/hook- 前缀)、created_at/updated_at 应用层 ISO、
|
|
263
292
|
* data 缺省 {}、type 必须 ∈ ENTITY_TYPES(非法抛错——路由层 schema 校验后一般不可达,防御)。
|
|
264
293
|
* @returns 新行(EntityRow,data 已解析)
|
|
@@ -273,19 +302,29 @@ export function createEntity(db, input) {
|
|
|
273
302
|
type: input.type,
|
|
274
303
|
name: input.name,
|
|
275
304
|
data: input.data ?? {},
|
|
276
|
-
//
|
|
305
|
+
// 新实体默认不参与手动排序(setting 首次手动移动时按组重写)
|
|
277
306
|
sort_order: null,
|
|
278
307
|
created_at: now,
|
|
279
308
|
updated_at: now,
|
|
280
309
|
deleted_at: null,
|
|
281
310
|
};
|
|
282
|
-
db
|
|
311
|
+
queryDb(db)
|
|
312
|
+
.insert(entities)
|
|
313
|
+
.values({
|
|
314
|
+
id: row.id,
|
|
315
|
+
type: row.type,
|
|
316
|
+
name: row.name,
|
|
317
|
+
data: JSON.stringify(row.data), // JSON 列 text 模式
|
|
318
|
+
created_at: row.created_at,
|
|
319
|
+
updated_at: row.updated_at,
|
|
320
|
+
})
|
|
321
|
+
.run();
|
|
283
322
|
return row;
|
|
284
323
|
}
|
|
285
324
|
/**
|
|
286
|
-
* 部分更新实体(PUT /api/v1/entity/:type/:id
|
|
287
|
-
* 仅合并传入字段——name 直接替换;data
|
|
288
|
-
* updated_at
|
|
325
|
+
* 部分更新实体(PUT /api/v1/entity/:type/:id,):
|
|
326
|
+
* 仅合并传入字段——name 直接替换;data **浅合并**(未传字段保留,);
|
|
327
|
+
* updated_at 刷新( 提案快照比对)。软删实体不可更新(getEntity 过滤 → null,路由层 404)。
|
|
289
328
|
* 读后写包 withTransaction(oracle 审核建议 2):better-sqlite3 同步单连接下无竞态(安全),
|
|
290
329
|
* 包事务统一风格(与 softDeleteEntity 一致);返回行在事务内直接构造,无额外复杂度。
|
|
291
330
|
* @returns 更新后的行;实体不存在或已软删返回 null
|
|
@@ -301,17 +340,25 @@ export function updateEntity(db, id, patch) {
|
|
|
301
340
|
data: patch.data === undefined ? existing.data : { ...existing.data, ...patch.data },
|
|
302
341
|
updated_at: nowIso(),
|
|
303
342
|
};
|
|
304
|
-
db
|
|
343
|
+
queryDb(db)
|
|
344
|
+
.update(entities)
|
|
345
|
+
.set({
|
|
346
|
+
name: next.name,
|
|
347
|
+
data: JSON.stringify(next.data), // JSON 列 text 模式
|
|
348
|
+
updated_at: next.updated_at,
|
|
349
|
+
})
|
|
350
|
+
.where(eq(entities.id, id))
|
|
351
|
+
.run();
|
|
305
352
|
return next;
|
|
306
353
|
});
|
|
307
354
|
}
|
|
308
355
|
/**
|
|
309
|
-
* 移动时间轴事件(PUT /api/v1/entity/event/:id/move
|
|
356
|
+
* 移动时间轴事件(PUT /api/v1/entity/event/:id/move,,):
|
|
310
357
|
* 事件排序为**全局线性序**(跨所有事件,0-based)——
|
|
311
358
|
* 1. 读出全部未软删 event 行按 sort_order 升序(NULL 沉底,id 作稳定次序)排成数组
|
|
312
359
|
* 2. 目标 id 不存在或已软删 → 返回 null(路由层映射 404 ENTITY_NOT_FOUND)
|
|
313
360
|
* 3. 剔除自身后,order clamp 到 [0, 剩余数](负数→0、超总数→末尾),splice 插入——
|
|
314
|
-
*
|
|
361
|
+
* 与大纲 moveOutlineNode 的数组 splice 语义一致、可验证
|
|
315
362
|
* 4. 重写整个数组的 sort_order 为 0..n-1(事务内逐行 UPDATE),并刷新该行 updated_at
|
|
316
363
|
*
|
|
317
364
|
* 事务:withTransaction 包住读改写(better-sqlite3 同步单连接下无竞态,包事务统一风格,
|
|
@@ -320,99 +367,115 @@ export function updateEntity(db, id, patch) {
|
|
|
320
367
|
*/
|
|
321
368
|
export function moveEvent(db, id, order, updatedAt) {
|
|
322
369
|
return withTransaction(db, () => {
|
|
370
|
+
const q = queryDb(db);
|
|
323
371
|
// 全部未软删 event,按 sort_order 升序(NULL 沉底)排成数组(id 作稳定次序)
|
|
324
|
-
const rows =
|
|
325
|
-
.
|
|
326
|
-
|
|
372
|
+
const rows = q
|
|
373
|
+
.select()
|
|
374
|
+
.from(entities)
|
|
375
|
+
.where(and(eq(entities.type, "event"), isNull(entities.deleted_at)))
|
|
376
|
+
.orderBy(sql `${entities.sort_order} IS NULL`, asc(entities.sort_order), asc(entities.id))
|
|
327
377
|
.all();
|
|
328
378
|
const idx = rows.findIndex((r) => r.id === id);
|
|
329
379
|
if (idx < 0)
|
|
330
|
-
return null; //
|
|
380
|
+
return null; // 不存在或已软删
|
|
331
381
|
const [moved] = rows.splice(idx, 1);
|
|
332
|
-
// clamp 到 [0, 剩余数]:负数→0、超总数→末尾(
|
|
382
|
+
// clamp 到 [0, 剩余数]:负数→0、超总数→末尾( )
|
|
333
383
|
const pos = Math.max(0, Math.min(Math.trunc(order), rows.length));
|
|
334
384
|
rows.splice(pos, 0, moved);
|
|
335
|
-
// 重写全局线性序 0..n-1;被移动行刷新 updated_at
|
|
336
|
-
|
|
385
|
+
// 重写全局线性序 0..n-1;被移动行刷新 updated_at,其余行不动
|
|
386
|
+
// 热循环:builder 每次 .run 重新编译 SQL(prepare 不复用);数据量小可接受,
|
|
387
|
+
// 如需极致性能可改 sql 模板( 混合风格边界)
|
|
337
388
|
for (let i = 0; i < rows.length; i++) {
|
|
338
389
|
const row = rows[i];
|
|
339
|
-
update
|
|
390
|
+
q.update(entities)
|
|
391
|
+
.set({ sort_order: i, updated_at: row.id === id ? updatedAt : row.updated_at })
|
|
392
|
+
.where(eq(entities.id, row.id))
|
|
393
|
+
.run();
|
|
340
394
|
}
|
|
341
395
|
return { moved: true };
|
|
342
396
|
});
|
|
343
397
|
}
|
|
344
|
-
// ============ 时间标签点(G2
|
|
398
|
+
// ============ 时间标签点(G2):listTimepoints / moveTimepoint / occurs_at 挂载 ============
|
|
345
399
|
/**
|
|
346
400
|
* 全部未软删时间标签点(G2):按 sort_order 升序(NULL 沉底,id 作稳定次序)——
|
|
347
401
|
* timepoint 的全局线性序(组间序),与 listEntities 的 event 查询同款语义。
|
|
348
402
|
* G2.2/2.3(时间轴渲染、AI 排序 propose_reorder_timepoints)共用,单一事实来源查询。
|
|
349
403
|
*/
|
|
350
404
|
export function listTimepoints(db) {
|
|
351
|
-
const rows = db
|
|
352
|
-
.
|
|
353
|
-
|
|
405
|
+
const rows = queryDb(db)
|
|
406
|
+
.select()
|
|
407
|
+
.from(entities)
|
|
408
|
+
.where(and(eq(entities.type, "timepoint"), isNull(entities.deleted_at)))
|
|
409
|
+
.orderBy(sql `${entities.sort_order} IS NULL`, asc(entities.sort_order), asc(entities.id))
|
|
354
410
|
.all();
|
|
355
411
|
return rows.map((r) => rowToEntityRow(r));
|
|
356
412
|
}
|
|
357
413
|
/**
|
|
358
|
-
* 移动时间标签点(PUT /api/v1/entity/timepoint/:id/move,G2
|
|
414
|
+
* 移动时间标签点(PUT /api/v1/entity/timepoint/:id/move,G2):
|
|
359
415
|
* 语义与 moveEvent 完全一致(timepoint 全局线性序 0..n-1)——
|
|
360
416
|
* 1. 读出全部未软删 timepoint 按 sort_order 升序(NULL 沉底,id 稳定次序)排成数组
|
|
361
417
|
* 2. 目标 id 不存在或已软删 → 返回 null(路由层映射 404)
|
|
362
418
|
* 3. 剔除自身后 order clamp 到 [0, 剩余数],splice 插入
|
|
363
|
-
* 4. 重写整个数组 sort_order 为 0..n-1,仅被移动行刷新 updated_at
|
|
419
|
+
* 4. 重写整个数组 sort_order 为 0..n-1,仅被移动行刷新 updated_at
|
|
364
420
|
*
|
|
365
|
-
* 注意:拖拽 timepoint
|
|
421
|
+
* 注意:拖拽 timepoint **不改其下事件序**(双独立线性序, G2 修订)——
|
|
366
422
|
* 本函数只碰 timepoint 行,event.sort_order 与 occurs_at 关系均不动。
|
|
367
423
|
* 事务:withTransaction 包住读改写(同 moveEvent,better-sqlite3 同步单连接无竞态)。
|
|
368
424
|
* @returns { moved: true };timepoint 不存在或已软删返回 null
|
|
369
425
|
*/
|
|
370
426
|
export function moveTimepoint(db, id, order, updatedAt) {
|
|
371
427
|
return withTransaction(db, () => {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
428
|
+
const q = queryDb(db);
|
|
429
|
+
const rows = q
|
|
430
|
+
.select()
|
|
431
|
+
.from(entities)
|
|
432
|
+
.where(and(eq(entities.type, "timepoint"), isNull(entities.deleted_at)))
|
|
433
|
+
.orderBy(sql `${entities.sort_order} IS NULL`, asc(entities.sort_order), asc(entities.id))
|
|
375
434
|
.all();
|
|
376
435
|
const idx = rows.findIndex((r) => r.id === id);
|
|
377
436
|
if (idx < 0)
|
|
378
|
-
return null; //
|
|
437
|
+
return null; // 不存在或已软删
|
|
379
438
|
const [moved] = rows.splice(idx, 1);
|
|
380
439
|
const pos = Math.max(0, Math.min(Math.trunc(order), rows.length));
|
|
381
440
|
rows.splice(pos, 0, moved);
|
|
382
|
-
|
|
441
|
+
// 热循环:同 moveEvent( 混合风格边界注释)
|
|
383
442
|
for (let i = 0; i < rows.length; i++) {
|
|
384
443
|
const row = rows[i];
|
|
385
|
-
update
|
|
444
|
+
q.update(entities)
|
|
445
|
+
.set({ sort_order: i, updated_at: row.id === id ? updatedAt : row.updated_at })
|
|
446
|
+
.where(eq(entities.id, row.id))
|
|
447
|
+
.run();
|
|
386
448
|
}
|
|
387
449
|
return { moved: true };
|
|
388
450
|
});
|
|
389
451
|
}
|
|
390
|
-
// ============
|
|
452
|
+
// ============ 设定手动排序(2026-08 批次十三):同级组内线性序 ============
|
|
391
453
|
/**
|
|
392
|
-
* 移动设定(PUT /api/v1/entity/setting/:id/move
|
|
454
|
+
* 移动设定(PUT /api/v1/entity/setting/:id/move,;修订「设定无 sort_order 语义」):
|
|
393
455
|
* **复合写**——改父 + 同级重排一次事务提交(对齐 G2 event move_to 先例):
|
|
394
456
|
* 1. 存在性:目标设定不存在/已软删 → null(路由层映射 404)
|
|
395
|
-
* 2.
|
|
396
|
-
*
|
|
457
|
+
* 2. 防环(与 POST /relation 同级校验):目标父 = 自身 → 自指;
|
|
458
|
+
* 目标父的祖先链含该设定 → 成环——均抛 RelationError SETTING_CYCLE(路由层映射 400)
|
|
397
459
|
* 3. 改父(targetParentId ≠ 当前父)→ 事务内先建新 belongs_to 边(createRelation 校验
|
|
398
|
-
*
|
|
460
|
+
* 父端点存在性 → ENDPOINT_NOT_FOUND)后物理删旧边(先建后删防数据丢失,对齐 EntityDetail)
|
|
399
461
|
* 4. 同级重排:目标组 = 新父的子级组 / 根 = 无父设定组(改父后按新边计算);
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
462
|
+
* 组内按(sort_order NULL 沉底 → sort_order → name → id)排齐,剔除自身后按 order
|
|
463
|
+
* (0-based,缺省 = 组尾)splice 插入,重写组内 sort_order 0..n-1——
|
|
464
|
+
* 仅被移行刷新 updated_at,其余行时间戳不动
|
|
403
465
|
* @returns { moved: true };设定不存在/已软删返回 null
|
|
404
466
|
* @throws RelationError SETTING_CYCLE(自指/成环)/ ENDPOINT_NOT_FOUND(目标父不存在/已软删)
|
|
405
467
|
*/
|
|
406
468
|
export function moveSetting(db, id, input, outlineDir) {
|
|
407
469
|
return withTransaction(db, () => {
|
|
470
|
+
const q = queryDb(db);
|
|
408
471
|
const row = getEntity(db, id);
|
|
409
472
|
if (row === null)
|
|
410
|
-
return null; //
|
|
473
|
+
return null; // 不存在或已软删
|
|
411
474
|
const edges = listSettingHierarchyEdges(db);
|
|
412
475
|
const parentOf = new Map(edges.map((e) => [e.childId, e.parentId]));
|
|
413
476
|
const currentParent = parentOf.get(id) ?? null;
|
|
414
477
|
const targetParent = input.parentId;
|
|
415
|
-
//
|
|
478
|
+
// 防环/自指:先于任何写操作校验——目标父的祖先链不得含该设定
|
|
416
479
|
if (targetParent !== null) {
|
|
417
480
|
if (targetParent === id) {
|
|
418
481
|
throw new RelationError("SETTING_CYCLE", "设定不能作为自己的上级");
|
|
@@ -437,13 +500,15 @@ export function moveSetting(db, id, input, outlineDir) {
|
|
|
437
500
|
}, outlineDir);
|
|
438
501
|
}
|
|
439
502
|
// 删旧边:按旧父 target_id 精确匹配——**不能按 source_id 删**(刚建的新边同 source,
|
|
440
|
-
// 会连同被删导致改父结果丢失,debug
|
|
503
|
+
// 会连同被删导致改父结果丢失,debug 实测踩坑);一设定一父,至多一条
|
|
441
504
|
if (currentParent !== null) {
|
|
442
|
-
const oldRows =
|
|
443
|
-
.
|
|
444
|
-
.
|
|
505
|
+
const oldRows = q
|
|
506
|
+
.select({ id: relationRecords.id })
|
|
507
|
+
.from(relationRecords)
|
|
508
|
+
.where(and(eq(relationRecords.source_type, "setting"), eq(relationRecords.source_id, id), eq(relationRecords.target_id, currentParent), eq(relationRecords.relation_type, "belongs_to"), eq(relationRecords.target_type, "setting"), isNull(relationRecords.deleted_at)))
|
|
509
|
+
.all();
|
|
445
510
|
for (const r of oldRows) {
|
|
446
|
-
deleteRelation(db, r.id); // 手动删关系 =
|
|
511
|
+
deleteRelation(db, r.id); // 手动删关系 = 物理删
|
|
447
512
|
}
|
|
448
513
|
}
|
|
449
514
|
}
|
|
@@ -452,18 +517,23 @@ export function moveSetting(db, id, input, outlineDir) {
|
|
|
452
517
|
const parentOfAfter = new Map(edgesAfter.map((e) => [e.childId, e.parentId]));
|
|
453
518
|
let groupIds;
|
|
454
519
|
if (targetParent === null) {
|
|
455
|
-
const all =
|
|
456
|
-
.
|
|
520
|
+
const all = q
|
|
521
|
+
.select({ id: entities.id })
|
|
522
|
+
.from(entities)
|
|
523
|
+
.where(and(eq(entities.type, "setting"), isNull(entities.deleted_at)))
|
|
457
524
|
.all();
|
|
458
525
|
groupIds = all.map((r) => r.id).filter((gid) => !parentOfAfter.has(gid));
|
|
459
526
|
}
|
|
460
527
|
else {
|
|
461
528
|
groupIds = edgesAfter.filter((e) => e.parentId === targetParent).map((e) => e.childId);
|
|
462
529
|
}
|
|
463
|
-
// 组内排齐(NULL 沉底 → sort_order → name → id,与 SQLite
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
.
|
|
530
|
+
// 组内排齐(NULL 沉底 → sort_order → name → id,与 SQLite 排序语义一致);
|
|
531
|
+
// inArray 空集生成恒假 SQL(无组 → 空集,原 IN (NULL) 语义等价)
|
|
532
|
+
const rows = (q
|
|
533
|
+
.select({ id: entities.id, name: entities.name, sort_order: entities.sort_order, updated_at: entities.updated_at })
|
|
534
|
+
.from(entities)
|
|
535
|
+
.where(inArray(entities.id, groupIds))
|
|
536
|
+
.all()).sort((a, b) => {
|
|
467
537
|
const ao = a.sort_order === null ? 1 : 0;
|
|
468
538
|
const bo = b.sort_order === null ? 1 : 0;
|
|
469
539
|
if (ao !== bo)
|
|
@@ -483,10 +553,13 @@ export function moveSetting(db, id, input, outlineDir) {
|
|
|
483
553
|
? rows.length
|
|
484
554
|
: Math.max(0, Math.min(Math.trunc(input.order), rows.length));
|
|
485
555
|
rows.splice(pos, 0, moved);
|
|
486
|
-
|
|
556
|
+
// 热循环:builder 每次 .run 重新编译 SQL(prepare 不复用);数据量小可接受( 边界)
|
|
487
557
|
const now = nowIso();
|
|
488
558
|
for (let i = 0; i < rows.length; i++) {
|
|
489
|
-
update
|
|
559
|
+
q.update(entities)
|
|
560
|
+
.set({ sort_order: i, updated_at: rows[i].id === id ? now : rows[i].updated_at })
|
|
561
|
+
.where(eq(entities.id, rows[i].id))
|
|
562
|
+
.run();
|
|
490
563
|
}
|
|
491
564
|
return { moved: true };
|
|
492
565
|
});
|
|
@@ -499,16 +572,18 @@ export function moveSetting(db, id, input, outlineDir) {
|
|
|
499
572
|
* @returns 挂载关系(RelationRow,source_id = timepoint id);未挂载/关系软删/timepoint 软删 → null
|
|
500
573
|
*/
|
|
501
574
|
export function eventOccursAt(db, eventId) {
|
|
502
|
-
const row = db
|
|
503
|
-
.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
575
|
+
const row = queryDb(db)
|
|
576
|
+
.select()
|
|
577
|
+
.from(relationRecords)
|
|
578
|
+
.where(and(eq(relationRecords.target_type, "event"), eq(relationRecords.target_id, eventId), eq(relationRecords.relation_type, "occurs_at"), isNull(relationRecords.deleted_at), eq(relationRecords.source_type, "timepoint"),
|
|
579
|
+
// EXISTS 防御:timepoint 软删时 occurs_at 已级联软删,
|
|
580
|
+
// 此处兜底校验 source 端点未软删(跨表互引,sql 模板表达,4A 允许)
|
|
581
|
+
sql `EXISTS (SELECT 1 FROM entities e WHERE e.id = ${relationRecords.source_id} AND e.deleted_at IS NULL)`))
|
|
582
|
+
.get();
|
|
508
583
|
return row === undefined ? null : rowToRelationRow(row);
|
|
509
584
|
}
|
|
510
585
|
/**
|
|
511
|
-
* occurs_at 1:n 挂载校验(G2
|
|
586
|
+
* occurs_at 1:n 挂载校验(G2,一个事件至多挂一个时间点):
|
|
512
587
|
* 建 occurs_at 前调用——事件已有未软删挂载 → 抛 RelationError(EVENT_ALREADY_MOUNTED,
|
|
513
588
|
* 与现有关系校验风格一致,G2.2 路由层 catch 映射 409)。
|
|
514
589
|
* 未挂载 → 无副作用(void),可继续建关系。
|
|
@@ -521,13 +596,13 @@ export function assertEventSingleOccursAt(db, eventId) {
|
|
|
521
596
|
}
|
|
522
597
|
}
|
|
523
598
|
/**
|
|
524
|
-
* 批量重排时间标签点(G2
|
|
599
|
+
* 批量重排时间标签点(G2,注记:LLM 按时间点 name 语义排序 → 提案确认后执行):
|
|
525
600
|
* 1. 事务内读出当前全部未软删 timepoint id 集合(listTimepoints 同款序)
|
|
526
601
|
* 2. **校验 orderedIds 与当前集合完全相等**(缺/多/重复 → 抛错)——正常由提案 references
|
|
527
|
-
*
|
|
602
|
+
* 快照校验先拦截,此处为防御纵深(LLM 幻觉漏时间点 / 确认前用户已拖拽增删)
|
|
528
603
|
* 3. 按 orderedIds 序重写全部 timepoint sort_order 0..n-1
|
|
529
604
|
* 4. **全部时间点 updated_at 统一刷新为 nowIso**——批量重排是**全量变化**,与
|
|
530
|
-
*
|
|
605
|
+
* moveTimepoint 只刷被移单行区分
|
|
531
606
|
*
|
|
532
607
|
* @returns 重排时间点数(n)
|
|
533
608
|
*/
|
|
@@ -544,39 +619,51 @@ export function reorderTimepoints(db, orderedIds, nowIsoTimestamp) {
|
|
|
544
619
|
const brief = (ids) => ids.length === 0 ? "" : `(${ids.slice(0, 5).join(", ")}${ids.length > 5 ? "…" : ""})`;
|
|
545
620
|
throw new Error(`时间点集合与当前时间轴不一致${dup}: 缺失 ${missing.length} 个${brief(missing)}、多余 ${extra.length} 个${brief(extra)}`);
|
|
546
621
|
}
|
|
547
|
-
const
|
|
622
|
+
const q = queryDb(db);
|
|
623
|
+
// 热循环:同 moveEvent( 混合风格边界注释)
|
|
548
624
|
for (let i = 0; i < orderedIds.length; i++) {
|
|
549
|
-
update
|
|
625
|
+
q.update(entities)
|
|
626
|
+
.set({ sort_order: i, updated_at: nowIsoTimestamp })
|
|
627
|
+
.where(eq(entities.id, orderedIds[i]))
|
|
628
|
+
.run();
|
|
550
629
|
}
|
|
551
630
|
return orderedIds.length;
|
|
552
631
|
});
|
|
553
632
|
}
|
|
554
633
|
/**
|
|
555
|
-
* 软删实体(DELETE /api/v1/entity/:type/:id
|
|
634
|
+
* 软删实体(DELETE /api/v1/entity/:type/:id):
|
|
556
635
|
* **级联软删关联关系与 Delta**(单库内事务保证原子性):
|
|
557
|
-
* - relation_records:source_id = id **或** target_id = id
|
|
558
|
-
* - delta_records:target_id = id
|
|
559
|
-
* - 自身:deleted_at 置位 + updated_at
|
|
636
|
+
* - relation_records:source_id = id **或** target_id = id(任一端点软删即不可见)
|
|
637
|
+
* - delta_records:target_id = id(目标实体软删即不可见)
|
|
638
|
+
* - 自身:deleted_at 置位 + updated_at 刷新(软删亦更新版本戳)
|
|
560
639
|
* 幂等:实体已软删(或不存在)→ 返回 null 且无任何副作用(级联 UPDATE 均带 deleted_at IS NULL 过滤)。
|
|
561
640
|
*
|
|
562
641
|
* @returns { relations, deltas } 实际级联行数;实体不存在/已软删返回 null
|
|
563
642
|
*/
|
|
564
643
|
export function softDeleteEntity(db, id, deletedAt) {
|
|
565
644
|
return withTransaction(db, () => {
|
|
566
|
-
const
|
|
645
|
+
const q = queryDb(db);
|
|
646
|
+
const exists = q.select({ id: entities.id }).from(entities).where(and(eq(entities.id, id), isNull(entities.deleted_at))).get();
|
|
567
647
|
if (exists === undefined)
|
|
568
648
|
return null;
|
|
569
|
-
const rel =
|
|
570
|
-
.
|
|
571
|
-
.
|
|
572
|
-
|
|
573
|
-
.
|
|
574
|
-
|
|
575
|
-
|
|
649
|
+
const rel = q
|
|
650
|
+
.update(relationRecords)
|
|
651
|
+
.set({ deleted_at: deletedAt })
|
|
652
|
+
.where(and(isNull(relationRecords.deleted_at), or(eq(relationRecords.source_id, id), eq(relationRecords.target_id, id))))
|
|
653
|
+
.run();
|
|
654
|
+
const delta = q
|
|
655
|
+
.update(deltaRecords)
|
|
656
|
+
.set({ deleted_at: deletedAt })
|
|
657
|
+
.where(and(isNull(deltaRecords.deleted_at), eq(deltaRecords.target_id, id)))
|
|
658
|
+
.run();
|
|
659
|
+
q.update(entities)
|
|
660
|
+
.set({ deleted_at: deletedAt, updated_at: deletedAt })
|
|
661
|
+
.where(and(eq(entities.id, id), isNull(entities.deleted_at)))
|
|
662
|
+
.run();
|
|
576
663
|
return { relations: rel.changes, deltas: delta.changes };
|
|
577
664
|
});
|
|
578
665
|
}
|
|
579
|
-
/**
|
|
666
|
+
/** 标签数组直方图(rules 数组展平计数;非字符串项/空串不计入) */
|
|
580
667
|
function countTags(values) {
|
|
581
668
|
const out = {};
|
|
582
669
|
for (const v of values) {
|
|
@@ -619,15 +706,17 @@ function topAbilityCounts(rows, limit) {
|
|
|
619
706
|
.map(([ability, count]) => ({ ability, count }));
|
|
620
707
|
}
|
|
621
708
|
/**
|
|
622
|
-
* 实体聚合统计(S6.3 工具 get_entity_summary
|
|
623
|
-
* 指定类型实体的总数 +
|
|
624
|
-
* 分布字段按类型稀疏出现:character→byRole/byStatus/topAbilities、setting→byTags
|
|
709
|
+
* 实体聚合统计(S6.3 工具 get_entity_summary 下沉,「聚合分析」):
|
|
710
|
+
* 指定类型实体的总数 + 类型专属分布。仅统计非软删实体;
|
|
711
|
+
* 分布字段按类型稀疏出现:character→byRole/byStatus/topAbilities、setting→byTags、
|
|
625
712
|
* location→byType、hook→byStatus/byPayoffTiming;缺字段(data 未填)不报错、不计入。
|
|
626
713
|
*/
|
|
627
714
|
export function getEntitySummaryStats(db, type) {
|
|
628
|
-
const rows = db
|
|
629
|
-
.
|
|
630
|
-
.
|
|
715
|
+
const rows = queryDb(db)
|
|
716
|
+
.select({ data: entities.data })
|
|
717
|
+
.from(entities)
|
|
718
|
+
.where(and(eq(entities.type, type), isNull(entities.deleted_at)))
|
|
719
|
+
.all();
|
|
631
720
|
const result = { type, total: rows.length };
|
|
632
721
|
const dataOf = (r) => parseDataColumn(r.data);
|
|
633
722
|
switch (type) {
|
|
@@ -637,7 +726,7 @@ export function getEntitySummaryStats(db, type) {
|
|
|
637
726
|
result.topAbilities = topAbilityCounts(rows, 10);
|
|
638
727
|
break;
|
|
639
728
|
case "setting":
|
|
640
|
-
//
|
|
729
|
+
// K2(2026-08):分类由 data.tags 承接——分布统计标签(数组展平计数)
|
|
641
730
|
result.byTags = countTags(rows.map((r) => dataOf(r).tags));
|
|
642
731
|
break;
|
|
643
732
|
case "location":
|