feat(web): 移动端显示修复 + 对外品牌名 ZCBOT_BRAND_NAME(bump 0.45.0)
移动端: - 浮层菜单(⋯ 任务菜单 / ⚙ 媒体模型弹层)原只右对齐触发按钮,手机上触发钮 靠左时菜单冲出屏幕左缘盖住正文;改为放不下时切左对齐 + 双向钳视口内 - 手机断点:菜单项加大触控目标、模型下拉限宽 48vw、header 品牌名单行省略 - 「管理」入口挪到 任务/对话/文件 tabs 行右侧小按钮(CSS order 实现, embed 与普通模式同款,embed 专属覆盖规则随之删除) 品牌名: - 新 env ZCBOT_BRAND_NAME(默认「总院科研辅助助手」),zcbot 只是内部代号 - /healthz 返 brand 字段,前端 boot 拉取覆盖页面标题/顶栏/登录卡 - 微信/企微测试推送、OAuth 提示页、绑定页等用户可见文案改品牌名或中性「助手」 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b0a8802d35
commit
7f77e5aca9
|
|
@ -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 重建占位段,工具轮之后不再空窗。
|
||||
|
|
|
|||
3
RUN.md
3
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`)。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.44.0"
|
||||
__version__ = "0.45.0"
|
||||
|
|
|
|||
18
web/app.py
18
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"<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'>"
|
||||
f"<div style='font:16px system-ui;max-width:420px;margin:80px auto;text-align:center;color:{color}'>"
|
||||
f"{msg}</div><div style='text-align:center;color:#888;font:13px system-ui'>可关闭本页返回 zcbot</div>"
|
||||
f"{msg}</div><div style='text-align:center;color:#888;font:13px system-ui'>可关闭本页返回{BRAND}</div>"
|
||||
)
|
||||
|
||||
@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}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>zcbot 控制台</title>
|
||||
<title>总院科研辅助助手</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23c0392b'/%3E%3Cstop offset='1' stop-color='%238e2a20'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' rx='22' fill='url(%23g)'/%3E%3Ctext x='50' y='54' font-family='Arial,Helvetica,sans-serif' font-size='62' font-weight='700' fill='%23fff' text-anchor='middle' dominant-baseline='central'%3EZ%3C/text%3E%3C/svg%3E" />
|
||||
|
||||
|
|
@ -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 @@
|
|||
<div class="card">
|
||||
<div class="brand">
|
||||
<div class="logo">Z</div>
|
||||
<div class="name">zcbot</div>
|
||||
<div class="name">总院科研辅助助手</div>
|
||||
</div>
|
||||
<h2>登录到控制台</h2>
|
||||
<div class="tabs">
|
||||
|
|
@ -1394,7 +1406,7 @@
|
|||
<button id="wx-close" class="sk-x" title="关闭">✕</button>
|
||||
</h3>
|
||||
<div id="wx-body">
|
||||
<div class="muted" style="font-size:12px;margin-bottom:12px;">绑定后可在个人微信「微信 ClawBot」里直接和 zcbot 对话;近 24h 有互动时,定时简报 / 结果也能主动推给你。</div>
|
||||
<div class="muted" style="font-size:12px;margin-bottom:12px;">绑定后可在个人微信「微信 ClawBot」里直接和助手对话;近 24h 有互动时,定时简报 / 结果也能主动推给你。</div>
|
||||
<div id="wx-state" class="wx-status wait">加载中…</div>
|
||||
<div class="wx-acts">
|
||||
<button id="wx-bind" class="small">绑定 / 重新绑定</button>
|
||||
|
|
@ -1411,7 +1423,7 @@
|
|||
</ul>
|
||||
<hr style="border:none;border-top:1px solid var(--line,#d0d7de);margin:18px 0;">
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:6px;">企业微信(推送 + 对话)</div>
|
||||
<div class="muted" style="font-size:12px;margin-bottom:10px;">无条件主动推(不挑活跃度、无 24h 窗口),适合定时简报必达。扫码授权一次拿成员身份。管理员在应用配「接收消息」回调后,还可在企业微信里直接和 zcbot 对话。</div>
|
||||
<div class="muted" style="font-size:12px;margin-bottom:10px;">无条件主动推(不挑活跃度、无 24h 窗口),适合定时简报必达。扫码授权一次拿成员身份。管理员在应用配「接收消息」回调后,还可在企业微信里直接和助手对话。</div>
|
||||
<div id="wc-state" class="wx-status wait">加载中…</div>
|
||||
<div class="wx-acts">
|
||||
<button id="wc-bind" class="small">扫码绑定</button>
|
||||
|
|
@ -1457,7 +1469,7 @@
|
|||
<header>
|
||||
<div class="brand">
|
||||
<div class="logo">Z</div>
|
||||
<div class="title">zcbot</div>
|
||||
<div class="title">总院科研辅助助手</div>
|
||||
</div>
|
||||
<div class="who" id="hd-who"></div>
|
||||
<div class="spacer"></div>
|
||||
|
|
@ -1574,7 +1586,7 @@
|
|||
<div id="file-crumbs" class="crumbs muted">加载中…</div>
|
||||
<div id="file-list"></div>
|
||||
<div id="storage-foot" class="storage-foot" title="">
|
||||
<span id="app-version" title="zcbot 版本"></span>
|
||||
<span id="app-version" title="版本号"></span>
|
||||
<span class="lbl">存储</span>
|
||||
<span class="bar"><i id="storage-foot-bar"></i></span>
|
||||
<span class="txt" id="storage-foot-txt"></span>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>绑定微信 · zcbot</title>
|
||||
<title>绑定微信 · 总院科研辅助助手</title>
|
||||
<style>
|
||||
:root { --fg:#1f2328; --muted:#656d76; --line:#d0d7de; --accent:#a01e1e; --bg:#f6f8fa; }
|
||||
* { box-sizing: border-box; }
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
<body>
|
||||
<div class="wrap"><div class="card">
|
||||
<h1>绑定微信(ClawBot)</h1>
|
||||
<p class="sub">绑定后可在个人微信「微信 ClawBot」里直接和 zcbot 对话;近 24h 内有互动时,定时简报 / 结果也能主动推给你。</p>
|
||||
<p class="sub">绑定后可在个人微信「微信 ClawBot」里直接和助手对话;近 24h 内有互动时,定时简报 / 结果也能主动推给你。</p>
|
||||
|
||||
<div id="state" class="status wait">加载中…</div>
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ function setState(cls, msg) {
|
|||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!token()) { setState("err", "未登录:请先在 zcbot 主页登录,再打开本页。"); return; }
|
||||
if (!token()) { setState("err", "未登录:请先在控制台登录,再打开本页。"); return; }
|
||||
try {
|
||||
const b = await api("/v1/wechat/bind");
|
||||
if (b.bound) {
|
||||
|
|
@ -147,6 +147,10 @@ $("btn-test").onclick = async () => {
|
|||
};
|
||||
|
||||
refresh();
|
||||
// 品牌名(ZCBOT_BRAND_NAME)刷页面标题;/healthz auth 豁免,失败静默保留默认
|
||||
fetch("/healthz").then(r => r.json())
|
||||
.then(d => { if (d && d.brand) document.title = "绑定微信 · " + d.brand; })
|
||||
.catch(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue