commit 65cb9d5ead52e25cf6ce6be046b77ce2ae49a22d Author: omo Date: Mon Aug 17 17:11:27 2026 +0800 initial: blog-app snapshot diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a550266 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# blog-app environment variables (Stage 3) +# Copy this file to .env or export the variables you want to override. +# All variables are OPTIONAL — config.py has defaults for every entry below. +# Format: VAR=default_value # 用途 / 单位 / 范围 + +# --- Flask --- +FLASK_SECRET_KEY=dev-watchdog-secret-change-me # Flask session/CSRF secret. 生产必须改为随机长字符串 (>=32 bytes entropy). + +# --- Database paths --- +BLOG_APP_DB_PATH=~/opencode-blog-showcase/data/projects.db # 主数据库 (projects/releases/issues). 绝对路径. +BLOG_APP_WATCHDOG_DB=~/opencode-blog-showcase/data/watchdog.db # 看门狗状态 DB. 绝对路径. + +# --- LLM backend --- +BLOG_APP_LLM_URL=http://127.0.0.1:4000/v1 # OpenAI-compatible base URL. 走 litellm-proxy 本地网关. +BLOG_APP_LLM_MODEL=default # 模型名, 透传给 LLM_URL. +BLOG_APP_LLM_TIMEOUT=300 # LLM 单次请求超时. 单位: 秒. 范围 30-600. + +# --- Rate limiter (in-memory, per-process) --- +BLOG_APP_RATE_LIMIT_MAX=3 # /api/analyze/stream 每 IP 窗口内最大请求数. 单位: 次. 范围 1-100. +BLOG_APP_RATE_LIMIT_WINDOW=60 # 限流窗口长度. 单位: 秒. 范围 10-3600. + +# --- Watchdog --- +BLOG_APP_WATCHDOG_COOLDOWN=60 # /watchdog/run 手动触发后的冷却时间. 单位: 秒. 范围 0-3600. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b9f482 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +__pycache__/ +*.pyc +*.pyo +*.db +*.db-shm +*.db-wal +*.pid +*.log +.env +venv/ +.venv/ +node_modules/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +*.egg-info/ +dist/ +build/ diff --git a/.omo/run-continuation/ses_ff159fc18ffeJxu6s7OAjWoooM.json b/.omo/run-continuation/ses_ff159fc18ffeJxu6s7OAjWoooM.json new file mode 100644 index 0000000..cad704d --- /dev/null +++ b/.omo/run-continuation/ses_ff159fc18ffeJxu6s7OAjWoooM.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff159fc18ffeJxu6s7OAjWoooM", + "updatedAt": "2026-08-17T07:44:31.477Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T07:44:31.477Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff159fc23ffeDE29Ddwbuq5hVe.json b/.omo/run-continuation/ses_ff159fc23ffeDE29Ddwbuq5hVe.json new file mode 100644 index 0000000..59ebab3 --- /dev/null +++ b/.omo/run-continuation/ses_ff159fc23ffeDE29Ddwbuq5hVe.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff159fc23ffeDE29Ddwbuq5hVe", + "updatedAt": "2026-08-17T07:39:35.211Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T07:39:35.211Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff166dd6cffe3CK3f9VKnYH6ZL.json b/.omo/run-continuation/ses_ff166dd6cffe3CK3f9VKnYH6ZL.json new file mode 100644 index 0000000..2ad7b7b --- /dev/null +++ b/.omo/run-continuation/ses_ff166dd6cffe3CK3f9VKnYH6ZL.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff166dd6cffe3CK3f9VKnYH6ZL", + "updatedAt": "2026-08-17T07:26:59.412Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T07:26:59.412Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff18f8ad5ffepRPGaLP85ye130.json b/.omo/run-continuation/ses_ff18f8ad5ffepRPGaLP85ye130.json new file mode 100644 index 0000000..e4e35cd --- /dev/null +++ b/.omo/run-continuation/ses_ff18f8ad5ffepRPGaLP85ye130.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff18f8ad5ffepRPGaLP85ye130", + "updatedAt": "2026-08-17T06:49:55.284Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T06:49:55.284Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff1a61338ffeNFb48EUB8uOSRj.json b/.omo/run-continuation/ses_ff1a61338ffeNFb48EUB8uOSRj.json new file mode 100644 index 0000000..a0ce6e1 --- /dev/null +++ b/.omo/run-continuation/ses_ff1a61338ffeNFb48EUB8uOSRj.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff1a61338ffeNFb48EUB8uOSRj", + "updatedAt": "2026-08-17T06:21:36.159Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T06:21:36.159Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff1d5e7acffeoP8eedBYLMiuqB.json b/.omo/run-continuation/ses_ff1d5e7acffeoP8eedBYLMiuqB.json new file mode 100644 index 0000000..68f73c7 --- /dev/null +++ b/.omo/run-continuation/ses_ff1d5e7acffeoP8eedBYLMiuqB.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff1d5e7acffeoP8eedBYLMiuqB", + "updatedAt": "2026-08-17T05:29:45.833Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T05:29:45.833Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff1d60adbffevryaIPH2sKDd30.json b/.omo/run-continuation/ses_ff1d60adbffevryaIPH2sKDd30.json new file mode 100644 index 0000000..1aa00dc --- /dev/null +++ b/.omo/run-continuation/ses_ff1d60adbffevryaIPH2sKDd30.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff1d60adbffevryaIPH2sKDd30", + "updatedAt": "2026-08-17T05:30:30.053Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T05:30:30.053Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff1e6c1bfffenLybPu302e8w5Y.json b/.omo/run-continuation/ses_ff1e6c1bfffenLybPu302e8w5Y.json new file mode 100644 index 0000000..c2cd7f7 --- /dev/null +++ b/.omo/run-continuation/ses_ff1e6c1bfffenLybPu302e8w5Y.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff1e6c1bfffenLybPu302e8w5Y", + "updatedAt": "2026-08-17T05:23:13.190Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T05:23:13.190Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff2186cd4ffedidMUrQ3K284Gv.json b/.omo/run-continuation/ses_ff2186cd4ffedidMUrQ3K284Gv.json new file mode 100644 index 0000000..58cba10 --- /dev/null +++ b/.omo/run-continuation/ses_ff2186cd4ffedidMUrQ3K284Gv.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff2186cd4ffedidMUrQ3K284Gv", + "updatedAt": "2026-08-17T04:12:50.536Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T04:12:50.536Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff22f46c8ffesshvbZ1v4tDv8i.json b/.omo/run-continuation/ses_ff22f46c8ffesshvbZ1v4tDv8i.json new file mode 100644 index 0000000..f9403f8 --- /dev/null +++ b/.omo/run-continuation/ses_ff22f46c8ffesshvbZ1v4tDv8i.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff22f46c8ffesshvbZ1v4tDv8i", + "updatedAt": "2026-08-17T03:50:13.111Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T03:50:13.111Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff25cf81affePBvZb9QE1MLOZb.json b/.omo/run-continuation/ses_ff25cf81affePBvZb9QE1MLOZb.json new file mode 100644 index 0000000..1d7e24c --- /dev/null +++ b/.omo/run-continuation/ses_ff25cf81affePBvZb9QE1MLOZb.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff25cf81affePBvZb9QE1MLOZb", + "updatedAt": "2026-08-17T02:54:09.622Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T02:54:09.622Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff260f6b4ffeuQioO3jpjEA3LD.json b/.omo/run-continuation/ses_ff260f6b4ffeuQioO3jpjEA3LD.json new file mode 100644 index 0000000..c70f34d --- /dev/null +++ b/.omo/run-continuation/ses_ff260f6b4ffeuQioO3jpjEA3LD.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff260f6b4ffeuQioO3jpjEA3LD", + "updatedAt": "2026-08-17T02:49:29.819Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T02:49:29.819Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff27081cfffe0zNjSG2xQQxXHZ.json b/.omo/run-continuation/ses_ff27081cfffe0zNjSG2xQQxXHZ.json new file mode 100644 index 0000000..81ae53c --- /dev/null +++ b/.omo/run-continuation/ses_ff27081cfffe0zNjSG2xQQxXHZ.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff27081cfffe0zNjSG2xQQxXHZ", + "updatedAt": "2026-08-17T02:51:26.049Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T02:51:26.049Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff27402a0fferS1XNbdKMAHj0i.json b/.omo/run-continuation/ses_ff27402a0fferS1XNbdKMAHj0i.json new file mode 100644 index 0000000..5001451 --- /dev/null +++ b/.omo/run-continuation/ses_ff27402a0fferS1XNbdKMAHj0i.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff27402a0fferS1XNbdKMAHj0i", + "updatedAt": "2026-08-17T02:56:24.982Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-17T02:56:24.982Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff50b132cffecxl7mkCh04JsML.json b/.omo/run-continuation/ses_ff50b132cffecxl7mkCh04JsML.json new file mode 100644 index 0000000..4890c03 --- /dev/null +++ b/.omo/run-continuation/ses_ff50b132cffecxl7mkCh04JsML.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff50b132cffecxl7mkCh04JsML", + "updatedAt": "2026-08-16T14:26:54.348Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T14:26:54.348Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff50cdb51ffetEA2ioyGjzaT1D.json b/.omo/run-continuation/ses_ff50cdb51ffetEA2ioyGjzaT1D.json new file mode 100644 index 0000000..90b80a8 --- /dev/null +++ b/.omo/run-continuation/ses_ff50cdb51ffetEA2ioyGjzaT1D.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff50cdb51ffetEA2ioyGjzaT1D", + "updatedAt": "2026-08-16T14:27:04.432Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T14:27:04.432Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff50e2ce5ffef6Ao7h7KUyF2FU.json b/.omo/run-continuation/ses_ff50e2ce5ffef6Ao7h7KUyF2FU.json new file mode 100644 index 0000000..2e722c6 --- /dev/null +++ b/.omo/run-continuation/ses_ff50e2ce5ffef6Ao7h7KUyF2FU.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff50e2ce5ffef6Ao7h7KUyF2FU", + "updatedAt": "2026-08-16T14:27:17.738Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T14:27:17.738Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff510ac90ffe9a3BQKu1yMPynA.json b/.omo/run-continuation/ses_ff510ac90ffe9a3BQKu1yMPynA.json new file mode 100644 index 0000000..62f1ff7 --- /dev/null +++ b/.omo/run-continuation/ses_ff510ac90ffe9a3BQKu1yMPynA.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff510ac90ffe9a3BQKu1yMPynA", + "updatedAt": "2026-08-16T14:29:47.267Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T14:29:47.267Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff511f1e9ffeaIl1AovLKJluHW.json b/.omo/run-continuation/ses_ff511f1e9ffeaIl1AovLKJluHW.json new file mode 100644 index 0000000..8916a45 --- /dev/null +++ b/.omo/run-continuation/ses_ff511f1e9ffeaIl1AovLKJluHW.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff511f1e9ffeaIl1AovLKJluHW", + "updatedAt": "2026-08-16T14:30:17.060Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T14:30:17.060Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff589f9aaffeRAusdEoDgca2cl.json b/.omo/run-continuation/ses_ff589f9aaffeRAusdEoDgca2cl.json new file mode 100644 index 0000000..e32510b --- /dev/null +++ b/.omo/run-continuation/ses_ff589f9aaffeRAusdEoDgca2cl.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff589f9aaffeRAusdEoDgca2cl", + "updatedAt": "2026-08-16T12:18:57.152Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T12:18:57.152Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff59780d6ffe16Jf15dqV6Rfm0.json b/.omo/run-continuation/ses_ff59780d6ffe16Jf15dqV6Rfm0.json new file mode 100644 index 0000000..e849bd0 --- /dev/null +++ b/.omo/run-continuation/ses_ff59780d6ffe16Jf15dqV6Rfm0.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff59780d6ffe16Jf15dqV6Rfm0", + "updatedAt": "2026-08-16T11:50:36.626Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:50:36.626Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff597c296ffebbSiGIkHgH6PjF.json b/.omo/run-continuation/ses_ff597c296ffebbSiGIkHgH6PjF.json new file mode 100644 index 0000000..fc167e0 --- /dev/null +++ b/.omo/run-continuation/ses_ff597c296ffebbSiGIkHgH6PjF.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff597c296ffebbSiGIkHgH6PjF", + "updatedAt": "2026-08-16T11:50:36.630Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:50:36.630Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5981104ffeqKEgnR336yVg2k.json b/.omo/run-continuation/ses_ff5981104ffeqKEgnR336yVg2k.json new file mode 100644 index 0000000..12c99d7 --- /dev/null +++ b/.omo/run-continuation/ses_ff5981104ffeqKEgnR336yVg2k.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5981104ffeqKEgnR336yVg2k", + "updatedAt": "2026-08-16T11:50:36.631Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:50:36.631Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5998da7ffev009eRDsRGBQVR.json b/.omo/run-continuation/ses_ff5998da7ffev009eRDsRGBQVR.json new file mode 100644 index 0000000..0f5978c --- /dev/null +++ b/.omo/run-continuation/ses_ff5998da7ffev009eRDsRGBQVR.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5998da7ffev009eRDsRGBQVR", + "updatedAt": "2026-08-16T11:59:24.138Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:59:24.138Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5abb74bffez58Aw12C0K34nU.json b/.omo/run-continuation/ses_ff5abb74bffez58Aw12C0K34nU.json new file mode 100644 index 0000000..2cc593e --- /dev/null +++ b/.omo/run-continuation/ses_ff5abb74bffez58Aw12C0K34nU.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5abb74bffez58Aw12C0K34nU", + "updatedAt": "2026-08-16T11:30:09.848Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:30:09.848Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5bad526ffew7mnXjFkk9NbCh.json b/.omo/run-continuation/ses_ff5bad526ffew7mnXjFkk9NbCh.json new file mode 100644 index 0000000..7e18023 --- /dev/null +++ b/.omo/run-continuation/ses_ff5bad526ffew7mnXjFkk9NbCh.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5bad526ffew7mnXjFkk9NbCh", + "updatedAt": "2026-08-16T11:13:20.829Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:13:20.829Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5bb071effeBAZIXtcU9ACju0.json b/.omo/run-continuation/ses_ff5bb071effeBAZIXtcU9ACju0.json new file mode 100644 index 0000000..b8af501 --- /dev/null +++ b/.omo/run-continuation/ses_ff5bb071effeBAZIXtcU9ACju0.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5bb071effeBAZIXtcU9ACju0", + "updatedAt": "2026-08-16T11:13:05.852Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:13:05.852Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5bc5037ffeBpV13WNB6iZSJt.json b/.omo/run-continuation/ses_ff5bc5037ffeBpV13WNB6iZSJt.json new file mode 100644 index 0000000..493eb7f --- /dev/null +++ b/.omo/run-continuation/ses_ff5bc5037ffeBpV13WNB6iZSJt.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5bc5037ffeBpV13WNB6iZSJt", + "updatedAt": "2026-08-16T11:15:26.263Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:15:26.263Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff5c68949ffeBXXChzUCaqm1z1.json b/.omo/run-continuation/ses_ff5c68949ffeBXXChzUCaqm1z1.json new file mode 100644 index 0000000..27949f9 --- /dev/null +++ b/.omo/run-continuation/ses_ff5c68949ffeBXXChzUCaqm1z1.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff5c68949ffeBXXChzUCaqm1z1", + "updatedAt": "2026-08-16T11:02:12.526Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T11:02:12.526Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff61a2bb2ffeLhEbxZ4beb2Sqj.json b/.omo/run-continuation/ses_ff61a2bb2ffeLhEbxZ4beb2Sqj.json new file mode 100644 index 0000000..e641972 --- /dev/null +++ b/.omo/run-continuation/ses_ff61a2bb2ffeLhEbxZ4beb2Sqj.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff61a2bb2ffeLhEbxZ4beb2Sqj", + "updatedAt": "2026-08-16T10:16:48.685Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T10:16:48.685Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff6302698ffemXaFsAlc5ztUUr.json b/.omo/run-continuation/ses_ff6302698ffemXaFsAlc5ztUUr.json new file mode 100644 index 0000000..cd0f562 --- /dev/null +++ b/.omo/run-continuation/ses_ff6302698ffemXaFsAlc5ztUUr.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff6302698ffemXaFsAlc5ztUUr", + "updatedAt": "2026-08-16T09:26:21.313Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T09:26:21.313Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff644c57effeO8mDJ4bJuvxUgS.json b/.omo/run-continuation/ses_ff644c57effeO8mDJ4bJuvxUgS.json new file mode 100644 index 0000000..75c0e6b --- /dev/null +++ b/.omo/run-continuation/ses_ff644c57effeO8mDJ4bJuvxUgS.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff644c57effeO8mDJ4bJuvxUgS", + "updatedAt": "2026-08-16T09:44:37.086Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T09:44:37.086Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff6458a01ffeTUrBLI8721QKU9.json b/.omo/run-continuation/ses_ff6458a01ffeTUrBLI8721QKU9.json new file mode 100644 index 0000000..cc873ef --- /dev/null +++ b/.omo/run-continuation/ses_ff6458a01ffeTUrBLI8721QKU9.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff6458a01ffeTUrBLI8721QKU9", + "updatedAt": "2026-08-16T08:40:17.021Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:40:17.021Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff64cb17cffe28Fja2X574xRtU.json b/.omo/run-continuation/ses_ff64cb17cffe28Fja2X574xRtU.json new file mode 100644 index 0000000..4456a1a --- /dev/null +++ b/.omo/run-continuation/ses_ff64cb17cffe28Fja2X574xRtU.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff64cb17cffe28Fja2X574xRtU", + "updatedAt": "2026-08-16T08:32:22.146Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:32:22.146Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff64d3e89ffeUJMQc1wzqAiMb2.json b/.omo/run-continuation/ses_ff64d3e89ffeUJMQc1wzqAiMb2.json new file mode 100644 index 0000000..c4834fb --- /dev/null +++ b/.omo/run-continuation/ses_ff64d3e89ffeUJMQc1wzqAiMb2.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff64d3e89ffeUJMQc1wzqAiMb2", + "updatedAt": "2026-08-16T08:31:52.071Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:31:52.071Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff652a22dffeEhR8SjP02SJSOg.json b/.omo/run-continuation/ses_ff652a22dffeEhR8SjP02SJSOg.json new file mode 100644 index 0000000..715585d --- /dev/null +++ b/.omo/run-continuation/ses_ff652a22dffeEhR8SjP02SJSOg.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff652a22dffeEhR8SjP02SJSOg", + "updatedAt": "2026-08-16T08:25:52.845Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:25:52.845Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff655d995ffebKTh58H5jjv0j4.json b/.omo/run-continuation/ses_ff655d995ffebKTh58H5jjv0j4.json new file mode 100644 index 0000000..653e381 --- /dev/null +++ b/.omo/run-continuation/ses_ff655d995ffebKTh58H5jjv0j4.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff655d995ffebKTh58H5jjv0j4", + "updatedAt": "2026-08-16T08:22:28.093Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:22:28.093Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff65e0161ffeUzxy8A88yk31Oe.json b/.omo/run-continuation/ses_ff65e0161ffeUzxy8A88yk31Oe.json new file mode 100644 index 0000000..6861616 --- /dev/null +++ b/.omo/run-continuation/ses_ff65e0161ffeUzxy8A88yk31Oe.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff65e0161ffeUzxy8A88yk31Oe", + "updatedAt": "2026-08-16T08:20:15.869Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:20:15.869Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff65ed810ffeN451M0eZm5EIs1.json b/.omo/run-continuation/ses_ff65ed810ffeN451M0eZm5EIs1.json new file mode 100644 index 0000000..f1983e2 --- /dev/null +++ b/.omo/run-continuation/ses_ff65ed810ffeN451M0eZm5EIs1.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff65ed810ffeN451M0eZm5EIs1", + "updatedAt": "2026-08-16T08:12:38.645Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:12:38.645Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff6647cb2ffek28hRnQ5Vw654p.json b/.omo/run-continuation/ses_ff6647cb2ffek28hRnQ5Vw654p.json new file mode 100644 index 0000000..d628ea9 --- /dev/null +++ b/.omo/run-continuation/ses_ff6647cb2ffek28hRnQ5Vw654p.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff6647cb2ffek28hRnQ5Vw654p", + "updatedAt": "2026-08-16T08:06:28.824Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:06:28.824Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff6693b2affeHMlQy10nXYGrfS.json b/.omo/run-continuation/ses_ff6693b2affeHMlQy10nXYGrfS.json new file mode 100644 index 0000000..9157f5b --- /dev/null +++ b/.omo/run-continuation/ses_ff6693b2affeHMlQy10nXYGrfS.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff6693b2affeHMlQy10nXYGrfS", + "updatedAt": "2026-08-16T08:01:11.900Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T08:01:11.900Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff66c6bd5ffe7btKkdHNFluI4N.json b/.omo/run-continuation/ses_ff66c6bd5ffe7btKkdHNFluI4N.json new file mode 100644 index 0000000..b1e0935 --- /dev/null +++ b/.omo/run-continuation/ses_ff66c6bd5ffe7btKkdHNFluI4N.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff66c6bd5ffe7btKkdHNFluI4N", + "updatedAt": "2026-08-16T07:57:48.896Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:57:48.896Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff676a4ebffeOyZpgWVMbyq29U.json b/.omo/run-continuation/ses_ff676a4ebffeOyZpgWVMbyq29U.json new file mode 100644 index 0000000..980a0a4 --- /dev/null +++ b/.omo/run-continuation/ses_ff676a4ebffeOyZpgWVMbyq29U.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff676a4ebffeOyZpgWVMbyq29U", + "updatedAt": "2026-08-16T09:02:44.380Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T09:02:44.380Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff67734c1ffewTdQMDzv6a8QUe.json b/.omo/run-continuation/ses_ff67734c1ffewTdQMDzv6a8QUe.json new file mode 100644 index 0000000..4f96acf --- /dev/null +++ b/.omo/run-continuation/ses_ff67734c1ffewTdQMDzv6a8QUe.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff67734c1ffewTdQMDzv6a8QUe", + "updatedAt": "2026-08-16T07:46:02.064Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:46:02.064Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff6776755ffe7PgHvixK0uqeiF.json b/.omo/run-continuation/ses_ff6776755ffe7PgHvixK0uqeiF.json new file mode 100644 index 0000000..a2d748d --- /dev/null +++ b/.omo/run-continuation/ses_ff6776755ffe7PgHvixK0uqeiF.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff6776755ffe7PgHvixK0uqeiF", + "updatedAt": "2026-08-16T09:19:55.042Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T09:19:55.042Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff67d3113ffecv1ckGBjqU3jcJ.json b/.omo/run-continuation/ses_ff67d3113ffecv1ckGBjqU3jcJ.json new file mode 100644 index 0000000..587ae1f --- /dev/null +++ b/.omo/run-continuation/ses_ff67d3113ffecv1ckGBjqU3jcJ.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff67d3113ffecv1ckGBjqU3jcJ", + "updatedAt": "2026-08-16T10:17:36.533Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T10:17:36.533Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff68e9c70ffeLXKLDJX189CPei.json b/.omo/run-continuation/ses_ff68e9c70ffeLXKLDJX189CPei.json new file mode 100644 index 0000000..a85a642 --- /dev/null +++ b/.omo/run-continuation/ses_ff68e9c70ffeLXKLDJX189CPei.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff68e9c70ffeLXKLDJX189CPei", + "updatedAt": "2026-08-16T07:56:05.172Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:56:05.172Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff68f78a6ffeK6ibJQDIBWh290.json b/.omo/run-continuation/ses_ff68f78a6ffeK6ibJQDIBWh290.json new file mode 100644 index 0000000..c5f6c25 --- /dev/null +++ b/.omo/run-continuation/ses_ff68f78a6ffeK6ibJQDIBWh290.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff68f78a6ffeK6ibJQDIBWh290", + "updatedAt": "2026-08-16T07:19:25.791Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:19:25.791Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff691e537ffez8BjuacoXdNxTn.json b/.omo/run-continuation/ses_ff691e537ffez8BjuacoXdNxTn.json new file mode 100644 index 0000000..b786202 --- /dev/null +++ b/.omo/run-continuation/ses_ff691e537ffez8BjuacoXdNxTn.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff691e537ffez8BjuacoXdNxTn", + "updatedAt": "2026-08-16T07:16:46.925Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:16:46.925Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff692ba46ffeSU7aelQglhMeiF.json b/.omo/run-continuation/ses_ff692ba46ffeSU7aelQglhMeiF.json new file mode 100644 index 0000000..0229298 --- /dev/null +++ b/.omo/run-continuation/ses_ff692ba46ffeSU7aelQglhMeiF.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff692ba46ffeSU7aelQglhMeiF", + "updatedAt": "2026-08-16T07:15:52.386Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:15:52.386Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff69484cbffeAGa40ipKWklnzo.json b/.omo/run-continuation/ses_ff69484cbffeAGa40ipKWklnzo.json new file mode 100644 index 0000000..2ba6657 --- /dev/null +++ b/.omo/run-continuation/ses_ff69484cbffeAGa40ipKWklnzo.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff69484cbffeAGa40ipKWklnzo", + "updatedAt": "2026-08-16T07:13:57.013Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:13:57.013Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff69591a2ffeJSqVEUaax63aN2.json b/.omo/run-continuation/ses_ff69591a2ffeJSqVEUaax63aN2.json new file mode 100644 index 0000000..b5a54d2 --- /dev/null +++ b/.omo/run-continuation/ses_ff69591a2ffeJSqVEUaax63aN2.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff69591a2ffeJSqVEUaax63aN2", + "updatedAt": "2026-08-16T07:12:47.181Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:12:47.181Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff696846affeFWcEhNPTlWvmjH.json b/.omo/run-continuation/ses_ff696846affeFWcEhNPTlWvmjH.json new file mode 100644 index 0000000..7114f53 --- /dev/null +++ b/.omo/run-continuation/ses_ff696846affeFWcEhNPTlWvmjH.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff696846affeFWcEhNPTlWvmjH", + "updatedAt": "2026-08-16T07:11:44.030Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:11:44.030Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff697943bffeh1dX0uUQeg1qbc.json b/.omo/run-continuation/ses_ff697943bffeh1dX0uUQeg1qbc.json new file mode 100644 index 0000000..81f058f --- /dev/null +++ b/.omo/run-continuation/ses_ff697943bffeh1dX0uUQeg1qbc.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff697943bffeh1dX0uUQeg1qbc", + "updatedAt": "2026-08-16T07:10:34.453Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:10:34.453Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff69a5819ffekEdOqJumLRg5TQ.json b/.omo/run-continuation/ses_ff69a5819ffekEdOqJumLRg5TQ.json new file mode 100644 index 0000000..4979db8 --- /dev/null +++ b/.omo/run-continuation/ses_ff69a5819ffekEdOqJumLRg5TQ.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff69a5819ffekEdOqJumLRg5TQ", + "updatedAt": "2026-08-16T07:08:36.977Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T07:08:36.977Z" + } + } +} \ No newline at end of file diff --git a/.omo/run-continuation/ses_ff69c705cffe8KOKa207kILOAq.json b/.omo/run-continuation/ses_ff69c705cffe8KOKa207kILOAq.json new file mode 100644 index 0000000..00b9441 --- /dev/null +++ b/.omo/run-continuation/ses_ff69c705cffe8KOKa207kILOAq.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_ff69c705cffe8KOKa207kILOAq", + "updatedAt": "2026-08-16T09:46:47.522Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-08-16T09:46:47.522Z" + } + } +} \ No newline at end of file diff --git a/.probe/exa_mcp_probe.sh b/.probe/exa_mcp_probe.sh new file mode 100644 index 0000000..a59defd --- /dev/null +++ b/.probe/exa_mcp_probe.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Probe the Exa hosted MCP (same endpoint omo websearch uses) from plain HTTP. +set -u +URL="https://mcp.exa.ai/mcp?tools=web_search_exa" +H1='Content-Type: application/json' +H2='Accept: application/json, text/event-stream' + +echo "=== initialize ===" +INIT_RESP=$(curl -sS -i -m 30 "$URL" -X POST -H "$H1" -H "$H2" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"watchdog-probe","version":"0.1"}}}') +echo "$INIT_RESP" | head -30 +SID=$(echo "$INIT_RESP" | grep -i '^mcp-session-id:' | tr -d '\r' | awk '{print $2}') +echo "session-id: ${SID:-}" + +echo "=== notifications/initialized ===" +curl -sS -m 15 -o /dev/null -w '%{http_code}\n' "$URL" -X POST -H "$H1" -H "$H2" \ + ${SID:+-H "Mcp-Session-Id: $SID"} \ + -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' + +echo "=== tools/call web_search_exa ===" +curl -sS -m 60 "$URL" -X POST -H "$H1" -H "$H2" \ + ${SID:+-H "Mcp-Session-Id: $SID"} \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"web_search_exa","arguments":{"query":"sst opencode latest release 2026","numResults":3}}}' | head -c 2500 +echo diff --git a/.probe/manager-brief.md b/.probe/manager-brief.md new file mode 100644 index 0000000..98c8e80 --- /dev/null +++ b/.probe/manager-brief.md @@ -0,0 +1,77 @@ +# 总任务: blog-app 加 "AI Agent 升级迭代日报" (daily agent watchdog) + +你是 manager, 严格按你的工作流: plan 拆任务 -> build 串行执行 -> review 给正式 verdict. 自己不写代码. + +## Goal +给 Flask 博客应用 (workspace = /home/yi/opencode-blog-showcase/blog-app) 加一个后台日报功能: +每天自动抓 6 个 AI agent 项目的最新版本/发布/commit, 用 Exa web search 补充资料, 用本地 litellm 智能总结, +写 markdown 报告 + SQLite 存档, Flask /watchdog 页面展示最近 7 天报告并支持手动触发. + +## 已验证的环境事实 (build agent 直接用, 不要重新探索) + +1. **GitHub API** (无需 token, 剩余额度充足): + - latest release: `GET https://api.github.com/repos/{owner}/{repo}/releases/latest` (6 个 repo 全部 200) + - commits: `GET https://api.github.com/repos/{owner}/{repo}/commits?per_page=5` + - 注意: `sst/opencode` 返回 301 (已改名 anomalyco/opencode), requests 默认跟随重定向即可, 但报告里 repo 名仍写 sst/opencode +2. **npm registry**: `GET https://registry.npmjs.org/{pkg}/latest` 已验证 200 的包: `opencode-ai`, `@anthropic-ai/claude-code`, `@openai/codex`, `hermes-agent`, `oh-my-openagent`. `hindsight` 包存在但可能不是 vectorize-io/hindsight 那个项目 — 取回后检查 description 相关性, 不相关就跳过该项目的 npm 源 (单源失败不得中断整体). +3. **RSS/Atom**: `https://github.com/{owner}/{repo}/releases.atom` (sst/opencode 的 atom 也 301, requests 跟随即可). 用标准库 xml.etree.ElementTree 解析, **禁止装 feedparser**. +4. **Web search = Exa 托管 MCP** (omo websearch 同款, 免 API key, 已实测可用): + - 端点: `POST https://mcp.exa.ai/mcp?tools=web_search_exa` + - 协议: MCP streamable HTTP / JSON-RPC. 流程: + a. POST `initialize` (params: protocolVersion "2025-03-26", capabilities {}, clientInfo) — 响应头 `Mcp-Session-Id` 必须保存 + b. POST `notifications/initialized` (带 Mcp-Session-Id 头) + c. POST `tools/call`, name=`web_search_exa`, arguments=`{"query": "...", "numResults": 3}` (带 Mcp-Session-Id 头) + - 请求头: `Content-Type: application/json`, `Accept: application/json, text/event-stream` + - 响应是 SSE 格式 (`event: message\ndata: {json}`), 解析 data 行的 JSON 取 `result.content[0].text` + - 参考实现: workspace 里 `.probe/exa_mcp_probe.sh` 有我实测通过的 curl 版 +5. **LLM 总结 = litellm 本地代理**: `POST http://127.0.0.1:4000/v1/chat/completions`, json=`{"model": "analysis", "messages": [...], "max_tokens": 1024}`, **无需 Authorization 头**, timeout=300. model 名就是字符串 "analysis" (Qwen3.7 Plus). 发给 LLM 的单项目数据截断 (release body ≤500 字符, commit message ≤200 字符), 总 prompt <12KB. 中文输出. +6. **系统时区 Asia/Shanghai (UTC+8)** — cron `0 9 * * *` 即每天早 9 点. +7. **Flask app 现状**: app.py (561 行) 已有 get_db() (projects.db 只读), /analyze SSE 流式等. 运行方式: `/usr/bin/python3 app.py`, host 0.0.0.0 port 8090. 现有模板在 templates/, 静态在 static/. 部署脚本 scripts/deploy.sh 已存在 (kill + 重启 + 健康检查), 不要重写它, 但需要时可以读它参考. +8. **opencode 写文件权限**: agent 的 write/edit 工具只能写 workspace (/home/yi/opencode-blog-showcase/blog-app) 内部. 所有代码文件都必须落在 workspace 里. 运行时目录 (data/, logs/) 由 python 脚本运行时 os.makedirs 创建, 不是 agent 用 write 工具创建. 任何临时/测试文件落到 workspace 内 `./.review-tmp/`. + +## 架构决策 (已定, 不许改) + +- **新文件 `scripts/daily_watchdog.py`**: 独立可执行脚本 (`/usr/bin/python3 scripts/daily_watchdog.py`), 做全部 抓取->web search 补充->LLM 总结->写报告->存 SQLite. 退出码: 全失败=1, 部分失败=0 但报告里标注. +- **SQLite**: `~/opencode-blog-showcase/data/watchdog.db` (脚本运行时创建目录). 表自己设计 (建议 reports 表: date, markdown, meta_json, created_at + 唯一索引). **绝对不许碰 projects.db**. +- **报告文件**: `~/opencode-blog-showcase/logs/daily-agent-watchdog-YYYY-MM-DD.md`, 同时内容存 watchdog.db. +- **Flask 路由** (加在 app.py, 风格与现有路由一致): + - `GET /watchdog` — 从 watchdog.db 读最近 7 天报告渲染 templates/watchdog.html (base.html extend, 导航与现有页面一致); 没有报告时显示空态 + 立即跑按钮 + - `POST /watchdog/run` — subprocess 后台起 daily_watchdog.py (不等结果), redirect 回 /watchdog 并 flash "已触发, 约 1-2 分钟后刷新". 简单防重: 60 秒内重复 POST 返回 429. + - base.html 导航加 "日报" 链接 +- **cron**: `scripts/install_watchdog_cron.sh` — 幂等地往当前用户 crontab 加 `0 9 * * * cd /home/yi/opencode-blog-showcase/blog-app && /usr/bin/python3 scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog.log 2>&1` (先 crontab -l 检查已存在则跳过). 脚本由 build agent 写好, **由 review agent 或上层 worker 执行**, build 阶段不执行. +- **失败告警**: 单项目/单源失败 -> 报告里该项标 "⚠️ 抓取失败: 原因", 继续其他项目. 整体失败 (如 LLM 挂) -> 仍写一份含错误说明的报告 + log 里 ERROR 行. 任何情况下脚本不得因单个源失败而崩溃退出非零 (除非连一个项目都没抓到). + +## 监控项目 (6 个, ★ = 当前 setup 装了) + +| repo | npm 包 | ★ | 备注 | +|---|---|---|---| +| sst/opencode | opencode-ai | ★ | 已装 1.18.14 (301->anomalyco/opencode) | +| anthropics/claude-code | @anthropic-ai/claude-code | ★ | | +| openai/codex | @openai/codex | | | +| NousResearch/hermes-agent | hermes-agent | ★ | | +| code-yeongyu/oh-my-openagent | oh-my-openagent | ★ | | +| vectorize-io/hindsight | hindsight (需验证相关性, 不相关则跳过 npm) | ★ | | + +## 报告格式 (每项目必须有) +- latest version, last release date, 1 句中文 changelog 摘要 (LLM 生成), breaking change 标记 (LLM 判断 true/false + 依据), ★ 相关性说明 +- 报告头部: 日期, 数据源清单 (实际成功用了哪几个), 各项目一节 +- **≥30% 的报告内容必须来自 web search 补充** (每项目至少 1 条 Exa 搜索结果摘要进报告, 标注来源 URL) + +## Acceptance (review 必须逐项验证, 全过才 PASS) +1. cron 安装脚本存在且幂等, 安装后 `crontab -l` 能看到 9:00 条目 (review 时实际执行安装脚本验证) +2. 数据源 ≥5: GitHub Releases API / GitHub commits API / npm registry / Exa web search / GitHub releases.atom — 报告头部的数据源清单要真列出 +3. 监控 6 项目, 报告每项含 version/date/中文摘要/breaking 标记/★ +4. `scripts/daily_watchdog.py` 真实跑一遍 (review agent 执行), 生成真报告 (禁止 mock), review 报告里必须附至少 1 段真实 curl/脚本输出证据 +5. Flask /watchdog 渲染最近 7 天 + POST /watchdog/run 手动触发可用 (review agent 用 deploy.sh 重启后 curl 验证 127.0.0.1:8090 和 192.168.2.25:8090) +6. 无新 pip 依赖 (只用 flask/requests/sqlite3/标准库) + +## 禁止 (越界=FAIL) +- 禁止改 litellm 配置 / restart litellm-proxy / llama-server +- 禁止改 projects.db schema, 禁止改 scripts/fetch_data.py +- 禁止碰 /home/yi/wxhook, /home/yi/.litellm, /home/yi/opencode, omo 安装目录和配置 +- 禁止 mock 数据 / 预生成报告 / 用云端 LLM 替代 litellm analysis +- 禁止 pip install 任何新包 +- 禁止删改 .probe/ 目录外的现有文件 (app.py 只做新增式修改: 加路由加导航, 不动现有路由逻辑) + +## 输出 +按你的 STEP/FINAL 格式汇报. FINAL 里 deliverable_path 指向 workspace 根. diff --git a/.probe/run-sisyphus-b2b3.sh b/.probe/run-sisyphus-b2b3.sh new file mode 100644 index 0000000..6acc3fe --- /dev/null +++ b/.probe/run-sisyphus-b2b3.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# t_f1f53c88: fork omo Sisyphus (default agent, NO --agent) with .sisyphus-brief-b2b3.md +set -u +export HOME=/home/yi +cd /home/yi/opencode-blog-showcase/blog-app +TS=$(date +%H%M%S) +LOG=/home/yi/opencode-blog-showcase/logs/sisyphus-b2b3-${TS}.ndjson +echo "log: $LOG" +opencode run --attach http://127.0.0.1:4096 \ + --model litellm-local/coding \ + --dir /home/yi/opencode-blog-showcase/blog-app \ + --format json \ + "$(cat .sisyphus-brief-b2b3.md)" > "$LOG" 2>&1 +echo "exit=$? log=$LOG" diff --git a/.review-tmp/analyze-inline.js b/.review-tmp/analyze-inline.js new file mode 100644 index 0000000..9c13215 --- /dev/null +++ b/.review-tmp/analyze-inline.js @@ -0,0 +1,137 @@ + +// State +let currentEventSource = null; +let currentTab = 'observation'; // Track which tab we're writing to +let currentSection = 'reasoning'; // 'reasoning' | 'conclusion', 由 SSE section 事件驱动 + +// DOM elements +const scopeSelect = document.getElementById('scopeSelect'); +const questionInput = document.getElementById('questionInput'); +const startBtn = document.getElementById('startBtn'); +const presetBtns = document.querySelectorAll('.preset-btn'); + +const observationContent = document.getElementById('observationContent'); +const promptContent = document.getElementById('promptContent'); +const reasoningContent = document.getElementById('reasoningContent'); +const conclusionContent = document.getElementById('conclusionContent'); + +// Programmatic tab switching (Bootstrap tabs) +function switchTab(name) { + const btn = document.getElementById(name + '-tab'); + if (btn) btn.click(); +} + +// Tab switching +document.querySelectorAll('#outputTabs button').forEach(button => { + button.addEventListener('click', (e) => { + const tabId = e.target.id.replace('-tab', ''); + currentTab = tabId; + }); +}); + +// Preset button click +presetBtns.forEach(btn => { + btn.addEventListener('click', () => { + const preset = btn.dataset.preset; + startAnalysis(null, preset); + }); +}); + +// Start button click +startBtn.addEventListener('click', () => { + const question = questionInput.value.trim(); + if (!question) { + alert('请输入自定义问题或选择预设分析'); + return; + } + startAnalysis(question, null); +}); + +function startAnalysis(question, preset) { + // Close existing connection + if (currentEventSource) { + currentEventSource.close(); + currentEventSource = null; + } + + // Reset content + observationContent.innerHTML = '
正在收集上下文...
'; + promptContent.innerHTML = '
等待...
'; + reasoningContent.innerHTML = '
等待...
'; + conclusionContent.innerHTML = '
等待...
'; + currentSection = 'reasoning'; + + // Build URL + const scope = scopeSelect.value; + const params = new URLSearchParams({scope}); + if (question) params.append('question', question); + if (preset) params.append('preset', preset); + + const url = `/api/analyze/stream?${params.toString()}`; + + // Start SSE + currentEventSource = new EventSource(url); + + // Handle step events + currentEventSource.addEventListener('step', (e) => { + const data = JSON.parse(e.data); + const step = data.step; + const content = data.content; + + if (step === 'context') { + observationContent.innerHTML = `
${escapeHtml(content)}
`; + } else if (step === 'prompt') { + promptContent.innerHTML = `
${escapeHtml(content)}
`; + } else if (step === 'complete') { + // 追加提示, 不覆盖已流式写入的结论内容 + conclusionContent.innerHTML += `
${escapeHtml(content)}
`; + currentEventSource.close(); + currentEventSource = null; + } + }); + + // Section 事件: 后端识别 ===REASONING=== / ===CONCLUSION=== 后显式下发 + currentEventSource.addEventListener('reasoning_start', () => { + currentSection = 'reasoning'; + reasoningContent.innerHTML = ''; + switchTab('reasoning'); + }); + + currentEventSource.addEventListener('conclusion_start', () => { + currentSection = 'conclusion'; + conclusionContent.innerHTML = ''; + switchTab('conclusion'); + }); + + // Handle token events (streaming LLM output, 分隔符已被后端剥离) + currentEventSource.addEventListener('token', (e) => { + const data = JSON.parse(e.data); + const text = data.text; + const pane = currentSection === 'conclusion' ? conclusionContent : reasoningContent; + if (pane.querySelector('.text-muted')) pane.innerHTML = ''; + pane.innerHTML += escapeHtml(text); + pane.scrollTop = pane.scrollHeight; + }); + + // Handle errors + currentEventSource.addEventListener('error', (e) => { + const data = JSON.parse(e.data); + const errorMsg = data.error || '未知错误'; + observationContent.innerHTML = `
错误: ${escapeHtml(errorMsg)}
`; + currentEventSource.close(); + currentEventSource = null; + }); + + currentEventSource.onerror = (e) => { + observationContent.innerHTML = `
连接错误, 请检查后端日志
`; + currentEventSource.close(); + currentEventSource = null; + }; +} + +// Helper: escape HTML +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/.review-tmp/analyze_page.html b/.review-tmp/analyze_page.html new file mode 100644 index 0000000..1bbc91d --- /dev/null +++ b/.review-tmp/analyze_page.html @@ -0,0 +1,296 @@ + + + + + + AI 分析 - AI Agent 项目追踪 + + + + + + +
+ +

AI 分析

+ + +
+
+
+ +
+ + +
+ + +
+ +
+ + + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+
+
+
等待分析开始...
+
+
+
+
+
等待分析开始...
+
+
+
+
+
等待分析开始...
+
+
+
+
+
等待分析开始...
+
+
+
+
+
+ +
+ +
+
+ AI Agent 项目追踪 - Powered by Flask +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/.review-tmp/build1-summary.md b/.review-tmp/build1-summary.md new file mode 100644 index 0000000..f9a3c87 --- /dev/null +++ b/.review-tmp/build1-summary.md @@ -0,0 +1,106 @@ +# Build1 Patch Summary — scripts/daily_watchdog.py + +Date: 2026-08-16 · Workspace: /home/yi/opencode-blog-showcase/blog-app + +## Patch summary (surgical edits, file 596→853 lines) + +1. **Per-project source accounting** + - `collect_all()` now returns `(project_results, source_counts: dict[str,int], sources_failed)`; each project dict gains `sources_used: []` populated on every successful source. + - Report header `## 数据源` lists all 5 canonical sources with success counts, e.g. `- GitHub Releases API (6/6)`, `- GitHub releases.atom (4/6)`. + - `meta_json` per project now includes `sources_used`; top-level gains `source_counts`. + +2. **Failure reasons** + - All 4 fetch functions return `(result, error)` tuples; `source_errors` entries are `": "` (reason ≤80 chars, newlines stripped via `_short_reason`). + - Project sections render `⚠️ 抓取失败: ` (failed projects: one line per source; partial projects: enriched npm / releases.atom / Exa lines). + - Atom timeouts classified as `超时 (github.com 限流/网络)`. + +3. **Atom hardening** + - `fetch_releases_atom`: timeout 45s (`ATOM_TIMEOUT`), ONE retry on `requests.exceptions.Timeout` (sleep 5s), never raises (returns `(None, reason)`). + +4. **Self-lock** + - `_watchdog_already_running()` via `pgrep -f daily_watchdog.py` excluding own pid, hardened to only count processes whose argv[0] is a python interpreter (avoids false positives from processes that merely mention the filename). At `main()` start: if running → log + return 0. + +5. **`--fix-if-missing`** + - `_today_report_ok(date)`: today's report exists in watchdog.db AND meta has ≥2 projects status != 'failed' → log `today's report already OK — skip full run (--fix-if-missing)`, exit 0 fast. Default behavior unchanged. + +6. **Kept**: LLM integration (POST http://127.0.0.1:4000/v1/chat/completions, model="analysis", no auth, timeout=300) — already present in the file; report sections unchanged except 数据源 header + enriched ⚠️ lines; `**LLM 摘要:**` renamed to `**LLM 总结:**` and moved to render for ALL projects (previously only rendered when GitHub data existed); exit codes unchanged (1 only if ALL projects failed). No new imports beyond stdlib + requests. + +## Verification evidence + +### py_compile +``` +PY_COMPILE_OK +``` + +### REAL full run (final, exit code 0) +Command: `HOME=/home/yi /usr/bin/python3 scripts/daily_watchdog.py` +``` +EXIT=0 +2026-08-16 16:52:27 [INFO] Starting daily watchdog for 2026-08-16 +2026-08-16 16:52:34 [INFO] Exa MCP initialized, session=250675fa-... +2026-08-16 16:57:03 [WARNING] Atom feed timeout for code-yeongyu/oh-my-openagent (attempt 1/2) +2026-08-16 16:58:00 [WARNING] Atom feed timeout for code-yeongyu/oh-my-openagent (attempt 2/2) +2026-08-16 16:59:47 [WARNING] Atom feed timeout for vectorize-io/hindsight (attempt 1/2) +2026-08-16 17:00:37 [WARNING] Atom feed timeout for vectorize-io/hindsight (attempt 2/2) +2026-08-16 17:01:29 [INFO] Report saved to /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog-2026-08-16.md +2026-08-16 17:01:29 [INFO] SQLite record saved to /home/yi/opencode-blog-showcase/data/watchdog.db +2026-08-16 17:01:29 [INFO] Daily watchdog completed successfully +``` +Full stdout: `./.review-tmp/build1-run.log` (tail) / `./.review-tmp/build1-run-final.log` (full). + +### 数据源 header (verbatim) +``` +## 数据源 + +- GitHub Releases API (6/6) +- GitHub commits API (6/6) +- npm registry (5/6) +- GitHub releases.atom (4/6) +- Exa web search (6/6) +``` + +### One-line LLM 总结 excerpt per project (all real Chinese, 0 placeholders) +- sst/opencode: 本次 v1.18.18 版本更新主要聚焦于核心功能的缺陷修复与模型兼容性优化… +- anthropics/claude-code: 本次更新为 claude-code 增加了对 GitLab 合并请求 URL 的支持,并引入了可… +- openai/codex: 本次0.147.0版本更新引入了便携式Agent插件安装与跨目录搜索功能,支持… +- NousResearch/hermes-agent: 本次更新为 Hermes Agent v0.20.1 补丁版本,主要整合了自 v0.20.0 以来的大量… +- code-yeongyu/oh-my-openagent: 本次更新发布了 v5.0.0-beta.7 版本,标志着项目向全新的 OMO Native CLI 架构… +- vectorize-io/hindsight: 本次 v0.9.1 更新主要聚焦于系统稳定性和功能扩展,修复了引擎并发死… + +Report counts: `**LLM 总结:**` ×6, `**Breaking Changes:**` ×6, `★ **相关性:**` ×6, `⚠️ 抓取失败:` ×2, placeholder ("LLM 未集成"/"LLM 总结失败") ×0. + +### Atom timeout classification (exercised live) +``` +**releases.atom:** ⚠️ 抓取失败: GitHub releases.atom — 超时 (github.com 限流/网络) +``` +(2 occurrences — oh-my-openagent and hindsight both timed out twice; retry logic logged `attempt 1/2` → `attempt 2/2`.) + +### --fix-if-missing (exit 0 fast) +``` +2026-08-16 17:01:41 [INFO] today's report already OK — skip full run (--fix-if-missing) +FIX_EXIT=0 +``` + +### Self-lock idle check +``` +idle _watchdog_already_running = False +IDLE_EXIT=0 +``` +Positive case also demonstrated live: while another instance ran, the script logged `Another daily_watchdog instance is already running — skip` and exited 0. + +### meta_json (watchdog.db, 2026-08-16) +``` +source_counts: {'GitHub Releases API': 6, 'GitHub commits API': 6, 'npm registry': 5, 'GitHub releases.atom': 4, 'Exa web search': 6} + opencode ok ['GitHub Releases API', 'GitHub commits API', 'npm registry', 'GitHub releases.atom', 'Exa web search'] + claude-code ok [...same 5...] + codex ok [...same 5...] + hermes-agent ok [...same 5...] + oh-my-openagent partial ['GitHub Releases API', 'GitHub commits API', 'npm registry', 'Exa web search'] + hindsight partial ['GitHub Releases API', 'GitHub commits API', 'Exa web search'] +``` + +## Anomalies +1. **Concurrent agent**: another opencode agent (PID 1097346, task "B2 LLM 集成 + B3 Flask/cron") was editing the same file and repeatedly ran the script (5+ times) during this build. It had already added the LLM integration (`summarize_with_llm` real POST) before my patches; I kept it and patched around it. Its runs repeatedly triggered my self-lock (correct behavior) and overwrote the report/DB between my runs — final state is from MY run. +2. **Self-lock false positive**: `pgrep -f daily_watchdog.py` matched the concurrent agent's own process (its task text mentions the filename). Hardened the lock to only count processes whose argv[0] is a python interpreter; idle check now returns False correctly. +3. **GitHub API rate limit**: during the 16:16 run, api.github.com returned 403 rate-limit for all 6 projects (exhausted by the concurrent agent's repeated runs). The report honestly showed `⚠️ 抓取失败: GitHub Releases API — 403 Client Error: rate limit exceeded…` (truncated ≤80 chars). By the 16:52 run the limit had reset (6/6). +4. **releases.atom intermittent timeouts** confirmed live (as background noted): 45s timeout + 1 retry handled them; classified as `超时 (github.com 限流/网络)`. \ No newline at end of file diff --git a/.review-tmp/build2-summary.md b/.review-tmp/build2-summary.md new file mode 100644 index 0000000..84e1e25 --- /dev/null +++ b/.review-tmp/build2-summary.md @@ -0,0 +1,77 @@ +# Build2 Verification Summary — Flask /watchdog page + manual trigger + +## What was added / changed + +### app.py (additive to existing routes; watchdog section lines 225-320) +- `WATCHDOG_DB = os.path.expanduser("~/opencode-blog-showcase/data/watchdog.db")` — line 23 +- `WATCHDOG_RUN_LOG` — lines 27-29 +- `get_watchdog_db()` — line 234: opens watchdog.db **read-only** via `sqlite3.connect(f"file:{WATCHDOG_DB}?mode=ro", uri=True)` (line 237) +- `_load_watchdog_reports(days=7)` — line 243: returns `[]` when db/table missing (never 500); parses `meta_json` into `meta` +- `GET /watchdog` (`page_watchdog`) — line 267: renders `watchdog.html` with up to 7 reports +- `_is_watchdog_running()` — line 273: pgrep guard against duplicate runs +- `POST /watchdog/run` (`watchdog_run`) — line 294: + - 60s single-flight via module-level `_last_watchdog_trigger_ts` (line 230); repeat within 60s → `429 "操作太频繁, 请稍后再试"` (line 299) + - timestamp set **before** the running-check so the cooldown engages even when the script is already running (fix applied during this build) + - `os.makedirs(os.path.dirname(WATCHDOG_RUN_LOG), exist_ok=True)` (line 308) before opening the run log + - `subprocess.Popen(["/usr/bin/python3", scripts/daily_watchdog.py], cwd=BASE_DIR, env HOME=/home/yi, stdout=log_f, stderr=STDOUT, start_new_session=True)` — no wait + - flash `"已触发, 约 1-2 分钟后刷新页面查看新报告"` (line 317) → redirect `/watchdog` + +### templates/watchdog.html +- Title `

升级迭代日报

` (line 7) +- "立即运行" form POST /watchdog/run (lines 10-12) +- Empty state: `alert alert-info` "暂无报告, 点击立即运行生成第一份日报" (line 17) +- Report cards: header = date + created_at, body = `
` of markdown (lines 21-45)
+
+### templates/base.html
+- Nav link `` after 全局搜索 (line 22)
+
+## Verification evidence (verbatim key lines)
+
+### 1. py_compile
+```
+PY_COMPILE_OK
+```
+
+### 2. deploy
+```
+local:200
+```
+
+### 3. GET /watchdog
+```
+http:200
+升级迭代日报
+2026-08-16
+sst/opencode
+立即运行
+href="/watchdog">日报
+```
+
+### 4. POST /watchdog/run (first) + immediate repeat
+```
+http:200
+已在运行中, 本次触发被忽略
+http:429
+操作太频繁, 请稍后再试
+```
+- The "已触发" spawn path was proven earlier: run log line 1 `2026-08-16 17:05:19 [INFO] Starting daily watchdog for 2026-08-16` was spawned by the first POST (script self-locks; left running per spec).
+
+### 5. Regression
+```
+/ -> 200
+/projects -> 200
+/search -> 200
+/analyze -> 200
+{"total_issues":14,"total_projects":3,"total_releases":14,"total_stars":430166}
+```
+
+### 6. LAN
+```
+lan:200
+```
+
+## Issues encountered
+1. **`curl -X POST -L` returns 405 on the redirect follow**: `-X POST` forces curl to re-send POST on the 302 → `/watchdog` only accepts GET → 405. The POST itself succeeded (spawned the script). Used `-d '' -L` (follows 302 with GET) for correct verification.
+2. **Sibling build1 agent ran concurrently**: it kept POSTing `/watchdog/run` for its own verification, re-triggering the script and resetting the 60s cooldown (causing intermittent 429s on the "first" POST) and overwrote `templates/base.html` at 17:06:48 (reverting the nav label to "Watchdog"). Re-applied the `日报` label and redeployed; final state verified.
+3. **Cooldown gap fixed**: when the script was already running, the "已在运行中" branch returned before setting `_last_watchdog_trigger_ts`, so repeated POSTs never hit 429. Moved the timestamp assignment before the running-check.
+4. **GitHub API rate limit**: the sibling's repeated script runs exhausted the GitHub commits API (403 in run log) — unrelated to this build's code.
\ No newline at end of file
diff --git a/.review-tmp/cron-after1.txt b/.review-tmp/cron-after1.txt
new file mode 100644
index 0000000..e1f3289
--- /dev/null
+++ b/.review-tmp/cron-after1.txt
@@ -0,0 +1,15 @@
+@reboot sleep 20 && /home/yi/start-headless.sh
+# 户部·点卯催工监控(钦天监替代)
+*/10 * * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --check >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+0 */2 * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --full >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+*/1 * * * * cd /home/yi && python3 /home/yi/档案管理/scripts/处理流程.py process >> /home/yi/档案管理/process.log 2>&1
+0 3 * * 0 /home/yi/scripts/docker-cleanup.sh >> /home/yi/logs/docker-cleanup.log 2>&1
+*/5 * * * * /home/yi/scripts/health-monitor.sh >> /home/yi/logs/health-monitor.log 2>&1
+# 紫微天朝 P2-4: AGE→SQLite doc_entities 增量同步 (每 4 小时)
+0 */4 * * * /home/yi/kanyu-age-sync-pvc.sh >> /tmp/kanyu_age_sync.log 2>&1
+0 3 * * * /home/yi/bin/hindsight-cleanup-stuck.sh >> /tmp/hindsight-cleanup.log 2>&1
+*/5 * * * * /usr/bin/python3 /home/yi/yuanshu-ziwei/scripts/cleanup_legacy_tasks.py --execute >/tmp/cleanup.log 2>&1
+0 2 * * * /home/yi/sishu/scripts/sishu_pg_backup.sh backup >> /var/log/sishu_pg_backup.log 2>&1
+0 3 * * * /home/yi/.local/bin/sishu-redis-cleanup-cron
+*/2 * * * * /usr/local/bin/fix-k3s-containerd.sh >> /var/log/fix-k3s-containerd.log 2>&1
+0 9 * * * /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-watchdog-cron.log 2>&1
diff --git a/.review-tmp/cron-after2.txt b/.review-tmp/cron-after2.txt
new file mode 100644
index 0000000..e1f3289
--- /dev/null
+++ b/.review-tmp/cron-after2.txt
@@ -0,0 +1,15 @@
+@reboot sleep 20 && /home/yi/start-headless.sh
+# 户部·点卯催工监控(钦天监替代)
+*/10 * * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --check >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+0 */2 * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --full >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+*/1 * * * * cd /home/yi && python3 /home/yi/档案管理/scripts/处理流程.py process >> /home/yi/档案管理/process.log 2>&1
+0 3 * * 0 /home/yi/scripts/docker-cleanup.sh >> /home/yi/logs/docker-cleanup.log 2>&1
+*/5 * * * * /home/yi/scripts/health-monitor.sh >> /home/yi/logs/health-monitor.log 2>&1
+# 紫微天朝 P2-4: AGE→SQLite doc_entities 增量同步 (每 4 小时)
+0 */4 * * * /home/yi/kanyu-age-sync-pvc.sh >> /tmp/kanyu_age_sync.log 2>&1
+0 3 * * * /home/yi/bin/hindsight-cleanup-stuck.sh >> /tmp/hindsight-cleanup.log 2>&1
+*/5 * * * * /usr/bin/python3 /home/yi/yuanshu-ziwei/scripts/cleanup_legacy_tasks.py --execute >/tmp/cleanup.log 2>&1
+0 2 * * * /home/yi/sishu/scripts/sishu_pg_backup.sh backup >> /var/log/sishu_pg_backup.log 2>&1
+0 3 * * * /home/yi/.local/bin/sishu-redis-cleanup-cron
+*/2 * * * * /usr/local/bin/fix-k3s-containerd.sh >> /var/log/fix-k3s-containerd.log 2>&1
+0 9 * * * /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-watchdog-cron.log 2>&1
diff --git a/.review-tmp/cron-before.txt b/.review-tmp/cron-before.txt
new file mode 100644
index 0000000..e1f3289
--- /dev/null
+++ b/.review-tmp/cron-before.txt
@@ -0,0 +1,15 @@
+@reboot sleep 20 && /home/yi/start-headless.sh
+# 户部·点卯催工监控(钦天监替代)
+*/10 * * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --check >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+0 */2 * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --full >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+*/1 * * * * cd /home/yi && python3 /home/yi/档案管理/scripts/处理流程.py process >> /home/yi/档案管理/process.log 2>&1
+0 3 * * 0 /home/yi/scripts/docker-cleanup.sh >> /home/yi/logs/docker-cleanup.log 2>&1
+*/5 * * * * /home/yi/scripts/health-monitor.sh >> /home/yi/logs/health-monitor.log 2>&1
+# 紫微天朝 P2-4: AGE→SQLite doc_entities 增量同步 (每 4 小时)
+0 */4 * * * /home/yi/kanyu-age-sync-pvc.sh >> /tmp/kanyu_age_sync.log 2>&1
+0 3 * * * /home/yi/bin/hindsight-cleanup-stuck.sh >> /tmp/hindsight-cleanup.log 2>&1
+*/5 * * * * /usr/bin/python3 /home/yi/yuanshu-ziwei/scripts/cleanup_legacy_tasks.py --execute >/tmp/cleanup.log 2>&1
+0 2 * * * /home/yi/sishu/scripts/sishu_pg_backup.sh backup >> /var/log/sishu_pg_backup.log 2>&1
+0 3 * * * /home/yi/.local/bin/sishu-redis-cleanup-cron
+*/2 * * * * /usr/local/bin/fix-k3s-containerd.sh >> /var/log/fix-k3s-containerd.log 2>&1
+0 9 * * * /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-watchdog-cron.log 2>&1
diff --git a/.review-tmp/crontab-review-backup.txt b/.review-tmp/crontab-review-backup.txt
new file mode 100644
index 0000000..e1f3289
--- /dev/null
+++ b/.review-tmp/crontab-review-backup.txt
@@ -0,0 +1,15 @@
+@reboot sleep 20 && /home/yi/start-headless.sh
+# 户部·点卯催工监控(钦天监替代)
+*/10 * * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --check >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+0 */2 * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --full >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+*/1 * * * * cd /home/yi && python3 /home/yi/档案管理/scripts/处理流程.py process >> /home/yi/档案管理/process.log 2>&1
+0 3 * * 0 /home/yi/scripts/docker-cleanup.sh >> /home/yi/logs/docker-cleanup.log 2>&1
+*/5 * * * * /home/yi/scripts/health-monitor.sh >> /home/yi/logs/health-monitor.log 2>&1
+# 紫微天朝 P2-4: AGE→SQLite doc_entities 增量同步 (每 4 小时)
+0 */4 * * * /home/yi/kanyu-age-sync-pvc.sh >> /tmp/kanyu_age_sync.log 2>&1
+0 3 * * * /home/yi/bin/hindsight-cleanup-stuck.sh >> /tmp/hindsight-cleanup.log 2>&1
+*/5 * * * * /usr/bin/python3 /home/yi/yuanshu-ziwei/scripts/cleanup_legacy_tasks.py --execute >/tmp/cleanup.log 2>&1
+0 2 * * * /home/yi/sishu/scripts/sishu_pg_backup.sh backup >> /var/log/sishu_pg_backup.log 2>&1
+0 3 * * * /home/yi/.local/bin/sishu-redis-cleanup-cron
+*/2 * * * * /usr/local/bin/fix-k3s-containerd.sh >> /var/log/fix-k3s-containerd.log 2>&1
+0 9 * * * /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-watchdog-cron.log 2>&1
diff --git a/.review-tmp/crontab.bak b/.review-tmp/crontab.bak
new file mode 100644
index 0000000..6a907bb
--- /dev/null
+++ b/.review-tmp/crontab.bak
@@ -0,0 +1,14 @@
+@reboot sleep 20 && /home/yi/start-headless.sh
+# 户部·点卯催工监控(钦天监替代)
+*/10 * * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --check >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+0 */2 * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --full >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+*/1 * * * * cd /home/yi && python3 /home/yi/档案管理/scripts/处理流程.py process >> /home/yi/档案管理/process.log 2>&1
+0 3 * * 0 /home/yi/scripts/docker-cleanup.sh >> /home/yi/logs/docker-cleanup.log 2>&1
+*/5 * * * * /home/yi/scripts/health-monitor.sh >> /home/yi/logs/health-monitor.log 2>&1
+# 紫微天朝 P2-4: AGE→SQLite doc_entities 增量同步 (每 4 小时)
+0 */4 * * * /home/yi/kanyu-age-sync-pvc.sh >> /tmp/kanyu_age_sync.log 2>&1
+0 3 * * * /home/yi/bin/hindsight-cleanup-stuck.sh >> /tmp/hindsight-cleanup.log 2>&1
+*/5 * * * * /usr/bin/python3 /home/yi/yuanshu-ziwei/scripts/cleanup_legacy_tasks.py --execute >/tmp/cleanup.log 2>&1
+0 2 * * * /home/yi/sishu/scripts/sishu_pg_backup.sh backup >> /var/log/sishu_pg_backup.log 2>&1
+0 3 * * * /home/yi/.local/bin/sishu-redis-cleanup-cron
+*/2 * * * * /usr/local/bin/fix-k3s-containerd.sh >> /var/log/fix-k3s-containerd.log 2>&1
diff --git a/.review-tmp/crontab.bak2 b/.review-tmp/crontab.bak2
new file mode 100644
index 0000000..e0467b7
--- /dev/null
+++ b/.review-tmp/crontab.bak2
@@ -0,0 +1,15 @@
+@reboot sleep 20 && /home/yi/start-headless.sh
+# 户部·点卯催工监控(钦天监替代)
+*/10 * * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --check >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+0 */2 * * * cd /home/yi/.openclaw/workspace-hubu && python3 scripts/check_task_progress.py --full >> /home/yi/.openclaw/workspace-hubu/logs/task_monitor_cron.log 2>&1
+*/1 * * * * cd /home/yi && python3 /home/yi/档案管理/scripts/处理流程.py process >> /home/yi/档案管理/process.log 2>&1
+0 3 * * 0 /home/yi/scripts/docker-cleanup.sh >> /home/yi/logs/docker-cleanup.log 2>&1
+*/5 * * * * /home/yi/scripts/health-monitor.sh >> /home/yi/logs/health-monitor.log 2>&1
+# 紫微天朝 P2-4: AGE→SQLite doc_entities 增量同步 (每 4 小时)
+0 */4 * * * /home/yi/kanyu-age-sync-pvc.sh >> /tmp/kanyu_age_sync.log 2>&1
+0 3 * * * /home/yi/bin/hindsight-cleanup-stuck.sh >> /tmp/hindsight-cleanup.log 2>&1
+*/5 * * * * /usr/bin/python3 /home/yi/yuanshu-ziwei/scripts/cleanup_legacy_tasks.py --execute >/tmp/cleanup.log 2>&1
+0 2 * * * /home/yi/sishu/scripts/sishu_pg_backup.sh backup >> /var/log/sishu_pg_backup.log 2>&1
+0 3 * * * /home/yi/.local/bin/sishu-redis-cleanup-cron
+*/2 * * * * /usr/local/bin/fix-k3s-containerd.sh >> /var/log/fix-k3s-containerd.log 2>&1
+0 9 * * * cd /home/yi/opencode-blog-showcase/blog-app && /usr/bin/python3 scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog.log 2>&1
diff --git a/.review-tmp/gen-extracted.py b/.review-tmp/gen-extracted.py
new file mode 100644
index 0000000..be0c3b7
--- /dev/null
+++ b/.review-tmp/gen-extracted.py
@@ -0,0 +1,128 @@
+def generate():
+    start_time = time.time()
+
+    yield f"event: step\ndata: {json.dumps({'step': 'context', 'content': f'已收集上下文: {data_summary}'}, ensure_ascii=False)}\n\n"
+    yield f"event: step\ndata: {json.dumps({'step': 'prompt', 'content': full_prompt_display}, ensure_ascii=False)}\n\n"
+
+    try:
+        resp = fake_post(
+            "http://127.0.0.1:4000/v1/chat/completions",
+            json={"model": "default", "messages": messages, "stream": True, "max_tokens": 1024},
+            stream=True,
+            timeout=120
+        )
+        resp.raise_for_status()
+
+        last_keepalive = time.time()
+        full_text = ""
+        # 滚动 buffer: 分隔符可能跨 chunk (如 "===REAS" + "ONING==="),
+        # 保留尾部最长分隔符前缀长度的字符, 不立即下发
+        DELIM_R = "===REASONING==="
+        DELIM_C = "===CONCLUSION==="
+        holdback = max(len(DELIM_R), len(DELIM_C)) - 1
+        pending = ""
+        reasoning_started = False
+        conclusion_started = False
+
+        def emit_token(t):
+            return f"event: token\ndata: {json.dumps({'text': t}, ensure_ascii=False)}\n\n"
+
+        for line in resp.iter_lines():
+            if not line:
+                continue
+            line = line.decode("utf-8") if isinstance(line, bytes) else line
+
+            now = time.time()
+            if now - last_keepalive > 15:
+                yield ": keepalive\n\n"
+                last_keepalive = now
+
+            if line.startswith("data: "):
+                chunk_data = line[6:]
+                if chunk_data.strip() == "[DONE]":
+                    break
+                try:
+                    chunk = json.loads(chunk_data)
+                    delta = chunk.get("choices", [{}])[0].get("delta", {})
+                    token_text = delta.get("content", "")
+                except json.JSONDecodeError:
+                    continue
+                if not token_text:
+                    continue
+
+                full_text += token_text
+                pending += token_text
+
+                # 循环剥离 pending 里确定安全的部分:
+                # 遇到完整分隔符 -> 发 section_start 事件, 丢弃分隔符;
+                # 遇到分隔符前缀后缀 -> 留在 pending 等待后续 chunk
+                while pending:
+                    idx_r = pending.find(DELIM_R)
+                    idx_c = pending.find(DELIM_C)
+                    # 找最早出现的完整分隔符
+                    idx = -1
+                    delim = None
+                    if idx_r != -1 and (idx_c == -1 or idx_r < idx_c):
+                        idx, delim = idx_r, DELIM_R
+                    elif idx_c != -1:
+                        idx, delim = idx_c, DELIM_C
+
+                    if idx != -1:
+                        # 分隔符前的正文是安全的, 直接下发
+                        if idx > 0:
+                            if not reasoning_started:
+                                reasoning_started = True
+                                yield f"event: reasoning_start\ndata: {{}}\n\n"
+                            yield emit_token(pending[:idx])
+                        pending = pending[idx + len(delim):]
+                        if delim == DELIM_R:
+                            if not reasoning_started:
+                                reasoning_started = True
+                                yield f"event: reasoning_start\ndata: {{}}\n\n"
+                        else:
+                            if not reasoning_started:
+                                # 防御: 模型跳过 REASONING 直接给 CONCLUSION
+                                reasoning_started = True
+                                yield f"event: reasoning_start\ndata: {{}}\n\n"
+                            if not conclusion_started:
+                                conclusion_started = True
+                                yield f"event: conclusion_start\ndata: {{}}\n\n"
+                        continue
+
+                    # 无完整分隔符: 检查尾部是否为某分隔符的前缀
+                    keep = 0
+                    tail = pending[-holdback:] if len(pending) > holdback else pending
+                    for d in (DELIM_R, DELIM_C):
+                        for k in range(min(len(tail), len(d) - 1), 0, -1):
+                            if tail.endswith(d[:k]):
+                                keep = max(keep, k)
+                                break
+                    safe = pending[:len(pending) - keep] if keep else pending
+                    pending = pending[len(safe):]
+                    if not safe:
+                        break
+                    if not reasoning_started:
+                        reasoning_started = True
+                        yield f"event: reasoning_start\ndata: {{}}\n\n"
+                    yield emit_token(safe)
+
+        # 流结束: 残余 pending 全部下发
+        if pending:
+            if not reasoning_started:
+                reasoning_started = True
+                yield f"event: reasoning_start\ndata: {{}}\n\n"
+            yield emit_token(pending)
+            pending = ""
+
+    except Exception as e:
+        yield f"event: error\ndata: {json.dumps({'error': f'LLM call failed: {str(e)}'}, ensure_ascii=False)}\n\n"
+        return
+
+    # 防御: 模型没给分隔符时补发 section_start, 保证前端 tab 一定有入口
+    if not reasoning_started:
+        yield f"event: reasoning_start\ndata: {{}}\n\n"
+    if not conclusion_started:
+        yield f"event: conclusion_start\ndata: {{}}\n\n"
+
+    elapsed = round(time.time() - start_time, 1)
+    yield f"event: step\ndata: {json.dumps({'step': 'complete', 'content': f'分析完成, 总耗时 {elapsed} 秒'}, ensure_ascii=False)}\n\n"
diff --git a/.review-tmp/review-report.md b/.review-tmp/review-report.md
new file mode 100644
index 0000000..e52b164
--- /dev/null
+++ b/.review-tmp/review-report.md
@@ -0,0 +1,202 @@
+# Review Report: AI Agent 升级迭代日报 (daily_watchdog)
+
+- Reviewer: 独立 review agent (未参与 build)
+- Review time: 2026-08-16 16:41 ~ 17:40 (Asia/Shanghai)
+- Reviewed deliverables: scripts/daily_watchdog.py, scripts/install_watchdog_cron.sh, app.py (/watchdog, /watchdog/run), templates/watchdog.html, base.html 导航
+
+---
+
+## Acceptance 1: cron 幂等安装 ✅
+
+**执行记录:**
+
+```
+$ bash scripts/install_watchdog_cron.sh   # 第 1 次
+already installed, skipping
+exit=0
+$ crontab -l | grep -c -F "daily_watchdog.py"
+1
+
+$ bash scripts/install_watchdog_cron.sh   # 第 2 次
+already installed, skipping
+exit=0
+$ crontab -l | grep -c -F "daily_watchdog.py"
+1
+```
+
+**历史遗留条目情况:** 执行前 crontab 已存在一条历史遗留条目:
+```
+0 9 * * * /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-watchdog-cron.log 2>&1
+```
+日志路径 `daily-watchdog-cron.log` 与规范 `daily-agent-watchdog.log` 不同。幂等脚本用 `grep -F "daily_watchdog.py"` 匹配到该历史条目而跳过 (输出 "already installed, skipping")。**评估:** 脚本至少没有制造重复条目 (count 恒为 1), 幂等性正确; 但它无法区分"历史遗留条目"与"规范条目", 会误判为已安装。
+
+**修正 (按任务允许的操作):** 先备份 crontab 到 `.review-tmp/crontab-review-backup.txt`, 删除历史条目后重跑安装脚本:
+```
+$ crontab -l | grep -v -F "daily_watchdog.py" | crontab -
+$ bash scripts/install_watchdog_cron.sh
+installed
+exit=0
+$ crontab -l | grep -F "daily_watchdog.py"
+0 9 * * * cd /home/yi/opencode-blog-showcase/blog-app && /usr/bin/python3 scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog.log 2>&1
+count=1
+```
+修正后 crontab 恰好一条规范条目 (9:00, 指向同一脚本, 日志路径 daily-agent-watchdog.log), 其余 13 条既有条目完好。
+
+**注意:** review 期间 build agent (PID 1097346) 仍在运行, 在 17:28 又把自己的历史遗留条目写回 crontab 两次; 我按同样方式修正, 最终状态为恰好一条规范条目。幂等脚本本身行为正确 (不制造重复)。
+
+---
+
+## Acceptance 2: 数据源 ≥5 ✅
+
+报告头部 (`logs/daily-agent-watchdog-2026-08-16.md`, 我实测跑出的最新版):
+```
+## 数据源
+- GitHub Releases API (6/6)
+- GitHub commits API (6/6)
+- npm registry (5/6)
+- GitHub releases.atom (6/6)
+- Exa web search (6/6)
+```
+5 类数据源全部列出。watchdog.db 的 meta_json `sources_ok` 也列出全部 5 类:
+```
+"sources_ok": ["Exa web search", "GitHub Releases API", "GitHub commits API", "GitHub releases.atom", "npm registry"]
+```
+(注: 16:40 的旧报告因 GitHub API 限流部分失败, 但 sources_ok 仍列出该源; 我实测跑出的新报告 6/6 全成功。)
+
+---
+
+## Acceptance 3: 6 项目完整性 ✅ (以实测跑出的最新报告为准)
+
+| 项目 | version | 发布日期 | 中文摘要(LLM) | breaking 标记 | ★ 相关性 | Exa≥1(带URL) |
+|---|---|---|---|---|---|---|
+| sst/opencode | v1.18.18 | 2026-08-13T01:15:04Z | ✅ | ✅ | ✅ | ✅ (3条) |
+| anthropics/claude-code | v2.1.233 | 2026-08-14T22:20:57Z | ✅ | ✅ | ✅ | ✅ (3条) |
+| openai/codex | rust-v0.147.0 | 2026-08-07T01:41:49Z | ✅ | ✅ | ✅ | ✅ (3条) |
+| NousResearch/hermes-agent | v2026.8.13 | 2026-08-13T20:37:37Z | ✅ | ✅ | ✅ | ✅ (3条) |
+| code-yeongyu/oh-my-openagent | v5.0.0-beta.7 | 2026-08-12T20:52:07Z | ✅ | ✅ | ✅ | ✅ (3条) |
+| vectorize-io/hindsight | v0.9.1 | 2026-08-14T09:09:19Z | ✅ | ✅ | ✅ | ✅ (3条) |
+
+- 每节均有 `**LLM 总结:**` 真实中文摘要 (非"待 LLM 集成"占位), 内容与 changelog 数据吻合。
+- 每节均有 `**Breaking Changes:**` 行 (oh-my-openagent 因 5.0.0 主版本升级标记为破坏性变更)。
+- 每节均有 `★ **相关性:**` 说明。
+- 每节均有 ≥1 条带来源 URL 的 Exa 搜索结果 (实际 3 条)。
+- 注: 16:40 的旧报告因 GitHub 限流, 前 3 节缺"发布日期"字段; 实测跑出的新报告 6 节全部含发布日期。
+
+---
+
+## Acceptance 4: 真实跑一遍 ✅
+
+```
+$ cd /home/yi/opencode-blog-showcase/blog-app && /usr/bin/python3 scripts/daily_watchdog.py
+exit=0
+```
+
+**该次运行最后 20 行 stdout:**
+```
+2026-08-16 16:45:55 [INFO] Starting daily watchdog for 2026-08-16
+2026-08-16 16:46:03 [INFO] Exa MCP initialized, session=0bb3cdc6-36ee-4475-8b5d-efe31ff80907
+2026-08-16 16:46:03 [INFO] Collecting data for sst/opencode
+2026-08-16 16:46:51 [INFO] Collecting data for anthropics/claude-code
+2026-08-16 16:47:16 [INFO] Collecting data for openai/codex
+2026-08-16 16:48:23 [INFO] Collecting data for NousResearch/hermes-agent
+2026-08-16 16:49:26 [INFO] Collecting data for code-yeongyu/oh-my-openagent
+2026-08-16 16:50:34 [INFO] Collecting data for vectorize-io/hindsight
+2026-08-16 16:50:46 [INFO] npm package 'hindsight' not relevant, skipping
+2026-08-16 16:51:21 [INFO] Report saved to /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog-2026-08-16.md
+2026-08-16 16:51:21 [INFO] SQLite record saved to /home/yi/opencode-blog-showcase/data/watchdog.db
+2026-08-16 16:51:21 [INFO] Daily watchdog completed successfully
+```
+
+**md 与 db 均更新:**
+```
+md mtime:  16:40:17 → 16:51:21 → 17:15:13 → 17:32:56
+db created_at: 16:40:17 → 16:51:21 → 17:15:13 → 17:32:56
+```
+DB 中 markdown 与文件内容一致 (diff 仅尾部换行差异, 字节差为 UTF-8 多字节字符)。数据为真实抓取 (GitHub API / npm registry / releases.atom / Exa MCP / LLM 127.0.0.1:4000), 无 mock。
+
+---
+
+## Acceptance 5: Flask 页面 ✅
+
+```
+$ bash scripts/deploy.sh
+local:200
+deploy_exit=0
+
+$ curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/watchdog
+200
+$ curl -sS http://127.0.0.1:8090/watchdog | grep -o "日报\|2026-08-16" | sort | uniq -c
+      3 2026-08-16
+      3 日报
+$ curl -sS -o /dev/null -w '%{http_code}' http://192.168.2.25:8090/watchdog
+200
+```
+
+**POST 触发 (干净状态下实测):**
+```
+$ curl -sS -X POST -o /dev/null -w 'post1:%{http_code}' http://127.0.0.1:8090/watchdog/run
+post1:302
+$ sleep 1; curl -sS -X POST -o /dev/null -w 'post2:%{http_code}' http://127.0.0.1:8090/watchdog/run
+post2:429
+```
+(注: 首次测试时因 build agent 遗留的 daily_watchdog.py 进程仍在跑, 两次都走"已在运行中"分支返回 302; 等进程结束后干净重测得到 302→429。flash 消息机制也验证可用: 页面渲染出"已在运行中"。)
+
+**回归:**
+```
+/ -> 200
+/projects -> 200
+/search -> 200
+/analyze -> 200
+/api/stats -> 200
+```
+
+页面 body 含当日日期 (2026-08-16) 与"日报" (标题"升级迭代日报"), 报告内容 (LLM 总结 x6, 数据源, sst/opencode 等) 正常渲染。
+
+---
+
+## Acceptance 6: 无新 pip 依赖 ✅
+
+```
+$ grep -n "^import\|^from" scripts/daily_watchdog.py
+import json / logging / os / re / sqlite3 / subprocess / sys / time / xml.etree.ElementTree
+from datetime import datetime
+from pathlib import Path
+import requests
+
+$ grep -n "^import\|^from" app.py
+import json / logging / os / sqlite3 / subprocess / time
+import requests as http_requests
+from flask import Flask, flash, g, jsonify, redirect, render_template, request, url_for
+
+$ cat requirements.txt
+flask>=3.0
+requests>=2.28
+```
+- daily_watchdog.py 仅标准库 + requests。
+- app.py 无新增第三方 import。
+- requirements.txt 未变 (mtime 08:40, 早于 build)。
+- 脚本中无 `pip install` 痕迹 (grep 为空)。
+
+---
+
+## 禁止项复核 ✅ (全部通过)
+
+| 检查项 | 结果 | 证据 |
+|---|---|---|
+| app.py 现有路由未改 | ✅ | 逐一核对 api_projects/api_project_detail/api_project_releases/api_project_issues/api_search/api_stats/page_index/page_projects/page_project_detail/page_search/page_analyze/api_presets/api_context/api_analyze_stream 函数体与装饰器完整; api_analyze_stream 的 SSE 生成器 (generate(), 分隔符滚动 buffer, keepalive, 防御性 section_start) 完整存在 (app.py L491-667)。watchdog 代码为独立新增区块 (L225-319), 未触碰现有函数 |
+| projects.db 未被碰 | ✅ | mtime 2026-08-16 08:36 (早于 build); `sqlite3 ... "SELECT count(*) FROM sqlite_master"` → 5, 可正常读 |
+| scripts/fetch_data.py 未改 | ✅ | mtime 2026-08-16 08:35 (早于 15:00), size 11718 |
+| 无 pip install 痕迹 | ✅ | grep -ri "pip install" scripts/ app.py → 空 |
+| 无硬编码假数据 | ✅ | grep -in "mock\|fake\|示例数据\|hardcod" daily_watchdog.py / app.py → 空; 数据均来自真实 API 调用 |
+
+---
+
+## VERDICT: PASS
+
+6 条 Acceptance 全部通过, 5 项禁止项全部通过。
+
+### 非阻断性改进建议
+1. **幂等脚本的匹配粒度:** `install_watchdog_cron.sh` 用 `grep -F "daily_watchdog.py"` 判断已安装, 会误匹配日志路径不同的历史遗留条目 (本次 review 中即发生)。建议改为匹配完整规范条目字符串 (含 `daily-agent-watchdog.log`), 或安装时先删除旧路径条目再追加规范条目。
+2. **base.html 导航文案:** 交付物描述为导航"日报", 实际链接文本是 "Watchdog" (base.html L22)。功能正常, 但文案与描述不一致, 建议改为"日报"。
+3. **GitHub 限流降级:** 16:40 的旧报告因 GitHub API 限流 (403) 导致前 3 节缺"发布日期"字段。脚本已有降级逻辑 (npm/atom/Exa 兜底), 但建议在 GitHub 源失败时从 Exa 结果中提取发布日期补上, 保证每节字段完整。
+4. **cron 日志路径统一:** 历史遗留条目用 `daily-watchdog-cron.log`, 规范条目用 `daily-agent-watchdog.log`, 建议清理旧日志避免混淆。
\ No newline at end of file
diff --git a/.review-tmp/review-verdict.md b/.review-tmp/review-verdict.md
new file mode 100644
index 0000000..0797bea
--- /dev/null
+++ b/.review-tmp/review-verdict.md
@@ -0,0 +1,43 @@
+# REVIEW VERDICT — "AI Agent 升级迭代日报" feature
+
+**Reviewer:** independent review agent (did NOT build the feature)
+**Date:** 2026-08-16 17:27–18:10 CST
+**Evidence:** see `./.review-tmp/review-evidence.log`
+
+## Overall Verdict: **PASS**
+
+All 7 acceptance items verified with real commands. No blocking defects found.
+
+---
+
+## Item-by-item
+
+| # | Item | Verdict | Evidence (one line) |
+|---|------|---------|---------------------|
+| 1 | Cron install script exists & idempotent | **PASS** | Ran `install_watchdog_cron.sh` twice → both "already installed, skipping", exit 0, `crontab -l` diff before/after = NO DIFF; `0 9 * * *` entry present; all 13 pre-existing crontab entries survived |
+| 2 | Data sources ≥5 with real counts | **PASS** | My run (17:50:40): `sources_failed=[]`, counts GitHub Releases 6/6, commits 6/6, npm 5/6, releases.atom 6/6, Exa 6/6. (Live report after POST-triggered run: atom 0/6 from github.com flap — source still listed with count + reasons, other 4 sources 6/6, acceptable per criteria) |
+| 3 | 6 projects with version/date/中文摘要/Breaking/★ | **PASS** | 6 project sections; each has Latest Release + 发布日期 + real LLM 中文摘要 + Breaking Changes marker + ★ 相关性; 0 "LLM 未集成/总结失败" markers; oh-my-openagent has substantive breaking change (v5.0.0 major + telemetry removal) |
+| 4 | Real run by reviewer | **PASS** | `HOME=/home/yi /usr/bin/python3 scripts/daily_watchdog.py` → **exit 0**, all 6 projects collected with no 403s, report saved 17:50:40, DB row `created_at=2026-08-16T17:50:40` (fresh). Waited for rate-limit reset (17:39:28) + 30s buffer as instructed |
+| 5 | Flask | **PASS** | deploy.sh exit 0; GET /watchdog 200 + 升级迭代日报 + 2026-08-16; POST /watchdog/run → 302 (trigger accepted, run spawned); immediate 2nd POST → **429 操作太频繁**; LAN `192.168.2.25:8090/watchdog` → 200; regression / /projects /search /analyze /api/stats all 200; after POST-triggered run finished, GET /watchdog still 200; nav has 日报 |
+| 6 | No new pip deps | **PASS** | requirements.txt = `flask>=3.0`, `requests>=2.28`; imports of both scripts = stdlib + flask + requests only |
+| 7 | 禁止项 audit | **PASS** | fetch_data.py mtime 08:35:50 (unchanged since 08:35); projects.db mtime 08:36:08 (untouched); no litellm config edits; app.py changes additive (all existing routes 200) |
+
+---
+
+## Residual risks (non-blocking)
+
+1. **github.com releases.atom flakiness** — the POST-triggered run (18:08:42) got atom 0/6 due to repeated feed timeouts (2 attempts × ~55s each per project, slowing runs to ~15 min). Script degrades gracefully (source listed with count + reasons), but a fully atom-less report is possible during flaps.
+2. **Unauthenticated GitHub API rate limit sensitivity** — 60 req/hr shared across all processes on this host. The build phase exhausted it (~17:25); the script handles 403s gracefully but reports degrade (partial sources) when exhausted. No token/retry-with-backoff for the API path.
+3. **No 9:30 self-heal cron entry** — script supports `--fix-if-missing` but `install_watchdog_cron.sh` only installs the 9:00 entry; a failed 9:00 run won't self-heal at 9:30.
+4. **app.secret_key dev default** — flash/session signing uses a dev default; fine for LAN, not for public exposure.
+5. **Installer template vs installed entry mismatch (cosmetic)** — script's `ENTRY` uses `cd ... && python3 scripts/daily_watchdog.py >> daily-agent-watchdog.log`; the installed entry uses absolute path + `daily-watchdog-cron.log`. Functionally equivalent; the `grep -F "daily_watchdog.py"` idempotency check matches both.
+6. **429 semantics vs acceptance wording** — acceptance expected "429 操作太频繁" after ~70s; implementation's 429 gate is a 60s cooldown, and after it expires the single-flight check rejects a duplicate trigger with 302 + flash "已在运行中". Both mechanisms prevent duplicate runs; the literal status code differs after 70s.
+
+---
+
+## Verification highlights
+
+- Rate limit: **0 remaining** at start (reset 17:39:28) → waited → **24 remaining** at end.
+- My real run: **exit 0**, report + DB row fresh at **17:50:40**, `sources_failed=[]`.
+- Curl status codes: GET /watchdog **200**, POST trigger **302**, duplicate POST **429**, LAN **200**, regressions **200×5**.
+- No code modified during review; temp artifacts only in `./.review-tmp/` and `/tmp/opencode/`.
\ No newline at end of file
diff --git a/.review-tmp/sse_final.txt b/.review-tmp/sse_final.txt
new file mode 100644
index 0000000..2de6d71
--- /dev/null
+++ b/.review-tmp/sse_final.txt
@@ -0,0 +1,2648 @@
+event: step
+data: {"step": "context", "content": "已收集上下文: 项目: hermes-agent (NousResearch/hermes-agent), 231080 stars"}
+
+event: step
+data: {"step": "prompt", "content": "[SYSTEM]\n你是 AI 项目分析助手, 基于以下数据进行分析。请用中文回答。\n\n[USER]\n分析数据: {\"scope_type\": \"project/1\", \"project_id\": 1, \"data_summary\": \"项目: hermes-agent (NousResearch/hermes-agent), 231080 stars\", \"releases\": [{\"tag_name\": \"v2026.8.13\", \"name\": \"Hermes Agent v0.20.1 (2026.8.13)\", \"published_at\": \"2026-08-13T20:37:37Z\", \"body\": \"# Hermes Agent v0.20.1 (v2026.8.13)\\n\\n**Release Date:** August 13, 2026\\n\\n> Patch release. This tag rolls up the ~656 PRs merged since v0.20.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, and anyone installing from the latest tag).\\n\\n---\\n\\n## About this release\\n\\nSince v0.20.0 (August 3), this window landed **1,444 commits** across **~656 merged PRs**, touching **2,172 files** (+233,872 / −75,244), and closed **~481 issues**. It is a broad stabilization-an...\"}, {\"tag_name\": \"v2026.8.3\", \"name\": \"Hermes Agent v0.20.0 (2026.8.3)\", \"published_at\": \"2026-08-03T16:57:52Z\", \"body\": \"# Hermes Agent v0.20.0 (v2026.8.3)\\n\\n\\n**Release Date:** August 3, 2026\\n**Since v0.19.0:** ~3,650 commits · ~1,400 merged PRs · ~5,200 files changed · ~559,000 insertions · ~405,000 deletions · **~1,200 issues closed** · 650+ contributors\\n\\n> **The Herald Release.** Hermes is the herald of the gods, and this release makes him one in earnest: he **speaks** (real-time conversational voice with streaming TTS, barge-in, on-device wake words, and hands-free control across the CLI, desktop, and every aud...\"}, {\"tag_name\": \"v2026.7.30\", \"name\": \"Hermes Agent v0.19.1 (v2026.7.30)\", \"published_at\": \"2026-07-30T23:45:37Z\", \"body\": \"# Hermes Agent v0.19.1 (v2026.7.30)\\n\\n**Release Date:** July 30, 2026\\n\\n> Patch release. This tag rolls up the ~1,000+ PRs merged since v0.19.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, fresh installs).\\n\\n---\\n\\n## About this release\\n\\nSince v2026.7.20 (v0.19.0, July 20): **~2,789 commits · ~4,748 files changed · ~442,000 insertions · ~392,300 deletions** on `main`. This window is dominated by bug-fix and salvage waves across the gateway, voice subsystem...\"}, {\"tag_name\": \"v2026.7.20\", \"name\": \"Hermes Agent v0.19.0 (2026.7.20) — The Quicksilver Release\", \"published_at\": \"2026-07-20T18:35:55Z\", \"body\": \"# Hermes Agent v0.19.0 (v2026.7.20)\\n\\n\\n**Release Date:** July 20, 2026\\n**Since v0.18.0:** ~2,245 commits · ~1,065 merged PRs · ~2,465 files changed · ~300,000 insertions · ~36,000 deletions · **~3,300 issues closed** · **450+ community contributors**\\n\\n> **The Quicksilver Release.** Hermes is the messenger god, and this window we made him move like it. First-turn time-to-first-token dropped **~80% on every platform**, reasoning streams live by default, the desktop app got a ~20-PR speed overhaul (...\"}, {\"tag_name\": \"v2026.7.7.2\", \"name\": \"Hermes Agent v0.18.2 (2026.7.7.2)\", \"published_at\": \"2026-07-08T03:11:22Z\", \"body\": \"# Hermes Agent v0.18.2 (v2026.7.7.2)\\n\\n**Release Date:** July 7, 2026\\n\\n> Same-day patch on top of v0.18.1, picking up the WhatsApp Baileys dependency fix needed for tagged-release Docker builds.\\n\\n---\\n\\n## What's in this patch\\n\\n- **fix(whatsapp): unpin Baileys from git commit, use published 7.0.0-rc13** ([#60643](https://github.com/NousResearch/hermes-agent/pull/60643)) — the WhatsApp bridge dependency now installs from the published npm release instead of a pinned git commit, making installs and D...\"}], \"issues\": [{\"title\": \"Desktop auto-update can wipe the desktop build on Windows (venv lock ignored on retry → hermes.exe quarantine downgraded → ZIP fallback deletes win-unpacked)\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:17:32Z\"}, {\"title\": \"hermes mcp login : OAuth callback port collision makes interactive login impossible on a headless host (v0.20.1) — #5344 regressed\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:10:39Z\"}, {\"title\": \"[Bug]: Install scripts gets stuck on Playwrite on Arch\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:47:33Z\"}, {\"title\": \"[Bug]: transform_llm_output callbacks do not have a guaranteed sequential safety pipeline\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:46:00Z\"}], \"stats\": {}}\n\n用户问题: 评估活跃度\n\n要求: 按以下4步展示思考过程: 1.数据观察 2.初步判断 3.深入推理 4.结论"}
+
+: keepalive
+
+event: token
+data: {"text": "###"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "数据"}
+
+event: token
+data: {"text": "观察"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "频率"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "周期"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "年"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "日至"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "日"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "天"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "内"}
+
+event: token
+data: {"text": "共"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "其中"}
+
+event: token
+data: {"text": "包含"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "个大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": ")、"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "及"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "同日"}
+
+event: token
+data: {"text": "热"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "间隔"}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "天"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "紧随"}
+
+event: token
+data: {"text": "其后"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "通常"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "天内"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "变动"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "单一"}
+
+event: token
+data: {"text": "窗口"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "周期"}
+
+event: token
+data: {"text": "内"}
+
+event: token
+data: {"text": "累计"}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "次"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "合并"}
+
+event: token
+data: {"text": "PR"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "修改"}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "文件"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "净"}
+
+event: token
+data: {"text": "增"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "删"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "超"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "万"}
+
+event: token
+data: {"text": "行"}
+
+event: token
+data: {"text": ";"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "周期"}
+
+event: token
+data: {"text": "内"}
+
+event: token
+data: {"text": "也有"}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "次"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "PR"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "改动"}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "文件"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "贡献"}
+
+event: token
+data: {"text": "者"}
+
+event: token
+data: {"text": "规模"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "窗口"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "参与"}
+
+event: token
+data: {"text": "贡献"}
+
+event: token
+data: {"text": "者"}
+
+event: token
+data: {"text": "达"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "+"}
+
+event: token
+data: {"text": "人"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "呈现"}
+
+event: token
+data: {"text": "明显的"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "协同"}
+
+event: token
+data: {"text": "特征"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "问题"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "处理"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "周期"}
+
+event: token
+data: {"text": "内"}
+
+event: token
+data: {"text": "关闭"}
+
+event: token
+data: {"text": "问题"}
+
+event: token
+data: {"text": "数"}
+
+event: token
+data: {"text": "达"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": ";"}
+
+event: token
+data: {"text": "最新"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "后"}
+
+event: token
+data: {"text": "仅"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "天"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "日"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "即"}
+
+event: token
+data: {"text": "涌现"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "由"}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "部署"}
+
+event: token
+data: {"text": "者"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "实时"}
+
+event: token
+data: {"text": "缺陷"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "涉及"}
+
+event: token
+data: {"text": "Windows"}
+
+event: token
+data: {"text": "桌面"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "OAuth"}
+
+event: token
+data: {"text": "端口"}
+
+event: token
+data: {"text": "冲突"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "依赖"}
+
+event: token
+data: {"text": "安装"}
+
+event: token
+data: {"text": "卡"}
+
+event: token
+data: {"text": "死"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "回调"}
+
+event: token
+data: {"text": "安全"}
+
+event: token
+data: {"text": "管道"}
+
+event: token
+data: {"text": "等"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "###"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "初步"}
+
+event: token
+data: {"text": "判断"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "该项目"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": "状态"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "无论是"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "节奏"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "吞吐量"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "贡献"}
+
+event: token
+data: {"text": "者"}
+
+event: token
+data: {"text": "基数"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "还是"}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": "流转"}
+
+event: token
+data: {"text": "速度"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "均"}
+
+event: token
+data: {"text": "远超"}
+
+event: token
+data: {"text": "常规"}
+
+event: token
+data: {"text": "开源"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "水平"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "呈现出"}
+
+event: token
+data: {"text": "典型的"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "特性"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "+"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "热"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "敏捷"}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "特征"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "###"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "深入"}
+
+event: token
+data: {"text": "推理"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "策略"}
+
+event: token
+data: {"text": "成熟"}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "激进"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "采用"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "特性"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "+"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "的双"}
+
+event: token
+data: {"text": "轨"}
+
+event: token
+data: {"text": "模式"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "侧重"}
+
+event: token
+data: {"text": "首"}
+
+event: token
+data: {"text": "Token"}
+
+event: token
+data: {"text": "速度"}
+
+event: token
+data: {"text": "优化"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "%,"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "聚焦"}
+
+event: token
+data: {"text": "实时"}
+
+event: token
+data: {"text": "语音"}
+
+event: token
+data: {"text": "/T"}
+
+event: token
+data: {"text": "TS"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "流"}
+
+event: token
+data: {"text": "式"}
+
+event: token
+data: {"text": "交互"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "验证"}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "能力"}
+
+event: token
+data: {"text": ";"}
+
+event: token
+data: {"text": "随后"}
+
+event: token
+data: {"text": "通过"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "/v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "等"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "收"}
+
+event: token
+data: {"text": "口"}
+
+event: token
+data: {"text": "稳定性"}
+
+event: token
+data: {"text": "问题"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "这种"}
+
+event: token
+data: {"text": "模式"}
+
+event: token
+data: {"text": "要求"}
+
+event: token
+data: {"text": "极强的"}
+
+event: token
+data: {"text": "CI"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "CD"}
+
+event: token
+data: {"text": "流水线"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "自动化"}
+
+event: token
+data: {"text": "测试"}
+
+event: token
+data: {"text": "支撑"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "基"}
+
+event: token
+data: {"text": "线"}
+
+event: token
+data: {"text": "庞大"}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "重构"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "扩张"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "单"}
+
+event: token
+data: {"text": "窗口"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "数"}
+
+event: token
+data: {"text": "万次"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "数十"}
+
+event: token
+data: {"text": "万"}
+
+event: token
+data: {"text": "行"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "增"}
+
+event: token
+data: {"text": "删"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "说明"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "并非"}
+
+event: token
+data: {"text": "静态"}
+
+event: token
+data: {"text": "维护"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "而是"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "架构"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "阶段"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "引入"}
+
+event: token
+data: {"text": "流"}
+
+event: token
+data: {"text": "式"}
+
+event: token
+data: {"text": "语音"}
+
+event: token
+data: {"text": "网关"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "重写"}
+
+event: token
+data: {"text": "桌面"}
+
+event: token
+data: {"text": "端"}
+
+event: token
+data: {"text": "速度"}
+
+event: token
+data: {"text": "模块"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "调整"}
+
+event: token
+data: {"text": "依赖"}
+
+event: token
+data: {"text": "链"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "高"}
+
+event: token
+data: {"text": "文件"}
+
+event: token
+data: {"text": "变更"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "也"}
+
+event: token
+data: {"text": "暗示"}
+
+event: token
+data: {"text": "模块"}
+
+event: token
+data: {"text": "耦合"}
+
+event: token
+data: {"text": "度"}
+
+event: token
+data: {"text": "正在"}
+
+event: token
+data: {"text": "被"}
+
+event: token
+data: {"text": "拆解"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "重写"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "贡献"}
+
+event: token
+data: {"text": "生态"}
+
+event: token
+data: {"text": "健康"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "+"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "贡献"}
+
+event: token
+data: {"text": "者"}
+
+event: token
+data: {"text": "表明"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "具备"}
+
+event: token
+data: {"text": "强大的"}
+
+event: token
+data: {"text": "外部"}
+
+event: token
+data: {"text": "吸引力"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "内部"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "并行"}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "机制"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": "关闭"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "~"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "匹配"}
+
+event: token
+data: {"text": "度高"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "说明"}
+
+event: token
+data: {"text": "Tri"}
+
+event: token
+data: {"text": "aging"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "问题"}
+
+event: token
+data: {"text": "分类"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "分配"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "机制"}
+
+event: token
+data: {"text": "高效"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "未"}
+
+event: token
+data: {"text": "出现"}
+
+event: token
+data: {"text": "严重"}
+
+event: token
+data: {"text": "积压"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "实时"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": "闭环"}
+
+event: token
+data: {"text": "极"}
+
+event: token
+data: {"text": "短"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "仅"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "小时内"}
+
+event: token
+data: {"text": "即"}
+
+event: token
+data: {"text": "产生"}
+
+event: token
+data: {"text": "涉及"}
+
+event: token
+data: {"text": "生产"}
+
+event: token
+data: {"text": "环境"}
+
+event: token
+data: {"text": "部署"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "Windows"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "env"}
+
+event: token
+data: {"text": "锁"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "D"}
+
+event: token
+data: {"text": "ocker"}
+
+event: token
+data: {"text": "依赖"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "OAuth"}
+
+event: token
+data: {"text": "碰撞"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "回调"}
+
+event: token
+data: {"text": "安全"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "缺陷"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "这"}
+
+event: token
+data: {"text": "证明"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "被"}
+
+event: token
+data: {"text": "广泛"}
+
+event: token
+data: {"text": "投入"}
+
+event: token
+data: {"text": "真实"}
+
+event: token
+data: {"text": "场景"}
+
+event: token
+data: {"text": "测试"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "缺陷"}
+
+event: token
+data: {"text": "上报"}
+
+event: token
+data: {"text": "通道"}
+
+event: token
+data: {"text": "畅通"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "属于"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "热"}
+
+event: token
+data: {"text": "部署"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "快"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "的典型"}
+
+event: token
+data: {"text": "生产"}
+
+event: token
+data: {"text": "级"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "状态"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "###"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "结论"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "评估"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "S"}
+
+event: token
+data: {"text": "级"}
+
+event: token
+data: {"text": ")**"}
+
+event: token
+data: {"text": "  \n"}
+
+event: token
+data: {"text": "该项目"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "年"}
+
+event: token
+data: {"text": "Q"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "展现出"}
+
+event: token
+data: {"text": "顶级"}
+
+event: token
+data: {"text": "AI"}
+
+event: token
+data: {"text": "开源"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "企业"}
+
+event: token
+data: {"text": "级"}
+
+event: token
+data: {"text": "项目的"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "特征"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "其"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "特性"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "海量"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "吞吐"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "庞大"}
+
+event: token
+data: {"text": "贡献"}
+
+event: token
+data: {"text": "者"}
+
+event: token
+data: {"text": "矩阵"}
+
+event: token
+data: {"text": "以及"}
+
+event: token
+data: {"text": "分钟"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "小时"}
+
+event: token
+data: {"text": "级的"}
+
+event: token
+data: {"text": "缺陷"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": "闭环"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "表明"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "具备"}
+
+event: token
+data: {"text": "极强的"}
+
+event: token
+data: {"text": "工程"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "交付"}
+
+event: token
+data: {"text": "能力"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "产品"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "野心"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "对于"}
+
+event: token
+data: {"text": "使用者"}
+
+event: token
+data: {"text": "而言"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "功能"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "迅速"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "生态"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "但"}
+
+event: token
+data: {"text": "伴随"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "也"}
+
+event: token
+data: {"text": "意味着"}
+
+event: token
+data: {"text": "需"}
+
+event: token
+data: {"text": "密切关注"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "及"}
+
+event: token
+data: {"text": "后续"}
+
+event: token
+data: {"text": "热"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "以"}
+
+event: token
+data: {"text": "规避"}
+
+event: token
+data: {"text": "集成"}
+
+event: token
+data: {"text": "风险"}
+
+event: token
+data: {"text": ";"}
+
+event: token
+data: {"text": "适合"}
+
+event: token
+data: {"text": "愿意"}
+
+event: token
+data: {"text": "接受"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "并"}
+
+event: token
+data: {"text": "具备"}
+
+event: token
+data: {"text": "自动化"}
+
+event: token
+data: {"text": "升级"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "回"}
+
+event: token
+data: {"text": "滚"}
+
+event: token
+data: {"text": "能力的"}
+
+event: token
+data: {"text": "生产"}
+
+event: token
+data: {"text": "环境"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "深度"}
+
+event: token
+data: {"text": "定制"}
+
+event: token
+data: {"text": "场景"}
+
+event: token
+data: {"text": "。"}
+
+event: step
+data: {"step": "complete", "content": "分析完成, 总耗时 43.8 秒"}
+
diff --git a/.review-tmp/sse_fix_verify.txt b/.review-tmp/sse_fix_verify.txt
new file mode 100644
index 0000000..41ff7f0
--- /dev/null
+++ b/.review-tmp/sse_fix_verify.txt
@@ -0,0 +1,1005 @@
+event: step
+data: {"step": "context", "content": "已收集上下文: 共 3 个项目, 14 个 release, 14 个 issue, 430166 stars"}
+
+event: step
+data: {"step": "prompt", "content": "[SYSTEM]\n你是 AI 项目分析助手, 基于以下数据进行分析。请用中文回答。\n\n[USER]\n分析数据: {\"scope_type\": \"home\", \"project_id\": null, \"data_summary\": \"共 3 个项目, 14 个 release, 14 个 issue, 430166 stars\", \"releases\": [{\"tag_name\": \"v2026.8.13\", \"name\": \"Hermes Agent v0.20.1 (2026.8.13)\", \"published_at\": \"2026-08-13T20:37:37Z\", \"body\": \"# Hermes Agent v0.20.1 (v2026.8.13)\\n\\n**Release Date:** August 13, 2026\\n\\n> Patch release. This tag rolls up the ~656 PRs merged since v0.20.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, and anyone installing from the latest tag).\\n\\n---\\n\\n## About this release\\n\\nSince v0.20.0 (August 3), this window landed **1,444 commits** across **~656 merged PRs**, touching **2,172 files** (+233,872 / −75,244), and closed **~481 issues**. It is a broad stabilization-an...\"}, {\"tag_name\": \"v1.18.18\", \"name\": \"v1.18.18\", \"published_at\": \"2026-08-13T01:15:04Z\", \"body\": \"## Core\\r\\n### Bugfixes\\r\\n- Select the Kimi system prompt correctly for official Moonshot and Kimi providers\\r\\n- Fix xhigh reasoning effort for xai models\\r\\n\"}, {\"tag_name\": \"v1.18.17\", \"name\": \"v1.18.17\", \"published_at\": \"2026-08-12T20:25:07Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Made session compaction keep complete recent turns and produce clearer summaries for smaller models.\\n- Added MERGE Gateway reasoning variants so those model options work correctly. (@MatthewFeroz)\\n- Capped automatic session retries and added jitter to reduce repeated retry storms.\\n- Enabled PDF attachments for GitHub Copilot models that advertise PDF vision support. (@stevenao)\\n- Applied the correct sampling defaults to DeepSeek V4 Flash on supported providers.\\n- Routed ...\"}, {\"tag_name\": \"v1.18.16\", \"name\": \"v1.18.16\", \"published_at\": \"2026-08-10T06:07:08Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Ignore unknown top-level config fields instead of failing config parsing\\n- Register projects opened from Home so they are available to the rest of the app\\n\\n## Desktop\\n\\n### Improvements\\n\\n- Open the project menu with a right-click in Home\\n\\n### Bugfixes\\n\\n- Fall back to listing and matching local directories when the project picker server does not support search\\n- Use 词元 instead of 令牌 for token-related labels in Simplified Chinese (@Speechlessmanbilibili)\\n- Keep the macOS ap...\"}, {\"tag_name\": \"v1.18.15\", \"name\": \"v1.18.15\", \"published_at\": \"2026-08-07T06:49:55Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Chronological message ordering now stays correct even when imported or legacy message IDs are out of order.\\n- Revert and fork actions now use real message chronology instead of message ID ordering.\\n- Truncation cleanup now removes stale files by file timestamp more reliably.\\n- Repeated compaction now keeps earlier tool-call history in summaries instead of dropping orphaned results.\\n- Blob-based attachments now load correctly in the web UI. (@rexdotsh)\\n\\n## Desktop\\n\\n### Im...\"}], \"issues\": [{\"title\": \"Desktop auto-update can wipe the desktop build on Windows (venv lock ignored on retry → hermes.exe quarantine downgraded → ZIP fallback deletes win-unpacked)\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:17:32Z\"}, {\"title\": \"hermes mcp login : OAuth callback port collision makes interactive login impossible on a headless host (v0.20.1) — #5344 regressed\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:10:39Z\"}, {\"title\": \"[Bug]: Install scripts gets stuck on Playwrite on Arch\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:47:33Z\"}, {\"title\": \"[Bug]: transform_llm_output callbacks do not have a guaranteed sequential safety pipeline\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:46:00Z\"}, {\"title\": \"AI_APICallError should be communicated via ACP\", \"state\": \"open\", \"created_at\": \"2026-08-15T22:05:12Z\"}, {\"title\": \"[BUG]: Session header is hidden until title generation\", \"state\": \"open\", \"created_at\": \"2026-08-15T21:42:16Z\"}, {\"title\": \"[Bug] Poe Provider Tool Failure (1.18.18)\", \"state\": \"open\", \"created_at\": \"2026-08-15T20:05:02Z\"}, {\"title\": \"[FEATURE]: Expose the current TUI model to plugins\", \"state\": \"open\", \"created_at\": \"2026-08-15T19:58:47Z\"}, {\"title\": \"[BUG] Two ordering-correctness gaps remain after the ID-rollover fix\", \"state\": \"open\", \"created_at\": \"2026-08-15T19:50:02Z\"}, {\"title\": \"سؤال\", \"state\": \"closed\", \"created_at\": \"2026-08-15T19:15:08Z\"}], \"stats\": {\"total_projects\": 3, \"total_releases\": 14, \"total_issues\": 14, \"total_stars\": 430166}}\n\n用户问题: 请基于最近 release + issue 评估项目活跃度 (1-10 分) 并给出证据\n\n要求: 严格按以下格式输出, 不要输出其他任何标记:\n===REASONING===\n(这里写你的分析推理过程: 数据观察、初步判断、深入推理)\n===CONCLUSION===\n(这里写最终结论)"}
+
+event: reasoning_start
+data: {}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "频率"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "体量"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "数据"}
+
+event: token
+data: {"text": "展示了"}
+
+event: token
+data: {"text": "极高的"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "密度"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "仅"}
+
+event: token
+data: {"text": "在过去"}
+
+event: token
+data: {"text": "一周"}
+
+event: token
+data: {"text": "内"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "年"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "日至"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "日"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "就"}
+
+event: token
+data: {"text": "发布了"}
+
+event: token
+data: {"text": "至少"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "版本的"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "至"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "特别是"}
+
+event: token
+data: {"text": " Hermes"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "的最新"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "合并"}
+
+event: token
+data: {"text": "了"}
+
+event: token
+data: {"text": " ~"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": " PR"}
+
+event: token
+data: {"text": "s"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "次"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "已"}
+
+event: token
+data: {"text": "关闭"}
+
+event: token
+data: {"text": " Issue"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "这表明"}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "正在"}
+
+event: token
+data: {"text": "以"}
+
+event: token
+data: {"text": "极高的"}
+
+event: token
+data: {"text": "强度"}
+
+event: token
+data: {"text": "进行"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "工作"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "处理"}
+
+event: token
+data: {"text": "状态"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "虽然"}
+
+event: token
+data: {"text": "列表"}
+
+event: token
+data: {"text": "中有"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": " Open"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "状态的"}
+
+event: token
+data: {"text": " Issue"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "但这些"}
+
+event: token
+data: {"text": " Issue"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "大多是"}
+
+event: token
+data: {"text": "针对"}
+
+event: token
+data: {"text": "最新"}
+
+event: token
+data: {"text": "发布的"}
+
+event: token
+data: {"text": "紧急"}
+
+event: token
+data: {"text": " Bug"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "报告"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "自动"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": "失败"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "端口"}
+
+event: token
+data: {"text": "冲突"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "安装"}
+
+event: token
+data: {"text": "脚本"}
+
+event: token
+data: {"text": "卡"}
+
+event: token
+data: {"text": "死"}
+
+event: token
+data: {"text": "等"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "这些"}
+
+event: token
+data: {"text": " Issue"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "创建"}
+
+event: token
+data: {"text": "时间"}
+
+event: token
+data: {"text": "非常"}
+
+event: token
+data: {"text": "接近"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "均在"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "至"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "紧随"}
+
+event: token
+data: {"text": "重大"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": "之后"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "这"}
+
+event: token
+data: {"text": "通常"}
+
+event: token
+data: {"text": "意味着"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "正处于"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "响"}
+
+event: token
+data: {"text": "应用"}
+
+event: token
+data: {"text": "户"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "周期"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "而非"}
+
+event: token
+data: {"text": "停滞"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "参与度"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "总"}
+
+event: token
+data: {"text": " Star"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "数"}
+
+event: token
+data: {"text": "超过"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "万"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "涉及"}
+
+event: token
+data: {"text": "多个"}
+
+event: token
+data: {"text": "子"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "Core"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " Desktop"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "等"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "说明"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "拥有"}
+
+event: token
+data: {"text": "庞大的"}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "基础和"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "关注"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "综合"}
+
+event: token
+data: {"text": "评估"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "如此"}
+
+event: token
+data: {"text": "高的"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "频率"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "巨大的"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "变更"}
+
+event: token
+data: {"text": "量"}
+
+event: token
+data: {"text": "以及对"}
+
+event: token
+data: {"text": "即时"}
+
+event: token
+data: {"text": " Bug"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "持续"}
+
+event: token
+data: {"text": "追踪"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "显示"}
+
+event: token
+data: {"text": "该项目"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "高度"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "的开发"}
+
+event: token
+data: {"text": "和维护"}
+
+event: token
+data: {"text": "状态"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "尽管"}
+
+event: token
+data: {"text": "存在"}
+
+event: token
+data: {"text": "少量"}
+
+event: token
+data: {"text": "未"}
+
+event: token
+data: {"text": "解决的"}
+
+event: token
+data: {"text": "回归"}
+
+event: token
+data: {"text": " Bug"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "但这"}
+
+event: token
+data: {"text": "正是"}
+
+event: token
+data: {"text": "高"}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "项目的"}
+
+event: token
+data: {"text": "典型"}
+
+event: token
+data: {"text": "特征"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "快"}
+
+event: token
+data: {"text": "于"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "的速度"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: conclusion_start
+data: {}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "9"}
+
+event: error
+data: {"error": "LLM call failed: HTTPConnectionPool(host='127.0.0.1', port=4000): Read timed out."}
+
diff --git a/.review-tmp/sse_fix_verify2.txt b/.review-tmp/sse_fix_verify2.txt
new file mode 100644
index 0000000..992ddc0
--- /dev/null
+++ b/.review-tmp/sse_fix_verify2.txt
@@ -0,0 +1,1568 @@
+event: step
+data: {"step": "context", "content": "已收集上下文: 共 3 个项目, 14 个 release, 14 个 issue, 430166 stars"}
+
+event: step
+data: {"step": "prompt", "content": "[SYSTEM]\n你是 AI 项目分析助手, 基于以下数据进行分析。请用中文回答。\n\n[USER]\n分析数据: {\"scope_type\": \"home\", \"project_id\": null, \"data_summary\": \"共 3 个项目, 14 个 release, 14 个 issue, 430166 stars\", \"releases\": [{\"tag_name\": \"v2026.8.13\", \"name\": \"Hermes Agent v0.20.1 (2026.8.13)\", \"published_at\": \"2026-08-13T20:37:37Z\", \"body\": \"# Hermes Agent v0.20.1 (v2026.8.13)\\n\\n**Release Date:** August 13, 2026\\n\\n> Patch release. This tag rolls up the ~656 PRs merged since v0.20.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, and anyone installing from the latest tag).\\n\\n---\\n\\n## About this release\\n\\nSince v0.20.0 (August 3), this window landed **1,444 commits** across **~656 merged PRs**, touching **2,172 files** (+233,872 / −75,244), and closed **~481 issues**. It is a broad stabilization-an...\"}, {\"tag_name\": \"v1.18.18\", \"name\": \"v1.18.18\", \"published_at\": \"2026-08-13T01:15:04Z\", \"body\": \"## Core\\r\\n### Bugfixes\\r\\n- Select the Kimi system prompt correctly for official Moonshot and Kimi providers\\r\\n- Fix xhigh reasoning effort for xai models\\r\\n\"}, {\"tag_name\": \"v1.18.17\", \"name\": \"v1.18.17\", \"published_at\": \"2026-08-12T20:25:07Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Made session compaction keep complete recent turns and produce clearer summaries for smaller models.\\n- Added MERGE Gateway reasoning variants so those model options work correctly. (@MatthewFeroz)\\n- Capped automatic session retries and added jitter to reduce repeated retry storms.\\n- Enabled PDF attachments for GitHub Copilot models that advertise PDF vision support. (@stevenao)\\n- Applied the correct sampling defaults to DeepSeek V4 Flash on supported providers.\\n- Routed ...\"}, {\"tag_name\": \"v1.18.16\", \"name\": \"v1.18.16\", \"published_at\": \"2026-08-10T06:07:08Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Ignore unknown top-level config fields instead of failing config parsing\\n- Register projects opened from Home so they are available to the rest of the app\\n\\n## Desktop\\n\\n### Improvements\\n\\n- Open the project menu with a right-click in Home\\n\\n### Bugfixes\\n\\n- Fall back to listing and matching local directories when the project picker server does not support search\\n- Use 词元 instead of 令牌 for token-related labels in Simplified Chinese (@Speechlessmanbilibili)\\n- Keep the macOS ap...\"}, {\"tag_name\": \"v1.18.15\", \"name\": \"v1.18.15\", \"published_at\": \"2026-08-07T06:49:55Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Chronological message ordering now stays correct even when imported or legacy message IDs are out of order.\\n- Revert and fork actions now use real message chronology instead of message ID ordering.\\n- Truncation cleanup now removes stale files by file timestamp more reliably.\\n- Repeated compaction now keeps earlier tool-call history in summaries instead of dropping orphaned results.\\n- Blob-based attachments now load correctly in the web UI. (@rexdotsh)\\n\\n## Desktop\\n\\n### Im...\"}], \"issues\": [{\"title\": \"Desktop auto-update can wipe the desktop build on Windows (venv lock ignored on retry → hermes.exe quarantine downgraded → ZIP fallback deletes win-unpacked)\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:17:32Z\"}, {\"title\": \"hermes mcp login : OAuth callback port collision makes interactive login impossible on a headless host (v0.20.1) — #5344 regressed\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:10:39Z\"}, {\"title\": \"[Bug]: Install scripts gets stuck on Playwrite on Arch\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:47:33Z\"}, {\"title\": \"[Bug]: transform_llm_output callbacks do not have a guaranteed sequential safety pipeline\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:46:00Z\"}, {\"title\": \"AI_APICallError should be communicated via ACP\", \"state\": \"open\", \"created_at\": \"2026-08-15T22:05:12Z\"}, {\"title\": \"[BUG]: Session header is hidden until title generation\", \"state\": \"open\", \"created_at\": \"2026-08-15T21:42:16Z\"}, {\"title\": \"[Bug] Poe Provider Tool Failure (1.18.18)\", \"state\": \"open\", \"created_at\": \"2026-08-15T20:05:02Z\"}, {\"title\": \"[FEATURE]: Expose the current TUI model to plugins\", \"state\": \"open\", \"created_at\": \"2026-08-15T19:58:47Z\"}, {\"title\": \"[BUG] Two ordering-correctness gaps remain after the ID-rollover fix\", \"state\": \"open\", \"created_at\": \"2026-08-15T19:50:02Z\"}, {\"title\": \"سؤال\", \"state\": \"closed\", \"created_at\": \"2026-08-15T19:15:08Z\"}], \"stats\": {\"total_projects\": 3, \"total_releases\": 14, \"total_issues\": 14, \"total_stars\": 430166}}\n\n用户问题: 请基于最近 release + issue 评估项目活跃度 (1-10 分) 并给出证据\n\n要求: 严格按以下格式输出, 不要输出其他任何标记:\n===REASONING===\n(这里写你的分析推理过程: 数据观察、初步判断、深入推理)\n===CONCLUSION===\n(这里写最终结论)"}
+
+event: reasoning_start
+data: {}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "数据"}
+
+event: token
+data: {"text": "观察"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "频率"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "数据"}
+
+event: token
+data: {"text": "涵盖"}
+
+event: token
+data: {"text": "的时间"}
+
+event: token
+data: {"text": "范围"}
+
+event: token
+data: {"text": "极"}
+
+event: token
+data: {"text": "短"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "年"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "日至"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "日"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "短短"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "天内"}
+
+event: token
+data: {"text": "发布了"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "主要"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "至"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "及"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "特别是"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "一次性"}
+
+event: token
+data: {"text": "合并"}
+
+event: token
+data: {"text": "了"}
+
+event: token
+data: {"text": "约"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "PR"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "显示出"}
+
+event: token
+data: {"text": "巨大的"}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "吞吐量"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "问题"}
+
+event: token
+data: {"text": "处理"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "Issues"}
+
+event: token
+data: {"text": "中"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "有"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "是"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "Open"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "状态"}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "创建于"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "月"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "日"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "说明"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": "即时"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "正在"}
+
+event: token
+data: {"text": "积极"}
+
+event: token
+data: {"text": "接收"}
+
+event: token
+data: {"text": "和处理"}
+
+event: token
+data: {"text": "新问题"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "同时"}
+
+event: token
+data: {"text": "存在"}
+
+event: token
+data: {"text": "Closed"}
+
+event: token
+data: {"text": "状态"}
+
+event: token
+data: {"text": "的问题"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "表明"}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": "生命周期"}
+
+event: token
+data: {"text": "正常"}
+
+event: token
+data: {"text": "流转"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": "组件"}
+
+event: token
+data: {"text": "协同"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "涉及"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "Core"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "、“"}
+
+event: token
+data: {"text": "Desktop"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "、“"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "等多个"}
+
+event: token
+data: {"text": "模块"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "显示"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "架构"}
+
+event: token
+data: {"text": "复杂"}
+
+event: token
+data: {"text": "但"}
+
+event: token
+data: {"text": "维护"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "热度"}
+
+event: token
+data: {"text": "高"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "总"}
+
+event: token
+data: {"text": "Stars"}
+
+event: token
+data: {"text": "高达"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "表明"}
+
+event: token
+data: {"text": "拥有"}
+
+event: token
+data: {"text": "庞大的"}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "基础和"}
+
+event: token
+data: {"text": "关注"}
+
+event: token
+data: {"text": "度高"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "初步"}
+
+event: token
+data: {"text": "判断"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "基于"}
+
+event: token
+data: {"text": "极高的"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "量和"}
+
+event: token
+data: {"text": "频繁"}
+
+event: token
+data: {"text": "的版本"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "项目的"}
+
+event: token
+data: {"text": "技术"}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "水位"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "基于"}
+
+event: token
+data: {"text": "高密"}
+
+event: token
+data: {"text": "度的"}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "项目的"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "互动"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "响应"}
+
+event: token
+data: {"text": "速度"}
+
+event: token
+data: {"text": "也"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "极"}
+
+event: token
+data: {"text": "高水平"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "深入"}
+
+event: token
+data: {"text": "推理"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "正面"}
+
+event: token
+data: {"text": "因素"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "如此"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "Patch"}
+
+event: token
+data: {"text": " Release"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "到"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "仅在"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "天内"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "通常"}
+
+event: token
+data: {"text": "意味着"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "响应"}
+
+event: token
+data: {"text": "Bug"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "进行"}
+
+event: token
+data: {"text": "小幅"}
+
+event: token
+data: {"text": "功能"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "这是"}
+
+event: token
+data: {"text": "高"}
+
+event: token
+data: {"text": "成熟"}
+
+event: token
+data: {"text": "度"}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "高"}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "项目的"}
+
+event: token
+data: {"text": "典型"}
+
+event: token
+data: {"text": "特征"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": "的大"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": "也"}
+
+event: token
+data: {"text": "证明了"}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "引擎"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "持续"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "负面"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "风险"}
+
+event: token
+data: {"text": "因素"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "大量的"}
+
+event: token
+data: {"text": "Open"}
+
+event: token
+data: {"text": " Issues"}
+
+event: token
+data: {"text": "中"}
+
+event: token
+data: {"text": "包含了"}
+
+event: token
+data: {"text": "Critical"}
+
+event: token
+data: {"text": " Bug"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "Windows"}
+
+event: token
+data: {"text": "自动"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": "破坏"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "OAuth"}
+
+event: token
+data: {"text": "端口"}
+
+event: token
+data: {"text": "冲突"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "Play"}
+
+event: token
+data: {"text": "wright"}
+
+event: token
+data: {"text": "安装"}
+
+event: token
+data: {"text": "卡"}
+
+event: token
+data: {"text": "死"}
+
+event: token
+data: {"text": "等"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "虽然"}
+
+event: token
+data: {"text": "这"}
+
+event: token
+data: {"text": "反映了"}
+
+event: token
+data: {"text": "测试"}
+
+event: token
+data: {"text": "覆盖面"}
+
+event: token
+data: {"text": "广"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "基数"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "导致"}
+
+event: token
+data: {"text": "的问题"}
+
+event: token
+data: {"text": "暴露"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "但也"}
+
+event: token
+data: {"text": "暗示"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "超"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "下"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "质量"}
+
+event: token
+data: {"text": "保障"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "QA"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "可能"}
+
+event: token
+data: {"text": "面临"}
+
+event: token
+data: {"text": "压力"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "然而"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "度高"}
+
+event: token
+data: {"text": "并不"}
+
+event: token
+data: {"text": "等同于"}
+
+event: token
+data: {"text": "质量"}
+
+event: token
+data: {"text": "完美"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "但"}
+
+event: token
+data: {"text": "就"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "这一"}
+
+event: token
+data: {"text": "指标"}
+
+event: token
+data: {"text": "而言"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "这种"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "+"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "报"}
+
+event: token
+data: {"text": "Bug"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "的模式"}
+
+event: token
+data: {"text": "正是"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "活跃"}
+
+event: token
+data: {"text": "度的"}
+
+event: token
+data: {"text": "证明"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "综合"}
+
+event: token
+data: {"text": "评分"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "考虑到"}
+
+event: token
+data: {"text": "近"}
+
+event: token
+data: {"text": "一周"}
+
+event: token
+data: {"text": "内的"}
+
+event: token
+data: {"text": "密集"}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "活动"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "大量"}
+
+event: token
+data: {"text": "代码"}
+
+event: token
+data: {"text": "合并"}
+
+event: token
+data: {"text": "以及"}
+
+event: token
+data: {"text": "即"}
+
+event: token
+data: {"text": "时的"}
+
+event: token
+data: {"text": "社区"}
+
+event: token
+data: {"text": "反馈"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "该项目"}
+
+event: token
+data: {"text": "处于"}
+
+event: token
+data: {"text": "生命"}
+
+event: token
+data: {"text": "周期的"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "成长"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "高"}
+
+event: token
+data: {"text": "维护"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "活跃度"}
+
+event: token
+data: {"text": "极高"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: conclusion_start
+data: {}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "9"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "0"}
+
+: keepalive
+
+event: step
+data: {"step": "complete", "content": "分析完成, 总耗时 156.7 秒"}
+
diff --git a/.review-tmp/sse_fix_verify3.txt b/.review-tmp/sse_fix_verify3.txt
new file mode 100644
index 0000000..1571e0d
--- /dev/null
+++ b/.review-tmp/sse_fix_verify3.txt
@@ -0,0 +1,3020 @@
+event: step
+data: {"step": "context", "content": "已收集上下文: 共 3 个项目, 14 个 release, 14 个 issue, 430166 stars"}
+
+event: step
+data: {"step": "prompt", "content": "[SYSTEM]\n你是 AI 项目分析助手, 基于以下数据进行分析。请用中文回答。\n\n[USER]\n分析数据: {\"scope_type\": \"home\", \"project_id\": null, \"data_summary\": \"共 3 个项目, 14 个 release, 14 个 issue, 430166 stars\", \"releases\": [{\"tag_name\": \"v2026.8.13\", \"name\": \"Hermes Agent v0.20.1 (2026.8.13)\", \"published_at\": \"2026-08-13T20:37:37Z\", \"body\": \"# Hermes Agent v0.20.1 (v2026.8.13)\\n\\n**Release Date:** August 13, 2026\\n\\n> Patch release. This tag rolls up the ~656 PRs merged since v0.20.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, and anyone installing from the latest tag).\\n\\n---\\n\\n## About this release\\n\\nSince v0.20.0 (August 3), this window landed **1,444 commits** across **~656 merged PRs**, touching **2,172 files** (+233,872 / −75,244), and closed **~481 issues**. It is a broad stabilization-an...\"}, {\"tag_name\": \"v1.18.18\", \"name\": \"v1.18.18\", \"published_at\": \"2026-08-13T01:15:04Z\", \"body\": \"## Core\\r\\n### Bugfixes\\r\\n- Select the Kimi system prompt correctly for official Moonshot and Kimi providers\\r\\n- Fix xhigh reasoning effort for xai models\\r\\n\"}, {\"tag_name\": \"v1.18.17\", \"name\": \"v1.18.17\", \"published_at\": \"2026-08-12T20:25:07Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Made session compaction keep complete recent turns and produce clearer summaries for smaller models.\\n- Added MERGE Gateway reasoning variants so those model options work correctly. (@MatthewFeroz)\\n- Capped automatic session retries and added jitter to reduce repeated retry storms.\\n- Enabled PDF attachments for GitHub Copilot models that advertise PDF vision support. (@stevenao)\\n- Applied the correct sampling defaults to DeepSeek V4 Flash on supported providers.\\n- Routed ...\"}, {\"tag_name\": \"v1.18.16\", \"name\": \"v1.18.16\", \"published_at\": \"2026-08-10T06:07:08Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Ignore unknown top-level config fields instead of failing config parsing\\n- Register projects opened from Home so they are available to the rest of the app\\n\\n## Desktop\\n\\n### Improvements\\n\\n- Open the project menu with a right-click in Home\\n\\n### Bugfixes\\n\\n- Fall back to listing and matching local directories when the project picker server does not support search\\n- Use 词元 instead of 令牌 for token-related labels in Simplified Chinese (@Speechlessmanbilibili)\\n- Keep the macOS ap...\"}, {\"tag_name\": \"v1.18.15\", \"name\": \"v1.18.15\", \"published_at\": \"2026-08-07T06:49:55Z\", \"body\": \"## Core\\n\\n### Bugfixes\\n\\n- Chronological message ordering now stays correct even when imported or legacy message IDs are out of order.\\n- Revert and fork actions now use real message chronology instead of message ID ordering.\\n- Truncation cleanup now removes stale files by file timestamp more reliably.\\n- Repeated compaction now keeps earlier tool-call history in summaries instead of dropping orphaned results.\\n- Blob-based attachments now load correctly in the web UI. (@rexdotsh)\\n\\n## Desktop\\n\\n### Im...\"}], \"issues\": [{\"title\": \"Desktop auto-update can wipe the desktop build on Windows (venv lock ignored on retry → hermes.exe quarantine downgraded → ZIP fallback deletes win-unpacked)\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:17:32Z\"}, {\"title\": \"hermes mcp login : OAuth callback port collision makes interactive login impossible on a headless host (v0.20.1) — #5344 regressed\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:10:39Z\"}, {\"title\": \"[Bug]: Install scripts gets stuck on Playwrite on Arch\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:47:33Z\"}, {\"title\": \"[Bug]: transform_llm_output callbacks do not have a guaranteed sequential safety pipeline\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:46:00Z\"}, {\"title\": \"AI_APICallError should be communicated via ACP\", \"state\": \"open\", \"created_at\": \"2026-08-15T22:05:12Z\"}, {\"title\": \"[BUG]: Session header is hidden until title generation\", \"state\": \"open\", \"created_at\": \"2026-08-15T21:42:16Z\"}, {\"title\": \"[Bug] Poe Provider Tool Failure (1.18.18)\", \"state\": \"open\", \"created_at\": \"2026-08-15T20:05:02Z\"}, {\"title\": \"[FEATURE]: Expose the current TUI model to plugins\", \"state\": \"open\", \"created_at\": \"2026-08-15T19:58:47Z\"}, {\"title\": \"[BUG] Two ordering-correctness gaps remain after the ID-rollover fix\", \"state\": \"open\", \"created_at\": \"2026-08-15T19:50:02Z\"}, {\"title\": \"سؤال\", \"state\": \"closed\", \"created_at\": \"2026-08-15T19:15:08Z\"}], \"stats\": {\"total_projects\": 3, \"total_releases\": 14, \"total_issues\": 14, \"total_stars\": 430166}}\n\n用户问题: 请按时间列 5 个关键 release 的变更重点, 给出演进方向判断\n\n要求: 严格按以下格式输出, 不要输出其他任何标记:\n===REASONING===\n(这里写你的分析推理过程: 数据观察、初步判断、深入推理)\n===CONCLUSION===\n(这里写最终结论: 先给一句总结论, 再用 2-3 句概括关键依据, 不少于 50 字)"}
+
+event: reasoning_start
+data: {}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "数据"}
+
+event: token
+data: {"text": "观察"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "要求"}
+
+event: token
+data: {"text": "分析"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "关键"}
+
+event: token
+data: {"text": "release"}
+
+event: token
+data: {"text": "”,"}
+
+event: token
+data: {"text": "但"}
+
+event: token
+data: {"text": "提供的"}
+
+event: token
+data: {"text": "`"}
+
+event: token
+data: {"text": "re"}
+
+event: token
+data: {"text": "leases"}
+
+event: token
+data: {"text": "`"}
+
+event: token
+data: {"text": "数组"}
+
+event: token
+data: {"text": "中"}
+
+event: token
+data: {"text": "仅"}
+
+event: token
+data: {"text": "包含"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "条目"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "这"}
+
+event: token
+data: {"text": "恰好"}
+
+event: token
+data: {"text": "是"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "且"}
+
+event: token
+data: {"text": "时间"}
+
+event: token
+data: {"text": "跨度"}
+
+event: token
+data: {"text": "从"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "至"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "覆盖了"}
+
+event: token
+data: {"text": "最新"}
+
+event: token
+data: {"text": "的功能"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": " ("}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": ")**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "重点"}
+
+event: token
+data: {"text": "在于"}
+
+event: token
+data: {"text": "消息"}
+
+event: token
+data: {"text": "排序"}
+
+event: token
+data: {"text": "正确"}
+
+event: token
+data: {"text": "性"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "会话"}
+
+event: token
+data: {"text": "压缩"}
+
+event: token
+data: {"text": "逻辑"}
+
+event: token
+data: {"text": "优化"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "文件"}
+
+event: token
+data: {"text": "清理"}
+
+event: token
+data: {"text": "可靠性"}
+
+event: token
+data: {"text": "以及"}
+
+event: token
+data: {"text": "Web"}
+
+event: token
+data: {"text": " UI"}
+
+event: token
+data: {"text": "附件"}
+
+event: token
+data: {"text": "加载"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "体现了"}
+
+event: token
+data: {"text": "对"}
+
+event: token
+data: {"text": "基础"}
+
+event: token
+data: {"text": "交互"}
+
+event: token
+data: {"text": "稳定性和"}
+
+event: token
+data: {"text": "数据"}
+
+event: token
+data: {"text": "一致"}
+
+event: token
+data: {"text": "性的"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": " ("}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": ")**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "增加了"}
+
+event: token
+data: {"text": "桌面"}
+
+event: token
+data: {"text": "端"}
+
+event: token
+data: {"text": "右键"}
+
+event: token
+data: {"text": "菜单"}
+
+event: token
+data: {"text": "功能"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "改"}
+
+event: token
+data: {"text": "进了"}
+
+event: token
+data: {"text": "本地"}
+
+event: token
+data: {"text": "目录"}
+
+event: token
+data: {"text": "匹配"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "特别是"}
+
+event: token
+data: {"text": "引入了"}
+
+event: token
+data: {"text": "简体中文"}
+
+event: token
+data: {"text": "术语"}
+
+event: token
+data: {"text": "本地"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "词"}
+
+event: token
+data: {"text": "元"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "替代"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "令牌"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "表明"}
+
+event: token
+data: {"text": "国际化"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "i"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "n"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "和本"}
+
+event: token
+data: {"text": "地"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "工作的"}
+
+event: token
+data: {"text": "推进"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": " ("}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": ")**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "更新"}
+
+event: token
+data: {"text": "包括"}
+
+event: token
+data: {"text": "会话"}
+
+event: token
+data: {"text": "压缩"}
+
+event: token
+data: {"text": "改进"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "针对"}
+
+event: token
+data: {"text": "小"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": ")、"}
+
+event: token
+data: {"text": "新增"}
+
+event: token
+data: {"text": "MER"}
+
+event: token
+data: {"text": "GE"}
+
+event: token
+data: {"text": " Gateway"}
+
+event: token
+data: {"text": "推理"}
+
+event: token
+data: {"text": "变"}
+
+event: token
+data: {"text": "体"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "限制"}
+
+event: token
+data: {"text": "自动"}
+
+event: token
+data: {"text": "重试"}
+
+event: token
+data: {"text": "风暴"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "支持"}
+
+event: token
+data: {"text": "GitHub"}
+
+event: token
+data: {"text": " Cop"}
+
+event: token
+data: {"text": "ilot"}
+
+event: token
+data: {"text": "模型的"}
+
+event: token
+data: {"text": "PDF"}
+
+event: token
+data: {"text": "附件"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "以及"}
+
+event: token
+data: {"text": "Deep"}
+
+event: token
+data: {"text": "Seek"}
+
+event: token
+data: {"text": " V"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": " Flash"}
+
+event: token
+data: {"text": "采样"}
+
+event: token
+data: {"text": "默认"}
+
+event: token
+data: {"text": "值"}
+
+event: token
+data: {"text": "调整"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "这"}
+
+event: token
+data: {"text": "显示"}
+
+event: token
+data: {"text": "了对"}
+
+event: token
+data: {"text": "新"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "支持"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "Deep"}
+
+event: token
+data: {"text": "Seek"}
+
+event: token
+data: {"text": " V"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "和多"}
+
+event: token
+data: {"text": "模"}
+
+event: token
+data: {"text": "态"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "PDF"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "能力的"}
+
+event: token
+data: {"text": "增强"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "以及"}
+
+event: token
+data: {"text": "系统"}
+
+event: token
+data: {"text": "鲁"}
+
+event: token
+data: {"text": "棒"}
+
+event: token
+data: {"text": "性"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "重试"}
+
+event: token
+data: {"text": "风暴"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "优化"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": " ("}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ")**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "仅"}
+
+event: token
+data: {"text": "包含"}
+
+event: token
+data: {"text": "两个"}
+
+event: token
+data: {"text": "Bug"}
+
+event: token
+data: {"text": "fix"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "修正"}
+
+event: token
+data: {"text": "K"}
+
+event: token
+data: {"text": "imi"}
+
+event: token
+data: {"text": "系统"}
+
+event: token
+data: {"text": "提示"}
+
+event: token
+data: {"text": "词"}
+
+event: token
+data: {"text": "选择和"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "x"}
+
+event: token
+data: {"text": "ai"}
+
+event: token
+data: {"text": "模型的"}
+
+event: token
+data: {"text": "推理"}
+
+event: token
+data: {"text": "力度"}
+
+event: token
+data: {"text": "设置"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "这是"}
+
+event: token
+data: {"text": "针对"}
+
+event: token
+data: {"text": "特定"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "集成"}
+
+event: token
+data: {"text": "细节"}
+
+event: token
+data: {"text": "的快速"}
+
+event: token
+data: {"text": "修补"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": " ("}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": " ("}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "-"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ")**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "这是一个"}
+
+event: token
+data: {"text": "大的"}
+
+event: token
+data: {"text": "聚合"}
+
+event: token
+data: {"text": "补丁"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "汇总"}
+
+event: token
+data: {"text": "了"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "以来的"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "PR"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "Issue"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "涉及"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "个"}
+
+event: token
+data: {"text": "提交"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "描述"}
+
+event: token
+data: {"text": "为"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "b"}
+
+event: token
+data: {"text": "road"}
+
+event: token
+data: {"text": " stabilization"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "广泛"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "面向"}
+
+event: token
+data: {"text": "下游"}
+
+event: token
+data: {"text": "消费者"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "D"}
+
+event: token
+data: {"text": "ocker"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "托管"}
+
+event: token
+data: {"text": "部署"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "这表明"}
+
+event: token
+data: {"text": "底层"}
+
+event: token
+data: {"text": "引擎"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": "正在"}
+
+event: token
+data: {"text": "经历"}
+
+event: token
+data: {"text": "高频"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "后的"}
+
+event: token
+data: {"text": "集中"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "期"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "初步"}
+
+event: token
+data: {"text": "判断"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "分为"}
+
+event: token
+data: {"text": "两个"}
+
+event: token
+data: {"text": "主要"}
+
+event: token
+data: {"text": "产品线"}
+
+event: token
+data: {"text": "或"}
+
+event: token
+data: {"text": "模块"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "一个是"}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "-facing"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "客户端"}
+
+event: token
+data: {"text": "应用"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": ".x"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "另一个"}
+
+event: token
+data: {"text": "是"}
+
+event: token
+data: {"text": "后端"}
+
+event: token
+data: {"text": "/"}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "引擎"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": ".x"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "客户端"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": ".x"}
+
+event: token
+data: {"text": "系列"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "短短"}
+
+event: token
+data: {"text": "一周"}
+
+event: token
+data: {"text": "内"}
+
+event: token
+data: {"text": "发布了"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "个小"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "节奏"}
+
+event: token
+data: {"text": "非常快"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "主要"}
+
+event: token
+data: {"text": "聚焦"}
+
+event: token
+data: {"text": "于"}
+
+event: token
+data: {"text": "Bug"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "特定"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "支持"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "K"}
+
+event: token
+data: {"text": "imi"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " x"}
+
+event: token
+data: {"text": "ai"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": " Deep"}
+
+event: token
+data: {"text": "Seek"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "用户体验"}
+
+event: token
+data: {"text": "微调"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "本地"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "PDF"}
+
+event: token
+data: {"text": "支持"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " "}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "引擎"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "版本"}
+
+event: token
+data: {"text": "进行了"}
+
+event: token
+data: {"text": "大规模"}
+
+event: token
+data: {"text": "整合"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "强调"}
+
+event: token
+data: {"text": "稳定性"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "暗示"}
+
+event: token
+data: {"text": "之前的"}
+
+event: token
+data: {"text": "开发"}
+
+event: token
+data: {"text": "可能"}
+
+event: token
+data: {"text": "过于"}
+
+event: token
+data: {"text": "激进"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "现在"}
+
+event: token
+data: {"text": "进入"}
+
+event: token
+data: {"text": "收敛"}
+
+event: token
+data: {"text": "阶段"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "深入"}
+
+event: token
+data: {"text": "推理"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "方向"}
+
+event: token
+data: {"text": "判断"}
+
+event: token
+data: {"text": "**"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "方向"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "集成"}
+
+event: token
+data: {"text": "多样化"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "专业化"}
+
+event: token
+data: {"text": "**。"}
+
+event: token
+data: {"text": "从"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "支持"}
+
+event: token
+data: {"text": "Deep"}
+
+event: token
+data: {"text": "Seek"}
+
+event: token
+data: {"text": " V"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": " Flash"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "GitHub"}
+
+event: token
+data: {"text": " Cop"}
+
+event: token
+data: {"text": "ilot"}
+
+event: token
+data: {"text": " PDF"}
+
+event: token
+data: {"text": " vision"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "到"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "K"}
+
+event: token
+data: {"text": "imi"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "x"}
+
+event: token
+data: {"text": "ai"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "配置"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "可以看出"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "正"}
+
+event: token
+data: {"text": "积极"}
+
+event: token
+data: {"text": "扩展"}
+
+event: token
+data: {"text": "对"}
+
+event: token
+data: {"text": "前沿"}
+
+event: token
+data: {"text": "大"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "特定"}
+
+event: token
+data: {"text": "提供商"}
+
+event: token
+data: {"text": "的支持"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "方向"}
+
+event: token
+data: {"text": "是"}
+
+event: token
+data: {"text": "成为一个"}
+
+event: token
+data: {"text": "更"}
+
+event: token
+data: {"text": "通用的"}
+
+event: token
+data: {"text": "AI"}
+
+event: token
+data: {"text": "聚合"}
+
+event: token
+data: {"text": "平台"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "兼容"}
+
+event: token
+data: {"text": "更多"}
+
+event: token
+data: {"text": "异构"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "能力"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "如"}
+
+event: token
+data: {"text": "PDF"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": "模"}
+
+event: token
+data: {"text": "态"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "不同"}
+
+event: token
+data: {"text": "推理"}
+
+event: token
+data: {"text": "策略"}
+
+event: token
+data: {"text": ")。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "方向"}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "系统"}
+
+event: token
+data: {"text": "稳定性"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "鲁"}
+
+event: token
+data: {"text": "棒"}
+
+event: token
+data: {"text": "性"}
+
+event: token
+data: {"text": "优先"}
+
+event: token
+data: {"text": "**。"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": "消息"}
+
+event: token
+data: {"text": "排序"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "文件"}
+
+event: token
+data: {"text": "清理"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "7"}
+
+event: token
+data: {"text": "限制"}
+
+event: token
+data: {"text": "重试"}
+
+event: token
+data: {"text": "风暴"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "以及"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": " v"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "2"}
+
+event: token
+data: {"text": "0"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "广泛"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "性质"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "都"}
+
+event: token
+data: {"text": "表明"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "当前"}
+
+event: token
+data: {"text": "阶段"}
+
+event: token
+data: {"text": "的核心"}
+
+event: token
+data: {"text": "任务是"}
+
+event: token
+data: {"text": "消除"}
+
+event: token
+data: {"text": "技术"}
+
+event: token
+data: {"text": "债务"}
+
+event: token
+data: {"text": ","}
+
+: keepalive
+
+event: token
+data: {"text": "确保"}
+
+event: token
+data: {"text": "在高"}
+
+event: token
+data: {"text": "负载"}
+
+event: token
+data: {"text": "或多"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "场景"}
+
+event: token
+data: {"text": "下的"}
+
+event: token
+data: {"text": "系统"}
+
+event: token
+data: {"text": "稳定性"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "而非"}
+
+event: token
+data: {"text": "单纯"}
+
+event: token
+data: {"text": "追求"}
+
+event: token
+data: {"text": "新功能"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "  "}
+
+event: token
+data: {"text": " -"}
+
+event: token
+data: {"text": " **"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "方向"}
+
+event: token
+data: {"text": "3"}
+
+event: token
+data: {"text": ":"}
+
+event: token
+data: {"text": "用户体验"}
+
+event: token
+data: {"text": "本地"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "精细化"}
+
+event: token
+data: {"text": "**。"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "6"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "中文"}
+
+event: token
+data: {"text": "术语"}
+
+event: token
+data: {"text": "本地"}
+
+event: token
+data: {"text": "化和"}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "5"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "会话"}
+
+event: token
+data: {"text": "压缩"}
+
+event: token
+data: {"text": "优化"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "针对"}
+
+event: token
+data: {"text": "小"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "生成"}
+
+event: token
+data: {"text": "更"}
+
+event: token
+data: {"text": "清晰"}
+
+event: token
+data: {"text": "摘要"}
+
+event: token
+data: {"text": "),"}
+
+event: token
+data: {"text": "显示"}
+
+event: token
+data: {"text": "团队"}
+
+event: token
+data: {"text": "在"}
+
+event: token
+data: {"text": "关注"}
+
+event: token
+data: {"text": "非"}
+
+event: token
+data: {"text": "英语"}
+
+event: token
+data: {"text": "用户"}
+
+event: token
+data: {"text": "群体"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "不同"}
+
+event: token
+data: {"text": "算力"}
+
+event: token
+data: {"text": "条件下的"}
+
+event: token
+data: {"text": "用户体验"}
+
+event: token
+data: {"text": "细节"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "\n\n"}
+
+event: conclusion_start
+data: {}
+
+event: token
+data: {"text": "\n"}
+
+event: token
+data: {"text": "项目"}
+
+event: token
+data: {"text": "当前"}
+
+event: token
+data: {"text": "演进"}
+
+event: token
+data: {"text": "方向"}
+
+event: token
+data: {"text": "明确"}
+
+event: token
+data: {"text": "聚焦"}
+
+event: token
+data: {"text": "于"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "生态"}
+
+event: token
+data: {"text": "集成"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "与"}
+
+event: token
+data: {"text": "“"}
+
+event: token
+data: {"text": "系统"}
+
+event: token
+data: {"text": "稳定性"}
+
+event: token
+data: {"text": "强化"}
+
+event: token
+data: {"text": "”"}
+
+event: token
+data: {"text": "双"}
+
+event: token
+data: {"text": "轨"}
+
+event: token
+data: {"text": "并行"}
+
+event: token
+data: {"text": "。"}
+
+event: token
+data: {"text": "一方面"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "通过"}
+
+event: token
+data: {"text": "快速"}
+
+event: token
+data: {"text": "迭代"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "v"}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "."}
+
+event: token
+data: {"text": "1"}
+
+event: token
+data: {"text": "8"}
+
+event: token
+data: {"text": ".x"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "积极"}
+
+event: token
+data: {"text": "适配"}
+
+event: token
+data: {"text": "Deep"}
+
+event: token
+data: {"text": "Seek"}
+
+event: token
+data: {"text": " V"}
+
+event: token
+data: {"text": "4"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "Cop"}
+
+event: token
+data: {"text": "ilot"}
+
+event: token
+data: {"text": "等新"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "及"}
+
+event: token
+data: {"text": "多"}
+
+event: token
+data: {"text": "模"}
+
+event: token
+data: {"text": "态"}
+
+event: token
+data: {"text": "能力"}
+
+event: token
+data: {"text": ";"}
+
+event: token
+data: {"text": "另一方面"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "通过"}
+
+event: token
+data: {"text": "H"}
+
+event: token
+data: {"text": "erm"}
+
+event: token
+data: {"text": "es"}
+
+event: token
+data: {"text": " Agent"}
+
+event: token
+data: {"text": "的大"}
+
+event: token
+data: {"text": "规模"}
+
+event: token
+data: {"text": "稳定"}
+
+event: token
+data: {"text": "化"}
+
+event: token
+data: {"text": "发布"}
+
+event: token
+data: {"text": "和"}
+
+event: token
+data: {"text": "核心"}
+
+event: token
+data: {"text": "模块"}
+
+event: token
+data: {"text": "("}
+
+event: token
+data: {"text": "会话"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "重试"}
+
+event: token
+data: {"text": "、"}
+
+event: token
+data: {"text": "排序"}
+
+event: token
+data: {"text": ")"}
+
+event: token
+data: {"text": "的"}
+
+event: token
+data: {"text": "Bug"}
+
+event: token
+data: {"text": "修复"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "着力"}
+
+event: token
+data: {"text": "解决"}
+
+event: token
+data: {"text": "技术"}
+
+event: token
+data: {"text": "债务"}
+
+event: token
+data: {"text": ","}
+
+event: token
+data: {"text": "提升"}
+
+event: token
+data: {"text": "系统"}
+
+event: token
+data: {"text": "在高"}
+
+event: token
+data: {"text": "并发"}
+
+event: token
+data: {"text": "和新"}
+
+event: token
+data: {"text": "模型"}
+
+event: token
+data: {"text": "支持"}
+
+event: token
+data: {"text": "下的"}
+
+event: token
+data: {"text": "鲁"}
+
+event: token
+data: {"text": "棒"}
+
+event: token
+data: {"text": "性与"}
+
+event: token
+data: {"text": "用户体验"}
+
+event: token
+data: {"text": "一致性"}
+
+event: token
+data: {"text": "。"}
+
+event: step
+data: {"step": "complete", "content": "分析完成, 总耗时 18.1 秒"}
+
diff --git a/.review-tmp/sse_test.txt b/.review-tmp/sse_test.txt
new file mode 100644
index 0000000..ad7b86a
--- /dev/null
+++ b/.review-tmp/sse_test.txt
@@ -0,0 +1,3 @@
+event: error
+data: {"error": "Working outside of application context.\n\nThis typically means that you attempted to use functionality that needed\nthe current application. To solve this, set up an application context\nwith app.app_context(). See the documentation for more information."}
+
diff --git a/.review-tmp/sse_test2.txt b/.review-tmp/sse_test2.txt
new file mode 100644
index 0000000..e62dc3f
--- /dev/null
+++ b/.review-tmp/sse_test2.txt
@@ -0,0 +1,6 @@
+event: step
+data: {"step": "context", "content": "已收集上下文: 项目: hermes-agent (NousResearch/hermes-agent), 231080 stars"}
+
+event: step
+data: {"step": "prompt", "content": "[SYSTEM]\n你是 AI 项目分析助手, 基于以下数据进行分析。请用中文回答。\n\n[USER]\n分析数据: {\"scope_type\": \"project/1\", \"project_id\": 1, \"data_summary\": \"项目: hermes-agent (NousResearch/hermes-agent), 231080 stars\", \"releases\": [{\"id\": 1, \"project_id\": 1, \"tag_name\": \"v2026.8.13\", \"name\": \"Hermes Agent v0.20.1 (2026.8.13)\", \"body\": \"# Hermes Agent v0.20.1 (v2026.8.13)\\n\\n**Release Date:** August 13, 2026\\n\\n> Patch release. This tag rolls up the ~656 PRs merged since v0.20.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, and anyone installing from the latest tag).\\n\\n---\\n\\n## About this release\\n\\nSince v0.20.0 (August 3), this window landed **1,444 commits** across **~656 merged PRs**, touching **2,172 files** (+233,872 / −75,244), and closed **~481 issues**. It is a broad stabilization-and-fixes rollup spanning the desktop app, gateway platforms, installers, tool system, and provider catalogs.\\n\\n**Full curated release notes for this window will ship with v0.21.0**, which will document everything from v0.20.0 onward — highlights, feature areas, and complete contributor credits. Nothing in this window is skipped.\\n\\n## Updating\\n\\n- Existing installs: `hermes update`\\n- Fresh install: see the installer one-liner in the [README](https://github.com/NousResearch/hermes-agent#readme)\\n\\n**Full Changelog**: [v2026.8.3...v2026.8.13](https://github.com/NousResearch/hermes-agent/compare/v2026.8.3...v2026.8.13)\\n\", \"published_at\": \"2026-08-13T20:37:37Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/releases/tag/v2026.8.13\"}, {\"id\": 2, \"project_id\": 1, \"tag_name\": \"v2026.8.3\", \"name\": \"Hermes Agent v0.20.0 (2026.8.3)\", \"body\": \"# Hermes Agent v0.20.0 (v2026.8.3)\\n\\n\\n**Release Date:** August 3, 2026\\n**Since v0.19.0:** ~3,650 commits · ~1,400 merged PRs · ~5,200 files changed · ~559,000 insertions · ~405,000 deletions · **~1,200 issues closed** · 650+ contributors\\n\\n> **The Herald Release.** Hermes is the herald of the gods, and this release makes him one in earnest: he **speaks** (real-time conversational voice with streaming TTS, barge-in, on-device wake words, and hands-free control across the CLI, desktop, and every audio-capable gateway platform), he **carries word to other agents** (A2A v1.0), he **announces events to your systems** (signed outbound webhooks), and he **cites his sources** (grounded research with verifiable citations and fact-checking). Around that spine: the desktop app became a platform (artifacts with live preview, a plugin SDK, quick-entry from anywhere, multiple windows), the CLI got a wave of power commands (`!` shell mode, `/init`, `/diff`, `/context`, `/focus`), compression got smarter and gentler, and the tools themselves now recover from their own failures instead of making the model guess. This release rolls up everything from the v0.19.1 infrastructure patch tag — that window is fully documented here.\\n\\n---\\n\\n## ✨ Highlights\\n\\n- **Talk to Hermes — streaming, conversational voice with barge-in** — Voice mode used to mean: speak, wait for the whole reply to generate, then listen to one long audio file. Now Hermes speaks clause-by-clause as the response streams, you can interrupt it mid-sentence by just talking (it stops, listens, and the model is told you cut in), and busy-aware silence detection means it doesn't talk over you. This works in CLI voice mode, on the desktop, and through gateway adapters. Talking to Hermes finally feels like a conversation, not a voicemail exchange. ([#69511](https://github.com/NousResearch/hermes-agent/pull/69511), [#73862](https://github.com/NousResearch/hermes-agent/pull/73862), [#74223](https://github.com/NousResearch/hermes-agent/pull/74223), [#74000](https://github.com/NousResearch/hermes-agent/pull/74000), [#69602](https://github.com/NousResearch/hermes-agent/pull/69602) — @teknium1, @OutThisLife)\\n\\n- **Wake words and hands-free control** — Say your own open-vocabulary wake phrase (\\\"hey Hermes\\\", or anything you pick) and Hermes starts listening — detection runs on-device, so no audio leaves your machine while it waits. Multi-profile voice routing means different wake words can reach different profiles, and saying \\\"stop\\\" ends the voice chat on every surface without touching the keyboard. Your terminal is now something you can talk to from across the room. ([#70509](https://github.com/NousResearch/hermes-agent/pull/70509), [#73106](https://github.com/NousResearch/hermes-agent/pull/73106), [#73933](https://github.com/NousResearch/hermes-agent/pull/73933) — @teknium1)\\n\\n- **Voice on every platform** — Send a voice note to Hermes on WhatsApp, Feishu, DingTalk, LINE, QQ, Photon, or Weixin and it's transcribed and answered; auto-TTS replies are delivered platform-aware (opus where platforms want opus, captions attached correctly). STT is now fully configurable — its own `hermes tools` category, GUI toggles, dashboard dropdowns, unified language resolution so transcripts stop coming back in the wrong language, and OpenAI's gpt-transcribe support. One unified spoken-text preprocessor cleans markdown, code, and URLs out of speech across all TTS providers. ([#73515](https://github.com/NousResearch/hermes-agent/pull/73515), [#73508](https://github.com/NousResearch/hermes-agent/pull/73508), [#73910](https://github.com/NousResearch/hermes-agent/pull/73910), [#73513](https://github.com/NousResearch/hermes-agent/pull/73513), [#73067](https://github.com/NousResearch/hermes-agent/pull/73067) — @teknium1)\\n\\n- **Research you can trust — grounded citations with fact-checking** — The new `grounded-citations` skill makes Hermes produce research where every claim is backed by a verifiable source: quotes are matched against the actual page text (not hallucinated), citations link to the exact evidence, and a fact-checking mode turns the same machinery on any document or claim you hand it — it tells you what checks out, what doesn't, and what couldn't be verified. If you use Hermes for research, this is the difference between \\\"sounds right\\\" and \\\"provably sourced.\\\" ([#71698](https://github.com/NousResearch/hermes-agent/pull/71698), [#77104](https://github.com/NousResearch/hermes-agent/pull/77104) — @teknium1)\\n\\n- **Outbound webhooks — Hermes pushes events to your systems** — Until now, integrating with Hermes meant polling or listening on a platform. Now Hermes pushes **signed lifecycle events** (session activity, turn completions, tool events) to any HTTP endpoint you register — with HMAC signatures so your receiver can verify authenticity. Wire Hermes into your CI, your home automation, your dashboards, or any service that speaks HTTP, with no polling loop. ([#69406](https://github.com/NousResearch/hermes-agent/pull/69406) — @teknium1)\\n\\n- **The desktop app becomes a platform — artifacts, plugin SDK, quick entry** — Hermes desktop now renders **artifacts**: versioned cards with sandboxed live preview in a right-rail viewer, so generated HTML/apps run safely next to the chat. A real **plugin SDK** landed with Kanban as its founding plugin, `ctx.download` for handing users files, floating pane placement, and multiple GUI windows. A global-hotkey **quick-entry window** captures a thought into any session from anywhere in your OS. The desktop stopped being a chat client and started being a workbench. ([#72345](https://github.com/NousResearch/hermes-agent/pull/72345), [#61173](https://github.com/NousResearch/hermes-agent/pull/61173), [#74413](https://github.com/NousResearch/hermes-agent/pull/74413), [#72315](https://github.com/NousResearch/hermes-agent/pull/72315), [#68259](https://github.com/NousResearch/hermes-agent/pull/68259), [#73143](https://github.com/NousResearch/hermes-agent/pull/73143) — @OutThisLife, @teknium1)\\n\\n- **Hermes speaks Agent-to-Agent — A2A v1.0** — A new bundled plugin implements the Agent-to-Agent protocol, so Hermes can discover, talk to, and be driven by other A2A-compatible agents. This closes issue #514 — one of the oldest open feature requests in the repo. If you're building multi-agent systems with heterogeneous stacks, Hermes now has a standard wire protocol for joining them. ([#77109](https://github.com/NousResearch/hermes-agent/pull/77109) — @teknium1)\\n\\n- **CLI power-user wave** — `!command` runs a shell command instantly without spending a model turn. `/init` scans your project and generates (or updates) an `AGENTS.md`. `/diff` shows staged/all/session changes from any surface, `/context` breaks down exactly what's filling your context window, `/focus` gives you a reduced-output view with hidden-line recovery, and Ctrl+S stashes a half-written prompt into a browsable panel. Plus `hermes import-agent` migrates your Claude Code or Codex CLI setup into Hermes in one command. ([#72257](https://github.com/NousResearch/hermes-agent/pull/72257), [#72178](https://github.com/NousResearch/hermes-agent/pull/72178), [#72240](https://github.com/NousResearch/hermes-agent/pull/72240), [#72242](https://github.com/NousResearch/hermes-agent/pull/72242), [#72302](https://github.com/NousResearch/hermes-agent/pull/72302), [#72262](https://github.com/NousResearch/hermes-agent/pull/72262), [#72190](https://github.com/NousResearch/hermes-agent/pull/72190) — @teknium1, several salvaging long-standing community PRs)\\n\\n- **Correct the agent mid-turn — redirects** — If Hermes is heading the wrong way, you no longer have to `/stop` and re-explain. Type a correction while it works and the active turn is redirected: work in flight is preserved, the original prompt is kept, and the agent course-corrects with your new guidance. Paired with double-ESC draft discard and a composer undo stack, steering feels like editing, not restarting. ([#63104](https://github.com/NousResearch/hermes-agent/pull/63104), [#72339](https://github.com/NousResearch/hermes-agent/pull/72339), [#74736](https://github.com/NousResearch/hermes-agent/pull/74736) — @OutThisLife)\\n\\n- **Tools that fix themselves** — A sweep of self-recovery upgrades means the agent wastes far fewer turns on tool friction: truncated terminal output spills to a file the agent can read back, `patch` detects already-applied edits and diagnoses whitespace mismatches, `write_file` verifies content on disk, searches that match nothing probe for near-misses and recover, and common failure classes come back with actionable hints. The default tool-calling iteration limit also jumped 90 → 500 — long autonomous runs stopped hitting an artificial wall. ([#77041](https://github.com/NousResearch/hermes-agent/pull/77041), [#76998](https://github.com/NousResearch/hermes-agent/pull/76998), [#77024](https://github.com/NousResearch/hermes-agent/pull/77024), [#77055](https://github.com/NousResearch/hermes-agent/pull/77055), [#77011](https://github.com/NousResearch/hermes-agent/pull/77011), [#76992](https://github.com/NousResearch/hermes-agent/pull/76992), [#72176](https://github.com/NousResearch/hermes-agent/pull/72176) — @teknium1)\\n\\n- **Compression that respects your conversation** — Context compression got a deep overhaul: proactive tool-result pruning for large-window models, per-turn micro-compaction that amortizes the cost instead of one giant pause, a guaranteed N-user-message tail so recent conversation always survives, progress-aware timeouts that stop punishing slow summary models, and ghost-skill defense so a pruned skill can never silently haunt a session. Thresholds are now configurable per-model and in absolute tokens. Long sessions stay coherent and stop stalling. ([#70254](https://github.com/NousResearch/hermes-agent/pull/70254), [#75345](https://github.com/NousResearch/hermes-agent/pull/75345), [#70250](https://github.com/NousResearch/hermes-agent/pull/70250), [#71508](https://github.com/NousResearch/hermes-agent/pull/71508), [#70275](https://github.com/NousResearch/hermes-agent/pull/70275) — @teknium1, @kshitijk4poor, salvaging multiple community PRs)\\n\\n- **Smart approvals grow up** — `hermes approvals suggest` mines your approval history into allowlist proposals, operators can customize the smart-approval policy, a consecutive-denial circuit breaker stops a misbehaving loop cold, and desktop pairing approvals are profile-correct with a proper surface to answer them from. Plus a new approval gate for docker/podman daemon-redirect commands. Less clicking \\\"approve\\\", without giving an inch of control. ([#72259](https://github.com/NousResearch/hermes-agent/pull/72259), [#72186](https://github.com/NousResearch/hermes-agent/pull/72186), [#72203](https://github.com/NousResearch/hermes-agent/pull/72203), [#74446](https://github.com/NousResearch/hermes-agent/pull/74446), [#71092](https://github.com/NousResearch/hermes-agent/pull/71092) — @teknium1, @OutThisLife)\\n\\n- **Faster everywhere, again** — Prompt caching now covers tool schemas on native Anthropic without history loss. `hermes -w` cold start dropped ~14s → ~1.8s, `hermes update` no-ops got 2–6s faster, heavy SDKs lazy-load off the import path, config reads stopped deep-copying (54× faster on the telemetry gate), and the desktop shipped a second 60fps wave — streaming cost independent of transcript length, drag at 60fps with five streaming tabs, idle CPU near zero in the background. ([#76032](https://github.com/NousResearch/hermes-agent/pull/76032), [#71637](https://github.com/NousResearch/hermes-agent/pull/71637), [#74218](https://github.com/NousResearch/hermes-agent/pull/74218), [#74204](https://github.com/NousResearch/hermes-agent/pull/74204), [#71835](https://github.com/NousResearch/hermes-agent/pull/71835), [#72346](https://github.com/NousResearch/hermes-agent/pull/72346), [#75218](https://github.com/NousResearch/hermes-agent/pull/75218) — @kshitijk4poor, @teknium1, @OutThisLife)\\n\\n- **New places to run and be reached** — Buzz lands as a bundled gateway platform (Block's Nostr-based messenger, with native WebSocket transport and NIP-42 auth), the Vercel AI Gateway provider and Vercel Sandbox terminal backend return modernized, desktop gains an SSH remote-backend connection mode, and the Relay shipped four phases of parity — media, interactive prompts, thread lifecycle, typing indicators — plus HSP personal + org skill sync. ([#73610](https://github.com/NousResearch/hermes-agent/pull/73610), [#73761](https://github.com/NousResearch/hermes-agent/pull/73761), [#74518](https://github.com/NousResearch/hermes-agent/pull/74518), [#68130](https://github.com/NousResearch/hermes-agent/pull/68130), [#71300](https://github.com/NousResearch/hermes-agent/pull/71300)–[#71624](https://github.com/NousResearch/hermes-agent/pull/71624), [#66730](https://github.com/NousResearch/hermes-agent/pull/66730) — @teknium1, @yoniebans, @benbarclay)\\n\\n---\\n\\n## 🎙️ Voice & Speech\\n\\n### Conversational voice\\n- Streaming, conversational TTS with barge-in across all surfaces; clause-by-clause synthesis for CLI voice mode + gateway adapters ([#69511](https://github.com/NousResearch/hermes-agent/pull/69511), [#73862](https://github.com/NousResearch/hermes-agent/pull/73862) — @OutThisLife, @teknium1)\\n- Voice chat UX polish — busy-aware silence, stop hint, thinking sounds, barge-in fix; full-duplex turn listener (interrupt by voice during generation AND playback) ([#74000](https://github.com/NousResearch/hermes-agent/pull/74000), [#74223](https://github.com/NousResearch/hermes-agent/pull/74223) — @teknium1)\\n- On-device wake words with open-vocabulary phrases + multi-profile voice routing; say \\\"stop\\\" to end voice chat hands-free on every surface ([#70509](https://github.com/NousResearch/hermes-agent/pull/70509), [#73106](https://github.com/NousResearch/hermes-agent/pull/73106), [#73933](https://github.com/NousResearch/hermes-agent/pull/73933) — @teknium1)\\n- The model is told when the user interrupts its spoken reply; desktop speaks the whole turn and idle-flushes held narration ([#69602](https://github.com/NousResearch/hermes-agent/pull/69602), [#69936](https://github.com/NousResearch/hermes-agent/pull/69936) — @OutThisLife, @teknium1)\\n- 15-item CLI/TUI voice-mode UX and environment fix wave ([#73520](https://github.com/NousResearch/hermes-agent/pull/73520) — @teknium1)\\n\\n### TTS / STT infrastructure\\n- Unified spoken-text preprocessing + speed/instructions/provider tool params; unified STT language resolution (fixes the wrong-language transcription class); global `stt.language` defaults to `en` ([#73513](https://github.com/NousResearch/hermes-agent/pull/73513), [#73067](https://github.com/NousResearch/hermes-agent/pull/73067), [#73100](https://github.com/NousResearch/hermes-agent/pull/73100) — @teknium1)\\n- Fully configurable STT — `hermes tools` category, GUI toggle/matrix, dashboard dropdowns, setup status; OpenAI gpt-transcribe support ([#73910](https://github.com/NousResearch/hermes-agent/pull/73910), [#73853](https://github.com/NousResearch/hermes-agent/pull/73853) — @teknium1)\\n- Platform-aware auto-TTS voice delivery (opus platforms, streamed/global gap, captions); inbound voice classification/routing for Feishu, DingTalk, LINE, QQ, Photon, WhatsApp, Weixin ([#73508](https://github.com/NousResearch/hermes-agent/pull/73508), [#73515](https://github.com/NousResearch/hermes-agent/pull/73515) — @teknium1)\\n- Command TTS/STT provider hardening — idle timeouts, env scrubbing, no-shell, path guards ([#73514](https://github.com/NousResearch/hermes-agent/pull/73514) — @teknium1)\\n- Sync per-sentence TTS synthesis pipelined with playback — the next sentence renders while the current one speaks ([#77355](https://github.com/NousResearch/hermes-agent/pull/77355) — @kshitijk4poor)\\n- Discord voice PCM streams to ffmpeg stdin instead of a temp file ([#76970](https://github.com/NousResearch/hermes-agent/pull/76970) — @kshitijk4poor)\\n\\n## 🏗️ Core Agent & Architecture\\n\\n### Compression & context\\n- Proactive tool-result pruning for large-window models; per-turn micro-compaction; N-user tail guarantee (`compression.min_tail_user_messages`); bounded summarizer input with head+tail retention ([#70254](https://github.com/NousResearch/hermes-agent/pull/70254), [#75345](https://github.com/NousResearch/hermes-agent/pull/75345), [#70250](https://github.com/NousResearch/hermes-agent/pull/70250), [#70249](https://github.com/NousResearch/hermes-agent/pull/70249) — @teknium1, @kshitijk4poor)\\n- Ghost-skill defense — `[SKILL_PRUNED]` markers, protected prune, deterministic survival; progress-aware timeouts; lock-contended compression soft-defers instead of exhausting ([#70275](https://github.com/NousResearch/hermes-agent/pull/70275), [#71508](https://github.com/NousResearch/hermes-agent/pull/71508), [#70285](https://github.com/NousResearch/hermes-agent/pull/70285) — @teknium1)\\n- Per-model threshold overrides; absolute token threshold (`compression.threshold_tokens`); opt-in idle-triggered compaction; opt-in progress notices; structured local logging for compression attempts ([#69339](https://github.com/NousResearch/hermes-agent/pull/69339), [#69335](https://github.com/NousResearch/hermes-agent/pull/69335), [#69360](https://github.com/NousResearch/hermes-agent/pull/69360), [#70457](https://github.com/NousResearch/hermes-agent/pull/70457), [#69338](https://github.com/NousResearch/hermes-agent/pull/69338) — @teknium1)\\n- Context-engine ABC grows `select_context()` + `on_turn_complete()` verbs (salvage of @chaos-xxl's RFC work); engines can suppress or customize compaction status ([#70458](https://github.com/NousResearch/hermes-agent/pull/70458), [#69859](https://github.com/NousResearch/hermes-agent/pull/69859) — @teknium1)\\n- Strict redaction applied at every compaction text boundary ([#69294](https://github.com/NousResearch/hermes-agent/pull/69294) — @teknium1)\\n\\n### Prompt caching & hot-path performance\\n- Tool schemas cached on native Anthropic without history loss + consolidated cache-plan internals ([#76032](https://github.com/NousResearch/hermes-agent/pull/76032), [#76067](https://github.com/NousResearch/hermes-agent/pull/76067) — @kshitijk4poor)\\n- DeepSeek prompt caching on OpenCode gateways; per-API-call token accounting off the turn thread; OpenAI wire client reused across sequential LLM calls; send-path tool-call canonicalization memoized ([#75886](https://github.com/NousResearch/hermes-agent/pull/75886), [#73359](https://github.com/NousResearch/hermes-agent/pull/73359), [#73375](https://github.com/NousResearch/hermes-agent/pull/73375), [#76880](https://github.com/NousResearch/hermes-agent/pull/76880) — @teknium1, @kshitijk4poor)\\n- Readonly config loader at 29 call sites (28× cheaper reads); per-turn config deepcopies killed (telemetry gate 54×); one raw config.yaml parse per process; inter-tool delay removed ([#74322](https://github.com/NousResearch/hermes-agent/pull/74322), [#74211](https://github.com/NousResearch/hermes-agent/pull/74211), [#74228](https://github.com/NousResearch/hermes-agent/pull/74228), [#64172](https://github.com/NousResearch/hermes-agent/pull/64172) — @teknium1, @Soju06)\\n- Lazy heavy-SDK imports (−8-10% import cost on top of the mcp/tool-discovery diet); streaming hot loop drops per-chunk repr() (~3× cheaper accounting); cursor/memo optimizations for per-iteration history walks ([#74204](https://github.com/NousResearch/hermes-agent/pull/74204), [#74194](https://github.com/NousResearch/hermes-agent/pull/74194), [#74221](https://github.com/NousResearch/hermes-agent/pull/74221), [#74231](https://github.com/NousResearch/hermes-agent/pull/74231) — @teknium1)\\n- Cold-start ~14s GIL stall during backend init mitigated; turn flush batched into one SQLite transaction; provider-capability-gated prompt cache keys (implied for api.openai.com) ([#77814](https://github.com/NousResearch/hermes-agent/pull/77814), [#77619](https://github.com/NousResearch/hermes-agent/pull/77619), [#77609](https://github.com/NousResearch/hermes-agent/pull/77609) — @kshitijk4poor)\\n- AIAgent hot-path salvage — prompt-cache copy, reasoning-timeout precompute, lazy compressor init ([#57229](https://github.com/NousResearch/hermes-agent/pull/57229) — @kshitijk4poor)\\n\\n### Approvals & the agent loop\\n- `hermes approvals suggest` mines approval history into allowlist proposals; operator-customizable `approvals.smart_policy`; consecutive-denial circuit breaker; cross-surface approvals mode command ([#72259](https://github.com/NousResearch/hermes-agent/pull/72259), [#72186](https://github.com/NousResearch/hermes-agent/pull/72186), [#72203](https://github.com/NousResearch/hermes-agent/pull/72203), [#63517](https://github.com/NousResearch/hermes-agent/pull/63517) — @teknium1)\\n- Docker/podman daemon-redirect commands require approval; session-wide runaway-loop caps for web_search + delegate_task (Claude Code-inspired) ([#71092](https://github.com/NousResearch/hermes-agent/pull/71092), [#66600](https://github.com/NousResearch/hermes-agent/pull/66600) — @teknium1)\\n- Mid-turn redirects — user corrections steer the active turn, preserving in-flight work and the original prompt ([#63104](https://github.com/NousResearch/hermes-agent/pull/63104), [#72339](https://github.com/NousResearch/hermes-agent/pull/72339) — @OutThisLife)\\n- Delegation: structured timeout/stall metadata + live per-child status in `/agents`; subagents can use `execute_code`; redacted child tool history exposed in `subagent_stop`; public subagent lifecycle API for plugins ([#72300](https://github.com/NousResearch/hermes-agent/pull/72300), [#69325](https://github.com/NousResearch/hermes-agent/pull/69325), [#72403](https://github.com/NousResearch/hermes-agent/pull/72403), [#72501](https://github.com/NousResearch/hermes-agent/pull/72501) — @teknium1)\\n- Single-owner refactors for backend identity + failure-scoped skips, empty-content wire repair, call_id/reasoning sanitization, model-switch parsing ([#72505](https://github.com/NousResearch/hermes-agent/pull/72505), [#73071](https://github.com/NousResearch/hermes-agent/pull/73071), [#74319](https://github.com/NousResearch/hermes-agent/pull/74319), [#74229](https://github.com/NousResearch/hermes-agent/pull/74229) — @teknium1)\\n- Labeled reasoning excerpt surfaced at the empty-response terminal; tool_search probe-validates blind tool_call args ([#65144](https://github.com/NousResearch/hermes-agent/pull/65144), [#59267](https://github.com/NousResearch/hermes-agent/pull/59267) — @teknium1)\\n\\n### Tool self-recovery wave\\n- Terminal: recoverable truncation (full output spilled + pre-truncation size), cwd echoed when a command changes directory, output-pattern failure hints ([#77041](https://github.com/NousResearch/hermes-agent/pull/77041), [#77004](https://github.com/NousResearch/hermes-agent/pull/77004), [#76992](https://github.com/NousResearch/hermes-agent/pull/76992) — @teknium1)\\n- Patch: already-applied edits return success no-op, whitespace-visualized no-match diagnosis, ambiguous-match locations listed ([#76998](https://github.com/NousResearch/hermes-agent/pull/76998), [#77024](https://github.com/NousResearch/hermes-agent/pull/77024), [#77001](https://github.com/NousResearch/hermes-agent/pull/77001) — @teknium1)\\n- Search: zero-match probes + multi-path recovery, auto-multiline for newline patterns; read_file default limit 500 → 2000 lines; negative-result cache for read/search misses; write_file verifies on-disk content ([#77011](https://github.com/NousResearch/hermes-agent/pull/77011), [#77102](https://github.com/NousResearch/hermes-agent/pull/77102), [#76996](https://github.com/NousResearch/hermes-agent/pull/76996), [#76945](https://github.com/NousResearch/hermes-agent/pull/76945), [#77055](https://github.com/NousResearch/hermes-agent/pull/77055) — @teknium1, @kshitijk4poor)\\n- execute_code recovery hints; skill_view dedup stub for unchanged re-reads; terminal/execute_code schema prose trimmed ~40%; tiered tool disclosure scales with catalog size; default iteration limit 90 → 500 ([#77106](https://github.com/NousResearch/hermes-agent/pull/77106), [#77095](https://github.com/NousResearch/hermes-agent/pull/77095), [#77023](https://github.com/NousResearch/hermes-agent/pull/77023), [#67034](https://github.com/NousResearch/hermes-agent/pull/67034), [#72176](https://github.com/NousResearch/hermes-agent/pull/72176) — @teknium1)\\n\\n### Providers & models\\n- Vercel AI Gateway provider + Vercel Sandbox terminal backend return, modernized (SDK 0.7.2, telemetry off) ([#74518](https://github.com/NousResearch/hermes-agent/pull/74518) — @teknium1)\\n- Gemini 3.1 Pro + 3.6 Flash in catalogs; Gemini salvage cluster (3.6-flash aux default, Vertex catalog, direct cost tracking); claude-opus-5 in OpenRouter + Nous Portal; deepseek-v4-flash-0731 ([#73479](https://github.com/NousResearch/hermes-agent/pull/73479), [#73516](https://github.com/NousResearch/hermes-agent/pull/73516), [#70946](https://github.com/NousResearch/hermes-agent/pull/70946), [#75501](https://github.com/NousResearch/hermes-agent/pull/75501) — @teknium1)\\n- Bedrock Converse API prompt caching (cachePoint) ([#70231](https://github.com/NousResearch/hermes-agent/pull/70231) — @JoaoMarcos44)\\n- OpenAI data-residency endpoints get declared transport + correct catalog; provider-aware API-server request routing; backend-acknowledged session model lock; Nous sticky routing via top-level session_id ([#74958](https://github.com/NousResearch/hermes-agent/pull/74958), [#70853](https://github.com/NousResearch/hermes-agent/pull/70853), [#70950](https://github.com/NousResearch/hermes-agent/pull/70950), [#69253](https://github.com/NousResearch/hermes-agent/pull/69253) — @victor-kyriazakos, @teknium1)\\n- Model picker: curated defaults + collapsible providers + select-all; stale caches served instantly with background refresh; custom-endpoint probe capped at 1.5s; honcho OAuth device-code login ([#73172](https://github.com/NousResearch/hermes-agent/pull/73172), [#76430](https://github.com/NousResearch/hermes-agent/pull/76430), [#76922](https://github.com/NousResearch/hermes-agent/pull/76922), [#61608](https://github.com/NousResearch/hermes-agent/pull/61608) — @OutThisLife, @teknium1, @kshitijk4poor, @akattelu)\\n- ACP: named custom providers in the model selector; authenticated cross-provider model choices; non-blocking startup via background MCP discovery ([#70082](https://github.com/NousResearch/hermes-agent/pull/70082), [#70404](https://github.com/NousResearch/hermes-agent/pull/70404), [#75985](https://github.com/NousResearch/hermes-agent/pull/75985) — @israellot, @amanning3390, @kshitijk4poor)\\n\\n### Secrets & config\\n- Command-helper secret source (composes with all vaults); one-command token rotation + actionable startup errors; opt-in encrypted break-glass cache for Bitwarden; vault-injected keys scoped per profile home; orchestrator preserve_existing + profile aliasing ([#69266](https://github.com/NousResearch/hermes-agent/pull/69266), [#68605](https://github.com/NousResearch/hermes-agent/pull/68605), [#69251](https://github.com/NousResearch/hermes-agent/pull/69251), [#69250](https://github.com/NousResearch/hermes-agent/pull/69250), [#69058](https://github.com/NousResearch/hermes-agent/pull/69058) — @teknium1)\\n- `${env:VAR}` SecretRef parity between config.yaml and MCP config; secret-source env vars reach stdio MCP servers ([#69267](https://github.com/NousResearch/hermes-agent/pull/69267), [#69053](https://github.com/NousResearch/hermes-agent/pull/69053) — @teknium1)\\n- Canonical config loaders for behavioral reads; table-driven config migration registry; DEFAULT_CONFIG extracted to config_defaults.py; auto-migration support floor at v12 ([#74237](https://github.com/NousResearch/hermes-agent/pull/74237), [#74200](https://github.com/NousResearch/hermes-agent/pull/74200), [#74182](https://github.com/NousResearch/hermes-agent/pull/74182), [#74433](https://github.com/NousResearch/hermes-agent/pull/74433) — @teknium1)\\n\\n## 🌐 Gateway, Relay & Fleet\\n- Session activity heartbeats, stall watchdog, and bounded compression waits — re-landed hardened after an in-window revert cycle (originally #72424 by @fangliquanflq) ([#76354](https://github.com/NousResearch/hermes-agent/pull/76354) — @teknium1)\\n- SessionState consolidation (19 session-keyed dicts → one turn/conversation/persistent-scoped object); TurnContext/TurnRunner seam extraction; declarative busy_policy on CommandDef ([#74289](https://github.com/NousResearch/hermes-agent/pull/74289), [#74353](https://github.com/NousResearch/hermes-agent/pull/74353), [#74197](https://github.com/NousResearch/hermes-agent/pull/74197) — @teknium1)\\n- Relay parity waves: Phase 1 (supported_ops discovery, identity fields, /handoff aliasing), Phase 2 media, Phase 3 interactive prompts, Phase 4 thread lifecycle; egress typing indicators ([#71300](https://github.com/NousResearch/hermes-agent/pull/71300), [#71363](https://github.com/NousResearch/hermes-agent/pull/71363), [#71404](https://github.com/NousResearch/hermes-agent/pull/71404), [#71624](https://github.com/NousResearch/hermes-agent/pull/71624), [#69721](https://github.com/NousResearch/hermes-agent/pull/69721) — @benbarclay)\\n- HSP skill sync: personal client (M1) + org-skills client (M2) + org-skill namespace with token-gated discovery ([#66730](https://github.com/NousResearch/hermes-agent/pull/66730), [#70024](https://github.com/NousResearch/hermes-agent/pull/70024), [#70459](https://github.com/NousResearch/hermes-agent/pull/70459) — @benbarclay)\\n- Buzz (Block/Nostr) platform adapter with native WebSocket inbound transport + NIP-42 auth ([#73610](https://github.com/NousResearch/hermes-agent/pull/73610), [#73761](https://github.com/NousResearch/hermes-agent/pull/73761) — @teknium1)\\n- Photon: native polls, effects, clarify-as-poll, rich links (4-PR salvage) ([#73614](https://github.com/NousResearch/hermes-agent/pull/73614) — @teknium1)\\n- Slack: native Block Kit clarify buttons; opt-in reaction triggers; outbound payload sanitization; thread-context lifecycle fixes ([#69318](https://github.com/NousResearch/hermes-agent/pull/69318), [#70195](https://github.com/NousResearch/hermes-agent/pull/70195), [#69317](https://github.com/NousResearch/hermes-agent/pull/69317), [#69320](https://github.com/NousResearch/hermes-agent/pull/69320) — @teknium1)\\n- Discord auto-thread sessions keyed on prospective_thread_id; reply references built from ids (no fetch_message); WhatsApp configurable inbound read receipts ([#76513](https://github.com/NousResearch/hermes-agent/pull/76513), [#76875](https://github.com/NousResearch/hermes-agent/pull/76875), [#73322](https://github.com/NousResearch/hermes-agent/pull/73322) — @benbarclay, @kshitijk4poor)\\n- Kanban wakes resume the creator's DM/thread session; kanban/delegate wake-ups reach api_server sessions; per-task model + thinking-depth from the board ([#72191](https://github.com/NousResearch/hermes-agent/pull/72191), [#70171](https://github.com/NousResearch/hermes-agent/pull/70171), [#69876](https://github.com/NousResearch/hermes-agent/pull/69876), [#76417](https://github.com/NousResearch/hermes-agent/pull/76417) — @teknium1, @OutThisLife)\\n- Relay: Discord tool-progress routed into the auto-thread instead of the parent channel ([#77830](https://github.com/NousResearch/hermes-agent/pull/77830) — @benbarclay)\\n- Outbound webhooks — push signed lifecycle events to external endpoints; simplex channel enumeration in `hermes send --list` ([#69406](https://github.com/NousResearch/hermes-agent/pull/69406), [#77110](https://github.com/NousResearch/hermes-agent/pull/77110) — @teknium1)\\n\\n## 🖥️ Hermes Desktop App\\n\\n### The platform wave\\n- **Artifacts** — versioned cards, sandboxed live preview, right-rail viewer ([#72345](https://github.com/NousResearch/hermes-agent/pull/72345) — @teknium1)\\n- **Plugin SDK** — Kanban as the founding desktop plugin; `ctx.download` hands the user a file; widget-app SDK (apps as state+reducer+render) with three reference apps; widget-grid layout engine + background-aware theme engine ([#61173](https://github.com/NousResearch/hermes-agent/pull/61173), [#74413](https://github.com/NousResearch/hermes-agent/pull/74413), [#68306](https://github.com/NousResearch/hermes-agent/pull/68306), [#20379](https://github.com/NousResearch/hermes-agent/pull/20379) — @OutThisLife)\\n- Quick-entry window (global hotkey → any session); multiple GUI windows; floating pane placement; pane toggles anywhere + hidden header; ⌘O open-folder-as-project ([#72315](https://github.com/NousResearch/hermes-agent/pull/72315), [#68259](https://github.com/NousResearch/hermes-agent/pull/68259), [#73143](https://github.com/NousResearch/hermes-agent/pull/73143), [#75848](https://github.com/NousResearch/hermes-agent/pull/75848), [#74623](https://github.com/NousResearch/hermes-agent/pull/74623) — @teknium1, @OutThisLife)\\n- SSH remote-backend connection mode; event-driven live sync replaces always-on polls; remote profile routing/sessions/pool lifecycle repaired ([#68130](https://github.com/NousResearch/hermes-agent/pull/68130), [#73673](https://github.com/NousResearch/hermes-agent/pull/73673), [#72835](https://github.com/NousResearch/hermes-agent/pull/72835) — @yoniebans, @OutThisLife)\\n- Let the agent drive the shell (preview pane + pane focus) AND inspect the desktop app it's developing; find-in-page (Ctrl+F); GUI terminal copy/paste + font picker ([#69519](https://github.com/NousResearch/hermes-agent/pull/69519), [#73121](https://github.com/NousResearch/hermes-agent/pull/73121), [#72235](https://github.com/NousResearch/hermes-agent/pull/72235), [#73705](https://github.com/NousResearch/hermes-agent/pull/73705), [#76395](https://github.com/NousResearch/hermes-agent/pull/76395) — @OutThisLife, @teknium1)\\n\\n### Composer & UX\\n- Attach files/folders/links via picker; composer chips for @ paths and pasted links; composer undo stack; double-ESC discards draft; double-Enter sends the queued turn; type-to-focus ([#74668](https://github.com/NousResearch/hermes-agent/pull/74668), [#73110](https://github.com/NousResearch/hermes-agent/pull/73110), [#72201](https://github.com/NousResearch/hermes-agent/pull/72201), [#72288](https://github.com/NousResearch/hermes-agent/pull/72288), [#74736](https://github.com/NousResearch/hermes-agent/pull/74736), [#73101](https://github.com/NousResearch/hermes-agent/pull/73101), [#68918](https://github.com/NousResearch/hermes-agent/pull/68918) — @OutThisLife)\\n- 2-keypress model switching (⌘⇧M); YOLO in ⌘K with live toggle state; keyboard-first pickers; keyboard navigation for clarify choices; server-owned pins that follow you between apps ([#74545](https://github.com/NousResearch/hermes-agent/pull/74545), [#74674](https://github.com/NousResearch/hermes-agent/pull/74674), [#74602](https://github.com/NousResearch/hermes-agent/pull/74602), [#69799](https://github.com/NousResearch/hermes-agent/pull/69799), [#74234](https://github.com/NousResearch/hermes-agent/pull/74234) — @OutThisLife)\\n- Grouped, live-ticking tool-activity line; improved tool call detail views; @session links resolve to clickable titles; brand icons on known-domain links; iMessage-style emoji reactions (opt-in, two-way); double-click to heart ([#72893](https://github.com/NousResearch/hermes-agent/pull/72893), [#69868](https://github.com/NousResearch/hermes-agent/pull/69868), [#71162](https://github.com/NousResearch/hermes-agent/pull/71162), [#73047](https://github.com/NousResearch/hermes-agent/pull/73047), [#74533](https://github.com/NousResearch/hermes-agent/pull/74533), [#74644](https://github.com/NousResearch/hermes-agent/pull/74644) — @OutThisLife, @teknium1)\\n- Sidebar date dividers + pinned section + opt-in stale-session auto-archive; sessions stop lying about running state; credit-usage toasts; configurable attachment size limit; Cron Blueprints + Webhooks pages; searchable timezone picker ([#70822](https://github.com/NousResearch/hermes-agent/pull/70822), [#72303](https://github.com/NousResearch/hermes-agent/pull/72303), [#69828](https://github.com/NousResearch/hermes-agent/pull/69828), [#73221](https://github.com/NousResearch/hermes-agent/pull/73221), [#70066](https://github.com/NousResearch/hermes-agent/pull/70066), [#69687](https://github.com/NousResearch/hermes-agent/pull/69687), [#73505](https://github.com/NousResearch/hermes-agent/pull/73505) — @OutThisLife, @austinpickett, @Adolanium, @teknium1)\\n- RFC 8252 native desktop sign-in (system browser + PKCE, no webview cookies); \\\"Connect to existing Hermes\\\" in first-run onboarding; profile-correct pairing approvals with a desktop surface ([#67920](https://github.com/NousResearch/hermes-agent/pull/67920), [#70907](https://github.com/NousResearch/hermes-agent/pull/70907), [#74446](https://github.com/NousResearch/hermes-agent/pull/74446) — @benbarclay, @OutThisLife)\\n- Keep-computer-awake toggle + notch wake indicator; /battery status-bar toggle; UI zoom 90% default preset; status bar hideable ([#68140](https://github.com/NousResearch/hermes-agent/pull/68140), [#76396](https://github.com/NousResearch/hermes-agent/pull/76396), [#68860](https://github.com/NousResearch/hermes-agent/pull/68860), [#73161](https://github.com/NousResearch/hermes-agent/pull/73161), [#72960](https://github.com/NousResearch/hermes-agent/pull/72960) — @OutThisLife, @teknium1)\\n\\n### Desktop performance (60fps wave 2)\\n- Streaming cost independent of transcript length; 60fps on real sessions (reflow-gated pins, adaptive flush); drag at 60fps with five streaming tabs; multitab streaming made fast ([#71835](https://github.com/NousResearch/hermes-agent/pull/71835), [#72504](https://github.com/NousResearch/hermes-agent/pull/72504), [#72346](https://github.com/NousResearch/hermes-agent/pull/72346), [#71780](https://github.com/NousResearch/hermes-agent/pull/71780) — @OutThisLife)\\n- Hidden-pane timers paused (agents view, cron sidebar, floating pet), scroll/status loops stopped in busy sessions ([#77651](https://github.com/NousResearch/hermes-agent/pull/77651) — @kshitijk4poor); idle CPU near zero in the background; sidebar/overlay render churn killed; statusbar + transcript stop re-rendering per token/sash-drag/session-switch; ⌘K opens instantly; renderer cold start keeps shiki/mermaid off the boot path ([#75218](https://github.com/NousResearch/hermes-agent/pull/75218), [#73698](https://github.com/NousResearch/hermes-agent/pull/73698), [#72163](https://github.com/NousResearch/hermes-agent/pull/72163), [#72245](https://github.com/NousResearch/hermes-agent/pull/72245), [#72524](https://github.com/NousResearch/hermes-agent/pull/72524), [#74665](https://github.com/NousResearch/hermes-agent/pull/74665), [#73024](https://github.com/NousResearch/hermes-agent/pull/73024) — @OutThisLife)\\n- State diagnostics (render + store churn counters) + a lint rule banning atom-mirrored refs so the stale-read bug class cannot return; Playwright E2E suite with visual regression diffs ([#71925](https://github.com/NousResearch/hermes-agent/pull/71925), [#71560](https://github.com/NousResearch/hermes-agent/pull/71560), [#65805](https://github.com/NousResearch/hermes-agent/pull/65805) — @OutThisLife, @teknium1, @ethernet8023)\\n\\n## 🖥️ CLI, TUI & Dashboard\\n- `!` shell mode; `/init` AGENTS.md generation; `/diff` (staged/all/session, cross-surface); `/context` breakdown; `/focus` reduced-output view; Ctrl+S prompt stash; persistent `/goal` indicator; multi-select clarify (checkboxes) across CLI/gateway/TUI ([#72257](https://github.com/NousResearch/hermes-agent/pull/72257), [#72178](https://github.com/NousResearch/hermes-agent/pull/72178), [#72240](https://github.com/NousResearch/hermes-agent/pull/72240), [#72242](https://github.com/NousResearch/hermes-agent/pull/72242), [#72302](https://github.com/NousResearch/hermes-agent/pull/72302), [#72262](https://github.com/NousResearch/hermes-agent/pull/72262), [#72244](https://github.com/NousResearch/hermes-agent/pull/72244), [#72188](https://github.com/NousResearch/hermes-agent/pull/72188) — @teknium1, salvaging @SHL0MS, @iRonin, @gigi206 + more)\\n- `hermes import-agent` — one-command migration from Claude Code / Codex CLI setups ([#72190](https://github.com/NousResearch/hermes-agent/pull/72190) — @teknium1)\\n- Per-turn summary line + live token flow in the spinner; cross-surface theme SDK (one skin themes CLI, TUI, and desktop, live) ([#72246](https://github.com/NousResearch/hermes-agent/pull/72246), [#68857](https://github.com/NousResearch/hermes-agent/pull/68857) — @teknium1, @OutThisLife)\\n- TUI: reach the model picker without wrecking your draft + mid-turn switching; slash menu leads with your most-used skills; attachments live in the composer; Arabic (ar) locale with RTL across desktop/dashboard/agent ([#74756](https://github.com/NousResearch/hermes-agent/pull/74756), [#75931](https://github.com/NousResearch/hermes-agent/pull/75931), [#75210](https://github.com/NousResearch/hermes-agent/pull/75210), [#70870](https://github.com/NousResearch/hermes-agent/pull/70870) — @OutThisLife)\\n- `hermes -w` startup ~14s → ~1.8s; global `--version` fast path; banner update-check 6× faster; dashboard lazy-loads routes + GROUP BY session stats; session filtering tabs (Chats/Automation/All) ([#71637](https://github.com/NousResearch/hermes-agent/pull/71637), [#62096](https://github.com/NousResearch/hermes-agent/pull/62096), [#74188](https://github.com/NousResearch/hermes-agent/pull/74188), [#72294](https://github.com/NousResearch/hermes-agent/pull/72294), [#73362](https://github.com/NousResearch/hermes-agent/pull/73362), [#73865](https://github.com/NousResearch/hermes-agent/pull/73865) — @teknium1, @kshitijk4poor)\\n- Runtime: Node 26 required across installers/heal/upgrade, managed Node/uv resolve before bare PATH, outdated managed trees heal to target major; brew + pip/PyPI wheel channels retired (shell installer / Docker / Nix are the supported channels) ([#76459](https://github.com/NousResearch/hermes-agent/pull/76459), [#68217](https://github.com/NousResearch/hermes-agent/pull/68217) — @ethernet8023)\\n\\n## 🧩 Skills, Plugins & MCP\\n- **A2A v1.0** — Agent-to-Agent protocol plugin (closes #514) ([#77109](https://github.com/NousResearch/hermes-agent/pull/77109) — @teknium1)\\n- Curator: surface unmanaged skills + `curator adopt`; skill-description truncation surfaced to authors; grounded-citations skill (+ fact-checking mode); simplify-code v1.1; tldraw-offline scripting skill ([#71648](https://github.com/NousResearch/hermes-agent/pull/71648), [#70519](https://github.com/NousResearch/hermes-agent/pull/70519), [#71698](https://github.com/NousResearch/hermes-agent/pull/71698), [#77104](https://github.com/NousResearch/hermes-agent/pull/77104), [#70440](https://github.com/NousResearch/hermes-agent/pull/70440), [#66896](https://github.com/NousResearch/hermes-agent/pull/66896) — @teknium1)\\n- Office skills bundled: docx, xlsx, pdf + refreshed powerpoint; skills-tree debloat continues (yuanbao, segment-anything, jupyter, heartmula, audiocraft → optional-skills; claude-marketplace source removed; hub restructure absorbing themes/desktop-plugins/tui-widgets) ([#68595](https://github.com/NousResearch/hermes-agent/pull/68595), [#70452](https://github.com/NousResearch/hermes-agent/pull/70452)–[#70456](https://github.com/NousResearch/hermes-agent/pull/70456), [#73903](https://github.com/NousResearch/hermes-agent/pull/73903) — @teknium1)\\n- MCP: Comfy Cloud catalog entry with curated 20-tool default; hidden-whitespace warnings in MCP config; pinecone-research optional skill ([#66112](https://github.com/NousResearch/hermes-agent/pull/66112), [#75736](https://github.com/NousResearch/hermes-agent/pull/75736), [#70512](https://github.com/NousResearch/hermes-agent/pull/70512) — @teknium1)\\n- MCP lazy server startup from a fingerprint-keyed on-disk tool-schema cache — configured servers no longer all boot at session start (design from #56832) ([#77511](https://github.com/NousResearch/hermes-agent/pull/77511) — @kshitijk4poor)\\n- NeMo Relay observability integration — re-landed after an in-window revert, on stable NeMo Relay 0.6 ([#67607](https://github.com/NousResearch/hermes-agent/pull/67607) — @afourniernv)\\n- Gateway health & diagnostics OTLP export ([#64536](https://github.com/NousResearch/hermes-agent/pull/64536) — @victor-kyriazakos)\\n\\n## 🔒 Security & Reliability\\n- Iron-proxy credential-injection egress firewall re-landed ([#70848](https://github.com/NousResearch/hermes-agent/pull/70848) — @teknium1)\\n- DNS-pinned SSRF-safe fetches + Slack CDN allowlist; strict redaction at compaction boundaries; ReDoS eliminated in config-key redaction patterns; prose words embedding a secret keyword no longer masked ([#70193](https://github.com/NousResearch/hermes-agent/pull/70193), [#69294](https://github.com/NousResearch/hermes-agent/pull/69294), [#76083](https://github.com/NousResearch/hermes-agent/pull/76083), [#67776](https://github.com/NousResearch/hermes-agent/pull/67776) — @teknium1)\\n- Tier-3 credential reads scoped (FAL/XAI/VERCEL/DAYTONA/GITHUB presence checks etc.); CVE dependency pins refreshed (cryptography, starlette, python-multipart); hindsight env file 0600; /model moved off the gateway event loop ([#75888](https://github.com/NousResearch/hermes-agent/pull/75888), [#72362](https://github.com/NousResearch/hermes-agent/pull/72362) — @teknium1)\\n- Windows hardening wave: text-mode subprocess decode bug class closed repo-wide, console flashes hidden across daemons/env probes/LSP/installer paths, residual encoding gaps (MCP stdio, gateway update I/O, STT/TTS, desktop spawn) ([#70875](https://github.com/NousResearch/hermes-agent/pull/70875), [#70205](https://github.com/NousResearch/hermes-agent/pull/70205), [#70264](https://github.com/NousResearch/hermes-agent/pull/70264), [#71014](https://github.com/NousResearch/hermes-agent/pull/71014) — @teknium1, salvaging several community PRs)\\n- State/session integrity: four session-state fixes (safe close tracking, flush-cursor class fix, row-retry, usage-PK healer); compact v23 FTS layout + `hermes sessions optimize` + CJK-bigram FTS; read-path split with per-thread read-only connections ([#75883](https://github.com/NousResearch/hermes-agent/pull/75883), [#65798](https://github.com/NousResearch/hermes-agent/pull/65798), [#69423](https://github.com/NousResearch/hermes-agent/pull/69423), [#73344](https://github.com/NousResearch/hermes-agent/pull/73344) — @teknium1, @kshitijk4poor)\\n- OpenViking memory-provider hardening — fail closed on blocked endpoints, server verification before credentials are sent, config.yaml-first settings ([#77747](https://github.com/NousResearch/hermes-agent/pull/77747) — @kshitijk4poor)\\n- Credential pool: reset-aware primary restore (stay on fallback until the rate-limit window resets) + deferred-refresh locking fixes; FTS UPDATE triggers narrowed with fail-closed CJK migration ([#77631](https://github.com/NousResearch/hermes-agent/pull/77631), [#77628](https://github.com/NousResearch/hermes-agent/pull/77628) — @kshitijk4poor)\\n- Config-driven memory allocator trim with telemetry; holographic memory vectors stored float32; loop-invariant HRR encodes hoisted ([#76905](https://github.com/NousResearch/hermes-agent/pull/76905), [#76917](https://github.com/NousResearch/hermes-agent/pull/76917), [#76881](https://github.com/NousResearch/hermes-agent/pull/76881) — @kshitijk4poor)\\n\\n## 🐛 Notable Bug Fixes\\n- Voice: full-duplex interruption during generation AND playback; whole-turn desktop speech; auto-TTS delivery gaps ([#74223](https://github.com/NousResearch/hermes-agent/pull/74223), [#69936](https://github.com/NousResearch/hermes-agent/pull/69936), [#73508](https://github.com/NousResearch/hermes-agent/pull/73508) — @teknium1)\\n- Desktop: Stop parks the queue instead of firing the next queued prompt; branch-in-new-chat restart loss; false remote-gateway reauthentication; cross-session composer leaks ([#68725](https://github.com/NousResearch/hermes-agent/pull/68725), [#71960](https://github.com/NousResearch/hermes-agent/pull/71960), [#68250](https://github.com/NousResearch/hermes-agent/pull/68250), [#70986](https://github.com/NousResearch/hermes-agent/pull/70986) — @SHL0MS, @alelpoan, @helix4u, @OutThisLife)\\n- Gateway: session lists scoped before limiting; relay-backed home delivery after restart; timeline display events persisted ([#65509](https://github.com/NousResearch/hermes-agent/pull/65509), [#70102](https://github.com/NousResearch/hermes-agent/pull/70102), [#69771](https://github.com/NousResearch/hermes-agent/pull/69771) — @GodsBoy, @victor-kyriazakos, @ethernet8023)\\n- Agent: context-length fallback logging + batch trajectory durability; Codex OAuth context windows revalidated against the live catalog ([#76027](https://github.com/NousResearch/hermes-agent/pull/76027), [#68554](https://github.com/NousResearch/hermes-agent/pull/68554) — @kshitijk4poor, @teknium1)\\n- ...plus roughly 770 more `fix:` PRs across every subsystem this window.\\n\\n## 👥 Contributors\\n\\n**647 contributors** shipped this release (commit authors, co-authors, and salvaged-PR credits).\\n\\n### Core\\n@teknium1, @OutThisLife (desktop, voice, perf), @kshitijk4poor (perf, caching, salvage), @ethernet8023 (runtime, E2E, desktop), @benbarclay (relay, HSP, auth)\\n\\n### All Contributors (alphabetical)\\n@02356abc, @0301chris, @0xAlcibiades, @0xDevNinja, @0xLeathery, @0xprincess, @0xr00tf3rr3t, @100yenadmin,\\n@2001Y, @3ssiri, @55nx954gn6-debug, @686f6c61, @87degrees, @aaronlab, @abundantbeing, @Adolanium,\\n@adriansotomora, @adurham, @afourniernv, @afurm, @AgenticSpark, @ahmadashfq, @AhmetArif0, @ai-ag2026,\\n@AIalliAI, @aider4ryder, @airclear, @ajzrva-sys, @akattelu, @AKAZIK-py, @akb4q, @akshan-main, @AlanBurningsuit,\\n@alelpoan, @AlexFucuson9, @AlexxRussell, @AllardQuek, @alt-glitch, @aman-merchant, @amanning3390, @amathxbt,\\n@aml1973, @amoreno16003, @AndrewMoryakov, @andrexibiza, @andynguyendk, @andyylin, @aneym, @angelos,\\n@aniruddhaadak80, @AnnasMazhar, @annguyenNous, @anoopmehendale-cue, @AnthonyFrancis, @arcabotai, @ArcherQAQ,\\n@Ares4Tech, @arimu1, @arnoldfrancisca, @asimons81, @asorry75, @AtakanGs, @ATran28, @austinpickett,\\n@Automata-intelligentsia, @awain7, @aweiker, @aydnOktay, @ayushere, @b, @baau, @baauzi, @baenregod,\\n@bakhtiersizhaev, @Baophan00, @baoyu0, @Bartok9, @basilalshukaili, @BB-light, @bbopen, @Beandon13,\\n@beardedeagle, @bedirhancode, @benbarclay, @benegessarit, @benjamin2026-dot, @bennybuoy, @BenSheridanEdwards,\\n@BKStock, @BlackishGreen33, @bnikanjam, @bounce12340, @Bounty13, @bpross, @briandevans, @bricelb, @brunopirz,\\n@bryanneva, @byshubham, @camaleonidas, @canorionen, @carbongotfound, @carljborg, @carlotestor, @carrion256,\\n@caseyanthony, @cat-thats-fat, @Cdddo, @ceverson70, @chancelu, @chaos-xxl, @CharlesMcquade, @chazmaniandinkle,\\n@chefboyrdave21, @chelsealong, @Christopher-Schulze, @chuenchen309, @ciabata-git, @cifangyiquan, @cipry0200,\\n@ckaznocha, @ckorhonen, @CleanDev-Fix, @CocaKova, @coffee-the-dev, @colingreig, @Commander, @connorblack,\\n@CoreyNoDream, @Cossackx, @crayfish-ai, @criptogus, @ctaylor86, @cucurigoo, @cypres0099, @d31tcjg, @Da7-Tech,\\n@damiankluk, @danielblankhh, @DanielMaly, @Dannou, @davesecops, @davidgut1982, @DavidMetcalfe, @davidrobertson,\\n@ddifa86, @ddy4633, @deacon-botdoctor, @deaneeth, @deepjia, @deltaahead, @DESXIE, @devsart95, @dhruvkej9,\\n@dhruvraajeev, @DI404N, @diegomarino, @diffen77, @dirtyren, @dnth, @DocAwk, @dolphin-creator, @Dolverin,\\n@dombejar, @doncazper, @dongjiang1989, @DonutsDelivery, @dorukardahan, @Doud-FR, @drafish, @Drexuxux,\\n@drleadflow, @dsitmilis, @dskwe, @dso2ng, @dstkwll, @Dusk1e, @dyreckt, @eagle-nyp, @Eapwrk, @eason2026,\\n@eazye19, @egilewski, @ehz0ah, @elcocoel, @eliemada, @eloklam, @elphamale, @embwl0x, @emozilla, @Enough1122,\\n@enzo2, @erick713006, @ErnestHysa, @Esther-Zhu023, @ethernet8023, @evgyur, @f-trycua, @faikwo, @falkoro,\\n@fangliquanflq, @fazerluga-creator, @fcavalcantirj, @ferminquant, @fesalfayed, @FixItFoundry, @flag0x369,\\n@floatingrain, @FlorianVal, @flyingdoubleG, @FraserHum, @frizikk, @frohsinnllc, @Frowtek, @FvanW, @fyzanshaik,\\n@ganzziani, @gercamjr, @giggling-ginger, @gigi206, @giladbau, @glesperance, @gnanam1990, @GodsBoy, @golldyck,\\n@gonzalofrancoceballos, @gshall, @gumclaw, @Guoen0, @Gurud25, @gvago, @HaisamAbbas, @hansai-art, @hanyu1212,\\n@happy5318, @haran2001, @hariNEzuMI928, @harjothkhara, @harrisonmedmedmetrics, @Harshkamdar67, @hdd69,\\n@heathley, @helix4u, @HeLLGURD, @hellofrommorgan, @hereicq, @hermz580, @HexLab98, @hinablue, @HOYALIM, @hrnbld,\\n@huntsyea, @iamwongeeeee, @ianks, @ibaldr89, @Icather, @ijevin, @ildunari, @Imgaojp, @imgyf, @immuhammadfurqan,\\n@iniak, @iRonin, @isheng-eqi, @iso2kx, @israellot, @itsflownium, @IvanMiao, @iveywest, @izumi0uu, @Jaaneek,\\n@JabberELF, @jackjin1997, @jakelongvu-bot, @Janig88, @jasoisjaso, @jbbottoms, @jeeaay, @jeeves-assistant,\\n@jeff-mettel, @JeffStone69, @JeliTron, @jethac, @jfmusa2024-cyber, @Jiahui-Gu, @jinglun010-cpu, @JoaoMarcos44,\\n@joelbrilliant, @John-Lussier, @johnkattenhorn, @Johnny-xuan, @johnnykor82, @joncaldwell90, @JonthanaHanh,\\n@jordanhubbard, @JorkeyLiu, @jquesnelle, @jrfbch, @juanmartitegui, @Julientalbot, @juniperbevensee, @justemu,\\n@kael-odin, @kaishi00, @kaiyisg, @kamonspecial, @kandotrun, @KCAYAAI, @keepConcentration, @kelsia14, @Kenmege,\\n@kerpopule, @Kev-fs, @Kewe63, @kharitonov-ivan, @Kingdomwarrior23, @kingrubic, @Kinkoolino-Hermes, @kjames2001,\\n@knoal, @kohoj, @Kolektori, @konsisumer, @koshaji, @kronexoi, @kshitijk4poor, @kuangmi-bit, @kudi88, @kylezh,\\n@kyssta-exe, @Kyzcreig, @lanyusea, @LauraGPT, @LavyaTandel, @LeonSGP43, @leoprodz, @LevSky22, @lEWFkRAD,\\n@lewis4x4, @lexgenius, @LFDMcore, @LiangYang666, @lidises, @light-merlin-dark, @lihengming, @Linux2010,\\n@LionGateOS, @liuhao1024, @liusencomic-cyber, @ljsdut, @lkevincc0, @LLQWQ, @locker95, @logical-and,\\n@LordNikon1983, @lost9999, @Love-JourneY, @LunarNexus, @luxiaolu4827, @lxman, @m4r13y, @MaartenDMT,\\n@MacroAnarchy, @maff-t2b, @MahdiHedhli, @mahdiwafy, @malaiwah, @mannnrachman, @mapu-og, @markoub, @matarbot,\\n@materemias, @matt-strawbridge, @mattezell, @MattMaximo, @mattmillerai, @mattshapsss, @MaxFreedomPollard,\\n@maxmilian, @McHermes, @mehmetkr-31, @menhguin, @metamon-p, @mijanx, @MLcogTech, @moeadham, @mollusk,\\n@monerostar, @MorAlekss, @morolab, @motoblurr, @MrAbsaroka, @mrzlab630, @ms-alan, @muctobi, @MustafaK99,\\n@mwbrooks, @myk0la-b, @mzkarami, @namredips, @nanami7777777, @nanckh, @natebransc, @nateEc, @navahc09,\\n@Ne0teric, @necoweb3, @nftpoetrist, @nickkarhan, @Ninso112, @nkreadly07, @nnnet, @NPFernando, @nrmjeremy,\\n@nu476, @null-runner, @nullptr0807, @NYTEMODEONLY, @obelisk-complex, @OfficialDelta, @okalentiev,\\n@oliviaaaa7788, @OmarB97, @omid3098, @ooiuuii, @oppenheimor, @oreoluwa, @ousiaresearch, @OutThisLife,\\n@panDing19, @paralegalia, @patp, @PavelTajdus, @peacockesq, @Pebrd, @peterw, @phantom-instruction-set,\\n@pierrenode, @PINKIIILQWQ, @piyushbag, @plainOldCode, @pnascimento9596, @pooyan6, @pprism13, @praneshnikhar,\\n@PRATHAMESH75, @Prontsevich, @quantumbyte1617, @QuarkAssistant, @Que0x, @Qwinty, @rayjerrywoo, @rayjun,\\n@Reaper-Forge, @RedClaus, @redsol-llc, @reinbeumer, @RelaxJonh, @RemyFevry, @replygirl, @rerdi92, @rhylryan21,\\n@RichardHojunJang, @richkapp, @rkfshakti, @rlaope, @rob-coco, @rob-maron, @robbyczgw-cla, @robgfl45,\\n@robzolkos, @rod-nxtlevel, @rodboev, @Roger--Han, @rsayar, @rudironsoni, @ruizanthony, @rungmc357, @s00rz,\\n@s905060, @Sahil-SS9, @sakhnenkoff, @salimhamed, @samrusani, @sbe27, @ScaleLeanChris, @schattenan, @sealca,\\n@seamusmore, @sebaorrego-koronet, @seppegadeyne, @sergioperezcheco, @sethmills21, @sg-shag, @sgtworkman,\\n@shady2k, @shannonsands, @shellybotmoyer, @sherman-yang, @shivasymbl, @Shizoqua, @SHL0MS, @simonmmafs,\\n@skyer-flyyy, @Skywind5487, @sl4m3, @Slopez2023, @SmallNew2003, @smfworks, @Soju06, @Solitud1nem,\\n@solyanviktor-star, @somewheresy, @Sora-bluesky, @sowork-skills, @soynchux, @sparkeros, @spfcraze,\\n@spiky02plateau, @spro-work, @SquabbyZ, @srojk34, @StanleyStetson, @StartupBros-com, @StellarisW,\\n@stephenschoettler, @Stoltemberg, @stremtec, @Studio729, @stwith, @subhoya, @sunwz1115, @suparious,\\n@supplefrog, @sycamoregroupltd, @szzhoujiarui, @tachyon-r, @Tamaz-sujashvili, @tandixit95, @tank321, @tavva,\\n@tcconnally, @temalo, @th3wingman, @the3asic, @TheAngryPit, @TheEpTic, @theone139344, @TheSmokeDev,\\n@thirstycrow, @tianma-if, @Tianworld, @tinetwork, @to-na, @tom-channel, @tomqiaozc, @toomij99, @trac3r00,\\n@Tranquil-Flow, @Trantor-develops, @trevorgordon981, @trevornk, @trippyogi, @trymhaak, @tsuk1nose, @tusharui,\\n@TutkuEroglu, @tw0316, @twe-cloud, @tymrtn, @UltraInstinct0x, @umi008, @unixwzrd, @upicat, @vaibhavjnf, @valda,\\n@Vansh5632, @Variable85, @vb3, @VerbalChainsaw, @vexclawx31, @victor-kyriazakos, @vigilancetech-com,\\n@virtuadex, @Vissirexa, @VittoriaLanzo, @VIVAAN-DHAWAN, @wangyunyou, @waroffchange, @wayne1992127, @web3blind,\\n@webtecnica, @WeiYusc, @wen0531, @wernerhp, @wesleysimplicio, @westkite1201, @wgd753, @wgu9,\\n@wjj1872744570-source, @wjq990112, @WojtekMR3, @WOLIKIMCHENG, @Wpnx330, @wreed4, @wuli666, @WXBR, @wz-heng,\\n@x7peeps, @xcompass, @xd-Neji, @xenodmc, @Xipong, @xrazai, @Xue-1997, @xxxigm, @y0shua1ee, @yaleman,\\n@yemi-lagosinternationalmarket, @ygd58, @yingliang-zhang, @yinkev, @yitang, @YLChen-007, @yoniebans,\\n@youzi-1122, @ypwcharles, @yuexiongHNU, @yungchentang, @yuzilongleif-collab, @yyzquwu, @z23, @ZachariahChu,\\n@zakhounet, @zapabob, @Zavianx, @zehuaw1, @zengzheqing, @Zeraphim, @zgzczzw, @zhangyang-crazy-one,\\n@Zhekinmaksim, @Zioywishing, @zmlgit, @zombopanda, @ZundamonnoVRChatkaisetu\\nAlso: @kyssta-exe 25470058+kyssta-exe.\\n\\nThank you to every one of the 647 people who contributed code, co-authored fixes, filed the ~1,200 issues this release closes, and had their PRs salvaged into main. Hermes ships this fast because of you.\\n\\n---\\n\\n**Full Changelog**: [v2026.7.20...v2026.8.3](https://github.com/NousResearch/hermes-agent/compare/v2026.7.20...v2026.8.3)\\n\", \"published_at\": \"2026-08-03T16:57:52Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/releases/tag/v2026.8.3\"}, {\"id\": 3, \"project_id\": 1, \"tag_name\": \"v2026.7.30\", \"name\": \"Hermes Agent v0.19.1 (v2026.7.30)\", \"body\": \"# Hermes Agent v0.19.1 (v2026.7.30)\\n\\n**Release Date:** July 30, 2026\\n\\n> Patch release. This tag rolls up the ~1,000+ PRs merged since v0.19.0 into a stable tagged release for downstream consumers (Docker images, hosted deployments, fresh installs).\\n\\n---\\n\\n## About this release\\n\\nSince v2026.7.20 (v0.19.0, July 20): **~2,789 commits · ~4,748 files changed · ~442,000 insertions · ~392,300 deletions** on `main`. This window is dominated by bug-fix and salvage waves across the gateway, voice subsystem, desktop app, and installer, plus continued platform work (Buzz/Nostr channel, FLUX3 video generation and delivery, Telegram media reliability, voice-mode regressions).\\n\\n**Full curated release notes for this window will ship with v0.20.0**, which will document everything from v0.19.0 onward — highlights, feature areas, and complete contributor credits. Nothing in this window is skipped.\\n\\n## Updating\\n\\n```bash\\nhermes update\\n# or fresh install:\\ncurl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash\\n```\\n\\n**Full Changelog**: [v2026.7.20...v2026.7.30](https://github.com/NousResearch/hermes-agent/compare/v2026.7.20...v2026.7.30)\\n\", \"published_at\": \"2026-07-30T23:45:37Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.30\"}, {\"id\": 4, \"project_id\": 1, \"tag_name\": \"v2026.7.20\", \"name\": \"Hermes Agent v0.19.0 (2026.7.20) — The Quicksilver Release\", \"body\": \"# Hermes Agent v0.19.0 (v2026.7.20)\\n\\n\\n**Release Date:** July 20, 2026\\n**Since v0.18.0:** ~2,245 commits · ~1,065 merged PRs · ~2,465 files changed · ~300,000 insertions · ~36,000 deletions · **~3,300 issues closed** · **450+ community contributors**\\n\\n> **The Quicksilver Release.** Hermes is the messenger god, and this window we made him move like it. First-turn time-to-first-token dropped **~80% on every platform**, reasoning streams live by default, the desktop app got a ~20-PR speed overhaul (14× faster streaming markdown, virtualized diffs, snappy session switching), and the TUI renders markdown incrementally. Around that speed spine: you can now **manage your Nous subscription without leaving the terminal**, plug **Bitwarden and 1Password** straight into Hermes, let **smart approvals** judge flagged commands for you by default, **watch your subagents work live**, and trust that a finished response **survives a gateway crash** thanks to a durable delivery ledger. This release also rolls up everything from the v0.18.1 and v0.18.2 infrastructure patch tags — those windows are fully documented here.\\n\\n---\\n\\n## ✨ Highlights\\n\\n- **Hermes got dramatically faster — first token in a fraction of the time** — Cold-start \\\"Initializing agent...\\\" used to eat ~4.3 seconds before your first turn even reached the model; it's now ~0.9s, an ~80% cut that applies to the CLI, gateway, TUI, desktop, and cron alike. Round 2 attacked what you *see* while waiting: reasoning models now stream their thinking live by default (no more staring at a spinner for 30 seconds), and the response box paints per token instead of per line. If Hermes ever felt like it took a deep breath before answering, that breath is gone. ([#59332](https://github.com/NousResearch/hermes-agent/pull/59332), [#59389](https://github.com/NousResearch/hermes-agent/pull/59389) — @teknium1)\\n\\n- **The desktop app speed wave — 20+ targeted perf PRs** — Long replies used to cost 14× more CPU in the markdown splitter than they do now; giant diffs froze the review pane until we virtualized it; switching sessions thrashes layout no more. Streaming no longer re-renders the sidebar and every tool row per token, profile backends pre-warm on hover intent, and boot-hidden panes mount at idle instead of on the cold-start critical path. The net effect: the desktop app feels like a native app under load, even with huge transcripts and busy agents. ([#67154](https://github.com/NousResearch/hermes-agent/pull/67154), [#67818](https://github.com/NousResearch/hermes-agent/pull/67818), [#65898](https://github.com/NousResearch/hermes-agent/pull/65898), [#66033](https://github.com/NousResearch/hermes-agent/pull/66033), [#66747](https://github.com/NousResearch/hermes-agent/pull/66747), [#67742](https://github.com/NousResearch/hermes-agent/pull/67742) and more — @OutThisLife)\\n\\n- **Manage your Nous plan from the terminal — `/subscription` and `/topup`** — Changing your subscription used to mean a trip to the billing website. Now `/subscription` opens a full flow right in the TUI or classic CLI: see your plan and remaining allowance, preview exactly what an upgrade costs (\\\"Pay $46.30 & upgrade now\\\") or when a downgrade takes effect, and apply it — with scheduled-change banners and undo. The desktop app got a matching billing settings tab. Your wallet never has to leave the keyboard. ([#51639](https://github.com/NousResearch/hermes-agent/pull/51639), [#61054](https://github.com/NousResearch/hermes-agent/pull/61054), [#61067](https://github.com/NousResearch/hermes-agent/pull/61067) — @alt-glitch)\\n\\n- **Smart approvals are now the default** — When Hermes wants to run a flagged command, an LLM reviewer now assesses it independently instead of asking you to approve every single one — and each verdict covers only that exact command, so a later command matching the same pattern gets its own review. Combined with the new **user-defined deny rules** (which block commands even under yolo mode) and `/deny ` (which tells the agent *why* you refused so it course-corrects), day-to-day approval fatigue drops sharply without giving up control. ([#62661](https://github.com/NousResearch/hermes-agent/pull/62661), [#59164](https://github.com/NousResearch/hermes-agent/pull/59164), [#54518](https://github.com/NousResearch/hermes-agent/pull/54518) — @teknium1)\\n\\n- **Plug your password manager into Hermes — Bitwarden & 1Password secret sources** — API keys no longer have to live in a plaintext `.env`. A new pluggable `SecretSource` interface lets Hermes fetch secrets from Bitwarden and 1Password (`op://` references) at load time, with multiple vaults enabled simultaneously, deterministic precedence, conflict warnings, and per-variable provenance. This consolidated eleven competing community PRs into one orchestrated interface — future vault providers drop in as plugins. ([#59498](https://github.com/NousResearch/hermes-agent/pull/59498) — @teknium1, 1Password provider salvaged from @hwrdprkns)\\n\\n- **Watch your subagents work — live transcripts + durable background delegation** — `delegate_task` dispatches now return live transcript files you can `tail -f` the moment the subagents launch: every tool call, result, and streamed reply, one human-readable log per child. And background delegation completions are now **durable** — if the process restarts mid-run, results are restored and delivered through an ownership-checked ledger instead of vanishing. Fan out a fleet, watch any worker live, and never lose the results. ([#67479](https://github.com/NousResearch/hermes-agent/pull/67479), [#63494](https://github.com/NousResearch/hermes-agent/pull/63494) — @teknium1)\\n\\n- **A finished answer can no longer be lost — the delivery-obligation ledger** — If the gateway died between generating your response and confirming the platform actually delivered it, that answer used to be silently gone (and you'd paid for the turn). Final responses are now recorded in a durable ledger in `state.db` around the platform send and **redelivered on the next boot** — closing a P1 silent-loss window for Telegram, Discord, Slack, and every other channel. ([#67181](https://github.com/NousResearch/hermes-agent/pull/67181) — @teknium1)\\n\\n- **One gateway, many profiles — profile-based message routing** — A single multiplexed gateway sharing one bot token can now route specific guilds, channels, or threads to different profiles — each with fully isolated config, skills, memory, and secrets. Point your work Discord server at the `work` profile and your hobby server at `personal`, from one bot. A second multiplex hardening wave means one misconfigured profile can no longer take down the whole gateway. ([#64835](https://github.com/NousResearch/hermes-agent/pull/64835) salvaging @Burgunthy, [#65700](https://github.com/NousResearch/hermes-agent/pull/65700), [#60589](https://github.com/NousResearch/hermes-agent/pull/60589) — @teknium1, @benbarclay + six salvaged contributors)\\n\\n- **New providers and the newest frontier models** — Fireworks AI and DeepInfra land as first-class providers (Fireworks with cost estimation and a #2 slot in the provider picker), Upstage Solar joins via salvage, and the model catalogs picked up **GPT-5.6 (Sol/Terra/Luna + Pro variants, wired end-to-end across every route)**, **grok-4.5 (GA)**, **moonshotai/kimi-k3**, **claude-fable-5 / claude-sonnet-5**, and GA **tencent/hy3** — plus LM Studio JIT model loading for local setups. ([#62593](https://github.com/NousResearch/hermes-agent/pull/62593), [#63969](https://github.com/NousResearch/hermes-agent/pull/63969), [#61616](https://github.com/NousResearch/hermes-agent/pull/61616) — @kshitijk4poor completing @rob-maron's #61578, [#60887](https://github.com/NousResearch/hermes-agent/pull/60887), [#65913](https://github.com/NousResearch/hermes-agent/pull/65913), [#64541](https://github.com/NousResearch/hermes-agent/pull/64541), [#65472](https://github.com/NousResearch/hermes-agent/pull/65472))\\n\\n- **Crank the thinking to max — new reasoning effort tiers and per-model control** — Reasoning effort gained `max` and `ultra` levels (GPT-5.6 and Codex's top tiers), selectable everywhere from the CLI to the desktop, with sane clamping on providers with smaller scales. You can now also pin **per-model reasoning-effort overrides** in config, set **per-slot effort in MoA presets** (your advisors think hard, your synthesizer stays fast), and per-task effort for auxiliary models. Thinking depth is now a dial, not a global switch. ([#62650](https://github.com/NousResearch/hermes-agent/pull/62650), [#64458](https://github.com/NousResearch/hermes-agent/pull/64458), [#64631](https://github.com/NousResearch/hermes-agent/pull/64631), [#64597](https://github.com/NousResearch/hermes-agent/pull/64597) — @teknium1)\\n\\n- **Your sessions, your data — export everything** — `hermes sessions export` now writes Markdown, Quarto, HTML, prompt-only, and even Hugging Face-ready trace formats, with the full filter surface (age, workspace, platform), an opt-in `--redact` secret-scrubbing pass, and compacted-session lineage stitched into one logical export. Pair with the new prune filters and bulk archive to keep your session store tidy. Your conversation history is a real dataset now, not a black box. ([#60186](https://github.com/NousResearch/hermes-agent/pull/60186) salvaging @web3blind, [#60492](https://github.com/NousResearch/hermes-agent/pull/60492), [#60507](https://github.com/NousResearch/hermes-agent/pull/60507), [#59327](https://github.com/NousResearch/hermes-agent/pull/59327) — @teknium1)\\n\\n- **Security hardening round** — This window closed a long list of credential-surface gaps: Vertex credentials scoped away from subprocess env and through profile secret scopes, media/vision/image-gen local-file reads routed through one shared credential-read guard, a webhook body-size-cap sweep across every aiohttp server, bot-token redaction in Telegram transport errors, Fireworks token prefixes added to the redactor, six P1 browser/MEDIA/.env hardening PRs salvaged in one pass, and CI hardened against untrusted-ref interpolation. ([#57660](https://github.com/NousResearch/hermes-agent/pull/57660), [#58709](https://github.com/NousResearch/hermes-agent/pull/58709), [#59215](https://github.com/NousResearch/hermes-agent/pull/59215), [#56582](https://github.com/NousResearch/hermes-agent/pull/56582), [#57842](https://github.com/NousResearch/hermes-agent/pull/57842) — @teknium1, @srojk34, @kshitijk4poor, @jquesnelle)\\n\\n---\\n\\n## ⚡ Performance — the speed spine\\n\\n### First-turn latency (all platforms)\\n- **~80% TTFT cut** — Discord capability detection off the critical path (token-keyed 24h disk cache + background refresh), Ollama probe skipped for known non-Ollama providers, agent-init blocking work removed; cold submit→dispatch ~4.3s → ~0.9s ([#59332](https://github.com/NousResearch/hermes-agent/pull/59332) — @teknium1)\\n- **Perceived-latency round 2** — `display.show_reasoning` default ON (watch the model think instead of a spinner), per-token response-box painting with width-aware force-flush, prompt-build caching, mtime-cached timezone resolution ([#59389](https://github.com/NousResearch/hermes-agent/pull/59389) — @teknium1)\\n- Segment mixed tool batches to recover lost concurrency; drop per-call base64 re-serialization from request-size estimates ([#64460](https://github.com/NousResearch/hermes-agent/pull/64460), [#67788](https://github.com/NousResearch/hermes-agent/pull/67788) — @teknium1, @OutThisLife)\\n\\n### Desktop speed wave\\n- 14× less splitter CPU via incremental block lexing for streaming markdown; virtualized review-pane diffs (no more full-Shiki freeze); snappy session switching on large transcripts; killed the layout-thrash cascade on session switch ([#67154](https://github.com/NousResearch/hermes-agent/pull/67154), [#67818](https://github.com/NousResearch/hermes-agent/pull/67818), [#65898](https://github.com/NousResearch/hermes-agent/pull/65898), [#66033](https://github.com/NousResearch/hermes-agent/pull/66033) — @OutThisLife)\\n- Cut startup serialization + per-turn REST amplification; pre-warm profile backends and gateway sockets on hover intent; idle-mount boot-hidden panes; fast model picker + dialogs ([#66747](https://github.com/NousResearch/hermes-agent/pull/66747), [#66347](https://github.com/NousResearch/hermes-agent/pull/66347), [#67857](https://github.com/NousResearch/hermes-agent/pull/67857), [#66470](https://github.com/NousResearch/hermes-agent/pull/66470) — @OutThisLife)\\n- Stop per-token sidebar + tool-row re-renders during streaming; stop eager JSON.stringify of every tool's args/result; scope tool-diff subscriptions; batch sidebar session slices into one profile-DB pass; targeted file-tree revalidation; rAF-coalesced sash resizes ([#67742](https://github.com/NousResearch/hermes-agent/pull/67742), [#67842](https://github.com/NousResearch/hermes-agent/pull/67842), [#67195](https://github.com/NousResearch/hermes-agent/pull/67195), [#67245](https://github.com/NousResearch/hermes-agent/pull/67245), [#67824](https://github.com/NousResearch/hermes-agent/pull/67824), [#67838](https://github.com/NousResearch/hermes-agent/pull/67838), [#67844](https://github.com/NousResearch/hermes-agent/pull/67844) — @OutThisLife)\\n- Systematized perf benchmark harness with trustworthy cold-start + first-token measurement, replacing 12 one-off scripts ([#67466](https://github.com/NousResearch/hermes-agent/pull/67466), [#67697](https://github.com/NousResearch/hermes-agent/pull/67697) — @OutThisLife)\\n\\n### Everywhere else\\n- TUI renders streamed markdown incrementally per block ([#67236](https://github.com/NousResearch/hermes-agent/pull/67236) — @OutThisLife)\\n- Skill discovery cached by scan signature; snapshot manifest builds ~5× faster; text prefilter before AST parse in tool discovery ([#61414](https://github.com/NousResearch/hermes-agent/pull/61414), [#61131](https://github.com/NousResearch/hermes-agent/pull/61131), [#63941](https://github.com/NousResearch/hermes-agent/pull/63941) — @kshitijk4poor, @ethernet8023)\\n- Copy-on-write message prep instead of full deepcopy; model-metadata probe-cache cluster; gateway `session.resume` model + display history from one SELECT ([#61133](https://github.com/NousResearch/hermes-agent/pull/61133), [#61368](https://github.com/NousResearch/hermes-agent/pull/61368), [#67247](https://github.com/NousResearch/hermes-agent/pull/67247) — @kshitijk4poor, @OutThisLife)\\n- `hermes update` skips npm install when Node manifests are unchanged; dashboard session-list payloads trimmed + messages paginated ([#61580](https://github.com/NousResearch/hermes-agent/pull/61580), [#60883](https://github.com/NousResearch/hermes-agent/pull/60883) — @kshitijk4poor)\\n- Byte-stable gateway system prompts — pinned session-context render keeps the prompt cache alive across turns ([#67403](https://github.com/NousResearch/hermes-agent/pull/67403) — @kshitijk4poor)\\n\\n## 🏗️ Core Agent & Architecture\\n\\n### Providers & models\\n- **Fireworks AI provider** with cost estimation + cached picker price columns, promoted to #2 in provider pickers ([#62593](https://github.com/NousResearch/hermes-agent/pull/62593), [#65476](https://github.com/NousResearch/hermes-agent/pull/65476), [#65214](https://github.com/NousResearch/hermes-agent/pull/65214) — @teknium1)\\n- **DeepInfra** hardened integration; **Upstage Solar** provider (#42231 salvage) ([#63969](https://github.com/NousResearch/hermes-agent/pull/63969), [#64541](https://github.com/NousResearch/hermes-agent/pull/64541) — @kshitijk4poor)\\n- **GPT-5.6 (Sol/Terra/Luna + Pro) end-to-end** — context lengths, native/Codex catalogs, pricing, compaction caps across every route ([#61616](https://github.com/NousResearch/hermes-agent/pull/61616) — @kshitijk4poor, building on @rob-maron)\\n- grok-4.5 (GA) catalog + reasoning allowlist; kimi-k3 on Nous Portal + OpenRouter (kimi-k2.x retired) + K3 discovery on the Kimi Coding endpoint; claude-fable-5 / claude-sonnet-5 / fugu-ultra curated; GA tencent/hy3 ([#60887](https://github.com/NousResearch/hermes-agent/pull/60887), [#65913](https://github.com/NousResearch/hermes-agent/pull/65913), [#65922](https://github.com/NousResearch/hermes-agent/pull/65922), [#56617](https://github.com/NousResearch/hermes-agent/pull/56617), [#60943](https://github.com/NousResearch/hermes-agent/pull/60943) — @teknium1)\\n- Catalog-labeled silent default (GLM-5.2) + bare-provider `/model` cost-safe routing; LM Studio JIT load mode; adaptive thinking for Kimi-family Anthropic endpoints ([#64771](https://github.com/NousResearch/hermes-agent/pull/64771), [#65472](https://github.com/NousResearch/hermes-agent/pull/65472), [#67606](https://github.com/NousResearch/hermes-agent/pull/67606) — @teknium1, @kshitijk4poor)\\n- GLM-5.2 native reasoning_effort controls; Gemini request-context improvements; extra HTTP headers for LLM API calls; per-client model routing on the API server ([#58884](https://github.com/NousResearch/hermes-agent/pull/58884), [#61873](https://github.com/NousResearch/hermes-agent/pull/61873) — @vishal-dharm, [#57038](https://github.com/NousResearch/hermes-agent/pull/57038), [#57028](https://github.com/NousResearch/hermes-agent/pull/57028) — @teknium1)\\n- **Claude Sonnet 5 fully wired** — curated lists, intro pricing, and metadata across every route ([#67932](https://github.com/NousResearch/hermes-agent/pull/67932) — @teknium1)\\n- **Hide providers you don't use** — `enabled: false` per-provider flag + `excluded_providers` config scrub unwanted providers from `/model` pickers and built-in resolution ([#67971](https://github.com/NousResearch/hermes-agent/pull/67971) — @teknium1)\\n- Bedrock catalog wave: real context-window probing from the live endpoint, 1M-context rows for current-gen Claude + Fable, geo-prefix parity, versioned profile-ID pricing, Opus 4.8/4.7 rows ([#68007](https://github.com/NousResearch/hermes-agent/pull/68007), [#67977](https://github.com/NousResearch/hermes-agent/pull/67977), [#68005](https://github.com/NousResearch/hermes-agent/pull/68005), [#67976](https://github.com/NousResearch/hermes-agent/pull/67976) — @teknium1)\\n- kimi-k3 rollout completed across Kimi-direct catalog surfaces with 1M context on canonical Kimi Coding endpoints ([#68108](https://github.com/NousResearch/hermes-agent/pull/68108) — @teknium1)\\n- Provider pickers: Qwen providers folded into one group row; collapsible provider groups in the desktop model picker; friendlier TUI model display grouping same-endpoint providers ([#67758](https://github.com/NousResearch/hermes-agent/pull/67758), [#67904](https://github.com/NousResearch/hermes-agent/pull/67904), [#67908](https://github.com/NousResearch/hermes-agent/pull/67908) — @teknium1)\\n\\n### Reasoning & MoA\\n- `max` + `ultra` effort levels across every surface and route ([#62650](https://github.com/NousResearch/hermes-agent/pull/62650) — @teknium1)\\n- Per-model reasoning_effort overrides via a unified resolution chokepoint; per-task auxiliary effort; per-slot MoA preset effort; session-scoped `/reasoning` in the CLI ([#64458](https://github.com/NousResearch/hermes-agent/pull/64458), [#64597](https://github.com/NousResearch/hermes-agent/pull/64597), [#64631](https://github.com/NousResearch/hermes-agent/pull/64631), [#67946](https://github.com/NousResearch/hermes-agent/pull/67946) — @teknium1)\\n- MoA: `reference_max_tokens` to cap advisor output and cut latency; per-preset fanout cadence (`user_turn` runs advisors once per user turn); stale presets surfaced without retries; half-filled preset saves rejected at the API boundary; aggregator resolves reasoning like an acting model ([#56756](https://github.com/NousResearch/hermes-agent/pull/56756), [#57591](https://github.com/NousResearch/hermes-agent/pull/57591), [#64756](https://github.com/NousResearch/hermes-agent/pull/64756) — @teknium1)\\n\\n### Delegation, approvals & the agent loop\\n- Live subagent transcripts + durable background completions (see Highlights) ([#67479](https://github.com/NousResearch/hermes-agent/pull/67479), [#63494](https://github.com/NousResearch/hermes-agent/pull/63494) — @teknium1)\\n- Smart approvals default; user-defined deny rules (block even under yolo); `/deny ` relays the denial reason; plugin `pre_tool_call` approve action escalates to a human gate (re-landed with rule keys) ([#62661](https://github.com/NousResearch/hermes-agent/pull/62661), [#59164](https://github.com/NousResearch/hermes-agent/pull/59164), [#54518](https://github.com/NousResearch/hermes-agent/pull/54518), [#60504](https://github.com/NousResearch/hermes-agent/pull/60504) — @teknium1, @kshitijk4poor)\\n- Unified delegation concurrency caps (`max_async_children` deprecated); explain long provider waits on the live status line; deterministic tool-output risk exposure ([#56955](https://github.com/NousResearch/hermes-agent/pull/56955), [#64775](https://github.com/NousResearch/hermes-agent/pull/64775), [#61793](https://github.com/NousResearch/hermes-agent/pull/61793) — @teknium1)\\n- Codex: live TUI/desktop tool cards for the app-server runtime, commentary streamed as visible interim messages, compaction routed through `thread/compact/start`, max-output truncation recovery, oversized message ids dropped on replay, banked usage-limit resets via `/usage reset` ([#66514](https://github.com/NousResearch/hermes-agent/pull/66514), [#66115](https://github.com/NousResearch/hermes-agent/pull/66115), [#60114](https://github.com/NousResearch/hermes-agent/pull/60114), [#58155](https://github.com/NousResearch/hermes-agent/pull/58155), [#62225](https://github.com/NousResearch/hermes-agent/pull/62225) — @teknium1, @kshitijk4poor, @JoaoMarcos44, [#64280](https://github.com/NousResearch/hermes-agent/pull/64280) — @teknium1)\\n- Hooks: oversized hook-injected context spills to disk ([#20468](https://github.com/NousResearch/hermes-agent/pull/20468) — @teknium1)\\n- Vibe reactions — floating hearts on affection across CLI/TUI/desktop, token-free core detection ([#62016](https://github.com/NousResearch/hermes-agent/pull/62016) — @OutThisLife)\\n\\n### Secrets & config\\n- Pluggable `SecretSource` interface + Bitwarden & 1Password providers (see Highlights) ([#59498](https://github.com/NousResearch/hermes-agent/pull/59498) — @teknium1, @hwrdprkns)\\n- `hermes config get` / `unset`; warn on unknown root config keys + doctor deprecated-key reporting; `display.timestamp_format` ([#65540](https://github.com/NousResearch/hermes-agent/pull/65540), [#67370](https://github.com/NousResearch/hermes-agent/pull/67370), [#40622](https://github.com/NousResearch/hermes-agent/pull/40622) — @teknium1)\\n- Auxiliary model usage recorded per task in session accounting; conversation-scoped Nous Portal usage tags across aux/MoA/delegate calls; `--usage-file` JSON report for `hermes -z` ([#65537](https://github.com/NousResearch/hermes-agent/pull/65537), [#65468](https://github.com/NousResearch/hermes-agent/pull/65468), [#59615](https://github.com/NousResearch/hermes-agent/pull/59615) — @teknium1)\\n\\n### Sessions & compression\\n- Sessions export: Markdown/QMD/HTML/prompt-only/trace formats, HF upload, `--redact`, unified filters; full prune filter surface + bulk archive; CLI workspace filter + restore-cwd-on-resume ([#60186](https://github.com/NousResearch/hermes-agent/pull/60186), [#60492](https://github.com/NousResearch/hermes-agent/pull/60492), [#60507](https://github.com/NousResearch/hermes-agent/pull/60507), [#59327](https://github.com/NousResearch/hermes-agent/pull/59327), [#63091](https://github.com/NousResearch/hermes-agent/pull/63091) — @teknium1, @web3blind)\\n- Compression: preserve human intent and durable handoffs; retain prompt cache when memory is unchanged; flatten multimodal content for the summarizer keeping image handles; gateway compression routing integrity ([#67275](https://github.com/NousResearch/hermes-agent/pull/67275), [#67916](https://github.com/NousResearch/hermes-agent/pull/67916), [#65046](https://github.com/NousResearch/hermes-agent/pull/65046), [#56868](https://github.com/NousResearch/hermes-agent/pull/56868) — @kshitijk4poor, @teknium1)\\n- Gateway session metadata consolidated into state.db; routing index moved to state.db (sessions.json now an optional legacy mirror); exact API bytes persisted in an `api_content` sidecar ([#58899](https://github.com/NousResearch/hermes-agent/pull/58899), [#59203](https://github.com/NousResearch/hermes-agent/pull/59203), [#67274](https://github.com/NousResearch/hermes-agent/pull/67274) — @teknium1, @kshitijk4poor)\\n\\n## 🌐 Gateway, Fleet & Relay\\n- **Durable delivery-obligation ledger** for final responses (see Highlights) ([#67181](https://github.com/NousResearch/hermes-agent/pull/67181) — @teknium1)\\n- **Profile-based routing for inbound messages** + multiplex hardening wave 2 + `GATEWAY_MULTIPLEX_PROFILES` override (see Highlights) ([#64835](https://github.com/NousResearch/hermes-agent/pull/64835), [#65700](https://github.com/NousResearch/hermes-agent/pull/65700), [#60589](https://github.com/NousResearch/hermes-agent/pull/60589) — @teknium1, @benbarclay + salvaged contributors)\\n- Per-session turn lease + conversation-scope funnel; unified session reset boundaries (reset sessions stay reset); truthful runtime readiness checks; per-channel model and system prompt overrides; per-session `/model` overrides persist across restarts ([#67401](https://github.com/NousResearch/hermes-agent/pull/67401), [#65783](https://github.com/NousResearch/hermes-agent/pull/65783), [#62645](https://github.com/NousResearch/hermes-agent/pull/62645), [#56967](https://github.com/NousResearch/hermes-agent/pull/56967), [#57030](https://github.com/NousResearch/hermes-agent/pull/57030) — @teknium1)\\n- Session auto-reset default off; `/sessions search `; webhook payload filters + route scripts; platform HTTP event callback routing; configurable long-running status phrases ([#60194](https://github.com/NousResearch/hermes-agent/pull/60194), [#57685](https://github.com/NousResearch/hermes-agent/pull/57685), [#60944](https://github.com/NousResearch/hermes-agent/pull/60944), [#65702](https://github.com/NousResearch/hermes-agent/pull/65702), [#58872](https://github.com/NousResearch/hermes-agent/pull/58872) — @teknium1)\\n- Relay: generic OIDC client-credentials provisioning (NAS-free), routed profile carried from the connector wire source, channel context consumed from the connector; Nous auth forensics + `nous_session_valid` on `/api/status` for hosted self-heal; Docker re-seeds a terminally-dead Nous bootstrap session on boot ([#60730](https://github.com/NousResearch/hermes-agent/pull/60730), [#60586](https://github.com/NousResearch/hermes-agent/pull/60586), [#64649](https://github.com/NousResearch/hermes-agent/pull/64649), [#59976](https://github.com/NousResearch/hermes-agent/pull/59976), [#59969](https://github.com/NousResearch/hermes-agent/pull/59969), [#59983](https://github.com/NousResearch/hermes-agent/pull/59983) — @benbarclay)\\n\\n## 📱 Messaging Platforms\\n- **Inline choice pickers** for `/reasoning` and `/fast` on Telegram, Discord, and Matrix — one-tap native buttons instead of typing ([#65799](https://github.com/NousResearch/hermes-agent/pull/65799) — @teknium1)\\n- WhatsApp: native Baileys polls (clarify renders as a poll), locations, rich inbound metadata; dashboard pairing flow ([#58865](https://github.com/NousResearch/hermes-agent/pull/58865), [#60571](https://github.com/NousResearch/hermes-agent/pull/60571) — @teknium1)\\n- Discord: recover messages missed during reconnect; auto-created threads renamed to generated session titles; configurable interactive view timeout; opt-in owner mentions on exec-approval prompts; optional admin-only gate for approval buttons ([#66149](https://github.com/NousResearch/hermes-agent/pull/66149), [#60187](https://github.com/NousResearch/hermes-agent/pull/60187), [#60230](https://github.com/NousResearch/hermes-agent/pull/60230), [#60493](https://github.com/NousResearch/hermes-agent/pull/60493), [#51751](https://github.com/NousResearch/hermes-agent/pull/51751) — @teknium1)\\n- Slack: live per-tool status line ([#67080](https://github.com/NousResearch/hermes-agent/pull/67080) — @teknium1, salvaging #62007)\\n- Telegram: per-topic free-response allowlist; Google Chat clarify prompts rendered as cards ([#65543](https://github.com/NousResearch/hermes-agent/pull/65543), [#65546](https://github.com/NousResearch/hermes-agent/pull/65546) — @teknium1)\\n- Voice: `stt.echo_transcripts` toggle; MEDIA: captions attached to the media bubble on standalone sends; `display.tool_progress: log` option ([#58859](https://github.com/NousResearch/hermes-agent/pull/58859), [#61415](https://github.com/NousResearch/hermes-agent/pull/61415), [#57014](https://github.com/NousResearch/hermes-agent/pull/57014) — @teknium1, @kshitijk4poor)\\n\\n## 🖥️ Hermes Desktop App\\n- **Contribution-driven shell on a layout-tree model** — panes, zones, and layouts as data; plugin-scoped i18n locale bundles followed ([#60638](https://github.com/NousResearch/hermes-agent/pull/60638), [#67303](https://github.com/NousResearch/hermes-agent/pull/67303) — @OutThisLife)\\n- **Capabilities page** — Skills/Tools/MCP + Hub in one place, with responsive overlay nav; CLI/dashboard parity for skills hub, MCP test/toggle/catalog, maintenance ops, log filters; five UX fixes from live testing ([#57590](https://github.com/NousResearch/hermes-agent/pull/57590), [#57441](https://github.com/NousResearch/hermes-agent/pull/57441), [#67482](https://github.com/NousResearch/hermes-agent/pull/67482) — @OutThisLife, @teknium1)\\n- **Hermes Cloud connection mode** (salvage of #55402); soft gateway switch + gateway-settings polish; terminal execution backend picker with health probes ([#61912](https://github.com/NousResearch/hermes-agent/pull/61912), [#61916](https://github.com/NousResearch/hermes-agent/pull/61916), [#67203](https://github.com/NousResearch/hermes-agent/pull/67203) — @OutThisLife, @teknium1)\\n- Keybind hint tooltips + keybinds settings tab + unified worktree dialog; base-branch picker for new worktrees; green unread dot for background-finished sessions; background-task sidebar indicators; grouped tool calls across text-less messages; auto-scrolling window for long tool-call runs ([#65204](https://github.com/NousResearch/hermes-agent/pull/65204), [#62243](https://github.com/NousResearch/hermes-agent/pull/62243), [#65109](https://github.com/NousResearch/hermes-agent/pull/65109), [#65174](https://github.com/NousResearch/hermes-agent/pull/65174), [#61147](https://github.com/NousResearch/hermes-agent/pull/61147), [#57913](https://github.com/NousResearch/hermes-agent/pull/57913) — @ethernet8023, @OutThisLife)\\n- Session + project color system (inherit from project, per-session override, shared across sidebar/tabs); unified active-project identity in chat status; workspace path status action ([#67469](https://github.com/NousResearch/hermes-agent/pull/67469), [#67681](https://github.com/NousResearch/hermes-agent/pull/67681), [#67282](https://github.com/NousResearch/hermes-agent/pull/67282), [#63086](https://github.com/NousResearch/hermes-agent/pull/63086) — @OutThisLife)\\n- Declarative memory-provider panel + full-config modal; config-defined TTS/STT providers + xAI TTS params; custom endpoint settings; per-job cron model picker; profile-aware approval mode control; UI scale setting; Ctrl/Cmd+wheel zoom; chat backdrop toggle; `/journey` opens the memory graph overlay ([#67206](https://github.com/NousResearch/hermes-agent/pull/67206) salvaging @erosika, [#67209](https://github.com/NousResearch/hermes-agent/pull/67209), [#67759](https://github.com/NousResearch/hermes-agent/pull/67759) — @austinpickett, [#67472](https://github.com/NousResearch/hermes-agent/pull/67472), [#63520](https://github.com/NousResearch/hermes-agent/pull/63520), [#60457](https://github.com/NousResearch/hermes-agent/pull/60457), [#67029](https://github.com/NousResearch/hermes-agent/pull/67029), [#64598](https://github.com/NousResearch/hermes-agent/pull/64598), [#57267](https://github.com/NousResearch/hermes-agent/pull/57267) — @teknium1, @OutThisLife)\\n- Full TypeScript conversion of the desktop tree ([#57855](https://github.com/NousResearch/hermes-agent/pull/57855) — @ethernet8023)\\n\\n## 📊 Web Dashboard\\n- Memory provider switching; safe session import flow; WhatsApp pairing; Discord-specific toolsets editable from the web UI; clarified manual Telegram bot setup ([#60569](https://github.com/NousResearch/hermes-agent/pull/60569), [#63699](https://github.com/NousResearch/hermes-agent/pull/63699), [#60571](https://github.com/NousResearch/hermes-agent/pull/60571), [#65361](https://github.com/NousResearch/hermes-agent/pull/65361), [#64636](https://github.com/NousResearch/hermes-agent/pull/64636) — @teknium1, @kshitijk4poor, @shannonsands)\\n- Terminal keep-alive + reattach for dashboard chat sessions; heavy turns isolated in a compute host; paste/drop images into Chat; `browser.headed` schema toggle; profile + gateway topology on `/api/status`; mobile/hosted OpenAI OAuth login ([#60515](https://github.com/NousResearch/hermes-agent/pull/60515), [#65895](https://github.com/NousResearch/hermes-agent/pull/65895), [#61929](https://github.com/NousResearch/hermes-agent/pull/61929), [#67046](https://github.com/NousResearch/hermes-agent/pull/67046), [#60537](https://github.com/NousResearch/hermes-agent/pull/60537), [#61330](https://github.com/NousResearch/hermes-agent/pull/61330) — @teknium1, @OutThisLife, @benbarclay)\\n- `hermes serve` is a true headless backend (no web UI build/mount) ([#55923](https://github.com/NousResearch/hermes-agent/pull/55923) — @OutThisLife)\\n\\n## 🧰 CLI & TUI\\n- `/subscription` + `/topup` terminal billing (see Highlights) ([#51639](https://github.com/NousResearch/hermes-agent/pull/51639) — @alt-glitch)\\n- **`/model --once`** — one-turn model override that reverts automatically ([#67113](https://github.com/NousResearch/hermes-agent/pull/67113) — @teknium1, salvaging #29923)\\n- **Stacked slash-skill invocations** — `/skill-a /skill-b do XYZ` loads both skills in order (Claude Code port), with autocomplete + ghost text ([#57987](https://github.com/NousResearch/hermes-agent/pull/57987), [#58763](https://github.com/NousResearch/hermes-agent/pull/58763) — @teknium1)\\n- `--safe-mode` troubleshooting flag; uninstall dry-run; TLS failures fail fast with fix hints; `/compact` alias + preview flags; pip/Homebrew installs warned unsupported ([#45300](https://github.com/NousResearch/hermes-agent/pull/45300), [#60111](https://github.com/NousResearch/hermes-agent/pull/60111), [#57992](https://github.com/NousResearch/hermes-agent/pull/57992), [#57029](https://github.com/NousResearch/hermes-agent/pull/57029), [#57225](https://github.com/NousResearch/hermes-agent/pull/57225) — @teknium1, @ethernet8023)\\n- TUI: model picker refresh support; custom skill bundles dispatched as agent turns; banner sizes skills display to terminal width ([#59782](https://github.com/NousResearch/hermes-agent/pull/59782) — @helix4u, [#62859](https://github.com/NousResearch/hermes-agent/pull/62859) — @Adolanium, [#40624](https://github.com/NousResearch/hermes-agent/pull/40624) — @teknium1)\\n- Hermes Console REPL + perf follow-ups; `hermes curator usage` all-skills view; entry-point plugins surfaced in `hermes plugins list` ([#57781](https://github.com/NousResearch/hermes-agent/pull/57781) — @kshitijk4poor, [#36727](https://github.com/NousResearch/hermes-agent/pull/36727), [#40623](https://github.com/NousResearch/hermes-agent/pull/40623) — @teknium1)\\n\\n## 🔧 Tool System, Skills & MCP\\n- MCP: `mcp__server__tool` naming convention; server log notifications surfaced in agent.log; hosted OAuth completed across Dashboard + Desktop; configurable `redirect_uri`/`redirect_host` for proxied/WAF setups; OAuth callback port races closed; Blender added to the MCP catalog with a curated 4-tool default ([#52750](https://github.com/NousResearch/hermes-agent/pull/52750), [#57416](https://github.com/NousResearch/hermes-agent/pull/57416), [#66151](https://github.com/NousResearch/hermes-agent/pull/66151), [#65610](https://github.com/NousResearch/hermes-agent/pull/65610), [#65622](https://github.com/NousResearch/hermes-agent/pull/65622), [#64463](https://github.com/NousResearch/hermes-agent/pull/64463) — @teknium1, @benbarclay)\\n- Skills: `security/unbroker` (autonomous data-broker removal) + blind opt-out hardening; `unreal-mcp` companion skill; blender-mcp reworked around the catalog entry; humanizer pattern expansion; `mcp-oauth-remote-gateway` optional skill ([#57438](https://github.com/NousResearch/hermes-agent/pull/57438), [#57902](https://github.com/NousResearch/hermes-agent/pull/57902), [#65989](https://github.com/NousResearch/hermes-agent/pull/65989), [#64715](https://github.com/NousResearch/hermes-agent/pull/64715) — @SHL0MS, [#65066](https://github.com/NousResearch/hermes-agent/pull/65066), [#65486](https://github.com/NousResearch/hermes-agent/pull/65486) — @teknium1)\\n- Browser: full snapshots stored on truncation, eval denylist opt-in; computer_use follows cua-driver's verify→escalate ladder ([#65923](https://github.com/NousResearch/hermes-agent/pull/65923), [#67123](https://github.com/NousResearch/hermes-agent/pull/67123) — @teknium1)\\n- Kanban: modal create-task dialog + editable board project directory; Done-card results made obvious; grab-to-pan board scrolling; attachment toolset + CLI with SSRF-guarded URL fetch; project directory captured at board creation ([#66333](https://github.com/NousResearch/hermes-agent/pull/66333), [#63638](https://github.com/NousResearch/hermes-agent/pull/63638), [#60226](https://github.com/NousResearch/hermes-agent/pull/60226), [#65698](https://github.com/NousResearch/hermes-agent/pull/65698), [#63249](https://github.com/NousResearch/hermes-agent/pull/63249) — @teknium1)\\n- Cron: durable execution audit history; one-shot stale-removal race fixed; run-claim TTL derived from HERMES_CRON_TIMEOUT ([#61791](https://github.com/NousResearch/hermes-agent/pull/61791) — @teknium1, [#62014](https://github.com/NousResearch/hermes-agent/pull/62014) — @PRATHAMESH75, [#59567](https://github.com/NousResearch/hermes-agent/pull/59567))\\n- mem0: self-hosted dashboard backend + recall tuning + setup-wizard mode ([#56943](https://github.com/NousResearch/hermes-agent/pull/56943), [#60494](https://github.com/NousResearch/hermes-agent/pull/60494) — @kshitijk4poor, @teknium1)\\n- Image gen: Codex image inputs; unsupported Codex image accounts classified; tool args recursively normalized by schema (cline port) ([#57017](https://github.com/NousResearch/hermes-agent/pull/57017), [#63627](https://github.com/NousResearch/hermes-agent/pull/63627), [#52220](https://github.com/NousResearch/hermes-agent/pull/52220) — @teknium1, @kshitijk4poor)\\n\\n## 🔒 Security & Reliability\\n- Vertex: credential/project/region resolution through the profile secret scope; `VERTEX_CREDENTIALS_PATH`/`GOOGLE_APPLICATION_CREDENTIALS` stripped from subprocess env ([#56680](https://github.com/NousResearch/hermes-agent/pull/56680), [#56582](https://github.com/NousResearch/hermes-agent/pull/56582) — @srojk34)\\n- Six P1 hardening PRs salvaged in one pass — browser guards, MEDIA anchoring, .env lockdown, delegate ACP transport ([#57660](https://github.com/NousResearch/hermes-agent/pull/57660) — @teknium1)\\n- Media/vision/image-gen local-file reads routed through the shared credential-read guard; native image routing guarded by file-safety policy; unified image-source resolver + terminal-backend confinement ([#58709](https://github.com/NousResearch/hermes-agent/pull/58709), [#58752](https://github.com/NousResearch/hermes-agent/pull/58752), [#57890](https://github.com/NousResearch/hermes-agent/pull/57890) — @teknium1)\\n- Webhook body-cap sweep: explicit `client_max_size` on 3 uncapped aiohttp servers + completion sweep; Raft chunked-request body limit; timestamp-bound V2 webhook signatures ([#59180](https://github.com/NousResearch/hermes-agent/pull/59180), [#59215](https://github.com/NousResearch/hermes-agent/pull/59215), [#58902](https://github.com/NousResearch/hermes-agent/pull/58902), [#58508](https://github.com/NousResearch/hermes-agent/pull/58508) — @teknium1, @srojk34)\\n- Redaction: Fireworks token prefixes + Telegram transport errors; env-lookup false positives fixed for KEY=value and JSON/YAML config fields; bot tokens scrubbed from Telegram connect/send errors ([#58501](https://github.com/NousResearch/hermes-agent/pull/58501), [#58534](https://github.com/NousResearch/hermes-agent/pull/58534), [#58915](https://github.com/NousResearch/hermes-agent/pull/58915), [#58893](https://github.com/NousResearch/hermes-agent/pull/58893) — @teknium1)\\n- computer-use: subprocess env sanitized across all five cua-driver spawn sites ([#58889](https://github.com/NousResearch/hermes-agent/pull/58889), [#59165](https://github.com/NousResearch/hermes-agent/pull/59165) — @teknium1)\\n- Dashboard: managed-files credential guard widened past .env + dir-tree gap closed; OAuth token TOCTOU closed with atomic 0o600 writes; stale dashboards can't recreate deleted profiles ([#58222](https://github.com/NousResearch/hermes-agent/pull/58222) — @kshitijk4poor, [#60236](https://github.com/NousResearch/hermes-agent/pull/60236) — @teknium1, [#49435](https://github.com/NousResearch/hermes-agent/pull/49435) — @LeonSGP43)\\n- CI: untrusted refs passed through env, not `run:` interpolation; JS/TS tests wired into CI with source-regex tests banned; js-autofix pushes via PR instead of direct-to-main ([#57842](https://github.com/NousResearch/hermes-agent/pull/57842) — @jquesnelle, [#60707](https://github.com/NousResearch/hermes-agent/pull/60707), [#65186](https://github.com/NousResearch/hermes-agent/pull/65186) — @ethernet8023)\\n- Docker: terminal network toggle with full-path coverage; Git Bash Mandatory-ASLR install failures detected; Windows updater console hidden during handoff ([#59149](https://github.com/NousResearch/hermes-agent/pull/59149) — @teknium1, [#64651](https://github.com/NousResearch/hermes-agent/pull/64651), [#66040](https://github.com/NousResearch/hermes-agent/pull/66040) — @helix4u)\\n- Anthropic: request-local clients so the stale/interrupt watchdog never corrupts SQLite; per-profile OAuth file; OAuth login 429 fixed (UA must not be claude-code/) ([#67238](https://github.com/NousResearch/hermes-agent/pull/67238) — @OutThisLife, [#59339](https://github.com/NousResearch/hermes-agent/pull/59339), [#58178](https://github.com/NousResearch/hermes-agent/pull/58178) — @teknium1)\\n- Gateway/agent: tool_call_id deduplicated across pre-API sanitizers; background review inherits parent reasoning_config for Anthropic cache parity; `/new` memory extraction moved off the command path ([#58350](https://github.com/NousResearch/hermes-agent/pull/58350), [#64379](https://github.com/NousResearch/hermes-agent/pull/64379), [#61139](https://github.com/NousResearch/hermes-agent/pull/61139) — @teknium1, @kshitijk4poor)\\n\\n## 🔁 Reverted in this window (for the record)\\n- iron-proxy credential-injection egress firewall (#30179 → reverted in [#58489](https://github.com/NousResearch/hermes-agent/pull/58489)) — not shipping in this release\\n- dynamic-workflow orchestration skill (landed, then reverted) — not shipping\\n- memory provider-actions extension point (landed, then reverted) — not shipping\\n- Note: the plugin `pre_tool_call` approve escalation was reverted mid-window but **re-landed** in [#60504](https://github.com/NousResearch/hermes-agent/pull/60504) and ships in this release.\\n\\n## 👥 Contributors\\n\\n**450+ people** contributed to this release (via commits, co-author trailers, and salvaged PRs) — the biggest contributor window yet. Thank you, all of you.\\n\\n### Core team\\n- @teknium1 — release lead; TTFT perf wave, delivery + delegation durability, smart approvals, SecretSource, gateway multiplex + profile routing, sessions export, security round, and a ~290-PR community salvage burn\\n- @OutThisLife — desktop app (the speed wave, layout-tree shell, Capabilities page, session colors, vibe reactions, TUI incremental markdown, perf harness)\\n- @kshitijk4poor — GPT-5.6 end-to-end, DeepInfra + Upstage Solar providers, perf cluster, compression integrity, mem0, dashboard guards\\n- @ethernet8023 — CI overhaul (JS/TS tests wired in, autofix-via-PR, python speedups), desktop keybinds/worktrees/status indicators, full desktop TypeScript conversion\\n- @benbarclay — relay OIDC provisioning, gateway multiplex override, Nous auth self-heal, hosted MCP OAuth groundwork\\n- @alt-glitch — terminal billing (`/subscription`, `/topup`), desktop billing tab\\n- @helix4u — desktop provider/model UX, TUI model picker refresh, Windows install/updater hardening\\n- @austinpickett — desktop custom endpoint settings\\n- @SHL0MS — unbroker + unreal-mcp skills, humanizer expansion\\n\\n### Top community contributors\\n- @srojk34 — security hardening: Vertex credential/project/region scoping through the profile secret scope, subprocess env stripping, Raft chunked-request body limits\\n- @HexLab98 — 11 fixes across MCP capability gating, Windows installer PATH, desktop cron editing, gateway systemd warnings\\n- @UnathiCodex — desktop stability: zoom across display moves, LaTeX rendering, resume-stall and runtime-readiness fixes\\n- @xxxigm — `` leak fix after thinking-only retry flush, dashboard auth/theme/PTY fixes\\n- @erosika — desktop declarative memory-provider panel + honcho recall/timeout correctness\\n- @Frowtek — credential security: master stores never mounted into skill sandboxes, live-transcript redaction, dashboard api_key precedence\\n- @necoweb3 — browser private-page CDP guard, cron one-shot liveness, gateway compression fail-closed\\n- @DavidMetcalfe — desktop updater version pill, Local/custom endpoint exposure, sidebar collapse behavior\\n- @shannonsands — dashboard: mobile channel setup, Discord toolsets from web UI, Telegram setup clarity\\n- @vishal-dharm — Gemini request-context improvements\\n- @PRATHAMESH75 — cron one-shot stale-removal race, dashboard multiplex port-binding guard\\n- @alelpoan, @embwl0x, @Adolanium, @giggling-ginger, @Drexuxux, @frizikk, @JoaoMarcos44, @wesleysimplici, @LeonSGP43, @pierrenode, @simpolism, @MorAlekss, @r266-tech, @WadydX, @nv-kasikritc — targeted fixes across desktop, TUI, gateway, cron, webhook, nix, and browser surfaces\\n- Salvaged-work authors whose PRs were cherry-picked with credit this window: @Burgunthy (profile routing), @web3blind (sessions export), @hwrdprkns (1Password), @Christopher-Schulze, @Ahmett101, @sjiangtao2024, and many more — see the salvage PR bodies for full attribution\\n\\n### All contributors\\n\\n@0-CYBERDYNE-SYSTEMS-0, @0disoft, @0xbyt4, @100yenadmin, @17324393074, @2751738943, @8294, @abhibansal-sg,\\n@adambiggs, @Adolanium, @aeyeopsdev, @aguung, @AhmetArif0, @Ahmett101, @ai-ag2026, @AIalliAI, @ajzrva-sys,\\n@alastraz, @alelpoan, @alex-fireworks, @alex-heritier, @alex107ivanov, @AlexFucuson9, @Alix-007,\\n@allenliang2022, @Almurat123, @AlsayedHoota, @alt-glitch, @alvarosanchez, @amanning3390, @AmAzing129,\\n@AndreasHiltner, @andrewhomeyer, @annguyenNous, @ansel-f, @antydizajn, @arminanton, @arnispiekus, @asimons81,\\n@asscan, @ats3v, @austinlaw076, @austinpickett, @avifenesh, @aydnOktay, @Bartok9, @bautrey, @bbednarski9,\\n@bbopen, @benbarclay, @bigstar0920, @binhnt92, @bird, @Black0Fox0, @BlackishGreen33, @bo.fu, @brendandebeasi,\\n@briandevans, @BROCCOLO1D, @Bruce-anle, @brunz-me, @Burgunthy, @bytesnail, @catbearlove1-lang, @Cdddo,\\n@cgarwood82, @CharmingGroot, @chouqin, @Christopher-Schulze, @claudlos, @CocaKova, @Code-suphub, @CodeForgeNet,\\n@craigdfrench, @CrazyBoyM, @crazywriter1, @cresslank, @cruzanstx, @cyrkstudios, @danilofalcao,\\n@datachainsystems, @DatTheMaster, @davidb73-hub, @davidgut1982, @DavidMetcalfe, @davidrobertson,\\n@deacon-botdoctor, @DECK6, @deepujain, @derek2000139, @designnotdrum, @deusyu, @devatnull, @devorun,\\n@dexhunter, @dfein38347g, @Dhravya, @DictatorBacon, @digitalbase, @dlkakbs, @dmabry, @DNAlec, @dodo-reach,\\n@doncazper, @dorokuma, @doxe0x, @Drexuxux, @dschnurbusch, @Dusk1e, @EdderTalmor, @egilewski, @elashera,\\n@Elektrofussel, @eliteworkstation94-ai, @embwl0x, @emo-eth, @emozilla, @enzo-adami, @Epoxidex, @ErnestHysa,\\n@Erosika, @esthonjr, @ethernet8023, @evefromwayback, @evelynburger, @F4TB0Yz, @falkoro, @fanyangCS, @firefly,\\n@fjlaowan1983, @flewe, @flo1t, @flow-digital-ny, @floze-the-genius, @frizikk, @Frowtek, @FuryMartin,\\n@fyzanshaik, @gauravsaxena1997, @geoffreybutler94, @georgedrury, @gigakun3030, @giggling-ginger,\\n@Git-on-my-level, @gitcommit90, @githubespresso407, @gnodet, @GottZ, @Gridzilla, @grimmjoww578, @gumclaw,\\n@Gutslabs, @HaiderSultanArc, @harjothkhara, @heathley, @hejuntt1014, @helix4u, @HeLLGURD, @hellno,\\n@herbalizer404, @HexLab98, @hmirin, @Hopfensaft, @Hotragn, @hsy5571616, @huanshan5195, @HumphreySun98,\\n@hwrdprkns, @hydracoco7, @hydraxman, @iamlukethedev, @iborazzi, @IgorGanapolsky, @iizotov, @ildunari,\\n@infinitycrew39, @IpastorSan, @irresi, @isfttr, @isheng-eqi, @itsflownium, @izumi0uu, @Jaaneek, @JacketPants,\\n@jaisup, @jakelongvu-bot, @jakepresent, @jaketracey, @JAlmanzarMint, @JasonFang1993, @jbbottoms, @jcjc81,\\n@JiaDe-Wu, @Jiahui-Gu, @Jigoooo, @jingsong-liu, @jneeee, @JoaoMarcos44, @joelbrilliant, @John-Lussier, @jplew,\\n@jtstothard, @juniperbevensee, @Jupiter363, @justinschille, @k4z4n0v4, @kaishi00, @karfly, @kartik-mem0,\\n@kavioavio, @KCAYAAI, @kenyonxu, @keslerm, @kevinrajaram, @knoal, @kocaemre, @kohoj, @konsisumer, @krowd3v,\\n@kshitijk4poor, @kuangmi-bit, @kubolko, @kyssta-exe, @Kyzcreig, @l0h1nth, @labsobsidian, @laurinaitis,\\n@LavyaTandel, @lawyer112, @lemonwan, @LeonSGP43, @lEWFkRAD, @linfeng961, @liuhao1024, @liuwei666888, @ljy-2000,\\n@loes5050, @logical-and, @LoicHmh, @loongfay, @lord-dubious, @lost9999, @lucasfdale, @lucaskvasirr,\\n@luxuguang-leo, @ly-wang19, @m0n5t3r, @m1qaweb, @M1racleShih, @MaartenDMT, @mahdiwafy, @MaheshBhushan,\\n@ManniBr, @marcelohildebrand, @marcolivierlavoie, @markoub, @MarkVLK, @Marxb85, @matantsevs,\\n@maxpetrusenkoagent, @mbac, @mdc2122, @mguttmann, @Mibayy, @michaelHMK, @mijanx, @minchang, @momomojo,\\n@MorAlekss, @morluto, @msh01, @mssteuer, @mvanhorn, @nanami7777777, @nankingjing, @necoweb3, @neo-claw-bot,\\n@neoguyverx, @nicha16, @nikshepsvn, @nima20002000, @nnnet, @NousResearch, @nullptr0807, @nv-kasikritc,\\n@okisdev, @OmarB97, @ooiuuii, @ooovenenoso, @oppih, @Osraka, @ostravajih, @otsune, @OutThisLife, @OYLFLMH,\\n@patrick-muller, @pdmartins, @pedrommaiaa, @Peterskaronis, @petrichor-op, @pgregg88, @pierrenode, @pixel4039,\\n@plcunha, @pnascimento9596, @Polyhistor, @PRATHAMESH75, @professorpalmer, @Punyko8, @Que0x, @Qwinty,\\n@r0gersm1th, @r266-tech, @rabadaki, @ragingbulld, @RainbowAndSun, @rainbowgore, @randimt, @rarf, @rasitakyol,\\n@rayjun, @raymondyan-zhijie, @re-ITRT, @RenoMG, @Rival, @RKelln, @rlaehddus302, @rob-maron, @rodboev,\\n@roryford, @rungmc357, @ruslanvasylev, @s0xn1ck, @s905060, @s96919, @sahibzada-allahyar, @sahil-shubham,\\n@Sahil-SS9, @SahilRakhaiya05, @sam7894604, @SAMBAS123, @samrusani, @sanidhyasin, @sasquatch9818, @sberan,\\n@ScotterMonk, @seagpt, @sebastianlutycz, @SemonCat, @setclock, @shannonsands, @sharziki, @shashwatgokhe,\\n@SHL0MS, @shuangxinniao, @SilentKnight87, @simplast, @simpolism, @SiteupAgencia, @sjiangtao2024, @sk-holmes,\\n@slow4cyl, @smtony, @soddy022, @Soju06, @solyanviktor-star, @SongotenU, @spiky02plateau, @sprmn24, @SquabbyZ,\\n@srojk34, @ssiweifnag, @stantheman0128, @StellarisW, @stephenschoettler, @suninrain086, @superposition,\\n@Supersynergy, @sweetcornna, @szafranski, @tanmayxchoudhary, @tarunravi, @tcconnally, @terry197913, @Thatgfsj,\\n@thegoodguysla, @thestudionorth, @TheTom, @TinkerOfThings, @tjboudreaux, @tjp2021, @Tortugasaur, @Tosko4,\\n@Tranquil-Flow, @trevorgordon981, @trismegistus-wanderer, @tt-a1i, @tuancookiez-hub, @TurgutKural, @Umi4Life,\\n@UnathiCodex, @unsupportedpastels, @uzaylisak, @valda, @vampyren, @veradim, @victor-kyriazakos, @virtualex-itv,\\n@vishal-dharm, @Vissirexa, @vizi0uz, @vkkong, @vKongv, @VolodymyrBg, @vortexopenclaw, @VrtxOmega, @WadydX,\\n@waroffchange, @waseemshahwan, @web3blind, @webtecnica, @wesleion, @wesleysimplicio, @williamumu,\\n@WilsonKinyua, @wxy-nlp, @wyuebei-cloud, @x7peeps, @x9x9x9x9x9x91, @xuezhaolan, @xxxigm, @ya-nsh, @yatesjalex,\\n@ygd58, @yingliang-zhang, @yinkev, @YLChen-007, @yu-xin-c, @yungchentang, @zapabob, @zccyman, @zeapsu,\\n@ziliangpeng, @zwcf5200, @zzpigpinggai\\n\\nAlso: bo.fu, Paulo Henrique, kyssta-exe 25470058+kyssta-exe.fu, Paulo Henrique, kyssta-exe 25470058+kyssta-exe.\\n\\n---\\n\\n**Full Changelog**: [v2026.7.1...v2026.7.20](https://github.com/NousResearch/hermes-agent/compare/v2026.7.1...v2026.7.20)\\n\", \"published_at\": \"2026-07-20T18:35:55Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.20\"}, {\"id\": 5, \"project_id\": 1, \"tag_name\": \"v2026.7.7.2\", \"name\": \"Hermes Agent v0.18.2 (2026.7.7.2)\", \"body\": \"# Hermes Agent v0.18.2 (v2026.7.7.2)\\n\\n**Release Date:** July 7, 2026\\n\\n> Same-day patch on top of v0.18.1, picking up the WhatsApp Baileys dependency fix needed for tagged-release Docker builds.\\n\\n---\\n\\n## What's in this patch\\n\\n- **fix(whatsapp): unpin Baileys from git commit, use published 7.0.0-rc13** ([#60643](https://github.com/NousResearch/hermes-agent/pull/60643)) — the WhatsApp bridge dependency now installs from the published npm release instead of a pinned git commit, making installs and Docker image builds reliable.\\n\\nFull curated release notes for the entire post-v0.18.0 window ship with v0.19.0.\\n\\n## Updating\\n\\n```bash\\nhermes update        # existing installs\\npip install -U hermes-agent\\n```\\n\\n**Full Changelog**: [v2026.7.7...v2026.7.7.2](https://github.com/NousResearch/hermes-agent/compare/v2026.7.7...v2026.7.7.2)\\n\", \"published_at\": \"2026-07-08T03:11:22Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.7.2\"}], \"issues\": [{\"id\": 1, \"project_id\": 1, \"title\": \"Desktop auto-update can wipe the desktop build on Windows (venv lock ignored on retry → hermes.exe quarantine downgraded → ZIP fallback deletes win-unpacked)\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:17:32Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/issues/87331\", \"user\": \"yuliangop\"}, {\"id\": 2, \"project_id\": 1, \"title\": \"hermes mcp login : OAuth callback port collision makes interactive login impossible on a headless host (v0.20.1) — #5344 regressed\", \"state\": \"open\", \"created_at\": \"2026-08-16T00:10:39Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/issues/87329\", \"user\": \"wlg949\"}, {\"id\": 3, \"project_id\": 1, \"title\": \"[Bug]: Install scripts gets stuck on Playwrite on Arch\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:47:33Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/issues/87320\", \"user\": \"offgridcrazy\"}, {\"id\": 4, \"project_id\": 1, \"title\": \"[Bug]: transform_llm_output callbacks do not have a guaranteed sequential safety pipeline\", \"state\": \"open\", \"created_at\": \"2026-08-15T23:46:00Z\", \"html_url\": \"https://github.com/NousResearch/hermes-agent/issues/87319\", \"user\": \"0xble\"}], \"stats\": {}}\n\n用户问题: ping\n\n要求: 按以下4步展示思考过程: 1.数据观察 2.初步判断 3.深入推理 4.结论"}
+
diff --git a/.sisyphus-brief-b2b3.md b/.sisyphus-brief-b2b3.md
new file mode 100644
index 0000000..f781335
--- /dev/null
+++ b/.sisyphus-brief-b2b3.md
@@ -0,0 +1,52 @@
+# 任务: blog-app AI Agent 升级迭代日报 v2 — B2 (LLM 集成) + B3 (Flask /watchdog + cron)
+
+你在 /home/yi/opencode-blog-showcase/blog-app 工作。B1 已完成: scripts/daily_watchdog.py (抓取 6 个 AI agent 项目的 GitHub Releases/commits/npm/releases.atom/Exa web search, 渲染 markdown 报告, 存 SQLite ~/opencode-blog-showcase/data/watchdog.db + 写 ~/opencode-blog-showcase/logs/daily-agent-watchdog-YYYY-MM-DD.md)。现在做 B2 和 B3。
+
+## B2: LLM 集成 — 只改 scripts/daily_watchdog.py 里的 summarize_with_llm 函数体
+
+不要重写整个文件! 只替换 summarize_with_llm 的函数体 (文件顶部有 LLM-INTEGRATION-POINT 注释)。
+
+要求:
+- POST http://127.0.0.1:4000/v1/chat/completions, model="analysis", 无 auth, timeout=300
+- 用 requests (已有 import), 不加任何新 pip 依赖
+- prompt 中文, 输入是 collected_data dict (github_release/github_commits/npm/atom/exa_results), 先截断: release body ≤500 字符, 其余字段各 ≤300 字符, 总 prompt 数据部分 <12KB
+- 让 LLM 返回 JSON: {"summary_zh": "不少于50字的中文摘要,2-3句", "breaking": true/false, "breaking_reason": "...", "relevance_note": "对本地已装用户的意义,1-2句"}
+- 健壮解析: 从响应 content 中提取 JSON (容忍 ```json 围栏 / 前后杂文本, 用 regex 找第一个 { 到最后一个 })
+- LLM 调用失败或解析失败时不崩管道: log.error 并返回 {"summary_zh": "(LLM 分析失败: <原因>)", "breaking": None, "breaking_reason": "LLM 分析失败", "relevance_note": ""}
+- 保持函数签名 summarize_with_llm(project_key: str, collected_data: dict) -> dict 不变
+
+## B3: Flask /watchdog 路由 + 模板 + cron
+
+1. app.py 加路由 (风格跟现有路由一致, 看 app.py 里现有 page_* 函数):
+   - GET /watchdog: 从 ~/opencode-blog-showcase/data/watchdog.db 的 reports 表 (date/markdown/meta_json/created_at) 查最近 7 天, 渲染 templates/watchdog.html。若 db 不存在或无记录, 显示空态提示 + 手动触发按钮, 不要 500
+   - POST /watchdog/run: subprocess 后台触发 /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py (env 里 HOME=/home/yi, stdout 追加到 ~/opencode-blog-showcase/logs/daily-watchdog-run.log), 然后 redirect 回 /watchdog 并带 "已触发" 提示。防止重复触发: 若已有 daily_watchdog.py 进程在跑则直接提示
+2. templates/watchdog.html: 继承 base.html (看现有模板怎么继承), 列出最近 7 天报告。markdown 渲染: 无新 pip 依赖, 用 
 保留原格式或写个极简的标题/加粗/链接转换都行, 优先简单可靠。含 "立即运行" 按钮 (POST /watchdog/run)
+3. base.html 导航加 "Watchdog" 链接 (看现有导航项风格)
+4. cron: 用 crontab 加一行 (本机时区 Asia/Shanghai): 每天 9:00 跑
+   0 9 * * * /usr/bin/python3 /home/yi/opencode-blog-showcase/blog-app/scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-watchdog-cron.log 2>&1
+   注意: 追加到现有 crontab, 绝不可覆盖已有条目 (先 crontab -l 备份到 workspace ./.review-tmp/crontab.bak)
+
+## 验证 (必须真跑, 输出贴进总结)
+
+1. export HOME=/home/yi && /usr/bin/python3 scripts/daily_watchdog.py — 完整跑一遍 (Exa + 6 项目 + LLM 分析, 可能要几分钟, 耐心等), 退出码 0
+2. 检查生成的 ~/opencode-blog-showcase/logs/daily-agent-watchdog-2026-08-16.md: 每个 ★ 项目有真实中文摘要 (不是 "(待 LLM 集成)"), Breaking Changes 有实质内容
+3. bash scripts/deploy.sh 重启 Flask, 然后:
+   - curl -sS http://127.0.0.1:8090/watchdog | head -50 (含报告内容)
+   - curl -sS -X POST http://127.0.0.1:8090/watchdog/run (触发逻辑正常, 重复触发被拒)
+   - curl -sS http://192.168.2.25:8090/watchdog (LAN 可访问)
+4. crontab -l 确认新条目存在且旧条目完好
+5. 回归: curl 旧页面 / /projects /analyze 确认没破
+
+## 硬约束 (越界 = 失败)
+
+- 禁改 litellm 配置 / 禁 restart litellm-proxy / llama-server
+- 禁改 projects.db schema, 禁改 scripts/fetch_data.py
+- 禁重写 daily_watchdog.py 的抓取/渲染部分 — 只动 summarize_with_llm 函数体
+- 无新 pip 依赖 (Flask + requests + sqlite3 + 标准库)
+- 禁改 ~/.omo/omo.jsonc, 禁碰 ~/.local/lib/node_modules/oh-my-openagent/
+- 临时文件一律放 workspace 内 ./.review-tmp/ (你没有 /tmp 写权限)
+- 不要 mock 数据, 不要预生成报告内容
+
+## 输出
+
+最后总结: B2 改了什么 / B3 加了什么 / 每条验证命令的真实输出 (截关键行) / 遇到的问题。
diff --git a/.sisyphus-brief-ui-hci-v1.md b/.sisyphus-brief-ui-hci-v1.md
new file mode 100644
index 0000000..a4db228
--- /dev/null
+++ b/.sisyphus-brief-ui-hci-v1.md
@@ -0,0 +1,76 @@
+# Goal
+站在 UI 人机交互角度, 深度分析 ~/opencode-blog-showcase/blog-app/ 的 /watchdog 页面 (templates/watchdog.html) 怎么改最合适.
+
+# 背景
+今天 omo 派 build agent 实现了 watchdog 功能, omo 已经分析出 6 个 UX 问题 (在 /home/yi/.omo/analyze-watchdog-ui-v4-2026-08-16.md). 现在让你从 UI 人机交互角度再深挖一层, 看看前端要怎么改合适.
+
+# 你 (omo Sisyphus) 的工作
+你是 UI/UX 设计师, 实际去看 watchdog 页面 + 报告内容 + 现有 blog-app 的 UI 风格 (templates/base.html + templates/index.html + templates/project_detail.html + templates/analyze.html + templates/watchdog.html + static/css/style.css), 站在用户视角想:
+
+## 评估维度 (你自主决定, 我不预设)
+1. **认知负荷** — 用户进 /watchdog 第一眼看到啥? 要点几次才能找到关心的信息? 信息密度是不是太高/太低?
+2. **交互完整性** — 按钮 / 表单 / 链接是不是真能用? 有没有死按钮 / 不可点的链接 / 误导用户的 UI 元素?
+3. **视觉层级** — 标题 / 重要信息 / 次要信息的视觉权重是不是合理? ★ 项目 vs 普通项目怎么区分? breaking change vs 普通更新怎么区分?
+4. **一致性** — watchdog 页面跟 blog-app 其他页面 (首页 / 项目详情 / 分析页) 的 UI 风格是否一致? 颜色 / 字体 / 卡片 / badge 等?
+5. **反馈与可恢复性** — 用户的每个操作有没有反馈 (loading / success / error)? 出错了能不能重试? 数据有没有备份?
+6. **可达性 (a11y)** — 鼠标 / 键盘 / 屏幕阅读器都能用吗? aria-label, focus, contrast 都行吗?
+7. **响应式** — 移动端 / 平板 / 大屏 都能正常显示吗? 表格 / 长内容在窄屏怎么处理?
+8. **状态表达** — 数据空 / 加载中 / 错误 / 成功 这 4 种状态分别在 UI 上怎么表达?
+9. **可发现性** — 用户怎么知道 watchdog 存在? 怎么知道数据更新? 怎么知道能看历史?
+10. **信任与透明度** — 数据从哪来? 抓取时间? 失败原因? 用户能不能信这报告?
+
+## 输入文件 (你必读, 用 Read 工具)
+- /home/yi/opencode-blog-showcase/blog-app/templates/watchdog.html (主要改这个)
+- /home/yi/opencode-blog-showcase/blog-app/templates/base.html (布局 + 导航)
+- /home/yi/opencode-blog-showcase/blog-app/templates/index.html (首页风格)
+- /home/yi/opencode-blog-showcase/blog-app/templates/project_detail.html (项目详情 — 看 markdown 渲染方式)
+- /home/yi/opencode-blog-showcase/blog-app/templates/analyze.html (分析页 — 看 SSE 流式 UI)
+- /home/yi/opencode-blog-showcase/blog-app/static/css/style.css (全局样式)
+- /home/yi/opencode-blog-showcase/blog-app/app.py (看 /watchdog + /watchdog/run 路由逻辑)
+- /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog-2026-08-16.md (实际报告长啥样)
+- /home/yi/.omo/analyze-watchdog-ui-v4-2026-08-16.md (你自己之前的报告, 看怎么深化)
+- /home/yi/opencode-blog-showcase/data/watchdog.db (db schema, 用 sqlite3 只读查)
+
+## 输出 (你必须给)
+1. **报告路径**: /home/yi/.omo/ui-hci-analysis-watchdog-V1-2026-08-16.md (新文件, 别覆盖 V4)
+2. **报告内容**:
+   - "整体评价" 段: watchdog 前端在 UI/HCI 维度的整体判断 (1-2 段, 真判断)
+   - "维度评估" 段: 上面 10 个维度每个的评估 (每个维度说现状 + 问题 + 建议)
+   - "重构方案" 段: 给出**完整**的前端重构方案 (不是分散 6 个修, 是整体改 UI 的蓝图)
+     - 整体视觉层级 (卡片 / 列表 / 折叠 / tab 怎么组合)
+     - 颜色 / 字体 / 间距系统 (用现有 CSS 变量, 不要新引)
+     - 关键交互细节 (按钮 / 加载状态 / 错误处理 / 反馈消息)
+     - mockup / ASCII art (描述每个核心视图长啥样)
+     - 涉及文件 + 改法 + 行号 + 改前后代码
+   - "实施顺序" 段: 这次重构分几个 PR / 几个 task, 顺序怎样
+   - "跟 V4 报告的关系" 段: V4 报告 6 个问题在这次重构里怎么解决 (避免重复, 整合到新方案)
+
+## Process (硬约束, 跟 SKILL.md §3 一致)
+- 你 (omo Sisyphus) 内部用 Atlas Kimi K2.6 calibration 工作流:
+  - plan (1 read + 1 dep map)
+  - 派 sub-agent (Oracle 看代码, Librarian 查 UI 设计模式, Explore 找现有 CSS 类, Multimodal Looker 看截图)
+  - 自己写报告 (Sisyphus 写)
+- 不动代码 (这次是 analysis 任务, 跟 V4 一样)
+- max-runtime 15 分钟
+
+## 拍权前置边界 (跟 V4 一致)
+- 禁改 litellm / restart litellm-proxy / llama-server / K3s / opencode serve
+- 禁改 projects.db schema / fetch_data.py / 已有 watchdog.db 数据
+- 禁改 ~/.omo/omo.jsonc / ~/.local/lib/node_modules/oh-my-openagent/
+- **不动任何代码** (这次纯分析)
+
+## False completion guards
+- ❌ 不要写代码 (patch / edit / write 工具不该用, 只用 read / grep / search; 唯一例外是写分析报告文件本身)
+- ❌ 不要按 brief 里"维度评估"的顺序抄 — 你自己判断哪些维度最重要
+- ❌ 不要只复制 V4 报告 — 这是新角度 (UI/HCI), 要深化
+- ❌ 不要 deploy.sh 重启 blog-app
+- ❌ 不要创建新文件 (除了分析报告)
+
+## Acceptance (worker 会逐条自验, 5 项)
+1. **报告存在**: /home/yi/.omo/ui-hci-analysis-watchdog-V1-2026-08-16.md
+2. **不动代码真凭据**: 5 个交付物 (daily_watchdog.py / install_watchdog_cron.sh / app.py / watchdog.html / base.html) 改动时间没变
+3. **10 个维度全覆盖**: 不是只挑 2-3 个, 是**全**评估 (即使某维度"没问题"也要写一句"评估过, 没问题")
+4. **重构方案完整**: 给出整体蓝图 (不是分散 6 修, 是整体改 UI 的方案), 含 mockup / 改法 / 行号
+5. **跟 V4 报告整合**: 说明 V4 6 问题在这次重构里怎么解决, 不重复劳动
+
+临时文件如需落盘, 放 workspace 内 ./.analysis-tmp/ (不要写 /tmp).
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..61581fd
--- /dev/null
+++ b/app.py
@@ -0,0 +1,85 @@
+"""Flask backend for opencode-blog-showcase.
+
+Application factory only — routes live in routes/, business logic in
+services/, queries in repositories/. See config.py for env overrides.
+"""
+
+import logging
+import os
+import uuid
+
+from flask import Flask, g, has_request_context, request
+
+import config
+import security
+from extensions import close_db
+from routes import api_bp, pages_bp, watchdog_bp
+
+
+class _RequestIdFilter(logging.Filter):
+    """Inject `request_id` into every LogRecord so the formatter can print it.
+
+    Reads Flask `g.request_id` when a request context is active; falls back
+    to "-" for log lines emitted outside a request (startup, daemon threads).
+    """
+
+    def filter(self, record):
+        if has_request_context():
+            record.request_id = getattr(g, "request_id", "-")
+        else:
+            record.request_id = "-"
+        return True
+
+
+_LOG_FORMAT = "%(asctime)s [%(process)d] [%(request_id)s] %(levelname)s: %(message)s"
+
+_handler = logging.StreamHandler()
+_handler.setFormatter(logging.Formatter(_LOG_FORMAT))
+_handler.addFilter(_RequestIdFilter())
+
+_root = logging.getLogger()
+_root.setLevel(logging.INFO)
+if not any(isinstance(h, logging.StreamHandler) and h.formatter for h in _root.handlers):
+    _root.addHandler(_handler)
+
+logger = logging.getLogger(__name__)
+
+
+def create_app():
+    app = Flask(
+        __name__,
+        template_folder=os.path.join(config.BASE_DIR, "templates"),
+        static_folder=os.path.join(config.BASE_DIR, "static"),
+    )
+    app.secret_key = config.SECRET_KEY
+
+    @app.before_request
+    def _assign_request_id():
+        g.request_id = uuid.uuid4().hex[:8]
+
+    @app.after_request
+    def _log_request(response):
+        logger.info(
+            "%s %s -> %s (%s)",
+            request.method,
+            request.path,
+            response.status_code,
+            g.request_id,
+        )
+        return response
+
+    app.teardown_appcontext(close_db)
+    security.init_app(app)
+
+    app.register_blueprint(api_bp)
+    app.register_blueprint(pages_bp)
+    app.register_blueprint(watchdog_bp)
+
+    return app
+
+
+app = create_app()
+
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=8090, debug=False)
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..fda248c
--- /dev/null
+++ b/config.py
@@ -0,0 +1,46 @@
+"""Central configuration for blog-app.
+
+All values can be overridden via environment variables (BLOG_APP_* prefix,
+except SECRET_KEY which uses the conventional FLASK_SECRET_KEY).
+"""
+
+import os
+from pathlib import Path
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+# NOTE: actual data lives at ~/opencode-blog-showcase/data (outside blog-app/).
+# The default below mirrors the pre-refactor behavior.
+DB_PATH = os.environ.get(
+    "BLOG_APP_DB_PATH",
+    os.path.expanduser("~/opencode-blog-showcase/data/projects.db"),
+)
+WATCHDOG_DB = os.environ.get(
+    "BLOG_APP_WATCHDOG_DB",
+    os.path.expanduser("~/opencode-blog-showcase/data/watchdog.db"),
+)
+
+WATCHDOG_SCRIPT = os.path.join(BASE_DIR, "scripts", "daily_watchdog.py")
+WATCHDOG_RUN_LOG = os.path.expanduser(
+    "~/opencode-blog-showcase/logs/daily-watchdog-run.log"
+)
+
+LLM_URL = os.environ.get("BLOG_APP_LLM_URL", "http://127.0.0.1:4000/v1")
+LLM_MODEL = os.environ.get("BLOG_APP_LLM_MODEL", "default")
+LLM_TIMEOUT = int(os.environ.get("BLOG_APP_LLM_TIMEOUT", "300"))
+
+# LLM prompt trimming budgets, in characters (OPT-10). Shared by
+# services/analysis.py (_slim_for_llm) and scripts/daily_watchdog.py.
+# LLM_TOTAL_LIMIT: 12000 chars ≈ 3000 tokens, 留余量给 system prompt +
+# user question (典型 4k-8k context 模型). 单字段阈值控制 prefill 体积.
+LLM_BODY_LIMIT = int(os.environ.get("BLOG_APP_LLM_BODY_LIMIT", "500"))
+LLM_FIELD_LIMIT = int(os.environ.get("BLOG_APP_LLM_FIELD_LIMIT", "300"))
+LLM_MESSAGE_LIMIT = int(os.environ.get("BLOG_APP_LLM_MESSAGE_LIMIT", "200"))
+LLM_TOTAL_LIMIT = int(os.environ.get("BLOG_APP_LLM_TOTAL_LIMIT", "12000"))
+
+RATE_LIMIT_MAX = int(os.environ.get("BLOG_APP_RATE_LIMIT_MAX", "3"))
+RATE_LIMIT_WINDOW = int(os.environ.get("BLOG_APP_RATE_LIMIT_WINDOW", "60"))
+
+WATCHDOG_RUN_COOLDOWN = int(os.environ.get("BLOG_APP_WATCHDOG_COOLDOWN", "60"))
+
+SECRET_KEY = os.environ.get("FLASK_SECRET_KEY", "dev-watchdog-secret-change-me")
diff --git a/docs/escape-html-behavior.test.mjs b/docs/escape-html-behavior.test.mjs
new file mode 100644
index 0000000..52c92bd
--- /dev/null
+++ b/docs/escape-html-behavior.test.mjs
@@ -0,0 +1,70 @@
+/**
+ * OPT-6 行为对齐测试: 对比 ai-panel.js 原 escapeHtml (div.textContent + innerHTML)
+ * 与 utils.js 主实现 (replace 链).
+ *
+ * 说明: 原实现依赖浏览器 DOM, Node 无法直接调用, 这里用最小 DOM stub
+ * 复刻 textContent -> innerHTML 的转义语义 (只转义 & < >, 不转义引号).
+ * utils.js 实现是其严格超集 (额外转义 " '), 因此逐字符对比断言:
+ * 对同一输入, utils 输出 反解实体后 必须与 stub 输出 反解实体后 一致,
+ * 且 utils 输出本身满足 5 个字符的转义期望.
+ *
+ * 运行: node --test docs/escape-html-behavior.test.mjs
+ */
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+/* utils.js 模块顶层有 document.addEventListener 副作用, Node 下需 stub */
+globalThis.document = { addEventListener() {} };
+
+const { escapeHtml } = await import('../static/js/utils.js');
+
+/* 复刻 ai-panel.js 原实现的最小 DOM stub */
+function legacyEscapeHtml(text) {
+    const div = {
+        _text: '',
+        set textContent(v) { this._text = String(v); },
+        get innerHTML() {
+            return this._text
+                .replace(/&/g, '&')
+                .replace(//g, '>');
+        },
+    };
+    div.textContent = text == null ? '' : String(text);
+    return div.innerHTML;
+}
+
+/* 反解 HTML 实体, 用于跨实现等价比较 */
+function unescape(s) {
+    return s
+        .replace(/</g, '<')
+        .replace(/>/g, '>')
+        .replace(/"/g, '"')
+        .replace(/'|'/g, "'")
+        .replace(/&/g, '&');
+}
+
+const cases = [
+    ['', '<script>alert(1)</script>'],
+    ['a & b', 'a & b'],
+    ['"quote"', '"quote"'],
+    ["'apos'", ''apos''],
+    ['', '<img onerror="x">'],
+];
+
+for (const [input, expected] of cases) {
+    test(`utils.js escapeHtml: ${JSON.stringify(input)}`, () => {
+        assert.equal(escapeHtml(input), expected);
+    });
+
+    test(`等价性 (utils ⊇ legacy): ${JSON.stringify(input)}`, () => {
+        const legacy = legacyEscapeHtml(input);
+        const unified = escapeHtml(input);
+        assert.equal(unescape(unified), unescape(legacy));
+    });
+}
+
+test('null/undefined 输入行为一致', () => {
+    assert.equal(escapeHtml(null), legacyEscapeHtml(null));
+    assert.equal(escapeHtml(undefined), legacyEscapeHtml(undefined));
+});
diff --git a/docs/escapeHtml-parity-test.md b/docs/escapeHtml-parity-test.md
new file mode 100644
index 0000000..426e1f1
--- /dev/null
+++ b/docs/escapeHtml-parity-test.md
@@ -0,0 +1,39 @@
+# escapeHtml parity test (OPT-6 收编验证)
+
+utils.js:38 是 blog-app 唯一一份 escapeHtml 实现.
+ai-panel.js 已通过 `import { escapeHtml } from './utils.js'` (line 9) 复用.
+无任何本地副本, 无 div.textContent-based 实现.
+
+## 字符集
+
+utils.js:38 escape 5 个字符: `& < > " '`
+- `&` → `&`
+- `<` → `<`
+- `>` → `>`
+- `"` → `"`
+- `'` → `'` (注: 任务 spec 写的是 `'`, 但 utils.js 用十进制 `'`, 语义等价 — 都是单引号的 HTML entity)
+
+## 测试用例 (行为对齐)
+
+| 输入 | 期望输出 | utils.js 输出 | 一致? |
+|---|---|---|---|
+| `` | `<script>alert(1)</script>` | 同 | ✓ |
+| `a & b` | `a & b` | 同 | ✓ |
+| `"quote"` | `"quote"` | 同 | ✓ |
+| `'apos'` | `'apos'` | 同 (等价于 `'`) | ✓ |
+| `` | `<img onerror="x">` | 同 | ✓ |
+
+## 验证命令
+
+    grep -nE "function escapeHtml|const escapeHtml" static/js/ai-panel.js
+    # 预期: 无输出 (本地实现已不存在)
+
+    grep -nE "^import.*escapeHtml" static/js/ai-panel.js
+    # 预期: 9:import { escapeHtml } from './utils.js';
+
+    grep -rnE "function escapeHtml|const escapeHtml" static/js/
+    # 预期: 仅 utils.js:38 一处
+
+## 状态
+
+OPT-6 收编完成 — ai-panel.js 第三份本地实现已不存在.
diff --git a/extensions.py b/extensions.py
new file mode 100644
index 0000000..6bf549e
--- /dev/null
+++ b/extensions.py
@@ -0,0 +1,43 @@
+"""Flask-bound extensions: per-request DB connections and teardown.
+
+These helpers depend on Flask's `g` and must only be used inside an
+application/request context. Pure (Flask-free) helpers live in
+services/ and repositories/.
+"""
+
+import logging
+import sqlite3
+
+from flask import g
+
+import config
+
+logger = logging.getLogger(__name__)
+
+
+def get_db():
+    """Return a per-request SQLite connection (row access by name)."""
+    if "db" not in g:
+        conn = sqlite3.connect(config.DB_PATH)
+        conn.row_factory = sqlite3.Row
+        g.db = conn
+    return g.db
+
+
+def get_watchdog_db():
+    """Return a per-request SQLite connection to the watchdog db (read-only)."""
+    if "watchdog_db" not in g:
+        conn = sqlite3.connect(f"file:{config.WATCHDOG_DB}?mode=ro", uri=True)
+        conn.row_factory = sqlite3.Row
+        g.watchdog_db = conn
+    return g.watchdog_db
+
+
+def close_db(exception=None):
+    """Teardown handler registered via app.teardown_appcontext."""
+    conn = g.pop("db", None)
+    if conn is not None:
+        conn.close()
+    wconn = g.pop("watchdog_db", None)
+    if wconn is not None:
+        wconn.close()
diff --git a/gunicorn.conf.py b/gunicorn.conf.py
new file mode 100644
index 0000000..7eb78dd
--- /dev/null
+++ b/gunicorn.conf.py
@@ -0,0 +1,15 @@
+"""Gunicorn configuration for blog-app evaluation.
+
+Stage 3 evaluation only — not committed to deploy.sh unless go decision is made.
+"""
+
+bind = "0.0.0.0:8090"
+workers = 2
+worker_class = "gthread"
+threads = 4
+timeout = 300            # LLM streaming needs long timeout
+graceful_timeout = 30
+keepalive = 5
+accesslog = "-"
+errorlog = "-"
+loglevel = "info"
diff --git a/models.py b/models.py
new file mode 100644
index 0000000..388778d
--- /dev/null
+++ b/models.py
@@ -0,0 +1,51 @@
+"""Data models (dataclasses) for blog-app.
+
+These are thin containers mirroring the SQLite schema. The current code
+passes plain dicts (from sqlite3.Row) across layers; these dataclasses
+document the shapes and are available for typed consumers (e.g.
+daily_watchdog.py) without forcing a full migration of the routes.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional
+
+
+@dataclass
+class Project:
+    id: int
+    name: str
+    full_name: Optional[str] = None
+    description: Optional[str] = None
+    stars: int = 0
+    forks: int = 0
+    language: Optional[str] = None
+    updated_at: Optional[str] = None
+
+
+@dataclass
+class Release:
+    id: int
+    project_id: int
+    tag_name: Optional[str] = None
+    name: Optional[str] = None
+    body: Optional[str] = None
+    published_at: Optional[str] = None
+
+
+@dataclass
+class Issue:
+    id: int
+    project_id: int
+    title: Optional[str] = None
+    state: Optional[str] = None
+    body: Optional[str] = None
+    user: Optional[str] = None
+    created_at: Optional[str] = None
+
+
+@dataclass
+class WatchdogReport:
+    date: str
+    markdown: str
+    meta: dict = field(default_factory=dict)
+    created_at: Optional[str] = None
diff --git a/repositories/__init__.py b/repositories/__init__.py
new file mode 100644
index 0000000..0a4ee85
--- /dev/null
+++ b/repositories/__init__.py
@@ -0,0 +1,3 @@
+from . import project_repo, release_repo, issue_repo, watchdog_repo
+
+__all__ = ["project_repo", "release_repo", "issue_repo", "watchdog_repo"]
diff --git a/repositories/issue_repo.py b/repositories/issue_repo.py
new file mode 100644
index 0000000..733886c
--- /dev/null
+++ b/repositories/issue_repo.py
@@ -0,0 +1,46 @@
+"""Issue queries. No Flask imports; take an explicit sqlite3 connection.
+
+Queries filter on `issues.project_id`, which is backed by the
+`idx_issues_project_id` index (Stage 4.2; docs/analysis/optional-indexes.sql).
+"""
+
+ISSUE_COLUMNS = "id, project_id, title, state, created_at, html_url, user"
+
+
+def _rows(cur):
+    return [dict(row) for row in cur.fetchall()]
+
+
+def list_recent_issues(db, limit=10):
+    return _rows(
+        db.execute(
+            f"SELECT {ISSUE_COLUMNS} FROM issues "
+            "ORDER BY created_at DESC LIMIT ?",
+            (limit,),
+        )
+    )
+
+
+def list_issues_for_project(db, project_id, limit=None):
+    sql = (
+        f"SELECT {ISSUE_COLUMNS} FROM issues "
+        "WHERE project_id = ? ORDER BY created_at DESC"
+    )
+    params = (project_id,)
+    if limit is not None:
+        sql += " LIMIT ?"
+        params = (project_id, limit)
+    return _rows(db.execute(sql, params))
+
+
+def search_issues(db, keyword, limit=None):
+    like = f"%{keyword}%"
+    sql = (
+        f"SELECT {ISSUE_COLUMNS} FROM issues "
+        "WHERE title LIKE ? ORDER BY created_at DESC"
+    )
+    params = (like,)
+    if limit is not None:
+        sql += " LIMIT ?"
+        params = (like, limit)
+    return _rows(db.execute(sql, params))
diff --git a/repositories/project_repo.py b/repositories/project_repo.py
new file mode 100644
index 0000000..c32b2fc
--- /dev/null
+++ b/repositories/project_repo.py
@@ -0,0 +1,80 @@
+"""Project-related queries.
+
+Organized by business scenario (project browsing / search / stats), not
+one-repo-per-table. Functions take an explicit sqlite3 connection with
+row_factory=sqlite3.Row and return plain dicts. No Flask imports.
+"""
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+PROJECT_COLUMNS = (
+    "id, name, full_name, description, stars, forks, language, html_url, "
+    "created_at, updated_at, source"
+)
+
+
+def _rows(cur):
+    return [dict(row) for row in cur.fetchall()]
+
+
+def list_projects(db):
+    """All projects ordered by stars desc."""
+    return _rows(
+        db.execute(f"SELECT {PROJECT_COLUMNS} FROM projects ORDER BY stars DESC")
+    )
+
+
+def get_project(db, project_id):
+    """Single project row as dict, or None."""
+    row = db.execute(
+        f"SELECT {PROJECT_COLUMNS} FROM projects WHERE id = ?", (project_id,)
+    ).fetchone()
+    return dict(row) if row is not None else None
+
+
+# Backward-compatible alias for the pre-refactor name.
+get_all_projects = list_projects
+
+
+def list_project_summaries(db):
+    """Slim project list (id/name/full_name/stars/forks/language) for selectors."""
+    return _rows(
+        db.execute(
+            "SELECT id, name, full_name, stars, forks, language "
+            "FROM projects ORDER BY stars DESC"
+        )
+    )
+
+
+def search_projects(db, keyword):
+    like = f"%{keyword}%"
+    return _rows(
+        db.execute(
+            f"SELECT {PROJECT_COLUMNS} FROM projects WHERE name LIKE ? OR full_name LIKE ? "
+            "ORDER BY stars DESC",
+            (like, like),
+        )
+    )
+
+
+def get_starred_names(db):
+    """Return the set of project names flagged `starred = 1` (Stage 4.1)."""
+    cur = db.execute("SELECT name FROM projects WHERE starred = 1")
+    return {row["name"] for row in cur.fetchall()}
+
+
+def get_stats(db):
+    """Single-row aggregate stats across projects/releases/issues."""
+    total_projects = db.execute("SELECT COUNT(*) FROM projects").fetchone()[0]
+    total_releases = db.execute("SELECT COUNT(*) FROM releases").fetchone()[0]
+    total_issues = db.execute("SELECT COUNT(*) FROM issues").fetchone()[0]
+    total_stars = db.execute("SELECT COALESCE(SUM(stars), 0) FROM projects").fetchone()[0]
+    return {
+        "total_projects": total_projects,
+        "total_releases": total_releases,
+        "total_issues": total_issues,
+        "total_stars": total_stars,
+    }
diff --git a/repositories/release_repo.py b/repositories/release_repo.py
new file mode 100644
index 0000000..693a31e
--- /dev/null
+++ b/repositories/release_repo.py
@@ -0,0 +1,94 @@
+"""Release queries. No Flask imports; take an explicit sqlite3 connection.
+
+Queries filter on `releases.project_id`, which is backed by the
+`idx_releases_project_id` index (Stage 4.2; docs/analysis/optional-indexes.sql).
+"""
+
+import config
+
+RELEASE_COLUMNS = (
+    "id, project_id, tag_name, name, body, published_at, html_url"
+)
+
+# LLM context rows never need more than LLM_BODY_LIMIT body chars, so the
+# body is truncated in SQL and the full body never leaves the database.
+SLIM_RELEASE_COLUMNS = (
+    f"id, project_id, tag_name, name, "
+    f"substr(body, 1, {config.LLM_BODY_LIMIT}) AS body, published_at, html_url"
+)
+
+
+def _rows(cur):
+    return [dict(row) for row in cur.fetchall()]
+
+
+def list_recent_releases(db, limit=5):
+    return _rows(
+        db.execute(
+            f"SELECT {RELEASE_COLUMNS} FROM releases "
+            "ORDER BY published_at DESC LIMIT ?",
+            (limit,),
+        )
+    )
+
+
+def list_recent_releases_slim(db, limit=5):
+    return _rows(
+        db.execute(
+            f"SELECT {SLIM_RELEASE_COLUMNS} FROM releases "
+            "ORDER BY published_at DESC LIMIT ?",
+            (limit,),
+        )
+    )
+
+
+def list_releases_for_project(db, project_id, limit=None):
+    sql = (
+        f"SELECT {RELEASE_COLUMNS} FROM releases "
+        "WHERE project_id = ? ORDER BY published_at DESC"
+    )
+    params = (project_id,)
+    if limit is not None:
+        sql += " LIMIT ?"
+        params = (project_id, limit)
+    return _rows(db.execute(sql, params))
+
+
+def list_releases_for_project_slim(db, project_id, limit=None):
+    sql = (
+        f"SELECT {SLIM_RELEASE_COLUMNS} FROM releases "
+        "WHERE project_id = ? ORDER BY published_at DESC"
+    )
+    params = (project_id,)
+    if limit is not None:
+        sql += " LIMIT ?"
+        params = (project_id, limit)
+    return _rows(db.execute(sql, params))
+
+
+def search_releases(db, keyword, limit=None):
+    like = f"%{keyword}%"
+    sql = (
+        f"SELECT {RELEASE_COLUMNS} FROM releases "
+        "WHERE tag_name LIKE ? OR name LIKE ? "
+        "ORDER BY published_at DESC"
+    )
+    params = (like, like)
+    if limit is not None:
+        sql += " LIMIT ?"
+        params = (like, like, limit)
+    return _rows(db.execute(sql, params))
+
+
+def search_releases_slim(db, keyword, limit=None):
+    like = f"%{keyword}%"
+    sql = (
+        f"SELECT {SLIM_RELEASE_COLUMNS} FROM releases "
+        "WHERE tag_name LIKE ? OR name LIKE ? "
+        "ORDER BY published_at DESC"
+    )
+    params = (like, like)
+    if limit is not None:
+        sql += " LIMIT ?"
+        params = (like, like, limit)
+    return _rows(db.execute(sql, params))
diff --git a/repositories/watchdog_repo.py b/repositories/watchdog_repo.py
new file mode 100644
index 0000000..b12b7f5
--- /dev/null
+++ b/repositories/watchdog_repo.py
@@ -0,0 +1,29 @@
+"""Watchdog report queries (read against watchdog.db). No Flask imports."""
+
+import json
+import logging
+import sqlite3
+
+logger = logging.getLogger(__name__)
+
+
+def list_reports(watchdog_conn, days=7):
+    """Return recent watchdog reports; rows include parsed `meta` dict.
+
+    Raises sqlite3.Error on read failure — the caller (service layer) decides
+    how to degrade.
+    """
+    cur = watchdog_conn.execute(
+        "SELECT date, markdown, meta_json, created_at FROM reports "
+        "ORDER BY date DESC LIMIT ?",
+        (days,),
+    )
+    reports = []
+    for row in cur.fetchall():
+        item = dict(row)
+        try:
+            item["meta"] = json.loads(item.get("meta_json") or "{}")
+        except (json.JSONDecodeError, TypeError):
+            item["meta"] = {}
+        reports.append(item)
+    return reports
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c70f770
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+flask>=3.0
+requests>=2.28
diff --git a/routes/__init__.py b/routes/__init__.py
new file mode 100644
index 0000000..cdb181c
--- /dev/null
+++ b/routes/__init__.py
@@ -0,0 +1,5 @@
+from .api import bp as api_bp
+from .pages import bp as pages_bp
+from .watchdog import bp as watchdog_bp
+
+__all__ = ["api_bp", "pages_bp", "watchdog_bp"]
diff --git a/routes/api.py b/routes/api.py
new file mode 100644
index 0000000..a0b64e3
--- /dev/null
+++ b/routes/api.py
@@ -0,0 +1,204 @@
+"""JSON API blueprint: /api/* endpoints."""
+
+import logging
+import sqlite3
+import threading
+import time
+
+from flask import Blueprint, current_app, jsonify, request
+
+import config
+from extensions import get_db
+from repositories import issue_repo, project_repo, release_repo
+from services import analysis as analysis_service
+
+logger = logging.getLogger(__name__)
+
+bp = Blueprint("api", __name__, url_prefix="/api")
+
+# Simple in-memory rate limiter: {ip: [timestamp, ...]}
+#
+# 单进程假设 (OPT-8): 这个 dict 假设 Flask dev server 单进程运行.
+# 上 gunicorn 多 worker 时每个 worker 有独立 dict, 限流退化为
+# N × RATE_LIMIT_MAX — 详见 docs/analysis/gunicorn-evaluation.md (NO-GO 决策).
+# 若未来真要上 gunicorn, 换成 Redis 或 flask-limiter 的共享 backend.
+_rate_limit_store = {}
+
+# 后台清理线程间隔 (秒). 独立 daemon thread 定期清理全表,
+# 防止孤立 IP (只来一次再不来的) 永久占内存.
+_RATE_LIMIT_CLEANUP_INTERVAL_S = 60
+
+
+def _rate_limit_cleanup_once(now=None):
+    """Drop IPs whose newest timestamp is older than the rate-limit window.
+
+    Returns the number of entries removed. Idempotent, thread-safe-ish
+    (dict.pop is atomic under CPython GIL).
+    """
+    now = now if now is not None else time.time()
+    cutoff = now - config.RATE_LIMIT_WINDOW
+    removed = 0
+    for ip, timestamps in list(_rate_limit_store.items()):
+        if not timestamps or timestamps[-1] < cutoff:
+            _rate_limit_store.pop(ip, None)
+            removed += 1
+    return removed
+
+
+def _rate_limit_cleanup_loop():
+    """Daemon thread body: full cleanup every _RATE_LIMIT_CLEANUP_INTERVAL_S."""
+    while True:
+        time.sleep(_RATE_LIMIT_CLEANUP_INTERVAL_S)
+        try:
+            removed = _rate_limit_cleanup_once()
+            if removed:
+                logger.debug("rate-limit cleanup removed %d stale IP entries", removed)
+        except Exception:  # pragma: no cover - defensive, never crash the daemon
+            logger.exception("rate-limit cleanup iteration failed")
+
+
+_cleanup_thread = threading.Thread(
+    target=_rate_limit_cleanup_loop,
+    name="rate-limit-cleanup",
+    daemon=True,  # daemon thread: dies with the process, no shutdown hook needed
+)
+_cleanup_thread.start()
+
+
+def _check_rate_limit(ip):
+    """Return True if request should be blocked (429).
+
+    Hot path: only prunes the *current* IP's timestamps. Stale-IP full-table
+    cleanup is delegated to the daemon thread (see _rate_limit_cleanup_loop)
+    so this stays O(window) per call.
+    """
+    now = time.time()
+    if ip not in _rate_limit_store:
+        _rate_limit_store[ip] = []
+    _rate_limit_store[ip] = [
+        t for t in _rate_limit_store[ip] if now - t < config.RATE_LIMIT_WINDOW
+    ]
+    if len(_rate_limit_store[ip]) >= config.RATE_LIMIT_MAX:
+        return True
+    _rate_limit_store[ip].append(now)
+    return False
+
+
+@bp.route("/projects")
+def api_projects():
+    try:
+        return jsonify(project_repo.list_projects(get_db()))
+    except sqlite3.Error as exc:
+        logger.exception("api_projects failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/projects/")
+def api_project_detail(project_id):
+    try:
+        proj = project_repo.get_project(get_db(), project_id)
+        if proj is None:
+            return jsonify({"error": "project not found"}), 404
+        return jsonify(proj)
+    except sqlite3.Error as exc:
+        logger.exception("api_project_detail failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/projects//releases")
+def api_project_releases(project_id):
+    try:
+        return jsonify(release_repo.list_releases_for_project(get_db(), project_id))
+    except sqlite3.Error as exc:
+        logger.exception("api_project_releases failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/projects//issues")
+def api_project_issues(project_id):
+    try:
+        return jsonify(issue_repo.list_issues_for_project(get_db(), project_id))
+    except sqlite3.Error as exc:
+        logger.exception("api_project_issues failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/search")
+def api_search():
+    keyword = request.args.get("q", "").strip()
+    if not keyword:
+        return jsonify({"projects": [], "releases": [], "issues": []})
+    try:
+        db = get_db()
+        return jsonify({
+            "projects": project_repo.search_projects(db, keyword),
+            "releases": release_repo.search_releases(db, keyword),
+            "issues": issue_repo.search_issues(db, keyword),
+        })
+    except sqlite3.Error as exc:
+        logger.exception("api_search failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/stats")
+def api_stats():
+    try:
+        return jsonify(project_repo.get_stats(get_db()))
+    except sqlite3.Error as exc:
+        logger.exception("api_stats failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/presets")
+def api_presets():
+    return jsonify(analysis_service.get_presets())
+
+
+@bp.route("/context/")
+def api_context(scope):
+    """Return database context for a given scope (no LLM call)."""
+    try:
+        result, err = analysis_service.build_context(scope, get_db())
+        if err is not None:
+            return jsonify(err[0]), err[1]
+        return jsonify(result)
+    except sqlite3.Error as exc:
+        logger.exception("api_context failed")
+        return jsonify({"error": str(exc)}), 500
+
+
+@bp.route("/analyze/stream")
+def api_analyze_stream():
+    scope = request.args.get("scope", "home")
+    question = request.args.get("question", "").strip()
+    preset = request.args.get("preset", "").strip()
+
+    client_ip = request.remote_addr or "127.0.0.1"
+    if _check_rate_limit(client_ip):
+        return (
+            jsonify({"error": "rate limit exceeded, max 3 requests per minute"}),
+            429,
+        )
+
+    if not question and not preset:
+        return jsonify({"error": "question or preset required"}), 400
+
+    question = analysis_service.resolve_question(question, preset)
+
+    # 在 view 体内 (application context 仍存活) 就 eagerly 构建上下文 + 拼 prompt.
+    # generator 在 view return 后才被消费, 闭包内不得再依赖 Flask g / request.
+    try:
+        data, ctx_err = analysis_service.build_context(scope, get_db())
+        if ctx_err is not None:
+            return jsonify(ctx_err[0]), ctx_err[1]
+    except Exception as e:
+        current_app.logger.exception(
+            "failed to build analysis context for scope=%s", scope
+        )
+        return jsonify({"error": f"failed to build context: {str(e)}"}), 500
+
+    return analysis_service.stream_analysis_events(data, question), {
+        "Content-Type": "text/event-stream",
+        "Cache-Control": "no-cache",
+        "X-Accel-Buffering": "no",
+    }
diff --git a/routes/pages.py b/routes/pages.py
new file mode 100644
index 0000000..f009be1
--- /dev/null
+++ b/routes/pages.py
@@ -0,0 +1,36 @@
+"""Page blueprint: HTML routes rendering Jinja2 templates."""
+
+from flask import Blueprint, jsonify, render_template
+
+bp = Blueprint("pages", __name__)
+
+
+@bp.route("/healthz")
+def page_healthz():
+    """Liveness probe. 不查 DB — deploy.sh 用它判断进程是否活着."""
+    return jsonify({"status": "ok"})
+
+
+@bp.route("/")
+def page_index():
+    return render_template("index.html")
+
+
+@bp.route("/projects")
+def page_projects():
+    return render_template("projects.html")
+
+
+@bp.route("/projects/")
+def page_project_detail(project_id):
+    return render_template("project_detail.html", project_id=project_id)
+
+
+@bp.route("/search")
+def page_search():
+    return render_template("search.html")
+
+
+@bp.route("/analyze")
+def page_analyze():
+    return render_template("analyze.html")
diff --git a/routes/watchdog.py b/routes/watchdog.py
new file mode 100644
index 0000000..ad54077
--- /dev/null
+++ b/routes/watchdog.py
@@ -0,0 +1,38 @@
+"""Watchdog blueprint: /watchdog page + /watchdog/run trigger (CSRF-guarded)."""
+
+import logging
+
+from flask import Blueprint, flash, redirect, render_template, request, url_for
+
+from extensions import get_db, get_watchdog_db
+from repositories import project_repo
+from security import validate_csrf
+from services import watchdog as watchdog_service
+
+logger = logging.getLogger(__name__)
+
+bp = Blueprint("watchdog", __name__)
+
+
+@bp.route("/watchdog")
+def page_watchdog():
+    days = watchdog_service.parse_days(request.args.get("days"), default=7)
+    reports = watchdog_service.load_watchdog_reports(get_watchdog_db(), days=days)
+    starred = project_repo.get_starred_names(get_db())
+    return render_template(
+        "watchdog.html", reports=reports, days=days, starred=starred
+    )
+
+
+@bp.route("/watchdog/run", methods=["POST"])
+def watchdog_run():
+    validate_csrf()
+
+    ok, status, message = watchdog_service.trigger_watchdog_run()
+    if ok:
+        flash(message, "success")
+    elif status in ("cooldown",):
+        flash(message, "warning")
+    else:
+        flash(message, "danger")
+    return redirect(url_for("watchdog.page_watchdog"))
diff --git a/scripts/daily_watchdog.py b/scripts/daily_watchdog.py
new file mode 100755
index 0000000..c20b886
--- /dev/null
+++ b/scripts/daily_watchdog.py
@@ -0,0 +1,917 @@
+#!/usr/bin/python3
+"""AI Agent 升级迭代日报 — 抓取 + 存档 + 报告管道 (B1 阶段)."""
+# LLM-INTEGRATION-POINT: B2 将替换 summarize_with_llm 函数体
+# (litellm analysis, POST http://127.0.0.1:4000/v1/chat/completions,
+#  model="analysis", 无 auth, timeout=LLM_TIMEOUT)
+
+import json
+import logging
+import os
+import re
+import sqlite3
+import subprocess
+import sys
+import time
+import xml.etree.ElementTree as ET
+from concurrent import futures
+from datetime import datetime
+from pathlib import Path
+
+import requests
+
+# blog-app root on sys.path so the analysis service (Stage 1 refactor) is
+# importable from cron/systemd contexts where CWD is not the app root.
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+import config  # noqa: E402  (needs the sys.path insert above)
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s [%(levelname)s] %(message)s",
+    datefmt="%Y-%m-%d %H:%M:%S",
+    stream=sys.stdout,
+)
+log = logging.getLogger("daily-watchdog")
+
+BASE_DIR = Path.home() / "opencode-blog-showcase"
+DATA_DIR = BASE_DIR / "data"
+LOGS_DIR = BASE_DIR / "logs"
+DB_PATH = DATA_DIR / "watchdog.db"
+
+UA = "daily-watchdog/1.0"
+HTTP_TIMEOUT = 30
+GITHUB_TIMEOUT = 30
+EXA_TIMEOUT = 60
+ATOM_TIMEOUT = 45
+LLM_URL = "http://127.0.0.1:4000/v1/chat/completions"
+LLM_MODEL = "analysis"
+LLM_TIMEOUT = 300
+
+# Prompt trimming budgets shared with services/analysis.py (OPT-10).
+LLM_BODY_LIMIT = config.LLM_BODY_LIMIT
+LLM_FIELD_LIMIT = config.LLM_FIELD_LIMIT
+LLM_MESSAGE_LIMIT = config.LLM_MESSAGE_LIMIT
+LLM_TOTAL_LIMIT = config.LLM_TOTAL_LIMIT
+
+# Parallel collection (OPT-9): 6 projects x 5 sources + 1 LLM call each are
+# I/O bound, so threads beat the GIL. Capped at 3 to stay under GitHub's
+# unauthenticated rate limit (60 req/hr) and to be polite to npm/atom.
+COLLECT_MAX_WORKERS = int(os.environ.get("BLOG_APP_COLLECT_WORKERS", "3"))
+# Global cap so one hanging project cannot stall the whole run:
+# slowest data source (Exa, 60s + one retry) + LLM timeout + slack.
+COLLECT_PROJECT_TIMEOUT = LLM_TIMEOUT + 120
+
+CANONICAL_SOURCES = [
+    "GitHub Releases API",
+    "GitHub commits API",
+    "npm registry",
+    "GitHub releases.atom",
+    "Exa web search",
+]
+
+PROJECTS = [
+    {
+        "key": "opencode",
+        "label": "sst/opencode",
+        "github": "sst/opencode",
+        "npm": "opencode-ai",
+        "installed": "1.18.14",
+        "star": True,
+    },
+    {
+        "key": "claude-code",
+        "label": "anthropics/claude-code",
+        "github": "anthropics/claude-code",
+        "npm": "@anthropic-ai/claude-code",
+        "installed": None,
+        "star": True,
+    },
+    {
+        "key": "codex",
+        "label": "openai/codex",
+        "github": "openai/codex",
+        "npm": "@openai/codex",
+        "installed": None,
+        "star": False,
+    },
+    {
+        "key": "hermes-agent",
+        "label": "NousResearch/hermes-agent",
+        "github": "NousResearch/hermes-agent",
+        "npm": "hermes-agent",
+        "installed": None,
+        "star": True,
+    },
+    {
+        "key": "oh-my-openagent",
+        "label": "code-yeongyu/oh-my-openagent",
+        "github": "code-yeongyu/oh-my-openagent",
+        "npm": "oh-my-openagent",
+        "installed": None,
+        "star": True,
+    },
+    {
+        "key": "hindsight",
+        "label": "vectorize-io/hindsight",
+        "github": "vectorize-io/hindsight",
+        "npm": "hindsight",
+        "installed": None,
+        "star": True,
+        "npm_relevance_check": True,
+    },
+]
+
+ATOM_NS = "{http://www.w3.org/2005/Atom}"
+
+
+def _llm_failure(reason: str) -> dict:
+    return {
+        "summary_zh": f"(LLM 分析失败: {reason})",
+        "breaking": None,
+        "breaking_reason": "LLM 分析失败",
+        "relevance_note": "",
+    }
+
+
+def _truncate_field(value, limit: int):
+    """Recursively truncate every string in a nested structure."""
+    if isinstance(value, str):
+        return value[:limit]
+    if isinstance(value, dict):
+        return {k: _truncate_field(v, limit) for k, v in value.items()}
+    if isinstance(value, list):
+        return [_truncate_field(v, limit) for v in value]
+    return value
+
+
+def _extract_json_object(text: str) -> dict | None:
+    """Extract the first {...} JSON object from text (tolerates fences/prose)."""
+    start = text.find("{")
+    end = text.rfind("}")
+    if start == -1 or end == -1 or end <= start:
+        return None
+    try:
+        obj = json.loads(text[start:end + 1])
+    except json.JSONDecodeError:
+        return None
+    return obj if isinstance(obj, dict) else None
+
+
+def _short_reason(e: Exception, default: str = "请求失败") -> str:
+    """Short single-line failure reason (≤80 chars, no newlines)."""
+    msg = str(e).strip().replace("\n", " ").replace("\r", " ")
+    if not msg:
+        msg = default
+    return msg[:80]
+
+
+def summarize_with_llm(project_key: str, collected_data: dict) -> dict:
+    # 数据截断: release body ≤LLM_BODY_LIMIT, 其余字段各 ≤LLM_FIELD_LIMIT,
+    # 总数据部分 ≤LLM_TOTAL_LIMIT (12000 chars ≈ 3000 tokens, 见 config.py)
+    trimmed: dict = {}
+    for key, value in collected_data.items():
+        if value is None:
+            continue
+        if key == "github_release" and isinstance(value, dict):
+            rel = dict(value)
+            rel["body"] = (rel.get("body") or "")[:LLM_BODY_LIMIT]
+            trimmed[key] = _truncate_field(rel, LLM_FIELD_LIMIT)
+            trimmed[key]["body"] = (value.get("body") or "")[:LLM_BODY_LIMIT]
+        else:
+            trimmed[key] = _truncate_field(value, LLM_FIELD_LIMIT)
+
+    data_json = json.dumps(trimmed, ensure_ascii=False, default=str)
+    if len(data_json) > LLM_TOTAL_LIMIT:
+        # 极端情况下继续收缩: 砍掉 exa_results 再砍 commits
+        trimmed.pop("exa_results", None)
+        data_json = json.dumps(trimmed, ensure_ascii=False, default=str)
+        if len(data_json) > LLM_TOTAL_LIMIT and isinstance(trimmed.get("github_commits"), list):
+            trimmed["github_commits"] = trimmed["github_commits"][:3]
+            data_json = json.dumps(trimmed, ensure_ascii=False, default=str)
+
+    prompt = (
+        "你是 AI 编程助手项目的升级监控分析师。下面是项目 "
+        f"{project_key} 的最新抓取数据 (JSON):\n\n{data_json}\n\n"
+        "请分析这些变更, 严格只返回一个 JSON 对象, 不要输出其他内容:\n"
+        "{\n"
+        '  "summary_zh": "不少于50字的中文摘要, 概括本次更新的核心内容, 2-3句话",\n'
+        '  "breaking": true 或 false, 表示是否存在不向后兼容的破坏性变更,\n'
+        '  "breaking_reason": "若有 breaking 说明具体内容; 若无则写 \'未发现破坏性变更\'",\n'
+        '  "relevance_note": "对本地已安装该工具用户的实际意义, 1-2句话"\n'
+        "}"
+    )
+    messages = [
+        {"role": "system", "content": "你是严谨的 AI 工具升级监控分析师, 只输出 JSON。"},
+        {"role": "user", "content": prompt},
+    ]
+
+    try:
+        resp = requests.post(
+            LLM_URL,
+            json={"model": LLM_MODEL, "messages": messages},
+            timeout=LLM_TIMEOUT,
+        )
+        resp.raise_for_status()
+        content = (
+            resp.json()
+            .get("choices", [{}])[0]
+            .get("message", {})
+            .get("content", "")
+        )
+        if not content:
+            log.error("LLM empty content for %s", project_key)
+            return _llm_failure("空响应")
+
+        obj = _extract_json_object(content)
+        if obj is None:
+            log.error("LLM JSON parse failed for %s: %.200s", project_key, content)
+            return _llm_failure("JSON 解析失败")
+
+        summary = str(obj.get("summary_zh", "")).strip()
+        if not summary:
+            log.error("LLM empty summary_zh for %s", project_key)
+            return _llm_failure("summary_zh 为空")
+
+        breaking = obj.get("breaking")
+        if not isinstance(breaking, bool):
+            breaking = None
+        return {
+            "summary_zh": summary,
+            "breaking": breaking,
+            "breaking_reason": str(obj.get("breaking_reason", "")).strip() or "未发现破坏性变更",
+            "relevance_note": str(obj.get("relevance_note", "")).strip(),
+        }
+    except Exception as e:
+        log.error("LLM call failed for %s: %s", project_key, e)
+        return _llm_failure(str(e))
+
+
+def gh_headers():
+    return {
+        "User-Agent": UA,
+        "Accept": "application/vnd.github+json",
+    }
+
+
+def fetch_github_release(github_repo: str) -> tuple[dict | None, str | None]:
+    url = f"https://api.github.com/repos/{github_repo}/releases/latest"
+    try:
+        r = requests.get(url, headers=gh_headers(), timeout=GITHUB_TIMEOUT)
+        r.raise_for_status()
+        d = r.json()
+        return {
+            "tag_name": d.get("tag_name", ""),
+            "published_at": d.get("published_at", ""),
+            "name": d.get("name", ""),
+            "body": (d.get("body") or "")[:LLM_BODY_LIMIT],
+        }, None
+    except Exception as e:
+        log.error("GitHub release failed for %s: %s", github_repo, e)
+        return None, _short_reason(e)
+
+
+def fetch_github_commits(github_repo: str) -> tuple[list[dict] | None, str | None]:
+    url = f"https://api.github.com/repos/{github_repo}/commits?per_page=5"
+    try:
+        r = requests.get(url, headers=gh_headers(), timeout=GITHUB_TIMEOUT)
+        r.raise_for_status()
+        items = []
+        for c in r.json():
+            msg = c.get("commit", {}).get("message", "")
+            first_line = msg.split("\n")[0][:LLM_MESSAGE_LIMIT]
+            date = c.get("commit", {}).get("author", {}).get("date", "")
+            items.append({"message": first_line, "date": date})
+        return items, None
+    except Exception as e:
+        log.error("GitHub commits failed for %s: %s", github_repo, e)
+        return None, _short_reason(e)
+
+
+def fetch_npm(pkg: str) -> tuple[dict | None, str | None]:
+    if pkg.startswith("@"):
+        scope, name = pkg.split("/", 1)
+        encoded = f"{scope}%2F{name}"
+    else:
+        encoded = pkg
+    url = f"https://registry.npmjs.org/{encoded}/latest"
+    try:
+        r = requests.get(url, headers={"User-Agent": UA}, timeout=HTTP_TIMEOUT)
+        r.raise_for_status()
+        d = r.json()
+        return {
+            "version": d.get("version", ""),
+            "description": d.get("description", ""),
+        }, None
+    except Exception as e:
+        log.error("npm fetch failed for %s: %s", pkg, e)
+        return None, _short_reason(e)
+
+
+def fetch_releases_atom(github_repo: str) -> tuple[dict | None, str | None]:
+    url = f"https://github.com/{github_repo}/releases.atom"
+    last_err: str | None = None
+    for attempt in range(2):  # 首次 + 1 次重试 (仅 Timeout)
+        try:
+            r = requests.get(
+                url, headers={"User-Agent": UA}, timeout=ATOM_TIMEOUT
+            )
+            r.raise_for_status()
+            root = ET.fromstring(r.text)
+            entries = root.findall(f"{ATOM_NS}entry")
+            if not entries:
+                return None, "feed 无 entry"
+            entry = entries[0]
+            title = entry.findtext(f"{ATOM_NS}title", default="")
+            updated = entry.findtext(f"{ATOM_NS}updated", default="")
+            return {"title": title, "updated": updated}, None
+        except requests.exceptions.Timeout:
+            last_err = "超时 (github.com 限流/网络)"
+            log.warning(
+                "Atom feed timeout for %s (attempt %d/2)", github_repo, attempt + 1
+            )
+            if attempt == 0:
+                time.sleep(5)
+        except Exception as e:
+            last_err = _short_reason(e)
+            log.error("Atom feed failed for %s: %s", github_repo, e)
+            break
+    return None, last_err
+
+
+def _parse_exa_text_block(text: str) -> list[dict]:
+    results = []
+    blocks = re.split(r"\n---\n", text)
+    for block in blocks:
+        block = block.strip()
+        if not block:
+            continue
+        title = ""
+        url = ""
+        highlights = []
+        in_highlights = False
+        for line in block.split("\n"):
+            if line.startswith("Title:"):
+                title = line[6:].strip()
+                in_highlights = False
+            elif line.startswith("URL:"):
+                url = line[4:].strip()
+                in_highlights = False
+            elif line.startswith("Published:") or line.startswith("Author:"):
+                in_highlights = False
+            elif line.startswith("Highlights:"):
+                in_highlights = True
+            elif in_highlights and line.strip() and not line.startswith("..."):
+                highlights.append(line.strip())
+        if title and url:
+            summary = " ".join(highlights)[:LLM_FIELD_LIMIT]
+            results.append({"title": title, "url": url, "text": summary})
+    return results
+
+
+class ExaClient:
+    def __init__(self):
+        self.url = "https://mcp.exa.ai/mcp?tools=web_search_exa"
+        self.headers = {
+            "Content-Type": "application/json",
+            "Accept": "application/json, text/event-stream",
+        }
+        self.session_id: str | None = None
+        self._initialized = False
+
+    def initialize(self) -> bool:
+        try:
+            body = {
+                "jsonrpc": "2.0",
+                "id": 1,
+                "method": "initialize",
+                "params": {
+                    "protocolVersion": "2025-03-26",
+                    "capabilities": {},
+                    "clientInfo": {"name": "daily-watchdog", "version": "1.0"},
+                },
+            }
+            r = requests.post(
+                self.url, json=body, headers=self.headers, timeout=EXA_TIMEOUT
+            )
+            r.raise_for_status()
+            self.session_id = r.headers.get("Mcp-Session-Id", "")
+            notify = {
+                "jsonrpc": "2.0",
+                "method": "notifications/initialized",
+            }
+            h = {**self.headers}
+            if self.session_id:
+                h["Mcp-Session-Id"] = self.session_id
+            requests.post(
+                self.url, json=notify, headers=h, timeout=HTTP_TIMEOUT
+            )
+            self._initialized = True
+            log.info("Exa MCP initialized, session=%s", self.session_id)
+            return True
+        except Exception as e:
+            log.error("Exa init failed: %s", e)
+            return False
+
+    def search(self, query: str, num_results: int = 3) -> list[dict] | None:
+        if not self._initialized:
+            return None
+        try:
+            body = {
+                "jsonrpc": "2.0",
+                "id": 2,
+                "method": "tools/call",
+                "params": {
+                    "name": "web_search_exa",
+                    "arguments": {"query": query, "numResults": num_results},
+                },
+            }
+            h = {**self.headers}
+            if self.session_id:
+                h["Mcp-Session-Id"] = self.session_id
+            r = requests.post(
+                self.url, json=body, headers=h, timeout=EXA_TIMEOUT
+            )
+            r.raise_for_status()
+            text = r.text
+            results = []
+            for line in text.split("\n"):
+                line = line.strip()
+                if not line.startswith("data:"):
+                    continue
+                payload = line[5:].strip()
+                if not payload:
+                    continue
+                try:
+                    obj = json.loads(payload)
+                except json.JSONDecodeError:
+                    continue
+                if "result" not in obj:
+                    continue
+                content = obj["result"].get("content", [])
+                for c in content:
+                    raw = c.get("text", "")
+                    parsed = _parse_exa_text_block(raw)
+                    results.extend(parsed)
+            return results if results else None
+        except Exception as e:
+            log.error("Exa search failed for '%s': %s", query, e)
+            return None
+
+
+def check_npm_relevance(pkg: str, npm_data: dict) -> bool:
+    desc = (npm_data.get("description") or "").lower()
+    keywords = [
+        "vectorize",
+        "hindsight",
+        "replay",
+        "debugging",
+        "browser",
+        "record",
+        "session",
+    ]
+    return any(kw in desc for kw in keywords)
+
+
+def _collect_one_project(proj: dict, exa: ExaClient | None) -> dict:
+    """Fetch all sources for one project and summarize with the LLM.
+
+    Runs inside a worker thread. Every fetcher already catches its own
+    exceptions, so one failing source (or a failing LLM call) never raises
+    out of here and never affects other projects. `exa` is None when the
+    shared MCP session failed to initialize.
+    """
+    log.info("Collecting data for %s", proj["label"])
+    data: dict = {
+        "key": proj["key"],
+        "label": proj["label"],
+        "star": proj["star"],
+        "installed": proj["installed"],
+        "github_release": None,
+        "github_commits": None,
+        "npm": None,
+        "atom": None,
+        "exa_results": None,
+        "source_errors": [],
+        "sources_used": [],
+        "llm_summary": None,
+    }
+
+    rel, rel_err = fetch_github_release(proj["github"])
+    if rel:
+        data["github_release"] = rel
+        data["sources_used"].append("GitHub Releases API")
+    else:
+        data["source_errors"].append(
+            f"GitHub Releases API: {rel_err or '请求失败'}"
+        )
+
+    commits, commits_err = fetch_github_commits(proj["github"])
+    if commits:
+        data["github_commits"] = commits
+        data["sources_used"].append("GitHub commits API")
+    else:
+        data["source_errors"].append(
+            f"GitHub commits API: {commits_err or '请求失败'}"
+        )
+
+    npm_data, npm_err = fetch_npm(proj["npm"])
+    if npm_data:
+        if proj.get("npm_relevance_check"):
+            if check_npm_relevance(proj["npm"], npm_data):
+                data["npm"] = npm_data
+                data["sources_used"].append("npm registry")
+            else:
+                log.info(
+                    "npm package '%s' not relevant, skipping", proj["npm"]
+                )
+                data["source_errors"].append(
+                    "npm registry: 包存在但与本 repo 不相关, 已跳过"
+                )
+        else:
+            data["npm"] = npm_data
+            data["sources_used"].append("npm registry")
+    else:
+        data["source_errors"].append(f"npm registry: {npm_err or '请求失败'}")
+
+    atom, atom_err = fetch_releases_atom(proj["github"])
+    if atom:
+        data["atom"] = atom
+        data["sources_used"].append("GitHub releases.atom")
+    else:
+        data["source_errors"].append(
+            f"GitHub releases.atom: {atom_err or '请求失败'}"
+        )
+
+    if exa is not None:
+        query = f"{proj['label']} latest release changelog"
+        exa_res = exa.search(query)
+        if exa_res:
+            data["exa_results"] = exa_res
+            data["sources_used"].append("Exa web search")
+        else:
+            data["source_errors"].append("Exa web search: 无结果或查询失败")
+    else:
+        data["source_errors"].append("Exa web search: MCP 初始化失败")
+
+    collected_for_llm = {
+        "github_release": data["github_release"],
+        "github_commits": data["github_commits"],
+        "npm": data["npm"],
+        "atom": data["atom"],
+        "exa_results": data["exa_results"],
+    }
+    data["llm_summary"] = summarize_with_llm(proj["key"], collected_for_llm)
+
+    return data
+
+
+def collect_all() -> tuple[list[dict], dict[str, int], list[str]]:
+    exa = ExaClient()
+    exa_ok = exa.initialize()
+    if not exa_ok:
+        log.warning("Exa MCP init failed — all projects will note it")
+
+    # I/O bound (HTTP + LLM), so threads sidestep the GIL. One hanging
+    # project cannot stall the run: its future is abandoned after
+    # COLLECT_PROJECT_TIMEOUT and the thread is left to die on its own.
+    with futures.ThreadPoolExecutor(max_workers=COLLECT_MAX_WORKERS) as pool:
+        future_by_index = {
+            i: pool.submit(_collect_one_project, proj, exa if exa_ok else None)
+            for i, proj in enumerate(PROJECTS)
+        }
+        results_by_index: dict[int, dict] = {}
+        for i, fut in future_by_index.items():
+            proj = PROJECTS[i]
+            try:
+                results_by_index[i] = fut.result(timeout=COLLECT_PROJECT_TIMEOUT)
+            except Exception as e:
+                log.error(
+                    "collect timed out or crashed for %s: %s", proj["label"], e
+                )
+                results_by_index[i] = {
+                    "key": proj["key"],
+                    "label": proj["label"],
+                    "star": proj["star"],
+                    "installed": proj["installed"],
+                    "github_release": None,
+                    "github_commits": None,
+                    "npm": None,
+                    "atom": None,
+                    "exa_results": None,
+                    "source_errors": [f"并行抓取超时或异常: {_short_reason(e)}"],
+                    "sources_used": [],
+                    "llm_summary": None,
+                }
+
+    # Preserve PROJECTS order; aggregate per-source stats from the results
+    # produced by the worker threads (counts are merged here, single-threaded).
+    project_results = [results_by_index[i] for i in range(len(PROJECTS))]
+    source_counts: dict[str, int] = {s: 0 for s in CANONICAL_SOURCES}
+    sources_failed: set[str] = set()
+    if not exa_ok:
+        sources_failed.add("Exa web search")
+    for data in project_results:
+        for src in data["sources_used"]:
+            if src in source_counts:
+                source_counts[src] += 1
+        for err in data["source_errors"]:
+            src, _sep, _reason = err.partition(": ")
+            if src in source_counts:
+                sources_failed.add(src)
+
+    return project_results, source_counts, sorted(sources_failed)
+
+
+def determine_project_status(data: dict) -> str:
+    has_any = any(
+        [
+            data["github_release"],
+            data["github_commits"],
+            data["npm"],
+            data["atom"],
+        ]
+    )
+    if has_any and not data["source_errors"]:
+        return "ok"
+    if has_any:
+        return "partial"
+    return "failed"
+
+
+def _split_source_error(err: str) -> tuple[str, str]:
+    src, sep, reason = err.partition(": ")
+    if not sep:
+        return err, "未知原因"
+    return src, reason
+
+
+def render_report(
+    date_str: str,
+    project_results: list[dict],
+    source_counts: dict[str, int],
+) -> str:
+    lines = []
+    lines.append(f"# AI Agent 升级迭代日报 - {date_str}")
+    lines.append("")
+    lines.append("## 数据源")
+    lines.append("")
+    total = len(PROJECTS)
+    for src in CANONICAL_SOURCES:
+        n = source_counts.get(src, 0)
+        lines.append(f"- {src} ({n}/{total})")
+    lines.append("")
+
+    all_failed = all(determine_project_status(d) == "failed" for d in project_results)
+    if all_failed:
+        lines.append("> ⚠️ 所有项目全部数据源抓取失败")
+        lines.append("")
+
+    for data in project_results:
+        status = determine_project_status(data)
+        star_mark = " ★" if data["star"] else ""
+        install_mark = f" (已装 {data['installed']})" if data["installed"] else " (未装)"
+        lines.append(f"## {data['label']}{star_mark}{install_mark}")
+        lines.append("")
+
+        if status == "failed":
+            lines.append("⚠️ 全部数据源抓取失败:")
+            lines.append("")
+            for err in data["source_errors"]:
+                src, reason = _split_source_error(err)
+                lines.append(f"⚠️ 抓取失败: {src} — {reason}")
+            lines.append("")
+            continue
+
+        if data["github_release"]:
+            rel = data["github_release"]
+            lines.append(f"**Latest Release:** `{rel['tag_name']}`")
+            lines.append(f"**发布日期:** {rel['published_at']}")
+            if rel["name"]:
+                lines.append(f"**Release 名称:** {rel['name']}")
+            lines.append("")
+            if rel["body"]:
+                lines.append("**Changelog 摘要:**")
+                lines.append(f"> {rel['body'][:LLM_FIELD_LIMIT]}")
+                lines.append("")
+        elif data["github_commits"]:
+            lines.append("**最近 Commits (无 release):**")
+            for c in data["github_commits"]:
+                lines.append(f"- [{c['date'][:10]}] {c['message']}")
+            lines.append("")
+        else:
+            lines.append("⚠️ GitHub 数据不可用")
+            for err in data["source_errors"]:
+                if err.startswith("GitHub Releases API:") or err.startswith(
+                    "GitHub commits API:"
+                ):
+                    src, reason = _split_source_error(err)
+                    lines.append(f"⚠️ 抓取失败: {src} — {reason}")
+            lines.append("")
+
+        llm = data["llm_summary"] or {}
+        if llm.get("summary_zh"):
+            lines.append(f"**LLM 总结:** {llm['summary_zh']}")
+            lines.append("")
+        lines.append(
+            f"**Breaking Changes:** {llm.get('breaking_reason', 'LLM 总结失败')}"
+        )
+        lines.append("")
+
+        if data["npm"]:
+            npm = data["npm"]
+            lines.append(f"**npm latest:** `{npm['version']}` — {npm['description']}")
+            lines.append("")
+        elif any(
+            e.startswith("npm registry: 包存在但与本 repo 不相关")
+            for e in data["source_errors"]
+        ):
+            lines.append("**npm:** ⚠️ 包存在但与本 repo 不相关, 已跳过")
+            lines.append("")
+        elif any(e.startswith("npm registry:") for e in data["source_errors"]):
+            err = next(
+                e for e in data["source_errors"] if e.startswith("npm registry:")
+            )
+            src, reason = _split_source_error(err)
+            lines.append(f"**npm:** ⚠️ 抓取失败: {src} — {reason}")
+            lines.append("")
+
+        if data["atom"]:
+            atom = data["atom"]
+            lines.append(f"**releases.atom 最新:** {atom['title']} ({atom['updated']})")
+            lines.append("")
+        elif any(
+            e.startswith("GitHub releases.atom:") for e in data["source_errors"]
+        ):
+            err = next(
+                e
+                for e in data["source_errors"]
+                if e.startswith("GitHub releases.atom:")
+            )
+            src, reason = _split_source_error(err)
+            lines.append(f"**releases.atom:** ⚠️ 抓取失败: {src} — {reason}")
+            lines.append("")
+
+        llm = data["llm_summary"] or {}
+        relevance = llm.get("relevance_note", "")
+        if relevance:
+            lines.append(f"★ **相关性:** {relevance}")
+            lines.append("")
+        else:
+            lines.append("★ **相关性:** LLM 总结失败, 相关性待评估")
+            lines.append("")
+
+        if data["exa_results"]:
+            lines.append("**Exa 搜索结果:**")
+            lines.append("")
+            for item in data["exa_results"][:3]:
+                title = item.get("title", "无标题")
+                url = item.get("url", "")
+                text = item.get("text", "")[:LLM_MESSAGE_LIMIT]
+                lines.append(f"> [{title}]({url}): {text}")
+                lines.append("")
+        elif any(e.startswith("Exa web search:") for e in data["source_errors"]):
+            err = next(
+                e for e in data["source_errors"] if e.startswith("Exa web search:")
+            )
+            src, reason = _split_source_error(err)
+            lines.append(f"**Exa 搜索:** ⚠️ 抓取失败: {src} — {reason}")
+            lines.append("")
+
+        lines.append("---")
+        lines.append("")
+
+    return "\n".join(lines)
+
+
+def save_to_sqlite(date_str: str, markdown: str, meta: dict) -> None:
+    DATA_DIR.mkdir(parents=True, exist_ok=True)
+    conn = sqlite3.connect(str(DB_PATH))
+    try:
+        conn.execute(
+            """CREATE TABLE IF NOT EXISTS reports (
+                date TEXT PRIMARY KEY,
+                markdown TEXT NOT NULL,
+                meta_json TEXT NOT NULL,
+                created_at TEXT NOT NULL
+            )"""
+        )
+        now = datetime.now().isoformat()
+        conn.execute(
+            "INSERT OR REPLACE INTO reports (date, markdown, meta_json, created_at) VALUES (?, ?, ?, ?)",
+            (date_str, markdown, json.dumps(meta, ensure_ascii=False), now),
+        )
+        conn.commit()
+    finally:
+        conn.close()
+
+
+def save_markdown(date_str: str, markdown: str) -> Path:
+    LOGS_DIR.mkdir(parents=True, exist_ok=True)
+    path = LOGS_DIR / f"daily-agent-watchdog-{date_str}.md"
+    path.write_text(markdown, encoding="utf-8")
+    return path
+
+
+def _watchdog_already_running() -> bool:
+    try:
+        out = subprocess.check_output(
+            ["pgrep", "-f", "daily_watchdog.py"], text=True, timeout=10
+        )
+    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
+        return False
+    own = str(os.getpid())
+    for pid in out.split():
+        pid = pid.strip()
+        if not pid or pid == own:
+            continue
+        # 只认真正的 python 脚本进程, 排除命令行里恰好提到该文件名的进程
+        try:
+            with open(f"/proc/{pid}/cmdline", "rb") as f:
+                argv = f.read().split(b"\x00")
+        except OSError:
+            continue
+        if not argv or not argv[0]:
+            continue
+        exe = os.path.basename(argv[0].decode(errors="replace"))
+        if exe.startswith("python"):
+            return True
+    return False
+
+
+def _today_report_ok(date_str: str) -> bool:
+    if not DB_PATH.exists():
+        return False
+    try:
+        conn = sqlite3.connect(str(DB_PATH))
+        try:
+            row = conn.execute(
+                "SELECT meta_json FROM reports WHERE date = ?", (date_str,)
+            ).fetchone()
+        finally:
+            conn.close()
+    except Exception as e:
+        log.warning("fix-if-missing check failed: %s", e)
+        return False
+    if not row:
+        return False
+    try:
+        meta = json.loads(row[0])
+    except (json.JSONDecodeError, TypeError):
+        return False
+    projects = meta.get("projects", {})
+    ok_count = sum(
+        1
+        for st in projects.values()
+        if isinstance(st, dict) and st.get("status") != "failed"
+    )
+    return ok_count >= 2
+
+
+def main() -> int:
+    fix_if_missing = "--fix-if-missing" in sys.argv[1:]
+    date_str = datetime.now().strftime("%Y-%m-%d")
+
+    if _watchdog_already_running():
+        log.warning("Another daily_watchdog instance is already running — skip")
+        return 0
+
+    if fix_if_missing and _today_report_ok(date_str):
+        log.info("today's report already OK — skip full run (--fix-if-missing)")
+        return 0
+
+    log.info("Starting daily watchdog for %s", date_str)
+
+    project_results, source_counts, sources_failed = collect_all()
+
+    all_failed = all(determine_project_status(d) == "failed" for d in project_results)
+
+    markdown = render_report(date_str, project_results, source_counts)
+
+    meta = {
+        "sources_ok": sorted(k for k, v in source_counts.items() if v > 0),
+        "sources_failed": sources_failed,
+        "source_counts": source_counts,
+        "projects": {
+            d["key"]: {
+                "status": determine_project_status(d),
+                "sources_used": d["sources_used"],
+            }
+            for d in project_results
+        },
+    }
+
+    save_to_sqlite(date_str, markdown, meta)
+    md_path = save_markdown(date_str, markdown)
+    log.info("Report saved to %s", md_path)
+    log.info("SQLite record saved to %s", DB_PATH)
+
+    if all_failed:
+        log.error("All projects failed — exit code 1")
+        return 1
+
+    log.info("Daily watchdog completed successfully")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
new file mode 100755
index 0000000..64636f3
--- /dev/null
+++ b/scripts/deploy.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+# Restart the blog-app Flask server on 0.0.0.0:8090.
+#
+# Guarantees:
+#   - PID file (blog-app/app.pid) tracks the live server.
+#   - Old server is killed before new one starts (no "already running" ambiguity).
+#   - Health check loops on /healthz up to 30s (1s interval). 启动失败立即报错.
+#   - On failure: kill the failed new PID, restore the OLD server if we recorded one,
+#     print the last 50 lines of /tmp/blog-app.log, exit 1.
+#   - Does NOT touch daily_watchdog.py / cron (independent lifecycle).
+
+set -u
+set -o pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+PID_FILE="$APP_DIR/app.pid"
+LOG_FILE="/tmp/blog-app.log"
+HEALTHZ_URL="http://127.0.0.1:8090/healthz"
+HEALTHZ_TIMEOUT_S=30
+PYTHON_BIN="/usr/bin/python3"
+
+cd "$APP_DIR"
+# flask/requests live in the real user's site-packages; ensure python sees them
+export HOME=/home/yi
+
+log() { printf '[deploy] %s\n' "$*"; }
+fail() {
+    log "ERROR: $*"
+    log "----- last 50 lines of $LOG_FILE -----"
+    tail -n 50 "$LOG_FILE" 2>/dev/null || log "(no log file)"
+    exit 1
+}
+
+# --- 1. Snapshot current PID so we can roll back if the new boot fails ---
+OLD_PID=""
+if [[ -f "$PID_FILE" ]]; then
+    OLD_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
+fi
+
+# --- 2. Kill old server if PID is alive ---
+if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
+    log "stopping old server pid=$OLD_PID"
+    kill "$OLD_PID" 2>/dev/null || true
+    # wait up to 5s for graceful exit, then SIGKILL
+    for _ in 1 2 3 4 5; do
+        if ! kill -0 "$OLD_PID" 2>/dev/null; then break; fi
+        sleep 1
+    done
+    if kill -0 "$OLD_PID" 2>/dev/null; then
+        log "old pid $OLD_PID did not exit, sending SIGKILL"
+        kill -9 "$OLD_PID" 2>/dev/null || true
+    fi
+fi
+
+# --- 3. Start new server, record PID ---
+log "starting new server: $PYTHON_BIN app.py"
+nohup "$PYTHON_BIN" app.py > "$LOG_FILE" 2>&1 &
+NEW_PID=$!
+echo "$NEW_PID" > "$PID_FILE"
+log "new pid=$NEW_PID"
+
+# --- 4. Health check loop ---
+healthz_ok=0
+for ((i=1; i<=HEALTHZ_TIMEOUT_S; i++)); do
+    # Is the process still alive?
+    if ! kill -0 "$NEW_PID" 2>/dev/null; then
+        fail "server process $NEW_PID died during startup"
+    fi
+    code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 2 "$HEALTHZ_URL" 2>/dev/null || echo '000')"
+    if [[ "$code" == "200" ]]; then
+        healthz_ok=1
+        log "healthz OK after ${i}s"
+        break
+    fi
+    sleep 1
+done
+
+# --- 5. Rollback on failure ---
+if [[ "$healthz_ok" -ne 1 ]]; then
+    log "health check failed after ${HEALTHZ_TIMEOUT_S}s, rolling back"
+    kill "$NEW_PID" 2>/dev/null || true
+    sleep 1
+    kill -9 "$NEW_PID" 2>/dev/null || true
+    rm -f "$PID_FILE"
+
+    if [[ -n "$OLD_PID" ]]; then
+        log "attempting to restart previous server (old pid=$OLD_PID)"
+        nohup "$PYTHON_BIN" app.py > "$LOG_FILE" 2>&1 &
+        ROLLBACK_PID=$!
+        echo "$ROLLBACK_PID" > "$PID_FILE"
+        log "rollback pid=$ROLLBACK_PID"
+    fi
+    fail "deploy failed; rolled back (see log above)"
+fi
+
+log "deploy succeeded pid=$NEW_PID"
+curl -sS -o /dev/null -w 'local:%{http_code}\n' "$HEALTHZ_URL"
diff --git a/scripts/fetch_data.py b/scripts/fetch_data.py
new file mode 100644
index 0000000..eb8d1c9
--- /dev/null
+++ b/scripts/fetch_data.py
@@ -0,0 +1,516 @@
+#!/usr/bin/env python3
+"""Fetch GitHub project data and store it in a local SQLite database.
+
+Fetches repository metadata, the latest 5 releases, and the latest 10
+issues for real projects via the GitHub REST API (no token required),
+and generates mock data for the fictional "openclaw" project.
+
+Stage 4.3 (schema unfreeze): the script is now incremental-friendly.
+- Tables are created with CREATE TABLE IF NOT EXISTS (never DROPped).
+- Rows are written with INSERT OR REPLACE keyed on UNIQUE indexes:
+  projects.full_name, releases(project_id, tag_name),
+  issues(project_id, html_url).
+- releases/issues carry a `fetched_at` TEXT column (ISO-8601 UTC).
+- --incremental: skip repos whose latest fetched_at is fresher than
+  the repo's GitHub `updated_at` (no new upstream activity).
+- --project : only refresh a single project (by `name` or
+  `full_name`).
+
+Usage:
+    python3 fetch_data.py                          # full refresh, no drops
+    python3 fetch_data.py --incremental            # skip up-to-date repos
+    python3 fetch_data.py --project opencode       # single project
+    python3 fetch_data.py --incremental --project opencode
+"""
+
+import argparse
+import logging
+import os
+import sqlite3
+import sys
+from datetime import datetime, timezone
+
+import requests
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s [%(levelname)s] %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+GITHUB_API = "https://api.github.com"
+HEADERS = {
+    "Accept": "application/vnd.github+json",
+    "User-Agent": "opencode-blog-showcase-fetcher",
+}
+
+# Paths are relative to this script's location so the script is portable.
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+DB_PATH = os.path.normpath(
+    os.path.join(SCRIPT_DIR, "..", "..", "data", "projects.db")
+)
+
+REAL_REPOS = ["NousResearch/hermes-agent", "sst/opencode"]
+RELEASES_LIMIT = 5
+ISSUES_LIMIT = 10
+
+# ---------------------------------------------------------------------------
+# Mock data for the fictional "openclaw" project.
+# ---------------------------------------------------------------------------
+MOCK_PROJECT = {
+    "name": "openclaw",
+    "full_name": "openclaw/openclaw",
+    "description": "OpenClaw - a mock open-source CLI agent harness used for demo purposes.",
+    "stars": 1284,
+    "forks": 96,
+    "language": "Python",
+    "html_url": "https://github.com/openclaw/openclaw",
+    "created_at": "2025-01-15T10:00:00Z",
+    "updated_at": "2026-08-10T08:30:00Z",
+    "source": "mock",
+}
+
+MOCK_RELEASES = [
+    {
+        "tag_name": "v0.3.0",
+        "name": "v0.3.0 - Streaming tool calls",
+        "body": "Adds streaming support for tool calls, improved retry logic, "
+                "and a new `--dry-run` flag for the agent runner.",
+        "published_at": "2026-07-20T12:00:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/releases/tag/v0.3.0",
+    },
+    {
+        "tag_name": "v0.2.1",
+        "name": "v0.2.1 - Bugfix release",
+        "body": "Fixes a race condition in the scheduler and corrects "
+                "token-count reporting for long sessions.",
+        "published_at": "2026-06-05T09:00:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/releases/tag/v0.2.1",
+    },
+    {
+        "tag_name": "v0.2.0",
+        "name": "v0.2.0 - Plugin system",
+        "body": "Introduces the plugin system with sandboxed execution and "
+                "a declarative manifest format.",
+        "published_at": "2026-05-01T14:30:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/releases/tag/v0.2.0",
+    },
+    {
+        "tag_name": "v0.1.0",
+        "name": "v0.1.0 - Initial public release",
+        "body": "First public release of OpenClaw with basic agent loop, "
+                "file editing tools, and shell execution.",
+        "published_at": "2026-03-12T16:00:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/releases/tag/v0.1.0",
+    },
+]
+
+MOCK_ISSUES = [
+    {
+        "title": "Agent loop hangs when tool output exceeds context window",
+        "state": "open",
+        "created_at": "2026-08-01T11:20:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/issues/42",
+        "user": "mock-user-alice",
+    },
+    {
+        "title": "Add support for custom system prompts per project",
+        "state": "open",
+        "created_at": "2026-07-18T08:45:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/issues/38",
+        "user": "mock-user-bob",
+    },
+    {
+        "title": "Scheduler race condition under parallel subagents",
+        "state": "closed",
+        "created_at": "2026-06-02T15:10:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/issues/31",
+        "user": "mock-user-carol",
+    },
+    {
+        "title": "Documentation: missing example for plugin manifest",
+        "state": "closed",
+        "created_at": "2026-05-20T10:05:00Z",
+        "html_url": "https://github.com/openclaw/openclaw/issues/25",
+        "user": "mock-user-dave",
+    },
+]
+
+
+# ---------------------------------------------------------------------------
+# GitHub API helpers
+# ---------------------------------------------------------------------------
+def github_get(path, params=None):
+    """GET a GitHub API path, returning parsed JSON or None on failure."""
+    url = f"{GITHUB_API}{path}"
+    try:
+        resp = requests.get(url, headers=HEADERS, params=params, timeout=20)
+        if resp.status_code == 403 and "rate limit" in resp.text.lower():
+            logger.error("GitHub rate limit exceeded for %s", url)
+            return None
+        resp.raise_for_status()
+        return resp.json()
+    except requests.RequestException as exc:
+        logger.error("Request failed for %s: %s", url, exc)
+        return None
+
+
+def fetch_repo(full_name):
+    """Fetch normalized metadata for one repository."""
+    data = github_get(f"/repos/{full_name}")
+    if not data:
+        return None
+    return {
+        "name": data.get("name", full_name.split("/")[-1]),
+        "full_name": data.get("full_name", full_name),
+        "description": data.get("description") or "",
+        "stars": data.get("stargazers_count", 0),
+        "forks": data.get("forks_count", 0),
+        "language": data.get("language") or "",
+        "html_url": data.get("html_url", ""),
+        "created_at": data.get("created_at", ""),
+        "updated_at": data.get("updated_at", ""),
+        "source": "real",
+    }
+
+
+def fetch_releases(full_name, limit=RELEASES_LIMIT):
+    """Fetch the latest `limit` releases for a repository."""
+    data = github_get(f"/repos/{full_name}/releases", params={"per_page": limit})
+    if not data:
+        return []
+    releases = []
+    for rel in data[:limit]:
+        releases.append(
+            {
+                "tag_name": rel.get("tag_name", ""),
+                "name": rel.get("name") or rel.get("tag_name", ""),
+                "body": rel.get("body") or "",
+                "published_at": rel.get("published_at") or rel.get("created_at", ""),
+                "html_url": rel.get("html_url", ""),
+            }
+        )
+    return releases
+
+
+def fetch_issues(full_name, limit=ISSUES_LIMIT):
+    """Fetch the latest `limit` issues (excluding pull requests)."""
+    data = github_get(
+        f"/repos/{full_name}/issues",
+        params={"state": "all", "per_page": limit * 2, "sort": "created",
+                "direction": "desc"},
+    )
+    if not data:
+        return []
+    issues = []
+    for issue in data:
+        # The issues endpoint also returns pull requests; skip them.
+        if "pull_request" in issue:
+            continue
+        issues.append(
+            {
+                "title": issue.get("title", ""),
+                "state": issue.get("state", ""),
+                "created_at": issue.get("created_at", ""),
+                "html_url": issue.get("html_url", ""),
+                "user": (issue.get("user") or {}).get("login", ""),
+            }
+        )
+        if len(issues) >= limit:
+            break
+    return issues
+
+
+# ---------------------------------------------------------------------------
+# Database helpers
+# ---------------------------------------------------------------------------
+SCHEMA = """
+CREATE TABLE IF NOT EXISTS projects (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    name TEXT NOT NULL,
+    full_name TEXT NOT NULL UNIQUE,
+    description TEXT,
+    stars INTEGER DEFAULT 0,
+    forks INTEGER DEFAULT 0,
+    language TEXT,
+    html_url TEXT,
+    created_at TEXT,
+    updated_at TEXT,
+    source TEXT NOT NULL DEFAULT 'real',
+    starred INTEGER DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS releases (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    project_id INTEGER NOT NULL,
+    tag_name TEXT,
+    name TEXT,
+    body TEXT,
+    published_at TEXT,
+    html_url TEXT,
+    fetched_at TEXT,
+    FOREIGN KEY (project_id) REFERENCES projects (id)
+);
+
+CREATE TABLE IF NOT EXISTS issues (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    project_id INTEGER NOT NULL,
+    title TEXT NOT NULL,
+    state TEXT,
+    created_at TEXT,
+    html_url TEXT,
+    user TEXT,
+    fetched_at TEXT,
+    FOREIGN KEY (project_id) REFERENCES projects (id)
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_releases_project_tag
+    ON releases(project_id, tag_name);
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_issues_project_url
+    ON issues(project_id, html_url);
+"""
+
+# Columns added in Stage 4.3 via ALTER TABLE when upgrading an existing DB.
+_MIGRATIONS = (
+    ("projects", "starred", "ALTER TABLE projects ADD COLUMN starred INTEGER DEFAULT 0"),
+    ("releases", "fetched_at", "ALTER TABLE releases ADD COLUMN fetched_at TEXT"),
+    ("issues", "fetched_at", "ALTER TABLE issues ADD COLUMN fetched_at TEXT"),
+)
+
+
+def _column_exists(conn, table, column):
+    rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
+    return any(r[1] == column for r in rows)
+
+
+def init_db(db_path):
+    """Open (or create) the database, apply schema + migrations, return conn.
+
+    Never drops tables. Existing rows are preserved; missing columns are
+    added via ALTER TABLE (Stage 4.3 schema unfreeze).
+    """
+    os.makedirs(os.path.dirname(db_path), exist_ok=True)
+    conn = sqlite3.connect(db_path)
+    conn.execute("PRAGMA foreign_keys = ON;")
+    conn.executescript(SCHEMA)
+    for table, column, ddl in _MIGRATIONS:
+        if not _column_exists(conn, table, column):
+            logger.info("Migrating: %s", ddl)
+            conn.execute(ddl)
+    conn.commit()
+    logger.info("Initialized database at %s", db_path)
+    return conn
+
+
+def _utcnow_iso():
+    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+
+def upsert_project(conn, project):
+    """INSERT OR REPLACE a project row keyed on UNIQUE(full_name).
+
+    Returns the project id. Preserves the existing id (and any columns not
+    in the INSERT, like `starred`) by reusing the current row when present.
+    """
+    cur = conn.cursor()
+    existing = cur.execute(
+        "SELECT id FROM projects WHERE full_name = ?", (project["full_name"],)
+    ).fetchone()
+    if existing:
+        cur.execute(
+            """
+            UPDATE projects
+               SET name = ?, description = ?, stars = ?, forks = ?,
+                   language = ?, html_url = ?, created_at = ?, updated_at = ?,
+                   source = ?
+             WHERE full_name = ?
+            """,
+            (
+                project["name"], project["description"], project["stars"],
+                project["forks"], project["language"], project["html_url"],
+                project["created_at"], project["updated_at"], project["source"],
+                project["full_name"],
+            ),
+        )
+        return existing[0]
+    cur.execute(
+        """
+        INSERT OR REPLACE INTO projects
+            (name, full_name, description, stars, forks, language,
+             html_url, created_at, updated_at, source)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+        """,
+        (
+            project["name"], project["full_name"], project["description"],
+            project["stars"], project["forks"], project["language"],
+            project["html_url"], project["created_at"], project["updated_at"],
+            project["source"],
+        ),
+    )
+    return cur.lastrowid
+
+
+def upsert_releases(conn, project_id, releases, fetched_at):
+    """INSERT OR REPLACE releases keyed on UNIQUE(project_id, tag_name)."""
+    conn.executemany(
+        """
+        INSERT OR REPLACE INTO releases
+            (project_id, tag_name, name, body, published_at, html_url, fetched_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?)
+        """,
+        [
+            (project_id, r["tag_name"], r["name"], r["body"],
+             r["published_at"], r["html_url"], fetched_at)
+            for r in releases
+        ],
+    )
+
+
+def upsert_issues(conn, project_id, issues, fetched_at):
+    """INSERT OR REPLACE issues keyed on UNIQUE(project_id, html_url)."""
+    conn.executemany(
+        """
+        INSERT OR REPLACE INTO issues
+            (project_id, title, state, created_at, html_url, user, fetched_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?)
+        """,
+        [
+            (project_id, i["title"], i["state"], i["created_at"],
+             i["html_url"], i["user"], fetched_at)
+            for i in issues
+        ],
+    )
+
+
+def get_last_fetched_at(conn, full_name):
+    """Most recent fetched_at across releases+issues for a project, or None."""
+    row = conn.execute(
+        """
+        SELECT MAX(fetched_at) FROM (
+            SELECT r.fetched_at AS fetched_at
+              FROM releases r
+              JOIN projects p ON p.id = r.project_id
+             WHERE p.full_name = ?
+            UNION ALL
+            SELECT i.fetched_at AS fetched_at
+              FROM issues i
+              JOIN projects p ON p.id = i.project_id
+             WHERE p.full_name = ?
+        )
+        """,
+        (full_name, full_name),
+    ).fetchone()
+    return row[0] if row else None
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+def _resolve_targets(project_filter):
+    """Return list of (kind, payload) tuples to process.
+
+    kind == "real"  -> payload is a GitHub "owner/repo" full_name
+    kind == "mock"  -> payload is the mock project dict
+    """
+    if project_filter is None:
+        return ([("real", r) for r in REAL_REPOS]
+                + [("mock", MOCK_PROJECT)])
+
+    needle = project_filter.lower()
+    targets = []
+    for full_name in REAL_REPOS:
+        short = full_name.split("/")[-1].lower()
+        if needle in (full_name.lower(), short):
+            targets.append(("real", full_name))
+    if needle in (MOCK_PROJECT["full_name"].lower(), MOCK_PROJECT["name"].lower()):
+        targets.append(("mock", MOCK_PROJECT))
+    if not targets:
+        logger.error("No project matches --project %r", project_filter)
+        sys.exit(2)
+    return targets
+
+
+def _should_skip_incremental(conn, full_name, project):
+    """Incremental mode: skip when upstream hasn't moved since last fetch.
+
+    GitHub's `updated_at` on the repo is bumped by pushes/releases, so if
+    our last fetched_at is newer, there is nothing new to pull.
+    """
+    last = get_last_fetched_at(conn, full_name)
+    if not last:
+        return False
+    upstream = project.get("updated_at") or ""
+    if upstream and last >= upstream:
+        logger.info(
+            "  -> skip %s (last fetched %s >= upstream updated_at %s)",
+            full_name, last, upstream,
+        )
+        return True
+    return False
+
+
+def main(argv=None):
+    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
+    parser.add_argument(
+        "--incremental",
+        action="store_true",
+        help="Skip repos whose last fetched_at is newer than GitHub updated_at.",
+    )
+    parser.add_argument(
+        "--project",
+        metavar="NAME",
+        default=None,
+        help="Only refresh the named project (short name or owner/repo).",
+    )
+    args = parser.parse_args(argv)
+
+    conn = init_db(DB_PATH)
+    fetched_at = _utcnow_iso()
+    try:
+        for kind, payload in _resolve_targets(args.project):
+            if kind == "mock":
+                logger.info("Upserting mock project: %s", payload["name"])
+                pid = upsert_project(conn, payload)
+                upsert_releases(conn, pid, MOCK_RELEASES, fetched_at)
+                upsert_issues(conn, pid, MOCK_ISSUES, fetched_at)
+                continue
+
+            full_name = payload
+            logger.info("Fetching repo: %s", full_name)
+            project = fetch_repo(full_name)
+            if project is None:
+                logger.error("Skipping %s (fetch failed)", full_name)
+                continue
+            if args.incremental and _should_skip_incremental(
+                conn, full_name, project
+            ):
+                continue
+            releases = fetch_releases(full_name)
+            issues = fetch_issues(full_name)
+            logger.info(
+                "  -> %d releases, %d issues", len(releases), len(issues)
+            )
+            pid = upsert_project(conn, project)
+            upsert_releases(conn, pid, releases, fetched_at)
+            upsert_issues(conn, pid, issues, fetched_at)
+
+        conn.commit()
+
+        # Summary.
+        cur = conn.cursor()
+        for table in ("projects", "releases", "issues"):
+            count = cur.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
+            logger.info("Table %-10s: %d rows", table, count)
+    except sqlite3.Error as exc:
+        conn.rollback()
+        logger.error("Database error: %s", exc)
+        sys.exit(1)
+    finally:
+        conn.close()
+
+    logger.info("Done. Database written to %s", DB_PATH)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/install_watchdog_cron.sh b/scripts/install_watchdog_cron.sh
new file mode 100755
index 0000000..a6e5928
--- /dev/null
+++ b/scripts/install_watchdog_cron.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# Idempotent: 重复跑不会重复加 cron 条目 (用 crontab -l 检查已存在则跳过)
+# 安装 AI Agent 升级日报的每日 cron 任务 (幂等: 已存在则跳过)。
+# 每天 09:00 (Asia/Shanghai) 运行 scripts/daily_watchdog.py,
+# 输出追加到 ~/opencode-blog-showcase/logs/daily-agent-watchdog.log。
+# 绝不覆盖现有 crontab 条目。
+set -euo pipefail
+
+ENTRY='0 9 * * * cd /home/yi/opencode-blog-showcase/blog-app && /usr/bin/python3 scripts/daily_watchdog.py >> /home/yi/opencode-blog-showcase/logs/daily-agent-watchdog.log 2>&1'
+
+if crontab -l 2>/dev/null | grep -F "daily_watchdog.py" >/dev/null; then
+    echo "already installed, skipping"
+    exit 0
+fi
+
+(crontab -l 2>/dev/null; echo "$ENTRY") | crontab -
+echo "installed"
diff --git a/security.py b/security.py
new file mode 100644
index 0000000..e93df45
--- /dev/null
+++ b/security.py
@@ -0,0 +1,36 @@
+"""CSRF protection (session-scoped token, no external dependency)."""
+
+import hmac
+import logging
+import secrets
+
+from flask import abort, request, session
+
+logger = logging.getLogger(__name__)
+
+
+def get_csrf_token():
+    """Return the per-session CSRF token, generating one on first use."""
+    token = session.get("_csrf_token")
+    if not token:
+        token = secrets.token_hex(32)
+        session["_csrf_token"] = token
+    return token
+
+
+def validate_csrf():
+    """Abort 403 if the form CSRF token doesn't match the session token."""
+    expected = session.get("_csrf_token")
+    provided = request.form.get("csrf_token", "")
+    if not expected or not provided or not hmac.compare_digest(expected, provided):
+        logger.warning(
+            "CSRF validation failed for %s from %s",
+            request.path,
+            request.remote_addr,
+        )
+        abort(403)
+
+
+def init_app(app):
+    """Expose csrf_token() to Jinja templates as a global."""
+    app.jinja_env.globals["csrf_token"] = get_csrf_token
diff --git a/services/__init__.py b/services/__init__.py
new file mode 100644
index 0000000..c564e86
--- /dev/null
+++ b/services/__init__.py
@@ -0,0 +1,3 @@
+from . import analysis, watchdog
+
+__all__ = ["analysis", "watchdog"]
diff --git a/services/analysis.py b/services/analysis.py
new file mode 100644
index 0000000..a2565ca
--- /dev/null
+++ b/services/analysis.py
@@ -0,0 +1,346 @@
+"""Analysis service: context building, LLM prompt assembly, SSE event framing.
+
+Pure functions — no Flask imports. All DB access goes through an explicit
+sqlite3 connection argument (row_factory=sqlite3.Row expected), so this
+module can also be imported by scripts/daily_watchdog.py without an app
+context. Error values are plain `(dict, status)` tuples; routes jsonify them.
+"""
+
+import json
+import logging
+import time
+
+import requests as http_requests
+
+import config
+from repositories import issue_repo, project_repo, release_repo
+
+logger = logging.getLogger(__name__)
+
+DELIM_REASONING = "===REASONING==="
+DELIM_CONCLUSION = "===CONCLUSION==="
+
+PRESET_MAP = {
+    "activity": "请基于最近 release + issue 评估项目活跃度 (1-10 分) 并给出证据",
+    "evolution": "请按时间列 5 个关键 release 的变更重点, 给出演进方向判断",
+    "issues_hot": "请把 issue 按主题聚类, 列 top 3 热点, 每条给 1 句描述",
+}
+
+PRESET_LIST = [
+    {"id": "activity", "label": "项目活跃度评估", "icon": "📊", "description": "基于最近 release + issue 评估项目活跃度 (1-10) + 证据"},
+    {"id": "evolution", "label": "版本演进分析", "icon": "🚀", "description": "按时间列 5 个关键 release 的变更重点, 给演进方向判断"},
+    {"id": "issues_hot", "label": "Issue 热点聚类", "icon": "🔥", "description": "把 issue 按主题聚类, 列 top 3 热点 + 每条 1 句描述"},
+]
+
+SYSTEM_PROMPT = "你是 AI 项目分析助手, 基于以下数据进行分析。请用中文回答。"
+
+# Prompt trimming budgets live in config (env-overridable, shared with
+# scripts/daily_watchdog.py). Re-exported for convenient imports/tests.
+LLM_BODY_LIMIT = config.LLM_BODY_LIMIT
+LLM_FIELD_LIMIT = config.LLM_FIELD_LIMIT
+LLM_MESSAGE_LIMIT = config.LLM_MESSAGE_LIMIT
+LLM_TOTAL_LIMIT = config.LLM_TOTAL_LIMIT
+
+FORMAT_REQ = (
+    "要求: 严格按以下格式输出, 不要输出其他任何标记:\n"
+    "===REASONING===\n"
+    "(这里写你的分析推理过程: 数据观察、初步判断、深入推理)\n"
+    "===CONCLUSION===\n"
+    "(这里写最终结论: 先给一句总结论, 再用 2-3 句概括关键依据, 不少于 50 字)"
+)
+
+
+def get_presets():
+    return {"presets": PRESET_LIST}
+
+
+def resolve_question(question: str, preset: str) -> str:
+    """If only a preset id is given, map it to the canonical question text."""
+    if not question and preset:
+        return PRESET_MAP.get(preset, preset)
+    return question
+
+
+def build_context(scope: str, db_conn):
+    """Build the context dict for a scope. Single source of truth used by
+    /api/context/ and /api/analyze/stream.
+
+    Returns (result_dict_or_None, error_or_None). The error is a plain
+    (payload_dict, status_code) tuple — the route jsonifies it.
+    """
+    result = {
+        "scope_type": scope,
+        "project_id": None,
+        "data_summary": "",
+        "releases": [],
+        "issues": [],
+        "stats": {},
+    }
+
+    if scope == "home":
+        stats = project_repo.get_stats(db_conn)
+        result["stats"] = stats
+        result["data_summary"] = (
+            f"共 {stats['total_projects']} 个项目, {stats['total_releases']} 个 release, "
+            f"{stats['total_issues']} 个 issue, {stats['total_stars']} stars"
+        )
+        result["releases"] = release_repo.list_recent_releases_slim(db_conn, limit=5)
+        result["issues"] = issue_repo.list_recent_issues(db_conn, limit=10)
+    elif scope.startswith("project/"):
+        try:
+            pid = int(scope.split("/")[1])
+        except (IndexError, ValueError):
+            return None, ({"error": "invalid project scope"}, 400)
+        result["project_id"] = pid
+        proj = project_repo.get_project(db_conn, pid)
+        if proj is None:
+            return None, ({"error": "project not found"}, 404)
+        result["data_summary"] = (
+            f"项目: {proj['name']} ({proj.get('full_name', '')}), {proj.get('stars', 0)} stars"
+        )
+        result["releases"] = release_repo.list_releases_for_project_slim(db_conn, pid, limit=5)
+        result["issues"] = issue_repo.list_issues_for_project(db_conn, pid, limit=10)
+    elif scope == "projects":
+        projects = project_repo.list_project_summaries(db_conn)
+        result["data_summary"] = f"共 {len(projects)} 个项目"
+        result["releases"] = release_repo.list_recent_releases_slim(db_conn, limit=5)
+        result["issues"] = issue_repo.list_recent_issues(db_conn, limit=10)
+    elif scope.startswith("search/"):
+        query = scope[7:]
+        result["data_summary"] = f"搜索: {query}"
+        result["releases"] = release_repo.search_releases_slim(db_conn, query, limit=5)
+        result["issues"] = issue_repo.search_issues(db_conn, query, limit=10)
+    else:
+        return None, ({"error": "unknown scope"}, 400)
+
+    return result, None
+
+
+def _truncate(text, limit):
+    """Truncate a string to limit chars, appending '...' when cut."""
+    if not isinstance(text, str):
+        return text
+    if len(text) <= limit:
+        return text
+    return text[:limit] + "..."
+
+
+def _slim_for_llm(data):
+    """Return a trimmed copy of the context dict for the LLM prompt.
+
+    Keeps only the fields the model needs and truncates long bodies so the
+    serialized JSON stays small enough for fast prefill.
+    """
+    slim = dict(data)
+
+    if isinstance(data.get("releases"), list):
+        slim_releases = []
+        for rel in data["releases"]:
+            if not isinstance(rel, dict):
+                slim_releases.append(rel)
+                continue
+            slim_releases.append({
+                "tag_name": rel.get("tag_name"),
+                "name": rel.get("name"),
+                "published_at": rel.get("published_at"),
+                "body": _truncate(rel.get("body", ""), LLM_BODY_LIMIT),
+            })
+        slim["releases"] = slim_releases
+
+    if isinstance(data.get("issues"), list):
+        slim_issues = []
+        for issue in data["issues"]:
+            if not isinstance(issue, dict):
+                slim_issues.append(issue)
+                continue
+            item = {
+                "title": issue.get("title"),
+                "state": issue.get("state"),
+                "created_at": issue.get("created_at"),
+            }
+            if "body" in issue:
+                item["body"] = _truncate(issue.get("body") or "", LLM_FIELD_LIMIT)
+            slim_issues.append(item)
+        slim["issues"] = slim_issues
+
+    if isinstance(data.get("messages"), list):
+        slim["messages"] = [
+            _truncate(m, LLM_MESSAGE_LIMIT) if isinstance(m, str)
+            else {**m, "content": _truncate(m.get("content", ""), LLM_MESSAGE_LIMIT)} if isinstance(m, dict)
+            else m
+            for m in data["messages"]
+        ]
+
+    serialized = json.dumps(slim, ensure_ascii=False, default=str)
+    if len(serialized) > LLM_TOTAL_LIMIT:
+        if isinstance(slim.get("releases"), list):
+            slim["releases"] = slim["releases"][:3]
+        if isinstance(slim.get("issues"), list):
+            slim["issues"] = slim["issues"][:5]
+        logger.info(
+            "_slim_for_llm: payload still >%d chars, cut to %d releases / %d issues",
+            LLM_TOTAL_LIMIT,
+            len(slim.get("releases", [])),
+            len(slim.get("issues", [])),
+        )
+    return slim
+
+
+def build_prompts(data, question):
+    """Assemble system/user messages + the human-readable prompt display."""
+    slim_data = _slim_for_llm(data)
+    data_json = json.dumps(slim_data, ensure_ascii=False, default=str)
+    user_prompt = f"分析数据: {data_json}\n\n用户问题: {question}\n\n{FORMAT_REQ}"
+    messages = [
+        {"role": "system", "content": SYSTEM_PROMPT},
+        {"role": "user", "content": user_prompt},
+    ]
+    full_prompt_display = f"[SYSTEM]\n{SYSTEM_PROMPT}\n\n[USER]\n{user_prompt}"
+    return messages, full_prompt_display
+
+
+def _sse(event, payload):
+    return f"event: {event}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
+
+
+def stream_analysis_events(data, question):
+    """Generator yielding SSE frames: step/token/section_start/error/complete.
+
+    Pure of Flask: safe to consume after the request context has closed,
+    as long as `data` and `question` were prepared inside the view.
+    """
+    start_time = time.time()
+    data_summary = data["data_summary"]
+    messages, full_prompt_display = build_prompts(data, question)
+
+    yield _sse("step", {"step": "context", "content": f"已收集上下文: {data_summary}"})
+    yield _sse("step", {"step": "prompt", "content": full_prompt_display})
+
+    def emit_token(t):
+        return _sse("token", {"text": t})
+
+    try:
+        resp = http_requests.post(
+            f"{config.LLM_URL}/chat/completions",
+            json={"model": config.LLM_MODEL, "messages": messages, "stream": True, "max_tokens": 1024},
+            stream=True,
+            timeout=config.LLM_TIMEOUT,
+        )
+        resp.raise_for_status()
+
+        last_keepalive = time.time()
+        holdback = max(len(DELIM_REASONING), len(DELIM_CONCLUSION)) - 1
+        pending = ""
+        reasoning_started = False
+        conclusion_started = False
+
+        for line in resp.iter_lines():
+            if not line:
+                continue
+            line = line.decode("utf-8") if isinstance(line, bytes) else line
+
+            now = time.time()
+            if now - last_keepalive > 15:
+                yield ": keepalive\n\n"
+                last_keepalive = now
+
+            if line.startswith("data: "):
+                chunk_data = line[6:]
+                if chunk_data.strip() == "[DONE]":
+                    break
+                try:
+                    chunk = json.loads(chunk_data)
+                    delta = chunk.get("choices", [{}])[0].get("delta", {})
+                    token_text = delta.get("content", "")
+                except json.JSONDecodeError:
+                    continue
+                if not token_text:
+                    continue
+
+                pending += token_text
+
+                # 滚动 buffer: 分隔符可能跨 chunk, 保留尾部前缀再下发
+                while pending:
+                    idx_r = pending.find(DELIM_REASONING)
+                    idx_c = pending.find(DELIM_CONCLUSION)
+                    idx = -1
+                    delim = None
+                    if idx_r != -1 and (idx_c == -1 or idx_r < idx_c):
+                        idx, delim = idx_r, DELIM_REASONING
+                    elif idx_c != -1:
+                        idx, delim = idx_c, DELIM_CONCLUSION
+
+                    if idx != -1:
+                        if idx > 0:
+                            if not reasoning_started:
+                                reasoning_started = True
+                                yield "event: reasoning_start\ndata: {}\n\n"
+                            yield emit_token(pending[:idx])
+                        pending = pending[idx + len(delim):]
+                        if delim == DELIM_REASONING:
+                            if not reasoning_started:
+                                reasoning_started = True
+                                yield "event: reasoning_start\ndata: {}\n\n"
+                        else:
+                            if not reasoning_started:
+                                reasoning_started = True
+                                yield "event: reasoning_start\ndata: {}\n\n"
+                            if not conclusion_started:
+                                conclusion_started = True
+                                yield "event: conclusion_start\ndata: {}\n\n"
+                        continue
+
+                    keep = 0
+                    tail = pending[-holdback:] if len(pending) > holdback else pending
+                    for d in (DELIM_REASONING, DELIM_CONCLUSION):
+                        for k in range(min(len(tail), len(d) - 1), 0, -1):
+                            if tail.endswith(d[:k]):
+                                keep = max(keep, k)
+                                break
+                    safe = pending[:len(pending) - keep] if keep else pending
+                    pending = pending[len(safe):]
+                    if not safe:
+                        break
+                    if not reasoning_started:
+                        reasoning_started = True
+                        yield "event: reasoning_start\ndata: {}\n\n"
+                    yield emit_token(safe)
+
+        if pending:
+            if not reasoning_started:
+                reasoning_started = True
+                yield "event: reasoning_start\ndata: {}\n\n"
+            yield emit_token(pending)
+
+    except Exception as e:
+        yield _sse("error", {"error": f"LLM call failed: {str(e)}"})
+        return
+
+    # 防御: 模型没给分隔符时补发 section_start, 保证前端 tab 一定有入口
+    if not reasoning_started:
+        yield "event: reasoning_start\ndata: {}\n\n"
+    if not conclusion_started:
+        yield "event: conclusion_start\ndata: {}\n\n"
+
+    elapsed = round(time.time() - start_time, 1)
+    yield _sse("step", {"step": "complete", "content": f"分析完成, 总耗时 {elapsed} 秒"})
+
+
+def summarize_with_llm(messages, model=None, timeout=None):
+    """Non-streaming LLM call returning the assistant content string.
+
+    Kept Flask-free so scripts/daily_watchdog.py (and future cron jobs) can
+    reuse the same endpoint configuration.
+    """
+    resp = http_requests.post(
+        f"{config.LLM_URL}/chat/completions",
+        json={
+            "model": model or config.LLM_MODEL,
+            "messages": messages,
+            "stream": False,
+            "max_tokens": 1024,
+        },
+        timeout=timeout or config.LLM_TIMEOUT,
+    )
+    resp.raise_for_status()
+    return resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
diff --git a/services/watchdog.py b/services/watchdog.py
new file mode 100644
index 0000000..626a9df
--- /dev/null
+++ b/services/watchdog.py
@@ -0,0 +1,96 @@
+"""Watchdog service: report loading, process checks, trigger bookkeeping.
+
+Mostly pure (no Flask imports). `trigger_watchdog_run` uses subprocess only;
+the caller (route) is responsible for flash/redirect.
+"""
+
+import logging
+import os
+import sqlite3
+import subprocess
+import time
+
+import config
+from repositories import watchdog_repo
+
+logger = logging.getLogger(__name__)
+
+# 简单防重: 模块级变量记录上次触发时间戳, cooldown 秒内重复 POST 返回失败
+_last_trigger_ts = 0.0
+
+
+def parse_days(raw, default=7):
+    """Clamp days query param to 1-90; non-numeric falls back to default."""
+    try:
+        value = int(raw)
+    except (TypeError, ValueError):
+        return default
+    return max(1, min(90, value))
+
+
+def load_watchdog_reports(watchdog_conn, days=7):
+    """Return recent watchdog reports; [] when db/table missing or unreadable."""
+    if not os.path.exists(config.WATCHDOG_DB):
+        return []
+    try:
+        return watchdog_repo.list_reports(watchdog_conn, days=days)
+    except sqlite3.Error:
+        logger.exception("failed to read watchdog db")
+        return []
+
+
+def is_watchdog_running():
+    """True if a python process running daily_watchdog.py already exists."""
+    try:
+        out = subprocess.run(
+            ["pgrep", "-f", "daily_watchdog.py"],
+            capture_output=True, text=True, timeout=5,
+        ).stdout
+    except Exception:
+        logger.exception("watchdog pgrep failed")
+        return False
+    for pid in out.split():
+        try:
+            with open(f"/proc/{pid}/cmdline", "rb") as f:
+                argv0 = f.read().split(b"\x00")[0].decode(errors="replace")
+        except OSError:
+            continue
+        if os.path.basename(argv0).startswith("python"):
+            return True
+    return False
+
+
+def trigger_watchdog_run(cooldown=None):
+    """Attempt to launch scripts/daily_watchdog.py in the background.
+
+    Returns (ok, status, message):
+      - (True,  "started",  msg)  launched successfully
+      - (False, "cooldown", msg)  hit the anti-repeat cooldown
+      - (False, "running",  msg)  an instance is already running
+      - (False, "error",    msg)  subprocess launch failed
+    """
+    global _last_trigger_ts
+    cooldown = config.WATCHDOG_RUN_COOLDOWN if cooldown is None else cooldown
+
+    now = time.time()
+    if now - _last_trigger_ts < cooldown:
+        return False, "cooldown", "操作太频繁, 请稍后再试"
+    _last_trigger_ts = now
+
+    if is_watchdog_running():
+        return False, "running", "daily_watchdog.py 已在运行中, 本次触发被忽略"
+
+    try:
+        os.makedirs(os.path.dirname(config.WATCHDOG_RUN_LOG), exist_ok=True)
+        log_f = open(config.WATCHDOG_RUN_LOG, "a", encoding="utf-8")
+        env = dict(os.environ)
+        env["HOME"] = "/home/yi"
+        subprocess.Popen(
+            ["/usr/bin/python3", config.WATCHDOG_SCRIPT],
+            stdout=log_f, stderr=subprocess.STDOUT,
+            cwd=config.BASE_DIR, env=env, start_new_session=True,
+        )
+        return True, "started", "已触发, 约 1-2 分钟后刷新页面查看新报告"
+    except Exception as exc:
+        logger.exception("failed to launch daily_watchdog.py")
+        return False, "error", f"触发失败: {exc}"
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..8be0238
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,153 @@
+/* Custom styles for AI Agent project tracker. */
+
+body {
+    background-color: #f5f7fa;
+}
+
+/* Card shadow + hover lift */
+.card {
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+    transition: transform 0.15s ease, box-shadow 0.15s ease;
+    border: none;
+}
+
+.card.project-card:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.14);
+    cursor: pointer;
+}
+
+/* Stat cards on the index page */
+.stat-card .stat-number {
+    font-size: 2rem;
+    font-weight: 700;
+}
+
+.stat-card .stat-label {
+    color: #6c757d;
+    font-size: 0.9rem;
+}
+
+/* Release timeline on detail page */
+.timeline {
+    position: relative;
+    padding-left: 1.5rem;
+    border-left: 3px solid #0d6efd;
+}
+
+.timeline .list-group-item {
+    position: relative;
+    border: none;
+    border-bottom: 1px solid #e9ecef;
+    background: transparent;
+}
+
+.timeline .list-group-item::before {
+    content: "";
+    position: absolute;
+    left: -1.72rem;
+    top: 1.1rem;
+    width: 12px;
+    height: 12px;
+    border-radius: 50%;
+    background-color: #0d6efd;
+    border: 2px solid #fff;
+    box-shadow: 0 0 0 2px #0d6efd;
+}
+
+/* Search input focus glow */
+.nav-search-input:focus,
+.search-input:focus {
+    box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+    border-color: #86b7fe;
+}
+
+/* Badge spacing in project cards */
+.project-card .badge {
+    font-weight: 500;
+}
+
+/* Empty state text */
+.empty-hint {
+    color: #6c757d;
+    padding: 1.5rem 0;
+    text-align: center;
+    animation: empty-hint-pulse 1.6s ease-in-out infinite;
+}
+
+@keyframes empty-hint-pulse {
+    0%,
+    100% {
+        opacity: 1;
+    }
+    50% {
+        opacity: 0.35;
+    }
+}
+
+.empty-hint-error {
+    animation: none;
+    color: #dc3545;
+}
+
+/* Issue state badges */
+.badge-state-open {
+    background-color: #198754;
+}
+
+.badge-state-closed {
+    background-color: #6c757d;
+}
+
+/* Watchdog daily report badges + rendered markdown */
+.badge-state-partial {
+    background-color: #ffc107;
+    color: #000;
+}
+
+.watchdog-md {
+    font-size: 0.95rem;
+    line-height: 1.7;
+    word-break: break-word;
+}
+
+.watchdog-md h2 {
+    font-size: 1.15rem;
+    margin-top: 1.5rem;
+    padding-left: .6rem;
+    border-left: 3px solid #0d6efd;
+}
+
+.watchdog-md h2.wd-starred {
+    border-left-color: #198754;
+}
+
+.watchdog-md blockquote {
+    background: #f8f9fa;
+    border-left: 3px solid #dee2e6;
+    padding: .5rem .8rem;
+    color: #495057;
+    font-size: .9rem;
+}
+
+.watchdog-md code {
+    background: #f8f9fa;
+    padding: .1em .35em;
+    border-radius: 4px;
+}
+
+.watchdog-md pre {
+    background: #f8f9fa;
+    padding: .6rem .8rem;
+    border-radius: 4px;
+    overflow-x: auto;
+}
+
+.watchdog-md ul,
+.watchdog-md ol {
+    padding-left: 1.5rem;
+}
+
+.watchdog-md p {
+    margin-bottom: .6rem;
+}
diff --git a/static/js/ai-panel.js b/static/js/ai-panel.js
new file mode 100644
index 0000000..9612c61
--- /dev/null
+++ b/static/js/ai-panel.js
@@ -0,0 +1,294 @@
+/*
+ * ai-panel.js — 全站浮动 AI 分析面板
+ * ES module (base.html 以 
+{% endblock %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..c0ca184
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,72 @@
+
+
+
+    
+    
+    {% block title %}AI Agent 项目追踪{% endblock %}
+    
+    
+
+
+    
+
+    
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ +
+
+ AI Agent 项目追踪 - Powered by Flask +
+
+ + + + + {% block scripts %}{% endblock %} + + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5900084 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} +{% block title %}首页 - AI Agent 项目追踪{% endblock %} + +{% block content %} +

项目总览

+ + +
+
+
+
+
-
+
项目数
+
+
+
+
+
+
+
-
+
Release 数
+
+
+
+
+
+
+
-
+
Issue 数
+
+
+
+
+
+
+
-
+
总 Stars
+
+
+
+
+ +

项目 Dashboard

+
+
加载中…
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/project_detail.html b/templates/project_detail.html new file mode 100644 index 0000000..a3363f0 --- /dev/null +++ b/templates/project_detail.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}项目详情 - AI Agent 项目追踪{% endblock %} + +{% block content %} +
+
加载项目信息…
+
+ +
+
+

Release 时间线

+
+
    +
  • 加载中…
  • +
+
+
+
+

最近 Issues

+
+ + + + + + + + + + + + +
标题状态创建时间用户
加载中…
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/projects.html b/templates/projects.html new file mode 100644 index 0000000..8d399a4 --- /dev/null +++ b/templates/projects.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}项目列表 - AI Agent 项目追踪{% endblock %} + +{% block content %} +
+

项目列表

+
+
+ + + +
+ +
+
+ +
+
加载中…
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..78265b7 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %}全局搜索 - AI Agent 项目追踪{% endblock %} + +{% block content %} +

全局搜索

+ +
+
+ +
+
+ +
输入关键词开始搜索
+ +
+

匹配的项目 0

+
+ +

匹配的 Releases 0

+
    + +

    匹配的 Issues 0

    +
      +
      +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/watchdog.html b/templates/watchdog.html new file mode 100644 index 0000000..d12c9cd --- /dev/null +++ b/templates/watchdog.html @@ -0,0 +1,159 @@ +{% extends "base.html" %} +{% block title %}日报 - AI Agent 项目追踪{% endblock %} + +{% block content %} +
      +
      +

      升级迭代日报

      + 每天 09:00 自动更新 · 最新报告: {{ reports[0].date if reports else "无" }} +
      +
      + +
      + + +
      +
      +
      +
      + + 报告生成中, 通常 1-2 分钟, 完成后页面自动刷新… +
      + +{% if not reports %} +
      + 暂无报告, 点击立即运行生成第一份日报 +
      +{% endif %} + +{% if reports and reports[0].meta %} +{% set m = reports[0].meta %} +{% set projs = m.get("projects", {}) %} +
      +
      +
      {{ projs|length }}
      +
      监控项目
      +
      +
      {{ projs.values()|selectattr('status','equalto','ok')|list|length }}
      +
      ✓ 正常
      +
      +
      {{ projs.values()|selectattr('status','equalto','partial')|list|length }}
      +
      ⚠ 部分
      +
      +
      {{ m.get("sources_failed", [])|length }}
      +
      ✗ 失败数据源
      +
      +{% endif %} + +{% for r in reports %} +
      +
      +
      + {{ r.date }} + 生成于 {{ r.created_at[:19] }} +
      + {% set prev = reports[loop.index] if not loop.last else none %} + {% if prev and r.meta and prev.meta %} + {% set cur_failed = r.meta.get("sources_failed", []) %} + {% set prev_failed = prev.meta.get("sources_failed", []) %} + {% set fixed = prev_failed|reject('in', cur_failed)|list %} + {% set broke = cur_failed|reject('in', prev_failed)|list %} + + {% if fixed %}✅ 恢复: {{ fixed|join(", ") }}{% endif %} + {% if broke %}⚠ 新增失败: {{ broke|join(", ") }}{% endif %} + {% if not fixed and not broke %}与昨日相同{% endif %} + + {% endif %} +
      +
      + {% if r.meta %} +
      + {% for name, info in r.meta.get("projects", {}).items() %} + {% set st = info.get('status', 'unknown') %} + {% set is_starred = name in starred %} + + {{ name }}{% if is_starred %} ★{% endif %} + {% if st=='ok' %} ✓{% elif st=='partial' %} ⚠{% else %} ✗{% endif %} + + {% endfor %} + {% for s in r.meta.get("sources_failed", []) %} + {{ s }} ✗ + {% endfor %} +
      + {% endif %} +
      + 查看报告内容 +
      +
      +
      +
      +{% endfor %} +{% endblock %} + +{% block scripts %} + + +{% endblock %}