feat: refactor doc write in native (#14272)

This commit is contained in:
DarkSky
2026-01-18 16:31:12 +08:00
committed by GitHub
parent 753b11deeb
commit f373e08583
52 changed files with 7140 additions and 3559 deletions
@@ -166,146 +166,217 @@ export class WorkspaceMcpProvider {
}
);
// Write tools - create and update documents
server.registerTool(
'create_document',
{
title: 'Create Document',
description:
'Create a new document in the workspace with the given title and markdown content. Returns the ID of the created document.',
inputSchema: z.object({
title: z.string().min(1).describe('The title of the new document'),
content: z
.string()
.describe(
'The markdown content for the document body (should NOT include a title H1 - the title parameter will be used)'
),
}),
},
async ({ title, content }) => {
try {
// Check if user can create docs in this workspace
await this.ac
if (env.dev || env.namespaces.canary) {
// Write tools - create and update documents
server.registerTool(
'create_document',
{
title: 'Create Document',
description:
'Create a new document in the workspace with the given title and markdown content. Returns the ID of the created document. This tool not support insert or update database block and image yet.',
inputSchema: z.object({
title: z.string().min(1).describe('The title of the new document'),
content: z
.string()
.describe('The markdown content for the document body'),
}),
},
async ({ title, content }) => {
try {
// Check if user can create docs in this workspace
await this.ac
.user(userId)
.workspace(workspaceId)
.assert('Workspace.CreateDoc');
// Sanitize title by removing newlines and trimming
const sanitizedTitle = title.replace(/[\r\n]+/g, ' ').trim();
if (!sanitizedTitle) {
throw new Error('Title cannot be empty');
}
// Strip any leading H1 from content to prevent duplicates
// Per CommonMark spec, ATX headings allow only 0-3 spaces before the #
// Handles: "# Title", " # Title", "# Title #"
const strippedContent = content.replace(
/^[ \t]{0,3}#\s+[^\n]*#*\s*\n*/,
''
);
// Create the document
const result = await this.writer.createDoc(
workspaceId,
sanitizedTitle,
strippedContent,
userId
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
docId: result.docId,
message: `Document "${title}" created successfully`,
}),
},
],
} as const;
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to create document: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
}
}
);
server.registerTool(
'update_document',
{
title: 'Update Document',
description:
'Update an existing document with new markdown content (body only). Uses structural diffing to apply minimal changes, preserving document history and enabling real-time collaboration. This does NOT update the document title. This tool not support insert or update database block and image yet.',
inputSchema: z.object({
docId: z.string().describe('The ID of the document to update'),
content: z
.string()
.describe(
'The complete new markdown content for the document body (do NOT include a title H1)'
),
}),
},
async ({ docId, content }) => {
const notFoundError: CallToolResult = {
isError: true,
content: [
{
type: 'text',
text: `Doc with id ${docId} not found.`,
},
],
};
// Use can() instead of assert() to avoid leaking doc existence info
const accessible = await this.ac
.user(userId)
.workspace(workspaceId)
.assert('Workspace.CreateDoc');
.doc(docId)
.can('Doc.Update');
// Combine title and content into markdown
// Sanitize title by removing newlines and trimming
const sanitizedTitle = title.replace(/[\r\n]+/g, ' ').trim();
if (!sanitizedTitle) {
throw new Error('Title cannot be empty');
if (!accessible) {
return notFoundError;
}
// Strip any leading H1 from content to prevent duplicates
// Per CommonMark spec, ATX headings allow only 0-3 spaces before the #
// Handles: "# Title", " # Title", "# Title #"
const strippedContent = content.replace(
/^[ \t]{0,3}#\s+[^\n]*#*\s*\n*/,
''
);
try {
// Update the document
await this.writer.updateDoc(workspaceId, docId, content, userId);
const markdown = `# ${sanitizedTitle}\n\n${strippedContent}`;
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
docId,
message: `Document updated successfully`,
}),
},
],
} as const;
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to update document: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
}
}
);
// Create the document
const result = await this.writer.createDoc(
workspaceId,
markdown,
userId
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
docId: result.docId,
message: `Document "${title}" created successfully`,
}),
},
],
} as const;
} catch (error) {
return {
server.registerTool(
'update_document_meta',
{
title: 'Update Document Metadata',
description: 'Update document metadata (currently title only).',
inputSchema: z.object({
docId: z.string().describe('The ID of the document to update'),
title: z.string().min(1).describe('The new document title'),
}),
},
async ({ docId, title }) => {
const notFoundError: CallToolResult = {
isError: true,
content: [
{
type: 'text',
text: `Failed to create document: ${error instanceof Error ? error.message : 'Unknown error'}`,
text: `Doc with id ${docId} not found.`,
},
],
};
}
}
);
server.registerTool(
'update_document',
{
title: 'Update Document',
description:
'Update an existing document with new markdown content. Uses structural diffing to apply minimal changes, preserving document history and enabling real-time collaboration.',
inputSchema: z.object({
docId: z.string().describe('The ID of the document to update'),
content: z
.string()
.describe(
'The complete new markdown content for the document (including title as H1)'
),
}),
},
async ({ docId, content }) => {
const notFoundError: CallToolResult = {
isError: true,
content: [
{
type: 'text',
text: `Doc with id ${docId} not found.`,
},
],
};
// Use can() instead of assert() to avoid leaking doc existence info
const accessible = await this.ac
.user(userId)
.workspace(workspaceId)
.doc(docId)
.can('Doc.Update');
// Use can() instead of assert() to avoid leaking doc existence info
const accessible = await this.ac
.user(userId)
.workspace(workspaceId)
.doc(docId)
.can('Doc.Update');
if (!accessible) {
return notFoundError;
}
if (!accessible) {
return notFoundError;
}
try {
const sanitizedTitle = title.replace(/[\r\n]+/g, ' ').trim();
if (!sanitizedTitle) {
throw new Error('Title cannot be empty');
}
try {
// Update the document
await this.writer.updateDoc(workspaceId, docId, content, userId);
return {
content: [
await this.writer.updateDocMeta(
workspaceId,
docId,
{
type: 'text',
text: JSON.stringify({
success: true,
docId,
message: `Document updated successfully`,
}),
title: sanitizedTitle,
},
],
} as const;
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to update document: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
userId
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
docId,
message: `Document title updated successfully`,
}),
},
],
} as const;
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to update document metadata: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
}
}
}
);
);
}
return server;
}