dsh-copilot-vision 1.0.0 → 1.3.1

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/src/diff.js CHANGED
@@ -3,8 +3,9 @@
3
3
  //
4
4
  // 边界(有意为之,不是欠债):
5
5
  // - localPath 仍是原尺寸裁剪:局部坐标 + bbox 左上角 = 全局物理坐标,模型可以直接换算。
6
- // - 额外产出的 gridPath 是 4x 最近邻放大的「网格放大镜」(见 render.js),
7
- // 它只用于目测/相对点击,不改变 localPath 的 1:1 语义。
6
+ // - 额外产出的 gridPath 是按面积与单边长度自适应倍率的「网格放大镜」(小区域 4x / 中区域 2x / 大区域 1x,
7
+ // 放大后单边不超过 2000 像素,见 render.js),
8
+ // 它只用于目测/相对点击,不改变 localPath 的 1:1 语义;倍数随区域以 gridScale 字段返回。
8
9
  // - 不做高频抽帧,只在调用时截一张当前图。
9
10
  // - 尺寸/缩放不一致时直接抛错,不做任何重采样或硬算 —— 显示器或缩放变了,
10
11
  // 旧截图的坐标系已经不可信,勉强对齐只会给出错误坐标。
@@ -13,14 +14,14 @@ import { join } from 'node:path'
13
14
  import { PNG } from 'pngjs'
14
15
  import pixelmatch from 'pixelmatch'
15
16
  import { screenshotAction } from './actions.js'
16
- import { get, recordRegions } from './shot-index.js'
17
+ import { get, recordRegions, assertShotFileExists } from './shot-index.js'
17
18
  import { resolveShotsDir } from './paths.js'
18
- import { generateGridImage, MAX_GRID_SOURCE_AREA } from './render.js'
19
+ import { generateGridImage, resolveGridScale } from './render.js'
19
20
 
20
21
  // 小于这个像素数的连通区域按噪声丢弃(光标闪烁、 blinking caret、亚像素抖动)。
21
22
  export const DEFAULT_MIN_REGION_AREA = 100
22
23
 
23
- // 网格图的成本是「每个区域一次 4x 放大 + PNG 编码」,差异块很多时(实测出现过单次 105 块)
24
+ // 网格图的成本是「每个区域一次放大 + PNG 编码」,差异块很多时(实测出现过单次 105 块)
24
25
  // 全量生成只会拖慢工具并堆满临时目录,所以只给前 N 个(按面积降序)配网格图。
25
26
  export const MAX_GRID_REGIONS = 12
26
27
 
