|
|
@@ -8,6 +8,7 @@ import { getWebMcpTools } from './webMcpService';
|
|
8
|
8
|
import { translateWebMcpInput } from '../share/webmcp';
|
|
9
|
9
|
import { listDocuments } from './documentService';
|
|
10
|
10
|
import { blockService } from './blockService';
|
|
|
11
|
+import { useEditorStore } from '../stores/editorStore';
|
|
11
|
12
|
|
|
12
|
13
|
export interface WebMcpChatResult {
|
|
13
|
14
|
handled: boolean;
|
|
|
@@ -52,7 +53,11 @@ const stringifyResult = (result: WebMcpToolResult): string => {
|
|
52
|
53
|
};
|
|
53
|
54
|
|
|
54
|
55
|
const getDocumentCandidates = async (): Promise<ExportCandidate[]> => {
|
|
55
|
|
- if (documentCandidatesCache && documentCandidatesCache.expiresAt > Date.now()) {
|
|
|
56
|
+ const currentDocumentId = useEditorStore.getState().documentId;
|
|
|
57
|
+ const cacheContainsCurrentDocument = !currentDocumentId || documentCandidatesCache?.value.some(
|
|
|
58
|
+ (candidate) => candidate.documentId === currentDocumentId
|
|
|
59
|
+ );
|
|
|
60
|
+ if (documentCandidatesCache && documentCandidatesCache.expiresAt > Date.now() && cacheContainsCurrentDocument) {
|
|
56
|
61
|
return documentCandidatesCache.value;
|
|
57
|
62
|
}
|
|
58
|
63
|
if (documentCandidatesRequest) return documentCandidatesRequest;
|
|
|
@@ -68,6 +73,34 @@ const getDocumentCandidates = async (): Promise<ExportCandidate[]> => {
|
|
68
|
73
|
|
|
69
|
74
|
const loadDocumentCandidates = async (): Promise<ExportCandidate[]> => {
|
|
70
|
75
|
const unique = new Map<string, ExportCandidate>();
|
|
|
76
|
+ const getBlockText = (block: unknown): string => {
|
|
|
77
|
+ if (!block || typeof block !== 'object') return '';
|
|
|
78
|
+ const content = (block as { content?: unknown }).content;
|
|
|
79
|
+ if (typeof content === 'string') return content.trim();
|
|
|
80
|
+ if (Array.isArray(content)) {
|
|
|
81
|
+ return content
|
|
|
82
|
+ .map((part) => part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string'
|
|
|
83
|
+ ? (part as { text: string }).text
|
|
|
84
|
+ : '')
|
|
|
85
|
+ .join('')
|
|
|
86
|
+ .trim();
|
|
|
87
|
+ }
|
|
|
88
|
+ return '';
|
|
|
89
|
+ };
|
|
|
90
|
+ const getFirstBlockText = (blocks: unknown): string =>
|
|
|
91
|
+ Array.isArray(blocks) ? blocks.map(getBlockText).find(Boolean) || '' : '';
|
|
|
92
|
+ const currentEditor = useEditorStore.getState();
|
|
|
93
|
+ if (currentEditor.documentId) {
|
|
|
94
|
+ const currentBlockAliases = currentEditor.blocks
|
|
|
95
|
+ .map(getBlockText)
|
|
|
96
|
+ .filter(Boolean)
|
|
|
97
|
+ .slice(0, 20);
|
|
|
98
|
+ unique.set(currentEditor.documentId, {
|
|
|
99
|
+ documentId: currentEditor.documentId,
|
|
|
100
|
+ title: getFirstBlockText(currentEditor.blocks) || currentEditor.documentId,
|
|
|
101
|
+ aliases: currentBlockAliases,
|
|
|
102
|
+ });
|
|
|
103
|
+ }
|
|
71
|
104
|
try {
|
|
72
|
105
|
const documentResult = await listDocuments({
|
|
73
|
106
|
userId: 'default-user',
|
|
|
@@ -82,23 +115,13 @@ const loadDocumentCandidates = async (): Promise<ExportCandidate[]> => {
|
|
82
|
115
|
? (detail.data as { blocks?: unknown }).blocks
|
|
83
|
116
|
: undefined;
|
|
84
|
117
|
if (!Array.isArray(blocks)) return;
|
|
85
|
|
- const firstText = blocks
|
|
86
|
|
- .map((block) => {
|
|
87
|
|
- if (!block || typeof block !== 'object') return '';
|
|
88
|
|
- const content = (block as { content?: unknown }).content;
|
|
89
|
|
- if (typeof content === 'string') return content.trim();
|
|
90
|
|
- if (Array.isArray(content)) {
|
|
91
|
|
- return content
|
|
92
|
|
- .map((part) => part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string'
|
|
93
|
|
- ? (part as { text: string }).text
|
|
94
|
|
- : '')
|
|
95
|
|
- .join('')
|
|
96
|
|
- .trim();
|
|
97
|
|
- }
|
|
98
|
|
- return '';
|
|
99
|
|
- })
|
|
100
|
|
- .find(Boolean);
|
|
101
|
|
- unique.set(document.id, { documentId: document.id, title: firstText || document.id });
|
|
|
118
|
+ const firstText = getFirstBlockText(blocks);
|
|
|
119
|
+ const existing = unique.get(document.id);
|
|
|
120
|
+ unique.set(document.id, {
|
|
|
121
|
+ ...existing,
|
|
|
122
|
+ documentId: document.id,
|
|
|
123
|
+ title: firstText || existing?.title || document.id,
|
|
|
124
|
+ });
|
|
102
|
125
|
}));
|
|
103
|
126
|
} catch (error) {
|
|
104
|
127
|
console.warn('[WebMCP] 获取文档名称失败,将继续使用导出记录:', error);
|
|
|
@@ -142,10 +165,24 @@ const findDocumentCandidate = (
|
|
142
|
165
|
candidates: ExportCandidate[]
|
|
143
|
166
|
): { candidate?: ExportCandidate; ambiguous: boolean } => {
|
|
144
|
167
|
const normalizedTitle = normalizeDocumentText(title);
|
|
|
168
|
+ const isChineseShortName = (value: string): boolean =>
|
|
|
169
|
+ value.length >= 3 && /^[\u3400-\u9fff]+$/.test(value);
|
|
|
170
|
+ const isOrderedChineseMatch = (shortName: string, fullName: string): boolean => {
|
|
|
171
|
+ if (!isChineseShortName(shortName)) return false;
|
|
|
172
|
+ let shortIndex = 0;
|
|
|
173
|
+ for (const character of fullName) {
|
|
|
174
|
+ if (character === shortName[shortIndex]) shortIndex += 1;
|
|
|
175
|
+ if (shortIndex === shortName.length) return true;
|
|
|
176
|
+ }
|
|
|
177
|
+ return false;
|
|
|
178
|
+ };
|
|
145
|
179
|
const matches = candidates.filter((candidate) => {
|
|
146
|
180
|
const candidateTitles = [candidate.title, ...(candidate.aliases || [])].map(normalizeDocumentText);
|
|
147
|
181
|
return candidate.documentId === title || candidate.recordId === title || candidateTitles.some((candidateTitle) =>
|
|
148
|
|
- candidateTitle === normalizedTitle || candidateTitle.includes(normalizedTitle) || normalizedTitle.includes(candidateTitle)
|
|
|
182
|
+ candidateTitle === normalizedTitle ||
|
|
|
183
|
+ candidateTitle.includes(normalizedTitle) ||
|
|
|
184
|
+ normalizedTitle.includes(candidateTitle) ||
|
|
|
185
|
+ isOrderedChineseMatch(normalizedTitle, candidateTitle)
|
|
149
|
186
|
);
|
|
150
|
187
|
});
|
|
151
|
188
|
return { candidate: matches.length === 1 ? matches[0] : undefined, ambiguous: matches.length > 1 };
|
|
|
@@ -231,8 +268,8 @@ const parseLocalDocumentCommand = async (content: string): Promise<
|
|
231
|
268
|
if (isBlockAction) {
|
|
232
|
269
|
const blockMatch = remainder.match(/\b(block-[\w-]+)\b/i);
|
|
233
|
270
|
blockId = blockMatch?.[1];
|
|
234
|
|
- blockContent = remainder.match(/(?:为|改为|内容为|插入)[::]?\s*(.+)$/)?.[1];
|
|
235
|
|
- remainder = remainder.replace(blockId || '', '').replace(/(?:为|改为|内容为|插入)[::]?\s*.+$/, '').trim();
|
|
|
271
|
+ blockContent = remainder.match(/(?:为|改为|内容为|插入|:|:)\s*(.+)$/)?.[1];
|
|
|
272
|
+ remainder = remainder.replace(blockId || '', '').replace(/(?:为|改为|内容为|插入|:|:)\s*.+$/, '').trim();
|
|
236
|
273
|
const parsedReference = parseBlockReference(remainder);
|
|
237
|
274
|
blockReference = parsedReference.reference;
|
|
238
|
275
|
remainder = parsedReference.remainder;
|
|
|
@@ -267,7 +304,10 @@ const parseLocalDocumentCommand = async (content: string): Promise<
|
|
267
|
304
|
const input: Record<string, unknown> = toolName === 'download_export_record'
|
|
268
|
305
|
? { recordId: candidate.recordId || candidate.documentId }
|
|
269
|
306
|
: { documentId: candidate.documentId };
|
|
270
|
|
- if (blockId) input.blockId = blockId;
|
|
|
307
|
+ if (blockId) {
|
|
|
308
|
+ if (toolName === 'insert_block') input.afterBlockId = blockId;
|
|
|
309
|
+ else input.blockId = blockId;
|
|
|
310
|
+ }
|
|
271
|
311
|
if (toolName === 'update_block' && blockContent) input.content = blockContent;
|
|
272
|
312
|
if (toolName === 'insert_block' && blockContent) {
|
|
273
|
313
|
input.type = 'paragraph';
|