argus-eye 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.d.ts CHANGED
@@ -8,6 +8,10 @@ export interface ResolvedConfig {
8
8
  backoff: number;
9
9
  color: boolean;
10
10
  detectFullscreen: boolean;
11
+ /** 永不视为 busy 的应用名列表(不区分大小写、子串匹配)。空数组 = 用默认列表。 */
12
+ allowApps: string[];
13
+ /** 强制视为 busy 的应用名列表。优先级高于 allowApps。 */
14
+ busyApps: string[];
11
15
  }
12
16
  export interface CliFlags {
13
17
  help?: boolean;
@@ -0,0 +1,5 @@
1
+ /** AES-256-GCM 加密。返回 base64(iv | tag | ciphertext)。 */
2
+ export declare function encryptBuffer(plain: Buffer, token: string): string;
3
+ export declare function decryptBuffer(payloadBase64: string, token: string): Buffer;
4
+ export declare const ENC_ALGO: "aes-256-gcm";
5
+ export type EncAlgo = typeof ENC_ALGO;
@@ -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
@@ -72,9 +72,82 @@ async function capture(options = {}) {
72
72
  __name(capture, "capture");
73
73
 
74
74
  // src/fullscreen.ts
75
+ var DEFAULT_ALLOW_APPS = [
76
+ // browsers
77
+ "chrome",
78
+ "edge",
79
+ "firefox",
80
+ "safari",
81
+ "opera",
82
+ "brave",
83
+ "vivaldi",
84
+ "arc",
85
+ // IDE / editors
86
+ "code",
87
+ // VS Code
88
+ "cursor",
89
+ "visual studio",
90
+ "devenv",
91
+ "idea",
92
+ "webstorm",
93
+ "pycharm",
94
+ "goland",
95
+ "rider",
96
+ "clion",
97
+ "rustrover",
98
+ "phpstorm",
99
+ "datagrip",
100
+ "fleet",
101
+ "sublime",
102
+ "atom",
103
+ "notepad",
104
+ "typora",
105
+ "obsidian",
106
+ "notion",
107
+ "logseq",
108
+ // shells / terminals
109
+ "windowsterminal",
110
+ "powershell",
111
+ "pwsh",
112
+ "cmd",
113
+ "conhost",
114
+ "wezterm",
115
+ "iterm",
116
+ "alacritty",
117
+ "kitty",
118
+ "tabby",
119
+ "hyper",
120
+ // file managers
121
+ "explorer",
122
+ "finder",
123
+ // common IM / collab
124
+ "wechat",
125
+ "weixin",
126
+ "qq",
127
+ "tim",
128
+ "telegram",
129
+ "discord",
130
+ "slack",
131
+ "teams",
132
+ "zoom",
133
+ "feishu",
134
+ "lark",
135
+ "dingtalk",
136
+ // office
137
+ "word",
138
+ "excel",
139
+ "powerpoint",
140
+ "wps",
141
+ "acrobat",
142
+ "sumatra"
143
+ ];
75
144
  var FullscreenDetector = class {
76
- constructor(enabled) {
77
- this.enabled = enabled;
145
+ constructor(options) {
146
+ this.options = options;
147
+ this.allowApps = (options.allowApps ?? DEFAULT_ALLOW_APPS).map(
148
+ (s) => s.toLowerCase()
149
+ );
150
+ this.busyApps = (options.busyApps ?? []).map((s) => s.toLowerCase());
78
151
  }
79
152
  static {
80
153
  __name(this, "FullscreenDetector");
@@ -82,17 +155,19 @@ var FullscreenDetector = class {
82
155
  mod;
83
156
  loading;
84
157
  warnedFailure = false;
158
+ allowApps;
159
+ busyApps;
85
160
  setEnabled(enabled) {
86
- this.enabled = enabled;
161
+ this.options.enabled = enabled;
87
162
  }
88
163
  isEnabled() {
89
- return this.enabled;
164
+ return this.options.enabled;
90
165
  }
91
166
  /**
92
167
  * 获取当前活动窗口。无法获取时返回 undefined。
93
168
  */
94
169
  async getActiveWindow() {
95
- if (!this.enabled) return void 0;
170
+ if (!this.options.enabled) return void 0;
96
171
  const mod = await this.load();
97
172
  if (!mod) return void 0;
98
173
  try {
@@ -105,7 +180,8 @@ var FullscreenDetector = class {
105
180
  return {
106
181
  app: win.owner?.name ?? "",
107
182
  title: win.title ?? "",
108
- bounds: win.bounds
183
+ bounds: win.bounds,
184
+ contentBounds: win.contentBounds
109
185
  };
110
186
  } catch (err) {
111
187
  const message = err instanceof Error ? err.message : String(err);
@@ -114,28 +190,64 @@ var FullscreenDetector = class {
114
190
  }
115
191
  }
116
192
  /**
117
- * 判断 `win` 是否在 `display` 上覆盖整块屏(全屏游戏 / 全屏视频 / 最大化应用)。
193
+ * 综合判定当前窗口是否应当视为 busy。
118
194
  *
119
- * 注意:`get-windows` 在 Windows 上返回的是 DPI 缩放后的逻辑像素,
120
- * `screenshot-desktop` 报告的是物理像素。所以比较的是缩放比例,
121
- * 该比例应该匹配某个常见的 DPI 缩放因子(100% / 125% / 150% / ...)。
195
+ * 优先级:
196
+ * 1. allowApps 命中 → 永不 busy(即使 F11 全屏 chrome)
197
+ * 2. busyApps 命中 永远 busy
198
+ * 3. 几何全屏:bounds 起点贴近 (0,0) AND `contentBounds == bounds`
199
+ * AND 覆盖整块显示器 → busy
200
+ */
201
+ check(win, display) {
202
+ const app = win.app.toLowerCase();
203
+ const title = win.title.toLowerCase();
204
+ if (this.matchAny(this.busyApps, app, title)) {
205
+ return { busy: true, reason: "busy_app" };
206
+ }
207
+ if (this.matchAny(this.allowApps, app, title)) {
208
+ return { busy: false, reason: "allow_app" };
209
+ }
210
+ if (this.isFullscreenGeometry(win, display)) {
211
+ return { busy: true, reason: "fullscreen_geometry" };
212
+ }
213
+ return { busy: false };
214
+ }
215
+ matchAny(patterns, ...haystack) {
216
+ if (patterns.length === 0) return false;
217
+ return patterns.some((p) => haystack.some((h) => h.includes(p)));
218
+ }
219
+ /**
220
+ * 几何上是否是真正的全屏(区别于"最大化窗口")。
122
221
  *
123
- * 这个检查会把"最大化的应用"也视作 fullscreen,这是有意为之 ——
124
- * 用户的意图是「看到某个独占应用就别曝光画面」。
222
+ * 关键观察(Windows 上):
223
+ * - 最大化的普通窗口:`bounds.x = -7, bounds.y = -7`(aero 边框 overscan),
224
+ * `contentBounds` 比 `bounds` 小一圈(标题栏 / 边框被排除)
225
+ * - 真正的全屏(F11 / 全屏游戏):`bounds.x = 0, bounds.y = 0`,
226
+ * `contentBounds == bounds`,整体尺寸贴合整块显示器
227
+ *
228
+ * macOS / Linux 通常没有 contentBounds,退化为只看起点和尺寸。
125
229
  */
126
- isFullscreen(win, display) {
230
+ isFullscreenGeometry(win, display) {
127
231
  if (!display.width || !display.height) return false;
128
- const { bounds } = win;
232
+ const { bounds, contentBounds } = win;
129
233
  if (bounds.width <= 0 || bounds.height <= 0) return false;
234
+ if (Math.abs(bounds.x) > 1 || Math.abs(bounds.y) > 1) return false;
235
+ if (contentBounds) {
236
+ const sameOrigin = contentBounds.x === bounds.x && contentBounds.y === bounds.y;
237
+ const sameSize = contentBounds.width === bounds.width && contentBounds.height === bounds.height;
238
+ if (!sameOrigin || !sameSize) return false;
239
+ }
130
240
  const widthRatio = bounds.width / display.width;
131
241
  const heightRatio = bounds.height / display.height;
132
242
  const ratioMismatch = Math.abs(widthRatio - heightRatio) / Math.max(widthRatio, heightRatio);
133
243
  if (ratioMismatch > 0.02) return false;
134
244
  const scale = (widthRatio + heightRatio) / 2;
135
245
  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;
246
+ return dpiScales.some((s) => Math.abs(scale - s) < 0.01);
247
+ }
248
+ /** 兼容旧名字。新代码用 `check`。 */
249
+ isFullscreen(win, display) {
250
+ return this.check(win, display).busy;
139
251
  }
140
252
  load() {
141
253
  if (this.mod) return Promise.resolve(this.mod);
@@ -160,6 +272,37 @@ var FullscreenDetector = class {
160
272
  }
161
273
  };
162
274
 
275
+ // src/crypto.ts
276
+ import {
277
+ createCipheriv,
278
+ createDecipheriv,
279
+ randomBytes,
280
+ scryptSync
281
+ } from "node:crypto";
282
+ var ALGO = "aes-256-gcm";
283
+ var IV_LEN = 12;
284
+ var KEY_LEN = 32;
285
+ var SALT = Buffer.from("argus.peek.v1", "utf8");
286
+ var keyCache = /* @__PURE__ */ new Map();
287
+ function deriveKey(token) {
288
+ let key = keyCache.get(token);
289
+ if (key) return key;
290
+ key = scryptSync(token, SALT, KEY_LEN);
291
+ keyCache.set(token, key);
292
+ return key;
293
+ }
294
+ __name(deriveKey, "deriveKey");
295
+ function encryptBuffer(plain, token) {
296
+ const key = deriveKey(token);
297
+ const iv = randomBytes(IV_LEN);
298
+ const cipher = createCipheriv(ALGO, key, iv);
299
+ const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
300
+ const tag2 = cipher.getAuthTag();
301
+ return Buffer.concat([iv, tag2, ct]).toString("base64");
302
+ }
303
+ __name(encryptBuffer, "encryptBuffer");
304
+ var ENC_ALGO = "aes-256-gcm";
305
+
163
306
  // src/client.ts
164
307
  var HEARTBEAT_TIMEOUT_MS = 9e4;
165
308
  var ArgusClient = class {
@@ -167,7 +310,13 @@ var ArgusClient = class {
167
310
  this.config = config;
168
311
  this.version = version;
169
312
  this.hooks = hooks;
170
- this.fullscreen = new FullscreenDetector(config.detectFullscreen);
313
+ this.fullscreen = new FullscreenDetector({
314
+ enabled: config.detectFullscreen,
315
+ // 用户传的 allowApps 与默认列表合并(用户的不区分大小写补丁是为了
316
+ // 不会因为忘记加常见应用而误伤)
317
+ allowApps: [...DEFAULT_ALLOW_APPS, ...config.allowApps],
318
+ busyApps: config.busyApps
319
+ });
171
320
  }
172
321
  static {
173
322
  __name(this, "ArgusClient");
@@ -344,18 +493,21 @@ var ArgusClient = class {
344
493
  if (this.fullscreen.isEnabled()) {
345
494
  const win = await this.fullscreen.getActiveWindow();
346
495
  const display = publicIndex !== void 0 ? this.displays[publicIndex] : void 0;
347
- if (win && display && this.fullscreen.isFullscreen(win, display)) {
348
- logger.info(
349
- `peek #${frame.id} → busy: ${win.app || win.title || "fullscreen"}`
350
- );
351
- this.send({
352
- type: "peek_busy",
353
- id: frame.id,
354
- app: win.app,
355
- title: win.title,
356
- reason: "fullscreen"
357
- });
358
- return;
496
+ if (win && display) {
497
+ const result = this.fullscreen.check(win, display);
498
+ if (result.busy) {
499
+ logger.info(
500
+ `peek #${frame.id} → busy (${result.reason}): ${win.app || win.title || "fullscreen"}`
501
+ );
502
+ this.send({
503
+ type: "peek_busy",
504
+ id: frame.id,
505
+ app: win.app,
506
+ title: win.title,
507
+ reason: result.reason ?? "fullscreen"
508
+ });
509
+ return;
510
+ }
359
511
  }
360
512
  }
361
513
  const start2 = Date.now();
@@ -364,18 +516,19 @@ var ArgusClient = class {
364
516
  display: target,
365
517
  format: this.config.format
366
518
  });
367
- const base64 = result.buffer.toString("base64");
519
+ const payload = encryptBuffer(result.buffer, this.config.token);
368
520
  this.send({
369
521
  type: "peek_result",
370
522
  id: frame.id,
371
- image: base64,
523
+ image: payload,
524
+ enc: ENC_ALGO,
372
525
  mime: result.mime,
373
526
  display: frame.display
374
527
  });
375
528
  const elapsed = Date.now() - start2;
376
529
  const kb = (result.buffer.length / 1024).toFixed(1);
377
530
  logger.info(
378
- `peek #${frame.id} → display ${frame.display ?? "default"} (${kb} KB ${this.config.format}, ${elapsed}ms)`
531
+ `peek #${frame.id} → display ${frame.display ?? "default"} (${kb} KB ${this.config.format} encrypted, ${elapsed}ms)`
379
532
  );
380
533
  } catch (err) {
381
534
  const message = err instanceof Error ? err.message : String(err);
@@ -468,9 +621,11 @@ var HELP_TEXT = `argus-eye [options]
468
621
  --no-reconnect 禁用断线重连
469
622
  --backoff <ms> 重连最大间隔(默认 30000)
470
623
  --no-detect-fullscreen
471
- 关闭"全屏即拒拍"行为(默认开启)。打开时若检测到
472
- 当前焦点窗口铺满整块显示器(如全屏游戏 / 视频),
473
- 则向服务端返回 peek_busy 而不是真截图。
624
+ 关闭"全屏即拒拍"行为(默认开启)。
625
+ --allow-app <name> 不视为 busy 的应用关键词(可重复)。
626
+ 会与默认白名单(chrome / edge / vscode / 终端 / IM 等)合并。
627
+ --busy-app <name> 强制视为 busy 的应用关键词(可重复),
628
+ 优先级高于 allow-app。例如 --busy-app valorant --busy-app csgo
474
629
  --config <path> JSON 配置文件
475
630
  --no-color 关闭着色输出
476
631
  -h, --help 显示帮助
@@ -490,6 +645,7 @@ function parseArgs(argv) {
490
645
  version: ["v"]
491
646
  },
492
647
  string: ["server", "token", "name", "config", "format", "display"],
648
+ array: ["allowApp", "busyApp"],
493
649
  number: ["backoff"],
494
650
  boolean: [
495
651
  "help",
@@ -526,7 +682,9 @@ function parseArgs(argv) {
526
682
  reconnect: asBoolean(parsed.reconnect) ?? asBoolean(fileConfig.reconnect) ?? true,
527
683
  backoff: asNumber(parsed.backoff) ?? asNumber(fileConfig.backoff) ?? 3e4,
528
684
  color: asBoolean(parsed.color) ?? true,
529
- detectFullscreen: asBoolean(parsed.detectFullscreen) ?? asBoolean(fileConfig.detectFullscreen) ?? true
685
+ detectFullscreen: asBoolean(parsed.detectFullscreen) ?? asBoolean(fileConfig.detectFullscreen) ?? true,
686
+ allowApps: asStringArray(parsed.allowApp) ?? asStringArray(fileConfig.allowApps) ?? [],
687
+ busyApps: asStringArray(parsed.busyApp) ?? asStringArray(fileConfig.busyApps) ?? []
530
688
  };
531
689
  if (parsed.listDisplays) {
532
690
  return {
@@ -540,7 +698,9 @@ function parseArgs(argv) {
540
698
  reconnect: merged.reconnect ?? true,
541
699
  backoff: merged.backoff ?? 3e4,
542
700
  color: merged.color ?? true,
543
- detectFullscreen: merged.detectFullscreen ?? true
701
+ detectFullscreen: merged.detectFullscreen ?? true,
702
+ allowApps: merged.allowApps ?? [],
703
+ busyApps: merged.busyApps ?? []
544
704
  }
545
705
  };
546
706
  }
@@ -563,7 +723,9 @@ function parseArgs(argv) {
563
723
  reconnect: merged.reconnect ?? true,
564
724
  backoff: merged.backoff ?? 3e4,
565
725
  color: merged.color ?? true,
566
- detectFullscreen: merged.detectFullscreen ?? true
726
+ detectFullscreen: merged.detectFullscreen ?? true,
727
+ allowApps: merged.allowApps ?? [],
728
+ busyApps: merged.busyApps ?? []
567
729
  }
568
730
  };
569
731
  }
@@ -624,6 +786,18 @@ function asDisplay(v) {
624
786
  return void 0;
625
787
  }
626
788
  __name(asDisplay, "asDisplay");
789
+ function asStringArray(v) {
790
+ if (Array.isArray(v)) {
791
+ const out = v.map((x) => typeof x === "string" ? x.trim() : "").filter((x) => x.length > 0);
792
+ return out.length > 0 ? out : void 0;
793
+ }
794
+ if (typeof v === "string" && v.length > 0) {
795
+ const out = v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
796
+ return out.length > 0 ? out : void 0;
797
+ }
798
+ return void 0;
799
+ }
800
+ __name(asStringArray, "asStringArray");
627
801
 
628
802
  // src/index.ts
629
803
  var require2 = createRequire(import.meta.url);
package/lib/protocol.d.ts CHANGED
@@ -27,6 +27,7 @@ export interface PeekResultFrame {
27
27
  type: 'peek_result';
28
28
  id: string;
29
29
  image: string;
30
+ enc?: 'aes-256-gcm' | 'none';
30
31
  mime?: string;
31
32
  width?: number;
32
33
  height?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argus-eye",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Argus eye —— koishi-plugin-argus 的截图客户端 CLI。",
6
6
  "author": "dingyi222666 <dingyi222666@foxmail.com>",
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)不支持,加载失败时会自动降级为永远「不忙」。
@@ -60,6 +93,12 @@ CLI 默认开启全屏检测:当截屏时检测到当前焦点窗口铺满整
60
93
 
61
94
  环境变量:`ARGUS_SERVER` / `ARGUS_TOKEN` / `ARGUS_NAME`。
62
95
 
96
+ ## 加密
97
+
98
+ 截图 buffer 走 WebSocket 之前会用 `AES-256-GCM` 加密,key 由 `--token`
99
+ 经 scrypt 派生,IV 每帧随机。同一 token 的插件端会解出来;token 不对的
100
+ 中间人 / 抓包工具拿到的都是密文。
101
+
63
102
  ## 平台说明
64
103
 
65
104
  底层使用 [`screenshot-desktop`](https://www.npmjs.com/package/screenshot-desktop):