@qomicex/cli 0.1.0 → 0.1.2
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/README.md +31 -0
- package/dist/commands/bump.js +30 -0
- package/dist/index.js +31 -20
- package/dist/lib/manifest.js +7 -7
- package/dist/lib/permissions.js +1 -0
- package/dist/lib/signature.js +1 -1
- package/dist/lib/store.js +1 -1
- package/package.json +3 -2
- package/skills/qomicex-plugin/SKILL.md +65 -0
- package/skills/qomicex-plugin/debugging.md +55 -0
- package/skills/qomicex-plugin/manifest-schema.md +130 -0
- package/skills/qomicex-plugin/permissions.md +98 -0
- package/skills/qomicex-plugin/plugin-api.md +113 -0
- package/skills/qomicex-plugin/rules.md +46 -0
- package/skills/qomicex-plugin/signing.md +80 -0
- package/skills/qomicex-plugin/theme.md +58 -0
package/README.md
CHANGED
|
@@ -62,6 +62,37 @@ qomicex publish --api http://127.0.0.1:8787/api/v1 # 本地商店(wrangler d
|
|
|
62
62
|
4. 查找/创建插件记录(`/plugins/mine` → 无则 `POST /plugins`),确认后 `POST /plugins/:id/versions` multipart 上传。
|
|
63
63
|
5. 成功后将签名包存为 `release/<id>-<version>.signed.qplugin` 供复验。
|
|
64
64
|
|
|
65
|
+
## AI 辅助开发
|
|
66
|
+
|
|
67
|
+
CLI 随包分发 **AI skill 包**(`skills/qomicex-plugin/`),供 AI agent(Claude / opencode / Cursor 等)在写插件时加载,获得准确且不过时的 manifest 字段、权限目录、桥 API 签名、主题 token、签名与调试流程——避免 AI 臆造字段。安装 CLI 后本地路径为 `node_modules/@qomicex/cli/skills/qomicex-plugin/`(npm 包内,随 `files` 分发)。
|
|
68
|
+
|
|
69
|
+
`qomicex create` 默认**不自动复制** skill 包到项目目录(保持项目最小化);需要时手动复制即可。
|
|
70
|
+
|
|
71
|
+
在 AI 会话中这样用:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
你是 Qomicex 插件开发工程师。请先阅读 <CLI 安装路径>/skills/qomicex-plugin/SKILL.md
|
|
75
|
+
(及其余分册,尤其 rules.md),然后帮我做一个「XXX」插件:
|
|
76
|
+
1. qomicex create com.example.xxx 生成项目
|
|
77
|
+
2. 实现功能(manifest / 权限 / API 签名以技能包文档为准,不臆造)
|
|
78
|
+
3. qomicex verify 过 0 error
|
|
79
|
+
4. qomicex pack 出包
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
skill 包结构:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
skills/qomicex-plugin/
|
|
86
|
+
SKILL.md # 主文档:何时用、快速开始、常见错误
|
|
87
|
+
manifest-schema.md # manifest.json 全字段 + layers + render + dependencies + contributes
|
|
88
|
+
permissions.md # 权限目录(39 项,normal/warning/danger)+ 方法→权限映射
|
|
89
|
+
plugin-api.md # 桥 API 签名速查(完整文档见 D:\docs\docs\plugins\plugin-api.md)
|
|
90
|
+
theme.md # 三级语义 token + var() 消费约定
|
|
91
|
+
signing.md # Ed25519 签名流程(生成密钥 → pack --key → publish)
|
|
92
|
+
debugging.md # harness 热重载调试
|
|
93
|
+
rules.md # AI 生成插件的硬性规则
|
|
94
|
+
```
|
|
95
|
+
|
|
65
96
|
## 签名密钥
|
|
66
97
|
|
|
67
98
|
生成 Ed25519 密钥对(PKCS#8 PEM 或 raw 32 字节 seed base64):
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// qomicex bump — 递增 manifest.json 版本号(major/minor/patch)。
|
|
2
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readManifestFile } from "../lib/project.js";
|
|
5
|
+
import { fail, info } from "../lib/io.js";
|
|
6
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/;
|
|
7
|
+
export async function bumpCommand(opts) {
|
|
8
|
+
const manifestFile = join(process.cwd(), 'manifest.json');
|
|
9
|
+
if (!existsSync(manifestFile))
|
|
10
|
+
fail('当前目录不是插件项目(缺少 manifest.json)');
|
|
11
|
+
const manifest = readManifestFile(manifestFile);
|
|
12
|
+
const current = String(manifest.version ?? '');
|
|
13
|
+
const next = opts.version ?? (() => {
|
|
14
|
+
const m = SEMVER_RE.exec(current);
|
|
15
|
+
if (!m)
|
|
16
|
+
fail(`当前版本不是合法 semver: ${current}`);
|
|
17
|
+
const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
18
|
+
if (opts.part === 'major')
|
|
19
|
+
return `${major + 1}.0.0`;
|
|
20
|
+
if (opts.part === 'patch')
|
|
21
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
22
|
+
return `${major}.${minor + 1}.0`;
|
|
23
|
+
})();
|
|
24
|
+
if (!SEMVER_RE.test(next))
|
|
25
|
+
fail(`目标版本不是合法 semver: ${next}`);
|
|
26
|
+
manifest.version = next;
|
|
27
|
+
writeFileSync(manifestFile, JSON.stringify(manifest, null, 2) + '\n');
|
|
28
|
+
info(`✔ manifest.json version: ${current} → ${next}`);
|
|
29
|
+
return next;
|
|
30
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// qomicex CLI 入口:create / dev / pack / verify / publish
|
|
2
|
+
// qomicex CLI 入口:create / dev / pack / verify / bump / publish
|
|
3
3
|
import { createCommand } from "./commands/create.js";
|
|
4
4
|
import { devCommand } from "./commands/dev.js";
|
|
5
5
|
import { packCommand } from "./commands/pack.js";
|
|
6
6
|
import { verifyCommand } from "./commands/verify.js";
|
|
7
7
|
import { publishCommand } from "./commands/publish.js";
|
|
8
|
+
import { bumpCommand } from "./commands/bump.js";
|
|
8
9
|
import { fail } from "./lib/io.js";
|
|
9
|
-
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
// 版本号单一来源:package.json(bump 只改一处)
|
|
12
|
+
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
10
13
|
function parseArgs(argv) {
|
|
11
14
|
const positional = [];
|
|
12
15
|
const options = {};
|
|
@@ -39,24 +42,26 @@ function parseArgs(argv) {
|
|
|
39
42
|
return { command, positional, options };
|
|
40
43
|
}
|
|
41
44
|
function helpText() {
|
|
42
|
-
return `
|
|
43
|
-
qomicex v${VERSION} — Qomicex 插件生态 CLI
|
|
44
|
-
|
|
45
|
-
用法:
|
|
46
|
-
qomicex create <id> 从内置模板生成合法插件项目(Vite+React+TS+plugin-ui+tailwind)
|
|
47
|
-
qomicex dev [--port <n>] 起本地 Vite dev server + 生成 dev 源插件配置(默认 5173)
|
|
48
|
-
qomicex pack [--out-dir <d>] [--version <v>] [--key <k>] [--skip-build]
|
|
49
|
-
构建并打 .qplugin(manifest.json 在 zip 根)
|
|
50
|
-
qomicex verify [--package <f>]
|
|
51
|
-
manifest 合法性 + 权限最小化 + 长循环告警 + 签名检查
|
|
52
|
-
qomicex
|
|
53
|
-
|
|
54
|
-
qomicex --
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
return `
|
|
46
|
+
qomicex v${VERSION} — Qomicex 插件生态 CLI
|
|
47
|
+
|
|
48
|
+
用法:
|
|
49
|
+
qomicex create <id> 从内置模板生成合法插件项目(Vite+React+TS+plugin-ui+tailwind)
|
|
50
|
+
qomicex dev [--port <n>] 起本地 Vite dev server + 生成 dev 源插件配置(默认 5173)
|
|
51
|
+
qomicex pack [--out-dir <d>] [--version <v>] [--key <k>] [--skip-build]
|
|
52
|
+
构建并打 .qplugin(manifest.json 在 zip 根)
|
|
53
|
+
qomicex verify [--package <f>]
|
|
54
|
+
manifest 合法性 + 权限最小化 + 长循环告警 + 签名检查
|
|
55
|
+
qomicex bump <major|minor|patch> [--version <v>]
|
|
56
|
+
递增 manifest.json 版本号(或 --version 直接指定)
|
|
57
|
+
qomicex publish [--key <k>] [--slug <s>] [--changelog <c>] [--api <url>] [--org-id <id>] [--package <f>] [--yes]
|
|
58
|
+
设备流登录 → 注册签名公钥 → 签名 → 上传到商店
|
|
59
|
+
qomicex --help | -h 显示帮助
|
|
60
|
+
qomicex --version | -v 显示版本
|
|
61
|
+
|
|
62
|
+
环境变量:
|
|
63
|
+
QOMICEX_SIGN_KEY 签名私钥(PKCS#8 PEM 或 raw base64 seed),publish 必填
|
|
64
|
+
QOMICEX_STORE_API 商店 API base(默认 https://plugins.qomicex.top/api/v1)
|
|
60
65
|
`;
|
|
61
66
|
}
|
|
62
67
|
async function main() {
|
|
@@ -89,6 +94,12 @@ async function main() {
|
|
|
89
94
|
case 'verify':
|
|
90
95
|
await verifyCommand({ package: typeof options['package'] === 'string' ? options['package'] : undefined });
|
|
91
96
|
break;
|
|
97
|
+
case 'bump':
|
|
98
|
+
await bumpCommand({
|
|
99
|
+
part: positional[0] ?? 'minor',
|
|
100
|
+
version: typeof options['version'] === 'string' ? options['version'] : undefined,
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
92
103
|
case 'publish':
|
|
93
104
|
await publishCommand({
|
|
94
105
|
key: typeof options['key'] === 'string' ? options['key'] : undefined,
|
package/dist/lib/manifest.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { PERMISSION_CATALOG } from "./permissions.js";
|
|
4
4
|
export const ID_RE = /^(?!.*\.\.)[a-z0-9]+([.-][a-z0-9]+)*$/;
|
|
5
5
|
export const SEMVER_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
6
|
-
export const LAYERS = ['l0', 'l1', 'l2', 'l3'];
|
|
6
|
+
export const LAYERS = ['l0', 'l1', 'l2', 'l3', 'l4'];
|
|
7
7
|
export function checkId(id) {
|
|
8
8
|
const errs = [];
|
|
9
9
|
if (typeof id !== 'string')
|
|
@@ -44,11 +44,11 @@ export function validateManifest(raw) {
|
|
|
44
44
|
add('error', 'layers', `未知 layer: ${String(l)}(可用 ${LAYERS.join('/')})`);
|
|
45
45
|
}
|
|
46
46
|
const hasFrontend = !!m.entry?.frontend;
|
|
47
|
-
if (!m.layers.includes('l2') && !m.layers.includes('l3') && hasFrontend) {
|
|
48
|
-
add('warning', 'layers', '声明了 frontend 入口但 layers 无 l2/l3,将无法渲染 UI');
|
|
47
|
+
if (!m.layers.includes('l2') && !m.layers.includes('l3') && !m.layers.includes('l4') && hasFrontend) {
|
|
48
|
+
add('warning', 'layers', '声明了 frontend 入口但 layers 无 l2/l3/l4,将无法渲染 UI');
|
|
49
49
|
}
|
|
50
|
-
if (!m.layers.includes('l2') && hasFrontend) {
|
|
51
|
-
add('warning', 'layers', 'UI 插件建议声明 l2(iframe
|
|
50
|
+
if (!m.layers.includes('l2') && !m.layers.includes('l4') && hasFrontend) {
|
|
51
|
+
add('warning', 'layers', 'UI 插件建议声明 l2(iframe 沙箱)或 l4(独立 WebView 窗口)');
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
if (!Array.isArray(m.permissions)) {
|
|
@@ -76,8 +76,8 @@ export function validateManifest(raw) {
|
|
|
76
76
|
}
|
|
77
77
|
if (m.dependencies !== undefined && !Array.isArray(m.dependencies))
|
|
78
78
|
add('error', 'dependencies', 'dependencies 必须是数组');
|
|
79
|
-
if (m.render !== undefined && m.render !== 'inline' && m.render !== 'iframe') {
|
|
80
|
-
add('warning', 'render', "render 仅支持 'inline' / 'iframe'");
|
|
79
|
+
if (m.render !== undefined && m.render !== 'inline' && m.render !== 'iframe' && m.render !== 'webview') {
|
|
80
|
+
add('warning', 'render', "render 仅支持 'inline' / 'iframe' / 'webview'");
|
|
81
81
|
}
|
|
82
82
|
if (m.contributes !== undefined) {
|
|
83
83
|
const c = m.contributes;
|
package/dist/lib/permissions.js
CHANGED
|
@@ -44,6 +44,7 @@ export const METHOD_PERMISSIONS = {
|
|
|
44
44
|
registerMethod: 'config:write', callPlugin: 'network:fetch', callWasm: 'wasm:execute', listWasmPlugins: 'wasm:execute',
|
|
45
45
|
readText: 'filesystem:read', readBytes: 'filesystem:read', writeText: 'filesystem:write', writeBytes: 'filesystem:write', deleteFile: 'filesystem:write', execCommand: 'shell:execute',
|
|
46
46
|
navigate: 'config:read', showToast: 'ui:toast', getSystemInfo: 'system:info', openUrl: 'system:notification', listPlugins: 'plugin:list',
|
|
47
|
+
getThemeColor: 'config:read', applyThemeOverride: 'config:write', clearThemeOverride: 'config:write',
|
|
47
48
|
'overlay.create': 'ui:sub_window', 'overlay.show': 'ui:sub_window', 'overlay.hide': 'ui:sub_window',
|
|
48
49
|
'overlay.destroy': 'ui:sub_window', 'overlay.setHtml': 'ui:sub_window', 'overlay.setPosition': 'ui:sub_window',
|
|
49
50
|
'download.addTask': 'download:manage', 'download.progress': 'download:manage', 'download.cancel': 'download:manage', 'download.list': 'download:manage', 'download.registerInstall': 'instance:write',
|
package/dist/lib/signature.js
CHANGED
|
@@ -8,7 +8,7 @@ export const SIGNATURE_FILE = 'signature.json';
|
|
|
8
8
|
export const CERT_FILE = 'signature.cert.json';
|
|
9
9
|
export const ALG = 'Ed25519';
|
|
10
10
|
/** 商店签名根公钥(raw base64),与 launcher plugin_signature.rs ROOT_PUBLIC_KEY_B64 一致 */
|
|
11
|
-
export const STORE_ROOT_PUBLIC_KEY_B64 = '
|
|
11
|
+
export const STORE_ROOT_PUBLIC_KEY_B64 = 'sPKcrc6QR5gcOnQMdq21Jo3yqxN7Mbm61OYxZnKuHE0=';
|
|
12
12
|
/** Ed25519 PKCS#8 DER 固定前缀(前 16 字节),后接 32 字节 seed */
|
|
13
13
|
const ED25519_PKCS8_PREFIX = Uint8Array.from([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]);
|
|
14
14
|
export function bytesToBase64(bytes) {
|
package/dist/lib/store.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function fetchMinePlugins(base, token) {
|
|
|
65
65
|
}
|
|
66
66
|
/** 新建插件(store 返回 201 {plugin:{id}}),slug 占用抛 409。 */
|
|
67
67
|
export async function createPlugin(base, token, input) {
|
|
68
|
-
const r = await request(base, '/plugins
|
|
68
|
+
const r = await request(base, '/plugins', {
|
|
69
69
|
method: 'POST',
|
|
70
70
|
body: JSON.stringify({
|
|
71
71
|
slug: input.slug,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qomicex/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
-
"templates"
|
|
14
|
+
"templates",
|
|
15
|
+
"skills"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "tsc",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Qomicex 插件开发技能包(qomicex-plugin)
|
|
2
|
+
|
|
3
|
+
面向 AI agent(Claude / opencode / Cursor 等)的 Qomicex 启动器插件(`.qplugin`)开发技能包,随 `@qomicex/cli` 分发。本技能把"生成合规插件"所需的全部事实(manifest 校验、权限目录、桥 API、主题 token、签名、调试)收敛到一个目录,避免 AI 臆造字段。
|
|
4
|
+
|
|
5
|
+
## 何时使用
|
|
6
|
+
|
|
7
|
+
用户要求**开发 / 修改 / 审查 / 打包 / 发布** Qomicex 启动器插件时加载本技能。判断依据:涉及 `manifest.json`、`__PLUGIN_API__`、`entry.frontend`、`contributes`、权限声明、`.qplugin` 打包等关键词。
|
|
8
|
+
|
|
9
|
+
## 文件导航(建议全读,勿跳)
|
|
10
|
+
|
|
11
|
+
| 文件 | 内容 |
|
|
12
|
+
|------|------|
|
|
13
|
+
| `manifest-schema.md` | manifest.json 全字段 + layers 语义 + render 默认 iframe + dependencies + contributes |
|
|
14
|
+
| `permissions.md` | 权限目录(normal/warning/danger)+ 每个权限对应的桥 API 方法 |
|
|
15
|
+
| `plugin-api.md` | 桥 API(`__PLUGIN_API__`)签名速查 |
|
|
16
|
+
| `theme.md` | 主题语义 token 三级体系 + `var()` 消费约定 + 主题贡献 |
|
|
17
|
+
| `signing.md` | Ed25519 签名流程(生成密钥 → pack --key → publish) |
|
|
18
|
+
| `debugging.md` | harness 热重载调试 |
|
|
19
|
+
| `rules.md` | AI 生成插件的硬性规则(必须逐条遵守) |
|
|
20
|
+
|
|
21
|
+
## 完整流程(create → dev → verify → pack → publish)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1. 生成项目(id 需反向域名格式,如 com.example.demo)
|
|
25
|
+
qomicex create com.example.demo
|
|
26
|
+
|
|
27
|
+
# 2. 本地调试(仓库内自动进 harness,固定端口 1420;仓库外裸 Vite 默认 5173)
|
|
28
|
+
cd com.example.demo && pnpm install && qomicex dev
|
|
29
|
+
|
|
30
|
+
# 3. 校验(提交/打包前必跑,0 error 才算通过)
|
|
31
|
+
qomicex verify
|
|
32
|
+
|
|
33
|
+
# 4. 打包(tsc && vite build → release/<id>-<version>.qplugin)
|
|
34
|
+
qomicex pack --version 0.2.0
|
|
35
|
+
qomicex pack --key ./dev-key.pem # 附签名(仅 signature.json;完整证书链走 publish)
|
|
36
|
+
|
|
37
|
+
# 5. 发布(设备流登录 → 商店签发证书 → 签名 → 上传)
|
|
38
|
+
export QOMICEX_SIGN_KEY=<私钥 base64/PEM>
|
|
39
|
+
qomicex publish
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 常见错误与规避
|
|
43
|
+
|
|
44
|
+
| 错误 | 规避 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| `id` 不含点 / 大写 / 连续双点 | id 用 `^[a-z0-9]+([.-][a-z0-9]+)*$`,3-128 字符,含至少一个点 |
|
|
47
|
+
| `version` 非 semver | 严格 `数字.数字.数字`(可带 `-预发布` / `+构建号`) |
|
|
48
|
+
| `entry.frontend` 指向非 `.html` | `verify` 警告:frontend 应指向 `.html`(如 `dist/index.html`) |
|
|
49
|
+
| 声明了 frontend 但 `layers` 无 `l2`/`l3` | 无法渲染 UI;UI 插件务必声明 `l2`(iframe 沙箱,默认推荐) |
|
|
50
|
+
| 权限声明与源码调用不一致 | `verify` 会做权限最小化扫描,声明未用 / 用了未声明**都会报错** |
|
|
51
|
+
| 相对 import 不带扩展名 | Vite 强制:`import { x } from './api.ts'`,漏 `.ts/.tsx` 直接构建失败 |
|
|
52
|
+
| 插件里 `fetch()` 外部 URL 被 CORS / SSRF 拦 | 用 `proxyFetch` / `proxyFetchStream`(带 SSRF 防护) |
|
|
53
|
+
| 沙箱内 `window.open` 被拦 | 用 `callBackend('/system/open-url', { url })` 或 `openUrl` |
|
|
54
|
+
| `vite.config.ts` 没设 `base: './'` | 产物 `/assets/...` 会被解析成站点根,插件白屏 |
|
|
55
|
+
| 长循环 / 无界 `setInterval` | `verify` 会告警;放 Worker / WASM / 后端 |
|
|
56
|
+
| 硬编码 API key 进插件源码 | 无密钥存储;让用户经 `setSettings` 配置,运行时 `getSettings` 读取 |
|
|
57
|
+
|
|
58
|
+
## 事实一致红线
|
|
59
|
+
|
|
60
|
+
- 本技能内容与代码事实对齐(`src/plugins/types.ts`、CLI `src/lib/*`、公开文档 `docs/plugins/plugin-api.md`)。字段含义有疑问时**以代码为准**,不确定就标注,不要虚构 API 或字段。
|
|
61
|
+
- `packages/plugin-ui`(`@qomicex/plugin-ui`)的组件库使用参见 `tailwind.config.js` 的 `@qomicex/plugin-ui/tailwind-preset`,主题类名(`bg-primary`、`text-muted-foreground` 等)直接消费语义 token。
|
|
62
|
+
|
|
63
|
+
## 起步提示词(给 AI 用)
|
|
64
|
+
|
|
65
|
+
> 你是 Qomicex 插件开发工程师。先读 `skills/qomicex-plugin/` 下的 SKILL.md 与各分册(尤其 rules.md),再按流程工作:`qomicex create <id>` 生成项目 → 实现功能 → `qomicex verify` 过 0 error → `qomicex pack` 出包。manifest 字段、权限、API 签名一律以技能包内文档为准,不臆造。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Harness 热重载调试
|
|
2
|
+
|
|
3
|
+
调试实现源:`packages/qomicex-cli/src/commands/dev.ts` + `scripts/harness/run.mjs`。
|
|
4
|
+
|
|
5
|
+
## qomicex dev 的两条路径
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
qomicex dev # 默认
|
|
9
|
+
qomicex dev --port 3000
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`dev` 命令从当前目录向上查找 `scripts/harness/run.mjs`:
|
|
13
|
+
|
|
14
|
+
| 场景 | 行为 |
|
|
15
|
+
|------|------|
|
|
16
|
+
| **仓库内(harness 模式)** | 检测到 harness → spawn `scripts/harness/run.mjs`,进入完整调试环境。**插件必须位于 `plugins-dev/{id}`**(harness 从该目录定位)。`--port` 在此模式不生效(固定 1420) |
|
|
17
|
+
| **仓库外(裸 Vite)** | 回退为 Vite dev server(默认 5173)+ 在项目根写 `.qomicex-dev.json`(dev 源插件配置,供手动注册 dev 源) |
|
|
18
|
+
|
|
19
|
+
## harness 模式做什么
|
|
20
|
+
|
|
21
|
+
不启动 Tauri、不启动 Rust 后端,纯浏览器调试:
|
|
22
|
+
|
|
23
|
+
1. 起 stub mock server(`scripts/harness/stub.mjs`,固定 `:5100`)。
|
|
24
|
+
2. 复用已有 Vite dev(`:1420`);未运行则自动 spawn `pnpm run dev`。
|
|
25
|
+
3. `addInitScript` 注入 **Tauri API mock**(`window.__TAURI_INTERNALS__` 等,跨导航保留)——否则前端在纯浏览器里无法挂载。
|
|
26
|
+
4. 打开插件页 `http://127.0.0.1:1420/plugins/p/{pluginId}`。
|
|
27
|
+
5. `fs.watch` 监听插件 `src/`(含 `index.html`/`theme.css`/`overlay.html`/`vite.config.ts`)→ 变更后重建(`pnpm run build`)→ 整页 reload(iframe 重新挂载)。
|
|
28
|
+
|
|
29
|
+
**前置要求**:Playwright + Chromium:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add -D playwright
|
|
33
|
+
pnpm exec playwright install chromium
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 手动运行 harness
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node scripts/harness/run.mjs <pluginId> [--headed] [--mock file.json] [--build-cmd "pnpm run build"]
|
|
40
|
+
# 或仓库根:pnpm run harness -- hello-plugin
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| 参数 | 说明 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| `--headed` | 有头模式(默认 headless,看不到窗口;调试用 --headed) |
|
|
46
|
+
| `--mock file.json` | 自定义 stub 返回(mock 数据) |
|
|
47
|
+
| `--build-cmd "..."` | 覆盖重建命令(默认 `pnpm run build`) |
|
|
48
|
+
|
|
49
|
+
## 注意事项
|
|
50
|
+
|
|
51
|
+
- **网络请求转发**:前端直连 `:5000` 的 `/api/**` 请求被 route 到 stub `:5100`。stub 返回什么,插件就拿到什么——需要真实后端数据时请先跑 Rust 后端或改 mock。
|
|
52
|
+
- **热重载陷阱**:浏览器(Chromium) ≠ WebView2,复合/backdrop-filter 等行为可能有差异,结论需在真实 Tauri 复核。
|
|
53
|
+
- **停止**:`Ctrl+C`,会一并清理 stub / Vite。
|
|
54
|
+
- **浏览器直开降级**:独立 `pnpm dev`(不经 harness)时 `window.__PLUGIN_API__` 为 `null`,`getApi()` 返回 null,UI 需优雅降级(模板已处理)。
|
|
55
|
+
- 只读操作可放心跑;避免在调试页触发写数据 / 启动实例类操作。
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# manifest.json 字段全解
|
|
2
|
+
|
|
3
|
+
`manifest.json` 位于 `.qplugin` 包**根目录**,是插件唯一身份文件。本册字段以 `src/plugins/types.ts` 的 `PluginManifest` 与 CLI 校验器(`packages/qomicex-cli/src/lib/manifest.ts`)为准,与启动器 store 上传校验一致。
|
|
4
|
+
|
|
5
|
+
## 完整示例
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"id": "com.example.demo",
|
|
10
|
+
"name": "示例插件",
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"minLauncherVersion": "0.1.0",
|
|
13
|
+
"layers": ["l2"],
|
|
14
|
+
"permissions": ["config:read", "ui:toast", "network:cors_proxy"],
|
|
15
|
+
"dependencies": [
|
|
16
|
+
{ "id": "top.qomicex.markdown", "version": ">=1.0.0", "optional": false }
|
|
17
|
+
],
|
|
18
|
+
"entry": {
|
|
19
|
+
"frontend": "dist/index.html",
|
|
20
|
+
"theme": "dist/theme.css"
|
|
21
|
+
},
|
|
22
|
+
"render": "iframe",
|
|
23
|
+
"contributes": {
|
|
24
|
+
"menuItems": [
|
|
25
|
+
{ "path": "/plugins/p/com.example.demo", "label": "示例插件", "icon": "🧩", "action": "page" }
|
|
26
|
+
],
|
|
27
|
+
"overlay": {
|
|
28
|
+
"file": "dist/overlay.html",
|
|
29
|
+
"title": "示例悬浮窗",
|
|
30
|
+
"width": 380,
|
|
31
|
+
"height": 500,
|
|
32
|
+
"minimizable": true,
|
|
33
|
+
"resizable": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"icon": "fa-solid fa-puzzle-piece"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 字段总表
|
|
41
|
+
|
|
42
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
43
|
+
|------|------|------|------|
|
|
44
|
+
| `id` | string | ✅ | 插件唯一 ID。格式 `^[a-z0-9]+([.-][a-z0-9]+)*$`,3-128 字符,**必须含至少一个点**(反向域名,如 `com.example.demo`)。含点但非法字符 → error;不含点 → warning。一经发布不要更改(用作安装目录 `plugins/{id}/`) |
|
|
45
|
+
| `name` | string | ✅ | 显示名,非空 |
|
|
46
|
+
| `version` | string | ✅ | 严格 semver:`^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$`,如 `1.2.0`、`0.1.0-beta.1` |
|
|
47
|
+
| `minLauncherVersion` | string | ✅(CLI 校验) | 最低启动器版本。CLI 校验必填;运行时行为以启动器为准 |
|
|
48
|
+
| `layers` | string[] | ✅ | 图层声明,至少一项,值 ∈ `l0`/`l1`/`l2`/`l3`。声明 frontend 但无 `l2`/`l3` → 无法渲染 UI(warning) |
|
|
49
|
+
| `permissions` | string[] | ✅ | 权限声明,值必须是权限目录中的 id(见 `permissions.md`)。未知权限 → warning |
|
|
50
|
+
| `dependencies` | PluginDependency[] | 可选 | 前置插件依赖,见下 |
|
|
51
|
+
| `entry` | object | ✅ | 入口声明,`frontend`/`backend`/`theme` **至少一个** |
|
|
52
|
+
| `render` | 'inline' \| 'iframe' | 可选 | **默认 `iframe`**(沙箱)。仅显式 `"inline"` 走内联渲染(与主界面同 window) |
|
|
53
|
+
| `contributes` | object | 可选 | 扩展点 |
|
|
54
|
+
| `icon` | string | 可选 | 顶层图标(插件管理/列表显示;库插件建议用顶层 icon 而非 menuItems) |
|
|
55
|
+
|
|
56
|
+
## entry 对象
|
|
57
|
+
|
|
58
|
+
| 字段 | 类型 | 说明 |
|
|
59
|
+
|------|------|------|
|
|
60
|
+
| `frontend` | string | 插件页面入口,`.qplugin` 内相对路径,应指向 `.html`(如 `dist/index.html`)。声明了 frontend 的插件才会被激活并渲染到 `/plugins/p/:id` |
|
|
61
|
+
| `theme` | string | 主题 CSS 路径(如 `dist/theme.css`),激活时注入。引用 `dist/` 下文件但源码在根目录时,`qomicex pack` 会自动拷入 dist |
|
|
62
|
+
| `backend` | string | 保留字段,当前未使用(以代码为准) |
|
|
63
|
+
|
|
64
|
+
## layers 图层语义
|
|
65
|
+
|
|
66
|
+
| 层级 | 技术 | 说明 |
|
|
67
|
+
|------|------|------|
|
|
68
|
+
| `l0` | 静态声明 | 主题 / 声明式内容,纯声明无执行能力 |
|
|
69
|
+
| `l1` | 声明式 | 新增下载源 / 镜像 / 端点等声明(当前为预留层级) |
|
|
70
|
+
| `l2` | JS 前端沙箱 | **UI 插件默认层级**。iframe 沙箱(`sandbox="allow-scripts"`,opaque origin,与主界面 DOM/CSS 隔离),经 postMessage 桥做权限检查,`__PLUGIN_API__` 全量可用,`registerMethod`/`callPlugin` 跨窗口中转 |
|
|
71
|
+
| `l3` | WASM(wasmtime) | 后端沙箱执行 `plugin.wasm`,Host API 权限门控。包内含 `plugin.wasm` + 声明 `l3` 即被加载 |
|
|
72
|
+
|
|
73
|
+
- **render 默认 iframe**:带 `entry.frontend` 的插件默认走 iframe 沙箱。内联渲染(`"render":"inline"`)与主界面同 window,仅适合需要访问主界面 DOM 的轻量插件。
|
|
74
|
+
- `layers` 可声明多个(如 `["l2","l3"]`)。
|
|
75
|
+
- 纯 `["l3"]` 且无 `entry.frontend` 的插件不会自动激活渲染 UI。
|
|
76
|
+
|
|
77
|
+
## dependencies 依赖语法
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
"dependencies": [
|
|
81
|
+
{ "id": "top.qomicex.markdown", "version": ">=1.0.0", "optional": false }
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| 字段 | 类型 | 说明 |
|
|
86
|
+
|------|------|------|
|
|
87
|
+
| `id` | string | 被依赖插件 id |
|
|
88
|
+
| `version` | string | 可选,版本约束 |
|
|
89
|
+
| `optional` | boolean | 可选,是否非必装(缺省 false) |
|
|
90
|
+
|
|
91
|
+
- 安装时检查必装前置,缺失拒绝安装(`PLUGIN_MISSING_DEPENDENCY`)。
|
|
92
|
+
- 激活时检查前置已启用,缺失则本插件禁用。
|
|
93
|
+
- 激活顺序由依赖拓扑排序保证。
|
|
94
|
+
- `version` 约束写法(以代码 / 公开文档为准):`>=1.0.0`、`<=2.0.0`、`>1.0`、`<2.0`、`=1.2.0`、裸版本 `1.2.0`、空格分隔多约束 `">=1.0 <2.0"`。不支持 `^`/`~`/`*`/`||`。
|
|
95
|
+
|
|
96
|
+
## contributes 扩展点
|
|
97
|
+
|
|
98
|
+
| 字段 | 类型 | 说明 |
|
|
99
|
+
|------|------|------|
|
|
100
|
+
| `downloadSources` | string[] | 保留(当前未使用,以代码为准) |
|
|
101
|
+
| `commands` | string[] | 保留(当前未使用,以代码为准) |
|
|
102
|
+
| `settingsPages` | string[] | 保留(当前未使用,以代码为准) |
|
|
103
|
+
| `menuItems` | PluginMenuItem[] | 侧边栏入口列表 |
|
|
104
|
+
| `overlay` | object | 悬浮窗配置 |
|
|
105
|
+
|
|
106
|
+
### menuItems 数组元素
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
{ path: string; label: string; icon?: string; action?: 'page' | 'overlay' }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `path`:入口目标路由(如 `/plugins/p/:id`)。
|
|
113
|
+
- `icon`:emoji / 文本 / 绝对 URL / 包内相对路径(`dist/icon.svg`,启动器自动解析为 `http://localhost:5000/api/plugins/{id}/files/dist/icon.svg`)。
|
|
114
|
+
- `action`:`"page"`(跳转页面,默认)或 `"overlay"`(打开悬浮窗,需配合 `contributes.overlay`)。
|
|
115
|
+
|
|
116
|
+
### overlay 对象
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
{ file: string; title?: string; width?: number; height?: number; minimizable?: boolean; resizable?: boolean }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `file`:悬浮窗 HTML 文件路径(必填,指向 `.html`,如 `dist/overlay.html`)。
|
|
123
|
+
- `title` / `width` / `height` / `minimizable` / `resizable`:可选,视觉/行为参数(具体默认值以启动器代码为准)。
|
|
124
|
+
|
|
125
|
+
## CLI 校验行为(`qomicex verify` 目录模式)
|
|
126
|
+
|
|
127
|
+
- manifest 合法性:id / name / version(semver) / minLauncherVersion / layers / permissions / entry / contributes。
|
|
128
|
+
- 权限最小化:对比 `permissions` 与源码实际调用的桥方法(`METHOD_PERMISSIONS` 表),**声明未用 / 用了未声明都会报错**。
|
|
129
|
+
- 长循环告警:`while(true)`、`for(;;)`、无界 `setInterval`。
|
|
130
|
+
- 校验通过标准:**0 error**(warning 可接受但建议消除)。
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 权限目录与最小权限原则
|
|
2
|
+
|
|
3
|
+
权限目录源:`src/plugins/types.ts` 的 `PERMISSION_CATALOG`(启动器)与 `packages/qomicex-cli/src/lib/permissions.ts`(CLI verify,两者一致)。方法→权限映射源:CLI `src/lib/permissions.ts` 的 `METHOD_PERMISSIONS` 表(与 `src/plugins/sandbox.ts` 一致)。
|
|
4
|
+
|
|
5
|
+
## 风险分级
|
|
6
|
+
|
|
7
|
+
| 级别 | 含义 | 安装详情弹窗视觉 |
|
|
8
|
+
|------|------|------|
|
|
9
|
+
| `normal` | 只读 / 低影响 | 蓝 |
|
|
10
|
+
| `warning` | 写操作 / 网络 / 进程类 | 黄 |
|
|
11
|
+
| `danger` | 系统命令 / 文件写 / 装插件 | 红 |
|
|
12
|
+
|
|
13
|
+
**最小权限原则**:只声明真正用到的权限。`qomicex verify` 会扫描源码实际调用的桥方法(按 `METHOD_PERMISSIONS` 表),**声明了未用到的 → 报错;用了没声明的 → 报错**。因此 AI 生成插件时务必按"最终调用了哪些 API"反推权限集合,宁可少而准。
|
|
14
|
+
|
|
15
|
+
## 完整权限目录(39 项)
|
|
16
|
+
|
|
17
|
+
| 权限 ID | 风险 | 用途 |
|
|
18
|
+
|---------|------|------|
|
|
19
|
+
| `instance:read` | normal | 读取实例列表 |
|
|
20
|
+
| `instance:write` | warning | 创建/修改/删除实例(含安装整合包) |
|
|
21
|
+
| `account:read` | normal | 读取账号列表 |
|
|
22
|
+
| `license:read` | normal | 读取许可证信息 |
|
|
23
|
+
| `config:read` | normal | 读取启动器配置 / 插件配置 |
|
|
24
|
+
| `config:write` | warning | 修改启动器配置 / 插件配置 |
|
|
25
|
+
| `cache:access` | normal | 读写插件缓存 |
|
|
26
|
+
| `endpoint:discover` | normal | 获取后端 API 端点 |
|
|
27
|
+
| `page:list` | normal | 获取页面列表 |
|
|
28
|
+
| `network:fetch` | warning | 调用后端 API(callBackend)/ 插件互调 |
|
|
29
|
+
| `network:cors_proxy` | warning | CORS 代理请求(proxyFetch / proxyFetchStream) |
|
|
30
|
+
| `network:websocket` | warning | WebSocket 连接 |
|
|
31
|
+
| `network:proxy` | warning | 修改代理设置 |
|
|
32
|
+
| `ui:inject_sidebar` | normal | 注入侧边栏菜单 |
|
|
33
|
+
| `ui:inject_settings` | normal | 注入设置页 |
|
|
34
|
+
| `ui:picture_in_picture` | warning | 画中画窗口 |
|
|
35
|
+
| `ui:sub_window` | warning | 独立子窗口 / 悬浮窗 |
|
|
36
|
+
| `ui:context_menu` | normal | 注入右键菜单 |
|
|
37
|
+
| `ui:toast` | normal | 应用内 toast 通知 |
|
|
38
|
+
| `ui:navigate` | normal | 跳转页面 |
|
|
39
|
+
| `system:info` | normal | 读取系统和启动器信息 |
|
|
40
|
+
| `system:notification` | normal | 发送系统通知 / 打开外链 |
|
|
41
|
+
| `clipboard:read` | warning | 读取剪贴板 |
|
|
42
|
+
| `clipboard:write` | warning | 写入剪贴板 |
|
|
43
|
+
| `wasm:execute` | warning | 执行 WASM 模块(callWasm) |
|
|
44
|
+
| `plugin:install` | danger | 安装/卸载/更新插件 |
|
|
45
|
+
| `plugin:list` | normal | 读取已安装插件列表 |
|
|
46
|
+
| `resource:read` | normal | 读取游戏资源文件 |
|
|
47
|
+
| `resource:write` | warning | 写入游戏资源文件 |
|
|
48
|
+
| `java:manage` | warning | 管理 Java 运行时 |
|
|
49
|
+
| `download:manage` | warning | 管理下载中心任务 |
|
|
50
|
+
| `game:process` | warning | 启停游戏进程 |
|
|
51
|
+
| `game:log` | normal | 检测游戏日志 |
|
|
52
|
+
| `connector:host` | warning | 启停联机 |
|
|
53
|
+
| `connector:scan` | normal | 扫描局域网联机 |
|
|
54
|
+
| `shell:execute` | danger | 执行系统命令 |
|
|
55
|
+
| `filesystem:read` | warning | 读取文件系统 |
|
|
56
|
+
| `filesystem:write` | danger | 写入/删除文件系统 |
|
|
57
|
+
| (例外)`addMenuItem` | — | 动态注册侧边栏菜单,**无需声明权限** |
|
|
58
|
+
|
|
59
|
+
## 方法 → 权限映射(生成权限列表时照此反推)
|
|
60
|
+
|
|
61
|
+
| API 方法 | 所需权限 |
|
|
62
|
+
|----------|----------|
|
|
63
|
+
| `getSettings` | `config:read` |
|
|
64
|
+
| `setSettings`、`registerMethod` | `config:write` |
|
|
65
|
+
| `getCache` / `setCache` | `cache:access` |
|
|
66
|
+
| `callBackend`、`callPlugin` | `network:fetch` |
|
|
67
|
+
| `proxyFetch` / `proxyFetchStream` | `network:cors_proxy` |
|
|
68
|
+
| `uploadPlugin` | `plugin:install` |
|
|
69
|
+
| `callWasm` / `listWasmPlugins` | `wasm:execute` |
|
|
70
|
+
| `readText` / `readBytes` | `filesystem:read` |
|
|
71
|
+
| `writeText` / `writeBytes` / `deleteFile` | `filesystem:write` |
|
|
72
|
+
| `execCommand` | `shell:execute` |
|
|
73
|
+
| `navigate` | `config:read` |
|
|
74
|
+
| `showToast` | `ui:toast` |
|
|
75
|
+
| `getSystemInfo` | `system:info` |
|
|
76
|
+
| `openUrl` | `system:notification` |
|
|
77
|
+
| `listPlugins` | `plugin:list` |
|
|
78
|
+
| `overlay.*`(create/show/hide/destroy/setHtml/setPosition) | `ui:sub_window` |
|
|
79
|
+
| `download.addTask` / `.progress` / `.cancel` / `.list` | `download:manage` |
|
|
80
|
+
| `download.registerInstall` | `instance:write` |
|
|
81
|
+
| `modpack.install` | `instance:write` |
|
|
82
|
+
| `addMenuItem` | 无需权限 |
|
|
83
|
+
|
|
84
|
+
## 常见权限组合
|
|
85
|
+
|
|
86
|
+
- **纯 UI 插件**(模板默认):`config:read` + `ui:toast` + `network:cors_proxy`。
|
|
87
|
+
- 需要联网的插件:`network:cors_proxy`(外网请求)或 `network:fetch`(调启动器后端)。
|
|
88
|
+
- 需要持久化自己的配置:加 `config:write`(配合 `getSettings`/`setSettings`)。
|
|
89
|
+
- 需要文件读写:`filesystem:read`(读)或 `filesystem:write`(写/删,danger)。文件访问是**授权制**——首次访问用户弹窗确认,按路径前缀持久化。
|
|
90
|
+
|
|
91
|
+
## 模板默认 manifest
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"layers": ["l2"],
|
|
96
|
+
"permissions": ["config:read", "ui:toast", "network:cors_proxy"]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# 桥 API 签名速查
|
|
2
|
+
|
|
3
|
+
插件脚本通过全局 `window.__PLUGIN_API__` 与启动器交互。L2 iframe 沙箱 / 内联渲染均可用;独立 `pnpm dev`(浏览器直开)时优雅降级为 `null`。
|
|
4
|
+
|
|
5
|
+
完整文档见 `D:\docs\docs\plugins\plugin-api.md`(启动器公开文档,内容更全含完整示例与错误码)。
|
|
6
|
+
|
|
7
|
+
## 全局变量
|
|
8
|
+
|
|
9
|
+
| 变量 | 说明 |
|
|
10
|
+
|------|------|
|
|
11
|
+
| `window.__PLUGIN_API__` | 桥 API 对象(`null` = 浏览器直开,优雅降级) |
|
|
12
|
+
| `window.__PLUGIN_API_BASE__` | L2 沙箱内自动注入,值为 `http://localhost:5000/api/plugins/{id}/files`,用于拼包内资源地址 |
|
|
13
|
+
| `window.__PLUGIN_ID__` | 当前插件 id |
|
|
14
|
+
|
|
15
|
+
## 调用方式
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const api = window.__PLUGIN_API__
|
|
19
|
+
|
|
20
|
+
// ① 通用 call(绝大多数方法)
|
|
21
|
+
const data = await api.call('getSettings')
|
|
22
|
+
|
|
23
|
+
// ② 3 个专用快捷方式
|
|
24
|
+
api.registerMethod('name', fn) // 注册方法
|
|
25
|
+
await api.callPlugin('id', 'method', ...args) // 调用其他插件方法
|
|
26
|
+
await api.proxyFetchStream(req, { onChunk, onError }) // 流式请求
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 方法签名速查
|
|
30
|
+
|
|
31
|
+
| 方法 | 签名 | 所需权限 |
|
|
32
|
+
|------|------|----------|
|
|
33
|
+
| **getSettings** | `call('getSettings') → Record<string, unknown>` | `config:read` |
|
|
34
|
+
| **setSettings** | `call('setSettings', key, value)` | `config:write` |
|
|
35
|
+
| **setCache** | `call('setCache', key, value, ttlSeconds?)` | `cache:access` |
|
|
36
|
+
| **getCache** | `call('getCache', key) → any \| null` | `cache:access` |
|
|
37
|
+
| **callBackend** | `call('callBackend', endpoint, data?) → any` | `network:fetch` |
|
|
38
|
+
| | 有 data → POST;无 data → GET。data 可传 `_method` 覆盖 HTTP 方法 | |
|
|
39
|
+
| **proxyFetch** | `call('proxyFetch', { url, method?, headers?, body?, timeoutMs? }) → { status, headers, body?, bodyBase64? }` | `network:cors_proxy` |
|
|
40
|
+
| | SSRF 防护:仅 http/https,禁止内网地址 | |
|
|
41
|
+
| **proxyFetchStream** | `proxyFetchStream(req, { onChunk, onError })` | `network:cors_proxy` |
|
|
42
|
+
| | 消费 SSE 流,逐块回调。`req.signal` 可传 AbortSignal 中断 | |
|
|
43
|
+
| **registerMethod** | `registerMethod(name, fn)` | `config:write` |
|
|
44
|
+
| | 注册方法到全局注册表,供其他插件 `callPlugin` 调用。插件停用时自动注销 | |
|
|
45
|
+
| **callPlugin** | `callPlugin(pluginId, method, ...args) → any` | `network:fetch` |
|
|
46
|
+
| | 目标未安装/未激活/未注册方法 → reject | |
|
|
47
|
+
| **callWasm** | `call('callWasm', pluginId, exportName?) → { ok, result }` | `wasm:execute` |
|
|
48
|
+
| | 调用 L3 WASM 插件导出函数,缺省 `on_load` | |
|
|
49
|
+
| **listWasmPlugins** | `call('listWasmPlugins') → string[]` | `wasm:execute` |
|
|
50
|
+
| **readText** | `call('readText', path, options?) → { path, content }` | `filesystem:read` |
|
|
51
|
+
| | `options: { start?, length? }`。授权制(首次访问用户弹窗确认) | |
|
|
52
|
+
| **readBytes** | `call('readBytes', path, options?) → { path, contentBase64 }` | `filesystem:read` |
|
|
53
|
+
| | 同 readText 授权制 | |
|
|
54
|
+
| **writeText** | `call('writeText', path, content) → { path }` | `filesystem:write` |
|
|
55
|
+
| | 授权制,自动创建父目录 | |
|
|
56
|
+
| **writeBytes** | `call('writeBytes', path, bytes: Uint8Array) → { path }` | `filesystem:write` |
|
|
57
|
+
| | 授权制,自动创建父目录 | |
|
|
58
|
+
| **deleteFile** | `call('deleteFile', path) → { path }` | `filesystem:write` |
|
|
59
|
+
| | 仅文件不支持目录;授权制 | |
|
|
60
|
+
| **execCommand** | `call('execCommand', command, timeoutMs?) → { exitCode, stdout, stderr }` | `shell:execute` |
|
|
61
|
+
| | Windows → powershell,Unix → /bin/sh。默认超时 15s,范围 1-120s | |
|
|
62
|
+
| **getSystemInfo** | `call('getSystemInfo') → { Os, Architecture, LauncherVersion, ... }` | `system:info` |
|
|
63
|
+
| **openUrl** | `call('openUrl', url)` | `system:notification` |
|
|
64
|
+
| | 仅 http/https,沙箱内 window.open 被拦时应使用 | |
|
|
65
|
+
| **listPlugins** | `call('listPlugins') → [{ id, name, version, status }]` | `plugin:list` |
|
|
66
|
+
| **uploadPlugin** | `call('uploadPlugin', fileData: number[], fileName)` | `plugin:install` |
|
|
67
|
+
| | 安装 .qplugin 包 | |
|
|
68
|
+
| **navigate** | `call('navigate', path)` | `config:read` |
|
|
69
|
+
| | 跳转启动器内部路由,勿用外部 URL | |
|
|
70
|
+
| **showToast** | `call('showToast', message, type?)` | `ui:toast` |
|
|
71
|
+
| | `type: 'info' \| 'error' \| 'success'`,默认 info | |
|
|
72
|
+
| **overlay.create** | `call('overlay.create', { title?, html, x?, y?, width?, height?, minimizable?, resizable? }) → overlayId` | `ui:sub_window` |
|
|
73
|
+
| **overlay.show/hide/destroy** | `call('overlay.show/hide/destroy', overlayId)` | `ui:sub_window` |
|
|
74
|
+
| **overlay.setHtml** | `call('overlay.setHtml', overlayId, html)` | `ui:sub_window` |
|
|
75
|
+
| **overlay.setPosition** | `call('overlay.setPosition', overlayId, x, y)` | `ui:sub_window` |
|
|
76
|
+
| **download.addTask** | `call('download.addTask', { url, targetPath \| (instanceId, category, fileName), ... }) → { taskId }` | `download:manage` |
|
|
77
|
+
| | 支持实例+类别自动解析隔离目录。`extract: true` 下载后自动解压 zip | |
|
|
78
|
+
| **download.progress** | `call('download.progress', taskId) → snapshot \| null` | `download:manage` |
|
|
79
|
+
| **download.cancel** | `call('download.cancel', taskId)` | `download:manage` |
|
|
80
|
+
| **download.list** | `call('download.list') → snapshot[]` | `download:manage` |
|
|
81
|
+
| **download.registerInstall** | `call('download.registerInstall', { instanceId, name, gameVersion, loader, loaderVersion })` | `instance:write` |
|
|
82
|
+
| | 仅登记安装任务到下载中心,不创建真实下载 | |
|
|
83
|
+
| **modpack.install** | `call('modpack.install', { id, gameDir, path | (type, projectId, fileId), ... }) → { instanceId }` | `instance:write` |
|
|
84
|
+
| | 一键安装整合包(本地 zip / mrpack 或在线 Modrinth/CurseForge/FTB) | |
|
|
85
|
+
| **addMenuItem** | `addMenuItem(item: PluginMenuItem)` | 无需权限 |
|
|
86
|
+
| | 运行时动态注册侧边栏菜单项,停用时自动移除 | |
|
|
87
|
+
|
|
88
|
+
## error 处理
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
try {
|
|
92
|
+
await api.call('getSettings')
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(e.message)
|
|
95
|
+
// 常见错误:
|
|
96
|
+
// "Permission denied: requires xxx" —— manifest 权限未包含
|
|
97
|
+
// "Backend error: 404" —— callBackend 端点不存在
|
|
98
|
+
// "Proxy failed: 400" —— proxyFetch 参数错误(含 SSRF 拦截)
|
|
99
|
+
// "插件 xxx 未提供方法 yyy" —— callPlugin 目标不可用
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 文件拖放事件
|
|
104
|
+
|
|
105
|
+
主窗口广播 `file-drop` 事件(`DragDrop::Drop` 触发),payload = 文件绝对路径数组。沙箱插件需经主界面中转:主界面监听后 `callPlugin(pluginId, method, paths)` 转发。
|
|
106
|
+
|
|
107
|
+
## 模板 api.ts
|
|
108
|
+
|
|
109
|
+
模板项目 `src/api.ts` 提供了 `getApi()` 和 `getPluginId()` 辅助函数(使用 `window.__PLUGIN_API__` / `window.__PLUGIN_ID__`):
|
|
110
|
+
```ts
|
|
111
|
+
import { getApi, getPluginId } from './api.ts'
|
|
112
|
+
const api = getApi() // null if browser direct
|
|
113
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# AI 生成插件硬性规则
|
|
2
|
+
|
|
3
|
+
AI agent 生成/修改插件时必须逐条遵守。违反任何一条都可能导致 `qomicex verify` 不通过或运行时错误。
|
|
4
|
+
|
|
5
|
+
## manifest 校验
|
|
6
|
+
|
|
7
|
+
1. **id 格式**:`^[a-z0-9]+([.-][a-z0-9]+)*$`,3-128 字符,**必须含至少一个点**(反向域名,如 `com.example.demo`)。大写字母、空格、连续双点 → error。
|
|
8
|
+
2. **version**:严格 semver `数字.数字.数字`(可带 `-预发布` / `+构建号`),如 `0.1.0`、`1.2.0-beta.1`。
|
|
9
|
+
3. **minLauncherVersion**:必填字符串。
|
|
10
|
+
4. **layers**:至少一项,值 ∈ `l0`/`l1`/`l2`/`l3`。声明了 `entry.frontend` 但 layers 无 `l2`/`l3` → UI 无法渲染。
|
|
11
|
+
5. **permissions**:值是权限目录 id。未知权限 → warning。
|
|
12
|
+
6. **entry**:`frontend`/`backend`/`theme` 至少一个。`frontend` 应指向 `.html`(如 `dist/index.html`)。
|
|
13
|
+
7. **render**:默认 `iframe`(沙箱),仅显式 `"inline"` 走内联渲染。对 UI 插件建议不做修改,保持 iframe 默认。
|
|
14
|
+
|
|
15
|
+
## 权限
|
|
16
|
+
|
|
17
|
+
8. **最小权限原则**:只声明真正用到的权限。`qomicex verify` 会扫描源码,声明未用 / 用了未声明**都会报错**。按"最终调用了哪些 API 方法"反推权限集合,从 `permissions.md` 的 `METHOD_PERMISSIONS` 表查对应的权限 id。
|
|
18
|
+
9. **danger 权限**:`shell:execute`、`filesystem:write`、`plugin:install` 安装时红色提示,非必要不声明。
|
|
19
|
+
10. **addMenuItem 无需权限**:`addMenuItem` 不要求 manifest 声明任何权限。
|
|
20
|
+
|
|
21
|
+
## 代码
|
|
22
|
+
|
|
23
|
+
11. **TS import 带扩展名**:Vite 强制要求,`import { foo } from './bar.ts'`,不得省略 `.ts`/`.tsx`。例外:目录 barrel(`index.ts`)可省略。
|
|
24
|
+
12. **vite.config.ts 必须设 `base: './'`**:否则产物 `/assets/...` 被解析为站点根路径,插件白屏。
|
|
25
|
+
13. **不使用 `<a>` 做内部导航**:内部路由用 `<Link>`(React Router),`<a>` 触发整页刷新丢失持久状态。外部链接用 `openUrl` / `callBackend('/system/open-url', { url })`。
|
|
26
|
+
14. **沙箱内不用 `window.open`**:L2 iframe 的 `sandbox` 属性不含 `allow-popups`,`window.open` 被拦截。用 `openUrl` 方法或 `callBackend('/system/open-url', { url })`。
|
|
27
|
+
15. **不用 `fetch` 请求外部 URL**:用 `proxyFetch`(非流式)或 `proxyFetchStream`(流式,SSE),两者自带 SSRF 防护(禁止内网)和 CORS 代理。
|
|
28
|
+
16. **不虚构 API**:权限目录里有但桥 API 没有对应方法的能力(如 `clipboard:read`/`clipboard:write`、`network:websocket` 等当前无对应 `__PLUGIN_API__` 方法)不要臆造调用方式。以 `plugin-api.md` 列出的方法为准,不确定就标注「以代码为准」。
|
|
29
|
+
|
|
30
|
+
## 主题
|
|
31
|
+
|
|
32
|
+
17. **CSS 全用 `var(--*)`**:禁止 `#hex` / `rgb()` / `hsl()` 字面量。插件主题文件(`theme.css`)也只覆盖 `var()` token。
|
|
33
|
+
18. **Tailwind 用语义类名**:`bg-primary`、`text-foreground`、`text-muted-foreground`、`bg-muted`、`border-border` 等(`@qomicex/plugin-ui/tailwind-preset` 已映射)。
|
|
34
|
+
|
|
35
|
+
## 安全
|
|
36
|
+
|
|
37
|
+
19. **禁止硬编码密钥**:插件源码 / manifest / package.json 不得包含 API key、token、密码。用户配置经 `setSettings`/`getSettings` 存取。
|
|
38
|
+
20. **不滥用 `shell:execute`**:系统命令调用有 15s 超时(范围 1-120s),仅用于纯本地工具链,不传用户输入到 shell。
|
|
39
|
+
21. **文件读写经授权制**:`filesystem:read`/`write` 首次访问未授权路径时用户弹窗确认,按路径前缀持久化。插件不能假设用户一定会授权。
|
|
40
|
+
|
|
41
|
+
## 开发流程
|
|
42
|
+
|
|
43
|
+
22. **生成项目用 `qomicex create`**:不要手写 `manifest.json` 和项目结构,从模板起步。
|
|
44
|
+
23. **打包前必须 `qomicex verify`**:0 error 才算通过。warning 可接受但建议消除。
|
|
45
|
+
24. **不触碰启动器核心代码**:插件只修改自己目录内的文件,不动 `src/`、`src-tauri/`、`src-backend/` 等。
|
|
46
|
+
25. **不 git commit**:插件开发阶段不提交改动到仓库。
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Ed25519 签名流程
|
|
2
|
+
|
|
3
|
+
签名实现源:`packages/qomicex-cli/src/lib/signature.ts`。规范:**ADR-050 三级信任链**(商店根钥 → 开发者证书 → 包体签名)。与 store `src/lib/signature.ts`、launcher `plugin_signature.rs` 字节级一致。
|
|
4
|
+
|
|
5
|
+
## 签名载荷(规范化)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
payload = canonicalJson({
|
|
9
|
+
manifest: sha256Hex(manifest.json 原始字节),
|
|
10
|
+
files: [{ path, sha256 }...] // 按 path 升序
|
|
11
|
+
})
|
|
12
|
+
signedHash = SHA-256(payload) // 文本 UTF-8
|
|
13
|
+
signature = Ed25519(私钥, payload 的 UTF-8 字节)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `canonicalJson`:递归按键排序、无空白 JSON(键序对哈希无影响,保证确定性)。
|
|
17
|
+
- 包内 `signature.json` 与 `signature.cert.json` 本身不参与签名。
|
|
18
|
+
|
|
19
|
+
## 产物文件
|
|
20
|
+
|
|
21
|
+
| 文件 | 内容 |
|
|
22
|
+
|------|------|
|
|
23
|
+
| `signature.json` | `{ alg: "Ed25519", signedHash, signerKeyId, signature }` |
|
|
24
|
+
| `signature.cert.json` | 商店根钥签发的开发者证书:`{ alg, keyId, developerId, developerName, publicKey, issuedAt, signature }` |
|
|
25
|
+
|
|
26
|
+
验包要求:**两个文件都必须存在**,缺任一 → 未签名(`verify` 警告不拒绝);根钥验证书失败 / 包体验签失败 / 哈希不符 → 拒绝。
|
|
27
|
+
|
|
28
|
+
## 1. 生成密钥对
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
openssl genpkey -algorithm Ed25519 -out dev-key.pem
|
|
32
|
+
# 可选:提取 raw 32 字节 seed base64(publish 也接受 PEM,通常不必)
|
|
33
|
+
openssl pkey -in dev-key.pem -outform DER | tail -c 32 | base64
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
私钥支持三种格式:PKCS#8 PEM、PKCS#8 DER base64、raw 32 字节 seed base64。
|
|
37
|
+
|
|
38
|
+
> ⚠️ 私钥 = 开发者身份。**禁止**写入插件源码 / manifest / git 仓库 / 提交任何公开位置。使用环境变量 `QOMICEX_SIGN_KEY` 传入。
|
|
39
|
+
|
|
40
|
+
## 2. pack --key(本地签名,仅 signature.json)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
qomicex pack --key ./dev-key.pem
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `keyId = ed25519:{公钥 base64 前 8 字符}`。
|
|
47
|
+
- 若项目根存在 `signature.cert.json` 会自动带上(发布过一次后即有)。
|
|
48
|
+
- 无证书时 CLI 会警告:商店上传仍需完整证书,建议走 publish。
|
|
49
|
+
|
|
50
|
+
## 3. publish(完整证书链)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export QOMICEX_SIGN_KEY=<私钥 base64/PEM> # 或 --key ./dev-key.pem
|
|
54
|
+
qomicex publish
|
|
55
|
+
qomicex publish --changelog "修复 X" --yes
|
|
56
|
+
qomicex publish --api http://127.0.0.1:8787/api/v1 # 本地商店(wrangler dev)调试
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
流程(RFC 8628 设备流):
|
|
60
|
+
1. `POST /api/v1/auth/device/code` → 打印授权码 + 验证 URL → 轮询 `device/token` 拿访问令牌。
|
|
61
|
+
2. `POST /api/v1/developer/keys` 上传 Ed25519 公钥 → 商店根钥签发开发者证书(返回 `keyId` + 证书内容)。
|
|
62
|
+
3. 用私钥对包体签名,写入 `signature.json`,与证书一起打进 `.qplugin`。
|
|
63
|
+
4. 查找/创建插件记录(`/plugins/mine` → 无则 `POST /plugins`),确认后 `POST /plugins/:id/versions` multipart 上传。
|
|
64
|
+
5. 成功后将签名包存为 `release/<id>-<version>.signed.qplugin` 供复验。
|
|
65
|
+
|
|
66
|
+
## 4. 验包
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
qomicex verify --package ./release/x.qplugin
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- 用内置商店根公钥验签:`STORE_ROOT_PUBLIC_KEY_B64`(与 launcher `plugin_signature.rs` 的 `ROOT_PUBLIC_KEY_B64` 一致)。
|
|
73
|
+
- 未签名 → 提示"未签名"(警告,不拒绝);签名无效 → error 退出。
|
|
74
|
+
|
|
75
|
+
## 商店根公钥
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
sPKcrc6QR5gcOnQMdq21Jo3yqxN7Mbm61OYxZnKuHE0=
|
|
79
|
+
```
|
|
80
|
+
(raw base64 Ed25519 公钥;开发 / 自建商店需替换为对应根钥)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 主题语义 Token
|
|
2
|
+
|
|
3
|
+
规范源:`docs/junsi-dev-docs/2-架构设计/主题语义Token规范v1.md`(实现:`src/theme/`)。插件 UI 主题的**唯一正确做法**:全量经 `var(--*)` 消费语义 token,禁止内联色值。
|
|
4
|
+
|
|
5
|
+
## 三级语义模型
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
primitives(原始色板)→ semantic(语义角色)→ component(CSS 变量,唯一被 var() 读取)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
v1 落点:`.qtheme` 主题直接表达 semantic/component 层(即 `--*` 平铺变量)。插件不写主题,只**消费**这些变量。token 点分命名,`.` 归一化为 `-`(`background.emphasis` → `--background-emphasis`)。
|
|
12
|
+
|
|
13
|
+
## 色板 token(plugin-ui 消费全集)
|
|
14
|
+
|
|
15
|
+
| token(theme.json 键) | CSS 变量 | 默认值(dark) | 语义 |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| background | `--background` | `230 20% 6%` | 页面底色 |
|
|
18
|
+
| foreground | `--foreground` | `220 20% 93%` | 主文字 |
|
|
19
|
+
| card / card-foreground | `--card` / `--card-foreground` | `228 18% 10%` / `220 20% 93%` | 卡片 |
|
|
20
|
+
| popover / popover-foreground | `--popover` / `--popover-foreground` | `228 18% 10%` / `220 20% 93%` | 浮层 |
|
|
21
|
+
| primary / primary-foreground | `--primary` / `--primary-foreground` | `142 71% 48%` / `230 20% 6%` | 主强调 |
|
|
22
|
+
| secondary / secondary-foreground | `--secondary` / `--secondary-foreground` | `228 18% 14%` / `220 20% 93%` | 次级 |
|
|
23
|
+
| muted / muted-foreground | `--muted` / `--muted-foreground` | `228 10% 18%` / `228 8% 55%` | 弱化 |
|
|
24
|
+
| accent / accent-foreground | `--accent` / `--accent-foreground` | `228 18% 14%` / `220 20% 93%` | 强调底 |
|
|
25
|
+
| destructive / destructive-foreground | `--destructive` / `--destructive-foreground` | `0 84% 60%` / `220 20% 93%` | 危险 |
|
|
26
|
+
| border | `--border` | `228 14% 21%` | 边框 |
|
|
27
|
+
| input | `--input` | `228 14% 21%` | 输入框 |
|
|
28
|
+
| ring | `--ring` | `142 71% 48%` | 焦点环 |
|
|
29
|
+
|
|
30
|
+
扩展语义(可选,emit 为 `--foreground-accent` 等):`foreground.accent`、`foreground.muted`、`foreground.destructive`、`background.elevated`、`background.emphasis`、`background.sunken`、`border.strong`、`border.accent`、`accent.hover`、`accent.active`、`status.success`、`status.warning`、`status.error`。
|
|
31
|
+
|
|
32
|
+
## 非色 token
|
|
33
|
+
|
|
34
|
+
| token | CSS 变量 | 默认值 | 说明 |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| radius | `--radius` | `0.625rem` | 圆角 |
|
|
37
|
+
| glass-blur | `--glass-blur` | `18px` | 毛玻璃模糊 |
|
|
38
|
+
|
|
39
|
+
## var() 消费约定
|
|
40
|
+
|
|
41
|
+
- **全部用 `var(--*)`,禁止内联色值**(`#hex` / `rgb()` / `hsl()` 字面量)。plugin-ui 组件已全量 var() 消费,换主题即时生效,无需重建 dist。
|
|
42
|
+
- 颜色值多为 HSL 三元组(如 `142 71% 48%`),组件库经 `hsl(var(--primary))` 解析。插件自定义 CSS 需要时同样写 `hsl(var(--primary) / <alpha>)` 形式。
|
|
43
|
+
- Tailwind 侧直接用语义类名:`bg-primary`、`text-foreground`、`bg-muted`、`text-muted-foreground`、`border-border` 等(`@qomicex/plugin-ui/tailwind-preset` 已映射)。
|
|
44
|
+
|
|
45
|
+
## 主题贡献(entry.theme)
|
|
46
|
+
|
|
47
|
+
- manifest `entry.theme` 指向 `dist/theme.css`,激活时注入 `<style data-plugin-theme>`。
|
|
48
|
+
- 主题 CSS 只能**覆盖/补充** token,仍须全部 `var()` 引用,禁止内联色值:
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
:root[data-theme] {
|
|
52
|
+
/* 可选:覆盖默认 HSL token */
|
|
53
|
+
--primary: 142 71% 48%;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `qomicex pack` 会自动把根目录的 `theme.css` 拷入 `dist/theme.css`(若 manifest 引用 `dist/theme.css`)。
|
|
58
|
+
- **不要在插件里定义与启动器冲突的平铺变量**;需要私有样式时走组件 class(Tailwind)或局部作用域。
|