From dbee79b3610ff67801186e25930ef85c6ed44dfd Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 13 Jan 2026 05:24:35 -0800 Subject: [PATCH 01/10] feat(script): Add AI code completion with monacopilot integration --- packages/plugins/script/package.json | 3 +- packages/plugins/script/src/Main.vue | 119 +++++++++++-- .../script/src/js/completionTrigger.js | 132 ++++++++++++++ .../plugins/script/src/js/requestManager.js | 168 ++++++++++++++++++ 4 files changed, 407 insertions(+), 15 deletions(-) create mode 100644 packages/plugins/script/src/js/completionTrigger.js create mode 100644 packages/plugins/script/src/js/requestManager.js diff --git a/packages/plugins/script/package.json b/packages/plugins/script/package.json index bca9fbaf82..207278bd3a 100644 --- a/packages/plugins/script/package.json +++ b/packages/plugins/script/package.json @@ -27,7 +27,8 @@ "dependencies": { "@opentiny/tiny-engine-common": "workspace:*", "@opentiny/tiny-engine-meta-register": "workspace:*", - "@opentiny/tiny-engine-utils": "workspace:*" + "@opentiny/tiny-engine-utils": "workspace:*", + "monacopilot": "^1.2.12" }, "devDependencies": { "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*", diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index de663b7112..cd4768e6b4 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -34,11 +34,14 @@ /* metaService: engine.plugins.pagecontroller.Main */ import { onBeforeUnmount, reactive, provide } from 'vue' import { Button } from '@opentiny/vue' -import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common' -import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register' +import { registerCompletion } from 'monacopilot' +import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component' +import { useHelp, useLayout, useResource, useCanvas } from '@opentiny/tiny-engine-meta-register' import { initCompletion } from '@opentiny/tiny-engine-common/js/completion' import { initLinter } from '@opentiny/tiny-engine-common/js/linter' import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method' +import { requestManager } from './js/requestManager' +import { shouldTriggerCompletion } from './js/completionTrigger' export const api = { saveMethod, @@ -59,7 +62,7 @@ export default { } }, emits: ['close'], - setup(props, { emit }) { + setup(_props, { emit }) { const docsUrl = useHelp().getDocsUrl('script') const docsContent = '同一页面/区块的添加事件会统一保存到对应的页面JS中。' const { state, monaco, change, close, saveMethods } = useMethod({ emit }) @@ -101,24 +104,112 @@ export default { wordWrapStrategy: 'advanced' } - const editorDidMount = (editor) => { - if (!monaco.value) { - return - } + const editorDidMount = (editor: any) => { + const monacoRef = monaco as any + if (!monacoRef.value) return + + // 保留原有的 Lowcode API 提示 + state.completionProvider = initCompletion( + monacoRef.value.getMonaco(), + monacoRef.value.getEditor()?.getModel() + ) as any + + // 保留原有的 ESLint + state.linterWorker = initLinter(editor, monacoRef.value.getMonaco(), state) as any + + // 🆕 新增: 注册 AI 补全 + try { + const monacoInstance = monacoRef.value.getMonaco() + const editorInstance = monacoRef.value.getEditor() + + // 构建低代码上下文 + const getLowcodeContext = () => { + const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState || {} + const { state: pageState = {}, methods = {} } = useCanvas().getPageSchema() || {} + const currentSchema = useCanvas().getCurrentSchema() + + return { + dataSource, + utils, + globalState, + state: pageState, + methods, + currentSchema + } + } + + // 配置请求管理器 + requestManager.setEndpoint('http://localhost:3000/code-completion') + requestManager.setDebounceDelay(300) // 设置防抖延迟为 300ms + requestManager.setDebounceEnabled(true) - // Lowcode API 提示 - state.completionProvider = initCompletion(monaco.value.getMonaco(), monaco.value.getEditor()?.getModel()) + // 创建增强的请求处理器 + const baseRequestHandler = requestManager.createRequestHandler() - // 初始化 ESLint worker - state.linterWorker = initLinter(editor, monaco.value.getMonaco(), state) + registerCompletion(monacoInstance, editorInstance, { + language: 'javascript', + endpoint: 'http://localhost:3000/code-completion', + filename: 'page.js', + trigger: 'onTyping', + maxContextLines: 50, + enableCaching: true, + allowFollowUpCompletions: true, + + // 🎯 智能触发判断(在请求前执行,避免不必要的请求) + triggerIf: (params) => { + const model = editorInstance.getModel() + const position = editorInstance.getPosition() + + if (!model || !position) return false + + return shouldTriggerCompletion({ + text: model.getValue(), + position: { + lineNumber: position.lineNumber, + column: position.column + }, + triggerType: params.triggerType || 'onTyping' + }) + }, + + // 🚀 请求处理器:防抖 + 请求取消 + 低代码元数据 + requestHandler: async (params) => { + try { + // 添加低代码元数据 + const lowcodeMetadata = getLowcodeContext() + const enhancedParams = { + body: { + completionMetadata: { + ...params.body.completionMetadata, + lowcodeMetadata + } + } + } + + // 使用请求管理器发送请求(带防抖和取消功能) + return await baseRequestHandler(enhancedParams) + } catch (error: any) { + return { + completion: null, + error: error.message + } + } + } + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('❌ AI 补全注册失败:', error) + } } onBeforeUnmount(() => { - state.completionProvider?.forEach((provider) => { - provider.dispose() + ;(state.completionProvider as any)?.forEach?.((provider: any) => { + provider?.dispose?.() }) // 终止 ESLint worker - state.linterWorker?.terminate?.() + ;(state.linterWorker as any)?.terminate?.() + // 清理请求管理器 + requestManager.reset() }) return { diff --git a/packages/plugins/script/src/js/completionTrigger.js b/packages/plugins/script/src/js/completionTrigger.js new file mode 100644 index 0000000000..8864b0cd79 --- /dev/null +++ b/packages/plugins/script/src/js/completionTrigger.js @@ -0,0 +1,132 @@ +/** + * 智能补全触发条件判断(JS/TS 专用) + */ + +/** + * 检测是否在注释中 + */ +function isInComment(beforeCursor, fullText) { + const trimmed = beforeCursor.trim() + + // 单行注释 + if (trimmed.startsWith('//') || trimmed.startsWith('*')) { + return true + } + + // 块注释 + const lastBlockStart = fullText.lastIndexOf('/*', fullText.indexOf(beforeCursor)) + const lastBlockEnd = fullText.lastIndexOf('*/', fullText.indexOf(beforeCursor)) + if (lastBlockStart > lastBlockEnd) { + return true + } + + return false +} + +/** + * 检测是否在字符串中 + */ +function isInString(beforeCursor) { + const singleQuotes = (beforeCursor.match(/'/g) || []).length + const doubleQuotes = (beforeCursor.match(/"/g) || []).length + return singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 +} + +/** + * 检测是否在模板字符串中 + */ +function isInTemplateString(beforeCursor) { + const backticks = (beforeCursor.match(/`/g) || []).length + return backticks % 2 === 1 +} + +/** + * 检测光标是否在语句结束符后(分号后) + */ +function isAfterStatementEnd(beforeCursor) { + // 检查是否以分号结尾(忽略尾部空格) + const trimmedEnd = beforeCursor.trimEnd() + + if (trimmedEnd.endsWith(';')) { + // 排除 for 循环中的分号:for (let i = 0; i < 10; i++) + // 检查是否在括号内 + const openParens = (beforeCursor.match(/\(/g) || []).length + const closeParens = (beforeCursor.match(/\)/g) || []).length + + // 如果括号未闭合,说明可能在 for 循环中 + if (openParens > closeParens) { + return false + } + + return true + } + + return false +} + +/** + * 检测光标是否在代码块结束符后(右花括号后) + */ +function isAfterBlockEnd(beforeCursor) { + const trimmedEnd = beforeCursor.trimEnd() + + // 检查是否以右花括号结尾 + if (trimmedEnd.endsWith('}')) { + // 检查后面是否只有空格(没有其他字符) + const afterBrace = beforeCursor.substring(trimmedEnd.length) + return afterBrace.trim().length === 0 + } + + return false +} + +/** + * 判断是否应该触发代码补全 + * @param {Object} params - 触发参数 + * @param {string} params.text - 完整文本 + * @param {Object} params.position - 光标位置 + * @param {number} params.position.lineNumber - 行号 + * @param {number} params.position.column - 列号 + * @param {string} params.triggerType - 触发类型 + * @returns {boolean} 是否触发补全 + */ +export function shouldTriggerCompletion(params) { + const { text, position } = params + const lines = text.split('\n') + const currentLine = lines[position.lineNumber - 1] || '' + const beforeCursor = currentLine.substring(0, position.column - 1) + const trimmedLine = beforeCursor.trim() + + // 1. 避免在注释中触发 + if (isInComment(beforeCursor, text)) { + return false + } + + // 2. 避免在普通字符串中触发(但允许模板字符串) + if (isInString(beforeCursor) && !isInTemplateString(beforeCursor)) { + return false + } + + // 3. 代码太短不触发(降低阈值) + if (text.trim().length < 5) { + return false + } + + // 4. 完全空行不触发 + if (trimmedLine.length === 0) { + return false + } + + // 5. 分号后不触发(语句已结束) + if (isAfterStatementEnd(beforeCursor)) { + return false + } + + // 6. 右花括号后不触发(块已结束) + if (isAfterBlockEnd(beforeCursor)) { + return false + } + + // 其他情况都允许触发 + return true +} diff --git a/packages/plugins/script/src/js/requestManager.js b/packages/plugins/script/src/js/requestManager.js new file mode 100644 index 0000000000..ebc5b2a4c5 --- /dev/null +++ b/packages/plugins/script/src/js/requestManager.js @@ -0,0 +1,168 @@ +/** + * 请求管理器 - 支持防抖和请求取消 + */ +class RequestManager { + constructor() { + this.abortController = null + this.endpoint = '' + this.debounceTimer = null + this.debounceDelay = 200 // 防抖延迟(毫秒) + this.lastTriggerTime = 0 + this.isDebounceEnabled = true + } + + /** + * 设置 API 端点 + */ + setEndpoint(endpoint) { + this.endpoint = endpoint + } + + /** + * 设置防抖延迟 + */ + setDebounceDelay(delay) { + this.debounceDelay = delay + } + + /** + * 启用/禁用防抖 + */ + setDebounceEnabled(enabled) { + this.isDebounceEnabled = enabled + } + + /** + * 创建新的请求信号 + * 如果有正在进行的请求,会先取消它 + */ + createSignal() { + // 取消之前的请求 + if (this.abortController) { + this.abortController.abort() + } + + // 创建新的 AbortController + this.abortController = new AbortController() + return this.abortController.signal + } + + /** + * 清理当前的 AbortController + */ + clear() { + this.abortController = null + } + + /** + * 清理防抖定时器 + */ + clearDebounceTimer() { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer) + this.debounceTimer = null + } + } + + /** + * 检查是否应该立即执行(不防抖) + * 某些情况下应该立即响应,不需要防抖 + */ + shouldExecuteImmediately() { + const now = Date.now() + const timeSinceLastTrigger = now - this.lastTriggerTime + + // 如果距离上次触发超过 1 秒,立即执行 + // 这避免了用户停止输入后再次输入时的延迟 + return timeSinceLastTrigger > 1000 + } + + /** + * 创建带防抖的请求处理器 + * 支持请求取消和智能防抖 + */ + createRequestHandler() { + return async (params) => { + this.lastTriggerTime = Date.now() + + // 如果启用了防抖且不应该立即执行 + if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { + // 清理之前的防抖定时器 + this.clearDebounceTimer() + + // 创建新的防抖 Promise + await new Promise((resolve) => { + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null + resolve() + }, this.debounceDelay) + }) + } + + // 执行实际的请求 + return this.executeRequest(params) + } + } + + /** + * 执行实际的 HTTP 请求 + */ + async executeRequest(params) { + const signal = this.createSignal() + + try { + const response = await fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params.body), + signal // 添加取消信号 + }) + + if (!response.ok) { + const errorMsg = `HTTP ${response.status}: ${response.statusText}` + this.clear() + return { + completion: null, + error: errorMsg + } + } + + const data = await response.json() + this.clear() // 请求成功,清理 controller + + return { + completion: data.completion || null, + error: data.error + } + } catch (error) { + // 如果是取消错误 + if (error.name === 'AbortError') { + return { + completion: null, + error: 'Request cancelled' + } + } + + this.clear() + return { + completion: null, + error: error.message + } + } + } + + /** + * 重置状态(用于清理) + */ + reset() { + this.clearDebounceTimer() + if (this.abortController) { + this.abortController.abort() + this.clear() + } + } +} + +export const requestManager = new RequestManager() From aadcfe73094da125fe8fc8d94b1dc40f4ff2012c Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 02:19:02 -0800 Subject: [PATCH 02/10] feat(script): Refactor AI completion system with multi-model adapter support --- .../robot/src/constants/model-config.ts | 29 +- packages/plugins/script/meta.js | 2 +- packages/plugins/script/src/Main.vue | 59 +--- .../ai-completion/adapters/deepseekAdapter.js | 64 ++++ .../src/ai-completion/adapters/index.js | 108 +++++++ .../src/ai-completion/adapters/qwenAdapter.js | 75 +++++ .../builders/fimPromptBuilder.js | 172 +++++++++++ .../src/ai-completion/builders/index.js | 3 + .../builders/lowcodeContextBuilder.js | 275 ++++++++++++++++++ .../ai-completion/builders/promptBuilder.js | 203 +++++++++++++ .../script/src/ai-completion/constants.js | 178 ++++++++++++ .../plugins/script/src/ai-completion/index.js | 7 + .../src/ai-completion/prompts/templates.js | 197 +++++++++++++ .../triggers}/completionTrigger.js | 0 .../ai-completion/utils/completionUtils.js | 93 ++++++ .../src/ai-completion/utils/modelUtils.js | 81 ++++++ .../src/ai-completion/utils/requestManager.js | 229 +++++++++++++++ .../plugins/script/src/js/requestManager.js | 168 ----------- 18 files changed, 1711 insertions(+), 232 deletions(-) create mode 100644 packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js create mode 100644 packages/plugins/script/src/ai-completion/adapters/index.js create mode 100644 packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js create mode 100644 packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js create mode 100644 packages/plugins/script/src/ai-completion/builders/index.js create mode 100644 packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js create mode 100644 packages/plugins/script/src/ai-completion/builders/promptBuilder.js create mode 100644 packages/plugins/script/src/ai-completion/constants.js create mode 100644 packages/plugins/script/src/ai-completion/index.js create mode 100644 packages/plugins/script/src/ai-completion/prompts/templates.js rename packages/plugins/script/src/{js => ai-completion/triggers}/completionTrigger.js (100%) create mode 100644 packages/plugins/script/src/ai-completion/utils/completionUtils.js create mode 100644 packages/plugins/script/src/ai-completion/utils/modelUtils.js create mode 100644 packages/plugins/script/src/ai-completion/utils/requestManager.js delete mode 100644 packages/plugins/script/src/js/requestManager.js diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts index 4458339325..1a34c745d9 100644 --- a/packages/plugins/robot/src/constants/model-config.ts +++ b/packages/plugins/robot/src/constants/model-config.ts @@ -81,8 +81,8 @@ export const DEFAULT_LLM_MODELS = [ } }, { - label: 'Qwen Coder编程模型(Flash)', - name: 'qwen3-coder-flash', + label: 'Qwen2.5 Coder编程模型-最快响应', + name: 'qwen-coder-turbo-latest', capabilities: { toolCalling: true, compact: true, @@ -90,14 +90,13 @@ export const DEFAULT_LLM_MODELS = [ } }, { - label: 'Qwen3(14b)', - name: 'qwen3-14b', - capabilities: { compact: true, toolCalling: true, jsonOutput: bailianJsonOutputExtraBody } - }, - { - label: 'Qwen3(8b)', - name: 'qwen3-8b', - capabilities: { compact: true, toolCalling: true, jsonOutput: bailianJsonOutputExtraBody } + label: 'Qwen2.5 Coder编程模型(32B)', + name: 'qwen2.5-coder-32b-instruct', + capabilities: { + toolCalling: true, + compact: true, + jsonOutput: bailianJsonOutputExtraBody + } } ] }, @@ -120,6 +119,16 @@ export const DEFAULT_LLM_MODELS = [ }, jsonOutput: jsonOutputExtraBody } + }, + { + // TODO: https://api.deepseek.com/beta 支持 FIM + label: 'Deepseek Coder编程模型', + name: 'deepseek-chat', + capabilities: { + toolCalling: true, + compact: true, + jsonOutput: bailianJsonOutputExtraBody + } } ] } diff --git a/packages/plugins/script/meta.js b/packages/plugins/script/meta.js index 6991805f54..b7f9e6134a 100644 --- a/packages/plugins/script/meta.js +++ b/packages/plugins/script/meta.js @@ -6,7 +6,7 @@ export default { width: 600, widthResizable: true, options: { - enableAICompletion: true + enableAICompletion: false // 禁用旧的 AI 补全系统,使用新的 monacopilot }, confirm: 'close' // 当点击插件栏切换或关闭前是否需要确认, 会调用插件中confirm值指定的方法,e.g. 此处指向 close方法,会调用插件的close方法执行确认逻辑 } diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index cd4768e6b4..d9629ffc0e 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -36,12 +36,12 @@ import { onBeforeUnmount, reactive, provide } from 'vue' import { Button } from '@opentiny/vue' import { registerCompletion } from 'monacopilot' import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component' -import { useHelp, useLayout, useResource, useCanvas } from '@opentiny/tiny-engine-meta-register' +import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register' import { initCompletion } from '@opentiny/tiny-engine-common/js/completion' import { initLinter } from '@opentiny/tiny-engine-common/js/linter' import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method' -import { requestManager } from './js/requestManager' -import { shouldTriggerCompletion } from './js/completionTrigger' +import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger' +import { createCompletionHandler } from './ai-completion/adapters/index' export const api = { saveMethod, @@ -122,33 +122,9 @@ export default { const monacoInstance = monacoRef.value.getMonaco() const editorInstance = monacoRef.value.getEditor() - // 构建低代码上下文 - const getLowcodeContext = () => { - const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState || {} - const { state: pageState = {}, methods = {} } = useCanvas().getPageSchema() || {} - const currentSchema = useCanvas().getCurrentSchema() - - return { - dataSource, - utils, - globalState, - state: pageState, - methods, - currentSchema - } - } - - // 配置请求管理器 - requestManager.setEndpoint('http://localhost:3000/code-completion') - requestManager.setDebounceDelay(300) // 设置防抖延迟为 300ms - requestManager.setDebounceEnabled(true) - - // 创建增强的请求处理器 - const baseRequestHandler = requestManager.createRequestHandler() - registerCompletion(monacoInstance, editorInstance, { language: 'javascript', - endpoint: 'http://localhost:3000/code-completion', + endpoint: '/app-center/api/chat/completions', filename: 'page.js', trigger: 'onTyping', maxContextLines: 50, @@ -172,29 +148,8 @@ export default { }) }, - // 🚀 请求处理器:防抖 + 请求取消 + 低代码元数据 - requestHandler: async (params) => { - try { - // 添加低代码元数据 - const lowcodeMetadata = getLowcodeContext() - const enhancedParams = { - body: { - completionMetadata: { - ...params.body.completionMetadata, - lowcodeMetadata - } - } - } - - // 使用请求管理器发送请求(带防抖和取消功能) - return await baseRequestHandler(enhancedParams) - } catch (error: any) { - return { - completion: null, - error: error.message - } - } - } + // 🚀 请求处理器:支持 DeepSeek 和 Qwen 模型 + requestHandler: createCompletionHandler() as any }) } catch (error) { // eslint-disable-next-line no-console @@ -208,8 +163,6 @@ export default { }) // 终止 ESLint worker ;(state.linterWorker as any)?.terminate?.() - // 清理请求管理器 - requestManager.reset() }) return { diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js new file mode 100644 index 0000000000..657c1e3974 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -0,0 +1,64 @@ +/** + * DeepSeek 专用适配器 + * 使用 Chat Completions API(通过后端代理) + */ +import { SYSTEM_BASE_PROMPT, createUserPrompt } from '../prompts/templates.js' +import { API_ENDPOINTS, HTTP_CONFIG } from '../constants.js' + +/** + * 构建 DeepSeek Chat 格式的 messages + * @param {string} context - 上下文信息 + * @param {string} instruction - 指令 + * @param {string} fileContent - 文件内容 + * @returns {{ messages: Array, cursorContext: null }} Messages 和上下文 + */ +export function buildDeepSeekMessages(context, instruction, fileContent) { + // eslint-disable-next-line no-console + console.log('🎯 使用 DeepSeek Chat 格式') + + const systemPrompt = `${context}\n\n${SYSTEM_BASE_PROMPT}` + const userPrompt = createUserPrompt(instruction, fileContent) + + return { + messages: [ + { + role: 'system', + content: systemPrompt + }, + { + role: 'user', + content: userPrompt + } + ], + cursorContext: null + } +} + +/** + * 调用 DeepSeek Chat API(通过后端代理) + * @param {Array} messages - Messages 数组 + * @param {Object} config - 配置对象 + * @param {string} apiKey - API 密钥 + * @param {string} baseUrl - 基础 URL + * @param {Object} httpClient - HTTP 客户端 + * @returns {Promise} 补全文本 + */ +export async function callDeepSeekAPI(messages, config, apiKey, baseUrl, httpClient) { + const response = await httpClient.post( + API_ENDPOINTS.CHAT_COMPLETIONS, + { + model: config.model, + messages, + baseUrl, + stream: HTTP_CONFIG.STREAM + }, + { + headers: { + 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, + Authorization: `Bearer ${apiKey || ''}` + } + } + ) + + return response?.choices?.[0]?.message?.content +} diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js new file mode 100644 index 0000000000..1c226e3acf --- /dev/null +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -0,0 +1,108 @@ +/** + * AI 补全适配器主入口 + */ +import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' +import { createSmartPrompt } from '../builders/promptBuilder.js' +import { FIMPromptBuilder } from '../builders/fimPromptBuilder.js' +import { detectModelType, calculateTokens, getStopSequences } from '../utils/modelUtils.js' +import { cleanCompletion, buildLowcodeMetadata } from '../utils/completionUtils.js' +import { buildQwenMessages, callQwenAPI } from './qwenAdapter.js' +import { buildDeepSeekMessages, callDeepSeekAPI } from './deepseekAdapter.js' +import { QWEN_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js' + +/** + * 创建请求处理器 + * @returns {Function} 请求处理函数 + */ +export function createCompletionHandler() { + const fimBuilder = new FIMPromptBuilder(QWEN_CONFIG) + + return async (params) => { + try { + // 1. 获取 AI 配置 + const { completeModel, apiKey, baseUrl } = getMetaApi(META_SERVICE.Robot).getSelectedQuickModelInfo() || {} + + if (!completeModel || !apiKey || !baseUrl) { + return { + completion: null, + error: ERROR_MESSAGES.CONFIG_MISSING + } + } + + // 2. 提取代码上下文 + const { + textBeforeCursor = '', + textAfterCursor = '', + language = DEFAULTS.LANGUAGE, + filename + } = params.body?.completionMetadata || {} + + // 3. 构建低代码元数据和 prompt + const lowcodeMetadata = buildLowcodeMetadata() + const { context, instruction, fileContent } = createSmartPrompt({ + textBeforeCursor, + textAfterCursor, + language, + filename, + technologies: DEFAULTS.TECHNOLOGIES, + lowcodeMetadata + }) + + // 4. 检测模型类型 + const modelType = detectModelType(completeModel) + + let completionText = null + let cursorContext = null + + // 5. 根据模型类型调用不同的 API + if (modelType === MODEL_CONFIG.QWEN.TYPE) { + // ===== Qwen 流程 ===== + const { messages, cursorContext: ctx } = buildQwenMessages(fileContent, fimBuilder) + cursorContext = ctx + + const config = { + model: completeModel, + maxTokens: calculateTokens(cursorContext), + stopSequences: getStopSequences(cursorContext, MODEL_CONFIG.QWEN.TYPE) + } + + completionText = await callQwenAPI(messages, config, apiKey, baseUrl) + } else { + // ===== DeepSeek 流程(默认) ===== + const { messages } = buildDeepSeekMessages(context, instruction, fileContent) + + const config = { model: completeModel } + const httpClient = getMetaApi(META_SERVICE.Http) + + completionText = await callDeepSeekAPI(messages, config, apiKey, baseUrl, httpClient) + } + + // 6. 处理补全结果 + if (completionText) { + completionText = completionText.trim() + + // eslint-disable-next-line no-console + console.log('✅ 收到补全:', completionText.substring(0, DEFAULTS.LOG_PREVIEW_LENGTH)) + + completionText = cleanCompletion(completionText, modelType, cursorContext) + + return { + completion: completionText, + error: null + } + } + + return { + completion: null, + error: ERROR_MESSAGES.NO_COMPLETION + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('❌ AI 补全请求失败:', error) + return { + completion: null, + error: error.message || ERROR_MESSAGES.REQUEST_FAILED + } + } + } +} diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js new file mode 100644 index 0000000000..ac9fb1829c --- /dev/null +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -0,0 +1,75 @@ +/** + * Qwen 专用适配器 + * 使用 Completions API + FIM (Fill-In-the-Middle) + */ +import { QWEN_CONFIG, API_ENDPOINTS, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' + +/** + * 构建 Qwen FIM 格式的 messages + * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记) + * @param {Object} fimBuilder - FIM 构建器实例 + * @returns {{ messages: Array, cursorContext: Object }} Messages 和上下文 + */ +export function buildQwenMessages(fileContent, fimBuilder) { + const { fimPrompt, cursorContext } = fimBuilder.buildOptimizedFIMPrompt(fileContent) + + // eslint-disable-next-line no-console + console.log('🎯 使用 Qwen FIM 格式') + // eslint-disable-next-line no-console + console.log('📊 FIM 上下文:', cursorContext.type) + // eslint-disable-next-line no-console + console.log('📏 FIM Prompt 长度:', fimPrompt.length) + + return { + messages: [ + { + role: 'user', + content: fimPrompt + } + ], + cursorContext + } +} + +/** + * 调用 Qwen Completions API + * @param {Array} messages - Messages 数组 + * @param {Object} config - 配置对象 + * @param {string} apiKey - API 密钥 + * @param {string} baseUrl - 基础 URL + * @returns {Promise} 补全文本 + */ +export async function callQwenAPI(messages, config, apiKey, baseUrl) { + const completionsUrl = `${baseUrl}${API_ENDPOINTS.COMPLETIONS_PATH}` + + // eslint-disable-next-line no-console + console.log('📦 模型:', config.model) + + const requestBody = { + model: config.model, + prompt: messages[0].content, // FIM prompt + max_tokens: config.maxTokens, + temperature: QWEN_CONFIG.DEFAULT_TEMPERATURE, + top_p: QWEN_CONFIG.TOP_P, + stream: HTTP_CONFIG.STREAM, + stop: config.stopSequences, + presence_penalty: QWEN_CONFIG.PRESENCE_PENALTY + } + + const fetchResponse = await fetch(completionsUrl, { + method: HTTP_CONFIG.METHOD, + headers: { + 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify(requestBody) + }) + + if (!fetchResponse.ok) { + const errorText = await fetchResponse.text() + throw new Error(`${ERROR_MESSAGES.QWEN_API_ERROR} ${fetchResponse.status}: ${errorText}`) + } + + const response = await fetchResponse.json() + return response?.choices?.[0]?.text +} diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js new file mode 100644 index 0000000000..00f9cb0ca1 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js @@ -0,0 +1,172 @@ +import { FIM_CONFIG } from '../constants.js' + +/** + * FIM (Fill-In-the-Middle) Prompt 构建器 + * 用于处理 FIM 格式的代码补全 + */ +export class FIMPromptBuilder { + constructor(config) { + this.config = config + } + + /** + * 构建优化的 FIM (Fill In the Middle) Prompt + * @param {string} fileContent - 文件内容,包含 [CURSOR] 标记 + * @returns {{ fimPrompt: string, cursorContext: Object }} FIM prompt 和上下文信息 + */ + buildOptimizedFIMPrompt(fileContent) { + // 1. 清理元信息注释 + let cleanedContent = this.cleanMetaInfo(fileContent) + + // 2. 查找光标位置 + const cursorIndex = cleanedContent.indexOf(FIM_CONFIG.MARKERS.CURSOR) + + if (cursorIndex === -1) { + return { + fimPrompt: `${FIM_CONFIG.MARKERS.PREFIX}${cleanedContent}${FIM_CONFIG.MARKERS.SUFFIX}`, + cursorContext: { type: 'unknown', hasPrefix: true, hasSuffix: false } + } + } + + // 3. 分割前缀和后缀 + const prefix = cleanedContent.substring(0, cursorIndex) + const suffix = cleanedContent.substring(cursorIndex + FIM_CONFIG.MARKERS.CURSOR.length) + + // 4. 分析光标上下文 + const cursorContext = this.analyzeCursorContext(prefix, suffix) + + // 5. 优化前缀和后缀 + const optimizedPrefix = this.optimizePrefix(prefix) + const optimizedSuffix = this.optimizeSuffix(suffix) + + // 6. 构建 FIM prompt + let fimPrompt + if (optimizedSuffix.trim().length > 0) { + // 有后缀:使用 prefix + suffix + middle 模式 + fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${optimizedPrefix}${FIM_CONFIG.MARKERS.SUFFIX}${optimizedSuffix}${FIM_CONFIG.MARKERS.MIDDLE}` + } else { + // 无后缀:只使用 prefix + suffix 模式 + fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${optimizedPrefix}${FIM_CONFIG.MARKERS.SUFFIX}` + } + + return { fimPrompt, cursorContext } + } + + /** + * 清理元信息注释 + * @param {string} content - 原始内容 + * @returns {string} 清理后的内容 + */ + cleanMetaInfo(content) { + return content.replace(FIM_CONFIG.META_INFO_PATTERN, '') + } + + /** + * 分析光标上下文 + * @param {string} prefix - 前缀代码 + * @param {string} suffix - 后缀代码 + * @returns {Object} 上下文信息 + */ + analyzeCursorContext(prefix, suffix) { + const context = { + type: 'unknown', + hasPrefix: prefix.trim().length > 0, + hasSuffix: suffix.trim().length > 0, + inFunction: false, + inClass: false, + inObject: false, + inArray: false, + needsExpression: false, + needsStatement: false + } + + // 分析前缀最后几个字符 + const prefixTrimmed = prefix.trimEnd() + + // 检测是否在表达式中 + if (/[=+\-*/%<>!&|,([]$/.test(prefixTrimmed)) { + context.needsExpression = true + context.type = 'expression' + } + // 检测是否在语句开始 + else if (/[{;]\s*$/.test(prefixTrimmed) || prefixTrimmed.length === 0) { + context.needsStatement = true + context.type = 'statement' + } + // 检测是否在对象字面量中 + else if (/{\s*$/.test(prefixTrimmed) || /,\s*$/.test(prefixTrimmed)) { + context.inObject = true + context.type = 'object-property' + } + + // 检测作用域 + const functionMatch = prefix.match(/function\s+\w+|const\s+\w+\s*=.*=>|async\s+function/g) + const classMatch = prefix.match(/class\s+\w+/g) + + context.inFunction = functionMatch && functionMatch.length > 0 + context.inClass = classMatch && classMatch.length > 0 + + return context + } + + /** + * 优化前缀(限制上下文长度) + * @param {string} prefix - 原始前缀 + * @returns {string} 优化后的前缀 + */ + optimizePrefix(prefix) { + const MAX_PREFIX_LINES = this.config.FIM.MAX_PREFIX_LINES + const lines = prefix.split('\n') + + if (lines.length <= MAX_PREFIX_LINES) { + return prefix + } + + // 保留最后 N 行 + return lines.slice(-MAX_PREFIX_LINES).join('\n') + } + + /** + * 优化后缀(限制上下文长度 + 智能截断) + * @param {string} suffix - 原始后缀 + * @returns {string} 优化后的后缀 + */ + optimizeSuffix(suffix) { + const MAX_SUFFIX_LINES = this.config.FIM.MAX_SUFFIX_LINES + const lines = suffix.split('\n') + + // 智能截断:找到下一个函数/类定义的位置 + let cutoffIndex = lines.length + for (let i = 0; i < Math.min(lines.length, MAX_SUFFIX_LINES); i++) { + const line = lines[i].trim() + + // 遇到新的函数/类定义,在此处截断 + if ( + line.startsWith('function ') || + line.startsWith('class ') || + (line.startsWith('const ') && line.includes('=>')) || + line.startsWith('export ') || + line.startsWith('import ') + ) { + cutoffIndex = i + break + } + + // 遇到闭合的大括号(可能是当前函数/对象的结束) + if (line === '}' || line === '};') { + cutoffIndex = i + 1 // 包含这个闭合括号 + break + } + } + + // 取较小值:要么是智能截断位置,要么是最大行数 + const finalLines = Math.min(cutoffIndex, MAX_SUFFIX_LINES) + + if (lines.length <= finalLines) { + return suffix + } + + // 保留前 N 行 + return lines.slice(0, finalLines).join('\n') + } +} diff --git a/packages/plugins/script/src/ai-completion/builders/index.js b/packages/plugins/script/src/ai-completion/builders/index.js new file mode 100644 index 0000000000..bba666e7c3 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/builders/index.js @@ -0,0 +1,3 @@ +export { createSmartPrompt } from './promptBuilder.js' +export { FIMPromptBuilder } from './fimPromptBuilder.js' +export { buildLowcodeContext } from './lowcodeContextBuilder.js' diff --git a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js new file mode 100644 index 0000000000..a2bb4aaac5 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -0,0 +1,275 @@ +/** + * 低代码上下文构建器 + * 用于从低代码平台的元数据中提取和构建代码补全所需的上下文信息 + */ + +/** + * 格式化数据源信息 + * @param {Array} dataSource - 数据源数组 + * @returns {Array} 格式化后的数据源 + */ +function formatDataSources(dataSource) { + return dataSource.map((ds) => ({ + name: ds.name, + type: ds.type || 'unknown', + description: ds.description || `Data source: ${ds.name}`, + // 只保留关键信息,避免上下文过大 + ...(ds.options && { options: ds.options }) + })) +} + +/** + * 从函数代码中提取函数签名 + * @param {string} functionCode - 函数代码字符串 + * @returns {string} 函数签名 + */ +function extractFunctionSignature(functionCode) { + if (!functionCode) return 'function()' + + // 匹配函数声明: function name(params) + const funcMatch = functionCode.match(/function\s+(\w+)?\s*\(([^)]*)\)/) + if (funcMatch) { + const name = funcMatch[1] || 'anonymous' + const params = funcMatch[2].trim() + return `function ${name}(${params})` + } + + // 匹配箭头函数: (params) => 或 params => + const arrowMatch = functionCode.match(/(?:\(([^)]*)\)|(\w+))\s*=>/) + if (arrowMatch) { + const params = arrowMatch[1] || arrowMatch[2] || '' + return `(${params}) => {}` + } + + return 'function()' +} + +/** + * 格式化工具类信息 + * @param {Array} utils - 工具类数组 + * @returns {Array} 格式化后的工具类 + */ +function formatUtils(utils) { + return utils.map((util) => { + const formatted = { + name: util.name, + type: util.type || 'function' + } + + // 处理 npm 类型的工具 + if (util.type === 'npm' && util.content) { + formatted.package = util.content.package + formatted.exportName = util.content.exportName + formatted.destructuring = util.content.destructuring + formatted.description = `Import from ${util.content.package}` + } + + // 处理函数类型的工具 + if (util.type === 'function' && util.content) { + if (util.content.type === 'JSFunction') { + // 提取函数签名而不是完整实现 + const funcSignature = extractFunctionSignature(util.content.value) + formatted.signature = funcSignature + formatted.description = `Utility function: ${util.name}` + } + } + + return formatted + }) +} + +/** + * 格式化全局状态信息 + * @param {Array} globalState - 全局状态数组 + * @returns {Array} 格式化后的全局状态 + */ +function formatGlobalState(globalState) { + return globalState.map((store) => ({ + id: store.id, + state: Object.keys(store.state || {}), + getters: Object.keys(store.getters || {}), + actions: Object.keys(store.actions || {}), + description: `Pinia store: ${store.id}` + })) +} + +/** + * 格式化本地状态 + * @param {Object} state - 状态对象 + * @returns {Object} 格式化后的状态 + */ +function formatState(state) { + // 只返回键名和类型信息,不返回实际值 + const formatted = {} + for (const [key, value] of Object.entries(state)) { + formatted[key] = { + type: typeof value, + isArray: Array.isArray(value), + isObject: value !== null && typeof value === 'object' && !Array.isArray(value) + } + } + return formatted +} + +/** + * 格式化本地方法 + * @param {Object} methods - 方法对象 + * @returns {Object} 格式化后的方法 + */ +function formatMethods(methods) { + const formatted = {} + for (const [key, value] of Object.entries(methods)) { + if (value && value.type === 'JSFunction') { + formatted[key] = { + signature: extractFunctionSignature(value.value), + description: `Method: ${key}` + } + } else { + formatted[key] = { + type: typeof value, + description: `Method: ${key}` + } + } + } + return formatted +} + +/** + * 格式化当前组件 schema + * @param {Object} schema - 组件 schema + * @returns {Object|null} 格式化后的 schema + */ +function formatCurrentSchema(schema) { + if (!schema) return null + + const formatted = { + componentName: schema.componentName, + ...(schema.ref && { ref: schema.ref }) + } + + // 格式化 props + if (schema.props) { + formatted.props = {} + for (const [key, value] of Object.entries(schema.props)) { + // 识别事件处理器 + if (key.startsWith('on')) { + formatted.props[key] = { + type: 'event', + isFunction: value && value.type === 'JSFunction' + } + } else { + formatted.props[key] = { + type: value && value.type ? value.type : 'static', + isDynamic: value && (value.type === 'JSExpression' || value.type === 'JSFunction') + } + } + } + } + + return formatted +} + +/** + * 验证低代码上下文的完整性 + * @param {Object} context - 低代码上下文 + * @returns {{ valid: boolean, warnings: string[] }} 验证结果 + */ +export function validateLowcodeContext(context) { + const warnings = [] + + if (!context) { + return { valid: false, warnings: ['Context is null or undefined'] } + } + + // 检查必要字段 + const requiredFields = ['dataSource', 'utils', 'globalState', 'state', 'methods'] + for (const field of requiredFields) { + if (!(field in context)) { + warnings.push(`Missing field: ${field}`) + } + } + + // 检查数据源格式 + if (context.dataSource && !Array.isArray(context.dataSource)) { + warnings.push('dataSource should be an array') + } + + // 检查工具类格式 + if (context.utils && !Array.isArray(context.utils)) { + warnings.push('utils should be an array') + } + + // 检查全局状态格式 + if (context.globalState && !Array.isArray(context.globalState)) { + warnings.push('globalState should be an array') + } + + return { + valid: warnings.length === 0, + warnings + } +} + +/** + * 合并多个低代码上下文 + * @param {...Object} contexts - 多个上下文对象 + * @returns {Object} 合并后的上下文 + */ +export function mergeLowcodeContexts(...contexts) { + const merged = { + dataSource: [], + utils: [], + globalState: [], + state: {}, + methods: {}, + currentSchema: null + } + + for (const context of contexts) { + if (!context) continue + + // 合并数组类型 + if (context.dataSource) { + merged.dataSource = [...merged.dataSource, ...context.dataSource] + } + if (context.utils) { + merged.utils = [...merged.utils, ...context.utils] + } + if (context.globalState) { + merged.globalState = [...merged.globalState, ...context.globalState] + } + + // 合并对象类型 + if (context.state) { + merged.state = { ...merged.state, ...context.state } + } + if (context.methods) { + merged.methods = { ...merged.methods, ...context.methods } + } + + // currentSchema 使用最后一个非空值 + if (context.currentSchema) { + merged.currentSchema = context.currentSchema + } + } + + return merged +} + +/** + * 从低代码平台元数据构建补全上下文 + * @param {Object} metadata - 低代码平台元数据 + * @returns {Object} 格式化的低代码上下文 + */ +export function buildLowcodeContext(metadata) { + const { dataSource = [], utils = [], globalState = [], state = {}, methods = {}, currentSchema = null } = metadata + + return { + dataSource: formatDataSources(dataSource), + utils: formatUtils(utils), + globalState: formatGlobalState(globalState), + state: formatState(state), + methods: formatMethods(methods), + currentSchema: formatCurrentSchema(currentSchema) + } +} diff --git a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js new file mode 100644 index 0000000000..9d7e91b718 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js @@ -0,0 +1,203 @@ +import { CODE_PATTERNS, CONTEXT_CONFIG } from '../constants.js' +import { + createCodeInstruction, + createLowcodeInstruction, + BLOCK_COMMENT_INSTRUCTION, + LINE_COMMENT_INSTRUCTION +} from '../prompts/templates.js' +import { buildLowcodeContext, validateLowcodeContext } from './lowcodeContextBuilder.js' + +/** + * 检测光标是否在注释中 + * @param {string} textBeforeCursor - 光标前的文本 + * @returns {{ isComment: boolean, type: string | null }} 注释状态 + */ +function isInComment(textBeforeCursor) { + const trimmed = textBeforeCursor.trim() + + // 单行注释 // + if (trimmed.includes('//')) { + const lastLineBreak = textBeforeCursor.lastIndexOf('\n') + const currentLine = textBeforeCursor.substring(lastLineBreak + 1) + if (currentLine.trim().startsWith('//')) { + return { isComment: true, type: 'line' } + } + } + + // 块注释 /* */ 或 JSDoc /** */ + const lastBlockStart = textBeforeCursor.lastIndexOf('/*') + const lastBlockEnd = textBeforeCursor.lastIndexOf('*/') + if (lastBlockStart > lastBlockEnd) { + return { isComment: true, type: 'block' } + } + + return { isComment: false, type: null } +} + +/** + * 提取当前代码上下文信息(函数名、类名、接口名等) + * @param {string} textBeforeCursor - 光标前的文本 + * @returns {{ functionName: string, className: string, interfaceName: string, typeName: string }} 代码上下文 + */ +function extractCodeContext(textBeforeCursor) { + const lines = textBeforeCursor.split('\n') + let functionName = '' + let className = '' + let interfaceName = '' + let typeName = '' + + // 从后往前查找最近的定义 + const startLine = Math.max(0, lines.length - CONTEXT_CONFIG.MAX_LINES_TO_SCAN) + + for (let i = lines.length - 1; i >= startLine; i--) { + const line = lines[i] + + if (!functionName) { + const funcMatch = line.match(CODE_PATTERNS.FUNCTION) + if (funcMatch) functionName = funcMatch[1] || funcMatch[2] || funcMatch[3] + } + + if (!className) { + const classMatch = line.match(CODE_PATTERNS.CLASS) + if (classMatch) className = classMatch[1] + } + + if (!interfaceName) { + const interfaceMatch = line.match(CODE_PATTERNS.INTERFACE) + if (interfaceMatch) interfaceName = interfaceMatch[1] + } + + if (!typeName) { + const typeMatch = line.match(CODE_PATTERNS.TYPE) + if (typeMatch) typeName = typeMatch[1] + } + + // 找到所有信息后提前退出 + if (functionName && className && interfaceName && typeName) break + } + + return { functionName, className, interfaceName, typeName } +} + +/** + * 构建元信息注释 + * @param {string} filename - 文件名 + * @param {string} language - 语言类型 + * @param {Object} codeContext - 代码上下文 + * @param {string[]} technologies - 技术栈 + * @returns {string} 元信息字符串 + */ +function buildMetaInfo(filename, language, codeContext, technologies) { + let metaInfo = '' + + if (filename) { + metaInfo += `// File: ${filename}\n` + } + + metaInfo += `// Language: ${language}\n` + + // 强调当前作用域 + if (codeContext.className) { + metaInfo += `// Current Class: ${codeContext.className}\n` + metaInfo += `// IMPORTANT: Only complete code within this class\n` + } + + if (codeContext.interfaceName) { + metaInfo += `// Current Interface: ${codeContext.interfaceName}\n` + } + + if (codeContext.typeName) { + metaInfo += `// Current Type: ${codeContext.typeName}\n` + } + + if (codeContext.functionName) { + metaInfo += `// Current Function: ${codeContext.functionName}\n` + metaInfo += `// IMPORTANT: Only complete code within this function scope\n` + } + + if (technologies.length > 0) { + metaInfo += `// Technologies: ${technologies.join(', ')}\n` + } + + metaInfo += `// NOTE: Do not reference variables or code from other functions\n` + metaInfo += '\n' + + return metaInfo +} + +/** + * 构建基础上下文 + * @param {string} language - 语言类型 + * @param {string} filename - 文件名 + * @returns {string} 上下文字符串 + */ +function buildContext(language, filename) { + let context = `You are an expert ${language} developer with deep knowledge of modern best practices.` + + if (filename) { + context += ` Currently editing: ${filename}` + } + + return context +} + +/** + * 构建注释补全指令 + * @param {string} commentType - 注释类型 ('line' | 'block') + * @returns {string} 指令文本 + */ +function buildCommentInstruction(commentType) { + return commentType === 'block' ? BLOCK_COMMENT_INSTRUCTION : LINE_COMMENT_INSTRUCTION +} + +/** + * 创建智能 Prompt,根据上下文优化补全 + * @param {Object} completionMetadata - 补全元数据 + * @returns {{ context: string, instruction: string, fileContent: string }} Prompt 对象 + */ +export function createSmartPrompt(completionMetadata) { + const { + textBeforeCursor = '', + textAfterCursor = '', + language = 'javascript', + filename, + technologies = [], + lowcodeMetadata = null + } = completionMetadata + + const commentStatus = isInComment(textBeforeCursor) + const codeContext = extractCodeContext(textBeforeCursor) + + // 构建文件元信息(伪装成注释,让 AI 理解上下文) + const metaInfo = buildMetaInfo(filename, language, codeContext, technologies) + + // 基础上下文 + const context = buildContext(language, filename) + + // 根据是否在注释中使用不同的 instruction + let instruction + if (commentStatus.isComment) { + instruction = buildCommentInstruction(commentStatus.type) + } else if (lowcodeMetadata) { + // 如果提供了低代码元数据,使用增强的指令 + const lowcodeContext = buildLowcodeContext(lowcodeMetadata) + const validation = validateLowcodeContext(lowcodeContext) + + if (!validation.valid) { + // console.warn('⚠️ Lowcode context validation warnings:', validation.warnings); + } + + instruction = createLowcodeInstruction(language, lowcodeContext) + } else { + instruction = createCodeInstruction(language) + } + + // 在文件内容前注入元信息 + const fileContent = `${metaInfo}${textBeforeCursor}[CURSOR]${textAfterCursor}` + + return { + context, + instruction, + fileContent + } +} diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js new file mode 100644 index 0000000000..f159dce636 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -0,0 +1,178 @@ +/** + * Qwen Coder API 配置(阿里云百炼) + */ +export const QWEN_CONFIG = { + API_URL: 'https://dashscope.aliyuncs.com/compatible-mode/v1/completions', + MODEL: 'qwen2.5-coder-32b-instruct', + DEFAULT_TEMPERATURE: 0.05, + TOP_P: 0.95, + PRESENCE_PENALTY: 0.2, + + // FIM (Fill-In-the-Middle) 优化配置 + FIM: { + MAX_PREFIX_LINES: 100, + MAX_SUFFIX_LINES: 50 + } +} + +/** + * 模型配置 + */ +export const MODEL_CONFIG = { + QWEN: { + TYPE: 'qwen', + KEYWORDS: ['qwen'] // 移除 'coder',避免误匹配 deepseek-coder + }, + DEEPSEEK: { + TYPE: 'deepseek', + KEYWORDS: ['deepseek'] + }, + UNKNOWN: { + TYPE: 'unknown', + KEYWORDS: [] + } +} + +/** + * API 端点配置 + */ +export const API_ENDPOINTS = { + COMPLETIONS_PATH: '/completions', + CHAT_COMPLETIONS: '/app-center/api/chat/completions' +} + +/** + * HTTP 请求配置 + */ +export const HTTP_CONFIG = { + METHOD: 'POST', + CONTENT_TYPE: 'application/json', + STREAM: false +} + +/** + * 默认配置 + */ +export const DEFAULTS = { + LANGUAGE: 'javascript', + LOG_PREVIEW_LENGTH: 100, + TECHNOLOGIES: [] +} + +/** + * 错误消息配置 + */ +export const ERROR_MESSAGES = { + CONFIG_MISSING: 'AI 配置未设置(缺少 model/apiKey/baseUrl)', + NO_COMPLETION: '未收到有效的补全结果', + REQUEST_FAILED: '请求失败', + QWEN_API_ERROR: 'Qwen API 错误' +} + +/** + * 通用模型配置 + */ +export const MODEL_COMMON_CONFIG = { + // Token 限制 + TOKEN_LIMITS: { + EXPRESSION: 64, + STATEMENT: 256, + FUNCTION: 200, + CLASS: 256, + DEFAULT: 128 + }, + + // 清理规则 + CLEANUP_PATTERNS: { + MARKDOWN_CODE_BLOCK: /^```[\w]*\n?|```$/g, + TRAILING_SEMICOLON: /;\s*$/, + LEADING_EMPTY_LINES: /^\n+/, + TRAILING_EMPTY_LINES: /\n+$/ + }, + + // 智能截断配置 + TRUNCATION: { + MAX_LINES: { + EXPRESSION: 1, + OBJECT: 5, + DEFAULT: 10 + }, + CUTOFF_KEYWORDS: ['function ', 'class ', 'export ', 'import '], + BLOCK_ENDINGS: ['}', '};'] + } +} + +/** + * 通用停止符配置(JS/TS) + */ +export const STOP_SEQUENCES = [ + // 通用停止符 + '\n\n', + '```', + + // JS/TS 语言特性 + '\nfunction ', + '\nclass ', + '\nconst ', + '\nlet ', + '\nvar ', + '\nexport ', + '\nimport ', + '\ninterface ', + '\ntype ', + '\nenum ', + + // 注释边界 + '\n//', + '\n/*', + + // 代码块边界 + '\n}', + '\n};' +] + +/** + * FIM (Fill-In-the-Middle) 配置 + */ +export const FIM_CONFIG = { + MARKERS: { + PREFIX: '<|fim_prefix|>', + SUFFIX: '<|fim_suffix|>', + MIDDLE: '<|fim_middle|>', + CURSOR: '[CURSOR]' + }, + + // FIM 专用停止符(会与 STOP_SEQUENCES 合并) + FIM_MARKERS_STOPS: ['<|fim_prefix|>', '<|fim_suffix|>', '<|fim_middle|>'], + + // 上下文特定的额外停止符 + CONTEXT_STOPS: { + EXPRESSION: [';', '\n)', ','], + STATEMENT: [], // 使用通用停止符即可 + OBJECT: [] // 使用通用停止符即可 + }, + + META_INFO_PATTERN: + /^(\/\/ File:.*\n)?(\/\/ Language:.*\n)?(\/\/ Current .*\n)*(\/\/ IMPORTANT:.*\n)*(\/\/ Technologies:.*\n)?(\/\/ NOTE:.*\n)*\n*/ +} + +/** + * 代码上下文分析配置 + */ +export const CONTEXT_CONFIG = { + MAX_LINES_TO_SCAN: 20 +} + +/** + * 代码模式匹配(JS/TS) + */ +export const CODE_PATTERNS = { + // 匹配函数定义:function name() / const name = () => / name() { + FUNCTION: /function\s+(\w+)|const\s+(\w+)\s*=.*=>|(\w+)\s*\([^)]*\)\s*{/, + // 匹配类定义 + CLASS: /class\s+(\w+)/, + // 匹配接口定义(TS) + INTERFACE: /interface\s+(\w+)/, + // 匹配类型定义(TS) + TYPE: /type\s+(\w+)/ +} diff --git a/packages/plugins/script/src/ai-completion/index.js b/packages/plugins/script/src/ai-completion/index.js new file mode 100644 index 0000000000..f34545c829 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/index.js @@ -0,0 +1,7 @@ +/** + * AI 补全模块统一导出 + */ +export { createCompletionHandler } from './adapters/index.js' +export { shouldTriggerCompletion } from './triggers/completionTrigger.js' +export { requestManager } from './utils/requestManager.js' +export { createSmartPrompt, FIMPromptBuilder } from './builders/index.js' diff --git a/packages/plugins/script/src/ai-completion/prompts/templates.js b/packages/plugins/script/src/ai-completion/prompts/templates.js new file mode 100644 index 0000000000..c4f4d286cb --- /dev/null +++ b/packages/plugins/script/src/ai-completion/prompts/templates.js @@ -0,0 +1,197 @@ +/** + * AI Prompt 模板集合 + * + * 这个文件包含所有用于代码补全的提示词模板。 + * 提示词的调整不会影响业务逻辑,可以独立进行 A/B 测试。 + */ + +/** + * 系统 Prompt - 定义 AI 的角色和基本规则 + */ +export const SYSTEM_BASE_PROMPT = `You are an AI code completion assistant specialized in JavaScript and TypeScript. + +CRITICAL RULES: +1. Return ONLY the code/text that should be inserted at the cursor position +2. DO NOT repeat any code that already exists before the cursor +3. DO NOT include markdown code blocks or language tags +4. DO NOT add explanations or comments unless explicitly requested +5. Match the exact indentation and style of the existing code +6. Keep completions focused and minimal - only what's needed +7. Pay attention to the file metadata (filename, language, current function/class/interface) for better context +8. For TypeScript, ensure type safety and proper type annotations +9. ONLY complete code within the CURRENT function/scope where [CURSOR] is located +10. DO NOT generate code for other functions, classes, or unrelated scopes +11. If you see multiple functions in the context, focus ONLY on the one containing [CURSOR] +12. Respect variable scope - do not reference variables from other functions` + +/** + * 代码补全指令模板 + * @param {string} language - 编程语言 + * @returns {string} 指令文本 + */ +export function createCodeInstruction(language) { + return `Complete the code after the cursor position. + +Rules: +1. Follow ${language} best practices and modern ES6+ syntax +2. Match the existing code style exactly (indentation, quotes, semicolons) +3. Generate only the necessary code to complete the current statement or block +4. Ensure proper indentation and formatting +5. DO NOT include explanatory comments unless they were already in the pattern +6. If completing a function, include the full implementation +7. For TypeScript, include proper type annotations +8. Return ONLY the completion code, no additional text +9. CRITICAL: Only complete code within the current function/scope +10. DO NOT generate variables or code from other functions in the file` +} + +/** + * 块注释补全指令(JSDoc) + */ +export const BLOCK_COMMENT_INSTRUCTION = `You are writing a JSDoc documentation comment. Complete the comment with clear, concise explanation. + +Focus on: +- Describing what the code does +- Explaining parameters with @param tags +- Documenting return values with @returns tag +- Adding usage examples with @example if appropriate +- Including type information for TypeScript + +DO NOT generate code. Only complete the comment text.` + +/** + * 行注释补全指令 + */ +export const LINE_COMMENT_INSTRUCTION = `You are writing an inline comment. Complete the comment with a brief, clear explanation. + +Focus on: +- Explaining WHY this code exists, not WHAT it does +- Keep it concise and on a single line +- Use clear, professional language + +DO NOT generate code. Only complete the comment text.` + +/** + * 低代码平台上下文增强 Prompt + * 用于在低代码环境中提供特定的 API 和数据结构提示 + */ +export const LOWCODE_CONTEXT_INSTRUCTION = `You are working in a low-code platform environment with specific APIs and data structures. + +AVAILABLE RUNTIME APIS (all accessed via 'this.'): +1. Data Sources (this.dataSource.xxx) + - Predefined data models for the application + - Access pattern: this.dataSource. + +2. Utility Functions (this.utils.xxx) + - Common utility methods and npm dependencies + - Access pattern: this.utils. + - May include imported libraries (check utils metadata for imports) + +3. Global State (this.stores.xxx) + - Pinia-based global state management + - Access pattern: this.stores.. + - Actions: this.stores..() + +4. Local State (this.state.xxx) + - Component-level reactive state + - Access pattern: this.state. + +5. Local Methods (this.xxx) + - Component-level methods + - Access pattern: this.() + +6. Component References (this.$('refName')) + - Access Vue component refs + - Access pattern: this.$('') + +IMPORTANT RULES: +- ONLY use APIs that are explicitly defined in the provided metadata +- DO NOT reference undefined utilities, data sources, or state properties +- Follow the JSExpression/JSFunction protocol for dynamic values +- Use 'function' keyword for function definitions, NOT arrow functions +- Respect the component schema structure (props, events, refs) + +PROTOCOL CONVENTIONS: +- Static values: { width: '300px' } +- Dynamic expressions: { width: { type: 'JSExpression', value: 'this.state.xxx' } } +- Function handlers: { onClick: { type: 'JSFunction', value: 'function onClick() {}' } }` + +/** + * 创建带低代码上下文的指令 + * @param {string} language - 编程语言 + * @param {Object} lowcodeContext - 低代码上下文数据 + * @returns {string} 增强的指令文本 + */ +export function createLowcodeInstruction(language, lowcodeContext = {}) { + const { + dataSource = [], + utils = [], + globalState = [], + state = {}, + methods = {}, + currentSchema = null + } = lowcodeContext + + let instruction = createCodeInstruction(language) + + // 如果提供了低代码上下文,添加特定信息 + if (Object.keys(lowcodeContext).length > 0) { + instruction += `\n\n${LOWCODE_CONTEXT_INSTRUCTION}` + + // 添加可用的数据源 + if (dataSource.length > 0) { + instruction += `\n\nAVAILABLE DATA SOURCES:\n${JSON.stringify(dataSource, null, 2)}` + } + + // 添加可用的工具类 + if (utils.length > 0) { + instruction += `\n\nAVAILABLE UTILITIES:\n${JSON.stringify(utils, null, 2)}` + } + + // 添加全局状态 + if (globalState.length > 0) { + instruction += `\n\nGLOBAL STATE (Pinia Stores):\n${JSON.stringify(globalState, null, 2)}` + } + + // 添加本地状态 + if (Object.keys(state).length > 0) { + instruction += `\n\nLOCAL STATE:\n${JSON.stringify(state, null, 2)}` + } + + // 添加本地方法 + if (Object.keys(methods).length > 0) { + instruction += `\n\nLOCAL METHODS:\n${JSON.stringify(methods, null, 2)}` + } + + // 添加当前组件 schema + if (currentSchema) { + instruction += `\n\nCURRENT COMPONENT SCHEMA:\n${JSON.stringify(currentSchema, null, 2)}` + instruction += `\n\nCOMPONENT CONTEXT:` + instruction += `\n- Component: ${currentSchema.componentName || 'Unknown'}` + if (currentSchema.props) { + instruction += `\n- Props: Use component props as defined in schema` + instruction += `\n- Events: Props starting with 'on' are event handlers` + } + if (currentSchema.ref) { + instruction += `\n- Ref: Access via this.$('${currentSchema.ref}')` + } + } + } + + return instruction +} + +/** + * 用户 Prompt 模板 + * @param {string} instruction - 指令文本 + * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记) + * @returns {string} 完整的用户 Prompt + */ +export function createUserPrompt(instruction, fileContent) { + return `${instruction} + +File content (cursor position marked with [CURSOR]): +${fileContent} + +Complete the code/text at the [CURSOR] position. Return ONLY the completion text.` +} diff --git a/packages/plugins/script/src/js/completionTrigger.js b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js similarity index 100% rename from packages/plugins/script/src/js/completionTrigger.js rename to packages/plugins/script/src/ai-completion/triggers/completionTrigger.js diff --git a/packages/plugins/script/src/ai-completion/utils/completionUtils.js b/packages/plugins/script/src/ai-completion/utils/completionUtils.js new file mode 100644 index 0000000000..e2476398ec --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js @@ -0,0 +1,93 @@ +/** + * 补全处理工具函数 + */ +import { useResource, useCanvas } from '@opentiny/tiny-engine-meta-register' +import { MODEL_COMMON_CONFIG, FIM_CONFIG } from '../constants.js' + +/** + * 构建低代码元数据 + * @returns {Object} 低代码元数据 + */ +export function buildLowcodeMetadata() { + const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState || {} + const { state: pageState = {}, methods = {} } = useCanvas().getPageSchema() || {} + const currentSchema = useCanvas().getCurrentSchema() + + return { + dataSource, + utils, + globalState, + state: pageState, + methods, + currentSchema + } +} + +/** + * 清理补全文本 + * @param {string} text - 原始补全文本 + * @param {string} modelType - 模型类型 + * @param {Object} cursorContext - 光标上下文信息(可选) + * @returns {string} 清理后的文本 + */ +export function cleanCompletion(text, modelType, cursorContext = null) { + if (!text) return text + + let cleaned = text + + // 1. 移除 markdown 代码块 + cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.MARKDOWN_CODE_BLOCK, '') + + // 2. 移除前后空行 + cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.LEADING_EMPTY_LINES, '') + cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.TRAILING_EMPTY_LINES, '') + + // 3. Qwen 特殊处理:移除 FIM 标记 + if (modelType === 'qwen') { + Object.values(FIM_CONFIG.MARKERS).forEach((marker) => { + if (marker !== FIM_CONFIG.MARKERS.CURSOR) { + cleaned = cleaned.replace(new RegExp(marker.replace(/[|<>]/g, '\\$&'), 'g'), '') + } + }) + } + + // 4. 表达式特殊处理:移除尾部分号 + if (cursorContext?.needsExpression) { + cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.TRAILING_SEMICOLON, '') + } + + // 5. 智能截断:防止返回过多不相关代码 + const lines = cleaned.split('\n') + + // 根据上下文确定最大行数 + const truncation = MODEL_COMMON_CONFIG.TRUNCATION + const maxLines = cursorContext?.needsExpression + ? truncation.MAX_LINES.EXPRESSION + : cursorContext?.inObject + ? truncation.MAX_LINES.OBJECT + : truncation.MAX_LINES.DEFAULT + + if (lines.length > maxLines) { + // 找到合适的截断点 + let cutoffIndex = maxLines + for (let i = 0; i < maxLines && i < lines.length; i++) { + const line = lines[i].trim() + + // 在函数/类定义处截断 + if (truncation.CUTOFF_KEYWORDS.some((keyword) => line.startsWith(keyword))) { + cutoffIndex = i + break + } + + // 在闭合大括号处截断(完整的代码块) + if (truncation.BLOCK_ENDINGS.includes(line)) { + cutoffIndex = i + 1 + break + } + } + + cleaned = lines.slice(0, cutoffIndex).join('\n') + } + + return cleaned +} diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js new file mode 100644 index 0000000000..8f611251b4 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -0,0 +1,81 @@ +/** + * 模型相关工具函数 + */ +import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, FIM_CONFIG } from '../constants.js' + +/** + * 检测模型类型 + * @param {string} modelName - 模型名称 + * @returns {'qwen' | 'deepseek' | 'unknown'} 模型类型 + */ +export function detectModelType(modelName) { + if (!modelName) return MODEL_CONFIG.UNKNOWN.TYPE + + const lowerName = modelName.toLowerCase() + + if (MODEL_CONFIG.QWEN.KEYWORDS.some((keyword) => lowerName.includes(keyword))) { + return MODEL_CONFIG.QWEN.TYPE + } + + if (MODEL_CONFIG.DEEPSEEK.KEYWORDS.some((keyword) => lowerName.includes(keyword))) { + return MODEL_CONFIG.DEEPSEEK.TYPE + } + + return MODEL_CONFIG.UNKNOWN.TYPE +} + +/** + * 计算动态 Token 数量 + * @param {Object} cursorContext - 光标上下文 + * @returns {number} Token 数量 + */ +export function calculateTokens(cursorContext) { + const limits = MODEL_COMMON_CONFIG.TOKEN_LIMITS + + if (!cursorContext) { + return limits.DEFAULT + } + + if (cursorContext.needsStatement) { + return limits.STATEMENT + } else if (cursorContext.needsExpression) { + return limits.EXPRESSION + } else if (cursorContext.inFunction) { + return limits.FUNCTION + } else if (cursorContext.inClass) { + return limits.CLASS + } + + return limits.DEFAULT +} + +/** + * 获取动态停止符 + * @param {Object} cursorContext - 光标上下文 + * @param {string} modelType - 模型类型 + * @returns {string[]} 停止符数组 + */ +export function getStopSequences(cursorContext, modelType) { + // 基础停止符:通用停止符 + const stops = [...STOP_SEQUENCES] + + // Qwen 模型添加 FIM 标记 + if (modelType === 'qwen') { + stops.push(...FIM_CONFIG.FIM_MARKERS_STOPS) + } + + if (!cursorContext) { + return stops + } + + // 根据上下文添加特定停止符 + if (cursorContext.needsExpression) { + stops.push(...FIM_CONFIG.CONTEXT_STOPS.EXPRESSION) + } else if (cursorContext.needsStatement) { + stops.push(...FIM_CONFIG.CONTEXT_STOPS.STATEMENT) + } else if (cursorContext.inObject) { + stops.push(...FIM_CONFIG.CONTEXT_STOPS.OBJECT) + } + + return stops +} diff --git a/packages/plugins/script/src/ai-completion/utils/requestManager.js b/packages/plugins/script/src/ai-completion/utils/requestManager.js new file mode 100644 index 0000000000..240205d024 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/requestManager.js @@ -0,0 +1,229 @@ +/** + * 请求管理器 - 支持防抖、请求取消和重试 + */ +class RequestManager { + constructor() { + this.abortController = null + this.endpoint = '' + this.debounceTimer = null + this.debounceDelay = 200 // 防抖延迟(毫秒) + this.lastTriggerTime = 0 + this.isDebounceEnabled = true + this.retryConfig = { + maxRetries: 2, + retryDelay: 1000 + } + } + + /** + * 设置 API 端点 + */ + setEndpoint(endpoint) { + this.endpoint = endpoint + } + + /** + * 设置防抖延迟 + */ + setDebounceDelay(delay) { + this.debounceDelay = delay + } + + /** + * 启用/禁用防抖 + */ + setDebounceEnabled(enabled) { + this.isDebounceEnabled = enabled + } + + /** + * 创建新的请求信号 + * 如果有正在进行的请求,会先取消它 + */ + createSignal() { + // 取消之前的请求 + if (this.abortController) { + this.abortController.abort() + } + + // 创建新的 AbortController + this.abortController = new AbortController() + return this.abortController.signal + } + + /** + * 清理当前的 AbortController + */ + clear() { + this.abortController = null + } + + /** + * 清理防抖定时器 + */ + clearDebounceTimer() { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer) + this.debounceTimer = null + } + } + + /** + * 检查是否应该立即执行(不防抖) + * 某些情况下应该立即响应,不需要防抖 + */ + shouldExecuteImmediately() { + const now = Date.now() + const timeSinceLastTrigger = now - this.lastTriggerTime + + // 如果距离上次触发超过 1 秒,立即执行 + // 这避免了用户停止输入后再次输入时的延迟 + return timeSinceLastTrigger > 1000 + } + + /** + * 创建带防抖的请求处理器 + * 支持请求取消和智能防抖 + */ + createRequestHandler() { + return async (params) => { + this.lastTriggerTime = Date.now() + + // 如果启用了防抖且不应该立即执行 + if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { + // 清理之前的防抖定时器 + this.clearDebounceTimer() + + // 创建新的防抖 Promise + await new Promise((resolve) => { + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null + resolve() + }, this.debounceDelay) + }) + } + + // 执行实际的请求 + return this.executeRequest(params) + } + } + + /** + * 执行实际的 HTTP 请求(带重试) + */ + async executeRequest(params) { + let lastError + + for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) { + try { + if (attempt > 0) { + // eslint-disable-next-line no-console + console.log(`🔄 重试第 ${attempt} 次...`) + await this.sleep(this.retryConfig.retryDelay * attempt) + } + + const signal = this.createSignal() + const response = await fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params.body), + signal + }) + + if (!response.ok) { + const errorText = await response.text() + const error = new Error(`HTTP ${response.status}: ${response.statusText}`) + error.status = response.status + error.response = errorText + throw error + } + + const data = await response.json() + this.clear() + + return { + completion: data.completion || null, + error: data.error + } + } catch (error) { + lastError = error + + // 如果是取消错误,不重试 + if (error.name === 'AbortError') { + return { + completion: null, + error: 'Request cancelled' + } + } + + // 认证错误不重试 + if (error.status === 401 || error.status === 403) { + this.clear() + return this.handleError(error) + } + + // 最后一次尝试失败 + if (attempt === this.retryConfig.maxRetries) { + this.clear() + return this.handleError(error) + } + } + } + + this.clear() + return this.handleError(lastError) + } + + /** + * 延迟函数 + */ + sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + /** + * 统一的错误处理 + */ + handleError(error) { + let errorMessage = error.message + + // 根据错误类型提供更详细的信息 + if (error.status === 401 || error.status === 403) { + errorMessage = 'API Key 无效或已过期' + } else if (error.status === 429) { + errorMessage = '请求过于频繁,已达到速率限制' + } else if (error.status >= 500) { + errorMessage = `服务器错误: ${error.message}` + } else if (error.name === 'TypeError' && error.message.includes('fetch')) { + errorMessage = '网络错误:无法连接到 API 服务器' + } + + // eslint-disable-next-line no-console + console.error('❌ 请求失败:', errorMessage) + + if (error.response) { + // eslint-disable-next-line no-console + console.error('📄 错误详情:', error.response.substring(0, 200)) + } + + return { + completion: null, + error: errorMessage + } + } + + /** + * 重置状态(用于清理) + */ + reset() { + this.clearDebounceTimer() + if (this.abortController) { + this.abortController.abort() + this.clear() + } + } +} + +export const requestManager = new RequestManager() diff --git a/packages/plugins/script/src/js/requestManager.js b/packages/plugins/script/src/js/requestManager.js deleted file mode 100644 index ebc5b2a4c5..0000000000 --- a/packages/plugins/script/src/js/requestManager.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * 请求管理器 - 支持防抖和请求取消 - */ -class RequestManager { - constructor() { - this.abortController = null - this.endpoint = '' - this.debounceTimer = null - this.debounceDelay = 200 // 防抖延迟(毫秒) - this.lastTriggerTime = 0 - this.isDebounceEnabled = true - } - - /** - * 设置 API 端点 - */ - setEndpoint(endpoint) { - this.endpoint = endpoint - } - - /** - * 设置防抖延迟 - */ - setDebounceDelay(delay) { - this.debounceDelay = delay - } - - /** - * 启用/禁用防抖 - */ - setDebounceEnabled(enabled) { - this.isDebounceEnabled = enabled - } - - /** - * 创建新的请求信号 - * 如果有正在进行的请求,会先取消它 - */ - createSignal() { - // 取消之前的请求 - if (this.abortController) { - this.abortController.abort() - } - - // 创建新的 AbortController - this.abortController = new AbortController() - return this.abortController.signal - } - - /** - * 清理当前的 AbortController - */ - clear() { - this.abortController = null - } - - /** - * 清理防抖定时器 - */ - clearDebounceTimer() { - if (this.debounceTimer) { - clearTimeout(this.debounceTimer) - this.debounceTimer = null - } - } - - /** - * 检查是否应该立即执行(不防抖) - * 某些情况下应该立即响应,不需要防抖 - */ - shouldExecuteImmediately() { - const now = Date.now() - const timeSinceLastTrigger = now - this.lastTriggerTime - - // 如果距离上次触发超过 1 秒,立即执行 - // 这避免了用户停止输入后再次输入时的延迟 - return timeSinceLastTrigger > 1000 - } - - /** - * 创建带防抖的请求处理器 - * 支持请求取消和智能防抖 - */ - createRequestHandler() { - return async (params) => { - this.lastTriggerTime = Date.now() - - // 如果启用了防抖且不应该立即执行 - if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { - // 清理之前的防抖定时器 - this.clearDebounceTimer() - - // 创建新的防抖 Promise - await new Promise((resolve) => { - this.debounceTimer = setTimeout(() => { - this.debounceTimer = null - resolve() - }, this.debounceDelay) - }) - } - - // 执行实际的请求 - return this.executeRequest(params) - } - } - - /** - * 执行实际的 HTTP 请求 - */ - async executeRequest(params) { - const signal = this.createSignal() - - try { - const response = await fetch(this.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params.body), - signal // 添加取消信号 - }) - - if (!response.ok) { - const errorMsg = `HTTP ${response.status}: ${response.statusText}` - this.clear() - return { - completion: null, - error: errorMsg - } - } - - const data = await response.json() - this.clear() // 请求成功,清理 controller - - return { - completion: data.completion || null, - error: data.error - } - } catch (error) { - // 如果是取消错误 - if (error.name === 'AbortError') { - return { - completion: null, - error: 'Request cancelled' - } - } - - this.clear() - return { - completion: null, - error: error.message - } - } - } - - /** - * 重置状态(用于清理) - */ - reset() { - this.clearDebounceTimer() - if (this.abortController) { - this.abortController.abort() - this.clear() - } - } -} - -export const requestManager = new RequestManager() From 674fc6315a2704fd2fca3ab4505a076479ab95ec Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 05:25:01 -0800 Subject: [PATCH 03/10] feat(ai-completion): Enhance completion system with debounce management and FIM support --- .../robot/src/constants/model-config.ts | 1 - packages/plugins/script/src/Main.vue | 32 ++- .../src/ai-completion/adapters/index.js | 16 +- .../src/ai-completion/adapters/qwenAdapter.js | 5 +- .../script/src/ai-completion/constants.js | 25 +- .../plugins/script/src/ai-completion/index.js | 2 +- .../triggers/completionTrigger.js | 65 +---- .../ai-completion/utils/debounceManager.js | 99 ++++++++ .../src/ai-completion/utils/requestManager.js | 229 ------------------ 9 files changed, 163 insertions(+), 311 deletions(-) create mode 100644 packages/plugins/script/src/ai-completion/utils/debounceManager.js delete mode 100644 packages/plugins/script/src/ai-completion/utils/requestManager.js diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts index 1a34c745d9..81cceb3a54 100644 --- a/packages/plugins/robot/src/constants/model-config.ts +++ b/packages/plugins/robot/src/constants/model-config.ts @@ -121,7 +121,6 @@ export const DEFAULT_LLM_MODELS = [ } }, { - // TODO: https://api.deepseek.com/beta 支持 FIM label: 'Deepseek Coder编程模型', name: 'deepseek-chat', capabilities: { diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index d9629ffc0e..e9344ab8b7 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -34,14 +34,15 @@ /* metaService: engine.plugins.pagecontroller.Main */ import { onBeforeUnmount, reactive, provide } from 'vue' import { Button } from '@opentiny/vue' -import { registerCompletion } from 'monacopilot' +import { registerCompletion, type CompletionRegistration } from 'monacopilot' import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component' import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register' import { initCompletion } from '@opentiny/tiny-engine-common/js/completion' import { initLinter } from '@opentiny/tiny-engine-common/js/linter' import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method' -import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger' import { createCompletionHandler } from './ai-completion/adapters/index' +import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger' +import { debounceManager } from './ai-completion/utils/debounceManager' export const api = { saveMethod, @@ -69,6 +70,9 @@ export default { const { PLUGIN_NAME } = useLayout() + // 存储 AI 补全注册信息 + let completionRegistration: CompletionRegistration | null = null + const panelState = reactive({ emitEvent: emit }) @@ -122,17 +126,19 @@ export default { const monacoInstance = monacoRef.value.getMonaco() const editorInstance = monacoRef.value.getEditor() - registerCompletion(monacoInstance, editorInstance, { + // 配置防抖管理器 + debounceManager.setDebounceDelay(300) // 防抖延迟 300ms + debounceManager.setDebounceEnabled(true) + + completionRegistration = registerCompletion(monacoInstance, editorInstance, { language: 'javascript', - endpoint: '/app-center/api/chat/completions', filename: 'page.js', - trigger: 'onTyping', maxContextLines: 50, enableCaching: true, allowFollowUpCompletions: true, // 🎯 智能触发判断(在请求前执行,避免不必要的请求) - triggerIf: (params) => { + triggerIf: () => { const model = editorInstance.getModel() const position = editorInstance.getPosition() @@ -143,13 +149,16 @@ export default { position: { lineNumber: position.lineNumber, column: position.column - }, - triggerType: params.triggerType || 'onTyping' + } }) }, - // 🚀 请求处理器:支持 DeepSeek 和 Qwen 模型 - requestHandler: createCompletionHandler() as any + requestHandler: debounceManager.createRequestHandler(createCompletionHandler()) + }) + + // 注册快捷键:Ctrl+Space 触发 AI 补全 + editorInstance.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.Space, () => { + completionRegistration?.trigger?.() }) } catch (error) { // eslint-disable-next-line no-console @@ -158,6 +167,9 @@ export default { } onBeforeUnmount(() => { + // 清理 AI 补全 + completionRegistration?.deregister?.() + debounceManager.reset() ;(state.completionProvider as any)?.forEach?.((provider: any) => { provider?.dispose?.() }) diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index 1c226e3acf..951479c6a9 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -8,7 +8,7 @@ import { detectModelType, calculateTokens, getStopSequences } from '../utils/mod import { cleanCompletion, buildLowcodeMetadata } from '../utils/completionUtils.js' import { buildQwenMessages, callQwenAPI } from './qwenAdapter.js' import { buildDeepSeekMessages, callDeepSeekAPI } from './deepseekAdapter.js' -import { QWEN_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js' +import { QWEN_CONFIG, DEEPSEEK_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js' /** * 创建请求处理器 @@ -71,10 +71,20 @@ export function createCompletionHandler() { // ===== DeepSeek 流程(默认) ===== const { messages } = buildDeepSeekMessages(context, instruction, fileContent) - const config = { model: completeModel } + // DeepSeek 使用 Chat API,也需要 stop 序列 + const config = { + model: completeModel, + stopSequences: getStopSequences(null, MODEL_CONFIG.DEEPSEEK.TYPE) + } const httpClient = getMetaApi(META_SERVICE.Http) - completionText = await callDeepSeekAPI(messages, config, apiKey, baseUrl, httpClient) + // 构建 DeepSeek FIM 端点:将 /v1 替换为 /beta + const completionBaseUrl = baseUrl.replace(DEEPSEEK_CONFIG.PATH_REPLACE, DEEPSEEK_CONFIG.COMPLETION_PATH) + + // eslint-disable-next-line no-console + console.log('🔧 DeepSeek FIM 端点:', completionBaseUrl) + + completionText = await callDeepSeekAPI(messages, config, apiKey, completionBaseUrl, httpClient) } // 6. 处理补全结果 diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js index ac9fb1829c..0a56992a00 100644 --- a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -2,7 +2,7 @@ * Qwen 专用适配器 * 使用 Completions API + FIM (Fill-In-the-Middle) */ -import { QWEN_CONFIG, API_ENDPOINTS, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' +import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' /** * 构建 Qwen FIM 格式的 messages @@ -40,7 +40,8 @@ export function buildQwenMessages(fileContent, fimBuilder) { * @returns {Promise} 补全文本 */ export async function callQwenAPI(messages, config, apiKey, baseUrl) { - const completionsUrl = `${baseUrl}${API_ENDPOINTS.COMPLETIONS_PATH}` + // 构建完整的 Completions API URL + const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}` // eslint-disable-next-line no-console console.log('📦 模型:', config.model) diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index f159dce636..aaf83e6f55 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -2,8 +2,7 @@ * Qwen Coder API 配置(阿里云百炼) */ export const QWEN_CONFIG = { - API_URL: 'https://dashscope.aliyuncs.com/compatible-mode/v1/completions', - MODEL: 'qwen2.5-coder-32b-instruct', + COMPLETION_PATH: '/completions', // Completions API 路径(追加到 baseUrl) DEFAULT_TEMPERATURE: 0.05, TOP_P: 0.95, PRESENCE_PENALTY: 0.2, @@ -15,13 +14,30 @@ export const QWEN_CONFIG = { } } +/** + * DeepSeek Coder API 配置 + */ +export const DEEPSEEK_CONFIG = { + COMPLETION_PATH: '/beta', // FIM 补全 API 路径 + PATH_REPLACE: '/v1', // 需要从 baseUrl 中替换的路径 + DEFAULT_TEMPERATURE: 0, + TOP_P: 1.0, + + // FIM (Fill-In-the-Middle) 配置 + FIM: { + MAX_PREFIX_LINES: 100, + MAX_SUFFIX_LINES: 50, + MAX_TOKENS: 4096 // FIM 最大补全长度 4K + } +} + /** * 模型配置 */ export const MODEL_CONFIG = { QWEN: { TYPE: 'qwen', - KEYWORDS: ['qwen'] // 移除 'coder',避免误匹配 deepseek-coder + KEYWORDS: ['qwen'] }, DEEPSEEK: { TYPE: 'deepseek', @@ -37,8 +53,7 @@ export const MODEL_CONFIG = { * API 端点配置 */ export const API_ENDPOINTS = { - COMPLETIONS_PATH: '/completions', - CHAT_COMPLETIONS: '/app-center/api/chat/completions' + CHAT_COMPLETIONS: '/app-center/api/chat/completions' // 后端代理端点(DeepSeek 使用) } /** diff --git a/packages/plugins/script/src/ai-completion/index.js b/packages/plugins/script/src/ai-completion/index.js index f34545c829..ba1e0b644b 100644 --- a/packages/plugins/script/src/ai-completion/index.js +++ b/packages/plugins/script/src/ai-completion/index.js @@ -3,5 +3,5 @@ */ export { createCompletionHandler } from './adapters/index.js' export { shouldTriggerCompletion } from './triggers/completionTrigger.js' -export { requestManager } from './utils/requestManager.js' +export { debounceManager } from './utils/debounceManager.js' export { createSmartPrompt, FIMPromptBuilder } from './builders/index.js' diff --git a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js index 8864b0cd79..417f532ea1 100644 --- a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js +++ b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js @@ -1,45 +1,7 @@ /** - * 智能补全触发条件判断(JS/TS 专用) + * 智能补全触发条件判断 */ -/** - * 检测是否在注释中 - */ -function isInComment(beforeCursor, fullText) { - const trimmed = beforeCursor.trim() - - // 单行注释 - if (trimmed.startsWith('//') || trimmed.startsWith('*')) { - return true - } - - // 块注释 - const lastBlockStart = fullText.lastIndexOf('/*', fullText.indexOf(beforeCursor)) - const lastBlockEnd = fullText.lastIndexOf('*/', fullText.indexOf(beforeCursor)) - if (lastBlockStart > lastBlockEnd) { - return true - } - - return false -} - -/** - * 检测是否在字符串中 - */ -function isInString(beforeCursor) { - const singleQuotes = (beforeCursor.match(/'/g) || []).length - const doubleQuotes = (beforeCursor.match(/"/g) || []).length - return singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 -} - -/** - * 检测是否在模板字符串中 - */ -function isInTemplateString(beforeCursor) { - const backticks = (beforeCursor.match(/`/g) || []).length - return backticks % 2 === 1 -} - /** * 检测光标是否在语句结束符后(分号后) */ @@ -87,7 +49,6 @@ function isAfterBlockEnd(beforeCursor) { * @param {Object} params.position - 光标位置 * @param {number} params.position.lineNumber - 行号 * @param {number} params.position.column - 列号 - * @param {string} params.triggerType - 触发类型 * @returns {boolean} 是否触发补全 */ export function shouldTriggerCompletion(params) { @@ -95,34 +56,18 @@ export function shouldTriggerCompletion(params) { const lines = text.split('\n') const currentLine = lines[position.lineNumber - 1] || '' const beforeCursor = currentLine.substring(0, position.column - 1) - const trimmedLine = beforeCursor.trim() - - // 1. 避免在注释中触发 - if (isInComment(beforeCursor, text)) { - return false - } - - // 2. 避免在普通字符串中触发(但允许模板字符串) - if (isInString(beforeCursor) && !isInTemplateString(beforeCursor)) { - return false - } - - // 3. 代码太短不触发(降低阈值) - if (text.trim().length < 5) { - return false - } - // 4. 完全空行不触发 - if (trimmedLine.length === 0) { + // 1. 代码太短不触发 + if (text.trim().length < 2) { return false } - // 5. 分号后不触发(语句已结束) + // 2. 分号后不触发(语句已结束) if (isAfterStatementEnd(beforeCursor)) { return false } - // 6. 右花括号后不触发(块已结束) + // 3. 右花括号后不触发(块已结束) if (isAfterBlockEnd(beforeCursor)) { return false } diff --git a/packages/plugins/script/src/ai-completion/utils/debounceManager.js b/packages/plugins/script/src/ai-completion/utils/debounceManager.js new file mode 100644 index 0000000000..14f8ed630b --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/debounceManager.js @@ -0,0 +1,99 @@ +/** + * 防抖管理器 - 仅支持防抖功能 + */ +class DebounceManager { + constructor() { + this.debounceTimer = null + this.debounceDelay = 200 // 防抖延迟(毫秒) + this.lastTriggerTime = 0 + this.isDebounceEnabled = true + } + + /** + * 设置防抖延迟 + */ + setDebounceDelay(delay) { + this.debounceDelay = delay + } + + /** + * 启用/禁用防抖 + */ + setDebounceEnabled(enabled) { + this.isDebounceEnabled = enabled + } + + /** + * 清理防抖定时器 + */ + clearDebounceTimer() { + if (this.debounceTimer) { + // eslint-disable-next-line no-console + console.log('⏱️ [DebounceManager] 清理防抖定时器') + clearTimeout(this.debounceTimer) + this.debounceTimer = null + } + } + + /** + * 检查是否应该立即执行(不防抖) + * 某些情况下应该立即响应,不需要防抖 + */ + shouldExecuteImmediately() { + const now = Date.now() + const timeSinceLastTrigger = now - this.lastTriggerTime + + // 如果距离上次触发超过 1 秒,立即执行 + // 这避免了用户停止输入后再次输入时的延迟 + return timeSinceLastTrigger > 1000 + } + + /** + * 创建带防抖的请求处理器 + * @param {Function} handler - 实际的请求处理函数 + */ + createRequestHandler(handler) { + return async (params) => { + this.lastTriggerTime = Date.now() + + // 如果启用了防抖且不应该立即执行 + if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { + // eslint-disable-next-line no-console + console.log(`⏳ [DebounceManager] 防抖延迟 ${this.debounceDelay}ms`) + // 清理之前的防抖定时器 + this.clearDebounceTimer() + + // 创建新的防抖 Promise + await new Promise((resolve) => { + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null + // eslint-disable-next-line no-console + console.log('✅ [DebounceManager] 防抖延迟结束,准备执行请求') + resolve() + }, this.debounceDelay) + }) + } else { + // eslint-disable-next-line no-console + console.log('⚡ [DebounceManager] 立即执行(无防抖)') + } + + // 执行实际的请求处理器 + if (handler) { + return await handler(params) + } + + return null + } + } + + /** + * 重置状态(用于清理) + */ + reset() { + // eslint-disable-next-line no-console + console.log('🔄 [DebounceManager] 重置状态') + this.clearDebounceTimer() + } +} + +export const debounceManager = new DebounceManager() diff --git a/packages/plugins/script/src/ai-completion/utils/requestManager.js b/packages/plugins/script/src/ai-completion/utils/requestManager.js deleted file mode 100644 index 240205d024..0000000000 --- a/packages/plugins/script/src/ai-completion/utils/requestManager.js +++ /dev/null @@ -1,229 +0,0 @@ -/** - * 请求管理器 - 支持防抖、请求取消和重试 - */ -class RequestManager { - constructor() { - this.abortController = null - this.endpoint = '' - this.debounceTimer = null - this.debounceDelay = 200 // 防抖延迟(毫秒) - this.lastTriggerTime = 0 - this.isDebounceEnabled = true - this.retryConfig = { - maxRetries: 2, - retryDelay: 1000 - } - } - - /** - * 设置 API 端点 - */ - setEndpoint(endpoint) { - this.endpoint = endpoint - } - - /** - * 设置防抖延迟 - */ - setDebounceDelay(delay) { - this.debounceDelay = delay - } - - /** - * 启用/禁用防抖 - */ - setDebounceEnabled(enabled) { - this.isDebounceEnabled = enabled - } - - /** - * 创建新的请求信号 - * 如果有正在进行的请求,会先取消它 - */ - createSignal() { - // 取消之前的请求 - if (this.abortController) { - this.abortController.abort() - } - - // 创建新的 AbortController - this.abortController = new AbortController() - return this.abortController.signal - } - - /** - * 清理当前的 AbortController - */ - clear() { - this.abortController = null - } - - /** - * 清理防抖定时器 - */ - clearDebounceTimer() { - if (this.debounceTimer) { - clearTimeout(this.debounceTimer) - this.debounceTimer = null - } - } - - /** - * 检查是否应该立即执行(不防抖) - * 某些情况下应该立即响应,不需要防抖 - */ - shouldExecuteImmediately() { - const now = Date.now() - const timeSinceLastTrigger = now - this.lastTriggerTime - - // 如果距离上次触发超过 1 秒,立即执行 - // 这避免了用户停止输入后再次输入时的延迟 - return timeSinceLastTrigger > 1000 - } - - /** - * 创建带防抖的请求处理器 - * 支持请求取消和智能防抖 - */ - createRequestHandler() { - return async (params) => { - this.lastTriggerTime = Date.now() - - // 如果启用了防抖且不应该立即执行 - if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { - // 清理之前的防抖定时器 - this.clearDebounceTimer() - - // 创建新的防抖 Promise - await new Promise((resolve) => { - this.debounceTimer = setTimeout(() => { - this.debounceTimer = null - resolve() - }, this.debounceDelay) - }) - } - - // 执行实际的请求 - return this.executeRequest(params) - } - } - - /** - * 执行实际的 HTTP 请求(带重试) - */ - async executeRequest(params) { - let lastError - - for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) { - try { - if (attempt > 0) { - // eslint-disable-next-line no-console - console.log(`🔄 重试第 ${attempt} 次...`) - await this.sleep(this.retryConfig.retryDelay * attempt) - } - - const signal = this.createSignal() - const response = await fetch(this.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params.body), - signal - }) - - if (!response.ok) { - const errorText = await response.text() - const error = new Error(`HTTP ${response.status}: ${response.statusText}`) - error.status = response.status - error.response = errorText - throw error - } - - const data = await response.json() - this.clear() - - return { - completion: data.completion || null, - error: data.error - } - } catch (error) { - lastError = error - - // 如果是取消错误,不重试 - if (error.name === 'AbortError') { - return { - completion: null, - error: 'Request cancelled' - } - } - - // 认证错误不重试 - if (error.status === 401 || error.status === 403) { - this.clear() - return this.handleError(error) - } - - // 最后一次尝试失败 - if (attempt === this.retryConfig.maxRetries) { - this.clear() - return this.handleError(error) - } - } - } - - this.clear() - return this.handleError(lastError) - } - - /** - * 延迟函数 - */ - sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - /** - * 统一的错误处理 - */ - handleError(error) { - let errorMessage = error.message - - // 根据错误类型提供更详细的信息 - if (error.status === 401 || error.status === 403) { - errorMessage = 'API Key 无效或已过期' - } else if (error.status === 429) { - errorMessage = '请求过于频繁,已达到速率限制' - } else if (error.status >= 500) { - errorMessage = `服务器错误: ${error.message}` - } else if (error.name === 'TypeError' && error.message.includes('fetch')) { - errorMessage = '网络错误:无法连接到 API 服务器' - } - - // eslint-disable-next-line no-console - console.error('❌ 请求失败:', errorMessage) - - if (error.response) { - // eslint-disable-next-line no-console - console.error('📄 错误详情:', error.response.substring(0, 200)) - } - - return { - completion: null, - error: errorMessage - } - } - - /** - * 重置状态(用于清理) - */ - reset() { - this.clearDebounceTimer() - if (this.abortController) { - this.abortController.abort() - this.clear() - } - } -} - -export const requestManager = new RequestManager() From 0a229a6ed482b11e665e113310fd060fdbd473a3 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 05:27:57 -0800 Subject: [PATCH 04/10] feat(ai-completion): Remove debug console logs from adapters and utilities --- .../src/ai-completion/adapters/deepseekAdapter.js | 3 --- .../script/src/ai-completion/adapters/index.js | 6 ------ .../script/src/ai-completion/adapters/qwenAdapter.js | 10 ---------- .../src/ai-completion/builders/promptBuilder.js | 3 ++- .../script/src/ai-completion/utils/debounceManager.js | 11 ----------- 5 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js index 657c1e3974..32fb7b22af 100644 --- a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -13,9 +13,6 @@ import { API_ENDPOINTS, HTTP_CONFIG } from '../constants.js' * @returns {{ messages: Array, cursorContext: null }} Messages 和上下文 */ export function buildDeepSeekMessages(context, instruction, fileContent) { - // eslint-disable-next-line no-console - console.log('🎯 使用 DeepSeek Chat 格式') - const systemPrompt = `${context}\n\n${SYSTEM_BASE_PROMPT}` const userPrompt = createUserPrompt(instruction, fileContent) diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index 951479c6a9..a3b9d1e351 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -81,9 +81,6 @@ export function createCompletionHandler() { // 构建 DeepSeek FIM 端点:将 /v1 替换为 /beta const completionBaseUrl = baseUrl.replace(DEEPSEEK_CONFIG.PATH_REPLACE, DEEPSEEK_CONFIG.COMPLETION_PATH) - // eslint-disable-next-line no-console - console.log('🔧 DeepSeek FIM 端点:', completionBaseUrl) - completionText = await callDeepSeekAPI(messages, config, apiKey, completionBaseUrl, httpClient) } @@ -91,9 +88,6 @@ export function createCompletionHandler() { if (completionText) { completionText = completionText.trim() - // eslint-disable-next-line no-console - console.log('✅ 收到补全:', completionText.substring(0, DEFAULTS.LOG_PREVIEW_LENGTH)) - completionText = cleanCompletion(completionText, modelType, cursorContext) return { diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js index 0a56992a00..ea8ffe9d68 100644 --- a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -13,13 +13,6 @@ import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' export function buildQwenMessages(fileContent, fimBuilder) { const { fimPrompt, cursorContext } = fimBuilder.buildOptimizedFIMPrompt(fileContent) - // eslint-disable-next-line no-console - console.log('🎯 使用 Qwen FIM 格式') - // eslint-disable-next-line no-console - console.log('📊 FIM 上下文:', cursorContext.type) - // eslint-disable-next-line no-console - console.log('📏 FIM Prompt 长度:', fimPrompt.length) - return { messages: [ { @@ -43,9 +36,6 @@ export async function callQwenAPI(messages, config, apiKey, baseUrl) { // 构建完整的 Completions API URL const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}` - // eslint-disable-next-line no-console - console.log('📦 模型:', config.model) - const requestBody = { model: config.model, prompt: messages[0].content, // FIM prompt diff --git a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js index 9d7e91b718..cbcbb2f0d4 100644 --- a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js @@ -184,7 +184,8 @@ export function createSmartPrompt(completionMetadata) { const validation = validateLowcodeContext(lowcodeContext) if (!validation.valid) { - // console.warn('⚠️ Lowcode context validation warnings:', validation.warnings); + // eslint-disable-next-line no-console + console.warn('⚠️ Lowcode context validation warnings:', validation.warnings) } instruction = createLowcodeInstruction(language, lowcodeContext) diff --git a/packages/plugins/script/src/ai-completion/utils/debounceManager.js b/packages/plugins/script/src/ai-completion/utils/debounceManager.js index 14f8ed630b..55b56cb2b6 100644 --- a/packages/plugins/script/src/ai-completion/utils/debounceManager.js +++ b/packages/plugins/script/src/ai-completion/utils/debounceManager.js @@ -28,8 +28,6 @@ class DebounceManager { */ clearDebounceTimer() { if (this.debounceTimer) { - // eslint-disable-next-line no-console - console.log('⏱️ [DebounceManager] 清理防抖定时器') clearTimeout(this.debounceTimer) this.debounceTimer = null } @@ -58,8 +56,6 @@ class DebounceManager { // 如果启用了防抖且不应该立即执行 if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { - // eslint-disable-next-line no-console - console.log(`⏳ [DebounceManager] 防抖延迟 ${this.debounceDelay}ms`) // 清理之前的防抖定时器 this.clearDebounceTimer() @@ -67,14 +63,9 @@ class DebounceManager { await new Promise((resolve) => { this.debounceTimer = setTimeout(() => { this.debounceTimer = null - // eslint-disable-next-line no-console - console.log('✅ [DebounceManager] 防抖延迟结束,准备执行请求') resolve() }, this.debounceDelay) }) - } else { - // eslint-disable-next-line no-console - console.log('⚡ [DebounceManager] 立即执行(无防抖)') } // 执行实际的请求处理器 @@ -90,8 +81,6 @@ class DebounceManager { * 重置状态(用于清理) */ reset() { - // eslint-disable-next-line no-console - console.log('🔄 [DebounceManager] 重置状态') this.clearDebounceTimer() } } From 32f9f336bc75d4aad13f7010f5907665b8186344 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 18:40:33 -0800 Subject: [PATCH 05/10] feat(ai-completion): Simplify completion system and remove debounce manager --- packages/plugins/script/src/Main.vue | 53 +++++------ .../ai-completion/adapters/deepseekAdapter.js | 2 +- .../script/src/ai-completion/constants.js | 2 +- .../plugins/script/src/ai-completion/index.js | 1 - .../ai-completion/utils/debounceManager.js | 88 ------------------- 5 files changed, 22 insertions(+), 124 deletions(-) delete mode 100644 packages/plugins/script/src/ai-completion/utils/debounceManager.js diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index e9344ab8b7..8d2057f64b 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -34,7 +34,7 @@ /* metaService: engine.plugins.pagecontroller.Main */ import { onBeforeUnmount, reactive, provide } from 'vue' import { Button } from '@opentiny/vue' -import { registerCompletion, type CompletionRegistration } from 'monacopilot' +import { registerCompletion, type CompletionRegistration, type RegisterCompletionOptions } from 'monacopilot' import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component' import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register' import { initCompletion } from '@opentiny/tiny-engine-common/js/completion' @@ -42,7 +42,6 @@ import { initLinter } from '@opentiny/tiny-engine-common/js/linter' import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method' import { createCompletionHandler } from './ai-completion/adapters/index' import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger' -import { debounceManager } from './ai-completion/utils/debounceManager' export const api = { saveMethod, @@ -70,8 +69,8 @@ export default { const { PLUGIN_NAME } = useLayout() - // 存储 AI 补全注册信息 - let completionRegistration: CompletionRegistration | null = null + type RequestHandler = NonNullable + let completion: CompletionRegistration | null = null const panelState = reactive({ emitEvent: emit @@ -123,42 +122,32 @@ export default { // 🆕 新增: 注册 AI 补全 try { - const monacoInstance = monacoRef.value.getMonaco() - const editorInstance = monacoRef.value.getEditor() + const monaco = monacoRef.value.getMonaco() + const editor = monacoRef.value.getEditor() - // 配置防抖管理器 - debounceManager.setDebounceDelay(300) // 防抖延迟 300ms - debounceManager.setDebounceEnabled(true) - - completionRegistration = registerCompletion(monacoInstance, editorInstance, { + completion = registerCompletion(monaco, editor, { language: 'javascript', filename: 'page.js', maxContextLines: 50, enableCaching: true, - allowFollowUpCompletions: true, - - // 🎯 智能触发判断(在请求前执行,避免不必要的请求) - triggerIf: () => { - const model = editorInstance.getModel() - const position = editorInstance.getPosition() - - if (!model || !position) return false - + allowFollowUpCompletions: false, + trigger: 'onIdle', + triggerIf: ({ text, position }) => { return shouldTriggerCompletion({ - text: model.getValue(), - position: { - lineNumber: position.lineNumber, - column: position.column - } + text, + position }) }, - - requestHandler: debounceManager.createRequestHandler(createCompletionHandler()) + requestHandler: createCompletionHandler() as RequestHandler }) - // 注册快捷键:Ctrl+Space 触发 AI 补全 - editorInstance.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.Space, () => { - completionRegistration?.trigger?.() + monaco.editor.addEditorAction({ + id: 'monacopilot.triggerCompletion', + label: 'Complete Code', + contextMenuGroupId: 'navigation', + run: () => { + completion!.trigger() + } }) } catch (error) { // eslint-disable-next-line no-console @@ -167,9 +156,7 @@ export default { } onBeforeUnmount(() => { - // 清理 AI 补全 - completionRegistration?.deregister?.() - debounceManager.reset() + completion?.deregister?.() ;(state.completionProvider as any)?.forEach?.((provider: any) => { provider?.dispose?.() }) diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js index 32fb7b22af..7e8f58789d 100644 --- a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -32,7 +32,7 @@ export function buildDeepSeekMessages(context, instruction, fileContent) { } /** - * 调用 DeepSeek Chat API(通过后端代理) + * 调用 DeepSeek Chat API * @param {Array} messages - Messages 数组 * @param {Object} config - 配置对象 * @param {string} apiKey - API 密钥 diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index aaf83e6f55..0be3f9b94f 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -53,7 +53,7 @@ export const MODEL_CONFIG = { * API 端点配置 */ export const API_ENDPOINTS = { - CHAT_COMPLETIONS: '/app-center/api/chat/completions' // 后端代理端点(DeepSeek 使用) + CHAT_COMPLETIONS: '/app-center/api/chat/completions' } /** diff --git a/packages/plugins/script/src/ai-completion/index.js b/packages/plugins/script/src/ai-completion/index.js index ba1e0b644b..ae14a7a2fb 100644 --- a/packages/plugins/script/src/ai-completion/index.js +++ b/packages/plugins/script/src/ai-completion/index.js @@ -3,5 +3,4 @@ */ export { createCompletionHandler } from './adapters/index.js' export { shouldTriggerCompletion } from './triggers/completionTrigger.js' -export { debounceManager } from './utils/debounceManager.js' export { createSmartPrompt, FIMPromptBuilder } from './builders/index.js' diff --git a/packages/plugins/script/src/ai-completion/utils/debounceManager.js b/packages/plugins/script/src/ai-completion/utils/debounceManager.js deleted file mode 100644 index 55b56cb2b6..0000000000 --- a/packages/plugins/script/src/ai-completion/utils/debounceManager.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 防抖管理器 - 仅支持防抖功能 - */ -class DebounceManager { - constructor() { - this.debounceTimer = null - this.debounceDelay = 200 // 防抖延迟(毫秒) - this.lastTriggerTime = 0 - this.isDebounceEnabled = true - } - - /** - * 设置防抖延迟 - */ - setDebounceDelay(delay) { - this.debounceDelay = delay - } - - /** - * 启用/禁用防抖 - */ - setDebounceEnabled(enabled) { - this.isDebounceEnabled = enabled - } - - /** - * 清理防抖定时器 - */ - clearDebounceTimer() { - if (this.debounceTimer) { - clearTimeout(this.debounceTimer) - this.debounceTimer = null - } - } - - /** - * 检查是否应该立即执行(不防抖) - * 某些情况下应该立即响应,不需要防抖 - */ - shouldExecuteImmediately() { - const now = Date.now() - const timeSinceLastTrigger = now - this.lastTriggerTime - - // 如果距离上次触发超过 1 秒,立即执行 - // 这避免了用户停止输入后再次输入时的延迟 - return timeSinceLastTrigger > 1000 - } - - /** - * 创建带防抖的请求处理器 - * @param {Function} handler - 实际的请求处理函数 - */ - createRequestHandler(handler) { - return async (params) => { - this.lastTriggerTime = Date.now() - - // 如果启用了防抖且不应该立即执行 - if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) { - // 清理之前的防抖定时器 - this.clearDebounceTimer() - - // 创建新的防抖 Promise - await new Promise((resolve) => { - this.debounceTimer = setTimeout(() => { - this.debounceTimer = null - resolve() - }, this.debounceDelay) - }) - } - - // 执行实际的请求处理器 - if (handler) { - return await handler(params) - } - - return null - } - } - - /** - * 重置状态(用于清理) - */ - reset() { - this.clearDebounceTimer() - } -} - -export const debounceManager = new DebounceManager() From 9cbb592a0f9055e942eff784bfe6afb7363725fc Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 19:26:53 -0800 Subject: [PATCH 06/10] feat(completion): Remove AI inline completion and context template --- .../common/js/completion-files/context.md | 51 ------- packages/common/js/completion.js | 137 +----------------- 2 files changed, 1 insertion(+), 187 deletions(-) delete mode 100644 packages/common/js/completion-files/context.md diff --git a/packages/common/js/completion-files/context.md b/packages/common/js/completion-files/context.md deleted file mode 100644 index a508be21a5..0000000000 --- a/packages/common/js/completion-files/context.md +++ /dev/null @@ -1,51 +0,0 @@ -你是一个JavaScript代码补全器,可以使用JS和ES的语法 - -以下是一些通用的协议: -常规属性如:{ width: '300px' } -一. 变量引用 -{ width: { type: 'JSExpression', value: 'this.state.xxx' } -即当type为JSExpression,取其value并将value的值当做变量调用 -二. 方法引用 -{ onClickNew: { type: 'JSFunction', value: 'function onClickNew() {}' } -即当type为JSFunction,取其value并将value的值函数调用 -以下是一些依赖,调用均以this.开头: -1. 数据源 -数据源是定义的数据模型 -const dataSource=$dataSource$ -调用方式为: this.dataSource.xxx -2. 工具类 -工具类是通用的调用方法或npm依赖 -const utils=$utils$ -调用方式为: this.utils.xxx -utils有两种类型 -type为npm时,读取content内容,可构造如下引用,例如content中package(依赖包名)为@opentiny/vue,destructuring(解构)为true,exportName(导出组件名称)为Notify,实际引用方式是import { Notify } from '@opentiny/vue'; -type为function时,读取content内容,当content.type为JSFunction则将value视为JS方法并调用,其他可参考通用的协议 -3. 全局变量 -全局变量是使用pinia创建的变量 -const stores=$globalState$ -调用方式为: this.stores.xxx -4. JS变量 -js变量 -const state=$state$ -调用方式为: this.state.xxx -5. JS方法 -js方法 -const methods=$methods$ -调用方式为: this.xxx - -以上依赖中没有的,则不能调用,如utils中没有axios,则axios不能使用 - -以下是当前选中的组件 -$currentSchema$ -请理解当前组件,componentName为组件名称,组件包括tinyVue组件、ElementPlus组件,和基本html元素 -对象中的ref属性即vue组件的ref属性,如ref值为testForm,使用方式为this.$('testForm') -props表示组件的属性,是一个对象,对应vue组件的defineProps和defineEmits中的内容 -props中以on开头的表示其传递的是方法,如onClick,其值可以参考通用协议 -props中没有以on开头的则是普通属性,如tinyInput组件中的placeholder -props的属性中值为对象,且包含type和value属性,type为JSExpression和JSFunction时,value的值则参考通用协议取用 - -直接上下文如下: -$codeBeforeCursor$$codeAfterCursor$ -请从(光标位置)后进行补全 -注意如果是函数时,须以function关键字开头,不使用箭头函数 -请只返回代码,且只返回一个示例,不需要思考过程和解释 \ No newline at end of file diff --git a/packages/common/js/completion.js b/packages/common/js/completion.js index d8a1aee624..99011d5398 100644 --- a/packages/common/js/completion.js +++ b/packages/common/js/completion.js @@ -9,9 +9,7 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import { ref } from 'vue' -import { useCanvas, useResource, getMergeMeta, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' -import completion from './completion-files/context.md?raw' +import { useCanvas, useResource } from '@opentiny/tiny-engine-meta-register' const keyWords = [ 'state', @@ -173,135 +171,6 @@ const getRange = (position, words) => ({ endColumn: words[words.length - 1].endColumn }) -const generateBaseReference = () => { - const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState - const { state, methods } = useCanvas().getPageSchema() - const currentSchema = useCanvas().getCurrentSchema() - let referenceContext = completion - referenceContext = referenceContext.replace('$dataSource$', JSON.stringify(dataSource)) - referenceContext = referenceContext.replace('$utils$', JSON.stringify(utils)) - referenceContext = referenceContext.replace('$globalState$', JSON.stringify(globalState)) - referenceContext = referenceContext.replace('$state$', JSON.stringify(state)) - referenceContext = referenceContext.replace('$methods$', JSON.stringify(methods)) - referenceContext = referenceContext.replace('$currentSchema$', JSON.stringify(currentSchema)) - return referenceContext -} - -const fetchAiInlineCompletion = (codeBeforeCursor, codeAfterCursor) => { - const { completeModel, apiKey, baseUrl } = getMetaApi(META_SERVICE.Robot).getSelectedQuickModelInfo() || {} - if (!completeModel || !apiKey || !baseUrl) { - return - } - const referenceContext = generateBaseReference() - return getMetaApi(META_SERVICE.Http).post( - '/app-center/api/chat/completions', - { - model: completeModel, - messages: [ - { - role: 'user', - content: referenceContext - .replace('$codeBeforeCursor$', codeBeforeCursor) - .replace('$codeAfterCursor$', codeAfterCursor) - } - ], - baseUrl, - stream: false - }, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey || ''}` - } - } - ) -} - -const initInlineCompletion = (monacoInstance, editorModel) => { - const requestAllowed = ref(true) - const timer = ref() - const inlineCompletionProvider = { - provideInlineCompletions(model, position, _context, _token) { - if (editorModel && model.id !== editorModel.id) { - return new Promise((resolve) => { - resolve({ items: [] }) - }) - } - - if (timer.value) { - clearTimeout(timer.value) - } - - const words = getWords(model, position) - const range = getRange(position, words) - const wordContent = words.map((item) => item.word).join('') - if (!wordContent || wordContent.lastIndexOf('}') === 0 || wordContent.length < 4) { - return new Promise((resolve) => { - resolve({ items: [] }) - }) - } - if (!requestAllowed.value) { - return new Promise((resolve) => { - resolve({ - items: [ - { - insertText: '', - range - } - ] - }) - }) - } - const codeBeforeCursor = model.getValueInRange({ - startLineNumber: 1, - startColumn: 1, - endLineNumber: position.lineNumber, - endColumn: position.column - }) - const codeAfterCursor = model.getValueInRange({ - startLineNumber: position.lineNumber, - startColumn: position.column, - endLineNumber: model.getLineCount(), - endColumn: model.getLineMaxColumn(model.getLineCount()) - }) - return new Promise((resolve) => { - // 延迟请求800ms - timer.value = setTimeout(() => { - // 节流操作,防止接口一直被请求 - requestAllowed.value = false - fetchAiInlineCompletion(codeBeforeCursor, codeAfterCursor) - .then((res) => { - let insertText = res.choices[0].message.content.trim() - const wordContentIndex = insertText.indexOf(wordContent) - if (wordContentIndex === -1) { - insertText = `${wordContent}${insertText}\n` - } - if (wordContentIndex > 0) { - insertText = insertText.slice(wordContentIndex) - } - requestAllowed.value = true - resolve({ - items: [ - { - insertText, - range - } - ] - }) - }) - .catch(() => { - requestAllowed.value = true - }) - }, 800) - }) - }, - freeInlineCompletions() {} - } - return ['javascript', 'typescript'].map((lang) => - monacoInstance.languages.registerInlineCompletionsProvider(lang, inlineCompletionProvider) - ) -} - export const initCompletion = (monacoInstance, editorModel, conditionFn) => { const completionItemProvider = { provideCompletionItems(model, position, _context, _token) { @@ -331,9 +200,5 @@ export const initCompletion = (monacoInstance, editorModel, conditionFn) => { const completions = ['javascript', 'typescript'].map((lang) => { return monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider) }) - const { enableAICompletion } = getMergeMeta('engine.plugins.pagecontroller')?.options || {} - if (enableAICompletion) { - return completions.concat(initInlineCompletion(monacoInstance, editorModel)) - } return completions } From fcfeca95a73f038bd0bb5fc663155a0c63076fef Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 19:34:06 -0800 Subject: [PATCH 07/10] feat(robot): Enhance model selection with code completion capabilities --- .../robot-setting/RobotSetting.vue | 38 +++++++--- .../robot/src/composables/core/useConfig.ts | 16 +++- .../robot/src/constants/model-config.ts | 8 +- .../plugins/robot/src/types/setting.types.ts | 1 + packages/plugins/script/meta.js | 3 +- packages/plugins/script/src/Main.vue | 76 ++++++++++--------- 6 files changed, 93 insertions(+), 49 deletions(-) diff --git a/packages/plugins/robot/src/components/header-extension/robot-setting/RobotSetting.vue b/packages/plugins/robot/src/components/header-extension/robot-setting/RobotSetting.vue index c224da35b6..469ae6f524 100644 --- a/packages/plugins/robot/src/components/header-extension/robot-setting/RobotSetting.vue +++ b/packages/plugins/robot/src/components/header-extension/robot-setting/RobotSetting.vue @@ -42,7 +42,7 @@ 快速模型 @@ -51,11 +51,25 @@ + popper-class="model-select-popper" + > + +
@@ -172,8 +186,8 @@ const emit = defineEmits(['close']) const { robotSettingState, saveRobotSettingState, - getAllAvailableModels, getCompactModels, + getNonCodeCompletionModels, addCustomService, updateService, deleteService, @@ -196,9 +210,9 @@ const state = reactive({ editingService: undefined as ModelService | undefined }) -// 获取所有可用模型选项 +// 获取所有可用模型选项(排除代码补全专用模型) const allModelOptions = computed(() => { - return getAllAvailableModels().map((model) => ({ + return getNonCodeCompletionModels().map((model) => ({ label: model.displayLabel, value: model.value, capabilities: model.capabilities @@ -207,10 +221,14 @@ const allModelOptions = computed(() => { // 获取快速模型选项 const compactModelOptions = computed(() => { - return getCompactModels().map((model) => ({ + const models = getCompactModels().map((model) => ({ label: model.displayLabel, - value: model.value + value: model.value, + capabilities: model.capabilities, + serviceName: model.serviceName })) + + return models.sort((a, b) => a.serviceName.localeCompare(b.serviceName, 'zh-CN')) }) // 获取当前选择的默认模型信息 @@ -270,8 +288,8 @@ const addService = () => { state.showServiceDialog = true } -const editService = (service: ModelService) => { - state.editingService = JSON.parse(JSON.stringify(service)) +const editService = (service: any) => { + state.editingService = JSON.parse(JSON.stringify(service)) as ModelService state.showServiceDialog = true } diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts index 01908a89a0..da8e174ec6 100644 --- a/packages/plugins/robot/src/composables/core/useConfig.ts +++ b/packages/plugins/robot/src/composables/core/useConfig.ts @@ -318,9 +318,19 @@ const getAllAvailableModels = () => { ) } -// 获取快速模型列表 +// 获取快速模型列表(包含 compact 或 codeCompletion 的模型) const getCompactModels = () => { - return getAllAvailableModels().filter((model) => model.capabilities?.compact) + return getAllAvailableModels().filter((model) => model.capabilities?.compact || model.capabilities?.codeCompletion) +} + +// 获取代码补全优化模型列表 +const getCodeCompletionModels = () => { + return getAllAvailableModels().filter((model) => model.capabilities?.codeCompletion) +} + +// 获取非代码补全模型列表(用于默认助手模型) +const getNonCodeCompletionModels = () => { + return getAllAvailableModels().filter((model) => !model.capabilities?.codeCompletion) } const updateThinkingState = (value: boolean) => { @@ -456,6 +466,8 @@ export default () => { getModelCapabilities, getAllAvailableModels, getCompactModels, + getCodeCompletionModels, // 代码补全模型列表 + getNonCodeCompletionModels, // 非代码补全模型列表 getSelectedModelInfo, // 对话模型信息 getSelectedQuickModelInfo, // 快速模型信息 diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts index 81cceb3a54..1e471cb54b 100644 --- a/packages/plugins/robot/src/constants/model-config.ts +++ b/packages/plugins/robot/src/constants/model-config.ts @@ -57,6 +57,7 @@ export const DEFAULT_LLM_MODELS = [ name: 'qwen3-coder-plus', capabilities: { toolCalling: true, + codeCompletion: true, reasoning: reasoningExtraBody, jsonOutput: bailianJsonOutputExtraBody } @@ -86,6 +87,7 @@ export const DEFAULT_LLM_MODELS = [ capabilities: { toolCalling: true, compact: true, + codeCompletion: true, jsonOutput: bailianJsonOutputExtraBody } }, @@ -95,6 +97,7 @@ export const DEFAULT_LLM_MODELS = [ capabilities: { toolCalling: true, compact: true, + codeCompletion: true, jsonOutput: bailianJsonOutputExtraBody } } @@ -122,11 +125,12 @@ export const DEFAULT_LLM_MODELS = [ }, { label: 'Deepseek Coder编程模型', - name: 'deepseek-chat', + name: 'deepseek-coder', capabilities: { toolCalling: true, compact: true, - jsonOutput: bailianJsonOutputExtraBody + codeCompletion: true, + jsonOutput: jsonOutputExtraBody } } ] diff --git a/packages/plugins/robot/src/types/setting.types.ts b/packages/plugins/robot/src/types/setting.types.ts index 9699afedc6..40420b08b3 100644 --- a/packages/plugins/robot/src/types/setting.types.ts +++ b/packages/plugins/robot/src/types/setting.types.ts @@ -30,6 +30,7 @@ export interface ModelConfig { vision?: boolean reasoning?: boolean | Capability compact?: boolean + codeCompletion?: boolean jsonOutput?: boolean | Capability } } diff --git a/packages/plugins/script/meta.js b/packages/plugins/script/meta.js index b7f9e6134a..779a029fb4 100644 --- a/packages/plugins/script/meta.js +++ b/packages/plugins/script/meta.js @@ -6,7 +6,8 @@ export default { width: 600, widthResizable: true, options: { - enableAICompletion: false // 禁用旧的 AI 补全系统,使用新的 monacopilot + aiCompletionEnabled: true + // aiCompletionTrigger: 'onIdle' // 可选:触发模式 'onIdle'(默认) | 'onTyping' | 'onDemand' }, confirm: 'close' // 当点击插件栏切换或关闭前是否需要确认, 会调用插件中confirm值指定的方法,e.g. 此处指向 close方法,会调用插件的close方法执行确认逻辑 } diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index 8d2057f64b..cc75cb86b5 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -36,7 +36,7 @@ import { onBeforeUnmount, reactive, provide } from 'vue' import { Button } from '@opentiny/vue' import { registerCompletion, type CompletionRegistration, type RegisterCompletionOptions } from 'monacopilot' import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component' -import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register' +import { useHelp, useLayout, getMergeMeta } from '@opentiny/tiny-engine-meta-register' import { initCompletion } from '@opentiny/tiny-engine-common/js/completion' import { initLinter } from '@opentiny/tiny-engine-common/js/linter' import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method' @@ -70,6 +70,7 @@ export default { const { PLUGIN_NAME } = useLayout() type RequestHandler = NonNullable + type TriggerMode = NonNullable let completion: CompletionRegistration | null = null const panelState = reactive({ @@ -120,43 +121,50 @@ export default { // 保留原有的 ESLint state.linterWorker = initLinter(editor, monacoRef.value.getMonaco(), state) as any - // 🆕 新增: 注册 AI 补全 - try { - const monaco = monacoRef.value.getMonaco() - const editor = monacoRef.value.getEditor() - - completion = registerCompletion(monaco, editor, { - language: 'javascript', - filename: 'page.js', - maxContextLines: 50, - enableCaching: true, - allowFollowUpCompletions: false, - trigger: 'onIdle', - triggerIf: ({ text, position }) => { - return shouldTriggerCompletion({ - text, - position - }) - }, - requestHandler: createCompletionHandler() as RequestHandler - }) - - monaco.editor.addEditorAction({ - id: 'monacopilot.triggerCompletion', - label: 'Complete Code', - contextMenuGroupId: 'navigation', - run: () => { - completion!.trigger() - } - }) - } catch (error) { - // eslint-disable-next-line no-console - console.error('❌ AI 补全注册失败:', error) + const { aiCompletionEnabled, aiCompletionTrigger = 'onIdle' } = + getMergeMeta('engine.plugins.pagecontroller')?.options || {} + + if (aiCompletionEnabled) { + try { + const monaco = monacoRef.value.getMonaco() + const editor = monacoRef.value.getEditor() + + completion = registerCompletion(monaco, editor, { + language: 'javascript', + filename: 'page.js', + maxContextLines: 50, + enableCaching: true, + allowFollowUpCompletions: false, + trigger: aiCompletionTrigger as TriggerMode, + triggerIf: ({ text, position }) => { + return shouldTriggerCompletion({ + text, + position + }) + }, + requestHandler: createCompletionHandler() as RequestHandler + }) + + monaco.editor.addEditorAction({ + id: 'monacopilot.triggerCompletion', + label: 'Complete Code', + contextMenuGroupId: 'navigation', + run: () => { + completion!.trigger() + } + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('❌ AI 补全注册失败:', error) + } } } onBeforeUnmount(() => { - completion?.deregister?.() + // 清理 AI 补全 + if (completion) { + completion.deregister() + } ;(state.completionProvider as any)?.forEach?.((provider: any) => { provider?.dispose?.() }) From fc69d6cbfdf5d09187530a8876748e18abb246d0 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 23:32:06 -0800 Subject: [PATCH 08/10] fix: review suggestion --- packages/plugins/script/src/Main.vue | 2 +- .../script/src/ai-completion/adapters/deepseekAdapter.js | 4 ---- .../plugins/script/src/ai-completion/adapters/index.js | 3 --- .../script/src/ai-completion/adapters/qwenAdapter.js | 4 ---- .../src/ai-completion/builders/lowcodeContextBuilder.js | 5 ----- .../plugins/script/src/ai-completion/prompts/templates.js | 7 ------- .../script/src/ai-completion/triggers/completionTrigger.js | 4 ---- .../script/src/ai-completion/utils/completionUtils.js | 3 --- .../plugins/script/src/ai-completion/utils/modelUtils.js | 3 --- 9 files changed, 1 insertion(+), 34 deletions(-) diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index cc75cb86b5..81ec35bcb5 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -35,7 +35,7 @@ import { onBeforeUnmount, reactive, provide } from 'vue' import { Button } from '@opentiny/vue' import { registerCompletion, type CompletionRegistration, type RegisterCompletionOptions } from 'monacopilot' -import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component' +import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common' import { useHelp, useLayout, getMergeMeta } from '@opentiny/tiny-engine-meta-register' import { initCompletion } from '@opentiny/tiny-engine-common/js/completion' import { initLinter } from '@opentiny/tiny-engine-common/js/linter' diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js index 7e8f58789d..bcba24a8c5 100644 --- a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -1,7 +1,3 @@ -/** - * DeepSeek 专用适配器 - * 使用 Chat Completions API(通过后端代理) - */ import { SYSTEM_BASE_PROMPT, createUserPrompt } from '../prompts/templates.js' import { API_ENDPOINTS, HTTP_CONFIG } from '../constants.js' diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index a3b9d1e351..669d349f11 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -1,6 +1,3 @@ -/** - * AI 补全适配器主入口 - */ import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' import { createSmartPrompt } from '../builders/promptBuilder.js' import { FIMPromptBuilder } from '../builders/fimPromptBuilder.js' diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js index ea8ffe9d68..7f509d5fb3 100644 --- a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -1,7 +1,3 @@ -/** - * Qwen 专用适配器 - * 使用 Completions API + FIM (Fill-In-the-Middle) - */ import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' /** diff --git a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js index a2bb4aaac5..754be23d9e 100644 --- a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -1,8 +1,3 @@ -/** - * 低代码上下文构建器 - * 用于从低代码平台的元数据中提取和构建代码补全所需的上下文信息 - */ - /** * 格式化数据源信息 * @param {Array} dataSource - 数据源数组 diff --git a/packages/plugins/script/src/ai-completion/prompts/templates.js b/packages/plugins/script/src/ai-completion/prompts/templates.js index c4f4d286cb..0569d8bd6c 100644 --- a/packages/plugins/script/src/ai-completion/prompts/templates.js +++ b/packages/plugins/script/src/ai-completion/prompts/templates.js @@ -1,10 +1,3 @@ -/** - * AI Prompt 模板集合 - * - * 这个文件包含所有用于代码补全的提示词模板。 - * 提示词的调整不会影响业务逻辑,可以独立进行 A/B 测试。 - */ - /** * 系统 Prompt - 定义 AI 的角色和基本规则 */ diff --git a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js index 417f532ea1..76d58b3122 100644 --- a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js +++ b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js @@ -1,7 +1,3 @@ -/** - * 智能补全触发条件判断 - */ - /** * 检测光标是否在语句结束符后(分号后) */ diff --git a/packages/plugins/script/src/ai-completion/utils/completionUtils.js b/packages/plugins/script/src/ai-completion/utils/completionUtils.js index e2476398ec..ce9d1e03b9 100644 --- a/packages/plugins/script/src/ai-completion/utils/completionUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js @@ -1,6 +1,3 @@ -/** - * 补全处理工具函数 - */ import { useResource, useCanvas } from '@opentiny/tiny-engine-meta-register' import { MODEL_COMMON_CONFIG, FIM_CONFIG } from '../constants.js' diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js index 8f611251b4..f93a72b9a9 100644 --- a/packages/plugins/script/src/ai-completion/utils/modelUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -1,6 +1,3 @@ -/** - * 模型相关工具函数 - */ import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, FIM_CONFIG } from '../constants.js' /** From a0f2de895dd6691cafc29ef532fe28b5a4115308 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Mon, 19 Jan 2026 04:00:16 -0800 Subject: [PATCH 09/10] feat(script): Add keyboard shortcut for code completion trigger --- packages/plugins/script/src/Main.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index 81ec35bcb5..836dde0926 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -149,6 +149,7 @@ export default { id: 'monacopilot.triggerCompletion', label: 'Complete Code', contextMenuGroupId: 'navigation', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Space], run: () => { completion!.trigger() } From 7e25b0a9cf61932b3e7d104c48101ff169737a40 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Mon, 19 Jan 2026 20:52:52 -0800 Subject: [PATCH 10/10] feat(ai-completion): Migrate DeepSeek adapter to FIM-based completion --- .../ai-completion/adapters/deepseekAdapter.js | 83 +++++----- .../src/ai-completion/adapters/index.js | 84 ++++++---- .../src/ai-completion/adapters/qwenAdapter.js | 5 +- .../builders/fimPromptBuilder.js | 145 ++++++++++++++---- .../script/src/ai-completion/constants.js | 5 +- .../ai-completion/utils/completionUtils.js | 17 +- 6 files changed, 225 insertions(+), 114 deletions(-) diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js index bcba24a8c5..7eb1278f88 100644 --- a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -1,57 +1,60 @@ -import { SYSTEM_BASE_PROMPT, createUserPrompt } from '../prompts/templates.js' -import { API_ENDPOINTS, HTTP_CONFIG } from '../constants.js' +import { HTTP_CONFIG, ERROR_MESSAGES, DEEPSEEK_CONFIG } from '../constants.js' /** - * 构建 DeepSeek Chat 格式的 messages - * @param {string} context - 上下文信息 - * @param {string} instruction - 指令 - * @param {string} fileContent - 文件内容 - * @returns {{ messages: Array, cursorContext: null }} Messages 和上下文 + * 构建 DeepSeek FIM 格式的请求参数 + * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记) + * @param {Object} fimBuilder - FIM 构建器实例 + * @param {Object} metadata - 元数据(language, lowcodeMetadata 等) + * @returns {{ prompt: string, suffix: string, cursorContext: Object }} FIM 参数和上下文 */ -export function buildDeepSeekMessages(context, instruction, fileContent) { - const systemPrompt = `${context}\n\n${SYSTEM_BASE_PROMPT}` - const userPrompt = createUserPrompt(instruction, fileContent) +export function buildDeepSeekFIMParams(fileContent, fimBuilder, metadata = {}) { + const { prefix, suffix, cursorContext } = fimBuilder.buildFIMComponents(fileContent, metadata) return { - messages: [ - { - role: 'system', - content: systemPrompt - }, - { - role: 'user', - content: userPrompt - } - ], - cursorContext: null + prompt: prefix, + suffix, + cursorContext } } /** - * 调用 DeepSeek Chat API - * @param {Array} messages - Messages 数组 + * 调用 DeepSeek FIM Completions API + * @param {string} prompt - 前缀内容 + * @param {string} suffix - 后缀内容 * @param {Object} config - 配置对象 * @param {string} apiKey - API 密钥 * @param {string} baseUrl - 基础 URL - * @param {Object} httpClient - HTTP 客户端 * @returns {Promise} 补全文本 */ -export async function callDeepSeekAPI(messages, config, apiKey, baseUrl, httpClient) { - const response = await httpClient.post( - API_ENDPOINTS.CHAT_COMPLETIONS, - { - model: config.model, - messages, - baseUrl, - stream: HTTP_CONFIG.STREAM +export async function callDeepSeekAPI(prompt, suffix, config, apiKey, baseUrl) { + // 构建 DeepSeek FIM API URL:将 /v1 替换为 /beta/completions + const completionsUrl = baseUrl.replace(DEEPSEEK_CONFIG.PATH_REPLACE, DEEPSEEK_CONFIG.COMPLETION_PATH) + '/completions' + + const requestBody = { + model: config.model, + prompt, + suffix, + max_tokens: config.maxTokens || DEEPSEEK_CONFIG.FIM.MAX_TOKENS, + temperature: DEEPSEEK_CONFIG.DEFAULT_TEMPERATURE, + top_p: DEEPSEEK_CONFIG.TOP_P, + stream: HTTP_CONFIG.STREAM, + stop: config.stopSequences + } + + const fetchResponse = await fetch(completionsUrl, { + method: HTTP_CONFIG.METHOD, + headers: { + 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, + Authorization: `Bearer ${apiKey}` }, - { - headers: { - 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, - Authorization: `Bearer ${apiKey || ''}` - } - } - ) + body: JSON.stringify(requestBody) + }) + + if (!fetchResponse.ok) { + const errorText = await fetchResponse.text() + throw new Error(`${ERROR_MESSAGES.REQUEST_FAILED} ${fetchResponse.status}: ${errorText}`) + } - return response?.choices?.[0]?.message?.content + const response = await fetchResponse.json() + return response?.choices?.[0]?.text } diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index 669d349f11..4f3c281028 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -4,7 +4,7 @@ import { FIMPromptBuilder } from '../builders/fimPromptBuilder.js' import { detectModelType, calculateTokens, getStopSequences } from '../utils/modelUtils.js' import { cleanCompletion, buildLowcodeMetadata } from '../utils/completionUtils.js' import { buildQwenMessages, callQwenAPI } from './qwenAdapter.js' -import { buildDeepSeekMessages, callDeepSeekAPI } from './deepseekAdapter.js' +import { buildDeepSeekFIMParams, callDeepSeekAPI } from './deepseekAdapter.js' import { QWEN_CONFIG, DEEPSEEK_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js' /** @@ -12,7 +12,9 @@ import { QWEN_CONFIG, DEEPSEEK_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } * @returns {Function} 请求处理函数 */ export function createCompletionHandler() { - const fimBuilder = new FIMPromptBuilder(QWEN_CONFIG) + // 为不同模型创建 FIM 构建器 + const qwenFimBuilder = new FIMPromptBuilder(QWEN_CONFIG) + const deepseekFimBuilder = new FIMPromptBuilder(DEEPSEEK_CONFIG) return async (params) => { try { @@ -36,7 +38,7 @@ export function createCompletionHandler() { // 3. 构建低代码元数据和 prompt const lowcodeMetadata = buildLowcodeMetadata() - const { context, instruction, fileContent } = createSmartPrompt({ + const { fileContent } = createSmartPrompt({ textBeforeCursor, textAfterCursor, language, @@ -45,43 +47,67 @@ export function createCompletionHandler() { lowcodeMetadata }) - // 4. 检测模型类型 + // 4. 检测模型类型并构建 FIM 参数 const modelType = detectModelType(completeModel) - let completionText = null - let cursorContext = null + // 5. 准备元数据(用于增强 FIM prompt) + const fimMetadata = { + language, + isComment: textBeforeCursor.trim().endsWith('//') || textBeforeCursor.includes('/*'), + lowcodeContext: lowcodeMetadata + ? { + dataSource: lowcodeMetadata.dataSource || [], + utils: lowcodeMetadata.utils || [], + globalState: lowcodeMetadata.globalState || [], + state: lowcodeMetadata.state || {}, + methods: lowcodeMetadata.methods || {}, + currentSchema: lowcodeMetadata.currentSchema || null + } + : null + } + + // 6. 根据模型类型构建请求参数 + let completionText + let cursorContext - // 5. 根据模型类型调用不同的 API if (modelType === MODEL_CONFIG.QWEN.TYPE) { // ===== Qwen 流程 ===== - const { messages, cursorContext: ctx } = buildQwenMessages(fileContent, fimBuilder) + const { messages, cursorContext: ctx } = buildQwenMessages(fileContent, qwenFimBuilder, fimMetadata) cursorContext = ctx - const config = { - model: completeModel, - maxTokens: calculateTokens(cursorContext), - stopSequences: getStopSequences(cursorContext, MODEL_CONFIG.QWEN.TYPE) - } - - completionText = await callQwenAPI(messages, config, apiKey, baseUrl) + completionText = await callQwenAPI( + messages, + { + model: completeModel, + maxTokens: calculateTokens(ctx), + stopSequences: getStopSequences(ctx, modelType) + }, + apiKey, + baseUrl + ) } else { - // ===== DeepSeek 流程(默认) ===== - const { messages } = buildDeepSeekMessages(context, instruction, fileContent) - - // DeepSeek 使用 Chat API,也需要 stop 序列 - const config = { - model: completeModel, - stopSequences: getStopSequences(null, MODEL_CONFIG.DEEPSEEK.TYPE) - } - const httpClient = getMetaApi(META_SERVICE.Http) - - // 构建 DeepSeek FIM 端点:将 /v1 替换为 /beta - const completionBaseUrl = baseUrl.replace(DEEPSEEK_CONFIG.PATH_REPLACE, DEEPSEEK_CONFIG.COMPLETION_PATH) + // ===== DeepSeek 流程(使用 FIM API) ===== + const { + prompt, + suffix, + cursorContext: ctx + } = buildDeepSeekFIMParams(fileContent, deepseekFimBuilder, fimMetadata) + cursorContext = ctx - completionText = await callDeepSeekAPI(messages, config, apiKey, completionBaseUrl, httpClient) + completionText = await callDeepSeekAPI( + prompt, + suffix, + { + model: completeModel, + maxTokens: calculateTokens(ctx), + stopSequences: getStopSequences(ctx, modelType) + }, + apiKey, + baseUrl + ) } - // 6. 处理补全结果 + // 7. 处理补全结果 if (completionText) { completionText = completionText.trim() diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js index 7f509d5fb3..aaae498270 100644 --- a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -4,10 +4,11 @@ import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' * 构建 Qwen FIM 格式的 messages * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记) * @param {Object} fimBuilder - FIM 构建器实例 + * @param {Object} metadata - 元数据(language, lowcodeMetadata 等) * @returns {{ messages: Array, cursorContext: Object }} Messages 和上下文 */ -export function buildQwenMessages(fileContent, fimBuilder) { - const { fimPrompt, cursorContext } = fimBuilder.buildOptimizedFIMPrompt(fileContent) +export function buildQwenMessages(fileContent, fimBuilder, metadata = {}) { + const { fimPrompt, cursorContext } = fimBuilder.buildOptimizedFIMPrompt(fileContent, metadata) return { messages: [ diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js index 00f9cb0ca1..b15c586e84 100644 --- a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js @@ -1,4 +1,11 @@ import { FIM_CONFIG } from '../constants.js' +import { + SYSTEM_BASE_PROMPT, + createCodeInstruction, + createLowcodeInstruction, + BLOCK_COMMENT_INSTRUCTION, + LINE_COMMENT_INSTRUCTION +} from '../prompts/templates.js' /** * FIM (Fill-In-the-Middle) Prompt 构建器 @@ -10,55 +17,118 @@ export class FIMPromptBuilder { } /** - * 构建优化的 FIM (Fill In the Middle) Prompt + * 构建增强的 FIM 组件(包含完整指令) * @param {string} fileContent - 文件内容,包含 [CURSOR] 标记 - * @returns {{ fimPrompt: string, cursorContext: Object }} FIM prompt 和上下文信息 + * @param {Object} metadata - 元数据(language, isComment, lowcodeContext 等) + * @returns {{ prefix: string, suffix: string, cursorContext: Object }} FIM 组件 */ - buildOptimizedFIMPrompt(fileContent) { - // 1. 清理元信息注释 - let cleanedContent = this.cleanMetaInfo(fileContent) + buildFIMComponents(fileContent, metadata = {}) { + const { language = 'javascript', isComment = false, lowcodeContext = null } = metadata - // 2. 查找光标位置 - const cursorIndex = cleanedContent.indexOf(FIM_CONFIG.MARKERS.CURSOR) + // 1. 查找光标位置 + const cursorIndex = fileContent.indexOf(FIM_CONFIG.MARKERS.CURSOR) if (cursorIndex === -1) { return { - fimPrompt: `${FIM_CONFIG.MARKERS.PREFIX}${cleanedContent}${FIM_CONFIG.MARKERS.SUFFIX}`, + prefix: fileContent, + suffix: '', cursorContext: { type: 'unknown', hasPrefix: true, hasSuffix: false } } } - // 3. 分割前缀和后缀 - const prefix = cleanedContent.substring(0, cursorIndex) - const suffix = cleanedContent.substring(cursorIndex + FIM_CONFIG.MARKERS.CURSOR.length) + // 2. 分割前缀和后缀 + const rawPrefix = fileContent.substring(0, cursorIndex) + const rawSuffix = fileContent.substring(cursorIndex + FIM_CONFIG.MARKERS.CURSOR.length) + + // 3. 分析光标上下文 + const cursorContext = this.analyzeCursorContext(rawPrefix, rawSuffix) - // 4. 分析光标上下文 - const cursorContext = this.analyzeCursorContext(prefix, suffix) + // 4. 构建完整的指令前缀 + const instructionPrefix = this.buildInstructionPrefix(language, isComment, lowcodeContext, cursorContext) // 5. 优化前缀和后缀 - const optimizedPrefix = this.optimizePrefix(prefix) - const optimizedSuffix = this.optimizeSuffix(suffix) + const optimizedPrefix = this.optimizePrefix(rawPrefix) + const optimizedSuffix = this.optimizeSuffix(rawSuffix) - // 6. 构建 FIM prompt - let fimPrompt - if (optimizedSuffix.trim().length > 0) { - // 有后缀:使用 prefix + suffix + middle 模式 - fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${optimizedPrefix}${FIM_CONFIG.MARKERS.SUFFIX}${optimizedSuffix}${FIM_CONFIG.MARKERS.MIDDLE}` + // 6. 组合:指令 + 代码前缀 + const fullPrefix = instructionPrefix + optimizedPrefix + + return { + prefix: fullPrefix, + suffix: optimizedSuffix, + cursorContext + } + } + + /** + * 构建指令前缀(将 system prompt 和 instruction 转换为注释形式) + * @param {string} language - 编程语言 + * @param {boolean} isComment - 是否在注释中 + * @param {Object} lowcodeContext - 低代码上下文 + * @param {Object} cursorContext - 光标上下文 + * @returns {string} 指令前缀 + */ + buildInstructionPrefix(language, isComment, lowcodeContext, cursorContext) { + let instruction = '' + + // 1. 添加系统基础提示(转换为注释) + instruction += '// ===== AI COMPLETION INSTRUCTIONS =====\n' + instruction += this.convertToComments(SYSTEM_BASE_PROMPT) + instruction += '//\n' + + // 2. 添加具体的补全指令 + let specificInstruction + if (isComment) { + // 注释补全 + specificInstruction = cursorContext.inBlockComment ? BLOCK_COMMENT_INSTRUCTION : LINE_COMMENT_INSTRUCTION + } else if (lowcodeContext) { + // 低代码补全 + specificInstruction = createLowcodeInstruction(language, lowcodeContext) } else { - // 无后缀:只使用 prefix + suffix 模式 - fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${optimizedPrefix}${FIM_CONFIG.MARKERS.SUFFIX}` + // 普通代码补全 + specificInstruction = createCodeInstruction(language) } - return { fimPrompt, cursorContext } + instruction += this.convertToComments(specificInstruction) + instruction += '//\n' + instruction += '// ===== CODE CONTEXT STARTS BELOW =====\n' + instruction += '\n' + + return instruction } /** - * 清理元信息注释 - * @param {string} content - 原始内容 - * @returns {string} 清理后的内容 + * 将多行文本转换为注释格式 + * @param {string} text - 原始文本 + * @returns {string} 注释格式的文本 */ - cleanMetaInfo(content) { - return content.replace(FIM_CONFIG.META_INFO_PATTERN, '') + convertToComments(text) { + return text + .split('\n') + .map((line) => (line.trim() ? `// ${line}` : '//')) + .join('\n') + } + + /** + * 构建优化的 FIM (Fill In the Middle) Prompt(Qwen 格式) + * @param {string} fileContent - 文件内容,包含 [CURSOR] 标记 + * @param {Object} metadata - 元数据 + * @returns {{ fimPrompt: string, cursorContext: Object }} FIM prompt 和上下文信息 + */ + buildOptimizedFIMPrompt(fileContent, metadata = {}) { + const { prefix, suffix, cursorContext } = this.buildFIMComponents(fileContent, metadata) + + // 构建 Qwen FIM prompt + let fimPrompt + if (suffix.trim().length > 0) { + // 有后缀:使用 prefix + suffix + middle 模式 + fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${prefix}${FIM_CONFIG.MARKERS.SUFFIX}${suffix}${FIM_CONFIG.MARKERS.MIDDLE}` + } else { + // 无后缀:只使用 prefix + suffix 模式 + fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${prefix}${FIM_CONFIG.MARKERS.SUFFIX}` + } + + return { fimPrompt, cursorContext } } /** @@ -76,10 +146,29 @@ export class FIMPromptBuilder { inClass: false, inObject: false, inArray: false, + inBlockComment: false, + inLineComment: false, needsExpression: false, needsStatement: false } + // 检测是否在注释中 + const lastBlockStart = prefix.lastIndexOf('/*') + const lastBlockEnd = prefix.lastIndexOf('*/') + if (lastBlockStart > lastBlockEnd) { + context.inBlockComment = true + context.type = 'block-comment' + return context + } + + const lastLineBreak = prefix.lastIndexOf('\n') + const currentLine = prefix.substring(lastLineBreak + 1) + if (currentLine.trim().startsWith('//')) { + context.inLineComment = true + context.type = 'line-comment' + return context + } + // 分析前缀最后几个字符 const prefixTrimmed = prefix.trimEnd() diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index 0be3f9b94f..e38ec7c364 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -165,10 +165,7 @@ export const FIM_CONFIG = { EXPRESSION: [';', '\n)', ','], STATEMENT: [], // 使用通用停止符即可 OBJECT: [] // 使用通用停止符即可 - }, - - META_INFO_PATTERN: - /^(\/\/ File:.*\n)?(\/\/ Language:.*\n)?(\/\/ Current .*\n)*(\/\/ IMPORTANT:.*\n)*(\/\/ Technologies:.*\n)?(\/\/ NOTE:.*\n)*\n*/ + } } /** diff --git a/packages/plugins/script/src/ai-completion/utils/completionUtils.js b/packages/plugins/script/src/ai-completion/utils/completionUtils.js index ce9d1e03b9..80e3b634e1 100644 --- a/packages/plugins/script/src/ai-completion/utils/completionUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js @@ -1,5 +1,5 @@ import { useResource, useCanvas } from '@opentiny/tiny-engine-meta-register' -import { MODEL_COMMON_CONFIG, FIM_CONFIG } from '../constants.js' +import { MODEL_COMMON_CONFIG } from '../constants.js' /** * 构建低代码元数据 @@ -35,19 +35,14 @@ export function cleanCompletion(text, modelType, cursorContext = null) { // 1. 移除 markdown 代码块 cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.MARKDOWN_CODE_BLOCK, '') - // 2. 移除前后空行 + // 2. 移除 [CURSOR] 标记(如果模型返回了它) + cleaned = cleaned.replace(/\[CURSOR\]/g, '') + cleaned = cleaned.replace(/\/\/ \[CURSOR\]/g, '') + + // 3. 移除前后空行 cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.LEADING_EMPTY_LINES, '') cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.TRAILING_EMPTY_LINES, '') - // 3. Qwen 特殊处理:移除 FIM 标记 - if (modelType === 'qwen') { - Object.values(FIM_CONFIG.MARKERS).forEach((marker) => { - if (marker !== FIM_CONFIG.MARKERS.CURSOR) { - cleaned = cleaned.replace(new RegExp(marker.replace(/[|<>]/g, '\\$&'), 'g'), '') - } - }) - } - // 4. 表达式特殊处理:移除尾部分号 if (cursorContext?.needsExpression) { cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.TRAILING_SEMICOLON, '')