abelien-harness 0.5.0 → 0.6.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/.env.example +1 -0
- package/README.md +11 -18
- package/package.json +2 -3
- package/skills/abelien-appearance/SKILL.md +1 -0
- package/skills/abelien-clarify/SKILL.md +12 -2
- package/skills/abelien-clarify/references/examples.md +22 -0
- package/skills/abelien-clarify/scripts/check_result.py +38 -3
- package/skills/abelien-clarify/scripts/test_check_result.py +32 -1
- package/skills/abelien-guide/SKILL.md +1 -1
- package/skills/abelien-hardware/SKILL.md +4 -4
- package/skills/abelien-hardware/domain.json +1 -1
- package/skills/abelien-hardware/references/search-index-format.md +4 -2
- package/skills/abelien-hardware/references/web-evidence-contract.md +3 -3
- package/skills/abelien-hardware/scripts/dimensions.mjs +23 -8
- package/skills/abelien-hardware/scripts/electrical.mjs +62 -19
- package/skills/abelien-hardware/scripts/finalize-selection.mjs +3 -5
- package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +17 -0
- package/skills/abelien-hardware/scripts/public-page.mjs +11 -121
- package/skills/abelien-hardware/scripts/response-redaction.mjs +1 -130
- package/skills/abelien-hardware/scripts/selection-request.mjs +6 -5
- package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +9 -6
- package/skills/abelien-hardware/scripts/volc-web-search.mjs +10 -32
- package/skills/abelien-hardware/scripts/web-common.mjs +4 -10
- package/skills/abelien-hardware/scripts/web-integrity.mjs +1 -1
- package/skills/abelien-hardware/scripts/web-provider-chain.mjs +2 -1
- package/skills/abelien-hardware/scripts/web-receipt.mjs +4 -2
- package/skills/abelien-hardware/scripts/web-search-plan.mjs +5 -1
- package/skills/abelien-hardware/scripts/web-search.mjs +4 -3
- package/skills/abelien-hardware-selection/domain.json +2 -2
- package/skills/abelien-hardware-selection/references/selection-service-contract.md +18 -10
- package/skills/abelien-hardware-selection/scripts/select.mjs +13 -5
- package/skills/abelien-harness/scripts/ab.mjs +24 -2
- package/skills/abelien-harness/scripts/web/common.mjs +20 -0
- package/skills/abelien-harness/scripts/web/public-read.mjs +168 -0
- package/skills/abelien-harness/scripts/web/redact.mjs +136 -0
- package/skills/abelien-harness/scripts/web/volc.mjs +49 -0
- package/skills/abelien-reference/SKILL.md +51 -0
- package/skills/abelien-reference/domain.json +92 -0
- package/skills/abelien-reference/fixtures/create.final.json +102 -0
- package/skills/abelien-reference/fixtures/restore.collected.json +189 -0
- package/skills/abelien-reference/fixtures/restore.final.json +267 -0
- package/skills/abelien-reference/references/research-contract.md +74 -0
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +140 -0
- package/skills/abelien-reference/scripts/evidence.mjs +60 -0
- package/skills/abelien-reference/scripts/finalize.mjs +116 -0
- package/skills/abelien-reference/scripts/measure.mjs +62 -0
- package/skills/abelien-reference/scripts/png.mjs +86 -0
- package/skills/abelien-reference/scripts/research.mjs +179 -0
- package/skills/abelien-structure/SKILL.md +2 -2
- package/skills/abelien-structure/domain.json +64 -0
- package/skills/abelien-structure/references/build123d-execution.md +1 -1
- package/skills/abelien-structure/references/handoff.md +1 -1
- package/skills/abelien-structure/worker/abelien_cad.py +585 -0
- package/skills/abelien-structure/worker/assembly-validation.mjs +106 -0
- package/skills/abelien-structure/worker/cad.py +1623 -0
- package/skills/abelien-structure/worker/cli-adapter.mjs +207 -0
- package/skills/abelien-structure/worker/component-fit-glb.mjs +173 -0
- package/skills/abelien-structure/worker/component-fit.d.mts +82 -0
- package/skills/abelien-structure/worker/component-fit.mjs +167 -0
- package/skills/abelien-structure/worker/component-target-image.d.mts +23 -0
- package/skills/abelien-structure/worker/component-target-image.mjs +107 -0
- package/skills/abelien-structure/worker/component-target-render.py +58 -0
- package/skills/abelien-structure/worker/config.mjs +6 -0
- package/skills/abelien-structure/worker/engineering-fit.d.mts +48 -0
- package/skills/abelien-structure/worker/engineering-fit.mjs +118 -0
- package/skills/abelien-structure/worker/engineering-policy.json +16 -0
- package/skills/abelien-structure/worker/engineering-repair.py +147 -0
- package/skills/abelien-structure/worker/engineering.py +324 -0
- package/skills/abelien-structure/worker/identity.mjs +98 -0
- package/skills/abelien-structure/worker/inspection-geometry.mjs +71 -0
- package/skills/abelien-structure/worker/jobs.mjs +451 -0
- package/skills/abelien-structure/worker/library-runtime.mjs +15 -0
- package/skills/abelien-structure/worker/main.mjs +102 -0
- package/skills/abelien-structure/worker/manifest-digest.mjs +11 -0
- package/skills/abelien-structure/worker/materials.mjs +52 -0
- package/skills/abelien-structure/worker/native-launcher.py +13 -0
- package/skills/abelien-structure/worker/native.mjs +157 -0
- package/skills/abelien-structure/worker/representation.mjs +47 -0
- package/skills/abelien-structure/worker/sandbox.mjs +41 -0
- package/skills/abelien-structure/worker/source-contract.mjs +143 -0
- package/skills/abelien-structure/worker/source_call_stats.py +59 -0
- package/skills/abelien-structure/worker/supervisor.mjs +38 -0
- package/skills/abelien-workspace/SKILL.md +11 -0
- package/skills/abelien-workspace/assets/journey-stage-plan.json +155 -0
- package/skills/abelien-workspace/domain.json +25 -2
- package/skills/abelien-workspace/references/hardware-journey-orchestration.md +1 -1
- package/skills/abelien-workspace/references/harness-binding.md +2 -2
- package/skills/abelien-workspace/references/user-journey.md +10 -0
- package/skills/abelien-workspace/scripts/journey-next.mjs +38 -0
- package/skills/abelien-workspace/workflows/definitions/399ef372790b7330d0ef88ce5096e7f69943d4793de97b2ed6c25165a332612f.json +230 -0
- package/skills/abelien-workspace/workflows/definitions/591a7f21e0b0ad586e340163f7533ac910356091ba8d2a26e250c0acbc80cd8b.json +176 -0
- package/skills/abelien-workspace/workflows/definitions/d9f8c6cdf0b2c41cdeb1c72ac5d1e37d95a7caac3272d434a3c7e8483fc565de.json +150 -0
- package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +3 -2
- package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +3 -2
- package/skills/abelien-workspace/workflows/user-journey-hardware.json +3 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// 响应正文脱敏(P2-13.03 自 hardware 移入内核):在原文、百分号编码与 JSON 转义的各层里找出密钥,
|
|
2
|
+
// 统一替换为 [redacted]。调用方决定哪些密钥要脱敏;本模块不读环境变量。
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// Four numbers describe a whole unchanged run or one decoded unit:
|
|
6
|
+
// output start/end, original start/end. No object is allocated per character.
|
|
7
|
+
function appendSpan(spans, start, end, originalStart, originalEnd) {
|
|
8
|
+
if (start === end) return;
|
|
9
|
+
const last = spans.length - 4;
|
|
10
|
+
if (last >= 0 && spans[last + 1] === start && spans[last + 3] === originalStart &&
|
|
11
|
+
spans[last + 1] - spans[last] === spans[last + 3] - spans[last + 2] &&
|
|
12
|
+
end - start === originalEnd - originalStart) {
|
|
13
|
+
spans[last + 1] = end; spans[last + 3] = originalEnd; return;
|
|
14
|
+
}
|
|
15
|
+
spans.push(start, end, originalStart, originalEnd);
|
|
16
|
+
}
|
|
17
|
+
function spanAt(spans, index) {
|
|
18
|
+
let low = 0, high = spans.length / 4 - 1;
|
|
19
|
+
while (low <= high) {
|
|
20
|
+
const middle = (low + high) >>> 1, offset = middle * 4;
|
|
21
|
+
if (index < spans[offset]) high = middle - 1;
|
|
22
|
+
else if (index >= spans[offset + 1]) low = middle + 1;
|
|
23
|
+
else return offset;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Redaction span missing");
|
|
26
|
+
}
|
|
27
|
+
function originalRange(projection, start, end) {
|
|
28
|
+
const spans = projection.spans, first = spanAt(spans, start), last = spanAt(spans, end - 1);
|
|
29
|
+
const from = spans[first + 1] - spans[first] === spans[first + 3] - spans[first + 2]
|
|
30
|
+
? spans[first + 2] + start - spans[first] : spans[first + 2];
|
|
31
|
+
const to = spans[last + 1] - spans[last] === spans[last + 3] - spans[last + 2]
|
|
32
|
+
? spans[last + 2] + end - spans[last] : spans[last + 3];
|
|
33
|
+
return [from, to];
|
|
34
|
+
}
|
|
35
|
+
function copyRange(projection, start, end, spans, output) {
|
|
36
|
+
if (start === end) return output;
|
|
37
|
+
const source = projection.spans;
|
|
38
|
+
for (let offset = spanAt(source, start); start < end; offset += 4) {
|
|
39
|
+
const stop = Math.min(end, source[offset + 1]);
|
|
40
|
+
const linear = source[offset + 1] - source[offset] === source[offset + 3] - source[offset + 2];
|
|
41
|
+
const from = linear ? source[offset + 2] + start - source[offset] : source[offset + 2];
|
|
42
|
+
const to = linear ? source[offset + 2] + stop - source[offset] : source[offset + 3];
|
|
43
|
+
appendSpan(spans, output, output + stop - start, from, to);
|
|
44
|
+
output += stop - start; start = stop;
|
|
45
|
+
}
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
function percentLayer(projection) {
|
|
49
|
+
const value = projection.value, chunks = [], spans = [];
|
|
50
|
+
let cursor = 0, output = 0, changed = false;
|
|
51
|
+
for (let index = value.indexOf("%"); index !== -1; index = value.indexOf("%", index + 1)) {
|
|
52
|
+
let encoded = "", decoded = null, size = 0;
|
|
53
|
+
// UTF-8 has at most four bytes per scalar; malformed units stay intact.
|
|
54
|
+
for (let bytes = 1; bytes <= 4; bytes++) {
|
|
55
|
+
const part = value.slice(index + (bytes - 1) * 3, index + bytes * 3);
|
|
56
|
+
if (!/^%[0-9a-f]{2}$/i.test(part)) break;
|
|
57
|
+
encoded += part;
|
|
58
|
+
try { decoded = decodeURIComponent(encoded); size = bytes * 3; break; } catch { /* Try this scalar's remaining bytes. */ }
|
|
59
|
+
}
|
|
60
|
+
if (decoded === null) continue;
|
|
61
|
+
chunks.push(value.slice(cursor, index), decoded);
|
|
62
|
+
output = copyRange(projection, cursor, index, spans, output);
|
|
63
|
+
const [from, to] = originalRange(projection, index, index + size);
|
|
64
|
+
appendSpan(spans, output, output + decoded.length, from, to); output += decoded.length;
|
|
65
|
+
cursor = index + size; index = cursor - 1; changed = true;
|
|
66
|
+
}
|
|
67
|
+
if (!changed) return null;
|
|
68
|
+
chunks.push(value.slice(cursor)); copyRange(projection, cursor, value.length, spans, output);
|
|
69
|
+
return { value: chunks.join(""), spans };
|
|
70
|
+
}
|
|
71
|
+
function* jsonChildren(projection) {
|
|
72
|
+
const value = projection.value, tokenPattern = /"(?:\\.|[^"\\])*"/y;
|
|
73
|
+
let coveredUntil = -1;
|
|
74
|
+
// Every possible start remains eligible, including a preceding token's closing
|
|
75
|
+
// quote. Interior escaped starts in a valid decoded child are already covered
|
|
76
|
+
// by that child's traversal; avoid materializing all of its overlapping suffixes.
|
|
77
|
+
for (let start = value.indexOf('"'); start !== -1; start = value.indexOf('"', start + 1)) {
|
|
78
|
+
if (start < coveredUntil) continue;
|
|
79
|
+
tokenPattern.lastIndex = start;
|
|
80
|
+
const match = tokenPattern.exec(value); if (!match) continue;
|
|
81
|
+
const token = match[0], end = start + token.length - 1;
|
|
82
|
+
// Removing quotes without decoding changes neither matches nor percent units.
|
|
83
|
+
// The parent already covers that identity projection.
|
|
84
|
+
if (!token.includes("\\")) continue;
|
|
85
|
+
let decoded; try { decoded = JSON.parse(token); } catch { continue; }
|
|
86
|
+
coveredUntil = end;
|
|
87
|
+
const spans = []; let cursor = start + 1, output = 0;
|
|
88
|
+
for (let escape = value.indexOf("\\", cursor); escape !== -1 && escape < end; escape = value.indexOf("\\", cursor)) {
|
|
89
|
+
output = copyRange(projection, cursor, escape, spans, output);
|
|
90
|
+
const size = value[escape + 1] === "u" ? 6 : 2;
|
|
91
|
+
const unit = JSON.parse('"' + value.slice(escape, escape + size) + '"');
|
|
92
|
+
const [from, to] = originalRange(projection, escape, escape + size);
|
|
93
|
+
appendSpan(spans, output, output + unit.length, from, to); output += unit.length;
|
|
94
|
+
cursor = escape + size;
|
|
95
|
+
}
|
|
96
|
+
copyRange(projection, cursor, end, spans, output);
|
|
97
|
+
if (decoded.length) yield { value: decoded, spans };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function identity(projection) {
|
|
101
|
+
// Fixed-size memo entries, with UTF-16 preserving even lone-surrogate spelling.
|
|
102
|
+
const spans = Float64Array.from(projection.spans);
|
|
103
|
+
return createHash("sha256").update(String(projection.value.length) + ":").update(Buffer.from(projection.value, "utf16le"))
|
|
104
|
+
.update(Buffer.from(spans.buffer)).digest("hex");
|
|
105
|
+
}
|
|
106
|
+
export function redact(raw, input) {
|
|
107
|
+
// Callers may pass keys in any order or include empties: a shorter key redacted first would leave a longer key's tail
|
|
108
|
+
// visible, and an empty key matches everywhere. Dedupe, drop empties and go longest first (stable, so sorted callers are unchanged).
|
|
109
|
+
const keys = [...new Set(input.filter(key => typeof key === "string" && key.length > 0))].sort((a, b) => b.length - a.length);
|
|
110
|
+
if (!keys.length) return raw;
|
|
111
|
+
if (!raw.includes("%") && !raw.includes("\\")) return keys.reduce((value, key) => value.split(key).join("[redacted]"), raw);
|
|
112
|
+
const ranges = [], seen = new Set(), stack = [{ projection: { value: raw, spans: raw.length ? [0, raw.length, 0, raw.length] : [] } }];
|
|
113
|
+
while (stack.length) {
|
|
114
|
+
const frame = stack[stack.length - 1], projection = frame.projection;
|
|
115
|
+
if (!frame.children) {
|
|
116
|
+
const memo = identity(projection); if (seen.has(memo)) { stack.pop(); continue; } seen.add(memo);
|
|
117
|
+
for (const key of keys) for (let index = projection.value.indexOf(key); index !== -1; index = projection.value.indexOf(key, index + 1)) {
|
|
118
|
+
const [start, end] = originalRange(projection, index, index + key.length); ranges.push({ start, end });
|
|
119
|
+
}
|
|
120
|
+
frame.children = jsonChildren(projection);
|
|
121
|
+
}
|
|
122
|
+
const next = frame.children.next();
|
|
123
|
+
if (!next.done) { stack.push({ projection: next.value }); continue; }
|
|
124
|
+
// A percent layer is a tail transition, releasing its finished parent instead
|
|
125
|
+
// of retaining an eager queue of full bodies. JSON children are visited lazily.
|
|
126
|
+
const percent = percentLayer(projection); stack.pop(); if (percent) stack.push({ projection: percent });
|
|
127
|
+
}
|
|
128
|
+
let result = "", start = 0, end = 0;
|
|
129
|
+
for (const range of ranges.sort((a, b) => a.start - b.start || b.end - a.end)) {
|
|
130
|
+
if (range.start < end) { end = Math.max(end, range.end); continue; }
|
|
131
|
+
if (end > start) { result += "[redacted]"; start = end; }
|
|
132
|
+
result += raw.slice(start, range.start); start = range.start; end = range.end;
|
|
133
|
+
}
|
|
134
|
+
if (end > start) { result += "[redacted]"; start = end; }
|
|
135
|
+
return result + raw.slice(start);
|
|
136
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Volc 联网搜索客户端(P2-13.03 自 hardware 移入内核):凭据只取受信宿主环境变量 WEB_SEARCH_API_KEY,
|
|
2
|
+
// 原文先脱敏再保存、计算摘要,失败按「确定拒绝 / 结果未知」区分。错误码与脱敏函数由调用方绑定。
|
|
3
|
+
import { failure, text, timed, sha256, digest } from "./common.mjs";
|
|
4
|
+
|
|
5
|
+
// The receipt request_id rule; hardware verification imports this so writer and verifier cannot drift.
|
|
6
|
+
export const volcRequestId = response => { const id = response?.ResponseMetadata?.RequestId; return text(id) ? id : null; };
|
|
7
|
+
export const validVolcResult = response => text(response?.ResponseMetadata?.RequestId) && !response.ResponseMetadata.Error && response.success !== false && Array.isArray(response.Result?.WebResults);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* codes:capability / config / provider / auth / rateLimit / response / transport / timeout;
|
|
11
|
+
* ownsError(error) 判断错误是否已是本域的码(是则原样抛出,否则记为 transport 结果未知);
|
|
12
|
+
* redact(raw, env) 在保存前去掉凭据;timeoutMessage 为超时错误的文本。
|
|
13
|
+
*/
|
|
14
|
+
export function createVolcSearch({ codes, ownsError, redact, timeoutMessage }) {
|
|
15
|
+
for (const key of ["capability", "config", "provider", "auth", "rateLimit", "response", "transport", "timeout"]) if (typeof codes?.[key] !== "string" || !codes[key]) throw new Error(`volc search code ${key} required`);
|
|
16
|
+
if (typeof ownsError !== "function" || typeof redact !== "function") throw new Error("volc search ownsError and redact required");
|
|
17
|
+
// Plans cap count at 5 (P2-05.02). Evidence saved by <=0.4.0 used the old default of 10;
|
|
18
|
+
// receipt validation still accepts that frozen shape.
|
|
19
|
+
const volcMaxCount = 5;
|
|
20
|
+
const volcBody = (item, count) => ({ Query: item.query, SearchType: "web", Count: count, Filter: { NeedUrl: true, NeedContent: false }, ContentFormats: "markdown", QueryControl: { QueryRewrite: false } });
|
|
21
|
+
const volcQueryRequest = item => volcBody(item, item.count ?? volcMaxCount);
|
|
22
|
+
const legacyVolcQueryRequest = item => volcBody(item, item.count ?? 10);
|
|
23
|
+
function validateVolcCapability(body) {
|
|
24
|
+
if (!text(body.Query) || !Number.isSafeInteger(body.Count) || body.Count < 1 || body.Count > 50) throw failure(codes.capability, "Volc Query must be nonempty and Count 1–50; original input is retained without truncation");
|
|
25
|
+
}
|
|
26
|
+
async function volcWebSearch(body, { env, fetchImpl = fetch, timeoutMs }) {
|
|
27
|
+
validateVolcCapability(body);
|
|
28
|
+
const key = env.WEB_SEARCH_API_KEY;
|
|
29
|
+
if (!text(key) || /[\r\n\0]/.test(key)) throw failure(codes.config, "WEB_SEARCH_API_KEY is required from the trusted host");
|
|
30
|
+
try {
|
|
31
|
+
return await timed(async signal => {
|
|
32
|
+
const response = await fetchImpl("https://open.feedcoopapi.com/search_api/web_search", { method: "POST", redirect: "error", signal, headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
33
|
+
const raw = redact(await response.text(), env), saved = { raw, raw_sha256: sha256(raw), response: null, response_sha256: null, request_id: null, http_status: response.status, received_at: new Date().toISOString() };
|
|
34
|
+
try {
|
|
35
|
+
saved.response = JSON.parse(raw); saved.response_sha256 = digest(saved.response);
|
|
36
|
+
saved.request_id = volcRequestId(saved.response);
|
|
37
|
+
} catch { /* Keep full received text. */ }
|
|
38
|
+
const reject = (code, message, unknown = false) => { const error = failure(code, message, { http_status: response.status }); Object.defineProperties(error, { providerEvidence: { value: saved }, ambiguous: { value: unknown } }); throw error; };
|
|
39
|
+
if (response.status >= 500) reject(codes.provider, "Volc server outcome is unknown", true);
|
|
40
|
+
if (response.status < 200 || response.status >= 300) reject([401, 403].includes(response.status) ? codes.auth : response.status === 429 ? codes.rateLimit : codes.provider, "Volc HTTP request rejected");
|
|
41
|
+
const error = saved.response?.ResponseMetadata?.Error;
|
|
42
|
+
if (error) reject(codes.provider, "Volc reported an explicit error", !/^(104\d\d|700429)$/.test(String(error.Code ?? error.CodeN)));
|
|
43
|
+
if (!validVolcResult(saved.response)) reject(codes.response, "Volc result is malformed or ambiguous", true);
|
|
44
|
+
return saved;
|
|
45
|
+
}, timeoutMs, codes.timeout, timeoutMessage);
|
|
46
|
+
} catch (error) { if (ownsError(error)) throw error; throw failure(codes.transport, "Volc transport outcome is unknown"); }
|
|
47
|
+
}
|
|
48
|
+
return { volcMaxCount, volcQueryRequest, legacyVolcQueryRequest, validateVolcCapability, volcWebSearch };
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: abelien-reference
|
|
3
|
+
description: Abelien 参考研究:为「还原现成产品」「创造新产品」「基于现成产品修改」三种意图收集并核验参考证据——识别产品型号、取得逆向资料(规格、拆解)、检索相似产品,抓取页面主图并测量用户原图的轮廓比例,产出可直接展示在画布网格的 reference-evidence。用于意图判定之后、外观与选型之前;不做选型、外观编辑或结构建模。
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility:
|
|
6
|
+
- claude-code
|
|
7
|
+
- hermes
|
|
8
|
+
- pi
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Abelien Reference
|
|
12
|
+
|
|
13
|
+
本包把「这是什么产品、有哪些可借鉴的东西」变成有来源、可核验的证据。收集由确定性代码完成,判断由 Agent 完成,判断再经确定性检查核验后才生效。意图的语义由 clarify 判定,调度由 guide 负责;本包不维护作品状态,结果交回 workspace,由宿主展示。
|
|
14
|
+
|
|
15
|
+
## 何时调用
|
|
16
|
+
|
|
17
|
+
- **还原现成产品(restore)与基于现成产品修改(modify)**:在需求登记之后、首张外观图之前调用,先识别产品,再取得规格与拆解资料。
|
|
18
|
+
- **创造新产品(create)**:在概念图之前调用,检索相似产品,作为尺寸、结构、器件和实现方式的参考。
|
|
19
|
+
- 首版之前不向用户提问:识别不确定时,按最可能的候选继续,同时保留其他候选;用户切换候选视为一次需求修订。
|
|
20
|
+
|
|
21
|
+
## 流程
|
|
22
|
+
|
|
23
|
+
1. **制定检索计划**:依据用户原话和 clarify 的意图判定,写出最多 6 条英文或中文检索,每条注明用途:`identify` 识别、`teardown` 逆向资料、`similar` 相似产品。用户给了链接时,链接优先读取。
|
|
24
|
+
2. **收集**:调用 `reference.research`。它执行 Volc 检索(每条最多 5 个结果)、对公开 HTTPS 页面做安全直读、抓取页面主图、测量用户原图(PNG)的轮廓高宽比,然后把证据保存为 `reference-evidence`(status=collected)。同一请求重复调用直接返回已有结果,不会重复产生检索费用。
|
|
25
|
+
3. **阅读证据并判断**:用 `ab resource get` 读取收集结果,用页面摘录(`excerpt`)和正文判断:哪个候选最可能是用户要的产品、它的尺寸是多少;哪些相似产品可以借用、借用什么、为什么、哪些不照搬;哪些条目应排除。
|
|
26
|
+
4. **核验**:调用 `reference.finalize` 提交判断,生成 status=final 的结果:
|
|
27
|
+
- 引用必须逐字出现在已保存的页面正文中;
|
|
28
|
+
- 尺寸数字必须出现在引用里;
|
|
29
|
+
- 比例判定由测量值和规格计算得出,不能由 Agent 自填。
|
|
30
|
+
|
|
31
|
+
所选候选比例不符时,结果会写明冲突,不会被隐藏。
|
|
32
|
+
5. **交回**:final 结果的 `items` 就是画布网格的格子,`unknowns` 是需要如实展示的缺口。宿主展示的是证据,不是作品已确认。
|
|
33
|
+
|
|
34
|
+
## 调用
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
AB=<仓库根>/skills/abelien-harness/scripts/ab.mjs
|
|
38
|
+
node "$AB" --workdir <作品目录> capability run reference.research --input-file research-input.json
|
|
39
|
+
node "$AB" --workdir <作品目录> resource get reference-evidence:<id>
|
|
40
|
+
node "$AB" --workdir <作品目录> capability run reference.finalize --input-file judgement.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
参考图(base64 PNG)会超过命令行长度限制,所以输入用 `--input-file` 传入,文件必须放在作品目录内。宿主负责把 JPEG、WebP 等格式先转成 PNG,本包只解码 PNG。字段、上限、错误码和判定规则见[参考研究契约](references/research-contract.md);数据格式见 `schemas/reference-evidence.v1.json`,样例见 `fixtures/`。
|
|
44
|
+
|
|
45
|
+
## 边界
|
|
46
|
+
|
|
47
|
+
- **不编造**:读不到的页面、打不开的链接、测量不了的图片,一律记为不可用或未知,并写入 `unknowns`;其他检索照常继续。
|
|
48
|
+
- **第三方图片只作参考**:保存时附带来源和哈希,不进入图鉴、不进入公开分享,也不当作作品资产。
|
|
49
|
+
- **找不到完整的相似产品时**,按声学、供电、交互等子系统分别参考,不硬凑数量。
|
|
50
|
+
- **来源产品的功能不等于本次要实现的功能**:还原时,功能按目标深度由 clarify 写入候选需求。
|
|
51
|
+
- **职责分界**:选型、外观编辑与结构建模分别由 hardware、appearance、structure 负责。来源 BOM 研究可以消费本包的证据,但 hardware 负责原件与候选件的对照。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "abelien-reference",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "参考研究域包:按 Agent 的检索计划收集产品识别、逆向资料与相似产品证据(检索、公开页面、主图、用户原图测量),再核验 Agent 的判断,产出 reference-evidence v1;判定码来自确定性检查",
|
|
5
|
+
"requires": [
|
|
6
|
+
"abelien-harness"
|
|
7
|
+
],
|
|
8
|
+
"resources": [
|
|
9
|
+
"reference-evidence"
|
|
10
|
+
],
|
|
11
|
+
"capabilities": {
|
|
12
|
+
"reference.research": {
|
|
13
|
+
"description": "执行检索计划并保存证据:Volc 检索(每次最多 5 条)、公开 HTTPS 页面安全直读、页面主图抓取、用户原图轮廓测量;同一请求重复运行直接返回已有结果",
|
|
14
|
+
"adapter": {
|
|
15
|
+
"type": "module",
|
|
16
|
+
"path": "scripts/research.mjs",
|
|
17
|
+
"export": "researchReference"
|
|
18
|
+
},
|
|
19
|
+
"input": {
|
|
20
|
+
"from": "input"
|
|
21
|
+
},
|
|
22
|
+
"resultContract": {
|
|
23
|
+
"nonEmptyStrings": [
|
|
24
|
+
"id",
|
|
25
|
+
"status",
|
|
26
|
+
"schema",
|
|
27
|
+
"request_digest"
|
|
28
|
+
],
|
|
29
|
+
"arrays": [
|
|
30
|
+
"searches",
|
|
31
|
+
"pages",
|
|
32
|
+
"images",
|
|
33
|
+
"items",
|
|
34
|
+
"unknowns"
|
|
35
|
+
],
|
|
36
|
+
"statusValues": [
|
|
37
|
+
"collected"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"produces": "reference-evidence",
|
|
41
|
+
"checkpoint": true,
|
|
42
|
+
"checkpointContract": {
|
|
43
|
+
"nonEmptyStrings": [
|
|
44
|
+
"id",
|
|
45
|
+
"status",
|
|
46
|
+
"schema",
|
|
47
|
+
"request_digest"
|
|
48
|
+
],
|
|
49
|
+
"arrays": [
|
|
50
|
+
"searches",
|
|
51
|
+
"pages",
|
|
52
|
+
"images",
|
|
53
|
+
"items",
|
|
54
|
+
"unknowns"
|
|
55
|
+
],
|
|
56
|
+
"statusValues": [
|
|
57
|
+
"collected"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"reference.finalize": {
|
|
62
|
+
"description": "核验 Agent 对已收集证据的识别、相似产品与排除判断:引用须逐字出现在已保存页面,尺寸数字须在引用中,比例判定由测量确定性计算",
|
|
63
|
+
"adapter": {
|
|
64
|
+
"type": "module",
|
|
65
|
+
"path": "scripts/finalize.mjs",
|
|
66
|
+
"export": "finalizeReference"
|
|
67
|
+
},
|
|
68
|
+
"input": {
|
|
69
|
+
"from": "input"
|
|
70
|
+
},
|
|
71
|
+
"resultContract": {
|
|
72
|
+
"nonEmptyStrings": [
|
|
73
|
+
"id",
|
|
74
|
+
"status",
|
|
75
|
+
"schema",
|
|
76
|
+
"collected_id"
|
|
77
|
+
],
|
|
78
|
+
"arrays": [
|
|
79
|
+
"items",
|
|
80
|
+
"similar",
|
|
81
|
+
"unknowns"
|
|
82
|
+
],
|
|
83
|
+
"statusValues": [
|
|
84
|
+
"final"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"produces": "reference-evidence"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"machines": {},
|
|
91
|
+
"adapters": {}
|
|
92
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"budget": {
|
|
3
|
+
"images": 0,
|
|
4
|
+
"pages": 1,
|
|
5
|
+
"queries": 1
|
|
6
|
+
},
|
|
7
|
+
"changeable": [
|
|
8
|
+
"外形",
|
|
9
|
+
"尺寸"
|
|
10
|
+
],
|
|
11
|
+
"depth": null,
|
|
12
|
+
"id": "ref-c26edd6284aed5b5-f89f84f29",
|
|
13
|
+
"images": [],
|
|
14
|
+
"intent": "create",
|
|
15
|
+
"items": [
|
|
16
|
+
{
|
|
17
|
+
"id": "u1",
|
|
18
|
+
"image": null,
|
|
19
|
+
"kind": "user_input",
|
|
20
|
+
"query": null,
|
|
21
|
+
"role": "做一个放在床头、能讲睡前故事的小音箱",
|
|
22
|
+
"source": null,
|
|
23
|
+
"status": "ready",
|
|
24
|
+
"title": "你的输入",
|
|
25
|
+
"verdict": null
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "p1",
|
|
29
|
+
"image": null,
|
|
30
|
+
"kind": "similar",
|
|
31
|
+
"query": "bedside story speaker teardown",
|
|
32
|
+
"role": "相似产品",
|
|
33
|
+
"source": {
|
|
34
|
+
"site": "shop.example.com",
|
|
35
|
+
"url": "https://shop.example.com/story-a"
|
|
36
|
+
},
|
|
37
|
+
"status": "ready",
|
|
38
|
+
"title": "床头故事机 A",
|
|
39
|
+
"verdict": {
|
|
40
|
+
"code": "borrowed",
|
|
41
|
+
"basis": "借用:300 mL 声腔与 40 mm 全频喇叭;原因:体积与床头摆放相近;不照搬:外观与按键布局"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"keep": [],
|
|
46
|
+
"pages": [
|
|
47
|
+
{
|
|
48
|
+
"excerpt": "床头故事机 A 腔体容积 300 mL,40 mm 全频喇叭,电池 2000 mAh。",
|
|
49
|
+
"final_url": "https://shop.example.com/story-a",
|
|
50
|
+
"id": "p1",
|
|
51
|
+
"image_url": null,
|
|
52
|
+
"purpose": "similar",
|
|
53
|
+
"raw_sha256": "db701b787482d7ae3c2967323df3013d93df7c7e628a801ab091218f55a1b87d",
|
|
54
|
+
"reason": null,
|
|
55
|
+
"search_id": "s1",
|
|
56
|
+
"status": "read",
|
|
57
|
+
"text_blob_ref": "sha256:a871926ca4bf9606ff9abc75f7b5da5a99a41c7535f03e9572095b1a272b32bc",
|
|
58
|
+
"text_sha256": "58a51339bbda6d0a69d726e2768faf47752f8529630a4705e5fe81552e187dff",
|
|
59
|
+
"title": "床头故事机 A",
|
|
60
|
+
"url": "https://shop.example.com/story-a"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"request_digest": "c26edd6284aed5b521396a1093bc0aeb9ff60d24f2c68d6bcf98a6529bd9c325",
|
|
64
|
+
"schema": "abelien.reference-evidence.v1",
|
|
65
|
+
"searches": [
|
|
66
|
+
{
|
|
67
|
+
"count": 3,
|
|
68
|
+
"error_code": null,
|
|
69
|
+
"id": "s1",
|
|
70
|
+
"purpose": "similar",
|
|
71
|
+
"query": "bedside story speaker teardown",
|
|
72
|
+
"receipt_sha256": "b4f07afb377bbf9556aef08b61cebb2bf0264df8924f91b712227186cd7157bc",
|
|
73
|
+
"request_id": "req-[redacted]",
|
|
74
|
+
"results": [
|
|
75
|
+
{
|
|
76
|
+
"rank": 1,
|
|
77
|
+
"snippet": "",
|
|
78
|
+
"title": "Story box A",
|
|
79
|
+
"url": "https://shop.example.com/story-a"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"status": "completed"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"status": "final",
|
|
86
|
+
"text": "做一个放在床头、能讲睡前故事的小音箱",
|
|
87
|
+
"unknowns": [],
|
|
88
|
+
"user_image": null,
|
|
89
|
+
"collected_id": "ref-c26edd6284aed5b5",
|
|
90
|
+
"judgement_digest": "89f84f293a6781fed8f80ea7f03d57da892c67f70bfdd836003421f88b2b0730",
|
|
91
|
+
"identity": null,
|
|
92
|
+
"similar": [
|
|
93
|
+
{
|
|
94
|
+
"item_id": "p1",
|
|
95
|
+
"decision": "borrow",
|
|
96
|
+
"borrowed": "300 mL 声腔与 40 mm 全频喇叭",
|
|
97
|
+
"why": "体积与床头摆放相近",
|
|
98
|
+
"not_copied": "外观与按键布局"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"spec_quotes": []
|
|
102
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ref-40ece5312c4ecbf6",
|
|
3
|
+
"schema": "abelien.reference-evidence.v1",
|
|
4
|
+
"status": "collected",
|
|
5
|
+
"request_digest": "40ece5312c4ecbf6f5548d36589de25734706b8a333d22e1a3c32adeb75e09f5",
|
|
6
|
+
"intent": "restore",
|
|
7
|
+
"keep": [
|
|
8
|
+
"外形尺寸与比例"
|
|
9
|
+
],
|
|
10
|
+
"changeable": [],
|
|
11
|
+
"depth": "assembly",
|
|
12
|
+
"text": "帮我根据图片还原小爱音箱",
|
|
13
|
+
"user_image": {
|
|
14
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
15
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11",
|
|
16
|
+
"media_type": "image/png",
|
|
17
|
+
"measurement": {
|
|
18
|
+
"status": "measured",
|
|
19
|
+
"method": "silhouette-v1",
|
|
20
|
+
"image": {
|
|
21
|
+
"width": 400,
|
|
22
|
+
"height": 600
|
|
23
|
+
},
|
|
24
|
+
"bbox": {
|
|
25
|
+
"x": 62,
|
|
26
|
+
"y": 49,
|
|
27
|
+
"width": 276,
|
|
28
|
+
"height": 502
|
|
29
|
+
},
|
|
30
|
+
"aspect_h_over_w": 1.8188,
|
|
31
|
+
"background": {
|
|
32
|
+
"mode": "color",
|
|
33
|
+
"rgb": [
|
|
34
|
+
255,
|
|
35
|
+
255,
|
|
36
|
+
255
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"searches": [
|
|
42
|
+
{
|
|
43
|
+
"id": "s1",
|
|
44
|
+
"purpose": "identify",
|
|
45
|
+
"query": "Xiaomi XiaoAI speaker Pro dimensions",
|
|
46
|
+
"count": 5,
|
|
47
|
+
"status": "completed",
|
|
48
|
+
"error_code": null,
|
|
49
|
+
"request_id": "req-[redacted]",
|
|
50
|
+
"receipt_sha256": "099206543e4389fde6c4838d3fc0b125dce0fb98cd57384db3062e1abcede7a8",
|
|
51
|
+
"results": [
|
|
52
|
+
{
|
|
53
|
+
"rank": 1,
|
|
54
|
+
"url": "https://www.mi.example.com/pro/specs",
|
|
55
|
+
"title": "XiaoAI Speaker Pro",
|
|
56
|
+
"snippet": ""
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"rank": 2,
|
|
60
|
+
"url": "https://www.mi.example.com/gen1/specs",
|
|
61
|
+
"title": "Mi AI Speaker",
|
|
62
|
+
"snippet": ""
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"pages": [
|
|
68
|
+
{
|
|
69
|
+
"id": "p1",
|
|
70
|
+
"url": "https://www.mi.example.com/pro/specs",
|
|
71
|
+
"purpose": "identify",
|
|
72
|
+
"search_id": "s1",
|
|
73
|
+
"status": "read",
|
|
74
|
+
"reason": null,
|
|
75
|
+
"final_url": "https://www.mi.example.com/pro/specs",
|
|
76
|
+
"title": "小爱音箱 Pro 参数",
|
|
77
|
+
"raw_sha256": "109cbac90dd51ef3dddfd21c4a97168a25f097b4211b2146d21669281d06e3b8",
|
|
78
|
+
"text_blob_ref": "sha256:a9d6a6d8668de3346efe1d94c1afd76e71faf37bee5a6e55d3a82f9bc60b9bf0",
|
|
79
|
+
"text_sha256": "bae09c2d8ac69290d90e7cf69d7809858b1149a391400d40030037db41896415",
|
|
80
|
+
"excerpt": "小爱音箱 Pro 参数 产品尺寸:118 × 118 × 215 mm,重量 1187 g。",
|
|
81
|
+
"image_url": "https://img.example.com/pro.png"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "p2",
|
|
85
|
+
"url": "https://www.mi.example.com/gen1/specs",
|
|
86
|
+
"purpose": "identify",
|
|
87
|
+
"search_id": "s1",
|
|
88
|
+
"status": "read",
|
|
89
|
+
"reason": null,
|
|
90
|
+
"final_url": "https://www.mi.example.com/gen1/specs",
|
|
91
|
+
"title": "小米AI音箱 规格",
|
|
92
|
+
"raw_sha256": "c139f165c41dc34c2cd30a816a80d7cbef9ecbda49e7496d46163c8692da33c7",
|
|
93
|
+
"text_blob_ref": "sha256:3dd43ad1e906a2e95af747cc8f8386c00b43df981af8b8bafe45bb2d173e7e3c",
|
|
94
|
+
"text_sha256": "2de9e3c9406bd62a2e9eef887fd42008382616377ac49fb751cd5a109f1e9a00",
|
|
95
|
+
"excerpt": "小米AI音箱 规格 尺寸 88 mm × 88 mm × 211.6 mm",
|
|
96
|
+
"image_url": null
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"images": [
|
|
100
|
+
{
|
|
101
|
+
"id": "i1",
|
|
102
|
+
"url": "https://img.example.com/pro.png",
|
|
103
|
+
"page_id": "p1",
|
|
104
|
+
"status": "read",
|
|
105
|
+
"reason": null,
|
|
106
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
107
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11",
|
|
108
|
+
"media_type": "image/png",
|
|
109
|
+
"measurement": {
|
|
110
|
+
"status": "measured",
|
|
111
|
+
"method": "silhouette-v1",
|
|
112
|
+
"image": {
|
|
113
|
+
"width": 400,
|
|
114
|
+
"height": 600
|
|
115
|
+
},
|
|
116
|
+
"bbox": {
|
|
117
|
+
"x": 62,
|
|
118
|
+
"y": 49,
|
|
119
|
+
"width": 276,
|
|
120
|
+
"height": 502
|
|
121
|
+
},
|
|
122
|
+
"aspect_h_over_w": 1.8188,
|
|
123
|
+
"background": {
|
|
124
|
+
"mode": "color",
|
|
125
|
+
"rgb": [
|
|
126
|
+
255,
|
|
127
|
+
255,
|
|
128
|
+
255
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"items": [
|
|
135
|
+
{
|
|
136
|
+
"id": "u1",
|
|
137
|
+
"kind": "user_input",
|
|
138
|
+
"title": "你的输入",
|
|
139
|
+
"role": "帮我根据图片还原小爱音箱",
|
|
140
|
+
"source": null,
|
|
141
|
+
"image": {
|
|
142
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
143
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11",
|
|
144
|
+
"media_type": "image/png"
|
|
145
|
+
},
|
|
146
|
+
"verdict": null,
|
|
147
|
+
"status": "ready",
|
|
148
|
+
"query": null
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "p1",
|
|
152
|
+
"kind": "candidate",
|
|
153
|
+
"title": "小爱音箱 Pro 参数",
|
|
154
|
+
"role": "候选产品",
|
|
155
|
+
"source": {
|
|
156
|
+
"url": "https://www.mi.example.com/pro/specs",
|
|
157
|
+
"site": "www.mi.example.com"
|
|
158
|
+
},
|
|
159
|
+
"image": {
|
|
160
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
161
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11",
|
|
162
|
+
"media_type": "image/png"
|
|
163
|
+
},
|
|
164
|
+
"verdict": null,
|
|
165
|
+
"status": "ready",
|
|
166
|
+
"query": "Xiaomi XiaoAI speaker Pro dimensions"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "p2",
|
|
170
|
+
"kind": "candidate",
|
|
171
|
+
"title": "小米AI音箱 规格",
|
|
172
|
+
"role": "候选产品",
|
|
173
|
+
"source": {
|
|
174
|
+
"url": "https://www.mi.example.com/gen1/specs",
|
|
175
|
+
"site": "www.mi.example.com"
|
|
176
|
+
},
|
|
177
|
+
"image": null,
|
|
178
|
+
"verdict": null,
|
|
179
|
+
"status": "ready",
|
|
180
|
+
"query": "Xiaomi XiaoAI speaker Pro dimensions"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"unknowns": [],
|
|
184
|
+
"budget": {
|
|
185
|
+
"queries": 1,
|
|
186
|
+
"pages": 2,
|
|
187
|
+
"images": 1
|
|
188
|
+
}
|
|
189
|
+
}
|