@hcmai/sdk 0.3.4 → 0.3.6

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/dist/index.js CHANGED
@@ -516,6 +516,9 @@ var CliErrorCode = /* @__PURE__ */ ((CliErrorCode2) => {
516
516
  CliErrorCode2["IDENTITY_NOT_IN_ENV"] = "IDENTITY_NOT_IN_ENV";
517
517
  CliErrorCode2["TOKEN_EXPIRED"] = "TOKEN_EXPIRED";
518
518
  CliErrorCode2["NO_ACTIVE_ENV"] = "NO_ACTIVE_ENV";
519
+ CliErrorCode2["SKILL_ID_CONFLICT"] = "SKILL_ID_CONFLICT";
520
+ CliErrorCode2["SKILL_SOURCE_UNREACHABLE"] = "SKILL_SOURCE_UNREACHABLE";
521
+ CliErrorCode2["SYNC_SOURCE_UNSUPPORTED"] = "SYNC_SOURCE_UNSUPPORTED";
519
522
  return CliErrorCode2;
520
523
  })(CliErrorCode || {});
521
524
  var CODE_TO_EXIT = {
@@ -546,10 +549,14 @@ var CODE_TO_EXIT = {
546
549
  ["IDENTITY_ALREADY_EXISTS" /* IDENTITY_ALREADY_EXISTS */]: 5 /* CONFIG_ERROR */,
547
550
  ["IDENTITY_NOT_IN_ENV" /* IDENTITY_NOT_IN_ENV */]: 5 /* CONFIG_ERROR */,
548
551
  ["TOKEN_EXPIRED" /* TOKEN_EXPIRED */]: 2 /* AUTH_ERROR */,
549
- ["NO_ACTIVE_ENV" /* NO_ACTIVE_ENV */]: 5 /* CONFIG_ERROR */
552
+ ["NO_ACTIVE_ENV" /* NO_ACTIVE_ENV */]: 5 /* CONFIG_ERROR */,
553
+ ["SKILL_ID_CONFLICT" /* SKILL_ID_CONFLICT */]: 3 /* BUSINESS_ERROR */,
554
+ // 🔴 装坏了不是网络问题:报 network 会让顾问先去查 VPN。
555
+ ["SKILL_SOURCE_UNREACHABLE" /* SKILL_SOURCE_UNREACHABLE */]: 5 /* CONFIG_ERROR */,
556
+ ["SYNC_SOURCE_UNSUPPORTED" /* SYNC_SOURCE_UNSUPPORTED */]: 1 /* USAGE_ERROR */
550
557
  };
551
558
  function exitCodeFor(code) {
552
- return CODE_TO_EXIT[code];
559
+ return CODE_TO_EXIT[code] ?? 1 /* USAGE_ERROR */;
553
560
  }
554
561
 
555
562
  // src/errors/cli-error.ts
@@ -581,6 +588,10 @@ var CliError = class extends Error {
581
588
  if (this.traceId) parts.push(`[traceId: ${this.traceId}]`);
582
589
  if (this.profile) parts.push(`[profile: ${this.profile}]`);
583
590
  let out = parts.join(" ");
591
+ const details = this.context?.details;
592
+ const detailText = formatDetails(details);
593
+ if (detailText) out += `
594
+ \u8BE6\u60C5: ${detailText}`;
584
595
  const nextStep = this.context?.nextStep;
585
596
  if (typeof nextStep === "string" && nextStep) out += `
586
597
  \u4E0B\u4E00\u6B65: ${nextStep}`;
@@ -633,6 +644,19 @@ function fromAxiosError(err, profile) {
633
644
  cause: err
634
645
  });
635
646
  }
647
+ function formatDetails(details) {
648
+ if (details === void 0 || details === null) return void 0;
649
+ if (typeof details === "string") return details.trim() || void 0;
650
+ if (typeof details === "number" || typeof details === "boolean") return String(details);
651
+ let text;
652
+ try {
653
+ text = JSON.stringify(details);
654
+ } catch {
655
+ return void 0;
656
+ }
657
+ if (!text || text === "{}" || text === "[]") return void 0;
658
+ return text.length > 800 ? `${text.slice(0, 800)}\u2026\uFF08\u5DF2\u622A\u65AD\uFF09` : text;
659
+ }
636
660
 
637
661
  // src/config/env.ts
638
662
  async function saveEnv(name, cfg) {
@@ -1044,18 +1068,18 @@ async function action(http, model, actionName, input) {
1044
1068
  if (scope === "object" && !input.id) {
1045
1069
  throw new Error(`object action ${model}.${actionName} requires id`);
1046
1070
  }
1047
- const path6 = scope === "object" ? `/api/models/${model}/${input.id}/action/${actionName}` : `/api/models/${model}/action/${actionName}`;
1071
+ const path7 = scope === "object" ? `/api/models/${model}/${input.id}/action/${actionName}` : `/api/models/${model}/action/${actionName}`;
1048
1072
  const resp = await (async () => {
1049
1073
  if (method === "GET") {
1050
- return http.get(path6, { params: queryParams(input) });
1074
+ return http.get(path7, { params: queryParams(input) });
1051
1075
  }
1052
1076
  if (method === "DELETE") {
1053
- return http.delete(path6, { params: queryParams(input) });
1077
+ return http.delete(path7, { params: queryParams(input) });
1054
1078
  }
1055
1079
  if (method === "PUT") {
1056
- return http.put(path6, scope === "object" ? objectBody(input) : classBody(input));
1080
+ return http.put(path7, scope === "object" ? objectBody(input) : classBody(input));
1057
1081
  }
1058
- return http.post(path6, scope === "object" ? objectBody(input) : classBody(input));
1082
+ return http.post(path7, scope === "object" ? objectBody(input) : classBody(input));
1059
1083
  })();
1060
1084
  return resp.data;
1061
1085
  }
@@ -1788,7 +1812,7 @@ var WsClient = class {
1788
1812
  });
1789
1813
  }
1790
1814
  this.ws = new WebSocket(this.opts.endpoint);
