dsh-cloudq 0.2.1 → 0.3.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.
Files changed (3) hide show
  1. package/lib/client.js +1 -1
  2. package/lib/index.js +235 -189
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -3385,7 +3385,7 @@ display: none;
3385
3385
  });
3386
3386
  } catch (error) {
3387
3387
  setValidated(false);
3388
- const invalid = error instanceof CloudQApiError && error.code !== "network-error" && error.code !== "invalid-response" && error.code !== "script-launch-failed";
3388
+ const invalid = error instanceof CloudQApiError && error.code !== "network-error" && error.code !== "invalid-response";
3389
3389
  setFeedback({
3390
3390
  kind: "error",
3391
3391
  text: invalid ? "AKSK 无效,请检查后重新配置。" : error.message
package/lib/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
- import { spawn, spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { get } from "node:https";
2
+ import { spawn } from "node:child_process";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { get, request } from "node:https";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import Schema from "@deepseek-ai/schemastery";
8
- import { Buffer as Buffer$1 } from "node:buffer";
8
+ import { Buffer } from "node:buffer";
9
9
  import yaml from "js-yaml";
10
+ import { createHash, createHmac } from "node:crypto";
11
+ import { homedir } from "node:os";
10
12
  //#region src/http.js
11
13
  /** Maximum accepted JSON request body size. */
12
14
  const MAX_JSON_BODY_BYTES = 65536;
@@ -115,7 +117,7 @@ function readJsonBody(request, { maxBytes = MAX_JSON_BODY_BYTES } = {}) {
115
117
  };
116
118
  request.on("data", (chunk) => {
117
119
  if (settled) return;
118
- const buffer = Buffer$1.isBuffer(chunk) ? chunk : Buffer$1.from(chunk);
120
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
119
121
  bytes += buffer.byteLength;
120
122
  if (bytes > maxBytes) {
121
123
  rejectOnce(new HttpError(413, "payload-too-large", "The JSON request body is too large."));
@@ -131,7 +133,7 @@ function readJsonBody(request, { maxBytes = MAX_JSON_BODY_BYTES } = {}) {
131
133
  return;
132
134
  }
133
135
  try {
134
- resolveBody(JSON.parse(Buffer$1.concat(chunks).toString("utf8")));
136
+ resolveBody(JSON.parse(Buffer.concat(chunks).toString("utf8")));
135
137
  } catch {
136
138
  rejectBody(new HttpError(400, "invalid-json", "The request body is not valid JSON."));
137
139
  }
@@ -288,118 +290,157 @@ function setPluginDisabled(baseUrl, id, disabled) {
288
290
  };
289
291
  }
290
292
  //#endregion
291
- //#region src/script-runner.js
292
- const MAX_SCRIPT_OUTPUT_BYTES = 1048576;
293
- let resolvedPythonCommand = null;
294
- function pythonCommand() {
295
- if (resolvedPythonCommand) return resolvedPythonCommand;
296
- for (const candidate of [
297
- "python3",
298
- "python",
299
- "py"
300
- ]) try {
301
- if (spawnSync(candidate, ["--version"], {
302
- stdio: "ignore",
303
- timeout: 5e3
304
- }).status === 0) {
305
- resolvedPythonCommand = candidate;
306
- return candidate;
307
- }
308
- } catch {}
309
- return "python3";
293
+ //#region src/tcloud.js
294
+ /**
295
+ * Node-native Tencent Cloud API client (TC3-HMAC-SHA256) and credential
296
+ * store for the dsh-cloudq host.
297
+ *
298
+ * The DSH host itself runs on Node, so — same as dsh-cos — every panel API
299
+ * call happens in-process with zero external runtime dependency. Python only
300
+ * remains for the agent-driven skill conversation flow.
301
+ *
302
+ * @module dsh-cloudq/tcloud
303
+ */
304
+ const CREDENTIAL_FILE = join(homedir(), ".tencent-cloudq", "credential.json");
305
+ const sha256 = (value) => createHash("sha256").update(value, "utf8").digest("hex");
306
+ const hmacSha256 = (key, value) => createHmac("sha256", key).update(value, "utf8").digest();
307
+ const hmacSha256Hex = (key, value) => createHmac("sha256", key).update(value, "utf8").digest("hex");
308
+ /** Mask a SecretId exactly like the Python helpers: keep only the last 4. */
309
+ function maskSecretId(value) {
310
+ const text = String(value ?? "");
311
+ return text.length <= 4 ? "*".repeat(text.length) : "*".repeat(text.length - 4) + text.slice(-4);
312
+ }
313
+ /**
314
+ * Read the stored credential shared with the CloudQ skill.
315
+ * Only the AK/SK entries (`type: "ak"`, optional STS token) can TC3-sign;
316
+ * OAuth/Connector entries carry no SecretKey and surface as NeedAuth.
317
+ */
318
+ function readCredential() {
319
+ let data;
320
+ try {
321
+ data = JSON.parse(readFileSync(CREDENTIAL_FILE, "utf8"));
322
+ } catch {
323
+ return null;
324
+ }
325
+ const secretId = typeof data?.secretId === "string" ? data.secretId : "";
326
+ const secretKey = typeof data?.secretKey === "string" ? data.secretKey : "";
327
+ if (!secretId || !secretKey) return null;
328
+ return {
329
+ secretId,
330
+ secretKey,
331
+ token: typeof data?.token === "string" ? data.token : "",
332
+ authType: typeof data?.type === "string" ? data.type : "ak"
333
+ };
310
334
  }
311
- function safeCode(value) {
312
- return typeof value === "string" && /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : "script-failed";
335
+ /** Credential state for the settings UI (never exposes the SecretKey). */
336
+ function credentialStatus$1() {
337
+ const credential = readCredential();
338
+ if (!credential) return { logged_in: false };
339
+ return {
340
+ logged_in: true,
341
+ secret_id_masked: maskSecretId(credential.secretId),
342
+ auth_type: credential.authType
343
+ };
344
+ }
345
+ /** Persist an AK/SK pair atomically, mirroring credential_manager (dir 700, file 600). */
346
+ function saveCredential(secretId, secretKey) {
347
+ const data = {
348
+ type: "ak",
349
+ secretId,
350
+ secretKey,
351
+ token: "",
352
+ expiresAt: 0,
353
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
354
+ };
355
+ const directory = dirname(CREDENTIAL_FILE);
356
+ mkdirSync(directory, { recursive: true });
357
+ const temporary = `${CREDENTIAL_FILE}.tmp-${process.pid}`;
358
+ writeFileSync(temporary, JSON.stringify(data, null, 2), {
359
+ encoding: "utf8",
360
+ mode: 384
361
+ });
362
+ renameSync(temporary, CREDENTIAL_FILE);
363
+ if (process.platform !== "win32") try {
364
+ chmodSync(directory, 448);
365
+ chmodSync(CREDENTIAL_FILE, 384);
366
+ } catch {}
313
367
  }
314
- function redact(value, sensitiveValues) {
315
- let text = String(value ?? "");
316
- for (const sensitive of sensitiveValues) if (sensitive) text = text.replaceAll(sensitive, "[redacted]");
317
- return text.replace(/[\r\n\t]+/g, " ").slice(0, 300);
368
+ /** Remove the stored credential (退出登录). */
369
+ function deleteCredential() {
370
+ try {
371
+ unlinkSync(CREDENTIAL_FILE);
372
+ } catch {}
318
373
  }
319
374
  /**
320
- * Run one bundled Python helper with bounded output and optional stdin.
321
- * @param {string} scriptsDirectory Absolute scripts directory.
322
- * @param {string} scriptName Bundled script filename.
323
- * @param {string[]} args Non-sensitive command-line arguments.
324
- * @param {object} options Execution options.
325
- * @returns {Promise<Record<string, unknown>>} Parsed helper response.
375
+ * Call a Tencent Cloud API with TC3-HMAC-SHA256 signing.
376
+ * Returns the unwrapped `Response` payload; API errors become HttpError with
377
+ * the upstream code (AuthFailure.* etc.) so callers can map them like the
378
+ * Python helpers did.
326
379
  */
327
- function runScript$1(scriptsDirectory, scriptName, args, { timeoutMs = 3e4, jsonOnly = true, stdin, sensitiveValues = [], spawnProcess = spawn } = {}) {
328
- return new Promise((resolveRun, rejectRun) => {
329
- const script = resolve(scriptsDirectory, scriptName);
330
- const child = spawnProcess(pythonCommand(), [script, ...args], {
331
- stdio: [
332
- stdin === void 0 ? "ignore" : "pipe",
333
- "pipe",
334
- "pipe"
335
- ],
336
- timeout: timeoutMs
337
- });
338
- let stdout = "";
339
- let stderr = "";
340
- let outputBytes = 0;
341
- let settled = false;
342
- const rejectOnce = (error) => {
343
- if (settled) return;
344
- settled = true;
345
- rejectRun(error);
346
- };
347
- const resolveOnce = (value) => {
348
- if (settled) return;
349
- settled = true;
350
- resolveRun(value);
351
- };
352
- const collect = (current, chunk) => {
353
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
354
- outputBytes += buffer.byteLength;
355
- if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
356
- child.kill();
357
- rejectOnce(new HttpError(502, "script-output-too-large", "The CloudQ helper returned too much data."));
358
- return current;
359
- }
360
- return current + buffer.toString("utf8");
361
- };
362
- child.stdout.on("data", (chunk) => {
363
- stdout = collect(stdout, chunk);
364
- });
365
- child.stderr.on("data", (chunk) => {
366
- stderr = collect(stderr, chunk);
367
- });
368
- child.on("error", () => {
369
- rejectOnce(new HttpError(502, "script-launch-failed", "无法启动 Python 运行环境,请先安装 Python 3 后重试。"));
380
+ async function callTcloudApi({ service, host, action, version, payload = {} }, credential = readCredential()) {
381
+ if (!credential) throw new HttpError(401, "NeedAuth", "尚未配置 AK/SK,请前往「设置 → 插件 → CloudQ」完成配置。");
382
+ const body = JSON.stringify(payload);
383
+ const timestamp = Math.floor(Date.now() / 1e3);
384
+ const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
385
+ const canonicalRequest = [
386
+ "POST",
387
+ "/",
388
+ "",
389
+ `content-type:application/json\nhost:${host}\nx-tc-action:${action.toLowerCase()}`,
390
+ "",
391
+ "content-type;host;x-tc-action",
392
+ sha256(body)
393
+ ].join("\n");
394
+ const stringToSign = [
395
+ "TC3-HMAC-SHA256",
396
+ String(timestamp),
397
+ `${date}/${service}/tc3_request`,
398
+ sha256(canonicalRequest)
399
+ ].join("\n");
400
+ const keyDate = hmacSha256(`TC3${credential.secretKey}`, date);
401
+ const keyService = hmacSha256(keyDate, service);
402
+ const keySigning = hmacSha256(keyService, "tc3_request");
403
+ const signature = hmacSha256Hex(keySigning, stringToSign);
404
+ const authorization = `TC3-HMAC-SHA256 Credential=${credential.secretId}/${date}/${service}/tc3_request, SignedHeaders=content-type;host;x-tc-action, Signature=${signature}`;
405
+ const headers = {
406
+ "Content-Type": "application/json",
407
+ "X-TC-Action": action,
408
+ "X-TC-Version": version,
409
+ "X-TC-Timestamp": String(timestamp),
410
+ Authorization: authorization
411
+ };
412
+ if (credential.token) headers["X-TC-Token"] = credential.token;
413
+ const text = await new Promise((resolveRequest, rejectRequest) => {
414
+ const request$1 = request({
415
+ host,
416
+ path: "/",
417
+ method: "POST",
418
+ headers,
419
+ timeout: 3e4
420
+ }, (response) => {
421
+ let bodyText = "";
422
+ response.on("data", (chunk) => {
423
+ bodyText += chunk;
424
+ if (bodyText.length > 1048576) request$1.destroy();
425
+ });
426
+ response.on("end", () => resolveRequest(bodyText));
370
427
  });
371
- child.on("close", (code) => {
372
- if (settled) return;
373
- const trimmed = stdout.trim();
374
- if (!jsonOnly) {
375
- if (code !== 0) rejectOnce(new HttpError(502, "script-failed", "The CloudQ helper failed."));
376
- else resolveOnce({
377
- success: true,
378
- message: redact(trimmed, sensitiveValues)
379
- });
380
- return;
381
- }
382
- let parsed;
383
- try {
384
- parsed = trimmed ? JSON.parse(trimmed) : {};
385
- } catch {
386
- rejectOnce(new HttpError(502, "script-invalid-output", "The CloudQ helper returned an invalid response."));
387
- return;
388
- }
389
- if (code !== 0 || parsed?.ok === false || parsed?.success === false) {
390
- const codeValue = safeCode(parsed?.error?.code ?? parsed?.code);
391
- rejectOnce(new HttpError(502, codeValue, "The CloudQ helper failed."));
392
- return;
393
- }
394
- resolveOnce(parsed);
428
+ request$1.on("error", () => rejectRequest(new HttpError(502, "network-error", "无法连接腾讯云服务,请检查网络后重试。")));
429
+ request$1.on("timeout", () => {
430
+ request$1.destroy();
431
+ rejectRequest(new HttpError(504, "network-timeout", "腾讯云服务响应超时,请稍后重试。"));
395
432
  });
396
- if (stdin !== void 0) {
397
- child.stdin.on("error", () => {
398
- rejectOnce(new HttpError(502, "script-input-failed", "The CloudQ helper could not receive its input."));
399
- });
400
- child.stdin.end(stdin);
401
- }
433
+ request$1.end(body);
402
434
  });
435
+ let parsed;
436
+ try {
437
+ parsed = JSON.parse(text);
438
+ } catch {
439
+ throw new HttpError(502, "invalid-response", "腾讯云服务返回了无法识别的响应。");
440
+ }
441
+ const envelope = parsed?.Response ?? {};
442
+ if (envelope.Error) throw new HttpError(502, String(envelope.Error.Code ?? "api-error"), String(envelope.Error.Message ?? "腾讯云接口调用失败。"));
443
+ return envelope;
403
444
  }
404
445
  //#endregion
405
446
  //#region src/index.js
@@ -554,119 +595,124 @@ function renderedSkillContent() {
554
595
  const baseDir = skillDirectory();
555
596
  return rawSkillContent().replace(/\{baseDir\}/g, baseDir);
556
597
  }
557
- /** Run one helper from the bundled CloudQ skill. */
558
- async function runScript(scriptName, args, options) {
598
+ /** Map upstream TC error codes to stable, actionable client-facing errors. */
599
+ function normalizeApiError(error) {
600
+ if (error instanceof HttpError) {
601
+ if (error.code === "NeedAuth") throw error;
602
+ if (typeof error.code === "string" && error.code.startsWith("AuthFailure")) throw new HttpError(401, "AuthFailure", "AK/SK 无效或权限不足,请前往「设置 → 插件 → CloudQ」检查配置。");
603
+ }
604
+ throw error;
605
+ }
606
+ /** Wrap one API call with the credential-error mapping. */
607
+ async function callCloudq(spec) {
559
608
  try {
560
- return await runScript$1(resolve(skillDirectory(), "scripts"), scriptName, args, options);
609
+ return { data: await callTcloudApi(spec) };
561
610
  } catch (error) {
562
- if (error instanceof HttpError) {
563
- if (error.code === "NeedAuth") throw new HttpError(401, "NeedAuth", "尚未配置 AK/SK,请前往「设置 → 插件 → CloudQ」完成配置。");
564
- if (error.code === "CredentialExpired") throw new HttpError(401, "CredentialExpired", "凭证已过期,请前往「设置 → 插件 → CloudQ」重新配置。");
565
- if (typeof error.code === "string" && error.code.startsWith("AuthFailure")) throw new HttpError(401, "AuthFailure", "AK/SK 无效或权限不足,请前往「设置 → 插件 → CloudQ」检查配置。");
566
- }
611
+ normalizeApiError(error);
567
612
  throw error;
568
613
  }
569
614
  }
570
- /** Remove host filesystem details before returning credential state to the browser. */
571
- function publicCredentialStatus(status) {
572
- if (!status || typeof status !== "object" || Array.isArray(status)) return status;
573
- const safeStatus = { ...status };
574
- delete safeStatus.credential_file;
575
- return safeStatus;
576
- }
577
- /** Wrap runScript so failures keep a stable envelope for the client. */
578
- async function credentialStatus() {
579
- return publicCredentialStatus(await runScript("login.py", ["--status"]));
615
+ /** Credential state for the settings UI (never exposes the SecretKey). */
616
+ function credentialStatus() {
617
+ return credentialStatus$1();
580
618
  }
581
619
  function logout() {
582
- return runScript("logout.py", [], { jsonOnly: false });
620
+ deleteCredential();
621
+ return { message: "已退出登录。" };
583
622
  }
584
623
  /**
585
624
  * Validate a long-lived Tencent Cloud AK/SK pair without persisting it.
586
- * The script performs one read-only CloudQ call to prove the key works.
625
+ * DescribeCloudQUsageOverview is read-only, parameterless, and CloudQ's own
626
+ * API — a successful call proves both the key and the CloudQ enrollment.
587
627
  */
588
- function testAccessKey(secretId, secretKey) {
589
- return runScript("save_ak.py", ["--test", "--stdin"], {
590
- stdin: JSON.stringify({
628
+ async function testAccessKey(secretId, secretKey) {
629
+ try {
630
+ await callTcloudApi({
631
+ service: "advisor",
632
+ host: "advisor.tencentcloudapi.com",
633
+ action: "DescribeCloudQUsageOverview",
634
+ version: "2020-07-21"
635
+ }, {
591
636
  secretId,
592
- secretKey
593
- }),
594
- sensitiveValues: [secretId, secretKey]
595
- });
637
+ secretKey,
638
+ token: ""
639
+ });
640
+ return {
641
+ valid: true,
642
+ secret_id_masked: maskSecretId(secretId)
643
+ };
644
+ } catch (error) {
645
+ normalizeApiError(error);
646
+ throw error;
647
+ }
596
648
  }
597
649
  /** Validate then persist a long-lived AK/SK pair as `type:"ak"`. */
598
650
  async function saveAccessKey(secretId, secretKey) {
599
- return publicCredentialStatus(await runScript("save_ak.py", ["--save", "--stdin"], {
600
- stdin: JSON.stringify({
601
- secretId,
602
- secretKey
603
- }),
604
- sensitiveValues: [secretId, secretKey]
605
- }));
651
+ await testAccessKey(secretId, secretKey);
652
+ saveCredential(secretId, secretKey);
653
+ return credentialStatus$1();
606
654
  }
607
655
  function cloudqUsageOverview() {
608
- return runScript("tcloud_api.py", [
609
- "advisor",
610
- "advisor.tencentcloudapi.com",
611
- "DescribeCloudQUsageOverview",
612
- "2020-07-21",
613
- "{}"
614
- ]);
656
+ return callCloudq({
657
+ service: "advisor",
658
+ host: "advisor.tencentcloudapi.com",
659
+ action: "DescribeCloudQUsageOverview",
660
+ version: "2020-07-21"
661
+ });
615
662
  }
616
663
  function cloudqUsageDetail({ startTime, endTime, limit = 20, offset = 0 }) {
617
- return runScript("tcloud_api.py", [
618
- "advisor",
619
- "advisor.tencentcloudapi.com",
620
- "DescribeCloudQUsageDetail",
621
- "2020-07-21",
622
- JSON.stringify({
664
+ return callCloudq({
665
+ service: "advisor",
666
+ host: "advisor.tencentcloudapi.com",
667
+ action: "DescribeCloudQUsageDetail",
668
+ version: "2020-07-21",
669
+ payload: {
623
670
  StartTime: startTime,
624
671
  EndTime: endTime,
625
672
  Limit: limit,
626
673
  Offset: offset
627
- })
628
- ]);
674
+ }
675
+ });
629
676
  }
630
677
  function cloudqInspirationList() {
631
- return runScript("tcloud_api.py", [
632
- "advisor",
633
- "advisor.tencentcloudapi.com",
634
- "DescribeCloudQInspirationList",
635
- "2020-07-21",
636
- "{\"Category\":0}"
637
- ]);
678
+ return callCloudq({
679
+ service: "advisor",
680
+ host: "advisor.tencentcloudapi.com",
681
+ action: "DescribeCloudQInspirationList",
682
+ version: "2020-07-21",
683
+ payload: { Category: 0 }
684
+ });
638
685
  }
639
686
  /** Fetch all CloudQ artifact sessions and their archived files. */
640
687
  function cloudqArtifactLibrary() {
641
- return runScript("tcloud_api.py", [
642
- "advisor",
643
- "advisor.tencentcloudapi.com",
644
- "DescribeCloudQArtifactLibrary",
645
- "2020-07-21",
646
- "{}"
647
- ]);
688
+ return callCloudq({
689
+ service: "advisor",
690
+ host: "advisor.tencentcloudapi.com",
691
+ action: "DescribeCloudQArtifactLibrary",
692
+ version: "2020-07-21"
693
+ });
648
694
  }
649
695
  /** Fetch the architecture directory tree available to the current account. */
650
696
  function cloudqArchitectureDirectories() {
651
- return runScript("tcloud_api.py", [
652
- "advisor",
653
- "advisor.tencentcloudapi.com",
654
- "ListDirectoryV2",
655
- "2020-07-21",
656
- JSON.stringify({
697
+ return callCloudq({
698
+ service: "advisor",
699
+ host: "advisor.tencentcloudapi.com",
700
+ action: "ListDirectoryV2",
701
+ version: "2020-07-21",
702
+ payload: {
657
703
  Tags: [],
658
704
  TagKeys: []
659
- })
660
- ]);
705
+ }
706
+ });
661
707
  }
662
708
  /** Fetch one page of diagrams in a CloudQ architecture directory. */
663
709
  function cloudqArchitectureList({ folderId, pageNumber = 1, pageSize = 30 }) {
664
- return runScript("tcloud_api.py", [
665
- "advisor",
666
- "advisor.tencentcloudapi.com",
667
- "DescribeArchList",
668
- "2020-07-21",
669
- JSON.stringify({
710
+ return callCloudq({
711
+ service: "advisor",
712
+ host: "advisor.tencentcloudapi.com",
713
+ action: "DescribeArchList",
714
+ version: "2020-07-21",
715
+ payload: {
670
716
  PageNumber: pageNumber,
671
717
  PageSize: pageSize,
672
718
  SearchKey: "",
@@ -674,8 +720,8 @@ function cloudqArchitectureList({ folderId, pageNumber = 1, pageSize = 30 }) {
674
720
  WithSvgURL: true,
675
721
  Tags: [],
676
722
  TagKeys: []
677
- })
678
- ]);
723
+ }
724
+ });
679
725
  }
680
726
  /**
681
727
  * Extract and validate an AK/SK pair from a request body.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cloudq",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CloudQ integration for DeepSeek Harness with secure credential, workspace, and plugin-management surfaces",
5
5
  "license": "MIT",
6
6
  "type": "module",