Skip to content

fix(milky): 修复掉线后前端仍显示已连接的问题 - #1822

Open
ShiaNyaa wants to merge 6 commits into
sealdice:masterfrom
ShiaNyaa:milky-disconnect-workaround
Open

ShiaNyaa wants to merge 6 commits into
sealdice:masterfrom
ShiaNyaa:milky-disconnect-workaround

Conversation

@ShiaNyaa

@ShiaNyaa ShiaNyaa commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Milky 服务断开或内置 Yogurt、LagrangeV2 客户端退出后,核心没有及时更新连接状态,导致日志已经出现 connection refused,前端仍显示“已连接”。

本 PR 在核心侧补充连接检测,保持原版 Milky SDK 及 go.modgo.sum 不变,无需修改或升级第三方库。

主要改动:

  • 每 3 秒检查 Milky 登录 API,探测超时为 3 秒。服务不可用时标记“断开”;恢复后还需确认 WebSocket 连接成功,才显示“已连接”。
  • 由核心接管重连,关闭 SDK 自动重连,避免两套重连同时运行。暂时掉线保留账号的启用状态;停用或重新登录时取消旧监测。
  • 处理内置客户端进程退出及 QQ 账号离线事件,并隔离旧进程、旧会话的状态更新,避免覆盖新连接。

连接监测与账号状态管理分开实现,后续 SDK 支持原生连接通知后,可以替换监测实现,保留共用的状态与进程管理逻辑。

验证:

  • Milky 相关回归测试及竞态检测通过。
  • 覆盖 API 掉线、仅 WebSocket 网关故障、恢复、QQ 离线及监测取消场景。
  • 本地模拟客户端验证前端能够从“已连接”变为“断开”,恢复后重新显示“已连接”。

这是轮询方案,识别掉线存在检测延迟;无法立即识别尚未产生读写错误的静默 WebSocket 故障,但是已可以基本满足状态识别的需求。

Sourcery 总结

改进 Milky 连接状态跟踪,并增强缓存日志上传功能,使其能够应对链接过期或暂时无法访问的情况。

Bug 修复:

  • 修复 Milky 服务、内置客户端进程退出及 QQ 离线后账号状态未及时更新的问题。
  • 修复缓存日志链接失效时无法自动重新上传,以及上传失败时缺少缓存链接回退的问题。

功能增强:

  • 统一由核心管理 Milky 连接健康检查与重连,并避免旧会话或旧进程覆盖新连接状态。
  • 增加 Milky API、WebSocket、账号离线和进程生命周期场景的回归覆盖。

测试:

  • 新增 Milky 连接状态、恢复、离线、取消监测及旧进程隔离测试。
  • 新增缓存日志链接探测、重新上传和失败回退测试。
Original summary in English

Sourcery 总结

改进 Milky 连接状态跟踪并增强缓存日志上传的可靠性。

Bug 修复:

  • 修复 Milky 服务、内置客户端进程退出或 QQ 账号离线后连接状态未及时反映到前端的问题。
  • 修复缓存日志链接失效时无法自动重新上传,以及重新上传失败时无法回退到缓存链接的问题。

增强功能:

  • 由核心统一管理 Milky 连接健康检查、重连和账号状态,避免旧会话或旧客户端进程覆盖新连接状态。

测试:

  • 补充 Milky 连接断开、恢复、账号离线、监测取消、启动失败及旧进程隔离的回归测试。
  • 补充缓存日志链接有效性探测、重新上传和失败回退测试。
Original summary in English

Sourcery 摘要

提高 Milky 连接状态的准确性,并增强缓存日志链接在过期和后端故障时的可靠性。

错误修复:

  • 修复 Milky 账户在服务中断、内置客户端退出或 QQ 离线事件后仍保持连接状态的问题。
  • 在缓存日志上传链接过期时恢复上传,并在无法确认重新上传成功时回退到缓存链接。

改进:

  • 集中管理 Milky 的健康监控、重连和生命周期状态,同时隔离过期会话和客户端进程,避免其影响当前连接。

测试:

  • 增加对 Milky 断开连接、恢复连接、离线事件、已取消的监控、启动失败和过期进程隔离的回归测试覆盖。
  • 增加对缓存日志 URL 验证、重新上传和回退行为的测试覆盖。
