argus-eye 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.
@@ -0,0 +1,19 @@
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
+ * 典型路径 ≤ 200ms:decode (~65ms) + 1 次 encode (~110ms) = ~175ms。
18
+ */
19
+ export declare function compressToBudget(input: Buffer, options: CompressOptions): Buffer;
package/lib/config.d.ts CHANGED
@@ -4,10 +4,16 @@ export interface ResolvedConfig {
4
4
  name: string;
5
5
  display?: number | string;
6
6
  format: 'png' | 'jpg';
7
+ /** 客户端预压缩目标体积(KB)。默认 500。0 = 不预压缩。 */
8
+ maxKB: number;
7
9
  reconnect: boolean;
8
10
  backoff: number;
9
11
  color: boolean;
10
12
  detectFullscreen: boolean;
13
+ /** 永不视为 busy 的应用名列表(不区分大小写、子串匹配)。空数组 = 用默认列表。 */
14
+ allowApps: string[];
15
+ /** 强制视为 busy 的应用名列表。优先级高于 allowApps。 */
16
+ busyApps: string[];
11
17
  }
12
18
  export interface CliFlags {
13
19
  help?: boolean;
@@ -8,24 +8,43 @@ export interface ActiveWindowInfo {
8
8
  width: number;
9
9
  height: number;
10
10
  };
11
- displayBounds?: {
11
+ contentBounds?: {
12
12
  x: number;
13
13
  y: number;
14
14
  width: number;
15
15
  height: number;
16
16
  };
17
17
  }
18
+ /**
19
+ * 默认的"允许截图"应用名(不区分大小写、子串匹配)。
20
+ * 这些是即使全屏也是普通工作 / 浏览场景,应当能被群友看到的应用。
21
+ */
22
+ export declare const DEFAULT_ALLOW_APPS: string[];
23
+ export interface FullscreenDetectorOptions {
24
+ enabled: boolean;
25
+ /** 不算 busy 的应用名(不区分大小写、子串匹配)。 */
26
+ allowApps?: string[];
27
+ /** 强制视为 busy 的应用名(不区分大小写、子串匹配)。优先级高于 allowApps。 */
28
+ busyApps?: string[];
29
+ }
30
+ export interface FullscreenCheckResult {
31
+ busy: boolean;
32
+ /** 命中的判定原因,方便排查 */
33
+ reason?: 'allow_app' | 'busy_app' | 'fullscreen_geometry';
34
+ }
18
35
  /**
19
36
  * 检测器:懒加载 `get-windows`(原生 npm 包,gyp 编译)。
20
37
  * 该模块在某些平台 / 环境(Wayland、CI 容器、缺乏权限的 macOS)上不可用,
21
38
  * 这种情况下返回 undefined,调用方按"无法判断"处理。
22
39
  */
23
40
  export declare class FullscreenDetector {
24
- private enabled;
41
+ private options;
25
42
  private mod?;
26
43
  private loading?;
27
44
  private warnedFailure;
28
- constructor(enabled: boolean);
45
+ private allowApps;
46
+ private busyApps;
47
+ constructor(options: FullscreenDetectorOptions);
29
48
  setEnabled(enabled: boolean): void;
30
49
  isEnabled(): boolean;
31
50
  /**
@@ -33,15 +52,32 @@ export declare class FullscreenDetector {
33
52
  */
34
53
  getActiveWindow(): Promise<ActiveWindowInfo | undefined>;
35
54
  /**
36
- * 判断 `win` 是否在 `display` 上覆盖整块屏(全屏游戏 / 全屏视频 / 最大化应用)。
55
+ * 综合判定当前窗口是否应当视为 busy。
56
+ *
57
+ * 优先级:
58
+ * 1. allowApps 命中 → 永不 busy(即使 F11 全屏 chrome)
59
+ * 2. busyApps 命中 → 永远 busy
60
+ * 3. 几何全屏:bounds 起点贴近 (0,0) AND `contentBounds == bounds`
61
+ * AND 覆盖整块显示器 → busy
62
+ */
63
+ check(win: ActiveWindowInfo, display: {
64
+ width?: number;
65
+ height?: number;
66
+ }): FullscreenCheckResult;
67
+ private matchAny;
68
+ /**
69
+ * 几何上是否是真正的全屏(区别于"最大化窗口")。
37
70
  *
38
- * 注意:`get-windows` 在 Windows 上返回的是 DPI 缩放后的逻辑像素,
39
- * `screenshot-desktop` 报告的是物理像素。所以比较的是缩放比例,
40
- * 该比例应该匹配某个常见的 DPI 缩放因子(100% / 125% / 150% / ...)。
71
+ * 关键观察(Windows 上):
72
+ * - 最大化的普通窗口:`bounds.x = -7, bounds.y = -7`(aero 边框 overscan),
73
+ * `contentBounds` `bounds` 小一圈(标题栏 / 边框被排除)
74
+ * - 真正的全屏(F11 / 全屏游戏):`bounds.x = 0, bounds.y = 0`,
75
+ * `contentBounds == bounds`,整体尺寸贴合整块显示器
41
76
  *
42
- * 这个检查会把"最大化的应用"也视作 fullscreen,这是有意为之 ——
43
- * 用户的意图是「看到某个独占应用就别曝光画面」。
77
+ * macOS / Linux 通常没有 contentBounds,退化为只看起点和尺寸。
44
78
  */
79
+ private isFullscreenGeometry;
80
+ /** 兼容旧名字。新代码用 `check`。 */
45
81
  isFullscreen(win: ActiveWindowInfo, display: {
46
82
  width?: number;
47
83
  height?: number;
package/lib/index.mjs CHANGED
@@ -71,10 +71,128 @@ async function capture(options = {}) {
71
71
  }
72
72
  __name(capture, "capture");
73
73
 
74
+ // src/compress.ts
75
+ import {
76
+ PhotonImage,
77
+ resize,
78
+ SamplingFilter
79
+ } from "@cf-wasm/photon/node";
80
+ function compressToBudget(input, options) {
81
+ const target = Math.max(8 * 1024, options.targetBytes);
82
+ if (input.length <= target) return input;
83
+ const initialQuality = options.initialQuality ?? 80;
84
+ const minQuality = options.minQuality ?? 40;
85
+ const img = PhotonImage.new_from_byteslice(new Uint8Array(input));
86
+ try {
87
+ const ratioByteWise = target / input.length;
88
+ const estQ = Math.max(
89
+ minQuality,
90
+ Math.min(initialQuality, Math.round(initialQuality * ratioByteWise))
91
+ );
92
+ let buf = Buffer.from(img.get_bytes_jpeg(estQ));
93
+ if (buf.length <= target) return buf;
94
+ const q2 = Math.max(
95
+ minQuality,
96
+ Math.round(estQ * (target / buf.length) * 0.95)
97
+ );
98
+ if (q2 < estQ) {
99
+ buf = Buffer.from(img.get_bytes_jpeg(q2));
100
+ if (buf.length <= target) return buf;
101
+ }
102
+ const w = img.get_width();
103
+ const h = img.get_height();
104
+ const scale = Math.sqrt(target / buf.length) * 0.9;
105
+ const newW = Math.max(64, Math.round(w * scale));
106
+ const newH = Math.max(64, Math.round(h * scale));
107
+ const small = resize(img, newW, newH, SamplingFilter.Triangle);
108
+ try {
109
+ return Buffer.from(small.get_bytes_jpeg(q2));
110
+ } finally {
111
+ small.free();
112
+ }
113
+ } finally {
114
+ img.free();
115
+ }
116
+ }
117
+ __name(compressToBudget, "compressToBudget");
118
+
74
119
  // src/fullscreen.ts
120
+ var DEFAULT_ALLOW_APPS = [
121
+ // browsers
122
+ "chrome",
123
+ "edge",
124
+ "firefox",
125
+ "safari",
126
+ "opera",
127
+ "brave",
128
+ "vivaldi",
129
+ "arc",
130
+ // IDE / editors
131
+ "code",
132
+ // VS Code
133
+ "cursor",
134
+ "visual studio",
135
+ "devenv",
136
+ "idea",
137
+ "webstorm",
138
+ "pycharm",
139
+ "goland",
140
+ "rider",
141
+ "clion",
142
+ "rustrover",
143
+ "phpstorm",
144
+ "datagrip",
145
+ "fleet",
146
+ "sublime",
147
+ "atom",
148
+ "notepad",
149
+ "typora",
150
+ "obsidian",
151
+ "notion",
152
+ "logseq",
153
+ // shells / terminals
154
+ "windowsterminal",
155
+ "powershell",
156
+ "pwsh",
157
+ "cmd",
158
+ "conhost",
159
+ "wezterm",
160
+ "iterm",
161
+ "alacritty",
162
+ "kitty",
163
+ "tabby",
164
+ "hyper",
165
+ // file managers
166
+ "explorer",
167
+ "finder",
168
+ // common IM / collab
169
+ "wechat",
170
+ "weixin",
171
+ "qq",
172
+ "tim",
173
+ "telegram",
174
+ "discord",
175
+ "slack",
176
+ "teams",
177
+ "zoom",
178
+ "feishu",
179
+ "lark",
180
+ "dingtalk",
181
+ // office
182
+ "word",
183
+ "excel",
184
+ "powerpoint",
185
+ "wps",
186
+ "acrobat",
187
+ "sumatra"
188
+ ];
75
189
  var FullscreenDetector = class {
76
- constructor(enabled) {
77
- this.enabled = enabled;
190
+ constructor(options) {
191
+ this.options = options;
192
+ this.allowApps = (options.allowApps ?? DEFAULT_ALLOW_APPS).map(
193
+ (s) => s.toLowerCase()
194
+ );
195
+ this.busyApps = (options.busyApps ?? []).map((s) => s.toLowerCase());
78
196
  }
79
197
  static {
80
198
  __name(this, "FullscreenDetector");
@@ -82,17 +200,19 @@ var FullscreenDetector = class {
82
200
  mod;
83
201
  loading;
84
202
  warnedFailure = false;
203
+ allowApps;
204
+ busyApps;
85
205
  setEnabled(enabled) {
86
- this.enabled = enabled;
206
+ this.options.enabled = enabled;
87
207
  }
88
208
  isEnabled() {
89
- return this.enabled;
209
+ return this.options.enabled;
90
210
  }
91
211
  /**
92
212
  * 获取当前活动窗口。无法获取时返回 undefined。
93
213
  */
94
214
  async getActiveWindow() {
95
- if (!this.enabled) return void 0;
215
+ if (!this.options.enabled) return void 0;
96
216
  const mod = await this.load();
97
217
  if (!mod) return void 0;
98
218
  try {
@@ -105,7 +225,8 @@ var FullscreenDetector = class {
105
225
  return {
106
226
  app: win.owner?.name ?? "",
107
227
  title: win.title ?? "",
108
- bounds: win.bounds
228
+ bounds: win.bounds,
229
+ contentBounds: win.contentBounds
109
230
  };
110
231
  } catch (err) {
111
232
  const message = err instanceof Error ? err.message : String(err);
@@ -114,28 +235,64 @@ var FullscreenDetector = class {
114
235
  }
115
236
  }
116
237
  /**
117
- * 判断 `win` 是否在 `display` 上覆盖整块屏(全屏游戏 / 全屏视频 / 最大化应用)。
238
+ * 综合判定当前窗口是否应当视为 busy。
239
+ *
240
+ * 优先级:
241
+ * 1. allowApps 命中 → 永不 busy(即使 F11 全屏 chrome)
242
+ * 2. busyApps 命中 → 永远 busy
243
+ * 3. 几何全屏:bounds 起点贴近 (0,0) AND `contentBounds == bounds`
244
+ * AND 覆盖整块显示器 → busy
245
+ */
246
+ check(win, display) {
247
+ const app = win.app.toLowerCase();
248
+ const title = win.title.toLowerCase();
249
+ if (this.matchAny(this.busyApps, app, title)) {
250
+ return { busy: true, reason: "busy_app" };
251
+ }
252
+ if (this.matchAny(this.allowApps, app, title)) {
253
+ return { busy: false, reason: "allow_app" };
254
+ }
255
+ if (this.isFullscreenGeometry(win, display)) {
256
+ return { busy: true, reason: "fullscreen_geometry" };
257
+ }
258
+ return { busy: false };
259
+ }
260
+ matchAny(patterns, ...haystack) {
261
+ if (patterns.length === 0) return false;
262
+ return patterns.some((p) => haystack.some((h) => h.includes(p)));
263
+ }
264
+ /**
265
+ * 几何上是否是真正的全屏(区别于"最大化窗口")。
118
266
  *
119
- * 注意:`get-windows` 在 Windows 上返回的是 DPI 缩放后的逻辑像素,
120
- * `screenshot-desktop` 报告的是物理像素。所以比较的是缩放比例,
121
- * 该比例应该匹配某个常见的 DPI 缩放因子(100% / 125% / 150% / ...)。
267
+ * 关键观察(Windows 上):
268
+ * - 最大化的普通窗口:`bounds.x = -7, bounds.y = -7`(aero 边框 overscan),
269
+ * `contentBounds` `bounds` 小一圈(标题栏 / 边框被排除)
270
+ * - 真正的全屏(F11 / 全屏游戏):`bounds.x = 0, bounds.y = 0`,
271
+ * `contentBounds == bounds`,整体尺寸贴合整块显示器
122
272
  *
123
- * 这个检查会把"最大化的应用"也视作 fullscreen,这是有意为之 ——
124
- * 用户的意图是「看到某个独占应用就别曝光画面」。
273
+ * macOS / Linux 通常没有 contentBounds,退化为只看起点和尺寸。
125
274
  */
126
- isFullscreen(win, display) {
275
+ isFullscreenGeometry(win, display) {
127
276
  if (!display.width || !display.height) return false;
128
- const { bounds } = win;
277
+ const { bounds, contentBounds } = win;
129
278
  if (bounds.width <= 0 || bounds.height <= 0) return false;
279
+ if (Math.abs(bounds.x) > 1 || Math.abs(bounds.y) > 1) return false;
280
+ if (contentBounds) {
281
+ const sameOrigin = contentBounds.x === bounds.x && contentBounds.y === bounds.y;
282
+ const sameSize = contentBounds.width === bounds.width && contentBounds.height === bounds.height;
283
+ if (!sameOrigin || !sameSize) return false;
284
+ }
130
285
  const widthRatio = bounds.width / display.width;
131
286
  const heightRatio = bounds.height / display.height;
132
287
  const ratioMismatch = Math.abs(widthRatio - heightRatio) / Math.max(widthRatio, heightRatio);
133
288
  if (ratioMismatch > 0.02) return false;
134
289
  const scale = (widthRatio + heightRatio) / 2;
135
290
  const dpiScales = [1, 0.8, 2 / 3, 4 / 7, 0.5, 4 / 9, 0.4];
136
- const matchesDpi = dpiScales.some((s) => Math.abs(scale - s) < 0.01);
137
- if (!matchesDpi) return false;
138
- return Math.abs(bounds.x) <= 8 && Math.abs(bounds.y) <= 8;
291
+ return dpiScales.some((s) => Math.abs(scale - s) < 0.01);
292
+ }
293
+ /** 兼容旧名字。新代码用 `check`。 */
294
+ isFullscreen(win, display) {
295
+ return this.check(win, display).busy;
139
296
  }
140
297
  load() {
141
298
  if (this.mod) return Promise.resolve(this.mod);
@@ -198,7 +355,13 @@ var ArgusClient = class {
198
355
  this.config = config;
199
356
  this.version = version;
200
357
  this.hooks = hooks;
201
- this.fullscreen = new FullscreenDetector(config.detectFullscreen);
358
+ this.fullscreen = new FullscreenDetector({
359
+ enabled: config.detectFullscreen,
360
+ // 用户传的 allowApps 与默认列表合并(用户的不区分大小写补丁是为了
361
+ // 不会因为忘记加常见应用而误伤)
362
+ allowApps: [...DEFAULT_ALLOW_APPS, ...config.allowApps],
363
+ busyApps: config.busyApps
364
+ });
202
365
  }
203
366
  static {
204
367
  __name(this, "ArgusClient");
@@ -375,18 +538,21 @@ var ArgusClient = class {
375
538
  if (this.fullscreen.isEnabled()) {
376
539
  const win = await this.fullscreen.getActiveWindow();
377
540
  const display = publicIndex !== void 0 ? this.displays[publicIndex] : void 0;
378
- if (win && display && this.fullscreen.isFullscreen(win, display)) {
379
- logger.info(
380
- `peek #${frame.id} → busy: ${win.app || win.title || "fullscreen"}`
381
- );
382
- this.send({
383
- type: "peek_busy",
384
- id: frame.id,
385
- app: win.app,
386
- title: win.title,
387
- reason: "fullscreen"
388
- });
389
- return;
541
+ if (win && display) {
542
+ const result = this.fullscreen.check(win, display);
543
+ if (result.busy) {
544
+ logger.info(
545
+ `peek #${frame.id} → busy (${result.reason}): ${win.app || win.title || "fullscreen"}`
546
+ );
547
+ this.send({
548
+ type: "peek_busy",
549
+ id: frame.id,
550
+ app: win.app,
551
+ title: win.title,
552
+ reason: result.reason ?? "fullscreen"
553
+ });
554
+ return;
555
+ }
390
556
  }
391
557
  }
392
558
  const start2 = Date.now();
@@ -395,19 +561,33 @@ var ArgusClient = class {
395
561
  display: target,
396
562
  format: this.config.format
397
563
  });
398
- const payload = encryptBuffer(result.buffer, this.config.token);
564
+ const compressStart = Date.now();
565
+ let final = result.buffer;
566
+ let mime = result.mime;
567
+ const rawSize = result.buffer.length;
568
+ const budgetBytes = this.config.maxKB * 1024;
569
+ if (budgetBytes > 0 && rawSize > budgetBytes) {
570
+ final = compressToBudget(result.buffer, {
571
+ targetBytes: budgetBytes
572
+ });
573
+ mime = "image/jpeg";
574
+ }
575
+ const compressMs = Date.now() - compressStart;
576
+ const payload = encryptBuffer(final, this.config.token);
399
577
  this.send({
400
578
  type: "peek_result",
401
579
  id: frame.id,
402
580
  image: payload,
403
581
  enc: ENC_ALGO,
404
- mime: result.mime,
582
+ mime,
405
583
  display: frame.display
406
584
  });
407
585
  const elapsed = Date.now() - start2;
408
- const kb = (result.buffer.length / 1024).toFixed(1);
586
+ const rawKb = (rawSize / 1024).toFixed(1);
587
+ const finalKb = (final.length / 1024).toFixed(1);
588
+ const same = final === result.buffer;
409
589
  logger.info(
410
- `peek #${frame.id} → display ${frame.display ?? "default"} (${kb} KB ${this.config.format} encrypted, ${elapsed}ms)`
590
+ `peek #${frame.id} → display ${frame.display ?? "default"} (${same ? finalKb : `${rawKb}→${finalKb}`} KB ${same ? mime.replace("image/", "") : "jpg"} encrypted, total=${elapsed}ms compress=${compressMs}ms)`
411
591
  );
412
592
  } catch (err) {
413
593
  const message = err instanceof Error ? err.message : String(err);
@@ -496,13 +676,18 @@ var HELP_TEXT = `argus-eye [options]
496
676
  -n, --name <name> 上报给服务端的名字(默认: os.hostname())
497
677
  -d, --display <id> 默认截哪块屏(数字 id,缺省=主屏)
498
678
  --list-displays 列出本机显示器后退出
499
- --format <png|jpg> 传输格式(默认 jpg,省流量)
679
+ --format <png|jpg> 原始捕获格式(默认 jpg)。
680
+ --max-kb <n> 预压缩目标体积上限(KB,默认 600)。
681
+ 客户端会用降质量 + 缩小图片把 buffer 压到这个上限以内
682
+ 再加密发出,省带宽。0 = 关闭预压缩,按原始截图发。
500
683
  --no-reconnect 禁用断线重连
501
684
  --backoff <ms> 重连最大间隔(默认 30000)
502
685
  --no-detect-fullscreen
503
- 关闭"全屏即拒拍"行为(默认开启)。打开时若检测到
504
- 当前焦点窗口铺满整块显示器(如全屏游戏 / 视频),
505
- 则向服务端返回 peek_busy 而不是真截图。
686
+ 关闭"全屏即拒拍"行为(默认开启)。
687
+ --allow-app <name> 不视为 busy 的应用关键词(可重复)。
688
+ 会与默认白名单(chrome / edge / vscode / 终端 / IM 等)合并。
689
+ --busy-app <name> 强制视为 busy 的应用关键词(可重复),
690
+ 优先级高于 allow-app。例如 --busy-app valorant --busy-app csgo
506
691
  --config <path> JSON 配置文件
507
692
  --no-color 关闭着色输出
508
693
  -h, --help 显示帮助
@@ -522,7 +707,8 @@ function parseArgs(argv) {
522
707
  version: ["v"]
523
708
  },
524
709
  string: ["server", "token", "name", "config", "format", "display"],
525
- number: ["backoff"],
710
+ array: ["allowApp", "busyApp"],
711
+ number: ["backoff", "maxKb"],
526
712
  boolean: [
527
713
  "help",
528
714
  "version",
@@ -555,10 +741,13 @@ function parseArgs(argv) {
555
741
  name: asString(parsed.name) ?? env.ARGUS_NAME ?? asString(fileConfig.name) ?? os.hostname(),
556
742
  display: asDisplay(parsed.display) ?? asDisplay(fileConfig.display) ?? void 0,
557
743
  format: asString(parsed.format) ?? asString(fileConfig.format) ?? "jpg",
744
+ maxKB: asNumber(parsed.maxKb) ?? asNumber(fileConfig.maxKB) ?? asNumber(fileConfig.maxKb) ?? 600,
558
745
  reconnect: asBoolean(parsed.reconnect) ?? asBoolean(fileConfig.reconnect) ?? true,
559
746
  backoff: asNumber(parsed.backoff) ?? asNumber(fileConfig.backoff) ?? 3e4,
560
747
  color: asBoolean(parsed.color) ?? true,
561
- detectFullscreen: asBoolean(parsed.detectFullscreen) ?? asBoolean(fileConfig.detectFullscreen) ?? true
748
+ detectFullscreen: asBoolean(parsed.detectFullscreen) ?? asBoolean(fileConfig.detectFullscreen) ?? true,
749
+ allowApps: asStringArray(parsed.allowApp) ?? asStringArray(fileConfig.allowApps) ?? [],
750
+ busyApps: asStringArray(parsed.busyApp) ?? asStringArray(fileConfig.busyApps) ?? []
562
751
  };
563
752
  if (parsed.listDisplays) {
564
753
  return {
@@ -569,10 +758,13 @@ function parseArgs(argv) {
569
758
  name: merged.name ?? os.hostname(),
570
759
  display: merged.display,
571
760
  format: merged.format ?? "jpg",
761
+ maxKB: merged.maxKB ?? 600,
572
762
  reconnect: merged.reconnect ?? true,
573
763
  backoff: merged.backoff ?? 3e4,
574
764
  color: merged.color ?? true,
575
- detectFullscreen: merged.detectFullscreen ?? true
765
+ detectFullscreen: merged.detectFullscreen ?? true,
766
+ allowApps: merged.allowApps ?? [],
767
+ busyApps: merged.busyApps ?? []
576
768
  }
577
769
  };
578
770
  }
@@ -592,10 +784,13 @@ function parseArgs(argv) {
592
784
  name: merged.name ?? os.hostname(),
593
785
  display: merged.display,
594
786
  format: merged.format,
787
+ maxKB: merged.maxKB ?? 600,
595
788
  reconnect: merged.reconnect ?? true,
596
789
  backoff: merged.backoff ?? 3e4,
597
790
  color: merged.color ?? true,
598
- detectFullscreen: merged.detectFullscreen ?? true
791
+ detectFullscreen: merged.detectFullscreen ?? true,
792
+ allowApps: merged.allowApps ?? [],
793
+ busyApps: merged.busyApps ?? []
599
794
  }
600
795
  };
601
796
  }
@@ -656,6 +851,18 @@ function asDisplay(v) {
656
851
  return void 0;
657
852
  }
658
853
  __name(asDisplay, "asDisplay");
854
+ function asStringArray(v) {
855
+ if (Array.isArray(v)) {
856
+ const out = v.map((x) => typeof x === "string" ? x.trim() : "").filter((x) => x.length > 0);
857
+ return out.length > 0 ? out : void 0;
858
+ }
859
+ if (typeof v === "string" && v.length > 0) {
860
+ const out = v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
861
+ return out.length > 0 ? out : void 0;
862
+ }
863
+ return void 0;
864
+ }
865
+ __name(asStringArray, "asStringArray");
659
866
 
660
867
  // src/index.ts
661
868
  var require2 = createRequire(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argus-eye",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Argus eye —— koishi-plugin-argus 的截图客户端 CLI。",
6
6
  "author": "dingyi222666 <dingyi222666@foxmail.com>",
@@ -22,6 +22,7 @@
22
22
  "lint-fix": "yarn eslint src --ext=ts --fix"
23
23
  },
24
24
  "dependencies": {
25
+ "@cf-wasm/photon": "^0.3.5",
25
26
  "kleur": "^4.1.5",
26
27
  "screenshot-desktop": "^1.15.0",
27
28
  "ws": "^8.18.0",
package/readme.md CHANGED
@@ -35,10 +35,43 @@ argus-eye [options]
35
35
 
36
36
  ## 全屏检测
37
37
 
38
- CLI 默认开启全屏检测:当截屏时检测到当前焦点窗口铺满整块显示器(典型如全屏游戏 / 全屏视频),
38
+ CLI 默认开启全屏检测:当截屏时检测到当前焦点窗口是**真正的全屏**(典型如全屏游戏),
39
39
  就向服务端返回一段「客户端正忙:xxx」的提示而不是真截图。
40
40
  群友看到的是程序名("League of Legends" / "Bilibili" 等),看不到画面。
41
- 如果你想关掉这个行为,加 `--no-detect-fullscreen`。
41
+
42
+ 判定规则(按优先级从高到低):
43
+
44
+ 1. `--busy-app` 显式黑名单命中 → 视为忙
45
+ 2. `--allow-app` 命中(默认包含 chrome、edge、firefox、vscode、cursor、idea、
46
+ 终端、explorer、QQ、微信、Discord、Office 等常见应用)→ 永远不忙
47
+ 3. 几何上是真全屏:起点贴 `(0, 0)`、`bounds == contentBounds`、覆盖整块显示器
48
+ → 视为忙
49
+
50
+ 第二条让最大化的 Chrome / VSCode 这种正常工作场景照常出图;
51
+ 第三条用 `bounds == contentBounds` 区分"真全屏"和"最大化"
52
+ (最大化窗口在 Windows 上 `bounds.x = -7` 且 `contentBounds` 比 `bounds` 小一圈)。
53
+
54
+ 例:
55
+
56
+ ```bash
57
+ # 把 valorant / csgo / lol 直接列到黑名单
58
+ argus-eye -s ... -t ... --busy-app valorant --busy-app csgo --busy-app "league of legends"
59
+
60
+ # 把"魔兽世界"也加进白名单(如果你想被群友看到)
61
+ argus-eye -s ... -t ... --allow-app wow
62
+
63
+ # 关掉这个行为
64
+ argus-eye -s ... -t ... --no-detect-fullscreen
65
+ ```
66
+
67
+ 也可以写在 `~/.argus-eye.json`:
68
+
69
+ ```json
70
+ {
71
+ "busyApps": ["valorant", "csgo", "league of legends"],
72
+ "allowApps": ["mygame"]
73
+ }
74
+ ```
42
75
 
43
76
  底层使用 [`get-windows`](https://www.npmjs.com/package/get-windows)(optional dependency)。
44
77
  该包是原生 napi 模块,部分平台(Linux Wayland)不支持,加载失败时会自动降级为永远「不忙」。