Skip to content

Commit 51ccadf

Browse files
committed
fix: prepare OpenGUI model and bound phone screenshots
1 parent 74f3c2e commit 51ccadf

9 files changed

Lines changed: 80 additions & 13 deletions

File tree

‎deepseek-harness-plugin/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Keep the phone unlocked and authorized for USB debugging, then send:
177177
/opengui Open Settings and report the Android version
178178
```
179179

180-
The native **OpenGUI** tab owns phone selection and status. One authorized phone is selected automatically. With multiple phones, check any subset in that workbench before sending `/opengui` or `@OpenGUI`. A non-empty task pauses before model routing until a phone is ready. Device selection remains editable while waiting, then locks after detection until the whole batch finishes.
180+
The native **OpenGUI** tab owns phone selection and status. One authorized phone is selected automatically. With multiple phones, check any subset in that workbench before sending `/opengui` or `@OpenGUI`. A non-empty task resolves the current-model choice or dedicated fallback setup before it waits for a phone, but it sends no provider request until the selected-device snapshot is ready. Device selection remains editable while waiting, then locks after detection until the whole batch finishes.
181181

182182
The native **OpenGUI** tab is a full-width device wall. It renders every visible phone in Host order and appends one connection guide, without reserving empty slots. Connected phones expand automatically into a complete, low-latency H.264 view while the tab, card, and page remain visible. First use asks before downloading and verifying the pinned scrcpy server; unsupported browsers or stream failures fall back to an uncropped JPEG preview. Each phone can also be opened in an optional independent scrcpy window.
183183

@@ -286,6 +286,7 @@ If you used a profile patch, also remove its `coremate-mobile` row. Leave `[]` w
286286
- Phone text accepts up to 500 Unicode characters. Safe ASCII uses `adb input text`; Chinese, emoji, and other Unicode text uses scrcpy's standard UTF-8 clipboard control message and must receive the matching device ACK. This is independent of the phone vendor and active input method, and never silently falls back to simulated keyboard typing.
287287
- `browser_control` exposes no arbitrary CDP, JavaScript execution, filesystem access, or non-HTTP(S) URL. Page mutations require the latest observation.
288288
- The current DSH route is proxied without copying credentials. Provider requests keep tool schemas, cancellation, streaming, and at most the latest phone screenshot.
289+
- Model-facing phone screenshots are JPEG quality 65 with preserved aspect ratio and a maximum 2048px long edge. Smaller frames are never enlarged; tap and swipe coordinates are mapped from that bounded screenshot back to the Android logical display size.
289290
- A capability failure never automatically retries a task that may already have caused phone or browser side effects.
290291
- Direct `/opengui` results are text-only; screenshots remain in the phone child session.
291292
- Native mirroring is view-only and opens only on the graphical computer running Harness; a headless Host cannot display the window.

‎deepseek-harness-plugin/README.zh.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ Usage: /opengui <task>
179179
/opengui 打开设置并报告 Android 版本
180180
```
181181

182-
手机选择和状态统一放在原生 **OpenGUI** Tab。只有一台已授权手机时会自动选中;有多台时,可在工作台中勾选任意一台或多台后再发送 `/opengui` 或 `@OpenGUI`。非空任务会先等待手机,不会提前调用模型;等待期间仍可连接、选择和投屏,检测成功后才锁定本次设备快照,直到整批结束。
182+
手机选择和状态统一放在原生 **OpenGUI** Tab。只有一台已授权手机时会自动选中;有多台时,可在工作台中勾选任意一台或多台后再发送 `/opengui` 或 `@OpenGUI`。非空任务会先确定当前模型或完成专用回退配置,再等待手机;在选定设备快照就绪前不会向 provider 发起模型请求。等待期间仍可连接、选择和投屏,检测成功后才锁定本次设备快照,直到整批结束。
183183

