@mce/ai 0.24.4

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * AI 画布 Agent 的「带类型 action schema」层(参考 tldraw AI SDK 范式)。
3
+ *
4
+ * LLM 不直接改像素,而是产出一组结构化 action;本模块负责**校验 / 清洗**——
5
+ * 拒绝结构非法项、丢弃不存在的节点 id、强制数值有限——再交由插件在单个 undo 事务内
6
+ * 映射到现有 exec 命令执行。校验是纯函数,无编辑器耦合,可独立单测。
7
+ */
8
+ export declare const AI_ALIGN_DIRECTIONS: readonly ["left", "horizontal-center", "right", "top", "vertical-center", "bottom"];
9
+ export type AiAlignDirection = typeof AI_ALIGN_DIRECTIONS[number];
10
+ export type AiAction = {
11
+ type: 'createText';
12
+ text: string;
13
+ x?: number;
14
+ y?: number;
15
+ style?: Record<string, any>;
16
+ } | {
17
+ type: 'createShape';
18
+ x?: number;
19
+ y?: number;
20
+ width?: number;
21
+ height?: number;
22
+ fill?: string;
23
+ path?: string;
24
+ } | {
25
+ type: 'setStyle';
26
+ id: string;
27
+ style: Record<string, any>;
28
+ } | {
29
+ type: 'move';
30
+ id: string;
31
+ x: number;
32
+ y: number;
33
+ } | {
34
+ type: 'delete';
35
+ ids: string[];
36
+ } | {
37
+ type: 'select';
38
+ ids: string[];
39
+ } | {
40
+ type: 'duplicate';
41
+ ids: string[];
42
+ } | {
43
+ type: 'align';
44
+ direction: AiAlignDirection;
45
+ ids?: string[];
46
+ };
47
+ export interface AiActionError {
48
+ index: number;
49
+ reason: string;
50
+ }
51
+ export interface AiValidationResult {
52
+ actions: AiAction[];
53
+ errors: AiActionError[];
54
+ }
55
+ /**
56
+ * 校验并清洗一批 AI action。
57
+ * @param input 任意(通常是 LLM 输出解析后的 JSON)。
58
+ * @param hasNode 判定某节点 id 是否存在于当前文档;默认全部存在(便于纯逻辑测试)。
59
+ */
60
+ export declare function validateAiActions(input: unknown, hasNode?: (id: string) => boolean): AiValidationResult;
61
+ /**
62
+ * 供 LLM 提示的 action schema 描述(JSON 可序列化)。消费方把它喂给模型,
63
+ * 模型据此产出 action 数组,再经 validateAiActions 校验后由 applyAiActions 执行。
64
+ */
65
+ export declare const AI_ACTION_SCHEMA: {
66
+ readonly createText: {
67
+ readonly text: "string (required)";
68
+ readonly x: "number?";
69
+ readonly y: "number?";
70
+ readonly style: "object?";
71
+ };
72
+ readonly createShape: {
73
+ readonly x: "number?";
74
+ readonly y: "number?";
75
+ readonly width: "number?";
76
+ readonly height: "number?";
77
+ readonly fill: "string?";
78
+ readonly path: "svg path data string?";
79
+ };
80
+ readonly setStyle: {
81
+ readonly id: "string (required, existing node)";
82
+ readonly style: "object (required)";
83
+ };
84
+ readonly move: {
85
+ readonly id: "string (required, existing node)";
86
+ readonly x: "number (required)";
87
+ readonly y: "number (required)";
88
+ };
89
+ readonly delete: {
90
+ readonly ids: "string[] (required, existing nodes)";
91
+ };
92
+ readonly select: {
93
+ readonly ids: "string[] (required, existing nodes)";
94
+ };
95
+ readonly duplicate: {
96
+ readonly ids: "string[] (required, existing nodes)";
97
+ };
98
+ readonly align: {
99
+ readonly direction: readonly ["left", "horizontal-center", "right", "top", "vertical-center", "bottom"];
100
+ readonly ids: "string[]? (defaults to current selection)";
101
+ };
102
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { plugin } from './plugin';
2
+ export * from './aiActions';
3
+ export * from './plugin';
4
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,295 @@
1
+ import { createShapeElement, definePlugin } from "mce";
2
+ //#region src/aiActions.ts
3
+ /**
4
+ * AI 画布 Agent 的「带类型 action schema」层(参考 tldraw AI SDK 范式)。
5
+ *
6
+ * LLM 不直接改像素,而是产出一组结构化 action;本模块负责**校验 / 清洗**——
7
+ * 拒绝结构非法项、丢弃不存在的节点 id、强制数值有限——再交由插件在单个 undo 事务内
8
+ * 映射到现有 exec 命令执行。校验是纯函数,无编辑器耦合,可独立单测。
9
+ */
10
+ var AI_ALIGN_DIRECTIONS = [
11
+ "left",
12
+ "horizontal-center",
13
+ "right",
14
+ "top",
15
+ "vertical-center",
16
+ "bottom"
17
+ ];
18
+ function isObject(v) {
19
+ return !!v && typeof v === "object" && !Array.isArray(v);
20
+ }
21
+ function finiteNumber(v) {
22
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
23
+ }
24
+ function sanitizeIds(ids, hasNode) {
25
+ if (!Array.isArray(ids)) return [];
26
+ return ids.filter((id) => typeof id === "string" && hasNode(id));
27
+ }
28
+ /**
29
+ * 校验并清洗一批 AI action。
30
+ * @param input 任意(通常是 LLM 输出解析后的 JSON)。
31
+ * @param hasNode 判定某节点 id 是否存在于当前文档;默认全部存在(便于纯逻辑测试)。
32
+ */
33
+ function validateAiActions(input, hasNode = () => true) {
34
+ const actions = [];
35
+ const errors = [];
36
+ (Array.isArray(input) ? input : []).forEach((raw, index) => {
37
+ if (!isObject(raw)) {
38
+ errors.push({
39
+ index,
40
+ reason: "不是合法对象"
41
+ });
42
+ return;
43
+ }
44
+ switch (raw.type) {
45
+ case "createText":
46
+ if (typeof raw.text !== "string" || raw.text === "") {
47
+ errors.push({
48
+ index,
49
+ reason: "createText 需要非空 text"
50
+ });
51
+ return;
52
+ }
53
+ actions.push({
54
+ type: "createText",
55
+ text: raw.text,
56
+ x: finiteNumber(raw.x),
57
+ y: finiteNumber(raw.y),
58
+ style: isObject(raw.style) ? raw.style : void 0
59
+ });
60
+ return;
61
+ case "createShape":
62
+ actions.push({
63
+ type: "createShape",
64
+ x: finiteNumber(raw.x),
65
+ y: finiteNumber(raw.y),
66
+ width: finiteNumber(raw.width),
67
+ height: finiteNumber(raw.height),
68
+ fill: typeof raw.fill === "string" ? raw.fill : void 0,
69
+ path: typeof raw.path === "string" ? raw.path : void 0
70
+ });
71
+ return;
72
+ case "setStyle":
73
+ if (typeof raw.id !== "string" || !hasNode(raw.id)) {
74
+ errors.push({
75
+ index,
76
+ reason: `setStyle 引用了不存在的 id: ${String(raw.id)}`
77
+ });
78
+ return;
79
+ }
80
+ if (!isObject(raw.style)) {
81
+ errors.push({
82
+ index,
83
+ reason: "setStyle 需要 style 对象"
84
+ });
85
+ return;
86
+ }
87
+ actions.push({
88
+ type: "setStyle",
89
+ id: raw.id,
90
+ style: raw.style
91
+ });
92
+ return;
93
+ case "move": {
94
+ if (typeof raw.id !== "string" || !hasNode(raw.id)) {
95
+ errors.push({
96
+ index,
97
+ reason: `move 引用了不存在的 id: ${String(raw.id)}`
98
+ });
99
+ return;
100
+ }
101
+ const x = finiteNumber(raw.x);
102
+ const y = finiteNumber(raw.y);
103
+ if (x === void 0 || y === void 0) {
104
+ errors.push({
105
+ index,
106
+ reason: "move 需要有限数值 x, y"
107
+ });
108
+ return;
109
+ }
110
+ actions.push({
111
+ type: "move",
112
+ id: raw.id,
113
+ x,
114
+ y
115
+ });
116
+ return;
117
+ }
118
+ case "delete":
119
+ case "select":
120
+ case "duplicate": {
121
+ const ids = sanitizeIds(raw.ids, hasNode);
122
+ if (ids.length === 0) {
123
+ errors.push({
124
+ index,
125
+ reason: `${raw.type} 需要至少一个存在的 id`
126
+ });
127
+ return;
128
+ }
129
+ actions.push({
130
+ type: raw.type,
131
+ ids
132
+ });
133
+ return;
134
+ }
135
+ case "align":
136
+ if (!AI_ALIGN_DIRECTIONS.includes(raw.direction)) {
137
+ errors.push({
138
+ index,
139
+ reason: `align 的 direction 非法: ${String(raw.direction)}`
140
+ });
141
+ return;
142
+ }
143
+ actions.push({
144
+ type: "align",
145
+ direction: raw.direction,
146
+ ids: raw.ids === void 0 ? void 0 : sanitizeIds(raw.ids, hasNode)
147
+ });
148
+ return;
149
+ default: errors.push({
150
+ index,
151
+ reason: `未知 action 类型: ${String(raw.type)}`
152
+ });
153
+ }
154
+ });
155
+ return {
156
+ actions,
157
+ errors
158
+ };
159
+ }
160
+ /**
161
+ * 供 LLM 提示的 action schema 描述(JSON 可序列化)。消费方把它喂给模型,
162
+ * 模型据此产出 action 数组,再经 validateAiActions 校验后由 applyAiActions 执行。
163
+ */
164
+ var AI_ACTION_SCHEMA = {
165
+ createText: {
166
+ text: "string (required)",
167
+ x: "number?",
168
+ y: "number?",
169
+ style: "object?"
170
+ },
171
+ createShape: {
172
+ x: "number?",
173
+ y: "number?",
174
+ width: "number?",
175
+ height: "number?",
176
+ fill: "string?",
177
+ path: "svg path data string?"
178
+ },
179
+ setStyle: {
180
+ id: "string (required, existing node)",
181
+ style: "object (required)"
182
+ },
183
+ move: {
184
+ id: "string (required, existing node)",
185
+ x: "number (required)",
186
+ y: "number (required)"
187
+ },
188
+ delete: { ids: "string[] (required, existing nodes)" },
189
+ select: { ids: "string[] (required, existing nodes)" },
190
+ duplicate: { ids: "string[] (required, existing nodes)" },
191
+ align: {
192
+ direction: AI_ALIGN_DIRECTIONS,
193
+ ids: "string[]? (defaults to current selection)"
194
+ }
195
+ };
196
+ //#endregion
197
+ //#region src/plugin.ts
198
+ function plugin() {
199
+ return definePlugin((editor) => {
200
+ const { root, getNodeById, addElement, selection, exec } = editor;
201
+ /** 收集当前文档所有节点 id,用于校验 action 引用的 id 是否存在。 */
202
+ function collectIds() {
203
+ const ids = /* @__PURE__ */ new Set();
204
+ root.value?.findOne((node) => {
205
+ ids.add(node.id);
206
+ return false;
207
+ });
208
+ return ids;
209
+ }
210
+ function findById(id) {
211
+ return getNodeById(id);
212
+ }
213
+ function selectByIds(ids) {
214
+ selection.value = ids.map(findById).filter((n) => Boolean(n));
215
+ }
216
+ function applyAiActions(input) {
217
+ const ids = collectIds();
218
+ const { actions, errors } = validateAiActions(input, (id) => ids.has(id));
219
+ const created = [];
220
+ actions.forEach((action) => {
221
+ switch (action.type) {
222
+ case "createText": {
223
+ const el = exec("addTextElement", {
224
+ content: action.text,
225
+ style: {
226
+ left: action.x ?? 0,
227
+ top: action.y ?? 0,
228
+ ...action.style
229
+ }
230
+ });
231
+ created.push(el.id);
232
+ break;
233
+ }
234
+ case "createShape": {
235
+ const el = addElement(createShapeElement(action.path ? [{ data: action.path }] : void 0, action.fill), { position: {
236
+ x: action.x ?? 0,
237
+ y: action.y ?? 0
238
+ } });
239
+ if (action.width !== void 0) el.style.width = action.width;
240
+ if (action.height !== void 0) el.style.height = action.height;
241
+ created.push(el.id);
242
+ break;
243
+ }
244
+ case "setStyle": {
245
+ const node = findById(action.id);
246
+ if (node?.style) Object.assign(node.style, action.style);
247
+ break;
248
+ }
249
+ case "move": {
250
+ const node = findById(action.id);
251
+ if (node?.style) {
252
+ node.style.left = action.x;
253
+ node.style.top = action.y;
254
+ }
255
+ break;
256
+ }
257
+ case "select":
258
+ selectByIds(action.ids);
259
+ break;
260
+ case "delete":
261
+ selectByIds(action.ids);
262
+ exec("delete");
263
+ break;
264
+ case "duplicate":
265
+ selectByIds(action.ids);
266
+ exec("duplicate");
267
+ break;
268
+ case "align":
269
+ if (action.ids) selectByIds(action.ids);
270
+ exec("align", action.direction);
271
+ break;
272
+ }
273
+ });
274
+ return {
275
+ created,
276
+ errors
277
+ };
278
+ }
279
+ return {
280
+ name: "mce:ai",
281
+ commands: [{
282
+ command: "applyAiActions",
283
+ handle: applyAiActions
284
+ }, {
285
+ command: "getAiActionSchema",
286
+ handle: () => AI_ACTION_SCHEMA
287
+ }]
288
+ };
289
+ });
290
+ }
291
+ //#endregion
292
+ //#region src/index.ts
293
+ var src_default = plugin;
294
+ //#endregion
295
+ export { AI_ACTION_SCHEMA, AI_ALIGN_DIRECTIONS, src_default as default, plugin, validateAiActions };
@@ -0,0 +1,22 @@
1
+ import type { AiActionError } from './aiActions';
2
+ import { AI_ACTION_SCHEMA } from './aiActions';
3
+ declare global {
4
+ namespace Mce {
5
+ interface AiApplyResult {
6
+ /** 新建元素的 id(按动作顺序)。 */
7
+ created: string[];
8
+ /** 被校验拒绝的动作及原因。 */
9
+ errors: AiActionError[];
10
+ }
11
+ interface Commands {
12
+ /**
13
+ * 应用一批 AI action(通常来自 LLM 输出)。先校验/清洗,再在单个 undo 步骤内
14
+ * 映射到现有命令执行。非法项被跳过并在返回值的 errors 中报告。
15
+ */
16
+ applyAiActions: (actions: unknown) => AiApplyResult;
17
+ /** 返回供 LLM 提示的 action schema 描述。 */
18
+ getAiActionSchema: () => typeof AI_ACTION_SCHEMA;
19
+ }
20
+ }
21
+ }
22
+ export declare function plugin(): import("mce").Plugin;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@mce/ai",
3
+ "type": "module",
4
+ "version": "0.24.4",
5
+ "description": "AI plugin for mce",
6
+ "author": "wxm",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/qq15725/mce",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/qq15725/mce.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/qq15725/mce/issues"
15
+ },
16
+ "keywords": [
17
+ "mce",
18
+ "ai",
19
+ "plugin"
20
+ ],
21
+ "sideEffects": true,
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.js"
27
+ },
28
+ "./*.mjs": "./*.js",
29
+ "./*": "./*"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "browser": "./dist/index.js",
34
+ "typings": "dist/index.d.ts",
35
+ "types": "dist/index.d.ts",
36
+ "typesVersions": {
37
+ "*": {
38
+ "*": [
39
+ "*",
40
+ "dist/*",
41
+ "dist/*.d.ts"
42
+ ]
43
+ }
44
+ },
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "devDependencies": {
49
+ "mce": "0.24.4"
50
+ },
51
+ "peerDependencies": {
52
+ "mce": "^0"
53
+ },
54
+ "scripts": {
55
+ "build:code": "vite build",
56
+ "build:tsc": "NODE_ENV=production tsc --emitDeclarationOnly --project tsconfig.json",
57
+ "build": "pnpm build:code && pnpm build:tsc",
58
+ "lint": "eslint src",
59
+ "typecheck": "tsc --noEmit --project tsconfig.json"
60
+ }
61
+ }