AidaWork Desktop — 架构可视化包

源码有据架构简报 · 6 个视角 · aida-desktop (Electron + Vite + React)

结论

一个基于类型化 IPC 的 Electron 宿主:受信任的 React 渲染器通过 SSE 流式传输聊天,服务端编排器将高风险本地工具调用委托给受 TTL 治理、需审批的混合本地执行器——安全意识强、分层清晰,但若干执行隔离与运行时 spawn 边界仍未确认。

系统地图

应用是单一 Electron 宿主,划分为三个信任域。主进程是组合根:拥有 12 个类型化 IPC 命名空间 (30 个 invoke + 8 个 push 通道),通过 shared/ipc-contract.ts 接线, 并负责 auth/token 交换、配置存储、混合本地执行器、文档摄入、更新生命周期、语音辅助与 BI 洞察通道。 preload 层仅桥接一个派生的窄接口(受信任 UI 用 aidaDesktop; 不受信任的 ChatBI 访客用 __aidaBiBridge)。渲染器组合 HR 工作台壳层、 聊天逻辑层(src/chat/core)与展示面。

横切主线:共享 TtlRegistry 支撑所有待决策状态机(写入审批、临时文件授权、文档摄入、提取任务)。 Token 身份绑定到 endpoint identity(origin + basePath)。聊天按派生的 chat identity(tenantId:userId)划分作用域。下方六张图从进程拓扑一直下探到各状态机。

1 · 进程拓扑与 IPC

进程拓扑与 IPC

进程拓扑与 IPC:Renderer / Preload / Main 与 12 个类型化命名空间

flowchart LR
  subgraph REN["Renderer 进程 (React)"]
    direction TB
    R["window.aidaDesktop<br/>(受信任 UI)"]
    GUEST["ChatBI 访客渲染器<br/>(不受信任 WebContentsView)"]
  end

  subgraph PRE["Preload (contextBridge 隔离)"]
    direction TB
    BRIDGE["IpcBridge<br/>aidaDesktop exposeInMainWorld"]
    BIB["__aidaBiBridge<br/>(窄接口,无 aida-* 通道)"]
  end

  subgraph MAIN["Main 进程 (组合根)"]
    direction TB
    SERVER["ipc/server.ts<br/>registerInvoke / pushTo 包装器"]
    CONTRACT["ipc-contract.ts<br/>IpcInvokeContract 30 invoke / IpcPushContract 8 push"]
    subgraph SVC["命名空间处理器 (12 个命名空间)"]
      direction TB
      N1["aida-config (4 invoke) / ConfigStore"]
      N2["aida-auth (3 invoke, 1 push)"]
      N3["aida-api (4 invoke) / AidaApiClient"]
      N4["aida-chat (4 invoke, 1 push)"]
      N5["aida-speech (3 invoke, 1 push) / SpeechHelper"]
      N6["aida-local-executor (17 invoke, 1 push) / LocalExecutorService"]
      N7["aida-document-intake (8 invoke, 1 push) / DocumentIntakeService"]
      N8["aida-update (4 invoke, 1 push) / VelopackDesktopUpdater"]
      N9["aida-diagnostics (2 invoke, 1 push) / OperationHistory"]
      N10["aida-notify (1 invoke)"]
      N11["aida-shell (1 invoke)"]
      N12["aida-bi (5 invoke, 1 push, BI_GUEST_CHANNEL on) / BiInsightChannel"]
    end
  end

  R -->|"属性访问 (invoke / subscribe)"| BRIDGE
  BRIDGE -->|"从契约派生形状"| CONTRACT
  BRIDGE -->|"ipcRenderer.invoke/on 到 ipcMain.handle"| SERVER
  SERVER -->|"强制 channel/payload 类型"| CONTRACT
  SERVER -->|"分发到"| SVC

  GUEST -->|"BiGuestMessage / BiHostMessage"| BIB
  BIB -.->|"ipcMain.on BI_GUEST_CHANNEL (不可达 aida-*)"| N12

  SVC -.->|"push 通道 main 到 renderer (共 8 个)"| BRIDGE
依据: shared/ipc-contract.ts:375-556 (30 invoke + 8 push), electron/preload.ts:139 (aidaDesktop), electron/chatbi-preload.ts:39 (__aidaBiBridge), electron/ipc/server.ts:1, electron/main.ts:52,139,442,517,560,628,659,767

2 · 聊天与 SSE 数据流

聊天与 SSE 数据流

聊天与 SSE 数据流:单次 Chat-Send 生命周期