Original summary in English

Summary by Sourcery

Improve Milky connection state accuracy and make cached log links resilient to expiration and backend failures.

Bug Fixes:

  • Fix Milky account states remaining connected after service outages, built-in client exits, or QQ offline events.
  • Recover cached log uploads when links expire and fall back to cached links when re-uploading cannot be confirmed.

Enhancements:

  • Centralize Milky health monitoring, reconnection, and lifecycle state management while isolating stale sessions and client processes from current connections.

Tests:

  • Add regression coverage for Milky disconnection, recovery, offline events, cancelled monitoring, startup failures, and stale process isolation.
  • Add coverage for cached log URL validation, re-uploading, and fallback behavior.
Original summary in English

保持原版 SDK 与依赖文件不变,由核心健康检查接管重连并更新连接状态。补充内置进程退出、QQ 离线及旧会话隔离处理;状态管理与最终 SDK 通知方案共用。
@sourcery-ai

sourcery-ai Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

审查者指南

本 PR 在不修改 Milky SDK 及第三方依赖的前提下,由核心接管 Milky 连接健康检查与重连,并通过会话/进程 generation 和生命周期锁隔离过期状态;同时增强故事日志缓存链接校验及上传失败降级,并补充相关回归测试。

Milky 连接健康监测与恢复时序图

sequenceDiagram
    participant Monitor as ConnectionMonitor
    participant Core as PlatformAdapterMilky
    participant API as MilkyRESTAPI
    participant WS as MilkyWebSocket
    participant UI as EndpointState

    loop Every 3 seconds
        Monitor->>API: probeMilkyHealth()
        alt Health check fails
            Monitor->>Core: onMilkyConnectionChange(session, false)
            Core->>UI: StateDisconnected
            Monitor->>WS: closeMilkyTransport(session)
        else Health check succeeds
            Monitor->>Core: openMilkyTransport(session)
            Core->>WS: session.Open()
            alt WebSocket reconnects
                WS-->>Core: connection established
                Core->>UI: StateConnected
            else WebSocket handshake fails
                Core->>UI: StateDisconnected
            end
        end
    end
Loading

Milky 会话生命周期状态图

stateDiagram-v2
    [*] --> Connecting: startMilkySession()
    Connecting --> Connected: finishMilkySession() and WebSocket connected
    Connecting --> ConnectionFailed: openMilkyTransport() fails
    Connected --> Disconnected: probeMilkyHealth() fails
    Connected --> Disconnected: onMilkyBotOffline()
    Disconnected --> Connected: probeMilkyHealth() succeeds and openMilkyTransport() succeeds
    Disconnected --> Disconnected: REST health succeeds but WebSocket reconnect fails
    Connected --> [*]: stopMilkySession()
    Disconnected --> [*]: stopMilkySession()
    ConnectionFailed --> [*]: stopMilkySession()
Loading

缓存故事日志 URL 校验与降级流程图

flowchart TD
    A[Upload log] --> B[Load cached URL and timestamps]
    B --> C{Cached URL is newer than log?}
    C -- No --> D[Upload updated log]
    C -- Yes --> E["checkCachedLogURL()"]
    E --> F{"probeCachedLogURL()"}
    F -- Alive --> G[Return cached URL]
    F -- Missing --> D
    F -- Unknown --> D
    D --> H{Upload succeeds?}
    H -- Yes --> I[Save upload metadata and return new URL]
    H -- No --> J["fallbackToCachedLogURL()"]
    J -- Cache status unknown --> K[Return cached URL with warning]
    J -- Otherwise --> L[Return upload error]
Loading

文件级变更

变更 详情 文件
增加 Milky 会话健康监测,并将连接、账号在线及启用状态解耦管理。
  • 关闭 SDK 自动重连,由核心每 3 秒轮询登录 API,使用 3 秒超时判断服务可用性。
  • 恢复时重新建立 WebSocket,只有 REST 与 WebSocket 均可用且会话有效才显示已连接。
  • 处理连接失败、QQ 离线、消息恢复及监测取消,临时掉线不清除账号启用状态。
