返回 AiToEarn
live-event-validation.mjs
根目录 / project / aitoearn-web / .agents / skills / impeccable / scripts / live-event-validation.mjs
1 /**
2 * Shared event validation for the live helper server.
3 * Extracted for unit testing (insert mode rules).
4 */
5
6 import { canCreateInsert } from './live-insert-ui.mjs';
7
8 export const VISUAL_ACTIONS = [
9 'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
10 'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
11 ];
12
13 const ID_PATTERN = /^[0-9a-f]{8}$/;
14 const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
15 const INSERT_POSITIONS = new Set(['before', 'after']);
16 const FORBIDDEN_MANUAL_EDIT_TEXT_CHARS = ['<', '{', '}', '`'];
17
18 function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
19 function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
20
21 function validateManualEditText(newText) {
22 if (typeof newText !== 'string') return null;
23 const hits = FORBIDDEN_MANUAL_EDIT_TEXT_CHARS.filter((char) => newText.includes(char));
24 return hits.length > 0 ? hits : null;
25 }
26
27 function validateAnnotationFields(msg) {
28 if (msg.screenshotPath !== undefined && typeof msg.screenshotPath !== 'string') {
29 return 'generate: screenshotPath must be string';
30 }
31 if (msg.comments !== undefined && !Array.isArray(msg.comments)) {
32 return 'generate: comments must be array';
33 }
34 if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) {
35 return 'generate: strokes must be array';
36 }
37 return null;
38 }
39
40 function validateInsertGenerate(msg) {
41 if (!msg.insert || typeof msg.insert !== 'object') return 'generate: insert mode requires insert object';
42 if (!INSERT_POSITIONS.has(msg.insert.position)) return 'generate: insert.position must be before or after';
43 const anchor = msg.insert.anchor;
44 if (!anchor || typeof anchor !== 'object') return 'generate: insert.anchor required';
45 if (!anchor.tagName && !anchor.outerHTML && !(Array.isArray(anchor.classes) && anchor.classes.length)) {
46 return 'generate: insert.anchor needs tagName, classes, or outerHTML';
47 }
48 if (!msg.placeholder || typeof msg.placeholder !== 'object') return 'generate: insert mode requires placeholder dimensions';
49 if (!Number.isFinite(msg.placeholder.width) || !Number.isFinite(msg.placeholder.height)) {
50 return 'generate: placeholder width and height must be numbers';
51 }
52 if (!canCreateInsert({
53 prompt: msg.freeformPrompt,
54 comments: msg.comments,
55 strokes: msg.strokes,
56 })) {
57 return 'generate: insert requires freeformPrompt or annotations';
58 }
59 return validateAnnotationFields(msg);
60 }
61
62 function validateReplaceGenerate(msg) {
63 if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';
64 if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';
65 return validateAnnotationFields(msg);
66 }
67
68 function validateManualEditEvent(msg, label) {
69 if (!isValidId(msg.id)) return label + ': missing or malformed id';
70 if (!msg.pageUrl || typeof msg.pageUrl !== 'string') return label + ': missing pageUrl';
71 if (!msg.element || typeof msg.element !== 'object') return label + ': missing element';
72 if (!Array.isArray(msg.ops) || msg.ops.length === 0) return label + ': ops must be non-empty array';
73 if (msg.ops.length > 100) return label + ': too many ops (max 100)';
74 for (const op of msg.ops) {
75 if (typeof op.ref !== 'string') return label + ': op.ref required';
76 if (typeof op.tag !== 'string') return label + ': op.tag required';
77 if (typeof op.originalText !== 'string') return label + ': op.originalText required';
78 if (op.deleted !== true && typeof op.newText !== 'string') {
79 return label + ': text op requires newText';
80 }
81 if (typeof op.newText === 'string') {
82 if (op.deleted !== true && op.newText.trim().length === 0) {
83 return label + ': newText cannot be empty';
84 }
85 const forbidden = validateManualEditText(op.newText);
86 if (forbidden) {
87 return label + ': newText cannot contain ' + forbidden.join(' ') + ' (plain text only; ask the AI to insert markup)';
88 }
89 }
90 }
91 return null;
92 }
93
94 export function validateEvent(msg) {
95 if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
96 switch (msg.type) {
97 case 'generate':
98 if (!isValidId(msg.id)) return 'generate: missing or malformed id';
99 if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
100 if (msg.mode === 'insert') return validateInsertGenerate(msg);
101 return validateReplaceGenerate(msg);
102 case 'accept':
103 if (!isValidId(msg.id)) return 'accept: missing or malformed id';
104 if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';
105 if (msg.paramValues !== undefined) {
106 if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {
107 return 'accept: paramValues must be an object';
108 }
109 }
110 return null;
111 case 'discard':
112 return isValidId(msg.id) ? null : 'discard: missing or malformed id';
113 case 'checkpoint':
114 if (!isValidId(msg.id)) return 'checkpoint: missing or malformed id';
115 if (!Number.isInteger(msg.revision) || msg.revision < 0) return 'checkpoint: revision must be a non-negative integer';
116 if (msg.paramValues !== undefined && (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues))) {
117 return 'checkpoint: paramValues must be an object';
118 }
119 return null;
120 case 'exit':
121 return null;
122 case 'prefetch':
123 if (!msg.pageUrl || typeof msg.pageUrl !== 'string') return 'prefetch: missing pageUrl';
124 return null;
125 case 'manual_edits':
126 return validateManualEditEvent(msg, 'manual_edits');
127 case 'steer':
128 if (!isValidId(msg.id)) return 'steer: missing or malformed id';
129 if (typeof msg.message !== 'string' || !msg.message.trim()) return 'steer: message required';
130 if (msg.message.length > 4000) return 'steer: message too long';
131 if (msg.pageUrl !== undefined && typeof msg.pageUrl !== 'string') return 'steer: pageUrl must be string';
132 return null;
133 default:
134 return 'Unknown event type: ' + msg.type;
135 }
136 }
137
137 lines Plain Text