From 7f77e5aca979fc61297ca39851e90fe14ed5621b Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 7 Jul 2026 10:44:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=BF=AE=E5=A4=8D=20+=20=E5=AF=B9=E5=A4=96?= =?UTF-8?q?=E5=93=81=E7=89=8C=E5=90=8D=20ZCBOT=5FBRAND=5FNAME(bump=200.45.?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动端: - 浮层菜单(⋯ 任务菜单 / ⚙ 媒体模型弹层)原只右对齐触发按钮,手机上触发钮 靠左时菜单冲出屏幕左缘盖住正文;改为放不下时切左对齐 + 双向钳视口内 - 手机断点:菜单项加大触控目标、模型下拉限宽 48vw、header 品牌名单行省略 - 「管理」入口挪到 任务/对话/文件 tabs 行右侧小按钮(CSS order 实现, embed 与普通模式同款,embed 专属覆盖规则随之删除) 品牌名: - 新 env ZCBOT_BRAND_NAME(默认「总院科研辅助助手」),zcbot 只是内部代号 - /healthz 返 brand 字段,前端 boot 拉取覆盖页面标题/顶栏/登录卡 - 微信/企微测试推送、OAuth 提示页、绑定页等用户可见文案改品牌名或中性「助手」 Co-Authored-By: Claude Fable 5 --- PROGRESS.md | 1 + RUN.md | 3 +++ core/__init__.py | 2 +- web/app.py | 18 ++++++++++++------ web/static/dev.html | 34 +++++++++++++++++++++++----------- web/static/js/chat.js | 11 +++++++++-- web/static/js/dom.js | 12 ++++++++++-- web/static/js/main.js | 17 ++++++++++++----- web/static/wechat_bind.html | 10 +++++++--- 9 files changed, 78 insertions(+), 30 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 081dd65..0e0f3bc 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,7 @@ ### 2026-07 +- **07-07 / 0.45.0**:移动端显示修复 + 对外品牌名 env——① 浮层菜单(⋯ 任务菜单 `dom.js`、⚙ 媒体模型弹层 `chat.js`)原只做右对齐,手机上触发钮靠左时菜单冲出屏幕左缘盖住正文,改成"右对齐放不下→左对齐 + 双向钳视口";手机断点补:菜单项加大触控目标、模型下拉限宽 48vw、「管理」入口统一挪到 任务/对话/文件 tabs 行右侧小按钮(embed 与普通模式同款,靠 CSS order 实现不动 DOM)、header 品牌名单行省略。② 新 env `ZCBOT_BRAND_NAME`(默认「总院科研辅助助手」,zcbot 只是内部代号):`/healthz` 返 brand 字段,前端 boot 拉取覆盖页面标题/顶栏/登录卡(静态页默认值也已改为默认品牌);微信/企微测试推送、OAuth 提示页、绑定页等用户可见文案全部改用品牌名或中性"助手",不再露 zcbot。RUN.md env 段同步。 - **07-07 / 0.44.0**:企业微信应用主页免登——企微后台「应用主页」填 `/v1/wecom/entry`,成员点开应用即静默授权(snsapi_base)登进 embed 控制台。后端两端点(entry 302 授权、callback code→userid→反查绑定→签 JWT→fragment 带 token 302 进 `dev.html?embed=1&wecom=1`),UA 分支让外部浏览器退 wwlogin 扫码;前端 wecom 变体(fragment 吃 token 即清 hash,401 回 entry 静默重签,不需要 parent_origin);未绑定成员提示先在控制台绑定(不自动建号,避免重复账号)。EMBED.md §9 + RUN.md 企微段同步。 - **07-07 / 0.43.1**:跳秒指示补两处空窗——① 刷新/重连接管后 phaseSince 随页面内存丢失,退回静态"思考中"不跳秒(恰是用户因疑似卡死而刷新的场景),重建直播卡时立即按"思考中"起跳,下一个阶段事件校正;② 发消息 POST 返回即起跳,覆盖 build_agent + 首轮 TTFT(原先要等 llm_start 才开始计时)。重连后秒数从重连时刻起算(非真实累计),可接受。 - **07-07 / 0.43.0**:run 长耗时可视化——修"前端一直静止显示思考中被当成卡死":① 新增 SSE `reasoning{delta}` 事件(loop 提取 `delta.reasoning_content`,thinking 模型分钟级推理原先整段丢弃、前端零反馈),前端渲染灰色折叠"思考过程"卡,历史消息里持久化的 reasoning_content 同款渲染;② 前端活跃状态指示:TTFT/推理期占位段每秒跳"思考中/深度思考中 · Ns"(CSS attr(data-status)),工具卡运行中加 spinner + 跳秒、结果到达定格为执行耗时;每轮 llm_start 重建占位段,工具轮之后不再空窗。 diff --git a/RUN.md b/RUN.md index f70ccaf..9e744c6 100644 --- a/RUN.md +++ b/RUN.md @@ -85,6 +85,9 @@ # 用户即可在企业微信里直接和 zcbot 对话(回调 URL = <公网 base>/v1/wecom/callback)。 # WECOM_CALLBACK_TOKEN=... # 接收消息 Token(企微后台生成) # WECOM_CALLBACK_AESKEY=... # EncodingAESKey(43 字符,企微后台生成) + # 对外品牌名:zcbot 是内部代号,所有用户可见文案(页面标题/顶栏/登录卡/微信·企微推送与 + # 提示页)统一用品牌名。/healthz 返回 brand 字段,前端 boot 拉取覆盖静态页默认值。 + # ZCBOT_BRAND_NAME=总院科研辅助助手 # 可选,默认即此值 ``` > litellm 在 import 时副作用加载 .env;入口走 `main.py`,`.env` 自动生效。直跑 `python -c "from core.storage import ..."` 不经 litellm 链路时记得自己 `import litellm` 触发,或手动 `export ZCBOT_DB_URL=...`。 - **依赖**:`pip install -r requirements.txt`(已在 `.venv` 里;含 `bcrypt`、`segno`、`cryptography`)。 diff --git a/core/__init__.py b/core/__init__.py index c4a1822..40a3f79 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.44.0" +__version__ = "0.45.0" diff --git a/web/app.py b/web/app.py index 1f450b2..82631f9 100644 --- a/web/app.py +++ b/web/app.py @@ -69,6 +69,10 @@ from .static_files import NoCacheStaticFiles # 读法)③ /healthz 返回,蓝绿切换时验证 nginx 已指到新实例。 INSTANCE = os.getenv("ZCBOT_INSTANCE", "").strip() +# 对外品牌名:zcbot 只是内部项目代号,所有用户可见文案(页面标题 / 推送消息 / OAuth +# 提示页)统一用这个。/healthz 也返回,前端 boot 时拉取覆盖静态页里的默认值。 +BRAND = os.getenv("ZCBOT_BRAND_NAME", "").strip() or "总院科研辅助助手" + STATUS_FILTERS = ("active", "completed", "abandoned") STATUS_WRITABLE = ("completed", "abandoned") # web 不让从 web 端切回 active(走 CLI) # 渠道镜像 task 的 channel 取值(每用户每渠道一条常驻只读对话):从普通任务列表排除, @@ -1300,7 +1304,9 @@ def create_app() -> FastAPI: @app.get("/healthz", tags=["misc"]) def healthz(): # instance:蓝绿部署时验证打到的是哪个实例(blue/green);单实例为 null - return {"status": "ok", "version": __version__, "instance": INSTANCE or None} + # brand:对外品牌名(ZCBOT_BRAND_NAME),前端 boot 拉取覆盖页面标题 / 顶栏 / 登录卡 + return {"status": "ok", "version": __version__, "instance": INSTANCE or None, + "brand": BRAND} @app.get("/WW_verify_{token}.txt", include_in_schema=False) def wecom_domain_verify(token: str): @@ -1391,7 +1397,7 @@ def create_app() -> FastAPI: """自检:给已绑用户推一条测试消息(需用户近 24h 在微信开口过)。""" from core.wechat import service as _wx res = await asyncio.to_thread( - _wx.push_clawbot, user_id, "zcbot 测试消息:绑定成功,这条来自你的 zcbot。" + _wx.push_clawbot, user_id, f"{BRAND} 测试消息:微信绑定成功,推送通道正常。" ) return {"ok": res.ok, "reason": res.reason} @@ -1404,7 +1410,7 @@ def create_app() -> FastAPI: return HTMLResponse( f"" f"
" - f"{msg}
可关闭本页返回 zcbot
" + f"{msg}
可关闭本页返回{BRAND}
" ) @app.get("/v1/wecom/oauth/url", tags=["wecom"]) @@ -1428,7 +1434,7 @@ def create_app() -> FastAPI: uid = wecom.verify_state(state) if not uid or uid == _WECOM_LOGIN_STATE: - return _wecom_page("绑定失败:授权已过期或无效,请回 zcbot 重试。", False) + return _wecom_page(f"绑定失败:授权已过期或无效,请回{BRAND}重试。", False) if not code: return _wecom_page("绑定失败:未拿到授权码。", False) try: @@ -1487,7 +1493,7 @@ def create_app() -> FastAPI: uid = await asyncio.to_thread(_wx.get_user_by_wecom_userid, wecom_userid) if uid is None: return _wecom_page( - "该企业微信还未绑定 zcbot 账号:请先在电脑端 zcbot 控制台登录," + f"该企业微信还未绑定{BRAND}账号:请先在电脑端{BRAND}控制台登录," "点左栏「企业微信」渠道卡片完成绑定,再回来打开本应用。", False) token, _exp = mint_token(auth_cfg, uid) # JWT 是 base64url + '.',UUID 是 hex + '-',都无需再转义,fragment 安全 @@ -1529,7 +1535,7 @@ def create_app() -> FastAPI: """自检:给已绑用户推一条企业微信测试消息(无 24h 窗口约束)。""" from core.wechat import service as _wx res = await asyncio.to_thread( - _wx.push_wecom, user_id, "zcbot 测试消息:企业微信绑定成功,这条来自你的 zcbot。" + _wx.push_wecom, user_id, f"{BRAND} 测试消息:企业微信绑定成功,推送通道正常。" ) return {"ok": res.ok, "reason": res.reason} diff --git a/web/static/dev.html b/web/static/dev.html index 5c1112d..2995e9e 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -2,7 +2,7 @@ -zcbot 控制台 +总院科研辅助助手 @@ -1154,14 +1154,25 @@ /* 折叠按钮在手机不可见 */ #pane-toggle-left, #pane-toggle-right, .splitter { display: none !important; } - /* header 紧凑化 */ + /* header 紧凑化;品牌名较长 → 标题单行省略,不挤掉右侧按钮 */ header { padding: 6px 10px; gap: 6px; flex-wrap: wrap; } header .who { display: none; } - header .title { font-size: 14px; } - /* tab 按钮:整行铺底,order:99 让它换行到 header 第二行 */ - .mobile-tabs { display: flex; order: 99; flex-basis: 100%; } + header .brand { min-width: 0; flex-shrink: 1; } + header .title { font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + /* tab 行:order 让 tabs 换行到 header 末行,「管理」(order 更大)跟到 tabs 右侧 + 成小按钮 —— 首行只留 brand + 改密码/退出。flex-basis 70% 是换行开关: + 首行剩余空间塞不下 70% → tabs 必换行;换行后 70% + 管理(~15%)同行放得下 */ + .mobile-tabs { display: flex; order: 99; flex: 1 1 70%; } .mobile-tabs button { flex: 1; } + #hd-admin { order: 100; flex-shrink: 0; padding: 5px 10px; font-size: 12px; } #hd-chpw, #hd-logout { padding: 4px 8px; font-size: 12px; } + /* embed(企业微信免登)时 brand/改密码/退出已藏 → tabs +「管理」自然收成单行 */ + body.embed-mode header .spacer { display: none; } + + /* 浮层菜单:加大触控目标(桌面 6px 高度在手机上难点准) */ + #floating-menu { min-width: 160px; max-width: calc(100vw - 8px); } + #media-model-pop { max-width: calc(100vw - 8px); } + .dd-item { padding: 10px 16px; font-size: 14px; } /* iOS 防 focus 自动缩放:input/textarea 字号 ≥ 16 */ textarea, @@ -1178,9 +1189,10 @@ #chat-meta .desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60vw; } - /* 模型下拉:手机端 label 文字 → emoji */ + /* 模型下拉:手机端 label 文字 → emoji;select 限宽防长模型名撑破 meta 行 */ .mdl-text { display: none; } .mdl-icon { display: inline; } + #chat-meta select { max-width: 48vw; } /* 对话面板顶栏:藏 "对话" label / spacer,5 个按钮自然换行,文字 nowrap 防内部断行 */ #pane-mid > .pane-head { flex-wrap: wrap; gap: 6px; padding: 6px 8px; } #pane-mid > .pane-head > .label, @@ -1268,7 +1280,7 @@
-
zcbot
+
总院科研辅助助手

登录到控制台

@@ -1394,7 +1406,7 @@
-
绑定后可在个人微信「微信 ClawBot」里直接和 zcbot 对话;近 24h 有互动时,定时简报 / 结果也能主动推给你。
+
绑定后可在个人微信「微信 ClawBot」里直接和助手对话;近 24h 有互动时,定时简报 / 结果也能主动推给你。
加载中…
@@ -1411,7 +1423,7 @@
企业微信(推送 + 对话)
-
无条件主动推(不挑活跃度、无 24h 窗口),适合定时简报必达。扫码授权一次拿成员身份。管理员在应用配「接收消息」回调后,还可在企业微信里直接和 zcbot 对话。
+
无条件主动推(不挑活跃度、无 24h 窗口),适合定时简报必达。扫码授权一次拿成员身份。管理员在应用配「接收消息」回调后,还可在企业微信里直接和助手对话。
加载中…
@@ -1457,7 +1469,7 @@
-
zcbot
+
总院科研辅助助手
@@ -1574,7 +1586,7 @@
加载中…
- + 存储 diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 055b612..8ac9c92 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -556,8 +556,15 @@ function openMediaModelPop(btn) { pop.style.visibility = "hidden"; pop.classList.add("show"); const ph = pop.offsetHeight || 100; - pop.style.right = Math.max(4, window.innerWidth - rect.right) + "px"; - pop.style.left = "auto"; + const pw = pop.offsetWidth || 220; + // 同 showMenu:触发按钮靠左时右对齐会冲出左边缘 → 改左对齐并钳在视口内 + if (rect.right - pw < 4) { + pop.style.left = Math.max(4, Math.min(rect.left, window.innerWidth - pw - 4)) + "px"; + pop.style.right = "auto"; + } else { + pop.style.right = Math.max(4, window.innerWidth - rect.right) + "px"; + pop.style.left = "auto"; + } pop.style.top = (rect.bottom + ph + 8 > window.innerHeight) ? Math.max(4, rect.top - ph - 4) + "px" : (rect.bottom + 4) + "px"; diff --git a/web/static/js/dom.js b/web/static/js/dom.js index d9a6d57..ba2def2 100644 --- a/web/static/js/dom.js +++ b/web/static/js/dom.js @@ -28,8 +28,16 @@ export function showMenu(triggerEl, items) { menu.style.visibility = "hidden"; menu.classList.add("show"); const mh = menu.offsetHeight || 120; - menu.style.right = Math.max(4, window.innerWidth - rect.right) + "px"; - menu.style.left = "auto"; + const mw = menu.offsetWidth || 132; + // 水平:默认右缘对齐触发按钮;触发按钮靠左(手机上 ⋯ 在顶栏左侧)会把菜单推出 + // 左边缘 → 改成左缘对齐,并双向钳在视口内 + if (rect.right - mw < 4) { + menu.style.left = Math.max(4, Math.min(rect.left, window.innerWidth - mw - 4)) + "px"; + menu.style.right = "auto"; + } else { + menu.style.right = Math.max(4, window.innerWidth - rect.right) + "px"; + menu.style.left = "auto"; + } if (rect.bottom + mh + 8 > window.innerHeight) { menu.style.top = Math.max(4, rect.top - mh - 4) + "px"; } else { diff --git a/web/static/js/main.js b/web/static/js/main.js index 128d467..2545e8d 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -79,15 +79,22 @@ async function loadStorage() { } -// 版本号:/healthz 是 auth 豁免接口,embed / 未登录都拿得到 → boot 时无条件拉一次填进左栏底部。 -// 失败静默(版本号是装饰性信息,不该因网络抖动报错)。 +// 版本号 + 品牌名:/healthz 是 auth 豁免接口,embed / 未登录都拿得到 → boot 时无条件拉一次。 +// 版本号填左栏底部;品牌名(ZCBOT_BRAND_NAME,默认「总院科研辅助助手」)覆盖页面标题 / +// 顶栏 / 登录卡 —— 静态 HTML 里写的是默认品牌,env 改名时这里统一刷。失败静默。 async function loadVersion() { - const el = $("app-version"); - if (!el) return; try { const r = await fetch("/healthz"); const d = await r.json(); - if (d && d.version) el.textContent = "v" + d.version; + const el = $("app-version"); + if (el && d && d.version) el.textContent = "v" + d.version; + if (d && d.brand) { + document.title = d.brand; + const hd = document.querySelector("header .brand .title"); + if (hd) hd.textContent = d.brand; + const li = document.querySelector("#login .brand .name"); + if (li) li.textContent = d.brand; + } } catch (e) {} } diff --git a/web/static/wechat_bind.html b/web/static/wechat_bind.html index c894bf4..48339a7 100644 --- a/web/static/wechat_bind.html +++ b/web/static/wechat_bind.html @@ -3,7 +3,7 @@ -绑定微信 · zcbot +绑定微信 · 总院科研辅助助手