koishi-plugin-argus 0.2.0 → 0.4.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/lib/blur.d.ts +13 -4
- package/lib/cache.d.ts +3 -1
- package/lib/compress.d.ts +17 -0
- package/lib/index.cjs +89 -24
- package/lib/index.d.ts +2 -1
- package/lib/index.mjs +98 -24
- package/package.json +2 -3
- package/readme.md +2 -1
package/lib/blur.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
export type BlurMode = 'gaussian' | 'fast';
|
|
2
2
|
export interface BlurOptions {
|
|
3
|
-
/** 模糊半径,越大越糊。 */
|
|
3
|
+
/** 模糊半径,越大越糊。0 = 不模糊。 */
|
|
4
4
|
radius: number;
|
|
5
|
-
/**
|
|
5
|
+
/** 兼容旧字段;当前实现都按 gaussian 处理。 */
|
|
6
6
|
mode?: BlurMode;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 真高斯模糊(photon WASM 实现)。
|
|
10
|
+
*
|
|
11
|
+
* 流程:
|
|
12
|
+
* 1. 把图缩到一半尺寸,减少模糊本身的计算量
|
|
13
|
+
* 2. 对缩小图做 gaussian_blur(半径按 radius 派生)
|
|
14
|
+
* 3. 放大回原尺寸(Triangle 让放大过程平滑,模糊就不会因放大变锯齿)
|
|
15
|
+
* 4. 编码 JPEG q=80
|
|
16
|
+
*
|
|
17
|
+
* 这种"先缩半 → 真高斯 → 拉回"的方式能在 ~500ms 内做出真正高斯模糊,
|
|
18
|
+
* 而不是简单的马赛克 / 像素化。
|
|
10
19
|
*/
|
|
11
|
-
export declare function blurImage(input: Buffer, options: BlurOptions):
|
|
20
|
+
export declare function blurImage(input: Buffer, options: BlurOptions): Buffer;
|
package/lib/cache.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ import type { PeekBusyFrame } from './types';
|
|
|
6
6
|
export interface CachedPeek {
|
|
7
7
|
cachedAt: number;
|
|
8
8
|
expiresAt: number;
|
|
9
|
-
/** 已经过模糊处理的最终
|
|
9
|
+
/** 已经过模糊处理的最终 buffer,busy 时为空。 */
|
|
10
10
|
image?: Buffer;
|
|
11
|
+
/** 缓存图片的 mime('image/png' | 'image/jpeg')。 */
|
|
12
|
+
mime?: string;
|
|
11
13
|
/** busy 状态:客户端在玩游戏 / 全屏。 */
|
|
12
14
|
busy?: PeekBusyFrame;
|
|
13
15
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CompressOptions {
|
|
2
|
+
/** 目标体积上限(字节)。 */
|
|
3
|
+
targetBytes: number;
|
|
4
|
+
/** 起始 jpeg 质量。 */
|
|
5
|
+
initialQuality?: number;
|
|
6
|
+
/** 最低 jpeg 质量。 */
|
|
7
|
+
minQuality?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 把图片压到 `targetBytes` 以内的 JPEG。
|
|
11
|
+
*
|
|
12
|
+
* 性能优先(photon WASM 实现):
|
|
13
|
+
* - 输入已经 ≤ target → 直接返回(0 ms)。
|
|
14
|
+
* - 否则估算 quality 编一遍。命中就完事;不行再 resize 一次。
|
|
15
|
+
* - photon 的 resize 偏慢(~180ms / 2560x1440),所以放在最后兜底。
|
|
16
|
+
*/
|
|
17
|
+
export declare function compressToBudget(input: Buffer, options: CompressOptions): Buffer;
|
package/lib/index.cjs
CHANGED
|
@@ -23,14 +23,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
23
23
|
// src/locales/zh-CN.schema.yml
|
|
24
24
|
var require_zh_CN_schema = __commonJS({
|
|
25
25
|
"src/locales/zh-CN.schema.yml"(exports2, module2) {
|
|
26
|
-
module2.exports = { $desc: "Argus 配置", $inner: { path: "WebSocket 服务挂载路径,必须以 / 开头。", token: "客户端鉴权 token;为空时所有连接都会被拒绝。", commandName: "主命令名(例如 peek、spy、look)。", blur: "默认模糊半径,越大越糊(0 = 不模糊)。", blurMode: { $desc: "模糊算法", $inner: ["高斯模糊(质量好,但慢)", "快速模糊(jimp.blur,速度快)"] }, minBlur: "命令里 -b 临时调小模糊时不可低于此值,防止裸奔。",
|
|
26
|
+
module2.exports = { $desc: "Argus 配置", $inner: { path: "WebSocket 服务挂载路径,必须以 / 开头。", token: "客户端鉴权 token;为空时所有连接都会被拒绝。", commandName: "主命令名(例如 peek、spy、look)。", blur: "默认模糊半径,越大越糊(0 = 不模糊)。", blurMode: { $desc: "模糊算法", $inner: ["高斯模糊(质量好,但慢)", "快速模糊(jimp.blur,速度快)"] }, minBlur: "命令里 -b 临时调小模糊时不可低于此值,防止裸奔。", maxImageKB: "单张截图(客户端→插件,加密后 base64)大小上限(KB)。超过会拒收。默认 8192(8MB)。", finalMaxKB: "发到群里的最终图片体积上限(KB)。插件做完模糊后会再压一遍 JPEG 到这个体积以内。默认 200。设 0 关闭。", timeout: "等待客户端响应的超时(毫秒)。", cacheDuration: "截图缓存时长(毫秒)。在此期间内同一客户端 + 显示器的反复调用会直接返回缓存的图。设为 0 关闭缓存。默认 5 分钟。", registerAlias: "是否给每个连进来的客户端自动注册同名命令作为别名。", authority: "命令所需的最低权限等级。", forceAuthority: "使用 -f / --force 绕过缓存所需的最低权限等级。" } };
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// src/locales/en-US.schema.yml
|
|
31
31
|
var require_en_US_schema = __commonJS({
|
|
32
32
|
"src/locales/en-US.schema.yml"(exports2, module2) {
|
|
33
|
-
module2.exports = { $desc: "Argus configuration", $inner: { path: "WebSocket mount path; must start with /.", token: "Auth token for clients; if empty all connections are rejected.", commandName: "Top-level command name (e.g. peek, spy, look).", blur: "Default blur radius; larger means blurrier (0 disables blur).", blurMode: { $desc: "Blur algorithm", $inner: ["Gaussian blur (better quality, slower)", "Fast blur (jimp.blur, faster)"] }, minBlur: "Lower bound for the temporary -b override, to prevent unblurred captures.",
|
|
33
|
+
module2.exports = { $desc: "Argus configuration", $inner: { path: "WebSocket mount path; must start with /.", token: "Auth token for clients; if empty all connections are rejected.", commandName: "Top-level command name (e.g. peek, spy, look).", blur: "Default blur radius; larger means blurrier (0 disables blur).", blurMode: { $desc: "Blur algorithm", $inner: ["Gaussian blur (better quality, slower)", "Fast blur (jimp.blur, faster)"] }, minBlur: "Lower bound for the temporary -b override, to prevent unblurred captures.", maxImageKB: "Max screenshot size (client -> plugin, encrypted base64) in KB. Larger payloads are rejected. Default 8192 (8MB).", finalMaxKB: "Final image size budget after blur (KB). The plugin recompresses to JPEG within this budget before sending to chat. Default 200. Set 0 to disable.", timeout: "Timeout (ms) waiting for a client response.", cacheDuration: "Screenshot cache duration in ms. Repeated calls within this window for the same client+display reuse the cached image. Set to 0 to disable. Default 5 minutes.", registerAlias: "Whether to register each connected client's name as a command alias.", authority: "Minimum authority level required to run the command.", forceAuthority: "Minimum authority level required to use -f / --force to bypass the cache." } };
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -304,25 +304,34 @@ __name(randomId, "randomId");
|
|
|
304
304
|
var import_koishi = require("koishi");
|
|
305
305
|
|
|
306
306
|
// src/blur.ts
|
|
307
|
-
var
|
|
308
|
-
|
|
309
|
-
var Jimp = (0, import_core.createJimp)({
|
|
310
|
-
formats: [...import_jimp.defaultFormats],
|
|
311
|
-
plugins: import_jimp.defaultPlugins
|
|
312
|
-
});
|
|
313
|
-
async function blurImage(input, options) {
|
|
307
|
+
var import_node = require("@cf-wasm/photon/node");
|
|
308
|
+
function blurImage(input, options) {
|
|
314
309
|
const radius = clamp(Math.round(options.radius), 0, 200);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
image.gaussian(r);
|
|
320
|
-
} else {
|
|
321
|
-
const r = Math.max(1, Math.min(100, radius));
|
|
322
|
-
image.blur(r);
|
|
310
|
+
const img = import_node.PhotonImage.new_from_byteslice(new Uint8Array(input));
|
|
311
|
+
try {
|
|
312
|
+
if (radius === 0) {
|
|
313
|
+
return Buffer.from(img.get_bytes_jpeg(85));
|
|
323
314
|
}
|
|
315
|
+
const w = img.get_width();
|
|
316
|
+
const h2 = img.get_height();
|
|
317
|
+
const halfW = Math.max(2, Math.round(w / 2));
|
|
318
|
+
const halfH = Math.max(2, Math.round(h2 / 2));
|
|
319
|
+
const halfRadius = Math.max(1, Math.round(radius / 2));
|
|
320
|
+
const half = (0, import_node.resize)(img, halfW, halfH, import_node.SamplingFilter.Triangle);
|
|
321
|
+
try {
|
|
322
|
+
(0, import_node.gaussian_blur)(half, halfRadius);
|
|
323
|
+
const back = (0, import_node.resize)(half, w, h2, import_node.SamplingFilter.Triangle);
|
|
324
|
+
try {
|
|
325
|
+
return Buffer.from(back.get_bytes_jpeg(80));
|
|
326
|
+
} finally {
|
|
327
|
+
back.free();
|
|
328
|
+
}
|
|
329
|
+
} finally {
|
|
330
|
+
half.free();
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
img.free();
|
|
324
334
|
}
|
|
325
|
-
return await image.getBuffer("image/png");
|
|
326
335
|
}
|
|
327
336
|
__name(blurImage, "blurImage");
|
|
328
337
|
function clamp(v, min, max) {
|
|
@@ -330,6 +339,47 @@ function clamp(v, min, max) {
|
|
|
330
339
|
}
|
|
331
340
|
__name(clamp, "clamp");
|
|
332
341
|
|
|
342
|
+
// src/compress.ts
|
|
343
|
+
var import_node2 = require("@cf-wasm/photon/node");
|
|
344
|
+
function compressToBudget(input, options) {
|
|
345
|
+
const target = Math.max(8 * 1024, options.targetBytes);
|
|
346
|
+
if (input.length <= target) return input;
|
|
347
|
+
const initialQuality = options.initialQuality ?? 80;
|
|
348
|
+
const minQuality = options.minQuality ?? 40;
|
|
349
|
+
const img = import_node2.PhotonImage.new_from_byteslice(new Uint8Array(input));
|
|
350
|
+
try {
|
|
351
|
+
const ratioByteWise = target / input.length;
|
|
352
|
+
const estQ = Math.max(
|
|
353
|
+
minQuality,
|
|
354
|
+
Math.min(initialQuality, Math.round(initialQuality * ratioByteWise))
|
|
355
|
+
);
|
|
356
|
+
let buf = Buffer.from(img.get_bytes_jpeg(estQ));
|
|
357
|
+
if (buf.length <= target) return buf;
|
|
358
|
+
const q2 = Math.max(
|
|
359
|
+
minQuality,
|
|
360
|
+
Math.round(estQ * (target / buf.length) * 0.95)
|
|
361
|
+
);
|
|
362
|
+
if (q2 < estQ) {
|
|
363
|
+
buf = Buffer.from(img.get_bytes_jpeg(q2));
|
|
364
|
+
if (buf.length <= target) return buf;
|
|
365
|
+
}
|
|
366
|
+
const w = img.get_width();
|
|
367
|
+
const h2 = img.get_height();
|
|
368
|
+
const scale = Math.sqrt(target / buf.length) * 0.9;
|
|
369
|
+
const newW = Math.max(64, Math.round(w * scale));
|
|
370
|
+
const newH = Math.max(64, Math.round(h2 * scale));
|
|
371
|
+
const small = (0, import_node2.resize)(img, newW, newH, import_node2.SamplingFilter.Triangle);
|
|
372
|
+
try {
|
|
373
|
+
return Buffer.from(small.get_bytes_jpeg(q2));
|
|
374
|
+
} finally {
|
|
375
|
+
small.free();
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
img.free();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
__name(compressToBudget, "compressToBudget");
|
|
382
|
+
|
|
333
383
|
// src/cache.ts
|
|
334
384
|
var PeekCache = class {
|
|
335
385
|
constructor(duration) {
|
|
@@ -467,7 +517,7 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
467
517
|
}
|
|
468
518
|
if (cached.image) {
|
|
469
519
|
return [
|
|
470
|
-
import_koishi.h.image(cached.image, "image/png"),
|
|
520
|
+
import_koishi.h.image(cached.image, cached.mime ?? "image/png"),
|
|
471
521
|
formatCacheNote(session, cached)
|
|
472
522
|
];
|
|
473
523
|
}
|
|
@@ -492,14 +542,28 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
492
542
|
);
|
|
493
543
|
return session.text(".failed", ["decrypt_failed"]);
|
|
494
544
|
}
|
|
495
|
-
const
|
|
545
|
+
const blurStart = Date.now();
|
|
546
|
+
const blurred = blurImage(buffer, {
|
|
496
547
|
radius,
|
|
497
548
|
mode: config.blurMode
|
|
498
549
|
});
|
|
550
|
+
const blurMs = Date.now() - blurStart;
|
|
551
|
+
const finalBudget = config.finalMaxKB * 1024;
|
|
552
|
+
const compressStart = Date.now();
|
|
553
|
+
const output = finalBudget > 0 && blurred.length > finalBudget ? compressToBudget(blurred, { targetBytes: finalBudget }) : blurred;
|
|
554
|
+
const compressMs = Date.now() - compressStart;
|
|
555
|
+
const mime = "image/jpeg";
|
|
556
|
+
ctx.logger.debug(
|
|
557
|
+
"peek pipeline: blur=%dms compress=%dms %dKB→%dKB",
|
|
558
|
+
blurMs,
|
|
559
|
+
compressMs,
|
|
560
|
+
Math.round(blurred.length / 1024),
|
|
561
|
+
Math.round(output.length / 1024)
|
|
562
|
+
);
|
|
499
563
|
if (opts.blur === void 0 || opts.blur === config.blur) {
|
|
500
|
-
cache.set(cacheKey, { image: output });
|
|
564
|
+
cache.set(cacheKey, { image: output, mime });
|
|
501
565
|
}
|
|
502
|
-
return import_koishi.h.image(output,
|
|
566
|
+
return import_koishi.h.image(output, mime);
|
|
503
567
|
} catch (err) {
|
|
504
568
|
const message = err instanceof Error ? err.message : String(err);
|
|
505
569
|
ctx.logger.warn(
|
|
@@ -627,7 +691,8 @@ var Config = import_koishi2.Schema.object({
|
|
|
627
691
|
blur: import_koishi2.Schema.natural().min(0).max(200).default(40),
|
|
628
692
|
blurMode: import_koishi2.Schema.union(["gaussian", "fast"]).default("fast"),
|
|
629
693
|
minBlur: import_koishi2.Schema.natural().min(0).max(200).default(10),
|
|
630
|
-
|
|
694
|
+
maxImageKB: import_koishi2.Schema.natural().default(8 * 1024),
|
|
695
|
+
finalMaxKB: import_koishi2.Schema.natural().default(200),
|
|
631
696
|
timeout: import_koishi2.Schema.natural().default(15e3),
|
|
632
697
|
cacheDuration: import_koishi2.Schema.natural().default(5 * 60 * 1e3),
|
|
633
698
|
registerAlias: import_koishi2.Schema.boolean().default(true),
|
|
@@ -652,7 +717,7 @@ function apply(ctx, config) {
|
|
|
652
717
|
path: config.path,
|
|
653
718
|
token: config.token,
|
|
654
719
|
timeout: config.timeout,
|
|
655
|
-
maxImageBytes: config.
|
|
720
|
+
maxImageBytes: config.maxImageKB * 1024,
|
|
656
721
|
onClientChange: /* @__PURE__ */ __name((event) => {
|
|
657
722
|
if (event.type === "connect") {
|
|
658
723
|
ctx.emit("argus/client-connect", event.name);
|
package/lib/index.d.ts
CHANGED
package/lib/index.mjs
CHANGED
|
@@ -8,14 +8,14 @@ var __commonJS = (cb, mod) => function __require() {
|
|
|
8
8
|
// src/locales/zh-CN.schema.yml
|
|
9
9
|
var require_zh_CN_schema = __commonJS({
|
|
10
10
|
"src/locales/zh-CN.schema.yml"(exports, module) {
|
|
11
|
-
module.exports = { $desc: "Argus 配置", $inner: { path: "WebSocket 服务挂载路径,必须以 / 开头。", token: "客户端鉴权 token;为空时所有连接都会被拒绝。", commandName: "主命令名(例如 peek、spy、look)。", blur: "默认模糊半径,越大越糊(0 = 不模糊)。", blurMode: { $desc: "模糊算法", $inner: ["高斯模糊(质量好,但慢)", "快速模糊(jimp.blur,速度快)"] }, minBlur: "命令里 -b 临时调小模糊时不可低于此值,防止裸奔。",
|
|
11
|
+
module.exports = { $desc: "Argus 配置", $inner: { path: "WebSocket 服务挂载路径,必须以 / 开头。", token: "客户端鉴权 token;为空时所有连接都会被拒绝。", commandName: "主命令名(例如 peek、spy、look)。", blur: "默认模糊半径,越大越糊(0 = 不模糊)。", blurMode: { $desc: "模糊算法", $inner: ["高斯模糊(质量好,但慢)", "快速模糊(jimp.blur,速度快)"] }, minBlur: "命令里 -b 临时调小模糊时不可低于此值,防止裸奔。", maxImageKB: "单张截图(客户端→插件,加密后 base64)大小上限(KB)。超过会拒收。默认 8192(8MB)。", finalMaxKB: "发到群里的最终图片体积上限(KB)。插件做完模糊后会再压一遍 JPEG 到这个体积以内。默认 200。设 0 关闭。", timeout: "等待客户端响应的超时(毫秒)。", cacheDuration: "截图缓存时长(毫秒)。在此期间内同一客户端 + 显示器的反复调用会直接返回缓存的图。设为 0 关闭缓存。默认 5 分钟。", registerAlias: "是否给每个连进来的客户端自动注册同名命令作为别名。", authority: "命令所需的最低权限等级。", forceAuthority: "使用 -f / --force 绕过缓存所需的最低权限等级。" } };
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
// src/locales/en-US.schema.yml
|
|
16
16
|
var require_en_US_schema = __commonJS({
|
|
17
17
|
"src/locales/en-US.schema.yml"(exports, module) {
|
|
18
|
-
module.exports = { $desc: "Argus configuration", $inner: { path: "WebSocket mount path; must start with /.", token: "Auth token for clients; if empty all connections are rejected.", commandName: "Top-level command name (e.g. peek, spy, look).", blur: "Default blur radius; larger means blurrier (0 disables blur).", blurMode: { $desc: "Blur algorithm", $inner: ["Gaussian blur (better quality, slower)", "Fast blur (jimp.blur, faster)"] }, minBlur: "Lower bound for the temporary -b override, to prevent unblurred captures.",
|
|
18
|
+
module.exports = { $desc: "Argus configuration", $inner: { path: "WebSocket mount path; must start with /.", token: "Auth token for clients; if empty all connections are rejected.", commandName: "Top-level command name (e.g. peek, spy, look).", blur: "Default blur radius; larger means blurrier (0 disables blur).", blurMode: { $desc: "Blur algorithm", $inner: ["Gaussian blur (better quality, slower)", "Fast blur (jimp.blur, faster)"] }, minBlur: "Lower bound for the temporary -b override, to prevent unblurred captures.", maxImageKB: "Max screenshot size (client -> plugin, encrypted base64) in KB. Larger payloads are rejected. Default 8192 (8MB).", finalMaxKB: "Final image size budget after blur (KB). The plugin recompresses to JPEG within this budget before sending to chat. Default 200. Set 0 to disable.", timeout: "Timeout (ms) waiting for a client response.", cacheDuration: "Screenshot cache duration in ms. Repeated calls within this window for the same client+display reuse the cached image. Set to 0 to disable. Default 5 minutes.", registerAlias: "Whether to register each connected client's name as a command alias.", authority: "Minimum authority level required to run the command.", forceAuthority: "Minimum authority level required to use -f / --force to bypass the cache." } };
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -281,25 +281,39 @@ __name(randomId, "randomId");
|
|
|
281
281
|
import { h } from "koishi";
|
|
282
282
|
|
|
283
283
|
// src/blur.ts
|
|
284
|
-
import {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
284
|
+
import {
|
|
285
|
+
PhotonImage,
|
|
286
|
+
gaussian_blur,
|
|
287
|
+
resize,
|
|
288
|
+
SamplingFilter
|
|
289
|
+
} from "@cf-wasm/photon/node";
|
|
290
|
+
function blurImage(input, options) {
|
|
291
291
|
const radius = clamp(Math.round(options.radius), 0, 200);
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
292
|
+
const img = PhotonImage.new_from_byteslice(new Uint8Array(input));
|
|
293
|
+
try {
|
|
294
|
+
if (radius === 0) {
|
|
295
|
+
return Buffer.from(img.get_bytes_jpeg(85));
|
|
296
|
+
}
|
|
297
|
+
const w = img.get_width();
|
|
298
|
+
const h2 = img.get_height();
|
|
299
|
+
const halfW = Math.max(2, Math.round(w / 2));
|
|
300
|
+
const halfH = Math.max(2, Math.round(h2 / 2));
|
|
301
|
+
const halfRadius = Math.max(1, Math.round(radius / 2));
|
|
302
|
+
const half = resize(img, halfW, halfH, SamplingFilter.Triangle);
|
|
303
|
+
try {
|
|
304
|
+
gaussian_blur(half, halfRadius);
|
|
305
|
+
const back = resize(half, w, h2, SamplingFilter.Triangle);
|
|
306
|
+
try {
|
|
307
|
+
return Buffer.from(back.get_bytes_jpeg(80));
|
|
308
|
+
} finally {
|
|
309
|
+
back.free();
|
|
310
|
+
}
|
|
311
|
+
} finally {
|
|
312
|
+
half.free();
|
|
300
313
|
}
|
|
314
|
+
} finally {
|
|
315
|
+
img.free();
|
|
301
316
|
}
|
|
302
|
-
return await image.getBuffer("image/png");
|
|
303
317
|
}
|
|
304
318
|
__name(blurImage, "blurImage");
|
|
305
319
|
function clamp(v, min, max) {
|
|
@@ -307,6 +321,51 @@ function clamp(v, min, max) {
|
|
|
307
321
|
}
|
|
308
322
|
__name(clamp, "clamp");
|
|
309
323
|
|
|
324
|
+
// src/compress.ts
|
|
325
|
+
import {
|
|
326
|
+
PhotonImage as PhotonImage2,
|
|
327
|
+
resize as resize2,
|
|
328
|
+
SamplingFilter as SamplingFilter2
|
|
329
|
+
} from "@cf-wasm/photon/node";
|
|
330
|
+
function compressToBudget(input, options) {
|
|
331
|
+
const target = Math.max(8 * 1024, options.targetBytes);
|
|
332
|
+
if (input.length <= target) return input;
|
|
333
|
+
const initialQuality = options.initialQuality ?? 80;
|
|
334
|
+
const minQuality = options.minQuality ?? 40;
|
|
335
|
+
const img = PhotonImage2.new_from_byteslice(new Uint8Array(input));
|
|
336
|
+
try {
|
|
337
|
+
const ratioByteWise = target / input.length;
|
|
338
|
+
const estQ = Math.max(
|
|
339
|
+
minQuality,
|
|
340
|
+
Math.min(initialQuality, Math.round(initialQuality * ratioByteWise))
|
|
341
|
+
);
|
|
342
|
+
let buf = Buffer.from(img.get_bytes_jpeg(estQ));
|
|
343
|
+
if (buf.length <= target) return buf;
|
|
344
|
+
const q2 = Math.max(
|
|
345
|
+
minQuality,
|
|
346
|
+
Math.round(estQ * (target / buf.length) * 0.95)
|
|
347
|
+
);
|
|
348
|
+
if (q2 < estQ) {
|
|
349
|
+
buf = Buffer.from(img.get_bytes_jpeg(q2));
|
|
350
|
+
if (buf.length <= target) return buf;
|
|
351
|
+
}
|
|
352
|
+
const w = img.get_width();
|
|
353
|
+
const h2 = img.get_height();
|
|
354
|
+
const scale = Math.sqrt(target / buf.length) * 0.9;
|
|
355
|
+
const newW = Math.max(64, Math.round(w * scale));
|
|
356
|
+
const newH = Math.max(64, Math.round(h2 * scale));
|
|
357
|
+
const small = resize2(img, newW, newH, SamplingFilter2.Triangle);
|
|
358
|
+
try {
|
|
359
|
+
return Buffer.from(small.get_bytes_jpeg(q2));
|
|
360
|
+
} finally {
|
|
361
|
+
small.free();
|
|
362
|
+
}
|
|
363
|
+
} finally {
|
|
364
|
+
img.free();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
__name(compressToBudget, "compressToBudget");
|
|
368
|
+
|
|
310
369
|
// src/cache.ts
|
|
311
370
|
var PeekCache = class {
|
|
312
371
|
constructor(duration) {
|
|
@@ -449,7 +508,7 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
449
508
|
}
|
|
450
509
|
if (cached.image) {
|
|
451
510
|
return [
|
|
452
|
-
h.image(cached.image, "image/png"),
|
|
511
|
+
h.image(cached.image, cached.mime ?? "image/png"),
|
|
453
512
|
formatCacheNote(session, cached)
|
|
454
513
|
];
|
|
455
514
|
}
|
|
@@ -474,14 +533,28 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
474
533
|
);
|
|
475
534
|
return session.text(".failed", ["decrypt_failed"]);
|
|
476
535
|
}
|
|
477
|
-
const
|
|
536
|
+
const blurStart = Date.now();
|
|
537
|
+
const blurred = blurImage(buffer, {
|
|
478
538
|
radius,
|
|
479
539
|
mode: config.blurMode
|
|
480
540
|
});
|
|
541
|
+
const blurMs = Date.now() - blurStart;
|
|
542
|
+
const finalBudget = config.finalMaxKB * 1024;
|
|
543
|
+
const compressStart = Date.now();
|
|
544
|
+
const output = finalBudget > 0 && blurred.length > finalBudget ? compressToBudget(blurred, { targetBytes: finalBudget }) : blurred;
|
|
545
|
+
const compressMs = Date.now() - compressStart;
|
|
546
|
+
const mime = "image/jpeg";
|
|
547
|
+
ctx.logger.debug(
|
|
548
|
+
"peek pipeline: blur=%dms compress=%dms %dKB→%dKB",
|
|
549
|
+
blurMs,
|
|
550
|
+
compressMs,
|
|
551
|
+
Math.round(blurred.length / 1024),
|
|
552
|
+
Math.round(output.length / 1024)
|
|
553
|
+
);
|
|
481
554
|
if (opts.blur === void 0 || opts.blur === config.blur) {
|
|
482
|
-
cache.set(cacheKey, { image: output });
|
|
555
|
+
cache.set(cacheKey, { image: output, mime });
|
|
483
556
|
}
|
|
484
|
-
return h.image(output,
|
|
557
|
+
return h.image(output, mime);
|
|
485
558
|
} catch (err) {
|
|
486
559
|
const message = err instanceof Error ? err.message : String(err);
|
|
487
560
|
ctx.logger.warn(
|
|
@@ -609,7 +682,8 @@ var Config = Schema.object({
|
|
|
609
682
|
blur: Schema.natural().min(0).max(200).default(40),
|
|
610
683
|
blurMode: Schema.union(["gaussian", "fast"]).default("fast"),
|
|
611
684
|
minBlur: Schema.natural().min(0).max(200).default(10),
|
|
612
|
-
|
|
685
|
+
maxImageKB: Schema.natural().default(8 * 1024),
|
|
686
|
+
finalMaxKB: Schema.natural().default(200),
|
|
613
687
|
timeout: Schema.natural().default(15e3),
|
|
614
688
|
cacheDuration: Schema.natural().default(5 * 60 * 1e3),
|
|
615
689
|
registerAlias: Schema.boolean().default(true),
|
|
@@ -634,7 +708,7 @@ function apply(ctx, config) {
|
|
|
634
708
|
path: config.path,
|
|
635
709
|
token: config.token,
|
|
636
710
|
timeout: config.timeout,
|
|
637
|
-
maxImageBytes: config.
|
|
711
|
+
maxImageBytes: config.maxImageKB * 1024,
|
|
638
712
|
onClientChange: /* @__PURE__ */ __name((event) => {
|
|
639
713
|
if (event.type === "connect") {
|
|
640
714
|
ctx.emit("argus/client-connect", event.name);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-argus",
|
|
3
3
|
"description": "百眼巨人 Argus:让群友通过 /peek 命令偷窥你电脑屏幕的 Koishi 插件。",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"main": "lib/index.cjs",
|
|
6
6
|
"module": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
"koishi": "^4.18.9"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@
|
|
38
|
-
"jimp": "^1.6.0"
|
|
37
|
+
"@cf-wasm/photon": "^0.3.5"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
41
40
|
"@types/ws": "^8.5.10",
|
package/readme.md
CHANGED
|
@@ -70,7 +70,8 @@ npx argus-eye -s ws://your-koishi-host:5140/argus -t a-strong-secret -n dingyi
|
|
|
70
70
|
| `blur` | `number` | `40` | 默认模糊半径(pixel)|
|
|
71
71
|
| `blurMode` | `'gaussian' \| 'fast'` | `'fast'` | 模糊算法 |
|
|
72
72
|
| `minBlur` | `number` | `10` | 命令里调小模糊时不可低于此值 |
|
|
73
|
-
| `
|
|
73
|
+
| `maxImageKB` | `number` | `8192` | 单张截图大小上限(KB) |
|
|
74
|
+
| `finalMaxKB` | `number` | `200` | 发到群里的最终图片体积上限(KB),插件会做二次压缩;0 = 关闭 |
|
|
74
75
|
| `timeout` | `number` | `15000` | 等待客户端响应超时(ms)|
|
|
75
76
|
| `cacheDuration` | `number` | `300000` | 截图缓存时长(ms),默认 5 分钟,0 = 关闭缓存 |
|
|
76
77
|
| `registerAlias` | `boolean` | `true` | 是否给每个客户端注册同名别名 |
|