184184
原生 **OpenGUI** Tab 是全宽设备照片墙。界面按 Host 顺序展示全部可见手机,并在末尾追加唯一的连接说明卡,不会为凑列数预留空卡。新检测到的手机默认展开完整、低延迟的 H.264 实时画面;只有 Tab、设备卡和页面都可见时才保持连接。首次使用会先征得同意,再下载并校验固定版本的 scrcpy server;浏览器不支持或视频流失败时,会降级为不裁切的 JPEG 截图预览。每台手机还可按需打开独立的 scrcpy 窗口。
185185

@@ -288,6 +288,7 @@ dsh plugin --profile web remove dsh-coremate-mobile
288288
- 手机文本输入最多 500 个 Unicode 字符。安全 ASCII 直接使用 `adb input text`;中文、emoji 等文本使用 scrcpy 标准控制协议发送 UTF-8 剪贴板消息,并必须收到对应的设备 ACK。该路径与手机厂商和当前输入法无关,也不会静默退化为模拟屏幕键盘逐字输入。
289289
- `browser_control` 不暴露任意 CDP、JavaScript 执行、文件系统访问或非 HTTP(S) URL;页面修改必须引用最新观察。
290290
- 当前 DSH 路由通过内部代理转发,不复制凭据;provider 请求保留工具 schema、取消信号和流式结果,并且最多只保留最新手机截图。
291+
- 发给模型的手机截图保持原始宽高比,统一编码为质量 65、最长边不超过 2048px 的 JPEG;小图不会放大。点击和滑动坐标会从该截图空间换算回 Android 逻辑显示尺寸。
291292
- 能力错误不会自动重试可能已经产生手机或浏览器副作用的任务。
292293
- 直接 `/opengui` 的最终结果只显示文本;截图保留在对应控制子会话中。
293294
- 原生投屏只读且只在运行 Harness 的桌面主机上打开;无图形桌面的主机无法显示窗口。

‎deepseek-harness-plugin/src/image.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@ import sharp from 'sharp'
33
/** JPEG quality used for every phone screenshot; intentionally within 60–70. */
44
export const PHONE_SCREENSHOT_JPEG_QUALITY = 65
55

6+
/** Conservative provider-compatible bound for the longest model-facing image edge. */
7+
export const PHONE_SCREENSHOT_MAX_LONG_EDGE = 2_048
8+
69
/**
7-
* Encode an ADB screenshot as JPEG without changing its pixel dimensions.
10+
* Encode an ADB screenshot as a bounded JPEG for model input.
811
* @param source Encoded source screenshot returned by ADB.
9-
* @returns JPEG bytes at the source image width and height.
12+
* @returns JPEG bytes whose aspect ratio is preserved and longest edge is at most 2048px.
1013
*/
1114
export async function encodePhoneScreenshot(source: Buffer): Promise<Buffer> {
1215
return sharp(source, { failOn: 'error', limitInputPixels: false })
16+
.resize({
17+
width: PHONE_SCREENSHOT_MAX_LONG_EDGE,
18+
height: PHONE_SCREENSHOT_MAX_LONG_EDGE,
19+
fit: 'inside',
20+
withoutEnlargement: true,
21+
})
1322
.jpeg({ quality: PHONE_SCREENSHOT_JPEG_QUALITY })
1423
.toBuffer()
1524
}

