From 1e5a4c1ff50f506ed896fce098569715fe7e713a Mon Sep 17 00:00:00 2001 From: fofolee Date: Fri, 21 Feb 2025 11:31:45 +0800 Subject: [PATCH] =?UTF-8?q?quickcommand.askAI=E6=94=AF=E6=8C=81=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=93=8D=E5=BA=94=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E9=BB=98=E8=AE=A4api=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/ai.js | 85 ++++++++----- plugin/lib/dialog/service.js | 110 ++-------------- plugin/lib/quickcommand.js | 5 + plugin/lib/quickcomposer/windows/monitor.js | 4 +- src/components/ai/AIAssistantSideBar.vue | 16 +-- src/js/composer/commands/uiCommands.js | 69 +--------- .../monaco/types/quickcommand.api.d.ts | 119 ++++++++---------- 7 files changed, 143 insertions(+), 265 deletions(-) diff --git a/plugin/lib/ai.js b/plugin/lib/ai.js index 6eb3cd7..8b59b46 100644 --- a/plugin/lib/ai.js +++ b/plugin/lib/ai.js @@ -72,7 +72,7 @@ function buildRequestConfig(apiConfig) { } // 构建请求数据 -function buildRequestData(content, apiConfig, stream = false) { +function buildRequestData(content, apiConfig) { const { model } = apiConfig; const { prompt, role, context = [] } = content; const rolePrompt = ROLE_PROMPTS[role] || role; @@ -105,7 +105,7 @@ function buildRequestData(content, apiConfig, stream = false) { return { model, messages, - stream, + stream: true, }; } @@ -222,7 +222,7 @@ async function handleStreamResponse(response, apiConfig, controller, onStream) { reader.releaseLock(); } - return { success: true, result: "流式请求完成" }; + return { success: true }; } /** @@ -234,28 +234,24 @@ async function handleStreamResponse(response, apiConfig, controller, onStream) { */ async function chat(content, apiConfig, options = {}) { try { - const { showLoadingBar = true, stream = false, onStream } = options; + const { showProcessBar = true, onStream = () => {} } = options; // 验证必要参数 if (!apiConfig.apiUrl || !content.prompt || !apiConfig.model) { throw new Error("API地址、模型名称和提示词不能为空"); } - if (stream && !onStream) { - throw new Error("使用流式请求时必须提供onStream回调函数"); - } - // 构建请求URL和配置 const url = buildApiUrl( apiConfig.apiUrl, API_ENDPOINTS[apiConfig.apiType].chat ); - const config = buildRequestConfig(apiConfig, stream); - const requestData = buildRequestData(content, apiConfig, stream); + const config = buildRequestConfig(apiConfig); + const requestData = buildRequestData(content, apiConfig); - // 显示加载条 - const loadingBar = showLoadingBar - ? await quickcommand.showLoadingBar({ + // 显示进度条 + const processBar = showProcessBar + ? await quickcommand.showProcessBar({ text: "AI思考中...", onClose: () => { if (controller) { @@ -265,6 +261,26 @@ async function chat(content, apiConfig, options = {}) { }) : null; + // 用于收集完整响应 + let fullResponse = ""; + + // 包装 onStream 回调以收集完整响应并更新进度条 + const streamHandler = (chunk, controller, isDone) => { + if (!isDone) { + fullResponse += chunk; + // 更新进度条显示最新的响应内容 + if (processBar) { + quickcommand.updateProcessBar( + { + text: fullResponse, // 只显示最后100个字符,避免内容过长 + }, + processBar + ); + } + } + onStream(chunk, controller, isDone); + }; + // 统一使用 fetch 处理请求 const controller = new AbortController(); const response = await fetch(url, { @@ -278,24 +294,35 @@ async function chat(content, apiConfig, options = {}) { throw new Error(`HTTP error! status: ${response.status}`); } - let result; - if (stream) { - result = await handleStreamResponse( - response, - apiConfig, - controller, - onStream - ); - } else { - const responseData = await response.json(); - result = { - success: true, - result: parseResponse({ data: responseData }, apiConfig.apiType), - }; + const result = await handleStreamResponse( + response, + apiConfig, + controller, + streamHandler + ); + + // 如果请求被取消,返回取消状态 + if (!result.success) { + processBar?.close(); + return result; } - loadingBar?.close(); - return result; + // 完成时更新进度条并关闭 + if (processBar) { + quickcommand.updateProcessBar( + { + text: "AI响应完成", + complete: true, + }, + processBar + ); + } + + // 返回完整的响应内容 + return { + success: true, + result: fullResponse, + }; } catch (error) { if (error.name === "AbortError") { return { diff --git a/plugin/lib/dialog/service.js b/plugin/lib/dialog/service.js index 836aaf1..ed25cad 100644 --- a/plugin/lib/dialog/service.js +++ b/plugin/lib/dialog/service.js @@ -305,7 +305,7 @@ let lastProcessBar = null; * @param {object} options - 配置选项 * @param {string} [options.title="进度"] - 对话框标题 * @param {string} [options.text="处理中..."] - 进度条上方的文本 - * @param {number} [options.value=0] - 初始进度值(0-100) + * @param {number} [options.value] - 初始进度值(0-100),不传则显示加载动画 * @param {string} [options.position="bottom-right"] - 进度条位置,可选值:top-left, top-right, bottom-left, bottom-right * @param {Function} [options.onClose] - 关闭按钮点击时的回调函数 * @param {Function} [options.onPause] - 暂停按钮点击时的回调函数 @@ -316,7 +316,7 @@ let lastProcessBar = null; const showProcessBar = async (options = {}) => { const { text = "处理中...", - value = 0, + value, position = "bottom-right", onClose, onPause, @@ -328,7 +328,7 @@ const showProcessBar = async (options = {}) => { throw new Error("onPause 和 onResume 必须同时配置"); } - const windowWidth = 350; + const windowWidth = 250; const windowHeight = 60; // 计算窗口位置 @@ -357,10 +357,11 @@ const showProcessBar = async (options = {}) => { ipcRenderer.sendTo(windowId, "dialog-config", { type: "process", text, - value, + value: value === undefined ? 0 : value, isDark: utools.isDarkColors(), platform: process.platform, showPause: Boolean(onPause && onResume), + isLoading: value === undefined, // 当不传value时显示加载动画 }); // 监听窗口准备就绪 @@ -410,7 +411,7 @@ const showProcessBar = async (options = {}) => { /** * 更新进度条的进度 * @param {object} options - 配置选项 - * @param {number} options.value - 新的进度值(0-100) + * @param {number} [options.value] - 新的进度值(0-100),不传则显示加载动画 * @param {string} [options.text] - 新的进度文本 * @param {boolean} [options.complete] - 是否完成并关闭进度条 * @param {{id: number, close: Function}|undefined} processBar - 进度条对象, 如果不传入则使用上一次创建的进度条 @@ -433,7 +434,11 @@ const updateProcessBar = (options = {}, processBar = null) => { } const { value, text, complete } = options; - ipcRenderer.sendTo(processBar.id, "update-process", { value, text }); + ipcRenderer.sendTo(processBar.id, "update-process", { + value, + text, + isLoading: value === undefined, + }); if (complete) { setTimeout(() => { @@ -442,97 +447,6 @@ const updateProcessBar = (options = {}, processBar = null) => { } }; -let lastLoadingBar = null; - -/** - * 显示一个加载条对话框 - * @param {object} options - 配置选项 - * @param {string} [options.text="加载中..."] - 加载条上方的文本 - * @param {string} [options.position="bottom-right"] - 加载条位置,可选值:top-left, top-right, bottom-left, bottom-right - * @param {Function} [options.onClose] - 关闭按钮点击时的回调函数 - * @returns {Promise<{id: number, close: Function}>} 返回加载条窗口ID和关闭函数 - */ -const showLoadingBar = async (options = {}) => { - const { text = "加载中...", position = "bottom-right", onClose } = options; - - const windowWidth = 250; - const windowHeight = 60; - - // 计算窗口位置 - const { x, y } = calculateWindowPosition({ - position, - width: windowWidth, - height: windowHeight, - }); - - return new Promise((resolve) => { - const UBrowser = createBrowserWindow( - dialogPath, - { - width: windowWidth, - height: windowHeight, - x, - y, - opacity: 0, - focusable: false, - ...commonBrowserWindowOptions, - }, - () => { - const windowId = UBrowser.webContents.id; - - // 发送配置到子窗口 - ipcRenderer.sendTo(windowId, "dialog-config", { - type: "process", - text, - value: 0, - isDark: utools.isDarkColors(), - platform: process.platform, - isLoading: true, // 标记为加载条模式 - }); - - // 监听窗口准备就绪 - ipcRenderer.once("dialog-ready", () => { - UBrowser.setOpacity(1); - }); - - // 监听对话框结果 - ipcRenderer.once("dialog-result", (event, result) => { - if (result === "close" && typeof onClose === "function") { - onClose(); - } - UBrowser.destroy(); - }); - - const loadingBar = { - id: windowId, - close: () => { - if (typeof onClose === "function") { - onClose(); - } - lastLoadingBar = null; - UBrowser.destroy(); - }, - }; - - lastLoadingBar = loadingBar; - - // 返回窗口ID和关闭函数 - resolve(loadingBar); - } - ); - }); -}; - -const closeLoadingBar = (loadingBar) => { - if (!loadingBar) { - if (!lastLoadingBar) { - throw new Error("没有找到已创建的加载条"); - } - loadingBar = lastLoadingBar; - } - loadingBar.close(); -}; - module.exports = { showSystemMessageBox, showSystemInputBox, @@ -543,6 +457,4 @@ module.exports = { showSystemWaitButton, showProcessBar, updateProcessBar, - showLoadingBar, - closeLoadingBar, }; diff --git a/plugin/lib/quickcommand.js b/plugin/lib/quickcommand.js index b8a371d..9ad9323 100644 --- a/plugin/lib/quickcommand.js +++ b/plugin/lib/quickcommand.js @@ -8,6 +8,8 @@ const axios = require("axios"); const marked = require("marked"); const { chat, getModels } = require("./ai"); +const { dbStorage } = utools; + window.getModelsFromAiApi = getModels; const systemDialog = require("./dialog/service"); @@ -197,6 +199,9 @@ const quickcommand = { }, askAI: async function (content, apiConfig, options) { + if (window.lodashM.isEmpty(apiConfig)) { + apiConfig = dbStorage.getItem("cfg_aiConfigs")?.[0] || {}; + } return await chat(content, apiConfig, options); }, diff --git a/plugin/lib/quickcomposer/windows/monitor.js b/plugin/lib/quickcomposer/windows/monitor.js index f896a6c..f7b50c9 100644 --- a/plugin/lib/quickcomposer/windows/monitor.js +++ b/plugin/lib/quickcomposer/windows/monitor.js @@ -8,7 +8,7 @@ const stopMonitor = () => { // 监控剪贴板变化 const watchClipboard = async function () { const args = ["-type", "clipboard", "-once"]; - const loadingBar = await quickcommand.showLoadingBar({ + const loadingBar = await quickcommand.showProcessBar({ text: "等待剪贴板变化...", onClose: () => { stopMonitor(); @@ -44,7 +44,7 @@ const watchFileSystem = async function (watchPath, options = {}) { args.push("-recursive", "false"); } - const loadingBar = await quickcommand.showLoadingBar({ + const loadingBar = await quickcommand.showProcessBar({ text: "等待文件变化...", onClose: () => { stopMonitor(); diff --git a/src/components/ai/AIAssistantSideBar.vue b/src/components/ai/AIAssistantSideBar.vue index 77f2478..7a1f0ed 100644 --- a/src/components/ai/AIAssistantSideBar.vue +++ b/src/components/ai/AIAssistantSideBar.vue @@ -91,7 +91,9 @@ :icon="streamingResponse ? 'stop' : 'send'" @click="handleSubmit" > - Enter 发送,Shift+Enter 换行 + + Enter 发送,Shift+Enter 换行 + @@ -183,8 +185,7 @@ export default defineComponent({ }, this.selectedApi, { - showLoadingBar: false, - stream: true, + showProcessBar: false, onStream: (text, controller, done) => { this.currentRequest = controller; if (text) { @@ -251,7 +252,8 @@ export default defineComponent({ const languageMap = { quickcommand: "NodeJS", javascript: "NodeJS", - cmd: "bat", + cmd: "windows 批处理脚本", + shell: "liunx shell脚本", }; const commonInstructions = `请根据我的需求编写${languageMap[language]}代码,并请遵循以下原则: - 编写简洁、可读性强的代码 @@ -275,7 +277,7 @@ export default defineComponent({ languageSpecific[language.toLowerCase()] || ""; const lastInstructions = - "\n请直接给我代码,任何情况下都不需要做解释和说明"; + "\n请直接给我MARKDOWN格式的代码,任何情况下都不需要做解释和说明"; return commonInstructions + specificInstructions + lastInstructions; }, @@ -306,13 +308,13 @@ export default defineComponent({ }, ]; - if (this.submitDocs) { + if (this.submitDocs && this.language === "quickcommand") { const docs = this.getLanguageDocs(this.language); presetContext.push( { role: "user", - content: `你现在使用的是一种特殊的环境,支持uTools和quickcommand两种特殊的接口,请优先使用uTools和quickcommand接口解决需求`, + content: `你现在使用的是一种特殊的环境,支持uTools和quickcommand两种特殊的接口,请优先使用uTools和quickcommand接口解决需求,然后再使用当前语言通用的解决方案`, }, { role: "assistant", diff --git a/src/js/composer/commands/uiCommands.js b/src/js/composer/commands/uiCommands.js index 2981351..413727a 100644 --- a/src/js/composer/commands/uiCommands.js +++ b/src/js/composer/commands/uiCommands.js @@ -446,8 +446,9 @@ export const uiCommands = { width: 4, }, value: { - label: "初始进度值", + label: "初始进度值(0-100)", component: "VariableInput", + placeholder: "留空则显示加载动画", disableToggleType: true, width: 4, }, @@ -486,7 +487,7 @@ export const uiCommands = { defaultValue: { title: newVarInputVal("str", "进度"), text: newVarInputVal("str", "处理中..."), - value: newVarInputVal("var", "0"), + value: newVarInputVal("var"), position: "bottom-right", onClose: newVarInputVal("var"), onPause: newVarInputVal("var"), @@ -504,8 +505,9 @@ export const uiCommands = { component: "OptionEditor", options: { value: { - label: "进度值", + label: "进度值(0-100)", component: "VariableInput", + placeholder: "留空则显示加载动画", width: 4, disableToggleType: true, }, @@ -522,7 +524,7 @@ export const uiCommands = { }, }, defaultValue: { - value: newVarInputVal("var", "0"), + value: newVarInputVal("var", "100"), text: newVarInputVal("str"), complete: false, }, @@ -537,65 +539,6 @@ export const uiCommands = { }, ], }, - { - value: "quickcommand.showLoadingBar", - label: "显示载入界面", - neverHasOutput: true, - asyncMode: "await", - config: [ - { - component: "OptionEditor", - options: { - text: { - label: "文本", - component: "VariableInput", - width: 4, - }, - position: { - label: "位置", - component: "QSelect", - width: 4, - options: [ - { label: "屏幕左上角", value: "top-left" }, - { label: "屏幕右上角", value: "top-right" }, - { label: "屏幕左下角", value: "bottom-left" }, - { label: "屏幕右下角", value: "bottom-right" }, - ], - }, - onClose: { - label: "关闭按钮回调函数", - component: "VariableInput", - disableToggleType: true, - width: 4, - }, - }, - defaultValue: { - text: newVarInputVal("str", "加载中..."), - position: "bottom-right", - onClose: newVarInputVal("var"), - }, - }, - ], - outputs: { - label: "载入条对象", - suggestName: "loadingBar", - }, - }, - { - value: "quickcommand.closeLoadingBar", - label: "关闭载入界面", - neverHasOutput: true, - config: [ - { - label: "载入条对象", - component: "VariableInput", - placeholder: "不传则关闭最近的载入条", - width: 12, - defaultValue: newVarInputVal("var"), - disableToggleType: true, - }, - ], - }, { value: "utools.showOpenDialog", label: "文件选择框", diff --git a/src/plugins/monaco/types/quickcommand.api.d.ts b/src/plugins/monaco/types/quickcommand.api.d.ts index 2f66d6f..76eeb17 100644 --- a/src/plugins/monaco/types/quickcommand.api.d.ts +++ b/src/plugins/monaco/types/quickcommand.api.d.ts @@ -750,7 +750,7 @@ interface quickcommandApi { * 显示一个带有暂停、恢复、关闭回调功能的进度条,支持动态更新进度 * @param {object} options - 配置选项 * @param {string} [options.text="处理中..."] - 进度条上方的文本 - * @param {number} [options.value=0] - 初始进度值(0-100) + * @param {number} [options.value] - 初始进度值(0-100),不传则显示加载动画 * @param {string} [options.position="bottom-right"] - 进度条位置,可选值:top-left, top-right, bottom-left, bottom-right * @param {Function} [options.onClose] - 关闭按钮点击时的回调函数 * @param {Function} [options.onPause] - 暂停按钮点击时的回调函数,必须和onResume一起配置 @@ -758,13 +758,19 @@ interface quickcommandApi { * @returns {Promise<{id: number, close: Function}>} 返回进度条对象 * * ```js - * // 基本使用 + * // 显示进度条 * const processBar = await quickcommand.showProcessBar({ * text: "正在下载文件...", * value: 0, * position: "bottom-right" * }); * + * // 显示加载动画 + * const processBar = await quickcommand.showProcessBar({ + * text: "正在加载...", + * position: "bottom-right" + * }); + * * // 带暂停/恢复,关闭回调功能 * let isPaused = false; * const processBar = await quickcommand.showProcessBar({ @@ -824,18 +830,23 @@ interface quickcommandApi { /** * 更新进度条的进度 * @param {object} options - 配置选项 - * @param {number} options.value - 新的进度值(0-100) + * @param {number} [options.value] - 新的进度值(0-100),不传则显示加载动画 * @param {string} [options.text] - 新的进度文本 * @param {boolean} [options.complete] - 是否完成并关闭进度条 * @param {{id: number, close: Function}|undefined} processBar - 进度条对象,如果不传入则使用上一次创建的进度条 * * ```js - * // 使用最近创建的进度条 + * // 更新进度 * quickcommand.updateProcessBar({ * value: 50, * text: "已完成50%" * }); * + * // 切换为加载动画 + * quickcommand.updateProcessBar({ + * text: "正在加载..." + * }); + * * // 使用指定的进度条 * quickcommand.updateProcessBar({ * value: 50, @@ -852,7 +863,7 @@ interface quickcommandApi { */ updateProcessBar( options: { - value: number; + value?: number; text?: string; complete?: boolean; }, @@ -862,71 +873,29 @@ interface quickcommandApi { } ): void; - /** - * 显示一个循环动画的加载条 - * @param {object} options - 配置选项 - * @param {string} [options.text="加载中..."] - 加载条上方的文本 - * @param {string} [options.position="bottom-right"] - 加载条位置,可选值:top-left, top-right, bottom-left, bottom-right - * @param {Function} [options.onClose] - 关闭按钮点击时的回调函数 - * @returns {Promise<{id: number, close: Function}>} 返回加载条对象 - * - * ```js - * // 基本使用 - * const loadingBar = await quickcommand.showLoadingBar({ - * text: "正在加载...", - * position: "bottom-right" - * }); - * - * // 带关闭回调 - * const loadingBar = await quickcommand.showLoadingBar({ - * text: "正在加载...", - * onClose: () => { - * console.log("用户关闭了加载条"); - * } - * }); - * - * // 手动关闭 - * loadingBar.close(); - * // 或者 - * quickcommand.closeLoadingBar(); - * ``` - */ - showLoadingBar(options?: { - text?: string; - position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; - onClose?: () => void; - }): Promise<{ - id: number; - close: () => void; - }>; - - /** - * 关闭加载条 - * @param {{id: number, close: Function}|undefined} loadingBar - 加载条对象,如果不传入则关闭上一次创建的加载条 - * - * ```js - * // 关闭最近创建的加载条 - * quickcommand.closeLoadingBar(); - * - * // 关闭指定的加载条 - * quickcommand.closeLoadingBar(loadingBar); - * ``` - */ - closeLoadingBar(loadingBar?: { id: number; close: () => void }): void; - /** * 与 AI 进行问答 * @param content 对话内容 * @param content.prompt 提示词 * @param content.role 预设角色 - * @param apiConfig API配置 + * @param apiConfig API配置,不传或传入null则使用用户配置的第一个API配置 * @param apiConfig.apiType 模型类型:openai/ollama * @param apiConfig.apiUrl API地址 * @param apiConfig.apiToken API令牌(仅 OpenAI 需要) * @param apiConfig.model 模型名称 * @param options 其他选项 - * @param options.showLoadingBar 是否显示加载条 - * @example + * @param options.showProcessBar 是否显示进度条 + * @param options.onStream 流式请求回调 + * + * + * ```js + * // 不传apiConfig时,需在配置页面-右下角菜单-AI配置中进行配置 + * const response = await quickcommand.askAI( + * { + * prompt: "你好", + * } + * ); + * * // OpenAI 示例 * const response = await quickcommand.askAI( * { @@ -939,9 +908,10 @@ interface quickcommandApi { * model: "gpt-3.5-turbo" * } * ); + * console.log(response); * - * // Ollama 示例 - * const response = await quickcommand.askAI( + * // Ollama 示例 (流式回调) + * await quickcommand.askAI( * { * prompt: "查找进程名为chrome的进程并关闭", * role: "shell" @@ -950,8 +920,20 @@ interface quickcommandApi { * apiType: "ollama", * apiUrl: "http://localhost:11434", * model: "qwen2.5:32b" + * }, + * { + * onStream: (chunk, controller, isDone) => { + * console.log(chunk); + * if (某个特定条件,中断请求) { + * controller.abort(); + * } + * if (isDone) { + * console.log("流式请求完成"); + * } + * } * } * ); + * ``` */ askAI( content: { @@ -960,7 +942,8 @@ interface quickcommandApi { /** 预设角色 */ role?: "translate" | "shell" | "summarize"; }, - apiConfig: { + /** API配置,不传或传入null则使用用户配置的第一个API配置 */ + apiConfig?: { /** 模型类型:openai/ollama */ apiType: "openai" | "ollama"; /** API地址 */ @@ -971,8 +954,14 @@ interface quickcommandApi { model: string; }, options?: { - /** 是否显示加载条, 默认 true */ - showLoadingBar?: boolean; + /** 是否显示进度条, 默认 true */ + showProcessBar?: boolean; + /** 流式请求回调 */ + onStream?: ( + chunk: string, + controller: AbortController, + isDone: boolean + ) => void; } ): Promise<{ /** 是否成功 */