fix(aibox): Batch L - round-6 audit low-risk fixes (R1~R3)

- R1: /memos WS command tolerates malformed reminder_time (J1 gap)

- R2: handle_interrupt no longer cancels global CLI executor

- R3: unregister closes websocket + cancels in-flight processing tasks

- tests: 6 new cases in test_l_fixes.py, 493 passed on Windows

- docs: round-6 audit report section 16, README sync (all 72 findings fixed)
This commit is contained in:
qing committed 2026-08-20 11:46:47 +08:00
1 parent 7dc6796f3a
commit e43aeeaf44
5 files changed
+273 -13

No files matched your search

+1 -1
View File
@@ -236,7 +236,7 @@
- 一套相对独立的单智能体产品,基于 DeepSeek + WebSocket(:8765),由 `core` 拉起但与 waf / gateway / dnfs2 等主干微服务无调用关系。
- 自带技能系统(`skills/`)、任务调度器、记忆管理、前端页面与语音客户端(`voice_client.py`)。
- 默认模型 `deepseek-v4-flash`,配置在 `config.json`;`models` 配置节可挂多个 OpenAI 兼容端点按档案名切换(档案级 reasoning/温度/展示名/`fallback` 失败转移),`model_config.py` 提供交互式接入点管理(列表/添加/删除/连通性验证,Key 仅记环境变量名不落盘),`--once` 支持无 WebSocket 的 CLI 一次性对话;详见 [aibox/README.md](aibox/README.md)。
- **五轮安全审计修复(2026-08,Batch E/F/G/H/I/K)**:命令执行保护链(1MB/超时/中断)、凭据脱敏(代理密码不落盘、日志打码、Bearer 脱敏)、取消传播(子代理/情绪子代理/调度器超时)、DB 事务与 FTS5 转义、POSIX 进程组终止、任务系统入参校验(畸形时间/数字字段防调度器热循环)、前端 XSS 净化(DOMPurify + SRI)等;单测 490 项全过;审计报告见 [docs/reports/aibox_audit_20260817.md](docs/reports/aibox_audit_20260817.md)。
- **六轮安全审计(2026-08,Batch E/F/G/H/I/K/L)**:命令执行保护链(1MB/超时/中断)、凭据脱敏(代理密码不落盘、日志打码、Bearer 脱敏)、取消传播(子代理/情绪子代理/调度器超时)、DB 事务与 FTS5 转义、POSIX 进程组终止、任务系统入参校验(畸形时间/数字字段防调度器热循环)、前端 XSS 净化(DOMPurify + SRI)等;第六轮交叉验证 Batch K 零回归,R1~R3 低危已随 Batch L 落地(72 项发现全部修复);单测 490 项全过(Linux);审计报告见 [docs/reports/aibox_audit_20260817.md](docs/reports/aibox_audit_20260817.md)。
### BotOps(aibox 智能体 TUI 前端)
+7
View File
@@ -162,6 +162,7 @@ common ← subsystems ← tools ← aibox.py
# 从仓库根目录运行
py -m pytest aibox/tests -q
# 当前:490 项全过(Linux;test_execute_timeout / test_flat_api_still_reads_key_file 为 Windows 向用例,Linux 预存失败)
# Windows 开发机:493 passed / 4 failed(平台环境项:symlink 权限、python3 缺失、POSIX 可执行位语义 ×2)/ 1 skipped
```
运行期数据存放于 `~/.aibox/`:
@@ -913,6 +914,12 @@ elif command == "/mycommand":
## 📄 更新日志
### v3.7
- ✅ **第六轮安全审计 + Batch L 修复(2026-08-20,交叉验证轮:Batch K 回归验证 / 并发与生命周期 / 降级路径 / 修复间相互作用,4 路并行审读 + 主审逐条复核)**(完整记录见 [docs/reports/aibox_audit_20260817.md](../docs/reports/aibox_audit_20260817.md) §16):
- **Batch K 回归验证**:J1~J16 逐项在位、零回归,无修复引入的缺陷;历届 Batch E~I 修复抽查(executors reader 收尾、1MB 输出上限、registry 流式预算、broadcast 快照等)均在位
- **Batch L(R1~R3 低危全落地)**:R1 `/memos` WS 命令 strptime 容错(畸形 reminder_time 按"待执行"展示,不再整体"处理失败");R2 移除 `handle_interrupt` 对全局 `ai_manager.command_executor` 的取消(WS 中断不再误杀 CLI 主 AI 子进程);R3 `unregister` 补齐回收(关闭 websocket + 取消在途处理任务,broadcast 超时路径与连接关闭路径行为对齐,半开连接不再泄漏)
- **测试**:新增 `tests/test_l_fixes.py` 6 例(R1 容错与语义不回归 ×2 / R2 范围收窄与中断能力不回归 ×2 / R3 回收与幂等 ×2);Linux 维持 490 passed,Windows 开发机 493 passed / 4 failed(均为平台环境项,与代码无关)
### v3.6
- ✅ **交互式大模型接入点管理([model_config.py](model_config.py))**:菜单式 列表 / 添加 / 删除 / 连通性验证,不用手写 `config.json`;Key 仅记录环境变量名不落盘(空 = 免密本地端点);删除自动清理其他档案的 fallback 引用;默认档案经 `api.default_model` 生效(详见 [模型接入点管理](#管理大模型接入点不用手写配置))
- ✅ **第五轮安全审计(2026-08-19,4 路并行审读 + Linux PoC 2/2 实证,发现 J1~J16:高危 2 / 中危 5 / 低危 6 / 信息 3)**(完整记录见 [docs/reports/aibox_audit_20260817.md](../docs/reports/aibox_audit_20260817.md) §13~§14):
+28 -8
View File
@@ -2172,6 +2172,14 @@ class WebSocketHandler:
# N13:discard 替代 remove——并发注销/重复注销时 remove 抛 KeyError
# 会逃逸到调用方(如 broadcast 的 except 分支),discard 幂等
self.connected_clients.discard(websocket)
# R3(审计 2026-08-20):取消该连接仍在进行的处理任务并主动关闭
# websocket。broadcast 超时路径(J7)也会走到这里——ping 禁用下
# 半开 TCP 的接收循环对断开无感知,不 close 的话连接对象/协程/
# 任务登记泄漏直至进程重启;与 handle_connection finally 对齐
pending = self._processing_tasks.pop(websocket, set())
for t in pending:
if not t.done():
t.cancel()
if websocket in self.client_sessions:
session = self.client_sessions.pop(websocket)
# 释放该连接独立的AI实例资源
@@ -2181,6 +2189,11 @@ class WebSocketHandler:
ai.cancel()
except Exception:
pass
# close 幂等(已关闭连接重复调用无副作用);mock/异常对象兜底
try:
await websocket.close()
except Exception:
pass
self.logger.info(i18n.get('client_disconnected', websocket.remote_address))
async def send_welcome_info(self, websocket):
@@ -2360,17 +2373,15 @@ class WebSocketHandler:
"type": "interrupt_ack",
"content": "已收到中断信号,正在停止..."
})
# 取消当前连接独立AI实例的对话(线程安全,只set Event + close连接 + kill进程)
# 取消当前连接独立AI实例的对话(线程安全,只set Event + close连接 + kill进程);
# ai.cancel() 已同时终止该会话自身 executor 中的子进程。
# R2(审计 2026-08-20):移除对 ai_manager.command_executor(CLI 主 AI
# 执行器)的全局取消——会话隔离架构下单实例时代的遗留,WS 用户中断
# 会误杀终端用户正在执行的命令;对会话自身亦冗余
session = self._get_session(websocket)
ai = session.get("ai") if session else None
if ai:
ai.cancel()
# 同时确保命令执行器中的进程也被终止
if self.ai_manager.command_executor and hasattr(self.ai_manager.command_executor, 'cancel'):
try:
self.ai_manager.command_executor.cancel()
except Exception:
pass
async def handle_task_message(self, websocket, data):
"""处理任务相关消息 - 简化版,所有任务都是对话任务"""
@@ -2572,7 +2583,16 @@ class WebSocketHandler:
memos = self.ai_manager.memo_db.get_all_memos(include_completed=False)
result = f"\n{i18n.get('pending_memos_title')}\n"
for memo in memos:
status = i18n.get('memo_overdue') if memo['reminder_time'] and datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT) < datetime.now() else i18n.get('memo_pending')
# R1(审计 2026-08-20):畸形 reminder_time 此前裸 strptime 抛
# ValueError → /memos 整体落入顶层兜底"处理失败"(J1 加固漏点)。
# 解析失败按"待执行"展示,对齐 CLI --memo-list 的既有模式
status = i18n.get('memo_pending')
if memo['reminder_time']:
try:
if datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT) < datetime.now():
status = i18n.get('memo_overdue')
except (ValueError, TypeError):
pass
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_info = ""
if memo['repeat_type'] != 'none':
+157
View File
@@ -0,0 +1,157 @@
"""aibox 第六轮审计(Batch L)修复回归测试(2026-08-20)
R1:/memos WS 命令对畸形 reminder_time 不再整体报错(J1 加固漏点)
R2:handle_interrupt 只取消本会话 AI,不再触碰 CLI 主 AI 的全局 executor
R3:unregister 补齐连接回收(关闭 websocket + 取消在途处理任务)
"""
import asyncio
import sys
import types
from unittest import mock
if "websockets" not in sys.modules:
sys.modules["websockets"] = types.ModuleType("websockets")
import aibox # noqa: E402
def _make_handler():
handler = aibox.WebSocketHandler(ai_manager=mock.MagicMock())
return handler
# ==================== R1:/memos 畸形时间容错 ====================
class TestR1MemosCommand:
def test_malformed_reminder_time_shows_pending(self):
"""存量脏数据(畸形 reminder_time):/memos 按"待执行"展示,不抛 ValueError"""
handler = _make_handler()
try:
handler.ai_manager.memo_db.get_all_memos.return_value = [
{"id": 1, "title": "脏数据任务", "reminder_time": "not-a-time",
"repeat_type": "none", "repeat_interval": 0, "is_immediate": False},
{"id": 2, "title": "正常过期任务", "reminder_time": "2020-01-01 00:00:00",
"repeat_type": "none", "repeat_interval": 0, "is_immediate": False},
]
sent = []
async def fake_send(ws, msg):
sent.append(msg)
handler.send_message = fake_send
ws = mock.AsyncMock()
asyncio.run(handler.handle_command(ws, {"command": "/memos"}))
results = [m for m in sent if m.get("type") == "command_result"]
errors = [m for m in sent if m.get("type") == "error"]
assert len(results) == 1
assert not errors # 不再落入顶层兜底"处理失败"
content = results[0]["content"]
assert "脏数据任务" in content and "正常过期任务" in content
finally:
handler.shutdown()
def test_valid_reminder_time_still_marks_overdue(self):
"""合法过期时间仍标 overdue(语义不回归)"""
handler = _make_handler()
try:
handler.ai_manager.memo_db.get_all_memos.return_value = [
{"id": 1, "title": "过期任务", "reminder_time": "2020-01-01 00:00:00",
"repeat_type": "none", "repeat_interval": 0, "is_immediate": False},
{"id": 2, "title": "未来任务", "reminder_time": "2099-01-01 00:00:00",
"repeat_type": "none", "repeat_interval": 0, "is_immediate": False},
]
sent = []
async def fake_send(ws, msg):
sent.append(msg)
handler.send_message = fake_send
ws = mock.AsyncMock()
asyncio.run(handler.handle_command(ws, {"command": "/memos"}))
content = next(m["content"] for m in sent
if m.get("type") == "command_result")
overdue = aibox.i18n.get('memo_overdue')
pending = aibox.i18n.get('memo_pending')
overdue_line = next(l for l in content.splitlines() if "过期任务" in l)
pending_line = next(l for l in content.splitlines() if "未来任务" in l)
assert overdue in overdue_line and pending not in overdue_line
assert pending in pending_line and overdue not in pending_line
finally:
handler.shutdown()
# ==================== R2:interrupt 不再误杀 CLI 主 AI 子进程 ====================
class TestR2InterruptScope:
def test_interrupt_does_not_touch_global_executor(self):
"""WS 中断不再取消 ai_manager.command_executor(CLI 主 AI 的执行器)"""
handler = _make_handler()
try:
ws = mock.AsyncMock()
asyncio.run(handler.handle_interrupt(ws, {}))
handler.ai_manager.command_executor.cancel.assert_not_called()
finally:
handler.shutdown()
def test_interrupt_still_cancels_session_ai(self):
"""本会话 AI 仍被正常取消(中断能力不回归)"""
handler = _make_handler()
try:
ws = mock.AsyncMock()
ai = mock.MagicMock()
handler.client_sessions[ws] = {"ai": ai, "is_processing": True}
asyncio.run(handler.handle_interrupt(ws, {}))
ai.cancel.assert_called_once()
handler.ai_manager.command_executor.cancel.assert_not_called()
finally:
handler.shutdown()
# ==================== R3:unregister 补齐连接回收 ====================
class TestR3UnregisterCleanup:
def test_unregister_closes_websocket_and_cancels_tasks(self):
"""broadcast 超时路径走 unregister 后:websocket 被关闭、在途任务被取消、登记清空"""
handler = _make_handler()
try:
ws = mock.AsyncMock()
async def scenario():
task = asyncio.create_task(asyncio.sleep(30))
handler.connected_clients.add(ws)
handler.client_sessions[ws] = {"ai": None, "is_processing": True}
handler._processing_tasks[ws] = {task}
await handler.unregister(ws)
await asyncio.sleep(0) # 让 cancel 传播
assert task.done() # 取消即视为完成(cancelled 或已收尾)
asyncio.run(scenario())
assert ws.close.called
assert ws not in handler.connected_clients
assert ws not in handler.client_sessions
assert ws not in handler._processing_tasks
finally:
handler.shutdown()
def test_unregister_idempotent_and_mock_safe(self):
"""重复注销幂等;close 抛异常的对象不炸(AttributeError 被兜底)"""
class _BadCloseWS:
remote_address = ("127.0.0.1", 1)
async def close(self):
raise AttributeError("no close")
handler = _make_handler()
try:
ws = _BadCloseWS()
handler.connected_clients.add(ws)
handler.client_sessions[ws] = {"ai": None}
asyncio.run(handler.unregister(ws))
asyncio.run(handler.unregister(ws)) # 二次注销不抛
assert ws not in handler.connected_clients
assert ws not in handler.client_sessions
finally:
handler.shutdown()
+80 -4
View File
@@ -1,9 +1,9 @@
# aibox 模块安全与质量审计报告
- **日期**:2026-08-17(第一轮)/ 2026-08-18(第二轮复审)/ 2026-08-18(第三轮:遗留复核 + v3.4 专审)/ 2026-08-18(第四轮:3 路并行审读 + Batch H 修复)/ 2026-08-19(第五轮:4 路并行审读,聚焦历届未覆盖区域)
- **审计基线**:第一轮 HEAD `ce142fe`(Batch A–D 后)→ 第二轮 HEAD `177a9ac`(Batch E 修复后)→ 第三轮 HEAD `aa15841`(L4a/b/c 落地后)→ 第四轮 HEAD `d1f780e`(Batch G + SOCKS5 验证后)→ 第五轮 HEAD `d513c5f`(Batch I + model_config.py 后,含工作区未提交的 max_turns 钳制)
- **日期**:2026-08-17(第一轮)/ 2026-08-18(第二轮复审)/ 2026-08-18(第三轮:遗留复核 + v3.4 专审)/ 2026-08-18(第四轮:3 路并行审读 + Batch H 修复)/ 2026-08-19(第五轮:4 路并行审读,聚焦历届未覆盖区域)/ 2026-08-20(第六轮:交叉视角——修复回归验证 / 并发与生命周期 / 降级路径 / 修复间相互作用)
- **审计基线**:第一轮 HEAD `ce142fe`(Batch A–D 后)→ 第二轮 HEAD `177a9ac`(Batch E 修复后)→ 第三轮 HEAD `aa15841`(L4a/b/c 落地后)→ 第四轮 HEAD `d1f780e`(Batch G + SOCKS5 验证后)→ 第五轮 HEAD `d513c5f`(Batch I + model_config.py 后,含工作区未提交的 max_turns 钳制)→ 第六轮 HEAD `7dc6796`(Batch K 全落地 + 五轮收束文档同步后)
- **审计范围**:`aibox.py`(~3458 行)+ `llm_client.py` + `model_config.py` + `tools/`(7 文件)+ `subsystems/`(6 文件)+ `common/`(5 文件)+ 前端 HTML(frontend.html / entry.html / entry1.html)+ `skills/` 脚本 + `client.py` / `voice_client.py`(前端与技能脚本为第五轮首次覆盖)
- **方法**:第一轮逐文件代码审读 + PoC 动态验证;第二轮 Batch E 修复复审 + 全量重审 + 6 处关键发现实证抽查;第三轮遗留项复核 + v3.4 专审;第四轮 3 路并行审读 + 主审逐条复核 + Linux PoC 实证(6/6);第五轮 4 路并行审读(聚焦历届未覆盖区域)+ 主审逐条复核 + Linux PoC 实证(2/2)
- **方法**:第一轮逐文件代码审读 + PoC 动态验证;第二轮 Batch E 修复复审 + 全量重审 + 6 处关键发现实证抽查;第三轮遗留项复核 + v3.4 专审;第四轮 3 路并行审读 + 主审逐条复核 + Linux PoC 实证(6/6);第五轮 4 路并行审读(聚焦历届未覆盖区域)+ 主审逐条复核 + Linux PoC 实证(2/2);第六轮 4 路并行审读(Batch K 修复回归验证与修复自身缺陷面 / 并发与资源生命周期交叉 / 数据边界与故障降级 / 工具链与修复间相互作用)+ 主审逐条代码复核(Windows 开发机,无 Linux PoC,动态行为以代码级核验 + Windows 测试基线代替)
### 轮次总览
@@ -14,6 +14,7 @@
| 第三轮 | 2026-08-18 | `aa15841` | P1~P7 | Batch G(G1~G5:P1~P6 + N2/N3/N5/N7/N8/N10/N12~N16/L1) | 375 passed |
| 第四轮 | 2026-08-18 | `d1f780e` | Batch H 全项(含 N5 取消传播回归,高危) | Batch H + Batch I 遗留收尾 | 424 passed |
| 第五轮 | 2026-08-19 | `d513c5f` | J1~J16(高危 2 / 中危 5 / 低危与信息 9) | Batch K(波次 1~5 全落地) | 490 passed |
| 第六轮 | 2026-08-20 | `7dc6796` | R1~R3(均低危;Batch K 回归验证零回归) | Batch L(R1~R3 全落地) | 490 passed(Linux)/ 493 passed(Windows) |
---
@@ -29,7 +30,9 @@
**第五轮**(基线 d513c5f):聚焦历届未覆盖区域(WS 深审、model_config.py、前端与 skills 脚本首覆盖),发现 J1~J16——含 **J1 任务系统投毒**(畸形时间/数字字段致调度器失败热循环 + 新连接握手即崩)与 **J2 前端 XSS**(AI 输出/命令结果全链路 innerHTML 渲染)两项高危;Batch K 波次 1~5 全落地,J1~J16 全数修复,490 passed 零回归。
**最终状态**:五轮累计发现 69 项(高危 7 / 中危 26 / 低危与信息 36)**全部修复落地**,无未修复安全发现;维持原判仅限设计决策项,见"十五、最终状态与遗留清单"。
**第六轮**(基线 7dc6796):交叉验证轮——Batch K 修复回归验证(J1~J16 逐项在位)、并发与生命周期、降级路径、修复间相互作用四路审读。**零回归、无修复引入的缺陷**;新发现 R1~R3 均为低危(`/memos` 裸 strptime 加固漏点 / handle_interrupt 全局 executor 取消的架构残留 / broadcast 超时后半开连接有界泄漏),已随 **Batch L 全部修复落地**。见"十六、第六轮审计"。
**最终状态**:六轮累计发现 72 项(高危 7 / 中危 26 / 低危与信息 39),**全部修复落地**(Batch E/F/G/H/I/K/L),无未修复安全发现;维持原判仅限设计决策项,见"十五、最终状态与遗留清单"与"十六、第六轮审计"。
---
@@ -655,3 +658,76 @@ J3 ×3(单条/批量删除无 FTS 残留 + 删后搜索不命中)、J4 ×2
### 15.3 最终测试基线
全量 **490 passed**(Linux,`aibox/venv`),零回归;`test_execute_timeout` / `test_flat_api_still_reads_key_file` 为 Windows 向用例(ping 语法 / 依赖 `~/.cert/deepseek.key`),Linux 预存失败与历轮改动无关。
---
## 十六、第六轮审计(2026-08-20,HEAD `7dc6796`)
方法:4 路并行审读(① Batch K 修复回归验证与修复自身缺陷面 ② 并发与资源生命周期交叉 ③ 数据边界与故障降级 ④ 工具链与修复间相互作用)+ 主审逐条代码复核。审计环境为 Windows 开发机(无 Linux PoC),动态行为以代码级核验 + Windows 测试基线代替。
### 16.1 Batch K 修复回归验证(J1~J16 全部在位,零回归)
| # | 核验结论 | 关键证据 |
|---|---|---|
| J1 | **五重防御链闭环** | 入库校验(`db.py:365-387`,空串/None 正确跳过 falsy 分支)→ WS 友好错误(`aibox.py:2405-2424`)→ 调度器预检(`aibox.py:184-196`)→ 触发事务防御(`db.py:590-680`)→ 展示防御 `_is_overdue_time`(`db.py:814`)→ register 入 try(`aibox.py:2989-2992`)。全库 strptime 调用点排查(19 处):CLI `--memo-list`、get_pending_memos(Q5)、memo 工具三处均已防护,唯一漏点见 R1 |
| J2 | **净化链完整** | 三页面 `marked.parse` 全部收敛于 `renderMarkdown()` helper 内(8 处调用点逐一核验);残留 innerHTML 均为静态字面量(复制按钮/空容器);DOMPurify 缺失退化分支仅转义 `<`——在 innerHTML 文本节点上下文中足以阻断标签注入(无法开口标签即无法创建元素),判定安全 |
| J5 | **原子写闭环** | `atomic.py` mkstemp 随机名 + fsync + os.replace,BaseException 路径清理临时文件;config 损坏备份(`config.py:193-199, 206-213`)与 `_save_default_config` 原子写均在位 |
| J7 | **语义正确** | `aibox.py:2277-2301` 每客户端并发 + wait_for 超时 + unregister(discard 幂等);CancelledError 为 BaseException 不被 `except Exception` 捕获,任务取消语义保留——实现与注释一致。残留面见 R3 |
| J9/J10/J11/J16 | 抽查在位 | `_connect()` 统一 busy_timeout;批量删除非事务但计数诚实(`deleted/n`);/new 等待 2s 为尽力而为(超时继续 reset,行为已注释);`MAX_HELP_LENGTH` 按 str 码点切片无半个多字节字符风险(第五轮 L5 顾虑在此消解)、同名键可执行优先在位 |
| FailoverClient | 契约清晰 | P4 并发契约(单线程串行 + 跨线程 cancel)文档化;非流式 None 透传(认证类 4xx 响亮暴露)、流式仅首事件前转移、链耗尽抛最后异常——均与 §9.2 设计一致 |
历届修复抽查(Batch E~I 的 H2/L5/L9/N4/N6/N8/M2/N13/N14):executors reader 线程收尾、输出 1MB 上限(流式/非流式两路)、registry 流式预算与统一清理、broadcast 快照——均在位。
### 16.2 新发现(3 项,均为低危)
#### R1:`/memos` WS 命令裸 strptime——J1 加固漏点
`aibox.py:2575`:`/memos` 命令对每条 memo 的 `reminder_time` 直接 `datetime.strptime(...)` 无 try/except。J1 加固了 `get_pending_tasks_for_user` 展示路径与调度器路径,但 `/memos` 列表路径漏防:存量脏数据(J1 落库前入库、或直接 SQL 写入的畸形时间)→ ValueError → 落入 handle_message 顶层兜底,用户收到笼统"处理失败",`/memos` 功能不可用。同功能的 CLI 路径(`aibox.py:3187-3192`)与 memo 工具三处(`db_tools.py:623/697/801`)均已防护,唯独此点遗漏。**影响**:单命令报错,无崩溃/热循环(顶层 except 接住),仅脏数据库场景触发。
#### R2:handle_interrupt 取消全局 command_executor——会话隔离架构残留
`aibox.py:2369-2371`:任一 WS 客户端发送 interrupt,除取消本会话 AI(`ai.cancel()` 已覆盖会话自身 executor,见 `DeepSeekChat.cancel` `aibox.py:1094-1096`)外,还取消 `ai_manager.command_executor`——这是 **CLI 主 AI 实例**的执行器(`aibox.py:2835/2894`)。单实例时代的遗留代码:CLI 交互对话与 WS 服务并发运行时,WS 用户的一次中断会误杀终端用户正在执行的命令(跨上下文干扰)。**影响**:可用性问题,无安全面扩大;对会话自身冗余(其 executor 已被 ai.cancel 终止)。
#### R3:broadcast 超时 unregister 后半开连接残留——J7 修复的残留面
`aibox.py:2290-2298`:broadcast 超时路径调用 `unregister`(回收 `connected_clients`/`client_sessions` 并 cancel 会话 AI),但①不关闭 websocket、②不取消 `_processing_tasks`。`ping_interval=None`(有意禁用)下 `handle_connection` 的 `async for` 接收循环对半开 TCP 永不退出——连接对象、协程与任务登记泄漏直至进程重启(**有界**:每半开连接一份,J7 已消除其对广播/调度器主循环的阻塞,主要危害已解)。次级场景:慢客户端(发送超 5s 但 TCP 存活)被 unregister 后恢复,后续消息在 `handle_user_message:2727-2728` 被**静默丢弃**(无提示,无崩溃)。**影响**:有界资源泄漏 + 边缘场景静默丢消息。
### 16.3 修复建议(Batch L,已全落地)
| # | 建议 | 规模 |
|---|---|---|
| R1 | `/memos` 的 strptime 包 try/except,解析失败按"待执行"展示(对齐 CLI `:3187-3192` 模式) | ~4 行 |
| R2 | 移除 `handle_interrupt` 中全局 executor 取消(`:2369-2371`)——会话隔离下全局取消无正当受益方,CLI AI 的中断由其自身入口处理 | -3 行 |
| R3 | unregister 内 `await websocket.close()` + 取消该连接 `_processing_tasks`(与 `handle_connection` finally 对齐);或维持现状记录为设计项(有界泄漏,进程重启即清) | ~8 行 |
三项均为低危,不构成阻塞;建议与下次功能迭代随批处理。
### 16.3a Batch L 修复落地(2026-08-20,随批当日完成)
| # | 修复内容 | 文件/位置 |
|---|---|---|
| R1 | `/memos` 的 strptime 包 try/except(ValueError/TypeError 按待执行展示),对齐 CLI `--memo-list` 模式 | `aibox.py`(/memos 分支) |
| R2 | 移除 `handle_interrupt` 对 `ai_manager.command_executor` 的全局取消——WS 中断不再误杀 CLI 主 AI 的子进程;注释说明 ai.cancel() 已覆盖会话自身 executor | `aibox.py` handle_interrupt |
| R3 | `unregister` 补齐回收:取消该连接 `_processing_tasks` 在途任务(pop + cancel,幂等)+ `await websocket.close()`(try/except 兜底,幂等)——broadcast 超时路径(J7)与连接正常关闭路径(finally)行为对齐,半开连接对象/协程/任务登记不再泄漏 | `aibox.py` unregister |
**新增测试**(`tests/test_l_fixes.py` 6 例,全部通过):
- R1 ×2:畸形 reminder_time 不抛错按待执行展示(command_result 而非 error)、合法过期仍标 overdue(语义不回归)
- R2 ×2:interrupt 不触碰全局 executor、本会话 AI 仍被取消(中断能力不回归)
- R3 ×2:unregister 关闭 websocket + 取消在途任务 + 三处登记清空、重复注销幂等且 close 异常被兜底
**测试基线(Windows 开发机)**:全量 **493 passed / 4 failed / 1 skipped**(26s)= 487 + Batch L 新增 6。4 失败与审计基线完全相同(平台环境项),零回归。
### 16.4 测试基线(Windows 开发机)
审计时全量 487 passed / 4 failed / 1 skipped;Batch L 落地后 **493 passed / 4 failed / 1 skipped**(+6 新增)。4 失败均为平台环境项,与代码质量无关:
- `test_symlink_outside_skills_dir_rejected`:Windows 符号链接创建需管理员权限(WinError 87)
- `test_truncated_output_has_no_replacement_char`:`python3` 不在 Windows PATH
- `test_markdown_skipped_when_executable_same_name` / `test_help_output_truncated`:J16 两例依赖 POSIX 可执行位语义(Windows 下 chmod 不产生执行位,"可执行"技能被跳过加载)
Linux 基线(`aibox/venv`)维持 **490 passed** 不受影响。
### 16.5 第六轮结论
交叉验证轮:**Batch K 修复零回归**(J1~J16 逐项在位,历届 E~I 修复抽查均在位),**无修复引入的回归**;新发现 R1~R3 均为低危边界项(加固漏点/架构残留/J7 残留面),无高危、无中危,**已随 Batch L 全部修复落地**。六轮累计发现 **72 项**(高危 7 / 中危 26 / 低危与信息 39),**全部修复落地**,无未修复安全发现。