@temsmie/dsh-tool-keymouse 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/LICENSE +21 -0
- package/README.md +125 -0
- package/bin/helper-x64.exe +0 -0
- package/cordis.patch.yml +7 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +454 -0
- package/lib/skill-content.d.ts +4 -0
- package/lib/skill-content.js +94 -0
- package/package.json +51 -0
- package/skills/keymouse.md +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dsh-tool-keymouse contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# dsh-tool-keymouse
|
|
2
|
+
|
|
3
|
+
Windows 键鼠 + 截图 + 窗口自动化工具插件(DeepSeek Harness)。通过常驻 helper(`bin/helper-x64.exe`)走管道 + JSON 行协议调用 Win32 `SendInput` 模拟真实键盘/鼠标输入,用于桌面自动化:打开/唤起应用、登录、点击、输入、拖拽、滚动、截图观察、窗口定位/置顶。
|
|
4
|
+
|
|
5
|
+
helper 已 **DPI-aware**:`GetSystemMetrics`、截图、`mouse_move` 全部统一到物理像素(如 2560x1600)。模型给坐标 = 截图里看到的坐标 = 物理像素,无需缩放换算。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 键盘:`key_press` / `key_up` / `key_tap`,字母数字、空格回车、F1-F12、方向键、Ctrl/Alt/Shift、Tab、Esc 等。
|
|
10
|
+
- 鼠标:`mouse_press`(按住拖动)、`mouse_move`(绝对物理坐标)、`mouse_roll`(滚轮)。
|
|
11
|
+
- 截图:`screenshot` 抓主屏保存 PNG,返回物理像素尺寸;多模态模型直接看图。
|
|
12
|
+
- 窗口:`get_window` / `win_rect`(查位置)、`win_find`(按进程名找窗口)、`win_activate`(唤起,含最小化/托盘)、`win_minimize` / `win_restore` / `win_maximize`、`win_top` / `release_top`(置顶)。
|
|
13
|
+
- 清理:`release_all` 一键抬起所有按住项防卡键;`release_top` 取消所有置顶。
|
|
14
|
+
- 自带常驻 `keymouse` 说明书(runtime skill),模型通过 skill 工具读取使用规则。
|
|
15
|
+
|
|
16
|
+
## 工具清单(17)
|
|
17
|
+
|
|
18
|
+
| 工具 | 参数 | 说明 |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `screenshot()` | 无 | 截主屏 PNG,返回 path/width/height(物理像素) |
|
|
21
|
+
| `get_window()` | 无 | 当前前台窗口 hwnd/title/process/pid/x/y/w/h |
|
|
22
|
+
| `win_rect(hwnd?)` | hwnd? | 窗口位置与尺寸(缺省前台) |
|
|
23
|
+
| `win_find(process)` | process | 按进程名找窗口(返回 hwnd/visible,false=托盘) |
|
|
24
|
+
| `win_activate(hwnd)` | hwnd | 唤起/前置窗口,处理最小化+托盘 |
|
|
25
|
+
| `win_minimize(hwnd?)` | hwnd? | 最小化 |
|
|
26
|
+
| `win_restore(hwnd?)` | hwnd? | 还原 |
|
|
27
|
+
| `win_maximize(hwnd?)` | hwnd? | 最大化 |
|
|
28
|
+
| `win_top(hwnd, on)` | hwnd, on | 置顶/取消置顶 |
|
|
29
|
+
| `release_top()` | 无 | 取消所有本插件置顶 |
|
|
30
|
+
| `key_tap(key)` | key | 按下+立刻抬起 |
|
|
31
|
+
| `key_press(key)` | key | 只按下(组合键用) |
|
|
32
|
+
| `key_up(key)` | key | 抬起 |
|
|
33
|
+
| `mouse_press(button)` | button | 只按下鼠标按钮 |
|
|
34
|
+
| `mouse_move(x,y)` | x, y | 绝对移动(物理像素) |
|
|
35
|
+
| `mouse_roll(up, amount?)` | up, amount? | 滚轮滚动 |
|
|
36
|
+
| `release_all()` | 无 | 统一抬起全部按住项 |
|
|
37
|
+
|
|
38
|
+
典型组合:`win_find('QQ')` → `win_activate(hwnd)` 唤起窗口(不点图标);`key_press('ctrl')` → `key_tap('s')` → `release_all`;`mouse_press('left')` → `mouse_move(...)` → `release_all`;`screenshot()` → `read_image(path)` 看图再操作。
|
|
39
|
+
|
|
40
|
+
## 安装
|
|
41
|
+
|
|
42
|
+
本地目录方式(开发/内网):
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
dsh plugin --profile <name> add D:\AI_production\dsh-keymouse
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
npm 方式(发布后):
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
dsh plugin --profile <name> add @temsmie/dsh-tool-keymouse
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 目录结构
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
dsh-keymouse/
|
|
58
|
+
package.json # npm 包契约(peerDeps/files/exports)
|
|
59
|
+
tsconfig.json
|
|
60
|
+
src/
|
|
61
|
+
index.ts # 插件入口: apply 里 spawn helper + 注册工具 + 注册skill
|
|
62
|
+
helper.cpp # helper 源码(SendInput + JSON行协议 + 键位表 + 窗口/截图)
|
|
63
|
+
skill-content.ts # keymouse 说明书内容(注册为 runtime skill)
|
|
64
|
+
skills/
|
|
65
|
+
keymouse.md # 说明书可读副本(与 skill-content 一致)
|
|
66
|
+
bin/
|
|
67
|
+
helper-x64.exe # 预编译 helper(Windows x64)
|
|
68
|
+
lib/ # tsc 编译产物(index.js / skill-content.js / *.d.ts)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## helper 协议
|
|
72
|
+
|
|
73
|
+
helper 从 stdin 逐行读 JSON,每行一条指令,每条输出一行 `{"ok":true|false,"error":...}` 到 stdout。stdin 关闭即退出(退出前自动 `release_all`)。
|
|
74
|
+
|
|
75
|
+
指令(`get_window`/`win_*` 返回窗口字段,`capture` 返回 path/width/height):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
{"cmd":"press","key":"space"}
|
|
79
|
+
{"cmd":"up","key":"space"}
|
|
80
|
+
{"cmd":"tap","key":"enter"}
|
|
81
|
+
{"cmd":"mpress","button":"left"}
|
|
82
|
+
{"cmd":"mmove","x":800,"y":450}
|
|
83
|
+
{"cmd":"mroll","up":true,"amount":1}
|
|
84
|
+
{"cmd":"release_all"}
|
|
85
|
+
{"cmd":"ping"}
|
|
86
|
+
{"cmd":"capture"}
|
|
87
|
+
{"cmd":"get_window"}
|
|
88
|
+
{"cmd":"win_rect","hwnd":123}
|
|
89
|
+
{"cmd":"win_top","hwnd":123,"on":true}
|
|
90
|
+
{"cmd":"release_top"}
|
|
91
|
+
{"cmd":"win_state","hwnd":123,"action":"minimize|restore|maximize"}
|
|
92
|
+
{"cmd":"win_find","process":"QQ"}
|
|
93
|
+
{"cmd":"win_activate","hwnd":123}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 重新编译 helper
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
g++ -O2 -std=c++17 src\helper.cpp -o bin\helper-x64.exe -luser32 -lwinmm -lgdiplus -lgdi32
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 重新编译插件(TypeScript -> lib)
|
|
103
|
+
|
|
104
|
+
首次需把本机 DSH 的 `@deepseek-ai` 依赖接(junction)到本项目 `node_modules`(`@deepseek-ai/dsh-tools` 等类型来自 DSH 安装目录):
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
cmd /c mklink /J "node_modules\@deepseek-ai" "C:\Users\<你的用户名>\AppData\Roaming\npm\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
然后:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
npm install
|
|
114
|
+
npx tsc
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
说明:该 junction 仅用于本机编译,不在发布白名单(`files`)内,不影响打包分发。`node_modules` 已 gitignore,`bin/helper-x64.exe`(预编译、按需分发)随包提交。
|
|
118
|
+
|
|
119
|
+
## 平台
|
|
120
|
+
|
|
121
|
+
仅支持 Windows(helper 使用 Win32 `SendInput`/`GetSystemMetrics`/`EnumWindows`)。DPI-aware,主屏物理像素坐标基准,单屏。
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
Binary file
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# dsh-keymouse bundle patch: mount the plugin under its package name,
|
|
2
|
+
# resolved through the profile's node_modules. DSH loads lib/index.js and
|
|
3
|
+
# calls apply(ctx) to spawn the helper and register the key/mouse tools + skill.
|
|
4
|
+
|
|
5
|
+
- insert:
|
|
6
|
+
- id: tool-keymouse
|
|
7
|
+
name: '@keymouse/dsh-tool-keymouse'
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
export declare const name = "tool-keymouse";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export declare const Config: z<Schemastery.ObjectS<{
|
|
6
|
+
helperPath: z<string, string>;
|
|
7
|
+
timeoutMs: z<number, number>;
|
|
8
|
+
}>, Schemastery.ObjectT<{
|
|
9
|
+
helperPath: z<string, string>;
|
|
10
|
+
timeoutMs: z<number, number>;
|
|
11
|
+
}>>;
|
|
12
|
+
export interface KeymouseConfig {
|
|
13
|
+
helperPath?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface HelperResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
/** capture 指令返回的 PNG 绝对路径与尺寸(仅截图时)。 */
|
|
20
|
+
path?: string;
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
/** 窗口工具返回的字段(仅窗口类指令)。 */
|
|
24
|
+
hwnd?: number;
|
|
25
|
+
title?: string;
|
|
26
|
+
pid?: number;
|
|
27
|
+
process?: string;
|
|
28
|
+
x?: number;
|
|
29
|
+
y?: number;
|
|
30
|
+
w?: number;
|
|
31
|
+
h?: number;
|
|
32
|
+
/** 窗口是否可见(false = 最小化到托盘隐藏状态)。 */
|
|
33
|
+
visible?: boolean;
|
|
34
|
+
}
|
|
35
|
+
declare function apply(ctx: Context, config?: KeymouseConfig): void;
|
|
36
|
+
export { apply };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// index.ts — dsh-keymouse 插件入口
|
|
2
|
+
// apply(ctx) 中:
|
|
3
|
+
// 1. spawn 常驻 helper-x64.exe(通过 stdin 发 JSON 行指令)
|
|
4
|
+
// 2. ctx.tools.register 七个键鼠工具
|
|
5
|
+
// 3. ctx.skills.register 常驻 keymouse 说明书(runtime skill)
|
|
6
|
+
// 只支持 Windows。helper 预编译进 bin/helper-x64.exe。
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
13
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
14
|
+
import { isSkillName } from '@deepseek-ai/dsh-skill';
|
|
15
|
+
import { KEYMOUSE_SKILL_NAME, KEYMOUSE_SKILL_DESCRIPTION, KEYMOUSE_SKILL_WHEN_TO_USE, KEYMOUSE_SKILL_CONTENT, } from './skill-content.js';
|
|
16
|
+
export const name = 'tool-keymouse';
|
|
17
|
+
export const inject = ['tools', 'skills'];
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const DEFAULT_HELPER_PATH = join(__dirname, '..', 'bin', 'helper-x64.exe');
|
|
20
|
+
export const Config = z.object({
|
|
21
|
+
helperPath: z.string(),
|
|
22
|
+
timeoutMs: z.number().min(1),
|
|
23
|
+
});
|
|
24
|
+
const HELPER_TIMEOUT_MS = 5000;
|
|
25
|
+
/** 常驻 helper 客户端:一次只发一条指令,等一行 JSON 响应;helper 崩溃后自动重启。 */
|
|
26
|
+
class HelperClient {
|
|
27
|
+
helperPath;
|
|
28
|
+
timeoutMs;
|
|
29
|
+
child = null;
|
|
30
|
+
rl = null;
|
|
31
|
+
started = false;
|
|
32
|
+
chain = Promise.resolve();
|
|
33
|
+
inFlight = null;
|
|
34
|
+
constructor(helperPath, timeoutMs) {
|
|
35
|
+
this.helperPath = helperPath;
|
|
36
|
+
this.timeoutMs = timeoutMs;
|
|
37
|
+
}
|
|
38
|
+
ensureStarted() {
|
|
39
|
+
if (!existsSync(this.helperPath)) {
|
|
40
|
+
return Promise.reject(new Error(`helper not found: ${this.helperPath}`));
|
|
41
|
+
}
|
|
42
|
+
if (this.started && this.child !== null)
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
this.started = true;
|
|
45
|
+
const child = spawn(this.helperPath, [], {
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
});
|
|
49
|
+
this.child = child;
|
|
50
|
+
this.rl = createInterface({ input: child.stdout });
|
|
51
|
+
this.rl.on('line', (line) => this.consume(line));
|
|
52
|
+
child.on('error', (err) => this.failInFlight(err));
|
|
53
|
+
child.on('exit', () => {
|
|
54
|
+
this.failInFlight(new Error('helper exited'));
|
|
55
|
+
if (this.child === child) {
|
|
56
|
+
this.child = null;
|
|
57
|
+
this.started = false;
|
|
58
|
+
this.rl?.close();
|
|
59
|
+
this.rl = null;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return new Promise((res, rej) => {
|
|
63
|
+
child.once('spawn', () => res());
|
|
64
|
+
child.once('error', (e) => rej(e));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
consume(line) {
|
|
68
|
+
const cur = this.inFlight;
|
|
69
|
+
if (cur === null)
|
|
70
|
+
return;
|
|
71
|
+
this.inFlight = null;
|
|
72
|
+
clearTimeout(cur.timer);
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(line.trim());
|
|
75
|
+
const ok = parsed.ok === true;
|
|
76
|
+
const error = typeof parsed.error === 'string' ? parsed.error : undefined;
|
|
77
|
+
const path = typeof parsed.path === 'string' ? parsed.path : undefined;
|
|
78
|
+
const width = typeof parsed.width === 'number' ? parsed.width : undefined;
|
|
79
|
+
const height = typeof parsed.height === 'number' ? parsed.height : undefined;
|
|
80
|
+
const hwnd = typeof parsed.hwnd === 'number' ? parsed.hwnd : undefined;
|
|
81
|
+
const pid = typeof parsed.pid === 'number' ? parsed.pid : undefined;
|
|
82
|
+
const title = typeof parsed.title === 'string' ? parsed.title : undefined;
|
|
83
|
+
const process = typeof parsed.process === 'string' ? parsed.process : undefined;
|
|
84
|
+
const visible = typeof parsed.visible === 'boolean' ? parsed.visible : undefined;
|
|
85
|
+
const x = typeof parsed.x === 'number' ? parsed.x : undefined;
|
|
86
|
+
const y = typeof parsed.y === 'number' ? parsed.y : undefined;
|
|
87
|
+
const w = typeof parsed.w === 'number' ? parsed.w : undefined;
|
|
88
|
+
const h = typeof parsed.h === 'number' ? parsed.h : undefined;
|
|
89
|
+
// 成功/无错误时省略 error 键,避免含 undefined 属性被判为 lossy JSON;
|
|
90
|
+
// 其余字段(截图/窗口)同样仅有值时才挂载。
|
|
91
|
+
const base = error === undefined ? { ok } : { ok, error };
|
|
92
|
+
if (path !== undefined)
|
|
93
|
+
base.path = path;
|
|
94
|
+
if (width !== undefined)
|
|
95
|
+
base.width = width;
|
|
96
|
+
if (height !== undefined)
|
|
97
|
+
base.height = height;
|
|
98
|
+
if (hwnd !== undefined)
|
|
99
|
+
base.hwnd = hwnd;
|
|
100
|
+
if (pid !== undefined)
|
|
101
|
+
base.pid = pid;
|
|
102
|
+
if (title !== undefined)
|
|
103
|
+
base.title = title;
|
|
104
|
+
if (process !== undefined)
|
|
105
|
+
base.process = process;
|
|
106
|
+
if (visible !== undefined)
|
|
107
|
+
base.visible = visible;
|
|
108
|
+
if (x !== undefined)
|
|
109
|
+
base.x = x;
|
|
110
|
+
if (y !== undefined)
|
|
111
|
+
base.y = y;
|
|
112
|
+
if (w !== undefined)
|
|
113
|
+
base.w = w;
|
|
114
|
+
if (h !== undefined)
|
|
115
|
+
base.h = h;
|
|
116
|
+
cur.resolve(base);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
cur.reject(new Error(`helper returned invalid response: ${line}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
failInFlight(err) {
|
|
123
|
+
const cur = this.inFlight;
|
|
124
|
+
if (cur === null)
|
|
125
|
+
return;
|
|
126
|
+
this.inFlight = null;
|
|
127
|
+
clearTimeout(cur.timer);
|
|
128
|
+
cur.reject(err instanceof Error ? err : new Error(String(err)));
|
|
129
|
+
}
|
|
130
|
+
/** 串行发送一条指令,helper 响应后解析返回。 */
|
|
131
|
+
send(cmd) {
|
|
132
|
+
const run = this.chain.then(async () => {
|
|
133
|
+
await this.ensureStarted();
|
|
134
|
+
if (this.child === null)
|
|
135
|
+
throw new Error('helper is not running');
|
|
136
|
+
return this.writeAndRead(cmd);
|
|
137
|
+
});
|
|
138
|
+
this.chain = run.catch(() => undefined);
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
writeAndRead(cmd) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const child = this.child;
|
|
144
|
+
const timer = setTimeout(() => this.failInFlight(new Error('helper timed out')), this.timeoutMs);
|
|
145
|
+
this.inFlight = { resolve, reject, timer };
|
|
146
|
+
const line = JSON.stringify(cmd) + '\n';
|
|
147
|
+
child.stdin.write(line, (err) => {
|
|
148
|
+
if (err)
|
|
149
|
+
this.failInFlight(err);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** 退出前统一 release_all,再掐断 stdin 并结束进程。 */
|
|
154
|
+
async dispose() {
|
|
155
|
+
const child = this.child;
|
|
156
|
+
this.child = null;
|
|
157
|
+
this.started = false;
|
|
158
|
+
this.failInFlight(new Error('disposed'));
|
|
159
|
+
if (child !== null && !child.killed) {
|
|
160
|
+
try {
|
|
161
|
+
this.inFlight = null;
|
|
162
|
+
child.stdin.write('{"cmd":"release_all"}\n');
|
|
163
|
+
child.stdin.end();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
child.kill();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** 统一的输出 schema:每次调用返回 ok/error;截图额外带 path/width/height。 */
|
|
173
|
+
const OUTPUT_SCHEMA = {
|
|
174
|
+
type: 'object',
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
properties: {
|
|
177
|
+
ok: { type: 'boolean', required: true },
|
|
178
|
+
error: { type: 'string' },
|
|
179
|
+
path: { type: 'string' },
|
|
180
|
+
width: { type: 'number' },
|
|
181
|
+
height: { type: 'number' },
|
|
182
|
+
hwnd: { type: 'number' },
|
|
183
|
+
title: { type: 'string' },
|
|
184
|
+
pid: { type: 'number' },
|
|
185
|
+
process: { type: 'string' },
|
|
186
|
+
x: { type: 'number' },
|
|
187
|
+
y: { type: 'number' },
|
|
188
|
+
w: { type: 'number' },
|
|
189
|
+
h: { type: 'number' },
|
|
190
|
+
visible: { type: 'boolean' },
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
/** 生成统一的 render:成功/失败各一条文本,尽量带上调用参数便于模型核对。 */
|
|
194
|
+
function renderOutput(kind) {
|
|
195
|
+
return (args, value) => {
|
|
196
|
+
const a = args;
|
|
197
|
+
const detail = a?.key ??
|
|
198
|
+
a?.button ??
|
|
199
|
+
(a?.x !== undefined && a?.y !== undefined ? `${a.x},${a.y}` : undefined) ??
|
|
200
|
+
undefined;
|
|
201
|
+
if (value.ok) {
|
|
202
|
+
return [{ type: 'text', text: detail !== undefined ? `${kind}: ${detail} 完成` : `${kind} 完成` }];
|
|
203
|
+
}
|
|
204
|
+
return [{ type: 'text', text: `${kind}失败: ${value.error ?? '未知错误'}` }];
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** 截图结果的 render:成功时显示保存路径与尺寸,失败显示错误。 */
|
|
208
|
+
function renderScreenshot(args, value) {
|
|
209
|
+
if (value.ok) {
|
|
210
|
+
const text = value.path ? `截图已保存: ${value.path} (${value.width}x${value.height})` : '截图完成';
|
|
211
|
+
return [{ type: 'text', text }];
|
|
212
|
+
}
|
|
213
|
+
return [{ type: 'text', text: `截图失败: ${value.error ?? '未知错误'}` }];
|
|
214
|
+
}
|
|
215
|
+
/** get_window 的 render:显示标题/进程/pid/位置。 */
|
|
216
|
+
function renderGetWindow(args, value) {
|
|
217
|
+
if (value.ok) {
|
|
218
|
+
const tex = `当前窗口: ${value.title ?? '(无标题)'} (${value.process ?? ''}, pid=${value.pid ?? '?'}, ${value.x ?? 0},${value.y ?? 0} ${value.w ?? 0}x${value.h ?? 0})`;
|
|
219
|
+
return [{ type: 'text', text: tex }];
|
|
220
|
+
}
|
|
221
|
+
return [{ type: 'text', text: `get_window失败: ${value.error ?? '未知错误'}` }];
|
|
222
|
+
}
|
|
223
|
+
/** win_rect 的 render:显示位置与尺寸。 */
|
|
224
|
+
function renderWinRect(args, value) {
|
|
225
|
+
if (value.ok) {
|
|
226
|
+
const tex = `窗口位置: (${value.x ?? 0},${value.y ?? 0}) ${value.w ?? 0}x${value.h ?? 0}`;
|
|
227
|
+
return [{ type: 'text', text: tex }];
|
|
228
|
+
}
|
|
229
|
+
return [{ type: 'text', text: `win_rect失败: ${value.error ?? '未知错误'}` }];
|
|
230
|
+
}
|
|
231
|
+
/** win_find 的 render:显示匹配窗口的句柄/标题/进程/可见性。 */
|
|
232
|
+
function renderFindWindow(args, value) {
|
|
233
|
+
if (value.ok) {
|
|
234
|
+
const tex = `找到窗口: hwnd=${value.hwnd} "${value.title ?? '(无标题)'}" (${value.process ?? ''}, pid=${value.pid ?? '?'}, visible=${value.visible ? '是' : '否,最小化到托盘'}, ${value.x ?? 0},${value.y ?? 0} ${value.w ?? 0}x${value.h ?? 0})`;
|
|
235
|
+
return [{ type: 'text', text: tex }];
|
|
236
|
+
}
|
|
237
|
+
return [{ type: 'text', text: `win_find失败: ${value.error ?? '未知错误'}` }];
|
|
238
|
+
}
|
|
239
|
+
function apply(ctx, config = {}) {
|
|
240
|
+
const helperPath = config.helperPath ?? DEFAULT_HELPER_PATH;
|
|
241
|
+
const timeoutMs = config.timeoutMs ?? HELPER_TIMEOUT_MS;
|
|
242
|
+
const client = new HelperClient(helperPath, timeoutMs);
|
|
243
|
+
// 插件卸载时清理常驻进程。
|
|
244
|
+
ctx.effect(() => async () => {
|
|
245
|
+
await client.dispose();
|
|
246
|
+
});
|
|
247
|
+
const register = (tool) => ctx.tools.register(tool);
|
|
248
|
+
register(defineTool({
|
|
249
|
+
name: 'key_press',
|
|
250
|
+
description: '按下某个键盘键(不抬起)。用于组合键:先 key_press 其他键,最后必须调用 release_all 抬起,否则键会一直按住。',
|
|
251
|
+
parameters: {
|
|
252
|
+
key: { type: 'string', required: true, description: '小写英文键名:字母 a-z、数字 0-9、space/enter/tab/esc/backspace/delete/home/end/pageup/pagedown、方向键 up/down/left/right、f1-f12、ctrl/alt/shift/win/capslock。' },
|
|
253
|
+
},
|
|
254
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('key_press') },
|
|
255
|
+
async execute(args, exec) {
|
|
256
|
+
if (exec.signal.aborted)
|
|
257
|
+
throw new Error('aborted');
|
|
258
|
+
return client.send({ cmd: 'press', key: args.key });
|
|
259
|
+
},
|
|
260
|
+
}));
|
|
261
|
+
register(defineTool({
|
|
262
|
+
name: 'key_up',
|
|
263
|
+
description: '抬起某个键盘键。与 key_press 配对;也可单独使用。',
|
|
264
|
+
parameters: {
|
|
265
|
+
key: { type: 'string', required: true, description: '小写英文键名,同 key_press。' },
|
|
266
|
+
},
|
|
267
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('key_up') },
|
|
268
|
+
async execute(args, exec) {
|
|
269
|
+
if (exec.signal.aborted)
|
|
270
|
+
throw new Error('aborted');
|
|
271
|
+
return client.send({ cmd: 'up', key: args.key });
|
|
272
|
+
},
|
|
273
|
+
}));
|
|
274
|
+
register(defineTool({
|
|
275
|
+
name: 'key_tap',
|
|
276
|
+
description: '按下并立刻抬起单个键(单个动作,不卡键)。用于点按/输入单字符或单个快捷键按键。',
|
|
277
|
+
parameters: {
|
|
278
|
+
key: { type: 'string', required: true, description: '小写英文键名,同 key_press。' },
|
|
279
|
+
},
|
|
280
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('key_tap') },
|
|
281
|
+
async execute(args, exec) {
|
|
282
|
+
if (exec.signal.aborted)
|
|
283
|
+
throw new Error('aborted');
|
|
284
|
+
return client.send({ cmd: 'tap', key: args.key });
|
|
285
|
+
},
|
|
286
|
+
}));
|
|
287
|
+
register(defineTool({
|
|
288
|
+
name: 'mouse_press',
|
|
289
|
+
description: '只按下鼠标按钮(不抬起)。用于按住左键拖动:mouse_press("left") → mouse_move(...) → release_all。',
|
|
290
|
+
parameters: {
|
|
291
|
+
button: { type: 'string', required: true, description: '鼠标按钮:left/right/middle/x1/x2。' },
|
|
292
|
+
},
|
|
293
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('mouse_press') },
|
|
294
|
+
async execute(args, exec) {
|
|
295
|
+
if (exec.signal.aborted)
|
|
296
|
+
throw new Error('aborted');
|
|
297
|
+
return client.send({ cmd: 'mpress', button: args.button });
|
|
298
|
+
},
|
|
299
|
+
}));
|
|
300
|
+
register(defineTool({
|
|
301
|
+
name: 'mouse_move',
|
|
302
|
+
description: '把鼠标绝对移动到屏幕坐标 (x,y)。以主屏左上为 (0,0),单屏。',
|
|
303
|
+
parameters: {
|
|
304
|
+
x: { type: 'number', required: true, description: '目标屏幕 X 坐标(像素)。' },
|
|
305
|
+
y: { type: 'number', required: true, description: '目标屏幕 Y 坐标(像素)。' },
|
|
306
|
+
},
|
|
307
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('mouse_move') },
|
|
308
|
+
async execute(args, exec) {
|
|
309
|
+
if (exec.signal.aborted)
|
|
310
|
+
throw new Error('aborted');
|
|
311
|
+
return client.send({ cmd: 'mmove', x: Math.round(args.x), y: Math.round(args.y) });
|
|
312
|
+
},
|
|
313
|
+
}));
|
|
314
|
+
register(defineTool({
|
|
315
|
+
name: 'mouse_roll',
|
|
316
|
+
description: '滚动鼠标滚轮。up 为 true 滚上、false 滚下;可指定滚动格数 amount(默认1)。',
|
|
317
|
+
parameters: {
|
|
318
|
+
up: { type: 'boolean', required: true, description: 'true 滚上,false 滚下。' },
|
|
319
|
+
amount: { type: 'number', description: '滚动格数,默认 1。' },
|
|
320
|
+
},
|
|
321
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('mouse_roll') },
|
|
322
|
+
async execute(args, exec) {
|
|
323
|
+
if (exec.signal.aborted)
|
|
324
|
+
throw new Error('aborted');
|
|
325
|
+
return client.send({ cmd: 'mroll', up: args.up, amount: args.amount ?? 1 });
|
|
326
|
+
},
|
|
327
|
+
}));
|
|
328
|
+
register(defineTool({
|
|
329
|
+
name: 'release_all',
|
|
330
|
+
description: '抬起所有仍被按住的键与鼠标按钮。每次键鼠操作结束后必须调用一次,避免卡键。',
|
|
331
|
+
parameters: {},
|
|
332
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('release_all') },
|
|
333
|
+
async execute(_args, exec) {
|
|
334
|
+
if (exec.signal.aborted)
|
|
335
|
+
throw new Error('aborted');
|
|
336
|
+
return client.send({ cmd: 'release_all' });
|
|
337
|
+
},
|
|
338
|
+
}));
|
|
339
|
+
register(defineTool({
|
|
340
|
+
name: 'screenshot',
|
|
341
|
+
description: '截取当前主屏(全屏)保存为 PNG,返回图片路径与尺寸。之后用 read_image 读取该路径即可查看屏幕内容,再配合键鼠操作。',
|
|
342
|
+
parameters: {},
|
|
343
|
+
output: { schema: OUTPUT_SCHEMA, render: renderScreenshot },
|
|
344
|
+
async execute(_args, exec) {
|
|
345
|
+
if (exec.signal.aborted)
|
|
346
|
+
throw new Error('aborted');
|
|
347
|
+
return client.send({ cmd: 'capture' });
|
|
348
|
+
},
|
|
349
|
+
}));
|
|
350
|
+
register(defineTool({
|
|
351
|
+
name: 'get_window',
|
|
352
|
+
description: '获取当前前台窗口的信息:窗口句柄 hwnd、标题 title、进程名 process、pid、位置(x,y,w,h)。用于确认目标窗口、拿到 hwnd 再给 win_rect/win_top。',
|
|
353
|
+
parameters: {},
|
|
354
|
+
output: { schema: OUTPUT_SCHEMA, render: renderGetWindow },
|
|
355
|
+
async execute(_args, exec) {
|
|
356
|
+
if (exec.signal.aborted)
|
|
357
|
+
throw new Error('aborted');
|
|
358
|
+
return client.send({ cmd: 'get_window' });
|
|
359
|
+
},
|
|
360
|
+
}));
|
|
361
|
+
register(defineTool({
|
|
362
|
+
name: 'win_rect',
|
|
363
|
+
description: '获取窗口位置与尺寸:传入 hwnd(来自 get_window),不传默认当前前台窗口。返回 x/y/w/h。',
|
|
364
|
+
parameters: {
|
|
365
|
+
hwnd: { type: 'number', description: '窗口句柄(来自 get_window 的 hwnd)。可选。' },
|
|
366
|
+
},
|
|
367
|
+
output: { schema: OUTPUT_SCHEMA, render: renderWinRect },
|
|
368
|
+
async execute(args, exec) {
|
|
369
|
+
if (exec.signal.aborted)
|
|
370
|
+
throw new Error('aborted');
|
|
371
|
+
return client.send(args.hwnd !== undefined ? { cmd: 'win_rect', hwnd: args.hwnd } : { cmd: 'win_rect' });
|
|
372
|
+
},
|
|
373
|
+
}));
|
|
374
|
+
register(defineTool({
|
|
375
|
+
name: 'win_top',
|
|
376
|
+
description: '置顶(true)/取消置顶(false)指定窗口(置顶后窗口常驻最前)。任务结束统一调 release_top 取消所有置顶,避免干扰。',
|
|
377
|
+
parameters: {
|
|
378
|
+
hwnd: { type: 'number', required: true, description: '窗口句柄(来自 get_window 的 hwnd)。' },
|
|
379
|
+
on: { type: 'boolean', required: true, description: 'true 置顶,false 取消置顶。' },
|
|
380
|
+
},
|
|
381
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('win_top') },
|
|
382
|
+
async execute(args, exec) {
|
|
383
|
+
if (exec.signal.aborted)
|
|
384
|
+
throw new Error('aborted');
|
|
385
|
+
return client.send({ cmd: 'win_top', hwnd: Math.round(args.hwnd), on: args.on });
|
|
386
|
+
},
|
|
387
|
+
}));
|
|
388
|
+
register(defineTool({
|
|
389
|
+
name: 'release_top',
|
|
390
|
+
description: '取消所有本插件置顶过的窗口,恢复默认层级。任务结束必须调用一次。',
|
|
391
|
+
parameters: {},
|
|
392
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('release_top') },
|
|
393
|
+
async execute(_args, exec) {
|
|
394
|
+
if (exec.signal.aborted)
|
|
395
|
+
throw new Error('aborted');
|
|
396
|
+
return client.send({ cmd: 'release_top' });
|
|
397
|
+
},
|
|
398
|
+
}));
|
|
399
|
+
const winState = (action, hint) => defineTool({
|
|
400
|
+
name: `win_${action}`,
|
|
401
|
+
description: hint,
|
|
402
|
+
parameters: {
|
|
403
|
+
hwnd: { type: 'number', description: '窗口句柄(来自 get_window 的 hwnd)。可选,不传默认当前前台窗口。' },
|
|
404
|
+
},
|
|
405
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput(`win_${action}`) },
|
|
406
|
+
async execute(args, exec) {
|
|
407
|
+
if (exec.signal.aborted)
|
|
408
|
+
throw new Error('aborted');
|
|
409
|
+
return client.send(args.hwnd !== undefined ? { cmd: 'win_state', action, hwnd: args.hwnd } : { cmd: 'win_state', action });
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
register(winState('minimize', '最小化指定窗口(不传默认当前前台窗口)。'));
|
|
413
|
+
register(winState('restore', '还原指定窗口(不传默认当前前台窗口):从最小化或最大化还原。'));
|
|
414
|
+
register(winState('maximize', '最大化指定窗口(不传默认当前前台窗口)。'));
|
|
415
|
+
register(defineTool({
|
|
416
|
+
name: 'win_find',
|
|
417
|
+
description: '按进程名查找窗口:返回第一个匹配的窗口句柄 hwnd、标题、进程、pid、visible(是否可见,false 表示最小化到托盘)。用于替代鼠标点任务栏图标,定位并唤起某程序窗口。',
|
|
418
|
+
parameters: {
|
|
419
|
+
process: { type: 'string', required: true, description: '进程名,如 "QQ"、"QQ.exe"、"firefox"(忽略大小写与 .exe 后缀)。' },
|
|
420
|
+
},
|
|
421
|
+
output: { schema: OUTPUT_SCHEMA, render: renderFindWindow },
|
|
422
|
+
async execute(args, exec) {
|
|
423
|
+
if (exec.signal.aborted)
|
|
424
|
+
throw new Error('aborted');
|
|
425
|
+
return client.send({ cmd: 'win_find', process: args.process });
|
|
426
|
+
},
|
|
427
|
+
}));
|
|
428
|
+
register(defineTool({
|
|
429
|
+
name: 'win_activate',
|
|
430
|
+
description: '激活/前置指定窗口:自动处理最小化与"最小化到托盘(隐藏)"两种情况,再置前台。配合 win_find 使用,任务结束无需额外清理。',
|
|
431
|
+
parameters: {
|
|
432
|
+
hwnd: { type: 'number', required: true, description: '窗口句柄(来自 win_find 或 get_window 的 hwnd)。' },
|
|
433
|
+
},
|
|
434
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput('win_activate') },
|
|
435
|
+
async execute(args, exec) {
|
|
436
|
+
if (exec.signal.aborted)
|
|
437
|
+
throw new Error('aborted');
|
|
438
|
+
return client.send({ cmd: 'win_activate', hwnd: Math.round(args.hwnd) });
|
|
439
|
+
},
|
|
440
|
+
}));
|
|
441
|
+
// 注册常驻 skill 说明书(runtime skill,随插件分发)。
|
|
442
|
+
if (isSkillName(KEYMOUSE_SKILL_NAME)) {
|
|
443
|
+
ctx.skills.register({
|
|
444
|
+
name: KEYMOUSE_SKILL_NAME,
|
|
445
|
+
description: KEYMOUSE_SKILL_DESCRIPTION,
|
|
446
|
+
whenToUse: KEYMOUSE_SKILL_WHEN_TO_USE,
|
|
447
|
+
invocation: { modelInvocable: true, userInvocable: false },
|
|
448
|
+
provider: 'tool-keymouse',
|
|
449
|
+
source: 'runtime',
|
|
450
|
+
content: KEYMOUSE_SKILL_CONTENT,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export { apply };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const KEYMOUSE_SKILL_NAME = "keymouse";
|
|
2
|
+
export declare const KEYMOUSE_SKILL_DESCRIPTION = "\u952E\u9F20\u63A7\u5236\u5DE5\u5177\uFF1A\u7528 SendInput \u6A21\u62DF\u771F\u5B9E\u952E\u76D8/\u9F20\u6807\u8F93\u5165\u3001\u622A\u5C4F\u3001\u67E5\u8BE2\u4E0E\u7F6E\u9876\u7A97\u53E3\uFF0C\u7528\u4E8E\u684C\u9762\u81EA\u52A8\u5316\uFF08\u6253\u5F00\u5E94\u7528\u3001\u767B\u5F55\u3001\u70B9\u51FB\u3001\u8F93\u5165\u3001\u62D6\u62FD\u3001\u6EDA\u52A8\u3001\u622A\u56FE\u89C2\u5BDF\u3001\u7A97\u53E3\u5B9A\u4F4D/\u7F6E\u9876\uFF09\u3002";
|
|
3
|
+
export declare const KEYMOUSE_SKILL_WHEN_TO_USE = "\u4EFB\u4F55\u9700\u8981\u6A21\u62DF\u771F\u5B9E\u952E\u76D8\u6309\u4E0B/\u62AC\u8D77\u3001\u9F20\u6807\u70B9\u51FB/\u79FB\u52A8/\u6EDA\u8F6E\u3001\u8F93\u5165\u6587\u5B57\u6216\u64CD\u4F5C\u524D\u53F0\u7A97\u53E3\u7684\u81EA\u52A8\u5316\u4EFB\u52A1\u3002";
|
|
4
|
+
export declare const KEYMOUSE_SKILL_CONTENT = "# \u952E\u9F20\u63A7\u5236\uFF08KeyMouse\uFF09\n\n\u901A\u8FC7\u5E38\u9A7B helper \u7528 SendInput \u6A21\u62DF\u771F\u5B9E\u952E\u9F20\u8F93\u5165\uFF0C\u5BF9\u5F53\u524D\u524D\u53F0\u7A97\u53E3\u751F\u6548\u3002\u6240\u6709\u5DE5\u5177\u8C03\u7528\u7ED3\u675F\u540E\u5FC5\u987B\u8DD1\u4E00\u6B21 release_all\u3002\n\n## \u5750\u6807\u4E0E\u952E\u540D\n\n- \u5750\u6807\uFF1A\u7EDD\u5BF9\u5C4F\u5E55**\u7269\u7406**\u50CF\u7D20\uFF08helper \u5DF2 DPI-aware\uFF0C\u65E0\u7F29\u653E\u6362\u7B97\uFF09\uFF0C\u4E3B\u5C4F\u5DE6\u4E0A\u89D2\u4E3A (0,0)\uFF0C\u5355\u5C4F\u3002x \u5411\u53F3\u3001y \u5411\u4E0B\u3002\u622A\u56FE\u5206\u8FA8\u7387\u5373\u7269\u7406\u50CF\u7D20\uFF0C\u8BC6\u56FE\u770B\u5230\u7684\u5750\u6807\u53EF\u76F4\u63A5\u7528\u4E8E mouse_move\uFF0C\u65E0\u9700\u6362\u7B97\u3002\n- \u952E\u540D\uFF08\u5C0F\u5199\u82F1\u6587\uFF09\uFF1A\u5B57\u6BCD `a`-`z`\u3001\u6570\u5B57 `0`-`9`\u3001`space`\u3001`enter`\u3001`tab`\u3001`esc`\u3001`backspace`\u3001`delete`\u3001`insert`\u3001`home`\u3001`end`\u3001`pageup`\u3001`pagedown`\u3001\u65B9\u5411\u952E `up`/`down`/`left`/`right`\u3001`f1`-`f12`\u3001\u4FEE\u9970\u952E `ctrl`\u3001`alt`\u3001`shift`\u3001`win`\u3001`capslock`\u3002\n- \u9F20\u6807\u6309\u94AE\uFF1A`left`\u3001`right`\u3001`middle`\u3001`x1`\u3001`x2`\u3002\n\n## \u5DE5\u5177\n\n### screenshot()\n\u622A\u53D6\u5F53\u524D\u4E3B\u5C4F\uFF08\u5168\u5C4F\uFF09\u4FDD\u5B58\u4E3A PNG\uFF0C\u8FD4\u56DE\u56FE\u7247\u8DEF\u5F84\u4E0E\u5C3A\u5BF8\u3002\u6A21\u578B\u662F\u591A\u6A21\u6001\u7684\uFF0C\u53EF\u76F4\u63A5\u770B\uFF1A\nscreenshot() \u2192 \u62FF\u5230 path \u2192 read_image(path) \u67E5\u770B\u5C4F\u5E55 \u2192 \u518D\u505A\u952E\u9F20\u64CD\u4F5C\u3002\n\n### get_window()\n\u83B7\u53D6\u5F53\u524D\u524D\u53F0\u7A97\u53E3\u4FE1\u606F\uFF1A\u7A97\u53E3\u53E5\u67C4 hwnd\u3001\u6807\u9898 title\u3001\u8FDB\u7A0B\u540D process\u3001pid\u3001\u4F4D\u7F6E (x,y,w,h)\u3002\n\u7528\u4E8E\u786E\u8BA4\u76EE\u6807\u7A97\u53E3\u3001\u62FF\u5230 hwnd \u518D\u5B9A\u4F4D\u6216\u7F6E\u9876\u3002\n\n### win_rect(hwnd?)\n\u83B7\u53D6\u7A97\u53E3\u4F4D\u7F6E\u4E0E\u5C3A\u5BF8\uFF1A\u4F20\u5165 hwnd\uFF08\u6765\u81EA get_window\uFF09\uFF0C\u4E0D\u4F20\u9ED8\u8BA4\u5F53\u524D\u524D\u53F0\u7A97\u53E3\u3002\u8FD4\u56DE x/y/w/h\u3002\n\n### win_find(process)\n\u6309\u8FDB\u7A0B\u540D\u67E5\u627E\u7A97\u53E3\uFF1A\u8FD4\u56DE\u7B2C\u4E00\u4E2A\u5339\u914D\u7684\u7A97\u53E3\u53E5\u67C4 hwnd\u3001\u6807\u9898\u3001\u8FDB\u7A0B\u3001pid\u3001visible\uFF08\u662F\u5426\u53EF\u89C1\uFF0Cfalse \u8868\u793A\u6700\u5C0F\u5316\u5230\u6258\u76D8\uFF09\u3002\u7528\u4E8E\u66FF\u4EE3\u9F20\u6807\u70B9\u4EFB\u52A1\u680F\u56FE\u6807\uFF0C\u5B9A\u4F4D\u5E76\u5524\u8D77\u67D0\u7A0B\u5E8F\u7A97\u53E3\u3002\n\u4F8B\uFF1Awin_find('QQ') \u2192 \u62FF\u5230 hwnd\uFF0C\u518D win_activate(hwnd) \u5524\u8D77 QQ \u4E3B\u7A97\u53E3\u3002\n\n### win_activate(hwnd)\n\u6FC0\u6D3B/\u524D\u7F6E\u6307\u5B9A\u7A97\u53E3\uFF1A\u81EA\u52A8\u5904\u7406\u6700\u5C0F\u5316\u4E0E\"\u6700\u5C0F\u5316\u5230\u6258\u76D8\uFF08\u9690\u85CF\uFF09\"\u4E24\u79CD\u60C5\u51B5\uFF0C\u518D\u7F6E\u524D\u53F0\u3002\u914D\u5408 win_find \u4F7F\u7528\uFF0C\u4EFB\u52A1\u7ED3\u675F\u65E0\u9700\u989D\u5916\u6E05\u7406\u3002\n\n### win_minimize(hwnd?)\n\u6700\u5C0F\u5316\u6307\u5B9A\u7A97\u53E3\uFF1A\u4F20\u5165 hwnd\uFF08\u6765\u81EA get_window\uFF09\uFF0C\u4E0D\u4F20\u9ED8\u8BA4\u5F53\u524D\u524D\u53F0\u7A97\u53E3\u3002\n\n### win_restore(hwnd?)\n\u8FD8\u539F\u6307\u5B9A\u7A97\u53E3\uFF1A\u4ECE\u6700\u5C0F\u5316\u6216\u6700\u5927\u5316\u8FD8\u539F\u5230\u4E4B\u524D\u5927\u5C0F\u3002\u4E0D\u4F20\u9ED8\u8BA4\u5F53\u524D\u524D\u53F0\u7A97\u53E3\u3002\n\n### win_maximize(hwnd?)\n\u6700\u5927\u5316\u6307\u5B9A\u7A97\u53E3\u3002\u4E0D\u4F20\u9ED8\u8BA4\u5F53\u524D\u524D\u53F0\u7A97\u53E3\u3002\n\n### win_top(hwnd, on)\n\u7F6E\u9876\uFF08true\uFF09/\u53D6\u6D88\u7F6E\u9876\uFF08false\uFF09\u6307\u5B9A\u7A97\u53E3\u3002\u7F6E\u9876\u540E\u7A97\u53E3\u5E38\u9A7B\u6700\u524D\u3002\u4EFB\u52A1\u7ED3\u675F\u5FC5\u987B\u7EDF\u4E00 release_top \u53D6\u6D88\u3002\n\n### release_top()\n\u53D6\u6D88\u6240\u6709\u672C\u63D2\u4EF6\u7F6E\u9876\u8FC7\u7684\u7A97\u53E3\uFF0C\u6062\u590D\u9ED8\u8BA4\u5C42\u7EA7\u3002\u4EFB\u52A1\u7ED3\u675F\u65F6\u5FC5\u987B\u8C03\u7528\u4E00\u6B21\u3002\n\n### key_tap(key)\n\u6309\u4E0B\u5E76\u7ACB\u523B\u62AC\u8D77\u5355\u4E2A\u952E\uFF08\u5355\u4E2A\u52A8\u4F5C\uFF0C\u4E0D\u5361\u952E\uFF09\u3002\u7528\u4E8E\u70B9\u6309\u3001\u8F93\u5165\u5355\u5B57\u7B26\u6216\u5355\u4E2A\u5FEB\u6377\u952E\u6309\u952E\u3002\n\u4F8B\uFF1Akey_tap('enter') \u786E\u8BA4\uFF1Bkey_tap('space') \u7FFB\u9875\u3002\n\n### key_press(key)\n\u53EA\u6309\u4E0B\u67D0\u952E\u3001\u4E0D\u62AC\u8D77\u3002\u7528\u4E8E\u7EC4\u5408\u952E\uFF1A\u5148 key_press('ctrl')\uFF0C\u518D\u6309\u5176\u4ED6\u952E\uFF0C\u6700\u540E release_all\u3002\n\u6CE8\u610F\uFF1A\u5FC5\u987B\u914D\u5408 release_all \u62AC\u8D77\uFF0C\u5426\u5219\u952E\u4F1A\u4E00\u76F4\u6309\u4F4F\u3002\n\n### key_up(key)\n\u62AC\u8D77\u67D0\u952E\u3002\n\n### mouse_press(button)\n\u53EA\u6309\u4E0B\u9F20\u6807\u6309\u94AE\u3001\u4E0D\u62AC\u8D77\u3002\u7528\u4E8E\u6309\u4F4F\u5DE6\u952E\u62D6\u52A8\uFF1Amouse_press('left') \u2192 mouse_move(...) \u2192 release_all\u3002\n\n### mouse_move(x,y)\n\u628A\u9F20\u6807\u7EDD\u5BF9\u79FB\u52A8\u5230\u5C4F\u5E55\u5750\u6807 (x,y)\u3002\n\n### mouse_roll(up)\n\u6EDA\u52A8\u9F20\u6807\u6EDA\u8F6E\uFF1Aup \u4E3A true \u6EDA\u4E0A\u3001false \u6EDA\u4E0B\u3002\n\n### release_all()\n\u62AC\u8D77\u6240\u6709\u4ECD\u88AB\u6309\u4F4F\u7684\u952E\u4E0E\u9F20\u6807\u6309\u94AE\u3002\u6BCF\u6B21\u952E\u9F20\u64CD\u4F5C\u7ED3\u675F\u540E\u5FC5\u987B\u8C03\u7528\u4E00\u6B21\uFF0C\u907F\u514D\u5361\u952E\u3002\n\n## \u8FD4\u56DE\n\n\u6BCF\u4E2A\u5DE5\u5177\u8FD4\u56DE ok=true \u8868\u793A\u8BE5\u547D\u4EE4\u5DF2\u4EA4\u7ED9\u7CFB\u7EDF\uFF1B\u8FD4\u56DE error \u8868\u793A\u53C2\u6570\u6216\u952E\u540D\u4E0D\u5408\u6CD5\u3002screenshot \u8FD8\u8FD4\u56DE path\uFF08PNG \u7EDD\u5BF9\u8DEF\u5F84\uFF09\u3001width/height\uFF08\u50CF\u7D20\u5C3A\u5BF8\uFF09\u3002win_find \u8FD8\u8FD4\u56DE hwnd\u3001title\u3001process\u3001pid\u3001visible\u3001x/y/w/h\u3002\n\n## \u5178\u578B\u7EC4\u5408\n\n- Ctrl+S \u4FDD\u5B58\uFF1Akey_press('ctrl')\uFF1Bkey_tap('s')\uFF1Brelease_all\u3002\n- Ctrl+O \u6253\u5F00\uFF1Akey_press('ctrl')\uFF1Bkey_tap('o')\uFF1Brelease_all\u3002\n- \u5DE6\u952E\u62D6\u62FD\uFF1Amouse_press('left')\uFF1Bmouse_move(x2,y2)\uFF1Bmouse_move(x1,y1)\uFF1Brelease_all\u3002\n- \u6EDA\u5230\u9875\u9762\u5E95\u90E8\uFF1Amouse_roll(false)\uFF1Bmouse_roll(false)\uFF1B\u2026\uFF08\u53EF\u591A\u6B21\uFF09\u3002\n- \u89C6\u89C9\u95ED\u73AF\uFF1Ascreenshot() \u5F97 path \u2192 read_image(path) \u770B\u5C4F\u5E55 \u2192 \u952E\u9F20\u64CD\u4F5C\u3002\n- \u5524\u8D77\u7A0B\u5E8F\u7A97\u53E3\uFF08\u4E0D\u70B9\u56FE\u6807\uFF09\uFF1Awin_find('QQ') \u2192 win_activate(hwnd) \u2192 \u524D\u53F0\u51FA\u73B0 QQ \u4E3B\u7A97\u53E3\uFF08\u6258\u76D8\u9690\u85CF/\u6700\u5C0F\u5316\u4E5F\u80FD\u5524\u56DE\uFF09\u3002\n\n## \u89C4\u8303\n\n1. \u64CD\u4F5C\u524D\u5148\u786E\u8BA4\u76EE\u6807\u7A97\u53E3\u4E0E\u9F20\u6807\u4F4D\u7F6E\uFF08\u53EF\u5148 mouse_move \u518D\u89C2\u5BDF\u7ED3\u679C\uFF09\u3002\n2. \u7528\u7EC4\u5408\u952E/\u6309\u4F4F\u540E\uFF0C\u7ED3\u675F\u7EDF\u4E00\u8C03\u7528 release_all\u3002\n3. \u7F6E\u9876\u7A97\u53E3\u540E\uFF0C\u4EFB\u52A1\u7ED3\u675F\u7EDF\u4E00\u8C03\u7528 release_top \u53D6\u6D88\u3002\n4. \u5DE5\u5177\u503C\u662F\u5373\u65F6\u7684\uFF1ASendInput \u5FAE\u79D2\u7EA7\u751F\u6548\uFF0C\u4E0D\u9700\u8981\u7B49\u5F85\u3002\n";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// skill-content.ts — 键鼠控制 skill 的固定说明书内容(单一来源)
|
|
2
|
+
// 服务端注册为 runtime skill,模型通过 skill 工具读取;技能名 keymouse。
|
|
3
|
+
// 此为"工具调用说明书",非流程:说明每个工具用途、参数、返回、一个例子,以及用完统一 release_all 的提醒。
|
|
4
|
+
export const KEYMOUSE_SKILL_NAME = 'keymouse';
|
|
5
|
+
export const KEYMOUSE_SKILL_DESCRIPTION = '键鼠控制工具:用 SendInput 模拟真实键盘/鼠标输入、截屏、查询与置顶窗口,用于桌面自动化(打开应用、登录、点击、输入、拖拽、滚动、截图观察、窗口定位/置顶)。';
|
|
6
|
+
export const KEYMOUSE_SKILL_WHEN_TO_USE = '任何需要模拟真实键盘按下/抬起、鼠标点击/移动/滚轮、输入文字或操作前台窗口的自动化任务。';
|
|
7
|
+
export const KEYMOUSE_SKILL_CONTENT = `# 键鼠控制(KeyMouse)
|
|
8
|
+
|
|
9
|
+
通过常驻 helper 用 SendInput 模拟真实键鼠输入,对当前前台窗口生效。所有工具调用结束后必须跑一次 release_all。
|
|
10
|
+
|
|
11
|
+
## 坐标与键名
|
|
12
|
+
|
|
13
|
+
- 坐标:绝对屏幕**物理**像素(helper 已 DPI-aware,无缩放换算),主屏左上角为 (0,0),单屏。x 向右、y 向下。截图分辨率即物理像素,识图看到的坐标可直接用于 mouse_move,无需换算。
|
|
14
|
+
- 键名(小写英文):字母 \`a\`-\`z\`、数字 \`0\`-\`9\`、\`space\`、\`enter\`、\`tab\`、\`esc\`、\`backspace\`、\`delete\`、\`insert\`、\`home\`、\`end\`、\`pageup\`、\`pagedown\`、方向键 \`up\`/\`down\`/\`left\`/\`right\`、\`f1\`-\`f12\`、修饰键 \`ctrl\`、\`alt\`、\`shift\`、\`win\`、\`capslock\`。
|
|
15
|
+
- 鼠标按钮:\`left\`、\`right\`、\`middle\`、\`x1\`、\`x2\`。
|
|
16
|
+
|
|
17
|
+
## 工具
|
|
18
|
+
|
|
19
|
+
### screenshot()
|
|
20
|
+
截取当前主屏(全屏)保存为 PNG,返回图片路径与尺寸。模型是多模态的,可直接看:
|
|
21
|
+
screenshot() → 拿到 path → read_image(path) 查看屏幕 → 再做键鼠操作。
|
|
22
|
+
|
|
23
|
+
### get_window()
|
|
24
|
+
获取当前前台窗口信息:窗口句柄 hwnd、标题 title、进程名 process、pid、位置 (x,y,w,h)。
|
|
25
|
+
用于确认目标窗口、拿到 hwnd 再定位或置顶。
|
|
26
|
+
|
|
27
|
+
### win_rect(hwnd?)
|
|
28
|
+
获取窗口位置与尺寸:传入 hwnd(来自 get_window),不传默认当前前台窗口。返回 x/y/w/h。
|
|
29
|
+
|
|
30
|
+
### win_find(process)
|
|
31
|
+
按进程名查找窗口:返回第一个匹配的窗口句柄 hwnd、标题、进程、pid、visible(是否可见,false 表示最小化到托盘)。用于替代鼠标点任务栏图标,定位并唤起某程序窗口。
|
|
32
|
+
例:win_find('QQ') → 拿到 hwnd,再 win_activate(hwnd) 唤起 QQ 主窗口。
|
|
33
|
+
|
|
34
|
+
### win_activate(hwnd)
|
|
35
|
+
激活/前置指定窗口:自动处理最小化与"最小化到托盘(隐藏)"两种情况,再置前台。配合 win_find 使用,任务结束无需额外清理。
|
|
36
|
+
|
|
37
|
+
### win_minimize(hwnd?)
|
|
38
|
+
最小化指定窗口:传入 hwnd(来自 get_window),不传默认当前前台窗口。
|
|
39
|
+
|
|
40
|
+
### win_restore(hwnd?)
|
|
41
|
+
还原指定窗口:从最小化或最大化还原到之前大小。不传默认当前前台窗口。
|
|
42
|
+
|
|
43
|
+
### win_maximize(hwnd?)
|
|
44
|
+
最大化指定窗口。不传默认当前前台窗口。
|
|
45
|
+
|
|
46
|
+
### win_top(hwnd, on)
|
|
47
|
+
置顶(true)/取消置顶(false)指定窗口。置顶后窗口常驻最前。任务结束必须统一 release_top 取消。
|
|
48
|
+
|
|
49
|
+
### release_top()
|
|
50
|
+
取消所有本插件置顶过的窗口,恢复默认层级。任务结束时必须调用一次。
|
|
51
|
+
|
|
52
|
+
### key_tap(key)
|
|
53
|
+
按下并立刻抬起单个键(单个动作,不卡键)。用于点按、输入单字符或单个快捷键按键。
|
|
54
|
+
例:key_tap('enter') 确认;key_tap('space') 翻页。
|
|
55
|
+
|
|
56
|
+
### key_press(key)
|
|
57
|
+
只按下某键、不抬起。用于组合键:先 key_press('ctrl'),再按其他键,最后 release_all。
|
|
58
|
+
注意:必须配合 release_all 抬起,否则键会一直按住。
|
|
59
|
+
|
|
60
|
+
### key_up(key)
|
|
61
|
+
抬起某键。
|
|
62
|
+
|
|
63
|
+
### mouse_press(button)
|
|
64
|
+
只按下鼠标按钮、不抬起。用于按住左键拖动:mouse_press('left') → mouse_move(...) → release_all。
|
|
65
|
+
|
|
66
|
+
### mouse_move(x,y)
|
|
67
|
+
把鼠标绝对移动到屏幕坐标 (x,y)。
|
|
68
|
+
|
|
69
|
+
### mouse_roll(up)
|
|
70
|
+
滚动鼠标滚轮:up 为 true 滚上、false 滚下。
|
|
71
|
+
|
|
72
|
+
### release_all()
|
|
73
|
+
抬起所有仍被按住的键与鼠标按钮。每次键鼠操作结束后必须调用一次,避免卡键。
|
|
74
|
+
|
|
75
|
+
## 返回
|
|
76
|
+
|
|
77
|
+
每个工具返回 ok=true 表示该命令已交给系统;返回 error 表示参数或键名不合法。screenshot 还返回 path(PNG 绝对路径)、width/height(像素尺寸)。win_find 还返回 hwnd、title、process、pid、visible、x/y/w/h。
|
|
78
|
+
|
|
79
|
+
## 典型组合
|
|
80
|
+
|
|
81
|
+
- Ctrl+S 保存:key_press('ctrl');key_tap('s');release_all。
|
|
82
|
+
- Ctrl+O 打开:key_press('ctrl');key_tap('o');release_all。
|
|
83
|
+
- 左键拖拽:mouse_press('left');mouse_move(x2,y2);mouse_move(x1,y1);release_all。
|
|
84
|
+
- 滚到页面底部:mouse_roll(false);mouse_roll(false);…(可多次)。
|
|
85
|
+
- 视觉闭环:screenshot() 得 path → read_image(path) 看屏幕 → 键鼠操作。
|
|
86
|
+
- 唤起程序窗口(不点图标):win_find('QQ') → win_activate(hwnd) → 前台出现 QQ 主窗口(托盘隐藏/最小化也能唤回)。
|
|
87
|
+
|
|
88
|
+
## 规范
|
|
89
|
+
|
|
90
|
+
1. 操作前先确认目标窗口与鼠标位置(可先 mouse_move 再观察结果)。
|
|
91
|
+
2. 用组合键/按住后,结束统一调用 release_all。
|
|
92
|
+
3. 置顶窗口后,任务结束统一调用 release_top 取消。
|
|
93
|
+
4. 工具值是即时的:SendInput 微秒级生效,不需要等待。
|
|
94
|
+
`;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@temsmie/dsh-tool-keymouse",
|
|
3
|
+
"description": "Windows key/mouse/screenshot/window automation (SendInput over a persistent helper) for the DeepSeek Harness",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"types": "lib/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./skill-content": {
|
|
17
|
+
"types": "./lib/skill-content.d.ts",
|
|
18
|
+
"default": "./lib/skill-content.js"
|
|
19
|
+
},
|
|
20
|
+
"./src/*": "./src/*",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib/index.js",
|
|
25
|
+
"lib/skill-content.js",
|
|
26
|
+
"lib/**/*.d.ts",
|
|
27
|
+
"bin/helper-x64.exe",
|
|
28
|
+
"skills/keymouse.md",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"cordis.patch.yml"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dsh": {
|
|
35
|
+
"bundle": {
|
|
36
|
+
"patch": "./cordis.patch.yml"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
+
"@deepseek-ai/dsh-skill": "^0.1.1-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^26.5.0",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 键鼠控制(KeyMouse)
|
|
2
|
+
|
|
3
|
+
通过常驻 helper 用 SendInput 模拟真实键鼠输入,对当前前台窗口生效。所有工具调用结束后必须跑一次 release_all。
|
|
4
|
+
|
|
5
|
+
## 坐标与键名
|
|
6
|
+
|
|
7
|
+
- 坐标:绝对屏幕**物理**像素(helper 已 DPI-aware,无缩放换算),主屏左上角为 (0,0),单屏。x 向右、y 向下。截图分辨率即物理像素,识图看到的坐标可直接用于 mouse_move,无需换算。
|
|
8
|
+
- 键名(小写英文):字母 `a`-`z`、数字 `0`-`9`、`space`、`enter`、`tab`、`esc`、`backspace`、`delete`、`insert`、`home`、`end`、`pageup`、`pagedown`、方向键 `up`/`down`/`left`/`right`、`f1`-`f12`、修饰键 `ctrl`、`alt`、`shift`、`win`、`capslock`。
|
|
9
|
+
- 鼠标按钮:`left`、`right`、`middle`、`x1`、`x2`。
|
|
10
|
+
|
|
11
|
+
## 工具
|
|
12
|
+
|
|
13
|
+
### screenshot()
|
|
14
|
+
截取当前主屏(全屏)保存为 PNG,返回图片路径与尺寸。模型是多模态的,可直接看:
|
|
15
|
+
screenshot() → 拿到 path → read_image(path) 查看屏幕 → 再做键鼠操作。
|
|
16
|
+
|
|
17
|
+
### get_window()
|
|
18
|
+
获取当前前台窗口信息:窗口句柄 hwnd、标题 title、进程名 process、pid、位置 (x,y,w,h)。
|
|
19
|
+
用于确认目标窗口、拿到 hwnd 再定位或置顶。
|
|
20
|
+
|
|
21
|
+
### win_rect(hwnd?)
|
|
22
|
+
获取窗口位置与尺寸:传入 hwnd(来自 get_window),不传默认当前前台窗口。返回 x/y/w/h。
|
|
23
|
+
|
|
24
|
+
### win_find(process)
|
|
25
|
+
按进程名查找窗口:返回第一个匹配的窗口句柄 hwnd、标题、进程、pid、visible(是否可见,false 表示最小化到托盘)。用于替代鼠标点任务栏图标,定位并唤起某程序窗口。
|
|
26
|
+
例:win_find('QQ') → 拿到 hwnd,再 win_activate(hwnd) 唤起 QQ 主窗口。
|
|
27
|
+
|
|
28
|
+
### win_activate(hwnd)
|
|
29
|
+
激活/前置指定窗口:自动处理最小化与"最小化到托盘(隐藏)"两种情况,再置前台。配合 win_find 使用,任务结束无需额外清理。
|
|
30
|
+
|
|
31
|
+
### win_minimize(hwnd?)
|
|
32
|
+
最小化指定窗口:传入 hwnd(来自 get_window),不传默认当前前台窗口。
|
|
33
|
+
|
|
34
|
+
### win_restore(hwnd?)
|
|
35
|
+
还原指定窗口:从最小化或最大化还原到之前大小。不传默认当前前台窗口。
|
|
36
|
+
|
|
37
|
+
### win_maximize(hwnd?)
|
|
38
|
+
最大化指定窗口。不传默认当前前台窗口。
|
|
39
|
+
|
|
40
|
+
### win_top(hwnd, on)
|
|
41
|
+
置顶(true)/取消置顶(false)指定窗口。置顶后窗口常驻最前。任务结束必须统一 release_top 取消。
|
|
42
|
+
|
|
43
|
+
### release_top()
|
|
44
|
+
取消所有本插件置顶过的窗口,恢复默认层级。任务结束时必须调用一次。
|
|
45
|
+
|
|
46
|
+
### key_tap(key)
|
|
47
|
+
按下并立刻抬起单个键(单个动作,不卡键)。用于点按、输入单字符或单个快捷键按键。
|
|
48
|
+
例:key_tap('enter') 确认;key_tap('space') 翻页。
|
|
49
|
+
|
|
50
|
+
### key_press(key)
|
|
51
|
+
只按下某键、不抬起。用于组合键:先 key_press('ctrl'),再按其他键,最后 release_all。
|
|
52
|
+
注意:必须配合 release_all 抬起,否则键会一直按住。
|
|
53
|
+
|
|
54
|
+
### key_up(key)
|
|
55
|
+
抬起某键。
|
|
56
|
+
|
|
57
|
+
### mouse_press(button)
|
|
58
|
+
只按下鼠标按钮、不抬起。用于按住左键拖动:mouse_press('left') → mouse_move(...) → release_all。
|
|
59
|
+
|
|
60
|
+
### mouse_move(x,y)
|
|
61
|
+
把鼠标绝对移动到屏幕坐标 (x,y)。
|
|
62
|
+
|
|
63
|
+
### mouse_roll(up)
|
|
64
|
+
滚动鼠标滚轮:up 为 true 滚上、false 滚下。
|
|
65
|
+
|
|
66
|
+
### release_all()
|
|
67
|
+
抬起所有仍被按住的键与鼠标按钮。每次键鼠操作结束后必须调用一次,避免卡键。
|
|
68
|
+
|
|
69
|
+
## 返回
|
|
70
|
+
|
|
71
|
+
每个工具返回 ok=true 表示该命令已交给系统;返回 error 表示参数或键名不合法。screenshot 还返回 path(PNG 绝对路径)、width/height(像素尺寸)。win_find 还返回 hwnd、title、process、pid、visible、x/y/w/h。
|
|
72
|
+
|
|
73
|
+
## 典型组合
|
|
74
|
+
|
|
75
|
+
- Ctrl+S 保存:key_press('ctrl');key_tap('s');release_all。
|
|
76
|
+
- Ctrl+O 打开:key_press('ctrl');key_tap('o');release_all。
|
|
77
|
+
- 左键拖拽:mouse_press('left');mouse_move(x2,y2);mouse_move(x1,y1);release_all。
|
|
78
|
+
- 滚到页面底部:mouse_roll(false);mouse_roll(false);…(可多次)。
|
|
79
|
+
- 视觉闭环:screenshot() 得 path → read_image(path) 看屏幕 → 键鼠操作。
|
|
80
|
+
- 唤起程序窗口(不点图标):win_find('QQ') → win_activate(hwnd) → 前台出现 QQ 主窗口(托盘隐藏/最小化也能唤回)。
|
|
81
|
+
|
|
82
|
+
## 规范
|
|
83
|
+
|
|
84
|
+
1. 操作前先确认目标窗口与鼠标位置(可先 mouse_move 再观察结果)。
|
|
85
|
+
2. 用组合键/按住后,结束统一调用 release_all。
|
|
86
|
+
3. 置顶窗口后,任务结束统一调用 release_top 取消。
|
|
87
|
+
4. 工具值是即时的:SendInput 微秒级生效,不需要等待。
|