14 Commits

Author SHA1 Message Date
fofolee
2625a25584 修复showSystemList无法通过回车进行选择的BUG 2025-06-11 09:28:00 +08:00
fofolee
724bd9e4e7 新增构建和开发脚本 2025-05-08 19:18:48 +08:00
fofolee
72abdf4524 修复编排中ubrowser.hide和show未正确添加的BUG 2025-04-30 22:13:52 +08:00
fofolee
b400bbb48d 添加AI时默认添加在首行 2025-04-25 21:50:01 +08:00
fofolee
c2514e9f2d 修复编排ubrowser设置值报错的BUG 2025-04-25 15:36:21 +08:00
fofolee
c32a5a4829 修复编排的控制流程被错误添加引号的BUG 2025-04-25 11:55:56 +08:00
fofolee
0d4f49fcf4 ai的前置提示词role均调整为system 2025-04-24 00:20:00 +08:00
fofolee
55516159ba 修复使用openai接口时reasoncontent未正确识别的bug 2025-04-24 00:15:49 +08:00
fofolee
d4e58e58be 补全utools.ai相关声明文件 2025-04-23 23:13:45 +08:00
fofolee
d1b117186f 添加支持utools.ai 2025-04-23 23:09:36 +08:00
fofolee
3fb17dcd99 简化ai设置中添加配置的操作 2025-04-23 21:28:20 +08:00
fofolee
ffbae3f33f ai设置添加ai后滚动条自动滚动 2025-04-23 19:21:48 +08:00
fofolee
a762b87d2b ai设置支持添加utools内置的ai 2025-04-23 19:21:31 +08:00
fofolee
3d2d15e177 添加option的空值校验 2025-04-23 18:58:04 +08:00
14 changed files with 336 additions and 114 deletions

4
build.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
git pull
cd plugin && npm i && cd .. && npm i
quasar build

4
dev.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
git pull
cd plugin && npm i && cd .. && npm i
quasar dev

12
package-lock.json generated
View File

@@ -6251,9 +6251,9 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15697,9 +15697,9 @@
}
},
"http-proxy-middleware": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"dev": true,
"requires": {
"@types/http-proxy": "^1.17.8",

View File

@@ -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,

View File

@@ -520,6 +520,10 @@ document.addEventListener("DOMContentLoaded", () => {
if (dialogType === "textarea" && !e.ctrlKey) {
return;
}
// select 类型有自己的键盘处理器,不需要全局处理器处理 Enter 键
if (dialogType === "select") {
return;
}
document.getElementById("ok-btn").click();
}
});

View File

@@ -173,10 +173,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,

View File

@@ -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() || "";

View File

@@ -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");

View File

@@ -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,15 +243,22 @@ 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.unshift(defaultConfig);
},
getConfigListHeight() {
const counts = Math.min(this.aiConfigs.length, 3);

View File

@@ -46,7 +46,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},
@@ -112,7 +112,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},
@@ -169,7 +169,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},
@@ -225,7 +225,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},
@@ -266,7 +266,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},
@@ -320,7 +320,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},
@@ -360,7 +360,7 @@ export const controlCommands = {
{
label: "结束",
value: "end",
codeTemplate: "}",
codeTemplate: "};",
},
],
},

View File

@@ -124,7 +124,7 @@ export function generateCode(flow) {
if (cmd.asyncMode === "await") {
cmdCode = `await ${cmdCode}`;
}
code.push(indent + cmdCode + comma);
code.push(indent + cmdCode + (cmd.isControlFlow ? "" : comma));
}
});

View File

@@ -70,8 +70,6 @@ export function generateUBrowserCode(argvs) {
// 添加其他操作
if (argvs.operations?.length) {
argvs.operations.forEach(({ value, args }) => {
if (!args?.length) return;
const stringifiedArgs = args
.map((arg) => stringifyArgv(arg))
.filter(Boolean);

View File

@@ -491,13 +491,13 @@ export const ubrowserOperationConfigs = {
],
},
setValue: {
value: "setValue",
value: "value",
label: "设置值",
icon: "check_box",
config: [
{
label: "元素选择器",
icon: "varInput",
icon: "find_in_page",
component: "VariableInput",
width: 6,
},

View File

@@ -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用户消息
* assistantAI 消息
*/
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;