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

388 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 节点 / 集合 / 环境的默认工厂。
*/
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: [],
};
}