@monaco-ai-editor/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1074 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +295 -0
- package/dist/index.d.ts +295 -0
- package/dist/index.js +1032 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/ai/aiCompletion.ts +310 -0
- package/src/bus/EditorBus.ts +102 -0
- package/src/bus/EventEmitter.ts +33 -0
- package/src/completion/sqlCompletion.ts +151 -0
- package/src/constants.ts +71 -0
- package/src/controllers/AiChatController.ts +198 -0
- package/src/controllers/EditorController.ts +231 -0
- package/src/index.ts +47 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type * as MonacoType from 'monaco-editor';
|
|
2
|
+
import { SQL_KEYWORDS, SQL_FUNCTIONS, SQL_DATA_TYPES } from '../constants';
|
|
3
|
+
import type { SqlSchema } from '../types';
|
|
4
|
+
|
|
5
|
+
/** 从 SQL 文档中动态解析表名、CTE 别名和变量 */
|
|
6
|
+
function parseDynamicItems(sql: string): { tables: string[]; variables: string[] } {
|
|
7
|
+
const tables: string[] = [];
|
|
8
|
+
const variables: string[] = [];
|
|
9
|
+
let m: RegExpExecArray | null;
|
|
10
|
+
|
|
11
|
+
const tableRe = /\b(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+([`"']?\w+[`"']?)/gi;
|
|
12
|
+
while ((m = tableRe.exec(sql)) !== null) {
|
|
13
|
+
const name = m[1].replace(/[`"']/g, '');
|
|
14
|
+
if (!tables.includes(name)) tables.push(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cteRe = /\bWITH\s+(\w+)\s+AS\s*\(/gi;
|
|
18
|
+
while ((m = cteRe.exec(sql)) !== null) {
|
|
19
|
+
if (!tables.includes(m[1])) tables.push(m[1]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const declareRe = /\bDECLARE\s+(@?\w+)/gi;
|
|
23
|
+
while ((m = declareRe.exec(sql)) !== null) {
|
|
24
|
+
if (!variables.includes(m[1])) variables.push(m[1]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const setVarRe = /\bSET\s+(@\w+)\s*=/gi;
|
|
28
|
+
while ((m = setVarRe.exec(sql)) !== null) {
|
|
29
|
+
if (!variables.includes(m[1])) variables.push(m[1]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const atVarRe = /@(\w+)/g;
|
|
33
|
+
while ((m = atVarRe.exec(sql)) !== null) {
|
|
34
|
+
const v = `@${m[1]}`;
|
|
35
|
+
if (!variables.includes(v)) variables.push(v);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { tables, variables };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 注册 SQL 内置补全提供者。
|
|
43
|
+
* schemaRef 使用 ref 模式以支持动态更新。
|
|
44
|
+
* excludeLabelsRef(可选)使用 ref 模式,提供需要从内置关键字/函数中排除的 label
|
|
45
|
+
* (大写匹配)。用于让使用方的自定义词表覆盖内置同名项,避免重复提示。
|
|
46
|
+
*/
|
|
47
|
+
export function registerSqlCompletion(
|
|
48
|
+
monaco: typeof MonacoType,
|
|
49
|
+
schemaRef: { readonly current: SqlSchema },
|
|
50
|
+
excludeLabelsRef?: { readonly current: Set<string> },
|
|
51
|
+
): MonacoType.IDisposable {
|
|
52
|
+
return monaco.languages.registerCompletionItemProvider('sql', {
|
|
53
|
+
triggerCharacters: ['.'],
|
|
54
|
+
provideCompletionItems(model, position, context) {
|
|
55
|
+
const word = model.getWordUntilPosition(position);
|
|
56
|
+
const range: MonacoType.IRange = {
|
|
57
|
+
startLineNumber: position.lineNumber,
|
|
58
|
+
endLineNumber: position.lineNumber,
|
|
59
|
+
startColumn: word.startColumn,
|
|
60
|
+
endColumn: word.endColumn,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 点号触发:提示该表的列名
|
|
64
|
+
if (context.triggerCharacter === '.') {
|
|
65
|
+
const lineText = model.getLineContent(position.lineNumber);
|
|
66
|
+
const textBeforeDot = lineText.substring(0, position.column - 2);
|
|
67
|
+
const tableMatch = textBeforeDot.match(/(\w+)$/);
|
|
68
|
+
if (tableMatch) {
|
|
69
|
+
const typed = tableMatch[1].toUpperCase();
|
|
70
|
+
const schema = schemaRef.current;
|
|
71
|
+
const tableKey = Object.keys(schema).find((k) => k.toUpperCase() === typed);
|
|
72
|
+
if (tableKey) {
|
|
73
|
+
const colRange: MonacoType.IRange = {
|
|
74
|
+
startLineNumber: position.lineNumber,
|
|
75
|
+
endLineNumber: position.lineNumber,
|
|
76
|
+
startColumn: position.column,
|
|
77
|
+
endColumn: position.column,
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
suggestions: schema[tableKey].map((col) => ({
|
|
81
|
+
label: col,
|
|
82
|
+
kind: monaco.languages.CompletionItemKind.Field,
|
|
83
|
+
insertText: col,
|
|
84
|
+
range: colRange,
|
|
85
|
+
detail: `${tableKey} · 列`,
|
|
86
|
+
documentation: `表 ${tableKey} 的字段`,
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { suggestions: [] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 普通触发:关键字 + 函数 + 类型 + 表名 + 变量
|
|
95
|
+
const fullText = model.getValue();
|
|
96
|
+
const { tables: dynamicTables, variables } = parseDynamicItems(fullText);
|
|
97
|
+
const schema = schemaRef.current;
|
|
98
|
+
const schemaTables = Object.keys(schema);
|
|
99
|
+
const allTables = [...new Set([...schemaTables, ...dynamicTables])];
|
|
100
|
+
|
|
101
|
+
// 需要排除的内置 label(大写匹配),让使用方自定义词表覆盖同名项
|
|
102
|
+
const exclude = excludeLabelsRef?.current;
|
|
103
|
+
const isExcluded = (label: string) => exclude?.has(label.toUpperCase()) ?? false;
|
|
104
|
+
|
|
105
|
+
const keywords = SQL_KEYWORDS.filter((kw) => !isExcluded(kw)).map((kw) => ({
|
|
106
|
+
label: kw,
|
|
107
|
+
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
108
|
+
insertText: kw,
|
|
109
|
+
range,
|
|
110
|
+
detail: 'SQL 关键字',
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
const functions = SQL_FUNCTIONS.filter((fn) => !isExcluded(fn)).map((fn) => ({
|
|
114
|
+
label: fn,
|
|
115
|
+
kind: monaco.languages.CompletionItemKind.Function,
|
|
116
|
+
insertText: `${fn}($1)`,
|
|
117
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
118
|
+
range,
|
|
119
|
+
detail: 'SQL 函数',
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const dataTypes = SQL_DATA_TYPES.map((dt) => ({
|
|
123
|
+
label: dt,
|
|
124
|
+
kind: monaco.languages.CompletionItemKind.TypeParameter,
|
|
125
|
+
insertText: dt,
|
|
126
|
+
range,
|
|
127
|
+
detail: 'SQL 数据类型',
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const tableItems = allTables.map((t) => ({
|
|
131
|
+
label: t,
|
|
132
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
133
|
+
insertText: t,
|
|
134
|
+
range,
|
|
135
|
+
detail: schemaTables.includes(t) ? '表名 (Schema)' : '表名 (文档)',
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
const variableItems = variables.map((v) => ({
|
|
139
|
+
label: v,
|
|
140
|
+
kind: monaco.languages.CompletionItemKind.Variable,
|
|
141
|
+
insertText: v,
|
|
142
|
+
range,
|
|
143
|
+
detail: 'SQL 变量',
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
suggestions: [...keywords, ...functions, ...dataTypes, ...tableItems, ...variableItems],
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const SQL_KEYWORDS = [
|
|
2
|
+
'SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
|
|
3
|
+
'CREATE', 'TABLE', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'INDEX', 'VIEW', 'DATABASE',
|
|
4
|
+
'SCHEMA', 'TRUNCATE', 'RENAME', 'AS', 'ON', 'JOIN', 'INNER', 'LEFT', 'RIGHT',
|
|
5
|
+
'FULL', 'OUTER', 'CROSS', 'NATURAL', 'USING', 'AND', 'OR', 'NOT', 'IN', 'EXISTS',
|
|
6
|
+
'BETWEEN', 'LIKE', 'IS', 'NULL', 'TRUE', 'FALSE', 'CASE', 'WHEN', 'THEN', 'ELSE',
|
|
7
|
+
'END', 'IF', 'UNION', 'ALL', 'DISTINCT', 'ORDER', 'BY', 'ASC', 'DESC', 'GROUP',
|
|
8
|
+
'HAVING', 'LIMIT', 'OFFSET', 'FETCH', 'NEXT', 'ROWS', 'ONLY', 'WITH', 'RECURSIVE',
|
|
9
|
+
'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'UNIQUE', 'NOT NULL', 'DEFAULT',
|
|
10
|
+
'CHECK', 'CONSTRAINT', 'AUTO_INCREMENT', 'IDENTITY', 'SEQUENCE', 'GRANT',
|
|
11
|
+
'REVOKE', 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'BEGIN', 'SAVEPOINT', 'EXPLAIN',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const SQL_FUNCTIONS = [
|
|
15
|
+
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'COALESCE', 'NULLIF', 'IFNULL', 'NVL',
|
|
16
|
+
'CONCAT', 'LENGTH', 'SUBSTR', 'SUBSTRING', 'TRIM', 'LTRIM', 'RTRIM', 'UPPER',
|
|
17
|
+
'LOWER', 'REPLACE', 'INSTR', 'LPAD', 'RPAD', 'CHAR_LENGTH', 'POSITION',
|
|
18
|
+
'NOW', 'CURDATE', 'CURTIME', 'DATE', 'TIME', 'YEAR', 'MONTH', 'DAY',
|
|
19
|
+
'HOUR', 'MINUTE', 'SECOND', 'DATEDIFF', 'DATE_ADD', 'DATE_SUB', 'DATEADD',
|
|
20
|
+
'DATESUB', 'EXTRACT', 'TO_DATE', 'TO_CHAR', 'DATE_FORMAT', 'TIMESTAMPDIFF',
|
|
21
|
+
'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MOD', 'POWER', 'SQRT', 'SIGN', 'RAND',
|
|
22
|
+
'CAST', 'CONVERT', 'ROW_NUMBER', 'RANK', 'DENSE_RANK', 'NTILE', 'LAG', 'LEAD',
|
|
23
|
+
'FIRST_VALUE', 'LAST_VALUE', 'OVER', 'PARTITION', 'LISTAGG', 'GROUP_CONCAT',
|
|
24
|
+
'STRING_AGG', 'JSON_OBJECT', 'JSON_ARRAY', 'JSON_EXTRACT', 'ISNULL',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const SQL_DATA_TYPES = [
|
|
28
|
+
'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'DECIMAL', 'NUMERIC',
|
|
29
|
+
'FLOAT', 'DOUBLE', 'REAL', 'CHAR', 'VARCHAR', 'TEXT', 'NCHAR', 'NVARCHAR',
|
|
30
|
+
'NTEXT', 'DATE', 'TIME', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'BOOL',
|
|
31
|
+
'BLOB', 'CLOB', 'BINARY', 'VARBINARY', 'JSON', 'UUID', 'SERIAL',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const LANGUAGES = [
|
|
35
|
+
'javascript',
|
|
36
|
+
'typescript',
|
|
37
|
+
'python',
|
|
38
|
+
'json',
|
|
39
|
+
'css',
|
|
40
|
+
'html',
|
|
41
|
+
'markdown',
|
|
42
|
+
'rust',
|
|
43
|
+
'go',
|
|
44
|
+
'sql',
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
/** Monaco 编辑器支持的 UI 本地化语言 */
|
|
48
|
+
export const LOCALES = {
|
|
49
|
+
ZH_CN: 'zh-cn',
|
|
50
|
+
EN: 'en',
|
|
51
|
+
DE: 'de',
|
|
52
|
+
ES: 'es',
|
|
53
|
+
FR: 'fr',
|
|
54
|
+
IT: 'it',
|
|
55
|
+
JA: 'ja',
|
|
56
|
+
KO: 'ko',
|
|
57
|
+
RU: 'ru',
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
export type Locale = (typeof LOCALES)[keyof typeof LOCALES];
|
|
61
|
+
|
|
62
|
+
export const THEMES = [
|
|
63
|
+
{ value: 'vs-dark', label: 'Dark' },
|
|
64
|
+
{ value: 'light', label: 'Light' },
|
|
65
|
+
{ value: 'hc-black', label: 'High Contrast' },
|
|
66
|
+
] as const;
|
|
67
|
+
|
|
68
|
+
export const AI_INLINE_LANGUAGES = [
|
|
69
|
+
'typescript', 'javascript', 'python', 'sql',
|
|
70
|
+
'java', 'cpp', 'csharp', 'go', 'rust',
|
|
71
|
+
];
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { EventEmitter } from '../bus/EventEmitter';
|
|
2
|
+
import type { ApiConfig, ChatMessage } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface AiChatControllerEvents {
|
|
5
|
+
messageAdded: ChatMessage;
|
|
6
|
+
messageUpdated: ChatMessage;
|
|
7
|
+
cleared: void;
|
|
8
|
+
loadingChange: { loading: boolean };
|
|
9
|
+
error: { message: string };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateUUID(): string {
|
|
13
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
14
|
+
return crypto.randomUUID();
|
|
15
|
+
}
|
|
16
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
17
|
+
const r = (Math.random() * 16) | 0;
|
|
18
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
19
|
+
return v.toString(16);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* AI 聊天控制器:管理消息列表、API 调用与流式响应。
|
|
25
|
+
* 框架无关:通过事件通知 UI 层更新。
|
|
26
|
+
*/
|
|
27
|
+
export class AiChatController extends EventEmitter<AiChatControllerEvents> {
|
|
28
|
+
private messages: ChatMessage[] = [];
|
|
29
|
+
private abortController: AbortController | null = null;
|
|
30
|
+
private loading = false;
|
|
31
|
+
private apiConfig: ApiConfig;
|
|
32
|
+
|
|
33
|
+
constructor(apiConfig: ApiConfig) {
|
|
34
|
+
super();
|
|
35
|
+
this.apiConfig = apiConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setApiConfig(config: ApiConfig): void {
|
|
39
|
+
this.apiConfig = config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getApiConfig(): ApiConfig {
|
|
43
|
+
return this.apiConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getMessages(): readonly ChatMessage[] {
|
|
47
|
+
return this.messages;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isLoading(): boolean {
|
|
51
|
+
return this.loading;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 发送消息,可选携带编辑器上下文 */
|
|
55
|
+
async send(
|
|
56
|
+
userInput: string,
|
|
57
|
+
options?: { editorContent?: string; language?: string },
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const trimmed = userInput.trim();
|
|
60
|
+
if (!trimmed || this.loading) return;
|
|
61
|
+
|
|
62
|
+
if (!this.apiConfig.apiKey.trim()) {
|
|
63
|
+
const userMsg: ChatMessage = { id: generateUUID(), role: 'user', content: trimmed };
|
|
64
|
+
this.messages.push(userMsg);
|
|
65
|
+
this.emit('messageAdded', userMsg);
|
|
66
|
+
const assistantMsg: ChatMessage = {
|
|
67
|
+
id: generateUUID(),
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
content: '⚠️ 尚未配置 API Key,请配置 Base URL、API Key 和模型名称后再开始对话。',
|
|
70
|
+
};
|
|
71
|
+
this.messages.push(assistantMsg);
|
|
72
|
+
this.emit('messageAdded', assistantMsg);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const userMsg: ChatMessage = { id: generateUUID(), role: 'user', content: trimmed };
|
|
77
|
+
this.messages.push(userMsg);
|
|
78
|
+
this.emit('messageAdded', userMsg);
|
|
79
|
+
|
|
80
|
+
const assistantMsg: ChatMessage = {
|
|
81
|
+
id: generateUUID(),
|
|
82
|
+
role: 'assistant',
|
|
83
|
+
content: '',
|
|
84
|
+
isStreaming: true,
|
|
85
|
+
};
|
|
86
|
+
this.messages.push(assistantMsg);
|
|
87
|
+
this.emit('messageAdded', assistantMsg);
|
|
88
|
+
|
|
89
|
+
this.loading = true;
|
|
90
|
+
this.emit('loadingChange', { loading: true });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
this.abortController = new AbortController();
|
|
94
|
+
|
|
95
|
+
const systemPrompt = options?.language
|
|
96
|
+
? `You are an expert coding assistant. The user is working with ${options.language} code. Respond concisely in the same language as the user's message, defaulting to Chinese.`
|
|
97
|
+
: "You are an expert coding assistant. Respond concisely in the same language as the user's message, defaulting to Chinese.";
|
|
98
|
+
|
|
99
|
+
const historyForApi = this.messages
|
|
100
|
+
.filter((m) => m.id !== assistantMsg.id)
|
|
101
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
102
|
+
|
|
103
|
+
const response = await fetch(`${this.apiConfig.baseUrl}/chat/completions`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
Authorization: `Bearer ${this.apiConfig.apiKey}`,
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
model: this.apiConfig.model,
|
|
111
|
+
messages: [{ role: 'system', content: systemPrompt }, ...historyForApi],
|
|
112
|
+
stream: true,
|
|
113
|
+
}),
|
|
114
|
+
signal: this.abortController.signal,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
let errText = '';
|
|
119
|
+
try {
|
|
120
|
+
errText = await response.text();
|
|
121
|
+
} catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
let hint = '';
|
|
125
|
+
if (response.status === 401) hint = '(API Key 无效或已过期)';
|
|
126
|
+
else if (response.status === 403) hint = '(无访问权限,请检查 API Key 和 Base URL)';
|
|
127
|
+
else if (response.status === 429) hint = '(请求过于频繁,请稍后再试)';
|
|
128
|
+
else if (response.status >= 500) hint = '(服务端错误)';
|
|
129
|
+
throw new Error(`HTTP ${response.status}${hint}${errText ? `\n${errText}` : ''}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const reader = response.body!.getReader();
|
|
133
|
+
const decoder = new TextDecoder();
|
|
134
|
+
let buffer = '';
|
|
135
|
+
let fullContent = '';
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line no-constant-condition
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
if (done) break;
|
|
141
|
+
buffer += decoder.decode(value, { stream: true });
|
|
142
|
+
const lines = buffer.split('\n');
|
|
143
|
+
buffer = lines.pop() ?? '';
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (!line.startsWith('data: ')) continue;
|
|
146
|
+
const data = line.slice(6).trim();
|
|
147
|
+
if (data === '[DONE]') break;
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(data) as {
|
|
150
|
+
choices?: Array<{ delta?: { content?: string } }>;
|
|
151
|
+
};
|
|
152
|
+
const delta = parsed.choices?.[0]?.delta?.content ?? '';
|
|
153
|
+
if (delta) {
|
|
154
|
+
fullContent += delta;
|
|
155
|
+
assistantMsg.content = fullContent;
|
|
156
|
+
this.emit('messageUpdated', { ...assistantMsg });
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
assistantMsg.isStreaming = false;
|
|
165
|
+
this.emit('messageUpdated', { ...assistantMsg });
|
|
166
|
+
} catch (err: unknown) {
|
|
167
|
+
const isAbort = err instanceof Error && err.name === 'AbortError';
|
|
168
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
169
|
+
const isNetworkErr = err instanceof TypeError && errMsg.toLowerCase().includes('fetch');
|
|
170
|
+
const errorContent = isAbort
|
|
171
|
+
? '⏹ 请求已取消'
|
|
172
|
+
: isNetworkErr
|
|
173
|
+
? `⚠️ 网络错误,请检查 Base URL 和网络。\n\n${errMsg}`
|
|
174
|
+
: `⚠️ 请求失败:${errMsg}`;
|
|
175
|
+
assistantMsg.content = errorContent;
|
|
176
|
+
assistantMsg.isStreaming = false;
|
|
177
|
+
this.emit('messageUpdated', { ...assistantMsg });
|
|
178
|
+
if (!isAbort) this.emit('error', { message: errMsg });
|
|
179
|
+
} finally {
|
|
180
|
+
this.loading = false;
|
|
181
|
+
this.emit('loadingChange', { loading: false });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
stop(): void {
|
|
186
|
+
this.abortController?.abort();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clear(): void {
|
|
190
|
+
this.messages = [];
|
|
191
|
+
this.emit('cleared', undefined);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
dispose(): void {
|
|
195
|
+
this.abortController?.abort();
|
|
196
|
+
this.removeAllListeners();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type * as MonacoType from 'monaco-editor';
|
|
2
|
+
import { EventEmitter } from '../bus/EventEmitter';
|
|
3
|
+
import type { ApiConfig, PendingCompletion, SqlSchema, CompletionProvider } from '../types';
|
|
4
|
+
import { registerSqlCompletion } from '../completion/sqlCompletion';
|
|
5
|
+
import { registerAiInlineCompletion } from '../ai/aiCompletion';
|
|
6
|
+
|
|
7
|
+
export interface EditorControllerEvents {
|
|
8
|
+
change: { value: string };
|
|
9
|
+
cursor: { line: number; column: number };
|
|
10
|
+
ready: { editor: MonacoType.editor.IStandaloneCodeEditor };
|
|
11
|
+
languageChange: { language: string };
|
|
12
|
+
focus: void;
|
|
13
|
+
blur: void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EditorControllerOptions {
|
|
17
|
+
container: HTMLElement;
|
|
18
|
+
language?: string;
|
|
19
|
+
theme?: string;
|
|
20
|
+
value?: string;
|
|
21
|
+
monaco: typeof MonacoType;
|
|
22
|
+
/** Monaco loader monaco-editor 路径配置 */
|
|
23
|
+
vsPath?: string;
|
|
24
|
+
sqlSchema?: SqlSchema;
|
|
25
|
+
apiConfig?: ApiConfig | null;
|
|
26
|
+
completionProviders?: CompletionProvider[];
|
|
27
|
+
/** Monaco editor options */
|
|
28
|
+
editorOptions?: MonacoType.editor.IStandaloneEditorConstructionOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 框架无关的编辑器控制器。
|
|
33
|
+
* 封装 Monaco 实例的生命周期、核心操作、补全注册。
|
|
34
|
+
* 通过 EventEmitter 暴露编辑器事件,不依赖任何 UI 框架。
|
|
35
|
+
*/
|
|
36
|
+
export class EditorController extends EventEmitter<EditorControllerEvents> {
|
|
37
|
+
private editor: MonacoType.editor.IStandaloneCodeEditor | null = null;
|
|
38
|
+
private monaco: typeof MonacoType;
|
|
39
|
+
private sqlProviderRegistered = false;
|
|
40
|
+
private aiProviderRegistered = false;
|
|
41
|
+
private pendingCompletionRef = { current: null as PendingCompletion | null };
|
|
42
|
+
private customProviders: CompletionProvider[] = [];
|
|
43
|
+
private disposables: MonacoType.IDisposable[] = [];
|
|
44
|
+
|
|
45
|
+
completed = false;
|
|
46
|
+
error: Error | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(private options: EditorControllerOptions) {
|
|
49
|
+
super();
|
|
50
|
+
this.monaco = options.monaco;
|
|
51
|
+
this.customProviders = options.completionProviders ?? [];
|
|
52
|
+
this.init();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private init(): void {
|
|
56
|
+
const { language, theme, value, sqlSchema, apiConfig, editorOptions } = this.options;
|
|
57
|
+
|
|
58
|
+
// 注册 SQL 补全(只注册一次)
|
|
59
|
+
if (!this.sqlProviderRegistered) {
|
|
60
|
+
const schemaRef = { current: sqlSchema ?? {} };
|
|
61
|
+
registerSqlCompletion(this.monaco, schemaRef);
|
|
62
|
+
this.sqlProviderRegistered = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 注册 AI 幽灵文本提供者
|
|
66
|
+
if (!this.aiProviderRegistered) {
|
|
67
|
+
registerAiInlineCompletion(this.monaco, this.pendingCompletionRef);
|
|
68
|
+
this.aiProviderRegistered = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 注册自定义补全提供者
|
|
72
|
+
this.registerCustomCompletionProviders();
|
|
73
|
+
|
|
74
|
+
// 创建编辑器
|
|
75
|
+
this.editor = this.monaco.editor.create(this.options.container, {
|
|
76
|
+
value: value ?? '',
|
|
77
|
+
language: language ?? 'typescript',
|
|
78
|
+
theme: theme ?? 'vs-dark',
|
|
79
|
+
fontSize: 14,
|
|
80
|
+
minimap: { enabled: true },
|
|
81
|
+
scrollBeyondLastLine: false,
|
|
82
|
+
automaticLayout: true,
|
|
83
|
+
lineNumbers: 'on',
|
|
84
|
+
renderLineHighlight: 'all',
|
|
85
|
+
smoothScrolling: true,
|
|
86
|
+
cursorBlinking: 'smooth',
|
|
87
|
+
bracketPairColorization: { enabled: true },
|
|
88
|
+
tabSize: 2,
|
|
89
|
+
wordWrap: 'on',
|
|
90
|
+
folding: true,
|
|
91
|
+
formatOnPaste: true,
|
|
92
|
+
suggestOnTriggerCharacters: true,
|
|
93
|
+
quickSuggestions: { other: true, comments: false, strings: false },
|
|
94
|
+
acceptSuggestionOnCommitCharacter: true,
|
|
95
|
+
acceptSuggestionOnEnter: 'on',
|
|
96
|
+
inlineSuggest: { enabled: true },
|
|
97
|
+
tabCompletion: 'on',
|
|
98
|
+
...editorOptions,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 绑定事件
|
|
102
|
+
this.bindEditorEvents();
|
|
103
|
+
this.emit('ready', { editor: this.editor });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private bindEditorEvents(): void {
|
|
107
|
+
const editor = this.editor!;
|
|
108
|
+
|
|
109
|
+
const d1 = editor.onDidChangeModelContent(() => {
|
|
110
|
+
this.emit('change', { value: editor.getValue() });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const d2 = editor.onDidChangeCursorPosition((e) => {
|
|
114
|
+
this.emit('cursor', {
|
|
115
|
+
line: e.position.lineNumber,
|
|
116
|
+
column: e.position.column,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const d3 = editor.onDidFocusEditorText(() => this.emit('focus', undefined));
|
|
121
|
+
const d4 = editor.onDidBlurEditorText(() => this.emit('blur', undefined));
|
|
122
|
+
|
|
123
|
+
this.disposables.push(d1, d2, d3, d4);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private registerCustomCompletionProviders(): void {
|
|
127
|
+
if (this.customProviders.length === 0) return;
|
|
128
|
+
|
|
129
|
+
for (const provider of this.customProviders) {
|
|
130
|
+
const langs = provider.languages ?? ['*'];
|
|
131
|
+
for (const lang of langs) {
|
|
132
|
+
this.monaco.languages.registerCompletionItemProvider(lang, {
|
|
133
|
+
triggerCharacters: provider.triggerCharacters,
|
|
134
|
+
provideCompletionItems: async (model, position, context) => {
|
|
135
|
+
const ctx = {
|
|
136
|
+
monaco: this.monaco,
|
|
137
|
+
model,
|
|
138
|
+
position,
|
|
139
|
+
lineText: model.getLineContent(position.lineNumber),
|
|
140
|
+
currentWord: model.getWordUntilPosition(position).word,
|
|
141
|
+
fullText: model.getValue(),
|
|
142
|
+
triggerCharacter: context.triggerCharacter,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (provider.shouldTrigger && !provider.shouldTrigger(ctx)) {
|
|
146
|
+
return { suggestions: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const suggestions = await provider.provide(ctx);
|
|
150
|
+
return { suggestions };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── 公开 API ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
getEditor(): MonacoType.editor.IStandaloneCodeEditor | null {
|
|
160
|
+
return this.editor;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getValue(): string {
|
|
164
|
+
return this.editor?.getValue() ?? '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setValue(value: string): void {
|
|
168
|
+
this.editor?.setValue(value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
insertText(text: string, range?: MonacoType.IRange): void {
|
|
172
|
+
const editor = this.editor;
|
|
173
|
+
if (!editor) return;
|
|
174
|
+
const targetRange = range ?? editor.getSelection();
|
|
175
|
+
if (targetRange) {
|
|
176
|
+
editor.executeEdits('controller-insert', [{ range: targetRange, text }]);
|
|
177
|
+
editor.focus();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
format(): void {
|
|
182
|
+
this.editor?.getAction('editor.action.formatDocument')?.run();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
focus(): void {
|
|
186
|
+
this.editor?.focus();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setLanguage(language: string): void {
|
|
190
|
+
const model = this.editor?.getModel();
|
|
191
|
+
if (model) {
|
|
192
|
+
this.monaco.editor.setModelLanguage(model, language);
|
|
193
|
+
this.emit('languageChange', { language });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setTheme(theme: string): void {
|
|
198
|
+
this.monaco.editor.setTheme(theme);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getCursorPosition(): { line: number; column: number } {
|
|
202
|
+
const pos = this.editor?.getPosition();
|
|
203
|
+
return pos
|
|
204
|
+
? { line: pos.lineNumber, column: pos.column }
|
|
205
|
+
: { line: 1, column: 1 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getSelection(): MonacoType.IRange | null {
|
|
209
|
+
return this.editor?.getSelection() ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 触发 AI 行内补全(Ctrl+Alt+L 快捷键)。
|
|
214
|
+
* 需要提前设置 apiConfig(通过 setApiConfig 或构造函数传入)。
|
|
215
|
+
*/
|
|
216
|
+
setApiConfig(config: ApiConfig | null): void {
|
|
217
|
+
this.options.apiConfig = config;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getPendingCompletionRef(): { current: PendingCompletion | null } {
|
|
221
|
+
return this.pendingCompletionRef;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
dispose(): void {
|
|
225
|
+
for (const d of this.disposables) d.dispose();
|
|
226
|
+
this.disposables = [];
|
|
227
|
+
this.editor?.dispose();
|
|
228
|
+
this.editor = null;
|
|
229
|
+
this.removeAllListeners();
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ─── 类型 ──────────────────────────────────────────────────────────────
|
|
2
|
+
export type {
|
|
3
|
+
ApiConfig,
|
|
4
|
+
SqlSchema,
|
|
5
|
+
PendingCompletion,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
EditorSnapshot,
|
|
8
|
+
CompletionContext,
|
|
9
|
+
CompletionProvider,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
// ─── 常量 ──────────────────────────────────────────────────────────────
|
|
13
|
+
export {
|
|
14
|
+
SQL_KEYWORDS,
|
|
15
|
+
SQL_FUNCTIONS,
|
|
16
|
+
SQL_DATA_TYPES,
|
|
17
|
+
LANGUAGES,
|
|
18
|
+
THEMES,
|
|
19
|
+
AI_INLINE_LANGUAGES,
|
|
20
|
+
LOCALES,
|
|
21
|
+
type Locale,
|
|
22
|
+
} from './constants';
|
|
23
|
+
|
|
24
|
+
// ─── 控制器 ────────────────────────────────────────────────────────────
|
|
25
|
+
export {
|
|
26
|
+
EditorController,
|
|
27
|
+
type EditorControllerOptions,
|
|
28
|
+
type EditorControllerEvents,
|
|
29
|
+
} from './controllers/EditorController';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
AiChatController,
|
|
33
|
+
type AiChatControllerEvents,
|
|
34
|
+
} from './controllers/AiChatController';
|
|
35
|
+
|
|
36
|
+
// ─── 总线 ──────────────────────────────────────────────────────────────
|
|
37
|
+
export { EditorBus, type EditorBusEvents } from './bus/EditorBus';
|
|
38
|
+
export { EventEmitter, type Listener } from './bus/EventEmitter';
|
|
39
|
+
|
|
40
|
+
// ─── 补全 / AI ────────────────────────────────────────────────────────
|
|
41
|
+
export { registerSqlCompletion } from './completion/sqlCompletion';
|
|
42
|
+
export {
|
|
43
|
+
registerAiInlineCompletion,
|
|
44
|
+
registerAiCompletionCommand,
|
|
45
|
+
fetchAiCompletionStream,
|
|
46
|
+
fetchAiCompletionNonStream,
|
|
47
|
+
} from './ai/aiCompletion';
|