abelien-harness 0.7.1 → 0.9.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 (38) hide show
  1. package/.env.example +19 -20
  2. package/README.md +12 -12
  3. package/package.json +1 -1
  4. package/skills/abelien-hardware/SKILL.md +4 -4
  5. package/skills/abelien-hardware/domain.json +4 -4
  6. package/skills/abelien-hardware/references/knowledge-recall-service-contract.md +4 -4
  7. package/skills/abelien-hardware/references/search-index-format.md +5 -5
  8. package/skills/abelien-hardware/references/search-index-release.md +7 -7
  9. package/skills/abelien-hardware/references/selection-service-contract.md +3 -3
  10. package/skills/abelien-hardware/scripts/build-search-index.mjs +1 -1
  11. package/skills/abelien-hardware/scripts/oss-object-store.mjs +247 -0
  12. package/skills/abelien-hardware/scripts/{tos-wiki-search.mjs → oss-wiki-search.mjs} +19 -19
  13. package/skills/abelien-hardware/scripts/search-index-release.mjs +25 -25
  14. package/skills/abelien-hardware-selection/SKILL.md +2 -2
  15. package/skills/abelien-hardware-selection/domain.json +2 -2
  16. package/skills/abelien-hardware-selection/scripts/load-guide.mjs +1 -1
  17. package/skills/abelien-harness/references/adapter-contract.md +1 -1
  18. package/skills/abelien-harness/references/m1-integration.md +2 -2
  19. package/skills/abelien-harness/scripts/ab.mjs +8 -1
  20. package/skills/abelien-harness/scripts/studio.mjs +15 -14
  21. package/skills/abelien-reference/SKILL.md +2 -2
  22. package/skills/abelien-reference/domain.json +2 -2
  23. package/skills/abelien-reference/references/research-contract.md +2 -1
  24. package/skills/abelien-reference/schemas/reference-evidence.v1.json +1 -1
  25. package/skills/abelien-reference/scripts/finalize.mjs +3 -1
  26. package/skills/abelien-reference/scripts/research.mjs +1 -1
  27. package/skills/abelien-structure/domain.json +1 -1
  28. package/skills/abelien-structure/worker/abelien_cad.py +5 -4
  29. package/skills/abelien-structure/worker/source-contract.mjs +38 -26
  30. package/skills/abelien-workspace/SKILL.md +1 -1
  31. package/skills/abelien-workspace/assets/journey-stage-plan.json +48 -15
  32. package/skills/abelien-workspace/domain.json +3 -2
  33. package/skills/abelien-workspace/references/hardware-journey-orchestration.md +2 -2
  34. package/skills/abelien-workspace/references/harness-binding.md +3 -3
  35. package/skills/abelien-workspace/references/user-journey.md +5 -5
  36. package/skills/abelien-workspace/scripts/journey-next.mjs +10 -2
  37. package/skills/abelien-workspace/workflows/user-journey.json +1 -1
  38. package/skills/abelien-hardware/scripts/tos-object-store.mjs +0 -202
