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 } 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 4458339325..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 } @@ -81,23 +82,24 @@ 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, + codeCompletion: true, jsonOutput: bailianJsonOutputExtraBody } }, { - 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, + codeCompletion: true, + jsonOutput: bailianJsonOutputExtraBody + } } ] }, @@ -120,6 +122,16 @@ export const DEFAULT_LLM_MODELS = [ }, jsonOutput: jsonOutputExtraBody } + }, + { + label: 'Deepseek Coder编程模型', + name: 'deepseek-coder', + capabilities: { + toolCalling: true, + compact: true, + 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 6991805f54..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: true + aiCompletionEnabled: true + // aiCompletionTrigger: 'onIdle' // 可选:触发模式 'onIdle'(默认) | 'onTyping' | 'onDemand' }, confirm: 'close' // 当点击插件栏切换或关闭前是否需要确认, 会调用插件中confirm值指定的方法,e.g. 此处指向 close方法,会调用插件的close方法执行确认逻辑 } 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..836dde0926 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 { registerCompletion, type CompletionRegistration, type RegisterCompletionOptions } from 'monacopilot' import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common' -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' +import { createCompletionHandler } from './ai-completion/adapters/index' +import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger' export const api = { saveMethod, @@ -59,13 +62,17 @@ 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 }) const { PLUGIN_NAME } = useLayout() + type RequestHandler = NonNullable + type TriggerMode = NonNullable + let completion: CompletionRegistration | null = null + const panelState = reactive({ emitEvent: emit }) @@ -101,24 +108,69 @@ 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 + + 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', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Space], + run: () => { + completion!.trigger() + } + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('❌ AI 补全注册失败:', error) + } } - - // Lowcode API 提示 - state.completionProvider = initCompletion(monaco.value.getMonaco(), monaco.value.getEditor()?.getModel()) - - // 初始化 ESLint worker - state.linterWorker = initLinter(editor, monaco.value.getMonaco(), state) } onBeforeUnmount(() => { - state.completionProvider?.forEach((provider) => { - provider.dispose() + // 清理 AI 补全 + if (completion) { + completion.deregister() + } + ;(state.completionProvider as any)?.forEach?.((provider: any) => { + provider?.dispose?.() }) // 终止 ESLint worker - state.linterWorker?.terminate?.() + ;(state.linterWorker as any)?.terminate?.() }) 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..bcba24a8c5 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -0,0 +1,57 @@ +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) { + 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..669d349f11 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -0,0 +1,109 @@ +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, DEEPSEEK_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) + + // 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) + + completionText = await callDeepSeekAPI(messages, config, apiKey, completionBaseUrl, httpClient) + } + + // 6. 处理补全结果 + if (completionText) { + completionText = completionText.trim() + + 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..7f509d5fb3 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -0,0 +1,62 @@ +import { QWEN_CONFIG, 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) + + 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) { + // 构建完整的 Completions API URL + const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}` + + 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..754be23d9e --- /dev/null +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -0,0 +1,270 @@ +/** + * 格式化数据源信息 + * @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..cbcbb2f0d4 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js @@ -0,0 +1,204 @@ +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) { + // eslint-disable-next-line no-console + 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..0be3f9b94f --- /dev/null +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -0,0 +1,193 @@ +/** + * Qwen Coder API 配置(阿里云百炼) + */ +export const QWEN_CONFIG = { + COMPLETION_PATH: '/completions', // Completions API 路径(追加到 baseUrl) + 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 + } +} + +/** + * 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'] + }, + DEEPSEEK: { + TYPE: 'deepseek', + KEYWORDS: ['deepseek'] + }, + UNKNOWN: { + TYPE: 'unknown', + KEYWORDS: [] + } +} + +/** + * API 端点配置 + */ +export const API_ENDPOINTS = { + 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..ae14a7a2fb --- /dev/null +++ b/packages/plugins/script/src/ai-completion/index.js @@ -0,0 +1,6 @@ +/** + * AI 补全模块统一导出 + */ +export { createCompletionHandler } from './adapters/index.js' +export { shouldTriggerCompletion } from './triggers/completionTrigger.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..0569d8bd6c --- /dev/null +++ b/packages/plugins/script/src/ai-completion/prompts/templates.js @@ -0,0 +1,190 @@ +/** + * 系统 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/ai-completion/triggers/completionTrigger.js b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js new file mode 100644 index 0000000000..76d58b3122 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js @@ -0,0 +1,73 @@ +/** + * 检测光标是否在语句结束符后(分号后) + */ +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 - 列号 + * @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) + + // 1. 代码太短不触发 + if (text.trim().length < 2) { + return false + } + + // 2. 分号后不触发(语句已结束) + if (isAfterStatementEnd(beforeCursor)) { + return false + } + + // 3. 右花括号后不触发(块已结束) + if (isAfterBlockEnd(beforeCursor)) { + return false + } + + // 其他情况都允许触发 + return true +} 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..ce9d1e03b9 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js @@ -0,0 +1,90 @@ +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..f93a72b9a9 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -0,0 +1,78 @@ +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 +}