diff --git a/.vscode/settings.json b/.vscode/settings.json index 773e4568..b78ce7ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,8 @@ "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[csharp]": { + "editor.defaultFormatter": "ms-dotnettools.csharp" } } diff --git a/README.md b/README.md index c4612ab5..50b7e27a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 快捷命令 +# 快捷命令 5.0 [![GitHub stars](https://img.shields.io/github/stars/fofolee/uTools-quickcommand?style=flat-square)](https://github.com/fofolee/uTools-quickcommand/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fofolee/uTools-quickcommand?style=flat-square)](https://github.com/fofolee/uTools-quickcommand/network/members) [![version](https://img.shields.io/badge/dynamic/json?color=f58142&label=version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Ffofolee%2FuTools-quickcommand%2Fmaster%2Fplugin%2Fplugin.json&style=flat-square)](https://www.yuque.com/fofolee/qcdocs3/ucnd2o) [![猿料](https://img.shields.io/badge/%E7%8C%BF%E6%96%99-%2Fd%2F424-red?style=flat-square)](https://yuanliao.info/d/424) [![评论](https://img.shields.io/badge/dynamic/json?color=e05d44&label=%E8%AF%84%E8%AE%BA&query=%24.data.attributes.commentCount&url=https%3A%2F%2Fyuanliao.info%2Fapi%2Fdiscussions%2F424&style=flat-square)](https://yuanliao.info/d/424) ![rating](https://img.shields.io/badge/dynamic/json?color=05d44&label=评分&query=%24.rating&url=http%3A%2F%2Fopen.u-tools.cn%2Fplugins%2F9a1d1d03%3Ftag_id%3D0%26mid%3Dd1fef324-b4fd-5f81-b05e-4d4d822277b3%26nid%3Df1960e006c87cf1107f2017711668d6c&style=flat-square) ![downloads](https://img.shields.io/badge/dynamic/json?color=05d44&label=下载&query=%24.downloads&url=http%3A%2F%2Fopen.u-tools.cn%2Fplugins%2F9a1d1d03%3Ftag_id%3D0%26mid%3Dd1fef324-b4fd-5f81-b05e-4d4d822277b3%26nid%3Df1960e006c87cf1107f2017711668d6c&style=flat-square) @@ -16,61 +16,30 @@ quasar build # 一、核心功能 +## ① 编写脚本 + +- AI加持:支持AI编写脚本并自动更新代码 - 快速执行命令:如打开文件夹、软件、网址等 - 快速运行脚本:如批处理、`shell`、`python` 等 -- 直接编写网页:可以直接编写简单的 `html` 页面 +- 直接编写网页:可以直接编写简单的 `html` 页面 - 无需编写插件:实现需要使用 `utools` 的 api 或者带 UI 界面的功能 +## ② 可视化编排 +- 自动化:文件操作、网络操作、系统操作、音频操作、图片操作、视频操作、uTools功能、Mac自动化、Window自动化、浏览器控制、数据处理、编码加密、流程控制、编程相关、用户交互、AI对话、模拟操作、获取状态、数学计算、用户数据、显示器、输出消息等20种以上不同类型命令。 +- 工具集:所有功能既可以组合使用,也可以单独运行,具备视频压缩、格式转换,图片裁剪、旋转,文本朗读,音频播放,编解码,模拟按键,鼠标连点等超过 100 种实用功能。 # 二、其他特色 -- 内置了执行`shell`命令、文本处理、文本替换、网址二维码等实用命令 -- 支持在插件内下载别人分享的命令 -- 快速编辑及运行代码 +- 可以对命令进行分享和下载 - 快速收藏文件、网址、插件别名,通过面板视图,实现类似软件启动器、网页搜藏夹、插件面板等功能 - 定时运行命令 - 提供后台服务,将插件内部和外部环境打通 +# 三、配置参数 - -# 三、功能一览 - -## ① 内置命令 - -当前内置的命令有:`Windows Terminal 中打开`、`执行 shell 命令`、`文本处理`、`文本替换`、`vscode代码片段生成器`、`通过 find 查找文件`、`网址二维码` - - - -## ② 导入、导出、分享命令 - -- 支持通过文件导入导出命令 -- 支持通过剪贴板导入导出命令 -- 支持一键分享命令 -- 支持在线获取及导入别人分享的命令 - - - -## ③ 自定义命令 - -### 「 快捷动作 」 - -- 打开文件/文件夹/软件 (实现在主输入框启动自定义的软件名称及路径 ) -- 在文件管理器中定位文件 -- 用默认浏览器打开网址(实现类似网页快开的功能) -- 用 `ubrowser` 打开网址 -- 执行系统命令 -- 将内容写入剪贴板 -- 发送系统消息 -- 弹窗显示消息 -- 发送文本到活动窗口 -- 转至指定插件(实现自定义插件关键字) -- 添加延时 - - - -### 「 匹配 」 +## 「 匹配 」 支持以下模式激活插件 @@ -84,26 +53,21 @@ quasar build - 窗口/进程 - 匹配呼出 uTools 前或唤出超级面板时的活动窗口,可以获取窗口的信息或文件夹路径作为变量 +匹配呼出 uTools 前或唤出超级面板时的活动窗口,可以获取窗口的信息或文件夹路径作为变量 - 复制/选中文件 - 匹配拖入主输入框的文件或唤出超级面板时选中的文件,可以获取复制及选中的文件信息作为变量 +匹配拖入主输入框的文件或唤出超级面板时选中的文件,可以获取复制及选中的文件信息作为变量 - 图片 - 匹配剪贴板的图片 +匹配剪贴板的图片 -- 专业模式 - -匹配 JSON 格式的配置,等效于插件开发中的`features.cmds` - - - -### 「 环境 」 +## 「 环境 」 支持以下环境 +- quickcomposer (可视化编排) - qucikcommand (electron + nodejs + utools) - html - cmd @@ -114,44 +78,125 @@ quasar build - javascript - 等 - - -### 「 输出 」 +## 「 输出 」 - 隐藏并忽略输出 - 显示纯文本输出 (不解析 html 内容) -- 显示html格式的输出 (可以进一步编写简单的 GUI 界面,参考内置动作特殊符号大全) +- 显示 html 格式的输出 (可以进一步编写简单的 GUI 界面,参考内置动作特殊符号大全) - 复制到剪贴板 - 发送到活动窗口(可实现发送常用短语之类的功能) - 发送到系统通知 - 在终端中显示 +# 四、可视化编排功能概览 + +## 音频操作 + +文本朗读(支持中文、英文、日语、韩语等)、系统音效播放(提示音、错误音、警告音等)、音频播放/停止、音频录制、音频信息分析(时长、声道、采样率)... + +## 浏览器操作 + +启动浏览器实例、标签页管理、Cookie操作、文本输入、页面滚动、尺寸控制、网络请求拦截、设备模拟、JavaScript注入、DOM元素操作、截图、表单提交... + +## 编码加密 + +Base64编解码、十六进制编解码、URL编解码、MD5哈希、SHA1哈希、SHA256哈希、SHA512哈希、SM3哈希、AES加密、SM4加密、RSA加密、SM2加密... + +## 流程控制 + +if-else条件判断、循环执行、数组遍历、对象遍历、switch-case分支、try-catch异常处理... + +## 数据处理 +字符串处理(反转、替换、分割、合并、去重、统计)、数组操作(过滤、排序、分组、聚合、扁平化、并集、交集、差集)、时间处理(格式化、计算、比较)、JSON处理、正则匹配... -### ④ 面板视图 +## 文件操作 -- 将某一个标签下的命令以面板形式展现 -- 可实现网址导航面板、软件启动面板之类的功能 +文件/文件夹创建、复制、移动、删除、重命名、属性获取、文件监控、文件图标获取、文件归档、快捷方式创建、默认程序打开... +## 图片处理 +格式转换、图片压缩、尺寸调整、旋转翻转、水印添加、PNG转图标、图片信息获取、亮度对比度调整、图片合并、图片裁剪... -### ⑤ 运行代码 +## macOS特定功能 -- 内置了一个简单的脚本编辑器,可以快速运行代码 -- 会自动记录上次运行的代码 +应用管理(启动、退出、前台切换)、系统设置(音量、亮度、Dock)、Finder操作(窗口控制、文件操作)、系统事件、快捷键绑定... +## Windows特定功能 +窗口控制(置顶、透明度、位置、大小、最大/最小化)、窗口查找(通过标题、类名、进程名)、窗口消息发送(按键、文本、命令)、进程管理(启动、结束、查找、权限提升)、注册表操作(读取、写入、删除、监控)、服务管理(启动、停止、重启、查询、创建、删除)、软件管理(安装、卸载、版本查询)、系统工具(磁盘管理、电源管理、网络配置)、快捷方式管理(创建、修改、删除)、系统设置修改(显示器、音频、电源等)、文件系统监控(文件变化、目录变化)、系统热键注册、UAC权限控制、界面自动化(UI元素查找、点击、输入)、系统事件监听(剪贴板变化、文件变化)... +## 数学计算 +基础运算、随机数生成、统计计算(平均值、中位数、众数)、几何计算、三角函数、对数运算、进制转换、单位换算... -详细介绍见 https://www.yuque.com/fofolee/mwsoos/bg31vl +## 通知消息 +控制台输出、系统通知、自定义通知样式... + +## 编程相关 + +JS代码注入、脚本执行(支持多种语言)、函数返回、变量管理... + +## 模拟操作 + +键盘按键模拟、按键序列、文本复制粘贴、鼠标点击和移动、屏幕截图(全屏、区域、窗口)、拖拽操作... + +## 系统操作 + +剪贴板读写(文本、图片、文件)、系统路径获取、系统信息获取、进程管理、环境变量操作、系统命令执行... + +## 用户数据 + +数据存取、数据删除、数据同步、数据导入导出... + +## 用户界面 + +消息提示框、确认框、输入框、按钮组、选择列表、进度条、文件选择框、颜色选择器、日期选择器... + +## uTools功能 + +匹配数据获取、插件跳转、窗口控制、版本信息获取、主题切换、快捷键管理... +Windows特定功能 +窗口控制(置顶、透明度、位置)、消息发送、文件系统监控、进程管理、注册表操作、服务管理、快捷方式管理、系统设置修改... + +## AI问答 + +AI问答(自由问答、翻译、总结、润色、扩写、生成shell代码)... + +## 视频处理 + +格式转换、视频压缩、视频剪辑、视频合并、速度调整、视频截图、GIF转换、音频提取、水印添加、分辨率调整、帧率设置、码率控制... + +## 状态获取 + +当前文件管理器路径、当前浏览器URL、选中文本、选中图片、选中文件、剪贴板内容、系统状态... + +## 脚本命令 + +Shell脚本执行、Python脚本执行、Node.js脚本执行、PowerShell脚本执行、AppleScript执行... + +## 其他功能 + +延时操作、定时任务... + + +# 五、截图 +>详细介绍见 https://www.yuque.com/fofolee/mwsoos/bg31vl + + +>划词 [![OhN9xJ.gif](https://s1.ax1x.com/2022/05/16/OhN9xJ.gif)](https://imgtu.com/i/OhN9xJ) -[![OhNYi8.png](https://s1.ax1x.com/2022/05/16/OhNYi8.png)](https://imgtu.com/i/OhNYi8) -[![OhNGIf.png](https://s1.ax1x.com/2022/05/16/OhNGIf.png)](https://imgtu.com/i/OhNGIf) -[![OhNAVx.png](https://s1.ax1x.com/2022/05/16/OhNAVx.png)](https://imgtu.com/i/OhNAVx) -[![OhNirR.png](https://s1.ax1x.com/2022/05/16/OhNirR.png)](https://imgtu.com/i/OhNirR) -[![OhNPM9.png](https://s1.ax1x.com/2022/05/16/OhNPM9.png)](https://imgtu.com/i/OhNPM9) -[![OhNFq1.png](https://s1.ax1x.com/2022/05/16/OhNFq1.png)](https://imgtu.com/i/OhNFq1) -[![OhNEa6.png](https://s1.ax1x.com/2022/05/16/OhNEa6.png)](https://imgtu.com/i/OhNEa6) -[![OhNVIK.png](https://s1.ax1x.com/2022/05/16/OhNVIK.png)](https://imgtu.com/i/OhNVIK) -[![OhNePO.png](https://s1.ax1x.com/2022/05/16/OhNePO.png)](https://imgtu.com/i/OhNePO) -[![OhNmGD.png](https://s1.ax1x.com/2022/05/16/OhNmGD.png)](https://imgtu.com/i/OhNmGD) + +>配置界面 +![xb2g30.png](https://files.catbox.moe/xb2g30.png) + +>AI代码生成 +![4kcqh9.png](https://files.catbox.moe/4kcqh9.png) + +>可视化编排界面 +![5mbyoa.png](https://files.catbox.moe/5mbyoa.png) + +>浏览器自动化 +![nq4q0c.png](https://files.catbox.moe/nq4q0c.png) + +>后台服务 +![iiv1jv.png](https://files.catbox.moe/iiv1jv.png) diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..f78339ce --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +git pull +cd plugin && npm i && cd .. && npm i +quasar build diff --git a/dev.sh b/dev.sh new file mode 100755 index 00000000..5c12fe53 --- /dev/null +++ b/dev.sh @@ -0,0 +1,4 @@ +#!/bin/sh +git pull +cd plugin && npm i && cd .. && npm i +quasar dev diff --git a/package-lock.json b/package-lock.json index 9c1c1f47..083c2e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "monaco-editor": "^0.33.0", "monaco-editor-webpack-plugin": "^7.0.1", "picture-compressor": "^1.1.0", - "pinyin-match": "^1.2.2", "quasar": "^2.7.0", "raw-loader": "^4.0.2", "vue": "^3.0.0", @@ -5778,6 +5777,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -7820,14 +7820,6 @@ "resolved": "https://registry.npmmirror.com/picture-compressor/-/picture-compressor-1.1.0.tgz", "integrity": "sha512-yJNpdb97h1KXGxGh1JPav1wYXDEPtuCVryt2VIb74i7WGZu4RyU+gr8u2nmLf24MjwgT4oo3US+yxUCGeadRgg==" }, - "node_modules/pinyin-match": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pinyin-match/-/pinyin-match-1.2.2.tgz", - "integrity": "sha512-C0yOq4LkToJMkDHiQFKOY69El2GRcwdS2lVEjgWjIV8go3wE4mloGFNkVicGHFGYHDg523m2/lKzW8Hh+JR9nw==", - "dependencies": { - "rollup": "^2.44.0" - } - }, "node_modules/pkg-dir": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", @@ -9014,21 +9006,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/rtlcss": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz", @@ -15402,6 +15379,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "optional": true }, "function-bind": { @@ -16809,14 +16787,6 @@ "resolved": "https://registry.npmmirror.com/picture-compressor/-/picture-compressor-1.1.0.tgz", "integrity": "sha512-yJNpdb97h1KXGxGh1JPav1wYXDEPtuCVryt2VIb74i7WGZu4RyU+gr8u2nmLf24MjwgT4oo3US+yxUCGeadRgg==" }, - "pinyin-match": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pinyin-match/-/pinyin-match-1.2.2.tgz", - "integrity": "sha512-C0yOq4LkToJMkDHiQFKOY69El2GRcwdS2lVEjgWjIV8go3wE4mloGFNkVicGHFGYHDg523m2/lKzW8Hh+JR9nw==", - "requires": { - "rollup": "^2.44.0" - } - }, "pkg-dir": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", @@ -17561,14 +17531,6 @@ "glob": "^7.1.3" } }, - "rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "requires": { - "fsevents": "~2.3.2" - } - }, "rtlcss": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz", diff --git a/package.json b/package.json index 63937ba3..8aa174a4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "monaco-editor": "^0.33.0", "monaco-editor-webpack-plugin": "^7.0.1", "picture-compressor": "^1.1.0", - "pinyin-match": "^1.2.2", "quasar": "^2.7.0", "raw-loader": "^4.0.2", "vue": "^3.0.0", diff --git a/plugin/lib/ai.js b/plugin/lib/ai.js new file mode 100644 index 00000000..10091d74 --- /dev/null +++ b/plugin/lib/ai.js @@ -0,0 +1,506 @@ +const DOMPurify = require("dompurify"); +const marked = require("marked"); + +window.aiResponseParser = (content) => { + const markedContent = marked.parse(content.trim()); + const processedContent = markedContent + .replace("

", "

") + .replace("

", "

") + .replace("\n\n", ""); + const purifiedContent = DOMPurify.sanitize(processedContent, { + ADD_TAGS: ["think"], + }); + return purifiedContent; +}; + +// 支持的模型类型 +const API_TYPES = { + OPENAI: "openai", + OLLAMA: "ollama", + UTOOLS: "utools", +}; + +// 角色提示词 +const ROLE_PROMPTS = { + // 翻译 + translate: `你是一名翻译专家,请将我给你的内容进行翻译,要求: +1. 无论给的内容长短,请直接翻译,不要进行任何解释 +2. 提供中文时,翻译成地道的英文,符合英文的表达习惯 +3. 提供英文时,翻译成地道的中文,符合中文的表达习惯 +4. 保持原文的专业性和准确性 +5. 保持原文的段落格式 +6. 不要输出原文 +7. 不要使用markdown格式,请直接输出翻译后的内容 +`, + + // 生成SHELL命令 + shell: `你是一名shell命令专家,请根据我的描述生成 shell 命令,要求: +1. 命令应当简洁高效 +2. 优先使用常见的命令行工具 +3. 确保命令的安全性和可靠性 +4. 对于复杂操作,添加注释说明 +5. 如果需要多个命令,使用 && 连接或使用脚本格式 +6. 直接输出命令,不要输出任何解释,不要使用markdown格式 +`, + + // 总结 + summarize: `你是一名总结专家,请总结我给你的内容的要点,要求: +1. 提取最重要和最有价值的信息 +2. 使用简洁的语言 +3. 按重要性排序 +4. 保持逻辑性和连贯性 +5. 请将所有内容放在一个段落内 +6. 不要输出原文 +7. 不要使用markdown格式 +`, + + // 润色 + polish: `你是一名文字润色专家,请对我给你的内容进行润色,要求: +1. 保持原文的核心意思不变 +2. 使用更优美、专业的表达方式 +3. 改善语言流畅度和可读性 +4. 纠正语法错误和不恰当的表达 +5. 保持原文的语言风格(中文/英文) +6. 保持原文的段落格式 +7. 直接输出润色后的内容,不要解释修改原因 +8. 不要使用markdown格式 +`, + + // 扩写 + expand: `你是一名文字扩写专家,请对我给你的内容进行扩写,要求: +1. 在保持原意的基础上扩充内容 +2. 添加相关的细节和例子 +3. 使用生动的描述和丰富的表达 +4. 确保扩写内容逻辑连贯 +5. 适度扩写,避免过度冗长 +6. 保持语言风格的一致性 +7. 直接输出扩写后的内容,不要解释扩写原因 +8. 不要使用markdown格式 +`, +}; + +// API URL 处理 +const API_ENDPOINTS = { + [API_TYPES.OPENAI]: { + chat: "/v1/chat/completions", + models: "/v1/models", + }, + [API_TYPES.OLLAMA]: { + chat: "/api/chat", + models: "/api/tags", + }, +}; + +// 构建API URL +function buildApiUrl(baseUrl, endpoint) { + if (!baseUrl.endsWith(endpoint)) { + return baseUrl.replace(/\/?$/, endpoint); + } + return baseUrl; +} + +// 构建请求配置 +function buildRequestConfig(apiConfig) { + const config = { + headers: { + "Content-Type": "application/json", + }, + }; + + if (apiConfig.apiType === API_TYPES.OPENAI && apiConfig.apiToken) { + config.headers["Authorization"] = `Bearer ${apiConfig.apiToken}`; + } + + return config; +} + +// 构建请求数据 +function buildRequestData(content, apiConfig) { + const { model } = apiConfig; + const { prompt, role, context = [] } = content; + const rolePrompt = ROLE_PROMPTS[role] || role; + + const roleMessage = rolePrompt + ? [ + { + role: "system", + content: rolePrompt, + }, + ] + : []; + + // 统一的消息格式处理 + const messages = [ + // 添加系统角色消息(如果有) + ...roleMessage, + // 添加上下文消息 + ...context.map((msg) => ({ + role: msg.role || "user", + content: msg.content, + })), + // 添加当前用户消息 + { + role: "user", + content: prompt, + }, + ]; + + return { + model, + messages, + stream: true, + }; +} + +// 处理模型列表响应 +function parseModelsResponse(response, apiType) { + if (apiType === API_TYPES.OPENAI) { + if (!response.data.data) { + throw new Error("OpenAI 响应格式错误"); + } + return response.data.data.map((model) => model.id); + } else { + if (!response.data.models) { + throw new Error("Ollama 响应格式错误"); + } + return response.data.models.map((model) => model.name); + } +} + +let reasoning_content_start = false; +function processContentWithReason(response, onStream) { + if (response.reasoning_content) { + if (!reasoning_content_start) { + reasoning_content_start = true; + onStream("", false); + } + onStream(response.reasoning_content, false); + } + if (response.content) { + if (reasoning_content_start) { + reasoning_content_start = false; + onStream("", false); + } + onStream(response.content, false); + } +} + +// 处理 OpenAI 流式响应 +async function handleOpenAIStreamResponse(line, onStream) { + if (line.startsWith("data:")) { + const jsonStr = line.replace(/^data:[ ]*/, ""); + if (jsonStr === "[DONE]") { + onStream("", true); + return; + } + const json = JSON.parse(jsonStr); + const response = json.choices[0]?.delta; + if (response) { + processContentWithReason(response, onStream); + } + } +} + +// 处理 Ollama 流式响应 +async function handleOllamaStreamResponse(line, onStream) { + const json = JSON.parse(line); + if (json.done) { + onStream("", true); + return; + } + 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 = ""; + + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim()) { + try { + if (apiConfig.apiType === API_TYPES.OPENAI) { + await handleOpenAIStreamResponse(line, onStream); + } else { + await handleOllamaStreamResponse(line, onStream); + } + } catch (e) { + console.error("解析响应失败:", e); + } + } + } + } + + // 处理剩余的缓冲区 + if (buffer.trim()) { + try { + if (apiConfig.apiType === API_TYPES.OPENAI) { + await handleOpenAIStreamResponse(buffer, onStream); + } else { + await handleOllamaStreamResponse(buffer, onStream); + } + } catch (e) { + console.error("解析剩余响应失败:", e); + } + } + } catch (error) { + if (error.name === "AbortError") { + return { + success: false, + error: "请求已取消", + cancelled: true, + }; + } + throw error; + } finally { + reader.releaseLock(); + } + + return { success: true }; +} + +/** + * AI对话功能 + * @param {Object} content - 对话内容参数 + * @param {Object} apiConfig - API配置参数 + * @param {Object} options - 其他选项 + * @returns {Promise} 对话响应 + */ +async function chat(content, apiConfig, options = {}) { + try { + const { + showProcessBar = true, + onStream = () => {}, + onFetch = () => {}, + } = options; + + // 验证必要参数 + 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地址、模型名称和提示词不能为空"); + } + } + + let controller; + + // 显示进度条 + const processBar = showProcessBar + ? await quickcommand.showProcessBar({ + text: "AI思考中...", + onClose: () => { + if (typeof controller !== "undefined") { + controller.abort(); + } + }, + }) + : null; + + // 用于收集完整响应 + let fullResponse = ""; + + // 包装 onStream 回调以收集完整响应并更新进度条 + const streamHandler = (chunk, isDone) => { + if (!isDone) { + fullResponse += chunk; + // 更新进度条显示最新的响应内容 + if (processBar) { + quickcommand.updateProcessBar( + { + text: window.aiResponseParser(fullResponse), + }, + processBar + ); + } + } + onStream(chunk, isDone); + }; + + // 处理 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, + body: JSON.stringify(requestData), + signal: controller.signal, + }); + + if (!response.ok) { + processBar?.close(); + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await handleStreamResponse( + response, + apiConfig, + streamHandler + ); + + // 如果请求被取消,返回取消状态 + if (!result.success) { + processBar?.close(); + return result; + } + + // 完成时更新进度条并关闭 + 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, + }; + } + return { + success: false, + error: error.response?.data?.error?.message || error.message, + }; + } +} + +/** + * 获取API支持的模型列表 + * @param {Object} apiConfig - API配置参数 + * @returns {Promise} 模型列表响应 + */ +async function getModels(apiConfig) { + try { + if (!apiConfig.apiUrl) { + throw new Error("API地址不能为空"); + } + + const url = buildApiUrl( + apiConfig.apiUrl, + API_ENDPOINTS[apiConfig.apiType].models + ); + const config = buildRequestConfig(apiConfig); + + const response = await fetch(url, { + method: "GET", + headers: config.headers, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.json(); + return { + success: true, + result: parseModelsResponse({ data: responseData }, apiConfig.apiType), + }; + } catch (error) { + return { + success: false, + error: error.response?.data?.error?.message || error.message, + }; + } +} +module.exports = { chat, getModels }; diff --git a/plugin/lib/createTerminalCommand.js b/plugin/lib/createTerminalCommand.js new file mode 100644 index 00000000..fd480b6c --- /dev/null +++ b/plugin/lib/createTerminalCommand.js @@ -0,0 +1,150 @@ +const fs = require("fs"); +const path = require("path"); + +// 终端配置 +const DEFAULT_TERMINALS = { + windows: ["wt", "cmd"], + macos: ["warp", "iterm", "terminal"], +}; + +// Windows 终端命令生成器 +const getWindowsTerminalCommand = (cmdline, options = {}) => { + const { dir, terminal = "wt" } = options; + const appPath = path.join( + window.utools.getPath("home"), + "/AppData/Local/Microsoft/WindowsApps/" + ); + + const terminalCommands = { + wt: () => { + if ( + fs.existsSync(appPath) && + fs.readdirSync(appPath).includes("wt.exe") + ) { + const escapedCmd = cmdline.replace(/"/g, `\\"`); + const cd = dir ? `-d "${dir.replace(/\\/g, "/")}"` : ""; + return `${appPath}wt.exe ${cd} cmd /k "${escapedCmd}"`; + } + return null; + }, + cmd: () => { + const escapedCmd = cmdline.replace(/"/g, `^"`); + const cd = dir ? `cd /d "${dir.replace(/\\/g, "/")}" &&` : ""; + return `${cd} start "" cmd /k "${escapedCmd}"`; + }, + }; + + // 按优先级尝试不同终端 + const terminalPriority = + terminal === "default" + ? DEFAULT_TERMINALS.windows + : [terminal, ...DEFAULT_TERMINALS.windows]; + + for (const term of terminalPriority) { + const command = terminalCommands[term]?.(); + if (command) return command; + } + + // 如果都失败了,返回默认的 cmd 命令 + return terminalCommands.cmd(); +}; + +// macOS 终端命令生成器 +const getMacTerminalCommand = (cmdline, options = {}) => { + const { dir, terminal = "warp" } = options; + + const terminalCommands = { + warp: () => { + if (fs.existsSync("/Applications/Warp.app")) { + const workingDir = dir || process.cwd(); + // 创建临时的 launch configuration + const configName = `temp_${Date.now()}`; + const configPath = path.join( + window.utools.getPath("home"), + ".warp/launch_configurations", + `${configName}.yml` + ); + + // 确保目录存在 + const configDir = path.dirname(configPath); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + + // 创建配置文件,对于 Warp,命令不需要转义,因为是通过 YAML 配置传递 + const config = `--- +name: ${configName} +windows: + - tabs: + - layout: + cwd: "${workingDir}" + commands: + - exec: ${cmdline}`; + + fs.writeFileSync(configPath, config); + + // 使用配置文件启动 Warp + return `open "warp://launch/${configName}" && sleep 0.5 && rm "${configPath}"`; + } + return null; + }, + iterm: () => { + const escapedCmd = cmdline.replace(/"/g, `\\"`); + const cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : ""; + if (fs.existsSync("/Applications/iTerm.app")) { + return `osascript -e 'tell application "iTerm" + if application "iTerm" is running then + create window with default profile + end if + tell current session of first window to write text "clear && ${cd} ${escapedCmd}" + activate + end tell'`; + } + return null; + }, + terminal: () => { + const escapedCmd = cmdline.replace(/"/g, `\\"`); + const cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : ""; + return `osascript -e 'tell application "Terminal" + if application "Terminal" is running then + do script "clear && ${cd} ${escapedCmd}" + else + do script "clear && ${cd} ${escapedCmd}" in window 1 + end if + activate + end tell'`; + }, + }; + + // 按优先级尝试不同终端 + const terminalPriority = + terminal === "default" + ? DEFAULT_TERMINALS.macos + : [terminal, ...DEFAULT_TERMINALS.macos]; + + for (const term of terminalPriority) { + const command = terminalCommands[term]?.(); + if (command) return command; + } + + // 如果都失败了,返回默认终端命令 + return terminalCommands.terminal(); +}; + +// 主函数 +const createTerminalCommand = (cmdline, options = {}) => { + const { windows = "default", macos = "default" } = options; + + if (window.utools.isWindows()) { + return getWindowsTerminalCommand(cmdline, { + ...options, + terminal: windows, + }); + } else if (window.utools.isMacOs()) { + return getMacTerminalCommand(cmdline, { ...options, terminal: macos }); + } + + throw new Error("Unsupported operating system"); +}; + +module.exports = createTerminalCommand; diff --git a/plugin/lib/csharp/automation.cs b/plugin/lib/csharp/automation.cs new file mode 100644 index 00000000..c58013e3 --- /dev/null +++ b/plugin/lib/csharp/automation.cs @@ -0,0 +1,1802 @@ +using System; +using System.Text; +using System.Runtime.InteropServices; +using System.Windows.Automation; +using System.Windows.Automation.Text; +using System.Windows; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using System.Linq; +using System.Threading; +using System.Windows.Forms; +using System.Drawing; +using System.Diagnostics; + +public class AutomationManager +{ + // UIA Control Type IDs + private static class UIA_ControlTypeIds + { + public const int Window = 50032; + } + + // 用于缓存找到的元素 + private static Dictionary elementCache = new Dictionary(); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [StructLayout(LayoutKind.Sequential)] + private struct Point + { + public int X; + public int Y; + + public override bool Equals(object obj) + { + if (!(obj is Point)) return false; + Point other = (Point)obj; + return X == other.X && Y == other.Y; + } + + public static bool operator ==(Point a, Point b) + { + return a.Equals(b); + } + + public static bool operator !=(Point a, Point b) + { + return !a.Equals(b); + } + + public override int GetHashCode() + { + return X.GetHashCode() ^ Y.GetHashCode(); + } + + public System.Windows.Point ToWindowsPoint() + { + return new System.Windows.Point(X, Y); + } + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetCursorPos(out Point lpPoint); + + [DllImport("user32.dll")] + private static extern IntPtr WindowFromPoint(Point point); + + [DllImport("user32.dll")] + static extern int GetWindowLong(IntPtr hwnd, int index); + + [DllImport("user32.dll")] + static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle); + + [DllImport("user32.dll")] + static extern bool IsWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + private const int GWL_EXSTYLE = -20; + private const int WS_EX_TRANSPARENT = 0x20; + private const int WS_EX_LAYERED = 0x80000; + private const int WS_EX_TOOLWINDOW = 0x80; + private const int WS_EX_NOACTIVATE = 0x08000000; + private const int HWND_TOPMOST = -1; + + [DllImport("user32.dll")] + static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + + [DllImport("user32.dll")] + private static extern bool EnableWindow(IntPtr hWnd, bool bEnable); + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); + + [DllImport("user32.dll")] + private static extern bool SetCursorPos(int x, int y); + + [DllImport("user32.dll")] + private static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, int dwExtraInfo); + + private const uint MOUSEEVENTF_LEFTDOWN = 0x0002; + private const uint MOUSEEVENTF_LEFTUP = 0x0004; + + // 添加静态字段 + private static System.Windows.Forms.Timer mouseTimer; + private static Form overlayForm; + private static Form previewForm; + private static AutomationElement lastElement; + private static Point lastCursorPos; + private static bool completed; + + private static CacheRequest CreateCacheRequest() + { + var cacheRequest = new CacheRequest(); + cacheRequest.Add(AutomationElement.NameProperty); + cacheRequest.Add(AutomationElement.ClassNameProperty); + cacheRequest.Add(AutomationElement.ControlTypeProperty); + cacheRequest.Add(AutomationElement.BoundingRectangleProperty); + cacheRequest.Add(AutomationElement.IsOffscreenProperty); + cacheRequest.Add(AutomationElement.IsEnabledProperty); + cacheRequest.TreeScope = TreeScope.Element | TreeScope.Children | TreeScope.Descendants; + return cacheRequest; + } + + private static AutomationElement GetTaskbarElement() + { + IntPtr taskbarHandle = FindWindow("Shell_TrayWnd", null); + if (taskbarHandle != IntPtr.Zero) + { + return AutomationElement.FromHandle(taskbarHandle); + } + return null; + } + + private static List GetTaskbarChildren(AutomationElement taskbarElement) + { + var children = new List(); + if (taskbarElement == null) return children; + + try + { + var cacheRequest = CreateCacheRequest(); + cacheRequest.Push(); + try + { + var conditions = new AndCondition( + new PropertyCondition(AutomationElement.IsOffscreenProperty, false), + new PropertyCondition(AutomationElement.IsEnabledProperty, true), + new PropertyCondition(AutomationElement.IsContentElementProperty, true) + ); + + var taskbarChildren = taskbarElement.FindAll(TreeScope.Children | TreeScope.Descendants, conditions); + foreach (AutomationElement child in taskbarChildren) + { + children.Add(child); + } + } + finally + { + cacheRequest.Pop(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + + return children; + } + + private static void InspectElementByPosition(string position) + { + Point cursorPos; + if (string.IsNullOrEmpty(position)) + { + // 获取当前鼠标位置 + GetCursorPos(out cursorPos); + } + else + { + // 解析传入的坐标 + string[] coords = position.Split(','); + cursorPos = new Point + { + X = int.Parse(coords[0]), + Y = int.Parse(coords[1]) + }; + } + + var element = AutomationElement.FromPoint(cursorPos.ToWindowsPoint()); + if (element != null) + { + InspectElementInfo(element, cursorPos); + return; + } + throw new Exception("在指定坐标未找到元素"); + } + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + Console.Error.WriteLine("Error: 必须指定操作类型 (-type)"); + return; + } + + try + { + switch (type.ToLower()) + { + case "inspect": + string position = GetArgumentValue(args, "-position"); + if (position != null) // 参数存在(一定会有值,可能是空字符串) + { + InspectElementByPosition(position); + } + else // 参数不存在 + { + InspectElement(args); + } + break; + + case "click": + ClickElement(GetTargetElement(args)); + break; + + case "setvalue": + string value = GetArgumentValue(args, "-value"); + SetElementValue(GetTargetElement(args), value); + string sendenter = GetArgumentValue(args, "-sendenter"); + if (sendenter != null) + { + System.Windows.Forms.SendKeys.SendWait("{ENTER}"); + } + break; + + case "getvalue": + GetElementValue(GetTargetElement(args)); + break; + + case "select": + string item = GetArgumentValue(args, "-item"); + SelectItem(GetTargetElement(args), item); + break; + + case "expand": + string expandStr = GetArgumentValue(args, "-expand"); + bool expand = false; + if (!string.IsNullOrEmpty(expandStr)) + { + expand = expandStr.ToLower() == "true"; + } + ExpandElement(GetTargetElement(args), expand); + break; + + case "scroll": + string direction = GetArgumentValue(args, "-direction"); + if (string.IsNullOrEmpty(direction)) + { + direction = "vertical"; + } + string amountStr = GetArgumentValue(args, "-amount"); + double amount = 0; + if (!string.IsNullOrEmpty(amountStr)) + { + amount = double.Parse(amountStr); + } + ScrollElement(GetTargetElement(args), direction, amount); + break; + + case "wait": + string timeoutStr = GetArgumentValue(args, "-timeout"); + int timeout = 30; + if (!string.IsNullOrEmpty(timeoutStr)) + { + timeout = int.Parse(timeoutStr); + } + WaitForElement(args, timeout); + break; + + case "focus": + SetFocus(GetTargetElement(args)); + break; + + case "highlight": + string durationStr = GetArgumentValue(args, "-duration"); + int duration = 2; + if (!string.IsNullOrEmpty(durationStr)) + { + duration = int.Parse(durationStr); + } + HighlightElement(GetTargetElement(args), duration); + break; + + case "sendkeys": + string keys = GetArgumentValue(args, "-keys"); + SendKeys(GetTargetElement(args), keys); + break; + + case "enable": + bool enable = GetArgumentValue(args, "-enable") == "true"; + EnableElement(GetTargetElement(args), enable); + break; + + default: + throw new Exception(string.Format("不支持的操作类型: {0}", type)); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + Environment.Exit(1); + } + } + + private static AutomationElement GetTargetElement(string[] args) + { + // 获取起始窗口 + AutomationElement root; + try + { + List targetWindows = GetTargetWindows(args); + if (targetWindows.Count == 0) + { + throw new Exception("未找到目标窗口"); + } + + // 总是使用第一个窗口 + IntPtr handle = targetWindows[0]; + + root = AutomationElement.FromHandle(handle); + } + catch + { + throw new Exception("无法获取指定的窗口"); + } + + if (root == null) + { + throw new Exception("无法获取指定的窗口"); + } + + // 通过 XPath 查找(优先) + string xpath = GetArgumentValue(args, "-xpath"); + if (!string.IsNullOrEmpty(xpath)) + { + var element = FindElementByXPath(xpath, root); + if (element != null) return element; + throw new Exception(string.Format("找不到指定的XPath: {0}", xpath)); + } + + // 通过 AutomationId 查找 + string id = GetArgumentValue(args, "-id"); + if (!string.IsNullOrEmpty(id)) + { + var element = root.FindFirst(TreeScope.Subtree, + new PropertyCondition(AutomationElement.AutomationIdProperty, id)); + if (element != null) return element; + throw new Exception(string.Format("找不到指定的AutomationId: {0}", id)); + } + + // 通过 Name 查找 + string name = GetArgumentValue(args, "-name"); + if (!string.IsNullOrEmpty(name)) + { + var element = root.FindFirst(TreeScope.Subtree, + new PropertyCondition(AutomationElement.NameProperty, name)); + if (element != null) return element; + throw new Exception(string.Format("找不到指定的Name: {0}", name)); + } + + // 通过组合条件查找 + string condition = GetArgumentValue(args, "-condition"); + if (!string.IsNullOrEmpty(condition)) + { + var conditions = new List(); + string[] parts = condition.Split(';'); + foreach (string part in parts) + { + string[] keyValue = part.Split('='); + if (keyValue.Length == 2) + { + switch (keyValue[0].ToLower()) + { + case "name": + conditions.Add(new PropertyCondition(AutomationElement.NameProperty, keyValue[1])); + break; + case "class": + conditions.Add(new PropertyCondition(AutomationElement.ClassNameProperty, keyValue[1])); + break; + case "automation": + conditions.Add(new PropertyCondition(AutomationElement.AutomationIdProperty, keyValue[1])); + break; + case "type": + string controlTypeName = keyValue[1]; + if (controlTypeName.StartsWith("ControlType.")) + { + controlTypeName = controlTypeName.Substring("ControlType.".Length); + } + var field = typeof(ControlType).GetField(controlTypeName); + if (field != null) + { + ControlType controlType = (ControlType)field.GetValue(null); + conditions.Add(new PropertyCondition(AutomationElement.ControlTypeProperty, controlType)); + } + break; + } + } + } + + if (conditions.Count > 0) + { + Condition searchCondition; + if (conditions.Count > 1) + { + searchCondition = new AndCondition(conditions.ToArray()); + } + else + { + searchCondition = conditions[0]; + } + var element = root.FindFirst(TreeScope.Subtree, searchCondition); + if (element != null) return element; + throw new Exception(string.Format("找不到符合条件的元素: {0}", condition)); + } + } + + throw new Exception("必须指定元素的识别方式: -xpath, -id, -name 或 -condition"); + } + + private static List GetTargetWindows(string[] args) + { + List targetWindows = new List(); + string method = GetArgumentValue(args, "-method") ?? "handle"; + string value = GetArgumentValue(args, "-window") ?? ""; + + switch (method.ToLower()) + { + case "handle": + IntPtr handle = new IntPtr(long.Parse(value)); + if (!IsWindow(handle)) + { + throw new Exception("指定的句柄不是一个有效的窗口句柄"); + } + targetWindows.Add(handle); + break; + + case "active": + targetWindows.Add(GetForegroundWindow()); + break; + + case "process": + var processes = Process.GetProcessesByName(value); + foreach (var process in processes) + { + if (process.MainWindowHandle != IntPtr.Zero) + { + targetWindows.Add(process.MainWindowHandle); + } + } + break; + + case "class": + EnumWindows((hwnd, param) => + { + if (!IsWindowVisible(hwnd)) + { + return true; + } + + StringBuilder className = new StringBuilder(256); + GetClassName(hwnd, className, className.Capacity); + string windowClassName = className.ToString(); + + if (windowClassName.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) + { + targetWindows.Add(hwnd); + } + return true; + }, IntPtr.Zero); + break; + + case "title": + default: + if (!string.IsNullOrEmpty(value)) + { + EnumWindows((hwnd, param) => + { + StringBuilder title = new StringBuilder(256); + GetWindowText(hwnd, title, title.Capacity); + string windowTitle = title.ToString(); + + if (!string.IsNullOrEmpty(windowTitle) && + windowTitle.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) + { + targetWindows.Add(hwnd); + } + return true; + }, IntPtr.Zero); + } + break; + } + + if (targetWindows.Count == 0) + { + throw new Exception("未找到匹配的窗口"); + } + + return targetWindows; + } + + + + private static void ShowHelp() + { + Console.WriteLine(@"UI自动化工具 v1.0 + +用法: automation.exe <操作类型> [参数] + +操作类型: + +1. inspect - 检查元素 + 无需其他参数,点击要检查的元素即可 + +2. click - 点击指定元素 + 参数: -xpath [-window <窗口句柄>] + 适用控件: 所有可点击的控件,包括: + - Button (按钮) + - MenuItem (菜单项) + - TreeItem (树项) + - ListItem (列表项) + - TabItem (标签页) + - RadioButton (单选按钮) + - CheckBox (复选框) + 示例: + - 点击按钮: -xpath ""//Button[@Name='确定']"" + - 点击菜单项: -xpath ""//MenuBar/MenuItem[@Name='文件']/MenuItem[@Name='打开']"" + - 点击树节点: -xpath ""//Tree/TreeItem[@Name='节点1']"" + +3. setvalue - 设置值 + 参数: -xpath -value <新值> [-window <窗口句柄>] + +4. getvalue - 获取值 + 参数: -xpath [-window <窗口句柄>] + +5. select - 选择指定项目 + 参数: -xpath -item <项目名称> [-window <窗口句柄>] + 适用控件及其特性: + - ComboBox (组合框) + * 自动展开下拉列表 + * 支持选择 ListItem + - TreeView (树视图) + * 支持选择所有层级的 TreeItem + * 使用完整路径查找 + - ListBox (列表框) + * 支持选择直接子项 ListItem + - DataGrid (数据网格) + * 支持选择 DataItem 和 ListItem + - Table (表格) + * 支持选择 DataItem 和 ListItem + - Tab (标签页) + * 支持选择直接子项 TabItem + - MenuBar/Menu (菜单) + * 支持选择所有层级的 MenuItem + - RadioButton (单选按钮) + * 直接选择匹配名称的按钮 + - CheckBox (复选框) + * 直接选择匹配名称的复选框 + 示例: + - 选择下拉框选项: -xpath ""//ComboBox"" -item ""选项1"" + - 选择树节点: -xpath ""//Tree"" -item ""父节点/子节点"" + - 选择列表项: -xpath ""//List"" -item ""列表项1"" + - 选择标签页: -xpath ""//Tab"" -item ""标签页2"" + - 选择菜单项: -xpath ""//MenuBar"" -item ""文件"" + - 选择单选按钮: -xpath ""//RadioButton[@Name='选项1']"" -item ""选项1"" + +6. expand - 展开/折叠 + 参数: -xpath -expand [-window <窗口句柄>] + 适用控件及其特性: + - TreeItem (树节点) 展开/折叠子节点 + - ComboBox (组合框) 展开/折叠下拉列表 + - Menu (菜单) 支持展开/折叠子菜单 + - GroupBox (分组框) 展开/折叠内容区域 + - Expander (展开器) 展开/折叠详细内容 + 示例: + - 展开树节点: -xpath ""//Tree/TreeItem[@Name='父节点']"" -expand true + - 折叠树节点: -xpath ""//Tree/TreeItem[@Name='父节点']"" -expand false + - 展开下拉框: -xpath ""//ComboBox"" -expand true + - 折叠下拉框: -xpath ""//ComboBox"" -expand false + - 展开菜单: -xpath ""//Menu/MenuItem[@Name='文件']"" -expand true + - 展开分组: -xpath ""//GroupBox[@Name='详细信息']"" -expand true + - 展开内容: -xpath ""//Expander"" -expand true + +7. scroll - 滚动 + 参数: -xpath -direction -amount <0-100> [-window <窗口句柄>] + 适用控件及其特性: + - ScrollBar (滚动条) + - ListBox (列表框) + - ComboBox (组合框) + - DataGrid (数据网格) + - TreeView (树视图) + - TextBox (文本框) + - Document (文档) + 示例: + - 垂直滚动列表到底部: -xpath ""//List"" -direction vertical -amount 100 + - 水平滚动表格到中间: -xpath ""//DataGrid"" -direction horizontal -amount 50 + - 垂直滚动文本框到顶部: -xpath ""//Edit"" -direction vertical -amount 0 + - 水平滚动文档到最右: -xpath ""//Document"" -direction horizontal -amount 100 + +8. wait - 等待元素 + 参数: -xpath -timeout <秒数> [-window <窗口句柄>] + +9. focus - 设置焦点 + 参数: -xpath [-window <窗口句柄>] + +10. highlight - 高亮显示 + 参数: -xpath -duration <秒数> [-window <窗口句柄>] + +11. sendkeys - 发送按键 + 参数: -xpath -keys <按键> [-window <窗口句柄>] + 按键格式说明: + - 普通字符直接输入,如 ""abc"" + - 特殊按键用 {} 包围,如 ""{ENTER}""、""{TAB}"" + - 组合键,如 ""^c"" 表示 Ctrl+C + 支持的特殊按键: + - {BACKSPACE}, {BS}, {BKSP} - 退格键 + - {BREAK} - Break键 + - {CAPSLOCK} - Caps Lock键 + - {DELETE}, {DEL} - Delete键 + - {DOWN} - 向下键 + - {END} - End键 + - {ENTER}, {RETURN} - Enter键 + - {ESC} - Esc键 + - {HELP} - Help键 + - {HOME} - Home键 + - {INSERT}, {INS} - Insert键 + - {LEFT} - 向左键 + - {NUMLOCK} - Num Lock键 + - {PGDN} - Page Down键 + - {PGUP} - Page Up键 + - {PRTSC} - Print Screen键 + - {RIGHT} - 向右键 + - {SCROLLLOCK} - Scroll Lock键 + - {TAB} - Tab键 + - {UP} - 向上键 + - {F1} - {F12} - 功能键 + - {ADD} - 数字键盘加号键 + - {SUBTRACT} - 数字键盘减号键 + - {MULTIPLY} - 数字键盘乘号键 + - {DIVIDE} - 数字键盘除号键 + - {NUMPAD0} - {NUMPAD9} - 数字键盘数字键 + +12. enable - 启用/禁用元素 + 参数: -xpath -enable [-window <窗口句柄>] + 示例: + - 启用按钮: -xpath ""//Button[@Name='确定']"" -enable true + +通用参数: +-window <窗口句柄> 指定要操作的窗口,如果不指定则使用当前活动窗口 + +元素定位方式: +1. XPath定位(推荐) + -xpath + 示例: -xpath ""//Button[@Name='确定']"" + +2. AutomationId定位 + -id + 示例: -id ""btnOK"" + +3. Name定位 + -name <名称> + 示例: -name ""确定"" + +4. 组合条件定位 + -condition ""name=xx;type=Button;class=xx;automation=xx"" + 示例: -condition ""name=确定;type=Button"""); + } + + private static string GetArgumentValue(string[] args, string key, bool checkNextArg = true) + { + int index = Array.IndexOf(args, key); + if (index >= 0) + { + if (index < args.Length - 1) + { + // 如果需要检查下一个参数是否是参数名 + if (checkNextArg && args[index + 1].StartsWith("-")) + { + return ""; // 参数存在但没有值,返回空字符串 + } + return args[index + 1]; + } + return ""; // 参数存在但没有值,返回空字符串 + } + return null; // 参数不存在 + } + + private static void SetElementValue(AutomationElement element, string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new Exception("必须指定要设置的值"); + } + + try + { + var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern; + if (valuePattern != null) + { + valuePattern.SetValue(value); + Console.WriteLine("true"); + return; + } + + throw new Exception("元素不支持设置值操作"); + } + catch (Exception ex) + { + throw new Exception("设置值失败: " + ex.Message); + } + } + + private static void GetElementValue(AutomationElement element) + { + try + { + var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern; + if (valuePattern != null) + { + Console.WriteLine(valuePattern.Current.Value); + return; + } + + throw new Exception("元素不支持获取值操作"); + } + catch (Exception ex) + { + throw new Exception("获取值失败: " + ex.Message); + } + } + + private static void SelectItem(AutomationElement element, string item) + { + if (string.IsNullOrEmpty(item)) + { + throw new Exception("必须指定要选择的项目"); + } + + // 根据控件类型使用不同的查找策略 + TreeScope searchScope; + Condition searchCondition; + + int controlTypeId = element.Current.ControlType.Id; + + if (controlTypeId == ControlType.ComboBox.Id) + { + // ComboBox需要先展开 + var expandPattern = element.GetCurrentPattern(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern; + if (expandPattern != null) + { + expandPattern.Expand(); + System.Threading.Thread.Sleep(100); + } + searchScope = TreeScope.Descendants; + searchCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ListItem); + } + else if (controlTypeId == ControlType.Tree.Id) + { + // TreeView查找所有TreeItem + searchScope = TreeScope.Descendants; + searchCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TreeItem); + } + else if (controlTypeId == ControlType.List.Id || controlTypeId == ControlType.DataGrid.Id || controlTypeId == ControlType.Table.Id) + { + // ListBox, DataGrid, Table 查找直接子项 + searchScope = TreeScope.Children; + searchCondition = new OrCondition( + new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ListItem), + new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.DataItem) + ); + } + else if (controlTypeId == ControlType.Tab.Id) + { + // Tab查找直接子项 + searchScope = TreeScope.Children; + searchCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem); + } + else if (controlTypeId == ControlType.MenuBar.Id || controlTypeId == ControlType.Menu.Id) + { + // 菜单项查找 + searchScope = TreeScope.Descendants; + searchCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.MenuItem); + } + else if (controlTypeId == ControlType.RadioButton.Id || controlTypeId == ControlType.CheckBox.Id) + { + // 单选框和复选框直接选择自身 + if (element.Current.Name == item) + { + var selectionItemPattern = element.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern; + if (selectionItemPattern != null) + { + selectionItemPattern.Select(); + Console.WriteLine("true"); + return; + } + } + throw new Exception(string.Format("找不到指定的选项: {0}", item)); + } + else + { + // 对于其他类型的控件,尝试直接使用SelectionPattern + var selectionPattern = element.GetCurrentPattern(SelectionPattern.Pattern) as SelectionPattern; + if (selectionPattern != null) + { + searchScope = TreeScope.Children; + searchCondition = Condition.TrueCondition; + } + else + { + throw new Exception("不支持的控件类型"); + } + } + + // 查找所有子项 + var children = element.FindAll(searchScope, searchCondition); + if (children.Count == 0) + { + throw new Exception("未找到可选择的项目"); + } + + // 遍历查找匹配项并选择 + foreach (AutomationElement child in children) + { + if (child.Current.Name == item) + { + // 尝试使用SelectionItemPattern选择 + var selectionItemPattern = child.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern; + if (selectionItemPattern != null) + { + selectionItemPattern.Select(); + Console.WriteLine("true"); + return; + } + } + } + + throw new Exception(string.Format("找不到指定的项目: {0}", item)); + } + + private static void ExpandElement(AutomationElement element, bool expand) + { + try + { + var expandCollapsePattern = element.GetCurrentPattern(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern; + if (expandCollapsePattern != null) + { + if (expand) + expandCollapsePattern.Expand(); + else + expandCollapsePattern.Collapse(); + Console.WriteLine("true"); + return; + } + + throw new Exception("元素不支持展开/折叠操作"); + } + catch (Exception ex) + { + throw new Exception("展开/折叠操作失败: " + ex.Message); + } + } + + private static void ScrollElement(AutomationElement element, string direction, double amount) + { + if (element == null) + { + throw new Exception("未找到目标元素"); + } + + try + { + // 首先尝试使用 ScrollPattern + object scrollPattern; + if (element.TryGetCurrentPattern(ScrollPattern.Pattern, out scrollPattern)) + { + var scroll = scrollPattern as ScrollPattern; + if (direction.ToLower() == "horizontal") + { + if (!scroll.Current.HorizontallyScrollable) + { + throw new Exception("元素不支持水平滚动"); + } + scroll.SetScrollPercent(amount, ScrollPattern.NoScroll); + } + else + { + if (!scroll.Current.VerticallyScrollable) + { + throw new Exception("元素不支持垂直滚动"); + } + scroll.SetScrollPercent(ScrollPattern.NoScroll, amount); + } + Console.WriteLine("true"); + return; + } + + // 尝试使用 ScrollItemPattern + object scrollItemPattern; + if (element.TryGetCurrentPattern(ScrollItemPattern.Pattern, out scrollItemPattern)) + { + var scrollItem = scrollItemPattern as ScrollItemPattern; + scrollItem.ScrollIntoView(); + Console.WriteLine("true"); + return; + } + + // 检查是否有滚动条子元素 + var scrollBars = element.FindAll(TreeScope.Children, new PropertyCondition( + AutomationElement.ControlTypeProperty, ControlType.ScrollBar)); + + if (scrollBars.Count > 0) + { + foreach (AutomationElement scrollBar in scrollBars) + { + // 获取滚动条的方向 + bool isHorizontal = scrollBar.Current.BoundingRectangle.Width > scrollBar.Current.BoundingRectangle.Height; + + if ((direction.ToLower() == "horizontal" && isHorizontal) || + (direction.ToLower() == "vertical" && !isHorizontal)) + { + // 使用 RangeValuePattern 设置滚动条的值 + var rangeValuePattern = scrollBar.GetCurrentPattern(RangeValuePattern.Pattern) as RangeValuePattern; + if (rangeValuePattern != null) + { + double maxValue = rangeValuePattern.Current.Maximum; + double minValue = rangeValuePattern.Current.Minimum; + double targetValue = minValue + ((maxValue - minValue) * amount / 100); + rangeValuePattern.SetValue(targetValue); + Console.WriteLine("true"); + return; + } + } + } + } + + throw new Exception("元素不支持滚动操作"); + } + catch (Exception ex) + { + throw new Exception(string.Format("滚动操作失败: {0}", ex.Message)); + } + } + + private static void WaitForElement(string[] args, int timeout) + { + DateTime endTime = DateTime.Now.AddSeconds(timeout); + while (DateTime.Now < endTime) + { + try + { + GetTargetElement(args); + Console.WriteLine("true"); + return; + } + catch + { + Thread.Sleep(500); + } + } + throw new Exception("等待超时"); + } + + private static AutomationElement FindElementByXPath(string xpath, AutomationElement root) + { + string[] segments = xpath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + AutomationElement current = root; + + foreach (string segment in segments) + { + if (string.IsNullOrEmpty(segment)) continue; + + // 解析控件类型和索引 + string controlType = segment; + int index = 1; + string condition = ""; + + // 提取索引 [n] + int indexStart = segment.IndexOf('['); + if (indexStart > 0) + { + int indexEnd = segment.IndexOf(']', indexStart); + if (indexEnd > indexStart) + { + controlType = segment.Substring(0, indexStart); + string indexStr = segment.Substring(indexStart + 1, indexEnd - indexStart - 1); + + // 检查是否是属性条件 + if (indexStr.StartsWith("@")) + { + condition = indexStr; + } + else + { + int parsedIndex; + if (int.TryParse(indexStr, out parsedIndex)) + { + index = parsedIndex; + } + } + } + } + + // 创建控件类型条件 + List conditions = new List(); + if (controlType.StartsWith("ControlType.")) + { + controlType = controlType.Substring("ControlType.".Length); + } + var field = typeof(ControlType).GetField(controlType); + if (field != null) + { + ControlType type = (ControlType)field.GetValue(null); + conditions.Add(new PropertyCondition(AutomationElement.ControlTypeProperty, type)); + } + + // 添加属性条件 + if (!string.IsNullOrEmpty(condition)) + { + if (condition.StartsWith("@Name='")) + { + string name = condition.Substring("@Name='".Length).TrimEnd('\''); + conditions.Add(new PropertyCondition(AutomationElement.NameProperty, name)); + } + else if (condition.StartsWith("@AutomationId='")) + { + string automationId = condition.Substring("@AutomationId='".Length).TrimEnd('\''); + conditions.Add(new PropertyCondition(AutomationElement.AutomationIdProperty, automationId)); + } + else if (condition.StartsWith("@ClassName='")) + { + string className = condition.Substring("@ClassName='".Length).TrimEnd('\''); + conditions.Add(new PropertyCondition(AutomationElement.ClassNameProperty, className)); + } + } + + // 查找元素 + Condition finalCondition = conditions.Count > 1 + ? new AndCondition(conditions.ToArray()) + : conditions[0]; + + var elements = current.FindAll(TreeScope.Children, finalCondition); + if (elements.Count == 0) return null; + + // 使用索引选择元素 + if (index > elements.Count) return null; + current = elements[index - 1]; // 转换为0基索引 + } + + return current; + } + + private static void SetFocus(AutomationElement element) + { + try + { + element.SetFocus(); + Console.WriteLine("true"); + } + catch (Exception ex) + { + throw new Exception("设置焦点失败: " + ex.Message); + } + } + + private static void HighlightElement(AutomationElement element, int duration) + { + var rect = element.Current.BoundingRectangle; + var highlightForm = new Form + { + StartPosition = FormStartPosition.Manual, + Location = new System.Drawing.Point((int)rect.Left, (int)rect.Top), + Size = new System.Drawing.Size((int)rect.Width, (int)rect.Height), + BackColor = Color.Yellow, + Opacity = 0.3, + ShowInTaskbar = false, + FormBorderStyle = FormBorderStyle.None, + TopMost = true + }; + + try + { + highlightForm.Show(); + Console.WriteLine("正在高亮显示元素"); + Thread.Sleep(duration * 1000); + } + finally + { + highlightForm.Close(); + highlightForm.Dispose(); + } + } + + private struct ElementHierarchyInfo + { + public string XPath; + public IntPtr WindowHandle; + } + + // 获取元素的层次结构信息 + private static ElementHierarchyInfo GetElementHierarchyInfo(AutomationElement element) + { + var path = new List(); + var current = element; + var walker = TreeWalker.ControlViewWalker; + IntPtr windowHandle = IntPtr.Zero; + + // 循环直到找到根元素 + while (current != null && current != AutomationElement.RootElement) + { + // 获取父元素,如果获取失败,则停止遍历 + AutomationElement parent; + try + { + parent = walker.GetParent(current); + // 是否是最后一个元素 + bool isLastElement = (parent == AutomationElement.RootElement || parent == null); + // 是否是句柄不为0的窗口 + bool isValidWindow = (current.Current.ControlType.Id == UIA_ControlTypeIds.Window && current.Current.NativeWindowHandle != 0); + // 是否是任务栏 + bool isTaskbar = (current.Current.ClassName == "Shell_TrayWnd" || + current.Current.ClassName == "Shell_SecondaryTrayWnd"); + + // 如果是窗口/任务栏,或者是最后一个元素,获取其句柄 + if (isValidWindow || isTaskbar || isLastElement) + { + windowHandle = new IntPtr(current.Current.NativeWindowHandle); + break; // 获取到句柄后就停止遍历 + } + else + { + // 获取同级元素中的索引 + int index = 1; + var siblings = parent.FindAll(TreeScope.Children, new PropertyCondition( + AutomationElement.ControlTypeProperty, current.Current.ControlType)); + + foreach (AutomationElement sibling in siblings) + { + if (sibling == current) break; + index++; + } + + // 构建路径段 + string type = current.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); + string pathSegment = type; + + // 如果有多个同类型元素,添加索引 + if (siblings.Count > 1) + { + pathSegment = string.Format("{0}[{1}]", type, index); + } + + path.Insert(0, pathSegment); + } + } + catch + { + break; + } + current = parent; + + } + if (windowHandle == IntPtr.Zero) + { + Point currentMousePosition; + GetCursorPos(out currentMousePosition); + windowHandle = WindowFromPoint(currentMousePosition); + } + + return new ElementHierarchyInfo + { + XPath = "/" + string.Join("/", path), + WindowHandle = windowHandle + }; + } + + private static void EnableElement(AutomationElement element, bool enable) + { + if (element == null) + { + throw new Exception("元素不能为空"); + } + try + { + EnableWindow((IntPtr)element.Current.NativeWindowHandle, enable); + Console.WriteLine("true"); + } + catch (Exception ex) + { + throw new Exception("无法更改元素的启用/禁用状态: " + ex.Message); + } + } + + private static void ClickElement(AutomationElement element) + { + if (element == null) + { + throw new Exception("未找到目标元素"); + } + + try + { + // 首先尝试使用 Invoke 模式(适用于按钮等) + object invokePattern; + if (element.TryGetCurrentPattern(InvokePattern.Pattern, out invokePattern)) + { + ((InvokePattern)invokePattern).Invoke(); + Console.WriteLine("true"); + return; + } + + // 尝试使用 SelectionItem 模式(适用于列表项、单选框等) + object selectionItemPattern; + if (element.TryGetCurrentPattern(SelectionItemPattern.Pattern, out selectionItemPattern)) + { + ((SelectionItemPattern)selectionItemPattern).Select(); + Console.WriteLine("true"); + return; + } + + // 尝试使用 Toggle 模式(适用于复选框等) + object togglePattern; + if (element.TryGetCurrentPattern(TogglePattern.Pattern, out togglePattern)) + { + ((TogglePattern)togglePattern).Toggle(); + Console.WriteLine("true"); + return; + } + + // 如果都不支持,尝试使用鼠标点击 + try + { + // 激活元素 + element.SetFocus(); + + // 获取元素的中心点坐标 + System.Windows.Point clickablePoint = element.GetClickablePoint(); + + // 转换为屏幕坐标 + var rect = element.Current.BoundingRectangle; + if (rect.IsEmpty) + { + throw new Exception("无法获取元素位置"); + } + + // 保存当前鼠标位置 + Point currentMousePosition; + GetCursorPos(out currentMousePosition); + + // 使用 mouse_event 执行点击 + int x = (int)clickablePoint.X; + int y = (int)clickablePoint.Y; + + // 移动鼠标到目标位置 + SetCursorPos(x, y); + Thread.Sleep(50); // 短暂延迟确保鼠标移动到位 + + // 模拟鼠标点击 + mouse_event(MOUSEEVENTF_LEFTDOWN, x, y, 0, 0); + Thread.Sleep(50); // 短暂延迟模拟真实点击 + mouse_event(MOUSEEVENTF_LEFTUP, x, y, 0, 0); + + // 恢复鼠标位置 + SetCursorPos(currentMousePosition.X, currentMousePosition.Y); + Console.WriteLine("true"); + } + catch (Exception ex) + { + throw new Exception(string.Format("鼠标点击失败: {0}", ex.Message)); + } + } + catch (Exception ex) + { + throw new Exception(string.Format("点击操作失败: {0}", ex.Message)); + } + } + + private static void SendKeys(AutomationElement element, string keys) + { + if (string.IsNullOrEmpty(keys)) + { + throw new Exception("必须指定要发送的按键"); + } + + try + { + // 确保元素可以接收输入 + if (!element.Current.IsKeyboardFocusable) + { + throw new Exception("元素不支持键盘输入"); + } + + element.SetFocus(); + System.Windows.Forms.SendKeys.SendWait(keys); + Console.WriteLine("true"); + } + catch (Exception ex) + { + throw new Exception("发送按键失败: " + ex.Message); + } + } + + // 元素检查器 + private static void InspectElement(string[] args) + { + // 创建一个半透明遮罩窗口 + overlayForm = new Form(); + overlayForm.FormBorderStyle = FormBorderStyle.None; + overlayForm.StartPosition = FormStartPosition.Manual; + overlayForm.TopMost = true; + overlayForm.BackColor = Color.Blue; + overlayForm.Opacity = 0.15; + overlayForm.ShowInTaskbar = false; + overlayForm.KeyPreview = true; + + // 设置窗口样式,确保能显示在任务栏上方 + overlayForm.Load += (sender, e) => + { + var hwnd = overlayForm.Handle; + var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_LAYERED); + }; + + // 创建预览窗口 + previewForm = new Form + { + FormBorderStyle = FormBorderStyle.None, + StartPosition = FormStartPosition.Manual, + BackColor = Color.FromArgb(40, 40, 40), + ShowInTaskbar = false, + TopMost = true, + Opacity = 0.9, + AutoSize = true, // 添加自动尺寸 + AutoSizeMode = AutoSizeMode.GrowAndShrink, // 根据内容调整大小 + KeyPreview = true + }; + + Label previewLabel = new Label + { + AutoSize = true, + Dock = DockStyle.None, + TextAlign = ContentAlignment.MiddleLeft, + Font = new Font("楷体", 9), + ForeColor = Color.White, + Padding = new Padding(5), + AutoEllipsis = true, + UseMnemonic = false, + MaximumSize = new System.Drawing.Size(600, 0) + }; + previewForm.Controls.Add(previewLabel); + + // 设置预览窗口样式 + previewForm.Load += (sender, e) => + { + var hwnd = previewForm.Handle; + var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_LAYERED); + + // 确保窗口显示在最顶层 + SetWindowPos( + hwnd, + new IntPtr(HWND_TOPMOST), + previewForm.Left, + previewForm.Top, + previewForm.Width, + previewForm.Height, + 0x0040 // SWP_SHOWWINDOW + ); + }; + + // 获取根元素 + var rootElement = AutomationElement.RootElement; + + completed = false; // 使用静态字段 + Rectangle lastRect = Rectangle.Empty; + lastElement = null; + lastCursorPos = new Point(); + AutomationElement taskbarElement = null; + List taskbarChildren = null; + + // 设置/取消窗口鼠标穿透的辅助方法 + Action setMousePenetrate = (penetrate) => + { + var hwnd = overlayForm.Handle; + var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + if (penetrate) + { + SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT); + } + else + { + SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle & ~WS_EX_TRANSPARENT); + } + }; + + // 更新遮罩层位置的辅助方法 + Action updateOverlayPosition = (rect) => + { + overlayForm.Location = new System.Drawing.Point(rect.Left, rect.Top); + overlayForm.Size = new System.Drawing.Size(rect.Width, rect.Height); + // 确保遮罩层始终在最顶层 + SetWindowPos( + overlayForm.Handle, + new IntPtr(HWND_TOPMOST), + rect.Left, + rect.Top, + rect.Width, + rect.Height, + 0x0040 // SWP_SHOWWINDOW + ); + overlayForm.Refresh(); + }; + + // 添加一个新的辅助方法来更新预览窗口位置 + Action updatePreviewPosition = (x, y) => + { + // 确保预览窗口始终在最顶层 + SetWindowPos( + previewForm.Handle, + new IntPtr(HWND_TOPMOST), + x, + y, + previewForm.Width, + previewForm.Height, + 0x0040 // SWP_SHOWWINDOW + ); + previewForm.Refresh(); + }; + + // 创建一个定时器来处理鼠标位置检测 + mouseTimer = new System.Windows.Forms.Timer(); + mouseTimer.Interval = 100; + mouseTimer.Tick += (sender, e) => + { + try + { + Point cursorPos; + GetCursorPos(out cursorPos); + + if (cursorPos == lastCursorPos) + { + return; + } + lastCursorPos = cursorPos; + + setMousePenetrate(true); + + // 获取鼠标位置的元素 + var element = AutomationElement.FromPoint(cursorPos.ToWindowsPoint()); + + setMousePenetrate(false); + + if (element != null && element != rootElement) + { + // AutomationElement.FromPoint只能获得到任务栏 + // 无法获得到任务栏的子元素,所以需要遍历 + element = HandleTaskbarElement( + element, + cursorPos, + ref taskbarElement, + ref taskbarChildren + ); + + var elementBounds = element.Current.BoundingRectangle; + Rectangle elementRect = new Rectangle( + (int)elementBounds.Left, + (int)elementBounds.Top, + (int)elementBounds.Width, + (int)elementBounds.Height + ); + + bool elementChanged = lastElement == null || + !element.Equals(lastElement) || + elementRect != lastRect; + + if (elementChanged) + { + lastElement = element; + lastRect = elementRect; + + // 获取元素后,显示遮罩层 + if (!overlayForm.Visible) + { + overlayForm.Show(); + } + updateOverlayPosition(elementRect); + + // 计算预览窗口位置 + int previewX = elementRect.Right + 10; + int previewY = elementRect.Top; + + // 确保预览窗口不会超出屏幕 + Screen screen = Screen.FromPoint(new System.Drawing.Point(previewX, previewY)); + if (previewX + previewForm.Width > screen.Bounds.Right) + { + previewX = elementRect.Left - previewForm.Width - 10; + } + + if (previewY + previewForm.Height > screen.Bounds.Bottom) + { + previewY = screen.Bounds.Bottom - previewForm.Height; + } + + updatePreviewPosition(previewX, previewY); + } + // 处理名称长度 + string elementName = element.Current.Name; + if (elementName.Length > 50) + { + elementName = elementName.Substring(0, 47) + "..."; + } + // 更新预览窗口内容 + previewLabel.Text = string.Format( + "坐标: {0}, {1}\r\n" + + "名称: {2}\r\n" + + "大小: {3}x{4}\r\n" + + "类型: {5}\r\n" + + "C:复制名称,X:复制名称并退出\r\n" + + "ESC:退出", + cursorPos.X, + cursorPos.Y, + elementName, + elementRect.Width, + elementRect.Height, + element.Current.ControlType.ProgrammaticName.Replace("ControlType.", "")); + previewForm.Show(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + }; + + overlayForm.MouseClick += (sender, e) => + { + if (e.Button == MouseButtons.Left && lastElement != null) + { + try + { + InspectElementInfo(lastElement, lastCursorPos); + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + finally + { + stopInspect(); + } + } + }; + + // 添加按键事件处理 + overlayForm.KeyPress += (sender, e) => + { + HandleKeyPress(sender, e); + }; + previewForm.KeyPress += (sender, e) => + { + HandleKeyPress(sender, e); + }; + + // 在新线程中显示窗口 + Thread thread = new Thread(() => + { + mouseTimer.Start(); + while (!completed) + { + Application.DoEvents(); + Thread.Sleep(100); + } + mouseTimer.Stop(); + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + } + + private static Dictionary GetWindowInfoFromHandle(IntPtr hwnd) + { + var windowInfo = new Dictionary(); + if (hwnd != IntPtr.Zero) + { + StringBuilder title = new StringBuilder(256); + StringBuilder className = new StringBuilder(256); + GetWindowText(hwnd, title, title.Capacity); + GetClassName(hwnd, className, className.Capacity); + + // 获取窗口位置和大小 + RECT rect = new RECT(); + GetWindowRect(hwnd, ref rect); + + windowInfo.Add("title", title.ToString()); + windowInfo.Add("class", className.ToString()); + windowInfo.Add("handle", hwnd.ToInt32()); + windowInfo.Add("x", rect.Left); + windowInfo.Add("y", rect.Top); + windowInfo.Add("width", rect.Right - rect.Left); + windowInfo.Add("height", rect.Bottom - rect.Top); + + // 获取进程信息 + uint processId; + GetWindowThreadProcessId(hwnd, out processId); + try + { + var process = System.Diagnostics.Process.GetProcessById((int)processId); + windowInfo.Add("processName", process.ProcessName); + windowInfo.Add("processPath", process.MainModule.FileName); + } + catch { } + } + return windowInfo; + } + + // 打印元素的完整信息 + private static void InspectElementInfo(AutomationElement element, Point position) + { + var hierarchyInfo = GetElementHierarchyInfo(element); + + Dictionary result = new Dictionary(); + // 元素信息 + Dictionary elementInfo = new Dictionary(); + elementInfo.Add("name", element.Current.Name); + elementInfo.Add("class", element.Current.ClassName); + elementInfo.Add("type", element.Current.ControlType.ProgrammaticName.Replace("ControlType.", "")); + elementInfo.Add("automationId", element.Current.AutomationId); + elementInfo.Add("xpath", hierarchyInfo.XPath); + elementInfo.Add("handle", element.Current.NativeWindowHandle); + + // 添加元素位置和大小信息 + var bounds = element.Current.BoundingRectangle; + elementInfo.Add("x", (int)bounds.Left); + elementInfo.Add("y", (int)bounds.Top); + elementInfo.Add("width", (int)bounds.Width); + elementInfo.Add("height", (int)bounds.Height); + + // 添加控件文本值 + try + { + var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern; + if (valuePattern != null) + { + elementInfo.Add("value", valuePattern.Current.Value); + } + } + catch { } + + // 根据传入的坐标获取窗口信息 + if (hierarchyInfo.WindowHandle != IntPtr.Zero) + { + var windowInfo = GetWindowInfoFromHandle(hierarchyInfo.WindowHandle); + foreach (var kvp in windowInfo) + { + result.Add(kvp.Key, kvp.Value); + } + } + + // 将元素信息添加到结果中 + result.Add("element", elementInfo); + + // 添加坐标信息 + result.Add("position", new Dictionary { + { "x", position.X }, + { "y", position.Y } + }); + + var serializer = new JavaScriptSerializer(); + Console.Write(serializer.Serialize(result)); + } + + private static AutomationElement HandleTaskbarElement(AutomationElement element, Point cursorPos, ref AutomationElement taskbarElement, ref List taskbarChildren) + { + // 检查是否是任务栏元素 + bool isTaskbarElement = element.Current.ClassName == "Shell_TrayWnd" || + element.Current.ClassName == "Shell_SecondaryTrayWnd"; + + // 如果是新的任务栏元素,获取其所有子元素 + if (isTaskbarElement && element != taskbarElement) + { + taskbarElement = GetTaskbarElement(); + if (taskbarElement != null) + { + taskbarChildren = GetTaskbarChildren(taskbarElement); + } + } + + // 如果是任务栏区域,在缓存的子元素中查找 + if (isTaskbarElement && taskbarChildren != null && taskbarChildren.Count > 0) + { + var point = cursorPos.ToWindowsPoint(); + AutomationElement bestMatch = null; + double minArea = double.MaxValue; + + foreach (var child in taskbarChildren) + { + try + { + var childRect = child.Cached.BoundingRectangle; + if (childRect.Contains(point)) + { + double area = childRect.Width * childRect.Height; + if (area < minArea && area > 0) + { + minArea = area; + bestMatch = child; + } + } + } + catch { } + } + + if (bestMatch != null) + { + return bestMatch; + } + } + + return element; + } + + private static void stopInspect() + { + mouseTimer.Stop(); + overlayForm.Close(); + previewForm.Close(); + Environment.Exit(0); + } + + private static void HandleKeyPress(object sender, KeyPressEventArgs e) + { + if (e.KeyChar == (char)27) // ESC键 + { + stopInspect(); + } + else if (e.KeyChar == 'c' || e.KeyChar == 'C' || e.KeyChar == 'x' || e.KeyChar == 'X') // 添加复制功能 + { + if (lastElement != null) + { + try + { + Clipboard.SetText(lastElement.Current.Name); + if (e.KeyChar == 'x' || e.KeyChar == 'X') + { + InspectElementInfo(lastElement, lastCursorPos); + stopInspect(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error copying name: {0}", ex.Message)); + } + } + } + } +} diff --git a/plugin/lib/csharp/dialog.cs b/plugin/lib/csharp/dialog.cs new file mode 100644 index 00000000..f647fde7 --- /dev/null +++ b/plugin/lib/csharp/dialog.cs @@ -0,0 +1,885 @@ +using System; +using System.Windows.Forms; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.IO; +using System.Drawing.Drawing2D; + +public class DialogGenerator +{ + [DllImport("user32.dll")] + private static extern bool SetProcessDPIAware(); + + [DllImport("gdi32.dll")] + private static extern int GetDeviceCaps(IntPtr hdc, int nIndex); + + private const int LOGPIXELSX = 88; + private const int LOGPIXELSY = 90; + + // 基础尺寸常量 (96 DPI下的尺寸,当前尺寸/2) + private const int BASE_WIDTH = 450; // 900/2 = 450 + private const int BASE_HEIGHT = 175; // 350/2 = 175 + private const int BASE_PADDING = 15; // 30/2 = 15 + private const int BASE_BUTTON_HEIGHT = 25; // 50/2 = 25 + private const int BASE_BUTTON_WIDTH = 70; // 140/2 = 70 + private const int BASE_INPUT_HEIGHT = 20; // 40/2 = 20 + private const int BASE_SPACING = 10; // 20/2 = 10 + + // 实际使用的缩放尺寸 + private static int DEFAULT_WIDTH; + private static int DEFAULT_HEIGHT; + private static int PADDING; + private static int BUTTON_HEIGHT; + private static int BUTTON_WIDTH; + private static int INPUT_HEIGHT; + private static int SPACING; + + // 添加获取DPI缩放比例的方法 + private static float GetScaleFactor() + { + using (Graphics g = Graphics.FromHwnd(IntPtr.Zero)) + { + IntPtr desktop = g.GetHdc(); + int dpiX = GetDeviceCaps(desktop, LOGPIXELSX); + g.ReleaseHdc(desktop); + return dpiX / 96f; + } + } + + // 初始化缩放尺寸的方法 + private static void InitializeScaledSizes() + { + float scaleFactor = GetScaleFactor(); + + DEFAULT_WIDTH = (int)(BASE_WIDTH * scaleFactor); + DEFAULT_HEIGHT = (int)(BASE_HEIGHT * scaleFactor); + PADDING = (int)(BASE_PADDING * scaleFactor); + BUTTON_HEIGHT = (int)(BASE_BUTTON_HEIGHT * scaleFactor); + BUTTON_WIDTH = (int)(BASE_BUTTON_WIDTH * scaleFactor); + INPUT_HEIGHT = (int)(BASE_INPUT_HEIGHT * scaleFactor); + SPACING = (int)(BASE_SPACING * scaleFactor); + } + + private static void InitializeDPIAwareness() + { + if (Environment.OSVersion.Version.Major >= 6) + { + SetProcessDPIAware(); + } + } + + public static DialogResult Show(string[] args) + { + InitializeScaledSizes(); // 初始化缩放尺寸 + + string type = GetArgumentValue(args, "-type"); + string title = GetArgumentValue(args, "-title"); + string content = GetArgumentValue(args, "-content"); + string iconPath = GetArgumentValue(args, "-iconpath"); + + if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(title)) + { + ShowHelp(); + return DialogResult.None; + } + + Form dialog = CreateBaseDialog(title); + + // 设置图标 + if (!string.IsNullOrEmpty(iconPath) && File.Exists(iconPath)) + { + try + { + using (Bitmap bmp = new Bitmap(iconPath)) + { + dialog.Icon = Icon.FromHandle(bmp.GetHicon()); + } + } + catch (Exception ex) + { + MessageBox.Show("加载图标失败: " + ex.Message); + } + } + + switch (type.ToLower()) + { + case "message": + CreateMessageDialog(dialog, content); + break; + case "input": + CreateInputDialog(dialog, content); + break; + case "confirm": + CreateConfirmDialog(dialog, content); + break; + case "buttons": + CreateButtonsDialog(dialog, content); + break; + case "textarea": + CreateTextAreaDialog(dialog, content); + break; + default: + MessageBox.Show("不支持的对话框类型"); + return DialogResult.None; + } + + return dialog.ShowDialog(); + } + + private static Form CreateBaseDialog(string title) + { + Form dialog = new Form(); + dialog.Text = title; + dialog.Width = DEFAULT_WIDTH; + dialog.Height = DEFAULT_HEIGHT; + dialog.StartPosition = FormStartPosition.CenterScreen; + dialog.AutoScaleMode = AutoScaleMode.None; // 禁用自动缩放 + + // 禁止调整窗口大小 + dialog.FormBorderStyle = FormBorderStyle.FixedDialog; + dialog.MaximizeBox = false; + dialog.MinimizeBox = false; + + float scaleFactor = GetScaleFactor(); + dialog.Font = new Font("Microsoft YaHei UI", 9F * scaleFactor, FontStyle.Regular, GraphicsUnit.Pixel); + + dialog.Paint += delegate(object sender, PaintEventArgs e) { + e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + }; + + return dialog; + } + + private static void CreateMessageDialog(Form dialog, string content) + { + const int MAX_CONTENT_HEIGHT = 500; + const int MIN_CONTENT_HEIGHT = 200; + int iconSize = 96; + + // 先计算所需的内容高度 + int requiredHeight = Math.Max( + TextRenderer.MeasureText(content, + new Font("Microsoft YaHei UI", 10F, FontStyle.Regular), + new Size(dialog.ClientSize.Width - PADDING * 3 - iconSize, int.MaxValue), + TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl + ).Height + 20, + MIN_CONTENT_HEIGHT + ); + + int contentHeight = Math.Min(requiredHeight, MAX_CONTENT_HEIGHT); + dialog.Height = contentHeight + PADDING * 3 + BUTTON_HEIGHT; + + // 创建内容面板 + Panel contentPanel = new Panel + { + AutoScroll = false, + Width = dialog.ClientSize.Width - PADDING * 2, + Height = dialog.ClientSize.Height - PADDING * 3 - BUTTON_HEIGHT, + Location = new Point(PADDING, PADDING), + BackColor = SystemColors.Control + }; + + dialog.Controls.Add(contentPanel); + + // 添加图标 + PictureBox iconBox = new PictureBox + { + Width = iconSize, + Height = iconSize, + Location = new Point(0, 0), + SizeMode = PictureBoxSizeMode.Zoom, + BackColor = Color.Transparent + }; + + try + { + Icon sysIcon = SystemIcons.Information; + Bitmap bmp = new Bitmap(iconSize, iconSize); + using (Graphics g = Graphics.FromImage(bmp)) + { + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.DrawIcon(sysIcon, new Rectangle(0, 0, iconSize, iconSize)); + } + iconBox.Image = bmp; + } + catch + { + iconBox.Visible = false; + } + contentPanel.Controls.Add(iconBox); + + // 使用普通TextBox替代RichTextBox + TextBox messageBox = new TextBox + { + Text = content, + ReadOnly = true, + Multiline = true, + BorderStyle = BorderStyle.None, + BackColor = SystemColors.Control, + Location = new Point(iconSize + PADDING, 0), + Width = contentPanel.ClientSize.Width - iconSize - PADDING, + Height = contentPanel.Height, + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular), + WordWrap = true, + TabStop = false, + Cursor = Cursors.IBeam, + ScrollBars = requiredHeight > MAX_CONTENT_HEIGHT ? ScrollBars.Vertical : ScrollBars.None + }; + + // 隐藏光标但允许选择 + messageBox.GotFocus += delegate(object sender, EventArgs e) { + if (messageBox.SelectionLength == 0) + { + NativeMethods.HideCaret(messageBox.Handle); + } + }; + + contentPanel.Controls.Add(messageBox); + + // 添加确定按钮 + Button okButton = CreateStyledButton("确定", DialogResult.OK); + okButton.Location = new Point( + dialog.ClientSize.Width - BUTTON_WIDTH - PADDING, + dialog.ClientSize.Height - PADDING - BUTTON_HEIGHT + ); + dialog.Controls.Add(okButton); + + // 确保按钮始终在最上层 + okButton.BringToFront(); + } + + private static void CreateInputDialog(Form dialog, string content) + { + const int MAX_CONTENT_HEIGHT = 500; + const int MIN_CONTENT_HEIGHT = 200; + + string[] prompts = content.Split(new[] { "|||||" }, StringSplitOptions.None); + var textBoxes = new System.Collections.Generic.List(); + + // 创建一个临时窗体和面板来准确计算高度 + using (Form tempForm = new Form()) + { + tempForm.Width = dialog.Width; + Panel tempPanel = new Panel + { + Width = dialog.ClientSize.Width - PADDING * 2, + AutoSize = true + }; + tempForm.Controls.Add(tempPanel); + + // 添加临时控件来计算实际高度 + int currentY = (int)(PADDING * 1.5); // 起始位置 + foreach (string prompt in prompts) + { + Label label = new Label + { + Text = prompt, + AutoSize = true, + Location = new Point(PADDING, currentY), + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular) + }; + tempPanel.Controls.Add(label); + + TextBox textBox = new TextBox + { + Width = tempPanel.Width - PADDING * 2, + Height = INPUT_HEIGHT, + Location = new Point(PADDING, currentY + label.Height + 5), + BorderStyle = BorderStyle.FixedSingle + }; + tempPanel.Controls.Add(textBox); + + currentY += label.Height + INPUT_HEIGHT + SPACING; + } + + // 获取实际需要的高度 + int totalContentHeight = currentY + PADDING; // 添加底部边距 + int requiredHeight = Math.Max(totalContentHeight, MIN_CONTENT_HEIGHT); + int contentHeight = Math.Min(requiredHeight, MAX_CONTENT_HEIGHT); + + // 设置对话框高度,保持原有的总体布局 + dialog.Height = contentHeight + PADDING * 3 + BUTTON_HEIGHT; + + // 创建实际的内容面板 + Panel contentPanel = new Panel + { + AutoScroll = false, + Width = dialog.ClientSize.Width - PADDING * 2, + Height = dialog.ClientSize.Height - PADDING * 3 - BUTTON_HEIGHT, + Location = new Point(PADDING, PADDING), + BackColor = SystemColors.Control + }; + + // 只有当实际内容超过最大高度时才启用滚动 + if (requiredHeight > MAX_CONTENT_HEIGHT) + { + contentPanel.AutoScroll = true; + contentPanel.AutoScrollMinSize = new Size(0, requiredHeight); + } + + dialog.Controls.Add(contentPanel); + + // 添加实际的输入控件 + currentY = (int)(PADDING * 0.5); + bool needScroll = requiredHeight > MAX_CONTENT_HEIGHT; + int scrollWidth = needScroll ? SystemInformation.VerticalScrollBarWidth + 10 : 0; + + foreach (string prompt in prompts) + { + Label label = new Label + { + Text = prompt, + AutoSize = true, + Location = new Point(0, currentY), + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular) + }; + contentPanel.Controls.Add(label); + + TextBox textBox = new TextBox + { + Width = contentPanel.ClientSize.Width - scrollWidth, + Height = INPUT_HEIGHT, + Location = new Point(0, currentY + label.Height + 5), + BorderStyle = BorderStyle.FixedSingle, + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular) + }; + contentPanel.Controls.Add(textBox); + textBoxes.Add(textBox); + + currentY += label.Height + INPUT_HEIGHT + SPACING; + } + } + + // 添加确定按钮 + Button okButton = CreateStyledButton("确定", DialogResult.OK); + okButton.Location = new Point( + dialog.ClientSize.Width - BUTTON_WIDTH - PADDING, + dialog.ClientSize.Height - PADDING - BUTTON_HEIGHT + ); + + // 处理确定按钮点击事件 + okButton.Click += (sender, e) => { + bool hasInput = false; + foreach (var textBox in textBoxes) + { + if (!string.IsNullOrEmpty(textBox.Text)) + { + hasInput = true; + break; + } + } + + if (hasInput) + { + Console.Write("["); + for (int i = 0; i < textBoxes.Count; i++) + { + string value = textBoxes[i].Text ?? ""; + Console.Write("\"" + value.Replace("\"", "\\\"") + "\""); + if (i < textBoxes.Count - 1) + { + Console.Write(","); + } + } + Console.Write("]"); + } + else + { + Console.Write("[]"); + } + }; + + dialog.Controls.Add(okButton); + okButton.BringToFront(); + } + + private static void CreateConfirmDialog(Form dialog, string content) + { + const int MAX_CONTENT_HEIGHT = 500; + const int MIN_CONTENT_HEIGHT = 200; + int iconSize = 96; + + // 先计算所需的内容高度 + int requiredHeight = Math.Max( + TextRenderer.MeasureText(content, + new Font("Microsoft YaHei UI", 10F, FontStyle.Regular), + new Size(dialog.ClientSize.Width - PADDING * 3 - iconSize, int.MaxValue), + TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl + ).Height + 20, + MIN_CONTENT_HEIGHT + ); + + int contentHeight = Math.Min(requiredHeight, MAX_CONTENT_HEIGHT); + dialog.Height = contentHeight + PADDING * 3 + BUTTON_HEIGHT; + + // 创建内容面板 + Panel contentPanel = new Panel + { + AutoScroll = false, + Width = dialog.ClientSize.Width - PADDING * 2, + Height = dialog.ClientSize.Height - PADDING * 3 - BUTTON_HEIGHT, + Location = new Point(PADDING, PADDING), + BackColor = SystemColors.Control + }; + + dialog.Controls.Add(contentPanel); + + // 添加图标 + PictureBox iconBox = new PictureBox + { + Width = iconSize, + Height = iconSize, + Location = new Point(0, 0), + SizeMode = PictureBoxSizeMode.Zoom, + BackColor = Color.Transparent + }; + + try + { + Icon sysIcon = SystemIcons.Warning; // 改为警告图标 + Bitmap bmp = new Bitmap(iconSize, iconSize); + using (Graphics g = Graphics.FromImage(bmp)) + { + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.DrawIcon(sysIcon, new Rectangle(0, 0, iconSize, iconSize)); + } + iconBox.Image = bmp; + } + catch + { + iconBox.Visible = false; + } + contentPanel.Controls.Add(iconBox); + + // 使用TextBox显示内容 + TextBox messageBox = new TextBox + { + Text = content, + ReadOnly = true, + Multiline = true, + BorderStyle = BorderStyle.None, + BackColor = SystemColors.Control, + Location = new Point(iconSize + PADDING, 0), + Width = contentPanel.ClientSize.Width - iconSize - PADDING, + Height = contentPanel.Height, + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular), + WordWrap = true, + TabStop = false, + Cursor = Cursors.IBeam, + ScrollBars = requiredHeight > MAX_CONTENT_HEIGHT ? ScrollBars.Vertical : ScrollBars.None + }; + + // 隐藏光标但允许选择 + messageBox.GotFocus += delegate(object sender, EventArgs e) { + if (messageBox.SelectionLength == 0) + { + NativeMethods.HideCaret(messageBox.Handle); + } + }; + + contentPanel.Controls.Add(messageBox); + + // 添加确定和取消按钮 + Button okButton = CreateStyledButton("确定", DialogResult.OK); + Button cancelButton = CreateStyledButton("取消", DialogResult.Cancel); + + // 修改取消按钮样式为灰色 + cancelButton.BackColor = Color.FromArgb(153, 153, 153); + cancelButton.MouseEnter += delegate(object sender, EventArgs e) { + cancelButton.BackColor = Color.FromArgb(133, 133, 133); + }; + cancelButton.MouseLeave += delegate(object sender, EventArgs e) { + cancelButton.BackColor = Color.FromArgb(153, 153, 153); + }; + + // 调整按钮位置,靠右对齐,确认在最右边 + okButton.Location = new Point( + dialog.ClientSize.Width - BUTTON_WIDTH - PADDING, + dialog.ClientSize.Height - PADDING - BUTTON_HEIGHT + ); + cancelButton.Location = new Point( + dialog.ClientSize.Width - BUTTON_WIDTH * 2 - PADDING * 2, + dialog.ClientSize.Height - PADDING - BUTTON_HEIGHT + ); + + // 处理按钮点击事件 + okButton.Click += (sender, e) => { + Console.Write("true"); + }; + + cancelButton.Click += (sender, e) => { + Console.Write("false"); + }; + + // 处理窗口关闭事件 + dialog.FormClosing += delegate(object sender, FormClosingEventArgs e) { + if (dialog.DialogResult == DialogResult.None) + { + Console.Write("{}"); + } + }; + + dialog.Controls.Add(okButton); + dialog.Controls.Add(cancelButton); + + // 确保按钮始终在最上层 + okButton.BringToFront(); + cancelButton.BringToFront(); + } + + private static void CreateButtonsDialog(Form dialog, string content) + { + const int MAX_CONTENT_HEIGHT = 500; + const int MIN_CONTENT_HEIGHT = 200; + + string[] buttonTexts = content.Split(new[] { "|||||" }, StringSplitOptions.None); + + // 计算所需的内容高度 + int totalHeight = buttonTexts.Length * (BUTTON_HEIGHT + SPACING) - SPACING; // 减去最后一个按钮后的间距 + int requiredHeight = Math.Max(totalHeight + PADDING * 2, MIN_CONTENT_HEIGHT); // 添加上下内边距 + int contentHeight = Math.Min(requiredHeight, MAX_CONTENT_HEIGHT); + + // 设置对话框高度,根据按钮数量添加底部空间 + int bottomPadding = buttonTexts.Length > 1 ? Math.Min(buttonTexts.Length * 10, 50) : 0; // 根据按钮数量增加底部空间,但不超过PADDING + dialog.ClientSize = new Size(dialog.ClientSize.Width, contentHeight + bottomPadding); + + // 创建内容面板 + Panel contentPanel = new Panel + { + AutoScroll = false, + Width = dialog.ClientSize.Width - PADDING * 2, + Height = dialog.ClientSize.Height - PADDING * 2, + Location = new Point(PADDING, PADDING), + BackColor = SystemColors.Control + }; + + // 只有当实际内容超过最大高度时才启用滚动 + if (requiredHeight > MAX_CONTENT_HEIGHT) + { + contentPanel.AutoScroll = true; + contentPanel.AutoScrollMinSize = new Size(0, totalHeight); + } + + dialog.Controls.Add(contentPanel); + + // 添加按钮 + int currentY = PADDING; // 从内边距开始 + bool needScroll = requiredHeight > MAX_CONTENT_HEIGHT; + int scrollWidth = needScroll ? SystemInformation.VerticalScrollBarWidth + 10 : 0; + + for (int i = 0; i < buttonTexts.Length; i++) + { + Button button = new Button + { + Text = buttonTexts[i], + Width = contentPanel.ClientSize.Width - scrollWidth, + Height = BUTTON_HEIGHT, + Location = new Point(0, currentY), + Tag = i, + FlatStyle = FlatStyle.Flat, + BackColor = Color.FromArgb(0, 122, 204), // 使用蓝色背景 + ForeColor = Color.White, // 白色文字 + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular), + Cursor = Cursors.Hand, + TextAlign = ContentAlignment.MiddleCenter // 文字居中 + }; + + // 设置边框 + button.FlatAppearance.BorderSize = 0; + + // 添加圆角效果 + GraphicsPath path = new GraphicsPath(); + int radius = 8; // 圆角半径 + Rectangle rect = new Rectangle(0, 0, button.Width, button.Height); + path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90); + path.AddArc(rect.X + rect.Width - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90); + path.AddArc(rect.X + rect.Width - radius * 2, rect.Y + rect.Height - radius * 2, radius * 2, radius * 2, 0, 90); + path.AddArc(rect.X, rect.Y + rect.Height - radius * 2, radius * 2, radius * 2, 90, 90); + path.CloseFigure(); + button.Region = new Region(path); + + // 修改鼠标悬停效果的颜色 + button.MouseEnter += delegate(object sender, EventArgs e) { + button.BackColor = Color.FromArgb(0, 102, 184); // 深一点的蓝色 + }; + button.MouseLeave += delegate(object sender, EventArgs e) { + button.BackColor = Color.FromArgb(0, 122, 204); // 恢复原来的蓝色 + }; + + // 修改文本绘制部分 + button.Paint += delegate(object sender, PaintEventArgs e) { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath buttonPath = new GraphicsPath()) + { + Rectangle newRect = new Rectangle(0, 0, button.Width, button.Height); + buttonPath.AddArc(newRect.X, newRect.Y, radius * 2, radius * 2, 180, 90); + buttonPath.AddArc(newRect.X + newRect.Width - radius * 2, newRect.Y, radius * 2, radius * 2, 270, 90); + buttonPath.AddArc(newRect.X + newRect.Width - radius * 2, newRect.Y + newRect.Height - radius * 2, radius * 2, radius * 2, 0, 90); + buttonPath.AddArc(newRect.X, newRect.Y + newRect.Height - radius * 2, radius * 2, radius * 2, 90, 90); + buttonPath.CloseFigure(); + + e.Graphics.FillPath(new SolidBrush(button.BackColor), buttonPath); + + // 绘制文本(居中对齐) + StringFormat sf = new StringFormat(); + sf.Alignment = StringAlignment.Center; // 水平居中 + sf.LineAlignment = StringAlignment.Center; // 垂直居中 + e.Graphics.DrawString(button.Text, button.Font, new SolidBrush(button.ForeColor), newRect, sf); + } + }; + + // 添加按钮点击事件 + button.Click += delegate(object sender, EventArgs e) { + Button clickedButton = (Button)sender; + int id = (int)clickedButton.Tag; + string text = clickedButton.Text; + Console.Write("{\"id\":" + id + ",\"text\":\"" + text.Replace("\"", "\\\"") + "\"}"); + dialog.DialogResult = DialogResult.OK; + }; + + contentPanel.Controls.Add(button); + currentY += BUTTON_HEIGHT + SPACING; + } + + // 处理窗口关闭事件 + dialog.FormClosing += delegate(object sender, FormClosingEventArgs e) { + if (dialog.DialogResult == DialogResult.None) + { + Console.Write("{}"); + } + }; + } + + private static void CreateTextAreaDialog(Form dialog, string content) + { + dialog.Height = 600; // 改为600 + + TextBox textArea = new TextBox + { + Multiline = true, + ScrollBars = ScrollBars.Vertical, + Width = dialog.ClientSize.Width - PADDING * 2, + Height = dialog.ClientSize.Height - PADDING * 3 - BUTTON_HEIGHT, + Location = new Point(PADDING, PADDING), + ForeColor = SystemColors.WindowText, + Text = content, // 使用传入的content作为默认文本 + Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Regular), + BorderStyle = BorderStyle.FixedSingle + }; + + dialog.Controls.Add(textArea); + + // 添加确定按钮 + Button okButton = CreateStyledButton("确定", DialogResult.OK); + okButton.Location = new Point( + dialog.ClientSize.Width - BUTTON_WIDTH - PADDING, + dialog.ClientSize.Height - PADDING - BUTTON_HEIGHT + ); + + // 处理确定按钮点击事件 + okButton.Click += delegate(object sender, EventArgs e) { + Console.Write(textArea.Text ?? ""); // 直接输出文本,不加引号 + }; + + dialog.Controls.Add(okButton); + okButton.BringToFront(); + } + + private static string GetArgumentValue(string[] args, string key) + { + int index = Array.IndexOf(args, key); + if (index >= 0 && index < args.Length - 1) + { + return args[index + 1]; + } + return string.Empty; + } + + private static void StyleButton(Button button) + { + button.FlatStyle = FlatStyle.Flat; + button.FlatAppearance.BorderSize = 0; + button.BackColor = Color.FromArgb(0, 122, 204); + button.ForeColor = Color.White; + button.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular); + button.Cursor = Cursors.Hand; + button.Width = BUTTON_WIDTH; + button.Height = BUTTON_HEIGHT; + + // 圆角绘制 + GraphicsPath path = new GraphicsPath(); + int radius = 8; // 圆角半径 + Rectangle rect = new Rectangle(0, 0, button.Width, button.Height); + path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90); + path.AddArc(rect.X + rect.Width - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90); + path.AddArc(rect.X + rect.Width - radius * 2, rect.Y + rect.Height - radius * 2, radius * 2, radius * 2, 0, 90); + path.AddArc(rect.X, rect.Y + rect.Height - radius * 2, radius * 2, radius * 2, 90, 90); + path.CloseFigure(); + button.Region = new Region(path); + + // 添加鼠标悬停效果 + button.MouseEnter += delegate(object sender, EventArgs e) { + button.BackColor = Color.FromArgb(0, 102, 184); + }; + button.MouseLeave += delegate(object sender, EventArgs e) { + button.BackColor = Color.FromArgb(0, 122, 204); + }; + + // 自定义绘制 + button.Paint += delegate(object sender, PaintEventArgs e) { + e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + using (GraphicsPath buttonPath = new GraphicsPath()) + { + Rectangle newRect = new Rectangle(0, 0, button.Width, button.Height); + buttonPath.AddArc(newRect.X, newRect.Y, radius * 2, radius * 2, 180, 90); + buttonPath.AddArc(newRect.X + newRect.Width - radius * 2, newRect.Y, radius * 2, radius * 2, 270, 90); + buttonPath.AddArc(newRect.X + newRect.Width - radius * 2, newRect.Y + newRect.Height - radius * 2, radius * 2, radius * 2, 0, 90); + buttonPath.AddArc(newRect.X, newRect.Y + newRect.Height - radius * 2, radius * 2, radius * 2, 90, 90); + buttonPath.CloseFigure(); + + e.Graphics.FillPath(new SolidBrush(button.BackColor), buttonPath); + + // 绘制文本 + StringFormat sf = new StringFormat(); + sf.Alignment = StringAlignment.Center; + sf.LineAlignment = StringAlignment.Center; + e.Graphics.DrawString(button.Text, button.Font, new SolidBrush(button.ForeColor), newRect, sf); + } + }; + } + + private static Button CreateStyledButton(string text, DialogResult dialogResult) + { + Button button = new Button(); + button.Text = text; + button.DialogResult = dialogResult; + StyleButton(button); + return button; + } + + public static void Main(string[] args) + { + if (Environment.OSVersion.Version.Major >= 6) + { + SetProcessDPIAware(); + } + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Show(args); + } + + private class NativeMethods + { + [DllImport("user32.dll")] + public static extern bool HideCaret(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref RECT lParam); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + } + + private static void ShowHelp() + { + string help = @" +Windows 对话框生成工具使用说明 +========================== + +基本语法: +dialog.exe -type <对话框类型> [参数...] + +对话框类型: +---------- +1. message - 消息对话框,显示一条消息 +2. input - 输入对话框,支持多个输入框 +3. confirm - 确认对话框,带确定和取消按钮 +4. buttons - 按钮对话框,支持多个自定义按钮 +5. textarea - 文本区域对话框,用于编辑大段文本 + +通用参数: +-------- +-type 对话框类型(必需) + 可选值:message, input, confirm, buttons, textarea + +-title 对话框标题(必需) + 示例:-title ""提示"" + +-content 对话框内容 + - message/confirm/textarea:显示的文本内容 + - input:输入框提示文本,多个输入框用|||||分隔 + - buttons:按钮文本,多个按钮用|||||分隔 + +-iconpath 对话框图标路径(可选) + 示例:-iconpath ""D:\icons\app.ico"" + +使用示例: +-------- +1. 显示消息对话框: + dialog.exe -type message -title ""提示"" -content ""操作已完成"" + +2. 显示输入对话框(单个输入): + dialog.exe -type input -title ""输入"" -content ""请输入用户名:"" + +3. 显示输入对话框(多个输入): + dialog.exe -type input -title ""用户信息"" -content ""用户名:|||||密码:|||||邮箱:"" + +4. 显示确认对话框: + dialog.exe -type confirm -title ""确认"" -content ""确定要删除这个文件吗?"" + +5. 显示按钮对话框: + dialog.exe -type buttons -title ""选择操作"" -content ""保存|||||不保存|||||取消"" + +6. 显示文本区域对话框: + dialog.exe -type textarea -title ""编辑文本"" -content ""在这里输入内容..."" + +返回值: +------ +1. message对话框: + 无返回值 + +2. input对话框: + 返回JSON数组,包含所有输入框的值 + 示例:[""user"",""123456"",""user@email.com""] + 用户取消:[] + +3. confirm对话框: + 确定:true + 取消:false + 关闭窗口:{} + +4. buttons对话框: + 返回JSON对象,包含按钮id和文本 + 示例:{""id"":0,""text"":""保存""} + 关闭窗口:{} + +5. textarea对话框: + 返回编辑后的文本内容 + 取消:空字符串 + +注意事项: +-------- +1. 包含空格的参数值需要用引号括起来 +2. 多个输入框或按钮的文本用|||||(5个竖线)分隔 +3. 所有对话框都支持自定义图标(使用-iconpath参数) +4. 对话框会自动适应DPI缩放 +5. 所有对话框都支持文本选择和复制 + +更多信息: +-------- +遇到问题请查看错误信息,错误信息会提供详细的原因说明。 +"; + Console.WriteLine(help); + } +} diff --git a/plugin/lib/csharp/explorer.cs b/plugin/lib/csharp/explorer.cs new file mode 100644 index 00000000..8e868f1e --- /dev/null +++ b/plugin/lib/csharp/explorer.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; + +public class ExplorerManager +{ + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + try + { + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + throw new Exception("必须指定操作类型 (-type)"); + } + + switch (type.ToLower()) + { + case "list": + ListExplorerWindows(); + break; + + case "navigate": + string handle = GetArgumentValue(args, "-handle"); + string path = GetArgumentValue(args, "-path"); + if (string.IsNullOrEmpty(handle) || string.IsNullOrEmpty(path)) + { + throw new Exception("必须指定窗口句柄和目标路径"); + } + NavigateToPath(long.Parse(handle), path); + break; + + default: + throw new Exception(string.Format("不支持的操作类型: {0}", type)); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static void ListExplorerWindows() + { + var explorerWindows = new List>(); + + try + { + dynamic shellApp = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application")); + dynamic windows = shellApp.Windows(); + + foreach (dynamic window in windows) + { + try + { + string locationUrl = window.LocationURL; + if (string.IsNullOrEmpty(locationUrl)) continue; + + string path = new Uri(locationUrl).LocalPath; + explorerWindows.Add(new Dictionary + { + { "handle", window.HWND }, + { "title", window.LocationName }, + { "path", path }, + { "class", "CabinetWClass" } + }); + } + catch + { + // 忽略获取信息失败的窗口 + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + return; + } + + var serializer = new JavaScriptSerializer(); + Console.WriteLine(serializer.Serialize(explorerWindows)); + } + + private static void NavigateToPath(long handle, string path) + { + try + { + dynamic shellApp = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application")); + dynamic windows = shellApp.Windows(); + + foreach (dynamic window in windows) + { + try + { + if (window.HWND == handle) + { + window.Navigate(path); + Console.WriteLine("true"); + return; + } + } + catch + { + // 忽略单个窗口的错误 + } + } + throw new Exception("未找到指定的窗口"); + } + catch (Exception ex) + { + throw new Exception(string.Format("导航失败: {0}", ex.Message)); + } + } + + private static string GetArgumentValue(string[] args, string key) + { + int index = Array.IndexOf(args, key); + if (index >= 0 && index < args.Length - 1) + { + return args[index + 1]; + } + return null; + } + + private static void ShowHelp() + { + Console.WriteLine(@" +Windows 资源管理器工具使用说明 +========================== + +基本语法: +explorer.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. list - 列出所有打开的资源管理器窗口 +2. navigate - 导航到指定路径 + 参数: + -handle 窗口句柄 + -path 目标路径 + +返回值: +------ +list: JSON格式的窗口信息数组 +navigate: true表示成功 + +使用示例: +-------- +1. 列出所有资源管理器窗口: + explorer.exe -type list +2. 导航到指定路径: + explorer.exe -type navigate -handle 12345 -path ""C:\Windows"" +"); + } +} diff --git a/plugin/lib/csharp/index.js b/plugin/lib/csharp/index.js new file mode 100644 index 00000000..339b2681 --- /dev/null +++ b/plugin/lib/csharp/index.js @@ -0,0 +1,209 @@ +const fs = require("fs"); +const path = require("path"); +const iconv = require("iconv-lite"); +const child_process = require("child_process"); +const { getQuickcommandFolderFile } = require("../getQuickcommandFile"); + +let currentChild = null; + +const getAssemblyPath = (assembly) => { + const { version } = getCscPath(); + const is64bit = process.arch === "x64"; + + const paths = [ + // v4.0 路径 + path.join( + process.env.WINDIR, + "Microsoft.NET", + "assembly", + "GAC_MSIL", + assembly, + "v4.0_4.0.0.0__31bf3856ad364e35", + assembly + ".dll" + ), + path.join( + process.env.WINDIR, + "Microsoft.NET", + is64bit ? "Framework64" : "Framework", + "v4.0.30319", + assembly + ".dll" + ), + path.join( + process.env.WINDIR, + "Microsoft.NET", + is64bit ? "Framework64" : "Framework", + "v4.0.30319", + "WPF", + assembly + ".dll" + ), + + // v3.0/v3.5 路径 + path.join( + process.arch === "x64" + ? process.env.ProgramFiles + : process.env["ProgramFiles(x86)"], + "Reference Assemblies", + "Microsoft", + "Framework", + "v3.0", + assembly + ".dll" + ), + path.join( + process.env.WINDIR, + "assembly", + "GAC_MSIL", + assembly, + "3.0.0.0__31bf3856ad364e35", + assembly + ".dll" + ), + ]; + + // 根据csc版本筛选合适的路径 + const filteredPaths = paths.filter((p) => { + if (version === "v4.0") return true; // v4.0可以使用所有版本 + return !p.includes("v4.0"); // v3.5只使用v3.0及以下版本 + }); + + for (const p of filteredPaths) { + if (fs.existsSync(p)) return p; + } + return null; +}; + +const getFeatureReferences = (feature) => { + let references = ""; + if (feature === "automation") { + const automationDll = getAssemblyPath("UIAutomationClient"); + const formsDll = getAssemblyPath("System.Windows.Forms"); + const typesDll = getAssemblyPath("UIAutomationTypes"); + const baseDll = getAssemblyPath("WindowsBase"); + const drawingDll = getAssemblyPath("System.Drawing"); + if (!automationDll) throw new Error("找不到UIAutomationClient.dll"); + if (!formsDll) throw new Error("找不到System.Windows.Forms.dll"); + if (!typesDll) throw new Error("找不到UIAutomationTypes.dll"); + if (!baseDll) throw new Error("找不到WindowsBase.dll"); + if (!drawingDll) throw new Error("找不到System.Drawing.dll"); + references = + `/reference:"${automationDll}" /reference:"${formsDll}" ` + + `/reference:"${typesDll}" /reference:"${baseDll}" ` + + `/reference:"${drawingDll}" `; + } + return references; +}; + +const getCsharpFeatureCs = (feature) => { + return path.join(__dirname, feature + ".cs"); +}; + +const getCsharpFeatureExe = async (feature, alwaysBuildNewExe = false) => { + const exePath = getQuickcommandFolderFile(feature, "exe"); + if (!fs.existsSync(exePath) || alwaysBuildNewExe) { + await buildCsharpFeature(feature); + } + return exePath; +}; + +const buildCsharpFeature = async (feature) => { + return new Promise((resolve, reject) => { + const exePath = getQuickcommandFolderFile(feature, "exe"); + const srcCsPath = getCsharpFeatureCs(feature); + const destCsPath = getQuickcommandFolderFile(feature, "cs"); + const { path: cscPath } = getCscPath(); + const references = getFeatureReferences(feature); + + fs.copyFile(srcCsPath, destCsPath, (err) => { + if (err) return reject(err.toString()); + const command = `${cscPath} /nologo ${references}/out:"${exePath}" "${destCsPath}"`; + console.log(command); + child_process.exec(command, { encoding: null }, (err, stdout) => { + if (err) return reject(iconv.decode(stdout, "gbk")); + else resolve(iconv.decode(stdout, "gbk")); + fs.unlink(destCsPath, () => {}); + }); + }); + }); +}; + +const getCscPath = () => { + const is64bit = process.arch === "x64"; + let cscPath = path.join( + process.env.WINDIR, + "Microsoft.NET", + is64bit ? "Framework64" : "Framework", + "v4.0.30319", + "csc.exe" + ); + let version = "v4.0"; + + if (!fs.existsSync(cscPath)) { + cscPath = path.join( + process.env.WINDIR, + "Microsoft.NET", + is64bit ? "Framework64" : "Framework", + "v3.5", + "csc.exe" + ); + version = "v3.5"; + } + if (!fs.existsSync(cscPath)) { + throw new Error("未安装.NET Framework"); + } + return { path: cscPath, version }; +}; + +/** + * 运行C#插件 + * @param {string} feature - 插件名称 + * @param {string[]} args - 参数 + * @param {object} options - 选项 + * @param {boolean} options.alwaysBuildNewExe - 是否总是构建新的可执行文件 + * @returns {Promise} - 返回结果 + */ +const runCsharpFeature = async (feature, args = [], options = {}) => { + return new Promise(async (reslove, reject) => { + const { alwaysBuildNewExe = window.utools.isDev(), killPrevious = true } = + options; + try { + if (killPrevious && currentChild) { + currentChild.kill(); + } + const featureExePath = await getCsharpFeatureExe( + feature, + alwaysBuildNewExe + ); + console.log(featureExePath, args.join(" ")); + currentChild = child_process.spawn(featureExePath, args, { + encoding: null, + }); + + let stdoutData = Buffer.from([]); + let stderrData = Buffer.from([]); + + currentChild.stdout.on("data", (data) => { + stdoutData = Buffer.concat([stdoutData, data]); + }); + + currentChild.stderr.on("data", (data) => { + stderrData = Buffer.concat([stderrData, data]); + }); + + currentChild.on("error", (err) => { + reject(err.toString()); + }); + + currentChild.on("close", (code) => { + if (code !== 0 || stderrData.length > 0) { + reject( + iconv.decode(stderrData.length ? stderrData : stdoutData, "gbk") + ); + } else { + reslove(iconv.decode(stdoutData, "gbk")); + } + }); + } catch (error) { + return reject(error.toString()); + } + }); +}; + +module.exports = { runCsharpFeature }; diff --git a/plugin/lib/csharp/monitor.cs b/plugin/lib/csharp/monitor.cs new file mode 100644 index 00000000..5db5adc7 --- /dev/null +++ b/plugin/lib/csharp/monitor.cs @@ -0,0 +1,279 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Windows.Forms; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Linq; +using System.Drawing; + +class Monitor { + // Win32 API + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext); + + private static FileSystemWatcher fsWatcher; + private static IntPtr nextClipboardViewer; + private static ClipboardForm clipboardForm; + private static bool running = true; + private static bool listenOnce = false; + private static bool isFirstClipboardEvent = true; + + private class ClipboardForm : Form { + public ClipboardForm() { + this.ShowInTaskbar = false; + this.Visible = false; + this.WindowState = FormWindowState.Minimized; + this.FormBorderStyle = FormBorderStyle.None; + this.Size = new System.Drawing.Size(1, 1); + this.Load += (sender, e) => { + nextClipboardViewer = SetClipboardViewer(this.Handle); + }; + this.FormClosing += (sender, e) => { + ChangeClipboardChain(this.Handle, nextClipboardViewer); + }; + } + + protected override CreateParams CreateParams { + get { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x80; // WS_EX_TOOLWINDOW + return cp; + } + } + + protected override void WndProc(ref Message m) { + if (m.Msg == 0x308) { // WM_DRAWCLIPBOARD + if (isFirstClipboardEvent) { + isFirstClipboardEvent = false; + } else { + HandleClipboardChanged(); + if (listenOnce) { + running = false; + this.BeginInvoke(new Action(this.Close)); + } + } + SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam); + } + else if (m.Msg == 0x30D) { // WM_CHANGECBCHAIN + if (m.WParam == nextClipboardViewer) + nextClipboardViewer = m.LParam; + else if (nextClipboardViewer != IntPtr.Zero) + SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam); + } + base.WndProc(ref m); + } + } + + public static void ShowHelp() { + Console.WriteLine(@"Windows 监控工具使用说明 +用法: monitor.exe -type <监控类型> [参数...] + +监控类型: + clipboard - 剪贴板监控 + 参数: + -once 只监听一次变化就停止(可选) + 示例: + monitor.exe -type clipboard -once + + filesystem - 文件系统监控 + 参数: + -path <路径> 要监控的文件夹路径 + -filter <过滤器> 文件过滤器,如 *.txt(可选,默认监控所有文件) + -recursive 是否监控子文件夹(可选,默认为 true) + -once 只监听一次变化就停止(可选) + 示例: + monitor.exe -type filesystem -path C:\MyFolder -filter *.txt -recursive true -once + +返回值: + 监控到的变化会实时输出 JSON 格式的事件信息: + 剪贴板事件: + {""type"": ""clipboard"", ""format"": ""文本/图片/文件"", ""content"": ""变化内容""} + 文件系统事件: + {""type"": ""filesystem"", ""event"": ""created/changed/deleted/renamed"", ""path"": ""文件路径"", ""oldPath"": ""重命名前的路径""} + +注意事项: + 1. 按 Ctrl+C 停止监控 + 2. 剪贴板监控支持文本、图片和文件格式 + 3. 文件系统监控支持文件和文件夹的创建、修改、删除和重命名事件 + 4. 使用 -once 参数可以在监听到第一次变化后自动停止"); + } + + private static string GetArgumentValue(string[] args, string key) { + for (int i = 0; i < args.Length - 1; i++) { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) { + return args[i + 1]; + } + } + return null; + } + + private static string EscapeJsonString(string str) { + if (str == null) return "null"; + StringBuilder sb = new StringBuilder(); + foreach (char c in str) { + switch (c) { + case '\"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + default: + if (c < ' ') { + sb.Append(string.Format("\\u{0:X4}", (int)c)); + } + else { + sb.Append(c); + } + break; + } + } + return string.Format("\"{0}\"", sb.ToString()); + } + + private static void OutputEvent(string type, string format, string content) { + string json = string.Format("{{\"type\": {0}, \"format\": {1}, \"content\": {2}}}", + EscapeJsonString(type), + EscapeJsonString(format), + EscapeJsonString(content)); + Console.WriteLine(json); + } + + private static void OutputFileSystemEvent(string type, string eventType, string path, string oldPath = null) { + StringBuilder json = new StringBuilder(); + json.Append("{\"type\": ").Append(EscapeJsonString(type)); + json.Append(", \"event\": ").Append(EscapeJsonString(eventType)); + json.Append(", \"path\": ").Append(EscapeJsonString(path)); + if (oldPath != null) { + json.Append(", \"oldPath\": ").Append(EscapeJsonString(oldPath)); + } + json.Append("}"); + Console.WriteLine(json.ToString()); + } + + private static void StartClipboardMonitor() { + Thread thread = new Thread(() => { + clipboardForm = new ClipboardForm(); + Application.Run(clipboardForm); + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); + + private static void HandleClipboardChanged() { + if (Clipboard.ContainsText()) { + OutputEvent("clipboard", "text", Clipboard.GetText()); + } + else if (Clipboard.ContainsImage()) { + OutputEvent("clipboard", "image", "image_data"); + } + else if (Clipboard.ContainsFileDropList()) { + OutputEvent("clipboard", "files", string.Join(", ", Clipboard.GetFileDropList().Cast())); + } + } + + private static void StartFileSystemMonitor(string path, string fileFilter, bool includeSubdirectories) { + fsWatcher = new FileSystemWatcher(path); + fsWatcher.Filter = fileFilter ?? "*.*"; + fsWatcher.IncludeSubdirectories = includeSubdirectories; + + FileSystemEventHandler handler = (s, e) => { + OutputFileSystemEvent("filesystem", e.ChangeType.ToString().ToLower(), e.FullPath); + if (listenOnce) { + running = false; + fsWatcher.Dispose(); + } + }; + + RenamedEventHandler renameHandler = (s, e) => { + OutputFileSystemEvent("filesystem", "renamed", e.FullPath, e.OldFullPath); + if (listenOnce) { + running = false; + fsWatcher.Dispose(); + } + }; + + fsWatcher.Created += handler; + fsWatcher.Changed += handler; + fsWatcher.Deleted += handler; + fsWatcher.Renamed += renameHandler; + + fsWatcher.EnableRaisingEvents = true; + } + + private static bool HasArgument(string[] args, string key) { + return Array.Exists(args, arg => arg.Equals(key, StringComparison.OrdinalIgnoreCase)); + } + + public static void Main(string[] args) { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) { + Console.Error.WriteLine("Error: 必须指定监控类型 (-type)"); + return; + } + + listenOnce = HasArgument(args, "-once"); + + Console.CancelKeyPress += (s, e) => { + running = false; + if (fsWatcher != null) { + fsWatcher.Dispose(); + } + if (clipboardForm != null) { + clipboardForm.Invoke(new Action(() => clipboardForm.Close())); + } + Environment.Exit(0); + }; + + try { + switch (type.ToLower()) { + case "clipboard": + StartClipboardMonitor(); + break; + + case "filesystem": + string path = GetArgumentValue(args, "-path"); + if (string.IsNullOrEmpty(path)) { + Console.Error.WriteLine("Error: 必须指定监控路径 (-path)"); + return; + } + + string filter = GetArgumentValue(args, "-filter"); + bool recursive = true; + string recursiveArg = GetArgumentValue(args, "-recursive"); + if (!string.IsNullOrEmpty(recursiveArg)) { + recursive = bool.Parse(recursiveArg); + } + + StartFileSystemMonitor(path, filter, recursive); + break; + + default: + Console.Error.WriteLine(string.Format("Error: 不支持的监控类型: {0}", type)); + return; + } + + while (running) { + Thread.Sleep(100); + } + } + catch (Exception ex) { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } +} diff --git a/plugin/lib/csharp/process.cs b/plugin/lib/csharp/process.cs new file mode 100644 index 00000000..32bad458 --- /dev/null +++ b/plugin/lib/csharp/process.cs @@ -0,0 +1,281 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.IO; + +public class ProcessManager +{ + [DllImport("kernel32.dll")] + private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll")] + private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + private const uint PROCESS_TERMINATE = 0x0001; + private const uint PROCESS_QUERY_INFORMATION = 0x0400; + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + Console.Error.WriteLine("Error: 必须指定操作类型 (-type)"); + return; + } + + try + { + switch (type.ToLower()) + { + case "list": + ListProcesses(); + break; + case "kill": + string target = GetArgumentValue(args, "-target"); + if (string.IsNullOrEmpty(target)) + { + Console.Error.WriteLine("Error: 必须指定目标进程 (-target)"); + return; + } + KillProcess(target); + break; + case "start": + string path = GetArgumentValue(args, "-path"); + string arguments = GetArgumentValue(args, "-args"); + if (arguments == null) + { + arguments = ""; + } + StartProcess(path, arguments); + break; + default: + Console.Error.WriteLine("Error: 不支持的操作类型"); + break; + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static void ListProcesses() + { + Console.Write("["); + Process[] processes = Process.GetProcesses(); + bool first = true; + foreach (Process proc in processes) + { + try + { + string processPath = ""; + DateTime startTime = DateTime.MinValue; + TimeSpan cpuTime = TimeSpan.Zero; + long memorySize = 0; + int threadCount = 0; + ProcessPriorityClass priority = ProcessPriorityClass.Normal; + string description = ""; + string company = ""; + string version = ""; + + try + { + if (proc.MainModule != null) + { + processPath = proc.MainModule.FileName; + } + else + { + processPath = ""; + } + startTime = proc.StartTime; + cpuTime = proc.TotalProcessorTime; + memorySize = proc.WorkingSet64; + threadCount = proc.Threads.Count; + priority = proc.PriorityClass; + + // 获取文件版本信息 + if (!string.IsNullOrEmpty(processPath) && File.Exists(processPath)) + { + var versionInfo = FileVersionInfo.GetVersionInfo(processPath); + description = versionInfo.FileDescription; + if (description == null) description = ""; + company = versionInfo.CompanyName; + if (company == null) company = ""; + version = versionInfo.FileVersion; + if (version == null) version = ""; + } + } + catch { } + + if (!first) + { + Console.Write(","); + } + first = false; + + Console.Write(string.Format( + "{{" + + "\"id\": {0}," + + "\"name\": \"{1}\"," + + "\"title\": \"{2}\"," + + "\"path\": \"{3}\"," + + "\"startTime\": \"{4}\"," + + "\"cpuTime\": \"{5}\"," + + "\"memory\": {6}," + + "\"threads\": {7}," + + "\"priority\": \"{8}\"," + + "\"description\": \"{9}\"," + + "\"company\": \"{10}\"," + + "\"version\": \"{11}\"" + + "}}", + proc.Id, + proc.ProcessName, + proc.MainWindowTitle.Replace("\"", "\\\""), + processPath.Replace("\\", "\\\\").Replace("\"", "\\\""), + startTime.ToString("yyyy-MM-dd HH:mm:ss"), + cpuTime.ToString(), + memorySize, + threadCount, + priority.ToString(), + description.Replace("\"", "\\\""), + company.Replace("\"", "\\\""), + version.Replace("\"", "\\\"") + )); + } + catch { } + } + Console.Write("]"); + } + + private static void KillProcess(string target) + { + int pid; + if (int.TryParse(target, out pid)) + { + KillProcessById(pid); + } + else + { + KillProcessByName(target); + } + } + + private static void KillProcessById(int pid) + { + Process proc = Process.GetProcessById(pid); + try + { + proc.Kill(); + Console.WriteLine(string.Format("成功终止进程: {0} (PID: {1})", proc.ProcessName, pid)); + } + catch (Exception ex) + { + throw new Exception(string.Format("无法终止进程: {0}", ex.Message)); + } + } + + private static void KillProcessByName(string name) + { + Process[] processes = Process.GetProcessesByName(name); + if (processes.Length == 0) + { + throw new Exception("找不到指定的进程"); + } + + foreach (Process proc in processes) + { + try + { + proc.Kill(); + Console.WriteLine(string.Format("成功终止进程: {0} (PID: {1})", name, proc.Id)); + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("无法终止进程: {0} (PID: {1}) - {2}", + name, proc.Id, ex.Message)); + } + } + } + + private static void StartProcess(string path, string args) + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = path, + Arguments = args, + UseShellExecute = true + }; + + Process proc = Process.Start(startInfo); + Console.WriteLine(string.Format("已启动进程: {0} (PID: {1})", path, proc.Id)); + } + + private static void ShowHelp() + { + string help = @" +进程管理工具使用说明 +================ + +基本语法: +process.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. list - 列出所有进程 + 示例: process.exe -type list + +2. kill - 终止进程 + 参数: + -target 要终止的进程ID或名称 + 示例: + process.exe -type kill -target notepad + process.exe -type kill -target 1234 + +3. start - 启动进程 + 参数: + -path <程序路径> 要启动的程序路径 + -args <参数> 启动参数(可选) + 示例: + process.exe -type start -path ""c:\windows\notepad.exe"" + process.exe -type start -path ""c:\program.exe"" -args ""-param value"" + +返回值: +------ +list操作返回JSON格式的进程信息: +{""id"": 进程ID, ""name"": ""进程名"", ""title"": ""窗口标题"", ""path"": ""进程路径"", +""startTime"": ""启动时间"", ""cpuTime"": ""CPU时间"", ""memory"": 内存使用量, +""threads"": 线程数, ""priority"": ""优先级"", ""description"": ""描述"", +""company"": ""公司"", ""version"": ""版本""} + +注意事项: +-------- +1. 终止进程可能需要管理员权限 +2. 进程名称不需要包含.exe后缀 +"; + Console.WriteLine(help); + } + + private static string GetArgumentValue(string[] args, string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } +} diff --git a/plugin/lib/csharp/registry.cs b/plugin/lib/csharp/registry.cs new file mode 100644 index 00000000..53c23274 --- /dev/null +++ b/plugin/lib/csharp/registry.cs @@ -0,0 +1,357 @@ +using System; +using System.Text; +using Microsoft.Win32; +using System.Collections.Generic; +using System.Web.Script.Serialization; + +public class RegistryManager +{ + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + Console.Error.WriteLine("Error: 必须指定操作类型 (-type)"); + return; + } + + try + { + string path = GetArgumentValue(args, "-path"); + string name = GetArgumentValue(args, "-name"); + string value = GetArgumentValue(args, "-value"); + string valueType = GetArgumentValue(args, "-valuetype"); + if (valueType == null) + { + valueType = "string"; + } + else + { + valueType = valueType.ToLower(); + } + + switch (type.ToLower()) + { + case "get": + if (string.IsNullOrEmpty(path)) + { + Console.Error.WriteLine("Error: 必须指定注册表路径 (-path)"); + return; + } + GetValue(path, name); + break; + + case "set": + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(name)) + { + Console.Error.WriteLine("Error: 必须指定注册表路径 (-path) 和键名 (-name)"); + return; + } + SetValue(path, name, value, valueType); + break; + + case "delete": + if (string.IsNullOrEmpty(path)) + { + Console.Error.WriteLine("Error: 必须指定注册表路径 (-path)"); + return; + } + DeleteValue(path, name); + break; + + case "list": + if (string.IsNullOrEmpty(path)) + { + Console.Error.WriteLine("Error: 必须指定注册表路径 (-path)"); + return; + } + ListKeys(path); + break; + + default: + Console.Error.WriteLine("Error: 不支持的操作类型"); + break; + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static void GetValue(string path, string name) + { + using (RegistryKey key = OpenRegistryKey(path)) + { + if (key != null) + { + object value = key.GetValue(name); + if (value != null) + { + var info = new Dictionary(); + info["path"] = path.Replace("\\", "\\\\"); + info["name"] = name; + info["value"] = value.ToString(); + info["type"] = key.GetValueKind(name).ToString(); + + var serializer = new JavaScriptSerializer(); + Console.Write(serializer.Serialize(info)); + return; + } + } + } + Console.Error.WriteLine("Error: 找不到指定的注册表值"); + } + + private static void SetValue(string path, string name, string value, string valueType) + { + using (RegistryKey key = OpenRegistryKey(path, true)) + { + if (key == null) + { + throw new Exception("找不到指定的注册表项"); + } + + object typedValue = ConvertValue(value, valueType); + RegistryValueKind kind = GetValueKind(valueType); + key.SetValue(name, typedValue, kind); + Console.WriteLine("成功设置注册表值"); + } + } + + private static void DeleteValue(string path, string name) + { + using (RegistryKey key = OpenRegistryKey(path, true)) + { + if (key == null) + { + throw new Exception("找不到指定的注册表项"); + } + + if (string.IsNullOrEmpty(name)) + { + // 删除整个键 + Registry.LocalMachine.DeleteSubKeyTree(GetRelativePath(path), false); + Console.WriteLine("成功删除注册表项"); + } + else + { + // 删除指定值 + key.DeleteValue(name, false); + Console.WriteLine("成功删除注册表值"); + } + } + } + + private static void ListKeys(string path) + { + var result = new List>(); + using (RegistryKey key = OpenRegistryKey(path)) + { + if (key != null) + { + foreach (string subKeyName in key.GetSubKeyNames()) + { + var info = new Dictionary(); + info["path"] = (path + "\\" + subKeyName).Replace("\\", "\\\\"); + info["name"] = subKeyName; + result.Add(info); + } + } + } + + var serializer = new JavaScriptSerializer(); + Console.Write(serializer.Serialize(result)); + } + + private static RegistryKey OpenRegistryKey(string path, bool writable = false) + { + string[] parts = path.Split('\\'); + if (parts.Length < 2) + { + throw new Exception("无效的注册表路径"); + } + + RegistryKey root = GetRootKey(parts[0]); + string subPath = string.Join("\\", parts, 1, parts.Length - 1); + return root.OpenSubKey(subPath, writable); + } + + private static RegistryKey GetRootKey(string name) + { + switch (name.ToUpper()) + { + case "HKLM": + case "HKEY_LOCAL_MACHINE": + return Registry.LocalMachine; + case "HKCU": + case "HKEY_CURRENT_USER": + return Registry.CurrentUser; + case "HKCR": + case "HKEY_CLASSES_ROOT": + return Registry.ClassesRoot; + case "HKU": + case "HKEY_USERS": + return Registry.Users; + case "HKCC": + case "HKEY_CURRENT_CONFIG": + return Registry.CurrentConfig; + default: + throw new Exception("无效的注册表根键"); + } + } + + private static string GetRelativePath(string path) + { + string[] parts = path.Split('\\'); + if (parts.Length < 2) + { + throw new Exception("无效的注册表路径"); + } + return string.Join("\\", parts, 1, parts.Length - 1); + } + + private static void OutputValue(string name, object value, RegistryValueKind kind) + { + string valueStr = FormatValue(value, kind); + Console.WriteLine(string.Format("{{\"type\": \"value\", \"name\": \"{0}\", \"value\": {1}, \"valueType\": \"{2}\"}}", + name.Replace("\"", "\\\""), + valueStr, + kind.ToString())); + } + + private static string FormatValue(object value, RegistryValueKind kind) + { + switch (kind) + { + case RegistryValueKind.String: + case RegistryValueKind.ExpandString: + return string.Format("\"{0}\"", value.ToString().Replace("\"", "\\\"")); + case RegistryValueKind.MultiString: + return string.Format("[{0}]", string.Join(",", Array.ConvertAll( + (string[])value, + s => string.Format("\"{0}\"", s.Replace("\"", "\\\""))))); + case RegistryValueKind.Binary: + return string.Format("[{0}]", string.Join(",", (byte[])value)); + default: + return value.ToString(); + } + } + + private static object ConvertValue(string value, string valueType) + { + switch (valueType) + { + case "string": + return value; + case "dword": + return int.Parse(value); + case "qword": + return long.Parse(value); + case "binary": + return Array.ConvertAll(value.Split(','), byte.Parse); + case "multistring": + return value.Split(','); + case "expandstring": + return value; + default: + throw new Exception("不支持的值类型"); + } + } + + private static RegistryValueKind GetValueKind(string valueType) + { + switch (valueType) + { + case "string": + return RegistryValueKind.String; + case "dword": + return RegistryValueKind.DWord; + case "qword": + return RegistryValueKind.QWord; + case "binary": + return RegistryValueKind.Binary; + case "multistring": + return RegistryValueKind.MultiString; + case "expandstring": + return RegistryValueKind.ExpandString; + default: + throw new Exception("不支持的值类型"); + } + } + + private static void ShowHelp() + { + string help = @" +Windows 注册表管理工具使用说明 +====================== + +基本语法: +registry.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. get - 获取注册表值 + 参数: + -path <注册表路径> 完整的注册表路径 + -name <值名称> 要获取的值名称(可选,不指定则列出所有值) + 示例: + registry.exe -type get -path ""HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion"" -name ""ProgramFilesDir"" + +2. set - 设置注册表值 + 参数: + -path <注册表路径> 完整的注册表路径 + -name <值名称> 要设置的值名称 + -value <值> 要设置的值 + -valuetype <类型> 值类型(可选,默认为string) + 支持的类型:string, dword, qword, binary, multistring, expandstring + 示例: + registry.exe -type set -path ""HKCU\Software\MyApp"" -name ""Setting"" -value ""123"" -valuetype dword + +3. delete - 删除注册表项或值 + 参数: + -path <注册表路径> 完整的注册表路径 + -name <值名称> 要删除的值名称(可选,不指定则删除整个键) + 示例: + registry.exe -type delete -path ""HKCU\Software\MyApp"" -name ""Setting"" + +4. list - 列出注册表项下的所有子项和值 + 参数: + -path <注册表路径> 完整的注册表路径 + 示例: + registry.exe -type list -path ""HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion"" + +返回值: +------ +JSON格式的注册表信息: +键:{""type"": ""key"", ""name"": ""键名""} +值:{""type"": ""value"", ""name"": ""值名"", ""value"": 值, ""valueType"": ""值类型""} + +注意事项: +-------- +1. 需要管理员权限才能修改系统关键注册表项 +2. 注册表路径必须以根键开头(HKLM、HKCU、HKCR、HKU、HKCC) +3. 修改注册表可能会影响系统稳定性,请谨慎操作 +4. 建议在修改前备份重要的注册表项 +"; + Console.WriteLine(help); + } + + private static string GetArgumentValue(string[] args, string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } +} diff --git a/plugin/lib/csharp/sendmessage.cs b/plugin/lib/csharp/sendmessage.cs new file mode 100644 index 00000000..484c110c --- /dev/null +++ b/plugin/lib/csharp/sendmessage.cs @@ -0,0 +1,919 @@ +using System; +using System.Windows.Forms; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Drawing; +using System.Collections.Generic; +using System.Linq; +using System.Web.Script.Serialization; + +public class AutomationTool +{ + #region Win32 API + [DllImport("user32.dll")] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll")] + private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); + + [DllImport("user32.dll")] + private static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam); + + [DllImport("user32.dll")] + private static extern bool SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam); + + [DllImport("user32.dll")] + private static extern bool SendMessage(IntPtr hWnd, uint Msg, int wParam, StringBuilder lParam); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern IntPtr GetFocus(); + + [DllImport("user32.dll")] + private static extern int EnumChildWindows(IntPtr hWnd, EnumWindowProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + + [DllImport("user32.dll")] + private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + private const int WM_KEYDOWN = 0x0100; + private const int WM_KEYUP = 0x0101; + private const int WM_CHAR = 0x0102; + private const int WM_SETTEXT = 0x000C; + private const int WM_LBUTTONDOWN = 0x0201; + private const int WM_LBUTTONUP = 0x0202; + private const int WM_RBUTTONDOWN = 0x0204; + private const int WM_RBUTTONUP = 0x0205; + private const int WM_LBUTTONDBLCLK = 0x0203; + + private delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam); + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int X; + public int Y; + } + + [DllImport("user32.dll")] + private static extern IntPtr GetParent(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern IntPtr GetAncestor(IntPtr hwnd, int flags); + + private const int GA_ROOT = 2; + + // 添加工具栏相关的常量和结构体 + private const uint TB_BUTTONCOUNT = 0x0418; + private const uint TB_GETBUTTON = 0x0417; + private const uint TB_GETBUTTONTEXT = 0x042D; + private const uint TB_GETITEMRECT = 0x041D; + + [StructLayout(LayoutKind.Sequential)] + private struct TBBUTTON + { + public int iBitmap; + public int idCommand; + public byte fsState; + public byte fsStyle; + public byte bReserved1; + public byte bReserved2; + public IntPtr dwData; + public IntPtr iString; + } + + // 添加 SendMessage 重载 + [DllImport("user32.dll")] + private static extern bool SendMessage(IntPtr hWnd, uint Msg, int wParam, ref TBBUTTON lParam); + + [DllImport("user32.dll")] + private static extern bool SendMessage(IntPtr hWnd, uint Msg, int wParam, ref RECT lParam); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKey(uint uCode, uint uMapType); + + [DllImport("user32.dll")] + private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); + + private const uint MAPVK_VK_TO_VSC = 0x00; + private const uint MAPVK_VSC_TO_VK = 0x01; + private const uint MAPVK_VK_TO_CHAR = 0x02; + private const uint MAPVK_VSC_TO_VK_EX = 0x03; + #endregion + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + try + { + List targetWindows = FindTargetWindows(args); + if (targetWindows.Count == 0) + { + throw new Exception("未找到目标窗口"); + } + + string type = GetArgumentValue(args, "-type"); + IntPtr targetHandle = targetWindows[0]; // 总是使用第一个窗口 + Dictionary operatedWindow = null; + + switch (type.ToLower()) + { + case "list": + // list 操作只获取第一个匹配窗口的控件树 + string filter = GetArgumentValue(args, "-filter"); + bool background = bool.Parse(GetArgumentValue(args, "-background") ?? "false"); + + // 只在非后台操作时激活窗口 + if (!background) + { + SetForegroundWindow(targetHandle); + Thread.Sleep(50); + } + + string treeJson = GetControlsTree(targetHandle, filter); + if (!string.IsNullOrEmpty(treeJson)) + { + Console.WriteLine("[" + treeJson + "]"); + } + return; // 直接返回,不输出窗口信息 + + case "keyboard": + HandleKeyboardOperation(targetHandle, args); + Console.WriteLine("true"); + break; + + case "mouse": + HandleMouseOperation(targetHandle, args); + Console.WriteLine("true"); + break; + + default: + throw new Exception("不支持的操作类型"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static List FindTargetWindows(string[] args) + { + List targetWindows = new List(); + string method = GetArgumentValue(args, "-method") ?? "title"; + string value = GetArgumentValue(args, "-window") ?? ""; + + // 如果是active方法,直接返回当前活动窗口 + if (method.ToLower() == "active") + { + targetWindows.Add(GetForegroundWindow()); + return targetWindows; + } + + // 如果是handle方法,直接返回指定句柄 + if (method.ToLower() == "handle") + { + IntPtr handle = new IntPtr(long.Parse(value)); + if (!IsWindow(handle)) + { + throw new Exception("指定的句柄不是一个有效的窗口句柄"); + } + targetWindows.Add(handle); + return targetWindows; + } + + // 如果没有指定窗口值,返回空列表 + if (string.IsNullOrEmpty(value)) + { + return targetWindows; + } + + switch (method.ToLower()) + { + case "process": + // 通过进程名查找 + var processes = System.Diagnostics.Process.GetProcessesByName(value); + foreach (var process in processes) + { + if (process.MainWindowHandle != IntPtr.Zero) + { + targetWindows.Add(process.MainWindowHandle); + } + } + break; + + case "class": + // 通过窗口类名查找 + EnumWindows((hwnd, param) => + { + if (!IsWindowVisible(hwnd)) + { + return true; + } + + StringBuilder className = new StringBuilder(256); + GetClassName(hwnd, className, className.Capacity); + string windowClassName = className.ToString(); + + if (windowClassName.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) + { + targetWindows.Add(hwnd); + } + return true; + }, IntPtr.Zero); + break; + + case "title": + default: + // 通过窗口标题查找(支持模糊匹配) + EnumWindows((hwnd, param) => + { + StringBuilder title = new StringBuilder(256); + GetWindowText(hwnd, title, title.Capacity); + string windowTitle = title.ToString(); + + if (!string.IsNullOrEmpty(windowTitle) && + windowTitle.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) + { + targetWindows.Add(hwnd); + } + return true; + }, IntPtr.Zero); + break; + } + + if (targetWindows.Count == 0) + { + Console.WriteLine(string.Format("Error: 未找到匹配的窗口 (method={0}, value={1})", method, value)); + } + + return targetWindows; + } + + private static void HandleKeyboardOperation(IntPtr targetHandle, string[] args) + { + string control = GetArgumentValue(args, "-control"); + string action = GetArgumentValue(args, "-action"); + string value = GetArgumentValue(args, "-value"); + bool background = bool.Parse(GetArgumentValue(args, "-background") ?? "false"); + + // 如果指定了控件,递归查找控件句柄 + IntPtr controlHandle = IntPtr.Zero; + if (!string.IsNullOrEmpty(control)) + { + StringBuilder windowTitle = new StringBuilder(256); + GetWindowText(targetHandle, windowTitle, windowTitle.Capacity); + + controlHandle = FindControl(targetHandle, control); + if (controlHandle == IntPtr.Zero) + { + throw new Exception(string.Format("在窗口中未找到指定控件 (窗口句柄={0}, 标题=\"{1}\", 控件类名=\"{2}\")", + targetHandle.ToInt64(), windowTitle.ToString(), control)); + } + targetHandle = controlHandle; + } + + // 只在非后台操作时激活窗口 + if (!background) + { + SetForegroundWindow(targetHandle); + Thread.Sleep(50); + } + + switch (action.ToLower()) + { + case "keys": + if (string.IsNullOrEmpty(value)) + { + throw new Exception("发送按键需要指定 -value 参数"); + } + SendKeys(targetHandle, value); + break; + + case "text": + if (string.IsNullOrEmpty(value)) + { + throw new Exception("发送文本需要指定 -value 参数"); + } + SendText(targetHandle, value); + break; + + default: + throw new Exception("不支持的keyboard操作类型"); + } + + // 返回操作结果 + return; + } + + private static void HandleMouseOperation(IntPtr targetHandle, string[] args) + { + string control = GetArgumentValue(args, "-control"); + string controlText = GetArgumentValue(args, "-text"); + string action = GetArgumentValue(args, "-action"); + string position = GetArgumentValue(args, "-pos"); + bool background = bool.Parse(GetArgumentValue(args, "-background") ?? "false"); + + if (string.IsNullOrEmpty(action)) + { + throw new Exception("mouse操作需要指定 -action 参数"); + } + + // 如果指定了控件类名和文本,查找匹配的控件 + IntPtr controlHandle = IntPtr.Zero; + if (!string.IsNullOrEmpty(control) || !string.IsNullOrEmpty(controlText)) + { + StringBuilder windowTitle = new StringBuilder(256); + GetWindowText(targetHandle, windowTitle, windowTitle.Capacity); + + controlHandle = FindControlByTextAndClass(targetHandle, controlText, control); + if (controlHandle == IntPtr.Zero) + { + throw new Exception(string.Format("在窗口中未找到指定控件 (窗口句柄={0}, 标题=\"{1}\", 控件类名=\"{2}\", 控件文本=\"{3}\")", + targetHandle.ToInt64(), windowTitle.ToString(), control ?? "", controlText ?? "")); + } + targetHandle = controlHandle; + } + + // 只在非后台操作时激活窗口 + if (!background) + { + SetForegroundWindow(targetHandle); + Thread.Sleep(50); + } + + // 获取点击坐标 + int x = 0, y = 0; + if (!string.IsNullOrEmpty(position)) + { + // 使用指定坐标 + string[] pos = position.Split(','); + if (pos.Length == 2) + { + x = int.Parse(pos[0]); + y = int.Parse(pos[1]); + } + } + else + { + // 如果没有指定坐标,点击控件中心 + RECT rect; + if (GetWindowRect(targetHandle, out rect)) + { + x = (rect.Right - rect.Left) / 2; + y = (rect.Bottom - rect.Top) / 2; + } + } + + int lParam = (y << 16) | (x & 0xFFFF); + + switch (action.ToLower()) + { + case "click": + SendMessage(targetHandle, WM_LBUTTONDOWN, 0, lParam); + SendMessage(targetHandle, WM_LBUTTONUP, 0, lParam); + break; + + case "rightclick": + SendMessage(targetHandle, WM_RBUTTONDOWN, 0, lParam); + SendMessage(targetHandle, WM_RBUTTONUP, 0, lParam); + break; + + case "doubleclick": + SendMessage(targetHandle, WM_LBUTTONDOWN, 0, lParam); + SendMessage(targetHandle, WM_LBUTTONUP, 0, lParam); + SendMessage(targetHandle, WM_LBUTTONDBLCLK, 0, lParam); + SendMessage(targetHandle, WM_LBUTTONUP, 0, lParam); + break; + + default: + Console.WriteLine("Error: 不支持的mouse操作类型"); + break; + } + + // 返回操作结果 + return; + } + + private static void SendKeys(IntPtr hWnd, string keys) + { + // 解析按键组合 + string[] keyArray = keys.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string keyCombo in keyArray) + { + string[] modifiers = keyCombo.Trim().Split('+'); + List modifierKeys = new List(); + byte mainKey = 0; + + // 处理每个按键 + for (int i = 0; i < modifiers.Length; i++) + { + byte vKey = GetVirtualKeyCode(modifiers[i].Trim()); + if (i < modifiers.Length - 1) + { + modifierKeys.Add(vKey); + } + else + { + mainKey = vKey; + } + } + + // 按下修饰键 + foreach (byte modifier in modifierKeys) + { + // 获取扫描码 + uint scanCode = MapVirtualKey((uint)modifier, MAPVK_VK_TO_VSC); + + // 构造 lParam + int lParamDown = 0x00000001 | // repeat count = 1 + ((int)scanCode << 16) | // scan code + (0x1 << 24); // extended key for modifiers + + PostMessage(hWnd, WM_KEYDOWN, modifier, lParamDown); + Thread.Sleep(10); // 短暂延迟确保修饰键被正确识别 + } + + // 发送主键 + if (mainKey > 0) + { + // 获取主键的扫描码 + uint scanCode = MapVirtualKey((uint)mainKey, MAPVK_VK_TO_VSC); + + // 构造主键的 lParam + int lParamDown = 0x00000001 | // repeat count = 1 + ((int)scanCode << 16); // scan code + + int lParamUp = 0x00000001 | // repeat count = 1 + ((int)scanCode << 16) | // scan code + (0xC0 << 24); // key up + previous key state + + // 发送按键按下 + PostMessage(hWnd, WM_KEYDOWN, mainKey, lParamDown); + Thread.Sleep(10); // 短暂延迟 + + // 发送按键释放 + PostMessage(hWnd, WM_KEYUP, mainKey, lParamUp); + Thread.Sleep(10); // 短暂延迟 + } + + // 释放修饰键(反序释放) + for (int i = modifierKeys.Count - 1; i >= 0; i--) + { + byte modifier = modifierKeys[i]; + uint scanCode = MapVirtualKey((uint)modifier, MAPVK_VK_TO_VSC); + + // 构造释放修饰键的 lParam + int lParamUp = 0x00000001 | // repeat count = 1 + ((int)scanCode << 16) | // scan code + (0xC1 << 24); // extended key + key up + previous key state + + PostMessage(hWnd, WM_KEYUP, modifier, lParamUp); + Thread.Sleep(10); // 短暂延迟 + } + + // 如果有多个按键组合,等待一下 + if (keyArray.Length > 1) + { + Thread.Sleep(50); + } + } + } + + private static void SendText(IntPtr hWnd, string text) + { + StringBuilder sb = new StringBuilder(text); + SendMessage(hWnd, WM_SETTEXT, 0, sb); + } + + private static string GetArgumentValue(string[] args, string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } + + private static byte GetVirtualKeyCode(string key) + { + switch (key.ToUpper()) + { + // 修饰键 + case "CTRL": + case "^": return 0x11; // VK_CONTROL + case "ALT": + case "%": return 0x12; // VK_MENU + case "SHIFT": return 0x10; // VK_SHIFT + + // 特殊按键 + case "{BACKSPACE}": + case "{BS}": + case "{BKSP}": return 0x08; // VK_BACK + case "{BREAK}": return 0x03; // VK_CANCEL + case "{CAPSLOCK}": return 0x14; // VK_CAPITAL + case "{DELETE}": + case "{DEL}": return 0x2E; // VK_DELETE + case "{DOWN}": return 0x28; // VK_DOWN + case "{END}": return 0x23; // VK_END + case "{ENTER}": + case "{RETURN}": return 0x0D; // VK_RETURN + case "{ESC}": return 0x1B; // VK_ESCAPE + case "{HELP}": return 0x2F; // VK_HELP + case "{HOME}": return 0x24; // VK_HOME + case "{INSERT}": + case "{INS}": return 0x2D; // VK_INSERT + case "{LEFT}": return 0x25; // VK_LEFT + case "{NUMLOCK}": return 0x90; // VK_NUMLOCK + case "{PGDN}": return 0x22; // VK_NEXT + case "{PGUP}": return 0x21; // VK_PRIOR + case "{PRTSC}": return 0x2C; // VK_SNAPSHOT + case "{RIGHT}": return 0x27; // VK_RIGHT + case "{SCROLLLOCK}": return 0x91; // VK_SCROLL + case "{TAB}": return 0x09; // VK_TAB + case "{UP}": return 0x26; // VK_UP + + // 功能键 F1-F16 + case "{F1}": return 0x70; + case "{F2}": return 0x71; + case "{F3}": return 0x72; + case "{F4}": return 0x73; + case "{F5}": return 0x74; + case "{F6}": return 0x75; + case "{F7}": return 0x76; + case "{F8}": return 0x77; + case "{F9}": return 0x78; + case "{F10}": return 0x79; + case "{F11}": return 0x7A; + case "{F12}": return 0x7B; + + // 数字键盘 + case "{ADD}": return 0x6B; // VK_ADD + case "{SUBTRACT}": return 0x6D; // VK_SUBTRACT + case "{MULTIPLY}": return 0x6A; // VK_MULTIPLY + case "{DIVIDE}": return 0x6F; // VK_DIVIDE + case "{NUMPAD0}": return 0x60; // VK_NUMPAD0 + case "{NUMPAD1}": return 0x61; + case "{NUMPAD2}": return 0x62; + case "{NUMPAD3}": return 0x63; + case "{NUMPAD4}": return 0x64; + case "{NUMPAD5}": return 0x65; + case "{NUMPAD6}": return 0x66; + case "{NUMPAD7}": return 0x67; + case "{NUMPAD8}": return 0x68; + case "{NUMPAD9}": return 0x69; + + default: + if (key.Length == 1) + { + return (byte)key.ToUpper()[0]; + } + throw new ArgumentException(string.Format("不支持的按键: {0}", key)); + } + } + + private static IntPtr FindControlRecursive(IntPtr parentHandle, string targetClassName) + { + if (string.IsNullOrEmpty(targetClassName)) + return IntPtr.Zero; + + IntPtr foundHandle = IntPtr.Zero; + List childHandles = new List(); + + // 枚举所有子窗口 + EnumWindowProc childProc = new EnumWindowProc((handle, param) => + { + // 获取类名 + StringBuilder classNameBuffer = new StringBuilder(256); + GetClassName(handle, classNameBuffer, classNameBuffer.Capacity); + + // 检查是否匹配 + if (classNameBuffer.ToString().Equals(targetClassName, StringComparison.OrdinalIgnoreCase)) + { + foundHandle = handle; + return false; // 找到后停止枚举 + } + + childHandles.Add(handle); + return true; + }); + + EnumChildWindows(parentHandle, childProc, IntPtr.Zero); + + // 如果在当前层级没找到,递归查找子窗口 + if (foundHandle == IntPtr.Zero) + { + foreach (IntPtr childHandle in childHandles) + { + foundHandle = FindControlRecursive(childHandle, targetClassName); + if (foundHandle != IntPtr.Zero) + break; + } + } + + return foundHandle; + } + + // 修改 HandleKeyboardOperation 和 HandleMouseOperation 中查找控件的部分 + private static IntPtr FindControl(IntPtr parentHandle, string controlClass) + { + // 先尝试直接查找 + IntPtr hControl = FindWindowEx(parentHandle, IntPtr.Zero, controlClass, null); + if (hControl != IntPtr.Zero) + return hControl; + + // 如果直接查找失败,进行递归查找 + return FindControlRecursive(parentHandle, controlClass); + } + + private static IntPtr FindControlByTextAndClass(IntPtr parentHandle, string controlText, string className) + { + if (string.IsNullOrEmpty(controlText) && string.IsNullOrEmpty(className)) + return IntPtr.Zero; + + List matchedControls = new List(); + Queue searchQueue = new Queue(); + searchQueue.Enqueue(parentHandle); + + while (searchQueue.Count > 0) + { + IntPtr currentHandle = searchQueue.Dequeue(); + List children = new List(); + + // 枚举当前层级的子窗口 + EnumWindowProc childProc = new EnumWindowProc((handle, param) => + { + bool match = true; + + // 检查类名(如果指定) + if (!string.IsNullOrEmpty(className)) + { + StringBuilder classNameBuffer = new StringBuilder(256); + GetClassName(handle, classNameBuffer, classNameBuffer.Capacity); + if (!className.Equals(classNameBuffer.ToString(), StringComparison.OrdinalIgnoreCase)) + { + match = false; + } + } + + // 检查控件文本(如果指定) + if (match && !string.IsNullOrEmpty(controlText)) + { + StringBuilder textBuffer = new StringBuilder(256); + GetWindowText(handle, textBuffer, textBuffer.Capacity); + string windowText = textBuffer.ToString(); + if (!windowText.Contains(controlText)) + { + match = false; + } + } + + // 检查控件是否可见 + if (match && !IsWindowVisible(handle)) + { + match = false; + } + + if (match) + { + matchedControls.Add(handle); + } + + // 将子窗口加入搜索队列 + children.Add(handle); + return true; + }); + + EnumChildWindows(currentHandle, childProc, IntPtr.Zero); + + // 将所有子窗口加入搜索队列 + foreach (IntPtr child in children) + { + searchQueue.Enqueue(child); + } + } + + if (matchedControls.Count == 0) + { + return IntPtr.Zero; + } + else if (matchedControls.Count > 1) + { + foreach (IntPtr handle in matchedControls) + { + StringBuilder text = new StringBuilder(256); + GetWindowText(handle, text, text.Capacity); + StringBuilder classBuffer = new StringBuilder(256); + GetClassName(handle, classBuffer, classBuffer.Capacity); + Console.WriteLine(string.Format("0x{0:X} - Class: {1}, Text: {2}", + handle.ToInt64(), classBuffer, text)); + } + } + + return matchedControls[0]; + } + + private static string GetControlsTree(IntPtr parentHandle, string filter, int depth = 0) + { + if (parentHandle == IntPtr.Zero) + return "{}"; + + StringBuilder json = new StringBuilder(); + json.Append("{"); + + // 获取窗口信息 + StringBuilder title = new StringBuilder(256); + StringBuilder className = new StringBuilder(256); + GetWindowText(parentHandle, title, title.Capacity); + GetClassName(parentHandle, className, className.Capacity); + + // 获取窗口位置和大小 + RECT windowRect; + GetWindowRect(parentHandle, out windowRect); + + bool isVisible = IsWindowVisible(parentHandle); + + // 检查当前节点是否匹配过滤条件 + bool matchFilter = true; + if (!string.IsNullOrEmpty(filter)) + { + matchFilter = + title.ToString().IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0 || + className.ToString().IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; + } + + // 添加节点信息 - 将句柄改为十进制格式 + json.AppendFormat( + "\"handle\":\"{0}\",\"class\":\"{1}\",\"text\":\"{2}\",\"visible\":{3},\"location\":{{\"x\":{4},\"y\":{5},\"width\":{6},\"height\":{7}}},\"matched\":{8},\"children\":[", + parentHandle.ToInt64(), // 直接使用十进制格式 + className.ToString().Replace("\"", "\\\""), + title.ToString().Replace("\"", "\\\""), + isVisible.ToString().ToLower(), + windowRect.Left, + windowRect.Top, + windowRect.Right - windowRect.Left, + windowRect.Bottom - windowRect.Top, + matchFilter.ToString().ToLower() + ); + + // 递归获取子控件树 + List childJsons = new List(); + + EnumChildWindows(parentHandle, (hwnd, param) => + { + string childJson = GetControlsTree(hwnd, filter, depth + 1); + if (!string.IsNullOrEmpty(childJson) && childJson != "{}") + { + childJsons.Add(childJson); + } + return true; + }, IntPtr.Zero); + + // 添加子节点JSON + if (childJsons.Count > 0) + { + json.Append(string.Join(",", childJsons)); + } + + json.Append("]}"); + + return json.ToString(); + } + + private static void ShowHelp() + { + string help = @" +Windows 界面自动化工具使用说明 +========================== + +基本语法: +sendmessage.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. keyboard - 键盘操作 +2. mouse - 鼠标操作 +3. list - 获取控件树 + +通用参数: +-------- +-method 窗口查找方式(可选,默认title) + 可选值: + - title 窗口标题(支持模糊匹配) + - handle 窗口句柄 + - active 当前活动窗口 + - process 进程名 + - class 窗口类名(支持模糊匹配) + +-window 要查找的窗口值(根据method解释) +-control 控件类名 +-background 后台操作,不激活窗口,默认激活 + +键盘操作参数: +----------- +-action 操作类型:keys(按键)或text(文本) +-value 要发送的按键或文本内容 + +鼠标操作参数: +----------- +-action 操作类型:click(单击)、doubleclick(双击)、rightclick(右键) +-text 控件文本 +-pos 点击坐标(x,y) + +控件树参数: +--------- +-filter 过滤条件(控件类名或文本) + +使用示例: +-------- +1. 发送按键到指定窗口: + sendmessage.exe -type keyboard -action keys -window ""记事本"" -value ""ctrl+a"" + +2. 发送文本到指定控件: + sendmessage.exe -type keyboard -action text -window ""记事本"" -control ""Edit"" -value ""Hello World"" + +3. 点击指定控件: + sendmessage.exe -type mouse -action click -window ""记事本"" -control ""Button"" -text ""确定"" + +4. 后台发送文本: + sendmessage.exe -type keyboard -action text -window ""记事本"" -value ""Hello"" -background + +5. 获取窗口控件树: + sendmessage.exe -type list -window ""记事本"" -filter ""button"" + +6. 使用句柄查找窗口: + sendmessage.exe -type keyboard -method handle -window ""0x12345"" -value ""Hello"" + +7. 操作当前活动窗口: + sendmessage.exe -type keyboard -method active -value ""ctrl+s"" + +8. 通过进程名查找窗口: + sendmessage.exe -type keyboard -method process -window ""notepad"" -value ""Hello"" + +9. 通过窗口类名查找: + sendmessage.exe -type keyboard -method class -window ""Chrome"" -value ""Hello"" # 会匹配 Chrome_WidgetWin_1 + sendmessage.exe -type keyboard -method class -window ""Chrome_WidgetWin_1"" -value ""Hello"" # 精确匹配 + +返回值: +------ +1. 均为JSON格式 +2. list操作返回控件树信息 +3. 其他操作返回操作的控件信息及其所在窗口信息 +4. 失败均抛出异常 + +注意事项: +-------- +1. 窗口标题、类名支持模糊匹配,active方式可不提供window参数 +2. 所有操作都只会处理第一个匹配的窗口 +"; + Console.WriteLine(help); + } +} diff --git a/plugin/lib/csharp/service.cs b/plugin/lib/csharp/service.cs new file mode 100644 index 00000000..07cffe87 --- /dev/null +++ b/plugin/lib/csharp/service.cs @@ -0,0 +1,233 @@ +using System; +using System.ServiceProcess; +using System.Runtime.InteropServices; +using System.Text; + +public class ServiceManager +{ + [DllImport("advapi32.dll", SetLastError = true)] + private static extern IntPtr OpenSCManager(string lpMachineName, string lpDatabaseName, uint dwDesiredAccess); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool StartService(IntPtr hService, uint dwNumServiceArgs, string[] lpServiceArgVectors); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool ControlService(IntPtr hService, uint dwControl, ref SERVICE_STATUS lpServiceStatus); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool CloseServiceHandle(IntPtr hSCObject); + + private const uint SC_MANAGER_ALL_ACCESS = 0xF003F; + private const uint SERVICE_ALL_ACCESS = 0xF01FF; + private const uint SERVICE_CONTROL_STOP = 0x00000001; + private const uint SERVICE_CONTROL_PAUSE = 0x00000002; + private const uint SERVICE_CONTROL_CONTINUE = 0x00000003; + + [StructLayout(LayoutKind.Sequential)] + private struct SERVICE_STATUS + { + public uint dwServiceType; + public uint dwCurrentState; + public uint dwControlsAccepted; + public uint dwWin32ExitCode; + public uint dwServiceSpecificExitCode; + public uint dwCheckPoint; + public uint dwWaitHint; + } + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + Console.Error.WriteLine("Error: 必须指定操作类型 (-type)"); + return; + } + + try + { + switch (type.ToLower()) + { + case "list": + ListServices(); + break; + case "start": + case "stop": + case "pause": + case "continue": + string name = GetArgumentValue(args, "-name"); + if (string.IsNullOrEmpty(name)) + { + Console.Error.WriteLine("Error: 必须指定服务名称 (-name)"); + return; + } + ControlServiceByName(name, type); + break; + default: + Console.Error.WriteLine("Error: 不支持的操作类型"); + break; + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static void ListServices() + { + Console.Write("["); + bool first = true; + ServiceController[] services = ServiceController.GetServices(); + foreach (ServiceController service in services) + { + if (!first) + { + Console.Write(","); + } + first = false; + Console.Write(string.Format("{{\"name\": \"{0}\", \"displayName\": \"{1}\", \"status\": \"{2}\"}}", + service.ServiceName, + service.DisplayName.Replace("\"", "\\\""), + service.Status)); + } + Console.Write("]"); + } + + private static void ControlServiceByName(string serviceName, string operation) + { + IntPtr scm = OpenSCManager(null, null, SC_MANAGER_ALL_ACCESS); + if (scm == IntPtr.Zero) + { + throw new Exception("无法打开服务控制管理器"); + } + + try + { + IntPtr service = OpenService(scm, serviceName, SERVICE_ALL_ACCESS); + if (service == IntPtr.Zero) + { + throw new Exception("无法打开服务"); + } + + try + { + SERVICE_STATUS status = new SERVICE_STATUS(); + bool success = false; + + switch (operation.ToLower()) + { + case "start": + success = StartService(service, 0, null); + break; + case "stop": + success = ControlService(service, SERVICE_CONTROL_STOP, ref status); + break; + case "pause": + success = ControlService(service, SERVICE_CONTROL_PAUSE, ref status); + break; + case "continue": + success = ControlService(service, SERVICE_CONTROL_CONTINUE, ref status); + break; + } + + if (success) + { + Console.WriteLine(string.Format("成功{0}服务: {1}", GetOperationName(operation), serviceName)); + } + else + { + throw new Exception(string.Format("无法{0}服务", GetOperationName(operation))); + } + } + finally + { + CloseServiceHandle(service); + } + } + finally + { + CloseServiceHandle(scm); + } + } + + private static string GetOperationName(string operation) + { + switch (operation.ToLower()) + { + case "start": return "启动"; + case "stop": return "停止"; + case "pause": return "暂停"; + case "continue": return "继续"; + default: return operation; + } + } + + private static void ShowHelp() + { + string help = @" +Windows 服务管理工具使用说明 +====================== + +基本语法: +service.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. list - 列出所有服务 + 示例: service.exe -type list + +2. start - 启动服务 + 参数: + -name <服务名> 服务名称 + 示例: service.exe -type start -name Spooler + +3. stop - 停止服务 + 参数: + -name <服务名> 服务名称 + 示例: service.exe -type stop -name Spooler + +4. pause - 暂停服务 + 参数: + -name <服务名> 服务名称 + 示例: service.exe -type pause -name Spooler + +5. continue - 继续服务 + 参数: + -name <服务名> 服务名称 + 示例: service.exe -type continue -name Spooler + +返回值: +------ +list操作返回JSON格式的服务信息: +{""name"": ""服务名"", ""displayName"": ""显示名称"", ""status"": ""状态""} + +注意事项: +-------- +1. 需要管理员权限 +2. 并非所有服务都支持暂停/继续操作 +"; + Console.WriteLine(help); + } + + private static string GetArgumentValue(string[] args, string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } +} diff --git a/plugin/lib/csharp/software.cs b/plugin/lib/csharp/software.cs new file mode 100644 index 00000000..0b964db9 --- /dev/null +++ b/plugin/lib/csharp/software.cs @@ -0,0 +1,352 @@ +using System; +using System.Text; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using System.Collections.Generic; +using System.Diagnostics; +using System.Web.Script.Serialization; +using System.Linq; + +public class SoftwareManager +{ + [DllImport("msi.dll", CharSet = CharSet.Unicode)] + private static extern uint MsiEnumProducts(uint iProductIndex, StringBuilder lpProductBuf); + + [DllImport("msi.dll", CharSet = CharSet.Unicode)] + private static extern uint MsiGetProductInfo(string szProduct, string szProperty, StringBuilder lpValueBuf, ref uint pcchValueBuf); + + [DllImport("msi.dll", CharSet = CharSet.Unicode)] + private static extern uint MsiConfigureProduct(string szProduct, int iInstallLevel, int eInstallState); + + private const int INSTALLSTATE_DEFAULT = -1; + private const int INSTALLSTATE_ABSENT = 2; + private const int INSTALLLEVEL_DEFAULT = 0; + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + Console.Error.WriteLine("Error: 必须指定操作类型 (-type)"); + return; + } + + try + { + switch (type.ToLower()) + { + case "list": + ListSoftware(); + break; + case "uninstall": + string target = GetArgumentValue(args, "-target"); + if (string.IsNullOrEmpty(target)) + { + Console.Error.WriteLine("Error: 必须指定目标软件 (-target)"); + return; + } + UninstallSoftware(target); + break; + case "repair": + string product = GetArgumentValue(args, "-target"); + if (string.IsNullOrEmpty(product)) + { + Console.Error.WriteLine("Error: 必须指定目标软件 (-target)"); + return; + } + RepairSoftware(product); + break; + default: + Console.Error.WriteLine("Error: 不支持的操作类型"); + break; + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static void ListSoftware() + { + Console.Write("["); + bool first = true; + + // 列出MSI安装的软件 + uint index = 0; + StringBuilder productCode = new StringBuilder(39); + while (MsiEnumProducts(index++, productCode) == 0) + { + uint charCount = 128; + StringBuilder displayName = new StringBuilder((int)charCount); + StringBuilder publisher = new StringBuilder((int)charCount); + StringBuilder version = new StringBuilder((int)charCount); + + MsiGetProductInfo(productCode.ToString(), "ProductName", displayName, ref charCount); + charCount = 128; + MsiGetProductInfo(productCode.ToString(), "Publisher", publisher, ref charCount); + charCount = 128; + MsiGetProductInfo(productCode.ToString(), "VersionString", version, ref charCount); + + if (!first) + { + Console.Write(","); + } + first = false; + Console.Write(string.Format("{{\"name\": \"{0}\", \"publisher\": \"{1}\", \"version\": \"{2}\", \"source\": \"{3}\", \"id\": \"{4}\"}}", + displayName.ToString().Replace("\"", "\\\""), + publisher.ToString().Replace("\"", "\\\""), + version.ToString().Replace("\"", "\\\""), + "MSI", + productCode.ToString().Replace("\\", "\\\\").Replace("\"", "\\\""))); + } + + // 列出注册表中的软件 + string[] registryPaths = { + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + }; + + foreach (string registryPath in registryPaths) + { + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath)) + { + if (key != null) + { + foreach (string subKeyName in key.GetSubKeyNames()) + { + using (RegistryKey subKey = key.OpenSubKey(subKeyName)) + { + if (subKey != null) + { + string displayName = subKey.GetValue("DisplayName") as string; + if (!string.IsNullOrEmpty(displayName)) + { + string publisher = subKey.GetValue("Publisher") as string; + if (publisher == null) publisher = ""; + string version = subKey.GetValue("DisplayVersion") as string; + if (version == null) version = ""; + string uninstallString = subKey.GetValue("UninstallString") as string; + if (uninstallString == null) uninstallString = ""; + + if (!first) + { + Console.Write(","); + } + first = false; + Console.Write(string.Format("{{\"name\": \"{0}\", \"publisher\": \"{1}\", \"version\": \"{2}\", \"source\": \"{3}\", \"id\": \"{4}\"}}", + displayName.Replace("\"", "\\\""), + publisher.Replace("\"", "\\\""), + version.Replace("\"", "\\\""), + "Registry", + uninstallString.Replace("\\", "\\\\").Replace("\"", "\\\""))); + } + } + } + } + } + } + } + Console.Write("]"); + } + + private static void OutputSoftwareInfo(string name, string publisher, string version, string source, string id) + { + Console.WriteLine(string.Format("{{\"name\": \"{0}\", \"publisher\": \"{1}\", \"version\": \"{2}\", \"source\": \"{3}\", \"id\": \"{4}\"}}", + name.Replace("\"", "\\\""), + publisher.Replace("\"", "\\\""), + version.Replace("\"", "\\\""), + source, + id.Replace("\"", "\\\""))); + } + + private static void UninstallSoftware(string target) + { + // 尝试通过MSI卸载 + if (target.Length == 38 && target.StartsWith("{") && target.EndsWith("}")) + { + uint result = MsiConfigureProduct(target, INSTALLLEVEL_DEFAULT, INSTALLSTATE_ABSENT); + if (result == 0) + { + Console.WriteLine("成功启动卸载程序"); + return; + } + } + + // 尝试通过注册表卸载字符串卸载 + string[] registryPaths = { + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + }; + + foreach (string registryPath in registryPaths) + { + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath)) + { + if (key != null) + { + foreach (string subKeyName in key.GetSubKeyNames()) + { + using (RegistryKey subKey = key.OpenSubKey(subKeyName)) + { + if (subKey != null) + { + string displayName = subKey.GetValue("DisplayName") as string; + if (displayName != null && displayName.Contains(target)) + { + string uninstallString = subKey.GetValue("UninstallString") as string; + if (!string.IsNullOrEmpty(uninstallString)) + { + // 启动卸载程序 + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = "/c " + uninstallString, + UseShellExecute = true + }; + Process.Start(startInfo); + Console.WriteLine("成功启动卸载程序"); + return; + } + } + } + } + } + } + } + } + + throw new Exception("找不到指定的软件或无法卸载"); + } + + private static void RepairSoftware(string productCode) + { + if (productCode.Length != 38 || !productCode.StartsWith("{") || !productCode.EndsWith("}")) + { + throw new Exception("无效的产品代码"); + } + + uint result = MsiConfigureProduct(productCode, INSTALLLEVEL_DEFAULT, INSTALLSTATE_DEFAULT); + if (result == 0) + { + Console.WriteLine("成功启动修复程序"); + } + else + { + throw new Exception("无法修复软件"); + } + } + + private static void ShowHelp() + { + string help = @" +Windows 软件管理工具使用说明 +====================== + +基本语法: +software.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. list - 列出已安装的软件 + 示例: software.exe -type list + +2. uninstall - 卸载软件 + 参数: + -target <软件名称或ID> 要卸载的软件名称或产品代码 + 示例: + software.exe -type uninstall -target ""Microsoft Office"" + software.exe -type uninstall -target ""{12345678-1234-1234-1234-123456789012}"" + +3. repair - 修复MSI安装的软件 + 参数: + -target <产品代码> 要修复的软件的产品代码 + 示例: software.exe -type repair -target ""{12345678-1234-1234-1234-123456789012}"" + +返回值: +------ +list操作返回JSON格式的软件信息: +{""name"": ""软件名称"", ""publisher"": ""发布者"", ""version"": ""版本"", ""source"": ""来源"", ""id"": ""标识符""} + +注意事项: +-------- +1. 需要管理员权限 +2. 卸载和修复操作可能需要用户确认 +3. 并非所有软件都支持修复功能 +4. 卸载操作会启动软件自带的卸载程序 +"; + Console.WriteLine(help); + } + + private static string GetArgumentValue(string[] args, string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } + + private static string GetSoftwareInfo() + { + var result = new List>(); + string uninstallKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; + string uninstallKey32 = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; + + // 获取64位和32位软件信息 + GetSoftwareFromRegistry(Registry.LocalMachine, uninstallKey, result); + GetSoftwareFromRegistry(Registry.LocalMachine, uninstallKey32, result); + + var serializer = new JavaScriptSerializer(); + return serializer.Serialize(result.Select(info => new { + id = info["id"], + name = info["name"], + version = info["version"], + publisher = info["publisher"], + installLocation = info.ContainsKey("installLocation") && info["installLocation"] != null ? + info["installLocation"].Replace("\\", "\\\\") : null, + uninstallString = info.ContainsKey("uninstallString") && info["uninstallString"] != null ? + info["uninstallString"].Replace("\\", "\\\\") : null + })); + } + + private static void GetSoftwareFromRegistry(RegistryKey root, string keyPath, List> result) + { + using (RegistryKey key = root.OpenSubKey(keyPath)) + { + if (key != null) + { + foreach (string subKeyName in key.GetSubKeyNames()) + { + using (RegistryKey subKey = key.OpenSubKey(subKeyName)) + { + if (subKey != null) + { + string displayName = subKey.GetValue("DisplayName") as string; + if (!string.IsNullOrEmpty(displayName)) + { + var info = new Dictionary(); + info["id"] = subKeyName; + info["name"] = displayName; + info["version"] = subKey.GetValue("DisplayVersion") as string ?? ""; + info["publisher"] = subKey.GetValue("Publisher") as string ?? ""; + info["installLocation"] = subKey.GetValue("InstallLocation") as string; + info["uninstallString"] = subKey.GetValue("UninstallString") as string; + result.Add(info); + } + } + } + } + } + } + } +} diff --git a/plugin/lib/csharp/utils.cs b/plugin/lib/csharp/utils.cs new file mode 100644 index 00000000..1002e6ee --- /dev/null +++ b/plugin/lib/csharp/utils.cs @@ -0,0 +1,497 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Net.NetworkInformation; +using System.Diagnostics; +using System.IO; +using Microsoft.Win32; +using System.Drawing; +using System.Drawing.Imaging; + +public class SystemUtils +{ + #region Win32 API + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); + + [DllImport("user32.dll")] + private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("PowrProf.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); + + [DllImport("kernel32.dll")] + private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); + + [DllImport("user32.dll")] + static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + static extern IntPtr GetWindowDC(IntPtr hWnd); + + [DllImport("gdi32.dll")] + static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, + int nWidth, int nHeight, IntPtr hdcSrc, + int nXSrc, int nYSrc, int dwRop); + + [DllImport("gdi32.dll")] + static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth, int nHeight); + + [DllImport("gdi32.dll")] + static extern IntPtr CreateCompatibleDC(IntPtr hDC); + + [DllImport("gdi32.dll")] + static extern bool DeleteDC(IntPtr hDC); + + [DllImport("gdi32.dll")] + static extern bool DeleteObject(IntPtr hObject); + + [DllImport("gdi32.dll")] + static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [DllImport("user32.dll")] + private static extern int GetWindowRect(IntPtr hWnd, ref RECT rect); + + [DllImport("user32.dll")] + private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC); + + private const int SPI_SETDESKWALLPAPER = 20; + private const int SPIF_UPDATEINIFILE = 0x01; + private const int SPIF_SENDCHANGE = 0x02; + private const int WM_SYSCOMMAND = 0x0112; + private const int SC_MONITORPOWER = 0xF170; + + [Flags] + private enum EXECUTION_STATE : uint + { + ES_AWAYMODE_REQUIRED = 0x00000040, + ES_CONTINUOUS = 0x80000000, + ES_DISPLAY_REQUIRED = 0x00000002, + ES_SYSTEM_REQUIRED = 0x00000001 + } + #endregion + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + string type = GetArgumentValue(args, "-type"); + if (string.IsNullOrEmpty(type)) + { + Console.Error.WriteLine("Error: 必须指定操作类型 (-type)"); + return; + } + + try + { + switch (type.ToLower()) + { + case "wallpaper": + string wallpaperPath = GetArgumentValue(args, "-path"); + if (string.IsNullOrEmpty(wallpaperPath)) + { + Console.Error.WriteLine("Error: 必须指定壁纸路径 (-path)"); + return; + } + SetWallpaper(wallpaperPath); + break; + + case "monitor": + string action = GetArgumentValue(args, "-action"); + if (string.IsNullOrEmpty(action)) + { + Console.Error.WriteLine("Error: 必须指定动作 (-action)"); + return; + } + ControlMonitor(action); + break; + + case "power": + string mode = GetArgumentValue(args, "-mode"); + if (string.IsNullOrEmpty(mode)) + { + Console.Error.WriteLine("Error: 必须指定电源模式 (-mode)"); + return; + } + PowerControl(mode); + break; + + case "network": + string interfaceName = GetArgumentValue(args, "-interface"); + string ip = GetArgumentValue(args, "-ip"); + string mask = GetArgumentValue(args, "-mask"); + string gateway = GetArgumentValue(args, "-gateway"); + string dns = GetArgumentValue(args, "-dns"); + ConfigureNetwork(interfaceName, ip, mask, gateway, dns); + break; + + case "startup": + string appPath = GetArgumentValue(args, "-path"); + string appName = GetArgumentValue(args, "-name"); + bool remove = HasArgument(args, "-remove"); + ManageStartup(appPath, appName, remove); + break; + + case "shortcut": + string targetPath = GetArgumentValue(args, "-target"); + string shortcutPath = GetArgumentValue(args, "-path"); + string shortcutArgs = GetArgumentValue(args, "-args"); + CreateShortcut(targetPath, shortcutPath, shortcutArgs); + break; + + case "brightness": + string brightness = GetArgumentValue(args, "-level"); + if (string.IsNullOrEmpty(brightness)) + { + Console.Error.WriteLine("Error: 必须指定亮度级别 (-level)"); + return; + } + SetBrightness(int.Parse(brightness)); + break; + + case "screenshot": + string savePath = GetArgumentValue(args, "-path"); + if (string.IsNullOrEmpty(savePath)) + { + Console.Error.WriteLine("Error: 必须指定保存路径 (-path)"); + return; + } + CaptureScreenToFile(savePath); + Console.WriteLine("成功保存截图"); + break; + + default: + Console.Error.WriteLine("Error: 不支持的操作类型"); + break; + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static void SetWallpaper(string path) + { + if (!File.Exists(path)) + { + throw new Exception("壁纸文件不存在"); + } + + SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, path, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + Console.WriteLine("成功设置壁纸"); + } + + private static void ControlMonitor(string action) + { + IntPtr hWnd = GetForegroundWindow(); + switch (action.ToLower()) + { + case "off": + SendMessage(hWnd, WM_SYSCOMMAND, SC_MONITORPOWER, 2); + break; + case "on": + SendMessage(hWnd, WM_SYSCOMMAND, SC_MONITORPOWER, -1); + break; + default: + throw new Exception("不支持的显示器操作"); + } + Console.WriteLine("成功控制显示器"); + } + + private static void PowerControl(string mode) + { + switch (mode.ToLower()) + { + case "sleep": + SetSuspendState(false, false, false); + break; + case "hibernate": + SetSuspendState(true, false, false); + break; + case "awake": + SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_SYSTEM_REQUIRED); + break; + case "normal": + SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS); + break; + default: + throw new Exception("不支持的电源模式"); + } + Console.WriteLine("成功设置电源模式"); + } + + private static void ConfigureNetwork(string interfaceName, string ip, string mask, string gateway, string dns) + { + // 使用netsh命令配置网络 + StringBuilder command = new StringBuilder(); + command.AppendFormat("interface ip set address \"{0}\" static {1} {2}", interfaceName, ip, mask); + if (!string.IsNullOrEmpty(gateway)) + { + command.AppendFormat(" {0}", gateway); + } + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "netsh", + Arguments = command.ToString(), + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new Exception("设置IP地址失败"); + } + } + + if (!string.IsNullOrEmpty(dns)) + { + command.Clear(); + command.AppendFormat("interface ip set dns \"{0}\" static {1}", interfaceName, dns); + startInfo.Arguments = command.ToString(); + + using (Process process = Process.Start(startInfo)) + { + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new Exception("设置DNS失败"); + } + } + } + + Console.WriteLine("成功配置网络"); + } + + private static void ManageStartup(string appPath, string appName, bool remove) + { + string keyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath, true)) + { + if (key == null) + { + throw new Exception("无法访问启动项注册表"); + } + + if (remove) + { + key.DeleteValue(appName, false); + Console.WriteLine("成功移除开机启动项"); + } + else + { + key.SetValue(appName, appPath); + Console.WriteLine("成功添加开机启动项"); + } + } + } + + private static void CreateShortcut(string targetPath, string shortcutPath, string args) + { + // 使用PowerShell创建快捷方式 + StringBuilder command = new StringBuilder(); + command.AppendFormat(@" + $WshShell = New-Object -comObject WScript.Shell + $Shortcut = $WshShell.CreateShortcut('{0}') + $Shortcut.TargetPath = '{1}'", + shortcutPath.Replace("'", "''"), + targetPath.Replace("'", "''")); + + if (!string.IsNullOrEmpty(args)) + { + command.AppendFormat(@" + $Shortcut.Arguments = '{0}'", + args.Replace("'", "''")); + } + + command.Append(@" + $Shortcut.Save()"); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "powershell", + Arguments = command.ToString(), + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new Exception("创建快捷方式失败"); + } + } + + Console.WriteLine("成功创建快捷方式"); + } + + private static void SetBrightness(int level) + { + if (level < 0 || level > 100) + { + throw new Exception("亮度级别必须在0-100之间"); + } + + // 使用PowerShell命令设置亮度 + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "powershell", + Arguments = string.Format("(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,{0})", level), + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new Exception("设置亮度失败"); + } + } + + Console.WriteLine("成功设置亮度"); + } + + private static Image CaptureScreen() + { + IntPtr handle = GetDesktopWindow(); + IntPtr hdcSrc = GetWindowDC(handle); + RECT windowRect = new RECT(); + GetWindowRect(handle, ref windowRect); + int width = windowRect.right - windowRect.left; + int height = windowRect.bottom - windowRect.top; + + IntPtr hdcDest = CreateCompatibleDC(hdcSrc); + IntPtr hBitmap = CreateCompatibleBitmap(hdcSrc, width, height); + IntPtr hOld = SelectObject(hdcDest, hBitmap); + + BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, 0x00CC0020); + SelectObject(hdcDest, hOld); + DeleteDC(hdcDest); + + Image img = Image.FromHbitmap(hBitmap); + DeleteObject(hBitmap); + ReleaseDC(handle, hdcSrc); + + return img; + } + + private static void CaptureScreenToFile(string path) + { + Image img = CaptureScreen(); + img.Save(path, ImageFormat.Png); + img.Dispose(); + } + + private static void ShowHelp() + { + string help = @" +Windows 系统工具使用说明 +=================== + +基本语法: +utils.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. wallpaper - 设置壁纸 + 参数: + -path <文件路径> 壁纸图片路径 + 示例: utils.exe -type wallpaper -path ""C:\wallpaper.jpg"" + +2. monitor - 控制显示器 + 参数: + -action <动作> on/off + 示例: utils.exe -type monitor -action off + +3. power - 电源控制 + 参数: + -mode <模式> sleep/hibernate/awake/normal + 示例: utils.exe -type power -mode sleep + +4. network - 配置网络 + 参数: + -interface <网卡名称> 网络接口名称 + -ip 要设置的IP地址 + -mask <子网掩码> 子网掩码 + -gateway <网关> 默认网关(可选) + -dns DNS服务器(可选) + 示例: utils.exe -type network -interface ""以太网"" -ip 192.168.1.100 -mask 255.255.255.0 + +5. startup - 管理开机启动项 + 参数: + -path <应用程序路径> 应用程序路径 + -name <启动项名称> 启动项名称 + -remove 移除开机启动项(可选) + 示例: utils.exe -type startup -path ""C:\Program Files\MyApp\MyApp.exe"" -name MyApp + +6. shortcut - 创建快捷方式 + 参数: + -target <目标路径> 目标路径 + -path <快捷方式路径> 快捷方式路径 + -args <参数> 参数(可选) + 示例: utils.exe -type shortcut -target ""C:\Program Files\MyApp\MyApp.exe"" -path ""C:\Users\MyUser\Desktop\MyApp.lnk"" + +7. brightness - 控制亮度 + 参数: + -level <亮度> 亮度级别(0-100) + 示例: utils.exe -type brightness -level 75 + +8. screenshot - 屏幕截图 + 参数: + -path <保存路径> 截图保存路径 + 示例: utils.exe -type screenshot -path ""C:\screenshot.png"" + +注意事项: +-------- +1. 某些操作可能需要管理员权限 +2. 网络配置更改可能会暂时断开网络连接 +3. 电源控制可能会影响正在运行的程序 +4. 建议在更改系统设置前先备份当前配置 +"; + Console.WriteLine(help); + } + + private static string GetArgumentValue(string[] args, string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i].Equals(key, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } + + private static bool HasArgument(string[] args, string key) + { + return Array.Exists(args, arg => arg.Equals(key, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/plugin/lib/csharp/window.cs b/plugin/lib/csharp/window.cs new file mode 100644 index 00000000..18eeeb82 --- /dev/null +++ b/plugin/lib/csharp/window.cs @@ -0,0 +1,467 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Diagnostics; +using System.Web.Script.Serialization; +using System.Collections.Generic; + +public class WindowManager +{ + #region Win32 API + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); + + [DllImport("user32.dll")] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); + + [DllImport("user32.dll")] + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsIconic(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsZoomed(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsWindow(IntPtr hWnd); + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOSIZE = 0x0001; + private const uint WM_CLOSE = 0x0010; + private const int GWL_STYLE = -16; + private const int GWL_EXSTYLE = -20; + private const int WS_BORDER = 0x00800000; + private const int WS_CAPTION = 0x00C00000; + private const int WS_CHILD = 0x40000000; + private const int WS_POPUP = unchecked((int)0x80000000); + private const int WS_SYSMENU = 0x00080000; + private const int WS_MINIMIZEBOX = 0x00020000; + private const int WS_MAXIMIZEBOX = 0x00010000; + private const int WS_EX_TOPMOST = 0x00000008; + private const int WS_EX_TRANSPARENT = 0x00000020; + private const int WS_EX_TOOLWINDOW = 0x00000080; + private const int WS_EX_LAYERED = 0x00080000; + private const uint LWA_ALPHA = 0x2; + private const int SW_HIDE = 0; + private const int SW_SHOW = 5; + private const int SW_NORMAL = 1; + private const int SW_MAXIMIZE = 3; + private const int SW_MINIMIZE = 6; + private const int SW_RESTORE = 9; + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + #endregion + + public static void Main(string[] args) + { + if (args.Length == 0 || args[0] == "-h" || args[0] == "--help") + { + ShowHelp(); + return; + } + + try + { + List targetWindows = GetTargetWindows(args); + if (targetWindows.Count == 0) + { + throw new Exception("未找到目标窗口"); + } + + string type = GetArgumentValue(args, "-type"); + if (type.ToLower() == "info") + { + var allWindowInfo = new List>(); + foreach (IntPtr windowHandle in targetWindows) + { + var windowInfo = GetBasicWindowInfo(windowHandle); + if (windowInfo != null) + { + allWindowInfo.Add(windowInfo); + } + } + var serializer = new JavaScriptSerializer(); + Console.WriteLine(serializer.Serialize(allWindowInfo)); + return; + } + + IntPtr targetHandle = targetWindows[0]; + Dictionary operatedWindow = null; + + switch (type.ToLower()) + { + case "topmost": + bool isTopMost = bool.Parse(GetArgumentValue(args, "-value") ?? "true"); + SetWindowPos(targetHandle, isTopMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "opacity": + int opacity = int.Parse(GetArgumentValue(args, "-value") ?? "100"); + SetWindowLong(targetHandle, GWL_EXSTYLE, GetWindowLong(targetHandle, GWL_EXSTYLE) | WS_EX_LAYERED); + SetLayeredWindowAttributes(targetHandle, 0, (byte)(opacity * 2.55), LWA_ALPHA); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "rect": + string[] rectValues = (GetArgumentValue(args, "-value") ?? "").Split(','); + if (rectValues.Length == 4) + { + int x = int.Parse(rectValues[0]); + int y = int.Parse(rectValues[1]); + int width = int.Parse(rectValues[2]); + int height = int.Parse(rectValues[3]); + MoveWindow(targetHandle, x, y, width, height, true); + } + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "state": + string state = GetArgumentValue(args, "-value") ?? "normal"; + int cmdShow = state == "maximize" ? SW_MAXIMIZE : + state == "minimize" ? SW_MINIMIZE : SW_NORMAL; + ShowWindow(targetHandle, cmdShow); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "visible": + bool visible = bool.Parse(GetArgumentValue(args, "-value") ?? "true"); + ShowWindow(targetHandle, visible ? SW_SHOW : SW_HIDE); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "close": + PostMessage(targetHandle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "focus": + if (IsIconic(targetHandle)) + { + ShowWindow(targetHandle, SW_RESTORE); + } + SetForegroundWindow(targetHandle); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "border": + bool hasBorder = bool.Parse(GetArgumentValue(args, "-value") ?? "true"); + int style = GetWindowLong(targetHandle, GWL_STYLE); + style = hasBorder ? style | WS_CAPTION : style & ~WS_CAPTION; + SetWindowLong(targetHandle, GWL_STYLE, style); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "clickthrough": + bool isTransparent = bool.Parse(GetArgumentValue(args, "-value") ?? "true"); + int exStyle = GetWindowLong(targetHandle, GWL_EXSTYLE); + exStyle |= WS_EX_LAYERED; + exStyle = isTransparent ? exStyle | WS_EX_TRANSPARENT : exStyle & ~WS_EX_TRANSPARENT; + SetWindowLong(targetHandle, GWL_EXSTYLE, exStyle); + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + case "info": + operatedWindow = GetBasicWindowInfo(targetHandle); + break; + + default: + Console.Error.WriteLine("Error: 不支持的操作类型"); + return; + } + + if (operatedWindow != null) + { + var serializer = new JavaScriptSerializer(); + Console.WriteLine(serializer.Serialize(operatedWindow)); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(string.Format("Error: {0}", ex.Message)); + } + } + + private static List GetTargetWindows(string[] args) + { + List targetWindows = new List(); + string method = GetArgumentValue(args, "-method") ?? "title"; + string value = GetArgumentValue(args, "-window") ?? ""; + + switch (method.ToLower()) + { + case "handle": + IntPtr handle = new IntPtr(long.Parse(value)); + if (!IsWindow(handle)) + { + throw new Exception("指定的句柄不是一个有效的窗口句柄"); + } + targetWindows.Add(handle); + break; + + case "active": + targetWindows.Add(GetForegroundWindow()); + break; + + case "process": + var processes = Process.GetProcessesByName(value); + foreach (var process in processes) + { + if (process.MainWindowHandle != IntPtr.Zero) + { + targetWindows.Add(process.MainWindowHandle); + } + } + break; + + case "class": + EnumWindows((hwnd, param) => + { + if (!IsWindowVisible(hwnd)) + { + return true; + } + + StringBuilder className = new StringBuilder(256); + GetClassName(hwnd, className, className.Capacity); + string windowClassName = className.ToString(); + + if (windowClassName.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) + { + targetWindows.Add(hwnd); + } + return true; + }, IntPtr.Zero); + break; + + case "title": + default: + if (!string.IsNullOrEmpty(value)) + { + EnumWindows((hwnd, param) => + { + StringBuilder title = new StringBuilder(256); + GetWindowText(hwnd, title, title.Capacity); + string windowTitle = title.ToString(); + + if (!string.IsNullOrEmpty(windowTitle) && + windowTitle.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) + { + targetWindows.Add(hwnd); + } + return true; + }, IntPtr.Zero); + } + break; + } + + if (targetWindows.Count == 0) + { + throw new Exception("未找到匹配的窗口"); + } + + return targetWindows; + } + + private static Dictionary GetBasicWindowInfo(IntPtr hwnd) + { + StringBuilder title = new StringBuilder(256); + StringBuilder className = new StringBuilder(256); + GetWindowText(hwnd, title, title.Capacity); + GetClassName(hwnd, className, className.Capacity); + + // 获取窗口位置和大小 + RECT rect = new RECT(); + GetWindowRect(hwnd, out rect); + + // 获取进程信息 + uint processId = 0; + GetWindowThreadProcessId(hwnd, out processId); + string processName = ""; + string processPath = ""; + try + { + var process = Process.GetProcessById((int)processId); + processName = process.ProcessName; + processPath = process.MainModule.FileName; + } + catch { } + + return new Dictionary + { + { "handle", hwnd.ToInt64() }, + { "title", title.ToString() }, + { "class", className.ToString() }, + { "x", rect.Left }, + { "y", rect.Top }, + { "width", rect.Right - rect.Left }, + { "height", rect.Bottom - rect.Top }, + { "processName", processName }, + { "processPath", processPath } + }; + } + + private static string GetArgumentValue(string[] args, string key) + { + int index = Array.IndexOf(args, key); + if (index >= 0 && index < args.Length - 1) + { + return args[index + 1]; + } + return null; + } + + private static void ShowHelp() + { + string help = @" +Windows 窗口管理工具使用说明 +========================== + +基本语法: +window.exe -type <操作类型> [参数...] + +操作类型: +-------- +1. topmost - 设置窗口置顶 +2. opacity - 设置窗口透明度 +3. rect - 设置窗口位置和大小 +4. state - 设置窗口状态 +5. visible - 设置窗口可见性 +6. close - 关闭窗口 +7. focus - 设置窗口焦点 +8. border - 设置窗口边框 +9. clickthrough - 设置点击穿透 +10. info - 获取窗口信息 + +通用参数: +-------- +-method 窗口查找方式(可选,默认title) + 可选值: + - title 窗口标题(支持模糊匹配) + - handle 窗口句柄 + - active 当前活动窗口 + - process 进程名 + - class 窗口类名(支持模糊匹配) + +-window 要查找的窗口值(根据method解释) + +操作参数说明: +----------- +1. topmost: + -value true/false,是否置顶 + +2. opacity: + -value 0-100,透明度 + +3. rect: + -value x,y,width,height,窗口位置和大小 + +4. state: + -value normal/maximize/minimize,窗口状态 + +5. visible: + -value true/false,是否可见 + +6. border: + -value true/false,是否显示边框 + +7. clickthrough: + -value true/false,是否点击穿透 + +使用示例: +-------- +1. 设置窗口置顶: + window.exe -type topmost -window ""记事本"" -value true + +2. 设置窗口透明度: + window.exe -type opacity -window ""记事本"" -value 80 + +3. 设置窗口位置和大小: + window.exe -type rect -window ""记事本"" -value ""100,100,800,600"" + +4. 最大化窗口: + window.exe -type state -window ""记事本"" -value maximize + +5. 隐藏窗口: + window.exe -type visible -window ""记事本"" -value false + +6. 关闭窗口: + window.exe -type close -window ""记事本"" + +7. 获取窗口信息: + window.exe -type info -window ""记事本"" + +8. 通过进程名查找窗口: + window.exe -type info -method process -window ""notepad"" + +9. 通过窗口类名查找: + window.exe -type info -method class -window ""Chrome"" # 会匹配 Chrome_WidgetWin_1 + +返回值: +------ +1. 均为JSON格式 +2. info操作返回所有匹配窗口信息 +3. 其他操作返回操作的窗口信息 +4. 失败均抛出异常 + +注意事项: +-------- +1. 窗口标题、类名支持模糊匹配,active方式可不提供window参数 +2. 只有info操作会返回所有匹配窗口的信息,其他操作只会操作第一个匹配的窗口 +"; + Console.WriteLine(help); + } +} diff --git a/plugin/lib/dialog/controller.js b/plugin/lib/dialog/controller.js new file mode 100644 index 00000000..0c2cba3d --- /dev/null +++ b/plugin/lib/dialog/controller.js @@ -0,0 +1,535 @@ +const { ipcRenderer } = require("electron"); +const pinyinMatch = require("pinyin-match"); + +// 等待 DOM 加载完成 +document.addEventListener("DOMContentLoaded", () => { + let parentId = null; + let dialogType = null; + + // 监听父窗口发来的对话框配置 + ipcRenderer.on("window-config", (event, config) => { + parentId = event.senderId; + dialogType = config.type; + windowId = config.windowId; + + // 设置主题 + document.documentElement.setAttribute( + "data-theme", + config.isDark ? "dark" : "light" + ); + + // 设置对话框标题 + document.getElementById("title-text").textContent = config.title || "提示"; + + // 设置对话框内容 + if (config.content) { + document.getElementById("content").textContent = config.content; + } + + // 添加平台类名 + document.body.classList.add(`platform-${config.platform}`); + + // 根据类型设置不同的对话框内容 + switch (config.type) { + case "message": + document.body.classList.add("dialog-message"); // 添加消息对话框的类 + break; + + case "input": + document.getElementById("input").style.display = "block"; + document.body.classList.add("dialog-input"); + // 创建输入框 + const inputContainer = document.getElementById("input-container"); + inputContainer.innerHTML = ""; // 清空现有内容 + config.inputOptions.forEach((inputOption, index) => { + console.log(inputOption); + const div = document.createElement("div"); + div.className = "input-group"; + + const label = document.createElement("label"); + label.textContent = + typeof inputOption === "string" ? inputOption : inputOption.label; + + const input = document.createElement("input"); + input.type = "text"; + input.id = `input-${index}`; + if (typeof inputOption !== "string") { + input.value = inputOption.value || ""; + input.placeholder = inputOption.hint || ""; + } + + div.appendChild(label); + div.appendChild(input); + inputContainer.appendChild(div); + }); + document.getElementById("input-0").focus(); + break; + + case "confirm": + document.getElementById("confirm").style.display = "block"; + document.body.classList.add("dialog-confirm"); + break; + + case "buttons": + document.getElementById("buttons").style.display = "block"; + document.body.classList.add("dialog-buttons"); + // 创建按钮 + const buttonContainer = document.getElementById("button-container"); + buttonContainer.innerHTML = ""; + config.buttons.forEach((btn, index) => { + const button = document.createElement("button"); + button.textContent = btn; + button.onclick = () => { + ipcRenderer.sendTo(parentId, `window-response-${windowId}`, { + id: index, + text: btn, + }); + }; + buttonContainer.appendChild(button); + }); + break; + + case "textarea": + document.getElementById("textarea").style.display = "block"; + document.body.classList.add("dialog-textarea"); + const textarea = document.getElementById("text-content"); + if (config.placeholder) { + textarea.placeholder = config.placeholder; + } + if (config.defaultText) { + textarea.value = config.defaultText; + } + textarea.focus(); + break; + + case "select": + document.getElementById("select").style.display = "block"; + document.body.classList.add("dialog-select"); + const selectContainer = document.getElementById("select-container"); + const filterInput = document.getElementById("filter-input"); + const selectList = document.querySelector(".select-list"); + selectContainer.innerHTML = ""; + let currentSelected = null; + let allItems = []; + let filteredItems = []; + let hoverTimeout = null; + let isKeyboardNavigation = false; + + if (!config.enableSearch) { + filterInput.style.display = "none"; + } + + if (config.placeholder) { + filterInput.placeholder = config.placeholder; + } + + // 创建选项 + const createSelectItem = (item, index) => { + const div = document.createElement("div"); + div.className = "select-item"; + + // 点击事件 + div.onclick = () => { + const originalIndex = allItems.indexOf(item); + const result = + typeof item === "string" + ? { + id: originalIndex, + text: item, + } + : item; + ipcRenderer.sendTo(parentId, `window-response-${windowId}`, result); + }; + + // 鼠标移入事件 + div.onmouseenter = () => { + if (isKeyboardNavigation) return; + if (hoverTimeout) { + clearTimeout(hoverTimeout); + } + hoverTimeout = setTimeout(() => { + if (currentSelected) { + currentSelected.classList.remove("selected"); + } + div.classList.add("selected"); + currentSelected = div; + }, 0); + }; + + // 鼠标移动事件 + div.onmousemove = () => { + if (isKeyboardNavigation) { + isKeyboardNavigation = false; + selectList.classList.remove("keyboard-nav"); + } + }; + + // 高亮文本 + const highlightText = (text, filterText) => { + if (!filterText) return text; + const matchResult = pinyinMatch.match(text, filterText); + if (!matchResult) return text; + + const [start, end] = matchResult; + return ( + text.slice(0, start) + + `${text.slice(start, end + 1)}` + + text.slice(end + 1) + ); + }; + + if (typeof item === "string" || typeof item === "number") { + const highlightedText = highlightText( + String(item), + filterInput.value + ); + div.innerHTML = ` +
+

${highlightedText}

+
+ `; + } else { + const highlightedTitle = highlightText( + item.title, + filterInput.value + ); + const highlightedDesc = item.description + ? highlightText(item.description, filterInput.value) + : ""; + div.innerHTML = ` + ${ + item.icon + ? ` +
+ +
+ ` + : "" + } +
+

${highlightedTitle}

+ ${ + item.description + ? ` +

${highlightedDesc}

+ ` + : "" + } +
+ `; + } + return div; + }; + + // 过滤并更新列表 + const updateList = (filterText = "") => { + selectContainer.innerHTML = ""; + filteredItems = allItems.filter((item) => { + if (typeof item === "string" || typeof item === "number") { + return ( + filterText === "" || pinyinMatch.match(String(item), filterText) + ); + } else { + const titleMatch = pinyinMatch.match(item.title, filterText); + const descMatch = item.description + ? pinyinMatch.match(item.description, filterText) + : false; + return filterText === "" || titleMatch || descMatch; + } + }); + + filteredItems.forEach((item, index) => { + const div = createSelectItem(item, index); + selectContainer.appendChild(div); + }); + + // 默认选中第一项 + if (selectContainer.firstChild) { + selectContainer.firstChild.classList.add("selected"); + currentSelected = selectContainer.firstChild; + } + }; + + // 初始化列表 + allItems = config.items; + updateList(); + + // 添加筛选功能 + if (config.enableSearch) { + let filterTimeout = null; + filterInput.addEventListener("input", (e) => { + if (filterTimeout) { + clearTimeout(filterTimeout); + } + filterTimeout = setTimeout(() => { + updateList(e.target.value); + }, 100); + }); + } + + // 添加键盘导航 + const keydownHandler = (e) => { + const items = selectContainer.children; + if (!items.length) return; + + if ( + !isKeyboardNavigation && + (e.key === "ArrowUp" || e.key === "ArrowDown") + ) { + isKeyboardNavigation = true; + selectList.classList.add("keyboard-nav"); + } + + const currentIndex = Array.from(items).indexOf(currentSelected); + let newIndex = currentIndex; + + switch (e.key) { + case "ArrowUp": + e.preventDefault(); + if (currentIndex > 0) { + newIndex = currentIndex - 1; + } + break; + case "ArrowDown": + e.preventDefault(); + if (currentIndex < items.length - 1) { + newIndex = currentIndex + 1; + } + break; + case "Enter": + e.preventDefault(); + if (currentSelected) { + currentSelected.click(); + } + break; + case "Escape": + e.preventDefault(); + cancelDialog(); + break; + } + + if (newIndex !== currentIndex) { + if (currentSelected) { + currentSelected.classList.remove("selected"); + } + items[newIndex].classList.add("selected"); + currentSelected = items[newIndex]; + currentSelected.scrollIntoView({ block: "nearest" }); + } + }; + + document.addEventListener("keydown", keydownHandler); + + // 聚焦筛选框 + if (config.enableSearch) { + filterInput.focus(); + } + break; + + case "wait-button": + document.getElementById("wait-button").style.display = "block"; + document.body.classList.add("dialog-wait-button"); + + // 创建按钮组 + const waitButtonContainer = document.getElementById("wait-button"); + const buttonGroup = document.createElement("div"); + buttonGroup.className = "wait-button-group"; + + // 创建主按钮 + const waitBtn = document.createElement("button"); + waitBtn.id = "wait-btn"; + waitBtn.textContent = config.text; + waitBtn.onclick = () => { + ipcRenderer.sendTo(parentId, `window-response-${windowId}`, true); + }; + + buttonGroup.appendChild(waitBtn); + + // 如果需要显示取消按钮 + if (config.showCancel) { + const cancelBtn = document.createElement("button"); + cancelBtn.id = "wait-cancel-btn"; + cancelBtn.innerHTML = "✕"; // 使用 × 符号 + cancelBtn.onclick = () => { + ipcRenderer.sendTo(parentId, `window-response-${windowId}`, false); + }; + buttonGroup.appendChild(cancelBtn); + } + + waitButtonContainer.appendChild(buttonGroup); + break; + + case "process": + document.getElementById("process").style.display = "block"; + document.body.classList.add("dialog-process"); + // 创建进度条 + const processBarInner = document.getElementById("process-bar-inner"); + if (config.isLoading) { + // 如果是加载条模式,使用动画效果 + processBarInner.className = "process-bar-loading"; + } else { + // 如果是进度条模式,设置初始进度 + processBarInner.className = "process-bar-inner"; + processBarInner.style.width = `${config.value}%`; + } + + // 设置初始文本 + document.getElementById("process-text").textContent = config.text; + + // 如果需要显示暂停按钮 + if (config.showPause) { + document.body.classList.add("show-pause"); + const pauseBtn = document.getElementById("process-pause-btn"); + let isPaused = false; + + pauseBtn.onclick = () => { + isPaused = !isPaused; + pauseBtn.classList.toggle("paused", isPaused); + ipcRenderer.sendTo(parentId, `process-pause-${windowId}`, isPaused); + }; + } + + // 添加关闭按钮点击事件 + document.getElementById("process-close-btn").onclick = () => { + ipcRenderer.sendTo(parentId, `process-close-${windowId}`); + }; + break; + } + ipcRenderer.sendTo( + parentId, + `window-resize-${windowId}`, + calculateHeight() + ); + }); + + // 监听进度条更新事件 + ipcRenderer.on("update-process", (event, data) => { + const { value, text } = data; + const processBarInner = document.getElementById("process-bar-inner"); + if ( + processBarInner && + processBarInner.className === "process-bar-inner" && + typeof value === "number" + ) { + processBarInner.style.width = `${value}%`; + } + if (text) { + const processText = document.getElementById("process-text"); + processText.innerHTML = text; + processText.scrollTop = processText.scrollHeight; + ipcRenderer.sendTo( + parentId, + `window-resize-${windowId}`, + calculateHeight() + ); + } + }); + + const calculateHeight = () => { + const titleBar = document.querySelector(".title-bar"); + const buttonBar = document.querySelector(".button-bar"); + const contentWrapper = document.querySelector(".content-wrapper"); + const processText = document.getElementById("process-text"); + + // 对于进度条对话框,特殊处理高度计算 + if (dialogType === "process") { + const processTextHeight = processText ? processText.scrollHeight : 0; + const totalHeight = Math.max(60, processTextHeight + 40); + return Math.min(totalHeight, 290); // 限制最大高度 + } + + // 其他对话框的高度计算保持不变 + const totalHeight = + titleBar.offsetHeight + + contentWrapper.scrollHeight + + (buttonBar.style.display !== "none" ? buttonBar.offsetHeight : 0); + + const maxHeight = dialogType === "select" ? 620 : 520; + const minHeight = 100; + + // 确保高度在最小值和最大值之间 + return Math.min(Math.max(totalHeight, minHeight), maxHeight); + }; + + // 确定按钮点击事件 + document.getElementById("ok-btn").onclick = () => { + let result; + + switch (dialogType) { + case "message": + result = true; + break; + + case "input": + const inputs = document.querySelectorAll("#input-container input"); + result = Array.from(inputs).map((input) => input.value); + break; + + case "confirm": + result = true; + break; + + case "textarea": + result = document.getElementById("text-content").value; + break; + } + + ipcRenderer.sendTo(parentId, `window-response-${windowId}`, result); + }; + + const cancelDialog = () => { + let result; + switch (dialogType) { + case "input": + result = []; + break; + case "textarea": + result = ""; + break; + case "confirm": + result = false; + break; + case "buttons": + result = {}; + break; + case "select": + result = {}; + break; + default: + result = null; + } + ipcRenderer.sendTo(parentId, `window-response-${windowId}`, result); + }; + + // 取消按钮点击事件 + document.getElementById("cancel-btn").onclick = () => { + cancelDialog(); + }; + + // ESC键关闭窗口 + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + cancelDialog(); + } + }); + + // 回车键确认 + document.addEventListener("keydown", (e) => { + // 如果是文本区域且按下回车,不触发确认 + if (e.key === "Enter") { + if (dialogType === "textarea" && !e.ctrlKey) { + return; + } + // select 类型有自己的键盘处理器,不需要全局处理器处理 Enter 键 + if (dialogType === "select") { + return; + } + document.getElementById("ok-btn").click(); + } + }); + + // 关闭按钮点击事件 + document.querySelector(".close-btn").onclick = () => { + cancelDialog(); + }; +}); diff --git a/plugin/lib/dialog/service.js b/plugin/lib/dialog/service.js new file mode 100644 index 00000000..48e3fa4a --- /dev/null +++ b/plugin/lib/dialog/service.js @@ -0,0 +1,483 @@ +const { ipcRenderer } = require("electron"); +const os = require("os"); +const { createBrowserWindow } = utools; + +const dialogPath = "lib/dialog/view.html"; +const preloadPath = "lib/dialog/controller.js"; + +const commonBrowserWindowOptions = { + resizable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + skipTaskbar: true, + alwaysOnTop: true, + frame: false, + movable: true, + webPreferences: { + preload: preloadPath, + devTools: utools.isDev(), + }, +}; + +/** + * 创建对话框窗口 + * @param {object} config - 对话框配置 + * @param {object} [customDialogOptions] - 自定义窗口选项 + * @returns {Promise} 返回对话框结果 + */ +const createDialog = (config, customDialogOptions = {}) => { + return new Promise((resolve) => { + // linux 和 win32 都使用 win32 的样式 + const platform = os.platform() === "darwin" ? "darwin" : "win32"; + + const dialogWidth = + config.type === "textarea" || config.type === "select" + ? 500 + : platform === "win32" + ? 370 + : 420; + + const dialogOptions = { + title: config.title || "对话框", + width: dialogWidth, + height: 80, + opacity: 0, + ...commonBrowserWindowOptions, + ...customDialogOptions, // 合并自定义选项 + }; + + // 创建窗口 + const UBrowser = createBrowserWindow(dialogPath, dialogOptions, () => { + const windowId = UBrowser.webContents.id; + + const windowResponseHandler = (event, result) => { + resolve(result); + UBrowser.destroy(); + }; + + const windowResizeHandler = (event, height) => { + // 获取当前窗口位置 + const bounds = UBrowser.getBounds(); + // 调整y坐标,保持窗口中心点不变 + const y = Math.round(bounds.y - (height - bounds.height) / 2); + // 确保坐标和尺寸都是有效的整数 + const newBounds = { + x: Math.round(bounds.x), + y: Math.max(0, y), // 确保不会超出屏幕顶部 + width: dialogWidth, + height: Math.round(height), + }; + // 设置新的位置和大小 + UBrowser.setBounds(newBounds); + UBrowser.setOpacity(1); + }; + + // 监听子窗口返回的计算高度, 等待按钮有自己的计算逻辑 + config.type !== "wait-button" && + ipcRenderer.once(`window-resize-${windowId}`, windowResizeHandler); + + // 监听子窗口返回的返回值 + ipcRenderer.once(`window-response-${windowId}`, windowResponseHandler); + + // 发送配置到子窗口 + ipcRenderer.sendTo(windowId, `window-config`, { + ...config, + isDark: utools.isDarkColors(), + platform, + windowId, + }); + }); + }); +}; + +/** + * 显示一个系统级消息框 + * @param {string} content - 消息内容 + * @param {string} [title] - 标题,默认为空 + * @returns {Promise} Promise + */ +const showSystemMessageBox = async (content, title = "") => { + await createDialog({ + type: "message", + title, + content, + }); +}; + +/** + * 显示一个系统级输入框组对话框 + * @param {string[]|{label:string,value:string,hint:string}[]} options - 输入框配置,可以是标签数组或者带属性的对象数组 + * @param {string} [title] - 标题,默认为空 + * @returns {Promise} 输入的内容数组 + */ +const showSystemInputBox = async (options, title = "") => { + // 确保 options 是数组 + const optionsArray = Array.isArray(options) ? options : [options]; + + // 转换每个选项为正确的格式 + const inputOptions = optionsArray.map((opt) => { + if (typeof opt === "string") { + return opt; + } + if (typeof opt === "object" && opt.label) { + return { + label: opt.label, + value: opt.value || "", + hint: opt.hint || "", + }; + } + throw new Error("输入框配置格式错误"); + }); + + return await createDialog({ + type: "input", + title, + inputOptions, + }); +}; + +/** + * 显示一个系统级确认框,返回是否点击了确认 + * @param {string} content - 确认内容 + * @param {string} [title] - 标题,默认为空 + * @returns {Promise} 是否确认 + */ +const showSystemConfirmBox = async (content, title = "") => { + const result = await createDialog({ + type: "confirm", + title, + content, + }); + return !!result; +}; + +/** + * 显示一个系统级按钮组对话框,返回点击的按钮的索引和文本 + * @param {string[]} buttons - 按钮文本数组 + * @param {string} [title] - 标题,默认为空 + * @returns {Promise<{id: number, text: string}>} 选择的按钮信息 + */ +const showSystemButtonBox = async (buttons, title = "") => { + return await createDialog({ + type: "buttons", + title, + buttons: Array.isArray(buttons) ? buttons : [buttons], + }); +}; + +/** + * 显示一个系统级多行文本输入框 + * @param {string} [placeholder] - 输入框的提示文本 + * @param {string} [defaultText] - 输入框的默认文本,默认为空 + * @returns {Promise} 编辑后的文本 + */ +const showSystemTextArea = async (placeholder = "请输入", defaultText = "") => { + return await createDialog({ + type: "textarea", + placeholder, + defaultText, + }); +}; + +/** + * 显示一个系统级选择列表对话框 + * @param {Array} items - 选项列表 + * @param {object} [options] - 配置选项 + * @param {string} [options.title] - 对话框标题 + * @param {string} [options.placeholder] - 输入框占位符 + * @param {boolean} [options.enableSearch] - 是否启用搜索 + * @returns {Promise<{id: number, text: string, data: any}>} 选择的结果 + */ +const showSystemSelectList = async (items, options = {}) => { + const { + title = "请选择", + placeholder = "", + enableSearch = true, + optionType = "plaintext", + } = options; + + return await createDialog({ + type: "select", + title, + placeholder, + enableSearch, + items, + optionType, + }); +}; + +/** + * 计算窗口位置 + * @param {object} options - 配置选项 + * @param {string} [options.position="bottom-right"] - 窗口位置,可选值:top-left, top-right, bottom-left, bottom-right + * @param {number} options.width - 窗口宽度 + * @param {number} options.height - 窗口高度 + * @param {number} [options.padding=20] - 边距 + * @returns {{x: number, y: number}} 窗口位置坐标 + */ +const calculateWindowPosition = (options) => { + const { position = "bottom-right", width, height, padding = 20 } = options; + + // 获取主屏幕尺寸 + const primaryDisplay = utools.getPrimaryDisplay(); + const { width: screenWidth, height: screenHeight } = + primaryDisplay.workAreaSize; + + let x, y; + switch (position) { + case "top-left": + x = padding; + y = padding; + break; + case "top-right": + x = screenWidth - width - padding; + y = padding; + break; + case "bottom-left": + x = padding; + y = screenHeight - height - padding; + break; + case "bottom-right": + default: + x = screenWidth - width - padding; + y = screenHeight - height - padding; + break; + } + + return { x, y }; +}; + +/** + * 显示一个系统级等待按钮 + * @param {object} options - 配置选项 + * @param {string} [options.text="等待操作"] - 按钮文本 + * @param {string} [options.position="bottom-right"] - 按钮位置,可选值:top-left, top-right, bottom-left, bottom-right + * @param {boolean} [options.showCancel=false] - 是否显示取消按钮 + * @returns {Promise} 点击确定返回true,点击取消返回false + */ +const showSystemWaitButton = async (options = {}) => { + const { + text = "等待操作", + position = "bottom-right", + showCancel = true, + } = options; + + // 创建临时span计算文本宽度 + const span = document.createElement("span"); + span.style.font = + '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; + span.style.visibility = "hidden"; + span.style.position = "absolute"; + span.textContent = text; + document.body.appendChild(span); + + // 计算窗口尺寸 + const textWidth = span.offsetWidth; + document.body.removeChild(span); + + const width = Math.max(textWidth + 32 + (showCancel ? 25 : 0), 80); + const height = 36; + + // 计算窗口位置 + const { x, y } = calculateWindowPosition({ position, width, height }); + + return await createDialog( + { + type: "wait-button", + text, + showCancel, + }, + { + width, + height, + x, + y, + opacity: 1, + } + ); +}; + +let lastProcessBar = null; + +/** + * 显示一个进度条对话框 + * @param {object} options - 配置选项 + * @param {string} [options.title="进度"] - 对话框标题 + * @param {string} [options.text="处理中..."] - 进度条上方的文本 + * @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] - 暂停按钮点击时的回调函数 + * @param {Function} [options.onResume] - 恢复按钮点击时的回调函数 + * @returns {Promise<{id: number, close: Function}>} 返回进度条窗口ID和关闭函数 + * @throws {Error} 如果只配置了onPause或onResume中的一个会抛出错误 + */ +const showProcessBar = async (options = {}) => { + const { + text = "处理中...", + value, + position = "bottom-right", + onClose, + onPause, + onResume, + } = options; + + // 校验暂停/恢复回调必须同时配置 + if ((onPause && !onResume) || (!onPause && onResume)) { + throw new Error("onPause 和 onResume 必须同时配置"); + } + + const windowWidth = 350; + 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; + let windowResizeHandler; + let processPauseHandler; + + // 创建事件处理器 + windowResizeHandler = (event, height) => { + const bounds = UBrowser.getBounds(); + const y = Math.round(bounds.y - (height - bounds.height)); + const newBounds = { + x: Math.round(bounds.x), + y: Math.max(0, y), + width: windowWidth, + height: Math.round(height), + }; + UBrowser.setBounds(newBounds); + UBrowser.setOpacity(1); + }; + + // 监听暂停/恢复事件 + if (onPause && onResume) { + processPauseHandler = (event, isPaused) => { + if (isPaused) { + onPause(); + } else { + onResume(); + } + }; + ipcRenderer.on(`process-pause-${windowId}`, processPauseHandler); + } + + // 监听子窗口返回的计算高度 + ipcRenderer.on(`window-resize-${windowId}`, windowResizeHandler); + + const closeProcessBar = () => { + if (typeof onClose === "function") { + onClose(); + } + // 清理所有事件监听器 + ipcRenderer.removeListener( + `window-resize-${windowId}`, + windowResizeHandler + ); + if (processPauseHandler) { + ipcRenderer.removeListener( + `process-pause-${windowId}`, + processPauseHandler + ); + } + lastProcessBar = null; + UBrowser.destroy(); + }; + + // 监听对话框结果 + ipcRenderer.once(`process-close-${windowId}`, () => { + closeProcessBar(); + }); + + // 发送配置到子窗口 + ipcRenderer.sendTo(windowId, "window-config", { + type: "process", + text, + value: value === undefined ? 0 : value, + isDark: utools.isDarkColors(), + platform: process.platform, + showPause: Boolean(onPause && onResume), + isLoading: value === undefined, + windowId, + }); + + const processBar = { + id: windowId, + close: closeProcessBar, + }; + + lastProcessBar = processBar; + resolve(processBar); + } + ); + }); +}; + +/** + * 更新进度条的进度 + * @param {object} options - 配置选项 + * @param {number} [options.value] - 新的进度值(0-100),不传则显示加载动画 + * @param {string} [options.text] - 新的进度文本 + * @param {boolean} [options.complete] - 是否完成并关闭进度条 + * @param {{id: number, close: Function}|undefined} processBar - 进度条对象, 如果不传入则使用上一次创建的进度条 + * @throws {Error} 如果传入的processBar对象不是有效的进度条对象 + */ +const updateProcessBar = (options = {}, processBar = null) => { + if (!processBar) { + if (!lastProcessBar) { + throw new Error("没有找到已创建的进度条"); + } + processBar = lastProcessBar; + } + // 校验processBar对象 + if ( + typeof processBar !== "object" || + typeof processBar.id !== "number" || + typeof processBar.close !== "function" + ) { + throw new Error("processBar对象格式错误"); + } + + const { value, text, complete } = options; + ipcRenderer.sendTo(processBar.id, "update-process", { + value, + text, + isLoading: value === undefined, + }); + + if (complete) { + setTimeout(() => { + processBar.close(); + }, 600); + } +}; + +module.exports = { + showSystemMessageBox, + showSystemInputBox, + showSystemConfirmBox, + showSystemButtonBox, + showSystemTextArea, + showSystemSelectList, + showSystemWaitButton, + showProcessBar, + updateProcessBar, +}; diff --git a/plugin/lib/dialog/style.css b/plugin/lib/dialog/style.css new file mode 100644 index 00000000..466967c0 --- /dev/null +++ b/plugin/lib/dialog/style.css @@ -0,0 +1,833 @@ +:root { + --bg-color: #fff; + --text-color: #333; + --border-color: #ddd; + --mac-title-bg: #f5f5f5; + --input-bg: #fff; + --input-border: #ddd; + --input-focus: #0d6efd; + --button-bg: #2767cf; + --button-text: #fff; + --cancel-bg: #67696B; + --cancel-border: #67696B; + --mac-close-btn: #ff5f57; + --win-title-bg: #F9F1EF; + --win-button-bar-bg: #F0F0F0; + --win-button-bar-border: #E0E0E0; + --win-button-bg: #fdfdfd; + --win-button-hover-border: #3C96DB; + --win-button-hover-bg: #E0EEF9; + --win-close-hover: #e81123; + --mac-close-x: rgba(0, 0, 0, 0.5); + --win-border: rgba(0, 0, 0, 0.1); + --win-text: #000; + --select-item-hover: rgba(13, 110, 253, 0.1); + --select-item-shadow: rgba(0, 0, 0, 0.02); + --select-item-shadow-dark: rgba(0, 0, 0, 0.1); + --select-description: rgba(0, 0, 0, 0.6); + --highlight-color: #ec3535; + --wait-btn-hover: #ec3535; + --scrollbar-thumb: rgba(0, 0, 0, 0.3); + --input-box-shadow: rgba(13, 110, 253, 0.25) +} + +:root[data-theme="dark"] { + --bg-color: #282727; + --text-color: #e0e0e0; + --border-color: #404040; + --mac-title-bg: #404143; + --input-bg: #2d2d2d; + --input-border: #404040; + --win-title-bg: #17040B; + --win-button-bar-bg: #1C1C1C; + --win-button-bar-border: #1C1C1C; + --win-button-bg: #353B3A; + --win-button-hover-bg: #495150; + --win-text: #fdfdfd; + --content-bg: #36383A; + --win-content-bg: #191919; + --scrollbar-thumb: rgba(255, 255, 255, 0.3); + --select-item-hover: rgba(13, 110, 253, 0.2); + --select-description: rgba(255, 255, 255, 0.6); +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + background: var(--bg-color); + color: var(--text-color); + user-select: none; + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 自定义滚动条样式 */ +::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + transition: background-color 0.3s; + border-radius: 6px; +} + +/* 悬浮时显示滚动条 */ +*:hover::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + transition: background-color 0.3s; +} + +/* 标题栏样式 */ +.title-bar { + background: var(--mac-title-bg); + padding: 2px 12px; + -webkit-app-region: drag; + display: flex; + align-items: center; + flex-shrink: 0; + height: 20px; +} + +.title-text { + font-size: 12px; + font-weight: normal; +} + +.title-left { + flex: 1; + display: flex; + align-items: center; +} + +.logo { + width: 20px; + height: 20px; + margin-right: 6px; +} + +.platform-darwin .title-left { + display: flex; + align-items: center; + justify-content: center +} + +/* macOS 样式 */ +.platform-darwin .title-bar { + border-bottom: 1px solid var(--border-color); +} + +.platform-darwin .close-btn { + position: absolute; + left: 8px; + top: 7px; + background-color: var(--mac-close-btn); + width: 12px; + height: 12px; + border-radius: 50%; + opacity: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + -webkit-app-region: no-drag; +} + +.platform-darwin .close-btn::before, +.platform-darwin .close-btn::after { + content: ""; + position: absolute; + width: 8px; + height: 1px; + background-color: var(--mac-close-x); + transform-origin: center; + opacity: 0; + transition: opacity 0.2s; +} + +.platform-darwin .close-btn::before { + transform: rotate(45deg); +} + +.platform-darwin .close-btn::after { + transform: rotate(-45deg); +} + +.platform-darwin .close-btn:hover { + background-color: var(--mac-close-btn); +} + +.platform-darwin .close-btn:hover::before, +.platform-darwin .close-btn:hover::after { + opacity: 1; +} + +/* Windows 样式 */ +.platform-win32 .title-bar { + padding: 4px 0 4px 12px; + background: var(--win-title-bg); +} + +.platform-win32 .close-btn { + -webkit-app-region: no-drag; + width: 35px; + height: 27px; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + position: relative; + opacity: 1; + background: transparent; +} + +.platform-win32 .close-btn::before, +.platform-win32 .close-btn::after { + content: ''; + position: absolute; + width: 12px; + height: 1px; + background-color: var(--win-text); +} + +.platform-win32 .close-btn::before { + transform: rotate(45deg); +} + +.platform-win32 .close-btn::after { + transform: rotate(-45deg); +} + +.platform-win32 .close-btn:hover { + background-color: var(--win-close-hover); +} + +.platform-win32 .close-btn:hover::before, +.platform-win32 .close-btn:hover::after { + background-color: #fff; +} + +/* 按钮栏样式 */ +.platform-win32 .button-bar { + padding: 6px 16px; + background: var(--win-button-bar-bg); + border-top: 1px solid var(--win-button-bar-border); + height: 24px; +} + +.container { + display: flex; + flex-direction: column; + height: 100vh; +} + +.content-wrapper { + padding: 16px 16px 0 16px; + min-height: 60px; + max-height: 449px; + overflow-y: auto; + overflow-x: hidden; + flex: 1; +} + +.platform-win32 .content-wrapper, +.dialog-buttons .content-wrapper { + padding-bottom: 16px; +} + +.content-wrapper { + background: var(--content-bg); +} + +.platform-win32 .content-wrapper { + background: var(--win-content-bg); +} + +/* 选择列表对话框的内容区域padding和高度 */ +.dialog-select .content-wrapper { + padding: 16px 8px; + max-height: 600px; +} + +#content { + line-height: 1.4; + font-size: 13px; + user-select: text; + white-space: pre-wrap; +} + +.button-bar { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px; + background: var(--content-bg); + flex-shrink: 0; +} + +button { + padding: 4px 12px; + border-radius: 4px; + border: 1px solid var(--button-bg); + background: var(--button-bg); + color: var(--button-text); + cursor: pointer; + font-size: 13px; + min-width: 70px; + transition: all 0.2s ease; +} + +button:hover { + filter: brightness(1.2); +} + +.platform-win32 #ok-btn, +.platform-win32 #cancel-btn { + background: var(--win-button-bg); + color: var(--win-text); + border: 1px solid var(--win-border); + padding: 2px 12px; + height: 24px; + border-radius: 2px; + box-shadow: none; +} + +.platform-win32 #ok-btn:hover, +.platform-win32 #cancel-btn:hover { + border: 1px solid var(--win-button-hover-border); + background-color: var(--win-button-hover-bg); +} + +#ok-btn, +#cancel-btn { + padding: 0 12px; + height: 20px; + width: 78px; +} + +#cancel-btn { + background: var(--cancel-bg); + border: 1px solid var(--cancel-border); +} + +#input-container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.input-group label { + display: block; + padding: 0 0 4px 2px; + color: var(--text-color); + font-size: 13px; +} + +.input-group input { + width: 100%; + padding: 6px 8px; + border: 1px solid var(--input-border); + border-radius: 4px; + font-size: 13px; + box-sizing: border-box; + background: var(--input-bg); + color: var(--text-color); +} + +.input-group input:focus { + border-color: var(--input-focus); + outline: none; + box-shadow: 0 0 0 2px var(--input-box-shadow); +} + +/* 文本区域样式 */ +textarea { + width: 100%; + height: 400px; + padding: 6px 8px; + border: 1px solid var(--input-border); + border-radius: 4px; + font-size: 13px; + resize: none; + box-sizing: border-box; + background: var(--input-bg); + color: var(--text-color); +} + +textarea:focus { + border-color: var(--input-focus); + outline: none; + box-shadow: 0 0 0 2px var(--input-box-shadow); +} + +/* 按钮组样式 */ +#button-container { + display: flex; + flex-direction: column; + gap: 10px; + padding-top: 4px; +} + +#button-container button { + width: 100%; + text-align: center; + padding: 6px 12px; +} + +/* 根据对话框类型显示/隐藏取消按钮 */ +.dialog-message #cancel-btn { + display: none; +} + +.dialog-buttons .button-bar { + display: none; +} + +/* 隐藏所有对话框内容 */ +#input, +#confirm, +#buttons, +#textarea, +#select { + display: none; +} + +/* 选择列表样式 */ +.select-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 505px; + overflow-y: auto; +} + +.filter-input { + padding: 0 2px 8px 2px; +} + +.filter-input input { + width: 100%; + padding: 6px 8px; + border: 1px solid var(--input-border); + border-radius: 4px; + font-size: 13px; + box-sizing: border-box; + background: var(--input-bg); + color: var(--text-color); +} + +.filter-input input:focus { + border-color: var(--input-focus); + outline: none; + box-shadow: 0 0 0 2px var(--input-box-shadow); +} + +.select-list-container { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 360px; + overflow-y: auto; +} + +.select-item { + display: flex; + align-items: center; + padding: 6px 8px; + border-radius: 8px; + cursor: pointer; + position: relative; + transform: translateY(0) scale(1); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform; +} + +.select-item.selected { + background-color: var(--select-item-hover); + position: relative; + transform: translateY(-1px) scale(0.995); + will-change: transform; +} + +.select-item-icon { + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform; + padding-right: 8px; +} + +.select-item.selected .select-item-icon { + transform: scale(1.05); + filter: brightness(1.05); +} + +/* 添加选择时的轻微阴影效果 */ +.select-item.selected, +.select-list:not(.keyboard-nav) .select-item:hover { + box-shadow: 0 1px 2px var(--select-item-shadow); +} + +.select-item-icon { + width: 34px; + height: 34px; + margin-right: 8px; + border-radius: 4px; + overflow: hidden; +} + +.select-item-icon img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.select-item-content { + flex: 1; + min-width: 0; +} + +.select-item-title { + font-size: 13px; + line-height: 1.4; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.select-item-description { + font-size: 12px; + color: var(--select-description); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 搜索结果高亮样式 */ +.highlight { + color: var(--highlight-color); +} + +/* 隐藏确定和取消按钮 */ +.dialog-select .button-bar { + display: none; +} + +.dialog-wait-button .title-bar, +.dialog-wait-button .button-bar { + display: none; +} + +.dialog-wait-button .content-wrapper { + padding: 0; +} + +/* 按钮容器和按钮样式 */ +#wait-button { + display: none; + position: absolute; + inset: 0; +} + +.wait-button-group { + display: flex; + height: 100%; + border: none; + background: none; +} + +#wait-btn, +#wait-cancel-btn { + height: 100%; + border-radius: 0; + cursor: pointer; + color: white; +} + +#wait-btn:hover, +#wait-cancel-btn:hover { + filter: brightness(1.1); +} + +#wait-btn { + width: 100%; + background: var(--button-bg); + font-size: 14px; + white-space: nowrap; +} + +#wait-cancel-btn { + width: 25px; + min-width: 25px; + font-size: 18px; + padding: 0; +} + +#wait-cancel-btn:hover { + background-color: var(--wait-btn-hover); +} + +/* 进度条对话框样式 */ +.dialog-process .title-bar, +.dialog-process .button-bar { + display: none; +} + +.dialog-process .content-wrapper { + padding: 0; + background: var(--button-bg); + color: white; + min-height: auto; + height: auto; + overflow: visible; +} + +#process { + display: none; + padding: 8px 12px; + position: relative; + -webkit-app-region: drag; +} + +.process-text { + font-size: 13px; + margin-bottom: 8px; + overflow-y: auto; + overflow-x: hidden; + padding-right: 2px; + width: calc(100% - 15px); + box-sizing: border-box; + /* 290 - 40 的 padding*/ + max-height: 250px; + -webkit-app-region: no-drag; +} + +.process-bar { + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.3); + border-radius: 2px; + overflow: hidden; + position: relative; +} + +.process-bar-inner { + width: 0; + height: 100%; + background: white; + border-radius: 2px; + transition: width 0.3s ease; +} + +@keyframes loading-bar-animation { + 0% { + left: -80px; + } + + 100% { + left: calc(100% + 80px); + } +} + +.process-bar-loading { + position: absolute; + width: 80px; + height: 100%; + background: white; + border-radius: 2px; + animation: loading-bar-animation 3s infinite linear; +} + +/* 进度条按钮容器 */ +.process-buttons { + position: absolute; + right: 8px; + top: 6px; + display: flex; + gap: 8px; + -webkit-app-region: no-drag; + z-index: 1; +} + +/* 进度条关闭按钮 */ +.process-close-btn { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; +} + +.process-close-btn:hover { + opacity: 1; +} + +.process-close-btn::before, +.process-close-btn::after { + content: ""; + position: absolute; + width: 12px; + height: 1px; + background-color: white; + transform-origin: center; +} + +.process-close-btn::before { + transform: rotate(45deg); +} + +.process-close-btn::after { + transform: rotate(-45deg); +} + +/* 进度条暂停按钮 */ +.process-pause-btn { + width: 16px; + height: 16px; + display: none; + /* 默认隐藏 */ + align-items: center; + justify-content: center; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; + position: relative; +} + +.process-pause-btn:hover { + opacity: 1; +} + +/* 暂停状态(显示两条竖线) */ +.process-pause-btn::before, +.process-pause-btn::after { + content: ""; + position: absolute; + width: 2px; + height: 10px; + background-color: white; + transition: all 0.2s ease; +} + +.process-pause-btn::before { + transform: translateX(-3px); +} + +.process-pause-btn::after { + transform: translateX(3px); +} + +/* 播放状态(显示三角形) */ +.process-pause-btn.paused::before { + width: 0; + height: 0; + background: none; + border-style: solid; + border-width: 6px 0 6px 10px; + border-color: transparent transparent transparent white; + transform: translateX(1px); +} + +.process-pause-btn.paused::after { + display: none; +} + +/* 显示暂停按钮 */ +.show-pause .process-pause-btn { + display: flex; +} + +/* markdown */ +#process p { + margin: 0; + line-height: 1.5; +} + +#process pre { + background-color: rgba(255, 255, 255, 0.1); + padding: 8px 10px; + border-radius: 4px; + margin: 4px 0; + overflow-x: auto; + max-width: 100%; + font-size: 12px; +} + +#process code { + font-family: Monaco, Consolas, Liberation Mono, monospace; + padding: 2px 4px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 3px; + font-size: 12px; +} + +#process pre code { + padding: 0; + background-color: transparent; +} + +#process ul, +#process ol { + margin: 4px 0; + padding-left: 1.2em; +} + +#process h1, +#process h2, +#process h3, +#process h4, +#process h5, +#process h6 { + margin: 6px 0 4px 0; + font-weight: 600; + line-height: 1.3; +} + +#process h1 { + font-size: 16px; +} + +#process h2 { + font-size: 15px; +} + +#process h3 { + font-size: 14px; +} + +#process h4, +#process h5, +#process h6 { + font-size: 13px; +} + +#process pre::-webkit-scrollbar { + height: 3px; +} + +#process think, +#process blockquote { + display: block; + color: rgba(255, 255, 255, 0.7); + border-left: 3px solid rgba(255, 255, 255, 0.3); + padding: 2px 0 2px 8px; + font-size: 12px; + margin: 4px 0; +} + +#process hr { + border-style: solid; + border-width: 0.5px; +} diff --git a/plugin/lib/dialog/view.html b/plugin/lib/dialog/view.html new file mode 100644 index 00000000..b4c8ad3a --- /dev/null +++ b/plugin/lib/dialog/view.html @@ -0,0 +1,73 @@ + + + + + 对话框 + + + +
+ +
+
+ +

对话框

+
+
+
+ +
+
+ + +
+
+
+ + +
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+ +
+ + +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + diff --git a/plugin/lib/getCommandToLaunchTerminal.js b/plugin/lib/getCommandToLaunchTerminal.js deleted file mode 100644 index 2aca1862..00000000 --- a/plugin/lib/getCommandToLaunchTerminal.js +++ /dev/null @@ -1,44 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -const getCommandToLaunchTerminal = (cmdline, dir) => { - let cd, command; - if (window.utools.isWindows()) { - let appPath = path.join( - window.utools.getPath("home"), - "/AppData/Local/Microsoft/WindowsApps/" - ); - // 直接 existsSync wt.exe 无效 - if (fs.existsSync(appPath) && fs.readdirSync(appPath).includes("wt.exe")) { - cmdline = cmdline.replace(/"/g, `\\"`); - cd = dir ? `-d "${dir.replace(/\\/g, "/")}"` : ""; - command = `${appPath}wt.exe ${cd} cmd /k "${cmdline}"`; - } else { - cmdline = cmdline.replace(/"/g, `^"`); - cd = dir ? `cd /d "${dir.replace(/\\/g, "/")}" &&` : ""; - command = `${cd} start "" cmd /k "${cmdline}"`; - } - } else if (window.utools.isMacOs()) { - cmdline = cmdline.replace(/"/g, `\\"`); - cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : ""; - command = fs.existsSync("/Applications/iTerm.app") - ? `osascript -e 'tell application "iTerm" - if application "iTerm" is running then - create window with default profile - end if - tell current session of first window to write text "clear && ${cd} ${cmdline}" - activate - end tell'` - : `osascript -e 'tell application "Terminal" - if application "Terminal" is running then - do script "clear && ${cd} ${cmdline}" - else - do script "clear && ${cd} ${cmdline}" in window 1 - end if - activate - end tell'`; - } - return command; -}; - -module.exports = getCommandToLaunchTerminal; diff --git a/plugin/lib/getQuickcommandTempFile.js b/plugin/lib/getQuickcommandFile.js similarity index 51% rename from plugin/lib/getQuickcommandTempFile.js rename to plugin/lib/getQuickcommandFile.js index d30b27fb..e966678f 100644 --- a/plugin/lib/getQuickcommandTempFile.js +++ b/plugin/lib/getQuickcommandFile.js @@ -9,4 +9,15 @@ const getQuickcommandTempFile = (ext, name, dir = "quickcommandTempDir") => { return path.join(tempDir, `${name}.${ext}`); }; -module.exports = getQuickcommandTempFile; +const getQuickcommandFolderFile = (name, ext) => { + const folderPath = path.join( + window.utools.getPath("userData"), + "quickcommand" + ); + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + return path.join(folderPath, `${name}.${ext}`); +}; + +module.exports = { getQuickcommandTempFile, getQuickcommandFolderFile }; diff --git a/plugin/lib/getUtoolsPlugins.js b/plugin/lib/getUtoolsPlugins.js index f5c81ba8..d6579e76 100644 --- a/plugin/lib/getUtoolsPlugins.js +++ b/plugin/lib/getUtoolsPlugins.js @@ -1,10 +1,11 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); -const _ = require("lodash"); const pluginInfo = () => { - return JSON.parse(fs.readFileSync(path.join(__dirname, "plugin.json"))); + return JSON.parse( + fs.readFileSync(path.resolve(__dirname, "..", "plugin.json")) + ); }; const getUtoolsPlugins = () => { diff --git a/plugin/lib/lodashMini.js b/plugin/lib/lodashMini.js index 0c49d174..f9cab6a3 100644 --- a/plugin/lib/lodashMini.js +++ b/plugin/lib/lodashMini.js @@ -249,27 +249,6 @@ const lodashMini = { ); }, - /** - * 移除数组中所有给定值 - * @param {Array} array - 要修改的数组 - * @param {...*} values - 要移除的值 - * @returns {Array} 返回数组本身 - * @example - * pull([1, 2, 3, 1, 2, 3], 2, 3) => [1, 1] - */ - pull: function (array, ...values) { - if (!array || !array.length || !values.length) return array; - let i = 0; - while (i < array.length) { - if (values.includes(array[i])) { - array.splice(i, 1); - } else { - i++; - } - } - return array; - }, - /** * 截断字符串 * @param {string} [string=''] - 要截断的字符串 diff --git a/plugin/lib/quickcommand.js b/plugin/lib/quickcommand.js index 1bb294d3..30f87a24 100644 --- a/plugin/lib/quickcommand.js +++ b/plugin/lib/quickcommand.js @@ -4,9 +4,22 @@ const fs = require("fs"); const kill = require("tree-kill"); const iconv = require("iconv-lite"); const path = require("path"); +const axios = require("axios"); +const marked = require("marked"); +const { chat, getModels } = require("./ai"); -const getQuickcommandTempFile = require("./getQuickcommandTempFile"); -const getCommandToLaunchTerminal = require("./getCommandToLaunchTerminal"); +const { dbStorage } = utools; + +window.getModelsFromAiApi = getModels; + +const systemDialog = require("./dialog/service"); + +const { + getQuickcommandTempFile, + getQuickcommandFolderFile, +} = require("./getQuickcommandFile"); + +const createTerminalCommand = require("./createTerminalCommand"); const ctlKey = window.utools.isMacOs() ? "command" : "control"; @@ -34,7 +47,6 @@ const quickcommand = { // setTimout 不能在 vm2 中使用,同时在 electron 中有 bug sleep: function (ms) { - var start = new Date().getTime(); try { // node 16.13.1 child_process.execSync(getSleepCodeByShell(ms), { @@ -42,23 +54,32 @@ const quickcommand = { windowsHide: true, }); } catch (ex) {} - var end = new Date().getTime(); - return end - start; + return; }, // 重写 setTimeout setTimeout: function (callback, ms) { - var start = new Date().getTime(); - child_process.exec( + const child = child_process.exec( getSleepCodeByShell(ms), { timeout: ms, }, - (err, stdout, stderr) => { - var end = new Date().getTime(); - callback(end - start); + () => { + if (child.signalCode === "SIGKILL") return; + callback(); } ); + return child.pid; + }, + + clearTimeout: function (pid) { + kill(pid, "SIGKILL"); + }, + + asyncSleep: async function (ms) { + return new Promise((resolve) => { + this.setTimeout(resolve, ms); + }); }, // 关闭进程 @@ -71,10 +92,15 @@ const quickcommand = { return new DOMParser().parseFromString(html, "text/html"); }, + // markdown 解析 + markdownParse: function (markdown) { + return marked.parse(markdown); + }, + // 下载文件 - downloadFile: function (url, file = {}) { + downloadFile: function (url, file) { return new Promise((reslove, reject) => { - if (file instanceof Object) + if (!file || file instanceof Object) file = window.utools.showSaveDialog(JSON.parse(JSON.stringify(file))); axios({ method: "get", @@ -95,13 +121,13 @@ const quickcommand = { }, // 上传文件 - uploadFile: function (url, file = {}, name = "file", formData = {}) { + uploadFile: function (url, file, name = "file", formData = {}) { return new Promise((reslove, reject) => { var objfile; if (file instanceof File) { objfile = file; } else { - if (file instanceof Object) + if (!file || file instanceof Object) file = window.utools.showOpenDialog( JSON.parse(JSON.stringify(file)) )[0]; @@ -130,18 +156,26 @@ const quickcommand = { }, // 载入在线资源 - loadRemoteScript: async function (url) { - if ( - !/^((ht|f)tps?):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?/.test( - url - ) - ) - throw "url 不合法"; - let local = getQuickcommandTempFile("js"); - await this.downloadFile(url, local); - let source = require(local); - fs.unlinkSync(local); - return source; + loadRemoteScript: async function (url, options) { + const urlReg = + /^((ht|f)tps?):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?/; + if (!urlReg.test(url)) throw "url 不合法"; + const { useCache = false } = options; + if (useCache) { + const urlHash = quickcomposer.coding.md5Hash(url); + const fileName = path.basename(url, ".js") + "." + urlHash.slice(8, -8); + const local = getQuickcommandFolderFile(fileName, "js"); + if (!fs.existsSync(local)) { + await this.downloadFile(url, local); + } + return require(local); + } else { + const local = getQuickcommandTempFile("js"); + await this.downloadFile(url, local); + let source = require(local); + fs.unlinkSync(local); + return source; + } }, // 唤醒 uTools @@ -174,13 +208,22 @@ const quickcommand = { } return null; }, + + askAI: async function (content, apiConfig, options) { + if (window.lodashM.isEmpty(apiConfig)) { + apiConfig = dbStorage.getItem("cfg_aiConfigs")?.[0] || {}; + } + return await chat(content, apiConfig, options); + }, + + ...systemDialog, }; if (process.platform === "win32") { // 运行vbs脚本 quickcommand.runVbs = function (script) { return new Promise((reslove, reject) => { - var tempfile = getQuickcommandTempFile("vbs", "TempVBSScript"); + var tempfile = getQuickcommandTempFile("vbs"); fs.writeFile(tempfile, iconv.encode(script, "gbk"), () => { child_process.exec( `cscript.exe /nologo "${tempfile}"`, @@ -214,53 +257,6 @@ if (process.platform === "win32") { ); }); }; - - // 运行C#脚本 - quickcommand.runCsharp = function (script) { - return new Promise((reslove, reject) => { - // 找到csc.exe - let cscPath = path.join( - process.env.WINDIR, - "Microsoft.NET", - "Framework", - "v4.0.30319", - "csc.exe" - ); - if (!fs.existsSync(cscPath)) { - cscPath = path.join( - process.env.WINDIR, - "Microsoft.NET", - "Framework", - "v3.5", - "csc.exe" - ); - } - if (!fs.existsSync(cscPath)) { - return reject("未安装.NET Framework"); - } - // 写入临时文件 - let tempCsharpFile = getQuickcommandTempFile("cs", "TempCsharpScript"); - let tempBuildFile = getQuickcommandTempFile("exe", "TempCsharpBuildExe"); - - fs.writeFile(tempCsharpFile, iconv.encode(script, "gbk"), (err) => { - if (err) return reject(err.toString()); - // 运行csc.exe - child_process.exec( - `${cscPath} /nologo /out:${tempBuildFile} ${tempCsharpFile} && ${tempBuildFile}`, - { - encoding: "buffer", - windowsHide: true, - }, - (err, stdout) => { - if (err) reject(iconv.decode(stdout, "gbk")); - else reslove(iconv.decode(stdout, "gbk")); - fs.unlink(tempCsharpFile, () => {}); - fs.unlink(tempBuildFile, () => {}); - } - ); - }); - }); - }; } if (process.platform === "darwin") { @@ -295,11 +291,14 @@ window.runPythonCommand = (py) => { } }; -// 在终端中执行 -if (process.platform !== "linux") - quickcommand.runInTerminal = function (cmdline, dir) { - let command = getCommandToLaunchTerminal(cmdline, dir); +if (process.platform !== "linux") { + // 在终端中执行 + quickcommand.runInTerminal = function (cmdline, options) { + // 兼容老版本接口, 老版本第二个参数是dir + if (typeof options === "string") options = { dir: options }; + let command = createTerminalCommand(cmdline, options); child_process.exec(command); }; +} module.exports = quickcommand; diff --git a/plugin/lib/quickcomposer.js b/plugin/lib/quickcomposer.js index f19007c5..8c05e619 100644 --- a/plugin/lib/quickcomposer.js +++ b/plugin/lib/quickcomposer.js @@ -4,6 +4,15 @@ const quickcomposer = { file: require("./quickcomposer/file"), system: require("./quickcomposer/system"), network: require("./quickcomposer/network"), + coding: require("./quickcomposer/coding"), + math: require("./quickcomposer/math"), + audio: require("./quickcomposer/audio"), + image: require("./quickcomposer/image"), + windows: require("./quickcomposer/windows"), + macos: require("./quickcomposer/macos"), + status: require("./quickcomposer/status"), + browser: require("./quickcomposer/browser"), + video: require("./quickcomposer/video"), }; module.exports = quickcomposer; diff --git a/plugin/lib/quickcomposer/audio/index.js b/plugin/lib/quickcomposer/audio/index.js new file mode 100644 index 00000000..0ddb2621 --- /dev/null +++ b/plugin/lib/quickcomposer/audio/index.js @@ -0,0 +1,9 @@ +const speech = require("./speech"); +const media = require("./media"); +const record = require("./record"); + +module.exports = { + speech, + media, + ...record, +}; diff --git a/plugin/lib/quickcomposer/audio/media.js b/plugin/lib/quickcomposer/audio/media.js new file mode 100644 index 00000000..bb0952cd --- /dev/null +++ b/plugin/lib/quickcomposer/audio/media.js @@ -0,0 +1,166 @@ +const { spawn } = require("child_process"); +const fs = require("fs"); + +// 存储当前播放的音频实例 +let currentAudio = null; + +// 系统音效映射 +const SYSTEM_SOUNDS = { + beep: { + win: "Windows Ding", + mac: "Ping.aiff", + }, + error: { + win: "Windows Critical Stop", + mac: "Basso.aiff", + }, + warning: { + win: "Windows Exclamation", + mac: "Sosumi.aiff", + }, + notification: { + win: "Windows Notify", + mac: "Glass.aiff", + }, + complete: { + win: "Windows Print complete", + mac: "Hero.aiff", + }, + click: { + win: "Windows Navigation Start", + mac: "Tink.aiff", + }, +}; + +/** + * 播放音频文件 + * @param {string} file 音频文件路径 + * @param {number} volume 音量 (0-1) + * @param {boolean} loop 是否循环播放 + * @param {boolean} autoplay 是否自动播放 + */ +async function play(file, volume = 1, loop = false, autoplay = true) { + // 先停止当前音频 + stop(); + + // 检查文件是否存在 + if (!fs.existsSync(file)) { + throw new Error(`音频文件不存在: ${file}`); + } + + // 创建新的音频实例 + const audio = new Audio(); + + // 设置音频属性 + audio.src = `file://${file}`; + audio.volume = parseFloat(volume) || 1; + audio.loop = !!loop; + + // 保存当前实例 + currentAudio = audio; + + // 如果设置了自动播放 + if (autoplay !== false) { + try { + await audio.play(); + } catch (error) { + console.warn("播放失败:", error); + currentAudio = null; + throw error; + } + } + + // 立即返回,不等待播放完成 + return Promise.resolve(); +} + +/** + * 停止音频播放 + */ +function stop() { + if (currentAudio) { + try { + currentAudio.pause(); + currentAudio.currentTime = 0; + } catch (error) { + console.warn("停止播放时发生错误:", error); + } finally { + currentAudio = null; + } + } +} + +/** + * 播放系统音效 + * @param {string} type 音效类型 + * @param {number} volume 音量 (0-1) + */ +async function beep(type = "beep", volume = 1) { + // 在 Windows 上使用 PowerShell 播放系统音效 + if (process.platform === "win32") { + const soundName = SYSTEM_SOUNDS[type]?.win || SYSTEM_SOUNDS.beep.win; + // 使用 PowerShell 播放 Windows 系统音效 + const script = `(New-Object System.Media.SoundPlayer "C:\\Windows\\Media\\${soundName}.wav").PlaySync()`; + + return new Promise((resolve, reject) => { + const ps = spawn("powershell", ["-Command", script]); + ps.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`PowerShell 命令执行失败,退出码: ${code}`)); + }); + }); + } + // 在 macOS 上使用 afplay 播放系统音效 + else if (process.platform === "darwin") { + const soundName = SYSTEM_SOUNDS[type]?.mac || SYSTEM_SOUNDS.beep.mac; + volume = parseFloat(volume) || 1; + return new Promise((resolve, reject) => { + const afplay = spawn("afplay", [ + `/System/Library/Sounds/${soundName}`, + "-v", + volume, + ]); + afplay.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`afplay 命令执行失败,退出码: ${code}`)); + }); + }); + } + // 在其他平台上使用 utools.shellBeep + else { + utools.shellBeep(); + return Promise.resolve(); + } +} + +/** + * 分析音频文件 + * @param {string} file 音频文件路径 + * @returns {Promise} 音频信息 + */ +async function analyze(file) { + if (!fs.existsSync(file)) { + throw new Error(`音频文件不存在: ${file}`); + } + + const audio = new Audio(); + audio.src = `file://${file}`; + + return new Promise((resolve, reject) => { + audio.onloadedmetadata = () => { + resolve({ + duration: audio.duration, + channels: audio.mozChannels || 2, + sampleRate: audio.mozSampleRate || 44100, + }); + }; + audio.onerror = reject; + }); +} + +module.exports = { + play, + beep, + stop, + analyze, +}; diff --git a/plugin/lib/quickcomposer/audio/record.js b/plugin/lib/quickcomposer/audio/record.js new file mode 100644 index 00000000..64ec1cb6 --- /dev/null +++ b/plugin/lib/quickcomposer/audio/record.js @@ -0,0 +1,44 @@ +const fs = require("fs"); + +/** + * 录制音频 + * @param {number} duration 录制时长(ms) + * @param {string} savePath 保存路径 + */ +async function record(duration = 5000, savePath) { + const format = "audio/webm"; + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const mediaRecorder = new MediaRecorder(stream, { mimeType: format }); + const chunks = []; + + return new Promise((resolve, reject) => { + mediaRecorder.ondataavailable = (e) => chunks.push(e.data); + mediaRecorder.onstop = async () => { + try { + const blob = new Blob(chunks, { type: format }); + if (savePath) { + // 使用 FileReader 读取 blob 数据 + const reader = new FileReader(); + reader.onload = () => { + const buffer = Buffer.from(reader.result); + fs.writeFileSync(savePath, buffer); + }; + reader.readAsArrayBuffer(blob); + } + stream.getTracks().forEach((track) => track.stop()); + resolve(blob); + } catch (error) { + reject(error); + } + }; + mediaRecorder.onerror = reject; + + mediaRecorder.start(); + setTimeout(() => mediaRecorder.stop(), duration); + }); +} + +module.exports = { + record, +}; diff --git a/plugin/lib/quickcomposer/audio/speech.js b/plugin/lib/quickcomposer/audio/speech.js new file mode 100644 index 00000000..508025bb --- /dev/null +++ b/plugin/lib/quickcomposer/audio/speech.js @@ -0,0 +1,59 @@ +// 存储当前朗读实例 +let currentSpeech = null; + +/** + * 朗读文本 + * @param {string} text 要朗读的文本 + * @param {Object} options 朗读选项 + * @param {number} options.rate 语速 (0.1-10) + * @param {number} options.pitch 音调 (0-2) + * @param {number} options.volume 音量 (0-1) + * @param {string} options.lang 语言 + */ +async function speak(text, options = {}) { + // 停止当前朗读 + stop(); + + // 创建新的语音合成实例 + const speech = new window.SpeechSynthesisUtterance(); + speech.text = text; + speech.rate = parseFloat(options.rate) || 1; + speech.pitch = parseFloat(options.pitch) || 1; + speech.volume = parseFloat(options.volume) || 1; + speech.lang = options.lang || "zh-CN"; + + // 保存当前实例 + currentSpeech = speech; + + // 开始朗读 + window.speechSynthesis.speak(speech); + + // 返回 Promise,在朗读结束时 resolve + return new Promise((resolve, reject) => { + speech.onend = () => { + currentSpeech = null; + resolve(); + }; + speech.onerror = (error) => { + currentSpeech = null; + reject(error); + }; + }); +} + +/** + * 停止文本朗读 + */ +function stop() { + if (window.speechSynthesis) { + window.speechSynthesis.cancel(); + } + if (currentSpeech) { + currentSpeech = null; + } +} + +module.exports = { + speak, + stop, +}; diff --git a/plugin/lib/quickcomposer/browser/cdp.js b/plugin/lib/quickcomposer/browser/cdp.js new file mode 100644 index 00000000..609deb95 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/cdp.js @@ -0,0 +1,48 @@ +const CDP = require("chrome-remote-interface"); +const { getCurrentClientPort } = require("./client"); + +const initCDP = async (targetId) => { + try { + const port = await getCurrentClientPort(); + const client = await CDP({ + target: targetId, + port, + }); + + const { Page, Runtime, Target, Network, Emulation, DOM } = client; + await Promise.all([ + Page.enable(), + Runtime.enable(), + DOM.enable(), + ]); + + return { + client, + Page, + Runtime, + Target, + Network, + Emulation, + DOM, + }; + } catch (err) { + console.log(err); + throw new Error(`连接到浏览器失败: ${err.message}`); + } +}; + +const cleanupCDP = async (targetId) => { + try { + // 直接关闭传入的 client + if (targetId?.client) { + await targetId.client.close(); + } + } catch (error) { + console.log("关闭CDP连接失败:", error); + } +}; + +module.exports = { + initCDP, + cleanupCDP, +}; diff --git a/plugin/lib/quickcomposer/browser/client.js b/plugin/lib/quickcomposer/browser/client.js new file mode 100644 index 00000000..df5451df --- /dev/null +++ b/plugin/lib/quickcomposer/browser/client.js @@ -0,0 +1,288 @@ +const { exec } = require("child_process"); +const path = require("path"); +const os = require("os"); +const fs = require("fs"); +const net = require("net"); +const CDP = require("chrome-remote-interface"); + +let currentClientPort = null; + +const getBrowserPath = (browser = "msedge") => { + const platform = os.platform(); + let paths = null; + if (platform === "win32") { + paths = { + chrome: [ + path.join( + process.env["ProgramFiles"], + "Google/Chrome/Application/chrome.exe" + ), + path.join( + process.env["ProgramFiles(x86)"], + "Google/Chrome/Application/chrome.exe" + ), + path.join( + process.env["LocalAppData"], + "Google/Chrome/Application/chrome.exe" + ), + ], + msedge: [ + path.join( + process.env["ProgramFiles"], + "Microsoft/Edge/Application/msedge.exe" + ), + path.join( + process.env["ProgramFiles(x86)"], + "Microsoft/Edge/Application/msedge.exe" + ), + path.join( + process.env["LocalAppData"], + "Microsoft/Edge/Application/msedge.exe" + ), + ], + }; + } else if (platform === "darwin") { + paths = { + chrome: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"], + msedge: [ + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + ], + }; + } else if (platform === "linux") { + paths = { + chrome: [ + "/opt/google/chrome/chrome", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + ], + msedge: [ + "/opt/microsoft/msedge/msedge", + "/usr/bin/microsoft-edge", + "/usr/bin/microsoft-edge-stable", + ], + }; + } else { + throw new Error("不支持的操作系统"); + } + return paths[browser].find((p) => fs.existsSync(p)); +}; + +const isPortAvailable = (port) => { + return new Promise((resolve) => { + const socket = new net.Socket(); + + const onError = () => { + socket.destroy(); + resolve(true); + }; + + socket.setTimeout(100); + socket.once("error", onError); + socket.once("timeout", onError); + + socket.connect(port, "127.0.0.1", () => { + socket.destroy(); + resolve(false); + }); + }); +}; + +const waitForPort = async (port, timeout = 30000) => { + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + try { + await CDP.Version({ port }); + return true; + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + return false; +}; + +const findAvailablePort = async (startPort) => { + let port = startPort; + while (port < startPort + 100) { + const available = await isPortAvailable(port); + if (available) { + return port; + } + port++; + } + throw new Error("无法找到可用的调试端口"); +}; + +const startClient = async (options) => { + const { + browserType = "msedge", + useSingleUserDataDir = true, + proxy = null, + browserPath = getBrowserPath(browserType), + windowSize = null, + windowPosition = null, + incognito = false, + headless = false, + disableExtensions = false, + } = options; + + if (!browserPath) { + throw new Error("未找到浏览器,或未指定浏览器路径"); + } + + const port = await findAvailablePort(9222); + setCurrentClientPort(port); + + const automationArgs = [ + `--remote-debugging-port=${port}`, + "--disable-infobars", + "--disable-notifications", + "--disable-popup-blocking", + "--disable-save-password-bubble", + "--disable-translate", + "--no-first-run", + "--no-default-browser-check", + "--user-data-start-with-quickcomposer", + ]; + + const incognitoArg = { + chrome: "--incognito", + msedge: "--inprivate", + }; + + const optionArgs = [ + windowSize ? `--window-size=${windowSize}` : "--start-maximized", + windowPosition ? `--window-position=${windowPosition}` : "", + proxy ? `--proxy-server=${proxy}` : "", + incognito ? incognitoArg[browserType] : "", + headless ? "--headless" : "", + disableExtensions ? "--disable-extensions" : "", + useSingleUserDataDir + ? `--user-data-dir=${path.join( + os.tmpdir(), + `${browserType}-debug-${port}` + )}` + : "", + ].filter(Boolean); + + const args = [...automationArgs, ...optionArgs]; + + return new Promise(async (resolve, reject) => { + if (!useSingleUserDataDir) { + try { + await killRunningBrowser(browserType); + } catch (e) { + reject(e); + return; + } + } + const child = exec( + `"${browserPath}" ${args.join(" ")}`, + { windowsHide: true }, + async (error) => { + if (error) { + reject(error); + return; + } + } + ); + + waitForPort(port).then((success) => { + if (success) { + resolve({ pid: child.pid, port }); + } else { + reject(new Error("浏览器启动超时,请检查是否有权限问题或防火墙限制")); + } + }); + }); +}; + +const killRunningBrowser = (browserType = "msedge") => { + return new Promise((resolve, reject) => { + if (os.platform() === "win32") { + exec(`taskkill /F /IM ${browserType}.exe`, (error) => { + if (error) reject(error); + else resolve(); + }); + } else { + exec(`kill -9 $(pgrep ${browserType})`, (error) => { + if (error) reject(error); + else resolve(); + }); + } + }); +}; + +const destroyClientByPort = async (port) => { + const currentPort = await getCurrentClientPort(); + if (!port) { + port = currentPort; + } + try { + const client = await CDP({ port }); + await client.Browser.close(); + + if (port === currentPort) { + setCurrentClientPort(null); + } + } catch (error) { + throw new Error(`销毁客户端失败,请手动关闭`); + } +}; + +const switchClientByPort = async (port) => { + try { + const versionInfo = await CDP.Version({ port }); + if (!versionInfo) { + throw new Error(`端口 ${port} 未找到活动的浏览器实例`); + } + setCurrentClientPort(port); + } catch (error) { + throw new Error(`切换客户端失败: ${error.message}`); + } +}; + +const getClientPorts = async () => { + try { + // 创建所有端口检查的 Promise 数组 + const portChecks = []; + for (let port = 9222; port < 9322; port++) { + portChecks.push( + CDP.List({ port }) + .then(() => port) + .catch(() => null) + ); + } + + // 如果不需要返回第一个端口或没有找到可用端口,并行执行所有检查 + const results = await Promise.all(portChecks); + + // 过滤出可用的端口 + return results.filter((port) => port !== null); + } catch (error) { + throw new Error(`获取客户端列表失败: ${error.message}`); + } +}; + +const getCurrentClientPort = async () => { + if (currentClientPort === null) { + const ports = await getClientPorts(); + if (!ports || ports.length === 0) { + throw new Error("未找到可用的浏览器实例,请先从实例管理里面启动新的实例"); + } + currentClientPort = ports[0]; + } + return currentClientPort; +}; + +const setCurrentClientPort = (port) => { + currentClientPort = port; +}; + + +module.exports = { + startClient, + destroyClientByPort, + switchClientByPort, + getClientPorts, + getCurrentClientPort, +}; diff --git a/plugin/lib/quickcomposer/browser/cookie.js b/plugin/lib/quickcomposer/browser/cookie.js new file mode 100644 index 00000000..254e5890 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/cookie.js @@ -0,0 +1,40 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +const setCookie = async (tab, cookies, options = {}) => { + const target = await searchTarget(tab); + const { Network, Page } = await initCDP(target.id); + try { + const { frameTree } = await Page.getFrameTree(); + const url = frameTree.frame.url; + + for (const cookie of cookies) { + await Network.setCookie({ + name: cookie.name, + value: cookie.value, + domain: options.domain || url.split("/")[2], + path: options.path || "/", + secure: options.secure || false, + expires: options.expires + ? Math.floor(Date.now() / 1000) + options.expires * 3600 + : undefined, + }); + } + } finally { + await cleanupCDP(target.id); + } +}; + +const getCookie = async (tab, name) => { + const target = await searchTarget(tab); + const { Network } = await initCDP(target.id); + const { cookies } = await Network.getCookies(); + await cleanupCDP(target.id); + if (!name) return cookies; + return cookies.find((cookie) => cookie.name === name); +}; + +module.exports = { + setCookie, + getCookie, +}; diff --git a/plugin/lib/quickcomposer/browser/device.js b/plugin/lib/quickcomposer/browser/device.js new file mode 100644 index 00000000..fa8a650f --- /dev/null +++ b/plugin/lib/quickcomposer/browser/device.js @@ -0,0 +1,301 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +// 预定义的设备列表 +const devices = { + // iOS 设备 + "iPhone X": { + userAgent: + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + viewport: { + width: 375, + height: 812, + deviceScaleFactor: 3, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "iPhone 12 Pro": { + userAgent: + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "iPhone 14 Pro Max": { + userAgent: + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + viewport: { + width: 430, + height: 932, + deviceScaleFactor: 3, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "iPad Pro": { + userAgent: + "Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + viewport: { + width: 1024, + height: 1366, + deviceScaleFactor: 2, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "iPad Mini": { + userAgent: + "Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + viewport: { + width: 768, + height: 1024, + deviceScaleFactor: 2, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + + // Android 设备 + "Pixel 5": { + userAgent: + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36", + viewport: { + width: 393, + height: 851, + deviceScaleFactor: 2.75, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "Pixel 7": { + userAgent: + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36", + viewport: { + width: 412, + height: 915, + deviceScaleFactor: 2.625, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "Samsung Galaxy S20 Ultra": { + userAgent: + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36", + viewport: { + width: 412, + height: 915, + deviceScaleFactor: 3.5, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "Samsung Galaxy Tab S7": { + userAgent: + "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + viewport: { + width: 1600, + height: 2560, + deviceScaleFactor: 2, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "Xiaomi 12 Pro": { + userAgent: + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36", + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + "HUAWEI Mate30 Pro": { + userAgent: + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36", + viewport: { + width: 392, + height: 835, + deviceScaleFactor: 3, + mobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + + // 桌面设备 + Desktop: { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + viewport: { + width: 1920, + height: 1080, + deviceScaleFactor: 1, + mobile: false, + hasTouch: false, + isLandscape: false, + }, + }, + "MacBook Pro 16": { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + viewport: { + width: 1728, + height: 1117, + deviceScaleFactor: 2, + mobile: false, + hasTouch: false, + isLandscape: false, + }, + }, + "4K Display": { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + viewport: { + width: 3840, + height: 2160, + deviceScaleFactor: 2, + mobile: false, + hasTouch: false, + isLandscape: false, + }, + }, +}; + +// 设置设备模拟 +const setDevice = async (tab, deviceName) => { + const target = await searchTarget(tab); + const { Network, Emulation } = await initCDP(target.id); + + try { + const device = devices[deviceName]; + if (!device) { + throw new Error(`未找到设备配置: ${deviceName}`); + } + + // 设置 User Agent + await Network.setUserAgentOverride({ + userAgent: device.userAgent, + }); + + // 设置视口 + await Emulation.setDeviceMetricsOverride({ + ...device.viewport, + screenWidth: device.viewport.width, + screenHeight: device.viewport.height, + }); + + // 设置触摸事件模拟 + if (device.viewport.hasTouch) { + await Emulation.setTouchEmulationEnabled({ + enabled: true, + maxTouchPoints: 5, + }); + } + } finally { + await cleanupCDP(target.id); + } +}; + +// 自定义设备模拟 +const setCustomDevice = async (tab, options) => { + const target = await searchTarget(tab); + const { Network, Emulation } = await initCDP(target.id); + + try { + const { + userAgent, + width = 1920, + height = 1080, + deviceScaleFactor = 1, + mobile = false, + hasTouch = false, + isLandscape = false, + } = options; + + // 设置 User Agent + if (userAgent) { + await Network.setUserAgentOverride({ + userAgent, + }); + } + + // 设置视口 + await Emulation.setDeviceMetricsOverride({ + width, + height, + deviceScaleFactor, + mobile, + isLandscape, + screenWidth: width, + screenHeight: height, + }); + + // 设置触摸事件模拟 + if (hasTouch) { + await Emulation.setTouchEmulationEnabled({ + enabled: true, + maxTouchPoints: 5, + }); + } + } finally { + await cleanupCDP(target.id); + } +}; + +// 清除设备模拟 +const clearDeviceEmulation = async (tab) => { + const target = await searchTarget(tab); + const { Network, Emulation } = await initCDP(target.id); + + try { + // 先禁用触摸事件模拟 + await Emulation.setTouchEmulationEnabled({ + enabled: false, + }); + + // 清除 User Agent 覆盖 + await Network.setUserAgentOverride({ + userAgent: "", + }); + + // 重置设备指标 + await Emulation.setDeviceMetricsOverride({ + width: 0, + height: 0, + deviceScaleFactor: 0, + mobile: false, + screenWidth: 0, + screenHeight: 0, + }); + + // 清除设备指标覆盖 + await Emulation.clearDeviceMetricsOverride(); + } catch (error) { + console.error("清除设备模拟失败:", error); + } finally { + await cleanupCDP(target.id); + } +}; + +module.exports = { + setDevice, + setCustomDevice, + clearDeviceEmulation, +}; diff --git a/plugin/lib/quickcomposer/browser/execScript.js b/plugin/lib/quickcomposer/browser/execScript.js new file mode 100644 index 00000000..b6106818 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/execScript.js @@ -0,0 +1,228 @@ +const fs = require("fs"); +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +const executeScript = async (tab, script, args = {}) => { + const target = await searchTarget(tab); + try { + const { Runtime } = await initCDP(target.id); + const argNames = Object.keys(args); + const argValues = Object.values(args).map((v) => JSON.stringify(v)); + + const wrappedScript = ` + (async function(${argNames.join(", ")}) { + ${script} + })(${argValues.join(", ")}) + `; + + const { result } = await Runtime.evaluate({ + expression: wrappedScript, + returnByValue: true, + awaitPromise: true, + }); + + await cleanupCDP(target.id); + return result.value; + } catch (e) { + console.log(e); + throw new Error("执行脚本失败"); + } +}; + +const clickElement = async (tab, selector) => { + return await executeScript( + tab, + `document.querySelector('${selector}').click()` + ); +}; + +const inputText = async (tab, selector, text) => { + return await executeScript( + tab, + ` + const el = document.querySelector('${selector}'); + el.value = '${text}'; + el.dispatchEvent(new Event('input')); + el.dispatchEvent(new Event('change')); + ` + ); +}; + +const submitForm = async (tab, buttonSelector, inputSelectors) => { + return await executeScript( + tab, + ` + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + let el = null; + ${inputSelectors + .map( + (i) => + `el = document.querySelector('${i.selector}'); + el.value = '${i.value}'; + el.dispatchEvent(new Event('input')); + el.dispatchEvent(new Event('change'));` + ) + .join("await sleep(200);")} + await sleep(200); + document.querySelector('${buttonSelector}').click(); + ` + ); +}; + +const getText = async (tab, selector) => { + return await executeScript( + tab, + `const element = document.querySelector('${selector}'); + return element?.textContent || element?.innerText || '';` + ); +}; + +const getHtml = async (tab, selector) => { + return await executeScript( + tab, + `const element = document.querySelector('${selector}'); + return element?.outerHTML || '';` + ); +}; + +const hideElement = async (tab, selector) => { + return await executeScript( + tab, + `document.querySelector('${selector}').style.display = 'none'` + ); +}; + +const showElement = async (tab, selector) => { + return await executeScript( + tab, + `document.querySelector('${selector}').style.display = ''` + ); +}; + +const scrollTo = async (tab, x, y) => { + return await executeScript(tab, `window.scrollTo(${x}, ${y})`); +}; + +const scrollToElement = async (tab, selector) => { + return await executeScript( + tab, + `document.querySelector('${selector}').scrollIntoView()` + ); +}; + +const getScrollPosition = async (tab) => { + const result = await executeScript( + tab, + ` + return JSON.stringify({ + x: window.pageXOffset || document.documentElement.scrollLeft, + y: window.pageYOffset || document.documentElement.scrollTop + }); + ` + ); + return JSON.parse(result); +}; + +const getPageSize = async (tab) => { + const result = await executeScript( + tab, + ` + return JSON.stringify({ + width: Math.max( + document.documentElement.scrollWidth, + document.documentElement.clientWidth + ), + height: Math.max( + document.documentElement.scrollHeight, + document.documentElement.clientHeight + ) + }); + ` + ); + return JSON.parse(result); +}; + +const waitForElement = async (tab, selector, timeout = 5000) => { + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + const result = await executeScript( + tab, + `!!document.querySelector('${selector}')` + ); + if (result) return; + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error(`等待元素 ${selector} 超时`); +}; + +const injectCSS = async (tab, css) => { + return await executeScript( + tab, + ` + const style = document.createElement('style'); + style.textContent = css; + document.head.appendChild(style); + `, + { css } + ); +}; + +const injectRemoteScript = async (tab, url) => { + return await executeScript( + tab, + ` + return await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + script.type = 'text/javascript'; + script.onload = () => resolve(true); + script.onerror = () => reject(new Error('Failed to load script: ' + url)); + document.head.appendChild(script); + }); + `, + { url } + ); +}; + +const injectLocalScript = async (tab, filePath) => { + try { + if (!fs.existsSync(filePath)) { + throw new Error(`文件不存在: ${filePath}`); + } + + const content = fs.readFileSync(filePath, "utf8"); + + return await executeScript( + tab, + ` + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.textContent = content; + document.head.appendChild(script); + return true; + `, + { content } + ); + } catch (error) { + throw new Error(`注入本地脚本失败: ${error.message}`); + } +}; + +module.exports = { + executeScript, + clickElement, + inputText, + submitForm, + getText, + getHtml, + hideElement, + showElement, + scrollTo, + scrollToElement, + getScrollPosition, + getPageSize, + waitForElement, + injectCSS, + injectRemoteScript, + injectLocalScript, +}; diff --git a/plugin/lib/quickcomposer/browser/getSelector.js b/plugin/lib/quickcomposer/browser/getSelector.js new file mode 100644 index 00000000..6aafddcc --- /dev/null +++ b/plugin/lib/quickcomposer/browser/getSelector.js @@ -0,0 +1,183 @@ +const { executeScript } = require("./execScript"); +const fs = require("fs"); +const path = require("path"); + +const getOptimalSelector = () => { + return ` + // 获取最优选择器 + function getOptimalSelector_secondary(element) { + if (!element || element === document.body) return 'body'; + + // 尝试使用id + if (element.id) { + return '#' + element.id; + } + + // 构建当前元素的选择器 + let currentSelector = element.tagName.toLowerCase(); + if (element.className && typeof element.className === 'string') { + const classes = element.className.trim().split(/\\s+/); + if (classes.length) { + currentSelector += '.' + classes.join('.'); + } + } + + // 1. 尝试仅使用类名组合 + if (element.className && typeof element.className === 'string') { + const classes = element.className.trim().split(/\\s+/); + if (classes.length) { + const classSelector = '.' + classes.join('.'); + if (document.querySelectorAll(classSelector).length === 1) { + return classSelector; + } + } + } + + // 2. 尝试仅使用标签名和类名组合 + if (document.querySelectorAll(currentSelector).length === 1) { + return currentSelector; + } + + // 3. 如果需要使用 nth-child,先尝试简单组合 + const siblings = Array.from(element.parentElement?.children || []); + const index = siblings.indexOf(element); + if (index !== -1) { + const nthSelector = currentSelector + ':nth-child(' + (index + 1) + ')'; + if (document.querySelectorAll(nthSelector).length === 1) { + return nthSelector; + } + } + + // 4. 向上查找最近的有id的祖先元素 + let ancestor = element; + let foundSelectors = []; + + while (ancestor && ancestor !== document.body) { + if (ancestor.id) { + foundSelectors.push({ + selector: '#' + ancestor.id, + element: ancestor + }); + } + + // 收集所有可能有用的类名组合 + if (ancestor.className && typeof ancestor.className === 'string') { + const classes = ancestor.className.trim().split(/\\s+/); + if (classes.length) { + const classSelector = ancestor.tagName.toLowerCase() + '.' + classes.join('.'); + if (document.querySelectorAll(classSelector).length < 10) { // 只收集相对独特的选择器 + foundSelectors.push({ + selector: classSelector, + element: ancestor + }); + } + } + } + + ancestor = ancestor.parentElement; + } + + // 5. 尝试各种组合,找到最短的唯一选择器 + for (const {selector: anchorSelector} of foundSelectors) { + // 尝试直接组合 + const simpleSelector = anchorSelector + ' ' + currentSelector; + if (document.querySelectorAll(simpleSelector).length === 1) { + return simpleSelector; + } + + // 如果直接组合不唯一,尝试加上 nth-child + if (index !== -1) { + const nthSelector = anchorSelector + ' ' + currentSelector + ':nth-child(' + (index + 1) + ')'; + if (document.querySelectorAll(nthSelector).length === 1) { + return nthSelector; + } + } + } + + // 6. 如果还是找不到唯一选择器,使用两层有特征的选择器组合 + for (let i = 0; i < foundSelectors.length - 1; i++) { + for (let j = i + 1; j < foundSelectors.length; j++) { + const combinedSelector = foundSelectors[i].selector + ' ' + foundSelectors[j].selector + ' ' + currentSelector; + if (document.querySelectorAll(combinedSelector).length === 1) { + return combinedSelector; + } + } + } + + // 7. 最后的后备方案:使用完整的父子选择器 + const parent = element.parentElement; + if (!parent) return null; + + const parentSelector = getOptimalSelector(parent); + if (!parentSelector) return null; + + return parentSelector + ' ' + currentSelector + (index !== -1 ? ':nth-child(' + (index + 1) + ')' : ''); + } + `; +}; + +const getSelector = async (tab) => { + return await executeScript( + tab, + ` + return new Promise((resolve) => { + // 创建高亮元素 + const highlight = document.createElement('div'); + highlight.style.cssText = 'position: fixed; pointer-events: none; z-index: 10000; background: rgba(130, 180, 230, 0.4); border: 2px solid rgba(130, 180, 230, 0.8); transition: all 0.2s;'; + document.body.appendChild(highlight); + + if (typeof OptimalSelect === 'undefined') { + ${fs.readFileSync(path.join(__dirname, "optimalSelect.js"), "utf-8")} + } + + function getOptimalSelector(element) { + return OptimalSelect.select(element) + } + + ${getOptimalSelector()} + + // 处理鼠标移动 + function handleMouseMove(e) { + const target = e.target; + if (!target || target === highlight) return; + + const rect = target.getBoundingClientRect(); + highlight.style.left = rect.left + 'px'; + highlight.style.top = rect.top + 'px'; + highlight.style.width = rect.width + 'px'; + highlight.style.height = rect.height + 'px'; + } + + // 处理点击 + function handleClick(e) { + e.preventDefault(); + e.stopPropagation(); + + const target = e.target; + if (!target || target === highlight) return; + let selector = null; + try { + selector = getOptimalSelector(target); + } catch (e) { + selector = getOptimalSelector_secondary(target); + } + + // 清理 + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('click', handleClick, true); + highlight.remove(); + + resolve(selector); + return false; + } + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('click', handleClick, true); + }); + ` + ); +}; + +module.exports = { + getSelector, +}; diff --git a/plugin/lib/quickcomposer/browser/index.js b/plugin/lib/quickcomposer/browser/index.js new file mode 100644 index 00000000..64779964 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/index.js @@ -0,0 +1,21 @@ +const getSelector = require("./getSelector"); +const execScript = require("./execScript"); +const browserManager = require("./client"); +const tabs = require("./tabs"); +const url = require("./url"); +const cookie = require("./cookie"); +const screenshot = require("./screenshot"); +const device = require("./device"); +const network = require("./network"); + +module.exports = { + ...url, + ...tabs, + ...getSelector, + ...execScript, + ...browserManager, + ...cookie, + ...screenshot, + device, + network, +}; diff --git a/plugin/lib/quickcomposer/browser/network.js b/plugin/lib/quickcomposer/browser/network.js new file mode 100644 index 00000000..dc656ad4 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/network.js @@ -0,0 +1,270 @@ +const { searchTarget } = require("./tabs"); +const { getCurrentClientPort } = require("./client"); +const CDP = require("chrome-remote-interface"); + +// 不从cdp.js导入initCDP,单独启用一个Fetch域的CDP +const initCDP = async (targetId) => { + try { + const port = await getCurrentClientPort(); + const client = await CDP({ + target: targetId, + port, + }); + + const { Fetch } = client; + await Promise.all([Fetch.enable()]); + + return { Fetch }; + } catch (err) { + throw new Error(`连接到浏览器失败: ${err.message}`); + } +}; + +const cleanupCDP = async (targetId) => { + try { + // 直接关闭传入的 client + if (targetId?.client) { + await targetId.client.close(); + } + } catch (error) { + console.log("关闭CDP连接失败:", error); + } +}; + +// 使用正则替换内容 +const replaceWithRegex = (content, pattern, replacement) => { + try { + const regex = new RegExp(pattern, "g"); + return content.replace(regex, replacement); + } catch (error) { + return content; + } +}; + +// 存储活动的拦截连接 +let activeInterceptions = new Map(); + +// 将对象格式的 headers 转换为数组格式 +const convertHeaders = (headers) => { + return Object.entries(headers).map(([name, value]) => ({ + name, + value: String(value), + })); +}; + +// 检查 URL 是否匹配规则 +const isUrlMatch = (url, pattern) => { + try { + const regex = new RegExp(pattern); + return regex.test(url); + } catch (error) { + return url.includes(pattern); + } +}; + +// 修改请求 +const setRequestInterception = async (tab, rules) => { + // 先清除所有拦截规则 + await clearInterception(); + + const target = await searchTarget(tab); + const client = await initCDP(target.id); + + try { + await client.Fetch.enable({ + patterns: [{ url: "*", requestStage: "Request" }], + }); + + client.Fetch.requestPaused(async ({ requestId, request }) => { + try { + let modified = null; + + for (const rule of rules) { + if (isUrlMatch(request.url, rule.url)) { + modified = { + url: rule.redirectUrl || request.url, + method: request.method, + headers: { ...request.headers }, + postData: request.postData, + }; + + if (rule.headerKey && rule.headerValue) { + modified.headers[rule.headerKey] = rule.headerValue; + } + + if (rule.pattern && rule.replacement) { + const url = new URL(modified.url); + for (const [key, value] of url.searchParams.entries()) { + const decodedValue = decodeURIComponent(value); + const newValue = replaceWithRegex( + decodedValue, + rule.pattern, + rule.replacement + ); + if (decodedValue !== newValue) { + url.searchParams.set(key, newValue); + } + } + modified.url = url.toString(); + + if (modified.postData) { + modified.postData = replaceWithRegex( + modified.postData, + rule.pattern, + rule.replacement + ); + } + } + break; + } + } + + if (modified) { + await client.Fetch.continueRequest({ + requestId, + url: modified.url, + method: modified.method, + headers: convertHeaders(modified.headers), + postData: modified.postData, + }); + } else { + await client.Fetch.continueRequest({ requestId }); + } + } catch (error) { + await client.Fetch.continueRequest({ requestId }); + } + }); + + activeInterceptions.set("request", client); + return { + success: true, + message: `设置请求拦截规则成功`, + rules, + }; + } catch (error) { + await cleanupCDP(client); + return { + success: false, + message: error.message, + }; + } +}; + +// 修改响应 +const setResponseInterception = async (tab, rules) => { + // 先清除所有拦截规则 + await clearInterception(); + + const target = await searchTarget(tab); + const client = await initCDP(target.id); + + try { + await client.Fetch.enable({ + patterns: [{ url: "*", requestStage: "Response" }], + }); + + client.Fetch.requestPaused( + async ({ requestId, request, responseHeaders, responseStatusCode }) => { + try { + const contentType = responseHeaders.find( + (h) => h.name.toLowerCase() === "content-type" + )?.value; + const isTextContent = contentType && contentType.includes("text"); + + if (!isTextContent) { + await client.Fetch.continueRequest({ requestId }); + return; + } + + let shouldIntercept = false; + let modifiedBody = null; + let modifiedStatus = responseStatusCode; + + for (const rule of rules) { + if (isUrlMatch(request.url, rule.url)) { + shouldIntercept = true; + + if (rule.statusCode) { + modifiedStatus = rule.statusCode; + } + + if (rule.pattern) { + try { + const response = await client.Fetch.getResponseBody({ + requestId, + }); + const originalBody = response.base64Encoded + ? Buffer.from(response.body, "base64").toString() + : response.body; + + if (originalBody) { + modifiedBody = replaceWithRegex( + originalBody, + rule.pattern, + rule.replacement || "" + ); + } + } catch (error) { + shouldIntercept = false; + } + } + } + } + + if (shouldIntercept && modifiedBody !== null) { + await client.Fetch.fulfillRequest({ + requestId, + responseCode: modifiedStatus, + responseHeaders, + body: Buffer.from(modifiedBody).toString("base64"), + }); + } else { + await client.Fetch.continueRequest({ requestId }); + } + } catch (error) { + await client.Fetch.continueRequest({ requestId }); + } + } + ); + + activeInterceptions.set("response", client); + return { + success: true, + message: `设置响应拦截规则成功`, + rules, + }; + } catch (error) { + await cleanupCDP(client); + return { + success: false, + message: error.message, + }; + } +}; + +// 清除所有拦截规则 +const clearInterception = async () => { + if (activeInterceptions.size === 0) { + return { + success: true, + message: `还没有设置拦截规则`, + }; + } + for (const [type, client] of activeInterceptions.entries()) { + try { + await client.Fetch.disable(); + await cleanupCDP(client); + } catch (error) {} + } + activeInterceptions.clear(); + return { + success: true, + message: `清除拦截规则成功`, + }; +}; + +module.exports = { + setRequestInterception, + setResponseInterception, + clearInterception, +}; diff --git a/plugin/lib/quickcomposer/browser/optimalSelect.js b/plugin/lib/quickcomposer/browser/optimalSelect.js new file mode 100644 index 00000000..8aed7025 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/optimalSelect.js @@ -0,0 +1,1767 @@ +// https://cdn.jsdelivr.net/npm/optimal-select@4.0.1/dist/optimal-select.js +(function webpackUniversalModuleDefinition(root, factory) { + if (typeof exports === "object" && typeof module === "object") + module.exports = factory(); + else if (typeof define === "function" && define.amd) define([], factory); + else if (typeof exports === "object") exports["OptimalSelect"] = factory(); + else root["OptimalSelect"] = factory(); +})(this, function () { + return /******/ (function (modules) { + // webpackBootstrap + /******/ // The module cache + /******/ var installedModules = {}; + /******/ + /******/ // The require function + /******/ function __webpack_require__(moduleId) { + /******/ + /******/ // Check if module is in cache + /******/ if (installedModules[moduleId]) + /******/ return installedModules[moduleId].exports; + /******/ + /******/ // Create a new module (and put it into the cache) + /******/ var module = (installedModules[moduleId] = { + /******/ i: moduleId, + /******/ l: false, + /******/ exports: {}, + /******/ + }); + /******/ + /******/ // Execute the module function + /******/ modules[moduleId].call( + module.exports, + module, + module.exports, + __webpack_require__ + ); + /******/ + /******/ // Flag the module as loaded + /******/ module.l = true; + /******/ + /******/ // Return the exports of the module + /******/ return module.exports; + /******/ + } + /******/ + /******/ + /******/ // expose the modules object (__webpack_modules__) + /******/ __webpack_require__.m = modules; + /******/ + /******/ // expose the module cache + /******/ __webpack_require__.c = installedModules; + /******/ + /******/ // identity function for calling harmony imports with the correct context + /******/ __webpack_require__.i = function (value) { + return value; + }; + /******/ + /******/ // define getter function for harmony exports + /******/ __webpack_require__.d = function (exports, name, getter) { + /******/ if (!__webpack_require__.o(exports, name)) { + /******/ Object.defineProperty(exports, name, { + /******/ configurable: false, + /******/ enumerable: true, + /******/ get: getter, + /******/ + }); + /******/ + } + /******/ + }; + /******/ + /******/ // getDefaultExport function for compatibility with non-harmony modules + /******/ __webpack_require__.n = function (module) { + /******/ var getter = + module && module.__esModule + ? /******/ function getDefault() { + return module["default"]; + } + : /******/ function getModuleExports() { + return module; + }; + /******/ __webpack_require__.d(getter, "a", getter); + /******/ return getter; + /******/ + }; + /******/ + /******/ // Object.prototype.hasOwnProperty.call + /******/ __webpack_require__.o = function (object, property) { + return Object.prototype.hasOwnProperty.call(object, property); + }; + /******/ + /******/ // __webpack_public_path__ + /******/ __webpack_require__.p = ""; + /******/ + /******/ // Load entry module and return exports + /******/ return __webpack_require__((__webpack_require__.s = 6)); + /******/ + })( + /************************************************************************/ + /******/ [ + /* 0 */ + /***/ function (module, exports, __webpack_require__) { + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true, + }); + exports.convertNodeList = convertNodeList; + exports.escapeValue = escapeValue; + /** + * # Utilities + * + * Convenience helpers. + */ + + /** + * Create an array with the DOM nodes of the list + * + * @param {NodeList} nodes - [description] + * @return {Array.} - [description] + */ + function convertNodeList(nodes) { + var length = nodes.length; + + var arr = new Array(length); + for (var i = 0; i < length; i++) { + arr[i] = nodes[i]; + } + return arr; + } + + /** + * Escape special characters and line breaks as a simplified version of 'CSS.escape()' + * + * Description of valid characters: https://mathiasbynens.be/notes/css-escapes + * + * @param {String?} value - [description] + * @return {String} - [description] + */ + function escapeValue(value) { + return ( + value && + value + .replace(/['"`\\/:\?&!#$%^()[\]{|}*+;,.<=>@~]/g, "\\$&") + .replace(/\n/g, "A") + ); + } + + /***/ + }, + /* 1 */ + /***/ function (module, exports, __webpack_require__) { + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true, + }); + exports.getCommonAncestor = getCommonAncestor; + exports.getCommonProperties = getCommonProperties; + /** + * # Common + * + * Process collections for similarities. + */ + + /** + * Find the last common ancestor of elements + * + * @param {Array.} elements - [description] + * @return {HTMLElement} - [description] + */ + function getCommonAncestor(elements) { + var options = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : {}; + var _options$root = options.root, + root = _options$root === undefined ? document : _options$root; + + var ancestors = []; + + elements.forEach(function (element, index) { + var parents = []; + while (element !== root) { + element = element.parentNode; + parents.unshift(element); + } + ancestors[index] = parents; + }); + + ancestors.sort(function (curr, next) { + return curr.length - next.length; + }); + + var shallowAncestor = ancestors.shift(); + + var ancestor = null; + + var _loop = function _loop() { + var parent = shallowAncestor[i]; + var missing = ancestors.some(function (otherParents) { + return !otherParents.some(function (otherParent) { + return otherParent === parent; + }); + }); + + if (missing) { + // TODO: find similar sub-parents, not the top root, e.g. sharing a class selector + return "break"; + } + + ancestor = parent; + }; + + for (var i = 0, l = shallowAncestor.length; i < l; i++) { + var _ret = _loop(); + + if (_ret === "break") break; + } + + return ancestor; + } + + /** + * Get a set of common properties of elements + * + * @param {Array.} elements - [description] + * @return {Object} - [description] + */ + function getCommonProperties(elements) { + var commonProperties = { + classes: [], + attributes: {}, + tag: null, + }; + + elements.forEach(function (element) { + var commonClasses = commonProperties.classes, + commonAttributes = commonProperties.attributes, + commonTag = commonProperties.tag; + + // ~ classes + + if (commonClasses !== undefined) { + var classes = element.getAttribute("class"); + if (classes) { + classes = classes.trim().split(" "); + if (!commonClasses.length) { + commonProperties.classes = classes; + } else { + commonClasses = commonClasses.filter(function (entry) { + return classes.some(function (name) { + return name === entry; + }); + }); + if (commonClasses.length) { + commonProperties.classes = commonClasses; + } else { + delete commonProperties.classes; + } + } + } else { + // TODO: restructure removal as 2x set / 2x delete, instead of modify always replacing with new collection + delete commonProperties.classes; + } + } + + // ~ attributes + if (commonAttributes !== undefined) { + (function () { + var elementAttributes = element.attributes; + var attributes = Object.keys(elementAttributes).reduce( + function (attributes, key) { + var attribute = elementAttributes[key]; + var attributeName = attribute.name; + // NOTE: workaround detection for non-standard phantomjs NamedNodeMap behaviour + // (issue: https://github.com/ariya/phantomjs/issues/14634) + if (attribute && attributeName !== "class") { + attributes[attributeName] = attribute.value; + } + return attributes; + }, + {} + ); + + var attributesNames = Object.keys(attributes); + var commonAttributesNames = Object.keys(commonAttributes); + + if (attributesNames.length) { + if (!commonAttributesNames.length) { + commonProperties.attributes = attributes; + } else { + commonAttributes = commonAttributesNames.reduce(function ( + nextCommonAttributes, + name + ) { + var value = commonAttributes[name]; + if (value === attributes[name]) { + nextCommonAttributes[name] = value; + } + return nextCommonAttributes; + }, + {}); + if (Object.keys(commonAttributes).length) { + commonProperties.attributes = commonAttributes; + } else { + delete commonProperties.attributes; + } + } + } else { + delete commonProperties.attributes; + } + })(); + } + + // ~ tag + if (commonTag !== undefined) { + var tag = element.tagName.toLowerCase(); + if (!commonTag) { + commonProperties.tag = tag; + } else if (tag !== commonTag) { + delete commonProperties.tag; + } + } + }); + + return commonProperties; + } + + /***/ + }, + /* 2 */ + /***/ function (module, exports, __webpack_require__) { + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true, + }); + exports.default = optimize; + + var _adapt = __webpack_require__(3); + + var _adapt2 = _interopRequireDefault(_adapt); + + var _utilities = __webpack_require__(0); + + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; + } + + /** + * Apply different optimization techniques + * + * @param {string} selector - [description] + * @param {HTMLElement|Array.} element - [description] + * @param {Object} options - [description] + * @return {string} - [description] + */ + /** + * # Optimize + * + * 1.) Improve efficiency through shorter selectors by removing redundancy + * 2.) Improve robustness through selector transformation + */ + + function optimize(selector, elements) { + var options = + arguments.length > 2 && arguments[2] !== undefined + ? arguments[2] + : {}; + + // convert single entry and NodeList + if (!Array.isArray(elements)) { + elements = !elements.length + ? [elements] + : (0, _utilities.convertNodeList)(elements); + } + + if ( + !elements.length || + elements.some(function (element) { + return element.nodeType !== 1; + }) + ) { + throw new Error( + 'Invalid input - to compare HTMLElements its necessary to provide a reference of the selected node(s)! (missing "elements")' + ); + } + + var globalModified = (0, _adapt2.default)(elements[0], options); + + // chunk parts outside of quotes (http://stackoverflow.com/a/25663729) + var path = selector + .replace(/> /g, ">") + .split(/\s+(?=(?:(?:[^"]*"){2})*[^"]*$)/); + + if (path.length < 2) { + return optimizePart("", selector, "", elements); + } + + var shortened = [path.pop()]; + while (path.length > 1) { + var current = path.pop(); + var prePart = path.join(" "); + var postPart = shortened.join(" "); + + var pattern = prePart + " " + postPart; + var matches = document.querySelectorAll(pattern); + if (matches.length !== elements.length) { + shortened.unshift( + optimizePart(prePart, current, postPart, elements) + ); + } + } + shortened.unshift(path[0]); + path = shortened; + + // optimize start + end + path[0] = optimizePart( + "", + path[0], + path.slice(1).join(" "), + elements + ); + path[path.length - 1] = optimizePart( + path.slice(0, -1).join(" "), + path[path.length - 1], + "", + elements + ); + + if (globalModified) { + delete true; + } + + return path.join(" ").replace(/>/g, "> ").trim(); + } + + /** + * Improve a chunk of the selector + * + * @param {string} prePart - [description] + * @param {string} current - [description] + * @param {string} postPart - [description] + * @param {Array.} elements - [description] + * @return {string} - [description] + */ + function optimizePart(prePart, current, postPart, elements) { + if (prePart.length) prePart = prePart + " "; + if (postPart.length) postPart = " " + postPart; + + // robustness: attribute without value (generalization) + if (/\[*\]/.test(current)) { + var key = current.replace(/=.*$/, "]"); + var pattern = "" + prePart + key + postPart; + var matches = document.querySelectorAll(pattern); + if (compareResults(matches, elements)) { + current = key; + } else { + // robustness: replace specific key-value with base tag (heuristic) + var references = document.querySelectorAll("" + prePart + key); + + var _loop = function _loop() { + var reference = references[i]; + if ( + elements.some(function (element) { + return reference.contains(element); + }) + ) { + var description = reference.tagName.toLowerCase(); + pattern = "" + prePart + description + postPart; + matches = document.querySelectorAll(pattern); + + if (compareResults(matches, elements)) { + current = description; + } + return "break"; + } + }; + + for (var i = 0, l = references.length; i < l; i++) { + var pattern; + var matches; + + var _ret = _loop(); + + if (_ret === "break") break; + } + } + } + + // robustness: descendant instead child (heuristic) + if (/>/.test(current)) { + var descendant = current.replace(/>/, ""); + var pattern = "" + prePart + descendant + postPart; + var matches = document.querySelectorAll(pattern); + if (compareResults(matches, elements)) { + current = descendant; + } + } + + // robustness: 'nth-of-type' instead 'nth-child' (heuristic) + if (/:nth-child/.test(current)) { + // TODO: consider complete coverage of 'nth-of-type' replacement + var type = current.replace(/nth-child/g, "nth-of-type"); + var pattern = "" + prePart + type + postPart; + var matches = document.querySelectorAll(pattern); + if (compareResults(matches, elements)) { + current = type; + } + } + + // efficiency: combinations of classname (partial permutations) + if (/\.\S+\.\S+/.test(current)) { + var names = current + .trim() + .split(".") + .slice(1) + .map(function (name) { + return "." + name; + }) + .sort(function (curr, next) { + return curr.length - next.length; + }); + while (names.length) { + var partial = current.replace(names.shift(), "").trim(); + var pattern = ("" + prePart + partial + postPart).trim(); + if ( + !pattern.length || + pattern.charAt(0) === ">" || + pattern.charAt(pattern.length - 1) === ">" + ) { + break; + } + var matches = document.querySelectorAll(pattern); + if (compareResults(matches, elements)) { + current = partial; + } + } + + // robustness: degrade complex classname (heuristic) + names = current && current.match(/\./g); + if (names && names.length > 2) { + var _references = document.querySelectorAll( + "" + prePart + current + ); + + var _loop2 = function _loop2() { + var reference = _references[i]; + if ( + elements.some(function (element) { + return reference.contains(element); + }) + ) { + // TODO: + // - check using attributes + regard excludes + var description = reference.tagName.toLowerCase(); + pattern = "" + prePart + description + postPart; + matches = document.querySelectorAll(pattern); + + if (compareResults(matches, elements)) { + current = description; + } + return "break"; + } + }; + + for (var i = 0, l = _references.length; i < l; i++) { + var pattern; + var matches; + + var _ret2 = _loop2(); + + if (_ret2 === "break") break; + } + } + } + + return current; + } + + /** + * Evaluate matches with expected elements + * + * @param {Array.} matches - [description] + * @param {Array.} elements - [description] + * @return {Boolean} - [description] + */ + function compareResults(matches, elements) { + var length = matches.length; + + return ( + length === elements.length && + elements.every(function (element) { + for (var i = 0; i < length; i++) { + if (matches[i] === element) { + return true; + } + } + return false; + }) + ); + } + module.exports = exports["default"]; + + /***/ + }, + /* 3 */ + /***/ function (module, exports, __webpack_require__) { + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true, + }); + + var _typeof = + typeof Symbol === "function" && typeof Symbol.iterator === "symbol" + ? function (obj) { + return typeof obj; + } + : function (obj) { + return obj && + typeof Symbol === "function" && + obj.constructor === Symbol && + obj !== Symbol.prototype + ? "symbol" + : typeof obj; + }; + + var _slicedToArray = (function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for ( + var _i = arr[Symbol.iterator](), _s; + !(_n = (_s = _i.next()).done); + _n = true + ) { + _arr.push(_s.value); + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + return _arr; + } + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError( + "Invalid attempt to destructure non-iterable instance" + ); + } + }; + })(); + + exports.default = adapt; + /** + * # Adapt + * + * Check and extend the environment for universal usage. + */ + + /** + * Modify the context based on the environment + * + * @param {HTMLELement} element - [description] + * @param {Object} options - [description] + * @return {boolean} - [description] + */ + function adapt(element, options) { + // detect environment setup + if (true) { + return false; + } else { + global.document = + options.context || + (function () { + var root = element; + while (root.parent) { + root = root.parent; + } + return root; + })(); + } + + // https://github.com/fb55/domhandler/blob/master/index.js#L75 + var ElementPrototype = Object.getPrototypeOf(true); + + // alternative descriptor to access elements with filtering invalid elements (e.g. textnodes) + if (!Object.getOwnPropertyDescriptor(ElementPrototype, "childTags")) { + Object.defineProperty(ElementPrototype, "childTags", { + enumerable: true, + get: function get() { + return this.children.filter(function (node) { + // https://github.com/fb55/domelementtype/blob/master/index.js#L12 + return ( + node.type === "tag" || + node.type === "script" || + node.type === "style" + ); + }); + }, + }); + } + + if ( + !Object.getOwnPropertyDescriptor(ElementPrototype, "attributes") + ) { + // https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes + // https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + Object.defineProperty(ElementPrototype, "attributes", { + enumerable: true, + get: function get() { + var attribs = this.attribs; + + var attributesNames = Object.keys(attribs); + var NamedNodeMap = attributesNames.reduce(function ( + attributes, + attributeName, + index + ) { + attributes[index] = { + name: attributeName, + value: attribs[attributeName], + }; + return attributes; + }, + {}); + Object.defineProperty(NamedNodeMap, "length", { + enumerable: false, + configurable: false, + value: attributesNames.length, + }); + return NamedNodeMap; + }, + }); + } + + if (!ElementPrototype.getAttribute) { + // https://docs.webplatform.org/wiki/dom/Element/getAttribute + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute + ElementPrototype.getAttribute = function (name) { + return this.attribs[name] || null; + }; + } + + if (!ElementPrototype.getElementsByTagName) { + // https://docs.webplatform.org/wiki/dom/Document/getElementsByTagName + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByTagName + ElementPrototype.getElementsByTagName = function (tagName) { + var HTMLCollection = []; + traverseDescendants(this.childTags, function (descendant) { + if (descendant.name === tagName || tagName === "*") { + HTMLCollection.push(descendant); + } + }); + return HTMLCollection; + }; + } + + if (!ElementPrototype.getElementsByClassName) { + // https://docs.webplatform.org/wiki/dom/Document/getElementsByClassName + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByClassName + ElementPrototype.getElementsByClassName = function (className) { + var names = className.trim().replace(/\s+/g, " ").split(" "); + var HTMLCollection = []; + traverseDescendants([this], function (descendant) { + var descendantClassName = descendant.attribs.class; + if ( + descendantClassName && + names.every(function (name) { + return descendantClassName.indexOf(name) > -1; + }) + ) { + HTMLCollection.push(descendant); + } + }); + return HTMLCollection; + }; + } + + if (!ElementPrototype.querySelectorAll) { + // https://docs.webplatform.org/wiki/css/selectors_api/querySelectorAll + // https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll + ElementPrototype.querySelectorAll = function (selectors) { + var _this = this; + + selectors = selectors.replace(/(>)(\S)/g, "$1 $2").trim(); // add space for '>' selector + + // using right to left execution => https://github.com/fb55/css-select#how-does-it-work + var instructions = getInstructions(selectors); + var discover = instructions.shift(); + + var total = instructions.length; + return discover(this).filter(function (node) { + var step = 0; + while (step < total) { + node = instructions[step](node, _this); + if (!node) { + // hierarchy doesn't match + return false; + } + step += 1; + } + return true; + }); + }; + } + + if (!ElementPrototype.contains) { + // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains + ElementPrototype.contains = function (element) { + var inclusive = false; + traverseDescendants([this], function (descendant, done) { + if (descendant === element) { + inclusive = true; + done(); + } + }); + return inclusive; + }; + } + + return true; + } + + /** + * Retrieve transformation steps + * + * @param {Array.} selectors - [description] + * @return {Array.} - [description] + */ + function getInstructions(selectors) { + return selectors + .split(" ") + .reverse() + .map(function (selector, step) { + var discover = step === 0; + + var _selector$split = selector.split(":"), + _selector$split2 = _slicedToArray(_selector$split, 2), + type = _selector$split2[0], + pseudo = _selector$split2[1]; + + var validate = null; + var instruction = null; + + (function () { + switch (true) { + // child: '>' + case />/.test(type): + instruction = function checkParent(node) { + return function (validate) { + return validate(node.parent) && node.parent; + }; + }; + break; + + // class: '.' + case /^\./.test(type): + var names = type.substr(1).split("."); + validate = function validate(node) { + var nodeClassName = node.attribs.class; + return ( + nodeClassName && + names.every(function (name) { + return nodeClassName.indexOf(name) > -1; + }) + ); + }; + instruction = function checkClass(node, root) { + if (discover) { + return node.getElementsByClassName(names.join(" ")); + } + return typeof node === "function" + ? node(validate) + : getAncestor(node, root, validate); + }; + break; + + // attribute: '[key="value"]' + case /^\[/.test(type): + var _type$replace$split = type + .replace(/\[|\]|"/g, "") + .split("="), + _type$replace$split2 = _slicedToArray( + _type$replace$split, + 2 + ), + attributeKey = _type$replace$split2[0], + attributeValue = _type$replace$split2[1]; + + validate = function validate(node) { + var hasAttribute = + Object.keys(node.attribs).indexOf(attributeKey) > -1; + if (hasAttribute) { + // regard optional attributeValue + if ( + !attributeValue || + node.attribs[attributeKey] === attributeValue + ) { + return true; + } + } + return false; + }; + instruction = function checkAttribute(node, root) { + if (discover) { + var _ret2 = (function () { + var NodeList = []; + traverseDescendants([node], function (descendant) { + if (validate(descendant)) { + NodeList.push(descendant); + } + }); + return { + v: NodeList, + }; + })(); + + if ( + (typeof _ret2 === "undefined" + ? "undefined" + : _typeof(_ret2)) === "object" + ) + return _ret2.v; + } + return typeof node === "function" + ? node(validate) + : getAncestor(node, root, validate); + }; + break; + + // id: '#' + case /^#/.test(type): + var id = type.substr(1); + validate = function validate(node) { + return node.attribs.id === id; + }; + instruction = function checkId(node, root) { + if (discover) { + var _ret3 = (function () { + var NodeList = []; + traverseDescendants( + [node], + function (descendant, done) { + if (validate(descendant)) { + NodeList.push(descendant); + done(); + } + } + ); + return { + v: NodeList, + }; + })(); + + if ( + (typeof _ret3 === "undefined" + ? "undefined" + : _typeof(_ret3)) === "object" + ) + return _ret3.v; + } + return typeof node === "function" + ? node(validate) + : getAncestor(node, root, validate); + }; + break; + + // universal: '*' + case /\*/.test(type): + validate = function validate(node) { + return true; + }; + instruction = function checkUniversal(node, root) { + if (discover) { + var _ret4 = (function () { + var NodeList = []; + traverseDescendants([node], function (descendant) { + return NodeList.push(descendant); + }); + return { + v: NodeList, + }; + })(); + + if ( + (typeof _ret4 === "undefined" + ? "undefined" + : _typeof(_ret4)) === "object" + ) + return _ret4.v; + } + return typeof node === "function" + ? node(validate) + : getAncestor(node, root, validate); + }; + break; + + // tag: '...' + default: + validate = function validate(node) { + return node.name === type; + }; + instruction = function checkTag(node, root) { + if (discover) { + var _ret5 = (function () { + var NodeList = []; + traverseDescendants([node], function (descendant) { + if (validate(descendant)) { + NodeList.push(descendant); + } + }); + return { + v: NodeList, + }; + })(); + + if ( + (typeof _ret5 === "undefined" + ? "undefined" + : _typeof(_ret5)) === "object" + ) + return _ret5.v; + } + return typeof node === "function" + ? node(validate) + : getAncestor(node, root, validate); + }; + } + })(); + + if (!pseudo) { + return instruction; + } + + var rule = pseudo.match(/-(child|type)\((\d+)\)$/); + var kind = rule[1]; + var index = parseInt(rule[2], 10) - 1; + + var validatePseudo = function validatePseudo(node) { + if (node) { + var compareSet = node.parent.childTags; + if (kind === "type") { + compareSet = compareSet.filter(validate); + } + var nodeIndex = compareSet.findIndex(function (child) { + return child === node; + }); + if (nodeIndex === index) { + return true; + } + } + return false; + }; + + return function enhanceInstruction(node) { + var match = instruction(node); + if (discover) { + return match.reduce(function (NodeList, matchedNode) { + if (validatePseudo(matchedNode)) { + NodeList.push(matchedNode); + } + return NodeList; + }, []); + } + return validatePseudo(match) && match; + }; + }); + } + + /** + * Walking recursive to invoke callbacks + * + * @param {Array.} nodes - [description] + * @param {Function} handler - [description] + */ + function traverseDescendants(nodes, handler) { + nodes.forEach(function (node) { + var progress = true; + handler(node, function () { + return (progress = false); + }); + if (node.childTags && progress) { + traverseDescendants(node.childTags, handler); + } + }); + } + + /** + * Bubble up from bottom to top + * + * @param {HTMLELement} node - [description] + * @param {HTMLELement} root - [description] + * @param {Function} validate - [description] + * @return {HTMLELement} - [description] + */ + function getAncestor(node, root, validate) { + while (node.parent) { + node = node.parent; + if (validate(node)) { + return node; + } + if (node === root) { + break; + } + } + return null; + } + module.exports = exports["default"]; + + /***/ + }, + /* 4 */ + /***/ function (module, exports, __webpack_require__) { + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true, + }); + + var _typeof = + typeof Symbol === "function" && typeof Symbol.iterator === "symbol" + ? function (obj) { + return typeof obj; + } + : function (obj) { + return obj && + typeof Symbol === "function" && + obj.constructor === Symbol && + obj !== Symbol.prototype + ? "symbol" + : typeof obj; + }; + /** + * # Select + * + * Construct a unique CSS query selector to access the selected DOM element(s). + * For longevity it applies different matching and optimization strategies. + */ + + exports.getSingleSelector = getSingleSelector; + exports.getMultiSelector = getMultiSelector; + exports.default = getQuerySelector; + + var _adapt = __webpack_require__(3); + + var _adapt2 = _interopRequireDefault(_adapt); + + var _match = __webpack_require__(5); + + var _match2 = _interopRequireDefault(_match); + + var _optimize = __webpack_require__(2); + + var _optimize2 = _interopRequireDefault(_optimize); + + var _utilities = __webpack_require__(0); + + var _common = __webpack_require__(1); + + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; + } + + /** + * Get a selector for the provided element + * + * @param {HTMLElement} element - [description] + * @param {Object} options - [description] + * @return {string} - [description] + */ + function getSingleSelector(element) { + var options = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : {}; + + if (element.nodeType === 3) { + element = element.parentNode; + } + + if (element.nodeType !== 1) { + throw new Error( + 'Invalid input - only HTMLElements or representations of them are supported! (not "' + + (typeof element === "undefined" + ? "undefined" + : _typeof(element)) + + '")' + ); + } + + var globalModified = (0, _adapt2.default)(element, options); + + var selector = (0, _match2.default)(element, options); + var optimized = (0, _optimize2.default)(selector, element, options); + + // debug + // console.log(` + // selector: ${selector} + // optimized: ${optimized} + // `) + + if (globalModified) { + delete true; + } + + return optimized; + } + + /** + * Get a selector to match multiple descendants from an ancestor + * + * @param {Array.|NodeList} elements - [description] + * @param {Object} options - [description] + * @return {string} - [description] + */ + function getMultiSelector(elements) { + var options = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : {}; + + if (!Array.isArray(elements)) { + elements = (0, _utilities.convertNodeList)(elements); + } + + if ( + elements.some(function (element) { + return element.nodeType !== 1; + }) + ) { + throw new Error( + "Invalid input - only an Array of HTMLElements or representations of them is supported!" + ); + } + + var globalModified = (0, _adapt2.default)(elements[0], options); + + var ancestor = (0, _common.getCommonAncestor)(elements, options); + var ancestorSelector = getSingleSelector(ancestor, options); + + // TODO: consider usage of multiple selectors + parent-child relation + check for part redundancy + var commonSelectors = getCommonSelectors(elements); + var descendantSelector = commonSelectors[0]; + + var selector = (0, _optimize2.default)( + ancestorSelector + " " + descendantSelector, + elements, + options + ); + var selectorMatches = (0, _utilities.convertNodeList)( + document.querySelectorAll(selector) + ); + + if ( + !elements.every(function (element) { + return selectorMatches.some(function (entry) { + return entry === element; + }); + }) + ) { + // TODO: cluster matches to split into similar groups for sub selections + return console.warn( + "\n The selected elements can't be efficiently mapped.\n Its probably best to use multiple single selectors instead!\n ", + elements + ); + } + + if (globalModified) { + delete true; + } + + return selector; + } + + /** + * Get selectors to describe a set of elements + * + * @param {Array.} elements - [description] + * @return {string} - [description] + */ + function getCommonSelectors(elements) { + var _getCommonProperties = (0, _common.getCommonProperties)(elements), + classes = _getCommonProperties.classes, + attributes = _getCommonProperties.attributes, + tag = _getCommonProperties.tag; + + var selectorPath = []; + + if (tag) { + selectorPath.push(tag); + } + + if (classes) { + var classSelector = classes + .map(function (name) { + return "." + name; + }) + .join(""); + selectorPath.push(classSelector); + } + + if (attributes) { + var attributeSelector = Object.keys(attributes) + .reduce(function (parts, name) { + parts.push("[" + name + '="' + attributes[name] + '"]'); + return parts; + }, []) + .join(""); + selectorPath.push(attributeSelector); + } + + if (selectorPath.length) { + // TODO: check for parent-child relation + } + + return [selectorPath.join("")]; + } + + /** + * Choose action depending on the input (multiple/single) + * + * NOTE: extended detection is used for special cases like the