工具状态失踪之谜:EventBus事件漏接与asyncio.Lock并发陷阱双线诊断

工具状态失踪之谜:EventBus 事件漏接与 asyncio.Lock 并发陷阱双线诊断

第二季系列文章第 2 篇(总第 19 篇) - EventBus · asyncio.Lock · 异步生成器 · 并发序列化 · 跨端消息隔离


📚 专栏信息

《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏 · 第二季

本文是模块五·问题诊断实战的第 2 篇,还原一次跨越桌面端与移动端的双线 Bug 诊断过程------一条线是"远程 PWA 请求工具状态不显示",另一条线是"多客户端并发导致 Session 历史交叉污染"。两条看似独立的问题,最终指向同一个设计缺陷:本地请求与远程请求走了不同的代码路径,导致事件订阅断裂和数据竞争。


📝 摘要

本文结构概览

从"远程 PWA 发消息,桌面端工具卡片静默"的现象出发,逐步拆解 WeClaw 的事件订阅架构,深挖 RemoteBridgeClient 与 GuiAgent 之间的协作盲区;同时揭示多客户端并发场景下的 Session 竞争风险,最终给出基于 EventBus 统一事件路径 + asyncio.Lock 请求序列化的双线修复方案。

背景

WeClaw 桌面端同时接收两种来源的 AI 请求:本地用户直接操作 UI 发起对话,以及远程 PWA 用户通过 WebSocket 桥接发来请求。两种请求共享同一个 Agent 实例,但走的是完全不同的代码路径。

核心问题

为什么本地请求工具状态正常,而远程 PWA 请求工具状态完全静默?更隐蔽的是:多客户端并发时 AI 的回复质量为什么会莫名下降?

解决方案

为 RemoteBridgeClient 引入远程请求生命周期事件(remote_request_started/ended),通过 EventBus 统一本地/远程的事件订阅路径;同时引入 asyncio.Lock 序列化所有 chat 请求,防止 Session 历史被并发写入污染。

关键成果

  • 远程 PWA 请求工具状态实时显示(📱 前缀标识来源)
  • 多客户端并发请求串行化,Session 历史零污染
  • PWA 端收到排队通知(⏳),用户体验透明
  • TypeScript 类型安全扩展,支持 queued 消息

适合读者:使用 asyncio / EventBus 进行事件驱动架构设计,或在多客户端共享状态场景下处理并发控制的开发者

阅读时长:约 15 分钟

关键词EventBusasyncio.Lockasync generator并发序列化RemoteBridgeClientSession 污染PWAQt 信号


一、问题现场还原 ------ 两个维度的"静默"

1.1 现象一:远程 PWA 请求工具状态不显示

用户报告:使用 PWA 手机端发送消息,桌面端右侧的"工具执行状态"面板完全静默------既不显示正在执行什么工具,也不显示工具执行结果。而本地用户操作时,工具卡片一切正常。

日志却显示工具确实被执行了:

复制代码
# 桌面端日志
2026-03-21 15:23:01 | Agent | INFO | 工具 file_search.search → success (342ms)
2026-03-21 15:23:05 | Agent | INFO | 工具 weather.query → success (891ms)

工具执行了,但 UI 没收到通知。

1.2 现象二:多客户端并发时回复"答非所问"

某用户同时使用桌面端和两台手机 PWA 端,在三端几乎同时发送消息后,发现 AI 的回复内容混乱------似乎把多个用户的消息混在一起了。

复制代码
手机A: "帮我写一首诗"         → AI 回复包含"天气"相关内容 ❌
桌面:  "今天天气如何"          → AI 回复包含"诗"的内容      ❌
手机B: "北京温度"              → AI 回复正常                  ✅

这说明 AI 看到了混乱的对话历史,导致上下文污染。

1.3 两个问题的共同指向

表面上是两个独立 Bug,深入分析后发现它们有共同根源------本地请求与远程请求走了不同的代码路径

复制代码
本地请求
  → 用户操作 UI
    → message_sent 信号
      → GuiAgent.chat()
        → 临时订阅 tool_call/tool_result 事件 ✅
          → 驱动工具状态面板

