Ver Fonte

feat(desktop): open Feishu login DevTools with F12

winewill há 1 semana atrás
pai
commit
6d05e76b89

+ 2 - 2
.agents/notes/implemented/feature/2026-09-11-desktop-mandatory-update-client.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-11-desktop-mandatory-update-client.md
-2026-09-11-desktop-mandatory-update-client.md: 1a241fedf85fedf32e0935d4d39bb6a2c488ad0e
-2026-09-11-desktop-mandatory-update-client.zh.md: 6a444ec418c52010ba79bd0f75aaba92c7feb811
+2026-09-11-desktop-mandatory-update-client.md: fe067a5d5f803fadd484401d666ca66390a32256
+2026-09-11-desktop-mandatory-update-client.zh.md: 12ed1c84c375157b82d6b2140ffab28a68899660

+ 3 - 1
.agents/notes/implemented/feature/2026-09-11-desktop-mandatory-update-client.md

@@ -10,7 +10,7 @@ A local business server does not deliver remote minimum-version policy. The Desk
 
 ## Decision
 
-The shell polls the guest protocol in the [API proposal](../../proposed/feature/2026-09-08-desktop-mandatory-update-api.md), independently of business traffic. Configuration and application identity are embedded at packaging; packaged applications ignore environment overrides. The [Desktop README](../../../../apps/desktop/README.md) owns configuration fields and defaults. Packaging requires the policy origin selected by the updater deployment before artifact preparation or signing. Test uses isolated Feishu authentication; production remains anonymous. Only unpackaged development can omit policy configuration. This prevents a missing policy setting from silently producing a release without mandatory checks or a test build querying production policy.
+The shell polls the guest protocol in the [API proposal](../../proposed/feature/2026-09-08-desktop-mandatory-update-api.md), independently of business traffic. Configuration and application identity are embedded at packaging; packaged applications ignore environment overrides. The [Desktop README](../../../../apps/desktop/README.md) owns configuration fields and defaults. Packaging requires the policy origin selected by the updater deployment before artifact preparation or signing. Test uses isolated Feishu authentication; production remains anonymous. The test login window opens detached DevTools on F12 while retaining its sandbox, navigation allowlist, and memory-only Session. Only unpackaged development can omit policy configuration. This prevents a missing policy setting from silently producing a release without mandatory checks or a test build querying production policy.
 
 A coordinator owns one immutable installed-client identity, one in-flight request, a deadline, interval jitter, and bounded failure backoff. Manual checks bypass scheduling but join in-flight work. Disposal aborts the request, awaits settlement, and prevents late publication. A flattened `40005` establishes blocking even when required presentation fields are missing or invalid; localized fallback text and a retry action remain available. Transport, JSON, business-code, or HTTP failures cannot clear a known block. Only a valid fresh no-force response clears it. Same-version offline persistence is not implemented because its product behavior remains undecided.
 
@@ -38,6 +38,8 @@ The product preload publishes semantic updater phase, version, progress, and cla
 
 **Use the system browser without a gateway handoff.** Its cookies are not shared with Electron requests. Explicit test deployments instead use a sandboxed BrowserWindow and a dedicated in-memory Session, without Node integration or a preload. Document navigation permits only the configured HTTPS policy origin and the reviewed Feishu origins; permissions, downloads, new windows, and HTTP authentication are denied. The gateway needs no new token-exchange endpoint. This trades persistent sign-in and unrestricted identity-provider navigation for isolation. The default anonymous protocol and updater transport are unchanged.
 
+**Keep DevTools disabled in the test login window.** This prevents local inspection of the third-party page but leaves authentication failures difficult to diagnose. F12 is an explicit local action on that window; ordinary login does not open DevTools.
+
 **Treat a completed login redirect as an update decision.** The Windows gateway probe changed from unauthenticated 401 to application-level 422 and reused its Session after closing the login window. That establishes same-process cookie transport, not compatibility with the Harness policy API. The client rechecks policy after login, retains known blocks on authentication expiry or malformed responses, and never uses the gateway-provided login URL. User confirmation precedes every login window. The initial packaged startup check can explain the authentication requirement before the local backend is ready; later automatic checks stay silent. One operation owns explanation, login, and recheck, so concurrent entry points cannot create competing login windows.
 
 **Make policy availability a precondition for ordinary update feedback.** A policy transport or protocol failure would suppress a valid updater result and make ordinary package delivery depend on the enforcement service. The two checks therefore share a user trigger but keep separate result handling; only an established block can preempt ordinary presentation.

