Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e04e258cc0 | ||
|
|
ad6a3b19ae | ||
|
|
3a986726d8 | ||
|
|
81d7cb523d | ||
|
|
c6dd174253 | ||
|
|
94ba2b654e | ||
|
|
4242f8377e | ||
|
|
97aaefe5fa | ||
|
|
159253a12f |
@@ -1,8 +1,11 @@
|
||||
# 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
|
||||
# 复制成 .env 后填写。不填也能跑(会回退到离线 provider)。
|
||||
|
||||
# ---- LLM ----
|
||||
DEEPSEEK_API_KEY=
|
||||
|
||||
# ---- Embedding(可选,留空则用本地或跳过向量检索)----
|
||||
EMBEDDING_API_KEY=
|
||||
|
||||
# ---- 运行时 ----
|
||||
CYBERGRIL_DB=companion.db
|
||||
CYBERGRIL_LOG_LEVEL=INFO
|
||||
|
||||
@@ -48,5 +48,3 @@ models/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.venv311/
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
不是聊天机器人。区别在于:**它在你不说话的时候也在运行**。
|
||||
|
||||
设计、施工与审核先读 [完整需求入口](docs/requirements/README.md) 和 [项目必读规则](AGENTS.md)。后续逐项确认的需求优先于本仓库早期构想;演示通过不代表整个项目完成。
|
||||
|
||||
---
|
||||
|
||||
## 先看一眼
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"kind": "mechanical source bundle, not a Git merge or release",
|
||||
"product_base": "db571ba204c8372989f96e1ed9b5cd00d3a306c7",
|
||||
"requirements_and_hel799_source": "63799f02474ed94fd793de9e2360d185c878165d",
|
||||
"approval_comment": "01a169a8-5884-7c26-a00f-c23a5f5399a9",
|
||||
"scope": "A1/A2 frontend continuation; do not replace 8300/8310 or production data",
|
||||
"hel815_source_attachment": "01a16845-b50a-7335-a87e-61bf11c44703",
|
||||
"unmodified_product_sources": true
|
||||
}
|
||||
@@ -18,9 +18,9 @@ tiers:
|
||||
# ---- 旗舰:人设浓度最高的时刻 ----
|
||||
high:
|
||||
provider: openai_compat
|
||||
model: halogen-qwen3.8-flash-next
|
||||
base_url: http://192.168.200.11:8731/v1
|
||||
api_key_env: null
|
||||
model: deepseek-v4.1-flash
|
||||
base_url: https://api.deepseek.com/v1
|
||||
api_key_env: DEEPSEEK_API_KEY
|
||||
timeout: 60.0
|
||||
temperature: 0.85
|
||||
|
||||
@@ -28,9 +28,9 @@ tiers:
|
||||
# 省钱的杠杆通常不是换更小的模型,而是少喂上下文。
|
||||
standard:
|
||||
provider: openai_compat
|
||||
model: halogen-qwen3.8-flash-next
|
||||
base_url: http://192.168.200.11:8731/v1
|
||||
api_key_env: null
|
||||
model: deepseek-v4.1-flash
|
||||
base_url: https://api.deepseek.com/v1
|
||||
api_key_env: DEEPSEEK_API_KEY
|
||||
timeout: 40.0
|
||||
temperature: 0.7
|
||||
|
||||
@@ -38,8 +38,8 @@ tiers:
|
||||
# 没有本地服务时,把它指向 openai_compat 的远端小模型即可。
|
||||
low:
|
||||
provider: openai_compat
|
||||
model: halogen-qwen3.8-flash-next
|
||||
base_url: http://192.168.200.11:8731/v1
|
||||
model: qwen3.5-4b-instruct
|
||||
base_url: http://127.0.0.1:8000/v1
|
||||
api_key_env: null
|
||||
timeout: 20.0
|
||||
temperature: 0.4
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -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, API/external requests, microphone or camera requests"
|
||||
"No uncaught browser errors, external requests, microphone or camera requests"
|
||||
],
|
||||
"errors": [],
|
||||
"requests": 23
|
||||
"requests": 36
|
||||
}
|
||||
@@ -15,8 +15,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, API/external requests, microphone or camera requests"
|
||||
"No uncaught browser errors, external requests, microphone or camera requests"
|
||||
],
|
||||
"errors": [],
|
||||
"requests": 54
|
||||
"requests": 88
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
HEL-780 · 8310独立部署/回滚材料(总工执行,尚未部署)
|
||||
|
||||
1. 审核确切任务分支和提交,复核8310空闲;保持8300和其他既有服务不动。建立 `/opt/cybergril-api/releases/<release>/`,构建生产前端和独立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是当前约束,后续扩展需独立设计。
|
||||
@@ -1,64 +0,0 @@
|
||||
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=<id>` | 升序 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;它不是部署依赖。
|
||||
@@ -1,7 +0,0 @@
|
||||
普通入口此前只有未接服务说明,无法创建真实档案或与模型聊天。本次接通唯一服务器档案、无副作用排盘预览、真实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和共享模型服务,交总工独立审核。
|
||||
@@ -1,20 +0,0 @@
|
||||
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。当前单用户内网、单进程;近轮上下文与进程内预算不是长期记忆或持久费用账本。
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
|
Before Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 535 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 90 KiB |
@@ -1,6 +1,6 @@
|
||||
# CyberGril 需求入口
|
||||
|
||||
当前完整基准:**v1.10**。批准需求继承办法的主卡评论:`01a1635b-b47c-79ed-ac66-9408c7905af9`。
|
||||
当前完整基准:**v1.9**。批准需求继承办法的主卡评论:`01a1635b-b47c-79ed-ac66-9408c7905af9`。
|
||||
|
||||
所有新任务、续办、设计、施工、审核先读完整当前需求,再读本次相关来源。只有白栖知明确修改的要求才可变更,阶段范围不能覆盖产品要求。
|
||||
|
||||
@@ -35,8 +35,4 @@ v1.8新增来源见 `sources/comment-evidence-visual-role-20261022.json`,最
|
||||
|
||||
v1.9新增来源见 `sources/comment-evidence-shared-space-20261023.json` 与 `sources/shared-space-static-acceptance-20261023.md`;仅更新VIS-011交流静图认可边界,相处、过渡、雾光、手机与真实人物动态仍待验收。
|
||||
|
||||
v1.10新增来源见 `sources/comment-evidence-dynamic-sample-20261023.json` 与 `sources/dynamic-sample-effect-acceptance-20261023.md`;对坐动态小样 v2 整体效果(演示层面)记入 VIS-011 与 CG-004 边界,手机真机、候选容器启用、真实人物动态与完整功能仍待验收。
|
||||
|
||||
main 当前仍为 v1.8(提交 `3a986726d8593e0ed2274f79db6cc5d807d2ebf0`);v1.9 与 v1.10 在待合并分支线(`agent/agent/54df81e610fd` → `agent/hel-811-handoff-f59f74dc4256` → 后续分支)上递进,合并入 main 由总管与白栖知决定。
|
||||
|
||||
后续以该入口所记录的最新版本为准,不把v1.7永久钉死为永不可新增的文档。每次新版本必须记录用户明确裁定,保留旧版来源与覆盖关系。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"version": "1.10",
|
||||
"approval_comment_id": "01a16810-3b46-7345-8966-dd87b76efe07",
|
||||
"version": "1.9",
|
||||
"approval_comment_id": "01a163a5-e2a7-7ac8-afdb-991868e721bc",
|
||||
"owner_id": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
|
||||
"scope": "完整要求;v1.8按用户明确指令新增视觉总监接手及必要代码职责,原58项和已批准边界继续有效,不证明功能已实现或已验收。",
|
||||
"requirements": [
|
||||
@@ -48,7 +48,7 @@
|
||||
"category": "产品",
|
||||
"topic": "交流镜头",
|
||||
"conclusion": "日常可看她在环境中生活;交谈时允许自然切近到同一人物、同一房间、同一套衣服的半身或脸部。",
|
||||
"boundary": "具体切镜方式和衔接效果仍要通过小样确认,不能据此认定用户接受突兀跳切。对坐动态小样 v2 的推近/退回过渡已按评论 01a16810-3b46-7345-8966-dd87b76efe07 获认可(演示层面,人物为参考静帧);真实视频的切近衔接仍待验收。",
|
||||
"boundary": "具体切镜方式和衔接效果仍要通过小样确认,不能据此认定用户接受突兀跳切。",
|
||||
"source_comment_ids": [
|
||||
"01a15724-dcaa-7d2b-ae23-430f24ca36b9",
|
||||
"01a15726-e178-72b0-9a3c-8b2041538730"
|
||||
@@ -501,7 +501,7 @@
|
||||
"category": "视觉",
|
||||
"topic": "人物独立视窗",
|
||||
"conclusion": "人物平时作为空间中独立视窗;交流时自然放大。已认可的人物位置、分量与首页文字分量沿用。",
|
||||
"boundary": "具体实现不得改变已确认体验;现有界面不等于用户最终满意。交流状态静图已按评论 01a1668f-5810-7524-b72f-33574a32c7d6 认可为 HEL-798 共同空间构图;对坐动态小样 v2 的整体效果已按评论 01a16810-3b46-7345-8966-dd87b76efe07 认可(同室相处、推近/退回过渡、交流呈现与两主题,演示层面);手机候选容器默认关闭未获批启用,真实人物动态、语音口型、真实生活与完整功能仍待验收。",
|
||||
"boundary": "具体实现不得改变已确认体验;现有界面不等于用户最终满意。交流状态静图已按评论 01a1668f-5810-7524-b72f-33574a32c7d6 认可为 HEL-798 共同空间构图;相处状态、过渡、雾光、手机与真实人物动态仍待验收。",
|
||||
"source_comment_ids": [
|
||||
"01a15f09-db1e-705c-8baf-cde63c7651fb",
|
||||
"01a15f1f-5a71-783c-a458-b326a0073d69"
|
||||
|
||||
@@ -87,17 +87,6 @@
|
||||
"VIS-011"
|
||||
],
|
||||
"impact": "仅交流状态静图获认可:不用独立人物框,环境铺满四边,前景为用户一侧桌沿/茶杯/烛灯,对话在视线下方。相处状态、过渡与动态、雾光、手机、真实人物动态与完整功能仍待验收;不永久取消 VIS-011 在其他状态的原规则,不把完整前端锁定为全屏照片或视频(CG-031 继续有效)。来源:sources/comment-evidence-shared-space-20261023.json 与 sources/shared-space-static-acceptance-20261023.md(原话经总工 HEL-811 对照平台逐条核验)。"
|
||||
},
|
||||
{
|
||||
"version": "1.10",
|
||||
"approval_comment_id": "01a16810-3b46-7345-8966-dd87b76efe07",
|
||||
"approved_by": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
|
||||
"summary": "对坐动态小样 v2 整体效果获认可:白栖知体验离线 HTML(SHA256 826bfc045d941c2114b16a198626ca952ff3d95dff02cfc6e32ee09858bcf80e)后回复“这个效果可以”;在 VIS-011 与 CG-004 边界注明演示层面的相处、推近/退回过渡、交流呈现与两主题认可。",
|
||||
"affected_ids": [
|
||||
"VIS-011",
|
||||
"CG-004"
|
||||
],
|
||||
"impact": "仅已交对坐动态小样的效果(电脑端、演示数据)。不视为完整产品、其余页面或真实功能验收;手机候选默认关闭未获批启用;相处静帧中的书本不恢复已否定的默认阅读(01a166aa);真实人物动态、语音口型、真实生活与完整功能仍待验收;不授权替换正式界面或合并 PR。来源:sources/comment-evidence-dynamic-sample-20261023.json 与 sources/dynamic-sample-effect-acceptance-20261023.md。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,13 +21,3 @@ v1.7:把原始31项、已确认视觉和后续用户裁定整理到同一入
|
||||
旧要求(VIS-011):人物平时作为空间中独立视窗;交流时自然放大。新认可:交流状态(靛蓝月光、电脑 1600×1000)的静态画面不再用独立人物框,环境铺满四边,前景为用户一侧桌沿/茶杯/烛灯,对话在近处视线下方;VIS-011 条文保留,边界注明该更新。
|
||||
|
||||
影响范围:仅交流状态静图。相处状态、过渡与动态、雾光、手机、真实人物动态与完整功能仍待验收;不永久取消 VIS-011 其他状态原规则,不把完整前端锁定为全屏照片或视频(CG-031 继续有效)。原话与适用范围见 `sources/comment-evidence-shared-space-20261023.json`、`sources/shared-space-static-acceptance-20261023.md`(原话经总工 HEL-811 对照平台逐条核验)。
|
||||
|
||||
## v1.10:对坐动态小样整体效果认可
|
||||
|
||||
白栖知体验《对坐动态小样 v2》(HEL-799,附件 `01a167c1-455b-751a-ac37-9fa984a1ed10`,SHA256 `826bfc045d941c2114b16a198626ca952ff3d95dff02cfc6e32ee09858bcf80e`)后明确认可:**“这个效果可以”**
|
||||
|
||||
来源评论:`01a16810-3b46-7345-8966-dd87b76efe07`(2026-10-23 09:20:56 UTC,回应总管交付评论 `01a167c1-458d-795b-81e1-584089bf3667`)。
|
||||
|
||||
旧边界(v1.9):仅交流静图获认可,相处、过渡、雾光、手机与整体动态仍待验收。新认可:该小样的整体效果(同室相处、推近/退回过渡、交流呈现与阅读方式、月光/雾光两主题,电脑端演示数据)在演示层面获认可;VIS-011 与 CG-004 条文保留,边界注明。
|
||||
|
||||
影响范围:不视为完整产品、其余页面或真实功能验收;手机候选容器默认关闭未获批启用,真机未验证;相处静帧中的书本不恢复已否定的默认阅读(`01a166aa-5f37-77b2-a2bd-ceb62f665813`);真实人物动态、语音口型、真实生活与完整功能仍待验收;不授权替换正式界面或合并 PR。原话与适用范围见 `sources/comment-evidence-dynamic-sample-20261023.json`、`sources/dynamic-sample-effect-acceptance-20261023.md`(原话经总工 HEL-816 对照平台核验)。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"baseline_version": "1.10",
|
||||
"baseline_version": "1.9",
|
||||
"items": [
|
||||
{
|
||||
"requirement_id": "CG-001",
|
||||
@@ -21,11 +21,9 @@
|
||||
},
|
||||
{
|
||||
"requirement_id": "CG-004",
|
||||
"status": "小样演示层面的推近/退回切镜已获用户认可(01a16810);真实视频切近衔接仍待验收",
|
||||
"evidence": [
|
||||
"docs/requirements/changes.json v1.10(01a16810)"
|
||||
],
|
||||
"next_step": "真实视频入场/退出/往返衔接按 CG-030 首批小队与 HEL-803 条件承接,验证后另行验收"
|
||||
"status": "未完成或未整体验收;不得据旧样机或测试数字声称已满足",
|
||||
"evidence": [],
|
||||
"next_step": "按完整需求与接入批次落实,交付时补真实证据"
|
||||
},
|
||||
{
|
||||
"requirement_id": "CG-005",
|
||||
@@ -251,13 +249,12 @@
|
||||
},
|
||||
{
|
||||
"requirement_id": "VIS-011",
|
||||
"status": "交流静图(HEL-798月光构图)与对坐动态小样 v2 整体效果(演示层面)已获用户认可;手机真机、候选容器启用、真实人物动态与完整功能仍待验收",
|
||||
"status": "交流静图(HEL-798月光构图)已获用户认可;相处、过渡、雾光、手机与整体动态仍待用户验收",
|
||||
"evidence": [
|
||||
"docs/requirements/changes.json v1.9(01a1668f)",
|
||||
"docs/requirements/changes.json v1.10(01a16810)",
|
||||
"prototypes/hel-799/(HEL-811 独立复核通过;v2 效果经白栖知体验认可)"
|
||||
"prototypes/hel-799/(HEL-811 独立复核通过)"
|
||||
],
|
||||
"next_step": "新素材(多时段天气穿搭、无书近景)按 HEL-802/803 承接;接入正式前端按 HEL-816 方案另行验收"
|
||||
"next_step": "HEL-799 动态小样交白栖知体验验收;相处/雾光/手机与真实素材按主卡承接"
|
||||
},
|
||||
{
|
||||
"requirement_id": "VIS-012",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| CG-001 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
| CG-002 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
| CG-003 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
| CG-004 | 小样演示层面的推近/退回切镜已获用户认可(01a16810);真实视频切近衔接仍待验收 | 真实视频入场/退出/往返衔接按 CG-030 首批小队与 HEL-803 条件承接,验证后另行验收 |
|
||||
| CG-004 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
| CG-005 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
| CG-006 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
| CG-007 | R1 已有相关实现与测试交付,尚不能认定整项已由用户验收 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
@@ -45,7 +45,7 @@
|
||||
| VIS-008 | 用户不满意当前对话与整体沉浸体验,独立小样待制作及验收 | 本轮批准的界面动态小样 |
|
||||
| VIS-009 | 用户不满意当前对话与整体沉浸体验,独立小样待制作及验收 | 本轮批准的界面动态小样 |
|
||||
| VIS-010 | 用户不满意当前对话与整体沉浸体验,独立小样待制作及验收 | 本轮批准的界面动态小样 |
|
||||
| VIS-011 | 交流静图(HEL-798月光构图)与对坐动态小样 v2 整体效果(演示层面)已获用户认可;手机真机、候选容器启用、真实人物动态与完整功能仍待验收 | 新素材(多时段天气穿搭、无书近景)按 HEL-802/803 承接;接入正式前端按 HEL-816 方案另行验收 |
|
||||
| VIS-011 | 交流静图(HEL-798月光构图)已获用户认可;相处、过渡、雾光、手机与整体动态仍待用户验收 | HEL-799 动态小样(总工 HEL-811 已独立复核)交白栖知体验验收 |
|
||||
| VIS-012 | 用户不满意当前对话与整体沉浸体验,独立小样待制作及验收 | 本轮批准的界面动态小样 |
|
||||
| VIS-013 | 用户不满意当前对话与整体沉浸体验,独立小样待制作及验收 | 本轮批准的界面动态小样 |
|
||||
| AM-001 | 未完成或未整体验收;不得据旧样机或测试数字声称已满足 | 按完整需求与接入批次落实,交付时补真实证据 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CyberGril 完整当前需求 · v1.10
|
||||
# CyberGril 完整当前需求 · v1.9
|
||||
|
||||
完整要求;版本1.10在v1.9(交流静图认可)之上,把白栖知已体验的对坐动态小样 v2 整体效果(演示层面)记入 VIS-011 与 CG-004 边界;不证明其余页面、真实功能或完整产品已验收。
|
||||
完整要求;版本1.9保留原完整要求与视觉总监接手分工,仅更新VIS-011的交流静图认可边界,不证明其余状态、动态或完整功能已验收。
|
||||
|
||||
## 阅读与效力
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
日常可看她在环境中生活;交谈时允许自然切近到同一人物、同一房间、同一套衣服的半身或脸部。
|
||||
|
||||
边界:具体切镜方式和衔接效果仍要通过小样确认,不能据此认定用户接受突兀跳切。对坐动态小样 v2 的推近/退回过渡已按评论 01a16810-3b46-7345-8966-dd87b76efe07 获认可(演示层面,人物为参考静帧);真实视频的切近衔接仍待验收。
|
||||
边界:具体切镜方式和衔接效果仍要通过小样确认,不能据此认定用户接受突兀跳切。
|
||||
|
||||
来源评论:`01a15724-dcaa-7d2b-ae23-430f24ca36b9`、`01a15726-e178-72b0-9a3c-8b2041538730`。
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
|
||||
人物平时作为空间中独立视窗;交流时自然放大。已认可的人物位置、分量与首页文字分量沿用。
|
||||
|
||||
边界:具体实现不得改变已确认体验;现有界面不等于用户最终满意。交流状态静图已按评论 01a1668f-5810-7524-b72f-33574a32c7d6 认可为 HEL-798 共同空间构图;对坐动态小样 v2 的整体效果已按评论 01a16810-3b46-7345-8966-dd87b76efe07 认可(同室相处、推近/退回过渡、交流呈现与两主题,演示层面);手机候选容器默认关闭未获批启用,真实人物动态、语音口型、真实生活与完整功能仍待验收。
|
||||
边界:具体实现不得改变已确认体验;现有界面不等于用户最终满意。交流状态静图已按评论 01a1668f-5810-7524-b72f-33574a32c7d6 认可为 HEL-798 共同空间构图;相处状态、过渡、雾光、手机与真实人物动态仍待验收。
|
||||
|
||||
来源评论:`01a15f09-db1e-705c-8baf-cde63c7651fb`、`01a15f1f-5a71-783c-a458-b326a0073d69`。
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"task": "HEL-816 总工:记录对坐动态小样 v2 效果认可(升版 v1.10),核对现有真实功能与 PR 状态,交完整界面与真实功能接续方案;本文件为总工交接",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "e04e258cc0c6ae097f57a4bd2c41a95a1bc6baa8",
|
||||
"read_notes": [
|
||||
"完整需求 v1.9(59项)与 sources/ 于待合分支线提交 e04e258(agent/hel-811-handoff-f59f74dc4256,含总工 HEL-811 保全 ad6a3b1 与总管版本说明修正)完整读取;origin/main 仍为 3a986726(v1.8),两者关系已在 README.md 说明。",
|
||||
"总工本轮以 CLI 对照平台核验新认可原话(01a16810,作者、时间、原话逐字一致)后,按治理规则升版 v1.10(仅 VIS-011 与 CG-004 边界注明小样演示层面认可),来源原话进 sources/ 并写入 source-manifest;未改任何条文结论、历史来源与产品代码。"
|
||||
],
|
||||
"requirement_ids": ["CG-001", "CG-002", "CG-003", "CG-004", "CG-005", "CG-006", "CG-007", "CG-008", "CG-009", "CG-010", "CG-011", "CG-012", "CG-013", "CG-014", "CG-015", "CG-016", "CG-017", "CG-018", "CG-019", "CG-020", "CG-021", "CG-022", "CG-023", "CG-024", "CG-025", "CG-026", "CG-027", "CG-028", "CG-029", "CG-030", "CG-031", "VIS-001", "VIS-002", "VIS-003", "VIS-004", "VIS-005", "VIS-006", "VIS-007", "VIS-008", "VIS-009", "VIS-010", "VIS-011", "VIS-012", "VIS-013", "AM-001", "AM-002", "AM-003", "UX-001", "UX-002", "UX-003", "UX-004", "UX-005", "UX-006", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "(1)需求治理:sources/ 新增 comment-evidence-dynamic-sample-20261023.json 与 dynamic-sample-effect-acceptance-20261023.md(原话、附件 ID、SHA256、旧/新范围、待实证项),升版 v1.10(VIS-011、CG-004 边界,changes/coverage/README 同步),main 与待合分支关系写入 README;(2)核对:main=3a98672(v1.8,无 R1 代码);PR#4=db571ba(HEL-780 真实档案+流式文字、HEL-783 修复,基点 4984293,未合);PR#6=ca5ddd6(HEL-790 旧小样,基点 4242f83,未合);PR#8=21170f9(HEL-793/795 沉浸对话原型,基点 81d7cb5,未合);8310 实际运行 release r2-20261022-1808-db571ba(=PR#4 head),/api/health 正常、仅 text 能力为真、companion 已创建(未读取档案内容);(3)接续方案《HEL-816 接续方案》(本卡附件):接缝清单、可机械复用/须视觉总监亲自的划分、下一批有限施工提案与验收样例、备份回退、须白栖知拍板项。未写产品代码、未部署、未合并 PR、未动 8300/8310 与用户数据。",
|
||||
"acceptance": [
|
||||
"原话核验:01a16810-3b46-7345-8966-dd87b76efe07(member e5c6c938,2026-10-23T09:20:56Z,根评论,内容『这个效果可以』)与交付评论 01a167c1-458d(2026-10-23T07:54:41Z,附件 01a167c1-455b)经 multica CLI 对照平台一致;HTML 附件下载后 SHA256=826bfc045d941c2114b16a198626ca952ff3d95dff02cfc6e32ee09858bcf80e,与仓库 prototypes/hel-799/index.html 及 HEL-811 重建记录逐字节一致。",
|
||||
"PR 状态核验:以 git 提交图核实 pr4=db571ba(2 提交,merge-base 4984293)、pr6=ca5ddd6(merge-base 4242f83)、pr8=21170f9(2 提交,merge-base 81d7cb5)均不在 main 历史内(merge-base --is-ancestor 复核);Multica 主卡/旧主卡链接表无关联 PR;本环境无 Gitea 令牌,PR 开合状态以平台页面为准,未擅合。",
|
||||
"服务只读核查(2026-10-23T09:33:44–55Z UTC,经既有授权 SSH 最小读取):.12 端口 8300/8310/8090/8010 均 LISTEN;/opt/cybergril-api/current→releases/r2-20261022-1808-db571ba;systemd active;GET /api/health 返回 ok、version R1、epoch 3、companion_created true、llm ok(halogen-qwen3.8-flash-next)、capabilities 仅 text=true。未读聊天正文与档案内容,未输出凭据。",
|
||||
"方案边界:提案不把外观换版与新增后端绑成巨型施工;不以口型未成阻塞完整前端;R1→R2/R3→R4、R5/R6 依赖 R1、R7 需授权的顺序保持;AM-001/002/003 与出生底色/记忆/关系守卫、演示与真实数据隔离逐条列入各批验收。"
|
||||
],
|
||||
"deferred": [
|
||||
"真实生活循环、记忆、主动联系、语音(R5 复用 /opt/voice-agent 底座需实测)、天气素材(R6)、全量备份、授权视觉观察(R7):均未实现未验证,按批次承接;R1 人格当前仅浅模板(系统提示组装),CG-014/015 的诠释自由与成长分层不足,保留为后续批次要求。",
|
||||
"视频日常→回应→交谈→退出→日常的同人同衣同光首尾对齐:无任何真实验证;缺生成工具与首尾可控能力,最低输入清单已在方案中列出(按 HEL-802 v2 母版/提示词、HEL-803 衔接条件),首批责任在小队(CG-030)不转给用户;未安装/付费/换模型。",
|
||||
"手机候选 4:5 容器与手机真机体验:默认关闭未获批启用,真机键盘/流畅度未测;Firefox/Safari 未测(HEL-811 已记录精确原因)。",
|
||||
"v1.9+v1.10 均在待合分支线(ad6a3b1→e04e258→本分支),main 仍 v1.8;本环境无 Gitea 令牌无法开 PR,合并与否由总管与白栖知决定。"
|
||||
],
|
||||
"requirement_changes": [
|
||||
{
|
||||
"approval_comment_id": "01a16810-3b46-7345-8966-dd87b76efe07",
|
||||
"approved_by": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
|
||||
"quote": "这个效果可以",
|
||||
"kind": "对坐动态小样整体效果认可",
|
||||
"affected_ids": ["VIS-011", "CG-004"],
|
||||
"old": "v1.9:仅交流静图获认可;相处状态、过渡、雾光、手机与真实人物动态仍待验收。",
|
||||
"new": "对坐动态小样 v2 整体效果(同室相处、推近/退回过渡、交流呈现与阅读方式、月光/雾光两主题;电脑端、演示数据)在演示层面获认可。",
|
||||
"scope": "不视为完整产品、其余页面或真实功能验收;手机候选默认关闭未获批启用;相处静帧中的书本不恢复已否定的默认阅读(01a166aa);真实人物动态、语音口型、真实生活与完整功能仍待验收;不授权替换正式界面或合并 PR。",
|
||||
"evidence_file": "docs/requirements/sources/comment-evidence-dynamic-sample-20261023.json",
|
||||
"artifact_sha256": "826bfc045d941c2114b16a198626ca952ff3d95dff02cfc6e32ee09858bcf80e"
|
||||
}
|
||||
],
|
||||
"verification": [
|
||||
"python3 scripts/check_requirements.py --base-ref origin/main:通过(59项;v1.10 升版、来源追加、历史保留、新批准评论未复用)。",
|
||||
"python3 scripts/check_requirements.py --base-ref origin/main --handoff docs/requirements/handoff-HEL-816.json:通过。",
|
||||
"git diff --check:通过。产品代码零改动(diff 仅 docs/requirements 与本文件)。"
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"task": "HEL-821 视觉总监:A1+A2 把对坐场景(hel-799 scene/ambient/room)和完整对话呈现接入正式 React 前端;本文件为视觉总监交接(额度中断后恢复续交)",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "63799f02474ed94fd793de9e2360d185c878165d",
|
||||
"read_notes": [
|
||||
"无 git 环境:需求与 prototypes/hel-799 读自总管机械包《CyberGril-A批施工基底-PR4与需求v1.10.zip》(SHA256 0c5b1665048960b3102216a834e26791dee2f665b467d49e62bfb50319f3c138),其 SOURCE-BUNDLE-PROVENANCE.json 记 requirements_and_hel799_source=63799f0、product_base=db571ba(PR#4 head)。非独立 checkout;origin/main 仍 3a986726(v1.8)。",
|
||||
"需求文件随包置于 docs/requirements(v1.10,59项)。原运行开工阅读范围以其执行记录为准;本次恢复复读 README.md、task-handoff-template.md、handoff-HEL-816.json、sources/shared-space-static-acceptance-20261023.md,并对照 issue 原文逐条核对本卡七项施工要求。current/coverage/changes 全文须审核时对照原话复核;附件 emil 动效参考(animate/MAIN、RECIPES、review-animations/MAIN、STANDARDS)仅作参考,未安装或改动运行时技能。包内无仓库根 AGENTS.md 与 scripts/,未能读取/运行,交总工在仓库内补。",
|
||||
"施工授权:主卡 HEL-797 白栖知评论 01a169a8-5884-7c26-a00f-c23a5f5399a9『继续吧,现在没什么主要的内容需要我确认的』,回应总管提案 01a16851-302d-75ef-9e8f-cf1719998076。这是本轮前端施工授权,不是真实能力验收或旧产品规则修改。"
|
||||
],
|
||||
"requirement_ids": ["CG-002", "CG-004", "CG-006", "CG-007", "CG-010", "CG-012", "CG-013", "CG-014", "CG-015", "CG-018", "CG-024", "CG-029", "CG-030", "CG-031", "VIS-001", "VIS-002", "VIS-003", "VIS-004", "VIS-005", "VIS-006", "VIS-007", "VIS-008", "VIS-009", "VIS-010", "VIS-011", "VIS-012", "VIS-013", "UX-001", "UX-002", "UX-003", "UX-005", "UX-006", "AM-001", "AM-002", "AM-003", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "正式 React 源码(基于 PR#4 db571ba,未合并、未改 main):A1 新增 web/src/stage/camera.ts(同脸对齐的单相机、对数缩放、可打断临界阻尼弹簧)、roomScene.ts(逐帧只写 transform/opacity,推近/退回、反向保留速度、减少动效改为静置交叉淡入、自动降级档、页面隐藏暂停)、ambient.ts(窗内雨、城市灯、近景桌面,两主题材质光线不同),Presenter.tsx 改为共享空间三层;shared/space.ts 把此刻/交谈/记录/面板映射到空间并管理焦点;shared/motion.ts 键盘快照(即时落位);tokens.css/components.css 落 hel-799 room/talk/nav/pane 样式。A2 新增 web/src/chat/reading.ts(按轮分组、一两句分页、字素安全、无标点长句也断、流式最新一句先压住、阅读位置用文本锚点)、lineStage.ts(已显示的句子不重渲染不重播,墨入每句只播一次);ChatPanel.tsx 在原 RealChatAdapter/TurnGate/epoch/AbortController 数据流上接自动续读、暂停/上一句/下一句/回到读到的地方、全文、完整记录、身份标签、取消保留未完片段并标『未完成 · 已取消』。App.tsx/RealApp.tsx 接空间、Esc 逐层、导航直接切换;删除不再使用的 shared/SoftText.tsx;浏览器检查脚本迁到新选择器。",
|
||||
"acceptance": [
|
||||
"相处↔交流同一空间:点她或『和她说话』推近,『结束交谈』/Esc 退回;Esc 即时落位,鼠标/点击走约0.85s弹簧,中途反向不跳(camera-in-reverse-esc-1600-moon.webm)。",
|
||||
"两主题同结构:月光/雾光切换只换材质、光线、雨色与玻璃,不改布局(desk-1600-moon/fog-*)。",
|
||||
"交流:一次一两句,长回复自动完整续读;暂停后 ←/→/按钮即时回看无动画,『回到读到的地方』回到进度;全文与完整记录可读全部内容;『你』与实际名字区分;停止回复保留已生成部分并标未完成,未存入记录。",
|
||||
"流式增长不重置阅读位置、不改已显示句子(reading.test.ts 流式只追加用例);历史恢复按 sessionStorage 轮次锚点;快速取消后新轮与旧轮迟到事件由 TurnGate/epoch 过滤。",
|
||||
"演示 ?demo=1 独立 localStorage、零网络;真实模式不读演示数据(rework_browser_check.py 断言);未改 API 协议、SQLite、后端。",
|
||||
"浮层打开时导航可见可直接切换(.nav z25 高于 .record z20/21);Esc 顺序:模态→全文→记录→退回空间;全文关闭焦点回『全文』,记录关闭焦点回记录按钮;普通面板 240ms 进入,不沿用空间长转场。",
|
||||
"布局:1600×1000、1024、390×844、320×568、390×430 键盘等效下长中文与无标点长句不溢出,舞台/阅读条/输入/导航不重叠(check/shots/*.json 几何报告)。"
|
||||
],
|
||||
"deferred": [
|
||||
"构建与测试未运行:本机无 Node/npm/git/Python。须总工在既有 Linux 环境执行 npm ci、npm run typecheck、npm run build、npm run lint、npm test(含新 reading.test.ts)、Playwright 三份浏览器检查、python3 scripts/check_requirements.py --base-ref origin/main 及 --handoff、git diff --check。",
|
||||
"check/ 证据为静态 harness:产品 CSS + 剥类型后的控制器 + 静态 DOM,用 Edge CDP 截图/录屏;不是正式 React 运行,也未跑真实 SSE;录屏帧率不代表产品 GPU 性能。",
|
||||
"rework_browser_check.py 本次恢复后才迁移(原运行未迁),未执行;lineStage 墨入每句只播一次为本次恢复时新增修正,未进 harness 录屏。",
|
||||
"A3 其余页面(日子/回忆/创建/设置按 HEL-815 稿)未改,保持原功能与入口。",
|
||||
"人物仍为旧参考静帧,推近是镜头不是人物动作;新日常视频、首尾衔接、真实口型、语音、天气、生活、记忆、备份、视觉观察均未接入(R1 人格深度、R2–R7 继续承接);手机 4:5 候选默认关闭;真机/Safari/Firefox/读屏未测。"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"未执行:tsc/vite build/vitest/eslint/Playwright/check_requirements/git diff --check(缺工具,如实交总工)。",
|
||||
"人工静态复核(本次恢复):base 对 orig 逐文件 SHA256 比对得 13 改、8 增(另加本交接 JSON)、1 删,逐文件哈希见包内 CHANGES.json;全部相对 import 可解析,无残留 SoftText 引用;tsconfig ES2022 覆盖 Array.at 与 Intl.Segmenter。",
|
||||
"原运行 harness 证据:check/shots 桌面两主题与 390/320/390×430 截图及几何 JSON;check/rec 三段 webm(进出、快速反向+Esc、手机进出)。"
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"task": "HEL-822 总工:HEL-821 A1+A2 源码机械落库、Linux 构建、四份浏览器检查、真实SSE独立审核;因发现必须修复项未部署",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "63799f02474ed94fd793de9e2360d185c878165d",
|
||||
"read_notes": "需求/治理文件与 63799f0 字节核对一致;产品基底 db571ba(PR#4 head,refs/pull/4/head 复核仍指向 db571ba);origin/main 3a986726(v1.8)。",
|
||||
"requirement_ids": ["CG-002", "CG-004", "CG-018", "CG-024", "CG-031", "VIS-003", "VIS-004", "VIS-007", "VIS-011", "VIS-013", "UX-001", "UX-002", "UX-005", "UX-006", "AM-001", "AM-002", "AM-003", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "候选分支 hel-822-candidate(自 db571ba):核验作者包 SHA256 后按 CHANGES.json 落库 13改/8增(另交接JSON)/1删,全部哈希一致;docs/requirements v1.10 与 prototypes/hel-799 字节等于治理提交 63799f0;补齐根 AGENTS.md 与 scripts/check_requirements.py。审核侧最小机械检查修正 3 处(browser_check 视觉观察(模拟)选择器、import_check 脏表单离开确认处理、rework_browser_check 长用户句预算60s/条件续读/紧凑打字沉降600ms)并附本机全部运行证据。",
|
||||
"acceptance": [
|
||||
"机械落库核验:ZIP 250条反斜杠路径规范化解包零越界;CHANGES 23条前后哈希全部复算一致;git status 恰为23条变更+治理增补。",
|
||||
"构建:npm ci/typecheck/lint/test/build 全过(lint 1条既有模式警告);vitest 22/22 含新 reading.test.ts 8条。",
|
||||
"后端:Python 3.12 下 142/142 通过;本机默认 3.10 缺 asyncio.timeout(pyproject 要求≥3.11)致 8 条流式相关测试失败,基底 db571ba 同样失败,属环境非回归。",
|
||||
"浏览器检查:browser_check 16/16、import_check 7/7、real_browser_check 全过(真实网关3轮真实SSE、取消、重启epoch 1→2、移动端、受控错误重试,隔离临时库);rework_browser_check 前3节过后被下方问题1阻塞(本地绕过后12视口节通过)。",
|
||||
"check_requirements 对 origin/main 与实际候选目标 origin/agent/agent/de8874a3803b 均通过(59项);git diff --check 干净。",
|
||||
"代码卫生:无硬编码密钥/console残留/debugger;reduced-motion 全局处理;hover 动效均以 (hover:hover) and (pointer:fine) 门控。"
|
||||
],
|
||||
"deferred": [
|
||||
"未部署:审核发现2项必须修复问题(见评论),按卡交总管退回原作者返工后复审一次。",
|
||||
"候选 PR 未开:Gitea ssh 远端无 CLI/API 凭据,分支 hel-822-candidate 已推送,开 PR 需总管经 Web/API 以 base=agent/agent/de8874a3803b 操作。",
|
||||
"真机/Safari/Firefox/读屏未测;录屏fps不代表GPU流畅度;人物仍为参考静帧;R1人格深度与R2–R7、A3页面(HEL-815稿)继续承接。"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"真实SSE证据:real_browser_check 全部PASS输出(含 REAL_REPLY 蓝色风铃 两轮上下文召回、取消后无完整assistant行、epoch重启、390×844移动端)。",
|
||||
"问题1证据:390×420 紧凑高度下聚焦输入后点击发送,click 事件落点 SECTION.conversation(trusted click 捕获),草稿未发出;先blur再点击则命中。发送失败连锁导致 rework 检查 390×420 节全文本按钮永不出现。",
|
||||
"问题2证据:受控慢流取消→立即新轮后,阅读停在用户句约10–15秒(dwell 多次被重置,进度条读数 0.06→0.93 证据),随后自行恢复推进到『答复』。",
|
||||
"本审核机环境:uv 安装 Python 3.12.14 于 .venv312(项目依赖+playwright);系统 Chromium 经 LD_LIBRARY_PATH=~/pwlibs 与 FONTCONFIG_FILE 启动。"
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"task": "HEL-823 视觉总监:把 HEL-815 的日子、已分享生活详情、共同回忆、首次创建、设置管理完整页面接到正式 React 产品(A3)",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "cef8ae5581f68ae81991533698ef05abcb82e659",
|
||||
"read_notes": "唯一施工基底为远端 hel-822-candidate 提交 cef8ae5(总管 git archive 附件 HEL-823-A3-base-cef8ae5.zip,SHA256 245dab3f124c0c987a322c4dd788dbb7e7f5e24c4fb96f9eb50f4ec6c61a3894,本机复算一致)。本机实际阅读该包内根 AGENTS.md、docs/requirements/README.md、current.md(v1.10,59项)、coverage.md、changes.md、task-handoff-template.md、handoff-HEL-824.json,并按编号核对 CG-006/007/012–016/018/024/029–031、VIS-001–013、AM-001–003、UX-001–006、GOV-001–005、DEL-001 原文与边界。页面设计依据为本人 HEL-815 原稿附件 01a16845-b50a-7335-a87e-61bf11c44703(README、src/pages.js、boards/01)。授权原话:主卡评论 01a169a8-5884-7c26-a00f-c23a5f5399a9「继续吧,现在没什么主要的内容需要我确认的」。模板里的旧 1.7 字样不覆盖当前入口 v1.10。",
|
||||
"requirement_ids": ["CG-006", "CG-007", "CG-012", "CG-013", "CG-014", "CG-015", "CG-016", "CG-018", "CG-024", "CG-029", "CG-030", "CG-031", "VIS-001", "VIS-002", "VIS-003", "VIS-004", "VIS-005", "VIS-006", "VIS-007", "VIS-008", "VIS-009", "VIS-010", "VIS-011", "VIS-012", "VIS-013", "AM-001", "AM-002", "AM-003", "UX-001", "UX-002", "UX-003", "UX-004", "UX-005", "UX-006", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "在 cef8ae5 上改 web 下 15 个文件(10 个源码、5 个测试)、新增本交接,无删除(清单与前后哈希见 CHANGES-HEL-823.json)。(1) web/src/life/LifePanel.tsx 重写为 HEL-815 日子页:标题区「她的日子 / 我们的回忆」+ 宽屏「日子 · 演示数据」/ ≤1100「她此刻 · …」;页签行附说明;列表按「她说过的打算」置顶、其余按日分组,行=时间·状态点(实心已发生/呼吸点正在/虚线打算)·标题·一句摘要·少量标签;分页「看更早的日子」→ 加载中 → 到底「再往前,就是她刚住进来的那天了」;演示失败开关下的读取失败卡与「再试一次」(加载骨架);空状态(首晚 / 真实后台未接)。详情:类别·时间·地点、何时告诉你、带身份的她的原话、关联回忆卡、那时候的聊天(只取已通过 publicView 的消息)、「在交谈里看这段」、演示说明。回忆四组带计数,详情「她是这样记得的」、成长注明不改写底色并可跳到设置·她、来自这些日子的反链或说明。任何解析不到的选择(过期、删除或从未讲过)统一显示「这一条现在打不开」,不暗示记录存在。每个页签保留自己的滚动位置,面板隐藏再打开(交谈返回日子)恢复;手机 Esc 先关推入详情。(2) web/src/manage/CreationWizard.tsx:左侧叙事随步骤与创建中/失败/完成变化;名字为空并有占位,错误时焦点落到出问题的字段(名字/生日/关系);生日「随机生成 / 我来指定」(默认随机一个满 19–35 岁的生日,可再随机;手动改日期或时辰即为指定);关系改为两张说明卡且不预选;新增可选相处边界(后端 CreationInput 已接受 boundary,未改 API);汇总含边界;创建中提示、失败「返回修改 / 再试一次」资料保留;真实 409「已经有一位她」时禁用再次创建并提供「重新打开,恢复已有的她」(只 reload,不覆盖);完成页「第一晚」。(3) web/src/manage/ManagePanel.tsx:分区列表加能力说明与手机推入箭头、底注「首版只有一位她 · 没有重建或清空」;分区标题旁能力标签(只读/可以调整/演示数据/未接入/文字已接通/本机生效);「她」改为名字只读+生辰只读、底色(沿用 BaseColor 真实排盘)、「她怎么理解自己」诚实未接卡(不放固定段落)、最初就有的/后来慢慢有的分栏(成长只取已分享来源的回忆);关系改为分段按钮,确认框写明「把关系从「X」改成「Y」?」并保留记忆与信任说明;真实城市/外观/备份/语音视觉开头加「还没有接上」说明卡(接上后做什么、现在什么都不会发生),原有待接入细节保留;减少动效改为开关样式。(4) web/src/stage/Presenter.tsx 新增「她此刻」浮签:面板打开且宽度 >1100 时贴在她下方,随相机落定与窗口变化跟随命中区;≤1100 改写在面板标题上方。(5) App.tsx / RealApp.tsx 传入此刻文字、演示失败开关与「在设置里看底色与成长」跳转;domain/model.ts LifeEvent 增可选 told;demo/fixtures.ts 演示条目补 told;adapters/storage.ts 恢复时只为已分享条目保留 told。(6) styles/components.css 追加 A3 区块(两主题共用 token,状态色只用于点、细线与短字;≤1100、≤720、≤720×620、≤360 断点)。(7) tests:domain.test.ts 新增 told 只保留在已分享记录的单测;browser_check / real_browser_check / import_check / rework_browser_check 只按本次有意的界面变化改选择器(关系下拉→分段按钮,「看那段聊天」→「在交谈里看这段」,私有深链断言改为中性「这一条现在打不开」),其余断言不动。",
|
||||
"acceptance": [
|
||||
"日子(演示 ?demo=1,载入三天记录):打算置顶并标「计划可能会变」,正在为呼吸点;列表与详情、回忆、搜索不出现未分享条目;#event=secret 深链只显示「这一条现在打不开。」,页面不含「秘密散步/私密地点」(browser_check 已改断言)。",
|
||||
"长记录:首屏 18 条,看更早的日子后 30 条;选第 11 条 → 在交谈里看这段 → 结束交谈 · 回到日子,列表滚动位置与选中项不变(import_check 原断言)。",
|
||||
"我们的回忆:真实入口四组各显示「暂无可核实的回忆」(.pane .list .grp = 4,rework 原断言);演示入口四组带计数,成长条目可跳到设置·她。",
|
||||
"首建:名字为空、关系不预选(两张卡 aria-pressed=false)、未满 18 岁与未选关系被拦并聚焦对应字段;勾选确认前「确认创建」禁用;演示失败后资料保留、再试一次成功;真实入口 409 不覆盖已有档案。",
|
||||
"设置:名字与生辰只读,无改名/新建/切换/清空/重建入口;改关系类型须确认,取消恢复,保存失败修改保留;离开有未保存修改须确认(原守卫未改);未接入能力全部标「未接入」且按钮不可用,演示能力标「演示数据」。",
|
||||
"两主题结构一致(同一 DOM,只换 token);1600×1000、1024、390×844、320×568、390×430 键盘等效下无关键遮挡——需总工以浏览器检查与截图实测,本机未能运行。",
|
||||
"未触碰 A1/A2:ChatPanel、reading.ts、lineStage、Viewport、compact/typing 规则、真实适配器/TurnGate/epoch 均未修改。"
|
||||
],
|
||||
"deferred": [
|
||||
"本机(Windows 运行时)没有 node/npm、python、git,也找不到可用的 TypeScript:未执行 npm ci/typecheck/lint/test/build、四份浏览器检查、check_requirements.py、git diff --check,也没有正式 React 页面截图或录屏;全部交总工机械执行,未运行即未通过,不以静态阅读或 HEL-815 静态样图代替。",
|
||||
"未提交、未开 PR:按 CHANGES-HEL-823.json 落到 hel-822-candidate(cef8ae5)之上的独立分支,产品目标仍为 agent/agent/de8874a3803b,不指 main;不合并、不部署 8320,8300/8310 未触碰。",
|
||||
"HEL-815 中「看接入后的设计稿」只作施工参照,按原稿施工须知未放进正式产品;城市、外观、备份、语音、视觉观察、生活、长期记忆、主动消息的真实能力仍未接通(R2–R7)。",
|
||||
"「她怎么理解自己」与最初小习惯在真实入口显示未接说明,由 R1 人格深度承接(CG-013/014/015、UX-004);不以固定模板填充。",
|
||||
"真机软键盘、Safari/Firefox、读屏未测;等效视口不冒充真机(UX-001)。人物仍为参考静帧,真实日常视频/首尾衔接/语音口型由主卡 HEL-797 与 HEL-802/803 承接(DEL-001)。"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"附件 SHA256 复算 245dab3f124c0c987a322c4dd788dbb7e7f5e24c4fb96f9eb50f4ec6c61a3894,与卡片一致;解包为只读 before 与可写 work 两份,修改只在 work,before 全部 250 个文件哈希另存。",
|
||||
"修改前后文件哈希与改/增/删清单见交付包 CHANGES-HEL-823.json。",
|
||||
"静态自查:逐文件回读 JSX 结构与类型(新增 props 均为可选、noUnusedLocals 下无多余变量),核对测试所依赖的可访问名称(她的名字、出生日期(公历)、1 / 4 · 出生资料、朋友/恋人 aria-pressed、确认创建、再试一次、去看看她、看更早的日子、结束交谈 · 回到日子、设置分区各按钮名、她怎么称呼你、相处边界、取消修改/保存/重试保存、.kv/.ev/.list/.grp/.city-list/.svc、关闭日子/关闭设置、has-detail 类串)保持不变。静态自查不等于构建或测试通过。",
|
||||
"待总工执行:cd web && npm ci && npm run typecheck && npm run lint && npm test && npm run build;PYTHONPATH=src python3.12 web/tests/{browser_check,import_check,real_browser_check,rework_browser_check}.py;python3 scripts/check_requirements.py --base-ref origin/main --handoff docs/requirements/handoff-HEL-823.json 及实际目标引用;git diff --check;并补 1600×1000、1024、390×844、320×568、390×430 两主题截图。"
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"task": "HEL-824 视觉总监:HEL-822 审核两项必须修复缺陷返工(紧凑高度聚焦后首击失灵;停止回复后立即新轮续读迟滞),并修正 rework 检查错误断言、核对 Viewport 无效逻辑",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "4321aebfdc491993e0648f68a0375047c3eb6359",
|
||||
"read_notes": "唯一返工基底为远端 hel-822-candidate 提交 4321aeb(总管 git archive 附件 HEL-822-candidate-4321aeb.zip,SHA256 d18d54891f365f4b7999b4defb8cc4f92dace3e296b96707c617ac08f3b3e870,本机复算一致)。已读根 AGENTS.md、docs/requirements/README.md、current.md(59项)、coverage.md、changes.md、task-handoff-template.md、sources/dynamic-sample-effect-acceptance-20261023.md,并按编号核对 CG-018、VIS-003/004/007/011/013、UX-001/002/005、GOV-001–005、DEL-001 原文与边界;模板中的旧1.7字样不覆盖当前入口 v1.10。审核原文:HEL-822 评论 01a16afe-0cb1-7146-b561-c348c9fc8ed8。",
|
||||
"requirement_ids": ["CG-018", "VIS-003", "VIS-004", "VIS-007", "VIS-011", "VIS-013", "UX-001", "UX-002", "UX-005", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "在 4321aeb 上修改 6 个文件、新增本交接:(1) web/src/styles/components.css 删除 html.compact.typing{--nav-space:6px}:聚焦输入只让导航淡出让位,对话区几何不随焦点变化,按下与松开落在同一按钮;交流视觉、两主题与导航淡出保留。(2) web/src/chat/reading.ts 新增 ReadingClock,阅读停留按真实时间计(每帧最多计 STALL=1000ms,页面隐藏后的间隔不计),同一句 key 不变即不重置;web/src/chat/ChatPanel.tsx 改用它(goTo→forget、setFlow→restart、visibilitychange→pause),自动续读、暂停、回看、回到进度、墨入只播一次的逻辑不变。(3) web/src/shared/Viewport.tsx 删除没有任何 CSS 对应的 viewport-resizing 类及其定时器,注释改为实际行为。(4) web/tests/reading.test.ts 新增 3 项时钟回归;web/tests/rework_browser_check.py 三处整行 to_have_text('答复') 改为只对正文 <p> 精确断言,新增 390×420/390×430 双主题聚焦后首击回归与 700ms 慢帧下停止→立即新轮回归。总工在 browser_check / import_check 的机械修正未触碰。",
|
||||
"root_causes": [
|
||||
"缺陷1:compact 下输入聚焦时 --nav-space 由 54px 变 6px;按下按钮使输入失焦,对话区在按下与松开之间下移 48px,click 落到两者共同祖先 SECTION.conversation。修法:对话区几何不随焦点改变(只保留导航淡出),未用先 blur、强制点击、脚本调 handler 或 pointerdown 绕过。",
|
||||
"缺陷2:阅读时钟每帧把 dt 截到 100ms(ChatPanel.tsx 原 113 行 Math.min(100, now - last)),停留时长随帧率缩放。停止→立即新轮时交叉淡化、模糊、墨入与房间画面同时进行,审核环境帧间隔约 1 秒,『新轮』1400ms 的停留被拉长为 14 帧≈14 秒;审核读到的 0.06…0.93 正是 1/14…13/14 的逐帧步进,满格后进入下一句再次从 0 起,看起来像反复重置。逐行核对:同一轮内 key 只由轮次与位置决定,流式增长、落库替换、旧轮迟到事件(TurnGate 拒收)都不会改变当前句 key,也没有其他自动调用 forget/restart 的路径。修法:按真实时间计时,不缩短任何设计停留、不取消自动阅读、不放宽测试等待(8s 超时保持)。"
|
||||
],
|
||||
"acceptance": [
|
||||
"正式 React 月光/雾光,390×420 与 390×430:输入框聚焦后直接点发送、上一句、暂停/继续、全文,均首击生效;草稿真正发出并落库(用户+她各1条)、全文浮层真正打开;按下与松开时对话区 top 相同、click 送达该按钮(rework_browser_check 新节,真实指针点击,脚本不 blur 不派发)。",
|
||||
"停止回复→立即新轮:取消即时、旧轮迟到不串入(当前句状态为空),新轮先显示你的一句(设计停留1.4s)后她的回复上台;在 rAF 每 700ms 一帧的慢帧下同样在原 8s 断言内完成(旧实现需约 9.8s 必然失败)。",
|
||||
"正常 60fps 下各句停留与修改前一致(每帧 dt≈16ms 远小于原 100ms 上限,行为不变);暂停/恢复、前后回看、回到读到的地方、全文、完整记录、同句墨入只播一次、历史恢复锚点逻辑未改动。",
|
||||
"reading.test.ts:任意帧间隔(16/100/250/700/1000ms)下你的一句在 1400ms 至 1400ms+一帧之间到期;流式增长与落库不回退进度;新句、回看、恢复从零开始,暂停/悬停与隐藏期间不计,单帧停顿最多计 1 秒。",
|
||||
"320×568、390×844、1024、1600×1000 布局:本次只删除 compact+typing 时的位移规则,非 compact 尺寸 CSS 无变化;仍需总工以 browser_check / rework 12 组视口复核。"
|
||||
],
|
||||
"deferred": [
|
||||
"本机(Windows 运行时)没有 node/npm、python、git,未能执行 typecheck/lint/test/build、四份浏览器检查、check_requirements.py 与 git diff --check;全部交总工机械执行,未运行即未通过,不以静态阅读代替。",
|
||||
"修前修后实测计时未在本机取得:修前数据沿用总工 HEL-822 观测(约10–15秒、0.06→0.93),修后应由总工以 rework 慢帧节的 slow_frames_new_turn_to_reply_s 及受控慢流原复现记录。",
|
||||
"未开 PR、未提交:本机无 git;按 changes 清单落到 hel-822-candidate 之上,PR 目标仍为 PR#4 产品线 agent/agent/de8874a3803b(原产品基底 db571ba),不指 main;远端引用名称须由总工用 git 实查。",
|
||||
"真机键盘、Safari/Firefox、读屏未测;等效视口不冒充真机(UX-001)。人物仍为参考静帧,真实视频/语音/口型/生活/记忆未接入(DEL-001)。A3 日子/回忆/创建/设置由 HEL-823 承接;R1 人格深度与 R2–R7 在主卡 HEL-797 追踪。"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"附件 SHA256 复算 d18d54891f365f4b7999b4defb8cc4f92dace3e296b96707c617ac08f3b3e870,与卡片一致;解包为只读 before 与可写 work 两份,修改只在 work。",
|
||||
"修改前后文件哈希与新增/删除清单见交付包 CHANGES-HEL-824.json(6 改、1 增、0 删)。",
|
||||
"未执行任何构建、测试或浏览器检查(工具缺失);待总工执行:cd web && npm ci && npm run typecheck && npm run lint && npm test && npm run build;PYTHONPATH=src python3.12 web/tests/{browser_check,import_check,real_browser_check,rework_browser_check}.py;python3 scripts/check_requirements.py --base-ref origin/main --handoff docs/requirements/handoff-HEL-824.json 及实际 PR 目标引用;git diff --check。",
|
||||
"真实网关 SSE 证据仍为 HEL-822 已有记录(本次未改后端、协议、SQLite、RealChatAdapter/TurnGate/epoch/AbortController);rework 新节使用隔离临时库与受控模型,二者分别标明。"
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"task": "HEL-825 总工:HEL-823(日子/回忆/首建/设置 A3)功能+界面独立审核,通过后更新 8320 隔离测试版",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "cef8ae5581f68ae81991533698ef05abcb82e659",
|
||||
"read_notes": "独立审核分支 hel-825-review 自 cef8ae5(hel-822-candidate 远端头,本机核验一致)。实际阅读根 AGENTS.md、docs/requirements/README.md、current.md(v1.10,59项)、coverage.md、changes.md、task-handoff-template.md,及 sources/design-acceptance-v1.md、visual-confirmation-v2.md、integration-plan-v1.md、dynamic-sample-effect-acceptance-20261023.md;核对 CG-006/007/012–016/018/024/029–031、VIS-001–013、AM-001–003、UX-001–006、GOV-001–005、DEL-001 原文与边界。交付包 SHA256 3a8e1720f347f7b5449c2ac5af116553a41a28982046d4d19eadbb1f4e2f2b27 本机独立复算一致;15改1增的 before 对 git cef8ae5、after 对源码字节逐项独立核验一致,无未列明改动、无删除、无路径越界。",
|
||||
"requirement_ids": ["CG-006", "CG-007", "CG-012", "CG-013", "CG-014", "CG-015", "CG-016", "CG-018", "CG-024", "CG-029", "CG-030", "CG-031", "VIS-001", "VIS-002", "VIS-003", "VIS-004", "VIS-005", "VIS-006", "VIS-007", "VIS-008", "VIS-009", "VIS-010", "VIS-011", "VIS-012", "VIS-013", "AM-001", "AM-002", "AM-003", "UX-001", "UX-002", "UX-003", "UX-004", "UX-005", "UX-006", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "审核通过并部署。分支 hel-825-review(cef8ae5 之上三提交:0bc4a9e 作者交付原字节、e32a1ac 审核接线修正与证据、本 handoff 提交)。审核接线修正仅一处:web/tests/browser_check.py 关联回忆卡点击改 exact=False——新版 link-card 可访问名称由「标题+来源」拼接,作者改版时漏改该选择器,断言强度不变(heading 断言保留),不放宽隐私或回归断言。另新增 web/tests/hel825_shot.py、hel825_tag_probe.py 两个审核证据脚本。8320 更新为新 release 并存切 current,数据目录与档案不动。",
|
||||
"acceptance": [
|
||||
"机械构建:npm ci/typecheck/lint/test/build 全通过;lint 0 errors 2 warnings(ChatPanel:156 为基线已有,ManagePanel:123 unused-directive 为本次引入的非阻塞建议项)。前端 vitest 26/26;后端回归 pytest 142/142(与上批基线一致,本轮实际数字)。",
|
||||
"四份浏览器脚本全部通过:browser_check 16 PASS(1440/1280/390 三视口:创建校验/失败重试/重开、两主题日子/交谈/设置往返、私密深链中性文案、取消隔离、IME/XSS、发送失败重试、离开摘要过滤);import_check 7 PASS(分页18→12滚动保持、损坏持久化不覆盖首建等);real_browser_check 8 PASS(真实网关流式/取消/epoch 重启/SSE 错误/手机关系恢复);rework_browser_check 9 PASS(12 视口主题组合无遮挡、390×430 首击命中、reading 真实时钟、演示零 API)。",
|
||||
"需求检查:check_requirements.py --base-ref origin/main 与 origin/agent/agent/de8874a3803b(均带 --handoff handoff-HEL-823.json)通过,59项;git diff --check 干净。",
|
||||
"隐私与深链:#event=secret 只显示「这一条现在打不开。」,页面无「秘密散步/私密地点」;列表/详情/回忆/摘要无未分享条目(脚本断言+人工核对 LifePanel publicView 路径)。",
|
||||
"首建:名字空拦、未成年拦、关系不预选(朋友/恋人 aria-pressed=false)、未勾选确认创建禁用、失败资料保留、409 只提供恢复已有她(源码 74-75 行核对+脚本验证)。",
|
||||
"设置:名字/生辰只读;关系确认框含「记忆与信任都保留」;未保存离开确认守卫(beforeunload+cg:leave-settings 双路径,ManagePanel:43-49 核对);未接入能力如实标注。",
|
||||
"她此刻浮签几何实测:1600/1440/1101 now-chip 显示于角色下方;1100/1024/390 面板标题上方 .k-near「她此刻 · 在窗边待着」(hel825_tag_probe.py 六档全过)。",
|
||||
"截图证据:两主题 × 1600×1000/1024/390×844/320×568/390×430 键盘等效共 46 张(docs/frontend/screenshots/hel825/)+ 宽屏/手机两段录屏(docs/frontend/recordings/)。"
|
||||
],
|
||||
"deferred": [
|
||||
"真机软键盘、Safari、Firefox、读屏未测(等效视口不冒充真机,UX-001);本机模型不支持图像目检,截图以 DOM 几何断言+脚本覆盖代替人工目检,全部截图已附卡供人工复核。",
|
||||
"ManagePanel.tsx:123 unused eslint-disable directive(本次引入,非阻塞)与 ChatPanel.tsx:156 ref cleanup warning(基线已有)留作后续小优化,不阻塞本轮。",
|
||||
"生活、长期记忆、主动消息、语音口型、天气素材备份、视觉观察真实能力仍未接通(R1–R7 主卡承接);人物仍为参考静帧;本卡不宣称完整产品完成。",
|
||||
"阶段工程审核通过不等于白栖知体验验收;小样与正式界面替换边界不变。"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"独立复算:交付 zip SHA256、逐文件 before/after 哈希、清单外无差异、无删除;commit 0bc4a9e 逐字节等于作者交付(16 文件哈希复验全 OK)。",
|
||||
"机械执行记录:npm ci 176 包、typecheck 0 错、lint 0/2、vitest 26/26、build 514ms 产物 263.64 kB js + 81.28 kB css、pytest 142 passed 6.75s、四浏览器脚本 exit=0、双 base-ref 需求检查通过、git diff --check 通过。",
|
||||
"审核环境:uv 管理 Python 3.12.14 独立 venv(.venv-hel825,项目 requirements.lock + pytest/playwright),Chromium 156.0.8078.4,系统库复用既有 .browser-libs 用户态副本;未改全局环境、未动队员运行时/模型/认证。",
|
||||
"部署验证见本卡成果评论:8300/8310 前后可用、8320 服务健康、?demo=1 与普通真实入口实测、epoch 历史保留、回滚方案(切回旧 release current 符号链接)。"
|
||||
]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"task": "HEL-826 总工:核清 R2/R3 数据接缝与 R1 人格缺口,交可直接派施工的小范围接续说明(只读核对,不改产品源码)",
|
||||
"baseline_version": "1.10",
|
||||
"read_commit": "2d97050c7e86f909590ffdec3e8bc1aa5627cade",
|
||||
"read_notes": "读取提交为 origin/hel-825-review 头 2d97050(59项 v1.10)。实际阅读根 AGENTS.md、docs/requirements/README.md、current.md、coverage.md、changes.md、task-handoff-template.md,及 sources/integration-plan-v1.md、design-acceptance-v1.md、visual-confirmation-v2.md、dynamic-sample-effect-acceptance-20261023.md、comment-evidence.json 中 CG-013/014/015 原话(01a15763-1fc5、01a15763-e729、01a15765-2fa8、01a15771-3a93、01a15772-3101、01a15774-ddc6)与 UX-004 来源(01a162eb-1eab);另读本卡附件 HEL-816-接续方案.md 第4–6节。源码核对 src/cybergril/memory/schema.sql、src/cybergril/api/app.py、src/cybergril/core/bazi.py、src/cybergril/core/birth_view.py、src/cybergril/think/scenes.py、config/{character,models}.yaml、web/src/{adapters/real.ts,app/RealApp.tsx,life/LifePanel.tsx,manage/ManagePanel.tsx,domain/privacy.ts}。服务核查仅 8310/8320 GET /api/health(两者 capabilities 如实仅 text=true;8310 epoch 3、8320 epoch 4),未读档案与聊天正文,未做任何变更。",
|
||||
"requirement_ids": ["CG-007", "CG-008", "CG-012", "CG-013", "CG-014", "CG-015", "CG-016", "CG-018", "CG-021", "CG-023", "CG-031", "AM-001", "AM-002", "AM-003", "UX-004", "UX-006", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "DEL-001"],
|
||||
"deliverable": "一份接续说明(本卡附件 HEL-826-接续说明.md):R2 最小生活接入的可审核批次设计(服务端循环、事件来源/时区/幂等键、重启不补造、故障预算停止条件、列表/详情/摘要/搜索/关联回忆/聊天引用的响应前裁剪、told 提交时机与可执行反例),R1 人格深度在 R2/R3 的落点(八字原始事实只读每轮独立、模型诠释/初始习惯/后天成长三层分离、已有档案诚实补建、失败幂等、关系变更不重建人格),含数据表/迁移草案、端点与事件契约、前端绑定点、测试与隔离部署建议、下一张施工卡草稿及 R3/R4 依赖;另加本 handoff。产品源码零改动。",
|
||||
"acceptance": [
|
||||
"接续说明标明实际读取版本 v1.10 / 提交 2d97050、关联要求原意与原话来源、已有与缺失的实证(逐条 file:line)、下一批可观察验收项、仍待用户决定的问题。",
|
||||
"已交付事实与缺失事实分清:R1 真实档案/流式/近20轮在 8310(epoch 3)运行、A1/A2/A3 已过工程审核并部署 8320(epoch 4);生活循环零代码(life_event 表无生产者)、记忆仅测试路径、近20轮上下文不冒充 R3 长期记忆、六段固定模板即 chat 系统提示的底色来源(app.py:385-395、birth_view.py:41-80)、capabilities 前端未消费(real.ts 仅读 epoch/llm.ok)。",
|
||||
"told 机制定义完整:生成即 told=0;仅在 chat.final 完整送达并经引用判定后才置 1,取消/失败/部分流不置;provenance 记录(telling_log UNIQUE message);方案附可执行反例清单。",
|
||||
"施工卡草稿可独立执行:覆盖服务重启幂等、流式取消不误标、伪造ID/搜索/摘要/关联引用零泄漏、底色指纹不变、关系变更不重建人格、演示零API、真实聊天回归;压缩时钟测试与真实三天运行分开表述。",
|
||||
"python3 scripts/check_requirements.py --base-ref origin/main 与 --base-ref origin/agent/agent/de8874a3803b(均带本 handoff)通过;git diff --check 干净。"
|
||||
],
|
||||
"deferred": [
|
||||
"本卡不写产品代码、不部署、不生成真实生活事件、不调用模型生成人格、不做迁移;一切实现进下一张施工卡。",
|
||||
"R3 长期记忆、R4 主动联系、R5 语音、R6 天气/素材/备份、R7 视觉观察按 DEL-001 与 integration-plan-v1 依赖顺序由主卡承接;真实视频首尾衔接、口型由 HEL-802/803 承接。",
|
||||
"仍待用户决定(经总管):R2 真实三天运行的启用位置(推荐 8310,R2 已获批,带回滚);生活时区默认 Asia/Shanghai 至 R6 城市选择(工程默认,可后续随城市调整)。",
|
||||
"需求变更:无。"
|
||||
],
|
||||
"requirement_changes": [],
|
||||
"verification": [
|
||||
"只读源码核对(file:line 证据见接续说明第一节);8310/8320 health 实测 2026-10-24(本机 curl,capabilities 全部如实 false 除 text)。",
|
||||
"git 状态干净(本分支仅新增本 handoff 与接续说明不落仓库、以附件交付);未动 8300/8310/8320 任何数据与服务;未访问 4080;未合并任何 PR。",
|
||||
"结构检查不能代替原话核对:CG-013/014/015 与 UX-004 原话已逐条对照 comment-evidence.json;本卡不声称任何产品功能已通过用户验收。"
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,5 @@
|
||||
"integration-plan-v1.md": "454af1d4a3b0360d33aea9c0e97f738b320968ba7be68e012a5fcec6f6ed0169",
|
||||
"comment-evidence-visual-role-20261022.json": "ecf30eb323129dca2a0d9c2ff2e0163884ee94d3fc7828b0540cd45f03103797",
|
||||
"comment-evidence-shared-space-20261023.json": "e56299db726e64fbeea9e07d352066ef2b578889cc9a33cf1789e3234862130d",
|
||||
"shared-space-static-acceptance-20261023.md": "9aec04ab40e40ce275c75104f004cd3d1473ff8fbef428767afbbe5fafd09cb9",
|
||||
"comment-evidence-dynamic-sample-20261023.json": "fe7528a2b68aca99968734b200d9d27a17d85f5110e60c6f52d7c569755c260d",
|
||||
"dynamic-sample-effect-acceptance-20261023.md": "f5e9fde65a58ebc365de47569053ad7c30ce77ae82bb78773cc070e3cbffd90e"
|
||||
"shared-space-static-acceptance-20261023.md": "9aec04ab40e40ce275c75104f004cd3d1473ff8fbef428767afbbe5fafd09cb9"
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "01a16810-3b46-7345-8966-dd87b76efe07",
|
||||
"author_type": "member",
|
||||
"author_id": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
|
||||
"content": "这个效果可以",
|
||||
"created_at": "2026-10-23T09:20:56Z",
|
||||
"parent_id": null
|
||||
}
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
## 对坐动态小样整体效果的认可(2026-10-23)
|
||||
|
||||
总工(HEL-816)按主卡交付与用户原话整理入库。本记录只写明用户已经说过的话及其适用范围,不改动任何已有来源文件,不扩大授权。
|
||||
|
||||
### 用户原话(主卡 HEL-797,作者白栖知 `e5c6c938-db48-4ff9-954f-4fb8b063a132`)
|
||||
|
||||
原文逐字保存在同目录 `comment-evidence-dynamic-sample-20261023.json`。
|
||||
|
||||
| 评论 | 时间(UTC) | 原话 | 回应的问题 |
|
||||
| --- | --- | --- | --- |
|
||||
| `01a16810-3b46-7345-8966-dd87b76efe07` | 2026-10-23 09:20:56 | 这个效果可以 | 总管交付评论 `01a167c1-458d-795b-81e1-584089bf3667`(2026-10-23 07:54:41)请其下载体验《对坐动态小样 v2》并回答「整体是否更像自己坐在她面前」 |
|
||||
|
||||
交付评论(总管撰写,不属于用户原话)列明的体验方式:走近她/结束交流、月光/雾光切换、长回复自动续读、暂停回看与全文、手机打开查看;并已声明人物仍是参考静帧、聊天生活天气均为演示、真实视频语音口型尚未接入、手机候选默认关闭。
|
||||
|
||||
### 被认可的作品
|
||||
|
||||
- 文件:`CyberGril-HEL-799-对坐动态小样-v2.html`,662,233 字节,SHA256 `826bfc045d941c2114b16a198626ca952ff3d95dff02cfc6e32ee09858bcf80e`。
|
||||
- 附件:主卡附件 `01a167c1-455b-751a-ac37-9fa984a1ed10`(随交付评论 `01a167c1-458d-795b-81e1-584089bf3667`)。
|
||||
- 仓库内同 hash 原稿:`prototypes/hel-799/index.html`(分支 `agent/agent/54df81e610fd` 提交 `ad6a3b1` 起保全;总工 HEL-811 独立重建逐字节一致,总管 2026-10-23 复下载核对一致)。
|
||||
- 作者:视觉总监(HEL-799 v2 续作);独立审核:总工 HEL-811(40 项行为通过,无必须返修项,Firefox/真机未测原因见该记录)。
|
||||
|
||||
### 认可范围(已交小样的效果)
|
||||
|
||||
对坐动态小样 v2 的**整体效果**(电脑端、演示数据):同室相处画面;点她或「和她说话」推近到获认可的交流构图(HEL-798 月光构图);「结束交谈」沿原路退回,中途可反向;Esc 立即回到相处画面;交流呈现与阅读方式(自动续读、暂停、‹ › 回看、全文、完整记录、身份标记);月光 ⇄ 雾光两主题切换(布局与操作位置不变);日子/设置浮层。
|
||||
|
||||
### 不在范围内(仍待验收或承接)
|
||||
|
||||
| 项 | 状态 |
|
||||
| --- | --- |
|
||||
| 手机真机 | 未验证;4:5 候选容器 `?frame=media` 默认关闭,本认可不视为批准启用;真机键盘与流畅度未测 |
|
||||
| 真实人物动态 | 人物仍是参考静帧;真实视频、眨眼/呼吸/口型/语音未接入、未验证 |
|
||||
| 真实生活、记忆、主动联系、天气、备份 | 均为演示数据,未接入 |
|
||||
| Firefox/Safari/真机/系统级后台节流 | HEL-811 因环境未测,不能用等效视口或合成事件代替 |
|
||||
| 新素材 | 多时段天气穿搭、无书无居家裤的新近景与远景、首尾衔接素材按 HEL-802/HEL-803 承接,尚未生成 |
|
||||
|
||||
### 不能推出的结论
|
||||
|
||||
- 不视为完整产品、整套界面或任何其余页面(生活、回忆、创建、管理)验收通过。
|
||||
- 相处静帧中她在看书:用户已否定以阅读作为默认日常(`01a166aa-5f37-77b2-a2bd-ceb62f665813`),本认可不恢复看书默认,也不把该静帧定为最终相处素材。
|
||||
- 不授权替换正式界面:8300/8310 及已有档案、聊天、记忆保持原状;PR#4/#6/#8 合并与否仍由总管与白栖知决定。
|
||||
- 不代表 CG-024 的真实动态验证(真实视频选片、动作循环衔接、实时口型语音)完成;小样通过只表示该演示阶段通过。
|
||||
|
||||
### 对其他要求的影响
|
||||
|
||||
- CG-004「交流镜头」:推近/退回的切镜方式与衔接在小样演示层面(两静帧对齐+镜头推近)获认可;真实视频的切近衔接仍待验收。
|
||||
- VIS-007「空间动效」、VIS-013「交流呈现」、VIS-004「两套首轮主题」:小样是该要求获认可的演示呈现;条文与边界不变。
|
||||
- VIS-011:v1.9 已认可交流静图,本次扩展到该小样的整体动态效果(演示层面);条文保留,边界更新。
|
||||
- 其余已确认要求不变。
|
||||
|
||||
### 入库操作(总工 HEL-816,2026-10-23)
|
||||
|
||||
1. 新增本文件与 `comment-evidence-dynamic-sample-20261023.json`,`source-manifest.json` 追加两者 SHA256;历史来源未动。
|
||||
2. 升版 v1.10:`changes.json`/`changes.md` 追加条目(批准评论 `01a16810-3b46-7345-8966-dd87b76efe07`,`affected_ids`=VIS-011、CG-004);两者边界在 `baseline.json` 与 `current.md` 同步更新;`coverage.json`/`coverage.md` 版本与对应项同步;`README.md` 入口版本更新。
|
||||
3. 检查:`python3 scripts/check_requirements.py --base-ref origin/main` 与 `--handoff docs/requirements/handoff-HEL-816.json` 通过;`git diff --check` 干净。结果见各自运行记录。
|
||||
@@ -1,77 +0,0 @@
|
||||
HEL-783 六项返工交付与复审说明 · 2026-10-22
|
||||
|
||||
本轮由总管在可用的 Codex 环境亲自完成。沿用获批 HEL-746 R2 两主题和现有 R1 API,在原 PR#4 的 `agent/agent/de8874a3803b` 上追加修复,目标仍为 `main`。没有更换业务模型、修改现网或读写用户真实数据库,没有合并。最终提交号以 PR#4 最新 head 为准。
|
||||
|
||||
## 六项反馈闭环
|
||||
|
||||
| 用户反馈 / Before | 本轮 After | 实际验证 / Why |
|
||||
| --- | --- | --- |
|
||||
| 手机对话被输入栏挡住;台词和输入分别固定定位,没有长文高度预算 | `ChatPanel` 统一布局,人物占位、可滚动台词、状态/取消、语音条、输入分别占位;`Viewport` 跟随 visualViewport 的高度、顶部和底部偏移,含安全区。视口变化时人物窗立即调整,不等待装饰过渡 | 1440×900、360×640、390×700、390×844、390×420、844×390,两主题共12组;长中文前后文均在 DOM,可滚到末尾、完整记录能读全文。420高是键盘缩小等效场景,未冒称真机键盘实测 |
|
||||
| 流式全文在结束时被 `slice(-90)` 截成尾部;用户也被 `slice(-48)` 截断 | 当前双方消息保留全文;滚动阅读,跟随阅读位置;完整记录继续提供历史入口。流式和最终使用同一 turn 身份 | 长用户句首尾与五段长回复逐字相等;流式完成前后为同一个 DOM,取消立即中断,未完成文本标明未保存 |
|
||||
| 只写 opacity transition,没有真实改变透明度 | `SoftText` 按轮次用 WAAPI 淡入200ms、旧句保留160ms淡出;流式增量只更新文字,不重播。使用现有 `--ease-out`;减少动效120ms轻淡化 | 实际动画0/80/200ms逐帧截图及 computed opacity/transform;旧句0/80/159ms仍挂载、透明度递减。全轮只有一次入场,完成不重播,取消不等动画 |
|
||||
| 滚动条脱离主题 | 两套主题各有轨道/滑块/悬停 token;全局标准属性与 Chromium 兼容样式,强制色使用系统色;触摸区仍原生滚动 | 实际消息、记录、首建、设置长内容滚动及两主题截图;强制色有 CSS 回退。没有做真实辅助设备和各系统滚动条外观全覆盖测试 |
|
||||
| 底色只有公历和几个内部短标签 | 新增只读日历展示与六维解释映射:表达社交、亲密边界、冲突修复、行动节律、压力恢复、成长张力。显示真实农历/闰月、钟点、四柱和术语提示;每段可展开解释依据。预览/确认/已有档案共用组件 | 48组日期×时辰检查,含1900边界、春节、立春附近、真实闰四月;预览不写库。旧档案使用已存四柱、十神、Derived,禁止重新编译;GET前后原始JSON、底色行一致,关系修改不改底色和历史;聊天系统提示逐段复用同一解释 |
|
||||
| 分不清谁在说话 | 当前对话中“你”右对齐,真实伴侣名字左对齐,独立角色行;阅读状态行持续标明伴侣名字,滚动后也能确认正在读谁的回复。流式、最终、失败、取消与记录都使用同一身份 | 两主题截图,失败/重试、取消新轮、历史恢复都检查;不单靠颜色辨别 |
|
||||
| 真实入口把完整页面缩成“未接通”卡片 | 恢复日子/回忆双栏与分组;城市、外观、备份、四类服务的完整结构和真实空态;服务页可读健康检查,未接通写入控件明确禁用;加入未保存离开确认 | 实际手机逐页进入、返回、搜索空态、服务检查、未保存取消/放弃、演示事件→聊天→返回。真实入口无演示记录,演示模式全流程零API请求 |
|
||||
|
||||
## R2 功能覆盖差异表
|
||||
|
||||
“已接真”表示已有 API 持续可用;“本批补齐”表示前端结构/交互已恢复;“后台待接”表示本轮没有增加该后台能力,不能据此验收为已接通。
|
||||
|
||||
| R2 落点 | 演示模式 | 真实入口 | 本批结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 四类区域、两主题、底部导航 | 原有此刻/交谈/日子/设置 | 同样四区与主题切换 | 保留;手机短高度首页内容可滚动 |
|
||||
| 首页留言与日子入口 | 固定演示留言、已分享日子入口 | 服务器最近回复可打开完整记录;日子入口明确后台待接 | 本批补齐真实入口,历史已接真;不伪称主动留言 |
|
||||
| 日子列表、按日分组、三类事件、分页 | 原有已分享事件与18+12分页 | 完整列表区、三类图例和空态 | 本批补齐结构;真实生活后台待接,无示例填充 |
|
||||
| 事件详情、回忆互链、聊天与返回 | 可走详情→关联→聊天→回到原日子 | 详情阅读区域保留,无数据时不造条目/链接 | 演示路径实测;真实数据后台待接 |
|
||||
| 我们一起的/小习惯/她知道的你/成长 | 四组演示回忆、来源、关联日子 | 四组独立空态和阅读区 | 本批补齐真实分组;长期记忆后台待接 |
|
||||
| 隐私:她说过才显示 | 继续使用 publicView 过滤 | 当前没有生活/回忆数据 | 不放宽隐私,不展示未讲述事件 |
|
||||
| 首建四步、关系无预选、同意确认 | 本机独立创建;排盘不以固定假值冒充 | 服务端唯一档案、只读预览、实际创建 | 已接真;补公农历/闰月/四柱帮助和六维说明 |
|
||||
| 首建失败、返回修改、重试 | 原有模拟失败且保留输入 | API 中文错误、输入保留、409恢复唯一档案 | 保留;未增建档入口或清空入口 |
|
||||
| 她的档案与底色锁定 | 演示底色明确未计算 | 旧档案只读补展示,原参数和历史不动 | 本批补齐;无数据库迁移 |
|
||||
| 最初与后来成长分层 | 两栏及成长回忆 | 出生说明与后续记忆区分,当前长期记忆未接 | 不把出生说明当作已经发生的经历 |
|
||||
| 关系、称呼、边界;改变关系确认 | 原有模拟保存/取消/失败 | PATCH真实保存、二次确认、重开恢复 | 已接真,回归通过 |
|
||||
| 未保存提示、取消和离开 | 原有暖色点、取消、beforeunload | 同样提示;关闭/导航离开新增确认 | 本批补齐;取消继续编辑,放弃不写服务器 |
|
||||
| 城市搜索与列表/无结果 | 原有候选城市本机保存与失败演示 | 可搜索候选目录,有空态;明确未设置真实城市,保存禁用 | 本批补齐;真实城市/天气后台待接 |
|
||||
| 外观选择、五项检查、对比、确认 | JSON结构检查、对比与模拟标识确认 | 当前参考、文件入口、五项待检查、新旧对比、应用入口 | 本批补齐;真实导入与媒体检查禁用,后台待接 |
|
||||
| 自动备份列表、完整导出 | 已标真实备份未接通;本机演示JSON可导出 | 自动备份空列表、完整导出说明和禁用按钮 | 本批补齐;不制造成功备份 |
|
||||
| 恢复检查、同一档案、双重确认、先导出 | 原有本机恢复检查和输入“恢复” | 明示四步恢复过程;文件和执行入口禁用 | 本批补齐结构;真实恢复后台待接,不能实际覆盖 |
|
||||
| 四类外接服务与测试反馈 | 只保存演示地址,不发请求 | 文字、语音识别、语音合成、视觉四项;文字可重新检查健康,配置只读 | 本批补齐;不暴露凭据,不模拟其他连接成功 |
|
||||
| 显示、减少动效 | 原有本机设置 | 主题与减少动效保存于本浏览器,和服务器资料分开 | 减少动效刷新恢复;系统设置也生效 |
|
||||
| 长文字、完整记录、滚动、角色 | 模拟流式继续可用 | SSE、完整记录、失败与取消状态 | 本批修复全文、占位和角色;已接真 |
|
||||
| 重试、轮次/epoch、重启、多设备恢复 | 原有 TurnGate | 原有传输机制保留 | 确定性API测试和浏览器回归通过 |
|
||||
| 连续语音、打断、结束、拒绝/失败状态 | 保留无收音模拟和权限说明;新布局语音条不挡输入 | 控件明确尚未接通,文字仍可用 | 真实语音后台待接,未申请设备权限 |
|
||||
| 视觉说明、时效、失败、总服务区 | 原有30秒模拟过期与失败 | 明确未接通、不采集;服务项保留 | 真实视觉后台待接 |
|
||||
| 断线/模型不可达/草稿 | 原有断线重连演示 | 实际请求错误、草稿保留、重试、服务状态;已有档案历史可读 | 保留已接真能力;自动重连推送属后续,不假造补消息 |
|
||||
| 离开欢迎摘要、主动消息与未读 | 原有明确标注的固定演示,来源为已分享事件 | 生活/主动消息未接通,不伪造欢迎摘要或来信 | 后台待接;真实最近回复入口已保留 |
|
||||
| 手机列表/详情与设置返回、Esc | 保留推入和返回、原日子位置 | 同样区域/分区返回;Esc关闭记录再退出交流 | 本批补齐真实Esc与未保存确认 |
|
||||
| 新设计建议 | 无 | 无 | 未把新增后台能力夹进本批 |
|
||||
|
||||
## 验证记录
|
||||
|
||||
- Python 3.11:`pytest -q` **142通过**,含原92项和新增50项。唯一提示为既有 Starlette TestClient 依赖弃用提示。
|
||||
- TypeScript类型检查、ESLint、前端 **14项** Vitest、Vite生产构建通过。后端变更文件 Ruff通过。没有新增运行时依赖。
|
||||
- `web/tests/rework_browser_check.py`:8组通过,12组尺寸/主题组合;记录 `browser-results.json`。使用隔离SQLite和可控模型验证长中文、错误、取消与完整页面,不把可控模型说成在线模型。
|
||||
- `web/tests/real_browser_check.py --controlled`:8组回归通过,记录 `r1-regression/browser-results.json`。包括真实HTTP SSE、浏览器关闭流、立即新轮、同源重启epoch变化、多设备恢复、关系取消/确认保存。
|
||||
- `web/tests/real_browser_check.py`:真实批准网关完成两轮对话,第二轮正确回答上一轮暗号“蓝色风铃”;第三轮收到增量后取消,上游请求结束且没有完整回复落库。随后同源重启和故障段使用可控provider,结果逐项区分,记录 `live-gateway/browser-results.json`。所有建档、聊天和关系写入仍只在临时库。
|
||||
- 动效证据:`motion-enter-0/80/200.png` 和结果JSON中的 `motion.entry_frames/exit_frames`。截图暂停实际 WAAPI 后逐帧采样,随后恢复;没有为截图修改生产动画时长。
|
||||
- 代表图:`long-start-360-640-*.png`、`long-start/long-end-390-420-*.png`、`full-history-*.png`、`birth-*.png`、`existing-*.png`、城市/外观/备份/服务/生活回忆各图。图中“星河/小满”和聊天全部是本轮隔离测试资料。
|
||||
- 已实际看过代表性手机长文、短视口、语音模拟、出生预览、已有档案说明、服务页、回忆分组截图;复审仍需独立看图、播放实际界面,不能只用坐标和测试数认定视觉通过。
|
||||
- 未测:实体手机键盘、Safari、真实强制色设备、屏幕阅读器、长时运行和帧率。420/390高只证明等效可见高度布局,不等于真机键盘实测。
|
||||
|
||||
## 说明层的兼容性与回滚
|
||||
|
||||
`core/birth_view.py` 是可维护的确定性解释映射,不调用模型润色。分档值只选择解释段落,不产生新的底色参数。敏感度的展示分档与普通0–1参数分别归一到解释选择范围。四柱与十神取旧库已存值,五行侧重和性格取旧Derived;日历只从不可变生日、时辰换算展示。没有重新调用旧档案的四柱编译器,也没有把新说明写回旧档案。新增档案会在原有JSON中携带可选展示字段;旧字段仍保留,旧前端不依赖新增字段。
|
||||
|
||||
聊天提示与UI共用六段解释,仍明确本体、现实边界、未接通能力和不能编造生活。取消、历史、epoch、路由和业务模型配置未改。服务端关系修改只保存关系字段及原档案JSON,不把本次只读展示偷偷写回旧档案。
|
||||
|
||||
本批无schema改动、不需要重建库。回滚只切回上一个程序版本;旧版忽略JSON新增字段。服务器切换前按HEL-784要求备份,不能删除、替换或重算用户档案。浏览器新加 `cg.real.display` 仅保存减少动效偏好;不存真实聊天或底色。
|
||||
|
||||
## 给HEL-784的部署交接
|
||||
|
||||
1. 重新读取PR#4最新head,与本轮交付SHA一致后审核;base仍main。远端若另有推进,先协调,不覆盖。
|
||||
2. 独立运行检查,实际看截图及流式/取消/渐入渐出,特别检查用户原截图对应的手机短视口和长回复。
|
||||
3. 安装方式沿用 `docs/r1/IMPLEMENTATION.md`:Python≥3.11、`requirements.lock`、安装当前包,`npm ci --prefix web` 和 `npm run build --prefix web`。浏览器测试可选Playwright,不是部署依赖。
|
||||
4. 审核通过才部署8310的新release,保留现有用户库,切换前备份;不要把本轮临时测试库、测试人物或演示存储带上服务器。8300和其他服务不动。
|
||||
5. 回读现有档案/历史与health,检查新静态资源和旧档案补充说明;附件不要泄露真实聊天全文。记录部署SHA、备份位置和回滚指令。
|
||||
6. 本批用户尚未验收,不合并、不归档、不推进到收尾。
|
||||
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
@@ -1,760 +0,0 @@
|
||||
{
|
||||
"checks": [
|
||||
"Real creation: lunar leap month, all six explanations, same final confirmation; preview writes no rows",
|
||||
"Long Chinese stream: shown lines never change or re-key, ink plays once per line; pause/keyboard review is instant; record keeps everything",
|
||||
"12 viewport/theme combinations: lines stay inside the stage, stage/reader/composer/navigation never overlap; Esc closes full text with focus back; full history reachable",
|
||||
"390x420 and 390x430, both themes: with the input focused, the first press on send, review, pause/continue and full text acts; the dialogue does not move between press and release",
|
||||
"Cancel then immediate new turn, model error and same-turn retry preserve roles and avoid duplicate user messages",
|
||||
"Stop then new turn with busy (700 ms) frames: your line keeps its own time and her reply follows; the reading clock counts real time",
|
||||
"Restored real pages: city search/empty, appearance/check/compare, backup/recovery, four services/live health, life/memory groups; unsaved close guard",
|
||||
"Escape navigation; browser display preference survives reload; settings/system reduced motion uses gentle opacity",
|
||||
"Demo has its own profile, voice simulation, life/detail/chat/return path and zero API requests"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "moon",
|
||||
"stage": {
|
||||
"top": 605.609375,
|
||||
"bottom": 733.609375,
|
||||
"left": 250,
|
||||
"right": 1190,
|
||||
"height": 128
|
||||
},
|
||||
"reader": {
|
||||
"top": 762,
|
||||
"bottom": 792,
|
||||
"left": 596.484375,
|
||||
"right": 843.515625,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 798,
|
||||
"bottom": 848,
|
||||
"left": 400,
|
||||
"right": 1040,
|
||||
"height": 50
|
||||
},
|
||||
"nav": {
|
||||
"top": 860,
|
||||
"bottom": 894,
|
||||
"left": 582,
|
||||
"right": 858,
|
||||
"height": 34
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 643.21875,
|
||||
"bottom": 665.71875,
|
||||
"left": 596.625,
|
||||
"right": 887.328125,
|
||||
"height": 22.5
|
||||
},
|
||||
{
|
||||
"top": 699.109375,
|
||||
"bottom": 733.609375,
|
||||
"left": 555.765625,
|
||||
"right": 884.21875,
|
||||
"height": 34.5
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "fog",
|
||||
"stage": {
|
||||
"top": 605.609375,
|
||||
"bottom": 733.609375,
|
||||
"left": 250,
|
||||
"right": 1190,
|
||||
"height": 128
|
||||
},
|
||||
"reader": {
|
||||
"top": 762,
|
||||
"bottom": 792,
|
||||
"left": 596.484375,
|
||||
"right": 843.515625,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 798,
|
||||
"bottom": 848,
|
||||
"left": 400,
|
||||
"right": 1040,
|
||||
"height": 50
|
||||
},
|
||||
"nav": {
|
||||
"top": 860,
|
||||
"bottom": 894,
|
||||
"left": 582,
|
||||
"right": 858,
|
||||
"height": 34
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 643.21875,
|
||||
"bottom": 665.71875,
|
||||
"left": 596.625,
|
||||
"right": 887.328125,
|
||||
"height": 22.5
|
||||
},
|
||||
{
|
||||
"top": 699.109375,
|
||||
"bottom": 733.609375,
|
||||
"left": 555.765625,
|
||||
"right": 884.21875,
|
||||
"height": 34.5
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 360,
|
||||
"height": 640,
|
||||
"theme": "moon",
|
||||
"stage": {
|
||||
"top": 362,
|
||||
"bottom": 494,
|
||||
"left": 10,
|
||||
"right": 350,
|
||||
"height": 132
|
||||
},
|
||||
"reader": {
|
||||
"top": 496,
|
||||
"bottom": 526,
|
||||
"left": 72.5,
|
||||
"right": 287.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 532,
|
||||
"bottom": 580,
|
||||
"left": 10,
|
||||
"right": 350,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 586,
|
||||
"bottom": 634,
|
||||
"left": 30,
|
||||
"right": 330,
|
||||
"height": 48
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 412.453125,
|
||||
"bottom": 432.59375,
|
||||
"left": 155.5625,
|
||||
"right": 248.390625,
|
||||
"height": 20.140625
|
||||
},
|
||||
{
|
||||
"top": 466.109375,
|
||||
"bottom": 494,
|
||||
"left": 51.46875,
|
||||
"right": 308.515625,
|
||||
"height": 27.890625
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 360,
|
||||
"height": 640,
|
||||
"theme": "fog",
|
||||
"stage": {
|
||||
"top": 362,
|
||||
"bottom": 494,
|
||||
"left": 10,
|
||||
"right": 350,
|
||||
"height": 132
|
||||
},
|
||||
"reader": {
|
||||
"top": 496,
|
||||
"bottom": 526,
|
||||
"left": 72.5,
|
||||
"right": 287.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 532,
|
||||
"bottom": 580,
|
||||
"left": 10,
|
||||
"right": 350,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 586,
|
||||
"bottom": 634,
|
||||
"left": 30,
|
||||
"right": 330,
|
||||
"height": 48
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 412.453125,
|
||||
"bottom": 432.59375,
|
||||
"left": 155.5625,
|
||||
"right": 248.390625,
|
||||
"height": 20.140625
|
||||
},
|
||||
{
|
||||
"top": 466.109375,
|
||||
"bottom": 494,
|
||||
"left": 51.46875,
|
||||
"right": 308.515625,
|
||||
"height": 27.890625
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 700,
|
||||
"theme": "moon",
|
||||
"stage": {
|
||||
"top": 422,
|
||||
"bottom": 554,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 132
|
||||
},
|
||||
"reader": {
|
||||
"top": 556,
|
||||
"bottom": 586,
|
||||
"left": 87.5,
|
||||
"right": 302.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 592,
|
||||
"bottom": 640,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 646,
|
||||
"bottom": 694,
|
||||
"left": 45,
|
||||
"right": 345,
|
||||
"height": 48
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 472.453125,
|
||||
"bottom": 492.59375,
|
||||
"left": 91,
|
||||
"right": 342.953125,
|
||||
"height": 20.140625
|
||||
},
|
||||
{
|
||||
"top": 526.109375,
|
||||
"bottom": 554,
|
||||
"left": 66.46875,
|
||||
"right": 323.515625,
|
||||
"height": 27.890625
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 700,
|
||||
"theme": "fog",
|
||||
"stage": {
|
||||
"top": 422,
|
||||
"bottom": 554,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 132
|
||||
},
|
||||
"reader": {
|
||||
"top": 556,
|
||||
"bottom": 586,
|
||||
"left": 87.5,
|
||||
"right": 302.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 592,
|
||||
"bottom": 640,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 646,
|
||||
"bottom": 694,
|
||||
"left": 45,
|
||||
"right": 345,
|
||||
"height": 48
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 472.453125,
|
||||
"bottom": 492.59375,
|
||||
"left": 91,
|
||||
"right": 342.953125,
|
||||
"height": 20.140625
|
||||
},
|
||||
{
|
||||
"top": 526.109375,
|
||||
"bottom": 554,
|
||||
"left": 66.46875,
|
||||
"right": 323.515625,
|
||||
"height": 27.890625
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"theme": "moon",
|
||||
"stage": {
|
||||
"top": 566,
|
||||
"bottom": 698,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 132
|
||||
},
|
||||
"reader": {
|
||||
"top": 700,
|
||||
"bottom": 730,
|
||||
"left": 87.5,
|
||||
"right": 302.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 736,
|
||||
"bottom": 784,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 790,
|
||||
"bottom": 838,
|
||||
"left": 45,
|
||||
"right": 345,
|
||||
"height": 48
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 616.453125,
|
||||
"bottom": 636.59375,
|
||||
"left": 91,
|
||||
"right": 342.953125,
|
||||
"height": 20.140625
|
||||
},
|
||||
{
|
||||
"top": 670.109375,
|
||||
"bottom": 698,
|
||||
"left": 66.46875,
|
||||
"right": 323.515625,
|
||||
"height": 27.890625
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"theme": "fog",
|
||||
"stage": {
|
||||
"top": 566,
|
||||
"bottom": 698,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 132
|
||||
},
|
||||
"reader": {
|
||||
"top": 700,
|
||||
"bottom": 730,
|
||||
"left": 87.5,
|
||||
"right": 302.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 736,
|
||||
"bottom": 784,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 790,
|
||||
"bottom": 838,
|
||||
"left": 45,
|
||||
"right": 345,
|
||||
"height": 48
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 616.453125,
|
||||
"bottom": 636.59375,
|
||||
"left": 91,
|
||||
"right": 342.953125,
|
||||
"height": 20.140625
|
||||
},
|
||||
{
|
||||
"top": 670.109375,
|
||||
"bottom": 698,
|
||||
"left": 66.46875,
|
||||
"right": 323.515625,
|
||||
"height": 27.890625
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 420,
|
||||
"theme": "moon",
|
||||
"stage": {
|
||||
"top": 206,
|
||||
"bottom": 284,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 78
|
||||
},
|
||||
"reader": {
|
||||
"top": 284,
|
||||
"bottom": 314,
|
||||
"left": 87.5,
|
||||
"right": 302.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 318,
|
||||
"bottom": 366,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 436,
|
||||
"bottom": 480,
|
||||
"left": 45,
|
||||
"right": 345,
|
||||
"height": 44
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 259.203125,
|
||||
"bottom": 284,
|
||||
"left": 80.75,
|
||||
"right": 309.234375,
|
||||
"height": 24.796875
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 420,
|
||||
"theme": "fog",
|
||||
"stage": {
|
||||
"top": 206,
|
||||
"bottom": 284,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 78
|
||||
},
|
||||
"reader": {
|
||||
"top": 284,
|
||||
"bottom": 314,
|
||||
"left": 87.5,
|
||||
"right": 302.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 318,
|
||||
"bottom": 366,
|
||||
"left": 10,
|
||||
"right": 380,
|
||||
"height": 48
|
||||
},
|
||||
"nav": {
|
||||
"top": 436,
|
||||
"bottom": 480,
|
||||
"left": 45,
|
||||
"right": 345,
|
||||
"height": 44
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 259.203125,
|
||||
"bottom": 284,
|
||||
"left": 80.75,
|
||||
"right": 309.234375,
|
||||
"height": 24.796875
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 844,
|
||||
"height": 390,
|
||||
"theme": "moon",
|
||||
"stage": {
|
||||
"top": 174,
|
||||
"bottom": 252,
|
||||
"left": 50.640625,
|
||||
"right": 793.359375,
|
||||
"height": 78
|
||||
},
|
||||
"reader": {
|
||||
"top": 252,
|
||||
"bottom": 282,
|
||||
"left": 359.484375,
|
||||
"right": 484.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 286,
|
||||
"bottom": 336,
|
||||
"left": 102,
|
||||
"right": 742,
|
||||
"height": 50
|
||||
},
|
||||
"nav": {
|
||||
"top": 406,
|
||||
"bottom": 450,
|
||||
"left": 284,
|
||||
"right": 560,
|
||||
"height": 44
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 227.203125,
|
||||
"bottom": 252,
|
||||
"left": 307.75,
|
||||
"right": 536.234375,
|
||||
"height": 24.796875
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 844,
|
||||
"height": 390,
|
||||
"theme": "fog",
|
||||
"stage": {
|
||||
"top": 174,
|
||||
"bottom": 252,
|
||||
"left": 50.640625,
|
||||
"right": 793.359375,
|
||||
"height": 78
|
||||
},
|
||||
"reader": {
|
||||
"top": 252,
|
||||
"bottom": 282,
|
||||
"left": 359.484375,
|
||||
"right": 484.5,
|
||||
"height": 30
|
||||
},
|
||||
"composer": {
|
||||
"top": 286,
|
||||
"bottom": 336,
|
||||
"left": 102,
|
||||
"right": 742,
|
||||
"height": 50
|
||||
},
|
||||
"nav": {
|
||||
"top": 406,
|
||||
"bottom": 450,
|
||||
"left": 284,
|
||||
"right": 560,
|
||||
"height": 44
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"top": 227.203125,
|
||||
"bottom": 252,
|
||||
"left": 307.75,
|
||||
"right": 536.234375,
|
||||
"height": 24.796875
|
||||
}
|
||||
],
|
||||
"overflow": false
|
||||
}
|
||||
],
|
||||
"motion": {
|
||||
"inked_lines": {
|
||||
"6fc4f8ff-218a-4c0a-933c-7982fef596b6:1:0": 1
|
||||
},
|
||||
"slow_frames_new_turn_to_reply_s": 2.31
|
||||
},
|
||||
"console_errors": [],
|
||||
"limits": [
|
||||
"Chromium only; physical phone keyboard, Safari and screen reader not tested.",
|
||||
"LLM is controlled for deterministic long-stream/error/cancel checks, not a live gateway."
|
||||
],
|
||||
"compact_first_press": [
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "moon",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "发送"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "moon",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "上一句"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "moon",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "暂停"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "moon",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "继续"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "moon",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "全文"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "fog",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "发送"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "fog",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "上一句"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "fog",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "暂停"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "fog",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "继续"
|
||||
},
|
||||
{
|
||||
"size": "390x420",
|
||||
"theme": "fog",
|
||||
"down": 182,
|
||||
"up": 182,
|
||||
"typing": false,
|
||||
"target": "全文"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "moon",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "发送"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "moon",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "上一句"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "moon",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "暂停"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "moon",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "继续"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "moon",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "全文"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "fog",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "发送"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "fog",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "上一句"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "fog",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "暂停"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "fog",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "继续"
|
||||
},
|
||||
{
|
||||
"size": "390x430",
|
||||
"theme": "fog",
|
||||
"down": 192,
|
||||
"up": 192,
|
||||
"typing": false,
|
||||
"target": "全文"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 276 KiB |
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 974 KiB |
|
Before Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 1017 KiB |
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
|
Before Width: | Height: | Size: 136 KiB |
@@ -16,10 +16,6 @@ dependencies = [
|
||||
"httpx>=0.27",
|
||||
"pyyaml>=6.0",
|
||||
"pydantic>=2.7",
|
||||
"fastapi>=0.115,<1",
|
||||
"anyio>=4,<5",
|
||||
"uvicorn>=0.30,<1",
|
||||
"lunar-python>=1.4,<2",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --python .venv311/bin/python pyproject.toml -o requirements.lock
|
||||
annotated-doc==0.0.5
|
||||
# via fastapi
|
||||
annotated-types==0.8.0
|
||||
# via pydantic
|
||||
anyio==4.15.1
|
||||
# via
|
||||
# cybergril (pyproject.toml)
|
||||
# httpx
|
||||
# starlette
|
||||
certifi==2026.10.19
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
click==8.5.0
|
||||
# via uvicorn
|
||||
fastapi==0.159.1
|
||||
# via cybergril (pyproject.toml)
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# uvicorn
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via cybergril (pyproject.toml)
|
||||
idna==3.20
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
lunar-python==1.4.8
|
||||
# via cybergril (pyproject.toml)
|
||||
opentelemetry-api==1.45.1
|
||||
# via fastapi
|
||||
pydantic==2.14.1
|
||||
# via
|
||||
# cybergril (pyproject.toml)
|
||||
# fastapi
|
||||
pydantic-core==2.50.1
|
||||
# via pydantic
|
||||
pyyaml==6.0.3
|
||||
# via cybergril (pyproject.toml)
|
||||
starlette==1.7.0
|
||||
# via fastapi
|
||||
typing-extensions==4.16.0
|
||||
# via
|
||||
# anyio
|
||||
# fastapi
|
||||
# opentelemetry-api
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# starlette
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.4
|
||||
# via
|
||||
# fastapi
|
||||
# pydantic
|
||||
uvicorn==0.54.0
|
||||
# via cybergril (pyproject.toml)
|
||||
@@ -1 +0,0 @@
|
||||
"""R1 same-origin persistent text service."""
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Run with python -m cybergril.api (one worker, no reload)."""
|
||||
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"cybergril.api.app:create_app",
|
||||
factory=True,
|
||||
host=os.environ.get("CYBERGRIL_HOST", "127.0.0.1"),
|
||||
port=int(os.environ.get("CYBERGRIL_PORT", "8310")),
|
||||
workers=1,
|
||||
)
|
||||
@@ -1,518 +0,0 @@
|
||||
"""Single-process R1 API. Store is authoritative; no demo state enters this module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from dataclasses import asdict
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import yaml
|
||||
from anyio import CancelScope
|
||||
from fastapi import FastAPI, HTTPException, Query, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from ..config import ModelsConfig
|
||||
from ..contracts import LLMMessage
|
||||
from ..core.bazi import (
|
||||
GAN_ELEM,
|
||||
Derived,
|
||||
Pillars,
|
||||
compile_pillars,
|
||||
element_scores,
|
||||
pillars_from_lunar,
|
||||
ten_god_distribution,
|
||||
)
|
||||
from ..core.birth_view import calendar_view, color_view
|
||||
from ..memory.store import Store
|
||||
from ..think.scenes import Scene
|
||||
from ..think.wiring import build_router_from_config
|
||||
|
||||
HOURS = [
|
||||
"子时",
|
||||
"丑时",
|
||||
"寅时",
|
||||
"卯时",
|
||||
"辰时",
|
||||
"巳时",
|
||||
"午时",
|
||||
"未时",
|
||||
"申时",
|
||||
"酉时",
|
||||
"戌时",
|
||||
"亥时",
|
||||
]
|
||||
ERROR = "她这会儿没接上话,稍后再试试。你写的话还在。"
|
||||
|
||||
|
||||
def safe_text(value: str, maximum: int, label: str, required: bool = False) -> str:
|
||||
value = value.strip()
|
||||
if (
|
||||
(required and not value)
|
||||
or len(value) > maximum
|
||||
or any(ord(c) < 32 or ord(c) == 127 or c in "<>" for c in value)
|
||||
):
|
||||
raise ValueError(f"{label}请填写{1 if required else 0}–{maximum}字,不含控制字符或尖括号。")
|
||||
return value
|
||||
|
||||
|
||||
class StrictBody(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", strict=True)
|
||||
|
||||
|
||||
class BirthInput(StrictBody):
|
||||
name: str
|
||||
birth: str
|
||||
hour: str
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_valid(cls, value: str) -> str:
|
||||
return safe_text(value, 20, "名字", True)
|
||||
|
||||
@field_validator("birth")
|
||||
@classmethod
|
||||
def birth_valid(cls, value: str) -> str:
|
||||
try:
|
||||
born = date.fromisoformat(value)
|
||||
today = datetime.now(ZoneInfo("Asia/Shanghai")).date()
|
||||
age = today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
||||
if born.isoformat() != value or born.year < 1900 or age < 18:
|
||||
raise ValueError
|
||||
except ValueError as exc:
|
||||
raise ValueError("请填写有效的出生日期,且已满18岁(不早于1900年)。") from exc
|
||||
return value
|
||||
|
||||
@field_validator("hour")
|
||||
@classmethod
|
||||
def hour_valid(cls, value: str) -> str:
|
||||
if value not in HOURS:
|
||||
raise ValueError("请选择有效的出生时辰。")
|
||||
return value
|
||||
|
||||
|
||||
class RelationInput(StrictBody):
|
||||
relation: Literal["朋友", "恋人"]
|
||||
nickname: str = ""
|
||||
boundary: str = "尊重彼此的节奏"
|
||||
|
||||
@field_validator("nickname")
|
||||
@classmethod
|
||||
def nickname_valid(cls, value: str) -> str:
|
||||
return safe_text(value, 40, "称呼")
|
||||
|
||||
@field_validator("boundary")
|
||||
@classmethod
|
||||
def boundary_valid(cls, value: str) -> str:
|
||||
return safe_text(value, 2000, "相处边界")
|
||||
|
||||
|
||||
class CreationInput(BirthInput, RelationInput):
|
||||
pass
|
||||
|
||||
|
||||
class ChatInput(StrictBody):
|
||||
text: str
|
||||
turn_id: str
|
||||
epoch: int
|
||||
|
||||
@field_validator("text")
|
||||
@classmethod
|
||||
def text_valid(cls, value: str) -> str:
|
||||
if not value.strip() or len(value) > 4000:
|
||||
raise ValueError("请输入1–4000字的消息。")
|
||||
return value.strip()
|
||||
|
||||
@field_validator("turn_id")
|
||||
@classmethod
|
||||
def turn_valid(cls, value: str) -> str:
|
||||
try:
|
||||
UUID(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError("轮次标识无效,请刷新后重试。") from exc
|
||||
return value
|
||||
|
||||
|
||||
def compile_birth(data: BirthInput) -> tuple[dict, object, object]:
|
||||
born = date.fromisoformat(data.birth)
|
||||
pillars = pillars_from_lunar(born.year, born.month, born.day, HOURS.index(data.hour) * 2)
|
||||
derived = compile_pillars(pillars)
|
||||
return color_view(data.birth, data.hour, pillars, derived), pillars, derived
|
||||
|
||||
|
||||
def create_app(
|
||||
*,
|
||||
db_path: Path | str | None = None,
|
||||
models: ModelsConfig | None = None,
|
||||
router=None,
|
||||
web_dir: Path | None = None,
|
||||
stream_timeout: float = 60,
|
||||
) -> FastAPI:
|
||||
"""Dependencies can be injected for isolated fault/cancellation tests, never via browser flags."""
|
||||
db_path = db_path or os.environ.get(
|
||||
"CYBERGRIL_DB", str(Path.home() / ".local/share/cybergril/companion.db")
|
||||
)
|
||||
if models is None:
|
||||
config = Path(os.environ.get("CYBERGRIL_MODELS", "config/models.yaml"))
|
||||
models = ModelsConfig(**yaml.safe_load(config.read_text()))
|
||||
router = router or build_router_from_config(models)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
store = Store(db_path)
|
||||
app.state.store = store
|
||||
with store.conn:
|
||||
store.execute(
|
||||
"INSERT INTO api_meta(key,value) VALUES('epoch',1) ON CONFLICT(key) DO UPDATE SET value=value+1"
|
||||
)
|
||||
store.execute("UPDATE chat_message SET status='cancelled' WHERE status='pending'")
|
||||
app.state.epoch = store.query_one("SELECT value FROM api_meta WHERE key='epoch'")["value"]
|
||||
app.state.active = None
|
||||
app.state.chat_lock = asyncio.Lock()
|
||||
app.state.health_cache = (0, False)
|
||||
app.state.health_lock = asyncio.Lock()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if app.state.active:
|
||||
app.state.active["cancel"].set()
|
||||
await app.state.active["finished"].wait()
|
||||
for provider in set(router.provider_for(t) for t in router.available_tiers()):
|
||||
await provider.aclose()
|
||||
store.close()
|
||||
|
||||
app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def invalid(_request, exc):
|
||||
fields = {}
|
||||
for e in exc.errors():
|
||||
key = str(e["loc"][-1])
|
||||
msg = e["msg"].removeprefix("Value error, ")
|
||||
if e["type"] == "missing":
|
||||
msg = "请填写此项。" if key != "relation" else "请亲自选择你们的关系。"
|
||||
elif e["type"] == "literal_error":
|
||||
msg = "请亲自选择朋友或恋人。"
|
||||
elif e["type"] == "extra_forbidden":
|
||||
msg = "此字段不允许修改。"
|
||||
elif not any("\u4e00" <= c <= "\u9fff" for c in msg):
|
||||
msg = "字段格式无效,请检查后重试。"
|
||||
fields[key] = msg
|
||||
return JSONResponse({"detail": "请检查填写内容。", "fields": fields}, status_code=422)
|
||||
|
||||
def companion(enrich=True):
|
||||
row = app.state.store.query_one("SELECT identity_json FROM companion WHERE id=1")
|
||||
if not row:
|
||||
raise HTTPException(404, "还没有创建真实伴侣。")
|
||||
identity = json.loads(row["identity_json"])
|
||||
if enrich:
|
||||
saved = app.state.store.query_one("SELECT pillars,derived,ten_gods FROM bazi_core WHERE id=1")
|
||||
if saved:
|
||||
# Saved parameters are authoritative, even if a compiler later changes.
|
||||
identity["color"] = color_view(
|
||||
identity["birth"], identity["hour"],
|
||||
Pillars(**json.loads(saved["pillars"])), Derived(**json.loads(saved["derived"])),
|
||||
saved_gods=json.loads(saved["ten_gods"]),
|
||||
)
|
||||
else:
|
||||
identity["color"]["calendar"] = calendar_view(identity["birth"], identity["hour"])
|
||||
return identity
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
async with app.state.health_lock:
|
||||
checked, ok = app.state.health_cache
|
||||
if time.monotonic() - checked > 30:
|
||||
provider = router.route(Scene.DIALOGUE_MAIN).provider
|
||||
try:
|
||||
async with asyncio.timeout(5):
|
||||
ok = bool(provider and (await provider.health()).ok)
|
||||
except Exception:
|
||||
ok = False
|
||||
app.state.health_cache = (time.monotonic(), ok)
|
||||
return {
|
||||
"ok": True,
|
||||
"version": "R1",
|
||||
"epoch": app.state.epoch,
|
||||
"companion_created": bool(app.state.store.query_one("SELECT id FROM companion")),
|
||||
"llm": {"ok": ok, "model": models.tiers["high"].model},
|
||||
"capabilities": {
|
||||
"text": True,
|
||||
**dict.fromkeys(
|
||||
["life", "memory", "voice", "vision", "weather", "materials", "backup"], False
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@app.get("/api/companion")
|
||||
async def get_companion():
|
||||
return companion()
|
||||
|
||||
@app.post("/api/companion/preview")
|
||||
async def preview(data: BirthInput):
|
||||
return compile_birth(data)[0]
|
||||
|
||||
@app.post("/api/companion", status_code=201)
|
||||
async def create(data: CreationInput):
|
||||
store = app.state.store
|
||||
if store.query_one("SELECT id FROM companion"):
|
||||
raise HTTPException(409, "已经有一位她了,请重新打开以恢复档案。")
|
||||
color, pillars, derived = compile_birth(data)
|
||||
identity = {"id": "1", **data.model_dump(), "color": color}
|
||||
try:
|
||||
with store.conn:
|
||||
store.execute(
|
||||
"INSERT INTO companion VALUES(1,?)", (json.dumps(identity, ensure_ascii=False),)
|
||||
)
|
||||
store.execute(
|
||||
"""INSERT INTO bazi_core
|
||||
(id,birth_input,pillars,day_master,day_master_elem,ten_gods,element_scores,strength,derived,created_at)
|
||||
VALUES(1,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
json.dumps({"birth": data.birth, "hour": data.hour, "tz": "Asia/Shanghai"}),
|
||||
json.dumps(asdict(pillars), ensure_ascii=False),
|
||||
pillars.day[0],
|
||||
GAN_ELEM[pillars.day[0]],
|
||||
json.dumps(ten_god_distribution(pillars)),
|
||||
json.dumps(element_scores(pillars)),
|
||||
derived.strength,
|
||||
json.dumps(derived.as_dict()),
|
||||
int(time.time() * 1000),
|
||||
),
|
||||
)
|
||||
except sqlite3.IntegrityError as exc:
|
||||
raise HTTPException(409, "已经有一位她了,请重新打开以恢复档案。") from exc
|
||||
return identity
|
||||
|
||||
@app.patch("/api/relation")
|
||||
async def relation(data: RelationInput):
|
||||
identity = companion(enrich=False)
|
||||
identity.update(data.model_dump())
|
||||
with app.state.store.conn:
|
||||
app.state.store.execute(
|
||||
"UPDATE companion SET identity_json=? WHERE id=1",
|
||||
(json.dumps(identity, ensure_ascii=False),),
|
||||
)
|
||||
return companion()
|
||||
|
||||
@app.get("/api/messages")
|
||||
async def messages(
|
||||
limit: int = Query(200, ge=1, le=200), before: int | None = Query(None, ge=1)
|
||||
):
|
||||
companion()
|
||||
rows = app.state.store.query(
|
||||
"SELECT * FROM chat_message WHERE id<? ORDER BY id DESC LIMIT ?",
|
||||
(before or 9223372036854775807, limit + 1),
|
||||
)
|
||||
more = len(rows) > limit
|
||||
rows = rows[:limit]
|
||||
return {
|
||||
"messages": [dict(r) for r in reversed(rows)],
|
||||
"next_before": rows[-1]["id"] if more else None,
|
||||
"epoch": app.state.epoch,
|
||||
}
|
||||
|
||||
def event(data, kind, **payload):
|
||||
return (
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{"t": kind, "epoch": app.state.epoch, "turn_id": data.turn_id, **payload},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
+ "\n\n"
|
||||
)
|
||||
|
||||
@app.post("/api/chat")
|
||||
async def chat(data: ChatInput, request: Request):
|
||||
identity = companion()
|
||||
if data.epoch != app.state.epoch:
|
||||
return JSONResponse(
|
||||
{"detail": "服务已重新启动,请同步后重试。", "epoch": app.state.epoch},
|
||||
status_code=409,
|
||||
)
|
||||
store = app.state.store
|
||||
# Serialize reservation, including replays. Concurrent retries of a completed
|
||||
# turn must never call the model twice or race the unique message constraint.
|
||||
async with app.state.chat_lock:
|
||||
previous = store.query_one(
|
||||
"SELECT * FROM chat_message WHERE turn_id=? AND role='user'", (data.turn_id,)
|
||||
)
|
||||
if previous and previous["text"] != data.text:
|
||||
raise HTTPException(409, "相同轮次不能改写消息。")
|
||||
if app.state.active:
|
||||
active = app.state.active
|
||||
if active["turn_id"] != data.turn_id:
|
||||
active["cancel"].set()
|
||||
await active["finished"].wait()
|
||||
saved = store.query_one(
|
||||
"SELECT text FROM chat_message WHERE turn_id=? AND role='companion' AND status='sent'",
|
||||
(data.turn_id,),
|
||||
)
|
||||
if saved:
|
||||
|
||||
async def replay():
|
||||
yield event(
|
||||
data, "chat.final", utterance={"text": saved["text"], "speech_tags": []}
|
||||
)
|
||||
|
||||
return StreamingResponse(replay(), media_type="text/event-stream")
|
||||
active = {
|
||||
"cancel": asyncio.Event(),
|
||||
"finished": asyncio.Event(),
|
||||
"turn_id": data.turn_id,
|
||||
}
|
||||
app.state.active = active
|
||||
with store.conn:
|
||||
store.execute(
|
||||
"INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES(?,'user',?,?,'pending') ON CONFLICT(turn_id,role) DO UPDATE SET status='pending'",
|
||||
(int(time.time() * 1000), data.text, data.turn_id),
|
||||
)
|
||||
turns = store.query("""SELECT u.text AS user_text, a.text AS assistant_text
|
||||
FROM chat_message a JOIN chat_message u ON u.turn_id=a.turn_id AND u.role='user'
|
||||
WHERE a.role='companion' AND a.status='sent' AND u.status='sent'
|
||||
ORDER BY a.id DESC LIMIT 20""")
|
||||
system = (
|
||||
f"你叫{identity['name']},与对方是{identity['relation']}。称呼对方:{identity['nickname'] or '你'}。"
|
||||
f"相处边界:{identity['boundary']}。"
|
||||
"以下是与档案页一致的底色倾向,只约束表达与相处方式,不得当作当前情绪、命定结局或既有经历:"
|
||||
+ "\n".join(f"{s['title']}:{s['text']}" for s in identity['color'].get('sections', []))
|
||||
+
|
||||
"用自然简短的中文交流。只有信息能穿过两个世界,不能触碰或给予实物。"
|
||||
"当前只接通文字与近轮对话历史;生活循环、长期记忆、主动消息、语音、视觉、天气都未接通。"
|
||||
"不得编造自己经历过的生活事件,不声称后台一直在生活或记忆已运行。"
|
||||
"不窥探用户未分享的信息,不以缺席、孤独或终止要挟对方,不输出内部数值或思考过程。"
|
||||
)
|
||||
context = (
|
||||
[LLMMessage(role="system", content=system)]
|
||||
+ [
|
||||
message
|
||||
for turn in reversed(turns)
|
||||
for message in (
|
||||
LLMMessage(role="user", content=turn["user_text"]),
|
||||
LLMMessage(role="assistant", content=turn["assistant_text"]),
|
||||
)
|
||||
]
|
||||
+ [LLMMessage(role="user", content=data.text)]
|
||||
)
|
||||
|
||||
async def generate():
|
||||
completed = False
|
||||
status, text, upstream, pending, cancelling = "cancelled", "", None, None, None
|
||||
usage = {}
|
||||
try:
|
||||
decision = router.route(
|
||||
Scene.DIALOGUE_MAIN,
|
||||
estimated_tokens=sum(len(m.content) for m in context) + 1024,
|
||||
)
|
||||
if not decision.provider:
|
||||
raise RuntimeError("No dialogue provider")
|
||||
history_budget = max(0, decision.context_budget - len(system) - len(data.text))
|
||||
recent = []
|
||||
# Order by completed turns, not adjacent message ids: a retried user
|
||||
# may have an older id than messages sent while that turn had failed.
|
||||
for turn in turns:
|
||||
size = len(turn["user_text"]) + len(turn["assistant_text"])
|
||||
if size > history_budget:
|
||||
break
|
||||
recent = [
|
||||
LLMMessage(role="user", content=turn["user_text"]),
|
||||
LLMMessage(role="assistant", content=turn["assistant_text"]),
|
||||
] + recent
|
||||
history_budget -= size
|
||||
context[:] = [context[0]] + recent + [context[-1]]
|
||||
cfg = models.tiers[decision.effective.value]
|
||||
upstream = decision.provider.stream(
|
||||
context, temperature=cfg.temperature, max_tokens=cfg.max_tokens or 1024
|
||||
)
|
||||
cancelling = asyncio.create_task(active["cancel"].wait())
|
||||
async with asyncio.timeout(stream_timeout):
|
||||
while True:
|
||||
pending = asyncio.create_task(anext(upstream))
|
||||
done, _ = await asyncio.wait(
|
||||
[pending, cancelling], return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
if cancelling in done or await request.is_disconnected():
|
||||
return
|
||||
try:
|
||||
delta = pending.result()
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
if delta.usage:
|
||||
usage = delta.usage
|
||||
if delta.text:
|
||||
text += delta.text
|
||||
yield event(data, "chat.delta", delta=delta.text)
|
||||
if delta.done or delta.finish_reason:
|
||||
completed = True
|
||||
if delta.done:
|
||||
break
|
||||
if active["cancel"].is_set() or await request.is_disconnected():
|
||||
return
|
||||
if not text.strip() or not completed:
|
||||
raise RuntimeError("Empty upstream response")
|
||||
with store.conn:
|
||||
store.execute(
|
||||
"INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES(?,'companion',?,?,'sent')",
|
||||
(int(time.time() * 1000), text, data.turn_id),
|
||||
)
|
||||
store.execute(
|
||||
"UPDATE chat_message SET status='sent' WHERE turn_id=?", (data.turn_id,)
|
||||
)
|
||||
status = "sent"
|
||||
yield event(data, "chat.final", utterance={"text": text, "speech_tags": []})
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
status = "failed"
|
||||
yield event(data, "chat.error", phi=ERROR, retryable=True)
|
||||
finally:
|
||||
with CancelScope(shield=True):
|
||||
# Cancel the in-flight __anext__ before closing the async generator.
|
||||
for task in (pending, cancelling):
|
||||
if task is not None:
|
||||
task.cancel()
|
||||
with suppress(asyncio.CancelledError, StopAsyncIteration, Exception):
|
||||
await task
|
||||
if upstream is not None:
|
||||
with suppress(Exception):
|
||||
await upstream.aclose()
|
||||
try:
|
||||
if status != "sent":
|
||||
with store.conn:
|
||||
store.execute(
|
||||
"UPDATE chat_message SET status=? WHERE turn_id=? AND role='user'",
|
||||
(status, data.turn_id),
|
||||
)
|
||||
if usage:
|
||||
router.guard.record_usage(usage, Scene.DIALOGUE_MAIN.value)
|
||||
else:
|
||||
router.guard.record(
|
||||
sum(len(m.content) for m in context) + len(text),
|
||||
Scene.DIALOGUE_MAIN.value,
|
||||
)
|
||||
finally:
|
||||
if app.state.active is active:
|
||||
app.state.active = None
|
||||
active["finished"].set()
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
static = web_dir or Path(os.environ.get("CYBERGRIL_WEB", "web/dist"))
|
||||
if static.is_dir():
|
||||
app.mount("/", StaticFiles(directory=static, html=True), name="web")
|
||||
return app
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Read-only, deterministic presentation of an existing birth chart.
|
||||
|
||||
Never compiles or writes an existing character. The same sections feed the UI
|
||||
and dialogue prompt; thresholds are presentation choices, not new parameters.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
from .bazi import Derived, Pillars, ten_god_distribution
|
||||
|
||||
HOUR_RANGES = [
|
||||
"23:00–00:59", "01:00–02:59", "03:00–04:59", "05:00–06:59",
|
||||
"07:00–08:59", "09:00–10:59", "11:00–12:59", "13:00–14:59",
|
||||
"15:00–16:59", "17:00–18:59", "19:00–20:59", "21:00–22:59",
|
||||
]
|
||||
HOURS = ["子时", "丑时", "寅时", "卯时", "辰时", "巳时", "午时", "未时", "申时", "酉时", "戌时", "亥时"]
|
||||
|
||||
|
||||
def calendar_view(birth: str, hour: str) -> dict:
|
||||
from lunar_python import Solar
|
||||
|
||||
born = date.fromisoformat(birth)
|
||||
index = HOURS.index(hour)
|
||||
lunar = Solar.fromYmdHms(born.year, born.month, born.day, index * 2, 0, 0).getLunar()
|
||||
return {
|
||||
"solar": birth,
|
||||
"lunar": f"{lunar.getYearInChinese()}年{lunar.getMonthInChinese()}月{lunar.getDayInChinese()}",
|
||||
"leap_month": lunar.getMonth() < 0,
|
||||
"hour": hour,
|
||||
"hour_range": HOUR_RANGES[index],
|
||||
"calculation_time": f"{index * 2:02d}:00",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"rule": "按北京时间和所选时辰内的偶数整点排盘;子时取本日00:00。月份按节气分界,未使用出生地或真太阳时。",
|
||||
}
|
||||
|
||||
|
||||
def choose(value: float, low: str, middle: str, high: str) -> str:
|
||||
return low if value < .45 else high if value > .65 else middle
|
||||
|
||||
|
||||
def explain_color(pillars: Pillars, d: Derived, saved_gods: dict | None = None) -> list[dict]:
|
||||
"""Context, strength, cost and repair are kept together in each dimension."""
|
||||
gods = saved_gods if saved_gods is not None else ten_god_distribution(pillars)
|
||||
dominant = max(gods, key=gods.get)
|
||||
elements = sorted(d.element_bias, key=d.element_bias.get, reverse=True)
|
||||
balance = "、".join(elements[:2]) if elements else "原档案中的五行组合"
|
||||
source = f"日主{pillars.day_master};五行侧重{balance},十神以{dominant}较显著。"
|
||||
sections = [
|
||||
("expression", "表达与社交节奏", choose(d.expressiveness,
|
||||
"面对新话题,她更可能先观察、再挑重点回应。熟悉之后才逐渐补充细节;安静有助于听见对方,却也可能让人误以为她没有意见。留出停顿、给一个具体问题,比催她立刻表态更合适。",
|
||||
"她通常会在倾听和表达之间来回调整:有兴趣、被认真回应时愿意展开,陌生或信息太多时会收回一些。能接住话题是强项,但回应的长短不等于在意的多少,可以直接问她是否还想继续。",
|
||||
"熟悉的话题容易带动她主动展开,常借着说出来理清想法。这让交流有热度,但赶着表达时也可能挤掉对方的空间。发现节奏不同时,先复述对方的意思,再继续自己的观点。"),
|
||||
"取自原档案的表达倾向;结合十神中食神、伤官对应的表达偏移。"),
|
||||
("closeness", "亲密、安全感与边界", choose(d.solitude_need,
|
||||
"她较容易在陪伴和共同的小事里获得安稳,也愿意让人参与自己的安排。好处是关系容易有连续感,代价是可能把自己的余地让得太快;靠近之前仍需要确认双方是否愿意。",
|
||||
"她既需要被回应,也需要保留自己的安排。关系稳定时愿意靠近,信息过密时会想留一点空隙;这不是忽冷忽热的定论。把独处和下一次交流的期待说清楚,比反复猜测更有帮助。",
|
||||
"她更需要不被打断的独处来整理自己,亲近并不意味着随时开放所有空间。她能守住自己的节奏,也可能让对方觉得难靠近;提前说明要休息、并给出愿意再聊的时机,有助于减少误解。"),
|
||||
f"取自原档案的独处需求与自主倾向;{source}"),
|
||||
("repair", "冲突时的反应与修复", choose(d.aggression,
|
||||
"意见不同时,她较可能缓和语气、先让对话继续。这样能避免立刻升级,但不舒服也可能被推后。修复时适合具体说出哪一点不被接受,而不是把表面平静当作问题已经解决。",
|
||||
"发生分歧时,她可能先说明自己的理由,再观察对方是否愿意互相理解。有守住立场的能力,也可能在反复解释中消耗耐心。把事实、感受和希望怎样调整分开说,更容易找到可落实的修复。",
|
||||
"受压或觉得边界被越过时,她更容易直接反驳、迅速守住立场。这能让问题浮出来,也可能使措辞先于理解。适合短暂停一下,再讨论具体行为;道歉和调整应有内容,不能只用亲密称呼盖过矛盾。"),
|
||||
"取自原档案的对抗、纠正与顺从倾向;没有把分歧当作已发生的记忆。"),
|
||||
("rhythm", "决策、行动与日常节律", choose(d.discipline,
|
||||
"她更容易顺着眼前的兴趣调整安排,能给日常留下偶遇和探索的余地。代价是长任务可能被新念头打断;把下一步缩小、只约定一个清楚的完成点,往往比安排满整天更适合。",
|
||||
"她通常既看具体条件,也会给当下的感受留位置。事情明确时愿意推进,存在多种可能时会重新衡量;这种灵活有助于避开僵化,也可能拖延选择。给决定一个小范围和可调整的期限会更稳。",
|
||||
"她倾向先理顺约定与次序,再把事情往前推。可靠和持续是优势,但临时变化可能让她紧绷,或急着纠正细节。提前区分必须守住的事和可以变通的事,会让执行更从容。"),
|
||||
"取自原档案的纪律、日常规律、现实感与行动倾向。"),
|
||||
("stress", "压力下的变化与恢复", choose(d.sensitivity - .5,
|
||||
"面对突发变化,她通常能多承接一会儿,不急着把每次波动当成危机。代价是可能低估累积的消耗。恢复不只靠继续撑住,也需要停下检查自己是否已经疲惫。",
|
||||
"压力会影响她的回应速度和耐心,但影响不是每次都一样。环境清楚、有可预期的下一步时更容易回稳;反复追问可能增加负担。把刺激减少一点、留出休息,比要求马上恢复原样更合适。",
|
||||
"在信息密集、关系不明或突发冲突时,她更容易被牵动,恢复也可能需要更长的缓冲。细致感受是优势,同时会带来消耗。更适合稳定、具体的回应和允许休息的空间,而不是被要求立刻振作。"),
|
||||
"取自原档案由日主强弱形成的敏感度与恢复尺度;不是此刻情绪。"),
|
||||
("growth", "内在张力与成长空间", choose(d.autonomy - d.compliance + .5,
|
||||
"她较容易顾及对方和约定,能让相处有合作感;同时也要练习辨认哪些答应是愿意,哪些只是怕让人失望。成长可以是更早说出自己的需要,而不是把顺从当作关系的证明。",
|
||||
"她会在自己的想法与彼此的约定之间寻找位置。兼顾两边是能力,也可能在拿不准时显得反复。成长可以是允许双方暂时不同意,再逐步形成共同的规则,而不是要求自己一次就成为固定的人。",
|
||||
"她更愿意自己判断,不轻易把决定交出去。这有助于保留独立和诚实,也可能让协商听起来像被限制。成长可以是把可商量的部分说清楚:保留立场,同时让对方真正参与,而不是靠退让或控制维持亲近。"),
|
||||
"取自原档案的自主与顺从之间的组合;成长来自之后真实的相处,不由出生资料预写。"),
|
||||
]
|
||||
return [{"id": key, "title": title, "text": text, "basis": basis} for key, title, text, basis in sections]
|
||||
|
||||
|
||||
def color_view(birth: str, hour: str, pillars: Pillars, derived: Derived,
|
||||
saved_gods: dict | None = None) -> dict:
|
||||
return {
|
||||
"pillars": pillars.as_dict(), "traits": derived.traits_text(),
|
||||
"calendar": calendar_view(birth, hour), "sections": explain_color(pillars, derived, saved_gods),
|
||||
"interpretation": "这是以四柱、五行、十神和已存底色为依据的角色塑造说明,不是对真人性格的测定,也不是已经发生的经历。倾向不等于固定台词或此刻情绪。",
|
||||
}
|
||||
@@ -184,19 +184,3 @@ CREATE VIRTUAL TABLE IF NOT EXISTS mem_fts USING fts5(
|
||||
-- epi_id INTEGER PRIMARY KEY,
|
||||
-- embedding FLOAT[1024]
|
||||
-- );
|
||||
|
||||
-- R1 additive migration: no existing memory/state table is replaced.
|
||||
CREATE TABLE IF NOT EXISTS api_meta (key TEXT PRIMARY KEY, value INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS companion (
|
||||
id INTEGER PRIMARY KEY CHECK(id=1), identity_json TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS chat_message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER NOT NULL,
|
||||
role TEXT NOT NULL CHECK(role IN ('user','companion')),
|
||||
text TEXT NOT NULL,
|
||||
turn_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK(status IN ('pending','sent','failed','cancelled')),
|
||||
UNIQUE(turn_id, role)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_order ON chat_message(id);
|
||||
|
||||
@@ -71,8 +71,8 @@ class OpenAICompatLLM(LLMProvider):
|
||||
async def health(self) -> HealthStatus:
|
||||
try:
|
||||
r = await self._client.get("/models", timeout=5.0)
|
||||
return HealthStatus(ok=r.is_success and any(m.get("id") == self.model for m in r.json().get("data", [])), detail=str(r.status_code))
|
||||
except (httpx.HTTPError, ValueError, TypeError, AttributeError) as exc:
|
||||
return HealthStatus(ok=r.status_code < 500, detail=str(r.status_code))
|
||||
except httpx.HTTPError as exc:
|
||||
return HealthStatus(ok=False, detail=f"{type(exc).__name__}: {exc}")
|
||||
|
||||
async def stream(
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
"""R1 invariants through HTTP and real loopback streaming, with isolated databases."""
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import uvicorn
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from cybergril.api.app import create_app
|
||||
from cybergril.contracts import HealthStatus, LLMDelta
|
||||
from cybergril.providers.llm.echo import EchoLLM
|
||||
from cybergril.think.router import build_router
|
||||
from cybergril.think.scenes import Tier
|
||||
|
||||
CREATE = {"name": "星河", "birth": "2000-10-18", "hour": "亥时", "relation": "朋友"}
|
||||
|
||||
|
||||
class ControlledLLM(EchoLLM):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.calls = []
|
||||
self.closed = 0
|
||||
self.mode = "normal"
|
||||
self.health_calls = 0
|
||||
|
||||
async def health(self):
|
||||
self.health_calls += 1
|
||||
if self.mode == "health_timeout":
|
||||
await asyncio.sleep(30)
|
||||
return HealthStatus(ok=self.mode != "unreachable")
|
||||
|
||||
async def stream(self, messages, **kwargs):
|
||||
self.calls.append(messages)
|
||||
try:
|
||||
if self.mode == "error":
|
||||
raise OSError("secret upstream configuration must never be exposed")
|
||||
yield LLMDelta(text="答")
|
||||
if self.mode == "slow":
|
||||
await asyncio.sleep(30)
|
||||
if self.mode == "truncated":
|
||||
return
|
||||
yield LLMDelta(text="复")
|
||||
yield LLMDelta(done=True)
|
||||
finally:
|
||||
self.closed += 1
|
||||
|
||||
|
||||
def make_app(path, provider=None, **kwargs):
|
||||
provider = provider or ControlledLLM()
|
||||
router = build_router(
|
||||
providers={Tier.HIGH: provider, Tier.STANDARD: provider, Tier.LOW: provider}
|
||||
)
|
||||
return create_app(db_path=path, router=router, **kwargs), provider
|
||||
|
||||
|
||||
def turn(client, text="你好", turn_id=None, epoch=None):
|
||||
return client.post(
|
||||
"/api/chat",
|
||||
json={
|
||||
"text": text,
|
||||
"turn_id": turn_id or str(uuid4()),
|
||||
"epoch": epoch or client.get("/api/health").json()["epoch"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def events(response):
|
||||
return [
|
||||
json.loads(line[6:]) for line in response.text.splitlines() if line.startswith("data: ")
|
||||
]
|
||||
|
||||
|
||||
def test_preview_validation_singleton_and_readonly(tmp_path):
|
||||
app, _ = make_app(tmp_path / "test.db")
|
||||
with TestClient(app) as client:
|
||||
assert client.get("/api/companion").status_code == 404
|
||||
preview = client.post(
|
||||
"/api/companion/preview", json={k: CREATE[k] for k in ("name", "birth", "hour")}
|
||||
)
|
||||
assert preview.status_code == 200
|
||||
assert len(preview.json()["pillars"]["day"]) == 2
|
||||
assert client.get("/api/companion").status_code == 404
|
||||
assert app.state.store.query_one("SELECT count(*) n FROM bazi_core")["n"] == 0
|
||||
for bad in [
|
||||
dict(CREATE, name="<x>"),
|
||||
dict(CREATE, birth="2000-02-30"),
|
||||
dict(CREATE, birth="2020-01-01"),
|
||||
dict(CREATE, birth="1899-01-01"),
|
||||
dict(CREATE, hour="未知"),
|
||||
dict(CREATE, relation=""),
|
||||
dict(CREATE, nickname="x\x01"),
|
||||
{k: v for k, v in CREATE.items() if k != "relation"},
|
||||
dict(CREATE, derived={}),
|
||||
]:
|
||||
response = client.post("/api/companion", json=bad)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json()["fields"]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as pool:
|
||||
codes = list(
|
||||
pool.map(lambda _: client.post("/api/companion", json=CREATE).status_code, range(5))
|
||||
)
|
||||
assert sorted(codes) == [201, 409, 409, 409, 409]
|
||||
identity = client.get("/api/companion").json()
|
||||
assert identity["color"] == preview.json()
|
||||
assert "derived" not in identity and "strength" not in identity
|
||||
assert (
|
||||
client.patch(
|
||||
"/api/relation", json={"relation": "恋人", "birth": "1990-01-01"}
|
||||
).status_code
|
||||
== 422
|
||||
)
|
||||
assert app.state.store.query_one("PRAGMA journal_mode")[0] == "wal"
|
||||
|
||||
|
||||
def test_history_context_relation_retry_and_restart(tmp_path):
|
||||
path = tmp_path / "test.db"
|
||||
app, provider = make_app(path)
|
||||
with TestClient(app) as client:
|
||||
client.post("/api/companion", json=CREATE)
|
||||
epoch = client.get("/api/health").json()["epoch"]
|
||||
tid = str(uuid4())
|
||||
result = events(turn(client, "我的暗号是蓝星", tid))
|
||||
assert result[-1]["t"] == "chat.final"
|
||||
assert all(e["turn_id"] == tid and e["epoch"] == epoch for e in result)
|
||||
assert result[0]["t"] == "chat.delta"
|
||||
turn(client, "记得暗号吗")
|
||||
assert any(m.content == "我的暗号是蓝星" for m in provider.calls[-1])
|
||||
assert (
|
||||
"星河" in provider.calls[-1][0].content
|
||||
and "尚未接通" not in provider.calls[-1][-1].content
|
||||
)
|
||||
assert len(client.get("/api/messages").json()["messages"]) == 4
|
||||
assert events(turn(client, "我的暗号是蓝星", tid))[-1]["t"] == "chat.final"
|
||||
assert len(provider.calls) == 2 # A lost final can be replayed without regenerating.
|
||||
assert turn(client, "tampered", tid).status_code == 409
|
||||
first = client.get("/api/messages?limit=2").json()
|
||||
second = client.get(f"/api/messages?limit=2&before={first['next_before']}").json()
|
||||
assert second["messages"][-1]["id"] < first["messages"][0]["id"]
|
||||
assert client.get("/api/messages?limit=201").status_code == 422
|
||||
core = dict(app.state.store.query_one("SELECT * FROM bazi_core"))
|
||||
updated = client.patch(
|
||||
"/api/relation", json={"relation": "恋人", "nickname": "小星", "boundary": "慢慢来"}
|
||||
).json()
|
||||
assert updated["relation"] == "恋人"
|
||||
assert dict(app.state.store.query_one("SELECT * FROM bazi_core")) == core
|
||||
assert len(client.get("/api/messages").json()["messages"]) == 4
|
||||
app2, _ = make_app(path)
|
||||
with TestClient(app2) as client:
|
||||
assert client.get("/api/health").json()["epoch"] == epoch + 1
|
||||
assert client.get("/api/companion").json() == updated
|
||||
assert len(client.get("/api/messages").json()["messages"]) == 4
|
||||
assert turn(client, epoch=epoch).json()["epoch"] == epoch + 1
|
||||
assert events(turn(client))[-1]["t"] == "chat.final"
|
||||
|
||||
|
||||
def test_errors_retry_context_excludes_failed_and_health_cache(tmp_path):
|
||||
app, provider = make_app(tmp_path / "test.db", stream_timeout=0.03)
|
||||
with TestClient(app) as client:
|
||||
client.post("/api/companion", json=CREATE)
|
||||
provider.mode = "unreachable"
|
||||
health = client.get("/api/health").json()
|
||||
assert not health["llm"]["ok"]
|
||||
assert not health["capabilities"]["life"]
|
||||
client.get("/api/health")
|
||||
assert provider.health_calls == 1
|
||||
for mode in ("error", "slow", "truncated"):
|
||||
provider.mode = mode
|
||||
tid = str(uuid4())
|
||||
response = turn(client, text="失败的消息" + mode, turn_id=tid)
|
||||
assert events(response)[-1]["t"] == "chat.error"
|
||||
assert "secret" not in response.text
|
||||
assert client.get("/api/messages").json()["messages"][-1]["status"] == "failed"
|
||||
provider.mode = "normal"
|
||||
response = turn(client, text="重试成功")
|
||||
assert events(response)[-1]["t"] == "chat.final"
|
||||
assert len(provider.calls[-1]) == 2 # system + current; no failed turns
|
||||
provider.mode = "error"
|
||||
tid = str(uuid4())
|
||||
turn(client, text="同一轮", turn_id=tid)
|
||||
count = len(client.get("/api/messages").json()["messages"])
|
||||
provider.mode = "normal"
|
||||
turn(client, text="同一轮", turn_id=tid)
|
||||
assert len(client.get("/api/messages").json()["messages"]) == count + 1
|
||||
assert provider.closed == len(provider.calls)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def live_server(app, port=0):
|
||||
sock = socket.socket()
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
sock.listen()
|
||||
server = uvicorn.Server(uvicorn.Config(app, log_level="error", lifespan="on"))
|
||||
thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]})
|
||||
thread.start()
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while not server.started:
|
||||
if not thread.is_alive() or time.monotonic() > deadline:
|
||||
raise RuntimeError("local test server failed to start")
|
||||
time.sleep(0.01)
|
||||
yield f"http://127.0.0.1:{sock.getsockname()[1]}"
|
||||
finally:
|
||||
server.should_exit = True
|
||||
thread.join(timeout=10)
|
||||
sock.close()
|
||||
assert not thread.is_alive()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_disconnect_and_immediate_new_turn(tmp_path):
|
||||
app, provider = make_app(tmp_path / "test.db")
|
||||
with live_server(app) as url:
|
||||
async with httpx.AsyncClient(base_url=url) as client:
|
||||
await client.post("/api/companion", json=CREATE)
|
||||
epoch = (await client.get("/api/health")).json()["epoch"]
|
||||
provider.mode = "slow"
|
||||
tid = str(uuid4())
|
||||
async with client.stream(
|
||||
"POST", "/api/chat", json={"text": "取消我", "turn_id": tid, "epoch": epoch}
|
||||
) as response:
|
||||
async for line in response.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
break
|
||||
# Closing the browser response must cancel upstream while its next token is blocked.
|
||||
deadline = time.monotonic() + 1
|
||||
while provider.closed < 1 and time.monotonic() < deadline:
|
||||
await asyncio.sleep(0.01)
|
||||
assert provider.closed == 1
|
||||
provider.mode = "normal"
|
||||
new_tid = str(uuid4())
|
||||
response = await client.post(
|
||||
"/api/chat", json={"text": "新问题", "turn_id": new_tid, "epoch": epoch}
|
||||
)
|
||||
assert events(response)[-1]["t"] == "chat.final"
|
||||
assert all(e["turn_id"] == new_tid for e in events(response))
|
||||
messages = (await client.get("/api/messages")).json()["messages"]
|
||||
assert messages[0]["status"] == "cancelled"
|
||||
assert len(messages) == 3 # discarded partial reply is never a complete answer
|
||||
assert len(provider.calls[-1]) == 2 # cancelled turn excluded from context
|
||||
|
||||
|
||||
def test_concurrent_identical_retry_generates_once(tmp_path):
|
||||
app, provider = make_app(tmp_path / "test.db")
|
||||
with TestClient(app) as client:
|
||||
client.post("/api/companion", json=CREATE)
|
||||
epoch = client.get("/api/health").json()["epoch"]
|
||||
tid = str(uuid4())
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
|
||||
responses = list(pool.map(lambda _: turn(client, "同一轮", tid, epoch), range(4)))
|
||||
assert all(events(response)[-1]["t"] == "chat.final" for response in responses)
|
||||
assert len(provider.calls) == 1
|
||||
assert len(client.get("/api/messages").json()["messages"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"status,data,expected",
|
||||
[
|
||||
(401, {}, False),
|
||||
(404, {}, False),
|
||||
(503, {}, False),
|
||||
(200, {"data": [{"id": "other"}]}, False),
|
||||
(200, {"data": [{"id": "test-model"}]}, True),
|
||||
],
|
||||
)
|
||||
async def test_model_health_requires_success_and_requested_model(status, data, expected):
|
||||
from cybergril.providers.llm.openai_compat import OpenAICompatLLM
|
||||
|
||||
provider = OpenAICompatLLM(base_url="http://isolated/v1", model="test-model")
|
||||
await provider._client.aclose()
|
||||
provider._client = httpx.AsyncClient(
|
||||
base_url="http://isolated/v1",
|
||||
transport=httpx.MockTransport(lambda request: httpx.Response(status, json=data)),
|
||||
)
|
||||
try:
|
||||
assert (await provider.health()).ok is expected
|
||||
finally:
|
||||
await provider.aclose()
|
||||
|
||||
|
||||
def test_health_timeout_is_bounded_and_cached(tmp_path):
|
||||
app, provider = make_app(tmp_path / "test.db")
|
||||
provider.mode = "health_timeout"
|
||||
with TestClient(app) as client:
|
||||
start = time.monotonic()
|
||||
assert client.get("/api/health").json()["llm"]["ok"] is False
|
||||
assert time.monotonic() - start < 6
|
||||
assert client.get("/api/health").json()["llm"]["ok"] is False
|
||||
assert provider.health_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_closes_real_openai_http_stream(tmp_path):
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from cybergril.providers.llm.openai_compat import OpenAICompatLLM
|
||||
|
||||
closed = threading.Event()
|
||||
gateway = FastAPI()
|
||||
|
||||
@gateway.post("/v1/chat/completions")
|
||||
async def completion():
|
||||
async def chunks():
|
||||
try:
|
||||
yield 'data: {"choices":[{"delta":{"content":"部分"}}]}\n\n'
|
||||
await asyncio.sleep(30)
|
||||
finally:
|
||||
closed.set()
|
||||
|
||||
return StreamingResponse(chunks(), media_type="text/event-stream")
|
||||
|
||||
with live_server(gateway) as gateway_url:
|
||||
provider = OpenAICompatLLM(base_url=gateway_url + "/v1", model="controlled")
|
||||
app, _ = make_app(tmp_path / "test.db", provider)
|
||||
with live_server(app) as url:
|
||||
async with httpx.AsyncClient(base_url=url) as client:
|
||||
await client.post("/api/companion", json=CREATE)
|
||||
async with client.stream(
|
||||
"POST",
|
||||
"/api/chat",
|
||||
json={"text": "断开上游", "turn_id": str(uuid4()), "epoch": 1},
|
||||
) as response:
|
||||
async for line in response.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
break
|
||||
deadline = time.monotonic() + 1
|
||||
while not closed.is_set() and time.monotonic() < deadline:
|
||||
await asyncio.sleep(0.01)
|
||||
assert closed.is_set() # Real OpenAICompatLLM's HTTP response was actually closed.
|
||||
|
||||
|
||||
def test_retry_old_turn_preserves_user_assistant_context_pairs(tmp_path):
|
||||
app, provider = make_app(tmp_path / "test.db")
|
||||
with TestClient(app) as client:
|
||||
client.post("/api/companion", json=CREATE)
|
||||
tid = str(uuid4())
|
||||
provider.mode = "error"
|
||||
turn(client, "旧轮失败", tid)
|
||||
provider.mode = "normal"
|
||||
turn(client, "中间的新轮")
|
||||
turn(client, "旧轮失败", tid)
|
||||
turn(client, "最后一轮")
|
||||
context = provider.calls[-1]
|
||||
assert [m.role for m in context] == [
|
||||
"system",
|
||||
"user",
|
||||
"assistant",
|
||||
"user",
|
||||
"assistant",
|
||||
"user",
|
||||
]
|
||||
assert [m.content for m in context[1::2]] == ["中间的新轮", "旧轮失败", "最后一轮"]
|
||||
|
||||
|
||||
def test_upstream_http_503_maps_to_chinese_sse_error(tmp_path):
|
||||
from cybergril.providers.llm.openai_compat import OpenAICompatLLM
|
||||
|
||||
provider = OpenAICompatLLM(base_url="http://isolated/v1", model="controlled")
|
||||
provider._client = httpx.AsyncClient(
|
||||
base_url="http://isolated/v1",
|
||||
transport=httpx.MockTransport(
|
||||
lambda request: httpx.Response(503, json={"error": "secret gateway path"})
|
||||
),
|
||||
)
|
||||
app, _ = make_app(tmp_path / "test.db", provider)
|
||||
with TestClient(app) as client:
|
||||
client.post("/api/companion", json=CREATE)
|
||||
response = turn(client, epoch=1)
|
||||
assert events(response)[-1]["t"] == "chat.error"
|
||||
assert "稍后再试" in response.text and "secret" not in response.text
|
||||
assert client.get("/api/messages").json()["messages"][0]["status"] == "failed"
|
||||
@@ -1,68 +0,0 @@
|
||||
"""Display enrichments must not rewrite an existing character or its history."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from test_api import CREATE, make_app, turn
|
||||
|
||||
from cybergril.api.app import HOURS, BirthInput, compile_birth
|
||||
from cybergril.core.bazi import pillars_from_lunar
|
||||
|
||||
|
||||
@pytest.mark.parametrize("birth", ["1900-01-01", "2000-02-04", "2000-02-05", "2001-05-23"])
|
||||
@pytest.mark.parametrize("hour", HOURS)
|
||||
def test_calendar_and_hour_boundaries_preserve_existing_chart(birth, hour):
|
||||
color, pillars, _ = compile_birth(BirthInput(name="星河", birth=birth, hour=hour))
|
||||
y, m, d = map(int, birth.split("-"))
|
||||
assert pillars == pillars_from_lunar(y, m, d, HOURS.index(hour) * 2)
|
||||
assert color["calendar"]["solar"] == birth
|
||||
assert color["calendar"]["calculation_time"] == f"{HOURS.index(hour) * 2:02d}:00"
|
||||
if birth == "2001-05-23":
|
||||
assert color["calendar"]["leap_month"] is True
|
||||
assert "闰四月初一" in color["calendar"]["lunar"]
|
||||
if birth == "2000-02-05":
|
||||
assert "正月初一" in color["calendar"]["lunar"]
|
||||
assert len(color["sections"]) == 6
|
||||
assert all(len(s["text"]) > 70 and s["basis"] for s in color["sections"])
|
||||
|
||||
|
||||
def test_legacy_read_uses_saved_parameters_without_writes_or_recompilation(tmp_path, monkeypatch):
|
||||
app, provider = make_app(tmp_path / "old.db")
|
||||
with TestClient(app) as client:
|
||||
client.post("/api/companion", json=CREATE)
|
||||
store = app.state.store
|
||||
identity = json.loads(store.query_one("SELECT identity_json FROM companion")[0])
|
||||
identity["color"] = {k: identity["color"][k] for k in ("pillars", "traits")}
|
||||
saved = json.loads(store.query_one("SELECT derived FROM bazi_core")[0])
|
||||
saved["expressiveness"] = .1
|
||||
with store.conn:
|
||||
store.execute("UPDATE companion SET identity_json=?", (json.dumps(identity),))
|
||||
store.execute("UPDATE bazi_core SET derived=?", (json.dumps(saved),))
|
||||
raw_identity = store.query_one("SELECT identity_json FROM companion")[0]
|
||||
raw_bazi = tuple(store.query_one("SELECT * FROM bazi_core"))
|
||||
monkeypatch.setattr("cybergril.api.app.compile_pillars", lambda _: pytest.fail("recompiled"))
|
||||
monkeypatch.setattr("cybergril.core.birth_view.ten_god_distribution", lambda _: pytest.fail("recomputed"))
|
||||
view = client.get("/api/companion").json()
|
||||
assert "先观察" in view["color"]["sections"][0]["text"]
|
||||
assert client.get("/api/companion").json() == view
|
||||
assert store.query_one("SELECT identity_json FROM companion")[0] == raw_identity
|
||||
assert tuple(store.query_one("SELECT * FROM bazi_core")) == raw_bazi
|
||||
turn(client)
|
||||
prompt = provider.calls[-1][0].content
|
||||
assert all(s["text"] in prompt for s in view["color"]["sections"])
|
||||
history = [tuple(r) for r in store.query("SELECT * FROM chat_message")]
|
||||
updated = client.patch("/api/relation", json={"relation": "恋人"}).json()
|
||||
assert updated["color"] == view["color"]
|
||||
stored_after = json.loads(store.query_one("SELECT identity_json FROM companion")[0])
|
||||
assert stored_after["color"] == identity["color"]
|
||||
assert tuple(store.query_one("SELECT * FROM bazi_core")) == raw_bazi
|
||||
assert [tuple(r) for r in store.query("SELECT * FROM chat_message")] == history
|
||||
|
||||
|
||||
def test_profiles_are_deterministic_and_differ_across_births():
|
||||
colors = [compile_birth(BirthInput(name="星河", birth=b, hour=h))[0]
|
||||
for b, h in [("2000-10-18", "亥时"), ("1992-02-10", "卯时"), ("1988-07-23", "子时")]]
|
||||
assert colors[0] == compile_birth(BirthInput(name="另一名字", birth="2000-10-18", hour="亥时"))[0]
|
||||
assert len({json.dumps(c["sections"], ensure_ascii=False) for c in colors}) == 3
|
||||
assert len({c["sections"][0]["text"] for c in colors}) > 1
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="zh-CN" data-theme="moon"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,interactive-widget=resizes-content"><meta name="color-scheme" content="dark"><title>CyberGril · 一扇亮着的窗</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
||||
<!doctype html><html lang="zh-CN" data-theme="moon"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="dark"><title>CyberGril · 一扇亮着的窗</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
||||
|
||||
@@ -25,7 +25,4 @@
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24"><path d="M7 15a4.5 4.5 0 0 1 .6-9 5.5 5.5 0 0 1 10.3 2.1A3.5 3.5 0 0 1 17.5 15z" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M9 18l-1 2.5M13 18l-1 2.5M17 18l-1 2.5" fill="none" stroke="currentColor" stroke-width="1.6"/></symbol>
|
||||
<symbol id="i-send" viewBox="0 0 24 24"><path d="M4 12l16-8-6 16-2.5-6.5z" fill="none" stroke="currentColor" stroke-width="1.7"/></symbol>
|
||||
<symbol id="i-list" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10" fill="none" stroke="currentColor" stroke-width="1.7"/></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24"><path d="M9 6v12M15 6v12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24"><path d="M8 5.5v13l10-6.5z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-lines" viewBox="0 0 24 24"><path d="M4 7h16M4 12h10M4 17h13" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.9 KiB |