远程请求
  → WebSocket 桥接
    → RemoteBridgeClient._handle_chat()
      → agent.chat_stream()           ← 完全绕过了 GuiAgent!
        → tool_call 事件被 emit
          → 但没有订阅者 ❌

本地请求通过 GuiAgent.chat() 临时订阅事件,远程请求直接调用 agent.chat_stream() 绕过了这层包装。


二、诊断过程 ------ 两条主线的逐步拆解

2.1 主线一:工具状态"失踪"的链路追踪

第一轮:确认事件是否被发射

从 Agent 源码入手,查看 tool_call 事件在哪里发射:

python 复制代码
# src/core/agent.py - 工具调用执行后发射事件
async def _execute_tool_call(self, tc, ...) -> ToolResult:
    # ... 执行逻辑 ...
    await self.event_bus.emit("tool_call", ToolCallEvent(
        tool_name=tool.name,
        action_name=action_name,
        arguments=arguments,
    ))
    # ... 执行工具 ...
    await self.event_bus.emit("tool_result", ToolResultEvent(
        tool_name=tool.name,
        action_name=action_name,
        output=result.output,
        status=result.status.value,
    ))

事件在 Agent 层被正确发射。那么问题一定在事件传递环节。

第二轮:追踪 GuiAgent 的事件订阅

查看 GuiAgent.chat() 如何订阅事件:

python 复制代码
# src/ui/gui_app.py - GuiAgent.chat() 中的临时订阅
async def chat(self, message: str) -> None:
    self.message_started.emit()
    
    # 临时订阅工具事件 ✅
    async def _on_tool_call(event_type, data):
        self.tool_call_started.emit(data.tool_name, data.action_name)
    
    async def _on_tool_result(event_type, data):
        self.tool_call_finished.emit(data.tool_name, data.action_name, ...)
    
    sub_tc = self._agent.event_bus.on("tool_call", _on_tool_call)
    sub_tr = self._agent.event_bus.on("tool_result", _on_tool_result)
    
    try:
        async for chunk in self._agent.chat_stream(message):
            self.message_chunk.emit(chunk)
    finally:
        self._agent.event_bus.off("tool_call", sub_tc)  # 取消订阅
        self._agent.event_bus.off("tool_result", sub_tr)

关键发现tool_call/tool_result 事件的订阅发生在 GuiAgent.chat() 的内部------这是一个临时订阅 ,只在 chat() 执行期间有效。

第三轮:对比远程请求的代码路径
python 复制代码
# src/remote_client/client.py - _stream_chat_response()
async def _stream_chat_response(self, request_id, user_id, content, ...):
    # 直接调用 agent.chat_stream(),没有 GuiAgent 包装
    async for chunk in self.agent.chat_stream(user_input=full_message):
        await self._send_stream_chunk(request_id, {"delta": chunk})
    # 没有任何事件订阅!

远程请求直接调用 agent.chat_stream(),根本没有经过 GuiAgent.chat(),因此临时订阅的事件处理器从未被注册

根因定位:RemoteBridgeClient 绕过了 GuiAgent 的事件订阅包装,导致工具事件无人监听。

2.2 主线二:Session 并发污染的根源

第一轮:确认 Session 是否共享
python 复制代码
# src/core/agent.py
class Agent:
    def __init__(self, ...):
        self._session_manager: SessionManager | None = None
    
    @property
    def session_manager(self):
        if self._session_manager is None:
            self._session_manager = SessionManager()
        return self._session_manager  # 只有一个!所有请求共享

所有请求共享同一个 SessionManager,同一个 current_session

第二轮:分析并发时序
复制代码
Task A (本地): session.add_message("user", "帮我写诗")
Task B (PWA):  session.add_message("user", "今天天气")
Task A: agent.chat_stream() → 调 LLM → 看到 [写诗, 天气] 顺序混乱
Task B: agent.chat_stream() → 调 LLM → 看到 [写诗, 天气] 同样混乱

