fastapi-docs-plus 1.0.0__py3-none-any.whl
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.
- fastapi_docs_plus/__init__.py +9 -0
- fastapi_docs_plus/ai_fill.py +239 -0
- fastapi_docs_plus/config.py +123 -0
- fastapi_docs_plus/docs_plus.py +171 -0
- fastapi_docs_plus/i18n.py +61 -0
- fastapi_docs_plus/pre_request.py +145 -0
- fastapi_docs_plus/schema_utils.py +174 -0
- fastapi_docs_plus/static/adapter.js +197 -0
- fastapi_docs_plus/static/docs-plus.css +122 -0
- fastapi_docs_plus/static/docs-plus.js +628 -0
- fastapi_docs_plus/static/docs.html +19 -0
- fastapi_docs_plus-1.0.0.dist-info/METADATA +271 -0
- fastapi_docs_plus-1.0.0.dist-info/RECORD +16 -0
- fastapi_docs_plus-1.0.0.dist-info/WHEEL +5 -0
- fastapi_docs_plus-1.0.0.dist-info/licenses/LICENSE +21 -0
- fastapi_docs_plus-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fastapi-docs-plus frontend bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Renders the top bar (extra environment variables / identity / language),
|
|
5
|
+
* patches outgoing requests through the server-side pre-request hooks, and
|
|
6
|
+
* injects AI generate / fill buttons into every operation summary.
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
/** Configuration injected by docs_plus.py at render time. */
|
|
12
|
+
var CONFIG = JSON.parse(document.getElementById("docs-plus-config").textContent);
|
|
13
|
+
var ENV_STORAGE_KEY = "docsPlus.env";
|
|
14
|
+
var LANGUAGE_STORAGE_KEY = "docsPlus.language";
|
|
15
|
+
var Adapter = window.DocsPlusAdapter;
|
|
16
|
+
var toastTimer = null;
|
|
17
|
+
var language = readLanguage();
|
|
18
|
+
var languageVersion = 0;
|
|
19
|
+
var countsVersion = 0;
|
|
20
|
+
var aiCounts = {};
|
|
21
|
+
|
|
22
|
+
/** Localized UI strings; also covers error codes thrown by adapter.js. */
|
|
23
|
+
var MESSAGES = {
|
|
24
|
+
en: {
|
|
25
|
+
language: "Language",
|
|
26
|
+
extraEnv: "Extra environment variables (JSON, sent to the pre_request hook with each request)",
|
|
27
|
+
envSaved: "Environment variables saved",
|
|
28
|
+
invalidEnv: "Invalid environment variables: enter a valid JSON object",
|
|
29
|
+
noHook: "No pre_request hook registered",
|
|
30
|
+
aiDisabled: "AI fill is disabled (missing DOCS_PLUS_LLM_API_KEY)",
|
|
31
|
+
identity: "Identity",
|
|
32
|
+
identityChanged: "Identity switched to {identity}",
|
|
33
|
+
hookFailed: "Pre-request hook failed: {detail}",
|
|
34
|
+
hookRequestFailed: "Pre-request hook request failed: {detail}",
|
|
35
|
+
generate: "AI Generate",
|
|
36
|
+
generating: "Generating…",
|
|
37
|
+
generateTitle: "Generate parameters from this operation's schema and cache them without changing the form; use history to avoid duplicates",
|
|
38
|
+
fill: "Fill ({count})",
|
|
39
|
+
fillTitle: "Cycle through cached results and fill the form; the number in parentheses is the cache count",
|
|
40
|
+
generated: "Generated and cached; {count} result(s) available for this operation",
|
|
41
|
+
generateFailed: "AI generation failed: {detail}",
|
|
42
|
+
filled: "Filled cached result {index}/{count} ({fields} fields)",
|
|
43
|
+
parameterNotFound: "Parameter {parameter} not found in OpenAPI; skipped",
|
|
44
|
+
paramActionMissing: "This Swagger UI version has no changeParamByIdentity action; update adapter.js to fill parameters",
|
|
45
|
+
bodyActionMissing: "This Swagger UI version has no setRequestBodyValue action; update adapter.js to fill the request body",
|
|
46
|
+
},
|
|
47
|
+
zh: {
|
|
48
|
+
language: "\u8BED\u8A00",
|
|
49
|
+
extraEnv: "\u989D\u5916\u73AF\u5883\u53D8\u91CF\uFF08JSON\uFF0C\u4F1A\u968F\u6BCF\u6B21\u8BF7\u6C42\u4F20\u7ED9 pre_request \u94A9\u5B50\uFF09",
|
|
50
|
+
envSaved: "\u73AF\u5883\u53D8\u91CF\u5DF2\u4FDD\u5B58",
|
|
51
|
+
invalidEnv: "\u73AF\u5883\u53D8\u91CF\u683C\u5F0F\u9519\u8BEF\uFF1A\u8BF7\u8F93\u5165\u6709\u6548\u7684 JSON \u5BF9\u8C61",
|
|
52
|
+
noHook: "\u672A\u6CE8\u518C pre_request \u94A9\u5B50",
|
|
53
|
+
aiDisabled: "AI \u586B\u5145\u672A\u542F\u7528\uFF08\u7F3A\u5C11 DOCS_PLUS_LLM_API_KEY\uFF09",
|
|
54
|
+
identity: "\u8C03\u7528\u8EAB\u4EFD",
|
|
55
|
+
identityChanged: "\u8C03\u7528\u8EAB\u4EFD\u5DF2\u5207\u6362\u4E3A {identity}",
|
|
56
|
+
hookFailed: "\u524D\u7F6E\u94A9\u5B50\u6267\u884C\u5931\u8D25\uFF1A{detail}",
|
|
57
|
+
hookRequestFailed: "\u524D\u7F6E\u94A9\u5B50\u8BF7\u6C42\u5F02\u5E38\uFF1A{detail}",
|
|
58
|
+
generate: "AI \u751F\u6210",
|
|
59
|
+
generating: "\u751F\u6210\u4E2D…",
|
|
60
|
+
generateTitle: "\u8C03\u7528 LLM \u6309\u8BE5\u63A5\u53E3 schema \u751F\u6210\u4E00\u7EC4\u5165\u53C2\u5E76\u5B58\u5165\u7F13\u5B58\u961F\u5217\uFF08\u4E0D\u6539\u52A8\u8868\u5355\uFF09\uFF1B\u5386\u53F2\u7ED3\u679C\u4F1A\u53C2\u4E0E\u53BB\u91CD",
|
|
61
|
+
fill: "\u586B\u5145 ({count})",
|
|
62
|
+
fillTitle: "\u628A\u7F13\u5B58\u7684\u751F\u6210\u7ED3\u679C\u8F6E\u6362\u586B\u5165\u8868\u5355\uFF1B\u62EC\u53F7\u5185\u4E3A\u5F53\u524D\u7F13\u5B58\u6761\u6570",
|
|
63
|
+
generated: "\u5DF2\u751F\u6210\u5E76\u7F13\u5B58\uFF0C\u8BE5\u63A5\u53E3\u5F53\u524D {count} \u6761",
|
|
64
|
+
generateFailed: "AI \u751F\u6210\u5931\u8D25\uFF1A{detail}",
|
|
65
|
+
filled: "\u5DF2\u586B\u5145\u7B2C {index}/{count} \u6761\u7F13\u5B58\uFF08{fields} \u4E2A\u5B57\u6BB5\uFF09",
|
|
66
|
+
parameterNotFound: "OpenAPI \u4E2D\u627E\u4E0D\u5230\u53C2\u6570 {parameter}\uFF0C\u8DF3\u8FC7\u56DE\u586B",
|
|
67
|
+
paramActionMissing: "\u5F53\u524D swagger-ui \u7248\u672C\u6CA1\u6709 changeParamByIdentity action\uFF0C\u53C2\u6570\u56DE\u586B\u9700\u8981\u9002\u914D adapter.js",
|
|
68
|
+
bodyActionMissing: "\u5F53\u524D swagger-ui \u7248\u672C\u6CA1\u6709 setRequestBodyValue action\uFF0C\u8BF7\u6C42\u4F53\u56DE\u586B\u9700\u8981\u9002\u914D adapter.js",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read the persisted UI language.
|
|
74
|
+
*
|
|
75
|
+
* @returns {"en"|"zh"} Stored language, defaulting to "en".
|
|
76
|
+
*/
|
|
77
|
+
function readLanguage() {
|
|
78
|
+
try {
|
|
79
|
+
return localStorage.getItem(LANGUAGE_STORAGE_KEY) === "zh" ? "zh" : "en";
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return "en";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Interpolate {placeholder} tokens in a localized message.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} key - Key in MESSAGES[language].
|
|
89
|
+
* @param {Object} [values] - Replacement values keyed by placeholder name.
|
|
90
|
+
* @returns {string} Localized message with placeholders resolved.
|
|
91
|
+
*/
|
|
92
|
+
function t(key, values) {
|
|
93
|
+
return MESSAGES[language][key].replace(/\{(\w+)\}/g, function (match, name) {
|
|
94
|
+
return values && values[name] !== undefined ? String(values[name]) : match;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve an error to a localized message.
|
|
100
|
+
*
|
|
101
|
+
* Errors carrying a `code` recognised by the current language table are
|
|
102
|
+
* localised; everything else falls back to `err.message`.
|
|
103
|
+
*
|
|
104
|
+
* @param {Error} err - Error thrown by adapter.js or a fetch call.
|
|
105
|
+
* @returns {string} Human-readable message.
|
|
106
|
+
*/
|
|
107
|
+
function errorMessage(err) {
|
|
108
|
+
return err.code && Object.prototype.hasOwnProperty.call(MESSAGES[language], err.code)
|
|
109
|
+
? t(err.code, err.values)
|
|
110
|
+
: err.message;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create an element whose text content is bound to an i18n key.
|
|
115
|
+
*
|
|
116
|
+
* The returned element has a `data-i18n` attribute so that
|
|
117
|
+
* {@link refreshLanguage} can update it later.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} tag - HTML tag name.
|
|
120
|
+
* @param {string} key - Key in MESSAGES[language].
|
|
121
|
+
* @returns {HTMLElement} Element with `data-i18n` set and translated text.
|
|
122
|
+
*/
|
|
123
|
+
function localizedElement(tag, key) {
|
|
124
|
+
var element = document.createElement(tag);
|
|
125
|
+
element.dataset.i18n = key;
|
|
126
|
+
element.textContent = t(key);
|
|
127
|
+
return element;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Re-apply translations to every element tagged with `data-i18n`.
|
|
132
|
+
*/
|
|
133
|
+
function refreshLanguage() {
|
|
134
|
+
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
|
135
|
+
document.querySelectorAll("[data-i18n]").forEach(function (element) {
|
|
136
|
+
element.textContent = t(element.dataset.i18n);
|
|
137
|
+
});
|
|
138
|
+
var textarea = document.querySelector("#docs-plus-bar textarea");
|
|
139
|
+
if (textarea) textarea.setAttribute("aria-label", t("extraEnv"));
|
|
140
|
+
document.querySelectorAll(".docs-plus-generate-btn").forEach(function (button) {
|
|
141
|
+
button.textContent = t(button.disabled ? "generating" : "generate");
|
|
142
|
+
button.title = t("generateTitle");
|
|
143
|
+
});
|
|
144
|
+
refreshFillButtons();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Switch the UI language and resynchronise AI cache counts.
|
|
149
|
+
*
|
|
150
|
+
* @param {"en"|"zh"} next - Target language.
|
|
151
|
+
*/
|
|
152
|
+
function changeLanguage(next) {
|
|
153
|
+
if (next === language) return;
|
|
154
|
+
language = next;
|
|
155
|
+
languageVersion += 1;
|
|
156
|
+
try {
|
|
157
|
+
localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// Storage may be unavailable in private or embedded browsing contexts.
|
|
160
|
+
}
|
|
161
|
+
aiCounts = {};
|
|
162
|
+
clearTimeout(toastTimer);
|
|
163
|
+
document.getElementById("docs-plus-toast").hidden = true;
|
|
164
|
+
refreshLanguage();
|
|
165
|
+
loadCounts();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Read persisted environment variables.
|
|
170
|
+
*
|
|
171
|
+
* @returns {Object} Stored environment object, or {} when unset / invalid.
|
|
172
|
+
*/
|
|
173
|
+
function readEnv() {
|
|
174
|
+
try {
|
|
175
|
+
var raw = JSON.parse(localStorage.getItem(ENV_STORAGE_KEY));
|
|
176
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Persist environment variables.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} env - Environment object to store.
|
|
186
|
+
*/
|
|
187
|
+
function writeEnv(env) {
|
|
188
|
+
localStorage.setItem(ENV_STORAGE_KEY, JSON.stringify(env));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build the environment payload sent to the pre-request endpoint.
|
|
193
|
+
*
|
|
194
|
+
* Merges the extra environment JSON into the top level and adds the
|
|
195
|
+
* selected identity.
|
|
196
|
+
*
|
|
197
|
+
* @returns {Object} Payload for the `env` field of the pre-request body.
|
|
198
|
+
*/
|
|
199
|
+
function envPayload() {
|
|
200
|
+
var env = readEnv();
|
|
201
|
+
var payload = {};
|
|
202
|
+
if (env.extra && typeof env.extra === "object") {
|
|
203
|
+
Object.keys(env.extra).forEach(function (key) {
|
|
204
|
+
payload[key] = env.extra[key];
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
payload.identity = env.identity || null;
|
|
208
|
+
return payload;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Show a transient toast message.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} message - Message text.
|
|
215
|
+
* @param {boolean} [isError] - When true, render as an error (longer
|
|
216
|
+
* timeout, red background).
|
|
217
|
+
*/
|
|
218
|
+
function toast(message, isError) {
|
|
219
|
+
var el = document.getElementById("docs-plus-toast");
|
|
220
|
+
el.textContent = message;
|
|
221
|
+
el.classList.toggle("is-error", !!isError);
|
|
222
|
+
el.hidden = false;
|
|
223
|
+
clearTimeout(toastTimer);
|
|
224
|
+
toastTimer = setTimeout(function () {
|
|
225
|
+
el.hidden = true;
|
|
226
|
+
}, isError ? 8000 : 3000);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build the top bar: extra environment editor, identity and language selectors.
|
|
231
|
+
*/
|
|
232
|
+
function buildBar() {
|
|
233
|
+
var bar = document.getElementById("docs-plus-bar");
|
|
234
|
+
var env = readEnv();
|
|
235
|
+
|
|
236
|
+
var details = document.createElement("details");
|
|
237
|
+
var summary = localizedElement("summary", "extraEnv");
|
|
238
|
+
var textarea = document.createElement("textarea");
|
|
239
|
+
textarea.spellcheck = false;
|
|
240
|
+
textarea.value = JSON.stringify(env.extra || {}, null, 2);
|
|
241
|
+
textarea.addEventListener("change", function () {
|
|
242
|
+
var next = readEnv();
|
|
243
|
+
try {
|
|
244
|
+
var parsed = JSON.parse(textarea.value || "{}");
|
|
245
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
246
|
+
throw new Error("invalidEnv");
|
|
247
|
+
}
|
|
248
|
+
next.extra = parsed;
|
|
249
|
+
writeEnv(next);
|
|
250
|
+
toast(t("envSaved"));
|
|
251
|
+
} catch (err) {
|
|
252
|
+
toast(t("invalidEnv"), true);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
details.appendChild(summary);
|
|
256
|
+
details.appendChild(textarea);
|
|
257
|
+
bar.appendChild(details);
|
|
258
|
+
|
|
259
|
+
if (!CONFIG.hasPreRequestHook) {
|
|
260
|
+
var hookNote = localizedElement("span", "noHook");
|
|
261
|
+
hookNote.className = "docs-plus-note";
|
|
262
|
+
bar.appendChild(hookNote);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!CONFIG.aiEnabled) {
|
|
266
|
+
var aiNote = localizedElement("span", "aiDisabled");
|
|
267
|
+
aiNote.className = "docs-plus-note";
|
|
268
|
+
bar.appendChild(aiNote);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (CONFIG.identities && CONFIG.identities.length) {
|
|
272
|
+
var label = document.createElement("label");
|
|
273
|
+
label.appendChild(localizedElement("span", "identity"));
|
|
274
|
+
var select = document.createElement("select");
|
|
275
|
+
CONFIG.identities.forEach(function (name) {
|
|
276
|
+
var option = document.createElement("option");
|
|
277
|
+
option.value = name;
|
|
278
|
+
option.textContent = name;
|
|
279
|
+
select.appendChild(option);
|
|
280
|
+
});
|
|
281
|
+
select.value = env.identity || CONFIG.identities[0];
|
|
282
|
+
select.addEventListener("change", function () {
|
|
283
|
+
var next = readEnv();
|
|
284
|
+
next.identity = select.value;
|
|
285
|
+
writeEnv(next);
|
|
286
|
+
toast(t("identityChanged", { identity: select.value }));
|
|
287
|
+
});
|
|
288
|
+
if (!env.identity) {
|
|
289
|
+
env.identity = select.value;
|
|
290
|
+
writeEnv(env);
|
|
291
|
+
}
|
|
292
|
+
label.appendChild(select);
|
|
293
|
+
bar.appendChild(label);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var languageLabel = document.createElement("label");
|
|
297
|
+
languageLabel.appendChild(localizedElement("span", "language"));
|
|
298
|
+
var languageSelect = document.createElement("select");
|
|
299
|
+
languageSelect.id = "docs-plus-language";
|
|
300
|
+
[["en", "English"], ["zh", "\u7B80\u4F53\u4E2D\u6587"]].forEach(function (item) {
|
|
301
|
+
var option = document.createElement("option");
|
|
302
|
+
option.value = item[0];
|
|
303
|
+
option.textContent = item[1];
|
|
304
|
+
option.lang = item[0] === "zh" ? "zh-CN" : "en";
|
|
305
|
+
languageSelect.appendChild(option);
|
|
306
|
+
});
|
|
307
|
+
languageSelect.value = language;
|
|
308
|
+
languageSelect.addEventListener("change", function () {
|
|
309
|
+
changeLanguage(languageSelect.value);
|
|
310
|
+
});
|
|
311
|
+
languageLabel.appendChild(languageSelect);
|
|
312
|
+
bar.appendChild(languageLabel);
|
|
313
|
+
refreshLanguage();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Convert header values to strings, dropping null/undefined entries.
|
|
318
|
+
*
|
|
319
|
+
* @param {Object} [headers] - Raw headers from Swagger UI.
|
|
320
|
+
* @returns {Object} Stringified headers.
|
|
321
|
+
*/
|
|
322
|
+
function stringifyHeaders(headers) {
|
|
323
|
+
var out = {};
|
|
324
|
+
Object.keys(headers || {}).forEach(function (key) {
|
|
325
|
+
var value = headers[key];
|
|
326
|
+
if (value !== null && value !== undefined) out[key] = String(value);
|
|
327
|
+
});
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Swagger UI request interceptor: run the server-side pre-request hook.
|
|
333
|
+
*
|
|
334
|
+
* Sends the pending request to ``{apiPrefix}/api/pre-request``, then
|
|
335
|
+
* applies the returned header / query patches in place. Internal API
|
|
336
|
+
* calls originating from this plugin are skipped.
|
|
337
|
+
*
|
|
338
|
+
* @param {Object} request - Request object provided by Swagger UI's
|
|
339
|
+
* requestInterceptor.
|
|
340
|
+
* @returns {Promise<Object>} The (possibly patched) request.
|
|
341
|
+
*/
|
|
342
|
+
async function applyPreRequest(request) {
|
|
343
|
+
if (!CONFIG.hasPreRequestHook) return request;
|
|
344
|
+
if ((request.url || "").indexOf(CONFIG.apiPrefix + "/api/") !== -1) return request;
|
|
345
|
+
try {
|
|
346
|
+
var response = await fetch(CONFIG.apiPrefix + "/api/pre-request", {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: { "Content-Type": "application/json" },
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
method: request.method || "GET",
|
|
351
|
+
url: request.url,
|
|
352
|
+
headers: stringifyHeaders(request.headers),
|
|
353
|
+
env: envPayload(),
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
if (!response.ok) {
|
|
357
|
+
toast(t("hookFailed", { detail: (await response.text()).slice(0, 400) }), true);
|
|
358
|
+
return request;
|
|
359
|
+
}
|
|
360
|
+
var patch = await response.json();
|
|
361
|
+
if (patch.headers) request.headers = patch.headers;
|
|
362
|
+
if (patch.query) {
|
|
363
|
+
var url = new URL(request.url, window.location.origin);
|
|
364
|
+
url.search = new URLSearchParams(patch.query).toString();
|
|
365
|
+
request.url = url.toString();
|
|
366
|
+
}
|
|
367
|
+
} catch (err) {
|
|
368
|
+
toast(t("hookRequestFailed", { detail: err.message }), true);
|
|
369
|
+
}
|
|
370
|
+
return request;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Cache key for an operation.
|
|
375
|
+
*
|
|
376
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
377
|
+
* @returns {string} Cache key of the form ``"METHOD path"``.
|
|
378
|
+
*/
|
|
379
|
+
function opKey(target) {
|
|
380
|
+
return target.method.toUpperCase() + " " + target.path;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Cached result count for an operation.
|
|
385
|
+
*
|
|
386
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
387
|
+
* @returns {number} Number of cached AI-generated results, or 0.
|
|
388
|
+
*/
|
|
389
|
+
function getCount(target) {
|
|
390
|
+
return aiCounts[opKey(target)] || 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Refresh labels and ``aria-disabled`` state of every fill button.
|
|
395
|
+
*/
|
|
396
|
+
function refreshFillButtons() {
|
|
397
|
+
document.querySelectorAll(".docs-plus-fill-btn").forEach(function (btn) {
|
|
398
|
+
var count = aiCounts[btn.getAttribute("data-op-key")] || 0;
|
|
399
|
+
btn.textContent = t("fill", { count: count });
|
|
400
|
+
btn.title = t("fillTitle");
|
|
401
|
+
btn.classList.toggle("is-empty", count === 0);
|
|
402
|
+
btn.setAttribute("aria-disabled", count === 0 ? "true" : "false");
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Update the cached result count of one operation and refresh fill buttons.
|
|
408
|
+
*
|
|
409
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
410
|
+
* @param {number} count - New cached result count.
|
|
411
|
+
*/
|
|
412
|
+
function setCount(target, count) {
|
|
413
|
+
countsVersion += 1;
|
|
414
|
+
aiCounts[opKey(target)] = count;
|
|
415
|
+
refreshFillButtons();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Fetch cache counts for all operations of the current language.
|
|
420
|
+
*/
|
|
421
|
+
async function loadCounts() {
|
|
422
|
+
if (!CONFIG.aiEnabled) return;
|
|
423
|
+
var version = ++countsVersion;
|
|
424
|
+
var requestVersion = languageVersion;
|
|
425
|
+
try {
|
|
426
|
+
var response = await fetch(CONFIG.apiPrefix + "/api/ai/cache?language=" + language);
|
|
427
|
+
if (!response.ok) return;
|
|
428
|
+
var data = await response.json();
|
|
429
|
+
if (requestVersion !== languageVersion || version !== countsVersion) return;
|
|
430
|
+
aiCounts = data.counts || {};
|
|
431
|
+
refreshFillButtons();
|
|
432
|
+
} catch (err) {
|
|
433
|
+
// The next successful AI operation will resynchronise the cache count.
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* POST to an AI endpoint and parse the JSON response.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} pathSeg - Endpoint path relative to ``CONFIG.apiPrefix``.
|
|
441
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
442
|
+
* @param {"en"|"zh"} requestLanguage - Output language for the request.
|
|
443
|
+
* @returns {Promise<Object>} Parsed response body.
|
|
444
|
+
*/
|
|
445
|
+
async function postAi(pathSeg, target, requestLanguage) {
|
|
446
|
+
var response = await fetch(CONFIG.apiPrefix + pathSeg, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: { "Content-Type": "application/json" },
|
|
449
|
+
body: JSON.stringify({ path: target.path, method: target.method, language: requestLanguage }),
|
|
450
|
+
});
|
|
451
|
+
var text = await response.text();
|
|
452
|
+
if (!response.ok) {
|
|
453
|
+
var detail = text;
|
|
454
|
+
try {
|
|
455
|
+
detail = JSON.parse(text).detail || text;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
// Non-JSON responses retain their original error text.
|
|
458
|
+
}
|
|
459
|
+
var error = new Error(detail);
|
|
460
|
+
error.status = response.status;
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
return JSON.parse(text);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Handle an "AI Generate" click: call the generation endpoint, cache, and
|
|
468
|
+
* update the count badge.
|
|
469
|
+
*
|
|
470
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
471
|
+
* @param {HTMLButtonElement} button - The clicked "AI Generate" button.
|
|
472
|
+
*/
|
|
473
|
+
async function handleGenerate(target, button) {
|
|
474
|
+
if (button.disabled) return;
|
|
475
|
+
var requestVersion = languageVersion;
|
|
476
|
+
button.disabled = true;
|
|
477
|
+
button.textContent = t("generating");
|
|
478
|
+
try {
|
|
479
|
+
var data = await postAi("/api/ai/generate", target, language);
|
|
480
|
+
if (requestVersion !== languageVersion) return;
|
|
481
|
+
setCount(target, data.count);
|
|
482
|
+
toast(t("generated", { count: data.count }));
|
|
483
|
+
} catch (err) {
|
|
484
|
+
if (requestVersion !== languageVersion) return;
|
|
485
|
+
toast(t("generateFailed", { detail: errorMessage(err) }), true);
|
|
486
|
+
} finally {
|
|
487
|
+
button.disabled = false;
|
|
488
|
+
button.textContent = t("generate");
|
|
489
|
+
if (requestVersion !== languageVersion) loadCounts();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Write a cached result envelope into the operation's form fields.
|
|
495
|
+
*
|
|
496
|
+
* @param {Object} system - Swagger UI system object.
|
|
497
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
498
|
+
* @param {Object} data - Envelope with ``path``, ``query``, ``header``,
|
|
499
|
+
* ``cookie`` and ``body`` keys.
|
|
500
|
+
* @returns {number} Number of fields successfully filled.
|
|
501
|
+
*/
|
|
502
|
+
function applyEnvelope(system, target, data) {
|
|
503
|
+
var filled = 0;
|
|
504
|
+
["path", "query", "header", "cookie"].forEach(function (location) {
|
|
505
|
+
var values = data[location] || {};
|
|
506
|
+
Object.keys(values).forEach(function (name) {
|
|
507
|
+
try {
|
|
508
|
+
Adapter.setParamValue(system, target.path, target.method, name, location, values[name]);
|
|
509
|
+
filled += 1;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
toast(errorMessage(err), true);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
if (data.body !== null && data.body !== undefined) {
|
|
516
|
+
Adapter.setRequestBody(system, target.path, target.method, data.body);
|
|
517
|
+
filled += 1;
|
|
518
|
+
}
|
|
519
|
+
Adapter.validateParams(system, target.path, target.method);
|
|
520
|
+
return filled;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Handle a "Fill" click: fetch the next cached result and apply it to the
|
|
525
|
+
* form.
|
|
526
|
+
*
|
|
527
|
+
* @param {Object} system - Swagger UI system object.
|
|
528
|
+
* @param {{path: string, method: string}} target - Operation target.
|
|
529
|
+
* @param {HTMLButtonElement} button - The clicked "Fill" button.
|
|
530
|
+
*/
|
|
531
|
+
async function handleFill(system, target, button) {
|
|
532
|
+
if (getCount(target) === 0 || button.dataset.busy) return;
|
|
533
|
+
var requestVersion = languageVersion;
|
|
534
|
+
button.dataset.busy = "1";
|
|
535
|
+
try {
|
|
536
|
+
var data = await postAi("/api/ai/fill", target, language);
|
|
537
|
+
if (requestVersion !== languageVersion) return;
|
|
538
|
+
setCount(target, data.cacheCount);
|
|
539
|
+
await Adapter.ensureOperationExpanded(button);
|
|
540
|
+
if (requestVersion !== languageVersion) return;
|
|
541
|
+
var filled = applyEnvelope(system, target, data);
|
|
542
|
+
toast(t("filled", { index: data.cacheIndex + 1, count: data.cacheCount, fields: filled }));
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (requestVersion !== languageVersion) return;
|
|
545
|
+
if (err.status === 409) setCount(target, 0);
|
|
546
|
+
toast(errorMessage(err), true);
|
|
547
|
+
} finally {
|
|
548
|
+
delete button.dataset.busy;
|
|
549
|
+
refreshFillButtons();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Swagger UI plugin providing the per-operation AI buttons.
|
|
555
|
+
*
|
|
556
|
+
* @param {Object} system - Swagger UI system object injected by the
|
|
557
|
+
* plugin system.
|
|
558
|
+
* @returns {Object} Plugin descriptor wrapping the ``OperationSummary``
|
|
559
|
+
* component.
|
|
560
|
+
*/
|
|
561
|
+
function AiFillPlugin(system) {
|
|
562
|
+
return {
|
|
563
|
+
wrapComponents: {
|
|
564
|
+
OperationSummary: function (Original, deps) {
|
|
565
|
+
var React = deps.React;
|
|
566
|
+
return function (props) {
|
|
567
|
+
var target = Adapter.readPathMethod(props);
|
|
568
|
+
var children = [React.createElement(Original, Object.assign({ key: "summary" }, props))];
|
|
569
|
+
if (target) {
|
|
570
|
+
children.push(
|
|
571
|
+
React.createElement(
|
|
572
|
+
"button",
|
|
573
|
+
{
|
|
574
|
+
key: "ai-generate",
|
|
575
|
+
type: "button",
|
|
576
|
+
className: "docs-plus-ai-btn docs-plus-generate-btn",
|
|
577
|
+
title: t("generateTitle"),
|
|
578
|
+
onClick: function (event) {
|
|
579
|
+
event.preventDefault();
|
|
580
|
+
event.stopPropagation();
|
|
581
|
+
handleGenerate(target, event.currentTarget);
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
t("generate")
|
|
585
|
+
),
|
|
586
|
+
React.createElement(
|
|
587
|
+
"button",
|
|
588
|
+
{
|
|
589
|
+
key: "ai-fill",
|
|
590
|
+
type: "button",
|
|
591
|
+
className:
|
|
592
|
+
"docs-plus-ai-btn docs-plus-fill-btn" + (getCount(target) === 0 ? " is-empty" : ""),
|
|
593
|
+
"data-op-key": opKey(target),
|
|
594
|
+
"aria-disabled": getCount(target) === 0 ? "true" : "false",
|
|
595
|
+
title: t("fillTitle"),
|
|
596
|
+
onClick: function (event) {
|
|
597
|
+
event.preventDefault();
|
|
598
|
+
event.stopPropagation();
|
|
599
|
+
handleFill(system, target, event.currentTarget);
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
t("fill", { count: getCount(target) })
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return React.createElement("div", { className: "docs-plus-op-summary" }, children);
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Bootstrap: build the top bar and start Swagger UI.
|
|
614
|
+
window.addEventListener("load", function () {
|
|
615
|
+
buildBar();
|
|
616
|
+
var params = Object.assign({}, CONFIG.swaggerUiParameters, {
|
|
617
|
+
url: CONFIG.openapiUrl,
|
|
618
|
+
dom_id: "#swagger-ui",
|
|
619
|
+
deepLinking: true,
|
|
620
|
+
layout: "BaseLayout",
|
|
621
|
+
presets: [window.SwaggerUIBundle.presets.apis],
|
|
622
|
+
plugins: CONFIG.aiEnabled ? [AiFillPlugin] : [],
|
|
623
|
+
requestInterceptor: applyPreRequest,
|
|
624
|
+
});
|
|
625
|
+
window.ui = window.SwaggerUIBundle(params);
|
|
626
|
+
loadCounts();
|
|
627
|
+
});
|
|
628
|
+
})();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
__SWAGGER_CSS_TAG__
|
|
7
|
+
<link rel="stylesheet" href="__STATIC_URL__/docs-plus.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header id="docs-plus-bar"></header>
|
|
11
|
+
<div id="swagger-ui"></div>
|
|
12
|
+
<div id="docs-plus-toast" role="status" aria-live="polite" hidden></div>
|
|
13
|
+
|
|
14
|
+
<script type="application/json" id="docs-plus-config">__CONFIG__</script>
|
|
15
|
+
__SWAGGER_JS_TAG__
|
|
16
|
+
<script src="__STATIC_URL__/adapter.js"></script>
|
|
17
|
+
<script src="__STATIC_URL__/docs-plus.js"></script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|