# 架构说明 本文档记录 mihari 的运行时架构与关键机制。对外行为见 [README](../README.zh-CN.md),完整命令见 [commands.md](commands.md)。 ## 控制面 出口网卡选择由 Manager 持久化为可选 `egress-interface`,通过认证本地 GET/PATCH `/v1/egress` 供 CLI/TUI 共用。平台枚举只提供网卡快照;精确名称是选择身份,不使用持久网卡库或自动回退。生成器在克隆的有效订阅上覆盖原生接口绑定,No-Override 不注入覆盖。在线修改遵循候选校验、重载及读回、关闭活动连接、保存设置的事务顺序;恢复失败使用现有 degraded 隔离。停止时只保存,启动生成器应用。能力不暴露至浏览器,也不改变 TUN 启用的冲突确认。参见 [ADR 0006](adr/0006-native-egress-binding.md) 与[命令语义](commands.md#egress--出口网卡)。 Mihari 围绕一个由守护进程持有的控制面(control plane)设计,由 CLI、TUI 和浏览器面板共享: - CLI、TUI 和浏览器面板通过本地命名管道 / Unix 域套接字连接同一守护进程控制面。 - 控制 API 从不绑定 TCP 端口。 - 控制面经过认证:Unix 系统令牌位于 B/control.token,Windows/显式私有 P 保留单根 control.token。 - 守护进程可以安装、校验、托管、查询并重启 mihomo,同时将 Mihari 托管的 TCP 控制器保持在 loopback。 - 守护进程还负责订阅持久化、有界的自动刷新、校验过的配置生成、重载回滚与离线配置切换。 - 控制面新增只读端点 `GET /v1/service/status`,返回 mihari 自身的 OS 服务注册状态(`running`/`stopped`/`not_installed`/`unknown`);`GET /v1/core` 增加可选 `localReady`/`localVersion` 字段反映本地 core 就绪。两者均为向后兼容增量,不改变现有协议字段、onboarding `Complete` 契约或持久化格式。 - `/v1` 的 `CoreStatus`、`CoreInstallResult` 增加可选 `channel`;`MutationRequest` 增加可选 `channel` 以显式指定本次安装通道。均为向后兼容增量。 - 统一日志配置:System → Logging → Level 同时控制 Mihari 文件日志与 mihomo 全局 `log-level`,生成配置覆盖订阅值但不修改原缓存。主动可选 `debug`、`info`、`warn`、`error`。在线修改经过校验、内核确认、reload 与保存,失败补偿恢复;内核停止时保存到下次启动应用。外部内核变化每 2 秒观察:保存失败保留内核现状、显示未保存并重试最新值。仅被动采纳内核的 `silent`,保留历史文件和操作错误提示,用户可切回四档。旧版本可能拒绝已保存的 `silent`,降级前应切回支持级别或恢复兼容的停机备份。TUI/Web 实时日志筛选独立于文件级别,silent 也不限制实时订阅。网关允许单字段 `PATCH /configs {"log-level":"debug"}`,混合及未知写入仍拒绝。 - LoggingStatus 的 `level` 保留为已保存的文件级别,可选 `core_level`、`sync_state`、`sync_message` 分别表示确认的实际值、applied/pending/unsaved/unknown 与安全提示;不放入原始错误。状态变化推进 revision,不单独推进配置 generation。受控写和外部采纳分别补偿与重试。外部采纳只保存 settings 并应用文件 logger,派生 runtime 在正常生成或每次子进程启动前更新;启动准备持有提交所有权直到 Start 返回,周期健康检查不覆盖外部日志修改。 - `GET /v1/logging` 与 `PATCH /v1/logging` 是稳定的 v1 本地控制协议:前者返回完整 Logging 状态,后者在 revision 预检后更新级别、单文件大小或保留数量。它们供 TUI 使用,不增加 CLI 命令。 - Windows 私有日志的授权主体优先为数据根的个人 owner SID。owner 为 Administrators 时,从既有 DACL 的显式个人用户 full-control ACE 解析主体,再写入个人用户与 LocalSystem 的受保护 DACL,避免未提权用户失去读取权限;多个用户、deny ACE 或无法解析的授权主体均拒绝猜测。旧 ACL 已丢失个人授权时,仅成功以 WRITE_DAC 打开数据根的交互进程可补回自身 SID,LocalSystem 不猜测桌面用户。日志写入器持有自身序列锁后,修复三个固定日志序列的当前文件、归档和锁文件,通过 no-follow handle 核对文件 identity,不改内容;其他序列轮转导致 identity 变化时有界重试。SYSTEM 尚不能确定用户时仅保留受保护的旧 BA/SYSTEM 根权限,不回写根目录。子项创建及加固重新读取根策略,并在应用后复查,避免服务缓存或并发迁移覆盖个人授权。 - daemon 装配失败但控制通道可 listen 时驻留降级控制面,`GET /v1/status` 的 `health` 为 `degraded`,并带可省略 `last_error`。 - OS 服务 `Start` 等待控制通道 Ready;listen 失败则向 SCM 返回错误,不得保持假 running。 - 托管端口预检失败时 details 可含占用 PID 与进程基名;不自动杀进程。 ## Settings 提交与降级 - Settings 的单文件 replace 是提交点:replace 前失败时磁盘仍为旧文件;replace 成功后的目录 sync 失败仅作为已提交后的 durability warning,上报诊断但不回滚、不把已生效的 mutation 报为失败。 - onboarding、系统代理或 TUN 等需要补偿的 mutation,若补偿写在提交点前失败,daemon 会按已经提交的磁盘状态收敛内存、推进 revision,并将 health 标为 `degraded`。只读请求仍可用;后续 mutation 返回 `invalid_state`,必须重启后重新加载并重试。 - 该 degraded 边界不新增事务文件或持久化 schema。旧版二进制以 `KnownFields(true)` 严格解码 `mihari.yaml`,不能读取非默认 `log:` 块;降级前须在 System → Logging 恢复 `info` / 10 MiB / 3 份文件以自动移除该块,或备份后手动删除 `log:`。 ## 诊断错误链 当前本地错误汇报策略由 [Issue #197 统一错误汇报设计](superpowers/specs/2026-09-16-unified-error-reporting-design.md)规定:日志与本地 CLI/TUI 错误均不脱敏,执行 owner 使用共享 formatter 采集原始原因并分配发生记录 ID;响应、warning 和历史查询复用同一快照。下文历史 Phase 记录中的“安全 envelope”“原文仅限文件日志”和 FailureReporter 脱敏限制不再适用。普通业务 DTO 字段、浏览器和网络认证边界保持。 本地 `/v1/diagnostics` 分页返回元数据,单条查询返回有界原文;daemon 256 条/32 MiB、TUI 本地 128 条/16 MiB。F2 合并窗口最多 384 条/16 MiB,并保留一条正在阅读的快照;历史无持久化、无内容去重或恢复关联。文件级别过滤不影响内存历史,缓存重放只复用既有 ID。CLI/TUI 共用可选 `diagnostic` 和 `warnings`,旧端、淘汰、重启及读取失败显式降级,绝不重放 mutation。业务正文保持 4 MiB;诊断超预算时使用只读引用,连引用也放不下时采用有界元数据响应头。stream 仅在能力协商后发送小型 terminal reference,然后按既有状态关闭,不发送原始正文。 本节后续 Phase 表格保留先前诊断建设的历史审计记录。其中“脱敏摘要”“取消静默”“预期拒绝 DEBUG”等旧策略由[完整错误日志设计](superpowers/specs/2026-09-14-full-error-logging-design.md)替代,实施与验证证据见[模块登记](superpowers/plans/2026-09-14-full-error-logging-audit.md)。 - 文件日志、快照、导出和本地 CLI/TUI 错误详情保留原始 cause、包装上下文、合并原因及已有堆栈,不脱敏。原文经授权本地诊断接口传输;独立终端 FailureReporter 同样保留有界原文,不依赖故障 logger。 - 仅使用已建立且可用的文件 reporter;普通 CLI 不新建或探测日志。预期拒绝与主动取消 INFO、重试与恢复 warning WARN、最终失败 ERROR,遵循配置过滤;实际执行 owner 去重,缓存重放不再报告。 - 诊断文本、HTTP 失败正文与核心逻辑行各限 256 KiB。保留错误图预算及截断标记;最坏 JSON 转义采用 UTF-8 分片,适配既有 1 MiB record / 2 MiB frame。快照校验、摘要、来源范围、权限与业务提交语义保持。 - Issue #204 的专用内部 HTTP cause 保留真实状态、失败阶段、URL、失败正文与原始报错。成功响应正文不转储。导出前与完成页面红色说明日志未经脱敏,不增加确认步骤或额外配置文件。 - provider 读取由 Manager 编排,最多三次,单次最多 1 秒、该读取总计最多 4 秒,退避可取消;普通节点查询不增加这项预算。原始全局节点映射用于测速路由,合并后的目录仅供展示。普通节点优先,否则按 provider 名排序选择首个候选;Compatible 投影不重复计数。 - `/v1/proxies` 成功响应增加可选 `duplicate_names`;失败不发布残缺目录。TUI 的启动首次成功完整检查消费一次同名提示机会;错误快照保留旧数据并标记过期,恢复后清除加载错误。原始原因、重试进度和来源身份不加入公开 DTO。 - Phase 1 的 operation metadata 已用于 Phase 2 的 Logging 更新链路和 Phase 3 的主要业务 mutation。CLI/TUI 生成 ID,既有 `/v1` mutation DTO 携带 `operation_id`,本地控制客户端、控制服务器和 daemon 在各自的诊断 ctx 中绑定同一 ID 与静态 operation 名;没有增加 header 或持久化状态。一个实际 mutation 执行使用一个 ID;订阅 Add 后的立即拉取是独立子操作,批量 provider 更新的每个子操作也保留各自既有 ID。 - settings 保存失败保留稳定的公开 `data_failure` / `persist settings` 分类,同时在内部错误链保留 cause,供 `errors.Is`/`errors.As` 与 daemon 的受控诊断使用。诊断 logger 输出有界、脱敏的类型化摘要,不能把路径、凭据、完整 URL 或配置原文带入公开响应、状态或事件。 - 每次实际 mutation 执行是详细失败诊断的唯一 owner;同一 key 的缓存重放和并发等待者不会重复记录。控制服务器只为未被 owner 标记的意外失败补一条记录。对这条 settings 链路,已提交后的目录同步 warning 保持成功、revision 与内存发布,并在业务锁释放后以实际原因记录 WARN。 - 控制客户端为已接入的 mutation 记录 DEBUG 的开始、成功或已解析 daemon 响应;本地传输和响应解码失败按统一失败策略记录。daemon 的 `runtime.doOperation` 在实际执行完成后记录一次最终失败:含真实 cause 的失败为 ERROR,revision、参数、系统代理/TUN 冲突等预期拒绝为 DEBUG,只有纯 ctx 取消时不制造失败记录;订阅和核心的实际成功执行另记 INFO。未知 `errorString` 只输出保守类型摘要,不复制其不可信文本。 Phase 3 已完成下表中的请求链和自主 supervisor 边界;这里的“完成”只表示表内真实入口及已列故障点已有 cause、关联和 owner 回归,并不表示全仓所有错误都已覆盖。 | 已覆盖入口 | operation ID 来源与关联 | cause 产生点 | 最终 owner 与级别 | 保持的状态不变量 | 主要回归测试 | | --- | --- | --- | --- | --- | --- | | 订阅 Add/Refresh/Use/Enabled/Set/Remove | CLI/TUI 生成并经既有 DTO 传递;静态名为 `subscription.*`;Add 的立即拉取使用独立 `add-fetch` 子 ID | `subscription.Downloader` 的传输转换,以及 `Service` 的 304/cache 读取和原子写入转换 | `runtime.doOperation`;真实失败 ERROR、revision 冲突 DEBUG、实际成功 INFO;客户端阶段 DEBUG | fallback 次序、失败 Add 后的注册结果、cache/catalog、revision、`LastError` 与 stale commit 拒绝不变;重放不再次下载或记录 | `TestSubscriptionDiagnostic_FailedReplayAndNewIDKeepCausesIsolated`、`TestSubscriptionDiagnostic_AddFailedChildKeepsSafeLastErrorAndCause`、`TestSubscriptionMutationRoutes_BindOperationMetadata` | | 核心 Install/Update/Restart | CLI、System/Setup TUI 的既有 DTO ID;`core.install` / `core.restart` | `core.Installer` 的下载、保存、校验、AIO 提示与二进制替换转换;runtime 的 Prepare/Commit/Restart/Maintain 返回链 | 显式请求由 `runtime.doOperation` 最终记录,真实失败 ERROR、成功 INFO;客户端阶段 DEBUG | channel、candidate identity、安装 revision、cleanup、trusted maintenance 路由和公开 API 分类不变 | `TestCoreDiagnostic_InstallFailureMatrixKeepsCauseAndSingleOwner`、`TestCoreDiagnostic_FailureJSONKeepsSafeCauseAndExecutionOperation`、`TestCoreMutationsBindDiagnosticOperationContext` | | 核心自主监督 | 无请求 ID;`Supervisor.Run` 清除继承的 operation metadata | 自主启动、进程异常退出、第三次健康检查以及终止失败 | supervisor 生命周期 owner;真实失败 ERROR,纯健康检查取消不记录 | 显式 Restart/Maintain 错误仍返回 runtime owner;backoff、停止与进程句柄回收顺序不变;`CoreStatus.last_error` 仍是静态安全摘要 | `TestSupervisorDiagnostic_AutonomousStartFailureIsSafeAndUncorrelated`、`TestSupervisorDiagnostic_ThirdHealthCancellationDoesNotEmitFailure`、`TestSupervisorDiagnostic_ExplicitRestartFailureReturnsToOwnerWithoutReport` | | 订阅配置生成、校验、发布、reload 与补偿 | 沿触发它的 `subscription.*` 执行 ctx;普通与 trusted 恢复分别保留原有 ctx 选择 | `prepareContent` 文件 IO、`ValidateConfig`,普通 `commitRuntimeConfig` 的首次 reload/restore/第二次 reload,trusted 提交的 capability/reload/restore,以及 `TrustedExecution.Publish` | `runtime.doOperation` 聚合并最终记录一次 ERROR;已提交的 settings durability warning 仍由同一批次在锁外记 WARN | 候选校验、原子替换、普通与 trusted 恢复顺序、generation/hash、cache/catalog/receipt、revision、degraded 状态和静态 `LastError` 不变 | `TestConfigDiagnostic_ReloadCompensation`、`TestConfigDiagnostic_CatalogRestoreKeepsBothCauses`、`TestConfigDiagnostic_TrustedPublicationKeepsRecoveryCauses` | | 系统代理 Enable/Disable | CLI/System TUI 的既有 DTO ID;`system_proxy.enable` / `system_proxy.disable` | backend Get/Enable/Disable/readback 与 settings/live 补偿 | `runtime.doOperation`;真实 backend/补偿失败 ERROR,外部代理冲突和非 Mihari 所有的 Disable 为 DEBUG;客户端阶段 DEBUG | force、所有权门控、settings/live 恢复顺序、revision、degraded 与 `LastError` 不变 | `TestSystemProxyDiagnostic_BackendFailuresKeepCauseAndReplayJSON`、`TestSystemProxyDiagnostic_ForeignConflictIsDebug`、`TestSystemProxyDiagnostic_IPCOwnerDedup` | | 普通与 trusted TUN Enable/Disable | CLI/System TUI 的既有 DTO ID;`tun.enable` / `tun.disable` | live snapshot/Configs、PATCH/permission/fallback 转换、应用确认和 settings/live/trusted 补偿 | `runtime.doOperation`;真实失败 ERROR、TUN 冲突 DEBUG;客户端阶段 DEBUG | detector 仍是 best effort;force、live 确认、配置字节、settings-first trusted 恢复、revision/degraded 和原 marker 不变 | `TestTunDiagnostic_RuntimeFailureMatrixJSON`、`TestTunDiagnostic_TrustedConfirmationAndRollbackJSON`、`TestTunDiagnostic_IPCOwnerDedup` | | GeoIP Update | Setup TUI 的既有 DTO ID;`geoip.update`;没有新增 CLI mutation | `FileCandidate.Commit` 激活恢复,以及 `PreparedUpdate.Commit` 的 pair reopen/restore;runtime 原样接收 prepare/commit cause | `runtime.doOperation`;真实失败 ERROR、stale revision 冲突 DEBUG;客户端阶段 DEBUG | 主错误文本、pair 原子切换/恢复次序、generation、stale 拒绝、失败 candidate cleanup 和公开 `internal` envelope 不变 | `TestGeoIPDiagnostic_PairOpenFailureKeepsBothRestoreCauses`、`TestGeoIPDiagnostic_FileActivationFailureKeepsRestoreAttempt`、`TestGeoIPDiagnostic_IPCRawFailureKeepsInternalEnvelope` | | Panel Install/Update/Activate/Rollback/Uninstall/Reinstall | CLI/Web GUI TUI 的既有 DTO ID;静态名为 `panel.*`;浏览器 Open 不在此链 | `panel.Service.download` 的请求创建、传输、响应读取/关闭转换;runtime 原样接收 prepare/commit cause | `runtime.doOperation` 的实际执行 owner 记录 ERROR;客户端阶段 DEBUG | identity/revision、staging cleanup、active/previous build 和公开 API 分类不变 | `TestPanelDiagnostic_OperationsKeepOwnerCauseAndReplayJSON`、`TestPanelDiagnostic_IPCOwnerDedup`、`TestPanelDiagnostic_TUIMetadata` | | 原生 rule provider Refresh(单个/批量) | Rules TUI 每个既有 mutation ID;`rule_provider.refresh`,批量子项不共用结果元数据 | `mihomo.Client.do` 在该实际调用链可达的请求创建、传输和响应读取转换 | `runtime.doOperation` 的实际执行 owner 记录 ERROR;客户端阶段 DEBUG | 原生 PUT method/escaped path、批量子项身份、HTTP status code/message/details 和响应关闭不变 | `TestProviderDiagnostic_AdapterKeepsCause`、`TestProviderDiagnostic_RealAdapterOwnerJSONAndReplay`、`TestProviderDiagnostic_IPCActualAdapterOwnerDedup` | 下列限制仍然成立:普通 CLI 没有持久化文件 logger,非流式 JSON 模式维持单一安全 envelope;只读请求没有 mutation operation ID,认证前失败也不会借用尚未解析的 ID。共享运行中的 mihomo stdout/stderr 不附加当前请求 ID,不能按 operation ID 精确归因。Panel `PreparedMutation.Cleanup` 的既有接口没有错误返回,因此该 cleanup 内部失败仍不可观察。logger 建立前只有 Unix 显式 system-service/launchd 或 Windows SCM 的非交互 daemon owner 可使用注入的安全 stderr 摘要;日志资源写入或关闭失败继续使用独立的非 JSON fallback,不能递归写入失效 logger。 Phase 4 完成了下表所列后台调度、Web gateway、CLI/TUI 本地任务、stream、关闭和 logger 故障入口的人工审计。表中的“覆盖”只表示列出的入口和断言已有明确 owner 或已如实登记可见性边界;它不表示每一种可能的 error、所有 cleanup 或所有平台原生路径都能产生诊断。 | 已审计入口 | 失败源与处理策略 | 最终 owner 与级别 | operation ID 来源或缺失原因 | 脱敏方式 | 具体回归断言 | 剩余限制 | | --- | --- | --- | --- | --- | --- | --- | | 订阅与 GeoIP scheduler 的单次刷新 | 每次回调在进入 Manager 前绑定新 ctx;下载、prepare/commit 失败沿既有 mutation 返回,重试/下轮检查仍由原 scheduler 决定 | 实际 mutation 仍由 `runtime.doOperation` 单点记录:真实失败 ERROR;订阅成功 INFO,GeoIP 不新增成功 summary;已有 marker 阻止 loop 重复记录 | 保留既有时间戳 ID:`scheduler--` / `scheduler-geoip-`;同一次 auto fallback 共用该次 ID,每次重试生成新 ID | 继续使用类型化、有界 cause 摘要;订阅 URL、token 和路径不进入 JSON | `TestSchedulerSubscriptionRefresh_RetryUsesFreshContextAndExecution` 断言 entry/backend ID、失败/成功事件、marker/cause、cache 提交和秘密缺失,但不直接检查 level;`TestSchedulerSubscriptionRefresh_FallbackKeepsAttemptID` 断言一次 clock 读取及 entry/direct/record 共用 ID;`TestSchedulerGeoIPRefresh_FailureStillChecksNextRound` 断言失败后继续、下一次新 ID、candidate commit/cleanup 和单条失败。Phase 3 已列的 `TestSubscriptionDiagnostic_FailedReplayAndNewIDKeepCausesIsolated` 与 `TestGeoIPDiagnostic_IPCRawFailureKeepsInternalEnvelope` 断言同一 runtime owner 的失败 ERROR | scheduler 成功 INFO 未由本行 focused 测试直接断言;时间戳 ID 保留既有碰撞性质;未新增全局 ID 服务 | | subscription/GeoIP scheduler loop 的终止结果 | 两个固定 runner 各自在返回且 cleanup 完成后分类;纯 owner 取消静默,joined 真实故障保留;一个 loop 失败不取消另一个,父取消后 join 两者 | `runSchedulers` 是 terminal owner,`background.failed` ERROR;Reporter 与 legacy callback 二选一;已报告结果静默 | loop 生命周期故障没有逻辑 mutation,不制造 operation ID | Reporter 使用安全 cause;legacy 仍只收到原错误对象和静态 component | `TestRunSchedulers_ReportsLoopFailureAndJoinsBoth` 断言报告后仍等待父取消及 sibling cleanup;`TestRunSchedulers_FailureClassificationAndLegacy` 断言取消/超时/join/marker分类与出口互斥;`TestRunSchedulers_ShutdownJoinsBothFailuresAndCleanup` 断言两次 cleanup 后两条无 ID ERROR | scheduler 现有 `Run` 通常只返回 nil/取消;没有为制造错误改变其返回语义 | | Manager 持有的 Web gateway、scheduler 外壳与 core recovery 后台结果 | 使用实际生命周期 ctx 分类;纯 shutdown 取消静默,live-context 上游取消/超时和 joined 真实故障可见,已有 mutation marker 去重 | `Manager.reportBackgroundContext`,`background.failed` ERROR;新 Reporter 优先,legacy 只作替代 | 这些是组件生命周期结果;core recovery 明确清除/不创建 operation metadata | 类型化摘要,不输出路径或 token | `TestManagerBackgroundDiagnostic_ReporterOwnsSchedulerFailure` 断言级别、cause、ctx 和安全 JSON;`TestManagerBackgroundDiagnostic_RealMutationMarkerPreventsSecondReport` 断言真实 mutation 不重复;`TestManagerBackgroundDiagnostic_CoreRecoveryHasNoInventedOperation` 断言无伪造 ID;既有 `TestManagerReportsWebGatewayError` / `TestManagerIgnoresWebGatewayCancellation` 分别断言 Web gateway component/cause 与 shutdown 取消静默 | 这里只负责最终后台结果,不替代实际 mutation owner | | Web 的 proxy select、connection close/close-all、config patch 与 TUN mutation | Web adapter 在调用 Manager 前绑定 ID;Manager marker 优先;未标记结果由 adapter 收口,server 仅兜底未标记意外错误 | `webMutator.reportResult`:成功 `mutation.succeeded` INFO,预期拒绝 DEBUG,真实失败 `mutation.failed` ERROR;server fallback 沿相同失败分类 | select/close/config 使用既有 UTC 纳秒 ID;TUN 使用既有 16-byte random hex,失败时退回时间戳;静态 operation 名由代码提供 | safe Reporter 只保留类型化原因;浏览器凭据、controller secret/address、query/body 和对象名不输出 | `TestWebMutatorDiagnostics_ContextOutcomeAndID` 覆盖五种操作的真实 Manager 入参、ID、成功/拒绝/失败/取消和单条 JSON;`TestWebMutatorDiagnostics_TUNRandomFallback` 直接断言 TUN random-source 失败后的时间戳 ID;`TestWebMutatorDiagnostics_RealManagerOwnsFailure` 与 `TestWebGatewayDiagnostics_RealManagerFailureIsReportedOnce` 断言真实 owner 去重及原 HTTP 400 envelope | ID 只在本地 Web→Manager 链使用,没有增加 header/DTO;缓存重放语义未变 | | Web mutation 的解析、shape、allowlist 与缺少 mutator 拒绝 | 解析错误保留错误对象供安全摘要;shape/allowlist 拒绝不附带动态输入;既有 400/403/502 响应不变 | gateway `mutation.rejected` DEBUG;调用返回未标记意外错误时 `mutation.failed` 按 FailureLevel | 拒绝发生在 adapter 创建 mutation ID 前,gateway 不伪造 ID | 静态事件和安全 parser cause;未知字段、正文、group/node/connection 值不进入日志 | `TestGatewayMutationDiagnostics_RejectionsAndFallback` 逐个断言 malformed/empty/managed/unknown/shape/missing-mutator 的状态码、级别、记录数、无 ID 与敏感值缺失 | 认证前和尚未解析的请求无法取得 mutation ID;没有为诊断额外解析 body | | Controller HTTP reverse proxy | 请求构造或 transport 失败由既有 ErrorHandler 返回原 502,同时产生安全诊断;认证剥离、controller credential 注入和响应 header 隔离不变 | proxy owner `proxy.failed`,真实失败 ERROR;实际 ctx 取消静默 | 不创建请求 ID;若调用者本地 ctx 已有合法 metadata,只在该进程日志中保留 | 类型化 transport cause;上游 URL、controller secret、请求 query/body/headers 不输出 | `TestControllerProxyDiagnostics_SafeFailureAndOriginalContext` 断言原 502、permission cause、ctx 和秘密缺失;`TestControllerProxyDiagnostics_PreservesLocalMetadata` 断言 metadata 只作本地关联 | 普通只读代理请求无跨 IPC operation ID | | WebSocket handshake 与双向 relay | controller 地址解析、upstream dial、browser accept 各自在失败处收口;relay 首个结果触发原取消/peer close,等待两个方向结束后再按方向与 close provenance 选唯一实际故障 | handshake owner `websocket.handshake.failed` ERROR;post-join owner `websocket.relay.failed` ERROR;normal/going-away close 与实际 ctx 取消静默 | WebSocket 路径无既有 mutation DTO,不新建跨 IPC ID | safe cause/Close 状态摘要;地址、credential、payload 和 close reason 原文不输出 | `TestGatewayWebSocketHandshakeFailuresAreSanitized` / `TestGatewayWebSocketAcceptFailureDiagnostic` 断言三类握手失败安全且单点;`TestGatewayWebSocketCloseReadArrivesAfterInducedWrite` 断言并发 graceful close 静默、异常 close 只记真实状态且两 relay 已 join;`TestWebSocketRelayFailure_PreservesIndependentFaults` 断言 joined 独立故障不被关闭噪声隐藏;`TestGatewayWebSocketReadLimitKeepsSingleFailureOwner` 断言 controller→browser 1 MiB gateway read bound、relay join、upstream cleanup 和单 owner;`TestGatewayWebSocketLargeUpstreamMessages` 验证 32 KiB 以上至 1 MiB 的 text/binary 消息完整转发且正常关闭无误报,`TestGatewayWebSocketBrowserReadLimit` 验证 browser→controller 32 KiB 边界 | 没有为 stream 增加协议关联字段 | | 本地控制客户端 `Stream` | focused 覆盖的 transport、HTTP/WebSocket handshake、read/size/decode 和异常关闭保留可用 cause;合法 daemon error envelope 与本地失败分开;callback 错误原样返回且不误归为 transport。地址校验和 token 获取也进入同一实现 owner,但没有对应 focused cause/级别断言 | `control.client`:本地 `stream_failed` 按 FailureLevel,合法远端 `stream_response` DEBUG;新报告结果加 marker,已有 marker 去重;正常关闭/实际取消静默 | 可读取调用者本地 ctx metadata,但 stream 请求不新增 query/header/DTO,因此 daemon 与客户端之间没有统一 ID | 复用安全 wrapper/Reporter;不输出 endpoint、token、原 payload 或未知文本 | `TestStreamDiagnostics_TransportCauseAndReportedOwnership`、`TestStreamDiagnostics_RemoteEnvelopeAndInvalidHandshake`、`TestStreamDiagnostics_ReadDecodeAndBounds` 断言其覆盖分支的 cause/API 分类、级别、单 owner、边界与秘密缺失;`TestStreamDiagnostics_CallbackAndNormalTerminationStayQuiet` 断言 callback identity/count、正常/取消静默和 nil Reporter;`TestStreamDiagnostics_CancellationDeadlineAndNilReporter` 断言 canceled dial、live deadline、nil Reporter 与 pre-reported ownership;`TestStreamDiagnostics_NativeTransportKeepsMetadataLocal` 断言原生 IPC 请求未携带 metadata 且连接完成清理 | 非取消 token 获取失败和无效 base address 的 cause/级别目前只有实现检查,没有 focused diagnostic assertion;`requestToken` 已抹去的底层 cause 无法恢复;`CloseNow`/body close 无新 error 接口,只验证功能清理 | | TUI stream generation、重连与恢复 | session 消费本代结果后先作本地诊断,再以 fresh Status 决定公开重连错误;正常 generation 结束按现有 reopen 处理;首次健康信号恢复 backoff;终止路径取消并 drain 活跃 generation | session 对未标记故障记 ERROR,正常 `streams_ended` DEBUG,首次 `streams_recovered` INFO;client 已标记故障不重复 | session 不创建跨 IPC ID;仅保留传入的进程内 ctx metadata(如有) | TUI Reporter 使用同一 redactor;公开结果仍由 fresh Status 的安全分类决定 | `TestSessionDiagnostics_FailureOwnershipAndFreshStatus` 断言 fresh Status 优先、marker/callback/normal-end ownership;`TestSessionDiagnostics_BackoffRecoveryAndJoinedReopen` 断言 backoff reset 和一次 INFO;`TestSessionDiagnostics_TerminalPathsJoinActiveProducers` / `TestSessionDiagnostics_CloseJoinsRealClientReports` 断言终止发布和 logger close 前 join | 每代仍使用既有 first-result 策略,不聚合所有 callback 故障;依赖注入的 Stream/Reporter 最终返回 | | CLI stream 命令 | 沿用 `control/client.Stream` owner;CLI callback/output writer 错误保留原退出码和 renderer,不当作传输故障 | 有显式注入 Reporter 时由 client 记录;CLI 展示层不重复 ERROR | 没有新增 CLI stream operation ID | text 与单一 JSON envelope 继续使用既有安全渲染;诊断不混入 stdout/stderr 协议输出 | `TestStreamCLI_DiagnosticInjectionPreservesOutputContracts` 断言 text、非 follow/follow JSON 字节、事件顺序、writer failure exit/envelope 和零误报 | 普通 CLI 不创建持久化日志;无注入 Reporter 时没有额外诊断输出 | | TUI installation inspect/plan 与 self-update prepare worker | 每个实际 worker 创建/复用值型 metadata,错误在 worker Done 和资源 join 前、mutex 外报告;异步 result 携带自己的 metadata,迟到结果不借用当前 UI task | TUI `installation.inspect.failed` / `.plan.failed` / `self.prepare.failed` 按 FailureLevel;真实失败 ERROR、实际取消静默、已有 marker 去重 | 每个实际任务取得独立本地 ID;生成失败只记静态 WARN 并以空 ID 继续执行。`Context` 的实现也保留同任务的现有 metadata | 安全 Reporter;ID 生成错误只输出固定 `diagnostic identity unavailable`,不输出原错误 | `TestLocalTaskDiagnostics_InstallationOwnersBindDistinctOperations` / `TestLocalTaskDiagnostics_InstallationConcurrentFailureIDs` 断言独立 actual ctx/并发 ID;`TestLocalTaskDiagnostics_InstallationFailureOnceAndCancellation` 断言 ERROR、取消、nil/unavailable Reporter 与 marker 分类;`TestLocalTaskDiagnostics_InstallationIDFailureStillExecutes` 断言安全 ID 失败且任务继续;`TestLocalTaskDiagnostics_InstallationAsyncResultsCarryOwnValues` / `TestLocalTaskDiagnostics_InstallationPlanResultCarriesOperation` 断言异步结果携带自己的值;`TestLocalTaskDiagnostics_PreparedUpdateFailure` 覆盖 self prepare;`TestLocalTaskDiagnostics_InstallationJoinIncludesReporterOutsideLock` 与 `TestRun_LocalInstallationFailureUsesOwnedReporterBeforeClose` 断言 reporter 在锁外且 join/logger close 前完成 | 没有 focused 测试把“已存在但 ID 为空”的 metadata 传入 worker;该保留行为只是实现检查。prepared candidate/worker 的 void cleanup 只能验证 join/清理结果,不能推断不存在内部失败 | | TUI logging apply | coalescer 每次真正调用 void `Apply` 时创建 `logging.apply` ctx;不为被合并请求制造身份或成功/失败事件 | `localLogging.Apply` 没有 error 返回,任务层无结果级别;logger 资源故障仍归 `FailureReporter` 的独立 outlet | 每次 actual Apply 生成本地 ID;不复用 lifecycle operation | FailureReporter 使用固定安全摘要;任务 ctx 不进入该 outlet | `TestLocalTaskDiagnostics_ApplierBindsActualExecution` / `TestLocalTaskDiagnostics_ApplierDoesNotReuseLifecycleOperation` 断言 actual call 的新 metadata;`TestLocalTaskDiagnostics_ApplierIDFailureAndVoidVisibility` 断言 ID 失败不阻止 Apply 且没有虚构结果记录 | void `Apply` 无法证明成功或暴露 task-correlated failure;FailureReporter 没有 operation ctx | | TUI 日志导出 | worker 捕获最终 error、panic 与第一个 warning;预期单 cause rejection 做有界透明 unwrap,joined IO/未知原因保守为真实失败;报告后再取消/join | `logs.export.rejected` DEBUG;`logs.export.warning` WARN;`logs.export.failed` 按 FailureLevel(真实失败 ERROR);已有 marker 去重 | 每次实际导出创建本地 ID;生成失败静态 WARN 后无 ID 继续 | 不记录输出路径、归档内容或 warning 原文;Reporter 使用安全 cause | `TestLocalTaskDiagnostics_ExportBindsActualExecution` / `TestLocalTaskDiagnostics_ExportResultsHaveIndependentValues` 断言实际执行 ID 与独立结果值;`TestLocalTaskDiagnostics_ExportExpectedOutcomesAreNotErrors` / `TestLocalTaskDiagnostics_NativeExportRejectionsRemainDebug` 断言 bare 与真实 exporter rejection 为 DEBUG;`TestLocalTaskDiagnostics_ExportWrapperClassificationPreservesRealCauses` 断言单 cause、joined/depth/cycle/marker;`TestLocalTaskDiagnostics_ExportFailureWarningAndCancellation` 断言 WARN/ERROR/取消/秘密缺失和单条记录;`TestLocalTaskDiagnostics_ExportWithoutReporterDoesNotInspectError` / `TestLocalTaskDiagnostics_ExportWithoutReporterPreservesTypedNilError` 断言 nil Reporter 不遍历 opaque/typed-nil error 且仍完成 cleanup/join | 启用 Reporter 后,通用 marker 对合成 typed-nil receiver 的既有风险仍待最终独立评审,不在 Phase 4 修改通用 helper | | TUI cleanup 后的 installation execute 与 prepared self apply | 旧 TUI 先关闭 worker、session、applier、logger、FS 并恢复终端,随后执行;只绑定值型 metadata,不延长已关闭 logger 生命周期。nil Reporter 是 `finishInstallationRun` / `finishPreparedRun` 的实现边界 | task helper 的 Reporter 明确为 nil;失败仍由既有同步返回、固定 warning 和外层 renderer 负责,没有持久日志级别 | cleanup 后分别绑定 `installation.execute` / `self.apply` 的本地 ID,供 executor ctx 使用 | 固定 warning 不包含原 cause;公开 partial-success/错误渲染保持安全 | `TestLocalTaskDiagnostics_LateTasksBindAfterCleanup` 只断言 cleanup 后执行、metadata 和原 error identity,不直接观察 Reporter;`TestPreparedUpdate_CleanupBeforeApplyAndRelaunch` 断言 cleanup→Apply/relaunch 顺序、原 cause 返回和固定安全 warning | cleanup 后没有持久诊断 outlet;nil Reporter 来自实现检查而非本行测试的直接可观察断言,不能声称失败已写入文件 | | CLI service action/status/apply、installation inspect/plan/execute 与 self update | 每个同步本地 owner 在调用前绑定 ctx,在既有分类/渲染前报告最终失败;self update 的 prepare/apply/cleanup cause 只在 owner 的局部变量聚合 | `cli` 的 `service.*.failed`、`service.apply.failed`、`installation.*.failed`、`self.update.failed` 按 FailureLevel;取消静默、预期拒绝 DEBUG、真实失败 ERROR | 复用既有 `Dependencies.NewOperationID`;每个实际任务一个 ID,生成失败静态 WARN 后继续;self prepare/apply 共用一次 `self.update` ID | DiagnosticReporter 只输出安全摘要;CLI stdout/stderr、单一 JSON envelope 和退出码保持原样 | `TestLocalTaskDiagnostics_ServiceActionBindsOperation`、`TestLocalTaskDiagnostics_ServiceApplyBindsAndReports`、`TestLocalTaskDiagnostics_CLILocalOwnersPreserveJSONAndExit` 断言实际 ctx、单条诊断以及注入前后 stdout/stderr/exit 完全一致;`TestLocalTaskDiagnostics_ServiceBorrowsReporterWithoutChangingOutput` 断言取消静默、nil Reporter、安全 ID 失败与成功 JSON 不变;`TestLocalTaskDiagnostics_LegacyServiceOwnersReport` 覆盖 legacy controller | 默认依赖没有持久 Reporter;普通 CLI 因而仍只有既有用户输出,不产生文件日志 | | daemon logger 写入/Apply/Close 与进程关闭 | rotator 的 lock/write/unlock/void Apply 失败走独立、按类别限速的 `FailureReporter`;runtime Close join 文件与 lock cause;daemon 汇总 stdout/stderr/mihomo/daemon/FS 的全部 close 后一次报告,并保留原 run/open error | 非 JSON 独立 outlet 的 `logging: :` 固定记录;没有 slog 级别或 operation ID;失败 outlet 只尝试一次,不递归 | logger 自身故障不属于业务操作;启动早期和 routine shutdown 都不伪造 ID | 已知 secret 注册后脱敏,早期 cleanup 用固定摘要;不输出路径、CR 或 raw cause | `TestRotatingWriter_FailuresKeepCountsAndSafeFallback` 断言 n/error/dropped/config、限速单行和秘密缺失;`TestRuntime_CloseJoinsFileAndLockErrorsWithoutClosingSharedFS` 断言 cause join/幂等/FS ownership;`TestRunDaemonWith_CloseFailureUsesIndependentSafeOutlet` 覆盖五个 closer、partial/early/nil/failed outlet、顺序、单次与安全文本 | nil outlet 可静默;失败 outlet 也可能不可见;同类别一秒窗口可抑制后续记录;void Apply 只能由资源 reporter 表示类别,不能绑定 task cause | | TUI logger bootstrap/cleanup 与 Run 资源关闭 | bootstrap 和 cleanup 使用独立固定 warning;cleanup 先 cancel/session,再 join exporter/applier,最后关闭 logger/FS,全部完成后报告;重复 cleanup 返回缓存的 joined error | 非 JSON `Warning: TUI file logging ... failed`,无 slog 级别/operation ID;按 bootstrap/cleanup 类别限速 | 资源生命周期事件不对应用户 task,不创建 ID | 固定文本且经既有 redactor;写入失败不递归 | `TestRunFactoryClosesPartialResourcesLogging` 断言真实 Run open failure 的固定 bootstrap warning 与 partial FS cleanup;`TestTUILoggingBootstrapFailureIsRateLimited` 断言 bootstrap 类别窗口;`TestRunCleanup_CloseFailureReportsAfterAllResourcesOnce` 断言 join cause、顺序、重复调用和关闭后单条固定 cleanup warning;`TestTUILoggingFailureReporter_OutputFailureDoesNotRecurse` 断言每类一次及窗口恢复;`TestRunCleanup_NetworkTerminateDoesNotDropDiskWorkers` 断言 disk worker 先于日志/FS 释放 | void worker/session/applier close 接口不能产生新增 returned error;nil/failing warning outlet 可静默 | | 控制服务器 shutdown 与 hijacked stream join | 保留既有 Shutdown timeout 后 force close、snapshot/handler join 和 `ErrServerClosed` 过滤;正常 shutdown 返回 nil,不增加第二个诊断 owner | 没有为预期 shutdown deadline 新增 ERROR;非预期 `Serve` 返回沿 daemon 外层返回/渲染边界传播,不增加专用诊断事件 | 生命周期关闭没有 operation ID | 不新增错误输出;daemon 外层若报告仍走安全 Reporter | `TestServeContextCancelToleratesShutdownTimeout` 直接断言注入 Shutdown timeout 后返回 nil;`TestServe_ForceClosesBeforeJoiningSnapshotWorker` 断言 force close 后 join;`TestServe_JoinsHijackedStreamBeforeReturning` 断言 runtime stream 结束后才返回且原返回为 nil;原生 IPC lifecycle 集成覆盖 stop/delete/start | 该接口未增加各 closer 的错误诊断;测试证明已知 join 结果,不能据此推断所有无 error cleanup 不会失败 | Phase 4 保留几条明确边界:认证前、预解析和只读请求没有统一跨 IPC ID;stream 的本地 metadata 不通过新增 header/DTO 传输;普通 CLI 没有持久化文件 logger;共享 mihomo stdout/stderr 不附加请求 ID。TUI logging `Apply`、部分 worker/cleanup 接口没有 error 返回,只能验证调用、顺序和可观察清理结果,不能推断内部从未失败。cleanup 后执行的 installation/self-update 使用 nil Reporter,只保留原同步错误或固定 warning。启用 Reporter 时,合成 typed-nil error receiver 进入通用 marker 遍历的既有风险留待最终独立评审;本阶段未修改 generic helper,也不据此承诺支持所有任意 error 实现。#197 的 Setup 提示仍不在本设计范围。 ## 核心安装 TUI 进入 System/Web GUI 时通过带认证的本地控制接口 `GET /v1/core/version-check`、`GET /v1/panels/{id}/version-check` 查询上游版本。响应为 `schema: mihari/v1`、`latest`,core 另有 `channel`;失败使用既有错误 envelope。daemon 复用 core installer / panel adapter 查询元数据,网络 IO 不持 mutation gate 或 panel 锁,不下载、不执行二进制、不修改业务文件或 revision。现有状态读取保持本地查询,不触发公网请求。alpha 展示对应平台资产中的 `alpha-{sha}`,面板沿用发布 tag 或构建提交标识。TUI 逐项保存检查状态并拒绝旧 core 通道/旧请求结果,普通状态轮询不清除检查结果。成功结果在 TUI 会话内缓存 5 分钟,失败不缓存;成功的 core 安装/通道切换在重新读取 core 状态后检查,面板安装/更新/回滚/重装/卸载只失效并重查对应条目,旧 generation 的结果不能覆盖刷新结果。安装权限与可信 core 限制保持不变。 守护进程通过同一条下载、校验、替换链路安装 mihomo,并支持 `stable` 与 `alpha` 两个通道: - `stable` 从 GitHub `/releases/latest` 取当前稳定版;`alpha` 从固定滚动 tag `Prerelease-Alpha` 取预览版。 - 展示与协议里的 Version 永远来自 `ParseVersion(mihomo -v)`:稳定版为 semver(如 `v1.19.x`),alpha 为 `alpha-{sha}`(实测 `-v` 形如 `Mihomo Meta alpha-dd7bc4c ...`,不是 `v1.19.x`)。GitHub tag `Prerelease-Alpha` 从不作为版本显示或写入 Version。 - 本次安装意图随请求进入安装链路;`settings.core-channel` 只在 Commit 成功后写入,表示上次成功安装的通道。切换失败或未提交时,持久化通道与界面仍为旧值。 - all-in-one 包在 `data/bin/core-channel` 写入 sidecar(第 1 行通道,第 2 行 stamp)。安装器覆盖 sidecar,不改 `mihari.yaml`;守护进程仅在 stamp 变化时把打包通道写入 settings,避免旧 sidecar 覆盖用户后来在 System 页切换的通道。 ## TUI - TUI 只通过 `internal/control/client` 经原生 IPC 控制面与本地守护进程通信。它从不打开 mihomo 控制器、从不接收控制器密钥。 - TUI 的日志直接写入例外是经 `internal/logging` 在当前 UID 的 U/logs 追加/轮转固定 `mihari-tui.log*`(Windows/显式私有 P 保留单根);不得写 `mihari.yaml`、订阅、token、面板或其他业务状态。日志配置变更仍只走 daemon 控制面。 - System 页面 Logging 下方的 Maintenance 区提供 **Completely Uninstall Mihari**。两次确认框都默认 Cancel,并列出将清空的文件夹;与安装根程序字节一致的 PATH 命令文件也会列在其中。若该路径是普通文件,但与安装根程序字节不一致或安装根程序已缺失,两次确认之后再出现一次对话框,写明路径和原因,默认 Cancel(保留文件并继续卸载,服务和其他内容仍清理;保留的命令位于清理根内时,保留文件及必要父目录,清理同根其他内容和其他目标;`--force` 不替代单独命令删除同意;但不宣称完全卸载成功,CLI 返回 `invalid_state`);Confirm 才删除它。确认后先关闭 TUI 资源,再由已提权的本地卸载路径尝试删除列出的或本次同意删除的命令文件,然后停止并注销 OS 服务,再删除这些文件夹。Unix 上该文件删除失败则停止,服务和目录保持不变。Windows 上删除失败会留下该文件并继续清理,不以完全卸载成功结束,错误里给出一条退出后可复制的 `Remove-Item -LiteralPath` 命令。CLI `mihari service uninstall --purge --yes` 仍只删除白名单内的受管文件;整根删除需再加 `--force`。对不上的普通命令文件还要 `--delete-unmatched-command`;只有交互终端且不是 `--json` 时才询问 `Delete unmatched command file <路径>? [y/N]`。`--purge --yes` 本身不算同意。目录和符号链接不在这次确认里删除。普通 `service uninstall` 仍只注销服务。 - 搜索与表单字段中的括号粘贴和 Ctrl+V 使用纯 Go 实现的 `github.com/atotto/clipboard` 辅助库;Mihari 本身从不把密钥写入剪贴板。 - 页面:独立的首次运行 Setup 路由、Overview、可展开的 Proxies、带本地 GeoIP 详情的活动/已关闭 Connections、Rules/Providers、有界的结构化 Logs 流、订阅管理表单、分类的 System 页面,以及驱动面板安装/更新/激活/打开/回滚的 Web GUI 页面(在守护进程通告 `web-gui` 能力之后)。 - Setup 安装核心、可添加初始订阅、准备本地 GeoIP 数据,并请求守护进程持久化校验过的本地端点。 - Setup 端口预检复用 PID owner 分类,区分本实例占用、确认的外部冲突、可用与未知;仅外部冲突允许自动建议新端口。搜索最多 `+1024`,不越过 65535,预留其他字段的端口,generation 守卫拒绝迟到探测结果。 - Setup 使用共享 Theme 的分步固定布局,按动作显示动态等待与耗时。异步命令只返回结果,页面字段仅在 Update 中发布;错误通过统一诊断快照保留原始 cause,F2 可滚动查看并复制;概要与详情分开展示,终端控制字符只在显示层转义。 - 每步经 daemon 提交,端口确认时 PATCH onboarding(Complete=nil),随后等待重启生效。最终 Review 只结束引导,不重交端口。SetupRequired 根据端口生效状态与核心资源判断,历史 Complete、可选订阅和 GeoIP 不再独自决定是否进入向导。读取失败不等于核心缺失;已有订阅自动略过,新订阅先获取/校验,失败不留下条目;已有条目刷新失败保留旧缓存。 - 仅已确认的启动端口占用可开放 daemon 内部受限 onboarding 适配器,复用 Manager 的校验和原子设置事务。健康仍为 degraded,不挂载完整 RuntimeAPI;不扩大权限错误、安装事务失败等场景的可写边界。当前服务适配器不提供实例身份,端口保存后提供手动重启及重连检查,不自动操作无法核对身份的服务。 - 新增认证只读 `GET /v1/operations/{operation_id}`(能力 `operation-status-v1`):响应 schema、operation_id、state(running/finished/unknown),不返回请求体或内部原因。内存最多保留 256 条固定长度摘要键记录,饱和时保守 unknown;重启/淘汰亦为 unknown。覆盖 setup 的 core install、GeoIP update、onboarding update、订阅 add(含首次刷新)及 profile mutation 的完整 handler 生命周期;同 ID 所有 handler 收尾后才可能 finished。finished 不代表业务成功,取消后仍读取对应领域状态;查询绝不重放 mutation。 - System 页面通过与 `mihari service` 相同的本地服务适配器管理 OS 服务(安装/卸载/启动/停止/重启/状态);这些操作要求进程已经提权,且不经过守护进程控制协议。当守护进程通告相应能力时,System 页面显示实时的系统代理与 TUN 状态,并通过本地控制 API 切换它们(开启外部代理或其他 TUN / mihomo 实例需要强制确认;Mihari 从不清除其他产品的代理)。 - System 页面的 Ports Config 可修改 Mixed / Controller / Web 端口;占用按本实例 PID 显示 `Owned`,或 `Occupied by name (pid)` / `Available`。写入复用 onboarding 更新,应用后通常 `RestartRequired`。没有对应 CLI。 - System 页面的 Logging 区可修改 daemon-owned 的 level、最大文件大小与保留数量;更新经稳定的 `/v1/logging` 控制协议热应用,不需要 daemon restart。Logs 页的 `e` 与 System → Logging 的 **Export logs** 打开同一个本地导出对话框;导出不增加 CLI 命令;Unix 系统模式使用可选的 machine-log-snapshot-v1 控制协议。 - System 页面在导航栏选中、按 Enter 进入之前,以只读方式检查 Mihari 的最新 GitHub Release 和 core 当前通道,并用 `当前版本 -> 最新版本 available`、`当前版本 · Up to date` 或 `ahead of ` 展示结果。再次选中会重新检查;按 Enter 进入页面不会再次发起,也不会中断进行中的检查。实际更新先准备固定候选,再按真实目标版本确认;确定降级保留完整风险;未知兼容性使用简短英文说明。更新确认专用布局保留可信已安装副本的安全非标准标识 `Unknown[label]`,按副本展示,支持正文滚动且默认取消;该内部显示证据不进入版本比较、preview ID、CLI/JSON、日志或持久化。确认后,本地 updater 在控制协议之外复核候选、目标和服务定义,替换 Mihari 可执行文件并尝试同步已安装服务;该写操作要求 TUI 进程已经具备管理员/root 权限,不会自动触发 UAC 或 sudo。旧 Bubble Tea 程序先关闭工作、IPC、日志与文件所有者并恢复终端,再提交和进入新 TUI。取消与迟到准备结果由 Run 所有者清理。Unix 复用安装锁,Windows 在主程序替换、服务停止及服务副本复制边界复核;后续同步失败保留主程序已更新的部分成功状态。预览只在本次调用中存在,跨 Unix helper 调用用不透明 preview_id 绑定,不改变 daemon /v1、安装请求或 journal 格式。 - 更新成功后,旧二进制及已完成事务残留延后到后续启动清理。daemon 取得实例所有权后经 core store 清理其管理的事务,daemon/TUI 经 app 用例清理当前 Mihari 程序旁残留;普通查询及安装验证子进程不清理。核心终态先持久保存独立的 `pending-cleanup.json`,再释放当前 journal,允许同次运行连续更新。启动清理核对记录、marker 与对象身份,保留未决或不明恢复材料;失败以 WARN 进入已有 F2 历史,不影响启动,下次再试。Unix binary-only stage 仅在候选已被发布消耗时可回收;安装证明与迁移保留树不纳入。详见 [ADR 0006](adr/0006-defer-update-residue-cleanup.md)。 - Windows 更新预览对管理员执行检查未通过的用户目录目标,增加同一用户的降权版本查询:持有并验证调用者关联的非管理员 UAC token,核对用户 SID、会话、elevation、管理员组、integrity 和 UIAccess,再以该用户的非管理员 owner/ACL 规则复查整个路径。若 linked token 只能识别身份、复制为 primary token 返回 `ERROR_BAD_IMPERSONATION_LEVEL`,可从桌面 Shell 取得 token,但必须再核对与调用者相同的用户 SID、桌面会话及 logon SID,并满足全部非管理员权限检查。Shell 只提供经过核验的 token,不作为子进程父进程,以兼容旧版 CLI 的 Explorer 启动拦截。提权调用者使用 CreateProcessWithTokenW 启动查询,持有的 token 仅增加创建进程所需的 default/session metadata 访问权,不启用调用者或子进程的额外特权;非提权调用者继续使用 Go 的普通 token 启动路径。只运行固定 `self version --json`,保留隔离环境、3 秒超时、4 KiB 输出上限和子进程回收;仅继承标准 IO 句柄,查询前后继续核对路径、文件身份、SHA256 与执行信任。另一个用户可写的路径、无 Shell、跨用户或跨登录会话、无法取得或验证降权令牌、启动失败仍保持 unknown;不回退为管理员执行该目标。原有管理员可信目标与 Unix 规则不变,不改变业务写入、CLI/JSON、确认标识或持久化契约。 - Mihari 应用通道 `main`/`dev` 与 mihomo Core 通道 `stable`/`alpha` 分开:应用通道写在 Unix B/P 的 `mihari-channel` sidecar(Windows 为旧数据根),不进 `mihari.yaml` / `/v1`;AIO `--channel` 只写该 sidecar;CLI/TUI 自更新仍走 GitHub Releases。 - System 页面的 `Core Channel` 行可在 `stable` / `alpha` 之间切换;切换后由守护进程按新通道重装核心。版本行显示 `ParseVersion(mihomo -v)` 的身份 token,从不显示 `Prerelease-Alpha`。 - 规则顺序从不排序;onboarding、系统、provider、订阅、面板和浏览器变更都经由守护进程变更协调器,破坏性或大范围操作需要确认。 ## 运行模式与 GLOBAL - Mihari 管理 `rule` / `global` / `direct` 的持久意图,settings 可选 `routing` 字段缺省为 Rule,覆盖订阅和 overrides 的 mode。GLOBAL 出口按稳定订阅 ID 保存,无订阅使用独立的 `bootstrap-global`。 - 可选能力 `routing-mode-v1` 提供 `GET/PATCH /v1/routing`,明确区分 saved/live 与 applied/pending/unknown。`GET /v1/proxies` 附带候选所属的 revision/subscription_id;选择请求可携带 `if_revision`。TUI/CLI 不写业务文件。 - 在线模式切换经统一 Manager mutation:观察旧模式/出口,按需 PATCH/PUT,读回确认,原子保存 settings 后发布 revision。写失败或保存失败恢复旧 live 状态;无法确认恢复则进入 degraded 并拒绝后续业务写入。请求超时后使用有界恢复 context 核对,不推断已停止或已成功。 - 普通模式切换不 reload,不关闭连接。runtime YAML 在正常配置生成时写入保存的模式;supervisor 的健康检查在报告 running 前恢复 settings 意图,覆盖已有 runtime 文件及自动重启。订阅 reload 在同一 mutation 内恢复新订阅出口,失败同时回滚配置、目录和旧 live 出口。 - GLOBAL 只使用实际可选择的候选。丢失出口时优先 DIRECT,否则退回 Rule,持久化后不会自动返回旧节点。Rule/Direct 下也可预选出口;删除订阅同步删除保存选择。 - Proxies 顶部 Mode 使用项目 Theme 的回车弹窗,GLOBAL 入口复用现有组。候选 revision、订阅身份和会话 epoch 防止迟到结果影响当前选择。 ## Web 网关 - 守护进程在 `web-addr`(默认 `127.0.0.1:9191`)上启动回环 Web 网关。 - 浏览器认证使用存储在数据根目录下的专用 Web 访问凭据;它绝不是 mihomo 控制器密钥,也不会出现在状态 DTO 或默认 CLI 输出中。错误自带的凭据可能保留在未脱敏文件日志内。 - `panel open` 铸造一次性本地 URL、启动 OS 浏览器,且不打印令牌。 - 面板静态资产位于 `web/{panel}/{build}/` 下,使用原子 `active.json` 切换,并保留一个先前构建用于回滚。 - 浏览器 REST 与 WebSocket 流量在网关处认证;网关只将控制器密钥注入被代理的控制器请求。 - 未知写入默认拒绝;核心升级与托管字段写入永远不会到达 mihomo。 - zashboard/MetaCubeXD 的单字段 `PATCH /configs {"mode":"..."}` 与 `PUT /proxies/GLOBAL` 进入同一持久化用例。mode 与 TUN/其他字段混合、任意完整配置 PUT 仍拒绝;面板自行发起的连接关闭是独立操作。 ## 订阅 - 订阅 URL 与缓存源 `cache-url` 由 daemon 持久化,list/show、事件与公开错误均省略。认证的本地 `GET /v1/subscriptions/{id}/url` 专门返回 schema 与当前 URL;所有已通过现有控制认证的本机用户都可读取,包括 Unix 共享控制凭据允许的本机用户。Web gateway 不挂载此接口;不新增能力标记,TUI/daemon 配套升级。 - reveal 响应不主动写日志;TUI 传输错误保留概要及原始诊断详情,文件诊断日志与本地错误汇报均不脱敏。 - `cache-url` 记录缓存来源,`schedule-from` 记录 URL/interval 变化的调度起点,`interval-refresh-required` 独立持久化强制过期。公共 DTO 添加 `cache_outdated`、`schedule_from`、`interval_refresh_required`。URL 修改保留缓存与 active;interval 修改统一强制过期;成功刷新清除调度起点和过期标记,失败或回滚保留。旧源在途成功/失败均受 profile version 守卫。 - 调度以 schedule-from(否则 UpdatedAt)加有效 interval 为基准,保留 jitter/backoff;URL/interval 实际变化使旧重试等待失效,执行排队任务前再检查到期。Name/Mode 等无关变化不重置调度。 - TUI Enter 合并详情与编辑,字段 diff PATCH 走统一 mutation path;PATCH 纳入既有 operation-status 跟踪,finished 仅表示 handler 已结束。冲突与未知结果的重提分别要求确认,新操作 ID 与最新 revision 防止静默覆盖。URL 草稿仅存在于当前弹层。 - 旧二进制严格解码不能读取新增持久化字段;不支持直接降级。升级前停机备份完整业务数据,降级恢复与旧二进制兼容的一致备份,详见 README。 - 每个有效配置都有独立缓存,因此 `sub use` 在无 provider 网络访问时也能工作。 - 每个订阅可独立配置拉取代理(`direct` / `proxy` / `auto`);`auto` 在代理失败时回退直连。 - 生成的配置总是在 `mihomo -t` 与重载之前恢复 Mihari 托管的内环回控制器、密钥与端口不变量。 ## 系统代理与 TUN - `sysproxy enable` 将桌面 HTTP/HTTPS/SOCKS 系统代理指向 Mihari 的混合端点。如果另一产品已持有代理,enable 会以 `system_proxy_conflict` 失败,除非传入 `--force`(TUI 会要求确认)。 - `sysproxy disable` 只清除**由 Mihari 持有**的代理;它不会关闭外部代理。 - 在 Windows 上,当 Mihari 作为 LocalSystem 服务运行时,它写入**交互式控制台用户**的 WinINET 配置单元(`HKEY_USERS\\…`),而不是 SYSTEM 自己的 `HKCU`,因此桌面浏览器能感知到变更。 - `tun enable|disable` 仅持久化并覆盖 `tun.enable`,保留订阅中的其他 TUN 参数,不自动注入 stack,并在可用时通过控制器实时生效。TUN 根据 OS 不同可能需要提权或安装服务。 - `tun enable` 前检测其他 TUN 网卡与其他 mihomo 进程,并按本实例内核 PID 与 live `tun.device` 扣除自身;Down 状态的残留适配器忽略。冲突时以 `tun_conflict` 失败,除非传入 `--force`(TUI 会要求确认)。`--force` 只绕过冲突门控,不绕过 live 核对:内核未真正开启则回滚 Desired。 ## GeoIP - GeoIP 连接详情由守护进程本地解析。国家与 ASN MMDB 文件从公共 `Loyalsoldier/geoip` release 分支下载,与对应的 `.sha256sum` 文件校验,并作为 MMDB 数据库验证。 - 当任一本地文件缺失或至少 30 天未更新时刷新。刷新失败会保留上一对有效的数据库,且不会禁用其他连接详情。 ## 数据路径与安装边界 Unix 默认入口 B 为 Linux `/var/lib/mihari` 或 macOS `/Library/Application Support/mihari`,业务 D 为 B/data;E/C/channel 分别为 B/control.sock、B/control.token、B/mihari-channel,I 默认 `/usr/local/lib/mihari`。B root0711、D root0700、C/channel root0644、E root0666,普通用户通过认证控制同一机器代理。机器日志为 D/logs,本用户 U/logs 与 U/logs-export 独立;Linux U 使用绝对 XDG_STATE_HOME 或可信 home,macOS 使用可信 home/Library/Logs/mihari。默认发现不使用 XDG_RUNTIME_DIR,root 不信 HOME/SUDO_USER/XDG。 显式 MIHARI_DATA=P 保留 P 本身的私有布局与0700/0600权限;Windows 路径、named pipe、DACL、服务与本地 ZIP v1 保留。Unix 系统导出为 mihari-logs-export/v2,通过固定机器快照协议组合机器和本用户日志,离线须明确选仅本用户日志。客户端不创建 B/D/C,也不直接读取 D。 业务写入归 daemon/Manager;窄例外为 root installer 的持锁停机迁移/安装事务,app 对固定 channel sidecar 的受锁原子维护,以及服务已停止后的全量卸载删除预览列出的目标根。安装锁顺序 B→私有服务 P→data→endpoint,永久锁不 unlink。activation 之前恢复 source,之后只修复 target;旧树和日志保留,未知身份拒绝覆盖。已安装服务启动先校验全局 B 的 matching activation 与 binary hash,取得 data/E 后再校验,不重入 installer 持有的 install lease。未标记 root P 前台只看 P。root P channel 写入持 P 锁时只读相关 B journal,相关未完成事务拒绝;非 root P 不读 B。 所有平台由共同的 subscription.Generate 保留非托管配置,仅覆盖 Mihari 管理的关键参数,TUN 只覆盖 enable;配置语义与原生 provider 由 mihomo 处理。root runtime 保留可信核心 provenance,仍只允许 v1.19.30 的四个 Unix hash;不再按完整字段白名单拒绝配置,也不保证限制所有额外 listener 或文件访问。旧 provider/resource WAL 先恢复,再从原订阅缓存生成;缺失原缓存时保留旧数据并报错,历史资源不主动清理。验证子进程先恢复历史事务,再执行认证和校验,不启动真实业务、后台刷新或核心。可信 I 及全部祖先必须满足 owner/ACL/挂载规则,不能自动修复主机祖先;离线信任位置为解析后的 I/install-trust。无服务 self-update 使用编译通道且不访问 B;服务更新走统一安装事务。 日志/export 持有真实目录 identity,逐来源 Finish 与完整 EOF 校验后才发布;工作、快照、流、进程、日志与 FS 全部关闭并 join 后才释放 daemon lease。SIGTERM 参与同一清理路径。完整路径、权限、迁移、credential 轮换与恢复合同见 [Unix 布局与恢复](unix-layout.md)。 覆盖项: ```text MIHARI_DATA=/abs/path MIHARI_CONTROL_ENDPOINT=... MIHARI_CONTROL_CREDENTIAL=... ``` ## 本地 YAML 来源与文件引用 来源沿用 catalog `url`,存 HTTP/HTTPS URL 或规范 file URI;类型由 scheme 推导并固定。daemon 单独承担本地文件读取和 catalog/cache 写入;CLI/TUI 仅规范化路径并调用 IPC。主缓存保留原始字节,`cache-base-dir` 保存成功读取的实际目标目录,与内容一起提交/回滚;本地相对输入引用只在派生运行配置改为绝对路径,远程保持原生路径语义。 创建流程为锁外读取、解析、候选校验及文件引用确认,锁内重新检查 revision 并发布 catalog/cache,仅当新条目成为活动订阅时提交运行配置;失败补偿。文件引用不构造资源快照,不新增主 YAML watcher。仅创建新条目时确认引用的独立变化、权限和回滚后果;原生 watcher/下载由 mihomo 管理。校验与运行的普通/可信核心环境均固定 `SKIP_SAFE_PATH_CHECK=true`,允许任意引用路径;这是明确批准的路径边界变更,核心身份及安装信任保持。旧安全路径限制在本功能范围由 [ADR 0009](adr/0009-local-yaml-source-and-cache.md) 替代。 ## Setup 目标执行 本地控制请求可选 `execution_mode: "setup"`,适用端口/完成标记、核心安装、订阅添加与显式刷新、GeoIP 更新。`setup-target-v1` 仅表示已开放资源支持这套语义,不扩大端口恢复 surface 或 Web gateway 权限。Setup TUI 先检查能力,不携带旧页面 `if_revision`;旧请求省略新字段时语义不变,同时提供 `if_revision` 仍执行全局严格前提。 Manager 固定本步目标,合并最新未覆盖设置并经原 mutation owner 提交。只有提交前的相关输入失效才允许最多两次追加准备,下载与选定制品复用;失败、回滚失败或未知结果不能作为内部重提理由。核心重新校验用最新设置和活动订阅生成的配置及同一核心候选,停止状态也不能跳过;核心复用在取得所有权后复查对象身份。添加并启用订阅同一步提交,失败恢复此前有效目录和运行配置。完成引导只写完成标记,跳过不写资源,其他订阅及面板不被清除。全局其他入口的 revision 误冲突另由 Issue #349 跟进。