sequenceDiagram
    autonumber
    participant Composer as "ChatSurface + AssistantRuntimeAdapter"
    participant Hook as "useDesktopSSEChat (transport + world effects)"
    participant Session as "useChatSessionStore (bindings)"
    participant Api as "createChatStream / authFetch"
    participant Main as "Electron main AidaApiClient (token to JWT)"
    participant Backend as "AIDA backend /api/chat (SSE)"
    participant SSE as "parseSSEStream (frame parser)"
    participant Run as "Run timeline reducer (reduceServer / reduceTransport)"
    participant Store as "useChatMessageStore (applyToMessage seam)"
    participant Local as "dispatchDelegatedToolCall + local executor IPC"
    participant Read as "Read-side projections (processPreview / timelineModel)"

    Composer->>Hook: send(message, sessionId, options)
    Hook->>Store: addMessage user + assistant (streaming, empty timeline)
    Hook->>Session: read bindings (backendSessionId, conversationId, routingMode, agentId)
    Note over Hook: createRunTimeline() per run / applyEvent wraps run.apply via applyToMessage

    Hook->>Api: createChatStream(message, sessionId, agentId or null for unified entry, localExecutorPayload)
    Api->>Main: authFetch attaches AIDA JWT
    Main->>Main: exchange backend token for JWT (single-flight refresh)
    Main->>Backend: POST /api/chat (server routes when no agentId)
    Backend-->>Api: SSE response stream

    alt response not ok
        Hook->>Run: applyEvent stream_failed (AUTH_REQUIRED / HTTP_ERROR / unified fallback)
        Run->>Store: write failed timeline
    else response ok
        Hook->>Run: applyEvent stream_connected (transport)
        Run->>Store: runDetail.streamConnectedAt

        Hook->>SSE: parseSSEStream(response, onEvent)
        Note over SSE: skip keepalive frames / silently drop malformed JSON
        loop each AgentEvent frame
            SSE-->>Hook: onEvent(AgentEvent)
            Hook->>Run: applyEvent first (timeline before world effect)
            Run->>Run: reduceServer pattern-matches AgentEvent / deriveToolResultStatus sets ToolCallInfo.status
            Run->>Store: applyToMessage updates ChatMessage (content, toolCalls, artifacts, routingEvents)

            alt session_bound
                Hook->>Session: setBackendSessionId(sessionId)
            else run_bound
                Hook->>Session: setConversationMeta(conversationId, runId)
            else tool_call with domain desktop
                Hook->>Local: dispatchDelegatedToolCall over IPC
                Local-->>Hook: applyEvent local_dispatch_settled (transport)
                Hook->>Run: reduceTransport settles artifact / tool status
                Run->>Store: applyToMessage writes TaskArtifact + status
            else capability route_decision
                Run->>Store: stored in runDetail.routingEvents
            end
        end
        Hook->>Run: applyEvent stream_disconnected (or stream_aborted on cancel)
        Run->>Store: sweep pending tools / runDetail.endedAt
    end

    Read->>Store: read ChatMessage timeline
    Read->>Composer: processPreview (运行过程) + timelineModel + convertAidaMessageToAssistantMessage render
已验证自 useDesktopSSEChat.ts:156-459, runTimeline.ts:243-575, sse.ts:9-57, store/message.ts:69-73, plus core/lib/types.ts and runDetail projections

3 · 认证与端点身份

认证与端点身份

后端 Token 生命周期与 AIDA Desktop 中的 Endpoint Identity 绑定