@@ -1,202 +0,0 @@
1
- import crypto from "node:crypto";
2
-
3
- const EMPTY = Buffer.alloc(0);
4
-
5
- function tosError(code, message, details) {
6
- const error = new Error(message);
7
- error.code = code;
8
- if (details !== undefined) error.details = details;
9
- return error;
10
- }
11
-
12
- export function sha256(value) {
13
- return crypto.createHash("sha256").update(value).digest("hex");
14
- }
15
-
16
- function hmac(key, value, encoding) {
17
- return crypto.createHmac("sha256", key).update(value).digest(encoding);
18
- }
19
-
20
- function first(env, primary, alias) {
21
- return env[primary] || env[alias] || "";
22
- }
23
-
24
- export function normalizeTosEnv(env = {}) {
25
- return {
26
- ...env,
27
- TOS_REGION: first(env, "TOS_REGION", "CAD_STORAGE_REGION"),
28
- TOS_ENDPOINT: first(env, "TOS_ENDPOINT", "CAD_STORAGE_ENDPOINT"),
29
- TOS_BUCKET: first(env, "TOS_BUCKET", "CAD_STORAGE_BUCKET"),
30
- TOS_ACCESS_KEY_ID: first(env, "TOS_ACCESS_KEY_ID", "CAD_STORAGE_ACCESS_KEY"),
31
- TOS_SECRET_ACCESS_KEY: first(env, "TOS_SECRET_ACCESS_KEY", "CAD_STORAGE_SECRET_KEY"),
32
- TOS_ADDRESSING_STYLE: first(env, "TOS_ADDRESSING_STYLE", "CAD_STORAGE_ADDRESSING_STYLE") || "virtual",
33
- };
34
- }
35
-
36
- function normalizePrefix(value) {
37
- const cleaned = String(value || "hardware_profile_wiki/").replace(/^\/+|\/+$/g, "");
38
- if (!cleaned || cleaned.split("/").some(part => part === "." || part === "..")) {
39
- throw tosError("VALIDATION", "invalid TOS Wiki prefix");
40
- }
41
- return `${cleaned}/`;
42
- }
43
-
44
- function positiveIntegerEnv(env, name, fallback) {
45
- if (env[name] === undefined || env[name] === "") return fallback;
46
- const value = Number(env[name]);
47
- if (!Number.isInteger(value) || value <= 0) throw tosError("VALIDATION", `${name} must be a positive integer`, { name });
48
- return value;
49
- }
50
-
51
- export function createTosConfig(inputEnv = {}) {
52
- const env = normalizeTosEnv(inputEnv);
53
- const required = ["TOS_REGION", "TOS_ENDPOINT", "TOS_BUCKET", "TOS_ACCESS_KEY_ID", "TOS_SECRET_ACCESS_KEY"];
54
- const missing = required.filter(name => !env[name]);
55
- if (missing.length) throw tosError("ENV_MISSING", `env var not set: ${missing[0]}`, { name: missing[0], missing });
56
- let endpoint;
57
- try { endpoint = new URL(env.TOS_ENDPOINT); }
58
- catch { throw tosError("VALIDATION", "TOS_ENDPOINT must be an absolute URL"); }
59
- if (!['http:', 'https:'].includes(endpoint.protocol)) throw tosError("VALIDATION", "TOS_ENDPOINT must use http or https");
60
- if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
61
- throw tosError("VALIDATION", "TOS_ENDPOINT must not contain credentials, query or fragment");
62
- }
63
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{1,62}$/.test(env.TOS_BUCKET)) throw tosError("VALIDATION", "invalid TOS_BUCKET");
64
- if (!['virtual', 'path'].includes(env.TOS_ADDRESSING_STYLE)) throw tosError("VALIDATION", "TOS_ADDRESSING_STYLE must be virtual or path");
65
- return {
66
- region: env.TOS_REGION,
67
- endpoint,
68
- bucket: env.TOS_BUCKET,
69
- accessKeyId: env.TOS_ACCESS_KEY_ID,
70
- secretAccessKey: env.TOS_SECRET_ACCESS_KEY,
71
- sessionToken: env.TOS_SESSION_TOKEN || "",
72
- addressingStyle: env.TOS_ADDRESSING_STYLE,
73
- prefix: normalizePrefix(env.TOS_WIKI_PREFIX),
74
- // 网络读取的两道边界。此前 module adapter 路径既无超时也无体积上限:一个不响应的
75
- // 对象存储会把召回挂到进程结束,一个超大对象会被整段读进内存(召回本身还并发 8 路)。
76
- // 默认值取"够宽但有限"——Wiki 与分类分片都是小对象,写读都要走这两道闸。
77
- timeoutMs: positiveIntegerEnv(env, "TOS_REQUEST_TIMEOUT_MS", 30000),
78
- maxObjectBytes: positiveIntegerEnv(env, "TOS_MAX_OBJECT_BYTES", 16 * 1024 * 1024),
79
- };
80
- }
81
-
82
- // 先按声明的 content-length 拦一道(能在读之前拒),再在读的过程中边读边数——
83
- // arrayBuffer() 是先整段进内存再回看长度,对"超大对象"这个失败形态等于没拦。
84
- async function readObjectBytes(response, maxBytes, key, method) {
85
- const declared = Number(response.headers?.get?.("content-length"));
86
- if (Number.isFinite(declared) && declared > maxBytes) {
87
- throw tosError("TOS_OBJECT_TOO_LARGE", `TOS object exceeds TOS_MAX_OBJECT_BYTES (${maxBytes})`, { key, method, maxBytes, contentLength: declared });
88
- }
89
- if (response.body && typeof response.body.getReader === "function") {
90
- const reader = response.body.getReader();
91
- const chunks = [];
92
- let total = 0;
93
- for (;;) {
94
- const { done, value } = await reader.read();
95
- if (done) break;
96
- total += value.byteLength;
97
- if (total > maxBytes) {
98
- await reader.cancel().catch(() => {});
99
- throw tosError("TOS_OBJECT_TOO_LARGE", `TOS object exceeds TOS_MAX_OBJECT_BYTES (${maxBytes})`, { key, method, maxBytes });
100
- }
101
- chunks.push(Buffer.from(value));
102
- }
103
- return Buffer.concat(chunks);
104
- }
105
- const bytes = typeof response.arrayBuffer === "function"
106
- ? Buffer.from(await response.arrayBuffer())
107
- : Buffer.from(await response.text(), "utf8");
108
- if (bytes.length > maxBytes) {
109
- throw tosError("TOS_OBJECT_TOO_LARGE", `TOS object exceeds TOS_MAX_OBJECT_BYTES (${maxBytes})`, { key, method, maxBytes, contentLength: bytes.length });
110
- }
111
- return bytes;
112
- }
113
-
114
- function encodePath(value) {
115
- return String(value).split("/").map(part => encodeURIComponent(part).replace(/[!'()*]/g, char => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
116
- }
117
-
118
- function objectLocation(config, key) {
119
- const cleanKey = String(key).replace(/^\/+/, "");
120
- if (!cleanKey || cleanKey.split("/").some(part => part === "." || part === "..")) throw tosError("VALIDATION", "invalid TOS object key", { key });
121
- const parts = [config.endpoint.pathname.replace(/^\/+|\/+$/g, "")].filter(Boolean);
122
- let host = config.endpoint.host;
123
- if (config.addressingStyle === "path") parts.push(config.bucket);
124
- else host = `${config.bucket}.${host}`;
125
- parts.push(cleanKey);
126
- const uri = `/${encodePath(parts.join("/"))}`;
127
- return { host, uri, url: `${config.endpoint.protocol}//${host}${uri}` };
128
- }
129
-
130
- export function signTosObjectRequest(config, key, { method = "GET", body = EMPTY, headers = {}, now = new Date() } = {}) {
131
- const payload = Buffer.isBuffer(body) ? body : Buffer.from(body ?? "");
132
- const location = objectLocation(config, key);
133
- const timestamp = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
134
- const date = timestamp.slice(0, 8);
135
- const signed = {
136
- host: location.host,
137
- "x-amz-content-sha256": sha256(payload),
138
- "x-amz-date": timestamp,
139
- };
140
- for (const [name, value] of Object.entries(headers)) signed[name.toLowerCase()] = String(value).trim();
141
- if (config.sessionToken) signed["x-amz-security-token"] = config.sessionToken;
142
- const names = Object.keys(signed).sort();
143
- const canonicalHeaders = names.map(name => `${name}:${signed[name]}\n`).join("");
144
- const signedHeaders = names.join(";");
145
- const canonicalRequest = [method.toUpperCase(), location.uri, "", canonicalHeaders, signedHeaders, sha256(payload)].join("\n");
146
- const scope = `${date}/${config.region}/s3/aws4_request`;
147
- const stringToSign = ["AWS4-HMAC-SHA256", timestamp, scope, sha256(canonicalRequest)].join("\n");
148
- const dateKey = hmac(`AWS4${config.secretAccessKey}`, date);
149
- const regionKey = hmac(dateKey, config.region);
150
- const serviceKey = hmac(regionKey, "s3");
151
- const signingKey = hmac(serviceKey, "aws4_request");
152
- const signature = hmac(signingKey, stringToSign, "hex");
153
- signed.authorization = `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
154
- return { url: location.url, method: method.toUpperCase(), headers: signed, body: payload };
155
- }
156
-
157
- export async function requestTosObject(config, key, { method = "GET", body = EMPTY, headers = {}, optional = false, fetchImpl = fetch } = {}) {
158
- const request = signTosObjectRequest(config, key, { method, body, headers });
159
- let response;
160
- try {
161
- response = await fetchImpl(request.url, {
162
- method: request.method,
163
- headers: request.headers,
164
- // 超时覆盖到响应体读完(undici 把 signal 一路带到 body 流):否则一个连上就不发完的
165
- // 对端同样能挂死召回。既有桩 fetch 忽略 signal,行为不变。
166
- signal: AbortSignal.timeout(config.timeoutMs),
167
- ...(request.method === "GET" || request.method === "HEAD" ? {} : { body: request.body }),
168
- });
169
- } catch (cause) {
170
- if (cause?.name === "TimeoutError" || cause?.code === "ABORT_ERR") {
171
- throw tosError("TOS_REQUEST_TIMEOUT", `TOS request timed out after ${config.timeoutMs}ms`, { key, method: request.method, timeout_ms: config.timeoutMs });
172
- }
173
- throw tosError("TOS_REQUEST_FAILED", `TOS request failed: ${cause.message}`, { key, method: request.method });
174
- }
175
- if (!response.ok) {
176
- if (optional && response.status === 404) return null;
177
- throw tosError("TOS_HTTP_STATUS", `TOS returned HTTP ${response.status}`, { key, method: request.method, status: response.status });
178
- }
179
- const bytes = request.method === "HEAD"
180
- ? EMPTY
181
- : await readObjectBytes(response, config.maxObjectBytes, key, request.method);
182
- return {
183
- key,
184
- bytes,
185
- etag: response.headers?.get?.("etag")?.replace(/^"|"$/g, "") || sha256(bytes),
186
- lastModified: response.headers?.get?.("last-modified") || null,
187
- contentLength: Number(response.headers?.get?.("content-length")) || null,
188
- metadataSha256: response.headers?.get?.("x-amz-meta-sha256") || response.headers?.get?.("x-tos-meta-sha256") || null,
189
- };
190
- }
191
-
192
- export function getTosObject(config, key, options = {}) {
193
- return requestTosObject(config, key, { ...options, method: "GET" });
194
- }
195
-
196
- export function putTosObject(config, key, body, options = {}) {
197
- return requestTosObject(config, key, { ...options, method: "PUT", body });
198
- }
199
-
200
- export function headTosObject(config, key, options = {}) {
201
- return requestTosObject(config, key, { ...options, method: "HEAD" });
202
- }