abelien-harness 0.5.0 → 0.6.1
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 +59 -0
- package/skills/abelien-reference/domain.json +92 -0
- package/skills/abelien-reference/fixtures/create.final.json +104 -0
- package/skills/abelien-reference/fixtures/restore.collected.json +191 -0
- package/skills/abelien-reference/fixtures/restore.final.json +269 -0
- package/skills/abelien-reference/references/research-contract.md +78 -0
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +153 -0
- package/skills/abelien-reference/scripts/evidence.mjs +68 -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 +191 -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,269 @@
|
|
|
1
|
+
{
|
|
2
|
+
"budget": {
|
|
3
|
+
"images": 1,
|
|
4
|
+
"pages": 2,
|
|
5
|
+
"queries": 1,
|
|
6
|
+
"skipped_pages": 0,
|
|
7
|
+
"skipped_images": 0
|
|
8
|
+
},
|
|
9
|
+
"changeable": [],
|
|
10
|
+
"depth": "assembly",
|
|
11
|
+
"id": "ref-40ece5312c4ecbf6-fd2b162cb",
|
|
12
|
+
"images": [
|
|
13
|
+
{
|
|
14
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
15
|
+
"id": "i1",
|
|
16
|
+
"measurement": {
|
|
17
|
+
"aspect_h_over_w": 1.8188,
|
|
18
|
+
"background": {
|
|
19
|
+
"mode": "color",
|
|
20
|
+
"rgb": [
|
|
21
|
+
255,
|
|
22
|
+
255,
|
|
23
|
+
255
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"bbox": {
|
|
27
|
+
"height": 502,
|
|
28
|
+
"width": 276,
|
|
29
|
+
"x": 62,
|
|
30
|
+
"y": 49
|
|
31
|
+
},
|
|
32
|
+
"image": {
|
|
33
|
+
"height": 600,
|
|
34
|
+
"width": 400
|
|
35
|
+
},
|
|
36
|
+
"method": "silhouette-v1",
|
|
37
|
+
"status": "measured"
|
|
38
|
+
},
|
|
39
|
+
"media_type": "image/png",
|
|
40
|
+
"page_id": "p1",
|
|
41
|
+
"reason": null,
|
|
42
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11",
|
|
43
|
+
"status": "read",
|
|
44
|
+
"url": "https://img.example.com/pro.png"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"intent": "restore",
|
|
48
|
+
"items": [
|
|
49
|
+
{
|
|
50
|
+
"id": "u1",
|
|
51
|
+
"image": {
|
|
52
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
53
|
+
"media_type": "image/png",
|
|
54
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11"
|
|
55
|
+
},
|
|
56
|
+
"kind": "user_input",
|
|
57
|
+
"query": null,
|
|
58
|
+
"role": "帮我根据图片还原小爱音箱",
|
|
59
|
+
"source": null,
|
|
60
|
+
"status": "ready",
|
|
61
|
+
"title": "你的输入",
|
|
62
|
+
"verdict": null
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "p1",
|
|
66
|
+
"image": {
|
|
67
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
68
|
+
"media_type": "image/png",
|
|
69
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11"
|
|
70
|
+
},
|
|
71
|
+
"kind": "candidate",
|
|
72
|
+
"query": "Xiaomi XiaoAI speaker Pro dimensions",
|
|
73
|
+
"role": "候选产品",
|
|
74
|
+
"source": {
|
|
75
|
+
"site": "www.mi.example.com",
|
|
76
|
+
"url": "https://www.mi.example.com/pro/specs"
|
|
77
|
+
},
|
|
78
|
+
"status": "ready",
|
|
79
|
+
"title": "小爱音箱 Pro",
|
|
80
|
+
"verdict": {
|
|
81
|
+
"code": "closest",
|
|
82
|
+
"basis": "选定为「小爱音箱 Pro」;实测高宽比 1.8188,规格 1.822,相差 0.2%"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "p2",
|
|
87
|
+
"image": null,
|
|
88
|
+
"kind": "candidate",
|
|
89
|
+
"query": "Xiaomi XiaoAI speaker Pro dimensions",
|
|
90
|
+
"role": "候选产品",
|
|
91
|
+
"source": {
|
|
92
|
+
"site": "www.mi.example.com",
|
|
93
|
+
"url": "https://www.mi.example.com/gen1/specs"
|
|
94
|
+
},
|
|
95
|
+
"status": "ready",
|
|
96
|
+
"title": "小米AI音箱",
|
|
97
|
+
"verdict": {
|
|
98
|
+
"code": "ratio_mismatch",
|
|
99
|
+
"basis": "实测高宽比 1.8188,规格 2.4045,相差 24.4%"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "sp1",
|
|
104
|
+
"kind": "spec",
|
|
105
|
+
"title": "外形尺寸",
|
|
106
|
+
"role": "Ø118 × 215 mm(引自 p1)",
|
|
107
|
+
"source": {
|
|
108
|
+
"url": "https://www.mi.example.com/pro/specs",
|
|
109
|
+
"site": "www.mi.example.com"
|
|
110
|
+
},
|
|
111
|
+
"image": null,
|
|
112
|
+
"verdict": null,
|
|
113
|
+
"status": "ready",
|
|
114
|
+
"query": null
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"keep": [
|
|
118
|
+
"外形尺寸与比例"
|
|
119
|
+
],
|
|
120
|
+
"pages": [
|
|
121
|
+
{
|
|
122
|
+
"excerpt": "小爱音箱 Pro 参数 产品尺寸:118 × 118 × 215 mm,重量 1187 g。",
|
|
123
|
+
"final_url": "https://www.mi.example.com/pro/specs",
|
|
124
|
+
"id": "p1",
|
|
125
|
+
"image_url": "https://img.example.com/pro.png",
|
|
126
|
+
"purpose": "identify",
|
|
127
|
+
"raw_sha256": "109cbac90dd51ef3dddfd21c4a97168a25f097b4211b2146d21669281d06e3b8",
|
|
128
|
+
"reason": null,
|
|
129
|
+
"search_id": "s1",
|
|
130
|
+
"status": "read",
|
|
131
|
+
"text_blob_ref": "sha256:a9d6a6d8668de3346efe1d94c1afd76e71faf37bee5a6e55d3a82f9bc60b9bf0",
|
|
132
|
+
"text_sha256": "bae09c2d8ac69290d90e7cf69d7809858b1149a391400d40030037db41896415",
|
|
133
|
+
"title": "小爱音箱 Pro 参数",
|
|
134
|
+
"url": "https://www.mi.example.com/pro/specs"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"excerpt": "小米AI音箱 规格 尺寸 88 mm × 88 mm × 211.6 mm",
|
|
138
|
+
"final_url": "https://www.mi.example.com/gen1/specs",
|
|
139
|
+
"id": "p2",
|
|
140
|
+
"image_url": null,
|
|
141
|
+
"purpose": "identify",
|
|
142
|
+
"raw_sha256": "c139f165c41dc34c2cd30a816a80d7cbef9ecbda49e7496d46163c8692da33c7",
|
|
143
|
+
"reason": null,
|
|
144
|
+
"search_id": "s1",
|
|
145
|
+
"status": "read",
|
|
146
|
+
"text_blob_ref": "sha256:3dd43ad1e906a2e95af747cc8f8386c00b43df981af8b8bafe45bb2d173e7e3c",
|
|
147
|
+
"text_sha256": "2de9e3c9406bd62a2e9eef887fd42008382616377ac49fb751cd5a109f1e9a00",
|
|
148
|
+
"title": "小米AI音箱 规格",
|
|
149
|
+
"url": "https://www.mi.example.com/gen1/specs"
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"request_digest": "40ece5312c4ecbf6f5548d36589de25734706b8a333d22e1a3c32adeb75e09f5",
|
|
153
|
+
"schema": "abelien.reference-evidence.v1",
|
|
154
|
+
"searches": [
|
|
155
|
+
{
|
|
156
|
+
"count": 5,
|
|
157
|
+
"error_code": null,
|
|
158
|
+
"id": "s1",
|
|
159
|
+
"purpose": "identify",
|
|
160
|
+
"query": "Xiaomi XiaoAI speaker Pro dimensions",
|
|
161
|
+
"receipt_sha256": "099206543e4389fde6c4838d3fc0b125dce0fb98cd57384db3062e1abcede7a8",
|
|
162
|
+
"request_id": "req-[redacted]",
|
|
163
|
+
"results": [
|
|
164
|
+
{
|
|
165
|
+
"rank": 1,
|
|
166
|
+
"snippet": "",
|
|
167
|
+
"title": "XiaoAI Speaker Pro",
|
|
168
|
+
"url": "https://www.mi.example.com/pro/specs"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"rank": 2,
|
|
172
|
+
"snippet": "",
|
|
173
|
+
"title": "Mi AI Speaker",
|
|
174
|
+
"url": "https://www.mi.example.com/gen1/specs"
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
"status": "completed"
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"status": "final",
|
|
181
|
+
"text": "帮我根据图片还原小爱音箱",
|
|
182
|
+
"unknowns": [],
|
|
183
|
+
"user_image": {
|
|
184
|
+
"blob_ref": "sha256:e2273b5b355a38441b20796072e08921b9ab30c4f37bb92070a53307af34a6f6",
|
|
185
|
+
"measurement": {
|
|
186
|
+
"aspect_h_over_w": 1.8188,
|
|
187
|
+
"background": {
|
|
188
|
+
"mode": "color",
|
|
189
|
+
"rgb": [
|
|
190
|
+
255,
|
|
191
|
+
255,
|
|
192
|
+
255
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
"bbox": {
|
|
196
|
+
"height": 502,
|
|
197
|
+
"width": 276,
|
|
198
|
+
"x": 62,
|
|
199
|
+
"y": 49
|
|
200
|
+
},
|
|
201
|
+
"image": {
|
|
202
|
+
"height": 600,
|
|
203
|
+
"width": 400
|
|
204
|
+
},
|
|
205
|
+
"method": "silhouette-v1",
|
|
206
|
+
"status": "measured"
|
|
207
|
+
},
|
|
208
|
+
"media_type": "image/png",
|
|
209
|
+
"sha256": "da0fadd6fd3ab5ccfbac94ca4e3f2b48e440987eee6834f1c30f04c02abd5d11"
|
|
210
|
+
},
|
|
211
|
+
"collected_id": "ref-40ece5312c4ecbf6",
|
|
212
|
+
"judgement_digest": "d2b162cb00ce8cc52049f321226a89e4e0328b55d54fb022fb4a4539088882c0",
|
|
213
|
+
"identity": {
|
|
214
|
+
"candidates": [
|
|
215
|
+
{
|
|
216
|
+
"item_id": "p1",
|
|
217
|
+
"name": "小爱音箱 Pro",
|
|
218
|
+
"dimensions_mm": {
|
|
219
|
+
"width": 118,
|
|
220
|
+
"depth": 118,
|
|
221
|
+
"height": 215
|
|
222
|
+
},
|
|
223
|
+
"quote": {
|
|
224
|
+
"page_id": "p1",
|
|
225
|
+
"text": "产品尺寸:118 × 118 × 215 mm"
|
|
226
|
+
},
|
|
227
|
+
"ratio_check": {
|
|
228
|
+
"status": "consistent",
|
|
229
|
+
"measured": 1.8188,
|
|
230
|
+
"spec": 1.822,
|
|
231
|
+
"relative_error": 0.0018,
|
|
232
|
+
"tolerance": 0.1
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"item_id": "p2",
|
|
237
|
+
"name": "小米AI音箱",
|
|
238
|
+
"dimensions_mm": {
|
|
239
|
+
"width": 88,
|
|
240
|
+
"depth": 88,
|
|
241
|
+
"height": 211.6
|
|
242
|
+
},
|
|
243
|
+
"quote": {
|
|
244
|
+
"page_id": "p2",
|
|
245
|
+
"text": "尺寸 88 mm × 88 mm × 211.6 mm"
|
|
246
|
+
},
|
|
247
|
+
"ratio_check": {
|
|
248
|
+
"status": "mismatch",
|
|
249
|
+
"measured": 1.8188,
|
|
250
|
+
"spec": 2.4045,
|
|
251
|
+
"relative_error": 0.2436,
|
|
252
|
+
"tolerance": 0.1
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"chosen_item_id": "p1",
|
|
257
|
+
"conflict": null
|
|
258
|
+
},
|
|
259
|
+
"similar": [],
|
|
260
|
+
"spec_quotes": [
|
|
261
|
+
{
|
|
262
|
+
"id": "sp1",
|
|
263
|
+
"quote": {
|
|
264
|
+
"page_id": "p1",
|
|
265
|
+
"text": "118 × 118 × 215 mm"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# 参考研究契约(reference.research / reference.finalize)
|
|
2
|
+
|
|
3
|
+
本文是 `abelien-reference` 两个 capability 的输入、输出、上限与错误码的正文。数据格式以 `schemas/reference-evidence.v1.json` 为准,样例见 `fixtures/`;判定逻辑在 `scripts/` 中,并有测试覆盖。
|
|
4
|
+
|
|
5
|
+
## reference.research
|
|
6
|
+
|
|
7
|
+
### 输入
|
|
8
|
+
|
|
9
|
+
只接受下列字段,出现其他字段一律 `VALIDATION`:
|
|
10
|
+
|
|
11
|
+
| 字段 | 规则 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `intent` | `restore` / `create` / `modify`,取自需求的 `intent.kind`(clarify 判定) |
|
|
14
|
+
| `keep` / `changeable` | 保留项与可改项,各最多 20 条,每条 1–500 字 |
|
|
15
|
+
| `depth` | `appearance` / `assembly` / `functional`,或 `null` |
|
|
16
|
+
| `text` | 用户原话,1–4000 字 |
|
|
17
|
+
| `links` | 最多 5 个公开 HTTPS 链接,不得带凭据或指向内网 |
|
|
18
|
+
| `user_image` | 可选,`{media_type: "image/png", base64}`,不超过 16MB;其他格式由宿主先转成 PNG |
|
|
19
|
+
| `plan.queries` | 最多 6 条,每条为 `{purpose: identify / teardown / similar, query, count?: 1–5}` |
|
|
20
|
+
| `plan.max_pages` / `plan.max_images` | 页面最多读几页(0–12,默认 8)、主图最多抓几张(0–8,默认 6) |
|
|
21
|
+
|
|
22
|
+
### 执行
|
|
23
|
+
|
|
24
|
+
- **检索**:只用宿主环境变量 `WEB_SEARCH_API_KEY` 调 Volc。原始回执先脱敏,再计算摘要。单条检索失败时,该条记为 `failed`,并写明错误码,其余照常进行;缺少 key 时,在发出任何请求之前就以 `REFERENCE_WEB_CONFIG` 失败。
|
|
25
|
+
- **页面**:先读链接,再从各条检索结果中轮流取页面,避免某一条检索占满读取预算。读取只走内核的安全直读:只读公网 HTTPS,每一跳都重新解析并钉住地址,限制重定向次数。每页最多读 8MB,超出时记为 `too_large`、不可用。正文存为内容 blob,资源里只保留前 1200 字的摘录。
|
|
26
|
+
- **主图**:取页面的 `og:image`、`twitter:image` 或 `image_src`。图片同样走钉住地址的直读,每张不超过 4MB,只接受 PNG、JPEG、WebP,原始字节存为内容 blob。其中的 PNG 会额外测量轮廓。
|
|
27
|
+
- **用户原图**:存为内容 blob 并测量轮廓(`silhouette-v1`),得到包围盒和高宽比;背景必须是单色或透明。背景杂乱、没有主体、隔行 PNG、超大图都记为 `unknown`,并写明原因。
|
|
28
|
+
- **幂等**:结果 id 由完整请求摘要(包括原图哈希)派生。已有同 id 的收集结果时直接返回,不再检索、不再读取。因此单条检索或页面的临时失败不会因为原样重试而自动恢复;要重试,就改写检索计划(比如换个检索词、改结果数),形成新的请求。
|
|
29
|
+
|
|
30
|
+
### 输出
|
|
31
|
+
|
|
32
|
+
输出 `reference-evidence`,status=collected。`items` 中第一项是 `user_input`,其后每个页面对应一格:识别用途为 `candidate`,逆向用途为 `teardown`,相似用途为 `similar`。读不到的页面状态为 `unavailable`。此时还没有任何 `verdict`。
|
|
33
|
+
|
|
34
|
+
`budget` 记录本次用量和截断:`queries`、`pages`、`images` 是已检索条数、已读页面数、已抓主图数;`skipped_pages` 是检索到、但因 `max_pages` 没有读取的去重公开页面数,`skipped_images` 是有主图、但因 `max_images` 没有抓取的页面数。宿主据此说明「已达本阶段检索上限」,不能把没读到当作没有。final 结果原样带上收集时的 `budget`。
|
|
35
|
+
|
|
36
|
+
请求摘要包含证据格式版本(`EVIDENCE_SHAPE`,当前为 2)。旧版本 harness 收集的证据保留原 id,不会被复用,也不会被覆盖;同一请求按新格式重新收集,得到新 id。
|
|
37
|
+
|
|
38
|
+
## reference.finalize
|
|
39
|
+
|
|
40
|
+
### 输入
|
|
41
|
+
|
|
42
|
+
| 字段 | 规则 |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `evidence_id` | 必须指向一份 status=collected 的 `reference-evidence:ref-…` |
|
|
45
|
+
| `identity` | 可选,`{candidates: [{item_id, name, dimensions_mm?: {width, depth?, height}, quote: {page_id, text}}], chosen_item_id}`,最多 6 个候选;只要有候选,`chosen_item_id` 就必须是其中之一。引用是必填的识别依据;页面没有给出尺寸时,可以不填 `dimensions_mm`,此时比例记为未核对 |
|
|
46
|
+
| `similar` | 可选,最多 12 条,每条为 `{item_id, decision: borrow / skip, borrowed?, why, not_copied?}`;`borrow` 时必须写明 `borrowed` |
|
|
47
|
+
| `excluded` | 可选,最多 12 条,每条为 `{item_id, reason}` |
|
|
48
|
+
| `specs` | 可选,最多 12 条,每条为 `{title, value, quote}`,生成 `spec` 格 |
|
|
49
|
+
|
|
50
|
+
### 核验
|
|
51
|
+
|
|
52
|
+
- 引用经过空白归一后,必须逐字出现在该页已保存的正文里。只能引用读取成功的页面。
|
|
53
|
+
- 给出尺寸时,宽、高(以及给出时的深)这些数字必须都出现在引用里;没给尺寸时,比例核对为 `unknown`(`no_dimensions`),并写入 `unknowns`。
|
|
54
|
+
- 条目种类必须匹配:候选必须是 `candidate` 格,相似必须是 `similar` 格;排除不能作用于 `user_input`。
|
|
55
|
+
|
|
56
|
+
### 判定(确定性)
|
|
57
|
+
|
|
58
|
+
- 正面视角下,比较高宽比:规格比例为 `height / width`,与用户原图的实测值比较,相对误差不超过 10% 记为 `consistent`,否则记为 `mismatch`;没有测量或缺尺寸时记为 `unknown`。
|
|
59
|
+
- 所选候选:比例一致或无法核对时,判定为 `closest`,依据中写明比例情况;比例不符时,判定为 `ratio_mismatch`,并在 `identity.conflict` 中写明。
|
|
60
|
+
- 其他候选:比例不符时判定为 `ratio_mismatch`,其余不加判定,仍保留为候选,供用户切换。
|
|
61
|
+
- 相似产品:`borrow` 判定为 `borrowed`,依据中写明借用了什么、原因、哪些不照搬;`skip` 判定为 `not_copied`。
|
|
62
|
+
- 排除项:状态改为 `excluded`,判定为 `excluded`。
|
|
63
|
+
- 同一份收集结果可以多次提交不同判断,每次生成一份新的 final 结果(id 带判断摘要),收集结果本身不变。
|
|
64
|
+
|
|
65
|
+
## 错误码
|
|
66
|
+
|
|
67
|
+
| 码 | 含义 |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `VALIDATION` | 输入不合规、引用不在原文、尺寸不在引用中、条目种类不符 |
|
|
70
|
+
| `REFERENCE_WEB_CONFIG` | 缺少 `WEB_SEARCH_API_KEY`,或 DNS 配置非法 |
|
|
71
|
+
| `REFERENCE_WEB_*` | 单条检索、读取的失败码,只记在对应的检索、页面或图片里,不使整次运行失败 |
|
|
72
|
+
| `REFERENCE_HOST` | 宿主没有提供可信的资源读取、内容读写端口 |
|
|
73
|
+
|
|
74
|
+
## 宿主责任
|
|
75
|
+
|
|
76
|
+
- 把上传的图片转成 PNG,写入作品目录中的输入文件,再用 `--input-file` 传入。
|
|
77
|
+
- 只展示 `items` 和 `unknowns`;`verdict` 原样展示,不重新解释,也不改写。
|
|
78
|
+
- 第三方图片从内容 blob 转存到私有存储,展示时附来源,不进入图鉴或公开分享。
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://git.abelien.cn/abelien/abelienharness/skills/abelien-reference/schemas/reference-evidence.v1.json",
|
|
4
|
+
"title": "abelien.reference-evidence.v1",
|
|
5
|
+
"description": "参考研究证据。status=collected 由 reference.research 产出,只有收集结果;status=final 由 reference.finalize 产出,带核验过的识别、相似产品与判定。宿主只展示与转存,不自行改写判定。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "schema", "status", "intent", "keep", "changeable", "depth", "text", "user_image", "searches", "pages", "images", "items", "unknowns", "budget"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": { "type": "string", "pattern": "^ref-[0-9a-f]{16}(-f[0-9a-f]{8})?$" },
|
|
10
|
+
"schema": { "const": "abelien.reference-evidence.v1" },
|
|
11
|
+
"status": { "enum": ["collected", "final"] },
|
|
12
|
+
"request_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
13
|
+
"intent": { "enum": ["restore", "create", "modify"] },
|
|
14
|
+
"keep": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
15
|
+
"changeable": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
16
|
+
"depth": { "enum": ["appearance", "assembly", "functional", null] },
|
|
17
|
+
"text": { "type": "string", "minLength": 1 },
|
|
18
|
+
"user_image": {
|
|
19
|
+
"oneOf": [
|
|
20
|
+
{ "type": "null" },
|
|
21
|
+
{
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["blob_ref", "sha256", "media_type", "measurement"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"blob_ref": { "$ref": "#/$defs/blobRef" },
|
|
26
|
+
"sha256": { "$ref": "#/$defs/sha256" },
|
|
27
|
+
"media_type": { "const": "image/png" },
|
|
28
|
+
"measurement": { "$ref": "#/$defs/measurement" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"searches": { "type": "array", "items": { "$ref": "#/$defs/search" } },
|
|
34
|
+
"pages": { "type": "array", "items": { "$ref": "#/$defs/page" } },
|
|
35
|
+
"images": { "type": "array", "items": { "$ref": "#/$defs/image" } },
|
|
36
|
+
"items": { "type": "array", "items": { "$ref": "#/$defs/item" } },
|
|
37
|
+
"unknowns": { "type": "array", "items": { "type": "string" } },
|
|
38
|
+
"budget": {
|
|
39
|
+
"description": "本次研究的用量:已检索条数、已读页面数、已抓主图数,以及因计划上限没有读取的去重公开页面数与主图数;宿主据此说明「已达检索上限」,不把没读到当作没有",
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["queries", "pages", "images", "skipped_pages", "skipped_images"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"queries": { "type": "integer", "minimum": 0 },
|
|
44
|
+
"pages": { "type": "integer", "minimum": 0 },
|
|
45
|
+
"images": { "type": "integer", "minimum": 0 },
|
|
46
|
+
"skipped_pages": { "type": "integer", "minimum": 0 },
|
|
47
|
+
"skipped_images": { "type": "integer", "minimum": 0 }
|
|
48
|
+
},
|
|
49
|
+
"additionalProperties": false
|
|
50
|
+
},
|
|
51
|
+
"collected_id": { "type": "string", "description": "status=final 时为来源收集结果的 id" },
|
|
52
|
+
"judgement_digest": { "type": "string" },
|
|
53
|
+
"identity": {
|
|
54
|
+
"description": "status=final;还原与改现成时为识别判断,新创时为 null",
|
|
55
|
+
"oneOf": [
|
|
56
|
+
{ "type": "null" },
|
|
57
|
+
{
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": ["candidates", "chosen_item_id", "conflict"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"candidates": { "type": "array", "items": { "$ref": "#/$defs/candidate" } },
|
|
62
|
+
"chosen_item_id": { "type": ["string", "null"] },
|
|
63
|
+
"conflict": { "type": ["string", "null"] }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"similar": { "type": "array", "items": { "$ref": "#/$defs/similar" } }
|
|
69
|
+
},
|
|
70
|
+
"$defs": {
|
|
71
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
72
|
+
"blobRef": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
|
|
73
|
+
"measurement": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"required": ["status", "method"],
|
|
76
|
+
"properties": {
|
|
77
|
+
"status": { "enum": ["measured", "unknown"] },
|
|
78
|
+
"method": { "const": "silhouette-v1" },
|
|
79
|
+
"reason": { "type": "string" },
|
|
80
|
+
"aspect_h_over_w": { "type": "number" },
|
|
81
|
+
"bbox": { "type": "object" },
|
|
82
|
+
"background": { "type": "object" }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"search": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"required": ["id", "purpose", "query", "count", "status", "results"],
|
|
88
|
+
"properties": {
|
|
89
|
+
"purpose": { "enum": ["identify", "teardown", "similar"] },
|
|
90
|
+
"status": { "enum": ["completed", "failed"] },
|
|
91
|
+
"error_code": { "type": ["string", "null"] },
|
|
92
|
+
"results": { "type": "array", "items": { "type": "object", "required": ["rank", "url", "title"] } }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"page": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"required": ["id", "url", "purpose", "status"],
|
|
98
|
+
"properties": {
|
|
99
|
+
"status": { "enum": ["read", "unavailable"] },
|
|
100
|
+
"text_blob_ref": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/blobRef" }] },
|
|
101
|
+
"excerpt": { "type": "string" },
|
|
102
|
+
"image_url": { "type": ["string", "null"] }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"image": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["id", "url", "page_id", "status"],
|
|
108
|
+
"properties": {
|
|
109
|
+
"status": { "enum": ["read", "unavailable"] },
|
|
110
|
+
"blob_ref": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/blobRef" }] },
|
|
111
|
+
"media_type": { "type": ["string", "null"] }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"verdict": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"required": ["code", "basis"],
|
|
117
|
+
"properties": {
|
|
118
|
+
"code": { "enum": ["closest", "ratio_mismatch", "excluded", "borrowed", "not_copied", "fits", "envelope"] },
|
|
119
|
+
"basis": { "type": "string", "minLength": 1 }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"item": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"description": "画布网格中的一格。verdict 只由 reference.finalize 的确定性检查给出。",
|
|
125
|
+
"required": ["id", "kind", "title", "role", "source", "image", "verdict", "status", "query"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"id": { "type": "string", "minLength": 1 },
|
|
128
|
+
"kind": { "enum": ["user_input", "candidate", "teardown", "spec", "similar"] },
|
|
129
|
+
"title": { "type": "string" },
|
|
130
|
+
"role": { "type": "string" },
|
|
131
|
+
"source": { "oneOf": [{ "type": "null" }, { "type": "object", "required": ["url", "site"], "properties": { "url": { "type": "string" }, "site": { "type": "string" } } }] },
|
|
132
|
+
"image": { "oneOf": [{ "type": "null" }, { "type": "object", "required": ["blob_ref", "sha256", "media_type"], "properties": { "blob_ref": { "$ref": "#/$defs/blobRef" }, "sha256": { "$ref": "#/$defs/sha256" }, "media_type": { "type": "string" } } }] },
|
|
133
|
+
"verdict": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/verdict" }] },
|
|
134
|
+
"status": { "enum": ["pending", "ready", "excluded", "unavailable"] },
|
|
135
|
+
"query": { "type": ["string", "null"], "description": "pending 时为真实检索词" }
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"candidate": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"required": ["item_id", "name", "dimensions_mm", "quote", "ratio_check"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"dimensions_mm": { "description": "页面未给尺寸时为 null,此时比例核对为 unknown", "oneOf": [{ "type": "null" }, { "type": "object", "required": ["width", "height"] }] },
|
|
143
|
+
"quote": { "type": "object", "required": ["page_id", "text"] },
|
|
144
|
+
"ratio_check": { "type": "object", "required": ["status"], "properties": { "status": { "enum": ["consistent", "mismatch", "unknown"] } } }
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"similar": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"required": ["item_id", "decision", "why"],
|
|
150
|
+
"properties": { "decision": { "enum": ["borrow", "skip"] } }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// reference-evidence v1 的唯一字段定义与校验(P2-13.04)。schemas/reference-evidence.v1.json 与本文件的枚举保持一致,
|
|
2
|
+
// 由测试核对;宿主据 schema 与 fixtures 做契约测试,不自行解释字段含义。
|
|
3
|
+
import { createPublicReader } from "../../abelien-harness/scripts/web/public-read.mjs";
|
|
4
|
+
import { createVolcSearch } from "../../abelien-harness/scripts/web/volc.mjs";
|
|
5
|
+
import { redact } from "../../abelien-harness/scripts/web/redact.mjs";
|
|
6
|
+
import { failure, text, record } from "../../abelien-harness/scripts/web/common.mjs";
|
|
7
|
+
|
|
8
|
+
export const SCHEMA = "abelien.reference-evidence.v1";
|
|
9
|
+
export const INTENTS = Object.freeze(["restore", "create", "modify"]);
|
|
10
|
+
export const DEPTHS = Object.freeze(["appearance", "assembly", "functional"]);
|
|
11
|
+
export const PURPOSES = Object.freeze(["identify", "teardown", "similar"]);
|
|
12
|
+
export const ITEM_KINDS = Object.freeze(["user_input", "candidate", "teardown", "spec", "similar"]);
|
|
13
|
+
export const ITEM_STATUSES = Object.freeze(["pending", "ready", "excluded", "unavailable"]);
|
|
14
|
+
export const VERDICT_CODES = Object.freeze(["closest", "ratio_mismatch", "excluded", "borrowed", "not_copied", "fits", "envelope"]);
|
|
15
|
+
export const EVIDENCE_STATUSES = Object.freeze(["collected", "final"]);
|
|
16
|
+
export const KIND_BY_PURPOSE = Object.freeze({ identify: "candidate", teardown: "teardown", similar: "similar" });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Version of the collected shape, part of every request digest: evidence collected by an older harness (e.g. without
|
|
20
|
+
* budget.skipped_*) keeps its own id and is never reused or overwritten; the same request collects afresh under a new id.
|
|
21
|
+
*/
|
|
22
|
+
export const EVIDENCE_SHAPE = 2;
|
|
23
|
+
export const LIMITS = Object.freeze({ queries: 6, count: 5, pages: 12, images: 8, links: 5, textChars: 4000, queryChars: 200,
|
|
24
|
+
pageBytes: 8 * 1024 * 1024, imageBytes: 4 * 1024 * 1024, userImageBytes: 16 * 1024 * 1024, excerptChars: 1200, pageTimeoutMs: 30000, searchTimeoutMs: 30000, concurrency: 4 });
|
|
25
|
+
|
|
26
|
+
export const CODES = Object.freeze({ transport: "REFERENCE_WEB_TRANSPORT", dns: "REFERENCE_WEB_DNS", access: "REFERENCE_WEB_URL", redirect: "REFERENCE_WEB_PAGE",
|
|
27
|
+
timeout: "REFERENCE_WEB_TIMEOUT", url: "REFERENCE_WEB_URL", config: "REFERENCE_WEB_CONFIG", readTimeout: "REFERENCE_PUBLIC_READ_TIMEOUT",
|
|
28
|
+
capability: "REFERENCE_WEB_CAPABILITY", provider: "REFERENCE_WEB_PROVIDER", auth: "REFERENCE_WEB_AUTH", rateLimit: "REFERENCE_WEB_RATE_LIMIT", response: "REFERENCE_WEB_RESPONSE" });
|
|
29
|
+
|
|
30
|
+
export const reader = createPublicReader({ codes: CODES, userAgent: "Abelien-Reference-Evidence/1", accept: "text/html,text/plain,application/xhtml+xml", decodeCharset: true });
|
|
31
|
+
export const imageReader = createPublicReader({ codes: CODES, userAgent: "Abelien-Reference-Evidence/1", accept: "image/png,image/jpeg,image/webp" });
|
|
32
|
+
export const volc = createVolcSearch({ codes: CODES, ownsError: error => Boolean(error?.code?.startsWith("REFERENCE_WEB_")),
|
|
33
|
+
redact: (raw, env = {}) => redact(raw, [env.WEB_SEARCH_API_KEY].filter(text)), timeoutMessage: "reference network request timed out" });
|
|
34
|
+
|
|
35
|
+
export const invalid = message => { throw failure("VALIDATION", message); };
|
|
36
|
+
export const stringList = (value, label, max = 20) => {
|
|
37
|
+
if (!Array.isArray(value) || value.length > max || value.some(item => !text(item) || item.length > 500)) invalid(`${label} must be a list of up to ${max} non-empty strings`);
|
|
38
|
+
return value.map(item => item.trim());
|
|
39
|
+
};
|
|
40
|
+
export const site = url => { try { return new URL(url).hostname; } catch { return null; } };
|
|
41
|
+
|
|
42
|
+
function checkItem(item, index) {
|
|
43
|
+
const at = `items[${index}]`;
|
|
44
|
+
if (!record(item) || !text(item.id) || !ITEM_KINDS.includes(item.kind) || !ITEM_STATUSES.includes(item.status) || typeof item.title !== "string" || typeof item.role !== "string") invalid(`${at} has invalid id, kind, status, title or role`);
|
|
45
|
+
if (item.source !== null && !(record(item.source) && text(item.source.url) && typeof item.source.site === "string")) invalid(`${at}.source must be null or {url, site}`);
|
|
46
|
+
if (item.image !== null && !(record(item.image) && /^sha256:[0-9a-f]{64}$/.test(item.image.blob_ref ?? "") && /^[0-9a-f]{64}$/.test(item.image.sha256 ?? "") && text(item.image.media_type))) invalid(`${at}.image must be null or {blob_ref, sha256, media_type}`);
|
|
47
|
+
if (item.verdict !== null && !(record(item.verdict) && VERDICT_CODES.includes(item.verdict.code) && text(item.verdict.basis))) invalid(`${at}.verdict must be null or {code, basis}`);
|
|
48
|
+
if (item.query !== null && typeof item.query !== "string") invalid(`${at}.query must be null or a string`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Strict structural check of a reference-evidence v1 body; returns the body. */
|
|
52
|
+
export function validateEvidence(body) {
|
|
53
|
+
if (!record(body) || body.schema !== SCHEMA || !text(body.id) || !EVIDENCE_STATUSES.includes(body.status)) invalid("not a reference-evidence v1 body");
|
|
54
|
+
if (!INTENTS.includes(body.intent) || (body.depth !== null && !DEPTHS.includes(body.depth))) invalid("intent or depth is invalid");
|
|
55
|
+
stringList(body.keep, "keep"); stringList(body.changeable, "changeable"); stringList(body.unknowns, "unknowns", 200);
|
|
56
|
+
for (const name of ["searches", "pages", "images", "items"]) if (!Array.isArray(body[name])) invalid(`${name} must be an array`);
|
|
57
|
+
const budget = body.budget, counts = ["queries", "pages", "images", "skipped_pages", "skipped_images"];
|
|
58
|
+
if (!record(budget) || Object.keys(budget).length !== counts.length
|
|
59
|
+
|| counts.some(key => !Number.isSafeInteger(budget[key]) || budget[key] < 0)) invalid(`budget must count ${counts.join(", ")}`);
|
|
60
|
+
body.items.forEach(checkItem);
|
|
61
|
+
const ids = body.items.map(item => item.id); if (new Set(ids).size !== ids.length) invalid("item ids must be unique");
|
|
62
|
+
if (body.status === "final") {
|
|
63
|
+
if (!record(body.identity) && body.identity !== null) invalid("identity must be null or an object");
|
|
64
|
+
if (!Array.isArray(body.similar)) invalid("similar must be an array");
|
|
65
|
+
if (!text(body.collected_id)) invalid("final evidence must name its collected_id");
|
|
66
|
+
}
|
|
67
|
+
return body;
|
|
68
|
+
}
|