diff --git a/.gitignore b/.gitignore index 065ac91..124a3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,18 +14,19 @@ coverage/ .turbo/ .idea/ .vscode/ -apps/server/opsxapi -apps/server/server -apps/desktop/src-tauri/gen/ *.exe *.dylib *.so Cargo.lock -!apps/desktop/src-tauri/Cargo.lock +!src-tauri/Cargo.lock logs/ .*.pid -apps/server/bin/ +server/bin/ +server/data/*.db +server/data/*.db-shm +server/data/*.db-wal +src-tauri/gen/ *.tsbuildinfo # UI review / acceptance screenshots (local only) diff --git a/Makefile b/Makefile index 4679e29..8c3680a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ install: npm install - npm --prefix apps/desktop install web: ./run.sh web @@ -26,7 +25,7 @@ desktop: ./run.sh desktop build-web: - npm run build:web + npm run build test: - npm run test:core + npm test diff --git a/README.md b/README.md index 2fc4b08..78cfa66 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,16 @@ **HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT,一个工作台全搞定;数据全在本地。** ``` -packages/core 数据模型 / 树操作 / 导入导出器(纯 TS,21 个单测) -packages/transport browser | gin-proxy | tauri-native + 流式/SSE/WS隧道/Socket.IO/MQTT 客户端 -packages/ui 共享工作台 UI(集合树 / 多标签 / 编辑器 / 对话框) -apps/web Vite + React 入口(:5173) -apps/desktop Tauri 2 壳(frontendDist → web dist) -apps/server Gin:HTTP 代理 + 流式代理 + WebSocket 隧道(:8787) +src/ 前端入口(React;进 Tauri WebView,勿当浏览器产品) +src-tauri/ Tauri 2 壳(唯一操作入口) +server/ Gin:代理 API + health(:8787;本地托管 homepage/dist) +homepage/ 官网落地页 + 文档静态站(不变) +src/core 数据模型 / 树操作 / 导入导出器 +src/transport browser | gin-proxy | tauri-native +src 工作台 UI ``` -核心原则:**一套 UI、一份请求契约,Web / Desktop 只换 Transport。** +核心原则:**一套 UI、一份请求契约;浏览器只看官网/docs,操作只走桌面。** ## 功能总览 @@ -53,14 +54,13 @@ apps/server Gin:HTTP 代理 + 流式代理 + WebSocket 隧道(:8787 ## 怎么跑 ```bash -make install # npm install(含 desktop 依赖) -./run.sh web # 浏览器:Gin(:8787) + Vite(:5173) ← 推荐 -./run.sh gui # 桌面 Tauri 窗口(共用 Vite) -./run.sh all # 全开 +make install +./run.sh web # 官网 + docs → http://127.0.0.1:8787/ +./run.sh gui # 桌面操作入口(推荐) ./run.sh stop | status ``` -打开 http://127.0.0.1:5173 —— 内置「示例集合」覆盖全部协议,开箱即用。 +打开 http://127.0.0.1:8787/ 看落地页;操作请用 `./run.sh gui`。`:5173` 只是桌面 WebView 源。 ## 用户文档站(/docs) @@ -69,7 +69,7 @@ Gin 内置产品文档,Markdown 嵌入二进制、SSR 渲染,随服务一键 - 地址:`http://127.0.0.1:8787/docs`(开发态 `http://127.0.0.1:5173/docs`,Vite 已反代;工作台顶栏「📖 文档」直达) - 布局:顶栏搜索 + 左侧分组导航(16 篇:开始使用 / 请求调试 / 数据管理 / 参考)+ 右侧本页目录 - 能力:全文搜索、代码块复制 + 高亮、提示块、上一页/下一页、反馈组件、打印导出 PDF、移动端响应式、404 页 -- 内容在 `apps/server/internal/docs/content/*.md`,改完重新 `go build` 即生效 +- 内容在 `server/internal/docs/content/*.md`(本地预览优先托管 `homepage/dist`) ## 产品官网(/home) @@ -81,7 +81,7 @@ Gin 内置营销单页,与 /docs 同为 SSR 嵌入交付(`internal/site`) - 让利机制四项:早鸟创始价(前 20 席 5–7 折永久)、教育/开源 6–8 折、多年预付 8–9 折、案例换授权;销售原则:不直接降价,用「永久授权 + 1 年升级 + 5 人团队 1–2 年回本」回应预算顾虑 - 交付前替换:联系邮箱 `hello@opsxapi.dev`(template.html 内两处)与对比表脚注中的价格核对 -## 服务端端点(apps/server) +## 服务端端点(server/) | 端点 | 用途 | | --- | --- | @@ -96,7 +96,7 @@ Gin 内置营销单页,与 /docs 同为 SSR 嵌入交付(`internal/site`) ## 架构决策 -- **一套 UI**:集合 / 编辑器 / 对话框全部在 `packages/ui`,Web 与桌面零分叉 +- **一套 UI**:集合 / 编辑器 / 对话框全部在 `src`,Web 与桌面零分叉 - **判别联合数据模型**:`node.kind === "http"` 自动收窄 `node.data` 类型 - **实时会话不落盘**:WS/SSE/Socket.IO/MQTT 会话存模块级注册表,消息(500 条上限)与状态进 store - **zustand 选择器必须返回稳定引用**:`?? []` 会触发 useSyncExternalStore 无限重渲染(踩过的坑) @@ -104,13 +104,56 @@ Gin 内置营销单页,与 /docs 同为 SSR 嵌入交付(`internal/site`) ## 测试 ```bash -make test # core 21 个单测:导入导出全格式往返、树操作、cURL 解析 -cd apps/server && go test ./... +npm test # core / transport / features 全量测试 +cd server && go test ./... ``` ## P0 自动化演示(本地) -1. `./run.sh web` → http://127.0.0.1:5173 +1. `./run.sh gui` → 桌面工作台(Gin `:8787` + Vite 源) 2. 进入自动化测试 → **装载回归示例** → **运行**(依赖本机 Gin `:8787/v1/echo`) 3. AI:**保存并运行** 与手动/定时共用同一执行器与报告 4. 定时:支持 5 段 Cron 或间隔;应用保持打开;见 `docs/HANDTEST-P0.md` + +## 根目录 Tauri 开发布局 + +前端全部位于 `src/`(`core/`、`transport/`、`features/`、`components/`),Rust 位于 `src-tauri/`。不再使用 `apps/` 或 npm workspace;`homepage/` 落地页保持独立,`server/` 保留本地辅助服务。 + +```bash +npm install +npm run build # 先类型检查,再构建前端 +npm test # 全量前端单测 +cargo test --manifest-path src-tauri/Cargo.toml --lib +./run.sh gui # 编译当前 Rust 后启动桌面,默认附带本地 Gin +``` + +`./run.sh gui` 每次通过 Cargo 增量编译当前源码,不再回退到旧 release。只运行原生桌面可用 `OPSXAPI_GUI_WITH_SERVER=0 ./run.sh gui`。标准 Tauri 开发热重载入口为 `npm run dev:desktop`。 + +## 请求代理与内网 Mock + +### API 文档与 OpenAPI 同步 + +侧栏「文档」以现有集合为项目,按 OpenAPI 标签分组浏览 HTTP 接口,显示参数、请求体、响应示例及说明。导入入口支持 OpenAPI 3.0/3.1、Swagger 2.0 的 JSON/YAML 文件、粘贴内容与 URL;导入时选择新建、按同名项目合并、或覆盖。合并按 HTTP 方法和相对路径更新同一接口,保留没有出现在本次导入里的接口;手动编辑并保存的说明与文档字段会在下次合并时保留。覆盖会移除该项目未导入的接口,使用前先检查选择。文档字段与请求一起保存在工作区备份;选择云端空间时也遵循现有工作区同步规则。 + +侧栏按「接口 → 文档 → Mock → 测试」排列。文档详情可直接打开请求页试用,或从 2xx 响应 example/schema 生成 Mock 草稿;已有相同方法和具体路径的 Mock 会显示关联状态并直接打开编辑,不覆盖手动修改的模拟响应。生成时把路径参数示例实例化为具体路径(例如 `/pets/{id}` 和 `id=42` 对应 `/pets/42`);发布后文档详情和内网只读文档快照均可展示对应 Mock 地址(快照需重新发布才会更新)。当前 Mock 服务只支持完整路径匹配,暂无动态路径匹配、请求体条件或多响应规则。 + +「发布当前项目」会将所选项目的名称、接口说明、参数和示例生成单独的本机 SQLite 只读快照;鉴权、环境变量及运行历史不会加入快照。分享地址是 `:8788/docs/<随机令牌>`,和 Mock 响应共用独立监听与来源限制。修改文档后需点「更新分享快照」;「撤销分享」立即令旧链接失效。公开访问需明确配置 `OPSXAPI_MOCK_ALLOW_PUBLIC=1`,自行处理 DNS/端口映射/HTTPS。分享页面不提供请求执行或编辑入口。 + +- 顶栏仅保留环境管理和环境切换;请求代理在「设置 → 网络」配置。支持自动检测、自定义 HTTP/HTTPS 代理、不使用代理、绕过主机和代理认证。配置仅存本机,不进入工作区同步/导出;适用于桌面 HTTP、GraphQL 及自动化测试,实时协议不受此设置控制。 +- 侧栏「Mock」是独立接口管理页面:新建接口 → 选择方法与路径 → 编辑 JSON/文本响应、状态码、延迟 → 保存草稿 → 发布全部已保存接口。也可在 HTTP 编辑器通过「创建 Mock」或「用响应创建 Mock」复制为独立接口,不修改原请求。 +- 草稿与未保存编辑缓存在本机浏览器存储;发布结果保存在本机 SQLite,不因登录而切换到团队或公网存储。发布会替换此本机服务的全部接口,禁用/删除接口需发布后生效;未保存编辑不参与发布。接口页提供已发布地址、复制和试运行。 +- Mock 页面顶部可直接启用或关闭 HTTP 响应,并可一键创建 `GET /demo` 示例;保存草稿后点击「发布全部已保存接口」,在已发布版本中试运行。设置 → Mock 服务保留相同开关及分享地址、环境关联配置。关闭后返回 503,保留数据和监听端口;彻底停止进程使用 `./run.sh stop`。随机数据生成规则位于「设置 → 数据生成」。 +- 第一版使用单服务、方法 + 完整路径匹配(忽略尾斜杠),暂不支持路径参数、多响应条件及请求体匹配。JSON 可使用已有 `@integer` 等模板表达式;文本响应原样返回。 +- `./run.sh gui` 默认启动本机管理服务 `127.0.0.1:8787` 和独立 Mock 响应监听 `0.0.0.0:8788`。后者默认仅接受回环/私有网段来源,不注册账号、发布或代理管理路由;内网同事按实际 HTTP 方法访问 `http://本机内网IP:8788/mock//接口路径`。 +- Mock 页的路径输入框只填写 `/example` 等相对路径;保存并发布后,编辑器顶部会显示可复制的完整地址。`http://localhost:5173/mock/example` 指向桌面开发界面,不是 Mock 响应。默认无需额外 Header,完整地址包含随机地址令牌。若要进一步限制已获知地址的访问,可在「设置 → Mock 服务 → 访问验证」生成并保存 Bearer 密钥;访问者须发送 `Authorization: Bearer <密钥>`,服务只保存密钥摘要。浏览器地址栏无法设置此 Header,请使用接口请求页或 curl;试运行框可临时填写密钥。 +- 分享域名需由内网 DNS/hosts 解析到运行本机服务的机器;HTTPS 由内网反向代理提供,可将域名解析到内网 IP,也可自行端口映射或反向代理至公网 IP;数据仍保存在自有机器。输入分享地址不会自动修改 DNS、监听或部署。 +- 若只允许本机访问,启动前设置 `OPSXAPI_MOCK_ADDR=127.0.0.1:8788`;更改监听配置需停止后重新启动。设置页会显示配置的监听地址及本机地址候选;访问仍需端口可达。 +- 更新后运行 `./run.sh gui` 会检查 Mock 管理接口,并在本项目旧后端缺少该接口时重启服务;也可手动运行 `./run.sh stop && ./run.sh gui`。`http://localhost:5173/mock` 不是内网 Mock 服务入口,`:5173` 只是桌面界面的开发源。已发布示例的分享地址为 `http://本机内网IP:8788/mock/<随机令牌>/demo`;令牌可在 Mock 页面「已发布版本」中复制。`homepage/` 不参与 Mock 数据保存。 + +### 环境地址联动与自定义域名 + +请求建议保存 `/pet/1` 等相对路径,环境分别填写前置 URL(例如本地 `http://127.0.0.1:3000`、正式 `https://api.example.com`)。顶栏切换环境会同时改变地址栏前缀、实际请求和代码生成地址;方法及接口路径不变。完整 URL 保持固定,可通过「关联环境」把匹配已知环境前缀的 URL 转为相对路径。 + +Mock 设置支持「使用此 Mock 环境」,生成前置 URL `<分享地址>/mock/`,同一接口路径即可在本地、正式、自托管 Mock 之间切换。下线后 token 失效,重新发布后需再次更新 Mock 环境。 + +分享地址支持自定义域名,用户自行配置 DNS、端口映射或反向代理及 HTTPS。若需要公网来源直接访问 Mock 响应端口,先停止服务,再运行 `OPSXAPI_MOCK_ALLOW_PUBLIC=1 ./run.sh gui`。此开关仅影响独立 Mock 响应端口,不开放发布、账号、工作区或代理管理接口,也不改变本地数据存储方式。 diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json deleted file mode 100644 index 48f0420..0000000 --- a/apps/desktop/package-lock.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "name": "@opsxapi/desktop", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@opsxapi/desktop", - "version": "0.1.0", - "devDependencies": { - "@tauri-apps/cli": "^2.4.0" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", - "dev": true, - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.4", - "@tauri-apps/cli-darwin-x64": "2.11.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", - "@tauri-apps/cli-linux-arm64-musl": "2.11.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-musl": "2.11.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", - "@tauri-apps/cli-win32-x64-msvc": "2.11.4" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - } - } -} diff --git a/apps/desktop/package.json b/apps/desktop/package.json deleted file mode 100644 index 9867536..0000000 --- a/apps/desktop/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@opsxapi/desktop", - "version": "0.1.0", - "private": true, - "scripts": { - "tauri": "tauri" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.4.0" - } -} diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png deleted file mode 100644 index 2bc70f0..0000000 Binary files a/apps/desktop/src-tauri/icons/128x128.png and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png deleted file mode 100644 index 94b0565..0000000 Binary files a/apps/desktop/src-tauri/icons/128x128@2x.png and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/256x256.png b/apps/desktop/src-tauri/icons/256x256.png deleted file mode 100644 index 94b0565..0000000 Binary files a/apps/desktop/src-tauri/icons/256x256.png and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png deleted file mode 100644 index e23bb22..0000000 Binary files a/apps/desktop/src-tauri/icons/32x32.png and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/512x512.png b/apps/desktop/src-tauri/icons/512x512.png deleted file mode 100644 index d777d0c..0000000 Binary files a/apps/desktop/src-tauri/icons/512x512.png and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png deleted file mode 100644 index 739fa50..0000000 Binary files a/apps/desktop/src-tauri/icons/64x64.png and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns deleted file mode 100644 index f55d7c8..0000000 Binary files a/apps/desktop/src-tauri/icons/icon.icns and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico deleted file mode 100644 index d73f964..0000000 Binary files a/apps/desktop/src-tauri/icons/icon.ico and /dev/null differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png deleted file mode 100644 index 1a5c156..0000000 Binary files a/apps/desktop/src-tauri/icons/icon.png and /dev/null differ diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs deleted file mode 100644 index 4dcacda..0000000 --- a/apps/desktop/src-tauri/src/lib.rs +++ /dev/null @@ -1,293 +0,0 @@ -use futures_util::{SinkExt, StreamExt}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Mutex; -use std::time::{Duration, Instant}; -use tauri::{AppHandle, Emitter, Manager, State}; -use tokio::sync::mpsc; -use tokio_tungstenite::tungstenite::Message; -use uuid::Uuid; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BodySpec { - mode: String, - raw: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct HttpSpec { - method: String, - url: String, - #[serde(default)] - headers: HashMap, - query: Option>, - body: Option, - timeout_ms: Option, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct HttpResult { - status: u16, - headers: HashMap, - body: String, - time_ms: u128, - size: usize, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WsSpec { - url: String, - protocols: Option>, - #[serde(default)] - headers: HashMap, -} - -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct WsEvent { - session_id: String, - kind: String, - data: String, -} - -enum WsCmd { - Send(String), - Close, -} - -struct WsHandle { - tx: mpsc::UnboundedSender, -} - -#[derive(Default)] -struct WsState { - sessions: Mutex>, -} - -fn validate_http_url(raw: &str) -> Result { - let parsed = url::Url::parse(raw).map_err(|e| format!("invalid URL: {e}"))?; - match parsed.scheme() { - "http" | "https" => {} - _ => return Err("only http/https schemes are allowed".into()), - } - match parsed.host_str().unwrap_or("").to_ascii_lowercase().as_str() { - "169.254.169.254" | "metadata.google.internal" => Err("blocked metadata host".into()), - _ => Ok(parsed), - } -} - -fn validate_ws_url(raw: &str) -> Result { - let mut parsed = url::Url::parse(raw).map_err(|e| format!("invalid URL: {e}"))?; - match parsed.scheme() { - "ws" | "wss" => {} - "http" => { - parsed - .set_scheme("ws") - .map_err(|_| "cannot set ws scheme".to_string())?; - } - "https" => { - parsed - .set_scheme("wss") - .map_err(|_| "cannot set wss scheme".to_string())?; - } - _ => return Err("only ws/wss schemes are allowed".into()), - } - match parsed.host_str().unwrap_or("").to_ascii_lowercase().as_str() { - "169.254.169.254" | "metadata.google.internal" => { - return Err("blocked metadata host".into()) - } - _ => {} - } - Ok(parsed.to_string()) -} - -#[tauri::command] -async fn http_send(spec: HttpSpec) -> Result { - let mut url = validate_http_url(&spec.url)?; - if let Some(query) = &spec.query { - let mut pairs = url.query_pairs_mut(); - for (key, value) in query { - pairs.append_pair(key, value); - } - } - - let method = reqwest::Method::from_bytes(spec.method.as_bytes()) - .map_err(|e| format!("invalid method: {e}"))?; - let timeout = Duration::from_millis(spec.timeout_ms.unwrap_or(30_000)); - let client = reqwest::Client::builder() - .timeout(timeout) - .build() - .map_err(|e| e.to_string())?; - let mut request = client.request(method, url); - for (key, value) in &spec.headers { - request = request.header(key, value); - } - if let Some(body) = spec.body { - if body.mode != "none" { - request = request.body(body.raw.unwrap_or_default()); - } - } - - let started = Instant::now(); - let response = request.send().await.map_err(|e| e.to_string())?; - let status = response.status().as_u16(); - let headers = response - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - let bytes = response.bytes().await.map_err(|e| e.to_string())?; - let size = bytes.len(); - let body = String::from_utf8_lossy(&bytes).into_owned(); - Ok(HttpResult { - status, - headers, - body, - time_ms: started.elapsed().as_millis(), - size, - }) -} - -#[tauri::command] -async fn ws_connect( - app: AppHandle, - state: State<'_, WsState>, - spec: WsSpec, -) -> Result { - let url = validate_ws_url(&spec.url)?; - let session_id = Uuid::new_v4().to_string(); - - let mut req = http::Request::builder().method("GET").uri(&url); - for (k, v) in &spec.headers { - req = req.header(k.as_str(), v.as_str()); - } - if let Some(protocols) = &spec.protocols { - let joined = protocols - .iter() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect::>() - .join(", "); - if !joined.is_empty() { - req = req.header("Sec-WebSocket-Protocol", joined); - } - } - let request = req - .body(()) - .map_err(|e| format!("build ws request: {e}"))?; - - let (tx, mut rx) = mpsc::unbounded_channel::(); - { - let mut map = state.sessions.lock().map_err(|e| e.to_string())?; - map.insert(session_id.clone(), WsHandle { tx }); - } - - let sid = session_id.clone(); - let app2 = app.clone(); - tauri::async_runtime::spawn(async move { - let emit = |kind: &str, data: String| { - let _ = app2.emit( - "ws-event", - WsEvent { - session_id: sid.clone(), - kind: kind.to_string(), - data, - }, - ); - }; - - match tokio_tungstenite::connect_async(request).await { - Ok((ws, _)) => { - emit("status", "open".into()); - let (mut write, mut read) = ws.split(); - loop { - tokio::select! { - cmd = rx.recv() => { - match cmd { - Some(WsCmd::Send(data)) => { - if write.send(Message::Text(data.into())).await.is_err() { - emit("status", "error".into()); - break; - } - } - Some(WsCmd::Close) | None => { - let _ = write.close().await; - break; - } - } - } - msg = read.next() => { - match msg { - Some(Ok(Message::Text(t))) => emit("message", t.to_string()), - Some(Ok(Message::Binary(b))) => { - emit("message", format!("[binary {} bytes]", b.len())) - } - Some(Ok(Message::Close(_))) | None => break, - Some(Ok(_)) => {} - Some(Err(e)) => { - emit("message", format!("[ws error] {e}")); - emit("status", "error".into()); - break; - } - } - } - } - } - emit("status", "closed".into()); - } - Err(e) => { - emit("message", format!("[ws_connect error] {e}")); - emit("status", "error".into()); - emit("status", "closed".into()); - } - } - - if let Some(st) = app2.try_state::() { - if let Ok(mut map) = st.sessions.lock() { - map.remove(&sid); - } - } - }); - - Ok(session_id) -} - -#[tauri::command] -async fn ws_send(state: State<'_, WsState>, session_id: String, data: String) -> Result<(), String> { - let map = state.sessions.lock().map_err(|e| e.to_string())?; - let handle = map - .get(&session_id) - .ok_or_else(|| format!("unknown ws session {session_id}"))?; - handle - .tx - .send(WsCmd::Send(data)) - .map_err(|_| "ws session closed".to_string())?; - Ok(()) -} - -#[tauri::command] -async fn ws_close(state: State<'_, WsState>, session_id: String) -> Result<(), String> { - let mut map = state.sessions.lock().map_err(|e| e.to_string())?; - if let Some(handle) = map.remove(&session_id) { - let _ = handle.tx.send(WsCmd::Close); - } - Ok(()) -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tauri::Builder::default() - .manage(WsState::default()) - .invoke_handler(tauri::generate_handler![ - http_send, - ws_connect, - ws_send, - ws_close - ]) - .run(tauri::generate_context!()) - .expect("error while running OpsxAPI desktop"); -} diff --git a/apps/server/data/opsxapi.db b/apps/server/data/opsxapi.db deleted file mode 100644 index cc04c10..0000000 Binary files a/apps/server/data/opsxapi.db and /dev/null differ diff --git a/apps/server/data/opsxapi.db-shm b/apps/server/data/opsxapi.db-shm deleted file mode 100644 index 7a967bc..0000000 Binary files a/apps/server/data/opsxapi.db-shm and /dev/null differ diff --git a/apps/server/data/opsxapi.db-wal b/apps/server/data/opsxapi.db-wal deleted file mode 100644 index b521df8..0000000 Binary files a/apps/server/data/opsxapi.db-wal and /dev/null differ diff --git a/apps/server/internal/server/router.go b/apps/server/internal/server/router.go deleted file mode 100644 index 5bf5a13..0000000 --- a/apps/server/internal/server/router.go +++ /dev/null @@ -1,73 +0,0 @@ -package server - -import ( - "net/http" - "os" - "strings" - - "github.com/gin-gonic/gin" - "opsxapi/server/internal/api" - "opsxapi/server/internal/docs" - "opsxapi/server/internal/echo" - "opsxapi/server/internal/mocksrv" - "opsxapi/server/internal/proxy" - "opsxapi/server/internal/site" -) - -func NewRouter() *gin.Engine { - r := gin.New() - _ = r.SetTrustedProxies(nil) - r.Use(gin.Logger(), gin.Recovery(), localhostCORS()) - - r.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"ok": true, "service": "opsxapi"}) - }) - p := proxy.NewHandler() - r.POST("/v1/proxy/http", p.ProxyHTTP) - r.POST("/v1/proxy/http/stream", p.ProxyHTTPStream) - - tunnel := proxy.NewWSTunnel() - r.POST("/v1/proxy/ws/connect", tunnel.Connect) - r.GET("/v1/proxy/ws/attach", tunnel.Attach) - r.POST("/v1/proxy/ws/close", tunnel.Close) - - // 账号 / 团队 / 云端空间(SQLite),store 同时供 Mock 服务复用 - accountAPI, err := api.New(os.Getenv("OPSXAPI_DB")) - if err != nil { - panic("init sqlite store: " + err.Error()) - } - accountAPI.Register(r) - - mocksrv.New(accountAPI.Store()).Register(r) - - echo.Mount(r) - docs.New().Register(r) - site.New().Register(r) - - return r -} - -func localhostCORS() gin.HandlerFunc { - allowed := map[string]struct{}{ - "http://localhost:5173": {}, - "http://127.0.0.1:5173": {}, - "http://localhost:1420": {}, - "http://127.0.0.1:1420": {}, - "tauri://localhost": {}, - "http://tauri.localhost": {}, - } - return func(c *gin.Context) { - origin := c.GetHeader("Origin") - if _, ok := allowed[strings.TrimSuffix(origin, "/")]; ok { - c.Header("Access-Control-Allow-Origin", origin) - c.Header("Vary", "Origin") - c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie, X-Opsx-Probe, X-Opsx-Cookie") - c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - } - if c.Request.Method == http.MethodOptions { - c.AbortWithStatus(http.StatusNoContent) - return - } - c.Next() - } -} diff --git a/apps/web/package.json b/apps/web/package.json deleted file mode 100644 index a6e506e..0000000 --- a/apps/web/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@opsxapi/web", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 127.0.0.1 --port 5173", - "build": "tsc -b && vite build", - "preview": "vite preview --host 127.0.0.1 --port 5173" - }, - "dependencies": { - "@opsxapi/core": "*", - "@opsxapi/transport": "*", - "@opsxapi/ui": "*", - "@tauri-apps/api": "^2.5.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@types/node": "^22.20.3", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^4.4.1", - "autoprefixer": "^10.4.21", - "postcss": "^8.5.3", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.2", - "vite": "^6.2.2" - } -} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx deleted file mode 100644 index efb08cb..0000000 --- a/apps/web/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import { OpsxWorkspace } from "@opsxapi/ui"; -import "./styles.css"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json deleted file mode 100644 index ea9d0cd..0000000 --- a/apps/web/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.node.json" - } - ] -} diff --git a/docs/api-tools-comparison.md b/docs/api-tools-comparison.md new file mode 100644 index 0000000..c937700 --- /dev/null +++ b/docs/api-tools-comparison.md @@ -0,0 +1,18 @@ +# OpsxAPI 与 Bruno / Hoppscotch / Yaak 对照(2026-10-15) + +评估以本机 `/Users/logan/devops/demo/api/{bruno,hoppscotch,yaak}` 的源码为准;这里比较交互模式,不代表三个项目的全部功能或版本承诺。OpsxAPI 保持桌面请求工作台 + 内网 Mock / 文档分享的产品方向。 + +| 任务 | 参照实现 | OpsxAPI 现状 | 取舍与优先级 | +| --- | --- | --- | --- | +| 阅读 HTTP 响应 | Bruno 的响应格式选择和响应头计数(`bruno-app/src/components/ResponsePane/index.js`、`QueryResultTypeSelector/index.jsx`);Yaak 的 Pretty / Raw 与复制、保存动作(`yaak-client/components/HttpResponsePane.tsx`) | 已有 Raw、响应头、状态与耗时。此次补齐 Content-Type 判断、折行、按行数增长的行号、可折叠 Pretty JSON、键/值/标点分色、窄窗布局、图标复制及大响应保护 | **高,已落地基础项。** 下一步优先做响应内搜索;图片/PDF/时间线先按真实需求排期。 | +| 请求与响应示例 | Hoppscotch 将当前响应保存为具名示例(`hoppscotch-common/src/components/http/Response.vue`) | 文档有响应示例,Mock 可从实际响应生成。此次增加“存为示例”,按状态码写入文档,同状态码先确认覆盖 | **高,已落地基础项。** 后续可考虑为同一状态码保留多条具名示例,但需先扩展内部文档结构和导入/导出格式。 | +| 环境与变量 | Hoppscotch 在参数和请求体编辑器中突出环境变量(`http/Parameters.vue`、`RawBody.vue`);Yaak 将主题、界面与代理设置分组 | OpsxAPI 已有环境切换、相对路径拼接、变量补全与本机代理。此次外观新增跟随系统主题 | **中。** 在请求地址旁持续展示解析后的目标;缺失变量应就地提示,不把更多说明塞进设置页。 | +| 自动化测试 | Hoppscotch 的请求前脚本、测试结果和 runner(`http/PreRequestScript.vue`、`http/test/Runner.vue`);Bruno 的测试结果与时间线(`ResponsePane/index.js`) | 已有用例步骤、断言、变量提取、报告和定时运行;没有任意脚本能力 | **中。** 先改善失败步骤定位及上次运行对照。脚本沙箱、变量作用域和权限边界明确后再引入脚本,不直接复制整个测试平台。 | +| 设置与快捷操作 | Yaak 将代理、证书、快捷键、主题分开(`yaak-client/components/Settings/Settings.tsx`) | 代理、外观、大模型、数据管理、Mock 服务已分区;快捷键和证书管理尚无完整入口 | **中。** 先补“发送 / 保存 / 切换请求 / 搜索”的快捷键及可发现提示,再做冲突处理;证书管理需结合桌面传输实现。 | +| 请求数据生成 | Bruno 在编辑器补全中提供 `$` 动态变量,并在 Mock 响应生成对话框中按 schema 生成示例;Hoppscotch 在编辑器高亮预定义变量(`HoppPredefinedVariables.ts`),通用设置页未单列随机规则;Yaak 使用请求模板函数及 Faker 插件 | OpsxAPI 的 `@` 表达式、字段匹配和预览在发送请求时生效,与 8788 内网 Mock 服务独立。保留开关、规则及预览,将“数据生成”从设置一级导航移到“网络 → 请求数据生成”,并在 HTTP Body 编辑器提供入口 | **保留能力、降低层级。** 暂不引入插件市场、脚本式生成器或更多设置开关;如后续发现规则维护频繁,可再做请求级规则作用域。 | +| 大模型配置 | OpsxShell 的供应商列表、详情表单、连接测试和明确的默认状态(`opsxshell/src/components/settings/AiProviderSettings.tsx`) | 目前仅支持 OpenAI 兼容 Chat Completions,用于自动化测试用例生成;按“模型 / 连接 / 测试与保存”重排并显示默认、未保存与测试反馈 | **保持协议边界。** OpsxShell 的 Anthropic、Responses、多模型与计费字段依赖其专用客户端和数据结构,不能仅通过增加表单照搬。当前 API Key 随工作区保存,团队空间同步会包含此字段,后续应单独评估系统凭据存储。 | +| 分享与 Mock | 三者重心分别偏向可复现集合、协作及本地请求体验 | OpsxAPI 的文档与 Mock 均能经独立本机服务分享到内网,且有服务开关与访问控制 | **保持优势。** 下一步应优先完善文档与 Mock 的同源状态提示、发布差异预览和真实内网访问反馈。 | + +交互原则:保留“响应体 / Raw / 响应头”这几个稳定入口;把状态、耗时、大小与复制动作做成在窄窗口仍可阅读的工具栏;设置项按任务归类,只有影响安全或数据保存的说明才占据常显空间。暂不引入竞品的全部脚本、插件或媒体预览,以免把请求工作台变成难维护的多套运行时。 + +本轮验证:`npm test -- --run`(78 项)、`npm run build`、`git diff --check`。本机 GUI 的自动查看被运行环境拒绝,窄窗的实际视觉效果仍待桌面窗口验收。 diff --git a/docs/recovery/2026-10-14.md b/docs/recovery/2026-10-14.md new file mode 100644 index 0000000..1aeeecc --- /dev/null +++ b/docs/recovery/2026-10-14.md @@ -0,0 +1,35 @@ +# 2026-10-14 开发代码恢复记录 + +## 恢复依据 + +当前 HEAD 为 `6452026`,reflog 保留了迁移半成品提交 `ca24b9c`。`stash@{0}` 及其第三父提交(未跟踪文件快照)保存了此前的开发内容。恢复采用逐文件映射,没有执行整体 stash apply、reset 或覆盖当前暂存的目录迁移。 + +已安装 `/Applications/OpsxAPI.app` 的可执行文件时间为 2026-09-19;包内仅找到可执行文件、图标和 Info.plist,未见独立源码或 source map。因此优先采用 Git 中的原始源码,未修改已安装 App。 + +截图中的环境管理(前置 URL、变量搜索)和八组合回归测试在恢复前源码中已经存在。 + +## 已恢复 + +- `src-tauri/src/`:Command、Service、Model、AppError 分层及相关测试。 +- `packages/transport/src/`:统一 IPC 类型/错误处理、WebSocket 生命周期修复和测试。 +- `packages/ui/src/features/`:workspace/testing 功能模块,执行状态共享与去重。 +- 通用按钮/空态、报告表格、主题 token、首屏主题恢复、关于页调整。 +- `package.json`:完整测试入口及独立类型检查命令。 + +保留当前 `src-tauri/`、`server/`、`src/` 与 `packages/` 布局,没有继续完成或回退整库目录迁移。 + +## 排除的半成品 + +- 外链 opener 调用缺少当前前后端插件接入,未恢复;保留现有链接方式。 +- Mock 状态卡依赖不存在的 GET `/mock/local`,未恢复该卡片及其调用函数。 +- 已撤销的 AGENTS.md 规则没有重新安装;历史文档仅保存在恢复备份中。 + +## 验证 + +- `npm run typecheck`:通过。 +- `npm run build`:通过,仍有大于 500 kB 的分包提示。 +- `npm test`:59 项通过(core 53、transport 4、UI 逻辑 2)。 +- `cargo test --manifest-path src-tauri/Cargo.toml --lib --offline`:3 项通过,1 项回环集成测试默认忽略。 +- 尚未完成桌面视觉、外部服务和真实 WebSocket 回环验收;不代表全部遗失开发均已找回。 + +恢复前文件、工作区/暂存区补丁、恢复映射及历史文档保存在 `/tmp/opsxapi-recovery-20261014`(临时目录,应另行长期备份)。原 stash 保持不变,恢复内容未自动提交。 diff --git a/apps/web/index.html b/index.html similarity index 50% rename from apps/web/index.html rename to index.html index 0da3773..d875da4 100644 --- a/apps/web/index.html +++ b/index.html @@ -5,6 +5,15 @@ + OpsxAPI diff --git a/package-lock.json b/package-lock.json index d074a02..609c66e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,44 +5,33 @@ "packages": { "": { "name": "opsxapi", - "workspaces": [ - "apps/web", - "packages/*" - ], "dependencies": { + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-opener": "^2.7.1", "js-yaml": "^5.4.2", "mqtt": "^5.16.0", - "socket.io-client": "^4.8.3" - }, - "devDependencies": { - "@types/js-yaml": "^4.0.9", - "playwright": "^1.55.0" - }, - "engines": { - "node": ">=20" - } - }, - "apps/web": { - "name": "@opsxapi/web", - "version": "0.1.0", - "dependencies": { - "@opsxapi/core": "*", - "@opsxapi/transport": "*", - "@opsxapi/ui": "*", - "@tauri-apps/api": "^2.5.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "socket.io-client": "^4.8.3", + "zustand": "^5.0.15" }, "devDependencies": { + "@tauri-apps/cli": "^2.4.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^22.20.3", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.4.1", "autoprefixer": "^10.4.21", + "playwright": "^1.55.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.2", - "vite": "^6.2.2" + "vite": "^6.2.2", + "vitest": "^3.2.7" + }, + "engines": { + "node": ">=20" } }, "node_modules/@alloc/quick-lru": { @@ -841,22 +830,6 @@ "node": ">= 8" } }, - "node_modules/@opsxapi/core": { - "resolved": "packages/core", - "link": true - }, - "node_modules/@opsxapi/transport": { - "resolved": "packages/transport", - "link": true - }, - "node_modules/@opsxapi/ui": { - "resolved": "packages/ui", - "link": true - }, - "node_modules/@opsxapi/web": { - "resolved": "apps/web", - "link": true - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1195,14 +1168,256 @@ "license": "MIT" }, "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.3.tgz", + "integrity": "sha512-9s0PKw0p4r3kFq92jhpivYSp+cSht15o0gXpZxuQd0+dV9KCexEVkNaLELVoLdJWyoH4KlfQo5h3W2LIM8siJg==", + "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/tauri" } }, + "node_modules/@tauri-apps/cli": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.2.tgz", + "integrity": "sha512-PMSppzAnlT+BvZtRKuidxjRSm2r5t8kDpJ/czmmQUa0AtDNTwrjLE8vApK3phCWGg7isfnYL46Dl1jB4ArI0hA==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.12.2", + "@tauri-apps/cli-darwin-x64": "2.12.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.12.2", + "@tauri-apps/cli-linux-arm64-musl": "2.12.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.12.2", + "@tauri-apps/cli-linux-x64-gnu": "2.12.2", + "@tauri-apps/cli-linux-x64-musl": "2.12.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.12.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.12.2", + "@tauri-apps/cli-win32-x64-msvc": "2.12.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.2.tgz", + "integrity": "sha512-Lk3zaBTfzNvA7670Al5PNgo2M/95uaDPe1yp+ILJeDOmol87XiYuK5E5+FUbK5e6U6OORx9qpZdOo2U+5tETtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.2.tgz", + "integrity": "sha512-2CyYjJ833JB3sKRz4B0W/Tv4oOVC16ws1bxdSCWj1XvrmanWwvZi99e5tNIoCHwHhcOm0jh+sh0PCGQyls6Z+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.2.tgz", + "integrity": "sha512-uvt2xbone4JftKarelssi86Kqnl7JiG1Smh5+Ae+nZmtM+JUhTGmqkcqfUPXRW8Ygwog92POZvd4oGDN9j8eXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.2.tgz", + "integrity": "sha512-DZlDJ24u3tlocKOYqzR+b2M+Qr+5WFcoMBZ7EfdhHN5j3ZXwBBpms4G4gyNvPCrak5vMzmdZ65ecBXPJcC+5sA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.2.tgz", + "integrity": "sha512-pb+RTZUYH/p0gR4j4ecCCq+FNfz3W3eKuUQvVEBn7MQZx6DzfLO7FaYzt2NyDKWpdNN3lP10mdzOvw3R5P5aAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.2.tgz", + "integrity": "sha512-nidEvDUJyHYNbIv9MUk6KiEix8IXfRh4Wj2G2SA5g8nFmgbBCwazUNWYiG/LhNeNbfZpBMLerRBUtoQ5K0GidA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.2.tgz", + "integrity": "sha512-gkXyYn3w+Xfc8nz/DleEbkm1M7bBuU2cejqUXNmI98cA2P5kp+oPmS4ALTorb1FaNWjpsKoYZBWsbS9BXxYFkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.2.tgz", + "integrity": "sha512-MrjgoZ09s8Hsn99XvxSNvK8NQTZxi7XiyK3FxxytzfHgz2jb3qUT69CZtsokRkN/zdi9rCgrMtq+w9XNICjcfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.2.tgz", + "integrity": "sha512-BR+CYM7iF3lkfyTtVkwMQU/s9hwegLpj3UqDjaxKCPmixQCa1csBBPy3C75V+FhLtOjIXNna4Gt6Vm9oxbV+qA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.2.tgz", + "integrity": "sha512-zODJo9l3TbNBM+GrO/yPqGeELNMK5cjIc+QHh1WQc3l8tD+jDZGTQyYuAxViCsM6Uh5uWJ//RnCZG9LXrvsXww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.2.tgz", + "integrity": "sha512-cQXI2ZaZmwBDt0XATCqASVanYizxkEE3ceQVlVHXaQ51HzsQUWA04c9Nz7LrrY2lgAu3jFMyat+8Yz3Z2R2qBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.1.tgz", + "integrity": "sha512-XkXdt30rontti9TII/gBBxFRXWfj8DiprFW7RgBFRBj1OhdfjBNVcFqSEWkkZ4hXafHnuEnB7F1sFsmu6uyywQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3662,6 +3877,7 @@ "version": "5.0.15", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "license": "MIT", "engines": { "node": ">=12.20.0" }, @@ -3685,78 +3901,6 @@ "optional": true } } - }, - "packages/core": { - "name": "@opsxapi/core", - "version": "0.1.0", - "dependencies": { - "js-yaml": "^4.1.1" - }, - "devDependencies": { - "@types/js-yaml": "^4.0.9", - "typescript": "^5.8.2", - "vitest": "^3.0.9" - } - }, - "packages/core/node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "packages/transport": { - "name": "@opsxapi/transport", - "version": "0.1.0", - "dependencies": { - "@opsxapi/core": "*", - "mqtt": "^5.10.1", - "socket.io-client": "^4.8.1" - }, - "devDependencies": { - "typescript": "^5.8.2" - }, - "peerDependencies": { - "@tauri-apps/api": "^2.0.0" - }, - "peerDependenciesMeta": { - "@tauri-apps/api": { - "optional": true - } - } - }, - "packages/ui": { - "name": "@opsxapi/ui", - "version": "0.1.0", - "dependencies": { - "@opsxapi/core": "*", - "@opsxapi/transport": "*", - "zustand": "^5.0.3" - }, - "devDependencies": { - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "typescript": "^5.8.2" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - } } } } diff --git a/package.json b/package.json index 0b79410..2467281 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,48 @@ { "name": "opsxapi", "private": true, - "workspaces": [ - "apps/web", - "packages/*" - ], + "type": "module", "scripts": { - "dev:web": "npm run dev -w @opsxapi/web", - "build:web": "npm run build -w @opsxapi/web", - "test": "npm run test:core", - "test:core": "npm run test -w @opsxapi/core", - "dev:desktop": "npm --prefix apps/desktop run tauri -- dev", - "build:desktop": "npm --prefix apps/desktop run tauri -- build" + "dev": "vite --host 127.0.0.1 --port 5173", + "dev:ui": "vite --host 127.0.0.1 --port 5173", + "build": "npm run typecheck && vite build", + "build:ui": "npm run build", + "preview": "vite preview --host 127.0.0.1 --port 5173", + "tauri": "tauri", + "dev:desktop": "tauri dev", + "build:desktop": "tauri build", + "test": "vitest run", + "test:core": "vitest run src/core", + "test:transport": "vitest run src/transport", + "test:ui": "vitest run src/features", + "typecheck": "tsc -p tsconfig.app.json --noEmit" }, "engines": { "node": ">=20" }, "dependencies": { + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-opener": "^2.7.1", "js-yaml": "^5.4.2", "mqtt": "^5.16.0", - "socket.io-client": "^4.8.3" + "react": "^19.0.0", + "react-dom": "^19.0.0", + "socket.io-client": "^4.8.3", + "zustand": "^5.0.15" }, "devDependencies": { + "@tauri-apps/cli": "^2.4.0", "@types/js-yaml": "^4.0.9", - "playwright": "^1.55.0" + "@types/node": "^22.20.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "playwright": "^1.55.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", + "vite": "^6.2.2", + "vitest": "^3.2.7" } } diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index daa55e2..0000000 --- a/packages/core/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@opsxapi/core", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "js-yaml": "^4.1.1" - }, - "devDependencies": { - "@types/js-yaml": "^4.0.9", - "typescript": "^5.8.2", - "vitest": "^3.0.9" - } -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index a47cede..0000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src" - ] -} diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts deleted file mode 100644 index f624398..0000000 --- a/packages/core/vitest.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - }, -}); diff --git a/packages/transport/package.json b/packages/transport/package.json deleted file mode 100644 index 1c6eee0..0000000 --- a/packages/transport/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@opsxapi/transport", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "dependencies": { - "@opsxapi/core": "*", - "mqtt": "^5.10.1", - "socket.io-client": "^4.8.1" - }, - "devDependencies": { - "typescript": "^5.8.2" - }, - "peerDependencies": { - "@tauri-apps/api": "^2.0.0" - }, - "peerDependenciesMeta": { - "@tauri-apps/api": { - "optional": true - } - } -} \ No newline at end of file diff --git a/packages/transport/src/tauriNative.ts b/packages/transport/src/tauriNative.ts deleted file mode 100644 index 55f377f..0000000 --- a/packages/transport/src/tauriNative.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { HttpResult, HttpSpec, Transport, WsSession, WsSpec } from "@opsxapi/core"; - -async function invoke(cmd: string, args?: Record): Promise { - try { - const { invoke: tauriInvoke } = await import("@tauri-apps/api/core"); - return await tauriInvoke(cmd, args); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - throw new Error( - `tauri-native invoke("${cmd}") failed (are you running inside OpsxAPI desktop?): ${msg}`, - ); - } -} - -export function isTauriRuntime(): boolean { - if (typeof window === "undefined") return false; - return Boolean((window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__); -} - -type WsEventPayload = { - sessionId: string; - kind: string; - data: string; -}; - -export const tauriNativeTransport: Transport = { - async http(spec: HttpSpec): Promise { - return invoke("http_send", { spec }); - }, - - ws(spec: WsSpec): WsSession { - const messageCbs: Array<(data: string) => void> = []; - const statusCbs: Array<(s: string) => void> = []; - let sessionId: string | null = null; - let unlisten: (() => void) | null = null; - let closed = false; - - const emitStatus = (s: string) => statusCbs.forEach((cb) => cb(s)); - - void (async () => { - try { - emitStatus("connecting"); - const { listen } = await import("@tauri-apps/api/event"); - unlisten = await listen("ws-event", (ev) => { - const p = ev.payload; - if (!sessionId || p.sessionId !== sessionId) return; - if (p.kind === "message") { - messageCbs.forEach((cb) => cb(p.data)); - return; - } - if (p.kind === "status") { - emitStatus(p.data); - if (p.data === "closed" || p.data === "error") { - // keep listening until explicit close() - } - } - }); - sessionId = await invoke("ws_connect", { spec }); - // open status also arrives via event; emit early for snappy UI - } catch (e) { - emitStatus("error"); - messageCbs.forEach((cb) => - cb(`[ws_connect error] ${e instanceof Error ? e.message : String(e)}`), - ); - emitStatus("closed"); - } - })(); - - return { - onMessage(cb) { - messageCbs.push(cb); - }, - onStatus(cb) { - statusCbs.push(cb); - }, - send(data) { - if (!sessionId) { - messageCbs.forEach((cb) => cb("[ws] not connected")); - return; - } - void invoke("ws_send", { sessionId, data }).catch((e) => { - messageCbs.forEach((cb) => - cb(`[ws_send error] ${e instanceof Error ? e.message : String(e)}`), - ); - }); - }, - close() { - if (closed) return; - closed = true; - const id = sessionId; - sessionId = null; - unlisten?.(); - unlisten = null; - if (!id) { - emitStatus("closed"); - return; - } - void invoke("ws_close", { sessionId: id }).finally(() => emitStatus("closed")); - }, - }; - }, -}; diff --git a/packages/transport/tsconfig.json b/packages/transport/tsconfig.json deleted file mode 100644 index a47cede..0000000 --- a/packages/transport/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src" - ] -} diff --git a/packages/ui/package.json b/packages/ui/package.json deleted file mode 100644 index 629f267..0000000 --- a/packages/ui/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@opsxapi/ui", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "dependencies": { - "@opsxapi/core": "*", - "@opsxapi/transport": "*", - "zustand": "^5.0.3" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "typescript": "^5.8.2" - } -} diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx deleted file mode 100644 index fe5c96a..0000000 --- a/packages/ui/src/App.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** - * OpsxAPI 工作台入口:组装顶栏 / 侧栏 / 标签页 / 编辑器 / 对话框。 - */ -import { findNode } from "@opsxapi/core"; -import { useEffect } from "react"; -import { closeSession } from "./send"; -import { Icon } from "./components/icons"; -import { Logo } from "./components/Logo"; -import { useWorkspace } from "./workspaceStore"; -import { ActivityRail } from "./components/ActivityRail"; -import { Sidebar } from "./components/Sidebar"; -import { TestingView } from "./components/TestingView"; -import { startScheduler, stopScheduler } from "./scheduler"; -import { TabBar } from "./components/TabBar"; -import { TopBar } from "./components/TopBar"; -import { GraphqlEditor } from "./components/editors/GraphqlEditor"; -import { HttpEditor } from "./components/editors/HttpEditor"; -import { MqttEditor } from "./components/editors/MqttEditor"; -import { SocketIoEditor } from "./components/editors/SocketIoEditor"; -import { SseEditor } from "./components/editors/SseEditor"; -import { WebsocketEditor } from "./components/editors/WebsocketEditor"; -import { ExportDialog } from "./components/dialogs/ExportDialog"; -import { ImportDialog } from "./components/dialogs/ImportDialog"; -import { AuthDialog, TeamDialog } from "./components/dialogs/AccountDialogs"; -import { MockLinkDialog } from "./components/dialogs/MockLinkDialog"; -import { - EnvironmentsDialog, - NewItemDialog, - RenameDialog, - SettingsDialog, -} from "./components/dialogs/WorkspaceDialogs"; - -export function OpsxWorkspace() { - const init = useWorkspace((s) => s.init); - const inited = useWorkspace((s) => s.collections.length > 0 || s.environments.length > 0); - const collections = useWorkspace((s) => s.collections); - const activeTabId = useWorkspace((s) => s.activeTabId); - const dialog = useWorkspace((s) => s.dialog); - const toast = useWorkspace((s) => s.toast); - const clearToast = useWorkspace((s) => s.clearToast); - const openTab = useWorkspace((s) => s.openTab); - const view = useWorkspace((s) => s.view); - - useEffect(() => { - init(); - }, [init]); - - // 关闭标签页时断开实时会话 - useEffect(() => { - useWorkspace.subscribe((state, prev) => { - const closed = prev.openTabIds.filter((id) => !state.openTabIds.includes(id)); - for (const id of closed) closeSession(id); - }); - }, []); - - // 定时任务调度器(应用运行期间常驻) - useEffect(() => { - startScheduler(); - return () => stopScheduler(); - }, []); - - const activeRef = activeTabId ? findNode(collections, activeTabId) : null; - const activeNode = activeRef?.node; - - return ( -
- -
- - {view === "testing" ? ( -
- -
- ) : ( -
- -
- - {inited && activeNode === undefined && ( - collections[0] && openTab(collections[0].id)} /> - )} - {activeNode && activeNode.type === "request" && ( - <> - {activeNode.kind === "http" && } - {activeNode.kind === "graphql" && } - {activeNode.kind === "websocket" && } - {activeNode.kind === "sse" && } - {activeNode.kind === "socketio" && } - {activeNode.kind === "mqtt" && } - - )} - {activeNode && activeNode.type === "folder" && ( - undefined} /> - )} -
-
- - )} -
- {dialog?.type === "new" && } - {dialog?.type === "import" && } - {dialog?.type === "export" && } - {dialog?.type === "environments" && } - {dialog?.type === "settings" && } - {dialog?.type === "rename" && } - {dialog?.type === "mocklink" && - (() => { - const ref = findNode(collections, dialog.nodeId); - return ref?.node.type === "request" && ref.node.kind === "http" ? ( - - ) : null; - })()} - {dialog?.type === "auth" && } - {dialog?.type === "team" && } - - {toast && ( -
clearToast()} - style={{ - position: "fixed", - bottom: 24, - left: "50%", - transform: "translateX(-50%)", - zIndex: 80, - maxWidth: 420, - padding: "10px 14px", - borderRadius: 10, - border: "1px solid var(--border)", - background: "var(--panel)", - color: "var(--text)", - boxShadow: "0 8px 24px rgba(0,0,0,.35)", - fontSize: 13, - cursor: "pointer", - }} - > - {toast.message} -
- )} - -
- ); -} - -function EmptyState({ onQuick }: { onQuick: (kind: string) => void }) { - const setDialog = useWorkspace((s) => s.setDialog); - void onQuick; - return ( -
-
-
-
OpsxAPI · 接口工作台
-
- 从左侧选择接口,或 - - / - -
-
- HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT -
- 支持 OpenAPI / Postman / Apifox / cURL / HAR 等格式导入导出 -
-
-
- ); -} diff --git a/packages/ui/src/components/TestingView.tsx b/packages/ui/src/components/TestingView.tsx deleted file mode 100644 index bbd52a4..0000000 --- a/packages/ui/src/components/TestingView.tsx +++ /dev/null @@ -1,756 +0,0 @@ -/** - * 自动化测试视图:场景用例列表 / 步骤编排(断言 + 变量提取)/ 运行报告 / 定时任务。 - * 三种触发方式:手动运行(本视图)、AI 生成(AiCaseDialog)、定时调度(scheduler + 定时设置)。 - */ -import type { - AssertOp, - AssertTarget, - Collection, - HttpRequestData, - RequestNode, - TestCase, - TestRun, -} from "@opsxapi/core"; -import { - ASSERT_OP_LABELS, - createSeedWorkspace, - exportRunJson, - exportRunMarkdown, - isValidCron, - newAssertion, - newExtract, - newStep, - runStats, -} from "@opsxapi/core"; -import { useEffect, useMemo, useState } from "react"; -import { nextRunAt } from "../scheduler"; -import { runTestCase } from "../testRunner"; -import { useWorkspace } from "../workspaceStore"; -import { cx } from "./common"; -import { Icon } from "./icons"; -import { AiCaseDialog } from "./dialogs/AiCaseDialog"; -import { ScheduleDialog } from "./dialogs/ScheduleDialog"; - -const TARGET_LABELS: Record = { - status: "状态码", - timeMs: "耗时(ms)", - header: "Header", - body: "Body", -}; - -/** 汇总所有集合中的 HTTP 节点为选择器选项 */ -function useHttpNodeOptions(): { id: string; label: string; method: string }[] { - const collections = useWorkspace((s) => s.collections); - return useMemo(() => { - const out: { id: string; label: string; method: string }[] = []; - const walk = (nodes: Collection["children"], prefix: string): void => { - for (const n of nodes) { - if (n.type === "folder") walk(n.children, prefix ? `${prefix}/${n.name}` : n.name); - else if (n.kind === "http") { - const d = (n as RequestNode & { kind: "http" }).data as HttpRequestData; - out.push({ id: n.id, label: `${prefix ? `${prefix}/` : ""}${n.name}`, method: d.method }); - } - } - }; - for (const c of collections) walk(c.children, c.name); - return out; - }, [collections]); -} - - -const REGRESSION_CASE_ID = "tc-opsx-http8-regression"; - -/** 装载 / 刷新本地 Gin echo 八组合回归用例(不整库清空) */ -function installHttp8Regression(): string { - const seed = createSeedWorkspace(); - const state = useWorkspace.getState(); - const seedCase = seed.testCases.find((c) => c.id === REGRESSION_CASE_ID) ?? seed.testCases[0]!; - const localEnv = seed.environments.find((e) => e.name === "本地") ?? seed.environments[0]; - const hasCase = state.testCases.some((c) => c.id === REGRESSION_CASE_ID); - const hasCollection = state.collections.some((c) => c.name === "示例集合"); - const localExisting = state.environments.find((e) => e.name === "本地"); - state.loadWorkspaceData({ - version: 2, - collections: hasCollection - ? state.collections.map((c) => (c.name === "示例集合" ? seed.collections[0]! : c)) - : [...state.collections, ...seed.collections], - environments: localExisting - ? state.environments - : [...state.environments, ...seed.environments.filter((e) => e.name === "本地")], - activeEnvironmentId: localExisting?.id ?? localEnv?.id ?? state.activeEnvironmentId, - history: state.history, - settings: state.settings, - testCases: hasCase - ? state.testCases.map((c) => (c.id === REGRESSION_CASE_ID ? seedCase : c)) - : [...state.testCases, seedCase], - testRuns: state.testRuns, - schedules: state.schedules, - }); - return seedCase.id; -} - -export function TestingView() { - const testCases = useWorkspace((s) => s.testCases); - const testRuns = useWorkspace((s) => s.testRuns); - const schedules = useWorkspace((s) => s.schedules); - const setView = useWorkspace((s) => s.setView); - const addTestCase = useWorkspace((s) => s.addTestCase); - - const [selected, setSelected] = useState(testCases[0]?.id ?? null); - const [showAi, setShowAi] = useState(false); - const [showSchedule, setShowSchedule] = useState(false); - const [runAllBusy, setRunAllBusy] = useState(false); - - const tc = testCases.find((c) => c.id === selected) ?? null; - const lastRunOf = (caseId: string): TestRun | undefined => testRuns.find((r) => r.caseId === caseId); - - const runAll = async () => { - setRunAllBusy(true); - try { - for (const c of testCases) await runTestCase(c.id, "manual"); - } finally { - setRunAllBusy(false); - } - }; - - return ( -
- {/* 左栏:用例列表 */} - - - {/* 右侧:用例详情 */} - {tc ? ( - - ) : ( -
-
-
-
自动化测试
-
- 手动编排 · AI 生成 · 定时调度,三种方式共用同一套场景用例 -
- 左侧新建用例;侧栏底部「自动化测试」随时进出本视图 -
-
-
- )} - - {showAi && setShowAi(false)} onSaved={(id) => setSelected(id)} />} - {showSchedule && setShowSchedule(false)} />} -
- ); -} - -/* ---------------- 用例详情:编辑 + 报告 ---------------- */ - -function CaseDetail({ tc }: { tc: TestCase }) { - const updateTestCase = useWorkspace((s) => s.updateTestCase); - const removeTestCase = useWorkspace((s) => s.removeTestCase); - const allRuns = useWorkspace((s) => s.testRuns); - const runs = useMemo(() => allRuns.filter((r) => r.caseId === tc.id), [allRuns, tc.id]); - const schedules = useWorkspace((s) => s.schedules); - const nodeOptions = useHttpNodeOptions(); - const [tab, setTab] = useState<"edit" | "report">("edit"); - const [busy, setBusy] = useState(false); - const sched = schedules.find((s) => s.caseId === tc.id); - - const patch = (p: Partial) => updateTestCase(tc.id, p); - - const patchStep = (stepId: string, p: Partial) => - patch({ steps: tc.steps.map((st) => (st.id === stepId ? { ...st, ...p } : st)) }); - - const moveStep = (idx: number, dir: -1 | 1) => { - const steps = [...tc.steps]; - const j = idx + dir; - if (j < 0 || j >= steps.length) return; - [steps[idx], steps[j]] = [steps[j], steps[idx]]; - patch({ steps }); - }; - - const run = async () => { - setBusy(true); - setTab("report"); - try { - await runTestCase(tc.id, "manual"); - } finally { - setBusy(false); - } - }; - - return ( -
- {/* 头部 */} -
- patch({ name: e.target.value })} - placeholder="用例名称" - /> - - - - -
- - {/* Tabs */} -
- - -
- - {tab === "edit" ? ( -
-