Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deepseek-harness-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ The plugin checks for its managed Chromium only when a task actually calls `brow
| `apiKeyEnv` | Harness credential reference; default `COREMATE_MOBILE_API_KEY`. |
| `commandTimeoutMs` | Base timeout for local ADB processes and browser actions; default 15 seconds. |
| `maxOperations` | Maximum `phone_control` or `browser_control` calls in one child task; default 100. |
| `maxParallelDevices` | Maximum selected phones processed concurrently by one OpenGUI task; default 4, range 1–16. |
| `contextWindow` | Declared model context capacity; default 262,144 tokens. |
| `maxTokens` | Declared maximum model output; default 32,768 tokens. |
| `streamIdleTimeoutMs` | Maximum interval without a model stream event; default 300 seconds. |
Expand Down
1 change: 1 addition & 0 deletions deepseek-harness-plugin/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ OpenGUI 任务运行时,输入框右侧会出现方形“停止 OpenGUI 操作
| `apiKeyEnv` | Harness 凭据引用;默认 `COREMATE_MOBILE_API_KEY`。 |
| `commandTimeoutMs` | 每个本地 ADB 进程及浏览器动作的基础超时;默认 15 秒。 |
| `maxOperations` | 单次子任务允许的 `phone_control` 或 `browser_control` 调用上限;默认 100 次。 |
| `maxParallelDevices` | 单个 OpenGUI 任务同时处理的已选手机上限;默认 4,范围 1–16。 |
| `contextWindow` | 声明的模型上下文容量;默认 262,144 token。 |
| `maxTokens` | 声明的最大模型输出;默认 32,768 token。 |
| `streamIdleTimeoutMs` | 模型 stream 无新事件的最长时间;默认 300 秒。 |
Expand Down
2 changes: 1 addition & 1 deletion deepseek-harness-plugin/SOURCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This directory was imported from the standalone
repository.

- Source branch: `main`
- Source commit: `2675d3c2e3428595d1d6cdb016dd641dece75ebf`
- Source commit: `7a77c0ab222121d466c23c92c5df24d680c7c3a1`
- Import date: 2026-08-24

The source commit is the migration baseline. Local uncommitted changes from the
Expand Down
2 changes: 1 addition & 1 deletion deepseek-harness-plugin/docs/install-for-beginners.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Usage: /opengui <task>

如果终端出现红色错误,请不要继续操作手机,先查看本文末尾的“常见问题”。

打开 DSH、查看手机列表或点击眼睛手动投屏,都不需要先填写 OpenGUI 专用模型。
打开 DSH、查看 OpenGUI Tab 手机列表或使用“独立窗口”,都不需要先填写 OpenGUI 专用模型。

页面顶部还会出现 `OpenGUI` Tab。进入后,已连接手机的画面默认展开;下方三个场景模板只会把 `@OpenGUI` 任务填入输入框,不会自动发送。

Expand Down
2 changes: 1 addition & 1 deletion deepseek-harness-plugin/docs/use-cases.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ OpenGUI 通过当前 DSH 会话模型操作已授权的 Android 手机,并按
- 空 `/opengui` 和裸 `@OpenGUI` 只显示用法,不检查手机或模型。
- 当前模型明确支持图片时直接使用;不兼容时才进入专用视觉模型回退。
- 能力错误不会自动重试,避免重复产生手机或浏览器副作用。
- 原生投屏只读,必须手动点击眼睛打开;任务完成不会自动关闭投屏
- OpenGUI Tab 的实时画面只读;需要单独放大查看时可手动打开“独立窗口”,任务完成不会自动关闭该窗口
- 点击任务完成卡片中的追问只会生成 `@OpenGUI` 草稿,请检查后再发送。

需要帮助可加入 [OpenGUI Discord](https://discord.gg/pqHHw7XgJ3)。本版本的微信群入口仍是不可扫码占位,请勿扫描来历不明的二维码。
10 changes: 7 additions & 3 deletions deepseek-harness-plugin/src/client/BrowserInstallPrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { CSSProperties } from 'react'
import {
BROWSER_INSTALL_APPROVE_PATH,
Expand Down Expand Up @@ -69,7 +69,7 @@ export function browserInstallPresentation(status?: BrowserInstallStatus): Brows
: undefined
return {
kind: 'progress',
label: percent === undefined ? '正在下载托管浏览器…' : `正在下载托管浏览器:${percent}%`,
label: '正在下载托管浏览器…',
...(percent === undefined ? {} : { percent }),
}
}
Expand All @@ -79,6 +79,7 @@ export function browserInstallPresentation(status?: BrowserInstallStatus): Brows

/** Inline first-use consent and progress for the plugin-managed browser. */
export function BrowserInstallPrompt(): JSX.Element | null {
const panel = useRef<HTMLElement>(null)
const [status, setStatus] = useState<BrowserInstallStatus>()
const [pending, setPending] = useState(false)
const [error, setError] = useState<string>()
Expand Down Expand Up @@ -120,10 +121,13 @@ export function BrowserInstallPrompt(): JSX.Element | null {
}, [])

const presentation = browserInstallPresentation(status)
useEffect(() => {
if (presentation.kind === 'confirmation') panel.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}, [presentation.kind])
if (presentation.kind === 'hidden') return null
if (presentation.kind === 'confirmation' && status !== undefined) {
return (
<section style={panelStyle} role="alert" data-coremate-browser-install="awaiting-confirmation">
<section ref={panel} style={panelStyle} role="alert" data-coremate-browser-install="awaiting-confirmation">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
<div style={{ flex: '1 1 260px', minWidth: 0 }}>
<strong style={{ display: 'block', marginBottom: 4, fontSize: 14 }}>启用托管浏览器</strong>
Expand Down
34 changes: 33 additions & 1 deletion deepseek-harness-plugin/src/client/CoremateClaimBridge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useEffect } from 'react'
import type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'
import { isUntitledSession, taskTitle } from './coremate-trigger.ts'
import { coremateTaskStatusStore } from './task-status-store.ts'

export interface CoremateDraftActions {
/** Upgrade the leading @OpenGUI token through the scoped DSH claim event. */
Expand All @@ -20,6 +23,16 @@ interface InputSnapshot {
interface CoremateClaimBridgeProps {
readonly useInput: <T>(selector: (state: InputSnapshot) => T) => T
readonly coremateDraftActions: CoremateDraftActions
readonly coremateSessionId: string
readonly coremateSessions: ISessions
}

export function directOpenGuiTask(draft: string): string | undefined {
return /^\/(?:opengui|coremate)\s+(.+)$/iu.exec(draft.trim())?.[1]?.trim() || undefined
}

export function submittedDirectOpenGuiTask(draft: string, phase: InputSnapshot['phase']): string | undefined {
return phase === 'submitting' ? directOpenGuiTask(draft) : undefined
}

/** Include one existing separator so the command claim does not duplicate it. */
Expand All @@ -34,7 +47,12 @@ export function coremateClaimSpan(draft: string, draftRev: number): CoremateClai
}

/** Make menu picks, typed text, pasted text, and template drafts share one native claim. */
export function CoremateClaimBridge({ useInput, coremateDraftActions }: CoremateClaimBridgeProps): null {
export function CoremateClaimBridge({
useInput,
coremateDraftActions,
coremateSessionId,
coremateSessions,
}: CoremateClaimBridgeProps): null {
const draft = useInput(state => state.draft)
const draftRev = useInput(state => state.draftRev)
const phase = useInput(state => state.phase)
Expand All @@ -44,5 +62,19 @@ export function CoremateClaimBridge({ useInput, coremateDraftActions }: Coremate
const span = coremateClaimSpan(draft, draftRev)
if (span) coremateDraftActions.claim(span)
}, [coremateDraftActions, draft, draftRev, phase])

useEffect(() => {
const task = submittedDirectOpenGuiTask(draft, phase)
if (task === undefined) return
coremateTaskStatusStore.markConsumedSession(coremateSessionId)
coremateTaskStatusStore.beginLaunch(coremateSessionId)
const sessions = coremateSessions
const id = coremateSessionId as keyof ReturnType<typeof sessions.list.getSnapshot>['byId']
const row = sessions.list.getSnapshot().byId[id]
const binding = sessions.binding(id)
if (isUntitledSession(row?.title) && binding !== undefined) {
void binding.session.rename(taskTitle(task)).catch(() => {})
}
}, [coremateSessionId, coremateSessions, draft, phase])
return null
}
62 changes: 50 additions & 12 deletions deepseek-harness-plugin/src/client/CoremateView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import type { CSSProperties } from 'react'
import {
DEVICE_SELECTION_PATH,
DEVICE_STREAM_ENABLE_PATH,
DEVICE_STREAM_STATUS_PATH,
MIRROR_START_PATH,
MIRROR_STOP_PATH,
} from '../mirror-contract.ts'
import type { MirrorDeviceStatus, MirrorStatus } from '../mirror-contract.ts'
import type { ScrcpyStreamStatus } from '../scrcpy-stream.ts'
import { BrowserInstallPrompt } from './BrowserInstallPrompt.tsx'
import { OpenGuiMark } from './OpenGuiMark.tsx'
import { postMirrorStatus, readMirrorStatus } from './MirrorButton.tsx'
import { mirrorBusy, mirrorLabel, mirrorProgress, postMirrorStatus, readMirrorStatus } from './MirrorButton.tsx'
import { TaskStopButton } from './TaskStopButton.tsx'
import { PhoneStream } from './PhoneStream.tsx'

Expand All @@ -18,11 +21,8 @@ const OPENGUI_WEBSITE_URL = 'https://opengui.ai/'

const rootStyle: CSSProperties = {
width: '100%',
height: 'calc(100dvh - 74px)',
margin: 0,
padding: '16px clamp(16px, 2vw, 32px) 180px',
overflowY: 'auto',
scrollbarGutter: 'stable both-edges',
boxSizing: 'border-box',
containerType: 'inline-size',
color: 'var(--dsw-alias-label-primary, #27272a)',
Expand Down Expand Up @@ -153,6 +153,8 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId
const seenDevices = useRef(new Set<string>())
const [pending, setPending] = useState(false)
const [error, setError] = useState<string>()
const [streamStatus, setStreamStatus] = useState<ScrcpyStreamStatus>()
const [streamGeneration, setStreamGeneration] = useState(0)

useEffect(() => {
const controller = new AbortController()
Expand All @@ -162,7 +164,9 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId
const next = await readMirrorStatus(controller.signal)
setStatus(next)
setExpanded(current => {
const value = new Set(current)
const connectedIds = new Set(next.devices.map(device => device.id))
const value = new Set([...current].filter(id => connectedIds.has(id)))
seenDevices.current = new Set([...seenDevices.current].filter(id => connectedIds.has(id)))
for (const device of next.devices) {
if (seenDevices.current.has(device.id)) continue
seenDevices.current.add(device.id)
Expand All @@ -184,6 +188,32 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId
}
}, [])

useEffect(() => {
const controller = new AbortController()
let timer: ReturnType<typeof setTimeout> | undefined
const poll = async (): Promise<void> => {
try {
const response = await fetch(DEVICE_STREAM_STATUS_PATH, { cache: 'no-store', signal: controller.signal })
if (response.ok) setStreamStatus(await response.json() as ScrcpyStreamStatus)
} catch { /* stream support is optional; each card retains screenshot fallback */ }
finally {
if (!controller.signal.aborted) timer = setTimeout(poll, document.hidden ? 5_000 : 1_500)
}
}
void poll()
return () => {
controller.abort()
if (timer !== undefined) clearTimeout(timer)
}
}, [])

const enableStream = useCallback(async (): Promise<void> => {
const response = await fetch(DEVICE_STREAM_ENABLE_PATH, { method: 'POST', headers: { Accept: 'application/json' } })
if (!response.ok) throw new Error(`启用实时画面失败 (${response.status})`)
setStreamStatus(await response.json() as ScrcpyStreamStatus)
setStreamGeneration(value => value + 1)
}, [])

const mutate = useCallback(async (path: string, ids: readonly string[]): Promise<void> => {
setPending(true)
try {
Expand All @@ -198,7 +228,6 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId

const selected = status?.devices.filter(device => device.selected).map(device => device.id) ?? []
const wallItems = buildDeviceWallItems(status?.devices ?? [])
const anyExpanded = status?.devices.some(device => expanded.has(device.id)) ?? false
return (
<main style={rootStyle} lang="zh-CN" data-coremate-view>
<style>{`
Expand Down Expand Up @@ -230,13 +259,13 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId
</div>
</header>

