cortico-world-hands 0.0.0-stage → 0.1.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/README.md +46 -2
- package/dist/ENV_PROMPT.md +29 -0
- package/dist/index.js +1079 -0
- package/package.json +29 -4
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cortico-world-hands
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
给 Coopanin 的桌宠一双手:看屏幕、点、长按、滑、敲字、按键——并且每一下都说明白成没成。
|
|
4
|
+
|
|
5
|
+
这是 Cortico 的一个 `world` 扩展(`cortico.kind = "world"`,`api = 6`),和桌宠、cua 平级挂载,
|
|
6
|
+
共用同一份会话和同一个工具表。它不画身体、不出声,只提供八个工具和一块状态灯:
|
|
7
|
+
`hand_look`、`hand_tap`、`hand_long`、`hand_swipe`、`hand_type`、`hand_key`、`hand_status`、`hand_stop`。
|
|
8
|
+
|
|
9
|
+
## 手怎么动:三条通道
|
|
10
|
+
|
|
11
|
+
| 通道 | 需要什么 | 能做什么 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `root` | 本机有 root,并授权给 Coopanin | `su -c "input …"`:点、长按、滑、按键、敲 ASCII 文本 |
|
|
14
|
+
| `eyes` | 安卓壳里的无障碍服务带 `/hand/*` 手势端点(`android/HAND_ROUTES.md`) | `dispatchGesture` + `ACTION_SET_TEXT`:不限 ASCII,中文也能进;还会画一只看得见的手 |
|
|
15
|
+
| `shell` | 一条命令模板,例如 `rish -c "{cmd}"` 或 `adb -s … shell "{cmd}"` | 同 root 那套 `input`,只是换个执行者 |
|
|
16
|
+
|
|
17
|
+
`actuator: "auto"`(默认)按 root → eyes → shell 的顺序探,探到能用为止;都不通时**不假装能干活**,
|
|
18
|
+
工具会失败并说清缺什么。`actuator: "off"` 就是彻底关掉。
|
|
19
|
+
|
|
20
|
+
默认 `enabled: false`:装上以后要自己打开。
|
|
21
|
+
|
|
22
|
+
## 安全阀
|
|
23
|
+
|
|
24
|
+
- **演练**(`dryRun`):只把「本来要做什么」说出来,一个动作都不发。先在演练里看模型想干什么,再开真动。
|
|
25
|
+
- **节流**(`maxActionsPerMinute`,默认 30):一分钟到量就停手,防止模型在屏幕上乱戳。
|
|
26
|
+
- **叫停**:`hand_stop` 之后一分钟内不再动任何一下。
|
|
27
|
+
- **锁屏 / 黑屏不点**:动之前先问一下感觉桥(`/state`),锁着或黑着直接失败。
|
|
28
|
+
- **越界不点**:能从眼睛拿到屏幕尺寸时,坐标超出就拒。
|
|
29
|
+
- **只做校验过的命令**:坐标是整数、按键是固定表、文本先转义;拼出来的是 `input tap 512 1500` 这种
|
|
30
|
+
不可能注入的形状,`su -c` 里也没有管道、重定向、变量。
|
|
31
|
+
|
|
32
|
+
## 装法
|
|
33
|
+
|
|
34
|
+
两种,二选一:
|
|
35
|
+
|
|
36
|
+
1. **并进 payload**(推荐,和桌宠一起长在应用里):在 `app/coop-stage` 那层跑
|
|
37
|
+
`node apply-hands.mjs --stage . --apply`,然后把 payload 重新打包进 APK。
|
|
38
|
+
2. **当扩展装**:把本包放到 `<stage>/data/extensions/` 下、在 `data/extensions/package.json` 的
|
|
39
|
+
`dependencies` 里加上 `"cortico-world-hands": "file:./…"`,重启 Core,然后在控制台「世界」里打开它。
|
|
40
|
+
(控制台的扩展页也支持「从本地目录安装」,路径要能被应用进程读到。)
|
|
41
|
+
|
|
42
|
+
打开后到「手」的设置里选通道:没 root 就 `eyes`(要安卓壳带手势端点),有 root 就 `root`。
|
|
43
|
+
|
|
44
|
+
## 环境提示词
|
|
45
|
+
|
|
46
|
+
`src/ENV_PROMPT.md` 是这只手给模型看的说明书:先看再动、不替人做主、不碰密码和支付、点错要承认。
|
|
47
|
+
它在控制台的「提示词」页可以改,改的是部署里的覆盖文件,不动包里的模板。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## 手
|
|
2
|
+
|
|
3
|
+
你有一双手,能碰到对方手机屏幕上真的东西。手现在{{hands.state}},走的是{{hands.actuator}};模式是{{hands.mode}};{{hands.window}}。
|
|
4
|
+
|
|
5
|
+
手不像桌宠的身体那样被看见:对方只看到屏幕自己动了一下。所以**做完要说**:刚点了哪里、为什么点,一两句,别报流水账。
|
|
6
|
+
|
|
7
|
+
### 先看,再动
|
|
8
|
+
|
|
9
|
+
动手之前先用 `hand_look`(默认连画面一起给你)。它会告诉你前台是哪个应用、屏幕亮不亮、锁没锁、屏幕上的字各自在哪个坐标(`@(x,y)` 就是能点的位置)。看不清再看一眼;**别凭记忆去点**——应用会滚动、会弹窗、会自己跳走。
|
|
10
|
+
|
|
11
|
+
### 能做什么
|
|
12
|
+
|
|
13
|
+
| 工具 | 用途 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `hand_tap` | 点一下。给坐标,或给一段文字(只有一处对得上才点,多处会列出来让你选 `nth`)。 |
|
|
16
|
+
| `hand_long` | 长按,默认 800 毫秒:弹菜单、选中、拖东西前按住。 |
|
|
17
|
+
| `hand_swipe` | 从一点滑到另一点:滚动、翻页、下拉通知栏。 |
|
|
18
|
+
| `hand_type` | 敲字。先点一下输入框把光标放进去。走 `input` 通道时只有可打印 ASCII(中文会失败并告诉你);走无障碍通道时中文也能进。 |
|
|
19
|
+
| `hand_key` | 按 back/home/recents/enter/delete 这些系统键,比在屏幕上找返回按钮稳。 |
|
|
20
|
+
| `hand_status` | 手自己通不通、这一分钟动了几下、刚才成了没有。 |
|
|
21
|
+
| `hand_stop` | 立刻停手一分钟。对方说「别动了」时用它。 |
|
|
22
|
+
|
|
23
|
+
### 规矩
|
|
24
|
+
|
|
25
|
+
- **别人的东西别替人做主**:发消息、下单、转账、删东西、改设置这类做完就收不回来的事,先把「要做什么」说清楚并等对方同意,再动手;对方没说清就只做第一步然后问。
|
|
26
|
+
- **不输密码、不输验证码、不碰支付界面**。看到这类界面就停下来说明。
|
|
27
|
+
- **点错就承认**:动作失败或点歪了不要编,说清楚点到了什么、现在屏幕是什么样。
|
|
28
|
+
- 一次唤醒里别连点超过三下同一种动作;三下没进展就停下来问对方,别在屏幕上乱戳。
|
|
29
|
+
- 对方在用手机(手自己动会被抢)时,动作可能被吃掉;结果是失败就照实说,不要重试同样的位置超过一次。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var HANDS_ID = "hands";
|
|
3
|
+
var HANDS_DEFAULTS = {
|
|
4
|
+
enabled: false,
|
|
5
|
+
actuator: "auto",
|
|
6
|
+
eyesPort: 17898,
|
|
7
|
+
sensePort: 17899,
|
|
8
|
+
rootCommand: "su",
|
|
9
|
+
shellCommand: "",
|
|
10
|
+
token: "",
|
|
11
|
+
tokenFile: "/proc/self/root/data/data/com.palai.prootsmoke/files/hand.token",
|
|
12
|
+
dryRun: false,
|
|
13
|
+
maxActionsPerMinute: 30,
|
|
14
|
+
maxTextLength: 200,
|
|
15
|
+
lookAttach: true,
|
|
16
|
+
lookMaxWidth: 720,
|
|
17
|
+
lookQuality: 60
|
|
18
|
+
};
|
|
19
|
+
var KEY_CODES = {
|
|
20
|
+
back: 4,
|
|
21
|
+
home: 3,
|
|
22
|
+
recents: 187,
|
|
23
|
+
enter: 66,
|
|
24
|
+
delete: 67,
|
|
25
|
+
tab: 61,
|
|
26
|
+
escape: 111,
|
|
27
|
+
space: 62,
|
|
28
|
+
up: 19,
|
|
29
|
+
down: 20,
|
|
30
|
+
left: 21,
|
|
31
|
+
right: 22,
|
|
32
|
+
volumeup: 24,
|
|
33
|
+
volumedown: 25,
|
|
34
|
+
power: 26,
|
|
35
|
+
search: 84,
|
|
36
|
+
menu: 82
|
|
37
|
+
};
|
|
38
|
+
var K = `worlds.${HANDS_ID}`;
|
|
39
|
+
var HANDS_CONFIG_GROUP = {
|
|
40
|
+
id: `world:${HANDS_ID}`,
|
|
41
|
+
owner: `world:${HANDS_ID}`,
|
|
42
|
+
schema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
title: "\u624B",
|
|
45
|
+
properties: {
|
|
46
|
+
[`${K}.actuator`]: {
|
|
47
|
+
type: "string",
|
|
48
|
+
title: "\u624B\u8D70\u54EA\u6761\u901A\u9053",
|
|
49
|
+
enum: ["auto", "root", "eyes", "shell", "off"],
|
|
50
|
+
description: "auto \u4F9D\u6B21\u8BD5 root\uFF08su \u2192 input\uFF09\u3001\u773C\u775B\uFF08\u65E0\u969C\u788D\u624B\u52BF\u7AEF\u70B9\uFF09\u3001shell\uFF08\u81EA\u5B9A\u4E49\u547D\u4EE4\uFF09\uFF1Beyes \u9700\u8981 Coopanin \u7684\u65E0\u969C\u788D\u670D\u52A1\u5E26 /hand/* \u624B\u52BF\u7AEF\u70B9\uFF1Broot \u9700\u8981\u672C\u673A\u6709 root \u5E76\u6388\u6743\u7ED9 Coopanin\uFF1Bshell \u7528\u4E0B\u9762\u7684\u547D\u4EE4\u6A21\u677F\u3002",
|
|
51
|
+
"x-hot": false
|
|
52
|
+
},
|
|
53
|
+
[`${K}.rootCommand`]: {
|
|
54
|
+
type: "string",
|
|
55
|
+
title: "root \u547D\u4EE4",
|
|
56
|
+
description: "\u4E00\u822C\u662F su\uFF1B\u4E5F\u53EF\u662F su \u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002\u53EA\u5728 actuator \u662F auto \u6216 root \u65F6\u7528\u3002",
|
|
57
|
+
"x-hot": false
|
|
58
|
+
},
|
|
59
|
+
[`${K}.shellCommand`]: {
|
|
60
|
+
type: "string",
|
|
61
|
+
title: "shell \u547D\u4EE4\u6A21\u677F",
|
|
62
|
+
description: '\u4F8B\u5982 rish -c "{cmd}" \u6216 adb -s 127.0.0.1:5555 shell "{cmd}"\u3002{cmd} \u4F1A\u6362\u6210\u4E00\u6761\u5DF2\u7ECF\u6821\u9A8C\u8FC7\u7684 input \u547D\u4EE4\u3002',
|
|
63
|
+
"x-hot": false
|
|
64
|
+
},
|
|
65
|
+
[`${K}.token`]: {
|
|
66
|
+
type: "string",
|
|
67
|
+
title: "\u624B\u52BF\u4EE4\u724C",
|
|
68
|
+
description: "\u72EC\u7ACB\u624B\u52BF App\uFF08\u624B\uFF09\u754C\u9762\u4E0A\u663E\u793A\u7684\u90A3\u4E32\u4EE4\u724C\u3002\u586B\u4E86\u5C31\u76F4\u63A5\u7528\uFF1B\u7559\u7A7A\u5219\u53BB\u8BFB\u4E0B\u9762\u7684\u4EE4\u724C\u6587\u4EF6\u3002",
|
|
69
|
+
"x-hot": false
|
|
70
|
+
},
|
|
71
|
+
[`${K}.tokenFile`]: {
|
|
72
|
+
type: "string",
|
|
73
|
+
title: "\u624B\u52BF\u4EE4\u724C\u6587\u4EF6",
|
|
74
|
+
description: "\u65E0\u969C\u788D\u670D\u52A1\u91CC\u5982\u679C\u6709\u4EBA\u5F80 files/hand.token \u5199\u4E86\u4EE4\u724C\uFF0C\u624B\u4F1A\u8BFB\u8FD9\u4E2A\u6587\u4EF6\u5E76\u5728\u6BCF\u4E2A\u624B\u52BF\u8BF7\u6C42\u4E0A\u5E26\u4E0A\u5B83\uFF1B\u7559\u7A7A\u5C31\u4E0D\u5E26\u3002\u88C5\u7684\u65F6\u5019\u6CA1\u751F\u6210\u8FD9\u4E2A\u6587\u4EF6\u4E5F\u80FD\u7528\u3002",
|
|
75
|
+
"x-hot": false
|
|
76
|
+
},
|
|
77
|
+
[`${K}.eyesPort`]: {
|
|
78
|
+
type: "integer",
|
|
79
|
+
title: "\u773C\u775B\u7AEF\u53E3",
|
|
80
|
+
minimum: 1024,
|
|
81
|
+
maximum: 65535,
|
|
82
|
+
description: "\u622A\u5C4F\u3001\u8BFB\u5B57\u548C\u624B\u52BF\u7AEF\u70B9\u90FD\u5728\u8FD9\u4E2A\u53E3\u4E0A\uFF0C\u968F\u5305\u7684\u65E0\u969C\u788D\u670D\u52A1\u7528 17898\u3002",
|
|
83
|
+
"x-hot": false
|
|
84
|
+
},
|
|
85
|
+
[`${K}.sensePort`]: {
|
|
86
|
+
type: "integer",
|
|
87
|
+
title: "\u611F\u89C9\u7AEF\u53E3",
|
|
88
|
+
minimum: 1024,
|
|
89
|
+
maximum: 65535,
|
|
90
|
+
description: "\u524D\u53F0\u5E94\u7528\u3001\u5C4F\u5E55\u4EAE\u7740\u6CA1\u6709\u3001\u7535\u91CF\u3002\u968F\u5305\u7684\u6865\u7528 17899\u3002",
|
|
91
|
+
"x-hot": false
|
|
92
|
+
},
|
|
93
|
+
[`${K}.dryRun`]: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
title: "\u6F14\u7EC3\uFF08\u53EA\u770B\u4E0D\u505A\uFF09",
|
|
96
|
+
description: "\u5F00\u7740\u65F6\u624B\u628A\u300C\u672C\u6765\u8981\u505A\u4EC0\u4E48\u300D\u8BF4\u51FA\u6765\uFF0C\u4F46\u4E00\u4E2A\u52A8\u4F5C\u90FD\u4E0D\u53D1\u3002",
|
|
97
|
+
"x-hot": true
|
|
98
|
+
},
|
|
99
|
+
[`${K}.maxActionsPerMinute`]: {
|
|
100
|
+
type: "integer",
|
|
101
|
+
title: "\u6BCF\u5206\u949F\u6700\u591A\u51E0\u4E2A\u52A8\u4F5C",
|
|
102
|
+
minimum: 1,
|
|
103
|
+
maximum: 300,
|
|
104
|
+
"x-suffix": "\u4E2A",
|
|
105
|
+
"x-hot": true
|
|
106
|
+
},
|
|
107
|
+
[`${K}.maxTextLength`]: {
|
|
108
|
+
type: "integer",
|
|
109
|
+
title: "\u4E00\u6B21\u6700\u591A\u8F93\u5165\u51E0\u4E2A\u5B57",
|
|
110
|
+
minimum: 1,
|
|
111
|
+
maximum: 1e3,
|
|
112
|
+
"x-suffix": "\u5B57",
|
|
113
|
+
description: "input text \u53EA\u5403\u5F97\u4E0B\u53EF\u6253\u5370 ASCII\uFF1B\u4E2D\u6587\u8981\u9760\u8F93\u5165\u6CD5\uFF0C\u624B\u53EA\u8D1F\u8D23\u6572\u952E\u76D8\u3002",
|
|
114
|
+
"x-hot": true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var HANDS_LOOK_CONFIG_GROUP = {
|
|
120
|
+
id: `world:${HANDS_ID}:look`,
|
|
121
|
+
owner: `world:${HANDS_ID}`,
|
|
122
|
+
schema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
title: "\u770B\u5C4F\u5E55",
|
|
125
|
+
properties: {
|
|
126
|
+
[`${K}.lookAttach`]: {
|
|
127
|
+
type: "boolean",
|
|
128
|
+
title: "\u628A\u753B\u9762\u4E00\u8D77\u4EA4\u7ED9\u6A21\u578B",
|
|
129
|
+
description: "\u5173\u6389\u53EA\u9001\u5C4F\u5E55\u4E0A\u7684\u6587\u5B57\uFF0C\u7701 token\uFF1B\u5F00\u7740\u6A21\u578B\u80FD\u771F\u7684\u770B\u5230\u753B\u9762\uFF0C\u7784\u5F97\u66F4\u51C6\u3002",
|
|
130
|
+
"x-hot": true
|
|
131
|
+
},
|
|
132
|
+
[`${K}.lookMaxWidth`]: {
|
|
133
|
+
type: "integer",
|
|
134
|
+
title: "\u753B\u9762\u5BBD\u5EA6\u4E0A\u9650",
|
|
135
|
+
minimum: 240,
|
|
136
|
+
maximum: 1440,
|
|
137
|
+
"x-suffix": "px",
|
|
138
|
+
"x-hot": true
|
|
139
|
+
},
|
|
140
|
+
[`${K}.lookQuality`]: {
|
|
141
|
+
type: "integer",
|
|
142
|
+
title: "\u753B\u9762\u8D28\u91CF",
|
|
143
|
+
minimum: 30,
|
|
144
|
+
maximum: 90,
|
|
145
|
+
"x-suffix": "JPEG",
|
|
146
|
+
"x-hot": true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/world.ts
|
|
153
|
+
import { fileURLToPath } from "node:url";
|
|
154
|
+
|
|
155
|
+
// src/hands.ts
|
|
156
|
+
import { execFile } from "node:child_process";
|
|
157
|
+
import { readFileSync } from "node:fs";
|
|
158
|
+
import { get as httpGet } from "node:http";
|
|
159
|
+
var ACTION_TIMEOUT_MS = 15e3;
|
|
160
|
+
var PROBE_TTL_MS = 2e4;
|
|
161
|
+
var COORD_LIMIT = 2e4;
|
|
162
|
+
var MIN_GESTURE_MS = 20;
|
|
163
|
+
var MAX_GESTURE_MS = 2e4;
|
|
164
|
+
var HandError = class extends Error {
|
|
165
|
+
};
|
|
166
|
+
function get(port, path, timeoutMs) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const req = httpGet({ host: "127.0.0.1", port, path, timeout: timeoutMs }, (res) => {
|
|
169
|
+
const chunks = [];
|
|
170
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
171
|
+
res.on("end", () => {
|
|
172
|
+
resolve({
|
|
173
|
+
status: res.statusCode ?? 0,
|
|
174
|
+
body: Buffer.concat(chunks),
|
|
175
|
+
header: (name) => {
|
|
176
|
+
const value = res.headers[name.toLowerCase()];
|
|
177
|
+
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
req.on("timeout", () => req.destroy(new Error(`\u7B49\u592A\u4E45\uFF08${timeoutMs}ms\uFF09`)));
|
|
183
|
+
req.on("error", (err) => reject(err));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function getJson(port, path, timeoutMs = 6e3) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await get(port, path, timeoutMs);
|
|
189
|
+
if (res.status !== 200) return null;
|
|
190
|
+
return JSON.parse(res.body.toString("utf8"));
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
var flat = (value, max = 200) => {
|
|
196
|
+
if (typeof value !== "string") return "";
|
|
197
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
198
|
+
return text.length <= max ? text : `${text.slice(0, max)}\u2026`;
|
|
199
|
+
};
|
|
200
|
+
function parseBounds(raw) {
|
|
201
|
+
if (!raw) return null;
|
|
202
|
+
const nums = raw.match(/-?\d+/g);
|
|
203
|
+
if (!nums || nums.length !== 4) return null;
|
|
204
|
+
const [l, t, r, b] = nums.map((n) => Number.parseInt(n, 10));
|
|
205
|
+
if (![l, t, r, b].every((n) => Number.isFinite(n))) return null;
|
|
206
|
+
if (r <= l || b <= t) return null;
|
|
207
|
+
return { l, t, r, b };
|
|
208
|
+
}
|
|
209
|
+
function coord(value, limit = COORD_LIMIT) {
|
|
210
|
+
const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
211
|
+
if (!Number.isFinite(n)) throw new HandError("\u5750\u6807\u5F97\u662F\u6570\u5B57\u3002");
|
|
212
|
+
const rounded = Math.round(n);
|
|
213
|
+
if (rounded < -limit || rounded > limit) throw new HandError(`\u5750\u6807 ${rounded} \u4E0D\u53EF\u80FD\u5728\u5C4F\u5E55\u4E0A\u3002`);
|
|
214
|
+
return rounded;
|
|
215
|
+
}
|
|
216
|
+
function milliseconds(value, fallback) {
|
|
217
|
+
const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
218
|
+
if (!Number.isFinite(n)) return fallback;
|
|
219
|
+
return Math.max(MIN_GESTURE_MS, Math.min(MAX_GESTURE_MS, Math.round(n)));
|
|
220
|
+
}
|
|
221
|
+
function escapeInputText(text) {
|
|
222
|
+
if (!text) throw new HandError("\u8981\u8F93\u5165\u7684\u6587\u5B57\u662F\u7A7A\u7684\u3002");
|
|
223
|
+
for (const ch of text) {
|
|
224
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
225
|
+
if (code < 32 || code > 126) throw new HandError(`\u300C${ch}\u300D\u4E0D\u662F\u53EF\u6253\u5370 ASCII\uFF1Binput text \u6572\u4E0D\u8FDB\u53BB\uFF0C\u8FD9\u6761\u901A\u9053\u53EA\u9002\u5408\u82F1\u6587\u3001\u6570\u5B57\u548C\u7B26\u53F7\u3002`);
|
|
226
|
+
if (ch === "'" || ch === "\\") throw new HandError(`\u300C${ch}\u300D\u4F1A\u88AB shell \u5403\u6389\uFF0C\u6362\u4E2A\u522B\u7684\u65B9\u5F0F\u8F93\u5165\u5B83\u3002`);
|
|
227
|
+
}
|
|
228
|
+
return text.replace(/ /g, "%s");
|
|
229
|
+
}
|
|
230
|
+
function keyCode(name) {
|
|
231
|
+
const key = String(name ?? "").trim().toLowerCase().replace(/[\s_-]/g, "");
|
|
232
|
+
const code = KEY_CODES[key];
|
|
233
|
+
if (code === void 0) throw new HandError(`\u4E0D\u8BA4\u8FD9\u4E2A\u6309\u952E\uFF1A${name || "(\u7A7A)"}\u3002\u80FD\u6309\u7684\u662F ${Object.keys(KEY_CODES).join("\u3001")}\u3002`);
|
|
234
|
+
return code;
|
|
235
|
+
}
|
|
236
|
+
function inputCommand(command) {
|
|
237
|
+
switch (command.kind) {
|
|
238
|
+
// 同一点上按住不放,对 input 就是一次等时长的 swipe
|
|
239
|
+
case "tap":
|
|
240
|
+
return command.ms >= 400 ? `input swipe ${command.x} ${command.y} ${command.x} ${command.y} ${command.ms}` : `input tap ${command.x} ${command.y}`;
|
|
241
|
+
case "swipe":
|
|
242
|
+
return `input swipe ${command.x1} ${command.y1} ${command.x2} ${command.y2} ${command.ms}`;
|
|
243
|
+
case "key":
|
|
244
|
+
return `input keyevent ${command.code}`;
|
|
245
|
+
// 文本在拼命令这一刻才转义:只有 input 这条路吃不下非 ASCII,无障碍那条能原样收
|
|
246
|
+
case "text":
|
|
247
|
+
return `input text '${escapeInputText(command.value)}'`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
var RootActuator = class {
|
|
251
|
+
kind = "root";
|
|
252
|
+
su;
|
|
253
|
+
log;
|
|
254
|
+
constructor(su, log) {
|
|
255
|
+
this.su = su;
|
|
256
|
+
this.log = log;
|
|
257
|
+
}
|
|
258
|
+
run(argv, timeoutMs = ACTION_TIMEOUT_MS) {
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
execFile(argv[0], argv.slice(1), { timeout: timeoutMs, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
|
|
261
|
+
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
262
|
+
resolve({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async probe() {
|
|
267
|
+
const su = this.su().trim() || "su";
|
|
268
|
+
const r = await this.run([su, "-c", "id"], 8e3);
|
|
269
|
+
if (r.code !== 0) {
|
|
270
|
+
const why = flat(r.err || r.out, 120);
|
|
271
|
+
return { ok: false, detail: why ? `su \u8DD1\u4E0D\u8D77\u6765\uFF08${why}\uFF09` : "su \u8DD1\u4E0D\u8D77\u6765\uFF1A\u53EF\u80FD\u6CA1 root\uFF0C\u6216\u8005\u6CA1\u6388\u6743\u7ED9 Coopanin\u3002" };
|
|
272
|
+
}
|
|
273
|
+
const id = flat(r.out, 120);
|
|
274
|
+
if (!/\buid=0\b/.test(id)) return { ok: false, detail: `su \u7ED9\u7684\u4E0D\u662F root\uFF08${id || "\u6CA1\u6709\u8F93\u51FA"}\uFF09` };
|
|
275
|
+
return { ok: true, detail: "root \u53EF\u7528\uFF08su \u2192 input\uFF09" };
|
|
276
|
+
}
|
|
277
|
+
async apply(command) {
|
|
278
|
+
const line = inputCommand(command);
|
|
279
|
+
const su = this.su().trim() || "su";
|
|
280
|
+
const r = await this.run([su, "-c", line]);
|
|
281
|
+
if (r.code !== 0) {
|
|
282
|
+
const why = flat(r.err || r.out, 160);
|
|
283
|
+
throw new HandError(`input \u6CA1\u6210\u529F\uFF08${why || `\u9000\u51FA\u7801 ${r.code}`}\uFF09\u3002`);
|
|
284
|
+
}
|
|
285
|
+
this.log?.()?.debug("\u624B\uFF1Aroot \u52A8\u4F5C\u5DF2\u53D1\u51FA", { line });
|
|
286
|
+
return { ok: true, detail: "root" };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var ShellActuator = class {
|
|
290
|
+
kind = "shell";
|
|
291
|
+
template;
|
|
292
|
+
log;
|
|
293
|
+
constructor(template, log) {
|
|
294
|
+
this.template = template;
|
|
295
|
+
this.log = log;
|
|
296
|
+
}
|
|
297
|
+
argv(command) {
|
|
298
|
+
const template = this.template().trim();
|
|
299
|
+
if (!template) throw new HandError("shell \u901A\u9053\u6CA1\u6709\u914D\u547D\u4EE4\u6A21\u677F\uFF08\u8BBE\u7F6E \u2192 \u624B \u2192 shell \u547D\u4EE4\u6A21\u677F\uFF09\u3002");
|
|
300
|
+
const line = template.replace(/\{cmd\}/g, inputCommand(command));
|
|
301
|
+
return ["sh", "-c", line];
|
|
302
|
+
}
|
|
303
|
+
async probe() {
|
|
304
|
+
const template = this.template().trim();
|
|
305
|
+
if (!template) return { ok: false, detail: "shell \u901A\u9053\u6CA1\u914D\u6A21\u677F" };
|
|
306
|
+
if (!template.includes("{cmd}")) return { ok: false, detail: "shell \u6A21\u677F\u91CC\u6CA1\u6709 {cmd}\uFF0C\u4E0D\u77E5\u9053\u628A\u547D\u4EE4\u653E\u54EA" };
|
|
307
|
+
const probe = template.replace(/\{cmd\}/g, "id");
|
|
308
|
+
const r = await new Promise((resolve) => {
|
|
309
|
+
execFile("sh", ["-c", probe], { timeout: 8e3, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
|
|
310
|
+
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
311
|
+
resolve({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
if (r.code !== 0) return { ok: false, detail: flat(r.err || r.out, 140) || "\u6A21\u677F\u8DD1\u4E0D\u8D77\u6765" };
|
|
315
|
+
const id = flat(r.out, 120);
|
|
316
|
+
return { ok: true, detail: id ? `shell \u901A\u9053\u53EF\u7528\uFF08${id}\uFF09` : "shell \u901A\u9053\u53EF\u7528" };
|
|
317
|
+
}
|
|
318
|
+
async apply(command) {
|
|
319
|
+
const argv = this.argv(command);
|
|
320
|
+
const r = await new Promise((resolve) => {
|
|
321
|
+
execFile(argv[0], argv.slice(1), { timeout: ACTION_TIMEOUT_MS, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
|
|
322
|
+
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
323
|
+
resolve({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
if (r.code !== 0) throw new HandError(`\u901A\u9053\u547D\u4EE4\u6CA1\u6210\u529F\uFF08${flat(r.err || r.out, 160) || `\u9000\u51FA\u7801 ${r.code}`}\uFF09\u3002`);
|
|
327
|
+
this.log?.()?.debug("\u624B\uFF1Ashell \u52A8\u4F5C\u5DF2\u53D1\u51FA", { argv: argv.join(" ") });
|
|
328
|
+
return { ok: true, detail: "shell" };
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var EyesActuator = class {
|
|
332
|
+
kind = "eyes";
|
|
333
|
+
port;
|
|
334
|
+
token;
|
|
335
|
+
log;
|
|
336
|
+
constructor(port, token, log) {
|
|
337
|
+
this.port = port;
|
|
338
|
+
this.token = token;
|
|
339
|
+
this.log = log;
|
|
340
|
+
}
|
|
341
|
+
/** 带令牌(服务里生成了才带)。 */
|
|
342
|
+
withToken(path) {
|
|
343
|
+
const token = this.token();
|
|
344
|
+
if (!token) return path;
|
|
345
|
+
return `${path}${path.includes("?") ? "&" : "?"}t=${encodeURIComponent(token)}`;
|
|
346
|
+
}
|
|
347
|
+
async send(raw) {
|
|
348
|
+
const path = this.withToken(raw);
|
|
349
|
+
const res = await get(this.port(), path, ACTION_TIMEOUT_MS);
|
|
350
|
+
if (res.status === 404) throw new HandError("\u65E0\u969C\u788D\u670D\u52A1\u6CA1\u6709\u624B\u52BF\u7AEF\u70B9\uFF08\u88C5\u7684\u662F\u300C\u53EA\u770B\u300D\u7684\u7248\u672C\uFF09\u3002");
|
|
351
|
+
if (res.status === 409) throw new HandError("\u5C4F\u5E55\u4E0A\u6CA1\u6709\u6B63\u5728\u8F93\u5165\u7684\u8F93\u5165\u6846\uFF0C\u5148\u70B9\u4E00\u4E0B\u8981\u5199\u7684\u5730\u65B9\u3002");
|
|
352
|
+
if (res.status !== 200) {
|
|
353
|
+
const body2 = flat(res.body.toString("utf8"), 140);
|
|
354
|
+
throw new HandError(`\u65E0\u969C\u788D\u670D\u52A1\u56DE\u4E86 ${res.status}${body2 ? `\uFF1A${body2}` : ""}\u3002`);
|
|
355
|
+
}
|
|
356
|
+
let body = {};
|
|
357
|
+
try {
|
|
358
|
+
body = JSON.parse(res.body.toString("utf8"));
|
|
359
|
+
} catch {
|
|
360
|
+
return { ok: true, detail: "" };
|
|
361
|
+
}
|
|
362
|
+
if (body.ok === false) throw new HandError(flat(body.error || body.detail, 160) || "\u65E0\u969C\u788D\u670D\u52A1\u6CA1\u505A\u6210\u8FD9\u4E2A\u52A8\u4F5C\u3002");
|
|
363
|
+
this.log?.()?.debug("\u624B\uFF1A\u65E0\u969C\u788D\u52A8\u4F5C\u5DF2\u53D1\u51FA", { path });
|
|
364
|
+
return { ok: true, detail: flat(body.detail, 60) };
|
|
365
|
+
}
|
|
366
|
+
async probe() {
|
|
367
|
+
const health = await getJson(this.port(), "/health", 4e3);
|
|
368
|
+
if (!health) return { ok: false, detail: "\u65E0\u969C\u788D\u670D\u52A1\u6CA1\u5728\u8DD1\uFF08\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u65E0\u969C\u788D\u91CC\u53EF\u80FD\u6CA1\u5F00\uFF09" };
|
|
369
|
+
if (health.canGesture !== true) return { ok: false, detail: "\u65E0\u969C\u788D\u670D\u52A1\u5728\u8DD1\uFF0C\u4F46\u8FD9\u4E2A\u7248\u672C\u53EA\u6709\u300C\u770B\u300D\uFF0C\u6CA1\u6709\u624B\u52BF\uFF08/hand/* \u7AEF\u70B9\uFF09" };
|
|
370
|
+
return { ok: true, detail: "\u65E0\u969C\u788D\u624B\u52BF\u53EF\u7528" };
|
|
371
|
+
}
|
|
372
|
+
async apply(command) {
|
|
373
|
+
switch (command.kind) {
|
|
374
|
+
case "tap":
|
|
375
|
+
return this.send(`/hand/tap?x=${command.x}&y=${command.y}&ms=${command.ms}`);
|
|
376
|
+
case "swipe":
|
|
377
|
+
return this.send(`/hand/swipe?x1=${command.x1}&y1=${command.y1}&x2=${command.x2}&y2=${command.y2}&ms=${command.ms}`);
|
|
378
|
+
case "key":
|
|
379
|
+
return this.send(`/hand/key?name=${encodeURIComponent(command.name)}&code=${command.code}`);
|
|
380
|
+
case "text":
|
|
381
|
+
return this.send(`/hand/text?value=${encodeURIComponent(command.value)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var EyesClient = class {
|
|
386
|
+
port;
|
|
387
|
+
constructor(port) {
|
|
388
|
+
this.port = port;
|
|
389
|
+
}
|
|
390
|
+
health() {
|
|
391
|
+
return getJson(this.port(), "/health", 4e3);
|
|
392
|
+
}
|
|
393
|
+
async state(sensePort) {
|
|
394
|
+
const body = await getJson(sensePort, "/state", 4e3);
|
|
395
|
+
if (!body) return null;
|
|
396
|
+
return {
|
|
397
|
+
appPkg: flat(body.app?.pkg, 120),
|
|
398
|
+
appLabel: flat(body.app?.label, 60),
|
|
399
|
+
screenOn: body.screen?.on !== false,
|
|
400
|
+
locked: body.screen?.locked === true,
|
|
401
|
+
battery: typeof body.power?.level === "number" ? body.power.level : void 0
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async ui() {
|
|
405
|
+
const body = await getJson(this.port(), "/ui", 6e3);
|
|
406
|
+
if (!body) return { ok: false, pkg: "", items: [], error: "\u8BFB\u4E0D\u5230\u5C4F\u5E55\u4E0A\u7684\u6587\u5B57" };
|
|
407
|
+
const items = [];
|
|
408
|
+
for (const raw of body.items ?? []) {
|
|
409
|
+
const text = flat(raw?.t, 160);
|
|
410
|
+
if (!text) continue;
|
|
411
|
+
const bounds = flat(raw?.b, 60) || void 0;
|
|
412
|
+
const box = parseBounds(bounds);
|
|
413
|
+
items.push({
|
|
414
|
+
text,
|
|
415
|
+
cls: flat(raw?.c, 40) || void 0,
|
|
416
|
+
bounds,
|
|
417
|
+
...box ? { center: { x: Math.round((box.l + box.r) / 2), y: Math.round((box.t + box.b) / 2) } } : {}
|
|
418
|
+
});
|
|
419
|
+
if (items.length >= 120) break;
|
|
420
|
+
}
|
|
421
|
+
return { ok: body.ok !== false, pkg: flat(body.pkg, 120), items };
|
|
422
|
+
}
|
|
423
|
+
async frame(maxWidth, quality) {
|
|
424
|
+
try {
|
|
425
|
+
const w = Math.max(160, Math.min(2160, Math.round(maxWidth)));
|
|
426
|
+
const q = Math.max(20, Math.min(95, Math.round(quality)));
|
|
427
|
+
const res = await get(this.port(), `/screen.jpg?max=${w}&q=${q}`, 12e3);
|
|
428
|
+
if (res.status !== 200 || res.body.length === 0) return null;
|
|
429
|
+
return {
|
|
430
|
+
bytes: new Uint8Array(res.body),
|
|
431
|
+
width: Number.parseInt(res.header("x-width"), 10) || 0,
|
|
432
|
+
height: Number.parseInt(res.header("x-height"), 10) || 0,
|
|
433
|
+
pkg: flat(res.header("x-pkg"), 120),
|
|
434
|
+
ageMs: Number.parseInt(res.header("x-age-ms"), 10) || 0
|
|
435
|
+
};
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
var describeCommand = (command) => {
|
|
442
|
+
switch (command.kind) {
|
|
443
|
+
case "tap":
|
|
444
|
+
return `\u70B9 (${command.x},${command.y})`;
|
|
445
|
+
case "swipe":
|
|
446
|
+
return `\u4ECE (${command.x1},${command.y1}) \u6ED1\u5230 (${command.x2},${command.y2})`;
|
|
447
|
+
case "key":
|
|
448
|
+
return `\u6309 ${command.name}`;
|
|
449
|
+
case "text":
|
|
450
|
+
return `\u8F93\u5165\u300C${command.value}\u300D`;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var Hands = class {
|
|
454
|
+
eyes;
|
|
455
|
+
rootActuator;
|
|
456
|
+
shellActuator;
|
|
457
|
+
eyesActuator;
|
|
458
|
+
/** 每分钟的动作用时间戳记;超过上限就停手。 */
|
|
459
|
+
stamps = [];
|
|
460
|
+
actuatorState = { kind: "off", configured: "off", ok: false, detail: "\u8FD8\u6CA1\u63A2\u8FC7", checkedAt: 0 };
|
|
461
|
+
screen = null;
|
|
462
|
+
lastScreenAt = 0;
|
|
463
|
+
looks = 0;
|
|
464
|
+
acts = 0;
|
|
465
|
+
log;
|
|
466
|
+
cfg;
|
|
467
|
+
constructor(cfg) {
|
|
468
|
+
this.cfg = cfg;
|
|
469
|
+
this.eyes = new EyesClient(() => this.port(this.cfg().eyesPort));
|
|
470
|
+
this.rootActuator = new RootActuator(() => this.cfg().rootCommand, () => this.log);
|
|
471
|
+
this.shellActuator = new ShellActuator(() => this.cfg().shellCommand, () => this.log);
|
|
472
|
+
this.eyesActuator = new EyesActuator(() => this.port(this.cfg().eyesPort), () => this.readToken(), () => this.log);
|
|
473
|
+
}
|
|
474
|
+
port(value) {
|
|
475
|
+
const n = Math.round(Number(value));
|
|
476
|
+
return Number.isFinite(n) && n >= 1024 && n <= 65535 ? n : 17898;
|
|
477
|
+
}
|
|
478
|
+
/** 世界挂上以后把日志接进来;探测和动作都写在这里。 */
|
|
479
|
+
attachLog(log) {
|
|
480
|
+
this.log = log;
|
|
481
|
+
}
|
|
482
|
+
/** 在屏幕上的字里找一段;只有读得出坐标的才算命中。 */
|
|
483
|
+
async findText(needle) {
|
|
484
|
+
const query = needle.trim().toLowerCase();
|
|
485
|
+
if (!query) return [];
|
|
486
|
+
const ui = await this.eyes.ui();
|
|
487
|
+
if (!ui.ok) throw new HandError(ui.error ?? "\u8BFB\u4E0D\u5230\u5C4F\u5E55\u4E0A\u7684\u5B57\u3002");
|
|
488
|
+
const hits = [];
|
|
489
|
+
for (const item of ui.items) {
|
|
490
|
+
if (!item.center) continue;
|
|
491
|
+
if (!item.text.toLowerCase().includes(query)) continue;
|
|
492
|
+
hits.push({ text: item.text, center: item.center });
|
|
493
|
+
if (hits.length >= 20) break;
|
|
494
|
+
}
|
|
495
|
+
return hits;
|
|
496
|
+
}
|
|
497
|
+
/** 手势令牌:文件里有就用它;读不到就是空(服务那边也不校验)。 */
|
|
498
|
+
tokenCache = { value: "", at: 0 };
|
|
499
|
+
readToken() {
|
|
500
|
+
const literal = this.cfg().token?.trim();
|
|
501
|
+
if (literal) return literal;
|
|
502
|
+
const file = this.cfg().tokenFile?.trim();
|
|
503
|
+
if (!file) return "";
|
|
504
|
+
if (Date.now() - this.tokenCache.at < 3e4) return this.tokenCache.value;
|
|
505
|
+
let value = "";
|
|
506
|
+
try {
|
|
507
|
+
const text = readFileSync(file, "utf8").trim();
|
|
508
|
+
if (text && text.length <= 200) value = text;
|
|
509
|
+
} catch {
|
|
510
|
+
value = "";
|
|
511
|
+
}
|
|
512
|
+
this.tokenCache = { value, at: Date.now() };
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
/** 通道现状;Probe 过就返回缓存。 */
|
|
516
|
+
state() {
|
|
517
|
+
return this.actuatorState;
|
|
518
|
+
}
|
|
519
|
+
stats() {
|
|
520
|
+
this.prune();
|
|
521
|
+
return { acts: this.acts, looks: this.looks, inWindow: this.stamps.length };
|
|
522
|
+
}
|
|
523
|
+
/** 选通道并验证。auto 依次试 root、眼睛、shell;结果缓存 20 秒。 */
|
|
524
|
+
async probe(force = false) {
|
|
525
|
+
const cfg = this.cfg();
|
|
526
|
+
const want = cfg.actuator;
|
|
527
|
+
const now = Date.now();
|
|
528
|
+
if (!force && want === this.actuatorState.configured && now - this.actuatorState.checkedAt < PROBE_TTL_MS) return this.actuatorState;
|
|
529
|
+
const candidates = want === "auto" ? ["root", "eyes", "shell"] : want === "off" ? [] : [want];
|
|
530
|
+
if (candidates.length === 0) {
|
|
531
|
+
this.actuatorState = { kind: "off", configured: want, ok: false, detail: "\u624B\u5173\u7740\uFF08actuator=off\uFF09", checkedAt: now };
|
|
532
|
+
return this.actuatorState;
|
|
533
|
+
}
|
|
534
|
+
const notes = [];
|
|
535
|
+
for (const kind of candidates) {
|
|
536
|
+
const actuator = kind === "root" ? this.rootActuator : kind === "eyes" ? this.eyesActuator : this.shellActuator;
|
|
537
|
+
let result;
|
|
538
|
+
try {
|
|
539
|
+
result = await actuator.probe();
|
|
540
|
+
} catch (err) {
|
|
541
|
+
result = { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
542
|
+
}
|
|
543
|
+
notes.push(`${kind}: ${result.detail}`);
|
|
544
|
+
if (result.ok) {
|
|
545
|
+
this.actuatorState = { kind, configured: want, ok: true, detail: result.detail, checkedAt: now };
|
|
546
|
+
this.log?.info(`\u624B\u80FD\u7528\u4E86\uFF1A${kind}\uFF08${result.detail}\uFF09`);
|
|
547
|
+
return this.actuatorState;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
this.actuatorState = { kind: want, configured: want, ok: false, detail: notes.join("\uFF1B"), checkedAt: now };
|
|
551
|
+
this.log?.warn(`\u624B\u6682\u65F6\u52A8\u4E0D\u4E86\uFF1A${this.actuatorState.detail}`);
|
|
552
|
+
return this.actuatorState;
|
|
553
|
+
}
|
|
554
|
+
async actuator() {
|
|
555
|
+
const state = await this.probe();
|
|
556
|
+
if (!state.ok) throw new HandError(`\u624B\u73B0\u5728\u52A8\u4E0D\u4E86\uFF1A${state.detail}`);
|
|
557
|
+
const picked = state.kind === "root" ? this.rootActuator : state.kind === "eyes" ? this.eyesActuator : state.kind === "shell" ? this.shellActuator : null;
|
|
558
|
+
if (!picked) throw new HandError(`\u624B\u73B0\u5728\u52A8\u4E0D\u4E86\uFF1A${state.detail}`);
|
|
559
|
+
return picked;
|
|
560
|
+
}
|
|
561
|
+
/** 屏幕尺寸;拿不到就不校验边界(root 通道下常见)。 */
|
|
562
|
+
async screenSize() {
|
|
563
|
+
if (this.screen && Date.now() - this.lastScreenAt < 6e4) return this.screen;
|
|
564
|
+
const health = await this.eyes.health();
|
|
565
|
+
if (health?.ok && health.frameWidth && health.frameHeight) {
|
|
566
|
+
this.screen = { w: health.frameWidth, h: health.frameHeight };
|
|
567
|
+
this.lastScreenAt = Date.now();
|
|
568
|
+
return this.screen;
|
|
569
|
+
}
|
|
570
|
+
const frame = await this.eyes.frame(240, 30);
|
|
571
|
+
if (frame && frame.width > 0 && frame.height > 0) {
|
|
572
|
+
this.screen = { w: frame.width, h: frame.height };
|
|
573
|
+
this.lastScreenAt = Date.now();
|
|
574
|
+
return this.screen;
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
prune() {
|
|
579
|
+
const cutoff = Date.now() - 6e4;
|
|
580
|
+
while (this.stamps.length && (this.stamps[0] ?? 0) < cutoff) this.stamps.shift();
|
|
581
|
+
}
|
|
582
|
+
/** 限速:一分钟内动作超过上限就停手,等窗口滑过去。 */
|
|
583
|
+
async throttle() {
|
|
584
|
+
const max = Math.max(1, Math.round(this.cfg().maxActionsPerMinute) || 30);
|
|
585
|
+
this.prune();
|
|
586
|
+
if (this.stamps.length >= max) {
|
|
587
|
+
const oldest = this.stamps[0] ?? Date.now();
|
|
588
|
+
const waitMs = Math.max(0, 6e4 - (Date.now() - oldest));
|
|
589
|
+
throw new HandError(`\u8FD9\u4E00\u5206\u949F\u5DF2\u7ECF\u52A8\u4E86 ${this.stamps.length} \u4E0B\uFF08\u4E0A\u9650 ${max}\uFF09\uFF0C${Math.ceil(waitMs / 1e3)} \u79D2\u540E\u518D\u6765\u3002`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/** 动作前先看屏幕亮着没有、锁着没有;锁屏时不该动手。 */
|
|
593
|
+
async screenGuard() {
|
|
594
|
+
const state = await this.eyes.state(this.port(this.cfg().sensePort));
|
|
595
|
+
if (!state) return "";
|
|
596
|
+
if (state.locked) throw new HandError("\u624B\u673A\u9501\u7740\uFF0C\u624B\u70B9\u4E0D\u5230\u4E1C\u897F\uFF1B\u5148\u89E3\u5F00\u3002");
|
|
597
|
+
if (!state.screenOn) throw new HandError("\u5C4F\u5E55\u9ED1\u7740\uFF0C\u624B\u70B9\u4E0D\u5230\u4E1C\u897F\uFF1B\u5148\u70B9\u4EAE\u3002");
|
|
598
|
+
return state.appLabel ? `${state.appLabel}\uFF08${state.appPkg}\uFF09` : state.appPkg;
|
|
599
|
+
}
|
|
600
|
+
async bounds(x, y) {
|
|
601
|
+
const screen = await this.screenSize();
|
|
602
|
+
if (!screen) return;
|
|
603
|
+
if (x < 0 || y < 0 || x > screen.w || y > screen.h) {
|
|
604
|
+
throw new HandError(`(${x},${y}) \u5728\u5C4F\u5E55\u5916\uFF1A\u8FD9\u5757\u5C4F\u662F ${screen.w}\xD7${screen.h}\u3002`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async tap(x, y, opts = {}) {
|
|
608
|
+
const px = coord(x);
|
|
609
|
+
const py = coord(y);
|
|
610
|
+
await this.bounds(px, py);
|
|
611
|
+
const ms = milliseconds(opts.ms, 60);
|
|
612
|
+
return this.perform({ kind: "tap", x: px, y: py, ms });
|
|
613
|
+
}
|
|
614
|
+
async longPress(x, y, opts = {}) {
|
|
615
|
+
const px = coord(x);
|
|
616
|
+
const py = coord(y);
|
|
617
|
+
await this.bounds(px, py);
|
|
618
|
+
const ms = milliseconds(opts.ms, 800);
|
|
619
|
+
return this.perform({ kind: "tap", x: px, y: py, ms });
|
|
620
|
+
}
|
|
621
|
+
async swipe(x1, y1, x2, y2, opts = {}) {
|
|
622
|
+
const a = coord(x1);
|
|
623
|
+
const b = coord(y1);
|
|
624
|
+
const c = coord(x2);
|
|
625
|
+
const d = coord(y2);
|
|
626
|
+
await this.bounds(a, b);
|
|
627
|
+
await this.bounds(c, d);
|
|
628
|
+
const ms = milliseconds(opts.ms, 300);
|
|
629
|
+
return this.perform({ kind: "swipe", x1: a, y1: b, x2: c, y2: d, ms });
|
|
630
|
+
}
|
|
631
|
+
async press(key) {
|
|
632
|
+
const name = String(key ?? "").trim().toLowerCase().replace(/[\s_-]/g, "");
|
|
633
|
+
const code = keyCode(name);
|
|
634
|
+
return this.perform({ kind: "key", name, code });
|
|
635
|
+
}
|
|
636
|
+
async type(text) {
|
|
637
|
+
const value = String(text ?? "");
|
|
638
|
+
const cfg = this.cfg();
|
|
639
|
+
const limit = Math.max(1, Math.round(cfg.maxTextLength) || 200);
|
|
640
|
+
if (value.length > limit) throw new HandError(`\u4E00\u6B21\u6700\u591A ${limit} \u4E2A\u5B57\uFF0C\u8FD9\u6BB5\u6709 ${value.length} \u4E2A\u3002`);
|
|
641
|
+
if (!value.trim()) throw new HandError("\u8981\u8F93\u5165\u7684\u6587\u5B57\u662F\u7A7A\u7684\u3002");
|
|
642
|
+
return this.perform({ kind: "text", value });
|
|
643
|
+
}
|
|
644
|
+
async perform(command) {
|
|
645
|
+
const state = await this.probe();
|
|
646
|
+
if (!state.ok) throw new HandError(`\u624B\u73B0\u5728\u52A8\u4E0D\u4E86\uFF1A${state.detail}`);
|
|
647
|
+
const where = await this.screenGuard();
|
|
648
|
+
await this.throttle();
|
|
649
|
+
const described = describeCommand(command);
|
|
650
|
+
const cfg = this.cfg();
|
|
651
|
+
if (cfg.dryRun) {
|
|
652
|
+
this.log?.info(`\u624B\uFF08\u6F14\u7EC3\uFF09\uFF1A${described}`, { actuator: state.kind });
|
|
653
|
+
return { text: `\u6F14\u7EC3\uFF1A\u672C\u6765\u8981${described}${where ? `\uFF08\u5728 ${where} \u4E0A\uFF09` : ""}\uFF0C\u4E00\u4E2A\u52A8\u4F5C\u90FD\u6CA1\u53D1\u3002`, performed: false };
|
|
654
|
+
}
|
|
655
|
+
const actuator = await this.actuator();
|
|
656
|
+
try {
|
|
657
|
+
const result = await actuator.apply(command);
|
|
658
|
+
this.stamps.push(Date.now());
|
|
659
|
+
this.acts += 1;
|
|
660
|
+
this.log?.info(`\u624B\uFF1A${described}`, { actuator: state.kind, where });
|
|
661
|
+
const via = state.kind === "root" ? "root \u7684 input" : state.kind === "shell" ? "\u4F60\u7ED9\u7684 shell \u901A\u9053" : "\u65E0\u969C\u788D\u624B\u52BF";
|
|
662
|
+
return {
|
|
663
|
+
text: `${described}${where ? `\uFF08\u5728 ${where} \u4E0A\uFF09` : ""}\uFF0C\u901A\u8FC7${via}\u53D1\u51FA\u53BB\u4E86${result.detail && state.kind === "eyes" ? `\uFF08${result.detail}\uFF09` : ""}\u3002`,
|
|
664
|
+
performed: true
|
|
665
|
+
};
|
|
666
|
+
} catch (err) {
|
|
667
|
+
this.actuatorState = { ...this.actuatorState, ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
668
|
+
throw err;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** 看一眼屏幕。detail: both(画面加文字,默认)、screen(只要画面)、text(只要文字)。 */
|
|
672
|
+
async look(detail = "both") {
|
|
673
|
+
const cfg = this.cfg();
|
|
674
|
+
const health = await this.eyes.health();
|
|
675
|
+
if (!health?.ok) {
|
|
676
|
+
throw new HandError(`\u773C\u775B\u6CA1\u5F00\uFF1A${health?.error ?? "\u65E0\u969C\u788D\u670D\u52A1\u6CA1\u5728\u8DD1\uFF08\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u65E0\u969C\u788D\uFF09"}\u3002`);
|
|
677
|
+
}
|
|
678
|
+
const state = await this.eyes.state(this.port(cfg.sensePort));
|
|
679
|
+
const where = state?.appLabel && state.appPkg ? `${state.appLabel}\uFF08${state.appPkg}\uFF09` : state?.appPkg || "\u672A\u77E5\u5E94\u7528";
|
|
680
|
+
const parts = [`\u73B0\u5728\u524D\u53F0\u662F ${where}\u3002`];
|
|
681
|
+
if (state) {
|
|
682
|
+
parts.push(`${state.screenOn ? "\u5C4F\u5E55\u4EAE\u7740" : "\u5C4F\u5E55\u9ED1\u7740"}\u3001${state.locked ? "\u9501\u7740" : "\u6CA1\u9501"}${typeof state.battery === "number" ? `\u3001\u7535\u91CF ${state.battery}%` : ""}\u3002`);
|
|
683
|
+
}
|
|
684
|
+
let blob;
|
|
685
|
+
let frame;
|
|
686
|
+
if (detail !== "text") {
|
|
687
|
+
const f = await this.eyes.frame(cfg.lookMaxWidth, cfg.lookQuality);
|
|
688
|
+
if (f) {
|
|
689
|
+
frame = f;
|
|
690
|
+
parts.push(`\u753B\u9762 ${f.width}\xD7${f.height}${f.ageMs > 1200 ? `\uFF0C${(f.ageMs / 1e3).toFixed(1)} \u79D2\u524D\u62CD\u7684` : ""}\u3002`);
|
|
691
|
+
if (cfg.lookAttach) {
|
|
692
|
+
blob = { bytes: f.bytes, mime: "image/jpeg", name: "screen.jpg", fallbackText: "\uFF08\u8FD9\u662F\u624B\u673A\u73B0\u5728\u8FD9\u4E00\u5C4F\u7684\u622A\u56FE\uFF09" };
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
parts.push("\u8FD9\u6B21\u6CA1\u62FF\u5230\u753B\u9762\u3002");
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (detail !== "screen") {
|
|
699
|
+
const ui = await this.eyes.ui();
|
|
700
|
+
if (ui.ok && ui.items.length) {
|
|
701
|
+
const lines = ui.items.slice(0, 40).map((item) => {
|
|
702
|
+
const spot = item.center ? `@(${item.center.x},${item.center.y})` : "";
|
|
703
|
+
return `- \u300C${item.text}\u300D${spot}`;
|
|
704
|
+
});
|
|
705
|
+
parts.push(`\u5C4F\u5E55\u4E0A\u7684\u5B57\uFF08\u80FD\u8BFB\u5230\u7684\u524D ${lines.length} \u6761\uFF0C@ \u540E\u9762\u662F\u53EF\u4EE5\u70B9\u7684\u4F4D\u7F6E\uFF09\uFF1A
|
|
706
|
+
${lines.join("\n")}`);
|
|
707
|
+
} else {
|
|
708
|
+
parts.push("\u5C4F\u5E55\u4E0A\u6CA1\u8BFB\u5230\u80FD\u70B9\u7684\u5B57\u3002");
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const bounds = await this.screenSize();
|
|
712
|
+
if (bounds) parts.push(`\u5C4F\u5E55\u5C3A\u5BF8 ${bounds.w}\xD7${bounds.h}\u3002`);
|
|
713
|
+
this.looks += 1;
|
|
714
|
+
return { text: parts.join("\n"), ...blob ? { blob } : {}, ...frame ? { frame } : {} };
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/tools.ts
|
|
719
|
+
var HANDS_TOOL_DECLS = [
|
|
720
|
+
{
|
|
721
|
+
name: "hand_look",
|
|
722
|
+
tags: ["read", "snapshot"],
|
|
723
|
+
description: "\u770B\u4E00\u773C\u5BF9\u65B9\u624B\u673A\u73B0\u5728\u8FD9\u4E00\u5C4F\uFF1A\u524D\u53F0\u662F\u54EA\u4E2A\u5E94\u7528\u3001\u5C4F\u5E55\u4EAE\u4E0D\u4EAE\u3001\u9501\u6CA1\u9501\uFF0C\u5C4F\u5E55\u4E0A\u80FD\u8BFB\u5230\u7684\u5B57\u5404\u81EA\u5728\u4EC0\u4E48\u5750\u6807\uFF0C\u5E76\uFF08\u9ED8\u8BA4\uFF09\u628A\u753B\u9762\u4E00\u8D77\u7ED9\u4F60\u3002\u60F3\u70B9\u3001\u60F3\u6ED1\u4E4B\u524D\u5148\u7528\u5B83\u7784\u51C6\uFF1B\u770B\u4E0D\u6E05\u5C31\u518D\u770B\u4E00\u773C\uFF0C\u522B\u51ED\u8BB0\u5FC6\u70B9\u3002",
|
|
724
|
+
parameters: {
|
|
725
|
+
type: "object",
|
|
726
|
+
properties: {
|
|
727
|
+
detail: {
|
|
728
|
+
type: "string",
|
|
729
|
+
enum: ["both", "screen", "text"],
|
|
730
|
+
description: "both \u753B\u9762\u52A0\u6587\u5B57\uFF08\u9ED8\u8BA4\uFF09\uFF1Bscreen \u53EA\u8981\u753B\u9762\uFF1Btext \u53EA\u8981\u6587\u5B57\uFF0C\u7701\u6D41\u91CF\u4E5F\u4E0D\u5360\u4E0A\u4E0B\u6587\u3002"
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
required: []
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: "hand_tap",
|
|
738
|
+
tags: ["act"],
|
|
739
|
+
description: "\u7528\u6307\u5C16\u70B9\u4E00\u4E0B\u5C4F\u5E55\u3002\u53EF\u4EE5\u7ED9\u5750\u6807 (x,y)\uFF0C\u4E5F\u53EF\u4EE5\u7ED9\u4E00\u4E2A\u6587\u5B57\u2014\u2014\u624B\u4F1A\u5728\u5C4F\u5E55\u4E0A\u7684\u5B57\u91CC\u627E\uFF0C\u53EA\u6709\u4E00\u5904\u5BF9\u5F97\u4E0A\u624D\u70B9\uFF0C\u6709\u597D\u51E0\u5904\u5C31\u628A\u5019\u9009\u5217\u7ED9\u4F60\u5E76\u62D2\u7EDD\u52A8\u624B\u3002\u70B9\u4E4B\u524D\u6700\u597D\u5148 hand_look \u786E\u8BA4\u4F4D\u7F6E\u3002",
|
|
740
|
+
parameters: {
|
|
741
|
+
type: "object",
|
|
742
|
+
properties: {
|
|
743
|
+
x: { type: "integer", description: "\u6A2A\u5411\u5750\u6807\uFF08\u50CF\u7D20\uFF09\uFF0C\u548C hand_look \u91CC @(...) \u7684\u7B2C\u4E8C\u534A\u4E00\u81F4\u3002" },
|
|
744
|
+
y: { type: "integer", description: "\u7EB5\u5411\u5750\u6807\uFF08\u50CF\u7D20\uFF09\u3002" },
|
|
745
|
+
text: { type: "string", description: "\u8981\u70B9\u7684\u90A3\u4E2A\u63A7\u4EF6\u4E0A\u7684\u6587\u5B57\uFF0C\u4F8B\u5982\u300C\u53D1\u9001\u300D\u300C\u8BBE\u7F6E\u300D\u3002\u7ED9\u4E86 text \u5C31\u4E0D\u7528\u7ED9\u5750\u6807\u3002" },
|
|
746
|
+
nth: { type: "integer", minimum: 0, description: "\u540C\u4E00\u6BB5\u6587\u5B57\u6709\u597D\u51E0\u5904\u65F6\u70B9\u7B2C\u51E0\u4E2A\uFF08\u4ECE 0 \u6570\uFF09\uFF1B\u4E0D\u7ED9\u5C31\u662F\u5168\u90FD\u8981\uFF0C\u624B\u4F1A\u62D2\u7EDD\u3002" }
|
|
747
|
+
},
|
|
748
|
+
required: []
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "hand_long",
|
|
753
|
+
tags: ["act"],
|
|
754
|
+
description: "\u5728\u4E00\u4E2A\u4F4D\u7F6E\u6309\u4F4F\u4E0D\u653E\uFF08\u957F\u6309\uFF09\uFF0C\u9ED8\u8BA4 800 \u6BEB\u79D2\u3002\u7528\u4E8E\u957F\u6309\u83DC\u5355\u3001\u62D6\u9009\u3001\u9009\u4E2D\u3002",
|
|
755
|
+
parameters: {
|
|
756
|
+
type: "object",
|
|
757
|
+
properties: {
|
|
758
|
+
x: { type: "integer", description: "\u6A2A\u5411\u5750\u6807\uFF08\u50CF\u7D20\uFF09\u3002" },
|
|
759
|
+
y: { type: "integer", description: "\u7EB5\u5411\u5750\u6807\uFF08\u50CF\u7D20\uFF09\u3002" },
|
|
760
|
+
ms: { type: "integer", minimum: 300, maximum: 5e3, description: "\u6309\u4F4F\u591A\u4E45\uFF0C\u9ED8\u8BA4 800 \u6BEB\u79D2\u3002" }
|
|
761
|
+
},
|
|
762
|
+
required: ["x", "y"]
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
name: "hand_swipe",
|
|
767
|
+
tags: ["act"],
|
|
768
|
+
description: "\u4ECE\u4E00\u70B9\u6ED1\u5230\u53E6\u4E00\u70B9\uFF1A\u7FFB\u9875\u3001\u6EDA\u52A8\u3001\u4E0B\u62C9\u901A\u77E5\u3001\u62D6\u4E1C\u897F\u3002\u9ED8\u8BA4 300 \u6BEB\u79D2\uFF1B\u60F3\u6162\u4E00\u70B9\u770B\u6E05\u5185\u5BB9\u5C31\u8C03\u5927\u3002",
|
|
769
|
+
parameters: {
|
|
770
|
+
type: "object",
|
|
771
|
+
properties: {
|
|
772
|
+
x1: { type: "integer", description: "\u8D77\u70B9\u6A2A\u5750\u6807\u3002" },
|
|
773
|
+
y1: { type: "integer", description: "\u8D77\u70B9\u7EB5\u5750\u6807\u3002" },
|
|
774
|
+
x2: { type: "integer", description: "\u7EC8\u70B9\u6A2A\u5750\u6807\u3002" },
|
|
775
|
+
y2: { type: "integer", description: "\u7EC8\u70B9\u7EB5\u5750\u6807\u3002" },
|
|
776
|
+
ms: { type: "integer", minimum: 50, maximum: 5e3, description: "\u6ED1\u591A\u4E45\uFF0C\u9ED8\u8BA4 300 \u6BEB\u79D2\u3002" }
|
|
777
|
+
},
|
|
778
|
+
required: ["x1", "y1", "x2", "y2"]
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: "hand_type",
|
|
783
|
+
tags: ["act"],
|
|
784
|
+
description: "\u5F80\u5F53\u524D\u5149\u6807\u5904\u6572\u5B57\u3002\u5148\u70B9\u4E00\u4E0B\u8981\u5199\u7684\u5730\u65B9\u628A\u8F93\u5165\u6846\u53EB\u8D77\u6765\u3002\u6CE8\u610F\uFF1A\u8D70 input \u8FD9\u7C7B\u901A\u9053\u65F6\u53EA\u8BA4\u53EF\u6253\u5370 ASCII\uFF08\u4E2D\u6587\u6572\u4E0D\u8FDB\u53BB\uFF0C\u4F1A\u76F4\u63A5\u5931\u8D25\u5E76\u8BF4\u660E\uFF09\uFF1B\u8D70\u65E0\u969C\u788D\u901A\u9053\u65F6\u4E2D\u6587\u4E5F\u80FD\u8FDB\u3002\u5BC6\u7801\u4E4B\u7C7B\u7684\u4E1C\u897F\u4E0D\u8981\u66FF\u5BF9\u65B9\u6572\u3002",
|
|
785
|
+
parameters: {
|
|
786
|
+
type: "object",
|
|
787
|
+
properties: {
|
|
788
|
+
text: { type: "string", description: "\u8981\u8F93\u5165\u7684\u6587\u5B57\uFF08\u4E0D\u542B\u56DE\u8F66\uFF09\u3002" }
|
|
789
|
+
},
|
|
790
|
+
required: ["text"]
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "hand_key",
|
|
795
|
+
tags: ["act"],
|
|
796
|
+
description: `\u6309\u4E00\u4E2A\u7CFB\u7EDF\u6309\u952E\uFF1A${"back\u3001home\u3001recents\u3001enter\u3001delete\u3001tab\u3001escape\u3001space\u3001up\u3001down\u3001left\u3001right\u3001volumeup\u3001volumedown\u3001power\u3001search\u3001menu"}\u3002\u8FD4\u56DE\u3001\u56DE\u684C\u9762\u3001\u786E\u8BA4\u641C\u7D22\u8FD9\u7C7B\u64CD\u4F5C\u7528\u5B83\uFF0C\u6BD4\u5728\u5C4F\u5E55\u4E0A\u627E\u6309\u94AE\u7A33\u3002`,
|
|
797
|
+
parameters: {
|
|
798
|
+
type: "object",
|
|
799
|
+
properties: {
|
|
800
|
+
key: {
|
|
801
|
+
type: "string",
|
|
802
|
+
enum: ["back", "home", "recents", "enter", "delete", "tab", "escape", "space", "up", "down", "left", "right", "volumeup", "volumedown", "power", "search", "menu"],
|
|
803
|
+
description: "\u6309\u952E\u540D\u3002"
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
required: ["key"]
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: "hand_status",
|
|
811
|
+
tags: ["read", "snapshot"],
|
|
812
|
+
description: "\u770B\u624B\u81EA\u5DF1\u7684\u72B6\u6001\uFF1A\u8D70\u54EA\u6761\u901A\u9053\u3001\u901A\u4E0D\u901A\u3001\u8FD9\u4E00\u5206\u949F\u52A8\u4E86\u51E0\u4E2A\u52A8\u4F5C\u3001\u521A\u624D\u90A3\u4E00\u4E0B\u6210\u4E86\u6CA1\u6709\u3002\u5BF9\u65B9\u95EE\u300C\u624B\u80FD\u7528\u5417\u300D\u300C\u4F60\u521A\u624D\u70B9\u4E86\u4EC0\u4E48\u300D\u65F6\u7528\u5B83\u3002",
|
|
813
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
name: "hand_stop",
|
|
817
|
+
tags: ["flow"],
|
|
818
|
+
description: "\u7ACB\u523B\u505C\u624B\uFF1A\u4E00\u5206\u949F\u4E4B\u5185\u4E0D\u518D\u52A8\u4EFB\u4F55\u4E00\u4E0B\uFF08\u5DF2\u7ECF\u53D1\u51FA\u7684\u4E0D\u7B97\uFF09\u3002\u5BF9\u65B9\u8BF4\u300C\u522B\u52A8\u4E86\u300D\u300C\u505C\u4E0B\u300D\u6216\u8005\u4F60\u53D1\u73B0\u81EA\u5DF1\u70B9\u9519\u5730\u65B9\u65F6\u7528\u5B83\u3002",
|
|
819
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
820
|
+
}
|
|
821
|
+
];
|
|
822
|
+
|
|
823
|
+
// src/world.ts
|
|
824
|
+
var ENV_PROMPT_FILE = fileURLToPath(new URL("./ENV_PROMPT.md", import.meta.url));
|
|
825
|
+
function clock(tz) {
|
|
826
|
+
try {
|
|
827
|
+
return new Intl.DateTimeFormat("zh-CN", { timeZone: tz, hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).format(/* @__PURE__ */ new Date());
|
|
828
|
+
} catch {
|
|
829
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
var STOP_MS = 6e4;
|
|
833
|
+
var HandsWorld = class {
|
|
834
|
+
id = HANDS_ID;
|
|
835
|
+
host = null;
|
|
836
|
+
log;
|
|
837
|
+
hands;
|
|
838
|
+
pausedUntil = 0;
|
|
839
|
+
recent = [];
|
|
840
|
+
opts;
|
|
841
|
+
constructor(opts) {
|
|
842
|
+
this.opts = opts;
|
|
843
|
+
this.hands = new Hands(() => this.opts.cfg);
|
|
844
|
+
}
|
|
845
|
+
/* ---------- 生命周期 ---------- */
|
|
846
|
+
async start(host) {
|
|
847
|
+
this.host = host;
|
|
848
|
+
this.log = host.log;
|
|
849
|
+
this.hands.attachLog(host.log);
|
|
850
|
+
const state = await this.hands.probe(true);
|
|
851
|
+
this.log?.info(state.ok ? `\u624B\uFF1A${state.detail}` : `\u624B\u8FD8\u52A8\u4E0D\u4E86\uFF1A${state.detail}`);
|
|
852
|
+
}
|
|
853
|
+
async stop() {
|
|
854
|
+
this.host = null;
|
|
855
|
+
}
|
|
856
|
+
/* ---------- 给模型的说明书 ---------- */
|
|
857
|
+
envPromptVars() {
|
|
858
|
+
const cfg = this.opts.cfg;
|
|
859
|
+
const state = this.hands.state();
|
|
860
|
+
const stats = this.hands.stats();
|
|
861
|
+
const actuator = state.kind === "root" ? "root \u7684 input" : state.kind === "eyes" ? "\u65E0\u969C\u788D\u624B\u52BF" : state.kind === "shell" ? "\u4F60\u7ED9\u7684 shell \u901A\u9053" : "\u8FD8\u6CA1\u6709";
|
|
862
|
+
return {
|
|
863
|
+
"hands.state": cfg.enabled ? state.ok ? "\u80FD\u7528" : "\u88C5\u4E86\u4F46\u52A8\u4E0D\u4E86" : "\u5173\u7740",
|
|
864
|
+
"hands.actuator": state.ok ? actuator : `\u6CA1\u6709\u53EF\u7528\u7684\u901A\u9053\uFF08${state.detail}\uFF09`,
|
|
865
|
+
"hands.mode": cfg.dryRun ? "\u6F14\u7EC3\uFF08\u53EA\u770B\u4E0D\u505A\uFF09" : "\u771F\u52A8",
|
|
866
|
+
"hands.window": `\u8FD9\u4E00\u5206\u949F ${stats.inWindow}\uFF0F${Math.max(1, Math.round(cfg.maxActionsPerMinute) || 30)} \u4E0B`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/* ---------- 工具 ---------- */
|
|
870
|
+
tools() {
|
|
871
|
+
return HANDS_TOOL_DECLS.map((decl) => ({
|
|
872
|
+
...decl,
|
|
873
|
+
handler: (args, ctx) => this.handle(decl.name, args, ctx)
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
async handle(name, args, ctx) {
|
|
877
|
+
try {
|
|
878
|
+
await this.guard();
|
|
879
|
+
ctx.signal?.throwIfAborted();
|
|
880
|
+
switch (name) {
|
|
881
|
+
case "hand_look":
|
|
882
|
+
return await this.look(args);
|
|
883
|
+
case "hand_tap":
|
|
884
|
+
return await this.tap(args);
|
|
885
|
+
case "hand_long":
|
|
886
|
+
return await this.long(args);
|
|
887
|
+
case "hand_swipe":
|
|
888
|
+
return await this.swipe(args);
|
|
889
|
+
case "hand_type":
|
|
890
|
+
return await this.type(args);
|
|
891
|
+
case "hand_key":
|
|
892
|
+
return await this.key(args);
|
|
893
|
+
case "hand_status":
|
|
894
|
+
return this.statusText();
|
|
895
|
+
case "hand_stop":
|
|
896
|
+
return this.stopHands();
|
|
897
|
+
default:
|
|
898
|
+
return failed(`\u6CA1\u6709\u8FD9\u4E2A\u5DE5\u5177\uFF1A${name}`);
|
|
899
|
+
}
|
|
900
|
+
} catch (err) {
|
|
901
|
+
if (err instanceof HandError) return failed(err.message);
|
|
902
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
903
|
+
this.log?.warn(`\u624B\u51FA\u9519\uFF1A${detail}`);
|
|
904
|
+
return failed(`\u624B\u51FA\u4E86\u70B9\u95EE\u9898\uFF1A${detail}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** 演练、叫停、总开关都在这里挡住。 */
|
|
908
|
+
async guard() {
|
|
909
|
+
if (!this.opts.cfg.enabled) throw new HandError("\u8FD9\u53EA\u624B\u8FD8\u6CA1\u6253\u5F00\uFF08\u8BBE\u7F6E \u2192 \u624B \u2192 \u542F\u7528\uFF09\u3002");
|
|
910
|
+
const left = this.pausedUntil - Date.now();
|
|
911
|
+
if (left > 0) throw new HandError(`\u624B\u88AB\u53EB\u505C\u4E86\uFF0C\u8FD8\u6709 ${Math.ceil(left / 1e3)} \u79D2\u624D\u80FD\u518D\u52A8\u3002`);
|
|
912
|
+
}
|
|
913
|
+
note(receipt) {
|
|
914
|
+
const stamp = clock(this.opts.timezone);
|
|
915
|
+
this.recent.unshift(`${stamp} ${receipt.text}`);
|
|
916
|
+
if (this.recent.length > 6) this.recent.length = 6;
|
|
917
|
+
}
|
|
918
|
+
async look(args) {
|
|
919
|
+
const detail = args.detail === "screen" || args.detail === "text" ? args.detail : "both";
|
|
920
|
+
const report = await this.hands.look(detail);
|
|
921
|
+
const blobs = report.blob ? [report.blob] : [];
|
|
922
|
+
return { text: report.text, ...blobs.length ? { blobs } : {} };
|
|
923
|
+
}
|
|
924
|
+
/** 给文字就先在屏幕上找,找到唯一一处才点;给坐标就直接点。 */
|
|
925
|
+
async tap(args) {
|
|
926
|
+
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
927
|
+
if (text) {
|
|
928
|
+
const hits = await this.hands.findText(text);
|
|
929
|
+
if (hits.length === 0) return failed(`\u5C4F\u5E55\u4E0A\u6CA1\u627E\u5230\u300C${text}\u300D\uFF0C\u5148 hand_look \u770B\u770B\u73B0\u5728\u662F\u4EC0\u4E48\u753B\u9762\u3002`);
|
|
930
|
+
const nth = args.nth === void 0 ? void 0 : Math.max(0, Math.round(Number(args.nth)));
|
|
931
|
+
if (nth === void 0) {
|
|
932
|
+
if (hits.length > 1) {
|
|
933
|
+
const list = hits.slice(0, 8).map((h, i) => `${i}. \u300C${h.text}\u300D@(${h.center.x},${h.center.y})`).join("\n");
|
|
934
|
+
return failed(`\u300C${text}\u300D\u5728\u5C4F\u5E55\u4E0A\u6709 ${hits.length} \u5904\uFF0C\u8BF4\u6E05\u695A\u70B9\u54EA\u4E2A\uFF08nth\uFF09\uFF1A
|
|
935
|
+
${list}`);
|
|
936
|
+
}
|
|
937
|
+
const only = hits[0];
|
|
938
|
+
const receipt3 = await this.hands.tap(only.center.x, only.center.y);
|
|
939
|
+
this.note(receipt3);
|
|
940
|
+
return `${receipt3.text}\uFF08\u70B9\u7684\u662F\u300C${only.text}\u300D\uFF09`;
|
|
941
|
+
}
|
|
942
|
+
const hit = hits[nth];
|
|
943
|
+
if (!hit) return failed(`\u300C${text}\u300D\u53EA\u6709 ${hits.length} \u5904\uFF0C\u6CA1\u6709\u7B2C ${nth} \u4E2A\u3002`);
|
|
944
|
+
const receipt2 = await this.hands.tap(hit.center.x, hit.center.y);
|
|
945
|
+
this.note(receipt2);
|
|
946
|
+
return `${receipt2.text}\uFF08\u70B9\u7684\u662F\u300C${hit.text}\u300D\uFF09`;
|
|
947
|
+
}
|
|
948
|
+
const x = args.x;
|
|
949
|
+
const y = args.y;
|
|
950
|
+
if (x === void 0 || y === void 0) return failed("\u8981\u4E48\u7ED9 x \u548C y\uFF0C\u8981\u4E48\u7ED9 text\u3002");
|
|
951
|
+
const receipt = await this.hands.tap(Number(x), Number(y));
|
|
952
|
+
this.note(receipt);
|
|
953
|
+
return receipt.text;
|
|
954
|
+
}
|
|
955
|
+
async long(args) {
|
|
956
|
+
const receipt = await this.hands.longPress(Number(args.x), Number(args.y), { ms: args.ms === void 0 ? void 0 : Number(args.ms) });
|
|
957
|
+
this.note(receipt);
|
|
958
|
+
return receipt.text;
|
|
959
|
+
}
|
|
960
|
+
async swipe(args) {
|
|
961
|
+
const receipt = await this.hands.swipe(Number(args.x1), Number(args.y1), Number(args.x2), Number(args.y2), {
|
|
962
|
+
ms: args.ms === void 0 ? void 0 : Number(args.ms)
|
|
963
|
+
});
|
|
964
|
+
this.note(receipt);
|
|
965
|
+
return receipt.text;
|
|
966
|
+
}
|
|
967
|
+
async type(args) {
|
|
968
|
+
const receipt = await this.hands.type(String(args.text ?? ""));
|
|
969
|
+
this.note(receipt);
|
|
970
|
+
return receipt.text;
|
|
971
|
+
}
|
|
972
|
+
async key(args) {
|
|
973
|
+
const receipt = await this.hands.press(String(args.key ?? ""));
|
|
974
|
+
this.note(receipt);
|
|
975
|
+
return receipt.text;
|
|
976
|
+
}
|
|
977
|
+
statusText() {
|
|
978
|
+
const cfg = this.opts.cfg;
|
|
979
|
+
const state = this.hands.state();
|
|
980
|
+
const stats = this.hands.stats();
|
|
981
|
+
const lines = [
|
|
982
|
+
cfg.enabled ? "\u624B\uFF1A\u5F00\u7740" : "\u624B\uFF1A\u5173\u7740",
|
|
983
|
+
cfg.dryRun ? "\u6A21\u5F0F\uFF1A\u6F14\u7EC3\uFF08\u53EA\u770B\u4E0D\u505A\uFF09" : "\u6A21\u5F0F\uFF1A\u771F\u52A8",
|
|
984
|
+
state.ok ? `\u901A\u9053\uFF1A${state.kind}\uFF08${state.detail}\uFF09` : `\u901A\u9053\uFF1A\u6CA1\u6709\u53EF\u7528\u7684\uFF08${state.detail}\uFF09`,
|
|
985
|
+
`\u8FD9\u4E00\u5206\u949F\uFF1A${stats.inWindow}\uFF0F${Math.max(1, Math.round(cfg.maxActionsPerMinute) || 30)} \u4E0B\uFF1B\u7D2F\u8BA1\u52A8\u8FC7 ${stats.acts} \u6B21\u3001\u770B\u8FC7 ${stats.looks} \u6B21`
|
|
986
|
+
];
|
|
987
|
+
if (this.pausedUntil > Date.now()) lines.push(`\u53EB\u505C\u4E2D\uFF1A\u8FD8\u6709 ${Math.ceil((this.pausedUntil - Date.now()) / 1e3)} \u79D2`);
|
|
988
|
+
if (this.recent.length) lines.push(`\u6700\u8FD1\uFF1A
|
|
989
|
+
${this.recent.map((r) => `- ${r}`).join("\n")}`);
|
|
990
|
+
return lines.join("\n");
|
|
991
|
+
}
|
|
992
|
+
stopHands() {
|
|
993
|
+
this.pausedUntil = Date.now() + STOP_MS;
|
|
994
|
+
this.log?.info("\u624B\uFF1A\u88AB\u53EB\u505C\u4E00\u5206\u949F");
|
|
995
|
+
return `\u505C\u624B\u4E86\uFF1A${STOP_MS / 1e3} \u79D2\u4E4B\u5185\u4E0D\u4F1A\u518D\u52A8\u4EFB\u4F55\u4E00\u4E0B\u3002`;
|
|
996
|
+
}
|
|
997
|
+
/* ---------- 控制台 ---------- */
|
|
998
|
+
/**
|
|
999
|
+
* world 契约 v5 的名字。手要能装进还没升级的 payload(手机里那份就是 v5),
|
|
1000
|
+
* 所以两个名字都给,返回同一张声明。
|
|
1001
|
+
*/
|
|
1002
|
+
console(language = "zh") {
|
|
1003
|
+
return this.webUi(language);
|
|
1004
|
+
}
|
|
1005
|
+
webUi(language = "zh") {
|
|
1006
|
+
const zh = language === "zh";
|
|
1007
|
+
const cfg = this.opts.cfg;
|
|
1008
|
+
const state = this.hands.state();
|
|
1009
|
+
const stats = this.hands.stats();
|
|
1010
|
+
const max = Math.max(1, Math.round(cfg.maxActionsPerMinute) || 30);
|
|
1011
|
+
return {
|
|
1012
|
+
label: zh ? "\u624B" : "Hands",
|
|
1013
|
+
lamps: [
|
|
1014
|
+
{
|
|
1015
|
+
label: zh ? "\u901A\u9053" : "Actuator",
|
|
1016
|
+
state: state.ok ? "online" : cfg.actuator === "off" ? "offline" : "error",
|
|
1017
|
+
hint: state.detail
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
label: zh ? "\u6A21\u5F0F" : "Mode",
|
|
1021
|
+
state: cfg.dryRun ? "loading" : "online",
|
|
1022
|
+
hint: cfg.dryRun ? zh ? "\u6F14\u7EC3\uFF1A\u53EA\u770B\u4E0D\u505A" : "Dry run" : zh ? "\u771F\u52A8" : "Live"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
label: zh ? "\u8282\u6D41" : "Throttle",
|
|
1026
|
+
state: stats.inWindow >= max ? "error" : "online",
|
|
1027
|
+
hint: `${stats.inWindow}/${max} ${zh ? "\u6BCF\u5206\u949F" : "per minute"}`
|
|
1028
|
+
}
|
|
1029
|
+
],
|
|
1030
|
+
badges: [
|
|
1031
|
+
{ label: zh ? "\u52A8\u8FC7" : "actions", value: stats.acts, tone: "plain" },
|
|
1032
|
+
{ label: zh ? "\u770B\u8FC7" : "looks", value: stats.looks, tone: "plain" }
|
|
1033
|
+
],
|
|
1034
|
+
config: [HANDS_CONFIG_GROUP, HANDS_LOOK_CONFIG_GROUP],
|
|
1035
|
+
promptDocs: [
|
|
1036
|
+
{
|
|
1037
|
+
key: `worlds.${HANDS_ID}.envPrompt`,
|
|
1038
|
+
title: zh ? "\u624B\u7684\u73AF\u5883" : "Hands environment",
|
|
1039
|
+
description: zh ? "\u63CF\u8FF0\u8FD9\u53EA\u624B\u80FD\u600E\u4E48\u52A8\u3001\u8D70\u54EA\u6761\u901A\u9053\u3001\u4EE5\u53CA\u300C\u5148\u770B\u518D\u52A8\u624B\u300D\u7684\u89C4\u77E9\u3002" : "What the hands can do, which actuator they use, and the look-before-touch rule.",
|
|
1040
|
+
path: ENV_PROMPT_FILE,
|
|
1041
|
+
role: "envPrompt",
|
|
1042
|
+
vars: [
|
|
1043
|
+
{ name: "hands.state", description: zh ? "\u80FD\u7528 / \u88C5\u4E86\u4F46\u52A8\u4E0D\u4E86 / \u5173\u7740" : "available / installed but dead / off" },
|
|
1044
|
+
{ name: "hands.actuator", description: zh ? "\u73B0\u5728\u8FD9\u6761\u901A\u9053\u662F\u4EC0\u4E48\u3001\u901A\u4E0D\u901A" : "which actuator, and whether it works" },
|
|
1045
|
+
{ name: "hands.mode", description: zh ? "\u6F14\u7EC3\u8FD8\u662F\u771F\u52A8" : "dry run or live" },
|
|
1046
|
+
{ name: "hands.window", description: zh ? "\u8FD9\u4E00\u5206\u949F\u5DF2\u7ECF\u52A8\u4E86\u51E0\u6B21\u3001\u4E0A\u9650\u591A\u5C11" : "actions in the last minute and the cap" }
|
|
1047
|
+
]
|
|
1048
|
+
}
|
|
1049
|
+
]
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function failed(text) {
|
|
1054
|
+
return { text, failed: true };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/definition.ts
|
|
1058
|
+
function handsDefinition() {
|
|
1059
|
+
return {
|
|
1060
|
+
id: HANDS_ID,
|
|
1061
|
+
label: "\u624B",
|
|
1062
|
+
defaults: () => structuredClone(HANDS_DEFAULTS),
|
|
1063
|
+
create: (ctx) => new HandsWorld({ cfg: ctx.cfg, timezone: ctx.timezone })
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
var HANDS = handsDefinition();
|
|
1067
|
+
var definition_default = HANDS;
|
|
1068
|
+
export {
|
|
1069
|
+
HANDS,
|
|
1070
|
+
HANDS_DEFAULTS,
|
|
1071
|
+
HANDS_ID,
|
|
1072
|
+
HANDS_TOOL_DECLS,
|
|
1073
|
+
HandError,
|
|
1074
|
+
Hands,
|
|
1075
|
+
HandsWorld,
|
|
1076
|
+
KEY_CODES,
|
|
1077
|
+
definition_default as default,
|
|
1078
|
+
handsDefinition
|
|
1079
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-hands",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cortico World: hands for the desktop pet — looks at the phone screen, then taps, swipes, types and presses keys through whatever actuator the device actually has",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"cortico-world",
|
|
9
|
+
"cortico",
|
|
10
|
+
"hands",
|
|
11
|
+
"automation",
|
|
12
|
+
"accessibility"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"cortico": {
|
|
22
|
+
"kind": "world",
|
|
23
|
+
"api": 5,
|
|
24
|
+
"displayName": "手"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {}
|
|
31
|
+
}
|