made-market 0.4.3 → 0.4.5
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/lib/market.mjs +101 -4
- package/lib/plugin.mjs +4 -2
- package/package.json +2 -2
- package/registry/index.json +68 -14
- package/src/publish.mjs +1 -1
package/lib/market.mjs
CHANGED
|
@@ -29,10 +29,16 @@ export const KINDS = Object.freeze({
|
|
|
29
29
|
theme: { source: 'themes', target: 'themes', label: '主题', labelEn: 'Theme', icon: 'theme' },
|
|
30
30
|
control: { source: 'control', target: 'plugins', label: '管控', labelEn: 'Control', icon: 'control' },
|
|
31
31
|
agent: { source: 'agents', target: 'plugins', label: 'agent 运行时', labelEn: 'Agent runtime', icon: 'agent' },
|
|
32
|
+
h5: { source: 'h5', target: 'plugins', label: 'H5 应用', labelEn: 'H5 app', icon: 'grid' },
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
export const KIND_ORDER = Object.freeze(['theme', 'control', 'agent']);
|
|
35
|
+
export const KIND_ORDER = Object.freeze(['theme', 'control', 'agent', 'h5']);
|
|
35
36
|
export const PLUGIN_KINDS = Object.freeze({ control: 'board-plugin', agent: 'coding-agent' });
|
|
37
|
+
|
|
38
|
+
/** H5 插件能力白名单(与桥接器 h5-apps.mjs 同名单:桥接器是裁决点,市场只做预检)。 */
|
|
39
|
+
export const H5_CAPABILITIES = Object.freeze([
|
|
40
|
+
'state.read', 'tasks.read', 'tasks.write', 'sessions.read', 'sessions.write', 'settings.read',
|
|
41
|
+
]);
|
|
36
42
|
const PAYLOAD_IGNORED = new Set(['market.json']);
|
|
37
43
|
|
|
38
44
|
export const sha256 = (buffer) => crypto.createHash('sha256').update(buffer).digest('hex');
|
|
@@ -162,11 +168,60 @@ function validateSpriteDir(dir, fail, warn) {
|
|
|
162
168
|
if (!Number.isInteger(sprite.version) || sprite.version < 1) {
|
|
163
169
|
fail('sprite/manifest.json 的 version 必须是 ≥1 的整数(改素材必须递增,否则设备不更新)');
|
|
164
170
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
171
|
+
// 两种素材形态二选一(与桥接器 agent-sprites.mjs validateFramesTable 同规则):
|
|
172
|
+
// frames 整帧状态表(新,推荐)— 有它就不需要 layers;layers 分层叠加(老)。
|
|
168
173
|
const pngs = fs.readdirSync(spriteDir).filter((name) => name.endsWith('.png'));
|
|
169
174
|
const referenced = new Set();
|
|
175
|
+
const FRAME_STATES = new Set(['idle', 'talking', 'running', 'waiting', 'success', 'sad', 'offline', 'blink']);
|
|
176
|
+
const frameTable = sprite.frames;
|
|
177
|
+
const isFramesForm = frameTable !== undefined;
|
|
178
|
+
if (isFramesForm) {
|
|
179
|
+
if (!frameTable || typeof frameTable !== 'object' || Array.isArray(frameTable)) {
|
|
180
|
+
fail('sprite/manifest.json 的 frames 必须是对象');
|
|
181
|
+
} else {
|
|
182
|
+
let total = 0;
|
|
183
|
+
const collect = (state, entry) => {
|
|
184
|
+
let files = null;
|
|
185
|
+
if (Array.isArray(entry)) {
|
|
186
|
+
files = entry;
|
|
187
|
+
} else if (entry && typeof entry === 'object') {
|
|
188
|
+
if (Object.keys(entry).some((key) => !['frames', 'ms', 'period_ms', 'hold_ms'].includes(key))) {
|
|
189
|
+
fail(`sprite/manifest.json 的 frames.${state} 含不支持的字段(只允许 frames/ms/period_ms/hold_ms)`);
|
|
190
|
+
}
|
|
191
|
+
if (!Array.isArray(entry.frames)) fail(`sprite/manifest.json 的 frames.${state}.frames 必须是数组`);
|
|
192
|
+
files = entry.frames || [];
|
|
193
|
+
} else {
|
|
194
|
+
fail(`sprite/manifest.json 的 frames.${state} 必须是数组或对象`);
|
|
195
|
+
}
|
|
196
|
+
for (const file of files || []) {
|
|
197
|
+
if (typeof file !== 'string') {
|
|
198
|
+
fail('sprite/manifest.json 的 frames 素材字段必须是字符串');
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
referenced.add(file);
|
|
202
|
+
if (!/^[A-Za-z0-9_-]{1,16}\.png$/i.test(file)) {
|
|
203
|
+
// 固件把素材平铺缓存在 SPIFFS(对象名上限 32 字符),文件名必须短。
|
|
204
|
+
fail(`sprite/${file} 文件名不合规:1–16 个字母/数字/下划线/连字符 + .png(总长 ≤20)`);
|
|
205
|
+
}
|
|
206
|
+
if (!pngs.includes(file)) fail(`sprite/manifest.json 引用了不存在的 ${file}`);
|
|
207
|
+
}
|
|
208
|
+
total += (files || []).length;
|
|
209
|
+
if (total > 12) fail('sprite/manifest.json 的 frames 总帧数超过 12(设备内存上限)');
|
|
210
|
+
};
|
|
211
|
+
for (const [state, entry] of Object.entries(frameTable)) {
|
|
212
|
+
if (!FRAME_STATES.has(state)) fail(`sprite/manifest.json 的 frames 含未知状态:${state}(可用:idle/talking/running/waiting/success/sad/offline/blink)`);
|
|
213
|
+
collect(state, entry);
|
|
214
|
+
}
|
|
215
|
+
if (!Object.keys(frameTable).length || !frameTable.idle) {
|
|
216
|
+
fail('sprite/manifest.json 的 frames 至少要声明 idle 轨道(未声明的状态回落到它)');
|
|
217
|
+
}
|
|
218
|
+
if (sprite.frame_ms !== undefined && (!Number.isInteger(sprite.frame_ms) || sprite.frame_ms < 30 || sprite.frame_ms > 2000)) {
|
|
219
|
+
fail('sprite/manifest.json 的 frame_ms 范围 30–2000');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else if (!Array.isArray(sprite.layers) || !sprite.layers.length) {
|
|
223
|
+
fail('sprite/manifest.json 的 layers 必须是非空数组(或改用 frames 整帧状态表)');
|
|
224
|
+
}
|
|
170
225
|
for (const layer of sprite.layers || []) {
|
|
171
226
|
for (const file of layer.frames || (layer.file ? [layer.file] : [])) {
|
|
172
227
|
if (typeof file !== 'string') {
|
|
@@ -212,6 +267,48 @@ export function validatePackage(pkg) { const errors = [];
|
|
|
212
267
|
entry = 'theme.json';
|
|
213
268
|
checkTheme(pkg.dir, dir, fail, warn);
|
|
214
269
|
if (!fs.existsSync(path.join(pkg.dir, 'theme.json'))) return { ...empty, installDir: dir };
|
|
270
|
+
} else if (pkg.kind === 'h5') {
|
|
271
|
+
entry = 'plugin.json';
|
|
272
|
+
if (pkg.market.pluginId !== undefined) warn('H5 包不应写 pluginId');
|
|
273
|
+
const manifestPath = path.join(pkg.dir, 'plugin.json');
|
|
274
|
+
if (!fs.existsSync(manifestPath)) {
|
|
275
|
+
fail('H5 包缺少 plugin.json');
|
|
276
|
+
return { ...empty, installDir: dir };
|
|
277
|
+
}
|
|
278
|
+
let manifest = null;
|
|
279
|
+
try {
|
|
280
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
281
|
+
} catch {
|
|
282
|
+
fail('plugin.json 不是合法 JSON');
|
|
283
|
+
return { ...empty, installDir: dir };
|
|
284
|
+
}
|
|
285
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
286
|
+
fail('plugin.json 顶层必须是对象');
|
|
287
|
+
return { ...empty, installDir: dir };
|
|
288
|
+
}
|
|
289
|
+
if (Buffer.byteLength(fs.readFileSync(manifestPath)) > 16 * 1024) fail('plugin.json 超过 16KB');
|
|
290
|
+
if (manifest.kind !== 'h5') fail(`plugin.json 的 kind 必须是 h5,收到 ${manifest.kind ?? '(空)'}`);
|
|
291
|
+
if (manifest.id !== pkg.id) fail(`plugin.json 的 id (${manifest.id ?? '(空)'}) 与包 ID ${pkg.id} 不一致`);
|
|
292
|
+
else if (!PACKAGE_DIR.test(manifest.id)) fail(`plugin.json 的 id ${manifest.id} 不符合命名规则`);
|
|
293
|
+
const www = typeof manifest.www === 'string' && manifest.www
|
|
294
|
+
? (manifest.www.endsWith('/') ? manifest.www : `${manifest.www}/`) : 'www/';
|
|
295
|
+
if (/(^|\/)\.\.(\/|$)/.test(www) || www.includes('\\') || path.isAbsolute(www) || !/^[A-Za-z0-9_][A-Za-z0-9_/-]*$/.test(www)) {
|
|
296
|
+
fail(`plugin.json 的 www 目录不安全:${www}`);
|
|
297
|
+
} else if (!fs.existsSync(path.join(pkg.dir, www, 'index.html'))) {
|
|
298
|
+
fail(`H5 包缺少入口页 ${www}index.html`);
|
|
299
|
+
}
|
|
300
|
+
if (manifest.capabilities !== undefined) {
|
|
301
|
+
if (!Array.isArray(manifest.capabilities) || manifest.capabilities.length > 8
|
|
302
|
+
|| manifest.capabilities.some((name) => !H5_CAPABILITIES.includes(name))) {
|
|
303
|
+
fail(`plugin.json 的 capabilities 必须是 ${H5_CAPABILITIES.join(' / ')} 的子集(≤8 项)`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const extra = Object.keys(manifest).filter((key) => !['id', 'kind', 'name', 'www', 'capabilities', 'settings'].includes(key));
|
|
307
|
+
if (extra.length) fail(`plugin.json 含不支持的键:${extra.join(', ')}`);
|
|
308
|
+
if (manifest.settings !== undefined) {
|
|
309
|
+
// 设置项规范与助手包同一套(桥接器 parseSettingsSpec 是裁决点);市场侧用同一解析器预检。
|
|
310
|
+
parseSettingsSpec({ id: manifest.id, settings: manifest.settings }, fail);
|
|
311
|
+
}
|
|
215
312
|
} else {
|
|
216
313
|
const source = ['plugin.mjs', 'plugin.js'].find((name) => fs.existsSync(path.join(pkg.dir, name)));
|
|
217
314
|
if (!source) {
|
package/lib/plugin.mjs
CHANGED
|
@@ -44,8 +44,10 @@ export function isKnownCommand(name, known) {
|
|
|
44
44
|
return name.startsWith('action.') || known.includes(name);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Event and command names share one shape, mirroring the bridge validator.
|
|
48
|
-
|
|
47
|
+
/** Event and command names share one shape, mirroring the bridge validator.
|
|
48
|
+
* Uppercase segments carry the abstract action slots (`action.A1`–`A9`, the
|
|
49
|
+
* two-level binding model in docs/SPRITES.md); legacy names stay lowercase. */
|
|
50
|
+
const NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,15}(?:\.[a-zA-Z][a-zA-Z0-9_]{0,15}){0,2}$/;
|
|
49
51
|
|
|
50
52
|
const visible = (value, max) => typeof value === 'string' && value.trim()
|
|
51
53
|
&& Buffer.byteLength(value) <= max && !/[\p{Cc}\p{Cf}]/u.test(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "made-market",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "码得(made)公开插件市场的命令行工具:列出、搜索、安装主题/管控/agent 包,以及登录后发布新包。市场服务端在 github.com/jobop/made-market 仓库内,不随本包分发。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"prepublishOnly": "cd .. && node tools/validate.mjs && node tools/build.mjs && node --test",
|
|
46
46
|
"prepack": "mkdir -p registry && cp ../registry/index.json registry/index.json"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|
package/registry/index.json
CHANGED
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
"packageHome": "~/made",
|
|
8
8
|
"packageHomeEnv": "MADE_HOME",
|
|
9
9
|
"counts": {
|
|
10
|
-
"total":
|
|
10
|
+
"total": 12,
|
|
11
11
|
"theme": 4,
|
|
12
12
|
"control": 4,
|
|
13
|
-
"agent": 3
|
|
13
|
+
"agent": 3,
|
|
14
|
+
"h5": 1
|
|
14
15
|
},
|
|
15
16
|
"installTargets": {
|
|
16
17
|
"theme": "~/made/themes/",
|
|
17
18
|
"control": "~/made/plugins/",
|
|
18
|
-
"agent": "~/made/plugins/"
|
|
19
|
+
"agent": "~/made/plugins/",
|
|
20
|
+
"h5": "~/made/plugins/"
|
|
19
21
|
},
|
|
20
22
|
"packages": [
|
|
21
23
|
{
|
|
@@ -80,7 +82,7 @@
|
|
|
80
82
|
],
|
|
81
83
|
"payloadBytes": 209717,
|
|
82
84
|
"archiveBytes": 39822,
|
|
83
|
-
"sha256": "
|
|
85
|
+
"sha256": "b656724a6be3466c0ad52010bfa9f2a3b043595b6b7b6bed90a01bdfcca3ba95",
|
|
84
86
|
"download": "packages/sunset-1.0.0.tar.gz",
|
|
85
87
|
"swatch": [
|
|
86
88
|
"#e0834a",
|
|
@@ -153,7 +155,7 @@
|
|
|
153
155
|
],
|
|
154
156
|
"payloadBytes": 212917,
|
|
155
157
|
"archiveBytes": 44910,
|
|
156
|
-
"sha256": "
|
|
158
|
+
"sha256": "3894aadcfdba77eb351abc4381d3b76141ed0d6a15d8b75a982a64092e1300fa",
|
|
157
159
|
"download": "packages/ocean-1.0.0.tar.gz",
|
|
158
160
|
"swatch": [
|
|
159
161
|
"#2fa3b8",
|
|
@@ -226,7 +228,7 @@
|
|
|
226
228
|
],
|
|
227
229
|
"payloadBytes": 198517,
|
|
228
230
|
"archiveBytes": 31125,
|
|
229
|
-
"sha256": "
|
|
231
|
+
"sha256": "ff66fd400882e4ce3017b32782825aca59e60e5a407c793b175d2c4500c1cb97",
|
|
230
232
|
"download": "packages/graphite-1.0.0.tar.gz",
|
|
231
233
|
"swatch": [
|
|
232
234
|
"#8e9aa6",
|
|
@@ -299,7 +301,7 @@
|
|
|
299
301
|
],
|
|
300
302
|
"payloadBytes": 206517,
|
|
301
303
|
"archiveBytes": 34163,
|
|
302
|
-
"sha256": "
|
|
304
|
+
"sha256": "24dbf64bb96520a16efbfbb553ccff91190ae4fde5b18444f3c6a7ee97b0993f",
|
|
303
305
|
"download": "packages/bamboo-1.0.0.tar.gz",
|
|
304
306
|
"swatch": [
|
|
305
307
|
"#5aa85e",
|
|
@@ -367,7 +369,7 @@
|
|
|
367
369
|
],
|
|
368
370
|
"payloadBytes": 1576,
|
|
369
371
|
"archiveBytes": 1067,
|
|
370
|
-
"sha256": "
|
|
372
|
+
"sha256": "c3f6afae9ef5f47b622c74c2faf7174ba2ba38a9db2d19651a992874fd6f5f9a",
|
|
371
373
|
"download": "packages/theme-cycler-1.0.0.tar.gz",
|
|
372
374
|
"sourcePath": "packages/control/theme-cycler"
|
|
373
375
|
},
|
|
@@ -430,7 +432,7 @@
|
|
|
430
432
|
],
|
|
431
433
|
"payloadBytes": 3023,
|
|
432
434
|
"archiveBytes": 1808,
|
|
433
|
-
"sha256": "
|
|
435
|
+
"sha256": "ab7acdc88895a391596182fa9425e1e81d0b7ec5de73c035b9fe0ee2a7a3a123",
|
|
434
436
|
"download": "packages/webhook-notify-1.0.0.tar.gz",
|
|
435
437
|
"sourcePath": "packages/control/webhook-notify"
|
|
436
438
|
},
|
|
@@ -492,7 +494,7 @@
|
|
|
492
494
|
],
|
|
493
495
|
"payloadBytes": 2370,
|
|
494
496
|
"archiveBytes": 1450,
|
|
495
|
-
"sha256": "
|
|
497
|
+
"sha256": "43512f16e9559f5984ab9cf827b9b8e0e7c5671bc1cd1cf211adc0b0344e9f37",
|
|
496
498
|
"download": "packages/key-chime-1.0.0.tar.gz",
|
|
497
499
|
"sourcePath": "packages/control/key-chime"
|
|
498
500
|
},
|
|
@@ -554,7 +556,7 @@
|
|
|
554
556
|
],
|
|
555
557
|
"payloadBytes": 3940,
|
|
556
558
|
"archiveBytes": 1959,
|
|
557
|
-
"sha256": "
|
|
559
|
+
"sha256": "c131b7a0e67a9e0a1e4ab23b9dcf5ec05f7bb73c8b43621fe849c6762009c4e0",
|
|
558
560
|
"download": "packages/alarm-clock-1.0.0.tar.gz",
|
|
559
561
|
"sourcePath": "packages/control/alarm-clock"
|
|
560
562
|
},
|
|
@@ -621,7 +623,7 @@
|
|
|
621
623
|
],
|
|
622
624
|
"payloadBytes": 3793,
|
|
623
625
|
"archiveBytes": 1612,
|
|
624
|
-
"sha256": "
|
|
626
|
+
"sha256": "7f3d1045be427b2d4cbe0b070cb63ca0fc1fe127c63201f9423f23970d37f736",
|
|
625
627
|
"download": "packages/demo-echo-1.0.0.tar.gz",
|
|
626
628
|
"icon": {
|
|
627
629
|
"format": "indexed4",
|
|
@@ -702,7 +704,7 @@
|
|
|
702
704
|
],
|
|
703
705
|
"payloadBytes": 6098,
|
|
704
706
|
"archiveBytes": 2672,
|
|
705
|
-
"sha256": "
|
|
707
|
+
"sha256": "0a33b62f6f186a80d370012ddd146f9c504daf182503cc23cb4c13bd9c7e35d3",
|
|
706
708
|
"download": "packages/ollama-agent-1.0.0.tar.gz",
|
|
707
709
|
"icon": {
|
|
708
710
|
"format": "indexed4",
|
|
@@ -786,7 +788,7 @@
|
|
|
786
788
|
],
|
|
787
789
|
"payloadBytes": 5814,
|
|
788
790
|
"archiveBytes": 2456,
|
|
789
|
-
"sha256": "
|
|
791
|
+
"sha256": "541eef0c992b636cfe5d0b8f1f03dffa15a58b414f061ed392971bd3723a6c0f",
|
|
790
792
|
"download": "packages/shell-agent-1.0.0.tar.gz",
|
|
791
793
|
"icon": {
|
|
792
794
|
"format": "indexed4",
|
|
@@ -807,6 +809,58 @@
|
|
|
807
809
|
"accent": "#7fe6c4"
|
|
808
810
|
},
|
|
809
811
|
"sourcePath": "packages/agents/shell-agent"
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
"id": "demo-hello",
|
|
815
|
+
"kind": "h5",
|
|
816
|
+
"version": "1.0.0",
|
|
817
|
+
"name": "H5 示例页",
|
|
818
|
+
"nameEn": "H5 hello demo",
|
|
819
|
+
"summary": "纯静态 H5 插件最小示例:零能力声明,装完在手机上打开一个页面。",
|
|
820
|
+
"summaryEn": "Minimal static H5 plugin: no capabilities, just a page you open on the phone.",
|
|
821
|
+
"author": "jobop",
|
|
822
|
+
"license": "MIT",
|
|
823
|
+
"tags": [
|
|
824
|
+
"示例",
|
|
825
|
+
"H5"
|
|
826
|
+
],
|
|
827
|
+
"glyph": "grid",
|
|
828
|
+
"highlights": [
|
|
829
|
+
"零能力声明:不调任何桥接器接口",
|
|
830
|
+
"H5 插件最小结构参考(market.json + plugin.json + www/)"
|
|
831
|
+
],
|
|
832
|
+
"requires": {
|
|
833
|
+
"apiVersion": 1
|
|
834
|
+
},
|
|
835
|
+
"publishedAt": "2026-11-04",
|
|
836
|
+
"changelog": [
|
|
837
|
+
{
|
|
838
|
+
"version": "1.0.0",
|
|
839
|
+
"notes": "首个版本",
|
|
840
|
+
"notesEn": "First release"
|
|
841
|
+
}
|
|
842
|
+
],
|
|
843
|
+
"dir": "demo-hello",
|
|
844
|
+
"target": "plugins",
|
|
845
|
+
"source": "packages/h5/demo-hello",
|
|
846
|
+
"entry": "plugin.json",
|
|
847
|
+
"files": [
|
|
848
|
+
{
|
|
849
|
+
"path": "plugin.json",
|
|
850
|
+
"size": 41,
|
|
851
|
+
"sha256": "a7df7b90150a8b135eecdd5464731a2db0766179a668d514046ab2027438f9aa"
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
"path": "www/index.html",
|
|
855
|
+
"size": 1262,
|
|
856
|
+
"sha256": "4f375a90122f64a026036de61ce7d71392bc281b81fbf367197876b5694dab7e"
|
|
857
|
+
}
|
|
858
|
+
],
|
|
859
|
+
"payloadBytes": 1303,
|
|
860
|
+
"archiveBytes": 1024,
|
|
861
|
+
"sha256": "537767ee43397e7a7374c952a4d6cc1ade383569a4c0874671b7fa4e5ff0aed1",
|
|
862
|
+
"download": "packages/demo-hello-1.0.0.tar.gz",
|
|
863
|
+
"sourcePath": "packages/h5/demo-hello"
|
|
810
864
|
}
|
|
811
865
|
]
|
|
812
866
|
}
|
package/src/publish.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export async function publish(dir, options = {}) {
|
|
|
34
34
|
throw new Error(`market.json 无法解析:${error.message}`);
|
|
35
35
|
}
|
|
36
36
|
const { kind, id } = market;
|
|
37
|
-
if (!KINDS[kind]) throw new Error(`market.json 的 kind 必须是 theme / control / agent 之一`);
|
|
37
|
+
if (!KINDS[kind]) throw new Error(`market.json 的 kind 必须是 theme / control / agent / h5 之一`);
|
|
38
38
|
const pkg = { id, kind, dir: root, market, parseError: '' };
|
|
39
39
|
const validation = validatePackage(pkg);
|
|
40
40
|
if (validation.errors.length) {
|