源码有据架构简报 · 6 个视角 · aida-desktop (Electron + Vite + React)
应用是单一 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)划分作用域。下方六张图从进程拓扑一直下探到各状态机。
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
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
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
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
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
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
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
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
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
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
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
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
将各架构决策记录(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 数组。无一被断言为事实。若未验证边界可能导致错误或不安全行为,标记为 风险;纯信息缺口标记为 未知。