flowchart TB
  classDef main fill:#1f2a44,stroke:#4f7cff,stroke-width:1.2px,color:#dce6ff
  classDef shared fill:#1d3a30,stroke:#36c08a,stroke-width:1.2px,color:#d6f5e8
  classDef renderer fill:#3a2440,stroke:#c069e0,stroke-width:1.2px,color:#f1dcff
  classDef doc fill:#3a3320,stroke:#d2a23c,stroke-width:1.2px,color:#f6e9c8
  classDef intent fill:#27313f,stroke:#6b7a93,stroke-width:1px,color:#c7d2e0,stroke-dasharray:3 3
  classDef unknownNode fill:#2b2030,stroke:#8a5a9c,stroke-width:1px,color:#e8d4f0,stroke-dasharray:5 4

  subgraph MAIN["Electron Main / Auth"]
    MainTs["electron/main.ts<br/>组合根:接线 server、store、api client、IPC"]:::main
    AuthServer["AuthCallbackServer<br/>回环 HTTP 捕获 /auth/callback"]:::main
    Extract["extractBackendToken<br/>解析 query/body 共 5 个字段名"]:::main
    ApiClient["AidaApiClient<br/>backend token 到 agent JWT 交换 + fetch"]:::main
    Classify["classifyExchangeError<br/>将交换失败映射为错误类"]:::main
    ConfigStore["ConfigStore<br/>持久化设置、endpoint intent、token 保留"]:::main
  end

  subgraph SHARED["共享契约"]
    ConfigTypes["config-types<br/>EndpointPersistenceIntent, resolvePersistedEndpoint, resolvers"]:::shared
    Endpoint["endpoint<br/>canonicalEndpointIdentity, normalizers, validators (origin, basePath)"]:::shared
    IpcContract["IpcBridge/IpcContract<br/>main/preload/renderer 的 auth/config 通道接缝"]:::shared
  end

  subgraph RENDERER["Renderer"]
    Orchestrator["AuthCallbackOrchestrator<br/>合并 hash/push/poll 通道并去重"]:::renderer
    UseAuth["useAuthSession<br/>React 适配器,login/logout 流程"]:::renderer
    Config["src/config.ts<br/>read/write, normalizeSettings, legacy browser token migration"]:::renderer
    Persist["persistSettingsChange<br/>reason 到 intent 映射,draft 构建"]:::renderer
    SettingsHandlers["useSettingsHandlers<br/>用户 endpoint/token 表单操作"]:::renderer
    ChatProvider["ChatProvider<br/>chat identity 按 tenantId:userId,localStorage 作用域,store 重置"]:::renderer
  end

  subgraph DOCS["词汇与流程"]
    ContextMd["CONTEXT.md<br/>Backend token, Endpoint identity, Chat identity, reasons, intents"]:::doc
    AgentsMd["AGENTS.md<br/>layout, setup, Desktop Chat Smoke Test"]:::doc
  end

  subgraph INTENTS["Settings change reason 到 Endpoint persistence intent"]
    R1["user-edited-settings -> set-explicit (identity 变更则丢弃 token)"]:::intent
    R2["apply-legacy-override -> set-explicit (丢弃 token,重新认证)"]:::intent
    R3["restore-defaults -> restore-default (丢弃 override,与默认一致则保留 token)"]:::intent
    R4["silent-update -> keep-current (仅 token,endpoint 不变)"]:::intent
  end

  MainTs -->|"startAuthCallbackServer 接线 isAllowedOrigin + acceptToken"| AuthServer
  MainTs -->|"以 configPath + runtimeDefault 实例化"| ConfigStore
  MainTs -->|"注入 configStore.read/write 用于 exchange"| ApiClient
  AuthServer -->|"解析 query/body 获取 raw token"| Extract
  AuthServer -->|"acceptToken: persistAuthCallbackToken 交换并持久化"| ApiClient
  AuthServer -->|"acceptToken 异常时分类失败"| Classify
  ApiClient -->|"用 classifyExchangeError 映射失败"| Classify
  ApiClient -->|"通过注入回调读写 config"| ConfigStore

  IpcContract -->|"定义 aida-config:* (read, save, restore-default, clearToken)"| ConfigStore
  IpcContract -->|"定义 aida-auth:* callback push/poll"| AuthServer
  ConfigStore -->|"通过 registerInvoke 实现 IPC 处理器"| IpcContract

  ConfigStore -->|"写入/恢复时应用 token/endpoint resolvers"| ConfigTypes
  ConfigStore -->|"校验 origin/path,计算有效 endpoint"| Endpoint
  ConfigTypes -->|"导入 canonicalEndpointIdentity 做 identity 检查"| Endpoint

  UseAuth -->|"包装 orchestrator 处理 hash/push/poll"| Orchestrator
  Orchestrator -->|"onCallback 分发 backendToken + alreadySaved"| UseAuth
  UseAuth -->|"调用 exchangeBackendToken (经 config.ts) 验证新 token"| ApiClient
  UseAuth -->|"持久化 token,completeLogin 时 silent-update reason"| Persist
  UseAuth -->|"readSettings, clearBackendToken, exchangeBackendToken"| Config
  SettingsHandlers -->|"用户表单操作:reason 驱动写入"| Persist

  Persist -->|"applyPersistedSettings 带派生 EndpointPersistenceIntent"| Config
  Persist -->|"canonicalEndpointIdentity 计算 token 绑定 identity"| Endpoint
  Persist -.->|"reason -> intent"| INTENTS
  Config -->|"resolvePersistedEndpoint, resolvePersistedToken, normalizers"| ConfigTypes
  Config -->|"window.aidaDesktop.config IPC (desktop store adapter)"| ConfigStore

  ChatProvider -->|"读取 aidaBase 计算 chatProviderKey"| Endpoint
  ChatProvider -->|"从 backend token JWT 派生 chat identity (tenantId:userId)"| UseAuth

  ChatProvider -.->|"实现 Chat identity 作用域"| ContextMd
  SettingsHandlers -.->|"实现 reasons: user-edited, restore-defaults, apply-legacy"| ContextMd
  ContextMd -.->|"作为规范词汇引用"| AgentsMd

  U1["未知: TtlRegistry 注入 clock 签名 + token TTL 影响"]:::unknownNode
  U2["未知: 精确 exchange endpoint 路径 (仅见 'api/auth/exchange')"]:::unknownNode
  U3["未知: electron.vite.config.ts 按构建模式计算的 runtime default"]:::unknownNode
  U4["未知: 后端 JWT claim 结构 (tenantId/userId/hashed fallback)"]:::unknownNode
  U5["未知: ConfigStore.write endpointIntent 与 main-ts 传播 (版本化)"]:::unknownNode
  U6["未知: legacy migration 顺序 vs useAuthSession (新 token 竞态)"]:::unknownNode
  U7["未知: window.aidaDesktop.auth 为 null 时 orchestrator 行为"]:::unknownNode

  ApiClient -.-> U1
  ApiClient -.-> U2
  MainTs -.-> U3
  ChatProvider -.-> U4
  ConfigStore -.-> U5
  Config -.-> U6
  Orchestrator -.-> U7
来源: electron/auth/{auth-callback-server,extract-backend-token,aida-api-client,classify-exchange-error}.ts, electron/{main,config/config-store}.ts, shared/{config-types,endpoint,ipc-contract}.ts, src/{config,useAuthSession,config/persistSettingsChange,app/useSettingsHandlers,auth/auth-callback-orchestrator,chat/ChatProvider}.tsx, CONTEXT.md, AGENTS.md

4 · 混合本地执行器与写入治理

混合本地执行器与写入治理

混合本地执行器与写入治理:服务端编排的委托本地工具执行

flowchart TB
  classDef adr fill:#1e293b,stroke:#64748b,color:#e2e8f0,stroke-width:1px;
  classDef svc fill:#0f2942,stroke:#3b82f6,color:#dbeafe,stroke-width:1px;
  classDef gate fill:#3b1f1f,stroke:#f87171,color:#fee2e2,stroke-width:1px;
  classDef readonly fill:#14321f,stroke:#34d399,color:#d1fae5,stroke-width:1px;
  classDef highrisk fill:#3a2a12,stroke:#f59e0b,color:#fde68a,stroke-width:1px;
  classDef stub fill:#2a2a2a,stroke:#71717a,color:#a1a1aa,stroke-dasharray:5 4,stroke-width:1px;
  classDef ui fill:#1f1633,stroke:#a78bfa,color:#ede9fe,stroke-width:1px;
  classDef wire fill:#102a2e,stroke:#22d3ee,color:#cffafe,stroke-width:1px;
  classDef unk fill:#2b2b2b,stroke:#52525b,color:#8b8b8b,stroke-dasharray:3 3,stroke-width:1px;

  subgraph Governance["决策记录 (ADR 与词汇)"]
    direction LR
    CTX["CONTEXT.md<br/>规范词汇"]:::adr
    A12["ADR-0012<br/>Hybrid-First Local Executor"]:::adr
    A13["ADR-0013<br/>Desktop Write Primitive Policy"]:::adr
    A14["ADR-0014<br/>Transient Grant and Document Intake"]:::adr
    A16["ADR-0016<br/>Bundled Script Runner Contract"]:::adr
    A12 -->|"取代高风险流程"| A13
    A12 -->|"取代 doc payload"| A14
    A12 -->|"runBundledScript 修正 no-shell"| A16
    CTX -.->|vocab| A12
    CTX -.->|vocab| A13
    CTX -.->|vocab| A14
  end

  subgraph Server["Server (aida-agent, 跨仓库)"]
    direction TB
    ORCH["Server Orchestrator<br/>单一 agent 循环,委托调用时挂起"]:::wire
    DELC["Delegated Local Tool Call<br/>SSE 流中 domain=desktop"]:::wire
    ORCH -->|"发出 domain=desktop tool_call"| DELC
  end

  subgraph Renderer["Desktop Renderer (React)"]
    direction TB
    SSE["useDesktopSSEChat<br/>检测 domain=desktop,dispatch,提交结果"]:::ui
    RTL["Run Timeline Reducer<br/>纯事件折叠"]:::ui
    MSG["Chat Message Store<br/>applyToMessage 接缝"]:::ui
    WCD["WriteConfirmationDialog<br/>路径编辑、override 警告、10 分钟计时、批量授权"]:::ui
    SSE -->|"applyToMessage + reducer"| RTL
    RTL -->|"拥有事件折叠"| MSG
  end

  subgraph Main["Desktop Electron Main (Hybrid Local Executor)"]
    direction TB
    IPC["IPC Bridge<br/>ipc-contract.ts,aida-* 通道单一来源"]:::wire
    REG["IPC Handler Registration<br/>roots, skills, payload, picker, dispatch, write confirm/cancel"]:::svc
    CONTRACT["Local Executor Wire Contract<br/>primitives enum, arg/result shapes, error codes"]:::wire
    SVC["LocalExecutorService<br/>委托分发器、审批门、task artifacts"]:::svc
    CAT["LocalSkillCatalog<br/>root/skill 配置、consent、session payload builder"]:::svc
    MAN["Skill Manifest<br/>SKILL.md 解析器、primitive 提取"]:::svc
    NARROW["Capability Narrowing<br/>root allowlist 与 skill 声明求交"]:::svc
    PATH["Path Policy<br/>resolveAllowedWritePath, symlink follow-protection"]:::svc
    GATE["LocalExecutorApprovalGate<br/>pending/executing/completed, 10 分钟过期, 一次性"]:::gate
    GRANT["TransientGrantStore<br/>snapshot 绑定 size+mtime+sha256, session 作用域"]:::gate
    TTL["TtlRegistry<br/>UUID mint, expiresAt, lazy expiry, bounded trim"]:::svc
    INTAKE["DocumentIntakeService<br/>两步 consent、intake 状态机、grant minting"]:::svc
    BROOT["Bundled Skill Root<br/>保留 aida.builtin, 只读 + runBundledScript"]:::svc
    SINT["Script Integrity Verifier<br/>manifest hash/size, 无额外 .py/.pth, safe paths"]:::gate

    subgraph ReadP["只读 Primitives (直接执行)"]
      direction LR
      P_LIST["listDirectory"]:::readonly
      P_READ["readFile"]:::readonly
      P_RSKILL["readSkillFile"]:::readonly
    end

    subgraph HighP["高风险 Primitives (需审批)"]
      direction LR
      P_WRITE["writeFile<br/>approval gate + path policy"]:::highrisk
      P_RUN["runBundledScript<br/>每 run 审批, 仅 aida.builtin"]:::highrisk
    end

    subgraph StubP["Stub Primitives — 不可执行 (返回 cancelled)"]
      direction LR
      P_SHELL["shell — NOT executable"]:::stub
      P_MCP["mcp — NOT executable"]:::stub
      P_NET["network — NOT executable"]:::stub
      P_SEC["secret — NOT executable"]:::stub
    end

    subgraph Sources["Wire 来源与目标 (wire 上无绝对路径)"]
      direction LR
      SELF["Selected File Source<br/>任务作用域原生打开对话框, fileId refs"]:::wire
      GOVR["Grant Overwrite Target<br/>临时 grant 作为写入来源"]:::wire
      SAVE["Save Dialog Target<br/>原生保存对话框, 按 runId+sessionId+conversationId 批量 grant"]:::wire
    end

    IPC -->|"通道唯一定义"| REG
    REG -->|"桥接所有通道"| SVC
    REG -->|"confirm-write / cancel-write"| GATE
    CONTRACT -->|"定义 event + shapes"| SVC
    CONTRACT -->|"定义 wire shapes"| REG

    SVC -->|"读取 config snapshot 接缝"| CAT
    SVC -->|"高风险经 requireApproval"| GATE
    SVC -->|"mint + verify grants"| GRANT
    SVC -->|"直接执行只读"| ReadP
    SVC -->|"包装高风险"| HighP
    SVC -->|"查询 resolveAllowedWritePath"| PATH

    CAT -->|"readLocalSkillManifest"| MAN
    CAT -->|"narrowCapabilities"| NARROW
    CAT -->|"创建虚拟 bundled root"| BROOT

    GATE -->|"pending write TTL + state"| TTL
    GATE -->|"push pending-write IPC"| WCD
    GRANT -->|"grant TTL, lazy expiry, trim"| TTL
    INTAKE -->|"intake 阶段 TTL + 生命周期"| TTL
    INTAKE -->|"consent 步骤 1 后 mint grants"| GRANT
    INTAKE -. extraction supervisor 未知 .-> EXTRACT

    P_WRITE -->|"经 PATH 校验"| PATH
    P_RUN -->|"仅 aida.builtin 可声明"| BROOT
    P_RUN -->|"spawn 前 verifyBundledScriptTree"| SINT

    SELF -->|"readFile source=selected_file"| SVC
    GOVR -->|"writeFile target=grant_overwrite"| SVC
    SAVE -->|"pick-save-path 原生对话框, 相对 root"| REG
  end

  subgraph Unknowns["未确认 / 未按已发布绘制"]
    direction TB
    EXTRACT["ExtractionSupervisor (UNCONFIRMED)<br/>spawn 路径 + 隔离进程 IPC 未知"]:::unk
    PYRT["Python Runtime (UNCONFIRMED)<br/>spawn args, env scrubbing, limits, 启动时存在?"]:::unk
    JAIL["Session Workspace Jail (UNCONFIRMED)<br/>路径方案 + 实现未知"]:::unk
    SBOX["Sandbox Profile (UNCONFIRMED)<br/>macOS Seatbelt 路径 / Windows v1 tier"]:::unk
    P_RUN -. 调用 runtime,形状未知 .-> PYRT
    P_RUN -. sandbox profile 未知 .-> SBOX
    PATH -. workspace jail 未知 .-> JAIL
  end

  A13 -->|"§2 approval gate 状态机"| GATE
  A13 -->|"§1 writeFile path policy + error codes"| P_WRITE
  A13 -->|"§1 resolveAllowedWritePath 比 read 更严"| PATH
  A14 -->|"§3 grant snapshot identity"| GRANT
  A14 -->|"§3 两步 consent 状态机"| INTAKE
  A16 -->|"§1 保留 aida.builtin 策略"| BROOT
  A16 -->|"§3 spawn 前 verify manifest"| SINT

  DELC -->|"在 originating stream 上触发 dispatch 周期"| SSE
  SSE -->|"dispatchToolCall IPC"| IPC
  WCD -->|"confirmWrite IPC 解析 pending write"| GATE
依据 docs/adr/0012-0016, apps/desktop/electron/local-executor/* (service, approval-gate, transient-grant-store, path-policy, primitives), shared/local-executor-contract.ts (shell/mcp/network/secret stubbed at local-executor-service.ts:373-378), document-intake/, bundled-skills/, script-runner/, src/chat/useDesktopSSEChat.ts, and CONTEXT.md.

5 · 待决策状态机

待决策状态机

待决策状态机:TtlRegistry 支撑的用户决策与文件操作生命周期

TtlRegistry — 共享内存存储
stateDiagram-v2
  direction LR
  state "TtlRegistry — 共享内存存储" as REG {
    [*] --> minted: mint(build, opts) UUID + expiresAt
    minted --> active: scheduleExpiry (active timer)
    minted --> lazy: lazy expiry (checked on get)
    active --> settled: patch / delete
    lazy --> settled: isExpired on access
    settled --> trimmed: trim(isSettled, cap=200)
    trimmed --> [*]
  }
  note right of REG
    Adapters: clock injected (now, default Date.now)
    Write approval TTL 10m / Grants 30m / Intake 30m
  end note
LocalExecutorApprovalGate — 写入审批
stateDiagram-v2
  direction LR
  [*] --> pending: requireApproval mints entry + scheduleExpiry
  pending --> executing: confirmWrite (clearExpiry)
  pending --> cancelled: cancelWrite (WRITE_USER_CANCELLED)
  pending --> expired: TTL timer (WRITE_EXPIRED, 10m)
  executing --> completed: execute resolves ok or error
  completed --> [*]
  cancelled --> [*]
  expired --> [*]
  note right of pending
    parked resolve promise per entry
    batchGrants Map skips dialog (not minted)
  end note
TransientGrantStore — 文件 Grant(lazy expiry)
stateDiagram-v2
  direction LR
  [*] --> active: mint grant (grantId, snapshot, expiresAt 30m)
  active --> active: verifyForRead / verifyForWrite (snapshot re-check)
  active --> revoked: revoke or bulk session revoke
  active --> expired_lazy: TTL elapsed (checked on verify)
  revoked --> [*]
  expired_lazy --> [*]
  note right of active
    verify checks sessionId match + not expired
    + not revoked + snapshot matches
    Errors: GRANT_EXPIRED, GRANT_NOT_FOUND,
    GRANT_STALE_SNAPSHOT, GRANT_WRITE_NOT_ALLOWED
  end note
DocumentIntakeService — 7 阶段 Intake
stateDiagram-v2
  direction LR
  [*] --> metadata_review: beginFromAbsolutePath
  metadata_review --> extracting: confirmMetadata (mint grant + enqueue)
  extracting --> preview_review: extraction_success
  extracting --> error: extraction_failed (revoke grant)
  preview_review --> ready: confirmSend (verify grant for read)
  preview_review --> metadata_review: resetForReconfirmation (stale snapshot, revoke grant)
  ready --> sent: markSent
  metadata_review --> cancelled: cancel (revoke grant)
  preview_review --> cancelled: cancel (revoke grant)
  metadata_review --> error: TTL expire (30m)
  preview_review --> error: TTL expire (30m)
  sent --> [*]
  cancelled --> [*]
  error --> [*]
  note right of extracting
    grant held during extracting + preview_review
    outcomes: extraction_success/failed,
    stale_reconfirm, sent, cancelled, expired
  end note
ExtractionSupervisor — 任务生命周期
stateDiagram-v2
  direction LR
  [*] --> queued: enqueue (resolve promise parked)
  queued --> running: pump dequeues + spawns child
  running --> running: heartbeat watcher polls lastHeartbeatAt
  running --> killed: stall or memory overage (kill child)
  running --> exited: child result or exit event
  killed --> settled: finishActive (clear timers, resolve)
  exited --> settled: finishActive (clear timers, resolve)
  settled --> [*]: pump next
  note right of running
    wallTimeoutMs guard + heartbeat watcher armed
    in-process fallback path (extractDocumentText)
  end note
SpeechHelper — macOS 子进程
stateDiagram-v2
  direction LR
  [*] --> idle
  idle --> starting: start (spawn helper)
  starting --> listening: state=recording event (clear startWatchdog)
  starting --> failed: spawn error / start timeout 5s SIGKILL
  listening --> stopping: stop (SIGTERM)
  stopping --> idle: child exit (or stopGrace 2s SIGKILL)
  listening --> failed: helper exit / JSON parse error
  failed --> idle: failWith auto-reset (emit error + state=stopped)
  idle --> [*]
  note right of failed
    terminal then settles to idle
    stdout line-by-line JSON: state / result / heartbeat
  end note
Update Lifecycle Reducer
stateDiagram-v2
  direction LR
  [*] --> idle
  idle --> checking: check:start
  checking --> available: check:available (targetVersion)
  checking --> idle: check:no-update / check:error
  available --> downloading: download:start (progress 0)
  downloading --> downloading: download:progress (percent clamped 0-100)
  downloading --> downloaded: download:done (progress 100)
  downloading --> available: download:error (revert)
  downloaded --> applying: apply:start
  applying --> downloaded: apply:error (revert)
  idle --> unsupported: unsupported (no Electron API)
  note right of downloading
    single observable state: phase, targetVersion,
    progress, statusText (UpdateApi IPC injected)
  end note
来源: electron/ttl-registry.ts; local-executor/approval-gate.ts:106-294 + transient-grant-store.ts:69-211; document-intake/intake-service.ts:182-441 + extraction-supervisor.ts:48-243; speech/speech-helper.ts:102-324; src/update/useUpdateLifecycle.ts:51-243; CONTEXT.md:400-662

6 · 渲染器组合(应用壳层)

渲染器组合(应用壳层)

渲染器组合:应用壳层 Bootstrap、Hooks 与 Surfaces

flowchart TB
  App["App.tsx<br/>(Shell Layout Root)"]

  subgraph HUB["编排中枢"]
    Boot["useAppBootstrap<br/>(Init Sequence and Bootstrap Seam)"]
  end

  subgraph HOOKS["组合的 Hooks 与纯模块"]
    Auth["useAuthSession<br/>(Auth Lifecycle and Token Management)"]
    Cap["useCapabilityFetch<br/>(Raw Agent and Skill Fetch)"]
    Gating["workbenchGating<br/>(Pure Gating Functions)"]
    WsSel["useWorkspaceSelection<br/>(Workspace Key and Category State)"]
    Update["useUpdateLifecycle<br/>(Update FSM and Notifications)"]
    Nav["useShellNavigation<br/>(Navigation State and Handlers)"]
    SetH["useSettingsHandlers<br/>(Settings Modal State and Handlers)"]
    WbExp["workbenchExperience.tsx<br/>(Workspace and Task Catalog)"]
    HrExp["hrExperience.tsx<br/>(HR-Specific Task Metadata)"]
  end

  subgraph STORES["聊天逻辑层 (Stores and Transport)"]
    Provider["ChatProvider<br/>(Chat Identity and Store Scope)"]
    SessStore["useChatSessionStore<br/>(Session CRUD and Agent Catalog)"]
    MsgStore["useChatMessageStore<br/>(Message Timeline and Streaming)"]
    Comp["Chat Store Composition Seam<br/>(./store/chat.ts)"]
    Snap["ChatWindowSnapshot<br/>(Snapshot Adapter)"]
    SSE["useDesktopSSEChat<br/>(SSE Transport and Reducer)"]
    RunTl["Run Timeline Reducer<br/>(Assistant Message State)"]
    RtAdapter["AidaAssistantRuntimeAdapter<br/>(assistant-ui Bridge)"]
  end

  subgraph SURFACES["展示面 (Leaves)"]
    Sidebar["AppSidebar<br/>(Brand, Search, Nav, Workspaces, Conversations, Account)"]
    Home["WorkbenchHome<br/>(HR Workbench Home - Greeting, Tasks, Automation)"]
    CPanel["ChatPanel<br/>(Chat Drawer Container)"]
    CSurface["ChatSurface<br/>(Chat UI - Header, Messages, Composer, Artifacts)"]
    CParts["ChatSurfaceParts<br/>(Pure Presenter - Header, Thread, Composer)"]
    ConvSide["ConversationSidebar<br/>(Chat Window Conversation List)"]
    Login["LoginModal<br/>(Auth Overlay)"]
    Settings["SettingsModal<br/>(Settings Overlay with Tab Modules)"]
    Gallery["SkillGalleryView<br/>(Skill Gallery View)"]
    Sched["ScheduledTasksView<br/>(Automation View)"]
  end

  App -->|"组合以顺序 boot"| Boot

  Boot -->|"Step 1: settings plus legacy migration"| Auth
  Boot -->|"Step 2a: raw agents plus skills"| Cap
  Boot -->|"Step 2b/c: derive gating snapshot"| Gating
  Boot -->|"Step 3: workspace plus category state"| WsSel
  Boot -->|"Step 4: update FSM"| Update
  Boot -->|"delegates navigation"| Nav
  Boot -->|"delegates settings modal"| SetH

  Gating -->|"composes agents plus skills"| Cap
  Gating -->|"reads agentWorkspaces"| WbExp
  WsSel -->|"resolves active workspace"| WbExp
  WbExp -.->|"sources HR task metadata"| HrExp

  Nav -->|"creates/manages sessions"| SessStore
  SetH -->|"syncSettings callback"| Auth

  App -->|"renders with state"| Sidebar
  App -->|"mainView=workspace"| Home
  App -->|"chatVisible=true"| CPanel
  App -->|"loginOpen=true"| Login
  App -->|"settingsOpen=true"| Settings
  App -->|"mainView=capabilities"| Gallery
  App -->|"mainView=scheduled"| Sched
  App -->|"wraps with providerKey"| Provider

  Sidebar -->|"navigation actions"| Nav
  Sidebar -->|"renders conversation list"| ConvSide
  Home -->|"onTaskPrompt routes to chat"| Nav
  Home -->|"reads workspace catalog"| WbExp
  Login -->|"calls login"| Auth
  Settings -->|"reads state plus handlers"| SetH

  CPanel -->|"renders with pendingPrompt"| CSurface
  CSurface -->|"composes header plus thread plus composer"| CParts
  CSurface -->|"reads activeSessionId"| SessStore
  CSurface -->|"reads messages, streaming"| MsgStore
  CSurface -->|"renders conversation history"| ConvSide
  CSurface -->|"wrapped by assistant-ui adapter"| RtAdapter

  Provider -->|"identity scoping"| SessStore

  SSE -->|"writes via applyToMessage"| RunTl
  RunTl -->|"pure updater contract"| MsgStore
  SessStore -->|"observes for invariants"| Comp
  MsgStore -->|"joined by composition seam"| Comp
  Snap -->|"composes session"| SessStore
  Snap -->|"composes messages"| MsgStore
依据 apps/desktop/src/App.tsx, src/app/{useAppBootstrap,useShellNavigation,useSettingsHandlers}.ts, src/{useAuthSession,workspace/*,hooks/useCapabilityFetch,chat/*,chat/core/store/*}.ts(x), and CONTEXT.md lines 32, 351, 359, 414, 541, 561, 576, 750.

决策矩阵

将各架构决策记录(ADR-0012 至 ADR-0016)映射到其治理的视角与主要实现代码路径。

ADR 决策 视角 主要代码路径
ADR-0012 Hybrid-First Local Executor — 服务端编排器挂起并将 domain=desktop 工具调用委托给桌面端;取代较早的高风险与 doc-payload 流程。 混合本地执行器与写入治理;聊天与 SSE 数据流 apps/desktop/electron/local-executor/local-executor-service.ts; apps/desktop/src/chat/useDesktopSSEChat.ts
ADR-0013 Desktop Write Primitive Policy — §1 writeFile 路径策略 + 错误码(resolveAllowedWritePath 比 read 更严);§2 approval-gate 状态机(pending/executing/completed,10 分钟过期,一次性)。 混合本地执行器与写入治理;待决策状态机 apps/desktop/electron/local-executor/approval-gate.ts:106-294; apps/desktop/electron/local-executor/path-policy.ts
ADR-0014 Transient Grant & Document Intake — §3 grant snapshot identity(size+mtime+sha256,session 作用域)与两步 consent intake 状态机。 混合本地执行器与写入治理;待决策状态机 apps/desktop/electron/local-executor/transient-grant-store.ts:69-211; apps/desktop/electron/document-intake/intake-service.ts:182-441
ADR-0015 所供图示证据中未明确承载;执行器/治理链引用 ADR-0012、0013、0014 与 0016。视为源材料缺口而非断言决策。 未知 (未确认 — 见风险与未知项 / 下一步) docs/adr/0015-*.md(0012–0016 系列中存在该文件;图示证据未呈现内容)
ADR-0016 Bundled Script Runner Contract — §1 保留 aida.builtin 只读根 + runBundledScript(每 run 审批);§3 spawn 前 verify manifest hash/size;修正 no-shell 立场。 混合本地执行器与写入治理 apps/desktop/electron/bundled-skills/; apps/desktop/electron/script-runner/(script integrity verifier)

风险与未知项

以下各项均直接来自各图 unknowns 数组。无一被断言为事实。若未验证边界可能导致错误或不安全行为,标记为 风险;纯信息缺口标记为 未知

进程拓扑与 IPC
聊天与 SSE 数据流
认证与端点身份
混合本地执行器与写入治理
待决策状态机
渲染器组合(应用壳层)

下一步

  1. 优先闭合进程隔离缺口。 最高杠杆风险均涉及不受信任工作执行位置:extraction supervisor、bundled-script Python runtime、speech 子进程。在 electron/document-intake/extraction-supervisor.tselectron/script-runner/electron/speech/speech-helper.ts 中确认各 spawn 路径、沙箱 profile(macOS Seatbelt / Windows v1 tier)与 env scrubbing。
  2. 钉住 ADR-0015。 决策矩阵存在缺口:ADR-0015 在系列中但图示证据缺失。阅读 docs/adr/0015-*.md 并填入矩阵,使 0012–0016 链完整。
  3. 验证 dispatch 确认模型。 厘清 dispatchToolCall 是 fire-and-forget 还是 await-on-reply,是否重试——这决定委托本地工具结果如何回写到 run timeline。
  4. 记录 IPC 接线接缝。 Approval gate、document intake 与三个 TtlRegistry clock 适配器均在 electron/main.ts 组合但未端到端追踪;捕获这些可闭合最大簇纯信息未知项。
  5. 渲染验证本包。 在浏览器中打开(Mermaid 从 CDN 加载,需网络)确认全部八张图渲染,再将新确认事实回填到受影响图。