+ 3 - 1
.agents/notes/implemented/feature/2026-09-11-desktop-mandatory-update-client.zh.md

@@ -10,7 +10,7 @@ Status: implemented
 
 ## 决策
 
-壳独立于业务流量轮询[接口提案](../../proposed/feature/2026-09-08-desktop-mandatory-update-api.zh.md)中的游客协议。配置和应用身份在打包时嵌入;打包应用忽略环境覆盖。[Desktop README](../../../../apps/desktop/README.zh.md)负责配置字段与默认值。打包在准备产物或签名前要求提供由 updater 部署环境选定的策略源站。测试环境使用隔离的飞书鉴权;正式环境保持匿名。只有未打包开发模式可以省略策略配置。这避免因遗漏策略配置而静默产出没有强更检查的发布包,或让测试包查询正式环境策略。
+壳独立于业务流量轮询[接口提案](../../proposed/feature/2026-09-08-desktop-mandatory-update-api.zh.md)中的游客协议。配置和应用身份在打包时嵌入;打包应用忽略环境覆盖。[Desktop README](../../../../apps/desktop/README.zh.md)负责配置字段与默认值。打包在准备产物或签名前要求提供由 updater 部署环境选定的策略源站。测试环境使用隔离的飞书鉴权;正式环境保持匿名。测试登录窗口按 F12 会打开独立的 DevTools,同时保留沙箱、导航允许列表和仅驻内存的 Session。只有未打包开发模式可以省略策略配置。这避免因遗漏策略配置而静默产出没有强更检查的发布包,或让测试包查询正式环境策略。
 
 协调器拥有一份不可变的已安装客户端身份、单一在途请求、截止时间、间隔抖动和有上限的失败退避。手动检查绕过调度,但复用在途请求。dispose(资源释放)中止请求、等待结算,并阻止迟到发布。扁平化 `40005` 即使缺少必需展示字段或字段无效也会建立阻塞;本地化兜底文案和重试操作仍可使用。传输、JSON、业务码或 HTTP 错误都不能清除已知阻塞。只有新的有效无需强更响应才能清除它。同版本离线持久化未实现,因为其产品行为尚未确定。
 
@@ -38,6 +38,8 @@ Host 退出与任务成功收尾是两个独立结果。进程所有者通过专
 
 **没有网关交接机制时使用系统浏览器。** 其 Cookie 不会与 Electron 请求共享。显式启用的测试部署改用沙箱 BrowserWindow 和独立内存 Session,不启用 Node 集成或预加载。文档导航只允许配置的 HTTPS 策略源站及已审核的飞书源站;拒绝权限、下载、新窗口和 HTTP 鉴权。网关不需要新增 token 交换接口。这以跨重启登录及不受限的身份提供方导航换取隔离。默认匿名协议及 updater 传输不变。
 
+**在测试登录窗口禁用 DevTools。** 这会阻止本地检查第三方页面,却使鉴权失败难以排查。F12 是该窗口上的显式本地操作;正常登录不会自动打开 DevTools。
+
 **将登录重定向完成视为更新决定。** Windows 网关探针由未鉴权的 401 变为应用层 422,关闭登录窗口后仍复用 Session。这验证了同进程 Cookie 传输,并不证明与 Harness 策略 API 兼容。客户端在登录后重新检查策略,鉴权过期或响应畸形时保留已知阻塞,且从不使用网关提供的登录 URL。每次登录窗口都先经过用户确认。打包应用的首次启动检查可在本地后端就绪前说明鉴权要求;之后的自动检查保持静默。说明、登录和重新检查由同一个操作负责,使并发入口无法创建相互竞争的登录窗口。
 
 **把策略可用作为常规更新反馈的前置条件。** 策略传输或协议失败会压制有效的 updater 结果,并使常规产物分发依赖强更服务。因此两个检查共用用户触发入口,但分别处理结果;只有已建立的强更状态可以抢占常规展示。

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: 503e2a937f4c49a49be6af8deb024db22a0b59f5
-README.zh.md: a3ebd75f25e155f2ce6dd8845e141622a4d05ea7
+README.md: 0ec72f3c5364866ff82111232ae3b61476ae4328
+README.zh.md: 5712f4686f3a317e11ea6b8d9364f98e6fd122b2

+ 1 - 1
apps/desktop/README.md

@@ -278,7 +278,7 @@ Optional `DSH_DESKTOP_MANDATORY_UPDATE_CONFIG` JSON supplies polling and downloa
 
 Durations are integers from 1000 through 2147483647 milliseconds. Startup and scheduled polling are independent of business requests; foreground/resume checks respect the next due time, while manual checks bypass it and join any request in flight. The client sends the installed platform, architecture, exact shell and bundled dsh versions, application ID, locale, and fixed Nightly. It uses no business login credentials or installation ID.
 
-With `feishu-test`, an HTTP 401 JSON response containing `error.code: "UNAUTHENTICATED"` offers login during user-initiated checks and the packaged application's initial startup check, without waiting for the local backend. A localized explanation identifies the test build, the need for Feishu authentication, and that login neither downloads nor installs updates. Confirmation closes the explanation before opening a sandboxed window at the configured origin’s root, not a response-provided login URL. Concurrent checks reuse the entire confirmation/login operation and focus its existing window. Cancellation does not trigger repeated prompts from periodic or foreground checks; users can retry manually.
+With `feishu-test`, an HTTP 401 JSON response containing `error.code: "UNAUTHENTICATED"` offers login during user-initiated checks and the packaged application's initial startup check, without waiting for the local backend. A localized explanation identifies the test build, the need for Feishu authentication, and that login neither downloads nor installs updates. Confirmation closes the explanation before opening a sandboxed window at the configured origin’s root, not a response-provided login URL. Concurrent checks reuse the entire confirmation/login operation and focus its existing window. Press F12 in the test login window to open detached DevTools for diagnosis. Cancellation does not trigger repeated prompts from periodic or foreground checks; users can retry manually.
 
 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.
 

+ 1 - 1
apps/desktop/README.zh.md