‎deepseek-harness-plugin/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,9 +764,9 @@ export function apply(ctx: Context, baseConfig: Config): void {
764764
interaction: TaskInteraction,
765765
operation: (lease: OpenGuiTaskLease<OpenGuiExecutionContext>) => Promise<CoremateTaskResult>,
766766
): Promise<CoremateTaskResult> => tasks.runRoot<CoremateTaskResult>(interaction.agent, interaction.signal, 'waiting-for-device', async lease => {
767+
const route = await prepareTask(interaction)
767768
const targets = await waitForSelectedPhone(interaction)
768769
lease.setPhase('routing')
769-
const route = await prepareTask(interaction)
770770
lease.context = { targets, route }
771771
lease.setPhase('running')
772772
try {

‎deepseek-harness-plugin/src/phone-task.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export type CoremateTaskStart = (
6060
/** Resolve the model route before a non-empty direct command starts. */
6161
export type CoremateTaskPrepare = (invocation: CommandInvocation) => Promise<AgentOptions>
6262

63-
/** Pause a non-empty command until its external prerequisites are ready. */
63+
/** Pause a non-empty command until its external device prerequisites are ready. */
6464
export type CoremateTaskPreflight = (invocation: CommandInvocation, signal: AbortSignal) => Promise<void>
6565

6666
/** Convert an inherited-model capability failure into a safe next action. */
@@ -145,9 +145,9 @@ export class CoremateTaskCoordinator {
145145
if (task.length === 0) return { kind: 'success', text: OPENGUI_USAGE }
146146
try {
147147
const result = await this.tasks.runRoot(invocation.agent, invocation.signal, 'waiting-for-device', async lease => {
148+
const agentOptions = await prepare?.({ ...invocation, signal: lease.signal })
148149
await preflight?.(invocation, lease.signal)
149150
lease.setPhase('routing')
150-
const agentOptions = await prepare?.({ ...invocation, signal: lease.signal })
151151
lease.setPhase('running')
152152
try {
153153
return await this.start(task, invocation.agent, lease.signal, 'parent-chat', agentOptions)

‎deepseek-harness-plugin/tests/adb.spec.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,34 @@ describe('coremate-mobile ADB policy', () => {
5353
})).toEqual(['shell', 'input', 'tap', '990', '155'])
5454
})
5555

56+
it('maps M153 model coordinates from the bounded screenshot back to the device', () => {
57+
expect(actionCommand({
58+
action: 'tap',
59+
observationId: ObservationId('m153-observation'),
60+
targetBBox: { left: 440, top: 990, right: 484, bottom: 1_058 },
61+
}, {
62+
width: 1_264,
63+
height: 2_800,
64+
screenshotWidth: 925,
65+
screenshotHeight: 2_048,
66+
})).toEqual(['shell', 'input', 'tap', '631', '1400'])
67+
68+
expect(actionCommand({
69+
action: 'swipe',
70+
observationId: ObservationId('m153-observation'),
71+
x1: 462,
72+
y1: 1_536,
73+
x2: 462,
74+
y2: 512,
75+
durationMs: 300,
76+
}, {
77+
width: 1_264,
78+
height: 2_800,
79+
screenshotWidth: 925,
80+
screenshotHeight: 2_048,
81+
})).toEqual(['shell', 'input', 'swipe', '631', '2100', '631', '700', '300'])
82+
})
83+
5684
it('does not impose a fixed delay after every device mutation', () => {
5785
expect(Config({})).not.toHaveProperty('settleDelayMs')
5886
})

‎deepseek-harness-plugin/tests/bundle.spec.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ describe('standalone DeepSeek Harness bundle', () => {
6565
expect(clientSource).not.toContain("'shell.overlay'")
6666
})
6767

68+
it('prepares the model before waiting for an explicit phone selection', async () => {
69+
const hostSource = await readFile(new URL('src/index.ts', root), 'utf8')
70+
const prepareIndex = hostSource.indexOf('const route = await prepareTask(interaction)')
71+
const deviceIndex = hostSource.indexOf('const targets = await waitForSelectedPhone(interaction)')
72+
73+
expect(prepareIndex).toBeGreaterThan(-1)
74+
expect(deviceIndex).toBeGreaterThan(-1)
75+
expect(prepareIndex).toBeLessThan(deviceIndex)
76+
})
77+
6878
it('keeps the stop glyph visible independently of the host foreground color', async () => {
6979
const stopButtonSource = await readFile(new URL('src/client/TaskStopButton.tsx', root), 'utf8')
7080

‎deepseek-harness-plugin/tests/image.spec.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import sharp from 'sharp'
22
import { describe, expect, it } from 'vitest'
33
import {
44
PHONE_SCREENSHOT_JPEG_QUALITY,
5+
PHONE_SCREENSHOT_MAX_LONG_EDGE,
56
encodePhoneScreenshot,
67
} from '../src/image.ts'
78

@@ -24,4 +25,21 @@ describe('coremate-mobile screenshot encoding', () => {
2425
expect([...jpeg.subarray(0, 3)]).toEqual([0xFF, 0xD8, 0xFF])
2526
expect(metadata).toMatchObject({ format: 'jpeg', width: 321, height: 654 })
2627
})
28+
29+
it('bounds Nubia M153 portrait and landscape screenshots without enlarging smaller frames', async () => {
30+
expect(PHONE_SCREENSHOT_MAX_LONG_EDGE).toBe(2_048)
31+
for (const [width, height, expectedWidth, expectedHeight] of [
32+
[1_264, 2_800, 925, 2_048],
33+
[2_800, 1_264, 2_048, 925],
34+
[1_080, 2_400, 922, 2_048],
35+
[321, 654, 321, 654],
36+
] as const) {
37+
const png = await sharp({
38+
create: { width, height, channels: 3, background: { r: 16, g: 32, b: 48 } },
39+
}).png().toBuffer()
40+
41+
const metadata = await sharp(await encodePhoneScreenshot(png)).metadata()
42+
expect(metadata).toMatchObject({ format: 'jpeg', width: expectedWidth, height: expectedHeight })
43+
}
44+
})
2745
})

‎deepseek-harness-plugin/tests/phone-task.spec.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe('OpenGUI task entry points', () => {
120120
expect(start).toHaveBeenCalledWith('open settings', call.agent, expect.any(AbortSignal), 'parent-chat', route)
121121
})
122122

123-
it('waits for a phone before resolving the model and locks selection only afterwards', async () => {
123+
it('resolves the model before waiting for a phone and locks selection only afterwards', async () => {
124124
let allowPhone!: () => void
125125
const phoneReady = new Promise<void>(resolve => { allowPhone = resolve })
126126
const order: string[] = []
@@ -139,16 +139,16 @@ describe('OpenGUI task entry points', () => {
139139
})
140140
const running = coordinator.command(preflight, prepare).handler(invocation('检查手机'))
141141

142-
await vi.waitFor(() => { expect(preflight).toHaveBeenCalledTimes(1) })
142+
await vi.waitFor(() => { expect(prepare).toHaveBeenCalledTimes(1) })
143143
expect(coordinator.state()).toEqual({ active: true, phase: 'waiting-for-device', selectionLocked: false, ownerSessionId: 'session-owner' })
144-
expect(prepare).not.toHaveBeenCalled()
144+
expect(preflight).toHaveBeenCalledTimes(1)
145145
allowPhone()
146146
await expect(running).resolves.toEqual({ kind: 'success', text: 'done' })
147-
expect(order).toEqual(['preflight', 'prepare', 'start'])
147+
expect(order).toEqual(['prepare', 'preflight', 'start'])
148148
expect(coordinator.state()).toEqual({ active: false, phase: 'idle', selectionLocked: false })
149149
})
150150

151-
it('can cancel while waiting for a phone without resolving a model or starting work', async () => {
151+
it('can cancel while waiting for a phone after resolving the model without starting work', async () => {
152152
const prepare = vi.fn(async () => ({ provider: 'current', model: 'vision' }))
153153
const start = vi.fn(async () => result('unused'))
154154
const coordinator = new CoremateTaskCoordinator(start)
@@ -160,7 +160,7 @@ describe('OpenGUI task entry points', () => {
160160
await vi.waitFor(() => { expect(coordinator.state().phase).toBe('waiting-for-device') })
161161
expect(coordinator.cancel()).toBe(true)
162162
await expect(running).resolves.toEqual({ kind: 'error', text: 'coremate-mobile: OpenGUI task stopped by user' })
163-
expect(prepare).not.toHaveBeenCalled()
163+
expect(prepare).toHaveBeenCalledTimes(1)
164164
expect(start).not.toHaveBeenCalled()
165165
})
166166

0 commit comments

Comments
 (0)