dsh-cloudq 0.1.0
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/LICENSE +21 -0
- package/README.md +89 -0
- package/assets/cloudq.png +0 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +3405 -0
- package/lib/index.js +910 -0
- package/lib/types/client/index.d.ts +4 -0
- package/lib/types/index.d.ts +13 -0
- package/package.json +119 -0
- package/skills/cloudq/SKILL.md +359 -0
- package/skills/cloudq/references/api/CloudQChatCompletions.md +178 -0
- package/skills/cloudq/scripts/check_env.py +811 -0
- package/skills/cloudq/scripts/cleanup.py +419 -0
- package/skills/cloudq/scripts/create_role.py +281 -0
- package/skills/cloudq/scripts/credential_manager.py +554 -0
- package/skills/cloudq/scripts/login.py +410 -0
- package/skills/cloudq/scripts/login_url.py +404 -0
- package/skills/cloudq/scripts/logout.py +36 -0
- package/skills/cloudq/scripts/save_ak.py +156 -0
- package/skills/cloudq/scripts/tcloud_api.py +329 -0
- package/skills/cloudq/scripts/tcloud_async_task.py +345 -0
- package/skills/cloudq/scripts/tcloud_sse_api.py +765 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
6
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
//#region src/http.js
|
|
10
|
+
/** Maximum accepted JSON request body size. */
|
|
11
|
+
const MAX_JSON_BODY_BYTES = 65536;
|
|
12
|
+
/** HTTP failure with a stable client-facing code. */
|
|
13
|
+
var HttpError = class extends Error {
|
|
14
|
+
/**
|
|
15
|
+
* @param {number} statusCode HTTP status code.
|
|
16
|
+
* @param {string} code Stable machine-readable code.
|
|
17
|
+
* @param {string} message Safe client-facing message.
|
|
18
|
+
*/
|
|
19
|
+
constructor(statusCode, code, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "HttpError";
|
|
22
|
+
this.statusCode = statusCode;
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Return whether a request originated from the local machine. */
|
|
27
|
+
function isLoopbackRequest(request) {
|
|
28
|
+
const address = request.socket?.remoteAddress;
|
|
29
|
+
return address === "::1" || address === "127.0.0.1" || address === "::ffff:127.0.0.1" || typeof address === "string" && /^::ffff:127\./.test(address);
|
|
30
|
+
}
|
|
31
|
+
/** Reject cross-site, non-loopback, or unexpected-method requests. */
|
|
32
|
+
function assertSafeRequest(request, expectedMethod) {
|
|
33
|
+
if (!isLoopbackRequest(request)) throw new HttpError(403, "forbidden", "CloudQ is available only from the local DSH page.");
|
|
34
|
+
if ((request.method ?? "GET").toUpperCase() !== expectedMethod) throw new HttpError(405, "method-not-allowed", "The request method is not supported.");
|
|
35
|
+
const fetchSite = request.headers["sec-fetch-site"];
|
|
36
|
+
if (typeof fetchSite === "string" && fetchSite !== "same-origin" && fetchSite !== "same-site" && fetchSite !== "none") throw new HttpError(403, "forbidden-origin", "Cross-site requests are not allowed.");
|
|
37
|
+
const origin = request.headers.origin;
|
|
38
|
+
if (origin !== void 0) {
|
|
39
|
+
const host = request.headers.host;
|
|
40
|
+
if (host === void 0) throw new HttpError(403, "forbidden-origin", "The request origin is invalid.");
|
|
41
|
+
let originHost;
|
|
42
|
+
try {
|
|
43
|
+
originHost = new URL(origin).host.toLowerCase();
|
|
44
|
+
} catch {
|
|
45
|
+
throw new HttpError(403, "forbidden-origin", "The request origin is invalid.");
|
|
46
|
+
}
|
|
47
|
+
if (originHost !== host.toLowerCase()) throw new HttpError(403, "forbidden-origin", "Cross-site requests are not allowed.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Send a JSON response with security and caching headers. */
|
|
51
|
+
function sendJson(response, statusCode, body) {
|
|
52
|
+
response.statusCode = statusCode;
|
|
53
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
54
|
+
response.setHeader("Cache-Control", "no-store");
|
|
55
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
56
|
+
response.end(JSON.stringify(body));
|
|
57
|
+
}
|
|
58
|
+
/** Send only stable, client-safe error details. */
|
|
59
|
+
function sendError(response, error) {
|
|
60
|
+
if (error instanceof HttpError) {
|
|
61
|
+
sendJson(response, error.statusCode, {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: {
|
|
64
|
+
code: error.code,
|
|
65
|
+
message: error.message
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
sendJson(response, 500, {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: {
|
|
73
|
+
code: "internal",
|
|
74
|
+
message: "The CloudQ request failed."
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function contentLength(request) {
|
|
79
|
+
const value = request.headers["content-length"];
|
|
80
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
81
|
+
if (raw === void 0) return void 0;
|
|
82
|
+
if (!/^\d+$/.test(raw)) throw new HttpError(400, "invalid-content-length", "The Content-Length header is invalid.");
|
|
83
|
+
const length = Number(raw);
|
|
84
|
+
if (!Number.isSafeInteger(length)) throw new HttpError(400, "invalid-content-length", "The Content-Length header is invalid.");
|
|
85
|
+
return length;
|
|
86
|
+
}
|
|
87
|
+
/** Read and parse a bounded application/json request body. */
|
|
88
|
+
function readJsonBody(request, { maxBytes = MAX_JSON_BODY_BYTES } = {}) {
|
|
89
|
+
const contentType = request.headers["content-type"];
|
|
90
|
+
if (typeof contentType !== "string" || contentType.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
|
|
91
|
+
request.resume?.();
|
|
92
|
+
return Promise.reject(new HttpError(415, "unsupported-media-type", "Content-Type must be application/json."));
|
|
93
|
+
}
|
|
94
|
+
let declaredLength;
|
|
95
|
+
try {
|
|
96
|
+
declaredLength = contentLength(request);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
request.resume?.();
|
|
99
|
+
return Promise.reject(error);
|
|
100
|
+
}
|
|
101
|
+
if (declaredLength !== void 0 && declaredLength > maxBytes) {
|
|
102
|
+
request.resume?.();
|
|
103
|
+
return Promise.reject(new HttpError(413, "payload-too-large", "The JSON request body is too large."));
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
let bytes = 0;
|
|
108
|
+
let settled = false;
|
|
109
|
+
const rejectOnce = (error) => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
settled = true;
|
|
112
|
+
chunks.length = 0;
|
|
113
|
+
rejectBody(error);
|
|
114
|
+
};
|
|
115
|
+
request.on("data", (chunk) => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
const buffer = Buffer$1.isBuffer(chunk) ? chunk : Buffer$1.from(chunk);
|
|
118
|
+
bytes += buffer.byteLength;
|
|
119
|
+
if (bytes > maxBytes) {
|
|
120
|
+
rejectOnce(new HttpError(413, "payload-too-large", "The JSON request body is too large."));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
chunks.push(buffer);
|
|
124
|
+
});
|
|
125
|
+
request.on("end", () => {
|
|
126
|
+
if (settled) return;
|
|
127
|
+
settled = true;
|
|
128
|
+
if (chunks.length === 0) {
|
|
129
|
+
rejectBody(new HttpError(400, "invalid-json", "The request body must contain JSON."));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
resolveBody(JSON.parse(Buffer$1.concat(chunks).toString("utf8")));
|
|
134
|
+
} catch {
|
|
135
|
+
rejectBody(new HttpError(400, "invalid-json", "The request body is not valid JSON."));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
request.on("aborted", () => {
|
|
139
|
+
rejectOnce(new HttpError(400, "request-aborted", "The request body was interrupted."));
|
|
140
|
+
});
|
|
141
|
+
request.on("error", () => {
|
|
142
|
+
rejectOnce(new HttpError(400, "request-read-failed", "The request body could not be read."));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/plugin-manager.js
|
|
148
|
+
/** Host-side management of optional profile bundle entries. */
|
|
149
|
+
const PROTECTED_BUNDLES = /* @__PURE__ */ new Set(["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]);
|
|
150
|
+
function profileDirectory(baseUrl) {
|
|
151
|
+
const url = new URL(".", baseUrl);
|
|
152
|
+
if (url.protocol !== "file:") throw new Error("The active DSH profile URL must use the file protocol.");
|
|
153
|
+
return fileURLToPath(url);
|
|
154
|
+
}
|
|
155
|
+
function patchPath(baseUrl) {
|
|
156
|
+
return resolve(profileDirectory(baseUrl), "cordis.patch.yml");
|
|
157
|
+
}
|
|
158
|
+
function parsePatchList(content) {
|
|
159
|
+
if (!content.trim()) return [];
|
|
160
|
+
const parsed = yaml.load(content);
|
|
161
|
+
if (parsed === null || parsed === void 0) return [];
|
|
162
|
+
if (!Array.isArray(parsed)) throw new Error("The DSH profile patch must contain a top-level array.");
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
function leadingComments(content) {
|
|
166
|
+
return (content.match(/^(#[^\n]*\n)+/) ?? [""])[0];
|
|
167
|
+
}
|
|
168
|
+
function serializePatchList(entries, originalContent) {
|
|
169
|
+
return `${leadingComments(originalContent)}${yaml.dump(entries, {
|
|
170
|
+
noRefs: true,
|
|
171
|
+
lineWidth: 120
|
|
172
|
+
})}`;
|
|
173
|
+
}
|
|
174
|
+
function packageDirectory(profileDir, packageName) {
|
|
175
|
+
const anchor = join(profileDir, "package.json");
|
|
176
|
+
for (const searchPath of createRequire(anchor).resolve.paths(packageName) ?? []) {
|
|
177
|
+
const candidate = join(searchPath, packageName);
|
|
178
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function bundleInsertRows(profileDir, packageName) {
|
|
182
|
+
const packageDir = packageDirectory(profileDir, packageName);
|
|
183
|
+
if (packageDir === void 0) return [];
|
|
184
|
+
const declaredPatch = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"))?.dsh?.bundle?.patch;
|
|
185
|
+
if (typeof declaredPatch !== "string" || !declaredPatch) return [];
|
|
186
|
+
const bundlePatchPath = join(packageDir, declaredPatch);
|
|
187
|
+
if (!existsSync(bundlePatchPath)) return [];
|
|
188
|
+
const rows = [];
|
|
189
|
+
for (const block of parsePatchList(readFileSync(bundlePatchPath, "utf8"))) if (block && typeof block === "object" && Array.isArray(block.insert)) rows.push(...block.insert);
|
|
190
|
+
return rows;
|
|
191
|
+
}
|
|
192
|
+
function readProfileBundles(profileDir) {
|
|
193
|
+
const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8"))?.dsh?.profile?.bundles;
|
|
194
|
+
return Array.isArray(bundles) ? bundles.filter((bundle) => typeof bundle === "string") : [];
|
|
195
|
+
}
|
|
196
|
+
function fileSnapshot(path) {
|
|
197
|
+
if (!existsSync(path)) return void 0;
|
|
198
|
+
const stat = statSync(path);
|
|
199
|
+
return {
|
|
200
|
+
size: stat.size,
|
|
201
|
+
mtimeMs: stat.mtimeMs,
|
|
202
|
+
mode: stat.mode
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function sameSnapshot(left, right) {
|
|
206
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
207
|
+
return left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
208
|
+
}
|
|
209
|
+
function writePatchAtomically(path, content, originalSnapshot) {
|
|
210
|
+
const temporary = join(dirname(path), `.${process.pid}.${Date.now()}.dsh-cloudq.tmp`);
|
|
211
|
+
try {
|
|
212
|
+
writeFileSync(temporary, content, {
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
flag: "wx",
|
|
215
|
+
mode: originalSnapshot?.mode ?? 384
|
|
216
|
+
});
|
|
217
|
+
if (!sameSnapshot(originalSnapshot, fileSnapshot(path))) throw new Error("The DSH profile patch changed while it was being updated. Retry the operation.");
|
|
218
|
+
renameSync(temporary, path);
|
|
219
|
+
} finally {
|
|
220
|
+
rmSync(temporary, { force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* List third-party bundle entries mounted by the active profile.
|
|
225
|
+
* @param {string | URL} baseUrl Loader base URL from `ctx.baseUrl`.
|
|
226
|
+
* @returns {Array<{id: string, name: string, bundle: string, disabled: boolean, self: boolean}>}
|
|
227
|
+
*/
|
|
228
|
+
function listPlugins(baseUrl) {
|
|
229
|
+
const profileDir = profileDirectory(baseUrl);
|
|
230
|
+
const userPatchPath = patchPath(baseUrl);
|
|
231
|
+
const overrides = (existsSync(userPatchPath) ? parsePatchList(readFileSync(userPatchPath, "utf8")) : []).filter((entry) => entry && typeof entry === "object" && typeof entry.id === "string");
|
|
232
|
+
const plugins = [];
|
|
233
|
+
for (const bundle of readProfileBundles(profileDir)) {
|
|
234
|
+
if (PROTECTED_BUNDLES.has(bundle)) continue;
|
|
235
|
+
for (const row of bundleInsertRows(profileDir, bundle)) {
|
|
236
|
+
if (!row || typeof row !== "object" || typeof row.id !== "string") continue;
|
|
237
|
+
const override = overrides.find((entry) => entry.id === row.id);
|
|
238
|
+
plugins.push({
|
|
239
|
+
id: row.id,
|
|
240
|
+
name: typeof row.name === "string" ? row.name : row.id,
|
|
241
|
+
bundle,
|
|
242
|
+
disabled: override?.disabled === true,
|
|
243
|
+
self: bundle === "dsh-cloudq"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return plugins;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Atomically update one mounted plugin's disabled override.
|
|
251
|
+
* @param {string | URL} baseUrl Loader base URL from `ctx.baseUrl`.
|
|
252
|
+
* @param {string} id Mounted plugin entry id.
|
|
253
|
+
* @param {boolean} disabled Desired disabled state.
|
|
254
|
+
* @returns {{ok: true, id: string, disabled: boolean}}
|
|
255
|
+
*/
|
|
256
|
+
function setPluginDisabled(baseUrl, id, disabled) {
|
|
257
|
+
const userPatchPath = patchPath(baseUrl);
|
|
258
|
+
if (!listPlugins(baseUrl).some((plugin) => plugin.id === id)) {
|
|
259
|
+
const error = /* @__PURE__ */ new Error(`Unknown plugin entry: ${id}`);
|
|
260
|
+
error.code = "unknown-plugin";
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
const originalSnapshot = fileSnapshot(userPatchPath);
|
|
264
|
+
const original = originalSnapshot === void 0 ? "" : readFileSync(userPatchPath, "utf8");
|
|
265
|
+
let patches = parsePatchList(original);
|
|
266
|
+
const target = patches.find((entry) => entry && typeof entry === "object" && entry.id === id);
|
|
267
|
+
if (target) {
|
|
268
|
+
if (disabled) target.disabled = true;
|
|
269
|
+
else {
|
|
270
|
+
delete target.disabled;
|
|
271
|
+
if (Object.keys(target).length === 1) patches = patches.filter((entry) => entry !== target);
|
|
272
|
+
}
|
|
273
|
+
} else if (disabled) patches.push({
|
|
274
|
+
id,
|
|
275
|
+
disabled: true
|
|
276
|
+
});
|
|
277
|
+
else return {
|
|
278
|
+
ok: true,
|
|
279
|
+
id,
|
|
280
|
+
disabled: false
|
|
281
|
+
};
|
|
282
|
+
writePatchAtomically(userPatchPath, serializePatchList(patches, original), originalSnapshot);
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
id,
|
|
286
|
+
disabled
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/script-runner.js
|
|
291
|
+
const MAX_SCRIPT_OUTPUT_BYTES = 1048576;
|
|
292
|
+
function safeCode(value) {
|
|
293
|
+
return typeof value === "string" && /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : "script-failed";
|
|
294
|
+
}
|
|
295
|
+
function redact(value, sensitiveValues) {
|
|
296
|
+
let text = String(value ?? "");
|
|
297
|
+
for (const sensitive of sensitiveValues) if (sensitive) text = text.replaceAll(sensitive, "[redacted]");
|
|
298
|
+
return text.replace(/[\r\n\t]+/g, " ").slice(0, 300);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Run one bundled Python helper with bounded output and optional stdin.
|
|
302
|
+
* @param {string} scriptsDirectory Absolute scripts directory.
|
|
303
|
+
* @param {string} scriptName Bundled script filename.
|
|
304
|
+
* @param {string[]} args Non-sensitive command-line arguments.
|
|
305
|
+
* @param {object} options Execution options.
|
|
306
|
+
* @returns {Promise<Record<string, unknown>>} Parsed helper response.
|
|
307
|
+
*/
|
|
308
|
+
function runScript$1(scriptsDirectory, scriptName, args, { timeoutMs = 3e4, jsonOnly = true, stdin, sensitiveValues = [], spawnProcess = spawn } = {}) {
|
|
309
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
310
|
+
const child = spawnProcess("python3", [resolve(scriptsDirectory, scriptName), ...args], {
|
|
311
|
+
stdio: [
|
|
312
|
+
stdin === void 0 ? "ignore" : "pipe",
|
|
313
|
+
"pipe",
|
|
314
|
+
"pipe"
|
|
315
|
+
],
|
|
316
|
+
timeout: timeoutMs
|
|
317
|
+
});
|
|
318
|
+
let stdout = "";
|
|
319
|
+
let stderr = "";
|
|
320
|
+
let outputBytes = 0;
|
|
321
|
+
let settled = false;
|
|
322
|
+
const rejectOnce = (error) => {
|
|
323
|
+
if (settled) return;
|
|
324
|
+
settled = true;
|
|
325
|
+
rejectRun(error);
|
|
326
|
+
};
|
|
327
|
+
const resolveOnce = (value) => {
|
|
328
|
+
if (settled) return;
|
|
329
|
+
settled = true;
|
|
330
|
+
resolveRun(value);
|
|
331
|
+
};
|
|
332
|
+
const collect = (current, chunk) => {
|
|
333
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
334
|
+
outputBytes += buffer.byteLength;
|
|
335
|
+
if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
336
|
+
child.kill();
|
|
337
|
+
rejectOnce(new HttpError(502, "script-output-too-large", "The CloudQ helper returned too much data."));
|
|
338
|
+
return current;
|
|
339
|
+
}
|
|
340
|
+
return current + buffer.toString("utf8");
|
|
341
|
+
};
|
|
342
|
+
child.stdout.on("data", (chunk) => {
|
|
343
|
+
stdout = collect(stdout, chunk);
|
|
344
|
+
});
|
|
345
|
+
child.stderr.on("data", (chunk) => {
|
|
346
|
+
stderr = collect(stderr, chunk);
|
|
347
|
+
});
|
|
348
|
+
child.on("error", () => {
|
|
349
|
+
rejectOnce(new HttpError(502, "script-launch-failed", "The CloudQ helper could not be started."));
|
|
350
|
+
});
|
|
351
|
+
child.on("close", (code) => {
|
|
352
|
+
if (settled) return;
|
|
353
|
+
const trimmed = stdout.trim();
|
|
354
|
+
if (!jsonOnly) {
|
|
355
|
+
if (code !== 0) rejectOnce(new HttpError(502, "script-failed", "The CloudQ helper failed."));
|
|
356
|
+
else resolveOnce({
|
|
357
|
+
success: true,
|
|
358
|
+
message: redact(trimmed, sensitiveValues)
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
let parsed;
|
|
363
|
+
try {
|
|
364
|
+
parsed = trimmed ? JSON.parse(trimmed) : {};
|
|
365
|
+
} catch {
|
|
366
|
+
rejectOnce(new HttpError(502, "script-invalid-output", "The CloudQ helper returned an invalid response."));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (code !== 0 || parsed?.ok === false || parsed?.success === false) {
|
|
370
|
+
const codeValue = safeCode(parsed?.error?.code ?? parsed?.code);
|
|
371
|
+
rejectOnce(new HttpError(502, codeValue, "The CloudQ helper failed."));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
resolveOnce(parsed);
|
|
375
|
+
});
|
|
376
|
+
if (stdin !== void 0) {
|
|
377
|
+
child.stdin.on("error", () => {
|
|
378
|
+
rejectOnce(new HttpError(502, "script-input-failed", "The CloudQ helper could not receive its input."));
|
|
379
|
+
});
|
|
380
|
+
child.stdin.end(stdin);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/index.js
|
|
386
|
+
/**
|
|
387
|
+
* dsh-cloudq host half.
|
|
388
|
+
*
|
|
389
|
+
* Registers the CloudQ skill (a multi-cloud AIOps expert driven by the Tencent
|
|
390
|
+
* Cloud SSE conversation API) so that entering "CloudQ mode" routes the model
|
|
391
|
+
* through the bundled skill. The skill body is shipped under `skills/cloudq/`
|
|
392
|
+
* and referenced through a directory `resourceBase`, so every `{baseDir}`
|
|
393
|
+
* placeholder in the body resolves against the installed package directory.
|
|
394
|
+
*
|
|
395
|
+
* It also exposes a small credential-management API (status / authorize-url /
|
|
396
|
+
* save-code / logout) that the client half renders in the settings panel, so
|
|
397
|
+
* the CloudQ login flow no longer requires touching a terminal.
|
|
398
|
+
*
|
|
399
|
+
* @module dsh-cloudq
|
|
400
|
+
*/
|
|
401
|
+
const LOGO_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "../assets/cloudq.png");
|
|
402
|
+
const LOGO_BUFFER = readFileSync(LOGO_PATH);
|
|
403
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
404
|
+
/** Installed skill directory (SKILL.md + scripts/ + references/). */
|
|
405
|
+
function skillDirectory() {
|
|
406
|
+
return resolve(__dirname, "../skills/cloudq");
|
|
407
|
+
}
|
|
408
|
+
/** Raw SKILL.md body. */
|
|
409
|
+
function rawSkillContent() {
|
|
410
|
+
return readFileSync(resolve(skillDirectory(), "SKILL.md"), "utf8");
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Render the skill body with `{baseDir}` substituted by the installed skill
|
|
414
|
+
* directory. The model then runs `python3 <absolute path>/scripts/...`
|
|
415
|
+
* directly, without having to infer the base directory from the rendered
|
|
416
|
+
* `<skill_resources>` block.
|
|
417
|
+
*/
|
|
418
|
+
function renderedSkillContent() {
|
|
419
|
+
const baseDir = skillDirectory();
|
|
420
|
+
return rawSkillContent().replace(/\{baseDir\}/g, baseDir);
|
|
421
|
+
}
|
|
422
|
+
/** Run one helper from the bundled CloudQ skill. */
|
|
423
|
+
function runScript(scriptName, args, options) {
|
|
424
|
+
return runScript$1(resolve(skillDirectory(), "scripts"), scriptName, args, options);
|
|
425
|
+
}
|
|
426
|
+
/** Remove host filesystem details before returning credential state to the browser. */
|
|
427
|
+
function publicCredentialStatus(status) {
|
|
428
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return status;
|
|
429
|
+
const safeStatus = { ...status };
|
|
430
|
+
delete safeStatus.credential_file;
|
|
431
|
+
return safeStatus;
|
|
432
|
+
}
|
|
433
|
+
/** Wrap runScript so failures keep a stable envelope for the client. */
|
|
434
|
+
async function credentialStatus() {
|
|
435
|
+
return publicCredentialStatus(await runScript("login.py", ["--status"]));
|
|
436
|
+
}
|
|
437
|
+
function authorizeUrl() {
|
|
438
|
+
return runScript("login.py", ["--authorize-url"]);
|
|
439
|
+
}
|
|
440
|
+
async function saveAuthCode(code) {
|
|
441
|
+
return publicCredentialStatus(await runScript("login.py", ["--save", "--stdin"], {
|
|
442
|
+
stdin: JSON.stringify({ code }),
|
|
443
|
+
sensitiveValues: [code]
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
function logout() {
|
|
447
|
+
return runScript("logout.py", [], { jsonOnly: false });
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Validate a long-lived Tencent Cloud AK/SK pair without persisting it.
|
|
451
|
+
* The script performs one read-only CloudQ call to prove the key works.
|
|
452
|
+
*/
|
|
453
|
+
function testAccessKey(secretId, secretKey) {
|
|
454
|
+
return runScript("save_ak.py", ["--test", "--stdin"], {
|
|
455
|
+
stdin: JSON.stringify({
|
|
456
|
+
secretId,
|
|
457
|
+
secretKey
|
|
458
|
+
}),
|
|
459
|
+
sensitiveValues: [secretId, secretKey]
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/** Validate then persist a long-lived AK/SK pair as `type:"ak"`. */
|
|
463
|
+
async function saveAccessKey(secretId, secretKey) {
|
|
464
|
+
return publicCredentialStatus(await runScript("save_ak.py", ["--save", "--stdin"], {
|
|
465
|
+
stdin: JSON.stringify({
|
|
466
|
+
secretId,
|
|
467
|
+
secretKey
|
|
468
|
+
}),
|
|
469
|
+
sensitiveValues: [secretId, secretKey]
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
function cloudqUsageOverview() {
|
|
473
|
+
return runScript("tcloud_api.py", [
|
|
474
|
+
"advisor",
|
|
475
|
+
"advisor.tencentcloudapi.com",
|
|
476
|
+
"DescribeCloudQUsageOverview",
|
|
477
|
+
"2020-07-21",
|
|
478
|
+
"{}"
|
|
479
|
+
]);
|
|
480
|
+
}
|
|
481
|
+
function cloudqUsageDetail({ startTime, endTime, limit = 20, offset = 0 }) {
|
|
482
|
+
return runScript("tcloud_api.py", [
|
|
483
|
+
"advisor",
|
|
484
|
+
"advisor.tencentcloudapi.com",
|
|
485
|
+
"DescribeCloudQUsageDetail",
|
|
486
|
+
"2020-07-21",
|
|
487
|
+
JSON.stringify({
|
|
488
|
+
StartTime: startTime,
|
|
489
|
+
EndTime: endTime,
|
|
490
|
+
Limit: limit,
|
|
491
|
+
Offset: offset
|
|
492
|
+
})
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
function cloudqInspirationList() {
|
|
496
|
+
return runScript("tcloud_api.py", [
|
|
497
|
+
"advisor",
|
|
498
|
+
"advisor.tencentcloudapi.com",
|
|
499
|
+
"DescribeCloudQInspirationList",
|
|
500
|
+
"2020-07-21",
|
|
501
|
+
"{\"Category\":0}"
|
|
502
|
+
]);
|
|
503
|
+
}
|
|
504
|
+
/** Fetch all CloudQ artifact sessions and their archived files. */
|
|
505
|
+
function cloudqArtifactLibrary() {
|
|
506
|
+
return runScript("tcloud_api.py", [
|
|
507
|
+
"advisor",
|
|
508
|
+
"advisor.tencentcloudapi.com",
|
|
509
|
+
"DescribeCloudQArtifactLibrary",
|
|
510
|
+
"2020-07-21",
|
|
511
|
+
"{}"
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
/** Fetch the architecture directory tree available to the current account. */
|
|
515
|
+
function cloudqArchitectureDirectories() {
|
|
516
|
+
return runScript("tcloud_api.py", [
|
|
517
|
+
"advisor",
|
|
518
|
+
"advisor.tencentcloudapi.com",
|
|
519
|
+
"ListDirectoryV2",
|
|
520
|
+
"2020-07-21",
|
|
521
|
+
JSON.stringify({
|
|
522
|
+
Tags: [],
|
|
523
|
+
TagKeys: []
|
|
524
|
+
})
|
|
525
|
+
]);
|
|
526
|
+
}
|
|
527
|
+
/** Fetch one page of diagrams in a CloudQ architecture directory. */
|
|
528
|
+
function cloudqArchitectureList({ folderId, pageNumber = 1, pageSize = 30 }) {
|
|
529
|
+
return runScript("tcloud_api.py", [
|
|
530
|
+
"advisor",
|
|
531
|
+
"advisor.tencentcloudapi.com",
|
|
532
|
+
"DescribeArchList",
|
|
533
|
+
"2020-07-21",
|
|
534
|
+
JSON.stringify({
|
|
535
|
+
PageNumber: pageNumber,
|
|
536
|
+
PageSize: pageSize,
|
|
537
|
+
SearchKey: "",
|
|
538
|
+
FolderId: folderId,
|
|
539
|
+
WithSvgURL: true,
|
|
540
|
+
Tags: [],
|
|
541
|
+
TagKeys: []
|
|
542
|
+
})
|
|
543
|
+
]);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Extract and validate an AK/SK pair from a request body.
|
|
547
|
+
* Both routes (test / save) share the same contract, so the shape check and
|
|
548
|
+
* the error messages live in one place.
|
|
549
|
+
*/
|
|
550
|
+
function readAccessKeyBody(body) {
|
|
551
|
+
const secretId = typeof body?.secretId === "string" ? body.secretId.trim() : "";
|
|
552
|
+
const secretKey = typeof body?.secretKey === "string" ? body.secretKey.trim() : "";
|
|
553
|
+
if (!secretId || !secretKey) throw new HttpError(400, "missing-credential", "SecretId 与 SecretKey 均为必填。");
|
|
554
|
+
return {
|
|
555
|
+
secretId,
|
|
556
|
+
secretKey
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/** Join the plain-text blocks carried by one DSH message. */
|
|
560
|
+
function messageText(content) {
|
|
561
|
+
if (!Array.isArray(content)) return "";
|
|
562
|
+
return content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
563
|
+
}
|
|
564
|
+
/** A durable log proves CloudQ mode through an explicit claim or skill call. */
|
|
565
|
+
function hasCloudqEvidence(events) {
|
|
566
|
+
if (!Array.isArray(events)) return false;
|
|
567
|
+
for (const event of events) {
|
|
568
|
+
if (event?.type === "user/message" && event?.data?.source?.kind === "user") {
|
|
569
|
+
if (/^\s*\/cloudq(?:\s|$)/i.test(messageText(event?.data?.content))) return true;
|
|
570
|
+
}
|
|
571
|
+
if (event?.type !== "tool/call" || event?.data?.name !== "skill") continue;
|
|
572
|
+
let args = event?.data?.arguments;
|
|
573
|
+
if (typeof args === "string") try {
|
|
574
|
+
args = JSON.parse(args);
|
|
575
|
+
} catch {
|
|
576
|
+
args = void 0;
|
|
577
|
+
}
|
|
578
|
+
if ((typeof args?.name === "string" ? args.name : typeof args?.skill === "string" ? args.skill : "").toLowerCase() === "cloudq") return true;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Derive CloudQ session ids from their durable DSH logs. This is the source of
|
|
584
|
+
* truth for historical rows; browser storage is only an optimistic cache for
|
|
585
|
+
* a brand-new blank session that has no log yet.
|
|
586
|
+
*/
|
|
587
|
+
async function detectCloudqSessionIds(ctx) {
|
|
588
|
+
const query = ctx.get("sessionQuery");
|
|
589
|
+
if (query === void 0) throw new HttpError(503, "session-query-unavailable", "当前 DSH 未启用会话查询服务。");
|
|
590
|
+
const records = await query.listSessions();
|
|
591
|
+
const ids = [];
|
|
592
|
+
const queue = [...records];
|
|
593
|
+
const worker = async () => {
|
|
594
|
+
while (queue.length > 0) {
|
|
595
|
+
const sessionId = queue.shift()?.header?.id;
|
|
596
|
+
if (!sessionId) continue;
|
|
597
|
+
try {
|
|
598
|
+
if (hasCloudqEvidence((await query.readSession(sessionId))?.events)) ids.push(sessionId);
|
|
599
|
+
} catch {}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
await Promise.all(Array.from({ length: Math.min(6, queue.length) }, () => worker()));
|
|
603
|
+
return ids;
|
|
604
|
+
}
|
|
605
|
+
/** Settings namespace for the browser card (must equal client card `key`). */
|
|
606
|
+
const SETTINGS_NAMESPACE = "dsh-cloudq";
|
|
607
|
+
/** Empty schema; the card is credential-focused and stores nothing. */
|
|
608
|
+
const Config = Schema.object({});
|
|
609
|
+
/** Settings namespace for the plugin-manager card. */
|
|
610
|
+
const PLUGIN_MANAGER_NAMESPACE = "dsh-plugin-manager";
|
|
611
|
+
const name = "dsh-cloudq";
|
|
612
|
+
const inject = [
|
|
613
|
+
"skills",
|
|
614
|
+
"webServer",
|
|
615
|
+
"settings",
|
|
616
|
+
"sessionQuery"
|
|
617
|
+
];
|
|
618
|
+
function apply(ctx) {
|
|
619
|
+
ctx.effect(() => ctx.settings.register(SETTINGS_NAMESPACE, Config, { base: {} }), "dsh-cloudq: settings namespace");
|
|
620
|
+
ctx.effect(() => ctx.settings.register(PLUGIN_MANAGER_NAMESPACE, Config, { base: {} }), "dsh-cloudq: plugin-manager settings namespace");
|
|
621
|
+
ctx.effect(() => ctx.skills.register({
|
|
622
|
+
name: "cloudq",
|
|
623
|
+
description: "用户咨询腾讯云产品资源、AWS、阿里云等多云资源时,查看智能顾问架构图、架构目录、架构详情、架构评估结果、绘制架构图、开通智能顾问时、AI智能巡检、AI容量监测、AI混沌演练、AI云诊断、主动预警、架构健康度、云运维问答、云资源查询、云成本优化、安全合规、云资源盘点、闲置资源检查、云产品最佳实践等AIOps、ChatOps、CloudOps操作时使用。",
|
|
624
|
+
whenToUse: "用户要求进入 CloudQ 模式、使用 CloudQ、咨询云上架构/资源/成本/巡检/诊断等多云或腾讯云运维问题,或点击输入栏 CloudQ 按钮时使用;也适用于架构图查看/评估、AI 巡检、容量监测、混沌演练、云诊断等场景。",
|
|
625
|
+
source: "bundled",
|
|
626
|
+
resourceBase: {
|
|
627
|
+
kind: "directory",
|
|
628
|
+
path: skillDirectory()
|
|
629
|
+
},
|
|
630
|
+
content: renderedSkillContent(),
|
|
631
|
+
invocation: {
|
|
632
|
+
modelInvocable: true,
|
|
633
|
+
userInvocable: true
|
|
634
|
+
}
|
|
635
|
+
}), "dsh-cloudq: CloudQ skill");
|
|
636
|
+
ctx.effect(() => {
|
|
637
|
+
const disposers = [];
|
|
638
|
+
disposers.push(ctx.webServer.register({
|
|
639
|
+
kind: "exact",
|
|
640
|
+
path: "/api/dsh-cloudq/credential",
|
|
641
|
+
handler: async (request, response) => {
|
|
642
|
+
try {
|
|
643
|
+
assertSafeRequest(request, "GET");
|
|
644
|
+
sendJson(response, 200, {
|
|
645
|
+
ok: true,
|
|
646
|
+
status: await credentialStatus()
|
|
647
|
+
});
|
|
648
|
+
} catch (error) {
|
|
649
|
+
sendError(response, error);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}));
|
|
653
|
+
disposers.push(ctx.webServer.register({
|
|
654
|
+
kind: "exact",
|
|
655
|
+
path: "/api/dsh-cloudq/login/url",
|
|
656
|
+
handler: async (request, response) => {
|
|
657
|
+
try {
|
|
658
|
+
assertSafeRequest(request, "POST");
|
|
659
|
+
const result = await authorizeUrl();
|
|
660
|
+
sendJson(response, 200, {
|
|
661
|
+
ok: true,
|
|
662
|
+
authorize_url: result.authorize_url,
|
|
663
|
+
state: result.state
|
|
664
|
+
});
|
|
665
|
+
} catch (error) {
|
|
666
|
+
sendError(response, error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}));
|
|
670
|
+
disposers.push(ctx.webServer.register({
|
|
671
|
+
kind: "exact",
|
|
672
|
+
path: "/api/dsh-cloudq/login/save",
|
|
673
|
+
handler: async (request, response) => {
|
|
674
|
+
try {
|
|
675
|
+
assertSafeRequest(request, "POST");
|
|
676
|
+
const body = await readJsonBody(request);
|
|
677
|
+
const code = typeof body?.code === "string" ? body.code.trim() : "";
|
|
678
|
+
if (!code) throw new HttpError(400, "missing-code", "缺少授权码。");
|
|
679
|
+
sendJson(response, 200, {
|
|
680
|
+
ok: true,
|
|
681
|
+
status: await saveAuthCode(code)
|
|
682
|
+
});
|
|
683
|
+
} catch (error) {
|
|
684
|
+
sendError(response, error);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}));
|
|
688
|
+
disposers.push(ctx.webServer.register({
|
|
689
|
+
kind: "exact",
|
|
690
|
+
path: "/api/dsh-cloudq/logout",
|
|
691
|
+
handler: async (request, response) => {
|
|
692
|
+
try {
|
|
693
|
+
assertSafeRequest(request, "POST");
|
|
694
|
+
sendJson(response, 200, {
|
|
695
|
+
ok: true,
|
|
696
|
+
status: await logout()
|
|
697
|
+
});
|
|
698
|
+
} catch (error) {
|
|
699
|
+
sendError(response, error);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}));
|
|
703
|
+
disposers.push(ctx.webServer.register({
|
|
704
|
+
kind: "exact",
|
|
705
|
+
path: "/api/dsh-cloudq/credential/test",
|
|
706
|
+
handler: async (request, response) => {
|
|
707
|
+
try {
|
|
708
|
+
assertSafeRequest(request, "POST");
|
|
709
|
+
const { secretId, secretKey } = readAccessKeyBody(await readJsonBody(request));
|
|
710
|
+
sendJson(response, 200, {
|
|
711
|
+
ok: true,
|
|
712
|
+
result: await testAccessKey(secretId, secretKey)
|
|
713
|
+
});
|
|
714
|
+
} catch (error) {
|
|
715
|
+
sendError(response, error);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}));
|
|
719
|
+
disposers.push(ctx.webServer.register({
|
|
720
|
+
kind: "exact",
|
|
721
|
+
path: "/api/dsh-cloudq/credential/save",
|
|
722
|
+
handler: async (request, response) => {
|
|
723
|
+
try {
|
|
724
|
+
assertSafeRequest(request, "POST");
|
|
725
|
+
const { secretId, secretKey } = readAccessKeyBody(await readJsonBody(request));
|
|
726
|
+
sendJson(response, 200, {
|
|
727
|
+
ok: true,
|
|
728
|
+
status: await saveAccessKey(secretId, secretKey)
|
|
729
|
+
});
|
|
730
|
+
} catch (error) {
|
|
731
|
+
sendError(response, error);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}));
|
|
735
|
+
disposers.push(ctx.webServer.register({
|
|
736
|
+
kind: "exact",
|
|
737
|
+
path: "/api/dsh-cloudq/logo.png",
|
|
738
|
+
handler: (request, response) => {
|
|
739
|
+
try {
|
|
740
|
+
assertSafeRequest(request, "GET");
|
|
741
|
+
response.statusCode = 200;
|
|
742
|
+
response.setHeader("Content-Type", "image/png");
|
|
743
|
+
response.setHeader("Cache-Control", "public, max-age=86400");
|
|
744
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
745
|
+
response.end(LOGO_BUFFER);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
sendError(response, error);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}));
|
|
751
|
+
disposers.push(ctx.webServer.register({
|
|
752
|
+
kind: "exact",
|
|
753
|
+
path: "/api/dsh-cloudq/plugins",
|
|
754
|
+
handler: (request, response) => {
|
|
755
|
+
try {
|
|
756
|
+
assertSafeRequest(request, "GET");
|
|
757
|
+
sendJson(response, 200, {
|
|
758
|
+
ok: true,
|
|
759
|
+
plugins: listPlugins(ctx.baseUrl)
|
|
760
|
+
});
|
|
761
|
+
} catch (error) {
|
|
762
|
+
sendError(response, error);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}));
|
|
766
|
+
disposers.push(ctx.webServer.register({
|
|
767
|
+
kind: "exact",
|
|
768
|
+
path: "/api/dsh-cloudq/plugins/toggle",
|
|
769
|
+
handler: async (request, response) => {
|
|
770
|
+
try {
|
|
771
|
+
assertSafeRequest(request, "POST");
|
|
772
|
+
const body = await readJsonBody(request);
|
|
773
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
774
|
+
if (!id) throw new HttpError(400, "missing-id", "缺少插件 ID。");
|
|
775
|
+
const disabled = body?.disabled === true;
|
|
776
|
+
sendJson(response, 200, {
|
|
777
|
+
ok: true,
|
|
778
|
+
result: setPluginDisabled(ctx.baseUrl, id, disabled)
|
|
779
|
+
});
|
|
780
|
+
} catch (error) {
|
|
781
|
+
sendError(response, error);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}));
|
|
785
|
+
disposers.push(ctx.webServer.register({
|
|
786
|
+
kind: "exact",
|
|
787
|
+
path: "/api/dsh-cloudq/usage",
|
|
788
|
+
handler: async (request, response) => {
|
|
789
|
+
try {
|
|
790
|
+
assertSafeRequest(request, "GET");
|
|
791
|
+
const url = new URL(request.url, "http://localhost");
|
|
792
|
+
const startTime = url.searchParams.get("start") ?? "";
|
|
793
|
+
const endTime = url.searchParams.get("end") ?? "";
|
|
794
|
+
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
795
|
+
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
796
|
+
const timestampPattern = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
797
|
+
if (!timestampPattern.test(startTime) || !timestampPattern.test(endTime)) throw new HttpError(400, "invalid-range", "The start and end timestamps are invalid.");
|
|
798
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0) throw new HttpError(400, "invalid-pagination", "The pagination parameters are invalid.");
|
|
799
|
+
const [overview, detail] = await Promise.all([cloudqUsageOverview(), cloudqUsageDetail({
|
|
800
|
+
startTime,
|
|
801
|
+
endTime,
|
|
802
|
+
limit,
|
|
803
|
+
offset
|
|
804
|
+
})]);
|
|
805
|
+
sendJson(response, 200, {
|
|
806
|
+
ok: true,
|
|
807
|
+
overview: overview?.data ?? {},
|
|
808
|
+
detail: detail?.data?.Detail ?? [],
|
|
809
|
+
total: detail?.data?.TotalCount ?? detail?.data?.Detail?.length ?? 0
|
|
810
|
+
});
|
|
811
|
+
} catch (error) {
|
|
812
|
+
sendError(response, error);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}));
|
|
816
|
+
disposers.push(ctx.webServer.register({
|
|
817
|
+
kind: "exact",
|
|
818
|
+
path: "/api/dsh-cloudq/inspirations",
|
|
819
|
+
handler: async (request, response) => {
|
|
820
|
+
try {
|
|
821
|
+
assertSafeRequest(request, "GET");
|
|
822
|
+
const result = await cloudqInspirationList();
|
|
823
|
+
sendJson(response, 200, {
|
|
824
|
+
ok: true,
|
|
825
|
+
inspirations: result?.data?.InspirationSet ?? [],
|
|
826
|
+
total: result?.data?.TotalCount ?? 0
|
|
827
|
+
});
|
|
828
|
+
} catch (error) {
|
|
829
|
+
sendError(response, error);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}));
|
|
833
|
+
disposers.push(ctx.webServer.register({
|
|
834
|
+
kind: "exact",
|
|
835
|
+
path: "/api/dsh-cloudq/artifacts",
|
|
836
|
+
handler: async (request, response) => {
|
|
837
|
+
try {
|
|
838
|
+
assertSafeRequest(request, "GET");
|
|
839
|
+
const result = await cloudqArtifactLibrary();
|
|
840
|
+
const sessions = Array.isArray(result?.data?.Sessions) ? result.data.Sessions : [];
|
|
841
|
+
sendJson(response, 200, {
|
|
842
|
+
ok: true,
|
|
843
|
+
sessions,
|
|
844
|
+
total: Number(result?.data?.TotalCount) || sessions.length
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
sendError(response, error);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}));
|
|
851
|
+
disposers.push(ctx.webServer.register({
|
|
852
|
+
kind: "exact",
|
|
853
|
+
path: "/api/dsh-cloudq/sessions",
|
|
854
|
+
handler: async (request, response) => {
|
|
855
|
+
try {
|
|
856
|
+
assertSafeRequest(request, "GET");
|
|
857
|
+
sendJson(response, 200, {
|
|
858
|
+
ok: true,
|
|
859
|
+
sessionIds: await detectCloudqSessionIds(ctx)
|
|
860
|
+
});
|
|
861
|
+
} catch (error) {
|
|
862
|
+
sendError(response, error);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}));
|
|
866
|
+
disposers.push(ctx.webServer.register({
|
|
867
|
+
kind: "exact",
|
|
868
|
+
path: "/api/dsh-cloudq/architecture/directories",
|
|
869
|
+
handler: async (request, response) => {
|
|
870
|
+
try {
|
|
871
|
+
assertSafeRequest(request, "GET");
|
|
872
|
+
const result = await cloudqArchitectureDirectories();
|
|
873
|
+
sendJson(response, 200, {
|
|
874
|
+
ok: true,
|
|
875
|
+
folders: Array.isArray(result?.data?.Folders) ? result.data.Folders : [],
|
|
876
|
+
firstFolderId: Number(result?.data?.FirstFolderId) || null
|
|
877
|
+
});
|
|
878
|
+
} catch (error) {
|
|
879
|
+
sendError(response, error);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}));
|
|
883
|
+
disposers.push(ctx.webServer.register({
|
|
884
|
+
kind: "exact",
|
|
885
|
+
path: "/api/dsh-cloudq/architecture/list",
|
|
886
|
+
handler: async (request, response) => {
|
|
887
|
+
try {
|
|
888
|
+
assertSafeRequest(request, "GET");
|
|
889
|
+
const url = new URL(request.url, "http://localhost");
|
|
890
|
+
const folderId = Number(url.searchParams.get("folderId"));
|
|
891
|
+
if (!Number.isSafeInteger(folderId) || folderId <= 0) throw new HttpError(400, "invalid-folder-id", "folderId 必须是正整数。");
|
|
892
|
+
const result = await cloudqArchitectureList({ folderId });
|
|
893
|
+
const architectures = Array.isArray(result?.data?.ArchList) ? result.data.ArchList : [];
|
|
894
|
+
sendJson(response, 200, {
|
|
895
|
+
ok: true,
|
|
896
|
+
architectures,
|
|
897
|
+
total: Number(result?.data?.TotalCount) || architectures.length
|
|
898
|
+
});
|
|
899
|
+
} catch (error) {
|
|
900
|
+
sendError(response, error);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}));
|
|
904
|
+
return () => {
|
|
905
|
+
for (const dispose of disposers) dispose();
|
|
906
|
+
};
|
|
907
|
+
}, "dsh-cloudq: credential, plugin-manager and data routes");
|
|
908
|
+
}
|
|
909
|
+
//#endregion
|
|
910
|
+
export { Config, apply, inject, name };
|