Commit c95bca7
feat(activity): 跨平台 OS 信号 push 通道 + agent_router remote 守卫 (#1477)
* refactor(activity): 合并 remote-deploy helper + agent_router 加 remote 守卫
PR B 的安全收紧 + 重构步骤,为后续 /api/activity_signal 端点铺路。
## 单源化 remote-deploy helper
`NEKO_ACTIVITY_TRACKER_REMOTE` 检查原本有两份实现:
- `main_logic/activity/system_signals._force_degraded_from_env`(活动采集器降级)
- `main_routers/system_router._is_remote_backend_deployment`(截图端点拒绝)
逻辑一致但分两处维护,新增 consumer(agent_router、即将加的 activity_signal 推送端点)只会让漂移风险继续累积。重命名前者为公共 `is_remote_backend_deployment()`,private 别名保留向后兼容;system_router 改为 import + 模块级别名,screenshot router 测试无需改动。
## agent_router 加 remote-mode 守卫
`/api/agent/{flags,command,admin/control,tasks/{id}/cancel}` 四个 mutation 端点之前没做远端部署感知 —— 远程部署时 computer_use/browser_use/openclaw 控制的是服务器机器而不是用户机器,转发给 localhost tool_server 既无意义又有安全风险(任何能调到公开后端 HTTP 的请求都能驱动服务器上的 agent)。
新增 `_remote_backend_block()` helper,在四个端点入口检查环境变量,命中就直接 501,和 `/api/screenshot` 同环境下的拒绝模式对齐。
## scope 注记
issue #1023 原本审计提到的"agent_router 没套 CSRF + Origin 守卫"是更宽泛的洞 —— 防的是 DNS rebinding 这类对本地后端的攻击。完整修复需要前端 15+ 个 agent fetch 调用点开始送 `X-CSRF-Token`(参考 `static/app-screen.js` 的 `secureLocalScreenshotFetch` 模式),那是 ~200 行前端改动,超出 PR B 预算。本 commit 只覆盖远端部署威胁这一半,CSRF 部分留 follow-up issue。
## 测试
- 现有 `tests/unit/test_system_screenshot_router.py` 35 个用例继续通过(私有别名保留)
- 新增 `tests/unit/test_agent_router_remote_block.py` 52 个用例:
* 4 端点 × 2 env 变量 × 5 truthy 值 = 40 个 blocked 路径
* 4 端点的 unset 路径不被误拦
* 6 个 truthy/falsy 边界 + helper 直接调用 + 跨模块同源验证
issue #1023 拍板路径中的 C1 步骤。
* feat(activity): 前端 push OS 信号端点 + Electron 心跳客户端
issue #1023 的主体功能。把 PR #1015 留下的 ``push_external_system_signal``
push 通道补成完整闭环 —— Electron 前端 5s 心跳读 OS 信号,POST 到后端,
覆盖 Win/Mac/Linux 桌面 + 远端服务器部署 + 移动 shell(受平台限制部分降级)。
## 后端 POST /api/activity_signal
新端点接受 ``window_title`` / ``process_name`` / ``idle_seconds`` /
``cpu_avg_30s`` / ``gpu_utilization`` 五个 OS 信号字段,转发给对应
lanlan_name 的 tracker:
- 字段全部可选(``lanlan_name`` 除外),缺啥跳啥,部分 snapshot 也优于无 push
- 400 校验:range(idle≥0, cpu/gpu ∈ [0, 100])+ type
- 404 当 lanlan_name 未注册
- 503 当 tracker 还没初始化(boot race)
- 429 + ``Retry-After`` 当 5s 内重复(``_EXTERNAL_SIGNAL_MIN_INTERVAL`` 在
``tracker.py`` 紧挨 TTL 常量,前后端节奏耦合,不进 config)
- 500 当 tracker 抛异常
节流字典按 lanlan_name 分桶,上限 64 个(防恶意 spray,实际 1-3 个)。
## 静态 JS 客户端(``static/app-activity-signal.js``)
5s 心跳模块,行为:
1. 检测 ``window.nekoActivitySignal.read`` 是否暴露(NEKO-PC sibling
PR 加的 Electron 桥)。没有就 log 一次后退出 —— 纯浏览器 / 手机
shell / 没装 NEKO-PC 的 dev 跑都安全
2. 每个 tick 从桥拉 camelCase 信号,按类型/范围验证后转 snake_case
POST 到 /api/activity_signal
3. 429 / 404 / 503 静默忽略(节流命中 + boot race,预期内);其他错误
计数到 3 次后停止 log 防 spam
4. ``visibilitychange`` 暂停/恢复,避免隐藏窗口浪费 IPC + 网络
5. 每 tick 重新解析 lanlan_name(角色切换后立即推到新 tracker)
只挂在 ``templates/index.html``(永远在的桌面 pet 窗口),不挂 chat.html
避免双心跳互相 throttle。
## 配套
- ``tests/unit/test_activity_signal_router.py``:26 个用例
* happy path(全字段/单字段/strip 空白)
* 校验 400(缺 lanlan_name、非对象 body、非法 JSON、各字段越界/类型错)
* 404 / 503 / 500
* 节流 429(含 Retry-After 头)、独立 lanlan_name 桶、TTL 过后恢复
* 节流字典 cap 测试(spray 攻击下不无限增长)
- ``docs/design/user-activity-tracker.md``:把"HTTP 端点 not yet
added"那段改成完整契约 + 渲染端客户端 + Electron 桥契约
## 跨仓库
桥的 Electron 半边在 NEKO-PC 仓库的 follow-up PR:``src/main.js`` 的
``ipcMain.handle('neko:read-activity-signal', ...)`` + 对应 preload。
本 PR 的 ``app-activity-signal.js`` 已经设计成桥缺席时优雅 no-op,所以
两个 PR 谁先 merge 都不会破东西。
issue #1023 拍板路径中的 C2 步骤。
* fix(activity-signal): 拦 NaN/Inf + inFlight 竞争 + bridge 失败入节流
PR #1477 review 反馈两条修复,对应 CodeRabbit + Codex P2 / Minor。
F2(远端模式下端点匿名可写)需要前端 CSRF 接入,scope 超出 PR B,已在
原 PR 描述里说明,留 follow-up,本 commit 不动。
## F1 NaN / ±Infinity 漏过验证(CodeRabbit + Codex 都点了)
``_activity_signal_validate_float`` 之前只做 ``< lo`` / ``> hi`` 比较,
``float('nan') < lo`` 静默 False → ``NaN`` / ``Infinity`` 全部能塞进
tracker。下游某些 JSON 序列化(``json.dumps`` 默认 ``allow_nan=False``,
日志 / 状态机回放、response 转出)会直接抛 ``ValueError`` 把上游
endpoint 拖成 500。
修:range 比较前加 ``math.isfinite`` 守卫,返回 ``"<field> must be
finite"`` 的 400。
新增 9 个测试:三个字段(idle/cpu/gpu)× 三个 token(NaN / Infinity /
-Infinity)。TestClient 的 ``json=`` 走 httpx 的 ``allow_nan=False``
serialiser 拦不下来,所以测试用 ``content=`` 直接送裸 bytes,对齐
攻击者 / buggy client 走 stdlib ``json.loads`` 的实际路径。
## F3 inFlight 竞争 + bridge 失败节流(CodeRabbit)
两个问题:
1. 之前 ``if (inFlight) return`` 检查在 ``await readSignalsFromBridge()``
之前,但 ``inFlight = true`` 在 await 之后。慢 IPC 时(Linux xprop /
macOS Screen Recording 提示)两个 tick 都能过 inFlight 检查、并发
进入 fetch,无谓触发后端 5s rate limit。
2. ``readSignalsFromBridge`` catch 块 log 用了 ``consecutiveFailures <
THRESHOLD`` 门控,但从来不 ``++`` 这个计数器。桥反复失败时每 5s
都会重复打同一条 warn,永不消音。
修:
- ``inFlight = true`` 提到 try 块开头、桥读之前;finally 统一清,
early return 也走 finally
- ``readSignalsFromBridge`` catch 块先 ``consecutiveFailures++`` 再
按新计数 gate log;和 fetch-side 失败节流共享同一个 3-then-quiet 策略
手测验证:模拟桥反复抛异常 6 次,warning log 严格 ≤ 3 条。
## 关联
- PR #1477 上 CodeRabbit / Codex inline comments
- 不影响 issue #1023 的整体架构决策(CSRF 仍是 follow-up)
* fix(activity-signal): Origin-present same-origin gate(CodeRabbit 建议)
push_activity_signal 入口加 ~10 行 Origin/Referer 同源校验,零前端改动
就能挡掉浏览器侧 drive-by CSRF。F2 scope 切分内的折中加固,PR #1477
CodeRabbit 审审里给的方案,原 PR 描述里承诺的 follow-up 安全 PR 会做
完整 CSRF 守卫。
## 决策点
browsers 自 2024 起对 POST 强制带 Origin header,所以"Origin 在 + 不在
allowed 集合" = 跨站浏览器 JS。捕这一类不需要 token:
- ``curl`` / Node 脚本 / Electron 主进程:无 Origin → 放行(原契约)
- 同源 Electron 渲染端 / 浏览器:Origin == ``request.base_url`` → 放行
- 恶意页面 (evil.com) 跨站 fetch:Origin == ``https://evil.com`` → 403
Referer 兜底覆盖少数没发 Origin 的客户端(``_get_request_origin``
helper 已经处理)。
## 与现有截图端点的差异
``/api/screenshot`` 用的是更严的 ``_validate_local_mutation_request``
(CSRF + Origin 双因子,必须前端送 ``X-CSRF-Token``)。本端点保持
"无强制 token"基线,理由是:
1. 完整 CSRF 需要前端 fetch 调用点统一改造,~200 行前端,超 PR B 预算
2. 影响面:本端点最坏只能伪造 tracker 软状态影响主动搭话内容选择,
不像截图能泄漏屏幕、agent 能驱动 computer_use
3. follow-up 安全 hardening PR 会一次性统一所有端点的 CSRF 策略
## 测试
新增 7 个用例:
- 无 Origin → 200(curl/Node 路径)
- 同源 Origin → 200(Electron 渲染 / 同源浏览器)
- evil.com / attacker.example.com / 子域伪装 → 403
- Referer-only off-origin → 403(fallback path)
- 无法 parse 的 Origin → 200(fall through to no-Origin, 同 screenshot 相反方向的兼容)
总计 ``test_activity_signal_router.py`` 42 个用例通过(原 35 + 7 新)。
## 关联
- PR #1477 上 CodeRabbit F2 thread (3292871371)
- follow-up issue: CodeRabbit 答应代开统一 CSRF/Origin/token hardening PR
* fix(activity-signal): resolveLanlanName 优先读 appState(Codex F4)
Codex P2 on PR #1477:在角色切换的 lag 窗口里 ``window.lanlan_config``
会暂时落后于 ``window.appState``。原来 ``resolveLanlanName`` 只读
``lanlan_config``,切换瞬间 → 心跳推到旧 lanlan_name 上(被后端 404 或
落入老 tracker),新角色短暂丢 OS 信号覆盖。
同项目里 ``static/app-react-chat-window.js:~1442`` 已经是这个 fix
pattern —— 注释写"角色切换时 appState 先更新,window.lanlan_config 可能
滞后",并标注 "CodeRabbit Major 指出"。本 PR 一致化处理。
## 修改
``resolveLanlanName`` 优先级链:
1. ``window.appState.lanlan_name``(切换时 first-update)
2. ``window.lanlan_config.lanlan_name``(兜底)
3. URL ``?lanlan_name=`` query param
4. 空字符串 → 当 tick 跳过
## 测试
新增 vm 沙箱 5 case 手测验证:
- 只有 appState → 用 appState
- 只有 lanlan_config → 兜底
- 切换 lag(两者都在但不同)→ 用 appState(不打到旧 tracker)
- URL 兜底
- 空 appState.lanlan_name 跳过、落回 lanlan_config
* fix(activity-signal): 拦 Origin=null + 空 payload (Codex F5/F6)
Codex P1/P2 on PR #1477 review。两条都关掉了原 Origin gate 之后还能渗
进 tracker 的旁路。
## F5 (P1): Origin "null" 旁路
opaque-origin(沙盒 iframe、file://、扩展上下文)浏览器送的字面量
``"null"`` 字符串。``urlsplit("null")`` 解析成空 scheme + netloc →
``_normalize_origin_value`` 返回 ``""`` → Origin gate 走 no-Origin
"allowed" 分支 → 跨站攻击页可以靠 ``<iframe sandbox>`` 注入伪造心跳。
修:``push_activity_signal`` 入口加 raw 字符串守卫,``Origin`` 或
``Referer`` 为 ``"null"``(大小写不敏感)直接 403。早于 normalize 调用,
不会被空字符串吃掉。
3 个新测试:Origin/Referer × ("null"/"NULL")。
## F6 (P2): 空 payload 污染 tracker 状态
之前 client 只在 ``payload === null`` 时 skip,但桥返回 ``{}`` 或字段
全部 type/range 校验失败时 payload 会是 ``{}``。POST ``{"lanlan_name":
"X"}`` 命中 ``push_external_system_signal`` 的硬编码默认值
(``idle_seconds=0.0`` / ``cpu_avg_30s=0.0``)+ ``os_signals_available=
True``,silently 把真实状态盖成"idle=0/cpu=0/no window",主动搭话分类
被污染。
修两层:
1. 前端 ``static/app-activity-signal.js``:``Object.keys(payload).length
=== 0`` 时 skip tick;保留 HTTP roundtrip + rate-limit 配额
2. 后端 ``push_activity_signal``:所有信号字段都 None 时 400
``"at least one signal field required"``;defence-in-depth 兼挡 native
/ 恶意 caller
8 个新测试:
- ``test_lanlan_name_only_payload_rejected_400``(裸 lanlan_name → 400)
- ``test_single_field_payload_accepted`` × 5(每个单字段都能 happy path)
- ``test_opaque_origin_null_rejected`` × 3(F5)
- 既有 11 个 happy-path 测试更新成带 ``idle_seconds: 0``(最小信号字段)
前端 4 个 vm 沙箱手测:桥返回 ``{}`` / null / 全字段非法 → 0 次 fetch;
返回 1 个有效字段 → 正常 POST。
## 关联
- PR #1477 Codex review on commit 67c8089
- 单元测试 ``test_activity_signal_router.py`` 现 50 个用例全过
* fix(activity-signal): 把空白字符串视为缺失 (CodeRabbit F7)
CodeRabbit Minor on PR #1477:F6 的空 payload 守卫只检查 ``None``,
但 ``{"lanlan_name":"X","window_title":""}`` / whitespace-only 字符串
能通过 str 验证器、绕过 ``all(None)`` 检查、然后让 tracker 记录"无前
台窗口 + 数值默认 0.0"的伪状态 —— 和 F6 要堵的空 payload 污染本质一
样。
修:守卫扩展为 ``v is None or (isinstance(v, str) and not v.strip())``,
覆盖 ``None`` + 空字符串 + 纯空白。
注:故意没在 ``_activity_signal_validate_str`` 里把空白 normalize 成
None。如果未来 tracker 想区分"看到桌面但没标题"(``""``)和"完全没观
察"(``None``),upstream 数据语义保留。守卫只在"全 payload 加起来
零信息"的边界判断上把它们等同对待。
## 测试
新增 8 个用例:
- 6 个 blank-only payload 被 400 (window_title=""/" "/process_name=""/
"\t\n "/双字段全空/双字段都空白)
- 2 个 blank + signal 配对仍 200(空白单独不算信号,但配上 idle/cpu
整体仍有效)
``test_activity_signal_router.py`` 现 58 个用例全过。
* fix(activity-signal): float 验证器拦 bool (Codex F8)
Codex P2 on PR #1477。``_activity_signal_validate_float`` 走 ``float(raw)``
强转,而 ``bool`` 是 Python 里 ``int`` 的子类 ——
>>> float(True)
1.0
>>> float(False)
0.0
所以 ``{"idle_seconds": true}`` / ``{"cpu_avg_30s": false}`` 之类的载荷
会被静默接受成 1.0 / 0.0,通过 range 检查,把伪造遥测写进 tracker。
``idle_seconds=True`` → "用户刚操作",``cpu_avg_30s=True`` → "1% 占用",
都会偏移活动分类。
修:``raw`` 进 ``float()`` 前加 ``isinstance(raw, bool)`` 守卫,命中就
返回和"类型错"同样的 400 "<field> must be a number"。``isinstance``
检查必须先于 ``float()`` 因为后者会 happy-path 吃 bool。
6 个新测试:3 字段 × (True / False),全部 400。
``test_activity_signal_router.py`` 现 64 个用例全过。
* fix(activity-signal): float 验证器拦 OverflowError (Codex F9)
Codex P2 on PR #1477。``float()`` 对 native Python big-int 会抛
``OverflowError``:
>>> float(10 ** 400)
Traceback (most recent call last):
...
OverflowError: int too large to convert to float
原 ``except (TypeError, ValueError)`` 漏了这种,请求直接成 500 而不是
正常 400 validation 错。JSON 规范不限整数精度,Starlette 的
``json.loads`` 会照实给我们 big-int → 任何人 POST oversized 整数都能
低成本把 endpoint 弄 500。
修:``except`` 元组加 ``OverflowError``。返回和其它类型错同样的 400
"<field> must be a number"。
新增 3 个测试:3 字段 × ``10^400`` raw-bytes payload(``json=`` helper
会被 httpx 转科学计数法 fit double,所以用 ``content=`` 直接送字面量
big-int)。``test_activity_signal_router.py`` 现 67 个用例全过。
* tune(activity-signal): _EXTERNAL_SIGNAL_TTL_SECONDS 30s→15s
统一前端作 OS 信号主源后,push 管道叠了两个不同步的 5s timer——NEKO-PC
桥主进程 sampler(读 OS 信号)+ 渲染端心跳(读桥缓存快照后 POST)——最坏
数据龄在无丢包时已逼近 ~10-12s。30s TTL 会在心跳死后继续拿陈旧的"用户
活跃"快照太久;10s 又会在远端丢一次包就在 fresh/degraded 间抖动。
15s = 3× 心跳,容忍 ~2 次连续丢包再回落本地采集器,同时把"心跳死后仍报
陈旧活跃"的窗口从 30s 砍半。顺带把 _EXTERNAL_SIGNAL_MIN_INTERVAL 注释里
"TTL≫interval(6×)"的旧说法改正为 3×。
tests/unit/test_activity_signal_router.py + test_activity_tracker_followup.py
共 135 用例全过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(activity-signal): TTL 描述同步到 15s(CodeRabbit)
上个 commit(42427d6)把 _EXTERNAL_SIGNAL_TTL_SECONDS 改成 15s,但
system_router.py 的端点 docstring + 节流注释、设计文档里还写着 30s。
逐处校正:
- system_router.py push_activity_signal docstring:"fresher than ...
(30s)" → (15s)
- system_router.py 节流注释:"TTL is 30s ... 5 of every 6" →
"TTL is 15s (3× this interval) ... 2 of every 3"(配合新比例)
- docs/design/user-activity-tracker.md:"When fresh (≤ 30s)" → (≤ 15s)
纯文档/注释,无行为改动;cpu_avg_30s 字段名、prefs 30s 缓存、
activity_guess 30s anti-thrash 等无关 30s 未动。67 用例全过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 0037b58 commit c95bca7
9 files changed
Lines changed: 1580 additions & 29 deletions
File tree
- docs/design
- main_logic/activity
- main_routers
- static
- templates
- tests/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
794 | 794 | | |
795 | 795 | | |
796 | 796 | | |
797 | | - | |
| 797 | + | |
798 | 798 | | |
799 | 799 | | |
800 | 800 | | |
| |||
815 | 815 | | |
816 | 816 | | |
817 | 817 | | |
818 | | - | |
819 | | - | |
820 | | - | |
821 | | - | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
822 | 865 | | |
823 | 866 | | |
824 | 867 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
53 | 71 | | |
54 | 72 | | |
55 | | - | |
| 73 | + | |
56 | 74 | | |
57 | 75 | | |
58 | 76 | | |
| |||
61 | 79 | | |
62 | 80 | | |
63 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
64 | 88 | | |
65 | 89 | | |
66 | 90 | | |
| |||
185 | 209 | | |
186 | 210 | | |
187 | 211 | | |
188 | | - | |
| 212 | + | |
189 | 213 | | |
190 | 214 | | |
191 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
75 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
76 | 99 | | |
77 | 100 | | |
78 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
| |||
56 | 57 | | |
57 | 58 | | |
58 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
59 | 91 | | |
60 | 92 | | |
61 | 93 | | |
| |||
197 | 229 | | |
198 | 230 | | |
199 | 231 | | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
200 | 235 | | |
201 | 236 | | |
202 | 237 | | |
| |||
276 | 311 | | |
277 | 312 | | |
278 | 313 | | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
279 | 317 | | |
280 | 318 | | |
281 | 319 | | |
| |||
521 | 559 | | |
522 | 560 | | |
523 | 561 | | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
524 | 565 | | |
525 | 566 | | |
526 | 567 | | |
| |||
534 | 575 | | |
535 | 576 | | |
536 | 577 | | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
537 | 581 | | |
538 | 582 | | |
539 | 583 | | |
| |||
0 commit comments