@@ -148,10 +149,8 @@ export function diffPair({
148
149
  }
149
150
  if (i >= MAX_GRID_REGIONS) {
150
151
  region.gridSkipped = `只给前 ${MAX_GRID_REGIONS} 个区域生成网格图(本区域排第 ${i + 1})`
151
- } else if (bbox.width * bbox.height > MAX_GRID_SOURCE_AREA) {
152
- region.gridSkipped =
153
- `区域 ${bbox.width}x${bbox.height} 超过网格图上限 ${MAX_GRID_SOURCE_AREA} 物理像素,直接看全屏更合适`
154
152
  } else {
153
+ region.gridScale = resolveGridScale(bbox.width, bbox.height)
155
154
  region.gridPath = generateGridImage(currentMeta.path, bbox, currentMeta.scaleFactor, {
156
155
  shotId: currentShotId,
157
156
  index: i + 1,
@@ -187,6 +186,7 @@ export async function diffAction({
187
186
  `vp_screenshot_diff: baseline shotId ${baseline} 不存在,可先用 vp_screenshot 截一张`,
188
187
  )
189
188
  }
189
+ assertShotFileExists(baseline, baselineMeta, 'vp_screenshot_diff')
190
190
  const current = await screenshotAction()
191
191
  return diffPair({
192
192
  baselineMeta,
package/src/keys.js ADDED
@@ -0,0 +1,175 @@
1
+ // 按键写法解析:把 "enter" / "ctrl+c" / "f5" 这类人类写法翻译成 robot-ts 的 Key 码与修饰键。
2
+ //
3
+ // 键码不在这里写死:直接从 robot-ts 导出的 Key 枚举反查(枚举名即官方键名),
4
+ // 别名表只补口语写法(esc / del / pgup / 数字键…)。这样上游改键值时我们跟着走,
5
+ // 不会像写死常量那样悄悄注入错键。
6
+ //
7
+ // 解析是纯函数:Key/Modifier 枚举由调用方(actions.js)从 robot-ts 取出后传入,
8
+ // 这样本模块既不 import native,也不依赖宿主。
9
+
10
+ // 别名 → robot-ts Key 枚举名(小写形式)
11
+ const KEY_ALIASES = {
12
+ esc: 'escape',
13
+ return: 'enter',
14
+ del: 'delete',
15
+ ins: 'insert',
16
+ pgup: 'pageup',
17
+ pgdn: 'pagedown',
18
+ pgdown: 'pagedown',
19
+ up: 'uparrow',
20
+ down: 'downarrow',
21
+ left: 'leftarrow',
22
+ right: 'rightarrow',
23
+ plus: 'equal',
24
+ 1: 'digit1',
25
+ 2: 'digit2',
26
+ 3: 'digit3',
27
+ 4: 'digit4',
28
+ 5: 'digit5',
29
+ 6: 'digit6',
30
+ 7: 'digit7',
31
+ 8: 'digit8',
32
+ 9: 'digit9',
33
+ 0: 'digit0',
34
+ '-': 'minus',
35
+ '=': 'equal',
36
+ '[': 'leftbracket',
37
+ ']': 'rightbracket',
38
+ '\\': 'backslash',
39
+ ';': 'semicolon',
40
+ "'": 'quote',
41
+ '`': 'grave',
42
+ ',': 'comma',
43
+ '.': 'period',
44
+ '/': 'slash',
45
+ }
46
+
47
+ // 别名 → robot-ts Modifier 枚举值
48
+ const MODIFIER_ALIASES = {
49
+ ctrl: 'Control',
50
+ control: 'Control',
51
+ shift: 'Shift',
52
+ alt: 'Alt',
53
+ option: 'Alt',
54
+ meta: 'Meta',
55
+ cmd: 'Meta',
56
+ win: 'Meta',
57
+ super: 'Meta',
58
+ }
59
+
60
+ // 字符键 → 它在文本里对应的字符。
61
+ //
62
+ // 只给「无修饰键的字符键」准备:这类键改走 Unicode 直注(见 resolveUnicodeChar)。
63
+ // 原因是 robot-ts 的按键注入走 KEYEVENTF_SCANCODE,会经过系统输入法 ——
64
+ // 本机中文输入法实测会把字母键吞成拼音组合串(按下不落字,直到下一次 Enter 才吐出幽灵字符),
65
+ // 还会把引号改写成中文弯引号。Unicode 直注绕开输入法,字符原样落下。
66
+ //
67
+ // space 不在表里:扫描码空格实测正常,而且只有真实按键才能激活聚焦的按钮。
68
+ const CHARACTER_KEYS = {
69
+ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g', h: 'h', i: 'i', j: 'j',
70
+ k: 'k', l: 'l', m: 'm', n: 'n', o: 'o', p: 'p', q: 'q', r: 'r', s: 's', t: 't',
71
+ u: 'u', v: 'v', w: 'w', x: 'x', y: 'y', z: 'z',
72
+ digit1: '1', digit2: '2', digit3: '3', digit4: '4', digit5: '5',
73
+ digit6: '6', digit7: '7', digit8: '8', digit9: '9', digit0: '0',
74
+ minus: '-', equal: '=', leftbracket: '[', rightbracket: ']', backslash: '\\',
75
+ nonushash: '#', semicolon: ';', quote: "'", grave: '`', comma: ',', period: '.', slash: '/',
76
+ }
77
+
78
+ export const MODIFIER_NAMES = ['ctrl', 'shift', 'alt', 'meta']
79
+
80
+ /** Key 枚举 → 小写名到码值的索引(枚举成员名即官方键名)。 */
81
+ function keyIndex(keyEnum) {
82
+ const index = new Map()
83
+ for (const [name, value] of Object.entries(keyEnum)) {
84
+ if (typeof value === 'number') index.set(name.toLowerCase(), value)
85
+ }
86
+ return index
87
+ }
88
+
89
+ /**
90
+ * 解析按键写法。
91
+ * 返回 { code, keyName, modifiers, spec }:
92
+ * code robot-ts 的 Key 码(数字)
93
+ * keyName 规范化的主键名(robot-ts 枚举名)
94
+ * modifiers 修饰键枚举值数组,如 ['Control']
95
+ * spec 原样回显的写法
96
+ * 解析失败一律抛错,错误文案里带上可用的修饰键名。
97
+ */
98
+ export function parseKeySpec(spec, keyEnum, modifierEnum) {
99
+ if (typeof spec !== 'string') {
100
+ throw new Error(`vp_key: key 必须是字符串,收到 ${spec === null ? 'null' : typeof spec}`)
101
+ }
102
+ const trimmed = spec.trim()
103
+ if (trimmed === '') {
104
+ throw new Error('vp_key: key 不能为空字符串')
105
+ }
106
+
107
+ const parts = trimmed
108
+ .split('+')
109
+ .map((part) => part.trim())
110
+ .filter((part) => part !== '')
111
+ if (parts.length === 0) {
112
+ throw new Error('vp_key: key 不能为空字符串')
113
+ }
114
+
115
+ const modifiers = []
116
+ for (const part of parts.slice(0, -1)) {
117
+ const modifier = MODIFIER_ALIASES[part.toLowerCase()]
118
+ if (modifier === undefined) {
119
+ throw new Error(
120
+ `vp_key: 不认识的修饰键 "${part}"(可用:${MODIFIER_NAMES.join(' / ')})`,
121
+ )
122
+ }
123
+ if (!Object.values(modifierEnum).includes(modifier)) {
124
+ throw new Error(`vp_key: robot-ts 不支持修饰键 "${modifier}"`)
125
+ }
126
+ modifiers.push(modifier)
127
+ }
128
+
129
+ const rawKey = parts[parts.length - 1]
130
+ const lowerKey = rawKey.toLowerCase()
131
+ if (MODIFIER_ALIASES[lowerKey] !== undefined) {
132
+ throw new Error(
133
+ `vp_key: "${rawKey}" 只是修饰键,必须同时给一个主键(例如 ctrl+c、shift+tab)`,
134
+ )
135
+ }
136
+
137
+ const canonical = KEY_ALIASES[lowerKey] ?? lowerKey
138
+ const code = keyIndex(keyEnum).get(canonical)
139
+ if (code === undefined) {
140
+ throw new Error(
141
+ `vp_key: 不认识的按键 "${rawKey}"。可用示例:enter / esc / tab / space / backspace / ` +
142
+ 'delete / up / down / left / right / f1-f12 / a-z / 0-9 / ctrl+c / shift+tab',
143
+ )
144
+ }
145
+
146
+ return {
147
+ code,
148
+ keyName: canonical,
149
+ modifiers,
150
+ spec: trimmed,
151
+ }
152
+ }
153
+
154
+ /** 该键是否是一个字符键;是则返回它对应的字符,否则返回 null。 */
155
+ export function characterForKey(keyName) {
156
+ return CHARACTER_KEYS[String(keyName).toLowerCase()] ?? null
157
+ }
158
+
159
+ /**
160
+ * 判断这个键是否该改走 Unicode 直注,返回要注入的字符;不该改走就返回 null。
161
+ *
162
+ * 规则(依据本机实测):
163
+ * - 无修饰键的字符键 → 注入该字符本身
164
+ * - 只带 shift 的字母键 → 注入大写字母
165
+ * - 其余(带 ctrl/alt/meta 的组合键、控制键、space)→ null,走真实扫描码
166
+ */
167
+ export function resolveUnicodeChar(spec) {
168
+ const base = characterForKey(spec.keyName)
169
+ if (base === null) return null
170
+ if (spec.modifiers.length === 0) return base
171
+ if (spec.modifiers.length === 1 && spec.modifiers[0] === 'Shift' && /^[a-z]$/.test(base)) {
172
+ return base.toUpperCase()
173
+ }
174
+ return null
175
+ }