dice/platform_adapter_milky.go
dice/platform_adapter_milky_state.go
dice/platform_adapter_milky_transport.go
增强内置 Milky 客户端进程与会话生命周期隔离,防止旧实例覆盖新实例状态。
  • 使用进程 generation 标识隔离旧进程、旧登录回调和旧会话。
  • 统一处理进程启动、退出、停止及超时等待,避免重复回收子进程。
  • 在 Yogurt、LagrangeV2 进程退出或账号停用、重登录时同步清理连接监测和状态。
dice/platform_adapter_milky.go
dice/platform_adapter_milky_helper.go
dice/platform_adapter_milky_state.go
为 Milky 连接状态、恢复流程及进程竞争场景补充回归与竞态覆盖。
  • 覆盖 API 掉线、WebSocket 网关故障、恢复、QQ 离线、监测取消和非法 API 响应。
  • 验证内置客户端旧进程退出不会清除新进程的连接状态。
dice/platform_adapter_milky_state_test.go
dice/platform_adapter_milky_transport_test.go
改进故事日志缓存链接校验和上传失败时的降级策略。
  • 上传前通过 load_data 探测缓存链接,区分有效、明确失效和无法确认三种结果。
  • 缓存链接无法确认且重新上传失败时返回旧链接,并向用户提示其可能仍有效。
  • 覆盖不同后端路径、鉴权、HTTP 状态、网络失败及非法缓存链接场景。
dice/storylog/storylog.go
dice/storylog/upload_v1.go
dice/storylog/upload_v105.go
dice/storylog/cache_validation_test.go
dice/ext_log_upload_cache_test.go

提示与命令

与 Sourcery 交互

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以在审查评论中回复 @sourcery-ai issue,以根据该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,以随时生成或重新生成标题。
  • 生成拉取请求摘要: 在拉取请求正文的任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,以随时生成或重新生成摘要。
  • 生成审查者指南: 在拉取请求中评论 @sourcery-ai guide,以随时生成或重新生成审查者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,以解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,此功能会很有用。
  • 忽略所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,以忽略所有现有的 Sourcery 审查。如果你想从新的审查开始,这一功能尤其有用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义使用体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南及其他功能。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 在不修改 Milky SDK 及第三方依赖的前提下,由核心接管 Milky 连接健康检查与重连,并通过会话/进程 generation 和生命周期锁隔离过期状态;同时增强故事日志缓存链接校验及上传失败降级,并补充相关回归测试。

Sequence diagram for Milky connection health monitoring and recovery

sequenceDiagram
    participant Monitor as ConnectionMonitor
    participant Core as PlatformAdapterMilky
    participant API as MilkyRESTAPI
    participant WS as MilkyWebSocket
    participant UI as EndpointState

    loop Every 3 seconds
        Monitor->>API: probeMilkyHealth()
        alt Health check fails
            Monitor->>Core: onMilkyConnectionChange(session, false)
            Core->>UI: StateDisconnected
            Monitor->>WS: closeMilkyTransport(session)
        else Health check succeeds
            Monitor->>Core: openMilkyTransport(session)
            Core->>WS: session.Open()
            alt WebSocket reconnects
                WS-->>Core: connection established
                Core->>UI: StateConnected
            else WebSocket handshake fails
                Core->>UI: StateDisconnected
            end
        end
    end
Loading

State diagram for Milky session lifecycle

stateDiagram-v2
    [*] --> Connecting: startMilkySession()
    Connecting --> Connected: finishMilkySession() and WebSocket connected
    Connecting --> ConnectionFailed: openMilkyTransport() fails
    Connected --> Disconnected: probeMilkyHealth() fails
    Connected --> Disconnected: onMilkyBotOffline()
    Disconnected --> Connected: probeMilkyHealth() succeeds and openMilkyTransport() succeeds
    Disconnected --> Disconnected: REST health succeeds but WebSocket reconnect fails
    Connected --> [*]: stopMilkySession()
    Disconnected --> [*]: stopMilkySession()
    ConnectionFailed --> [*]: stopMilkySession()
Loading

Flow diagram for cached story-log URL validation and fallback

