@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.
- package/copy-static.mjs +11 -0
- package/dist/browser_control.js +767 -0
- package/dist/browser_snapshot.js +174 -0
- package/dist/cli.js +36 -0
- package/dist/code_executor.js +95 -0
- package/dist/config_server.js +658 -0
- package/dist/connection_util.js +14 -0
- package/dist/constants.js +2 -0
- package/dist/desktop_state_store.js +57 -0
- package/dist/desktop_types.js +1 -0
- package/dist/desktop_verifier.js +40 -0
- package/dist/dingtalk-handler.js +173 -0
- package/dist/dingtalk-types.js +1 -0
- package/dist/email_handler.js +501 -0
- package/dist/exec_tool.js +90 -0
- package/dist/feishu-handler.js +620 -0
- package/dist/feishu-types.js +8 -0
- package/dist/feishu-utils.js +162 -0
- package/dist/fs_tools.js +398 -0
- package/dist/index.js +433 -0
- package/dist/mcp/config-manager.js +64 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/rpc.js +109 -0
- package/dist/mcp/session.js +140 -0
- package/dist/mcp_manager.js +253 -0
- package/dist/mouse_keyboard.js +516 -0
- package/dist/qq-handler.js +153 -0
- package/dist/qq-types.js +15 -0
- package/dist/qq-ws.js +178 -0
- package/dist/screenshot.js +271 -0
- package/dist/skills_hub.js +212 -0
- package/dist/skills_manager.js +103 -0
- package/dist/static/AGENTS.md +25 -0
- package/dist/static/app.css +539 -0
- package/dist/static/app.html +292 -0
- package/dist/static/app.js +380 -0
- package/dist/static/chat.html +994 -0
- package/dist/time_tool.js +22 -0
- package/dist/utils.js +15 -0
- package/package.json +38 -0
- package/src/browser_control.ts +739 -0
- package/src/browser_snapshot.ts +196 -0
- package/src/cli.ts +44 -0
- package/src/code_executor.ts +101 -0
- package/src/config_server.ts +723 -0
- package/src/connection_util.ts +23 -0
- package/src/constants.ts +2 -0
- package/src/desktop_state_store.ts +64 -0
- package/src/desktop_types.ts +44 -0
- package/src/desktop_verifier.ts +45 -0
- package/src/dingtalk-types.ts +26 -0
- package/src/exec_tool.ts +93 -0
- package/src/feishu-handler.ts +722 -0
- package/src/feishu-types.ts +66 -0
- package/src/feishu-utils.ts +174 -0
- package/src/fs_tools.ts +411 -0
- package/src/index.ts +474 -0
- package/src/mcp/config-manager.ts +85 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/rpc.ts +131 -0
- package/src/mcp/session.ts +182 -0
- package/src/mcp_manager.ts +273 -0
- package/src/mouse_keyboard.ts +526 -0
- package/src/qq-types.ts +49 -0
- package/src/qq-ws.ts +223 -0
- package/src/screenshot.ts +297 -0
- package/src/static/app.css +539 -0
- package/src/static/app.html +292 -0
- package/src/static/app.js +380 -0
- package/src/static/chat.html +994 -0
- package/src/time_tool.ts +24 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports from SDK + config.json loading helper for desktop.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
hasValidConfig,
|
|
7
|
+
backoffDelayMs,
|
|
8
|
+
readConfig,
|
|
9
|
+
} from "@greatlhd/ailo-endpoint-sdk";
|
|
10
|
+
export type { AiloConnectionConfig } from "@greatlhd/ailo-endpoint-sdk";
|
|
11
|
+
|
|
12
|
+
import { readConfig } from "@greatlhd/ailo-endpoint-sdk";
|
|
13
|
+
import type { AiloConnectionConfig } from "@greatlhd/ailo-endpoint-sdk";
|
|
14
|
+
|
|
15
|
+
export function loadConnectionConfig(configPath: string): AiloConnectionConfig {
|
|
16
|
+
const raw = readConfig(configPath);
|
|
17
|
+
const ailo = (raw as Record<string, unknown>).ailo as Record<string, unknown> | undefined;
|
|
18
|
+
return {
|
|
19
|
+
url: process.env.AILO_WS_URL || (ailo?.wsUrl as string) || "",
|
|
20
|
+
apiKey: process.env.AILO_API_KEY || (ailo?.apiKey as string) || "",
|
|
21
|
+
endpointId: process.env.AILO_ENDPOINT_ID || (ailo?.endpointId as string) || "",
|
|
22
|
+
};
|
|
23
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DesktopActionResult, DesktopObservation, DesktopVerdict } from "./desktop_types.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OBSERVATION_TTL_MS = 60_000;
|
|
4
|
+
const MAX_OBSERVATIONS = 20;
|
|
5
|
+
|
|
6
|
+
export class DesktopStateStore {
|
|
7
|
+
private readonly observations = new Map<string, DesktopObservation>();
|
|
8
|
+
private lastObservationId: string | null = null;
|
|
9
|
+
private lastAction: DesktopActionResult | null = null;
|
|
10
|
+
private lastVerdict: DesktopVerdict | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(private readonly observationTtlMs = DEFAULT_OBSERVATION_TTL_MS) {}
|
|
13
|
+
|
|
14
|
+
saveObservation(observation: DesktopObservation): void {
|
|
15
|
+
this.pruneExpired();
|
|
16
|
+
this.observations.set(observation.id, observation);
|
|
17
|
+
this.lastObservationId = observation.id;
|
|
18
|
+
while (this.observations.size > MAX_OBSERVATIONS) {
|
|
19
|
+
const oldestKey = this.observations.keys().next().value;
|
|
20
|
+
if (!oldestKey) break;
|
|
21
|
+
this.observations.delete(oldestKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getObservation(id: string): DesktopObservation | null {
|
|
26
|
+
this.pruneExpired();
|
|
27
|
+
return this.observations.get(id) ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getLatestObservation(): DesktopObservation | null {
|
|
31
|
+
this.pruneExpired();
|
|
32
|
+
if (!this.lastObservationId) return null;
|
|
33
|
+
return this.observations.get(this.lastObservationId) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isExpired(observation: DesktopObservation): boolean {
|
|
37
|
+
return Date.now() - observation.timestamp > this.observationTtlMs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setLastAction(action: DesktopActionResult): void {
|
|
41
|
+
this.lastAction = action;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getLastAction(): DesktopActionResult | null {
|
|
45
|
+
return this.lastAction;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setLastVerdict(verdict: DesktopVerdict): void {
|
|
49
|
+
this.lastVerdict = verdict;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getLastVerdict(): DesktopVerdict | null {
|
|
53
|
+
return this.lastVerdict;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private pruneExpired(): void {
|
|
57
|
+
for (const [id, observation] of this.observations) {
|
|
58
|
+
if (this.isExpired(observation)) this.observations.delete(id);
|
|
59
|
+
}
|
|
60
|
+
if (this.lastObservationId && !this.observations.has(this.lastObservationId)) {
|
|
61
|
+
this.lastObservationId = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface Rect {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type DesktopScopeKind = "virtual_screen" | "screen" | "window" | "region";
|
|
9
|
+
export type CoordinateSpace = "virtual_screen" | "screen_local" | "window_local" | "region_local";
|
|
10
|
+
|
|
11
|
+
export interface DesktopScope {
|
|
12
|
+
kind: DesktopScopeKind;
|
|
13
|
+
bounds: Rect;
|
|
14
|
+
screenIndex?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DesktopObservation {
|
|
18
|
+
id: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
scope: DesktopScope;
|
|
21
|
+
coordinateSpace: CoordinateSpace;
|
|
22
|
+
imageWidth: number;
|
|
23
|
+
imageHeight: number;
|
|
24
|
+
image: {
|
|
25
|
+
path: string;
|
|
26
|
+
mime: string;
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DesktopActionResult {
|
|
32
|
+
accepted: boolean;
|
|
33
|
+
executed: boolean;
|
|
34
|
+
action: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
observationId?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
details?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DesktopVerdict {
|
|
42
|
+
status: "success" | "failure" | "uncertain";
|
|
43
|
+
reason: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { PNG } from "pngjs";
|
|
3
|
+
import pixelmatch from "pixelmatch";
|
|
4
|
+
import type { DesktopActionResult, DesktopObservation, DesktopVerdict } from "./desktop_types.js";
|
|
5
|
+
|
|
6
|
+
function imagesDiffer(beforePath: string, afterPath: string): boolean {
|
|
7
|
+
const before = PNG.sync.read(readFileSync(beforePath));
|
|
8
|
+
const after = PNG.sync.read(readFileSync(afterPath));
|
|
9
|
+
if (before.width !== after.width || before.height !== after.height) return true;
|
|
10
|
+
const diff = new PNG({ width: before.width, height: before.height });
|
|
11
|
+
// pixelmatch is exported as CJS export =, use .default in ESM context
|
|
12
|
+
const pm = (pixelmatch as unknown as { default: typeof pixelmatch }).default ?? pixelmatch;
|
|
13
|
+
const numDiffPixels = pm(before.data, after.data, diff.data, before.width, before.height, { threshold: 0.1 });
|
|
14
|
+
// 少于 0.5% 像素变化视为无变化(防误报)
|
|
15
|
+
const totalPixels = before.width * before.height;
|
|
16
|
+
return numDiffPixels / totalPixels > 0.005;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function verifyDesktopAction(args: {
|
|
20
|
+
beforeObservation: DesktopObservation | null;
|
|
21
|
+
afterObservation: DesktopObservation | null;
|
|
22
|
+
actionResult: DesktopActionResult;
|
|
23
|
+
}): DesktopVerdict {
|
|
24
|
+
const { beforeObservation, afterObservation, actionResult } = args;
|
|
25
|
+
if (!actionResult.accepted) {
|
|
26
|
+
return { status: "failure", reason: actionResult.error ?? "动作被拒绝执行" };
|
|
27
|
+
}
|
|
28
|
+
if (!actionResult.executed) {
|
|
29
|
+
return { status: "failure", reason: actionResult.error ?? "动作未执行" };
|
|
30
|
+
}
|
|
31
|
+
if (!beforeObservation || !afterObservation) {
|
|
32
|
+
return { status: "uncertain", reason: "缺少动作前后 observation,无法完成验证" };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (imagesDiffer(beforeObservation.image.path, afterObservation.image.path)) {
|
|
36
|
+
return { status: "success", reason: "动作后界面发生变化" };
|
|
37
|
+
}
|
|
38
|
+
return { status: "uncertain", reason: "动作后未观察到明显界面变化" };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
status: "uncertain",
|
|
42
|
+
reason: `无法比较动作前后截图: ${error instanceof Error ? error.message : String(error)}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface DingTalkConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface DingTalkBotMessage {
|
|
7
|
+
conversationId: string;
|
|
8
|
+
conversationType: "1" | "2";
|
|
9
|
+
chatbotCorpId?: string;
|
|
10
|
+
chatbotUserId?: string;
|
|
11
|
+
msgId: string;
|
|
12
|
+
msgtype: string;
|
|
13
|
+
text?: { content: string };
|
|
14
|
+
senderNick: string;
|
|
15
|
+
senderStaffId: string;
|
|
16
|
+
senderCorpId?: string;
|
|
17
|
+
sessionWebhook: string;
|
|
18
|
+
sessionWebhookExpiredTime: number;
|
|
19
|
+
createAt: number;
|
|
20
|
+
conversationTitle?: string;
|
|
21
|
+
atUsers?: Array<{ dingtalkId: string; staffId?: string }>;
|
|
22
|
+
isAdmin?: boolean;
|
|
23
|
+
isInAtList?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const STALE_MESSAGE_THRESHOLD_MS = 5 * 60 * 1000;
|
package/src/exec_tool.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
5
|
+
import type { EndpointContext } from "@greatlhd/ailo-endpoint-sdk";
|
|
6
|
+
|
|
7
|
+
const MAX_OUTPUT = 50000;
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 120_000; // 2 分钟
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 异步执行 shell 命令。
|
|
12
|
+
* 立即返回"已启动",命令在后台运行,完成后通过 sendSignal("tool_result") 推送结果给 LLM。
|
|
13
|
+
*/
|
|
14
|
+
export async function execTool(ctx: EndpointContext, args: Record<string, unknown>): Promise<string> {
|
|
15
|
+
const command = String(args.command ?? "").trim();
|
|
16
|
+
if (!command) throw new Error("command 必填");
|
|
17
|
+
const cwd = (args.cwd as string) || undefined;
|
|
18
|
+
const timeoutSec = Math.max(5, Math.min(600, Number(args.timeout) || DEFAULT_TIMEOUT_MS / 1000));
|
|
19
|
+
const timeoutMs = timeoutSec * 1000;
|
|
20
|
+
|
|
21
|
+
const isWin = os.platform() === "win32";
|
|
22
|
+
const shell = isWin ? "powershell" : "/bin/sh";
|
|
23
|
+
const wrappedCommand = isWin
|
|
24
|
+
? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}`
|
|
25
|
+
: command;
|
|
26
|
+
const shellArgs = isWin ? ["-Command", wrappedCommand] : ["-c", command];
|
|
27
|
+
const env = {
|
|
28
|
+
...process.env,
|
|
29
|
+
PYTHONIOENCODING: "utf-8",
|
|
30
|
+
PYTHONUTF8: "1",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 在临时目录执行命令,避免 cwd 污染
|
|
34
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), "ailo-exec-"));
|
|
35
|
+
const proc = spawn(shell, shellArgs, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd || tmpDir, env });
|
|
36
|
+
|
|
37
|
+
const output: string[] = [];
|
|
38
|
+
let sent = false;
|
|
39
|
+
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
if (timeoutHandle) { clearTimeout(timeoutHandle); timeoutHandle = null; }
|
|
42
|
+
rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const sendResult = (content: string) => {
|
|
46
|
+
if (sent) return;
|
|
47
|
+
sent = true;
|
|
48
|
+
ctx.sendSignal("tool_result", { content });
|
|
49
|
+
cleanup();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = setTimeout(() => {
|
|
53
|
+
timeoutHandle = null;
|
|
54
|
+
try { proc.kill("SIGTERM"); } catch {}
|
|
55
|
+
setTimeout(() => { try { proc.kill("SIGKILL"); } catch {} }, 2000);
|
|
56
|
+
sendResult(JSON.stringify({
|
|
57
|
+
ok: false,
|
|
58
|
+
type: "timeout",
|
|
59
|
+
command,
|
|
60
|
+
timeoutSec: timeoutMs / 1000,
|
|
61
|
+
message: `超过 ${timeoutMs / 1000}s 未完成,已终止`,
|
|
62
|
+
}));
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
|
|
65
|
+
proc.stdout?.on("data", (d: Buffer) => output.push(d.toString("utf-8")));
|
|
66
|
+
proc.stderr?.on("data", (d: Buffer) => output.push(d.toString("utf-8")));
|
|
67
|
+
|
|
68
|
+
proc.on("close", (code) => {
|
|
69
|
+
const text = output.join("").trim();
|
|
70
|
+
const truncated = text.length > MAX_OUTPUT
|
|
71
|
+
? text.slice(0, MAX_OUTPUT / 2) + "\n...[截断]...\n" + text.slice(-MAX_OUTPUT / 2)
|
|
72
|
+
: text;
|
|
73
|
+
|
|
74
|
+
sendResult(JSON.stringify({
|
|
75
|
+
ok: true,
|
|
76
|
+
type: "completed",
|
|
77
|
+
command,
|
|
78
|
+
exitCode: code,
|
|
79
|
+
output: truncated,
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
proc.on("error", (err) => {
|
|
84
|
+
sendResult(JSON.stringify({
|
|
85
|
+
ok: false,
|
|
86
|
+
type: "error",
|
|
87
|
+
command,
|
|
88
|
+
message: err.message,
|
|
89
|
+
}));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return JSON.stringify({ ok: true, type: "started", command });
|
|
93
|
+
}
|