From 3cb95451219356950f4dd67726fe5ca9cbd271d5 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Fri, 16 Oct 2026 04:16:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(homepage):=20=E5=AE=98=E7=BD=91/?= =?UTF-8?q?=E6=96=87=E6=A1=A3/=E5=AE=9A=E4=BB=B7=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E8=BF=81=E5=87=BA=E7=A8=8B=E5=BA=8F=20+=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=8E=88=E6=9D=83=E4=B8=8E=E6=BF=80=E6=B4=BB?= =?UTF-8?q?=E9=A1=B5=20/license?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 官网源码归拢到 homepage/(index/pricing/terms/license + content/ + assets/ + docs-assets/), 删除程序内 go:embed 的 server/internal/{site,docs},后端仅静态托管 homepage/dist 做本地预览 - 新增独立静态页 /license(授权与激活):交付与激活四步(邮件交付授权码)、 在线授权码 LIC- 与离线许可证 LICOFF- 对比、授权服务器 licence.rlwj.cn 通信范围说明、 授权类型与价格、10 条授权 FAQ - tools/homepage-build 支持可选单页,产出 .html 与 /index.html - 导航/页脚接入 /license,跨页锚点补 / 前缀,页脚底栏文案同步为静态站口径 --- Makefile | 5 +- README.md | 26 +- .../site => homepage}/assets/site.css | 0 .../docs => homepage}/content/01-intro.md | 0 .../content/02-quickstart.md | 0 .../docs => homepage}/content/03-interface.md | 0 .../docs => homepage}/content/04-http.md | 0 .../docs => homepage}/content/05-auth.md | 0 .../docs => homepage}/content/06-env.md | 0 .../docs => homepage}/content/07-graphql.md | 0 .../docs => homepage}/content/08-websocket.md | 0 .../docs => homepage}/content/09-sse.md | 0 .../docs => homepage}/content/10-socketio.md | 0 .../docs => homepage}/content/11-mqtt.md | 0 .../docs => homepage}/content/12-import.md | 0 .../docs => homepage}/content/13-export.md | 0 .../docs => homepage}/content/14-mock.md | 0 .../docs => homepage}/content/15-manage.md | 0 .../docs => homepage}/content/16-testing.md | 0 .../docs => homepage}/content/17-ai.md | 0 .../content/18-server-api.md | 0 .../docs => homepage}/content/19-faq.md | 0 .../docs => homepage}/content/20-account.md | 0 .../docs => homepage}/content/manifest.json | 0 homepage/dist/docs/account/index.html | 2 +- homepage/dist/docs/ai/index.html | 10 +- homepage/dist/docs/search-data.json | 2 +- homepage/dist/index.html | 1 + homepage/dist/license.html | 211 ++++++++++++++ homepage/dist/license/index.html | 211 ++++++++++++++ homepage/dist/pricing.html | 19 +- homepage/dist/pricing/index.html | 211 ++++++++++++++ .../assets => homepage/docs-assets}/docs.css | 0 .../assets => homepage/docs-assets}/docs.js | 0 .../docs/template.html => homepage/docs.html | 0 .../site/template.html => homepage/index.html | 19 +- homepage/license.html | 211 ++++++++++++++ homepage/pricing.html | 211 ++++++++++++++ {server/internal/site => homepage}/terms.html | 0 scripts/build-homepage.sh | 8 + server/internal/docs/docs.go | 264 ------------------ server/internal/docs/docs_test.go | 122 -------- server/internal/docs/render.go | 25 -- server/internal/server/router.go | 10 +- server/internal/site/site.go | 80 ------ server/internal/site/site_test.go | 63 ----- tools/homepage-build/go.mod | 5 + tools/homepage-build/go.sum | 2 + tools/homepage-build/main.go | 190 +++++++++++++ 49 files changed, 1308 insertions(+), 600 deletions(-) rename {server/internal/site => homepage}/assets/site.css (100%) rename {server/internal/docs => homepage}/content/01-intro.md (100%) rename {server/internal/docs => homepage}/content/02-quickstart.md (100%) rename {server/internal/docs => homepage}/content/03-interface.md (100%) rename {server/internal/docs => homepage}/content/04-http.md (100%) rename {server/internal/docs => homepage}/content/05-auth.md (100%) rename {server/internal/docs => homepage}/content/06-env.md (100%) rename {server/internal/docs => homepage}/content/07-graphql.md (100%) rename {server/internal/docs => homepage}/content/08-websocket.md (100%) rename {server/internal/docs => homepage}/content/09-sse.md (100%) rename {server/internal/docs => homepage}/content/10-socketio.md (100%) rename {server/internal/docs => homepage}/content/11-mqtt.md (100%) rename {server/internal/docs => homepage}/content/12-import.md (100%) rename {server/internal/docs => homepage}/content/13-export.md (100%) rename {server/internal/docs => homepage}/content/14-mock.md (100%) rename {server/internal/docs => homepage}/content/15-manage.md (100%) rename {server/internal/docs => homepage}/content/16-testing.md (100%) rename {server/internal/docs => homepage}/content/17-ai.md (100%) rename {server/internal/docs => homepage}/content/18-server-api.md (100%) rename {server/internal/docs => homepage}/content/19-faq.md (100%) rename {server/internal/docs => homepage}/content/20-account.md (100%) rename {server/internal/docs => homepage}/content/manifest.json (100%) create mode 100644 homepage/dist/license.html create mode 100644 homepage/dist/license/index.html create mode 100644 homepage/dist/pricing/index.html rename {server/internal/docs/assets => homepage/docs-assets}/docs.css (100%) rename {server/internal/docs/assets => homepage/docs-assets}/docs.js (100%) rename server/internal/docs/template.html => homepage/docs.html (100%) rename server/internal/site/template.html => homepage/index.html (97%) create mode 100644 homepage/license.html create mode 100644 homepage/pricing.html rename {server/internal/site => homepage}/terms.html (100%) create mode 100755 scripts/build-homepage.sh delete mode 100644 server/internal/docs/docs.go delete mode 100644 server/internal/docs/docs_test.go delete mode 100644 server/internal/docs/render.go delete mode 100644 server/internal/site/site.go delete mode 100644 server/internal/site/site_test.go create mode 100644 tools/homepage-build/go.mod create mode 100644 tools/homepage-build/go.sum create mode 100644 tools/homepage-build/main.go diff --git a/Makefile b/Makefile index 8c3680a..b0b004f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install web gui all server stop status build-web test desktop +.PHONY: install web gui all server stop status build-web test desktop homepage install: npm install @@ -24,6 +24,9 @@ status: desktop: ./run.sh desktop +homepage: + scripts/build-homepage.sh + build-web: npm run build diff --git a/README.md b/README.md index 05236ff..dc6cee3 100644 --- a/README.md +++ b/README.md @@ -62,24 +62,18 @@ make install 打开 http://127.0.0.1:8787/ 看落地页;操作请用 `./run.sh gui`。`:5173` 只是桌面 WebView 源。 -## 用户文档站(/docs) +## 官网 / 文档 / 定价(homepage/,纯静态) -Gin 内置产品文档,Markdown 嵌入二进制、SSR 渲染,随服务一键部署: +官网(落地页 + `/docs` + `/pricing`)已拆分为独立静态站:源在 `homepage/`,与后端/GUI 解耦;后端仅在本地静态托管产物 `homepage/dist`(nginx 直接托管同一目录)。构建入口 `scripts/build-homepage.sh`(= `make homepage`),工具在独立 Go module `tools/homepage-build/`。 -- 地址:`http://127.0.0.1:8787/docs`(开发态 `http://127.0.0.1:5173/docs`,Vite 已反代;工作台顶栏「📖 文档」直达) -- 布局:顶栏搜索 + 左侧分组导航(16 篇:开始使用 / 请求调试 / 数据管理 / 参考)+ 右侧本页目录 -- 能力:全文搜索、代码块复制 + 高亮、提示块、上一页/下一页、反馈组件、打印导出 PDF、移动端响应式、404 页 -- 内容在 `server/internal/docs/content/*.md`(本地预览优先托管 `homepage/dist`) - -## 产品官网(/home) - -Gin 内置营销单页,与 /docs 同为 SSR 嵌入交付(`internal/site`): - -- 地址:`/` 与 `/home` 双入口(开发态 `http://127.0.0.1:5173/home`);文档站顶栏「官网」互链 -- 区块:Hero(纯 CSS 产品界面 mock)/ 卖点 / 功能 / 六协议 / 导入导出生态 / 与 Apifox·Apipost 对比表 / 定价 / 让利计划 / 购买 FAQ / 联系 CTA / Footer -- 定价三档(标价/成交价分离):个人版 ¥99/年、团队版 ¥99/人/年(入门锚点);企业私有化版标价 **¥8,000/年 · ¥30,000 买断**,经早鸟/教育/多年预付/案例合作落在成交区间 ¥5,000–8,000/年、¥15,000–30,000 -- 让利机制四项:早鸟创始价(前 20 席 5–7 折永久)、教育/开源 6–8 折、多年预付 8–9 折、案例换授权;销售原则:不直接降价,用「永久授权 + 1 年升级 + 5 人团队 1–2 年回本」回应预算顾虑 -- 交付前替换:联系邮箱 `hello@opsxapi.dev`(template.html 内两处)与对比表脚注中的价格核对 +- 源结构: + - `homepage/index.html` 落地页;`homepage/pricing.html` 定价页;`homepage/terms.html` 服务条款 + - `homepage/docs.html` 文档模板;`homepage/content/*.md` + `manifest.json`(20 篇,6 分组) + - `homepage/assets/`(官网资源,产物落 `dist/site/assets/`);`homepage/docs-assets/`(文档资源,产物落 `dist/docs/assets/`) +- 产物 `homepage/dist/`(结构固定):`index.html`、`docs/index.html`、`docs//index.html`、`docs/search-data.json`、`pricing.html` + `pricing/index.html`、`site/assets/*` +- 本地预览:`./run.sh web` → 落地页 `http://127.0.0.1:8787/`、文档 `http://127.0.0.1:8787/docs`、定价 `http://127.0.0.1:8787/pricing` +- `.gitignore` 含 `dist/`,产物默认不入库;如需入库 `git add -f homepage/dist` +- 交付前替换:联系邮箱 `hello@opsxapi.dev`(`homepage/index.html`) ## 服务端端点(server/) diff --git a/server/internal/site/assets/site.css b/homepage/assets/site.css similarity index 100% rename from server/internal/site/assets/site.css rename to homepage/assets/site.css diff --git a/server/internal/docs/content/01-intro.md b/homepage/content/01-intro.md similarity index 100% rename from server/internal/docs/content/01-intro.md rename to homepage/content/01-intro.md diff --git a/server/internal/docs/content/02-quickstart.md b/homepage/content/02-quickstart.md similarity index 100% rename from server/internal/docs/content/02-quickstart.md rename to homepage/content/02-quickstart.md diff --git a/server/internal/docs/content/03-interface.md b/homepage/content/03-interface.md similarity index 100% rename from server/internal/docs/content/03-interface.md rename to homepage/content/03-interface.md diff --git a/server/internal/docs/content/04-http.md b/homepage/content/04-http.md similarity index 100% rename from server/internal/docs/content/04-http.md rename to homepage/content/04-http.md diff --git a/server/internal/docs/content/05-auth.md b/homepage/content/05-auth.md similarity index 100% rename from server/internal/docs/content/05-auth.md rename to homepage/content/05-auth.md diff --git a/server/internal/docs/content/06-env.md b/homepage/content/06-env.md similarity index 100% rename from server/internal/docs/content/06-env.md rename to homepage/content/06-env.md diff --git a/server/internal/docs/content/07-graphql.md b/homepage/content/07-graphql.md similarity index 100% rename from server/internal/docs/content/07-graphql.md rename to homepage/content/07-graphql.md diff --git a/server/internal/docs/content/08-websocket.md b/homepage/content/08-websocket.md similarity index 100% rename from server/internal/docs/content/08-websocket.md rename to homepage/content/08-websocket.md diff --git a/server/internal/docs/content/09-sse.md b/homepage/content/09-sse.md similarity index 100% rename from server/internal/docs/content/09-sse.md rename to homepage/content/09-sse.md diff --git a/server/internal/docs/content/10-socketio.md b/homepage/content/10-socketio.md similarity index 100% rename from server/internal/docs/content/10-socketio.md rename to homepage/content/10-socketio.md diff --git a/server/internal/docs/content/11-mqtt.md b/homepage/content/11-mqtt.md similarity index 100% rename from server/internal/docs/content/11-mqtt.md rename to homepage/content/11-mqtt.md diff --git a/server/internal/docs/content/12-import.md b/homepage/content/12-import.md similarity index 100% rename from server/internal/docs/content/12-import.md rename to homepage/content/12-import.md diff --git a/server/internal/docs/content/13-export.md b/homepage/content/13-export.md similarity index 100% rename from server/internal/docs/content/13-export.md rename to homepage/content/13-export.md diff --git a/server/internal/docs/content/14-mock.md b/homepage/content/14-mock.md similarity index 100% rename from server/internal/docs/content/14-mock.md rename to homepage/content/14-mock.md diff --git a/server/internal/docs/content/15-manage.md b/homepage/content/15-manage.md similarity index 100% rename from server/internal/docs/content/15-manage.md rename to homepage/content/15-manage.md diff --git a/server/internal/docs/content/16-testing.md b/homepage/content/16-testing.md similarity index 100% rename from server/internal/docs/content/16-testing.md rename to homepage/content/16-testing.md diff --git a/server/internal/docs/content/17-ai.md b/homepage/content/17-ai.md similarity index 100% rename from server/internal/docs/content/17-ai.md rename to homepage/content/17-ai.md diff --git a/server/internal/docs/content/18-server-api.md b/homepage/content/18-server-api.md similarity index 100% rename from server/internal/docs/content/18-server-api.md rename to homepage/content/18-server-api.md diff --git a/server/internal/docs/content/19-faq.md b/homepage/content/19-faq.md similarity index 100% rename from server/internal/docs/content/19-faq.md rename to homepage/content/19-faq.md diff --git a/server/internal/docs/content/20-account.md b/homepage/content/20-account.md similarity index 100% rename from server/internal/docs/content/20-account.md rename to homepage/content/20-account.md diff --git a/server/internal/docs/content/manifest.json b/homepage/content/manifest.json similarity index 100% rename from server/internal/docs/content/manifest.json rename to homepage/content/manifest.json diff --git a/homepage/dist/docs/account/index.html b/homepage/dist/docs/account/index.html index 168f8a0..65aee58 100644 --- a/homepage/dist/docs/account/index.html +++ b/homepage/dist/docs/account/index.html @@ -139,7 +139,7 @@
更新于 2026-09-17

账号与团队

-

登录后可使用云端空间:个人空间「我的空间」与团队共享空间,数据保存在本机服务的 SQLite 数据库(apps/server/data/opsxapi.db),不上传任何第三方。

+

登录后可使用云端空间:个人空间「我的空间」与团队共享空间,数据保存在本机服务的 SQLite 数据库(server/data/opsxapi.db),不上传任何第三方。

注册 / 登录

