@chuchen3d/wallpaper-plugin-sdk 0.0.0-stage → 1.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.
- package/LICENSE +20 -0
- package/README.md +449 -2
- package/package.json +57 -4
- package/src/cli.js +99 -0
- package/src/commands/build.js +135 -0
- package/src/commands/example.js +375 -0
- package/src/commands/init.js +200 -0
- package/src/commands/validate.js +76 -0
- package/src/index.js +40 -0
- package/src/templates/dev-project.js +357 -0
- package/src/templates/manifest.js +143 -0
- package/src/templates/plugin-entry.js +82 -0
- package/src/templates/vue-component.js +213 -0
- package/src/utils/colors.js +31 -0
- package/src/utils/packager.js +210 -0
- package/src/utils/paramsConverter.js +44 -0
- package/src/utils/validator.js +119 -0
- package/src/utils/viteBuilder.js +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 沈阳市浑南区初尘软件工作室
|
|
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 DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,450 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🧩 @chuchen3d/wallpaper-plugin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
初尘壁纸插件开发工具包 — 创建、预览、打包 `.yizirwp` 壁纸插件。
|
|
4
|
+
|
|
5
|
+
- CLI:`chuchen-plugin init / build / validate / example`
|
|
6
|
+
- 程序化调用:`import { ... } from '@chuchen3d/wallpaper-plugin-sdk'`
|
|
7
|
+
|
|
8
|
+
> **v1.1.0 起为真隔离沙箱模型**:插件运行在宿主创建的跨源 iframe 沙箱内,
|
|
9
|
+
> 构建产物**自包含**(Vue/Three 全部打进 IIFE bundle,不依赖宿主注入的运行时),
|
|
10
|
+
> manifest 新增 `apiVersion` 与 `network.domains` 字段,联网必须在 manifest 声明。
|
|
11
|
+
> 详见下文 [沙箱运行时](#沙箱运行时v110-起)。组件 props 契约不变,存量组件代码零改动,
|
|
12
|
+
> 用新 SDK 重跑 `build` 即完成迁移。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 全局安装
|
|
18
|
+
npm install -g @chuchen3d/wallpaper-plugin-sdk
|
|
19
|
+
|
|
20
|
+
# 或从私有仓库安装
|
|
21
|
+
npm install -g @chuchen3d/wallpaper-plugin-sdk --registry=https://registry.yiziran.com.cn/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> 运行时依赖:Node ≥ 16。项目内需 `vue ^3.4`、`vite ^5.2`、`@vitejs/plugin-vue ^5.0`;使用 Three.js 时需 `three ^0.162`(可选,会随包打进 bundle,zip 体积 +150~250KB 属正常)。
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
chuchen-plugin init my-wallpaper # 初始化项目(manifest 自动带 apiVersion/network 字段)
|
|
30
|
+
cd my-wallpaper
|
|
31
|
+
npm install
|
|
32
|
+
npm run dev # 启动预览,浏览器右侧实时调节参数
|
|
33
|
+
# 编辑 MyWallpaper.vue 实现动画
|
|
34
|
+
chuchen-plugin build # 打包为自包含 my-wallpaper-1.0.0.yizirwp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
将生成的 `.yizirwp` 文件在「初尘壁纸 → 功能面板 → 开发者中心」中上传插件即可,之后管理员审核后即可在壁纸商店中下载。
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## CLI 命令
|
|
42
|
+
|
|
43
|
+
| 命令 | 说明 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| `chuchen-plugin init <项目名>` | 初始化壁纸插件项目 |
|
|
46
|
+
| `chuchen-plugin build` | 编译 Vue SFC 并打包为 `.yizirwp` |
|
|
47
|
+
| `chuchen-plugin validate` | 校验 `manifest.json` |
|
|
48
|
+
| `chuchen-plugin example [项目名]` | 生成完整示例项目(粒子之舞) |
|
|
49
|
+
|
|
50
|
+
### init 选项
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
chuchen-plugin init my-wallpaper \
|
|
54
|
+
--name "我的壁纸" \
|
|
55
|
+
--description "一款精美的壁纸" \
|
|
56
|
+
--author "开发者" \
|
|
57
|
+
--category abstract \ # natural/tech/abstract/festival/utility
|
|
58
|
+
--three # 启用 Three.js 支持
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### build 选项
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
chuchen-plugin build \
|
|
65
|
+
--output my-plugin.yizirwp \ # 自定义输出文件名
|
|
66
|
+
--dry-run # 仅校验,不生成文件
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 预览视频/GIF(可选)
|
|
70
|
+
|
|
71
|
+
插件可以附带一段预览视频或 GIF,商店详情页会循环播放。
|
|
72
|
+
|
|
73
|
+
1. 在项目根放一个预览文件,文件名必须是 `preview.{mp4,webm,mov,gif}`:
|
|
74
|
+
```bash
|
|
75
|
+
cp ~/my-preview.mp4 ./preview.mp4
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
2. 打包(SDK 自动检测并嵌入):
|
|
79
|
+
```bash
|
|
80
|
+
npx chuchen-plugin build
|
|
81
|
+
# 或自定义文件名/路径:
|
|
82
|
+
npx chuchen-plugin build --preview ./assets/preview.mp4
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
3. 上传插件包到服务端。服务端首次上传时会自动抽取预览到独立存储。
|
|
86
|
+
|
|
87
|
+
4. 替换预览:在 admin 后台 → 审核详情 → "替换预览",或客户端开发者中心 → "预览管理"。
|
|
88
|
+
不需要重新上传整个 .yizirwp。
|
|
89
|
+
|
|
90
|
+
**文件约束**
|
|
91
|
+
|
|
92
|
+
| 格式 | 扩展名 | 上限 |
|
|
93
|
+
|------|--------|------|
|
|
94
|
+
| 视频 | mp4 / webm / mov | 10 MB |
|
|
95
|
+
| 动图 | gif | 5 MB |
|
|
96
|
+
|
|
97
|
+
**注意**:服务端 mime 推断**只用扩展名**,请确保文件扩展名与实际类型一致。
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 项目结构
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
my-wallpaper/
|
|
105
|
+
├── manifest.json # 插件元数据(必选)
|
|
106
|
+
├── MyWallpaper.vue # 壁纸组件(必选,文件名与 entry 对应)
|
|
107
|
+
├── package.json # 开发依赖(不打入包)
|
|
108
|
+
├── vite.config.js # Vite 配置(不打入包)
|
|
109
|
+
├── index.html # 预览入口(不打入包)
|
|
110
|
+
├── dev-preview.js # Mock props 注入脚本(不打入包)
|
|
111
|
+
├── thumbnail.png # 缩略图(可选,256×256 推荐,自动打入包)
|
|
112
|
+
└── README.md # 开发说明(不打入包)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## manifest.json
|
|
118
|
+
|
|
119
|
+
### 完整示例
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"schemaVersion": "2.0",
|
|
124
|
+
"id": "code-planet",
|
|
125
|
+
"name": "雷电",
|
|
126
|
+
"version": "1.0.2",
|
|
127
|
+
"author": { "name": "忆自然咖啡" },
|
|
128
|
+
"description": "一款精美的初尘壁纸插件-雷电",
|
|
129
|
+
"tags": ["自然", "奇景", "线状闪电"],
|
|
130
|
+
"category": "abstract",
|
|
131
|
+
"entry": "CodePlanet.vue",
|
|
132
|
+
"files": ["CodePlanet.vue"],
|
|
133
|
+
"params": {
|
|
134
|
+
"frequency": {
|
|
135
|
+
"type": "number", "default": 5, "min": 1, "max": 15, "step": 1,
|
|
136
|
+
"label": "闪电频率(秒)"
|
|
137
|
+
},
|
|
138
|
+
"intensity": {
|
|
139
|
+
"type": "number", "default": 1.0, "min": 0.1, "max": 2.0, "step": 0.1,
|
|
140
|
+
"label": "亮度强度"
|
|
141
|
+
},
|
|
142
|
+
"colorTheme": {
|
|
143
|
+
"type": "select", "default": "blue",
|
|
144
|
+
"options": ["blue", "purple", "white", "warm"],
|
|
145
|
+
"label": "颜色主题"
|
|
146
|
+
},
|
|
147
|
+
"cameraHeight": {
|
|
148
|
+
"type": "number", "default": 11, "min": 2, "max": 30, "step": 0.5,
|
|
149
|
+
"label": "相机高度",
|
|
150
|
+
"tier": "premium"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"previewBg": "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)",
|
|
154
|
+
"emoji": "🎨",
|
|
155
|
+
"minAppVersion": "1.0.0",
|
|
156
|
+
"apiVersion": 1,
|
|
157
|
+
"network": { "domains": [] },
|
|
158
|
+
"isPremium": false,
|
|
159
|
+
"originalPrice": 19.9,
|
|
160
|
+
"price": 9.9,
|
|
161
|
+
"tier": "yearly"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 字段说明
|
|
166
|
+
|
|
167
|
+
#### 元数据(基础)
|
|
168
|
+
|
|
169
|
+
| 字段 | 类型 | 必需 | 说明 |
|
|
170
|
+
|------|------|:----:|------|
|
|
171
|
+
| `schemaVersion` | string | ✅ | Schema 版本(`2.0`) |
|
|
172
|
+
| `id` | string | ✅ | 插件 ID,kebab-case(如 `my-wallpaper`) |
|
|
173
|
+
| `name` | string | ✅ | 显示名称 |
|
|
174
|
+
| `version` | string | ✅ | 语义化版本(如 `1.0.0`) |
|
|
175
|
+
| `author` | object | ✅ | `{ name, email? }` |
|
|
176
|
+
| `entry` | string | ✅ | 入口 `.vue` 文件名 |
|
|
177
|
+
| `files` | string[] | ✅ | 打包文件列表(构建产物由 index.js 替代,此字段仅源码声明) |
|
|
178
|
+
| `minAppVersion` | string | ✅ | 最低应用版本 |
|
|
179
|
+
|
|
180
|
+
#### 展示(推荐)
|
|
181
|
+
|
|
182
|
+
| 字段 | 类型 | 说明 |
|
|
183
|
+
|------|------|------|
|
|
184
|
+
| `description` | string | 描述 |
|
|
185
|
+
| `tags` | string[] | 标签数组 |
|
|
186
|
+
| `category` | string | 分类:`natural` / `tech` / `abstract` / `festival` / `utility` |
|
|
187
|
+
| `previewBg` | string | 商店预览背景(CSS 渐变) |
|
|
188
|
+
| `emoji` | string | Emoji 图标 |
|
|
189
|
+
|
|
190
|
+
#### 商业化(可选)
|
|
191
|
+
|
|
192
|
+
| 字段 | 类型 | 说明 |
|
|
193
|
+
|------|------|------|
|
|
194
|
+
| `isPremium` | boolean | 是否付费插件 |
|
|
195
|
+
| `price` | number | 售价(非负) |
|
|
196
|
+
| `originalPrice` | number | 原价(非负,用于划线价) |
|
|
197
|
+
| `tier` | string | 订阅档位:`free` / `monthly` / `yearly` / `lifetime` |
|
|
198
|
+
|
|
199
|
+
#### 沙箱契约(SDK ≥1.1.0,init 模板自动生成)
|
|
200
|
+
|
|
201
|
+
| 字段 | 类型 | 说明 |
|
|
202
|
+
|------|------|------|
|
|
203
|
+
| `apiVersion` | number | 宿主插件 API 大版本,当前为 `1`。宿主按它做版本门禁:包要求版本高于宿主支持时**拒绝加载**并提示用户升级应用。init 生成的模板自带,**勿手改**;存量项目手动补 `"apiVersion": 1` 即可 |
|
|
204
|
+
| `network` | object | `{ domains: string[] }`,≤8 个 https 域名(裸域名自动补 `https://`,带 path 会被截断)。**空数组/缺省 = 默认全禁网**:沙箱 CSP 只放行声明域名,fetch/XHR/WebSocket/图片/音频/字体等一切远程请求访问未声明域名都会被浏览器拦截 |
|
|
205
|
+
|
|
206
|
+
联网壁纸示例(如拉取天气 API):
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
"apiVersion": 1,
|
|
210
|
+
"network": { "domains": ["https://devapi.qweather.com"] }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`validate` / `build` 会校验:`apiVersion` 为 ≥1 的整数(缺省合法,视为 v1);
|
|
214
|
+
`network` 为对象、`domains` 为合法 https 域名的字符串数组且 ≤8 项。
|
|
215
|
+
声明与代码不符的包会在服务端审核的风险报告中标记(未声明域名 / 声明未使用)。
|
|
216
|
+
|
|
217
|
+
### params 字段
|
|
218
|
+
|
|
219
|
+
`params` 是「参数 key → 定义」的对象,key 即组件中 `paramValues` 的读取键。支持四种控件类型:
|
|
220
|
+
|
|
221
|
+
| type | 字段 | 控件 |
|
|
222
|
+
|------|------|------|
|
|
223
|
+
| `number` | `default` / `min` / `max` / `step` / `label` | 滑块 |
|
|
224
|
+
| `select` | `default` / `options` / `label` | 下拉 |
|
|
225
|
+
| `toggle` | `default` / `label` | 开关 |
|
|
226
|
+
| `color` | `default` / `label` | 颜色选择器 |
|
|
227
|
+
|
|
228
|
+
- 所有类型均可选填 **`tier`**(如 `"premium"`),标记为会员专属参数。
|
|
229
|
+
- `select` 的 `options` 支持两种写法:
|
|
230
|
+
- **字符串数组**(简写):`["blue", "purple", "white"]`
|
|
231
|
+
- **对象数组**(带显示标签):`[{ "value": "rainbow", "label": "彩虹" }]`
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"speed": { "type": "number", "default": 1.0, "min": 0.1, "max": 3.0, "step": 0.1, "label": "动画速度" },
|
|
236
|
+
"colorPreset": {
|
|
237
|
+
"type": "select", "default": "rainbow",
|
|
238
|
+
"options": [
|
|
239
|
+
{ "value": "rainbow", "label": "彩虹" },
|
|
240
|
+
{ "value": "ocean", "label": "海洋蓝" }
|
|
241
|
+
],
|
|
242
|
+
"label": "配色方案"
|
|
243
|
+
},
|
|
244
|
+
"mouseInteraction": { "type": "toggle", "default": true, "label": "鼠标交互" },
|
|
245
|
+
"bgColor": { "type": "color", "default": "#0c0c1d", "label": "背景色" }
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
> 打包时 SDK 会把 `params` 自动转换为 `{ defaults, definitions }` 格式,开发者保持扁平写法即可。
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 沙箱运行时(v1.1.0 起)
|
|
254
|
+
|
|
255
|
+
宿主把 `.yizirwp` 解包后,让插件运行在 `sandbox="allow-scripts"` 的跨源 iframe(opaque origin)内。这是浏览器层面的隔离保证:
|
|
256
|
+
|
|
257
|
+
| 能力 | 状态 | 替代方案 |
|
|
258
|
+
|------|------|---------|
|
|
259
|
+
| 宿主 localStorage / cookie | ❌ 不可达(访问直接抛 SecurityError) | `__YIZIRAN_PLUGIN_API__.storage`(见下) |
|
|
260
|
+
| `window.parent` / `window.top`(宿主 DOM/位置/存储) | ❌ 跨源拦截 | ——(插件不应针对宿主) |
|
|
261
|
+
| Tauri IPC(`__TAURI__` / `__TAURI_IPC__`) | ❌ 沙箱内不存在 | ——(插件无系统能力) |
|
|
262
|
+
| 联网(fetch/XHR/WebSocket/图片/音频/字体) | ⚠️ 仅放行 manifest `network.domains` 声明的域名 | 未声明域名一律被沙箱 CSP 拦截 |
|
|
263
|
+
| `window.open` 弹窗 | ❌ 禁止 | —— |
|
|
264
|
+
| 与宿主通信 | 仅 postMessage 协议 | SDK 入口已封装,勿直接调 `postMessage` |
|
|
265
|
+
|
|
266
|
+
### 插件内持久化(storage KV)
|
|
267
|
+
|
|
268
|
+
沙箱内 `localStorage` 不可用。需要持久化(API key 缓存、用户偏好等)时使用宿主代理的 KV 存储:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
const api = window.__YIZIRAN_PLUGIN_API__
|
|
272
|
+
|
|
273
|
+
await api.storage.set('myKey', { a: 1 })
|
|
274
|
+
const val = await api.storage.get('myKey') // null = 不存在
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
配额:单值 ≤16KB、键数 ≤32、总量 ≤64KB;按插件 ID 命名空间隔离,卸载插件时清空。
|
|
278
|
+
key 仅允许字母/数字/`_`/`.`/`-`,长度 ≤64。
|
|
279
|
+
|
|
280
|
+
### 沙箱内可用的宿主 API(`window.__YIZIRAN_PLUGIN_API__`)
|
|
281
|
+
|
|
282
|
+
参数与性能策略由 SDK 入口包装器自动接入组件 props(`paramValues` / `performancePolicy`),
|
|
283
|
+
通常无需直接调用;以下按需使用:
|
|
284
|
+
|
|
285
|
+
| 方法 | 说明 |
|
|
286
|
+
|------|------|
|
|
287
|
+
| `storage.get(key)` / `storage.set(key, value)` | 宿主侧 KV 持久化(Promise) |
|
|
288
|
+
| `getMousePosition()` | `{x, y}` 或 null |
|
|
289
|
+
| `getContainerSize()` | `{width, height}` |
|
|
290
|
+
| `setCaptureHandler(fn)` | 覆盖默认截图:`fn` 返回 `{ dataUrl, width, height }`(多 canvas 场景用) |
|
|
291
|
+
| `getPluginId()` | 当前插件 ID |
|
|
292
|
+
| `log(level, ...args)` / `trackEvent(name, data)` | 日志转发宿主 / 埋点桥接 |
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Vue 组件规范
|
|
297
|
+
|
|
298
|
+
### 必需 Props
|
|
299
|
+
|
|
300
|
+
宿主通过入口包装器向组件注入 3 个 props:
|
|
301
|
+
|
|
302
|
+
```vue
|
|
303
|
+
<script setup>
|
|
304
|
+
const props = defineProps({
|
|
305
|
+
/** 容器 DOM 引用(取尺寸、绑事件) */
|
|
306
|
+
containerRef: { type: Object, default: null },
|
|
307
|
+
/** 用户调节的参数值,key 与 manifest.params 一一对应,实时更新 */
|
|
308
|
+
paramValues: { type: Object, default: () => ({}) },
|
|
309
|
+
/** 性能策略:fpsLimit(0=暂停)、pixelRatioCap、mouseInteraction */
|
|
310
|
+
performancePolicy: { type: Object, default: () => ({}) },
|
|
311
|
+
})
|
|
312
|
+
</script>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 生命周期骨架
|
|
316
|
+
|
|
317
|
+
```vue
|
|
318
|
+
<script setup>
|
|
319
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
320
|
+
|
|
321
|
+
const canvasRef = ref(null)
|
|
322
|
+
let animationId = null
|
|
323
|
+
|
|
324
|
+
/** 安全读取参数(带默认值回退) */
|
|
325
|
+
function getParam(key, defaultValue) {
|
|
326
|
+
const val = props.paramValues?.[key]
|
|
327
|
+
return val !== undefined ? val : defaultValue
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
onMounted(() => {
|
|
331
|
+
init() // 建场景、绑事件、启动动画
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
onUnmounted(() => {
|
|
335
|
+
if (animationId) cancelAnimationFrame(animationId)
|
|
336
|
+
// 移除事件监听;释放 GPU 资源(dispose / forceContextLoss)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
function animate() {
|
|
340
|
+
animationId = requestAnimationFrame(animate)
|
|
341
|
+
const fpsLimit = props.performancePolicy?.fpsLimit ?? 60
|
|
342
|
+
if (fpsLimit === 0) return // 暂停渲染
|
|
343
|
+
// 读取参数并绘制
|
|
344
|
+
}
|
|
345
|
+
</script>
|
|
346
|
+
|
|
347
|
+
<template>
|
|
348
|
+
<canvas ref="canvasRef" class="wallpaper-canvas"
|
|
349
|
+
style="width:100%;height:100%;display:block;"></canvas>
|
|
350
|
+
</template>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
> ★ **样式必须内联在模板上**(`style="..."`):构建时 `<style scoped>` 会输出到
|
|
354
|
+
> `dist/style.css`,而打包产物只含 `index.js` —— 样式表不进包,壁纸尺寸/布局会
|
|
355
|
+
> 整个丢掉。动态样式用 JS 设置 `el.style.xxx`。
|
|
356
|
+
|
|
357
|
+
### Three.js 壁纸
|
|
358
|
+
|
|
359
|
+
- `three` 随包打进自包含 IIFE bundle(v1.1.0 起不再依赖宿主注入的运行时),组件内 `import * as THREE from 'three'` 即可。
|
|
360
|
+
- 项目 `package.json` 中 `three` 版本对齐 `^0.162`,避免本地预览与打包产物 API 不一致。
|
|
361
|
+
- 着色器以内联 JS 模板字面量编写,**不需要** `vite-plugin-glsl`。
|
|
362
|
+
- `onUnmounted` 必须 dispose 全部 geometry / material / texture,并 `renderer.forceContextLoss()`;沙箱在壁纸移除时会销毁整个 iframe 上下文兜底,但优雅清理仍是组件的责任。
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 截图兼容性要求
|
|
367
|
+
|
|
368
|
+
宿主「截图与分享」面板依赖以下契约才能正确截取插件画面。**强烈建议遵循** —— 不满足时截图会失败或得到空白图。
|
|
369
|
+
|
|
370
|
+
| 要求 | 必填 | 说明 |
|
|
371
|
+
|------|:----:|------|
|
|
372
|
+
| canvas 是组件模板内的**第一个** canvas | ✅ | 宿主截图协议取沙箱 iframe 内 `querySelector('canvas')` 的第一个元素。多 canvas 场景用 `__YIZIRAN_PLUGIN_API__.setCaptureHandler(fn)` 指定(fn 返回 `{ dataUrl, width, height }`)。class `wallpaper-canvas` 建议保留(兼容旧宿主回退路径) |
|
|
373
|
+
| WebGL renderer 设 `preserveDrawingBuffer: true` | ✅ | Three.js: `new THREE.WebGLRenderer({ canvas, preserveDrawingBuffer: true })`。否则 `canvas.toBlob()` 拿到的 PNG 是空白/透明帧(不是错误,但图像无效)。 |
|
|
374
|
+
| 不加载无 CORS 头的跨域图片 | ✅ | 否则 canvas 被 taint,`canvas.toBlob()` 抛 `SecurityError`。使用跨域纹理时需服务器返回 `Access-Control-Allow-Origin` 头或设置 `crossOrigin = 'anonymous'`,**且该图片域名必须已声明进 manifest `network.domains`**(沙箱 CSP 对 img-src 同样管控)。 |
|
|
375
|
+
| canvas 写在组件模板内 | ✅ | 标准流程自动满足——SDK 入口把 Vue 应用挂载到沙箱 iframe 内的铺满 div 上。 |
|
|
376
|
+
|
|
377
|
+
### Three.js 截图友好写法示例
|
|
378
|
+
|
|
379
|
+
```js
|
|
380
|
+
import * as THREE from 'three'
|
|
381
|
+
|
|
382
|
+
function init() {
|
|
383
|
+
const canvas = canvasRef.value
|
|
384
|
+
const renderer = new THREE.WebGLRenderer({
|
|
385
|
+
canvas,
|
|
386
|
+
preserveDrawingBuffer: true, // ★ 让 canvas.toBlob() 能拿到帧
|
|
387
|
+
antialias: true,
|
|
388
|
+
alpha: true,
|
|
389
|
+
})
|
|
390
|
+
// ...
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Canvas 2D 注意事项
|
|
395
|
+
|
|
396
|
+
如果用纯 Canvas 2D(无需 WebGL),`preserveDrawingBuffer` 不适用 —— `toBlob` 直接可用,但同样**不要加载跨域图**。
|
|
397
|
+
|
|
398
|
+
### 调试技巧
|
|
399
|
+
|
|
400
|
+
打开 DevTools,在 Console 执行(dev 预览页或宿主沙箱内均可):
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
const c = document.querySelector('canvas')
|
|
404
|
+
console.log(c?.toDataURL('image/png')?.length, 'bytes') // >1000 表示 canvas 有内容
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
若长度很小(< 1000),canvas 是空白 —— 检查 `preserveDrawingBuffer` 是否开启。
|
|
408
|
+
|
|
409
|
+
若壁纸布局/尺寸整个不对 —— 检查样式是否写在了 `<style scoped>` 里(不入包,改内联)。
|
|
410
|
+
若 fetch/图片报 CSP 拦截错误 —— 域名未声明进 manifest `network.domains`,声明后重新 build。
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 打包规则
|
|
415
|
+
|
|
416
|
+
打包 `.yizirwp` 时,自动**排除**以下文件(仅开发用):
|
|
417
|
+
|
|
418
|
+
- `node_modules/`、`.git/`、`.gitignore`、`.DS_Store`、`Thumbs.db`
|
|
419
|
+
- `package.json`、`package-lock.json`、`yarn.lock`、`pnpm-lock.yaml`
|
|
420
|
+
- `vite.config.js`、`index.html`、`dev-preview.js`
|
|
421
|
+
- `README.md`、`ICON_GUIDE.md`
|
|
422
|
+
|
|
423
|
+
**打包产物**:`manifest.json`(重写 `entry=index.js` + params 格式转换)、`index.js`(**自包含** IIFE bundle,内含整套 Vue/Three 运行时)、`thumbnail.*`(如有)、`preview/<file>`(如有)。
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 程序化 API
|
|
428
|
+
|
|
429
|
+
SDK 也支持在脚本中调用:
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
import {
|
|
433
|
+
cmdInit, cmdBuild, cmdValidate, cmdExample, // 命令
|
|
434
|
+
generateManifest, generateVueComponentTemplate, generateDevProjectFiles, // 模板
|
|
435
|
+
generatePluginEntry, // 沙箱入口包装器生成
|
|
436
|
+
validateManifest, convertParamsToAppFormat, // 工具
|
|
437
|
+
buildPlugin, packProject, packFromDist, // 构建/打包
|
|
438
|
+
VERSION, PACKAGE_NAME,
|
|
439
|
+
} from '@chuchen3d/wallpaper-plugin-sdk'
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 许可证
|
|
445
|
+
|
|
446
|
+
MIT
|
|
447
|
+
|
|
448
|
+
## 相关链接
|
|
449
|
+
|
|
450
|
+
- [初尘3D壁纸官网](https://www.yiziran.com.cn)
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chuchen3d/wallpaper-plugin-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "初尘3D壁纸插件开发工具包 — 创建、预览、打包 .yizirwp 壁纸插件",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chuchen-plugin": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"publish:private": "npm publish --registry=https://registry.yiziran.com.cn/",
|
|
18
|
+
"publish:public": "npm publish --registry=https://registry.npmjs.org/ --access public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"chuchen",
|
|
22
|
+
"wallpaper",
|
|
23
|
+
"plugin-sdk",
|
|
24
|
+
"vue3",
|
|
25
|
+
"threejs"
|
|
26
|
+
],
|
|
27
|
+
"author": "Chuchen Team <chuchen_software@163.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": ""
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=16.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vue": "^3.4.0",
|
|
38
|
+
"vite": "^5.2.0",
|
|
39
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
40
|
+
"three": "^0.162.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"three": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"commander": "^12.0.0",
|
|
49
|
+
"chalk": "^4.1.2",
|
|
50
|
+
"jszip": "^3.10.1",
|
|
51
|
+
"ora": "^5.4.1",
|
|
52
|
+
"vite": "^5.2.0",
|
|
53
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
54
|
+
"vue": "^3.4.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"vitest": "^1.6.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ════════════════════════════════════════════════════════════
|
|
5
|
+
* 🧩 初尘3D壁纸 — 插件 SDK CLI 入口
|
|
6
|
+
* ════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* 使用方式:
|
|
9
|
+
* chuchen-plugin init <项目名>
|
|
10
|
+
* chuchen-plugin build
|
|
11
|
+
* chuchen-plugin validate
|
|
12
|
+
* chuchen-plugin example
|
|
13
|
+
* chuchen-plugin help
|
|
14
|
+
*
|
|
15
|
+
* @author Chuchen Team
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Command } from 'commander'
|
|
19
|
+
import chalk from 'chalk'
|
|
20
|
+
import { createRequire } from 'node:module'
|
|
21
|
+
import { cmdInit } from './commands/init.js'
|
|
22
|
+
import { cmdBuild } from './commands/build.js'
|
|
23
|
+
import { cmdValidate } from './commands/validate.js'
|
|
24
|
+
import { cmdExample } from './commands/example.js'
|
|
25
|
+
|
|
26
|
+
// 版本号动态读 package.json(单一事实源,避免发版忘改两处)
|
|
27
|
+
const pkg = createRequire(import.meta.url)('../package.json')
|
|
28
|
+
|
|
29
|
+
const program = new Command()
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.name('chuchen-plugin')
|
|
33
|
+
.description('初尘3D壁纸插件开发工具包')
|
|
34
|
+
.version(pkg.version)
|
|
35
|
+
|
|
36
|
+
// ===== init 命令 =====
|
|
37
|
+
program
|
|
38
|
+
.command('init')
|
|
39
|
+
.argument('[project-name]', '项目名称(kebab-case)', 'my-wallpaper')
|
|
40
|
+
.option('-n, --name <name>', '插件显示名称')
|
|
41
|
+
.option('-d, --description <desc>', '插件描述')
|
|
42
|
+
.option('-a, --author <author>', '作者名称')
|
|
43
|
+
.option('-c, --category <category>', '分类(natural/tech/abstract/festival/utility)', 'abstract')
|
|
44
|
+
.option('--three', '启用 Three.js 支持(3D 壁纸)')
|
|
45
|
+
.description('初始化壁纸插件项目')
|
|
46
|
+
.action(async (projectName, options) => {
|
|
47
|
+
await cmdInit(projectName, options)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ===== build 命令 =====
|
|
51
|
+
program
|
|
52
|
+
.command('build')
|
|
53
|
+
.option('-o, --output <filename>', '输出文件名')
|
|
54
|
+
.option('--dry-run', '仅校验,不生成文件')
|
|
55
|
+
.option('--preview <path>', '预览文件路径(mp4/webm/mov/gif,自动打入 preview/ 子目录)')
|
|
56
|
+
.description('打包为 .yizirwp 文件')
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
await cmdBuild(options)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ===== validate 命令 =====
|
|
62
|
+
program
|
|
63
|
+
.command('validate')
|
|
64
|
+
.description('校验 manifest.json 格式')
|
|
65
|
+
.action(async () => {
|
|
66
|
+
await cmdValidate()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// ===== example 命令 =====
|
|
70
|
+
program
|
|
71
|
+
.command('example')
|
|
72
|
+
.argument('[project-name]', '示例项目名称', 'example-particle-dance')
|
|
73
|
+
.description('生成完整示例项目(粒子之舞)')
|
|
74
|
+
.action(async (projectName) => {
|
|
75
|
+
await cmdExample(projectName)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ===== 错误处理 =====
|
|
79
|
+
program.on('command:*', () => {
|
|
80
|
+
console.log('')
|
|
81
|
+
console.log(chalk.red('❌ 未知命令') + ': ' + chalk.yellow(program.args[0]))
|
|
82
|
+
console.log('')
|
|
83
|
+
console.log('可用命令:')
|
|
84
|
+
console.log(' ' + chalk.cyan('init') + ' 📁 初始化壁纸项目')
|
|
85
|
+
console.log(' ' + chalk.cyan('build') + ' 📦 打包为 .yizirwp 文件')
|
|
86
|
+
console.log(' ' + chalk.cyan('validate') + ' ✅ 校验 manifest.json')
|
|
87
|
+
console.log(' ' + chalk.cyan('example') + ' 🌰 生成完整示例项目')
|
|
88
|
+
console.log(' ' + chalk.cyan('help') + ' ❓ 帮助信息')
|
|
89
|
+
console.log('')
|
|
90
|
+
process.exit(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ===== 主入口 =====
|
|
94
|
+
program.parse(process.argv)
|
|
95
|
+
|
|
96
|
+
// 如果没有参数,显示帮助
|
|
97
|
+
if (!process.argv.slice(2).length) {
|
|
98
|
+
program.outputHelp()
|
|
99
|
+
}
|