Skip to content

Commit 674fc63

Browse files
committed
feat(ai-completion): Enhance completion system with debounce management and FIM support
1 parent aadcfe7 commit 674fc63

9 files changed

Lines changed: 163 additions & 311 deletions

File tree

packages/plugins/robot/src/constants/model-config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ export const DEFAULT_LLM_MODELS = [
121121
}
122122
},
123123
{
124-
// TODO: https://api.deepseek.com/beta 支持 FIM
125124
label: 'Deepseek Coder编程模型',
126125
name: 'deepseek-chat',
127126
capabilities: {

packages/plugins/script/src/Main.vue

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@
3434
/* metaService: engine.plugins.pagecontroller.Main */
3535
import { onBeforeUnmount, reactive, provide } from 'vue'
3636
import { Button } from '@opentiny/vue'
37-
import { registerCompletion } from 'monacopilot'
37+
import { registerCompletion, type CompletionRegistration } from 'monacopilot'
3838
import { VueMonaco, PluginPanel } from '@opentiny/tiny-engine-common/component'
3939
import { useHelp, useLayout } from '@opentiny/tiny-engine-meta-register'
4040
import { initCompletion } from '@opentiny/tiny-engine-common/js/completion'
4141
import { initLinter } from '@opentiny/tiny-engine-common/js/linter'
4242
import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method'
43-
import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger'
4443
import { createCompletionHandler } from './ai-completion/adapters/index'
44+
import { shouldTriggerCompletion } from './ai-completion/triggers/completionTrigger'
45+
import { debounceManager } from './ai-completion/utils/debounceManager'
4546
4647
export const api = {
4748
saveMethod,
@@ -69,6 +70,9 @@ export default {
6970
7071
const { PLUGIN_NAME } = useLayout()
7172
73+
// 存储 AI 补全注册信息
74+
let completionRegistration: CompletionRegistration | null = null
75+
7276
const panelState = reactive({
7377
emitEvent: emit
7478
})
@@ -122,17 +126,19 @@ export default {
122126
const monacoInstance = monacoRef.value.getMonaco()
123127
const editorInstance = monacoRef.value.getEditor()
124128
125-
registerCompletion(monacoInstance, editorInstance, {
129+
// 配置防抖管理器
130+
debounceManager.setDebounceDelay(300) // 防抖延迟 300ms
131+
debounceManager.setDebounceEnabled(true)
132+
133+
completionRegistration = registerCompletion(monacoInstance, editorInstance, {
126134
language: 'javascript',
127-
endpoint: '/app-center/api/chat/completions',
128135
filename: 'page.js',
129-
trigger: 'onTyping',
130136
maxContextLines: 50,
131137
enableCaching: true,
132138
allowFollowUpCompletions: true,
133139
134140
// 🎯 智能触发判断(在请求前执行,避免不必要的请求)
135-
triggerIf: (params) => {
141+
triggerIf: () => {
136142
const model = editorInstance.getModel()
137143
const position = editorInstance.getPosition()
138144
@@ -143,13 +149,16 @@ export default {
143149
position: {
144150
lineNumber: position.lineNumber,
145151
column: position.column
146-
},
147-
triggerType: params.triggerType || 'onTyping'
152+
}
148153
})
149154
},
150155
151-
// 🚀 请求处理器:支持 DeepSeek 和 Qwen 模型
152-
requestHandler: createCompletionHandler() as any
156+
requestHandler: debounceManager.createRequestHandler(createCompletionHandler())
157+
})
158+
159+
// 注册快捷键:Ctrl+Space 触发 AI 补全
160+
editorInstance.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.Space, () => {
161+
completionRegistration?.trigger?.()
153162
})
154163
} catch (error) {
155164
// eslint-disable-next-line no-console
@@ -158,6 +167,9 @@ export default {
158167
}
159168
160169
onBeforeUnmount(() => {
170+
// 清理 AI 补全
171+
completionRegistration?.deregister?.()
172+
debounceManager.reset()
161173
;(state.completionProvider as any)?.forEach?.((provider: any) => {
162174
provider?.dispose?.()
163175
})

packages/plugins/script/src/ai-completion/adapters/index.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { detectModelType, calculateTokens, getStopSequences } from '../utils/mod
88
import { cleanCompletion, buildLowcodeMetadata } from '../utils/completionUtils.js'
99
import { buildQwenMessages, callQwenAPI } from './qwenAdapter.js'
1010
import { buildDeepSeekMessages, callDeepSeekAPI } from './deepseekAdapter.js'
11-
import { QWEN_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js'
11+
import { QWEN_CONFIG, DEEPSEEK_CONFIG, DEFAULTS, ERROR_MESSAGES, MODEL_CONFIG } from '../constants.js'
1212

1313
/**
1414
* 创建请求处理器
@@ -71,10 +71,20 @@ export function createCompletionHandler() {
7171
// ===== DeepSeek 流程(默认) =====
7272
const { messages } = buildDeepSeekMessages(context, instruction, fileContent)
7373

74-
const config = { model: completeModel }
74+
// DeepSeek 使用 Chat API,也需要 stop 序列
75+
const config = {
76+
model: completeModel,
77+
stopSequences: getStopSequences(null, MODEL_CONFIG.DEEPSEEK.TYPE)
78+
}
7579
const httpClient = getMetaApi(META_SERVICE.Http)
7680

77-
completionText = await callDeepSeekAPI(messages, config, apiKey, baseUrl, httpClient)
81+
// 构建 DeepSeek FIM 端点:将 /v1 替换为 /beta
82+
const completionBaseUrl = baseUrl.replace(DEEPSEEK_CONFIG.PATH_REPLACE, DEEPSEEK_CONFIG.COMPLETION_PATH)
83+
84+
// eslint-disable-next-line no-console
85+
console.log('🔧 DeepSeek FIM 端点:', completionBaseUrl)
86+
87+
completionText = await callDeepSeekAPI(messages, config, apiKey, completionBaseUrl, httpClient)
7888
}
7989

8090
// 6. 处理补全结果

packages/plugins/script/src/ai-completion/adapters/qwenAdapter.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Qwen 专用适配器
33
* 使用 Completions API + FIM (Fill-In-the-Middle)
44
*/
5-
import { QWEN_CONFIG, API_ENDPOINTS, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js'
5+
import { QWEN_CONFIG, HTTP_CONFIG, ERROR_MESSAGES } from '../constants.js'
66

77
/**
88
* 构建 Qwen FIM 格式的 messages
@@ -40,7 +40,8 @@ export function buildQwenMessages(fileContent, fimBuilder) {
4040
* @returns {Promise<string>} 补全文本
4141
*/
4242
export async function callQwenAPI(messages, config, apiKey, baseUrl) {
43-
const completionsUrl = `${baseUrl}${API_ENDPOINTS.COMPLETIONS_PATH}`
43+
// 构建完整的 Completions API URL
44+
const completionsUrl = `${baseUrl}${QWEN_CONFIG.COMPLETION_PATH}`
4445

4546
// eslint-disable-next-line no-console
4647
console.log('📦 模型:', config.model)

packages/plugins/script/src/ai-completion/constants.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
* Qwen Coder API 配置(阿里云百炼)
33
*/
44
export const QWEN_CONFIG = {
5-
API_URL: 'https://dashscope.aliyuncs.com/compatible-mode/v1/completions',
6-
MODEL: 'qwen2.5-coder-32b-instruct',
5+
COMPLETION_PATH: '/completions', // Completions API 路径(追加到 baseUrl)
76
DEFAULT_TEMPERATURE: 0.05,
87
TOP_P: 0.95,
98
PRESENCE_PENALTY: 0.2,
@@ -15,13 +14,30 @@ export const QWEN_CONFIG = {
1514
}
1615
}
1716

17+
/**
18+
* DeepSeek Coder API 配置
19+
*/
20+
export const DEEPSEEK_CONFIG = {
21+
COMPLETION_PATH: '/beta', // FIM 补全 API 路径
22+
PATH_REPLACE: '/v1', // 需要从 baseUrl 中替换的路径
23+
DEFAULT_TEMPERATURE: 0,
24+
TOP_P: 1.0,
25+
26+
// FIM (Fill-In-the-Middle) 配置
27+
FIM: {
28+
MAX_PREFIX_LINES: 100,
29+
MAX_SUFFIX_LINES: 50,
30+
MAX_TOKENS: 4096 // FIM 最大补全长度 4K
31+
}
32+
}
33+
1834
/**
1935
* 模型配置
2036
*/
2137
export const MODEL_CONFIG = {
2238
QWEN: {
2339
TYPE: 'qwen',
24-
KEYWORDS: ['qwen'] // 移除 'coder',避免误匹配 deepseek-coder
40+
KEYWORDS: ['qwen']
2541
},
2642
DEEPSEEK: {
2743
TYPE: 'deepseek',
@@ -37,8 +53,7 @@ export const MODEL_CONFIG = {
3753
* API 端点配置
3854
*/
3955
export const API_ENDPOINTS = {
40-
COMPLETIONS_PATH: '/completions',
41-
CHAT_COMPLETIONS: '/app-center/api/chat/completions'
56+
CHAT_COMPLETIONS: '/app-center/api/chat/completions' // 后端代理端点(DeepSeek 使用)
4257
}
4358

4459
/**

packages/plugins/script/src/ai-completion/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
*/
44
export { createCompletionHandler } from './adapters/index.js'
55
export { shouldTriggerCompletion } from './triggers/completionTrigger.js'
6-
export { requestManager } from './utils/requestManager.js'
6+
export { debounceManager } from './utils/debounceManager.js'
77
export { createSmartPrompt, FIMPromptBuilder } from './builders/index.js'

packages/plugins/script/src/ai-completion/triggers/completionTrigger.js

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,7 @@
11
/**
2-
* 智能补全触发条件判断(JS/TS 专用)
2+
* 智能补全触发条件判断
33
*/
44

5-
/**
6-
* 检测是否在注释中
7-
*/
8-
function isInComment(beforeCursor, fullText) {
9-
const trimmed = beforeCursor.trim()
10-
11-
// 单行注释
12-
if (trimmed.startsWith('//') || trimmed.startsWith('*')) {
13-
return true
14-
}
15-
16-
// 块注释
17-
const lastBlockStart = fullText.lastIndexOf('/*', fullText.indexOf(beforeCursor))
18-
const lastBlockEnd = fullText.lastIndexOf('*/', fullText.indexOf(beforeCursor))
19-
if (lastBlockStart > lastBlockEnd) {
20-
return true
21-
}
22-
23-
return false
24-
}
25-
26-
/**
27-
* 检测是否在字符串中
28-
*/
29-
function isInString(beforeCursor) {
30-
const singleQuotes = (beforeCursor.match(/'/g) || []).length
31-
const doubleQuotes = (beforeCursor.match(/"/g) || []).length
32-
return singleQuotes % 2 === 1 || doubleQuotes % 2 === 1
33-
}
34-
35-
/**
36-
* 检测是否在模板字符串中
37-
*/
38-
function isInTemplateString(beforeCursor) {
39-
const backticks = (beforeCursor.match(/`/g) || []).length
40-
return backticks % 2 === 1
41-
}
42-
435
/**
446
* 检测光标是否在语句结束符后(分号后)
457
*/
@@ -87,42 +49,25 @@ function isAfterBlockEnd(beforeCursor) {
8749
* @param {Object} params.position - 光标位置
8850
* @param {number} params.position.lineNumber - 行号
8951
* @param {number} params.position.column - 列号
90-
* @param {string} params.triggerType - 触发类型
9152
* @returns {boolean} 是否触发补全
9253
*/
9354
export function shouldTriggerCompletion(params) {
9455
const { text, position } = params
9556
const lines = text.split('\n')
9657
const currentLine = lines[position.lineNumber - 1] || ''
9758
const beforeCursor = currentLine.substring(0, position.column - 1)
98-
const trimmedLine = beforeCursor.trim()
99-
100-
// 1. 避免在注释中触发
101-
if (isInComment(beforeCursor, text)) {
102-
return false
103-
}
104-
105-
// 2. 避免在普通字符串中触发(但允许模板字符串)
106-
if (isInString(beforeCursor) && !isInTemplateString(beforeCursor)) {
107-
return false
108-
}
109-
110-
// 3. 代码太短不触发(降低阈值)
111-
if (text.trim().length < 5) {
112-
return false
113-
}
11459

115-
// 4. 完全空行不触发
116-
if (trimmedLine.length === 0) {
60+
// 1. 代码太短不触发
61+
if (text.trim().length < 2) {
11762
return false
11863
}
11964

120-
// 5. 分号后不触发(语句已结束)
65+
// 2. 分号后不触发(语句已结束)
12166
if (isAfterStatementEnd(beforeCursor)) {
12267
return false
12368
}
12469

125-
// 6. 右花括号后不触发(块已结束)
70+
// 3. 右花括号后不触发(块已结束)
12671
if (isAfterBlockEnd(beforeCursor)) {
12772
return false
12873
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* 防抖管理器 - 仅支持防抖功能
3+
*/
4+
class DebounceManager {
5+
constructor() {
6+
this.debounceTimer = null
7+
this.debounceDelay = 200 // 防抖延迟(毫秒)
8+
this.lastTriggerTime = 0
9+
this.isDebounceEnabled = true
10+
}
11+
12+
/**
13+
* 设置防抖延迟
14+
*/
15+
setDebounceDelay(delay) {
16+
this.debounceDelay = delay
17+
}
18+
19+
/**
20+
* 启用/禁用防抖
21+
*/
22+
setDebounceEnabled(enabled) {
23+
this.isDebounceEnabled = enabled
24+
}
25+
26+
/**
27+
* 清理防抖定时器
28+
*/
29+
clearDebounceTimer() {
30+
if (this.debounceTimer) {
31+
// eslint-disable-next-line no-console
32+
console.log('⏱️ [DebounceManager] 清理防抖定时器')
33+
clearTimeout(this.debounceTimer)
34+
this.debounceTimer = null
35+
}
36+
}
37+
38+
/**
39+
* 检查是否应该立即执行(不防抖)
40+
* 某些情况下应该立即响应,不需要防抖
41+
*/
42+
shouldExecuteImmediately() {
43+
const now = Date.now()
44+
const timeSinceLastTrigger = now - this.lastTriggerTime
45+
46+
// 如果距离上次触发超过 1 秒,立即执行
47+
// 这避免了用户停止输入后再次输入时的延迟
48+
return timeSinceLastTrigger > 1000
49+
}
50+
51+
/**
52+
* 创建带防抖的请求处理器
53+
* @param {Function} handler - 实际的请求处理函数
54+
*/
55+
createRequestHandler(handler) {
56+
return async (params) => {
57+
this.lastTriggerTime = Date.now()
58+
59+
// 如果启用了防抖且不应该立即执行
60+
if (this.isDebounceEnabled && !this.shouldExecuteImmediately()) {
61+
// eslint-disable-next-line no-console
62+
console.log(`⏳ [DebounceManager] 防抖延迟 ${this.debounceDelay}ms`)
63+
// 清理之前的防抖定时器
64+
this.clearDebounceTimer()
65+
66+
// 创建新的防抖 Promise
67+
await new Promise((resolve) => {
68+
this.debounceTimer = setTimeout(() => {
69+
this.debounceTimer = null
70+
// eslint-disable-next-line no-console
71+
console.log('✅ [DebounceManager] 防抖延迟结束,准备执行请求')
72+
resolve()
73+
}, this.debounceDelay)
74+
})
75+
} else {
76+
// eslint-disable-next-line no-console
77+
console.log('⚡ [DebounceManager] 立即执行(无防抖)')
78+
}
79+
80+
// 执行实际的请求处理器
81+
if (handler) {
82+
return await handler(params)
83+
}
84+
85+
return null
86+
}
87+
}
88+
89+
/**
90+
* 重置状态(用于清理)
91+
*/
92+
reset() {
93+
// eslint-disable-next-line no-console
94+
console.log('🔄 [DebounceManager] 重置状态')
95+
this.clearDebounceTimer()
96+
}
97+
}
98+
99+
export const debounceManager = new DebounceManager()

0 commit comments

Comments
 (0)