flowchart TD
    A[Upload log] --> B[Load cached URL and timestamps]
    B --> C{Cached URL is newer than log?}
    C -- No --> D[Upload updated log]
    C -- Yes --> E["checkCachedLogURL()"]
    E --> F{"probeCachedLogURL()"}
    F -- Alive --> G[Return cached URL]
    F -- Missing --> D
    F -- Unknown --> D
    D --> H{Upload succeeds?}
    H -- Yes --> I[Save upload metadata and return new URL]
    H -- No --> J["fallbackToCachedLogURL()"]
    J -- Cache status unknown --> K[Return cached URL with warning]
    J -- Otherwise --> L[Return upload error]
Loading

File-Level Changes

Change Details Files
增加 Milky 会话健康监测,并将连接、账号在线及启用状态解耦管理。
  • 关闭 SDK 自动重连,由核心每 3 秒轮询登录 API,使用 3 秒超时判断服务可用性。
  • 恢复时重新建立 WebSocket,只有 REST 与 WebSocket 均可用且会话有效才显示已连接。
  • 处理连接失败、QQ 离线、消息恢复及监测取消,临时掉线不清除账号启用状态。
dice/platform_adapter_milky.go
dice/platform_adapter_milky_state.go
dice/platform_adapter_milky_transport.go
增强内置 Milky 客户端进程与会话生命周期隔离,防止旧实例覆盖新实例状态。
  • 使用进程 generation 标识隔离旧进程、旧登录回调和旧会话。
  • 统一处理进程启动、退出、停止及超时等待,避免重复回收子进程。
  • 在 Yogurt、LagrangeV2 进程退出或账号停用、重登录时同步清理连接监测和状态。
dice/platform_adapter_milky.go
dice/platform_adapter_milky_helper.go
dice/platform_adapter_milky_state.go
为 Milky 连接状态、恢复流程及进程竞争场景补充回归与竞态覆盖。
  • 覆盖 API 掉线、WebSocket 网关故障、恢复、QQ 离线、监测取消和非法 API 响应。
  • 验证内置客户端旧进程退出不会清除新进程的连接状态。
dice/platform_adapter_milky_state_test.go
dice/platform_adapter_milky_transport_test.go
改进故事日志缓存链接校验和上传失败时的降级策略。
  • 上传前通过 load_data 探测缓存链接,区分有效、明确失效和无法确认三种结果。
  • 缓存链接无法确认且重新上传失败时返回旧链接,并向用户提示其可能仍有效。
  • 覆盖不同后端路径、鉴权、HTTP 状态、网络失败及非法缓存链接场景。
dice/storylog/storylog.go
dice/storylog/upload_v1.go
dice/storylog/upload_v105.go
dice/storylog/cache_validation_test.go
dice/ext_log_upload_cache_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你好——我发现了 2 个问题

AI Agent 提示词
请处理本次代码审查中的评论:

## 个别评论

### 评论 1
<location path="dice/platform_adapter_milky_helper.go" line_range="245" />
<code_context>
 	conn := ep.Adapter.(*PlatformAdapterMilky)
