// mcp/src/tools/objectTools.ts
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { SocketClient } from '../client/SocketClient.js';
import type { RoomSnapshot, TreeNode, PageObject, PathPoint } from '@whkerdb/shared';
import { EventType, ObjectType, createTextObject, createPathObject, createImageObject } from '@whkerdb/shared';
import { v4 as uuid } from 'uuid';
/**
* 标注对象工具定义
*/
export const objectTools: Tool[] = [
{
name: 'whkerdb_get_page_objects',
description: '获取页面上的所有标注对象',
inputSchema: {
type: 'object',
properties: {
nodeId: {
type: 'string',
description: '页面节点 ID',
},
},
required: ['nodeId'],
},
},
{
name: 'whkerdb_add_text',
description: '在页面上添加文本标注',
inputSchema: {
type: 'object',
properties: {
nodeId: {
type: 'string',
description: '页面节点 ID',
},
text: {
type: 'string',
description: '文本内容',
},
x: {
type: 'number',
description: 'X 坐标',
},
y: {
type: 'number',
description: 'Y 坐标',
},
fontSize: {
type: 'number',
description: '字体大小(默认 16)',
},
color: {
type: 'string',
description: '文本颜色(默认 #000000)',
},
},
required: ['nodeId', 'text', 'x', 'y'],
},
},
{
name: 'whkerdb_add_path',
description: '在页面上添加路径(画笔/高亮笔)',
inputSchema: {
type: 'object',
properties: {
nodeId: {
type: 'string',
description: '页面节点 ID',
},
points: {
type: 'array',
items: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
},
required: ['x', 'y'],
},
description: '路径点数组',
},
strokeColor: {
type: 'string',
description: '线条颜色(默认 #000000)',
},
strokeWidth: {
type: 'number',
description: '线条宽度(默认 2)',
},
isHighlighter: {
type: 'boolean',
description: '是否为高亮笔(默认 false)',
},
},
required: ['nodeId', 'points'],
},
},
{
name: 'whkerdb_add_image',
description: '在页面上添加图片',
inputSchema: {
type: 'object',
properties: {
nodeId: {
type: 'string',
description: '页面节点 ID',
},
imageId: {
type: 'string',
description: '已上传的图片 ID',
},
x: {
type: 'number',
description: 'X 坐标',
},
y: {
type: 'number',
description: 'Y 坐标',
},
width: {
type: 'number',
description: '图片宽度',
},
height: {
type: 'number',
description: '图片高度',
},
},
required: ['nodeId', 'imageId', 'x', 'y', 'width', 'height'],
},
},
{
name: 'whkerdb_update_object',
description: '更新标注对象的属性',
inputSchema: {
type: 'object',
properties: {
nodeId: {
type: 'string',
description: '页面节点 ID',
},
objectId: {
type: 'string',
description: '对象 ID',
},
x: {
type: 'number',
description: '新的 X 坐标',
},
y: {
type: 'number',
description: '新的 Y 坐标',
},
width: {
type: 'number',
description: '新的宽度',
},
height: {
type: 'number',
description: '新的高度',
},
text: {
type: 'string',
description: '新的文本内容(仅文本对象)',
},
},
required: ['nodeId', 'objectId'],
},
},
{
name: 'whkerdb_delete_object',
description: '删除标注对象',
inputSchema: {
type: 'object',
properties: {
nodeId: {
type: 'string',
description: '页面节点 ID',
},
objectId: {
type: 'string',
description: '对象 ID',
},
},
required: ['nodeId', 'objectId'],
},
},
];
/**
* 辅助函数:从快照中获取节点
*/
function getNodeFromSnapshot(snapshot: RoomSnapshot, nodeId: string): TreeNode | undefined {
const entry = snapshot.noteTree.nodes.find(([id]) => id === nodeId);
return entry?.[1];
}
/**
* 标注对象工具处理器
*/
export function createObjectToolHandlers(getSocketClient: () => SocketClient | null) {
const ensureConnected = () => {
const client = getSocketClient();
if (!client?.isConnected()) {
throw new Error('未连接到任何房间,请先使用 whkerdb_join_room 加入房间');
}
return client;
};
return {
whkerdb_get_page_objects: async (args: { nodeId: string }) => {
const client = ensureConnected();
const snapshot = client.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, args.nodeId);
if (!node) throw new Error(`节点 ${args.nodeId} 不存在`);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
nodeId: args.nodeId,
objectCount: node.objects.length,
objects: node.objects.map((obj) => ({
id: obj.id,
type: obj.type,
x: obj.x,
y: obj.y,
width: obj.width,
height: obj.height,
...(obj.type === 'text' ? { text: (obj as any).text } : {}),
...(obj.type === 'path' ? { pointCount: (obj as any).points?.length } : {}),
...(obj.type === 'image' ? { imageId: (obj as any).imageId } : {}),
})),
},
null,
2
),
},
],
};
},
whkerdb_add_text: async (args: {
nodeId: string;
text: string;
x: number;
y: number;
fontSize?: number;
color?: string;
}) => {
const client = ensureConnected();
const snapshot = client.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, args.nodeId);
if (!node) throw new Error(`节点 ${args.nodeId} 不存在`);
const textObj = createTextObject(
args.text,
args.x,
args.y,
args.fontSize ?? 16,
args.color ?? '#000000'
);
await client.applyEvent({
id: uuid(),
type: EventType.OBJECT_ADDED,
roomId: client.getCurrentRoomId()!,
userId: client.getUserId(),
clientId: client.getClientId(),
timestamp: Date.now(),
version: client.getNextVersion(),
aggregateId: client.getCurrentRoomId()!,
payload: {
nodeId: args.nodeId,
object: textObj,
},
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
objectId: textObj.id,
type: textObj.type,
nodeId: args.nodeId,
},
null,
2
),
},
],
};
},
whkerdb_add_path: async (args: {
nodeId: string;
points: PathPoint[];
strokeColor?: string;
strokeWidth?: number;
isHighlighter?: boolean;
}) => {
const client = ensureConnected();
const snapshot = client.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, args.nodeId);
if (!node) throw new Error(`节点 ${args.nodeId} 不存在`);
if (!args.points || args.points.length < 2) {
throw new Error('路径至少需要 2 个点');
}
const pathObj = createPathObject(
args.points,
args.strokeColor ?? '#000000',
args.strokeWidth ?? 2,
args.isHighlighter ?? false
);
await client.applyEvent({
id: uuid(),
type: EventType.OBJECT_ADDED,
roomId: client.getCurrentRoomId()!,
userId: client.getUserId(),
clientId: client.getClientId(),
timestamp: Date.now(),
version: client.getNextVersion(),
aggregateId: client.getCurrentRoomId()!,
payload: {
nodeId: args.nodeId,
object: pathObj,
},
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
objectId: pathObj.id,
type: pathObj.type,
nodeId: args.nodeId,
pointCount: args.points.length,
},
null,
2
),
},
],
};
},
whkerdb_add_image: async (args: {
nodeId: string;
imageId: string;
x: number;
y: number;
width: number;
height: number;
}) => {
const client = ensureConnected();
const snapshot = client.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, args.nodeId);
if (!node) throw new Error(`节点 ${args.nodeId} 不存在`);
const imageObj = createImageObject(
args.imageId,
args.x,
args.y,
args.width,
args.height
);
await client.applyEvent({
id: uuid(),
type: EventType.OBJECT_ADDED,
roomId: client.getCurrentRoomId()!,
userId: client.getUserId(),
clientId: client.getClientId(),
timestamp: Date.now(),
version: client.getNextVersion(),
aggregateId: client.getCurrentRoomId()!,
payload: {
nodeId: args.nodeId,
object: imageObj,
},
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
objectId: imageObj.id,
type: imageObj.type,
nodeId: args.nodeId,
imageId: args.imageId,
},
null,
2
),
},
],
};
},
whkerdb_update_object: async (args: {
nodeId: string;
objectId: string;
x?: number;
y?: number;
width?: number;
height?: number;
text?: string;
}) => {
const client = ensureConnected();
const snapshot = client.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, args.nodeId);
if (!node) throw new Error(`节点 ${args.nodeId} 不存在`);
const obj = node.objects.find((o) => o.id === args.objectId);
if (!obj) throw new Error(`对象 ${args.objectId} 不存在`);
const changes: Partial<PageObject> = {};
if (args.x !== undefined) changes.x = args.x;
if (args.y !== undefined) changes.y = args.y;
if (args.width !== undefined) changes.width = args.width;
if (args.height !== undefined) changes.height = args.height;
if (args.text !== undefined && obj.type === 'text') {
(changes as any).text = args.text;
}
if (Object.keys(changes).length === 0) {
throw new Error('没有提供要更新的属性');
}
await client.applyEvent({
id: uuid(),
type: EventType.OBJECT_UPDATED,
roomId: client.getCurrentRoomId()!,
userId: client.getUserId(),
clientId: client.getClientId(),
timestamp: Date.now(),
version: client.getNextVersion(),
aggregateId: client.getCurrentRoomId()!,
payload: {
nodeId: args.nodeId,
objectId: args.objectId,
changes,
},
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
nodeId: args.nodeId,
objectId: args.objectId,
changes,
},
null,
2
),
},
],
};
},
whkerdb_delete_object: async (args: { nodeId: string; objectId: string }) => {
const client = ensureConnected();
const snapshot = client.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, args.nodeId);
if (!node) throw new Error(`节点 ${args.nodeId} 不存在`);
const obj = node.objects.find((o) => o.id === args.objectId);
if (!obj) throw new Error(`对象 ${args.objectId} 不存在`);
await client.applyEvent({
id: uuid(),
type: EventType.OBJECT_DELETED,
roomId: client.getCurrentRoomId()!,
userId: client.getUserId(),
clientId: client.getClientId(),
timestamp: Date.now(),
version: client.getNextVersion(),
aggregateId: client.getCurrentRoomId()!,
payload: {
nodeId: args.nodeId,
objectId: args.objectId,
},
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
nodeId: args.nodeId,
deletedObjectId: args.objectId,
},
null,
2
),
},
],
};
},
};
}