fix(desktop): remove cancel action from mandatory native fallback
This commit is contained in:
1 parent
8d66b7236b
commit
4ff619c0e3
5 files changed
+14
-15
No files matched your search
@@ -63,8 +63,8 @@
|
||||
en: 2e57457c44c85b5f
|
||||
zh: 5b8510a3dc4c6c9d
|
||||
/deepseek-harness-desktop/updates/mandatory-update-policy:
|
||||
en: 6ccb92c6543d16df
|
||||
zh: 9fc99202ded89b5e
|
||||
en: 411a432663396e8c
|
||||
zh: d2c5a16c0e4724e5
|
||||
/deepseek-harness-desktop/updates/local-updater-qualification:
|
||||
en: 68ec2f8bd5763ea6
|
||||
zh: 23b932dfe8503602
|
||||
|
||||
@@ -332,7 +332,7 @@ When permitted, login is offered during user-initiated checks and the packaged a
|
||||
|
||||
Login and policy requests share an in-memory Session, separate from product windows and the updater; restarting requires a new login. Closing cancels login, and navigation failure provides localized retry guidance. Returning to the service triggers a fresh policy query; a redirect, cookie, or HTTP 422 is not a valid policy decision. Cancellation, expiry, and invalid responses retain any known mandatory block. Fixed login outcomes appear in process diagnostics and the optional update journal; cookies, OAuth parameters, and remote error text are not recorded by the login controller. Live Harness gateway/API integration and macOS login qualification remain unverified.
|
||||
|
||||
A flattened `40005` opens a shell-owned modal and refuses subsequent plugin mutations without stopping existing Host tasks. Server title and detail are optional plain text and fall back to client copy; an absent or unapproved download URL hides the external-page actions without clearing the block. On macOS the mandatory overlay uses an isolated shell-origin WebContentsView inside its native owner. It fades in and out in place, retains its backdrop across update-state changes, and blocks product keyboard input. Loading or updating the view never shows or activates a separate native window; returning to an already focused owner focuses the view. Closing its native owner follows the [normal window-close behavior](#closing-the-window-and-quitting). View loading failures retain policy and input blocking without changing the owner title. A native recovery dialog appears only while the owner is visible and focused, showing the policy explanation and available target version. Open download page uses the policy URL after the existing origin validation and is omitted when no approved URL is available; Quit uses normal application exit confirmation; Cancel retains blocking and offers recovery on the next foreground return. Hiding the owner, policy clearance, successful loading, and disposal dismiss the recovery dialog; obsolete responses have no effect. This recovery covers the isolated view, not the Windows embedded frame. On Windows the isolated application preload mounts a shell-origin frame inside the main window, covering the content below the 40 DIP caption with a backdrop and dialog. It blocks background page input without creating another native window; moving and maximizing affect only the main window. Caption menus retain mouse and keyboard operation; Web controls in the caption remain reachable. The shared DOM is a presentation constraint, not a security boundary: product scripts can hide the overlay but cannot clear main-process policy or authorize installation through it. Update actions and state use a private MessageChannel between the isolated preload and shell frame. The parent remains enabled for native move, resize, minimize, maximize, and close controls. Closing the application performs cleanup without clearing the update requirement; Esc does not dismiss the overlay. Once installation is approved, installer-owned quit releases the modal before Electron closes windows. Download, file verification including preparation, task inspection, and installation confirmation share this same modal. Only the second user approval permits task teardown and installation; deferral retains the block and package. Restart feedback mentions task stopping only when affected tasks exist. Policy is not persisted across application restarts, and policy responses never revoke or replace an updater artifact.
|
||||
A flattened `40005` opens a shell-owned modal and refuses subsequent plugin mutations without stopping existing Host tasks. Server title and detail are optional plain text and fall back to client copy; an absent or unapproved download URL hides the external-page actions without clearing the block. On macOS the mandatory overlay uses an isolated shell-origin WebContentsView inside its native owner. It fades in and out in place, retains its backdrop across update-state changes, and blocks product keyboard input. Loading or updating the view never shows or activates a separate native window; returning to an already focused owner focuses the view. Closing its native owner follows the [normal window-close behavior](#closing-the-window-and-quitting). View loading failures retain policy and input blocking without changing the owner title. A native recovery dialog appears only while the owner is visible and focused, showing the policy explanation and available target version. Open download page uses the policy URL after the existing origin validation and is omitted when no approved URL is available; Quit uses normal application exit confirmation. No Cancel button is offered; non-action dismissals retain blocking and reopen recovery. Hiding the owner, policy clearance, successful loading, and disposal dismiss the recovery dialog; obsolete responses have no effect. This recovery covers the isolated view, not the Windows embedded frame. On Windows the isolated application preload mounts a shell-origin frame inside the main window, covering the content below the 40 DIP caption with a backdrop and dialog. It blocks background page input without creating another native window; moving and maximizing affect only the main window. Caption menus retain mouse and keyboard operation; Web controls in the caption remain reachable. The shared DOM is a presentation constraint, not a security boundary: product scripts can hide the overlay but cannot clear main-process policy or authorize installation through it. Update actions and state use a private MessageChannel between the isolated preload and shell frame. The parent remains enabled for native move, resize, minimize, maximize, and close controls. Closing the application performs cleanup without clearing the update requirement; Esc does not dismiss the overlay. Once installation is approved, installer-owned quit releases the modal before Electron closes windows. Download, file verification including preparation, task inspection, and installation confirmation share this same modal. Only the second user approval permits task teardown and installation; deferral retains the block and package. Restart feedback mentions task stopping only when affected tasks exist. Policy is not persisted across application restarts, and policy responses never revoke or replace an updater artifact.
|
||||
|
||||
Failures retain blocking, localized retry guidance, and folded diagnostics inside the modal. The allowed download-page action appears in recovery states, not beside normal download or installation. Requesting the browser immediately exposes a copy alternative even while the OS request is pending; a resolved request does not prove the page opened. Copy failure reveals the complete, read-only address for manual copying. Browser and clipboard outcomes do not overwrite updater errors. Only a fresh valid no-force response clears the block; the top-menu check remains available while blocked.
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ Electron 主进程在启动时读取一次 `app.getPath('userData')/desktop/sett
|
||||
|
||||
登录和策略请求共用内存 Session,与产品窗口及 updater 隔离;应用重启后需要重新登录。关闭窗口取消登录,导航失败提供本地化重试提示。返回服务后重新查询策略;重定向、Cookie 或 HTTP 422 都不是有效策略决定。取消、登录过期及无效响应均保留已知强更阻塞。固定登录结果写入进程诊断及可选更新日志;登录控制器不记录 Cookie、OAuth 参数或远程错误原文。真实 Harness 网关/API 联调及 macOS 登录验收仍未完成。
|
||||
|
||||
扁平化的 `40005` 打开壳拥有的模态窗口,并拒绝后续插件修改,不停止现有 Host 任务。服务端标题与详情是可选纯文本,缺失时使用客户端兜底文案;缺少下载地址或地址未获批准时隐藏外部页面操作,不解除阻塞。macOS 强更蒙层使用原生窗口内、隔离的壳静态源 WebContentsView,原位渐入渐出,在更新状态切换时保留蒙层并拦截产品键盘输入。加载或更新视图不会显示或激活独立原生窗口;回到已获焦点的父窗口时聚焦视图。关闭原生父窗口遵循[常规关窗行为](#closing-the-window-and-quitting)。视图加载失败保留策略与输入阻断,不改写父窗口标题。仅当所属窗口可见且有焦点时显示原生恢复提示,包含策略说明与可用目标版本。打开下载页面使用现有来源校验后的策略 URL,缺少获准地址时不显示该按钮;退出走正常应用退出确认;取消保留阻断,下次返回前台再次提供恢复。隐藏窗口、策略解除、加载成功或释放控制器会撤销提示,过期响应无效。此恢复覆盖独立视图,不包含 Windows 内嵌 frame。Windows 由隔离的应用 preload 在主窗口内挂载 shell 来源的 frame,以蒙层和弹窗覆盖 40 DIP 顶栏下方的内容区域。它阻止背景页面输入,不创建额外的原生窗口;移动和最大化只作用于主窗口。顶栏菜单保留鼠标和键盘操作,顶栏中的 Web 控件仍可触达。共享 DOM 只约束展示,不构成安全边界:产品脚本可以隐藏蒙层,但不能借此清除主进程策略或授权安装。更新操作与状态通过隔离 preload 和壳 frame 之间的私有 MessageChannel 传递。父窗口保持启用,原生移动、缩放、最小化、最大化和关闭控件仍可操作。退出应用会完成清理,不会解除更新要求;Esc 不会关闭覆盖层。批准安装后,安装器接管的退出流程会在 Electron 关闭窗口前释放模态窗口。下载、含准备步骤的文件校验、任务检查和安装确认共用同一弹窗。只有第二次用户批准才允许任务收尾和安装;稍后更新保留阻塞与安装包。仅存在受影响任务时,重启文案才提示正在停止任务。策略不跨应用重启持久化,策略响应也不作废或替换 updater 产物。
|
||||
扁平化的 `40005` 打开壳拥有的模态窗口,并拒绝后续插件修改,不停止现有 Host 任务。服务端标题与详情是可选纯文本,缺失时使用客户端兜底文案;缺少下载地址或地址未获批准时隐藏外部页面操作,不解除阻塞。macOS 强更蒙层使用原生窗口内、隔离的壳静态源 WebContentsView,原位渐入渐出,在更新状态切换时保留蒙层并拦截产品键盘输入。加载或更新视图不会显示或激活独立原生窗口;回到已获焦点的父窗口时聚焦视图。关闭原生父窗口遵循[常规关窗行为](#closing-the-window-and-quitting)。视图加载失败保留策略与输入阻断,不改写父窗口标题。仅当所属窗口可见且有焦点时显示原生恢复提示,包含策略说明与可用目标版本。打开下载页面使用现有来源校验后的策略 URL,缺少获准地址时不显示该按钮;退出走正常应用退出确认。不提供取消按钮,非操作性关闭保留阻断并重新显示恢复提示。隐藏窗口、策略解除、加载成功或释放控制器会撤销提示,过期响应无效。此恢复覆盖独立视图,不包含 Windows 内嵌 frame。Windows 由隔离的应用 preload 在主窗口内挂载 shell 来源的 frame,以蒙层和弹窗覆盖 40 DIP 顶栏下方的内容区域。它阻止背景页面输入,不创建额外的原生窗口;移动和最大化只作用于主窗口。顶栏菜单保留鼠标和键盘操作,顶栏中的 Web 控件仍可触达。共享 DOM 只约束展示,不构成安全边界:产品脚本可以隐藏蒙层,但不能借此清除主进程策略或授权安装。更新操作与状态通过隔离 preload 和壳 frame 之间的私有 MessageChannel 传递。父窗口保持启用,原生移动、缩放、最小化、最大化和关闭控件仍可操作。退出应用会完成清理,不会解除更新要求;Esc 不会关闭覆盖层。批准安装后,安装器接管的退出流程会在 Electron 关闭窗口前释放模态窗口。下载、含准备步骤的文件校验、任务检查和安装确认共用同一弹窗。只有第二次用户批准才允许任务收尾和安装;稍后更新保留阻塞与安装包。仅存在受影响任务时,重启文案才提示正在停止任务。策略不跨应用重启持久化,策略响应也不作废或替换 updater 产物。
|
||||
|
||||
失败时在同一弹窗内保留阻塞、本地化重试提示和折叠诊断。白名单下载页面操作只在恢复状态出现,不与正常下载或安装并列。请求打开浏览器后立即提供复制替代入口,即使系统请求尚未返回;请求成功不证明网页已打开。复制失败时展示完整、只读的地址供手动复制。浏览器与剪贴板结果不覆盖 updater 错误。只有新的有效无需强更响应才解除阻塞;阻塞期间仍可使用顶部菜单检查。
|
||||
|
||||
|
||||
@@ -275,14 +275,14 @@ export class DesktopMandatoryUpdateWindow {
|
||||
const policy = this.options.policy()
|
||||
const update = this.options.update()
|
||||
const destination = desktopPolicyPage(policy.page, this.options.allowedPageOrigins)
|
||||
const actions: readonly ('page' | 'quit' | 'cancel')[] = destination === undefined ? ['quit', 'cancel'] : ['page', 'quit', 'cancel']
|
||||
const labels = { page: messages.mandatoryViewDownload, quit: messages.quit, cancel: messages.cancel }
|
||||
const actions: readonly ('page' | 'quit')[] = destination === undefined ? ['quit'] : ['page', 'quit']
|
||||
const labels = { page: messages.mandatoryViewDownload, quit: messages.quit }
|
||||
const detail = [policy.detail ?? messages.mandatoryDetail,
|
||||
...('version' in update ? [messages.mandatoryVersion.replace('{version}', update.version)] : []),
|
||||
...(this.navigation?.page === 'failed' ? [messages.mandatoryViewPageFailed] : [])].join('\n\n')
|
||||
void dialog.showMessageBox(parent, { type: 'error', title: messages.mandatoryTitle,
|
||||
message: policy.title ?? messages.mandatoryTitle, detail,
|
||||
buttons: actions.map(action => labels[action]), defaultId: destination === undefined ? 1 : 0, cancelId: actions.length - 1,
|
||||
buttons: actions.map(action => labels[action]), defaultId: 0, cancelId: -1,
|
||||
noLink: true, signal: controller.signal }).then(({ response }) => {
|
||||
if (prompt === controller) prompt = undefined
|
||||
if (controller.signal.aborted || !valid() || !failed) return
|
||||
@@ -291,7 +291,7 @@ export class DesktopMandatoryUpdateWindow {
|
||||
if (destination !== desktopPolicyPage(this.options.policy().page, this.options.allowedPageOrigins)) { show(); return }
|
||||
void this.navigate('page').then(() => { if (this.navigation?.page === 'failed') show() })
|
||||
.catch((error: unknown) => { console.error('desktop policy: recovery download page failed', error) })
|
||||
}
|
||||
} else show()
|
||||
}).catch((error: unknown) => {
|
||||
if (prompt === controller) prompt = undefined
|
||||
console.error('desktop policy: native update recovery failed', error)
|
||||
|
||||
@@ -271,8 +271,8 @@ it('defers native recovery until foreground and opens the policy download page w
|
||||
f.owner.emit('focus')
|
||||
expect(native.messageBox).toHaveBeenCalledOnce()
|
||||
expect(native.messageBox).toHaveBeenCalledWith(f.owner, expect.objectContaining({
|
||||
message: '请更新后继续使用', cancelId: 2,
|
||||
buttons: ['打开下载页面', '退出', '取消'],
|
||||
message: '请更新后继续使用', cancelId: -1,
|
||||
buttons: ['打开下载页面', '退出'],
|
||||
}))
|
||||
expect(native.messageBox.mock.calls[0]![1].detail).toContain('1.0.1-nightly.1')
|
||||
answer.resolve({ response: 0 })
|
||||
@@ -314,7 +314,7 @@ it('uses the normal quit entry only for an explicit native recovery exit', async
|
||||
expect(f.install).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retains blocking after cancellation and offers recovery on the next foreground return', async () => {
|
||||
it('retains blocking and reopens native recovery after a non-action dismissal', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const answer = Promise.withResolvers<{ response: number }>()
|
||||
const f = setup()
|
||||
@@ -323,12 +323,11 @@ it('retains blocking after cancellation and offers recovery on the next foregrou
|
||||
window.loadURL.mockRejectedValueOnce(new Error('local recovery failed'))
|
||||
window.webContents.emit('render-process-gone')
|
||||
await Promise.resolve()
|
||||
answer.resolve({ response: 2 })
|
||||
answer.resolve({ response: -1 })
|
||||
await Promise.resolve()
|
||||
expect(native.quit).not.toHaveBeenCalled()
|
||||
expect(f.view().policy.blocking).toBe(true)
|
||||
expect(window.loadURL).toHaveBeenCalledTimes(2)
|
||||
f.owner.emit('focus')
|
||||
expect(native.messageBox).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@@ -356,8 +355,8 @@ it.each([undefined, 'https://unapproved.example.com/download'])('omits native do
|
||||
window.loadURL.mockRejectedValueOnce(new Error('local recovery failed'))
|
||||
window.webContents.emit('render-process-gone')
|
||||
await Promise.resolve()
|
||||
expect(native.messageBox.mock.calls[0]![1].buttons).toEqual(['退出', '取消'])
|
||||
expect(native.messageBox.mock.calls[0]![1].cancelId).toBe(1)
|
||||
expect(native.messageBox.mock.calls[0]![1].buttons).toEqual(['退出'])
|
||||
expect(native.messageBox.mock.calls[0]![1].cancelId).toBe(-1)
|
||||
expect(native.open).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user