dsh-plugin-wallpaper-engine-pet-link 0.0.0-stage → 1.4.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/README.md +35 -2
- package/assets/memes//345/220/223/345/223/255/344/272/206.png +0 -0
- package/assets/memes//346/232/264/346/200/222/351/262/270/345/260/276/345/245/263/344/273/206/350/264/264/347/272/270.png +0 -0
- package/assets/memes//347/264/257/347/230/253/351/262/270/351/261/274/347/232/204/344/270/244/347/242/227/351/245/255.png +0 -0
- package/assets/memes//350/223/235/345/217/221/351/262/270/345/260/276/345/245/263/344/273/206/346/213/222/347/273/235/346/215/242/345/243/201/347/272/270.png +0 -0
- package/assets/memes//357/274/237/357/274/201/346/254/272/350/264/237/344/270/200/345/217/252/350/202/245/351/261/274/357/274/201/357/274/237.png +0 -0
- package/assets/webm//344/276/247/350/276/271/346/240/217-/345/220/216/346/213/211/346/216/245/347/226/262/346/200/201/347/273/223/346/235/237-dshwe.webm +0 -0
- package/assets/webm//344/276/247/350/276/271/346/240/217-/350/223/204/345/212/233/345/207/206/345/244/207/345/276/200/345/220/216/346/213/211-dshwe.webm +0 -0
- package/assets/webm//345/220/211/347/245/245/347/211/251-/350/267/263/346/212/223/347/234/251/346/231/225/350/220/275/345/234/260/350/265/267/350/272/253-dshwe.webm +0 -0
- package/assets/webm//345/275/251/350/233/213-/344/276/247/350/276/271/346/240/217-/345/215/212/350/272/253/350/275/254/345/220/221/350/270/242/350/205/277-dshwe.webm +0 -0
- package/assets/webm//345/275/251/350/233/213-/344/276/247/350/276/271/346/240/217-/350/220/275/345/234/260-dshwe.webm +0 -0
- package/assets/webm//345/275/251/350/233/213-/344/276/247/350/276/271/346/240/217-/350/242/253/345/274/271/351/243/236-dshwe.webm +0 -0
- package/assets/webm//345/275/251/350/233/213-/345/220/211/347/245/245/347/211/251-/345/223/210/346/260/224-dshwe.webm +0 -0
- package/assets/webm//351/200/232/347/224/250-/345/217/263/344/276/247/350/236/203/350/237/271/346/255/245-dshwe.webm +0 -0
- package/assets/webm//351/200/232/347/224/250-/345/267/246/344/276/247/350/236/203/350/237/271/346/255/245-dshwe.webm +0 -0
- package/cordis.patch.yml +3 -0
- package/icon.png +0 -0
- package/lib/client.pet-link.js +641 -0
- package/lib/index.js +34 -0
- package/lib/official-provider.js +151 -0
- package/lib/prepared.js +10 -0
- package/lib/routes/official-pet.js +118 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +33 -4
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 桌宠联动子包
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本包只包含官方 pet 桥接、四条演出排播、调参面板、九段定制动画及五张配图。不包含 pet 渲染器、Electron、完整基础素材或第三方补丁安装器。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
1. 按仓库的完整源码部署步骤安装 Wallpaper 的主包和七个子包。
|
|
8
|
+
2. 自行安装并启用官方 [dsh-pet](https://github.com/PC2005-cloud/dsh-pet),将目标设为网页显示。官方需提供同页 `petApi7340.pose/progress` 和 `/dsh-pet-7340/` 下的 config、lock、anim、position、throw 接口。审计基线为官方提交 `7440b963ab7fb962cb7694e094aa0c9c7a438a7b`;同为 `0.3.7` 的旧构建不一定有这些接口。
|
|
9
|
+
3. 将 Wallpaper 吉祥物设为官方侧栏模式,按 `Ctrl+Shift+D` 打开面板,刷新并选择目标 petId,再分别试跑四条逻辑。
|
|
10
|
+
|
|
11
|
+
没有官方 pet、服务缺失、目标隐藏或联动关闭时,只停用联动,吉祥物原生侧栏操作继续可用。不会自动安装、启用或修改官方 pet。目标缺失不会自动改控另一只宠物。
|
|
12
|
+
|
|
13
|
+
## 控制边界
|
|
14
|
+
|
|
15
|
+
- 宿主按 petId 协调页面,保管官方锁凭证并顺序下发白名单指令;短租约、失锁和旧响应清理不会强占新运行。
|
|
16
|
+
- 官方同一 pet 的动作会同步到其他显示端。`surface` 不是单页定向,本包不承诺跨端隔离。
|
|
17
|
+
- 发起页面自绘气泡并读取官方 pose 跟随。动作结束释放官方唯一锁后,气泡可以继续存在;不调用官方 broadcast 或 bubble/clear,也不注入桌面透明窗。
|
|
18
|
+
- 侧栏、抓绳、踢腿弹飞、哈气四条排播使用同页 playbackId 与实际视频进度触发节点。位置和抛掷交给官方 `position/throw`;不查询宠物 DOM、不改 React 私有字段、不逐帧发 HTTP。
|
|
19
|
+
- 官方直线位移按墙钟推进,不能保证视频缓冲时的逐帧贴边同步。旧私有运动面的精度不作为当前接口已实现的能力。
|
|
20
|
+
|
|
21
|
+
## 参数与验证
|
|
22
|
+
|
|
23
|
+
面板提供目标选择、四逻辑参数、运行、停止、重置和 JSON 导出。五张气泡各有出现与隐藏的动作、开始/结束锚点及毫秒偏移;重置当前逻辑同时清除该逻辑的气泡节点。参数是当前页运行时覆盖,刷新前请导出。
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm run build
|
|
27
|
+
npm run verify:pet-link
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
回归覆盖协议协调、真实本地 HTTP、素材 Range、独立气泡生命周期、四条排播、缺席回退和卸载竞态。`npm run verify:pet-link:integration` 检查真实 Cordis 配置更新与启停;`npm run verify:pet-link:browser` 检查独立浏览器中的自有气泡、配图加载及调参面板。浏览器夹具不等于官方完整 React 宠物或桌面窗口的视觉验收;不能把接口单测当成所有动画均已真机验收。
|
|
31
|
+
|
|
32
|
+
## 从旧版本迁移
|
|
33
|
+
|
|
34
|
+
内置 pet 和修改第三方文件的安装器已退役。旧 profile 里的派生依赖、bundle 选择、数据及曾经安装的外部补丁不会自动清除。关闭宿主后先停用旧内置 bundle,再更新 Wallpaper;外部补丁应通过其原备份恢复或重装官方包,不对活动安装做故障注入。
|
|
35
|
+
|
|
36
|
+
本包保留联动定制素材,不包含官方完整资源、字体及个人数据。代码许可证不替代素材授权;素材分发与商用范围须分别确认。本轮不发布 npm。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/cordis.patch.yml
ADDED
package/icon.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-plugin-wallpaper-engine-pet-link",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
const host = require("dsh-plugin-wallpaper-engine");
|
|
8
|
+
// ── 内联模块:packages/pet-link/src/official-pet.js —— 官方 pet 同页只读实况与独立气泡客户端(构建期注入;勿手改本段)──
|
|
9
|
+
/** 官方桥接客户端;独立气泡不占用、不清理官方原生气泡。 */
|
|
10
|
+
function createOfficialPetClient(ctx, options) {
|
|
11
|
+
const doc = options.document || document;
|
|
12
|
+
const raf = options.requestAnimationFrame || requestAnimationFrame;
|
|
13
|
+
const caf = options.cancelAnimationFrame || cancelAnimationFrame;
|
|
14
|
+
const later = options.setTimeout || setTimeout;
|
|
15
|
+
const clear = options.clearTimeout || clearTimeout;
|
|
16
|
+
const viewport = options.viewport || (() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
17
|
+
const pageId = options.pageId || crypto.randomUUID();
|
|
18
|
+
let disposed = false;
|
|
19
|
+
let generation = 0;
|
|
20
|
+
let run = null;
|
|
21
|
+
let bubble = null;
|
|
22
|
+
let heartbeat = null;
|
|
23
|
+
let playbackId = null;
|
|
24
|
+
const service = () => {
|
|
25
|
+
try { return ctx.get('petApi7340') || null; } catch { return null; }
|
|
26
|
+
};
|
|
27
|
+
const rpc = async input => {
|
|
28
|
+
try { return await options.request(input); } catch { return { ok: false, reason: 'transport-error' }; }
|
|
29
|
+
};
|
|
30
|
+
const hideBubble = () => {
|
|
31
|
+
if (!bubble) return;
|
|
32
|
+
const own = bubble; bubble = null;
|
|
33
|
+
clear(own.timer); caf(own.frame); own.node.remove();
|
|
34
|
+
};
|
|
35
|
+
const finish = async () => {
|
|
36
|
+
generation++;
|
|
37
|
+
const own = run; run = null; playbackId = null;
|
|
38
|
+
clear(heartbeat); heartbeat = null;
|
|
39
|
+
return own ? rpc({ ...own.ticket, op: 'finish' }) : { ok: true };
|
|
40
|
+
};
|
|
41
|
+
const stop = () => { hideBubble(); return finish(); };
|
|
42
|
+
const armHeartbeat = own => {
|
|
43
|
+
heartbeat = later(async () => {
|
|
44
|
+
if (disposed || run !== own) return;
|
|
45
|
+
const result = await rpc({ ...own.ticket, op: 'heartbeat' });
|
|
46
|
+
if (run !== own) return;
|
|
47
|
+
if (!result?.ok || service() !== own.service) { await stop(); return; }
|
|
48
|
+
armHeartbeat(own);
|
|
49
|
+
}, 1000);
|
|
50
|
+
};
|
|
51
|
+
const command = async action => {
|
|
52
|
+
const own = run;
|
|
53
|
+
if (!own || disposed) return { ok: false, reason: 'run-lost' };
|
|
54
|
+
if (service() !== own.service) { await stop(); return { ok: false, reason: 'service-lost' }; }
|
|
55
|
+
const result = await rpc({ ...own.ticket, op: 'command', action });
|
|
56
|
+
if (run !== own) return { ok: false, reason: 'run-lost' };
|
|
57
|
+
if (!result?.ok) await stop();
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
const client = {
|
|
61
|
+
async begin(petId) {
|
|
62
|
+
hideBubble();
|
|
63
|
+
const finishing = finish();
|
|
64
|
+
const epoch = generation;
|
|
65
|
+
await finishing;
|
|
66
|
+
if (disposed) return { ok: false, reason: 'client-disposed' };
|
|
67
|
+
if (epoch !== generation) return { ok: false, reason: 'run-lost' };
|
|
68
|
+
const api = service();
|
|
69
|
+
if (!api || typeof api.pose !== 'function' || typeof api.progress !== 'function' || !api.pose(petId)) {
|
|
70
|
+
return { ok: false, reason: 'client-service-unavailable' };
|
|
71
|
+
}
|
|
72
|
+
const ticket = await rpc({ op: 'start', petId, pageId });
|
|
73
|
+
if (!ticket?.ok) return ticket;
|
|
74
|
+
if (disposed || epoch !== generation || api !== service()) {
|
|
75
|
+
await rpc({ ...ticket, op: 'finish' });
|
|
76
|
+
return { ok: false, reason: 'client-disposed' };
|
|
77
|
+
}
|
|
78
|
+
run = { ticket, service: api }; armHeartbeat(run);
|
|
79
|
+
return ticket;
|
|
80
|
+
},
|
|
81
|
+
async play(name, facing) {
|
|
82
|
+
const own = run;
|
|
83
|
+
const result = await command({ kind: 'anim', name, facing });
|
|
84
|
+
if (result?.ok && run === own) playbackId = result.playbackId;
|
|
85
|
+
return result;
|
|
86
|
+
},
|
|
87
|
+
position(x, y, mode, duration) { return command({ kind: 'position', x, y, mode, duration }); },
|
|
88
|
+
throwPet(angle, power) { return command({ kind: 'throw', angle, power }); },
|
|
89
|
+
progress() {
|
|
90
|
+
if (!run || service() !== run.service || !playbackId) return null;
|
|
91
|
+
const value = run.service.progress(run.ticket.petId);
|
|
92
|
+
return value?.playbackId === playbackId ? value : null;
|
|
93
|
+
},
|
|
94
|
+
showBubble({ text = '', image = null, duration = 5000 }) {
|
|
95
|
+
if (!run || disposed || !Number.isFinite(duration) || duration <= 0 || duration > 180000) return false;
|
|
96
|
+
if (image !== null && (!Number.isInteger(image) || image < 0 || image > 4 || !options.bubbleUrl)) return false;
|
|
97
|
+
if (typeof text !== 'string' || text.length > 2000) return false;
|
|
98
|
+
const ownRun = run;
|
|
99
|
+
const size = Number(options.sizeOf(ownRun.ticket.petId));
|
|
100
|
+
if (!Number.isFinite(size) || size <= 0 || size > 1000) return false;
|
|
101
|
+
hideBubble();
|
|
102
|
+
const node = doc.createElement('div');
|
|
103
|
+
node.setAttribute('data-we-official-pet-bubble', ownRun.ticket.runId);
|
|
104
|
+
Object.assign(node.style, { position: 'fixed', pointerEvents: 'none', zIndex: '50',
|
|
105
|
+
width: 'min(240px, calc(100vw - 16px))', boxSizing: 'border-box', overflowWrap: 'anywhere',
|
|
106
|
+
padding: '8px', borderRadius: '8px', background: 'Canvas', color: 'CanvasText',
|
|
107
|
+
border: '1px solid GrayText', transform: 'translate(-50%, -100%)' });
|
|
108
|
+
node.textContent = text;
|
|
109
|
+
if (image !== null) {
|
|
110
|
+
const picture = doc.createElement('img');
|
|
111
|
+
picture.src = options.bubbleUrl(image); picture.alt = '';
|
|
112
|
+
Object.assign(picture.style, { display: 'block', width: '180px', maxWidth: '100%', height: '180px', objectFit: 'contain' });
|
|
113
|
+
node.appendChild(picture);
|
|
114
|
+
}
|
|
115
|
+
const own = { node, timer: null, frame: null };
|
|
116
|
+
const render = () => {
|
|
117
|
+
if (bubble !== own) return;
|
|
118
|
+
const pose = service() === ownRun.service ? ownRun.service.pose(ownRun.ticket.petId) : null;
|
|
119
|
+
if (!pose || !Number.isFinite(pose.x) || !Number.isFinite(pose.y)) { stop(); return; }
|
|
120
|
+
const view = viewport();
|
|
121
|
+
const box = node.getBoundingClientRect?.() || { width: Math.min(240, view.width - 16), height: 80 };
|
|
122
|
+
if (box.height > view.height - 16) { hideBubble(); return; }
|
|
123
|
+
node.style.left = `${Math.max(box.width / 2 + 8, Math.min(view.width - box.width / 2 - 8, pose.x * view.width))}px`;
|
|
124
|
+
node.style.top = `${Math.max(box.height + 8, Math.min(view.height - 8, pose.y * view.height - size / 2 - 8))}px`;
|
|
125
|
+
own.frame = raf(render);
|
|
126
|
+
};
|
|
127
|
+
bubble = own; doc.body.appendChild(node); render();
|
|
128
|
+
if (bubble !== own) return false;
|
|
129
|
+
own.timer = later(() => { if (bubble === own) hideBubble(); }, duration);
|
|
130
|
+
return bubble === own;
|
|
131
|
+
},
|
|
132
|
+
hideBubble,
|
|
133
|
+
finish,
|
|
134
|
+
dispose() { if (!disposed) { disposed = true; stop(); } },
|
|
135
|
+
};
|
|
136
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => () => client.dispose());
|
|
137
|
+
return client;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── 内联模块:packages/pet-link/src/pet-link-tuner.js —— 页内调参面板(Ctrl+Shift+D;调试工具,非产品功能;自 packages/pet-link/src/pet-link-tuner.js 原样迁出内联通路)(构建期注入;勿手改本段)──
|
|
141
|
+
/** 官方联动调参面板;仅暴露原生接口实际支持的参数,不保留无效的逐帧宠物 DOM 旋钮。 */
|
|
142
|
+
function createOfficialPetTuner(link, options = {}) {
|
|
143
|
+
const doc = options.document || document;
|
|
144
|
+
let root = null, logic = 'sidebar', installed = false;
|
|
145
|
+
const weT = options.translate || (value => value);
|
|
146
|
+
const fields = [
|
|
147
|
+
['sidebarWalkMs', weT('走位时长(毫秒)')], ['braceMs', weT('准备时长(毫秒)')],
|
|
148
|
+
['slowOpenMs', weT('侧栏展开(毫秒)')], ['openDelayMs', weT('展开延迟(毫秒)')],
|
|
149
|
+
['bubbleMs', weT('气泡时长(毫秒)')], ['holdMaxMs', weT('等待关闭(毫秒)')], ['eggAt', weT('彩蛋触发次数')],
|
|
150
|
+
['mascotOpenAtMs', weT('抓绳触发(毫秒)')], ['mascotRopeDownPx', weT('绳子下拉(像素)')],
|
|
151
|
+
['mascotReleaseAtMs', weT('绳子释放(毫秒)')], ['egg1FlingPower', weT('原生抛掷力度')],
|
|
152
|
+
['egg1LeadMs', weT('踢腿提前量(毫秒)')], ['egg1PostLandMs', weT('落地停留(毫秒)')],
|
|
153
|
+
['egg2LeadMs', weT('哈气提前量(毫秒)')], ['egg2OpenAtPct', weT('哈气展开进度(%)')],
|
|
154
|
+
];
|
|
155
|
+
const labels = { sidebar: weT('拉动侧边栏'), mascot: weT('抓绳互动'), egg1: weT('踢开并弹飞'), egg2: weT('站立哈气') };
|
|
156
|
+
const text = value => options.translate ? options.translate(value) : value;
|
|
157
|
+
function element(tag, label) {
|
|
158
|
+
const node = doc.createElement(tag);
|
|
159
|
+
if (label) node.textContent = text(label);
|
|
160
|
+
return node;
|
|
161
|
+
}
|
|
162
|
+
function button(label, title, action) {
|
|
163
|
+
const node = element('button', label);
|
|
164
|
+
node.type = 'button'; node.title = text(title); node.setAttribute('aria-label', text(title));
|
|
165
|
+
node.style.cssText = 'min-width:32px;min-height:32px;border:1px solid GrayText;background:ButtonFace;color:ButtonText;border-radius:4px;cursor:pointer';
|
|
166
|
+
node.addEventListener('click', action);
|
|
167
|
+
return node;
|
|
168
|
+
}
|
|
169
|
+
function sync() {
|
|
170
|
+
if (!root) return;
|
|
171
|
+
const value = link.tuner.snapshot();
|
|
172
|
+
root.querySelector('[data-status]').textContent = value.busy ? value.phase : value.reason || text(weT('联动可用'));
|
|
173
|
+
const target = root.querySelector('[data-target]');
|
|
174
|
+
target.replaceChildren();
|
|
175
|
+
const pets = value.pets.filter(item => ['web', 'both'].includes(item.display));
|
|
176
|
+
if (!pets.some(item => item.id === value.target)) pets.unshift({ id: value.target, name: value.target });
|
|
177
|
+
for (const pet of pets) {
|
|
178
|
+
const option = element('option'); option.value = pet.id; option.textContent = pet.name || pet.id;
|
|
179
|
+
option.selected = pet.id === value.target; target.appendChild(option);
|
|
180
|
+
}
|
|
181
|
+
for (const input of root.querySelectorAll('[data-param]')) {
|
|
182
|
+
const key = input.getAttribute('data-param');
|
|
183
|
+
input.value = String(value.logicTuning[logic]?.[key] ?? value.defaults[key]);
|
|
184
|
+
}
|
|
185
|
+
for (const group of root.querySelectorAll('[data-bubble-group]')) {
|
|
186
|
+
const id = group.getAttribute('data-bubble-group');
|
|
187
|
+
const spec = PET_BUBBLE_SPECS.find(item => item.id === id);
|
|
188
|
+
group.hidden = spec.logic !== logic;
|
|
189
|
+
for (const row of group.querySelectorAll('[data-bubble-rule]')) {
|
|
190
|
+
const rule = value.bubbleTiming[id]?.[row.getAttribute('data-bubble-rule')];
|
|
191
|
+
row.querySelector('[data-bubble-action]').value = rule?.action || '';
|
|
192
|
+
row.querySelector('[data-bubble-edge]').value = rule?.edge || 'start';
|
|
193
|
+
row.querySelector('[data-bubble-offset]').value = String(rule?.offsetMs || 0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const tab of root.querySelectorAll('[data-logic]')) tab.setAttribute('aria-selected', String(tab.getAttribute('data-logic') === logic));
|
|
197
|
+
}
|
|
198
|
+
function open() {
|
|
199
|
+
if (!root) {
|
|
200
|
+
root = element('section'); root.setAttribute('data-pet-tuner', 'official');
|
|
201
|
+
root.setAttribute('aria-label', text(weT('桌宠联动调参'))); root.setAttribute('role', 'dialog');
|
|
202
|
+
root.style.cssText = 'position:fixed;inset:8px 8px auto auto;width:min(460px,calc(100vw - 16px));max-height:calc(100vh - 16px);overflow:auto;box-sizing:border-box;padding:12px;border:1px solid GrayText;border-radius:8px;background:Canvas;color:CanvasText;z-index:1000;font:13px/1.5 system-ui;letter-spacing:0';
|
|
203
|
+
const header = element('header'); header.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px';
|
|
204
|
+
const heading = element('strong', weT('桌宠联动调参')); heading.style.flex = '1'; header.appendChild(heading);
|
|
205
|
+
header.appendChild(button('↻', weT('刷新桌宠'), () => { link.warmup().then(sync); }));
|
|
206
|
+
header.appendChild(button('■', weT('停止联动'), () => { link.tuner.cancel(); sync(); }));
|
|
207
|
+
header.appendChild(button('×', weT('关闭'), close)); root.appendChild(header);
|
|
208
|
+
const targetLabel = element('label', weT('目标桌宠')); targetLabel.style.cssText = 'display:grid;grid-template-columns:100px minmax(0,1fr);gap:8px';
|
|
209
|
+
const target = element('select'); target.setAttribute('data-target', ''); target.style.minWidth = '0';
|
|
210
|
+
target.addEventListener('change', () => { link.tuner.setTarget(target.value); link.warmup().then(sync); });
|
|
211
|
+
targetLabel.appendChild(target); root.appendChild(targetLabel);
|
|
212
|
+
const tabs = element('div'); tabs.setAttribute('role', 'tablist'); tabs.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin:8px 0';
|
|
213
|
+
for (const key of PET_TUNER_LOGICS) {
|
|
214
|
+
const tab = button(labels[key], labels[key], () => { logic = key; sync(); });
|
|
215
|
+
tab.setAttribute('role', 'tab'); tab.setAttribute('data-logic', key); tabs.appendChild(tab);
|
|
216
|
+
}
|
|
217
|
+
root.appendChild(tabs);
|
|
218
|
+
for (const [key, label] of fields) {
|
|
219
|
+
const row = element('label', label); row.style.cssText = 'display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:8px;align-items:center;margin:5px 0;overflow-wrap:anywhere';
|
|
220
|
+
const input = element('input'); input.type = 'number'; input.min = '0'; input.step = key === 'egg1FlingPower' ? '0.1' : '1';
|
|
221
|
+
input.style.cssText = 'min-width:0;width:100%;box-sizing:border-box'; input.setAttribute('data-param', key);
|
|
222
|
+
input.addEventListener('change', () => {
|
|
223
|
+
if (!link.tuner.setForLogic(logic, key, Number(input.value))) sync();
|
|
224
|
+
}); row.appendChild(input); root.appendChild(row);
|
|
225
|
+
}
|
|
226
|
+
for (const spec of PET_BUBBLE_SPECS) {
|
|
227
|
+
const group = element('fieldset'); group.setAttribute('data-bubble-group', spec.id);
|
|
228
|
+
group.style.cssText = 'margin:10px 0 0;padding:8px;min-width:0;border:1px solid GrayText';
|
|
229
|
+
group.appendChild(element('legend', spec.text(weT)));
|
|
230
|
+
for (const [edge, label] of [['show', weT('出现')], ['hide', weT('隐藏')]]) {
|
|
231
|
+
const row = element('div'); row.setAttribute('data-bubble-rule', edge);
|
|
232
|
+
row.style.cssText = 'display:grid;grid-template-columns:32px minmax(0,1fr) 60px 72px;gap:4px;margin:4px 0;align-items:center';
|
|
233
|
+
row.appendChild(element('span', label));
|
|
234
|
+
const action = element('select'); action.setAttribute('data-bubble-action', '');
|
|
235
|
+
action.setAttribute('aria-label', edge === 'show' ? weT('出现动作') : weT('隐藏动作')); action.style.minWidth = '0';
|
|
236
|
+
for (const [key, name] of [['', weT('默认')], ...Object.keys(PET_ANIM).map(key => [key, key])]) {
|
|
237
|
+
const option = element('option', name); option.value = key; action.appendChild(option);
|
|
238
|
+
}
|
|
239
|
+
const anchor = element('select'); anchor.setAttribute('data-bubble-edge', '');
|
|
240
|
+
anchor.setAttribute('aria-label', edge === 'show' ? weT('出现时间锚点') : weT('隐藏时间锚点'));
|
|
241
|
+
for (const [key, name] of [['start', weT('开始')], ['end', weT('结束')]]) {
|
|
242
|
+
const option = element('option', name); option.value = key; anchor.appendChild(option);
|
|
243
|
+
}
|
|
244
|
+
const offset = element('input'); offset.type = 'number'; offset.min = '-180000'; offset.max = '180000'; offset.step = '1';
|
|
245
|
+
offset.style.cssText = 'width:100%;min-width:0;box-sizing:border-box';
|
|
246
|
+
offset.setAttribute('data-bubble-offset', ''); offset.setAttribute('aria-label', edge === 'show' ? weT('出现偏移(毫秒)') : weT('隐藏偏移(毫秒)'));
|
|
247
|
+
const change = () => {
|
|
248
|
+
const config = { ...link.tuner.snapshot().bubbleTiming[spec.id] };
|
|
249
|
+
if (action.value) config[edge] = { action: action.value, edge: anchor.value, offsetMs: Number(offset.value) };
|
|
250
|
+
else delete config[edge];
|
|
251
|
+
if (!link.tuner.setBubbleTiming(spec.id, config)) sync();
|
|
252
|
+
};
|
|
253
|
+
for (const input of [action, anchor, offset]) { input.addEventListener('change', change); row.appendChild(input); }
|
|
254
|
+
group.appendChild(row);
|
|
255
|
+
}
|
|
256
|
+
root.appendChild(group);
|
|
257
|
+
}
|
|
258
|
+
const actions = element('div'); actions.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-top:10px';
|
|
259
|
+
actions.appendChild(button('▶', weT('运行当前逻辑'), () => { link.tuner.run(logic).then(sync); sync(); }));
|
|
260
|
+
actions.appendChild(button('↶', weT('重置当前逻辑'), () => { link.tuner.reset(logic); sync(); }));
|
|
261
|
+
actions.appendChild(button('↓', weT('导出 JSON'), () => {
|
|
262
|
+
const out = root.querySelector('textarea'); out.value = JSON.stringify(link.tuner.snapshot(), null, 2); out.hidden = false; out.focus(); out.select();
|
|
263
|
+
})); root.appendChild(actions);
|
|
264
|
+
const status = element('output'); status.setAttribute('data-status', ''); status.setAttribute('aria-live', 'polite'); root.appendChild(status);
|
|
265
|
+
const out = element('textarea'); out.hidden = true; out.readOnly = true; out.rows = 6;
|
|
266
|
+
out.setAttribute('aria-label', text(weT('当前调参 JSON'))); out.style.cssText = 'width:100%;box-sizing:border-box;margin-top:8px'; root.appendChild(out);
|
|
267
|
+
doc.body.appendChild(root);
|
|
268
|
+
}
|
|
269
|
+
root.hidden = false; sync(); link.warmup().then(sync); return true;
|
|
270
|
+
}
|
|
271
|
+
function close() { if (root) root.hidden = true; return false; }
|
|
272
|
+
const keydown = event => {
|
|
273
|
+
if (!event.ctrlKey || !event.shiftKey || String(event.key).toLowerCase() !== 'd') return;
|
|
274
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
275
|
+
if (root && !root.hidden) close(); else open();
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
open, close, toggle() { return root && !root.hidden ? close() : open(); }, isOpen() { return Boolean(root && !root.hidden); },
|
|
279
|
+
install() { if (!installed) { doc.addEventListener('keydown', keydown, true); installed = true; } },
|
|
280
|
+
dispose() { doc.removeEventListener('keydown', keydown, true); installed = false; root?.remove(); root = null; },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── 内联模块:packages/pet-link/src/pet-link.js —— 桌宠联动的排播 / 吉祥物触摸面 / 能力探测(自 packages/pet-link/src/pet-link.js 原样迁出内联通路)(构建期注入;勿手改本段)──
|
|
285
|
+
/** 官方原生联动。只读同页服务,所有宠物写操作经宿主协调器,不查询或改写宠物 DOM。 */
|
|
286
|
+
const PET_BASE = '/dsh-pet-7340';
|
|
287
|
+
const PET_ANIM = {
|
|
288
|
+
walkRight: '通用-右侧螃蟹步-dshwe', walkLeft: '通用-左侧螃蟹步-dshwe',
|
|
289
|
+
brace: '侧边栏-蓄力准备往后拉-dshwe', pull: '侧边栏-后拉接疲态结束-dshwe',
|
|
290
|
+
mascotTumble: '吉祥物-跳抓眩晕落地起身-dshwe', eggKick: '彩蛋-侧边栏-半身转向踢腿-dshwe',
|
|
291
|
+
eggFlung: '彩蛋-侧边栏-被弹飞-dshwe', eggLand: '彩蛋-侧边栏-落地-dshwe', eggHiss: '彩蛋-吉祥物-哈气-dshwe',
|
|
292
|
+
};
|
|
293
|
+
const PET_HOLD = { walkRight: 5167, walkLeft: 5167, brace: 5167, pull: 10125,
|
|
294
|
+
mascotTumble: 8000, eggKick: 5167, eggFlung: 10125, eggLand: 5167, eggHiss: 5167 };
|
|
295
|
+
const PET_TUNE_DEFAULTS = {
|
|
296
|
+
sidebarWalkMs: 5167, braceMs: 1000, slowOpenMs: 8125, openDelayMs: 500,
|
|
297
|
+
bubbleMs: 3000, holdMaxMs: 180000, eggAt: 3, mascotOpenAtMs: 2500,
|
|
298
|
+
mascotRopeDownPx: 40, mascotReleaseAtMs: 2920, egg1FlingPower: 1,
|
|
299
|
+
egg1LeadMs: 600, egg1PostLandMs: 600, egg2LeadMs: 600, egg2OpenAtPct: 35,
|
|
300
|
+
};
|
|
301
|
+
const PET_TUNER_LOGICS = ['sidebar', 'mascot', 'egg1', 'egg2'];
|
|
302
|
+
const PET_BUBBLE_SPECS = [
|
|
303
|
+
{ id: 'sidebar', logic: 'sidebar', image: 0, text(weT = value => value) { return weT('累死本鲸了,给我添两碗饭'); } },
|
|
304
|
+
{ id: 'mascot', logic: 'mascot', image: 1, text(weT = value => value) { return weT('可以换壁纸了,不要用××√哦'); } },
|
|
305
|
+
{ id: 'egg1Angry', logic: 'egg1', image: 2, text(weT = value => value) { return weT('偶鲸鲸!!!'); } },
|
|
306
|
+
{ id: 'egg1Land', logic: 'egg1', image: 3, text(weT = value => value) { return weT('哇啊啊啊啊啊!!!'); } },
|
|
307
|
+
{ id: 'egg2', logic: 'egg2', image: 4, text(weT = value => value) { return weT('?!欺负一只肥鱼!?'); } },
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
function createOfficialPetPerformer(options) {
|
|
311
|
+
const now = options.now || Date.now;
|
|
312
|
+
const later = options.setTimeout || setTimeout;
|
|
313
|
+
const clear = options.clearTimeout || clearTimeout;
|
|
314
|
+
const wait = options.wait || (ms => new Promise(resolve => later(resolve, ms)));
|
|
315
|
+
const view = options.viewport || (() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
316
|
+
const doc = options.document || (typeof document === 'undefined' ? null : document);
|
|
317
|
+
const storage = options.storage === undefined ? (typeof localStorage === 'undefined' ? null : localStorage) : options.storage;
|
|
318
|
+
const storageKey = 'dsh-we-official-pet-target';
|
|
319
|
+
let target = 'main';
|
|
320
|
+
try { target = storage?.getItem(storageKey) || target; } catch { /* 私密模式仍可用。 */ }
|
|
321
|
+
const state = { available: false, reason: 'official-unavailable', pets: [], epoch: 0, busy: false,
|
|
322
|
+
phase: 'idle', logic: '', farCount: 0, nearCount: 0, tuning: {}, bubbleTiming: {}, timer: null,
|
|
323
|
+
client: null, sidebar: null, disposed: false, home: null, activeLogic: 'sidebar' };
|
|
324
|
+
const tune = (key, logic = state.activeLogic) => state.tuning[logic]?.[key] ?? PET_TUNE_DEFAULTS[key];
|
|
325
|
+
const service = () => { try { return options.getContext()?.get('petApi7340') || null; } catch { return null; } };
|
|
326
|
+
const pet = () => state.pets.find(item => item.id === target);
|
|
327
|
+
const pose = () => { try { return service()?.pose(target) || null; } catch { return null; } };
|
|
328
|
+
function ensureClient() {
|
|
329
|
+
if (!state.client) state.client = createOfficialPetClient({ get: () => service() }, {
|
|
330
|
+
...options, document: doc, viewport: view, sizeOf: () => pet()?.size,
|
|
331
|
+
});
|
|
332
|
+
return state.client;
|
|
333
|
+
}
|
|
334
|
+
async function probe() {
|
|
335
|
+
const epoch = state.epoch;
|
|
336
|
+
try {
|
|
337
|
+
const result = await options.fetchTargets();
|
|
338
|
+
if (epoch !== state.epoch || state.disposed) return false;
|
|
339
|
+
state.pets = result?.ok && Array.isArray(result.pets) ? result.pets : [];
|
|
340
|
+
const api = service(), chosen = pet();
|
|
341
|
+
state.reason = !result?.ok ? result?.reason || 'official-unavailable'
|
|
342
|
+
: !chosen ? 'unknown-pet'
|
|
343
|
+
: !['web', 'both'].includes(chosen.display) ? 'pet-not-displayed'
|
|
344
|
+
: !api || typeof api.pose !== 'function' || typeof api.progress !== 'function' || !pose()
|
|
345
|
+
? 'client-service-unavailable' : '';
|
|
346
|
+
state.available = !state.reason;
|
|
347
|
+
} catch { state.available = false; state.reason = 'official-unavailable'; }
|
|
348
|
+
return state.available;
|
|
349
|
+
}
|
|
350
|
+
const clamp = value => Math.max(0, Math.min(1, value));
|
|
351
|
+
function point() {
|
|
352
|
+
const value = pose();
|
|
353
|
+
if (!value || ![value.x, value.y].every(Number.isFinite)) throw new Error('pose-unavailable');
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function sidebarCall(method, ...args) {
|
|
357
|
+
const fn = state.sidebar?.[method];
|
|
358
|
+
return typeof fn === 'function' ? fn(...args) : undefined;
|
|
359
|
+
}
|
|
360
|
+
function cancel() {
|
|
361
|
+
state.epoch++; state.busy = false;
|
|
362
|
+
const oldClient = state.client;
|
|
363
|
+
state.client = null;
|
|
364
|
+
oldClient?.dispose();
|
|
365
|
+
sidebarCall('commit');
|
|
366
|
+
options.restoreRope?.();
|
|
367
|
+
}
|
|
368
|
+
function fallback() {
|
|
369
|
+
let handled = false;
|
|
370
|
+
try { handled = options.fallback() === true; } catch { /* 不影响壁纸主界面。 */ }
|
|
371
|
+
return { logic: 'disabled', phase: 'done', busy: false, fallback: handled, reason: state.reason };
|
|
372
|
+
}
|
|
373
|
+
async function run(logic, gesture = false) {
|
|
374
|
+
if (state.busy) return { logic: 'busy', busy: true };
|
|
375
|
+
if (!PET_TUNER_LOGICS.includes(logic) && !gesture) return { logic: 'invalid', busy: false };
|
|
376
|
+
state.busy = true;
|
|
377
|
+
const epoch = state.epoch;
|
|
378
|
+
const alive = () => epoch === state.epoch && !state.disposed && Boolean(pose());
|
|
379
|
+
const check = () => { if (!alive()) throw new Error('run-lost'); };
|
|
380
|
+
const assertResult = result => { if (!result?.ok) throw new Error(result?.reason || 'official-unavailable'); };
|
|
381
|
+
let client, released = false, openedSidebar = false;
|
|
382
|
+
const openSidebar = () => { openedSidebar = true; sidebarCall('open'); };
|
|
383
|
+
try {
|
|
384
|
+
if (!await probe() || !alive()) return gesture ? { logic: 'disabled', busy: false } : fallback();
|
|
385
|
+
client = ensureClient();
|
|
386
|
+
assertResult(await client.begin(target)); check();
|
|
387
|
+
state.home = point(); state.logic = logic; state.activeLogic = logic;
|
|
388
|
+
const hooks = new Set();
|
|
389
|
+
const shown = new Map();
|
|
390
|
+
const hide = id => { if (shown.has(id)) { client.hideBubble(); shown.clear(); } };
|
|
391
|
+
const show = id => {
|
|
392
|
+
const spec = PET_BUBBLE_SPECS.find(item => item.id === id);
|
|
393
|
+
if (!spec) return;
|
|
394
|
+
shown.clear(); shown.set(id, now());
|
|
395
|
+
client.showBubble({ text: spec.text(options.translate), image: spec.image, duration: Math.max(1, tune('bubbleMs')) });
|
|
396
|
+
};
|
|
397
|
+
const node = (action, seconds, duration) => {
|
|
398
|
+
for (const spec of PET_BUBBLE_SPECS.filter(item => item.logic === logic)) {
|
|
399
|
+
const config = state.bubbleTiming[spec.id];
|
|
400
|
+
for (const edge of ['show', 'hide']) {
|
|
401
|
+
const rule = config?.[edge];
|
|
402
|
+
if (!rule || rule.action !== action || hooks.has(spec.id + edge)) continue;
|
|
403
|
+
const at = (rule.edge === 'end' ? duration * 1000 : 0) + rule.offsetMs;
|
|
404
|
+
if (seconds * 1000 < at) continue;
|
|
405
|
+
hooks.add(spec.id + edge);
|
|
406
|
+
if (edge === 'show') show(spec.id); else hide(spec.id);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const defaultShow = id => { if (!state.bubbleTiming[id]?.show) show(id); };
|
|
411
|
+
const defaultHide = id => { if (!state.bubbleTiming[id]?.hide) hide(id); };
|
|
412
|
+
const clip = async (key, tick, limitMs = null) => {
|
|
413
|
+
check(); state.phase = key;
|
|
414
|
+
const current = point();
|
|
415
|
+
assertResult(await client.play(PET_ANIM[key], current.facing === 'right' ? 'right' : 'left')); check();
|
|
416
|
+
const deadline = now() + Math.max(30000, PET_HOLD[key] + 20000);
|
|
417
|
+
let started = false;
|
|
418
|
+
while (alive() && now() < deadline) {
|
|
419
|
+
const p = client.progress();
|
|
420
|
+
if (p && Number.isFinite(p.currentTime) && Number.isFinite(p.duration) && p.duration > 0) {
|
|
421
|
+
started = true;
|
|
422
|
+
node(key, p.currentTime, p.duration);
|
|
423
|
+
if (tick) await tick(p.currentTime * 1000, p.duration * 1000);
|
|
424
|
+
if (p.currentTime >= p.duration - 0.045 || (limitMs !== null && p.currentTime * 1000 >= limitMs)) return;
|
|
425
|
+
} else if (started) throw new Error('playback-replaced');
|
|
426
|
+
await wait(32);
|
|
427
|
+
}
|
|
428
|
+
throw new Error('playback-timeout');
|
|
429
|
+
};
|
|
430
|
+
const move = async (destination, duration = 600) => {
|
|
431
|
+
check();
|
|
432
|
+
assertResult(await client.position(clamp(destination.x), clamp(destination.y), 'line', duration));
|
|
433
|
+
check();
|
|
434
|
+
};
|
|
435
|
+
const walk = async destination => {
|
|
436
|
+
const current = point();
|
|
437
|
+
const key = destination.x < current.x ? 'walkLeft' : 'walkRight';
|
|
438
|
+
let moving = false;
|
|
439
|
+
await clip(key, async () => { if (!moving) { moving = true; await move(destination, tune('sidebarWalkMs')); } });
|
|
440
|
+
};
|
|
441
|
+
const edgePoint = () => {
|
|
442
|
+
const width = view().width;
|
|
443
|
+
const edge = sidebarCall('edgeX');
|
|
444
|
+
return { x: clamp(((Number.isFinite(edge) ? edge : width) - Number(pet()?.size || 180) * 0.15) / width), y: point().y };
|
|
445
|
+
};
|
|
446
|
+
if (gesture) {
|
|
447
|
+
await clip(logic === 'grab' ? 'brace' : 'pull', null, logic === 'grab' ? tune('braceMs') : null);
|
|
448
|
+
} else if (logic === 'sidebar') {
|
|
449
|
+
await walk(edgePoint());
|
|
450
|
+
await clip('brace', null, tune('braceMs'));
|
|
451
|
+
let opened = false;
|
|
452
|
+
await clip('pull', async ms => {
|
|
453
|
+
if (!opened && ms >= tune('openDelayMs')) {
|
|
454
|
+
opened = true; sidebarCall('beginPull', tune('slowOpenMs')); openSidebar();
|
|
455
|
+
await move(edgePoint(), tune('slowOpenMs'));
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
if (!opened) openSidebar();
|
|
459
|
+
sidebarCall('commit'); defaultShow('sidebar');
|
|
460
|
+
} else if (logic === 'mascot') {
|
|
461
|
+
const rope = options.ropePoint?.() || { x: 0.25, y: point().y };
|
|
462
|
+
await walk({ x: clamp(rope.x), y: point().y });
|
|
463
|
+
let opened = false;
|
|
464
|
+
await clip('mascotTumble', ms => {
|
|
465
|
+
if (!opened && ms >= tune('mascotOpenAtMs')) { opened = true; openSidebar(); }
|
|
466
|
+
const pull = ms >= tune('mascotOpenAtMs') && ms < tune('mascotReleaseAtMs');
|
|
467
|
+
options.moveRope?.(pull ? tune('mascotRopeDownPx') : 0);
|
|
468
|
+
});
|
|
469
|
+
if (!opened) openSidebar();
|
|
470
|
+
options.restoreRope?.(); defaultShow('mascot');
|
|
471
|
+
} else if (logic === 'egg1') {
|
|
472
|
+
await walk(edgePoint()); defaultShow('egg1Angry');
|
|
473
|
+
await wait(tune('egg1LeadMs')); check(); await clip('eggKick'); defaultHide('egg1Angry');
|
|
474
|
+
let thrown = false;
|
|
475
|
+
await clip('eggFlung', async () => {
|
|
476
|
+
if (!thrown) { thrown = true; assertResult(await client.throwPet(155, tune('egg1FlingPower'))); sidebarCall('rebound', true, 240); }
|
|
477
|
+
});
|
|
478
|
+
defaultShow('egg1Land'); await clip('eggLand'); defaultHide('egg1Land');
|
|
479
|
+
openSidebar(); await wait(tune('egg1PostLandMs')); check();
|
|
480
|
+
} else {
|
|
481
|
+
defaultShow('egg2'); await wait(tune('egg2LeadMs')); check();
|
|
482
|
+
let opened = false;
|
|
483
|
+
await clip('eggHiss', (ms, duration) => {
|
|
484
|
+
if (!opened && ms >= duration * tune('egg2OpenAtPct') / 100) { opened = true; openSidebar(); }
|
|
485
|
+
});
|
|
486
|
+
if (!opened) openSidebar(); defaultHide('egg2');
|
|
487
|
+
}
|
|
488
|
+
check();
|
|
489
|
+
assertResult(await client.finish()); released = true;
|
|
490
|
+
if (!gesture && ['sidebar', 'mascot'].includes(logic)) {
|
|
491
|
+
state.phase = 'waiting';
|
|
492
|
+
const deadline = now() + tune('holdMaxMs');
|
|
493
|
+
while (alive() && sidebarCall('isOpen') === true && now() < deadline) await wait(100);
|
|
494
|
+
}
|
|
495
|
+
if (!gesture && alive() && logic !== 'egg2') {
|
|
496
|
+
// 等待期间已归还官方唯一锁;回程重新申请,不强占用户或其他插件的新锁。
|
|
497
|
+
assertResult(await client.begin(target)); released = false;
|
|
498
|
+
await walk(state.home);
|
|
499
|
+
assertResult(await client.finish()); released = true;
|
|
500
|
+
}
|
|
501
|
+
state.phase = 'done';
|
|
502
|
+
return { logic, phase: 'done', busy: false };
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (epoch === state.epoch) {
|
|
505
|
+
state.reason = error.message || 'official-unavailable'; state.phase = 'cancelled';
|
|
506
|
+
if (!gesture && !released && !openedSidebar) { try { options.fallback(); } catch { /* 保留主交互。 */ } }
|
|
507
|
+
}
|
|
508
|
+
return { logic, phase: 'cancelled', busy: false, reason: error.message || 'official-unavailable' };
|
|
509
|
+
} finally {
|
|
510
|
+
if (client && !released) { client.hideBubble(); await client.finish(); }
|
|
511
|
+
if (epoch === state.epoch) { state.busy = false; sidebarCall('commit'); options.restoreRope?.(); }
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function warmup() {
|
|
515
|
+
state.disposed = false;
|
|
516
|
+
clear(state.timer); state.timer = null;
|
|
517
|
+
const result = await probe();
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
const snapshot = () => ({ available: state.available, reason: state.reason, busy: state.busy,
|
|
521
|
+
phase: state.phase, logic: state.logic, target, pets: state.pets.map(item => ({ ...item })),
|
|
522
|
+
farCount: state.farCount, nearCount: state.nearCount, home: state.home });
|
|
523
|
+
const result = {
|
|
524
|
+
names: PET_ANIM, warmup,
|
|
525
|
+
dispose() { state.disposed = true; clear(state.timer); state.timer = null; cancel(); state.client?.dispose(); state.client = null; },
|
|
526
|
+
mascot: {
|
|
527
|
+
async tap() {
|
|
528
|
+
if (state.busy) return { logic: 'busy', busy: true };
|
|
529
|
+
if (!await probe()) return fallback();
|
|
530
|
+
const p = point(), rope = options.ropePoint?.() || { x: 0.25, y: p.y };
|
|
531
|
+
let logic = Math.abs(p.x - rope.x) < Math.abs(p.x - 1) ? 'mascot' : 'sidebar';
|
|
532
|
+
const key = logic === 'sidebar' ? 'farCount' : 'nearCount';
|
|
533
|
+
if (++state[key] >= tune('eggAt', logic)) { state[key] = 0; logic = logic === 'sidebar' ? 'egg1' : 'egg2'; }
|
|
534
|
+
return run(logic);
|
|
535
|
+
}, snapshot,
|
|
536
|
+
registerSidebar(api) { if (!api && state.busy) cancel(); state.sidebar = api; },
|
|
537
|
+
registerGesture(fn) { state.cancelGesture = fn; return () => { if (state.cancelGesture === fn) state.cancelGesture = null; }; },
|
|
538
|
+
reset() { state.farCount = 0; state.nearCount = 0; },
|
|
539
|
+
},
|
|
540
|
+
performer: {
|
|
541
|
+
onGrab() { return run('grab', true).then(value => value.phase === 'done'); },
|
|
542
|
+
onPull(progress) { return Number.isFinite(progress) && progress >= 0.12 ? run('pull', true).then(value => value.phase === 'done') : Promise.resolve(false); },
|
|
543
|
+
onRelease() { return Promise.resolve(false); }, snapshot,
|
|
544
|
+
},
|
|
545
|
+
tuner: {
|
|
546
|
+
run: logic => run(logic), snapshot: () => ({ ...snapshot(), defaults: { ...PET_TUNE_DEFAULTS }, logicTuning: structuredClone(state.tuning), bubbleTiming: structuredClone(state.bubbleTiming) }),
|
|
547
|
+
cancel,
|
|
548
|
+
setTarget(id) {
|
|
549
|
+
if (typeof id !== 'string' || !id.trim() || id !== id.trim() || id.length > 128 || /[\u0000-\u001f]/.test(id)) return false;
|
|
550
|
+
cancel(); target = id; state.available = false; state.reason = 'target-changed';
|
|
551
|
+
try { storage?.setItem(storageKey, id); } catch { /* 当前页选择仍生效。 */ }
|
|
552
|
+
return true;
|
|
553
|
+
},
|
|
554
|
+
setForLogic(logic, key, value) {
|
|
555
|
+
if (!PET_TUNER_LOGICS.includes(logic) || !(key in PET_TUNE_DEFAULTS) || !Number.isFinite(value) || value < 0 || value > 180000) return false;
|
|
556
|
+
if (key === 'egg1FlingPower' && (value <= 0 || value > 10)) return false;
|
|
557
|
+
if (key === 'egg2OpenAtPct' && value > 100) return false;
|
|
558
|
+
if (key === 'eggAt' && (!Number.isInteger(value) || value < 1)) return false;
|
|
559
|
+
state.tuning[logic] ??= {}; state.tuning[logic][key] = value; return true;
|
|
560
|
+
},
|
|
561
|
+
setBubbleTiming(id, config) {
|
|
562
|
+
if (!PET_BUBBLE_SPECS.some(item => item.id === id) || !config || typeof config !== 'object') return false;
|
|
563
|
+
for (const edge of ['show', 'hide']) {
|
|
564
|
+
const rule = config[edge];
|
|
565
|
+
if (rule && (!(rule.action in PET_ANIM) || !['start', 'end'].includes(rule.edge) || !Number.isFinite(rule.offsetMs) || Math.abs(rule.offsetMs) > 180000)) return false;
|
|
566
|
+
}
|
|
567
|
+
state.bubbleTiming[id] = structuredClone(config); return true;
|
|
568
|
+
},
|
|
569
|
+
reset(logic) {
|
|
570
|
+
if (logic) {
|
|
571
|
+
delete state.tuning[logic];
|
|
572
|
+
for (const spec of PET_BUBBLE_SPECS.filter(item => item.logic === logic)) delete state.bubbleTiming[spec.id];
|
|
573
|
+
} else { state.tuning = {}; state.bubbleTiming = {}; }
|
|
574
|
+
return result.tuner.snapshot();
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── 客户端正文(packages/pet-link/src/client.js)───────────────────────────────
|
|
582
|
+
/** 官方 pet 联动客户端入口;主包只消费登记门面,运行期通过本页 Cordis 服务读取实况。 */
|
|
583
|
+
const PET_LINK_HOST_REQUIRED = [
|
|
584
|
+
'weT', 'apiUrl', 'apiFetch', 'wallSidebarOpen', 'wallSidebarClose', 'wallSidebarToggle', 'registerPetLink',
|
|
585
|
+
];
|
|
586
|
+
for (const key of PET_LINK_HOST_REQUIRED) {
|
|
587
|
+
if (!(key in host)) throw new Error('[wallpaper-engine-pet-link] 主 bundle 键集缺少 "' + key + '"');
|
|
588
|
+
}
|
|
589
|
+
const { weT, apiUrl, apiFetch, wallSidebarOpen, wallSidebarClose, wallSidebarToggle, registerPetLink } = host;
|
|
590
|
+
let petLinkContext = null;
|
|
591
|
+
const petLink = createOfficialPetPerformer({
|
|
592
|
+
translate: weT,
|
|
593
|
+
getContext: () => petLinkContext,
|
|
594
|
+
async fetchTargets() {
|
|
595
|
+
const response = await apiFetch(apiUrl('/pet-link/official'));
|
|
596
|
+
return response.data || { ok: false, reason: response.error || 'transport-error' };
|
|
597
|
+
},
|
|
598
|
+
async request(payload) {
|
|
599
|
+
const response = await apiFetch(apiUrl('/pet-link/official'), {
|
|
600
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
|
|
601
|
+
});
|
|
602
|
+
return response.data || { ok: false, reason: response.error || 'transport-error' };
|
|
603
|
+
},
|
|
604
|
+
bubbleUrl: index => apiUrl('/pet-link/official/assets/m' + index + '.png'),
|
|
605
|
+
ropePoint() {
|
|
606
|
+
const rope = document.querySelector('.we-rope');
|
|
607
|
+
const box = rope?.getBoundingClientRect();
|
|
608
|
+
return box ? { x: (box.left + box.width / 2) / window.innerWidth, y: box.bottom / window.innerHeight } : null;
|
|
609
|
+
},
|
|
610
|
+
moveRope(px) {
|
|
611
|
+
const rope = document.querySelector('.we-rope');
|
|
612
|
+
if (rope) rope.style.setProperty('--we-rope-pull', px + 'px');
|
|
613
|
+
},
|
|
614
|
+
restoreRope() { document.querySelector('.we-rope')?.style.removeProperty('--we-rope-pull'); },
|
|
615
|
+
fallback() {
|
|
616
|
+
if (typeof wallSidebarToggle === 'function') return wallSidebarToggle('mascot') === true;
|
|
617
|
+
if (typeof wallSidebarOpen === 'function') return wallSidebarOpen('mascot') === true;
|
|
618
|
+
return false;
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
const petTuner = createOfficialPetTuner(petLink, { translate: weT });
|
|
622
|
+
Object.assign(petLink.tuner, petTuner);
|
|
623
|
+
const petLinkWarmup = petLink.warmup;
|
|
624
|
+
petLink.warmup = () => { petTuner.install(); return petLinkWarmup(); };
|
|
625
|
+
const petLinkDispose = petLink.dispose;
|
|
626
|
+
petLink.dispose = () => { petTuner.dispose(); petLinkDispose(); };
|
|
627
|
+
registerPetLink(petLink);
|
|
628
|
+
|
|
629
|
+
exports.inject = [];
|
|
630
|
+
exports.apply = ctx => {
|
|
631
|
+
petLinkContext = ctx;
|
|
632
|
+
petLink.warmup();
|
|
633
|
+
ctx.effect(() => () => {
|
|
634
|
+
if (petLinkContext === ctx) { petLink.dispose(); petLinkContext = null; }
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
if (typeof exports.apply !== "function") exports.apply = () => undefined;
|
|
638
|
+
if (!Array.isArray(exports.inject)) exports.inject = [];
|
|
639
|
+
return module.exports;
|
|
640
|
+
},
|
|
641
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { registerOfficialPetRoutes } from './routes/official-pet.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'dsh-plugin-wallpaper-engine-pet-link';
|
|
4
|
+
export const inject = ['webServer', 'wallpaperEngineHost'];
|
|
5
|
+
export const routeConfigs = Object.freeze([Object.freeze({})]);
|
|
6
|
+
|
|
7
|
+
/** 官方 pet 是用户独立安装的可选插件;装载时不写它的配置或资源目录。 */
|
|
8
|
+
export function apply(ctx, config) {
|
|
9
|
+
const host = ctx.wallpaperEngineHost;
|
|
10
|
+
if (!host) throw new Error('[wallpaper-engine] pet-link 缺少 wallpaperEngineHost 服务');
|
|
11
|
+
const keys = host.keys('pet-link', ctx);
|
|
12
|
+
const deps = Object.freeze({ ...keys, http: host.http });
|
|
13
|
+
return keys.handOffTeardown(ctx, keys.runComponent('pet-link', ctx, config || {}, deps, applyImpl));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function applyImpl(ctx, config = {}, c) {
|
|
17
|
+
const { webServer } = ctx;
|
|
18
|
+
const { base, http } = c;
|
|
19
|
+
const disposers = [];
|
|
20
|
+
let active = true;
|
|
21
|
+
const dispose = () => {
|
|
22
|
+
if (!active) return;
|
|
23
|
+
active = false;
|
|
24
|
+
for (const off of disposers.splice(0).reverse()) {
|
|
25
|
+
try { off(); } catch { /* 继续归还其余注册。 */ }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
registerOfficialPetRoutes(webServer, { disposers, base, http }, {
|
|
30
|
+
isLinkEnabled: () => config.linkEnabled !== false,
|
|
31
|
+
});
|
|
32
|
+
} catch (error) { dispose(); throw error; }
|
|
33
|
+
return dispose;
|
|
34
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const validId = value => typeof value === 'string' && value.trim() === value
|
|
4
|
+
&& value.length > 0 && value.length <= 128 && !/[\u0000-\u001f]/.test(value);
|
|
5
|
+
const failure = reason => ({ ok: false, reason });
|
|
6
|
+
|
|
7
|
+
/** 宿主独占指令流;官方锁凭证不交给页面,旧轮只能释放自己的凭证。 */
|
|
8
|
+
export function createOfficialPetCoordinator({ request, assetUrl, now = Date.now, leaseMs = 5000 }) {
|
|
9
|
+
const generation = randomUUID();
|
|
10
|
+
const runs = new Map();
|
|
11
|
+
let active = true;
|
|
12
|
+
async function call(path, options) {
|
|
13
|
+
try { return await request(path, options); }
|
|
14
|
+
catch { return failure('official-unavailable'); }
|
|
15
|
+
}
|
|
16
|
+
const current = run => active && runs.get(run.petId) === run && now() < run.expiresAt;
|
|
17
|
+
const ticket = run => ({ ok: true, generation, petId: run.petId, pageId: run.pageId,
|
|
18
|
+
runId: run.runId, expiresAt: run.expiresAt });
|
|
19
|
+
function find(input) {
|
|
20
|
+
const run = runs.get(input?.petId);
|
|
21
|
+
return run && input.generation === generation && input.runId === run.runId
|
|
22
|
+
&& input.pageId === run.pageId && current(run) ? run : null;
|
|
23
|
+
}
|
|
24
|
+
async function close(run) {
|
|
25
|
+
if (runs.get(run.petId) === run) runs.delete(run.petId);
|
|
26
|
+
await run.queue;
|
|
27
|
+
if (run.lockId) return call('lock/release', { query: { pet: run.petId }, body: { lockId: run.lockId } });
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
|
30
|
+
function serial(run, fn) {
|
|
31
|
+
const next = run.queue.then(async () => {
|
|
32
|
+
if (!current(run)) return failure('run-lost');
|
|
33
|
+
return fn();
|
|
34
|
+
});
|
|
35
|
+
run.queue = next.then(() => undefined, () => undefined);
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
async function targets() {
|
|
39
|
+
if (!active) return failure('bridge-disposed');
|
|
40
|
+
const config = await call('config', { method: 'GET' });
|
|
41
|
+
if (!config || typeof config !== 'object' || config.ok === false) return failure('official-unavailable');
|
|
42
|
+
const pets = [];
|
|
43
|
+
const ids = new Set();
|
|
44
|
+
for (const group of Object.values(config)) {
|
|
45
|
+
if (!Array.isArray(group?.pets)) continue;
|
|
46
|
+
for (const pet of group.pets) {
|
|
47
|
+
if (!validId(pet?.id) || ids.has(pet.id)) return failure('invalid-pet-config');
|
|
48
|
+
ids.add(pet.id);
|
|
49
|
+
pets.push({ id: pet.id, name: typeof pet.name === 'string' ? pet.name : pet.id,
|
|
50
|
+
display: pet.display, size: pet.size });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, pets };
|
|
54
|
+
}
|
|
55
|
+
async function sweep() {
|
|
56
|
+
await Promise.all([...runs.values()].filter(run => !current(run)).map(close));
|
|
57
|
+
}
|
|
58
|
+
async function start(input) {
|
|
59
|
+
if (!active) return failure('bridge-disposed');
|
|
60
|
+
if (!validId(input?.petId) || !validId(input?.pageId)) return failure('bad-request');
|
|
61
|
+
await sweep();
|
|
62
|
+
const listed = await targets();
|
|
63
|
+
if (!listed.ok) return listed;
|
|
64
|
+
const pet = listed.pets.find(item => item.id === input.petId);
|
|
65
|
+
if (!pet) return failure('unknown-pet');
|
|
66
|
+
if (pet.display !== 'web' && pet.display !== 'both') return failure('pet-not-displayed');
|
|
67
|
+
// 查询配置后再次检查,两个并发 start 不能同时越过这道门。
|
|
68
|
+
if (!active) return failure('bridge-disposed');
|
|
69
|
+
if (runs.has(input.petId)) return failure('busy');
|
|
70
|
+
if (runs.size >= 32) return failure('bridge-busy');
|
|
71
|
+
const run = { petId: input.petId, pageId: input.pageId, runId: randomUUID(),
|
|
72
|
+
expiresAt: now() + leaseMs, lockId: '', queue: Promise.resolve() };
|
|
73
|
+
runs.set(run.petId, run);
|
|
74
|
+
const pending = call('lock', { query: { pet: run.petId, surface: 'web' }, body: { ttl: leaseMs } });
|
|
75
|
+
run.queue = pending.then(() => undefined);
|
|
76
|
+
const result = await pending;
|
|
77
|
+
if (result?.ok === true && validId(result.lockId)) run.lockId = result.lockId;
|
|
78
|
+
if (!current(run) || !run.lockId || result.petId !== run.petId
|
|
79
|
+
|| !Number.isFinite(result.expiresAt) || result.expiresAt <= now()) {
|
|
80
|
+
await close(run);
|
|
81
|
+
return failure(result?.ok === false ? result.reason : 'run-lost');
|
|
82
|
+
}
|
|
83
|
+
run.expiresAt = Math.min(run.expiresAt, result.expiresAt);
|
|
84
|
+
return ticket(run);
|
|
85
|
+
}
|
|
86
|
+
async function heartbeat(input) {
|
|
87
|
+
const run = find(input);
|
|
88
|
+
if (!run) return failure('run-lost');
|
|
89
|
+
const result = await serial(run, async () => {
|
|
90
|
+
const renewed = await call('lock', { query: { pet: run.petId, surface: 'web' },
|
|
91
|
+
body: { lockId: run.lockId, ttl: leaseMs } });
|
|
92
|
+
if (!current(run)) return failure('run-lost');
|
|
93
|
+
if (renewed?.ok !== true || renewed.lockId !== run.lockId || renewed.petId !== run.petId
|
|
94
|
+
|| !Number.isFinite(renewed.expiresAt) || renewed.expiresAt <= now()) {
|
|
95
|
+
return failure(renewed?.ok === false ? renewed.reason : 'invalid-lock-response');
|
|
96
|
+
}
|
|
97
|
+
run.expiresAt = Math.min(now() + leaseMs, renewed.expiresAt);
|
|
98
|
+
return ticket(run);
|
|
99
|
+
});
|
|
100
|
+
if (!result.ok) await close(run);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function commandBody(input) {
|
|
104
|
+
const action = input?.action;
|
|
105
|
+
if (action?.kind === 'anim') {
|
|
106
|
+
const url = assetUrl(action.name);
|
|
107
|
+
if (!url || !['left', 'right'].includes(action.facing)) return null;
|
|
108
|
+
return { path: 'anim', body: { url, facing: action.facing } };
|
|
109
|
+
}
|
|
110
|
+
if (action?.kind === 'position') {
|
|
111
|
+
if (![action.x, action.y].every(value => Number.isFinite(value) && value >= 0 && value <= 1)
|
|
112
|
+
|| !['instant', 'line', 'spring'].includes(action.mode)) return null;
|
|
113
|
+
if (action.mode === 'line' && (!Number.isFinite(action.duration) || action.duration < 0
|
|
114
|
+
|| action.duration > 180000)) return null;
|
|
115
|
+
return { path: 'position', body: { x: action.x, y: action.y, mode: action.mode,
|
|
116
|
+
...(action.mode === 'line' ? { duration: action.duration } : {}) } };
|
|
117
|
+
}
|
|
118
|
+
if (action?.kind === 'throw' && Number.isFinite(action.angle)
|
|
119
|
+
&& Number.isFinite(action.power) && action.power > 0 && action.power <= 10) {
|
|
120
|
+
return { path: 'throw', body: { angle: action.angle, power: action.power } };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
async function command(input) {
|
|
125
|
+
const run = find(input);
|
|
126
|
+
if (!run) return failure('run-lost');
|
|
127
|
+
const action = commandBody(input);
|
|
128
|
+
if (!action) return failure('bad-request');
|
|
129
|
+
const result = await serial(run, async () => {
|
|
130
|
+
const response = await call(action.path, { query: { pet: run.petId, surface: 'web', lock: run.lockId },
|
|
131
|
+
body: action.body });
|
|
132
|
+
if (!current(run)) return failure('run-lost');
|
|
133
|
+
if (response?.ok !== true) return failure(response?.reason || 'invalid-action-response');
|
|
134
|
+
if (action.path === 'anim' && !validId(response.playbackId)) return failure('missing-playback-id');
|
|
135
|
+
return { ok: true, ...(action.path === 'anim' ? { playbackId: response.playbackId } : {}) };
|
|
136
|
+
});
|
|
137
|
+
if (!result.ok) await close(run);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async function finish(input) {
|
|
141
|
+
const run = find(input);
|
|
142
|
+
if (!run) return failure('run-lost');
|
|
143
|
+
const released = await close(run);
|
|
144
|
+
return released?.ok === true ? { ok: true } : failure(released?.reason || 'release-failed');
|
|
145
|
+
}
|
|
146
|
+
async function dispose() {
|
|
147
|
+
active = false;
|
|
148
|
+
await Promise.all([...runs.values()].map(close));
|
|
149
|
+
}
|
|
150
|
+
return { generation, targets, start, heartbeat, command, finish, sweep, dispose };
|
|
151
|
+
}
|
package/lib/prepared.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 桥接专属素材名单;不包含官方 pet 的客户端、宿主或基础动画。 */
|
|
2
|
+
export const PET_NAMES = Object.freeze([
|
|
3
|
+
'通用-右侧螃蟹步-dshwe', '通用-左侧螃蟹步-dshwe',
|
|
4
|
+
'侧边栏-蓄力准备往后拉-dshwe', '侧边栏-后拉接疲态结束-dshwe',
|
|
5
|
+
'吉祥物-跳抓眩晕落地起身-dshwe', '彩蛋-侧边栏-半身转向踢腿-dshwe',
|
|
6
|
+
'彩蛋-侧边栏-被弹飞-dshwe', '彩蛋-侧边栏-落地-dshwe', '彩蛋-吉祥物-哈气-dshwe',
|
|
7
|
+
]);
|
|
8
|
+
export const MEME_NAMES = Object.freeze([
|
|
9
|
+
'累瘫鲸鱼的两碗饭', '蓝发鲸尾女仆拒绝换壁纸', '暴怒鲸尾女仆贴纸', '吓哭了', '?!欺负一只肥鱼!?',
|
|
10
|
+
]);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createReadStream, statSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { createOfficialPetCoordinator } from '../official-provider.js';
|
|
4
|
+
import { PET_NAMES, MEME_NAMES } from '../prepared.js';
|
|
5
|
+
|
|
6
|
+
export const DEPS = ['disposers', 'base', 'http'];
|
|
7
|
+
|
|
8
|
+
/** 正式官方桥接;端口来自宿主服务,不能把页面请求变成任意 URL 代理。 */
|
|
9
|
+
export function registerOfficialPetRoutes(webServer, c, options = {}) {
|
|
10
|
+
const { disposers, base, http } = c;
|
|
11
|
+
const BASE = base;
|
|
12
|
+
const origin = () => {
|
|
13
|
+
const port = webServer.port;
|
|
14
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('官方桌宠宿主端口尚未就绪');
|
|
15
|
+
return `http://127.0.0.1:${port}`;
|
|
16
|
+
};
|
|
17
|
+
const streams = new Set();
|
|
18
|
+
const assets = new Map();
|
|
19
|
+
for (const [index, name] of PET_NAMES.entries()) {
|
|
20
|
+
assets.set(name, { path: `${BASE}/pet-link/official/assets/a${index}.webm`,
|
|
21
|
+
file: fileURLToPath(new URL(`../../assets/webm/${name}.webm`, import.meta.url)), type: 'video/webm' });
|
|
22
|
+
}
|
|
23
|
+
const images = MEME_NAMES.map((name, index) => ({ path: `${BASE}/pet-link/official/assets/m${index}.png`,
|
|
24
|
+
file: fileURLToPath(new URL(`../../assets/memes/${name}.png`, import.meta.url)), type: 'image/png' }));
|
|
25
|
+
const request = options.request || (async (path, args) => {
|
|
26
|
+
const url = new URL(`/dsh-pet-7340/${path}`, origin());
|
|
27
|
+
for (const [key, value] of Object.entries(args.query || {})) url.searchParams.set(key, value);
|
|
28
|
+
const method = args.method || 'POST';
|
|
29
|
+
const response = await fetch(url, { method, redirect: 'error', cache: 'no-store',
|
|
30
|
+
signal: AbortSignal.timeout(2500), headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
...(method === 'GET' ? {} : { body: JSON.stringify(args.body) }) });
|
|
32
|
+
if (!response.ok) throw new Error('官方接口请求失败');
|
|
33
|
+
return response.json();
|
|
34
|
+
});
|
|
35
|
+
const coordinator = createOfficialPetCoordinator({ request,
|
|
36
|
+
assetUrl: name => assets.has(name) ? origin() + assets.get(name).path : null });
|
|
37
|
+
let active = true;
|
|
38
|
+
const timer = setInterval(() => { void coordinator.sweep(); }, 1000);
|
|
39
|
+
timer.unref?.();
|
|
40
|
+
disposers.push(() => {
|
|
41
|
+
active = false;
|
|
42
|
+
clearInterval(timer);
|
|
43
|
+
for (const stream of streams) stream.destroy();
|
|
44
|
+
streams.clear();
|
|
45
|
+
void coordinator.dispose();
|
|
46
|
+
});
|
|
47
|
+
const send = (res, status, value) => http.sendJson(res, status, value);
|
|
48
|
+
const enabled = () => active && (!options.isLinkEnabled || options.isLinkEnabled());
|
|
49
|
+
disposers.push(webServer.register({ kind: 'exact', path: `${BASE}/pet-link/official`,
|
|
50
|
+
handler: async (req, res) => {
|
|
51
|
+
if (req.method !== 'GET' && req.method !== 'POST') return send(res, 405, { ok: false, reason: 'method-not-allowed' });
|
|
52
|
+
if (!enabled()) return send(res, 200, { ok: false, reason: 'bridge-disabled' });
|
|
53
|
+
if (req.method === 'GET') return send(res, 200, await coordinator.targets());
|
|
54
|
+
// 本地 API 没有认证,仍拒绝浏览器的跨站写入;不信任 Host 头构造上游地址。
|
|
55
|
+
let sameOrigin = true;
|
|
56
|
+
if (req.headers?.origin) {
|
|
57
|
+
try {
|
|
58
|
+
const from = new URL(req.headers.origin);
|
|
59
|
+
sameOrigin = from.protocol === 'http:' && Number(from.port || 80) === webServer.port
|
|
60
|
+
&& ['127.0.0.1', 'localhost', '[::1]'].includes(from.hostname);
|
|
61
|
+
} catch { sameOrigin = false; }
|
|
62
|
+
}
|
|
63
|
+
if (!sameOrigin || req.headers?.['sec-fetch-site'] === 'cross-site') {
|
|
64
|
+
return send(res, 403, { ok: false, reason: 'cross-site-request' });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let size = 0;
|
|
69
|
+
for await (const chunk of req) {
|
|
70
|
+
size += Buffer.byteLength(chunk);
|
|
71
|
+
if (size > 8192) return send(res, 413, { ok: false, reason: 'body-too-large' });
|
|
72
|
+
chunks.push(Buffer.from(chunk));
|
|
73
|
+
}
|
|
74
|
+
const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
75
|
+
if (!enabled()) return send(res, 200, { ok: false, reason: 'bridge-disabled' });
|
|
76
|
+
const methods = { start: coordinator.start, heartbeat: coordinator.heartbeat,
|
|
77
|
+
command: coordinator.command, finish: coordinator.finish };
|
|
78
|
+
if (!Object.hasOwn(methods, input?.op)) return send(res, 400, { ok: false, reason: 'bad-request' });
|
|
79
|
+
return send(res, 200, await methods[input.op](input));
|
|
80
|
+
} catch { return send(res, 400, { ok: false, reason: 'bad-request' }); }
|
|
81
|
+
} }));
|
|
82
|
+
const assetPaths = new Map([...assets.values(), ...images].map(asset => [asset.path, asset]));
|
|
83
|
+
disposers.push(webServer.register({ kind: 'prefix', path: `${BASE}/pet-link/official/assets/`, handler: (req, res) => {
|
|
84
|
+
const asset = assetPaths.get(new URL(req.url, 'http://localhost').pathname);
|
|
85
|
+
if (!asset) { res.statusCode = 404; res.end(); return; }
|
|
86
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') { res.statusCode = 405; res.end(); return; }
|
|
87
|
+
if (!active) { res.statusCode = 410; res.end(); return; }
|
|
88
|
+
try {
|
|
89
|
+
const { size } = statSync(asset.file);
|
|
90
|
+
const range = req.headers?.range;
|
|
91
|
+
let start = 0;
|
|
92
|
+
let end = size - 1;
|
|
93
|
+
if (range) {
|
|
94
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
|
|
95
|
+
if (match && (match[1] || match[2])) {
|
|
96
|
+
if (match[1]) { start = Number(match[1]); end = match[2] ? Math.min(Number(match[2]), end) : end; }
|
|
97
|
+
else start = Math.max(0, size - Number(match[2]));
|
|
98
|
+
} else start = -1;
|
|
99
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start > end || start >= size) {
|
|
100
|
+
res.statusCode = 416; res.setHeader('Content-Range', `bytes */${size}`); res.end(); return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
res.statusCode = range ? 206 : 200;
|
|
104
|
+
res.setHeader('Content-Type', asset.type);
|
|
105
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
106
|
+
res.setHeader('Content-Length', end - start + 1);
|
|
107
|
+
if (range) res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
108
|
+
if (req.method === 'HEAD') { res.end(); return; }
|
|
109
|
+
const stream = createReadStream(asset.file, { start, end });
|
|
110
|
+
streams.add(stream);
|
|
111
|
+
stream.on('error', () => res.destroy());
|
|
112
|
+
stream.on('close', () => streams.delete(stream));
|
|
113
|
+
res.on('close', () => stream.destroy());
|
|
114
|
+
stream.pipe(res);
|
|
115
|
+
} catch { res.statusCode = 404; res.end(); }
|
|
116
|
+
} }));
|
|
117
|
+
return coordinator;
|
|
118
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wallpaper-engine-pet-link",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Optional official dsh-pet bridge: public API control, four interaction sequences and independent page bubbles; no bundled pet or patching.",
|
|
5
|
+
"icon": "./icon.png",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.pet-link.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./locale/*.json": "./locale/*.json",
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
14
|
+
},
|
|
15
|
+
"dsh": {
|
|
16
|
+
"bundle": {
|
|
17
|
+
"patch": "./cordis.patch.yml"
|
|
18
|
+
},
|
|
19
|
+
"client": {
|
|
20
|
+
"inject": ["@deepseek-ai/dsh-client-runtime"],
|
|
21
|
+
"platform": "web",
|
|
22
|
+
"immediately": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": ["lib/", "cordis.patch.yml", "locale/", "icon.png", "package.json", "assets/"],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.5-rc.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|