diff --git a/deepseek-harness-plugin/src/client/TaskStopButton.tsx b/deepseek-harness-plugin/src/client/TaskStopButton.tsx
index 88c821d..407512b 100644
--- a/deepseek-harness-plugin/src/client/TaskStopButton.tsx
+++ b/deepseek-harness-plugin/src/client/TaskStopButton.tsx
@@ -25,6 +25,14 @@ const squareStyle: CSSProperties = {
background: '#dc2626',
}
+const errorStyle: CSSProperties = {
+ maxWidth: 220,
+ color: 'var(--dsw-alias-label-error, #b91c1c)',
+ fontSize: 11,
+ lineHeight: 1.3,
+ overflowWrap: 'anywhere',
+}
+
/** Stop the active OpenGUI phone or browser task from the composer's right tool row. */
export function TaskStopButton({ coremateSessionId }: { readonly coremateSessionId?: string }): JSX.Element | null {
const { task } = useCoremateTaskStatus()
@@ -46,15 +54,18 @@ export function TaskStopButton({ coremateSessionId }: { readonly coremateSession
if (!task.active || (task.ownerSessionId !== undefined && task.ownerSessionId !== coremateSessionId)) return null
const stopping = pending || task.phase === 'stopping'
return (
-
+
+ {error === undefined ? null : {error}}
+
+
)
}
diff --git a/deepseek-harness-plugin/src/client/task-status-store.ts b/deepseek-harness-plugin/src/client/task-status-store.ts
index 4007110..d63ccf5 100644
--- a/deepseek-harness-plugin/src/client/task-status-store.ts
+++ b/deepseek-harness-plugin/src/client/task-status-store.ts
@@ -10,6 +10,8 @@ export interface CoremateTaskSnapshot {
}
const IDLE: CoremateTaskStatus = { active: false, phase: 'idle', selectionLocked: false }
+const STOP_TIMEOUT_MS = 5_000
+const STOP_REFRESH_TIMEOUT_MS = 1_000
export class CoremateTaskStatusStore {
private snapshot: CoremateTaskSnapshot = { task: IDLE, launching: false }
@@ -61,9 +63,31 @@ export class CoremateTaskStatusStore {
}
async stop(): Promise {
- const response = await fetch(PHONE_TASK_STOP_PATH, { method: 'POST', headers: { Accept: 'application/json' } })
- if (!response.ok && response.status !== 409) throw new Error(`停止 OpenGUI 操作失败 (${response.status})`)
- try { await this.refresh() } catch { /* polling retains the last truthful Host state */ }
+ const stopController = new AbortController()
+ const stopTimer = setTimeout(() => stopController.abort(new Error('停止 OpenGUI 操作超时,请检查 Host 后重试。')), STOP_TIMEOUT_MS)
+ try {
+ const response = await fetch(PHONE_TASK_STOP_PATH, {
+ method: 'POST',
+ headers: { Accept: 'application/json' },
+ signal: stopController.signal,
+ })
+ if (!response.ok && response.status !== 409) throw new Error(`停止 OpenGUI 操作失败 (${response.status})`)
+ } catch (error) {
+ if (stopController.signal.aborted) throw stopController.signal.reason
+ throw error
+ } finally {
+ clearTimeout(stopTimer)
+ }
+
+ const refreshController = new AbortController()
+ const refreshTimer = setTimeout(() => refreshController.abort(), STOP_REFRESH_TIMEOUT_MS)
+ try {
+ await this.refresh(refreshController.signal)
+ } catch {
+ /* polling retains the last truthful Host state after an accepted stop */
+ } finally {
+ clearTimeout(refreshTimer)
+ }
}
/** Reserve the short gap before the Host publishes the admitted task. */
diff --git a/deepseek-harness-plugin/src/index.ts b/deepseek-harness-plugin/src/index.ts
index f6be85f..809ee7a 100644
--- a/deepseek-harness-plugin/src/index.ts
+++ b/deepseek-harness-plugin/src/index.ts
@@ -34,7 +34,7 @@ import type { FleetDevice } from './device-fleet.ts'
import { OwnedForwardRegistry } from './forward-registry.ts'
import { installMirrorHttp } from './mirror-http.ts'
import { relayNestedTaskProgress, relayPhoneTaskProgress } from './phone-progress.ts'
-import { OpenGuiTaskManager, OPENGUI_USAGE } from './phone-task.ts'
+import { OpenGuiTaskManager, OPENGUI_USAGE, runPreparedOpenGuiTask } from './phone-task.ts'
import type { CoremateTaskPresentation, CoremateTaskResult, OpenGuiTaskLease } from './phone-task.ts'
import { resolveMobileProfile, type MobileApi } from './provider.ts'
import { latestPhoneScreenshotMessages } from './runtime.ts'
@@ -674,6 +674,7 @@ export function apply(ctx: Context, baseConfig: Config): void {
const info = await ctx.llm.resolveModelInfo(route.provider, route.model, signal)
modalities = info.inputModalities
} catch (error) {
+ if (signal.aborted) throw signal.reason
ctx.logger.debug(error instanceof Error ? error : new Error(String(error)))
}
const piAiDescriptor = ctx.settings.describe().find(item => item.ns === LLM_PI_AI_NS)
@@ -715,6 +716,7 @@ export function apply(ctx: Context, baseConfig: Config): void {
const info = await ctx.llm.resolveModelInfo(route.provider, route.model, signal)
if (info.inputModalities?.includes('image')) return
} catch (error) {
+ if (signal.aborted) throw signal.reason
ctx.logger.debug(error instanceof Error ? error : new Error(String(error)))
}
if (Date.now() >= deadline) throw new Error('coremate-mobile: DSH 模型配置已保存,但热更新未及时生效;请重新提交任务')
@@ -911,22 +913,17 @@ export function apply(ctx: Context, baseConfig: Config): void {
const runRootTask = async (
interaction: TaskInteraction,
operation: (lease: OpenGuiTaskLease) => Promise,
- ): Promise => tasks.runRoot(interaction.agent, interaction.signal, 'waiting-for-device', async lease => {
- const route = await prepareTask(interaction)
- const targets = await waitForSelectedPhone(interaction)
- lease.setPhase('routing')
- lease.context = { targets, route }
- lease.setPhase('running')
- try {
- return await operation(lease)
- } catch (error) {
- if (error instanceof Error) {
- const recovered = await recoverTask(error, interaction, route)
- if (recovered !== undefined) throw new Error(recovered)
- }
- throw error
- }
- })
+ ): Promise => tasks.runRoot(interaction.agent, interaction.signal, 'waiting-for-device', lease => runPreparedOpenGuiTask(
+ interaction,
+ lease,
+ {
+ prepare: prepareTask,
+ waitForTargets: waitForSelectedPhone,
+ context: (route, targets) => ({ targets, route }),
+ execute: operation,
+ recover: recoverTask,
+ },
+ ))
const directCommand = (commandName: 'opengui' | 'coremate'): CommandDefinition => ({
name: commandName,
diff --git a/deepseek-harness-plugin/src/phone-task.ts b/deepseek-harness-plugin/src/phone-task.ts
index a53889a..7358742 100644
--- a/deepseek-harness-plugin/src/phone-task.ts
+++ b/deepseek-harness-plugin/src/phone-task.ts
@@ -146,14 +146,19 @@ export class CoremateTaskCoordinator {
try {
const result = await this.tasks.runRoot(invocation.agent, invocation.signal, 'waiting-for-device', async lease => {
const agentOptions = await prepare?.({ ...invocation, signal: lease.signal })
+ lease.signal.throwIfAborted()
await preflight?.(invocation, lease.signal)
+ lease.signal.throwIfAborted()
lease.setPhase('routing')
lease.setPhase('running')
+ lease.signal.throwIfAborted()
try {
return await this.start(task, invocation.agent, lease.signal, 'parent-chat', agentOptions)
} catch (error) {
if (error instanceof Error && agentOptions !== undefined && recover !== undefined) {
+ lease.signal.throwIfAborted()
const recovered = await recover(error, { ...invocation, signal: lease.signal }, agentOptions)
+ lease.signal.throwIfAborted()
if (recovered !== undefined) throw new CoremateRecoveredError(recovered)
}
throw error
@@ -193,6 +198,53 @@ export interface OpenGuiTaskLease {
capabilityFailure(): Error | undefined
}
+/** Lifecycle hooks for preparing and executing one admitted OpenGUI root task. */
+export interface PreparedOpenGuiTaskHooks {
+ prepare(interaction: Interaction): Promise
+ waitForTargets(interaction: Interaction): Promise
+ context(route: Route, targets: Targets): Context
+ execute(lease: OpenGuiTaskLease): Promise
+ recover(error: Error, interaction: Interaction, route: Route): Promise
+}
+
+/**
+ * Run every cancellable stage with the admitted task's fused lease signal.
+ * The caller's original signal alone cannot observe cancellation from the
+ * workbench stop control.
+ */
+export async function runPreparedOpenGuiTask<
+ Interaction extends { readonly signal: AbortSignal },
+ Route,
+ Targets,
+ Context,
+ Result,
+>(
+ interaction: Interaction,
+ lease: OpenGuiTaskLease,
+ hooks: PreparedOpenGuiTaskHooks,
+): Promise {
+ const scopedInteraction = { ...interaction, signal: lease.signal }
+ const route = await hooks.prepare(scopedInteraction)
+ lease.signal.throwIfAborted()
+ const targets = await hooks.waitForTargets(scopedInteraction)
+ lease.signal.throwIfAborted()
+ lease.setPhase('routing')
+ lease.context = hooks.context(route, targets)
+ lease.setPhase('running')
+ lease.signal.throwIfAborted()
+ try {
+ return await hooks.execute(lease)
+ } catch (error) {
+ if (error instanceof Error) {
+ lease.signal.throwIfAborted()
+ const recovered = await hooks.recover(error, scopedInteraction, route)
+ lease.signal.throwIfAborted()
+ if (recovered !== undefined) throw new Error(recovered)
+ }
+ throw error
+ }
+}
+
interface ActiveOpenGuiTask {
readonly controller: AbortController
readonly agents: WeakSet