dsh-perf-boost 0.0.0-stage → 0.2.1
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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/assets/icon-180.png +0 -0
- package/assets/icon.svg +19 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1230 -0
- package/lib/host/warmup.js +146 -0
- package/lib/index.js +278 -0
- package/package.json +65 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// 大会话预热:把「最近用过的大会话」的文件先读一遍,让系统文件缓存里已经摊开,
|
|
2
|
+
// 这样你点进去时读的是内存而不是硬盘。只读、分段、可中断,完全不碰会话数据本身。
|
|
3
|
+
//
|
|
4
|
+
// 为什么不做「直接热投影」:投影缓存属于宿主内部实现(sessionProjections /
|
|
5
|
+
// sessionProjectionCache),上游没给预热接口。硬塞进去一旦签名变化就会在启动时
|
|
6
|
+
// 抛错,违背「稳定性别变差」。这里只做没有任何耦合面的文件缓存预热。
|
|
7
|
+
|
|
8
|
+
import { open, readdir, stat } from 'node:fs/promises';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
minBytes: 8 * 1024 * 1024, // 只预热 8MB 以上的会话(本机 39 个)
|
|
14
|
+
maxSessions: 10, // 最多 10 个
|
|
15
|
+
maxBytes: 400 * 1024 * 1024, // 一次最多读 400MB,到量就停
|
|
16
|
+
maxWarmMs: 180 * 1000, // 最多占用 3 分钟(可中断)
|
|
17
|
+
chunkBytes: 4 * 1024 * 1024,
|
|
18
|
+
pauseMs: 40, // 每读完一块歇一下,别跟正常使用抢 IO
|
|
19
|
+
delayMs: 15 * 1000, // 启动后等 15 秒再开始,避开首屏高峰
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function num(value, fallback) {
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function settingsFrom(config) {
|
|
28
|
+
return {
|
|
29
|
+
minBytes: num(config?.minBytes, DEFAULTS.minBytes),
|
|
30
|
+
maxSessions: num(config?.maxSessions, DEFAULTS.maxSessions),
|
|
31
|
+
maxBytes: num(config?.maxBytes, DEFAULTS.maxBytes),
|
|
32
|
+
maxWarmMs: num(config?.maxWarmMs, DEFAULTS.maxWarmMs),
|
|
33
|
+
chunkBytes: num(config?.chunkBytes, DEFAULTS.chunkBytes),
|
|
34
|
+
pauseMs: num(config?.pauseMs, DEFAULTS.pauseMs),
|
|
35
|
+
delayMs: num(config?.delayMs, DEFAULTS.delayMs),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SESSION_FILE = /^session\.[\w.-]+\.(jsonl|zstd|jsonl\.zstd)$/;
|
|
40
|
+
|
|
41
|
+
async function collectCandidates(root, minBytes, limit) {
|
|
42
|
+
const found = [];
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const projects = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
45
|
+
for (const project of projects) {
|
|
46
|
+
if (!project.isDirectory()) continue;
|
|
47
|
+
const projectDir = join(root, project.name);
|
|
48
|
+
const sessions = await readdir(projectDir, { withFileTypes: true }).catch(() => []);
|
|
49
|
+
for (const entry of sessions) {
|
|
50
|
+
if (!entry.isDirectory()) continue;
|
|
51
|
+
const dir = join(projectDir, entry.name);
|
|
52
|
+
const files = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (!file.isFile() || !SESSION_FILE.test(file.name)) continue;
|
|
55
|
+
const path = join(dir, file.name);
|
|
56
|
+
const info = await stat(path).catch(() => null);
|
|
57
|
+
if (!info || info.size < minBytes) continue;
|
|
58
|
+
// 最近十分钟还在写的文件就是当前会话:这时候读它只会和首屏、流式落盘抢磁盘。
|
|
59
|
+
if (now - info.mtimeMs < 10 * 60 * 1000) continue;
|
|
60
|
+
found.push({ path, size: info.size, mtimeMs: info.mtimeMs });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
found.sort((a, b) => b.mtimeMs - a.mtimeMs || b.size - a.size);
|
|
65
|
+
return found.slice(0, limit);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function warmOne(path, size, options, deadline) {
|
|
69
|
+
const handle = await open(path, 'r').catch(() => null);
|
|
70
|
+
if (!handle) return 0;
|
|
71
|
+
const buffer = Buffer.allocUnsafe(Math.min(options.chunkBytes, 8 * 1024 * 1024));
|
|
72
|
+
let read = 0;
|
|
73
|
+
try {
|
|
74
|
+
for (;;) {
|
|
75
|
+
if (Date.now() > deadline) break;
|
|
76
|
+
const result = await handle.read(buffer, 0, buffer.length, read).catch(() => null);
|
|
77
|
+
if (!result || result.bytesRead <= 0) break;
|
|
78
|
+
read += result.bytesRead;
|
|
79
|
+
if (read >= size) break;
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, options.pauseMs));
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
await handle.close().catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
return read;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {{ ctx: any, logger: any, config: any }} options
|
|
90
|
+
*/
|
|
91
|
+
export function createWarmup({ ctx, logger, config }) {
|
|
92
|
+
const options = settingsFrom(config);
|
|
93
|
+
let timer = null;
|
|
94
|
+
let stopped = false;
|
|
95
|
+
|
|
96
|
+
function stop() {
|
|
97
|
+
stopped = true;
|
|
98
|
+
if (timer !== null) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
timer = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function run() {
|
|
105
|
+
if (stopped) return;
|
|
106
|
+
const root = join(homedir(), '.dsh', 'sessions');
|
|
107
|
+
const candidates = await collectCandidates(root, options.minBytes, options.maxSessions);
|
|
108
|
+
if (candidates.length === 0) {
|
|
109
|
+
logger.info?.('warmup: 没有需要预热的大会话');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const deadline = Date.now() + options.maxWarmMs;
|
|
113
|
+
let total = 0;
|
|
114
|
+
let done = 0;
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
if (stopped || Date.now() > deadline) break;
|
|
117
|
+
if (total >= options.maxBytes) break;
|
|
118
|
+
const bytes = await warmOne(candidate.path, candidate.size, options, deadline);
|
|
119
|
+
total += bytes;
|
|
120
|
+
if (bytes > 0) done += 1;
|
|
121
|
+
}
|
|
122
|
+
if (total > 0) {
|
|
123
|
+
logger.info?.(`warmup: 已预热 ${(total / 1024 / 1024).toFixed(0)}MB(${done} 个大会话)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
start() {
|
|
129
|
+
// 宿主关闭时清掉定时器,别留下悬空的句柄。
|
|
130
|
+
try {
|
|
131
|
+
if (ctx && typeof ctx.on === 'function') ctx.on('dispose', stop);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
void error;
|
|
134
|
+
}
|
|
135
|
+
timer = setTimeout(() => {
|
|
136
|
+
timer = null;
|
|
137
|
+
run().catch((error) => logger.warn?.('warmup 失败:', error?.message ?? error));
|
|
138
|
+
}, options.delayMs);
|
|
139
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
140
|
+
},
|
|
141
|
+
stop,
|
|
142
|
+
options,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const warmupDefaults = DEFAULTS;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// lib/index.js —— 宿主半边。
|
|
2
|
+
//
|
|
3
|
+
// 插件的能力绝大部分在客户端(渲染进程)实现:帧率上限、长对话省渲染、动画开关。
|
|
4
|
+
// 宿主侧只做两件事:
|
|
5
|
+
//
|
|
6
|
+
// 1. 提供一个挂载点,让 cordis.patch.yml 把插件插进 profile,从而把 lib/client.js
|
|
7
|
+
// 作为客户端 bundle 下发;
|
|
8
|
+
// 2. 提供一条同源路由 `POST /api/dsh-perf-boost/update-check`,代替页面去问 npm
|
|
9
|
+
// 上的最新版本(页面自己请求跨域会被 CSP 拦掉,读不到 npm registry)。
|
|
10
|
+
//
|
|
11
|
+
// 路由只读、无副作用:只取 `latest` 标签的版本号,不下载、不安装、不落盘。
|
|
12
|
+
// 网络不可用时返回 `latest: null`,页面显示「未查到」,其余功能不受影响。
|
|
13
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { createWarmup } from './host/warmup.js';
|
|
18
|
+
|
|
19
|
+
export const name = 'dsh-perf-boost';
|
|
20
|
+
|
|
21
|
+
// 不声明 inject:宿主侧只读文件、不依赖任何服务,少一个依赖就少一种启动失败的方式。
|
|
22
|
+
|
|
23
|
+
const ROUTE = '/api/dsh-perf-boost/update-check';
|
|
24
|
+
const LAST_ROUTE = '/api/dsh-perf-boost/last-run';
|
|
25
|
+
// 页面每次启动端口都不一样(localhost:随机端口),localStorage 跟着端口走,
|
|
26
|
+
// 所以要跨启动留一份「上次实测」只能落到宿主侧的文件里。
|
|
27
|
+
const LAST_FILE = join(homedir(), '.dsh', 'perf-boost-last.json');
|
|
28
|
+
// 大会话预热开关:客户端只在设置页里改,宿主要能持久化,重启后仍然是你的选择。
|
|
29
|
+
const WARM_ROUTE = '/api/dsh-perf-boost/warmup';
|
|
30
|
+
const WARM_FILE = join(homedir(), '.dsh', 'perf-boost-warmup.json');
|
|
31
|
+
const PACKAGE_NAME = 'dsh-perf-boost';
|
|
32
|
+
// npmmirror 优先(国内快),失败再问官方源。
|
|
33
|
+
const REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org'];
|
|
34
|
+
const CACHE_MS = 10 * 60 * 1000;
|
|
35
|
+
const TIMEOUT_MS = 8000;
|
|
36
|
+
|
|
37
|
+
let cache = { at: 0, latest: null, source: '', error: '' };
|
|
38
|
+
|
|
39
|
+
async function readInstalledVersion() {
|
|
40
|
+
try {
|
|
41
|
+
const text = await readFile(new URL('../package.json', import.meta.url), 'utf8');
|
|
42
|
+
const pkg = JSON.parse(text);
|
|
43
|
+
return typeof pkg.version === 'string' ? pkg.version : '';
|
|
44
|
+
} catch {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseVersion(value) {
|
|
50
|
+
if (typeof value !== 'string') return null;
|
|
51
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
|
|
52
|
+
if (match === null) return null;
|
|
53
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isNewer(candidate, current) {
|
|
57
|
+
const a = parseVersion(candidate);
|
|
58
|
+
const b = parseVersion(current);
|
|
59
|
+
if (a === null || b === null) return false;
|
|
60
|
+
for (let i = 0; i < 3; i += 1) {
|
|
61
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fetchLatest(registry) {
|
|
67
|
+
const url = `${registry}/${PACKAGE_NAME}/latest`;
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
headers: { accept: 'application/json' },
|
|
70
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
73
|
+
const body = await response.json();
|
|
74
|
+
const version = body?.version;
|
|
75
|
+
if (typeof version !== 'string' || version === '') throw new Error('响应里没有 version');
|
|
76
|
+
return { version, source: registry };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function resolveLatest() {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (cache.at !== 0 && cache.at !== undefined && now - cache.at < CACHE_MS) return cache;
|
|
82
|
+
let lastError = '未连接';
|
|
83
|
+
let notPublished = false;
|
|
84
|
+
let best = null;
|
|
85
|
+
let bestSource = '';
|
|
86
|
+
for (const registry of REGISTRIES) {
|
|
87
|
+
try {
|
|
88
|
+
const { version, source } = await fetchLatest(registry);
|
|
89
|
+
// 镜像可能比官方慢半拍,两边都问,取版本更高的那个,别被慢的镜像带偏。
|
|
90
|
+
if (best === null || isNewer(version, best)) {
|
|
91
|
+
best = version;
|
|
92
|
+
bestSource = source;
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
lastError = error?.message ?? String(error);
|
|
96
|
+
// 所有镜像都 404 = 这个包还没发到 npm,不算「网络失败」。
|
|
97
|
+
if (lastError === 'HTTP 404') notPublished = true;
|
|
98
|
+
else notPublished = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (best !== null) {
|
|
102
|
+
cache = { at: now, latest: best, source: bestSource, error: '' };
|
|
103
|
+
return cache;
|
|
104
|
+
}
|
|
105
|
+
cache = { at: now, latest: null, source: '', error: notPublished ? 'not-published' : lastError };
|
|
106
|
+
return cache;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readWarmSetting() {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(await readFile(WARM_FILE, 'utf8'));
|
|
112
|
+
if (typeof parsed?.enabled === 'boolean') return parsed.enabled;
|
|
113
|
+
} catch {
|
|
114
|
+
// 没有文件 / 文件坏了:用默认值(开)
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeWarmSetting(enabled) {
|
|
120
|
+
try {
|
|
121
|
+
await mkdir(join(homedir(), '.dsh'), { recursive: true });
|
|
122
|
+
await writeFile(WARM_FILE, JSON.stringify({ enabled: !!enabled }), 'utf8');
|
|
123
|
+
} catch {
|
|
124
|
+
// 写不进去只影响「下次启动还记得吗」,本次开关照旧生效
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function apply(ctx, config) {
|
|
129
|
+
// 大会话预热(宿主侧,可关):把投影冷折叠从首屏挪到后台分片做。
|
|
130
|
+
// 结构不认识时它会自己静默退出,不影响下面这条路由与客户端功能。
|
|
131
|
+
let warm = null;
|
|
132
|
+
let warmSetting = true; // 落盘的开关状态,和 warm 实例分开记
|
|
133
|
+
const warmBlocked = config?.warmup === false;
|
|
134
|
+
const warmLogger = (() => {
|
|
135
|
+
try {
|
|
136
|
+
return typeof ctx.logger === 'function' ? ctx.logger('perf-boost') : { info() {}, warn: console.warn };
|
|
137
|
+
} catch {
|
|
138
|
+
return { info() {}, warn: console.warn };
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
function startWarm() {
|
|
143
|
+
if (warmBlocked || warm !== null) return;
|
|
144
|
+
try {
|
|
145
|
+
warm = createWarmup({ ctx, logger: warmLogger, config: config || {} });
|
|
146
|
+
warm.start();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
warm = null;
|
|
149
|
+
try { console.warn('[perf-boost] warmup 装配失败:', error?.message ?? error); } catch { void 0; }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stopWarm() {
|
|
154
|
+
if (warm === null) return;
|
|
155
|
+
try { warm.stop(); } catch { void 0; }
|
|
156
|
+
warm = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!warmBlocked) {
|
|
160
|
+
readWarmSetting()
|
|
161
|
+
.then((on) => { warmSetting = on; if (on) startWarm(); })
|
|
162
|
+
.catch(() => { void 0; });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ctx.inject(['connection'], (c) => {
|
|
166
|
+
const connection = c.get('connection');
|
|
167
|
+
const register = typeof connection?.fetch?.register === 'function'
|
|
168
|
+
? connection.fetch.register.bind(connection.fetch)
|
|
169
|
+
: undefined;
|
|
170
|
+
if (register === undefined) return;
|
|
171
|
+
|
|
172
|
+
// 读/写上一条实测结果。写只接受四个数字字段,不落任何别的东西。
|
|
173
|
+
const lastHandler = async (request) => {
|
|
174
|
+
let body = null;
|
|
175
|
+
try {
|
|
176
|
+
const text = typeof request?.text === 'function' ? await request.text() : '';
|
|
177
|
+
if (text) body = JSON.parse(text);
|
|
178
|
+
} catch {
|
|
179
|
+
body = null;
|
|
180
|
+
}
|
|
181
|
+
const wantWrite = body && typeof body.pct === 'number';
|
|
182
|
+
if (wantWrite) {
|
|
183
|
+
const round = (n) => (Number.isFinite(n) ? Math.round(n) : 0);
|
|
184
|
+
const record = {
|
|
185
|
+
pct: Math.max(0, Math.min(100, round(body.pct))),
|
|
186
|
+
saved: Math.max(0, round(body.saved)),
|
|
187
|
+
busy: Math.max(0, round(body.busy)),
|
|
188
|
+
at: Number.isFinite(body.at) ? Math.round(body.at) : Date.now(),
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
await mkdir(join(homedir(), '.dsh'), { recursive: true });
|
|
192
|
+
await writeFile(LAST_FILE, JSON.stringify(record), 'utf8');
|
|
193
|
+
} catch {
|
|
194
|
+
// 写不进去不影响使用,下一次再试。
|
|
195
|
+
}
|
|
196
|
+
return Response.json({ ok: true, value: record }, { headers: { 'cache-control': 'no-store' } });
|
|
197
|
+
}
|
|
198
|
+
let value = null;
|
|
199
|
+
try {
|
|
200
|
+
const text = await readFile(LAST_FILE, 'utf8');
|
|
201
|
+
const parsed = JSON.parse(text);
|
|
202
|
+
if (typeof parsed?.pct === 'number') value = parsed;
|
|
203
|
+
} catch {
|
|
204
|
+
value = null;
|
|
205
|
+
}
|
|
206
|
+
return Response.json({ ok: true, value }, { headers: { 'cache-control': 'no-store' } });
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
c.effect(
|
|
211
|
+
() => register({ path: LAST_ROUTE, methods: ['POST'], requestBody: 'buffered', fetch: lastHandler }),
|
|
212
|
+
'dsh-perf-boost: /api/dsh-perf-boost/last-run',
|
|
213
|
+
);
|
|
214
|
+
} catch {
|
|
215
|
+
// 注册失败只是少一条缓存读取,面板会退回到只显示本次数据。
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 预热开关:读/写都走这一条,写会落盘,重启后保持。
|
|
219
|
+
const warmHandler = async (request) => {
|
|
220
|
+
let body = null;
|
|
221
|
+
try {
|
|
222
|
+
const text = typeof request?.text === 'function' ? await request.text() : '';
|
|
223
|
+
if (text) body = JSON.parse(text);
|
|
224
|
+
} catch {
|
|
225
|
+
body = null;
|
|
226
|
+
}
|
|
227
|
+
if (body && typeof body.enabled === 'boolean') {
|
|
228
|
+
warmSetting = body.enabled;
|
|
229
|
+
await writeWarmSetting(body.enabled);
|
|
230
|
+
if (body.enabled) startWarm();
|
|
231
|
+
else stopWarm();
|
|
232
|
+
} else {
|
|
233
|
+
warmSetting = await readWarmSetting();
|
|
234
|
+
if (!warmBlocked && warmSetting && warm === null) startWarm();
|
|
235
|
+
}
|
|
236
|
+
return Response.json({ ok: true, value: { enabled: warmSetting && !warmBlocked } }, { headers: { 'cache-control': 'no-store' } });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
c.effect(
|
|
241
|
+
() => register({ path: WARM_ROUTE, methods: ['POST'], requestBody: 'buffered', fetch: warmHandler }),
|
|
242
|
+
'dsh-perf-boost: /api/dsh-perf-boost/warmup',
|
|
243
|
+
);
|
|
244
|
+
} catch {
|
|
245
|
+
// 注册失败只是设置页那个开关不出现,预热仍按配置文件走。
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const handler = async () => {
|
|
249
|
+
const [installed, resolved] = await Promise.all([readInstalledVersion(), resolveLatest()]);
|
|
250
|
+
const latest = resolved.latest;
|
|
251
|
+
return Response.json(
|
|
252
|
+
{
|
|
253
|
+
ok: true,
|
|
254
|
+
value: {
|
|
255
|
+
installed,
|
|
256
|
+
latest,
|
|
257
|
+
updateAvailable: latest === null ? false : isNewer(latest, installed),
|
|
258
|
+
source: resolved.source,
|
|
259
|
+
checkedAt: new Date(resolved.at === 0 ? Date.now() : resolved.at).toISOString(),
|
|
260
|
+
error: latest === null ? resolved.error : '',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{ headers: { 'cache-control': 'no-store' } },
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
c.effect(
|
|
269
|
+
() => register({ path: ROUTE, methods: ['POST'], requestBody: 'buffered', fetch: handler }),
|
|
270
|
+
'dsh-perf-boost: /api/dsh-perf-boost/update-check',
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
// 注册失败只影响版本检查,插件其余功能照旧。
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export default { name, apply };
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-perf-boost",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "DSH 桌面端性能优化(纯客户端):长对话省渲染、窗口隐藏不排帧默认开,关背景模糊/减少动画/空闲降帧按需打开,设置页显示实测省下的主线程时间。Client-side rendering optimizations for DeepSeek Harness Desktop.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"deepseek-harness",
|
|
14
|
+
"dsh",
|
|
15
|
+
"dsh-plugin",
|
|
16
|
+
"performance",
|
|
17
|
+
"fps",
|
|
18
|
+
"requestanimationframe",
|
|
19
|
+
"content-visibility",
|
|
20
|
+
"性能优化",
|
|
21
|
+
"性能",
|
|
22
|
+
"优化",
|
|
23
|
+
"增强",
|
|
24
|
+
"流畅",
|
|
25
|
+
"加速",
|
|
26
|
+
"卡顿",
|
|
27
|
+
"省内存",
|
|
28
|
+
"faster",
|
|
29
|
+
"smooth",
|
|
30
|
+
"speed-up"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"lib",
|
|
34
|
+
"assets",
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"dsh": ">=0.2.0-0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/AlLHHH/dsh-perf-boost.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/AlLHHH/dsh-perf-boost#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/AlLHHH/dsh-perf-boost/issues"
|
|
53
|
+
},
|
|
54
|
+
"sideEffects": false,
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"platform": "web",
|
|
61
|
+
"inject": [
|
|
62
|
+
"slots",
|
|
63
|
+
"sessions"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|