diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index b88e023..93e9f28 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,6 +4,7 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through +- `55e61b7`: macOS Robot hotspot discovery performs one bounded worker-thread rescan after an empty or SSID-redacted CoreWLAN result; persistent SSID redaction maps to the existing permission error instead of a misleading empty-device state, while firmware and the open `Xiaozhi-*` contract remain unchanged. - `f5d47c8` / `b6148a5`: AI Programming voice capture is available after an Agent is selected but before the lazy first OpenCode session exists; transcription fills the composer draft without creating an empty session, while runtime, loading, transcribing, busy, and recording guards remain unchanged. - `4013edc` / `3b799af`: integrated per-user Code/Canvas/Learning/Robot entry policy from Works Square, projected by Electron Main as four booleans and enforced before disabled module routes initialize. - `01bee31`: enabled AI Learning course catalog/generation/download/playback, Main-owned cloud/runtime bridges, verified external OpenMAIC player-artifact packaging, account profile reuse, removal of the transient `game-engine` Skill, and project-root `planning-with-files` output from the authoritative remote main. The merge hardens this with strict DTO/error projection, account-isolated local state, bounded same-origin downloads/packages, a nonce-protected account-bound player HTTP session, and an exact-source/origin single-document iframe bridge. @@ -44,10 +45,11 @@ AI 编程首次发送在新建 OpenCode session 已知为空时不再等待冗 Updater 仍由 Electron Main 选择目标 feed、记录原始诊断并保持失败语义。正式稳定源缺少对应平台 manifest 时,设置页只显示一条简洁中文提示并允许重试,不把缺包误报为已是最新版,也不向普通界面暴露堆栈、URL、路径或错误码;签名产物发布和真实升级安装仍属于外部 Release Gate。 -Robot 绑定设备默认先显示“引导配网 / 已有激活码”路径选择。引导路径在页面内扫描附近符合条件的开放 `Xiaozhi-*` 热点,要求用户明确选择后由 Main-owned Windows/macOS 原生适配器连接并核验当前 SSID;失败时保留系统 Wi-Fi 兜底。后续继续使用固定 `http://192.168.4.1/` 系统浏览器页面、恢复互联网和现有六位 Binding;精确环境值 `0` 可回滚为直接六位码。Makelore 不接收 Wi-Fi 密码、不修改固件,也不把热点发现或 Binding 成功等同于可信身份/在线。签名 macOS、Windows 真机、指定固件/发行契约和完整整链仍未完成验证。 +Robot 绑定设备默认先显示“引导配网 / 已有激活码”路径选择。引导路径在页面内扫描附近符合条件的开放 `Xiaozhi-*` 热点,要求用户明确选择后由 Main-owned Windows/macOS 原生适配器连接并核验当前 SSID;失败时保留系统 Wi-Fi 兜底。macOS 在首次 CoreWLAN 结果为空或 SSID 全不可见时只进行一次 250 ms worker-thread 延迟重扫;第二次仍有网络但没有可读 SSID 时显示定位权限错误,不再冒充“没有发现设备热点”。后续继续使用固定 `http://192.168.4.1/` 系统浏览器页面、恢复互联网和现有六位 Binding;精确环境值 `0` 可回滚为直接六位码。Makelore 不接收 Wi-Fi 密码、不修改固件,也不把热点发现或 Binding 成功等同于可信身份/在线。签名 macOS、Windows 真机、指定固件/发行契约和完整整链仍未完成验证。 ## Recently Completed +- 2026-08-17: Corrected macOS Robot hotspot discovery after a system-visible `Xiaozhi-*` report. CoreWLAN now gets one bounded retry when its first result is empty or all SSIDs are unavailable; a persistent non-empty redacted result becomes the existing safe permission state. Open-only filtering, firmware, Host/Renderer contracts, exact-current-SSID verification, and the system-Wi-Fi fallback are unchanged; signed-package physical smoke remains pending. - 2026-08-17: Created merge commit `4013edc` for the reviewed per-user module-entry policy source tip `3b799af`. Main exposes only four booleans from `/api/auth/me`; missing fields remain enabled, `design` maps to `painting`, disabled root/deep/alias routes stop before module initialization, Code provider startup waits for policy hydration, terminal `401` clears both session layers, and global settings remains reachable. - 2026-08-17: Integrated remote `01bee31`: Learning is enabled with course browsing, strict bounded generation materials, verified atomic course installation, multi-module playback, Main-owned Agent/ASR/runtime bridges, and a manifest-verified external OpenMAIC player artifact. Merge review added account-isolated generation/library/player state, fixed-binding token/fetch/401 guards, passive-only course media with hardened responses, pre-existing active-registration checks before side-effect-free identity resolution, nonce-protected single-document player sessions, and a recoverable deep-link profile error gate. Publishing now reflects the actual Works contract: first create is coverless, existing draft/published are version-only, and races fail closed without cover/PATCH side effects. The transient `game-engine` Skill was removed and `planning-with-files` writes its files to the project root. Production Works/player-artifact/signed-package acceptance remains pending. - 2026-08-16: Integrated remote `26b52d7`: Canvas now has server-backed Prompt Museum navigation, editable server-repriced generation Quotes, task result details/downloads, guarded project deletion, cloud-default development, and Chinese-only UI. That tip briefly bundled `game-engine`; authoritative successor `01bee31` removed it. Client integration is verified separately from production Prompt Museum data/backend deployment. diff --git a/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md b/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md index 6a00d7c..afefcdf 100644 --- a/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md +++ b/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md @@ -12,6 +12,7 @@ ## Scope +- On 2026-08-17, resume the existing Integration owner to merge reviewed macOS Robot hotspot scan source `55e61b7` into local `main`, preserve the firmware/open-SSID/Main-owned boundaries, retain signed-package physical smoke as a release gate, and keep remote push outside this resumption. - On 2026-08-17, resume the existing Integration owner at the user's request to merge reviewed AI Programming fresh-Agent voice-input source `b6148a5` into local `main`, preserve lazy first-session creation and existing voice guards, run merged-tree verification and independent review, and keep remote push outside this resumption. - On 2026-08-17, resume the existing Integration owner after the user's explicit takeover authorization to preserve the task's existing uncommitted record, merge reviewed Makelore module-access source tip `3b799af` into local `main`, promote its accepted per-user entry-policy facts, run merged-tree verification and independent review, and keep remote push outside this resumption unless separately authorized. - On 2026-08-16, resume the existing Integration owner after the user restored remote credentials: fetch authoritative `origin/main`, complete the already-started merge of remote tip `26b52d7`, preserve both remote Canvas/Prompt Museum work and local Robot hotspot work, run merged-tree verification and independent review, then perform a normal non-forced push and verify remote-tip equality. @@ -119,6 +120,14 @@ Gate result: - Canonical reconciliation, merged-tree checks and independent final review all passed. Git created normal no-ff merge commit `4013edcbbc982fcbef0a0ff6287c5307cd87bf8a` with first parent `eb16f73e4dece86a193dee0f797a58d77869470f` and reviewed source `3b799af17370dfa6c4c995c3fcbab8f2d84c3dda` as second parent. - Gate result: Passed for canonical promotion and merged-tree verification. Works migration/API deployment, a newly built client package, real-account four-module smoke and server-side API authorization validation remain release commitments. +### 2026-08-17 macOS Robot Hotspot Scan Integration Resume + +- Reused the existing Integration owner because it exclusively owns clean local `main` at `7e8d9e38114158992c03e589de32535274796d04`; `task_context.py touch` refreshed the reservation and registry status confirms mode `integration`, branch `main`, and this worktree. +- Formed reviewed source commit `55e61b7d8f9caccdad54dc9a42092fc7960fb394` from feature task `20260817-robot-hotspot-scan-missing-4c7e91`. The source task is `ready_for_integration`, its worktree is clean, and final Sol review returned Standards/Spec/overall `PASS` with no blocking findings. +- Read the source outcome and follow-ups against ADR-003, the Main-owned Robot Hotspot Module, current state, evidence, and the existing signed-macOS/physical-device commitment. The repair changes no architecture, firmware, Host/Renderer DTO, candidate filter, credential, Portal, Binding, or rollback decision. +- Started a normal `--no-ff --no-commit` merge of `55e61b7`; Git reported no textual conflict. The source task record remains on the source commit/branch and is excluded from the integrated `main` tree. +- Gate result: Passed for a local no-ff merge, minimal current-state reconciliation, proportionate merged-tree verification, and independent final review. Remote push remains outside this resumption. + ### 2026-08-16 Remote `26b52d7` Synchronization Resume - Reused the existing Integration owner and refreshed its reservation. Local `main` entered this resumption at `9af6c526a9500a0dbfb88e39ba0dee1eb7e1d097`; the worktree already contained an unfinished merge whose `MERGE_HEAD` was `26b52d76e3dedd754ca1b1c428abaa074b7f98da`. @@ -196,6 +205,13 @@ Gate result: ## Plan +### 2026-08-17 macOS Robot Hotspot Scan Integration Plan + +1. Merge reviewed source `55e61b7` into local `main` with a normal no-ff merge, preserving source history while excluding the source-owned task record from the final tree. +2. Record the integrated macOS retry/permission behavior in current state without changing ADR-003, firmware, Host/Renderer, open `Xiaozhi-*`, exact-current-SSID, manual-fallback, or rollback contracts. +3. Run the 14-test Robot hotspot module suite, full unit suite, typecheck, scoped lint, production build, task-aware document gates, whitespace/unmerged checks, source-record exclusion, and exact topology checks. +4. Obtain an independent read-only Sol `PASS`/`FAIL` review, create the verified local merge commit, record its exact parents, and do not push remotely. + ### 2026-08-17 Fresh-Agent Voice Input Integration Plan 1. Merge reviewed source `b6148a5` into local `main` with a normal no-ff merge, preserving source history while excluding the source-owned task record from the final tree. @@ -282,6 +298,10 @@ Gate result: ## Outcome +- On 2026-08-17, staged a normal no-ff merge of reviewed macOS Robot hotspot source `55e61b7` onto clean local `main` at `7e8d9e3`; Git reported no textual conflicts. The source task record remains reachable on its source commit/branch and is excluded from the integrated tree. +- The staged tree performs exactly one 250 ms worker-thread rescan when the first macOS CoreWLAN result is empty or every returned SSID is unavailable. A recovered candidate continues through the unchanged open/printable `Xiaozhi-*` Module filter; two empty scans remain an empty result; a second non-empty fully redacted result becomes the existing fixed permission error. +- No firmware, entitlements, Host API, Renderer, Portal, Binding, credential, Windows adapter, or remote ref changes are part of this resumption. Signed macOS x64/arm64 physical scan/connect smoke remains an explicit release gate. + - Created normal no-ff merge commit `f5d47c849e57db87051d59d4ccfb3fda7cab1271` with local `main` baseline `9fd9a7761d3872f25e853ac1b059e55251c6ae8f` as first parent and reviewed source `b6148a5cb45e70a75c055a57f8b269f24c619a6a` as second parent. This resumption is complete locally and did not push a remote ref. - Independent final staged-merge review returned `PASS` with no P0-P3 findings. It confirmed the exact no-ff topology, four-file scope, source-task-record exclusion, lazy-session behavior, preserved guards, regression strength, and document ownership boundaries. - The staged voice-input merge preserves the exact first-parent/second-parent topology `9fd9a77` + `b6148a5`, contains no unmerged entries or unstaged changes, and has passed merged-tree product, build, and project-document gates. It is ready for independent final review before the local merge commit is created. @@ -410,6 +430,12 @@ Gate result: ## Verification +- 2026-08-17 independent final macOS Robot hotspot staged-merge review — Standards `PASS`, Spec `PASS`, overall `PASS`, with no P0-P3 findings. It confirmed exact staged topology/scope, source-record exclusion, bounded worker-only retry, retained 8s abort/termination, unchanged filtering/connect contracts, and honest signed-macOS physical-smoke residual risk. +- 2026-08-17 macOS Robot hotspot staged-merge regression — `tests/unit/robot-hotspot-module.test.ts` passed 14/14, covering first-scan recovery, persistent SSID redaction, bounded double-empty behavior, security filtering, RSSI/current-SSID projection, worker termination, candidate expiry, exclusivity, and exact-current-SSID verification. +- 2026-08-17 macOS Robot hotspot staged-merge `pnpm test` — 175 files / 2052 tests passed. +- 2026-08-17 staged-merge `pnpm run typecheck` and scoped ESLint on `macos.ts` plus its unit test — passed. +- 2026-08-17 staged-merge `pnpm run build:vite` — Renderer, Electron Main, and Preload passed; the emitted macOS chunk retains the one 250 ms worker wait, bounded rescan, and fixed permission mapping. Only existing mixed-import and chunk-size warnings remain. +- `check_project_docs.py`, integration task-aware `check_doc_drift.py`, task registry doctor, staged/unstaged whitespace checks, unmerged-entry check, source-task-record exclusion, and merge-base checks — passed before independent review. - Post-commit topology check confirmed merge `f5d47c8` has exact parents `9fd9a77` and `b6148a5`, the source tip is a `main` ancestor, the source task record is absent from the `main` tree, and the product worktree was clean before this evidence-only update. - 2026-08-17 independent final fresh-Agent voice integration review — `PASS`, no P0-P3 findings; exact staged topology/scope, source-record exclusion, lazy session creation, existing guards, test quality, and project-document boundaries were confirmed. - 2026-08-17 fresh-Agent/lazy-session/voice focused selection — 5/5 passed; the complete `opencode-chat-panel.test.tsx` file passed 85/85 on the staged merged tree. diff --git a/electron/robot-hotspot/macos.ts b/electron/robot-hotspot/macos.ts index 1419cb4..bfa4be6 100644 --- a/electron/robot-hotspot/macos.ts +++ b/electron/robot-hotspot/macos.ts @@ -10,6 +10,7 @@ const LOCATION_AUTHORIZED_ALWAYS = 3; const LOCATION_AUTHORIZED_WHEN_IN_USE = 4; const CORE_WLAN_PERMISSION_DENIED = -3930; const CORE_WLAN_UNSUPPORTED = -3903; +const SCAN_RETRY_DELAY_MS = 250; type StableReason = 'unsupported' | 'permission_denied' | 'scan_failed' | 'connect_failed'; type ObjcId = unknown; @@ -21,6 +22,36 @@ interface WorkerCandidate { open: boolean; } +export interface MacosScanNetwork { + ssid: string | null; + rssi: number; + open: boolean; +} + +export function scanMacosCandidatesWithRetry( + readNetworks: () => MacosScanNetwork[], + currentSsid: string | null, + waitBeforeRetry: () => void, +): WorkerCandidate[] { + let networks = readNetworks(); + if (networks.length === 0 || networks.every((network) => !network.ssid)) { + waitBeforeRetry(); + networks = readNetworks(); + } + if (networks.length > 0 && networks.every((network) => !network.ssid)) { + throw new RobotHotspotAdapterError('permission_denied'); + } + return networks.flatMap((network): WorkerCandidate[] => { + if (!network.open || !network.ssid) return []; + return [{ + ssid: network.ssid, + signalPercent: Math.max(0, Math.min(100, Math.round((network.rssi + 100) * 2))), + connected: currentSsid === network.ssid, + open: true, + }]; + }); +} + type WorkerRequest = { id: number; operation: 'scan' } | { id: number; operation: 'connect'; ssid: string }; type WorkerResponse = { id: number; ok: true; result: WorkerCandidate[] | { ssid: string } } | { id: number; ok: false; reason: StableReason }; @@ -219,13 +250,16 @@ async function runCoreWlanWorker(): Promise { return withPool(() => { const wifi = wifiInterface('scan'); const current = stringValue(sendId(wifi, selectors.ssid)); - return scanNetworks('scan').flatMap((network): WorkerCandidate[] => { - if (!sendSupportsSecurity(network, selectors.supportsSecurity, 0)) return []; - const ssid = stringValue(sendId(network, selectors.ssid)); - if (!ssid) return []; - const rssi = Number(sendLong(network, selectors.rssiValue)); - return [{ ssid, signalPercent: Math.max(0, Math.min(100, Math.round((rssi + 100) * 2))), connected: current === ssid, open: true }]; - }); + const readNetworks = (): MacosScanNetwork[] => scanNetworks('scan').map((network) => ({ + ssid: stringValue(sendId(network, selectors.ssid)), + rssi: Number(sendLong(network, selectors.rssiValue)), + open: sendSupportsSecurity(network, selectors.supportsSecurity, 0), + })); + return scanMacosCandidatesWithRetry( + readNetworks, + current, + () => { Atomics.wait(pollSleep, 0, 0, SCAN_RETRY_DELAY_MS); }, + ); }); } diff --git a/tests/unit/robot-hotspot-module.test.ts b/tests/unit/robot-hotspot-module.test.ts index ce70786..779f4e3 100644 --- a/tests/unit/robot-hotspot-module.test.ts +++ b/tests/unit/robot-hotspot-module.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { RobotHotspotAdapter, RobotHotspotAdapterCandidate } from '@electron/robot-hotspot/adapter'; import { RobotHotspotError, createRobotHotspotModule } from '@electron/robot-hotspot'; -import { MacosWorkerController, requestMacosWorker } from '@electron/robot-hotspot/macos'; +import { MacosWorkerController, requestMacosWorker, scanMacosCandidatesWithRetry } from '@electron/robot-hotspot/macos'; import type { MacosWorkerLike } from '@electron/robot-hotspot/macos'; function candidate(ssid: string, signalPercent: number, overrides: Partial = {}): RobotHotspotAdapterCandidate { @@ -22,6 +22,62 @@ async function expectCode(promise: Promise, code: string): Promise { + it('retries once when the first macOS scan hides every SSID and returns the recovered open hotspot', () => { + const readNetworks = vi.fn() + .mockReturnValueOnce([ + { ssid: null, rssi: -80, open: true }, + { ssid: '', rssi: -60, open: true }, + ]) + .mockReturnValueOnce([ + { ssid: 'Xiaozhi-7A2B', rssi: -58, open: true }, + { ssid: 'Private-Network', rssi: -42, open: false }, + ]); + const waitBeforeRetry = vi.fn(); + + expect(scanMacosCandidatesWithRetry(readNetworks, 'Xiaozhi-7A2B', waitBeforeRetry)).toEqual([ + { ssid: 'Xiaozhi-7A2B', signalPercent: 84, connected: true, open: true }, + ]); + expect(readNetworks).toHaveBeenCalledTimes(2); + expect(waitBeforeRetry).toHaveBeenCalledOnce(); + }); + + it('reports permission denied when both macOS scans find networks but expose no SSIDs', () => { + const readNetworks = vi.fn() + .mockReturnValueOnce([{ ssid: null, rssi: -70, open: true }]) + .mockReturnValueOnce([{ ssid: '', rssi: -65, open: true }]); + + expect(() => scanMacosCandidatesWithRetry(readNetworks, null, vi.fn())).toThrowError( + expect.objectContaining({ reason: 'permission_denied' }), + ); + expect(readNetworks).toHaveBeenCalledTimes(2); + }); + + it('bounds an empty macOS scan retry and keeps an empty result empty', () => { + const readNetworks = vi.fn().mockReturnValue([]); + const waitBeforeRetry = vi.fn(); + + expect(scanMacosCandidatesWithRetry(readNetworks, null, waitBeforeRetry)).toEqual([]); + expect(readNetworks).toHaveBeenCalledTimes(2); + expect(waitBeforeRetry).toHaveBeenCalledOnce(); + }); + + it('does not retry a readable macOS scan and filters secured networks while clamping RSSI', () => { + const readNetworks = vi.fn().mockReturnValue([ + { ssid: 'Xiaozhi-weak', rssi: -120, open: true }, + { ssid: 'Xiaozhi-strong', rssi: -20, open: true }, + { ssid: 'Xiaozhi-secured', rssi: -50, open: false }, + { ssid: null, rssi: -40, open: true }, + ]); + const waitBeforeRetry = vi.fn(); + + expect(scanMacosCandidatesWithRetry(readNetworks, 'Xiaozhi-strong', waitBeforeRetry)).toEqual([ + { ssid: 'Xiaozhi-weak', signalPercent: 0, connected: false, open: true }, + { ssid: 'Xiaozhi-strong', signalPercent: 100, connected: true, open: true }, + ]); + expect(readNetworks).toHaveBeenCalledOnce(); + expect(waitBeforeRetry).not.toHaveBeenCalled(); + }); + it('terminates a hanging macOS native worker when the request is aborted', async () => { const worker = { postMessage: vi.fn(),