1791
- return new Promise((resolve3, reject) => {
1815
+ return new Promise((resolve4, reject) => {
1792
1816
  const settle = (fn) => {
1793
1817
  off();
1794
1818
  fn();
@@ -1827,7 +1851,7 @@ var WsClient = class {
1827
1851
  this.intentionalClose = false;
1828
1852
  this.sessionId = msg.sessionId;
1829
1853
  this.startHeartbeat();
1830
- settle(resolve3);
1854
+ settle(resolve4);
1831
1855
  this.ws.on("message", this.persistentOnMessage);
1832
1856
  this.ws.on("close", this.persistentOnClose);
1833
1857
  this.ws.on("error", this.persistentOnClose);
@@ -2097,13 +2121,13 @@ async function sendMessageAndStream(ws, prompt, opts = {}) {
2097
2121
  ws.send(cmd);
2098
2122
  const events = {
2099
2123
  [Symbol.asyncIterator]: () => ({
2100
- next: () => new Promise((resolve3) => {
2124
+ next: () => new Promise((resolve4) => {
2101
2125
  if (queue.length) {
2102
- resolve3({ value: queue.shift(), done: false });
2126
+ resolve4({ value: queue.shift(), done: false });
2103
2127
  } else if (done) {
2104
- resolve3({ value: void 0, done: true });
2128
+ resolve4({ value: void 0, done: true });
2105
2129
  } else {
2106
- waiters.push(resolve3);
2130
+ waiters.push(resolve4);
2107
2131
  }
2108
2132
  })
2109
2133
  })
@@ -5011,18 +5035,18 @@ async function listTenantMeta(http, opts = {}) {
5011
5035
  const items = payload?.items ?? payload?.list ?? payload?.records ?? [];
5012
5036
  return { items, total: payload?.total, raw: payload };
5013
5037
  }
5014
- async function getTenantMeta(http, path6) {
5015
- const resp = await http.get("/api/tenant-meta/meta", { params: { path: path6 } });
5038
+ async function getTenantMeta(http, path7) {
5039
+ const resp = await http.get("/api/tenant-meta/meta", { params: { path: path7 } });
5016
5040
  const payload = resp.data?.data ?? resp.data;
5017
5041
  if (typeof payload === "string") return payload;
5018
5042
  return String(payload?.content ?? "");
5019
5043
  }
5020
- async function saveTenantMeta(http, path6, content) {
5021
- const resp = await http.put("/api/tenant-meta/meta", { content }, { params: { path: path6 } });
5044
+ async function saveTenantMeta(http, path7, content) {
5045
+ const resp = await http.put("/api/tenant-meta/meta", { content }, { params: { path: path7 } });
5022
5046
  return resp.data?.data ?? resp.data;
5023
5047
  }
5024
- async function deleteTenantMeta(http, path6) {
5025
- const resp = await http.delete("/api/tenant-meta/meta", { params: { path: path6 } });
5048
+ async function deleteTenantMeta(http, path7) {
5049
+ const resp = await http.delete("/api/tenant-meta/meta", { params: { path: path7 } });
5026
5050
  return resp.data?.data ?? resp.data;
5027
5051
  }
5028
5052
 
@@ -5124,14 +5148,14 @@ function resolveSkillInstallOrder(manifest2, targetId) {
5124
5148
  throw invalid(`\u6280\u80FD\u76EE\u5F55\u4E2D\u4E0D\u5B58\u5728 ${targetId}`);
5125
5149
  }
5126
5150
  const state = /* @__PURE__ */ new Map();
5127
- const path6 = [];
5151
+ const path7 = [];
5128
5152
  const order = [];
5129
5153
  const visit = (id, requiredBy) => {
5130
5154
  const current = state.get(id);
5131
5155
  if (current === "done") return;
5132
5156
  if (current === "visiting") {
5133
- const cycleStart = path6.indexOf(id);
5134
- const cycle = [...path6.slice(Math.max(0, cycleStart)), id];
5157
+ const cycleStart = path7.indexOf(id);
5158
+ const cycle = [...path7.slice(Math.max(0, cycleStart)), id];
5135
5159
  throw invalid(`\u6280\u80FD\u4F9D\u8D56\u5B58\u5728\u73AF: ${cycle.join(" -> ")}`);
5136
5160
  }
5137
5161
  const skill = byId.get(id);
@@ -5140,11 +5164,11 @@ function resolveSkillInstallOrder(manifest2, targetId) {
5140
5164
  }
5141
5165
  assertSafeSkillId(skill.id);
5142
5166
  state.set(id, "visiting");
5143
- path6.push(id);
5167
+ path7.push(id);
5144
5168
  for (const required of normalizeRequirements(skill.requires, skill.id)) {
5145
5169
  visit(required, skill.id);
5146
5170
  }
5147
- path6.pop();
5171
+ path7.pop();
5148
5172
  state.set(id, "done");
5149
5173
  order.push(skill);
5150
5174
  };
@@ -5235,30 +5259,517 @@ async function resetSettingItem(http, domain, namespace, settingKey, revision) {
5235
5259
  function parseSettingAssignment(raw) {
5236
5260
  const eq = raw.indexOf("=");
5237
5261
  if (eq <= 0) throw new Error(`--set \u9700\u8981 namespace.key=value \u5F62\u6001\uFF0C\u6536\u5230 "${raw}"`);
5238
- const path6 = raw.slice(0, eq).trim();
5262
+ const path7 = raw.slice(0, eq).trim();
5239
5263
  const value = raw.slice(eq + 1);
5240
- const dot = path6.lastIndexOf(".");
5241
- if (dot <= 0) throw new Error(`--set \u7684\u952E\u9700\u542B namespace\uFF0C\u5982 security.password.passwordPolicy=complex\uFF0C\u6536\u5230 "${path6}"`);
5242
- return { namespace: path6.slice(0, dot), settingKey: path6.slice(dot + 1), value };
5264
+ const dot = path7.lastIndexOf(".");
5265
+ if (dot <= 0) throw new Error(`--set \u7684\u952E\u9700\u542B namespace\uFF0C\u5982 security.password.passwordPolicy=complex\uFF0C\u6536\u5230 "${path7}"`);
5266
+ return { namespace: path7.slice(0, dot), settingKey: path7.slice(dot + 1), value };
5267
+ }
5268
+
5269
+ // src/fde/layout.ts
5270
+ import { createHash as createHash2 } from "crypto";
5271
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5272
+ import * as path6 from "path";
5273
+ var UNSAFE = /[^A-Za-z0-9._-]/g;
5274
+ function slug(value) {
5275
+ return (value || "unknown").replace(UNSAFE, "_");
5276
+ }
5277
+ function safeSegment(value) {
5278
+ const raw = value || "unknown";
5279
+ const digest = createHash2("sha256").update(raw, "utf8").digest("hex").slice(0, 10);
5280
+ return `${slug(raw)}-${digest}`;
5281
+ }
5282
+ function tenantSegment(value) {
5283
+ return safeSegment(value);
5284
+ }
5285
+ function isInside(p, container) {
5286
+ const rel = path6.relative(container, p);
5287
+ if (rel === "") return true;
5288
+ if (path6.isAbsolute(rel)) return false;
5289
+ return !rel.split(path6.sep).includes("..");
5290
+ }
5291
+ function hasDotDotComponent(rel) {
5292
+ return rel.split(/[/\\]/).includes("..");
5293
+ }
5294
+ var ReconLayout = class {
5295
+ root;
5296
+ tenantId;
5297
+ sourceKey;
5298
+ dir;
5299
+ constructor(root, tenantId, sourceKey) {
5300
+ this.root = root;
5301
+ this.tenantId = String(tenantId);
5302
+ this.sourceKey = sourceKey;
5303
+ this.dir = path6.join(
5304
+ this.root,
5305
+ "recon",
5306
+ tenantSegment(this.tenantId),
5307
+ safeSegment(sourceKey)
5308
+ );
5309
+ const container = path6.resolve(this.root, "recon");
5310
+ if (!isInside(path6.resolve(this.dir), container)) {
5311
+ throw new LayoutContainmentError(
5312
+ `\u79DF\u6237\u6BB5\u8D8A\u754C\uFF0C\u62D2\u7EDD\u6784\u9020\u4EA7\u7269\u76EE\u5F55\uFF1AtenantId=${JSON.stringify(tenantId)}\uFF08\u4EA7\u7269\u6839 ${container}\uFF09`
5313
+ );
5314
+ }
5315
+ }
5316
+ /**
5317
+ * 把相对路径解析成绝对路径,并过两道**互相印证**的遏制闸。
5318
+ *
5319
+ * 🔴 `rel` 里会嵌入**现爬的 modelKey**:`meta/<k>.model.json`、`valuedomain/<k>.json`、
5320
+ * `mapping/<k>.json`。服务端说什么,这里就拼什么。
5321
+ *
5322
+ * 实测分档(沿用 Python 侧 2026-08-16 的实跑结论):
5323
+ * `../evil` → 规范化后**仍在产物根内**,写成 `<产物根>/evil.json`。第二道闸**拦不住**。
5324
+ * `../models` → 同上,**顶掉 recon 自己的模型名册**,写入方拿到「成功」,产物里没有留痕。
5325
+ * `../../evil` → 真的逃出产物根 ⇒ 第二道闸拦得住。
5326
+ * `/etc/passwd` → 前导 `/` 被路径规范化吃掉,落在 `valuedomain/etc/passwd.json`。
5327
+ * ⇒ 真正的危害不是「逃出产物根」(那一档本来就拦得住),是「**逃出预期子目录、
5328
+ * 静默顶掉同级产物**」——`models.json` / `fingerprint.json` / `gates.json` 全在射程内。
5329
+ *
5330
+ * ⚠️ 拒绝而不是消毒:消毒会让 `meta/<modelKey>.json` 不再能按 modelKey grep 回去。
5331
+ */
5332
+ resolvePath(rel, createParents = true) {
5333
+ const p = path6.join(this.dir, rel);
5334
+ if (hasDotDotComponent(rel)) {
5335
+ throw new LayoutContainmentError(
5336
+ `\u4EA7\u7269\u76F8\u5BF9\u8DEF\u5F84\u542B \`..\` \u5206\u91CF\uFF0C\u62D2\u7EDD\u5199\u5165\uFF1A${JSON.stringify(rel)}\uFF08\u4F1A\u8D8A\u51FA\u9884\u671F\u5B50\u76EE\u5F55\u3001\u9759\u9ED8\u9876\u6389\u540C\u7EA7\u4EA7\u7269\uFF1B\u4EA7\u7269\u6839 ${this.dir}\uFF09`
5337
+ );
5338
+ }
5339
+ if (!isInside(path6.resolve(p), path6.resolve(this.dir))) {
5340
+ throw new LayoutContainmentError(
5341
+ `\u4EA7\u7269\u76F8\u5BF9\u8DEF\u5F84\u8D8A\u754C\uFF0C\u62D2\u7EDD\u5199\u5165\uFF1A${JSON.stringify(rel)}\uFF08\u4EA7\u7269\u6839 ${this.dir}\uFF09`
5342
+ );
5343
+ }
5344
+ if (createParents) {
5345
+ mkdirSync(path6.dirname(p), { recursive: true });
5346
+ }
5347
+ return p;
5348
+ }
5349
+ writeJson(rel, obj) {
5350
+ const p = this.resolvePath(rel);
5351
+ writeFileSync(p, JSON.stringify(obj, null, 2), "utf8");
5352
+ return p;
5353
+ }
5354
+ writeJsonl(rel, rows) {
5355
+ const p = this.resolvePath(rel);
5356
+ let body = "";
5357
+ for (const row of rows) body += `${JSON.stringify(row)}
5358
+ `;
5359
+ writeFileSync(p, body, "utf8");
5360
+ return p;
5361
+ }
5362
+ writeText(rel, text) {
5363
+ const p = this.resolvePath(rel);
5364
+ writeFileSync(p, text, "utf8");
5365
+ return p;
5366
+ }
5367
+ /**
5368
+ * 逐行读一份 JSONL 产物。
5369
+ *
5370
+ * 🔴 与 `readJson` 同走那两道越界闸——同一个收口点不能一半有闸一半没有。
5371
+ * 🔴 **跳过空行**(末尾换行是 `writeJsonl` 必然产生的),但**不跳过坏行**:
5372
+ * 坏行原样抛,因为「这一行读不出来」和「这个产物没有这一行」是两件事。
5373
+ */
5374
+ readJsonlRows(rel) {
5375
+ const body = readFileSync2(this.resolvePath(rel, false), "utf8");
5376
+ const rows = [];
5377
+ for (const line of body.split("\n")) {
5378
+ const trimmed = line.trim();
5379
+ if (trimmed) rows.push(JSON.parse(trimmed));
5380
+ }
5381
+ return rows;
5382
+ }
5383
+ readJson(rel) {
5384
+ return JSON.parse(readFileSync2(this.resolvePath(rel, false), "utf8"));
5385
+ }
5386
+ };
5387
+ var LayoutContainmentError = class extends Error {
5388
+ constructor(message) {
5389
+ super(message);
5390
+ this.name = "LayoutContainmentError";
5391
+ }
5392
+ };
5393
+
5394
+ // src/fde/fingerprint.ts
5395
+ import { createHash as createHash3 } from "crypto";
5396
+ var GATE_COMPONENTS = [
5397
+ "tenantMetaManifestDigest",
5398
+ "extensionSource",
5399
+ "modelListHash"
5400
+ ];
5401
+ var BLIND_SPOTS = "\u4EA7\u54C1\u5347\u7EA7\uFF1Amanifest \u53EA\u8BFB\u79DF\u6237 workspace \u7684 metas/ \u76EE\u5F55\uFF0C\u4EA7\u54C1\u5347\u7EA7\u6539\u7684\u662F\u5B89\u88C5\u5305\u5185 _metas/\uFF0C\u56DB\u5143\u7EC4\u4E0D\u4F1A\u53D8\u2014\u2014\u8BE5\u573A\u666F\u7684\u9632\u7EBF\u53EA\u6709\u884C\u4E3A\u63A2\u9488\u4E00\u6761\u3002 | \u540C\u5B57\u8282\u6570\u5185\u5BB9\u66FF\u6362\uFF1A\u6E05\u5355\u53EA\u7ED9 name/size/lastModified\uFF0C\u628A\u6807\u7B7E\u300C\u90E8\u95E8\u300D\u6539\u6210\u300C\u673A\u6784\u300D\u5728 UTF-8 \u4E0B\u6070\u597D\u90FD\u662F 6 \u5B57\u8282\uFF0C\u6458\u8981\u4E0D\u53D8\u3002\u21D2 \u5B57\u6BB5\u540D\u53EB ManifestDigest \u800C\u975E Fingerprint\uFF0C\u540D\u5B57\u5FC5\u987B\u8BF4\u5B9E\u8BDD\u3002";
5402
+ var MODELS_PATH = "/api/system/models";
5403
+ var MANIFEST_PATH = "/api/tenant-meta/list";
5404
+ var VERSION_PATH = "/api/version/backend";
5405
+ var META_PATH = "/api/models/{model}/meta";
5406
+ var EXTENSION_SOURCE_HEADER = "x-hcm-extension-source";
5407
+ var MAX_SOURCE_PROBES = 50;
5408
+ var MAX_MANIFEST_PAGES = 200;
5409
+ var GATE_SOURCES = [MODELS_PATH, MANIFEST_PATH, META_PATH];
5410
+ function modelEntries(body) {
5411
+ if (!isPlainObject2(body)) return [];
5412
+ const raw = body.models;
5413
+ if (!Array.isArray(raw)) return [];
5414
+ return raw.filter(isPlainObject2);
5415
+ }
5416
+ function modelKeys(body) {
5417
+ return modelEntries(body).map((m) => typeof m.modelKey === "string" ? m.modelKey : "").filter((k) => k !== "").sort();
5418
+ }
5419
+ async function probeExtensionSource(probe, keys) {
5420
+ let attempted = 0;
5421
+ let answered = 0;
5422
+ for (const key of [...keys].sort().slice(0, MAX_SOURCE_PROBES)) {
5423
+ attempted += 1;
5424
+ const out = await probe.get(META_PATH.replace("{model}", key));
5425
+ if (out.error !== "ok") continue;
5426
+ answered += 1;
5427
+ const value = out.headers[EXTENSION_SOURCE_HEADER];
5428
+ if (value) return { sourceKey: value, probeModelKey: key, attempted, answered };
5429
+ }
5430
+ return { sourceKey: "", probeModelKey: "", attempted, answered };
5431
+ }
5432
+ async function fetchManifestRows(probe, failures) {
5433
+ const rows = [];
5434
+ let page = 0;
5435
+ const pageSize = 200;
5436
+ for (; ; ) {
5437
+ const out = await probe.get(MANIFEST_PATH, { page, pageSize });
5438
+ if (out.error !== "ok" || !isPlainObject2(out.body)) {
5439
+ failures.push(MANIFEST_PATH);
5440
+ break;
5441
+ }
5442
+ const body = out.body;
5443
+ const chunk = body.data ?? [];
5444
+ if (!Array.isArray(chunk)) {
5445
+ failures.push(MANIFEST_PATH);
5446
+ break;
5447
+ }
5448
+ rows.push(...chunk.filter(isPlainObject2));
5449
+ const totalPages = typeof body.totalPages === "number" ? body.totalPages : 0;
5450
+ page += 1;
5451
+ if (page >= totalPages || chunk.length === 0) break;
5452
+ if (page >= MAX_MANIFEST_PAGES) {
5453
+ failures.push(`${MANIFEST_PATH}#page-cap`);
5454
+ break;
5455
+ }
5456
+ }
5457
+ return rows;
5458
+ }
5459
+ function manifestDigest(rows) {
5460
+ const lines = rows.map((r) => `${r.name}|${r.size}|${r.lastModified}`).sort();
5461
+ return sha2562(lines.join("\n"));
5462
+ }
5463
+ async function computeFingerprint(probe, opts = {}) {
5464
+ const ttlDays = opts.ttlDays ?? 30;
5465
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
5466
+ const failures = [];
5467
+ const modelsOut = await probe.get(MODELS_PATH);
5468
+ const modelsOk = modelsOut.error === "ok" && isPlainObject2(modelsOut.body);
5469
+ if (!modelsOk) failures.push(MODELS_PATH);
5470
+ const models = modelsOk ? modelEntries(modelsOut.body) : [];
5471
+ const keys = modelsOk ? modelKeys(modelsOut.body) : [];
5472
+ if (modelsOut.error === "ok" && keys.length === 0) {
5473
+ failures.push(`${MODELS_PATH}#empty-model-list`);
5474
+ }
5475
+ const p = await probeExtensionSource(probe, keys);
5476
+ let sourceKey = p.sourceKey;
5477
+ if (!sourceKey) {
5478
+ failures.push(
5479
+ p.answered > 0 ? `${META_PATH}#X-HCM-Extension-Source` : `${META_PATH}#no-meta-answered`
5480
+ );
5481
+ sourceKey = "unknown";
5482
+ }
5483
+ const hashLines = models.map((m) => `${m.modelKey ?? ""}|${m.type ?? ""}`).sort();
5484
+ const modelHash = sha2562(hashLines.join("\n"));
5485
+ const rows = await fetchManifestRows(probe, failures);
5486
+ const versionOut = await probe.get(VERSION_PATH);
5487
+ let backendVersion;
5488
+ if (versionOut.error === "ok" && versionOut.body !== null && versionOut.body !== void 0) {
5489
+ backendVersion = versionOut.body;
5490
+ } else {
5491
+ failures.push(VERSION_PATH);
5492
+ backendVersion = { version: "unavailable" };
5493
+ }
5494
+ return {
5495
+ tenantMetaManifestDigest: manifestDigest(rows),
5496
+ manifestEntryCount: rows.length,
5497
+ extensionSource: sourceKey,
5498
+ // 🔴 记下**是从哪个模型探到的**:让「这个值是谁给的」在产物里留痕。
5499
+ extensionSourceProbe: p.probeModelKey,
5500
+ // 🔴 没有这两个数,`extensionSource: "unknown"` 就是一个**不可追问的结论**。
5501
+ extensionSourceProbeAttempted: p.attempted,
5502
+ extensionSourceProbeAnswered: p.answered,
5503
+ modelListHash: modelHash,
5504
+ modelCount: models.length,
5505
+ backendVersion,
5506
+ capabilities: probe.capabilities,
5507
+ createdAt: `${now().toISOString().slice(0, 19)}Z`,
5508
+ ttlDays,
5509
+ coverageNotes: {
5510
+ gateComponents: [...GATE_COMPONENTS],
5511
+ blindSpots: BLIND_SPOTS,
5512
+ // 空数组 = 三个端点都真取到了;非空 = 这份四元组是**残的**,
5513
+ // 上面那些 0 和空摘要不代表环境真的空。
5514
+ fetchFailures: failures
5515
+ }
5516
+ };
5517
+ }
5518
+ function compareFingerprint(recorded, current) {
5519
+ const diffs = GATE_COMPONENTS.filter((c) => recorded[c] !== current[c]);
5520
+ if (recorded.capabilities !== current.capabilities) diffs.push("capabilities");
5521
+ for (const [fp, tag] of [
5522
+ [recorded, "recorded"],
5523
+ [current, "current"]
5524
+ ]) {
5525
+ const notes = fp.coverageNotes;
5526
+ if (!isPlainObject2(notes) || !("fetchFailures" in notes)) {
5527
+ diffs.push(`incomplete:${tag}:coverageNotes-missing`);
5528
+ continue;
5529
+ }
5530
+ const raw = notes.fetchFailures;
5531
+ const list = Array.isArray(raw) ? raw : [];
5532
+ const gateFailures = [...new Set(list.map(String))].filter((x) => GATE_SOURCES.some((s) => x.startsWith(s))).sort();
5533
+ for (const f of gateFailures) diffs.push(`incomplete:${tag}:${f}`);
5534
+ }
5535
+ return diffs;
5536
+ }
5537
+ function sha2562(s) {
5538
+ return createHash3("sha256").update(s, "utf8").digest("hex");
5539
+ }
5540
+ function isPlainObject2(v) {
5541
+ return typeof v === "object" && v !== null && !Array.isArray(v);
5542
+ }
5543
+
5544
+ // src/fde/doctor.ts
5545
+ var DOCTOR_ANCHORS = [
5546
+ "connectivity",
5547
+ "identity",
5548
+ "config-admin",
5549
+ "layer"
5550
+ ];
5551
+ async function runDoctor(probe, identity) {
5552
+ const checks = [];
5553
+ const modelsOut = await probe.get(MODELS_PATH);
5554
+ const connected = modelsOut.error === "ok";
5555
+ const modelsBody = isObj(modelsOut.body) ? modelsOut.body : {};
5556
+ checks.push({
5557
+ anchor: "connectivity",
5558
+ ok: connected,
5559
+ detail: connected ? `\u6A21\u578B\u6E05\u5355\u53EF\u8FBE\uFF0C\u5171 ${modelsBody.totalCount ?? 0} \u4E2A\u6A21\u578B` : `\u4E0D\u53EF\u8FBE\uFF1A${modelsOut.error}\uFF08status=${modelsOut.status ?? "?"}\uFF09`
5560
+ });
5561
+ const keys = modelKeys(modelsOut.body);
5562
+ const p = await probeExtensionSource(probe, keys);
5563
+ checks.push({ anchor: "identity", ...judgeIdentity(identity) });
5564
+ const manifestOut = await probe.get(MANIFEST_PATH, { page: 0, pageSize: 1 });
5565
+ const caOk = manifestOut.error === "ok";
5566
+ const manifestBody = isObj(manifestOut.body) ? manifestOut.body : {};
5567
+ const total = caOk ? manifestBody.total ?? "?" : "?";
5568
+ let caDetail;
5569
+ if (caOk) {
5570
+ caDetail = `\u79DF\u6237 meta \u6E05\u5355\u53EF\u8BFB\uFF0C\u5171 ${total} \u6761\uFF080 \u6761\u662F\u5408\u6CD5\u72B6\u6001\uFF1A\u514B\u9686\u5E93\u5E26\u5F97\u8D70 workspace \u7ED1\u5B9A\uFF0C\u5E26\u4E0D\u8D70 workspace \u6587\u4EF6\uFF09`;
5571
+ } else if (manifestOut.status === 401 || manifestOut.status === 403) {
5572
+ caDetail = `HTTP ${manifestOut.status}\uFF1A\u5F53\u524D\u8D26\u53F7**\u6CA1\u6709 configAdmin \u6743\u9650**\u3002\u627E\u5BA2\u6237\u7BA1\u7406\u5458\u52A0\u89D2\u8272\uFF1B\u8FD9\u4E0D\u662F\u7F51\u7EDC\u95EE\u9898`;
5573
+ } else {
5574
+ caDetail = `\u4E0D\u53EF\u8BFB\uFF1A${manifestOut.error}\uFF08status=${manifestOut.status ?? "?"}\uFF09\u3002\u5148\u6392\u8FDE\u901A/\u7F51\u5173\uFF0C\u518D\u8C08\u6743\u9650\u3002\u7F3A configAdmin \u65F6\u4E3B\u951A\u70B9\u7B97\u4E0D\u51FA\u6765`;
5575
+ }
5576
+ checks.push({ anchor: "config-admin", ok: caOk, detail: caDetail });
5577
+ checks.push({
5578
+ anchor: "layer",
5579
+ ok: Boolean(p.sourceKey),
5580
+ detail: layerDetail(p, connected)
5581
+ });
5582
+ return { ok: checks.every((c) => c.ok), checks };
5583
+ }
5584
+ function judgeIdentity(ident) {
5585
+ if (ident === null) {
5586
+ return { ok: false, detail: "\u672A\u7ECF\u672C\u5DE5\u5177\u767B\u5F55\uFF0C\u65E0\u6CD5\u6838\u5BF9 token \u5F52\u5C5E\u7684\u79DF\u6237" };
5587
+ }
5588
+ const want = String(ident.requestedTenantId ?? "").trim();
5589
+ const got = String(ident.tenantId ?? "").trim();
5590
+ if (!want) {
5591
+ return {
5592
+ ok: false,
5593
+ detail: `\u672C\u5DE5\u5177\u6CA1\u6709\u8BB0\u4E0B\u300C\u4F60\u8981\u7684\u79DF\u6237\u300D\uFF08\u547D\u4EE4\u5C42\u672A\u586B requestedTenantId\uFF09\uFF0C\u65E0\u6CD5\u4E0E token \u5F52\u5C5E\u7684\u79DF\u6237 ${got || "(\u672A\u77E5)"} \u6838\u5BF9 \u2014\u2014 \u8FD9\u662F\u5DE5\u5177\u63A5\u7EBF\u95EE\u9898\uFF0C\u4E0D\u662F\u73AF\u5883\u95EE\u9898`
5594
+ };
5595
+ }
5596
+ if (!got) {
5597
+ return {
5598
+ ok: false,
5599
+ detail: `\u670D\u52A1\u7AEF\u767B\u5F55\u54CD\u5E94\u91CC\u6CA1\u6709 user.tenantId\uFF0C\u65E0\u6CD5\u6838\u5BF9\uFF08\u4F60\u8981\u7684\u662F ${want}\uFF09`
5600
+ };
5601
+ }
5602
+ if (want !== got) {
5603
+ return {
5604
+ ok: false,
5605
+ detail: `\u{1F534} **token \u5F52\u5C5E\u79DF\u6237 ${got}\uFF0C\u800C\u4F60\u8981\u7684\u662F ${want}** \u2014\u2014 \u4F60\u6B63\u5728\u722C\u53E6\u4E00\u4E2A\u79DF\u6237\uFF0C\u5148\u505C\u4E0B\u6765\u3002\u8FD9\u4E00\u5BF9\u8EAB\u4EFD\u4F1A\u968F\u8EAB\u4EFD\u6BB5\u843D\u8FDB\u4EA7\u7269\u7D22\u5F15\uFF0C\u6240\u4EE5\u722C\u5B8C\u4E4B\u540E\u5728\u4EA7\u7269\u91CC\u4E5F\u5BF9\u5F97\u51FA\u6765\uFF1B\u4F46**\u843D\u76D8\u8DEF\u5F84**\u4E0A\u90A3\u4E2A\u79DF\u6237\u6BB5\u53D6\u81EA\u4F60\u7ED9\u7684 \`--tenant\` \u2014\u2014 \u76EE\u5F55\u540D\u7167\u65E7\u662F\u4F60\u8981\u7684\u90A3\u4E2A\u53F7\u3002`
5606
+ };
5607
+ }
5608
+ return {
5609
+ ok: true,
5610
+ detail: `\u79DF\u6237 ${got} \u6838\u5BF9\u4E00\u81F4\uFF1B\u767B\u5F55\u8EAB\u4EFD ${ident.username || "(\u672A\u77E5)"}\uFF0C\u89D2\u8272 ${JSON.stringify(ident.roles ?? [])}`
5611
+ };
5612
+ }
5613
+ function layerDetail(p, connected) {
5614
+ if (p.sourceKey) {
5615
+ return `\u4F60\u7AD9\u5728 \`${p.sourceKey}\` \u8FD9\u4E00\u5C42\uFF08\u63A2\u81EA \`${p.probeModelKey}\` \u7684 meta\uFF09\u3002\u4E0D\u540C sourceKey \u7684 recon \u4EA7\u7269\u4E0D\u53EF\u4E92\u6362 \u2014\u2014 \u4E24\u4E2A FDE \u540C\u4E00\u6761\u547D\u4EE4\u4F1A\u5F97\u5230\u4E24\u4EFD\u4E0D\u540C\u7684\u4E8B\u5B9E\uFF0C\u90A3\u4E0D\u662F\u9648\u65E7\uFF0C\u662F\u7AD9\u5728\u4E0D\u540C\u7684\u5C42\u4E0A`;
5616
+ }
5617
+ if (p.attempted === 0) {
5618
+ return connected ? "\u6A21\u578B\u6E05\u5355\u53D6\u5230\u4E86\uFF0C\u4F46\u91CC\u9762**\u4E00\u4E2A\u53EF\u7528 modelKey \u90FD\u6CA1\u6709**\uFF0C**\u4E00\u4E2A\u6A21\u578B\u90FD\u6CA1\u5F97\u8BD5** \u21D2 \u8FD9\u4E0D\u662F\u5C42\u7684\u95EE\u9898\uFF1A\u67E5\u6E05\u5355\u7684 modelKey \u952E\u540D\u662F\u4E0D\u662F\u88AB\u7F51\u5173\u6539\u5199\u3001\u6216\u5F53\u524D\u8D26\u53F7\u770B\u4E0D\u5230\u4EFB\u4F55\u6A21\u578B" : "\u6A21\u578B\u6E05\u5355\u6CA1\u53D6\u5230\uFF0C**\u4E00\u4E2A\u6A21\u578B\u90FD\u6CA1\u5F97\u8BD5** \u21D2 \u5148\u770B\u4E0A\u9762 connectivity \u90A3\u6761";
5619
+ }
5620
+ if (p.answered > 0) {
5621
+ return `\u8BD5\u4E86 ${p.attempted} \u4E2A\u6A21\u578B\u7684 meta\uFF0C${p.answered} \u4E2A\u7B54\u5E94\u4E86\uFF0C\u4F46\u90FD\u6CA1\u56DE\u663E X-HCM-Extension-Source \u21D2 \u5148\u67E5\u53CD\u4EE3\u662F\u4E0D\u662F\u628A X-HCM-* \u54CD\u5E94\u5934\u5265\u4E86`;
5622
+ }
5623
+ return `\u8BD5\u4E86 ${p.attempted} \u4E2A\u6A21\u578B\u7684 meta\uFF0C**\u4E00\u4E2A\u90FD\u6CA1\u7B54\u5E94** \u21D2 \u5148\u67E5\u8FDE\u901A\u4E0E\u6743\u9650\uFF0C\u518D\u770B\u6A21\u578B\u6E05\u5355\u662F\u4E0D\u662F\u7A7A\u7684\u3002\u26A0\uFE0F \u672C\u5DE5\u5177\u53EA\u77E5\u9053\u300C\u4E00\u4E2A\u90FD\u6CA1\u7B54\u5E94\u300D\uFF0C**\u5206\u4E0D\u51FA**\u662F\u8FDE\u901A\u3001\u6743\u9650\u8FD8\u662F\u4E2D\u95F4\u6709\u4EBA\u6321\u4E86 \u2014\u2014 \u4E0A\u4E00\u6863\uFF08\u7B54\u5E94\u4E86\u4F46\u6CA1\u56DE\u663E X-HCM-* \u5934\uFF09\u624D\u6307\u5F97\u52A8\u53CD\u4EE3\uFF0C\u8FD9\u4E00\u6863\u6307\u4E0D\u52A8\u4EFB\u4F55\u4E00\u65B9`;
5624
+ }
5625
+ function isObj(v) {
5626
+ return typeof v === "object" && v !== null && !Array.isArray(v);
5627
+ }
5628
+ var AUTH_INFO_PATH = "/api/auth/info";
5629
+ async function fetchReconIdentity(probe, requestedTenantId) {
5630
+ const out = await probe.get(AUTH_INFO_PATH);
5631
+ if (out.error !== "ok") return null;
5632
+ const principal = toPrincipal(out.body);
5633
+ if (!principal) return { requestedTenantId };
5634
+ return {
5635
+ requestedTenantId,
5636
+ tenantId: principal.tenantId,
5637
+ username: principal.username,
5638
+ roles: principal.roles
5639
+ };
5640
+ }
5641
+
5642
+ // src/fde/probe.ts
5643
+ var STATUS_CLASS = {
5644
+ 401: "unauthorized",
5645
+ 403: "forbidden",
5646
+ 404: "not-found"
5647
+ };
5648
+ function classifyStatus(status) {
5649
+ if (status >= 200 && status < 300) return "ok";
5650
+ if (STATUS_CLASS[status]) return STATUS_CLASS[status];
5651
+ if (status >= 500) return "server-error";
5652
+ return "bad-response";
5653
+ }
5654
+ var CAPABILITIES_HEADER = "x-hcm-capabilities";
5655
+ function buildUrl(path7, params) {
5656
+ const entries = Object.entries(params ?? {}).filter(([, v]) => v !== void 0 && v !== null);
5657
+ if (entries.length === 0) return path7;
5658
+ const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
5659
+ return `${path7}${path7.includes("?") ? "&" : "?"}${qs}`;
5660
+ }
5661
+ function lowerHeaders(raw) {
5662
+ const out = {};
5663
+ if (raw && typeof raw === "object") {
5664
+ for (const [k, v] of Object.entries(raw)) {
5665
+ out[k.toLowerCase()] = String(v);
5666
+ }
5667
+ }
5668
+ return out;
5669
+ }
5670
+ function toOutcome(status, data, headers) {
5671
+ const cls = classifyStatus(status);
5672
+ if (status === 204) return { status, body: null, error: "ok", headers };
5673
+ const empty = data === void 0 || data === null || typeof data === "string" && data.trim() === "";
5674
+ if (empty) {
5675
+ return { status, body: null, error: cls === "ok" ? "bad-response" : cls, headers };
5676
+ }
5677
+ if (typeof data === "string") {
5678
+ try {
5679
+ return { status, body: JSON.parse(data), error: cls, headers };
5680
+ } catch {
5681
+ return { status, body: null, error: cls === "ok" ? "bad-response" : cls, headers };
5682
+ }
5683
+ }
5684
+ return { status, body: data, error: cls, headers };
5685
+ }
5686
+ function httpProbe(http, capabilities) {
5687
+ async function once(fn) {
5688
+ try {
5689
+ const res = await fn();
5690
+ return toOutcome(res.status, res.data, lowerHeaders(res.headers));
5691
+ } catch (e) {
5692
+ const msg = e instanceof Error ? e.message : String(e);
5693
+ const local = /Invalid URL|ERR_INVALID_URL|ERR_UNESCAPED_CHARACTERS/i.test(msg);
5694
+ return { status: -1, body: null, error: local ? "bad-response" : "network", headers: {} };
5695
+ }
5696
+ }
5697
+ const headers = { [CAPABILITIES_HEADER]: capabilities };
5698
+ const probe = {
5699
+ capabilities,
5700
+ get(path7, params) {
5701
+ return once(
5702
+ () => http.get(buildUrl(path7, params), { headers, validateStatus: () => true })
5703
+ );
5704
+ },
5705
+ post(path7, body) {
5706
+ return once(
5707
+ () => http.post(path7, body, {
5708
+ headers: { ...headers, "content-type": "application/json" },
5709
+ validateStatus: () => true
5710
+ })
5711
+ );
5712
+ },
5713
+ /**
5714
+ * 🔴 **按原始路径回填、输入全覆盖**:调用方拿 `out[path]` 取结果,
5715
+ * 少一个键就会读成 `undefined` 然后当成「没这个东西」。
5716
+ * 🔴 一路炸不许拖垮整批——逐路径独立归类。
5717
+ */
5718
+ async getMany(paths) {
5719
+ const pairs = await Promise.all(
5720
+ paths.map(async (p) => [
5721
+ p,
5722
+ await probe.get(p).catch(() => networkOutcome())
5723
+ ])
5724
+ );
5725
+ return Object.fromEntries(pairs);
5726
+ },
5727
+ async postMany(jobs) {
5728
+ const pairs = await Promise.all(
5729
+ jobs.map(async (j) => [
5730
+ j.path,
5731
+ await probe.post(j.path, j.body).catch(() => networkOutcome())
5732
+ ])
5733
+ );
5734
+ return Object.fromEntries(pairs);
5735
+ }
5736
+ };
5737
+ return probe;
5738
+ }
5739
+ function networkOutcome() {
5740
+ return { status: -1, body: null, error: "network", headers: {} };
5243
5741
  }
5244
5742
  export {
5743
+ AUTH_INFO_PATH,
5744
+ BLIND_SPOTS,
5245
5745
  CliError,
5246
5746
  CliErrorCode,
5247
5747
  DEFAULT_AGENT_ID,
5248
5748
  DEFAULT_SESSION_TTL_SECONDS,
5749
+ DOCTOR_ANCHORS,
5750
+ EXTENSION_SOURCE_HEADER,
5249
5751
  ExitCode,
5752
+ GATE_COMPONENTS,
5250
5753
  HcmClient,
5754
+ LayoutContainmentError,
5755
+ MANIFEST_PATH,
5756
+ MAX_MANIFEST_PAGES,
5757
+ MAX_SOURCE_PROBES,
5758
+ META_PATH,
5251
5759
  MINI_CONTEXT_FILE,
5252
5760
  MINI_CONTEXT_KIND,
5253
5761
  MINI_CONTEXT_VERSION,
5254
5762
  MINI_PULL_MANIFEST_FILE,
5255
5763
  MINI_REMOTE_ROOT,
5256
5764
  MINI_STATE_DIR,
5765
+ MODELS_PATH,
5257
5766
  PASSWORD_CHANGE_REQUIRED,
5258
5767
  PROACTIVE_REFRESH_MARGIN_SECONDS,
5768
+ ReconLayout,
5259
5769
  RefStore,
5260
5770
  SDK_VERSION,
5261
5771
  TokenStore,
5772
+ VERSION_PATH,
5262
5773
  WsClient,
5263
5774
  absoluteDownloadUrl,
5264
5775
  adminResetPassword,
@@ -5281,6 +5792,8 @@ export {
5281
5792
  classifyMessage,
5282
5793
  clearMetaCache,
5283
5794
  clearModelCache,
5795
+ compareFingerprint,
5796
+ computeFingerprint,
5284
5797
  conversationStateFile,
5285
5798
  create,
5286
5799
  createConversation,
@@ -5310,6 +5823,7 @@ export {
5310
5823
  fetchConversationTimeline,
5311
5824
  fetchConversationTimelineStrict,
5312
5825
  fetchRecentConversations,
5826
+ fetchReconIdentity,
5313
5827
  fetchSkillCatalog,
5314
5828
  fetchSkillFile,
5315
5829
  fetchSkillMarkdown,
@@ -5328,6 +5842,7 @@ export {
5328
5842
  globalConfigFile,
5329
5843
  guessMimeType,
5330
5844
  hcmConfigDir,
5845
+ httpProbe,
5331
5846
  identitiesDir,
5332
5847
  identityDir,
5333
5848
  identityMetaFile,
@@ -5353,6 +5868,8 @@ export {
5353
5868
  loginPat,
5354
5869
  matchSkills,
5355
5870
  migrateLegacyProfiles,
5871
+ modelEntries,
5872
+ modelKeys,
5356
5873
  needsRefresh,
5357
5874
  normalizeReferences,
5358
5875
  oneShot,
@@ -5365,6 +5882,7 @@ export {
5365
5882
  parseSkillFrontmatter,
5366
5883
  parseSkillRequirements,
5367
5884
  patchSettingDomain,
5885
+ probeExtensionSource,
5368
5886
  profileDir,
5369
5887
  profileFile,
5370
5888
  pullMiniAppProject,
@@ -5382,8 +5900,10 @@ export {
5382
5900
  resolveChironBase,
5383
5901
  resolveRefs,
5384
5902
  resolveSkillInstallOrder,
5903
+ runDoctor,
5385
5904
  runImport,
5386
5905
  runMiniSmokeChecks,
5906
+ safeSegment,
5387
5907
  saveConversationState,
5388
5908
  saveEnv,
5389
5909
  saveGlobalConfig,
@@ -5393,6 +5913,7 @@ export {
5393
5913
  saveWorkspaceFileContent,
5394
5914
  sendMessageAndStream,
5395
5915
  snakeToCamel,
5916
+ tenantSegment,
5396
5917
  toJson,
5397
5918
  toOrigin,
5398
5919
  toPrincipal,