桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)

- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
This commit is contained in:
logan committed 2026-10-16 03:03:36 +08:00
1 parent 6452026fd6
commit 2bb4ef7a95
299 files changed
+6059 -3322

No files matched your search

+387
View File
@@ -0,0 +1,387 @@
/**
* 节点 / 集合 / 环境的默认工厂。
*/
import { normalizeMockSettings } from "./mock";
import type {
AuthConfig,
Collection,
CollectionNode,
Environment,
FolderNode,
GraphqlRequestData,
HttpRequestData,
KVRow,
MqttRequestData,
RequestKind,
RequestNode,
RequestDataOf,
SocketIoRequestData,
SseRequestData,
WebsocketRequestData,
WorkspaceData,
TestCase,
TestStep,
} from "./model";
import { newId } from "./tree";
export function kv(
key = "",
value = "",
enabled = true,
description?: string,
): KVRow {
return { id: newId(), key, value, enabled, description };
}
export const noAuth: AuthConfig = { mode: "none" };
export function defaultRequestData(kind: RequestKind) {
switch (kind) {
case "http":
return {
kind,
method: "GET",
url: "/",
query: [],
headers: [],
cookies: [],
auth: { ...noAuth },
bodyMode: "none",
bodyRaw: "",
bodyForm: [],
} satisfies HttpRequestData;
case "graphql":
return {
kind,
url: "/graphql",
headers: [kv("Content-Type", "application/json")],
auth: { ...noAuth },
query: "query {\n \n}",
variables: "{}",
} satisfies GraphqlRequestData;
case "websocket":
return {
kind,
url: "wss://echo.websocket.org",
headers: [],
protocols: "",
} satisfies WebsocketRequestData;
case "sse":
return {
kind,
url: "/events",
headers: [kv("Accept", "text/event-stream")],
query: [],
auth: { ...noAuth },
lastEventId: "",
dataOnly: true,
} satisfies SseRequestData;
case "socketio":
return {
kind,
url: "http://127.0.0.1:3000",
options: '{\n "path": "/socket.io",\n "transports": ["websocket"]\n}',
listenEvents: ["message"],
sendEvent: "message",
sendArgs: '[\n "hello"\n]',
} satisfies SocketIoRequestData;
case "mqtt":
return {
kind,
brokerUrl: "wss://broker.emqx.io:8084/mqtt",
username: "",
password: "",
clientId: `opsxapi-${Math.random().toString(16).slice(2, 10)}`,
subscribeTopics: "opsxapi/demo",
publishTopic: "opsxapi/demo",
publishPayload: '{\n "hello": "opsxapi"\n}',
qos: 0,
} satisfies MqttRequestData;
}
}
export function createRequest<K extends RequestKind>(
kind: K,
name?: string,
partial?: { data?: Partial<RequestDataOf<K>>; description?: string },
): Extract<RequestNode, { kind: K }> {
const data = {
...defaultRequestData(kind),
...(partial?.data ?? {}),
kind,
} as RequestDataOf<K>;
const base = {
id: newId(),
type: "request" as const,
name: name ?? defaultName(kind),
description: partial?.description,
createdAt: Date.now(),
updatedAt: Date.now(),
};
return { ...base, kind, data } as unknown as Extract<RequestNode, { kind: K }>;
}
export function defaultName(kind: RequestKind): string {
switch (kind) {
case "http":
return "新建 HTTP 接口";
case "graphql":
return "新建 GraphQL 查询";
case "websocket":
return "新建 WebSocket";
case "sse":
return "新建 SSE";
case "socketio":
return "新建 Socket.IO";
case "mqtt":
return "新建 MQTT";
}
}
export function createFolder(name = "新建目录", children: CollectionNode[] = []): FolderNode {
return {
id: newId(),
type: "folder",
name,
children,
createdAt: Date.now(),
updatedAt: Date.now(),
};
}
export function createCollection(
name: string,
children: CollectionNode[] = [],
): Collection {
return {
id: newId(),
type: "collection",
name,
children,
createdAt: Date.now(),
updatedAt: Date.now(),
};
}
export function createEnvironment(
name: string,
vars: { key: string; value: string }[] = [],
baseUrl = "",
): Environment {
return { id: newId(), name, baseUrl, variables: vars.map((v) => kv(v.key, v.value)) };
}
/** 首次启动的演示集合:覆盖全部协议,开箱即用 */
export function createSeedWorkspace(): WorkspaceData {
const http = (
name: string,
method: HttpRequestData["method"],
url: string,
extra?: Partial<HttpRequestData>,
description?: string,
): RequestNode =>
createRequest("http", name, {
data: { method, url, ...extra },
description,
});
const localBase = "http://127.0.0.1:8787/v1/echo";
// 固定引用,供种子场景用例 steps.nodeId 绑定
const echoGet = http(
"回归 · GET Query",
"GET",
`${localBase}/get?ping=opsx`,
{ query: [kv("ping", "opsx")] },
"本地回显:Query 参数。需 Gin :8787。",
);
const echoJson = http(
"回归 · POST JSON",
"POST",
`${localBase}/json`,
{
bodyMode: "json",
bodyRaw: '{\n "name": "opsxapi",\n "lang": "zh-CN"\n}',
headers: [kv("Content-Type", "application/json")],
},
);
const echoForm = http(
"回归 · POST urlencoded",
"POST",
`${localBase}/form`,
{
bodyMode: "urlencoded",
bodyForm: [kv("username", "logan"), kv("role", "ops")],
},
);
const echoMultipart = http(
"回归 · POST form-data",
"POST",
`${localBase}/multipart`,
{
bodyMode: "form-data",
bodyForm: [kv("fileMeta", "demo"), kv("env", "local")],
},
);
const echoHeaders = http(
"回归 · 自定义 Header",
"GET",
`${localBase}/headers`,
{ headers: [kv("X-Opsx-Probe", "ok")] },
);
const echoCookies = http(
"回归 · Cookie",
"GET",
`${localBase}/cookies`,
{ headers: [kv("X-Opsx-Cookie", "opsx_session=demo-cookie")] },
);
const echoBearer = http(
"回归 · Bearer",
"GET",
`${localBase}/bearer`,
{ auth: { mode: "bearer", bearer: "{{token}}" } },
);
const echoBasic = http(
"回归 · Basic Auth",
"GET",
`${localBase}/basic`,
{ auth: { mode: "basic", basicUser: "opsx", basicPass: "demo" } },
);
const demo = createCollection("示例集合", [
createFolder("HTTP · 本地回归(P0)", [
echoGet,
echoJson,
echoForm,
echoMultipart,
echoHeaders,
echoCookies,
echoBearer,
echoBasic,
]),
createFolder("HTTP · httpbin", [
http("获取请求回显", "GET", "https://httpbin.org/get", undefined, "经典回显接口,返回请求头 / IP / 参数。"),
http(
"提交 JSON",
"POST",
"https://httpbin.org/post",
{
bodyMode: "json",
bodyRaw: '{\n "name": "opsxapi",\n "lang": "zh-CN"\n}',
headers: [kv("Content-Type", "application/json")],
},
"POST 一个 JSON 体,观察 Pretty 视图。",
),
http(
"表单提交",
"POST",
"https://httpbin.org/post",
{
bodyMode: "urlencoded",
bodyForm: [kv("username", "logan"), kv("role", "ops")],
},
),
http(
"Bearer 鉴权示例",
"GET",
"https://httpbin.org/bearer",
{
auth: { mode: "bearer", bearer: "{{token}}" },
},
"在 Auth 标签填 Token;环境变量 token 默认 demo-token。",
),
http("状态码 · 404", "GET", "https://httpbin.org/status/404"),
]),
createFolder("GraphQL", [
createRequest("graphql", "国家查询 · countries", {
data: {
url: "https://countries.trevorblades.com/graphql",
query:
"query ($code: ID!) {\n country(code: $code) {\n name\n capital\n currency\n }\n}",
variables: '{\n "code": "CN"\n}',
},
description: "公共 GraphQL 服务,无需鉴权。",
}),
]),
createFolder("实时协议", [
createRequest("websocket", "WebSocket · echo", {
data: { url: "wss://echo.websocket.org" },
description: "发送什么就回什么。",
}),
createRequest("sse", "SSE · 维基媒体变更流", {
data: {
url: "https://stream.wikimedia.org/v2/stream/recentchange",
headers: [kv("Accept", "text/event-stream")],
},
description: "维基百科实时编辑事件流。",
}),
createRequest("mqtt", "MQTT · EMQX 公共 Broker", {
data: {
subscribeTopics: "opsxapi/demo",
publishTopic: "opsxapi/demo",
},
description: "wss 公共 broker;两个窗口同时打开可互相收发。",
}),
createRequest("socketio", "Socket.IO · 本地服务", {
description: "连你的本地 socket.io 服务,或改 URL 到任意 demo 站点。",
}),
]),
]);
const step = (nodeId: string, assertions: TestStep["assertions"], extracts: TestStep["extracts"] = []): TestStep => ({
id: newId(),
enabled: true,
nodeId,
assertions,
extracts,
});
const as = (
target: "status" | "timeMs" | "header" | "body",
op: TestStep["assertions"][number]["op"],
expected: string,
locator = "",
) => ({
id: newId(),
enabled: true,
target,
locator,
op,
expected,
});
const regressionCase: TestCase = {
id: "tc-opsx-http8-regression",
name: "HTTP 八组合回归(本地 echo)",
description: "对 Gin /v1/echo 的 8 种请求组合:Query / JSON / urlencoded / form-data / Header / Cookie / Bearer / Basic。请先 ./run.sh web 并切换环境到「本地」。",
continueOnFailure: true,
steps: [
step(echoGet.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "true", "$.ok"), as("body", "jsonpath_eq", "opsx", "$.query.ping")]),
step(echoJson.id, [as("status", "eq", "200"), as("body", "contains", "opsxapi")]),
step(echoForm.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "logan", "$.form.username")]),
step(echoMultipart.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "demo", "$.form.fileMeta")]),
step(echoHeaders.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "ok", "$.xOpsxProbe")]),
step(echoCookies.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "demo-cookie", "$.cookies.opsx_session")]),
step(echoBearer.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "true", "$.authenticated")]),
step(echoBasic.id, [as("status", "eq", "200"), as("body", "jsonpath_eq", "opsx", "$.user")]),
],
createdAt: Date.now(),
updatedAt: Date.now(),
};
const envs = [
createEnvironment("正式", [{ key: "token", value: "demo-token" }], "https://httpbin.org"),
createEnvironment("本地", [{ key: "token", value: "demo-token" }], "http://127.0.0.1:8787"),
];
return {
version: 2,
collections: [demo],
environments: envs,
activeEnvironmentId: envs[1]?.id ?? envs[0]?.id ?? null,
history: [],
settings: { theme: "dark", timeoutMs: 30000, mock: normalizeMockSettings(null), ai: { providers: [], activeProviderId: null } },
testCases: [regressionCase],
testRuns: [],
schedules: [],
};
}