Fetching metadata from the HF Docker repository...
Oct 27 2 days ago 14:01 Update README.md 11:00 111 Oct 19 10 days ago 07:00 xiufu shijian xunhuan bei suoding de genyin: qingqiu relu bu zai xie ku xiaolongmr
堆栈证据:事件循环线程卡在
chat_room.list_direct_conversations -> require_identity
-> authenticate -> _update_items_locked -> update_auth_keys_locked
即「async 路由里同步鉴权,鉴权里同步写整张 auth_keys 表」。
三处改动:
1. authenticate 热路径零存储 IO
last_used_at 只更新内存快照并记脏,由后台线程每 30 秒批量合并落库。
事件循环再也不等待存储锁。
2. 缓存未命中也不回读存储
未知/失效 token 以前会 _reload_locked() 读全表,把事件循环拖进数据库。
现在直接返回 401,回读交给后台线程兜底(单副本下内存快照即权威)。
3. update_auth_keys_locked 只写变化的行
以前是无条件 row.data = payload,等于每次改一条都把整表标记 dirty
并逐行 UPDATE + commit。图片任务线程并发结算积分时,
SQLite 写锁被占用数百毫秒,事件循环只能排队。
附带:去掉 21 处 with self._lock: 包 _update_items_locked 的反向锁嵌套,
锁顺序统一为「存储 -> 鉴权」,消除再次 ABBA 的可能。 06:40 xiufu jiandeng yu shangdian huxiang dengdai de sizi suo; tanzhen buzai zhudong tuichu xiaolongmr
06:34 jiaru shijian xunhuan qiasi tanzhen: dump duizhan bing zidong chongqi xiaolongmr
06:27 jiandeng last_used_at xiechu jieliu, bimian mei ci qingqiu zhongxie zheng zhang jian quan biao xiaolongmr
06:17 tupian duxie ga dao duli xianchengchi, bing jiaru shijian xunhuan kadun tanzhen xiaolongmr
06:05 yongjiu shibai de fenxiang jilu zhijie tuo li duilie, bu zai rang shangyou chongshi xiaolongmr
05:56 shangyou 401 403 zhongzhi chongshi, bimian chongshi fengbao tuo kua rongqi xiaolongmr
Oct 17 12 days ago 08:30 zidong shibie fumian tishici quduan, wu xu shoudong jia fangxing duanyu xiaolongmr
08:18 zuhe guize gai wei tong ju pipei; fangxing duanyu he changjing kaiguan ke zai shezhi ye peizhi xiaolongmr
08:00 fuzhuang silie bu zai bei pan wei xiexing; bufang fuzhuang yujing wushang xiaolongmr
Oct 16 13 days ago 08:43 tupian he jiankang jiekou gai wei xianchengchi, jiefang shijian xunhuan xiaolongmr
08:29 shan chu tupian yuchong xiazai, chidu gengxin an zhen he bing xiaolongmr
08:10 houtai xintiao jiang pin dao 50s, fangkuan zaixian chuangkou dao 120s xiaolongmr
presence xintiao shi wei yi "qie dao houtai ye zhao da" de quanju lunxun,
xian shang shi ce dan zhe yi xiang jiu you 300+ req/s, er rongqi zhi you
2 ge gongxiang vCPU。
- qian tai: 25s (bu bian, hu ying bu shou ying xiang)
- hou tai: 50s (jiang yi ban)
- fuwu duan zaixian chuang kou: 60s -> 120s
50s x 2 = 100s < 120s, lian diu liang ci xintiao reng suan zaixian,
bi yuan lai de 25s/60s geng bu rongyi wu pan li xian。
qie hui qian tai shi li ke bu yi ci, zaixian renshu dangchang bian zhun。
zhen ji: artifacts/_visibility_gate_test.py 10/10; tsc / eslint jun lv。 07:57 guanbi uvicorn access log (xu yong --no-access-log) xiaolongmr
shang yi ci zhi shi shan diao le --access-log, dan uvicorn de access log
mo ren jiu shi kai de, shan diao deng yu mei gai: xian shang reng ran
285 KB/s / 1800 hang mei miao, ri zhi li mian zhao yang you GET/POST xing.
zheng que zuo fa shi xie ming --no-access-log。
⚠️ main.py mo wei de uvicorn.run(access_log=False) zai xian shang bu hui zhi xing
(CLI qi dong), suo yi zhi neng gai Dockerfile。 07:53 降低线上轮询压力:后台标签页暂停轮询、关闭 access log xiaolongmr
线上容器(cpu-basic,2 共享 vCPU)实测 1841 req/s,超出承载数倍,
导致连返回静态 JS 都要 55 秒。日志拆解后,热点全是自家前端的全局轮询接口:
/auth/session 375/s
/api/chat-room/unread 359/s
/api/chat-room/direct-conversations 350/s
/api/presence/ping 334/s
/api/announcements 106/s
四项改动(均不改变任何功能,用户感知不到差别):
1) Dockerfile 去掉 --access-log
实测 access log 占日志体积一半(约 285 KB/s、1800 行/秒),容器只有
2 核,每条请求都多一次格式化 + write 系统调用。注意 main.py 末尾的
uvicorn.run(access_log=False) 在线上不会执行(CLI 启动),所以必须改这里。
2) use-chat-room-unread:只在页面可见时轮询
这个 hook 挂在全局导航,每个标签页都在跑,含切到后台/被遗忘的标签页。
后台没人看角标,请求却照打。切回页面时立刻补拉一次。
3) announcements-popover:同样只在可见时轮询
该组件在顶栏挂了 2 份(移动端/桌面端各一),每个标签页跑两份。
4) use-auth-guard:权限兜底同步 5s → 30s
原本是 force 校验(绕过本地缓存直打服务端),是全站最重接口、占请求 20%。
权限和登录态不会 5 秒一变;focus / visibilitychange 仍会立刻强制校验一次,
真实感知比 30 秒更灵敏。
presence 心跳保持后台上报(约 25s)不变 —— 那是「在线人数」要统计后台标签页的
有意设计,属功能行为,未改动。
真机验证(artifacts/_visibility_gate_test.py)10/10 通过:
可见时 unread / direct-conversations… Oct 15 14 days ago 15:47 聊天室未读角标:改成红色圆形气泡,并把私聊未读一并计入 xiaolongmr
1) 样式:顶部「聊天室」导航项的未读提示原来是纯文字红色数字,
改成微信/QQ 那种红色圆形气泡(rose-500 底 + 白字 + rounded-full)。
用 min-w-4 + px-1 让个位数是正圆、两位数自然撑成胶囊,
避免固定 size-4 时 "99+" 溢出;补 aria-label 标注未读条数。
2) 计数:原来只算公共大厅未读,私聊未读拿不到。
useChatRoomUnread 改为 大厅未读 + 所有私聊未读之和,
两个请求并发发(Promise.all),传私聊已读位置整表给服务端做差集。
真实机验证 9/9 通过(含「造 2 条私聊 → 角标 +2,读掉后回落 2」),
回归 _chatroom_unread_test / _pin_test / _api_e2e 共 49 项全过。 15:29 README 中性化对外表述,降低被平台风控扫到的概率 xiaolongmr
把 Space 首页会直接展示的定性词改掉:
- title / 大标题:去掉 "ChatGPT2API"
- 项目说明:去掉「逆向封装」「OpenAI 兼容代理」
- 免责声明:去掉「对 ChatGPT 官网相关能力的逆向研究」
- 部署章节标题:去掉「到 HuggingFace」
仅改对外展示的措辞,不动任何技术内容与配置项名
(CHATGPT2API_AUTH_KEY 等环境变量名保持原样,改了会和线上配置失配)。 15:11 聊天室:消息里的 URL 自动变成可点击链接(新标签页打开) xiaolongmr
- 改造 `renderContent`:原来只处理 @昵称 高亮,现在 @高亮 与 URL 链接化并存。
非 @ 片段再走一遍链接识别,两套 pattern 不互相干扰。
- ⚠️⚠️ **URL 正则必须把中文标点排除在字符类外**:
中文没有空格,`https://example.org。以及后面的话` 若不在 `。` 处停下,
会把整个句子吞进链接里 —— 真机实测踩到,第一版就是错的。
同时 ASCII 尾部标点(`)]}>.,;:!?`)在用 trimUrlTail 剥掉。
- ⚠️ 故意**不**匹配裸域名(`abc.com`):中文聊天里「图片.png」「3.5 折」极易误判。
- ⚠️ `target="_blank"` 必须配 `rel="noopener noreferrer"`,否则新页面能通过
`window.opener` 反向操作当前页。
- ⚠️ 链接 `onClick` 要 `stopPropagation()`:消息区外层有点击处理。
验证:解析逻辑 11 例单测 + 真机(Playwright)8 项断言全过,
含「中文句末标点未被包进链接」「裸域名不链接化」「点击真的开新标签页并导航到目标地址」。 15:08 聊天室:新增「回到底部」按钮,与画图页同款 xiaolongmr
- 离开底部(>80px)才出现,点一下平滑滚回最新;回到底部自动收起。
- ⚠️ 聊天室滚动在**内层消息容器**(listRef),不是 window —— 不能照抄画图页的
`window.scrollTo`,必须滚 `el.scrollTo`。
- ⚠️ 直接改 `el.scrollTop` 不一定触发 onScroll,所以自动滚到底后要**手动同步**
按钮状态,否则会出现「已经到底了按钮还挂着」。
- 按钮用 `absolute bottom-full` 贴在输入框上方(footer 是 relative,父级不裁剪);
⚠️ 外层容器必须 `pointer-events-none`、只给按钮本身 `pointer-events-auto`,
否则这块透明区域会挡住下面消息区的点击。
- 消息区加 `data-chat-room-list` 钩子:同页有 3 个 `hide-scrollbar` 容器,
按 class 选会选错(真机验证脚本实测踩到)。
验证:真机(Playwright + Chrome)5 项断言全过 —— 按钮渲染 44×44、底部时隐藏、
上滚后出现、点击后距底 0px、回底后再次隐藏。 15:00 聊天室:修复置顶 422,未读角标改挂到用户行 xiaolongmr
置顶报 422 的根因:
- 前端把 body 做了 JSON.stringify 再交给 httpRequest,而 httpRequest 是直接把它
当 axios 的 data 传下去的 —— **传字符串时 axios 不会自动补
Content-Type: application/json**,FastAPI 就把 body 当非 JSON,
报「Field required」→ 422。
- 修法:改成传**原始对象** `body: { pinned }`,与文件里其他 POST 调用一致。
(curl 复现:不带 Content-Type 的字符串 body → 422,带上 → 200。)
未读角标的位置与语义:
- ⚠️ 不再把「会话数」红色角标挂在「私聊」分组标题上 —— 那个数字分不清是谁发来的。
改成挂在**每个用户那一行**(对方昵称右侧),显示「这个人有几条我没看」。
- ⚠️ 原来 direct_conversations() 的 total 是「所有非我发的消息总数」,
没有已读概念 → 角标只增不减(读完也不消)。现在按已读位置过滤。
- 已读位置**不落库**:存 localStorage(`chatgpt2api_chat_room_direct_read`,
peer_id → 已读到的最大 id),接口用 `mark=peer:last_id,...` 把整张表带过去,
服务端只数 `id > last_read` 的对方消息。这样不必新建「每用户每会话已读表」。
- ⚠️ 私聊的已读位置**与公共大厅分开存**:两边消息 id 不同源,共用一个 key 会互相顶掉。
- 打开会话 / 正在看该会话时收到新消息 → 立刻写已读位置并广播事件,
收件箱监听该事件即时重拉,角标当场归零,不必等下一个轮询周期。
- `_parse_marks` 把前端输入一律当不可信:格式不对整段丢弃,宁多显示未读也不能 500。
另外顺手改:收件箱摘要里自己发的消息加「我:」前缀(否则看起来像对方说的),
已撤回的显示「[消息已撤回]」而不是空白。
验证:四套聊天室测试共 64 项全过(服务层 24 + HTTP 11 + 用户侧端到端 15 + 未读 14)。
tsc / eslint 全绿。已清… 14:14 聊天室:管理员置顶公告 + 修复私聊收不到/回不了 xiaolongmr
置顶(仅管理员,服务端强制):
- 消息表新增 pinned_ts / pinned_by 两列,_row_to_message 下发 pinned / pinned_at。
- set_pinned 只放行 admin;已删除的消息不能置顶;撤回时一并清掉置顶。
- 新增 GET /api/chat-room/pinned 与 POST /api/chat-room/messages/{id}/pin。
- 置顶条**独立于翻页列表**:置顶消息可能早就不在当前页,仍要常驻顶部。
- 前端在 header 与消息区之间加置顶横幅,多条默认只露第一条(可展开),
点击跳转到原消息(不在当前页时给提示,不静默无反应)。
修复「管理员私聊用户,用户看不到消息,也无法回复」——两个独立成因:
1) 权限判定用错:_assert_can_direct(只允许 admin)被误用到读历史/回复路径,
导致收件人侧一律 403。拆出 _assert_can_access_conversation(会话双方可访问),
读历史与「回复已有会话」走它;只有「发起全新会话」才要求 admin。
⚠️ 判定新会话不能只看「me 在参与者里」——新会话的键就是 dm:me|peer,恒成立,
必须用 _has_history() 查是否有实际往来,否则普通用户能私聊任何人。
2) 收件人没有入口:原未读统计只看公共大厅,收件人根本不知道有人私聊自己。
新增 direct_conversations() + GET /api/chat-room/direct-conversations,
前端侧栏加「私聊」区块(头像/昵称/摘要/在线点 + 计数徽章)。
验证:服务层 24 项、HTTP 接口 11 项、用户侧真实鉴权端到端 15 项,共 50 项全过。
后端全量测试 948 passed,失败 26 项均为既有环境漂移(chrome 指纹版本、导航迁移),
与本次改动无交集(chat_room 相关 0 项失败)。 13:59 聊天室:图片改用灯箱查看,修复 @ 用户面板被裁掉 xiaolongmr
1. 聊天图片不再新标签页打开
原本用 <a target="_blank"> 包图片,点击会跳新标签页。改为复用站内已有的
ImageLightbox(Fancybox),点击原地放大;并把当前已加载消息里的图片一并
传入,灯箱内可左右翻看,不用退出去再点下一张。
2. 修复 @ 唤出面板被遮挡
@ 面板是 `absolute bottom-full`(贴在输入框上方),而 PromptComposerSurface
默认带 `overflow-hidden`,会把面板整块裁掉 —— 所以看起来像「没弹出来」。
按 chat-composer 的既有做法,在面板打开时给 Surface 加 `z-50 overflow-visible`。 13:11 区分「上游 CDN 拦截」与「token 失效」,修复导入账号报 403 xiaolongmr
导入账号时报 /backend-api/accounts/check failed: HTTP 403。实测确认这不是
账号问题:同一个 token 本地直连 200,HF 容器内 403;补 sentinel token 后本地
依然 200,说明与请求内容无关,只与出口 IP 有关(容器出口被 Cloudflare 拦,
响应体是 HTML 拦截页,与此前 share_evidence_release_failed 的 403 形态一致)。
原代码把任何非 401 失败都当成单纯刷新失败,账号虽未被判死,但错误文案会让人
误以为号坏了。
改动:
- openai_backend_api:新增 UpstreamBlockedError;_get_default_account() 增加
CDN 拦截识别(content-type/body 形态)与一次退避重试,403+HTML 抛
UpstreamBlockedError,403+JSON 仍抛 RuntimeError。
- account_service.refresh_accounts():单独捕获 UpstreamBlockedError,账号保持
原样、不计失效,错误文案改为可读的「上游 CDN 拦截…非账号问题」,新增
blocked 计数随结果返回。
- api/accounts.py:导入响应透出 blocked 字段。
- api/system.py:新增管理员诊断接口 POST /api/upstream/probe,可从容器出口
直连指定路由并回报 status / cf-ray / body / 是否 CF 拦截页。 12:43 移除画图页的客服聊天挂件 xiaolongmr
删除 web/src/app/image/page.tsx 里注入 //dh.z-l.top/chat.js 的 useEffect,
普通用户画图页不再加载该挂件。
背景:zh 挂件由 ecf6886b 加入主线;zlcy/li 远端曾用 e0d9a201 单独 revert 掉,
但后续把 zlcy/li 强制同步到主线时又把挂件带回了线上,故在主线一并移除。 12:24 修复镜像缺少 mini-racer 导致 HF 上 V8 求解超时 xiaolongmr
pyproject.toml 里声明了 mini-racer,但 uv.lock 未同步,而 Dockerfile
用 `uv sync --frozen` 严格按 lock 安装、忽略 pyproject 新增依赖,
所以镜像里根本没有 py_mini_racer。
表现:容器日志出现 sentinel_v8_solve_failed: JavaScript was terminated
by timeout,sentinel token 拿不到 → f/conversation 返回伪装 404,
同一账号在本地(pip 装过 mini-racer)正常、在 HF 上必失败。
用 uv lock 重新解析,补入 mini-racer 0.14.1(含 manylinux_2_27_x86_64
wheel,适配 python:3.13-slim)。 11:10 修复 .dockerignore 排除 assets 导致镜像构建失败 xiaolongmr
Dockerfile 里 COPY assets ./assets 是 V8 求解器的 SDK 资产入口,
但 .dockerignore 中的 `assets` 一行把整个目录挡在构建上下文之外,
HF 构建报 `failed to calculate checksum ... "/assets": not found`。
该行属历史遗留,删除。 10:54 生图求解增加进程内 V8 兜底,单容器部署无需外部 solver xiaolongmr
上游 chatgpt.com 只接受在真实页面上下文里算出的 Turnstile,缺失或质量不够时
f/conversation 返回伪装成 404 的 {"detail":"Model not found"}。此前唯一可行路线是
外部 sentinel_solver 服务(常驻无头 Chromium),但 HF Space 等单容器环境跑不了
第二个容器,导致线上生图必然 404。
本次移植 py-mini-racer 路线,在进程内嵌 V8 直接跑上游真实 sentinel SDK:
- 新增 utils/sentinel_v8.py:加载 assets/sentinel/{sdk.js,shims.js},
按指纹缓存 V8 上下文,全局锁保证不可跨线程并发
- 新增 assets/sentinel/{sdk.js,shims.js}:上游 SDK 与浏览器环境 shim,
sdk.js 构建时打补丁暴露内部求解函数(__reg / __solve_dx)
- services/sentinel_solver_client.py:求解优先级改为
外部服务 → 进程内 V8 → 旧手写 VM;solver_user_agent() 在外部服务
探测不到 UA 时回落内置 Chromium UA,避免与 V8 shim 环境不一致
- pyproject.toml:新增 mini-racer>=0.14.1(导入名仍是 py_mini_racer)
- Dockerfile:COPY assets 进镜像
实测:solver_url 置空时 V8 自动接管,算出 1076 字符 turnstile token,
与外部浏览器路线的 1200 字符量级一致;两条路线在账号层面均返回 404,
说明该 404 与被测账号本身相关,与求解方式无关。
已知边界:V8 目前只覆盖 Turnstile(dx),so 仍需外部求解服务。 07:34 聊天室:隐藏消息区与侧栏的滚动条 xiaolongmr
给消息区、侧栏在线列表、@ 唤出面板的 overflow-y-auto 容器加上站点现成的
hide-scrollbar 工具类(globals.css 已定义,画图页消息区同款)。
滚动功能不受影响,只是滚动条不再显示、也不再占位。 07:31 聊天室:头像回退微信头像、复用画图页输入框、撤回删除、布局收口 xiaolongmr
头像
- 根因是聊天室与 presence 只读 identity["avatar_url"],而微信登录用户的头像
存在 wechat_avatar 里,于是恒为空、界面只显示首字占位(签到页能显示是因为
它两个字段都读)。新增 presence_service.resolve_avatar_url 统一口径:
先 avatar_url,再回退 wechat_avatar,与顶栏 / 画图页 / 批量页 / 签到页一致。
- 在线列表、消息落库、summary.me 三处都改走这个函数。
输入框
- 底部输入框改用画图页同一套 @/components/prompt-composer 部件
(Surface / Textarea / Toolbar / SubmitButton),外形与画图页完全一致。
- @ 唤出面板通过 suggestions 传入,键盘拦截走 onKeyDownCapture
(返回 true 表示面板已消费该按键),粘贴图片仍走 onPaste。
撤回与删除
- 软删除:新增 deleted_ts / deleted_by 两列,不做物理删。消息 id 是前端翻页
游标,物理删会留下空洞、导致增量轮询的序列错乱。
- 已删消息的正文与图片在服务端就抹空(不能只靠前端不渲染,否则直接读接口
仍能拿到内容)。前端显示「你/某人撤回了一条消息」或「管理员删除了一条消息」。
- 权限在服务端强制:作者本人 2 分钟内可撤回(RECALL_WINDOW_SECS=120),
管理员可删任意消息。前端按钮只按同一规则决定是否渲染,不作为权限依据。
- 新增 DELETE /api/chat-room/messages/{id}。
- mergeMessages 支持就地更新:撤回后 id 不变而内容变了,若只做「去重后追加」,
轮询拉回的已删状态会被旧数据挡住,界面永远不会更新。
布局
- 输入框区去掉「所有登录用户可见,请勿发送隐私信息」。
- 左上角侧栏头部由「在线用户 + 计数 + 搜索框」改为单个「公告聊天室」标题。
- section 高度由 min-h 改为固定 h:AppShell 内层本身是 min-h-screen 加
pt-[5.25rem],section 再叠 min-h 会与之相… 06:41 新增聊天室页面:登录用户群聊 xiaolongmr
- 后端:services/chat_room_service.py(SQLite 只追加存储,经镜像 runtime_file 持久化)+ api/chat_room.py(消息增删查 / 汇总接口,均需登录)
- 前端:/chat-room 页面,3s 轮询增量拉取、乐观发送、回车发送、500 字上限、自动滚底、日期分隔与角色徽章
- 导航:三套角色导航均插入「聊天室」(对话之后),含一次性迁移 06:14 Revert "失败卡片补兜底「查看官网」入口" xiaolongmr
This reverts commit 0075c950d9b85b912a12531beb709356550fab2a. 06:12 失败卡片补兜底「查看官网」入口 xiaolongmr
请求在创建上游会话之前就被上游挡掉时(如伪装 404 "Model not found"),
卡片上没有 conversationId,原有的「查看官网对话」取证按钮不渲染。
现在这种卡片会在「重新生成这一张」左边显示一个直达 chatgpt.com 的
「查看官网」链接,方便用户自己去官网核对账号状态;后端明确
canViewOfficial=false(账号配额耗尽)时仍然不渲染。画图页与批量页同步。 05:40 求解服务页面语言改可配置,与出口地域对齐 xiaolongmr
原先把页面 locale 硬编码成 zh-CN,但求解服务出口实测在美国(104.238.221.29
洛杉矶),时区/语言与出口 IP 所在地矛盾本身就是一个可疑特征。改为
SENTINEL_SOLVER_LOCALE 环境变量(默认 en-US),并同步更新文档说明里
SENTINEL_SOLVER_TIMEZONE 的示例。
本次排查同时确认(与本次提交无关、仅记录结论):
- 号池 8 个账号 access_token 的 JWT sub 全部是 auth0| 前缀,amr 为空,
没有一个是 google-oauth2| —— 即"邮箱+验证码/脚本注册"体系,不是
「Continue with Google」真实登录号;
- 其中 6 个 duck.com 号连 chatgpt_plan_type claim 都没有,属未完成
onboarding 的残缺号;
- f/conversation 在同一账号上重复请求会稳定返回伪装 404
{"detail":"Model not found"},与 model / thinking_effort 取值无关。 05:11 放开 chrome150 指纹白名单 xiaolongmr
社区实测 chatgpt.com 对 chrome146 的 TLS 指纹仍可能判为低可信,对话接口
返回伪装成 404 的 "Model not found",chrome150 才通过(curl_cffi 0.16.3 已支持
chrome150,可在本机 __version__ 与 BrowserTypeLiteral 里确认)。
_SUPPORTED_CHROME_MAJORS 是 _canonical_browser_profile 判断「指纹是否受支持」
的白名单:major 不在集合内会直接落到 fallback,用回 DEFAULT_CHATGPT_IMPERSONATE。
此前实测传入 impersonate="chrome150" 实际生效仍是 chrome146,原因就是 150 不在
白名单。补上 150 后,显式指定 chrome150 才能真正生效。
刻意不改 DEFAULT_CHATGPT_IMPERSONATE:该值参与 fp 计算,_client_context_changed
以 original["fp"] != normalized["fp"] 判定迁移,改默认值会让所有存量账号 fp 变化
并触发一次全量重写。默认行为保持 chrome146 不变,升 150 待拿到干净账号后 A/B 验证。 05:07 忽略 Windows 保留设备名残留文件 nul xiaolongmr
早期误执行 rmdir /s /q ... 2>nul 时,shell 未识别 2>nul 重定向,
在仓库根目录生成了一个名为 nul 的 74 字节文件。nul 是 Windows 保留
设备名,DeleteFileW / git clean / Remove-Item 均报 Access Denied
(0x800704B0 指定的设备名无效),无法删除。
该文件会让 ripgrep 扫描整个仓库时报 os error 1 而失败,
把 /nul 加入 .gitignore 使其不再出现在 git status 并恢复目录扫描。 04:59 补齐 sentinel SO token 并让求解服务支持指定 Chrome 版本 xiaolongmr
排查 2026-10-14 生图 404:定位到 404 实际发生在 /backend-api/f/conversation,
chat-requirements 的 prepare/finalize 两步本身可通、Turnstile 也能解出。
- prepare 响应里 so 也是 required(collector_dx + snapshot_dx),
chatgpt.com 对缺 SO token 的对话请求返回伪装成 404 的 "Model not found"。
- sentinel_solver_client 新增 solve_so(),调用求解服务 /v1/so 端点。
- _get_chat_requirements 解出 SO token 并随 finalize 提交,落进 ChatRequirements.so_token,
由 _image_headers / _conversation_headers 带成 OpenAI-Sentinel-SO-Token 头。
- sentinel_solver/app.py 支持 SENTINEL_SOLVER_CHROME_PATH:优先用指定 Chrome 可执行文件
(默认指向 chrome-for-testing 153.0.8010.12,与 Playwright chromium 1243 对齐),
找不到时回退 channel="chromium"。
注:本地实测仍 404,根因指向出口 IP 归属 CN(非代码问题),详见当日工作日志。 03:16 借鉴 MEIZU16 把 chat-requirements 改为 prepare/finalize 两步流程修复 404 xiaolongmr
chatgpt.com 自 2026-10-14 起,旧的「单步」chat-requirements 算出的 token 被判为低可信,
对话/生图接口统一返回伪装成 404 的 "Model not found"。
改动:
- 把 _get_chat_requirements 从单步 POST 改为 prepare + finalize 两步:先 prepare 拿 challenge
参数(proofofwork / turnstile / prepare_token),用真实浏览器(sentinel_solver 服务)解出
Turnstile,再 finalize 换回真正可用的 token。复用既有 _build_requirements 算
proofofwork/turnstile,文本对话与生图链路的所有调用方(2201/2251/2441/2846)自动生效。
- sentinel_solver/app.py 同步上游最新的 SENTINEL_SOLVER_TIMEZONE 配置(页面时区需与出口
IP 所在地一致,利于过 Cloudflare 校验),含 docstring、环境变量、上下文 timezone_id 与
/health 状态输出。
生效前提:必须实际运行 sentinel_solver 服务并配置 CHATGPT2API_SENTINEL_SOLVER_URL,
否则回落本地兜底求解器仍会 404。 02:46 借鉴 MEIZU16/chatgpt2api 完整方案修复 404:接入 sentinel 求解服务 xiaolongmr
404 根因(issue #416):2026-10-14 起 chatgpt.com 只接受在真实页面算出的
Turnstile/SO token,纯 Python 模拟被拒(403 或伪装成 404 的 "Model not found")。
TLS 升 chrome146 只能解决「低可信」那一截,强制要求 Turnstile 时必须靠真实浏览器。
- 新增 sentinel_solver/:常驻无头 Chromium 的 chatgpt.com 页面,通过 HTTP 替主服务
算 Turnstile/SO token(playwright + playwright-stealth,移植官方挑战 VM)
- 新增 services/sentinel_solver_client.py:后端调用求解服务;未配置或失败时回落本地
utils.turnstile 兜底(已跟不上新版,仅兜底)
- services/openai_backend_api.py:turnstile 改走 solve_turnstile;_sentinel_user_agent()
回落 solver_user_agent(),保证 requirements/proof token 的 UA 与求解页一致
- services/config.py:sentinel_solver_url / sentinel_solver_key(env 或 config.json)
- docker-compose.yml:加 sentinel-solver 服务,主服务注入 CHATGPT2API_SENTINEL_SOLVER_URL
生效前提:需实际运行求解服务并配置 CHATGPT2API_SENTINEL_SOLVER_URL,否则仍回落本地兜底。 02:31 修复 2026-10-14 后 /backend-api/f/conversation 伪装成 404 的报错 xiaolongmr
chatgpt.com 自 2026-10-14 起对低质量 sentinel 请求返回伪装成 404 的
"Model not found":chrome110 的 TLS 指纹被判低可信,且图片链路必须带齐
Turnstile/SO 头。对齐上游修复:
- TLS impersonation 由 chrome110 升到 chrome146(curl_cffi>=0.13 才支持,
本机已升级到 0.16.3);账号 fp 里残留的 chrome110 等旧 impersonate 统一
清除后回落 chrome146,保证 TLS 与 UA 一致
- _image_headers 补上 OpenAI-Sentinel-Turnstile-Token / OpenAI-Sentinel-SO-Token
(之前只有 Proof-Token),404 正好出在图片接口 /backend-api/f/conversation
- 新增 _sentinel_user_agent(),POW/legacy token 里的 UA 收敛到与发请求浏览器
一致(预留接入 sentinel 求解服务的入口) 02:20 普通用户画图页加入客服聊天挂件 xiaolongmr
在 web/src/app/image/page.tsx 用 useEffect 注入 //dh.z-l.top/chat.js,
保留原样的 async 与 fold 属性(next/script 会剥掉非标准属性故手动注入),
仅 role=normal 的普通用户加载,管理员/内部账号不挂载。 01:16 撤回提示词编辑器「图N 引用参考图」与 @ 唤出缩略图功能 xiaolongmr
该功能几轮修复后仍无法稳定使用(@ 选图后立刻用输入法打字时空格丢失、
缩略图与光标行为异常),按用户要求整体撤掉,输入框恢复为普通 textarea。
- prompt-composer.tsx 回到 1b90ec42(功能引入前)的 textarea 版本,
移除 contentEditable chip 编辑器、序列化/光标偏移换算、@ 候选菜单、
缩略图渲染与自动补空格等全部相关代码
- 画图页 / 批量生图页 / 聊天页 / 产品图页 4 个调用方同步移除
referenceTokens 传参与 PromptComposerEditorHandle 类型标注
- 移除画图页的「写『图1』『图片1』或按 @ 都能引用参考图」提示文案
tsc 与 eslint 均通过。 Oct 14 15 days ago 19:24 提示词编辑器对齐 infinite-canvas 的 chip 输入架构,修「@选图/手打标签后空格丢失」 xiaolongmr
换掉「每次输入都重建 DOM」的做法,迁移 infinite-canvas 的核心机制:
- 输入时只在「DOM 序列化结果 != 目标字符串」才重建 DOM 并按字符串偏移还原光标,
普通打字(含 IME 组字)完全不碰 DOM,输入法不再被冲掉
- 空格以 DOM 为准:chip 集合相同但 DOM 缺空格时(如「的」顶掉空格的那次输入)
也会重建,杜绝「空格只在字符串里、界面看不到」的脱节
- @ 选中插入后光标落进空格文本节点内部(原先悬在节点边界,caret 矩形是 0x0)
- domPointAtOffset 落 chip 边界时优先落进相邻文本节点内部,不再悬空
- 迁移 infinite-canvas 的原子删除:keydown 拦截紧贴 chip 的退格/Delete 并
preventDefault 后整块删除,避免与浏览器原生删除重复执行
真机验证(agent-browser 真实按键):@选图后立刻打字、手打标签、逐字输入、
中文混排、chip 原子边界(左右键/退格/Delete)、双 chip 相邻删除共 14 例全过。 17:31 提示词引用参考图:同时支持「图片1」写法 xiaolongmr
用户要求除「图1」外也认「图片1」。实现为同义别名,命中任一都渲染同一张缩略图,
插入提示词时仍写主标签「图1」,不改变既有输出。
- PromptReferenceToken 新增可选 aliases;referenceIndexByLabel 与
referenceLabels 都把 label + aliases 一起收录,referenceLabels 继续按长度倒序。
倒序很关键:「图片12」必须先于「图片1」匹配,否则会被拆成「图片1」+「2」。
- 两个 image-composer(画图页 / 批量页)都传 aliases: [`图片${index + 1}`]。
- mentionCandidates 的搜索串也拼上别名,「@图片」能筛出候选。
- 缩略图角标的取号正则放宽成 /^@?图(?:片)?/,否则「图片1」会显示成「片1」。
验证:
- 断言 artifacts/prompt-refs/verify-alias.mjs,23 条全绿:两种写法命中同一张图、
混用不串、越界 图片9 不命中、图片12 优先于图片1、别名在退格/左右键/补空格
四条路径上与主标签完全等价。另回归 verify-cursor.mjs 18 条仍全绿。
- 特别确认「图片1」里的「图1」子串不会被当成独立标签误匹配
(图1 要求「图」紧跟「1」,图片1 中不成立)。
- 真机 artifacts/prompt-refs/alias-check.sh,7 用例全绿:图片1/图片2 各渲染对应
缩略图、与图1 混用正确、越界图片9 不渲染、caret=4 按← 一下跳到 1、
退格整块删成「用 换」、@图片 能筛出候选、@ 回车仍插入「图1」。 17:24 提示词引用参考图:「图N」在键盘上当成一个单位,左右键一下跨过 xiaolongmr
用户反馈:参考项目里缩略图是一个字符,左右移动光标和删除都只需一下。
查 infinite-canvas 源码确认:它的 canvas-prompt-chip-input.tsx 用的是
contentEditable + contentEditable="false" 的真实 DOM chip,天然是原子节点;
而 canvas-resource-mention-textarea.tsx(textarea 版)和我们一样,
是靠 keydown 拦截模拟原子性。我们是纯 textarea,补齐这部分键盘行为。
- 新增 referenceLabelRangeAt():找出光标贴着的标签区间 [start, end)。
start 算上可选的 @ 前缀,光标停在 @ 前面时也能整块跨过去。
- 左右方向键:光标贴着标签尾部按 ← 跳到开头,贴着开头按 → 跳到尾部之后;
从标签中间按 ← 也会一下跳到开头。Shift+方向键保留选区扩展。
带 alt/ctrl/meta 修饰时不拦截(留给词级跳转)。普通文本行为不变。
- 顺带修退格的两个 bug:
· 原来要求标签前面必须是行首或空格,「用图1」退格会只删掉「1」留下「用图」。
改成只要求标签结尾紧贴光标。
· 向前删的正则 ^(@?)(label) 匹配不到光标在 @ 前面时的 @,会留下孤零零的
「@」;改成 ^((?:@)?label) 一并吃掉。
- 验证:
· 逻辑断言 artifacts/prompt-refs/verify-cursor.mjs,18 条全绿
(含图12 不被图1 抢匹配、@ 前缀、普通文本不误伤)。
· 真机 artifacts/prompt-refs/cursor-check.sh,7 个用例全绿:
caret=3 按← → 1;caret=1 按→ → 3;caret=2 按← → 1;
退格/Delete 均整块删成「用 换」;「用图1」无空格也整块删;
普通文本按← 仍是 4→3。 17:02 提示词引用参考图:@ 随手可唤出 + 手打「图N」自动补空格 xiaolongmr
- @ 候选菜单改成光标前有 @ 就唤出,不再要求前面是空格或行首。
参考项目 infinite-canvas 用的是 (^|\s)@,这里放宽成 @([^\s@]*)$,
「把@」「中间@」都能直接唤出,插入位置仍按 @ 的起点算。
- 手动打完「图N」后自动补一个空格,跟 @ 选中插入的「图N 」一致,
接着写文字不会和标签粘在一起。
· 参考图 ≥10 张时「图1」可能是「图12」的前缀,此时等用户打完数字再补,
避免把「图12」拆成「图1 2」。
· 标签后面已经紧跟汉字(如「图2的」)时在标签与汉字之间插入,不影响语义。
· 越界编号(图9 而只有 2 张图)不补空格,保持纯文本。
· 组字期间(isComposing)不插空格,避免打断 IME 的拼音串。
- 真机验证 artifacts/prompt-refs/at-check.sh:
把@ / 中间@ / @ / 已有文字后面@ 四种都能唤出候选;
用图1 → 用图1 ;用图2的 → 用图2 的;用图9 不动;@城市 只剩图2。 16:51 画图页提示词支持「图N」引用参考图:输入框内直接显示缩略图、可点开大图、可用 @ 唤出候选 xiaolongmr
- PromptComposerTextarea 新增 referenceTokens:高亮层把命中的「图1」「图2」原地画成缩略图。
外层 inline-block 用 visibility:hidden 的文字占位、内层绝对定位铺满那一格,所以缩略图
不改变文本流里的宽高,多行混排时光标也不会错位(复用 $技能 已有的镜像层机制)。
- 缩略图可点击看大图(复用 ImageLightbox);mousedown 阻止默认行为,点击不抢输入框焦点与光标。
- 新增 @ 唤出候选:输入 @ 后在光标处弹出列表(缩略图 + 图N + 文件名),上下键选择、
回车插入、Esc 或点击外部关闭;光标坐标用镜像 div 量算,菜单会按空间自动向上翻。
- 退格 / 删除把「图N」按整体删掉(含可选的 @ 和尾随空格),不会删一半留下孤零零的「图」。
- 输入法组字期间临时关掉高亮层:高亮生效时 textarea 文字是透明的,不关掉中文打字会看不见。
- mention 与选中项的真值放在 ref:textarea 光标一动 React 会补发 onSelect,
只认 state 会让键盘选中的项在与按键同一批事件里被打回第一项。
- 画图页与批量生图页的输入框都接上了(批量页是画图页的副本,保持同步)。 16:33 批量生图页:图池默认带到每一条、每条可自传参考图、行级模型与比例改用输入框同款菜单 xiaolongmr
- 输入框上传的参考图进「图池」,提交时默认带到每一条提示词;参考图条上方加一句说明。
- 每条提示词可以单独上传自己的参考图(与图池合并,单条合计上限 16 张),
行内显示缩略图 + 常显的删除角标;列表行用角标显示本条自带几张。
- 行级「模型 / 比例」不再用原生 select,抽出 BatchParamMenu 复用输入框工具栏
那套「胶囊 + ChevronDown + fixed 浮层菜单」的样式与定位逻辑:
第一项固定是「默认 · 全局值」,未覆盖时值用浅色、覆盖后用深色加粗;
同一时刻只开一个菜单,外部点击关闭。
- 换模型时清掉该条的比例覆盖(第三方模型各自支持的比例不同,旧值可能不适用)。
- 每条自己的参考图用 ref 记录上传目标,避免点按钮与 change 之间拿到陈旧 state。 16:09 批量生图页改版:提示词列表移到输入框上方、支持分隔符与 xlsx 导入 xiaolongmr
- 布局重构:不再把多行编辑器塞进输入框,改成「列表在上、原输入框编辑选中项」——
点列表里任意一条,输入框就显示并编辑那一条(原来输入框的内容会在进入批量模式时带进第 1 条)
- 每条独立设置平铺出来:参考图、张数、模型、比例直接可见可改(不再藏在「参数」展开里);
改过之后旁边出现「恢复跟全局」用来清掉行级覆盖
- 分隔符:默认 ----,可在批量区自定义;「拆分输入框」把当前这条按分隔符拆成多条;
批量模式下粘贴含分隔符的内容也会自动拆
- 导入 xlsx / xls / csv(SheetJS):读各工作表第一列的非空单元格,跳过「提示词 / prompt」
这类表头,单元格内还能再按分隔符拆;导入结果追加进列表
- 修两个 bug:
① 追加条目改用函数式 setState —— 否则拆分时会把同一批次里刚改好的那条顶回原文;
② 导入时不能提前把 input.value = "" —— 会让已拿到的 File 变成不可读(NotReadableError)
- 依赖:新增 xlsx(SheetJS)