From 4ee0a831fb5b9d6cb5233988fb8ce2486f33372e Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 13 Jan 2026 05:24:35 -0800 Subject: [PATCH 01/15] 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 ccb85614e7..2dfcd073fb 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 c877933f51bffe2717375fcee44b481f97f1a876 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 02:19:02 -0800 Subject: [PATCH 02/15] 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 2f6d19bc2309036bf8b213155671297de8618246 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 05:25:01 -0800 Subject: [PATCH 03/15] 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 2a4b04815c96e254a81db576d0178bfd61cc865a Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 05:27:57 -0800 Subject: [PATCH 04/15] 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 c9c7cbba0005c96cd06e6f097a6372f5776b1f0a Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 18:40:33 -0800 Subject: [PATCH 05/15] 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 a503d2131299dce673240eba5596f98e007af6a5 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 19:26:53 -0800 Subject: [PATCH 06/15] 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 3c8cc861bd44fe889f80f84f49528243f6d5e748 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 19:34:06 -0800 Subject: [PATCH 07/15] 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 038f8aa2e4..515047ff55 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 576739e50aa7b30f57bf5e2c3c580f4188f4c9e6 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 15 Jan 2026 23:32:06 -0800 Subject: [PATCH 08/15] 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 245e55c726cfb29fb38bff595e0fae5bdac6125b Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Mon, 19 Jan 2026 04:00:16 -0800 Subject: [PATCH 09/15] 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 df287b6871766ca620791f62a7ce44b8b5d934e0 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Mon, 19 Jan 2026 20:52:52 -0800 Subject: [PATCH 10/15] 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, '') From 1bfa61ded344d78489610c241b13f6681926c8fa Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 29 Jan 2026 01:52:50 -0800 Subject: [PATCH 11/15] refactor(ai-completion): Restructure stop sequences and context handling --- .../script/src/ai-completion/constants.js | 56 +++++-------------- .../src/ai-completion/utils/modelUtils.js | 56 +++++++++++-------- 2 files changed, 47 insertions(+), 65 deletions(-) diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index e38ec7c364..3f9d18df8f 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -117,54 +117,28 @@ export const MODEL_COMMON_CONFIG = { } } -/** - * 通用停止符配置(JS/TS) - */ -export const STOP_SEQUENCES = [ - // 通用停止符 - '\n\n', - '```', - - // JS/TS 语言特性 - '\nfunction ', - '\nclass ', - '\nconst ', - '\nlet ', - '\nvar ', - '\nexport ', - '\nimport ', - '\ninterface ', - '\ntype ', - '\nenum ', - - // 注释边界 - '\n//', - '\n/*', - - // 代码块边界 - '\n}', - '\n};' -] +// 停止符配置(API 限制:最多 16 个) +export const STOP_SEQUENCES = { + CORE: ['\n\n', '```'], + NEW_SCOPE: ['\nfunction ', '\nclass ', '\nexport ', '\nimport '], + BLOCK_END: ['\n}', '\n};'] +} -/** - * FIM (Fill-In-the-Middle) 配置 - */ +// 上下文特定停止符 +export const CONTEXT_STOP_SEQUENCES = { + EXPRESSION: [';', ',', '\n)'], + COMMENT: ['\n\n', '*/'], + OBJECT: ['\n}', '\n};'], + FUNCTION: ['\n}', '\nfunction ', '\nreturn '] +} + +// FIM 标记配置 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: [] // 使用通用停止符即可 } } diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js index f93a72b9a9..52bbde7f1b 100644 --- a/packages/plugins/script/src/ai-completion/utils/modelUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -1,4 +1,4 @@ -import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, FIM_CONFIG } from '../constants.js' +import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, CONTEXT_STOP_SEQUENCES } from '../constants.js' /** * 检测模型类型 @@ -46,33 +46,41 @@ export function calculateTokens(cursorContext) { return limits.DEFAULT } -/** - * 获取动态停止符 - * @param {Object} cursorContext - 光标上下文 - * @param {string} modelType - 模型类型 - * @returns {string[]} 停止符数组 - */ -export function getStopSequences(cursorContext, modelType) { - // 基础停止符:通用停止符 - const stops = [...STOP_SEQUENCES] +// 获取动态停止符(最多 16 个) +export function getStopSequences(cursorContext, _modelType) { + const stops = [] - // Qwen 模型添加 FIM 标记 - if (modelType === 'qwen') { - stops.push(...FIM_CONFIG.FIM_MARKERS_STOPS) - } + // 核心停止符 + stops.push(...STOP_SEQUENCES.CORE) - if (!cursorContext) { - return stops + if (cursorContext) { + if (cursorContext.inBlockComment || cursorContext.inLineComment) { + stops.push(...CONTEXT_STOP_SEQUENCES.COMMENT) + } else if (cursorContext.needsExpression) { + stops.push(...CONTEXT_STOP_SEQUENCES.EXPRESSION) + stops.push(...STOP_SEQUENCES.NEW_SCOPE) + } else if (cursorContext.inObject) { + stops.push(...CONTEXT_STOP_SEQUENCES.OBJECT) + stops.push(...STOP_SEQUENCES.NEW_SCOPE) + } else if (cursorContext.inFunction) { + stops.push(...CONTEXT_STOP_SEQUENCES.FUNCTION) + stops.push(...STOP_SEQUENCES.BLOCK_END) + } else { + stops.push(...STOP_SEQUENCES.NEW_SCOPE) + stops.push(...STOP_SEQUENCES.BLOCK_END) + } + } else { + stops.push(...STOP_SEQUENCES.NEW_SCOPE) + stops.push(...STOP_SEQUENCES.BLOCK_END) } - // 根据上下文添加特定停止符 - 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) + const uniqueStops = [...new Set(stops)] + + if (uniqueStops.length > 16) { + // eslint-disable-next-line no-console + console.warn(`⚠️ 停止符超过限制: ${uniqueStops.length},截断到 16 个`) + return uniqueStops.slice(0, 16) } - return stops + return uniqueStops } From 3ebe1b838d282c2de83f427762913f5ec3651ee5 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 14 Apr 2026 19:12:49 -0700 Subject: [PATCH 12/15] feat: Enhance AI completion capabilities with new model support and improved context handling --- .../robot-setting/RobotSetting.vue | 120 +++++--- .../robot/src/composables/core/useConfig.ts | 78 +++-- .../robot/src/constants/model-config.ts | 32 +-- .../plugins/robot/src/types/setting.types.ts | 5 +- packages/plugins/script/src/Main.vue | 15 +- .../src/ai-completion/adapters/index.js | 54 ++-- .../builders/fimPromptBuilder.js | 50 ++-- .../builders/lowcodeContextBuilder.js | 272 +++++++++++------- .../ai-completion/builders/promptBuilder.js | 56 +--- .../script/src/ai-completion/constants.js | 10 +- .../src/ai-completion/prompts/templates.js | 243 +++++++--------- .../ai-completion/utils/completionUtils.js | 32 ++- .../src/ai-completion/utils/modelUtils.js | 29 +- .../script/test/ai-completion.test.mjs | 112 ++++++++ 14 files changed, 683 insertions(+), 425 deletions(-) create mode 100644 packages/plugins/script/test/ai-completion.test.mjs 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 515047ff55..0cf4a31b86 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,25 +51,11 @@ - - + >
@@ -186,8 +172,8 @@ const emit = defineEmits(['close']) const { robotSettingState, saveRobotSettingState, + getAllAvailableModels, getCompactModels, - getNonCodeCompletionModels, addCustomService, updateService, deleteService, @@ -200,6 +186,15 @@ const getModelValue = (serviceId: string, modelName: string) => { return serviceId && modelName ? `${serviceId}::${modelName}` : '' } +const parseModelValue = (value = '') => { + const [serviceId = '', modelName = ''] = value.split('::') + + return { + serviceId, + modelName + } +} + const state = reactive({ activeTab: 'model-selection', modelSelection: { @@ -210,9 +205,28 @@ const state = reactive({ editingService: undefined as ModelService | undefined }) -// 获取所有可用模型选项(排除代码补全专用模型) +const syncModelSelection = () => { + state.modelSelection.defaultModel = getModelValue( + robotSettingState.defaultModel.serviceId, + robotSettingState.defaultModel.modelName + ) + state.modelSelection.quickModel = getModelValue( + robotSettingState.quickModel.serviceId, + robotSettingState.quickModel.modelName + ) +} + +const notifyMissingApiKey = (service: ModelService) => { + useNotify({ + type: 'warning', + title: '未配置API Key', + message: `请先为 ${service.label} 配置API Key` + }) +} + +// 获取所有可用模型选项 const allModelOptions = computed(() => { - return getNonCodeCompletionModels().map((model) => ({ + return getAllAvailableModels().map((model) => ({ label: model.displayLabel, value: model.value, capabilities: model.capabilities @@ -221,14 +235,10 @@ const allModelOptions = computed(() => { // 获取快速模型选项 const compactModelOptions = computed(() => { - const models = getCompactModels().map((model) => ({ + return getCompactModels().map((model) => ({ label: model.displayLabel, - value: model.value, - capabilities: model.capabilities, - serviceName: model.serviceName + value: model.value })) - - return models.sort((a, b) => a.serviceName.localeCompare(b.serviceName, 'zh-CN')) }) // 获取当前选择的默认模型信息 @@ -249,16 +259,15 @@ const handleBack = () => { } const handleModelChange = () => { - const [defaultServiceId, defaultModelName] = state.modelSelection.defaultModel.split('::') + const { serviceId: defaultServiceId, modelName: defaultModelName } = parseModelValue( + state.modelSelection.defaultModel + ) // 检查API Key const defaultService = getServiceById(defaultServiceId) if (defaultService && !defaultService.apiKey && !defaultService.allowEmptyApiKey) { - useNotify({ - type: 'warning', - title: '未配置API Key', - message: `请先为 ${defaultService.label} 配置API Key` - }) + notifyMissingApiKey(defaultService) + syncModelSelection() state.activeTab = 'services' return } @@ -273,7 +282,16 @@ const handleModelChange = () => { } const handleCompactModelChange = () => { - const [quickServiceId = '', quickModelName = ''] = (state.modelSelection.quickModel || '').split('::') + const { serviceId: quickServiceId, modelName: quickModelName } = parseModelValue(state.modelSelection.quickModel) + const quickService = getServiceById(quickServiceId) + + if (quickService && !quickService.apiKey && !quickService.allowEmptyApiKey) { + notifyMissingApiKey(quickService) + syncModelSelection() + state.activeTab = 'services' + return + } + const updatedState = { quickModel: { serviceId: quickServiceId, @@ -289,22 +307,50 @@ const addService = () => { } const editService = (service: any) => { - state.editingService = JSON.parse(JSON.stringify(service)) as ModelService + state.editingService = JSON.parse(JSON.stringify(service)) state.showServiceDialog = true } const handleDeleteService = (serviceId: string) => { deleteService(serviceId) + + const shouldResetDefaultModel = robotSettingState.defaultModel.serviceId === serviceId + const shouldResetQuickModel = robotSettingState.quickModel.serviceId === serviceId + + if (shouldResetDefaultModel || shouldResetQuickModel) { + saveRobotSettingState({ + ...(shouldResetDefaultModel + ? { + defaultModel: { + serviceId: '', + modelName: '' + } + } + : {}), + ...(shouldResetQuickModel + ? { + quickModel: { + serviceId: '', + modelName: '' + } + } + : {}) + }) + } + + syncModelSelection() } -const handleServiceConfirm = (serviceData: Partial) => { +const handleServiceConfirm = async (serviceData: Partial) => { if (serviceData.id) { // 更新现有服务 - updateService(serviceData.id, serviceData) + await updateService(serviceData.id, serviceData) } else { // 添加新服务 - addCustomService(serviceData as any) + await addCustomService(serviceData as any) } + + syncModelSelection() } diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts index da8e174ec6..a47318c100 100644 --- a/packages/plugins/robot/src/composables/core/useConfig.ts +++ b/packages/plugins/robot/src/composables/core/useConfig.ts @@ -15,7 +15,13 @@ import { reactive, readonly } from 'vue' import { DEFAULT_LLM_MODELS } from '../../constants' import { getRobotServiceOptions } from '../../utils' import { ChatMode } from '../../types/mode.types' -import type { ModelConfig, ModelService, RobotSettings, SelectedModelInfo } from '../../types/setting.types' +import type { + CompletionProtocol, + ModelConfig, + ModelService, + RobotSettings, + SelectedModelInfo +} from '../../types/setting.types' import apiService from '../../services/api' const SETTING_STORAGE_KEY = 'tiny-engine-robot-settings' @@ -44,6 +50,48 @@ const getAIModelOptions = () => { return mergeAIModelOptions(DEFAULT_LLM_MODELS, customAIModels) // eslint-disable-line } +const inferCompletionProtocol = ({ + provider = '', + baseUrl = '', + modelName = '', + capabilities = {} +}: { + provider?: string + baseUrl?: string + modelName?: string + capabilities?: ModelConfig['capabilities'] +}): CompletionProtocol | null => { + if (capabilities?.completionProtocol) { + return capabilities.completionProtocol + } + + const normalizedProvider = provider.toLowerCase() + if (normalizedProvider === 'bailian') { + return 'qwen' + } + if (normalizedProvider === 'deepseek') { + return 'deepseek' + } + + const normalizedBaseUrl = baseUrl.toLowerCase() + if (normalizedBaseUrl.includes('dashscope.aliyuncs.com')) { + return 'qwen' + } + if (normalizedBaseUrl.includes('deepseek.com')) { + return 'deepseek' + } + + const normalizedModelName = modelName.toLowerCase() + if (normalizedModelName.includes('qwen')) { + return 'qwen' + } + if (normalizedModelName.includes('deepseek')) { + return 'deepseek' + } + + return null +} + // 初始化内置服务 const initBuiltInServices = (): ModelService[] => { return getAIModelOptions().map((service: any) => ({ @@ -120,7 +168,9 @@ const migrateOldSettings = (oldSettings: any): RobotSettings | null => { customService.models.push({ name: customizeModel.completeModel, label: customizeModel.completeModel, - capabilities: { compact: true } + capabilities: { + compact: true + } }) } services.push(customService) @@ -318,19 +368,9 @@ const getAllAvailableModels = () => { ) } -// 获取快速模型列表(包含 compact 或 codeCompletion 的模型) +// 获取快速模型列表 const getCompactModels = () => { - 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) + return getAllAvailableModels().filter((model) => model.capabilities?.compact) } const updateThinkingState = (value: boolean) => { @@ -430,6 +470,13 @@ const getSelectedQuickModelInfo = (): SelectedModelInfo => { (m) => m.name === robotSettingState.quickModel.modelName ) const { name = '', label = '', capabilities = {} } = currentModel || {} + const completionProtocol = + inferCompletionProtocol({ + provider: currentService?.provider, + baseUrl: currentService?.baseUrl, + modelName: name, + capabilities + }) || null const { models, ...service } = currentService ?? ({} as Partial) @@ -444,6 +491,7 @@ const getSelectedQuickModelInfo = (): SelectedModelInfo => { // 模型兼容字段 model: robotSettingState.quickModel.modelName, completeModel: robotSettingState.quickModel.modelName || '', + completionProtocol, // 服务兼容字段 baseUrl: currentService?.baseUrl || '', apiKey: currentService?.apiKey || '' @@ -466,8 +514,6 @@ 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 1e471cb54b..4458339325 100644 --- a/packages/plugins/robot/src/constants/model-config.ts +++ b/packages/plugins/robot/src/constants/model-config.ts @@ -57,7 +57,6 @@ export const DEFAULT_LLM_MODELS = [ name: 'qwen3-coder-plus', capabilities: { toolCalling: true, - codeCompletion: true, reasoning: reasoningExtraBody, jsonOutput: bailianJsonOutputExtraBody } @@ -82,24 +81,23 @@ export const DEFAULT_LLM_MODELS = [ } }, { - label: 'Qwen2.5 Coder编程模型-最快响应', - name: 'qwen-coder-turbo-latest', + label: 'Qwen Coder编程模型(Flash)', + name: 'qwen3-coder-flash', capabilities: { toolCalling: true, compact: true, - codeCompletion: true, jsonOutput: bailianJsonOutputExtraBody } }, { - label: 'Qwen2.5 Coder编程模型(32B)', - name: 'qwen2.5-coder-32b-instruct', - 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 } } ] }, @@ -122,16 +120,6 @@ 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 40420b08b3..8cca70e85d 100644 --- a/packages/plugins/robot/src/types/setting.types.ts +++ b/packages/plugins/robot/src/types/setting.types.ts @@ -14,6 +14,8 @@ * 模型配置接口 */ +export type CompletionProtocol = 'qwen' | 'deepseek' + export interface Capability { extraBody: { enable: Record | null @@ -30,7 +32,7 @@ export interface ModelConfig { vision?: boolean reasoning?: boolean | Capability compact?: boolean - codeCompletion?: boolean + completionProtocol?: CompletionProtocol jsonOutput?: boolean | Capability } } @@ -81,6 +83,7 @@ export type SelectedModelInfo = ModelConfig & { // 模型兼容字段 model?: string completeModel?: string + completionProtocol?: CompletionProtocol | null // 服务兼容字段 baseUrl?: string apiKey?: string diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index 836dde0926..9e5f2bd7a3 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -72,6 +72,7 @@ export default { type RequestHandler = NonNullable type TriggerMode = NonNullable let completion: CompletionRegistration | null = null + let completionAction: { dispose?: () => void } | null = null const panelState = reactive({ emitEvent: emit @@ -126,10 +127,13 @@ export default { if (aiCompletionEnabled) { try { - const monaco = monacoRef.value.getMonaco() - const editor = monacoRef.value.getEditor() + const monacoInstance = monacoRef.value.getMonaco() + const editorInstance = monacoRef.value.getEditor() - completion = registerCompletion(monaco, editor, { + completion?.deregister?.() + completionAction?.dispose?.() + + completion = registerCompletion(monacoInstance, editorInstance, { language: 'javascript', filename: 'page.js', maxContextLines: 50, @@ -145,11 +149,11 @@ export default { requestHandler: createCompletionHandler() as RequestHandler }) - monaco.editor.addEditorAction({ + completionAction = monacoInstance.editor.addEditorAction({ id: 'monacopilot.triggerCompletion', label: 'Complete Code', contextMenuGroupId: 'navigation', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Space], + keybindings: [monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyMod.Shift | monacoInstance.KeyCode.Space], run: () => { completion!.trigger() } @@ -166,6 +170,7 @@ export default { if (completion) { completion.deregister() } + completionAction?.dispose?.() ;(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 4f3c281028..08dcf72171 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -19,15 +19,28 @@ export function createCompletionHandler() { return async (params) => { try { // 1. 获取 AI 配置 - const { completeModel, apiKey, baseUrl } = getMetaApi(META_SERVICE.Robot).getSelectedQuickModelInfo() || {} + const { + completeModel, + apiKey, + baseUrl, + capabilities = {}, + service = null + } = getMetaApi(META_SERVICE.Robot).getSelectedQuickModelInfo() || {} - if (!completeModel || !apiKey || !baseUrl) { + if (!completeModel || !baseUrl) { return { completion: null, error: ERROR_MESSAGES.CONFIG_MISSING } } + if (!apiKey && !service?.allowEmptyApiKey) { + return { + completion: null, + error: ERROR_MESSAGES.API_KEY_MISSING + } + } + // 2. 提取代码上下文 const { textBeforeCursor = '', @@ -38,7 +51,7 @@ export function createCompletionHandler() { // 3. 构建低代码元数据和 prompt const lowcodeMetadata = buildLowcodeMetadata() - const { fileContent } = createSmartPrompt({ + const { fileContent, commentStatus, lowcodeContext } = createSmartPrompt({ textBeforeCursor, textAfterCursor, language, @@ -48,22 +61,24 @@ export function createCompletionHandler() { }) // 4. 检测模型类型并构建 FIM 参数 - const modelType = detectModelType(completeModel) + const modelType = detectModelType(completeModel, { + provider: service?.provider, + baseUrl, + capabilities + }) + + if (modelType === MODEL_CONFIG.UNKNOWN.TYPE) { + return { + completion: null, + error: ERROR_MESSAGES.UNSUPPORTED_MODEL + } + } // 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 + isComment: commentStatus.isComment, + lowcodeContext } // 6. 根据模型类型构建请求参数 @@ -109,9 +124,14 @@ export function createCompletionHandler() { // 7. 处理补全结果 if (completionText) { - completionText = completionText.trim() + completionText = cleanCompletion(completionText, modelType, cursorContext, textAfterCursor) - completionText = cleanCompletion(completionText, modelType, cursorContext) + if (!completionText) { + return { + completion: null, + error: ERROR_MESSAGES.NO_COMPLETION + } + } return { completion: completionText, diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js index b15c586e84..eba698a8bd 100644 --- a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js @@ -1,6 +1,5 @@ import { FIM_CONFIG } from '../constants.js' import { - SYSTEM_BASE_PROMPT, createCodeInstruction, createLowcodeInstruction, BLOCK_COMMENT_INSTRUCTION, @@ -69,32 +68,16 @@ export class FIMPromptBuilder { * @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 { - // 普通代码补全 specificInstruction = createCodeInstruction(language) } - instruction += this.convertToComments(specificInstruction) - instruction += '//\n' - instruction += '// ===== CODE CONTEXT STARTS BELOW =====\n' - instruction += '\n' - - return instruction + return `${this.convertToComments(specificInstruction)}\n\n` } /** @@ -171,9 +154,33 @@ export class FIMPromptBuilder { // 分析前缀最后几个字符 const prefixTrimmed = prefix.trimEnd() + const isObjectLiteralStart = () => { + if (!/{\s*$/.test(prefixTrimmed)) { + return false + } + + const beforeBrace = prefixTrimmed.slice(0, prefixTrimmed.lastIndexOf('{')).trimEnd() + return !beforeBrace || /[=(:,[]$/.test(beforeBrace) || /\breturn$/.test(beforeBrace) + } + + const isObjectPropertyContinuation = () => { + if (!/,\s*$/.test(prefixTrimmed)) { + return false + } + + const beforeComma = prefixTrimmed.slice(0, -1) + const openBraces = (beforeComma.match(/{/g) || []).length + const closeBraces = (beforeComma.match(/}/g) || []).length + return openBraces > closeBraces + } + // 检测是否在对象字面量中 + if (isObjectLiteralStart() || isObjectPropertyContinuation()) { + context.inObject = true + context.type = 'object-property' + } // 检测是否在表达式中 - if (/[=+\-*/%<>!&|,([]$/.test(prefixTrimmed)) { + else if (/[=+\-*/%<>!&|([]$/.test(prefixTrimmed) || /,\s*$/.test(prefixTrimmed)) { context.needsExpression = true context.type = 'expression' } @@ -182,11 +189,6 @@ export class FIMPromptBuilder { 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) diff --git a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js index 754be23d9e..923fa18a26 100644 --- a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -1,42 +1,75 @@ +const FACT_LIMITS = { + ITEMS: 20, + STORE_MEMBERS: 12, + SCHEMA_KEYS: 16 +} + +function limitList(items, max = FACT_LIMITS.ITEMS) { + if (!Array.isArray(items)) { + return [] + } + + return items.filter(Boolean).slice(0, max) +} + +function normalizeDescription(text) { + return typeof text === 'string' ? text.replace(/\s+/g, ' ').trim() : '' +} + +function getValueType(value) { + if (Array.isArray(value)) { + return 'array' + } + + if (value === null) { + return 'null' + } + + return typeof value +} + /** - * 格式化数据源信息 + * 格式化数据信息 * @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 }) - })) + return limitList( + dataSource + .filter((ds) => ds?.name) + .map((ds) => ({ + name: ds.name, + type: ds.type || 'unknown', + accessPath: `this.dataSourceMap.${ds.name}.load()`, + description: normalizeDescription(ds.description || `Data source: ${ds.name}`) + })) + ) } /** - * 从函数代码中提取函数签名 + * 从函数代码中提取参数列表 * @param {string} functionCode - 函数代码字符串 - * @returns {string} 函数签名 + * @returns {string} 参数列表 */ -function extractFunctionSignature(functionCode) { - if (!functionCode) return 'function()' +function extractFunctionParams(functionCode) { + if (!functionCode) return '' - // 匹配函数声明: function name(params) - const funcMatch = functionCode.match(/function\s+(\w+)?\s*\(([^)]*)\)/) + const funcMatch = functionCode.match(/function(?:\s+\w+)?\s*\(([^)]*)\)/) if (funcMatch) { - const name = funcMatch[1] || 'anonymous' - const params = funcMatch[2].trim() - return `function ${name}(${params})` + return funcMatch[1].trim() } - // 匹配箭头函数: (params) => 或 params => const arrowMatch = functionCode.match(/(?:\(([^)]*)\)|(\w+))\s*=>/) if (arrowMatch) { - const params = arrowMatch[1] || arrowMatch[2] || '' - return `(${params}) => {}` + return (arrowMatch[1] || arrowMatch[2] || '').trim() } - return 'function()' + return '' +} + +function createCallableAccess(prefix, name, functionCode) { + const params = extractFunctionParams(functionCode) + return `${prefix}${name}(${params})` } /** @@ -45,32 +78,46 @@ function extractFunctionSignature(functionCode) { * @returns {Array} 格式化后的工具类 */ function formatUtils(utils) { - return utils.map((util) => { - const formatted = { - name: util.name, - type: util.type || 'function' - } + return limitList( + utils + .filter((util) => util?.name) + .map((util) => { + const formatted = { + name: util.name, + type: util.type || 'function', + accessPath: `this.utils.${util.name}` + } - // 处理 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 === 'npm' && util.content) { + formatted.package = util.content.package + formatted.description = `npm utility 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}` - } - } + if (util.type === 'function' && util.content?.type === 'JSFunction') { + formatted.signature = createCallableAccess('this.utils.', util.name, util.content.value) + formatted.description = `Utility function: ${util.name}` + } + + return formatted + }) + ) +} - return formatted - }) +/** + * 格式化 bridge 信息 + * @param {Array} bridge - bridge 数组 + * @returns {Array} 格式化后的 bridge 信息 + */ +function formatBridge(bridge) { + return limitList( + bridge + .filter((item) => item?.name) + .map((item) => ({ + name: item.name, + accessPath: `this.bridge.${item.name}`, + description: normalizeDescription(item.description || `Bridge API: ${item.name}`) + })) + ) } /** @@ -79,54 +126,48 @@ function formatUtils(utils) { * @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}` - })) + return limitList( + globalState + .filter((store) => store?.id) + .map((store) => ({ + id: store.id, + state: limitList(Object.keys(store.state || {}), FACT_LIMITS.STORE_MEMBERS), + getters: limitList(Object.keys(store.getters || {}), FACT_LIMITS.STORE_MEMBERS), + actions: limitList(Object.keys(store.actions || {}), FACT_LIMITS.STORE_MEMBERS), + description: `Pinia store: ${store.id}` + })) + ) } /** * 格式化本地状态 * @param {Object} state - 状态对象 - * @returns {Object} 格式化后的状态 + * @returns {Array} 格式化后的状态 */ 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 + return limitList( + Object.entries(state).map(([key, value]) => ({ + name: key, + accessPath: `this.state.${key}`, + type: getValueType(value) + })) + ) } /** * 格式化本地方法 * @param {Object} methods - 方法对象 - * @returns {Object} 格式化后的方法 + * @returns {Array} 格式化后的方法 */ 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 + return limitList( + Object.entries(methods).map(([key, value]) => ({ + name: key, + accessPath: `this.${key}`, + signature: value?.type === 'JSFunction' ? createCallableAccess('this.', key, value.value) : `this.${key}()`, + description: `Method: ${key}` + })) + ) } /** @@ -138,27 +179,30 @@ function formatCurrentSchema(schema) { if (!schema) return null const formatted = { - componentName: schema.componentName, - ...(schema.ref && { ref: schema.ref }) + componentName: schema.componentName || 'Unknown', + ...(schema.ref && { ref: schema.ref, refAccess: `this.$('${schema.ref}')` }) } - // 格式化 props if (schema.props) { - formatted.props = {} + const propKeys = [] + const eventKeys = [] + const dynamicPropKeys = [] + for (const [key, value] of Object.entries(schema.props)) { - // 识别事件处理器 if (key.startsWith('on')) { - formatted.props[key] = { - type: 'event', - isFunction: value && value.type === 'JSFunction' - } + eventKeys.push(key) } else { - formatted.props[key] = { - type: value && value.type ? value.type : 'static', - isDynamic: value && (value.type === 'JSExpression' || value.type === 'JSFunction') - } + propKeys.push(key) + } + + if (value && (value.type === 'JSExpression' || value.type === 'JSFunction')) { + dynamicPropKeys.push(key) } } + + formatted.props = limitList(propKeys, FACT_LIMITS.SCHEMA_KEYS) + formatted.events = limitList(eventKeys, FACT_LIMITS.SCHEMA_KEYS) + formatted.dynamicProps = limitList(dynamicPropKeys, FACT_LIMITS.SCHEMA_KEYS) } return formatted @@ -176,29 +220,37 @@ export function validateLowcodeContext(context) { return { valid: false, warnings: ['Context is null or undefined'] } } - // 检查必要字段 - const requiredFields = ['dataSource', 'utils', 'globalState', 'state', 'methods'] + const requiredFields = ['dataSource', 'utils', 'bridge', '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.bridge && !Array.isArray(context.bridge)) { + warnings.push('bridge should be an array') + } + if (context.globalState && !Array.isArray(context.globalState)) { warnings.push('globalState should be an array') } + if (context.state && !Array.isArray(context.state)) { + warnings.push('state should be an array') + } + + if (context.methods && !Array.isArray(context.methods)) { + warnings.push('methods should be an array') + } + return { valid: warnings.length === 0, warnings @@ -214,35 +266,34 @@ export function mergeLowcodeContexts(...contexts) { const merged = { dataSource: [], utils: [], + bridge: [], globalState: [], - state: {}, - methods: {}, + 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.bridge) { + merged.bridge = [...merged.bridge, ...context.bridge] + } if (context.globalState) { merged.globalState = [...merged.globalState, ...context.globalState] } - - // 合并对象类型 if (context.state) { - merged.state = { ...merged.state, ...context.state } + merged.state = [...merged.state, ...context.state] } if (context.methods) { - merged.methods = { ...merged.methods, ...context.methods } + merged.methods = [...merged.methods, ...context.methods] } - - // currentSchema 使用最后一个非空值 if (context.currentSchema) { merged.currentSchema = context.currentSchema } @@ -257,11 +308,20 @@ export function mergeLowcodeContexts(...contexts) { * @returns {Object} 格式化的低代码上下文 */ export function buildLowcodeContext(metadata) { - const { dataSource = [], utils = [], globalState = [], state = {}, methods = {}, currentSchema = null } = metadata + const { + dataSource = [], + utils = [], + bridge = [], + globalState = [], + state = {}, + methods = {}, + currentSchema = null + } = metadata return { dataSource: formatDataSources(dataSource), utils: formatUtils(utils), + bridge: formatBridge(bridge), globalState: formatGlobalState(globalState), state: formatState(state), methods: formatMethods(methods), diff --git a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js index cbcbb2f0d4..dbfb044a7e 100644 --- a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js @@ -1,10 +1,4 @@ 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' /** @@ -125,35 +119,10 @@ function buildMetaInfo(filename, language, codeContext, technologies) { 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 对象 + * @returns {{ fileContent: string, commentStatus: object, lowcodeContext: object | null }} Prompt 对象 */ export function createSmartPrompt(completionMetadata) { const { @@ -167,38 +136,27 @@ export function createSmartPrompt(completionMetadata) { const commentStatus = isInComment(textBeforeCursor) const codeContext = extractCodeContext(textBeforeCursor) + let lowcodeContext = null // 构建文件元信息(伪装成注释,让 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) + if (lowcodeMetadata) { + 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 + fileContent, + commentStatus, + lowcodeContext } } diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index 3f9d18df8f..35af15856b 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -37,11 +37,15 @@ export const DEEPSEEK_CONFIG = { export const MODEL_CONFIG = { QWEN: { TYPE: 'qwen', - KEYWORDS: ['qwen'] + KEYWORDS: ['qwen'], + PROVIDERS: ['bailian'], + BASE_URL_KEYWORDS: ['dashscope.aliyuncs.com'] }, DEEPSEEK: { TYPE: 'deepseek', - KEYWORDS: ['deepseek'] + KEYWORDS: ['deepseek'], + PROVIDERS: ['deepseek'], + BASE_URL_KEYWORDS: ['deepseek.com'] }, UNKNOWN: { TYPE: 'unknown', @@ -79,6 +83,8 @@ export const DEFAULTS = { */ export const ERROR_MESSAGES = { CONFIG_MISSING: 'AI 配置未设置(缺少 model/apiKey/baseUrl)', + API_KEY_MISSING: 'AI 配置未设置(缺少 API Key)', + UNSUPPORTED_MODEL: '当前快速模型未配置可用的补全协议,请在模型设置中指定协议或选择内置代码模型', NO_COMPLETION: '未收到有效的补全结果', REQUEST_FAILED: '请求失败', QWEN_API_ERROR: 'Qwen API 错误' diff --git a/packages/plugins/script/src/ai-completion/prompts/templates.js b/packages/plugins/script/src/ai-completion/prompts/templates.js index 0569d8bd6c..3e82e78eb0 100644 --- a/packages/plugins/script/src/ai-completion/prompts/templates.js +++ b/packages/plugins/script/src/ai-completion/prompts/templates.js @@ -1,21 +1,92 @@ +function createSection(title, lines = []) { + const validLines = lines.filter(Boolean) + + if (!validLines.length) { + return '' + } + + return `${title}:\n${validLines.map((line) => `- ${line}`).join('\n')}` +} + +function formatFactWithDescription(accessPath, description = '', extra = '') { + const details = [extra, description].filter(Boolean).join(' | ') + return details ? `${accessPath} // ${details}` : accessPath +} + +function formatStoreFacts(globalState) { + return globalState.map((store) => { + const members = [...store.state, ...store.getters].map((name) => `this.stores.${store.id}.${name}`) + const actions = store.actions.map((name) => `this.stores.${store.id}.${name}()`) + const facts = [...members, ...actions] + return facts.length ? facts.join(', ') : `this.stores.${store.id}` + }) +} + +function formatSchemaFacts(currentSchema) { + if (!currentSchema) { + return [] + } + + return [ + `component: ${currentSchema.componentName || 'Unknown'}`, + currentSchema.refAccess ? `ref: ${currentSchema.refAccess}` : '', + currentSchema.props?.length ? `props: ${currentSchema.props.join(', ')}` : '', + currentSchema.events?.length ? `events: ${currentSchema.events.join(', ')}` : '', + currentSchema.dynamicProps?.length ? `dynamic props: ${currentSchema.dynamicProps.join(', ')}` : '' + ] +} + +function buildLowcodeFacts(lowcodeContext = {}) { + const { + dataSource = [], + utils = [], + bridge = [], + globalState = [], + state = [], + methods = [], + currentSchema = null + } = lowcodeContext + + const sections = [ + createSection('Platform APIs', [ + 'this.props, this.emit, this.setState, this.route, this.history', + 'this.i18n, this.getLocale(), this.setLocale()', + "this.$('refName') for component refs", + 'this.dataSourceMap..load() for data loading' + ]), + createSection( + 'Data sources', + dataSource.map((item) => formatFactWithDescription(item.accessPath, item.description, item.type)) + ), + createSection( + 'Utilities', + utils.map((item) => formatFactWithDescription(item.signature || item.accessPath, item.description, item.package)) + ), + createSection( + 'Bridge APIs', + bridge.map((item) => formatFactWithDescription(item.accessPath, item.description)) + ), + createSection('Global stores', formatStoreFacts(globalState)), + createSection( + 'Local state', + state.map((item) => `${item.accessPath}: ${item.type}`) + ), + createSection( + 'Local methods', + methods.map((item) => item.signature || `${item.accessPath}()`) + ), + createSection('Current component', formatSchemaFacts(currentSchema)) + ].filter(Boolean) + + return sections.join('\n\n') +} + /** * 系统 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` +export const SYSTEM_BASE_PROMPT = `Return only the text to insert at the cursor. +Match the surrounding style and indentation. +Stay in the current scope and do not repeat existing code.` /** * 代码补全指令模板 @@ -23,91 +94,31 @@ CRITICAL RULES: * @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` + return `Complete the ${language} code at the cursor. +Return only the inserted code. +Keep the completion minimal and stay in the current scope.` } /** * 块注释补全指令(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 BLOCK_COMMENT_INSTRUCTION = `Complete the current JSDoc comment only. +Keep it concise, accurate, and aligned with the nearby code. +Do not generate code.` /** * 行注释补全指令 */ -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.` +export const LINE_COMMENT_INSTRUCTION = `Complete the current inline comment only. +Keep it brief and explain intent, not implementation details. +Do not generate code.` /** * 低代码平台上下文增强 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() {}' } }` +export const LOWCODE_CONTEXT_INSTRUCTION = `You are completing code inside a TinyEngine low-code page script. +Prefer the project symbols and runtime APIs listed below. +Use data sources via this.dataSourceMap..load().` /** * 创建带低代码上下文的指令 @@ -116,62 +127,10 @@ PROTOCOL CONVENTIONS: * @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}')` - } - } - } + const instruction = [createCodeInstruction(language), LOWCODE_CONTEXT_INSTRUCTION].join('\n') + const facts = buildLowcodeFacts(lowcodeContext) - return instruction + return facts ? `${instruction}\n\n${facts}` : instruction } /** diff --git a/packages/plugins/script/src/ai-completion/utils/completionUtils.js b/packages/plugins/script/src/ai-completion/utils/completionUtils.js index 80e3b634e1..7e008352e3 100644 --- a/packages/plugins/script/src/ai-completion/utils/completionUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js @@ -1,18 +1,41 @@ import { useResource, useCanvas } from '@opentiny/tiny-engine-meta-register' import { MODEL_COMMON_CONFIG } from '../constants.js' +function trimSuffixOverlap(text, suffix = '') { + if (!text || !suffix) { + return text + } + + const candidates = [suffix, suffix.trimStart()].filter(Boolean) + let bestOverlap = 0 + + for (const candidate of candidates) { + const maxOverlap = Math.min(text.length, candidate.length) + + for (let size = maxOverlap; size > bestOverlap; size--) { + if (text.endsWith(candidate.slice(0, size))) { + bestOverlap = size + break + } + } + } + + return bestOverlap > 0 ? text.slice(0, -bestOverlap) : text +} + /** * 构建低代码元数据 * @returns {Object} 低代码元数据 */ export function buildLowcodeMetadata() { - const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState || {} + const { dataSource = [], utils = [], bridge = [], globalState = [] } = useResource().appSchemaState || {} const { state: pageState = {}, methods = {} } = useCanvas().getPageSchema() || {} const currentSchema = useCanvas().getCurrentSchema() return { dataSource, utils, + bridge, globalState, state: pageState, methods, @@ -25,9 +48,10 @@ export function buildLowcodeMetadata() { * @param {string} text - 原始补全文本 * @param {string} modelType - 模型类型 * @param {Object} cursorContext - 光标上下文信息(可选) + * @param {string} suffix - 光标后的原始文本 * @returns {string} 清理后的文本 */ -export function cleanCompletion(text, modelType, cursorContext = null) { +export function cleanCompletion(text, modelType, cursorContext = null, suffix = '') { if (!text) return text let cleaned = text @@ -81,5 +105,7 @@ export function cleanCompletion(text, modelType, cursorContext = null) { cleaned = lines.slice(0, cutoffIndex).join('\n') } - return cleaned + cleaned = trimSuffixOverlap(cleaned, suffix) + + return cleaned.trim() } diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js index 52bbde7f1b..a02cff9584 100644 --- a/packages/plugins/script/src/ai-completion/utils/modelUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -3,11 +3,38 @@ import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, CONTEXT_STOP_SEQUENC /** * 检测模型类型 * @param {string} modelName - 模型名称 + * @param {Object} options - 额外上下文 + * @param {string} options.provider - 服务 provider + * @param {string} options.baseUrl - 服务 baseUrl + * @param {Object} options.capabilities - 模型能力 * @returns {'qwen' | 'deepseek' | 'unknown'} 模型类型 */ -export function detectModelType(modelName) { +export function detectModelType(modelName, options = {}) { if (!modelName) return MODEL_CONFIG.UNKNOWN.TYPE + const { provider = '', baseUrl = '', capabilities = {} } = options + if (capabilities?.completionProtocol) { + return capabilities.completionProtocol + } + + const lowerProvider = provider.toLowerCase() + if (MODEL_CONFIG.QWEN.PROVIDERS.some((item) => item === lowerProvider)) { + return MODEL_CONFIG.QWEN.TYPE + } + + if (MODEL_CONFIG.DEEPSEEK.PROVIDERS.some((item) => item === lowerProvider)) { + return MODEL_CONFIG.DEEPSEEK.TYPE + } + + const lowerBaseUrl = baseUrl.toLowerCase() + if (MODEL_CONFIG.QWEN.BASE_URL_KEYWORDS.some((keyword) => lowerBaseUrl.includes(keyword))) { + return MODEL_CONFIG.QWEN.TYPE + } + + if (MODEL_CONFIG.DEEPSEEK.BASE_URL_KEYWORDS.some((keyword) => lowerBaseUrl.includes(keyword))) { + return MODEL_CONFIG.DEEPSEEK.TYPE + } + const lowerName = modelName.toLowerCase() if (MODEL_CONFIG.QWEN.KEYWORDS.some((keyword) => lowerName.includes(keyword))) { diff --git a/packages/plugins/script/test/ai-completion.test.mjs b/packages/plugins/script/test/ai-completion.test.mjs new file mode 100644 index 0000000000..bf3adfe466 --- /dev/null +++ b/packages/plugins/script/test/ai-completion.test.mjs @@ -0,0 +1,112 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { buildLowcodeContext } from '../src/ai-completion/builders/lowcodeContextBuilder.js' +import { FIMPromptBuilder } from '../src/ai-completion/builders/fimPromptBuilder.js' +import { createLowcodeInstruction } from '../src/ai-completion/prompts/templates.js' + +function createMetadata() { + return { + dataSource: [ + { + name: 'users', + type: 'list', + description: 'Load user records', + options: { + shouldNotLeak: true + } + } + ], + utils: [ + { + name: 'formatDate', + type: 'function', + content: { + type: 'JSFunction', + value: 'function formatDate(value, pattern) { return value }' + } + } + ], + bridge: [ + { + name: 'toast', + description: 'Show a toast message' + } + ], + globalState: [ + { + id: 'userStore', + state: { + token: '', + profile: {} + }, + getters: { + displayName: {} + }, + actions: { + fetchProfile: {} + } + } + ], + state: { + keyword: '', + rows: [] + }, + methods: { + searchUsers: { + type: 'JSFunction', + value: 'function searchUsers(keyword) { return keyword }' + } + }, + currentSchema: { + componentName: 'TinyGrid', + ref: 'gridRef', + props: { + data: { + type: 'JSExpression' + }, + pager: true, + onRowClick: { + type: 'JSFunction' + } + } + } + } +} + +test('buildLowcodeContext keeps compact runtime facts', () => { + const context = buildLowcodeContext(createMetadata()) + + assert.equal(context.dataSource[0].accessPath, 'this.dataSourceMap.users.load()') + assert.equal('options' in context.dataSource[0], false) + assert.equal(context.bridge[0].accessPath, 'this.bridge.toast') + assert.deepEqual(context.currentSchema.props, ['data', 'pager']) + assert.deepEqual(context.currentSchema.events, ['onRowClick']) +}) + +test('createLowcodeInstruction uses TinyEngine runtime access paths', () => { + const instruction = createLowcodeInstruction('javascript', buildLowcodeContext(createMetadata())) + + assert.match(instruction, /this\.dataSourceMap\.users\.load\(\)/) + assert.match(instruction, /this\.bridge\.toast/) + assert.match(instruction, /this\.searchUsers\(keyword\)/) + assert.doesNotMatch(instruction, /this\.dataSource\./) +}) + +test('FIM prompt no longer injects verbose banner comments', () => { + const builder = new FIMPromptBuilder({ + FIM: { + MAX_PREFIX_LINES: 100, + MAX_SUFFIX_LINES: 50 + } + }) + + const { prefix } = builder.buildFIMComponents('function demo() {\n [CURSOR]\n}\n', { + language: 'javascript', + lowcodeContext: buildLowcodeContext(createMetadata()) + }) + + assert.match(prefix, /this\.dataSourceMap\.users\.load\(\)/) + assert.doesNotMatch(prefix, /AI COMPLETION INSTRUCTIONS/) + assert.doesNotMatch(prefix, /CODE CONTEXT STARTS BELOW/) +}) From 47032268316bb5bc199dda67954df7db0236db94 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 14 Apr 2026 23:32:45 -0700 Subject: [PATCH 13/15] feat: add Qwen Coder Turbo model and enhance completion model handling --- .../robot-setting/RobotSetting.vue | 75 +++- .../robot/src/composables/core/useConfig.ts | 145 ++++++-- .../robot/src/constants/model-config.ts | 12 + .../plugins/robot/src/types/setting.types.ts | 1 + .../src/ai-completion/adapters/index.js | 2 +- .../builders/fimPromptBuilder.js | 22 +- .../builders/lowcodeContextBuilder.js | 320 +++++++++++++----- .../ai-completion/builders/promptBuilder.js | 38 +-- .../script/src/ai-completion/constants.js | 8 +- .../src/ai-completion/prompts/templates.js | 62 +++- .../ai-completion/utils/contextAnalysis.js | 299 ++++++++++++++++ .../src/ai-completion/utils/modelUtils.js | 34 +- .../script/test/ai-completion.test.mjs | 77 +++++ 13 files changed, 921 insertions(+), 174 deletions(-) create mode 100644 packages/plugins/script/src/ai-completion/utils/contextAnalysis.js 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 0cf4a31b86..ed1c368c2f 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 @@ -58,6 +58,27 @@ > + + + + +
服务: @@ -174,6 +195,7 @@ const { saveRobotSettingState, getAllAvailableModels, getCompactModels, + getCompletionModels, addCustomService, updateService, deleteService, @@ -199,7 +221,11 @@ const state = reactive({ activeTab: 'model-selection', modelSelection: { defaultModel: getModelValue(robotSettingState.defaultModel.serviceId, robotSettingState.defaultModel.modelName), - quickModel: getModelValue(robotSettingState.quickModel.serviceId, robotSettingState.quickModel.modelName) + quickModel: getModelValue(robotSettingState.quickModel.serviceId, robotSettingState.quickModel.modelName), + completionModel: getModelValue( + robotSettingState.completionModel.serviceId, + robotSettingState.completionModel.modelName + ) }, showServiceDialog: false, editingService: undefined as ModelService | undefined @@ -214,9 +240,13 @@ const syncModelSelection = () => { robotSettingState.quickModel.serviceId, robotSettingState.quickModel.modelName ) + state.modelSelection.completionModel = getModelValue( + robotSettingState.completionModel.serviceId, + robotSettingState.completionModel.modelName + ) } -const notifyMissingApiKey = (service: ModelService) => { +const notifyMissingApiKey = (service: any) => { useNotify({ type: 'warning', title: '未配置API Key', @@ -241,6 +271,13 @@ const compactModelOptions = computed(() => { })) }) +const completionModelOptions = computed(() => { + return getCompletionModels().map((model) => ({ + label: model.displayLabel, + value: model.value + })) +}) + // 获取当前选择的默认模型信息 const selectedDefaultModelInfo = computed(() => { const [serviceId] = state.modelSelection.defaultModel.split('::') @@ -301,12 +338,33 @@ const handleCompactModelChange = () => { saveRobotSettingState(updatedState) } +const handleCompletionModelChange = () => { + const { serviceId: completionServiceId, modelName: completionModelName } = parseModelValue( + state.modelSelection.completionModel + ) + const completionService = getServiceById(completionServiceId) + + if (completionService && !completionService.apiKey && !completionService.allowEmptyApiKey) { + notifyMissingApiKey(completionService) + syncModelSelection() + state.activeTab = 'services' + return + } + + saveRobotSettingState({ + completionModel: { + serviceId: completionServiceId, + modelName: completionModelName + } + }) +} + const addService = () => { state.editingService = undefined state.showServiceDialog = true } -const editService = (service: any) => { +const editService = (service: ModelService) => { state.editingService = JSON.parse(JSON.stringify(service)) state.showServiceDialog = true } @@ -316,8 +374,9 @@ const handleDeleteService = (serviceId: string) => { const shouldResetDefaultModel = robotSettingState.defaultModel.serviceId === serviceId const shouldResetQuickModel = robotSettingState.quickModel.serviceId === serviceId + const shouldResetCompletionModel = robotSettingState.completionModel.serviceId === serviceId - if (shouldResetDefaultModel || shouldResetQuickModel) { + if (shouldResetDefaultModel || shouldResetQuickModel || shouldResetCompletionModel) { saveRobotSettingState({ ...(shouldResetDefaultModel ? { @@ -327,6 +386,14 @@ const handleDeleteService = (serviceId: string) => { } } : {}), + ...(shouldResetCompletionModel + ? { + completionModel: { + serviceId: '', + modelName: '' + } + } + : {}), ...(shouldResetQuickModel ? { quickModel: { diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts index a47318c100..a9dfede186 100644 --- a/packages/plugins/robot/src/composables/core/useConfig.ts +++ b/packages/plugins/robot/src/composables/core/useConfig.ts @@ -25,7 +25,7 @@ import type { import apiService from '../../services/api' const SETTING_STORAGE_KEY = 'tiny-engine-robot-settings' -const SETTING_VERSION = 2 // 新版本号 +const SETTING_VERSION = 3 // 新版本号 const robotSettingState = reactive({ version: SETTING_VERSION, @@ -37,6 +37,10 @@ const robotSettingState = reactive({ serviceId: '', modelName: '' }, + completionModel: { + serviceId: '', + modelName: '' + }, services: [], chatMode: ChatMode.Agent, enableThinking: false @@ -50,6 +54,23 @@ const getAIModelOptions = () => { return mergeAIModelOptions(DEFAULT_LLM_MODELS, customAIModels) // eslint-disable-line } +const QWEN_FIM_MODEL_PATTERNS = [/^qwen-coder-turbo(?:-latest|-0919)?$/, /^qwen2\.5-coder-(7|14|32)b-instruct$/] +const DEEPSEEK_FIM_MODELS = new Set(['deepseek-chat']) + +const matchesCompletionModel = (modelName = '', patterns: RegExp[] = [], exactModels: Set = new Set()) => { + const normalizedModelName = modelName.toLowerCase() + + if (!normalizedModelName) { + return false + } + + if (exactModels.has(normalizedModelName)) { + return true + } + + return patterns.some((pattern) => pattern.test(normalizedModelName)) +} + const inferCompletionProtocol = ({ provider = '', baseUrl = '', @@ -65,34 +86,65 @@ const inferCompletionProtocol = ({ return capabilities.completionProtocol } - const normalizedProvider = provider.toLowerCase() - if (normalizedProvider === 'bailian') { + if (matchesCompletionModel(modelName, QWEN_FIM_MODEL_PATTERNS)) { return 'qwen' } - if (normalizedProvider === 'deepseek') { + + if (matchesCompletionModel(modelName, [], DEEPSEEK_FIM_MODELS)) { return 'deepseek' } + const normalizedProvider = provider.toLowerCase() const normalizedBaseUrl = baseUrl.toLowerCase() - if (normalizedBaseUrl.includes('dashscope.aliyuncs.com')) { - return 'qwen' - } - if (normalizedBaseUrl.includes('deepseek.com')) { + if ( + normalizedProvider === 'deepseek' && + normalizedBaseUrl.includes('deepseek.com') && + matchesCompletionModel(modelName, [], DEEPSEEK_FIM_MODELS) + ) { return 'deepseek' } - const normalizedModelName = modelName.toLowerCase() - if (normalizedModelName.includes('qwen')) { - return 'qwen' + return null +} + +// 初始化内置服务 +const isCompletionCapableModel = (service: ModelService | undefined, model: ModelConfig | undefined) => { + if (!service || !model) { + return false } - if (normalizedModelName.includes('deepseek')) { - return 'deepseek' + + return ( + inferCompletionProtocol({ + provider: service.provider, + baseUrl: service.baseUrl, + modelName: model.name, + capabilities: model.capabilities + }) !== null + ) +} + +const getFallbackCompletionModel = (services: ModelService[], preferredServiceId = '') => { + const preferredService = services.find((service) => service.id === preferredServiceId) + const orderedServices = preferredService + ? [preferredService, ...services.filter((service) => service.id !== preferredServiceId)] + : services + + for (const service of orderedServices) { + const supportedModel = service.models.find((model) => isCompletionCapableModel(service, model)) + if (supportedModel) { + return { + serviceId: service.id, + modelName: supportedModel.name + } + } } - return null + return { + serviceId: '', + modelName: '' + } } -// 初始化内置服务 const initBuiltInServices = (): ModelService[] => { return getAIModelOptions().map((service: any) => ({ id: service.provider, @@ -122,6 +174,7 @@ const initDefaultSettings = (): RobotSettings => { serviceId: '', modelName: '' }, + completionModel: getFallbackCompletionModel(builtInServices), services: builtInServices, chatMode: ChatMode.Agent, enableThinking: false @@ -181,16 +234,23 @@ const migrateOldSettings = (oldSettings: any): RobotSettings | null => { const defaultServiceId = activeName === 'existingModels' ? services.find((s) => s.baseUrl === selectedModel?.baseUrl)?.id : '' + const quickModel = { + serviceId: defaultServiceId || '', + modelName: selectedModel?.completeModel || '' + } + const completionModel = + quickModel.modelName && quickModel.serviceId + ? getFallbackCompletionModel(services, quickModel.serviceId) + : getFallbackCompletionModel(services, defaultServiceId || services[0]?.id || '') + return { version: SETTING_VERSION, defaultModel: { serviceId: defaultServiceId || services[0]?.id || '', modelName: selectedModel?.model || services[0]?.models[0]?.name || '' }, - quickModel: { - serviceId: defaultServiceId || '', - modelName: selectedModel?.completeModel || '' - }, + quickModel, + completionModel, services, chatMode: chatMode || ChatMode.Agent, enableThinking: enableThinking || false @@ -373,6 +433,22 @@ const getCompactModels = () => { return getAllAvailableModels().filter((model) => model.capabilities?.compact) } +const getCompletionModels = () => { + return robotSettingState.services.flatMap((service) => + service.models + .filter((model) => isCompletionCapableModel(service, model)) + .map((model) => ({ + serviceId: service.id, + serviceName: service.label, + modelName: model.name, + modelLabel: model.label, + capabilities: model.capabilities || {}, + displayLabel: `${service.label} - ${model.label}`, + value: `${service.id}::${model.name}` + })) + ) +} + const updateThinkingState = (value: boolean) => { robotSettingState.enableThinking = value saveRobotSettingState({ enableThinking: robotSettingState.enableThinking }) @@ -498,6 +574,35 @@ const getSelectedQuickModelInfo = (): SelectedModelInfo => { } } +const getSelectedCompletionModelInfo = (): SelectedModelInfo => { + const currentService: ModelService | undefined = getServiceById(robotSettingState.completionModel.serviceId) + const currentModel: ModelConfig | undefined = currentService?.models.find( + (m) => m.name === robotSettingState.completionModel.modelName + ) + const { name = '', label = '', capabilities = {} } = currentModel || {} + const completionProtocol = + inferCompletionProtocol({ + provider: currentService?.provider, + baseUrl: currentService?.baseUrl, + modelName: name, + capabilities + }) || null + + const { models, ...service } = currentService ?? ({} as Partial) + + return { + name, + label, + capabilities, + service: (currentService ? service : null) as ModelService | null, + model: robotSettingState.completionModel.modelName, + completeModel: robotSettingState.completionModel.modelName || '', + completionProtocol, + baseUrl: currentService?.baseUrl || '', + apiKey: currentService?.apiKey || '' + } +} + export default () => { return { // 配置状态 @@ -514,8 +619,10 @@ export default () => { getModelCapabilities, getAllAvailableModels, getCompactModels, + getCompletionModels, getSelectedModelInfo, // 对话模型信息 getSelectedQuickModelInfo, // 快速模型信息 + getSelectedCompletionModelInfo, // 代码补全模型信息 // 服务管理 addCustomService, diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts index 4458339325..b131a2f3f2 100644 --- a/packages/plugins/robot/src/constants/model-config.ts +++ b/packages/plugins/robot/src/constants/model-config.ts @@ -89,6 +89,16 @@ export const DEFAULT_LLM_MODELS = [ jsonOutput: bailianJsonOutputExtraBody } }, + { + label: 'Qwen Coder编程模型(Turbo)', + name: 'qwen-coder-turbo-latest', + capabilities: { + toolCalling: true, + compact: true, + jsonOutput: bailianJsonOutputExtraBody, + completionProtocol: 'qwen' + } + }, { label: 'Qwen3(14b)', name: 'qwen3-14b', @@ -112,6 +122,8 @@ export const DEFAULT_LLM_MODELS = [ name: 'deepseek-chat', capabilities: { toolCalling: true, + compact: true, + completionProtocol: 'deepseek', reasoning: { extraBody: { enable: { model: 'deepseek-reasoner' }, diff --git a/packages/plugins/robot/src/types/setting.types.ts b/packages/plugins/robot/src/types/setting.types.ts index 8cca70e85d..5ab0017d45 100644 --- a/packages/plugins/robot/src/types/setting.types.ts +++ b/packages/plugins/robot/src/types/setting.types.ts @@ -66,6 +66,7 @@ export interface RobotSettings { version?: number defaultModel: ModelSelection quickModel: ModelSelection + completionModel: ModelSelection services: ModelService[] chatMode: string enableThinking: boolean diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index 08dcf72171..6c88822edf 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -25,7 +25,7 @@ export function createCompletionHandler() { baseUrl, capabilities = {}, service = null - } = getMetaApi(META_SERVICE.Robot).getSelectedQuickModelInfo() || {} + } = getMetaApi(META_SERVICE.Robot).getSelectedCompletionModelInfo() || {} if (!completeModel || !baseUrl) { return { diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js index eba698a8bd..8216cbf2ab 100644 --- a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js @@ -5,6 +5,7 @@ import { BLOCK_COMMENT_INSTRUCTION, LINE_COMMENT_INSTRUCTION } from '../prompts/templates.js' +import { getCommentState, getOpenScopeContext, sanitizeStructuralText } from '../utils/contextAnalysis.js' /** * FIM (Fill-In-the-Middle) Prompt 构建器 @@ -135,25 +136,22 @@ export class FIMPromptBuilder { needsStatement: false } - // 检测是否在注释中 - const lastBlockStart = prefix.lastIndexOf('/*') - const lastBlockEnd = prefix.lastIndexOf('*/') - if (lastBlockStart > lastBlockEnd) { + const commentState = getCommentState(prefix) + if (commentState.inBlockComment) { context.inBlockComment = true context.type = 'block-comment' return context } - const lastLineBreak = prefix.lastIndexOf('\n') - const currentLine = prefix.substring(lastLineBreak + 1) - if (currentLine.trim().startsWith('//')) { + if (commentState.inLineComment) { context.inLineComment = true context.type = 'line-comment' return context } // 分析前缀最后几个字符 - const prefixTrimmed = prefix.trimEnd() + const sanitizedPrefix = sanitizeStructuralText(prefix) + const prefixTrimmed = sanitizedPrefix.trimEnd() const isObjectLiteralStart = () => { if (!/{\s*$/.test(prefixTrimmed)) { return false @@ -191,11 +189,9 @@ export class FIMPromptBuilder { } // 检测作用域 - 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 + const openScope = getOpenScopeContext(prefix) + context.inFunction = Boolean(openScope.functionName) + context.inClass = Boolean(openScope.className) return context } diff --git a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js index 923fa18a26..53aeea43ad 100644 --- a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -1,7 +1,8 @@ const FACT_LIMITS = { ITEMS: 20, STORE_MEMBERS: 12, - SCHEMA_KEYS: 16 + SCHEMA_KEYS: 16, + HINT_TOKENS: 80 } function limitList(items, max = FACT_LIMITS.ITEMS) { @@ -12,6 +13,94 @@ function limitList(items, max = FACT_LIMITS.ITEMS) { return items.filter(Boolean).slice(0, max) } +function buildHintContext(hintText = '') { + if (typeof hintText !== 'string' || !hintText.trim()) { + return { + recentTokens: [], + tokenSet: new Set(), + currentToken: '' + } + } + + const identifiers = hintText.match(/[A-Za-z_$][\w$]*/g) || [] + const recentTokens = identifiers.slice(-FACT_LIMITS.HINT_TOKENS).map((token) => token.toLowerCase()) + const currentTokenMatch = hintText.match(/([A-Za-z_$][\w$]*)$/) + + return { + recentTokens, + tokenSet: new Set(recentTokens), + currentToken: currentTokenMatch?.[1]?.toLowerCase() || '' + } +} + +function scoreByHint(item, hintContext, getLookupText) { + if (!hintContext.recentTokens.length && !hintContext.currentToken) { + return 0 + } + + const primaryName = String(item?.name || item?.id || '').toLowerCase() + const lookupText = String(getLookupText(item) || primaryName).toLowerCase() + let score = 0 + + if (hintContext.currentToken) { + if (primaryName.startsWith(hintContext.currentToken)) { + score += 8 + } else if (lookupText.includes(hintContext.currentToken)) { + score += 4 + } + } + + if (primaryName && hintContext.tokenSet.has(primaryName)) { + score += 6 + } + + for (const token of hintContext.recentTokens) { + if (!token || token === hintContext.currentToken) { + continue + } + + if (primaryName && primaryName.startsWith(token)) { + score += 3 + break + } + + if (lookupText.includes(token)) { + score += 1 + } + } + + return score +} + +function prioritizeItems(items, max = FACT_LIMITS.ITEMS, hintContext, getLookupText = () => '') { + const filteredItems = Array.isArray(items) ? items.filter(Boolean) : [] + + if (!filteredItems.length) { + return { + items: [], + omitted: 0 + } + } + + const rankedItems = filteredItems.map((item, index) => ({ + item, + index, + score: scoreByHint(item, hintContext, getLookupText) + })) + + rankedItems.sort((left, right) => right.score - left.score || left.index - right.index) + + const selectedItems = rankedItems + .slice(0, max) + .sort((left, right) => left.index - right.index) + .map((entry) => entry.item) + + return { + items: selectedItems, + omitted: Math.max(filteredItems.length - selectedItems.length, 0) + } +} + function normalizeDescription(text) { return typeof text === 'string' ? text.replace(/\s+/g, ' ').trim() : '' } @@ -33,16 +122,18 @@ function getValueType(value) { * @param {Array} dataSource - 数据源数组 * @returns {Array} 格式化后的数据源 */ -function formatDataSources(dataSource) { - return limitList( - dataSource - .filter((ds) => ds?.name) - .map((ds) => ({ - name: ds.name, - type: ds.type || 'unknown', - accessPath: `this.dataSourceMap.${ds.name}.load()`, - description: normalizeDescription(ds.description || `Data source: ${ds.name}`) - })) +function formatDataSources(dataSource, hintContext) { + const candidates = dataSource + .filter((ds) => ds?.name) + .map((ds) => ({ + name: ds.name, + type: ds.type || 'unknown', + accessPath: `this.dataSourceMap.${ds.name}.load()`, + description: normalizeDescription(ds.description || `Data source: ${ds.name}`) + })) + + return prioritizeItems(candidates, FACT_LIMITS.ITEMS, hintContext, (item) => + [item.name, item.accessPath, item.description].filter(Boolean).join(' ') ) } @@ -77,29 +168,31 @@ function createCallableAccess(prefix, name, functionCode) { * @param {Array} utils - 工具类数组 * @returns {Array} 格式化后的工具类 */ -function formatUtils(utils) { - return limitList( - utils - .filter((util) => util?.name) - .map((util) => { - const formatted = { - name: util.name, - type: util.type || 'function', - accessPath: `this.utils.${util.name}` - } - - if (util.type === 'npm' && util.content) { - formatted.package = util.content.package - formatted.description = `npm utility from ${util.content.package}` - } - - if (util.type === 'function' && util.content?.type === 'JSFunction') { - formatted.signature = createCallableAccess('this.utils.', util.name, util.content.value) - formatted.description = `Utility function: ${util.name}` - } - - return formatted - }) +function formatUtils(utils, hintContext) { + const candidates = utils + .filter((util) => util?.name) + .map((util) => { + const formatted = { + name: util.name, + type: util.type || 'function', + accessPath: `this.utils.${util.name}` + } + + if (util.type === 'npm' && util.content) { + formatted.package = util.content.package + formatted.description = `npm utility from ${util.content.package}` + } + + if (util.type === 'function' && util.content?.type === 'JSFunction') { + formatted.signature = createCallableAccess('this.utils.', util.name, util.content.value) + formatted.description = `Utility function: ${util.name}` + } + + return formatted + }) + + return prioritizeItems(candidates, FACT_LIMITS.ITEMS, hintContext, (item) => + [item.name, item.signature, item.accessPath, item.package, item.description].filter(Boolean).join(' ') ) } @@ -108,15 +201,17 @@ function formatUtils(utils) { * @param {Array} bridge - bridge 数组 * @returns {Array} 格式化后的 bridge 信息 */ -function formatBridge(bridge) { - return limitList( - bridge - .filter((item) => item?.name) - .map((item) => ({ - name: item.name, - accessPath: `this.bridge.${item.name}`, - description: normalizeDescription(item.description || `Bridge API: ${item.name}`) - })) +function formatBridge(bridge, hintContext) { + const candidates = bridge + .filter((item) => item?.name) + .map((item) => ({ + name: item.name, + accessPath: `this.bridge.${item.name}`, + description: normalizeDescription(item.description || `Bridge API: ${item.name}`) + })) + + return prioritizeItems(candidates, FACT_LIMITS.ITEMS, hintContext, (item) => + [item.name, item.accessPath, item.description].filter(Boolean).join(' ') ) } @@ -125,17 +220,19 @@ function formatBridge(bridge) { * @param {Array} globalState - 全局状态数组 * @returns {Array} 格式化后的全局状态 */ -function formatGlobalState(globalState) { - return limitList( - globalState - .filter((store) => store?.id) - .map((store) => ({ - id: store.id, - state: limitList(Object.keys(store.state || {}), FACT_LIMITS.STORE_MEMBERS), - getters: limitList(Object.keys(store.getters || {}), FACT_LIMITS.STORE_MEMBERS), - actions: limitList(Object.keys(store.actions || {}), FACT_LIMITS.STORE_MEMBERS), - description: `Pinia store: ${store.id}` - })) +function formatGlobalState(globalState, hintContext) { + const candidates = globalState + .filter((store) => store?.id) + .map((store) => ({ + id: store.id, + state: limitList(Object.keys(store.state || {}), FACT_LIMITS.STORE_MEMBERS), + getters: limitList(Object.keys(store.getters || {}), FACT_LIMITS.STORE_MEMBERS), + actions: limitList(Object.keys(store.actions || {}), FACT_LIMITS.STORE_MEMBERS), + description: `Pinia store: ${store.id}` + })) + + return prioritizeItems(candidates, FACT_LIMITS.ITEMS, hintContext, (item) => + [item.id, ...item.state, ...item.getters, ...item.actions].filter(Boolean).join(' ') ) } @@ -144,13 +241,15 @@ function formatGlobalState(globalState) { * @param {Object} state - 状态对象 * @returns {Array} 格式化后的状态 */ -function formatState(state) { - return limitList( - Object.entries(state).map(([key, value]) => ({ - name: key, - accessPath: `this.state.${key}`, - type: getValueType(value) - })) +function formatState(state, hintContext) { + const candidates = Object.entries(state).map(([key, value]) => ({ + name: key, + accessPath: `this.state.${key}`, + type: getValueType(value) + })) + + return prioritizeItems(candidates, FACT_LIMITS.ITEMS, hintContext, (item) => + [item.name, item.accessPath, item.type].filter(Boolean).join(' ') ) } @@ -159,30 +258,57 @@ function formatState(state) { * @param {Object} methods - 方法对象 * @returns {Array} 格式化后的方法 */ -function formatMethods(methods) { - return limitList( - Object.entries(methods).map(([key, value]) => ({ - name: key, - accessPath: `this.${key}`, - signature: value?.type === 'JSFunction' ? createCallableAccess('this.', key, value.value) : `this.${key}()`, - description: `Method: ${key}` - })) +function formatMethods(methods, hintContext) { + const candidates = Object.entries(methods).map(([key, value]) => ({ + name: key, + accessPath: `this.${key}`, + signature: value?.type === 'JSFunction' ? createCallableAccess('this.', key, value.value) : `this.${key}()`, + description: `Method: ${key}` + })) + + return prioritizeItems(candidates, FACT_LIMITS.ITEMS, hintContext, (item) => + [item.name, item.signature, item.accessPath, item.description].filter(Boolean).join(' ') ) } +function prioritizeSchemaKeys(keys, max, hintContext) { + const candidates = keys.map((name) => ({ name })) + const { items, omitted } = prioritizeItems(candidates, max, hintContext, (item) => item.name) + + return { + items: items.map((item) => item.name), + omitted + } +} + /** * 格式化当前组件 schema * @param {Object} schema - 组件 schema * @returns {Object|null} 格式化后的 schema */ -function formatCurrentSchema(schema) { - if (!schema) return null +function formatCurrentSchema(schema, hintContext) { + if (!schema) { + return { + schema: null, + truncated: { + props: 0, + events: 0, + dynamicProps: 0 + } + } + } const formatted = { componentName: schema.componentName || 'Unknown', ...(schema.ref && { ref: schema.ref, refAccess: `this.$('${schema.ref}')` }) } + const truncated = { + props: 0, + events: 0, + dynamicProps: 0 + } + if (schema.props) { const propKeys = [] const eventKeys = [] @@ -200,12 +326,23 @@ function formatCurrentSchema(schema) { } } - formatted.props = limitList(propKeys, FACT_LIMITS.SCHEMA_KEYS) - formatted.events = limitList(eventKeys, FACT_LIMITS.SCHEMA_KEYS) - formatted.dynamicProps = limitList(dynamicPropKeys, FACT_LIMITS.SCHEMA_KEYS) + const prioritizedProps = prioritizeSchemaKeys(propKeys, FACT_LIMITS.SCHEMA_KEYS, hintContext) + const prioritizedEvents = prioritizeSchemaKeys(eventKeys, FACT_LIMITS.SCHEMA_KEYS, hintContext) + const prioritizedDynamicProps = prioritizeSchemaKeys(dynamicPropKeys, FACT_LIMITS.SCHEMA_KEYS, hintContext) + + formatted.props = prioritizedProps.items + formatted.events = prioritizedEvents.items + formatted.dynamicProps = prioritizedDynamicProps.items + + truncated.props = prioritizedProps.omitted + truncated.events = prioritizedEvents.omitted + truncated.dynamicProps = prioritizedDynamicProps.omitted } - return formatted + return { + schema: formatted, + truncated + } } /** @@ -307,7 +444,7 @@ export function mergeLowcodeContexts(...contexts) { * @param {Object} metadata - 低代码平台元数据 * @returns {Object} 格式化的低代码上下文 */ -export function buildLowcodeContext(metadata) { +export function buildLowcodeContext(metadata, options = {}) { const { dataSource = [], utils = [], @@ -317,14 +454,31 @@ export function buildLowcodeContext(metadata) { methods = {}, currentSchema = null } = metadata + const hintContext = buildHintContext(options.hintText) + const formattedDataSource = formatDataSources(dataSource, hintContext) + const formattedUtils = formatUtils(utils, hintContext) + const formattedBridge = formatBridge(bridge, hintContext) + const formattedGlobalState = formatGlobalState(globalState, hintContext) + const formattedState = formatState(state, hintContext) + const formattedMethods = formatMethods(methods, hintContext) + const formattedSchema = formatCurrentSchema(currentSchema, hintContext) return { - dataSource: formatDataSources(dataSource), - utils: formatUtils(utils), - bridge: formatBridge(bridge), - globalState: formatGlobalState(globalState), - state: formatState(state), - methods: formatMethods(methods), - currentSchema: formatCurrentSchema(currentSchema) + dataSource: formattedDataSource.items, + utils: formattedUtils.items, + bridge: formattedBridge.items, + globalState: formattedGlobalState.items, + state: formattedState.items, + methods: formattedMethods.items, + currentSchema: formattedSchema.schema, + truncated: { + dataSource: formattedDataSource.omitted, + utils: formattedUtils.omitted, + bridge: formattedBridge.omitted, + globalState: formattedGlobalState.omitted, + state: formattedState.omitted, + methods: formattedMethods.omitted, + currentSchema: formattedSchema.truncated + } } } diff --git a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js index dbfb044a7e..41b03b788c 100644 --- a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js @@ -1,5 +1,6 @@ import { CODE_PATTERNS, CONTEXT_CONFIG } from '../constants.js' import { buildLowcodeContext, validateLowcodeContext } from './lowcodeContextBuilder.js' +import { getCommentState, getOpenScopeContext } from '../utils/contextAnalysis.js' /** * 检测光标是否在注释中 @@ -7,21 +8,13 @@ import { buildLowcodeContext, validateLowcodeContext } from './lowcodeContextBui * @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' } - } + const commentState = getCommentState(textBeforeCursor) + + if (commentState.inLineComment) { + return { isComment: true, type: 'line' } } - // 块注释 /* */ 或 JSDoc /** */ - const lastBlockStart = textBeforeCursor.lastIndexOf('/*') - const lastBlockEnd = textBeforeCursor.lastIndexOf('*/') - if (lastBlockStart > lastBlockEnd) { + if (commentState.inBlockComment) { return { isComment: true, type: 'block' } } @@ -35,8 +28,9 @@ function isInComment(textBeforeCursor) { */ function extractCodeContext(textBeforeCursor) { const lines = textBeforeCursor.split('\n') - let functionName = '' - let className = '' + const openScope = getOpenScopeContext(textBeforeCursor) + let functionName = openScope.functionName + let className = openScope.className let interfaceName = '' let typeName = '' @@ -46,16 +40,6 @@ function extractCodeContext(textBeforeCursor) { 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] @@ -142,7 +126,9 @@ export function createSmartPrompt(completionMetadata) { const metaInfo = buildMetaInfo(filename, language, codeContext, technologies) if (lowcodeMetadata) { - lowcodeContext = buildLowcodeContext(lowcodeMetadata) + lowcodeContext = buildLowcodeContext(lowcodeMetadata, { + hintText: textBeforeCursor + }) const validation = validateLowcodeContext(lowcodeContext) if (!validation.valid) { diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index 35af15856b..f61f98bd08 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -39,13 +39,17 @@ export const MODEL_CONFIG = { TYPE: 'qwen', KEYWORDS: ['qwen'], PROVIDERS: ['bailian'], - BASE_URL_KEYWORDS: ['dashscope.aliyuncs.com'] + BASE_URL_KEYWORDS: ['dashscope.aliyuncs.com'], + COMPLETION_MODELS: ['qwen-coder-turbo-latest', 'qwen-coder-turbo-0919', 'qwen-coder-turbo'], + COMPLETION_MODEL_PATTERNS: [/^qwen2\.5-coder-(7|14|32)b-instruct$/] }, DEEPSEEK: { TYPE: 'deepseek', KEYWORDS: ['deepseek'], PROVIDERS: ['deepseek'], - BASE_URL_KEYWORDS: ['deepseek.com'] + BASE_URL_KEYWORDS: ['deepseek.com'], + COMPLETION_MODELS: ['deepseek-chat'], + COMPLETION_MODEL_PATTERNS: [] }, UNKNOWN: { TYPE: 'unknown', diff --git a/packages/plugins/script/src/ai-completion/prompts/templates.js b/packages/plugins/script/src/ai-completion/prompts/templates.js index 3e82e78eb0..39e39d883e 100644 --- a/packages/plugins/script/src/ai-completion/prompts/templates.js +++ b/packages/plugins/script/src/ai-completion/prompts/templates.js @@ -36,6 +36,14 @@ function formatSchemaFacts(currentSchema) { ] } +function appendOverflow(lines, omitted, label) { + if (!omitted) { + return lines + } + + return [...lines, `...and ${omitted} more ${label} not shown`] +} + function buildLowcodeFacts(lowcodeContext = {}) { const { dataSource = [], @@ -44,8 +52,10 @@ function buildLowcodeFacts(lowcodeContext = {}) { globalState = [], state = [], methods = [], - currentSchema = null + currentSchema = null, + truncated = {} } = lowcodeContext + const schemaTruncated = truncated.currentSchema || {} const sections = [ createSection('Platform APIs', [ @@ -56,26 +66,62 @@ function buildLowcodeFacts(lowcodeContext = {}) { ]), createSection( 'Data sources', - dataSource.map((item) => formatFactWithDescription(item.accessPath, item.description, item.type)) + appendOverflow( + dataSource.map((item) => formatFactWithDescription(item.accessPath, item.description, item.type)), + truncated.dataSource, + 'data sources' + ) ), createSection( 'Utilities', - utils.map((item) => formatFactWithDescription(item.signature || item.accessPath, item.description, item.package)) + appendOverflow( + utils.map((item) => + formatFactWithDescription(item.signature || item.accessPath, item.description, item.package) + ), + truncated.utils, + 'utilities' + ) ), createSection( 'Bridge APIs', - bridge.map((item) => formatFactWithDescription(item.accessPath, item.description)) + appendOverflow( + bridge.map((item) => formatFactWithDescription(item.accessPath, item.description)), + truncated.bridge, + 'bridge APIs' + ) + ), + createSection( + 'Global stores', + appendOverflow(formatStoreFacts(globalState), truncated.globalState, 'global stores') ), - createSection('Global stores', formatStoreFacts(globalState)), createSection( 'Local state', - state.map((item) => `${item.accessPath}: ${item.type}`) + appendOverflow( + state.map((item) => `${item.accessPath}: ${item.type}`), + truncated.state, + 'local state fields' + ) ), createSection( 'Local methods', - methods.map((item) => item.signature || `${item.accessPath}()`) + appendOverflow( + methods.map((item) => item.signature || `${item.accessPath}()`), + truncated.methods, + 'local methods' + ) ), - createSection('Current component', formatSchemaFacts(currentSchema)) + createSection( + 'Current component', + appendOverflow( + appendOverflow( + appendOverflow(formatSchemaFacts(currentSchema), schemaTruncated.props, 'component props'), + schemaTruncated.events, + 'component events' + ), + schemaTruncated.dynamicProps, + 'dynamic component props' + ) + ) ].filter(Boolean) return sections.join('\n\n') diff --git a/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js b/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js new file mode 100644 index 0000000000..8626545f6f --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js @@ -0,0 +1,299 @@ +const CONTROL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'catch', 'with']) + +function maskChar(char) { + return char === '\n' ? '\n' : ' ' +} + +export function getCommentState(text = '') { + let inSingleQuote = false + let inDoubleQuote = false + let inTemplate = false + let templateExpressionDepth = 0 + let inBlockComment = false + let inLineComment = false + + for (let i = 0; i < text.length; i++) { + const char = text[i] + const next = text[i + 1] + + if (inLineComment) { + if (char === '\n') { + inLineComment = false + } + continue + } + + if (inBlockComment) { + if (char === '*' && next === '/') { + inBlockComment = false + i++ + } + continue + } + + if (inSingleQuote) { + if (char === '\\') { + i++ + } else if (char === "'") { + inSingleQuote = false + } + continue + } + + if (inDoubleQuote) { + if (char === '\\') { + i++ + } else if (char === '"') { + inDoubleQuote = false + } + continue + } + + if (inTemplate && templateExpressionDepth === 0) { + if (char === '\\') { + i++ + } else if (char === '`') { + inTemplate = false + } else if (char === '$' && next === '{') { + templateExpressionDepth = 1 + i++ + } + continue + } + + if (char === '/' && next === '/') { + inLineComment = true + i++ + continue + } + + if (char === '/' && next === '*') { + inBlockComment = true + i++ + continue + } + + if (char === "'") { + inSingleQuote = true + continue + } + + if (char === '"') { + inDoubleQuote = true + continue + } + + if (char === '`') { + inTemplate = true + continue + } + + if (inTemplate && templateExpressionDepth > 0) { + if (char === '{') { + templateExpressionDepth++ + } else if (char === '}') { + templateExpressionDepth-- + } + } + } + + return { + inBlockComment, + inLineComment, + inComment: inBlockComment || inLineComment + } +} + +export function sanitizeStructuralText(text = '') { + const sanitized = [] + let inSingleQuote = false + let inDoubleQuote = false + let inTemplate = false + let templateExpressionDepth = 0 + let inBlockComment = false + let inLineComment = false + + for (let i = 0; i < text.length; i++) { + const char = text[i] + const next = text[i + 1] + + if (inLineComment) { + sanitized.push(maskChar(char)) + if (char === '\n') { + inLineComment = false + } + continue + } + + if (inBlockComment) { + sanitized.push(maskChar(char)) + if (char === '*' && next === '/') { + sanitized.push(maskChar(next)) + inBlockComment = false + i++ + } + continue + } + + if (inSingleQuote) { + sanitized.push(maskChar(char)) + if (char === '\\') { + sanitized.push(maskChar(next)) + i++ + } else if (char === "'") { + inSingleQuote = false + } + continue + } + + if (inDoubleQuote) { + sanitized.push(maskChar(char)) + if (char === '\\') { + sanitized.push(maskChar(next)) + i++ + } else if (char === '"') { + inDoubleQuote = false + } + continue + } + + if (inTemplate && templateExpressionDepth === 0) { + sanitized.push(maskChar(char)) + if (char === '\\') { + sanitized.push(maskChar(next)) + i++ + } else if (char === '`') { + inTemplate = false + } else if (char === '$' && next === '{') { + sanitized.push(maskChar(next)) + templateExpressionDepth = 1 + i++ + } + continue + } + + if (char === '/' && next === '/') { + sanitized.push(maskChar(char)) + sanitized.push(maskChar(next)) + inLineComment = true + i++ + continue + } + + if (char === '/' && next === '*') { + sanitized.push(maskChar(char)) + sanitized.push(maskChar(next)) + inBlockComment = true + i++ + continue + } + + if (char === "'") { + sanitized.push(maskChar(char)) + inSingleQuote = true + continue + } + + if (char === '"') { + sanitized.push(maskChar(char)) + inDoubleQuote = true + continue + } + + if (char === '`') { + sanitized.push(maskChar(char)) + inTemplate = true + continue + } + + if (inTemplate && templateExpressionDepth > 0) { + if (char === '{') { + templateExpressionDepth++ + sanitized.push(char) + continue + } + + if (char === '}') { + if (templateExpressionDepth === 1) { + sanitized.push(maskChar(char)) + } else { + sanitized.push(char) + } + templateExpressionDepth-- + continue + } + } + + sanitized.push(char) + } + + return sanitized.join('') +} + +function detectScopeFromHeader(header) { + const trimmedHeader = header.trimEnd() + + const classMatch = trimmedHeader.match(/class\s+([A-Za-z_$][\w$]*)[^{]*$/) + if (classMatch) { + return { + type: 'class', + name: classMatch[1] + } + } + + const functionPatterns = [ + /function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)[^{]*$/, + /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>[^{]*$/, + /(?:^|[\n;]\s*)(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*$/ + ] + + for (const pattern of functionPatterns) { + const match = trimmedHeader.match(pattern) + if (!match) { + continue + } + + const name = match[1] + if (!name || CONTROL_KEYWORDS.has(name)) { + continue + } + + return { + type: 'function', + name + } + } + + return { + type: 'other', + name: '' + } +} + +export function getOpenScopeContext(text = '') { + const sanitizedText = sanitizeStructuralText(text) + const scopeStack = [] + + for (let i = 0; i < sanitizedText.length; i++) { + const char = sanitizedText[i] + + if (char === '{') { + const header = sanitizedText.slice(Math.max(0, i - 200), i) + const scope = detectScopeFromHeader(header) + scopeStack.push(scope) + continue + } + + if (char === '}' && scopeStack.length > 0) { + scopeStack.pop() + } + } + + const functionScope = [...scopeStack].reverse().find((scope) => scope.type === 'function') + const classScope = [...scopeStack].reverse().find((scope) => scope.type === 'class') + + return { + functionName: functionScope?.name || '', + className: classScope?.name || '' + } +} diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js index a02cff9584..56228a0453 100644 --- a/packages/plugins/script/src/ai-completion/utils/modelUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -17,31 +17,29 @@ export function detectModelType(modelName, options = {}) { return capabilities.completionProtocol } - const lowerProvider = provider.toLowerCase() - if (MODEL_CONFIG.QWEN.PROVIDERS.some((item) => item === lowerProvider)) { + const lowerName = modelName.toLowerCase() + const isQwenCompletionModel = + MODEL_CONFIG.QWEN.COMPLETION_MODELS.some((item) => item === lowerName) || + MODEL_CONFIG.QWEN.COMPLETION_MODEL_PATTERNS.some((pattern) => pattern.test(lowerName)) + const isDeepSeekCompletionModel = + MODEL_CONFIG.DEEPSEEK.COMPLETION_MODELS.some((item) => item === lowerName) || + MODEL_CONFIG.DEEPSEEK.COMPLETION_MODEL_PATTERNS.some((pattern) => pattern.test(lowerName)) + + if (isQwenCompletionModel) { return MODEL_CONFIG.QWEN.TYPE } - if (MODEL_CONFIG.DEEPSEEK.PROVIDERS.some((item) => item === lowerProvider)) { + if (isDeepSeekCompletionModel) { return MODEL_CONFIG.DEEPSEEK.TYPE } + const lowerProvider = provider.toLowerCase() const lowerBaseUrl = baseUrl.toLowerCase() - if (MODEL_CONFIG.QWEN.BASE_URL_KEYWORDS.some((keyword) => lowerBaseUrl.includes(keyword))) { - return MODEL_CONFIG.QWEN.TYPE - } - - if (MODEL_CONFIG.DEEPSEEK.BASE_URL_KEYWORDS.some((keyword) => lowerBaseUrl.includes(keyword))) { - return MODEL_CONFIG.DEEPSEEK.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))) { + if ( + isDeepSeekCompletionModel && + MODEL_CONFIG.DEEPSEEK.PROVIDERS.some((item) => item === lowerProvider) && + MODEL_CONFIG.DEEPSEEK.BASE_URL_KEYWORDS.some((keyword) => lowerBaseUrl.includes(keyword)) + ) { return MODEL_CONFIG.DEEPSEEK.TYPE } diff --git a/packages/plugins/script/test/ai-completion.test.mjs b/packages/plugins/script/test/ai-completion.test.mjs index bf3adfe466..249bb2bf46 100644 --- a/packages/plugins/script/test/ai-completion.test.mjs +++ b/packages/plugins/script/test/ai-completion.test.mjs @@ -2,8 +2,11 @@ import test from 'node:test' import assert from 'node:assert/strict' import { buildLowcodeContext } from '../src/ai-completion/builders/lowcodeContextBuilder.js' +import { createSmartPrompt } from '../src/ai-completion/builders/promptBuilder.js' import { FIMPromptBuilder } from '../src/ai-completion/builders/fimPromptBuilder.js' import { createLowcodeInstruction } from '../src/ai-completion/prompts/templates.js' +import { cleanCompletion } from '../src/ai-completion/utils/completionUtils.js' +import { detectModelType } from '../src/ai-completion/utils/modelUtils.js' function createMetadata() { return { @@ -84,6 +87,30 @@ test('buildLowcodeContext keeps compact runtime facts', () => { assert.deepEqual(context.currentSchema.events, ['onRowClick']) }) +test('buildLowcodeContext prioritizes hinted symbols and reports truncation', () => { + const metadata = createMetadata() + metadata.methods = Object.fromEntries( + Array.from({ length: 24 }, (_, index) => [ + `method${index}`, + { + type: 'JSFunction', + value: `function method${index}(value) { return value }` + } + ]) + ) + metadata.methods.searchUsers = { + type: 'JSFunction', + value: 'function searchUsers(keyword) { return keyword }' + } + + const context = buildLowcodeContext(metadata, { + hintText: 'this.search' + }) + + assert.ok(context.methods.some((item) => item.name === 'searchUsers')) + assert.ok(context.truncated.methods > 0) +}) + test('createLowcodeInstruction uses TinyEngine runtime access paths', () => { const instruction = createLowcodeInstruction('javascript', buildLowcodeContext(createMetadata())) @@ -110,3 +137,53 @@ test('FIM prompt no longer injects verbose banner comments', () => { assert.doesNotMatch(prefix, /AI COMPLETION INSTRUCTIONS/) assert.doesNotMatch(prefix, /CODE CONTEXT STARTS BELOW/) }) + +test('createSmartPrompt does not treat comment markers inside strings as active comments', () => { + const { commentStatus, fileContent } = createSmartPrompt({ + textBeforeCursor: 'const text = "/* not a real comment */"\nconst label = `// still string`\n', + filename: 'page.js' + }) + + assert.equal(commentStatus.isComment, false) + assert.doesNotMatch(fileContent, /Current Function:/) +}) + +test('createSmartPrompt only marks an open function scope', () => { + const { fileContent } = createSmartPrompt({ + textBeforeCursor: 'function helper() {\n return 1\n}\n\nconst value = ', + filename: 'page.js' + }) + + assert.doesNotMatch(fileContent, /Current Function: helper/) +}) + +test('FIM cursor analysis ignores closed scopes and string comment markers', () => { + const builder = new FIMPromptBuilder({ + FIM: { + MAX_PREFIX_LINES: 100, + MAX_SUFFIX_LINES: 50 + } + }) + + const closedScope = builder.analyzeCursorContext('function helper() {\n return 1\n}\n\nconst value = ', '') + assert.equal(closedScope.inFunction, false) + + const stringComment = builder.analyzeCursorContext('const text = "/* not a real comment */"\n', '') + assert.equal(stringComment.inBlockComment, false) + assert.equal(stringComment.inLineComment, false) +}) + +test('cleanCompletion trims duplicated suffix overlap', () => { + const cleaned = cleanCompletion('load()\n}', 'qwen', { needsExpression: false }, '\n}') + + assert.equal(cleaned, 'load()') +}) + +test('detectModelType only accepts supported FIM models without explicit capability', () => { + assert.equal(detectModelType('qwen3-coder-flash', { provider: 'bailian' }), 'unknown') + assert.equal(detectModelType('qwen-coder-turbo-latest', { provider: 'bailian' }), 'qwen') + assert.equal( + detectModelType('deepseek-chat', { provider: 'deepseek', baseUrl: 'https://api.deepseek.com/v1' }), + 'deepseek' + ) +}) From f1090759953bd0f2a9db579ea32dc51c1269f5ef Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Wed, 15 Apr 2026 01:46:00 -0700 Subject: [PATCH 14/15] Refactor AI completion module: enhance model handling, improve context analysis, and streamline prompt generation --- .../robot-setting/RobotSetting.vue | 4 +- .../robot/src/composables/core/useConfig.ts | 38 ++-- .../robot/src/constants/model-config.ts | 2 - packages/plugins/script/src/Main.vue | 5 +- .../ai-completion/adapters/deepseekAdapter.js | 37 +++- .../src/ai-completion/adapters/index.js | 13 +- .../src/ai-completion/adapters/qwenAdapter.js | 21 +- .../builders/fimPromptBuilder.js | 8 +- .../src/ai-completion/builders/index.js | 3 - .../builders/lowcodeContextBuilder.js | 94 --------- .../ai-completion/builders/promptBuilder.js | 91 ++------- .../script/src/ai-completion/constants.js | 26 +-- .../plugins/script/src/ai-completion/index.js | 6 - .../src/ai-completion/prompts/templates.js | 22 -- .../triggers/completionTrigger.js | 15 +- .../ai-completion/utils/completionUtils.js | 3 +- .../ai-completion/utils/contextAnalysis.js | 6 +- .../src/ai-completion/utils/modelUtils.js | 16 +- .../script/test/ai-completion.test.mjs | 189 ------------------ packages/plugins/script/test/test.ts | 0 ....timestamp-1776240075469-6e177b92731d7.mjs | 29 +++ 21 files changed, 147 insertions(+), 481 deletions(-) delete mode 100644 packages/plugins/script/src/ai-completion/builders/index.js delete mode 100644 packages/plugins/script/src/ai-completion/index.js delete mode 100644 packages/plugins/script/test/ai-completion.test.mjs delete mode 100644 packages/plugins/script/test/test.ts create mode 100644 packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs 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 ed1c368c2f..f084666ea7 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 @@ -246,7 +246,7 @@ const syncModelSelection = () => { ) } -const notifyMissingApiKey = (service: any) => { +const notifyMissingApiKey = (service: ModelService) => { useNotify({ type: 'warning', title: '未配置API Key', @@ -364,7 +364,7 @@ const addService = () => { state.showServiceDialog = true } -const editService = (service: ModelService) => { +const editService = (service: any) => { state.editingService = JSON.parse(JSON.stringify(service)) state.showServiceDialog = true } diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts index a9dfede186..b61f22db7d 100644 --- a/packages/plugins/robot/src/composables/core/useConfig.ts +++ b/packages/plugins/robot/src/composables/core/useConfig.ts @@ -145,6 +145,22 @@ const getFallbackCompletionModel = (services: ModelService[], preferredServiceId } } +const resolveCompletionModelSelection = (services: ModelService[], serviceId = '', modelName = '') => { + if (serviceId && modelName) { + const selectedService = services.find((service) => service.id === serviceId) + const selectedModel = selectedService?.models.find((model) => model.name === modelName) + + if (isCompletionCapableModel(selectedService, selectedModel)) { + return { + serviceId, + modelName + } + } + } + + return getFallbackCompletionModel(services, serviceId) +} + const initBuiltInServices = (): ModelService[] => { return getAIModelOptions().map((service: any) => ({ id: service.provider, @@ -229,19 +245,21 @@ const migrateOldSettings = (oldSettings: any): RobotSettings | null => { services.push(customService) } - // 确定默认模型和快速模型 + // 确定默认模型、快速模型和代码补全模型 const selectedModel = activeName === 'existingModels' ? existModel : customizeModel const defaultServiceId = activeName === 'existingModels' ? services.find((s) => s.baseUrl === selectedModel?.baseUrl)?.id : '' + const legacyCompleteModelName = selectedModel?.completeModel || '' const quickModel = { serviceId: defaultServiceId || '', - modelName: selectedModel?.completeModel || '' + modelName: legacyCompleteModelName } - const completionModel = - quickModel.modelName && quickModel.serviceId - ? getFallbackCompletionModel(services, quickModel.serviceId) - : getFallbackCompletionModel(services, defaultServiceId || services[0]?.id || '') + const completionModel = resolveCompletionModelSelection( + services, + defaultServiceId || services[0]?.id || '', + legacyCompleteModelName + ) return { version: SETTING_VERSION, @@ -546,13 +564,6 @@ const getSelectedQuickModelInfo = (): SelectedModelInfo => { (m) => m.name === robotSettingState.quickModel.modelName ) const { name = '', label = '', capabilities = {} } = currentModel || {} - const completionProtocol = - inferCompletionProtocol({ - provider: currentService?.provider, - baseUrl: currentService?.baseUrl, - modelName: name, - capabilities - }) || null const { models, ...service } = currentService ?? ({} as Partial) @@ -567,7 +578,6 @@ const getSelectedQuickModelInfo = (): SelectedModelInfo => { // 模型兼容字段 model: robotSettingState.quickModel.modelName, completeModel: robotSettingState.quickModel.modelName || '', - completionProtocol, // 服务兼容字段 baseUrl: currentService?.baseUrl || '', apiKey: currentService?.apiKey || '' diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts index b131a2f3f2..a2bcb348ec 100644 --- a/packages/plugins/robot/src/constants/model-config.ts +++ b/packages/plugins/robot/src/constants/model-config.ts @@ -94,7 +94,6 @@ export const DEFAULT_LLM_MODELS = [ name: 'qwen-coder-turbo-latest', capabilities: { toolCalling: true, - compact: true, jsonOutput: bailianJsonOutputExtraBody, completionProtocol: 'qwen' } @@ -122,7 +121,6 @@ export const DEFAULT_LLM_MODELS = [ name: 'deepseek-chat', capabilities: { toolCalling: true, - compact: true, completionProtocol: 'deepseek', reasoning: { extraBody: { diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index 9e5f2bd7a3..7e98a63cfd 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -122,8 +122,9 @@ export default { // 保留原有的 ESLint state.linterWorker = initLinter(editor, monacoRef.value.getMonaco(), state) as any - const { aiCompletionEnabled, aiCompletionTrigger = 'onIdle' } = - getMergeMeta('engine.plugins.pagecontroller')?.options || {} + const pageControllerOptions = getMergeMeta('engine.plugins.pagecontroller')?.options || {} + const aiCompletionEnabled = pageControllerOptions.aiCompletionEnabled ?? pageControllerOptions.enableAICompletion + const aiCompletionTrigger = pageControllerOptions.aiCompletionTrigger || 'onIdle' if (aiCompletionEnabled) { try { diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js index 7eb1278f88..890f7b5c99 100644 --- a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -1,5 +1,39 @@ import { HTTP_CONFIG, ERROR_MESSAGES, DEEPSEEK_CONFIG } from '../constants.js' +export function buildDeepSeekCompletionsUrl(baseUrl) { + const normalizedBaseUrl = String(baseUrl || '').trim() + + if (!normalizedBaseUrl) { + return '' + } + + const normalizePath = (path = '') => { + const trimmedPath = path.replace(/\/+$/, '') + + if (trimmedPath.endsWith('/beta/completions')) { + return trimmedPath + } + + if (trimmedPath.endsWith('/beta')) { + return `${trimmedPath}/completions` + } + + if (/\/v1$/i.test(trimmedPath)) { + return trimmedPath.replace(/\/v1$/i, '/beta/completions') + } + + return `${trimmedPath}/beta/completions` + } + + try { + const parsedUrl = new URL(normalizedBaseUrl) + parsedUrl.pathname = normalizePath(parsedUrl.pathname) + return parsedUrl.toString() + } catch { + return normalizePath(normalizedBaseUrl) + } +} + /** * 构建 DeepSeek FIM 格式的请求参数 * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记) @@ -27,8 +61,7 @@ export function buildDeepSeekFIMParams(fileContent, fimBuilder, metadata = {}) { * @returns {Promise} 补全文本 */ 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 completionsUrl = buildDeepSeekCompletionsUrl(baseUrl) const requestBody = { model: config.model, diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index 6c88822edf..f28110d21a 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -3,7 +3,7 @@ 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 { buildQwenFIMPrompt, callQwenAPI } from './qwenAdapter.js' import { buildDeepSeekFIMParams, callDeepSeekAPI } from './deepseekAdapter.js' import { QWEN_CONFIG, DEEPSEEK_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js' @@ -56,7 +56,6 @@ export function createCompletionHandler() { textAfterCursor, language, filename, - technologies: DEFAULTS.TECHNOLOGIES, lowcodeMetadata }) @@ -87,15 +86,15 @@ export function createCompletionHandler() { if (modelType === MODEL_CONFIG.QWEN.TYPE) { // ===== Qwen 流程 ===== - const { messages, cursorContext: ctx } = buildQwenMessages(fileContent, qwenFimBuilder, fimMetadata) + const { prompt, cursorContext: ctx } = buildQwenFIMPrompt(fileContent, qwenFimBuilder, fimMetadata) cursorContext = ctx completionText = await callQwenAPI( - messages, + prompt, { model: completeModel, maxTokens: calculateTokens(ctx), - stopSequences: getStopSequences(ctx, modelType) + stopSequences: getStopSequences(ctx) }, apiKey, baseUrl @@ -115,7 +114,7 @@ export function createCompletionHandler() { { model: completeModel, maxTokens: calculateTokens(ctx), - stopSequences: getStopSequences(ctx, modelType) + stopSequences: getStopSequences(ctx) }, apiKey, baseUrl @@ -124,7 +123,7 @@ export function createCompletionHandler() { // 7. 处理补全结果 if (completionText) { - completionText = cleanCompletion(completionText, modelType, cursorContext, textAfterCursor) + completionText = cleanCompletion(completionText, cursorContext, textAfterCursor) if (!completionText) { return { diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js index aaae498270..986083d390 100644 --- a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -1,41 +1,36 @@ import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' /** - * 构建 Qwen FIM 格式的 messages + * 构建 Qwen FIM prompt * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记) * @param {Object} fimBuilder - FIM 构建器实例 - * @param {Object} metadata - 元数据(language, lowcodeMetadata 等) - * @returns {{ messages: Array, cursorContext: Object }} Messages 和上下文 + * @param {Object} metadata - 元数据 + * @returns {{ prompt: string, cursorContext: Object }} Prompt 和上下文 */ -export function buildQwenMessages(fileContent, fimBuilder, metadata = {}) { +export function buildQwenFIMPrompt(fileContent, fimBuilder, metadata = {}) { const { fimPrompt, cursorContext } = fimBuilder.buildOptimizedFIMPrompt(fileContent, metadata) return { - messages: [ - { - role: 'user', - content: fimPrompt - } - ], + prompt: fimPrompt, cursorContext } } /** * 调用 Qwen Completions API - * @param {Array} messages - Messages 数组 + * @param {string} prompt - FIM prompt * @param {Object} config - 配置对象 * @param {string} apiKey - API 密钥 * @param {string} baseUrl - 基础 URL * @returns {Promise} 补全文本 */ -export async function callQwenAPI(messages, config, apiKey, baseUrl) { +export async function callQwenAPI(prompt, config, apiKey, baseUrl) { // 构建完整的 Completions API URL const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}` const requestBody = { model: config.model, - prompt: messages[0].content, // FIM prompt + prompt, max_tokens: config.maxTokens, temperature: QWEN_CONFIG.DEFAULT_TEMPERATURE, top_p: QWEN_CONFIG.TOP_P, diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js index 8216cbf2ab..e5761193a8 100644 --- a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js @@ -32,7 +32,7 @@ export class FIMPromptBuilder { return { prefix: fileContent, suffix: '', - cursorContext: { type: 'unknown', hasPrefix: true, hasSuffix: false } + cursorContext: { type: 'unknown' } } } @@ -118,18 +118,14 @@ export class FIMPromptBuilder { /** * 分析光标上下文 * @param {string} prefix - 前缀代码 - * @param {string} suffix - 后缀代码 * @returns {Object} 上下文信息 */ - analyzeCursorContext(prefix, suffix) { + analyzeCursorContext(prefix) { const context = { type: 'unknown', - hasPrefix: prefix.trim().length > 0, - hasSuffix: suffix.trim().length > 0, inFunction: false, inClass: false, inObject: false, - inArray: false, inBlockComment: false, inLineComment: false, needsExpression: false, diff --git a/packages/plugins/script/src/ai-completion/builders/index.js b/packages/plugins/script/src/ai-completion/builders/index.js deleted file mode 100644 index bba666e7c3..0000000000 --- a/packages/plugins/script/src/ai-completion/builders/index.js +++ /dev/null @@ -1,3 +0,0 @@ -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 index 53aeea43ad..37f0355980 100644 --- a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -345,100 +345,6 @@ function formatCurrentSchema(schema, hintContext) { } } -/** - * 验证低代码上下文的完整性 - * @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', 'bridge', '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.bridge && !Array.isArray(context.bridge)) { - warnings.push('bridge should be an array') - } - - if (context.globalState && !Array.isArray(context.globalState)) { - warnings.push('globalState should be an array') - } - - if (context.state && !Array.isArray(context.state)) { - warnings.push('state should be an array') - } - - if (context.methods && !Array.isArray(context.methods)) { - warnings.push('methods should be an array') - } - - return { - valid: warnings.length === 0, - warnings - } -} - -/** - * 合并多个低代码上下文 - * @param {...Object} contexts - 多个上下文对象 - * @returns {Object} 合并后的上下文 - */ -export function mergeLowcodeContexts(...contexts) { - const merged = { - dataSource: [], - utils: [], - bridge: [], - 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.bridge) { - merged.bridge = [...merged.bridge, ...context.bridge] - } - 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] - } - if (context.currentSchema) { - merged.currentSchema = context.currentSchema - } - } - - return merged -} - /** * 从低代码平台元数据构建补全上下文 * @param {Object} metadata - 低代码平台元数据 diff --git a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js index 41b03b788c..4ff06ce0d0 100644 --- a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js @@ -1,5 +1,4 @@ -import { CODE_PATTERNS, CONTEXT_CONFIG } from '../constants.js' -import { buildLowcodeContext, validateLowcodeContext } from './lowcodeContextBuilder.js' +import { buildLowcodeContext } from './lowcodeContextBuilder.js' import { getCommentState, getOpenScopeContext } from '../utils/contextAnalysis.js' /** @@ -22,85 +21,36 @@ function isInComment(textBeforeCursor) { } /** - * 提取当前代码上下文信息(函数名、类名、接口名等) + * 提取当前代码上下文信息(函数名、类名) * @param {string} textBeforeCursor - 光标前的文本 - * @returns {{ functionName: string, className: string, interfaceName: string, typeName: string }} 代码上下文 + * @returns {{ functionName: string, className: string }} 代码上下文 */ function extractCodeContext(textBeforeCursor) { - const lines = textBeforeCursor.split('\n') const openScope = getOpenScopeContext(textBeforeCursor) - let functionName = openScope.functionName - let className = openScope.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 (!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: openScope.functionName, + className: openScope.className } - - 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` +function buildMetaInfo(codeContext) { + const metaLines = [] - // 强调当前作用域 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` + metaLines.push(`// Current Class: ${codeContext.className}`) } if (codeContext.functionName) { - metaInfo += `// Current Function: ${codeContext.functionName}\n` - metaInfo += `// IMPORTANT: Only complete code within this function scope\n` + metaLines.push(`// Current Function: ${codeContext.functionName}`) } - 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 + return metaLines.length ? `${metaLines.join('\n')}\n\n` : '' } /** @@ -109,32 +59,19 @@ function buildMetaInfo(filename, language, codeContext, technologies) { * @returns {{ fileContent: string, commentStatus: object, lowcodeContext: object | null }} Prompt 对象 */ export function createSmartPrompt(completionMetadata) { - const { - textBeforeCursor = '', - textAfterCursor = '', - language = 'javascript', - filename, - technologies = [], - lowcodeMetadata = null - } = completionMetadata + const { textBeforeCursor = '', textAfterCursor = '', lowcodeMetadata = null } = completionMetadata const commentStatus = isInComment(textBeforeCursor) const codeContext = extractCodeContext(textBeforeCursor) let lowcodeContext = null - // 构建文件元信息(伪装成注释,让 AI 理解上下文) - const metaInfo = buildMetaInfo(filename, language, codeContext, technologies) + // 用极少量上下文注释提醒当前开放作用域,避免重复注入过多控制信息 + const metaInfo = buildMetaInfo(codeContext) if (lowcodeMetadata) { lowcodeContext = buildLowcodeContext(lowcodeMetadata, { hintText: textBeforeCursor }) - const validation = validateLowcodeContext(lowcodeContext) - - if (!validation.valid) { - // eslint-disable-next-line no-console - console.warn('⚠️ Lowcode context validation warnings:', validation.warnings) - } } // 在文件内容前注入元信息 diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index f61f98bd08..1df262e627 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -37,33 +37,19 @@ export const DEEPSEEK_CONFIG = { export const MODEL_CONFIG = { QWEN: { TYPE: 'qwen', - KEYWORDS: ['qwen'], - PROVIDERS: ['bailian'], - BASE_URL_KEYWORDS: ['dashscope.aliyuncs.com'], COMPLETION_MODELS: ['qwen-coder-turbo-latest', 'qwen-coder-turbo-0919', 'qwen-coder-turbo'], COMPLETION_MODEL_PATTERNS: [/^qwen2\.5-coder-(7|14|32)b-instruct$/] }, DEEPSEEK: { TYPE: 'deepseek', - KEYWORDS: ['deepseek'], - PROVIDERS: ['deepseek'], - BASE_URL_KEYWORDS: ['deepseek.com'], COMPLETION_MODELS: ['deepseek-chat'], COMPLETION_MODEL_PATTERNS: [] }, UNKNOWN: { - TYPE: 'unknown', - KEYWORDS: [] + TYPE: 'unknown' } } -/** - * API 端点配置 - */ -export const API_ENDPOINTS = { - CHAT_COMPLETIONS: '/app-center/api/chat/completions' -} - /** * HTTP 请求配置 */ @@ -77,9 +63,7 @@ export const HTTP_CONFIG = { * 默认配置 */ export const DEFAULTS = { - LANGUAGE: 'javascript', - LOG_PREVIEW_LENGTH: 100, - TECHNOLOGIES: [] + LANGUAGE: 'javascript' } /** @@ -88,7 +72,7 @@ export const DEFAULTS = { export const ERROR_MESSAGES = { CONFIG_MISSING: 'AI 配置未设置(缺少 model/apiKey/baseUrl)', API_KEY_MISSING: 'AI 配置未设置(缺少 API Key)', - UNSUPPORTED_MODEL: '当前快速模型未配置可用的补全协议,请在模型设置中指定协议或选择内置代码模型', + UNSUPPORTED_MODEL: '当前代码补全模型未配置可用的补全协议,请在模型设置中指定协议或选择内置代码模型', NO_COMPLETION: '未收到有效的补全结果', REQUEST_FAILED: '请求失败', QWEN_API_ERROR: 'Qwen API 错误' @@ -163,10 +147,6 @@ export const CONTEXT_CONFIG = { * 代码模式匹配(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) diff --git a/packages/plugins/script/src/ai-completion/index.js b/packages/plugins/script/src/ai-completion/index.js deleted file mode 100644 index ae14a7a2fb..0000000000 --- a/packages/plugins/script/src/ai-completion/index.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 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 index 39e39d883e..3c48a9a0a6 100644 --- a/packages/plugins/script/src/ai-completion/prompts/templates.js +++ b/packages/plugins/script/src/ai-completion/prompts/templates.js @@ -127,13 +127,6 @@ function buildLowcodeFacts(lowcodeContext = {}) { return sections.join('\n\n') } -/** - * 系统 Prompt - 定义 AI 的角色和基本规则 - */ -export const SYSTEM_BASE_PROMPT = `Return only the text to insert at the cursor. -Match the surrounding style and indentation. -Stay in the current scope and do not repeat existing code.` - /** * 代码补全指令模板 * @param {string} language - 编程语言 @@ -178,18 +171,3 @@ export function createLowcodeInstruction(language, lowcodeContext = {}) { return facts ? `${instruction}\n\n${facts}` : 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 index 76d58b3122..858d352eb0 100644 --- a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js +++ b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js @@ -1,3 +1,5 @@ +import { getCommentState } from '../utils/contextAnalysis.js' + /** * 检测光标是否在语句结束符后(分号后) */ @@ -52,18 +54,27 @@ export function shouldTriggerCompletion(params) { const lines = text.split('\n') const currentLine = lines[position.lineNumber - 1] || '' const beforeCursor = currentLine.substring(0, position.column - 1) + const textBeforeCursor = `${lines.slice(0, position.lineNumber - 1).join('\n')}${ + position.lineNumber > 1 ? '\n' : '' + }${beforeCursor}` + const lexicalState = getCommentState(textBeforeCursor) // 1. 代码太短不触发 if (text.trim().length < 2) { return false } - // 2. 分号后不触发(语句已结束) + // 2. 注释和字符串里不触发 + if (lexicalState.inComment || lexicalState.inString) { + return false + } + + // 3. 分号后不触发(语句已结束) if (isAfterStatementEnd(beforeCursor)) { return false } - // 3. 右花括号后不触发(块已结束) + // 4. 右花括号后不触发(块已结束) if (isAfterBlockEnd(beforeCursor)) { return false } diff --git a/packages/plugins/script/src/ai-completion/utils/completionUtils.js b/packages/plugins/script/src/ai-completion/utils/completionUtils.js index 7e008352e3..776dff4fe7 100644 --- a/packages/plugins/script/src/ai-completion/utils/completionUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js @@ -46,12 +46,11 @@ export function buildLowcodeMetadata() { /** * 清理补全文本 * @param {string} text - 原始补全文本 - * @param {string} modelType - 模型类型 * @param {Object} cursorContext - 光标上下文信息(可选) * @param {string} suffix - 光标后的原始文本 * @returns {string} 清理后的文本 */ -export function cleanCompletion(text, modelType, cursorContext = null, suffix = '') { +export function cleanCompletion(text, cursorContext = null, suffix = '') { if (!text) return text let cleaned = text diff --git a/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js b/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js index 8626545f6f..d78fb59da8 100644 --- a/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js +++ b/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js @@ -97,10 +97,14 @@ export function getCommentState(text = '') { } } + const inTemplateString = inTemplate && templateExpressionDepth === 0 + return { inBlockComment, inLineComment, - inComment: inBlockComment || inLineComment + inComment: inBlockComment || inLineComment, + inString: inSingleQuote || inDoubleQuote || inTemplateString, + inTemplateString } } diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js index 56228a0453..5f0e260d8d 100644 --- a/packages/plugins/script/src/ai-completion/utils/modelUtils.js +++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js @@ -4,15 +4,13 @@ import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, CONTEXT_STOP_SEQUENC * 检测模型类型 * @param {string} modelName - 模型名称 * @param {Object} options - 额外上下文 - * @param {string} options.provider - 服务 provider - * @param {string} options.baseUrl - 服务 baseUrl * @param {Object} options.capabilities - 模型能力 * @returns {'qwen' | 'deepseek' | 'unknown'} 模型类型 */ export function detectModelType(modelName, options = {}) { if (!modelName) return MODEL_CONFIG.UNKNOWN.TYPE - const { provider = '', baseUrl = '', capabilities = {} } = options + const { capabilities = {} } = options if (capabilities?.completionProtocol) { return capabilities.completionProtocol } @@ -33,16 +31,6 @@ export function detectModelType(modelName, options = {}) { return MODEL_CONFIG.DEEPSEEK.TYPE } - const lowerProvider = provider.toLowerCase() - const lowerBaseUrl = baseUrl.toLowerCase() - if ( - isDeepSeekCompletionModel && - MODEL_CONFIG.DEEPSEEK.PROVIDERS.some((item) => item === lowerProvider) && - MODEL_CONFIG.DEEPSEEK.BASE_URL_KEYWORDS.some((keyword) => lowerBaseUrl.includes(keyword)) - ) { - return MODEL_CONFIG.DEEPSEEK.TYPE - } - return MODEL_CONFIG.UNKNOWN.TYPE } @@ -72,7 +60,7 @@ export function calculateTokens(cursorContext) { } // 获取动态停止符(最多 16 个) -export function getStopSequences(cursorContext, _modelType) { +export function getStopSequences(cursorContext) { const stops = [] // 核心停止符 diff --git a/packages/plugins/script/test/ai-completion.test.mjs b/packages/plugins/script/test/ai-completion.test.mjs deleted file mode 100644 index 249bb2bf46..0000000000 --- a/packages/plugins/script/test/ai-completion.test.mjs +++ /dev/null @@ -1,189 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' - -import { buildLowcodeContext } from '../src/ai-completion/builders/lowcodeContextBuilder.js' -import { createSmartPrompt } from '../src/ai-completion/builders/promptBuilder.js' -import { FIMPromptBuilder } from '../src/ai-completion/builders/fimPromptBuilder.js' -import { createLowcodeInstruction } from '../src/ai-completion/prompts/templates.js' -import { cleanCompletion } from '../src/ai-completion/utils/completionUtils.js' -import { detectModelType } from '../src/ai-completion/utils/modelUtils.js' - -function createMetadata() { - return { - dataSource: [ - { - name: 'users', - type: 'list', - description: 'Load user records', - options: { - shouldNotLeak: true - } - } - ], - utils: [ - { - name: 'formatDate', - type: 'function', - content: { - type: 'JSFunction', - value: 'function formatDate(value, pattern) { return value }' - } - } - ], - bridge: [ - { - name: 'toast', - description: 'Show a toast message' - } - ], - globalState: [ - { - id: 'userStore', - state: { - token: '', - profile: {} - }, - getters: { - displayName: {} - }, - actions: { - fetchProfile: {} - } - } - ], - state: { - keyword: '', - rows: [] - }, - methods: { - searchUsers: { - type: 'JSFunction', - value: 'function searchUsers(keyword) { return keyword }' - } - }, - currentSchema: { - componentName: 'TinyGrid', - ref: 'gridRef', - props: { - data: { - type: 'JSExpression' - }, - pager: true, - onRowClick: { - type: 'JSFunction' - } - } - } - } -} - -test('buildLowcodeContext keeps compact runtime facts', () => { - const context = buildLowcodeContext(createMetadata()) - - assert.equal(context.dataSource[0].accessPath, 'this.dataSourceMap.users.load()') - assert.equal('options' in context.dataSource[0], false) - assert.equal(context.bridge[0].accessPath, 'this.bridge.toast') - assert.deepEqual(context.currentSchema.props, ['data', 'pager']) - assert.deepEqual(context.currentSchema.events, ['onRowClick']) -}) - -test('buildLowcodeContext prioritizes hinted symbols and reports truncation', () => { - const metadata = createMetadata() - metadata.methods = Object.fromEntries( - Array.from({ length: 24 }, (_, index) => [ - `method${index}`, - { - type: 'JSFunction', - value: `function method${index}(value) { return value }` - } - ]) - ) - metadata.methods.searchUsers = { - type: 'JSFunction', - value: 'function searchUsers(keyword) { return keyword }' - } - - const context = buildLowcodeContext(metadata, { - hintText: 'this.search' - }) - - assert.ok(context.methods.some((item) => item.name === 'searchUsers')) - assert.ok(context.truncated.methods > 0) -}) - -test('createLowcodeInstruction uses TinyEngine runtime access paths', () => { - const instruction = createLowcodeInstruction('javascript', buildLowcodeContext(createMetadata())) - - assert.match(instruction, /this\.dataSourceMap\.users\.load\(\)/) - assert.match(instruction, /this\.bridge\.toast/) - assert.match(instruction, /this\.searchUsers\(keyword\)/) - assert.doesNotMatch(instruction, /this\.dataSource\./) -}) - -test('FIM prompt no longer injects verbose banner comments', () => { - const builder = new FIMPromptBuilder({ - FIM: { - MAX_PREFIX_LINES: 100, - MAX_SUFFIX_LINES: 50 - } - }) - - const { prefix } = builder.buildFIMComponents('function demo() {\n [CURSOR]\n}\n', { - language: 'javascript', - lowcodeContext: buildLowcodeContext(createMetadata()) - }) - - assert.match(prefix, /this\.dataSourceMap\.users\.load\(\)/) - assert.doesNotMatch(prefix, /AI COMPLETION INSTRUCTIONS/) - assert.doesNotMatch(prefix, /CODE CONTEXT STARTS BELOW/) -}) - -test('createSmartPrompt does not treat comment markers inside strings as active comments', () => { - const { commentStatus, fileContent } = createSmartPrompt({ - textBeforeCursor: 'const text = "/* not a real comment */"\nconst label = `// still string`\n', - filename: 'page.js' - }) - - assert.equal(commentStatus.isComment, false) - assert.doesNotMatch(fileContent, /Current Function:/) -}) - -test('createSmartPrompt only marks an open function scope', () => { - const { fileContent } = createSmartPrompt({ - textBeforeCursor: 'function helper() {\n return 1\n}\n\nconst value = ', - filename: 'page.js' - }) - - assert.doesNotMatch(fileContent, /Current Function: helper/) -}) - -test('FIM cursor analysis ignores closed scopes and string comment markers', () => { - const builder = new FIMPromptBuilder({ - FIM: { - MAX_PREFIX_LINES: 100, - MAX_SUFFIX_LINES: 50 - } - }) - - const closedScope = builder.analyzeCursorContext('function helper() {\n return 1\n}\n\nconst value = ', '') - assert.equal(closedScope.inFunction, false) - - const stringComment = builder.analyzeCursorContext('const text = "/* not a real comment */"\n', '') - assert.equal(stringComment.inBlockComment, false) - assert.equal(stringComment.inLineComment, false) -}) - -test('cleanCompletion trims duplicated suffix overlap', () => { - const cleaned = cleanCompletion('load()\n}', 'qwen', { needsExpression: false }, '\n}') - - assert.equal(cleaned, 'load()') -}) - -test('detectModelType only accepts supported FIM models without explicit capability', () => { - assert.equal(detectModelType('qwen3-coder-flash', { provider: 'bailian' }), 'unknown') - assert.equal(detectModelType('qwen-coder-turbo-latest', { provider: 'bailian' }), 'qwen') - assert.equal( - detectModelType('deepseek-chat', { provider: 'deepseek', baseUrl: 'https://api.deepseek.com/v1' }), - 'deepseek' - ) -}) diff --git a/packages/plugins/script/test/test.ts b/packages/plugins/script/test/test.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs b/packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs new file mode 100644 index 0000000000..338f6698c2 --- /dev/null +++ b/packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs @@ -0,0 +1,29 @@ +// vite.config.ts +import { defineConfig } from 'file:///E:/LS_WorkSpace/web/tiny-engine/node_modules/.pnpm/vite@5.4.21_@types+node@22.19.7_less@4.5.1/node_modules/vite/dist/node/index.js' +import path from 'path' +import dts from 'file:///E:/LS_WorkSpace/web/tiny-engine/node_modules/.pnpm/vite-plugin-dts@4.5.4_@type_1710e030ba0c4aa7433fc0cb72d4e195/node_modules/vite-plugin-dts/dist/index.mjs' +var __vite_injected_original_dirname = 'E:\\LS_WorkSpace\\web\\tiny-engine\\packages\\register' +var vite_config_default = defineConfig({ + plugins: [ + dts({ + tsconfigPath: path.resolve(__vite_injected_original_dirname, './tsconfig.json'), + rollupTypes: true + }) + ], + publicDir: false, + resolve: {}, + build: { + sourcemap: true, + lib: { + entry: path.resolve(__vite_injected_original_dirname, './src/index.ts'), + name: 'tiny-engine-meta-register', + fileName: (_format, entryName) => `${entryName}.js`, + formats: ['es'] + }, + rollupOptions: { + external: ['vue', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/] + } + } +}) +export { vite_config_default as default } +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJFOlxcXFxMU19Xb3JrU3BhY2VcXFxcd2ViXFxcXHRpbnktZW5naW5lXFxcXHBhY2thZ2VzXFxcXHJlZ2lzdGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJFOlxcXFxMU19Xb3JrU3BhY2VcXFxcd2ViXFxcXHRpbnktZW5naW5lXFxcXHBhY2thZ2VzXFxcXHJlZ2lzdGVyXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9FOi9MU19Xb3JrU3BhY2Uvd2ViL3RpbnktZW5naW5lL3BhY2thZ2VzL3JlZ2lzdGVyL3ZpdGUuY29uZmlnLnRzXCI7LyoqXHJcbiAqIENvcHlyaWdodCAoYykgMjAyMyAtIHByZXNlbnQgVGlueUVuZ2luZSBBdXRob3JzLlxyXG4gKiBDb3B5cmlnaHQgKGMpIDIwMjMgLSBwcmVzZW50IEh1YXdlaSBDbG91ZCBDb21wdXRpbmcgVGVjaG5vbG9naWVzIENvLiwgTHRkLlxyXG4gKlxyXG4gKiBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSBhbiBNSVQtc3R5bGUgbGljZW5zZS5cclxuICpcclxuICogVEhFIE9QRU4gU09VUkNFIFNPRlRXQVJFIElOIFRISVMgUFJPRFVDVCBJUyBESVNUUklCVVRFRCBJTiBUSEUgSE9QRSBUSEFUIElUIFdJTEwgQkUgVVNFRlVMLFxyXG4gKiBCVVQgV0lUSE9VVCBBTlkgV0FSUkFOVFksIFdJVEhPVVQgRVZFTiBUSEUgSU1QTElFRCBXQVJSQU5UWSBPRiBNRVJDSEFOVEFCSUxJVFkgT1IgRklUTkVTUyBGT1JcclxuICogQSBQQVJUSUNVTEFSIFBVUlBPU0UuIFNFRSBUSEUgQVBQTElDQUJMRSBMSUNFTlNFUyBGT1IgTU9SRSBERVRBSUxTLlxyXG4gKlxyXG4gKi9cclxuXHJcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnXHJcbmltcG9ydCBwYXRoIGZyb20gJ3BhdGgnXHJcbmltcG9ydCBkdHMgZnJvbSAndml0ZS1wbHVnaW4tZHRzJ1xyXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gIHBsdWdpbnM6IFtcclxuICAgIGR0cyh7XHJcbiAgICAgIHRzY29uZmlnUGF0aDogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4vdHNjb25maWcuanNvbicpLFxyXG4gICAgICByb2xsdXBUeXBlczogdHJ1ZVxyXG4gICAgfSlcclxuICBdLFxyXG4gIHB1YmxpY0RpcjogZmFsc2UsXHJcbiAgcmVzb2x2ZToge30sXHJcbiAgYnVpbGQ6IHtcclxuICAgIHNvdXJjZW1hcDogdHJ1ZSxcclxuICAgIGxpYjoge1xyXG4gICAgICBlbnRyeTogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4vc3JjL2luZGV4LnRzJyksXHJcbiAgICAgIG5hbWU6ICd0aW55LWVuZ2luZS1tZXRhLXJlZ2lzdGVyJyxcclxuICAgICAgZmlsZU5hbWU6IChfZm9ybWF0LCBlbnRyeU5hbWUpID0+IGAke2VudHJ5TmFtZX0uanNgLFxyXG4gICAgICBmb3JtYXRzOiBbJ2VzJ11cclxuICAgIH0sXHJcbiAgICByb2xsdXBPcHRpb25zOiB7XHJcbiAgICAgIGV4dGVybmFsOiBbJ3Z1ZScsIC9Ab3BlbnRpbnlcXC90aW55LWVuZ2luZS4qLywgL0BvcGVudGlueVxcL3Z1ZS4qL11cclxuICAgIH1cclxuICB9XHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFZQSxTQUFTLG9CQUFvQjtBQUM3QixPQUFPLFVBQVU7QUFDakIsT0FBTyxTQUFTO0FBZGhCLElBQU0sbUNBQW1DO0FBZ0J6QyxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsTUFDRixjQUFjLEtBQUssUUFBUSxrQ0FBVyxpQkFBaUI7QUFBQSxNQUN2RCxhQUFhO0FBQUEsSUFDZixDQUFDO0FBQUEsRUFDSDtBQUFBLEVBQ0EsV0FBVztBQUFBLEVBQ1gsU0FBUyxDQUFDO0FBQUEsRUFDVixPQUFPO0FBQUEsSUFDTCxXQUFXO0FBQUEsSUFDWCxLQUFLO0FBQUEsTUFDSCxPQUFPLEtBQUssUUFBUSxrQ0FBVyxnQkFBZ0I7QUFBQSxNQUMvQyxNQUFNO0FBQUEsTUFDTixVQUFVLENBQUMsU0FBUyxjQUFjLEdBQUcsU0FBUztBQUFBLE1BQzlDLFNBQVMsQ0FBQyxJQUFJO0FBQUEsSUFDaEI7QUFBQSxJQUNBLGVBQWU7QUFBQSxNQUNiLFVBQVUsQ0FBQyxPQUFPLDRCQUE0QixrQkFBa0I7QUFBQSxJQUNsRTtBQUFBLEVBQ0Y7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= From bc01030326e949b8162f11331534cb9232cc882e Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Fri, 17 Apr 2026 01:02:48 -0700 Subject: [PATCH 15/15] fix: review suggestion --- .../robot/src/composables/core/useConfig.ts | 2 +- .../ai-completion/adapters/deepseekAdapter.js | 22 +- .../src/ai-completion/adapters/index.js | 8 +- .../src/ai-completion/adapters/qwenAdapter.js | 22 +- .../builders/fimPromptBuilder.js | 2 +- .../builders/lowcodeContextBuilder.js | 39 ++-- .../script/src/ai-completion/constants.js | 5 +- .../triggers/completionTrigger.js | 12 +- .../ai-completion/utils/contextAnalysis.js | 216 ++++++++++++++++++ .../src/ai-completion/utils/requestUtils.js | 57 +++++ ....timestamp-1776240075469-6e177b92731d7.mjs | 29 --- 11 files changed, 345 insertions(+), 69 deletions(-) create mode 100644 packages/plugins/script/src/ai-completion/utils/requestUtils.js delete mode 100644 packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts index b61f22db7d..822c302712 100644 --- a/packages/plugins/robot/src/composables/core/useConfig.ts +++ b/packages/plugins/robot/src/composables/core/useConfig.ts @@ -55,7 +55,7 @@ const getAIModelOptions = () => { } const QWEN_FIM_MODEL_PATTERNS = [/^qwen-coder-turbo(?:-latest|-0919)?$/, /^qwen2\.5-coder-(7|14|32)b-instruct$/] -const DEEPSEEK_FIM_MODELS = new Set(['deepseek-chat']) +const DEEPSEEK_FIM_MODELS = new Set(['deepseek-chat', 'deepseek-coder']) const matchesCompletionModel = (modelName = '', patterns: RegExp[] = [], exactModels: Set = new Set()) => { const normalizedModelName = modelName.toLowerCase() diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js index 890f7b5c99..5f68ea3d87 100644 --- a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js @@ -1,4 +1,5 @@ import { HTTP_CONFIG, ERROR_MESSAGES, DEEPSEEK_CONFIG } from '../constants.js' +import { fetchWithTimeout } from '../utils/requestUtils.js' export function buildDeepSeekCompletionsUrl(baseUrl) { const normalizedBaseUrl = String(baseUrl || '').trim() @@ -60,7 +61,7 @@ export function buildDeepSeekFIMParams(fileContent, fimBuilder, metadata = {}) { * @param {string} baseUrl - 基础 URL * @returns {Promise} 补全文本 */ -export async function callDeepSeekAPI(prompt, suffix, config, apiKey, baseUrl) { +export async function callDeepSeekAPI(prompt, suffix, config, apiKey, baseUrl, signal) { const completionsUrl = buildDeepSeekCompletionsUrl(baseUrl) const requestBody = { @@ -74,14 +75,19 @@ export async function callDeepSeekAPI(prompt, suffix, config, apiKey, baseUrl) { stop: config.stopSequences } - const fetchResponse = await fetch(completionsUrl, { - method: HTTP_CONFIG.METHOD, - headers: { - 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, - Authorization: `Bearer ${apiKey}` + const fetchResponse = await fetchWithTimeout( + completionsUrl, + { + method: HTTP_CONFIG.METHOD, + headers: { + 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify(requestBody) }, - body: JSON.stringify(requestBody) - }) + HTTP_CONFIG.REQUEST_TIMEOUT_MS, + signal + ) if (!fetchResponse.ok) { const errorText = await fetchResponse.text() diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js index f28110d21a..3d705f3a8b 100644 --- a/packages/plugins/script/src/ai-completion/adapters/index.js +++ b/packages/plugins/script/src/ai-completion/adapters/index.js @@ -18,6 +18,8 @@ export function createCompletionHandler() { return async (params) => { try { + const requestSignal = params?.signal ?? null + // 1. 获取 AI 配置 const { completeModel, @@ -97,7 +99,8 @@ export function createCompletionHandler() { stopSequences: getStopSequences(ctx) }, apiKey, - baseUrl + baseUrl, + requestSignal ) } else { // ===== DeepSeek 流程(使用 FIM API) ===== @@ -117,7 +120,8 @@ export function createCompletionHandler() { stopSequences: getStopSequences(ctx) }, apiKey, - baseUrl + baseUrl, + requestSignal ) } diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js index 986083d390..ba5c6e2d0e 100644 --- a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js +++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js @@ -1,4 +1,5 @@ import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js' +import { fetchWithTimeout } from '../utils/requestUtils.js' /** * 构建 Qwen FIM prompt @@ -24,7 +25,7 @@ export function buildQwenFIMPrompt(fileContent, fimBuilder, metadata = {}) { * @param {string} baseUrl - 基础 URL * @returns {Promise} 补全文本 */ -export async function callQwenAPI(prompt, config, apiKey, baseUrl) { +export async function callQwenAPI(prompt, config, apiKey, baseUrl, signal) { // 构建完整的 Completions API URL const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}` @@ -39,14 +40,19 @@ export async function callQwenAPI(prompt, config, apiKey, baseUrl) { 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}` + const fetchResponse = await fetchWithTimeout( + completionsUrl, + { + method: HTTP_CONFIG.METHOD, + headers: { + 'Content-Type': HTTP_CONFIG.CONTENT_TYPE, + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify(requestBody) }, - body: JSON.stringify(requestBody) - }) + HTTP_CONFIG.REQUEST_TIMEOUT_MS, + signal + ) if (!fetchResponse.ok) { const errorText = await fetchResponse.text() diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js index e5761193a8..c0eb28a4ea 100644 --- a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js @@ -41,7 +41,7 @@ export class FIMPromptBuilder { const rawSuffix = fileContent.substring(cursorIndex + FIM_CONFIG.MARKERS.CURSOR.length) // 3. 分析光标上下文 - const cursorContext = this.analyzeCursorContext(rawPrefix, rawSuffix) + const cursorContext = this.analyzeCursorContext(rawPrefix) // 4. 构建完整的指令前缀 const instructionPrefix = this.buildInstructionPrefix(language, isComment, lowcodeContext, cursorContext) diff --git a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js index 37f0355980..5fc28720e8 100644 --- a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js +++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js @@ -5,6 +5,14 @@ const FACT_LIMITS = { HINT_TOKENS: 80 } +function asArray(value) { + return Array.isArray(value) ? value : [] +} + +function asRecord(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : {} +} + function limitList(items, max = FACT_LIMITS.ITEMS) { if (!Array.isArray(items)) { return [] @@ -123,7 +131,7 @@ function getValueType(value) { * @returns {Array} 格式化后的数据源 */ function formatDataSources(dataSource, hintContext) { - const candidates = dataSource + const candidates = asArray(dataSource) .filter((ds) => ds?.name) .map((ds) => ({ name: ds.name, @@ -169,7 +177,7 @@ function createCallableAccess(prefix, name, functionCode) { * @returns {Array} 格式化后的工具类 */ function formatUtils(utils, hintContext) { - const candidates = utils + const candidates = asArray(utils) .filter((util) => util?.name) .map((util) => { const formatted = { @@ -202,7 +210,7 @@ function formatUtils(utils, hintContext) { * @returns {Array} 格式化后的 bridge 信息 */ function formatBridge(bridge, hintContext) { - const candidates = bridge + const candidates = asArray(bridge) .filter((item) => item?.name) .map((item) => ({ name: item.name, @@ -221,7 +229,7 @@ function formatBridge(bridge, hintContext) { * @returns {Array} 格式化后的全局状态 */ function formatGlobalState(globalState, hintContext) { - const candidates = globalState + const candidates = asArray(globalState) .filter((store) => store?.id) .map((store) => ({ id: store.id, @@ -242,7 +250,7 @@ function formatGlobalState(globalState, hintContext) { * @returns {Array} 格式化后的状态 */ function formatState(state, hintContext) { - const candidates = Object.entries(state).map(([key, value]) => ({ + const candidates = Object.entries(asRecord(state)).map(([key, value]) => ({ name: key, accessPath: `this.state.${key}`, type: getValueType(value) @@ -259,7 +267,7 @@ function formatState(state, hintContext) { * @returns {Array} 格式化后的方法 */ function formatMethods(methods, hintContext) { - const candidates = Object.entries(methods).map(([key, value]) => ({ + const candidates = Object.entries(asRecord(methods)).map(([key, value]) => ({ name: key, accessPath: `this.${key}`, signature: value?.type === 'JSFunction' ? createCallableAccess('this.', key, value.value) : `this.${key}()`, @@ -287,7 +295,9 @@ function prioritizeSchemaKeys(keys, max, hintContext) { * @returns {Object|null} 格式化后的 schema */ function formatCurrentSchema(schema, hintContext) { - if (!schema) { + const normalizedSchema = schema && typeof schema === 'object' && !Array.isArray(schema) ? schema : null + + if (!normalizedSchema) { return { schema: null, truncated: { @@ -299,8 +309,8 @@ function formatCurrentSchema(schema, hintContext) { } const formatted = { - componentName: schema.componentName || 'Unknown', - ...(schema.ref && { ref: schema.ref, refAccess: `this.$('${schema.ref}')` }) + componentName: normalizedSchema.componentName || 'Unknown', + ...(normalizedSchema.ref && { ref: normalizedSchema.ref, refAccess: `this.$('${normalizedSchema.ref}')` }) } const truncated = { @@ -309,12 +319,14 @@ function formatCurrentSchema(schema, hintContext) { dynamicProps: 0 } - if (schema.props) { + const schemaProps = asRecord(normalizedSchema.props) + + if (Object.keys(schemaProps).length > 0) { const propKeys = [] const eventKeys = [] const dynamicPropKeys = [] - for (const [key, value] of Object.entries(schema.props)) { + for (const [key, value] of Object.entries(schemaProps)) { if (key.startsWith('on')) { eventKeys.push(key) } else { @@ -350,7 +362,8 @@ function formatCurrentSchema(schema, hintContext) { * @param {Object} metadata - 低代码平台元数据 * @returns {Object} 格式化的低代码上下文 */ -export function buildLowcodeContext(metadata, options = {}) { +export function buildLowcodeContext(metadata = {}, options = {}) { + const normalizedMetadata = metadata && typeof metadata === 'object' ? metadata : {} const { dataSource = [], utils = [], @@ -359,7 +372,7 @@ export function buildLowcodeContext(metadata, options = {}) { state = {}, methods = {}, currentSchema = null - } = metadata + } = normalizedMetadata const hintContext = buildHintContext(options.hintText) const formattedDataSource = formatDataSources(dataSource, hintContext) const formattedUtils = formatUtils(utils, hintContext) diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js index 1df262e627..62639bfcef 100644 --- a/packages/plugins/script/src/ai-completion/constants.js +++ b/packages/plugins/script/src/ai-completion/constants.js @@ -42,7 +42,7 @@ export const MODEL_CONFIG = { }, DEEPSEEK: { TYPE: 'deepseek', - COMPLETION_MODELS: ['deepseek-chat'], + COMPLETION_MODELS: ['deepseek-chat', 'deepseek-coder'], COMPLETION_MODEL_PATTERNS: [] }, UNKNOWN: { @@ -56,7 +56,8 @@ export const MODEL_CONFIG = { export const HTTP_CONFIG = { METHOD: 'POST', CONTENT_TYPE: 'application/json', - STREAM: false + STREAM: false, + REQUEST_TIMEOUT_MS: 15000 } /** diff --git a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js index 858d352eb0..d316c497b6 100644 --- a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js +++ b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js @@ -1,17 +1,19 @@ -import { getCommentState } from '../utils/contextAnalysis.js' +import { getCommentState, sanitizeStructuralText } from '../utils/contextAnalysis.js' /** * 检测光标是否在语句结束符后(分号后) */ -function isAfterStatementEnd(beforeCursor) { +function isAfterStatementEnd(beforeCursor, textBeforeCursor) { // 检查是否以分号结尾(忽略尾部空格) const trimmedEnd = beforeCursor.trimEnd() if (trimmedEnd.endsWith(';')) { + const structuralTextBeforeCursor = sanitizeStructuralText(textBeforeCursor) + // 排除 for 循环中的分号:for (let i = 0; i < 10; i++) // 检查是否在括号内 - const openParens = (beforeCursor.match(/\(/g) || []).length - const closeParens = (beforeCursor.match(/\)/g) || []).length + const openParens = (structuralTextBeforeCursor.match(/\(/g) || []).length + const closeParens = (structuralTextBeforeCursor.match(/\)/g) || []).length // 如果括号未闭合,说明可能在 for 循环中 if (openParens > closeParens) { @@ -70,7 +72,7 @@ export function shouldTriggerCompletion(params) { } // 3. 分号后不触发(语句已结束) - if (isAfterStatementEnd(beforeCursor)) { + if (isAfterStatementEnd(beforeCursor, textBeforeCursor)) { return false } diff --git a/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js b/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js index d78fb59da8..3e576cc25e 100644 --- a/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js +++ b/packages/plugins/script/src/ai-completion/utils/contextAnalysis.js @@ -1,9 +1,90 @@ const CONTROL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'catch', 'with']) +const REGEX_PREFIX_KEYWORDS = new Set([ + 'await', + 'case', + 'delete', + 'do', + 'else', + 'in', + 'instanceof', + 'new', + 'of', + 'return', + 'throw', + 'typeof', + 'void', + 'yield' +]) +const REGEX_PREFIX_CHARS = new Set([ + '(', + '[', + '{', + '=', + ':', + ',', + ';', + '!', + '?', + '~', + '+', + '-', + '*', + '%', + '^', + '&', + '|', + '<', + '>' +]) function maskChar(char) { return char === '\n' ? '\n' : ' ' } +function isIdentifierStart(char = '') { + return /[A-Za-z_$]/.test(char) +} + +function isIdentifierPart(char = '') { + return /[\w$]/.test(char) +} + +function readIdentifier(text = '', start = 0) { + let end = start + 1 + + while (end < text.length && isIdentifierPart(text[end])) { + end++ + } + + return { + value: text.slice(start, end), + end + } +} + +function isRegexStart(lastToken, nextChar = '') { + if (!nextChar || nextChar === '/' || nextChar === '*') { + return false + } + + if (!lastToken) { + return true + } + + if (lastToken.type === 'word') { + return REGEX_PREFIX_KEYWORDS.has(lastToken.value) + } + + return lastToken.type === 'char' ? REGEX_PREFIX_CHARS.has(lastToken.value) : false +} + +function createLiteralToken() { + return { + type: 'literal', + value: 'literal' + } +} + export function getCommentState(text = '') { let inSingleQuote = false let inDoubleQuote = false @@ -11,6 +92,9 @@ export function getCommentState(text = '') { let templateExpressionDepth = 0 let inBlockComment = false let inLineComment = false + let inRegex = false + let inRegexCharClass = false + let lastToken = null for (let i = 0; i < text.length; i++) { const char = text[i] @@ -31,11 +115,37 @@ export function getCommentState(text = '') { continue } + if (inRegex) { + if (char === '\\') { + i++ + continue + } + + if (inRegexCharClass) { + if (char === ']') { + inRegexCharClass = false + } + continue + } + + if (char === '[') { + inRegexCharClass = true + continue + } + + if (char === '/') { + inRegex = false + lastToken = createLiteralToken() + } + continue + } + if (inSingleQuote) { if (char === '\\') { i++ } else if (char === "'") { inSingleQuote = false + lastToken = createLiteralToken() } continue } @@ -45,6 +155,7 @@ export function getCommentState(text = '') { i++ } else if (char === '"') { inDoubleQuote = false + lastToken = createLiteralToken() } continue } @@ -54,9 +165,11 @@ export function getCommentState(text = '') { i++ } else if (char === '`') { inTemplate = false + lastToken = createLiteralToken() } else if (char === '$' && next === '{') { templateExpressionDepth = 1 i++ + lastToken = null } continue } @@ -73,6 +186,12 @@ export function getCommentState(text = '') { continue } + if (char === '/' && isRegexStart(lastToken, next)) { + inRegex = true + inRegexCharClass = false + continue + } + if (char === "'") { inSingleQuote = true continue @@ -91,8 +210,37 @@ export function getCommentState(text = '') { if (inTemplate && templateExpressionDepth > 0) { if (char === '{') { templateExpressionDepth++ + lastToken = { + type: 'char', + value: '{' + } } else if (char === '}') { templateExpressionDepth-- + lastToken = + templateExpressionDepth === 0 + ? createLiteralToken() + : { + type: 'char', + value: '}' + } + } + continue + } + + if (isIdentifierStart(char)) { + const identifier = readIdentifier(text, i) + lastToken = { + type: 'word', + value: identifier.value + } + i = identifier.end - 1 + continue + } + + if (!/\s/.test(char)) { + lastToken = { + type: 'char', + value: char } } } @@ -116,6 +264,9 @@ export function sanitizeStructuralText(text = '') { let templateExpressionDepth = 0 let inBlockComment = false let inLineComment = false + let inRegex = false + let inRegexCharClass = false + let lastToken = null for (let i = 0; i < text.length; i++) { const char = text[i] @@ -139,6 +290,33 @@ export function sanitizeStructuralText(text = '') { continue } + if (inRegex) { + sanitized.push(maskChar(char)) + if (char === '\\') { + sanitized.push(maskChar(next)) + i++ + continue + } + + if (inRegexCharClass) { + if (char === ']') { + inRegexCharClass = false + } + continue + } + + if (char === '[') { + inRegexCharClass = true + continue + } + + if (char === '/') { + inRegex = false + lastToken = createLiteralToken() + } + continue + } + if (inSingleQuote) { sanitized.push(maskChar(char)) if (char === '\\') { @@ -146,6 +324,7 @@ export function sanitizeStructuralText(text = '') { i++ } else if (char === "'") { inSingleQuote = false + lastToken = createLiteralToken() } continue } @@ -157,6 +336,7 @@ export function sanitizeStructuralText(text = '') { i++ } else if (char === '"') { inDoubleQuote = false + lastToken = createLiteralToken() } continue } @@ -168,10 +348,12 @@ export function sanitizeStructuralText(text = '') { i++ } else if (char === '`') { inTemplate = false + lastToken = createLiteralToken() } else if (char === '$' && next === '{') { sanitized.push(maskChar(next)) templateExpressionDepth = 1 i++ + lastToken = null } continue } @@ -192,6 +374,13 @@ export function sanitizeStructuralText(text = '') { continue } + if (char === '/' && isRegexStart(lastToken, next)) { + sanitized.push(maskChar(char)) + inRegex = true + inRegexCharClass = false + continue + } + if (char === "'") { sanitized.push(maskChar(char)) inSingleQuote = true @@ -214,21 +403,48 @@ export function sanitizeStructuralText(text = '') { if (char === '{') { templateExpressionDepth++ sanitized.push(char) + lastToken = { + type: 'char', + value: '{' + } continue } if (char === '}') { if (templateExpressionDepth === 1) { sanitized.push(maskChar(char)) + lastToken = createLiteralToken() } else { sanitized.push(char) + lastToken = { + type: 'char', + value: '}' + } } templateExpressionDepth-- continue } } + if (isIdentifierStart(char)) { + const identifier = readIdentifier(text, i) + sanitized.push(identifier.value) + lastToken = { + type: 'word', + value: identifier.value + } + i = identifier.end - 1 + continue + } + sanitized.push(char) + + if (!/\s/.test(char)) { + lastToken = { + type: 'char', + value: char + } + } } return sanitized.join('') diff --git a/packages/plugins/script/src/ai-completion/utils/requestUtils.js b/packages/plugins/script/src/ai-completion/utils/requestUtils.js new file mode 100644 index 0000000000..d3445982e1 --- /dev/null +++ b/packages/plugins/script/src/ai-completion/utils/requestUtils.js @@ -0,0 +1,57 @@ +import { HTTP_CONFIG } from '../constants.js' + +function createAbortError(message) { + const error = new Error(message) + error.name = 'AbortError' + return error +} + +export async function fetchWithTimeout(url, options = {}, timeoutMs = HTTP_CONFIG.REQUEST_TIMEOUT_MS, externalSignal) { + const controller = new AbortController() + let timedOut = false + let timeoutId + + const abortFromExternalSignal = () => { + if (!controller.signal.aborted) { + controller.abort() + } + } + + if (Number.isFinite(timeoutMs) && timeoutMs > 0) { + timeoutId = setTimeout(() => { + timedOut = true + controller.abort() + }, timeoutMs) + } + + if (externalSignal) { + if (externalSignal.aborted) { + controller.abort() + } else { + externalSignal.addEventListener('abort', abortFromExternalSignal, { once: true }) + } + } + + try { + return await fetch(url, { + ...options, + signal: controller.signal + }) + } catch (error) { + if (controller.signal.aborted && timedOut) { + throw createAbortError(`Request timed out after ${timeoutMs}ms`) + } + + if (controller.signal.aborted && externalSignal?.aborted) { + throw createAbortError('Request aborted') + } + + throw error + } finally { + if (timeoutId) { + clearTimeout(timeoutId) + } + + externalSignal?.removeEventListener('abort', abortFromExternalSignal) + } +} diff --git a/packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs b/packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs deleted file mode 100644 index 338f6698c2..0000000000 --- a/packages/register/vite.config.ts.timestamp-1776240075469-6e177b92731d7.mjs +++ /dev/null @@ -1,29 +0,0 @@ -// vite.config.ts -import { defineConfig } from 'file:///E:/LS_WorkSpace/web/tiny-engine/node_modules/.pnpm/vite@5.4.21_@types+node@22.19.7_less@4.5.1/node_modules/vite/dist/node/index.js' -import path from 'path' -import dts from 'file:///E:/LS_WorkSpace/web/tiny-engine/node_modules/.pnpm/vite-plugin-dts@4.5.4_@type_1710e030ba0c4aa7433fc0cb72d4e195/node_modules/vite-plugin-dts/dist/index.mjs' -var __vite_injected_original_dirname = 'E:\\LS_WorkSpace\\web\\tiny-engine\\packages\\register' -var vite_config_default = defineConfig({ - plugins: [ - dts({ - tsconfigPath: path.resolve(__vite_injected_original_dirname, './tsconfig.json'), - rollupTypes: true - }) - ], - publicDir: false, - resolve: {}, - build: { - sourcemap: true, - lib: { - entry: path.resolve(__vite_injected_original_dirname, './src/index.ts'), - name: 'tiny-engine-meta-register', - fileName: (_format, entryName) => `${entryName}.js`, - formats: ['es'] - }, - rollupOptions: { - external: ['vue', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/] - } - } -}) -export { vite_config_default as default } -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJFOlxcXFxMU19Xb3JrU3BhY2VcXFxcd2ViXFxcXHRpbnktZW5naW5lXFxcXHBhY2thZ2VzXFxcXHJlZ2lzdGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJFOlxcXFxMU19Xb3JrU3BhY2VcXFxcd2ViXFxcXHRpbnktZW5naW5lXFxcXHBhY2thZ2VzXFxcXHJlZ2lzdGVyXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9FOi9MU19Xb3JrU3BhY2Uvd2ViL3RpbnktZW5naW5lL3BhY2thZ2VzL3JlZ2lzdGVyL3ZpdGUuY29uZmlnLnRzXCI7LyoqXHJcbiAqIENvcHlyaWdodCAoYykgMjAyMyAtIHByZXNlbnQgVGlueUVuZ2luZSBBdXRob3JzLlxyXG4gKiBDb3B5cmlnaHQgKGMpIDIwMjMgLSBwcmVzZW50IEh1YXdlaSBDbG91ZCBDb21wdXRpbmcgVGVjaG5vbG9naWVzIENvLiwgTHRkLlxyXG4gKlxyXG4gKiBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSBhbiBNSVQtc3R5bGUgbGljZW5zZS5cclxuICpcclxuICogVEhFIE9QRU4gU09VUkNFIFNPRlRXQVJFIElOIFRISVMgUFJPRFVDVCBJUyBESVNUUklCVVRFRCBJTiBUSEUgSE9QRSBUSEFUIElUIFdJTEwgQkUgVVNFRlVMLFxyXG4gKiBCVVQgV0lUSE9VVCBBTlkgV0FSUkFOVFksIFdJVEhPVVQgRVZFTiBUSEUgSU1QTElFRCBXQVJSQU5UWSBPRiBNRVJDSEFOVEFCSUxJVFkgT1IgRklUTkVTUyBGT1JcclxuICogQSBQQVJUSUNVTEFSIFBVUlBPU0UuIFNFRSBUSEUgQVBQTElDQUJMRSBMSUNFTlNFUyBGT1IgTU9SRSBERVRBSUxTLlxyXG4gKlxyXG4gKi9cclxuXHJcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnXHJcbmltcG9ydCBwYXRoIGZyb20gJ3BhdGgnXHJcbmltcG9ydCBkdHMgZnJvbSAndml0ZS1wbHVnaW4tZHRzJ1xyXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gIHBsdWdpbnM6IFtcclxuICAgIGR0cyh7XHJcbiAgICAgIHRzY29uZmlnUGF0aDogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4vdHNjb25maWcuanNvbicpLFxyXG4gICAgICByb2xsdXBUeXBlczogdHJ1ZVxyXG4gICAgfSlcclxuICBdLFxyXG4gIHB1YmxpY0RpcjogZmFsc2UsXHJcbiAgcmVzb2x2ZToge30sXHJcbiAgYnVpbGQ6IHtcclxuICAgIHNvdXJjZW1hcDogdHJ1ZSxcclxuICAgIGxpYjoge1xyXG4gICAgICBlbnRyeTogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4vc3JjL2luZGV4LnRzJyksXHJcbiAgICAgIG5hbWU6ICd0aW55LWVuZ2luZS1tZXRhLXJlZ2lzdGVyJyxcclxuICAgICAgZmlsZU5hbWU6IChfZm9ybWF0LCBlbnRyeU5hbWUpID0+IGAke2VudHJ5TmFtZX0uanNgLFxyXG4gICAgICBmb3JtYXRzOiBbJ2VzJ11cclxuICAgIH0sXHJcbiAgICByb2xsdXBPcHRpb25zOiB7XHJcbiAgICAgIGV4dGVybmFsOiBbJ3Z1ZScsIC9Ab3BlbnRpbnlcXC90aW55LWVuZ2luZS4qLywgL0BvcGVudGlueVxcL3Z1ZS4qL11cclxuICAgIH1cclxuICB9XHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFZQSxTQUFTLG9CQUFvQjtBQUM3QixPQUFPLFVBQVU7QUFDakIsT0FBTyxTQUFTO0FBZGhCLElBQU0sbUNBQW1DO0FBZ0J6QyxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsTUFDRixjQUFjLEtBQUssUUFBUSxrQ0FBVyxpQkFBaUI7QUFBQSxNQUN2RCxhQUFhO0FBQUEsSUFDZixDQUFDO0FBQUEsRUFDSDtBQUFBLEVBQ0EsV0FBVztBQUFBLEVBQ1gsU0FBUyxDQUFDO0FBQUEsRUFDVixPQUFPO0FBQUEsSUFDTCxXQUFXO0FBQUEsSUFDWCxLQUFLO0FBQUEsTUFDSCxPQUFPLEtBQUssUUFBUSxrQ0FBVyxnQkFBZ0I7QUFBQSxNQUMvQyxNQUFNO0FBQUEsTUFDTixVQUFVLENBQUMsU0FBUyxjQUFjLEdBQUcsU0FBUztBQUFBLE1BQzlDLFNBQVMsQ0FBQyxJQUFJO0FBQUEsSUFDaEI7QUFBQSxJQUNBLGVBQWU7QUFBQSxNQUNiLFVBQVUsQ0FBQyxPQUFPLDRCQUE0QixrQkFBa0I7QUFBQSxJQUNsRTtBQUFBLEVBQ0Y7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=