made-market 0.4.4 → 0.4.6
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/package.json +1 -1
- package/registry/index.json +57 -3
- package/src/account.mjs +2 -2
- 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/package.json
CHANGED
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
|
{
|
|
@@ -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/account.mjs
CHANGED
|
@@ -83,8 +83,8 @@ export async function login({ server, name, password, home = '' }) {
|
|
|
83
83
|
|
|
84
84
|
export async function whoami({ server, home = '', token = '' } = {}) {
|
|
85
85
|
const creds = loadCredentials(home);
|
|
86
|
-
const base = (server || creds?.server || '').replace(/\/+$/, '');
|
|
87
|
-
const bearer = token || creds?.token || '';
|
|
86
|
+
const base = (server || process.env.MADE_MARKET_SERVER || creds?.server || '').replace(/\/+$/, '');
|
|
87
|
+
const bearer = token || process.env.MADE_MARKET_TOKEN || creds?.token || '';
|
|
88
88
|
if (!base || !bearer) throw new Error('还没登录过:先 npx made-market login <用户名> --server <服务地址>');
|
|
89
89
|
const response = await fetch(`${base}/api/me`, { headers: { authorization: `Bearer ${bearer}` }, signal: AbortSignal.timeout(15000) });
|
|
90
90
|
const data = await response.json().catch(() => ({}));
|
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) {
|