made-market 0.0.0-stage → 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -2
  3. package/bin/made-market.mjs +297 -0
  4. package/docs/BACKEND.md +189 -0
  5. package/docs/INSTALL.md +95 -0
  6. package/docs/PUBLISH.md +127 -0
  7. package/docs/SPEC.md +119 -0
  8. package/package.json +56 -4
  9. package/registry/index.json +812 -0
  10. package/server/auth.mjs +121 -0
  11. package/server/db.mjs +485 -0
  12. package/server/errors.mjs +7 -0
  13. package/server/server.mjs +210 -0
  14. package/server/service.mjs +254 -0
  15. package/server/store.mjs +97 -0
  16. package/site/glyphs.js +22 -0
  17. package/site/index.html +158 -0
  18. package/site/market.css +915 -0
  19. package/site/market.js +521 -0
  20. package/site/packages/alarm-clock-1.0.0.tar.gz +0 -0
  21. package/site/packages/bamboo-1.0.0.tar.gz +0 -0
  22. package/site/packages/demo-echo-1.0.0.tar.gz +0 -0
  23. package/site/packages/graphite-1.0.0.tar.gz +0 -0
  24. package/site/packages/key-chime-1.0.0.tar.gz +0 -0
  25. package/site/packages/ocean-1.0.0.tar.gz +0 -0
  26. package/site/packages/ollama-agent-1.0.0.tar.gz +0 -0
  27. package/site/packages/shell-agent-1.0.0.tar.gz +0 -0
  28. package/site/packages/sunset-1.0.0.tar.gz +0 -0
  29. package/site/packages/theme-cycler-1.0.0.tar.gz +0 -0
  30. package/site/packages/webhook-notify-1.0.0.tar.gz +0 -0
  31. package/site/registry.js +813 -0
  32. package/tools/account.mjs +106 -0
  33. package/tools/build.mjs +79 -0
  34. package/tools/gen-assets.mjs +67 -0
  35. package/tools/install.mjs +181 -0
  36. package/tools/lib/assets.mjs +198 -0
  37. package/tools/lib/glyphs.mjs +25 -0
  38. package/tools/lib/icon.mjs +110 -0
  39. package/tools/lib/market.mjs +263 -0
  40. package/tools/lib/oss.mjs +75 -0
  41. package/tools/lib/pack.mjs +54 -0
  42. package/tools/lib/plugin.mjs +169 -0
  43. package/tools/lib/tar.mjs +78 -0
  44. package/tools/lib/theme.mjs +142 -0
  45. package/tools/new.mjs +126 -0
  46. package/tools/publish-oss.mjs +119 -0
  47. package/tools/publish.mjs +63 -0
  48. package/tools/serve.mjs +63 -0
  49. package/tools/validate.mjs +44 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jobop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # made-market · 码得公开插件市场
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 码得的公开插件市场。**只做包管理**:登记和分发三类包,本身不执行插件、不连接码得、不保存配对令牌或 hook 密钥。
4
+
5
+ 包装进固定的用户目录 `~/made`(可用 `MADE_HOME` 覆盖)之后才生效:桥接器每次启动扫描 `~/made/plugins` 与 `~/made/themes`,不需要登记,也不需要和配置文件放在一起。
6
+
7
+ | 类别 | 管什么 | 安装到 |
8
+ | --- | --- | --- |
9
+ | **主题** | 码得的外观:调色板、锁屏图、提示音 | `~/made/themes/<dir>/` |
10
+ | **管控** | 接收板侧事件、把命令放进设备队列、调外部 hook | `~/made/plugins/<dir>/` |
11
+ | **agent 运行时** | 接上一种助手的执行方式:会话、模型、进度、取消 | `~/made/plugins/<dir>/` |
12
+
13
+ 桥接器里另有一份随代码发布的内置包 `bridge/builtin/`;查找顺序是「内置在前、用户目录在后」,同名时 `~/made` 覆盖内置。
14
+
15
+ 技能不在本仓库建广场,沿用现成技能广场;人格归专家市场。边界与归类见码得仓库的 `docs/MARKETS.md`。
16
+
17
+ ## 安装一个包
18
+
19
+ ```sh
20
+ npx made-market install sunset # 主题
21
+ npx made-market install alarm-clock # 管控插件
22
+ npx made-market list # 看看有什么
23
+ ```
24
+
25
+ 包从哪里下载?索引解析顺序是 `--registry` > 环境变量 `MADE_MARKET_REGISTRY`(指向 OSS 上的公开索引)> npm 包内自带的索引快照。**权威分发源是 OSS**,日常安装走它;远端不可达时自动回落到包内快照,离线也能装。发布流程见 [docs/PUBLISH.md](docs/PUBLISH.md)。
26
+
27
+ ## 目录
28
+
29
+ ```
30
+ bin/ npx 入口(made-market)
31
+ packages/ 包源:themes/ control/ agents/,每包一个目录
32
+ registry/ 公开索引 index.json(生成物,是分发的唯一入口)
33
+ site/ 公开站点(静态,直接托管 site/ 即可)+ 归档 site/packages/
34
+ tools/ 校验、构建、打包、安装、发布(OSS / 上传服务)、脚手架
35
+ server/ 上传分发服务(用户上传 → 服务端校验 → 动态索引)
36
+ docs/ 包规范、安装说明、提交说明、后端部署
37
+ test/ 校验/打包/安装/上传服务的测试
38
+ ```
39
+
40
+ ## 维护者命令
41
+
42
+ ```sh
43
+ node tools/validate.mjs # 校验全部包
44
+ node tools/build.mjs # 生成索引与下载归档
45
+ node tools/serve.mjs # 本地预览站点 http://127.0.0.1:8899
46
+ node tools/install.mjs alarm-clock --home ~/made # 不经 npx 直接装
47
+ npm test # 跑测试
48
+ ```
49
+
50
+ ## 文档
51
+
52
+ - [包规范 v1](docs/SPEC.md) — 目录结构、`market.json`、三类包的桥接器文件
53
+ - [安装说明](docs/INSTALL.md) — 一条命令或手动解包,卸载与安全边界
54
+ - [提交说明](docs/PUBLISH.md) — 从脚手架到发布(含 OSS)
55
+ - [上传服务](docs/BACKEND.md) — 自建后端:用户上传、服务端校验、动态索引、部署
56
+
57
+ ## 用户上传
58
+
59
+ 开放上传靠自建的上传分发服务(`server/`,无依赖 Node 服务):作者
60
+ `npx made-market publish <包目录>` 上传,服务端用与 `validate.mjs` 相同的规则
61
+ 校验("CI 过审即上架"),版本不可变,索引动态重建,归档与元数据存阿里云 OSS。
62
+ 部署与 token 管理见 [docs/BACKEND.md](docs/BACKEND.md)。
63
+
64
+ ## 安全
65
+
66
+ 插件是本机 JavaScript,拥有桥接器进程的权限。只安装你信任的代码。市场能提供的是源码公开、静态校验和逐文件校验和;运行时的权限边界由桥接器决定。
67
+
68
+ ## 许可
69
+
70
+ MIT
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 码得插件市场的命令行入口 —— 供 `npx made-market` 直接使用。
4
+ *
5
+ * 市场只做包管理:列出、查询、下载、校验和落盘到一个固定目录,从不执行包代码。
6
+ * 包装到 <home>/plugins 或 <home>/themes(默认 ~/made),桥接器启动时自己扫。
7
+ *
8
+ * npx made-market list
9
+ * npx made-market info sunset
10
+ * npx made-market install sunset alarm-clock
11
+ * npx made-market install demo-echo --home /tmp/home --dry-run
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { ROOT } from '../tools/lib/market.mjs';
16
+ import { install, loadRegistry, resolveHome, resolveRegistry } from '../tools/install.mjs';
17
+ import { publish } from '../tools/publish.mjs';
18
+ import { login, logout, register, resolvePassword, whoami } from '../tools/account.mjs';
19
+
20
+ const KIND_LABELS = { theme: '主题', control: '管控', agent: 'agent 运行时' };
21
+ const KIND_ORDER = ['theme', 'control', 'agent'];
22
+
23
+ const USAGE = `码得 · 插件市场 (made-market)
24
+
25
+ 用法
26
+ npx made-market <命令> [参数] [选项]
27
+
28
+ 命令
29
+ list 列出索引里的全部包
30
+ search <关键词> 在包名、简介和标签里搜索
31
+ info <包 ID> 看一个包的详情:文件、校验和、安装位置、创建人
32
+ install <包 ID…> 装到包目录,重启桥接器后生效(匿名,无需登录)
33
+ publish <包目录> 把一个包目录上传到市场服务(需先登录)
34
+ register <用户名> 注册账号并登录(凭证存 <home>/credentials.json)
35
+ login <用户名> 登录市场服务(发布的前提;90 天会话)
36
+ whoami 看当前登录的账号
37
+ logout 退出登录(吊销服务端会话并删除本地凭证)
38
+ rebuild-from-store 管理命令:从存储 manifests 全量重建 PG 索引
39
+
40
+ 选项
41
+ --home <目录> 包目录根,默认 $MADE_HOME 或 ~/made
42
+ --registry <路径|URL> 索引来源。默认优先 $MADE_MARKET_REGISTRY(如市场服务的索引),
43
+ 不可用或未设置时回落到本包自带的索引(离线可用)
44
+ --server <URL> 市场服务地址(register/login/publish;默认 $MADE_MARKET_SERVER,
45
+ 已登录时也可从凭证读取)
46
+ --token <t> 发布 token(publish;默认 $MADE_MARKET_TOKEN 或登录凭证)
47
+ --password <pw> 密码(register/login;缺省在 TTY 下不回显输入)
48
+ --kind theme|control|agent 只列某一类(list / search)
49
+ --json 以 JSON 输出
50
+ --force 目标目录已存在时覆盖
51
+ --dry-run 只校验并列出将要写入的文件
52
+ -h, --help 显示帮助
53
+ -v, --version 显示版本
54
+
55
+ 装好的包在:
56
+ <home>/plugins/ 管控插件与 agent 运行时
57
+ <home>/themes/ 主题包
58
+
59
+ 桥接器每次启动扫描这两个目录,不需要登记,也不需要和配置文件放在一起。`;
60
+
61
+ function parse(argv) {
62
+ const options = { command: '', args: [], home: '', registry: '', server: '', token: '', password: '', kind: '', json: false, force: false, dryRun: false, help: false, version: false };
63
+ for (let i = 0; i < argv.length; i++) {
64
+ const arg = argv[i];
65
+ if (arg === '--home') options.home = argv[++i] || '';
66
+ else if (arg === '--registry') options.registry = argv[++i] || '';
67
+ else if (arg === '--server') options.server = argv[++i] || '';
68
+ else if (arg === '--token') options.token = argv[++i] || '';
69
+ else if (arg === '--password') options.password = argv[++i] || '';
70
+ else if (arg === '--kind') options.kind = argv[++i] || '';
71
+ else if (arg === '--json') options.json = true;
72
+ else if (arg === '--force') options.force = true;
73
+ else if (arg === '--dry-run') options.dryRun = true;
74
+ else if (arg === '--help' || arg === '-h') options.help = true;
75
+ else if (arg === '--version' || arg === '-v') options.version = true;
76
+ else if (arg.startsWith('-')) throw new Error(`未知参数 ${arg}`);
77
+ else if (!options.command) options.command = arg;
78
+ else options.args.push(arg);
79
+ }
80
+ return options;
81
+ }
82
+
83
+ const semverGreater = (a, b) => {
84
+ const left = String(a).split('.').map(Number);
85
+ const right = String(b).split('.').map(Number);
86
+ for (let i = 0; i < 3; i++) {
87
+ if ((left[i] || 0) !== (right[i] || 0)) return (left[i] || 0) > (right[i] || 0);
88
+ }
89
+ return false;
90
+ };
91
+
92
+ /**
93
+ * API 模式安装:按包直查市场服务的 /api/packages/<id>,不下载全量索引。
94
+ * 大市场(索引几十 MB)时用这个,归档地址改写成服务上的绝对地址。
95
+ */
96
+ async function resolveViaApi(ids, server) {
97
+ const base = String(server).replace(/\/+$/, '');
98
+ const packages = [];
99
+ for (const id of ids) {
100
+ const response = await fetch(`${base}/api/packages/${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(15000) });
101
+ if (!response.ok) throw new Error(`查询 ${id} 失败:HTTP ${response.status}`);
102
+ const versions = await response.json();
103
+ const latest = versions.reduce((best, item) => (!best || semverGreater(item.version, best.version) ? item : best), null);
104
+ if (!latest) throw new Error(`服务上没有包 ${id}`);
105
+ packages.push({ ...latest, download: latest.download.startsWith('http') ? latest.download : `${base}/${latest.download}` });
106
+ }
107
+ return { packages, base };
108
+ }
109
+
110
+ const human = (bytes) => (bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(2)} MB`);
111
+ const homeOf = (options) => resolveHome(options.home);
112
+
113
+ function installCommand(entry) {
114
+ return `npx made-market install ${entry.id}`;
115
+ }
116
+
117
+ function describe(entry) {
118
+ return {
119
+ id: entry.id,
120
+ kind: entry.kind,
121
+ version: entry.version,
122
+ name: entry.name,
123
+ summary: entry.summary,
124
+ author: entry.author,
125
+ license: entry.license,
126
+ tags: entry.tags,
127
+ target: entry.target,
128
+ dir: entry.dir,
129
+ createdBy: entry.createdBy || '',
130
+ publishedBy: entry.publishedBy || '',
131
+ sha256: entry.sha256,
132
+ archiveBytes: entry.archiveBytes,
133
+ files: entry.files,
134
+ install: installCommand(entry),
135
+ };
136
+ }
137
+
138
+ function listRows(index, options) {
139
+ const rows = options.kind ? index.packages.filter((entry) => entry.kind === options.kind) : index.packages;
140
+ const ordered = KIND_ORDER.flatMap((kind) => rows.filter((entry) => entry.kind === kind));
141
+ return ordered.length ? ordered : rows;
142
+ }
143
+
144
+ function printTable(rows) {
145
+ if (!rows.length) {
146
+ console.log('没有匹配的包。');
147
+ return;
148
+ }
149
+ const width = Math.max(...rows.map((entry) => entry.id.length), 2);
150
+ for (const entry of rows) {
151
+ console.log(`${entry.kind.padEnd(8)} ${entry.id.padEnd(width)} v${entry.version.padEnd(7)} ${human(entry.archiveBytes).padStart(9)} ${entry.name}`);
152
+ }
153
+ console.log(`\n共 ${rows.length} 个包。安装:npx made-market install <包 ID>`);
154
+ }
155
+
156
+ async function main() {
157
+ let options;
158
+ try {
159
+ options = parse(process.argv.slice(2));
160
+ } catch (error) {
161
+ console.error(error.message);
162
+ console.error(`\n${USAGE}`);
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+
167
+ if (options.version) {
168
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
169
+ console.log(pkg.version);
170
+ return;
171
+ }
172
+ if (options.help || !options.command) {
173
+ console.log(USAGE);
174
+ return;
175
+ }
176
+
177
+ // 索引来源按需加载:--registry > $MADE_MARKET_REGISTRY > 包内自带(远端失败自动回落,离线可用)。
178
+ // rebuild-from-store 与 install --server(API 模式)不依赖全量索引。
179
+ const loadIndex = () => loadRegistry(resolveRegistry(options.registry));
180
+
181
+ if (options.command === 'list') {
182
+ const rows = listRows((await loadIndex()).index, options);
183
+ if (options.json) console.log(JSON.stringify(rows.map(describe), null, 2));
184
+ else printTable(rows);
185
+ return;
186
+ }
187
+
188
+ if (options.command === 'search') {
189
+ const query = options.args.join(' ').trim().toLowerCase();
190
+ if (!query) throw new Error('search 需要一个关键词');
191
+ const rows = listRows((await loadIndex()).index, options).filter((entry) => [
192
+ entry.id, entry.name, entry.nameEn, entry.summary, entry.summaryEn, ...(entry.tags || []), ...(entry.highlights || []),
193
+ ].filter(Boolean).some((field) => String(field).toLowerCase().includes(query)));
194
+ if (options.json) console.log(JSON.stringify(rows.map(describe), null, 2));
195
+ else printTable(rows);
196
+ return;
197
+ }
198
+
199
+ if (options.command === 'info') {
200
+ const id = options.args[0];
201
+ if (!id) throw new Error('info 需要一个包 ID');
202
+ const entry = (await loadIndex()).index.packages.find((item) => item.id === id);
203
+ if (!entry) throw new Error(`索引里没有包 ${id}`);
204
+ if (options.json) {
205
+ console.log(JSON.stringify(describe(entry), null, 2));
206
+ return;
207
+ }
208
+ console.log(`${entry.name} · ${entry.id} v${entry.version} · ${KIND_LABELS[entry.kind] || entry.kind}`);
209
+ console.log(`${entry.summary}`);
210
+ if (entry.description) console.log(`\n${entry.description}`);
211
+ console.log('\n信息');
212
+ console.log(` 作者 ${entry.author}`);
213
+ console.log(` 许可 ${entry.license}`);
214
+ console.log(` 创建人 ${entry.createdBy || '—'}`);
215
+ console.log(` 最近发布 ${entry.publishedBy || '—'}${entry.publishedAt ? ` · ${entry.publishedAt}` : ''}`);
216
+ console.log(` 标签 ${(entry.tags || []).join('、') || '—'}`);
217
+ console.log(` 安装到 ${homeOf(options)}/${entry.target}/${entry.dir}/`);
218
+ console.log(` 归档 ${human(entry.archiveBytes)} sha256 ${entry.sha256}`);
219
+ console.log('\n文件与校验和');
220
+ for (const file of entry.files || []) console.log(` ${file.path.padEnd(14)} ${human(file.size).padStart(9)} ${file.sha256.slice(0, 16)}`);
221
+ if (entry.highlights?.length) {
222
+ console.log('\n要点');
223
+ for (const item of entry.highlights) console.log(` · ${item}`);
224
+ }
225
+ console.log(`\n安装:${installCommand(entry)}`);
226
+ return;
227
+ }
228
+
229
+ if (options.command === 'install') {
230
+ if (!options.args.length) throw new Error('install 需要至少一个包 ID');
231
+ // 配了 --server / $MADE_MARKET_SERVER:按包直查 API,不拉全量索引(大市场友好)。
232
+ if (options.server || process.env.MADE_MARKET_SERVER) {
233
+ const { packages, base } = await resolveViaApi(options.args, options.server || process.env.MADE_MARKET_SERVER);
234
+ await install(options.args, { home: options.home, registry: base, index: { packages }, force: options.force, dryRun: options.dryRun });
235
+ return;
236
+ }
237
+ // 归档从索引所在的位置取:远端索引配远端归档,自带索引配包内归档,可离线。
238
+ const { source, index } = await loadIndex();
239
+ await install(options.args, { home: options.home, registry: source, index, force: options.force, dryRun: options.dryRun });
240
+ return;
241
+ }
242
+
243
+ if (options.command === 'publish') {
244
+ const dir = options.args[0];
245
+ if (!dir) throw new Error('publish 需要一个包目录');
246
+ await publish(dir, { server: options.server, token: options.token, home: options.home });
247
+ return;
248
+ }
249
+
250
+ if (options.command === 'register' || options.command === 'login') {
251
+ const name = options.args[0];
252
+ if (!name) throw new Error(`${options.command} 需要一个用户名`);
253
+ const server = options.server || process.env.MADE_MARKET_SERVER;
254
+ const password = await resolvePassword({ ...options, name });
255
+ const run = options.command === 'register' ? register : login;
256
+ const result = await run({ server, name, password, home: options.home });
257
+ const who = result.user || { name };
258
+ console.log(`${options.command === 'register' ? '注册' : '登录'}成功:${who.name}${who.displayName && who.displayName !== who.name ? `(${who.displayName})` : ''}`);
259
+ console.log(` 服务 ${server}`);
260
+ console.log(` 凭证已存 ${resolveHome(options.home)}/credentials.json(0600),publish 直接可用`);
261
+ return;
262
+ }
263
+
264
+ if (options.command === 'whoami') {
265
+ const result = await whoami({ server: options.server, home: options.home, token: options.token });
266
+ console.log(`已登录:${result.user}${result.server ? ` @ ${result.server}` : ''}`);
267
+ return;
268
+ }
269
+
270
+ if (options.command === 'logout') {
271
+ await logout({ home: options.home, token: options.token });
272
+ console.log('已退出登录(服务端会话已吊销,本地凭证已删除)。');
273
+ return;
274
+ }
275
+
276
+ if (options.command === 'rebuild-from-store') {
277
+ const { createPool, initSchema, rebuildFromStore } = await import('../server/db.mjs');
278
+ const { createStore } = await import('../server/store.mjs');
279
+ if (!process.env.DATABASE_URL) throw new Error('rebuild-from-store 需要 DATABASE_URL 指向 PG 索引库');
280
+ const db = createPool(process.env.DATABASE_URL);
281
+ try {
282
+ await initSchema(db);
283
+ const count = await rebuildFromStore(db, createStore(), { log: (key) => console.log(` 同步 ${key}`) });
284
+ console.log(`索引重建完成:${count} 条版本事实。`);
285
+ } finally {
286
+ await db.close();
287
+ }
288
+ return;
289
+ }
290
+
291
+ throw new Error(`未知命令 ${options.command}\n\n${USAGE}`);
292
+ }
293
+
294
+ main().catch((error) => {
295
+ console.error(error.message);
296
+ process.exitCode = 1;
297
+ });
@@ -0,0 +1,189 @@
1
+ # 上传分发服务
2
+
3
+ 用户自己上传包的后端。**"CI 过审即上架"的那道 CI 就是这个服务**:归档在服务端解包、
4
+ 复用与 `tools/validate.mjs` 完全相同的规则校验,过不了就永远进不了索引。
5
+
6
+ ## 形态
7
+
8
+ 一个**无依赖的 Node HTTP 服务**(`node:http`,无 npm 依赖,连 PG 客户端都是自带的):
9
+
10
+ ```
11
+ server/
12
+ ├── server.mjs HTTP 路由:上传 / 索引 / 归档下载 / 版本查询 / 搜索
13
+ ├── service.mjs 发布流水线:校验 → 入库 → 同步索引(核心逻辑,测试直接驱动)
14
+ ├── store.mjs 存储驱动:local(开发/测试)| oss(生产,阿里云)—— 事实源
15
+ └── db.mjs Postgres 派生索引:零依赖线协议客户端(SCRAM-SHA-256)+ 表结构 + 查询
16
+ ```
17
+
18
+ 事实源与索引的分工:
19
+
20
+ - **存储(local/oss)里的 manifests 是唯一事实源**,每条事实一个不可变对象,随时可以从它全量重建任何派生数据;
21
+ - **Postgres 是派生索引**:发布时同步一行版本事实,搜索/查询/统计走 SQL。索引库没配、断了、报错,服务都自动回落到存储路径照常工作,修好后用 `rebuild-from-store` 补数据。
22
+
23
+ ## 接口
24
+
25
+ | 方法 | 路径 | 说明 |
26
+ | --- | --- | --- |
27
+ | `POST` | `/api/register` | 注册账号 `{name, password, displayName?}` → `{token}`,注册即登录 |
28
+ | `POST` | `/api/login` | 登录 `{name, password}` → `{token}`(90 天会话) |
29
+ | `GET` | `/api/me` | 当前登录的账号(Bearer 会话 token) |
30
+ | `POST` | `/api/logout` | 吊销当前会话 |
31
+ | `POST` | `/api/packages` | 发布一个包。Bearer = **登录会话**(或服务令牌),body = tar.gz(**带** market.json) |
32
+ | `GET` | `/registry/index.json` | 公开索引(有 PG 从索引库拼装,没有则从 manifests 重建),与 CLI/站点约定完全一致 |
33
+ | `GET` | `/packages/<归档名>` | 归档下载。oss 存储 302 跳公共读直链(客户端直连 OSS,服务不经手流量);local 存储由服务读文件返回 |
34
+ | `GET` | `/api/packages/<id>` | 一个包的全部版本(install API 模式按这个直查,不拉全量索引) |
35
+ | `GET` | `/api/search?q=&kind=&limit=` | 搜索:包名/英文名/简介/标签/创建人 ILIKE,kind 过滤 |
36
+ | `GET` | `/` | 状态(含索引层 postgres/store) |
37
+
38
+ **鉴权边界一句话:发布要登录,下载永远匿名。** 索引、搜索、版本查询、归档下载
39
+ 都不需要任何凭证;只有 `POST /api/packages` 检查 Bearer。
40
+
41
+ ## 发布流水线(服务端,上传方无法绕过)
42
+
43
+ 1. 归档 ≤ 10 MB,gunzip + 解 tar,条目路径检查(拒绝 `..` / 绝对路径);
44
+ 2. 读 `market.json`:id / kind / version 合法性;
45
+ 3. **服务端校验**:`validatePackage` —— 与 `npm run validate` 同一套规则,
46
+ 含 plugin.json 逐字段规范、主题资产存在性与尺寸、WAV 格式、indexed4 图标;
47
+ 4. **版本不可变**:同 id + version 已存在即 409,只能提升版本重新发布;
48
+ 5. 服务端**重新打包**(剥离 market.json),sha256 以服务端算的为准,不信任上传方;
49
+ 6. 写入 `packages/<id>-<version>.tar.gz` 与 `manifests/<id>/<version>.json`,同步 PG 索引,刷新存储索引。
50
+
51
+ 每次发布记录两个人:**`createdBy` = 该包首个版本的发布者(创建人,跨版本固定)**、
52
+ `publishedBy` = 本版本的发布者。两者都进 manifest、公开索引、PG 搜索结果,可追溯。
53
+
54
+ ## 存储是事实源,PG 是派生索引
55
+
56
+ 存储(local/oss)里的对象布局不变:
57
+
58
+ ```
59
+ packages/<id>-<version>.tar.gz 归档本体(分发,不含 market.json)
60
+ manifests/<id>/<version>.json 版本清单 = 索引条目 + createdBy/publishedBy/publishedAt(事实)
61
+ registry/index.json 兼容用的公开索引快照(发布时刷新)
62
+ users/<名>.json 账号:scrypt 加盐口令散列,不含明文
63
+ sessions/<token>.json 登录会话:user + expiresAt(吊销 = 覆盖为 revoked 记录)
64
+ access/tokens.json 服务令牌(CI/管理员兜底通道,可选,也可用环境变量)
65
+ ```
66
+
67
+ 配了 `DATABASE_URL` 时,PG 里有两张表:
68
+
69
+ ```
70
+ packages 每包一行:kind/name/summary/tags + latest_version/total_versions(搜索摘要)
71
+ versions 每版本一行:entry = 整条 manifest(jsonb),version_key = 可数值排序的 bigint
72
+ ```
73
+
74
+ - `versions.entry` 与存储里的 manifest 一字不差——**PG 丢了可以从 manifests 全量重建,存储坏了以 PG 为准读,两条腿互为备份**;
75
+ - 发布只写受影响的行,不再全量扫描,10 万包规模下发布仍是 O(1);
76
+ - 搜索/未来登录、审核流、下载统计都在这层做,OSS 只管存放。
77
+
78
+ 规模化路径:几千包以内不配 `DATABASE_URL` 也完全够用;包量上万或要上账号体系时加一行环境变量即可,客户端接口不变。
79
+
80
+ ## PG 客户端为什么是自带的
81
+
82
+ 延续仓库零 npm 依赖的原则,`db.mjs` 内置了最小 Postgres 线协议实现:
83
+ 协议 3.0 握手、SCRAM-SHA-256(RFC 7677,测试含密钥推导交叉验证)、md5/明文/信任兜底、
84
+ 简单查询协议、带断线重连的小连接池。**无 TLS**:仅限内网/同机部署,公网请套反向代理。
85
+
86
+ ## 账号与登录
87
+
88
+ 发布必须先登录;账号直接存进存储驱动(与 manifests 同一套事实源,local/OSS 通用,
89
+ 不依赖 PG),口令用 scrypt 加盐散列,会话 90 天有效。
90
+
91
+ ```sh
92
+ npx made-market register alice --server https://market.example.com
93
+ # 或:login(已有账号)。凭证存 <home>/credentials.json(0600)
94
+ npx made-market publish ./my-theme --server https://market.example.com
95
+ # 登录过就不再需要 --token
96
+ npx made-market whoami # 看当前账号
97
+ npx made-market logout # 吊销服务端会话并删除本地凭证
98
+ ```
99
+
100
+ 会话 token 只用于**上传**;下载和索引是公开的。
101
+
102
+ ## 服务令牌(CI / 管理员兜底)
103
+
104
+ 自动化通道可以不走账号,用预签的服务令牌,两个来源(可并用):
105
+
106
+ - 环境变量 `MADE_MARKET_TOKENS=name:token,name:token`(重启生效);
107
+ - 存储里的 `access/tokens.json`:`[{"name":"alice","token":"…"}]`(写入即生效)。
108
+
109
+ 服务令牌发布的包,创建人/发布者记录为令牌名(如 `ci-bot`)。生成一个随机令牌:
110
+
111
+ ```sh
112
+ openssl rand -hex 24
113
+ ```
114
+
115
+ ## 部署
116
+
117
+ 配置全部走环境变量:
118
+
119
+ ```sh
120
+ PORT=8787 # 监听端口
121
+ MADE_MARKET_STORE=oss # local | oss
122
+ MADE_MARKET_PUBLIC_URL=https://market.example.com # 对外基地址(写进索引 registryUrl)
123
+ MADE_MARKET_TOKENS=alice:…,bob:… # 发布 token
124
+ DATABASE_URL=postgres://made:…@pg:5432/made_market # 可选:PG 派生索引
125
+ OSS_REGION=oss-cn-hangzhou # 以下为 oss 驱动所需
126
+ OSS_BUCKET=made-market
127
+ OSS_ACCESS_KEY_ID=…
128
+ OSS_ACCESS_KEY_SECRET=…
129
+ OSS_PREFIX= # 可选,key 前缀
130
+ ```
131
+
132
+ 直接跑(Node ≥ 22,无依赖):
133
+
134
+ ```sh
135
+ node server/server.mjs
136
+ ```
137
+
138
+ 或 Docker(PG 一起起:`docker compose up -d`,见 `docker-compose.yml`):
139
+
140
+ ```sh
141
+ docker build -t made-market-server .
142
+ docker run -d -p 8787:8787 --env-file server.env made-market-server
143
+ ```
144
+
145
+ **安全提示**:OSS AccessKey 与数据库口令只放在服务端环境变量里,绝不进前端代码、仓库或
146
+ `site/`——静态站点托管(GitHub Pages / OSS 静态托管)上的任何文件都是公开的。
147
+
148
+ ## 索引自愈
149
+
150
+ ```sh
151
+ # 从存储 manifests 全量重建 PG 索引(换库、丢库、补数据都走这里)
152
+ DATABASE_URL=postgres://… npx made-market rebuild-from-store
153
+ ```
154
+
155
+ ## 客户端怎么接
156
+
157
+ 服务上线后,它就是一个普通的索引来源,三种接法任选:
158
+
159
+ ```sh
160
+ # 单次安装(拉全量索引)
161
+ npx made-market install alarm-clock --registry https://market.example.com/registry/index.json
162
+
163
+ # 大市场推荐:按包直查 API,不拉全量索引
164
+ npx made-market install alarm-clock --server https://market.example.com
165
+
166
+ # 设为默认下载源(服务优先,断网回落包内快照)
167
+ export MADE_MARKET_REGISTRY=https://market.example.com/registry/index.json
168
+
169
+ # 上传(作者侧):先注册/登录一次,之后 publish 不再需要凭证参数
170
+ npx made-market register me --server https://market.example.com
171
+ npx made-market publish ./my-theme --server https://market.example.com
172
+ ```
173
+
174
+ ## 本地开发
175
+
176
+ ```sh
177
+ docker compose up -d # 起 PG
178
+ DATABASE_URL=postgres://made:made@127.0.0.1:5432/made_market \
179
+ node server/server.mjs &
180
+ npx made-market register me --server http://127.0.0.1:8787 --password dev123
181
+ npx made-market publish packages/themes/sunset --server http://127.0.0.1:8787
182
+ curl -s 'http://127.0.0.1:8787/api/search?q=日落'
183
+ ```
184
+
185
+ `local` 驱动的数据落在 `./data/`(已 gitignore)。测试覆盖:SCRAM/协议单元测试、
186
+ 口令散列与会话生命周期、发布鉴权(未登录 401 / 登录可发 / 创建人跨版本固定 / 下载匿名)、
187
+ CLI 账号链路(register → publish 免 token → whoami → logout)、真实 PG 集成测试
188
+ (设 `DATABASE_URL` 启用,未设自动跳过)、无 PG 时服务全链路、索引库故障自动回落存储、
189
+ rebuild-from-store CLI 端到端。
@@ -0,0 +1,95 @@
1
+ # 安装市场包
2
+
3
+ 市场只发包,安装就是下载、校验、落盘到**一个固定目录**。桥接器每次启动自己扫这个目录,所以不需要登记,也不需要把包和配置文件放在一起。
4
+
5
+ ## 一条命令
6
+
7
+ ```sh
8
+ npx made-market install <包 ID>
9
+ ```
10
+
11
+ 装到 `~/made`:
12
+
13
+ - 主题包 → `~/made/themes/<dir>/`
14
+ - 管控包、agent 包 → `~/made/plugins/<dir>/`
15
+
16
+ 装完重启桥接器即可生效。也可以一次装多个:
17
+
18
+ ```sh
19
+ npx made-market install sunset alarm-clock demo-echo
20
+ ```
21
+
22
+ 常用命令与参数:
23
+
24
+ ```sh
25
+ npx made-market list # 列出全部包
26
+ npx made-market search 主题 # 搜索
27
+ npx made-market info sunset # 看详情:文件、校验和、安装位置
28
+ npx made-market install sunset --dry-run # 只校验,不落盘
29
+ ```
30
+
31
+ | 参数 | 说明 |
32
+ | --- | --- |
33
+ | `--home <目录>` | 包目录根。默认 `$MADE_HOME`,再默认 `~/made` |
34
+ | `--registry <路径或 URL>` | 指定索引来源。默认优先 `$MADE_MARKET_REGISTRY`(OSS 公开索引),不可达时自动回落到包内自带索引(离线可用) |
35
+ | `--kind theme\|control\|agent` | 只列某一类(配合 `list` / `search`) |
36
+ | `--json` | 以 JSON 输出,便于脚本消费 |
37
+ | `--force` | 目标目录已存在时覆盖 |
38
+ | `--dry-run` | 只校验和展示将要写入的文件,不落盘 |
39
+
40
+ **包从哪里下载**:索引解析顺序是 `--registry` > `$MADE_MARKET_REGISTRY` > 包内自带快照,归档永远从索引进驻的同一位置按约定取(`<根>/registry/index.json` ↔ `<根>/packages/`)。权威分发源是 OSS,设一次环境变量即可:
41
+
42
+ ```sh
43
+ export MADE_MARKET_REGISTRY=https://<bucket>.<region>.aliyuncs.com/registry/index.json
44
+ ```
45
+
46
+ 每个归档的 `sha256` 都记录在索引里,安装前逐字节核对,改过一个字节都会拒绝。
47
+
48
+ 安装工具**不执行**任何插件代码,也不写入任何密钥。
49
+
50
+ ## 包目录在哪
51
+
52
+ 固定的用户目录,桥接器启动时扫描 `<home>/plugins` 与 `<home>/themes`:
53
+
54
+ | 优先级 | 目录 | 谁写 |
55
+ | --- | --- | --- |
56
+ | 1(被覆盖) | `bridge/builtin/plugins/`、`bridge/builtin/themes/` | 随桥接器代码发布,只读 |
57
+ | 2(覆盖上面) | `~/made/plugins/`、`~/made/themes/` | 市场安装,以及你自己放进去的包 |
58
+
59
+ **同名时 `~/made` 覆盖内置**。所以在 `~/made/plugins/` 放一个和内置同名的目录,就能本地顶掉内置那版。
60
+
61
+ 改根目录的两种办法,优先级从高到低:命令行 `--home`、环境变量 `MADE_HOME`、桥接器 `config.local.json` 里的 `madeHome`、默认 `~/made`。
62
+
63
+ ## 不用 npx 的两种装法
64
+
65
+ 仓库内直接跑(开发时用):
66
+
67
+ ```sh
68
+ node tools/install.mjs alarm-clock --home ~/made
69
+ ```
70
+
71
+ 手动解包:从站点下载 `<id>-<version>.tar.gz`,解开覆盖到目标目录即可。
72
+
73
+ ```sh
74
+ mkdir -p ~/made/themes/sunset
75
+ tar -xzf sunset-1.0.0.tar.gz -C ~/made/themes/sunset
76
+ ```
77
+
78
+ 归档里只有桥接器需要的文件,没有 `market.json`,可以直接解开覆盖。
79
+
80
+ ## 卸载
81
+
82
+ 在桥接器工作台的「插件」页点移除,或直接删掉目录:
83
+
84
+ ```sh
85
+ rm -rf ~/made/themes/sunset
86
+ rm -rf ~/made/plugins/alarm-clock
87
+ ```
88
+
89
+ 内置包不能从工作台移除——它跟代码走。要停用就在工作台点停用,要覆盖就放同名目录到 `~/made`。
90
+
91
+ 桥接器重启后生效。已下发的主题需要码得重新拉取才会回退。
92
+
93
+ ## 安全
94
+
95
+ 插件是本机 JavaScript,拥有桥接器进程的权限。**只安装你信任的代码。**市场侧能做的是:静态校验、公开源码、逐文件校验和(归档 sha256 + 每个文件的 sha256)。管控插件调用外部 hook 用的地址与密钥应当留在桥接器本机配置里,不要写进包。