@@ -278,7 +278,7 @@ macOS 打包在组装 App 时、代码签名前写入 `Contents/Resources/app-up
 
 时长必须是 1000 至 2147483647 毫秒的整数。启动与定时轮询独立于业务请求;前台/恢复检查遵守下次到期时间,手动检查绕过该时间并复用在途请求。客户端发送已安装平台、架构、完整壳与内置 dsh 版本、应用 ID、语言和固定 Nightly。不使用业务登录凭据或安装 ID。
 
-启用 `feishu-test` 时,包含 `error.code: "UNAUTHENTICATED"` 的 HTTP 401 JSON 响应会在用户主动检查和打包应用首次启动检查时提供登录入口,不等待本地后端就绪。本地化说明指出这是测试版、需要飞书鉴权,且登录不会下载或安装更新。确认后先关闭说明,再打开配置源站根路径的沙箱窗口,不使用响应中的登录 URL。并发检查复用整个确认/登录流程,并聚焦已有窗口。取消后,定时或前台检查不会反复弹窗;用户可手动重试。
+启用 `feishu-test` 时,包含 `error.code: "UNAUTHENTICATED"` 的 HTTP 401 JSON 响应会在用户主动检查和打包应用首次启动检查时提供登录入口,不等待本地后端就绪。本地化说明指出这是测试版、需要飞书鉴权,且登录不会下载或安装更新。确认后先关闭说明,再打开配置源站根路径的沙箱窗口,不使用响应中的登录 URL。并发检查复用整个确认/登录流程,并聚焦已有窗口。在测试环境登录窗口按 F12 可打开独立的 DevTools 进行排查。取消后,定时或前台检查不会反复弹窗;用户可手动重试。
 
 登录和策略请求共用内存 Session,与产品窗口及 updater 隔离;应用重启后需要重新登录。关闭窗口取消登录,导航失败提供本地化重试提示。返回服务后重新查询策略;重定向、Cookie 或 HTTP 422 都不是有效策略决定。取消、登录过期及无效响应均保留已知强更阻塞。固定登录结果写入进程诊断及可选更新日志;登录控制器不记录 Cookie、OAuth 参数或远程错误原文。真实 Harness 网关/API 联调及 macOS 登录验收仍未完成。
 

+ 6 - 1
apps/desktop/src/policy-test-auth.ts

@@ -74,7 +74,7 @@ export class DesktopPolicyTestAuth {
     const window = new BrowserWindow({ width: 720, height: 760, ...(parent === undefined ? {} : { parent }),
       title: this.locale.messages.policyLoginTitle, autoHideMenuBar: true,
       webPreferences: { session: this.browserSession, nodeIntegration: false, contextIsolation: true,
-        sandbox: true, webSecurity: true, webviewTag: false, devTools: false, spellcheck: false } })
+        sandbox: true, webSecurity: true, webviewTag: false, devTools: true, spellcheck: false } })
     this.pending = result.promise
     this.window = window
     let settled = false
@@ -93,6 +93,11 @@ export class DesktopPolicyTestAuth {
     window.on('closed', () => { finish('cancelled') })
     window.on('page-title-updated', (event) => { event.preventDefault() })
     const contents = window.webContents
+    contents.on('before-input-event', (event, input) => {
+      if (input.type !== 'keyDown' || input.key !== 'F12' || input.isAutoRepeat) return
+      event.preventDefault()
+      contents.openDevTools({ mode: 'detach' })
+    })
     contents.setWindowOpenHandler(() => ({ action: 'deny' }))
     contents.on('will-navigate', (event, url) => {
       if (!this.allowed(url)) { event.preventDefault(); finish('failed') }

+ 18 - 2
apps/desktop/tests/policy-test-auth.spec.ts

@@ -25,7 +25,7 @@ function makeSession() {
 function makeWindow() {
   let destroyed = false
   const instance = Object.assign(new EventEmitter(), { webContents: Object.assign(new EventEmitter(), {
-    setWindowOpenHandler: vi.fn<(handler: () => { action: string }) => void>() }),
+    setWindowOpenHandler: vi.fn<(handler: () => { action: string }) => void>(), openDevTools: vi.fn() }),
   show: vi.fn(), focus: vi.fn(), setMenu: vi.fn(), loadFile: vi.fn(async () => {}),
   loadURL: vi.fn(async () => {}), isDestroyed: () => destroyed,
   destroy: () => { if (!destroyed) { destroyed = true; instance.emit('closed') } } })
@@ -57,7 +57,7 @@ it('opens a sandboxed window on explicit action and coalesces logins without aut
   expect(native.create).toHaveBeenCalledTimes(1)
   expect(native.create.mock.calls[0]![0]).toMatchObject({ title: '登录测试环境', webPreferences: {
     session: browserSession, nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true,
-    webviewTag: false, devTools: false } })
+    webviewTag: false, devTools: true } })
   expect(native.create.mock.calls[0]![0].webPreferences?.preload).toBeUndefined()
   expect(window.loadFile).toHaveBeenCalledWith('renderer/policy-login-loading.html', { query: { label: '正在加载登录页面…' } })
   // The placeholder is the first document: the remote page waits for it.
@@ -72,6 +72,22 @@ it('opens a sandboxed window on explicit action and coalesces logins without aut
   expect(browserSession.fetch).not.toHaveBeenCalled()
 })
 
+it('opens only the Feishu login window DevTools on a single F12 keydown', () => {
+  void auth.login()
+  const preventDefault = vi.fn()
+  const press = (type: string, key: string, isAutoRepeat = false) => {
+    window.webContents.emit('before-input-event', { preventDefault }, { type, key, isAutoRepeat })
+  }
+  press('keyDown', 'F11')
+  press('keyUp', 'F12')
+  press('keyDown', 'F12', true)
+  expect(window.webContents.openDevTools).not.toHaveBeenCalled()
+  expect(preventDefault).not.toHaveBeenCalled()
+  press('keyDown', 'F12')
+  expect(preventDefault).toHaveBeenCalledOnce()
+  expect(window.webContents.openDevTools).toHaveBeenCalledExactlyOnceWith({ mode: 'detach' })
+})
+
 it('shows the placeholder without letting it outlive the first remote document', async () => {
   const placeholder = { resourceType: 'mainFrame', url: 'file:///app/renderer/policy-login-loading.html?label=x' }
   const callback = vi.fn()