agent-remnote 0.0.1 → 0.0.2
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/cli.js +2 -0
- package/dist/apps/cli/src/adapters/mcp.js +1 -0
- package/dist/apps/cli/src/commands/_enqueue.js +138 -0
- package/dist/apps/cli/src/commands/_shared.js +57 -0
- package/dist/apps/cli/src/commands/_tool.js +28 -0
- package/dist/apps/cli/src/commands/apply.js +81 -0
- package/dist/apps/cli/src/commands/config/index.js +3 -0
- package/dist/apps/cli/src/commands/config/print.js +28 -0
- package/dist/apps/cli/src/commands/daily/index.js +4 -0
- package/dist/apps/cli/src/commands/daily/summary.js +25 -0
- package/dist/apps/cli/src/commands/daily/write.js +145 -0
- package/dist/apps/cli/src/commands/db/backups.js +23 -0
- package/dist/apps/cli/src/commands/db/index.js +4 -0
- package/dist/apps/cli/src/commands/db/recent.js +178 -0
- package/dist/apps/cli/src/commands/doctor.js +124 -0
- package/dist/apps/cli/src/commands/index.js +73 -0
- package/dist/apps/cli/src/commands/ops/index.js +4 -0
- package/dist/apps/cli/src/commands/ops/list.js +12 -0
- package/dist/apps/cli/src/commands/ops/schema.js +77 -0
- package/dist/apps/cli/src/commands/queue/enqueue.js +73 -0
- package/dist/apps/cli/src/commands/queue/index.js +5 -0
- package/dist/apps/cli/src/commands/queue/inspect.js +26 -0
- package/dist/apps/cli/src/commands/queue/stats.js +14 -0
- package/dist/apps/cli/src/commands/read/by-reference.js +35 -0
- package/dist/apps/cli/src/commands/read/connections.js +15 -0
- package/dist/apps/cli/src/commands/read/index.js +21 -0
- package/dist/apps/cli/src/commands/read/inspect.js +34 -0
- package/dist/apps/cli/src/commands/read/outline.js +59 -0
- package/dist/apps/cli/src/commands/read/query.js +95 -0
- package/dist/apps/cli/src/commands/read/references.js +41 -0
- package/dist/apps/cli/src/commands/read/resolve-ref.js +32 -0
- package/dist/apps/cli/src/commands/read/search.js +40 -0
- package/dist/apps/cli/src/commands/read/table.js +32 -0
- package/dist/apps/cli/src/commands/todos/index.js +3 -0
- package/dist/apps/cli/src/commands/todos/list.js +33 -0
- package/dist/apps/cli/src/commands/topic/index.js +3 -0
- package/dist/apps/cli/src/commands/topic/summary.js +44 -0
- package/dist/apps/cli/src/commands/wechat/index.js +3 -0
- package/dist/apps/cli/src/commands/wechat/outline.js +430 -0
- package/dist/apps/cli/src/commands/write/bullet.js +76 -0
- package/dist/apps/cli/src/commands/write/index.js +4 -0
- package/dist/apps/cli/src/commands/write/md.js +91 -0
- package/dist/apps/cli/src/commands/ws/_shared.js +129 -0
- package/dist/apps/cli/src/commands/ws/ensure.js +22 -0
- package/dist/apps/cli/src/commands/ws/health.js +15 -0
- package/dist/apps/cli/src/commands/ws/index.js +21 -0
- package/dist/apps/cli/src/commands/ws/logs.js +95 -0
- package/dist/apps/cli/src/commands/ws/restart.js +73 -0
- package/dist/apps/cli/src/commands/ws/serve.js +52 -0
- package/dist/apps/cli/src/commands/ws/start.js +70 -0
- package/dist/apps/cli/src/commands/ws/status.js +60 -0
- package/dist/apps/cli/src/commands/ws/stop.js +59 -0
- package/dist/apps/cli/src/commands/ws/trigger.js +20 -0
- package/dist/apps/cli/src/main.js +79 -0
- package/dist/apps/cli/src/services/AppConfig.js +3 -0
- package/dist/apps/cli/src/services/Config.js +91 -0
- package/dist/apps/cli/src/services/DaemonFiles.js +91 -0
- package/dist/apps/cli/src/services/Errors.js +49 -0
- package/dist/apps/cli/src/services/Output.js +16 -0
- package/dist/apps/cli/src/services/Payload.js +90 -0
- package/dist/apps/cli/src/services/Process.js +94 -0
- package/dist/apps/cli/src/services/Queue.js +120 -0
- package/dist/apps/cli/src/services/RefResolver.js +111 -0
- package/dist/apps/cli/src/services/RemDb.js +35 -0
- package/dist/apps/cli/src/services/WsClient.js +170 -0
- package/dist/apps/cli/tests/apply.contract.test.js +31 -0
- package/dist/apps/cli/tests/db-recent.contract.test.js +22 -0
- package/dist/apps/cli/tests/help.contract.test.js +30 -0
- package/dist/apps/cli/tests/helpers/runCli.js +45 -0
- package/dist/apps/cli/tests/ids-output.contract.test.js +30 -0
- package/dist/apps/cli/tests/payload-stdin.contract.test.js +15 -0
- package/dist/apps/cli/tests/read-search.contract.test.js +22 -0
- package/dist/apps/cli/tests/ws-health.contract.test.js +36 -0
- package/dist/apps/cli/vitest.config.js +7 -0
- package/dist/main.js +100985 -0
- package/dist/packages/mcp/src/public.js +18 -0
- package/dist/packages/mcp/src/queue/dao.js +165 -0
- package/dist/packages/mcp/src/queue/db.js +26 -0
- package/dist/packages/mcp/src/tools/executeSearchQuery.js +914 -0
- package/dist/packages/mcp/src/tools/findRemsByReference.js +447 -0
- package/dist/packages/mcp/src/tools/getRemConnections.js +566 -0
- package/dist/packages/mcp/src/tools/inspectRemDoc.js +60 -0
- package/dist/packages/mcp/src/tools/listRemBackups.js +35 -0
- package/dist/packages/mcp/src/tools/listRemReferences.js +421 -0
- package/dist/packages/mcp/src/tools/listSupportedOps.js +41 -0
- package/dist/packages/mcp/src/tools/listTodos.js +815 -0
- package/dist/packages/mcp/src/tools/outlineRemSubtree.js +203 -0
- package/dist/packages/mcp/src/tools/readRemTable.js +252 -0
- package/dist/packages/mcp/src/tools/resolveRemReference.js +174 -0
- package/dist/packages/mcp/src/tools/searchQueryTypes.js +127 -0
- package/dist/packages/mcp/src/tools/searchRemOverview.js +422 -0
- package/dist/packages/mcp/src/tools/searchUtils.js +32 -0
- package/dist/packages/mcp/src/tools/shared.js +393 -0
- package/dist/packages/mcp/src/tools/summarizeDailyNotes.js +221 -0
- package/dist/packages/mcp/src/tools/summarizeTopicActivity.js +605 -0
- package/dist/packages/mcp/src/tools/timeFilters.js +130 -0
- package/dist/packages/mcp/src/ws/bridge.js +377 -0
- package/package.json +40 -8
- package/README.md +0 -3
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const attributeOperatorSchema = z.enum([
|
|
3
|
+
"equals",
|
|
4
|
+
"notEquals",
|
|
5
|
+
"contains",
|
|
6
|
+
"notContains",
|
|
7
|
+
"greaterThan",
|
|
8
|
+
"greaterThanOrEquals",
|
|
9
|
+
"lessThan",
|
|
10
|
+
"lessThanOrEquals",
|
|
11
|
+
"between",
|
|
12
|
+
"empty",
|
|
13
|
+
"notEmpty",
|
|
14
|
+
"before",
|
|
15
|
+
"after",
|
|
16
|
+
"on",
|
|
17
|
+
"relative",
|
|
18
|
+
]);
|
|
19
|
+
const numericValueSchema = z.number();
|
|
20
|
+
const stringValueSchema = z.string();
|
|
21
|
+
const booleanValueSchema = z.boolean();
|
|
22
|
+
const dateValueSchema = z.union([z.string(), z.number()]);
|
|
23
|
+
const attributeConditionSchema = z.object({
|
|
24
|
+
type: z.literal("attribute"),
|
|
25
|
+
attributeId: z.string().min(1, "attributeId is required"),
|
|
26
|
+
operator: attributeOperatorSchema,
|
|
27
|
+
value: z.union([stringValueSchema, numericValueSchema, booleanValueSchema]).optional(),
|
|
28
|
+
values: z.array(z.union([stringValueSchema, numericValueSchema])).optional(),
|
|
29
|
+
range: z
|
|
30
|
+
.object({
|
|
31
|
+
start: z.union([stringValueSchema, numericValueSchema, dateValueSchema]).optional(),
|
|
32
|
+
end: z.union([stringValueSchema, numericValueSchema, dateValueSchema]).optional(),
|
|
33
|
+
})
|
|
34
|
+
.optional(),
|
|
35
|
+
unit: z.string().optional(),
|
|
36
|
+
relativeAmount: z.number().optional(),
|
|
37
|
+
includeEmpty: z.boolean().optional(),
|
|
38
|
+
});
|
|
39
|
+
const textConditionSchema = z.object({
|
|
40
|
+
type: z.literal("text"),
|
|
41
|
+
value: z.string().min(1, "text value is required"),
|
|
42
|
+
mode: z.enum(["contains", "phrase", "prefix", "suffix"]).default("contains"),
|
|
43
|
+
});
|
|
44
|
+
const tagConditionSchema = z.object({
|
|
45
|
+
type: z.literal("tag"),
|
|
46
|
+
id: z.string().min(1, "tag id is required"),
|
|
47
|
+
includeDescendants: z.boolean().optional(),
|
|
48
|
+
});
|
|
49
|
+
const remConditionSchema = z.object({
|
|
50
|
+
type: z.literal("rem"),
|
|
51
|
+
id: z.string().min(1, "rem id is required"),
|
|
52
|
+
});
|
|
53
|
+
const pageConditionSchema = z.object({
|
|
54
|
+
type: z.literal("page"),
|
|
55
|
+
});
|
|
56
|
+
const queryNodeSchemaInternal = z.lazy(() => z.union([
|
|
57
|
+
attributeConditionSchema,
|
|
58
|
+
textConditionSchema,
|
|
59
|
+
tagConditionSchema,
|
|
60
|
+
remConditionSchema,
|
|
61
|
+
pageConditionSchema,
|
|
62
|
+
z.object({ type: z.literal("not"), node: queryNodeSchemaInternal }),
|
|
63
|
+
z.object({
|
|
64
|
+
type: z.union([z.literal("and"), z.literal("or")]),
|
|
65
|
+
nodes: z
|
|
66
|
+
.array(queryNodeSchemaInternal)
|
|
67
|
+
.min(1, "logical node requires at least one child"),
|
|
68
|
+
}),
|
|
69
|
+
]));
|
|
70
|
+
export const queryNodeSchema = queryNodeSchemaInternal;
|
|
71
|
+
export const sortModeSchema = z.discriminatedUnion("mode", [
|
|
72
|
+
z.object({ mode: z.literal("rank") }),
|
|
73
|
+
z.object({ mode: z.literal("updatedAt"), direction: z.enum(["asc", "desc"]).default("desc") }),
|
|
74
|
+
z.object({ mode: z.literal("createdAt"), direction: z.enum(["asc", "desc"]).default("desc") }),
|
|
75
|
+
z.object({
|
|
76
|
+
mode: z.literal("attribute"),
|
|
77
|
+
attributeId: z.string().min(1, "attributeId is required for attribute sort"),
|
|
78
|
+
direction: z.enum(["asc", "desc"]).default("asc"),
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
export function normalizeQueryNode(node) {
|
|
82
|
+
if (node.type === "and" || node.type === "or") {
|
|
83
|
+
const normalizedChildren = node.nodes.map((child) => normalizeQueryNode(child));
|
|
84
|
+
const flatChildren = [];
|
|
85
|
+
for (const child of normalizedChildren) {
|
|
86
|
+
if (child.type === node.type) {
|
|
87
|
+
flatChildren.push(...child.nodes);
|
|
88
|
+
}
|
|
89
|
+
else if (child.type === "and" || child.type === "or") {
|
|
90
|
+
flatChildren.push(child);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
flatChildren.push(child);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (flatChildren.length === 1) {
|
|
97
|
+
return flatChildren[0];
|
|
98
|
+
}
|
|
99
|
+
return { type: node.type, nodes: flatChildren };
|
|
100
|
+
}
|
|
101
|
+
if (node.type === "not") {
|
|
102
|
+
return { type: "not", node: normalizeQueryNode(node.node) };
|
|
103
|
+
}
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
export function describeQueryNode(node) {
|
|
107
|
+
switch (node.type) {
|
|
108
|
+
case "text":
|
|
109
|
+
return `文本包含「${node.value}」`;
|
|
110
|
+
case "tag":
|
|
111
|
+
return `包含标签 ${node.id}`;
|
|
112
|
+
case "rem":
|
|
113
|
+
return `目标 Rem 为 ${node.id}`;
|
|
114
|
+
case "page":
|
|
115
|
+
return `顶层 Page(parent=null)`;
|
|
116
|
+
case "attribute":
|
|
117
|
+
return `属性 ${node.attributeId} ${node.operator}`;
|
|
118
|
+
case "not":
|
|
119
|
+
return `非(${describeQueryNode(node.node)})`;
|
|
120
|
+
case "and":
|
|
121
|
+
return node.nodes.map(describeQueryNode).join(" 且 ");
|
|
122
|
+
case "or":
|
|
123
|
+
return node.nodes.map(describeQueryNode).join(" 或 ");
|
|
124
|
+
default:
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { buildGuidedResponse, withResolvedDatabase, getDateFormatting, formatDateWithPattern, } from "./shared.js";
|
|
3
|
+
import { TIME_RANGE_PATTERN, timeValueSchema, resolveTimeFilters, } from "./timeFilters.js";
|
|
4
|
+
import { createPreview, coalesceText, stringifyAncestor } from "./searchUtils.js";
|
|
5
|
+
import { parseOrThrow } from "./shared.js";
|
|
6
|
+
const SEARCH_MODE = z.enum(["auto", "like", "fts"]);
|
|
7
|
+
const inputShape = {
|
|
8
|
+
query: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1, "query 必填")
|
|
11
|
+
.describe("关键词(1-3 个),大小写不敏感模糊匹配"),
|
|
12
|
+
limit: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(100)
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("返回条数上限(默认 10)"),
|
|
19
|
+
mode: SEARCH_MODE.optional().describe("检索模式:auto/like/fts(默认 auto)"),
|
|
20
|
+
dbPath: z.string().optional().describe("数据库文件路径(默认自动发现)"),
|
|
21
|
+
useCurrentDate: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("将查询替换为当前日期(配合 dateOffsetDays)"),
|
|
25
|
+
dateOffsetDays: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("相对今天的天数偏移(如 -1=昨天)"),
|
|
30
|
+
parentId: z.string().optional().describe("限定父节点或其子树范围"),
|
|
31
|
+
// Page = 顶层 Rem(parentId 为空/深度=1)
|
|
32
|
+
pagesOnly: z.boolean().optional().describe("仅返回顶层 Page"),
|
|
33
|
+
excludePages: z.boolean().optional().describe("排除顶层 Page"),
|
|
34
|
+
// 是否优先将“标题/别名精确等于查询”的结果置顶
|
|
35
|
+
preferExact: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("标题/别名精确匹配时置顶(默认 true)"),
|
|
39
|
+
// 精确命中时仅返回 1 条;未命中则返回少量(limit 或默认 5 条)
|
|
40
|
+
exactFirstSingle: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("精确命中仅返回 1 条,否则返回少量(默认 false)"),
|
|
44
|
+
offset: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.min(0)
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("分页偏移量"),
|
|
50
|
+
snippetLength: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.min(30)
|
|
54
|
+
.max(400)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("结果摘要长度(字符数)"),
|
|
57
|
+
timeRange: z
|
|
58
|
+
.union([
|
|
59
|
+
z.literal("all"),
|
|
60
|
+
z.literal("*"),
|
|
61
|
+
z.string().regex(TIME_RANGE_PATTERN, "timeRange 需形如 '30d'、'2w'、'12h'"),
|
|
62
|
+
])
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("时间范围(如 30d/2w/12h 或 all/*)"),
|
|
65
|
+
createdAfter: timeValueSchema.optional().describe("创建时间下界(ISO/毫秒/秒)"),
|
|
66
|
+
createdBefore: timeValueSchema.optional().describe("创建时间上界(ISO/毫秒/秒)"),
|
|
67
|
+
updatedAfter: timeValueSchema.optional().describe("更新时间下界(ISO/毫秒/秒)"),
|
|
68
|
+
updatedBefore: timeValueSchema.optional().describe("更新时间上界(ISO/毫秒/秒)"),
|
|
69
|
+
detail: z.boolean().optional().describe("是否返回 parentId/ancestorIds/depth 等细节"),
|
|
70
|
+
};
|
|
71
|
+
export const searchRemOverviewSchema = z
|
|
72
|
+
.object(inputShape)
|
|
73
|
+
.superRefine((value, ctx) => {
|
|
74
|
+
if (value.pagesOnly && value.excludePages) {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: z.ZodIssueCode.custom,
|
|
77
|
+
path: ["pagesOnly"],
|
|
78
|
+
message: "pagesOnly 与 excludePages 不能同时为 true",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
export async function executeSearchRemOverview(params) {
|
|
83
|
+
const parsed = parseOrThrow(searchRemOverviewSchema, params, { label: "search_rem_overview" });
|
|
84
|
+
const limit = parsed.limit ?? 10;
|
|
85
|
+
const mode = parsed.mode ?? "auto";
|
|
86
|
+
const offset = parsed.offset ?? 0;
|
|
87
|
+
const { filters, summary } = resolveTimeFilters(parsed);
|
|
88
|
+
const detail = parsed.detail ?? false;
|
|
89
|
+
const preferExact = parsed.preferExact ?? true;
|
|
90
|
+
const exactFirstSingle = parsed.exactFirstSingle ?? false;
|
|
91
|
+
const { result, info } = await withResolvedDatabase(parsed.dbPath, async (db) => {
|
|
92
|
+
let effectiveQuery = parsed.query;
|
|
93
|
+
if (parsed.useCurrentDate) {
|
|
94
|
+
const offset = parsed.dateOffsetDays ?? 0;
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const target = new Date(now.getFullYear(), now.getMonth(), now.getDate() + offset);
|
|
97
|
+
const format = (await getDateFormatting(db)) ?? "yyyy/MM/dd";
|
|
98
|
+
effectiveQuery = formatDateWithPattern(target, format);
|
|
99
|
+
}
|
|
100
|
+
const items = searchRems(db, {
|
|
101
|
+
query: effectiveQuery,
|
|
102
|
+
limit: limit + 1,
|
|
103
|
+
offset,
|
|
104
|
+
mode,
|
|
105
|
+
filters,
|
|
106
|
+
preferExact,
|
|
107
|
+
pagesOnly: parsed.pagesOnly,
|
|
108
|
+
excludePages: parsed.excludePages,
|
|
109
|
+
});
|
|
110
|
+
let filtered = items;
|
|
111
|
+
if (exactFirstSingle && preferExact) {
|
|
112
|
+
const exact = filtered.filter((it) => it.exactScore === 1);
|
|
113
|
+
if (exact.length > 0) {
|
|
114
|
+
// 仅返回第一条精确命中
|
|
115
|
+
filtered = [exact[0]];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (parsed.pagesOnly) {
|
|
119
|
+
filtered = filtered.filter((item) => (item.depth ?? null) === 1 || item.parentId == null);
|
|
120
|
+
}
|
|
121
|
+
else if (parsed.excludePages) {
|
|
122
|
+
filtered = filtered.filter((item) => (item.depth ?? null) !== 1 && item.parentId != null);
|
|
123
|
+
}
|
|
124
|
+
if (parsed.parentId) {
|
|
125
|
+
filtered = filtered.filter((item) => {
|
|
126
|
+
if (item.parentId === parsed.parentId)
|
|
127
|
+
return true;
|
|
128
|
+
return item.ancestor?.ids?.includes(parsed.parentId) ?? false;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { items: filtered, query: effectiveQuery };
|
|
132
|
+
});
|
|
133
|
+
const effectiveLimit = exactFirstSingle
|
|
134
|
+
? // 若命中精确且被裁剪为单条,则上面已经只剩一条;否则兜底使用 limit 或默认 5 条
|
|
135
|
+
(result.items.length === 1 ? 1 : parsed.limit ?? 5)
|
|
136
|
+
: limit;
|
|
137
|
+
const visible = result.items.slice(0, effectiveLimit);
|
|
138
|
+
const hasMore = result.items.length > effectiveLimit;
|
|
139
|
+
const snippetLength = parsed.snippetLength ?? 180;
|
|
140
|
+
const simplifiedMatches = visible.map((item) => simplifyMatch(item, snippetLength, detail));
|
|
141
|
+
const markdown = buildMatchesMarkdown(simplifiedMatches);
|
|
142
|
+
return {
|
|
143
|
+
dbPath: info.dbPath,
|
|
144
|
+
resolution: info.source,
|
|
145
|
+
dirName: info.dirName,
|
|
146
|
+
count: visible.length,
|
|
147
|
+
offset,
|
|
148
|
+
limit: effectiveLimit,
|
|
149
|
+
hasMore,
|
|
150
|
+
nextOffset: hasMore ? offset + effectiveLimit : null,
|
|
151
|
+
filtersApplied: summary,
|
|
152
|
+
markdown,
|
|
153
|
+
matches: simplifiedMatches,
|
|
154
|
+
queryUsed: result.query,
|
|
155
|
+
totalFetched: result.items.length,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function registerSearchRemOverview(server) {
|
|
159
|
+
server.tool("search_rem_overview", `<usecase>定位 Rem 的通用关键词搜索工具,适合轻量、即时查找。</usecase>
|
|
160
|
+
<instructions>
|
|
161
|
+
- 传入 1-3 个简短关键词;若要快速定位每日笔记,可以设置 useCurrentDate/dateOffsetDays。
|
|
162
|
+
- 支持时间筛选:timeRange(例如 '30d')、createdAfter/updatedAfter(毫秒时间戳或 ISO 日期)。
|
|
163
|
+
- 默认每次返回最多 10 条摘要,若还有更多结果,可使用 offset 进行分页。
|
|
164
|
+
- snippetLength 控制结果摘要长度,必要时减少值以节约 Token。
|
|
165
|
+
- 命中后可继续调用 outline_rem_subtree / inspect_rem_doc 查看全文;若需要组合更多条件或减少多次调用,请优先使用 build_search_query + execute_search_query。
|
|
166
|
+
- Page 支持:Page 指 parentId 为空/深度=1 的顶层 Rem。可传 pagesOnly=true 仅返回 Page,或 excludePages=true 排除 Page(两者不可同时为 true)。
|
|
167
|
+
- 精确置顶:preferExact(默认 true)。当标题/别名等于查询(忽略大小写与空格)时,将被优先置顶;设为 false 可关闭该置顶策略。
|
|
168
|
+
- 返回项包含 isPage 字段,便于快速判断是否为 Page;detail=true 时附带 parentId、ancestorIds、depth。
|
|
169
|
+
</instructions>`, inputShape, async (input) => {
|
|
170
|
+
const parsed = parseOrThrow(searchRemOverviewSchema, input, { label: "search_rem_overview" });
|
|
171
|
+
const result = await executeSearchRemOverview(parsed);
|
|
172
|
+
const suggestions = [];
|
|
173
|
+
if (result.count > 0) {
|
|
174
|
+
suggestions.push("如需阅读全文,可调用 outline_rem_subtree(支持 startOffset/limit 分页)");
|
|
175
|
+
suggestions.push("如需展开引用内容,可调用 resolve_rem_reference(ids=[...])");
|
|
176
|
+
suggestions.push("若需组合更多过滤条件或减少多次调用,可改用 build_search_query → execute_search_query");
|
|
177
|
+
if (result.hasMore && result.nextOffset != null) {
|
|
178
|
+
suggestions.push(`还有更多搜索结果,可再次调用 search_rem_overview 并设置 offset=${result.nextOffset}`);
|
|
179
|
+
}
|
|
180
|
+
if (parsed.parentId) {
|
|
181
|
+
suggestions.push("已按 parentId 过滤,可调整 parentId 或移除过滤条件以放宽范围");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
suggestions.push("可尝试调整关键词,或使用 summarize_topic_activity keywords=[...] timeRange='30d' 获取专题摘要");
|
|
186
|
+
suggestions.push("若仍需要结构化检索,可改用 build_search_query → execute_search_query");
|
|
187
|
+
}
|
|
188
|
+
const filterDescription = describeFilterSummary(result.filtersApplied);
|
|
189
|
+
const filterSuffix = filterDescription ? `(${filterDescription})` : "";
|
|
190
|
+
const guidance = result.count > 0
|
|
191
|
+
? `找到 ${result.count} 条 Rem 匹配(查询 "${result.queryUsed}",${result.hasMore ? "可继续分页" : "已到末尾"}${filterSuffix})。`
|
|
192
|
+
: `未找到 Rem 匹配(使用查询 "${result.queryUsed}"${filterSuffix})。`;
|
|
193
|
+
return buildGuidedResponse({ guidance, ...result }, suggestions);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function searchRems(db, options) {
|
|
197
|
+
const normalized = options.query.trim();
|
|
198
|
+
if (!normalized) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const lowerQuery = normalized.toLowerCase();
|
|
202
|
+
const likePattern = `%${lowerQuery.replace(/\s+/g, "%")}%`;
|
|
203
|
+
const likePatternCompact = `%${lowerQuery.replace(/\s+/g, "")}%`;
|
|
204
|
+
const { clause, params: filterParams } = buildTimeFilterClause(options.filters);
|
|
205
|
+
const pageConds = [];
|
|
206
|
+
if (options.pagesOnly) {
|
|
207
|
+
pageConds.push("CAST(json_extract(doc, '$.rd') AS INTEGER) = 1");
|
|
208
|
+
}
|
|
209
|
+
else if (options.excludePages) {
|
|
210
|
+
pageConds.push("CAST(json_extract(doc, '$.rd') AS INTEGER) <> 1");
|
|
211
|
+
}
|
|
212
|
+
const extra = [clause, ...pageConds].filter(Boolean).join(" AND ");
|
|
213
|
+
const whereSuffix = extra ? ` AND ${extra}` : "";
|
|
214
|
+
const likeStmt = db.prepare(`SELECT
|
|
215
|
+
aliasId,
|
|
216
|
+
id,
|
|
217
|
+
json_extract(doc, '$.kt') AS kt,
|
|
218
|
+
json_extract(doc, '$.ke') AS ke,
|
|
219
|
+
json_extract(doc, '$.p') AS parentId,
|
|
220
|
+
json_extract(doc, '$.rd') AS depth,
|
|
221
|
+
COALESCE((SELECT rank FROM remsSearchRanks WHERE ftsRowId = remsSearchInfos.ftsRowId), 0) AS rank,
|
|
222
|
+
freqCounter,
|
|
223
|
+
freqTime,
|
|
224
|
+
ancestor_not_ref_text AS ancestorNotRefText,
|
|
225
|
+
ancestor_ids AS ancestorIds,
|
|
226
|
+
-- 精准匹配标记:标题/别名(r)等于查询(忽略大小写与空格)
|
|
227
|
+
CASE WHEN (
|
|
228
|
+
lower(json_extract(doc, '$.kt')) = @exact
|
|
229
|
+
OR REPLACE(lower(json_extract(doc, '$.kt')), ' ', '') = @exactCompact
|
|
230
|
+
OR lower(json_extract(doc, '$.ke')) = @exact
|
|
231
|
+
OR REPLACE(lower(json_extract(doc, '$.ke')), ' ', '') = @exactCompact
|
|
232
|
+
OR lower(json_extract(doc, '$.r')) = @exact
|
|
233
|
+
) THEN 1 ELSE 0 END AS exactScore,
|
|
234
|
+
-- 根据 preferExact 动态控制置顶权重
|
|
235
|
+
CASE WHEN @preferExact != 0 THEN (
|
|
236
|
+
CASE WHEN (
|
|
237
|
+
lower(json_extract(doc, '$.kt')) = @exact
|
|
238
|
+
OR REPLACE(lower(json_extract(doc, '$.kt')), ' ', '') = @exactCompact
|
|
239
|
+
OR lower(json_extract(doc, '$.ke')) = @exact
|
|
240
|
+
OR REPLACE(lower(json_extract(doc, '$.ke')), ' ', '') = @exactCompact
|
|
241
|
+
OR lower(json_extract(doc, '$.r')) = @exact
|
|
242
|
+
) THEN 1 ELSE 0 END
|
|
243
|
+
) ELSE 0 END AS orderExact
|
|
244
|
+
FROM remsSearchInfos
|
|
245
|
+
WHERE (
|
|
246
|
+
lower(json_extract(doc, '$.kt')) LIKE @pattern
|
|
247
|
+
OR lower(json_extract(doc, '$.ke')) LIKE @pattern
|
|
248
|
+
OR REPLACE(lower(json_extract(doc, '$.kt')), ' ', '') LIKE @patternCompact
|
|
249
|
+
OR REPLACE(lower(json_extract(doc, '$.ke')), ' ', '') LIKE @patternCompact
|
|
250
|
+
OR lower(json_extract(doc, '$.r')) LIKE @pattern
|
|
251
|
+
)${whereSuffix}
|
|
252
|
+
ORDER BY orderExact DESC, rank DESC, freqCounter DESC
|
|
253
|
+
LIMIT @limit OFFSET @offset`);
|
|
254
|
+
const rows = likeStmt.all({
|
|
255
|
+
pattern: likePattern,
|
|
256
|
+
patternCompact: likePatternCompact,
|
|
257
|
+
limit: options.limit,
|
|
258
|
+
offset: options.offset,
|
|
259
|
+
preferExact: options.preferExact ? 1 : 0,
|
|
260
|
+
exact: lowerQuery,
|
|
261
|
+
exactCompact: lowerQuery.replace(/\s+/g, ""),
|
|
262
|
+
...filterParams,
|
|
263
|
+
});
|
|
264
|
+
let items = rows.map(mapRowToItem);
|
|
265
|
+
if (options.mode === "fts" || (options.mode === "auto" && items.length === 0)) {
|
|
266
|
+
try {
|
|
267
|
+
const ftsStmt = db.prepare(`SELECT
|
|
268
|
+
aliasId,
|
|
269
|
+
id,
|
|
270
|
+
json_extract(doc, '$.kt') AS kt,
|
|
271
|
+
json_extract(doc, '$.ke') AS ke,
|
|
272
|
+
json_extract(doc, '$.p') AS parentId,
|
|
273
|
+
json_extract(doc, '$.rd') AS depth,
|
|
274
|
+
COALESCE((SELECT rank FROM remsSearchRanks WHERE ftsRowId = remsSearchInfos.ftsRowId), 0) AS rank,
|
|
275
|
+
freqCounter,
|
|
276
|
+
freqTime,
|
|
277
|
+
ancestor_not_ref_text AS ancestorNotRefText,
|
|
278
|
+
ancestor_ids AS ancestorIds,
|
|
279
|
+
-- 精准匹配标记
|
|
280
|
+
CASE WHEN (
|
|
281
|
+
lower(json_extract(doc, '$.kt')) = @exact
|
|
282
|
+
OR REPLACE(lower(json_extract(doc, '$.kt')), ' ', '') = @exactCompact
|
|
283
|
+
OR lower(json_extract(doc, '$.ke')) = @exact
|
|
284
|
+
OR REPLACE(lower(json_extract(doc, '$.ke')), ' ', '') = @exactCompact
|
|
285
|
+
OR lower(json_extract(doc, '$.r')) = @exact
|
|
286
|
+
) THEN 1 ELSE 0 END AS exactScore,
|
|
287
|
+
CASE WHEN @preferExact != 0 THEN (
|
|
288
|
+
CASE WHEN (
|
|
289
|
+
lower(json_extract(doc, '$.kt')) = @exact
|
|
290
|
+
OR REPLACE(lower(json_extract(doc, '$.kt')), ' ', '') = @exactCompact
|
|
291
|
+
OR lower(json_extract(doc, '$.ke')) = @exact
|
|
292
|
+
OR REPLACE(lower(json_extract(doc, '$.ke')), ' ', '') = @exactCompact
|
|
293
|
+
OR lower(json_extract(doc, '$.r')) = @exact
|
|
294
|
+
) THEN 1 ELSE 0 END
|
|
295
|
+
) ELSE 0 END AS orderExact
|
|
296
|
+
FROM remsSearchInfos
|
|
297
|
+
WHERE ftsRowId IN (
|
|
298
|
+
SELECT rowid FROM remsContents WHERE remsContents MATCH @matchQuery
|
|
299
|
+
)${whereSuffix}
|
|
300
|
+
ORDER BY orderExact DESC, rank DESC, freqCounter DESC
|
|
301
|
+
LIMIT @limit OFFSET @offset`);
|
|
302
|
+
const ftsRows = ftsStmt.all({
|
|
303
|
+
matchQuery: normalized,
|
|
304
|
+
limit: options.limit,
|
|
305
|
+
offset: options.offset,
|
|
306
|
+
exact: lowerQuery,
|
|
307
|
+
exactCompact: lowerQuery.replace(/\s+/g, ""),
|
|
308
|
+
preferExact: options.preferExact ? 1 : 0,
|
|
309
|
+
...filterParams,
|
|
310
|
+
});
|
|
311
|
+
if (ftsRows.length > 0) {
|
|
312
|
+
items = ftsRows.map(mapRowToItem);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (options.mode === "fts") {
|
|
317
|
+
throw new Error(`FTS 查询失败(可能未启用 RemNote 自定义分词器或语法不兼容)。建议改用 mode=\"like\",或省略 mode 使用 auto。原始错误:${String(error)}`);
|
|
318
|
+
}
|
|
319
|
+
// otherwise ignore and fall back to LIKE results
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return items;
|
|
323
|
+
}
|
|
324
|
+
function mapRowToItem(row) {
|
|
325
|
+
return {
|
|
326
|
+
id: row.id,
|
|
327
|
+
parentId: row.parentId,
|
|
328
|
+
text: coalesceText(row.kt, row.ke),
|
|
329
|
+
depth: row.depth,
|
|
330
|
+
exactScore: row.exactScore ?? 0,
|
|
331
|
+
ancestor: stringifyAncestor(row.ancestorNotRefText, row.ancestorIds),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function simplifyMatch(item, snippetLength, detail) {
|
|
335
|
+
const preview = createPreview(item.text, snippetLength);
|
|
336
|
+
const base = {
|
|
337
|
+
id: item.id,
|
|
338
|
+
title: preview.title,
|
|
339
|
+
snippet: preview.snippet,
|
|
340
|
+
truncated: preview.truncated,
|
|
341
|
+
ancestor: item.ancestor?.text ?? null,
|
|
342
|
+
isPage: (item.depth ?? null) === 1 || item.parentId == null,
|
|
343
|
+
};
|
|
344
|
+
if (!detail) {
|
|
345
|
+
return base;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...base,
|
|
349
|
+
parentId: item.parentId,
|
|
350
|
+
ancestorIds: item.ancestor?.ids ?? [],
|
|
351
|
+
rawText: item.text,
|
|
352
|
+
depth: item.depth ?? null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function buildMatchesMarkdown(matches) {
|
|
356
|
+
if (!matches || matches.length === 0) {
|
|
357
|
+
return "未找到匹配 Rem。";
|
|
358
|
+
}
|
|
359
|
+
const lines = ["# 搜索结果"];
|
|
360
|
+
matches.forEach((match, index) => {
|
|
361
|
+
const title = match.title?.trim() ? match.title.trim() : "(未命名)";
|
|
362
|
+
const ancestor = match.ancestor ? `,所在:${match.ancestor}` : "";
|
|
363
|
+
const snippet = match.snippet ? `\n - ${match.snippet}` : "";
|
|
364
|
+
lines.push(`- **${index + 1}. ${title}**(ID: ${match.id}${ancestor})${snippet}`);
|
|
365
|
+
});
|
|
366
|
+
return lines.join("\n");
|
|
367
|
+
}
|
|
368
|
+
function buildTimeFilterClause(filters) {
|
|
369
|
+
const conditions = [];
|
|
370
|
+
const params = {};
|
|
371
|
+
const createdExpr = `COALESCE(
|
|
372
|
+
CAST(json_extract(doc, '$.c') AS INTEGER),
|
|
373
|
+
(SELECT CAST(json_extract(q.doc, '$.createdAt') AS INTEGER) FROM quanta q WHERE q._id = remsSearchInfos.id)
|
|
374
|
+
)`;
|
|
375
|
+
const updatedExpr = `COALESCE(
|
|
376
|
+
(SELECT CAST(json_extract(q.doc, '$.m') AS INTEGER) FROM quanta q WHERE q._id = remsSearchInfos.id),
|
|
377
|
+
CAST(json_extract(doc, '$.c') AS INTEGER)
|
|
378
|
+
)`;
|
|
379
|
+
if (filters.createdAfter !== undefined) {
|
|
380
|
+
conditions.push(`${createdExpr} >= @createdAfter`);
|
|
381
|
+
params.createdAfter = filters.createdAfter;
|
|
382
|
+
}
|
|
383
|
+
if (filters.createdBefore !== undefined) {
|
|
384
|
+
conditions.push(`${createdExpr} <= @createdBefore`);
|
|
385
|
+
params.createdBefore = filters.createdBefore;
|
|
386
|
+
}
|
|
387
|
+
if (filters.updatedAfter !== undefined) {
|
|
388
|
+
conditions.push(`${updatedExpr} >= @updatedAfter`);
|
|
389
|
+
params.updatedAfter = filters.updatedAfter;
|
|
390
|
+
}
|
|
391
|
+
if (filters.updatedBefore !== undefined) {
|
|
392
|
+
conditions.push(`${updatedExpr} <= @updatedBefore`);
|
|
393
|
+
params.updatedBefore = filters.updatedBefore;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
clause: conditions.join(" AND "),
|
|
397
|
+
params,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function describeFilterSummary(summary) {
|
|
401
|
+
if (!summary)
|
|
402
|
+
return null;
|
|
403
|
+
const parts = [];
|
|
404
|
+
if (summary.timeRange) {
|
|
405
|
+
parts.push(`timeRange=${summary.timeRange}`);
|
|
406
|
+
}
|
|
407
|
+
if (summary.updatedAfter) {
|
|
408
|
+
parts.push(`updated ≥ ${summary.updatedAfter.iso}`);
|
|
409
|
+
}
|
|
410
|
+
if (summary.updatedBefore) {
|
|
411
|
+
parts.push(`updated ≤ ${summary.updatedBefore.iso}`);
|
|
412
|
+
}
|
|
413
|
+
if (summary.createdAfter) {
|
|
414
|
+
parts.push(`created ≥ ${summary.createdAfter.iso}`);
|
|
415
|
+
}
|
|
416
|
+
if (summary.createdBefore) {
|
|
417
|
+
parts.push(`created ≤ ${summary.createdBefore.iso}`);
|
|
418
|
+
}
|
|
419
|
+
if (parts.length === 0)
|
|
420
|
+
return null;
|
|
421
|
+
return parts.join(",");
|
|
422
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function createPreview(text, maxLength) {
|
|
2
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3
|
+
if (!normalized) {
|
|
4
|
+
return {
|
|
5
|
+
title: "(空)",
|
|
6
|
+
snippet: "",
|
|
7
|
+
truncated: false,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const snippet = normalized.length > maxLength ? `${normalized.slice(0, maxLength)}…` : normalized;
|
|
11
|
+
const title = normalized.split(/\n| - |——|。|!|?|\.|: /)[0]?.trim() || normalized.slice(0, 80);
|
|
12
|
+
return {
|
|
13
|
+
title: title.slice(0, 120),
|
|
14
|
+
snippet,
|
|
15
|
+
truncated: normalized.length > maxLength,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function coalesceText(kt, ke) {
|
|
19
|
+
const combined = [kt, ke]
|
|
20
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(" | ");
|
|
23
|
+
return combined;
|
|
24
|
+
}
|
|
25
|
+
export function stringifyAncestor(text, ids) {
|
|
26
|
+
const ancestorText = typeof text === "string" ? text.trim() : "";
|
|
27
|
+
const ancestorIds = typeof ids === "string" ? ids.trim().split(/\s+/).filter(Boolean) : [];
|
|
28
|
+
return {
|
|
29
|
+
text: ancestorText,
|
|
30
|
+
ids: ancestorIds,
|
|
31
|
+
};
|
|
32
|
+
}
|