diff --git a/plugins/ide-open/.gitignore b/plugins/ide-open/.gitignore
new file mode 100644
index 00000000..6d6c01d8
--- /dev/null
+++ b/plugins/ide-open/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Test reference project
+test/
diff --git a/plugins/ide-open/CHANGELOG.md b/plugins/ide-open/CHANGELOG.md
new file mode 100644
index 00000000..faffa7ef
--- /dev/null
+++ b/plugins/ide-open/CHANGELOG.md
@@ -0,0 +1,19 @@
+# Changelog
+
+## 1.0.0
+
+### Added
+- 从 uTools VscodeOpen 插件迁移至 ZTools 平台
+- 支持 VSCode 系编辑器(VS Code、Cursor、VSCodium、Qoder)最近项目读取(SQLite/JSON)
+- 支持 JetBrains 系编辑器(IntelliJ IDEA、PyCharm、WebStorm、GoLand)最近项目读取(XML)
+- 动态 Feature 注册,输入别名即可进入对应编辑器项目列表
+- setSubInput 搜索 + 键盘导航(↑↓ 选择、Enter 打开、Ctrl+D 删除)
+- IDE 配置页面,支持快速填入预设路径
+- 暗色模式支持
+
+### Fixed
+- 修复命令注入漏洞,添加 escapeShellArg 对参数进行 shell 转义
+- JetBrains 版本目录排序,优先匹配最新版本
+- 添加 safeDecodeURIComponent 防止畸形 URI 导致崩溃
+- 使用 fileURLToPath 替代手动路径转换,修复 Windows 平台路径问题
+- 移除 Settings 页面冗余的 doRegister 逻辑
diff --git a/plugins/ide-open/README.md b/plugins/ide-open/README.md
new file mode 100644
index 00000000..22934525
--- /dev/null
+++ b/plugins/ide-open/README.md
@@ -0,0 +1,73 @@
+# ide-open
+
+> 统一管理 VSCode 系及 JetBrains 系编辑器的最近项目 — ZTools 插件
+
+## 功能
+
+- 从编辑器的本地数据库/配置文件中读取最近项目列表
+- 搜索并快速打开项目,支持键盘导航(↑↓ 选择、Enter 打开、Ctrl+D 删除)
+- 支持动态注册快捷指令,输入别名即可进入对应编辑器的项目列表
+
+### 支持的编辑器
+
+| 编辑器 | 数据源格式 | 启动命令 |
+|---|---|---|
+| VS Code | SQLite (.vscdb) / JSON | `code` |
+| Cursor | SQLite (.vscdb) | `cursor` |
+| VSCodium | SQLite (.vscdb) | `codium` |
+| Qoder | SQLite (.vscdb) | `qoder` |
+| IntelliJ IDEA | XML (recentProjects.xml) | `idea` |
+| PyCharm | XML | `pycharm` |
+| WebStorm | XML | `webstorm` |
+| GoLand | XML | `goland` |
+
+## 项目结构
+
+```
+├── public/
+│ ├── preload/
+│ │ ├── package.json # preload 依赖(sql.js)
+│ │ ├── services.js # Node.js 能力:SQLite/JSON/XML 读取、项目打开、删除
+│ │ └── sql-wasm.wasm # sql.js 的 wasm 文件
+│ ├── plugin.json # 插件配置
+│ └── logo.png
+├── src/
+│ ├── App.tsx # 入口:onPluginEnter 路由分发
+│ ├── store.ts # 渲染层数据封装
+│ ├── env.d.ts # 类型声明
+│ ├── Settings/ # IDE 配置页面(增删改、快速填入预设)
+│ └── ProjectList/ # 项目列表页面(setSubInput 搜索 + 键盘导航)
+├── vite.config.js # base: './'
+└── tsconfig.json
+```
+
+## 开发
+
+```bash
+# 安装依赖(前端 + preload)
+npm install
+cd public/preload && npm install && cd ..
+
+# 启动开发服务器
+npm run dev
+
+# 构建
+npm run build
+```
+
+在 ZTools 中配置插件开发地址指向 `http://localhost:5173` 即可热加载调试。
+
+## 使用
+
+1. 在 ZTools 主搜索框输入 `ideopen` 进入设置页
+2. 点击「+ 新增配置」,可点击「快速填入」按钮自动填充预设路径
+3. 保存后,ZTools 主搜索框输入配置的别名(如 `vsc`、`cursor`)即可进入项目列表
+4. 在项目列表中搜索并打开项目
+
+## 技术栈
+
+React 19 + Vite + TypeScript + sql.js
+
+## License
+
+MIT
diff --git a/plugins/ide-open/index.html b/plugins/ide-open/index.html
new file mode 100644
index 00000000..84b7eb68
--- /dev/null
+++ b/plugins/ide-open/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/ide-open/package-lock.json b/plugins/ide-open/package-lock.json
new file mode 100644
index 00000000..65036e58
--- /dev/null
+++ b/plugins/ide-open/package-lock.json
@@ -0,0 +1,929 @@
+{
+ "name": "ide-open",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ide-open",
+ "version": "1.0.0",
+ "dependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.1",
+ "@ztools-center/ztools-api-types": "^1.0.1",
+ "typescript": "^5.3.0",
+ "vite": "^6.0.11"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@ztools-center/ztools-api-types": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.40",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.4",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.38",
+ "caniuse-lite": "^1.0.30001799",
+ "electron-to-chromium": "^1.5.376",
+ "node-releases": "^2.0.48",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001799",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.381",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.50",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/plugins/ide-open/package.json b/plugins/ide-open/package.json
new file mode 100644
index 00000000..7a0d22f2
--- /dev/null
+++ b/plugins/ide-open/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "ide-open",
+ "version": "1.0.0",
+ "description": "插件 — 统一管理 VSCode 系及 JetBrains 系编辑器的最近项目。",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "changelog": "node scripts/gen-changelog.mjs",
+ "release": "npm run changelog && git add CHANGELOG.md && git commit -m \"chore: update CHANGELOG\" && ztools publish"
+ },
+ "dependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.1",
+ "@ztools-center/ztools-api-types": "^1.0.1",
+ "typescript": "^5.3.0",
+ "vite": "^6.0.11"
+ }
+}
diff --git a/plugins/ide-open/public/logo.png b/plugins/ide-open/public/logo.png
new file mode 100644
index 00000000..ae084fc5
Binary files /dev/null and b/plugins/ide-open/public/logo.png differ
diff --git a/plugins/ide-open/public/plugin.json b/plugins/ide-open/public/plugin.json
new file mode 100644
index 00000000..f8433224
--- /dev/null
+++ b/plugins/ide-open/public/plugin.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json",
+ "name": "ide-open",
+ "title": "ide-open",
+ "description": "统一管理 VSCode 系及 JetBrains 系编辑器的最近项目",
+ "author": "svenzhao",
+ "version": "1.0.0",
+ "main": "index.html",
+ "preload": "preload/services.js",
+ "logo": "logo.png",
+ "development": {
+ "main": "http://localhost:5173"
+ },
+ "features": [
+ {
+ "code": "ideopen",
+ "explain": "打开 ideOpen 设置,配置编辑器并注册快捷指令",
+ "icon": "logo.png",
+ "cmds": [
+ "ideopen"
+ ]
+ }
+ ],
+ "platform": [
+ "darwin",
+ "win32",
+ "linux"
+ ]
+}
diff --git a/plugins/ide-open/public/preload/package-lock.json b/plugins/ide-open/public/preload/package-lock.json
new file mode 100644
index 00000000..a96bcff2
--- /dev/null
+++ b/plugins/ide-open/public/preload/package-lock.json
@@ -0,0 +1,18 @@
+{
+ "name": "preload",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "sql.js": "^1.14.1"
+ }
+ },
+ "node_modules/sql.js": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
+ "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/plugins/ide-open/public/preload/package.json b/plugins/ide-open/public/preload/package.json
new file mode 100644
index 00000000..ac041d2f
--- /dev/null
+++ b/plugins/ide-open/public/preload/package.json
@@ -0,0 +1,6 @@
+{
+ "type": "commonjs",
+ "dependencies": {
+ "sql.js": "^1.14.1"
+ }
+}
diff --git a/plugins/ide-open/public/preload/services.js b/plugins/ide-open/public/preload/services.js
new file mode 100644
index 00000000..bbae817d
--- /dev/null
+++ b/plugins/ide-open/public/preload/services.js
@@ -0,0 +1,570 @@
+const fs = require('node:fs')
+const path = require('node:path')
+const { exec } = require('node:child_process')
+const { fileURLToPath } = require('node:url')
+
+// ─── 文件日志(开发调试) ──
+
+let _logFile = ''
+function getLogFile() {
+ if (_logFile) return _logFile
+ try {
+ const base = window.ztools.getPath('userData')
+ _logFile = path.join(base, 'ideopen-debug.log')
+ } catch {
+ _logFile = path.join(__dirname, 'ideopen-debug.log')
+ }
+ return _logFile
+}
+
+function debugLog(message, data) {
+ let isDev = false
+ try { isDev = window.ztools.isDev() } catch {}
+ if (!isDev) return
+
+ const now = new Date().toISOString()
+ const line = data !== undefined
+ ? `[${now}] [ideOpen] ${message} ${JSON.stringify(data, null, 2)}`
+ : `[${now}] [ideOpen] ${message}`
+ console.log(line)
+ try {
+ fs.appendFileSync(getLogFile(), line + '\n')
+ } catch (e) {
+ console.warn('[ideOpen] 写入日志文件失败:', e)
+ }
+}
+
+// ─── sql.js 原生加载 ──
+
+const WASM_DIR = path.join(__dirname, 'node_modules', 'sql.js', 'dist')
+
+let _SQL = null
+async function getSQL() {
+ if (!_SQL) {
+ debugLog('初始化 SQL.js...')
+ const sqlJsCode = fs.readFileSync(path.join(WASM_DIR, 'sql-wasm.js'), 'utf-8')
+ const initSqlJs = new Function(
+ 'require', 'module', 'exports',
+ sqlJsCode + '\nreturn module.exports;'
+ )(require, { exports: {} }, {}).default || require
+ const wasmBinary = fs.readFileSync(path.join(WASM_DIR, 'sql-wasm.wasm'))
+ _SQL = await initSqlJs({ wasmBinary })
+ debugLog('SQL.js 初始化完成')
+ }
+ return _SQL
+}
+
+// ─── glob 路径解析(JetBrains 版本目录含通配符) ──
+
+function resolveGlobPath(pattern) {
+ if (!pattern.includes('*')) return pattern
+
+ const starIdx = pattern.indexOf('*')
+ const before = pattern.substring(0, starIdx)
+ const after = pattern.substring(starIdx + 1)
+ const dir = path.dirname(before)
+ const prefix = path.basename(before)
+
+ if (!fs.existsSync(dir)) return pattern
+
+ try {
+ const entries = fs.readdirSync(dir).sort().reverse()
+ for (const entry of entries) {
+ if (entry.startsWith(prefix)) {
+ const resolved = path.join(dir, entry) + after
+ if (fs.existsSync(resolved)) return resolved
+ }
+ }
+ } catch {}
+
+ return pattern
+}
+
+// ─── 类型 ──
+
+// IDEItem: { code, name, command, dbPath, shell }
+// ProjectItem: { name, path, uri, type, label }
+
+// ─── SQLite 读取 ──
+
+const RECENT_KEYS = [
+ 'history.recentlyOpenedPathsList',
+ 'history.openedPathsList',
+ 'openedPathsList'
+]
+
+async function readProjectsFromSQLite(dbPath) {
+ const SQL = await getSQL()
+ const resolvedPath = resolveGlobPath(dbPath)
+ const candidates = [resolvedPath]
+ // VSCode 共享存储 fallback
+ const home = process.env.HOME || process.env.USERPROFILE
+ if (home && resolvedPath.includes(path.join('Code', 'User', 'globalStorage'))) {
+ const shared = path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb')
+ if (fs.existsSync(shared) && !candidates.includes(shared)) candidates.push(shared)
+ }
+
+ for (const filePath of candidates) {
+ if (!fs.existsSync(filePath)) {
+ debugLog(`数据库不存在: ${filePath}`)
+ continue
+ }
+ debugLog(`读取数据库: ${filePath}`)
+ const buffer = fs.readFileSync(filePath)
+ const db = new SQL.Database(buffer)
+ try {
+ for (const key of RECENT_KEYS) {
+ const results = db.exec(`SELECT value FROM ItemTable WHERE key = '${key}'`)
+ if (results.length > 0 && results[0].values.length > 0) {
+ const value = results[0].values[0][0]
+ const data = JSON.parse(value)
+ const entries = data.entries || []
+ debugLog(`命中 key=${key}, entries=${entries.length}`)
+ if (entries.length > 0) return parseEntries(entries)
+ }
+ }
+ } finally {
+ db.close()
+ }
+ }
+ debugLog('未找到项目记录')
+ return []
+}
+
+// ─── JSON 读取 ──
+
+async function readProjectsFromJSON(jsonPath) {
+ debugLog(`读取 JSON: ${jsonPath}`)
+ const content = fs.readFileSync(jsonPath, 'utf-8')
+ const data = JSON.parse(content)
+ for (const key of RECENT_KEYS) {
+ const entries = data?.[key]?.entries || []
+ if (entries.length > 0) {
+ debugLog(`JSON 命中 key=${key}, entries=${entries.length}`)
+ return parseEntries(entries)
+ }
+ }
+ debugLog('JSON 未找到项目记录')
+ return []
+}
+
+// ─── XML 读取(JetBrains) ──
+
+function readProjectsFromXML(xmlPath) {
+ const resolvedPath = resolveGlobPath(xmlPath)
+ if (!fs.existsSync(resolvedPath)) return []
+ const content = fs.readFileSync(resolvedPath, 'utf-8')
+
+ const pathRe = //g
+ const paths = []
+ let m
+ while ((m = pathRe.exec(content)) !== null) {
+ const v = m[1]
+ if (v.startsWith('/') || v.match(/^[A-Z]:\\/)) paths.push(v)
+ }
+
+ const nameByPath = {}
+ const entryRe = /]*>[\s\S]*?[\s\S]*?<\/entry>/g
+ let n
+ while ((n = entryRe.exec(content)) !== null) {
+ nameByPath[n[1]] = n[2]
+ }
+
+ return paths.map(p => ({
+ name: nameByPath[p] || path.basename(p),
+ path: p,
+ uri: p,
+ type: 'folder',
+ label: nameByPath[p] || ''
+ }))
+}
+
+// ─── 统一解析 ──
+
+function parseEntries(entries) {
+ debugLog(`parseEntries 输入: ${entries.length} 条`)
+ const result = entries
+ .map(e => {
+ if (e == null) return null
+ if (typeof e === 'string') {
+ if (!e) return null
+ const isRemote = /^[a-z]+-remote:\/\//.test(e)
+ const isWorkspace = e.endsWith('.code-workspace')
+ const localPath = isRemote ? '' : uriToPath(e)
+ return {
+ name: path.basename(safeDecodeURIComponent(e).replace(/^file:\/\//, '').replace(/^[a-z]+-remote:\/\//, '')) || '未命名',
+ path: localPath,
+ uri: e,
+ type: isRemote ? 'remote' : isWorkspace ? 'workspace' : 'folder',
+ label: ''
+ }
+ }
+ const uri = e.folderUri || e.fileUri || e.workspace?.configPath || ''
+ if (!uri) return null
+ const decoded = safeDecodeURIComponent(uri)
+ const name = path.basename(
+ decoded.replace(/^file:\/\//, '').replace(/^[a-z]+-remote:\/\//, '')
+ )
+ const isRemote = /^[a-z]+-remote:\/\//.test(uri)
+ const isWorkspace = uri.endsWith('.code-workspace')
+ const isFile = !!e.fileUri && !e.folderUri
+ const localPath = isRemote ? '' : uriToPath(uri)
+ return {
+ name: e.label || name || '未命名',
+ path: localPath,
+ uri,
+ type: isRemote ? 'remote' : isWorkspace ? 'workspace' : isFile ? 'file' : 'folder',
+ label: e.label || ''
+ }
+ })
+ .filter(Boolean)
+ debugLog(`parseEntries 输出: ${result.length} 条`)
+ return result
+}
+
+function safeDecodeURIComponent(str) {
+ try {
+ return decodeURIComponent(str)
+ } catch {
+ return str
+ }
+}
+
+function uriToPath(uri) {
+ try {
+ let p
+ if (uri.startsWith('file://')) {
+ p = fileURLToPath(uri)
+ } else {
+ const url = new URL(uri)
+ p = decodeURIComponent(url.pathname)
+ }
+ // 清理路径末尾的反斜杠(防止 Windows 风格路径残留 \)
+ return p.replace(/\\+$/, '')
+ } catch {
+ return uri
+ }
+}
+
+// ─── 主入口 ──
+
+async function readProjects(filePath) {
+ debugLog(`readProjects 入口: ${filePath}`)
+ const resolvedPath = resolveGlobPath(filePath)
+ if (!fs.existsSync(resolvedPath)) {
+ debugLog(`文件不存在: ${resolvedPath}`)
+ throw new Error(`文件不存在: ${resolvedPath}`)
+ }
+ const ext = path.extname(resolvedPath).toLowerCase()
+ debugLog(`文件扩展名: ${ext}`)
+ if (ext === '.vscdb' || ext === '.db') {
+ const items = await readProjectsFromSQLite(resolvedPath)
+ debugLog(`SQLite 解析结果: ${items.length} 个项目`)
+ return items
+ }
+ if (ext === '.json') {
+ const items = await readProjectsFromJSON(resolvedPath)
+ debugLog(`JSON 解析结果: ${items.length} 个项目`)
+ return items
+ }
+ if (ext === '.xml') {
+ const items = readProjectsFromXML(resolvedPath)
+ debugLog(`XML 解析结果: ${items.length} 个项目`)
+ return items
+ }
+ try {
+ const items = await readProjectsFromSQLite(resolvedPath)
+ debugLog(`自动探测 SQLite 结果: ${items.length} 个项目`)
+ return items
+ } catch {
+ const items = await readProjectsFromJSON(resolvedPath)
+ debugLog(`自动探测 JSON 结果: ${items.length} 个项目`)
+ return items
+ }
+}
+
+// ─── 删除项目记录 ──
+
+async function deleteProject(dbPath, uri) {
+ const resolvedPath = resolveGlobPath(dbPath)
+ const candidates = [resolvedPath]
+ const home = process.env.HOME || process.env.USERPROFILE
+ if (home && resolvedPath.includes(path.join('Code', 'User', 'globalStorage'))) {
+ const shared = path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb')
+ if (fs.existsSync(shared) && !candidates.includes(shared)) candidates.push(shared)
+ }
+
+ for (const filePath of candidates) {
+ if (!fs.existsSync(filePath)) continue
+ const ext = path.extname(filePath).toLowerCase()
+ if (ext !== '.vscdb' && ext !== '.db') continue
+
+ const SQL = await getSQL()
+ const buffer = fs.readFileSync(filePath)
+ const db = new SQL.Database(buffer)
+
+ try {
+ for (const key of RECENT_KEYS) {
+ const results = db.exec(`SELECT value FROM ItemTable WHERE key = '${key}'`)
+ if (results.length === 0 || results[0].values.length === 0) continue
+
+ const value = results[0].values[0][0]
+ const data = JSON.parse(value)
+ const entries = data.entries || []
+ const before = entries.length
+ data.entries = entries.filter(e => {
+ if (typeof e === 'string') return e !== uri
+ const ep = e.folderUri || e.fileUri || e.workspace?.configPath
+ return ep !== uri
+ })
+ if (data.entries.length === before) continue
+
+ const updated = JSON.stringify(data)
+ db.run(`UPDATE ItemTable SET value = ? WHERE key = '${key}'`, [updated])
+ const out = db.export()
+ fs.writeFileSync(filePath, Buffer.from(out))
+ return
+ }
+ } finally {
+ db.close()
+ }
+ }
+ throw new Error('未找到匹配的记录或数据库不支持删除')
+}
+
+// ─── IDE 配置管理 ──
+
+const STORAGE_KEY = 'ide-ides'
+const LEGACY_STORAGE_KEY = 'vsc-ides'
+
+function getIDEs() {
+ try {
+ const data = window.ztools.dbStorage.getItem(STORAGE_KEY)
+ if (Array.isArray(data) && data.length > 0) {
+ debugLog(`getIDEs 从新 key 读取: ${data.length} 个`)
+ return data
+ }
+ const legacy = window.ztools.dbStorage.getItem(LEGACY_STORAGE_KEY)
+ if (Array.isArray(legacy) && legacy.length > 0) {
+ debugLog(`getIDEs 从旧 key 迁移: ${legacy.length} 个`)
+ window.ztools.dbStorage.setItem(STORAGE_KEY, legacy)
+ return legacy
+ }
+ debugLog('getIDEs 未找到配置')
+ return []
+ } catch (e) {
+ debugLog(`getIDEs 异常: ${e}`)
+ return []
+ }
+}
+
+function saveIDEs(ides) {
+ debugLog(`saveIDEs: ${ides.length} 个`)
+ window.ztools.dbStorage.setItem(STORAGE_KEY, ides)
+}
+
+// ─── 打开项目 ──
+
+const defaultShell = process.platform === 'darwin' ? 'zsh -l -i -c' : process.platform === 'linux' ? 'bash -l -i -c' : ''
+
+function openProject(command, uri, shell) {
+ const effectiveShell = shell || defaultShell
+ const isRemote = /^[a-z]+-remote:\/\//.test(uri)
+ const localPath = isRemote ? '' : uriToPath(uri)
+
+ const run = (cmd, timeout = 10000) => new Promise((resolve, reject) => {
+ const fullCmd = effectiveShell ? `${effectiveShell} '${cmd}'` : cmd
+ debugLog(`执行: ${fullCmd}`)
+ exec(fullCmd, { env: process.env, windowsHide: true, timeout }, (err) => {
+ if (err) {
+ debugLog(`命令失败: ${err.message}`)
+ reject(err)
+ } else resolve()
+ })
+ })
+
+ return new Promise((resolve, reject) => {
+ if (localPath) {
+ run(`${command} "${localPath}"`)
+ .then(() => resolve())
+ .catch(err => reject(new Error(`启动失败: ${err.message}`)))
+ return
+ }
+ const isWorkspace = uri.endsWith('.code-workspace')
+ const flag = isWorkspace ? '--file-uri' : '--folder-uri'
+ run(`${command} ${flag} "${uri}"`)
+ .then(() => resolve())
+ .catch(err => reject(new Error(`启动失败: ${err.message}`)))
+ })
+}
+
+// ─── 预设 ──
+
+const homeDir = () => process.env.HOME || process.env.USERPROFILE || ''
+const jetBrainsDir = () => {
+ const home = homeDir()
+ if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'JetBrains')
+ if (process.platform === 'win32') return path.join(process.env.APPDATA || home, 'JetBrains')
+ return path.join(home, '.config', 'JetBrains')
+}
+
+function getPresets() {
+ const appData = window.ztools.getPath('appData')
+ const jbDir = jetBrainsDir()
+ const home = homeDir()
+ return {
+ vscode: {
+ name: 'VSCode',
+ command: 'code',
+ dbPaths: [
+ path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb'),
+ path.join(appData, 'Code', 'User', 'globalStorage', 'state.vscdb'),
+ path.join(appData, 'Code', 'storage.json')
+ ]
+ },
+ cursor: {
+ name: 'Cursor',
+ command: 'cursor',
+ dbPaths: [
+ path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
+ path.join(appData, 'Cursor', 'storage.json')
+ ]
+ },
+ vscodium: {
+ name: 'VSCodium',
+ command: 'codium',
+ dbPaths: [
+ path.join(appData, 'VSCodium', 'User', 'globalStorage', 'state.vscdb'),
+ path.join(appData, 'VSCodium', 'storage.json')
+ ]
+ },
+ idea: {
+ name: 'IntelliJ IDEA',
+ command: 'idea',
+ dbPaths: [path.join(jbDir, 'IntelliJIdea*', 'options', 'recentProjects.xml')]
+ },
+ pycharm: {
+ name: 'PyCharm',
+ command: 'pycharm',
+ dbPaths: [path.join(jbDir, 'PyCharm*', 'options', 'recentProjects.xml')]
+ },
+ webstorm: {
+ name: 'WebStorm',
+ command: 'webstorm',
+ dbPaths: [path.join(jbDir, 'WebStorm*', 'options', 'recentProjects.xml')]
+ },
+ goland: {
+ name: 'GoLand',
+ command: 'goland',
+ dbPaths: [path.join(jbDir, 'GoLand*', 'options', 'recentProjects.xml')]
+ },
+ qoder: {
+ name: 'Qoder',
+ command: 'qoder',
+ dbPaths: [
+ path.join(appData, 'Qoder', 'User', 'globalStorage', 'state.vscdb'),
+ path.join(appData, 'Qoder', 'storage.json'),
+ path.join(appData, 'Qoder', 'User', 'globalStorage', 'storage.json')
+ ]
+ }
+ }
+}
+
+function getQuickFillPresets() {
+ const home = process.env.HOME || process.env.USERPROFILE || ''
+ const appData = window.ztools.getPath('appData')
+ const jbDir = jetBrainsDir()
+ const vscDbPath = home
+ ? path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb')
+ : path.join(appData, 'Code', 'User', 'globalStorage', 'state.vscdb')
+ return [
+ { code: 'vsc', name: 'VS Code', command: 'code', dbPath: vscDbPath, shell: defaultShell },
+ { code: 'cursor', name: 'Cursor', command: 'cursor', dbPath: path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb'), shell: defaultShell },
+ { code: 'codium', name: 'VSCodium', command: 'codium', dbPath: path.join(appData, 'VSCodium', 'User', 'globalStorage', 'state.vscdb'), shell: defaultShell },
+ { code: 'idea', name: 'IntelliJ IDEA', command: 'idea', dbPath: path.join(jbDir, 'IntelliJIdea*', 'options', 'recentProjects.xml'), shell: defaultShell },
+ { code: 'pycharm', name: 'PyCharm', command: 'pycharm', dbPath: path.join(jbDir, 'PyCharm*', 'options', 'recentProjects.xml'), shell: defaultShell },
+ { code: 'webstorm', name: 'WebStorm', command: 'webstorm', dbPath: path.join(jbDir, 'WebStorm*', 'options', 'recentProjects.xml'), shell: defaultShell },
+ { code: 'goland', name: 'GoLand', command: 'goland', dbPath: path.join(jbDir, 'GoLand*', 'options', 'recentProjects.xml'), shell: defaultShell },
+ { code: 'qoder', name: 'Qoder', command: 'qoder', dbPath: path.join(appData, 'Qoder', 'User', 'globalStorage', 'state.vscdb'), shell: defaultShell }
+ ]
+}
+
+function detectPresetPaths() {
+ const presets = getPresets()
+ const result = {}
+ for (const [key, preset] of Object.entries(presets)) {
+ debugLog(`detectPresetPaths ${key}: 尝试 ${preset.dbPaths.length} 个路径`)
+ for (const p of preset.dbPaths) {
+ const resolved = resolveGlobPath(p)
+ const exists = fs.existsSync(resolved)
+ debugLog(` ${exists ? '✓' : '✗'} ${resolved}`)
+ if (exists) {
+ result[key] = resolved
+ break
+ }
+ }
+ }
+ debugLog('detectPresetPaths 结果:', result)
+ return result
+}
+
+// ─── 动态 Feature 注册 ──
+
+const REG_CODES_KEY = 'ide-registered-codes'
+
+function registerFeatures() {
+ const ides = getIDEs()
+ const currentCodes = ides.filter(i => i.code).map(i => i.code)
+ debugLog(`registerFeatures: current=${currentCodes.join(',') || '(none)'}`)
+ const prevCodes = window.ztools.dbStorage.getItem(REG_CODES_KEY) || []
+
+ for (const ide of ides) {
+ if (!ide.code) continue
+ try {
+ window.ztools.setFeature({
+ code: ide.code,
+ explain: `打开 ${ide.name || ide.code} 最近项目`,
+ cmds: [ide.code],
+ icon: 'logo.png'
+ })
+ debugLog(`✅ 注册 feature: ${ide.code}`)
+ } catch (e) {
+ debugLog(`❌ 注册 ${ide.code} 失败: ${e}`)
+ }
+ }
+
+ for (const oldCode of prevCodes) {
+ if (!currentCodes.includes(oldCode)) {
+ try {
+ window.ztools.removeFeature(oldCode)
+ debugLog(`🗑 删除旧 feature: ${oldCode}`)
+ } catch (e) {
+ debugLog(`❌ 删除 feature ${oldCode} 失败: ${e}`)
+ }
+ }
+ }
+
+ window.ztools.dbStorage.setItem(REG_CODES_KEY, currentCodes)
+}
+
+// ─── 启动时注册所有已保存 IDE 的动态指令 ──
+
+registerFeatures()
+
+// ─── 暴露给渲染进程 ──
+
+window.services = {
+ readProjects,
+ openProject,
+ deleteProject,
+ getIDEs,
+ saveIDEs,
+ registerFeatures,
+ getPresets,
+ getQuickFillPresets,
+ detectPresetPaths,
+ getAppDataPath: () => window.ztools.getPath('appData'),
+ getDefaultShell: () => defaultShell,
+ debugLog,
+ getLogFile
+}
diff --git a/plugins/ide-open/public/preload/sql-wasm.wasm b/plugins/ide-open/public/preload/sql-wasm.wasm
new file mode 100755
index 00000000..b32b6647
Binary files /dev/null and b/plugins/ide-open/public/preload/sql-wasm.wasm differ
diff --git a/plugins/ide-open/scripts/gen-changelog.mjs b/plugins/ide-open/scripts/gen-changelog.mjs
new file mode 100644
index 00000000..5609ccd5
--- /dev/null
+++ b/plugins/ide-open/scripts/gen-changelog.mjs
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+/**
+ * 从 ztools-last-publish tag 之后的 commit 自动生成 CHANGELOG.md 条目
+ * 约定 commit 格式: type(scope): message
+ * feat: → Added
+ * fix: → Fixed
+ * refactor: → Changed
+ * chore: → (跳过,不记入)
+ * docs: → (跳过)
+ */
+import { execSync } from 'node:child_process'
+import { readFileSync, writeFileSync, existsSync } from 'node:fs'
+
+const TAG = 'ztools-last-publish'
+const VERSION = JSON.parse(readFileSync('package.json', 'utf-8')).version
+
+// 获取上次发布后的 commit messages
+function getCommits() {
+ let range
+ try {
+ // 检查 tag 是否存在
+ execSync(`git rev-parse --verify refs/tags/${TAG}`, { stdio: 'pipe' })
+ range = `${TAG}..HEAD`
+ } catch {
+ // tag 不存在,取所有 commit
+ range = 'HEAD'
+ }
+
+ const log = execSync(`git log ${range} --pretty=format:"%s" --no-merges`, { encoding: 'utf-8' })
+ return log.trim().split('\n').filter(Boolean)
+}
+
+const TYPE_MAP = {
+ feat: 'Added',
+ fix: 'Fixed',
+ refactor: 'Changed',
+ perf: 'Changed',
+}
+
+const groups = { Added: [], Fixed: [], Changed: [] }
+
+for (const line of getCommits()) {
+ const m = line.match(/^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/)
+ if (!m) continue
+ const [, type, scope, msg] = m
+ const section = TYPE_MAP[type]
+ if (!section) continue // chore, docs, ci 等跳过
+ const prefix = scope ? `**${scope}** ` : ''
+ groups[section].push(`- ${prefix}${msg}`)
+}
+
+// 检查是否已有该版本条目
+const existing = existsSync('CHANGELOG.md') ? readFileSync('CHANGELOG.md', 'utf-8') : ''
+if (existing.includes(`## ${VERSION}`)) {
+ console.log(`CHANGELOG.md 中已存在 v${VERSION} 条目,跳过生成`)
+ process.exit(0)
+}
+
+// 生成新条目
+const sections = Object.entries(groups)
+ .filter(([, items]) => items.length > 0)
+ .map(([title, items]) => `### ${title}\n${items.join('\n')}`)
+ .join('\n\n')
+
+if (!sections) {
+ console.log('没有可记录的变更(feat/fix/refactor)')
+ process.exit(0)
+}
+
+const entry = `## ${VERSION}\n\n${sections}\n`
+const output = existing
+ ? existing.replace(/^# Changelog\n/, `# Changelog\n\n${entry}`)
+ : `# Changelog\n\n${entry}`
+
+writeFileSync('CHANGELOG.md', output)
+console.log(`✓ 已生成 v${VERSION} 的 CHANGELOG 条目:`)
+console.log(entry)
diff --git a/plugins/ide-open/src/App.tsx b/plugins/ide-open/src/App.tsx
new file mode 100644
index 00000000..54dd23d7
--- /dev/null
+++ b/plugins/ide-open/src/App.tsx
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react'
+import Settings from './Settings'
+import ProjectList from './ProjectList'
+import { getIDEs, type IDEItem } from './store'
+
+type View =
+ | { page: 'settings' }
+ | { page: 'projects'; ide: IDEItem }
+
+export default function App() {
+ const [view, setView] = useState({ page: 'settings' })
+
+ useEffect(() => {
+ window.ztools.onPluginEnter((action) => {
+ window.services?.debugLog('onPluginEnter', action.code)
+ if (action.code === 'ideopen') { setView({ page: 'settings' }); return }
+ const ides = getIDEs()
+ window.services?.debugLog('当前 IDE 列表', ides.map(i => i.code))
+ const ide = ides.find(i => i.code === action.code)
+ if (ide) {
+ window.services?.debugLog('匹配到 IDE', ide)
+ setView({ page: 'projects', ide })
+ } else {
+ window.services?.debugLog('未找到匹配的 IDE', action.code)
+ }
+ })
+ window.ztools.onPluginOut(() => {
+ setView({ page: 'settings' })
+ })
+ }, [])
+
+ if (view.page === 'projects') return setView({ page: 'settings' })} />
+
+ return
+}
diff --git a/plugins/ide-open/src/ProjectList/index.css b/plugins/ide-open/src/ProjectList/index.css
new file mode 100644
index 00000000..f587e48f
--- /dev/null
+++ b/plugins/ide-open/src/ProjectList/index.css
@@ -0,0 +1,87 @@
+.project-list {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
+}
+
+.pl-ide-name { font-weight: 600; font-size: 14px; }
+.pl-count { color: var(--fg2); font-size: 12px; }
+
+.btn-edit-top {
+ margin-left: auto;
+ color: var(--blue);
+ font-size: 12px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+}
+.btn-edit-top:hover { border-color: var(--blue); background: rgba(22,119,255,.04); }
+
+.pl-loading, .pl-empty {
+ text-align: center;
+ padding: 32px;
+ color: var(--fg2);
+ font-size: 12px;
+}
+
+.pl-items {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 12px 12px;
+}
+
+.pl-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-bottom: 2px;
+ transition: background .15s;
+}
+
+.pl-item:hover { background: var(--bg2); }
+.pl-item-selected { background: rgba(22,119,255,.08) !important; }
+
+.pl-item-icon { font-size: 16px; flex-shrink: 0; width: 20px; text-align: center; color: var(--blue); }
+
+.pl-item-info { flex: 1; min-width: 0; }
+.pl-item-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.pl-item-path { font-size: 11px; color: var(--fg2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px; }
+
+.pl-item-del {
+ font-size: 12px;
+ color: var(--red);
+ opacity: 0;
+ padding: 0 4px;
+ border-radius: 3px;
+ transition: opacity .15s;
+}
+.pl-item:hover .pl-item-del,
+.pl-item-selected .pl-item-del { opacity: .5; }
+.pl-item .pl-item-del:hover { opacity: 1; background: rgba(255,77,79,.08); }
+
+.pl-item-badge {
+ font-size: 10px;
+ padding: 0 5px;
+ line-height: 18px;
+ border: 1px solid rgba(22,119,255,.25);
+ color: var(--blue);
+ border-radius: 3px;
+ flex-shrink: 0;
+}
+
+.project-error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ gap: 8px;
+ color: var(--red);
+ font-size: 12px;
+ padding: 20px;
+ text-align: center;
+}
diff --git a/plugins/ide-open/src/ProjectList/index.tsx b/plugins/ide-open/src/ProjectList/index.tsx
new file mode 100644
index 00000000..e6cd4040
--- /dev/null
+++ b/plugins/ide-open/src/ProjectList/index.tsx
@@ -0,0 +1,175 @@
+import { useEffect, useState, useRef } from 'react'
+import { readProjects, openProject, deleteProject, type IDEItem, type ProjectItem } from '../store'
+import './index.css'
+
+function IconFolder() {
+ return (
+
+ )
+}
+
+function IconFile() {
+ return (
+
+ )
+}
+
+function IconRemote() {
+ return (
+
+ )
+}
+
+export default function ProjectList({ ide, onBack, onEdit }: { ide: IDEItem; onBack?: () => void; onEdit?: () => void }) {
+ const [projects, setProjects] = useState([])
+ const [filtered, setFiltered] = useState([])
+ const [search, setSearch] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState('')
+ const [selected, setSelected] = useState(-1)
+ const listRef = useRef(null)
+
+ // refs for document-level keydown (避免频繁重新注册)
+ const filteredRef = useRef(filtered)
+ const selectedRef = useRef(selected)
+ filteredRef.current = filtered
+ selectedRef.current = selected
+
+ const load = () => {
+ window.services?.debugLog('ProjectList load', { code: ide.code, command: ide.command, dbPath: ide.dbPath, shell: ide.shell })
+ if (!ide.command || !ide.dbPath) {
+ setError(`请先在设置页配置「${ide.name}」的数据文件路径`)
+ setLoading(false)
+ return
+ }
+ setLoading(true)
+ setError('')
+ readProjects(ide.dbPath)
+ .then(items => {
+ window.services?.debugLog(`ProjectList 加载完成: ${items.length} 个项目`)
+ setProjects(items); setFiltered(items)
+ })
+ .catch((err: Error) => {
+ window.services?.debugLog('ProjectList 读取失败', err.message)
+ setError(`读取失败: ${err.message}`)
+ })
+ .finally(() => setLoading(false))
+ }
+
+ // 设置子输入框 + 加载数据
+ useEffect(() => {
+ setSearch('')
+ window.ztools.setSubInput((input: { text: string }) => setSearch(input.text), '搜索项目,Enter 打开...', true)
+ window.ztools.setSubInputValue('')
+ window.ztools.setExpendHeight(500)
+ load()
+ return () => { window.ztools.removeSubInput() }
+ }, [ide.code])
+
+ // 过滤
+ useEffect(() => {
+ const q = search.toLowerCase().trim()
+ if (!q) { setFiltered(projects); return }
+ const terms = q.split(/\s+/)
+ setFiltered(projects.filter(p =>
+ terms.every(t =>
+ p.name.toLowerCase().includes(t) || p.path.toLowerCase().includes(t) || p.label.toLowerCase().includes(t)
+ )
+ ))
+ setSelected(-1)
+ }, [search, projects])
+
+ // 滚动到选中项
+ useEffect(() => {
+ if (selected < 0 || !listRef.current) return
+ const el = listRef.current.children[selected] as HTMLElement
+ if (el) el.scrollIntoView({ block: 'nearest' })
+ }, [selected])
+
+ const handleOpen = async (project: ProjectItem) => {
+ window.services?.debugLog('handleOpen', { command: ide.command, uri: project.uri, shell: ide.shell })
+ try {
+ await openProject(ide.command, project.uri, ide.shell)
+ window.ztools.hideMainWindow()
+ window.ztools.outPlugin()
+ } catch (err: any) {
+ window.services?.debugLog('handleOpen 失败', err.message)
+ alert(`打开失败: ${err.message}`)
+ }
+ }
+
+ const handleDelete = async (project: ProjectItem) => {
+ if (!confirm(`确定从历史记录中删除「${project.name}」?`)) return
+ try {
+ await deleteProject(ide.dbPath, project.uri)
+ load()
+ } catch (err: any) {
+ alert(`删除失败: ${err.message}`)
+ }
+ }
+
+ // 键盘导航(document 级,因为焦点在 ZTools 主搜索框上)
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ const f = filteredRef.current
+ const s = selectedRef.current
+ if (e.key === 'ArrowDown') {
+ e.preventDefault(); setSelected(i => i >= f.length - 1 ? 0 : i + 1)
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault(); setSelected(i => i <= 0 ? f.length - 1 : i - 1)
+ } else if (e.key === 'Enter' && s >= 0) {
+ e.preventDefault(); handleOpen(f[s])
+ }
+ }
+ document.addEventListener('keydown', onKeyDown)
+ return () => document.removeEventListener('keydown', onKeyDown)
+ }, [])
+
+ return (
+
+
+ {onBack && }
+ {ide.name}
+ {filtered.length} 个项目
+ {onEdit && }
+
+
+ {loading &&
加载中...
}
+ {error && !loading &&
}
+ {!loading && !error && filtered.length === 0 && (
+
{search ? '没有匹配的项目' : '暂无最近项目'}
+ )}
+
+
+ {filtered.map((p, i) => (
+
handleOpen(p)}
+ onContextMenu={e => { e.preventDefault(); handleDelete(p) }}
+ onMouseEnter={() => setSelected(i)}>
+
+ {p.type === 'remote' ? : p.type === 'workspace' || p.type === 'file' ? : }
+
+
+
{p.name}
+
{p.path || p.uri}
+
+
+ {p.type === 'remote' &&
远程}
+
+ ))}
+
+
+ )
+}
diff --git a/plugins/ide-open/src/Settings/index.css b/plugins/ide-open/src/Settings/index.css
new file mode 100644
index 00000000..54b1d8a8
--- /dev/null
+++ b/plugins/ide-open/src/Settings/index.css
@@ -0,0 +1,121 @@
+.settings { padding: 12px; }
+
+.settings h2 {
+ margin: 0 0 2px;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.settings .subtitle {
+ color: var(--fg2);
+ font-weight: 400;
+ font-size: 12px;
+}
+
+.empty-hint { color: var(--fg2); text-align: center; padding: 32px 0; }
+
+.btn-toggle {
+ width: 100%;
+ padding: 4px;
+ text-align: center;
+ border: 1px dashed var(--border);
+ border-radius: 4px;
+ color: var(--fg2);
+ font-size: 12px;
+ margin-bottom: 8px;
+}
+.btn-toggle:hover { border-color: var(--blue); color: var(--blue); }
+
+.add-form {
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ margin-bottom: 8px;
+ background: var(--bg2);
+}
+
+.add-form label {
+ display: block;
+ margin-bottom: 6px;
+ font-size: 12px;
+}
+.add-form label span { color: var(--fg2); margin-bottom: 2px; display: block; }
+.add-form input { display: block; width: 100%; }
+
+.quick-fill {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+ margin-bottom: 8px;
+}
+.qf-label { color: var(--fg2); line-height: 22px; font-size: 12px; }
+
+.btn-qf {
+ padding: 1px 8px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg2);
+ font-size: 12px;
+ line-height: 20px;
+}
+.btn-qf:hover { border-color: var(--blue); color: var(--blue); }
+
+.btn-submit {
+ width: 100%;
+ padding: 4px;
+ margin-top: 4px;
+ background: var(--blue);
+ color: #fff;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 22px;
+}
+.btn-submit:hover { opacity: .85; }
+
+.ide-table { width: 100%; border-collapse: collapse; font-size: 12px; }
+
+.ide-table th {
+ text-align: left;
+ padding: 5px 8px;
+ font-weight: 500;
+ color: var(--fg2);
+ border-bottom: 1px solid var(--border);
+ font-size: 11px;
+}
+
+.ide-table td {
+ padding: 4px 8px;
+ border-bottom: 1px solid var(--border);
+ vertical-align: middle;
+}
+
+.ide-table td code { font-weight: 600; font-family: monospace; font-size: 12px; color: var(--blue); }
+
+.ide-table td input {
+ width: 100%;
+ padding: 2px 6px;
+ border: 1px solid transparent;
+ background: transparent;
+ border-radius: 2px;
+}
+.ide-table td input:focus { border-color: var(--blue); background: var(--bg); }
+
+.td-path { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.td-actions { white-space: nowrap; text-align: right; }
+
+.btn-edit {
+ color: var(--blue);
+ font-size: 12px;
+ padding: 1px 6px;
+ border-radius: 3px;
+}
+.btn-edit:hover { background: rgba(22,119,255,.06); }
+
+.btn-remove {
+ color: var(--red);
+ font-size: 13px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ opacity: .6;
+}
+.btn-remove:hover { opacity: 1; background: rgba(255,77,79,.06); }
diff --git a/plugins/ide-open/src/Settings/index.tsx b/plugins/ide-open/src/Settings/index.tsx
new file mode 100644
index 00000000..ee26d526
--- /dev/null
+++ b/plugins/ide-open/src/Settings/index.tsx
@@ -0,0 +1,141 @@
+import { useEffect, useState } from 'react'
+import { getIDEs, saveIDEs, getQuickFillPresets, getAppDataPath, getDefaultShell, type IDEItem } from '../store'
+import './index.css'
+
+const emptyForm = (): IDEItem => ({ code: '', name: '', command: '', dbPath: '', shell: '' })
+
+export default function Settings({ onBack, editIDE }: { onBack?: () => void; editIDE?: IDEItem }) {
+ const [ides, setIDEs] = useState([])
+ const [showForm, setShowForm] = useState(false)
+ const [form, setForm] = useState(emptyForm())
+ const [editingIdx, setEditingIdx] = useState(-1)
+ const defaultShell = getDefaultShell()
+ const appData = getAppDataPath()
+
+ useEffect(() => {
+ setIDEs(getIDEs())
+ }, [])
+
+ useEffect(() => {
+ if (!editIDE) return
+ const idx = ides.findIndex(i => i.code === editIDE.code)
+ if (idx >= 0) {
+ setForm({ ...editIDE })
+ setEditingIdx(idx)
+ setShowForm(true)
+ }
+ }, [editIDE])
+
+ const save = () => {
+ if (!form.code.trim()) { alert('请输入别名'); return }
+ if (!form.command.trim()) { alert('请输入启动命令'); return }
+ if (!form.dbPath.trim()) { alert('请输入历史文件路径'); return }
+ const entry: IDEItem = { code: form.code.trim(), name: form.name.trim() || form.code.trim(), command: form.command.trim(), dbPath: form.dbPath.trim(), shell: form.shell.trim() }
+ let list
+ if (editingIdx >= 0) {
+ list = ides.map((item, i) => i === editingIdx ? entry : item)
+ } else {
+ list = [...ides, entry]
+ }
+ setIDEs(list)
+ saveIDEs(list)
+ setForm(emptyForm())
+ setEditingIdx(-1)
+ setShowForm(false)
+ }
+
+ const del = (idx: number) => {
+ const list = ides.filter((_, i) => i !== idx)
+ setIDEs(list)
+ saveIDEs(list)
+ }
+
+ const startEdit = (idx: number) => {
+ const ide = ides[idx]
+ setForm({ code: ide.code, name: ide.name, command: ide.command, dbPath: ide.dbPath, shell: ide.shell || defaultShell })
+ setEditingIdx(idx)
+ setShowForm(true)
+ }
+
+ const startAdd = () => {
+ setForm(emptyForm())
+ setEditingIdx(-1)
+ setShowForm(true)
+ }
+
+ const fill = (preset: any) => {
+ setForm({ code: preset.code, name: preset.name, command: preset.command, dbPath: preset.dbPath, shell: preset.shell || '' })
+ }
+
+ const isEditing = editingIdx >= 0
+ const defaults = getQuickFillPresets()
+
+ return (
+
+
+ {onBack && }
+
ideOpen 配置 IDE
+
+
+
+
+ {showForm && (
+
+ )}
+
+ {ides.length === 0 && (
+
还没有配置,点「+ 新增配置」添加
+ )}
+
+ {ides.length > 0 && (
+
+
+
+ | 别名 |
+ 名称 |
+ 启动命令 |
+ shell |
+ 历史文件路径 |
+ |
+
+
+
+ {ides.map((ide, idx) => (
+
+ {ide.code} |
+ {ide.name} |
+ {ide.command} |
+ {ide.shell || '-'} |
+ {ide.dbPath} |
+
+
+
+ |
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/plugins/ide-open/src/env.d.ts b/plugins/ide-open/src/env.d.ts
new file mode 100644
index 00000000..72914eee
--- /dev/null
+++ b/plugins/ide-open/src/env.d.ts
@@ -0,0 +1,26 @@
+///
+///
+
+interface Services {
+ readProjects: (dbPath: string) => Promise
+ openProject: (command: string, uri: string, shell?: string) => Promise
+ deleteProject: (dbPath: string, uri: string) => Promise
+ getIDEs: () => any[]
+ saveIDEs: (ides: any[]) => void
+ registerFeatures: () => void
+ getPresets: () => Record
+ getQuickFillPresets: () => any[]
+ detectPresetPaths: () => Record
+ getAppDataPath: () => string
+ getDefaultShell: () => string
+ debugLog: (message: string, data?: any) => void
+ getLogFile: () => string
+}
+
+declare global {
+ interface Window {
+ services: Services
+ }
+}
+
+export {}
diff --git a/plugins/ide-open/src/main.css b/plugins/ide-open/src/main.css
new file mode 100644
index 00000000..bd6d869d
--- /dev/null
+++ b/plugins/ide-open/src/main.css
@@ -0,0 +1,171 @@
+:root {
+ --blue: #1677ff;
+ --red: #ff4d4f;
+ --fg: #333;
+ --fg2: #999;
+ --bg: #fff;
+ --bg2: #fafafa;
+ --border: #e8e8e8;
+ --shadow: rgba(0,0,0,.06);
+}
+
+* { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--bg);
+ color: var(--fg);
+ font: 13px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+::-webkit-scrollbar { width: 5px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 4px; }
+
+input {
+ font: inherit;
+ color: var(--fg);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 3px 7px;
+ outline: none;
+ transition: border .2s;
+}
+input:focus { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(22,119,255,.1); }
+input::placeholder { color: #bbb; }
+
+button {
+ font: inherit;
+ cursor: pointer;
+ border: none;
+ background: none;
+ color: inherit;
+}
+button:active { opacity: .7; }
+
+/* ── 首页 ── */
+
+.app-home { padding: 12px; }
+
+.app-home h2 {
+ margin: 0 0 8px;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.app-home .subtitle {
+ color: var(--fg2);
+ font-weight: 400;
+ font-size: 12px;
+}
+
+.app-home .empty-hint {
+ color: var(--fg2);
+ text-align: center;
+ padding: 32px 0;
+}
+
+.ide-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 10px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ margin-bottom: 6px;
+ transition: border-color .2s;
+}
+.ide-card:hover { border-color: var(--blue); }
+
+.ide-card-body {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ flex: 1;
+}
+
+.ide-card-body:hover .ide-card-name { color: var(--blue); }
+
+.ide-card-code {
+ font-weight: 600;
+ font-family: monospace;
+ font-size: 12px;
+ color: var(--blue);
+ background: rgba(22,119,255,.06);
+ padding: 0 6px;
+ border-radius: 3px;
+ line-height: 20px;
+}
+
+.ide-card-name { font-size: 13px; transition: color .2s; }
+
+.ide-card-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.ide-card-btn {
+ font-size: 11px;
+ color: var(--fg2);
+ padding: 1px 6px;
+ border-radius: 3px;
+}
+.ide-card-btn:hover { background: rgba(22,119,255,.06); color: var(--blue); }
+
+.ide-card-arrow {
+ color: #ccc;
+ font-size: 14px;
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 3px;
+}
+.ide-card-arrow:hover { background: var(--bg2); color: var(--blue); }
+
+.btn-add-ide {
+ display: block;
+ width: 100%;
+ padding: 4px;
+ text-align: center;
+ border: 1px dashed var(--border);
+ border-radius: 4px;
+ color: var(--fg2);
+ font-size: 12px;
+ margin-top: 4px;
+}
+.btn-add-ide:hover { border-color: var(--blue); color: var(--blue); }
+
+/* ── 顶部栏 ── */
+
+.top-bar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px 4px;
+ margin-bottom: 4px;
+}
+
+.btn-back {
+ color: var(--blue);
+ font-size: 12px;
+ padding: 2px 4px;
+}
+.btn-back:hover { opacity: .75; }
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --fg: #e0e0e0;
+ --fg2: #888;
+ --bg: #1a1a1a;
+ --bg2: #222;
+ --border: #333;
+ --shadow: transparent;
+ }
+ input { background: #222; border-color: #444; }
+ input:focus { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(22,119,255,.15); }
+ input::placeholder { color: #666; }
+ .ide-card-code { background: rgba(22,119,255,.12); }
+}
diff --git a/plugins/ide-open/src/main.tsx b/plugins/ide-open/src/main.tsx
new file mode 100644
index 00000000..73fb2013
--- /dev/null
+++ b/plugins/ide-open/src/main.tsx
@@ -0,0 +1,6 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import './main.css'
+import App from './App'
+
+ReactDOM.createRoot(document.getElementById('root')!).render()
diff --git a/plugins/ide-open/src/store.ts b/plugins/ide-open/src/store.ts
new file mode 100644
index 00000000..5d3f4796
--- /dev/null
+++ b/plugins/ide-open/src/store.ts
@@ -0,0 +1,74 @@
+export interface IDEItem {
+ code: string
+ name: string
+ command: string
+ dbPath: string
+ shell?: string
+}
+
+export interface ProjectItem {
+ name: string
+ path: string
+ uri: string
+ type: 'folder' | 'file' | 'workspace' | 'remote'
+ label: string
+}
+
+const STORAGE_KEY = 'ide-ides'
+
+export function getIDEs(): IDEItem[] {
+ try {
+ const data = window.ztools.dbStorage.getItem(STORAGE_KEY)
+ return Array.isArray(data) ? data : []
+ } catch { return [] }
+}
+
+export function saveIDEs(ides: IDEItem[]) {
+ window.ztools.dbStorage.setItem(STORAGE_KEY, ides)
+ window.services?.registerFeatures()
+}
+
+export function addIDE(ide: IDEItem) {
+ const ides = getIDEs()
+ const idx = ides.findIndex(i => i.code === ide.code)
+ if (idx >= 0) ides[idx] = ide
+ else ides.push(ide)
+ saveIDEs(ides)
+}
+
+export function removeIDE(code: string) {
+ saveIDEs(getIDEs().filter(i => i.code !== code))
+}
+
+export function getPresets() {
+ return window.services?.getPresets() || {}
+}
+
+export function getQuickFillPresets() {
+ return window.services?.getQuickFillPresets() || []
+}
+
+export async function detectPresetPaths() {
+ return window.services?.detectPresetPaths() || {}
+}
+
+export async function readProjects(dbPath: string): Promise {
+ if (!window.services) throw new Error('preload 未就绪')
+ return window.services.readProjects(dbPath)
+}
+
+export async function openProject(command: string, uri: string, shell?: string) {
+ return window.services.openProject(command, uri, shell)
+}
+
+export async function deleteProject(dbPath: string, uri: string) {
+ return window.services.deleteProject(dbPath, uri)
+}
+
+export function getAppDataPath() {
+ return window.services?.getAppDataPath() || ''
+}
+
+export function getDefaultShell() {
+ return window.services?.getDefaultShell() || ''
+}
diff --git a/plugins/ide-open/tsconfig.json b/plugins/ide-open/tsconfig.json
new file mode 100644
index 00000000..d49c64de
--- /dev/null
+++ b/plugins/ide-open/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": false,
+ "noImplicitAny": false,
+ "types": ["@ztools-center/ztools-api-types"]
+ },
+ "include": ["src"]
+}
diff --git a/plugins/ide-open/vite.config.js b/plugins/ide-open/vite.config.js
new file mode 100644
index 00000000..2d511098
--- /dev/null
+++ b/plugins/ide-open/vite.config.js
@@ -0,0 +1,8 @@
+import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ base: './'
+})