add_message() 不是线程安全的,在 asyncio 并发协程中会出现交错写入,导致 LLM 看到的消息顺序不确定。

第三轮:确认无任何锁保护
python 复制代码
# Agent.__init__ - 没有 lock
self.model_registry = model_registry
self.tool_registry = tool_registry
self.event_bus = event_bus or EventBus()
# 没有 _chat_lock

没有任何并发保护机制。


三、解决方案 ------ 双线并行的完整修复

3.1 架构设计:引入远程请求生命周期事件

核心思路 :RemoteBridgeClient 在处理远程请求时,主动发射请求生命周期事件,使 GUI 能以统一的方式感知远程请求的开始和结束:

复制代码
RemoteBridgeClient                          GuiApp
     │                                         │
     │  remote_request_started (user, id)      │
     ├────────────────────────────────────────►│  设置状态 📱[PWA:用户名] 生成中...
     │                                         │
     │  tool_call / tool_result (由 Agent emit)│
     ├────────────────────────────────────────►│  追加日志 📱 ▶ xxx
     │                                         │
     │  remote_request_ended (id)               │
     └────────────────────────────────────────►│  设置状态 📱[PWA:用户名] 完成

这样,无论请求来自本地还是远程,GUI 都能通过同一套事件订阅来更新工具状态面板。

3.2 修复一:RemoteBridgeClient 新增生命周期事件

文件src/remote_client/client.py

新增辅助方法,获取用户名用于标识来源:

python 复制代码
def _get_username_for_user(self, user_id: str) -> str:
    """根据 user_id 查找 PWA 用户名。"""
    for conn in self._stats.pwa_connections:
        if conn.user_id == user_id and conn.username:
            return conn.username
    return user_id[:8] if user_id else "远程用户"

_stream_chat_response() 中发射事件:

python 复制代码
async def _stream_chat_response(self, request_id, user_id, content, ...):
    try:
        # 通知 GUI:远程请求开始
        if self.event_bus:
            username = self._get_username_for_user(user_id)
            await self.event_bus.emit("remote_request_started", {
                "user_id": user_id,
                "username": username,
                "request_id": request_id,
            })
        
        # 调用 Agent 流式处理(核心逻辑不变)
        async for chunk in self.agent.chat_stream(user_input=full_message):
            await self._send_stream_chunk(request_id, {"delta": chunk})
    
    except Exception as e:
        logger.error(f"聊天处理失败: {e}", exc_info=True)
        await self._send_error("CHAT_ERROR", str(e), request_id)
    
    finally:
        # 通知 GUI:远程请求结束(确保总是触发)
        if self.event_bus:
            await self.event_bus.emit("remote_request_ended", {
                "request_id": request_id,
                "user_id": user_id,
            })

设计亮点finally 块确保事件一定会被发射,即使中途异常退出。这比在正常路径中发射更健壮。

3.3 修复二:GuiApp 新增远程事件订阅

文件src/ui/gui_app.py

新增两个状态标志:

python 复制代码
# WinClawGuiApp.__init__
self._remote_request_active: bool = False  # 是否正在处理远程请求
self._remote_username: str = ""           # 当前远程用户名

新增事件订阅方法:

python 复制代码
def _setup_remote_events(self) -> None:
    """订阅远程 PWA 请求事件,实时更新工具执行面板并标注来源。
    
    远程 PWA 请求直接调用 agent.chat_stream() 而不经过 GuiAgent.chat(),
    因此需要在 agent.event_bus 上单独订阅,以驱动工具状态面板更新。
    工具日志条目将以 📱[PWA] 前缀标注,区别于本地请求。
    """
    if not self._agent:
        return

    event_bus = self._agent.event_bus

    async def on_remote_request_started(event_type, data):
        """远程请求开始:清空日志、设置来源标识。"""
        username = data.get("username") or data.get("user_id", "远程用户")
        self._remote_request_active = True
        self._remote_username = username
        self._window.set_tool_status(f"📱[PWA:{username}] 生成中...")
        self._window.clear_tool_log()
        self._window.add_tool_log(f"📱 [PWA 远程请求] 用户: {username}")

    async def on_remote_request_ended(event_type, data):
        """远程请求结束:标记完成状态。"""
        username = self._remote_username or "远程用户"
        self._remote_request_active = False
        self._remote_username = ""
        self._window.set_tool_status(f"📱[PWA:{username}] 完成")

    async def on_tool_call_remote(event_type, data):
        """工具调用开始(仅在远程请求期间生效)。"""
        if not self._remote_request_active:
            return
        self._window.set_tool_status(f"📱[PWA] 执行:{data.tool_name}.{data.action_name}")
        self._window.add_tool_log(f"📱 ▶ {data.tool_name}.{data.action_name}")

    async def on_tool_result_remote(event_type, data):
        """工具调用完成(仅在远程请求期间生效)。"""
        if not self._remote_request_active:
            return
        preview = (data.output or "")[:150] + ("..." if len(data.output) > 150 else "")
        self._window.add_tool_log(f"📱 ✔ {data.tool_name}.{data.action_name} → {preview}")

    event_bus.on("remote_request_started", on_remote_request_started)
    event_bus.on("remote_request_ended", on_remote_request_ended)
    event_bus.on("tool_call", on_tool_call_remote)
    event_bus.on("tool_result", on_tool_result_remote)

关键设计tool_call/tool_result 的订阅是永久的 (在 _setup_signals() 中调用一次),但处理逻辑中用 _remote_request_active 标志区分------只有远程请求期间才更新 GUI,本地请求仍然由 GuiAgent.chat() 的临时订阅处理,两者互不干扰。


四、并发控制 ------ asyncio.Lock 的正确打开方式

4.1 为什么不能用简单的队列?

有同学可能会问:为什么不直接用 asyncio.Queue 做请求队列?答案是:Queue 适合生产者-消费者模式(任务产生和消费分开),而这里是同一个 Agent 实例被多个调用方共享 ,需要的是互斥,不是排队。

4.2 锁的懒加载:绕过事件循环约束

asyncio.Lock 必须在事件循环已启动后才能创建。在 Agent.__init__ 中直接创建会报错:

python 复制代码
# ❌ 错误:在 __init__ 中创建锁
def __init__(self, ...):
    self._chat_lock = asyncio.Lock()  # RuntimeError: asyncio.run() cannot be called from a running event loop

解决方案:懒加载 property------在首次访问时,在已经运行的事件循环中创建锁:

python 复制代码
class Agent:
    def __init__(self, ...):
        self._chat_lock: asyncio.Lock | None = None  # 不在这里创建
    
    @property
    def chat_lock(self) -> asyncio.Lock:
        """懒加载聊天序列化锁(首次访问时在当前事件循环中创建)。"""
        if self._chat_lock is None:
            self._chat_lock = asyncio.Lock()
        return self._chat_lock

4.3 普通函数用 async with:最简洁

对于普通异步函数,直接用 async with 上下文管理器:

python 复制代码
# Agent._chat_impl() - 内部实现,已持有锁
async def chat(self, user_input: str) -> AgentResponse:
    async with self.chat_lock:
        return await self._chat_impl(user_input)  # 锁自动获取和释放

4.4 异步生成器:必须手动 acquire/release

这是本次修复的技术难点chat_stream() 返回的是异步生成器(async for ... yield),不能用 async with 包裹整个生成器------那样锁会在第一个 yield 后被持有,但永远不会释放(因为 with 块没有结束)。

正确做法:在生成器入口处 acquire,在 finally 中 release

python 复制代码
async def chat_stream(self, user_input: str) -> AsyncGenerator[str, None]:
    """流式处理用户输入,yield 文本片段。
    
    通过 chat_lock 序列化并发请求:
    锁在生成器整个生命周期内保持持有,确保 session 历史不被
    并发请求污染。后续请求会等待当前请求完成后再开始。
    """
    # 等待获取锁(串行化所有请求:本地 UI + 远程 PWA)
    await self.chat_lock.acquire()
    try:
        async for chunk in self._chat_stream_impl(user_input):
            yield chunk  # 每个 chunk 发出后锁仍然持有
    finally:
        self.chat_lock.release()  # 生成器完全结束后才释放

为什么要持有整个生命周期?

复制代码
第1个请求(持有锁):
  async for chunk in chat_stream():
    yield "部"        → 锁持有 ✅
    yield "分内容"     → 锁持有 ✅
    session.add_message(...)  → 锁保护 ✅
    yield "更多内容"  → 锁持有 ✅
  → 循环结束
  → finally: lock.release()

第2个请求(等待锁):
  → await self.chat_lock.acquire()  → 现在获取到锁了
  → 开始自己的流式输出

这样保证了整个请求处理过程中 session 历史不会被其他请求干扰

4.5 排队通知:优雅的用户体验

光有锁还不够------如果远程 PWA 发来请求后桌面端正在处理另一个请求,PWA 端应该立即知道"我在排队",而不是"假死等待"。

python 复制代码
async def _handle_chat(self, request_id: str, payload: dict):
    # 如果 Agent 已持有锁,先通知 PWA 排队等待
    agent = self.agent
    if (hasattr(agent, '_chat_lock') and 
        agent._chat_lock is not None and 
        agent._chat_lock.locked()):
        
        logger.info(f"Agent 正忙,请求排队: request={request_id[:8]}")
        await self._send_response(request_id, {
            "type": "queued",
            "payload": {
                "message": "当前 AI 正在处理其他请求,您的请求已排队,请稍等...",
                "request_id": request_id,
            }
        })
    
    # 继续创建处理任务(会自动等待锁)
    stream_task = asyncio.create_task(
        self._stream_chat_response(...)
    )

PWA 前端处理 queued 消息:

typescript 复制代码
// winclaw_server/pwa/src/stores/chat.ts
} else if (data.type === 'queued') {
    // AI 正忙,请求已排队 --- 插入系统提示
    messages.value.push({
        message_id: generateUUID(),
        session_id: sessionId,
        role: 'assistant',
        content: `⏳ ${queuedHint}`,
        created_at: new Date().toISOString(),
        metadata: { isQueueNotice: true }
    })
}

4.6 完整的数据流

复制代码
┌────────────────────────────────────────────────────────────────────┐
│                        PWA 手机端                                   │
│  用户发送消息 → WebSocket → server /ws/bridge                       │
└──────────────────────────────┬─────────────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────────┐
│                 RemoteBridgeClient._handle_chat()                 │
│                                                                  │
│  检测 _chat_lock 是否被占用                                       │
│      ├─ 被占用 → 发送 type:"queued" → PWA 显示 ⏳ 提示            │
│      │            任务仍然创建(会等待锁)                         │
│      └─ 未占用 → 立即开始处理                                      │
│                    │                                              │
│                    ▼                                              │
│  _stream_chat_response(request_id, user_id, content)               │
│      │                                                             │
│      ├─ emit("remote_request_started", {username})                │
│      │     → event_bus → _setup_remote_events()                   │
│      │           → set_tool_status("📱[PWA:用户名] 生成中...")     │
│      │                                                             │
│      ├─ await agent.chat_stream(full_message) ← 持有 _chat_lock  │
│      │     │                                                      │
│      │     ├─ session.add_message() ← 锁保护,写入安全            │
│      │     ├─ emit("tool_call") → 事件总线                         │
│      │     │     → _setup_remote_events() → add_tool_log("📱 ▶") │
│      │     ├─ LLM 推理(流式 yield chunks)                        │
│      │     ├─ emit("tool_result")                                │
│      │     │     → add_tool_log("📱 ✔ ...")                      │
│      │     └─ yield final_response                                │
│      │                                                             │
│      └─ emit("remote_request_ended")                              │
│            → set_tool_status("📱[PWA:用户名] 完成")                │
│                                                                  │
│  finally: _chat_lock.release()  ← 下一个等待的请求可以开始了       │
└──────────────────────────────────────────────────────────────────┘

五、深入理解 ------ asyncio 协程调度的关键特性

5.1 什么是 asyncio.Lock?

asyncio.Lock 是协程级别的互斥锁,与线程锁类似但专为协程设计:

python 复制代码
lock = asyncio.Lock()

async def task_a():
    await lock.acquire()
    try:
        # 临界区:session 操作
        await session.add_message(...)
        await llm.call()
    finally:
        lock.release()

async def task_b():
    await lock.acquire()  # 如果 task_a 持有锁,这里会等待
    try:
        await session.add_message(...)
    finally:
        lock.release()

关键特性await lock.acquire() 在锁被占用时会挂起当前协程,而不是忙等(busy-wait)。这意味着事件循环可以在这段时间里调度其他协程,CPU 效率极高。

5.2 异步生成器的特殊性

异步生成器(async def ... yield)是一个状态机 ------每次 yield 时协程被挂起,但锁仍然持有

python 复制代码
async def chat_stream(msg):
    await lock.acquire()          # 持有锁
    try:
        async for chunk in llm.stream(msg):
            yield chunk            # 每次 yield 后锁仍持有
    finally:
        lock.release()            # 生成器结束后才释放

这正是我们想要的行为:整个请求处理期间(从开始到最后一个 chunk 被 yield 之前)锁都被持有 ,保护 session.add_message() 的原子性。

5.3 为什么不能用 async with 包裹生成器?

python 复制代码
# ❌ 错误做法:async with 不能用于异步生成器
async def chat_stream(msg):
    async with self.chat_lock:  # 锁在这里被获取
        async for chunk in llm.stream(msg):
            yield chunk  # 每个 chunk yield 后,锁在 with 块内持有
                       # 但生成器外部无法知道何时结束
                       # 实际上 Python 会在这里报 "TypeError: ...

Python 不允许对异步生成器使用 async with------因为 __aexit__ 只在生成器完全结束 后才会被调用,而在此之前 async with 块已经结束了。


六、诊断思路总结

6.1 工具状态"失踪"的排查 Checklist

复制代码
□ 1. 确认事件是否被发射:查看 Agent 源码中的 emit() 调用
□ 2. 确认事件总线是否正常:event_bus.emit() 返回值
□ 3. 追踪事件订阅路径:谁在监听这个事件?
□ 4. 对比调用路径差异:本地 vs 远程请求走了哪些不同的函数?
□ 5. 查找"特殊路径":哪些代码绕过了事件订阅包装层?
□ 6. 设计统一订阅点:将所有来源的事件在同一个地方统一处理

6.2 并发问题的排查 Checklist

复制代码
□ 1. 确认数据是否共享:多个协程/线程访问同一份数据?
□ 2. 检查是否有并发保护:锁、队列、原子操作?
□ 3. 分析时序图:多个操作交错执行会发生什么?
□ 4. 验证"最坏情况":极端并发下系统行为是否正确?
□ 5. 用户体验设计:并发等待时是否有反馈(排队通知)?

6.3 异步生成器 + 锁的最佳实践

复制代码
1. 在生成器入口 acquire()
2. 在 try...finally 中执行生成器逻辑
3. 在 finally 中 release()
4. 避免在生成器中使用 async with
5. 考虑使用 with await 形式的上下文管理器(仅限普通异步函数)

七、总结

7.1 核心要点回顾

3 个关键认知

  1. 事件订阅路径必须统一:本地请求和远程请求走了不同的代码路径时,一定要检查事件订阅是否也"分叉"了。最优解是在共享的事件总线上统一订阅,避免"临时订阅"的局部路径遗漏。

  2. asyncio.Lock 的正确用法

    • 普通异步函数 → async with lock:
    • 异步生成器 → await lock.acquire() + finally: lock.release()
    • 锁必须懒加载(事件循环启动后才能创建)
  3. 并发共享状态的危害:多协程并发访问共享可变状态(Session 历史)时,必须序列化写入操作。数据竞争的症状往往不是崩溃,而是"AI 答非所问"------这种隐式错误更难调试。

1 个核心公式

复制代码
远程 PWA 工具状态显示
  = remote_request_started 事件(请求开始)
  + tool_call/tool_result 事件(工具执行)
  + remote_request_ended 事件(请求结束)
  + _remote_request_active 标志(区分来源)
  + 📱 前缀标识(视觉区分)

并发 Session 安全
  = asyncio.Lock.acquire()(入口)
  + session.add_message()(被保护的操作)
  + Lock.release()(finally 块)
  + type:"queued" 通知(用户体验)

7.2 下一步学习方向

前置知识

  • ✅ Python asyncio 基础(协程、事件循环、异步生成器)
  • ✅ EventBus 事件总线设计模式
  • ✅ PySide6/PyQt 信号与槽机制

后续主题

  • 📖 下一篇:《第 20 篇:多设备 Session 隔离------为每个 PWA 用户分配独立会话空间》
  • 🔜 下下一篇:《第 21 篇:流式响应的艺术------SSE 与 WebSocket 的深度融合》

扩展阅读


下期预告:《第 20 篇:多设备 Session 隔离》

  • 🔐 每个 PWA 用户拥有独立 Session,拒绝历史污染
  • 🏷️ Session 路由:根据 user_id 动态选择会话上下文
  • ⚡ 懒加载策略:按需创建会话,避免内存浪费
  • 📊 多会话并发下的 Token 用量分摊统计

敬请期待!


附录 A:完整代码清单

文件路径 变更类型 说明
src/remote_client/client.py 修改 新增 remote_request_started/ended 事件发射、排队通知
src/ui/gui_app.py 修改 新增 _remote_request_active 标志、_setup_remote_events() 订阅方法
src/core/agent.py 修改 新增 _chat_lock 懒加载锁、chat()/chat_stream() 加锁保护
winclaw_server/pwa/src/stores/chat.ts 修改 处理 type:"queued" 消息、扩展 isQueueNotice 类型

关键方法

  • RemoteBridgeClient._get_username_for_user() - 用户名查询
  • RemoteBridgeClient._stream_chat_response() - 生命周期事件发射
  • RemoteBridgeClient._handle_chat() - 排队通知
  • WinClawGuiApp._setup_remote_events() - 远程事件订阅
  • Agent.chat_lock (property) - 懒加载锁
  • Agent.chat() - 加锁调用
  • Agent.chat_stream() - 加锁异步生成器

附录 B:参考资料

  1. Python asyncio 官方文档
  2. PEP 525 -- Asynchronous Generators
  3. EventBus 事件总线设计模式
  4. PySide6 Signals & Slots
  5. 上一篇:《第 18 篇:TTS 静默之谜》

版权声明:本文为 CSDN 博主「翁勇刚」的原创文章,遵循 CC 4.0 BY-SA 版权协议,版权归作者所有。

原文链接https://blog.csdn.net/yweng18/article/details/159324083

相关推荐
不想看见4043 小时前
C++/Qt 代码规范指南
开发语言·qt
li星野3 小时前
QT模拟题:QT项目实践与架构设计(120分钟)
开发语言·qt
笑鸿的学习笔记8 小时前
qt-C++语法笔记之Qt中的delete ui、ui的本质与Q_OBJECT
c++·笔记·qt
特立独行的猫a8 小时前
ESP32小智AI的WebSocket 调试工具实现,小智AI后台交互过程揭秘(一、开篇介绍 )
人工智能·websocket·网络协议·esp32·小智ai
不想看见4049 小时前
Qt 框架中的信号与槽机制【详解】
服务器·数据库·qt
行者..................10 小时前
第2课:恢复出厂、掌握 Linux 基础命令并完成首次 GCC 编译
linux·qt·driver
Lhan.zzZ11 小时前
深入浅出 Qt 信号槽连接方式:从 AutoConnection 到 BlockingQueuedConnectionQt
开发语言·c++·qt
特立独行的猫a11 小时前
ESP32小智AI的WebSocket 调试工具的实现,小智AI后台交互过程揭秘(二、技术原理与实现过程详解 )
人工智能·websocket·网络协议·esp32·调试工具·小智ai
Ronin30511 小时前
【Qt窗口】Qt窗口
开发语言·qt·qt窗口