@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
@@ -0,0 +1,57 @@
1
+ const DEFAULT_OBSERVATION_TTL_MS = 60_000;
2
+ const MAX_OBSERVATIONS = 20;
3
+ export class DesktopStateStore {
4
+ observationTtlMs;
5
+ observations = new Map();
6
+ lastObservationId = null;
7
+ lastAction = null;
8
+ lastVerdict = null;
9
+ constructor(observationTtlMs = DEFAULT_OBSERVATION_TTL_MS) {
10
+ this.observationTtlMs = observationTtlMs;
11
+ }
12
+ saveObservation(observation) {
13
+ this.pruneExpired();
14
+ this.observations.set(observation.id, observation);
15
+ this.lastObservationId = observation.id;
16
+ while (this.observations.size > MAX_OBSERVATIONS) {
17
+ const oldestKey = this.observations.keys().next().value;
18
+ if (!oldestKey)
19
+ break;
20
+ this.observations.delete(oldestKey);
21
+ }
22
+ }
23
+ getObservation(id) {
24
+ this.pruneExpired();
25
+ return this.observations.get(id) ?? null;
26
+ }
27
+ getLatestObservation() {
28
+ this.pruneExpired();
29
+ if (!this.lastObservationId)
30
+ return null;
31
+ return this.observations.get(this.lastObservationId) ?? null;
32
+ }
33
+ isExpired(observation) {
34
+ return Date.now() - observation.timestamp > this.observationTtlMs;
35
+ }
36
+ setLastAction(action) {
37
+ this.lastAction = action;
38
+ }
39
+ getLastAction() {
40
+ return this.lastAction;
41
+ }
42
+ setLastVerdict(verdict) {
43
+ this.lastVerdict = verdict;
44
+ }
45
+ getLastVerdict() {
46
+ return this.lastVerdict;
47
+ }
48
+ pruneExpired() {
49
+ for (const [id, observation] of this.observations) {
50
+ if (this.isExpired(observation))
51
+ this.observations.delete(id);
52
+ }
53
+ if (this.lastObservationId && !this.observations.has(this.lastObservationId)) {
54
+ this.lastObservationId = null;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from "fs";
2
+ import { PNG } from "pngjs";
3
+ import pixelmatch from "pixelmatch";
4
+ function imagesDiffer(beforePath, afterPath) {
5
+ const before = PNG.sync.read(readFileSync(beforePath));
6
+ const after = PNG.sync.read(readFileSync(afterPath));
7
+ if (before.width !== after.width || before.height !== after.height)
8
+ return true;
9
+ const diff = new PNG({ width: before.width, height: before.height });
10
+ // pixelmatch is exported as CJS export =, use .default in ESM context
11
+ const pm = pixelmatch.default ?? pixelmatch;
12
+ const numDiffPixels = pm(before.data, after.data, diff.data, before.width, before.height, { threshold: 0.1 });
13
+ // 少于 0.5% 像素变化视为无变化(防误报)
14
+ const totalPixels = before.width * before.height;
15
+ return numDiffPixels / totalPixels > 0.005;
16
+ }
17
+ export function verifyDesktopAction(args) {
18
+ const { beforeObservation, afterObservation, actionResult } = args;
19
+ if (!actionResult.accepted) {
20
+ return { status: "failure", reason: actionResult.error ?? "动作被拒绝执行" };
21
+ }
22
+ if (!actionResult.executed) {
23
+ return { status: "failure", reason: actionResult.error ?? "动作未执行" };
24
+ }
25
+ if (!beforeObservation || !afterObservation) {
26
+ return { status: "uncertain", reason: "缺少动作前后 observation,无法完成验证" };
27
+ }
28
+ try {
29
+ if (imagesDiffer(beforeObservation.image.path, afterObservation.image.path)) {
30
+ return { status: "success", reason: "动作后界面发生变化" };
31
+ }
32
+ return { status: "uncertain", reason: "动作后未观察到明显界面变化" };
33
+ }
34
+ catch (error) {
35
+ return {
36
+ status: "uncertain",
37
+ reason: `无法比较动作前后截图: ${error instanceof Error ? error.message : String(error)}`,
38
+ };
39
+ }
40
+ }
@@ -0,0 +1,173 @@
1
+ import { textPart, } from "@lmcl/ailo-endpoint-sdk";
2
+ import { STALE_MESSAGE_THRESHOLD_MS, } from "./dingtalk-types.js";
3
+ import { createChannelLogger } from "./utils.js";
4
+ export class DingTalkHandler {
5
+ config;
6
+ ctx = null;
7
+ client = null;
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ get storage() {
12
+ return this.ctx?.storage ?? null;
13
+ }
14
+ _log = createChannelLogger("dingtalk", () => this.ctx);
15
+ static WEBHOOK_STORE_KEY = "session_webhooks";
16
+ async saveWebhook(sessionKey, webhook, expiresAt) {
17
+ if (!this.storage)
18
+ return;
19
+ try {
20
+ const raw = await this.storage.getData(DingTalkHandler.WEBHOOK_STORE_KEY);
21
+ const store = raw ? JSON.parse(raw) : {};
22
+ const now = Date.now();
23
+ for (const [k, v] of Object.entries(store)) {
24
+ if (v.exp < now)
25
+ delete store[k];
26
+ }
27
+ store[sessionKey] = { url: webhook, exp: expiresAt };
28
+ await this.storage.setData(DingTalkHandler.WEBHOOK_STORE_KEY, JSON.stringify(store));
29
+ }
30
+ catch (err) {
31
+ this._log("warn", "保存 webhook 失败", { err: String(err) });
32
+ }
33
+ }
34
+ async getWebhook(sessionKey) {
35
+ if (!this.storage)
36
+ return null;
37
+ try {
38
+ const raw = await this.storage.getData(DingTalkHandler.WEBHOOK_STORE_KEY);
39
+ if (!raw)
40
+ return null;
41
+ const store = JSON.parse(raw);
42
+ const entry = store[sessionKey];
43
+ if (!entry)
44
+ return null;
45
+ if (entry.exp < Date.now())
46
+ return null;
47
+ return entry.url;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ sessionKeyFromMessage(msg) {
54
+ if (msg.conversationType === "2" && msg.conversationId) {
55
+ return `grp:${msg.conversationId.slice(-16)}`;
56
+ }
57
+ return `p2p:${msg.senderStaffId || msg.senderNick}`;
58
+ }
59
+ acceptMessage(msg) {
60
+ if (!this.ctx)
61
+ return;
62
+ this.ctx.accept(msg).catch((err) => this._log("error", "accept failed", { err: String(err) }));
63
+ }
64
+ buildAcceptMessage(opts) {
65
+ const { chatId, text, chatType, senderId = "", senderName = "", chatName } = opts;
66
+ const isPrivate = chatType === "私聊";
67
+ const tags = [
68
+ { kind: "channel", value: "dingtalk", groupWith: true },
69
+ { kind: "conv_type", value: chatType, groupWith: false },
70
+ { kind: "chat_id", value: chatId, groupWith: true, passToTool: true },
71
+ ];
72
+ if (!isPrivate && chatName) {
73
+ tags.push({ kind: "group", value: chatName, groupWith: false });
74
+ }
75
+ if (senderName) {
76
+ tags.push({ kind: "participant", value: senderName, groupWith: false });
77
+ }
78
+ if (senderId) {
79
+ tags.push({ kind: "sender_id", value: senderId, groupWith: false, passToTool: true });
80
+ }
81
+ const content = [];
82
+ if (text)
83
+ content.push(textPart(text));
84
+ return { content, contextTags: tags };
85
+ }
86
+ async start(ctx) {
87
+ this.ctx = ctx;
88
+ const { DWClient, TOPIC_ROBOT, EventAck } = await import("dingtalk-stream");
89
+ const client = new DWClient({
90
+ clientId: this.config.clientId,
91
+ clientSecret: this.config.clientSecret,
92
+ });
93
+ this.client = client;
94
+ const onBotMessage = async (event) => {
95
+ try {
96
+ const msg = JSON.parse(event.data);
97
+ const text = (msg.text?.content ?? "").trim();
98
+ const isGroup = msg.conversationType === "2";
99
+ const chatType = isGroup ? "群聊" : "私聊";
100
+ const sessionKey = this.sessionKeyFromMessage(msg);
101
+ const chatId = sessionKey;
102
+ if (msg.createAt && Date.now() - msg.createAt > STALE_MESSAGE_THRESHOLD_MS) {
103
+ this._log("info", `dropped stale message ${msg.msgId}`, {
104
+ create_time: msg.createAt,
105
+ age_min: Math.round((Date.now() - msg.createAt) / 60000),
106
+ });
107
+ return { status: EventAck.SUCCESS, message: "stale" };
108
+ }
109
+ if (msg.sessionWebhook) {
110
+ await this.saveWebhook(sessionKey, msg.sessionWebhook, msg.sessionWebhookExpiredTime);
111
+ }
112
+ this._log("debug", `received ${msg.msgtype} ${chatType} from ${msg.senderNick}`, {
113
+ conversationId: msg.conversationId,
114
+ text_len: text.length,
115
+ });
116
+ if (!text) {
117
+ this._log("debug", `skipped empty message ${msg.msgId}`);
118
+ return { status: EventAck.SUCCESS, message: "empty" };
119
+ }
120
+ this.acceptMessage(this.buildAcceptMessage({
121
+ chatId,
122
+ text,
123
+ chatType,
124
+ senderId: msg.senderStaffId,
125
+ senderName: msg.senderNick,
126
+ chatName: isGroup ? (msg.conversationTitle || undefined) : undefined,
127
+ }));
128
+ }
129
+ catch (err) {
130
+ this._log("error", "处理钉钉消息失败", { err: String(err) });
131
+ }
132
+ return { status: EventAck.SUCCESS, message: "OK" };
133
+ };
134
+ client.registerCallbackListener(TOPIC_ROBOT, onBotMessage).connect();
135
+ this._log("info", "钉钉 Stream 连接已建立");
136
+ ctx.reportHealth("connected");
137
+ }
138
+ async sendText(chatId, text) {
139
+ if (!text?.trim())
140
+ return;
141
+ const webhook = await this.getWebhook(chatId);
142
+ if (!webhook) {
143
+ this._log("warn", `无法发送消息到 ${chatId}:sessionWebhook 不存在或已过期`);
144
+ throw new Error(`sessionWebhook 不存在或已过期 (chatId=${chatId})`);
145
+ }
146
+ const isMarkdown = /[#*`\[\]|]/.test(text);
147
+ const body = isMarkdown
148
+ ? { msgtype: "markdown", markdown: { title: "回复", text } }
149
+ : { msgtype: "text", text: { content: text } };
150
+ const res = await fetch(webhook, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ body: JSON.stringify(body),
154
+ });
155
+ if (!res.ok) {
156
+ const detail = await res.text().catch(() => "");
157
+ this._log("error", `发送消息失败: HTTP ${res.status}`, { detail });
158
+ throw new Error(`钉钉发送失败: HTTP ${res.status}`);
159
+ }
160
+ this._log("debug", `消息已发送到 ${chatId}`);
161
+ }
162
+ async stop() {
163
+ if (this.client) {
164
+ try {
165
+ if (typeof this.client.disconnect === "function")
166
+ this.client.disconnect();
167
+ }
168
+ catch { /* best-effort */ }
169
+ this.client = null;
170
+ }
171
+ this.ctx = null;
172
+ }
173
+ }
@@ -0,0 +1 @@
1
+ export const STALE_MESSAGE_THRESHOLD_MS = 5 * 60 * 1000;