@@ -172,8 +186,8 @@ const emit = defineEmits(['close'])
const {
robotSettingState,
saveRobotSettingState,
- getAllAvailableModels,
getCompactModels,
+ getNonCodeCompletionModels,
addCustomService,
updateService,
deleteService,
@@ -196,9 +210,9 @@ const state = reactive({
editingService: undefined as ModelService | undefined
})
-// 获取所有可用模型选项
+// 获取所有可用模型选项(排除代码补全专用模型)
const allModelOptions = computed(() => {
- return getAllAvailableModels().map((model) => ({
+ return getNonCodeCompletionModels().map((model) => ({
label: model.displayLabel,
value: model.value,
capabilities: model.capabilities
@@ -207,10 +221,14 @@ const allModelOptions = computed(() => {
// 获取快速模型选项
const compactModelOptions = computed(() => {
- return getCompactModels().map((model) => ({
+ const models = getCompactModels().map((model) => ({
label: model.displayLabel,
- value: model.value
+ value: model.value,
+ capabilities: model.capabilities,
+ serviceName: model.serviceName
}))
+
+ return models.sort((a, b) => a.serviceName.localeCompare(b.serviceName, 'zh-CN'))
})
// 获取当前选择的默认模型信息
@@ -270,8 +288,8 @@ const addService = () => {
state.showServiceDialog = true
}
-const editService = (service: ModelService) => {
- state.editingService = JSON.parse(JSON.stringify(service))
+const editService = (service: any) => {
+ state.editingService = JSON.parse(JSON.stringify(service)) as ModelService
state.showServiceDialog = true
}
diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts
index 01908a89a0..da8e174ec6 100644
--- a/packages/plugins/robot/src/composables/core/useConfig.ts
+++ b/packages/plugins/robot/src/composables/core/useConfig.ts
@@ -318,9 +318,19 @@ const getAllAvailableModels = () => {
)
}
-// 获取快速模型列表
+// 获取快速模型列表(包含 compact 或 codeCompletion 的模型)
const getCompactModels = () => {
- return getAllAvailableModels().filter((model) => model.capabilities?.compact)
+ return getAllAvailableModels().filter((model) => model.capabilities?.compact || model.capabilities?.codeCompletion)
+}
+
+// 获取代码补全优化模型列表
+const getCodeCompletionModels = () => {
+ return getAllAvailableModels().filter((model) => model.capabilities?.codeCompletion)
+}
+
+// 获取非代码补全模型列表(用于默认助手模型)
+const getNonCodeCompletionModels = () => {
+ return getAllAvailableModels().filter((model) => !model.capabilities?.codeCompletion)
}
const updateThinkingState = (value: boolean) => {
@@ -456,6 +466,8 @@ export default () => {
getModelCapabilities,
getAllAvailableModels,
getCompactModels,
+ getCodeCompletionModels, // 代码补全模型列表
+ getNonCodeCompletionModels, // 非代码补全模型列表
getSelectedModelInfo, // 对话模型信息
getSelectedQuickModelInfo, // 快速模型信息
diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts
index 4458339325..1e471cb54b 100644
--- a/packages/plugins/robot/src/constants/model-config.ts
+++ b/packages/plugins/robot/src/constants/model-config.ts
@@ -57,6 +57,7 @@ export const DEFAULT_LLM_MODELS = [
name: 'qwen3-coder-plus',
capabilities: {
toolCalling: true,
+ codeCompletion: true,
reasoning: reasoningExtraBody,
jsonOutput: bailianJsonOutputExtraBody
}
@@ -81,23 +82,24 @@ export const DEFAULT_LLM_MODELS = [
}
},
{
- label: 'Qwen Coder编程模型(Flash)',
- name: 'qwen3-coder-flash',
+ label: 'Qwen2.5 Coder编程模型-最快响应',
+ name: 'qwen-coder-turbo-latest',
capabilities: {
toolCalling: true,
compact: true,
+ codeCompletion: true,
jsonOutput: bailianJsonOutputExtraBody
}
},
{
- label: 'Qwen3(14b)',
- name: 'qwen3-14b',
- capabilities: { compact: true, toolCalling: true, jsonOutput: bailianJsonOutputExtraBody }
- },
- {
- label: 'Qwen3(8b)',
- name: 'qwen3-8b',
- capabilities: { compact: true, toolCalling: true, jsonOutput: bailianJsonOutputExtraBody }
+ label: 'Qwen2.5 Coder编程模型(32B)',
+ name: 'qwen2.5-coder-32b-instruct',
+ capabilities: {
+ toolCalling: true,
+ compact: true,
+ codeCompletion: true,
+ jsonOutput: bailianJsonOutputExtraBody
+ }
}
]
},
@@ -120,6 +122,16 @@ export const DEFAULT_LLM_MODELS = [
},
jsonOutput: jsonOutputExtraBody
}
+ },
+ {
+ label: 'Deepseek Coder编程模型',
+ name: 'deepseek-coder',
+ capabilities: {
+ toolCalling: true,
+ compact: true,
+ codeCompletion: true,
+ jsonOutput: jsonOutputExtraBody
+ }
}
]
}
diff --git a/packages/plugins/robot/src/types/setting.types.ts b/packages/plugins/robot/src/types/setting.types.ts
index 9699afedc6..40420b08b3 100644
--- a/packages/plugins/robot/src/types/setting.types.ts
+++ b/packages/plugins/robot/src/types/setting.types.ts
@@ -30,6 +30,7 @@ export interface ModelConfig {
vision?: boolean
reasoning?: boolean | Capability
compact?: boolean
+ codeCompletion?: boolean
jsonOutput?: boolean | Capability
}
}
diff --git a/packages/plugins/script/meta.js b/packages/plugins/script/meta.js
index 6991805f54..779a029fb4 100644
--- a/packages/plugins/script/meta.js
+++ b/packages/plugins/script/meta.js
@@ -6,7 +6,8 @@ export default {
width: 600,
widthResizable: true,
options: {
- enableAICompletion: true
+ aiCompletionEnabled: true
+ // aiCompletionTrigger: 'onIdle' // 可选:触发模式 'onIdle'(默认) | 'onTyping' | 'onDemand'
},
confirm: 'close' // 当点击插件栏切换或关闭前是否需要确认, 会调用插件中confirm值指定的方法,e.g. 此处指向 close方法,会调用插件的close方法执行确认逻辑
}
diff --git a/packages/plugins/script/package.json b/packages/plugins/script/package.json
index bca9fbaf82..207278bd3a 100644
--- a/packages/plugins/script/package.json
+++ b/packages/plugins/script/package.json
@@ -27,7 +27,8 @@
"dependencies": {
"@opentiny/tiny-engine-common": "workspace:*",
"@opentiny/tiny-engine-meta-register": "workspace:*",
- "@opentiny/tiny-engine-utils": "workspace:*"
+ "@opentiny/tiny-engine-utils": "workspace:*",
+ "monacopilot": "^1.2.12"
},
"devDependencies": {
"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",
diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue
index de663b7112..836dde0926 100644
--- a/packages/plugins/script/src/Main.vue
+++ b/packages/plugins/script/src/Main.vue
@@ -34,11 +34,14 @@
/* metaService: engine.plugins.pagecontroller.Main */
import { onBeforeUnmount, reactive, provide } from 'vue'
import { Button } from '@opentiny/vue'
+import { registerCompletion, type CompletionRegistration, type RegisterCompletionOptions } from 'monacopilot'
import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common'
-import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register'
+import { useHelp, useLayout, getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import { initCompletion } from '@opentiny/tiny-engine-common/js/completion'
import { initLinter } from '@opentiny/tiny-engine-common/js/linter'
import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method'
+import { createCompletionHandler } from './ai-completion/adapters/index'
+import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger'
export const api = {
saveMethod,
@@ -59,13 +62,17 @@ export default {
}
},
emits: ['close'],
- setup(props, { emit }) {
+ setup(_props, { emit }) {
const docsUrl = useHelp().getDocsUrl('script')
const docsContent = '同一页面/区块的添加事件会统一保存到对应的页面JS中。'
const { state, monaco, change, close, saveMethods } = useMethod({ emit })
const { PLUGIN_NAME } = useLayout()
+ type RequestHandler = NonNullable
+ type TriggerMode = NonNullable
+ let completion: CompletionRegistration | null = null
+
const panelState = reactive({
emitEvent: emit
})
@@ -101,24 +108,69 @@ export default {
wordWrapStrategy: 'advanced'
}
- const editorDidMount = (editor) => {
- if (!monaco.value) {
- return
+ const editorDidMount = (editor: any) => {
+ const monacoRef = monaco as any
+ if (!monacoRef.value) return
+
+ // 保留原有的 Lowcode API 提示
+ state.completionProvider = initCompletion(
+ monacoRef.value.getMonaco(),
+ monacoRef.value.getEditor()?.getModel()
+ ) as any
+
+ // 保留原有的 ESLint
+ state.linterWorker = initLinter(editor, monacoRef.value.getMonaco(), state) as any
+
+ const { aiCompletionEnabled, aiCompletionTrigger = 'onIdle' } =
+ getMergeMeta('engine.plugins.pagecontroller')?.options || {}
+
+ if (aiCompletionEnabled) {
+ try {
+ const monaco = monacoRef.value.getMonaco()
+ const editor = monacoRef.value.getEditor()
+
+ completion = registerCompletion(monaco, editor, {
+ language: 'javascript',
+ filename: 'page.js',
+ maxContextLines: 50,
+ enableCaching: true,
+ allowFollowUpCompletions: false,
+ trigger: aiCompletionTrigger as TriggerMode,
+ triggerIf: ({ text, position }) => {
+ return shouldTriggerCompletion({
+ text,
+ position
+ })
+ },
+ requestHandler: createCompletionHandler() as RequestHandler
+ })
+
+ monaco.editor.addEditorAction({
+ id: 'monacopilot.triggerCompletion',
+ label: 'Complete Code',
+ contextMenuGroupId: 'navigation',
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Space],
+ run: () => {
+ completion!.trigger()
+ }
+ })
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('❌ AI 补全注册失败:', error)
+ }
}
-
- // Lowcode API 提示
- state.completionProvider = initCompletion(monaco.value.getMonaco(), monaco.value.getEditor()?.getModel())
-
- // 初始化 ESLint worker
- state.linterWorker = initLinter(editor, monaco.value.getMonaco(), state)
}
onBeforeUnmount(() => {
- state.completionProvider?.forEach((provider) => {
- provider.dispose()
+ // 清理 AI 补全
+ if (completion) {
+ completion.deregister()
+ }
+ ;(state.completionProvider as any)?.forEach?.((provider: any) => {
+ provider?.dispose?.()
})
// 终止 ESLint worker
- state.linterWorker?.terminate?.()
+ ;(state.linterWorker as any)?.terminate?.()
})
return {
diff --git a/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js
new file mode 100644
index 0000000000..bcba24a8c5
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/adapters/deepseekAdapter.js
@@ -0,0 +1,57 @@
+import { SYSTEM_BASE_PROMPT, createUserPrompt } from '../prompts/templates.js'
+import { API_ENDPOINTS, HTTP_CONFIG } from '../constants.js'
+
+/**
+ * 构建 DeepSeek Chat 格式的 messages
+ * @param {string} context - 上下文信息
+ * @param {string} instruction - 指令
+ * @param {string} fileContent - 文件内容
+ * @returns {{ messages: Array, cursorContext: null }} Messages 和上下文
+ */
+export function buildDeepSeekMessages(context, instruction, fileContent) {
+ const systemPrompt = `${context}\n\n${SYSTEM_BASE_PROMPT}`
+ const userPrompt = createUserPrompt(instruction, fileContent)
+
+ return {
+ messages: [
+ {
+ role: 'system',
+ content: systemPrompt
+ },
+ {
+ role: 'user',
+ content: userPrompt
+ }
+ ],
+ cursorContext: null
+ }
+}
+
+/**
+ * 调用 DeepSeek Chat API
+ * @param {Array} messages - Messages 数组
+ * @param {Object} config - 配置对象
+ * @param {string} apiKey - API 密钥
+ * @param {string} baseUrl - 基础 URL
+ * @param {Object} httpClient - HTTP 客户端
+ * @returns {Promise} 补全文本
+ */
+export async function callDeepSeekAPI(messages, config, apiKey, baseUrl, httpClient) {
+ const response = await httpClient.post(
+ API_ENDPOINTS.CHAT_COMPLETIONS,
+ {
+ model: config.model,
+ messages,
+ baseUrl,
+ stream: HTTP_CONFIG.STREAM
+ },
+ {
+ headers: {
+ 'Content-Type': HTTP_CONFIG.CONTENT_TYPE,
+ Authorization: `Bearer ${apiKey || ''}`
+ }
+ }
+ )
+
+ return response?.choices?.[0]?.message?.content
+}
diff --git a/packages/plugins/script/src/ai-completion/adapters/index.js b/packages/plugins/script/src/ai-completion/adapters/index.js
new file mode 100644
index 0000000000..669d349f11
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/adapters/index.js
@@ -0,0 +1,109 @@
+import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
+import { createSmartPrompt } from '../builders/promptBuilder.js'
+import { FIMPromptBuilder } from '../builders/fimPromptBuilder.js'
+import { detectModelType, calculateTokens, getStopSequences } from '../utils/modelUtils.js'
+import { cleanCompletion, buildLowcodeMetadata } from '../utils/completionUtils.js'
+import { buildQwenMessages, callQwenAPI } from './qwenAdapter.js'
+import { buildDeepSeekMessages, callDeepSeekAPI } from './deepseekAdapter.js'
+import { QWEN_CONFIG, DEEPSEEK_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js'
+
+/**
+ * 创建请求处理器
+ * @returns {Function} 请求处理函数
+ */
+export function createCompletionHandler() {
+ const fimBuilder = new FIMPromptBuilder(QWEN_CONFIG)
+
+ return async (params) => {
+ try {
+ // 1. 获取 AI 配置
+ const { completeModel, apiKey, baseUrl } = getMetaApi(META_SERVICE.Robot).getSelectedQuickModelInfo() || {}
+
+ if (!completeModel || !apiKey || !baseUrl) {
+ return {
+ completion: null,
+ error: ERROR_MESSAGES.CONFIG_MISSING
+ }
+ }
+
+ // 2. 提取代码上下文
+ const {
+ textBeforeCursor = '',
+ textAfterCursor = '',
+ language = DEFAULTS.LANGUAGE,
+ filename
+ } = params.body?.completionMetadata || {}
+
+ // 3. 构建低代码元数据和 prompt
+ const lowcodeMetadata = buildLowcodeMetadata()
+ const { context, instruction, fileContent } = createSmartPrompt({
+ textBeforeCursor,
+ textAfterCursor,
+ language,
+ filename,
+ technologies: DEFAULTS.TECHNOLOGIES,
+ lowcodeMetadata
+ })
+
+ // 4. 检测模型类型
+ const modelType = detectModelType(completeModel)
+
+ let completionText = null
+ let cursorContext = null
+
+ // 5. 根据模型类型调用不同的 API
+ if (modelType === MODEL_CONFIG.QWEN.TYPE) {
+ // ===== Qwen 流程 =====
+ const { messages, cursorContext: ctx } = buildQwenMessages(fileContent, fimBuilder)
+ cursorContext = ctx
+
+ const config = {
+ model: completeModel,
+ maxTokens: calculateTokens(cursorContext),
+ stopSequences: getStopSequences(cursorContext, MODEL_CONFIG.QWEN.TYPE)
+ }
+
+ completionText = await callQwenAPI(messages, config, apiKey, baseUrl)
+ } else {
+ // ===== DeepSeek 流程(默认) =====
+ const { messages } = buildDeepSeekMessages(context, instruction, fileContent)
+
+ // DeepSeek 使用 Chat API,也需要 stop 序列
+ const config = {
+ model: completeModel,
+ stopSequences: getStopSequences(null, MODEL_CONFIG.DEEPSEEK.TYPE)
+ }
+ const httpClient = getMetaApi(META_SERVICE.Http)
+
+ // 构建 DeepSeek FIM 端点:将 /v1 替换为 /beta
+ const completionBaseUrl = baseUrl.replace(DEEPSEEK_CONFIG.PATH_REPLACE, DEEPSEEK_CONFIG.COMPLETION_PATH)
+
+ completionText = await callDeepSeekAPI(messages, config, apiKey, completionBaseUrl, httpClient)
+ }
+
+ // 6. 处理补全结果
+ if (completionText) {
+ completionText = completionText.trim()
+
+ completionText = cleanCompletion(completionText, modelType, cursorContext)
+
+ return {
+ completion: completionText,
+ error: null
+ }
+ }
+
+ return {
+ completion: null,
+ error: ERROR_MESSAGES.NO_COMPLETION
+ }
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('❌ AI 补全请求失败:', error)
+ return {
+ completion: null,
+ error: error.message || ERROR_MESSAGES.REQUEST_FAILED
+ }
+ }
+ }
+}
diff --git a/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js
new file mode 100644
index 0000000000..7f509d5fb3
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js
@@ -0,0 +1,62 @@
+import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js'
+
+/**
+ * 构建 Qwen FIM 格式的 messages
+ * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记)
+ * @param {Object} fimBuilder - FIM 构建器实例
+ * @returns {{ messages: Array, cursorContext: Object }} Messages 和上下文
+ */
+export function buildQwenMessages(fileContent, fimBuilder) {
+ const { fimPrompt, cursorContext } = fimBuilder.buildOptimizedFIMPrompt(fileContent)
+
+ return {
+ messages: [
+ {
+ role: 'user',
+ content: fimPrompt
+ }
+ ],
+ cursorContext
+ }
+}
+
+/**
+ * 调用 Qwen Completions API
+ * @param {Array} messages - Messages 数组
+ * @param {Object} config - 配置对象
+ * @param {string} apiKey - API 密钥
+ * @param {string} baseUrl - 基础 URL
+ * @returns {Promise} 补全文本
+ */
+export async function callQwenAPI(messages, config, apiKey, baseUrl) {
+ // 构建完整的 Completions API URL
+ const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}`
+
+ const requestBody = {
+ model: config.model,
+ prompt: messages[0].content, // FIM prompt
+ max_tokens: config.maxTokens,
+ temperature: QWEN_CONFIG.DEFAULT_TEMPERATURE,
+ top_p: QWEN_CONFIG.TOP_P,
+ stream: HTTP_CONFIG.STREAM,
+ stop: config.stopSequences,
+ presence_penalty: QWEN_CONFIG.PRESENCE_PENALTY
+ }
+
+ const fetchResponse = await fetch(completionsUrl, {
+ method: HTTP_CONFIG.METHOD,
+ headers: {
+ 'Content-Type': HTTP_CONFIG.CONTENT_TYPE,
+ Authorization: `Bearer ${apiKey}`
+ },
+ body: JSON.stringify(requestBody)
+ })
+
+ if (!fetchResponse.ok) {
+ const errorText = await fetchResponse.text()
+ throw new Error(`${ERROR_MESSAGES.QWEN_API_ERROR} ${fetchResponse.status}: ${errorText}`)
+ }
+
+ const response = await fetchResponse.json()
+ return response?.choices?.[0]?.text
+}
diff --git a/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js
new file mode 100644
index 0000000000..00f9cb0ca1
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/builders/fimPromptBuilder.js
@@ -0,0 +1,172 @@
+import { FIM_CONFIG } from '../constants.js'
+
+/**
+ * FIM (Fill-In-the-Middle) Prompt 构建器
+ * 用于处理 FIM 格式的代码补全
+ */
+export class FIMPromptBuilder {
+ constructor(config) {
+ this.config = config
+ }
+
+ /**
+ * 构建优化的 FIM (Fill In the Middle) Prompt
+ * @param {string} fileContent - 文件内容,包含 [CURSOR] 标记
+ * @returns {{ fimPrompt: string, cursorContext: Object }} FIM prompt 和上下文信息
+ */
+ buildOptimizedFIMPrompt(fileContent) {
+ // 1. 清理元信息注释
+ let cleanedContent = this.cleanMetaInfo(fileContent)
+
+ // 2. 查找光标位置
+ const cursorIndex = cleanedContent.indexOf(FIM_CONFIG.MARKERS.CURSOR)
+
+ if (cursorIndex === -1) {
+ return {
+ fimPrompt: `${FIM_CONFIG.MARKERS.PREFIX}${cleanedContent}${FIM_CONFIG.MARKERS.SUFFIX}`,
+ cursorContext: { type: 'unknown', hasPrefix: true, hasSuffix: false }
+ }
+ }
+
+ // 3. 分割前缀和后缀
+ const prefix = cleanedContent.substring(0, cursorIndex)
+ const suffix = cleanedContent.substring(cursorIndex + FIM_CONFIG.MARKERS.CURSOR.length)
+
+ // 4. 分析光标上下文
+ const cursorContext = this.analyzeCursorContext(prefix, suffix)
+
+ // 5. 优化前缀和后缀
+ const optimizedPrefix = this.optimizePrefix(prefix)
+ const optimizedSuffix = this.optimizeSuffix(suffix)
+
+ // 6. 构建 FIM prompt
+ let fimPrompt
+ if (optimizedSuffix.trim().length > 0) {
+ // 有后缀:使用 prefix + suffix + middle 模式
+ fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${optimizedPrefix}${FIM_CONFIG.MARKERS.SUFFIX}${optimizedSuffix}${FIM_CONFIG.MARKERS.MIDDLE}`
+ } else {
+ // 无后缀:只使用 prefix + suffix 模式
+ fimPrompt = `${FIM_CONFIG.MARKERS.PREFIX}${optimizedPrefix}${FIM_CONFIG.MARKERS.SUFFIX}`
+ }
+
+ return { fimPrompt, cursorContext }
+ }
+
+ /**
+ * 清理元信息注释
+ * @param {string} content - 原始内容
+ * @returns {string} 清理后的内容
+ */
+ cleanMetaInfo(content) {
+ return content.replace(FIM_CONFIG.META_INFO_PATTERN, '')
+ }
+
+ /**
+ * 分析光标上下文
+ * @param {string} prefix - 前缀代码
+ * @param {string} suffix - 后缀代码
+ * @returns {Object} 上下文信息
+ */
+ analyzeCursorContext(prefix, suffix) {
+ const context = {
+ type: 'unknown',
+ hasPrefix: prefix.trim().length > 0,
+ hasSuffix: suffix.trim().length > 0,
+ inFunction: false,
+ inClass: false,
+ inObject: false,
+ inArray: false,
+ needsExpression: false,
+ needsStatement: false
+ }
+
+ // 分析前缀最后几个字符
+ const prefixTrimmed = prefix.trimEnd()
+
+ // 检测是否在表达式中
+ if (/[=+\-*/%<>!&|,([]$/.test(prefixTrimmed)) {
+ context.needsExpression = true
+ context.type = 'expression'
+ }
+ // 检测是否在语句开始
+ else if (/[{;]\s*$/.test(prefixTrimmed) || prefixTrimmed.length === 0) {
+ context.needsStatement = true
+ context.type = 'statement'
+ }
+ // 检测是否在对象字面量中
+ else if (/{\s*$/.test(prefixTrimmed) || /,\s*$/.test(prefixTrimmed)) {
+ context.inObject = true
+ context.type = 'object-property'
+ }
+
+ // 检测作用域
+ const functionMatch = prefix.match(/function\s+\w+|const\s+\w+\s*=.*=>|async\s+function/g)
+ const classMatch = prefix.match(/class\s+\w+/g)
+
+ context.inFunction = functionMatch && functionMatch.length > 0
+ context.inClass = classMatch && classMatch.length > 0
+
+ return context
+ }
+
+ /**
+ * 优化前缀(限制上下文长度)
+ * @param {string} prefix - 原始前缀
+ * @returns {string} 优化后的前缀
+ */
+ optimizePrefix(prefix) {
+ const MAX_PREFIX_LINES = this.config.FIM.MAX_PREFIX_LINES
+ const lines = prefix.split('\n')
+
+ if (lines.length <= MAX_PREFIX_LINES) {
+ return prefix
+ }
+
+ // 保留最后 N 行
+ return lines.slice(-MAX_PREFIX_LINES).join('\n')
+ }
+
+ /**
+ * 优化后缀(限制上下文长度 + 智能截断)
+ * @param {string} suffix - 原始后缀
+ * @returns {string} 优化后的后缀
+ */
+ optimizeSuffix(suffix) {
+ const MAX_SUFFIX_LINES = this.config.FIM.MAX_SUFFIX_LINES
+ const lines = suffix.split('\n')
+
+ // 智能截断:找到下一个函数/类定义的位置
+ let cutoffIndex = lines.length
+ for (let i = 0; i < Math.min(lines.length, MAX_SUFFIX_LINES); i++) {
+ const line = lines[i].trim()
+
+ // 遇到新的函数/类定义,在此处截断
+ if (
+ line.startsWith('function ') ||
+ line.startsWith('class ') ||
+ (line.startsWith('const ') && line.includes('=>')) ||
+ line.startsWith('export ') ||
+ line.startsWith('import ')
+ ) {
+ cutoffIndex = i
+ break
+ }
+
+ // 遇到闭合的大括号(可能是当前函数/对象的结束)
+ if (line === '}' || line === '};') {
+ cutoffIndex = i + 1 // 包含这个闭合括号
+ break
+ }
+ }
+
+ // 取较小值:要么是智能截断位置,要么是最大行数
+ const finalLines = Math.min(cutoffIndex, MAX_SUFFIX_LINES)
+
+ if (lines.length <= finalLines) {
+ return suffix
+ }
+
+ // 保留前 N 行
+ return lines.slice(0, finalLines).join('\n')
+ }
+}
diff --git a/packages/plugins/script/src/ai-completion/builders/index.js b/packages/plugins/script/src/ai-completion/builders/index.js
new file mode 100644
index 0000000000..bba666e7c3
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/builders/index.js
@@ -0,0 +1,3 @@
+export { createSmartPrompt } from './promptBuilder.js'
+export { FIMPromptBuilder } from './fimPromptBuilder.js'
+export { buildLowcodeContext } from './lowcodeContextBuilder.js'
diff --git a/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js
new file mode 100644
index 0000000000..754be23d9e
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/builders/lowcodeContextBuilder.js
@@ -0,0 +1,270 @@
+/**
+ * 格式化数据源信息
+ * @param {Array} dataSource - 数据源数组
+ * @returns {Array} 格式化后的数据源
+ */
+function formatDataSources(dataSource) {
+ return dataSource.map((ds) => ({
+ name: ds.name,
+ type: ds.type || 'unknown',
+ description: ds.description || `Data source: ${ds.name}`,
+ // 只保留关键信息,避免上下文过大
+ ...(ds.options && { options: ds.options })
+ }))
+}
+
+/**
+ * 从函数代码中提取函数签名
+ * @param {string} functionCode - 函数代码字符串
+ * @returns {string} 函数签名
+ */
+function extractFunctionSignature(functionCode) {
+ if (!functionCode) return 'function()'
+
+ // 匹配函数声明: function name(params)
+ const funcMatch = functionCode.match(/function\s+(\w+)?\s*\(([^)]*)\)/)
+ if (funcMatch) {
+ const name = funcMatch[1] || 'anonymous'
+ const params = funcMatch[2].trim()
+ return `function ${name}(${params})`
+ }
+
+ // 匹配箭头函数: (params) => 或 params =>
+ const arrowMatch = functionCode.match(/(?:\(([^)]*)\)|(\w+))\s*=>/)
+ if (arrowMatch) {
+ const params = arrowMatch[1] || arrowMatch[2] || ''
+ return `(${params}) => {}`
+ }
+
+ return 'function()'
+}
+
+/**
+ * 格式化工具类信息
+ * @param {Array} utils - 工具类数组
+ * @returns {Array} 格式化后的工具类
+ */
+function formatUtils(utils) {
+ return utils.map((util) => {
+ const formatted = {
+ name: util.name,
+ type: util.type || 'function'
+ }
+
+ // 处理 npm 类型的工具
+ if (util.type === 'npm' && util.content) {
+ formatted.package = util.content.package
+ formatted.exportName = util.content.exportName
+ formatted.destructuring = util.content.destructuring
+ formatted.description = `Import from ${util.content.package}`
+ }
+
+ // 处理函数类型的工具
+ if (util.type === 'function' && util.content) {
+ if (util.content.type === 'JSFunction') {
+ // 提取函数签名而不是完整实现
+ const funcSignature = extractFunctionSignature(util.content.value)
+ formatted.signature = funcSignature
+ formatted.description = `Utility function: ${util.name}`
+ }
+ }
+
+ return formatted
+ })
+}
+
+/**
+ * 格式化全局状态信息
+ * @param {Array} globalState - 全局状态数组
+ * @returns {Array} 格式化后的全局状态
+ */
+function formatGlobalState(globalState) {
+ return globalState.map((store) => ({
+ id: store.id,
+ state: Object.keys(store.state || {}),
+ getters: Object.keys(store.getters || {}),
+ actions: Object.keys(store.actions || {}),
+ description: `Pinia store: ${store.id}`
+ }))
+}
+
+/**
+ * 格式化本地状态
+ * @param {Object} state - 状态对象
+ * @returns {Object} 格式化后的状态
+ */
+function formatState(state) {
+ // 只返回键名和类型信息,不返回实际值
+ const formatted = {}
+ for (const [key, value] of Object.entries(state)) {
+ formatted[key] = {
+ type: typeof value,
+ isArray: Array.isArray(value),
+ isObject: value !== null && typeof value === 'object' && !Array.isArray(value)
+ }
+ }
+ return formatted
+}
+
+/**
+ * 格式化本地方法
+ * @param {Object} methods - 方法对象
+ * @returns {Object} 格式化后的方法
+ */
+function formatMethods(methods) {
+ const formatted = {}
+ for (const [key, value] of Object.entries(methods)) {
+ if (value && value.type === 'JSFunction') {
+ formatted[key] = {
+ signature: extractFunctionSignature(value.value),
+ description: `Method: ${key}`
+ }
+ } else {
+ formatted[key] = {
+ type: typeof value,
+ description: `Method: ${key}`
+ }
+ }
+ }
+ return formatted
+}
+
+/**
+ * 格式化当前组件 schema
+ * @param {Object} schema - 组件 schema
+ * @returns {Object|null} 格式化后的 schema
+ */
+function formatCurrentSchema(schema) {
+ if (!schema) return null
+
+ const formatted = {
+ componentName: schema.componentName,
+ ...(schema.ref && { ref: schema.ref })
+ }
+
+ // 格式化 props
+ if (schema.props) {
+ formatted.props = {}
+ for (const [key, value] of Object.entries(schema.props)) {
+ // 识别事件处理器
+ if (key.startsWith('on')) {
+ formatted.props[key] = {
+ type: 'event',
+ isFunction: value && value.type === 'JSFunction'
+ }
+ } else {
+ formatted.props[key] = {
+ type: value && value.type ? value.type : 'static',
+ isDynamic: value && (value.type === 'JSExpression' || value.type === 'JSFunction')
+ }
+ }
+ }
+ }
+
+ return formatted
+}
+
+/**
+ * 验证低代码上下文的完整性
+ * @param {Object} context - 低代码上下文
+ * @returns {{ valid: boolean, warnings: string[] }} 验证结果
+ */
+export function validateLowcodeContext(context) {
+ const warnings = []
+
+ if (!context) {
+ return { valid: false, warnings: ['Context is null or undefined'] }
+ }
+
+ // 检查必要字段
+ const requiredFields = ['dataSource', 'utils', 'globalState', 'state', 'methods']
+ for (const field of requiredFields) {
+ if (!(field in context)) {
+ warnings.push(`Missing field: ${field}`)
+ }
+ }
+
+ // 检查数据源格式
+ if (context.dataSource && !Array.isArray(context.dataSource)) {
+ warnings.push('dataSource should be an array')
+ }
+
+ // 检查工具类格式
+ if (context.utils && !Array.isArray(context.utils)) {
+ warnings.push('utils should be an array')
+ }
+
+ // 检查全局状态格式
+ if (context.globalState && !Array.isArray(context.globalState)) {
+ warnings.push('globalState should be an array')
+ }
+
+ return {
+ valid: warnings.length === 0,
+ warnings
+ }
+}
+
+/**
+ * 合并多个低代码上下文
+ * @param {...Object} contexts - 多个上下文对象
+ * @returns {Object} 合并后的上下文
+ */
+export function mergeLowcodeContexts(...contexts) {
+ const merged = {
+ dataSource: [],
+ utils: [],
+ globalState: [],
+ state: {},
+ methods: {},
+ currentSchema: null
+ }
+
+ for (const context of contexts) {
+ if (!context) continue
+
+ // 合并数组类型
+ if (context.dataSource) {
+ merged.dataSource = [...merged.dataSource, ...context.dataSource]
+ }
+ if (context.utils) {
+ merged.utils = [...merged.utils, ...context.utils]
+ }
+ if (context.globalState) {
+ merged.globalState = [...merged.globalState, ...context.globalState]
+ }
+
+ // 合并对象类型
+ if (context.state) {
+ merged.state = { ...merged.state, ...context.state }
+ }
+ if (context.methods) {
+ merged.methods = { ...merged.methods, ...context.methods }
+ }
+
+ // currentSchema 使用最后一个非空值
+ if (context.currentSchema) {
+ merged.currentSchema = context.currentSchema
+ }
+ }
+
+ return merged
+}
+
+/**
+ * 从低代码平台元数据构建补全上下文
+ * @param {Object} metadata - 低代码平台元数据
+ * @returns {Object} 格式化的低代码上下文
+ */
+export function buildLowcodeContext(metadata) {
+ const { dataSource = [], utils = [], globalState = [], state = {}, methods = {}, currentSchema = null } = metadata
+
+ return {
+ dataSource: formatDataSources(dataSource),
+ utils: formatUtils(utils),
+ globalState: formatGlobalState(globalState),
+ state: formatState(state),
+ methods: formatMethods(methods),
+ currentSchema: formatCurrentSchema(currentSchema)
+ }
+}
diff --git a/packages/plugins/script/src/ai-completion/builders/promptBuilder.js b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js
new file mode 100644
index 0000000000..cbcbb2f0d4
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/builders/promptBuilder.js
@@ -0,0 +1,204 @@
+import { CODE_PATTERNS, CONTEXT_CONFIG } from '../constants.js'
+import {
+ createCodeInstruction,
+ createLowcodeInstruction,
+ BLOCK_COMMENT_INSTRUCTION,
+ LINE_COMMENT_INSTRUCTION
+} from '../prompts/templates.js'
+import { buildLowcodeContext, validateLowcodeContext } from './lowcodeContextBuilder.js'
+
+/**
+ * 检测光标是否在注释中
+ * @param {string} textBeforeCursor - 光标前的文本
+ * @returns {{ isComment: boolean, type: string | null }} 注释状态
+ */
+function isInComment(textBeforeCursor) {
+ const trimmed = textBeforeCursor.trim()
+
+ // 单行注释 //
+ if (trimmed.includes('//')) {
+ const lastLineBreak = textBeforeCursor.lastIndexOf('\n')
+ const currentLine = textBeforeCursor.substring(lastLineBreak + 1)
+ if (currentLine.trim().startsWith('//')) {
+ return { isComment: true, type: 'line' }
+ }
+ }
+
+ // 块注释 /* */ 或 JSDoc /** */
+ const lastBlockStart = textBeforeCursor.lastIndexOf('/*')
+ const lastBlockEnd = textBeforeCursor.lastIndexOf('*/')
+ if (lastBlockStart > lastBlockEnd) {
+ return { isComment: true, type: 'block' }
+ }
+
+ return { isComment: false, type: null }
+}
+
+/**
+ * 提取当前代码上下文信息(函数名、类名、接口名等)
+ * @param {string} textBeforeCursor - 光标前的文本
+ * @returns {{ functionName: string, className: string, interfaceName: string, typeName: string }} 代码上下文
+ */
+function extractCodeContext(textBeforeCursor) {
+ const lines = textBeforeCursor.split('\n')
+ let functionName = ''
+ let className = ''
+ let interfaceName = ''
+ let typeName = ''
+
+ // 从后往前查找最近的定义
+ const startLine = Math.max(0, lines.length - CONTEXT_CONFIG.MAX_LINES_TO_SCAN)
+
+ for (let i = lines.length - 1; i >= startLine; i--) {
+ const line = lines[i]
+
+ if (!functionName) {
+ const funcMatch = line.match(CODE_PATTERNS.FUNCTION)
+ if (funcMatch) functionName = funcMatch[1] || funcMatch[2] || funcMatch[3]
+ }
+
+ if (!className) {
+ const classMatch = line.match(CODE_PATTERNS.CLASS)
+ if (classMatch) className = classMatch[1]
+ }
+
+ if (!interfaceName) {
+ const interfaceMatch = line.match(CODE_PATTERNS.INTERFACE)
+ if (interfaceMatch) interfaceName = interfaceMatch[1]
+ }
+
+ if (!typeName) {
+ const typeMatch = line.match(CODE_PATTERNS.TYPE)
+ if (typeMatch) typeName = typeMatch[1]
+ }
+
+ // 找到所有信息后提前退出
+ if (functionName && className && interfaceName && typeName) break
+ }
+
+ return { functionName, className, interfaceName, typeName }
+}
+
+/**
+ * 构建元信息注释
+ * @param {string} filename - 文件名
+ * @param {string} language - 语言类型
+ * @param {Object} codeContext - 代码上下文
+ * @param {string[]} technologies - 技术栈
+ * @returns {string} 元信息字符串
+ */
+function buildMetaInfo(filename, language, codeContext, technologies) {
+ let metaInfo = ''
+
+ if (filename) {
+ metaInfo += `// File: ${filename}\n`
+ }
+
+ metaInfo += `// Language: ${language}\n`
+
+ // 强调当前作用域
+ if (codeContext.className) {
+ metaInfo += `// Current Class: ${codeContext.className}\n`
+ metaInfo += `// IMPORTANT: Only complete code within this class\n`
+ }
+
+ if (codeContext.interfaceName) {
+ metaInfo += `// Current Interface: ${codeContext.interfaceName}\n`
+ }
+
+ if (codeContext.typeName) {
+ metaInfo += `// Current Type: ${codeContext.typeName}\n`
+ }
+
+ if (codeContext.functionName) {
+ metaInfo += `// Current Function: ${codeContext.functionName}\n`
+ metaInfo += `// IMPORTANT: Only complete code within this function scope\n`
+ }
+
+ if (technologies.length > 0) {
+ metaInfo += `// Technologies: ${technologies.join(', ')}\n`
+ }
+
+ metaInfo += `// NOTE: Do not reference variables or code from other functions\n`
+ metaInfo += '\n'
+
+ return metaInfo
+}
+
+/**
+ * 构建基础上下文
+ * @param {string} language - 语言类型
+ * @param {string} filename - 文件名
+ * @returns {string} 上下文字符串
+ */
+function buildContext(language, filename) {
+ let context = `You are an expert ${language} developer with deep knowledge of modern best practices.`
+
+ if (filename) {
+ context += ` Currently editing: ${filename}`
+ }
+
+ return context
+}
+
+/**
+ * 构建注释补全指令
+ * @param {string} commentType - 注释类型 ('line' | 'block')
+ * @returns {string} 指令文本
+ */
+function buildCommentInstruction(commentType) {
+ return commentType === 'block' ? BLOCK_COMMENT_INSTRUCTION : LINE_COMMENT_INSTRUCTION
+}
+
+/**
+ * 创建智能 Prompt,根据上下文优化补全
+ * @param {Object} completionMetadata - 补全元数据
+ * @returns {{ context: string, instruction: string, fileContent: string }} Prompt 对象
+ */
+export function createSmartPrompt(completionMetadata) {
+ const {
+ textBeforeCursor = '',
+ textAfterCursor = '',
+ language = 'javascript',
+ filename,
+ technologies = [],
+ lowcodeMetadata = null
+ } = completionMetadata
+
+ const commentStatus = isInComment(textBeforeCursor)
+ const codeContext = extractCodeContext(textBeforeCursor)
+
+ // 构建文件元信息(伪装成注释,让 AI 理解上下文)
+ const metaInfo = buildMetaInfo(filename, language, codeContext, technologies)
+
+ // 基础上下文
+ const context = buildContext(language, filename)
+
+ // 根据是否在注释中使用不同的 instruction
+ let instruction
+ if (commentStatus.isComment) {
+ instruction = buildCommentInstruction(commentStatus.type)
+ } else if (lowcodeMetadata) {
+ // 如果提供了低代码元数据,使用增强的指令
+ const lowcodeContext = buildLowcodeContext(lowcodeMetadata)
+ const validation = validateLowcodeContext(lowcodeContext)
+
+ if (!validation.valid) {
+ // eslint-disable-next-line no-console
+ console.warn('⚠️ Lowcode context validation warnings:', validation.warnings)
+ }
+
+ instruction = createLowcodeInstruction(language, lowcodeContext)
+ } else {
+ instruction = createCodeInstruction(language)
+ }
+
+ // 在文件内容前注入元信息
+ const fileContent = `${metaInfo}${textBeforeCursor}[CURSOR]${textAfterCursor}`
+
+ return {
+ context,
+ instruction,
+ fileContent
+ }
+}
diff --git a/packages/plugins/script/src/ai-completion/constants.js b/packages/plugins/script/src/ai-completion/constants.js
new file mode 100644
index 0000000000..0be3f9b94f
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/constants.js
@@ -0,0 +1,193 @@
+/**
+ * Qwen Coder API 配置(阿里云百炼)
+ */
+export const QWEN_CONFIG = {
+ COMPLETION_PATH: '/completions', // Completions API 路径(追加到 baseUrl)
+ DEFAULT_TEMPERATURE: 0.05,
+ TOP_P: 0.95,
+ PRESENCE_PENALTY: 0.2,
+
+ // FIM (Fill-In-the-Middle) 优化配置
+ FIM: {
+ MAX_PREFIX_LINES: 100,
+ MAX_SUFFIX_LINES: 50
+ }
+}
+
+/**
+ * DeepSeek Coder API 配置
+ */
+export const DEEPSEEK_CONFIG = {
+ COMPLETION_PATH: '/beta', // FIM 补全 API 路径
+ PATH_REPLACE: '/v1', // 需要从 baseUrl 中替换的路径
+ DEFAULT_TEMPERATURE: 0,
+ TOP_P: 1.0,
+
+ // FIM (Fill-In-the-Middle) 配置
+ FIM: {
+ MAX_PREFIX_LINES: 100,
+ MAX_SUFFIX_LINES: 50,
+ MAX_TOKENS: 4096 // FIM 最大补全长度 4K
+ }
+}
+
+/**
+ * 模型配置
+ */
+export const MODEL_CONFIG = {
+ QWEN: {
+ TYPE: 'qwen',
+ KEYWORDS: ['qwen']
+ },
+ DEEPSEEK: {
+ TYPE: 'deepseek',
+ KEYWORDS: ['deepseek']
+ },
+ UNKNOWN: {
+ TYPE: 'unknown',
+ KEYWORDS: []
+ }
+}
+
+/**
+ * API 端点配置
+ */
+export const API_ENDPOINTS = {
+ CHAT_COMPLETIONS: '/app-center/api/chat/completions'
+}
+
+/**
+ * HTTP 请求配置
+ */
+export const HTTP_CONFIG = {
+ METHOD: 'POST',
+ CONTENT_TYPE: 'application/json',
+ STREAM: false
+}
+
+/**
+ * 默认配置
+ */
+export const DEFAULTS = {
+ LANGUAGE: 'javascript',
+ LOG_PREVIEW_LENGTH: 100,
+ TECHNOLOGIES: []
+}
+
+/**
+ * 错误消息配置
+ */
+export const ERROR_MESSAGES = {
+ CONFIG_MISSING: 'AI 配置未设置(缺少 model/apiKey/baseUrl)',
+ NO_COMPLETION: '未收到有效的补全结果',
+ REQUEST_FAILED: '请求失败',
+ QWEN_API_ERROR: 'Qwen API 错误'
+}
+
+/**
+ * 通用模型配置
+ */
+export const MODEL_COMMON_CONFIG = {
+ // Token 限制
+ TOKEN_LIMITS: {
+ EXPRESSION: 64,
+ STATEMENT: 256,
+ FUNCTION: 200,
+ CLASS: 256,
+ DEFAULT: 128
+ },
+
+ // 清理规则
+ CLEANUP_PATTERNS: {
+ MARKDOWN_CODE_BLOCK: /^```[\w]*\n?|```$/g,
+ TRAILING_SEMICOLON: /;\s*$/,
+ LEADING_EMPTY_LINES: /^\n+/,
+ TRAILING_EMPTY_LINES: /\n+$/
+ },
+
+ // 智能截断配置
+ TRUNCATION: {
+ MAX_LINES: {
+ EXPRESSION: 1,
+ OBJECT: 5,
+ DEFAULT: 10
+ },
+ CUTOFF_KEYWORDS: ['function ', 'class ', 'export ', 'import '],
+ BLOCK_ENDINGS: ['}', '};']
+ }
+}
+
+/**
+ * 通用停止符配置(JS/TS)
+ */
+export const STOP_SEQUENCES = [
+ // 通用停止符
+ '\n\n',
+ '```',
+
+ // JS/TS 语言特性
+ '\nfunction ',
+ '\nclass ',
+ '\nconst ',
+ '\nlet ',
+ '\nvar ',
+ '\nexport ',
+ '\nimport ',
+ '\ninterface ',
+ '\ntype ',
+ '\nenum ',
+
+ // 注释边界
+ '\n//',
+ '\n/*',
+
+ // 代码块边界
+ '\n}',
+ '\n};'
+]
+
+/**
+ * FIM (Fill-In-the-Middle) 配置
+ */
+export const FIM_CONFIG = {
+ MARKERS: {
+ PREFIX: '<|fim_prefix|>',
+ SUFFIX: '<|fim_suffix|>',
+ MIDDLE: '<|fim_middle|>',
+ CURSOR: '[CURSOR]'
+ },
+
+ // FIM 专用停止符(会与 STOP_SEQUENCES 合并)
+ FIM_MARKERS_STOPS: ['<|fim_prefix|>', '<|fim_suffix|>', '<|fim_middle|>'],
+
+ // 上下文特定的额外停止符
+ CONTEXT_STOPS: {
+ EXPRESSION: [';', '\n)', ','],
+ STATEMENT: [], // 使用通用停止符即可
+ OBJECT: [] // 使用通用停止符即可
+ },
+
+ META_INFO_PATTERN:
+ /^(\/\/ File:.*\n)?(\/\/ Language:.*\n)?(\/\/ Current .*\n)*(\/\/ IMPORTANT:.*\n)*(\/\/ Technologies:.*\n)?(\/\/ NOTE:.*\n)*\n*/
+}
+
+/**
+ * 代码上下文分析配置
+ */
+export const CONTEXT_CONFIG = {
+ MAX_LINES_TO_SCAN: 20
+}
+
+/**
+ * 代码模式匹配(JS/TS)
+ */
+export const CODE_PATTERNS = {
+ // 匹配函数定义:function name() / const name = () => / name() {
+ FUNCTION: /function\s+(\w+)|const\s+(\w+)\s*=.*=>|(\w+)\s*\([^)]*\)\s*{/,
+ // 匹配类定义
+ CLASS: /class\s+(\w+)/,
+ // 匹配接口定义(TS)
+ INTERFACE: /interface\s+(\w+)/,
+ // 匹配类型定义(TS)
+ TYPE: /type\s+(\w+)/
+}
diff --git a/packages/plugins/script/src/ai-completion/index.js b/packages/plugins/script/src/ai-completion/index.js
new file mode 100644
index 0000000000..ae14a7a2fb
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/index.js
@@ -0,0 +1,6 @@
+/**
+ * AI 补全模块统一导出
+ */
+export { createCompletionHandler } from './adapters/index.js'
+export { shouldTriggerCompletion } from './triggers/completionTrigger.js'
+export { createSmartPrompt, FIMPromptBuilder } from './builders/index.js'
diff --git a/packages/plugins/script/src/ai-completion/prompts/templates.js b/packages/plugins/script/src/ai-completion/prompts/templates.js
new file mode 100644
index 0000000000..0569d8bd6c
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/prompts/templates.js
@@ -0,0 +1,190 @@
+/**
+ * 系统 Prompt - 定义 AI 的角色和基本规则
+ */
+export const SYSTEM_BASE_PROMPT = `You are an AI code completion assistant specialized in JavaScript and TypeScript.
+
+CRITICAL RULES:
+1. Return ONLY the code/text that should be inserted at the cursor position
+2. DO NOT repeat any code that already exists before the cursor
+3. DO NOT include markdown code blocks or language tags
+4. DO NOT add explanations or comments unless explicitly requested
+5. Match the exact indentation and style of the existing code
+6. Keep completions focused and minimal - only what's needed
+7. Pay attention to the file metadata (filename, language, current function/class/interface) for better context
+8. For TypeScript, ensure type safety and proper type annotations
+9. ONLY complete code within the CURRENT function/scope where [CURSOR] is located
+10. DO NOT generate code for other functions, classes, or unrelated scopes
+11. If you see multiple functions in the context, focus ONLY on the one containing [CURSOR]
+12. Respect variable scope - do not reference variables from other functions`
+
+/**
+ * 代码补全指令模板
+ * @param {string} language - 编程语言
+ * @returns {string} 指令文本
+ */
+export function createCodeInstruction(language) {
+ return `Complete the code after the cursor position.
+
+Rules:
+1. Follow ${language} best practices and modern ES6+ syntax
+2. Match the existing code style exactly (indentation, quotes, semicolons)
+3. Generate only the necessary code to complete the current statement or block
+4. Ensure proper indentation and formatting
+5. DO NOT include explanatory comments unless they were already in the pattern
+6. If completing a function, include the full implementation
+7. For TypeScript, include proper type annotations
+8. Return ONLY the completion code, no additional text
+9. CRITICAL: Only complete code within the current function/scope
+10. DO NOT generate variables or code from other functions in the file`
+}
+
+/**
+ * 块注释补全指令(JSDoc)
+ */
+export const BLOCK_COMMENT_INSTRUCTION = `You are writing a JSDoc documentation comment. Complete the comment with clear, concise explanation.
+
+Focus on:
+- Describing what the code does
+- Explaining parameters with @param tags
+- Documenting return values with @returns tag
+- Adding usage examples with @example if appropriate
+- Including type information for TypeScript
+
+DO NOT generate code. Only complete the comment text.`
+
+/**
+ * 行注释补全指令
+ */
+export const LINE_COMMENT_INSTRUCTION = `You are writing an inline comment. Complete the comment with a brief, clear explanation.
+
+Focus on:
+- Explaining WHY this code exists, not WHAT it does
+- Keep it concise and on a single line
+- Use clear, professional language
+
+DO NOT generate code. Only complete the comment text.`
+
+/**
+ * 低代码平台上下文增强 Prompt
+ * 用于在低代码环境中提供特定的 API 和数据结构提示
+ */
+export const LOWCODE_CONTEXT_INSTRUCTION = `You are working in a low-code platform environment with specific APIs and data structures.
+
+AVAILABLE RUNTIME APIS (all accessed via 'this.'):
+1. Data Sources (this.dataSource.xxx)
+ - Predefined data models for the application
+ - Access pattern: this.dataSource.
+
+2. Utility Functions (this.utils.xxx)
+ - Common utility methods and npm dependencies
+ - Access pattern: this.utils.
+ - May include imported libraries (check utils metadata for imports)
+
+3. Global State (this.stores.xxx)
+ - Pinia-based global state management
+ - Access pattern: this.stores..
+ - Actions: this.stores..()
+
+4. Local State (this.state.xxx)
+ - Component-level reactive state
+ - Access pattern: this.state.
+
+5. Local Methods (this.xxx)
+ - Component-level methods
+ - Access pattern: this.()
+
+6. Component References (this.$('refName'))
+ - Access Vue component refs
+ - Access pattern: this.$('')
+
+IMPORTANT RULES:
+- ONLY use APIs that are explicitly defined in the provided metadata
+- DO NOT reference undefined utilities, data sources, or state properties
+- Follow the JSExpression/JSFunction protocol for dynamic values
+- Use 'function' keyword for function definitions, NOT arrow functions
+- Respect the component schema structure (props, events, refs)
+
+PROTOCOL CONVENTIONS:
+- Static values: { width: '300px' }
+- Dynamic expressions: { width: { type: 'JSExpression', value: 'this.state.xxx' } }
+- Function handlers: { onClick: { type: 'JSFunction', value: 'function onClick() {}' } }`
+
+/**
+ * 创建带低代码上下文的指令
+ * @param {string} language - 编程语言
+ * @param {Object} lowcodeContext - 低代码上下文数据
+ * @returns {string} 增强的指令文本
+ */
+export function createLowcodeInstruction(language, lowcodeContext = {}) {
+ const {
+ dataSource = [],
+ utils = [],
+ globalState = [],
+ state = {},
+ methods = {},
+ currentSchema = null
+ } = lowcodeContext
+
+ let instruction = createCodeInstruction(language)
+
+ // 如果提供了低代码上下文,添加特定信息
+ if (Object.keys(lowcodeContext).length > 0) {
+ instruction += `\n\n${LOWCODE_CONTEXT_INSTRUCTION}`
+
+ // 添加可用的数据源
+ if (dataSource.length > 0) {
+ instruction += `\n\nAVAILABLE DATA SOURCES:\n${JSON.stringify(dataSource, null, 2)}`
+ }
+
+ // 添加可用的工具类
+ if (utils.length > 0) {
+ instruction += `\n\nAVAILABLE UTILITIES:\n${JSON.stringify(utils, null, 2)}`
+ }
+
+ // 添加全局状态
+ if (globalState.length > 0) {
+ instruction += `\n\nGLOBAL STATE (Pinia Stores):\n${JSON.stringify(globalState, null, 2)}`
+ }
+
+ // 添加本地状态
+ if (Object.keys(state).length > 0) {
+ instruction += `\n\nLOCAL STATE:\n${JSON.stringify(state, null, 2)}`
+ }
+
+ // 添加本地方法
+ if (Object.keys(methods).length > 0) {
+ instruction += `\n\nLOCAL METHODS:\n${JSON.stringify(methods, null, 2)}`
+ }
+
+ // 添加当前组件 schema
+ if (currentSchema) {
+ instruction += `\n\nCURRENT COMPONENT SCHEMA:\n${JSON.stringify(currentSchema, null, 2)}`
+ instruction += `\n\nCOMPONENT CONTEXT:`
+ instruction += `\n- Component: ${currentSchema.componentName || 'Unknown'}`
+ if (currentSchema.props) {
+ instruction += `\n- Props: Use component props as defined in schema`
+ instruction += `\n- Events: Props starting with 'on' are event handlers`
+ }
+ if (currentSchema.ref) {
+ instruction += `\n- Ref: Access via this.$('${currentSchema.ref}')`
+ }
+ }
+ }
+
+ return instruction
+}
+
+/**
+ * 用户 Prompt 模板
+ * @param {string} instruction - 指令文本
+ * @param {string} fileContent - 文件内容(包含 [CURSOR] 标记)
+ * @returns {string} 完整的用户 Prompt
+ */
+export function createUserPrompt(instruction, fileContent) {
+ return `${instruction}
+
+File content (cursor position marked with [CURSOR]):
+${fileContent}
+
+Complete the code/text at the [CURSOR] position. Return ONLY the completion text.`
+}
diff --git a/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js
new file mode 100644
index 0000000000..76d58b3122
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/triggers/completionTrigger.js
@@ -0,0 +1,73 @@
+/**
+ * 检测光标是否在语句结束符后(分号后)
+ */
+function isAfterStatementEnd(beforeCursor) {
+ // 检查是否以分号结尾(忽略尾部空格)
+ const trimmedEnd = beforeCursor.trimEnd()
+
+ if (trimmedEnd.endsWith(';')) {
+ // 排除 for 循环中的分号:for (let i = 0; i < 10; i++)
+ // 检查是否在括号内
+ const openParens = (beforeCursor.match(/\(/g) || []).length
+ const closeParens = (beforeCursor.match(/\)/g) || []).length
+
+ // 如果括号未闭合,说明可能在 for 循环中
+ if (openParens > closeParens) {
+ return false
+ }
+
+ return true
+ }
+
+ return false
+}
+
+/**
+ * 检测光标是否在代码块结束符后(右花括号后)
+ */
+function isAfterBlockEnd(beforeCursor) {
+ const trimmedEnd = beforeCursor.trimEnd()
+
+ // 检查是否以右花括号结尾
+ if (trimmedEnd.endsWith('}')) {
+ // 检查后面是否只有空格(没有其他字符)
+ const afterBrace = beforeCursor.substring(trimmedEnd.length)
+ return afterBrace.trim().length === 0
+ }
+
+ return false
+}
+
+/**
+ * 判断是否应该触发代码补全
+ * @param {Object} params - 触发参数
+ * @param {string} params.text - 完整文本
+ * @param {Object} params.position - 光标位置
+ * @param {number} params.position.lineNumber - 行号
+ * @param {number} params.position.column - 列号
+ * @returns {boolean} 是否触发补全
+ */
+export function shouldTriggerCompletion(params) {
+ const { text, position } = params
+ const lines = text.split('\n')
+ const currentLine = lines[position.lineNumber - 1] || ''
+ const beforeCursor = currentLine.substring(0, position.column - 1)
+
+ // 1. 代码太短不触发
+ if (text.trim().length < 2) {
+ return false
+ }
+
+ // 2. 分号后不触发(语句已结束)
+ if (isAfterStatementEnd(beforeCursor)) {
+ return false
+ }
+
+ // 3. 右花括号后不触发(块已结束)
+ if (isAfterBlockEnd(beforeCursor)) {
+ return false
+ }
+
+ // 其他情况都允许触发
+ return true
+}
diff --git a/packages/plugins/script/src/ai-completion/utils/completionUtils.js b/packages/plugins/script/src/ai-completion/utils/completionUtils.js
new file mode 100644
index 0000000000..ce9d1e03b9
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/utils/completionUtils.js
@@ -0,0 +1,90 @@
+import { useResource, useCanvas } from '@opentiny/tiny-engine-meta-register'
+import { MODEL_COMMON_CONFIG, FIM_CONFIG } from '../constants.js'
+
+/**
+ * 构建低代码元数据
+ * @returns {Object} 低代码元数据
+ */
+export function buildLowcodeMetadata() {
+ const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState || {}
+ const { state: pageState = {}, methods = {} } = useCanvas().getPageSchema() || {}
+ const currentSchema = useCanvas().getCurrentSchema()
+
+ return {
+ dataSource,
+ utils,
+ globalState,
+ state: pageState,
+ methods,
+ currentSchema
+ }
+}
+
+/**
+ * 清理补全文本
+ * @param {string} text - 原始补全文本
+ * @param {string} modelType - 模型类型
+ * @param {Object} cursorContext - 光标上下文信息(可选)
+ * @returns {string} 清理后的文本
+ */
+export function cleanCompletion(text, modelType, cursorContext = null) {
+ if (!text) return text
+
+ let cleaned = text
+
+ // 1. 移除 markdown 代码块
+ cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.MARKDOWN_CODE_BLOCK, '')
+
+ // 2. 移除前后空行
+ cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.LEADING_EMPTY_LINES, '')
+ cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.TRAILING_EMPTY_LINES, '')
+
+ // 3. Qwen 特殊处理:移除 FIM 标记
+ if (modelType === 'qwen') {
+ Object.values(FIM_CONFIG.MARKERS).forEach((marker) => {
+ if (marker !== FIM_CONFIG.MARKERS.CURSOR) {
+ cleaned = cleaned.replace(new RegExp(marker.replace(/[|<>]/g, '\\$&'), 'g'), '')
+ }
+ })
+ }
+
+ // 4. 表达式特殊处理:移除尾部分号
+ if (cursorContext?.needsExpression) {
+ cleaned = cleaned.replace(MODEL_COMMON_CONFIG.CLEANUP_PATTERNS.TRAILING_SEMICOLON, '')
+ }
+
+ // 5. 智能截断:防止返回过多不相关代码
+ const lines = cleaned.split('\n')
+
+ // 根据上下文确定最大行数
+ const truncation = MODEL_COMMON_CONFIG.TRUNCATION
+ const maxLines = cursorContext?.needsExpression
+ ? truncation.MAX_LINES.EXPRESSION
+ : cursorContext?.inObject
+ ? truncation.MAX_LINES.OBJECT
+ : truncation.MAX_LINES.DEFAULT
+
+ if (lines.length > maxLines) {
+ // 找到合适的截断点
+ let cutoffIndex = maxLines
+ for (let i = 0; i < maxLines && i < lines.length; i++) {
+ const line = lines[i].trim()
+
+ // 在函数/类定义处截断
+ if (truncation.CUTOFF_KEYWORDS.some((keyword) => line.startsWith(keyword))) {
+ cutoffIndex = i
+ break
+ }
+
+ // 在闭合大括号处截断(完整的代码块)
+ if (truncation.BLOCK_ENDINGS.includes(line)) {
+ cutoffIndex = i + 1
+ break
+ }
+ }
+
+ cleaned = lines.slice(0, cutoffIndex).join('\n')
+ }
+
+ return cleaned
+}
diff --git a/packages/plugins/script/src/ai-completion/utils/modelUtils.js b/packages/plugins/script/src/ai-completion/utils/modelUtils.js
new file mode 100644
index 0000000000..f93a72b9a9
--- /dev/null
+++ b/packages/plugins/script/src/ai-completion/utils/modelUtils.js
@@ -0,0 +1,78 @@
+import { MODEL_CONFIG, MODEL_COMMON_CONFIG, STOP_SEQUENCES, FIM_CONFIG } from '../constants.js'
+
+/**
+ * 检测模型类型
+ * @param {string} modelName - 模型名称
+ * @returns {'qwen' | 'deepseek' | 'unknown'} 模型类型
+ */
+export function detectModelType(modelName) {
+ if (!modelName) return MODEL_CONFIG.UNKNOWN.TYPE
+
+ const lowerName = modelName.toLowerCase()
+
+ if (MODEL_CONFIG.QWEN.KEYWORDS.some((keyword) => lowerName.includes(keyword))) {
+ return MODEL_CONFIG.QWEN.TYPE
+ }
+
+ if (MODEL_CONFIG.DEEPSEEK.KEYWORDS.some((keyword) => lowerName.includes(keyword))) {
+ return MODEL_CONFIG.DEEPSEEK.TYPE
+ }
+
+ return MODEL_CONFIG.UNKNOWN.TYPE
+}
+
+/**
+ * 计算动态 Token 数量
+ * @param {Object} cursorContext - 光标上下文
+ * @returns {number} Token 数量
+ */
+export function calculateTokens(cursorContext) {
+ const limits = MODEL_COMMON_CONFIG.TOKEN_LIMITS
+
+ if (!cursorContext) {
+ return limits.DEFAULT
+ }
+
+ if (cursorContext.needsStatement) {
+ return limits.STATEMENT
+ } else if (cursorContext.needsExpression) {
+ return limits.EXPRESSION
+ } else if (cursorContext.inFunction) {
+ return limits.FUNCTION
+ } else if (cursorContext.inClass) {
+ return limits.CLASS
+ }
+
+ return limits.DEFAULT
+}
+
+/**
+ * 获取动态停止符
+ * @param {Object} cursorContext - 光标上下文
+ * @param {string} modelType - 模型类型
+ * @returns {string[]} 停止符数组
+ */
+export function getStopSequences(cursorContext, modelType) {
+ // 基础停止符:通用停止符
+ const stops = [...STOP_SEQUENCES]
+
+ // Qwen 模型添加 FIM 标记
+ if (modelType === 'qwen') {
+ stops.push(...FIM_CONFIG.FIM_MARKERS_STOPS)
+ }
+
+ if (!cursorContext) {
+ return stops
+ }
+
+ // 根据上下文添加特定停止符
+ if (cursorContext.needsExpression) {
+ stops.push(...FIM_CONFIG.CONTEXT_STOPS.EXPRESSION)
+ } else if (cursorContext.needsStatement) {
+ stops.push(...FIM_CONFIG.CONTEXT_STOPS.STATEMENT)
+ } else if (cursorContext.inObject) {
+ stops.push(...FIM_CONFIG.CONTEXT_STOPS.OBJECT)
+ }
+
+ return stops
+}