Skip to content

Commit 7035edd

Browse files
committed
refactor(ai): Implement Service-Based Client Architecture (#8196)
- Break up monolithic Neo.ai.Client into domain-specific services. - Add Neo.ai.client.Service base class. - Add ComponentService, DataService, and RuntimeService. - Implement dispatch logic using Neo.snakeToCamel for method mapping. - Ensure strict adherence to camelCase for internal service methods.
1 parent 700d58c commit 7035edd

5 files changed

Lines changed: 418 additions & 232 deletions

File tree

src/ai/Client.mjs

Lines changed: 59 additions & 232 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import Base from '../core/Base.mjs';
2-
import ClassSystemUtil from '../util/ClassSystem.mjs';
3-
import Socket from '../data/connection/WebSocket.mjs';
4-
import StoreManager from '../manager/Store.mjs';
1+
import Base from '../core/Base.mjs';
2+
import ClassSystemUtil from '../util/ClassSystem.mjs';
3+
import ComponentService from './client/ComponentService.mjs';
4+
import DataService from './client/DataService.mjs';
5+
import RuntimeService from './client/RuntimeService.mjs';
6+
import Socket from '../data/connection/WebSocket.mjs';
57

68
/**
79
* The AI Client establishes a WebSocket connection to the Neural Link MCP Server.
@@ -40,6 +42,17 @@ class Client extends Base {
4042
* @protected
4143
*/
4244
isConnected = false
45+
/**
46+
* Map JSON-RPC method prefixes to service instances
47+
* @member {Object} serviceMap
48+
* @protected
49+
*/
50+
serviceMap = null
51+
/**
52+
* @member {Object} services=null
53+
* @protected
54+
*/
55+
services = null
4356
/**
4457
* @member {Neo.data.connection.WebSocket|null} socket=null
4558
* @protected
@@ -52,13 +65,37 @@ class Client extends Base {
5265
construct(config) {
5366
super.construct(config);
5467

68+
let me = this;
69+
70+
me.services = {
71+
component: Neo.create(ComponentService, {client: me}),
72+
data : Neo.create(DataService, {client: me}),
73+
runtime : Neo.create(RuntimeService, {client: me})
74+
};
75+
76+
me.serviceMap = {
77+
get_component : me.services.component,
78+
get_vdom : me.services.component,
79+
get_vnode : me.services.component,
80+
query_component : me.services.component,
81+
set_component : me.services.component,
82+
83+
get_record : me.services.data,
84+
inspect_store : me.services.data,
85+
list_stores : me.services.data,
86+
87+
get_drag : me.services.runtime,
88+
get_window : me.services.runtime,
89+
reload_page : me.services.runtime
90+
};
91+
5592
Neo.currentWorker.on({
56-
connect : this.onAppWorkerWindowConnect,
57-
disconnect: this.onAppWorkerWindowDisconnect,
58-
scope : this
93+
connect : me.onAppWorkerWindowConnect,
94+
disconnect: me.onAppWorkerWindowDisconnect,
95+
scope : me
5996
});
6097

61-
this.connect();
98+
me.connect();
6299
}
63100

64101
/**
@@ -200,210 +237,26 @@ class Client extends Base {
200237
* @returns {Promise<*>} The result of the operation
201238
*/
202239
async handleRequest(method, params) {
203-
let me = this,
204-
component;
205-
206-
switch (method) {
207-
case 'get_component_property':
208-
component = Neo.getComponent(params.id);
209-
if (!component) throw new Error(`Component not found: ${params.id}`);
210-
return {value: me.safeSerialize(component[params.property])};
211-
212-
case 'query_component':
213-
let {selector, rootId} = params,
214-
matches = [];
215-
216-
if (rootId) {
217-
component = Neo.getComponent(rootId);
218-
if (!component) throw new Error(`Root component not found: ${rootId}`);
219-
// down() returns a single item or array based on returnFirstMatch param.
220-
// We want all matches, so we pass false.
221-
matches = component.down(selector, false)
222-
} else {
223-
matches = Neo.manager.Component.find(selector)
224-
}
225-
226-
if (!Array.isArray(matches)) {
227-
matches = matches ? [matches] : []
228-
}
229-
230-
return {
231-
components: matches.map(c => ({
232-
id : c.id,
233-
className: c.className,
234-
ntype : c.ntype
235-
}))
236-
};
237-
238-
case 'get_component_tree':
239-
return {tree: me.serializeComponent(me.getComponentRoot(params.rootId), params.depth || -1)};
240-
241-
case 'get_drag_state':
242-
const dragCoordinator = Neo.manager?.DragCoordinator;
243-
244-
if (dragCoordinator) {
245-
return {
246-
activeTargetZone: dragCoordinator.activeTargetZone ? {
247-
id : dragCoordinator.activeTargetZone.id,
248-
sortGroup: dragCoordinator.activeTargetZone.sortGroup,
249-
windowId : dragCoordinator.activeTargetZone.windowId
250-
} : null,
251-
sortZones: Array.from(dragCoordinator.sortZones.entries()).map(([group, map]) => ({
252-
group,
253-
windows: Array.from(map.keys())
254-
}))
255-
}
256-
}
257-
258-
return {};
259-
260-
case 'get_vdom_tree':
261-
component = me.getComponentRoot(params.rootId);
262-
if (!component) throw new Error('Root component not found');
263-
return {vdom: component.vdom};
264-
265-
case 'get_vnode_tree':
266-
component = me.getComponentRoot(params.rootId);
267-
if (!component) throw new Error('Root component not found');
268-
return {vnode: component.vnode};
269-
270-
case 'get_record':
271-
let {recordId, storeId} = params,
272-
record;
273-
274-
if (storeId) {
275-
const store = Neo.get(storeId);
276-
if (!store) throw new Error(`Store not found: ${storeId}`);
277-
record = store.get(recordId)
278-
} else {
279-
const matches = [];
280-
StoreManager.items.forEach(store => {
281-
const rec = store.get(recordId);
282-
if (rec) matches.push(rec)
283-
});
284-
285-
if (matches.length > 1) {
286-
throw new Error(`Multiple records found with ID ${recordId}. Please specify storeId.`)
287-
} else if (matches.length === 1) {
288-
record = matches[0]
289-
}
290-
}
291-
292-
if (!record) throw new Error(`Record not found: ${recordId}`);
293-
294-
return record.toJSON();
295-
296-
case 'get_window_info':
297-
const windowManager = Neo.manager?.Window;
298-
299-
if (windowManager) {
300-
return {
301-
windows: windowManager.items.map(win => ({
302-
id : win.id,
303-
appName : win.appName,
304-
chrome : win.chrome,
305-
innerRect: win.innerRect,
306-
outerRect: win.outerRect
307-
}))
308-
}
309-
}
310-
311-
return {windows: []};
312-
313-
case 'inspect_store':
314-
const store = Neo.get(params.storeId);
315-
if (!store) throw new Error(`Store not found: ${params.storeId}`);
316-
317-
const items = [];
318-
const limit = Math.min(store.count, 50);
319-
320-
for (let i = 0; i < limit; i++) {
321-
const record = store.getAt(i);
322-
if (record) {
323-
items.push(record.toJSON())
324-
}
325-
}
326-
327-
return {
328-
id : store.id,
329-
count : store.count,
330-
model : store.model?.className || 'N/A',
331-
filters: store.exportFilters?.() || [],
332-
sorters: store.exportSorters?.() || [],
333-
items
334-
};
335-
336-
case 'list_stores':
337-
return {
338-
stores: StoreManager.items.map(s => ({
339-
id : s.id,
340-
model : s.model?.className || 'N/A',
341-
count : s.count,
342-
isLoaded: s.isLoaded
343-
}))
344-
};
345-
346-
case 'reload_page':
347-
Neo.Main.reloadWindow();
348-
return {status: 'reloading'};
349-
350-
case 'set_component_property':
351-
component = Neo.getComponent(params.id);
352-
if (!component) throw new Error(`Component not found: ${params.id}`);
353-
component[params.property] = params.value;
354-
return {success: true};
355-
356-
default:
357-
throw new Error(`Unknown method: ${method}`);
358-
}
359-
}
360-
361-
/**
362-
* @param {String} [rootId]
363-
* @returns {Neo.component.Base|null}
364-
*/
365-
getComponentRoot(rootId) {
366-
if (rootId) {
367-
return Neo.getComponent(rootId)
368-
}
369-
370-
const apps = Object.values(Neo.apps || {});
371-
372-
if (apps.length > 0) {
373-
return apps[0].mainView
374-
}
375-
376-
return null
377-
}
378-
379-
/**
380-
* @param {*} value
381-
* @returns {*}
382-
*/
383-
safeSerialize(value) {
384-
const type = Neo.typeOf(value);
385-
386-
if (type === 'NeoInstance') {
387-
return {
388-
neoInstance: true,
389-
id : value.id,
390-
className : value.className
240+
let me = this,
241+
service = null,
242+
prefix;
243+
244+
// Find matching service based on prefix
245+
// e.g. "get_component_property" -> matches "get_component" prefix
246+
for (prefix in me.serviceMap) {
247+
if (method.startsWith(prefix)) {
248+
service = me.serviceMap[prefix];
249+
break
391250
}
392251
}
393252

394-
if (type === 'Object') {
395-
const result = {};
396-
Object.entries(value).forEach(([k, v]) => {
397-
result[k] = this.safeSerialize(v)
398-
});
399-
return result
400-
}
253+
const fnName = Neo.snakeToCamel(method);
401254

402-
if (type === 'Array') {
403-
return value.map(v => this.safeSerialize(v))
255+
if (service && typeof service[fnName] === 'function') {
256+
return service[fnName](params)
404257
}
405258

406-
return value
259+
throw new Error(`Unknown method: ${method}`);
407260
}
408261

409262
/**
@@ -455,32 +308,6 @@ class Client extends Base {
455308
}))
456309
}
457310
}
458-
459-
/**
460-
* @param {Neo.component.Base} component
461-
* @param {Number} maxDepth
462-
* @param {Number} currentDepth
463-
* @returns {Object}
464-
*/
465-
serializeComponent(component, maxDepth, currentDepth=1) {
466-
if (!component) return null;
467-
468-
const result = {
469-
id : component.id,
470-
className: component.className,
471-
ntype : component.ntype
472-
};
473-
474-
if (maxDepth === -1 || currentDepth < maxDepth) {
475-
const children = Neo.manager.Component.getChildren(component);
476-
477-
if (children && children.length > 0) {
478-
result.items = children.map(child => this.serializeComponent(child, maxDepth, currentDepth + 1))
479-
}
480-
}
481-
482-
return result
483-
}
484311
}
485312

486313
export default Neo.setupClass(Client);

0 commit comments

Comments
 (0)