桌面 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:
1 parent
6452026fd6
commit
2bb4ef7a95
299 files changed
+6059
-3322
No files matched your search
@@ -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: [],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user