+	pa := conn
+	BuiltinMilkyClientKill(d, ep)
+	generation, done := pa.beginMilkyProcess()
 	doServe := func() {
-		if ep.Platform == "QQ" {
</code_context>
<issue_to_address>
**问题 (bug_risk):** beginMilkyProcess 会将端点设置为 Connecting 并创建 processDone,但此后的设置失败(GetRandomFreePort、配置不受支持或 WriteFile 失败)会直接返回,而不会调用 failMilkyProcessStart 或关闭 done。账号会一直卡在 Connecting 状态,之后 BuiltinMilkyClientKill 会等待永远不会关闭的 processDone 通道,直到五秒超时。

**触发条件:** 当内嵌的 Milky 客户端在子进程启动前无法分配端口或写入配置时。

**建议修复:** 让 beginMilkyProcess 之后的每个返回路径都经过统一的启动失败清理逻辑,将连接标记为失败,并关闭或替换 processDone。
</issue_to_address>

### 评论 2
<location path="dice/platform_adapter_milky.go" line_range="160" />
<code_context>
+	log := zap.S().Named(logger.LogKeyAdapter)
+	wsGateway := strings.TrimSuffix(pa.WsGateway, "/")
+	restGateway := strings.TrimSuffix(pa.RestGateway, "/")
+	session, err := milky.New(wsGateway, restGateway, pa.Token, log.Named(logger.LogKeyAdapter))
 	if err != nil {
 		log.Errorf("Milky SDK initialization failed: %v", err)
</code_context>
<issue_to_address>
**问题 (broader_impact):** 如果 milky.New 返回初始化错误,serveMilky 会立即返回,而不会更新端点状态,也不会禁用失败的会话或以其他方式标记该会话。DoRelogin 会使端点停留在 Connecting 状态,而 SetEnable(true) 可能会继续显示之前的 Connected 状态,尽管实际上并未创建会话。

**触发条件:** 当 Milky SDK 在 New 期间拒绝已配置的网关或令牌时。

**建议修复:** 在 New 的错误路径返回前设置 StateConnectionFailed(以及适当的 Enable 值),或者让调用方一致地处理非零的 Serve 返回结果。
</issue_to_address>

Sourcery 评估

需要人工审查。 有 2 个发现需要优先处理;此外,新的健康监视器以及会话/进程代际机制可能会关闭一个可用的 Milky 连接、拒绝有效的恢复,或让过时的生命周期回调使账号保持断开状态或被错误地报告为已连接;禁用 SDK 的重连路径会加剧此类竞态的影响。回滚可以移除新行为,但在错误状态生效期间发生的任何中断或漏收消息,都无法通过回滚恢复。

阻塞性发现:dice/platform_adapter_milky_helper.go:245dice/platform_adapter_milky.go:160


Sourcery 对开源项目免费——如果您喜欢我们的审查,请考虑分享 ✨
Original comment in English

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dice/platform_adapter_milky_helper.go" line_range="245" />
<code_context>
 	conn := ep.Adapter.(*PlatformAdapterMilky)
+	pa := conn
+	BuiltinMilkyClientKill(d, ep)
+	generation, done := pa.beginMilkyProcess()
 	doServe := func() {
-		if ep.Platform == "QQ" {
</code_context>
<issue_to_address>
**issue (bug_risk):** beginMilkyProcess sets the endpoint to Connecting and creates processDone, but setup failures after this point (GetRandomFreePort, unsupported configuration, or WriteFile failure) return without calling failMilkyProcessStart or closing done. The account remains stuck in Connecting, and later BuiltinMilkyClientKill waits for the never-closed processDone channel until its five-second timeout.

**Triggers:** When an embedded Milky client cannot allocate its port or write its configuration before the child process starts.

**Suggested fix:** Route every return after beginMilkyProcess through a shared startup-failure cleanup that marks the connection failed and closes or replaces processDone.
</issue_to_address>

### Comment 2
<location path="dice/platform_adapter_milky.go" line_range="160" />
<code_context>
+	log := zap.S().Named(logger.LogKeyAdapter)
+	wsGateway := strings.TrimSuffix(pa.WsGateway, "/")
+	restGateway := strings.TrimSuffix(pa.RestGateway, "/")
+	session, err := milky.New(wsGateway, restGateway, pa.Token, log.Named(logger.LogKeyAdapter))
 	if err != nil {
 		log.Errorf("Milky SDK initialization failed: %v", err)
</code_context>
<issue_to_address>
**issue (broader_impact):** If milky.New returns an initialization error, serveMilky returns immediately without updating the endpoint state or disabling/otherwise marking the failed session. DoRelogin leaves the endpoint in Connecting, while SetEnable(true) can leave the previous Connected state visible even though no session was created.

**Triggers:** When the Milky SDK rejects the configured gateway or token during New.

**Suggested fix:** Set StateConnectionFailed (and the appropriate Enable value) before returning from the New error path, or have callers handle the nonzero Serve result consistently.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and the new health monitor and session/process-generation machinery can close a usable Milky connection, reject a valid recovery, or let a stale lifecycle callback leave the account disconnected or falsely reported as connected; disabling the SDK's reconnect path increases the impact of such a race. Reverting removes the new behavior, but any outage or missed messages that occurred while the wrong state was active cannot be recovered by the revert.

Blocking findings: dice/platform_adapter_milky_helper.go:245, dice/platform_adapter_milky.go:160


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread dice/platform_adapter_milky_helper.go
Comment thread dice/platform_adapter_milky.go
统一清理端口分配、配置生成、目录创建和文件写入失败后的登录状态与完成通道,并隔离旧启动尝试。补充实际配置写入失败、子进程启动失败及无效网关入口的回归测试。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant