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 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
- /** 模糊算法。`gaussian` 质量好但慢;`fast` jimp.blur,速度快。 */
5
+ /** 兼容旧字段;photon 实现里 'gaussian' 会多过一次 box_blur。 */
6
6
  mode?: BlurMode;
7
7
  }
8
8
  /**
9
- * 加载任意常见格式(png/jpg/webp 等)的图片,应用模糊后输出 PNG buffer。
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): Promise<Buffer>;
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
- /** 已经过模糊处理的最终 PNG buffer,busy 时为空。 */
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 临时调小模糊时不可低于此值,防止裸奔。", maxImageBytes: "单张截图大小上限(字节)。", timeout: "等待客户端响应的超时(毫秒)。", cacheDuration: "截图缓存时长(毫秒)。在此期间内同一客户端 + 显示器的反复调用会直接返回缓存的图。设为 0 关闭缓存。默认 5 分钟。", registerAlias: "是否给每个连进来的客户端自动注册同名命令作为别名。", authority: "命令所需的最低权限等级。", forceAuthority: "使用 -f / --force 绕过缓存所需的最低权限等级。" } };
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.", maxImageBytes: "Max screenshot size in bytes.", 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." } };
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 import_core = require("@jimp/core");
308
- var import_jimp = require("jimp");
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 image = await Jimp.read(input);
316
- if (radius > 0) {
317
- if (options.mode === "gaussian") {
318
- const r = Math.max(1, Math.min(10, Math.round(radius / 20)));
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));
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 output = await blurImage(buffer, {
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, "image/png");
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
- maxImageBytes: import_koishi2.Schema.natural().default(8 * 1024 * 1024),
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.maxImageBytes,
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
@@ -9,7 +9,8 @@ export interface Config {
9
9
  blur: number;
10
10
  blurMode: BlurMode;
11
11
  minBlur: number;
12
- maxImageBytes: number;
12
+ maxImageKB: number;
13
+ finalMaxKB: number;
13
14
  timeout: number;
14
15
  cacheDuration: number;
15
16
  registerAlias: boolean;
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 临时调小模糊时不可低于此值,防止裸奔。", maxImageBytes: "单张截图大小上限(字节)。", timeout: "等待客户端响应的超时(毫秒)。", cacheDuration: "截图缓存时长(毫秒)。在此期间内同一客户端 + 显示器的反复调用会直接返回缓存的图。设为 0 关闭缓存。默认 5 分钟。", registerAlias: "是否给每个连进来的客户端自动注册同名命令作为别名。", authority: "命令所需的最低权限等级。", forceAuthority: "使用 -f / --force 绕过缓存所需的最低权限等级。" } };
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.", maxImageBytes: "Max screenshot size in bytes.", 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." } };
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 { createJimp } from "@jimp/core";
285
- import { defaultFormats, defaultPlugins } from "jimp";
286
- var Jimp = createJimp({
287
- formats: [...defaultFormats],
288
- plugins: defaultPlugins
289
- });
290
- async function blurImage(input, options) {
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 image = await Jimp.read(input);
293
- if (radius > 0) {
294
- if (options.mode === "gaussian") {
295
- const r = Math.max(1, Math.min(10, Math.round(radius / 20)));
296
- image.gaussian(r);
297
- } else {
298
- const r = Math.max(1, Math.min(100, radius));
299
- image.blur(r);
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 output = await blurImage(buffer, {
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, "image/png");
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
- maxImageBytes: Schema.natural().default(8 * 1024 * 1024),
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.maxImageBytes,
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.2.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
- "@jimp/core": "^1.6.0",
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
- | `maxImageBytes` | `number` | `8 * 1024 * 1024` | 单张截图大小上限 |
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` | 是否给每个客户端注册同名别名 |