Skip to content

Commit dbee79b

Browse files
committed
feat(script): Add AI code completion with monacopilot integration
1 parent 1d85e2e commit dbee79b

4 files changed

Lines changed: 407 additions & 15 deletions

File tree

packages/plugins/script/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"dependencies": {
2828
"@opentiny/tiny-engine-common": "workspace:*",
2929
"@opentiny/tiny-engine-meta-register": "workspace:*",
30-
"@opentiny/tiny-engine-utils": "workspace:*"
30+
"@opentiny/tiny-engine-utils": "workspace:*",
31+
"monacopilot": "^1.2.12"
3132
},
3233
"devDependencies": {
3334
"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",

packages/plugins/script/src/Main.vue

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@
3434
/* metaService: engine.plugins.pagecontroller.Main */
3535
import { onBeforeUnmount, reactive, provide } from 'vue'
3636
import { Button } from '@opentiny/vue'
37-
import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common'
38-
import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register'
37+
import { registerCompletion } from 'monacopilot'
38+
import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component'
39+
import { useHelp, useLayout, useResource, useCanvas } from '@opentiny/tiny-engine-meta-register'
3940
import { initCompletion } from '@opentiny/tiny-engine-common/js/completion'
4041
import { initLinter } from '@opentiny/tiny-engine-common/js/linter'
4142
import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method'
43+
import { requestManager } from './js/requestManager'
44+
import { shouldTriggerCompletion } from './js/completionTrigger'
4245
4346
export const api = {
4447
saveMethod,
@@ -59,7 +62,7 @@ export default {
5962
}
6063
},
6164
emits: ['close'],
62-
setup(props, { emit }) {
65+
setup(_props, { emit }) {
6366
const docsUrl = useHelp().getDocsUrl('script')
6467
const docsContent = '同一页面/区块的添加事件会统一保存到对应的页面JS中。'
6568
const { state, monaco, change, close, saveMethods } = useMethod({ emit })
@@ -101,24 +104,112 @@ export default {
101104
wordWrapStrategy: 'advanced'
102105
}
103106
104-
const editorDidMount = (editor) => {
105-
if (!monaco.value) {
106-
return
107-
}
107+
const editorDidMount = (editor: any) => {
108+
const monacoRef = monaco as any
109+
if (!monacoRef.value) return
110+
111+
// 保留原有的 Lowcode API 提示
112+
state.completionProvider = initCompletion(
113+
monacoRef.value.getMonaco(),
114+
monacoRef.value.getEditor()?.getModel()
115+
) as any
116+
117+
// 保留原有的 ESLint
118+
state.linterWorker = initLinter(editor, monacoRef.value.getMonaco(), state) as any
119+
120+
// 🆕 新增: 注册 AI 补全
121+
try {
122+
const monacoInstance = monacoRef.value.getMonaco()
123+
const editorInstance = monacoRef.value.getEditor()
124+
125+
// 构建低代码上下文
126+
const getLowcodeContext = () => {
127+
const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState || {}
128+
const { state: pageState = {}, methods = {} } = useCanvas().getPageSchema() || {}
129+
const currentSchema = useCanvas().getCurrentSchema()
130+
131+
return {
132+
dataSource,
133+
utils,
134+
globalState,
135+
state: pageState,
136+
methods,
137+
currentSchema
138+
}
139+
}
140+
141+
// 配置请求管理器
142+
requestManager.setEndpoint('http://localhost:3000/code-completion')
143+
requestManager.setDebounceDelay(300) // 设置防抖延迟为 300ms
144+
requestManager.setDebounceEnabled(true)
108145
109-
// Lowcode API 提示
110-
state.completionProvider = initCompletion(monaco.value.getMonaco(), monaco.value.getEditor()?.getModel())
146+
// 创建增强的请求处理器
147+
const baseRequestHandler = requestManager.createRequestHandler()
111148
112-
// 初始化 ESLint worker
113-
state.linterWorker = initLinter(editor, monaco.value.getMonaco(), state)
149+
registerCompletion(monacoInstance, editorInstance, {
150+
language: 'javascript',
151+
endpoint: 'http://localhost:3000/code-completion',
152+
filename: 'page.js',
153+
trigger: 'onTyping',
154+
maxContextLines: 50,
155+
enableCaching: true,
156+
allowFollowUpCompletions: true,
157+
158+
// 🎯 智能触发判断(在请求前执行,避免不必要的请求)
159+
triggerIf: (params) => {
160+
const model = editorInstance.getModel()
161+
const position = editorInstance.getPosition()
162+
163+
if (!model || !position) return false
164+
165+
return shouldTriggerCompletion({
166+
text: model.getValue(),
167+
position: {
168+
lineNumber: position.lineNumber,
169+
column: position.column
170+
},
171+
triggerType: params.triggerType || 'onTyping'
172+
})
173+
},
174+
175+
// 🚀 请求处理器:防抖 + 请求取消 + 低代码元数据
176+
requestHandler: async (params) => {
177+
try {
178+
// 添加低代码元数据
179+
const lowcodeMetadata = getLowcodeContext()
180+
const enhancedParams = {
181+
body: {
182+
completionMetadata: {
183+
...params.body.completionMetadata,
184+
lowcodeMetadata
185+
}
186+
}
187+
}
188+
189+
// 使用请求管理器发送请求(带防抖和取消功能)
190+
return await baseRequestHandler(enhancedParams)
191+
} catch (error: any) {
192+
return {
193+
completion: null,
194+
error: error.message
195+
}
196+
}
197+
}
198+
})
199+
} catch (error) {
200+
// eslint-disable-next-line no-console
201+
console.error('❌ AI 补全注册失败:', error)
202+
}
114203
}
115204
116205
onBeforeUnmount(() => {
117-
state.completionProvider?.forEach((provider) => {
118-
provider.dispose()
206+
;(state.completionProvider as any)?.forEach?.((provider: any) => {
207+
provider?.dispose?.()
119208
})
120209
// 终止 ESLint worker
121-
state.linterWorker?.terminate?.()
210+
;(state.linterWorker as any)?.terminate?.()
211+
// 清理请求管理器
212+
requestManager.reset()
122213
})
123214
124215
return {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* 智能补全触发条件判断(JS/TS 专用)
3+
*/
4+
5+
/**
6+
* 检测是否在注释中
7+
*/
8+
function isInComment(beforeCursor, fullText) {
9+
const trimmed = beforeCursor.trim()
10+
11+
// 单行注释
12+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) {
13+
return true
14+
}
15+
16+
// 块注释
17+
const lastBlockStart = fullText.lastIndexOf('/*', fullText.indexOf(beforeCursor))
18+
const lastBlockEnd = fullText.lastIndexOf('*/', fullText.indexOf(beforeCursor))
19+
if (lastBlockStart > lastBlockEnd) {
20+
return true
21+
}
22+
23+
return false
24+
}
25+
26+
/**
27+
* 检测是否在字符串中
28+
*/
29+
function isInString(beforeCursor) {
30+
const singleQuotes = (beforeCursor.match(/'/g) || []).length
31+
const doubleQuotes = (beforeCursor.match(/"/g) || []).length
32+
return singleQuotes % 2 === 1 || doubleQuotes % 2 === 1
33+
}
34+
35+
/**
36+
* 检测是否在模板字符串中
37+
*/
38+
function isInTemplateString(beforeCursor) {
39+
const backticks = (beforeCursor.match(/`/g) || []).length
40+
return backticks % 2 === 1
41+
}
42+
43+
/**
44+
* 检测光标是否在语句结束符后(分号后)
45+
*/
46+
function isAfterStatementEnd(beforeCursor) {
47+
// 检查是否以分号结尾(忽略尾部空格)
48+
const trimmedEnd = beforeCursor.trimEnd()
49+
50+
if (trimmedEnd.endsWith(';')) {
51+
// 排除 for 循环中的分号:for (let i = 0; i < 10; i++)
52+
// 检查是否在括号内
53+
const openParens = (beforeCursor.match(/\(/g) || []).length
54+
const closeParens = (beforeCursor.match(/\)/g) || []).length
55+
56+
// 如果括号未闭合,说明可能在 for 循环中
57+
if (openParens > closeParens) {
58+
return false
59+
}
60+
61+
return true
62+
}
63+
64+
return false
65+
}
66+
67+
/**
68+
* 检测光标是否在代码块结束符后(右花括号后)
69+
*/
70+
function isAfterBlockEnd(beforeCursor) {
71+
const trimmedEnd = beforeCursor.trimEnd()
72+
73+
// 检查是否以右花括号结尾
74+
if (trimmedEnd.endsWith('}')) {
75+
// 检查后面是否只有空格(没有其他字符)
76+
const afterBrace = beforeCursor.substring(trimmedEnd.length)
77+
return afterBrace.trim().length === 0
78+
}
79+
80+
return false
81+
}
82+
83+
/**
84+
* 判断是否应该触发代码补全
85+
* @param {Object} params - 触发参数
86+
* @param {string} params.text - 完整文本
87+
* @param {Object} params.position - 光标位置
88+
* @param {number} params.position.lineNumber - 行号
89+
* @param {number} params.position.column - 列号
90+
* @param {string} params.triggerType - 触发类型
91+
* @returns {boolean} 是否触发补全
92+
*/
93+
export function shouldTriggerCompletion(params) {
94+
const { text, position } = params
95+
const lines = text.split('\n')
96+
const currentLine = lines[position.lineNumber - 1] || ''
97+
const beforeCursor = currentLine.substring(0, position.column - 1)
98+
const trimmedLine = beforeCursor.trim()
99+
100+
// 1. 避免在注释中触发
101+
if (isInComment(beforeCursor, text)) {
102+
return false
103+
}
104+
105+
// 2. 避免在普通字符串中触发(但允许模板字符串)
106+
if (isInString(beforeCursor) && !isInTemplateString(beforeCursor)) {
107+
return false
108+
}
109+
110+
// 3. 代码太短不触发(降低阈值)
111+
if (text.trim().length < 5) {
112+
return false
113+
}
114+
115+
// 4. 完全空行不触发
116+
if (trimmedLine.length === 0) {
117+
return false
118+
}
119+
120+
// 5. 分号后不触发(语句已结束)
121+
if (isAfterStatementEnd(beforeCursor)) {
122+
return false
123+
}
124+
125+
// 6. 右花括号后不触发(块已结束)
126+
if (isAfterBlockEnd(beforeCursor)) {
127+
return false
128+
}
129+
130+
// 其他情况都允许触发
131+
return true
132+
}

0 commit comments

Comments
 (0)