abelien-harness 0.11.0 → 0.13.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 +7 -0
- package/package.json +1 -1
- package/skills/abelien-clarify/SKILL.md +1 -1
- package/skills/abelien-hardware/domain.json +1 -1
- package/skills/abelien-hardware/references/selection-service-contract.md +1 -1
- package/skills/abelien-hardware/references/web-evidence-contract.md +1 -1
- package/skills/abelien-hardware/scripts/admission.mjs +149 -0
- package/skills/abelien-hardware/scripts/dimensions.mjs +6 -0
- package/skills/abelien-hardware/scripts/finalize-selection.mjs +6 -2
- package/skills/abelien-hardware-selection/SKILL.md +1 -1
- package/skills/abelien-hardware-selection/domain.json +3 -3
- package/skills/abelien-hardware-selection/references/selection-service-contract.md +12 -0
- package/skills/abelien-hardware-selection/scripts/select.mjs +9 -0
- package/skills/abelien-harness/scripts/ab.mjs +1 -1
- package/skills/abelien-workspace/SKILL.md +1 -1
- package/skills/abelien-workspace/assets/journey-stage-plan.json +87 -18
- package/skills/abelien-workspace/domain.json +3 -2
- package/skills/abelien-workspace/references/journey-stage-plan.md +48 -13
- package/skills/abelien-workspace/references/user-journey.md +1 -1
- package/skills/abelien-workspace/scripts/journey-next.mjs +14 -0
package/.env.example
CHANGED
|
@@ -25,6 +25,13 @@ OSS_ALLOW_LEGACY_MANIFEST_FALLBACK=false
|
|
|
25
25
|
# Optional external hardware revision service
|
|
26
26
|
ABELIEN_HARDWARE_REVISE_URL=
|
|
27
27
|
|
|
28
|
+
# Optional component admission (P2-05): a selected web part enters the component library before the
|
|
29
|
+
# selection is saved. Set URL and token together (HTTPS, or HTTP on loopback; token at least 32 chars).
|
|
30
|
+
# Leave both empty to keep web parts under their external: ID with an EXTERNAL_NOT_ADMITTED unknown.
|
|
31
|
+
ABELIEN_HARDWARE_ADMISSION_URL=
|
|
32
|
+
ABELIEN_HARDWARE_ADMISSION_TOKEN=
|
|
33
|
+
ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS=30000
|
|
34
|
+
|
|
28
35
|
# Optional loopback HTTP wrapper only; main hardware.select executes locally without a URL.
|
|
29
36
|
# At least 32 bytes, keep the real token in a private env file. Do not expose this server publicly.
|
|
30
37
|
ABELIEN_HARDWARE_SELECT_TOKEN=
|
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@ ID 是不透明标识,不能按数组位置重新编号;新 ID 不得与现
|
|
|
82
82
|
|
|
83
83
|
**意图与目标深度。** 用户给出图片、链接或产品名,或说“还原/复刻/同款/拆解/这款”时,先按原话判定本次意图:还原现成产品(restore)、创造新产品(create)或基于现成产品修改(modify)。判定依据是原话,不是输入形式:商品图配“参考这个风格做一个”是 create;“参考某产品,再加一个屏幕”是 modify。判定结果写入 `result.intent`:`kind` 为意图;`keep` 列出保留的来源产品特征(restore 默认含外形尺寸与比例,modify 逐项列出,create 没有来源产品,必须为空);`changeable` 列出允许重新设计的特征;`depth` 为还原或修改的目标深度(appearance 外观模型、assembly 可装配结构、functional 功能样机),原话推断不出时省略。原话直接说明意图(如“还原这款”“参考它再加一个屏幕”“做一个新的”),或者根本没有提到任何现成产品时,`source` 记 user;需要从图片、链接或措辞推断时记 system,并在 note 写明依据。宿主按 `intent.kind` 安排旅程,参考研究的输入也从这里取;它不单独构成首版之前的问题,以系统假设展示,用户随时可改,改动就是一次需求修订。`intent` 是需求的一部分,硬件召回与联网证据的 `request_digest` 绑定整份需求,所以只改 `intent`(包括 `keep`、`changeable`、`depth`)也会让进行中的召回与证据失效,须重新检索并再次产生检索费用;不要为了省检索而把意图改动挪到需求之外。来源产品的功能只能以 source=system 写入 reaction,并注明来自来源产品;“原产品有这个功能”不等于“本次要实现”,按目标深度决定是否纳入,外观模型不纳入语音等功能实现。
|
|
84
84
|
|
|
85
|
-
**提问时机(2026-10-12 裁定)。** 首版 CAD 之前默认不向用户提问:按原话和合理默认继续,按默认处理的事项登记为系统假设或待确认项,由宿主展示、用户可随时修改;首版之后,把仍按默认处理的事项合并成一张问题卡。唯一例外:用户提出的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如“必须放进这个卡槽”却没给尺寸)时,可以在首版之前提问,至多一张卡,沿用问题卡上限(最多三题、每题最多三个选项)。调用方说明首版是否已形成;未说明时按首版之前处理。首版之前返回的 questions 只能属于这一例外,其余未定项按默认写入 result(source=system)并在 note
|
|
85
|
+
**提问时机(2026-10-12 裁定)。** 首版 CAD 之前默认不向用户提问:按原话和合理默认继续,按默认处理的事项登记为系统假设或待确认项,由宿主展示、用户可随时修改;首版之后,把仍按默认处理的事项合并成一张问题卡。唯一例外:用户提出的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如“必须放进这个卡槽”却没给尺寸)时,可以在首版之前提问,至多一张卡,沿用问题卡上限(最多三题、每题最多三个选项)。调用方说明首版是否已形成;未说明时按首版之前处理。首版之前返回的 questions 只能属于这一例外,其余未定项按默认写入 result(source=system)并在 note 说明,留到首版之后合并询问。意图、目标深度与型号识别不属于例外。确认卡(2026-10-14 决定)不属于这里的提问:它由宿主在识别、找参考、概念图、选硬件这几个环节结束后按阶段计划的 gate 固定发出,问题与选项事先写死(继续 / 重新生成 / 改需求),首版之前也会出现;卡片不等人,只有阶段计划 waits_for 声明要用这个结果的阶段才停下等答复。首版之后的合并卡第一题固定问“这一版怎么样?”。用户在确认卡上选了改需求的选项(换候选、换参考、调整方向、换零件、改要求)时,按普通需求修改处理。
|
|
86
86
|
|
|
87
87
|
只提出现在必须由用户决定、且符合提问时机的问题:闭环歧义、落点歧义或挡住当前选择的硬要求缺值/相互矛盾;缺少 PRD 字段本身不是问题。提问前能在 note 中说明:不回答会卡住哪个选择、为何现有事实和授权不足以代定。默认一题,相互独立才合并,最多三题;每题最多三个实质选项,表达遵循已注入的共用规则。用户意图选项可以来自原话支持的不同解释,不冒充已有硬件或工程可行候选。
|
|
88
88
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-hardware",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Hardware library recall, English Volc query and source evidence (Prismer off by default) with envelope candidates; formal selection remains hardware.select",
|
|
5
5
|
"resources": [
|
|
6
6
|
"hardware-recall",
|
|
@@ -22,7 +22,7 @@ URL 来自 `ABELIEN_HARDWARE_REVISE_URL`。请求包含 `operation: "revise_exis
|
|
|
22
22
|
|
|
23
23
|
## Supplemental source binding
|
|
24
24
|
|
|
25
|
-
For new journeys, `hardware.web-search` and `hardware.web-merge` follow [web-evidence-contract](web-evidence-contract.md). Proposal-only finalize consumes the merged recall; formal selection uses its persisted ID and revalidates original recall/evidence. Finalize now keeps only model assessment fields from the proposal and uses candidate facts for manufacturer/model/classification/source/geometry/CAD. No self-reported source or CAD claim can override an external candidate.
|
|
25
|
+
For new journeys, `hardware.web-search` and `hardware.web-merge` follow [web-evidence-contract](web-evidence-contract.md). Proposal-only finalize consumes the merged recall; formal selection uses its persisted ID and revalidates original recall/evidence. Finalize now keeps only model assessment fields from the proposal and uses candidate facts for manufacturer/model/classification/source/geometry/CAD. No self-reported source or CAD claim can override an external candidate. Proposal-only finalize never admits anything into the component library; only formal `hardware.select` does, and only for selected parts (「联网件入组件库」 in the [local selection contract](../../abelien-hardware-selection/references/selection-service-contract.md)).
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
Dimension metadata is copied only from frozen candidates, with deterministic single-part screening shared by search/finalize/select. Name comes from the candidate title; `bom_item_id` is explicitly a selection-line identity, not a catalog BOM identity. Active envelope output may add advisory occupancy. The [local selection contract](../../abelien-hardware-selection/references/selection-service-contract.md#结构化尺寸与正式门禁) defines unknown/pending behavior and the [index contract](search-index-format.md#数据库尺寸快照-v1) defines database provenance. Finalize never upgrades its proposed result to formal verified authority.
|
|
@@ -20,7 +20,7 @@ After original source/task/hash/monotonic checks, an already saved kind=result o
|
|
|
20
20
|
|
|
21
21
|
hardware.web-merge input is exactly {requirements,slots,recall_id,evidence_id,candidates}. Each candidate is {slot_id,page_id,manufacturer,manufacturer_quote,model,model_quote,parts}. Each physical part is {name,name_quote,shape,shape_quote,dimensions_mm,dimension_quotes?}, optionally dimensions_quote/dimension_axes. All claims match persisted raw/Parse source text, including its original provider response. Manufacturer/model quotes have at least eight characters and contain the literal. Shape is box/cylinder/sphere/unknown; one attributed positive finite measurement/unit per axis, or an exact attributed tuple with explicit original-order axes. No values borrowed from another part/imperial alternative. Missing coil thickness, source conflicts, OCR/tolerance/shape/orientation and assembly stay unknown; box only represents the dimensional envelope.
|
|
22
22
|
|
|
23
|
-
The merged hardware-recall retains original library candidates/unmet requirements, source IDs/digests and web_extractions for formal deterministic rebuilding. external:<hash> is separate from catalog IDs. Wiki object_key/relative_path/sha256 explicitly represent HTTPS document provenance. CAD_available and engineering_verified remain false. The same formal hardware.select covers quantity/all constraints/compatibility and emits ten root fields with external needs_confirmation; proposal fields cannot override candidate source/geometry/model. No verified/adopted/delivered or true CAD claim follows from a document parse.
|
|
23
|
+
The merged hardware-recall retains original library candidates/unmet requirements, source IDs/digests and web_extractions for formal deterministic rebuilding. external:<hash> is separate from catalog IDs. Wiki object_key/relative_path/sha256 explicitly represent HTTPS document provenance. CAD_available and engineering_verified remain false. The same formal hardware.select covers quantity/all constraints/compatibility and emits ten root fields with external needs_confirmation; proposal fields cannot override candidate source/geometry/model. No verified/adopted/delivered or true CAD claim follows from a document parse. When the host configures component admission, `hardware.select` admits each *selected* external part into the component library and saves it under the library UUID (`source_type=admitted_web`, provenance unchanged, size as `estimate` + `bounding_box` only for one fully quoted part); unselected candidates keep their `external:` identity. See 「联网件入组件库」 in the [selection contract](../../abelien-hardware-selection/references/selection-service-contract.md).
|
|
24
24
|
|
|
25
25
|
Configuration: PRISMER_API_KEY; WEB_SEARCH_API_KEY; ABELIEN_HARDWARE_WEB_SEARCH_TIMEOUT_MS=30000; ABELIEN_HARDWARE_WEB_PAGE_TIMEOUT_MS=30000; ABELIEN_HARDWARE_WEB_PARSE_TIMEOUT_MS=120000; ABELIEN_HARDWARE_WEB_DNS_TRANSPORT=system|doh. The submitted public URL/DNS check does not certify remote Prismer redirects. Authorized direct reads recheck all answers/redirects and pin original-hostname TLS to the chosen public IP. HTTP errors, success=false/error, malformed JSON, raw missing and timeout are explicit evidence/unknowns, never empty successful facts. Secret echoes are redacted; headers are never stored.
|
|
26
26
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Selected web parts enter the component library before their selection is saved (P2-05, 2026-10-14).
|
|
2
|
+
// The component database issues the ID: this module builds the request from source-bound web evidence,
|
|
3
|
+
// calls the host's admission endpoint, checks the reply, and rewrites the selected rows to the library ID.
|
|
4
|
+
// It never invents dimensions: only one fully quoted, unconflicted part becomes an outer bounding box.
|
|
5
|
+
import { assemblyOccupancy, dimensionRevision, evaluateDimensions } from "./dimensions.mjs";
|
|
6
|
+
import { selectionLineId } from "./finalize-selection.mjs";
|
|
7
|
+
import { digest } from "./selection-request.mjs";
|
|
8
|
+
|
|
9
|
+
export const ADMISSION_REQUEST_SCHEMA = "abelien.hardware-admission-request.v1";
|
|
10
|
+
export const ADMISSION_RESPONSE_SCHEMA = "abelien.hardware-admission-response.v1";
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
13
|
+
// Provenance stays as recorded on the web candidate; only identity references move to the library ID.
|
|
14
|
+
const PROVENANCE_KEYS = new Set(["source", "geometry", "wiki", "admission"]);
|
|
15
|
+
const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
const text = value => typeof value === "string" && value.trim().length > 0;
|
|
17
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
18
|
+
const UNKNOWN_SIZE = Object.freeze({ dimensions_mm: null, dimension_basis: "unknown", dimension_geometry: "unknown" });
|
|
19
|
+
|
|
20
|
+
// A third-party page of unknown authority gives an estimate of the outer box, never a datasheet fact.
|
|
21
|
+
export function admissionEnvelope(geometry) {
|
|
22
|
+
const parts = Array.isArray(geometry?.parts) ? geometry.parts : [];
|
|
23
|
+
if (parts.length !== 1 || parts[0]?.status !== "source_dimensions" || !record(parts[0].dimensions_mm)) return { ...UNKNOWN_SIZE };
|
|
24
|
+
const { shape, dimensions_mm: size } = parts[0];
|
|
25
|
+
const box = shape === "box" ? [size.length, size.width, size.height]
|
|
26
|
+
: shape === "cylinder" ? [size.diameter, size.diameter, size.height]
|
|
27
|
+
: shape === "sphere" ? [size.diameter, size.diameter, size.diameter] : null;
|
|
28
|
+
if (!box || !box.every(value => typeof value === "number" && Number.isFinite(value) && value > 0)) return { ...UNKNOWN_SIZE };
|
|
29
|
+
return { dimensions_mm: box, dimension_basis: "estimate", dimension_geometry: "bounding_box" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function admissionItem(model) {
|
|
33
|
+
const source = model?.source, wiki = model?.wiki;
|
|
34
|
+
if (model?.source_type !== "external_web" || !String(model.component_id).startsWith("external:") || !record(source)
|
|
35
|
+
|| !text(source.evidence_id) || !text(source.page_id) || !HEX64.test(wiki?.sha256 ?? ""))
|
|
36
|
+
fail("VALIDATION", "only a source-bound external web selection can be admitted");
|
|
37
|
+
const [part] = model.geometry?.parts?.length === 1 ? model.geometry.parts : [];
|
|
38
|
+
return {
|
|
39
|
+
external_id: model.component_id, slot_id: model.slot_id,
|
|
40
|
+
manufacturer: model.manufacturer, model: model.model, category_path: [...model.category_path],
|
|
41
|
+
...admissionEnvelope(model.geometry),
|
|
42
|
+
evidence: {
|
|
43
|
+
// The page text the Agent quoted from, as persisted with the web evidence.
|
|
44
|
+
evidence_ref: `web-evidence:${source.evidence_id}/${source.page_id}/sha256/${wiki.sha256}`,
|
|
45
|
+
evidence_sha256: wiki.sha256,
|
|
46
|
+
source_url: source.source_url ?? null, url: source.url ?? null, retrieval: source.retrieval ?? null,
|
|
47
|
+
manufacturer_quote: source.manufacturer_quote, model_quote: source.model_quote,
|
|
48
|
+
part: part ? { name: part.name, shape: part.shape, dimensions_mm: part.dimensions_mm,
|
|
49
|
+
...(part.dimension_quotes ? { dimension_quotes: part.dimension_quotes } : {}),
|
|
50
|
+
...(part.dimension_tuple ? { dimension_tuple: part.dimension_tuple } : {}) } : null,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function admissionTarget(env) {
|
|
56
|
+
const url = env?.ABELIEN_HARDWARE_ADMISSION_URL, token = env?.ABELIEN_HARDWARE_ADMISSION_TOKEN;
|
|
57
|
+
if (!text(url) && !text(token)) return null;
|
|
58
|
+
if (!text(url) || !text(token) || token.length < 32) fail("ADMISSION_UNAVAILABLE", "ABELIEN_HARDWARE_ADMISSION_URL and a token of at least 32 characters must be set together");
|
|
59
|
+
let target;
|
|
60
|
+
try { target = new URL(url); } catch { fail("ADMISSION_UNAVAILABLE", "ABELIEN_HARDWARE_ADMISSION_URL is not a URL"); }
|
|
61
|
+
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(target.hostname);
|
|
62
|
+
if (target.username || target.password || !(target.protocol === "https:" || (target.protocol === "http:" && loopback)))
|
|
63
|
+
fail("ADMISSION_UNAVAILABLE", "the admission endpoint must be HTTPS, or HTTP on loopback, without credentials in the URL");
|
|
64
|
+
const timeout = Number(env.ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS ?? 30000);
|
|
65
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1000 || timeout > 120000) fail("ADMISSION_UNAVAILABLE", "ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS must be 1000-120000");
|
|
66
|
+
return { target, token, timeout };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseAdmissionResponse(reply, items) {
|
|
70
|
+
const invalid = message => fail("ADMISSION_RESPONSE_INVALID", message);
|
|
71
|
+
if (!record(reply) || reply.schema !== ADMISSION_RESPONSE_SCHEMA || !Array.isArray(reply.items) || reply.items.length !== items.length)
|
|
72
|
+
invalid("admission reply must answer every requested item once");
|
|
73
|
+
const requested = new Map(items.map(item => [item.external_id, item])), admitted = new Map();
|
|
74
|
+
for (const row of reply.items) {
|
|
75
|
+
const asked = requested.get(row?.external_id);
|
|
76
|
+
if (!asked || admitted.has(row.external_id)) invalid("admission reply names an unrequested or repeated part");
|
|
77
|
+
if (!UUID.test(row.component_id ?? "") || !text(row.urn) || !row.urn.startsWith("urn:abelien:component:")
|
|
78
|
+
|| !["created", "existing"].includes(row.outcome) || (row.evidence_ref !== undefined && !text(row.evidence_ref)))
|
|
79
|
+
invalid("admission reply needs a library UUID, URN and outcome");
|
|
80
|
+
const size = { component_id: row.component_id, dimensions_mm: row.dimensions_mm ?? null, dimension_basis: row.dimension_basis,
|
|
81
|
+
dimension_geometry: row.dimension_geometry, ...(row.evidence_ref !== undefined ? { evidence_ref: row.evidence_ref } : {}) };
|
|
82
|
+
try { dimensionRevision(size); } catch { invalid("admission reply carries an invalid dimension record"); }
|
|
83
|
+
// A new library row stores exactly what was sent; an existing row keeps the library's own facts.
|
|
84
|
+
if (row.outcome === "created" && (JSON.stringify(size.dimensions_mm) !== JSON.stringify(asked.dimensions_mm)
|
|
85
|
+
|| size.dimension_basis !== asked.dimension_basis || size.dimension_geometry !== asked.dimension_geometry
|
|
86
|
+
|| (asked.dimensions_mm !== null && size.evidence_ref !== asked.evidence.evidence_ref)))
|
|
87
|
+
invalid("a created library part must store the requested envelope and evidence");
|
|
88
|
+
// Two pages of one product may resolve to the same library part; each slot still keeps its own selection line.
|
|
89
|
+
admitted.set(row.external_id, { ...size, urn: row.urn, outcome: row.outcome });
|
|
90
|
+
}
|
|
91
|
+
return admitted;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function remap(value, ids, key) {
|
|
95
|
+
if (key !== undefined && PROVENANCE_KEYS.has(key)) return value;
|
|
96
|
+
if (typeof value === "string") return ids.get(value) ?? value;
|
|
97
|
+
if (Array.isArray(value)) return value.map(item => remap(item, ids));
|
|
98
|
+
if (record(value)) return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, remap(item, ids, name)]));
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function applyAdmission(selection, admitted, { requirements, slots }) {
|
|
103
|
+
const ids = new Map(), issues = [];
|
|
104
|
+
const models = selection.selected_models.map(model => {
|
|
105
|
+
const row = model.source_type === "external_web" ? admitted.get(model.component_id) : undefined;
|
|
106
|
+
if (!row) return model;
|
|
107
|
+
const bom = selectionLineId(model.slot_id, row.component_id);
|
|
108
|
+
ids.set(model.component_id, row.component_id); ids.set(model.bom_item_id, bom);
|
|
109
|
+
const evidenceRef = row.evidence_ref !== undefined ? { evidence_ref: row.evidence_ref } : {};
|
|
110
|
+
const source = { component_id: row.component_id, revision: dimensionRevision(row),
|
|
111
|
+
snapshot_id: digest({ schema: ADMISSION_RESPONSE_SCHEMA, admitted: row }), ...evidenceRef };
|
|
112
|
+
const next = { ...model, component_id: row.component_id, bom_item_id: bom, resource_urn: row.urn,
|
|
113
|
+
dimensions_mm: row.dimensions_mm, dimension_basis: row.dimension_basis, dimension_geometry: row.dimension_geometry,
|
|
114
|
+
dimension_source: source, source_type: "admitted_web", admission: { external_component_id: model.component_id, outcome: row.outcome } };
|
|
115
|
+
const slot = slots.find(item => item.slot_id === model.slot_id);
|
|
116
|
+
next.dimension_check = evaluateDimensions(next, requirements, slot);
|
|
117
|
+
// An existing library row may carry trusted sizes the web page did not: they can now prove a misfit.
|
|
118
|
+
if (next.dimension_check.status === "incompatible") fail("VALIDATION", "the admitted library part is dimensionally incompatible; choose another candidate");
|
|
119
|
+
if (next.dimension_check.decision_required) issues.push({ code: "DIMENSION_DECISION_REQUIRED", slot_id: slot.slot_id, component_id: row.component_id, dimension_check: next.dimension_check });
|
|
120
|
+
return next;
|
|
121
|
+
});
|
|
122
|
+
const { selected_models: _models, occupancy, ...rest } = selection;
|
|
123
|
+
const rewritten = remap(rest, ids);
|
|
124
|
+
return { ...rewritten, selected_models: models.map(model => remap(model, ids)),
|
|
125
|
+
...(occupancy !== undefined ? { occupancy: assemblyOccupancy(models, requirements) } : {}),
|
|
126
|
+
unknowns: [...rewritten.unknowns, ...issues] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Hosts that configure admission must admit every selected web part; hosts without it keep the web ID and say so.
|
|
130
|
+
export async function admitSelectedHardware({ selection, requirements, slots, env, fetchImpl = fetch }) {
|
|
131
|
+
const external = selection.selected_models.filter(model => model.source_type === "external_web");
|
|
132
|
+
if (!external.length) return selection;
|
|
133
|
+
const endpoint = admissionTarget(env);
|
|
134
|
+
if (!endpoint) return { ...selection, unknowns: [...selection.unknowns,
|
|
135
|
+
...external.map(model => ({ code: "EXTERNAL_NOT_ADMITTED", slot_id: model.slot_id, component_id: model.component_id }))] };
|
|
136
|
+
const items = external.map(admissionItem);
|
|
137
|
+
let response, reply;
|
|
138
|
+
try {
|
|
139
|
+
response = await fetchImpl(endpoint.target.href, { method: "POST", redirect: "error",
|
|
140
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${endpoint.token}` },
|
|
141
|
+
body: JSON.stringify({ schema: ADMISSION_REQUEST_SCHEMA, selection_id: selection.id, items }),
|
|
142
|
+
signal: AbortSignal.timeout(endpoint.timeout) });
|
|
143
|
+
reply = await response.json();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
fail("ADMISSION_FAILED", `component admission request failed (${error?.name === "TimeoutError" ? "timeout" : "network or reply"})`);
|
|
146
|
+
}
|
|
147
|
+
if (!response.ok) fail("ADMISSION_FAILED", `component admission returned HTTP ${response.status}${text(reply?.error?.code) ? ` ${reply.error.code}` : ""}`);
|
|
148
|
+
return applyAdmission(selection, parseAdmissionResponse(reply, items), { requirements, slots });
|
|
149
|
+
}
|
|
@@ -59,6 +59,12 @@ function validRecord(value) {
|
|
|
59
59
|
&& (value.evidence_ref === undefined || text(value.evidence_ref))
|
|
60
60
|
&& (value.dimensions_mm !== null || (value.dimension_basis === 'unknown' && value.dimension_geometry === 'unknown'));
|
|
61
61
|
}
|
|
62
|
+
// Same revision the component database export writes, for a record that did not come from a snapshot file.
|
|
63
|
+
export function dimensionRevision(value) {
|
|
64
|
+
const record = dimensionRecord(value);
|
|
65
|
+
if (!validRecord(record)) invalid('component dimension record is invalid');
|
|
66
|
+
return digest(record);
|
|
67
|
+
}
|
|
62
68
|
export function validateComponentSnapshot(snapshot) {
|
|
63
69
|
keys(snapshot, ['schema', 'snapshot_id', 'records'], [], 'component snapshot');
|
|
64
70
|
if (snapshot.schema !== 'abelien.component-dimensions.v1' || !Array.isArray(snapshot.records) || !hash(snapshot.snapshot_id)) invalid('unsupported component dimension snapshot');
|
|
@@ -14,6 +14,11 @@ function digest(value) {
|
|
|
14
14
|
return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// A deterministic selection-line identity, NOT an upstream catalog BOM identity.
|
|
18
|
+
export function selectionLineId(slotId, componentId) {
|
|
19
|
+
return `selection-line-${digest({ slot_id: slotId, component_id: componentId }).slice(0, 24)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
function requireArray(value, name) {
|
|
18
23
|
if (!Array.isArray(value)) throw error(`selection_proposal.${name} must be an array`);
|
|
19
24
|
return value;
|
|
@@ -79,8 +84,7 @@ export async function finalizeHardwareSelection({ input }) {
|
|
|
79
84
|
if (dimensionCheck.status === "incompatible") throw error("selected component is dimensionally incompatible; choose a surviving candidate", { slot_id: selected.slot_id, component_id: selected.component_id, dimension_check: dimensionCheck });
|
|
80
85
|
return {
|
|
81
86
|
name: String(candidate.title ?? candidate.name ?? candidate.model ?? candidate.component_id),
|
|
82
|
-
|
|
83
|
-
bom_item_id: `selection-line-${digest({ slot_id: selected.slot_id, component_id: candidate.component_id }).slice(0, 24)}`,
|
|
87
|
+
bom_item_id: selectionLineId(selected.slot_id, candidate.component_id),
|
|
84
88
|
...dimensionFields(candidate), dimension_check: dimensionCheck, ...electricalFields(candidate),
|
|
85
89
|
...(candidate.interfaces !== undefined ? { interfaces: candidate.interfaces } : {}),
|
|
86
90
|
slot_id: selected.slot_id, component_id: selected.component_id, quantity: selected.quantity,
|
|
@@ -24,6 +24,6 @@ compatibility:
|
|
|
24
24
|
|
|
25
25
|
## Source-bound online fallback
|
|
26
26
|
|
|
27
|
-
The new hardware journeys review library suitability, supplement unresolved slots with [web evidence](../abelien-hardware/references/web-evidence-contract.md), then consume the merged recall through this same `hardware.select`. Select rereads original recall and web evidence and rebuilds the merge; its trusted reader must resolve all three current-workdir resources. External identity is `external:<hash>` with envelope-only geometry and source/engineering unknowns. All quantities, hard constraints and compatibility checks remain complete; an external choice stays `needs_confirmation`. The old fixed 256 KiB input gate is removed without relaxing shape or provenance.
|
|
27
|
+
The new hardware journeys review library suitability, supplement unresolved slots with [web evidence](../abelien-hardware/references/web-evidence-contract.md), then consume the merged recall through this same `hardware.select`. Select rereads original recall and web evidence and rebuilds the merge; its trusted reader must resolve all three current-workdir resources. External identity is `external:<hash>` with envelope-only geometry and source/engineering unknowns. All quantities, hard constraints and compatibility checks remain complete; an external choice stays `needs_confirmation`. When the host configures component admission, a selected external part enters the component library first and is saved under its library UUID (`source_type=admitted_web`); see 「联网件入组件库」 in the [selection contract](references/selection-service-contract.md). The old fixed 256 KiB input gate is removed without relaxing shape or provenance.
|
|
28
28
|
|
|
29
29
|
新运行显式开启 `AB_HARNESS_AUDIT_V1=true` 后,正式选型的 needs_confirmation 可进入 selected_pending_confirmation 并结束本次选型,仍待用户确认、不执行算法。普通关闭开关的运行保留原 verified-only 门禁;安装前无摘要/新格式标记的硬件运行保持只读,不能补造摘要续跑。网页页引用通过 `resource get <evidenceId> --page <pageId>` 读取所需完整原文,再做语义判断,不能把整页抄进回复。接口与迁移见 [选型服务契约](references/selection-service-contract.md)。
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-hardware-selection",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "本地硬件选型:真实 OSS 召回、Agent 证据判断与受信资源校验;不是数据库兼容性求解器",
|
|
5
5
|
"resources": ["hardware-selection"],
|
|
6
6
|
"requires": ["abelien-harness", "abelien-hardware"],
|
|
7
7
|
"capabilities": {
|
|
8
8
|
"hardware.select": {
|
|
9
|
-
"description": "回读同需求的真实召回资源,验证 Agent 选择的型号、数量、逐约束引用与兼容评估(电源与接口可由可信电气数据确定性判定,其未知仍阻断;结构/散热/软件只提示并列入 advisories
|
|
9
|
+
"description": "回读同需求的真实召回资源,验证 Agent 选择的型号、数量、逐约束引用与兼容评估(电源与接口可由可信电气数据确定性判定,其未知仍阻断;结构/散热/软件只提示并列入 advisories),保存选型、可选尺寸占用摘要与兼容报告;宿主配置了组件入库端点时,选中的联网件先入组件库、以库内 ID 保存",
|
|
10
10
|
"adapter": {
|
|
11
11
|
"type": "module",
|
|
12
12
|
"path": "scripts/select.mjs",
|
|
13
|
-
"export": "
|
|
13
|
+
"export": "selectAndAdmitHardware"
|
|
14
14
|
},
|
|
15
15
|
"input": { "from": "input" },
|
|
16
16
|
"resultContract": {
|
|
@@ -32,6 +32,18 @@ constraint_checks 非数组、ID 重复或不属于当前槽位时仍以 VALIDAT
|
|
|
32
32
|
|
|
33
33
|
verified 只代表同源、完整数量和有归属证据的 Agent 选型判断通过,不代表 CAD、几何、电气实测、采用或交付。缺关键评估、非空 unknowns、未覆盖槽位均返回 needs_confirmation;机器门拒绝,但保留结果。整机继承/覆盖 manifest 求解尚未实现,adapt_system 始终待确认;不能借 assemble_new 自动抵扣其他槽位。换型仍走 hardware.revise,不重新排序未点名组件。
|
|
34
34
|
|
|
35
|
+
## 联网件入组件库(P2-05,2026-10-14)
|
|
36
|
+
|
|
37
|
+
组件数据库是硬件 ID 的唯一权威。联网补位选中的件(`external:<hash>`)在选型保存前先入组件库,以库内 UUID 保存,下游(登记、CAD 输入、BOM)只见库内 ID。
|
|
38
|
+
|
|
39
|
+
- **谁做**:`hardware.select` 能力入口 `selectAndAdmitHardware` = 纯选型 `selectHardware` + 入库。宿主预检与可选 HTTP 包装仍调用纯 `selectHardware`,不发任何写请求。
|
|
40
|
+
- **何时做**:只在选中的型号里有 `source_type=external_web` 时;没选中的联网候选不入库。全部校验(数量、约束、证据、召回回读)通过之后才发入库请求,校验失败不会留下库内行。
|
|
41
|
+
- **宿主配置**:`ABELIEN_HARDWARE_ADMISSION_URL`(HTTPS,或回环地址上的 HTTP,URL 不带凭据)与 `ABELIEN_HARDWARE_ADMISSION_TOKEN`(至少 32 字符,作 Bearer)必须同时设置;可选 `ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS`(1000–120000,默认 30000)。两项都不设:保持原行为,联网件保留 `external:` 身份,并为每个这样的型号追加 unknowns `EXTERNAL_NOT_ADMITTED`。只设一项或格式不对:`ADMISSION_UNAVAILABLE`,不静默跳过。
|
|
42
|
+
- **请求** `abelien.hardware-admission-request.v1`:`{schema, selection_id, items:[{external_id, slot_id, manufacturer, model, category_path[3], dimensions_mm, dimension_basis, dimension_geometry, evidence}]}`。尺寸只取**唯一一个**引用完整、无冲突的物理件:box 取长宽高,cylinder 取 `[直径, 直径, 高]`,sphere 取三个直径;记为 `estimate` + `bounding_box`(第三方页面来源权威未知,不当规格书)。多件、缺轴、冲突一律 `null` + `unknown`,不拼整机包络。`evidence` 带 `evidence_ref = web-evidence:<evidence_id>/<page_id>/sha256/<页面正文 SHA-256>`、`evidence_sha256`、来源 URL、厂商与型号引用、该件的原始尺寸与引用。
|
|
43
|
+
- **回复** `abelien.hardware-admission-response.v1`:`{schema, items:[{external_id, component_id(UUID), urn, outcome:created|existing, dimensions_mm, dimension_basis, dimension_geometry, evidence_ref?}]}`,每个请求项恰好一条。`created` 必须原样存下请求的尺寸、依据、几何与 `evidence_ref`;`existing` 是库里已有的同件,以库内事实为准(库内可信尺寸可以据此判出放不下,此时选型以 `VALIDATION` 拒绝,要求换件)。
|
|
44
|
+
- **改写**:入库件的 `component_id` 换成库内 UUID,`bom_item_id` 按新 ID 重新派生,`resource_urn` 取回复的 URN,尺寸四字段与 `dimension_source{component_id, revision, snapshot_id, evidence_ref?}` 取回复(`revision` 与组件库尺寸快照同一公式),`dimension_check` 与 `occupancy` 重算;`source_type=admitted_web`,新增 `admission{external_component_id, outcome}`。覆盖账本、unknowns、兼容报告与约束证据里的旧 ID 一并换成新 ID;`source`、`geometry`、`wiki` 这些联网出处原样保留。状态不变:联网件照旧 `needs_confirmation`(来源权威与工程验证仍未知)。
|
|
45
|
+
- **失败**:网络错误、超时或非 2xx 为 `ADMISSION_FAILED`;回复缺项、多项、非 UUID、`created` 改了尺寸等为 `ADMISSION_RESPONSE_INVALID`。两者都使选型步骤失败、不保存选型;入库按同件去重,重跑安全。
|
|
46
|
+
|
|
35
47
|
## 可选本地 HTTP 包装
|
|
36
48
|
|
|
37
49
|
scripts/server.mjs 使用同一 selectHardware,固定一个已存在作品 workdir,只读该目录的 Harness store,不另建状态。显式设置至少 32 字节的 ABELIEN_HARDWARE_SELECT_TOKEN(仅私有环境文件),在该环境文件所在目录运行:
|
|
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { mergeHardwareWeb } from "../../abelien-hardware/scripts/web-merge.mjs";
|
|
5
5
|
import { finalizeHardwareSelection } from "../../abelien-hardware/scripts/finalize-selection.mjs";
|
|
6
6
|
import { evaluateElectrical } from "../../abelien-hardware/scripts/electrical.mjs";
|
|
7
|
+
import { admitSelectedHardware } from "../../abelien-hardware/scripts/admission.mjs";
|
|
7
8
|
import { canonical, digest, selectionRequestDigest, validateSelectionSlots } from "../../abelien-hardware/scripts/selection-request.mjs";
|
|
8
9
|
const record = value => value && typeof value === "object" && !Array.isArray(value);
|
|
9
10
|
const text = value => typeof value === "string" && value.trim().length > 0;
|
|
@@ -153,3 +154,11 @@ export async function selectHardware({ input, readResource, readBlob }) {
|
|
|
153
154
|
...(advisories.length ? { advisories } : {}) };
|
|
154
155
|
}
|
|
155
156
|
export const run = selectHardware;
|
|
157
|
+
|
|
158
|
+
// Capability entry (P2-05): the pure selection above, then any selected web part enters the component library
|
|
159
|
+
// through the host's admission endpoint and is saved under its library ID. Host preflights keep calling the
|
|
160
|
+
// pure selectHardware, which never writes anywhere.
|
|
161
|
+
export async function selectAndAdmitHardware({ input, readResource, readBlob, env, fetchImpl }) {
|
|
162
|
+
const selection = await selectHardware({ input, readResource, readBlob });
|
|
163
|
+
return admitSelectedHardware({ selection, requirements: input.requirements, slots: input.slots, env, fetchImpl });
|
|
164
|
+
}
|
|
@@ -205,7 +205,7 @@ export function createStore(workdir) {
|
|
|
205
205
|
// doctor must report the shipped package version, never a frozen constant (0.2.3 drifted across
|
|
206
206
|
// six releases). The npm tree always ships package.json at the skill root; standalone skill copies
|
|
207
207
|
// fall back to FALLBACK_VERSION, which scripts/release-check.mjs pins to package.json.
|
|
208
|
-
const FALLBACK_VERSION = "0.
|
|
208
|
+
const FALLBACK_VERSION = "0.13.0";
|
|
209
209
|
export let VERSION = FALLBACK_VERSION;
|
|
210
210
|
try {
|
|
211
211
|
VERSION = String(JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || FALLBACK_VERSION);
|
|
@@ -43,7 +43,7 @@ node <skills目录>/abelien-harness/scripts/ab.mjs --workdir <作品工作目录
|
|
|
43
43
|
--input '{"intent":"restore","artifacts":{"reference_final":true}}'
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
它是纯计算,返回下一阶段、目标、`directives` 与 `actions`。宿主先跑完自己的守卫,再把 `directives` 原样附在自己的执行说明(工具名、字段)之后;`actions` 是该阶段允许的动作(取自计划的 `action_catalog
|
|
46
|
+
它是纯计算,返回下一阶段、目标、`directives` 与 `actions`。宿主先跑完自己的守卫,再把 `directives` 原样附在自己的执行说明(工具名、字段)之后;`actions` 是该阶段允许的动作(取自计划的 `action_catalog`),宿主把它们映射成自己的工具,只放行这些。`gate` 是该阶段结束后宿主固定发出的确认卡(问题、选项与各选项对应的动作:继续 / 重新生成 / 改需求),`waits_for` 列出该阶段开始前必须已答的确认卡所属阶段:卡片不等人,只有要用到那个结果的阶段才停下等用户答复。restore 默认锁定外形;锁定时,CAD 阶段的规则会写明不得放大外壳;未锁定时只允许在 appearance 认可的外观容差内调整外壳,超出容差换件或交用户取舍。意图的划分见 [意图路由](../../docs/architecture/intent-routing.md)。阶段计划是 A1 通过之后到交付首版这一段(及其旁路)阶段顺序、目标、规则与允许动作的唯一来源;「[阶段计划一览](references/journey-stage-plan.md)」由 `npm run render:journey-plan` 从它生成;[完整用户旅程](references/user-journey.md#journey-stage-plan)维护 A1–C 判定与产品规则,两者不得冲突。本能力在[能力映射](references/harness-binding.md#当前能力映射)中登记。
|
|
47
47
|
|
|
48
48
|
本包定义作品事实之间的关系,不表示已实现完整的产品后台。现有可用能力和未接入项集中维护在[能力映射](references/harness-binding.md),不使用旧 `workspace.*` mock 顶替。
|
|
49
49
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "abelien.journey-stage-plan.v1",
|
|
3
|
-
"plan_version": "2026-10-14.
|
|
4
|
-
"description": "按意图的作品旅程阶段计划,是 A1 通过之后到交付首版这一段(及其旁路)的阶段顺序、阶段目标、领域规则与允许动作的唯一来源;Web、Claude Code、Codex 等所有宿主都经 workspace.journey-next 读取,不另维护阶段顺序或工具白名单。orders 是自动推进的主线;side 阶段由宿主守卫按真实结果触发。每个阶段的 directives 是领域规则(宿主中立),宿主把它们附在自己的执行说明(工具名、字段)之后,不改写、不删减。每个阶段的 actions 是该阶段允许的动作,取自 action_catalog(宿主中立的动作目录),按意图不同时写在 action_variants;宿主把动作映射为自己的工具,自动阶段只放行这些工具和宿主自己的只读工具。references/journey-stage-plan.md(阶段计划一览)由 tools/render-journey-plan.mjs 从本计划生成;references/user-journey.md 维护 A1–C
|
|
3
|
+
"plan_version": "2026-10-14.4",
|
|
4
|
+
"description": "按意图的作品旅程阶段计划,是 A1 通过之后到交付首版这一段(及其旁路)的阶段顺序、阶段目标、领域规则与允许动作的唯一来源;Web、Claude Code、Codex 等所有宿主都经 workspace.journey-next 读取,不另维护阶段顺序或工具白名单。orders 是自动推进的主线;side 阶段由宿主守卫按真实结果触发。每个阶段的 directives 是领域规则(宿主中立),宿主把它们附在自己的执行说明(工具名、字段)之后,不改写、不删减。每个阶段的 actions 是该阶段允许的动作,取自 action_catalog(宿主中立的动作目录),按意图不同时写在 action_variants;宿主把动作映射为自己的工具,自动阶段只放行这些工具和宿主自己的只读工具。references/journey-stage-plan.md(阶段计划一览)由 tools/render-journey-plan.mjs 从本计划生成;references/user-journey.md 维护 A1–C 判定与产品规则,本计划不得与之冲突。gate 是环节结束后由宿主固定发出的确认卡(问题、选项与各选项的动作,动作取自 gate_actions;answers 是非继续动作的领域规则),waits_for 是该阶段开始前必须已答的确认卡所属阶段(按意图不同时写在 waits_for_variants),卡片不等人,只有声明了 waits_for 的阶段才停下等答复。改动本计划须升 plan_version、在 test/journey-next.test.mjs 的 HISTORY 追加一行,并重新生成一览。",
|
|
5
5
|
"artifacts": ["reference_final", "concept_image", "hardware", "appearance_baseline", "cad_candidate", "review"],
|
|
6
6
|
"orders": {
|
|
7
7
|
"create": ["reference", "concept", "hardware", "real_image", "cad", "review"],
|
|
@@ -37,16 +37,33 @@
|
|
|
37
37
|
"visual.review": "按契约审查这一版的实际渲染",
|
|
38
38
|
"question.card": "发布一张问题卡,等待用户回答"
|
|
39
39
|
},
|
|
40
|
+
"gate_actions": {
|
|
41
|
+
"continue": "确认这一环节的结果;等它的环节随即可以继续",
|
|
42
|
+
"regenerate": "需求不变,重新生成这一环节的结果,旧结果保留",
|
|
43
|
+
"revise": "把用户的回答整理成需求修改,形成新的需求版本,从受影响的环节重做"
|
|
44
|
+
},
|
|
40
45
|
"stages": {
|
|
41
46
|
"identify": {
|
|
42
47
|
"goal": "识别用户要的现成产品,并取得规格与拆解资料",
|
|
43
48
|
"directives": [
|
|
44
49
|
"调用 reference.research 执行识别与拆解资料检索(用途 identify / teardown;用户给了链接时先读链接),再读取证据,用 reference.finalize 提交识别判断。",
|
|
45
50
|
"识别不确定时按最可能的候选继续,保留其他候选供用户切换;首版之前默认不向用户提问。",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
51
|
+
"完成后展示识别出的原产品图并注明来源(页面与站点),再用一两句话说明识别为哪款产品、依据是什么,以及比例核对结果。",
|
|
52
|
+
"本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。"
|
|
48
53
|
],
|
|
49
|
-
"actions": ["reference.research", "reference.page", "reference.finalize"]
|
|
54
|
+
"actions": ["reference.research", "reference.page", "reference.finalize"],
|
|
55
|
+
"gate": {
|
|
56
|
+
"question": "是不是这一款?",
|
|
57
|
+
"options": [
|
|
58
|
+
{"label": "是这款", "action": "continue"},
|
|
59
|
+
{"label": "换另一个候选", "action": "revise"},
|
|
60
|
+
{"label": "我直接说型号", "action": "revise"}
|
|
61
|
+
],
|
|
62
|
+
"answers": {
|
|
63
|
+
"revise": "用户要换识别结果:把用户指定的型号或选中的其他候选整理成需求修改(连同 context 与 intent.keep),形成新的需求版本后系统重新识别;用户只选了“换另一个候选”、没说是哪一款时,先发一张问题卡问清楚。"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"waits_for": []
|
|
50
67
|
},
|
|
51
68
|
"reference": {
|
|
52
69
|
"goal": "检索相似产品,作为尺寸、结构、器件与实现方式的参考",
|
|
@@ -54,24 +71,53 @@
|
|
|
54
71
|
"调用 reference.research 检索相似产品(用途 similar),再读取证据,用 reference.finalize 提交借用什么、为什么合适、哪些不照搬。",
|
|
55
72
|
"找不到完整的相似产品时,按声学、供电、交互等子系统分别参考,不硬凑数量;首版之前默认不向用户提问。",
|
|
56
73
|
"相似产品只作参考,不成为可编辑模板,也不自动采用其 BOM。",
|
|
57
|
-
"
|
|
74
|
+
"本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。"
|
|
58
75
|
],
|
|
59
|
-
"actions": ["reference.research", "reference.page", "reference.finalize"]
|
|
76
|
+
"actions": ["reference.research", "reference.page", "reference.finalize"],
|
|
77
|
+
"gate": {
|
|
78
|
+
"question": "参考这几款可以吗?",
|
|
79
|
+
"options": [
|
|
80
|
+
{"label": "可以", "action": "continue"},
|
|
81
|
+
{"label": "换一批", "action": "revise"},
|
|
82
|
+
{"label": "我补充参考", "action": "revise"}
|
|
83
|
+
],
|
|
84
|
+
"answers": {
|
|
85
|
+
"revise": "用户要换参考:把要避开的参考或要补充的参考产品整理成需求修改,形成新的需求版本后系统重新检索参考;用户只选了“我补充参考”、没说是哪款时,先发一张问题卡问清楚。"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"waits_for": []
|
|
60
89
|
},
|
|
61
90
|
"concept": {
|
|
62
91
|
"goal": "生成第一张概念图",
|
|
63
92
|
"directives": [
|
|
64
93
|
"按外观目标生成第一张概念图;需求没说到的外观细节按常见做法补全,首版之前默认不向用户提问。",
|
|
65
94
|
"出图后用一两句话告诉用户并结束本轮,硬件选型在下一阶段进行。",
|
|
66
|
-
"
|
|
95
|
+
"本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。"
|
|
67
96
|
],
|
|
68
97
|
"variants": {
|
|
69
98
|
"modify": [
|
|
70
99
|
"以识别出的原产品图为输入生成修改后的图,只改用户要求修改的部分,保留清单中的特征。",
|
|
71
|
-
"出图后用一两句话说明改了什么、保留了什么,并结束本轮;硬件选型在下一阶段进行。"
|
|
100
|
+
"出图后用一两句话说明改了什么、保留了什么,并结束本轮;硬件选型在下一阶段进行。",
|
|
101
|
+
"本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。"
|
|
72
102
|
]
|
|
73
103
|
},
|
|
74
|
-
"actions": ["studio.prepare", "image.generate"]
|
|
104
|
+
"actions": ["studio.prepare", "image.generate"],
|
|
105
|
+
"gate": {
|
|
106
|
+
"question": "用这张图吗?",
|
|
107
|
+
"options": [
|
|
108
|
+
{"label": "就用这张", "action": "continue"},
|
|
109
|
+
{"label": "重新生成一张", "action": "regenerate"},
|
|
110
|
+
{"label": "调整方向", "action": "revise"}
|
|
111
|
+
],
|
|
112
|
+
"answers": {
|
|
113
|
+
"regenerate": "需求不变,再生成一张概念图:用 studio.prepare 准备新的出图版本(这是用户要求的重画),再用 image.generate 生成;旧图保留。",
|
|
114
|
+
"revise": "用户要调整外观方向:把回答里的外观要求整理成需求修改,形成新的需求版本后系统重定外观目标再画;回答没说具体怎么调时,先发一张问题卡问清楚。"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"waits_for": [],
|
|
118
|
+
"waits_for_variants": {
|
|
119
|
+
"modify": ["identify"]
|
|
120
|
+
}
|
|
75
121
|
},
|
|
76
122
|
"hardware": {
|
|
77
123
|
"goal": "为当前需求选型并登记",
|
|
@@ -79,17 +125,32 @@
|
|
|
79
125
|
"主要元器件选定即可;供电、电池、联网方式等用户没说的未定项按推荐默认值选型,并作为待确认项登记,首版之前默认不向用户提问。",
|
|
80
126
|
"不要修改需求,也不要出图。",
|
|
81
127
|
"登记后用一两句话说明选了什么、哪些按默认值处理,然后结束本轮;待确认项在首版之后合并成一张问题卡统一询问。",
|
|
82
|
-
"
|
|
128
|
+
"本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。"
|
|
83
129
|
],
|
|
84
130
|
"variants": {
|
|
85
131
|
"restore": [
|
|
86
132
|
"以参考证据中的来源产品规格与拆解资料为对照逐项选择替代件,区分原件、替代件与未知;主要元器件选定即可,未定项按推荐默认值登记为待确认项,首版之前默认不向用户提问。",
|
|
87
133
|
"不要修改需求,也不要出图。",
|
|
88
134
|
"登记后用一两句话说明替代关系与默认值,然后结束本轮;待确认项在首版之后合并成一张问题卡统一询问。",
|
|
89
|
-
"
|
|
135
|
+
"本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。"
|
|
90
136
|
]
|
|
91
137
|
},
|
|
92
|
-
"actions": ["component.search", "hardware.guide", "hardware.wiki", "hardware.resource", "hardware.workflow", "hardware.web-extract", "hardware.prepare"]
|
|
138
|
+
"actions": ["component.search", "hardware.guide", "hardware.wiki", "hardware.resource", "hardware.workflow", "hardware.web-extract", "hardware.prepare"],
|
|
139
|
+
"gate": {
|
|
140
|
+
"question": "用这套硬件吗?",
|
|
141
|
+
"options": [
|
|
142
|
+
{"label": "用这套", "action": "continue"},
|
|
143
|
+
{"label": "换某个零件", "action": "revise"},
|
|
144
|
+
{"label": "改要求", "action": "revise"}
|
|
145
|
+
],
|
|
146
|
+
"answers": {
|
|
147
|
+
"revise": "用户要改硬件:把要换的零件或新的要求整理成需求修改,形成新的需求版本后系统按新需求整套重新选型;回答没说换哪个零件、改成什么时,先发一张问题卡问清楚。"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"waits_for": [],
|
|
151
|
+
"waits_for_variants": {
|
|
152
|
+
"restore": ["identify"]
|
|
153
|
+
}
|
|
93
154
|
},
|
|
94
155
|
"hardware_reselect": {
|
|
95
156
|
"goal": "当前硬件放不下用户的硬尺寸上限或锁定分区时重新选型",
|
|
@@ -129,6 +190,11 @@
|
|
|
129
190
|
"actions": ["studio.prepare"],
|
|
130
191
|
"action_variants": {
|
|
131
192
|
"restore": ["studio.prepare", "image.import"]
|
|
193
|
+
},
|
|
194
|
+
"waits_for": ["concept", "hardware"],
|
|
195
|
+
"waits_for_variants": {
|
|
196
|
+
"restore": ["identify", "hardware"],
|
|
197
|
+
"remix": ["concept"]
|
|
132
198
|
}
|
|
133
199
|
},
|
|
134
200
|
"cad": {
|
|
@@ -147,7 +213,8 @@
|
|
|
147
213
|
"true": "外形尺寸与比例已锁定:不得放大或缩小外壳;装不下时停止建模,先交回硬件换件,仍不行再调整结构骨架,改外形必须由用户决定。"
|
|
148
214
|
}
|
|
149
215
|
},
|
|
150
|
-
"actions": ["cad.build"]
|
|
216
|
+
"actions": ["cad.build"],
|
|
217
|
+
"waits_for": []
|
|
151
218
|
},
|
|
152
219
|
"fallback": {
|
|
153
220
|
"goal": "建模没有产出可展示的 CAD 时,由宿主生成标注清楚的简化草模",
|
|
@@ -168,7 +235,8 @@
|
|
|
168
235
|
"不要重新建模、不要重新出图、不要修改需求;待答的问题卡保持原样,不代用户回答。"
|
|
169
236
|
]
|
|
170
237
|
},
|
|
171
|
-
"actions": ["visual.review"]
|
|
238
|
+
"actions": ["visual.review"],
|
|
239
|
+
"waits_for": []
|
|
172
240
|
},
|
|
173
241
|
"repair": {
|
|
174
242
|
"goal": "只修正检查发现的问题",
|
|
@@ -180,12 +248,13 @@
|
|
|
180
248
|
"actions": ["cad.build"]
|
|
181
249
|
},
|
|
182
250
|
"questions": {
|
|
183
|
-
"goal": "
|
|
251
|
+
"goal": "首版之后问用户这一版怎么样,并合并询问按默认处理的事项",
|
|
184
252
|
"directives": [
|
|
185
|
-
"
|
|
253
|
+
"首版之后发一张问题卡,第一题固定问“这一版怎么样?”,选项依次为“就用这一版”“继续修改”“有问题要说明”;选“就用这一版”只是表态,采用仍要用户点「采用」。",
|
|
254
|
+
"其余最多两题:读取已登记的硬件选型与系统假设,把首版之前按默认处理、至今仍按默认处理的事项合并进这张卡,每题最多三个可直接选的方案,第一个选项写首版当前采用的默认值并注明“(首版已按此)”。",
|
|
186
255
|
"这张卡每次按当前状态重新生成,不是保存的问题队列:用户已改掉或已不再适用的默认不再出现,首版之前按例外问过并已答复的事项不重复询问。",
|
|
187
256
|
"意图、目标深度或型号识别仍有疑问时,可以作为其中一题,第一个选项写首版采用的判断。",
|
|
188
|
-
"
|
|
257
|
+
"没有待确认项时这张卡只有第一题。",
|
|
189
258
|
"不要修改需求、重新选型、建模或出图;用户的答复作为对首版的修订处理。"
|
|
190
259
|
],
|
|
191
260
|
"actions": ["question.card"]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-workspace",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "作品旅程、PRD 与业务契约;作品推进由宿主按 workspace.journey-next 的阶段计划进行,不是 workflow;draft 供真实硬件阶段使用;不声明假 PRD、确认、报价或支付能力",
|
|
5
5
|
"resources": [
|
|
6
6
|
"draft"
|
|
7
7
|
],
|
|
8
8
|
"capabilities": {
|
|
9
9
|
"workspace.journey-next": {
|
|
10
|
-
"description": "按意图(create / restore / modify / remix)的阶段计划给出下一阶段与该阶段的领域规则;纯计算,不联网、不写资源。宿主先跑完自己的守卫,再把 directives 附在自己的执行说明之后;actions
|
|
10
|
+
"description": "按意图(create / restore / modify / remix)的阶段计划给出下一阶段与该阶段的领域规则;纯计算,不联网、不写资源。宿主先跑完自己的守卫,再把 directives 附在自己的执行说明之后;actions 是该阶段允许的宿主中立动作,宿主据此放行自己的工具;gate 是该阶段结束后宿主固定发出的确认卡,waits_for 是该阶段开始前必须已答的确认卡所属阶段",
|
|
11
11
|
"adapter": {
|
|
12
12
|
"type": "module",
|
|
13
13
|
"path": "scripts/journey-next.mjs",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"arrays": [
|
|
25
25
|
"directives",
|
|
26
26
|
"actions",
|
|
27
|
+
"waits_for",
|
|
27
28
|
"order",
|
|
28
29
|
"completed"
|
|
29
30
|
]
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<!-- journey-stage-plan:begin 由 tools/render-journey-plan.mjs 从 assets/journey-stage-plan.json 生成,勿手改 -->
|
|
6
6
|
|
|
7
|
-
#### 阶段计划一览(plan_version 2026-10-14.
|
|
7
|
+
#### 阶段计划一览(plan_version 2026-10-14.4)
|
|
8
8
|
|
|
9
9
|
| 意图 | 主线顺序 |
|
|
10
10
|
| --- | --- |
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
旁路阶段由宿主按真实结果触发:hardware_reselect、hardware_decision、fallback、repair、questions。还原(restore)默认锁定外形,其他意图默认不锁定。
|
|
17
17
|
|
|
18
|
+
确认卡的动作:`continue`(确认这一环节的结果;等它的环节随即可以继续)、`regenerate`(需求不变,重新生成这一环节的结果,旧结果保留)、`revise`(把用户的回答整理成需求修改,形成新的需求版本,从受影响的环节重做)。卡片不等人,只有「开始前要等」列出的阶段才停下等用户答复。
|
|
19
|
+
|
|
18
20
|
**identify**:识别用户要的现成产品,并取得规格与拆解资料
|
|
19
21
|
|
|
20
22
|
完成标志:宿主已保存 `reference_final`。
|
|
@@ -25,8 +27,13 @@
|
|
|
25
27
|
|
|
26
28
|
1. 调用 reference.research 执行识别与拆解资料检索(用途 identify / teardown;用户给了链接时先读链接),再读取证据,用 reference.finalize 提交识别判断。
|
|
27
29
|
2. 识别不确定时按最可能的候选继续,保留其他候选供用户切换;首版之前默认不向用户提问。
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
30
|
+
3. 完成后展示识别出的原产品图并注明来源(页面与站点),再用一两句话说明识别为哪款产品、依据是什么,以及比例核对结果。
|
|
31
|
+
4. 本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。
|
|
32
|
+
|
|
33
|
+
环节结束后宿主发确认卡:「是不是这一款?」选项:是这款(`continue`)、换另一个候选(`revise`)、我直接说型号(`revise`)。
|
|
34
|
+
- 选 `revise` 时:用户要换识别结果:把用户指定的型号或选中的其他候选整理成需求修改(连同 context 与 intent.keep),形成新的需求版本后系统重新识别;用户只选了“换另一个候选”、没说是哪一款时,先发一张问题卡问清楚。
|
|
35
|
+
|
|
36
|
+
开始前要等:不等确认卡。
|
|
30
37
|
|
|
31
38
|
**reference**:检索相似产品,作为尺寸、结构、器件与实现方式的参考
|
|
32
39
|
|
|
@@ -39,7 +46,12 @@
|
|
|
39
46
|
1. 调用 reference.research 检索相似产品(用途 similar),再读取证据,用 reference.finalize 提交借用什么、为什么合适、哪些不照搬。
|
|
40
47
|
2. 找不到完整的相似产品时,按声学、供电、交互等子系统分别参考,不硬凑数量;首版之前默认不向用户提问。
|
|
41
48
|
3. 相似产品只作参考,不成为可编辑模板,也不自动采用其 BOM。
|
|
42
|
-
4.
|
|
49
|
+
4. 本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。
|
|
50
|
+
|
|
51
|
+
环节结束后宿主发确认卡:「参考这几款可以吗?」选项:可以(`continue`)、换一批(`revise`)、我补充参考(`revise`)。
|
|
52
|
+
- 选 `revise` 时:用户要换参考:把要避开的参考或要补充的参考产品整理成需求修改,形成新的需求版本后系统重新检索参考;用户只选了“我补充参考”、没说是哪款时,先发一张问题卡问清楚。
|
|
53
|
+
|
|
54
|
+
开始前要等:不等确认卡。
|
|
43
55
|
|
|
44
56
|
**concept**:生成第一张概念图
|
|
45
57
|
|
|
@@ -51,12 +63,20 @@
|
|
|
51
63
|
|
|
52
64
|
1. 按外观目标生成第一张概念图;需求没说到的外观细节按常见做法补全,首版之前默认不向用户提问。
|
|
53
65
|
2. 出图后用一两句话告诉用户并结束本轮,硬件选型在下一阶段进行。
|
|
54
|
-
3.
|
|
66
|
+
3. 本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。
|
|
55
67
|
|
|
56
68
|
改现成(modify)改用:
|
|
57
69
|
|
|
58
70
|
1. 以识别出的原产品图为输入生成修改后的图,只改用户要求修改的部分,保留清单中的特征。
|
|
59
71
|
2. 出图后用一两句话说明改了什么、保留了什么,并结束本轮;硬件选型在下一阶段进行。
|
|
72
|
+
3. 本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。
|
|
73
|
+
|
|
74
|
+
环节结束后宿主发确认卡:「用这张图吗?」选项:就用这张(`continue`)、重新生成一张(`regenerate`)、调整方向(`revise`)。
|
|
75
|
+
- 选 `regenerate` 时:需求不变,再生成一张概念图:用 studio.prepare 准备新的出图版本(这是用户要求的重画),再用 image.generate 生成;旧图保留。
|
|
76
|
+
- 选 `revise` 时:用户要调整外观方向:把回答里的外观要求整理成需求修改,形成新的需求版本后系统重定外观目标再画;回答没说具体怎么调时,先发一张问题卡问清楚。
|
|
77
|
+
|
|
78
|
+
开始前要等:不等确认卡。
|
|
79
|
+
改现成(modify)改为:`identify` 的确认卡已答。
|
|
60
80
|
|
|
61
81
|
**hardware**:为当前需求选型并登记
|
|
62
82
|
|
|
@@ -69,14 +89,20 @@
|
|
|
69
89
|
1. 主要元器件选定即可;供电、电池、联网方式等用户没说的未定项按推荐默认值选型,并作为待确认项登记,首版之前默认不向用户提问。
|
|
70
90
|
2. 不要修改需求,也不要出图。
|
|
71
91
|
3. 登记后用一两句话说明选了什么、哪些按默认值处理,然后结束本轮;待确认项在首版之后合并成一张问题卡统一询问。
|
|
72
|
-
4.
|
|
92
|
+
4. 本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。
|
|
73
93
|
|
|
74
94
|
还原(restore)改用:
|
|
75
95
|
|
|
76
96
|
1. 以参考证据中的来源产品规格与拆解资料为对照逐项选择替代件,区分原件、替代件与未知;主要元器件选定即可,未定项按推荐默认值登记为待确认项,首版之前默认不向用户提问。
|
|
77
97
|
2. 不要修改需求,也不要出图。
|
|
78
98
|
3. 登记后用一两句话说明替代关系与默认值,然后结束本轮;待确认项在首版之后合并成一张问题卡统一询问。
|
|
79
|
-
4.
|
|
99
|
+
4. 本环节结束后系统会固定发一张确认卡请用户确认,这张卡不由你发;正文不要另问用户是否满意、要不要继续。
|
|
100
|
+
|
|
101
|
+
环节结束后宿主发确认卡:「用这套硬件吗?」选项:用这套(`continue`)、换某个零件(`revise`)、改要求(`revise`)。
|
|
102
|
+
- 选 `revise` 时:用户要改硬件:把要换的零件或新的要求整理成需求修改,形成新的需求版本后系统按新需求整套重新选型;回答没说换哪个零件、改成什么时,先发一张问题卡问清楚。
|
|
103
|
+
|
|
104
|
+
开始前要等:不等确认卡。
|
|
105
|
+
还原(restore)改为:`identify` 的确认卡已答。
|
|
80
106
|
|
|
81
107
|
**hardware_reselect**(旁路):当前硬件放不下用户的硬尺寸上限或锁定分区时重新选型
|
|
82
108
|
|
|
@@ -117,6 +143,10 @@
|
|
|
117
143
|
2. 用户明确要求重画时,在用户回合准备新的图片版本,保留原产物。
|
|
118
144
|
3. 不要修改需求;待答的问题卡保持原样,不代用户回答。
|
|
119
145
|
|
|
146
|
+
开始前要等:`concept`、`hardware` 的确认卡已答。
|
|
147
|
+
还原(restore)改为:`identify`、`hardware` 的确认卡已答。
|
|
148
|
+
二创(remix)改为:`concept` 的确认卡已答。
|
|
149
|
+
|
|
120
150
|
**cad**:形成可旋转的首版 CAD
|
|
121
151
|
|
|
122
152
|
完成标志:宿主已保存 `cad_candidate`。
|
|
@@ -132,6 +162,8 @@
|
|
|
132
162
|
5. 失败就按回执诊断修正后重试;同一错误连续出现时换一种实现方式。
|
|
133
163
|
6. 不要重新登记硬件、不要修改需求或重新出图;视觉审查在下一阶段进行;待答的问题卡保持原样,不代用户回答。
|
|
134
164
|
|
|
165
|
+
开始前要等:不等确认卡。
|
|
166
|
+
|
|
135
167
|
外形未锁定(`locks.shape=false`)时插入为第 2 条:外形未锁定:硬件放得下优先,但外壳只能在 appearance 认可的外观容差内调整尺寸与比例,并记录调整量及依据;超出容差时换件(交回硬件)或把取舍交给用户,不得自行放大外壳。
|
|
136
168
|
|
|
137
169
|
外形已锁定(`locks.shape=true`)时插入为第 2 条:外形尺寸与比例已锁定:不得放大或缩小外壳;装不下时停止建模,先交回硬件换件,仍不行再调整结构骨架,改外形必须由用户决定。
|
|
@@ -160,6 +192,8 @@
|
|
|
160
192
|
1. 以识别出的真实产品图为基准,按契约审查这一版,审查对象全部取自 CAD 回执,不要猜;然后用一两句话说明与原产品的差异。
|
|
161
193
|
2. 不要重新建模、不要重新出图、不要修改需求;待答的问题卡保持原样,不代用户回答。
|
|
162
194
|
|
|
195
|
+
开始前要等:不等确认卡。
|
|
196
|
+
|
|
163
197
|
**repair**(旁路):只修正检查发现的问题
|
|
164
198
|
|
|
165
199
|
允许的动作:`cad.build`(构建 CAD 候选,含几何检查与实际渲染)。
|
|
@@ -170,16 +204,17 @@
|
|
|
170
204
|
2. 尽量保留外观基准图的轮廓、比例、特征位置与材质;不缩放原生元器件。
|
|
171
205
|
3. 不要修改需求、重新选型或重新出图;先完成轻量预览,视觉复检在下一阶段进行;待答的问题卡保持原样,不代用户回答。
|
|
172
206
|
|
|
173
|
-
**questions
|
|
207
|
+
**questions**(旁路):首版之后问用户这一版怎么样,并合并询问按默认处理的事项
|
|
174
208
|
|
|
175
209
|
允许的动作:`question.card`(发布一张问题卡,等待用户回答)。
|
|
176
210
|
|
|
177
211
|
规则:
|
|
178
212
|
|
|
179
|
-
1.
|
|
180
|
-
2.
|
|
181
|
-
3.
|
|
182
|
-
4.
|
|
183
|
-
5.
|
|
213
|
+
1. 首版之后发一张问题卡,第一题固定问“这一版怎么样?”,选项依次为“就用这一版”“继续修改”“有问题要说明”;选“就用这一版”只是表态,采用仍要用户点「采用」。
|
|
214
|
+
2. 其余最多两题:读取已登记的硬件选型与系统假设,把首版之前按默认处理、至今仍按默认处理的事项合并进这张卡,每题最多三个可直接选的方案,第一个选项写首版当前采用的默认值并注明“(首版已按此)”。
|
|
215
|
+
3. 这张卡每次按当前状态重新生成,不是保存的问题队列:用户已改掉或已不再适用的默认不再出现,首版之前按例外问过并已答复的事项不重复询问。
|
|
216
|
+
4. 意图、目标深度或型号识别仍有疑问时,可以作为其中一题,第一个选项写首版采用的判断。
|
|
217
|
+
5. 没有待确认项时这张卡只有第一题。
|
|
218
|
+
6. 不要修改需求、重新选型、建模或出图;用户的答复作为对首版的修订处理。
|
|
184
219
|
|
|
185
220
|
<!-- journey-stage-plan:end -->
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
无现成 CAD 的目标文字链为:保存需求 → concept 首图 → hardware 自动选型登记 → real_image 原图绑定 → CAD preview → review → 必要 repair/复检。real_image 是历史 key,不是再次生图;无可用图转入有生图工具的 concept,重试与重画按[图片请求规则](#image-retry-intent)结合上下文处理。未知/在途调用只恢复原身份。硬件待确认项按草稿合同披露,不把整体 verified 作为首图前置。
|
|
8
8
|
|
|
9
|
-
提问时机(2026-10-12 裁定)。首版 CAD
|
|
9
|
+
提问时机(2026-10-12 裁定)。首版 CAD 之前默认不向用户提问:按原话和合理默认继续,按默认处理的事项登记为系统假设或待确认项,由宿主展示、用户可随时修改;首版之后,把仍按默认处理的事项合并成一张问题卡。唯一例外:用户提出的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如“必须放进这个卡槽”却没给尺寸)时,可以在首版之前提问,至多一张卡,沿用问题卡上限(最多三题、每题最多三个选项)。确认卡(2026-10-14 决定)不属于这里的提问:它由宿主在识别、找参考、概念图、选硬件这几个环节结束后按阶段计划的 gate 固定发出,问题与选项事先写死(继续 / 重新生成 / 改需求),首版之前也会出现;卡片不等人,只有阶段计划 waits_for 声明要用这个结果的阶段才停下等答复。首版之后的合并卡第一题固定问“这一版怎么样?”。问题卡按持久问题工具调用保存;用户独立提交后以原工具调用的结果承接原任务、阶段和待执行动作,等待期间不保持 HTTP/Worker。系统依据落盘结果继续,不在每段末尾询问是否开始下一段。模型不能在绑定阶段调用未注册的生图工具。普通咨询不触发执行。
|
|
10
10
|
|
|
11
11
|
Web 已提交基线 91fac59a 仍在 real_image 生图;原图绑定、CAD 原图像素输入及 review 后自动首显属于本地未交付修复。组件资产导入与“审查前自动首显”仍有接线缺口。以上目标不代表宿主已实现,实际准入始终按安装版本读取。具体实现与验证边界由 abelienwebapplication 的 docs/studio-user-journey.md 及实际宿主契约维护;已安装 release 不因源文档更新自动升级。下文 A1–C、通用领域反馈和体验目标按其范围适用,不能覆盖宿主当前受控工具。
|
|
12
12
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 纯函数:不联网、不写资源、不读宿主状态。宿主把自己保存的产物映射为 artifacts,先跑完自己的守卫
|
|
3
3
|
// (结算、计费、保底、预算、幂等),再调用本能力;拿到的 directives 原样附在宿主自己的执行说明之后,
|
|
4
4
|
// actions(action_catalog 中的宿主中立动作)由宿主映射为自己的工具,是该阶段唯一的放行依据。
|
|
5
|
+
// gate 是该阶段结束后宿主固定发出的确认卡;waits_for 是该阶段开始前必须已答的确认卡所属阶段,宿主据此决定是否停下等用户。
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
6
7
|
|
|
7
8
|
const deepFreeze = value => {
|
|
@@ -29,6 +30,18 @@ export function stageActions(stage, intent) {
|
|
|
29
30
|
return [...(entry.action_variants?.[intent] ?? entry.actions)];
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/** The confirmation card the host issues once this stage's result is saved, or null for a stage without one. */
|
|
34
|
+
export function stageGate(stage) {
|
|
35
|
+
const gate = PLAN.stages[stage].gate;
|
|
36
|
+
return gate ? structuredClone(gate) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Stages whose confirmation cards must be answered before this stage starts: the intent's variant if present, else the stage's list. */
|
|
40
|
+
export function stageWaitsFor(stage, intent) {
|
|
41
|
+
const entry = PLAN.stages[stage];
|
|
42
|
+
return [...(entry.waits_for_variants?.[intent] ?? entry.waits_for ?? [])];
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export function journeyNext({ input }) {
|
|
33
46
|
if (!record(input) || Object.keys(input).some(key => !["intent", "artifacts", "locks", "stage"].includes(key))) invalid("input accepts intent, artifacts, locks and stage only");
|
|
34
47
|
if (!INTENTS.includes(input.intent)) invalid(`intent must be one of ${INTENTS.join(", ")}`);
|
|
@@ -53,6 +66,7 @@ export function journeyNext({ input }) {
|
|
|
53
66
|
return {
|
|
54
67
|
plan_version: PLAN.plan_version, intent: input.intent, stage, goal: stage ? PLAN.stages[stage].goal : null,
|
|
55
68
|
directives: stage ? stageDirectives(stage, input.intent, locks) : [], actions: stage ? stageActions(stage, input.intent) : [],
|
|
69
|
+
gate: stage ? stageGate(stage) : null, waits_for: stage ? stageWaitsFor(stage, input.intent) : [],
|
|
56
70
|
order: [...order], completed, locks,
|
|
57
71
|
};
|
|
58
72
|
}
|