agent-remnote 0.1.0 → 0.3.0
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/CHANGELOG.md +19 -0
- package/README.md +47 -0
- package/dist/main.js +7969 -4691
- package/package.json +19 -4
- package/dist/apps/cli/src/adapters/mcp.js +0 -1
- package/dist/apps/cli/src/commands/_enqueue.js +0 -138
- package/dist/apps/cli/src/commands/_shared.js +0 -57
- package/dist/apps/cli/src/commands/_tool.js +0 -28
- package/dist/apps/cli/src/commands/apply.js +0 -81
- package/dist/apps/cli/src/commands/config/index.js +0 -3
- package/dist/apps/cli/src/commands/config/print.js +0 -28
- package/dist/apps/cli/src/commands/daily/index.js +0 -4
- package/dist/apps/cli/src/commands/daily/summary.js +0 -25
- package/dist/apps/cli/src/commands/daily/write.js +0 -145
- package/dist/apps/cli/src/commands/db/backups.js +0 -23
- package/dist/apps/cli/src/commands/db/index.js +0 -4
- package/dist/apps/cli/src/commands/db/recent.js +0 -178
- package/dist/apps/cli/src/commands/doctor.js +0 -124
- package/dist/apps/cli/src/commands/index.js +0 -73
- package/dist/apps/cli/src/commands/ops/index.js +0 -4
- package/dist/apps/cli/src/commands/ops/list.js +0 -12
- package/dist/apps/cli/src/commands/ops/schema.js +0 -77
- package/dist/apps/cli/src/commands/queue/enqueue.js +0 -73
- package/dist/apps/cli/src/commands/queue/index.js +0 -5
- package/dist/apps/cli/src/commands/queue/inspect.js +0 -26
- package/dist/apps/cli/src/commands/queue/stats.js +0 -14
- package/dist/apps/cli/src/commands/read/by-reference.js +0 -35
- package/dist/apps/cli/src/commands/read/connections.js +0 -15
- package/dist/apps/cli/src/commands/read/index.js +0 -21
- package/dist/apps/cli/src/commands/read/inspect.js +0 -34
- package/dist/apps/cli/src/commands/read/outline.js +0 -59
- package/dist/apps/cli/src/commands/read/query.js +0 -95
- package/dist/apps/cli/src/commands/read/references.js +0 -41
- package/dist/apps/cli/src/commands/read/resolve-ref.js +0 -32
- package/dist/apps/cli/src/commands/read/search.js +0 -40
- package/dist/apps/cli/src/commands/read/table.js +0 -32
- package/dist/apps/cli/src/commands/todos/index.js +0 -3
- package/dist/apps/cli/src/commands/todos/list.js +0 -33
- package/dist/apps/cli/src/commands/topic/index.js +0 -3
- package/dist/apps/cli/src/commands/topic/summary.js +0 -44
- package/dist/apps/cli/src/commands/wechat/index.js +0 -3
- package/dist/apps/cli/src/commands/wechat/outline.js +0 -430
- package/dist/apps/cli/src/commands/write/bullet.js +0 -76
- package/dist/apps/cli/src/commands/write/index.js +0 -4
- package/dist/apps/cli/src/commands/write/md.js +0 -91
- package/dist/apps/cli/src/commands/ws/_shared.js +0 -129
- package/dist/apps/cli/src/commands/ws/ensure.js +0 -22
- package/dist/apps/cli/src/commands/ws/health.js +0 -15
- package/dist/apps/cli/src/commands/ws/index.js +0 -21
- package/dist/apps/cli/src/commands/ws/logs.js +0 -95
- package/dist/apps/cli/src/commands/ws/restart.js +0 -73
- package/dist/apps/cli/src/commands/ws/serve.js +0 -52
- package/dist/apps/cli/src/commands/ws/start.js +0 -70
- package/dist/apps/cli/src/commands/ws/status.js +0 -60
- package/dist/apps/cli/src/commands/ws/stop.js +0 -59
- package/dist/apps/cli/src/commands/ws/trigger.js +0 -20
- package/dist/apps/cli/src/main.js +0 -79
- package/dist/apps/cli/src/services/AppConfig.js +0 -3
- package/dist/apps/cli/src/services/Config.js +0 -91
- package/dist/apps/cli/src/services/DaemonFiles.js +0 -91
- package/dist/apps/cli/src/services/Errors.js +0 -49
- package/dist/apps/cli/src/services/Output.js +0 -16
- package/dist/apps/cli/src/services/Payload.js +0 -90
- package/dist/apps/cli/src/services/Process.js +0 -94
- package/dist/apps/cli/src/services/Queue.js +0 -120
- package/dist/apps/cli/src/services/RefResolver.js +0 -111
- package/dist/apps/cli/src/services/RemDb.js +0 -35
- package/dist/apps/cli/src/services/WsClient.js +0 -170
- package/dist/apps/cli/tests/apply.contract.test.js +0 -31
- package/dist/apps/cli/tests/db-recent.contract.test.js +0 -22
- package/dist/apps/cli/tests/help.contract.test.js +0 -30
- package/dist/apps/cli/tests/helpers/runCli.js +0 -45
- package/dist/apps/cli/tests/ids-output.contract.test.js +0 -30
- package/dist/apps/cli/tests/payload-stdin.contract.test.js +0 -15
- package/dist/apps/cli/tests/read-search.contract.test.js +0 -22
- package/dist/apps/cli/tests/ws-health.contract.test.js +0 -36
- package/dist/apps/cli/vitest.config.js +0 -7
- package/dist/packages/mcp/src/public.js +0 -18
- package/dist/packages/mcp/src/queue/dao.js +0 -165
- package/dist/packages/mcp/src/queue/db.js +0 -26
- package/dist/packages/mcp/src/tools/executeSearchQuery.js +0 -914
- package/dist/packages/mcp/src/tools/findRemsByReference.js +0 -447
- package/dist/packages/mcp/src/tools/getRemConnections.js +0 -566
- package/dist/packages/mcp/src/tools/inspectRemDoc.js +0 -60
- package/dist/packages/mcp/src/tools/listRemBackups.js +0 -35
- package/dist/packages/mcp/src/tools/listRemReferences.js +0 -421
- package/dist/packages/mcp/src/tools/listSupportedOps.js +0 -41
- package/dist/packages/mcp/src/tools/listTodos.js +0 -815
- package/dist/packages/mcp/src/tools/outlineRemSubtree.js +0 -203
- package/dist/packages/mcp/src/tools/readRemTable.js +0 -252
- package/dist/packages/mcp/src/tools/resolveRemReference.js +0 -174
- package/dist/packages/mcp/src/tools/searchQueryTypes.js +0 -127
- package/dist/packages/mcp/src/tools/searchRemOverview.js +0 -422
- package/dist/packages/mcp/src/tools/searchUtils.js +0 -32
- package/dist/packages/mcp/src/tools/shared.js +0 -393
- package/dist/packages/mcp/src/tools/summarizeDailyNotes.js +0 -221
- package/dist/packages/mcp/src/tools/summarizeTopicActivity.js +0 -605
- package/dist/packages/mcp/src/tools/timeFilters.js +0 -130
- package/dist/packages/mcp/src/ws/bridge.js +0 -377
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { queryNodeSchema, normalizeQueryNode, sortModeSchema, } from "./searchQueryTypes.js";
|
|
3
|
-
import { withResolvedDatabase, buildGuidedResponse, safeJsonParse, parseOrThrow, } from "./shared.js";
|
|
4
|
-
import { coalesceText, createPreview, stringifyAncestor } from "./searchUtils.js";
|
|
5
|
-
export const executeSearchQuerySchema = z.object({
|
|
6
|
-
query: z
|
|
7
|
-
.object({
|
|
8
|
-
root: queryNodeSchema.describe("查询 AST 根节点,支持 text/tag/rem/attribute/page 与 and/or/not 组合"),
|
|
9
|
-
limitHint: z
|
|
10
|
-
.number()
|
|
11
|
-
.int()
|
|
12
|
-
.min(1)
|
|
13
|
-
.max(500)
|
|
14
|
-
.optional()
|
|
15
|
-
.describe("返回上限提示(供执行阶段参考)"),
|
|
16
|
-
pageSizeHint: z
|
|
17
|
-
.number()
|
|
18
|
-
.int()
|
|
19
|
-
.min(5)
|
|
20
|
-
.max(200)
|
|
21
|
-
.optional()
|
|
22
|
-
.describe("分页大小提示(供执行阶段参考)"),
|
|
23
|
-
sort: sortModeSchema.optional().describe("排序策略:rank/updatedAt/createdAt/attribute"),
|
|
24
|
-
description: z.string().optional().describe("查询的人类可读说明"),
|
|
25
|
-
})
|
|
26
|
-
.describe("由 build_search_query 生成或手动构造的查询对象"),
|
|
27
|
-
dbPath: z
|
|
28
|
-
.string()
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("数据库文件路径(默认自动发现最新 remnote.db)"),
|
|
31
|
-
limit: z
|
|
32
|
-
.number()
|
|
33
|
-
.int()
|
|
34
|
-
.min(1)
|
|
35
|
-
.max(100)
|
|
36
|
-
.optional()
|
|
37
|
-
.describe("返回条数上限(分页)"),
|
|
38
|
-
offset: z
|
|
39
|
-
.number()
|
|
40
|
-
.int()
|
|
41
|
-
.min(0)
|
|
42
|
-
.optional()
|
|
43
|
-
.describe("分页偏移量"),
|
|
44
|
-
maxLeafResults: z
|
|
45
|
-
.number()
|
|
46
|
-
.int()
|
|
47
|
-
.min(50)
|
|
48
|
-
.max(5000)
|
|
49
|
-
.optional()
|
|
50
|
-
.describe("单个叶子条件的候选上限(避免集合过大)"),
|
|
51
|
-
snippetLength: z
|
|
52
|
-
.number()
|
|
53
|
-
.int()
|
|
54
|
-
.min(30)
|
|
55
|
-
.max(400)
|
|
56
|
-
.optional()
|
|
57
|
-
.describe("结果摘要长度(字符数)"),
|
|
58
|
-
});
|
|
59
|
-
const TEXT_WEIGHT = 8;
|
|
60
|
-
const TAG_WEIGHT = 5;
|
|
61
|
-
const REM_WEIGHT = 12;
|
|
62
|
-
const ATTRIBUTE_WEIGHT = 7;
|
|
63
|
-
export async function executeSearchQuery(input) {
|
|
64
|
-
const parsed = parseOrThrow(executeSearchQuerySchema, input, { label: "execute_search_query" });
|
|
65
|
-
const query = {
|
|
66
|
-
...parsed.query,
|
|
67
|
-
root: normalizeQueryNode(parsed.query.root),
|
|
68
|
-
};
|
|
69
|
-
const limit = parsed.limit ?? query.pageSizeHint ?? query.limitHint ?? 20;
|
|
70
|
-
const offset = parsed.offset ?? 0;
|
|
71
|
-
const snippetLength = parsed.snippetLength ?? 200;
|
|
72
|
-
const maxLeaf = parsed.maxLeafResults ?? 800;
|
|
73
|
-
const { result, info } = await withResolvedDatabase(parsed.dbPath, async (db) => {
|
|
74
|
-
return executeQueryInternal({
|
|
75
|
-
db,
|
|
76
|
-
query,
|
|
77
|
-
limit,
|
|
78
|
-
offset,
|
|
79
|
-
snippetLength,
|
|
80
|
-
maxLeafResults: maxLeaf,
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
const suggestions = [];
|
|
84
|
-
if (result.items.length > 0) {
|
|
85
|
-
suggestions.push("查看全文:outline_rem_subtree 或 inspect_rem_doc");
|
|
86
|
-
if (result.hasMore && result.nextOffset != null) {
|
|
87
|
-
suggestions.push(`还有更多结果,可再次调用 execute_search_query 并设置 offset=${result.nextOffset}`);
|
|
88
|
-
}
|
|
89
|
-
suggestions.push("如需调整条件,可修改 build_search_query 的 root 后重新执行");
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
suggestions.push("未命中,可尝试放宽关键词或删减属性过滤条件");
|
|
93
|
-
}
|
|
94
|
-
const guidance = result.items.length > 0
|
|
95
|
-
? `共命中 ${result.totalMatched} 条,当前返回 ${result.items.length} 条(offset=${offset},limit=${limit})。`
|
|
96
|
-
: "未找到匹配 Rem,可调整查询条件后重试。";
|
|
97
|
-
const payload = {
|
|
98
|
-
guidance,
|
|
99
|
-
queryUsed: query,
|
|
100
|
-
dbPath: info.dbPath,
|
|
101
|
-
resolution: info.source,
|
|
102
|
-
dirName: info.dirName,
|
|
103
|
-
limit,
|
|
104
|
-
offset,
|
|
105
|
-
hasMore: result.hasMore,
|
|
106
|
-
nextOffset: result.nextOffset,
|
|
107
|
-
totalCandidates: result.totalCandidates,
|
|
108
|
-
totalMatched: result.totalMatched,
|
|
109
|
-
items: result.items,
|
|
110
|
-
};
|
|
111
|
-
return { payload, suggestions };
|
|
112
|
-
}
|
|
113
|
-
export function registerExecuteSearchQuery(server) {
|
|
114
|
-
server.tool("execute_search_query", `<usecase>执行 build_search_query 生成的查询 AST,返回匹配 Rem 列表。</usecase>
|
|
115
|
-
<instructions>
|
|
116
|
-
- 传入 query.root(可由 build_search_query 生成或手动构造),可配合 limit/offset 实现分页。
|
|
117
|
-
- 默认每页返回 20 项,maxLeafResults 控制单个条件的候选上限(避免查询过大)。
|
|
118
|
-
- snippetLength 控制结果摘要长度;若需阅读全文,请调用 outline_rem_subtree / inspect_rem_doc。
|
|
119
|
-
- 响应会给出 guidance 与 next 建议,指引后续分页或细化条件。
|
|
120
|
-
</instructions>`, executeSearchQuerySchema.shape, async (input) => {
|
|
121
|
-
const { payload, suggestions } = await executeSearchQuery(input);
|
|
122
|
-
return buildGuidedResponse(payload, suggestions);
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
async function executeQueryInternal(params) {
|
|
126
|
-
const { db, query, limit, offset, snippetLength, maxLeafResults } = params;
|
|
127
|
-
const leaves = collectLeaves(query.root);
|
|
128
|
-
const leafEvaluations = leaves.map((leaf) => ({
|
|
129
|
-
node: leaf,
|
|
130
|
-
result: evaluateLeaf(db, leaf, maxLeafResults),
|
|
131
|
-
}));
|
|
132
|
-
const universe = buildUniverse(leafEvaluations);
|
|
133
|
-
const rootResult = evaluateNode(query.root, leafEvaluations, universe);
|
|
134
|
-
const matchedIds = Array.from(rootResult.ids);
|
|
135
|
-
const sortMode = query.sort ?? { mode: "rank" };
|
|
136
|
-
const metadata = fetchMetadata(db, matchedIds, snippetLength);
|
|
137
|
-
const scoreMap = rootResult.scores;
|
|
138
|
-
if (query.sort?.mode === "attribute") {
|
|
139
|
-
const sortValues = fetchAttributeSortValues(db, matchedIds, query.sort.attributeId);
|
|
140
|
-
for (const [id, value] of sortValues) {
|
|
141
|
-
const meta = metadata.get(id);
|
|
142
|
-
if (meta) {
|
|
143
|
-
meta.sortValue = value ?? undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const sortedIds = sortResults(matchedIds, sortMode, metadata, scoreMap);
|
|
148
|
-
const totalMatched = sortedIds.length;
|
|
149
|
-
const paginated = sortedIds.slice(offset, offset + limit);
|
|
150
|
-
const hasMore = offset + limit < sortedIds.length;
|
|
151
|
-
const nextOffset = hasMore ? offset + limit : null;
|
|
152
|
-
const items = paginated.map((id) => {
|
|
153
|
-
const meta = metadata.get(id);
|
|
154
|
-
if (!meta) {
|
|
155
|
-
return formatResultItem({
|
|
156
|
-
id,
|
|
157
|
-
aliasId: id,
|
|
158
|
-
text: "",
|
|
159
|
-
ancestorText: null,
|
|
160
|
-
ancestorIds: [],
|
|
161
|
-
parentId: null,
|
|
162
|
-
rank: 0,
|
|
163
|
-
freqCounter: 0,
|
|
164
|
-
freqTime: 0,
|
|
165
|
-
updatedAt: null,
|
|
166
|
-
createdAt: null,
|
|
167
|
-
snippetLength,
|
|
168
|
-
score: scoreMap.get(id) ?? 0,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return formatResultItem({
|
|
172
|
-
...meta,
|
|
173
|
-
snippetLength,
|
|
174
|
-
score: scoreMap.get(id) ?? 0,
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
return {
|
|
178
|
-
items,
|
|
179
|
-
totalCandidates: universe.allIds.size,
|
|
180
|
-
totalMatched,
|
|
181
|
-
hasMore,
|
|
182
|
-
nextOffset,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
function collectLeaves(node, acc = []) {
|
|
186
|
-
if (node.type === "and" || node.type === "or") {
|
|
187
|
-
for (const child of node.nodes) {
|
|
188
|
-
collectLeaves(child, acc);
|
|
189
|
-
}
|
|
190
|
-
return acc;
|
|
191
|
-
}
|
|
192
|
-
if (node.type === "not") {
|
|
193
|
-
collectLeaves(node.node, acc);
|
|
194
|
-
return acc;
|
|
195
|
-
}
|
|
196
|
-
acc.push(node);
|
|
197
|
-
return acc;
|
|
198
|
-
}
|
|
199
|
-
function evaluateLeaf(db, node, maxLeafResults) {
|
|
200
|
-
switch (node.type) {
|
|
201
|
-
case "text":
|
|
202
|
-
return searchText(db, node.value, node.mode ?? "contains", maxLeafResults);
|
|
203
|
-
case "tag":
|
|
204
|
-
return searchTag(db, node.id, maxLeafResults);
|
|
205
|
-
case "rem":
|
|
206
|
-
return searchSpecificRem(node.id);
|
|
207
|
-
case "page":
|
|
208
|
-
return searchPages(db, maxLeafResults);
|
|
209
|
-
case "attribute":
|
|
210
|
-
return searchAttribute(db, node, maxLeafResults);
|
|
211
|
-
default:
|
|
212
|
-
return { ids: new Set(), scores: new Map() };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function buildUniverse(evaluations) {
|
|
216
|
-
const allIds = new Set();
|
|
217
|
-
const baseScores = new Map();
|
|
218
|
-
for (const { result } of evaluations) {
|
|
219
|
-
for (const id of result.ids) {
|
|
220
|
-
allIds.add(id);
|
|
221
|
-
baseScores.set(id, (baseScores.get(id) ?? 0) + (result.scores.get(id) ?? 0));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return { allIds, baseScores };
|
|
225
|
-
}
|
|
226
|
-
function evaluateNode(node, evaluations, universe) {
|
|
227
|
-
if (node.type === "and") {
|
|
228
|
-
const children = node.nodes.map((child) => evaluateNode(child, evaluations, universe));
|
|
229
|
-
if (children.length === 0) {
|
|
230
|
-
return { ids: new Set(), scores: new Map() };
|
|
231
|
-
}
|
|
232
|
-
let ids = new Set(children[0].ids);
|
|
233
|
-
for (let i = 1; i < children.length; i++) {
|
|
234
|
-
ids = intersectSets(ids, children[i].ids);
|
|
235
|
-
if (ids.size === 0)
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
const scores = new Map();
|
|
239
|
-
for (const id of ids) {
|
|
240
|
-
let score = 0;
|
|
241
|
-
for (const child of children) {
|
|
242
|
-
score += child.scores.get(id) ?? 0;
|
|
243
|
-
}
|
|
244
|
-
scores.set(id, score);
|
|
245
|
-
}
|
|
246
|
-
return { ids, scores };
|
|
247
|
-
}
|
|
248
|
-
if (node.type === "or") {
|
|
249
|
-
const children = node.nodes.map((child) => evaluateNode(child, evaluations, universe));
|
|
250
|
-
const ids = new Set();
|
|
251
|
-
const scores = new Map();
|
|
252
|
-
for (const child of children) {
|
|
253
|
-
for (const id of child.ids) {
|
|
254
|
-
ids.add(id);
|
|
255
|
-
const existing = scores.get(id) ?? 0;
|
|
256
|
-
const candidate = child.scores.get(id) ?? 0;
|
|
257
|
-
scores.set(id, Math.max(existing, candidate));
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return { ids, scores };
|
|
261
|
-
}
|
|
262
|
-
if (node.type === "not") {
|
|
263
|
-
const child = evaluateNode(node.node, evaluations, universe);
|
|
264
|
-
const ids = new Set();
|
|
265
|
-
const scores = new Map();
|
|
266
|
-
for (const id of universe.allIds) {
|
|
267
|
-
if (!child.ids.has(id)) {
|
|
268
|
-
ids.add(id);
|
|
269
|
-
scores.set(id, universe.baseScores.get(id) ?? 1);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return { ids, scores };
|
|
273
|
-
}
|
|
274
|
-
// leaf node
|
|
275
|
-
const evaluation = evaluations.find((entry) => entry.node === node);
|
|
276
|
-
if (!evaluation) {
|
|
277
|
-
return { ids: new Set(), scores: new Map() };
|
|
278
|
-
}
|
|
279
|
-
return evaluation.result;
|
|
280
|
-
}
|
|
281
|
-
function intersectSets(a, b) {
|
|
282
|
-
const result = new Set();
|
|
283
|
-
for (const id of a) {
|
|
284
|
-
if (b.has(id))
|
|
285
|
-
result.add(id);
|
|
286
|
-
}
|
|
287
|
-
return result;
|
|
288
|
-
}
|
|
289
|
-
function searchText(db, value, mode, max) {
|
|
290
|
-
const normalized = value.trim();
|
|
291
|
-
if (!normalized) {
|
|
292
|
-
return { ids: new Set(), scores: new Map() };
|
|
293
|
-
}
|
|
294
|
-
const ids = new Set();
|
|
295
|
-
const scores = new Map();
|
|
296
|
-
const tryFts = (query) => {
|
|
297
|
-
try {
|
|
298
|
-
const stmt = db.prepare(`SELECT aliasId, id, COALESCE((SELECT rank FROM remsSearchRanks WHERE ftsRowId = remsSearchInfos.ftsRowId), 0) AS rank
|
|
299
|
-
FROM remsSearchInfos
|
|
300
|
-
WHERE ftsRowId IN (SELECT rowid FROM remsContents WHERE remsContents MATCH @query)
|
|
301
|
-
LIMIT @limit`);
|
|
302
|
-
const rows = stmt.all({ query, limit: max });
|
|
303
|
-
for (const row of rows) {
|
|
304
|
-
ids.add(row.id);
|
|
305
|
-
scores.set(row.id, (scores.get(row.id) ?? 0) + TEXT_WEIGHT + row.rank);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
const message = String(error ?? "");
|
|
310
|
-
if (!/malformed MATCH/i.test(message) && !/no such tokenizer/i.test(message)) {
|
|
311
|
-
throw error;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
switch (mode) {
|
|
316
|
-
case "phrase":
|
|
317
|
-
tryFts(`"${normalized.replace(/"/g, '""')}"`);
|
|
318
|
-
if (ids.size === 0) {
|
|
319
|
-
runLikeSearch(db, `%${normalized.toLowerCase().replace(/\s+/g, "%")}%`, max, ids, scores);
|
|
320
|
-
}
|
|
321
|
-
break;
|
|
322
|
-
case "prefix":
|
|
323
|
-
tryFts(`${normalized.replace(/"/g, ' ')}*`);
|
|
324
|
-
if (ids.size === 0) {
|
|
325
|
-
runLikeSearch(db, `${normalized.toLowerCase().replace(/\s+/g, "%")}%`, max, ids, scores);
|
|
326
|
-
}
|
|
327
|
-
break;
|
|
328
|
-
case "suffix":
|
|
329
|
-
runLikeSearch(db, `%${normalized.toLowerCase()}`, max, ids, scores);
|
|
330
|
-
break;
|
|
331
|
-
case "contains":
|
|
332
|
-
default:
|
|
333
|
-
tryFts(normalized);
|
|
334
|
-
if (ids.size === 0) {
|
|
335
|
-
runLikeSearch(db, `%${normalized.toLowerCase().replace(/\s+/g, "%")}%`, max, ids, scores);
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
return { ids, scores };
|
|
340
|
-
}
|
|
341
|
-
function searchPages(db, max) {
|
|
342
|
-
const ids = new Set();
|
|
343
|
-
const scores = new Map();
|
|
344
|
-
const stmt = db.prepare(`SELECT id, COALESCE((SELECT rank FROM remsSearchRanks WHERE ftsRowId = remsSearchInfos.ftsRowId), 0) AS rank
|
|
345
|
-
FROM remsSearchInfos
|
|
346
|
-
WHERE CAST(json_extract(doc,'$.rd') AS INTEGER) = 1
|
|
347
|
-
LIMIT @limit`);
|
|
348
|
-
const rows = stmt.all({ limit: max });
|
|
349
|
-
for (const row of rows) {
|
|
350
|
-
ids.add(row.id);
|
|
351
|
-
scores.set(row.id, (scores.get(row.id) ?? 0) + REM_WEIGHT + row.rank);
|
|
352
|
-
}
|
|
353
|
-
return { ids, scores };
|
|
354
|
-
}
|
|
355
|
-
function runLikeSearch(db, pattern, max, ids, scores) {
|
|
356
|
-
const compactSource = pattern.replace(/%/g, "").replace(/\s+/g, "");
|
|
357
|
-
const patternCompact = compactSource ? `%${compactSource}%` : pattern;
|
|
358
|
-
const stmt = db.prepare(`SELECT aliasId, id, 0 AS rank
|
|
359
|
-
FROM remsSearchInfos
|
|
360
|
-
WHERE lower(json_extract(doc, '$.kt')) LIKE @pattern
|
|
361
|
-
OR lower(json_extract(doc, '$.ke')) LIKE @pattern
|
|
362
|
-
OR REPLACE(lower(json_extract(doc, '$.kt')), ' ', '') LIKE @patternCompact
|
|
363
|
-
OR REPLACE(lower(json_extract(doc, '$.ke')), ' ', '') LIKE @patternCompact
|
|
364
|
-
LIMIT @limit`);
|
|
365
|
-
const rows = stmt.all({ pattern, patternCompact, limit: max });
|
|
366
|
-
for (const row of rows) {
|
|
367
|
-
ids.add(row.id);
|
|
368
|
-
scores.set(row.id, (scores.get(row.id) ?? 0) + TEXT_WEIGHT);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
function searchTag(db, tagId, max) {
|
|
372
|
-
const stmt = db.prepare(`SELECT _id
|
|
373
|
-
FROM quanta
|
|
374
|
-
WHERE json_extract(doc, '$.tp."${tagId}".t') = 1
|
|
375
|
-
LIMIT @limit`);
|
|
376
|
-
const rows = stmt.all({ limit: max });
|
|
377
|
-
const ids = new Set();
|
|
378
|
-
const scores = new Map();
|
|
379
|
-
for (const row of rows) {
|
|
380
|
-
ids.add(row._id);
|
|
381
|
-
scores.set(row._id, (scores.get(row._id) ?? 0) + TAG_WEIGHT);
|
|
382
|
-
}
|
|
383
|
-
return { ids, scores };
|
|
384
|
-
}
|
|
385
|
-
function searchSpecificRem(remId) {
|
|
386
|
-
const ids = new Set([remId]);
|
|
387
|
-
const scores = new Map([[remId, REM_WEIGHT]]);
|
|
388
|
-
return { ids, scores };
|
|
389
|
-
}
|
|
390
|
-
function searchAttribute(db, condition, max) {
|
|
391
|
-
const stmt = db.prepare(`SELECT json_extract(doc, '$.parent') AS parentId, doc
|
|
392
|
-
FROM quanta
|
|
393
|
-
WHERE json_extract(doc, '$.key[0]._id') = @attributeId
|
|
394
|
-
LIMIT @limit`);
|
|
395
|
-
const rows = stmt.all({ attributeId: condition.attributeId, limit: max });
|
|
396
|
-
const ids = new Set();
|
|
397
|
-
const scores = new Map();
|
|
398
|
-
const dateCache = new Map();
|
|
399
|
-
for (const row of rows) {
|
|
400
|
-
if (!row.parentId)
|
|
401
|
-
continue;
|
|
402
|
-
const parsed = safeJsonParse(row.doc);
|
|
403
|
-
if (!parsed)
|
|
404
|
-
continue;
|
|
405
|
-
const match = evaluateAttributeCondition(db, parsed, condition, dateCache);
|
|
406
|
-
if (!match)
|
|
407
|
-
continue;
|
|
408
|
-
ids.add(row.parentId);
|
|
409
|
-
scores.set(row.parentId, (scores.get(row.parentId) ?? 0) + ATTRIBUTE_WEIGHT);
|
|
410
|
-
}
|
|
411
|
-
return { ids, scores };
|
|
412
|
-
}
|
|
413
|
-
function evaluateAttributeCondition(db, doc, condition, dateCache) {
|
|
414
|
-
const value = doc.value;
|
|
415
|
-
const type = doc.type;
|
|
416
|
-
const tokens = Array.isArray(value) ? value : value != null ? [value] : [];
|
|
417
|
-
const strings = [];
|
|
418
|
-
const refs = [];
|
|
419
|
-
for (const token of tokens) {
|
|
420
|
-
if (typeof token === "string") {
|
|
421
|
-
strings.push(token);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (token && typeof token === "object") {
|
|
425
|
-
const obj = token;
|
|
426
|
-
if (obj.i === "q" && typeof obj._id === "string") {
|
|
427
|
-
refs.push(obj._id);
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
if (typeof obj.text === "string") {
|
|
431
|
-
strings.push(obj.text);
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
if (typeof obj.title === "string") {
|
|
435
|
-
strings.push(obj.title);
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
const isEmpty = tokens.length === 0;
|
|
441
|
-
switch (condition.operator) {
|
|
442
|
-
case "equals":
|
|
443
|
-
if (condition.value === undefined)
|
|
444
|
-
return false;
|
|
445
|
-
const equalsValue = normalizeConditionValue(condition.value);
|
|
446
|
-
return strings.includes(equalsValue) || refs.includes(String(condition.value));
|
|
447
|
-
case "notEquals":
|
|
448
|
-
if (condition.value === undefined)
|
|
449
|
-
return false;
|
|
450
|
-
const notEqualsValue = normalizeConditionValue(condition.value);
|
|
451
|
-
return !strings.includes(notEqualsValue) && !refs.includes(String(condition.value));
|
|
452
|
-
case "contains":
|
|
453
|
-
if (!condition.value && !condition.values)
|
|
454
|
-
return false;
|
|
455
|
-
if (condition.values) {
|
|
456
|
-
return condition.values.every((val) => strings.includes(String(val)) || refs.includes(String(val)));
|
|
457
|
-
}
|
|
458
|
-
if (condition.value === undefined)
|
|
459
|
-
return false;
|
|
460
|
-
const containsValue = normalizeConditionValue(condition.value).toLowerCase();
|
|
461
|
-
return strings.some((str) => str.toLowerCase().includes(containsValue));
|
|
462
|
-
case "notContains":
|
|
463
|
-
if (!condition.value && !condition.values)
|
|
464
|
-
return true;
|
|
465
|
-
if (condition.values) {
|
|
466
|
-
return condition.values.every((val) => !strings.includes(String(val)) && !refs.includes(String(val)));
|
|
467
|
-
}
|
|
468
|
-
if (condition.value === undefined)
|
|
469
|
-
return true;
|
|
470
|
-
const notContainsValue = normalizeConditionValue(condition.value).toLowerCase();
|
|
471
|
-
return !strings.some((str) => str.toLowerCase().includes(notContainsValue));
|
|
472
|
-
case "greaterThan":
|
|
473
|
-
case "greaterThanOrEquals":
|
|
474
|
-
case "lessThan":
|
|
475
|
-
case "lessThanOrEquals":
|
|
476
|
-
case "between":
|
|
477
|
-
case "before":
|
|
478
|
-
case "after":
|
|
479
|
-
case "on":
|
|
480
|
-
case "relative": {
|
|
481
|
-
const numeric = strings
|
|
482
|
-
.map((entry) => Number(entry))
|
|
483
|
-
.filter((entry) => Number.isFinite(entry));
|
|
484
|
-
if (numeric.length > 0) {
|
|
485
|
-
return evaluateNumericCondition(numeric, condition);
|
|
486
|
-
}
|
|
487
|
-
const dateValues = [];
|
|
488
|
-
for (const ref of refs) {
|
|
489
|
-
const ts = resolveDateReference(db, ref, dateCache);
|
|
490
|
-
if (ts != null)
|
|
491
|
-
dateValues.push(ts);
|
|
492
|
-
}
|
|
493
|
-
if (dateValues.length > 0) {
|
|
494
|
-
return evaluateDateCondition(dateValues, condition);
|
|
495
|
-
}
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
498
|
-
case "empty":
|
|
499
|
-
return isEmpty;
|
|
500
|
-
case "notEmpty":
|
|
501
|
-
return !isEmpty;
|
|
502
|
-
default:
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function evaluateNumericCondition(numericValues, condition) {
|
|
507
|
-
const target = condition.value != null && typeof condition.value !== "boolean"
|
|
508
|
-
? Number(condition.value)
|
|
509
|
-
: undefined;
|
|
510
|
-
const range = condition.range;
|
|
511
|
-
switch (condition.operator) {
|
|
512
|
-
case "greaterThan":
|
|
513
|
-
if (target == null)
|
|
514
|
-
return false;
|
|
515
|
-
return numericValues.some((value) => value > target);
|
|
516
|
-
case "greaterThanOrEquals":
|
|
517
|
-
if (target == null)
|
|
518
|
-
return false;
|
|
519
|
-
return numericValues.some((value) => value >= target);
|
|
520
|
-
case "lessThan":
|
|
521
|
-
if (target == null)
|
|
522
|
-
return false;
|
|
523
|
-
return numericValues.some((value) => value < target);
|
|
524
|
-
case "lessThanOrEquals":
|
|
525
|
-
if (target == null)
|
|
526
|
-
return false;
|
|
527
|
-
return numericValues.some((value) => value <= target);
|
|
528
|
-
case "between":
|
|
529
|
-
if (!range)
|
|
530
|
-
return false;
|
|
531
|
-
const start = range.start != null ? Number(range.start) : -Infinity;
|
|
532
|
-
const end = range.end != null ? Number(range.end) : Infinity;
|
|
533
|
-
return numericValues.some((value) => value >= start && value <= end);
|
|
534
|
-
case "after":
|
|
535
|
-
if (target == null)
|
|
536
|
-
return false;
|
|
537
|
-
return numericValues.some((value) => value > target);
|
|
538
|
-
case "before":
|
|
539
|
-
if (target == null)
|
|
540
|
-
return false;
|
|
541
|
-
return numericValues.some((value) => value < target);
|
|
542
|
-
case "on":
|
|
543
|
-
if (target == null)
|
|
544
|
-
return false;
|
|
545
|
-
return numericValues.some((value) => value === target);
|
|
546
|
-
case "relative":
|
|
547
|
-
if (condition.relativeAmount == null)
|
|
548
|
-
return false;
|
|
549
|
-
const base = Date.now();
|
|
550
|
-
const unit = condition.unit ?? "day";
|
|
551
|
-
const delta = computeRelativeOffsetMs(condition.relativeAmount, unit);
|
|
552
|
-
const boundary = base + delta;
|
|
553
|
-
if (condition.relativeAmount >= 0) {
|
|
554
|
-
return numericValues.some((value) => value <= boundary);
|
|
555
|
-
}
|
|
556
|
-
return numericValues.some((value) => value >= boundary);
|
|
557
|
-
default:
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
function evaluateDateCondition(dateValues, condition) {
|
|
562
|
-
const target = condition.value != null &&
|
|
563
|
-
(typeof condition.value === "string" || typeof condition.value === "number")
|
|
564
|
-
? normalizeDateInput(condition.value)
|
|
565
|
-
: undefined;
|
|
566
|
-
const range = condition.range;
|
|
567
|
-
const now = Date.now();
|
|
568
|
-
switch (condition.operator) {
|
|
569
|
-
case "before":
|
|
570
|
-
if (target == null)
|
|
571
|
-
return false;
|
|
572
|
-
return dateValues.some((value) => value < target);
|
|
573
|
-
case "after":
|
|
574
|
-
if (target == null)
|
|
575
|
-
return false;
|
|
576
|
-
return dateValues.some((value) => value > target);
|
|
577
|
-
case "on":
|
|
578
|
-
if (target == null)
|
|
579
|
-
return false;
|
|
580
|
-
return dateValues.some((value) => isSameDay(value, target));
|
|
581
|
-
case "between":
|
|
582
|
-
if (!range)
|
|
583
|
-
return false;
|
|
584
|
-
const start = range.start != null ? normalizeDateInput(range.start) ?? -Infinity : -Infinity;
|
|
585
|
-
const end = range.end != null ? normalizeDateInput(range.end) ?? Infinity : Infinity;
|
|
586
|
-
return dateValues.some((value) => value >= start && value <= end);
|
|
587
|
-
case "relative":
|
|
588
|
-
if (condition.relativeAmount == null)
|
|
589
|
-
return false;
|
|
590
|
-
const unit = condition.unit ?? "day";
|
|
591
|
-
const delta = computeRelativeOffsetMs(condition.relativeAmount, unit);
|
|
592
|
-
const boundary = now + delta;
|
|
593
|
-
if (condition.relativeAmount >= 0) {
|
|
594
|
-
return dateValues.some((value) => value <= boundary);
|
|
595
|
-
}
|
|
596
|
-
return dateValues.some((value) => value >= boundary);
|
|
597
|
-
default:
|
|
598
|
-
// 其他比较交给数值逻辑
|
|
599
|
-
return evaluateNumericCondition(dateValues, condition);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function computeRelativeOffsetMs(amount, unit) {
|
|
603
|
-
switch (unit) {
|
|
604
|
-
case "minute":
|
|
605
|
-
case "minutes":
|
|
606
|
-
case "m":
|
|
607
|
-
return amount * 60 * 1000;
|
|
608
|
-
case "hour":
|
|
609
|
-
case "hours":
|
|
610
|
-
case "h":
|
|
611
|
-
return amount * 60 * 60 * 1000;
|
|
612
|
-
case "day":
|
|
613
|
-
case "days":
|
|
614
|
-
case "d":
|
|
615
|
-
return amount * 24 * 60 * 60 * 1000;
|
|
616
|
-
case "week":
|
|
617
|
-
case "weeks":
|
|
618
|
-
case "w":
|
|
619
|
-
return amount * 7 * 24 * 60 * 60 * 1000;
|
|
620
|
-
case "month":
|
|
621
|
-
case "months":
|
|
622
|
-
case "M":
|
|
623
|
-
return amount * 30 * 24 * 60 * 60 * 1000;
|
|
624
|
-
case "year":
|
|
625
|
-
case "years":
|
|
626
|
-
case "y":
|
|
627
|
-
return amount * 365 * 24 * 60 * 60 * 1000;
|
|
628
|
-
default:
|
|
629
|
-
return amount * 24 * 60 * 60 * 1000;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
function normalizeDateInput(value) {
|
|
633
|
-
if (typeof value === "number") {
|
|
634
|
-
return value > 10_000 ? value : value * 1000;
|
|
635
|
-
}
|
|
636
|
-
const trimmed = value.trim();
|
|
637
|
-
if (/^\d{10}$/.test(trimmed)) {
|
|
638
|
-
return Number(trimmed) * 1000;
|
|
639
|
-
}
|
|
640
|
-
if (/^\d{13}$/.test(trimmed)) {
|
|
641
|
-
return Number(trimmed);
|
|
642
|
-
}
|
|
643
|
-
const parsed = Date.parse(trimmed);
|
|
644
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
645
|
-
}
|
|
646
|
-
function resolveDateReference(db, remId, cache) {
|
|
647
|
-
if (cache.has(remId)) {
|
|
648
|
-
return cache.get(remId) ?? null;
|
|
649
|
-
}
|
|
650
|
-
const row = db
|
|
651
|
-
.prepare("SELECT doc FROM quanta WHERE _id = ?")
|
|
652
|
-
.get(remId);
|
|
653
|
-
if (!row?.doc) {
|
|
654
|
-
cache.set(remId, null);
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
const data = safeJsonParse(row.doc);
|
|
658
|
-
let result = null;
|
|
659
|
-
const crt = data?.crt;
|
|
660
|
-
const d = crt?.d;
|
|
661
|
-
const seconds = (() => {
|
|
662
|
-
const s = d?.s?.v;
|
|
663
|
-
if (Array.isArray(s)) {
|
|
664
|
-
const candidate = s[0];
|
|
665
|
-
if (typeof candidate === "string" && candidate.trim()) {
|
|
666
|
-
const num = Number(candidate);
|
|
667
|
-
if (Number.isFinite(num))
|
|
668
|
-
return num;
|
|
669
|
-
}
|
|
670
|
-
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
671
|
-
return candidate;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return undefined;
|
|
675
|
-
})();
|
|
676
|
-
if (seconds && Number.isFinite(seconds)) {
|
|
677
|
-
result = seconds * 1000;
|
|
678
|
-
}
|
|
679
|
-
if (!result) {
|
|
680
|
-
const dArray = d?.d?.v;
|
|
681
|
-
if (Array.isArray(dArray) && dArray[0] != null) {
|
|
682
|
-
const iso = String(dArray[0]);
|
|
683
|
-
const parsed = Date.parse(iso);
|
|
684
|
-
if (Number.isFinite(parsed)) {
|
|
685
|
-
result = parsed;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
if (!result && Array.isArray(data?.key)) {
|
|
690
|
-
const keyCandidate = data.key[0];
|
|
691
|
-
if (typeof keyCandidate === "string") {
|
|
692
|
-
const fromKey = Date.parse(keyCandidate);
|
|
693
|
-
if (Number.isFinite(fromKey)) {
|
|
694
|
-
result = fromKey;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
cache.set(remId, result);
|
|
699
|
-
return result;
|
|
700
|
-
}
|
|
701
|
-
function isSameDay(a, b) {
|
|
702
|
-
const dayA = new Date(a);
|
|
703
|
-
const dayB = new Date(b);
|
|
704
|
-
return (dayA.getFullYear() === dayB.getFullYear() &&
|
|
705
|
-
dayA.getMonth() === dayB.getMonth() &&
|
|
706
|
-
dayA.getDate() === dayB.getDate());
|
|
707
|
-
}
|
|
708
|
-
function fetchMetadata(db, ids, snippetLength) {
|
|
709
|
-
if (ids.length === 0)
|
|
710
|
-
return new Map();
|
|
711
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
712
|
-
const stmt = db.prepare(`SELECT aliasId, id, doc, ancestor_not_ref_text AS ancestorNotRefText, ancestor_ids AS ancestorIds,
|
|
713
|
-
freqCounter, freqTime
|
|
714
|
-
FROM remsSearchInfos
|
|
715
|
-
WHERE id IN (${placeholders})`);
|
|
716
|
-
const rows = stmt.all(...ids);
|
|
717
|
-
const quantaStmt = db.prepare(`SELECT _id, doc
|
|
718
|
-
FROM quanta
|
|
719
|
-
WHERE _id IN (${placeholders})`);
|
|
720
|
-
const quantaRows = quantaStmt.all(...ids);
|
|
721
|
-
const quantaMap = new Map();
|
|
722
|
-
for (const row of quantaRows) {
|
|
723
|
-
const doc = safeJsonParse(row.doc);
|
|
724
|
-
if (doc)
|
|
725
|
-
quantaMap.set(row._id, doc);
|
|
726
|
-
}
|
|
727
|
-
const result = new Map();
|
|
728
|
-
for (const row of rows) {
|
|
729
|
-
const doc = safeJsonParse(row.doc) ?? {};
|
|
730
|
-
const text = coalesceText(doc.kt, doc.ke);
|
|
731
|
-
const ancestor = stringifyAncestor(row.ancestorNotRefText, row.ancestorIds);
|
|
732
|
-
const remDoc = quantaMap.get(row.id);
|
|
733
|
-
const updatedAt = typeof remDoc?.u === "number" ? remDoc.u : null;
|
|
734
|
-
const createdAt = typeof remDoc?.createdAt === "number" ? remDoc.createdAt : null;
|
|
735
|
-
const parentId = typeof remDoc?.parent === "string" ? remDoc.parent : null;
|
|
736
|
-
result.set(row.id, {
|
|
737
|
-
id: row.id,
|
|
738
|
-
aliasId: row.aliasId,
|
|
739
|
-
text,
|
|
740
|
-
ancestorText: ancestor.text || null,
|
|
741
|
-
ancestorIds: ancestor.ids,
|
|
742
|
-
parentId,
|
|
743
|
-
rank: typeof doc.x === "number" ? doc.x : 0,
|
|
744
|
-
freqCounter: row.freqCounter,
|
|
745
|
-
freqTime: row.freqTime,
|
|
746
|
-
updatedAt,
|
|
747
|
-
createdAt,
|
|
748
|
-
sortValue: undefined,
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
return result;
|
|
752
|
-
}
|
|
753
|
-
function sortResults(ids, sortMode, metadata, scoreMap) {
|
|
754
|
-
const candidates = ids.map((id) => ({ id, meta: metadata.get(id) }));
|
|
755
|
-
switch (sortMode.mode) {
|
|
756
|
-
case "updatedAt":
|
|
757
|
-
return candidates
|
|
758
|
-
.sort((a, b) => {
|
|
759
|
-
const av = a.meta?.updatedAt ?? 0;
|
|
760
|
-
const bv = b.meta?.updatedAt ?? 0;
|
|
761
|
-
const dir = sortMode.direction === "asc" ? 1 : -1;
|
|
762
|
-
if (av !== bv)
|
|
763
|
-
return dir * (av - bv);
|
|
764
|
-
const ascore = scoreMap.get(a.id) ?? 0;
|
|
765
|
-
const bscore = scoreMap.get(b.id) ?? 0;
|
|
766
|
-
return bscore - ascore;
|
|
767
|
-
})
|
|
768
|
-
.map((entry) => entry.id);
|
|
769
|
-
case "createdAt":
|
|
770
|
-
return candidates
|
|
771
|
-
.sort((a, b) => {
|
|
772
|
-
const av = a.meta?.createdAt ?? 0;
|
|
773
|
-
const bv = b.meta?.createdAt ?? 0;
|
|
774
|
-
const dir = sortMode.direction === "asc" ? 1 : -1;
|
|
775
|
-
if (av !== bv)
|
|
776
|
-
return dir * (av - bv);
|
|
777
|
-
const ascore = scoreMap.get(a.id) ?? 0;
|
|
778
|
-
const bscore = scoreMap.get(b.id) ?? 0;
|
|
779
|
-
return bscore - ascore;
|
|
780
|
-
})
|
|
781
|
-
.map((entry) => entry.id);
|
|
782
|
-
case "attribute":
|
|
783
|
-
return candidates
|
|
784
|
-
.sort((a, b) => {
|
|
785
|
-
const av = a.meta?.sortValue;
|
|
786
|
-
const bv = b.meta?.sortValue;
|
|
787
|
-
const dir = sortMode.direction === "asc" ? 1 : -1;
|
|
788
|
-
if (typeof av === "number" && typeof bv === "number") {
|
|
789
|
-
if (av !== bv)
|
|
790
|
-
return dir * (av - bv);
|
|
791
|
-
}
|
|
792
|
-
else if (typeof av === "string" && typeof bv === "string") {
|
|
793
|
-
const cmp = av.localeCompare(bv);
|
|
794
|
-
if (cmp !== 0)
|
|
795
|
-
return dir * cmp;
|
|
796
|
-
}
|
|
797
|
-
else if (av != null && bv == null) {
|
|
798
|
-
return -1;
|
|
799
|
-
}
|
|
800
|
-
else if (av == null && bv != null) {
|
|
801
|
-
return 1;
|
|
802
|
-
}
|
|
803
|
-
const ascore = scoreMap.get(a.id) ?? 0;
|
|
804
|
-
const bscore = scoreMap.get(b.id) ?? 0;
|
|
805
|
-
return bscore - ascore;
|
|
806
|
-
})
|
|
807
|
-
.map((entry) => entry.id);
|
|
808
|
-
case "rank":
|
|
809
|
-
default:
|
|
810
|
-
return candidates
|
|
811
|
-
.sort((a, b) => {
|
|
812
|
-
const ascore = scoreMap.get(a.id) ?? 0;
|
|
813
|
-
const bscore = scoreMap.get(b.id) ?? 0;
|
|
814
|
-
if (ascore !== bscore)
|
|
815
|
-
return bscore - ascore;
|
|
816
|
-
const au = a.meta?.updatedAt ?? 0;
|
|
817
|
-
const bu = b.meta?.updatedAt ?? 0;
|
|
818
|
-
return bu - au;
|
|
819
|
-
})
|
|
820
|
-
.map((entry) => entry.id);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
function formatResultItem(params) {
|
|
824
|
-
const { title, snippet, truncated } = createPreview(params.text, params.snippetLength);
|
|
825
|
-
return {
|
|
826
|
-
id: params.id,
|
|
827
|
-
aliasId: params.aliasId,
|
|
828
|
-
title,
|
|
829
|
-
snippet,
|
|
830
|
-
truncated,
|
|
831
|
-
ancestor: params.ancestorText,
|
|
832
|
-
ancestorIds: params.ancestorIds,
|
|
833
|
-
parentId: params.parentId,
|
|
834
|
-
rank: params.rank,
|
|
835
|
-
freqCounter: params.freqCounter,
|
|
836
|
-
freqTime: params.freqTime,
|
|
837
|
-
updatedAt: params.updatedAt,
|
|
838
|
-
createdAt: params.createdAt,
|
|
839
|
-
score: Number(params.score.toFixed(2)),
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
function fetchAttributeSortValues(db, ids, attributeId) {
|
|
843
|
-
if (ids.length === 0)
|
|
844
|
-
return new Map();
|
|
845
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
846
|
-
const stmt = db.prepare(`SELECT json_extract(doc, '$.parent') AS parentId, doc
|
|
847
|
-
FROM quanta
|
|
848
|
-
WHERE json_extract(doc, '$.key[0]._id') = ?
|
|
849
|
-
AND json_extract(doc, '$.parent') IN (${placeholders})`);
|
|
850
|
-
const rows = stmt.all(attributeId, ...ids);
|
|
851
|
-
const cache = new Map();
|
|
852
|
-
const result = new Map();
|
|
853
|
-
for (const row of rows) {
|
|
854
|
-
if (!row.parentId)
|
|
855
|
-
continue;
|
|
856
|
-
const parsed = safeJsonParse(row.doc);
|
|
857
|
-
if (!parsed)
|
|
858
|
-
continue;
|
|
859
|
-
const value = extractSortValue(parsed, db, cache);
|
|
860
|
-
if (value != null) {
|
|
861
|
-
result.set(row.parentId, value);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
return result;
|
|
865
|
-
}
|
|
866
|
-
function extractSortValue(doc, db, cache) {
|
|
867
|
-
const rawValue = doc.value;
|
|
868
|
-
const tokens = Array.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : [];
|
|
869
|
-
const numbers = [];
|
|
870
|
-
const refs = [];
|
|
871
|
-
const strings = [];
|
|
872
|
-
for (const token of tokens) {
|
|
873
|
-
if (typeof token === "string") {
|
|
874
|
-
strings.push(token);
|
|
875
|
-
const num = Number(token);
|
|
876
|
-
if (Number.isFinite(num))
|
|
877
|
-
numbers.push(num);
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
if (token && typeof token === "object") {
|
|
881
|
-
const obj = token;
|
|
882
|
-
if (obj.i === "q" && typeof obj._id === "string") {
|
|
883
|
-
refs.push(obj._id);
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
if (typeof obj.text === "string") {
|
|
887
|
-
strings.push(obj.text);
|
|
888
|
-
const num = Number(obj.text);
|
|
889
|
-
if (Number.isFinite(num))
|
|
890
|
-
numbers.push(num);
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
if (numbers.length > 0) {
|
|
896
|
-
return numbers[0];
|
|
897
|
-
}
|
|
898
|
-
for (const ref of refs) {
|
|
899
|
-
const ts = resolveDateReference(db, ref, cache);
|
|
900
|
-
if (ts != null) {
|
|
901
|
-
return ts;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
if (strings.length > 0) {
|
|
905
|
-
return strings[0].toLowerCase();
|
|
906
|
-
}
|
|
907
|
-
return null;
|
|
908
|
-
}
|
|
909
|
-
function normalizeConditionValue(value) {
|
|
910
|
-
if (typeof value === "boolean") {
|
|
911
|
-
return value ? "Yes" : "No";
|
|
912
|
-
}
|
|
913
|
-
return String(value);
|
|
914
|
-
}
|