@zhhoceanfly/protoview 0.0.0-stage → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -3
- package/bin/protoview.js +3 -0
- package/dist/cli.js +115 -0
- package/dist/core/catalog.js +493 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/clientModel.js +111 -0
- package/dist/core/clientModel.js.map +1 -0
- package/dist/core/grpcInvoke.js +178 -0
- package/dist/core/grpcInvoke.js.map +1 -0
- package/dist/core/grpcStress.js +83 -0
- package/dist/core/grpcStress.js.map +1 -0
- package/dist/core/hostCore.js +292 -0
- package/dist/core/hostCore.js.map +1 -0
- package/dist/core/httpInvoke.js +152 -0
- package/dist/core/httpInvoke.js.map +1 -0
- package/dist/core/invokeDispatch.js +26 -0
- package/dist/core/invokeDispatch.js.map +1 -0
- package/dist/core/meta.js +158 -0
- package/dist/core/meta.js.map +1 -0
- package/dist/core/parser.js +638 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/sourceCore.js +235 -0
- package/dist/core/sourceCore.js.map +1 -0
- package/dist/core/sseInvoke.js +244 -0
- package/dist/core/sseInvoke.js.map +1 -0
- package/dist/core/wsInvoke.js +326 -0
- package/dist/core/wsInvoke.js.map +1 -0
- package/dist/server.js +159 -0
- package/dist/uploadProtoDir.js +78 -0
- package/package.json +55 -6
- package/public/app.js +2025 -0
- package/public/donate-wechat.png +0 -0
- package/public/flow.js +1481 -0
- package/public/index.html +424 -0
- package/public/styles.css +1481 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,2025 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const isVsCodeWebview = typeof acquireVsCodeApi === "function";
|
|
3
|
+
const vscode = isVsCodeWebview ? acquireVsCodeApi() : createWsBridge();
|
|
4
|
+
const THEME_KEY = "protoview.client.theme";
|
|
5
|
+
const DRAFT_KEY = "protoview.client.drafts";
|
|
6
|
+
const SPLIT_KEY = "protoview.client.split";
|
|
7
|
+
|
|
8
|
+
/** Browser / CLI: talk to local Node host over WebSocket (same message protocol as the extension). */
|
|
9
|
+
function createWsBridge() {
|
|
10
|
+
if (typeof WebSocket === "undefined" || !location.host) return null;
|
|
11
|
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
12
|
+
const url = `${proto}//${location.host}/ws`;
|
|
13
|
+
let socket = null;
|
|
14
|
+
const queue = [];
|
|
15
|
+
let closed = false;
|
|
16
|
+
|
|
17
|
+
function connect() {
|
|
18
|
+
if (closed) return;
|
|
19
|
+
try {
|
|
20
|
+
socket = new WebSocket(url);
|
|
21
|
+
} catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
socket.addEventListener("open", () => {
|
|
25
|
+
while (queue.length && socket?.readyState === WebSocket.OPEN) {
|
|
26
|
+
socket.send(queue.shift());
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
socket.addEventListener("message", (event) => {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(String(event.data || ""));
|
|
32
|
+
window.dispatchEvent(new MessageEvent("message", { data }));
|
|
33
|
+
} catch { /* ignore bad frames */ }
|
|
34
|
+
});
|
|
35
|
+
socket.addEventListener("close", () => {
|
|
36
|
+
socket = null;
|
|
37
|
+
if (!closed) setTimeout(connect, 1500);
|
|
38
|
+
});
|
|
39
|
+
socket.addEventListener("error", () => {
|
|
40
|
+
try { socket?.close(); } catch { /* ignore */ }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
connect();
|
|
45
|
+
return {
|
|
46
|
+
postMessage(msg) {
|
|
47
|
+
const text = JSON.stringify(msg || {});
|
|
48
|
+
if (socket && socket.readyState === WebSocket.OPEN) socket.send(text);
|
|
49
|
+
else queue.push(text);
|
|
50
|
+
},
|
|
51
|
+
dispose() {
|
|
52
|
+
closed = true;
|
|
53
|
+
try { socket?.close(); } catch { /* ignore */ }
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const state = {
|
|
59
|
+
catalog: { packages: [], types: {}, sourceLabel: "独立模式" },
|
|
60
|
+
envs: defaultEnvs(),
|
|
61
|
+
activeEnvId: "local",
|
|
62
|
+
selected: null,
|
|
63
|
+
tab: "message",
|
|
64
|
+
expanded: new Set(),
|
|
65
|
+
expandedReady: false,
|
|
66
|
+
docsNav: [],
|
|
67
|
+
metadata: [{ name: "", value: "", description: "" }],
|
|
68
|
+
auth: defaultAuth(),
|
|
69
|
+
history: [],
|
|
70
|
+
drafts: loadDrafts(),
|
|
71
|
+
messageDirty: false,
|
|
72
|
+
stressMode: "light",
|
|
73
|
+
stressId: "",
|
|
74
|
+
ghzLog: "",
|
|
75
|
+
source: {
|
|
76
|
+
mode: "directory",
|
|
77
|
+
dirPath: "",
|
|
78
|
+
gitUrl: "",
|
|
79
|
+
gitBranch: "main",
|
|
80
|
+
localPath: "",
|
|
81
|
+
lastError: "",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const $ = (id) => document.getElementById(id);
|
|
86
|
+
const els = {
|
|
87
|
+
tree: $("tree"),
|
|
88
|
+
filter: $("filter"),
|
|
89
|
+
sourceMeta: $("sourceMeta"),
|
|
90
|
+
envSelect: $("envSelect"),
|
|
91
|
+
address: $("address"),
|
|
92
|
+
methodLabel: $("methodLabel"),
|
|
93
|
+
messageEditor: $("messageEditor"),
|
|
94
|
+
rpcInfo: $("rpcInfo"),
|
|
95
|
+
metaBody: $("metaBody"),
|
|
96
|
+
docsView: $("docsView"),
|
|
97
|
+
historyView: $("historyView"),
|
|
98
|
+
responseView: $("responseView"),
|
|
99
|
+
stressResult: $("stressResult"),
|
|
100
|
+
sendBtn: $("sendBtn"),
|
|
101
|
+
toolbar: $("toolbar"),
|
|
102
|
+
toolbarHint: $("toolbarHint"),
|
|
103
|
+
themeBtn: $("themeBtn"),
|
|
104
|
+
envName: $("envName"),
|
|
105
|
+
envHost: $("envHost"),
|
|
106
|
+
envNote: $("envNote"),
|
|
107
|
+
envList: $("envList"),
|
|
108
|
+
envModal: $("envModal"),
|
|
109
|
+
sourceModal: $("sourceModal"),
|
|
110
|
+
bulkModal: $("bulkModal"),
|
|
111
|
+
bulkText: $("bulkText"),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function defaultEnvs() {
|
|
115
|
+
return [
|
|
116
|
+
{ id: "local", name: "local", host: "127.0.0.1:19200", note: "本地" },
|
|
117
|
+
{ id: "test", name: "test", host: "", note: "测试" },
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function defaultAuth() {
|
|
122
|
+
return {
|
|
123
|
+
type: "none",
|
|
124
|
+
token: "",
|
|
125
|
+
username: "",
|
|
126
|
+
password: "",
|
|
127
|
+
apiKey: "x-api-key",
|
|
128
|
+
apiValue: "",
|
|
129
|
+
oauthToken: "",
|
|
130
|
+
wsseUser: "",
|
|
131
|
+
wssePass: "",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeAuth(raw) {
|
|
136
|
+
return { ...defaultAuth(), ...(raw && typeof raw === "object" ? raw : {}) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function persistCurrentDraft() {
|
|
140
|
+
if (!state.selected) return;
|
|
141
|
+
state.drafts[state.selected.id] = {
|
|
142
|
+
message: els.messageEditor.value,
|
|
143
|
+
metadata: state.metadata,
|
|
144
|
+
auth: state.auth,
|
|
145
|
+
protocol: $("protocolSelect")?.value || "grpc",
|
|
146
|
+
httpMethod: $("httpMethodSelect")?.value || "POST",
|
|
147
|
+
httpPath: $("httpPath")?.value || "",
|
|
148
|
+
};
|
|
149
|
+
saveDrafts();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function loadDrafts() {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(localStorage.getItem(DRAFT_KEY) || "{}");
|
|
155
|
+
} catch {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function saveDrafts() {
|
|
161
|
+
localStorage.setItem(DRAFT_KEY, JSON.stringify(state.drafts));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function activeEnv() {
|
|
165
|
+
return state.envs.find((item) => item.id === state.activeEnvId) || state.envs[0];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function initTheme() {
|
|
169
|
+
const saved = localStorage.getItem(THEME_KEY) || "dark";
|
|
170
|
+
document.documentElement.setAttribute("data-theme", saved);
|
|
171
|
+
els.themeBtn.textContent = saved === "light" ? "☾" : "◐";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toggleTheme() {
|
|
175
|
+
const next = document.documentElement.getAttribute("data-theme") === "light" ? "dark" : "light";
|
|
176
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
177
|
+
localStorage.setItem(THEME_KEY, next);
|
|
178
|
+
els.themeBtn.textContent = next === "light" ? "☾" : "◐";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function esc(value) {
|
|
182
|
+
return String(value ?? "")
|
|
183
|
+
.replace(/&/g, "&")
|
|
184
|
+
.replace(/</g, "<")
|
|
185
|
+
.replace(/>/g, ">")
|
|
186
|
+
.replace(/"/g, """);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function treeIsOpen() {
|
|
190
|
+
for (const pkg of state.catalog.packages || []) {
|
|
191
|
+
if (state.expanded.has(`pkg:${pkg.name}`)) return true;
|
|
192
|
+
for (const service of pkg.services || []) {
|
|
193
|
+
if (state.expanded.has(`svc:${service.id}`)) return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function syncTreeToggle() {
|
|
200
|
+
const btn = $("treeToggleBtn");
|
|
201
|
+
if (!btn) return;
|
|
202
|
+
const open = treeIsOpen();
|
|
203
|
+
btn.textContent = open ? "▸" : "▾";
|
|
204
|
+
btn.title = open ? "全部收起" : "全部展开";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setAllExpanded(open) {
|
|
208
|
+
state.expanded.clear();
|
|
209
|
+
state.expandedReady = true;
|
|
210
|
+
if (open) {
|
|
211
|
+
for (const pkg of state.catalog.packages || []) {
|
|
212
|
+
state.expanded.add(`pkg:${pkg.name}`);
|
|
213
|
+
for (const service of pkg.services || []) state.expanded.add(`svc:${service.id}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
renderTree();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ensureExpandedDefaults() {
|
|
220
|
+
if (state.expandedReady) return;
|
|
221
|
+
state.expandedReady = true;
|
|
222
|
+
for (const pkg of state.catalog.packages || []) {
|
|
223
|
+
state.expanded.add(`pkg:${pkg.name}`);
|
|
224
|
+
for (const service of pkg.services || []) state.expanded.add(`svc:${service.id}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function envOptionsHtml() {
|
|
229
|
+
return state.envs
|
|
230
|
+
.map((env) => `<option value="${esc(env.id)}"${env.id === state.activeEnvId ? " selected" : ""}>${esc(env.name)}</option>`)
|
|
231
|
+
.join("");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderEnvSelect() {
|
|
235
|
+
const html = envOptionsHtml();
|
|
236
|
+
els.envSelect.innerHTML = html;
|
|
237
|
+
const env = activeEnv();
|
|
238
|
+
if (env && document.activeElement !== els.address) els.address.value = env.host || "";
|
|
239
|
+
fillEnvForm();
|
|
240
|
+
renderEnvList();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function applyEnvId(id) {
|
|
244
|
+
if (!id) return;
|
|
245
|
+
state.activeEnvId = id;
|
|
246
|
+
const env = activeEnv();
|
|
247
|
+
els.address.value = env?.host || "";
|
|
248
|
+
els.envSelect.value = id;
|
|
249
|
+
fillEnvForm();
|
|
250
|
+
persistEnvs();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function currentProtocol() {
|
|
254
|
+
const raw = ($("protocolSelect")?.value || "grpc").toLowerCase();
|
|
255
|
+
if (raw === "http" || raw === "sse" || raw === "websocket") return raw;
|
|
256
|
+
return "grpc";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function syncProtocolUi() {
|
|
260
|
+
const protocol = currentProtocol();
|
|
261
|
+
const needsPath = protocol === "http" || protocol === "sse" || protocol === "websocket";
|
|
262
|
+
const needsMethod = protocol === "http" || protocol === "sse";
|
|
263
|
+
const grpcOnly = protocol === "grpc";
|
|
264
|
+
$("httpMethodSelect")?.classList.toggle("hidden", !needsMethod);
|
|
265
|
+
$("httpPath")?.classList.toggle("hidden", !needsPath);
|
|
266
|
+
document.querySelector('[data-stress-mode="ghz"]')?.classList.toggle("hidden", !grpcOnly);
|
|
267
|
+
if (!grpcOnly && state.stressMode === "ghz") setStressMode("light");
|
|
268
|
+
if (els.address) {
|
|
269
|
+
const placeholders = {
|
|
270
|
+
grpc: "gRPC host:port",
|
|
271
|
+
http: "HTTP host:port 或 https://host",
|
|
272
|
+
sse: "SSE host:port 或 https://host",
|
|
273
|
+
websocket: "WS host:port 或 wss://host",
|
|
274
|
+
};
|
|
275
|
+
els.address.placeholder = placeholders[protocol] || placeholders.grpc;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function fillEnvForm() {
|
|
280
|
+
const env = activeEnv();
|
|
281
|
+
if (!env) return;
|
|
282
|
+
els.envName.value = env.name;
|
|
283
|
+
els.envHost.value = env.host || "";
|
|
284
|
+
els.envNote.value = env.note || "";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderEnvList() {
|
|
288
|
+
els.envList.innerHTML = state.envs.map((env) => `
|
|
289
|
+
<div class="env-item">
|
|
290
|
+
<div>
|
|
291
|
+
<strong>${esc(env.name)}</strong>
|
|
292
|
+
<span>${esc(env.host || "(未配置地址)")}</span>
|
|
293
|
+
</div>
|
|
294
|
+
<button class="btn" type="button" data-pick-env="${esc(env.id)}">编辑</button>
|
|
295
|
+
</div>
|
|
296
|
+
`).join("");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function renderTree() {
|
|
300
|
+
const query = (els.filter.value || "").trim().toLowerCase();
|
|
301
|
+
const packs = state.catalog.packages || [];
|
|
302
|
+
if (!packs.length) {
|
|
303
|
+
els.tree.innerHTML = `<div class="empty-side">还没有接口数据。点击刷新,或确认 Proto 来源已解析成功。</div>`;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
ensureExpandedDefaults();
|
|
307
|
+
let html = "";
|
|
308
|
+
for (const pkg of packs) {
|
|
309
|
+
const services = (pkg.services || []).filter((service) => matchService(pkg, service, query));
|
|
310
|
+
if (!services.length) continue;
|
|
311
|
+
const pkgKey = `pkg:${pkg.name}`;
|
|
312
|
+
const pkgOpen = state.expanded.has(pkgKey);
|
|
313
|
+
const rpcCount = services.reduce((n, s) => n + (s.rpcs || []).length, 0);
|
|
314
|
+
html += `<div class="node">
|
|
315
|
+
<button class="node-row" type="button" data-toggle="${esc(pkgKey)}">
|
|
316
|
+
<span class="caret">${pkgOpen ? "▼" : "▶"}</span>
|
|
317
|
+
<span class="kind">📦</span>
|
|
318
|
+
<span class="label">${esc(pkg.name)}</span>
|
|
319
|
+
<span class="count">${rpcCount}</span>
|
|
320
|
+
</button>
|
|
321
|
+
<div class="children${pkgOpen ? "" : " collapsed"}">`;
|
|
322
|
+
for (const service of services) {
|
|
323
|
+
const svcKey = `svc:${service.id}`;
|
|
324
|
+
const svcOpen = state.expanded.has(svcKey);
|
|
325
|
+
const rpcs = (service.rpcs || []).filter((rpc) => matchRpc(pkg, service, rpc, query));
|
|
326
|
+
html += `<div class="node">
|
|
327
|
+
<button class="node-row" type="button" data-toggle="${esc(svcKey)}">
|
|
328
|
+
<span class="caret">${svcOpen ? "▼" : "▶"}</span>
|
|
329
|
+
<span class="kind">◇</span>
|
|
330
|
+
<span class="label">${esc(service.name)}</span>
|
|
331
|
+
<span class="count">${rpcs.length}</span>
|
|
332
|
+
</button>
|
|
333
|
+
<div class="children${svcOpen ? "" : " collapsed"}">`;
|
|
334
|
+
for (const rpc of rpcs) {
|
|
335
|
+
const active = state.selected?.id === rpc.id ? " active" : "";
|
|
336
|
+
html += `<div>
|
|
337
|
+
<button class="node-row${active}" type="button" data-rpc="${esc(rpc.id)}" title="在链路调试中按住拖到画布">
|
|
338
|
+
<span class="caret"></span>
|
|
339
|
+
<span class="kind">⇢</span>
|
|
340
|
+
<span class="label">${esc(rpc.name)}</span>
|
|
341
|
+
</button>
|
|
342
|
+
<div class="rpc-desc">${esc(rpc.comment || `${rpc.request} → ${rpc.response}`)}</div>
|
|
343
|
+
</div>`;
|
|
344
|
+
}
|
|
345
|
+
html += `</div></div>`;
|
|
346
|
+
}
|
|
347
|
+
html += `</div></div>`;
|
|
348
|
+
}
|
|
349
|
+
els.tree.innerHTML = html || `<div class="empty-side">没有匹配的接口</div>`;
|
|
350
|
+
syncTreeToggle();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function matchService(pkg, service, query) {
|
|
354
|
+
if (!query) return true;
|
|
355
|
+
if (pkg.name.toLowerCase().includes(query) || service.name.toLowerCase().includes(query)) return true;
|
|
356
|
+
return (service.rpcs || []).some((rpc) => matchRpc(pkg, service, rpc, query));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function matchRpc(pkg, service, rpc, query) {
|
|
360
|
+
if (!query) return true;
|
|
361
|
+
const hay = `${pkg.name} ${service.name} ${rpc.name} ${rpc.comment || ""} ${rpc.request} ${rpc.response}`.toLowerCase();
|
|
362
|
+
return hay.includes(query);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function findRpc(id) {
|
|
366
|
+
for (const pkg of state.catalog.packages || []) {
|
|
367
|
+
for (const service of pkg.services || []) {
|
|
368
|
+
const rpc = (service.rpcs || []).find((item) => item.id === id);
|
|
369
|
+
if (rpc) return { pkg, service, rpc };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function selectRpc(id, opts = {}) {
|
|
376
|
+
const hit = findRpc(id);
|
|
377
|
+
if (!hit) return;
|
|
378
|
+
if (state.selected && state.messageDirty) persistCurrentDraft();
|
|
379
|
+
const { pkg, service, rpc } = hit;
|
|
380
|
+
state.expanded.add(`pkg:${pkg.name}`);
|
|
381
|
+
state.expanded.add(`svc:${service.id}`);
|
|
382
|
+
state.selected = { ...rpc, package: pkg.name, service: service.name, serviceId: service.id };
|
|
383
|
+
els.methodLabel.textContent = `${pkg.name}.${service.name}/${rpc.name}`;
|
|
384
|
+
els.sendBtn.disabled = false;
|
|
385
|
+
|
|
386
|
+
const draft = state.drafts[rpc.id];
|
|
387
|
+
if (draft && !opts.forceGenerate) {
|
|
388
|
+
els.messageEditor.value = draft.message || generateMessage(rpc);
|
|
389
|
+
state.metadata = Array.isArray(draft.metadata) && draft.metadata.length
|
|
390
|
+
? draft.metadata
|
|
391
|
+
: [{ name: "", value: "", description: "" }];
|
|
392
|
+
state.auth = normalizeAuth(draft.auth);
|
|
393
|
+
if ($("protocolSelect")) {
|
|
394
|
+
const p = (draft.protocol || "grpc").toLowerCase();
|
|
395
|
+
$("protocolSelect").value = ["http", "sse", "websocket"].includes(p) ? p : "grpc";
|
|
396
|
+
}
|
|
397
|
+
if ($("httpMethodSelect")) {
|
|
398
|
+
$("httpMethodSelect").value = draft.httpMethod
|
|
399
|
+
|| (draft.protocol === "sse" ? "GET" : rpc.httpVerb || "POST");
|
|
400
|
+
}
|
|
401
|
+
if ($("httpPath")) $("httpPath").value = draft.httpPath || rpc.httpPath || "";
|
|
402
|
+
} else {
|
|
403
|
+
els.messageEditor.value = generateMessage(rpc);
|
|
404
|
+
state.metadata = [{ name: "", value: "", description: "" }];
|
|
405
|
+
state.auth = defaultAuth();
|
|
406
|
+
if ($("protocolSelect")) $("protocolSelect").value = "grpc";
|
|
407
|
+
if ($("httpMethodSelect")) $("httpMethodSelect").value = rpc.httpVerb || "POST";
|
|
408
|
+
if ($("httpPath")) $("httpPath").value = rpc.httpPath || "";
|
|
409
|
+
}
|
|
410
|
+
state.messageDirty = false;
|
|
411
|
+
state.docsNav = [];
|
|
412
|
+
syncProtocolUi();
|
|
413
|
+
renderMetadata();
|
|
414
|
+
renderAuth();
|
|
415
|
+
renderRpcInfo();
|
|
416
|
+
paintDocs();
|
|
417
|
+
refreshGhzCommand();
|
|
418
|
+
renderTree();
|
|
419
|
+
setTab(state.tab);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function renderRpcInfo() {
|
|
423
|
+
const el = els.rpcInfo;
|
|
424
|
+
if (!el) return;
|
|
425
|
+
const rpc = state.selected;
|
|
426
|
+
if (!rpc) {
|
|
427
|
+
el.innerHTML = `<div class="rpc-info-empty">选择左侧接口后显示定义与说明。</div>`;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const chips = [];
|
|
431
|
+
if (rpc.httpVerb) chips.push(`<span class="rpc-info-chip">${esc(rpc.httpVerb)} ${esc(rpc.httpPath || "")}</span>`);
|
|
432
|
+
for (const badge of rpc.badges || []) {
|
|
433
|
+
chips.push(`<span class="rpc-info-chip${badge.warn ? " warn" : ""}">${esc(badge.text)}</span>`);
|
|
434
|
+
}
|
|
435
|
+
const source = rpc.source
|
|
436
|
+
? `<details class="rpc-info-def" open><summary>接口定义</summary><pre>${esc(rpc.source)}</pre></details>`
|
|
437
|
+
: "";
|
|
438
|
+
el.innerHTML = `
|
|
439
|
+
<h3 class="rpc-info-title">${esc(rpc.name)}</h3>
|
|
440
|
+
<p class="rpc-info-lead">${esc(rpc.comment || "无注释")}</p>
|
|
441
|
+
<p class="rpc-info-meta">${esc(rpc.package)}.${esc(rpc.service)} · ${esc(rpc.file || "")}${rpc.line ? `:${rpc.line}` : ""}</p>
|
|
442
|
+
${chips.length ? `<div class="rpc-info-chips">${chips.join("")}</div>` : ""}
|
|
443
|
+
<div class="rpc-info-row"><span>请求</span><code>${esc(rpc.request)}</code></div>
|
|
444
|
+
<div class="rpc-info-row"><span>响应</span><code>${esc(rpc.response)}</code></div>
|
|
445
|
+
<div class="rpc-info-row"><span>字段</span><code>${(rpc.requestFields || []).length} 个请求字段</code></div>
|
|
446
|
+
${source}
|
|
447
|
+
${rpc.path ? `<button class="btn" type="button" style="margin-top:10px" data-src="${esc(rpc.path)}" data-line="${rpc.line || 1}">跳转到源码</button>` : ""}
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function sampleValue(field, depth = 0, seen = new Set()) {
|
|
452
|
+
if (field.repeated) {
|
|
453
|
+
if (depth >= 2) return [];
|
|
454
|
+
return [sampleValue({ ...field, repeated: false }, depth + 1, seen)];
|
|
455
|
+
}
|
|
456
|
+
if (field.key) {
|
|
457
|
+
if (depth >= 2) return {};
|
|
458
|
+
const keySample = sampleScalar(field.key, `${field.name}Key`);
|
|
459
|
+
return { [String(keySample)]: sampleValue({ ...field, key: undefined }, depth + 1, seen) };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const type = (field.type || "").replace(/^\./, "");
|
|
463
|
+
const typeDef = field.typeId ? state.catalog.types?.[field.typeId] : undefined;
|
|
464
|
+
if (typeDef?.kind === "enum") return sampleEnum(typeDef);
|
|
465
|
+
if (typeDef?.kind === "message") return sampleMessage(typeDef, depth, seen);
|
|
466
|
+
return sampleScalar(type, field.name);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function sampleEnum(typeDef) {
|
|
470
|
+
const values = typeDef.values || [];
|
|
471
|
+
if (!values.length) return 0;
|
|
472
|
+
const preferred = values.find((item) => item.number !== 0) || values[0];
|
|
473
|
+
return preferred.number;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function sampleMessage(typeDef, depth, seen) {
|
|
477
|
+
if (depth >= 2 || seen.has(typeDef.id)) return {};
|
|
478
|
+
const next = new Set(seen);
|
|
479
|
+
next.add(typeDef.id);
|
|
480
|
+
const body = {};
|
|
481
|
+
for (const field of typeDef.fields || []) {
|
|
482
|
+
body[field.name] = sampleValue(field, depth + 1, next);
|
|
483
|
+
}
|
|
484
|
+
return body;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function sampleScalar(type, fieldName) {
|
|
488
|
+
const name = fieldName || "";
|
|
489
|
+
const lower = name.toLowerCase();
|
|
490
|
+
const hint = nameHint(lower);
|
|
491
|
+
|
|
492
|
+
if (type === "bool") {
|
|
493
|
+
if (hint === "enable" || /enable|active|valid|success|ok|visible|open/.test(lower)) return true;
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
if (type === "bytes") return "Ynl0ZXM=";
|
|
497
|
+
if (type === "string") return sampleString(name, lower, hint);
|
|
498
|
+
if (["double", "float"].includes(type)) {
|
|
499
|
+
if (hint === "ratio" || /rate|ratio|percent|score/.test(lower)) return 0.85;
|
|
500
|
+
if (hint === "price" || /price|amount|cost|money|fee/.test(lower)) return 9.9;
|
|
501
|
+
return 1.5;
|
|
502
|
+
}
|
|
503
|
+
if (["int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64"].includes(type)) {
|
|
504
|
+
return sampleNumber(name, lower, hint, type.includes("64"));
|
|
505
|
+
}
|
|
506
|
+
return sampleString(name, lower, "default");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function nameHint(lower) {
|
|
510
|
+
if (/(email|mail)/.test(lower)) return "email";
|
|
511
|
+
if (/(phone|mobile|tel)/.test(lower)) return "phone";
|
|
512
|
+
if (/(url|uri|link|href)/.test(lower)) return "url";
|
|
513
|
+
if (/(token|jwt|authorization|auth|secret|password|passwd|pwd)/.test(lower)) return "token";
|
|
514
|
+
if (/(uuid|guid)/.test(lower)) return "uuid";
|
|
515
|
+
if (/(ip$|ipaddr|host)/.test(lower)) return "host";
|
|
516
|
+
if (/(time|timestamp|expire|deadline|created|updated|at$)/.test(lower)) return "time";
|
|
517
|
+
if (/(date|day|birthday)/.test(lower)) return "date";
|
|
518
|
+
if (/(count|num|amount|qty|quantity|size|len|limit|page|offset|index|cap)/.test(lower)) return "count";
|
|
519
|
+
if (/(price|cost|money|fee|balance)/.test(lower)) return "price";
|
|
520
|
+
if (/(rate|ratio|percent|score)/.test(lower)) return "ratio";
|
|
521
|
+
if (/(version|ver)/.test(lower)) return "version";
|
|
522
|
+
if (/(code|status)/.test(lower)) return "code";
|
|
523
|
+
if (/(name|title|nick)/.test(lower)) return "name";
|
|
524
|
+
if (/(desc|comment|remark|note|msg|message|text|content|reason)/.test(lower)) return "text";
|
|
525
|
+
if (/(id$|_id)/.test(lower) || lower === "id") return "id";
|
|
526
|
+
if (/(enable|active|valid|success|ok|visible|open)/.test(lower)) return "enable";
|
|
527
|
+
return "default";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function sampleString(name, lower, hint) {
|
|
531
|
+
switch (hint) {
|
|
532
|
+
case "email": return "player@example.com";
|
|
533
|
+
case "phone": return "13800138000";
|
|
534
|
+
case "url": return "https://example.com/api";
|
|
535
|
+
case "token": return "Bearer demo-token";
|
|
536
|
+
case "uuid": return "550e8400-e29b-41d4-a716-446655440000";
|
|
537
|
+
case "host": return "127.0.0.1";
|
|
538
|
+
case "time": return String(Math.floor(Date.now() / 1000));
|
|
539
|
+
case "date": return new Date().toISOString().slice(0, 10);
|
|
540
|
+
case "version": return "1.0.0";
|
|
541
|
+
case "code": return "OK";
|
|
542
|
+
case "name": return name.replace(/name$/i, "") ? `${prettyLabel(name)}Demo` : "Demo";
|
|
543
|
+
case "text": return `${prettyLabel(name) || "示例"}说明`;
|
|
544
|
+
case "id": return `${prettyLabel(name) || "id"}_demo`;
|
|
545
|
+
default:
|
|
546
|
+
if (/lang|locale/.test(lower)) return "zh-CN";
|
|
547
|
+
if (/currency|coin/.test(lower)) return "CNY";
|
|
548
|
+
return prettyLabel(name) || "demo";
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function sampleNumber(name, lower, hint, is64) {
|
|
553
|
+
if (hint === "time") return Math.floor(Date.now() / 1000);
|
|
554
|
+
if (hint === "count") {
|
|
555
|
+
if (/page/.test(lower)) return 1;
|
|
556
|
+
if (/offset|index/.test(lower)) return 0;
|
|
557
|
+
if (/limit|size|len|cap/.test(lower)) return 20;
|
|
558
|
+
return 3;
|
|
559
|
+
}
|
|
560
|
+
if (hint === "price") return 100;
|
|
561
|
+
if (hint === "version") return 1;
|
|
562
|
+
if (hint === "code") return 0;
|
|
563
|
+
if (hint === "id" || /id$|_id/.test(lower) || lower === "id") return is64 ? "10001" : 10001;
|
|
564
|
+
if (/level|lv/.test(lower)) return 1;
|
|
565
|
+
if (/port/.test(lower)) return 19200;
|
|
566
|
+
if (/pos|slot|site/.test(lower)) return 1;
|
|
567
|
+
return is64 ? "1" : 1;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function prettyLabel(name) {
|
|
571
|
+
return String(name || "")
|
|
572
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
573
|
+
.replace(/[_-]+/g, " ")
|
|
574
|
+
.trim()
|
|
575
|
+
.replace(/\s+/g, "");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function generateMessage(rpc) {
|
|
579
|
+
const body = {};
|
|
580
|
+
for (const field of rpc.requestFields || []) {
|
|
581
|
+
body[field.name] = sampleValue(field);
|
|
582
|
+
}
|
|
583
|
+
return JSON.stringify(body, null, 2);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function formatMessage() {
|
|
587
|
+
try {
|
|
588
|
+
const obj = JSON.parse(els.messageEditor.value || "{}");
|
|
589
|
+
els.messageEditor.value = JSON.stringify(obj, null, 2);
|
|
590
|
+
state.messageDirty = true;
|
|
591
|
+
setRespBadge("ok", "Message 已格式化");
|
|
592
|
+
} catch (error) {
|
|
593
|
+
setRespBadge("err", `JSON 无法格式化:${error.message}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function paintDocs() {
|
|
598
|
+
const hit = state.selected ? findRpc(state.selected.id) : null;
|
|
599
|
+
if (!hit) {
|
|
600
|
+
els.docsView.innerHTML = `<div class="doc-empty">选择左侧接口后显示注释与请求/响应字段。</div>`;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const typeId = state.docsNav[state.docsNav.length - 1];
|
|
604
|
+
if (typeId && state.catalog.types?.[typeId]) renderTypeDoc(hit, typeId);
|
|
605
|
+
else renderDocs(hit.pkg, hit.service, hit.rpc);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function openType(id) {
|
|
609
|
+
if (!id || !state.catalog.types?.[id]) return;
|
|
610
|
+
state.docsNav.push(id);
|
|
611
|
+
setTab("docs");
|
|
612
|
+
paintDocs();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function typeLink(text, id) {
|
|
616
|
+
if (!id || !state.catalog.types?.[id]) return `<span class="doc-type">${esc(text)}</span>`;
|
|
617
|
+
return `<button class="doc-link" type="button" data-type="${esc(id)}">${esc(text)}</button>`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function fieldTypeHtml(field) {
|
|
621
|
+
const prefix = field.repeated ? "repeated " : field.optional ? "optional " : "";
|
|
622
|
+
const oneof = field.oneof ? `<span class="doc-chip">oneof ${esc(field.oneof)}</span> ` : "";
|
|
623
|
+
if (field.key) {
|
|
624
|
+
return `${oneof}<span class="doc-type">${esc(prefix)}map<</span>${typeLink(field.key, field.keyId)}<span class="doc-type">, </span>${typeLink(field.type, field.typeId)}<span class="doc-type">></span>`;
|
|
625
|
+
}
|
|
626
|
+
return `${oneof}${prefix ? `<span class="doc-type">${esc(prefix)}</span>` : ""}${typeLink(field.type, field.typeId)}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function fieldRows(fields) {
|
|
630
|
+
return (fields || []).map((field) => `<tr>
|
|
631
|
+
<td class="doc-num">${field.number}</td>
|
|
632
|
+
<td>${fieldTypeHtml(field)}</td>
|
|
633
|
+
<td class="doc-name">${esc(field.name)}</td>
|
|
634
|
+
<td class="doc-comment">${esc(field.comment || "")}</td>
|
|
635
|
+
</tr>`).join("") || `<tr><td colspan="4" class="doc-comment">无字段</td></tr>`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function sourceButton(path, line) {
|
|
639
|
+
if (!path) return "";
|
|
640
|
+
return `<button class="btn" type="button" data-src="${esc(path)}" data-line="${Number(line) || 1}">跳转到源码</button>`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function renderDocs(pkg, service, rpc) {
|
|
644
|
+
els.docsView.innerHTML = `<div class="doc-page">
|
|
645
|
+
<div class="doc-toolbar">${sourceButton(rpc.path || rpc.file, rpc.line)}</div>
|
|
646
|
+
<div class="doc-crumbs"><span>${esc(pkg.name)}</span><span>/</span><span>${esc(service.name)}</span><span>/</span><span>${esc(rpc.name)}</span></div>
|
|
647
|
+
<h2 class="doc-title">${esc(rpc.name)}</h2>
|
|
648
|
+
<p class="doc-lead">${esc(rpc.comment || "无注释")}</p>
|
|
649
|
+
<p class="doc-file">${esc(rpc.file || "")}${rpc.line ? `:${rpc.line}` : ""}</p>
|
|
650
|
+
<div class="doc-pair">
|
|
651
|
+
${docCard("请求", rpc.request, rpc.requestId, rpc.requestFields)}
|
|
652
|
+
${docCard("响应", rpc.response, rpc.responseId, rpc.responseFields)}
|
|
653
|
+
</div>
|
|
654
|
+
</div>`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function renderTypeDoc(hit, id) {
|
|
658
|
+
const type = state.catalog.types[id];
|
|
659
|
+
const back = `<button class="btn" type="button" data-doc-back="1">返回</button>`;
|
|
660
|
+
const crumbs = state.docsNav.map((item) => {
|
|
661
|
+
const node = state.catalog.types[item];
|
|
662
|
+
return node ? esc(node.fullName || node.name) : esc(item);
|
|
663
|
+
}).join(`<span>/</span>`);
|
|
664
|
+
const body = type.kind === "enum" ? enumTable(type) : `<div class="doc-card"><table>
|
|
665
|
+
<thead><tr><th>#</th><th>类型</th><th>字段</th><th>说明</th></tr></thead>
|
|
666
|
+
<tbody>${fieldRows(type.fields)}</tbody>
|
|
667
|
+
</table></div>${nestedList(type)}`;
|
|
668
|
+
els.docsView.innerHTML = `<div class="doc-page">
|
|
669
|
+
<div class="doc-toolbar">${back}${sourceButton(type.path || type.file, type.line)}</div>
|
|
670
|
+
<div class="doc-crumbs"><span>${esc(hit.pkg.name)}</span><span>/</span><span>${esc(hit.service.name)}</span><span>/</span><span>${esc(hit.rpc.name)}</span><span>/</span>${crumbs}</div>
|
|
671
|
+
<h2 class="doc-title">${esc(type.fullName || type.name)}</h2>
|
|
672
|
+
<p class="doc-lead">${esc(type.comment || (type.kind === "enum" ? "枚举" : "message"))}</p>
|
|
673
|
+
<p class="doc-file">${esc(type.file || "")}${type.line ? `:${type.line}` : ""}</p>
|
|
674
|
+
${body}
|
|
675
|
+
</div>`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function enumTable(type) {
|
|
679
|
+
const rows = (type.values || []).map((value) => `<tr>
|
|
680
|
+
<td class="doc-num">${value.number}</td>
|
|
681
|
+
<td class="doc-name">${esc(value.name)}</td>
|
|
682
|
+
<td class="doc-comment">${esc(value.comment || "")}</td>
|
|
683
|
+
</tr>`).join("") || `<tr><td colspan="3" class="doc-comment">无枚举项</td></tr>`;
|
|
684
|
+
return `<div class="doc-card"><table>
|
|
685
|
+
<thead><tr><th>#</th><th>名称</th><th>说明</th></tr></thead>
|
|
686
|
+
<tbody>${rows}</tbody>
|
|
687
|
+
</table></div>`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function nestedList(type) {
|
|
691
|
+
const items = type.nested || [];
|
|
692
|
+
if (!items.length) return "";
|
|
693
|
+
const rows = items.map((item) => `<button class="doc-row" type="button" data-type="${esc(item.id)}">
|
|
694
|
+
<span class="doc-name">${esc(item.name)}</span>
|
|
695
|
+
<span class="doc-comment">${esc(item.comment || item.kind)}</span>
|
|
696
|
+
</button>`).join("");
|
|
697
|
+
return `<div class="doc-nested"><div class="doc-kicker">嵌套定义</div>${rows}</div>`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function docCard(title, typeName, typeId, fields) {
|
|
701
|
+
return `<div class="doc-card">
|
|
702
|
+
<div class="doc-kicker">${esc(title)}</div>
|
|
703
|
+
<h3>${typeLink(typeName, typeId)}</h3>
|
|
704
|
+
<table>
|
|
705
|
+
<thead><tr><th>#</th><th>类型</th><th>字段</th><th>说明</th></tr></thead>
|
|
706
|
+
<tbody>${fieldRows(fields)}</tbody>
|
|
707
|
+
</table>
|
|
708
|
+
</div>`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function shellQuote(value) {
|
|
712
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function toBase64(text) {
|
|
716
|
+
try {
|
|
717
|
+
return btoa(unescape(encodeURIComponent(text)));
|
|
718
|
+
} catch {
|
|
719
|
+
return btoa(text);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function authMetadata() {
|
|
724
|
+
const auth = state.auth || defaultAuth();
|
|
725
|
+
const out = {};
|
|
726
|
+
switch (auth.type) {
|
|
727
|
+
case "bearer": {
|
|
728
|
+
const token = (auth.token || "").trim();
|
|
729
|
+
if (token) out.authorization = token.toLowerCase().startsWith("bearer ") ? token : `Bearer ${token}`;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case "basic": {
|
|
733
|
+
const user = auth.username || "";
|
|
734
|
+
const pass = auth.password || "";
|
|
735
|
+
if (user || pass) out.authorization = `Basic ${toBase64(`${user}:${pass}`)}`;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
case "apikey": {
|
|
739
|
+
const key = (auth.apiKey || "").trim();
|
|
740
|
+
if (key) out[key] = auth.apiValue ?? "";
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case "oauth2": {
|
|
744
|
+
const token = (auth.oauthToken || "").trim();
|
|
745
|
+
if (token) out.authorization = token.toLowerCase().startsWith("bearer ") ? token : `Bearer ${token}`;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case "wsse": {
|
|
749
|
+
const user = (auth.wsseUser || "").trim();
|
|
750
|
+
const pass = auth.wssePass || "";
|
|
751
|
+
if (user) {
|
|
752
|
+
const created = new Date().toISOString();
|
|
753
|
+
const nonce = Math.random().toString(36).slice(2, 14);
|
|
754
|
+
out["x-wsse"] = `UsernameToken Username="${user}", PasswordDigest="${toBase64(pass)}", Nonce="${nonce}", Created="${created}"`;
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
default:
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function requestMetadata() {
|
|
765
|
+
return { ...metadataObject(), ...authMetadata() };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function buildGrpcurl() {
|
|
769
|
+
const rpc = state.selected;
|
|
770
|
+
if (!rpc) return "";
|
|
771
|
+
const host = els.address.value.trim() || activeEnv()?.host || "localhost:50051";
|
|
772
|
+
let body = (els.messageEditor.value || "{}").trim();
|
|
773
|
+
try { body = JSON.stringify(JSON.parse(body)); } catch { /* keep raw */ }
|
|
774
|
+
const headers = Object.entries(requestMetadata())
|
|
775
|
+
.map(([name, value]) => ` -H ${shellQuote(`${name}: ${value ?? ""}`)}`);
|
|
776
|
+
const proto = rpc.file ? ` -proto ${shellQuote(rpc.file)}` : "";
|
|
777
|
+
const method = `${rpc.package}.${rpc.service}/${rpc.name}`;
|
|
778
|
+
return [
|
|
779
|
+
"grpcurl -plaintext \\",
|
|
780
|
+
...headers.map((line) => `${line} \\`),
|
|
781
|
+
` -d ${shellQuote(body)} \\`,
|
|
782
|
+
proto ? `${proto} \\` : "",
|
|
783
|
+
` ${shellQuote(host)} \\`,
|
|
784
|
+
` ${shellQuote(method)}`,
|
|
785
|
+
].filter(Boolean).join("\n");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function renderAuth() {
|
|
789
|
+
const auth = state.auth = normalizeAuth(state.auth);
|
|
790
|
+
const typeEl = $("authType");
|
|
791
|
+
if (typeEl) typeEl.value = auth.type || "none";
|
|
792
|
+
const body = $("authBody");
|
|
793
|
+
if (!body) return;
|
|
794
|
+
const field = (label, id, value, opts = {}) => `<label class="auth-field">
|
|
795
|
+
<span>${esc(label)}</span>
|
|
796
|
+
<input id="${esc(id)}" type="${opts.type || "text"}" value="${esc(value || "")}" placeholder="${esc(opts.placeholder || "")}" ${opts.autocomplete ? `autocomplete="${esc(opts.autocomplete)}"` : ""} />
|
|
797
|
+
</label>`;
|
|
798
|
+
let html = "";
|
|
799
|
+
switch (auth.type) {
|
|
800
|
+
case "bearer":
|
|
801
|
+
html = field("Token", "authToken", auth.token, { placeholder: "eyJhbGciOi..." });
|
|
802
|
+
break;
|
|
803
|
+
case "basic":
|
|
804
|
+
html = field("Username", "authUsername", auth.username, { autocomplete: "username" })
|
|
805
|
+
+ field("Password", "authPassword", auth.password, { type: "password", autocomplete: "current-password" });
|
|
806
|
+
break;
|
|
807
|
+
case "apikey":
|
|
808
|
+
html = field("Key", "authApiKey", auth.apiKey, { placeholder: "x-api-key" })
|
|
809
|
+
+ field("Value", "authApiValue", auth.apiValue, { placeholder: "your-api-key" });
|
|
810
|
+
break;
|
|
811
|
+
case "oauth2":
|
|
812
|
+
html = `<p class="auth-hint">仅粘贴已有 Access Token(不做授权码跳转)。发送时写入 authorization Bearer。</p>`
|
|
813
|
+
+ field("Access Token", "authOauthToken", auth.oauthToken, { placeholder: "access_token" });
|
|
814
|
+
break;
|
|
815
|
+
case "wsse":
|
|
816
|
+
html = field("Username", "authWsseUser", auth.wsseUser)
|
|
817
|
+
+ field("Password", "authWssePass", auth.wssePass, { type: "password" });
|
|
818
|
+
break;
|
|
819
|
+
default:
|
|
820
|
+
html = `<p class="auth-hint">当前不注入鉴权头。需要登录时可选择 Bearer / Basic / API Key 等;也可在 Metadata 里手动添加 authorization。</p>`;
|
|
821
|
+
}
|
|
822
|
+
body.innerHTML = html;
|
|
823
|
+
updateAuthPreview();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function readAuthForm() {
|
|
827
|
+
const auth = normalizeAuth(state.auth);
|
|
828
|
+
auth.type = $("authType")?.value || "none";
|
|
829
|
+
if ($("authToken")) auth.token = $("authToken").value;
|
|
830
|
+
if ($("authUsername")) auth.username = $("authUsername").value;
|
|
831
|
+
if ($("authPassword")) auth.password = $("authPassword").value;
|
|
832
|
+
if ($("authApiKey")) auth.apiKey = $("authApiKey").value;
|
|
833
|
+
if ($("authApiValue")) auth.apiValue = $("authApiValue").value;
|
|
834
|
+
if ($("authOauthToken")) auth.oauthToken = $("authOauthToken").value;
|
|
835
|
+
if ($("authWsseUser")) auth.wsseUser = $("authWsseUser").value;
|
|
836
|
+
if ($("authWssePass")) auth.wssePass = $("authWssePass").value;
|
|
837
|
+
state.auth = auth;
|
|
838
|
+
return auth;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function updateAuthPreview() {
|
|
842
|
+
const preview = $("authPreview");
|
|
843
|
+
if (!preview) return;
|
|
844
|
+
const injected = authMetadata();
|
|
845
|
+
const keys = Object.keys(injected);
|
|
846
|
+
if (!keys.length) {
|
|
847
|
+
preview.textContent = "将注入:无";
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
preview.textContent = `将注入:${keys.map((key) => `${key}: ${maskAuthValue(key, injected[key])}`).join(" · ")}`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function maskAuthValue(key, value) {
|
|
854
|
+
const text = String(value ?? "");
|
|
855
|
+
if (!text) return "";
|
|
856
|
+
if (/authorization|password|token|secret|wsse/i.test(key) || /Bearer |Basic /i.test(text)) {
|
|
857
|
+
if (text.length <= 12) return "••••";
|
|
858
|
+
return `${text.slice(0, 10)}…`;
|
|
859
|
+
}
|
|
860
|
+
return text.length > 48 ? `${text.slice(0, 48)}…` : text;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function renderMetadata() {
|
|
864
|
+
if (!state.metadata.length) state.metadata = [{ name: "", value: "", description: "" }];
|
|
865
|
+
els.metaBody.innerHTML = state.metadata.map((row, index) => `
|
|
866
|
+
<tr data-meta-index="${index}">
|
|
867
|
+
<td class="col-name"><input data-meta-key="name" placeholder="Name" value="${esc(row.name)}" /></td>
|
|
868
|
+
<td class="col-value"><input data-meta-key="value" placeholder="Value" value="${esc(row.value)}" /></td>
|
|
869
|
+
<td class="col-desc"><input data-meta-key="description" placeholder="Description" value="${esc(row.description)}" /></td>
|
|
870
|
+
<td class="col-act"><button class="link danger" type="button" data-meta-del="${index}">删除</button></td>
|
|
871
|
+
</tr>
|
|
872
|
+
`).join("");
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function metadataObject() {
|
|
876
|
+
const out = {};
|
|
877
|
+
for (const row of state.metadata) {
|
|
878
|
+
const name = (row.name || "").trim();
|
|
879
|
+
if (!name) continue;
|
|
880
|
+
out[name] = row.value ?? "";
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function setTab(tab) {
|
|
886
|
+
state.tab = tab;
|
|
887
|
+
for (const node of document.querySelectorAll(".tab")) {
|
|
888
|
+
node.classList.toggle("active", node.getAttribute("data-tab") === tab);
|
|
889
|
+
}
|
|
890
|
+
for (const node of document.querySelectorAll(".view")) {
|
|
891
|
+
node.classList.toggle("active", node.id === `view-${tab}`);
|
|
892
|
+
}
|
|
893
|
+
const messageTools = tab === "message";
|
|
894
|
+
$("genMsgBtn").hidden = !messageTools;
|
|
895
|
+
$("formatMsgBtn").hidden = !messageTools;
|
|
896
|
+
$("clearMsgBtn").hidden = !messageTools;
|
|
897
|
+
els.toolbarHint.textContent = tab === "message"
|
|
898
|
+
? "左侧为接口说明,右侧编辑请求 JSON"
|
|
899
|
+
: tab === "metadata"
|
|
900
|
+
? "表格编辑 metadata,支持 Bulk Edit"
|
|
901
|
+
: tab === "auth"
|
|
902
|
+
? "鉴权写入 Metadata;与表格同名键时以 Auth 为准"
|
|
903
|
+
: tab === "stress"
|
|
904
|
+
? "轻量压测或 ghz;结果在下方面板查看"
|
|
905
|
+
: "";
|
|
906
|
+
if (tab === "auth") renderAuth();
|
|
907
|
+
if (tab === "stress") {
|
|
908
|
+
setStressMode(state.stressMode);
|
|
909
|
+
refreshGhzCommand();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function persistEnvs() {
|
|
914
|
+
const payload = { activeEnvId: state.activeEnvId, envs: state.envs };
|
|
915
|
+
if (vscode) vscode.postMessage({ type: "saveEnvs", payload });
|
|
916
|
+
else localStorage.setItem("protoview.client.envs", JSON.stringify(payload));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function loadLocalEnvs() {
|
|
920
|
+
try {
|
|
921
|
+
const raw = localStorage.getItem("protoview.client.envs");
|
|
922
|
+
if (!raw) return;
|
|
923
|
+
const data = JSON.parse(raw);
|
|
924
|
+
if (Array.isArray(data.envs) && data.envs.length) {
|
|
925
|
+
state.envs = data.envs;
|
|
926
|
+
state.activeEnvId = data.activeEnvId || data.envs[0].id;
|
|
927
|
+
}
|
|
928
|
+
} catch {
|
|
929
|
+
// ignore
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function applyBootstrap(data) {
|
|
934
|
+
const prevSelected = state.selected?.id;
|
|
935
|
+
if (data.catalog) state.catalog = data.catalog;
|
|
936
|
+
if (data.envs && Array.isArray(data.envs.envs) && data.envs.envs.length) {
|
|
937
|
+
state.envs = data.envs.envs;
|
|
938
|
+
state.activeEnvId = data.envs.activeEnvId || state.envs[0].id;
|
|
939
|
+
}
|
|
940
|
+
if (data.source && typeof data.source === "object") {
|
|
941
|
+
const mode = data.source.mode === "git" ? "git" : "directory";
|
|
942
|
+
state.source = {
|
|
943
|
+
mode,
|
|
944
|
+
dirPath: data.source.dirPath || data.source.localPath || "",
|
|
945
|
+
gitUrl: data.source.gitUrl || "",
|
|
946
|
+
gitBranch: data.source.gitBranch || "main",
|
|
947
|
+
localPath: data.source.localPath || "",
|
|
948
|
+
lastError: data.source.lastError || "",
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
els.sourceMeta.textContent = state.catalog.sourceLabel || "ProtoView Client";
|
|
952
|
+
if (data.qr) state.qr = data.qr;
|
|
953
|
+
renderEnvSelect();
|
|
954
|
+
renderTree();
|
|
955
|
+
if (prevSelected && findRpc(prevSelected)) selectRpc(prevSelected);
|
|
956
|
+
else if (!state.selected) {
|
|
957
|
+
const first = state.catalog.packages?.[0]?.services?.[0]?.rpcs?.[0];
|
|
958
|
+
if (first) selectRpc(first.id);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function syncSourceModalUi() {
|
|
963
|
+
const mode = document.querySelector('input[name="sourceMode"]:checked')?.value || "directory";
|
|
964
|
+
$("sourceGitFields")?.classList.toggle("hidden", mode !== "git");
|
|
965
|
+
$("sourceDirFields")?.classList.toggle("hidden", mode !== "directory");
|
|
966
|
+
const pickBtn = $("pickSourceDirBtn");
|
|
967
|
+
if (pickBtn) pickBtn.classList.toggle("hidden", !vscode);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function bytesToBase64(bytes) {
|
|
971
|
+
const chunk = 0x8000;
|
|
972
|
+
let binary = "";
|
|
973
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
974
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
|
975
|
+
}
|
|
976
|
+
return btoa(binary);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function collectProtosFromDirHandle(dirHandle, prefix, out) {
|
|
980
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
981
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
982
|
+
if (handle.kind === "directory") {
|
|
983
|
+
await collectProtosFromDirHandle(handle, rel, out);
|
|
984
|
+
} else if (handle.kind === "file" && /\.proto$/i.test(name)) {
|
|
985
|
+
const file = await handle.getFile();
|
|
986
|
+
out.push({ path: rel.replace(/\\/g, "/"), content: await file.text() });
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** CLI:浏览器原生选目录(无完整路径,上传 .proto 到宿主临时目录) */
|
|
992
|
+
async function pickProtoDirViaBrowser() {
|
|
993
|
+
if (!vscode) return;
|
|
994
|
+
const files = [];
|
|
995
|
+
let label = "";
|
|
996
|
+
try {
|
|
997
|
+
if (typeof showDirectoryPicker === "function") {
|
|
998
|
+
const root = await showDirectoryPicker({ mode: "read" });
|
|
999
|
+
label = root.name || "proto";
|
|
1000
|
+
await collectProtosFromDirHandle(root, "", files);
|
|
1001
|
+
} else {
|
|
1002
|
+
await new Promise((resolve, reject) => {
|
|
1003
|
+
const input = document.createElement("input");
|
|
1004
|
+
input.type = "file";
|
|
1005
|
+
input.multiple = true;
|
|
1006
|
+
input.setAttribute("webkitdirectory", "");
|
|
1007
|
+
input.style.display = "none";
|
|
1008
|
+
input.addEventListener("change", async () => {
|
|
1009
|
+
try {
|
|
1010
|
+
const list = Array.from(input.files || []);
|
|
1011
|
+
input.remove();
|
|
1012
|
+
for (const file of list) {
|
|
1013
|
+
if (!/\.proto$/i.test(file.name)) continue;
|
|
1014
|
+
const rel = String(file.webkitRelativePath || file.name).replace(/\\/g, "/");
|
|
1015
|
+
if (!label) label = rel.split("/")[0] || file.name;
|
|
1016
|
+
files.push({ path: rel, content: await file.text() });
|
|
1017
|
+
}
|
|
1018
|
+
resolve();
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
reject(error);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
input.addEventListener("cancel", () => {
|
|
1024
|
+
input.remove();
|
|
1025
|
+
resolve();
|
|
1026
|
+
});
|
|
1027
|
+
document.body.appendChild(input);
|
|
1028
|
+
input.click();
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (error && error.name === "AbortError") return;
|
|
1033
|
+
setRespBadge("err", (error && error.message) || "选择目录失败");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (!files.length) {
|
|
1037
|
+
setRespBadge("err", "目录中未找到 .proto 文件");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
setRespBadge("wait", `正在导入 ${files.length} 个 .proto…`);
|
|
1041
|
+
vscode.postMessage({
|
|
1042
|
+
type: "uploadProtoDir",
|
|
1043
|
+
payload: { name: label || "proto", files },
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function pickGhzViaBrowser() {
|
|
1048
|
+
if (!vscode) return;
|
|
1049
|
+
try {
|
|
1050
|
+
let file;
|
|
1051
|
+
if (typeof showOpenFilePicker === "function") {
|
|
1052
|
+
const handles = await showOpenFilePicker({
|
|
1053
|
+
multiple: false,
|
|
1054
|
+
excludeAcceptAllOption: false,
|
|
1055
|
+
});
|
|
1056
|
+
file = await handles[0].getFile();
|
|
1057
|
+
} else {
|
|
1058
|
+
file = await new Promise((resolve) => {
|
|
1059
|
+
const input = document.createElement("input");
|
|
1060
|
+
input.type = "file";
|
|
1061
|
+
input.style.display = "none";
|
|
1062
|
+
input.addEventListener("change", () => {
|
|
1063
|
+
const f = input.files?.[0];
|
|
1064
|
+
input.remove();
|
|
1065
|
+
resolve(f || null);
|
|
1066
|
+
});
|
|
1067
|
+
input.addEventListener("cancel", () => {
|
|
1068
|
+
input.remove();
|
|
1069
|
+
resolve(null);
|
|
1070
|
+
});
|
|
1071
|
+
document.body.appendChild(input);
|
|
1072
|
+
input.click();
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (!file) return;
|
|
1076
|
+
setRespBadge("wait", "正在导入 ghz…");
|
|
1077
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
1078
|
+
vscode.postMessage({
|
|
1079
|
+
type: "uploadGhzBinary",
|
|
1080
|
+
payload: { name: file.name, base64: bytesToBase64(buf) },
|
|
1081
|
+
});
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (error && error.name === "AbortError") return;
|
|
1084
|
+
setRespBadge("err", (error && error.message) || "选择文件失败");
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function fillSourceModal() {
|
|
1089
|
+
const mode = state.source.mode === "git" ? "git" : "directory";
|
|
1090
|
+
for (const input of document.querySelectorAll('input[name="sourceMode"]')) {
|
|
1091
|
+
input.checked = input.value === mode;
|
|
1092
|
+
}
|
|
1093
|
+
if ($("sourceDirPath")) $("sourceDirPath").value = state.source.dirPath || "";
|
|
1094
|
+
if ($("sourceGitUrl")) $("sourceGitUrl").value = state.source.gitUrl || "";
|
|
1095
|
+
if ($("sourceGitBranch")) $("sourceGitBranch").value = state.source.gitBranch || "main";
|
|
1096
|
+
const dirHint = $("sourceDirHint");
|
|
1097
|
+
if (dirHint) {
|
|
1098
|
+
dirHint.textContent = state.source.dirPath
|
|
1099
|
+
? `当前:${state.source.dirPath}`
|
|
1100
|
+
: (isVsCodeWebview
|
|
1101
|
+
? "填写或浏览选择含 .proto 的目录。"
|
|
1102
|
+
: "浏览用浏览器选目录(导入 .proto);也可直接填本机绝对路径。");
|
|
1103
|
+
}
|
|
1104
|
+
const hint = $("sourceGitHint");
|
|
1105
|
+
if (hint) {
|
|
1106
|
+
const parts = [];
|
|
1107
|
+
if (state.source.localPath) parts.push(`临时目录:${state.source.localPath}`);
|
|
1108
|
+
if (state.source.lastError) parts.push(`上次错误:${state.source.lastError}`);
|
|
1109
|
+
hint.textContent = parts.join(" · ") || "应用后会 clone/fetch 到系统临时目录并刷新接口树。";
|
|
1110
|
+
}
|
|
1111
|
+
syncSourceModalUi();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function openSourceModal() {
|
|
1115
|
+
fillSourceModal();
|
|
1116
|
+
openModal(els.sourceModal);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function applySourceFromModal() {
|
|
1120
|
+
const mode = document.querySelector('input[name="sourceMode"]:checked')?.value === "git" ? "git" : "directory";
|
|
1121
|
+
const dirPath = ($("sourceDirPath")?.value || "").trim();
|
|
1122
|
+
const gitUrl = ($("sourceGitUrl")?.value || "").trim();
|
|
1123
|
+
const gitBranch = ($("sourceGitBranch")?.value || "main").trim() || "main";
|
|
1124
|
+
if (mode === "directory") {
|
|
1125
|
+
if (!dirPath) {
|
|
1126
|
+
setRespBadge("err", "请填写目录路径");
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (mode === "git") {
|
|
1131
|
+
if (!gitUrl) {
|
|
1132
|
+
setRespBadge("err", "请填写 Git 地址");
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (!/^https?:\/\/|^git@|^ssh:\/\//i.test(gitUrl)) {
|
|
1136
|
+
setRespBadge("err", "Git 地址格式无效");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
state.source = { ...state.source, mode, dirPath, gitUrl, gitBranch, lastError: "" };
|
|
1141
|
+
closeModal(els.sourceModal);
|
|
1142
|
+
if (!vscode) {
|
|
1143
|
+
els.sourceMeta.textContent = mode === "git"
|
|
1144
|
+
? `Git · ${gitBranch} · ${gitUrl}`
|
|
1145
|
+
: `目录 · ${dirPath}`;
|
|
1146
|
+
setRespBadge("wait", "未连接宿主:请用扩展打开 Client,或运行 npx @zhhoceanfly/protoview");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
setRespBadge("wait", mode === "git" ? "正在应用 Git 来源…" : "正在加载目录…");
|
|
1150
|
+
vscode.postMessage({
|
|
1151
|
+
type: "setSource",
|
|
1152
|
+
payload: { mode, dirPath, gitUrl, gitBranch },
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function openModal(el) { el.classList.add("show"); }
|
|
1157
|
+
function closeModal(el) { el.classList.remove("show"); }
|
|
1158
|
+
|
|
1159
|
+
function pushHistory(entry) {
|
|
1160
|
+
state.history.unshift(entry);
|
|
1161
|
+
state.history = state.history.slice(0, 30);
|
|
1162
|
+
els.historyView.textContent = state.history.map((item, i) =>
|
|
1163
|
+
`${i + 1}. ${item.time} ${item.package ? `${item.package}.${item.service}/` : ""}${item.method}\n ${item.host}\n`).join("\n") || "暂无历史";
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// events
|
|
1167
|
+
initTheme();
|
|
1168
|
+
els.themeBtn.addEventListener("click", toggleTheme);
|
|
1169
|
+
$("aiBtn").addEventListener("click", openAi);
|
|
1170
|
+
$("closeAiBtn").addEventListener("click", closeAi);
|
|
1171
|
+
$("aiShuffle").addEventListener("click", () => {
|
|
1172
|
+
if (ai.solved) return;
|
|
1173
|
+
ai.tiles = shuffleTiles(ai.target.slice());
|
|
1174
|
+
ai.selected = -1;
|
|
1175
|
+
renderAiBoard();
|
|
1176
|
+
});
|
|
1177
|
+
$("refreshBtn").addEventListener("click", () => {
|
|
1178
|
+
if (vscode) vscode.postMessage({ type: "refresh" });
|
|
1179
|
+
else location.reload();
|
|
1180
|
+
});
|
|
1181
|
+
$("sourceConfigBtn")?.addEventListener("click", openSourceModal);
|
|
1182
|
+
$("closeSourceModal")?.addEventListener("click", () => closeModal(els.sourceModal));
|
|
1183
|
+
$("applySourceBtn")?.addEventListener("click", applySourceFromModal);
|
|
1184
|
+
$("pickSourceDirBtn")?.addEventListener("click", () => {
|
|
1185
|
+
if (!vscode) return;
|
|
1186
|
+
if (isVsCodeWebview) vscode.postMessage({ type: "pickProtoDir" });
|
|
1187
|
+
else void pickProtoDirViaBrowser();
|
|
1188
|
+
});
|
|
1189
|
+
for (const input of document.querySelectorAll('input[name="sourceMode"]')) {
|
|
1190
|
+
input.addEventListener("change", syncSourceModalUi);
|
|
1191
|
+
}
|
|
1192
|
+
els.sourceModal?.addEventListener("click", (event) => {
|
|
1193
|
+
if (event.target === els.sourceModal) closeModal(els.sourceModal);
|
|
1194
|
+
});
|
|
1195
|
+
$("treeToggleBtn").addEventListener("click", () => setAllExpanded(!treeIsOpen()));
|
|
1196
|
+
$("grpcurlBtn").addEventListener("click", () => {
|
|
1197
|
+
if (!state.selected) return;
|
|
1198
|
+
$("curlText").value = buildGrpcurl();
|
|
1199
|
+
openModal($("curlModal"));
|
|
1200
|
+
});
|
|
1201
|
+
$("closeCurlModal").addEventListener("click", () => closeModal($("curlModal")));
|
|
1202
|
+
$("copyCurlBtn").addEventListener("click", async () => {
|
|
1203
|
+
const text = $("curlText").value;
|
|
1204
|
+
try {
|
|
1205
|
+
await navigator.clipboard.writeText(text);
|
|
1206
|
+
} catch {
|
|
1207
|
+
$("curlText").select();
|
|
1208
|
+
document.execCommand("copy");
|
|
1209
|
+
}
|
|
1210
|
+
setRespBadge("ok", "grpcurl 命令已复制");
|
|
1211
|
+
});
|
|
1212
|
+
els.filter.addEventListener("input", renderTree);
|
|
1213
|
+
els.envSelect.addEventListener("change", () => applyEnvId(els.envSelect.value));
|
|
1214
|
+
$("protocolSelect")?.addEventListener("change", () => {
|
|
1215
|
+
const protocol = currentProtocol();
|
|
1216
|
+
if (protocol === "sse" && $("httpMethodSelect") && !$("httpMethodSelect").dataset.userSet) {
|
|
1217
|
+
$("httpMethodSelect").value = "GET";
|
|
1218
|
+
}
|
|
1219
|
+
syncProtocolUi();
|
|
1220
|
+
persistCurrentDraft();
|
|
1221
|
+
});
|
|
1222
|
+
$("httpMethodSelect")?.addEventListener("change", persistCurrentDraft);
|
|
1223
|
+
$("httpPath")?.addEventListener("change", persistCurrentDraft);
|
|
1224
|
+
els.address.addEventListener("change", () => {
|
|
1225
|
+
const env = activeEnv();
|
|
1226
|
+
if (!env) return;
|
|
1227
|
+
env.host = els.address.value.trim();
|
|
1228
|
+
fillEnvForm();
|
|
1229
|
+
persistEnvs();
|
|
1230
|
+
});
|
|
1231
|
+
function openSourceFromUi(src) {
|
|
1232
|
+
const file = src.getAttribute("data-src");
|
|
1233
|
+
const line = Number(src.getAttribute("data-line") || 1);
|
|
1234
|
+
if (vscode) vscode.postMessage({ type: "openSource", file, line });
|
|
1235
|
+
else setRespBadge("err", "独立浏览器模式无法打开 VS Code 源码");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
els.rpcInfo.addEventListener("click", (event) => {
|
|
1239
|
+
const src = event.target.closest("[data-src]");
|
|
1240
|
+
if (src) openSourceFromUi(src);
|
|
1241
|
+
});
|
|
1242
|
+
els.docsView.addEventListener("click", (event) => {
|
|
1243
|
+
const back = event.target.closest("[data-doc-back]");
|
|
1244
|
+
if (back) {
|
|
1245
|
+
state.docsNav.pop();
|
|
1246
|
+
paintDocs();
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const src = event.target.closest("[data-src]");
|
|
1250
|
+
if (src) {
|
|
1251
|
+
openSourceFromUi(src);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const link = event.target.closest("[data-type]");
|
|
1255
|
+
if (link) openType(link.getAttribute("data-type"));
|
|
1256
|
+
});
|
|
1257
|
+
els.tree.addEventListener("click", (event) => {
|
|
1258
|
+
if (window.__protoviewFlowDragged) {
|
|
1259
|
+
window.__protoviewFlowDragged = false;
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const toggle = event.target.closest("[data-toggle]");
|
|
1263
|
+
if (toggle) {
|
|
1264
|
+
const key = toggle.getAttribute("data-toggle");
|
|
1265
|
+
if (state.expanded.has(key)) state.expanded.delete(key);
|
|
1266
|
+
else state.expanded.add(key);
|
|
1267
|
+
renderTree();
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const rpc = event.target.closest("[data-rpc]");
|
|
1271
|
+
if (rpc) selectRpc(rpc.getAttribute("data-rpc"));
|
|
1272
|
+
});
|
|
1273
|
+
els.tree.addEventListener("dblclick", (event) => {
|
|
1274
|
+
const rpc = event.target.closest("[data-rpc]");
|
|
1275
|
+
if (!rpc) return;
|
|
1276
|
+
const rpcId = rpc.getAttribute("data-rpc");
|
|
1277
|
+
const label = rpc.querySelector(".label")?.textContent || rpcId;
|
|
1278
|
+
placeFlowRpc(rpcId, label);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// ProcessOn-style: press on sidebar RPC, drag onto canvas, release to place
|
|
1282
|
+
let flowDrag = null;
|
|
1283
|
+
function isFlowMode() {
|
|
1284
|
+
return document.getElementById("app")?.classList.contains("flow-mode");
|
|
1285
|
+
}
|
|
1286
|
+
function placeFlowRpc(rpcId, label, clientX, clientY) {
|
|
1287
|
+
if (!rpcId) return;
|
|
1288
|
+
if (window.ProtoViewFlow?.addAtScreen && Number.isFinite(clientX) && Number.isFinite(clientY)) {
|
|
1289
|
+
window.ProtoViewFlow.addAtScreen(rpcId, clientX, clientY, label);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (window.ProtoViewFlow?.addNode) {
|
|
1293
|
+
window.ProtoViewFlow.addNode(rpcId, undefined, undefined, label);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
window.dispatchEvent(new CustomEvent("protoview-flow-add", {
|
|
1297
|
+
detail: { rpcId, label, clientX, clientY },
|
|
1298
|
+
}));
|
|
1299
|
+
}
|
|
1300
|
+
document.addEventListener("mousedown", (event) => {
|
|
1301
|
+
if (!isFlowMode() || event.button !== 0) return;
|
|
1302
|
+
const rpc = event.target.closest?.("[data-rpc]");
|
|
1303
|
+
if (!rpc || !els.tree.contains(rpc)) return;
|
|
1304
|
+
flowDrag = {
|
|
1305
|
+
rpcId: rpc.getAttribute("data-rpc"),
|
|
1306
|
+
label: rpc.querySelector(".label")?.textContent || rpc.getAttribute("data-rpc"),
|
|
1307
|
+
x: event.clientX,
|
|
1308
|
+
y: event.clientY,
|
|
1309
|
+
active: false,
|
|
1310
|
+
ghost: null,
|
|
1311
|
+
};
|
|
1312
|
+
}, true);
|
|
1313
|
+
document.addEventListener("mousemove", (event) => {
|
|
1314
|
+
if (!flowDrag) return;
|
|
1315
|
+
const dist = Math.hypot(event.clientX - flowDrag.x, event.clientY - flowDrag.y);
|
|
1316
|
+
if (!flowDrag.active && dist > 6) {
|
|
1317
|
+
flowDrag.active = true;
|
|
1318
|
+
flowDrag.ghost = document.createElement("div");
|
|
1319
|
+
flowDrag.ghost.className = "flow-drag-ghost";
|
|
1320
|
+
flowDrag.ghost.innerHTML = `<span class="flow-node-icon">⇢</span><strong></strong>`;
|
|
1321
|
+
flowDrag.ghost.querySelector("strong").textContent = flowDrag.label;
|
|
1322
|
+
document.body.appendChild(flowDrag.ghost);
|
|
1323
|
+
document.getElementById("flowStage")?.classList.add("drop-target");
|
|
1324
|
+
}
|
|
1325
|
+
if (flowDrag.ghost) {
|
|
1326
|
+
flowDrag.ghost.style.left = `${event.clientX + 14}px`;
|
|
1327
|
+
flowDrag.ghost.style.top = `${event.clientY + 14}px`;
|
|
1328
|
+
}
|
|
1329
|
+
}, true);
|
|
1330
|
+
document.addEventListener("mouseup", (event) => {
|
|
1331
|
+
if (!flowDrag) return;
|
|
1332
|
+
const drag = flowDrag;
|
|
1333
|
+
flowDrag = null;
|
|
1334
|
+
document.getElementById("flowStage")?.classList.remove("drop-target");
|
|
1335
|
+
if (drag.ghost) drag.ghost.remove();
|
|
1336
|
+
if (!drag.active) return;
|
|
1337
|
+
window.__protoviewFlowDragged = true;
|
|
1338
|
+
const stage = document.getElementById("flowStage");
|
|
1339
|
+
if (!stage) return;
|
|
1340
|
+
const rect = stage.getBoundingClientRect();
|
|
1341
|
+
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
placeFlowRpc(drag.rpcId, drag.label, event.clientX, event.clientY);
|
|
1345
|
+
}, true);
|
|
1346
|
+
$("tabs").addEventListener("click", (event) => {
|
|
1347
|
+
const tab = event.target.closest("[data-tab]");
|
|
1348
|
+
if (tab) setTab(tab.getAttribute("data-tab"));
|
|
1349
|
+
});
|
|
1350
|
+
$("genMsgBtn").addEventListener("click", () => {
|
|
1351
|
+
if (!state.selected) return;
|
|
1352
|
+
els.messageEditor.value = generateMessage(state.selected);
|
|
1353
|
+
state.messageDirty = true;
|
|
1354
|
+
});
|
|
1355
|
+
$("formatMsgBtn").addEventListener("click", formatMessage);
|
|
1356
|
+
$("clearMsgBtn").addEventListener("click", () => {
|
|
1357
|
+
els.messageEditor.value = "{}";
|
|
1358
|
+
state.messageDirty = true;
|
|
1359
|
+
});
|
|
1360
|
+
els.messageEditor.addEventListener("input", () => { state.messageDirty = true; });
|
|
1361
|
+
$("clearRespBtn").addEventListener("click", () => {
|
|
1362
|
+
setRespBadge("", "");
|
|
1363
|
+
els.responseView.innerHTML = `<div class="resp-empty">选择接口,配置地址后发送。</div>`;
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
els.metaBody.addEventListener("input", (event) => {
|
|
1367
|
+
const input = event.target.closest("input[data-meta-key]");
|
|
1368
|
+
if (!input) return;
|
|
1369
|
+
const tr = input.closest("[data-meta-index]");
|
|
1370
|
+
const index = Number(tr.getAttribute("data-meta-index"));
|
|
1371
|
+
const key = input.getAttribute("data-meta-key");
|
|
1372
|
+
state.metadata[index][key] = input.value;
|
|
1373
|
+
state.messageDirty = true;
|
|
1374
|
+
persistCurrentDraft();
|
|
1375
|
+
});
|
|
1376
|
+
els.metaBody.addEventListener("click", (event) => {
|
|
1377
|
+
const del = event.target.closest("[data-meta-del]");
|
|
1378
|
+
if (!del) return;
|
|
1379
|
+
const index = Number(del.getAttribute("data-meta-del"));
|
|
1380
|
+
state.metadata.splice(index, 1);
|
|
1381
|
+
renderMetadata();
|
|
1382
|
+
persistCurrentDraft();
|
|
1383
|
+
});
|
|
1384
|
+
$("addMetaBtn").addEventListener("click", () => {
|
|
1385
|
+
state.metadata.push({ name: "", value: "", description: "" });
|
|
1386
|
+
renderMetadata();
|
|
1387
|
+
persistCurrentDraft();
|
|
1388
|
+
});
|
|
1389
|
+
$("bulkMetaBtn").addEventListener("click", () => {
|
|
1390
|
+
els.bulkText.value = state.metadata
|
|
1391
|
+
.filter((row) => row.name)
|
|
1392
|
+
.map((row) => `${row.name}: ${row.value}`)
|
|
1393
|
+
.join("\n");
|
|
1394
|
+
openModal(els.bulkModal);
|
|
1395
|
+
});
|
|
1396
|
+
$("closeBulkModal").addEventListener("click", () => closeModal(els.bulkModal));
|
|
1397
|
+
$("applyBulkBtn").addEventListener("click", () => {
|
|
1398
|
+
const rows = els.bulkText.value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
1399
|
+
const idx = line.indexOf(":");
|
|
1400
|
+
if (idx < 0) return { name: line, value: "", description: "" };
|
|
1401
|
+
return { name: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim(), description: "" };
|
|
1402
|
+
});
|
|
1403
|
+
state.metadata = rows.length ? rows : [{ name: "", value: "", description: "" }];
|
|
1404
|
+
renderMetadata();
|
|
1405
|
+
persistCurrentDraft();
|
|
1406
|
+
closeModal(els.bulkModal);
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
$("authType").addEventListener("change", () => {
|
|
1410
|
+
readAuthForm();
|
|
1411
|
+
renderAuth();
|
|
1412
|
+
persistCurrentDraft();
|
|
1413
|
+
});
|
|
1414
|
+
$("authBody").addEventListener("input", () => {
|
|
1415
|
+
readAuthForm();
|
|
1416
|
+
updateAuthPreview();
|
|
1417
|
+
persistCurrentDraft();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
$("envConfigBtn").addEventListener("click", () => {
|
|
1421
|
+
fillEnvForm();
|
|
1422
|
+
renderEnvList();
|
|
1423
|
+
openModal(els.envModal);
|
|
1424
|
+
});
|
|
1425
|
+
$("closeEnvModal").addEventListener("click", () => closeModal(els.envModal));
|
|
1426
|
+
els.envList.addEventListener("click", (event) => {
|
|
1427
|
+
const btn = event.target.closest("[data-pick-env]");
|
|
1428
|
+
if (!btn) return;
|
|
1429
|
+
state.activeEnvId = btn.getAttribute("data-pick-env");
|
|
1430
|
+
renderEnvSelect();
|
|
1431
|
+
});
|
|
1432
|
+
$("addEnvBtn").addEventListener("click", () => {
|
|
1433
|
+
const id = `env_${Date.now().toString(36)}`;
|
|
1434
|
+
state.envs.push({ id, name: `env-${state.envs.length + 1}`, host: "", note: "" });
|
|
1435
|
+
state.activeEnvId = id;
|
|
1436
|
+
renderEnvSelect();
|
|
1437
|
+
});
|
|
1438
|
+
$("saveEnvBtn").addEventListener("click", () => {
|
|
1439
|
+
const env = activeEnv();
|
|
1440
|
+
if (!env) return;
|
|
1441
|
+
env.name = els.envName.value.trim() || env.id;
|
|
1442
|
+
env.host = els.envHost.value.trim();
|
|
1443
|
+
env.note = els.envNote.value.trim();
|
|
1444
|
+
els.address.value = env.host;
|
|
1445
|
+
persistEnvs();
|
|
1446
|
+
renderEnvSelect();
|
|
1447
|
+
closeModal(els.envModal);
|
|
1448
|
+
setRespBadge("ok", `环境「${env.name}」已保存`);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
function setRespBadge(kind, text) {
|
|
1452
|
+
const badge = $("respBadge");
|
|
1453
|
+
badge.className = `resp-badge${kind ? ` ${kind}` : ""}`;
|
|
1454
|
+
badge.textContent = text || "";
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function highlightJson(value) {
|
|
1458
|
+
const text = esc(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
1459
|
+
return text.replace(
|
|
1460
|
+
/("(?:\\.|[^&])*?")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,
|
|
1461
|
+
(match, str, colon, bool) => {
|
|
1462
|
+
if (str) return `<span class="${colon ? "k" : "s"}">${str}</span>${colon || ""}`;
|
|
1463
|
+
if (bool) return `<span class="b">${bool}</span>`;
|
|
1464
|
+
return `<span class="n">${match}</span>`;
|
|
1465
|
+
},
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function showInvokeResult(data) {
|
|
1470
|
+
const kind = data.ok ? "ok" : "err";
|
|
1471
|
+
const timing = typeof data.ms === "number" ? ` · ${data.ms} ms` : "";
|
|
1472
|
+
setRespBadge(kind, `${data.status || (data.ok ? "OK" : "ERROR")}${timing}`);
|
|
1473
|
+
const parts = [`<div class="resp-line">${esc(data.method || "")}${data.host ? ` · ${esc(data.host)}` : ""}</div>`];
|
|
1474
|
+
if (data.error) parts.push(`<pre class="resp-error">${esc(data.error)}</pre>`);
|
|
1475
|
+
if (data.body !== undefined) parts.push(`<pre class="resp-json">${highlightJson(data.body)}</pre>`);
|
|
1476
|
+
els.responseView.innerHTML = `<div class="resp-block">${parts.join("")}</div>`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
els.sendBtn.addEventListener("click", () => {
|
|
1480
|
+
const rpc = state.selected;
|
|
1481
|
+
if (!rpc) return;
|
|
1482
|
+
const env = activeEnv();
|
|
1483
|
+
const host = els.address.value.trim() || env?.host || "";
|
|
1484
|
+
let body = {};
|
|
1485
|
+
try {
|
|
1486
|
+
body = JSON.parse(els.messageEditor.value || "{}");
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
showInvokeResult({ ok: false, status: "INVALID", method: `${rpc.package}.${rpc.service}/${rpc.name}`, host, error: `Message 不是合法 JSON:${error.message}` });
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const protocol = currentProtocol();
|
|
1492
|
+
const httpMethod = $("httpMethodSelect")?.value || (protocol === "sse" ? "GET" : "POST");
|
|
1493
|
+
const httpPath = $("httpPath")?.value || rpc.httpPath || "/";
|
|
1494
|
+
const payload = {
|
|
1495
|
+
id: String(Date.now()),
|
|
1496
|
+
time: new Date().toLocaleString(),
|
|
1497
|
+
protocol,
|
|
1498
|
+
httpMethod,
|
|
1499
|
+
httpPath,
|
|
1500
|
+
host,
|
|
1501
|
+
package: rpc.package,
|
|
1502
|
+
service: rpc.service,
|
|
1503
|
+
method: rpc.name,
|
|
1504
|
+
file: rpc.file,
|
|
1505
|
+
body,
|
|
1506
|
+
metadata: requestMetadata(),
|
|
1507
|
+
tls: $("tlsCheck").checked,
|
|
1508
|
+
message: els.messageEditor.value,
|
|
1509
|
+
};
|
|
1510
|
+
state.pendingInvoke = payload.id;
|
|
1511
|
+
pushHistory(payload);
|
|
1512
|
+
persistCurrentDraft();
|
|
1513
|
+
if (!vscode) {
|
|
1514
|
+
showInvokeResult({
|
|
1515
|
+
ok: false,
|
|
1516
|
+
status: "UNAVAILABLE",
|
|
1517
|
+
method: `${rpc.package}.${rpc.service}/${rpc.name}`,
|
|
1518
|
+
host,
|
|
1519
|
+
error: "浏览器页面不能直接发送原生 gRPC。请在 VS Code 里打开 ProtoView Client,由插件进程代发。",
|
|
1520
|
+
});
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
setRespBadge("wait", "发送中…");
|
|
1524
|
+
const label = protocol === "grpc"
|
|
1525
|
+
? `${rpc.package}.${rpc.service}/${rpc.name}`
|
|
1526
|
+
: protocol === "websocket"
|
|
1527
|
+
? `WS ${httpPath}`
|
|
1528
|
+
: protocol === "sse"
|
|
1529
|
+
? `SSE ${httpMethod} ${httpPath}`
|
|
1530
|
+
: `${httpMethod} ${httpPath}`;
|
|
1531
|
+
els.responseView.innerHTML = `<div class="resp-empty">正在请求 ${esc(host || "(未配置)")} · ${esc(label)}</div>`;
|
|
1532
|
+
vscode.postMessage({ type: "invoke", payload });
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
function currentInvokeBase() {
|
|
1536
|
+
const rpc = state.selected;
|
|
1537
|
+
if (!rpc) return null;
|
|
1538
|
+
let body = {};
|
|
1539
|
+
try { body = JSON.parse(els.messageEditor.value || "{}"); }
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
return { error: `Message 不是合法 JSON:${error.message}` };
|
|
1542
|
+
}
|
|
1543
|
+
const protocol = currentProtocol();
|
|
1544
|
+
return {
|
|
1545
|
+
protocol,
|
|
1546
|
+
httpMethod: $("httpMethodSelect")?.value || (protocol === "sse" ? "GET" : rpc.httpVerb || "POST"),
|
|
1547
|
+
httpPath: $("httpPath")?.value || rpc.httpPath || "/",
|
|
1548
|
+
host: els.address.value.trim() || activeEnv()?.host || "",
|
|
1549
|
+
package: rpc.package,
|
|
1550
|
+
service: rpc.service,
|
|
1551
|
+
method: rpc.name,
|
|
1552
|
+
file: rpc.file,
|
|
1553
|
+
body,
|
|
1554
|
+
metadata: requestMetadata(),
|
|
1555
|
+
tls: $("tlsCheck").checked,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function setStressMode(mode) {
|
|
1560
|
+
state.stressMode = mode === "ghz" ? "ghz" : "light";
|
|
1561
|
+
for (const btn of document.querySelectorAll("[data-stress-mode]")) {
|
|
1562
|
+
btn.classList.toggle("active", btn.getAttribute("data-stress-mode") === state.stressMode);
|
|
1563
|
+
}
|
|
1564
|
+
$("stressLight").classList.toggle("hidden", state.stressMode !== "light");
|
|
1565
|
+
$("stressGhz").classList.toggle("hidden", state.stressMode !== "ghz");
|
|
1566
|
+
$("stressNote").textContent = state.stressMode === "light"
|
|
1567
|
+
? "复用当前 Message / Auth / Metadata,在插件进程内有限并发调用(并发≤10,总数≤200)。"
|
|
1568
|
+
: "高并发请用本机 ghz。可复制命令,或尝试由插件 spawn(需 PATH 中有 ghz)。";
|
|
1569
|
+
if (state.stressMode === "ghz") refreshGhzCommand();
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function refreshGhzCommand() {
|
|
1573
|
+
const area = $("ghzCommand");
|
|
1574
|
+
if (!area) return;
|
|
1575
|
+
area.value = buildGhzCommand();
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function buildGhzArgs() {
|
|
1579
|
+
const base = currentInvokeBase();
|
|
1580
|
+
if (!base || base.error || base.protocol !== "grpc") return null;
|
|
1581
|
+
const rpc = state.selected;
|
|
1582
|
+
const concurrency = Math.max(1, Number($("ghzConcurrency").value || 20));
|
|
1583
|
+
const total = Math.max(1, Number($("ghzTotal").value || 200));
|
|
1584
|
+
const method = `${rpc.package}.${rpc.service}/${rpc.name}`;
|
|
1585
|
+
const args = [
|
|
1586
|
+
"--insecure",
|
|
1587
|
+
"-c", String(concurrency),
|
|
1588
|
+
"-n", String(total),
|
|
1589
|
+
"-d", JSON.stringify(base.body),
|
|
1590
|
+
];
|
|
1591
|
+
for (const [name, value] of Object.entries(base.metadata || {})) {
|
|
1592
|
+
args.push("-m", `${name}: ${value}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (rpc.path || rpc.file) {
|
|
1595
|
+
args.push("-proto", rpc.path || rpc.file);
|
|
1596
|
+
const importPath = (rpc.path || rpc.file).replace(/[\\/][^\\/]+$/, "") || ".";
|
|
1597
|
+
args.push("-i", importPath);
|
|
1598
|
+
}
|
|
1599
|
+
args.push(base.host || "127.0.0.1:50051", method);
|
|
1600
|
+
return args;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function ghzBinary() {
|
|
1604
|
+
return ($("ghzPath")?.value || "").trim() || "ghz";
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function buildGhzCommand() {
|
|
1608
|
+
const args = buildGhzArgs();
|
|
1609
|
+
if (!args) return "# 请先选择接口并填写合法 Message JSON";
|
|
1610
|
+
return `${shellQuote(ghzBinary())} ${args.map((part) => shellQuote(part)).join(" ")}`;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function setStressRunning(running) {
|
|
1614
|
+
$("stressStartBtn").disabled = running;
|
|
1615
|
+
$("stressStopBtn").disabled = !running;
|
|
1616
|
+
$("runGhzBtn").disabled = running;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function showStressProgress(data) {
|
|
1620
|
+
const pct = data.total ? Math.round((data.done / data.total) * 100) : 0;
|
|
1621
|
+
$("stressProgressBar").style.width = `${pct}%`;
|
|
1622
|
+
$("stressProgressText").textContent = `${data.done}/${data.total} · 成功 ${data.ok} · 失败 ${data.fail}`;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function showStressResult(data) {
|
|
1626
|
+
setStressRunning(false);
|
|
1627
|
+
if (data.error) {
|
|
1628
|
+
els.stressResult.innerHTML = `<div class="stress-empty stress-fail">${esc(data.error)}</div>`;
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const stats = [
|
|
1632
|
+
["总数", data.total],
|
|
1633
|
+
["成功", data.ok],
|
|
1634
|
+
["失败", data.fail],
|
|
1635
|
+
["耗时", `${data.durationMs} ms`],
|
|
1636
|
+
["RPS", data.rps],
|
|
1637
|
+
["平均", `${data.avgMs} ms`],
|
|
1638
|
+
["P50", `${data.p50Ms} ms`],
|
|
1639
|
+
["P95", `${data.p95Ms} ms`],
|
|
1640
|
+
["P99", `${data.p99Ms} ms`],
|
|
1641
|
+
["最小", `${data.minMs} ms`],
|
|
1642
|
+
["最大", `${data.maxMs} ms`],
|
|
1643
|
+
].map(([label, value]) => `<div class="stress-stat"><b>${esc(String(value))}</b><span>${esc(label)}</span></div>`).join("");
|
|
1644
|
+
const rows = (data.samples || []).map((item) => `<tr>
|
|
1645
|
+
<td>#${item.index + 1}</td>
|
|
1646
|
+
<td class="${item.ok ? "stress-ok" : "stress-fail"}">${esc(item.status)}</td>
|
|
1647
|
+
<td>${item.ms} ms</td>
|
|
1648
|
+
<td>${esc(item.error || "")}</td>
|
|
1649
|
+
</tr>`).join("");
|
|
1650
|
+
els.stressResult.innerHTML = `
|
|
1651
|
+
<div class="stress-stats">${stats}</div>
|
|
1652
|
+
${data.cancelled ? `<p class="stress-note">已停止,以下为已完成样本。</p>` : ""}
|
|
1653
|
+
<table class="stress-samples">
|
|
1654
|
+
<thead><tr><th>#</th><th>状态</th><th>延迟</th><th>错误</th></tr></thead>
|
|
1655
|
+
<tbody>${rows || `<tr><td colspan="4">无样本</td></tr>`}</tbody>
|
|
1656
|
+
</table>
|
|
1657
|
+
<p class="stress-note" style="margin-top:8px">仅展示前 50 条样本明细。</p>
|
|
1658
|
+
`;
|
|
1659
|
+
setRespBadge(data.fail ? "err" : "ok", `压测完成 · RPS ${data.rps}`);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
$("stressModes").addEventListener("click", (event) => {
|
|
1663
|
+
const btn = event.target.closest("[data-stress-mode]");
|
|
1664
|
+
if (btn) setStressMode(btn.getAttribute("data-stress-mode"));
|
|
1665
|
+
});
|
|
1666
|
+
$("stressConcurrency").addEventListener("change", () => {
|
|
1667
|
+
const n = Math.min(10, Math.max(1, Number($("stressConcurrency").value || 1)));
|
|
1668
|
+
$("stressConcurrency").value = String(n);
|
|
1669
|
+
});
|
|
1670
|
+
$("stressTotal").addEventListener("change", () => {
|
|
1671
|
+
const n = Math.min(200, Math.max(1, Number($("stressTotal").value || 1)));
|
|
1672
|
+
$("stressTotal").value = String(n);
|
|
1673
|
+
});
|
|
1674
|
+
$("ghzConcurrency").addEventListener("change", refreshGhzCommand);
|
|
1675
|
+
$("ghzTotal").addEventListener("change", refreshGhzCommand);
|
|
1676
|
+
const GHZ_PATH_KEY = "protoview.client.ghzPath";
|
|
1677
|
+
try { $("ghzPath").value = localStorage.getItem(GHZ_PATH_KEY) || ""; } catch { /* ignore */ }
|
|
1678
|
+
function setGhzPath(value) {
|
|
1679
|
+
const path = String(value || "").trim();
|
|
1680
|
+
if ($("ghzPath")) $("ghzPath").value = path;
|
|
1681
|
+
try { localStorage.setItem(GHZ_PATH_KEY, path); } catch { /* ignore */ }
|
|
1682
|
+
refreshGhzCommand();
|
|
1683
|
+
}
|
|
1684
|
+
$("ghzPath")?.addEventListener("change", () => setGhzPath($("ghzPath").value));
|
|
1685
|
+
$("ghzPath")?.addEventListener("input", refreshGhzCommand);
|
|
1686
|
+
$("pickGhzPathBtn")?.addEventListener("click", () => {
|
|
1687
|
+
if (!vscode) return;
|
|
1688
|
+
if (isVsCodeWebview) vscode.postMessage({ type: "pickGhzBinary" });
|
|
1689
|
+
else void pickGhzViaBrowser();
|
|
1690
|
+
});
|
|
1691
|
+
$("stressStartBtn").addEventListener("click", () => {
|
|
1692
|
+
const base = currentInvokeBase();
|
|
1693
|
+
if (!base) return;
|
|
1694
|
+
if (base.error) {
|
|
1695
|
+
els.stressResult.innerHTML = `<div class="stress-empty stress-fail">${esc(base.error)}</div>`;
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (!vscode) {
|
|
1699
|
+
els.stressResult.innerHTML = `<div class="stress-empty stress-fail">请在 VS Code 中打开 ProtoView Client 进行压测。</div>`;
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
state.stressId = `stress-${Date.now()}`;
|
|
1703
|
+
setStressRunning(true);
|
|
1704
|
+
$("stressProgressBar").style.width = "0%";
|
|
1705
|
+
$("stressProgressText").textContent = "准备中…";
|
|
1706
|
+
els.stressResult.innerHTML = `<div class="stress-empty">压测进行中…</div>`;
|
|
1707
|
+
vscode.postMessage({
|
|
1708
|
+
type: "stress",
|
|
1709
|
+
payload: {
|
|
1710
|
+
...base,
|
|
1711
|
+
id: state.stressId,
|
|
1712
|
+
concurrency: Number($("stressConcurrency").value || 1),
|
|
1713
|
+
total: Number($("stressTotal").value || 1),
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
$("stressStopBtn").addEventListener("click", () => {
|
|
1718
|
+
if (state.stressId && vscode) vscode.postMessage({ type: "stressCancel", id: state.stressId });
|
|
1719
|
+
});
|
|
1720
|
+
$("copyGhzBtn").addEventListener("click", async () => {
|
|
1721
|
+
refreshGhzCommand();
|
|
1722
|
+
const text = $("ghzCommand").value;
|
|
1723
|
+
try { await navigator.clipboard.writeText(text); }
|
|
1724
|
+
catch { $("ghzCommand").select(); document.execCommand("copy"); }
|
|
1725
|
+
setRespBadge("ok", "ghz 命令已复制");
|
|
1726
|
+
});
|
|
1727
|
+
$("runGhzBtn").addEventListener("click", () => {
|
|
1728
|
+
const args = buildGhzArgs();
|
|
1729
|
+
if (!args) {
|
|
1730
|
+
els.stressResult.innerHTML = `<div class="stress-empty stress-fail">请先选择接口并填写合法 Message</div>`;
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (!vscode) {
|
|
1734
|
+
els.stressResult.innerHTML = `<div class="stress-empty stress-fail">请在 VS Code 中运行 ghz。</div>`;
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
state.stressId = `ghz-${Date.now()}`;
|
|
1738
|
+
state.ghzLog = "";
|
|
1739
|
+
setStressRunning(true);
|
|
1740
|
+
els.stressResult.innerHTML = `<pre class="stress-log" id="ghzLogView"></pre>`;
|
|
1741
|
+
vscode.postMessage({ type: "runGhz", payload: { id: state.stressId, args, binary: ghzBinary() } });
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
function invokeOnce(partial) {
|
|
1745
|
+
return new Promise((resolve) => {
|
|
1746
|
+
if (!vscode) {
|
|
1747
|
+
resolve({ ok: false, status: "UNAVAILABLE", ms: 0, error: "需要 VS Code Client" });
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const id = `flow-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
1751
|
+
const host = partial?.host || els.address.value.trim() || activeEnv()?.host || "";
|
|
1752
|
+
const handler = (event) => {
|
|
1753
|
+
const data = event.data;
|
|
1754
|
+
if (!data || data.type !== "invokeResult" || data.id !== id) return;
|
|
1755
|
+
window.removeEventListener("message", handler);
|
|
1756
|
+
resolve(data);
|
|
1757
|
+
};
|
|
1758
|
+
window.addEventListener("message", handler);
|
|
1759
|
+
vscode.postMessage({
|
|
1760
|
+
type: "invoke",
|
|
1761
|
+
payload: {
|
|
1762
|
+
id,
|
|
1763
|
+
host,
|
|
1764
|
+
tls: $("tlsCheck").checked,
|
|
1765
|
+
...partial,
|
|
1766
|
+
},
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
window.ProtoViewClient = {
|
|
1772
|
+
findRpc,
|
|
1773
|
+
getType(typeId) {
|
|
1774
|
+
if (!typeId) return undefined;
|
|
1775
|
+
return state.catalog.types?.[typeId];
|
|
1776
|
+
},
|
|
1777
|
+
sampleField(field) {
|
|
1778
|
+
try { return sampleValue(field); }
|
|
1779
|
+
catch { return ""; }
|
|
1780
|
+
},
|
|
1781
|
+
invokeOnce,
|
|
1782
|
+
postHost(type, payload) {
|
|
1783
|
+
if (!vscode) return false;
|
|
1784
|
+
vscode.postMessage(payload === undefined ? { type } : { type, payload });
|
|
1785
|
+
return true;
|
|
1786
|
+
},
|
|
1787
|
+
isVsCodeHost: () => isVsCodeWebview,
|
|
1788
|
+
getSelectedRpcId: () => state.selected?.id || "",
|
|
1789
|
+
getSelectedRpcLabel: () => state.selected?.name || "",
|
|
1790
|
+
listEnvs: () => state.envs.map((env) => ({ id: env.id, name: env.name, host: env.host || "" })),
|
|
1791
|
+
getActiveEnvId: () => state.activeEnvId,
|
|
1792
|
+
resolveHost(envId) {
|
|
1793
|
+
const env = state.envs.find((item) => item.id === envId) || activeEnv();
|
|
1794
|
+
return env?.host || "";
|
|
1795
|
+
},
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
window.addEventListener("message", (event) => {
|
|
1799
|
+
const data = event.data;
|
|
1800
|
+
if (!data) return;
|
|
1801
|
+
if (data.type === "bootstrap") applyBootstrap(data);
|
|
1802
|
+
if (data.type === "invokeResult") {
|
|
1803
|
+
if (state.pendingInvoke && data.id && data.id !== state.pendingInvoke) return;
|
|
1804
|
+
showInvokeResult(data);
|
|
1805
|
+
}
|
|
1806
|
+
if (data.type === "stressProgress" && data.id === state.stressId) showStressProgress(data);
|
|
1807
|
+
if (data.type === "stressResult" && (!data.id || data.id === state.stressId)) showStressResult(data);
|
|
1808
|
+
if (data.type === "ghzProgress" && data.id === state.stressId) {
|
|
1809
|
+
state.ghzLog += data.text || "";
|
|
1810
|
+
const view = $("ghzLogView");
|
|
1811
|
+
if (view) {
|
|
1812
|
+
view.textContent = state.ghzLog;
|
|
1813
|
+
view.scrollTop = view.scrollHeight;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (data.type === "ghzResult" && data.id === state.stressId) {
|
|
1817
|
+
state.ghzLog += data.text || "";
|
|
1818
|
+
setStressRunning(false);
|
|
1819
|
+
els.stressResult.innerHTML = `<pre class="stress-log">${esc(state.ghzLog)}</pre>`;
|
|
1820
|
+
setRespBadge(data.ok ? "ok" : "err", data.ok ? "ghz 完成" : "ghz 失败/未安装");
|
|
1821
|
+
}
|
|
1822
|
+
if (data.type === "pickedProtoDir" && data.path) {
|
|
1823
|
+
if ($("sourceDirPath")) $("sourceDirPath").value = String(data.path);
|
|
1824
|
+
const dirHint = $("sourceDirHint");
|
|
1825
|
+
if (dirHint) {
|
|
1826
|
+
dirHint.textContent = data.label
|
|
1827
|
+
? `已选择:${data.label}(${data.count || "?"} 个 .proto)`
|
|
1828
|
+
: `已选择:${data.path}`;
|
|
1829
|
+
}
|
|
1830
|
+
setRespBadge("ok", data.label ? `已导入 ${data.count || ""} 个 .proto`.trim() : "已选择目录");
|
|
1831
|
+
}
|
|
1832
|
+
if (data.type === "pickedGhzBinary" && data.path) {
|
|
1833
|
+
setGhzPath(data.path);
|
|
1834
|
+
setRespBadge("ok", "已选择 ghz");
|
|
1835
|
+
}
|
|
1836
|
+
if (data.type === "toast" && data.text) {
|
|
1837
|
+
setRespBadge(data.level === "err" ? "err" : "ok", String(data.text));
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
els.bulkModal.addEventListener("click", (event) => {
|
|
1842
|
+
if (event.target === els.bulkModal) closeModal(els.bulkModal);
|
|
1843
|
+
});
|
|
1844
|
+
els.envModal.addEventListener("click", (event) => {
|
|
1845
|
+
if (event.target === els.envModal) closeModal(els.envModal);
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
const ai = { tiles: [], selected: -1, solved: false, target: ["山", "海", "平", "爱"] };
|
|
1849
|
+
|
|
1850
|
+
function openAi() {
|
|
1851
|
+
const overlay = $("aiOverlay");
|
|
1852
|
+
overlay.classList.add("show");
|
|
1853
|
+
const qr = $("aiQr");
|
|
1854
|
+
if (state.qr) {
|
|
1855
|
+
qr.src = state.qr;
|
|
1856
|
+
qr.hidden = false;
|
|
1857
|
+
}
|
|
1858
|
+
if (!ai.tiles.length || ai.solved) {
|
|
1859
|
+
ai.solved = false;
|
|
1860
|
+
ai.selected = -1;
|
|
1861
|
+
ai.tiles = shuffleTiles(ai.target.slice());
|
|
1862
|
+
$("aiReward").classList.add("hidden");
|
|
1863
|
+
$("aiGame").classList.remove("hidden");
|
|
1864
|
+
}
|
|
1865
|
+
renderAiBoard();
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function closeAi() {
|
|
1869
|
+
$("aiOverlay").classList.remove("show");
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function renderAiBoard() {
|
|
1873
|
+
const board = $("aiBoard");
|
|
1874
|
+
board.innerHTML = "";
|
|
1875
|
+
ai.tiles.forEach((label, index) => {
|
|
1876
|
+
const btn = document.createElement("button");
|
|
1877
|
+
btn.type = "button";
|
|
1878
|
+
btn.className = "ai-tile" + (ai.solved ? " done" : "") + (ai.selected === index ? " selected" : "");
|
|
1879
|
+
btn.textContent = label;
|
|
1880
|
+
btn.addEventListener("click", () => {
|
|
1881
|
+
if (ai.solved) return;
|
|
1882
|
+
if (ai.selected < 0) ai.selected = index;
|
|
1883
|
+
else if (ai.selected === index) ai.selected = -1;
|
|
1884
|
+
else {
|
|
1885
|
+
const from = ai.selected;
|
|
1886
|
+
const tmp = ai.tiles[from];
|
|
1887
|
+
ai.tiles[from] = ai.tiles[index];
|
|
1888
|
+
ai.tiles[index] = tmp;
|
|
1889
|
+
ai.selected = -1;
|
|
1890
|
+
if (ai.tiles.join("") === ai.target.join("")) {
|
|
1891
|
+
ai.solved = true;
|
|
1892
|
+
$("aiReward").classList.remove("hidden");
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
renderAiBoard();
|
|
1896
|
+
});
|
|
1897
|
+
board.appendChild(btn);
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function shuffleTiles(list) {
|
|
1902
|
+
for (let i = list.length - 1; i > 0; i--) {
|
|
1903
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1904
|
+
const tmp = list[i];
|
|
1905
|
+
list[i] = list[j];
|
|
1906
|
+
list[j] = tmp;
|
|
1907
|
+
}
|
|
1908
|
+
if (list.join("") === "山海平爱") return shuffleTiles(list);
|
|
1909
|
+
return list;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function loadSplitState() {
|
|
1913
|
+
try {
|
|
1914
|
+
return JSON.parse(localStorage.getItem(SPLIT_KEY) || "{}");
|
|
1915
|
+
} catch {
|
|
1916
|
+
return {};
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function saveSplitState(next) {
|
|
1921
|
+
localStorage.setItem(SPLIT_KEY, JSON.stringify(next));
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function initSplitLayout() {
|
|
1925
|
+
const workspace = $("workspace");
|
|
1926
|
+
const handle = $("splitHandle");
|
|
1927
|
+
const reqBtn = $("collapseReqBtn");
|
|
1928
|
+
const respBtn = $("collapseRespBtn");
|
|
1929
|
+
if (!workspace || !handle || !reqBtn || !respBtn) return;
|
|
1930
|
+
|
|
1931
|
+
const saved = loadSplitState();
|
|
1932
|
+
let ratio = typeof saved.ratio === "number" ? saved.ratio : 0.62;
|
|
1933
|
+
let reqCollapsed = Boolean(saved.reqCollapsed);
|
|
1934
|
+
let respCollapsed = Boolean(saved.respCollapsed);
|
|
1935
|
+
if (reqCollapsed && respCollapsed) respCollapsed = false;
|
|
1936
|
+
|
|
1937
|
+
function persist() {
|
|
1938
|
+
saveSplitState({ ratio, reqCollapsed, respCollapsed });
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function apply() {
|
|
1942
|
+
workspace.style.setProperty("--req-size", `${Math.round(ratio * 10000) / 100}%`);
|
|
1943
|
+
workspace.classList.toggle("req-collapsed", reqCollapsed);
|
|
1944
|
+
workspace.classList.toggle("resp-collapsed", respCollapsed);
|
|
1945
|
+
reqBtn.title = reqCollapsed ? "展开请求区" : "收起请求区";
|
|
1946
|
+
respBtn.title = respCollapsed ? "展开响应区" : "收起响应区";
|
|
1947
|
+
persist();
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
reqBtn.addEventListener("click", (event) => {
|
|
1951
|
+
event.stopPropagation();
|
|
1952
|
+
if (reqCollapsed) {
|
|
1953
|
+
reqCollapsed = false;
|
|
1954
|
+
} else {
|
|
1955
|
+
reqCollapsed = true;
|
|
1956
|
+
if (respCollapsed) respCollapsed = false;
|
|
1957
|
+
}
|
|
1958
|
+
apply();
|
|
1959
|
+
});
|
|
1960
|
+
respBtn.addEventListener("click", (event) => {
|
|
1961
|
+
event.stopPropagation();
|
|
1962
|
+
if (respCollapsed) {
|
|
1963
|
+
respCollapsed = false;
|
|
1964
|
+
} else {
|
|
1965
|
+
respCollapsed = true;
|
|
1966
|
+
if (reqCollapsed) reqCollapsed = false;
|
|
1967
|
+
}
|
|
1968
|
+
apply();
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
let dragging = false;
|
|
1972
|
+
const onMove = (event) => {
|
|
1973
|
+
if (!dragging) return;
|
|
1974
|
+
const rect = workspace.getBoundingClientRect();
|
|
1975
|
+
const y = event.clientY - rect.top;
|
|
1976
|
+
const next = y / rect.height;
|
|
1977
|
+
ratio = Math.min(0.85, Math.max(0.18, next));
|
|
1978
|
+
if (reqCollapsed || respCollapsed) {
|
|
1979
|
+
reqCollapsed = false;
|
|
1980
|
+
respCollapsed = false;
|
|
1981
|
+
}
|
|
1982
|
+
apply();
|
|
1983
|
+
};
|
|
1984
|
+
const onUp = () => {
|
|
1985
|
+
if (!dragging) return;
|
|
1986
|
+
dragging = false;
|
|
1987
|
+
workspace.classList.remove("splitting");
|
|
1988
|
+
window.removeEventListener("pointermove", onMove);
|
|
1989
|
+
window.removeEventListener("pointerup", onUp);
|
|
1990
|
+
};
|
|
1991
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
1992
|
+
event.preventDefault();
|
|
1993
|
+
dragging = true;
|
|
1994
|
+
workspace.classList.add("splitting");
|
|
1995
|
+
window.addEventListener("pointermove", onMove);
|
|
1996
|
+
window.addEventListener("pointerup", onUp);
|
|
1997
|
+
});
|
|
1998
|
+
$("splitBar").addEventListener("dblclick", () => {
|
|
1999
|
+
ratio = 0.62;
|
|
2000
|
+
reqCollapsed = false;
|
|
2001
|
+
respCollapsed = false;
|
|
2002
|
+
apply();
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
apply();
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
setTab("message");
|
|
2009
|
+
renderMetadata();
|
|
2010
|
+
renderAuth();
|
|
2011
|
+
renderRpcInfo();
|
|
2012
|
+
initSplitLayout();
|
|
2013
|
+
setStressMode("light");
|
|
2014
|
+
|
|
2015
|
+
if (!vscode) {
|
|
2016
|
+
loadLocalEnvs();
|
|
2017
|
+
if (window.__PROTOVIEW_BOOTSTRAP__) applyBootstrap(window.__PROTOVIEW_BOOTSTRAP__);
|
|
2018
|
+
else {
|
|
2019
|
+
renderEnvSelect();
|
|
2020
|
+
renderTree();
|
|
2021
|
+
}
|
|
2022
|
+
} else {
|
|
2023
|
+
vscode.postMessage({ type: "ready" });
|
|
2024
|
+
}
|
|
2025
|
+
})();
|