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,393 +0,0 @@
|
|
|
1
|
-
import BetterSqlite3 from "better-sqlite3";
|
|
2
|
-
import { promises as fs } from "fs";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { format as formatDate } from "date-fns";
|
|
6
|
-
export const REMNOTE_RELATIVE_DIR = "remnote";
|
|
7
|
-
export const REMNOTE_DB_NAME = "remnote.db";
|
|
8
|
-
export const REMNOTE_DIR_PREFIX = "remnote-";
|
|
9
|
-
const SECONDARY_DIRS = new Set(["remnote-browser", "lnotes"]);
|
|
10
|
-
export const SYSTEM_REM_IDS = new Set([
|
|
11
|
-
"u71eGVAt1upM7uwC6", // Date String
|
|
12
|
-
"DikxdKKfeGh52AP7p", // timestamp
|
|
13
|
-
"SLg5kZsuzw3GyN6Kh", // Status
|
|
14
|
-
]);
|
|
15
|
-
export const SYSTEM_REM_KEYS = new Set(["Date String", "timestamp", "Status"]);
|
|
16
|
-
export async function withResolvedDatabase(dbPath, fn) {
|
|
17
|
-
const info = await resolveDatabasePath(dbPath);
|
|
18
|
-
let db;
|
|
19
|
-
try {
|
|
20
|
-
db = new BetterSqlite3(info.dbPath, { readonly: true });
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
const normalized = String(error ?? "");
|
|
24
|
-
if (/\b(SQLITE_(BUSY|LOCKED|CANTOPEN))\b/i.test(normalized) || /busy/i.test(normalized)) {
|
|
25
|
-
throw new Error(`无法打开数据库 ${info.dbPath}(可能正在被 RemNote 客户端占用或尚未写入完成)。` +
|
|
26
|
-
" 请确认 RemNote 已完成同步,或稍候重试。如需立即使用,可显式指定备份路径 dbPath=...。");
|
|
27
|
-
}
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
try {
|
|
32
|
-
const result = await fn(db);
|
|
33
|
-
return { result, info };
|
|
34
|
-
}
|
|
35
|
-
catch (inner) {
|
|
36
|
-
const message = String(inner ?? "");
|
|
37
|
-
if (/no such table: (remsSearchInfos|remsContents)/i.test(message)) {
|
|
38
|
-
throw new Error("数据库缺少搜索索引表 remsSearchInfos/remsContents。请在 RemNote 客户端完成索引生成或使用较新的备份后重试。");
|
|
39
|
-
}
|
|
40
|
-
if (/no such table: quanta/i.test(message)) {
|
|
41
|
-
throw new Error("数据库结构不匹配(缺少 quanta 表)。请确认 dbPath 是否指向 RemNote 的 remnote.db 文件,或选择正确账户目录下的数据库/备份。");
|
|
42
|
-
}
|
|
43
|
-
if (/malformed MATCH/i.test(message) || /no such tokenizer/i.test(message)) {
|
|
44
|
-
throw new Error(`全文检索 FTS 当前不可用(分词器未加载或语法不兼容)。建议改用 mode=\"like\",或在 RemNote 客户端完成索引后重试。原始错误:${message}`);
|
|
45
|
-
}
|
|
46
|
-
throw inner;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
db?.close();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
export async function resolveDatabasePath(dbPath) {
|
|
54
|
-
if (dbPath) {
|
|
55
|
-
const expanded = expandHome(dbPath.trim());
|
|
56
|
-
const stat = await fs.stat(expanded);
|
|
57
|
-
if (!stat.isFile()) {
|
|
58
|
-
throw new Error(`${expanded} is not a file`);
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
dbPath: expanded,
|
|
62
|
-
source: "explicit",
|
|
63
|
-
baseDir: path.dirname(expanded),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const baseDir = path.join(os.homedir(), REMNOTE_RELATIVE_DIR);
|
|
67
|
-
const entries = await fs
|
|
68
|
-
.readdir(baseDir, { withFileTypes: true })
|
|
69
|
-
.catch(() => {
|
|
70
|
-
throw new Error(`RemNote directory ${baseDir} not found – please specify dbPath manually`);
|
|
71
|
-
});
|
|
72
|
-
const candidates = [];
|
|
73
|
-
for (const entry of entries) {
|
|
74
|
-
if (!entry.isDirectory())
|
|
75
|
-
continue;
|
|
76
|
-
if (entry.name === "backups")
|
|
77
|
-
continue;
|
|
78
|
-
const isPrimary = entry.name.startsWith(REMNOTE_DIR_PREFIX);
|
|
79
|
-
const isSecondary = SECONDARY_DIRS.has(entry.name);
|
|
80
|
-
if (!isPrimary && !isSecondary)
|
|
81
|
-
continue;
|
|
82
|
-
const candidatePath = path.join(baseDir, entry.name, REMNOTE_DB_NAME);
|
|
83
|
-
try {
|
|
84
|
-
const stat = await fs.stat(candidatePath);
|
|
85
|
-
candidates.push({
|
|
86
|
-
dbPath: candidatePath,
|
|
87
|
-
dirName: entry.name,
|
|
88
|
-
mtimeMs: stat.mtimeMs,
|
|
89
|
-
priority: isPrimary ? 0 : 1,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
catch (_) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (candidates.length === 0) {
|
|
97
|
-
throw new Error(`No ${REMNOTE_DB_NAME} found under ${baseDir}. Specify dbPath manually or ensure RemNote has run.`);
|
|
98
|
-
}
|
|
99
|
-
candidates.sort((a, b) => {
|
|
100
|
-
if (a.priority !== b.priority)
|
|
101
|
-
return a.priority - b.priority;
|
|
102
|
-
return b.mtimeMs - a.mtimeMs;
|
|
103
|
-
});
|
|
104
|
-
const best = candidates[0];
|
|
105
|
-
return {
|
|
106
|
-
dbPath: best.dbPath,
|
|
107
|
-
source: "auto",
|
|
108
|
-
dirName: best.dirName,
|
|
109
|
-
baseDir,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
export async function discoverBackups(basePath) {
|
|
113
|
-
const backups = [];
|
|
114
|
-
let entries;
|
|
115
|
-
try {
|
|
116
|
-
entries = await fs.readdir(basePath, { withFileTypes: true });
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
throw new Error(`Unable to read ${basePath}: ${String(error)}`);
|
|
120
|
-
}
|
|
121
|
-
for (const entry of entries) {
|
|
122
|
-
if (!entry.isDirectory())
|
|
123
|
-
continue;
|
|
124
|
-
const accountDir = path.join(basePath, entry.name);
|
|
125
|
-
const backupDir = path.join(accountDir, "backups");
|
|
126
|
-
let backupFiles;
|
|
127
|
-
try {
|
|
128
|
-
backupFiles = await fs.readdir(backupDir, { withFileTypes: true });
|
|
129
|
-
}
|
|
130
|
-
catch (_) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
for (const file of backupFiles) {
|
|
134
|
-
if (!file.isFile())
|
|
135
|
-
continue;
|
|
136
|
-
if (!file.name.endsWith(".db") && !file.name.endsWith(".db.zip"))
|
|
137
|
-
continue;
|
|
138
|
-
const fullPath = path.join(backupDir, file.name);
|
|
139
|
-
const stat = await fs.stat(fullPath);
|
|
140
|
-
backups.push({
|
|
141
|
-
accountDir: entry.name,
|
|
142
|
-
file: file.name,
|
|
143
|
-
fullPath,
|
|
144
|
-
size: stat.size,
|
|
145
|
-
mtime: new Date(stat.mtimeMs).toISOString(),
|
|
146
|
-
type: file.name.endsWith(".zip") ? "zip" : "db",
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
backups.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
|
|
151
|
-
return backups;
|
|
152
|
-
}
|
|
153
|
-
export function expandHome(targetPath) {
|
|
154
|
-
if (!targetPath.startsWith("~"))
|
|
155
|
-
return targetPath;
|
|
156
|
-
const home = os.homedir();
|
|
157
|
-
if (targetPath === "~")
|
|
158
|
-
return home;
|
|
159
|
-
if (targetPath.startsWith("~/"))
|
|
160
|
-
return path.join(home, targetPath.slice(2));
|
|
161
|
-
return targetPath.replace(/^~(?=$|\/)/, home);
|
|
162
|
-
}
|
|
163
|
-
export function safeJsonParse(input) {
|
|
164
|
-
if (typeof input !== "string")
|
|
165
|
-
return null;
|
|
166
|
-
try {
|
|
167
|
-
return JSON.parse(input);
|
|
168
|
-
}
|
|
169
|
-
catch (_) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
export function summarizeKey(rawKey, db, options) {
|
|
174
|
-
const fragments = [];
|
|
175
|
-
const references = new Set();
|
|
176
|
-
const visited = new Set();
|
|
177
|
-
const stmt = db ? db.prepare("SELECT doc FROM quanta WHERE _id = ?") : undefined;
|
|
178
|
-
const helper = (key, depth) => {
|
|
179
|
-
if (!Array.isArray(key))
|
|
180
|
-
return;
|
|
181
|
-
for (const item of key) {
|
|
182
|
-
if (typeof item === "string") {
|
|
183
|
-
fragments.push(item);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (item && typeof item === "object") {
|
|
187
|
-
const maybeObj = item;
|
|
188
|
-
if (maybeObj.i === "q" && typeof maybeObj._id === "string") {
|
|
189
|
-
const refId = maybeObj._id;
|
|
190
|
-
references.add(refId);
|
|
191
|
-
let expanded = false;
|
|
192
|
-
if (options.expand &&
|
|
193
|
-
depth < options.maxDepth &&
|
|
194
|
-
stmt &&
|
|
195
|
-
!visited.has(refId)) {
|
|
196
|
-
visited.add(refId);
|
|
197
|
-
try {
|
|
198
|
-
const row = stmt.get(refId);
|
|
199
|
-
if (row) {
|
|
200
|
-
const refDoc = safeJsonParse(row.doc);
|
|
201
|
-
helper(refDoc?.key, depth + 1);
|
|
202
|
-
expanded = true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
// keep visited entry to prevent cycles
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (!expanded) {
|
|
210
|
-
fragments.push(`{ref:${refId}}`);
|
|
211
|
-
}
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const richText = extractRichText(maybeObj);
|
|
215
|
-
if (richText) {
|
|
216
|
-
fragments.push(richText);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (typeof maybeObj.t === "string") {
|
|
220
|
-
fragments.push(maybeObj.t);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
fragments.push(JSON.stringify(item));
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (item != null) {
|
|
227
|
-
fragments.push(String(item));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
helper(rawKey, 0);
|
|
232
|
-
const text = fragments
|
|
233
|
-
.join("")
|
|
234
|
-
.replace(/\s+/g, " ")
|
|
235
|
-
.trim();
|
|
236
|
-
return {
|
|
237
|
-
text,
|
|
238
|
-
references: Array.from(references),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function extractRichText(obj) {
|
|
242
|
-
if (typeof obj.text === "string" && obj.text.trim()) {
|
|
243
|
-
return obj.text;
|
|
244
|
-
}
|
|
245
|
-
if (typeof obj.title === "string" && obj.title.trim()) {
|
|
246
|
-
const suffix = typeof obj.text === "string" && obj.text.trim() ? ` ${obj.text}` : "";
|
|
247
|
-
return `${obj.title}${suffix}`.trim();
|
|
248
|
-
}
|
|
249
|
-
if (Array.isArray(obj.children)) {
|
|
250
|
-
const nested = obj.children
|
|
251
|
-
.map((child) => {
|
|
252
|
-
if (typeof child === "string")
|
|
253
|
-
return child;
|
|
254
|
-
if (child && typeof child === "object") {
|
|
255
|
-
return extractRichText(child) ?? "";
|
|
256
|
-
}
|
|
257
|
-
return "";
|
|
258
|
-
})
|
|
259
|
-
.filter(Boolean)
|
|
260
|
-
.join("");
|
|
261
|
-
if (nested) {
|
|
262
|
-
return nested;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return undefined;
|
|
266
|
-
}
|
|
267
|
-
export function makeToolResponse(structuredContent) {
|
|
268
|
-
return {
|
|
269
|
-
content: [
|
|
270
|
-
{
|
|
271
|
-
type: "text",
|
|
272
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
structuredContent,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
export function parseOrThrow(schema, input, options) {
|
|
279
|
-
const res = schema.safeParse(input);
|
|
280
|
-
if (res.success)
|
|
281
|
-
return res.data;
|
|
282
|
-
const issues = res.error.issues;
|
|
283
|
-
const lines = issues.map((i) => formatZodIssueCN(i));
|
|
284
|
-
const prefix = options?.label ? `参数错误(${options.label}):` : "参数错误:";
|
|
285
|
-
throw new Error(prefix + lines.join("; "));
|
|
286
|
-
}
|
|
287
|
-
function formatZodIssueCN(issue) {
|
|
288
|
-
const path = issue.path && issue.path.length > 0 ? issue.path.join(".") : undefined;
|
|
289
|
-
const where = path ? `字段 ${path}:` : "";
|
|
290
|
-
switch (issue.code) {
|
|
291
|
-
case "invalid_type": {
|
|
292
|
-
const expected = toCNType(issue.expected);
|
|
293
|
-
const received = toCNType(issue.received);
|
|
294
|
-
return `${where}类型应为 ${expected},实际为 ${received}`;
|
|
295
|
-
}
|
|
296
|
-
case "too_small": {
|
|
297
|
-
if (issue.type === "string") {
|
|
298
|
-
return `${where}长度至少 ${issue.minimum}`;
|
|
299
|
-
}
|
|
300
|
-
if (issue.type === "number") {
|
|
301
|
-
return `${where}数值应 ≥ ${issue.minimum}`;
|
|
302
|
-
}
|
|
303
|
-
if (issue.type === "array") {
|
|
304
|
-
return `${where}至少包含 ${issue.minimum} 个元素`;
|
|
305
|
-
}
|
|
306
|
-
return `${where}值过小`;
|
|
307
|
-
}
|
|
308
|
-
case "too_big": {
|
|
309
|
-
if (issue.type === "string") {
|
|
310
|
-
return `${where}长度至多 ${issue.maximum}`;
|
|
311
|
-
}
|
|
312
|
-
if (issue.type === "number") {
|
|
313
|
-
return `${where}数值应 ≤ ${issue.maximum}`;
|
|
314
|
-
}
|
|
315
|
-
if (issue.type === "array") {
|
|
316
|
-
return `${where}至多包含 ${issue.maximum} 个元素`;
|
|
317
|
-
}
|
|
318
|
-
return `${where}值过大`;
|
|
319
|
-
}
|
|
320
|
-
case "invalid_string": {
|
|
321
|
-
return `${where}字符串不符合要求`;
|
|
322
|
-
}
|
|
323
|
-
case "invalid_enum_value": {
|
|
324
|
-
const opts = Array.isArray(issue.options) ? issue.options.join("/") : String(issue.options);
|
|
325
|
-
return `${where}不在允许取值范围(${opts})内`;
|
|
326
|
-
}
|
|
327
|
-
case "invalid_union":
|
|
328
|
-
case "invalid_union_discriminator": {
|
|
329
|
-
return `${where}不符合联合类型约束`;
|
|
330
|
-
}
|
|
331
|
-
case "custom": {
|
|
332
|
-
return `${where}${issue.message || "不符合约束"}`;
|
|
333
|
-
}
|
|
334
|
-
default: {
|
|
335
|
-
return `${where}${issue.message || "参数不合法"}`;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function toCNType(t) {
|
|
340
|
-
const map = {
|
|
341
|
-
string: "字符串",
|
|
342
|
-
number: "数字",
|
|
343
|
-
boolean: "布尔",
|
|
344
|
-
bigint: "大整数",
|
|
345
|
-
date: "日期",
|
|
346
|
-
undefined: "未定义",
|
|
347
|
-
null: "空",
|
|
348
|
-
object: "对象",
|
|
349
|
-
array: "数组",
|
|
350
|
-
};
|
|
351
|
-
return map[String(t)] ?? String(t);
|
|
352
|
-
}
|
|
353
|
-
export function buildGuidedResponse(structuredContent, suggestions) {
|
|
354
|
-
const payload = {
|
|
355
|
-
...structuredContent,
|
|
356
|
-
};
|
|
357
|
-
if (suggestions && suggestions.length > 0) {
|
|
358
|
-
payload.next = suggestions;
|
|
359
|
-
}
|
|
360
|
-
return makeToolResponse(payload);
|
|
361
|
-
}
|
|
362
|
-
export async function getUserSetting(db, key) {
|
|
363
|
-
const row = db
|
|
364
|
-
.prepare("SELECT json_extract(doc, '$.value') AS value FROM user_data WHERE json_extract(doc, '$.key') = ?")
|
|
365
|
-
.get(key);
|
|
366
|
-
if (!row)
|
|
367
|
-
return undefined;
|
|
368
|
-
if (typeof row.value === "string") {
|
|
369
|
-
return (safeJsonParse(row.value) ?? row.value);
|
|
370
|
-
}
|
|
371
|
-
return row.value;
|
|
372
|
-
}
|
|
373
|
-
export async function getDateFormatting(db) {
|
|
374
|
-
const format = await getUserSetting(db, "dateFormatting");
|
|
375
|
-
if (typeof format === "string" && format.trim()) {
|
|
376
|
-
return normalizeDateFormat(format.trim());
|
|
377
|
-
}
|
|
378
|
-
return undefined;
|
|
379
|
-
}
|
|
380
|
-
export function formatDateWithPattern(date, pattern) {
|
|
381
|
-
try {
|
|
382
|
-
return formatDate(date, pattern);
|
|
383
|
-
}
|
|
384
|
-
catch (_) {
|
|
385
|
-
return formatDate(date, "yyyy/MM/dd");
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
function normalizeDateFormat(format) {
|
|
389
|
-
return format
|
|
390
|
-
.replace(/YYYY/g, "yyyy")
|
|
391
|
-
.replace(/YY/g, "yy")
|
|
392
|
-
.replace(/DD/g, "dd");
|
|
393
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { buildGuidedResponse, withResolvedDatabase, getDateFormatting, formatDateWithPattern, parseOrThrow, } from "./shared.js";
|
|
3
|
-
import { executeSearchRemOverview } from "./searchRemOverview.js";
|
|
4
|
-
import { executeOutlineRemSubtree } from "./outlineRemSubtree.js";
|
|
5
|
-
const inputShape = {
|
|
6
|
-
days: z
|
|
7
|
-
.number()
|
|
8
|
-
.int()
|
|
9
|
-
.min(1)
|
|
10
|
-
.max(30)
|
|
11
|
-
.default(7)
|
|
12
|
-
.describe("统计最近 N 天(默认 7)"),
|
|
13
|
-
includeEmpty: z.boolean().optional().describe("是否包含空行/空文档(默认 false)"),
|
|
14
|
-
expandReferences: z.boolean().optional().describe("是否展开 [[引用]] 文本(默认 true)"),
|
|
15
|
-
maxReferenceDepth: z
|
|
16
|
-
.number()
|
|
17
|
-
.int()
|
|
18
|
-
.min(0)
|
|
19
|
-
.max(5)
|
|
20
|
-
.optional()
|
|
21
|
-
.describe("引用展开的最大深度(默认 1)"),
|
|
22
|
-
maxLines: z
|
|
23
|
-
.number()
|
|
24
|
-
.int()
|
|
25
|
-
.min(1)
|
|
26
|
-
.max(200)
|
|
27
|
-
.optional()
|
|
28
|
-
.describe("单篇大纲的最大行数(默认 40)"),
|
|
29
|
-
dbPath: z.string().optional().describe("数据库文件路径(默认自动发现)"),
|
|
30
|
-
detail: z.boolean().optional().describe("是否返回逐日的节点详情(默认 false)"),
|
|
31
|
-
};
|
|
32
|
-
export const summarizeDailyNotesSchema = z.object(inputShape);
|
|
33
|
-
export async function executeSummarizeDailyNotes(params) {
|
|
34
|
-
const parsed = parseOrThrow(summarizeDailyNotesSchema, params, { label: "summarize_daily_notes" });
|
|
35
|
-
const days = parsed.days;
|
|
36
|
-
const includeEmpty = parsed.includeEmpty ?? false;
|
|
37
|
-
const expandReferences = parsed.expandReferences ?? true;
|
|
38
|
-
const maxReferenceDepth = parsed.maxReferenceDepth ?? 1;
|
|
39
|
-
const maxLines = parsed.maxLines ?? 40;
|
|
40
|
-
const detail = parsed.detail ?? false;
|
|
41
|
-
const detailResults = [];
|
|
42
|
-
const summaries = [];
|
|
43
|
-
const sections = [];
|
|
44
|
-
const referenceIndex = new Map();
|
|
45
|
-
const { info } = await withResolvedDatabase(parsed.dbPath, async (db) => {
|
|
46
|
-
const format = (await getDateFormatting(db)) ?? "yyyy/MM/dd";
|
|
47
|
-
for (let offset = 0; offset > -days; offset--) {
|
|
48
|
-
const targetDate = new Date();
|
|
49
|
-
targetDate.setDate(targetDate.getDate() + offset);
|
|
50
|
-
const dateString = formatDateWithPattern(targetDate, format);
|
|
51
|
-
const sectionLines = [`## ${dateString}`];
|
|
52
|
-
try {
|
|
53
|
-
const searchResult = await executeSearchRemOverview({
|
|
54
|
-
query: dateString,
|
|
55
|
-
dbPath: parsed.dbPath,
|
|
56
|
-
});
|
|
57
|
-
if (searchResult.count === 0 || searchResult.matches.length === 0) {
|
|
58
|
-
summaries.push({ date: dateString, status: "not_found" });
|
|
59
|
-
sectionLines.push("- 未找到对应笔记");
|
|
60
|
-
detailResults.push({ date: dateString });
|
|
61
|
-
sections.push(sectionLines.join("\n"));
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const remId = searchResult.matches[0]?.id;
|
|
65
|
-
if (!remId) {
|
|
66
|
-
summaries.push({ date: dateString, status: "error" });
|
|
67
|
-
sectionLines.push("- 未找到有效 Rem ID");
|
|
68
|
-
detailResults.push({
|
|
69
|
-
date: dateString,
|
|
70
|
-
error: "未找到有效 Rem ID",
|
|
71
|
-
});
|
|
72
|
-
sections.push(sectionLines.join("\n"));
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const outlineResult = await executeOutlineRemSubtree({
|
|
76
|
-
id: remId,
|
|
77
|
-
dbPath: parsed.dbPath,
|
|
78
|
-
includeEmpty,
|
|
79
|
-
expandReferences,
|
|
80
|
-
maxReferenceDepth,
|
|
81
|
-
startOffset: 0,
|
|
82
|
-
maxNodes: maxLines,
|
|
83
|
-
format: "markdown",
|
|
84
|
-
detail: true,
|
|
85
|
-
});
|
|
86
|
-
const markdown = outlineResult.markdown ?? `- ${dateString}`;
|
|
87
|
-
const lines = markdown.split("\n");
|
|
88
|
-
const trimmed = lines.slice(0, maxLines).join("\n");
|
|
89
|
-
const truncated = lines.length > maxLines || outlineResult.hasMore;
|
|
90
|
-
const nodes = Array.isArray(outlineResult.tree)
|
|
91
|
-
? (outlineResult.tree ?? [])
|
|
92
|
-
: [];
|
|
93
|
-
collectReferenceSummaries(referenceIndex, nodes, remId, outlineResult.title ?? remId);
|
|
94
|
-
summaries.push({
|
|
95
|
-
date: dateString,
|
|
96
|
-
status: truncated ? "truncated" : "ok",
|
|
97
|
-
remId,
|
|
98
|
-
title: outlineResult.title ?? remId,
|
|
99
|
-
lineCount: lines.length,
|
|
100
|
-
});
|
|
101
|
-
sectionLines.push(trimmed);
|
|
102
|
-
if (truncated) {
|
|
103
|
-
sectionLines.push(`> 内容已截断,请调用 outline_rem_subtree id=${remId} startOffset=${maxLines} 继续阅读`);
|
|
104
|
-
}
|
|
105
|
-
const detailEntry = {
|
|
106
|
-
date: dateString,
|
|
107
|
-
remId,
|
|
108
|
-
markdown: trimmed,
|
|
109
|
-
truncated,
|
|
110
|
-
lineCount: lines.length,
|
|
111
|
-
};
|
|
112
|
-
if (detail) {
|
|
113
|
-
detailEntry.nodes = nodes;
|
|
114
|
-
}
|
|
115
|
-
detailResults.push(detailEntry);
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
summaries.push({ date: dateString, status: "error" });
|
|
119
|
-
sectionLines.push(`- 读取失败:${String(error)}`);
|
|
120
|
-
detailResults.push({
|
|
121
|
-
date: dateString,
|
|
122
|
-
error: String(error),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
sections.push(sectionLines.join("\n"));
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
const aggregatedMarkdown = buildAggregateMarkdown(days, sections);
|
|
129
|
-
const response = {
|
|
130
|
-
dbPath: info.dbPath,
|
|
131
|
-
resolution: info.source,
|
|
132
|
-
dirName: info.dirName,
|
|
133
|
-
days,
|
|
134
|
-
maxLines,
|
|
135
|
-
count: summaries.length,
|
|
136
|
-
markdown: aggregatedMarkdown,
|
|
137
|
-
daysSummary: summaries,
|
|
138
|
-
referenceIndex: convertReferenceIndex(referenceIndex),
|
|
139
|
-
};
|
|
140
|
-
if (detail) {
|
|
141
|
-
Object.assign(response, { results: detailResults });
|
|
142
|
-
}
|
|
143
|
-
return response;
|
|
144
|
-
}
|
|
145
|
-
export function registerSummarizeDailyNotes(server) {
|
|
146
|
-
server.tool("summarize_daily_notes", `<usecase>汇总最近 N 天的每日笔记,自动匹配用户设定的日期格式。</usecase>
|
|
147
|
-
<instructions>
|
|
148
|
-
- 默认统计最近 7 天,可通过 days 调整范围。
|
|
149
|
-
- maxLines 控制单篇输出的最大行数;若被截断,结果会返回 truncated=true。
|
|
150
|
-
- truncated=true 时,可进一步调用 outline_rem_subtree id=<remId> startOffset=<maxLines> 继续阅读。
|
|
151
|
-
- 输出为 Markdown 大纲。若需要原始字段,可结合 inspect_rem_doc。
|
|
152
|
-
</instructions>`, inputShape, async (input) => {
|
|
153
|
-
const parsed = parseOrThrow(summarizeDailyNotesSchema, input, { label: "summarize_daily_notes" });
|
|
154
|
-
const result = await executeSummarizeDailyNotes(parsed);
|
|
155
|
-
const suggestions = [
|
|
156
|
-
"如需深入某一天的笔记,可继续调用 outline_rem_subtree 或 inspect_rem_doc",
|
|
157
|
-
];
|
|
158
|
-
const truncatedExists = Array.isArray(result.results)
|
|
159
|
-
? result.results.some((item) => item.truncated && item.remId)
|
|
160
|
-
: Array.isArray(result.daysSummary)
|
|
161
|
-
? result.daysSummary.some((item) => item.status === "truncated")
|
|
162
|
-
: false;
|
|
163
|
-
if (truncatedExists) {
|
|
164
|
-
suggestions.unshift("若看到 truncated=true,可使用 outline_rem_subtree id=<remId> startOffset=<maxLines> 继续阅读");
|
|
165
|
-
}
|
|
166
|
-
const guidance = `已汇总最近 ${result.days} 天的每日笔记(按用户日期格式,单条最多 ${result.maxLines} 行)。`;
|
|
167
|
-
return buildGuidedResponse({ guidance, ...result }, suggestions);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
function buildAggregateMarkdown(days, sections) {
|
|
171
|
-
const header = `# 最近 ${days} 天每日笔记`;
|
|
172
|
-
if (sections.length === 0) {
|
|
173
|
-
return `${header}\n\n- 未生成任何结果`;
|
|
174
|
-
}
|
|
175
|
-
return [header, ...sections].join("\n\n");
|
|
176
|
-
}
|
|
177
|
-
function collectReferenceSummaries(index, nodes, remId, remTitle) {
|
|
178
|
-
if (!nodes || nodes.length === 0)
|
|
179
|
-
return;
|
|
180
|
-
for (const node of nodes) {
|
|
181
|
-
if (!node.references || node.references.length === 0)
|
|
182
|
-
continue;
|
|
183
|
-
for (const refId of node.references) {
|
|
184
|
-
if (!refId)
|
|
185
|
-
continue;
|
|
186
|
-
const entry = ensureReferenceEntry(index, refId);
|
|
187
|
-
entry.totalOccurrences += 1;
|
|
188
|
-
if (entry.samples.length < 5) {
|
|
189
|
-
entry.samples.push({
|
|
190
|
-
remId,
|
|
191
|
-
remTitle,
|
|
192
|
-
nodeId: node.id,
|
|
193
|
-
text: node.text,
|
|
194
|
-
depth: node.depth,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function ensureReferenceEntry(index, id) {
|
|
201
|
-
let entry = index.get(id);
|
|
202
|
-
if (!entry) {
|
|
203
|
-
entry = {
|
|
204
|
-
id,
|
|
205
|
-
totalOccurrences: 0,
|
|
206
|
-
samples: [],
|
|
207
|
-
};
|
|
208
|
-
index.set(id, entry);
|
|
209
|
-
}
|
|
210
|
-
return entry;
|
|
211
|
-
}
|
|
212
|
-
function convertReferenceIndex(index) {
|
|
213
|
-
const result = {};
|
|
214
|
-
for (const [id, entry] of index.entries()) {
|
|
215
|
-
result[id] = {
|
|
216
|
-
totalOccurrences: entry.totalOccurrences,
|
|
217
|
-
samples: entry.samples,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
return result;
|
|
221
|
-
}
|