@greatlhd/ailo-desktop 1.0.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.
Files changed (73) hide show
  1. package/copy-static.mjs +11 -0
  2. package/dist/browser_control.js +767 -0
  3. package/dist/browser_snapshot.js +174 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/code_executor.js +95 -0
  6. package/dist/config_server.js +658 -0
  7. package/dist/connection_util.js +14 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/desktop_state_store.js +57 -0
  10. package/dist/desktop_types.js +1 -0
  11. package/dist/desktop_verifier.js +40 -0
  12. package/dist/dingtalk-handler.js +173 -0
  13. package/dist/dingtalk-types.js +1 -0
  14. package/dist/email_handler.js +501 -0
  15. package/dist/exec_tool.js +90 -0
  16. package/dist/feishu-handler.js +620 -0
  17. package/dist/feishu-types.js +8 -0
  18. package/dist/feishu-utils.js +162 -0
  19. package/dist/fs_tools.js +398 -0
  20. package/dist/index.js +433 -0
  21. package/dist/mcp/config-manager.js +64 -0
  22. package/dist/mcp/index.js +3 -0
  23. package/dist/mcp/rpc.js +109 -0
  24. package/dist/mcp/session.js +140 -0
  25. package/dist/mcp_manager.js +253 -0
  26. package/dist/mouse_keyboard.js +516 -0
  27. package/dist/qq-handler.js +153 -0
  28. package/dist/qq-types.js +15 -0
  29. package/dist/qq-ws.js +178 -0
  30. package/dist/screenshot.js +271 -0
  31. package/dist/skills_hub.js +212 -0
  32. package/dist/skills_manager.js +103 -0
  33. package/dist/static/AGENTS.md +25 -0
  34. package/dist/static/app.css +539 -0
  35. package/dist/static/app.html +292 -0
  36. package/dist/static/app.js +380 -0
  37. package/dist/static/chat.html +994 -0
  38. package/dist/time_tool.js +22 -0
  39. package/dist/utils.js +15 -0
  40. package/package.json +38 -0
  41. package/src/browser_control.ts +739 -0
  42. package/src/browser_snapshot.ts +196 -0
  43. package/src/cli.ts +44 -0
  44. package/src/code_executor.ts +101 -0
  45. package/src/config_server.ts +723 -0
  46. package/src/connection_util.ts +23 -0
  47. package/src/constants.ts +2 -0
  48. package/src/desktop_state_store.ts +64 -0
  49. package/src/desktop_types.ts +44 -0
  50. package/src/desktop_verifier.ts +45 -0
  51. package/src/dingtalk-types.ts +26 -0
  52. package/src/exec_tool.ts +93 -0
  53. package/src/feishu-handler.ts +722 -0
  54. package/src/feishu-types.ts +66 -0
  55. package/src/feishu-utils.ts +174 -0
  56. package/src/fs_tools.ts +411 -0
  57. package/src/index.ts +474 -0
  58. package/src/mcp/config-manager.ts +85 -0
  59. package/src/mcp/index.ts +7 -0
  60. package/src/mcp/rpc.ts +131 -0
  61. package/src/mcp/session.ts +182 -0
  62. package/src/mcp_manager.ts +273 -0
  63. package/src/mouse_keyboard.ts +526 -0
  64. package/src/qq-types.ts +49 -0
  65. package/src/qq-ws.ts +223 -0
  66. package/src/screenshot.ts +297 -0
  67. package/src/static/app.css +539 -0
  68. package/src/static/app.html +292 -0
  69. package/src/static/app.js +380 -0
  70. package/src/static/chat.html +994 -0
  71. package/src/time_tool.ts +24 -0
  72. package/src/utils.ts +22 -0
  73. package/tsconfig.json +13 -0
