From 1e1d119a2af2dc337a77f77f8aaa1bdb9f2a67f4 Mon Sep 17 00:00:00 2001 From: harveysang Date: Mon, 24 Aug 2026 23:06:22 +0800 Subject: [PATCH] feat: sync DeepSeek Harness plugin main --- deepseek-harness-plugin/README.md | 1 + deepseek-harness-plugin/README.zh.md | 1 + deepseek-harness-plugin/SOURCE.md | 2 +- .../docs/install-for-beginners.zh.md | 2 +- deepseek-harness-plugin/docs/use-cases.zh.md | 2 +- .../src/client/BrowserInstallPrompt.tsx | 10 +- .../src/client/CoremateClaimBridge.tsx | 34 +- .../src/client/CoremateView.tsx | 62 +++- .../src/client/MirrorButton.tsx | 10 +- .../src/client/PhoneStream.tsx | 118 ++++--- .../src/client/coremate-trigger.ts | 13 +- deepseek-harness-plugin/src/client/index.tsx | 2 +- .../src/client/promotion-data.ts | 53 ++- .../src/client/session-bridge.ts | 43 ++- .../src/client/task-status-store.ts | 4 + .../src/client/video-decoder.ts | 85 +++-- deepseek-harness-plugin/src/concurrency.ts | 53 +++ deepseek-harness-plugin/src/configuration.ts | 21 +- deepseek-harness-plugin/src/device-fleet.ts | 2 +- .../src/forward-registry.ts | 280 ++++++++++++++++ deepseek-harness-plugin/src/index.ts | 316 +++++++++++++----- deepseek-harness-plugin/src/phone-progress.ts | 12 +- deepseek-harness-plugin/src/phone-task.ts | 197 +++++++---- deepseek-harness-plugin/src/preview.ts | 86 ++--- deepseek-harness-plugin/src/scrcpy-stream.ts | 54 ++- deepseek-harness-plugin/src/scrcpy.ts | 231 +++++++++++-- deepseek-harness-plugin/src/suggestions.ts | 14 +- deepseek-harness-plugin/tests/adb.spec.ts | 7 + .../tests/browser-install-prompt.spec.ts | 2 +- deepseek-harness-plugin/tests/bundle.spec.ts | 4 +- .../tests/concurrency.spec.ts | 38 +++ .../tests/coremate-claim-bridge.spec.ts | 19 +- .../tests/coremate-trigger.spec.ts | 11 +- .../tests/coremate-view.spec.ts | 2 +- .../tests/device-fleet.spec.ts | 15 + .../tests/forward-registry.spec.ts | 143 ++++++++ .../tests/phone-progress.spec.ts | 6 +- .../tests/phone-task.spec.ts | 40 ++- deepseek-harness-plugin/tests/preview.spec.ts | 34 ++ .../tests/promotion.spec.ts | 37 +- deepseek-harness-plugin/tests/scrcpy.spec.ts | 115 +++++++ .../tests/session-bridge.spec.ts | 72 ++++ .../tests/stream-manager.spec.ts | 55 ++- .../tests/suggestions.spec.ts | 24 ++ .../tests/task-status-store.spec.ts | 6 + .../tests/video-decoder.spec.ts | 130 +++++++ 46 files changed, 2055 insertions(+), 413 deletions(-) create mode 100644 deepseek-harness-plugin/src/concurrency.ts create mode 100644 deepseek-harness-plugin/src/forward-registry.ts create mode 100644 deepseek-harness-plugin/tests/concurrency.spec.ts create mode 100644 deepseek-harness-plugin/tests/forward-registry.spec.ts create mode 100644 deepseek-harness-plugin/tests/video-decoder.spec.ts diff --git a/deepseek-harness-plugin/README.md b/deepseek-harness-plugin/README.md index dd1d8f4..cac51a3 100644 --- a/deepseek-harness-plugin/README.md +++ b/deepseek-harness-plugin/README.md @@ -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. | diff --git a/deepseek-harness-plugin/README.zh.md b/deepseek-harness-plugin/README.zh.md index a27e40a..c9e72b8 100644 --- a/deepseek-harness-plugin/README.zh.md +++ b/deepseek-harness-plugin/README.zh.md @@ -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 秒。 | diff --git a/deepseek-harness-plugin/SOURCE.md b/deepseek-harness-plugin/SOURCE.md index 7004117..a37edc3 100644 --- a/deepseek-harness-plugin/SOURCE.md +++ b/deepseek-harness-plugin/SOURCE.md @@ -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 diff --git a/deepseek-harness-plugin/docs/install-for-beginners.zh.md b/deepseek-harness-plugin/docs/install-for-beginners.zh.md index 30bda47..e346853 100644 --- a/deepseek-harness-plugin/docs/install-for-beginners.zh.md +++ b/deepseek-harness-plugin/docs/install-for-beginners.zh.md @@ -94,7 +94,7 @@ Usage: /opengui 如果终端出现红色错误,请不要继续操作手机,先查看本文末尾的“常见问题”。 -打开 DSH、查看手机列表或点击眼睛手动投屏,都不需要先填写 OpenGUI 专用模型。 +打开 DSH、查看 OpenGUI Tab 手机列表或使用“独立窗口”,都不需要先填写 OpenGUI 专用模型。 页面顶部还会出现 `OpenGUI` Tab。进入后,已连接手机的画面默认展开;下方三个场景模板只会把 `@OpenGUI` 任务填入输入框,不会自动发送。 diff --git a/deepseek-harness-plugin/docs/use-cases.zh.md b/deepseek-harness-plugin/docs/use-cases.zh.md index 0c591d2..81ed20e 100644 --- a/deepseek-harness-plugin/docs/use-cases.zh.md +++ b/deepseek-harness-plugin/docs/use-cases.zh.md @@ -51,7 +51,7 @@ OpenGUI 通过当前 DSH 会话模型操作已授权的 Android 手机,并按 - 空 `/opengui` 和裸 `@OpenGUI` 只显示用法,不检查手机或模型。 - 当前模型明确支持图片时直接使用;不兼容时才进入专用视觉模型回退。 - 能力错误不会自动重试,避免重复产生手机或浏览器副作用。 -- 原生投屏只读,必须手动点击眼睛打开;任务完成不会自动关闭投屏。 +- OpenGUI Tab 的实时画面只读;需要单独放大查看时可手动打开“独立窗口”,任务完成不会自动关闭该窗口。 - 点击任务完成卡片中的追问只会生成 `@OpenGUI` 草稿,请检查后再发送。 需要帮助可加入 [OpenGUI Discord](https://discord.gg/pqHHw7XgJ3)。本版本的微信群入口仍是不可扫码占位,请勿扫描来历不明的二维码。 diff --git a/deepseek-harness-plugin/src/client/BrowserInstallPrompt.tsx b/deepseek-harness-plugin/src/client/BrowserInstallPrompt.tsx index 992a9f9..f2e217b 100644 --- a/deepseek-harness-plugin/src/client/BrowserInstallPrompt.tsx +++ b/deepseek-harness-plugin/src/client/BrowserInstallPrompt.tsx @@ -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, @@ -69,7 +69,7 @@ export function browserInstallPresentation(status?: BrowserInstallStatus): Brows : undefined return { kind: 'progress', - label: percent === undefined ? '正在下载托管浏览器…' : `正在下载托管浏览器:${percent}%`, + label: '正在下载托管浏览器…', ...(percent === undefined ? {} : { percent }), } } @@ -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(null) const [status, setStatus] = useState() const [pending, setPending] = useState(false) const [error, setError] = useState() @@ -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 ( -
+
启用托管浏览器 diff --git a/deepseek-harness-plugin/src/client/CoremateClaimBridge.tsx b/deepseek-harness-plugin/src/client/CoremateClaimBridge.tsx index 1920073..a61f38d 100644 --- a/deepseek-harness-plugin/src/client/CoremateClaimBridge.tsx +++ b/deepseek-harness-plugin/src/client/CoremateClaimBridge.tsx @@ -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. */ @@ -20,6 +23,16 @@ interface InputSnapshot { interface CoremateClaimBridgeProps { readonly useInput: (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. */ @@ -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) @@ -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['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 } diff --git a/deepseek-harness-plugin/src/client/CoremateView.tsx b/deepseek-harness-plugin/src/client/CoremateView.tsx index 9fcd2dc..4d99756 100644 --- a/deepseek-harness-plugin/src/client/CoremateView.tsx +++ b/deepseek-harness-plugin/src/client/CoremateView.tsx @@ -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' @@ -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)', @@ -153,6 +153,8 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId const seenDevices = useRef(new Set()) const [pending, setPending] = useState(false) const [error, setError] = useState() + const [streamStatus, setStreamStatus] = useState() + const [streamGeneration, setStreamGeneration] = useState(0) useEffect(() => { const controller = new AbortController() @@ -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) @@ -184,6 +188,32 @@ export function CoremateView({ coremateSessionId }: { readonly coremateSessionId } }, []) + useEffect(() => { + const controller = new AbortController() + let timer: ReturnType | undefined + const poll = async (): Promise => { + 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 => { + 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 => { setPending(true) try { @@ -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 (