@mseep/affine-mcp-server 2.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/LICENSE +22 -0
- package/README.md +270 -0
- package/bin/affine-mcp +5 -0
- package/dist/auth.js +61 -0
- package/dist/cli.js +726 -0
- package/dist/config.js +178 -0
- package/dist/edgeless/layout.js +222 -0
- package/dist/graphqlClient.js +116 -0
- package/dist/httpAuth.js +147 -0
- package/dist/httpDiagnostics.js +38 -0
- package/dist/index.js +209 -0
- package/dist/markdown/parse.js +559 -0
- package/dist/markdown/render.js +227 -0
- package/dist/markdown/types.js +1 -0
- package/dist/oauth.js +154 -0
- package/dist/sse.js +261 -0
- package/dist/toolSurface.js +349 -0
- package/dist/tools/accessTokens.js +45 -0
- package/dist/tools/auth.js +18 -0
- package/dist/tools/blobStorage.js +136 -0
- package/dist/tools/comments.js +104 -0
- package/dist/tools/docs.js +7478 -0
- package/dist/tools/history.js +22 -0
- package/dist/tools/icons.js +125 -0
- package/dist/tools/notifications.js +79 -0
- package/dist/tools/organize.js +1145 -0
- package/dist/tools/properties.js +426 -0
- package/dist/tools/user.js +13 -0
- package/dist/tools/userCRUD.js +77 -0
- package/dist/tools/workspaces.js +322 -0
- package/dist/util/explorerIcon.js +95 -0
- package/dist/util/mcp.js +28 -0
- package/dist/ws.js +113 -0
- package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
- package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
- package/docs/client-setup.md +174 -0
- package/docs/configuration-and-deployment.md +265 -0
- package/docs/edgeless-canvas-cookbook.md +226 -0
- package/docs/getting-started.md +229 -0
- package/docs/tool-reference.md +200 -0
- package/docs/workflow-recipes.md +147 -0
- package/package.json +118 -0
- package/tool-manifest.json +99 -0
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as Y from "yjs";
|
|
4
|
+
import { generateKeyBetween } from "fractional-indexing";
|
|
5
|
+
import { text } from "../util/mcp.js";
|
|
6
|
+
import { connectWorkspaceSocket, joinWorkspace, loadDoc, pushDocUpdate, wsUrlFromGraphQLEndpoint, } from "../ws.js";
|
|
7
|
+
const WorkspaceId = z.string().min(1, "workspaceId required");
|
|
8
|
+
const DocId = z.string().min(1, "docId required");
|
|
9
|
+
const CollectionId = z.string().min(1, "collectionId required");
|
|
10
|
+
const FolderId = z.string().min(1, "folderId required");
|
|
11
|
+
const OrganizeNodeId = z.string().min(1, "nodeId required");
|
|
12
|
+
const FolderName = z.string().trim().min(1, "name required");
|
|
13
|
+
const CollectionRuleFieldSchema = z.enum(["title", "tag", "docId"]);
|
|
14
|
+
const CollectionRuleOperatorSchema = z.enum(["contains", "equals", "startsWith", "in"]);
|
|
15
|
+
const CollectionRuleSchema = z.object({
|
|
16
|
+
field: CollectionRuleFieldSchema,
|
|
17
|
+
operator: CollectionRuleOperatorSchema,
|
|
18
|
+
value: z.union([z.string(), z.array(z.string())]),
|
|
19
|
+
});
|
|
20
|
+
const CollectionRulesSchema = z.object({
|
|
21
|
+
match: z.enum(["all", "any"]).optional(),
|
|
22
|
+
filters: z.array(CollectionRuleSchema),
|
|
23
|
+
});
|
|
24
|
+
function generateId(length = 21) {
|
|
25
|
+
const chars = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
|
|
26
|
+
const bytes = randomBytes(length);
|
|
27
|
+
let result = "";
|
|
28
|
+
for (let i = 0; i < length; i += 1) {
|
|
29
|
+
result += chars[bytes[i] % chars.length];
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function hasSamePrefix(a, b) {
|
|
34
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
35
|
+
}
|
|
36
|
+
// Adapted from AFFiNE's packages/common/infra/src/utils/fractional-indexing.ts
|
|
37
|
+
function generateFractionalIndexingKeyBetween(a, b) {
|
|
38
|
+
const randomSize = 32;
|
|
39
|
+
function postfix(length = randomSize) {
|
|
40
|
+
const chars = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
41
|
+
const values = randomBytes(length);
|
|
42
|
+
let result = "";
|
|
43
|
+
for (let i = 0; i < length; i += 1) {
|
|
44
|
+
result += chars[values[i] % chars.length];
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function subkey(key) {
|
|
49
|
+
if (key === null) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (key.length <= randomSize + 1) {
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
return key.substring(0, key.length - randomSize - 1);
|
|
56
|
+
}
|
|
57
|
+
const aSubkey = subkey(a);
|
|
58
|
+
const bSubkey = subkey(b);
|
|
59
|
+
if (aSubkey === null && bSubkey === null) {
|
|
60
|
+
return generateKeyBetween(null, null) + "0" + postfix();
|
|
61
|
+
}
|
|
62
|
+
if (aSubkey === null && bSubkey !== null) {
|
|
63
|
+
return generateKeyBetween(null, bSubkey) + "0" + postfix();
|
|
64
|
+
}
|
|
65
|
+
if (bSubkey === null && aSubkey !== null) {
|
|
66
|
+
return generateKeyBetween(aSubkey, null) + "0" + postfix();
|
|
67
|
+
}
|
|
68
|
+
if (aSubkey !== null && bSubkey !== null) {
|
|
69
|
+
if (hasSamePrefix(aSubkey, bSubkey) && a !== null && b !== null) {
|
|
70
|
+
return generateKeyBetween(a, b) + "0" + postfix();
|
|
71
|
+
}
|
|
72
|
+
return generateKeyBetween(aSubkey, bSubkey) + "0" + postfix();
|
|
73
|
+
}
|
|
74
|
+
throw new Error("Unreachable fractional indexing state");
|
|
75
|
+
}
|
|
76
|
+
function normalizeCollection(value) {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const collection = value;
|
|
81
|
+
if (typeof collection.id !== "string" || typeof collection.name !== "string") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const allowList = Array.isArray(collection.allowList)
|
|
85
|
+
? collection.allowList.filter((entry) => typeof entry === "string")
|
|
86
|
+
: [];
|
|
87
|
+
const rules = normalizeCollectionRules(collection.rules);
|
|
88
|
+
return {
|
|
89
|
+
id: collection.id,
|
|
90
|
+
name: collection.name,
|
|
91
|
+
rules,
|
|
92
|
+
allowList,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function normalizeOrganizeNode(value) {
|
|
96
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const raw = value;
|
|
100
|
+
if (typeof raw.id !== "string" ||
|
|
101
|
+
typeof raw.type !== "string" ||
|
|
102
|
+
typeof raw.data !== "string" ||
|
|
103
|
+
typeof raw.index !== "string") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!["folder", "doc", "tag", "collection"].includes(raw.type)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
id: raw.id,
|
|
111
|
+
parentId: raw.parentId === null || typeof raw.parentId === "string" ? raw.parentId : null,
|
|
112
|
+
type: raw.type,
|
|
113
|
+
data: raw.data,
|
|
114
|
+
index: raw.index,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function specialWorkspaceDbDocId(workspaceId, tableName) {
|
|
118
|
+
return `db$${workspaceId}$${tableName}`;
|
|
119
|
+
}
|
|
120
|
+
function isDeletedRecord(record) {
|
|
121
|
+
return record.get("$$DELETED") === true || record.size === 0;
|
|
122
|
+
}
|
|
123
|
+
function ensureRecord(doc, id) {
|
|
124
|
+
return doc.getMap(id);
|
|
125
|
+
}
|
|
126
|
+
function deleteRecord(record, keepId = true) {
|
|
127
|
+
const keys = Array.from(record.keys());
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
if (keepId && key === "id") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
record.delete(key);
|
|
133
|
+
}
|
|
134
|
+
record.set("$$DELETED", true);
|
|
135
|
+
}
|
|
136
|
+
function readCollections(array) {
|
|
137
|
+
const collections = [];
|
|
138
|
+
for (let i = 0; i < array.length; i += 1) {
|
|
139
|
+
const normalized = normalizeCollection(array.get(i));
|
|
140
|
+
if (normalized) {
|
|
141
|
+
collections.push(normalized);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return collections;
|
|
145
|
+
}
|
|
146
|
+
function findCollectionIndex(array, id) {
|
|
147
|
+
for (let i = 0; i < array.length; i += 1) {
|
|
148
|
+
const normalized = normalizeCollection(array.get(i));
|
|
149
|
+
if (normalized?.id === id) {
|
|
150
|
+
return i;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return -1;
|
|
154
|
+
}
|
|
155
|
+
function readOrganizeNodes(doc) {
|
|
156
|
+
const nodes = [];
|
|
157
|
+
for (const key of doc.share.keys()) {
|
|
158
|
+
if (!doc.share.has(key)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const record = doc.getMap(key);
|
|
162
|
+
if (!(record instanceof Y.Map) || isDeletedRecord(record)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const normalized = normalizeOrganizeNode(record.toJSON());
|
|
166
|
+
if (normalized) {
|
|
167
|
+
nodes.push(normalized);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return nodes;
|
|
171
|
+
}
|
|
172
|
+
function organizeNodeMap(nodes) {
|
|
173
|
+
return new Map(nodes.map(node => [node.id, node]));
|
|
174
|
+
}
|
|
175
|
+
function getYMap(target, key) {
|
|
176
|
+
const value = target.get(key);
|
|
177
|
+
return value instanceof Y.Map ? value : null;
|
|
178
|
+
}
|
|
179
|
+
function getStringArray(value) {
|
|
180
|
+
if (!(value instanceof Y.Array)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const values = [];
|
|
184
|
+
value.forEach((entry) => {
|
|
185
|
+
if (typeof entry === "string") {
|
|
186
|
+
values.push(entry);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return values;
|
|
190
|
+
}
|
|
191
|
+
function getTagArray(target, key = "tags") {
|
|
192
|
+
const value = target.get(key);
|
|
193
|
+
return value instanceof Y.Array ? value : null;
|
|
194
|
+
}
|
|
195
|
+
function getWorkspaceTagOptionsArray(meta) {
|
|
196
|
+
const properties = getYMap(meta, "properties");
|
|
197
|
+
if (!properties) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const tags = getYMap(properties, "tags");
|
|
201
|
+
if (!tags) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const options = tags.get("options");
|
|
205
|
+
return options instanceof Y.Array ? options : null;
|
|
206
|
+
}
|
|
207
|
+
function getWorkspaceTagOptions(meta) {
|
|
208
|
+
const options = getWorkspaceTagOptionsArray(meta);
|
|
209
|
+
if (!options) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const parsed = [];
|
|
213
|
+
options.forEach((raw) => {
|
|
214
|
+
let id;
|
|
215
|
+
let value;
|
|
216
|
+
if (raw instanceof Y.Map) {
|
|
217
|
+
id = raw.get("id");
|
|
218
|
+
value = raw.get("value");
|
|
219
|
+
}
|
|
220
|
+
else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
221
|
+
const record = raw;
|
|
222
|
+
id = record.id;
|
|
223
|
+
value = record.value;
|
|
224
|
+
}
|
|
225
|
+
if (typeof id !== "string" || typeof value !== "string") {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const normalizedId = id.trim();
|
|
229
|
+
const normalizedValue = value.trim();
|
|
230
|
+
if (!normalizedId || !normalizedValue) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
parsed.push({ id: normalizedId, value: normalizedValue });
|
|
234
|
+
});
|
|
235
|
+
return parsed;
|
|
236
|
+
}
|
|
237
|
+
function getWorkspaceTagOptionMaps(meta) {
|
|
238
|
+
const options = getWorkspaceTagOptions(meta);
|
|
239
|
+
const byId = new Map();
|
|
240
|
+
const byValueLower = new Map();
|
|
241
|
+
for (const option of options) {
|
|
242
|
+
if (!byId.has(option.id)) {
|
|
243
|
+
byId.set(option.id, option);
|
|
244
|
+
}
|
|
245
|
+
const key = option.value.toLocaleLowerCase();
|
|
246
|
+
if (!byValueLower.has(key)) {
|
|
247
|
+
byValueLower.set(key, option);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { byId, byValueLower };
|
|
251
|
+
}
|
|
252
|
+
function resolveTagLabels(tagEntries, byId) {
|
|
253
|
+
const deduped = new Set();
|
|
254
|
+
const resolved = [];
|
|
255
|
+
for (const entry of tagEntries) {
|
|
256
|
+
const raw = entry.trim();
|
|
257
|
+
if (!raw) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const option = byId.get(raw);
|
|
261
|
+
const label = (option ? option.value : raw).trim();
|
|
262
|
+
if (!label) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const dedupeKey = label.toLocaleLowerCase();
|
|
266
|
+
if (deduped.has(dedupeKey)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
deduped.add(dedupeKey);
|
|
270
|
+
resolved.push(label);
|
|
271
|
+
}
|
|
272
|
+
return resolved;
|
|
273
|
+
}
|
|
274
|
+
function getWorkspacePageEntries(meta, tagOptionById) {
|
|
275
|
+
const pages = meta.get("pages");
|
|
276
|
+
if (!(pages instanceof Y.Array)) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const entries = [];
|
|
280
|
+
pages.forEach((value) => {
|
|
281
|
+
if (!(value instanceof Y.Map)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const id = value.get("id");
|
|
285
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const title = value.get("title");
|
|
289
|
+
const createDate = value.get("createDate");
|
|
290
|
+
const updatedDate = value.get("updatedDate");
|
|
291
|
+
entries.push({
|
|
292
|
+
id,
|
|
293
|
+
title: typeof title === "string" ? title : null,
|
|
294
|
+
createDate: typeof createDate === "number" ? createDate : null,
|
|
295
|
+
updatedDate: typeof updatedDate === "number" ? updatedDate : null,
|
|
296
|
+
tags: resolveTagLabels(getStringArray(getTagArray(value)), tagOptionById),
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
return entries;
|
|
300
|
+
}
|
|
301
|
+
function normalizeCollectionRuleFilter(value) {
|
|
302
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const filter = value;
|
|
306
|
+
if (filter.field !== "title" && filter.field !== "tag" && filter.field !== "docId") {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const allowedOperators = filter.field === "docId"
|
|
310
|
+
? ["equals", "in"]
|
|
311
|
+
: filter.field === "title"
|
|
312
|
+
? ["contains", "equals", "startsWith"]
|
|
313
|
+
: ["contains", "equals"];
|
|
314
|
+
if (!allowedOperators.includes(filter.operator)) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (filter.operator === "in") {
|
|
318
|
+
if (!Array.isArray(filter.value)) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const values = filter.value
|
|
322
|
+
.filter((entry) => typeof entry === "string")
|
|
323
|
+
.map(entry => entry.trim())
|
|
324
|
+
.filter(Boolean);
|
|
325
|
+
if (values.length === 0) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
field: filter.field,
|
|
330
|
+
operator: "in",
|
|
331
|
+
value: Array.from(new Set(values)),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (typeof filter.value !== "string") {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const valueText = filter.value.trim();
|
|
338
|
+
if (!valueText) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
field: filter.field,
|
|
343
|
+
operator: filter.operator,
|
|
344
|
+
value: valueText,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function normalizeCollectionRules(value) {
|
|
348
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
349
|
+
return { match: "all", filters: [] };
|
|
350
|
+
}
|
|
351
|
+
const rules = value;
|
|
352
|
+
const match = rules.match === "any" ? "any" : "all";
|
|
353
|
+
const filters = Array.isArray(rules.filters)
|
|
354
|
+
? rules.filters
|
|
355
|
+
.map(normalizeCollectionRuleFilter)
|
|
356
|
+
.filter((entry) => entry !== null)
|
|
357
|
+
: [];
|
|
358
|
+
return { match, filters };
|
|
359
|
+
}
|
|
360
|
+
function matchesCollectionRule(doc, filter) {
|
|
361
|
+
const title = (doc.title ?? "").trim();
|
|
362
|
+
const lowerTitle = title.toLocaleLowerCase();
|
|
363
|
+
const tagValues = doc.tags.map(tag => tag.toLocaleLowerCase());
|
|
364
|
+
switch (filter.field) {
|
|
365
|
+
case "title": {
|
|
366
|
+
const target = filter.value.toString().toLocaleLowerCase();
|
|
367
|
+
if (filter.operator === "contains") {
|
|
368
|
+
return lowerTitle.includes(target);
|
|
369
|
+
}
|
|
370
|
+
if (filter.operator === "startsWith") {
|
|
371
|
+
return lowerTitle.startsWith(target);
|
|
372
|
+
}
|
|
373
|
+
return lowerTitle === target;
|
|
374
|
+
}
|
|
375
|
+
case "tag": {
|
|
376
|
+
const target = filter.value.toString().toLocaleLowerCase();
|
|
377
|
+
if (filter.operator === "contains") {
|
|
378
|
+
return tagValues.some(tag => tag.includes(target));
|
|
379
|
+
}
|
|
380
|
+
return tagValues.some(tag => tag === target);
|
|
381
|
+
}
|
|
382
|
+
case "docId": {
|
|
383
|
+
if (filter.operator === "in") {
|
|
384
|
+
return Array.isArray(filter.value)
|
|
385
|
+
? filter.value.some(value => value === doc.id)
|
|
386
|
+
: false;
|
|
387
|
+
}
|
|
388
|
+
return doc.id === filter.value;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function matchesCollectionRules(doc, rules) {
|
|
393
|
+
if (rules.filters.length === 0) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const matches = rules.filters.map(filter => matchesCollectionRule(doc, filter));
|
|
397
|
+
return rules.match === "any" ? matches.some(Boolean) : matches.every(Boolean);
|
|
398
|
+
}
|
|
399
|
+
function sortOrganizeNodes(nodes) {
|
|
400
|
+
return [...nodes].sort((left, right) => {
|
|
401
|
+
const parentCompare = (left.parentId ?? "").localeCompare(right.parentId ?? "");
|
|
402
|
+
if (parentCompare !== 0) {
|
|
403
|
+
return parentCompare;
|
|
404
|
+
}
|
|
405
|
+
const indexCompare = left.index.localeCompare(right.index);
|
|
406
|
+
if (indexCompare !== 0) {
|
|
407
|
+
return indexCompare;
|
|
408
|
+
}
|
|
409
|
+
return left.id.localeCompare(right.id);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function ensureFolderParent(nodes, parentId) {
|
|
413
|
+
if (parentId === null) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const parent = nodes.get(parentId);
|
|
417
|
+
if (!parent || parent.type !== "folder") {
|
|
418
|
+
throw new Error(`Parent folder '${parentId}' was not found.`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function ensureNodeIsFolder(nodes, nodeId) {
|
|
422
|
+
const node = nodes.get(nodeId);
|
|
423
|
+
if (!node || node.type !== "folder") {
|
|
424
|
+
throw new Error(`Folder '${nodeId}' was not found.`);
|
|
425
|
+
}
|
|
426
|
+
return node;
|
|
427
|
+
}
|
|
428
|
+
function isAncestor(nodes, childId, ancestorId) {
|
|
429
|
+
if (childId === ancestorId) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
const seen = new Set([childId]);
|
|
433
|
+
let current = childId;
|
|
434
|
+
while (true) {
|
|
435
|
+
const node = nodes.get(current);
|
|
436
|
+
if (!node?.parentId) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
current = node.parentId;
|
|
440
|
+
if (seen.has(current)) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
seen.add(current);
|
|
444
|
+
if (current === ancestorId) {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function nextOrganizeIndex(nodes, parentId) {
|
|
450
|
+
const siblings = nodes
|
|
451
|
+
.filter(node => node.parentId === parentId)
|
|
452
|
+
.sort((left, right) => left.index.localeCompare(right.index));
|
|
453
|
+
const last = siblings.at(-1);
|
|
454
|
+
return generateFractionalIndexingKeyBetween(last?.index ?? null, null);
|
|
455
|
+
}
|
|
456
|
+
async function loadFoldersDoc(socket, workspaceId) {
|
|
457
|
+
const docId = specialWorkspaceDbDocId(workspaceId, "folders");
|
|
458
|
+
const snapshot = await loadDoc(socket, workspaceId, docId);
|
|
459
|
+
const doc = new Y.Doc();
|
|
460
|
+
if (snapshot.missing) {
|
|
461
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
462
|
+
}
|
|
463
|
+
return { docId, doc, snapshot };
|
|
464
|
+
}
|
|
465
|
+
async function saveFoldersDoc(socket, workspaceId, docId, doc) {
|
|
466
|
+
const update = Y.encodeStateAsUpdate(doc);
|
|
467
|
+
await pushDocUpdate(socket, workspaceId, docId, Buffer.from(update).toString("base64"));
|
|
468
|
+
}
|
|
469
|
+
export async function addOrganizeLinkToFolder(socket, workspaceId, { folderId, type, targetId, index, }) {
|
|
470
|
+
const { docId, doc } = await loadFoldersDoc(socket, workspaceId);
|
|
471
|
+
const nodes = readOrganizeNodes(doc);
|
|
472
|
+
const nodeMap = organizeNodeMap(nodes);
|
|
473
|
+
ensureNodeIsFolder(nodeMap, folderId);
|
|
474
|
+
const linkId = generateId();
|
|
475
|
+
const nextIndex = index ?? nextOrganizeIndex(nodes, folderId);
|
|
476
|
+
const record = ensureRecord(doc, linkId);
|
|
477
|
+
record.set("id", linkId);
|
|
478
|
+
record.set("type", type);
|
|
479
|
+
record.set("data", targetId);
|
|
480
|
+
record.set("parentId", folderId);
|
|
481
|
+
record.set("index", nextIndex);
|
|
482
|
+
record.delete("$$DELETED");
|
|
483
|
+
await saveFoldersDoc(socket, workspaceId, docId, doc);
|
|
484
|
+
return {
|
|
485
|
+
id: linkId,
|
|
486
|
+
parentId: folderId,
|
|
487
|
+
type,
|
|
488
|
+
data: targetId,
|
|
489
|
+
index: nextIndex,
|
|
490
|
+
storageDocId: docId,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
export function registerOrganizeTools(server, gql, defaults) {
|
|
494
|
+
async function getSocketContext() {
|
|
495
|
+
const endpoint = gql.endpoint;
|
|
496
|
+
const cookie = gql.cookie;
|
|
497
|
+
const bearer = gql.bearer;
|
|
498
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
499
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
500
|
+
return { socket };
|
|
501
|
+
}
|
|
502
|
+
async function loadWorkspaceRootDoc(socket, workspaceId) {
|
|
503
|
+
const snapshot = await loadDoc(socket, workspaceId, workspaceId);
|
|
504
|
+
const doc = new Y.Doc();
|
|
505
|
+
if (snapshot.missing) {
|
|
506
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
507
|
+
}
|
|
508
|
+
return { doc, snapshot };
|
|
509
|
+
}
|
|
510
|
+
async function saveWorkspaceRootDoc(socket, workspaceId, doc) {
|
|
511
|
+
const update = Y.encodeStateAsUpdate(doc);
|
|
512
|
+
await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(update).toString("base64"));
|
|
513
|
+
}
|
|
514
|
+
function sleep(ms) {
|
|
515
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
516
|
+
}
|
|
517
|
+
async function listWorkspaceDocsForCollectionRules(socket, workspaceId) {
|
|
518
|
+
const { doc } = await loadWorkspaceRootDoc(socket, workspaceId);
|
|
519
|
+
const meta = doc.getMap("meta");
|
|
520
|
+
const tagOptionById = getWorkspaceTagOptionMaps(meta).byId;
|
|
521
|
+
const pageEntries = getWorkspacePageEntries(meta, tagOptionById);
|
|
522
|
+
const docs = [];
|
|
523
|
+
for (const entry of pageEntries) {
|
|
524
|
+
let mergedTitle = entry.title;
|
|
525
|
+
let mergedTags = entry.tags;
|
|
526
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
527
|
+
const snapshot = await loadDoc(socket, workspaceId, entry.id);
|
|
528
|
+
if (snapshot.missing) {
|
|
529
|
+
const pageDoc = new Y.Doc();
|
|
530
|
+
Y.applyUpdate(pageDoc, Buffer.from(snapshot.missing, "base64"));
|
|
531
|
+
const pageMeta = pageDoc.getMap("meta");
|
|
532
|
+
const docTitle = pageMeta.get("title");
|
|
533
|
+
if (typeof docTitle === "string" && docTitle.trim().length > 0) {
|
|
534
|
+
mergedTitle = docTitle;
|
|
535
|
+
}
|
|
536
|
+
const docTags = getStringArray(getTagArray(pageMeta));
|
|
537
|
+
const resolvedDocTags = resolveTagLabels(docTags, tagOptionById);
|
|
538
|
+
if (resolvedDocTags.length > 0) {
|
|
539
|
+
mergedTags = resolvedDocTags;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (mergedTitle || mergedTags.length > 0 || attempt === 4) {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
await sleep(150);
|
|
546
|
+
}
|
|
547
|
+
docs.push({
|
|
548
|
+
id: entry.id,
|
|
549
|
+
title: mergedTitle,
|
|
550
|
+
tags: mergedTags,
|
|
551
|
+
createDate: entry.createDate,
|
|
552
|
+
updatedDate: entry.updatedDate,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return docs;
|
|
556
|
+
}
|
|
557
|
+
async function createFolderInternal({ workspaceId, name, parentId, index, }) {
|
|
558
|
+
const resolvedParentId = parentId ?? null;
|
|
559
|
+
const { socket } = await getSocketContext();
|
|
560
|
+
try {
|
|
561
|
+
await joinWorkspace(socket, workspaceId);
|
|
562
|
+
const { docId, doc } = await loadFoldersDoc(socket, workspaceId);
|
|
563
|
+
const nodes = readOrganizeNodes(doc);
|
|
564
|
+
const nodeMap = organizeNodeMap(nodes);
|
|
565
|
+
ensureFolderParent(nodeMap, resolvedParentId);
|
|
566
|
+
const folderId = generateId();
|
|
567
|
+
const folderIndex = index ?? nextOrganizeIndex(nodes, resolvedParentId);
|
|
568
|
+
const record = ensureRecord(doc, folderId);
|
|
569
|
+
record.set("id", folderId);
|
|
570
|
+
record.set("type", "folder");
|
|
571
|
+
record.set("data", name);
|
|
572
|
+
record.set("parentId", resolvedParentId);
|
|
573
|
+
record.set("index", folderIndex);
|
|
574
|
+
record.delete("$$DELETED");
|
|
575
|
+
await saveFoldersDoc(socket, workspaceId, docId, doc);
|
|
576
|
+
return {
|
|
577
|
+
id: folderId,
|
|
578
|
+
parentId: resolvedParentId,
|
|
579
|
+
type: "folder",
|
|
580
|
+
data: name,
|
|
581
|
+
index: folderIndex,
|
|
582
|
+
storageDocId: docId,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
socket.disconnect();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function updateCollectionRulesInternal({ workspaceId, collectionId, rules, }) {
|
|
590
|
+
const { socket } = await getSocketContext();
|
|
591
|
+
try {
|
|
592
|
+
await joinWorkspace(socket, workspaceId);
|
|
593
|
+
const { doc } = await loadWorkspaceRootDoc(socket, workspaceId);
|
|
594
|
+
const setting = doc.getMap("setting");
|
|
595
|
+
const current = setting.get("collections");
|
|
596
|
+
if (!(current instanceof Y.Array)) {
|
|
597
|
+
throw new Error("Workspace does not contain any collections.");
|
|
598
|
+
}
|
|
599
|
+
const index = findCollectionIndex(current, collectionId);
|
|
600
|
+
if (index < 0) {
|
|
601
|
+
throw new Error(`Collection '${collectionId}' was not found.`);
|
|
602
|
+
}
|
|
603
|
+
const previous = normalizeCollection(current.get(index));
|
|
604
|
+
if (!previous) {
|
|
605
|
+
throw new Error(`Collection '${collectionId}' is malformed.`);
|
|
606
|
+
}
|
|
607
|
+
const docs = await listWorkspaceDocsForCollectionRules(socket, workspaceId);
|
|
608
|
+
const allowList = docs.filter(doc => matchesCollectionRules(doc, rules)).map(doc => doc.id);
|
|
609
|
+
const next = {
|
|
610
|
+
...previous,
|
|
611
|
+
rules,
|
|
612
|
+
allowList,
|
|
613
|
+
};
|
|
614
|
+
doc.transact(() => {
|
|
615
|
+
current.delete(index, 1);
|
|
616
|
+
current.insert(index, [next]);
|
|
617
|
+
});
|
|
618
|
+
await saveWorkspaceRootDoc(socket, workspaceId, doc);
|
|
619
|
+
return {
|
|
620
|
+
collection: next,
|
|
621
|
+
matchedDocIds: allowList,
|
|
622
|
+
matchedCount: allowList.length,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
socket.disconnect();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function requireWorkspaceId(workspaceId) {
|
|
630
|
+
const resolved = workspaceId || defaults.workspaceId;
|
|
631
|
+
if (!resolved) {
|
|
632
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
633
|
+
}
|
|
634
|
+
return resolved;
|
|
635
|
+
}
|
|
636
|
+
const listCollectionsHandler = async ({ workspaceId }) => {
|
|
637
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
638
|
+
const { socket } = await getSocketContext();
|
|
639
|
+
try {
|
|
640
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
641
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
642
|
+
const setting = doc.getMap("setting");
|
|
643
|
+
const current = setting.get("collections");
|
|
644
|
+
const collections = current instanceof Y.Array ? readCollections(current) : [];
|
|
645
|
+
return text([...collections].sort((left, right) => left.name.localeCompare(right.name)));
|
|
646
|
+
}
|
|
647
|
+
finally {
|
|
648
|
+
socket.disconnect();
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
server.registerTool("list_collections", {
|
|
652
|
+
title: "List Collections",
|
|
653
|
+
description: "List AFFiNE collections stored in the workspace sidebar.",
|
|
654
|
+
inputSchema: {
|
|
655
|
+
workspaceId: WorkspaceId.optional(),
|
|
656
|
+
},
|
|
657
|
+
}, listCollectionsHandler);
|
|
658
|
+
const getCollectionHandler = async ({ workspaceId, collectionId }) => {
|
|
659
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
660
|
+
const { socket } = await getSocketContext();
|
|
661
|
+
try {
|
|
662
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
663
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
664
|
+
const setting = doc.getMap("setting");
|
|
665
|
+
const current = setting.get("collections");
|
|
666
|
+
const collections = current instanceof Y.Array ? readCollections(current) : [];
|
|
667
|
+
const collection = collections.find(entry => entry.id === collectionId);
|
|
668
|
+
if (!collection) {
|
|
669
|
+
throw new Error(`Collection '${collectionId}' was not found.`);
|
|
670
|
+
}
|
|
671
|
+
return text(collection);
|
|
672
|
+
}
|
|
673
|
+
finally {
|
|
674
|
+
socket.disconnect();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
server.registerTool("get_collection", {
|
|
678
|
+
title: "Get Collection",
|
|
679
|
+
description: "Get an AFFiNE collection by id.",
|
|
680
|
+
inputSchema: {
|
|
681
|
+
workspaceId: WorkspaceId.optional(),
|
|
682
|
+
collectionId: CollectionId,
|
|
683
|
+
},
|
|
684
|
+
}, getCollectionHandler);
|
|
685
|
+
const createCollectionHandler = async ({ workspaceId, name, rules, }) => {
|
|
686
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
687
|
+
const { socket } = await getSocketContext();
|
|
688
|
+
try {
|
|
689
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
690
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
691
|
+
const setting = doc.getMap("setting");
|
|
692
|
+
let current = setting.get("collections");
|
|
693
|
+
if (!(current instanceof Y.Array)) {
|
|
694
|
+
current = new Y.Array();
|
|
695
|
+
setting.set("collections", current);
|
|
696
|
+
}
|
|
697
|
+
const collection = {
|
|
698
|
+
id: generateId(),
|
|
699
|
+
name,
|
|
700
|
+
rules: rules ? normalizeCollectionRules(rules) : { match: "all", filters: [] },
|
|
701
|
+
allowList: [],
|
|
702
|
+
};
|
|
703
|
+
current.push([collection]);
|
|
704
|
+
await saveWorkspaceRootDoc(socket, resolvedWorkspaceId, doc);
|
|
705
|
+
return text(collection);
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
socket.disconnect();
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
server.registerTool("create_collection", {
|
|
712
|
+
title: "Create Collection",
|
|
713
|
+
description: "Create a new AFFiNE collection in the workspace sidebar.",
|
|
714
|
+
inputSchema: {
|
|
715
|
+
workspaceId: WorkspaceId.optional(),
|
|
716
|
+
name: FolderName.describe("Collection name"),
|
|
717
|
+
rules: CollectionRulesSchema.optional().describe("Optional rule set to initialize the collection with."),
|
|
718
|
+
},
|
|
719
|
+
}, createCollectionHandler);
|
|
720
|
+
const updateCollectionRulesHandler = async ({ workspaceId, collectionId, rules, }) => {
|
|
721
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
722
|
+
const normalizedRules = normalizeCollectionRules(rules);
|
|
723
|
+
const result = await updateCollectionRulesInternal({
|
|
724
|
+
workspaceId: resolvedWorkspaceId,
|
|
725
|
+
collectionId,
|
|
726
|
+
rules: normalizedRules,
|
|
727
|
+
});
|
|
728
|
+
return text({
|
|
729
|
+
workspaceId: resolvedWorkspaceId,
|
|
730
|
+
collectionId,
|
|
731
|
+
rules: normalizedRules,
|
|
732
|
+
allowList: result.collection.allowList,
|
|
733
|
+
matchedDocIds: result.matchedDocIds,
|
|
734
|
+
matchedCount: result.matchedCount,
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
server.registerTool("update_collection_rules", {
|
|
738
|
+
title: "Update Collection Rules",
|
|
739
|
+
description: "Replace a collection's rules and rebuild its allow-list from workspace docs.",
|
|
740
|
+
inputSchema: {
|
|
741
|
+
workspaceId: WorkspaceId.optional(),
|
|
742
|
+
collectionId: CollectionId,
|
|
743
|
+
rules: CollectionRulesSchema.describe("Rule set used to rebuild the collection allow-list."),
|
|
744
|
+
},
|
|
745
|
+
}, updateCollectionRulesHandler);
|
|
746
|
+
const updateCollectionHandler = async ({ workspaceId, collectionId, name, }) => {
|
|
747
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
748
|
+
const { socket } = await getSocketContext();
|
|
749
|
+
try {
|
|
750
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
751
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
752
|
+
const setting = doc.getMap("setting");
|
|
753
|
+
const current = setting.get("collections");
|
|
754
|
+
if (!(current instanceof Y.Array)) {
|
|
755
|
+
throw new Error("Workspace does not contain any collections.");
|
|
756
|
+
}
|
|
757
|
+
const index = findCollectionIndex(current, collectionId);
|
|
758
|
+
if (index < 0) {
|
|
759
|
+
throw new Error(`Collection '${collectionId}' was not found.`);
|
|
760
|
+
}
|
|
761
|
+
const previous = normalizeCollection(current.get(index));
|
|
762
|
+
if (!previous) {
|
|
763
|
+
throw new Error(`Collection '${collectionId}' is malformed.`);
|
|
764
|
+
}
|
|
765
|
+
const next = {
|
|
766
|
+
...previous,
|
|
767
|
+
name: name ?? previous.name,
|
|
768
|
+
};
|
|
769
|
+
doc.transact(() => {
|
|
770
|
+
current.delete(index, 1);
|
|
771
|
+
current.insert(index, [next]);
|
|
772
|
+
});
|
|
773
|
+
await saveWorkspaceRootDoc(socket, resolvedWorkspaceId, doc);
|
|
774
|
+
return text(next);
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
socket.disconnect();
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
server.registerTool("update_collection", {
|
|
781
|
+
title: "Update Collection",
|
|
782
|
+
description: "Rename an AFFiNE collection.",
|
|
783
|
+
inputSchema: {
|
|
784
|
+
workspaceId: WorkspaceId.optional(),
|
|
785
|
+
collectionId: CollectionId,
|
|
786
|
+
name: FolderName.optional().describe("Updated collection name"),
|
|
787
|
+
},
|
|
788
|
+
}, updateCollectionHandler);
|
|
789
|
+
const deleteCollectionHandler = async ({ workspaceId, collectionId, }) => {
|
|
790
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
791
|
+
const { socket } = await getSocketContext();
|
|
792
|
+
try {
|
|
793
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
794
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
795
|
+
const setting = doc.getMap("setting");
|
|
796
|
+
const current = setting.get("collections");
|
|
797
|
+
if (!(current instanceof Y.Array)) {
|
|
798
|
+
throw new Error("Workspace does not contain any collections.");
|
|
799
|
+
}
|
|
800
|
+
const index = findCollectionIndex(current, collectionId);
|
|
801
|
+
if (index < 0) {
|
|
802
|
+
throw new Error(`Collection '${collectionId}' was not found.`);
|
|
803
|
+
}
|
|
804
|
+
current.delete(index, 1);
|
|
805
|
+
await saveWorkspaceRootDoc(socket, resolvedWorkspaceId, doc);
|
|
806
|
+
return text({ success: true, collectionId });
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
socket.disconnect();
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
server.registerTool("delete_collection", {
|
|
813
|
+
title: "Delete Collection",
|
|
814
|
+
description: "Delete an AFFiNE collection from the workspace sidebar.",
|
|
815
|
+
inputSchema: {
|
|
816
|
+
workspaceId: WorkspaceId.optional(),
|
|
817
|
+
collectionId: CollectionId,
|
|
818
|
+
},
|
|
819
|
+
}, deleteCollectionHandler);
|
|
820
|
+
const addDocToCollectionHandler = async ({ workspaceId, collectionId, docId, }) => {
|
|
821
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
822
|
+
const { socket } = await getSocketContext();
|
|
823
|
+
try {
|
|
824
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
825
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
826
|
+
const setting = doc.getMap("setting");
|
|
827
|
+
const current = setting.get("collections");
|
|
828
|
+
if (!(current instanceof Y.Array)) {
|
|
829
|
+
throw new Error("Workspace does not contain any collections.");
|
|
830
|
+
}
|
|
831
|
+
const index = findCollectionIndex(current, collectionId);
|
|
832
|
+
if (index < 0) {
|
|
833
|
+
throw new Error(`Collection '${collectionId}' was not found.`);
|
|
834
|
+
}
|
|
835
|
+
const previous = normalizeCollection(current.get(index));
|
|
836
|
+
if (!previous) {
|
|
837
|
+
throw new Error(`Collection '${collectionId}' is malformed.`);
|
|
838
|
+
}
|
|
839
|
+
const next = {
|
|
840
|
+
...previous,
|
|
841
|
+
allowList: Array.from(new Set([...previous.allowList, docId])),
|
|
842
|
+
};
|
|
843
|
+
doc.transact(() => {
|
|
844
|
+
current.delete(index, 1);
|
|
845
|
+
current.insert(index, [next]);
|
|
846
|
+
});
|
|
847
|
+
await saveWorkspaceRootDoc(socket, resolvedWorkspaceId, doc);
|
|
848
|
+
return text(next);
|
|
849
|
+
}
|
|
850
|
+
finally {
|
|
851
|
+
socket.disconnect();
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
server.registerTool("add_doc_to_collection", {
|
|
855
|
+
title: "Add Doc To Collection",
|
|
856
|
+
description: "Add a document id to an AFFiNE collection allow-list.",
|
|
857
|
+
inputSchema: {
|
|
858
|
+
workspaceId: WorkspaceId.optional(),
|
|
859
|
+
collectionId: CollectionId,
|
|
860
|
+
docId: DocId,
|
|
861
|
+
},
|
|
862
|
+
}, addDocToCollectionHandler);
|
|
863
|
+
const removeDocFromCollectionHandler = async ({ workspaceId, collectionId, docId, }) => {
|
|
864
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
865
|
+
const { socket } = await getSocketContext();
|
|
866
|
+
try {
|
|
867
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
868
|
+
const { doc } = await loadWorkspaceRootDoc(socket, resolvedWorkspaceId);
|
|
869
|
+
const setting = doc.getMap("setting");
|
|
870
|
+
const current = setting.get("collections");
|
|
871
|
+
if (!(current instanceof Y.Array)) {
|
|
872
|
+
throw new Error("Workspace does not contain any collections.");
|
|
873
|
+
}
|
|
874
|
+
const index = findCollectionIndex(current, collectionId);
|
|
875
|
+
if (index < 0) {
|
|
876
|
+
throw new Error(`Collection '${collectionId}' was not found.`);
|
|
877
|
+
}
|
|
878
|
+
const previous = normalizeCollection(current.get(index));
|
|
879
|
+
if (!previous) {
|
|
880
|
+
throw new Error(`Collection '${collectionId}' is malformed.`);
|
|
881
|
+
}
|
|
882
|
+
const next = {
|
|
883
|
+
...previous,
|
|
884
|
+
allowList: previous.allowList.filter(id => id !== docId),
|
|
885
|
+
};
|
|
886
|
+
doc.transact(() => {
|
|
887
|
+
current.delete(index, 1);
|
|
888
|
+
current.insert(index, [next]);
|
|
889
|
+
});
|
|
890
|
+
await saveWorkspaceRootDoc(socket, resolvedWorkspaceId, doc);
|
|
891
|
+
return text(next);
|
|
892
|
+
}
|
|
893
|
+
finally {
|
|
894
|
+
socket.disconnect();
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
server.registerTool("remove_doc_from_collection", {
|
|
898
|
+
title: "Remove Doc From Collection",
|
|
899
|
+
description: "Remove a document id from an AFFiNE collection allow-list.",
|
|
900
|
+
inputSchema: {
|
|
901
|
+
workspaceId: WorkspaceId.optional(),
|
|
902
|
+
collectionId: CollectionId,
|
|
903
|
+
docId: DocId,
|
|
904
|
+
},
|
|
905
|
+
}, removeDocFromCollectionHandler);
|
|
906
|
+
const listOrganizeNodesHandler = async ({ workspaceId }) => {
|
|
907
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
908
|
+
const { socket } = await getSocketContext();
|
|
909
|
+
try {
|
|
910
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
911
|
+
const { docId, doc } = await loadFoldersDoc(socket, resolvedWorkspaceId);
|
|
912
|
+
const nodes = sortOrganizeNodes(readOrganizeNodes(doc));
|
|
913
|
+
return text({
|
|
914
|
+
workspaceId: resolvedWorkspaceId,
|
|
915
|
+
storageDocId: docId,
|
|
916
|
+
nodes,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
socket.disconnect();
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
server.registerTool("list_organize_nodes", {
|
|
924
|
+
title: "List Organize Nodes",
|
|
925
|
+
description: "Experimental: list AFFiNE sidebar organize nodes from the folders workspace DB.",
|
|
926
|
+
inputSchema: {
|
|
927
|
+
workspaceId: WorkspaceId.optional(),
|
|
928
|
+
},
|
|
929
|
+
}, listOrganizeNodesHandler);
|
|
930
|
+
const createFolderHandler = async ({ workspaceId, name, parentId, index, }) => {
|
|
931
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
932
|
+
return text(await createFolderInternal({
|
|
933
|
+
workspaceId: resolvedWorkspaceId,
|
|
934
|
+
name,
|
|
935
|
+
parentId,
|
|
936
|
+
index,
|
|
937
|
+
}));
|
|
938
|
+
};
|
|
939
|
+
server.registerTool("create_folder", {
|
|
940
|
+
title: "Create Folder",
|
|
941
|
+
description: "Experimental: create an AFFiNE organize folder node.",
|
|
942
|
+
inputSchema: {
|
|
943
|
+
workspaceId: WorkspaceId.optional(),
|
|
944
|
+
name: FolderName.describe("Folder name"),
|
|
945
|
+
parentId: FolderId.nullable().optional().describe("Parent folder id. Omit for root-level folders."),
|
|
946
|
+
index: z.string().optional().describe("Optional fractional index. Defaults to append-after-last."),
|
|
947
|
+
},
|
|
948
|
+
}, createFolderHandler);
|
|
949
|
+
const createWorkspaceBlueprintHandler = async ({ workspaceId, rootFolderName, childFolderNames, }) => {
|
|
950
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
951
|
+
const normalizedChildFolderNames = Array.from(new Set((childFolderNames ?? []).map(name => name.trim()).filter(Boolean)));
|
|
952
|
+
const rootFolder = await createFolderInternal({
|
|
953
|
+
workspaceId: resolvedWorkspaceId,
|
|
954
|
+
name: rootFolderName,
|
|
955
|
+
});
|
|
956
|
+
const childFolders = [];
|
|
957
|
+
for (const childName of normalizedChildFolderNames) {
|
|
958
|
+
childFolders.push(await createFolderInternal({
|
|
959
|
+
workspaceId: resolvedWorkspaceId,
|
|
960
|
+
name: childName,
|
|
961
|
+
parentId: rootFolder.id,
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
964
|
+
return text({
|
|
965
|
+
workspaceId: resolvedWorkspaceId,
|
|
966
|
+
rootFolderId: rootFolder.id,
|
|
967
|
+
rootFolderName,
|
|
968
|
+
childFolders,
|
|
969
|
+
childFolderCount: childFolders.length,
|
|
970
|
+
storageDocId: rootFolder.storageDocId,
|
|
971
|
+
});
|
|
972
|
+
};
|
|
973
|
+
server.registerTool("create_workspace_blueprint", {
|
|
974
|
+
title: "Create Workspace Blueprint",
|
|
975
|
+
description: "Create a simple workspace folder blueprint under the organize tree.",
|
|
976
|
+
inputSchema: {
|
|
977
|
+
workspaceId: WorkspaceId.optional(),
|
|
978
|
+
rootFolderName: FolderName.describe("Root folder name"),
|
|
979
|
+
childFolderNames: z.array(FolderName).optional().describe("Optional child folder names to seed under the root folder."),
|
|
980
|
+
},
|
|
981
|
+
}, createWorkspaceBlueprintHandler);
|
|
982
|
+
const renameFolderHandler = async ({ workspaceId, folderId, name, }) => {
|
|
983
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
984
|
+
const { socket } = await getSocketContext();
|
|
985
|
+
try {
|
|
986
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
987
|
+
const { docId, doc } = await loadFoldersDoc(socket, resolvedWorkspaceId);
|
|
988
|
+
const nodeMap = organizeNodeMap(readOrganizeNodes(doc));
|
|
989
|
+
ensureNodeIsFolder(nodeMap, folderId);
|
|
990
|
+
const record = ensureRecord(doc, folderId);
|
|
991
|
+
record.set("data", name);
|
|
992
|
+
await saveFoldersDoc(socket, resolvedWorkspaceId, docId, doc);
|
|
993
|
+
return text({ id: folderId, name });
|
|
994
|
+
}
|
|
995
|
+
finally {
|
|
996
|
+
socket.disconnect();
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
server.registerTool("rename_folder", {
|
|
1000
|
+
title: "Rename Folder",
|
|
1001
|
+
description: "Experimental: rename an AFFiNE organize folder node.",
|
|
1002
|
+
inputSchema: {
|
|
1003
|
+
workspaceId: WorkspaceId.optional(),
|
|
1004
|
+
folderId: FolderId,
|
|
1005
|
+
name: FolderName,
|
|
1006
|
+
},
|
|
1007
|
+
}, renameFolderHandler);
|
|
1008
|
+
const deleteFolderHandler = async ({ workspaceId, folderId, }) => {
|
|
1009
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
1010
|
+
const { socket } = await getSocketContext();
|
|
1011
|
+
try {
|
|
1012
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
1013
|
+
const { docId, doc } = await loadFoldersDoc(socket, resolvedWorkspaceId);
|
|
1014
|
+
const nodes = readOrganizeNodes(doc);
|
|
1015
|
+
const nodeMap = organizeNodeMap(nodes);
|
|
1016
|
+
ensureNodeIsFolder(nodeMap, folderId);
|
|
1017
|
+
const stack = [folderId];
|
|
1018
|
+
const deletedIds = [];
|
|
1019
|
+
while (stack.length > 0) {
|
|
1020
|
+
const currentId = stack.pop();
|
|
1021
|
+
const current = nodeMap.get(currentId);
|
|
1022
|
+
if (!current) {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (current.type === "folder") {
|
|
1026
|
+
const children = nodes.filter(node => node.parentId === current.id);
|
|
1027
|
+
for (const child of children) {
|
|
1028
|
+
stack.push(child.id);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
deleteRecord(ensureRecord(doc, currentId));
|
|
1032
|
+
deletedIds.push(currentId);
|
|
1033
|
+
}
|
|
1034
|
+
await saveFoldersDoc(socket, resolvedWorkspaceId, docId, doc);
|
|
1035
|
+
return text({ success: true, deletedIds });
|
|
1036
|
+
}
|
|
1037
|
+
finally {
|
|
1038
|
+
socket.disconnect();
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
server.registerTool("delete_folder", {
|
|
1042
|
+
title: "Delete Folder",
|
|
1043
|
+
description: "Experimental: delete an AFFiNE organize folder and all nested nodes.",
|
|
1044
|
+
inputSchema: {
|
|
1045
|
+
workspaceId: WorkspaceId.optional(),
|
|
1046
|
+
folderId: FolderId,
|
|
1047
|
+
},
|
|
1048
|
+
}, deleteFolderHandler);
|
|
1049
|
+
const moveOrganizeNodeHandler = async ({ workspaceId, nodeId, parentId, index, }) => {
|
|
1050
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
1051
|
+
const resolvedParentId = parentId ?? null;
|
|
1052
|
+
const { socket } = await getSocketContext();
|
|
1053
|
+
try {
|
|
1054
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
1055
|
+
const { docId, doc } = await loadFoldersDoc(socket, resolvedWorkspaceId);
|
|
1056
|
+
const nodes = readOrganizeNodes(doc);
|
|
1057
|
+
const nodeMap = organizeNodeMap(nodes);
|
|
1058
|
+
const node = nodeMap.get(nodeId);
|
|
1059
|
+
if (!node) {
|
|
1060
|
+
throw new Error(`Organize node '${nodeId}' was not found.`);
|
|
1061
|
+
}
|
|
1062
|
+
ensureFolderParent(nodeMap, resolvedParentId);
|
|
1063
|
+
if (resolvedParentId === null && node.type !== "folder") {
|
|
1064
|
+
throw new Error("Root organize section can only contain folders.");
|
|
1065
|
+
}
|
|
1066
|
+
if (resolvedParentId && node.type === "folder" && isAncestor(nodeMap, resolvedParentId, nodeId)) {
|
|
1067
|
+
throw new Error("Cannot move a folder into its descendant.");
|
|
1068
|
+
}
|
|
1069
|
+
const nextIndex = index ?? nextOrganizeIndex(nodes.filter(entry => entry.id !== nodeId), resolvedParentId);
|
|
1070
|
+
const record = ensureRecord(doc, nodeId);
|
|
1071
|
+
record.set("parentId", resolvedParentId);
|
|
1072
|
+
record.set("index", nextIndex);
|
|
1073
|
+
await saveFoldersDoc(socket, resolvedWorkspaceId, docId, doc);
|
|
1074
|
+
return text({ id: nodeId, parentId: resolvedParentId, index: nextIndex });
|
|
1075
|
+
}
|
|
1076
|
+
finally {
|
|
1077
|
+
socket.disconnect();
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
server.registerTool("move_organize_node", {
|
|
1081
|
+
title: "Move Organize Node",
|
|
1082
|
+
description: "Experimental: move an AFFiNE organize folder or link node.",
|
|
1083
|
+
inputSchema: {
|
|
1084
|
+
workspaceId: WorkspaceId.optional(),
|
|
1085
|
+
nodeId: OrganizeNodeId,
|
|
1086
|
+
parentId: FolderId.nullable().optional().describe("Destination folder id. Omit for root-level placement."),
|
|
1087
|
+
index: z.string().optional().describe("Optional fractional index. Defaults to append-after-last."),
|
|
1088
|
+
},
|
|
1089
|
+
}, moveOrganizeNodeHandler);
|
|
1090
|
+
const addOrganizeLinkHandler = async ({ workspaceId, folderId, type, targetId, index, }) => {
|
|
1091
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
1092
|
+
const { socket } = await getSocketContext();
|
|
1093
|
+
try {
|
|
1094
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
1095
|
+
const link = await addOrganizeLinkToFolder(socket, resolvedWorkspaceId, {
|
|
1096
|
+
folderId,
|
|
1097
|
+
type,
|
|
1098
|
+
targetId,
|
|
1099
|
+
index,
|
|
1100
|
+
});
|
|
1101
|
+
return text(link);
|
|
1102
|
+
}
|
|
1103
|
+
finally {
|
|
1104
|
+
socket.disconnect();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
server.registerTool("add_organize_link", {
|
|
1108
|
+
title: "Add Organize Link",
|
|
1109
|
+
description: "Experimental: add a doc/tag/collection link under an AFFiNE organize folder.",
|
|
1110
|
+
inputSchema: {
|
|
1111
|
+
workspaceId: WorkspaceId.optional(),
|
|
1112
|
+
folderId: FolderId,
|
|
1113
|
+
type: z.enum(["doc", "tag", "collection"]),
|
|
1114
|
+
targetId: z.string().min(1).describe("Target doc/tag/collection id"),
|
|
1115
|
+
index: z.string().optional().describe("Optional fractional index. Defaults to append-after-last."),
|
|
1116
|
+
},
|
|
1117
|
+
}, addOrganizeLinkHandler);
|
|
1118
|
+
const deleteOrganizeLinkHandler = async ({ workspaceId, nodeId, }) => {
|
|
1119
|
+
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
|
|
1120
|
+
const { socket } = await getSocketContext();
|
|
1121
|
+
try {
|
|
1122
|
+
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
1123
|
+
const { docId, doc } = await loadFoldersDoc(socket, resolvedWorkspaceId);
|
|
1124
|
+
const nodeMap = organizeNodeMap(readOrganizeNodes(doc));
|
|
1125
|
+
const node = nodeMap.get(nodeId);
|
|
1126
|
+
if (!node || node.type === "folder") {
|
|
1127
|
+
throw new Error(`Organize link '${nodeId}' was not found.`);
|
|
1128
|
+
}
|
|
1129
|
+
deleteRecord(ensureRecord(doc, nodeId));
|
|
1130
|
+
await saveFoldersDoc(socket, resolvedWorkspaceId, docId, doc);
|
|
1131
|
+
return text({ success: true, nodeId });
|
|
1132
|
+
}
|
|
1133
|
+
finally {
|
|
1134
|
+
socket.disconnect();
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
server.registerTool("delete_organize_link", {
|
|
1138
|
+
title: "Delete Organize Link",
|
|
1139
|
+
description: "Experimental: delete an AFFiNE organize doc/tag/collection link.",
|
|
1140
|
+
inputSchema: {
|
|
1141
|
+
workspaceId: WorkspaceId.optional(),
|
|
1142
|
+
nodeId: OrganizeNodeId,
|
|
1143
|
+
},
|
|
1144
|
+
}, deleteOrganizeLinkHandler);
|
|
1145
|
+
}
|