package/src/qq-ws.ts ADDED
@@ -0,0 +1,223 @@
1
+ import WebSocket from "ws";
2
+ import {
3
+ type QQConfig,
4
+ type QQGatewayPayload,
5
+ OP_DISPATCH,
6
+ OP_HEARTBEAT,
7
+ OP_IDENTIFY,
8
+ OP_RESUME,
9
+ OP_RECONNECT,
10
+ OP_INVALID_SESSION,
11
+ OP_HELLO,
12
+ OP_HEARTBEAT_ACK,
13
+ INTENT_PUBLIC_GUILD_MESSAGES,
14
+ INTENT_DIRECT_MESSAGE,
15
+ INTENT_GROUP_AND_C2C,
16
+ DEFAULT_API_BASE,
17
+ TOKEN_URL,
18
+ RECONNECT_DELAYS,
19
+ MAX_RECONNECT_ATTEMPTS,
20
+ } from "./qq-types.js";
21
+
22
+ type DispatchHandler = (event: string, data: any) => void;
23
+
24
+ export class QQGatewayClient {
25
+ private ws: WebSocket | null = null;
26
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
27
+ private lastSeq: number | null = null;
28
+ private sessionId: string | null = null;
29
+ private reconnectAttempts = 0;
30
+ private closed = false;
31
+
32
+ private accessToken: string = "";
33
+ private tokenExpiresAt = 0;
34
+
35
+ private onDispatch: DispatchHandler;
36
+ private log: (level: string, msg: string, data?: Record<string, unknown>) => void;
37
+
38
+ constructor(
39
+ private config: QQConfig,
40
+ onDispatch: DispatchHandler,
41
+ log?: (level: string, msg: string, data?: Record<string, unknown>) => void,
42
+ ) {
43
+ this.onDispatch = onDispatch;
44
+ this.log = log ?? ((level, msg, data) => console.log(`[qq-ws] [${level}] ${msg}`, data ?? ""));
45
+ }
46
+
47
+ private get apiBase(): string {
48
+ return (this.config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
49
+ }
50
+
51
+ async refreshToken(): Promise<string> {
52
+ if (this.accessToken && Date.now() < this.tokenExpiresAt - 30_000) {
53
+ return this.accessToken;
54
+ }
55
+ const res = await fetch(TOKEN_URL, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ appId: this.config.appId, clientSecret: this.config.appSecret }),
59
+ });
60
+ if (!res.ok) throw new Error(`QQ token refresh failed: HTTP ${res.status}`);
61
+ const body = (await res.json()) as { access_token: string; expires_in: number };
62
+ this.accessToken = body.access_token;
63
+ this.tokenExpiresAt = Date.now() + body.expires_in * 1000;
64
+ this.log("info", "access token refreshed", { expires_in: body.expires_in });
65
+ return this.accessToken;
66
+ }
67
+
68
+ getAccessToken(): string {
69
+ return this.accessToken;
70
+ }
71
+
72
+ async connect(): Promise<void> {
73
+ this.closed = false;
74
+ await this.refreshToken();
75
+
76
+ const gatewayUrl = await this.fetchGatewayUrl();
77
+ this.log("info", `connecting to gateway: ${gatewayUrl}`);
78
+ this.createConnection(gatewayUrl);
79
+ }
80
+
81
+ private async fetchGatewayUrl(): Promise<string> {
82
+ const token = await this.refreshToken();
83
+ const res = await fetch(`${this.apiBase}/gateway`, {
84
+ headers: { Authorization: `QQBot ${token}` },
85
+ });
86
+ if (!res.ok) throw new Error(`QQ gateway fetch failed: HTTP ${res.status}`);
87
+ const body = (await res.json()) as { url: string };
88
+ return body.url;
89
+ }
90
+
91
+ private createConnection(url: string): void {
92
+ const ws = new WebSocket(url);
93
+ this.ws = ws;
94
+
95
+ ws.on("open", () => {
96
+ this.log("info", "WebSocket connected");
97
+ this.reconnectAttempts = 0;
98
+ });
99
+
100
+ ws.on("message", (raw: WebSocket.Data) => {
101
+ try {
102
+ const payload = JSON.parse(raw.toString("utf-8")) as QQGatewayPayload;
103
+ this.handlePayload(payload);
104
+ } catch (err) {
105
+ this.log("error", "failed to parse WS message", { err: String(err) });
106
+ }
107
+ });
108
+
109
+ ws.on("close", (code: number, reason: Buffer) => {
110
+ this.log("warn", `WebSocket closed: ${code} ${reason.toString("utf-8")}`);
111
+ this.stopHeartbeat();
112
+ if (!this.closed) this.scheduleReconnect();
113
+ });
114
+
115
+ ws.on("error", (err: Error) => {
116
+ this.log("error", "WebSocket error", { err: err.message });
117
+ });
118
+ }
119
+
120
+ private handlePayload(payload: QQGatewayPayload): void {
121
+ if (payload.s != null) this.lastSeq = payload.s;
122
+
123
+ switch (payload.op) {
124
+ case OP_HELLO:
125
+ this.startHeartbeat((payload.d as Record<string, unknown>)?.heartbeat_interval as number ?? 41250);
126
+ if (this.sessionId) {
127
+ this.sendResume();
128
+ } else {
129
+ this.sendIdentify();
130
+ }
131
+ break;
132
+
133
+ case OP_DISPATCH:
134
+ if (payload.t === "READY") {
135
+ this.sessionId = (payload.d as Record<string, unknown>)?.session_id as string ?? null;
136
+ this.log("info", "READY", { session_id: this.sessionId });
137
+ }
138
+ if (payload.t) {
139
+ this.onDispatch(payload.t, payload.d);
140
+ }
141
+ break;
142
+
143
+ case OP_HEARTBEAT_ACK:
144
+ break;
145
+
146
+ case OP_RECONNECT:
147
+ this.log("info", "server requested reconnect");
148
+ this.ws?.close(4000, "reconnect");
149
+ break;
150
+
151
+ case OP_INVALID_SESSION:
152
+ this.log("warn", "invalid session, re-identifying");
153
+ this.sessionId = null;
154
+ this.lastSeq = null;
155
+ setTimeout(() => this.sendIdentify(), 2000);
156
+ break;
157
+
158
+ default:
159
+ this.log("debug", `unhandled op: ${payload.op}`, { d: payload.d });
160
+ }
161
+ }
162
+
163
+ private sendIdentify(): void {
164
+ const intents = INTENT_PUBLIC_GUILD_MESSAGES | INTENT_DIRECT_MESSAGE | INTENT_GROUP_AND_C2C;
165
+ this.send({
166
+ op: OP_IDENTIFY,
167
+ d: { token: `QQBot ${this.accessToken}`, intents, shard: [0, 1] },
168
+ });
169
+ this.log("debug", "sent IDENTIFY");
170
+ }
171
+
172
+ private sendResume(): void {
173
+ this.send({
174
+ op: OP_RESUME,
175
+ d: { token: `QQBot ${this.accessToken}`, session_id: this.sessionId, seq: this.lastSeq },
176
+ });
177
+ this.log("debug", "sent RESUME");
178
+ }
179
+
180
+ private startHeartbeat(intervalMs: number): void {
181
+ this.stopHeartbeat();
182
+ this.heartbeatTimer = setInterval(() => {
183
+ this.send({ op: OP_HEARTBEAT, d: this.lastSeq });
184
+ }, intervalMs);
185
+ }
186
+
187
+ private stopHeartbeat(): void {
188
+ if (this.heartbeatTimer) {
189
+ clearInterval(this.heartbeatTimer);
190
+ this.heartbeatTimer = null;
191
+ }
192
+ }
193
+
194
+ private send(payload: QQGatewayPayload): void {
195
+ if (this.ws?.readyState === WebSocket.OPEN) {
196
+ this.ws.send(JSON.stringify(payload));
197
+ }
198
+ }
199
+
200
+ private scheduleReconnect(): void {
201
+ if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
202
+ this.log("error", "max reconnect attempts reached, giving up");
203
+ return;
204
+ }
205
+ const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempts, RECONNECT_DELAYS.length - 1)] * 1000;
206
+ this.reconnectAttempts++;
207
+ this.log("info", `reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);
208
+ setTimeout(() => {
209
+ if (!this.closed) {
210
+ this.connect().catch((err) => this.log("error", "reconnect failed", { err: String(err) }));
211
+ }
212
+ }, delay);
213
+ }
214
+
215
+ close(): void {
216
+ this.closed = true;
217
+ this.stopHeartbeat();
218
+ if (this.ws) {
219
+ this.ws.close(1000, "shutdown");
220
+ this.ws = null;
221
+ }
222
+ }
223
+ }
@@ -0,0 +1,297 @@
1
+ import { randomUUID } from "crypto";
2
+ import { spawnSync } from "child_process";
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import type { ContentPart } from "@greatlhd/ailo-endpoint-sdk";
7
+ import type { CoordinateSpace, DesktopObservation, DesktopScope, Rect } from "./desktop_types.js";
8
+
9
+ interface ScreenDescriptor extends Rect {
10
+ index: number;
11
+ primary: boolean;
12
+ }
13
+
14
+ export interface ScreenInfo {
15
+ count: number;
16
+ screens: ScreenDescriptor[];
17
+ virtualBounds: Rect;
18
+ }
19
+
20
+ export interface ScreenshotOptions {
21
+ capture_window?: boolean;
22
+ /** 多显示器时:不传或 "all" 截取全部,0/1/2... 截取指定显示器(0-based)*/
23
+ screen?: number | "all";
24
+ }
25
+
26
+ export interface ScreenshotCaptureResult {
27
+ parts: ContentPart[];
28
+ observation?: DesktopObservation;
29
+ }
30
+
31
+ function ok(message: string, observation: DesktopObservation): ContentPart[] {
32
+ return [
33
+ {
34
+ type: "text",
35
+ text: JSON.stringify({
36
+ ok: true,
37
+ message,
38
+ observation: {
39
+ observation_id: observation.id,
40
+ timestamp: observation.timestamp,
41
+ scope: observation.scope,
42
+ coordinate_space: observation.coordinateSpace,
43
+ image_width: observation.imageWidth,
44
+ image_height: observation.imageHeight,
45
+ image_path: observation.image.path,
46
+ },
47
+ }, null, 2),
48
+ },
49
+ {
50
+ type: "image",
51
+ media: {
52
+ type: "image",
53
+ path: observation.image.path,
54
+ mime: observation.image.mime,
55
+ name: observation.image.name,
56
+ },
57
+ },
58
+ ];
59
+ }
60
+
61
+ function fail(error: string): ScreenshotCaptureResult {
62
+ return {
63
+ parts: [{ type: "text", text: JSON.stringify({ ok: false, error }, null, 2) }],
64
+ };
65
+ }
66
+
67
+ export function getScreenInfo(): ScreenInfo {
68
+ const platform = os.platform();
69
+ if (platform === "win32") {
70
+ const ps = [
71
+ "Add-Type -AssemblyName System.Windows.Forms",
72
+ "$screens=[System.Windows.Forms.Screen]::AllScreens",
73
+ "$primary=[System.Windows.Forms.Screen]::PrimaryScreen",
74
+ "$vs=[System.Windows.Forms.SystemInformation]::VirtualScreen",
75
+ "$r=@()",
76
+ "for($i=0;$i -lt $screens.Count;$i++){",
77
+ " $b=$screens[$i].Bounds",
78
+ " $r+=@{index=$i;x=$b.X;y=$b.Y;width=$b.Width;height=$b.Height;primary=($screens[$i] -eq $primary)}",
79
+ "}",
80
+ "Write-Output (@{count=$screens.Count;screens=$r;virtualBounds=@{x=$vs.X;y=$vs.Y;width=$vs.Width;height=$vs.Height}}|ConvertTo-Json -Compress)",
81
+ ].join("; ");
82
+ const r = spawnSync("powershell", ["-Command", ps], { encoding: "utf-8", timeout: 5000 });
83
+ try {
84
+ const j = JSON.parse((r.stdout ?? "").trim());
85
+ return {
86
+ count: j.count ?? 1,
87
+ screens: (j.screens ?? []).map((s: any) => ({
88
+ index: s.index,
89
+ x: s.x ?? 0,
90
+ y: s.y ?? 0,
91
+ width: s.width,
92
+ height: s.height,
93
+ primary: !!s.primary,
94
+ })),
95
+ virtualBounds: {
96
+ x: j.virtualBounds?.x ?? 0,
97
+ y: j.virtualBounds?.y ?? 0,
98
+ width: j.virtualBounds?.width ?? 1920,
99
+ height: j.virtualBounds?.height ?? 1080,
100
+ },
101
+ };
102
+ } catch {
103
+ return {
104
+ count: 1,
105
+ screens: [{ index: 0, x: 0, y: 0, width: 1920, height: 1080, primary: true }],
106
+ virtualBounds: { x: 0, y: 0, width: 1920, height: 1080 },
107
+ };
108
+ }
109
+ } else if (platform === "darwin") {
110
+ const r = spawnSync("system_profiler", ["SPDisplaysDataType", "-json"], { encoding: "utf-8", timeout: 5000 });
111
+ try {
112
+ const j = JSON.parse((r.stdout ?? "").trim());
113
+ const displays = (j?.SPDisplaysDataType ?? []).flatMap((d: any) => d.spdisplays_ndrvs ?? [d]);
114
+ const screens = displays
115
+ .filter((d: any) => d.spdisplays_resolution)
116
+ .map((d: any, i: number) => {
117
+ const m = String(d.spdisplays_resolution ?? "").match(/(\d+)\s*x\s*(\d+)/);
118
+ return {
119
+ index: i,
120
+ x: 0,
121
+ y: 0,
122
+ width: m ? Number(m[1]) : 1920,
123
+ height: m ? Number(m[2]) : 1080,
124
+ primary: i === 0,
125
+ };
126
+ });
127
+ const primary = screens[0] ?? { index: 0, x: 0, y: 0, width: 1920, height: 1080, primary: true };
128
+ return { count: screens.length || 1, screens: screens.length ? screens : [primary], virtualBounds: { x: 0, y: 0, width: primary.width, height: primary.height } };
129
+ } catch {
130
+ return {
131
+ count: 1,
132
+ screens: [{ index: 0, x: 0, y: 0, width: 1920, height: 1080, primary: true }],
133
+ virtualBounds: { x: 0, y: 0, width: 1920, height: 1080 },
134
+ };
135
+ }
136
+ } else {
137
+ const r = spawnSync("xdpyinfo", [], { encoding: "utf-8", timeout: 5000 });
138
+ const m = (r.stdout ?? "").match(/dimensions:\s*(\d+)x(\d+)/);
139
+ const width = m ? Number(m[1]) : 1920;
140
+ const height = m ? Number(m[2]) : 1080;
141
+ return {
142
+ count: 1,
143
+ screens: [{ index: 0, x: 0, y: 0, width, height, primary: true }],
144
+ virtualBounds: { x: 0, y: 0, width, height },
145
+ };
146
+ }
147
+ }
148
+
149
+ function buildObservation(args: {
150
+ imagePath: string;
151
+ imageWidth: number;
152
+ imageHeight: number;
153
+ captureWindow: boolean;
154
+ screenOpt?: number | "all";
155
+ }): DesktopObservation {
156
+ const { imagePath, imageWidth, imageHeight, captureWindow, screenOpt } = args;
157
+ const screenInfo = getScreenInfo();
158
+ let scope: DesktopScope;
159
+ let coordinateSpace: CoordinateSpace;
160
+
161
+ if (captureWindow) {
162
+ scope = {
163
+ kind: "window",
164
+ bounds: { x: 0, y: 0, width: imageWidth, height: imageHeight },
165
+ };
166
+ coordinateSpace = "window_local";
167
+ } else if (typeof screenOpt === "number") {
168
+ const screen = screenInfo.screens.find((item) => item.index === screenOpt) ?? screenInfo.screens[0];
169
+ scope = {
170
+ kind: "screen",
171
+ screenIndex: screen.index,
172
+ bounds: { x: screen.x, y: screen.y, width: screen.width, height: screen.height },
173
+ };
174
+ coordinateSpace = "screen_local";
175
+ } else {
176
+ scope = {
177
+ kind: "virtual_screen",
178
+ bounds: screenInfo.virtualBounds,
179
+ };
180
+ coordinateSpace = "virtual_screen";
181
+ }
182
+
183
+ return {
184
+ id: `obs_${randomUUID()}`,
185
+ timestamp: Date.now(),
186
+ scope,
187
+ coordinateSpace,
188
+ imageWidth,
189
+ imageHeight,
190
+ image: {
191
+ path: imagePath,
192
+ mime: "image/png",
193
+ name: "screenshot.png",
194
+ },
195
+ };
196
+ }
197
+
198
+ export async function captureDesktopObservation(opts: ScreenshotOptions | boolean = false): Promise<ScreenshotCaptureResult> {
199
+ const captureWindow = typeof opts === "boolean" ? opts : !!opts.capture_window;
200
+ const screenOpt = typeof opts === "boolean" ? undefined : opts?.screen;
201
+ const tmpPath = path.join(os.tmpdir(), `ailo_screenshot_${Date.now()}.png`);
202
+ const platform = os.platform();
203
+
204
+ if (platform === "darwin") {
205
+ if (captureWindow) {
206
+ spawnSync("screencapture", ["-w", tmpPath]);
207
+ } else if (typeof screenOpt === "number") {
208
+ spawnSync("screencapture", ["-x", "-D", String(screenOpt + 1), tmpPath]);
209
+ } else {
210
+ spawnSync("screencapture", ["-x", tmpPath]);
211
+ }
212
+ } else if (platform === "win32") {
213
+ const escaped = tmpPath.replace(/'/g, "''").replace(/\\/g, "\\\\");
214
+ let ps: string;
215
+ if (typeof screenOpt === "number") {
216
+ ps = [
217
+ "Add-Type -AssemblyName System.Windows.Forms",
218
+ "Add-Type -AssemblyName System.Drawing",
219
+ "$screens=[System.Windows.Forms.Screen]::AllScreens",
220
+ `$idx=${screenOpt}`,
221
+ "if($idx -ge $screens.Count){ $idx=0 }",
222
+ "$screen=$screens[$idx].Bounds",
223
+ "$bmp=New-Object System.Drawing.Bitmap($screen.Width,$screen.Height)",
224
+ "$g=[System.Drawing.Graphics]::FromImage($bmp)",
225
+ "$g.CopyFromScreen($screen.Location,[System.Drawing.Point]::Empty,$screen.Size)",
226
+ `$bmp.Save('${escaped}')`,
227
+ ].join("; ");
228
+ } else {
229
+ ps = [
230
+ "Add-Type -AssemblyName System.Windows.Forms",
231
+ "Add-Type -AssemblyName System.Drawing",
232
+ "$vs=[System.Windows.Forms.SystemInformation]::VirtualScreen",
233
+ "$bmp=New-Object System.Drawing.Bitmap($vs.Width,$vs.Height)",
234
+ "$g=[System.Drawing.Graphics]::FromImage($bmp)",
235
+ "$g.CopyFromScreen($vs.X,$vs.Y,0,0,[System.Drawing.Size]::new($vs.Width,$vs.Height))",
236
+ `$bmp.Save('${escaped}')`,
237
+ ].join("; ");
238
+ }
239
+ spawnSync("powershell", ["-Command", ps]);
240
+ } else {
241
+ const r = spawnSync("scrot", [tmpPath]);
242
+ if (r.status !== 0) spawnSync("import", ["-window", "root", tmpPath]);
243
+ }
244
+
245
+ if (!fs.existsSync(tmpPath)) return fail("截图失败:未能生成截图文件");
246
+
247
+ let imageWidth = 0;
248
+ let imageHeight = 0;
249
+ try {
250
+ const screenInfo = getScreenInfo();
251
+ if (typeof screenOpt === "number") {
252
+ const screen = screenInfo.screens.find((item) => item.index === screenOpt) ?? screenInfo.screens[0];
253
+ imageWidth = screen.width;
254
+ imageHeight = screen.height;
255
+ } else {
256
+ imageWidth = screenInfo.virtualBounds.width;
257
+ imageHeight = screenInfo.virtualBounds.height;
258
+ }
259
+ } catch {
260
+ imageWidth = 1920;
261
+ imageHeight = 1080;
262
+ }
263
+
264
+ const observation = buildObservation({
265
+ imagePath: tmpPath,
266
+ imageWidth,
267
+ imageHeight,
268
+ captureWindow,
269
+ screenOpt,
270
+ });
271
+
272
+ let message: string;
273
+ if (captureWindow) {
274
+ message = "窗口截图完成";
275
+ } else {
276
+ const screenInfo = getScreenInfo();
277
+ if (screenInfo.count > 1) {
278
+ if (typeof screenOpt === "number") {
279
+ message = `截图完成(第 ${screenOpt + 1} 块/共 ${screenInfo.count} 块屏。可用 screen=0~${screenInfo.count - 1} 分别截取其他屏)`;
280
+ } else {
281
+ message = `截图完成(共 ${screenInfo.count} 块屏,已截取全部。可用 screen=0~${screenInfo.count - 1} 分别截取单块)`;
282
+ }
283
+ } else {
284
+ message = typeof screenOpt === "number" ? `显示器 ${screenOpt} 截图完成` : "截图完成";
285
+ }
286
+ }
287
+
288
+ return {
289
+ parts: ok(message, observation),
290
+ observation,
291
+ };
292
+ }
293
+
294
+ export async function takeScreenshot(opts: ScreenshotOptions | boolean = false): Promise<ContentPart[]> {
295
+ const result = await captureDesktopObservation(opts);
296
+ return result.parts;
297
+ }