mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-12-17 17:24:27 +08:00
Compare commits
9 Commits
dependabot
...
features/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1c98fb4a | ||
|
|
0d4f49fcf4 | ||
|
|
55516159ba | ||
|
|
d4e58e58be | ||
|
|
d1b117186f | ||
|
|
3fb17dcd99 | ||
|
|
ffbae3f33f | ||
|
|
a762b87d2b | ||
|
|
3d2d15e177 |
144
plugin/lib/ai.js
144
plugin/lib/ai.js
@@ -17,6 +17,7 @@ window.aiResponseParser = (content) => {
|
||||
const API_TYPES = {
|
||||
OPENAI: "openai",
|
||||
OLLAMA: "ollama",
|
||||
UTOOLS: "utools",
|
||||
};
|
||||
|
||||
// 角色提示词
|
||||
@@ -122,7 +123,7 @@ function buildRequestData(content, apiConfig) {
|
||||
const roleMessage = rolePrompt
|
||||
? [
|
||||
{
|
||||
role: "user",
|
||||
role: "system",
|
||||
content: rolePrompt,
|
||||
},
|
||||
]
|
||||
@@ -151,21 +152,6 @@ function buildRequestData(content, apiConfig) {
|
||||
};
|
||||
}
|
||||
|
||||
// 处理普通响应
|
||||
function parseResponse(response, apiType) {
|
||||
if (apiType === API_TYPES.OPENAI) {
|
||||
if (!response.data.choices || !response.data.choices[0]) {
|
||||
throw new Error("OpenAI 响应格式错误");
|
||||
}
|
||||
return response.data.choices[0].message.content;
|
||||
} else {
|
||||
if (!response.data.message) {
|
||||
throw new Error("Ollama 响应格式错误");
|
||||
}
|
||||
return response.data.message.content;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模型列表响应
|
||||
function parseModelsResponse(response, apiType) {
|
||||
if (apiType === API_TYPES.OPENAI) {
|
||||
@@ -181,6 +167,24 @@ function parseModelsResponse(response, apiType) {
|
||||
}
|
||||
}
|
||||
|
||||
let reasoning_content_start = false;
|
||||
function processContentWithReason(response, onStream) {
|
||||
if (response.reasoning_content) {
|
||||
if (!reasoning_content_start) {
|
||||
reasoning_content_start = true;
|
||||
onStream("<think>", false);
|
||||
}
|
||||
onStream(response.reasoning_content, false);
|
||||
}
|
||||
if (response.content) {
|
||||
if (reasoning_content_start) {
|
||||
reasoning_content_start = false;
|
||||
onStream("</think>", false);
|
||||
}
|
||||
onStream(response.content, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 OpenAI 流式响应
|
||||
async function handleOpenAIStreamResponse(line, onStream) {
|
||||
if (line.startsWith("data:")) {
|
||||
@@ -190,9 +194,9 @@ async function handleOpenAIStreamResponse(line, onStream) {
|
||||
return;
|
||||
}
|
||||
const json = JSON.parse(jsonStr);
|
||||
const content = json.choices[0]?.delta?.content;
|
||||
if (content) {
|
||||
onStream(content, false);
|
||||
const response = json.choices[0]?.delta;
|
||||
if (response) {
|
||||
processContentWithReason(response, onStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,13 +208,30 @@ async function handleOllamaStreamResponse(line, onStream) {
|
||||
onStream("", true);
|
||||
return;
|
||||
}
|
||||
if (json.message?.content) {
|
||||
onStream(json.message.content, false);
|
||||
const response = json.message;
|
||||
if (response) {
|
||||
processContentWithReason(response, onStream);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 uTools AI 流式响应
|
||||
async function handleUToolsAIStreamResponse(response, onStream) {
|
||||
processContentWithReason(response, onStream);
|
||||
}
|
||||
|
||||
// 处理流式响应
|
||||
async function handleStreamResponse(response, apiConfig, onStream) {
|
||||
// 处理 uTools AI 响应
|
||||
if (apiConfig.apiType === API_TYPES.UTOOLS) {
|
||||
try {
|
||||
await handleUToolsAIStreamResponse(response, onStream);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他 API 的流式响应
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
@@ -283,24 +304,27 @@ async function chat(content, apiConfig, options = {}) {
|
||||
} = options;
|
||||
|
||||
// 验证必要参数
|
||||
if (!apiConfig.apiUrl || !content.prompt || !apiConfig.model) {
|
||||
throw new Error("API地址、模型名称和提示词不能为空");
|
||||
if (apiConfig.apiType === API_TYPES.UTOOLS) {
|
||||
if (!content.prompt || !apiConfig.model) {
|
||||
throw new Error("模型名称和提示词不能为空");
|
||||
}
|
||||
} else {
|
||||
if (!apiConfig.apiUrl) {
|
||||
throw new Error("API地址不能为空");
|
||||
}
|
||||
if (!apiConfig.apiUrl || !content.prompt || !apiConfig.model) {
|
||||
throw new Error("API地址、模型名称和提示词不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求URL和配置
|
||||
const url = buildApiUrl(
|
||||
apiConfig.apiUrl,
|
||||
API_ENDPOINTS[apiConfig.apiType].chat
|
||||
);
|
||||
const config = buildRequestConfig(apiConfig);
|
||||
const requestData = buildRequestData(content, apiConfig);
|
||||
let controller;
|
||||
|
||||
// 显示进度条
|
||||
const processBar = showProcessBar
|
||||
? await quickcommand.showProcessBar({
|
||||
text: "AI思考中...",
|
||||
onClose: () => {
|
||||
if (controller) {
|
||||
if (typeof controller !== "undefined") {
|
||||
controller.abort();
|
||||
}
|
||||
},
|
||||
@@ -327,11 +351,65 @@ async function chat(content, apiConfig, options = {}) {
|
||||
onStream(chunk, isDone);
|
||||
};
|
||||
|
||||
// 统一使用 fetch 处理请求
|
||||
const controller = new AbortController();
|
||||
// 处理 uTools AI 请求
|
||||
if (apiConfig.apiType === API_TYPES.UTOOLS) {
|
||||
try {
|
||||
const messages = buildRequestData(content, apiConfig).messages;
|
||||
controller = utools.ai(
|
||||
{
|
||||
model: apiConfig.model,
|
||||
messages: messages,
|
||||
},
|
||||
(chunk) => {
|
||||
handleUToolsAIStreamResponse(chunk, streamHandler);
|
||||
}
|
||||
);
|
||||
onFetch(controller);
|
||||
|
||||
await controller;
|
||||
|
||||
// 在流式响应完全结束后,发送一个空字符串表示结束
|
||||
streamHandler("", true);
|
||||
|
||||
// 完成时更新进度条并关闭
|
||||
if (processBar) {
|
||||
quickcommand.updateProcessBar(
|
||||
{
|
||||
text: "AI响应完成",
|
||||
complete: true,
|
||||
},
|
||||
processBar
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: fullResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
return {
|
||||
success: false,
|
||||
error: "请求已取消",
|
||||
cancelled: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 统一使用 fetch 处理其他 API 请求
|
||||
controller = new AbortController();
|
||||
|
||||
onFetch(controller);
|
||||
|
||||
const url = buildApiUrl(
|
||||
apiConfig.apiUrl,
|
||||
API_ENDPOINTS[apiConfig.apiType].chat
|
||||
);
|
||||
const config = buildRequestConfig(apiConfig);
|
||||
const requestData = buildRequestData(content, apiConfig);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: config.headers,
|
||||
|
||||
30
plugin/lib/pin/controller.js
Normal file
30
plugin/lib/pin/controller.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
// 等待 DOM 加载完成
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
let parentId = null;
|
||||
let windowId = null;
|
||||
let commandCode = null;
|
||||
|
||||
// 监听父窗口发来的配置
|
||||
ipcRenderer.on("window-config", (event, config) => {
|
||||
parentId = event.senderId;
|
||||
windowId = config.windowId;
|
||||
commandCode = config.commandCode;
|
||||
|
||||
// 设置主题
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
config.isDark ? "dark" : "light"
|
||||
);
|
||||
|
||||
// 设置图标
|
||||
document.getElementById("command-icon").src = config.icon;
|
||||
});
|
||||
|
||||
// 点击图标执行命令
|
||||
document.querySelector(".pin-container").addEventListener("click", () => {
|
||||
console.log("click", parentId, `pin-execute-${windowId}`);
|
||||
ipcRenderer.sendTo(parentId, `pin-execute-${windowId}`, commandCode);
|
||||
});
|
||||
});
|
||||
108
plugin/lib/pin/service.js
Normal file
108
plugin/lib/pin/service.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { createBrowserWindow } = utools;
|
||||
|
||||
const pinPath = "lib/pin/view.html";
|
||||
const preloadPath = "lib/pin/controller.js";
|
||||
|
||||
// 存储所有pin窗口的信息
|
||||
const pinWindows = new Map();
|
||||
|
||||
/**
|
||||
* 创建pin窗口
|
||||
* @param {object} commandInfo - 命令信息
|
||||
* @param {object} position - 窗口位置 {x, y}
|
||||
* @returns {Promise} 返回窗口对象
|
||||
*/
|
||||
const createPinWindow = (commandInfo, position = null) => {
|
||||
return new Promise((resolve) => {
|
||||
const windowOptions = {
|
||||
width: 52,
|
||||
height: 52,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
skipTaskbar: true,
|
||||
alwaysOnTop: true,
|
||||
focusable: false,
|
||||
movable: true,
|
||||
webPreferences: {
|
||||
preload: preloadPath,
|
||||
devTools: utools.isDev(),
|
||||
},
|
||||
};
|
||||
|
||||
// 如果指定了位置,添加到选项中
|
||||
if (position) {
|
||||
windowOptions.x = position.x;
|
||||
windowOptions.y = position.y;
|
||||
}
|
||||
|
||||
// 创建窗口
|
||||
const UBrowser = createBrowserWindow(pinPath, windowOptions, () => {
|
||||
const windowId = UBrowser.webContents.id;
|
||||
UBrowser.webContents.openDevTools({
|
||||
mode: "undocked",
|
||||
});
|
||||
|
||||
// 监听命令执行请求
|
||||
ipcRenderer.once(`pin-execute-${windowId}`, (event, code) => {
|
||||
// 执行命令
|
||||
console.log("execute command", event, code, commandInfo);
|
||||
});
|
||||
|
||||
// 保存窗口信息
|
||||
pinWindows.set(commandInfo.features.code, {
|
||||
window: UBrowser,
|
||||
windowId,
|
||||
position: UBrowser.getPosition(),
|
||||
});
|
||||
|
||||
// 发送配置到子窗口
|
||||
ipcRenderer.sendTo(windowId, "window-config", {
|
||||
isDark: utools.isDarkColors(),
|
||||
icon: commandInfo.features.icon,
|
||||
commandCode: commandInfo.features.code,
|
||||
windowId,
|
||||
});
|
||||
|
||||
resolve(UBrowser);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除pin窗口
|
||||
* @param {string} commandCode - 命令代码
|
||||
*/
|
||||
const removePinWindow = (commandCode) => {
|
||||
const pinInfo = pinWindows.get(commandCode);
|
||||
if (pinInfo) {
|
||||
pinInfo.window.destroy();
|
||||
pinWindows.delete(commandCode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有pin窗口信息
|
||||
* @returns {Map} pin窗口信息Map
|
||||
*/
|
||||
const getPinWindows = () => {
|
||||
return pinWindows;
|
||||
};
|
||||
|
||||
/**
|
||||
* 恢复所有pin窗口
|
||||
* @param {Array} pinnedCommands - pin命令列表
|
||||
*/
|
||||
const restorePinWindows = async (pinnedCommands) => {
|
||||
for (const command of pinnedCommands) {
|
||||
await createPinWindow(command.info, command.position);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createPinWindow,
|
||||
removePinWindow,
|
||||
getPinWindows,
|
||||
restorePinWindows,
|
||||
};
|
||||
40
plugin/lib/pin/style.css
Normal file
40
plugin/lib/pin/style.css
Normal file
@@ -0,0 +1,40 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pin-container {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.pin-container:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pin-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
body[data-theme="dark"] .pin-icon {
|
||||
background: rgba(57, 57, 57, 0.09);
|
||||
border: 1px solid rgb(59 58 58 / 5%);
|
||||
box-shadow: 0 1px 5px rgb(0 0 0 / 20%), 0 2px 2px rgb(0 0 0 / 14%),
|
||||
0 3px 1px -2px rgb(69 67 67 / 12%);
|
||||
}
|
||||
15
plugin/lib/pin/view.html
Normal file
15
plugin/lib/pin/view.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Pin</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="pin-container">
|
||||
<div class="pin-icon">
|
||||
<img id="command-icon" src="" alt="command icon" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,6 +20,7 @@ const md5 = (input) => {
|
||||
|
||||
window.lodashM = require("./lib/lodashMini");
|
||||
window.pinyinMatch = require("pinyin-match");
|
||||
window.pinService = require("./lib/pin/service");
|
||||
window.DOMPurify = DOMPurify;
|
||||
|
||||
const createTerminalCommand = require("./lib/createTerminalCommand");
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@@ -122,6 +122,10 @@ export default defineComponent({
|
||||
window.quickcommandHttpServer().run(this.nativeProfile.serverPort);
|
||||
console.log("Server Start...");
|
||||
}
|
||||
// 恢复固定到桌面的命令
|
||||
if (this.nativeProfile.pinnedCommands) {
|
||||
window.pinService.restorePinWindows(this.nativeProfile.pinnedCommands);
|
||||
}
|
||||
},
|
||||
enterPlugin(enter) {
|
||||
this.updateExp();
|
||||
@@ -173,10 +177,10 @@ export default defineComponent({
|
||||
} else {
|
||||
let option =
|
||||
command.program === "custom"
|
||||
? command.customOptions
|
||||
: this.programs[command.program];
|
||||
option.scptarg = command.scptarg;
|
||||
option.charset = command.charset;
|
||||
? command.customOptions || {}
|
||||
: this.programs[command.program] || {};
|
||||
option.scptarg = command.scptarg || "";
|
||||
option.charset = command.charset || {};
|
||||
window.runCodeFile(
|
||||
commandCode,
|
||||
option,
|
||||
|
||||
@@ -170,8 +170,8 @@ export default {
|
||||
getCommandOpt(command) {
|
||||
let option =
|
||||
command.program === "custom"
|
||||
? command.customOptions
|
||||
: programs[command.program];
|
||||
? command.customOptions || {}
|
||||
: programs[command.program] || {};
|
||||
option.scptarg = command.scptarg || "";
|
||||
option.charset = command.charset || {};
|
||||
option.envPath = this.$root.nativeProfile.envPath.trim() || "";
|
||||
|
||||
@@ -213,7 +213,7 @@ export default defineComponent({
|
||||
const response = await window.quickcommand.askAI(
|
||||
{
|
||||
prompt: promptText,
|
||||
context: [...presetContext, ...this.chatHistory.slice(0, -2)],
|
||||
context: [presetContext, ...this.chatHistory.slice(0, -2)],
|
||||
},
|
||||
this.selectedApi,
|
||||
{
|
||||
@@ -291,7 +291,7 @@ export default defineComponent({
|
||||
shell: "liunx shell脚本",
|
||||
};
|
||||
const languageName = languageMap[language] || language;
|
||||
const commonInstructions = `接下来我所有的对话中的需求都请通过编写${languageName}代码来实现,并请遵循以下原则:
|
||||
const commonInstructions = `接下来所有的对话中的需求都请通过编写${languageName}代码来实现,并请遵循以下原则:
|
||||
- 编写简洁、可读性强的代码
|
||||
- 遵循${languageName}最佳实践和设计模式
|
||||
- 使用恰当的命名规范和代码组织
|
||||
@@ -312,7 +312,7 @@ export default defineComponent({
|
||||
const specificInstructions = languageSpecific[language] || "";
|
||||
|
||||
const lastInstructions =
|
||||
"\n请直接给我MARKDOWN格式的代码(以```脚本语言开头,以```结尾),任何情况下都不需要做解释和说明";
|
||||
"\n请直接提供MARKDOWN格式的代码(以```脚本语言开头,以```结尾),任何情况下都不需要做解释和说明";
|
||||
|
||||
return commonInstructions + specificInstructions + lastInstructions;
|
||||
},
|
||||
@@ -330,48 +330,26 @@ export default defineComponent({
|
||||
];
|
||||
},
|
||||
getPresetContext() {
|
||||
let finnalPrompt = ""
|
||||
|
||||
const languagePrompt = this.getLanguagePrompt(this.language);
|
||||
|
||||
let presetContext = [
|
||||
{
|
||||
role: "user",
|
||||
content: languagePrompt,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "好的,我会严格按照你的要求编写代码。",
|
||||
},
|
||||
];
|
||||
finnalPrompt += languagePrompt;
|
||||
|
||||
if (this.submitDocs && this.language === "quickcommand") {
|
||||
const docs = this.getLanguageDocs(this.language);
|
||||
|
||||
presetContext.push(
|
||||
{
|
||||
role: "user",
|
||||
content: `你现在使用的是一种特殊的环境,支持uTools和quickcommand两种特殊的接口,请优先使用uTools和quickcommand接口解决需求,然后再使用当前语言通用的解决方案`,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "好的,我会注意。",
|
||||
}
|
||||
);
|
||||
finnalPrompt += `\n你现在使用的是一种特殊的环境,支持uTools和quickcommand两种特殊的接口,请优先使用uTools和quickcommand接口解决需求,然后再使用当前语言通用的解决方案`;
|
||||
|
||||
docs.forEach((doc) => {
|
||||
presetContext.push(
|
||||
{
|
||||
role: "user",
|
||||
content: `这是${doc.name}的API文档:\n${doc.api}`,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "好的,我会认真学习并记住这些接口。",
|
||||
}
|
||||
);
|
||||
finnalPrompt += `\n这是${doc.name}的API文档:\n${doc.api}`;
|
||||
});
|
||||
}
|
||||
|
||||
return presetContext;
|
||||
return {
|
||||
role: "system",
|
||||
content: finnalPrompt,
|
||||
};
|
||||
},
|
||||
openAIAssistantHelp() {
|
||||
window.showUb.help("#KUCwm");
|
||||
|
||||
@@ -2,25 +2,20 @@
|
||||
<q-card style="width: 800px" class="q-pa-sm">
|
||||
<div class="text-h5 q-my-md q-px-sm">API配置</div>
|
||||
<div>
|
||||
<div class="flex q-mb-md q-px-sm" style="height: 26px">
|
||||
<ButtonGroup
|
||||
v-model="apiToAdd"
|
||||
class="col"
|
||||
:options="[
|
||||
{ label: 'OPENAI', value: 'openai' },
|
||||
{ label: 'OLLAMA', value: 'ollama' },
|
||||
]"
|
||||
height="26px"
|
||||
/>
|
||||
<q-icon
|
||||
name="add_box"
|
||||
@click="addModel"
|
||||
<div class="q-pa-sm row q-gutter-sm">
|
||||
<q-btn
|
||||
v-for="option in aiOptions"
|
||||
:key="option.value"
|
||||
icon="add_link"
|
||||
dense
|
||||
color="primary"
|
||||
size="26px"
|
||||
class="cursor-pointer q-ml-sm"
|
||||
:label="option.label"
|
||||
@click="addModel(option.value)"
|
||||
class="col"
|
||||
/>
|
||||
</div>
|
||||
<q-scroll-area
|
||||
ref="scrollArea"
|
||||
:style="`height: ${getConfigListHeight()}px;`"
|
||||
class="q-px-sm"
|
||||
:vertical-thumb-style="{
|
||||
@@ -80,6 +75,7 @@
|
||||
? '例:https://api.openai.com'
|
||||
: '例:http://localhost:11434'
|
||||
"
|
||||
v-show="aiConfig.apiType !== 'utools'"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
@@ -179,21 +175,33 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { dbManager } from "js/utools.js";
|
||||
import ButtonGroup from "components/composer/common/ButtonGroup.vue";
|
||||
import draggable from "vuedraggable";
|
||||
import { getUniqueId } from "js/common/uuid.js";
|
||||
|
||||
export default defineComponent({
|
||||
name: "AIConfig",
|
||||
components: {
|
||||
ButtonGroup,
|
||||
draggable,
|
||||
},
|
||||
setup() {
|
||||
const initAiOptions = utools.allAiModels
|
||||
? [{ label: "uTools内置AI", value: "utools" }]
|
||||
: [];
|
||||
|
||||
const aiOptions = ref([
|
||||
...initAiOptions,
|
||||
{ label: "OPENAI接口(需Key)", value: "openai" },
|
||||
{ label: "OLLAMA接口", value: "ollama" },
|
||||
]);
|
||||
|
||||
return {
|
||||
aiOptions,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
apiToAdd: "openai",
|
||||
aiConfigs: [],
|
||||
models: [],
|
||||
tokenInputTypes: [],
|
||||
@@ -202,6 +210,19 @@ export default defineComponent({
|
||||
emits: ["save"],
|
||||
methods: {
|
||||
async getModels(aiConfig) {
|
||||
if (aiConfig.apiType === "utools") {
|
||||
try {
|
||||
const models = await utools.allAiModels();
|
||||
this.models = models.map((model) => model.id);
|
||||
} catch (error) {
|
||||
quickcommand.showMessageBox(
|
||||
"获取 uTools AI 模型失败: " + error.message,
|
||||
"error"
|
||||
);
|
||||
this.models = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { success, result, error } = await window.getModelsFromAiApi(
|
||||
aiConfig
|
||||
);
|
||||
@@ -222,14 +243,25 @@ export default defineComponent({
|
||||
deleteModel(index) {
|
||||
this.aiConfigs.splice(index, 1);
|
||||
},
|
||||
addModel() {
|
||||
this.aiConfigs.push({
|
||||
addModel(apiType) {
|
||||
const defaultConfig = {
|
||||
id: getUniqueId(),
|
||||
apiType: this.apiToAdd,
|
||||
apiType: apiType,
|
||||
apiUrl: "",
|
||||
apiToken: "",
|
||||
model: "",
|
||||
name: "",
|
||||
};
|
||||
|
||||
if (apiType === "utools") {
|
||||
defaultConfig.apiUrl = "";
|
||||
}
|
||||
|
||||
this.aiConfigs.push(defaultConfig);
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.$refs.scrollArea.setScrollPosition("vertical", 99999);
|
||||
});
|
||||
},
|
||||
getConfigListHeight() {
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
v-ripple
|
||||
:class="{ [`text-${disabledColor}`]: !isActivated, command: 1 }"
|
||||
>
|
||||
<q-badge floating transparent style="z-index: 1000" v-if="isActivated">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
:color="isPinned ? 'amber' : 'grey'"
|
||||
icon="push_pin"
|
||||
@click.stop="togglePin"
|
||||
size="12px"
|
||||
>
|
||||
<q-tooltip>{{ isPinned ? "取消固定到桌面" : "固定到桌面" }}</q-tooltip>
|
||||
</q-btn>
|
||||
</q-badge>
|
||||
|
||||
<component
|
||||
:is="currentLayout"
|
||||
:commandInfo="commandInfo"
|
||||
@@ -56,6 +70,33 @@ export default {
|
||||
"--icon-url": `url(${this.commandInfo.features.icon})`,
|
||||
};
|
||||
},
|
||||
isPinned() {
|
||||
return (
|
||||
this.$root.nativeProfile.pinnedCommands?.some(
|
||||
(cmd) => cmd.info.features.code === this.commandInfo.features.code
|
||||
) || false
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async togglePin() {
|
||||
if (this.isPinned) {
|
||||
window.pinService.removePinWindow(this.commandInfo.features.code);
|
||||
this.$root.nativeProfile.pinnedCommands =
|
||||
this.$root.nativeProfile.pinnedCommands.filter(
|
||||
(cmd) => cmd.info.features.code !== this.commandInfo.features.code
|
||||
);
|
||||
} else {
|
||||
await window.pinService.createPinWindow(this.commandInfo);
|
||||
if (!this.$root.nativeProfile.pinnedCommands) {
|
||||
this.$root.nativeProfile.pinnedCommands = [];
|
||||
}
|
||||
this.$root.nativeProfile.pinnedCommands.push({
|
||||
info: this.commandInfo,
|
||||
position: utools.getCursorScreenPoint(),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -80,4 +121,28 @@ export default {
|
||||
box-shadow: 0 1px 5px rgb(0 0 0 / 20%), 0 2px 2px rgb(0 0 0 / 14%),
|
||||
0 3px 1px -2px rgb(69 67 67 / 12%);
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
visibility: hidden;
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform, opacity, visibility;
|
||||
}
|
||||
|
||||
.q-card:hover .pin-icon {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.pin-icon .q-btn {
|
||||
transition: transform 0.35s cubic-bezier(0.68, -0.6, 0.32, 1.6);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.pin-icon .q-btn:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
128
src/plugins/monaco/types/utools.api.d.ts
vendored
128
src/plugins/monaco/types/utools.api.d.ts
vendored
@@ -667,6 +667,134 @@ interface UToolsApi {
|
||||
isLinux(): boolean;
|
||||
|
||||
ubrowser: UBrowser;
|
||||
|
||||
/**
|
||||
* 调用 AI 能力,支持 Function Calling
|
||||
* @param option AI 选项
|
||||
* @param streamCallback 流式调用函数 (可选)
|
||||
* @returns 返回定制的 PromiseLike
|
||||
*/
|
||||
ai(option: AiOption): PromiseLike<Message>;
|
||||
ai(
|
||||
option: AiOption,
|
||||
streamCallback: (chunk: Message) => void
|
||||
): PromiseLike<void>;
|
||||
|
||||
/**
|
||||
* 获取所有 AI 模型
|
||||
* @returns 返回 AI 模型数组
|
||||
*/
|
||||
allAiModels(): Promise<AiModel[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 选项接口
|
||||
*/
|
||||
interface AiOption {
|
||||
/**
|
||||
* AI 模型, 为空默认使用 deepseek-v3
|
||||
*/
|
||||
model?: string;
|
||||
/**
|
||||
* 消息列表
|
||||
*/
|
||||
messages: Message[];
|
||||
/**
|
||||
* 工具列表
|
||||
*/
|
||||
tools?: Tool[];
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 消息接口
|
||||
*/
|
||||
interface Message {
|
||||
/**
|
||||
* 消息角色
|
||||
* system:系统消息
|
||||
* user:用户消息
|
||||
* assistant:AI 消息
|
||||
*/
|
||||
role: "system" | "user" | "assistant";
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
content?: string;
|
||||
/**
|
||||
* 消息推理内容,一般只有推理模型会返回
|
||||
*/
|
||||
reasoning_content?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 工具接口
|
||||
*/
|
||||
interface Tool {
|
||||
/**
|
||||
* 工具类型
|
||||
* function:函数工具
|
||||
*/
|
||||
type: "function";
|
||||
/**
|
||||
* 函数工具配置
|
||||
*/
|
||||
function?: {
|
||||
/**
|
||||
* 函数名称
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 函数描述
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* 函数参数
|
||||
*/
|
||||
parameters: {
|
||||
type: "object";
|
||||
properties: Record<string, any>;
|
||||
};
|
||||
/**
|
||||
* 必填参数
|
||||
*/
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 模型接口
|
||||
*/
|
||||
interface AiModel {
|
||||
/**
|
||||
* AI 模型 ID,用于 utools.ai 调用的 model 参数
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* AI 模型名称
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* AI 模型描述
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* AI 模型图标
|
||||
*/
|
||||
icon: string;
|
||||
/**
|
||||
* AI 模型调用消耗
|
||||
*/
|
||||
cost: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise 扩展类型,包含 abort() 函数
|
||||
*/
|
||||
interface PromiseLike<T> extends Promise<T> {
|
||||
/**
|
||||
* 中止 AI 调用
|
||||
*/
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
declare var utools: UToolsApi;
|
||||
|
||||
Reference in New Issue
Block a user