affine-mcp-server 1.3.0 → 1.5.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/README.md +62 -5
- package/dist/index.js +1 -1
- package/dist/tools/docs.js +990 -57
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted or cloud). It exposes AFFiNE workspaces and documents to AI assistants over stdio.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/dawncr0w/affine-mcp-server/releases)
|
|
6
6
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
7
7
|
[](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
@@ -16,15 +16,15 @@ A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted
|
|
|
16
16
|
- Purpose: Manage AFFiNE workspaces and documents through MCP
|
|
17
17
|
- Transport: stdio only (Claude Desktop / Codex compatible)
|
|
18
18
|
- Auth: Token, Cookie, or Email/Password (priority order)
|
|
19
|
-
- Tools:
|
|
19
|
+
- Tools: 32 focused tools with WebSocket-based document editing
|
|
20
20
|
- Status: Active
|
|
21
21
|
|
|
22
|
-
> New in v1.
|
|
22
|
+
> New in v1.5.0: `append_block` now supports 30 verified block profiles, including database/edgeless (`frame`, `edgeless_text`, `surface_ref`, `note`) insertion paths. For stability on AFFiNE 0.26.x, `type=\"data_view\"` is currently mapped to a database block.
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
26
|
- Workspace: create (with initial doc), read, update, delete
|
|
27
|
-
- Documents: list/get/publish/revoke + create/append paragraph/delete (WebSocket‑based)
|
|
27
|
+
- Documents: list/get/read/publish/revoke + create/append paragraph/delete (WebSocket‑based)
|
|
28
28
|
- Comments: full CRUD and resolve
|
|
29
29
|
- Version History: list
|
|
30
30
|
- Users & Tokens: current user, sign in, profile/settings, and personal access tokens
|
|
@@ -112,6 +112,43 @@ Notes
|
|
|
112
112
|
- Command: `affine-mcp`
|
|
113
113
|
- Environment: `AFFINE_BASE_URL` + one auth method (`AFFINE_API_TOKEN` | `AFFINE_COOKIE` | `AFFINE_EMAIL`/`AFFINE_PASSWORD`)
|
|
114
114
|
|
|
115
|
+
### Cursor
|
|
116
|
+
|
|
117
|
+
Cursor also supports MCP over stdio with `mcp.json`.
|
|
118
|
+
|
|
119
|
+
Project-local (`.cursor/mcp.json`) example:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"affine": {
|
|
125
|
+
"command": "affine-mcp",
|
|
126
|
+
"env": {
|
|
127
|
+
"AFFINE_BASE_URL": "https://your-affine-instance.com",
|
|
128
|
+
"AFFINE_API_TOKEN": "apt_xxx"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If you prefer `npx`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"affine": {
|
|
141
|
+
"command": "npx",
|
|
142
|
+
"args": ["-y", "-p", "affine-mcp-server", "affine-mcp"],
|
|
143
|
+
"env": {
|
|
144
|
+
"AFFINE_BASE_URL": "https://your-affine-instance.com",
|
|
145
|
+
"AFFINE_API_TOKEN": "apt_xxx"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
115
152
|
## Available Tools
|
|
116
153
|
|
|
117
154
|
### Workspace
|
|
@@ -124,11 +161,12 @@ Notes
|
|
|
124
161
|
### Documents
|
|
125
162
|
- `list_docs` – list documents with pagination
|
|
126
163
|
- `get_doc` – get document metadata
|
|
164
|
+
- `read_doc` – read document block content and plain text snapshot (WebSocket)
|
|
127
165
|
- `publish_doc` – make document public
|
|
128
166
|
- `revoke_doc` – revoke public access
|
|
129
167
|
- `create_doc` – create a new document (WebSocket)
|
|
130
168
|
- `append_paragraph` – append a paragraph block (WebSocket)
|
|
131
|
-
- `append_block` – append
|
|
169
|
+
- `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`data_view` currently falls back to database)
|
|
132
170
|
- `delete_doc` – delete a document (WebSocket)
|
|
133
171
|
|
|
134
172
|
### Comments
|
|
@@ -186,6 +224,15 @@ Connection
|
|
|
186
224
|
- GraphQL endpoint default is `/graphql`
|
|
187
225
|
- Check firewall/proxy rules; verify CORS if self‑hosted
|
|
188
226
|
|
|
227
|
+
Method not found
|
|
228
|
+
- MCP tool names (for example `list_workspaces`) are not JSON-RPC top-level method names.
|
|
229
|
+
- Use an MCP client (`tools/list`, `tools/call`) instead of sending direct JSON-RPC calls like `{\"method\":\"list_workspaces\"}`.
|
|
230
|
+
- From v1.3.0, only canonical tool names are exposed (legacy `affine_*` aliases were removed).
|
|
231
|
+
|
|
232
|
+
Workspace visibility
|
|
233
|
+
- This MCP server can access server-backed workspaces only (AFFiNE cloud/self-hosted).
|
|
234
|
+
- Browser local-storage workspaces are client-side data, so they are not visible via server GraphQL/WebSocket APIs.
|
|
235
|
+
|
|
189
236
|
## Security Considerations
|
|
190
237
|
|
|
191
238
|
- Never commit `.env` with secrets
|
|
@@ -196,6 +243,16 @@ Connection
|
|
|
196
243
|
|
|
197
244
|
## Version History
|
|
198
245
|
|
|
246
|
+
### 1.5.0 (2026‑02‑13)
|
|
247
|
+
- Expanded `append_block` from Step1 to Step4 profiles: canonical text/list/code/divider/callout/latex/table/bookmark/media/embed plus `database`, `data_view`, `surface_ref`, `frame`, `edgeless_text`, `note` (`data_view` currently mapped to database for stability)
|
|
248
|
+
- Added strict field validation and canonical parent enforcement for page/note/surface containers
|
|
249
|
+
- Added local integration runner coverage for all 30 append_block cases against a live AFFINE server
|
|
250
|
+
|
|
251
|
+
### 1.4.0 (2026‑02‑13)
|
|
252
|
+
- Added `read_doc` for reading document block snapshot + plain text
|
|
253
|
+
- Added Cursor setup examples and troubleshooting notes for JSON-RPC method usage
|
|
254
|
+
- Added explicit local-storage workspace limitation notes
|
|
255
|
+
|
|
199
256
|
### 1.3.0 (2026‑02‑13)
|
|
200
257
|
- Added `append_block` for slash-command style editing (`heading/list/todo/code/divider/quote`)
|
|
201
258
|
- Tool surface simplified to 31 canonical tools (duplicate aliases removed)
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { loginWithPassword } from "./auth.js";
|
|
|
15
15
|
import { registerAuthTools } from "./tools/auth.js";
|
|
16
16
|
const config = loadConfig();
|
|
17
17
|
async function buildServer() {
|
|
18
|
-
const server = new McpServer({ name: "affine-mcp", version: "1.
|
|
18
|
+
const server = new McpServer({ name: "affine-mcp", version: "1.5.0" });
|
|
19
19
|
// Initialize GraphQL client with authentication
|
|
20
20
|
const gql = new GraphQLClient({
|
|
21
21
|
endpoint: `${config.baseUrl}${config.graphqlPath}`,
|
package/dist/tools/docs.js
CHANGED
|
@@ -4,19 +4,52 @@ import { wsUrlFromGraphQLEndpoint, connectWorkspaceSocket, joinWorkspace, loadDo
|
|
|
4
4
|
import * as Y from "yjs";
|
|
5
5
|
const WorkspaceId = z.string().min(1, "workspaceId required");
|
|
6
6
|
const DocId = z.string().min(1, "docId required");
|
|
7
|
-
const
|
|
7
|
+
const APPEND_BLOCK_CANONICAL_TYPE_VALUES = [
|
|
8
8
|
"paragraph",
|
|
9
|
-
"
|
|
10
|
-
"heading2",
|
|
11
|
-
"heading3",
|
|
9
|
+
"heading",
|
|
12
10
|
"quote",
|
|
13
|
-
"
|
|
14
|
-
"numbered_list",
|
|
15
|
-
"todo",
|
|
11
|
+
"list",
|
|
16
12
|
"code",
|
|
17
13
|
"divider",
|
|
14
|
+
"callout",
|
|
15
|
+
"latex",
|
|
16
|
+
"table",
|
|
17
|
+
"bookmark",
|
|
18
|
+
"image",
|
|
19
|
+
"attachment",
|
|
20
|
+
"embed_youtube",
|
|
21
|
+
"embed_github",
|
|
22
|
+
"embed_figma",
|
|
23
|
+
"embed_loom",
|
|
24
|
+
"embed_html",
|
|
25
|
+
"embed_linked_doc",
|
|
26
|
+
"embed_synced_doc",
|
|
27
|
+
"embed_iframe",
|
|
28
|
+
"database",
|
|
29
|
+
"data_view",
|
|
30
|
+
"surface_ref",
|
|
31
|
+
"frame",
|
|
32
|
+
"edgeless_text",
|
|
33
|
+
"note",
|
|
18
34
|
];
|
|
19
|
-
const
|
|
35
|
+
const APPEND_BLOCK_LEGACY_ALIAS_MAP = {
|
|
36
|
+
heading1: "heading",
|
|
37
|
+
heading2: "heading",
|
|
38
|
+
heading3: "heading",
|
|
39
|
+
bulleted_list: "list",
|
|
40
|
+
numbered_list: "list",
|
|
41
|
+
todo: "list",
|
|
42
|
+
};
|
|
43
|
+
const APPEND_BLOCK_LIST_STYLE_VALUES = ["bulleted", "numbered", "todo"];
|
|
44
|
+
const AppendBlockListStyle = z.enum(APPEND_BLOCK_LIST_STYLE_VALUES);
|
|
45
|
+
const APPEND_BLOCK_BOOKMARK_STYLE_VALUES = [
|
|
46
|
+
"vertical",
|
|
47
|
+
"horizontal",
|
|
48
|
+
"list",
|
|
49
|
+
"cube",
|
|
50
|
+
"citation",
|
|
51
|
+
];
|
|
52
|
+
const AppendBlockBookmarkStyle = z.enum(APPEND_BLOCK_BOOKMARK_STYLE_VALUES);
|
|
20
53
|
function blockVersion(flavour) {
|
|
21
54
|
switch (flavour) {
|
|
22
55
|
case "affine:page":
|
|
@@ -49,6 +82,32 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
49
82
|
}
|
|
50
83
|
return yText;
|
|
51
84
|
}
|
|
85
|
+
function asText(value) {
|
|
86
|
+
if (value instanceof Y.Text)
|
|
87
|
+
return value.toString();
|
|
88
|
+
if (typeof value === "string")
|
|
89
|
+
return value;
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
function childIdsFrom(value) {
|
|
93
|
+
if (!(value instanceof Y.Array))
|
|
94
|
+
return [];
|
|
95
|
+
const childIds = [];
|
|
96
|
+
value.forEach((entry) => {
|
|
97
|
+
if (typeof entry === "string") {
|
|
98
|
+
childIds.push(entry);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(entry)) {
|
|
102
|
+
for (const child of entry) {
|
|
103
|
+
if (typeof child === "string") {
|
|
104
|
+
childIds.push(child);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return childIds;
|
|
110
|
+
}
|
|
52
111
|
function setSysFields(block, blockId, flavour) {
|
|
53
112
|
block.set("sys:id", blockId);
|
|
54
113
|
block.set("sys:flavour", flavour);
|
|
@@ -95,71 +154,816 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
95
154
|
pageChildren.push([noteId]);
|
|
96
155
|
return noteId;
|
|
97
156
|
}
|
|
98
|
-
function
|
|
157
|
+
function ensureSurfaceBlock(blocks) {
|
|
158
|
+
const existingSurfaceId = findBlockIdByFlavour(blocks, "affine:surface");
|
|
159
|
+
if (existingSurfaceId) {
|
|
160
|
+
return existingSurfaceId;
|
|
161
|
+
}
|
|
162
|
+
const pageId = findBlockIdByFlavour(blocks, "affine:page");
|
|
163
|
+
if (!pageId) {
|
|
164
|
+
throw new Error("Document has no page block; unable to create/find surface.");
|
|
165
|
+
}
|
|
166
|
+
const surfaceId = generateId();
|
|
167
|
+
const surface = new Y.Map();
|
|
168
|
+
setSysFields(surface, surfaceId, "affine:surface");
|
|
169
|
+
surface.set("sys:parent", pageId);
|
|
170
|
+
surface.set("sys:children", new Y.Array());
|
|
171
|
+
const elements = new Y.Map();
|
|
172
|
+
elements.set("type", "$blocksuite:internal:native$");
|
|
173
|
+
elements.set("value", new Y.Map());
|
|
174
|
+
surface.set("prop:elements", elements);
|
|
175
|
+
blocks.set(surfaceId, surface);
|
|
176
|
+
const page = blocks.get(pageId);
|
|
177
|
+
let pageChildren = page.get("sys:children");
|
|
178
|
+
if (!(pageChildren instanceof Y.Array)) {
|
|
179
|
+
pageChildren = new Y.Array();
|
|
180
|
+
page.set("sys:children", pageChildren);
|
|
181
|
+
}
|
|
182
|
+
pageChildren.push([surfaceId]);
|
|
183
|
+
return surfaceId;
|
|
184
|
+
}
|
|
185
|
+
function normalizeBlockTypeInput(typeInput) {
|
|
186
|
+
const key = typeInput.trim().toLowerCase();
|
|
187
|
+
if (APPEND_BLOCK_CANONICAL_TYPE_VALUES.includes(key)) {
|
|
188
|
+
return { type: key };
|
|
189
|
+
}
|
|
190
|
+
if (Object.prototype.hasOwnProperty.call(APPEND_BLOCK_LEGACY_ALIAS_MAP, key)) {
|
|
191
|
+
const legacyType = key;
|
|
192
|
+
const type = APPEND_BLOCK_LEGACY_ALIAS_MAP[legacyType];
|
|
193
|
+
const listStyleFromAlias = legacyType === "bulleted_list"
|
|
194
|
+
? "bulleted"
|
|
195
|
+
: legacyType === "numbered_list"
|
|
196
|
+
? "numbered"
|
|
197
|
+
: legacyType === "todo"
|
|
198
|
+
? "todo"
|
|
199
|
+
: undefined;
|
|
200
|
+
const headingLevelFromAlias = legacyType === "heading1"
|
|
201
|
+
? 1
|
|
202
|
+
: legacyType === "heading2"
|
|
203
|
+
? 2
|
|
204
|
+
: legacyType === "heading3"
|
|
205
|
+
? 3
|
|
206
|
+
: undefined;
|
|
207
|
+
return { type, legacyType, headingLevelFromAlias, listStyleFromAlias };
|
|
208
|
+
}
|
|
209
|
+
const supported = [
|
|
210
|
+
...APPEND_BLOCK_CANONICAL_TYPE_VALUES,
|
|
211
|
+
...Object.keys(APPEND_BLOCK_LEGACY_ALIAS_MAP),
|
|
212
|
+
].join(", ");
|
|
213
|
+
throw new Error(`Unsupported append_block type '${typeInput}'. Supported types: ${supported}`);
|
|
214
|
+
}
|
|
215
|
+
function normalizePlacement(placement) {
|
|
216
|
+
if (!placement)
|
|
217
|
+
return undefined;
|
|
218
|
+
const normalized = {};
|
|
219
|
+
if (placement.parentId?.trim())
|
|
220
|
+
normalized.parentId = placement.parentId.trim();
|
|
221
|
+
if (placement.afterBlockId?.trim())
|
|
222
|
+
normalized.afterBlockId = placement.afterBlockId.trim();
|
|
223
|
+
if (placement.beforeBlockId?.trim())
|
|
224
|
+
normalized.beforeBlockId = placement.beforeBlockId.trim();
|
|
225
|
+
if (placement.index !== undefined)
|
|
226
|
+
normalized.index = placement.index;
|
|
227
|
+
const hasAfter = Boolean(normalized.afterBlockId);
|
|
228
|
+
const hasBefore = Boolean(normalized.beforeBlockId);
|
|
229
|
+
if (hasAfter && hasBefore) {
|
|
230
|
+
throw new Error("placement.afterBlockId and placement.beforeBlockId are mutually exclusive.");
|
|
231
|
+
}
|
|
232
|
+
if (normalized.index !== undefined) {
|
|
233
|
+
if (!Number.isInteger(normalized.index) || normalized.index < 0) {
|
|
234
|
+
throw new Error("placement.index must be an integer greater than or equal to 0.");
|
|
235
|
+
}
|
|
236
|
+
if (hasAfter || hasBefore) {
|
|
237
|
+
throw new Error("placement.index cannot be used with placement.afterBlockId/beforeBlockId.");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!normalized.parentId && !normalized.afterBlockId && !normalized.beforeBlockId && normalized.index === undefined) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
245
|
+
function validateNormalizedAppendBlockInput(normalized, raw) {
|
|
246
|
+
if (normalized.type === "heading") {
|
|
247
|
+
if (!Number.isInteger(normalized.headingLevel) || normalized.headingLevel < 1 || normalized.headingLevel > 6) {
|
|
248
|
+
throw new Error("Heading level must be an integer from 1 to 6.");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (raw.level !== undefined && normalized.strict) {
|
|
252
|
+
throw new Error("The 'level' field can only be used with type='heading'.");
|
|
253
|
+
}
|
|
254
|
+
if (normalized.type === "list") {
|
|
255
|
+
if (!APPEND_BLOCK_LIST_STYLE_VALUES.includes(normalized.listStyle)) {
|
|
256
|
+
throw new Error(`Invalid list style '${normalized.listStyle}'.`);
|
|
257
|
+
}
|
|
258
|
+
if (normalized.listStyle !== "todo" && raw.checked !== undefined && normalized.strict) {
|
|
259
|
+
throw new Error("The 'checked' field can only be used when list style is 'todo'.");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
if (raw.style !== undefined && normalized.strict) {
|
|
264
|
+
throw new Error("The 'style' field can only be used with type='list'.");
|
|
265
|
+
}
|
|
266
|
+
if (raw.checked !== undefined && normalized.strict) {
|
|
267
|
+
throw new Error("The 'checked' field can only be used with type='list' (style='todo').");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (normalized.type !== "code") {
|
|
271
|
+
if (raw.language !== undefined && normalized.strict) {
|
|
272
|
+
throw new Error("The 'language' field can only be used with type='code'.");
|
|
273
|
+
}
|
|
274
|
+
const allowsCaption = normalized.type === "bookmark" ||
|
|
275
|
+
normalized.type === "image" ||
|
|
276
|
+
normalized.type === "attachment" ||
|
|
277
|
+
normalized.type === "surface_ref" ||
|
|
278
|
+
normalized.type.startsWith("embed_");
|
|
279
|
+
if (raw.caption !== undefined && !allowsCaption && normalized.strict) {
|
|
280
|
+
throw new Error("The 'caption' field is not valid for this block type.");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (normalized.language.length > 64) {
|
|
284
|
+
throw new Error("Code language is too long (max 64 chars).");
|
|
285
|
+
}
|
|
286
|
+
if (normalized.type === "divider" && raw.text && raw.text.length > 0 && normalized.strict) {
|
|
287
|
+
throw new Error("Divider blocks do not accept text.");
|
|
288
|
+
}
|
|
289
|
+
const requiresUrl = [
|
|
290
|
+
"bookmark",
|
|
291
|
+
"embed_youtube",
|
|
292
|
+
"embed_github",
|
|
293
|
+
"embed_figma",
|
|
294
|
+
"embed_loom",
|
|
295
|
+
"embed_iframe",
|
|
296
|
+
];
|
|
297
|
+
const urlAllowedTypes = [...requiresUrl];
|
|
298
|
+
if (urlAllowedTypes.includes(normalized.type)) {
|
|
299
|
+
if (!normalized.url) {
|
|
300
|
+
throw new Error(`${normalized.type} blocks require a non-empty url.`);
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
new URL(normalized.url);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new Error(`Invalid url for ${normalized.type} block: '${normalized.url}'.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (normalized.type === "bookmark") {
|
|
310
|
+
if (!APPEND_BLOCK_BOOKMARK_STYLE_VALUES.includes(normalized.bookmarkStyle)) {
|
|
311
|
+
throw new Error(`Invalid bookmark style '${normalized.bookmarkStyle}'.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
if (raw.bookmarkStyle !== undefined && normalized.strict) {
|
|
316
|
+
throw new Error("The 'bookmarkStyle' field can only be used with type='bookmark'.");
|
|
317
|
+
}
|
|
318
|
+
if (raw.url !== undefined && !urlAllowedTypes.includes(normalized.type) && normalized.strict) {
|
|
319
|
+
throw new Error("The 'url' field is not valid for this block type.");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (normalized.type === "image" || normalized.type === "attachment") {
|
|
323
|
+
if (!normalized.sourceId) {
|
|
324
|
+
throw new Error(`${normalized.type} blocks require sourceId (use upload_blob first).`);
|
|
325
|
+
}
|
|
326
|
+
if (normalized.type === "attachment" && (!normalized.name || !normalized.mimeType)) {
|
|
327
|
+
throw new Error("attachment blocks require valid name and mimeType.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (raw.sourceId !== undefined && normalized.strict) {
|
|
331
|
+
throw new Error("The 'sourceId' field can only be used with type='image' or type='attachment'.");
|
|
332
|
+
}
|
|
333
|
+
else if ((raw.name !== undefined || raw.mimeType !== undefined || raw.embed !== undefined || raw.size !== undefined) &&
|
|
334
|
+
normalized.strict) {
|
|
335
|
+
throw new Error("The 'name'/'mimeType'/'embed'/'size' fields are only valid for image/attachment blocks.");
|
|
336
|
+
}
|
|
337
|
+
if (normalized.type === "latex") {
|
|
338
|
+
if (!normalized.latex && normalized.strict) {
|
|
339
|
+
throw new Error("latex blocks require a non-empty 'latex' value in strict mode.");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (raw.latex !== undefined && normalized.strict) {
|
|
343
|
+
throw new Error("The 'latex' field can only be used with type='latex'.");
|
|
344
|
+
}
|
|
345
|
+
if (normalized.type === "embed_linked_doc" || normalized.type === "embed_synced_doc") {
|
|
346
|
+
if (!normalized.pageId) {
|
|
347
|
+
throw new Error(`${normalized.type} blocks require pageId.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (raw.pageId !== undefined && normalized.strict) {
|
|
351
|
+
throw new Error("The 'pageId' field can only be used with linked/synced doc embed types.");
|
|
352
|
+
}
|
|
353
|
+
if (normalized.type === "embed_html") {
|
|
354
|
+
if (!normalized.html && !normalized.design && normalized.strict) {
|
|
355
|
+
throw new Error("embed_html blocks require html or design.");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if ((raw.html !== undefined || raw.design !== undefined) && normalized.strict) {
|
|
359
|
+
throw new Error("The 'html'/'design' fields can only be used with type='embed_html'.");
|
|
360
|
+
}
|
|
361
|
+
if (normalized.type === "embed_iframe") {
|
|
362
|
+
if (raw.iframeUrl !== undefined && !normalized.iframeUrl && normalized.strict) {
|
|
363
|
+
throw new Error("embed_iframe iframeUrl cannot be empty when provided.");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else if (raw.iframeUrl !== undefined && normalized.strict) {
|
|
367
|
+
throw new Error("The 'iframeUrl' field can only be used with type='embed_iframe'.");
|
|
368
|
+
}
|
|
369
|
+
if (normalized.type === "surface_ref") {
|
|
370
|
+
if (!normalized.reference) {
|
|
371
|
+
throw new Error("surface_ref blocks require 'reference' (target element/block id).");
|
|
372
|
+
}
|
|
373
|
+
if (!normalized.refFlavour) {
|
|
374
|
+
throw new Error("surface_ref blocks require 'refFlavour' (for example affine:frame).");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if ((raw.reference !== undefined || raw.refFlavour !== undefined) && normalized.strict) {
|
|
378
|
+
throw new Error("The 'reference'/'refFlavour' fields can only be used with type='surface_ref'.");
|
|
379
|
+
}
|
|
380
|
+
if (normalized.type === "frame" || normalized.type === "edgeless_text" || normalized.type === "note") {
|
|
381
|
+
if (!Number.isInteger(normalized.width) || normalized.width < 1 || normalized.width > 10000) {
|
|
382
|
+
throw new Error(`${normalized.type} width must be an integer between 1 and 10000.`);
|
|
383
|
+
}
|
|
384
|
+
if (!Number.isInteger(normalized.height) || normalized.height < 1 || normalized.height > 10000) {
|
|
385
|
+
throw new Error(`${normalized.type} height must be an integer between 1 and 10000.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if ((raw.width !== undefined || raw.height !== undefined) && normalized.strict) {
|
|
389
|
+
throw new Error("The 'width'/'height' fields are only valid for frame/edgeless_text/note.");
|
|
390
|
+
}
|
|
391
|
+
if (normalized.type !== "frame" && normalized.type !== "note" && raw.background !== undefined && normalized.strict) {
|
|
392
|
+
throw new Error("The 'background' field is only valid for frame/note.");
|
|
393
|
+
}
|
|
394
|
+
if (normalized.type === "table") {
|
|
395
|
+
if (!Number.isInteger(normalized.rows) || normalized.rows < 1 || normalized.rows > 20) {
|
|
396
|
+
throw new Error("table rows must be an integer between 1 and 20.");
|
|
397
|
+
}
|
|
398
|
+
if (!Number.isInteger(normalized.columns) || normalized.columns < 1 || normalized.columns > 20) {
|
|
399
|
+
throw new Error("table columns must be an integer between 1 and 20.");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if ((raw.rows !== undefined || raw.columns !== undefined) && normalized.strict) {
|
|
403
|
+
throw new Error("The 'rows'/'columns' fields can only be used with type='table'.");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function normalizeAppendBlockInput(parsed) {
|
|
407
|
+
const strict = parsed.strict !== false;
|
|
408
|
+
const typeInfo = normalizeBlockTypeInput(parsed.type);
|
|
409
|
+
const headingLevelCandidate = parsed.level ?? typeInfo.headingLevelFromAlias ?? 1;
|
|
410
|
+
const headingLevelNumber = Number(headingLevelCandidate);
|
|
411
|
+
const headingLevel = Math.max(1, Math.min(6, headingLevelNumber));
|
|
412
|
+
const listStyle = typeInfo.listStyleFromAlias ?? parsed.style ?? "bulleted";
|
|
413
|
+
const bookmarkStyle = parsed.bookmarkStyle ?? "horizontal";
|
|
414
|
+
const language = (parsed.language ?? "txt").trim().toLowerCase() || "txt";
|
|
415
|
+
const placement = normalizePlacement(parsed.placement);
|
|
416
|
+
const url = (parsed.url ?? "").trim();
|
|
417
|
+
const pageId = (parsed.pageId ?? "").trim();
|
|
418
|
+
const iframeUrl = (parsed.iframeUrl ?? "").trim();
|
|
419
|
+
const html = parsed.html ?? "";
|
|
420
|
+
const design = parsed.design ?? "";
|
|
421
|
+
const reference = (parsed.reference ?? "").trim();
|
|
422
|
+
const refFlavour = (parsed.refFlavour ?? "").trim();
|
|
423
|
+
const width = Number.isFinite(parsed.width) ? Math.max(1, Math.floor(parsed.width)) : 100;
|
|
424
|
+
const height = Number.isFinite(parsed.height) ? Math.max(1, Math.floor(parsed.height)) : 100;
|
|
425
|
+
const background = (parsed.background ?? "transparent").trim() || "transparent";
|
|
426
|
+
const sourceId = (parsed.sourceId ?? "").trim();
|
|
427
|
+
const name = (parsed.name ?? "attachment").trim() || "attachment";
|
|
428
|
+
const mimeType = (parsed.mimeType ?? "application/octet-stream").trim() || "application/octet-stream";
|
|
429
|
+
const size = Number.isFinite(parsed.size) ? Math.max(0, Math.floor(parsed.size)) : 0;
|
|
430
|
+
const rows = Number.isInteger(parsed.rows) ? parsed.rows : 3;
|
|
431
|
+
const columns = Number.isInteger(parsed.columns) ? parsed.columns : 3;
|
|
432
|
+
const latex = (parsed.latex ?? "").trim();
|
|
433
|
+
const normalized = {
|
|
434
|
+
workspaceId: parsed.workspaceId,
|
|
435
|
+
docId: parsed.docId,
|
|
436
|
+
type: typeInfo.type,
|
|
437
|
+
strict,
|
|
438
|
+
placement,
|
|
439
|
+
text: parsed.text ?? "",
|
|
440
|
+
url,
|
|
441
|
+
pageId,
|
|
442
|
+
iframeUrl,
|
|
443
|
+
html,
|
|
444
|
+
design,
|
|
445
|
+
reference,
|
|
446
|
+
refFlavour,
|
|
447
|
+
width,
|
|
448
|
+
height,
|
|
449
|
+
background,
|
|
450
|
+
sourceId,
|
|
451
|
+
name,
|
|
452
|
+
mimeType,
|
|
453
|
+
size,
|
|
454
|
+
embed: Boolean(parsed.embed),
|
|
455
|
+
rows,
|
|
456
|
+
columns,
|
|
457
|
+
latex,
|
|
458
|
+
headingLevel,
|
|
459
|
+
listStyle,
|
|
460
|
+
bookmarkStyle,
|
|
461
|
+
checked: Boolean(parsed.checked),
|
|
462
|
+
language,
|
|
463
|
+
caption: parsed.caption,
|
|
464
|
+
legacyType: typeInfo.legacyType,
|
|
465
|
+
};
|
|
466
|
+
validateNormalizedAppendBlockInput(normalized, parsed);
|
|
467
|
+
return normalized;
|
|
468
|
+
}
|
|
469
|
+
function findBlockById(blocks, blockId) {
|
|
470
|
+
const value = blocks.get(blockId);
|
|
471
|
+
if (value instanceof Y.Map)
|
|
472
|
+
return value;
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
function ensureChildrenArray(block) {
|
|
476
|
+
const current = block.get("sys:children");
|
|
477
|
+
if (current instanceof Y.Array)
|
|
478
|
+
return current;
|
|
479
|
+
const created = new Y.Array();
|
|
480
|
+
block.set("sys:children", created);
|
|
481
|
+
return created;
|
|
482
|
+
}
|
|
483
|
+
function indexOfChild(children, blockId) {
|
|
484
|
+
let index = -1;
|
|
485
|
+
children.forEach((entry, i) => {
|
|
486
|
+
if (index >= 0)
|
|
487
|
+
return;
|
|
488
|
+
if (typeof entry === "string") {
|
|
489
|
+
if (entry === blockId)
|
|
490
|
+
index = i;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(entry)) {
|
|
494
|
+
for (const child of entry) {
|
|
495
|
+
if (child === blockId) {
|
|
496
|
+
index = i;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
return index;
|
|
503
|
+
}
|
|
504
|
+
function resolveInsertContext(blocks, normalized) {
|
|
505
|
+
const placement = normalized.placement;
|
|
506
|
+
let parentId;
|
|
507
|
+
let referenceBlockId;
|
|
508
|
+
let mode = "append";
|
|
509
|
+
if (placement?.afterBlockId) {
|
|
510
|
+
mode = "after";
|
|
511
|
+
referenceBlockId = placement.afterBlockId;
|
|
512
|
+
const referenceBlock = findBlockById(blocks, referenceBlockId);
|
|
513
|
+
if (!referenceBlock)
|
|
514
|
+
throw new Error(`placement.afterBlockId '${referenceBlockId}' was not found.`);
|
|
515
|
+
const refParentId = referenceBlock.get("sys:parent");
|
|
516
|
+
if (typeof refParentId !== "string" || !refParentId) {
|
|
517
|
+
throw new Error(`Block '${referenceBlockId}' has no parent.`);
|
|
518
|
+
}
|
|
519
|
+
parentId = refParentId;
|
|
520
|
+
}
|
|
521
|
+
else if (placement?.beforeBlockId) {
|
|
522
|
+
mode = "before";
|
|
523
|
+
referenceBlockId = placement.beforeBlockId;
|
|
524
|
+
const referenceBlock = findBlockById(blocks, referenceBlockId);
|
|
525
|
+
if (!referenceBlock)
|
|
526
|
+
throw new Error(`placement.beforeBlockId '${referenceBlockId}' was not found.`);
|
|
527
|
+
const refParentId = referenceBlock.get("sys:parent");
|
|
528
|
+
if (typeof refParentId !== "string" || !refParentId) {
|
|
529
|
+
throw new Error(`Block '${referenceBlockId}' has no parent.`);
|
|
530
|
+
}
|
|
531
|
+
parentId = refParentId;
|
|
532
|
+
}
|
|
533
|
+
else if (placement?.parentId) {
|
|
534
|
+
mode = placement.index !== undefined ? "index" : "append";
|
|
535
|
+
parentId = placement.parentId;
|
|
536
|
+
}
|
|
537
|
+
if (!parentId) {
|
|
538
|
+
if (normalized.type === "frame" || normalized.type === "edgeless_text") {
|
|
539
|
+
parentId = ensureSurfaceBlock(blocks);
|
|
540
|
+
}
|
|
541
|
+
else if (normalized.type === "note") {
|
|
542
|
+
parentId = findBlockIdByFlavour(blocks, "affine:page") || undefined;
|
|
543
|
+
if (!parentId) {
|
|
544
|
+
throw new Error("Document has no page block; unable to insert note.");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
parentId = ensureNoteBlock(blocks);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const parentBlock = findBlockById(blocks, parentId);
|
|
552
|
+
if (!parentBlock) {
|
|
553
|
+
throw new Error(`Target parent block '${parentId}' was not found.`);
|
|
554
|
+
}
|
|
555
|
+
const parentFlavour = parentBlock.get("sys:flavour");
|
|
556
|
+
if (normalized.strict) {
|
|
557
|
+
if (parentFlavour === "affine:page" && normalized.type !== "note") {
|
|
558
|
+
throw new Error(`Cannot append '${normalized.type}' directly under 'affine:page'.`);
|
|
559
|
+
}
|
|
560
|
+
if (parentFlavour === "affine:surface" &&
|
|
561
|
+
normalized.type !== "frame" &&
|
|
562
|
+
normalized.type !== "edgeless_text") {
|
|
563
|
+
throw new Error(`Cannot append '${normalized.type}' directly under 'affine:surface'.`);
|
|
564
|
+
}
|
|
565
|
+
if (normalized.type === "note" && parentFlavour !== "affine:page") {
|
|
566
|
+
throw new Error("note blocks must be appended under affine:page.");
|
|
567
|
+
}
|
|
568
|
+
if ((normalized.type === "frame" || normalized.type === "edgeless_text") &&
|
|
569
|
+
parentFlavour !== "affine:surface") {
|
|
570
|
+
throw new Error(`${normalized.type} blocks must be appended under affine:surface.`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const children = ensureChildrenArray(parentBlock);
|
|
574
|
+
let insertIndex = children.length;
|
|
575
|
+
if (mode === "after" || mode === "before") {
|
|
576
|
+
const idx = indexOfChild(children, referenceBlockId);
|
|
577
|
+
if (idx < 0) {
|
|
578
|
+
throw new Error(`Reference block '${referenceBlockId}' is not a child of parent '${parentId}'.`);
|
|
579
|
+
}
|
|
580
|
+
insertIndex = mode === "after" ? idx + 1 : idx;
|
|
581
|
+
}
|
|
582
|
+
else if (mode === "index") {
|
|
583
|
+
const requestedIndex = placement?.index ?? children.length;
|
|
584
|
+
if (requestedIndex > children.length && normalized.strict) {
|
|
585
|
+
throw new Error(`placement.index ${requestedIndex} is out of range (max ${children.length}).`);
|
|
586
|
+
}
|
|
587
|
+
insertIndex = Math.min(requestedIndex, children.length);
|
|
588
|
+
}
|
|
589
|
+
return { parentId, parentBlock, children, insertIndex };
|
|
590
|
+
}
|
|
591
|
+
function createBlock(parentId, normalized) {
|
|
99
592
|
const blockId = generateId();
|
|
100
593
|
const block = new Y.Map();
|
|
101
|
-
const content =
|
|
102
|
-
switch (
|
|
594
|
+
const content = normalized.text;
|
|
595
|
+
switch (normalized.type) {
|
|
103
596
|
case "paragraph":
|
|
104
|
-
case "
|
|
105
|
-
case "heading2":
|
|
106
|
-
case "heading3":
|
|
597
|
+
case "heading":
|
|
107
598
|
case "quote": {
|
|
108
599
|
setSysFields(block, blockId, "affine:paragraph");
|
|
109
|
-
block.set("sys:parent",
|
|
600
|
+
block.set("sys:parent", parentId);
|
|
110
601
|
block.set("sys:children", new Y.Array());
|
|
111
|
-
const blockType =
|
|
112
|
-
?
|
|
113
|
-
:
|
|
114
|
-
? "
|
|
115
|
-
:
|
|
116
|
-
? "h3"
|
|
117
|
-
: parsed.type === "quote"
|
|
118
|
-
? "quote"
|
|
119
|
-
: "text";
|
|
602
|
+
const blockType = normalized.type === "heading"
|
|
603
|
+
? `h${normalized.headingLevel}`
|
|
604
|
+
: normalized.type === "quote"
|
|
605
|
+
? "quote"
|
|
606
|
+
: "text";
|
|
120
607
|
block.set("prop:type", blockType);
|
|
121
608
|
block.set("prop:text", makeText(content));
|
|
122
609
|
return { blockId, block, flavour: "affine:paragraph", blockType };
|
|
123
610
|
}
|
|
124
|
-
case "
|
|
125
|
-
case "numbered_list":
|
|
126
|
-
case "todo": {
|
|
611
|
+
case "list": {
|
|
127
612
|
setSysFields(block, blockId, "affine:list");
|
|
128
|
-
block.set("sys:parent",
|
|
613
|
+
block.set("sys:parent", parentId);
|
|
129
614
|
block.set("sys:children", new Y.Array());
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
: parsed.type === "numbered_list"
|
|
133
|
-
? "numbered"
|
|
134
|
-
: "todo";
|
|
135
|
-
block.set("prop:type", blockType);
|
|
136
|
-
if (blockType === "todo") {
|
|
137
|
-
block.set("prop:checked", Boolean(parsed.checked));
|
|
138
|
-
}
|
|
615
|
+
block.set("prop:type", normalized.listStyle);
|
|
616
|
+
block.set("prop:checked", normalized.listStyle === "todo" ? normalized.checked : false);
|
|
139
617
|
block.set("prop:text", makeText(content));
|
|
140
|
-
return { blockId, block, flavour: "affine:list", blockType };
|
|
618
|
+
return { blockId, block, flavour: "affine:list", blockType: normalized.listStyle };
|
|
141
619
|
}
|
|
142
620
|
case "code": {
|
|
143
621
|
setSysFields(block, blockId, "affine:code");
|
|
144
|
-
block.set("sys:parent",
|
|
622
|
+
block.set("sys:parent", parentId);
|
|
145
623
|
block.set("sys:children", new Y.Array());
|
|
146
|
-
block.set("prop:language",
|
|
147
|
-
if (
|
|
148
|
-
block.set("prop:caption",
|
|
624
|
+
block.set("prop:language", normalized.language);
|
|
625
|
+
if (normalized.caption) {
|
|
626
|
+
block.set("prop:caption", normalized.caption);
|
|
149
627
|
}
|
|
150
628
|
block.set("prop:text", makeText(content));
|
|
151
629
|
return { blockId, block, flavour: "affine:code" };
|
|
152
630
|
}
|
|
153
631
|
case "divider": {
|
|
154
632
|
setSysFields(block, blockId, "affine:divider");
|
|
155
|
-
block.set("sys:parent",
|
|
633
|
+
block.set("sys:parent", parentId);
|
|
156
634
|
block.set("sys:children", new Y.Array());
|
|
157
635
|
return { blockId, block, flavour: "affine:divider" };
|
|
158
636
|
}
|
|
637
|
+
case "callout": {
|
|
638
|
+
setSysFields(block, blockId, "affine:callout");
|
|
639
|
+
block.set("sys:parent", parentId);
|
|
640
|
+
block.set("sys:children", new Y.Array());
|
|
641
|
+
block.set("prop:icon", { type: "emoji", unicode: "💡" });
|
|
642
|
+
block.set("prop:backgroundColorName", "grey");
|
|
643
|
+
block.set("prop:text", makeText(content));
|
|
644
|
+
return { blockId, block, flavour: "affine:callout" };
|
|
645
|
+
}
|
|
646
|
+
case "latex": {
|
|
647
|
+
setSysFields(block, blockId, "affine:latex");
|
|
648
|
+
block.set("sys:parent", parentId);
|
|
649
|
+
block.set("sys:children", new Y.Array());
|
|
650
|
+
block.set("prop:xywh", "[0,0,16,16]");
|
|
651
|
+
block.set("prop:index", "a0");
|
|
652
|
+
block.set("prop:lockedBySelf", false);
|
|
653
|
+
block.set("prop:scale", 1);
|
|
654
|
+
block.set("prop:rotate", 0);
|
|
655
|
+
block.set("prop:latex", normalized.latex);
|
|
656
|
+
return { blockId, block, flavour: "affine:latex" };
|
|
657
|
+
}
|
|
658
|
+
case "table": {
|
|
659
|
+
setSysFields(block, blockId, "affine:table");
|
|
660
|
+
block.set("sys:parent", parentId);
|
|
661
|
+
block.set("sys:children", new Y.Array());
|
|
662
|
+
const rows = {};
|
|
663
|
+
const columns = {};
|
|
664
|
+
const cells = {};
|
|
665
|
+
for (let i = 0; i < normalized.rows; i++) {
|
|
666
|
+
const rowId = generateId();
|
|
667
|
+
rows[rowId] = { rowId, order: `r${String(i).padStart(4, "0")}` };
|
|
668
|
+
}
|
|
669
|
+
for (let i = 0; i < normalized.columns; i++) {
|
|
670
|
+
const columnId = generateId();
|
|
671
|
+
columns[columnId] = { columnId, order: `c${String(i).padStart(4, "0")}` };
|
|
672
|
+
}
|
|
673
|
+
for (const rowId of Object.keys(rows)) {
|
|
674
|
+
for (const columnId of Object.keys(columns)) {
|
|
675
|
+
cells[`${rowId}:${columnId}`] = { text: makeText("") };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
block.set("prop:rows", rows);
|
|
679
|
+
block.set("prop:columns", columns);
|
|
680
|
+
block.set("prop:cells", cells);
|
|
681
|
+
block.set("prop:comments", undefined);
|
|
682
|
+
block.set("prop:textAlign", undefined);
|
|
683
|
+
return { blockId, block, flavour: "affine:table" };
|
|
684
|
+
}
|
|
685
|
+
case "bookmark": {
|
|
686
|
+
setSysFields(block, blockId, "affine:bookmark");
|
|
687
|
+
block.set("sys:parent", parentId);
|
|
688
|
+
block.set("sys:children", new Y.Array());
|
|
689
|
+
block.set("prop:style", normalized.bookmarkStyle);
|
|
690
|
+
block.set("prop:url", normalized.url);
|
|
691
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
692
|
+
block.set("prop:description", null);
|
|
693
|
+
block.set("prop:icon", null);
|
|
694
|
+
block.set("prop:image", null);
|
|
695
|
+
block.set("prop:title", null);
|
|
696
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
697
|
+
block.set("prop:index", "a0");
|
|
698
|
+
block.set("prop:lockedBySelf", false);
|
|
699
|
+
block.set("prop:rotate", 0);
|
|
700
|
+
block.set("prop:footnoteIdentifier", null);
|
|
701
|
+
return { blockId, block, flavour: "affine:bookmark" };
|
|
702
|
+
}
|
|
703
|
+
case "image": {
|
|
704
|
+
setSysFields(block, blockId, "affine:image");
|
|
705
|
+
block.set("sys:parent", parentId);
|
|
706
|
+
block.set("sys:children", new Y.Array());
|
|
707
|
+
block.set("prop:caption", normalized.caption ?? "");
|
|
708
|
+
block.set("prop:sourceId", normalized.sourceId);
|
|
709
|
+
block.set("prop:width", 0);
|
|
710
|
+
block.set("prop:height", 0);
|
|
711
|
+
block.set("prop:size", normalized.size || -1);
|
|
712
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
713
|
+
block.set("prop:index", "a0");
|
|
714
|
+
block.set("prop:lockedBySelf", false);
|
|
715
|
+
block.set("prop:rotate", 0);
|
|
716
|
+
return { blockId, block, flavour: "affine:image" };
|
|
717
|
+
}
|
|
718
|
+
case "attachment": {
|
|
719
|
+
setSysFields(block, blockId, "affine:attachment");
|
|
720
|
+
block.set("sys:parent", parentId);
|
|
721
|
+
block.set("sys:children", new Y.Array());
|
|
722
|
+
block.set("prop:name", normalized.name);
|
|
723
|
+
block.set("prop:size", normalized.size);
|
|
724
|
+
block.set("prop:type", normalized.mimeType);
|
|
725
|
+
block.set("prop:sourceId", normalized.sourceId);
|
|
726
|
+
block.set("prop:caption", normalized.caption ?? undefined);
|
|
727
|
+
block.set("prop:embed", normalized.embed);
|
|
728
|
+
block.set("prop:style", "horizontalThin");
|
|
729
|
+
block.set("prop:index", "a0");
|
|
730
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
731
|
+
block.set("prop:lockedBySelf", false);
|
|
732
|
+
block.set("prop:rotate", 0);
|
|
733
|
+
block.set("prop:footnoteIdentifier", null);
|
|
734
|
+
return { blockId, block, flavour: "affine:attachment" };
|
|
735
|
+
}
|
|
736
|
+
case "embed_youtube": {
|
|
737
|
+
setSysFields(block, blockId, "affine:embed-youtube");
|
|
738
|
+
block.set("sys:parent", parentId);
|
|
739
|
+
block.set("sys:children", new Y.Array());
|
|
740
|
+
block.set("prop:index", "a0");
|
|
741
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
742
|
+
block.set("prop:lockedBySelf", false);
|
|
743
|
+
block.set("prop:rotate", 0);
|
|
744
|
+
block.set("prop:style", "video");
|
|
745
|
+
block.set("prop:url", normalized.url);
|
|
746
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
747
|
+
block.set("prop:image", null);
|
|
748
|
+
block.set("prop:title", null);
|
|
749
|
+
block.set("prop:description", null);
|
|
750
|
+
block.set("prop:creator", null);
|
|
751
|
+
block.set("prop:creatorUrl", null);
|
|
752
|
+
block.set("prop:creatorImage", null);
|
|
753
|
+
block.set("prop:videoId", null);
|
|
754
|
+
return { blockId, block, flavour: "affine:embed-youtube" };
|
|
755
|
+
}
|
|
756
|
+
case "embed_github": {
|
|
757
|
+
setSysFields(block, blockId, "affine:embed-github");
|
|
758
|
+
block.set("sys:parent", parentId);
|
|
759
|
+
block.set("sys:children", new Y.Array());
|
|
760
|
+
block.set("prop:index", "a0");
|
|
761
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
762
|
+
block.set("prop:lockedBySelf", false);
|
|
763
|
+
block.set("prop:rotate", 0);
|
|
764
|
+
block.set("prop:style", "horizontal");
|
|
765
|
+
block.set("prop:owner", "");
|
|
766
|
+
block.set("prop:repo", "");
|
|
767
|
+
block.set("prop:githubType", "issue");
|
|
768
|
+
block.set("prop:githubId", "");
|
|
769
|
+
block.set("prop:url", normalized.url);
|
|
770
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
771
|
+
block.set("prop:image", null);
|
|
772
|
+
block.set("prop:status", null);
|
|
773
|
+
block.set("prop:statusReason", null);
|
|
774
|
+
block.set("prop:title", null);
|
|
775
|
+
block.set("prop:description", null);
|
|
776
|
+
block.set("prop:createdAt", null);
|
|
777
|
+
block.set("prop:assignees", null);
|
|
778
|
+
return { blockId, block, flavour: "affine:embed-github" };
|
|
779
|
+
}
|
|
780
|
+
case "embed_figma": {
|
|
781
|
+
setSysFields(block, blockId, "affine:embed-figma");
|
|
782
|
+
block.set("sys:parent", parentId);
|
|
783
|
+
block.set("sys:children", new Y.Array());
|
|
784
|
+
block.set("prop:index", "a0");
|
|
785
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
786
|
+
block.set("prop:lockedBySelf", false);
|
|
787
|
+
block.set("prop:rotate", 0);
|
|
788
|
+
block.set("prop:style", "figma");
|
|
789
|
+
block.set("prop:url", normalized.url);
|
|
790
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
791
|
+
block.set("prop:title", null);
|
|
792
|
+
block.set("prop:description", null);
|
|
793
|
+
return { blockId, block, flavour: "affine:embed-figma" };
|
|
794
|
+
}
|
|
795
|
+
case "embed_loom": {
|
|
796
|
+
setSysFields(block, blockId, "affine:embed-loom");
|
|
797
|
+
block.set("sys:parent", parentId);
|
|
798
|
+
block.set("sys:children", new Y.Array());
|
|
799
|
+
block.set("prop:index", "a0");
|
|
800
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
801
|
+
block.set("prop:lockedBySelf", false);
|
|
802
|
+
block.set("prop:rotate", 0);
|
|
803
|
+
block.set("prop:style", "video");
|
|
804
|
+
block.set("prop:url", normalized.url);
|
|
805
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
806
|
+
block.set("prop:image", null);
|
|
807
|
+
block.set("prop:title", null);
|
|
808
|
+
block.set("prop:description", null);
|
|
809
|
+
block.set("prop:videoId", null);
|
|
810
|
+
return { blockId, block, flavour: "affine:embed-loom" };
|
|
811
|
+
}
|
|
812
|
+
case "embed_html": {
|
|
813
|
+
setSysFields(block, blockId, "affine:embed-html");
|
|
814
|
+
block.set("sys:parent", parentId);
|
|
815
|
+
block.set("sys:children", new Y.Array());
|
|
816
|
+
block.set("prop:index", "a0");
|
|
817
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
818
|
+
block.set("prop:lockedBySelf", false);
|
|
819
|
+
block.set("prop:rotate", 0);
|
|
820
|
+
block.set("prop:style", "html");
|
|
821
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
822
|
+
block.set("prop:html", normalized.html || undefined);
|
|
823
|
+
block.set("prop:design", normalized.design || undefined);
|
|
824
|
+
return { blockId, block, flavour: "affine:embed-html" };
|
|
825
|
+
}
|
|
826
|
+
case "embed_linked_doc": {
|
|
827
|
+
setSysFields(block, blockId, "affine:embed-linked-doc");
|
|
828
|
+
block.set("sys:parent", parentId);
|
|
829
|
+
block.set("sys:children", new Y.Array());
|
|
830
|
+
block.set("prop:index", "a0");
|
|
831
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
832
|
+
block.set("prop:lockedBySelf", false);
|
|
833
|
+
block.set("prop:rotate", 0);
|
|
834
|
+
block.set("prop:style", "horizontal");
|
|
835
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
836
|
+
block.set("prop:pageId", normalized.pageId);
|
|
837
|
+
block.set("prop:title", undefined);
|
|
838
|
+
block.set("prop:description", undefined);
|
|
839
|
+
block.set("prop:footnoteIdentifier", null);
|
|
840
|
+
return { blockId, block, flavour: "affine:embed-linked-doc" };
|
|
841
|
+
}
|
|
842
|
+
case "embed_synced_doc": {
|
|
843
|
+
setSysFields(block, blockId, "affine:embed-synced-doc");
|
|
844
|
+
block.set("sys:parent", parentId);
|
|
845
|
+
block.set("sys:children", new Y.Array());
|
|
846
|
+
block.set("prop:index", "a0");
|
|
847
|
+
block.set("prop:xywh", "[0,0,800,100]");
|
|
848
|
+
block.set("prop:lockedBySelf", false);
|
|
849
|
+
block.set("prop:rotate", 0);
|
|
850
|
+
block.set("prop:style", "syncedDoc");
|
|
851
|
+
block.set("prop:caption", normalized.caption ?? undefined);
|
|
852
|
+
block.set("prop:pageId", normalized.pageId);
|
|
853
|
+
block.set("prop:scale", undefined);
|
|
854
|
+
block.set("prop:preFoldHeight", undefined);
|
|
855
|
+
block.set("prop:title", undefined);
|
|
856
|
+
block.set("prop:description", undefined);
|
|
857
|
+
return { blockId, block, flavour: "affine:embed-synced-doc" };
|
|
858
|
+
}
|
|
859
|
+
case "embed_iframe": {
|
|
860
|
+
setSysFields(block, blockId, "affine:embed-iframe");
|
|
861
|
+
block.set("sys:parent", parentId);
|
|
862
|
+
block.set("sys:children", new Y.Array());
|
|
863
|
+
block.set("prop:index", "a0");
|
|
864
|
+
block.set("prop:xywh", "[0,0,0,0]");
|
|
865
|
+
block.set("prop:lockedBySelf", false);
|
|
866
|
+
block.set("prop:scale", 1);
|
|
867
|
+
block.set("prop:url", normalized.url);
|
|
868
|
+
block.set("prop:iframeUrl", normalized.iframeUrl || normalized.url);
|
|
869
|
+
block.set("prop:width", undefined);
|
|
870
|
+
block.set("prop:height", undefined);
|
|
871
|
+
block.set("prop:caption", normalized.caption ?? null);
|
|
872
|
+
block.set("prop:title", null);
|
|
873
|
+
block.set("prop:description", null);
|
|
874
|
+
return { blockId, block, flavour: "affine:embed-iframe" };
|
|
875
|
+
}
|
|
876
|
+
case "database": {
|
|
877
|
+
setSysFields(block, blockId, "affine:database");
|
|
878
|
+
block.set("sys:parent", parentId);
|
|
879
|
+
block.set("sys:children", new Y.Array());
|
|
880
|
+
block.set("prop:views", new Y.Array());
|
|
881
|
+
block.set("prop:title", makeText(content));
|
|
882
|
+
block.set("prop:cells", new Y.Map());
|
|
883
|
+
block.set("prop:columns", new Y.Array());
|
|
884
|
+
block.set("prop:comments", undefined);
|
|
885
|
+
return { blockId, block, flavour: "affine:database" };
|
|
886
|
+
}
|
|
887
|
+
case "data_view": {
|
|
888
|
+
// AFFiNE 0.26.x currently crashes on raw affine:data-view render path.
|
|
889
|
+
// Keep API compatibility for type="data_view" by mapping it to the stable database block.
|
|
890
|
+
setSysFields(block, blockId, "affine:database");
|
|
891
|
+
block.set("sys:parent", parentId);
|
|
892
|
+
block.set("sys:children", new Y.Array());
|
|
893
|
+
block.set("prop:views", new Y.Array());
|
|
894
|
+
block.set("prop:title", makeText(content));
|
|
895
|
+
block.set("prop:cells", new Y.Map());
|
|
896
|
+
block.set("prop:columns", new Y.Array());
|
|
897
|
+
block.set("prop:comments", undefined);
|
|
898
|
+
return { blockId, block, flavour: "affine:database", blockType: "data_view_fallback" };
|
|
899
|
+
}
|
|
900
|
+
case "surface_ref": {
|
|
901
|
+
setSysFields(block, blockId, "affine:surface-ref");
|
|
902
|
+
block.set("sys:parent", parentId);
|
|
903
|
+
block.set("sys:children", new Y.Array());
|
|
904
|
+
block.set("prop:reference", normalized.reference);
|
|
905
|
+
block.set("prop:caption", normalized.caption ?? "");
|
|
906
|
+
block.set("prop:refFlavour", normalized.refFlavour);
|
|
907
|
+
block.set("prop:comments", undefined);
|
|
908
|
+
return { blockId, block, flavour: "affine:surface-ref" };
|
|
909
|
+
}
|
|
910
|
+
case "frame": {
|
|
911
|
+
setSysFields(block, blockId, "affine:frame");
|
|
912
|
+
block.set("sys:parent", parentId);
|
|
913
|
+
block.set("sys:children", new Y.Array());
|
|
914
|
+
block.set("prop:title", makeText(content || "Frame"));
|
|
915
|
+
block.set("prop:background", normalized.background);
|
|
916
|
+
block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
|
|
917
|
+
block.set("prop:index", "a0");
|
|
918
|
+
block.set("prop:childElementIds", new Y.Map());
|
|
919
|
+
block.set("prop:presentationIndex", "a0");
|
|
920
|
+
block.set("prop:lockedBySelf", false);
|
|
921
|
+
return { blockId, block, flavour: "affine:frame" };
|
|
922
|
+
}
|
|
923
|
+
case "edgeless_text": {
|
|
924
|
+
setSysFields(block, blockId, "affine:edgeless-text");
|
|
925
|
+
block.set("sys:parent", parentId);
|
|
926
|
+
block.set("sys:children", new Y.Array());
|
|
927
|
+
block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
|
|
928
|
+
block.set("prop:index", "a0");
|
|
929
|
+
block.set("prop:lockedBySelf", false);
|
|
930
|
+
block.set("prop:scale", 1);
|
|
931
|
+
block.set("prop:rotate", 0);
|
|
932
|
+
block.set("prop:hasMaxWidth", false);
|
|
933
|
+
block.set("prop:comments", undefined);
|
|
934
|
+
block.set("prop:color", "black");
|
|
935
|
+
block.set("prop:fontFamily", "Inter");
|
|
936
|
+
block.set("prop:fontStyle", "normal");
|
|
937
|
+
block.set("prop:fontWeight", "regular");
|
|
938
|
+
block.set("prop:textAlign", "left");
|
|
939
|
+
return { blockId, block, flavour: "affine:edgeless-text" };
|
|
940
|
+
}
|
|
941
|
+
case "note": {
|
|
942
|
+
setSysFields(block, blockId, "affine:note");
|
|
943
|
+
block.set("sys:parent", parentId);
|
|
944
|
+
block.set("sys:children", new Y.Array());
|
|
945
|
+
block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
|
|
946
|
+
block.set("prop:background", normalized.background);
|
|
947
|
+
block.set("prop:index", "a0");
|
|
948
|
+
block.set("prop:lockedBySelf", false);
|
|
949
|
+
block.set("prop:hidden", false);
|
|
950
|
+
block.set("prop:displayMode", "both");
|
|
951
|
+
const edgeless = new Y.Map();
|
|
952
|
+
const style = new Y.Map();
|
|
953
|
+
style.set("borderRadius", 8);
|
|
954
|
+
style.set("borderSize", 1);
|
|
955
|
+
style.set("borderStyle", "solid");
|
|
956
|
+
style.set("shadowType", "none");
|
|
957
|
+
edgeless.set("style", style);
|
|
958
|
+
block.set("prop:edgeless", edgeless);
|
|
959
|
+
block.set("prop:comments", undefined);
|
|
960
|
+
return { blockId, block, flavour: "affine:note" };
|
|
961
|
+
}
|
|
159
962
|
}
|
|
160
963
|
}
|
|
161
964
|
async function appendBlockInternal(parsed) {
|
|
162
|
-
const
|
|
965
|
+
const normalized = normalizeAppendBlockInput(parsed);
|
|
966
|
+
const workspaceId = normalized.workspaceId || defaults.workspaceId;
|
|
163
967
|
if (!workspaceId)
|
|
164
968
|
throw new Error("workspaceId is required");
|
|
165
969
|
const { endpoint, cookie } = await getCookieAndEndpoint();
|
|
@@ -168,25 +972,24 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
168
972
|
try {
|
|
169
973
|
await joinWorkspace(socket, workspaceId);
|
|
170
974
|
const doc = new Y.Doc();
|
|
171
|
-
const snapshot = await loadDoc(socket, workspaceId,
|
|
975
|
+
const snapshot = await loadDoc(socket, workspaceId, normalized.docId);
|
|
172
976
|
if (snapshot.missing) {
|
|
173
977
|
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
174
978
|
}
|
|
175
979
|
const prevSV = Y.encodeStateVector(doc);
|
|
176
980
|
const blocks = doc.getMap("blocks");
|
|
177
|
-
const
|
|
178
|
-
const { blockId, block, flavour, blockType } = createBlock(
|
|
981
|
+
const context = resolveInsertContext(blocks, normalized);
|
|
982
|
+
const { blockId, block, flavour, blockType } = createBlock(context.parentId, normalized);
|
|
179
983
|
blocks.set(blockId, block);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
984
|
+
if (context.insertIndex >= context.children.length) {
|
|
985
|
+
context.children.push([blockId]);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
context.children.insert(context.insertIndex, [blockId]);
|
|
185
989
|
}
|
|
186
|
-
noteChildren.push([blockId]);
|
|
187
990
|
const delta = Y.encodeStateAsUpdate(doc, prevSV);
|
|
188
|
-
await pushDocUpdate(socket, workspaceId,
|
|
189
|
-
return { appended: true, blockId, flavour, blockType };
|
|
991
|
+
await pushDocUpdate(socket, workspaceId, normalized.docId, Buffer.from(delta).toString("base64"));
|
|
992
|
+
return { appended: true, blockId, flavour, blockType, normalizedType: normalized.type, legacyType: normalized.legacyType || null };
|
|
190
993
|
}
|
|
191
994
|
finally {
|
|
192
995
|
socket.disconnect();
|
|
@@ -228,6 +1031,103 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
228
1031
|
docId: DocId
|
|
229
1032
|
}
|
|
230
1033
|
}, getDocHandler);
|
|
1034
|
+
const readDocHandler = async (parsed) => {
|
|
1035
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
1036
|
+
if (!workspaceId) {
|
|
1037
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
1038
|
+
}
|
|
1039
|
+
const { endpoint, cookie } = await getCookieAndEndpoint();
|
|
1040
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
1041
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie);
|
|
1042
|
+
try {
|
|
1043
|
+
await joinWorkspace(socket, workspaceId);
|
|
1044
|
+
const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
|
|
1045
|
+
if (!snapshot.missing) {
|
|
1046
|
+
return text({
|
|
1047
|
+
docId: parsed.docId,
|
|
1048
|
+
title: null,
|
|
1049
|
+
exists: false,
|
|
1050
|
+
blockCount: 0,
|
|
1051
|
+
blocks: [],
|
|
1052
|
+
plainText: "",
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const doc = new Y.Doc();
|
|
1056
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
1057
|
+
const blocks = doc.getMap("blocks");
|
|
1058
|
+
const pageId = findBlockIdByFlavour(blocks, "affine:page");
|
|
1059
|
+
const noteId = findBlockIdByFlavour(blocks, "affine:note");
|
|
1060
|
+
const visited = new Set();
|
|
1061
|
+
const blockRows = [];
|
|
1062
|
+
const plainTextLines = [];
|
|
1063
|
+
let title = "";
|
|
1064
|
+
const visit = (blockId) => {
|
|
1065
|
+
if (visited.has(blockId))
|
|
1066
|
+
return;
|
|
1067
|
+
visited.add(blockId);
|
|
1068
|
+
const raw = blocks.get(blockId);
|
|
1069
|
+
if (!(raw instanceof Y.Map))
|
|
1070
|
+
return;
|
|
1071
|
+
const flavour = raw.get("sys:flavour");
|
|
1072
|
+
const parentId = raw.get("sys:parent");
|
|
1073
|
+
const type = raw.get("prop:type");
|
|
1074
|
+
const textValue = asText(raw.get("prop:text"));
|
|
1075
|
+
const language = raw.get("prop:language");
|
|
1076
|
+
const checked = raw.get("prop:checked");
|
|
1077
|
+
const childIds = childIdsFrom(raw.get("sys:children"));
|
|
1078
|
+
if (flavour === "affine:page") {
|
|
1079
|
+
title = asText(raw.get("prop:title")) || title;
|
|
1080
|
+
}
|
|
1081
|
+
if (textValue.length > 0) {
|
|
1082
|
+
plainTextLines.push(textValue);
|
|
1083
|
+
}
|
|
1084
|
+
blockRows.push({
|
|
1085
|
+
id: blockId,
|
|
1086
|
+
parentId: typeof parentId === "string" ? parentId : null,
|
|
1087
|
+
flavour: typeof flavour === "string" ? flavour : null,
|
|
1088
|
+
type: typeof type === "string" ? type : null,
|
|
1089
|
+
text: textValue.length > 0 ? textValue : null,
|
|
1090
|
+
checked: typeof checked === "boolean" ? checked : null,
|
|
1091
|
+
language: typeof language === "string" ? language : null,
|
|
1092
|
+
childIds,
|
|
1093
|
+
});
|
|
1094
|
+
for (const childId of childIds) {
|
|
1095
|
+
visit(childId);
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
if (pageId) {
|
|
1099
|
+
visit(pageId);
|
|
1100
|
+
}
|
|
1101
|
+
else if (noteId) {
|
|
1102
|
+
visit(noteId);
|
|
1103
|
+
}
|
|
1104
|
+
for (const [id] of blocks) {
|
|
1105
|
+
const blockId = String(id);
|
|
1106
|
+
if (!visited.has(blockId)) {
|
|
1107
|
+
visit(blockId);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return text({
|
|
1111
|
+
docId: parsed.docId,
|
|
1112
|
+
title: title || null,
|
|
1113
|
+
exists: true,
|
|
1114
|
+
blockCount: blockRows.length,
|
|
1115
|
+
blocks: blockRows,
|
|
1116
|
+
plainText: plainTextLines.join("\n"),
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
finally {
|
|
1120
|
+
socket.disconnect();
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
server.registerTool("read_doc", {
|
|
1124
|
+
title: "Read Document Content",
|
|
1125
|
+
description: "Read document block content via WebSocket snapshot (blocks + plain text).",
|
|
1126
|
+
inputSchema: {
|
|
1127
|
+
workspaceId: WorkspaceId.optional(),
|
|
1128
|
+
docId: DocId,
|
|
1129
|
+
},
|
|
1130
|
+
}, readDocHandler);
|
|
231
1131
|
const publishDocHandler = async (parsed) => {
|
|
232
1132
|
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
233
1133
|
if (!workspaceId) {
|
|
@@ -397,19 +1297,52 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
397
1297
|
blockId: result.blockId,
|
|
398
1298
|
flavour: result.flavour,
|
|
399
1299
|
type: result.blockType || null,
|
|
1300
|
+
normalizedType: result.normalizedType,
|
|
1301
|
+
legacyType: result.legacyType,
|
|
400
1302
|
});
|
|
401
1303
|
};
|
|
402
1304
|
server.registerTool("append_block", {
|
|
403
1305
|
title: "Append Block",
|
|
404
|
-
description: "Append
|
|
1306
|
+
description: "Append document blocks with canonical types and legacy aliases (supports placement + strict validation).",
|
|
405
1307
|
inputSchema: {
|
|
406
1308
|
workspaceId: WorkspaceId.optional(),
|
|
407
1309
|
docId: DocId,
|
|
408
|
-
type:
|
|
1310
|
+
type: z.string().min(1).describe("Block type. Canonical: paragraph|heading|quote|list|code|divider|callout|latex|table|bookmark|image|attachment|embed_youtube|embed_github|embed_figma|embed_loom|embed_html|embed_linked_doc|embed_synced_doc|embed_iframe|database|data_view|surface_ref|frame|edgeless_text|note. Legacy aliases remain supported."),
|
|
409
1311
|
text: z.string().optional().describe("Block content text"),
|
|
1312
|
+
url: z.string().optional().describe("URL for bookmark/embeds"),
|
|
1313
|
+
pageId: z.string().optional().describe("Target page/doc id for linked/synced doc embeds"),
|
|
1314
|
+
iframeUrl: z.string().optional().describe("Override iframe src for embed_iframe"),
|
|
1315
|
+
html: z.string().optional().describe("Raw html for embed_html"),
|
|
1316
|
+
design: z.string().optional().describe("Design payload for embed_html"),
|
|
1317
|
+
reference: z.string().optional().describe("Target id for surface_ref"),
|
|
1318
|
+
refFlavour: z.string().optional().describe("Target flavour for surface_ref (e.g. affine:frame)"),
|
|
1319
|
+
width: z.number().int().min(1).max(10000).optional().describe("Width for frame/edgeless_text/note"),
|
|
1320
|
+
height: z.number().int().min(1).max(10000).optional().describe("Height for frame/edgeless_text/note"),
|
|
1321
|
+
background: z.string().optional().describe("Background for frame/note"),
|
|
1322
|
+
sourceId: z.string().optional().describe("Blob source id for image/attachment"),
|
|
1323
|
+
name: z.string().optional().describe("Attachment file name"),
|
|
1324
|
+
mimeType: z.string().optional().describe("Attachment mime type"),
|
|
1325
|
+
size: z.number().optional().describe("Attachment/image file size in bytes"),
|
|
1326
|
+
embed: z.boolean().optional().describe("Attachment embed mode"),
|
|
1327
|
+
rows: z.number().int().min(1).max(20).optional().describe("Table row count"),
|
|
1328
|
+
columns: z.number().int().min(1).max(20).optional().describe("Table column count"),
|
|
1329
|
+
latex: z.string().optional().describe("Latex expression"),
|
|
1330
|
+
level: z.number().int().min(1).max(6).optional().describe("Heading level for type=heading"),
|
|
1331
|
+
style: AppendBlockListStyle.optional().describe("List style for type=list"),
|
|
1332
|
+
bookmarkStyle: AppendBlockBookmarkStyle.optional().describe("Bookmark card style"),
|
|
410
1333
|
checked: z.boolean().optional().describe("Todo state when type is todo"),
|
|
411
1334
|
language: z.string().optional().describe("Code language when type is code"),
|
|
412
1335
|
caption: z.string().optional().describe("Code caption when type is code"),
|
|
1336
|
+
strict: z.boolean().optional().describe("Strict validation mode (default true)"),
|
|
1337
|
+
placement: z
|
|
1338
|
+
.object({
|
|
1339
|
+
parentId: z.string().optional(),
|
|
1340
|
+
afterBlockId: z.string().optional(),
|
|
1341
|
+
beforeBlockId: z.string().optional(),
|
|
1342
|
+
index: z.number().int().min(0).optional(),
|
|
1343
|
+
})
|
|
1344
|
+
.optional()
|
|
1345
|
+
.describe("Optional insertion target/position"),
|
|
413
1346
|
},
|
|
414
1347
|
}, appendBlockHandler);
|
|
415
1348
|
// DELETE DOC
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "affine-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Model Context Protocol server for AFFiNE - enables AI assistants to interact with AFFiNE workspaces, documents, and collaboration features.",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"form-data": "^4.0.4",
|
|
56
56
|
"node-fetch": "^3.3.2",
|
|
57
57
|
"socket.io-client": "^4.8.1",
|
|
58
|
-
"undici": "^
|
|
58
|
+
"undici": "^7.21.0",
|
|
59
59
|
"yjs": "^13.6.27",
|
|
60
60
|
"zod": "^3.23.8"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@types/node": "^
|
|
63
|
+
"@types/node": "^25.2.3",
|
|
64
64
|
"tsx": "^4.16.2",
|
|
65
65
|
"typescript": "^5.5.4"
|
|
66
66
|
}
|