最左活动栏底部 登录 按钮打开弹窗:

    diff --git a/homepage/dist/docs/ai/index.html b/homepage/dist/docs/ai/index.html index 4ee9007..f4455ae 100644 --- a/homepage/dist/docs/ai/index.html +++ b/homepage/dist/docs/ai/index.html @@ -139,9 +139,9 @@
    更新于 2026-09-17

    AI 助手

    -

    接入 OpenAI 兼容协议的大模型,为接口调试与自动化测试提供 AI 能力。密钥只保存在本机(localStorage),请求直连供应商,不经过任何第三方中转。

    +

    接入 OpenAI 兼容协议的大模型,为自动化测试提供 AI 能力。模型调用直连供应商;供应商配置(包括密钥)随当前工作区保存,团队工作区会同步工作区数据。

    配置模型供应商

    -

    设置 → AI 助手 → 模型供应商,点「添加供应商」,内置常用预设:

    +

    设置 → 大模型,选择预设并点击「+」,在右侧填写模型 ID、密钥和 Base URL,测试连接后点击「保存配置」。内置常用预设:

    @@ -180,8 +180,8 @@
    • API Key:Ollama 等本地服务可留空;
    • -
    • 测试连接:保存前先发一条 ping 验证地址 / 密钥 / 模型名;
    • -
    • 多个供应商时可切换「当前使用」的模型。
    • +
    • 测试连接:发一条 ping 验证地址 / 密钥 / 模型名;离线时也可先保存,之后再测试;
    • +
    • 多个供应商时可在左侧选择,保存后点「设为默认」指定自动化测试使用的模型。

    AI 生成场景用例

    自动化测试视图 →「AI 生成」:

    @@ -194,7 +194,7 @@

    生成结果只是起点 —— 步骤、断言、提取规则都可再手动调整。

    隐私与边界

      -
    • API Key 仅存本机,随工作区数据一起持久化;导出全量备份时会包含,分享备份文件前请注意;
    • +
    • API Key 随当前工作区数据保存;团队工作区会同步,导出全量备份时也会包含。分享工作区或备份文件前请检查密钥;
    • 供应商收到的内容为接口清单摘要(方法 / URL / Body 片段)与你的指令,不含环境变量值与 API Key;
    • 未配置供应商时,Mock 数据、环境变量等离线功能不受影响。
    diff --git a/homepage/dist/docs/search-data.json b/homepage/dist/docs/search-data.json index 3b17eab..06107d4 100644 --- a/homepage/dist/docs/search-data.json +++ b/homepage/dist/docs/search-data.json @@ -1 +1 @@ -[{"Slug":"intro","Title":"产品介绍","Group":"开始使用","Text":"OpsxAPI 是一款 本地优先 的专业接口工作台,面向运维与开发,把 HTTP、GraphQL、WebSocket、SSE、Socket.IO、MQTT 六种协议的调试放进同一个窗口。 无需注册、无需联网同步,数据全部保存在本地浏览器(localStorage),导出备份随时带走。 功能总览 能力 说明 六种协议 HTTP / GraphQL / WebSocket / SSE / Socket.IO / MQTT 集合管理 目录树、搜索、右键菜单、拖拽移动、多标签页 多环境变量 、 全局替换,一键切换环境 数据导入 OpenAPI/Swagger、Postman、Apifox、Apipost、cURL、HAR、Insomnia、Hoppscotch、Markdown 等 数据导出 OpenAPI 3.0、Postman v2.1、Markdown 文档、全量备份 代码生成 cURL / JavaScript fetch / Python requests / Go net/http 代理能力 本地 Gin 代理绕开 CORS;WebSocket 隧道支持自定义握手 Header 双端形态 浏览器(Web)与桌面端(Tauri)共用同一套界面 支持的协议与场景 协议 典型场景 特色 HTTP / REST 调试 REST 接口、运维平台 API 7 种方法、6 种请求体、3 种鉴权 GraphQL 查询网关、中台接口 Query + Variables 独立编辑 WebSocket 实时推送、行情、IM 自定义握手 Header(经本地隧道) SSE AI 流式输出、监控事件流 直连失败自动切代理、断点续传 Socket.IO Node 实时应用 事件监听、emit 参数、path/auth 配置 MQTT IoT 设备、消息总线 通配符订阅、QoS、自动重连 为什么选 OpsxAPI 数据主权 :所有集合、环境、历史保存在本机,不经过任何第三方服务器。 开箱即用 : 一条命令启动;首次打开自带覆盖全部协议的示例集合。 工程友好 :导入自动建目录、路径参数自动转变量;导出即标准 OpenAPI,可回流 Apifox / Swagger 生态。 轻量克制 :一个 Go 二进制 + 一份前端,资源占用小,内网可部署。 提示 :想快速感受?直接看下一章《快速开始》,30 秒发出第一个请求。"},{"Slug":"quickstart","Title":"快速开始","Group":"开始使用","Text":"环境要求 组件 版本 用途 Node.js ≥ 20 Web 前端(Vite) Go ≥ 1.23 本地代理服务(Gin) Rust + Cargo 可选 仅编译桌面端(Tauri)时需要 启动 看到下面的输出即可打开浏览器: 命令 说明 浏览器版(推荐) 桌面窗口(Tauri,共用同一 UI) Web + 桌面一起开 / 查看状态 / 停止 30 秒发出第一个请求 1. 打开 \u003chttp://127.0.0.1:5173 ,左侧是内置的「示例集合」。 2. 点击 HTTP · httpbin → 获取请求回显 。 3. 点击右上角 发送 。 4. 下方响应区显示 、耗时与 JSON 高亮响应体 —— 完成! 桌面端(可选) 桌面端复用同一份界面与数据通道,HTTP 请求走 Tauri 原生网络栈,不依赖浏览器 CORS。 提示 :首次打开会自动创建两个环境(正式 / 本地)和一个覆盖全部协议的示例集合,可随时在「设置」中重置。"},{"Slug":"interface","Title":"界面导览","Group":"开始使用","Text":"OpsxAPI 主界面分为五个区域: 顶栏 元素 作用 + 新建 快捷创建各协议请求 / 目录(默认建在第一个集合) 导入 / 导出 打开数据导入、导出对话框 环境选择器 切换当前环境(变量随之生效),「⚙ 环境」管理环境与变量 🌙 / ☀️ 明暗主题切换 ⋯ 设置(主题、超时、重置工作区) 左侧集合树 点击请求 → 在标签页中打开;点击目录 → 展开 / 折叠。 右键 任意节点:新建、重命名、复制、导出、删除。 拖拽 请求或目录到另一个目录,即可移动(支持跨集合)。 顶部搜索框按名称 / 方法 / URL 实时过滤。 底部为 历史记录 :显示最近请求的状态码与耗时,点击可回跳到对应接口。 标签页栏 同时打开多个接口,中键或 × 关闭;右键支持「关闭其他 / 关闭全部」。正在连接中的实时协议标签会显示呼吸圆点。 请求编辑区(上半部分) 按协议呈现不同编辑器: 协议 标签页 HTTP Params · Body · Headers · Cookies · Auth · 说明 GraphQL Query · Variables · Headers · Auth WebSocket 消息 · 握手配置 · 说明 SSE 事件流 · 连接配置 · 说明 Socket.IO 事件 · 连接配置 · 说明 MQTT 消息 · Broker / 订阅 · 发布 · 说明 响应区(下半部分) HTTP / GraphQL :响应体(Pretty JSON 高亮)、Raw、响应头,右上角状态 / 耗时 / 大小 / 复制。 实时协议 :分色消息日志(接收 · 绿、发送 · 蓝、系统 · 灰、错误 · 红),顶部实时统计收发条数与连接时长。"},{"Slug":"http","Title":"发送 HTTP 请求","Group":"请求调试","Text":"发送请求 1. 选择方法(GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS)。 2. 输入 URL,支持 形式的变量(见《环境与变量》)。 3. 点击 发送 ,或直接在 URL 输入框按 Enter 。 请求默认经本地 Gin 代理( )发出,天然绕开浏览器 CORS 限制。 Params(查询参数) 键值对编辑器,每行可勾选启用 / 停用、填写说明。URL 中已有的查询串会被拆分到表格里,两边双向同步。 Body(请求体) 模式 适用场景 none GET 等无体请求 JSON REST 接口(默认自动补 ,带格式化按钮) XML / Text / HTML 原始报文 x www form urlencoded 表单提交 form data multipart 文本字段(文件上传请用代码生成的 cURL) Headers / Cookies Headers:键值对,支持 ; 、 由 Auth / Cookies 标签统一管理时避免重复。 Cookies:键值对,发送时自动拼接为 请求头。 代码生成 点击 URL 栏右侧 代码 ,可生成: cURL(可直接在终端执行) JavaScript fetch Python requests Go net/http 生成内容保留 原样,便于粘贴到脚本后再替换。 响应面板 标签 内容 响应体 JSON 自动格式化 + 语法高亮;其他类型原样展示 Raw 未格式化的原始文本 响应头 全部响应头表格 右上角常驻 状态码 / 耗时 / 大小 / Content Type ,并提供「复制体」按钮。"},{"Slug":"auth","Title":"接口鉴权","Group":"请求调试","Text":"在 HTTP / GraphQL / SSE 请求的 Auth 标签中选择鉴权方式: Bearer Token Token 支持 变量引用 —— 推荐把 token 放进环境变量,一次配置全局生效。 Basic Auth 填写用户名 / 密码,自动进行 Base64 编码后放入 头。 API Key 字段 说明 Key 名 如 值 支持 位置 Header(默认)或 Query 参数 组合规则 鉴权产生的 Header 会与 Headers 标签中的同名项合并(鉴权优先)。 API Key 放 Query 时,会追加到 Params,不会重复出现在 Headers。 导入 OpenAPI / Postman 时,文档中声明的 会自动映射到对应鉴权方式。 注意 :鉴权信息保存在本地集合数据中。如需分享集合,请先通过「导出」审查是否包含敏感 token。"},{"Slug":"env","Title":"环境与变量","Group":"请求调试","Text":"前置 URL 环境可配置 前置 URL (如 )。配置后,请求里只需写相对路径: URL 输入框会以 前缀块 + 路径 的形式一体展示,前缀部分不可误编辑(点击可跳转环境管理); 切换环境即自动换成对应服务地址 ,无需改请求; 路径以 开头(或为空)才启用拼接;写完整 地址的请求不受影响; 未配置前置 URL 的环境,请求仍可写完整 URL 或用 。 历史数据自动迁移:旧的 会被自动转为「前置 URL + /path」, 变量写入环境的前置 URL 字段。 变量语法 在 URL、Params、Headers、Cookies、Body、鉴权字段中,使用双花括号引用变量: 发送时统一替换为 当前启用环境 中同名变量的值;未定义的变量保留原样,便于发现遗漏。 切换环境 顶栏环境切换器显示 彩色单字徽标 + 环境名 + 变量数 ,点击弹出环境列表(当前环境带「当前」标记),一眼防误发;底部可进入「管理环境」或「不使用环境」。 管理环境 顶栏 环境 按钮打开管理弹窗(左侧环境列表 + 右侧编辑区): 左栏 :环境列表(徽标 + 名称 + 当前标记),底部虚线按钮新建环境。 右栏 :提示条(变量引用语法 + 文档直达)、环境重命名、设为当前 / 取消使用、 复制环境 、删除;变量表支持 按名称 / 描述搜索 、逐行启用停用、内联编辑。 所有修改即时生效并自动保存。 输入 {{ 自动补全 在 URL 输入框或任意「值」输入框键入 ,会弹出当前环境的变量浮层: ↑↓ 选择、Enter / Tab 插入完整的 ;Esc 关闭。 浮层同时显示变量值,选之前即可确认。 内置示例环境: 环境 baseUrl token 正式 demo token 本地 dev token 变量从哪来自动生成 来源 行为 导入 OpenAPI / Swagger 地址写入 ;路径参数 转为 并写入示例值 导入 Postman 写入当前环境 手动输入 在环境面板随时增改 实际地址预览 URL 含 时,编辑器会在 URL 栏下方实时显示替换后的实际地址,切换环境立即更新。 提示 :路径参数用变量而不是写死,是接口可复用的关键 —— 复制接口后只改变量值即可调试另一个资源。"},{"Slug":"graphql","Title":"GraphQL","Group":"请求调试","Text":"编辑查询 GraphQL 请求由三部分组成: 部分 说明 Query GraphQL 查询语句,支持变量定义 Variables JSON 对象,随查询一起发送;非法 JSON 会红框提示 Headers / Auth 与 HTTP 请求一致 发送 点击 执行 (或 URL 栏按 Enter)。实际发送的是标准 GraphQL over HTTP: 响应面板会自动格式化 / ,字段名高亮。 内置示例 示例集合中的「国家查询 · countries」连接公共 GraphQL 服务 \u003chttps://countries.trevorblades.com/graphql ,无需鉴权,可直接体验。 最佳实践 把 endpoint 存为 ,环境切换即切换网关。 需要鉴权时在 Auth 标签配置 Bearer,不必手写 Header。 想看实际报文?点 代码 生成 cURL,即为此查询的完整 HTTP 请求。"},{"Slug":"websocket","Title":"WebSocket","Group":"请求调试","Text":"连接 1. 填写 或 地址。 2. 点击 连接 ;状态点变绿(已连接)后即可收发消息。 3. 再次点击按钮即断开。 消息收发 消息日志 :接收(绿)/ 发送(蓝)/ 系统(灰)/ 错误(红)分色展示,顶部统计 ↑↓ 条数与连接时长。 发送框 :支持 JSON / 纯文本; ⌘/Ctrl + Enter 快速发送;「格式化 JSON」一键美化。 子协议 「握手配置」标签中的 子协议 填逗号分隔列表,例如: 自定义握手 Header(隧道模式) 浏览器原生 WebSocket 不能 携带自定义 Header(如 、 )。OpsxAPI 的处理方式: 1. 「握手配置」中填写 Header 后,工具自动改走 本地 Gin 隧道 : 浏览器 → Gin (普通 WS) Gin → 目标服务器(携带你填的全部 Header) 2. 不填 Header 时直连目标,零额外开销。 注意 :隧道要求本地 Gin 服务(:8787)在运行,即用 / 启动的默认形态。 调试技巧 消息过多时点「清空」;日志上限 500 条,超出自动滚动淘汰。 把常用报文保存在「说明」里,或为不同报文建多个 WS 请求节点。"},{"Slug":"sse","Title":"SSE 事件流","Group":"请求调试","Text":"连接事件流 1. 填写 SSE 地址(如 )。 2. 点击 连接 ,事件按到达顺序流入消息日志。 3. 每条消息按 名称归类显示; 为 JSON 时自动格式化。 直连与代理自动切换 默认 直连 目标站(fetch 流式读取,延迟最低)。 直连因 CORS / 网络失败时,自动降级走 本地 Gin 流式代理 ,日志中会提示切换过程,无需手工干预。 断点续传(Last Event ID) 「连接配置」中填写 Last Event ID,连接时以 请求头携带,支持断点续传的流会在该事件之后继续推送。 连接配置项 配置 说明 Query 参数 / Headers 以键值对附加 Last Event ID 断点续传游标 仅显示 data 内容 开启后只显示 载荷;关闭则连同 名称一起显示 鉴权 与 HTTP 相同(Bearer / Basic / API Key) 典型场景 LLM 流式输出接口( ) 监控 / 审计事件流 需要长期挂机观察的推送通道"},{"Slug":"socketio","Title":"Socket.IO","Group":"请求调试","Text":"连接 1. 填写服务地址( 不含 路径),如 。 2. 点击 连接 ,状态变为「已连接」后即可收发事件。 监听事件 「连接配置」→ 监听事件 填逗号分隔的事件名: 留空则接收全部服务端事件(onAny 模式)。事件名在每条日志中标注。 发送(emit) 「事件」标签底部的发送区: 事件名 :如 参数 :JSON 数组,作为 的参数 连接选项 以 JSON 配置传给 socket.io client: 选项 说明 path 服务端挂载路径,默认 transports / ,默认两者皆可 auth 握手鉴权载荷,支持 提示 :浏览器直连外部站点同样受 CORS 限制,Socket.IO 服务端需配置对应 origin,或把服务与本工具部署在同源下。"},{"Slug":"mqtt","Title":"MQTT","Group":"请求调试","Text":"连接 Broker 浏览器环境要求 WebSocket 形式 的 Broker 地址: 配置 说明 用户名 / 密码 可选,支持 ClientId 默认随机生成;同一 Broker 上必须唯一 QoS 0 至多一次 · 1 至少一次 · 2 恰好一次 订阅 「Broker / 订阅」中填写逗号分隔的主题,支持通配符: 注意 :订阅在 建立连接时 生效,修改主题后需断开重连。 发布 「发布」标签配置主题与 Payload(JSON / 纯文本),点击发布按钮即送达;已发布消息以蓝色出现在日志中并标注主题。 公共测试 Broker Broker 地址 EMQX HiveMQ 两个浏览器窗口连接同一主题,即可互相收发验证。 行为说明 断线自动重连(3 秒间隔),状态点显示「重连中」。 消息日志上限 500 条;QoS 与主题逐条标注。"},{"Slug":"import","Title":"数据导入","Group":"数据管理","Text":"顶栏 导入 打开导入对话框,支持三种来源: 来源 说明 粘贴文本 / cURL 直接粘贴任意受支持格式的内容 文件导入 选择或拖入 / / / / URL 导入(Swagger) 填写 OpenAPI 地址,经本地 Gin 代理拉取,避开 CORS 格式 自动识别 ,也可手动指定。 支持格式 格式 细节 OpenAPI 3.0 / 3.1、Swagger 2.0 JSON 或 YAML;Apifox 导出的 OpenAPI 同样适用 Postman Collection v2.0 / v2.1,含鉴权、变量、GraphQL body Apipost 项目 格式(宽容解析) cURL 引号、续行、 / / / 、cookie、代理等参数 HAR 按域名分组、相同请求去重 Insomnia 导出文件 v3 / v4,含 workspace / group 层级 Hoppscotch 集合 JSON(数组或单对象) Markdown 提取文中的 tags /pets/{petId} {{petId}} servers baseUrl variable` 当前环境变量 结果确认 导入完成显示汇总:新建集合、接口数量、写入的环境变量与告警(如跳过的文件上传字段)。集合名重复时自动追加序号。"},{"Slug":"export","Title":"导出与代码生成","Group":"数据管理","Text":"顶栏 导出 (多集合时可选择目标集合): 格式 用途 OpenAPI 3.0 · JSON / YAML 回流 Apifox、Swagger UI、代码生成器 Postman v2.1 直接导入 Postman Markdown 接口文档 带目录、参数表格、请求体代码块,可放 Wiki 全量备份 全部集合 + 环境,用于迁移与灾备 每种格式均支持 复制到剪贴板 与 下载文件 ;对话框内实时预览导出内容。 全量备份与恢复 备份文件是包含 标记的 JSON。 恢复:导入对话框 → 粘贴备份内容或选择备份文件 → 格式自动识别为「OpsxAPI 备份」。 代码生成(单请求) 在 HTTP / GraphQL 编辑器点 代码 : 目标 说明 cURL 含方法、头、体、cookie,可直接执行 JavaScript fetch 风格 Python requests 含 完整脚本 Go net/http 完整 函数 保留原样输出,方便接入你自己的配置体系。 提示 :导出的 OpenAPI 中,目录名会成为 ,请求说明写入 ,鉴权自动生成 。"},{"Slug":"mock","Title":"Mock 数据","Group":"数据管理","Text":"Mock 数据 联调时后端接口没就绪?OpsxAPI 内置 Mock.js 风格的数据生成器 ,两种用法覆盖绝大多数场景: 1. 显式表达式 —— 在参数值、Body 中直接写 、 ; 2. 智能匹配 —— 开启后按 字段名 自动套用规则表( → 姓名、 → 邮箱…),一行不用写。 开启与配置 顶栏 ⋯ → Mock 数据 (或「设置 → Mock 数据」): 启用 Mock 数据 :总开关,关闭后 表达式按普通文本原样发送; 智能 Mock 匹配 :JSON Body 字段值未写表达式时,按字段名匹配规则自动生成; 智能匹配规则表 :数据类型(string / number / integer / boolean)、匹配方式(通配符 / 完全匹配 / 正则)、匹配规则(如 )、Mock 表达式、备注与启用开关,支持搜索、新增、删除与恢复默认。 规则 从上到下 匹配,先命中先用;类型需与 JSON 字段实际类型一致(KV 参数一律按 string 匹配)。 表达式速查 表达式 说明 示例输出 中文姓名 邮箱 手机号 形式 11 位 身份证号 18 位 UUID / GUID 整数 小数(保留 2 位) 布尔 随机字符串 日期时间 / 日期 / 时间 — 毫秒时间戳 / / URL / 域名 / IPv4 — / / 省市区 / 中文标题 / 句子 — 图片地址 picsum 随机图 从列表中选择 未列出的写法会 原样发送 ,可放心与真实值混用。 在请求中使用 Params / x www form urlencoded / form data :值一栏直接填表达式。 JSON Body :叶子值写表达式,或交给智能匹配: Body 工具栏提供两个辅助按钮: 智能填充 :按规则把 JSON 各字段值批量替换为对应 表达式,生成可编辑的 Mock 模板; Mock 预览 :即时查看发送时实际生成的数据,不必真的发请求。 生效范围 生效:Query 参数、JSON / XML / Text Body、x www form urlencoded、form data、GraphQL Variables; 不生效:Headers、Cookies、鉴权字段(避免意外改坏签名与令牌)。 提示:Mock 在 发送瞬间 求值,同一请求每次发送都会得到新数据;预览结果仅供核对格式。 本地 Mock 服务(/mock) 除了发送时的离线 Mock,还能把接口 发布成本地 Mock 服务 ,给前端 / 同事直接联调(类似 Apifox 的 Mock URL): 1. 登录并切换到「我的空间」或团队空间(Mock 按空间隔离); 2. HTTP 请求 URL 行点 Mock 链接 → 选择响应状态码与延迟 → 发布 (一次发布当前空间集合内全部 HTTP 接口); 3. 得到地址: (每空间一个 16 位随机片段,不可枚举、不暴露归属),例如 。 特点: 每次请求随机生成 :响应模板里的 、 等表达式由服务端实时求值; 发布时自动把智能 Mock 规则烘焙进模板(开启智能匹配时); 非 GET 接口可用 快速验证;延迟最高 5 秒,用于模拟慢接口 / loading 态。 频率限制(防滥用) 本地 Mock 服务按 IP + 空间 限流: 20 QPS(每秒 20 次)或 60 QPM(每分钟 60 次) ,超限返回 与 。依据 :未经授权或滥用行为平台有权采取限流、暂停服务等措施; 通过技术手段规避限流属于违规行为 。Mock 定位是联调辅助,请勿当作对外流量服务使用。"},{"Slug":"manage","Title":"集合与历史","Group":"数据管理","Text":"新建 入口 行为 顶栏「+ 新建」 快捷菜单,建在第一个集合 树右键「新建请求 / 目录」 精确指定位置 新建对话框 选择类型(六协议 + 目录)、命名、目标集合与目录 接口名自动避免重复(追加序号)。 集合树操作 操作 方式 重命名 右键 → 重命名 复制 右键 → 复制(深拷贝子树,名字加「副本」) 移动 拖拽到目标目录(支持跨集合) 删除 右键 → 删除(直接生效,请先备份重要数据) 搜索 侧栏搜索框,匹配名称 / 方法 / URL 标签页 点击树节点在标签页打开;同时打开数量不限。 中键 / × 关闭;右键「关闭其他 / 关闭全部」。 关闭实时协议(WS / SSE / MQTT…)标签时自动断开连接。 历史记录 每次 HTTP / GraphQL 发送记录:时间、方法、接口名、状态码,失败标红。 点击记录跳回对应接口;最多保留 100 条;可一键清空。 数据存储与备份 位置:浏览器 localStorage(键 ),自动保存(防抖 400ms)。 内容:集合、环境、历史、设置。标签页会话与实时消息不落盘。 备份:导出 → 全量备份;恢复:导入备份文件。 重置:设置 → 重置工作区(恢复初始示例,请先备份)。 注意 :桌面端与浏览器是两份独立的 localStorage。跨端迁移请使用全量备份 / 导入。"},{"Slug":"testing","Title":"自动化测试","Group":"自动化测试与 AI","Text":"自动化测试 把日常联调的接口串成 场景用例 ,一条龙验证业务流程。三种触发方式共用同一套用例模型: 方式 说明 适合 手动运行 用例编排好后点「运行」 日常自测、回归 AI 生成 模型按接口清单自动编排步骤与断言 快速起盘、补断言思路 定时任务 应用运行期间按间隔自动执行 持续监控、冒烟 场景用例 从侧栏底部「自动化测试」进入。用例 = 有序步骤列表,每个步骤: 引用一个 HTTP 接口 (从集合中选择,可跨集合、跨目录); 断言 :状态码 / 耗时 / Header / Body 四类目标,支持 等于、不等于、大于、小于、包含、存在 与 JSONPath 取值比较; 变量提取 : → 环境变量 ,后续步骤 URL / Body 用 引用,实现「创建 → 查询 → 删除」链式传值; 可单独禁用、上下移动、删除。 用例级选项「 失败后继续 」:默认失败即停(快速定位首个问题);打开后跑完全部步骤再看整体报告。 运行报告 统计卡:通过 / 失败步骤数、总耗时、断言数、通过率; 步骤明细:方法 + URL + 状态码 + 耗时,逐条断言的期望 / 实际对照(可按 通过 / 失败 过滤); 「再次运行」一键回归;「导出报告」生成 Markdown 存档或归档到缺陷单; 每个用例保留最近 50 次运行记录,可下拉切换历史报告。 定时任务 用例头部的 ⏱ 按钮或左侧「定时任务」入口: 间隔可选 5 / 15 / 30 / 60 分钟、6 小时、每天; 独立 启用 / 暂停 开关,显示下次运行时间与上次结果; 应用保持运行时生效 :OpsxAPI 窗口(或 Web 页面)开着才会按时触发并记录报告,关闭后自动暂停 —— 适合工作时段的持续监控,不能替代服务器侧的 CI。 提示:定时触发的请求同样走当前环境与 Mock 设置;跨环境回归前先在顶栏切换环境。 AI 生成用例 见 。配置模型供应商后,「AI 生成」会读取集合的 HTTP 接口清单(方法、URL、Body 示例),按业务依赖编排步骤、断言与变量提取,生成前可附加一句要求(如「重点覆盖登录态与分页」),预览确认后保存。"},{"Slug":"ai","Title":"AI 助手","Group":"自动化测试与 AI","Text":"AI 助手 接入 OpenAI 兼容协议 的大模型,为接口调试与自动化测试提供 AI 能力。密钥只保存在本机(localStorage),请求直连供应商,不经过任何第三方中转。 配置模型供应商 设置 → AI 助手 → 模型供应商 ,点「添加供应商」,内置常用预设: 供应商 Base URL 模型示例 DeepSeek 智谱 GLM Moonshot Kimi OpenAI Ollama 本地 API Key :Ollama 等本地服务可留空; 测试连接 :保存前先发一条 ping 验证地址 / 密钥 / 模型名; 多个供应商时可切换「当前使用」的模型。 AI 生成场景用例 自动化测试视图 →「 AI 生成 」: 1. 选择集合(AI 读取其中全部 HTTP 接口:方法、URL、Body 示例); 2. 可选填写一句要求,如「覆盖下单 → 支付 → 查询流程,重点断言金额字段」; 3. 模型按业务依赖编排步骤,输出断言与变量提取(如 ,后续步骤用 ); 4. 预览生成的步骤清单(未识别的接口会自动跳过并提示),确认后保存为正式用例。 生成结果只是起点 —— 步骤、断言、提取规则都可再手动调整。 隐私与边界 API Key 仅存本机,随工作区数据一起持久化;导出全量备份时会包含,分享备份文件前请注意; 供应商收到的内容为接口清单摘要(方法 / URL / Body 片段)与你的指令, 不含 环境变量值与 API Key; 未配置供应商时,Mock 数据、环境变量等离线功能不受影响。 常见问题 生成失败? 依次检查:Base URL 是否带 、模型名是否属于该供应商、Key 是否有效(用「测试连接」验证)、本机能否直连该域名(内网代理环境注意放行)。 想用公司内网模型? 添加自定义供应商,Base URL 填内网网关地址即可(需兼容 OpenAI Chat Completions 协议)。"},{"Slug":"account","Title":"账号与团队","Group":"账号与团队","Text":"账号与团队 登录后可使用 云端空间 :个人空间「我的空间」与团队共享空间,数据保存在本机服务的 SQLite 数据库( ),不上传任何第三方。 注册 / 登录 最左 活动栏 底部 登录 按钮打开弹窗: 注册只需 昵称 + 邮箱 + 密码(≥ 6 位); 登录态保存在本机,令牌失效会自动清除。 空间切换 活动栏 顶部空间徽标 (未登录时为产品 Logo,云端空间显示彩色徽标)点击弹出空间列表: 空间 说明 本地空间 未登录时默认;数据在浏览器 localStorage,不上云 我的空间 个人云端空间,注册即有;换浏览器 / 换设备登录即得同一份数据 团队 · ×× 团队共享空间,成员读写同一份数据 首次切入空的云端空间时,会自动把 当前本地数据播种上去 ; 在云端空间内的所有改动(接口、环境、测试用例、设置) 自动同步 (改动后约 1.5 秒),活动栏底部头像上的状态点显示 已同步 / 同步中(闪烁)/ 同步失败(红); 切回本地空间时恢复离开前的本地快照,两端互不覆盖; 同步策略为 后写覆盖 :多人同时编辑以最后一次保存为准。 团队管理 活动栏账号菜单或空间菜单 → 团队管理 : 新建团队 :创建后自动获得同名共享空间; 重命名 / 删除 :团队卡片右上角的 ✎ / 🗑(仅创建者可见)。重命名就地保存;删除需二次确认,将同时删除成员关系、共享空间与已发布 Mock 数据,不可恢复(若当前正停在该团队空间,会自动切回本地空间); 添加成员 :输入已注册成员邮箱(创建者可操作),邀请时可顺手点选 擅长 预设;成员登录后即可看到并切换到该团队空间; 成员与擅长 :每个团队卡片展示成员列表(头像 / 名称 / 角色 / 擅长标签)。擅长支持预设点选(前端、后端、全栈、测试、运维、移动端、数据库、AI、接口设计、安全)与自定义输入,空格或顿号分隔;创建者可修改任何成员的擅长,成员只能修改自己的(行尾「擅长」按钮)。 数据与安全 密码加盐哈希后存储;令牌为随机 Bearer Token,可随时退出登录作废; 云端空间单份数据上限 20MB; 团队空间权限:全体成员可读写,仅创建者可管理成员与修改他人擅长; 不登录不影响任何本地功能。"},{"Slug":"server-api","Title":"服务端 API 参考","Group":"参考","Text":"本地服务(Gin)默认监听 ,可用环境变量 修改。所有端点仅监听本机回环,供 Web UI 与桌面端调用。 端点总览 方法 路径 用途 GET 健康检查 POST HTTP 代理(全方法、自定义头、超时) POST NDJSON 流式代理(SSE / chunked 透传) POST 建立到目标 WS 的隧道会话 GET 浏览器 attach 到隧道会话(双向桥接) POST 关闭隧道会话 GET 本文档站 HTTP 代理 响应: 请求体 支持 / / / / / , 为原始报文; 可选。 流式代理 请求体与 HTTP 代理一致,响应为 NDJSON 事件流: WebSocket 隧道 attach 后第一条消息为控制帧 ;此后浏览器与目标服务器双向透传。 安全边界 仅允许 (代理)与 (隧道)目标。 拒绝云元数据地址(169.254.169.254 等)防 SSRF。 单响应上限 10 MiB;隧道会话 10 分钟无活动自动回收。 部署建议 开发: (Vite :5173 反代 、 到 :8787)。 生产: 产物交给任意静态服务器, 独立部署;生产环境建议置于内网并按需增加鉴权反向代理。"},{"Slug":"faq","Title":"常见问题","Group":"参考","Text":"请求报 CORS 错误? Web 端请求默认经本地 Gin 代理(gin proxy)发出,不会遇到 CORS。若仍报错: 确认 Gin 在运行: 。 用 / 场景启动(Vite 已配置 反代)。 桌面端优先走 Tauri 原生网络栈,同样不受 CORS 限制。 浏览器 WebSocket 不能加自定义 Header? 规范限制。在 WS「握手配置」里填写 Header 后,OpsxAPI 自动改走本地 Gin 隧道(见《WebSocket》章节),Header 由 Gin 在握手时代为携带。不填 Header 则直连。 外部 wss:// 地址连不上(DNS 失败)? 本机网络环境可能限制部分域名解析。可先用 或换用公共 echo 服务验证;自建服务请确认防火墙与证书。 MQTT 一定要 wss:// 吗? 浏览器只能通过 WebSocket 连 MQTT。常见 Broker 的 WS 端口:EMQX (TLS)、 (明文,仅 http 页面)。 TCP 直连仅在桌面原生环境可行。 数据存在哪里?换电脑怎么办? 集合 / 环境 / 历史保存在浏览器 localStorage。迁移: 导出 → 全量备份 生成 JSON,在新设备 导入 即可。 误删了接口 / 想回到初始状态? 设置(顶栏 ⋯)→ 重置工作区,恢复内置示例集合。日常请养成导出备份的习惯。 SSE 事件流看着卡住了? 事件频率低(如告警流)属正常,等待即可。 部分网关会缓冲 SSE,工具会在直连失败时自动切换 Gin 流式代理;若仍无事件,检查中间层(Nginx 需关闭 )。 发送按钮点击后没反应? 查看响应区是否有红色错误提示(如代理不可达)。 代码 生成 cURL 在终端执行可快速分辨是工具问题还是网络 / 接口问题。 支持文件上传吗? form data 当前支持文本字段;文件上传请用 代码 生成的 cURL / 脚本执行。 更多问题? 服务端能力与边界:见《服务端 API 参考》 各协议用法:见左侧「请求调试」分组"}] \ No newline at end of file +[{"Slug":"intro","Title":"产品介绍","Group":"开始使用","Text":"OpsxAPI 是一款 本地优先 的专业接口工作台,面向运维与开发,把 HTTP、GraphQL、WebSocket、SSE、Socket.IO、MQTT 六种协议的调试放进同一个窗口。 无需注册、无需联网同步,数据全部保存在本地浏览器(localStorage),导出备份随时带走。 功能总览 能力 说明 六种协议 HTTP / GraphQL / WebSocket / SSE / Socket.IO / MQTT 集合管理 目录树、搜索、右键菜单、拖拽移动、多标签页 多环境变量 、 全局替换,一键切换环境 数据导入 OpenAPI/Swagger、Postman、Apifox、Apipost、cURL、HAR、Insomnia、Hoppscotch、Markdown 等 数据导出 OpenAPI 3.0、Postman v2.1、Markdown 文档、全量备份 代码生成 cURL / JavaScript fetch / Python requests / Go net/http 代理能力 本地 Gin 代理绕开 CORS;WebSocket 隧道支持自定义握手 Header 双端形态 浏览器(Web)与桌面端(Tauri)共用同一套界面 支持的协议与场景 协议 典型场景 特色 HTTP / REST 调试 REST 接口、运维平台 API 7 种方法、6 种请求体、3 种鉴权 GraphQL 查询网关、中台接口 Query + Variables 独立编辑 WebSocket 实时推送、行情、IM 自定义握手 Header(经本地隧道) SSE AI 流式输出、监控事件流 直连失败自动切代理、断点续传 Socket.IO Node 实时应用 事件监听、emit 参数、path/auth 配置 MQTT IoT 设备、消息总线 通配符订阅、QoS、自动重连 为什么选 OpsxAPI 数据主权 :所有集合、环境、历史保存在本机,不经过任何第三方服务器。 开箱即用 : 一条命令启动;首次打开自带覆盖全部协议的示例集合。 工程友好 :导入自动建目录、路径参数自动转变量;导出即标准 OpenAPI,可回流 Apifox / Swagger 生态。 轻量克制 :一个 Go 二进制 + 一份前端,资源占用小,内网可部署。 提示 :想快速感受?直接看下一章《快速开始》,30 秒发出第一个请求。"},{"Slug":"quickstart","Title":"快速开始","Group":"开始使用","Text":"环境要求 组件 版本 用途 Node.js ≥ 20 Web 前端(Vite) Go ≥ 1.23 本地代理服务(Gin) Rust + Cargo 可选 仅编译桌面端(Tauri)时需要 启动 看到下面的输出即可打开浏览器: 命令 说明 浏览器版(推荐) 桌面窗口(Tauri,共用同一 UI) Web + 桌面一起开 / 查看状态 / 停止 30 秒发出第一个请求 1. 打开 \u003chttp://127.0.0.1:5173 ,左侧是内置的「示例集合」。 2. 点击 HTTP · httpbin → 获取请求回显 。 3. 点击右上角 发送 。 4. 下方响应区显示 、耗时与 JSON 高亮响应体 —— 完成! 桌面端(可选) 桌面端复用同一份界面与数据通道,HTTP 请求走 Tauri 原生网络栈,不依赖浏览器 CORS。 提示 :首次打开会自动创建两个环境(正式 / 本地)和一个覆盖全部协议的示例集合,可随时在「设置」中重置。"},{"Slug":"interface","Title":"界面导览","Group":"开始使用","Text":"OpsxAPI 主界面分为五个区域: 顶栏 元素 作用 + 新建 快捷创建各协议请求 / 目录(默认建在第一个集合) 导入 / 导出 打开数据导入、导出对话框 环境选择器 切换当前环境(变量随之生效),「⚙ 环境」管理环境与变量 🌙 / ☀️ 明暗主题切换 ⋯ 设置(主题、超时、重置工作区) 左侧集合树 点击请求 → 在标签页中打开;点击目录 → 展开 / 折叠。 右键 任意节点:新建、重命名、复制、导出、删除。 拖拽 请求或目录到另一个目录,即可移动(支持跨集合)。 顶部搜索框按名称 / 方法 / URL 实时过滤。 底部为 历史记录 :显示最近请求的状态码与耗时,点击可回跳到对应接口。 标签页栏 同时打开多个接口,中键或 × 关闭;右键支持「关闭其他 / 关闭全部」。正在连接中的实时协议标签会显示呼吸圆点。 请求编辑区(上半部分) 按协议呈现不同编辑器: 协议 标签页 HTTP Params · Body · Headers · Cookies · Auth · 说明 GraphQL Query · Variables · Headers · Auth WebSocket 消息 · 握手配置 · 说明 SSE 事件流 · 连接配置 · 说明 Socket.IO 事件 · 连接配置 · 说明 MQTT 消息 · Broker / 订阅 · 发布 · 说明 响应区(下半部分) HTTP / GraphQL :响应体(Pretty JSON 高亮)、Raw、响应头,右上角状态 / 耗时 / 大小 / 复制。 实时协议 :分色消息日志(接收 · 绿、发送 · 蓝、系统 · 灰、错误 · 红),顶部实时统计收发条数与连接时长。"},{"Slug":"http","Title":"发送 HTTP 请求","Group":"请求调试","Text":"发送请求 1. 选择方法(GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS)。 2. 输入 URL,支持 形式的变量(见《环境与变量》)。 3. 点击 发送 ,或直接在 URL 输入框按 Enter 。 请求默认经本地 Gin 代理( )发出,天然绕开浏览器 CORS 限制。 Params(查询参数) 键值对编辑器,每行可勾选启用 / 停用、填写说明。URL 中已有的查询串会被拆分到表格里,两边双向同步。 Body(请求体) 模式 适用场景 none GET 等无体请求 JSON REST 接口(默认自动补 ,带格式化按钮) XML / Text / HTML 原始报文 x www form urlencoded 表单提交 form data multipart 文本字段(文件上传请用代码生成的 cURL) Headers / Cookies Headers:键值对,支持 ; 、 由 Auth / Cookies 标签统一管理时避免重复。 Cookies:键值对,发送时自动拼接为 请求头。 代码生成 点击 URL 栏右侧 代码 ,可生成: cURL(可直接在终端执行) JavaScript fetch Python requests Go net/http 生成内容保留 原样,便于粘贴到脚本后再替换。 响应面板 标签 内容 响应体 JSON 自动格式化 + 语法高亮;其他类型原样展示 Raw 未格式化的原始文本 响应头 全部响应头表格 右上角常驻 状态码 / 耗时 / 大小 / Content Type ,并提供「复制体」按钮。"},{"Slug":"auth","Title":"接口鉴权","Group":"请求调试","Text":"在 HTTP / GraphQL / SSE 请求的 Auth 标签中选择鉴权方式: Bearer Token Token 支持 变量引用 —— 推荐把 token 放进环境变量,一次配置全局生效。 Basic Auth 填写用户名 / 密码,自动进行 Base64 编码后放入 头。 API Key 字段 说明 Key 名 如 值 支持 位置 Header(默认)或 Query 参数 组合规则 鉴权产生的 Header 会与 Headers 标签中的同名项合并(鉴权优先)。 API Key 放 Query 时,会追加到 Params,不会重复出现在 Headers。 导入 OpenAPI / Postman 时,文档中声明的 会自动映射到对应鉴权方式。 注意 :鉴权信息保存在本地集合数据中。如需分享集合,请先通过「导出」审查是否包含敏感 token。"},{"Slug":"env","Title":"环境与变量","Group":"请求调试","Text":"前置 URL 环境可配置 前置 URL (如 )。配置后,请求里只需写相对路径: URL 输入框会以 前缀块 + 路径 的形式一体展示,前缀部分不可误编辑(点击可跳转环境管理); 切换环境即自动换成对应服务地址 ,无需改请求; 路径以 开头(或为空)才启用拼接;写完整 地址的请求不受影响; 未配置前置 URL 的环境,请求仍可写完整 URL 或用 。 历史数据自动迁移:旧的 会被自动转为「前置 URL + /path」, 变量写入环境的前置 URL 字段。 变量语法 在 URL、Params、Headers、Cookies、Body、鉴权字段中,使用双花括号引用变量: 发送时统一替换为 当前启用环境 中同名变量的值;未定义的变量保留原样,便于发现遗漏。 切换环境 顶栏环境切换器显示 彩色单字徽标 + 环境名 + 变量数 ,点击弹出环境列表(当前环境带「当前」标记),一眼防误发;底部可进入「管理环境」或「不使用环境」。 管理环境 顶栏 环境 按钮打开管理弹窗(左侧环境列表 + 右侧编辑区): 左栏 :环境列表(徽标 + 名称 + 当前标记),底部虚线按钮新建环境。 右栏 :提示条(变量引用语法 + 文档直达)、环境重命名、设为当前 / 取消使用、 复制环境 、删除;变量表支持 按名称 / 描述搜索 、逐行启用停用、内联编辑。 所有修改即时生效并自动保存。 输入 {{ 自动补全 在 URL 输入框或任意「值」输入框键入 ,会弹出当前环境的变量浮层: ↑↓ 选择、Enter / Tab 插入完整的 ;Esc 关闭。 浮层同时显示变量值,选之前即可确认。 内置示例环境: 环境 baseUrl token 正式 demo token 本地 dev token 变量从哪来自动生成 来源 行为 导入 OpenAPI / Swagger 地址写入 ;路径参数 转为 并写入示例值 导入 Postman 写入当前环境 手动输入 在环境面板随时增改 实际地址预览 URL 含 时,编辑器会在 URL 栏下方实时显示替换后的实际地址,切换环境立即更新。 提示 :路径参数用变量而不是写死,是接口可复用的关键 —— 复制接口后只改变量值即可调试另一个资源。"},{"Slug":"graphql","Title":"GraphQL","Group":"请求调试","Text":"编辑查询 GraphQL 请求由三部分组成: 部分 说明 Query GraphQL 查询语句,支持变量定义 Variables JSON 对象,随查询一起发送;非法 JSON 会红框提示 Headers / Auth 与 HTTP 请求一致 发送 点击 执行 (或 URL 栏按 Enter)。实际发送的是标准 GraphQL over HTTP: 响应面板会自动格式化 / ,字段名高亮。 内置示例 示例集合中的「国家查询 · countries」连接公共 GraphQL 服务 \u003chttps://countries.trevorblades.com/graphql ,无需鉴权,可直接体验。 最佳实践 把 endpoint 存为 ,环境切换即切换网关。 需要鉴权时在 Auth 标签配置 Bearer,不必手写 Header。 想看实际报文?点 代码 生成 cURL,即为此查询的完整 HTTP 请求。"},{"Slug":"websocket","Title":"WebSocket","Group":"请求调试","Text":"连接 1. 填写 或 地址。 2. 点击 连接 ;状态点变绿(已连接)后即可收发消息。 3. 再次点击按钮即断开。 消息收发 消息日志 :接收(绿)/ 发送(蓝)/ 系统(灰)/ 错误(红)分色展示,顶部统计 ↑↓ 条数与连接时长。 发送框 :支持 JSON / 纯文本; ⌘/Ctrl + Enter 快速发送;「格式化 JSON」一键美化。 子协议 「握手配置」标签中的 子协议 填逗号分隔列表,例如: 自定义握手 Header(隧道模式) 浏览器原生 WebSocket 不能 携带自定义 Header(如 、 )。OpsxAPI 的处理方式: 1. 「握手配置」中填写 Header 后,工具自动改走 本地 Gin 隧道 : 浏览器 → Gin (普通 WS) Gin → 目标服务器(携带你填的全部 Header) 2. 不填 Header 时直连目标,零额外开销。 注意 :隧道要求本地 Gin 服务(:8787)在运行,即用 / 启动的默认形态。 调试技巧 消息过多时点「清空」;日志上限 500 条,超出自动滚动淘汰。 把常用报文保存在「说明」里,或为不同报文建多个 WS 请求节点。"},{"Slug":"sse","Title":"SSE 事件流","Group":"请求调试","Text":"连接事件流 1. 填写 SSE 地址(如 )。 2. 点击 连接 ,事件按到达顺序流入消息日志。 3. 每条消息按 名称归类显示; 为 JSON 时自动格式化。 直连与代理自动切换 默认 直连 目标站(fetch 流式读取,延迟最低)。 直连因 CORS / 网络失败时,自动降级走 本地 Gin 流式代理 ,日志中会提示切换过程,无需手工干预。 断点续传(Last Event ID) 「连接配置」中填写 Last Event ID,连接时以 请求头携带,支持断点续传的流会在该事件之后继续推送。 连接配置项 配置 说明 Query 参数 / Headers 以键值对附加 Last Event ID 断点续传游标 仅显示 data 内容 开启后只显示 载荷;关闭则连同 名称一起显示 鉴权 与 HTTP 相同(Bearer / Basic / API Key) 典型场景 LLM 流式输出接口( ) 监控 / 审计事件流 需要长期挂机观察的推送通道"},{"Slug":"socketio","Title":"Socket.IO","Group":"请求调试","Text":"连接 1. 填写服务地址( 不含 路径),如 。 2. 点击 连接 ,状态变为「已连接」后即可收发事件。 监听事件 「连接配置」→ 监听事件 填逗号分隔的事件名: 留空则接收全部服务端事件(onAny 模式)。事件名在每条日志中标注。 发送(emit) 「事件」标签底部的发送区: 事件名 :如 参数 :JSON 数组,作为 的参数 连接选项 以 JSON 配置传给 socket.io client: 选项 说明 path 服务端挂载路径,默认 transports / ,默认两者皆可 auth 握手鉴权载荷,支持 提示 :浏览器直连外部站点同样受 CORS 限制,Socket.IO 服务端需配置对应 origin,或把服务与本工具部署在同源下。"},{"Slug":"mqtt","Title":"MQTT","Group":"请求调试","Text":"连接 Broker 浏览器环境要求 WebSocket 形式 的 Broker 地址: 配置 说明 用户名 / 密码 可选,支持 ClientId 默认随机生成;同一 Broker 上必须唯一 QoS 0 至多一次 · 1 至少一次 · 2 恰好一次 订阅 「Broker / 订阅」中填写逗号分隔的主题,支持通配符: 注意 :订阅在 建立连接时 生效,修改主题后需断开重连。 发布 「发布」标签配置主题与 Payload(JSON / 纯文本),点击发布按钮即送达;已发布消息以蓝色出现在日志中并标注主题。 公共测试 Broker Broker 地址 EMQX HiveMQ 两个浏览器窗口连接同一主题,即可互相收发验证。 行为说明 断线自动重连(3 秒间隔),状态点显示「重连中」。 消息日志上限 500 条;QoS 与主题逐条标注。"},{"Slug":"import","Title":"数据导入","Group":"数据管理","Text":"顶栏 导入 打开导入对话框,支持三种来源: 来源 说明 粘贴文本 / cURL 直接粘贴任意受支持格式的内容 文件导入 选择或拖入 / / / / URL 导入(Swagger) 填写 OpenAPI 地址,经本地 Gin 代理拉取,避开 CORS 格式 自动识别 ,也可手动指定。 支持格式 格式 细节 OpenAPI 3.0 / 3.1、Swagger 2.0 JSON 或 YAML;Apifox 导出的 OpenAPI 同样适用 Postman Collection v2.0 / v2.1,含鉴权、变量、GraphQL body Apipost 项目 格式(宽容解析) cURL 引号、续行、 / / / 、cookie、代理等参数 HAR 按域名分组、相同请求去重 Insomnia 导出文件 v3 / v4,含 workspace / group 层级 Hoppscotch 集合 JSON(数组或单对象) Markdown 提取文中的 tags /pets/{petId} {{petId}} servers baseUrl variable` 当前环境变量 结果确认 导入完成显示汇总:新建集合、接口数量、写入的环境变量与告警(如跳过的文件上传字段)。集合名重复时自动追加序号。"},{"Slug":"export","Title":"导出与代码生成","Group":"数据管理","Text":"顶栏 导出 (多集合时可选择目标集合): 格式 用途 OpenAPI 3.0 · JSON / YAML 回流 Apifox、Swagger UI、代码生成器 Postman v2.1 直接导入 Postman Markdown 接口文档 带目录、参数表格、请求体代码块,可放 Wiki 全量备份 全部集合 + 环境,用于迁移与灾备 每种格式均支持 复制到剪贴板 与 下载文件 ;对话框内实时预览导出内容。 全量备份与恢复 备份文件是包含 标记的 JSON。 恢复:导入对话框 → 粘贴备份内容或选择备份文件 → 格式自动识别为「OpsxAPI 备份」。 代码生成(单请求) 在 HTTP / GraphQL 编辑器点 代码 : 目标 说明 cURL 含方法、头、体、cookie,可直接执行 JavaScript fetch 风格 Python requests 含 完整脚本 Go net/http 完整 函数 保留原样输出,方便接入你自己的配置体系。 提示 :导出的 OpenAPI 中,目录名会成为 ,请求说明写入 ,鉴权自动生成 。"},{"Slug":"mock","Title":"Mock 数据","Group":"数据管理","Text":"Mock 数据 联调时后端接口没就绪?OpsxAPI 内置 Mock.js 风格的数据生成器 ,两种用法覆盖绝大多数场景: 1. 显式表达式 —— 在参数值、Body 中直接写 、 ; 2. 智能匹配 —— 开启后按 字段名 自动套用规则表( → 姓名、 → 邮箱…),一行不用写。 开启与配置 顶栏 ⋯ → Mock 数据 (或「设置 → Mock 数据」): 启用 Mock 数据 :总开关,关闭后 表达式按普通文本原样发送; 智能 Mock 匹配 :JSON Body 字段值未写表达式时,按字段名匹配规则自动生成; 智能匹配规则表 :数据类型(string / number / integer / boolean)、匹配方式(通配符 / 完全匹配 / 正则)、匹配规则(如 )、Mock 表达式、备注与启用开关,支持搜索、新增、删除与恢复默认。 规则 从上到下 匹配,先命中先用;类型需与 JSON 字段实际类型一致(KV 参数一律按 string 匹配)。 表达式速查 表达式 说明 示例输出 中文姓名 邮箱 手机号 形式 11 位 身份证号 18 位 UUID / GUID 整数 小数(保留 2 位) 布尔 随机字符串 日期时间 / 日期 / 时间 — 毫秒时间戳 / / URL / 域名 / IPv4 — / / 省市区 / 中文标题 / 句子 — 图片地址 picsum 随机图 从列表中选择 未列出的写法会 原样发送 ,可放心与真实值混用。 在请求中使用 Params / x www form urlencoded / form data :值一栏直接填表达式。 JSON Body :叶子值写表达式,或交给智能匹配: Body 工具栏提供两个辅助按钮: 智能填充 :按规则把 JSON 各字段值批量替换为对应 表达式,生成可编辑的 Mock 模板; Mock 预览 :即时查看发送时实际生成的数据,不必真的发请求。 生效范围 生效:Query 参数、JSON / XML / Text Body、x www form urlencoded、form data、GraphQL Variables; 不生效:Headers、Cookies、鉴权字段(避免意外改坏签名与令牌)。 提示:Mock 在 发送瞬间 求值,同一请求每次发送都会得到新数据;预览结果仅供核对格式。 本地 Mock 服务(/mock) 除了发送时的离线 Mock,还能把接口 发布成本地 Mock 服务 ,给前端 / 同事直接联调(类似 Apifox 的 Mock URL): 1. 登录并切换到「我的空间」或团队空间(Mock 按空间隔离); 2. HTTP 请求 URL 行点 Mock 链接 → 选择响应状态码与延迟 → 发布 (一次发布当前空间集合内全部 HTTP 接口); 3. 得到地址: (每空间一个 16 位随机片段,不可枚举、不暴露归属),例如 。 特点: 每次请求随机生成 :响应模板里的 、 等表达式由服务端实时求值; 发布时自动把智能 Mock 规则烘焙进模板(开启智能匹配时); 非 GET 接口可用 快速验证;延迟最高 5 秒,用于模拟慢接口 / loading 态。 频率限制(防滥用) 本地 Mock 服务按 IP + 空间 限流: 20 QPS(每秒 20 次)或 60 QPM(每分钟 60 次) ,超限返回 与 。依据 :未经授权或滥用行为平台有权采取限流、暂停服务等措施; 通过技术手段规避限流属于违规行为 。Mock 定位是联调辅助,请勿当作对外流量服务使用。"},{"Slug":"manage","Title":"集合与历史","Group":"数据管理","Text":"新建 入口 行为 顶栏「+ 新建」 快捷菜单,建在第一个集合 树右键「新建请求 / 目录」 精确指定位置 新建对话框 选择类型(六协议 + 目录)、命名、目标集合与目录 接口名自动避免重复(追加序号)。 集合树操作 操作 方式 重命名 右键 → 重命名 复制 右键 → 复制(深拷贝子树,名字加「副本」) 移动 拖拽到目标目录(支持跨集合) 删除 右键 → 删除(直接生效,请先备份重要数据) 搜索 侧栏搜索框,匹配名称 / 方法 / URL 标签页 点击树节点在标签页打开;同时打开数量不限。 中键 / × 关闭;右键「关闭其他 / 关闭全部」。 关闭实时协议(WS / SSE / MQTT…)标签时自动断开连接。 历史记录 每次 HTTP / GraphQL 发送记录:时间、方法、接口名、状态码,失败标红。 点击记录跳回对应接口;最多保留 100 条;可一键清空。 数据存储与备份 位置:浏览器 localStorage(键 ),自动保存(防抖 400ms)。 内容:集合、环境、历史、设置。标签页会话与实时消息不落盘。 备份:导出 → 全量备份;恢复:导入备份文件。 重置:设置 → 重置工作区(恢复初始示例,请先备份)。 注意 :桌面端与浏览器是两份独立的 localStorage。跨端迁移请使用全量备份 / 导入。"},{"Slug":"testing","Title":"自动化测试","Group":"自动化测试与 AI","Text":"自动化测试 把日常联调的接口串成 场景用例 ,一条龙验证业务流程。三种触发方式共用同一套用例模型: 方式 说明 适合 手动运行 用例编排好后点「运行」 日常自测、回归 AI 生成 模型按接口清单自动编排步骤与断言 快速起盘、补断言思路 定时任务 应用运行期间按间隔自动执行 持续监控、冒烟 场景用例 从侧栏底部「自动化测试」进入。用例 = 有序步骤列表,每个步骤: 引用一个 HTTP 接口 (从集合中选择,可跨集合、跨目录); 断言 :状态码 / 耗时 / Header / Body 四类目标,支持 等于、不等于、大于、小于、包含、存在 与 JSONPath 取值比较; 变量提取 : → 环境变量 ,后续步骤 URL / Body 用 引用,实现「创建 → 查询 → 删除」链式传值; 可单独禁用、上下移动、删除。 用例级选项「 失败后继续 」:默认失败即停(快速定位首个问题);打开后跑完全部步骤再看整体报告。 运行报告 统计卡:通过 / 失败步骤数、总耗时、断言数、通过率; 步骤明细:方法 + URL + 状态码 + 耗时,逐条断言的期望 / 实际对照(可按 通过 / 失败 过滤); 「再次运行」一键回归;「导出报告」生成 Markdown 存档或归档到缺陷单; 每个用例保留最近 50 次运行记录,可下拉切换历史报告。 定时任务 用例头部的 ⏱ 按钮或左侧「定时任务」入口: 间隔可选 5 / 15 / 30 / 60 分钟、6 小时、每天; 独立 启用 / 暂停 开关,显示下次运行时间与上次结果; 应用保持运行时生效 :OpsxAPI 窗口(或 Web 页面)开着才会按时触发并记录报告,关闭后自动暂停 —— 适合工作时段的持续监控,不能替代服务器侧的 CI。 提示:定时触发的请求同样走当前环境与 Mock 设置;跨环境回归前先在顶栏切换环境。 AI 生成用例 见 。配置模型供应商后,「AI 生成」会读取集合的 HTTP 接口清单(方法、URL、Body 示例),按业务依赖编排步骤、断言与变量提取,生成前可附加一句要求(如「重点覆盖登录态与分页」),预览确认后保存。"},{"Slug":"ai","Title":"AI 助手","Group":"自动化测试与 AI","Text":"AI 助手 接入 OpenAI 兼容协议 的大模型,为自动化测试提供 AI 能力。模型调用直连供应商;供应商配置(包括密钥)随当前工作区保存,团队工作区会同步工作区数据。 配置模型供应商 设置 → 大模型 ,选择预设并点击「+」,在右侧填写模型 ID、密钥和 Base URL,测试连接后点击「保存配置」。内置常用预设: 供应商 Base URL 模型示例 DeepSeek 智谱 GLM Moonshot Kimi OpenAI Ollama 本地 API Key :Ollama 等本地服务可留空; 测试连接 :发一条 ping 验证地址 / 密钥 / 模型名;离线时也可先保存,之后再测试; 多个供应商时可在左侧选择,保存后点「设为默认」指定自动化测试使用的模型。 AI 生成场景用例 自动化测试视图 →「 AI 生成 」: 1. 选择集合(AI 读取其中全部 HTTP 接口:方法、URL、Body 示例); 2. 可选填写一句要求,如「覆盖下单 → 支付 → 查询流程,重点断言金额字段」; 3. 模型按业务依赖编排步骤,输出断言与变量提取(如 ,后续步骤用 ); 4. 预览生成的步骤清单(未识别的接口会自动跳过并提示),确认后保存为正式用例。 生成结果只是起点 —— 步骤、断言、提取规则都可再手动调整。 隐私与边界 API Key 随当前工作区数据保存;团队工作区会同步,导出全量备份时也会包含。分享工作区或备份文件前请检查密钥; 供应商收到的内容为接口清单摘要(方法 / URL / Body 片段)与你的指令, 不含 环境变量值与 API Key; 未配置供应商时,Mock 数据、环境变量等离线功能不受影响。 常见问题 生成失败? 依次检查:Base URL 是否带 、模型名是否属于该供应商、Key 是否有效(用「测试连接」验证)、本机能否直连该域名(内网代理环境注意放行)。 想用公司内网模型? 添加自定义供应商,Base URL 填内网网关地址即可(需兼容 OpenAI Chat Completions 协议)。"},{"Slug":"account","Title":"账号与团队","Group":"账号与团队","Text":"账号与团队 登录后可使用 云端空间 :个人空间「我的空间」与团队共享空间,数据保存在本机服务的 SQLite 数据库( ),不上传任何第三方。 注册 / 登录 最左 活动栏 底部 登录 按钮打开弹窗: 注册只需 昵称 + 邮箱 + 密码(≥ 6 位); 登录态保存在本机,令牌失效会自动清除。 空间切换 活动栏 顶部空间徽标 (未登录时为产品 Logo,云端空间显示彩色徽标)点击弹出空间列表: 空间 说明 本地空间 未登录时默认;数据在浏览器 localStorage,不上云 我的空间 个人云端空间,注册即有;换浏览器 / 换设备登录即得同一份数据 团队 · ×× 团队共享空间,成员读写同一份数据 首次切入空的云端空间时,会自动把 当前本地数据播种上去 ; 在云端空间内的所有改动(接口、环境、测试用例、设置) 自动同步 (改动后约 1.5 秒),活动栏底部头像上的状态点显示 已同步 / 同步中(闪烁)/ 同步失败(红); 切回本地空间时恢复离开前的本地快照,两端互不覆盖; 同步策略为 后写覆盖 :多人同时编辑以最后一次保存为准。 团队管理 活动栏账号菜单或空间菜单 → 团队管理 : 新建团队 :创建后自动获得同名共享空间; 重命名 / 删除 :团队卡片右上角的 ✎ / 🗑(仅创建者可见)。重命名就地保存;删除需二次确认,将同时删除成员关系、共享空间与已发布 Mock 数据,不可恢复(若当前正停在该团队空间,会自动切回本地空间); 添加成员 :输入已注册成员邮箱(创建者可操作),邀请时可顺手点选 擅长 预设;成员登录后即可看到并切换到该团队空间; 成员与擅长 :每个团队卡片展示成员列表(头像 / 名称 / 角色 / 擅长标签)。擅长支持预设点选(前端、后端、全栈、测试、运维、移动端、数据库、AI、接口设计、安全)与自定义输入,空格或顿号分隔;创建者可修改任何成员的擅长,成员只能修改自己的(行尾「擅长」按钮)。 数据与安全 密码加盐哈希后存储;令牌为随机 Bearer Token,可随时退出登录作废; 云端空间单份数据上限 20MB; 团队空间权限:全体成员可读写,仅创建者可管理成员与修改他人擅长; 不登录不影响任何本地功能。"},{"Slug":"server-api","Title":"服务端 API 参考","Group":"参考","Text":"本地服务(Gin)默认监听 ,可用环境变量 修改。所有端点仅监听本机回环,供 Web UI 与桌面端调用。 端点总览 方法 路径 用途 GET 健康检查 POST HTTP 代理(全方法、自定义头、超时) POST NDJSON 流式代理(SSE / chunked 透传) POST 建立到目标 WS 的隧道会话 GET 浏览器 attach 到隧道会话(双向桥接) POST 关闭隧道会话 GET 本文档站 HTTP 代理 响应: 请求体 支持 / / / / / , 为原始报文; 可选。 流式代理 请求体与 HTTP 代理一致,响应为 NDJSON 事件流: WebSocket 隧道 attach 后第一条消息为控制帧 ;此后浏览器与目标服务器双向透传。 安全边界 仅允许 (代理)与 (隧道)目标。 拒绝云元数据地址(169.254.169.254 等)防 SSRF。 单响应上限 10 MiB;隧道会话 10 分钟无活动自动回收。 部署建议 开发: (Vite :5173 反代 、 到 :8787)。 生产: 产物交给任意静态服务器, 独立部署;生产环境建议置于内网并按需增加鉴权反向代理。"},{"Slug":"faq","Title":"常见问题","Group":"参考","Text":"请求报 CORS 错误? Web 端请求默认经本地 Gin 代理(gin proxy)发出,不会遇到 CORS。若仍报错: 确认 Gin 在运行: 。 用 / 场景启动(Vite 已配置 反代)。 桌面端优先走 Tauri 原生网络栈,同样不受 CORS 限制。 浏览器 WebSocket 不能加自定义 Header? 规范限制。在 WS「握手配置」里填写 Header 后,OpsxAPI 自动改走本地 Gin 隧道(见《WebSocket》章节),Header 由 Gin 在握手时代为携带。不填 Header 则直连。 外部 wss:// 地址连不上(DNS 失败)? 本机网络环境可能限制部分域名解析。可先用 或换用公共 echo 服务验证;自建服务请确认防火墙与证书。 MQTT 一定要 wss:// 吗? 浏览器只能通过 WebSocket 连 MQTT。常见 Broker 的 WS 端口:EMQX (TLS)、 (明文,仅 http 页面)。 TCP 直连仅在桌面原生环境可行。 数据存在哪里?换电脑怎么办? 集合 / 环境 / 历史保存在浏览器 localStorage。迁移: 导出 → 全量备份 生成 JSON,在新设备 导入 即可。 误删了接口 / 想回到初始状态? 设置(顶栏 ⋯)→ 重置工作区,恢复内置示例集合。日常请养成导出备份的习惯。 SSE 事件流看着卡住了? 事件频率低(如告警流)属正常,等待即可。 部分网关会缓冲 SSE,工具会在直连失败时自动切换 Gin 流式代理;若仍无事件,检查中间层(Nginx 需关闭 )。 发送按钮点击后没反应? 查看响应区是否有红色错误提示(如代理不可达)。 代码 生成 cURL 在终端执行可快速分辨是工具问题还是网络 / 接口问题。 支持文件上传吗? form data 当前支持文本字段;文件上传请用 代码 生成的 cURL / 脚本执行。 更多问题? 服务端能力与边界:见《服务端 API 参考》 各协议用法:见左侧「请求调试」分组"}] \ No newline at end of file diff --git a/homepage/dist/index.html b/homepage/dist/index.html index 841cadb..78a0d17 100644 --- a/homepage/dist/index.html +++ b/homepage/dist/index.html @@ -45,6 +45,7 @@ 协议 导入导出 定价 + 授权 文档 @@ -57,18 +58,18 @@
    -
    v1.0 正式发布 · 数据 100% 本地
    +
    v0.1 公测 · 数据 100% 本地

    一个工作台,
    调完六种协议的接口

    HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT。
    集合管理、多环境变量、全格式导入导出、代码生成、内置文档站 —— 运维与开发的自托管接口利器。

    - 本地优先自定义 WS 握手头SSE 断点续传OpenAPI 双向同步 + 本地优先自定义 WS 握手头SSE 断点续传本机 Mock 服务
    @@ -227,14 +228,14 @@

    先在工作台免费用起来,需要授权与服务时再付费。

    -
    个人版
    +
    专业版
    ¥99/ 年
    单设备授权 · 全功能
    • ✓ 六种协议全部调试能力
    • ✓ 集合 / 环境 / 历史无限量
    • ✓ 全格式导入导出 + 代码生成
    • -
    • ✓ 桌面端 + Web 双形态
    • +
    • ✓ 桌面端单机全功能
    • ✓ 1 年免费升级
    购买咨询 @@ -317,9 +318,9 @@

    把接口调试收回自己手里

    -

    现在打开工作台,30 秒发出第一个请求;采购事宜邮件联系,当天回复。

    +

    按快速上手启动工作台,30 秒发出第一个请求;采购事宜邮件联系,当天回复。

    diff --git a/homepage/license.html b/homepage/license.html new file mode 100644 index 0000000..2324b90 --- /dev/null +++ b/homepage/license.html @@ -0,0 +1,211 @@ + + + + + +授权与激活 · OpsxAPI + + + + + + + + +
    +
    + 30 天免费试用 · 首次启动自动开始 · 无需注册 +

    授权与激活

    +

    一次说清四件事:怎么买、授权码怎么交付、程序里怎么激活、换机与断网怎么办。授权绑定设备而非账号,全流程不需要注册。

    +
    + +
    +
    +

    交付与激活:四步

    +

    从下单到激活通常当天完成,最短几分钟。

    +
    +
    + STEP 1 +

    下单确认

    +

    邮件说明产品、授权类型(包年 / 永久 / 团队席位 / 私有化)与设备数量。支持对公转账与开具发票,工作日当天回复。

    +
    +
    + STEP 2 +

    邮件交付授权码

    +

    付款确认后即刻签发,授权码以邮件送达你的邮箱:联网用 LIC- 前缀的在线授权码,隔离内网用 LICOFF- 前缀的离线许可证。

    +
    +
    + STEP 3 +

    应用内激活

    +

    打开「设置 → 授权」,粘贴授权码点击激活。程序连接授权服务器 licence.rlwj.cn 登记本机设备并回写一次性激活令牌,此后每 7 天自动校验一次。

    +
    +
    + STEP 4 +

    换机与续期

    +

    在本机自助解绑即可把席位迁到新机器,每年免费换绑 2 次;到期前 30 天邮件提醒,续期后签发新授权码替换即可。

    +
    +
    +
    +
    + +
    +
    +

    两种激活方式

    +

    能联网就用在线授权码,完全不出网就用离线许可证——功能与有效期一致。

    +
    + + + + + + + + + + + + +
    对比项在线授权码 LIC-离线许可证 LICOFF-
    是否联网激活时联网一次,之后每 7 天校验一次全程无需联网,本地验签
    设备绑定绑定设备 ID,一机一码绑定设备 ID,一机一码
    断网可用激活后可连续离线使用 30 天始终有效,与网络无关
    有效期包年 365 天;永久授权长期有效单张最长 365 天,可续签
    换绑应用内自助解绑,每年免费换绑 2 次邮件换取新证并解绑旧设备
    适合场景办公网、开发机、云主机生产内网、专有云、隔离环境
    +
    +
    +

    授权服务器会收到什么?客户端只与 licence.rlwj.cn(443)通信,用于设备登记与周期校验,只发送授权码与设备标识(设备 ID、设备名称)——不发送接口内容、请求/响应报文、环境变量与集合数据等任何业务数据。接口数据本机存储,全流程不上云。服务端返回 Ed25519 签名,客户端本地验签,验签失败按未授权处理。

    +
      +
    • 内网只放行白名单出网:把 licence.rlwj.cn 加入白名单,或直接使用离线许可证。
    • +
    • 授权服务器不可达:按宽限期继续可用,业务不会当场中断;恢复网络后自动补校验。
    • +
    • 不想让客户端出网:一律选离线许可证,客户端永不发起网络请求。
    • +
    +
    +
    +
    + +
    +
    +

    授权类型与价格

    +

    个人非商业用途免费;工作中的商业用途需要授权。

    +
    + + + + + + + + + + + +
    授权价格设备数包含
    个人 · 非商业¥0不涉及授权六种协议调试、集合与环境本地保存;商业用途需购买授权
    专业版 · 包年¥99 / 年3 台全协议调试能力、集合/环境/历史无限量、全格式导入导出与代码生成、本机 Mock、设备自助解绑
    专业版 · 永久¥199 一次性3 台包年全部能力,一次买断、无续费,含发布版本与一年内更新
    团队版¥99 / 人 / 年按席位,5 席起个人版全部能力 + 集合批量导入分发、团队共享空间(自托管)、优先技术支持
    企业私有化¥8,000 / 年(标价)单法人不限席位整机 / 内网部署、数据不出企业、远程部署协助、定制开发通道;可选 ¥30,000 一次性买断
    +
    +

    完整功能对比、让利与组合报价见 定价页。表中为标价,成交可按规模与场景议价。

    +
    +
    + +
    +
    +

    授权常见问题

    +
    +
    授权码多久能收到?

    付款确认后即刻签发并邮件发出,工作日通常当天完成(含开票需求)。邮件里就是完整授权码,无需注册账号或登录任何后台。

    +
    授权绑定设备还是账号?

    绑定设备。每台机器有唯一设备 ID,一个授权码按授权的设备数占座,与账号无关;团队版按席位签发多个授权码,组织内可转移。

    +
    换电脑 / 重装系统后设备 ID 变了怎么办?

    旧机还在:在「设置 → 授权」点解绑,腾出席位后在新机激活。旧机已不在手:邮件与我们说明(附授权码后四位与新设备 ID),由我们在授权后台释放旧设备。每年免费换绑 2 次。

    +
    断网了授权会失效吗?

    不会。离线许可证本地自验,永不联网也有效;在线授权码激活后可连续离线使用 30 天,超时后联网校验一次即可继续。授权服务器不可达时按宽限期放行,不会打断正在进行的调试。

    +
    生产内网完全不出网,怎么激活?

    改用离线许可证(LICOFF-):在应用内导出本机机器码,邮件发我们换取离线证,本地导入即完成激活,全程不需要客户端联网。

    +
    续期怎么操作?

    到期前 30 天我们会邮件提醒。付款后签发新的授权码,在「设置 → 授权」里替换旧码即可,同一台设备无需先解绑;永久授权无需续期。

    +
    试用期结束后我的接口集合和环境还在吗?

    都在。授权只控制专业功能是否可用,本机集合、环境、历史记录与配置不做任何改动;试用结束后个人非商业用途可继续使用,购买授权后专业功能立即恢复。

    +
    激活失败怎么排查?

    先在「设置 → 授权」看当前状态与到期时间。若提示无效或不匹配,请把授权码后四位、设备 ID 与报错截图一并发邮件,我们核对授权服务器上的记录后处理(常见原因是设备数已占满或授权码输漏字符)。

    +
    能开发票、走对公转账吗?

    可以。个人与企业均可开具发票,支持对公转账、多产品捆绑与批量授权;邮件说明抬头与税号即可。

    +
    三个产品(OpsxAPI / OpsxDB / OpsXShell)要分别买吗?

    三个产品独立授权、独立计价;它们共用同一套授权体系(同一个设备 ID 规则与同一个授权服务器)。多产品组合、团队或教育用途可邮件说明需求,给捆绑价。

    +
    +

    本页更新于 2026-10-16,授权口径与授权服务器 licence.rlwj.cn 实际配置一致。

    +
    +
    + +
    +
    +

    准备好购买授权

    +

    邮件说明产品、授权类型与设备数,当天回复并开票。

    + +

    付款后当场发放授权码(LIC-),粘贴到「设置 → 授权」即完成激活——整个流程无需注册账号。

    +
    +
    +
    + + + + diff --git a/homepage/pricing.html b/homepage/pricing.html new file mode 100644 index 0000000..a5c4e28 --- /dev/null +++ b/homepage/pricing.html @@ -0,0 +1,211 @@ + + + + + +授权与定价 · OpsxAPI + +"> + + + + + + +
    +
    + 30 天免费试用 · 首次启动自动开始 · 无需注册 +

    授权与定价

    +

    OpsxAPI 对个人非商业用途永久免费;在工作中商用需要授权。授权绑定设备而非账号,支持在线激活与完全离线激活两种方式。

    +
    + +
    +
    +

    先试用,再决定

    +

    下载即含 30 天全功能试用;激活支持在线与离线两种方式。

    +
    +

    30 天全功能试用

    首次启动自动开始;无需注册、无需信用卡。试用结束后个人非商业用途仍可继续使用。

    +

    在线授权 LIC-

    联网激活并绑定设备;支持自助解绑换机;激活后可连续离线使用 30 天。

    +

    离线许可证 LICOFF-

    Ed25519 自包含签名,绑定设备;完全无网络也能激活,适合隔离内网环境。

    +
    +
    +
    + +
    +
    +

    选择适合的授权

    +

    个人版可选年付或一次性买断;团队按席位,私有化不限席位。

    +
    +
    +
    专业版
    +
    +
    ¥99¥199/ 年
    +
    单人不分设备多少 · 最多 3 台设备同时激活
    +
      +
    • 六种协议全部调试能力
    • +
    • 集合 / 环境 / 历史无限量
    • +
    • 全格式导入导出 + 代码生成
    • +
    • 本机 Mock 服务
    • +
    • 设备可自助解绑迁移
    • +
    • 1 年免费升级(永久版含后续更新)
    • +
    + 购买咨询 +
    + +
    +
    私有化 · 不限席位
    +
    企业私有化版
    +
    ¥8,000/ 年 · 标价
    +
    或 ¥30,000 一次性买断 · 单法人不限席位
    +
      +
    • 整机 / 内网部署,数据不出企业
    • +
    • 团队版全部能力
    • +
    • 远程部署协助 + 专属支持
    • +
    • 定制开发通道(另行报价)
    • +
    • 买断可选:永久使用 + 1 年升级
    • +
    + 获取报价 +
    +
    +

    支付后当场发放授权码(LIC-),粘贴到「设置 → 授权」即完成激活——整个流程无需注册账号;许可证一机一码绑定设备,每年可免费换绑 2 次。
    标价透明,成交灵活:早鸟 / 教育 / 多年预付可议价,常见成交区间见 首页定价区。

    +
    +
    + +
    +
    +

    常见问题

    +
    +
    试用是怎么计算的?

    30 天全功能试用,从应用首次启动那一刻自动开始——无需注册账号,无需信用卡。试用期结束后,个人非商业用途可继续使用;在工作中的商业用途需要购买授权。

    +
    在线授权和离线激活有什么区别?

    在线授权码(LIC- 前缀):联网激活,绑定设备,支持在设置中自助解绑换机;激活后可连续离线使用最长 30 天。离线许可证(LICOFF- 前缀):同样绑定设备,但采用 Ed25519 自包含签名,在完全无网络的环境也能激活——适合隔离内网。两种方式在「设置 → 授权」中粘贴即用。

    +
    一个授权能用几台设备?

    授权绑定设备而非订阅账号。个人版最多 3 台设备;团队版按席位授权、组织内可转移。设备可以在「设置 → 授权」中解绑后迁移到新机器。

    +
    断网了授权会失效吗?

    离线许可证(LICOFF-)本地自验,永不联网也有效。在线授权(LIC-)容忍连续 30 天离线,超过后需联网重新验证一次。

    +
    退款政策是怎样的?

    自付款之日起 30 天内无理由全额退款——先用起来,不满意直接退。

    +
    怎么购买?

    邮件咨询购买,当天回复;支持对公转账与开票。团队与私有化支持按规模议价。

    +
    三个产品(OpsxAPI / OpsxDB / OpsXShell)要分别买吗?

    三个产品独立授权、独立计价。多产品组合或家庭/学生优惠可直接邮件说明需求,给捆绑价。

    +
    授权包含哪些升级?

    年付授权含 1 年免费升级;终身授权包含发布版本与后续一年内的全部更新。

    +
    +
    +
    + +
    +
    +

    独立开发,认真长跑。
    Opsx 系列工具由独立开发者打造,每一份授权的收入都直接投入产品迭代——没有投资人,也没有广告。

    +

    还没装?立即下载

    +
    +
    +
    + + + + \ No newline at end of file diff --git a/server/internal/site/terms.html b/homepage/terms.html similarity index 100% rename from server/internal/site/terms.html rename to homepage/terms.html diff --git a/scripts/build-homepage.sh b/scripts/build-homepage.sh new file mode 100755 index 0000000..ab6d788 --- /dev/null +++ b/scripts/build-homepage.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# 官网构建入口:从 homepage/ 源生成 homepage/dist/(纯静态,供 nginx 直接托管)。 +# 用法: scripts/build-homepage.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT/tools/homepage-build" +exec go run . -src "$ROOT/homepage" -out "$ROOT/homepage/dist" diff --git a/server/internal/docs/docs.go b/server/internal/docs/docs.go deleted file mode 100644 index e1611d8..0000000 --- a/server/internal/docs/docs.go +++ /dev/null @@ -1,264 +0,0 @@ -// Package docs 在 /docs 路径提供产品用户文档(Markdown 嵌入二进制,SSR 渲染)。 -package docs - -import ( - "embed" - "encoding/json" - "fmt" - "net/http" - "path" - "regexp" - "strings" - - "github.com/gin-gonic/gin" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - gmhtml "github.com/yuin/goldmark/renderer/html" -) - -//go:embed content assets template.html -var files embed.FS - -type PageDef struct { - File string `json:"file"` - Slug string `json:"slug"` - Title string `json:"title"` - Updated string `json:"updated,omitempty"` -} - -type GroupDef struct { - Name string `json:"name"` - Pages []PageDef `json:"pages"` -} - -type Manifest struct { - ProductVersion string `json:"productVersion"` - Groups []GroupDef `json:"groups"` -} - -// TocItem 本页目录项 -type TocItem struct { - ID string `json:"id"` - Level int `json:"level"` - Text string `json:"text"` -} - -type SearchEntry struct { - Slug string `json:"slug"` - Title string `json:"title"` - Group string `json:"group"` - Text string `json:"text"` -} - -type NavLink struct { - Slug string - Title string -} - -type Page struct { - Slug string - Title string - Group string - Updated string - HTML string - TOC []TocItem - Prev *NavLink - Next *NavLink -} - -// Handler 文档服务 -type Handler struct { - manifest Manifest - pages map[string]*Page - order []string - searchData []byte - tpl string -} - -func New() *Handler { - h := &Handler{pages: map[string]*Page{}} - raw, err := files.ReadFile("content/manifest.json") - if err != nil { - panic("docs: manifest.json 缺失: " + err.Error()) - } - if err := json.Unmarshal(raw, &h.manifest); err != nil { - panic("docs: manifest.json 解析失败: " + err.Error()) - } - tplRaw, err := files.ReadFile("template.html") - if err != nil { - panic("docs: template.html 缺失: " + err.Error()) - } - h.tpl = string(tplRaw) - - md := goldmark.New( - goldmark.WithExtensions(extension.Table, extension.Strikethrough, extension.TaskList), - goldmark.WithRendererOptions(gmhtml.WithHardWraps()), - ) - - search := []SearchEntry{} - var prev *NavLink - for _, group := range h.manifest.Groups { - for _, def := range group.Pages { - src, err := files.ReadFile("content/" + def.File) - if err != nil { - panic("docs: 缺少内容文件 " + def.File) - } - var buf strings.Builder - if err := md.Convert(src, &buf); err != nil { - panic("docs: 渲染失败 " + def.File + ": " + err.Error()) - } - body, toc := injectHeadingIDs(buf.String()) - page := &Page{ - Slug: def.Slug, - Title: def.Title, - Group: group.Name, - Updated: def.Updated, - HTML: body, - TOC: toc, - Prev: prev, - } - h.pages[def.Slug] = page - h.order = append(h.order, def.Slug) - prev = &NavLink{Slug: def.Slug, Title: def.Title} - search = append(search, SearchEntry{ - Slug: def.Slug, - Title: def.Title, - Group: group.Name, - Text: plainText(string(src)), - }) - } - } - // 下一页链接 - for i, slug := range h.order { - if i+1 < len(h.order) { - next := h.pages[h.order[i+1]] - h.pages[slug].Next = &NavLink{Slug: next.Slug, Title: next.Title} - } - } - searchJSON, _ := json.Marshal(search) - h.searchData = searchJSON - return h -} - -// Register 注册路由(单一 catch-all,避免静态/参数段冲突) -func (h *Handler) Register(r *gin.Engine) { - r.GET("/docs", func(c *gin.Context) { - c.Redirect(http.StatusFound, "/docs/"+h.order[0]) - }) - r.GET("/docs/*path", h.dispatch) -} - -func (h *Handler) dispatch(c *gin.Context) { - p := strings.Trim(c.Param("path"), "/") - switch { - case p == "": - c.Redirect(http.StatusFound, "/docs/"+h.order[0]) - case p == "search-data.json": - h.SearchData(c) - case strings.HasPrefix(p, "assets/"): - c.Params = []gin.Param{{Key: "file", Value: "/" + strings.TrimPrefix(p, "assets")}} - h.Asset(c) - default: - // 取第一段作为 slug(忽略子路径) - slug := p - if i := strings.IndexByte(p, '/'); i >= 0 { - slug = p[:i] - } - c.Params = []gin.Param{{Key: "slug", Value: slug}} - h.Page(c) - } -} - -// Page 渲染文档页 -func (h *Handler) Page(c *gin.Context) { - slug := c.Param("slug") - page, ok := h.pages[slug] - if !ok { - c.Status(http.StatusNotFound) - _, _ = c.Writer.WriteString(notFoundHTML(h.order[0])) //nolint:errcheck - return - } - c.Header("Content-Type", "text/html; charset=utf-8") - c.Header("Cache-Control", "no-cache") - _, _ = c.Writer.WriteString(renderPage(h, page)) -} - -// SearchData 搜索索引 -func (h *Handler) SearchData(c *gin.Context) { - c.Data(http.StatusOK, "application/json; charset=utf-8", h.searchData) -} - -// Asset 静态资源 -func (h *Handler) Asset(c *gin.Context) { - name := path.Clean(c.Param("file")) - if strings.Contains(name, "..") { - c.Status(http.StatusBadRequest) - return - } - data, err := files.ReadFile("assets" + name) - if err != nil { - c.Status(http.StatusNotFound) - return - } - switch { - case strings.HasSuffix(name, ".css"): - c.Header("Content-Type", "text/css; charset=utf-8") - case strings.HasSuffix(name, ".js"): - c.Header("Content-Type", "application/javascript; charset=utf-8") - case strings.HasSuffix(name, ".svg"): - c.Header("Content-Type", "image/svg+xml") - default: - c.Header("Content-Type", "application/octet-stream") - } - c.Header("Cache-Control", "public, max-age=3600") - c.Data(http.StatusOK, "", data) -} - -// ManifestExport 供测试使用 -func (h *Handler) ManifestExport() Manifest { return h.manifest } - -func (h *Handler) FirstSlug() string { return h.order[0] } - -var ( - hRe = regexp.MustCompile(`(?s)<(h[23])>(.*?)`) - tagRe = regexp.MustCompile(`<[^>]+>`) -) - -// injectHeadingIDs 给 h2/h3 加锚点 id,并产出目录 -func injectHeadingIDs(html string) (string, []TocItem) { - var toc []TocItem - counter := 0 - out := hRe.ReplaceAllStringFunc(html, func(m string) string { - sub := hRe.FindStringSubmatch(m) - tag, inner := sub[1], sub[2] - counter++ - id := fmt.Sprintf("sec-%d", counter) - text := strings.TrimSpace(tagRe.ReplaceAllString(inner, "")) - level := 2 - if tag == "h3" { - level = 3 - } - toc = append(toc, TocItem{ID: id, Level: level, Text: text}) - return fmt.Sprintf(`<%s id="%s">%s`, tag, id, inner, tag) - }) - return out, toc -} - -var mdNoise = regexp.MustCompile("(?s)```.*?```|`[^`]*`|!\\[[^\\]]*\\]\\([^)]*\\)|\\[[^\\]]*\\]\\([^)]*\\)|[#>*_\\-|]+") - -// plainText 供搜索索引的纯文本 -func plainText(src string) string { - s := mdNoise.ReplaceAllString(src, " ") - s = strings.Join(strings.Fields(s), " ") - if len(s) > 6000 { - s = s[:6000] - } - return s -} - -func notFoundHTML(home string) string { - return fmt.Sprintf(`404 · OpsxAPI 文档 - -

    404

    文档页面不存在或已被移动。

    返回文档首页
    -`, home) -} diff --git a/server/internal/docs/docs_test.go b/server/internal/docs/docs_test.go deleted file mode 100644 index 1bd41ac..0000000 --- a/server/internal/docs/docs_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package docs - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gin-gonic/gin" -) - -func newTestRouter(t *testing.T) *gin.Engine { - t.Helper() - gin.SetMode(gin.TestMode) - r := gin.New() - New().Register(r) - return r -} - -func TestDocsIndexRedirect(t *testing.T) { - r := newTestRouter(t) - w := httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs", nil)) - if w.Code != http.StatusFound { - t.Fatalf("expect 302, got %d", w.Code) - } - if !strings.HasPrefix(w.Header().Get("Location"), "/docs/") { - t.Fatalf("bad redirect: %s", w.Header().Get("Location")) - } -} - -func TestDocsAllPagesRender(t *testing.T) { - h := New() - r := newTestRouter(t) - for _, slug := range h.order { - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/docs/"+slug, nil) - r.ServeHTTP(w, req) - if w.Code != http.StatusOK { - t.Fatalf("page %s: expect 200, got %d", slug, w.Code) - } - body := w.Body.String() - if !strings.Contains(body, "") || !strings.Contains(body, `class="md"`) { - t.Fatalf("page %s: html structure incomplete", slug) - } - // 正文有渲染出的标题与段落 - if !strings.Contains(body, "<h1") { - t.Fatalf("page %s: missing h1", slug) - } - // 侧栏包含全部分组 - for _, g := range h.manifest.Groups { - if !strings.Contains(body, g.Name) { - t.Fatalf("page %s: sidebar missing group %s", slug, g.Name) - } - } - } -} - -func TestDocsPageContainsTOCAndNav(t *testing.T) { - r := newTestRouter(t) - w := httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/http", nil)) - body := w.Body.String() - if !strings.Contains(body, "sec-1") { - t.Fatal("expect heading anchors") - } - if !strings.Contains(body, "上一页") { - t.Fatal("expect prev link") - } -} - -func TestDocsSearchData(t *testing.T) { - r := newTestRouter(t) - w := httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/search-data.json", nil)) - if w.Code != http.StatusOK { - t.Fatalf("expect 200, got %d", w.Code) - } - var entries []SearchEntry - if err := json.Unmarshal(w.Body.Bytes(), &entries); err != nil { - t.Fatalf("invalid json: %v", err) - } - if len(entries) < 10 { - t.Fatalf("expect >= 10 search entries, got %d", len(entries)) - } - // MQTT 页应可被检索(标题或正文命中) - found := false - for _, e := range entries { - if e.Slug == "mqtt" && (e.Title == "MQTT" || strings.Contains(e.Text, "MQTT")) { - found = true - } - } - if !found { - t.Fatal("mqtt entry missing searchable text") - } -} - -func TestDocsAssetsAnd404(t *testing.T) { - r := newTestRouter(t) - - w := httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/assets/docs.css", nil)) - if w.Code != http.StatusOK || !strings.Contains(w.Header().Get("Content-Type"), "text/css") { - t.Fatalf("css asset: %d %s", w.Code, w.Header().Get("Content-Type")) - } - - w = httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/assets/docs.js", nil)) - if w.Code != http.StatusOK { - t.Fatalf("js asset: %d", w.Code) - } - - w = httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/not-exist-page", nil)) - if w.Code != http.StatusNotFound { - t.Fatalf("404 page: %d", w.Code) - } - if !strings.Contains(w.Body.String(), "返回文档首页") { - t.Fatal("404 page missing home link") - } -} diff --git a/server/internal/docs/render.go b/server/internal/docs/render.go deleted file mode 100644 index 9f464ab..0000000 --- a/server/internal/docs/render.go +++ /dev/null @@ -1,25 +0,0 @@ -package docs - -import ( - "html/template" - "strings" -) - -func renderPage(h *Handler, p *Page) string { - tpl, err := template.New("doc").Parse(h.tpl) - if err != nil { - return "<!doctype html><meta charset=utf-8><title>errortemplate error: " + template.HTMLEscapeString(err.Error()) - } - - var sb strings.Builder - data := map[string]any{ - "Manifest": h.manifest, - "Page": p, - "TOC": p.TOC, - "Content": template.HTML(p.HTML), //nolint:gosec —— 内容为内置受信 Markdown - } - if err := tpl.Execute(&sb, data); err != nil { - return "errorrender error: " + template.HTMLEscapeString(err.Error()) - } - return sb.String() -} diff --git a/server/internal/server/router.go b/server/internal/server/router.go index 85d8283..d31451e 100644 --- a/server/internal/server/router.go +++ b/server/internal/server/router.go @@ -1,6 +1,7 @@ package server import ( + "log" "net" "net/http" "os" @@ -9,11 +10,9 @@ import ( "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 { @@ -43,15 +42,16 @@ func NewRouter() *gin.Engine { mocksrv.New(accountAPI.Store()).Register(r) echo.Mount(r) + // 官网/文档/定价已拆分独立静态站(源在 homepage/,构建见 scripts/build-homepage.sh), + // 本地仅静态托管 homepage/dist 预览;不再内置 embed 兜底,与后端彻底解耦。 if !mountHomepage(r) { - docs.New().Register(r) - site.New().Register(r) + log.Printf("[warn] homepage/dist 未找到,官网/文档/定价未挂载;请先运行 scripts/build-homepage.sh 生成产物") } return r } -// mountHomepage 本地预览 homepage/dist(落地页 + 文档)。不改 homepage/ 源。 +// mountHomepage 本地预览 homepage/dist(落地页 + 文档 + 定价)。只读产物,不碰 homepage/ 源。 func mountHomepage(r *gin.Engine) bool { for _, dir := range []string{ os.Getenv("OPSXAPI_HOMEPAGE"), diff --git a/server/internal/site/site.go b/server/internal/site/site.go deleted file mode 100644 index 0927238..0000000 --- a/server/internal/site/site.go +++ /dev/null @@ -1,80 +0,0 @@ -// Package site 在 /home(及 /)提供产品官网单页(SSR,资源嵌入二进制)。 -package site - -import ( - "embed" - "net/http" - "strings" - - "github.com/gin-gonic/gin" -) - -//go:embed template.html terms.html assets -var files embed.FS - -// Handler 官网服务 -type Handler struct { - html string - terms string -} - -func New() *Handler { - raw, err := files.ReadFile("template.html") - if err != nil { - panic("site: template.html 缺失: " + err.Error()) - } - terms, err := files.ReadFile("terms.html") - if err != nil { - panic("site: terms.html 缺失: " + err.Error()) - } - return &Handler{html: string(raw), terms: string(terms)} -} - -// Register 注册路由:/ 与 /home 均渲染官网 -func (h *Handler) Register(r *gin.Engine) { - r.GET("/", h.Page) - r.GET("/home", h.Page) - r.GET("/index.html", h.Page) - r.GET("/site/assets/*file", h.Asset) - r.GET("/terms", h.Terms) -} - -// Terms 渲染服务条款 -func (h *Handler) Terms(c *gin.Context) { - c.Header("Content-Type", "text/html; charset=utf-8") - c.Header("Cache-Control", "no-cache") - _, _ = c.Writer.WriteString(h.terms) -} - -// Page 渲染官网 -func (h *Handler) Page(c *gin.Context) { - c.Header("Content-Type", "text/html; charset=utf-8") - c.Header("Cache-Control", "no-cache") - _, _ = c.Writer.WriteString(h.html) -} - -// Asset 静态资源 -func (h *Handler) Asset(c *gin.Context) { - name := strings.TrimPrefix(c.Param("file"), "/") - if strings.Contains(name, "..") { - c.Status(http.StatusBadRequest) - return - } - data, err := files.ReadFile("assets/" + name) - if err != nil { - c.Status(http.StatusNotFound) - return - } - switch { - case strings.HasSuffix(name, ".css"): - c.Header("Content-Type", "text/css; charset=utf-8") - case strings.HasSuffix(name, ".js"): - c.Header("Content-Type", "application/javascript; charset=utf-8") - case strings.HasSuffix(name, ".svg"): - c.Header("Content-Type", "image/svg+xml") - default: - c.Header("Content-Type", "application/octet-stream") - } - c.Header("Cache-Control", "public, max-age=3600") - c.Data(http.StatusOK, "", data) -} diff --git a/server/internal/site/site_test.go b/server/internal/site/site_test.go deleted file mode 100644 index f9e0272..0000000 --- a/server/internal/site/site_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package site - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gin-gonic/gin" -) - -func newTestRouter(t *testing.T) *gin.Engine { - t.Helper() - gin.SetMode(gin.TestMode) - r := gin.New() - New().Register(r) - return r -} - -func TestSitePageRender(t *testing.T) { - r := newTestRouter(t) - for _, path := range []string{"/", "/home", "/index.html"} { - w := httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil)) - if w.Code != http.StatusOK { - t.Fatalf("%s: expect 200, got %d", path, w.Code) - } - body := w.Body.String() - for _, want := range []string{ - "OpsxAPI", // 标题 - `id="features"`, // 功能区 - `id="pricing"`, // 定价区 - "¥99", // 入门价 - "¥8,000", // 企业档标价 - "¥30,000", // 买断标价 - "¥5,000–8,000", // 成交区间 - "永久买断", // 买断 FAQ - "早鸟创始价", // 让利策略 - "Socket.IO", // 协议覆盖 - "hello@opsxapi.dev", // 联系方式 - `href="/docs"`, // 文档互链 - `href="/site/assets/site.css"`, // 样式资源 - } { - if !strings.Contains(body, want) { - t.Fatalf("%s: missing %q", path, want) - } - } - } -} - -func TestSiteAsset(t *testing.T) { - r := newTestRouter(t) - w := httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/site/assets/site.css", nil)) - if w.Code != http.StatusOK || !strings.Contains(w.Header().Get("Content-Type"), "text/css") { - t.Fatalf("css: %d %s", w.Code, w.Header().Get("Content-Type")) - } - w = httptest.NewRecorder() - r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/site/assets/../template.html", nil)) - if w.Code != http.StatusBadRequest && w.Code != http.StatusNotFound { - t.Fatalf("path traversal should be rejected, got %d", w.Code) - } -} diff --git a/tools/homepage-build/go.mod b/tools/homepage-build/go.mod new file mode 100644 index 0000000..73835c9 --- /dev/null +++ b/tools/homepage-build/go.mod @@ -0,0 +1,5 @@ +module opsxapi/tools/homepage-build + +go 1.25.0 + +require github.com/yuin/goldmark v1.8.6 diff --git a/tools/homepage-build/go.sum b/tools/homepage-build/go.sum new file mode 100644 index 0000000..816585d --- /dev/null +++ b/tools/homepage-build/go.sum @@ -0,0 +1,2 @@ +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= diff --git a/tools/homepage-build/main.go b/tools/homepage-build/main.go new file mode 100644 index 0000000..45f50ef --- /dev/null +++ b/tools/homepage-build/main.go @@ -0,0 +1,190 @@ +// homepage-build:把 homepage/ 源(落地页 + 定价页 + 服务条款 + 文档 md)构建为纯静态站点 homepage/dist/。 +// 产物可直接部署到任意静态托管(nginx/Pages)——官网与后端二进制彻底解耦。 +// 用法: go run . [-src homepage] [-out homepage/dist] +package main + +import ( + "encoding/json" + "flag" + "fmt" + "html/template" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + gmhtml "github.com/yuin/goldmark/renderer/html" +) + +type PageDef struct { + File string `json:"file"` + Slug string `json:"slug"` + Title string `json:"title"` + Updated string `json:"updated,omitempty"` +} +type GroupDef struct { + Name string `json:"name"` + Pages []PageDef `json:"pages"` +} +type Manifest struct { + ProductVersion string `json:"productVersion"` + Groups []GroupDef `json:"groups"` +} +type TocItem struct { + ID string + Level int + Text string +} +type SearchEntry struct { + Slug, Title, Group, Text string +} +type NavLink struct { + Slug, Title string +} +type Page struct { + Slug, Title, Group, Updated string + HTML string + TOC []TocItem + Prev, Next *NavLink +} + +var ( + hRe = regexp.MustCompile(`(?s)<(h[23])>(.*?)</h[23]>`) + tagRe = regexp.MustCompile(`<[^>]+>`) + mdNoise = regexp.MustCompile("(?s)```.*?```|`[^`]*`|!\\[[^\\]]*\\]\\([^)]*\\)|\\[[^\\]]*\\]\\([^)]*\\)|[#>*_\\-|]+") +) + +func injectHeadingIDs(html string) (string, []TocItem) { + var toc []TocItem + counter := 0 + out := hRe.ReplaceAllStringFunc(html, func(m string) string { + sub := hRe.FindStringSubmatch(m) + tag, inner := sub[1], sub[2] + counter++ + id := fmt.Sprintf("sec-%d", counter) + text := strings.TrimSpace(tagRe.ReplaceAllString(inner, "")) + level := 2 + if tag == "h3" { + level = 3 + } + toc = append(toc, TocItem{ID: id, Level: level, Text: text}) + return fmt.Sprintf(`<%s id="%s">%s</%s>`, tag, id, inner, tag) + }) + return out, toc +} + +func plainText(src string) string { + s := mdNoise.ReplaceAllString(src, " ") + s = strings.Join(strings.Fields(s), " ") + if len(s) > 6000 { + s = s[:6000] + } + return s +} + +func must(err error, what string) { + if err != nil { + fmt.Fprintf(os.Stderr, "[homepage-build] %s: %v\n", what, err) + os.Exit(1) + } +} + +func writeFile(path, content string) { + must(os.MkdirAll(filepath.Dir(path), 0o755), "mkdir "+path) + must(os.WriteFile(path, []byte(content), 0o644), "write "+path) + fmt.Println("[OK]", path) +} + +func main() { + src := flag.String("src", "homepage", "源目录") + out := flag.String("out", "homepage/dist", "产物目录") + flag.Parse() + + // 幂等重建:清理上一轮生成的单页/资源目录(os.CopyFS 不会覆盖已存在的文件) + for _, d := range []string{"site", "docs", "pricing", "terms", "license"} { + must(os.RemoveAll(filepath.Join(*out, d)), "清理 "+d) + } + + // 1) 落地页 + 附加单页:index.html 必需;pricing/terms/license 源存在即产出。 + // 附加页同时产出 <name>.html 与 <name>/index.html:兼容 nginx 的干净 + // URL(location = /license) 与纯静态服务器(python -m http.server)。 // Logan.Li + for _, page := range []string{"index.html", "pricing.html", "terms.html", "license.html"} { + raw, err := os.ReadFile(filepath.Join(*src, page)) + if err != nil { + if page == "index.html" { + must(err, "读 index.html") + } + continue + } + writeFile(filepath.Join(*out, page), string(raw)) + if page != "index.html" { + writeFile(filepath.Join(*out, strings.TrimSuffix(page, ".html"), "index.html"), string(raw)) + } + } + siteAssets := filepath.Join(*src, "assets") + if st, _ := os.Stat(siteAssets); st != nil { + // 落地页模板引用 /site/assets/*,故产物固定落在 dist/site/assets/ + must(os.CopyFS(filepath.Join(*out, "site", "assets"), os.DirFS(siteAssets)), "拷 site assets") + fmt.Println("[OK]", filepath.Join(*out, "site", "assets")) + } + + // 2) 文档: manifest → goldmark 渲染 → docs.html 模板 + mraw, err := os.ReadFile(filepath.Join(*src, "content", "manifest.json")) + must(err, "读 manifest") + var manifest Manifest + must(json.Unmarshal(mraw, &manifest), "解析 manifest") + tplRaw, err := os.ReadFile(filepath.Join(*src, "docs.html")) + must(err, "读 docs.html") + tpl, err := template.New("doc").Parse(string(tplRaw)) + must(err, "解析 docs 模板") + md := goldmark.New(goldmark.WithExtensions(extension.Table, extension.Strikethrough, extension.TaskList), goldmark.WithRendererOptions(gmhtml.WithHardWraps())) + + var order []string + pages := map[string]*Page{} + var search []SearchEntry + var prev *NavLink + for _, g := range manifest.Groups { + for _, def := range g.Pages { + srcb, err := os.ReadFile(filepath.Join(*src, "content", def.File)) + must(err, "读文档 "+def.File) + var buf strings.Builder + must(md.Convert(srcb, &buf), "渲染 "+def.File) + body, toc := injectHeadingIDs(buf.String()) + p := &Page{Slug: def.Slug, Title: def.Title, Group: g.Name, Updated: def.Updated, HTML: body, TOC: toc, Prev: prev} + pages[def.Slug] = p + order = append(order, def.Slug) + prev = &NavLink{Slug: def.Slug, Title: def.Title} + search = append(search, SearchEntry{Slug: def.Slug, Title: def.Title, Group: g.Name, Text: plainText(string(srcb))}) + } + } + for i, slug := range order { + if i+1 < len(order) { + n := pages[order[i+1]] + pages[slug].Next = &NavLink{Slug: n.Slug, Title: n.Title} + } + } + + render := func(p *Page) string { + var sb strings.Builder + must(tpl.Execute(&sb, map[string]any{"Manifest": manifest, "Page": p, "TOC": p.TOC, "Content": template.HTML(p.HTML)}), "渲染模板 "+p.Slug) + return sb.String() + } + // /docs/ 首页 = 第一篇(静态站无重定向,直接渲染) + writeFile(filepath.Join(*out, "docs", "index.html"), render(pages[order[0]])) + for _, slug := range order { + writeFile(filepath.Join(*out, "docs", slug, "index.html"), render(pages[slug])) + } + sj, _ := json.Marshal(search) + writeFile(filepath.Join(*out, "docs", "search-data.json"), string(sj)) + + // docs assets(模板引用 /docs/assets/*) + da := filepath.Join(*src, "docs-assets") + if st, _ := os.Stat(da); st != nil { + must(os.CopyFS(filepath.Join(*out, "docs", "assets"), os.DirFS(da)), "拷 docs assets") + fmt.Println("[OK]", filepath.Join(*out, "docs", "assets")) + } + + fmt.Printf("[DONE] 落地页 + %d 页文档 → %s(纯静态,可部署任意域名)\n", len(order), *out) +}