<BrowserInstallPrompt />
<div style={{ position: 'sticky', top: 0, zIndex: 10 }}><BrowserInstallPrompt /></div>

<section aria-label="设备照片墙" style={gridStyle} data-coremate-device-wall>
{wallItems.map(item => {
if (item.kind === 'connect-more') {
return (
<aside key="connect-more" style={{ ...connectStyle, minHeight: anyExpanded ? 'min(56vh, 620px)' : 360 }} data-coremate-connect-more>
<aside key="connect-more" style={connectStyle} data-coremate-connect-more>
<div>
<span aria-hidden="true" style={{ display: 'inline-grid', placeItems: 'center', width: 48, height: 48, marginBottom: 16, border: '1px solid currentColor', borderRadius: 999, fontSize: 30, fontWeight: 300, lineHeight: 1 }}>+</span>
<h2 style={{ margin: '0 0 8px', fontSize: 17, lineHeight: 1.35, fontWeight: 720 }}>连接更多设备</h2>
Expand All @@ -252,7 +281,7 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId
const active = mirrorActive(device)
const open = expanded.has(device.id)
return (
<article key={device.id} style={{ ...deviceStyle, alignSelf: open ? 'stretch' : 'start' }} data-coremate-device-card>
<article key={device.id} style={deviceStyle} data-coremate-device-card>
<div style={deviceHeaderStyle}>
<label style={{ display: 'flex', flex: '1 1 92px', alignItems: 'center', gap: 7, minWidth: 0, cursor: status?.selectionLocked === true ? 'not-allowed' : 'pointer' }}>
<input
Expand All @@ -265,16 +294,25 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId
<span aria-label={device.connected ? '在线' : '已断开'} title={device.connected ? '在线' : '已断开'} style={{ width: 6, height: 6, flex: '0 0 auto', borderRadius: 999, background: device.connected ? '#16a34a' : '#a1a1aa' }} />
</label>
<div style={{ display: 'flex', flex: '0 0 auto', gap: 6 }}>
<button data-coremate-press type="button" style={actionStyle} aria-expanded={open} onClick={() => setExpanded(current => {
<button data-coremate-press type="button" style={actionStyle} aria-label={`${open ? '收起' : '展开'} ${device.label} 画面`} aria-expanded={open} onClick={() => setExpanded(current => {
const next = new Set(current)
if (next.has(device.id)) next.delete(device.id)
else next.add(device.id)
return next
})}>{open ? '收起' : '展开'}</button>
<button data-coremate-press type="button" style={{ ...actionStyle, color: active ? '#b45309' : actionStyle.color }} disabled={pending || !device.connected} onClick={() => void mutate(active ? MIRROR_STOP_PATH : MIRROR_START_PATH, [device.id])}>{active ? '关闭窗口' : '独立窗口'}</button>
<button
data-coremate-press
type="button"
style={{ ...actionStyle, color: active ? '#b45309' : actionStyle.color }}
aria-label={mirrorLabel(device)}
aria-busy={mirrorBusy(device)}
disabled={pending || device.phase === 'unsupported' || (!device.connected && !active)}
onClick={() => void mutate(active ? MIRROR_STOP_PATH : MIRROR_START_PATH, [device.id])}
>{mirrorProgress(device) ?? (active ? '关闭窗口' : '独立窗口')}</button>
</div>
</div>
<PhoneStream device={device} expanded={open} />
{device.message === undefined ? null : <div role="status" style={{ padding: '0 12px 8px', color: device.phase === 'error' ? '#b91c1c' : 'var(--dsw-alias-label-secondary, #52525b)', fontSize: 11 }}>{device.message}</div>}
<PhoneStream device={device} expanded={open} streamStatus={streamStatus} streamGeneration={streamGeneration} enableStream={enableStream} />
</article>
)
})}
Expand Down
10 changes: 5 additions & 5 deletions deepseek-harness-plugin/src/client/MirrorButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const messageStyle: CSSProperties = {
overflowWrap: 'anywhere',
}

function mirrorLabel(device: MirrorDeviceStatus): string {
export function mirrorLabel(device: MirrorDeviceStatus): string {
switch (device.phase) {
case 'downloading': {
const percent = device.downloadedBytes !== undefined && device.totalBytes
Expand All @@ -102,7 +102,7 @@ function mirrorLabel(device: MirrorDeviceStatus): string {
}
}

function progressFor(device: MirrorDeviceStatus): string | undefined {
export function mirrorProgress(device: MirrorDeviceStatus): string | undefined {
if (device.phase === 'downloading') {
return device.downloadedBytes !== undefined && device.totalBytes
? `${Math.min(100, Math.round((device.downloadedBytes / device.totalBytes) * 100))}%`
Expand All @@ -122,7 +122,7 @@ function WindowIcon({ active }: { active: boolean }): JSX.Element {
)
}

function isBusy(device: MirrorDeviceStatus): boolean {
export function mirrorBusy(device: MirrorDeviceStatus): boolean {
return device.phase === 'downloading' || device.phase === 'extracting' || device.phase === 'launching'
}

Expand Down Expand Up @@ -195,10 +195,10 @@ export function MirrorButton(): JSX.Element | null {
{status.devices.length === 0
? <span style={messageStyle}>未检测到已授权的 Android 手机</span>
: status.devices.map(device => {
const busy = isBusy(device)
const busy = mirrorBusy(device)
const mirrorActive = busy || device.phase === 'running'
const mirrorDisabled = pending || device.phase === 'unsupported' || (!device.connected && !mirrorActive)
const progress = progressFor(device)
const progress = mirrorProgress(device)
return (
<div
key={device.id}
Expand Down
Loading
Loading