From 6484ff58ca0a6786edb0732c2291e0e7e2efe647 Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Tue, 21 Oct 2025 19:05:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=E4=BB=A3=E7=A0=81=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E6=96=B0=E5=A2=9EAI=E8=A1=A5=E5=85=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/completion-item/completion-item.ts | 10 +- packages/ai-code/src/module/index.ts | 22 +- .../inline-completions/inline-completions.ts | 239 +++++++++++++++--- .../inline-completions/worker-manager.ts | 156 ++++++++++++ packages/ai-code/src/module/interface.ts | 27 ++ .../src/monaco-editor/monaco-editor.tsx | 29 ++- 6 files changed, 437 insertions(+), 46 deletions(-) create mode 100644 packages/ai-code/src/module/inline-completions/worker-manager.ts diff --git a/packages/ai-code/src/module/completion-item/completion-item.ts b/packages/ai-code/src/module/completion-item/completion-item.ts index 39e96740..7b245eef 100644 --- a/packages/ai-code/src/module/completion-item/completion-item.ts +++ b/packages/ai-code/src/module/completion-item/completion-item.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as IMonaco from 'monaco-editor'; import { createUUID } from 'qx-util'; -import { IConfig, Monaco } from '../interface'; +import { IConfig, Monaco, IModule } from '../interface'; import { cssVariableCode, TypeScriptVariable } from './variable'; import { resource } from '../../util'; /** @@ -10,7 +10,7 @@ import { resource } from '../../util'; * @export * @class CompletionItem */ -export class CompletionItem { +export class CompletionItem implements IModule { /** * 唯一标识 * @@ -337,4 +337,10 @@ export class CompletionItem { return false; return true; } + + /** + * @description 销毁 + * @memberof CompletionItem + */ + destroy(): void {} } diff --git a/packages/ai-code/src/module/index.ts b/packages/ai-code/src/module/index.ts index aa565125..edaa9ba1 100644 --- a/packages/ai-code/src/module/index.ts +++ b/packages/ai-code/src/module/index.ts @@ -1,7 +1,7 @@ /* eslint-disable no-shadow */ import * as IMonaco from 'monaco-editor'; import { InlineCompletions } from './inline-completions/inline-completions'; -import { IConfig, Monaco } from './interface'; +import { IConfig, IModule, Monaco } from './interface'; import { CompletionItem } from './completion-item/completion-item'; import { CustomLanguageTheme } from './custom-language-theme/custom-language-theme'; @@ -16,10 +16,10 @@ export class CodeModuleCenter { * 模块 * * @private - * @type {IData[]} + * @type {IModule[]} * @memberof CodeModuleCenter */ - private modules: IData[] = []; + private modules: IModule[] = []; /** * 主题 @@ -44,9 +44,21 @@ export class CodeModuleCenter { * @memberof CodeModuleCenter */ initModules(editor: IMonaco.editor.IStandaloneCodeEditor): void { - // 添加内联代码补全模块 - this.modules.push(new InlineCompletions(this.monaco, editor, this.config)); // 添加完成项补全模块 this.modules.push(new CompletionItem(this.monaco, editor, this.config)); + if (this.config.enableAICompletion) { + // 添加内联代码补全模块 + this.modules.push( + new InlineCompletions(this.monaco, editor, this.config), + ); + } + } + + /** + * @description 销毁 + * @memberof CodeModuleCenter + */ + destroy(): void { + this.modules.forEach(module => module.destroy()); } } diff --git a/packages/ai-code/src/module/inline-completions/inline-completions.ts b/packages/ai-code/src/module/inline-completions/inline-completions.ts index 083a5806..fc40d186 100644 --- a/packages/ai-code/src/module/inline-completions/inline-completions.ts +++ b/packages/ai-code/src/module/inline-completions/inline-completions.ts @@ -1,7 +1,11 @@ +/* eslint-disable no-useless-escape */ +import { calcResPath } from '@ibiz-template/runtime'; import * as IMonaco from 'monaco-editor'; import { createUUID } from 'qx-util'; import { getToken } from '@ibiz-template/core'; +import { IAppDataEntity } from '@ibiz/model-core'; import { IConfig, Monaco } from '../interface'; +import { WorkerManager } from './worker-manager'; interface ICompletionContext { /** @@ -90,13 +94,28 @@ export class InlineCompletions { private debounceTimer: number | null = null; /** - * 请求终止控制器 - * + * @description worker 管理器 + * @private + * @type {WorkerManager} + * @memberof InlineCompletions + */ + private workerManager: WorkerManager = new WorkerManager(); + + /** + * @description 应用实体模型 + * @private + * @type {IAppDataEntity} + * @memberof InlineCompletions + */ + private appEntityModel?: IAppDataEntity; + + /** + * @description 历史数据 * @private - * @type {(AbortController | null)} + * @type {IData[]} * @memberof InlineCompletions */ - private abortController: AbortController | null = null; + private history: IData[] = []; /** * Creates an instance of InlineCompletions. @@ -110,7 +129,67 @@ export class InlineCompletions { private editor: IMonaco.editor.IStandaloneCodeEditor, private config: IConfig, ) { + this.onInit(); + } + + /** + * @description 初始化 + * @private + * @returns {*} {Promise} + * @memberof InlineCompletions + */ + private async onInit(): Promise { + const { appDataEntityId, context } = this.config; + if (!appDataEntityId) return; + this.appEntityModel = await ibiz.hub.getAppDataEntity( + appDataEntityId, + context.srfappid, + ); this.registerInlineCompletions(); + await this.initHistory(); + } + + /** + * @description 计算AI请求路径 + * @private + * @param {boolean} [isHistories=false] 是否请求历史数据 + * @returns {*} {(string | undefined)} + * @memberof InlineCompletions + */ + private calcAIPath(isHistories: boolean = false): string | undefined { + const { deACMode, context } = this.config; + if (!this.appEntityModel || !deACMode) return; + const baseUrl = `${window.location.origin}${ibiz.env.baseUrl}/${ibiz.env.appId}`; + const srfkey = context[this.appEntityModel.codeName!.toLowerCase()]; + const curPath = `/${this.appEntityModel.deapicodeName2}/chatcompletion${ + isHistories ? '/histories' : '' + }${srfkey ? `/${srfkey}` : ''}`; + const resPath = calcResPath(context, this.appEntityModel); + const path = resPath + ? `/${resPath}${curPath}?srfactag=${deACMode.codeName}` + : `${curPath}`; + return `${baseUrl}${path}`; + } + + /** + * @description 初始化历史 + * @private + * @memberof InlineCompletions + */ + private async initHistory(): Promise { + const url = this.calcAIPath(true); + if (!url) return undefined; + const headers = this.getRequestHeaders(); + const result = await this.workerManager.post({ + url, + headers, + body: null, + enableAbort: false, + }); + if (result.ok) + this.history = result.data.filter(item => + ['USER', 'ASSISTANT'].includes(item.role), + ); } /** @@ -126,10 +205,8 @@ export class InlineCompletions { { provideInlineCompletions: async (model, position) => { this.visible = false; - const lineContent = model.getLineContent(position.lineNumber); - // 排除空白行 - if (!this.validate(model) || !lineContent.trim()) - return { items: [] }; + // 校验 + if (!this.validate(model, position)) return { items: [] }; // 清除之前的计时器 if (this.debounceTimer) { @@ -158,7 +235,7 @@ export class InlineCompletions { }, ], }); - }, 500); + }, 1000); }); }, freeInlineCompletions(_completion) {}, @@ -182,14 +259,16 @@ export class InlineCompletions { } /** - * 检查模型是否属于当前编辑器实例 - * + * @description 校验是否需要AI补全 * @private * @param {IMonaco.editor.ITextModel} model - * @return {*} {boolean} + * @returns {*} {boolean} * @memberof InlineCompletions */ - private validate(model: IMonaco.editor.ITextModel): boolean { + private validate( + model: IMonaco.editor.ITextModel, + position: IMonaco.Position, + ): boolean { const currentEditor = this.monaco.editor .getEditors() .find(e => e.getModel() === model); @@ -199,9 +278,68 @@ export class InlineCompletions { (currentEditor as IData).__inlineCompletionsId !== this.UUID ) return false; + const lineContent = model.getLineContent(position.lineNumber).trim(); + const languageId = model.getLanguageId(); + // 校验注释行 + if (this.isCommentLine(lineContent, languageId)) return false; + // 校验空白行 + if (lineContent === '') { + const prevLineNumber = position.lineNumber - 1; + if (prevLineNumber >= 1) { + const prevLineContent = model.getLineContent(prevLineNumber); + // 空白行上方为注释行才可补全 + return this.isCommentLine(prevLineContent, languageId); + } + return false; + } return true; } + /** + * @description 校验注释行 + * @private + * @param {string} lineContent 行内容 + * @param {string} languageId 语言 + * @returns {*} {boolean} + * @memberof InlineCompletions + */ + private isCommentLine(lineContent: string, languageId: string): boolean { + const trimmedLine = lineContent.trim(); + // 根据配置的语言来判断注释格式 + switch (languageId) { + case 'javascript': + case 'typescript': + case 'java': + case 'c': + case 'cpp': + case 'csharp': + return ( + trimmedLine.startsWith('//') || + trimmedLine.startsWith('/*') || + trimmedLine.startsWith('*') || + trimmedLine.endsWith('*/') + ); + + case 'python': + case 'ruby': + case 'yaml': + return trimmedLine.startsWith('#'); + + case 'html': + case 'xml': + return trimmedLine.startsWith(''); + + default: + // 默认使用 JavaScript 风格的注释判断 + return ( + trimmedLine.startsWith('//') || + trimmedLine.startsWith('/*') || + trimmedLine.startsWith('*') || + trimmedLine.endsWith('*/') + ); + } + } + /** * 获取指定位置前后行数据 * @@ -248,13 +386,13 @@ export class InlineCompletions { * * @param {IMonaco.editor.ITextModel} model * @param {IMonaco.Position} position - * @return {*} {(ICompletionContext | undefined)} + * @return {*} {(ICompletionContext)} * @memberof InlineCompletions */ private calcContext( model: IMonaco.editor.ITextModel, position: IMonaco.Position, - ): ICompletionContext | undefined { + ): ICompletionContext { const currentLine = model.getLineContent(position.lineNumber); const token = model.getWordAtPosition(position) || model.getWordUntilPosition(position); @@ -292,6 +430,36 @@ export class InlineCompletions { return headers; } + /** + * @description 构建AI消息 + * @private + * @param {ICompletionContext} context + * @returns {*} {string} + * @memberof InlineCompletions + */ + private buildMessages(context: ICompletionContext): IData { + const { currentLine, aboveLines, belowLines, languageId, position } = + context; + const above = '```' + `${languageId}\n` + aboveLines + '\n```'; + const current = '```' + `${languageId}\n` + currentLine + '\n```'; + const below = '```' + `${languageId}\n` + belowLines + '\n```'; + return { + messages: [ + ...this.history, + { + role: 'USER', + content: ` + 语言:${languageId} + 上文代码: ${above} + 当前行: ${current} + 下文代码: ${below} + 光标位置:${position.column - 2} + `, + }, + ], + }; + } + /** * 获取补全 * @@ -302,26 +470,29 @@ export class InlineCompletions { * @memberof InlineCompletions */ private async getCompletion( - _model: IMonaco.editor.ITextModel, - _position: IMonaco.Position, + model: IMonaco.editor.ITextModel, + position: IMonaco.Position, ): Promise { - // const context = this.calcContext(model, position); + const context = this.calcContext(model, position); + const messages = this.buildMessages(context); + const url = this.calcAIPath(); + if (!url) return undefined; + const headers = this.getRequestHeaders(); + const result = await this.workerManager.post({ + url, + headers, + enableAbort: true, + body: JSON.stringify(messages), + }); + if (result.ok) return result.data.choices?.[0].content; return undefined; - // if (this.abortController) this.abortController.abort(); - // this.abortController = new AbortController(); - // try { - // const response = await fetch('', { - // method: 'POST', - // body: JSON.stringify(context), - // headers: this.getRequestHeaders(), - // signal: this.abortController.signal, - // }); - // this.abortController = null; - // if (response.status !== 200) return; - // const completion = await response.text(); - // return completion; - // } catch (err: any) { - // if (err.name !== 'AbortError') ibiz.log.error(err); - // } + } + + /** + * @description 销毁 + * @memberof InlineCompletions + */ + destroy(): void { + this.workerManager.destroy(); } } diff --git a/packages/ai-code/src/module/inline-completions/worker-manager.ts b/packages/ai-code/src/module/inline-completions/worker-manager.ts new file mode 100644 index 00000000..17ea994e --- /dev/null +++ b/packages/ai-code/src/module/inline-completions/worker-manager.ts @@ -0,0 +1,156 @@ +/** + * @description worker 消息接口 + * @export + * @interface IWorkerMessage + */ +export interface IWorkerMessage { + /** + * @description 请求路径 + * @type {string} + * @memberof IWorkerMessage + */ + url: string; + /** + * @description 请求体 + * @type {(string | null)} + * @memberof IWorkerMessage + */ + body: string | null; + /** + * @description 请求头 + * @type {HeadersInit} + * @memberof IWorkerMessage + */ + headers: HeadersInit; + /** + * @description 是否启用中断 + * @type {boolean} + * @memberof IWorkerMessage + */ + enableAbort: boolean; +} + +/** + * @description Worker管理器 + * @export + * @class WorkerManager + */ +export class WorkerManager { + /** + * @description worker实例 + * @private + * @type {(Worker | null)} + * @memberof WorkerManager + */ + private worker: Worker | null; + + /** + * @description blobUrl + * @private + * @type {(string | null)} + * @memberof WorkerManager + */ + private blobUrl: string | null; + + /** + * @description worker线程代码 + * @readonly + * @private + * @type {string} + * @memberof WorkerManager + */ + private get workerCode(): string { + return ` + let abortController = null; + self.onmessage = async event => { + const { url, body, headers, method, enableAbort } = event.data; + try { + if (abortController) abortController.abort(); + if (enableAbort) abortController = new AbortController(); + const response = await fetch(url, { + body, + headers, + method, + signal: abortController?.signal, + }); + if (response.ok) { + const data = await response.json(); + self.postMessage({ + type: 'success', + data, + }); + } else { + self.postMessage({ + type: 'error', + error: response.message, + }); + } + } catch (error) { + // 不是取消操作的错误才抛出 + if (error.name !== 'AbortError') { + self.postMessage({ + type: 'error', + error: error.message, + }); + } + } finally { + abortController = null; + } + }; + `; + } + + /** + * Creates an instance of ManagedWorker. + * @memberof ManagedWorker + */ + constructor() { + const blob = new Blob([this.workerCode], { + type: 'application/javascript', + }); + this.blobUrl = URL.createObjectURL(blob); + this.worker = new Worker(this.blobUrl); + } + + /** + * @description post请求 + * @param {IWorkerMessage} message + * @returns {*} {Promise<{ ok: boolean, data: any }>} + * @memberof WorkerManager + */ + async post(message: IWorkerMessage): Promise<{ ok: boolean; data: any }> { + if (!this.worker) return { ok: false, data: undefined }; + return new Promise(resolve => { + this.worker!.postMessage({ + ...message, + method: 'POST', + }); + this.worker!.onmessage = event => { + const { type, data } = event.data; + const result = { + ok: type === 'success', + data: type === 'success' ? data : undefined, + }; + resolve(result); + }; + this.worker!.onerror = () => { + resolve({ ok: false, data: undefined }); + }; + }); + } + + /** + * @description 销毁 + * @memberof WorkerManager + */ + destroy(): void { + if (this.worker) { + this.worker.terminate(); + this.worker = null; + } + if (this.blobUrl) { + URL.revokeObjectURL(this.blobUrl); + this.blobUrl = null; + } + } +} diff --git a/packages/ai-code/src/module/interface.ts b/packages/ai-code/src/module/interface.ts index e2c3eaef..e56da8b2 100644 --- a/packages/ai-code/src/module/interface.ts +++ b/packages/ai-code/src/module/interface.ts @@ -1,3 +1,4 @@ +import { IAppDEACMode } from '@ibiz/model-core'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; export type Monaco = typeof monacoEditor; @@ -51,6 +52,24 @@ export interface IConfig { * @memberof IConfig */ libName: string; + /** + * @description 应用实体标识 + * @type {string} + * @memberof IConfig + */ + appDataEntityId?: string; + /** + * @description 自填模式 + * @type {IAppDEACMode} + * @memberof IConfig + */ + deACMode?: IAppDEACMode; + /** + * @description 是否启用AI补全 + * @type {boolean} + * @memberof IConfig + */ + enableAICompletion: boolean; /** * 全局变量映射 * @@ -59,3 +78,11 @@ export interface IConfig { */ globalVariable?: string; } + +export interface IModule { + /** + * @description 销毁 + * @memberof IModule + */ + destroy(): void; +} diff --git a/packages/ai-code/src/monaco-editor/monaco-editor.tsx b/packages/ai-code/src/monaco-editor/monaco-editor.tsx index b504bdfd..3d10da06 100644 --- a/packages/ai-code/src/monaco-editor/monaco-editor.tsx +++ b/packages/ai-code/src/monaco-editor/monaco-editor.tsx @@ -43,7 +43,7 @@ export const IBizAICode = defineComponent({ setup(props, { emit }) { const codeEditBox = ref(); const ns = useNamespace('code'); - const c = props.controller!; + const c: CodeEditorController = props.controller!; const UUID = createUUID(); const currentVal = ref(''); @@ -71,6 +71,12 @@ export const IBizAICode = defineComponent({ // 额外库文件名称 const libName = 'ibiz-global.d.ts'; + // 补全模式 + let completionmode: 'aichat' | 'inline' | 'all' = 'all'; + + // 行内补全模式 + let inlinecompletionmode: string; + // 全局变量映射 let globalVariable = ''; @@ -89,6 +95,15 @@ export const IBizAICode = defineComponent({ editorModel.editorParams.enableFullScreen, ); } + if (editorModel.editorParams.completionmode) { + completionmode = editorModel.editorParams.completionmode; + } + if ( + completionmode === 'inline' && + editorModel.editorParams.completionmode + ) { + inlinecompletionmode = editorModel.editorParams.completionmode; + } if (editorModel.editorParams.GLOBALVARIABLE) { globalVariable = editorModel.editorParams.GLOBALVARIABLE; } @@ -164,6 +179,7 @@ export const IBizAICode = defineComponent({ inlineCompletionsProviderDisposable?.dispose(); inlineCompletionsProviderDisposable = null; chatInstance?.close(); + codeModuleCenter?.destroy(); }; /** @@ -527,13 +543,16 @@ export const IBizAICode = defineComponent({ // 初始化编辑器 if (!editor) { codeModuleCenter = new CodeModuleCenter(loaderMonaco, { + libName, + globalVariable, id: c.model.id!, data: props.data, - context: c.context, params: c.params, + context: c.context, + deACMode: c.deACMode, + enableAICompletion: ['all', 'inline'].includes(completionmode), + appDataEntityId: c.model.appDataEntityId, language: props.language || props.controller.language, - libName, - globalVariable, }); monacoEditor = loaderMonaco.editor; editor = monacoEditor.create(codeEditBox.value, { @@ -571,7 +590,7 @@ export const IBizAICode = defineComponent({ )?.widget?.value?._setDetailsVisible(true); waitForTSFileReady(loaderMonaco, libName); codeModuleCenter.initModules(editor); - if (c.deACMode) { + if (c.deACMode && ['all', 'aichat'].includes(completionmode)) { codeLensProviderDisposable = loaderMonaco.languages.registerCodeLensProvider( props.language || props.controller.language, -- Gitee