koishi-plugin-argus 0.2.0 → 0.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/lib/blur.d.ts +14 -4
- package/lib/cache.d.ts +3 -1
- package/lib/compress.d.ts +17 -0
- package/lib/index.cjs +84 -24
- package/lib/index.d.ts +2 -1
- package/lib/index.mjs +93 -24
- package/package.json +2 -3
- package/readme.md +2 -1
package/lib/blur.d.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
export type BlurMode = 'gaussian' | 'fast';
|
|
2
2
|
export interface BlurOptions {
|
|
3
|
-
/** 模糊半径,越大越糊。 */
|
|
3
|
+
/** 模糊半径,越大越糊。0 = 不模糊。 */
|
|
4
4
|
radius: number;
|
|
5
|
-
/**
|
|
5
|
+
/** 兼容旧字段;photon 实现里 'gaussian' 会多过一次 box_blur。 */
|
|
6
6
|
mode?: BlurMode;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 模糊算法(photon WASM 实现):
|
|
10
|
+
*
|
|
11
|
+
* 把图缩到 1/N 尺寸(N 由 radius 决定),不做 upsample 直接编 JPEG。
|
|
12
|
+
* 缩小本身就是强力模糊(细节都被平均掉了),同时 JPEG 编码体积小、耗时短,
|
|
13
|
+
* 整体在 100ms 量级完成。聊天客户端展示时会自动放大,看起来就是糊图。
|
|
14
|
+
*
|
|
15
|
+
* - radius 0 → 直接编 JPEG(不模糊)
|
|
16
|
+
* - radius 1..50 → factor = round(radius / 4) + 2 ≈ 2-15 倍下采样
|
|
17
|
+
* - radius 51..200 → factor = round(radius / 6) + 4 ≈ 12-37 倍下采样
|
|
18
|
+
*
|
|
19
|
+
* `mode='gaussian'` 时多过一次 box_blur 让边缘柔和。
|
|
10
20
|
*/
|
|
11
|
-
export declare function blurImage(input: Buffer, options: BlurOptions):
|
|
21
|
+
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,29 @@ __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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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));
|
|
314
|
+
}
|
|
315
|
+
const factor = radius <= 50 ? Math.round(radius / 4) + 2 : Math.round(radius / 6) + 4;
|
|
316
|
+
const w = img.get_width();
|
|
317
|
+
const h2 = img.get_height();
|
|
318
|
+
const sw = Math.max(2, Math.round(w / factor));
|
|
319
|
+
const sh = Math.max(2, Math.round(h2 / factor));
|
|
320
|
+
const small = (0, import_node.resize)(img, sw, sh, import_node.SamplingFilter.Triangle);
|
|
321
|
+
try {
|
|
322
|
+
if (options.mode === "gaussian") (0, import_node.box_blur)(small);
|
|
323
|
+
return Buffer.from(small.get_bytes_jpeg(85));
|
|
324
|
+
} finally {
|
|
325
|
+
small.free();
|
|
323
326
|
}
|
|
327
|
+
} finally {
|
|
328
|
+
img.free();
|
|
324
329
|
}
|
|
325
|
-
return await image.getBuffer("image/png");
|
|
326
330
|
}
|
|
327
331
|
__name(blurImage, "blurImage");
|
|
328
332
|
function clamp(v, min, max) {
|
|
@@ -330,6 +334,47 @@ function clamp(v, min, max) {
|
|
|
330
334
|
}
|
|
331
335
|
__name(clamp, "clamp");
|
|
332
336
|
|
|
337
|
+
// src/compress.ts
|
|
338
|
+
var import_node2 = require("@cf-wasm/photon/node");
|
|
339
|
+
function compressToBudget(input, options) {
|
|
340
|
+
const target = Math.max(8 * 1024, options.targetBytes);
|
|
341
|
+
if (input.length <= target) return input;
|
|
342
|
+
const initialQuality = options.initialQuality ?? 80;
|
|
343
|
+
const minQuality = options.minQuality ?? 40;
|
|
344
|
+
const img = import_node2.PhotonImage.new_from_byteslice(new Uint8Array(input));
|
|
345
|
+
try {
|
|
346
|
+
const ratioByteWise = target / input.length;
|
|
347
|
+
const estQ = Math.max(
|
|
348
|
+
minQuality,
|
|
349
|
+
Math.min(initialQuality, Math.round(initialQuality * ratioByteWise))
|
|
350
|
+
);
|
|
351
|
+
let buf = Buffer.from(img.get_bytes_jpeg(estQ));
|
|
352
|
+
if (buf.length <= target) return buf;
|
|
353
|
+
const q2 = Math.max(
|
|
354
|
+
minQuality,
|
|
355
|
+
Math.round(estQ * (target / buf.length) * 0.95)
|
|
356
|
+
);
|
|
357
|
+
if (q2 < estQ) {
|
|
358
|
+
buf = Buffer.from(img.get_bytes_jpeg(q2));
|
|
359
|
+
if (buf.length <= target) return buf;
|
|
360
|
+
}
|
|
361
|
+
const w = img.get_width();
|
|
362
|
+
const h2 = img.get_height();
|
|
363
|
+
const scale = Math.sqrt(target / buf.length) * 0.9;
|
|
364
|
+
const newW = Math.max(64, Math.round(w * scale));
|
|
365
|
+
const newH = Math.max(64, Math.round(h2 * scale));
|
|
366
|
+
const small = (0, import_node2.resize)(img, newW, newH, import_node2.SamplingFilter.Triangle);
|
|
367
|
+
try {
|
|
368
|
+
return Buffer.from(small.get_bytes_jpeg(q2));
|
|
369
|
+
} finally {
|
|
370
|
+
small.free();
|
|
371
|
+
}
|
|
372
|
+
} finally {
|
|
373
|
+
img.free();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
__name(compressToBudget, "compressToBudget");
|
|
377
|
+
|
|
333
378
|
// src/cache.ts
|
|
334
379
|
var PeekCache = class {
|
|
335
380
|
constructor(duration) {
|
|
@@ -467,7 +512,7 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
467
512
|
}
|
|
468
513
|
if (cached.image) {
|
|
469
514
|
return [
|
|
470
|
-
import_koishi.h.image(cached.image, "image/png"),
|
|
515
|
+
import_koishi.h.image(cached.image, cached.mime ?? "image/png"),
|
|
471
516
|
formatCacheNote(session, cached)
|
|
472
517
|
];
|
|
473
518
|
}
|
|
@@ -492,14 +537,28 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
492
537
|
);
|
|
493
538
|
return session.text(".failed", ["decrypt_failed"]);
|
|
494
539
|
}
|
|
495
|
-
const
|
|
540
|
+
const blurStart = Date.now();
|
|
541
|
+
const blurred = blurImage(buffer, {
|
|
496
542
|
radius,
|
|
497
543
|
mode: config.blurMode
|
|
498
544
|
});
|
|
545
|
+
const blurMs = Date.now() - blurStart;
|
|
546
|
+
const finalBudget = config.finalMaxKB * 1024;
|
|
547
|
+
const compressStart = Date.now();
|
|
548
|
+
const output = finalBudget > 0 && blurred.length > finalBudget ? compressToBudget(blurred, { targetBytes: finalBudget }) : blurred;
|
|
549
|
+
const compressMs = Date.now() - compressStart;
|
|
550
|
+
const mime = "image/jpeg";
|
|
551
|
+
ctx.logger.debug(
|
|
552
|
+
"peek pipeline: blur=%dms compress=%dms %dKB→%dKB",
|
|
553
|
+
blurMs,
|
|
554
|
+
compressMs,
|
|
555
|
+
Math.round(blurred.length / 1024),
|
|
556
|
+
Math.round(output.length / 1024)
|
|
557
|
+
);
|
|
499
558
|
if (opts.blur === void 0 || opts.blur === config.blur) {
|
|
500
|
-
cache.set(cacheKey, { image: output });
|
|
559
|
+
cache.set(cacheKey, { image: output, mime });
|
|
501
560
|
}
|
|
502
|
-
return import_koishi.h.image(output,
|
|
561
|
+
return import_koishi.h.image(output, mime);
|
|
503
562
|
} catch (err) {
|
|
504
563
|
const message = err instanceof Error ? err.message : String(err);
|
|
505
564
|
ctx.logger.warn(
|
|
@@ -627,7 +686,8 @@ var Config = import_koishi2.Schema.object({
|
|
|
627
686
|
blur: import_koishi2.Schema.natural().min(0).max(200).default(40),
|
|
628
687
|
blurMode: import_koishi2.Schema.union(["gaussian", "fast"]).default("fast"),
|
|
629
688
|
minBlur: import_koishi2.Schema.natural().min(0).max(200).default(10),
|
|
630
|
-
|
|
689
|
+
maxImageKB: import_koishi2.Schema.natural().default(8 * 1024),
|
|
690
|
+
finalMaxKB: import_koishi2.Schema.natural().default(200),
|
|
631
691
|
timeout: import_koishi2.Schema.natural().default(15e3),
|
|
632
692
|
cacheDuration: import_koishi2.Schema.natural().default(5 * 60 * 1e3),
|
|
633
693
|
registerAlias: import_koishi2.Schema.boolean().default(true),
|
|
@@ -652,7 +712,7 @@ function apply(ctx, config) {
|
|
|
652
712
|
path: config.path,
|
|
653
713
|
token: config.token,
|
|
654
714
|
timeout: config.timeout,
|
|
655
|
-
maxImageBytes: config.
|
|
715
|
+
maxImageBytes: config.maxImageKB * 1024,
|
|
656
716
|
onClientChange: /* @__PURE__ */ __name((event) => {
|
|
657
717
|
if (event.type === "connect") {
|
|
658
718
|
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,34 @@ __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
|
+
box_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 factor = radius <= 50 ? Math.round(radius / 4) + 2 : Math.round(radius / 6) + 4;
|
|
298
|
+
const w = img.get_width();
|
|
299
|
+
const h2 = img.get_height();
|
|
300
|
+
const sw = Math.max(2, Math.round(w / factor));
|
|
301
|
+
const sh = Math.max(2, Math.round(h2 / factor));
|
|
302
|
+
const small = resize(img, sw, sh, SamplingFilter.Triangle);
|
|
303
|
+
try {
|
|
304
|
+
if (options.mode === "gaussian") box_blur(small);
|
|
305
|
+
return Buffer.from(small.get_bytes_jpeg(85));
|
|
306
|
+
} finally {
|
|
307
|
+
small.free();
|
|
300
308
|
}
|
|
309
|
+
} finally {
|
|
310
|
+
img.free();
|
|
301
311
|
}
|
|
302
|
-
return await image.getBuffer("image/png");
|
|
303
312
|
}
|
|
304
313
|
__name(blurImage, "blurImage");
|
|
305
314
|
function clamp(v, min, max) {
|
|
@@ -307,6 +316,51 @@ function clamp(v, min, max) {
|
|
|
307
316
|
}
|
|
308
317
|
__name(clamp, "clamp");
|
|
309
318
|
|
|
319
|
+
// src/compress.ts
|
|
320
|
+
import {
|
|
321
|
+
PhotonImage as PhotonImage2,
|
|
322
|
+
resize as resize2,
|
|
323
|
+
SamplingFilter as SamplingFilter2
|
|
324
|
+
} from "@cf-wasm/photon/node";
|
|
325
|
+
function compressToBudget(input, options) {
|
|
326
|
+
const target = Math.max(8 * 1024, options.targetBytes);
|
|
327
|
+
if (input.length <= target) return input;
|
|
328
|
+
const initialQuality = options.initialQuality ?? 80;
|
|
329
|
+
const minQuality = options.minQuality ?? 40;
|
|
330
|
+
const img = PhotonImage2.new_from_byteslice(new Uint8Array(input));
|
|
331
|
+
try {
|
|
332
|
+
const ratioByteWise = target / input.length;
|
|
333
|
+
const estQ = Math.max(
|
|
334
|
+
minQuality,
|
|
335
|
+
Math.min(initialQuality, Math.round(initialQuality * ratioByteWise))
|
|
336
|
+
);
|
|
337
|
+
let buf = Buffer.from(img.get_bytes_jpeg(estQ));
|
|
338
|
+
if (buf.length <= target) return buf;
|
|
339
|
+
const q2 = Math.max(
|
|
340
|
+
minQuality,
|
|
341
|
+
Math.round(estQ * (target / buf.length) * 0.95)
|
|
342
|
+
);
|
|
343
|
+
if (q2 < estQ) {
|
|
344
|
+
buf = Buffer.from(img.get_bytes_jpeg(q2));
|
|
345
|
+
if (buf.length <= target) return buf;
|
|
346
|
+
}
|
|
347
|
+
const w = img.get_width();
|
|
348
|
+
const h2 = img.get_height();
|
|
349
|
+
const scale = Math.sqrt(target / buf.length) * 0.9;
|
|
350
|
+
const newW = Math.max(64, Math.round(w * scale));
|
|
351
|
+
const newH = Math.max(64, Math.round(h2 * scale));
|
|
352
|
+
const small = resize2(img, newW, newH, SamplingFilter2.Triangle);
|
|
353
|
+
try {
|
|
354
|
+
return Buffer.from(small.get_bytes_jpeg(q2));
|
|
355
|
+
} finally {
|
|
356
|
+
small.free();
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
img.free();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
__name(compressToBudget, "compressToBudget");
|
|
363
|
+
|
|
310
364
|
// src/cache.ts
|
|
311
365
|
var PeekCache = class {
|
|
312
366
|
constructor(duration) {
|
|
@@ -449,7 +503,7 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
449
503
|
}
|
|
450
504
|
if (cached.image) {
|
|
451
505
|
return [
|
|
452
|
-
h.image(cached.image, "image/png"),
|
|
506
|
+
h.image(cached.image, cached.mime ?? "image/png"),
|
|
453
507
|
formatCacheNote(session, cached)
|
|
454
508
|
];
|
|
455
509
|
}
|
|
@@ -474,14 +528,28 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
474
528
|
);
|
|
475
529
|
return session.text(".failed", ["decrypt_failed"]);
|
|
476
530
|
}
|
|
477
|
-
const
|
|
531
|
+
const blurStart = Date.now();
|
|
532
|
+
const blurred = blurImage(buffer, {
|
|
478
533
|
radius,
|
|
479
534
|
mode: config.blurMode
|
|
480
535
|
});
|
|
536
|
+
const blurMs = Date.now() - blurStart;
|
|
537
|
+
const finalBudget = config.finalMaxKB * 1024;
|
|
538
|
+
const compressStart = Date.now();
|
|
539
|
+
const output = finalBudget > 0 && blurred.length > finalBudget ? compressToBudget(blurred, { targetBytes: finalBudget }) : blurred;
|
|
540
|
+
const compressMs = Date.now() - compressStart;
|
|
541
|
+
const mime = "image/jpeg";
|
|
542
|
+
ctx.logger.debug(
|
|
543
|
+
"peek pipeline: blur=%dms compress=%dms %dKB→%dKB",
|
|
544
|
+
blurMs,
|
|
545
|
+
compressMs,
|
|
546
|
+
Math.round(blurred.length / 1024),
|
|
547
|
+
Math.round(output.length / 1024)
|
|
548
|
+
);
|
|
481
549
|
if (opts.blur === void 0 || opts.blur === config.blur) {
|
|
482
|
-
cache.set(cacheKey, { image: output });
|
|
550
|
+
cache.set(cacheKey, { image: output, mime });
|
|
483
551
|
}
|
|
484
|
-
return h.image(output,
|
|
552
|
+
return h.image(output, mime);
|
|
485
553
|
} catch (err) {
|
|
486
554
|
const message = err instanceof Error ? err.message : String(err);
|
|
487
555
|
ctx.logger.warn(
|
|
@@ -609,7 +677,8 @@ var Config = Schema.object({
|
|
|
609
677
|
blur: Schema.natural().min(0).max(200).default(40),
|
|
610
678
|
blurMode: Schema.union(["gaussian", "fast"]).default("fast"),
|
|
611
679
|
minBlur: Schema.natural().min(0).max(200).default(10),
|
|
612
|
-
|
|
680
|
+
maxImageKB: Schema.natural().default(8 * 1024),
|
|
681
|
+
finalMaxKB: Schema.natural().default(200),
|
|
613
682
|
timeout: Schema.natural().default(15e3),
|
|
614
683
|
cacheDuration: Schema.natural().default(5 * 60 * 1e3),
|
|
615
684
|
registerAlias: Schema.boolean().default(true),
|
|
@@ -634,7 +703,7 @@ function apply(ctx, config) {
|
|
|
634
703
|
path: config.path,
|
|
635
704
|
token: config.token,
|
|
636
705
|
timeout: config.timeout,
|
|
637
|
-
maxImageBytes: config.
|
|
706
|
+
maxImageBytes: config.maxImageKB * 1024,
|
|
638
707
|
onClientChange: /* @__PURE__ */ __name((event) => {
|
|
639
708
|
if (event.type === "connect") {
|
|
640
709
|
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.0",
|
|
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` | 是否给每个客户端注册同名别名 |
|