- 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT
- 集合树/多标签/多环境变量/历史记录,localStorage 持久化
- 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、
Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖)
- 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、
全量备份;单请求生成 cURL/fetch/Python/Go 代码
- Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、
WebSocket 隧道(浏览器自定义握手 Header)
- 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付)
- lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题
- 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全
696 lines
20 KiB
TypeScript
696 lines
20 KiB
TypeScript
/**
|
||
* 工作台主 store:集合 / 环境 / 标签页 / 历史 / 设置 / 运行时状态。
|
||
* 持久化:localStorage(debounce),实时会话等运行时数据不落盘。
|
||
*/
|
||
import type {
|
||
Collection,
|
||
CollectionNode,
|
||
Environment,
|
||
FolderNode,
|
||
HistoryEntry,
|
||
HttpRequestData,
|
||
RequestKind,
|
||
RequestNode,
|
||
ThemeMode,
|
||
WorkspaceData,
|
||
WorkspaceSettings,
|
||
} from "@opsxapi/core";
|
||
import {
|
||
createRequest,
|
||
createSeedWorkspace,
|
||
defaultRequestData,
|
||
deepCopyNode,
|
||
findNode,
|
||
insertIntoCollection,
|
||
kv,
|
||
moveNode,
|
||
newId,
|
||
uniqueName,
|
||
} from "@opsxapi/core";
|
||
import { create } from "zustand";
|
||
|
||
const STORAGE_KEY = "opsxapi.workspace.v2";
|
||
|
||
export type LiveMessage = {
|
||
id: string;
|
||
dir: "in" | "out" | "system" | "error";
|
||
at: number;
|
||
text: string;
|
||
/** mqtt topic / socketio event / sse event */
|
||
topic?: string;
|
||
};
|
||
|
||
export type RequestRuntime = {
|
||
loading: boolean;
|
||
error: string | null;
|
||
/** http/graphql 响应 */
|
||
result: {
|
||
status: number;
|
||
headers: Record<string, string>;
|
||
body: string;
|
||
timeMs: number;
|
||
size: number;
|
||
} | null;
|
||
responseTab: "body" | "headers" | "raw";
|
||
/** 实时协议状态 */
|
||
status: string;
|
||
messages: LiveMessage[];
|
||
sent: number;
|
||
received: number;
|
||
startedAt: number | null;
|
||
};
|
||
|
||
export type Dialog =
|
||
| { type: "new"; collectionId: string; parentId: string | null }
|
||
| { type: "import" }
|
||
| { type: "export"; collectionId: string }
|
||
| { type: "environments" }
|
||
| { type: "settings" }
|
||
| { type: "rename"; id: string }
|
||
| { type: "confirm"; title: string; message: string; action: () => void };
|
||
|
||
export function emptyRuntime(): RequestRuntime {
|
||
return {
|
||
loading: false,
|
||
error: null,
|
||
result: null,
|
||
responseTab: "body",
|
||
status: "closed",
|
||
messages: [],
|
||
sent: 0,
|
||
received: 0,
|
||
startedAt: null,
|
||
};
|
||
}
|
||
|
||
type PersistedShape = Pick<
|
||
WorkspaceData,
|
||
"collections" | "environments" | "activeEnvironmentId" | "history" | "settings"
|
||
>;
|
||
|
||
type WorkspaceStore = PersistedShape & {
|
||
openTabIds: string[];
|
||
activeTabId: string | null;
|
||
search: string;
|
||
expanded: Record<string, boolean>;
|
||
dialog: Dialog | null;
|
||
runtime: Record<string, RequestRuntime>;
|
||
|
||
init(): void;
|
||
|
||
addCollection(name?: string): string;
|
||
renameNode(id: string, name: string): void;
|
||
removeNodeById(id: string): void;
|
||
duplicateNodeById(id: string): void;
|
||
moveNodeTo(id: string, collectionId: string, parentId: string | null): void;
|
||
addFolder(collectionId: string, parentId: string | null, name?: string): string;
|
||
addRequest(collectionId: string, parentId: string | null, kind: RequestKind, name?: string): string;
|
||
updateRequestData<K extends RequestKind>(
|
||
id: string,
|
||
patch: Partial<Extract<RequestNode, { kind: K }>["data"]>,
|
||
): void;
|
||
updateNodeDescription(id: string, description: string): void;
|
||
importCollections(collections: Collection[], envVars: { key: string; value: string }[]): string[];
|
||
loadWorkspaceData(data: WorkspaceData): void;
|
||
resetWorkspace(): void;
|
||
|
||
openTab(id: string): void;
|
||
closeTab(id: string): void;
|
||
closeOtherTabs(id: string): void;
|
||
setActiveTab(id: string | null): void;
|
||
|
||
setActiveEnvironment(id: string | null): void;
|
||
addEnvironment(name?: string): string;
|
||
duplicateEnvironment(id: string): string;
|
||
removeEnvironment(id: string): void;
|
||
updateEnvironment(id: string, patch: Partial<Omit<Environment, "id">>): void;
|
||
addEnvVariable(envId: string): void;
|
||
updateEnvVariable(envId: string, rowId: string, patch: { key?: string; value?: string; enabled?: boolean }): void;
|
||
removeEnvVariable(envId: string, rowId: string): void;
|
||
|
||
setTheme(theme: ThemeMode): void;
|
||
setTimeoutMs(ms: number): void;
|
||
|
||
setRuntime(id: string, patch: Partial<RequestRuntime>): void;
|
||
pushLive(id: string, dir: LiveMessage["dir"], text: string, topic?: string): void;
|
||
clearLive(id: string): void;
|
||
resetRuntime(id: string): void;
|
||
|
||
addHistory(entry: HistoryEntry): void;
|
||
clearHistory(): void;
|
||
setSearch(term: string): void;
|
||
toggleExpanded(id: string, force?: boolean): void;
|
||
setDialog(dialog: Dialog | null): void;
|
||
};
|
||
|
||
/** 自愈历史坏数据:请求节点缺 data 时按 kind 补默认值(保名称与说明) */
|
||
function sanitizeCollections(collections: Collection[]): Collection[] {
|
||
const fix = (nodes: CollectionNode[]): CollectionNode[] =>
|
||
nodes.map((n) => {
|
||
if (n.type === "folder") return { ...n, children: fix(n.children) };
|
||
if (n.type === "request" && (!n.data || !n.kind)) {
|
||
const kind = (n.kind ?? "http") as RequestKind;
|
||
return {
|
||
...n,
|
||
kind,
|
||
data: { ...defaultRequestData(kind) },
|
||
} as RequestNode;
|
||
}
|
||
return n;
|
||
});
|
||
return collections.map((c) => ({ ...c, children: fix(c.children) }));
|
||
}
|
||
|
||
function loadPersisted(): WorkspaceData | null {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (!raw) return null;
|
||
const data = JSON.parse(raw) as Partial<WorkspaceData>;
|
||
if (!Array.isArray(data.collections) || !Array.isArray(data.environments)) return null;
|
||
return {
|
||
version: 2,
|
||
collections: sanitizeCollections(data.collections),
|
||
environments: data.environments,
|
||
activeEnvironmentId: data.activeEnvironmentId ?? data.environments[0]?.id ?? null,
|
||
history: Array.isArray(data.history) ? data.history : [],
|
||
settings: {
|
||
theme: data.settings?.theme === "light" ? "light" : "dark",
|
||
timeoutMs: data.settings?.timeoutMs ?? 30000,
|
||
},
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
function schedulePersist(state: WorkspaceStore): void {
|
||
if (saveTimer) clearTimeout(saveTimer);
|
||
saveTimer = setTimeout(() => {
|
||
try {
|
||
const data: PersistedShape & { version: number } = {
|
||
version: 2,
|
||
collections: state.collections,
|
||
environments: state.environments,
|
||
activeEnvironmentId: state.activeEnvironmentId,
|
||
history: state.history.slice(0, 100),
|
||
settings: state.settings,
|
||
};
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||
} catch (e) {
|
||
console.warn("[opsxapi] 工作区保存失败", e);
|
||
}
|
||
}, 400);
|
||
}
|
||
|
||
export function applyThemeToDocument(theme: ThemeMode): void {
|
||
if (typeof document === "undefined") return;
|
||
document.documentElement.dataset.theme = theme;
|
||
}
|
||
|
||
export const useWorkspace = create<WorkspaceStore>((set, get) => {
|
||
const persist = () => schedulePersist(get());
|
||
|
||
return {
|
||
collections: [],
|
||
environments: [],
|
||
activeEnvironmentId: null,
|
||
history: [],
|
||
settings: { theme: "dark", timeoutMs: 30000 },
|
||
|
||
openTabIds: [],
|
||
activeTabId: null,
|
||
search: "",
|
||
expanded: {},
|
||
dialog: null,
|
||
|
||
init() {
|
||
const data = loadPersisted() ?? createSeedWorkspace();
|
||
set({
|
||
collections: data.collections,
|
||
environments: data.environments,
|
||
activeEnvironmentId: data.activeEnvironmentId,
|
||
history: data.history,
|
||
settings: data.settings,
|
||
});
|
||
applyThemeToDocument(data.settings.theme);
|
||
// 展开全部目录
|
||
const expanded: Record<string, boolean> = {};
|
||
for (const c of data.collections) {
|
||
walkFolders(c.children, (f) => {
|
||
expanded[f.id] = true;
|
||
});
|
||
}
|
||
set({ expanded });
|
||
},
|
||
|
||
addCollection(name) {
|
||
const collection: Collection = {
|
||
id: newId(),
|
||
type: "collection",
|
||
name: name || "新建集合",
|
||
children: [],
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
};
|
||
set({ collections: [...get().collections, collection] });
|
||
persist();
|
||
return collection.id;
|
||
},
|
||
|
||
renameNode(id, name) {
|
||
set({
|
||
collections: get().collections.map((c) =>
|
||
c.id === id
|
||
? { ...c, name, updatedAt: Date.now() }
|
||
: renameIn(c, id, name),
|
||
),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
removeNodeById(id) {
|
||
const state = get();
|
||
const openTabIds = state.openTabIds.filter((t) => t !== id);
|
||
const activeTabId =
|
||
state.activeTabId === id ? (openTabIds[openTabIds.length - 1] ?? null) : state.activeTabId;
|
||
const collections = state.collections.map((c) => removeFrom(c, id));
|
||
set({ collections, openTabIds, activeTabId });
|
||
persist();
|
||
},
|
||
|
||
duplicateNodeById(id) {
|
||
const state = get();
|
||
const ref = findNode(state.collections, id);
|
||
if (!ref) return;
|
||
const copy = deepCopyNode(ref.node, (name) => `${name} 副本`);
|
||
const collections = insertIntoCollection(
|
||
state.collections,
|
||
ref.collection.id,
|
||
ref.parent?.id ?? null,
|
||
copy,
|
||
);
|
||
set({ collections });
|
||
if (copy.type === "request") get().openTab(copy.id);
|
||
persist();
|
||
},
|
||
|
||
moveNodeTo(id, collectionId, parentId) {
|
||
set({ collections: moveNode(get().collections, id, collectionId, parentId) });
|
||
persist();
|
||
},
|
||
|
||
addFolder(collectionId, parentId, name) {
|
||
const state = get();
|
||
const siblings = parentChildren(state.collections, collectionId, parentId);
|
||
const folder: FolderNode = {
|
||
id: newId(),
|
||
type: "folder",
|
||
name: uniqueName(siblings, name ?? "新建目录"),
|
||
children: [],
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
};
|
||
set({
|
||
collections: insertIntoCollection(state.collections, collectionId, parentId, folder),
|
||
expanded: { ...state.expanded, [folder.id]: true },
|
||
});
|
||
persist();
|
||
return folder.id;
|
||
},
|
||
|
||
addRequest(collectionId, parentId, kind, name) {
|
||
const state = get();
|
||
const siblings = parentChildren(state.collections, collectionId, parentId);
|
||
const node: RequestNode = createRequest(kind, uniqueName(siblings, name ?? defaultNames(kind)));
|
||
set({ collections: insertIntoCollection(state.collections, collectionId, parentId, node) });
|
||
get().openTab(node.id);
|
||
persist();
|
||
return node.id;
|
||
},
|
||
|
||
updateRequestData(id, patch) {
|
||
set({
|
||
collections: get().collections.map((c) => patchRequestIn(c, id, patch)),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
updateNodeDescription(id, description) {
|
||
set({
|
||
collections: get().collections.map((c) => descRequestIn(c, id, description)),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
importCollections(collections, envVars) {
|
||
const state = get();
|
||
const existingNames = new Set(state.collections.map((c) => c.name));
|
||
const renamed = collections.map((c) => {
|
||
if (!existingNames.has(c.name)) return c;
|
||
let i = 2;
|
||
while (existingNames.has(`${c.name} ${i}`)) i++;
|
||
return { ...c, name: `${c.name} ${i}` };
|
||
});
|
||
// 环境变量并入当前环境
|
||
let environments = state.environments;
|
||
let activeEnvironmentId = state.activeEnvironmentId;
|
||
if (envVars.length) {
|
||
const active = environments.find((e) => e.id === activeEnvironmentId) ?? environments[0];
|
||
if (active) {
|
||
const known = new Set(active.variables.map((v) => v.key));
|
||
for (const v of envVars) {
|
||
if (!known.has(v.key)) {
|
||
active.variables.push(kv(v.key, v.value));
|
||
known.add(v.key);
|
||
}
|
||
}
|
||
environments = environments.map((e) => (e.id === active.id ? { ...active } : e));
|
||
} else {
|
||
const env: Environment = {
|
||
id: newId(),
|
||
name: "默认环境",
|
||
variables: envVars.map((v) => kv(v.key, v.value)),
|
||
};
|
||
environments = [...environments, env];
|
||
activeEnvironmentId = env.id;
|
||
}
|
||
}
|
||
set({ collections: [...state.collections, ...renamed], environments, activeEnvironmentId });
|
||
persist();
|
||
return renamed.map((c) => c.id);
|
||
},
|
||
|
||
loadWorkspaceData(data) {
|
||
set({
|
||
collections: data.collections,
|
||
environments: data.environments,
|
||
activeEnvironmentId: data.activeEnvironmentId ?? data.environments[0]?.id ?? null,
|
||
history: data.history ?? [],
|
||
settings: data.settings,
|
||
openTabIds: [],
|
||
activeTabId: null,
|
||
});
|
||
applyThemeToDocument(data.settings.theme);
|
||
persist();
|
||
},
|
||
|
||
resetWorkspace() {
|
||
const seed = createSeedWorkspace();
|
||
get().loadWorkspaceData(seed);
|
||
},
|
||
|
||
openTab(id) {
|
||
const state = get();
|
||
if (state.openTabIds.includes(id)) {
|
||
set({ activeTabId: id });
|
||
return;
|
||
}
|
||
set({ openTabIds: [...state.openTabIds, id], activeTabId: id });
|
||
},
|
||
|
||
closeTab(id) {
|
||
const state = get();
|
||
const openTabIds = state.openTabIds.filter((t) => t !== id);
|
||
set({
|
||
openTabIds,
|
||
activeTabId:
|
||
state.activeTabId === id ? (openTabIds[openTabIds.length - 1] ?? null) : state.activeTabId,
|
||
});
|
||
},
|
||
|
||
closeOtherTabs(id) {
|
||
set({ openTabIds: [id], activeTabId: id });
|
||
},
|
||
|
||
setActiveTab(id) {
|
||
set({ activeTabId: id });
|
||
},
|
||
|
||
setActiveEnvironment(id) {
|
||
set({ activeEnvironmentId: id });
|
||
persist();
|
||
},
|
||
|
||
addEnvironment(name) {
|
||
const env: Environment = {
|
||
id: newId(),
|
||
name: name || `环境 ${get().environments.length + 1}`,
|
||
variables: [],
|
||
};
|
||
set({ environments: [...get().environments, env] });
|
||
persist();
|
||
return env.id;
|
||
},
|
||
|
||
duplicateEnvironment(id) {
|
||
const state = get();
|
||
const source = state.environments.find((e) => e.id === id);
|
||
if (!source) return "";
|
||
let name = `${source.name} 副本`;
|
||
let i = 2;
|
||
const names = new Set(state.environments.map((e) => e.name));
|
||
while (names.has(name)) name = `${source.name} 副本 ${i++}`;
|
||
const copy: Environment = {
|
||
id: newId(),
|
||
name,
|
||
variables: source.variables.map((v) => kv(v.key, v.value, v.enabled, v.description)),
|
||
};
|
||
set({ environments: [...state.environments, copy] });
|
||
persist();
|
||
return copy.id;
|
||
},
|
||
|
||
removeEnvironment(id) {
|
||
const state = get();
|
||
const environments = state.environments.filter((e) => e.id !== id);
|
||
set({
|
||
environments,
|
||
activeEnvironmentId:
|
||
state.activeEnvironmentId === id ? (environments[0]?.id ?? null) : state.activeEnvironmentId,
|
||
});
|
||
persist();
|
||
},
|
||
|
||
updateEnvironment(id, patch) {
|
||
set({
|
||
environments: get().environments.map((e) => (e.id === id ? { ...e, ...patch } : e)),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
addEnvVariable(envId) {
|
||
set({
|
||
environments: get().environments.map((e) =>
|
||
e.id === envId ? { ...e, variables: [...e.variables, kv()] } : e,
|
||
),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
updateEnvVariable(envId, rowId, patch) {
|
||
set({
|
||
environments: get().environments.map((e) =>
|
||
e.id === envId
|
||
? {
|
||
...e,
|
||
variables: e.variables.map((v) => (v.id === rowId ? { ...v, ...patch } : v)),
|
||
}
|
||
: e,
|
||
),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
removeEnvVariable(envId, rowId) {
|
||
set({
|
||
environments: get().environments.map((e) =>
|
||
e.id === envId ? { ...e, variables: e.variables.filter((v) => v.id !== rowId) } : e,
|
||
),
|
||
});
|
||
persist();
|
||
},
|
||
|
||
setTheme(theme) {
|
||
set({ settings: { ...get().settings, theme } });
|
||
applyThemeToDocument(theme);
|
||
persist();
|
||
},
|
||
|
||
setTimeoutMs(ms) {
|
||
set({ settings: { ...get().settings, timeoutMs: ms } });
|
||
persist();
|
||
},
|
||
|
||
setRuntime(id, patch) {
|
||
const state = get();
|
||
set({
|
||
runtime: {
|
||
...state.runtime,
|
||
[id]: { ...(state.runtime[id] ?? emptyRuntime()), ...patch },
|
||
},
|
||
});
|
||
},
|
||
|
||
pushLive(id, dir, text, topic) {
|
||
const state = get();
|
||
const prev = state.runtime[id] ?? emptyRuntime();
|
||
const msg: LiveMessage = { id: newId(), dir, at: Date.now(), text, topic };
|
||
const messages = [...prev.messages, msg].slice(-500);
|
||
set({
|
||
runtime: {
|
||
...state.runtime,
|
||
[id]: {
|
||
...prev,
|
||
messages,
|
||
sent: dir === "out" ? prev.sent + 1 : prev.sent,
|
||
received: dir === "in" ? prev.received + 1 : prev.received,
|
||
},
|
||
},
|
||
});
|
||
},
|
||
|
||
clearLive(id) {
|
||
const state = get();
|
||
const prev = state.runtime[id] ?? emptyRuntime();
|
||
set({ runtime: { ...state.runtime, [id]: { ...prev, messages: [] } } });
|
||
},
|
||
|
||
resetRuntime(id) {
|
||
const state = get();
|
||
set({ runtime: { ...state.runtime, [id]: emptyRuntime() } });
|
||
},
|
||
|
||
addHistory(entry) {
|
||
set({ history: [entry, ...get().history].slice(0, 100) });
|
||
persist();
|
||
},
|
||
|
||
clearHistory() {
|
||
set({ history: [] });
|
||
persist();
|
||
},
|
||
|
||
setSearch(term) {
|
||
set({ search: term });
|
||
},
|
||
|
||
toggleExpanded(id, force) {
|
||
const expanded = { ...get().expanded };
|
||
expanded[id] = force ?? !expanded[id];
|
||
set({ expanded });
|
||
},
|
||
|
||
setDialog(dialog) {
|
||
set({ dialog });
|
||
},
|
||
|
||
runtime: {} as Record<string, RequestRuntime>,
|
||
};
|
||
});
|
||
|
||
// ---------- 纯帮助函数 ----------
|
||
|
||
function walkFolders(nodes: CollectionNode[], fn: (f: FolderNode) => void): void {
|
||
for (const node of nodes) {
|
||
if (node.type === "folder") {
|
||
fn(node);
|
||
walkFolders(node.children, fn);
|
||
}
|
||
}
|
||
}
|
||
|
||
function renameIn(collection: Collection, id: string, name: string): Collection {
|
||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||
nodes.map((n) => {
|
||
if (n.id === id) return { ...n, name, updatedAt: Date.now() };
|
||
if (n.type === "folder") return { ...n, children: visit(n.children) };
|
||
return n;
|
||
});
|
||
return { ...collection, children: visit(collection.children) };
|
||
}
|
||
|
||
function removeFrom(collection: Collection, id: string): Collection {
|
||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||
nodes
|
||
.filter((n) => n.id !== id)
|
||
.map((n) => (n.type === "folder" ? { ...n, children: visit(n.children) } : n));
|
||
return { ...collection, children: visit(collection.children) };
|
||
}
|
||
|
||
function patchRequestIn(
|
||
collection: Collection,
|
||
id: string,
|
||
patch: Record<string, unknown>,
|
||
): Collection {
|
||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||
nodes.map((n) => {
|
||
if (n.id === id && n.type === "request") {
|
||
const merged = { ...n.data, ...patch } as RequestNode["data"];
|
||
return { ...n, data: merged, updatedAt: Date.now() } as CollectionNode;
|
||
}
|
||
if (n.type === "folder") return { ...n, children: visit(n.children) };
|
||
return n;
|
||
});
|
||
return { ...collection, children: visit(collection.children) };
|
||
}
|
||
|
||
function descRequestIn(collection: Collection, id: string, description: string): Collection {
|
||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||
nodes.map((n) => {
|
||
if (n.id === id) return { ...n, description: description || undefined, updatedAt: Date.now() };
|
||
if (n.type === "folder") return { ...n, children: visit(n.children) };
|
||
return n;
|
||
});
|
||
return { ...collection, children: visit(collection.children) };
|
||
}
|
||
|
||
function parentChildren(
|
||
collections: Collection[],
|
||
collectionId: string,
|
||
parentId: string | null,
|
||
): CollectionNode[] {
|
||
const collection = collections.find((c) => c.id === collectionId);
|
||
if (!collection) return [];
|
||
if (parentId === null) return collection.children;
|
||
let foundChildren: CollectionNode[] | null = null;
|
||
const visit = (nodes: CollectionNode[]) => {
|
||
for (const n of nodes) {
|
||
if (n.id === parentId) {
|
||
if (n.type === "folder") foundChildren = n.children;
|
||
return;
|
||
}
|
||
if (n.type === "folder") visit(n.children);
|
||
}
|
||
};
|
||
visit(collection.children);
|
||
return foundChildren ?? collection.children;
|
||
}
|
||
|
||
function defaultNames(kind: RequestKind): string {
|
||
const labels: Record<RequestKind, string> = {
|
||
http: "新建 HTTP 接口",
|
||
graphql: "新建 GraphQL",
|
||
websocket: "新建 WebSocket",
|
||
sse: "新建 SSE",
|
||
socketio: "新建 Socket.IO",
|
||
mqtt: "新建 MQTT",
|
||
};
|
||
return labels[kind];
|
||
}
|
||
|
||
/** 便捷选择器:当前环境变量表 */
|
||
export function activeEnvMap(): Record<string, string> {
|
||
const state = useWorkspace.getState();
|
||
const active = state.environments.find((e) => e.id === state.activeEnvironmentId);
|
||
const out: Record<string, string> = {};
|
||
for (const v of active?.variables ?? []) {
|
||
if (v.enabled && v.key) out[v.key] = v.value;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 便捷选择器:请求体构建辅助(http/graphql 共用) */
|
||
export type { HttpRequestData };
|