feat(plugins): add guarded editing for structured notes

This commit is contained in:
tianma-if committed 2026-10-13 13:25:16 +08:00
1 parent 973a5a09a8
commit 9011a1ce62
36 files changed
+3002 -994

No files matched your search

+63
View File
@@ -143,6 +143,10 @@ The plugin context object (`context`) exposes these capability interfaces:
| Namespace | Permission (Disclosure) | Core Methods | Description |
| :--- | :--- | :--- | :--- |
| **`context.notes`** | `notes:read`<br>`notes:write`<br>`notes:delete` | `query()`, `queryContent()`, `get()`, `create()`, `update()`, `editMarkdown()`, `delete()`, `move()`, `pin()`, `restore()`, `revisions` | Query notes, retrieve bodies, optimistic concurrent edits, and revision history |
| **`context.diagrams`** | `notes:read`<br>`notes:write` | `get()`, `edit()` | Read diagram graphs/layouts and edit nodes and edges in batches |
| **`context.tables`** | `notes:read`<br>`notes:write` | `get()`, `edit()` | Read complete tables and edit fields, records, and cells in batches |
| **`context.posters`** | `notes:read`<br>`notes:write` | `capabilities()`, `get()`, `edit()` | Edit poster elements, layers, groups, styles, and canvas |
| **`context.infographics`** | `notes:read`<br>`notes:write` | `capabilities()`, `get()`, `edit()` | Edit nested infographic data, templates, and themes |
| **`context.notebooks`** | `metadata:read`<br>`metadata:write` | `list()`, `create()`, `update()`, `delete()` | Manage notebook trees, hierarchy, and CRUD operations |
| **`context.tags`** | `metadata:read`<br>`metadata:write` | `list()`, `rename()`, `delete()` | Global tag listing, renaming, and removal |
| **`context.templates`** | `templates:read`<br>`templates:write` | `list()`, `create()`, `update()`, `delete()`, `use()` | Template management and one-click note creation |
@@ -241,6 +245,65 @@ await context.tags.delete("unused-tag");
---
#### Reading and editing structured notes
`notes.query()` / `queryContent()` / `get()` return `kind` (`text`, `mind-map`, `flowchart`, `architecture`, `table`, `poster`, `infographic`, or `video`). Full notes also include a detached `contentJson` copy. The full persisted special-note payload remains available in `contentMarkdown`; reading these fields does not mutate the note.
Read a diagram, table, poster, or infographic through its dedicated `get()` before calling `edit()` with the returned `note.revision`, `note.contentHash`, and `editToken`. The snapshot token also detects unsynced local changes whose cloud revision has not advanced. A batch is fully validated before one repository write. `dryRun: true` returns a preview without saving or emitting a note mutation event; its `note` and `editToken` still refer to the saved baseline.
```ts
const snapshot = await context.diagrams.get(noteId, { includeLayout: true });
const result = await context.diagrams.edit(noteId, {
expectedRevision: snapshot.note.revision,
expectedContentHash: snapshot.note.contentHash,
expectedEditToken: snapshot.editToken,
reflow: "preserve",
operations: [{ op: "update_node", nodeId: "root", changes: { label: "Updated" } }],
});
const table = await context.tables.get(tableNoteId);
await context.tables.edit(tableNoteId, {
expectedRevision: table.note.revision,
expectedContentHash: table.note.contentHash,
expectedEditToken: table.editToken,
operations: [
{ op: "update_record", recordId: "row-1", cells: { "field-1": "Updated" } },
{ op: "add_record", recordId: "row-2", cells: { "field-1": "New" } },
],
});
```
- Diagram operations: `add_node`, `update_node`, `remove_node`, `add_edge`, `update_edge`, `remove_edge`; 1–100 per batch. Removing a node with children requires `cascade: true`. Existing layout is preserved by default; `reflow: "all"` recomputes it. Theme and structure remain unchanged.
- Table operations: `add_field`, `update_field`, `remove_field`, `add_record`, `update_record`, `remove_record`; 1–100 per batch. Cells are keyed by field ID. Attachment writes use resource ID arrays; the host checks note ownership and resolves metadata. New records may specify a unique `recordId`; omitted IDs are generated.
- When schema changes convert or remove populated cells, preview `changedCellCount` / `removedAttachmentCount` with `dryRun`, then explicitly pass `allowDataChanges: true`. Record deletion executes directly; plugins should provide appropriate user interaction.
- Posters accept `edits: [{ id, patch }]` and `operations`: `add`, `remove`, `duplicate`, `reorder`, `group`, `ungroup`, `align`, `lock`, `apply_template`. Element patches run first, ordered operations second, canvas/platform resizing last. Array order is back to front. Discover editable fields, platforms, and fonts with `posters.capabilities()`. Locked elements must be explicitly unlocked before editing/removal. `apply_template` replaces all elements; use only for a requested layout replacement. Images must belong to the note. Changes clear the old preview reference but do not render a new image.
- Infographics support `set_data` / `remove_data` / `insert_data` at nested paths of field names and array indexes; insertions have a separate `index`. Batches contain 1–100 operations. `set_template` / `set_theme` explicitly change template/theme names from `infographics.capabilities()`. Ordinary data edits preserve templates, theme overrides, canvas configuration, and conversation history. The note title is unchanged; use `notes.update()` separately if needed. The existing AntV parser validates changes. The data section may be reformatted, but unrequested parsed values and styles must remain equal. Values that cannot round-trip (such as unsupported arrays or multiline scalars) throw `INVALID_STRUCTURED_EDIT` instead of being truncated or silently dropped. Theme changes replace only the theme name and preserve explicit color/style overrides.
```ts
const poster = await context.posters.get(posterNoteId);
await context.posters.edit(posterNoteId, {
expectedRevision: poster.note.revision,
expectedContentHash: poster.note.contentHash,
expectedEditToken: poster.editToken,
edits: [{ id: "heading", patch: { text: "Updated", fill: "#16A06E" } }],
});
const infographic = await context.infographics.get(infographicNoteId);
await context.infographics.edit(infographicNoteId, {
expectedRevision: infographic.note.revision,
expectedContentHash: infographic.note.contentHash,
expectedEditToken: infographic.editToken,
operations: [{ op: "set_data", path: ["lists", 0, "label"], value: "Updated" }],
});
```
**Compatibility and errors**: Updating title/tags through `notes.update()` preserves the original body. Supplying `contentMarkdown` for a special note, using `notes.editMarkdown()`, or writing to its body through editor Markdown/embed methods throws `STRUCTURED_NOTE_REQUIRED`. Generic Markdown creation/writes also reject special-note markers. Existing plugins must select the API based on `kind`. Wrong note types throw `NOTE_TYPE_MISMATCH`; invalid batches throw `INVALID_STRUCTURED_EDIT`; schema data changes require `TABLE_DATA_CHANGES_REQUIRED`; stale snapshots or unsaved editor changes throw `NOTE_CONFLICT` and require saving/reloading. Dedicated writes use the host repository's existing sync, revision history, and refresh mechanisms. An offline result does not confirm cloud synchronization.
Plugin saves also compare the local snapshot inside the repository transaction, preventing multiple windows from overwriting each other after host validation. Unresolved sync conflicts block further plugin writes and preserve the queued draft and diagnostics. Desktop plugin saves use a dedicated native method that commits content, history, and outbox together. Older hosts without that method throw `HOST_UPGRADE_REQUIRED`; no unguarded fallback is attempted. Ordinary editor save behavior is unchanged. Server history retains the existing five-minute snapshot policy, so each plugin write is not guaranteed a separate cloud revision.
After building the sidecar, maintainers can run `bun run test:plugin-sync` to verify an isolated Web IndexedDB → local API/SQLite → native sidecar → Web round trip, offline persistence, restart, and conflict recovery. `test:desktop:sidecar` includes this check. Set `EDGE_EVER_LEGACY_SIDECAR_PATH` and optionally `EDGE_EVER_LEGACY_MIGRATIONS_PATH` to also verify data created by an older native service upgrading to the current build. All native processes use temporary test directories; the installed desktop application is not launched and real account data is not read. This verifies the native service on the current host and does not replace device or in-app acceptance on other platforms.
### 2. Templates
Manage workspace-shared templates and apply them to create new notes: