From f6614d4bf9a9e7bb9e36bed2ab9cde8828196da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=A0=96=E7=9F=A5?= Date: Thu, 22 Oct 2026 16:25:35 +0800 Subject: [PATCH] HEL-780: connect persistent companion and real streaming text chat Co-authored-by: multica-agent --- .env.example | 19 +- .gitignore | 2 + config/models.yaml | 16 +- deploy/cybergril-api.service.example | 28 ++ docs/frontend/browser-results-1440.json | 12 + docs/frontend/browser-results-390.json | 4 +- docs/r1/DEPLOY-ROLLBACK.md | 15 + docs/r1/IMPLEMENTATION.md | 64 +++ docs/r1/PR.md | 7 + docs/r1/TEST-RECORD.md | 20 + docs/r1/browser-results.json | 35 ++ docs/r1/controlled/browser-results.json | 35 ++ docs/r1/desktop-history-moon.png | Bin 0 -> 730147 bytes docs/r1/desktop-model-error.png | Bin 0 -> 1121765 bytes docs/r1/desktop-preview-moon.png | Bin 0 -> 547797 bytes docs/r1/desktop-real-chat-moon.png | Bin 0 -> 1122881 bytes docs/r1/mobile-chat-fog.png | Bin 0 -> 354216 bytes docs/r1/mobile-history-moon.png | Bin 0 -> 218455 bytes docs/r1/mobile-relation-fog.png | Bin 0 -> 92316 bytes pyproject.toml | 4 + requirements.lock | 60 +++ src/cybergril/api/__init__.py | 1 + src/cybergril/api/__main__.py | 13 + src/cybergril/api/app.py | 500 +++++++++++++++++++ src/cybergril/memory/schema.sql | 16 + src/cybergril/providers/llm/openai_compat.py | 4 +- tests/test_api.py | 381 ++++++++++++++ web/src/adapters/real.ts | 80 +++ web/src/app/RealApp.tsx | 37 ++ web/src/chat/ChatPanel.tsx | 35 +- web/src/domain/model.ts | 2 + web/src/main.tsx | 3 +- web/src/manage/CreationWizard.tsx | 22 +- web/src/manage/ManagePanel.tsx | 27 +- web/src/shared/ui.tsx | 4 +- web/src/stage/Presenter.tsx | 5 +- web/tests/browser_check.py | 9 +- web/tests/real.test.ts | 32 ++ web/tests/real_browser_check.py | 163 ++++++ 39 files changed, 1591 insertions(+), 64 deletions(-) create mode 100644 deploy/cybergril-api.service.example create mode 100644 docs/frontend/browser-results-1440.json create mode 100644 docs/r1/DEPLOY-ROLLBACK.md create mode 100644 docs/r1/IMPLEMENTATION.md create mode 100644 docs/r1/PR.md create mode 100644 docs/r1/TEST-RECORD.md create mode 100644 docs/r1/browser-results.json create mode 100644 docs/r1/controlled/browser-results.json create mode 100644 docs/r1/desktop-history-moon.png create mode 100644 docs/r1/desktop-model-error.png create mode 100644 docs/r1/desktop-preview-moon.png create mode 100644 docs/r1/desktop-real-chat-moon.png create mode 100644 docs/r1/mobile-chat-fog.png create mode 100644 docs/r1/mobile-history-moon.png create mode 100644 docs/r1/mobile-relation-fog.png create mode 100644 requirements.lock create mode 100644 src/cybergril/api/__init__.py create mode 100644 src/cybergril/api/__main__.py create mode 100644 src/cybergril/api/app.py create mode 100644 tests/test_api.py create mode 100644 web/src/adapters/real.ts create mode 100644 web/src/app/RealApp.tsx create mode 100644 web/tests/real.test.ts create mode 100644 web/tests/real_browser_check.py diff --git a/.env.example b/.env.example index ab96030..2e98ebc 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,8 @@ -# 复制成 .env 后填写。不填也能跑(会回退到离线 provider)。 - -# ---- LLM ---- -DEEPSEEK_API_KEY= - -# ---- Embedding(可选,留空则用本地或跳过向量检索)---- -EMBEDDING_API_KEY= - -# ---- 运行时 ---- -CYBERGRIL_DB=companion.db -CYBERGRIL_LOG_LEVEL=INFO +# R1 uses the approved no-key gateway from config/models.yaml. +# These variables are read from the process environment; this file is not auto-loaded. +# Keep data outside release directories. Use an independent directory for local testing. +CYBERGRIL_DB=/opt/cybergril-data/cybergril.db +CYBERGRIL_MODELS=config/models.yaml +CYBERGRIL_WEB=web/dist +CYBERGRIL_HOST=127.0.0.1 +CYBERGRIL_PORT=8310 diff --git a/.gitignore b/.gitignore index 5782304..3abc61c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ models/ *.swp .DS_Store Thumbs.db + +.venv311/ diff --git a/config/models.yaml b/config/models.yaml index 4a5d7a8..9daa167 100644 --- a/config/models.yaml +++ b/config/models.yaml @@ -18,9 +18,9 @@ tiers: # ---- 旗舰:人设浓度最高的时刻 ---- high: provider: openai_compat - model: deepseek-v4.1-flash - base_url: https://api.deepseek.com/v1 - api_key_env: DEEPSEEK_API_KEY + model: halogen-qwen3.8-flash-next + base_url: http://192.168.200.11:8731/v1 + api_key_env: null timeout: 60.0 temperature: 0.85 @@ -28,9 +28,9 @@ tiers: # 省钱的杠杆通常不是换更小的模型,而是少喂上下文。 standard: provider: openai_compat - model: deepseek-v4.1-flash - base_url: https://api.deepseek.com/v1 - api_key_env: DEEPSEEK_API_KEY + model: halogen-qwen3.8-flash-next + base_url: http://192.168.200.11:8731/v1 + api_key_env: null timeout: 40.0 temperature: 0.7 @@ -38,8 +38,8 @@ tiers: # 没有本地服务时,把它指向 openai_compat 的远端小模型即可。 low: provider: openai_compat - model: qwen3.5-4b-instruct - base_url: http://127.0.0.1:8000/v1 + model: halogen-qwen3.8-flash-next + base_url: http://192.168.200.11:8731/v1 api_key_env: null timeout: 20.0 temperature: 0.4 diff --git a/deploy/cybergril-api.service.example b/deploy/cybergril-api.service.example new file mode 100644 index 0000000..cd94143 --- /dev/null +++ b/deploy/cybergril-api.service.example @@ -0,0 +1,28 @@ +# Review/install by 总工 only. Single worker; never share DB with demo or tests. +[Unit] +Description=CyberGril R1 persistent text API +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=cybergril +WorkingDirectory=/opt/cybergril-api/current +Environment=CYBERGRIL_DB=/opt/cybergril-data/cybergril.db +Environment=CYBERGRIL_MODELS=/opt/cybergril-api/current/config/models.yaml +Environment=CYBERGRIL_WEB=/opt/cybergril-api/current/web/dist +Environment=CYBERGRIL_HOST=0.0.0.0 +Environment=CYBERGRIL_PORT=8310 +ExecStart=/opt/cybergril-api/current/.venv/bin/python -m cybergril.api +Restart=on-failure +RestartSec=3 +TimeoutStopSec=15 +UMask=0077 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/cybergril-data + +[Install] +WantedBy=multi-user.target diff --git a/docs/frontend/browser-results-1440.json b/docs/frontend/browser-results-1440.json new file mode 100644 index 0000000..065155e --- /dev/null +++ b/docs/frontend/browser-results-1440.json @@ -0,0 +1,12 @@ +{ + "checks": [ + "1440: ordinary entry, creation validation/failure/retry and reopen", + "1440: both themes, home/talk/days/memory return, relation confirm/cancel/save", + "1440: private deep-link guard, cancel/exit isolation, IME/Shift+Enter and XSS text", + "1440: failed send/retry, voice end reachable, no-camera simulation, reconnect failure/retry, save failure/retry", + "1440: privacy-filtered away summary and proactive message", + "No uncaught browser errors, API/external requests, microphone or camera requests" + ], + "errors": [], + "requests": 31 +} \ No newline at end of file diff --git a/docs/frontend/browser-results-390.json b/docs/frontend/browser-results-390.json index aa792df..27ece3f 100644 --- a/docs/frontend/browser-results-390.json +++ b/docs/frontend/browser-results-390.json @@ -5,8 +5,8 @@ "390: private deep-link guard, cancel/exit isolation, IME/Shift+Enter and XSS text", "390: failed send/retry, voice end reachable, no-camera simulation, reconnect failure/retry, save failure/retry", "390: privacy-filtered away summary and proactive message", - "No uncaught browser errors, external requests, microphone or camera requests" + "No uncaught browser errors, API/external requests, microphone or camera requests" ], "errors": [], - "requests": 36 + "requests": 23 } \ No newline at end of file diff --git a/docs/r1/DEPLOY-ROLLBACK.md b/docs/r1/DEPLOY-ROLLBACK.md new file mode 100644 index 0000000..05185d1 --- /dev/null +++ b/docs/r1/DEPLOY-ROLLBACK.md @@ -0,0 +1,15 @@ +HEL-780 · 8310独立部署/回滚材料(总工执行,尚未部署) + +1. 审核确切任务分支和提交,复核8310空闲;保持8300和其他既有服务不动。建立 `/opt/cybergril-api/releases//`,构建生产前端和独立Python≥3.11虚拟环境,按 IMPLEMENTATION 安装。服务账号示例为 cybergril,由总工按主机现有规范准备,不默认root。 +2. 准备代码目录外的 `/opt/cybergril-data/`,仅服务账号可写。首次上线保持数据库没有真实档案,不运行创建POST、不替用户创建。已有数据升级前,停止该API,用 SQLite backup API 做一致快照至独立回滚目录;禁止只复制正在写入中的主DB文件而漏WAL。 +3. 将 `current` 指向已审核release,审查 `deploy/cybergril-api.service.example` 后再安装服务。模板限制单进程,端口8310,持久数据目录独立;模板不改防火墙、证书或反向代理。按现场systemd版本核对保护项和目录权限。uvicorn直连SSE无代理缓冲;若后续接代理,需另行配置关闭SSE缓冲。 +4. 启动前确认 `web/dist/index.html`、models配置、虚拟环境可读和数据目录可写。启动时自动执行幂等增量建表、epoch加一和pending取消。使用 `GET /api/health`、`GET /api/companion`(首装预期404)与普通页面做无副作用检查;模型健康异常先记录,不改共享网关、不循环调参。不要以调用preview之外的写接口作为上线健康检查。 +5. 审核通过后由总工记录服务URL、release、SHA、`journalctl -u cybergril-api` 日志位置,再交用户创建。普通入口 `/`,演示 `/?demo=1`。本轮不切换8300入口,不合并、不部署。 + +回滚: + +- 停止 `cybergril-api`,记录当时release与数据快照;把 current 切回上一已审核release,重新启动。数据目录保持原样,不随release回退或删除。R1迁移仅增表,保留旧表;每次重新启动epoch递增。回滚到纯前端版本时真实API将不可用,应明确告知,服务器数据仍保留。 +- 数据恢复与代码回滚分别处理。需恢复快照时先再次备份当前数据,由总管确认可接受的数据时间点,再由总工停服恢复;禁止把本地测试库覆盖用户库。 +- 取消部署的清理只针对新建8310服务及明确release;不删除 `/opt/cybergril-data/`,不停止8300或其他项目服务。 + +限制:单用户内网、无新增鉴权体系;应沿既有内网访问边界使用。尚无产品级备份功能,本文的升级快照属于部署运维操作;真实页面不会声称已自动备份。SQLite单worker是当前约束,后续扩展需独立设计。 diff --git a/docs/r1/IMPLEMENTATION.md b/docs/r1/IMPLEMENTATION.md new file mode 100644 index 0000000..1573f7b --- /dev/null +++ b/docs/r1/IMPLEMENTATION.md @@ -0,0 +1,64 @@ +HEL-780 · R1 实现与安装运行说明 + +基准为 main `498429399812a0776dadd4b56662d31b455a9ea5`,保留原前端两主题与统一样式,未增加 CSS 或运行中的服务。代码审核后由总工决定部署;施工阶段未登录部署服务器、未占用8310,也未修改8300、模型服务或用户档案。 + +- `src/cybergril/api/app.py`:单进程 FastAPI 工厂;字段中文复验、无副作用真实排盘预览、唯一档案、只允许关系/称呼/边界修改、历史游标分页和 SSE。复用 `Store`、八字编译器、`ModelRouter` 与 `OpenAICompatLLM`。 +- `memory/schema.sql`:只增 `companion`、`api_meta`、`chat_message` 及索引;既有八字/生活/记忆/关系表保留。WAL;启动自动加 epoch,并把上次意外退出遗留的 pending 标记 cancelled。 +- `web/src/adapters/real.ts`、`app/RealApp.tsx`:普通入口只读服务器数据,404首建;复用创建、聊天、管理和人物静帧组件。真实入口不读/写 `cybergril.demo.v1`,草稿使用独立 `cg.real.draft`。`?demo=1` 仍由原 App 与 DemoAdapter 处理,零 API 请求。 +- 创建第二步通过 `POST /api/companion/preview` 排盘,不写档案;仅最终确认 POST 才在一个事务内写唯一档案和八字。SQLite 单行主键与事务保证并发只成功一次。生辰按 Asia/Shanghai、公历、1900年起、满18岁复验;时辰映射为对应双小时内偶数点,不引入出生地点/真太阳时规则。 +- SSE 使用同一 `turn_id` 和持久 `epoch`。前端每次发送先同步 epoch,并可处理发送前恰逢重启的409。轮次门槛继续拒绝旧事件;取消/退出直接 abort;服务端取消并等待上游结束后才开始新轮。响应缺少完成标志、空回答、异常或60秒超时均作为失败。部分回答丢弃,用户消息标 failed/cancelled,失败与取消的轮次不进下一轮上下文。成功回答可按同一 turn_id 回放,重试不会重复用户记录或重新生成已完成回答;并发相同轮次也只生成一次。 +- 模型三档均使用批准网关与模型,保留原 temperature、provider timeout 与预算。dialogue_main 路由限制最近20轮,再按档位上下文预算保留完整近轮;不截断当前用户消息。每轮最多默认1024输出 token,usage 缺失时按字符数保守估算计账,沿用原预算降档策略。原 BudgetGuard 的预算是进程内运维量,重启重置;不是持久化费用账本。 + +API 契约: + +| API | 成功/错误 | +|---|---| +| GET `/api/health` | 200;epoch、是否已创建、模型探测与能力声明。模型 `/models` 2xx且包含配置模型才为真;5秒上限、30秒缓存、并发探测合并;不返回地址或凭据 | +| GET `/api/companion` | 200档案(含只读四柱/白话),404未创建 | +| POST `/api/companion/preview` | `{name,birth,hour}`→只读底色,422中文字段错误 | +| POST `/api/companion` | `{name,birth,hour,relation,nickname?}`→201,409已有档案,422字段错误;拒绝额外字段 | +| PATCH `/api/relation` | `{relation,nickname?,boundary?}`→更新后档案;404/422;无任何生辰/底色修改入口 | +| GET `/api/messages?limit=200&before=` | 升序 messages、next_before、epoch;每页1–200条;按id稳定排序;前端逐页恢复 | +| POST `/api/chat` | `{text,turn_id,epoch}`;UUID turn_id;1–4000字;SSE chat.delta/final/error;404/409/422。chat.error带中文phi和retryable | + +已接通:真实唯一档案、排盘、文字流式对话、服务器聊天历史、关系修改。尚未接通:生活循环、长期记忆检索/整合、主动消息、语音、视觉、天气、素材包与服务器备份。真实页面明确不可用,不填示例经历或伪造执行结果。当前只有近轮对话上下文,不把它说成长期记忆。 + +安装(Python ≥3.11;隔离环境,Node/npm用于构建): + +```bash +python3.11 -m venv .venv +.venv/bin/pip install -r requirements.lock +.venv/bin/pip install --no-deps . +npm ci --prefix web +npm run build --prefix web +``` + +`requirements.lock` 固定本轮已解析的运行时依赖;开发检查另装 `pip install -e '.[dev]'`。默认 Python 3.10 不满足项目要求。服务不自动读取 `.env`,变量需由启动环境或 systemd 提供: + +```bash +export CYBERGRIL_DB=/absolute/separate/test-data/companion.db +export CYBERGRIL_HOST=127.0.0.1 +export CYBERGRIL_PORT=18310 +.venv/bin/python -m cybergril.api +``` + +从仓库根目录启动;默认模型配置 `config/models.yaml`、静态产物 `web/dist`,可分别通过 `CYBERGRIL_MODELS` / `CYBERGRIL_WEB` 指定绝对路径。本地默认数据库在用户 `.local/share/cybergril/`,生产必须显式指定独立数据目录。**单进程单worker,不启用reload或多worker。** 8310由总工部署时使用,本地测试脚本使用随机loopback端口、临时数据库并在结束时关闭服务。 + +自测: + +```bash +.venv/bin/pytest -q +.venv/bin/ruff check src/cybergril/api src/cybergril/providers/llm/openai_compat.py tests/test_api.py +npm run typecheck --prefix web +npm run lint --prefix web +npm test --prefix web +npm run build --prefix web +# 下列为可选浏览器工具,只装在测试虚拟环境: +.venv/bin/pip install playwright +.venv/bin/python -m playwright install chromium +.venv/bin/python web/tests/real_browser_check.py +.venv/bin/python web/tests/browser_check.py 1440 +.venv/bin/python web/tests/browser_check.py 390 +``` + +浏览器脚本启动/关闭自己的HTTP服务。真实脚本只向批准网关发两轮完整对话和一轮中途取消;随后切同源隔离桩验证失败、重启和立即新轮,结果区分真实模型与桩。环境若需额外浏览器运行库,应使用现有用户级库,不改系统/GPU环境。本轮复用 `/home/xiaomo/pwlibs/root/usr/lib/x86_64-linux-gnu` 设置 LD_LIBRARY_PATH;它不是部署依赖。 diff --git a/docs/r1/PR.md b/docs/r1/PR.md new file mode 100644 index 0000000..9490bf9 --- /dev/null +++ b/docs/r1/PR.md @@ -0,0 +1,7 @@ +普通入口此前只有未接服务说明,无法创建真实档案或与模型聊天。本次接通唯一服务器档案、无副作用排盘预览、真实SSE文字对话、历史恢复与关系修改;演示入口继续独立使用本机资料,零API请求,保持原两主题和统一组件。 + +服务端复用Store/八字编译/模型路由与OpenAI兼容provider,增量建表,持久epoch随启动递增。取消会关闭上游且不把残句存成完整回答;失败、取消轮不进入后续上下文;同轮重试/并发回放去重。未接生活、长期记忆、语音、视觉、天气、素材和备份均明确不可用。 + +验证:Python92项通过(原78+新增14),前端14项通过;typecheck/lint/build及新增Pythonruff通过。真实网关两轮上下文与一次中途取消有浏览器证据;电脑与390×844独立上下文、重启epoch、隔离错误/超时/断流、演示双主题零API请求通过。手机为浏览器模拟,无真机。验证细节及版本边界见docs/r1/TEST-RECORD.md。 + +运行安装/数据语义见docs/r1/IMPLEMENTATION.md;8310部署回滚材料及单进程systemd示例已备。数据必须独立于代码release,回滚不删;首次上线留待用户自行创建。本次未部署、未合并、未触碰8300和共享模型服务,交总工独立审核。 diff --git a/docs/r1/TEST-RECORD.md b/docs/r1/TEST-RECORD.md new file mode 100644 index 0000000..70468b1 --- /dev/null +++ b/docs/r1/TEST-RECORD.md @@ -0,0 +1,20 @@ +HEL-780 · R1 自测记录 · 2026-10-22(北京时间) + +环境:Python3.11.16独立虚拟环境、Node24/npm11、Playwright1.64无头Chromium;生产前端构建;服务均为本机随机loopback端口、临时SQLite。未部署8310、未连接部署服务器、未访问4080或改动8300。真实模型仅连接批准的 .11:8731/v1,不安装或修改模型。 + +| 批准方案2.3 / 本卡补充验收 | 实测 | +|---|---| +| 1. 全新首建、真实排盘、必选关系 | 电脑四步浏览器通过;第二步四柱为庚辰/丙戌/己酉/乙亥;preview后companion与bazi_core均零记录;最终确认才写。API绕过校验:非法名/日期/未成年/早于1900/时辰/关系/未知字段均422中文错误;5个并发创建仅1个201,其余409;底色字段不可PATCH | +| 2. 真实流式、取消和新轮隔离 | 网关两轮完成:设置“蓝色风铃”暗号,次轮直接答“蓝色风铃”。第三轮长故事在首个delta后取消;用户记录cancelled、无完整回答入库、上游请求已结束。隔离慢provider紧接新轮通过;另以真实OpenAICompatLLM连接隔离HTTP网关,验证浏览器侧断流后上游HTTP响应在1秒内关闭 | +| 3. 重开/另一设备恢复 | 实际reload及另一独立Chromium上下文(390×844)恢复同一“星河”和两轮历史。**没有物理手机或另一台物理设备,独立上下文与手机视口模拟不称真机** | +| 4. 重启epoch | 同origin停/启本地API,复用同一临时DB;epoch1→2,原页面直接续发通过。API测试同时验证档案/关系/历史保留,旧epoch409携当前epoch;前端单测覆盖过期epoch、其他turn拒绝 | +| 5. 模型异常、失败草稿与去重 | 隔离错误provider抛异常→中文chat.error,隐藏内部详情、草稿恢复;同轮重试只增加1条回答,无重复用户记录。后端覆盖超时和上游提前结束;SSE单测覆盖UTF-8逐字节/CRLF/多帧/坏JSON/无final;并发相同turn只调用模型一次。另验证较早失败轮在较新轮完成后重试,上下文仍按完成轮次正确成对 | +| 6. 演示隔离 | 演示电脑1440与手机390双主题全流程通过;每尺寸23个本机静态请求,**零API/外部请求**;无麦克风/摄像头申请。真实入口用存储读取陷阱断言cybergril.demo.v1零读取。正常页面无演示控制台;日子/长期记忆/语音/视觉/天气/素材/备份明确未接通 | +| 7. health与history | health模型要求/models成功且含配置模型;401/404/503及其他模型均false。5秒超时、30秒缓存测试通过;不含地址/敏感配置。历史limit1–200、确定id顺序与before分页;非法页大小422 | +| 8. 构建与测试 | Python **92 passed**(原78+新增14个参数化实例);前端 **14 passed**(原11+SSE3);typecheck/lint/build通过;新增Python与provider的ruff通过。CSS仍为原61.20KB,JS约207.70KB/gzip67.45KB;未加覆盖样式 | + +浏览器截图在本目录:desktop-preview-moon、desktop-real-chat-moon、desktop-history-moon、desktop-model-error(该张为隔离故障)、mobile-history-moon、mobile-chat-fog、mobile-relation-fog。原始真实网关对话及逐项记录见 `browser-results.json`;末版隔离复测见 `controlled/browser-results.json`,来源明确区分,未拿桩回复冒充真实模型。 + +最终运行时锁定为requirements.lock(FastAPI0.159.1、Starlette1.7.0、uvicorn0.54.0、lunar-python1.4.8等)。初次真实网关浏览器证据采集时Starlette为1.9.0;锁定后在1.7.0完整重跑92项Python(含真实OpenAI兼容HTTP断流)、前端检查与相同浏览器关键路径隔离复测,均通过,避免为凑线重复调用共享模型。pytest有1条上游TestClient/httpx弃用提示,不影响结果,未另装不需要的httpx2。 + +没有覆盖:真机浏览器、正式8310/系统服务重启、生产数据迁移/权限与真实部署回滚、长期并发/长时间运行。上述部署验证由总工在审核后执行;模板与数据保留步骤见DEPLOY-ROLLBACK。当前单用户内网、单进程;近轮上下文与进程内预算不是长期记忆或持久费用账本。 diff --git a/docs/r1/browser-results.json b/docs/r1/browser-results.json new file mode 100644 index 0000000..20c30ac --- /dev/null +++ b/docs/r1/browser-results.json @@ -0,0 +1,35 @@ +{ + "checks": [ + "desktop: four-step real creation, side-effect-free lunar preview, mandatory relationship", + "real gateway: complete streamed reply and second turn recalls 蓝色风铃 from server context", + "real gateway: cancel after first delta, upstream request closed, no complete assistant row", + "independent Chromium context 390×844: same companion/history; both themes; IME and Shift+Enter", + "server restart on same origin: epoch 1→2, existing page sends automatically, data retained", + "controlled SSE model error: Chinese notice, draft retained, same-turn retry without duplicate user row", + "controlled blocked upstream: immediate cancellation/new question isolation", + "mobile: relation cancel/confirm/save restores from server; unfinished life has no demo records/console" + ], + "real_dialogue": [ + { + "input": "请记住我们这次聊天的暗号是蓝色风铃。简短回应一句就好。", + "reply": "星河暗号记住了,蓝色风铃。", + "source": "real gateway" + }, + { + "input": "刚才我们的暗号是什么?请直接回答。", + "reply": "星河蓝色风铃。", + "source": "real gateway" + }, + { + "input": "重启后继续", + "reply": "星河答复", + "source": "controlled provider" + }, + { + "input": "新轮不会串台", + "reply": "星河答复", + "source": "controlled provider" + } + ], + "console_errors": [] +} diff --git a/docs/r1/controlled/browser-results.json b/docs/r1/controlled/browser-results.json new file mode 100644 index 0000000..c851403 --- /dev/null +++ b/docs/r1/controlled/browser-results.json @@ -0,0 +1,35 @@ +{ + "checks": [ + "desktop: four-step real creation, side-effect-free lunar preview, mandatory relationship", + "controlled provider: two complete streamed turns", + "controlled provider: cancel after first delta, upstream request closed, no complete assistant row", + "independent Chromium context 390×844: same companion/history; both themes; IME and Shift+Enter", + "server restart on same origin: epoch 1→2, existing page sends automatically, data retained", + "controlled SSE model error: Chinese notice, draft retained, same-turn retry without duplicate user row", + "controlled blocked upstream: immediate cancellation/new question isolation", + "mobile: relation cancel/confirm/save restores from server; unfinished life has no demo records/console" + ], + "real_dialogue": [ + { + "input": "请记住我们这次聊天的暗号是蓝色风铃。简短回应一句就好。", + "reply": "星河答复", + "source": "controlled provider" + }, + { + "input": "刚才我们的暗号是什么?请直接回答。", + "reply": "星河答复", + "source": "controlled provider" + }, + { + "input": "重启后继续", + "reply": "星河答复", + "source": "controlled provider" + }, + { + "input": "新轮不会串台", + "reply": "星河答复", + "source": "controlled provider" + } + ], + "console_errors": [] +} diff --git a/docs/r1/desktop-history-moon.png b/docs/r1/desktop-history-moon.png new file mode 100644 index 0000000000000000000000000000000000000000..73141cb242fc9859b329f7d1e5d8c77bbf47c57d GIT binary patch literal 730147 zcmV(?K-a&CP)w0ssI2*e?Nv000mGNkl=NV=I4Gc2_LSY4h0VITEA;~tf5DP0?mV@?R$@@#c)9*#A zUz3<-p)t+;@ySl2n`+VoS?;(44d)|EaoDNmh z)m7Eq)!~dT+wdI#fCxZ9)o=A&Kf1s5{|H1N6Zo0xV&tB&3gk0$4v2pV)N6c0ol?Q< zd6)q-Ko6kb{8E!Yl&G;&LqbD-{VbrP#-!0Q;Si`pLs?`4a&>*lNP7!{z#jgUHFboC zQ~=L4hS)R(Vx$e2641;Gc_f$9ig*Tz#t=;u3x%4-N?SOxAyLuBQc|Z-mLL>OYM73_@Px$7$hfcRI=GJV4_iL1<}roCXEKevQP&ACaJI_ZK>?z zU>I}(gp15*H|VBfRWCGE1TL%=^Pxo>u#2daQYn`JU4+%bqOwu&aghTtwg`v@!#9Q| zmGN=~GAe6q5#|WVSf|!NvN0)I11F7uY?y5CRz%RkheBawi{zLeXaLhW>3XHyh@_*o zR8$Zmirq<+WRErc39P_SF@?*SaY~v>k|>@jU{G0l%~KyD8xSF2w{SRu`G5sU2vuv* zW-i9Pi4(MJi+fw7?R86Fi{|St2e_u~90?iK8(^)oHZ8Bah7U>;S@68=4Sk2^gKq`R zhd9eYi#4=bmJbB1V?oRt#1x6kw0jmli2A1EmbS8##!5aoSw$%iqEM_lWjp;8XQ;0t zAz%w|KwrW{?2*(CH;Nv>)l?GFEF?$}(HXm# zkz@g1DnRpArSv2eZkUWLaSLKJIfg&sSxo|+M z#3)HfFylHwRZqYd@tS#~flGGgPKsXvooHyUm`?ruSR7C{A3vjRVQDWh)(M-@Jh%Zr zL}LIvi*pI2*y9QBVKak>aVo^e z{5{cs4y30Qm+HVEY7`Aw-!iVb^Pv;osIcJs0?g${06ln54I4jeDN@|;hT6tXivdUizW^1qbV;{?yYAV4`nt;J2NJQ3}*G5TTV-3xCp_ZkgkmQ zXk``o3DCAh9%|Ae8hk=CYAgeBD6+LY7%7K@4;g+%jgx%`FdtCxfy+>tR5X9XtK)|7 zpZN?;yQ?~(rVjJ)0o35x=!N72f>vov6ToC~8$6qn~A#!q@I}O@(3VwPF&X0#*+~->yHyBgsf~K zGXkxIgd`U~%JI?hMGTaJtn>P|j|Ah%Yo5~nfO$%)PeM{sjNHxvf*H=`(f!M*}YlR{UxP_8NVzhbj97PiepzQ@}Q!doH zXfD7skyd>yt0?P9sJk=_nqGj!I4Cf{Qk_G?`*Bc}LNUb3(N>7SLO$0da{Uy;4j`@` zhsatx77WkQTuUz)QkprmdtjgGY@||=q9LgY3erTh%#I06k(a_W+Oq*6(Vc9NQX&ih zn=erlC-fNPJhep9$XVF&(d#XW*HG5xigbap0BpAEPKp4;t{L9D;qnS~Rlf;yA^*;O z1j#hx;3sd7f+ckP=`%p$r5(>b4N6M~mfH&rfyf;h*|}#i&@j)gS}~rTGpa?Rc`!6E z&&>9c1eZ3-Mu`ggn>3UsBCOq(aK*YXIBKI!WEQWEI!%KyR21`Itkhs!plV3g+(f)? zMyEStezo33DPWNX)K;Znm@|CH(FPe)6)}3f6H+uRmKM$69*uN9D%FTvP~!q`_q1^} zt)t=O@+hAM2|khTrLh$zVXiKP=`ICx;Ov#;oLn~3%2}+JfaxkB&2DH%of85Q)s8|Z zQtKgMreu(|JXsP|oHLI+pr;tg@}_k~mo2fN`RTbPbuVSr9V86_7WGlmZpZG>^GlBqh0v8#b^e4^5-378~kX$xhM zi!G(L;Dg+-rQmdtWI=FjcToGAWR1L*%jI*(@RmTCCqF(!A+wYtqLnq027p03Zam#3 zr-^PYq$mt(JSByah&>jVCE9Sqbq+*k&gz)Q4a0T8xT}+T0ydH`0byDUapRoN>a7eV zEYKM|&X`S)5~bgnH)!JWjWVLRnHmhZ4m!X(9lml;$cV;Q53g-e5Eq z$}g4-%DRV~L@JsZcO2 z{<+sA^yLUZ=g-E2j(17ThQ~TC?AP`dx84>i*kn7|B!td4bf%&6b?lDp30jO|Z@6Bm zfKEdyfft0lXZg@A$i5Olx?aTui8=yJ_@JY+wxzofDYPpMlR2M15@8_80bx%w*WLCSiB z_)PQqwjeJA5k5tY#z0#yi;LqEMyJ9El7=W_zLo>pF5AE<0x27$x;^5ZeaMdQkdL$> z>t(Q5aRHF~!l0lvUS1(C;KeL46N2duD7Pc2Th7%OwaOML6Rq1llPf{h#@kMZO1D=e!biu%94{=SkDtm#LZcYkPsVodn$@m2)w z1M_>3LG4p=9uabEq#om52Pv7#3t_B8%Bi0Cb)IwSKhNzjOacbaG#V*D zn=g8EK`!t2d`Sd7(LqyMG?I5%SyELANwF9lCr_qG082U^3q>rs=r}>KlMTNJ#hHxR z+M;S26Kd~Kv#*^psTkOor81=2Jd$P?ptW{|6l}?Fl7mKNXn;V06gn(GLnb@dHpOSu z@urbM?A@S3g(y=~3&zJI3293JfuUMujX|Z0)MtdeRp;yVx_Qkq zqTCCh7d!ZN1xK5|T#7FvwAUei@G)Vupnv}* z8JGw;V^(+;j<9yQO-vVBXvnCC0 zILqOTd}ZryGDS~qNcEM;An<-Q=~(yv3-o|7!#zFFu3!8~?7Cb}7V+Y&CVaZ3bF~y@ z4R%yXy6`9?a9a;tJ?cA@0zwVfMzpfOF_(MUhRWC^I{N~ z*?Y(!v_u+2Z17;*>h+CS=U+IzZ8&arTT7XaL2U z7?cF!{#n0gM@SR{$Mo6eLdba_qD1EkMXCWDBNszDl0AK0j=cq1uhiLnJ2#47WpG$?VUa0sH=Gx5O2RnwfMlM3;W@&PPN zi5E0In5%EhFLXHN50l1qgk<#?Gx;;PKu8Eeg22ELwYBGka`ip$@jAv-f=sKW9LQS^ zR!(y!LSOU>N}Kje-UfRvT!YuX^ROQcP&mKy%_!&z+#?FhNL zTRvdXZw_KPltLZ6>NWU-xQksRy~w6!W1E7;&t!HFQ6U~wz|5=3Lh=crHAgOBYpP%r zWgB9k#$i@HlHvr39BYV}F&=sJ+13ZY-AGffFld5RJCZpj!v*-*46zzRRB2@Oj)UTS zYqZFKiI#9O&$$y2hNBeBjS%dJ2F()zwhC0_YA${NwUAp@#yErvbym#dQ?sbm2?IbG z?+iOyOt}h^jqs*Oj0$8Kd8tAuZ_QzlK$J^zqB4jD)_dV<0~~BD000mGNkl)fjvOD~3)!Q;p?S~76G-nt=t5mfAnRS@EJ4y+ zeCkr8k$X~sAXsV)A19!Aj}_0EF;psQy_R6-z(|chMZ+KiiFi5tqPWjR+|%LU$HEej z5ad262|)21NJh|Q!3(Yw#*D5ACPfZF5CjS>;o70s_<+2;C=dYFjeyb~viz7bTAl;V zuDH;D!+%I!DKVENEQw_S%L8zGEcwGWPHF{3Xy$>6w&YpI2u}h+(ijYmELFvTu|g0v zUWo{>Bs7ic7044oU@w7|JOZx0(@j|uIgJD&MVuCOvQ|L{lz0H?qYnl-b3|7-M zI7NvP%pis%z@gPfk%tYiH`@(*GI)a%$Xjs|jV-t&Hopl*1u5zUe9_otR7LD*Q+z=& zm}m>A>t?DDJ-Dl(pb#zhBqlm#Tv2XVEWF}WYgt>EC*+(o?S^TYPAf; z@_8C4AyBuKDo=^Dq(}fvCok3j(!nUIo;0V30#0Zt=5QaZ+l1k zz7Mptc=6}TE06WfS1D@puD*eWp% z7CT0QQ5s4}Go`;II+Fxq7CQ=)* z9U^}yk_upmD!>6vZzjK>AzsLlR$8OTo1!EjPWr?lcKVdM-7cj@D79aAwh;Lf~-jdsR@}@ zFI&%q&eqgmV=yfh;P#@y@$vNsPMbo}+%(UbJHsWOpckK+ta zH=4q%OIU10$LzQuiu??b2F6Z3-4}@-QSHSs^r6sjR8D{3#zb|;sHa&sTlDs*4@|Ix zAQOg(K$AiS{t!@fv&tPSgIya!?=jYrN()1jDHD3pov#Fo!hRL2Lmkh^7~A6tPypa3bEW>1{=J+ovXK-kA2 z>yZFGflkG4M8Rc1xN^ zqDWK{D_96E z-~@T$15astWuT&MLBL$La{d8=ktTp@g*7*xIw_rpic5;>YLP`sGpGvmosb1 zMB5o(ga<={4}MdfroXT2LYgp!Pnva_5|YNnFxmh#d{C*{flEodLqlydFYwP9TA~e& z7RBzqHU)^YD==}op(P|ID8yq_*NcSyOMPC1RPQNZl65C#Il#{>6ncEYsHLOF>R#MC zfcejl*!HEe_PJRW&N$M*qDlUK5O1P; z4Ih|1jDr)dFoCtnQQSTG5H~2e(iz^OL>a0C zwd$DwaxDminloWnK_8t?kdm~xr6CxOU@5&Ag~}`K5P>qSpGbx70OA}|)!ygeeP@af zDB+tfH;fYV+b2WsN4j?*0Y5k``ovd~L^_Zqhc^X_vv)2bI*}<RRJ{1Ful^;Xj6 zbnZ1_(u#f!fE-MaYXd?>WP&M;#;b)9HASgt_{xppMamNzGKKwhAr*ov0+}KJtKzav zP~tZx1rsTgiJ~P*F$QI+I#-k3k3k4~j)k);HH)Dg0pq>PA-kZt9PT6SGP16CoWT zqDb_VhuBPPG7$OW+-0#kDkREtd85YQk#47SN)uI>1m+b&uRsQ3frf|%q-Ngw7w z6ytapmbVEdI;|uplt=cmxKU=vr;xFyia}tJ1kTV&DjFIR#$c2ORUGZCnIEH^7!r5{ z$5;^^ z_)(=lfqdu84J*jJ6Twx$Ia_#7hhRw}zph$gw#$44QlIeRWoTvPDDVKBcOxwHE53{A zaD>jaaQirv3Q2_`VcHN)8VW37#Cjym4vPGuO8y95+;PLO1aSZZ41x2WIFuB0PXNjm zMoFA&6B1Pj^hZ(7NC81&sS}Fb3ufr?XiR<*dy1?%NSKZ}niU^F$P8tZ(~5-df^URL#IS@@(QF=EZZ_hi6AQFN42B^c zaFw^b=i=&^o_H3urPhK>Wjf=R_$?m_FkAQlgR+(Qz;qZIf;9`7F#ZXBb&(!2z-2tP zvPqRLtam46l+lJx7JE>T_Kis)kTyqSX`>Icw4gQY3FpP2#HH&Tx^xj&R={l}a#PCQ zBXg+&Wh;vVgtDl8xw46U*M!FP;{wdGMt2F0t6Htitw4eg!{QS83st}%R*1IU)1J5u zaY@Ut@a*2hSlcK*K;gy#MY^8xW_I+X^kUHxk(@TLCWw6;feNepMD6fZw~eu!$WKwf zlNMH-&(=%#sv=(Rt&rU_fkq`bNRSTVkfj1s_#szex0el`4*|Ix!U6Jl-d9t}pG*lW z9C8p4`G5kM=D(^|ly%c=c8`o{8kCkf;)%H0tIycMXj(Y90Eq@>!BWAauO%%Mj||Un z47fE#2S#y*r$8r?+*rskAVZ0?Y%=l^YvO}gTH_!K0Fb=H!q!bZqi#@)74<(pv*8d+ zj<*LsTGHC{`_tmeq2JTtGK7hyIqd?v=<$ zY-nqAn^Qo@w_+OwgVz&o5D&)3vqCBZ=hPQt3@>Fz zp>naxXDq!sqQKTN$(A4vJKFhE^?9;2`|_2yRU^scVyz+i>{9_)mxN1C@tAbf7||hK4&_e!~YBSEvoD0`a9y`N}v|A%N2K z9>56yU`Z>qx@l<-JM^_k&}q_Ake3{A8?RfFataNK54v#@%sL;;+3>bF#WGDRw z2=$>9(suL~iImbLdjxHMZ$T+zk@A*Gt_e`yT0)|@z6T1pvQ{+Z0}&grA@~qV$qhX~ zC3%H>KWxNB0S{GD&#IWw_q-g_KuI=W1_qU%t1k;9mFhVNWPCT`MY_LJ*-1rnE{&C_ zl@HXl_Nu7Z_A3-Y*@YR|4MESLq000mGNklk#EbPM0G zErLYzVA@nN-Ux|IW@uW#wNp)D$Ap8S@sRg>i0Js7k+dK?I@&{m0Su6D!Ixyyqu3Qr z7MT>%_2q3VM*Jvd(BucFzu$=y`=+pWL)0RriX$V!CZq&0_Y9C#Q4qM$#99J{c2M|jTF{5+-2nA*nu0JI=pAAt zr8p2`T5O0Qkg%;B4#HeXZHJXLo@7;x=CZ(sQ?uG&4l$ zWp?*;P(WYt*>5rr$A0;a48Nr++?`{ZE6%sQN5{XA-31n8s~5ho4)Ief0hXWjiwGly zh^4VTdn;m$*bFWk6#TxNcz(x$W@xGWtJQ5^4-!XUSz=;_a9*d$9Mj^N1u$4`g+jXG zdBPLKY&j@Rx(xtSznM*oS;faZExsKJBq8XACEe_xn5PN0^KD{Wp7qRKzMP$3B~YG&}ZHI3K;fF0r6DX$y=}05p0)M~^ zqQxmD2BBxgl|D)>MjkOSJlko+)fLes#P$#qXzoMOysZr5(VA!o{HGi3{dOGsOtZXL zOSoV~6UXm3Q)o^Mf~JKOd$E1_>&s0%QlH}WNWsW*MFWSm7zu{QXNvU>k{+*43veV% z5ifPn*P1{xUMr0#m&D8bj&`XFF$3n{gU$n`cw=cV^bJ8%7>*uf$~pHRzEz79kGHk=N610Qpv;!9C#Oj1_IK? zX5uYUz&X%qM;6Y05}17uiVwN9U>|yk=4LGwiM^gieWVrQjent#hfkUh1uK`MLFHF8 zkoZ=MY>Ymj*wsynwgeX>5R5d1yW`#9JI?`0IRyWE$w(0;Iuro)Dd|1^=DkqkAb_Bk zP&0JPKb!&MTc>~$AP&%GA&{Q^t=U!3;K@n>XAbp677S!rPg1gLyzF#i|6pAYN7qUuO=nGtZ;QH0G5yMT8;1D2lH&?)o2wEz2 zjZCc*;lNBT7B%Cr3dRf-5xx}1sWQqoGiqH}Q7WqeVEC#^k}Rr}Swqgzo8XpRgxFD^ z!wZHUaWu)+N7pE-Fd8$W;N{FJ28~4+WKt=JWSoD?WI%hX2<4h%P(W#RoPD9;p#YPJ z;yF}ESUdu~aW}}VVX$B^1!>K3Ov!k_s*syvsFf!g9WVp7f~a_6DZV;LBxN0S0Ra-g znth0F5mbnYl)&kmF9PKFfuPt)MLK&bfmaeXb_K;T73tJ%@6^rM^3?SzYECFF-m1Hj0L-sltI>a&x4N9J{f^s!sn0PGZ z$0#3NLe?7yiIymp>I&P!DFGi6Qq;P<%>kicxg2>5+RA}w$^9!^G88r<&F`r+opV}( za=}nXe68R8-=!y(vY@QD?iNLf~dJl-q&%*;OsbWqzOV5FI(&DK+riDqhO*B?T zb>pVdoXXLIramOPr!>g>&{QK)n>6Oz%S($h<6yAogX(e;u&HVRMi-gd%N9gQA&;K4&pbGMEm@0Ehqk@t{UZfS8}GLs?N5yXTP@y4Q{ zNX3HPb+n0+3Y&Wnc2uW%20}}9P?Iim8wJYm#Xtip`${Q|5-tQI;GIT(4cc9i3kr-Y z;Xj3%s}3f)Gel{ni3;Td8ZvWrq^Khzr64$QSB#c1-`;~=_ZFVOg__UNlSxpbCFD-p z!dl9hUpX2;TZLKx4O+vi&@>uAgU1PP^{4N*%+*^hxCce;iBT|f0)s_8 z(j)c3`Mm^4an}m;cNK=zH4apYo?8Ws4az3vunir~g*-}PY&3zC>GKq*Q!C8{<@pvi z)q#kZZlPoWhXa-sAT=b5zwPA)2F!sN3@0-_^hTIh%DUs{uLmkSoTUbVhs_C&qNOta zdCKOG?H=sAEaZA5ySoiCJ#I#TOa_%R+HeaY$?pkK7)wKf+&fPnH4%XdOvj&LV|NT> z2`V*oi!n4A?ARgAXC$AJ=vHYNJ-;&KmeZM?W*hGn>g9?V%j1; zla}1*NE49~V!1_RcFIm3OZJ>FSCu$W2RNg-Qk*P5-b=)A*{M_ zL_SZJpkHQ^6(TR)vE{sb!Gm!c?P|osIn<4$=~J{y_M+L-q#BBkC3yBlApRpXkhoD; z6Dc_a)&#db6$5-I@z|+0#d<~ipg>{@#gmNcer^g}t2byu%<%TUcvmre_h0Jp@w&oy0CH(h3F% zl!SJ(Q5w?DSRg?q0!g$G=A2w5r1BmaXsG}cLe7-LRzoq?P^9TuTQG^5D)o%%GYXR# z5`{%#%zUr84Z>z-8?e}@lr4o&$_$w7l2Pv^MBJj3OA=2w$oM7+Qk|Hh4CS$uOYfru zMg==6EVr1816aOgT9E%B8xh3l%gomX6jhizBI#4Kg$Z-y>c4)(WlHiy+FK=nSR>Tb z$og1wtKeA;^pITPQI>Ly9y{iTUr*4F!OA!`@T@fE z_p<1n1?Vk@Y_a-J&*f==^;G)`_@YT5>BU^GRKt``ix@ObAxs4S=9D}0EU3+zVAqrR zc&+nb5XdQHN%(zKrMz*i3TiMyv(zp6DUhVfGKsY#k0qqoz4&Kwo9qHxnPPi z(ic4N1OrbEHl^EGAfK7g=+|~{`7TXIgtGEIp`<01Q5Q1k>0KCsP%CFacTQv}&)Ijd zQm(Q|mZdg3$x@;xlR*j;SVEPoOFbmfUK)G0BhNQMyi_NODheP0La|z?AJE#w0R^!g zsEn|>lynSv7?gNsqUvKpi;uxh$`yqQ^O|MyhtNaeYoM@@TS{D55CXOyK_o$#GV+^!*_Yw#2!103`xP^wNj(@NEg!s5 zAR*^uKm;0lJ{DQ6{YffkWWxUk8N(E>6ZQ zHk&^sjqO;Hcc?)5NGy?~L|MoxwE|4yQL6!41)T{+Zm?1ybx<%-2&sORIm(2kMC4*9 z_Z)L$ONluk%*hbOKcZ)YxWT0jOJFBKH1P(LF#2%|BNc(66cGP_d0e1rBS?2rSjUSt z)(uIr#VJdYdf-6@CzLFhqf=}h5jzEdK&?#_aLHG(b1@qVrHopmd^gNNH)TUb)c|A^ zv`2q=kcmB zt1LZ9r;uc;Ely?|;PToF6m^W^g0gQ$v29zd21kieRYqwySQ-__2$UI;lr{rxz|@{_ z8K2u%F|s2o-{yIgZ64UsM-$J^jb`oLd1!hOZ!c_}mVV?KOw8l=cFWp^0||bCn&YFY zXHirMn;Z4`=rvQ1Ag>AkU7N@yvfj^NBa_2zJ7 zprlcGBC&<`e84=`tRaYM8QxL0sGz_oY~~jQfaKUvfzWn&(0Lh+G>5#?AWkp0id^miJ~6sS8}lNeXIsvKY|ZO%Lq>+H>YNy&Cg`^5X>0c?1_NT?_q2V>;*9nd)@qAlU`#Mp_AT3g&W&qvmo$zB+==1*x4&+VVmogPj^xg|x&Vk)lGm zOlj%!IY$MNN{ksAPz_bPgf6+{>}s%`^8HX8Oj9>foE!y^ zX+n|*aZa2f3`_pppSb^#liUiV?4rz5d%%Qd1m$;7C$@Ezqcm*PjoqGGDwS>y?2{kO zX=9xdpI>eE;N%wLCQ~r4u}W%Kk(zyqFJ3Oi z&`12Sd`|rZn9Z(fK+wQNp5QA*r2=uqBSD3szWFXgMoEc9Ap%gK6Quu#A2Fc-Sg|QG z*=;r8O2zE*NwXwFK|VPC@whAa_Z$#$dLWRb9jXDNORXi4Vlm!`&nI`9d82nb_JSLf zb;I+}pG>jR6*ql|`0$vKEG4lNd>Kk|{kN6;jVwB?)jbr4c1`Yn9Bt)HztDhUdkCoz zF_C=8Su79gIn2cmpbga{ z59=@v2*TPOa6gm*g$44kOmp1!${X0i@*$%saXw+Fszj30N16m_wxlqT&{@twDiI*| zI(ft;E~$9T6HWlps^#E-W^a}T(}JNEWGHQuG)#4@D6*2zDKU@*Ouu1doc<{m|emszmlJeL-!qxG2?olERK$DnS??eQ%%y ze1ISq(Ca+S8VVBW1xw%axUUNYhImf$uvaid!nD-{okr=av6Yb}+A}3aH5p8~kRuE8 z-C__xMQ|vOG(eK2fMFOl0kb*M|l zI7q0=EcC}HCy$;RB$K`M5#~MC-40Bf=w94Vr1E4mq+C0cOGcr$$yZdK-NII&nbjC_ z`wKt}VIl>xl;6yTRO;u-(@bOp5S7uh*6(WbIVcFeaM&d9FdThAQ?FpakZLurxuv{G z(XE5t(HksjN*eEHi$;+p)W@r~chvMltx%XrMDwg`6cG5%N1)wFo&t+)FAc2W4{V(2uqo^7(lXjaVA0nVN#M8dNZ* zSvcu{J%Y(}#dZdp5Ks(c^XJIZB92B=T7y*ME}<7PtHuy=<^>JXAqfU`A?j9ZgUis& zsu2hDm7lvI>00oT)|2`s2vogtM)t-Wc` zzB(F6rnE8}a~#fP0nuBK0^LE(OobEpV9aq+y1)iZgq^L5KUyfzl9fwM1Latu@QS`K zih$Bk1W2r<5)*<%BS=*=R*X{Glt{|_VQ#|{_%L)uRN|0Y*A$ne1ogA;I1$wxUNDrv z9i0srF+%GiB{FyphZ-R&H*rSrT$4lCQ%R_8lzwcAV#f_N78MFunFMjx5{say1FnDv zloQQllHGoq+KK{|v(-v%9v&w2W17zoqNEI@wMNLuC4lPqQ_D{f7Z40yBXc06UuU$h z+JTEo(QHojnh2Q2;^BI36s4`ta^!z{p&sZaK`5U&s@cTh-5ia)qxVsig)SfLkR{Z= z?^9eq=Bx;jduqBF9!3ZEk1LJgyl`CfLN}Cd((MvD+EI?@tuT4v4~louBC(9jtXj9{ zB!M*-{n;86d637Y-jr(W2wa}cys)`Q+RP){Vs0S^2{})k-Q%d#hQL7tttAdSD*ug# zN~|1m`4B^sxHq-*!XPltWWs7BcvVm>#PNYd8L#CF4Vp|EidE=9NIaW^kOGaxyg~Fk z#UU`YDC1oe0z-pHt^&{K^AIFI`IZ9|b|L9;g8)&d4j@TD7&0K{yP9DEMYB&~_UB%s z6!9dO{THkoLA)sR=gbTgU|bdo%^>w3g_ROD1KSFMUbVXJ^t+|f!*f>R5_}N80L%?M z2C_bm#*BV@pd`(N>7$JFwxXdzrv;JQsPq?AvMGm&1t2lBkcL@W1gYtyNLTJ+uSwqS zd21FvWK2r-KzRH+1fi%ARjeG!8(2_wgx>zJjQMJ~c_#wI)s1rK)=(j9pm0b@ZPk)O z?qPs;t-(-0v*DaEVQ5OiT(5Kv@V_m5NKo(-*(j}?0Aa96{Snx}*2iKSqS;ZI%N|QP;}traPMZXV)f>tx1L&Rv)5a7* zcKEHV9!wh#BI2wg#~w`jZq(A!jSog3G4MQ7UpYoXkRw~nEdi9cfgKx&f}|ugXj(J0 zbu>0+yw}k}$+~+}4w{mFd~q3^gRtkXc|+h;(kjfKzhLQ-<%^aq9~+xDIy%xD8|~pp zoZPu_ppmaR1i$M>m`ZOr$W3sX$Yu%9-Lpd8ci?wya83yU#K_EQbB@-oJrX7_2APO& z_}K5y&Q8zH(9F!)>60gpojiH$?BpcGrObluj$%$0S4tFU<4gH0s^s!bulMbS5+$G# z<{l5Vd)(N;q07qFG(-=S`gtf}j;y3A zxRB|1lYKT~!=-D0xLc_#N}8=LRBq{ls8Ck42Zxpt*gZcD$TB=#EupB( zt(sw#LF)k-QG-<41WJ>KD78ht5DSV)DcBsJ)Gx%7rO1i67N}u(O_>O}8z;lz42Ev9 zm*#3=LKGx-vlkoN7kX~~;TNprLM0qYtlFk0*hUHRe$uD4$>t)fm)V%#J6=uj-OcWw?291xGWM95b^3{S8=*U@Zw>5Yw#jaDo@xPR}lV+W%&MUMdh z;6NY0pftf4#UgkTGF?|V+`$k4@Ea^dw;E{hqT#n$gcW4n_5c7707*na zRKlcJ!bz>whc8)BL?cyJNFp`gfM`bKVW(VH{u@H<4hzNoxa*vf7RA_|V>lEaOQQxO z;X{H-w1uS59o;Dk04Y$`m?kpjJF|icq1hTr@vEy480bwRrn@73Yyor@mG^KOy0P`C z3^QloFk{hDBFP)0$W9EFxn~wv!1+WQX`s}rt@d#QL@vLq490IXg>6d_M;I4GPb~T& zFmL|$r&yJ0C_WTJmLcJf-3b-CK)1I=R#ia477WJO$5ui}wG$}0y~8=bLG-g3Qv|xN z^#&*UQPatI$V%Sm@Od!eV@T9|U^~FBGxA_0Y%!sTKJ(0(JvT||CZB5dt_|U6^rp&R z0>#!cI$V=R6DAL0+fd{OqXlbg0#MCk0v1YRfhFaMoTY?u5(-%!H1@)L$qr#I16n9Q^*FH#91{rVMr$!C7JCgIKj-M-GoshK^xdsH+MFGrh#FI zQKHOGDt|P9l#^=W7g9Dd+q^8RMtBk-65BO$jjVu6uRpft7uy3tG?sDyM>TRCaO`3{ zGVkbNYsoAiRbk4oJf&U)Of9x-UD+b;Kz*Wv1{$=8Z2R z{aH7GB|zTb5lHrD4j{&Mud!^Zu(u@a%OD8OjJkX2ll#ANhPeNmSC`(LZBhGqA<&~4 zka@GQo&I71s=R{(>TizpD(;T-Mi$H;pBW#oWOm(#&8ya~-@j+)$x{>DOl)HGzKRIY z7&6iJy)fnpt&T`W<3jK~-||g2yN)ENaG02dIWAiC@UmKeWYD2CR~=sCJwFNj&* z2MJn#12}Go_kf8h4L^yMWV$kx>e8_ZEn8}|6 z()$`+^%XFMJF+rVLb{fEu^VR1FvyE+0q#g6H6aBvGIl{BWok*=G&a9bXenrov@MlgB1L%=x7e~R&!8kE z*utD(gGQ0Zl~ag>4+x11DGj;S@pfHnC|b4a+tSoZxdJADiX5FTx%U9JUtBL+h z-5CA;Y!#nDl2STE$Y;h5F#0gZpvE3q$o;-^<&2C}6Xtpi{@BsO2lwyp_h(c-Qct}8 zO#Q%oB)NCP-B;24^fqkTwrs_!*_o-cXQxh`IXg2wQ}ed=eQ>s(-|i9o907i;Hxk02 zK$wV}KYs#olLkLjlvBUH7!~`~U+>&-4>DWH$HA)HLJ$t~HwD=0;4txH<6}z}EgYXe zZ*+XYiHRfocE8l9sMr@1TpVJ90;R{woxt=#tT@K}P@IuD1HEGN%@qsXw}7--y029>E78C41&Tb+~noZZ1 zr8S!{xQ-#P+U65cTSzB*J?Dy(8%V{r2EFMnpwRnXXjn2O0nOj&7UccApoD^QTo+(M z7OCfI^3G<`v_^D?ri~O)6$8c*LT2uoM)BZ?yj~cz{eVz&A@G4>C6u!Hmax+m=1|nQ^H)Zn7CA8 z+e*y=(V!wFc2fxj!JroGgjb>p3xg2uY+@(vxw1ZIF{HeAsBsA}Ny7?Z=wtT#wU>vC z-n(+Lvz6I(Ug#2+h2Kr^IUo$mA53+|KA%$~Qoh$HQP2UPdd&1DppFZb>B@1k2__yP zcGPKp*8fBjDJ`28ED%?qsbl7LpGFtcok-ds%S>e+=$NE{?%-QctX#c*)#`QBpP4>; zwwifQO-(naCz^S$A?BO}Y(Qol)XmJ@!O=n2dUe3DvC;AIYMNYydA;7~;lq0m9o$F)}*7ZO0|!^X5&SK6T>MEgu;7B88cJiY7X=c>xcnT@~|K8PFQk@Ezk(#a@PxAfcqS-$z^+z8$8zX(SF zQGJ;(R>4Pk>n_}7%}RE!BDq=7wmc69dzCAS`(L?J02S>?+gWz!GDPDdvgEhMh&SuU zD2Pp@FKJ|zL(r2;E9~9Cv^JI^Q!hi50)r*6&_;u0GNtg#`BikeL?&+tR7%k!-Yjry zOVEH5V#Uy?NPm;D)B&;vWT1SMDdECcAnE{`WcM55kfte>sSz0^nfy5+xh6Ig9K5Uf$ zwk0LR(CeW+u~G_V<9~Zr{WcK#@XcF*k^?q0*tFS9JS1udn!hda9r4eB7aeMRt{l!m zk%AurW-=85o?mkF4vr#tJYNnD2{UJ7_*C>c;v(+Hr~r&|+74}!X<&*G<6s9H4QyFw z4CQJ<19>1|{wbtMXePN!CzgN{Y=g90&1{$)GFT44z@>T|xLYK|6R52pg~lhd?<3no zQIaQHmzL0$*xu~Yj$o)5Bo&?uZW=_`fTWB@>jB|lxKvEmQZdkwS2OUf+bW!bPV}o_OWuXNfPf$k}A- zL;NZS`VWmX%kD65{(|inURDusYVy?a6Q^ruMz1$EuXdn}^n?%&ur$M0ASvEbewO-Y z&rZ!uO*dy#X6qds{l>?^dpe-+w}HrE=`aosH1FB?OePrG!4Z;{EnB>B;nK>NwDYB> ztJ-PB2sxFB2(AYpaYT(|DhWc##``JZ$bzL8@G>NSDaY5Oe(3RY*y!yNFjgd-S#dtH zn$^9q*QXJnUU+F0042LHbcf#LJW9B>)i;x7wcKbH79_suG`Hat0G&)9{oehx)W3NlR(N(%ocaPH2cjInSt)5l1!= zrVzZU$0l-M;%9N^5d1EZcu_{+28o)e0UmkbL6E0&kmG#;$&9FAkavVp!hgz0I0_NR z6kIfjF`bE08%N|3sIYm6l@JHFdw|#>tIN(7Bip*UA}EY%7jeuAF@V@vj_wdSH)ns2WPA{$v%Lh24}jfpTiDsx0VOdN9t#GCf|BHr!bM^!dMYC>6pOG=&Sl~sNb%NC&!T6miiC)BBfLaU~=G;#*@ z_`G@BFTA`rGIH|d@uSC1Oi#~12khrC%^nPVV3-^B&Po--%=FCZGm{AY1q&CgT)pn( ziDUIP2gq>UOAimAAWl4M|Iv<1M@B|YRPLiwllAGBdE*P_FBl&mOZIDYYWlhT8yG5L z#zx0R#zyPC8?)|`2a*##ajY!pK&@j9#3}xv4~bF|6B`)BFK{hl*=DYCf&&3XtIfaD2*rj0={i&Yz&@kTpxqL>XdP zuAyyuxBWY8^7I8TXVFCsuo#~gNDM36D8AAVfwahH0w)`l$z32Lq){=GD4sFzC@%UzeY2G8M=4heNgYZ;#Zw}(U}w9cQUFH0T7y0S zVyHzJylOU7Q0`HiqW%KN`!YaYI6>lW9N8|x>q_p2#MxAW@KJ4_*Q3hDgKXFczAO%b zRj6(zb?{LhQMc=FdW>ap@r@S=4YD+vXNolGQu4}GFGO+_hSmhLWQ&;^ zLBJ*982QY|g|}2q{(>}9@ui#_bMeHgt@7~rI3qfkz|4q?_a4f4N=3mpMra~R$jg}k zi;J_9?IWlqKP8xQTgjA}klYx^a|5Lqivme|{;nvfD}j`#{(wS6B|BCCKTIymh!hvL zN`h%nEjc7J0}Ph5lHr3O$)L~XVb1h0@Cd)r>2_RTiig{yVJN}ip@!OALOPiW#nL9u z#=B}s%bA5AM!{qhGi8G||(o7WDLTH)kOUEVCoOT^=V zDhQ7NYMk+0F&SUVuV^iXT=xXsOiZeMNz#zTR3^+ReTeCejP1Da^2I4R8UO$g07*na zRB8f$Y+~ZnsWZ?y@KB*|@pTL|5OPbAzq1`292PHLxOC|f#F6Kod#pb@qbOpLaUCR} zTQK9b^$nZ0FI&F)`0>N1PoJ$;-ABjA=FOkqYjK#i?c6NgT!_S;um2nVeG42ETX#axaU`r$jqph;Y7qk&o^Q~C9y1tKN@5__| z@CM0WP=REo6+E1!+?Hz;JsI+Zp_lRcP=+#;prTa?q`(Xw z6OSS}{9-Prc;s0FnxbAycA~a&d{9 z47`>|ZhxB+iFM2~j~P?z5ouV2QW4h{Grw#-ajyo~xwMEuFTz}3xcqm)K)AP zx><#-+b*u496oY%DDQ?0Jvh&yOCQn;|IRz)QaE|?^wDDzm6_bS?NZP9bl4=slJ{mU zS+ZjJ@-?SVpEz~;Y~>#roj0~%!GhlT==UbO{lz0Jl1ed z%|CqIanR$SP&MDF6BA2UtX{Nexwew=fQ^`Dp2VS=OF={gt(-~Wq_L52T0!!~Q9hA{jW#c~R22n79m^=tf_!|Sc|ug! z34|O*#{fWZD554nGY#|@6bkMgp(}`yL(+u#HYV@v30G~Pml`e~i7AX!P@I-XnNB(z zW>^nep_3*gA*?2s); z$9Sp~+8ZFe&9u%WzQmT)zF&C5HqB@7c3h74@TGbZb7YySsgGkmtPWdhqM z5fu4k9NzEHEq{kLnPbou6AbbTNiakRnu3t*_IDt2u2ioxtYy-Qc~i>q2&i=&TdHL` z!oh?G?QW9{D_q)99$%4vl+Uw`-aL{VtpJ7((iEo_2csv;;>`Cl0Vv6Af{V7HLVkfO zW&@45IaKZshe|lH!g@S_8V>W^C?`gi$C}0$A(>2HDn;FdB^xjuoOUZWK!k@crpjf+ z2v@Dyuz1OeiDO4jpB`}OMiw@QVJ+QIz}us7NbGZL)F#YK&k)TnS-Pxh-)g$-z&Usn z#i|k9Fe|NQq!(IE&QhN+?xmt= z%Z%o=5QY3B-ZOu}v=8Q>XCooCn+ccBOiqoDjV@lg;@Hsx5gFgO0H9|_>et7M3kE<~ z8gUriT_RB*1sMeh*X}FBPsC!P;1K)v!^du*{_2WD**G`qgDE5KYq-Iq(Kw&niGlh< zIL(c~6@)(Y>s8j-s$9QMJ!(D>96mG~k$?FA|G#b8upWwBtiz#0M?dsWei4k7MfmBT z{Lvr%k=q}9=!swcx4*Gy@xt%=&YPhv)Li)Wk9=(M%#`e*0z)ENHl`ZvJjeRl%tFG57Et?w2C4=z5KgmBQ{x)CdV} z28Gtr=b6?d`%Ca0ETA-Ta7OkF!r67!2KH;U~D zBy1)KgV$0&?y!$TsG?TgoEUkxV{c?^)#?qWPEMRWd1f$Gt-&pV&-vpf=NNi_ zhp11UJUu=>v1-l6qlXX9&Q7QD?}qmNJ~G7RE7y&VjvqO4)SWMww{U(nKRqAx&aJ(K zwwP0}xllQ1DuU0PoUNzJJvVS}J{j3Z>{6a_mXu_0xjBb?>0`$x)^FIbblJ+2?lKOQ z3o-r{Ad=RsCiHYBP8?<3!+0(bfoCb+a3iQ8syO1;0PcmQ@8;Vn^`4E9ddEf;#_D|> z%{PA>YIhlIuRc*!c|B(Pm2md3{%}mM-!xcrAqV{LVoo(ey@V}-oJPI$A9<3ci;Q)cfRo^7!>}~@BYb|GgI7?MV86q z`|$?D(7#8#_QnrRLZm{I8wim2!_^}tg?-R=9CH9HG=o-msiA{V85|}=G#v{86gECM zp7ek}nSrs0@1%6d58y0UHRDZ2-CL@X-uUUFmgnVQk?IH)EnoFBl|Yw~f*b^*>_&Aa z6cosP8yS$vpv$mV;BZ44kD5TYI|S}E0`6(W2!*`zWBjaQWE~5M{hD-aB;>sh0XXUE zdX+;voPwAo`24iYlU&^r_lBtJT4E)g22*4XauoxwSgT=(c=r|FIu~P^WNPr`Y6->$Rj^oxpK|qz z2caZNF=DJYGBZ9lJy|(8`n~4pbwyS`E~E}30n1UC%aW~eX+l|wEVFHTdV2EA=~b)N zIR}R^U=l#lu<3YZZoU3_3p3i#-G{hQjP$M-*|&+_&4G2(h31TjKQY;JpG~O+sW&!O zA4?e>bsyt#E!mr$tv~5of9bbrpHZ5fZMIX-)_y5*)2IrE48FzZCn4=tHSQ2!^6opu1PG$Ze;?d0z2g zh+=IRj?P|IXa!S97RHaNtjmf(0Shnp0&N%&JpuvJtz4FrXN$wMXw z=}bjnS&-BM&k$DvoL;DsKBvgA@SF=>^f*(1hREAF`#E5eg}Rr>U`U4-=OM^`2Xlj$ zOz2#TIk8C>PNG7uFV!+o4_-;nC16`zFv-f?VMpzYo6XqbkT&<}17fu6%AQLr1#D5( zNtKdDr%BQX3SI&mWjQKw_BLL!McRfZkRit&?#fYh2_yiSZjk~N;(ttFBg6rY+%9M^ zi&u%#FcoxKri+bs9?B+@3~)`5=+}spKnILrXCf7(%(uU$S84;c=%KwJ5Dd%A7G6P? z&DlW@Jo|#pA)-Nc?%VasD>7R(ojH2+Xa)J2Yp>b5ZJPn| z$iol6{L;%XC{*MfKXH8J>b3QCm9ycivEFX@0kS*bG%`9iK7a9n1N)kHoHuVAh6;my z9I$!w`fXb_r}0lb{p^t=6YhE4y45F6ojP6b(}Caljeq@{|LG4OdGyKZ`3~LJ2lU)s8`Zzjk&*gV(fWGP z%3;ZU6uf07*na zR6LL0ZxTpt!u=0E_U<43km42@Uy0%_#JK{ek>FkALKT1Il~=7)ok{4~b=^glq6%XAqJ*XB9T*G_ zRRJ|-ER;ynNEhdQyJ%Om5#mk?4V%gXFd=}~YYGj^(j8Y!FmHG+5Ha zl;9U6$Ki_mJw*|M#fz8E&d%18+iuX_{`R+Bu=N5+>21Pu&prD$pZi<)Jb%H0tFFGX z@=#oH)s<=9-Me-jJ2p|x%)awG-&Ezi>+5$(j3Xl>?|jGYpZfHl!(79rO&ecz!;RI$ z`gLpG^{#ik_`*w2&uJ&_xZ|rEHf;FupZKwleeC0V_v~?*tLf6Ih69&hap~CTc;(>e zh^6A&@|7zWFIs-$#L*ZV1+wSvVOX+k74@rG`E-4zp#CB(YjNjOH3NJc_{Lkl(v|Fd8CH%~nI9JGdd z&%pR-f3i>5*M?J!cT=fJ)Hh9ShSfMWGJ3G`0W}J^e8tN38#h1o_#>Qg@sgz{Pu81^ zF1qNFijYSic|bxP9UXi1YhVA>uYBS7#L*BvKqaT9re>$77cX8haqMtHA!{E&YBGGU zknShpVZOZ7hei!>zrs%5=m_IvVFr_k;199*yEnpTR7UD^DwUICWc0>2z47`t+`N4C z>WRZg9=_|YyT9;7aOQfX-|vrLa~}t|LoQ^FG<%HSy!$~uqC&zpy5}E$&)c_Nu&E(% zz-6+lRxG>j+AAdd<0ntvd;cSL9X!lJq#J~!Pzh;zW!EbQ4<6(;z3FwYfArDEkmB}; z(`QasziZd7z2>TGPM$i+h^A*|F1!3PmuvCj#g9Gy=+2j4mbI=xoH})C!}<-AQ&TQx zLj~EU@j`;} z@$risO86a@hM3XLnx~xY_OpmBWLP?XE zZ^CiwOKx8bauI$K>m)*Ld?Wk0y;5=2?JItQ9-st438SD0lYobI-c1-T&~QM5YLSpa zuXDBf63HXi>KYCo)dRCV8pMXzP(7mPiS&yoAzmu5{|$Ran7`Tv$gI#f(c@a8I&x#$ zO7mQk=&@0b%@&rHqPp4pyaP!}6q0j@P^^SRsAwyNn3=M6jdgzG|}qD z>{@OpF;n>l63+Z^W>Z4L#72R64*ZPqShR0G5$oIVIKEeqwp1EgW`L3y0=rEGT+21R zq49Xi5fU0<@xuiLD2$5|3i7%PZ3ei|C{h}_B>;rlnZXspqNPix&rVWjyY$dQ4?Xw1 zp9wEowD|kq_SQfBlRss&CyvK+E^oNy<^%f=ZrHdn3+MR6vC6?QGc)taPk#FE|AU`+ z?2*SR?*pfP=bPX3;)^d;&Jh4VqI=KZd(WEHei3)s@?{mlJ9cb$uM-m!AOC}orzl3p zMk-&%D?4{Vy(xQbHA6UYe4=Wwy?ggou9F}8@%R4W$N$SC4?j|^S-$@R@2`HVspFAD zNB->3J|Sz-9x5`X&rU5~nm9OEQ|enLVd26>)eHrial+{6Xcx$1<74mq!FOMB`Be)S zFWN`&Y#a!qoLS5S zkq?VR@Q4~x)A=-Uy?Xt<=bRkXLcII@tNT)H)g-U^t~YLV%_Z0eyoE{U(Y{>oqY`49ca zk9_<~ciiQ2UV8aOzxLr@lw}`(^4a&i_h*a^2WEqcZ;X^n5b+Ze$6*=VcIz9RjW~An z=;fDHZVMP68$WgW)Lq~F217k~@W3OFJ$liOi=KG$@yfOHhBw^$^{?OQjN47$ag&4j zn%8{Crj50;=8d<#>G+A`Pd@qhmJ2R;_St8lc7iOKH*emdg9oAUnavVib>&qR!1j&hcGP8!#aKdCGse0-eAnyJJFoS{t zO9bUN%%}0l6LWNyE`(uyw__0f9MsTXT;^3Slr#fIQGr6tNGOYvu=sz7UX~#1+HGmveMNaA0DJ=5Snr0`JoOVKM}GK2y=(unKsOzwxksY zIdcd!RJ3ldIbp-3O6Mh5HVw<2js%?_rVqkMNzN{K%_g@jsB+o-G!C(NJwsA9V-O8J zrM^p)HY8_>%9fX9q@B(7+_p+QMzY=%hJ^9j5ztkJJPJ<>MH-07qpaD}15mLRf#SUQ zU{gvV9u+RgHJWqXAb4j35hNV2@D;_RXfPqtoX;^pMw#_o7X^AQ7KFDyw&#MZ5K!m* z9#>tA#L#8If`w-$hxjh+%P;Tb=QV5BP{059V^2i*0q2SpD{j8|<}ZHXi_4cUPk}uA z(8If5+3l8sUw+}mZ+z|ROP4L-;e8cWuU_-1PkowSE3wyz_!>TUV}J`Jtcr&~Ja_cfNYZoget%2Y&aXzc)2G`M#fe->z47ef5s7 zLNgM1{^@7F{Dm)7e7O4BYqo9M_SmD3U32ZVix(~0wQJXHZ+zqa{rm3u);-WIoH;YO zaKXZ;-0rUv^PX4hvC;7}XQq115rcXKUPDORX~%^Z{=`rId%ul*FX@=4$OS@g;)HmpZJ&SmT#a z-pG!NF8S(LJ`W%?v^_NGlarJ47mQXM;CmlGss&~6Fg1SOp%q*Q z>@ybYgBZ;}mlp&f1e4{)?bKYv;r4M5_Fi?f{cy>vZn^oJfAhEXt)jizYNYPI-Pddo z&)5a`KKO{r-UI0bVa4)g3l}cHj=FQM(D>dHeUS zUcG`6j9m9DF58bZT&-H`$tRxx-!65iRDH#&m7xTkn3$;6T`S+%!bOW-chl>?ao5*Z ztz1Rn%MrWx>^^a#GBJ41J$H|cjlT8!-ujJie&f)AgIIrKm(PsRBais)Ta`U}@%iT~ zxEEh~>CvM{Yp(~w$jIn**IobUqmNX6%|0cA1p?IfIEubr->OLUnUu#Kf1-MIQrcVlU}d>wvu9~=OJ%CvEGf)fD|k23|X|8pfU=wG;zQuV)~@`5Kbu)@Dc4W zbT&(v(sGxFZ|W2!TI&ixf&CQT8tndPjR)atgnZ~;oKneKG98LbTCxrzyUI$*g8)lB zphpEj>q-izaaBe)wR|FI5VhW_szF3P- zAu_cqp(N}c#gbG84#PD~YbXN>TV4su?bCq(Mj$!^)XDn9pcxIOBd2VK2GOoiiNa8z z-s9aDNrDku?tB_j8L}V*k_v;NnVgm5L*e0>^jMb2jMWl$#Ic4H7ytke07*na zR1U$ef{C250)hw%{k7%;NKbuiJJH$na!mD%;~s+Zi)=~YF**5thH@n^!#s_Vtc+w0 zyoA$J0!^jHR1l1Q2%De6_YDYQlwyNbHx!JBZG@7okYS>f`+iY9gs@2dc{+@5%bLCZ6KJ)Bz)6H)5`|rEI`hDF^uU)fd&7M8`_V3+$ zU}WT#ov*A~xpK|w)!iK&lT%Ymn)!maOx$Qa|66gpnomuiIO$D@9a6BrBc4^c>Y8hu zcVp`8?Ahtc$I(AKO$(Q-*|Po0L;H7mMknFYi?^>_u}sFSUA@ZW`TalquV=)UfG5vP z{oK$0>z7~I104eok6ld)o@N(ry6Uy-mT#Jxnyq{^&QnwUu3Ws~%FQ>{4h{jS3RRxxHR==@4Vv+r%v+Ggo2hOiD#$JE?u$& zplJLWgW9+@oGId=lQW@qw>g@5f(xS%K9tdnhMhmf58QihEKS@$V9t^m`om`V=0=Xn zU$$)JN}Z*iGb4Y&2g1@LCLK*^(0~2!{^LLUr|*CCv8TTFwY#ghMT-|+e%VD&J@dlJ z;8Uz}ot~Ujm2GJL(j9kw{jPie=l8$&AN=?aUU$vqm4m}kd*1_(zOrj?_56mLUsE|a zOfC4HMo>MW;e2!!xZlyyku_`Af*6x8T)0RS$72)6t69>5`Sb6&=UbOudinVHc-6p+ z>y=k_GNQF>)>gi{tFOBHzWeTd)%7=27)D1%E4RoEud1Hm(7{7bJo)(P)2FI`+qdng zn0esff!RJ)B6#`b7d`awLwk1b&ddz7cjNNQFT3oLOP_lBsi&TL8Vu_ptZX;W&prFh zt8aYuv(G*2_SJ0Jyk&A~^4P=#i<@&Q-}TMA*to1(weq@auYcy5rz$tZ^Dn&M;4WCW z=(byKz30CBPM$nT{rY&zMC}i%5nnrY&B>Fes+@sn+_rV=)6YEZ2|_XLg)X7kjU|Jg zFyd1i2+#mR3pD3mASE%NK{Op2$|&@sGL)kp8M@U}W>0BiT$X5ege~P~@Mc1ke3Q!# z0S#cyJ86xX#CYdL6VpSoMS}yzpa5l_9Sx z#C0V#O3JirAs87Gb~?HSP-SrhBLBL9EPBTV`07&^vJqrTOa!iAsxtaVjDeP`tQi!QmuJ#X8#ZPDU-F5|eqY}xY4XK`h`+JmaO$crz&D6!n| z>Kis}*ib#Z{^pyH9Xobl|AAZIcx&~#XZN1l-tv8$oBKF__dow`1@iFW!!N$@!lH!> zdLtv%!}RpojT`dEYO-PG?DWX^xa|a7i}bu*86TTB)1UQ&rQTpwzUKN@JC8u)-Kc)a zJ+IkRF{HWTBsq{ZHZu05uio|B|M`z(q&%YXhadfwEt@yU)L;J(|4P1o_}4%Bm!JD0 zw1#+lh|$5EQr~*@EtP{tdN(E~>)-9`t^xdH7XG2b2QR$j(y@`zsmU`ZjvrsLWT^wS zX5G5|_17@qyTA8siyM~&hc~_HdnD5P?z?;EOD{kvXx`7x&W_d>zEUm|Yan5?edvu- z98-p|zOuRg*Ts*<;kvkO0XzUbo)&{z^d})rpIh&1^S)oV^}J6fPo7w=eH;@`b$0q! ze*O3U;4`$1CYCn5ECGfT+R=^bIAnz?6QuIj;&s`TBbEeR8B{1bw!q3ckOkL zKk<0wSy;Yu#njZKW}ip4X;bBRn>sr=wQcM6r=NOKW?8j*_2R{gT}|iDTX1aR*h3FK zw0y;~9XobFz06FNb7Z8~+kfCdINZUEQ6~~MuCI;A#toaEdioi^e<5uA!FUlzy%*=u zk)vnMoWAnPs~&&+vC7Xcef+5> zxbbpUDnBg=8zsnOjC%(}3t=)WLH!H*YionEaX+Yzzza`hJ(Zsi+hcCaF8$IR*1wBufBH! zBD=%T*C;y0Yd-?}KvXHP#gj$GkdbCxhM}ZLiC2t}-=H0KNuY|?Rni?U84Qr8L37_r zdSG2NM${+vOB3(0li$rz%1L1Re@bvdG>SH!UonjrQXnE)f;luc7ok^T65pwmk>Ff# zbflr}t^o-)wL zf6>f0fZM%7=_M)%cB~e|4^VhCjbKt;18GrcD;g5dw_inkR-Z-yzIssgH z<(0d3?_viB)T`)IPdxVcjjy?J!}<-(y|3PJXMc8<9URxc>blLFx47qd^TyY&TL<;m zZP$hmDbU`%d)dKJ&2irSo_BxZPd@?84w1kA4}apff9tm^hX`YQ@PP-PdFGjS{lE{5 zjMm?3UA1aeHF;UNVg-(j)DOvWczc-bhn*K`(36f8gVP^ygPxaq-Xm^iTZMhyFis9)dQ5 z&D)B_Yn6AS{;m8S{blpl%Q76Uyz<&rYuBAVdFtee<1fAV+~nlshK-xI@3`pL#L<2A zy^3}C{NMaHSMs_WUVHk~$!DK>V%hSQ*Wd7(JHGTc0U{2dQH0obZ8egWA_CJlS|yjk zxm}U3wT@w?> zHf`Ecy{%Zj{KScq(XNN5(b3Tr%U7H_b@J(FpI*CW-G2SK*|G64cY>pGb6$7t^(RlA zsDKzj8~MD5HFixcA=sz)uyuOovHb{kysWzIgGH ziY*nuD)Hp0Q>V|I_6z5#R!KlH(n^M(Xqr8&BB1TJx&5N-N6-V*IcO;wyr@%*0A{;L zghJ05cM>IMYsH~mMX6VEVJ+m}aP|Y$z!JhXS)up9)P||I+~lQjfhtT6yV7Lna^Q302vRHO<+~Cca&31|cm_N`te^CD2fbbZ*EX z&lk$afBF!{q|9}uwcOc_^T%H9ZDF)+j^U?(9MQ$&!;9OJK*+(gagWQK_`?8C6Ml z6JFpZ>T@o_wTZ#wAnxTKV0yz-Pvp<_RtT!MMQ)syz6(*~3W+_ft0`?PG>J(|1vXqn zirS+ToiWhHh*sr9`RF$=iv=Gn7KDV-u-F0}!p!v9(b16^^>IzmWXEv)`0>B|Z}H=- zANpTDxPSlt+1b8~BrJ1k1;)-fMcI;?1MW<`7zxMw7?*~z< zKmYV+IOjk32Y-L{>iT5;f&B;m=nwx$!hgp*-?44Swvmz1V-pka`>FRWn7^QMc8v7K z-v5F3bMepp)n7mVTz#SD+i!oni#dGw@Cz@zuyEmm-snj6aCYWwwfNf3ZW%^LMrNm* zrQFyEXc~NG_UtH*xD9Fb?d&62<&0lc*|+cD(iLmu-i^lJL5C0SvxS12W7EcU?|S?9 zyzj@~ZPNejleTF#Z&-iNy$?Y?4Cwr-Hu6t0S3#V2?%%y;>88ZT(cgV!R~Go5y}S1A z+p}=tqKhuR^x{h|pFCYnvhd*HdJ1{*#g|QaYnOx80j~gGiv`uzjD#^ zX213MFN}_jUUc=<%U7XfkN>SQ2s0b?tlGHzyD|M`NmgWbIFClZ9oSmC(!*4wzi!RGAA zj%_>k?%TU+<;u!G`s9;OyyNzF7<7B~>}?z`xbqb+a7&jgt^BXcmWp>LtN;KI07*na zR4;EfchvAJSHharYY!heRv(?hbj*QIJ^l34&pd4xo|_8y!p#X3#u1ehdeEt9ou)X(X0T>pTA(oj_uPk)7E)N7e&!hp zJ&9=u?&acSJfkST0W30 zjJz&E0VsIh@~L(4fgm;L;^s0r`hjgW;h(MMGnHi+(80{A@%0X=Th`npC`8*Xt zt68M@7_Zqx)wcj-o^@a$(lMA}C>jo8ftv_wPpImN3|{|C3Ld3-xYH!kKKi))=(leN zi4`5O0!6Y@w5NFh^1Wx+$KemIy9h;GW(%~q@5AYJUqeRIFPl!<@>TOF|J_F@=RE*tX%_IbG(ke_C!$3{l`c?U<^Crq!q?%Ha(@mGH3S3mH9 z_g{GNh0j0xJPZmIEvHVOt`-ZksrTRi;1iEMUgg>zK92%IQQda@AO3sq*|2fLLk~UN zpPjwziYtEL-9PZjPsGCx)yVwt;X_wmb=Bwp`fo42{IV^Zw|vJ