@yxzpro/agent-acta 2.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LOGFORMATS.md +960 -0
- package/README.md +238 -0
- package/REFERENCE.md +1428 -0
- package/agent-acta-page.html +1998 -0
- package/agent-acta-server.mjs +1066 -0
- package/agent-acta-widget.html +724 -0
- package/archive.mjs +335 -0
- package/core/log.mjs +20 -0
- package/core/service.mjs +4285 -0
- package/hooks/cursor-usage-hook.mjs +89 -0
- package/mcp-server.mjs +372 -0
- package/package.json +68 -0
- package/page/dialog-add-agent.js +65 -0
- package/page/dialog-alert.js +159 -0
- package/page/dialog-archive.js +342 -0
- package/page/dialog-compare.js +238 -0
- package/page/dialog-ctxw.js +101 -0
- package/page/dialog-diag.js +72 -0
- package/page/dialog-heatmap.js +215 -0
- package/page/dialog-selftest.js +202 -0
- package/page/dialog-sess-timeline.js +108 -0
- package/page/dialog-stats.js +694 -0
- package/page/dialog-usage.js +158 -0
- package/page/log-card.js +277 -0
- package/page/log-view.js +273 -0
- package/page/sess-view.js +382 -0
- package/page/shared.js +408 -0
- package/page/sidebar.js +129 -0
- package/page/style.css +973 -0
- package/page/topbar.js +61 -0
- package/parsers/atomcode.mjs +321 -0
- package/parsers/buddy.mjs +272 -0
- package/parsers/buddyext.mjs +582 -0
- package/parsers/claude.mjs +496 -0
- package/parsers/cline.mjs +468 -0
- package/parsers/codearts.mjs +96 -0
- package/parsers/codex.mjs +188 -0
- package/parsers/comate.mjs +195 -0
- package/parsers/copilot.mjs +308 -0
- package/parsers/cursor.mjs +493 -0
- package/parsers/devindb.mjs +358 -0
- package/parsers/discovery.mjs +1230 -0
- package/parsers/doubao.mjs +406 -0
- package/parsers/dsh.mjs +377 -0
- package/parsers/gemini.mjs +267 -0
- package/parsers/generic.mjs +494 -0
- package/parsers/hermes.mjs +398 -0
- package/parsers/kilo.mjs +42 -0
- package/parsers/kimi.mjs +249 -0
- package/parsers/mimocode.mjs +531 -0
- package/parsers/minimax.mjs +503 -0
- package/parsers/openclaw.mjs +490 -0
- package/parsers/opencode.mjs +465 -0
- package/parsers/shared.mjs +418 -0
- package/parsers/trace.mjs +66 -0
- package/parsers/tracecode.mjs +348 -0
- package/parsers/traedb.mjs +528 -0
- package/parsers/zcode.mjs +356 -0
- package/plugin/client.js +51 -0
- package/plugin/cordis.patch.yml +5 -0
- package/plugin/plugin.mjs +220 -0
- package/search.mjs +554 -0
- package/tools/grab-trae-key.ps1 +265 -0
- package/vendor/element-plus-icons.js +3 -0
- package/vendor/element-plus.css +1 -0
- package/vendor/element-plus.dark.css +1 -0
- package/vendor/element-plus.full.min.js +65 -0
- package/vendor/element-plus.zh-cn.js +2 -0
- package/vendor/svg/antgroup-color.svg +1 -0
- package/vendor/svg/bytedance-color.svg +1 -0
- package/vendor/svg/chatglm-color.svg +1 -0
- package/vendor/svg/claude-color.svg +1 -0
- package/vendor/svg/claudecode-color.svg +1 -0
- package/vendor/svg/cline.svg +1 -0
- package/vendor/svg/codebuddy-color.svg +1 -0
- package/vendor/svg/codebuddy.svg +1 -0
- package/vendor/svg/codex-color.svg +1 -0
- package/vendor/svg/copilot.svg +1 -0
- package/vendor/svg/cursor.svg +1 -0
- package/vendor/svg/deepseek-color.svg +1 -0
- package/vendor/svg/deepseek-harness-whale.svg +8 -0
- package/vendor/svg/doubao-color.svg +1 -0
- package/vendor/svg/gemini-color.svg +1 -0
- package/vendor/svg/glmv-color.svg +1 -0
- package/vendor/svg/grok.svg +1 -0
- package/vendor/svg/hermesagent.svg +1 -0
- package/vendor/svg/huawei-color.svg +1 -0
- package/vendor/svg/hunyuan-color.svg +1 -0
- package/vendor/svg/kilocode-avatar.svg +1 -0
- package/vendor/svg/kilocode.svg +1 -0
- package/vendor/svg/kimi-color.svg +1 -0
- package/vendor/svg/kimi.svg +1 -0
- package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
- package/vendor/svg/logo-AtomGit-dark.svg +4 -0
- package/vendor/svg/logo-AtomGit-light.svg +4 -0
- package/vendor/svg/meta-color.svg +1 -0
- package/vendor/svg/mimo.svg +66 -0
- package/vendor/svg/minimax-code-logo.svg +8 -0
- package/vendor/svg/minimax-color.svg +1 -0
- package/vendor/svg/moonshot.svg +1 -0
- package/vendor/svg/nex-color.svg +1 -0
- package/vendor/svg/nex.svg +1 -0
- package/vendor/svg/nvidia-color.svg +1 -0
- package/vendor/svg/openai.svg +1 -0
- package/vendor/svg/openclaw-color.svg +1 -0
- package/vendor/svg/openclaw.svg +1 -0
- package/vendor/svg/opencode.svg +1 -0
- package/vendor/svg/openrouter-color.svg +1 -0
- package/vendor/svg/qoder-color.svg +1 -0
- package/vendor/svg/qwen-color.svg +1 -0
- package/vendor/svg/sensenova-color.svg +1 -0
- package/vendor/svg/stepfun.svg +1 -0
- package/vendor/svg/trae-color.svg +1 -0
- package/vendor/svg/traework-v2.svg +1 -0
- package/vendor/svg/wenxin-color.svg +1 -0
- package/vendor/svg/windsurf.svg +1 -0
- package/vendor/svg/workbuddy-color.svg +29 -0
- package/vendor/svg/xiaomimimo.svg +1 -0
- package/vendor/svg/yuanbao-color.svg +1 -0
- package/vendor/svg/yuanbao.svg +1 -0
- package/vendor/svg/zai.svg +1 -0
- package/vendor/svg/zhipu-color.svg +1 -0
- package/vendor/vue.global.prod.js +9 -0
- package/widget/main.js +202 -0
- package/widget/package.json +7 -0
- package/widget/preload.js +11 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<!-- 服务端发这张页面时把 content 换成进程指纹(见 agent-acta-server.mjs 的 '/widget' 路由)。
|
|
7
|
+
全文件只放这一处、不参与 JS 判断的左右两边 —— 否则注入后两边一起变,永远判「一致」。 -->
|
|
8
|
+
<meta name="agent-log-build" content="__AGENT_LOG_BUILD__">
|
|
9
|
+
<title>Agent 实况</title>
|
|
10
|
+
<style>
|
|
11
|
+
:root{
|
|
12
|
+
--bg:#fff; --card:#fff; --border:#e5e7ee; --border-light:#eef0f4;
|
|
13
|
+
--text:#1f2329; --regular:#3d4454; --secondary:#646a73; --placeholder:#8f959e;
|
|
14
|
+
--primary:#3b74e7; --green:#18a058; --red:#d03050; --orange:#c26a1b; --purple:#7c5cd6;
|
|
15
|
+
--panel:#f7f8fa; --scroll:rgba(0,0,0,.2);
|
|
16
|
+
}
|
|
17
|
+
/* ===== 悬浮模式(?float=1,Electron 无边框透明窗口)=====
|
|
18
|
+
深色「毛玻璃」:Win10(19045)拿不到系统的真模糊(DWM 的 acrylic/mica 是 Win11 的东西),
|
|
19
|
+
所以底是 80% 不透明的深色 —— 看着是玻璃感,实际是诚实的高透明,别在这上面堆 backdrop-filter。
|
|
20
|
+
(2026-09-23 从 88% 降到 80%:用户要更透;再低白字在亮色桌面上会飘。卡片与球同皮,一起降。)
|
|
21
|
+
圆角由 #wrap 画;卡片本体不打外阴影(2026-09-21 用户要求去掉),透明留白只剩球的阴影/辉光在用。 */
|
|
22
|
+
html.float{--bg:rgba(9,9,11,.80); --card:rgba(255,255,255,.06); --border:rgba(255,255,255,.12);
|
|
23
|
+
--border-light:rgba(255,255,255,.07); --text:#f4f4f5; --regular:#e4e4e7; --secondary:#a1a1aa;
|
|
24
|
+
--placeholder:#7d8392; --primary:#7dd3fc; --green:#34d399; --red:#f87171; --orange:#fb923c;
|
|
25
|
+
--purple:#d8b4fe; --panel:rgba(255,255,255,.05); --scroll:rgba(255,255,255,.25)}
|
|
26
|
+
/* 2026-09-24 配色换霓虹锌黑系(参照 gemini-code demo 第二版):底 zinc-950、入 sky-300 /
|
|
27
|
+
出 orange-400 / 缓 purple-300、积分 amber-300、状态灯 emerald-400、失败红 red-400。
|
|
28
|
+
--placeholder 没跟 demo 的 zinc-500:80% 透明底叠亮色桌面会洗掉对比度,沿用调过的 #7d8392。 */
|
|
29
|
+
/* ⚠️ 透明窗留边也是「点了没反应」的死区。球的阴影/辉光要罩得住:
|
|
30
|
+
blur/2 + 下移量 ≤ 留白,超出去的部分会被窗口边**切平成直边**,看着就是球外面
|
|
31
|
+
糊了一圈方影子(旧版卡片 0 10px 34px 要 27px、只给了 8px,四方全被切)。 */
|
|
32
|
+
html.float body{background:transparent;padding:12px}
|
|
33
|
+
/* inset 顶部高光:玻璃皮的「上沿受光」,扁平口径下唯一允许的层次线(纯色、1px、不外发光) */
|
|
34
|
+
/* demo 的 glass-border:透明 border + 双层背景(padding-box 画卡底、border-box 画 sky→purple 渐变),
|
|
35
|
+
边框就是一条上 sky 下紫的 1px 微光线。卡底用 var(--bg) 保持 80% 透明,不像 demo 那样实心。 */
|
|
36
|
+
html.float #wrap{border:1px solid transparent;border-radius:16px;overflow:hidden;
|
|
37
|
+
background-image:linear-gradient(var(--bg),var(--bg)),
|
|
38
|
+
linear-gradient(to bottom,rgba(56,189,248,.3),rgba(168,85,247,.15));
|
|
39
|
+
background-origin:border-box;background-clip:padding-box,border-box;
|
|
40
|
+
box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
|
|
41
|
+
/* demo 的「背景弥散光晕」:两上角各一团 sky/purple 模糊圆斑,overflow:hidden 裁在卡内。
|
|
42
|
+
球模式这两个伪元素被后面的辉光规则接管(同特异性、球规则在后),这里只管卡片态。 */
|
|
43
|
+
html.float:not(.ball) #wrap::before{content:'';position:absolute;top:-48px;left:-48px;width:160px;height:160px;
|
|
44
|
+
border-radius:50%;background:rgba(56,189,248,.18);filter:blur(48px);pointer-events:none}
|
|
45
|
+
html.float:not(.ball) #wrap::after{content:'';position:absolute;top:-48px;right:-48px;width:160px;height:160px;
|
|
46
|
+
border-radius:50%;background:rgba(168,85,247,.16);filter:blur(48px);pointer-events:none}
|
|
47
|
+
/* 内容压到光晕之上(同 demo 内容层的 z-10) */
|
|
48
|
+
html.float:not(.ball) header,html.float:not(.ball) #stats,html.float:not(.ball) #feed{position:relative;z-index:1}
|
|
49
|
+
/* 拖区:整个顶栏拖着走;里面的链接/按钮单独豁免,不然点不动。
|
|
50
|
+
⚠️ drag 区在 Windows 上是系统级命中测试,落在里面的元素收不到 click —— 所以小球也要 no-drag。 */
|
|
51
|
+
html.float header{-webkit-app-region:drag;user-select:none}
|
|
52
|
+
html.float header a,html.float header button,html.float header .dot{-webkit-app-region:no-drag}
|
|
53
|
+
/* 窗控按钮(最小化/关闭):只在悬浮模式存在,悬停顶栏才显形 */
|
|
54
|
+
.winbtn{display:none}
|
|
55
|
+
html.float .winbtn{display:inline}
|
|
56
|
+
/* 顶栏动作(重扫/面板↗/○/✕)平时收起、悬停顶栏才显形 —— 324px 的顶栏,常驻按钮吃掉四分之一。
|
|
57
|
+
必须带 visibility:opacity:0 的按钮看不见但**还能点**,而且它带着 no-drag —— 在 drag 区里
|
|
58
|
+
是个「拖不动的死点」,用户会以为是拖动失灵。 */
|
|
59
|
+
/* 悬停显隐的**触发面必须是 #wrap,不能是 header**:header 整条是 -webkit-app-region:drag
|
|
60
|
+
(系统级命中测试 HTCAPTION),鼠标在它上面根本收不到 mouseover ⇒ `header:hover` 在 Electron 里
|
|
61
|
+
永不成立;而三个能收事件的子元素(按钮/链接)此刻正是 visibility:hidden,隐藏元素不参与命中,
|
|
62
|
+
于是「靠 hover 出现」自己把自己锁死了 —— 实测就是顶栏按钮怎么悬都不出来(浏览器里能复现不了:
|
|
63
|
+
Chrome 不实现 app-region,header:hover 照常触发,所以这个坑只在真卡片上露)。
|
|
64
|
+
#wrap 的正文区不是拖区,hover 得到 ⇒ 以整张卡为触发面。 */
|
|
65
|
+
html.float header .winbtn,html.float header .hd-act{opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s}
|
|
66
|
+
html.float #wrap:hover header .winbtn,html.float #wrap:hover header .hd-act,
|
|
67
|
+
html.float #wrap:focus-within header .winbtn,html.float #wrap:focus-within header .hd-act{opacity:1;visibility:visible}
|
|
68
|
+
/* 悬浮模式布局收紧一档:窗口小,间距字号全跟着降。留白保持 12px 不动 ——
|
|
69
|
+
球的阴影还要它罩住(见上面那条),卡片模式留着只是同一窗口壳的代价。 */
|
|
70
|
+
html.float header{padding:6px 8px}
|
|
71
|
+
html.float #stats{padding:6px 8px}
|
|
72
|
+
html.float .stat-big{font-size:15px;font-weight:900;text-shadow:0 0 12px rgba(255,255,255,.45)} /* demo 的 font-black + glow-text */
|
|
73
|
+
html.float .stat-sub{font-size:10.5px}
|
|
74
|
+
/* 汇总区的入/出/缓数字带发光(demo 的 glow-blue / glow-amber);缓存紫 demo 没给光,不加 */
|
|
75
|
+
html.float #stats .tin{text-shadow:0 0 8px rgba(125,211,252,.55);font-weight:700}
|
|
76
|
+
html.float #stats .tout{text-shadow:0 0 8px rgba(251,146,60,.55);font-weight:700}
|
|
77
|
+
html.float #stats .tcache{font-weight:700}
|
|
78
|
+
html.float #feed{padding:5px 6px 16px;user-select:none} /* 底部留位给拖拽柄;user-select:none —— 列表区整片是拖窗把手(见 JS feed 那段),不禁掉选择一拖就糊一片蓝 */
|
|
79
|
+
/* 卡片布局口径对齐根目录 gemini-code demo(列表项三段式):
|
|
80
|
+
r1 左右分栏 —— 左「灯 + agent + 型号徽标」、右端时间(DOM 里 time 仍在灯后,float 用 order 挪行尾,
|
|
81
|
+
不动 DOM 是为了普通模式保持原样);卡内边距 10/12、卡距 10(demo p-3 / space-y-2.5 按 324px 窗宽缩一档);
|
|
82
|
+
r1 下距 6、摘要 12px 下距 8、明细行 11px + 8px 列距(demo 的 mb-1.5 / mb-2 / gap-2),时长照旧压右端。
|
|
83
|
+
状态点叠 demo 同款辉光(shadow-[0_0_8px_#34d399]);明细数字发光不加 —— demo 列表项里的数字本身不带光。 */
|
|
84
|
+
html.float .card{padding:10px 12px;margin-bottom:10px}
|
|
85
|
+
html.float .card .r1{margin-bottom:6px}
|
|
86
|
+
html.float .card .time{order:1;margin-left:auto}
|
|
87
|
+
html.float .card .pv{font-size:12px;margin:0 0 8px}
|
|
88
|
+
html.float .card .r3{gap:8px;font-size:11px}
|
|
89
|
+
html.float .sdot.ok{box-shadow:0 0 8px rgba(52,211,153,.7)}
|
|
90
|
+
html.float .stat-label{font-size:10.5px}
|
|
91
|
+
/* 右下角拖拽柄:无边框透明窗没有系统拉边框,缩放全靠它(悬停卡片才显形) */
|
|
92
|
+
#wrap{position:relative}
|
|
93
|
+
.grip{display:none}
|
|
94
|
+
html.float .grip{display:block;position:absolute;right:4px;bottom:4px;width:13px;height:13px;z-index:5;
|
|
95
|
+
cursor:nwse-resize;opacity:0;transition:opacity .2s;touch-action:none;
|
|
96
|
+
border-right:2px solid var(--secondary);border-bottom:2px solid var(--secondary);border-bottom-right-radius:5px}
|
|
97
|
+
html.float #wrap:hover .grip{opacity:.45}
|
|
98
|
+
html.float .grip:hover{opacity:.9 !important}
|
|
99
|
+
|
|
100
|
+
/* ===== 球模式(html.float.ball):收成一颗球,只显示今日 token =====
|
|
101
|
+
2026-09-24 水球换成「暗黑发光球」(参照 gemini-code demo:纯色球底 + sky 辉光脉动 + 悬停放大)。
|
|
102
|
+
⚠️ 球**不用** -webkit-app-region:drag —— 那东西在 Windows 上是系统级命中测试(HTCAPTION),
|
|
103
|
+
鼠标事件到不了页面,click 会被整个吞掉(2026-09-20 实测:CDP 合成事件能点、真鼠标不行)。
|
|
104
|
+
拖动走 pointer 事件 + floatApi.dragBegin/dragBy(IPC 让壳挪窗口),点击与拖动按 4px 阈值分家。 */
|
|
105
|
+
/* 球的大小只有这一个旋钮:--bs = 球本体直径。球体几何都写成 calc(var(--bs) * N / 116),
|
|
106
|
+
N 是 116px 那版实测调出来的数值,改 --bs 就整球等比缩放(字号、按钮位置一起跟)。
|
|
107
|
+
⚠️ 必须与壳同步:widget/main.js 的 BALL_SIZE = --bs + 24(body 四周各 12px 透明留白)。
|
|
108
|
+
只对页面不改壳:球会缩小但窗口不变,多出来的一圈透明区照旧吞鼠标点击(透明区不穿透),
|
|
109
|
+
看着像「球周围一圈点不动的死区」—— 所以 --bs 一动,壳那边要 npm pack + 全局重装才生效。 */
|
|
110
|
+
html.float.ball{--bs:88px}
|
|
111
|
+
#ball{display:none}
|
|
112
|
+
html.float.ball header,html.float.ball #stats,html.float.ball #feed,html.float.ball .grip,html.float.ball #stale{display:none}
|
|
113
|
+
/* 球的留白同卡片:12px(html.float body 那条已经给了,这里不另设)—— 阴影/辉光罩得住才不会被切平 */
|
|
114
|
+
html.float.ball #wrap{background:none;border:none;box-shadow:none;overflow:visible;align-items:center;justify-content:center}
|
|
115
|
+
/* 球体 = demo 的 neon-ball:实心 zinc-950 底 + sky→indigo→purple 渐变环(glass-border 同款
|
|
116
|
+
双背景技法,环宽 2px)。球比卡片更像个物件,用实心底不跟卡片的 80% 透明 —— 渐变环要实心底才压得住。
|
|
117
|
+
断连态的 .off 把渐变环换成红色;悬停放大 1.08(demo 是 1.10,88px 球在 112px 窗里 1.10 只剩 7px 边)。
|
|
118
|
+
入场 scale-in:卡片 ⇄ 球切换时球是「长出来」的,不是凭空闪现。 */
|
|
119
|
+
html.float.ball #ball{display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
120
|
+
position:relative;width:var(--bs);height:var(--bs);border-radius:50%;
|
|
121
|
+
cursor:pointer;user-select:none;touch-action:none;
|
|
122
|
+
border:2px solid transparent;
|
|
123
|
+
background:linear-gradient(#09090b,#09090b) padding-box,
|
|
124
|
+
linear-gradient(135deg,#38bdf8,#6366f1 45%,#a855f7) border-box;
|
|
125
|
+
transition:transform .2s;
|
|
126
|
+
animation:ball-in .34s cubic-bezier(.3,1.4,.5,1)}
|
|
127
|
+
@keyframes ball-in{from{opacity:0;transform:scale(.55)}to{opacity:1;transform:scale(1)}}
|
|
128
|
+
html.float.ball #ball:hover{transform:scale(1.08)}
|
|
129
|
+
/* ⚠️ 球的阴影/辉光都不画在 #ball 上 —— 元素自身带动画/过渡时 Chromium 走合成快路径,
|
|
130
|
+
box-shadow 会忽略 border-radius 画成方的(2026-09-20 白底实测锁定)。
|
|
131
|
+
所以挪给 #wrap 的两个圆形假影:::before 常驻接触影 + sky 辉光,::after 是 purple 辉光层 ——
|
|
132
|
+
两层同相 opacity 脉动(opacity 是合成属性,不踩上面那个坑)= demo 的 neon-glow 双色呼吸。
|
|
133
|
+
demo 原版 blur 到 25/50px,这里压到 10/12px:透明窗留白就 12px,超出的部分会被窗口边切平。
|
|
134
|
+
接触影参数不变:88px 球最外一圈 ≈ 8.3px,仍 < 12px。
|
|
135
|
+
background/filter 必须显式归 none —— 卡片态的弥散光晕也写在这两个伪元素上,同特异性靠后覆盖,
|
|
136
|
+
不重置的话 160px 的模糊圆斑会漏到球后面。 */
|
|
137
|
+
html.float.ball #wrap::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
|
138
|
+
width:var(--bs);height:var(--bs);border-radius:50%;pointer-events:none;background:none;filter:none;
|
|
139
|
+
box-shadow:0 calc(var(--bs) * 3 / 116) calc(var(--bs) * 14 / 116) rgba(0,0,0,.45),
|
|
140
|
+
0 calc(var(--bs) * 1 / 116) calc(var(--bs) * 3 / 116) rgba(0,0,0,.3),
|
|
141
|
+
0 0 10px rgba(56,189,248,.5);
|
|
142
|
+
animation:glow-breathe 2.5s ease-in-out infinite}
|
|
143
|
+
html.float.ball #wrap::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
|
144
|
+
width:var(--bs);height:var(--bs);border-radius:50%;pointer-events:none;background:none;filter:none;
|
|
145
|
+
box-shadow:0 0 12px 2px rgba(168,85,247,.42);
|
|
146
|
+
animation:glow-breathe 2.5s ease-in-out infinite}
|
|
147
|
+
@keyframes glow-breathe{0%,100%{opacity:.45}50%{opacity:1}}
|
|
148
|
+
/* 断连:渐变环换红(background 两层一起换,padding-box 的实心底不动) */
|
|
149
|
+
html.float.ball #ball.off{background:linear-gradient(#09090b,#09090b) padding-box,
|
|
150
|
+
linear-gradient(135deg,rgba(248,113,113,.9),rgba(248,113,113,.45)) border-box}
|
|
151
|
+
/* 数字整块正中:水球时代要躲水线才往上提 14%,发光球没有水线。z-index 压着辉光假影。 */
|
|
152
|
+
#ball .ball-num{display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:2}
|
|
153
|
+
/* 数字纯白 + glow-text;24/116 这档 88px 球上约 18px,「9999.9万」这种 7 字串约 76px、球内宽 84px 放得下。
|
|
154
|
+
transform 过渡是给悬停放大用的(见下面 hover 那条) */
|
|
155
|
+
#ball .ball-num b{color:#fff;font-size:calc(var(--bs) * 24 / 116);font-weight:800;letter-spacing:.2px;font-variant-numeric:tabular-nums;
|
|
156
|
+
text-shadow:0 0 10px rgba(255,255,255,.5);transition:transform .2s ease} /* demo 的 glow-text */
|
|
157
|
+
/* 悬停时数字再放大一档(球本体那条 1.08 之外叠加),移开收回 —— 增量角标挂在 b 里,跟着一起缩 */
|
|
158
|
+
html.float.ball #ball:hover .ball-num b{transform:scale(1.15)}
|
|
159
|
+
/* ⚠️ 副标题规则必须点名 #ball-sub,不能用 .ball-num span —— 大数字本体也是 span(#ball-num 在 b 里),
|
|
160
|
+
同特异性且这条在后,会把大数字盖成 9px 天空蓝(2026-09-24 实测踩中:壳里球数一直是小的蓝字)。
|
|
161
|
+
max() 里的下限是给「球缩到 88px」这一档留的:纯按比例副标题只剩 7.6px、按钮只剩 9px,
|
|
162
|
+
前者读不清、后者点不准。数值取的是「最小也能看/能点」的那一档,不是审美值。 */
|
|
163
|
+
#ball-sub{color:#38bdf8;font-size:max(9px,calc(var(--bs) * 10 / 116));margin-top:calc(var(--bs) * 3 / 116);letter-spacing:.4px}
|
|
164
|
+
#ball .ball-btn{position:absolute;top:calc(var(--bs) * 8 / 116);z-index:3;border:none;background:none;cursor:pointer;
|
|
165
|
+
color:rgba(255,255,255,.85);font-size:max(11px,calc(var(--bs) * 12 / 116));line-height:1;padding:2px;opacity:0;transition:opacity .2s;
|
|
166
|
+
-webkit-app-region:no-drag}
|
|
167
|
+
#ball:hover .ball-btn{opacity:.9}
|
|
168
|
+
#ball .bopen{left:calc(var(--bs) * 12 / 116)} #ball .bclose{right:calc(var(--bs) * 12 / 116)}
|
|
169
|
+
|
|
170
|
+
/* ===== 卡片 ⇄ 球的形变动画(「从哪来回哪去」)=====
|
|
171
|
+
窗口尺寸只能瞬移(壳 setBounds 一步跳、锚定右下角),形变全在页面里做,把窗口跳变藏进
|
|
172
|
+
「前后两帧画面重合」的瞬间:
|
|
173
|
+
收球(morph-out):窗口还是卡片大小时,#wrap 朝球心缩成一个点并淡出,到位同帧才换 class +
|
|
174
|
+
叫壳缩窗 —— 紧跟着真球自带的 ball-in 在那个点弹出,看着就是卡片坍缩成了球。
|
|
175
|
+
回卡片(morph-in):壳先把窗口放大,#wrap 从球心那个点长回整张卡。
|
|
176
|
+
球心位置:球窗 112px、球居中 ⇒ 距窗口右/下边各 56px;#wrap 比窗口小一圈 body 留白 12px,
|
|
177
|
+
换算成 #wrap 的 transform-origin 就是 calc(100% - 44px)。 */
|
|
178
|
+
html.float.morph-out #wrap{transform-origin:calc(100% - 44px) calc(100% - 44px);
|
|
179
|
+
animation:wrap-to-ball .26s ease-in forwards}
|
|
180
|
+
html.float.morph-in #wrap{transform-origin:calc(100% - 44px) calc(100% - 44px);
|
|
181
|
+
animation:wrap-from-ball .3s cubic-bezier(.3,1.3,.5,1)}
|
|
182
|
+
@keyframes wrap-to-ball{to{transform:scale(.12);border-radius:50%;opacity:0}}
|
|
183
|
+
@keyframes wrap-from-ball{from{transform:scale(.12);border-radius:50%;opacity:0}}
|
|
184
|
+
/* 回卡片的预藏帧:窗口放大锚定右下角 ⇒ 左上角往左上挪,Windows 在新帧交付前会把
|
|
185
|
+
旧窗口的最后一帧钉在新窗口**左上角**重放 —— 不先把球藏起来,用户就会看到球在左上闪一下
|
|
186
|
+
(2026-09-28 实测)。opacity 挂在 #wrap 上连同两个辉光伪元素一起藏。 */
|
|
187
|
+
html.float.morph-pre #wrap{opacity:0}
|
|
188
|
+
/* 水球整套(液面/波浪/水色档/满球气泡)2026-09-24 移除:换成 gemini-code demo 的暗黑发光球。
|
|
189
|
+
卡通(表情脸/托球小手/☺开关)更早一轮(2026-09-23)已删。 */
|
|
190
|
+
|
|
191
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
192
|
+
html,body{height:100%}
|
|
193
|
+
body{background:var(--bg);color:var(--regular);font:13px/1.55 "PingFang SC",Arial,"Microsoft YaHei",sans-serif;
|
|
194
|
+
-webkit-font-smoothing:antialiased;overflow:hidden}
|
|
195
|
+
/* #wrap 是唯一的高度容器:普通模式它就是整张页面;悬浮模式它是那张圆角卡片 */
|
|
196
|
+
#wrap{height:100%;display:flex;flex-direction:column}
|
|
197
|
+
/* 与主页同一条滚动条纪律:标准属性 + 悬停显形(为什么不用 ::-webkit-scrollbar 做显隐,见主页 CSS 那段实测注释)。
|
|
198
|
+
悬浮卡片例外:窗口窄,显形那条滚动条会占掉右侧一条宽度,日志列表左右不对称 —— 这里彻底藏掉
|
|
199
|
+
(scrollbar-width:none 连布局宽度一起归零),滚轮/拖拽照常滚,位置感交给卡片内容自己。 */
|
|
200
|
+
html.float *{scrollbar-width:none}
|
|
201
|
+
html.float *::-webkit-scrollbar{width:0;height:0;display:none}
|
|
202
|
+
|
|
203
|
+
/* demo 的 font-mono 口径:数字/时间/摘要/明细行走等宽(汉字标签自动回退系统中文字体,不受这条影响) */
|
|
204
|
+
html.float .stat-big,html.float .stat-sub,html.float .time,html.float .model,
|
|
205
|
+
html.float .card .pv,html.float .card .r3,html.float .ball-num{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace}
|
|
206
|
+
|
|
207
|
+
/* ===== 顶栏 ===== */
|
|
208
|
+
header{display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid var(--border-light);flex:none}
|
|
209
|
+
.dot{width:8px;height:8px;border-radius:50%;background:#c9cdd4;flex:none;transition:background .3s}
|
|
210
|
+
.dot.on{background:var(--green);animation:dot-pulse 2.4s ease-out infinite}
|
|
211
|
+
.dot.off{background:var(--red);animation:none}
|
|
212
|
+
/* 连接灯的呼吸脉冲:绿光圈每 2.4s 荡开一次。两个色档分开写(亮/暗两套 --green),
|
|
213
|
+
不用 color-mix —— widget-runtime 里的 Electron 版本不归我们控,少赌一个特性。 */
|
|
214
|
+
@keyframes dot-pulse{0%{box-shadow:0 0 0 0 rgba(24,160,88,.35)}70%{box-shadow:0 0 0 6px rgba(24,160,88,0)}
|
|
215
|
+
100%{box-shadow:0 0 0 0 rgba(24,160,88,0)}}
|
|
216
|
+
html.float .dot.on{animation-name:dot-pulse-f}
|
|
217
|
+
/* float 档在脉冲外圈之外再叠一层常驻光晕(demo 的 shadow-[0_0_8px_#34d399]) */
|
|
218
|
+
@keyframes dot-pulse-f{0%{box-shadow:0 0 8px rgba(52,211,153,.7),0 0 0 0 rgba(52,211,153,.4)}
|
|
219
|
+
70%{box-shadow:0 0 8px rgba(52,211,153,.7),0 0 0 6px rgba(52,211,153,0)}
|
|
220
|
+
100%{box-shadow:0 0 8px rgba(52,211,153,.7),0 0 0 0 rgba(52,211,153,0)}}
|
|
221
|
+
.title{font-weight:700;color:var(--text);font-size:13px}
|
|
222
|
+
header .sp{flex:1}
|
|
223
|
+
header a,header button{border:none;background:none;color:var(--placeholder);cursor:pointer;font-size:12px;
|
|
224
|
+
padding:2px 6px;border-radius:5px;text-decoration:none;font-family:inherit;line-height:1.4}
|
|
225
|
+
header a:hover,header button:hover{background:var(--panel);color:var(--text)}
|
|
226
|
+
|
|
227
|
+
/* ===== 用量汇总 ===== */
|
|
228
|
+
#stats{flex:none;padding:8px 10px;border-bottom:1px solid var(--border-light);background:var(--panel)}
|
|
229
|
+
.stat-row{display:flex;align-items:baseline;gap:8px}
|
|
230
|
+
.stat-row+.stat-row{margin-top:4px}
|
|
231
|
+
.stat-label{font-size:11px;color:var(--placeholder);flex:none;width:28px}
|
|
232
|
+
.stat-big{font-size:19px;font-weight:800;color:var(--text);font-variant-numeric:tabular-nums;line-height:1.2}
|
|
233
|
+
.stat-sub{font-size:11px;color:var(--secondary);font-variant-numeric:tabular-nums}
|
|
234
|
+
.stat-sub b{font-weight:600}
|
|
235
|
+
/* 「N 条」收成 demo 那种药丸角标:float 下是 sky 字 + sky 底 + sky 描边,亮色模式沿用灰底 */
|
|
236
|
+
#today-sub{background:var(--panel);border:1px solid var(--border-light);border-radius:999px;
|
|
237
|
+
padding:1px 8px;color:var(--placeholder)}
|
|
238
|
+
html.float #today-sub{color:#38bdf8;background:rgba(56,189,248,.1);border-color:rgba(56,189,248,.3)}
|
|
239
|
+
.tin{color:var(--primary)} .tout{color:var(--orange)} .tcache{color:var(--purple)}
|
|
240
|
+
#row-credits{display:none} /* 积分条件行:paintStats 按「累计有积分」放开,默认收起 */
|
|
241
|
+
/* 积分是次要信息:比「今天」大数小一档、降亮度,层级拉开(float 模式下 15px → 13px) */
|
|
242
|
+
html.float #row-credits .stat-big{font-size:13px;font-weight:700;color:#fcd34d} /* demo:积分 amber-300 */
|
|
243
|
+
/* 「+xxx」增量角标:统计区那份从大数左上往右上飘(rise-fade-y,transform 方案);
|
|
244
|
+
球上那份从数字下缘往上飘(rise-fade,bottom 方案)—— 它要靠 translateX(-50%) 居中,
|
|
245
|
+
keyframes 不能动 transform,所以两份各走各的 keyframes。
|
|
246
|
+
统计区定位行太挤(大数+tokens+「N 条」),角标放左/右都会压字(2026-09-23 实测两版都重叠),
|
|
247
|
+
只能往数字上方飘。锚用外层 #today-total 而不是 #today-num:数字有滚动动画(tweenNum 在重写
|
|
248
|
+
today-num 的 textContent),角标挂数字里会被滚动帧抹掉。 */
|
|
249
|
+
#today-total,#ball-total{position:relative}
|
|
250
|
+
.token-pop{position:absolute;white-space:nowrap;pointer-events:none;
|
|
251
|
+
font-size:11px;font-weight:700;color:var(--green);font-variant-numeric:tabular-nums}
|
|
252
|
+
#today-total .token-pop{left:2px;top:-13px;animation:rise-fade-y 1.6s ease forwards}
|
|
253
|
+
#ball .token-pop{left:50%;bottom:-16px;font-size:10px;transform:translateX(-50%);
|
|
254
|
+
animation:rise-fade 1.6s ease forwards}
|
|
255
|
+
@keyframes rise-fade{from{opacity:1;bottom:0}to{opacity:0;bottom:14px}}
|
|
256
|
+
@keyframes rise-fade-y{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-12px)}}
|
|
257
|
+
|
|
258
|
+
/* ===== 卡片列表 ===== */
|
|
259
|
+
#feed{flex:1;overflow-y:auto;padding:6px 8px 10px}
|
|
260
|
+
.card{background:var(--card);border:1px solid var(--border-light);border-radius:12px;padding:6px 8px;margin-bottom:6px;
|
|
261
|
+
transition:border-color .2s,background-color .2s;animation:pop .3s cubic-bezier(.3,1.35,.5,1)}
|
|
262
|
+
.card:hover{border-color:var(--border)}
|
|
263
|
+
/* demo 的悬停:底色变深(bg-zinc-800/80)+ 描边转 sky —— float 深色下照这个口径,亮色系照旧只换边框 */
|
|
264
|
+
html.float .card:hover{background:rgba(255,255,255,.09);border-color:rgba(56,189,248,.5)}
|
|
265
|
+
@keyframes pop{from{opacity:0;transform:translateY(-6px) scale(.98)}to{opacity:1;transform:none}}
|
|
266
|
+
/* 球 ⇄ 卡片切换时,卡片的三个区块淡入归位 —— 球模式里它们是 display:none,
|
|
267
|
+
切回来时 display 恢复会重播 animation,白捡一个入场(不用 JS 配合)。 */
|
|
268
|
+
html.float header,html.float #stats,html.float #feed{animation:card-in .26s ease}
|
|
269
|
+
@keyframes card-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
270
|
+
.card .r1{display:flex;align-items:center;gap:6px;min-width:0}
|
|
271
|
+
.sdot{width:7px;height:7px;border-radius:50%;flex:none}
|
|
272
|
+
.sdot.ok{background:var(--green)} .sdot.error{background:var(--red)} .sdot.aborted{background:var(--orange)} .sdot.other{background:#c9cdd4}
|
|
273
|
+
.time{color:var(--placeholder);font-size:11px;font-variant-numeric:tabular-nums;flex:none}
|
|
274
|
+
.agent{font-weight:700;color:var(--text);font-size:12px;flex:none}
|
|
275
|
+
.model{color:var(--secondary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
|
|
276
|
+
/* demo 的型号徽标:灰底描边小胶囊。float 下不再 flex:1 撑满 —— 撑满的胶囊像条横杠,收成自适应宽度 */
|
|
277
|
+
html.float .model{flex:none;max-width:50%;color:#d4d4d8;background:rgba(255,255,255,.07);
|
|
278
|
+
border:1px solid var(--border-light);border-radius:4px;padding:0 4px;font-size:10px}
|
|
279
|
+
.card .pv{color:var(--regular);font-size:12px;margin-top:2px;
|
|
280
|
+
display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;word-break:break-all}
|
|
281
|
+
.card .r3{display:flex;gap:8px;margin-top:3px;font-size:11px;color:var(--secondary);font-variant-numeric:tabular-nums}
|
|
282
|
+
.card .r3 .dur{margin-left:auto;color:var(--placeholder)}
|
|
283
|
+
/* 异常条目的左侧色条:7px 小点在卡片流里扫不出来(主面板是整卡红左边条,这里空间小,收成 inset 竖条)。
|
|
284
|
+
用 inset box-shadow 不用 border-left —— border 会挤内容盒,状态从 ok 变 error 时整卡跳 2px。 */
|
|
285
|
+
.card[data-st=error]{box-shadow:inset 2px 0 0 var(--red)}
|
|
286
|
+
.card[data-st=aborted]{box-shadow:inset 2px 0 0 var(--orange)}
|
|
287
|
+
.nollm{color:var(--placeholder);font-style:italic}
|
|
288
|
+
#empty{color:var(--placeholder);text-align:center;padding:24px 0;font-size:12px}
|
|
289
|
+
#stale{display:none;background:var(--red);color:#fff;font-size:12px;text-align:center;padding:3px;flex:none}
|
|
290
|
+
</style>
|
|
291
|
+
</head>
|
|
292
|
+
<body>
|
|
293
|
+
<div id="wrap">
|
|
294
|
+
<div id="stale">服务已换新版本,正在刷新…</div>
|
|
295
|
+
<header>
|
|
296
|
+
<span class="dot" id="dot" title="未连接"></span>
|
|
297
|
+
<span class="title">Agent 实况</span>
|
|
298
|
+
<span class="sp"></span>
|
|
299
|
+
<button id="rescan" class="hd-act" title="立刻重扫一遍日志目录">重扫</button>
|
|
300
|
+
<a href="/" target="_blank" class="hd-act" title="在浏览器里打开完整面板">面板 ↗</a>
|
|
301
|
+
<button class="winbtn" id="wmin" title="收成小球">○</button>
|
|
302
|
+
<button class="winbtn" id="wclose" title="关闭">✕</button>
|
|
303
|
+
</header>
|
|
304
|
+
<!-- 「今天」拆两行:大数+条数 / 入出缓(空 label 是 28px 占位,明细对齐大数)。
|
|
305
|
+
第三行 qoder 积分是条件行:累计有积分才显示 —— 有积分三行、没积分两行;显隐归 paintStats。 -->
|
|
306
|
+
<div id="stats">
|
|
307
|
+
<div class="stat-row">
|
|
308
|
+
<span class="stat-label">今天</span>
|
|
309
|
+
<span class="stat-big" id="today-total"><span id="today-num">—</span> tokens</span>
|
|
310
|
+
<span class="stat-sub" id="today-sub"></span>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="stat-row">
|
|
313
|
+
<span class="stat-label"></span>
|
|
314
|
+
<span class="stat-sub" id="today-detail"></span>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="stat-row" id="row-credits">
|
|
317
|
+
<span class="stat-label">积分</span>
|
|
318
|
+
<span class="stat-big" id="credit-val">—</span>
|
|
319
|
+
<span class="stat-sub" id="credit-sub"></span>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div id="feed"><div id="empty">等待数据…</div></div>
|
|
323
|
+
<div class="grip" id="grip" title="拖动调整大小;Ctrl+滚轮缩放内容"></div>
|
|
324
|
+
<div id="ball">
|
|
325
|
+
<div class="ball-num"><b id="ball-total"><span id="ball-num">—</span></b><span id="ball-sub">—</span></div>
|
|
326
|
+
<button class="ball-btn bopen" id="bopen" title="打开完整面板">↗</button>
|
|
327
|
+
<button class="ball-btn bclose" id="bclose" title="关闭">✕</button>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<script>
|
|
332
|
+
'use strict';
|
|
333
|
+
const PAGE_BUILD = document.querySelector('meta[name=agent-log-build]').content;
|
|
334
|
+
// 悬浮模式:Electron 壳带 ?float=1 进来,切深色玻璃皮;窗控按钮走 preload 暴露的 floatApi
|
|
335
|
+
const FLOAT = new URLSearchParams(location.search).has('float');
|
|
336
|
+
if (FLOAT) document.documentElement.classList.add('float');
|
|
337
|
+
const $ = id => document.getElementById(id);
|
|
338
|
+
const feed = $('feed');
|
|
339
|
+
|
|
340
|
+
// ---- 数字格式化:亿/万 分档,四位以内原样(桌面小卡片,tabular-nums 防抖) ----
|
|
341
|
+
function fmt(n) {
|
|
342
|
+
n = n || 0;
|
|
343
|
+
if (n >= 1e8) return (n / 1e8).toFixed(2) + '亿';
|
|
344
|
+
if (n >= 1e4) return (n / 1e4).toFixed(1) + '万';
|
|
345
|
+
return String(n);
|
|
346
|
+
}
|
|
347
|
+
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
348
|
+
// 千分位精确值:fmt() 的万/亿分档好读但抹尾数 —— 「实际 token 数」用它(统计区 sub 行 + 各处悬停 title)
|
|
349
|
+
const fmtExact = n => Math.round(n || 0).toLocaleString('en-US');
|
|
350
|
+
const two = n => String(n).padStart(2, '0');
|
|
351
|
+
function fmtTime(t) {
|
|
352
|
+
const d = new Date(t), now = new Date();
|
|
353
|
+
const hm = two(d.getHours()) + ':' + two(d.getMinutes()) + ':' + two(d.getSeconds());
|
|
354
|
+
return (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate())
|
|
355
|
+
? hm : (d.getMonth() + 1) + '/' + d.getDate() + ' ' + hm;
|
|
356
|
+
}
|
|
357
|
+
const fmtDur = ms => !ms ? '' : ms >= 60000 ? (ms / 60000).toFixed(1) + 'm' : (ms / 1000).toFixed(1) + 's';
|
|
358
|
+
|
|
359
|
+
// ---- 数据:byId 是唯一事实源;CAP 限制渲染条数(map 本身也不无限长,超出裁掉最老的) ----
|
|
360
|
+
// CAP 跟窗口高度走(2026-09-23):写死 5 的时候把窗口拉高,底下三分之一是空白。
|
|
361
|
+
// 一张卡 float 模式下约 100px(2026-09-24 对齐 demo 三段式布局后实测,含 10px 卡距;旧紧凑布局是 64px);
|
|
362
|
+
// 下限 3(默认 404 高的档刚好 ~3 张,同 demo max-h-320 露 3.2 张的观感),上限 20。球模式 feed 是 display:none
|
|
363
|
+
// (clientHeight=0 → 落回下限 3),切回卡片时 resize 会重算。
|
|
364
|
+
let CAP = 3;
|
|
365
|
+
function syncCap() {
|
|
366
|
+
const c = Math.max(3, Math.min(20, Math.floor((feed.clientHeight - 8) / 100)));
|
|
367
|
+
if (c === CAP) return;
|
|
368
|
+
CAP = c;
|
|
369
|
+
fetchFeed(); // 变多要补拉(手里的条数可能不够);变少 render 自己裁
|
|
370
|
+
}
|
|
371
|
+
window.addEventListener('resize', syncCap);
|
|
372
|
+
const byId = new Map();
|
|
373
|
+
|
|
374
|
+
// ---- 卡片 DOM 按 id 复用(增量渲染)----
|
|
375
|
+
// 之前一整段 innerHTML 重建:每批 SSE 更新都把 40 张卡连 DOM 一起换掉,.card 的 pop 入场动画
|
|
376
|
+
// 跟着全部重播 —— 列表整片闪烁。现在只有新条目才创建元素(才播入场动画),已有条目原地改字段;
|
|
377
|
+
// 排序用 insertBefore 挪节点,且「已在位」的节点不动(挪动会重启动画,能省则省)。
|
|
378
|
+
const els = new Map(); // id -> .card 元素
|
|
379
|
+
|
|
380
|
+
const stOf = e => e.status === 'ok' ? 'ok' : e.status === 'error' ? 'error' : e.status === 'aborted' ? 'aborted' : 'other';
|
|
381
|
+
function cardHTML(e) {
|
|
382
|
+
const st = stOf(e);
|
|
383
|
+
const model = (e.models && e.models[0]) || '';
|
|
384
|
+
const pv = e.preview ? esc(e.preview) : '<span class="nollm">(无对话内容)</span>';
|
|
385
|
+
// qoder 的 token 恒 0、真实用量是 credits(见 parsers/claude.mjs)——无 token 数据就不摆一排 0,只留积分
|
|
386
|
+
const hasTok = !!(e.tin || e.tout || e.tcache);
|
|
387
|
+
return '<div class="r1"><span class="sdot ' + st + '"></span>'
|
|
388
|
+
+ '<span class="time">' + fmtTime(e.time) + '</span>'
|
|
389
|
+
+ '<span class="agent">' + esc(e.agent) + '</span>'
|
|
390
|
+
+ '<span class="model">' + esc(model) + '</span></div>'
|
|
391
|
+
+ '<div class="pv">' + pv + '</div>'
|
|
392
|
+
+ '<div class="r3">'
|
|
393
|
+
+ (hasTok ? '<span class="tin" title="输入 ' + fmtExact(e.tin) + '">输入 ' + fmt(e.tin) + '</span>'
|
|
394
|
+
+ '<span class="tout" title="输出 ' + fmtExact(e.tout) + '">输出 ' + fmt(e.tout) + '</span>'
|
|
395
|
+
+ (e.tcache ? '<span class="tcache" title="缓存 ' + fmtExact(e.tcache) + '">缓存 ' + fmt(e.tcache) + '</span>' : '') : '')
|
|
396
|
+
+ (e.credits ? '<span>积分 ' + e.credits.toFixed(2) + '</span>' : '')
|
|
397
|
+
+ '<span class="dur">' + fmtDur(e.dur) + '</span>'
|
|
398
|
+
+ '</div>';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function render() {
|
|
402
|
+
const list = [...byId.values()].sort((a, b) => (b.time || 0) - (a.time || 0));
|
|
403
|
+
for (const e of list.slice(CAP)) byId.delete(e.id);
|
|
404
|
+
const show = list.slice(0, CAP);
|
|
405
|
+
// 先清掉已经不在数据里的元素(被 CAP 挤出 / 服务端 remove)
|
|
406
|
+
for (const [id, el] of els) if (!byId.has(id)) { el.remove(); els.delete(id); }
|
|
407
|
+
$('empty').style.display = show.length ? 'none' : '';
|
|
408
|
+
if (!show.length) { $('empty').textContent = '暂无日志'; return; }
|
|
409
|
+
// 从老到新逐个「插到 ref 前面」:新条目落到最前;已在正确位置的节点一次都不挪
|
|
410
|
+
let ref = null;
|
|
411
|
+
for (let i = show.length - 1; i >= 0; i--) {
|
|
412
|
+
const e = show[i];
|
|
413
|
+
let el = els.get(e.id);
|
|
414
|
+
if (!el) {
|
|
415
|
+
el = document.createElement('div');
|
|
416
|
+
el.className = 'card';
|
|
417
|
+
els.set(e.id, el);
|
|
418
|
+
}
|
|
419
|
+
const f = cardHTML(e);
|
|
420
|
+
if (el._f !== f) { el.innerHTML = f; el._f = f; } // 字段会补全(如进行中→完成),变了才重写
|
|
421
|
+
const st = stOf(e);
|
|
422
|
+
if (el.dataset.st !== st) el.dataset.st = st; // 状态也会变(进行中→失败),色条跟着换
|
|
423
|
+
// 守卫必须带 parentNode:游离新元素的 nextSibling 是 null,首轮 ref 也是 null,
|
|
424
|
+
// 只比 nextSibling 会把它误判成「已在位」跳过插入,下一轮还拿游离节点当参照直接抛异常
|
|
425
|
+
if (el.parentNode !== feed || el.nextSibling !== ref) feed.insertBefore(el, ref);
|
|
426
|
+
ref = el;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---- 汇总:今天 + 累计(/api/models 的 totals,口径与主面板「按模型」页完全一致) ----
|
|
431
|
+
function paintStats(today, all) {
|
|
432
|
+
// 今天拆两行(大数+条数 / 入出缓);积分行是条件行:累计有积分才出现,没积分整块两行(见 #stats 结构注释)。
|
|
433
|
+
// 累计 token 仍只进完整面板 —— all 在这里只为 qoder 积分服务。
|
|
434
|
+
if (today) {
|
|
435
|
+
tweenNum($('today-num'), today.total);
|
|
436
|
+
// 条数行只给条数 —— 精确 token 值太长,挤在条数后面视觉噪音,悬停 title 里还留着
|
|
437
|
+
$('today-sub').innerHTML = '<b>' + today.count + '</b> 条';
|
|
438
|
+
$('today-total').title = '今日实际 ' + fmtExact(today.total) + ' tokens';
|
|
439
|
+
$('today-detail').innerHTML =
|
|
440
|
+
'<span class="tin" title="输入 ' + fmtExact(today.tin) + '">输入 ' + fmt(today.tin) + '</span>'
|
|
441
|
+
+ ' · <span class="tout" title="输出 ' + fmtExact(today.tout) + '">输出 ' + fmt(today.tout) + '</span>'
|
|
442
|
+
+ ' · <span class="tcache" title="缓存 ' + fmtExact(today.tcache) + '">缓存 ' + fmt(today.tcache) + '</span>';
|
|
443
|
+
}
|
|
444
|
+
if (all) {
|
|
445
|
+
const cr = (all.credits || 0);
|
|
446
|
+
$('row-credits').style.display = cr ? 'flex' : 'none';
|
|
447
|
+
if (cr) {
|
|
448
|
+
$('credit-val').textContent = ((today && today.credits) || 0).toFixed(2);
|
|
449
|
+
$('credit-sub').innerHTML = '累计 ' + cr.toFixed(2);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// 「+xxx」增量角标:锚元素(#today-total / #ball-total)里浮一个增量、上飘淡出后自删;
|
|
454
|
+
// 每次先清旧的 —— 连续更新时只留最新一个。数字本体走 today-num / ball-num 子节点,
|
|
455
|
+
// textContent 刷新不会把角标抹掉。
|
|
456
|
+
function popDelta(anchor, delta) {
|
|
457
|
+
if (!(delta > 0)) return;
|
|
458
|
+
for (const p of anchor.querySelectorAll('.token-pop')) p.remove();
|
|
459
|
+
const s = document.createElement('span');
|
|
460
|
+
s.className = 'token-pop';
|
|
461
|
+
s.textContent = '+' + fmt(delta);
|
|
462
|
+
anchor.appendChild(s);
|
|
463
|
+
s.addEventListener('animationend', () => s.remove());
|
|
464
|
+
}
|
|
465
|
+
let lastTotal = null; // 上一次「今天」total;null = 首帧不报增量(跨天清零走 delta>0 的闸)
|
|
466
|
+
// 数字滚动:值变化时 480ms 缓动从旧值滚到新值(首帧从 0 滚起,算开屏入场)。
|
|
467
|
+
// 只重写元素自己的 textContent —— 增量角标挂在父节点(today-total / ball-total)上,不会被滚动帧抹掉。
|
|
468
|
+
// el._v 是上一次的目标值:新一轮 fetch 到了旧动画没跑完,从旧目标接着滚,不倒退。
|
|
469
|
+
function tweenNum(el, to) {
|
|
470
|
+
to = Math.round(to || 0);
|
|
471
|
+
const from = el._v || 0;
|
|
472
|
+
el._v = to;
|
|
473
|
+
cancelAnimationFrame(el._raf);
|
|
474
|
+
if (from === to) { el.textContent = fmt(to); return; }
|
|
475
|
+
const t0 = performance.now();
|
|
476
|
+
const tick = t => {
|
|
477
|
+
const k = Math.min(1, (t - t0) / 480);
|
|
478
|
+
el.textContent = fmt(Math.round(from + (to - from) * (1 - Math.pow(1 - k, 3))));
|
|
479
|
+
if (k < 1 && el._v === to) el._raf = requestAnimationFrame(tick);
|
|
480
|
+
};
|
|
481
|
+
el._raf = requestAnimationFrame(tick);
|
|
482
|
+
}
|
|
483
|
+
async function fetchStats() {
|
|
484
|
+
try {
|
|
485
|
+
const [t, a] = await Promise.all([
|
|
486
|
+
fetch('/api/models?range=today').then(r => r.json()),
|
|
487
|
+
fetch('/api/models').then(r => r.json()),
|
|
488
|
+
]);
|
|
489
|
+
paintStats(t.ok && t.totals, a.ok && a.totals);
|
|
490
|
+
if (t.ok && t.totals) {
|
|
491
|
+
tweenNum($('ball-num'), t.totals.total);
|
|
492
|
+
$('ball-total').title = '今日实际 ' + fmtExact(t.totals.total) + ' tokens';
|
|
493
|
+
$('ball-sub').textContent = t.totals.count + ' 条';
|
|
494
|
+
// 今天 token 涨了 → 大数和球各浮一个「+xxx」;跨天清零(负数)不打扰
|
|
495
|
+
if (lastTotal != null && t.totals.total > lastTotal) {
|
|
496
|
+
popDelta($('today-total'), t.totals.total - lastTotal);
|
|
497
|
+
popDelta($('ball-total'), t.totals.total - lastTotal);
|
|
498
|
+
}
|
|
499
|
+
lastTotal = t.totals.total;
|
|
500
|
+
}
|
|
501
|
+
} catch { /* 服务短暂不可用时下轮事件会再拉 */ }
|
|
502
|
+
}
|
|
503
|
+
let statsTimer = 0;
|
|
504
|
+
function scheduleStats() { clearTimeout(statsTimer); statsTimer = setTimeout(fetchStats, 600); } // 一批更新合并成一次拉取
|
|
505
|
+
|
|
506
|
+
async function fetchFeed() {
|
|
507
|
+
try {
|
|
508
|
+
const j = await fetch('/api/snapshot?limit=' + CAP).then(r => r.json());
|
|
509
|
+
byId.clear();
|
|
510
|
+
for (const e of j.entries || []) byId.set(e.id, e);
|
|
511
|
+
render();
|
|
512
|
+
} catch { /* 同上 */ }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ---- SSE:新条目实时进来。断线 EventSource 自己重连,灯色跟着 readyState 走 ----
|
|
516
|
+
const dot = $('dot');
|
|
517
|
+
function connect() {
|
|
518
|
+
const es = new EventSource('/api/events');
|
|
519
|
+
es.onopen = () => { dot.className = 'dot on'; dot.title = '实时连接中 · 点击收成小球'; $('ball').classList.remove('off'); };
|
|
520
|
+
es.onerror = () => { dot.className = 'dot off'; dot.title = '连接断开,重试中…'; $('ball').classList.add('off'); };
|
|
521
|
+
es.onmessage = ev => {
|
|
522
|
+
let m; try { m = JSON.parse(ev.data); } catch { return; }
|
|
523
|
+
if (m.type === 'hello') {
|
|
524
|
+
// 服务在背后被重启换版:这张常驻卡片自己刷新拿新页面(主页的红条方案在小卡片上没有地方放)
|
|
525
|
+
if (m.build && m.build !== PAGE_BUILD) { $('stale').style.display = ''; location.reload(); return; }
|
|
526
|
+
fetchFeed(); fetchStats();
|
|
527
|
+
} else if (m.type === 'update') {
|
|
528
|
+
for (const e of m.entries || []) byId.set(e.id, e);
|
|
529
|
+
render(); scheduleStats();
|
|
530
|
+
} else if (m.type === 'remove') {
|
|
531
|
+
for (const id of m.ids || []) byId.delete(id);
|
|
532
|
+
render(); scheduleStats();
|
|
533
|
+
} else if (m.type === 'resync') {
|
|
534
|
+
fetchFeed(); fetchStats();
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
$('rescan').onclick = () => { fetch('/api/snapshot?scan=1&limit=1').catch(() => {}); };
|
|
540
|
+
// 悬浮模式的窗控:preload 注入了 floatApi;浏览器里这两个按钮根本不渲染(.winbtn 只在 float 下显示)。
|
|
541
|
+
// 注意没有「最小化」:skipTaskbar 的窗口最小化后没有入口叫回来 —— 「缩小」就是收球(见下面 FLOAT 块)。
|
|
542
|
+
$('wclose').onclick = () => { if (window.floatApi) window.floatApi.hide(); };
|
|
543
|
+
|
|
544
|
+
// 悬浮模式的两个缩放通道:
|
|
545
|
+
// 1) 右下角拖拽柄改窗口大小 —— 无边框透明窗没有系统拉边框,自己从拖动起点算目标尺寸报给壳(壳负责夹紧)
|
|
546
|
+
// 2) Ctrl+滚轮缩放内容(Electron 无边框窗不管这套默认手势,自己拦)
|
|
547
|
+
// 卡片 ⇄ 小球:点顶栏小球收成一颗球(只显示今日 token),点球回来。
|
|
548
|
+
// 窗口尺寸切换在壳里(floatApi.setMode,锚定右下角);页面换 class + 播形变动画
|
|
549
|
+
// (morph-out / morph-in,见 CSS「从哪来回哪去」那段),窗口跳变藏进形变缝里。
|
|
550
|
+
// 上次以球模式关的,壳会用 &mode=ball 打开,这里照 URL 落 initial class。
|
|
551
|
+
// mode/applyMode 放在 floatApi 守卫**外面**:浏览器直接开 /widget?float=1&mode=ball 也能预览球
|
|
552
|
+
// (没有壳只是切不了窗口尺寸,球的样式照样渲染),调球的样式不用起 Electron。
|
|
553
|
+
let mode = new URLSearchParams(location.search).get('mode') === 'ball' ? 'ball' : 'card';
|
|
554
|
+
const applyMode = () => document.documentElement.classList.toggle('ball', mode === 'ball');
|
|
555
|
+
if (FLOAT) applyMode();
|
|
556
|
+
|
|
557
|
+
if (FLOAT && window.floatApi) {
|
|
558
|
+
const grip = $('grip');
|
|
559
|
+
grip.addEventListener('pointerdown', e => {
|
|
560
|
+
e.preventDefault();
|
|
561
|
+
const sx = e.screenX, sy = e.screenY, sw = window.innerWidth, sh = window.innerHeight;
|
|
562
|
+
// ⚠️ 这里原来有三个洞,叠起来就是「没人碰它、卡片却自己一路缩到 230×160 下限,顶栏挤成一团」
|
|
563
|
+
// (2026-09-24 实测,窗口量到 230x263;用户确认收小前没点过 ○):
|
|
564
|
+
// ① up() 只摘 window 上的监听,**grip 上那份 pointermove 永远留着** —— 之后鼠标只是划过这个
|
|
565
|
+
// 13px 的柄,就会拿「最早那次按下」的基准重设一次窗口尺寸;
|
|
566
|
+
// ② move 不看 buttons,于是「划过」与「按住拖动」在它眼里是一回事;
|
|
567
|
+
// ③ 每按一次就多叠一份监听。
|
|
568
|
+
// 现在:一次按下 = 一对监听,抬手 / 取消 / 失焦都成对摘干净;且真鼠标移动必须按着左键才算拖动。
|
|
569
|
+
let live = true;
|
|
570
|
+
const move = ev => {
|
|
571
|
+
if (!live) return;
|
|
572
|
+
// isTrusted 那半是为了不挡 CDP 合成事件(自动验证时发的 PointerEvent 没有活动指针)
|
|
573
|
+
if (ev.isTrusted && ev.buttons !== undefined && !(ev.buttons & 1)) return;
|
|
574
|
+
window.floatApi.resize(sw + ev.screenX - sx, sh + ev.screenY - sy);
|
|
575
|
+
};
|
|
576
|
+
const up = () => {
|
|
577
|
+
if (!live) return;
|
|
578
|
+
live = false;
|
|
579
|
+
for (const t of [grip, window]) {
|
|
580
|
+
t.removeEventListener('pointermove', move);
|
|
581
|
+
t.removeEventListener('pointerup', up);
|
|
582
|
+
t.removeEventListener('pointercancel', up);
|
|
583
|
+
}
|
|
584
|
+
window.removeEventListener('blur', up);
|
|
585
|
+
};
|
|
586
|
+
// 合成事件(CDP 验证用)没有活动指针,setPointerCapture 会抛 —— 能捕获就捕获,不行靠 window 监听兜底
|
|
587
|
+
try { grip.setPointerCapture(e.pointerId); } catch { /* 有 window 兜底就够 */ }
|
|
588
|
+
for (const t of [grip, window]) {
|
|
589
|
+
t.addEventListener('pointermove', move);
|
|
590
|
+
t.addEventListener('pointerup', up);
|
|
591
|
+
t.addEventListener('pointercancel', up);
|
|
592
|
+
}
|
|
593
|
+
// 在窗口外抬手时 pointerup 可能根本不进来(透明无边框窗 + 系统级拖区更容易)—— 失焦也当抬手
|
|
594
|
+
window.addEventListener('blur', up);
|
|
595
|
+
});
|
|
596
|
+
window.addEventListener('wheel', e => {
|
|
597
|
+
if (!e.ctrlKey || mode === 'ball') return; // 球模式禁用内容缩放:窗口锁死 112px,放大只会把球裁掉
|
|
598
|
+
e.preventDefault();
|
|
599
|
+
window.floatApi.zoom(e.deltaY < 0 ? 0.1 : -0.1);
|
|
600
|
+
}, { passive: false });
|
|
601
|
+
|
|
602
|
+
// 收球:先播 #wrap 缩进球心的形变(此刻窗口还是卡片大小),到位同帧换 class + 叫壳缩窗。
|
|
603
|
+
// morphing 期间重复点 ○ / 状态灯 / 球都吞掉,不打断半截动画。
|
|
604
|
+
let morphing = false;
|
|
605
|
+
const MORPH_OUT_MS = 240, MORPH_IN_MS = 330;
|
|
606
|
+
const enterBall = () => {
|
|
607
|
+
if (morphing || mode === 'ball') return;
|
|
608
|
+
morphing = true;
|
|
609
|
+
document.documentElement.classList.add('morph-out');
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
document.documentElement.classList.remove('morph-out');
|
|
612
|
+
mode = 'ball'; applyMode();
|
|
613
|
+
window.floatApi.setMode('ball');
|
|
614
|
+
morphing = false;
|
|
615
|
+
}, MORPH_OUT_MS);
|
|
616
|
+
};
|
|
617
|
+
// 回卡片:窗口放大是瞬移,顺序得倒过来 —— 先叫壳放大,等 resize 落定(窗口已是卡片尺寸)
|
|
618
|
+
// 页面才换 class,#wrap 从球心长回整张卡:形变发生在新窗口里,不是在 112px 球窗里硬撑。
|
|
619
|
+
// 但直接放大会把「旧球窗的最后一帧」钉在新窗口左上角重放一帧(球在左上闪一下)——
|
|
620
|
+
// 所以先加 morph-pre 把球连同辉光藏掉,等两帧确认合成器送出的已是全透明,才叫壳放大。
|
|
621
|
+
// 代价是几十 ms 的空窗(球先没、卡片再从原地长出来),比左上闪球干净。
|
|
622
|
+
// resize 判定用「宽度涨过球窗的 1.5 倍」而不是定值:Ctrl+滚轮缩放过的内容 CSS px 会跟窗口物理 px 错开。
|
|
623
|
+
// resize 等不到(壳异常)有 600ms 兜底直接换皮,不会永远卡在球里。
|
|
624
|
+
const expandCard = () => {
|
|
625
|
+
if (morphing || mode !== 'ball') return;
|
|
626
|
+
morphing = true;
|
|
627
|
+
const root = document.documentElement;
|
|
628
|
+
root.classList.add('morph-pre');
|
|
629
|
+
const ballW = window.innerWidth;
|
|
630
|
+
let done = false;
|
|
631
|
+
const finish = grew => {
|
|
632
|
+
if (done) return;
|
|
633
|
+
done = true;
|
|
634
|
+
window.removeEventListener('resize', onResize);
|
|
635
|
+
root.classList.remove('morph-pre'); // morph-in 的 from{opacity:0} 接住,不会闪回可见
|
|
636
|
+
mode = 'card'; applyMode();
|
|
637
|
+
if (grew) { // 兜底路径窗没动就不播形变:在 112px 球窗里播展开只会看到一闪
|
|
638
|
+
root.classList.add('morph-in');
|
|
639
|
+
setTimeout(() => { root.classList.remove('morph-in'); morphing = false; }, MORPH_IN_MS);
|
|
640
|
+
} else morphing = false;
|
|
641
|
+
};
|
|
642
|
+
const onResize = () => { if (window.innerWidth > ballW * 1.5) finish(true); };
|
|
643
|
+
window.addEventListener('resize', onResize);
|
|
644
|
+
requestAnimationFrame(() => requestAnimationFrame(() => window.floatApi.setMode('card')));
|
|
645
|
+
setTimeout(() => finish(false), 600);
|
|
646
|
+
};
|
|
647
|
+
dot.style.cursor = 'pointer';
|
|
648
|
+
dot.title = '收成小球';
|
|
649
|
+
dot.onclick = enterBall;
|
|
650
|
+
$('wmin').onclick = enterBall; // 「缩小」= 收球,不做真最小化(skipTaskbar 的窗口最小化后叫不回来)
|
|
651
|
+
$('bopen').onclick = e => { e.stopPropagation(); window.open('/', '_blank'); };
|
|
652
|
+
$('bclose').onclick = e => { e.stopPropagation(); window.floatApi.hide(); };
|
|
653
|
+
|
|
654
|
+
// 球的拖动 + 点击:整球不设系统拖区(那样 click 会被吞),自己按 4px 阈值分家 ——
|
|
655
|
+
// 按住移动超阈值 = 拖(IPC 报增量,壳挪窗口);原地松开 = 点击(回卡片模式)。
|
|
656
|
+
const ball = $('ball');
|
|
657
|
+
ball.addEventListener('pointerdown', e => {
|
|
658
|
+
if (e.target.closest('.ball-btn')) return; // 落在小按钮上不归球管
|
|
659
|
+
e.preventDefault();
|
|
660
|
+
const sx = e.screenX, sy = e.screenY;
|
|
661
|
+
let moved = false, live = true;
|
|
662
|
+
window.floatApi.dragBegin();
|
|
663
|
+
const move = ev => {
|
|
664
|
+
if (!live) return;
|
|
665
|
+
if (ev.isTrusted && ev.buttons !== undefined && !(ev.buttons & 1)) return; // 同拖拽柄:没按着就不算拖
|
|
666
|
+
const dx = ev.screenX - sx, dy = ev.screenY - sy;
|
|
667
|
+
if (!moved && Math.abs(dx) + Math.abs(dy) < 4) return;
|
|
668
|
+
moved = true;
|
|
669
|
+
window.floatApi.dragBy(dx, dy);
|
|
670
|
+
};
|
|
671
|
+
// 与拖拽柄同一类收口:pointerup 进不来(在窗口外抬手 / 系统吞了)时,move 会一直挂在 window 上,
|
|
672
|
+
// 之后每次鼠标移动都变成「拖球」—— 球会自己乱跑。抬手 / 取消 / 失焦都算结束,且只结束一次。
|
|
673
|
+
const up = () => {
|
|
674
|
+
if (!live) return;
|
|
675
|
+
live = false;
|
|
676
|
+
window.removeEventListener('pointermove', move);
|
|
677
|
+
window.removeEventListener('pointerup', up);
|
|
678
|
+
window.removeEventListener('pointercancel', up);
|
|
679
|
+
window.removeEventListener('blur', up);
|
|
680
|
+
if (!moved) expandCard(); // 原地松开 = 点击 = 长回卡片(带形变动画)
|
|
681
|
+
};
|
|
682
|
+
window.addEventListener('pointermove', move);
|
|
683
|
+
window.addEventListener('pointerup', up);
|
|
684
|
+
window.addEventListener('pointercancel', up);
|
|
685
|
+
window.addEventListener('blur', up);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// 列表区拖拽整窗(2026-09-28 用户点名:原本只有顶栏能拖,列表区大得多、更好抓)。
|
|
689
|
+
// 不用 app-region:drag —— 那是系统级命中测试,卡片的悬停效果会整片死掉;
|
|
690
|
+
// 走和球同一路:pointer 增量报给壳,4px 阈值分开「按住拖」与「原地松开」(松开不做任何事,
|
|
691
|
+
// 列表本来就没有点击行为)。副作用:列表文字不可选中(user-select:none),要复制去面板页。
|
|
692
|
+
feed.addEventListener('pointerdown', e => {
|
|
693
|
+
if (mode === 'ball' || (e.button !== undefined && e.button !== 0)) return; // 球模式 feed 本就隐藏;只认左键
|
|
694
|
+
const sx = e.screenX, sy = e.screenY;
|
|
695
|
+
let moved = false, live = true;
|
|
696
|
+
window.floatApi.dragBegin();
|
|
697
|
+
const move = ev => {
|
|
698
|
+
if (!live) return;
|
|
699
|
+
if (ev.isTrusted && ev.buttons !== undefined && !(ev.buttons & 1)) return; // 没按着就不算拖
|
|
700
|
+
const dx = ev.screenX - sx, dy = ev.screenY - sy;
|
|
701
|
+
if (!moved && Math.abs(dx) + Math.abs(dy) < 4) return;
|
|
702
|
+
moved = true;
|
|
703
|
+
window.floatApi.dragBy(dx, dy);
|
|
704
|
+
};
|
|
705
|
+
const up = () => {
|
|
706
|
+
if (!live) return;
|
|
707
|
+
live = false;
|
|
708
|
+
window.removeEventListener('pointermove', move);
|
|
709
|
+
window.removeEventListener('pointerup', up);
|
|
710
|
+
window.removeEventListener('pointercancel', up);
|
|
711
|
+
window.removeEventListener('blur', up);
|
|
712
|
+
};
|
|
713
|
+
window.addEventListener('pointermove', move);
|
|
714
|
+
window.addEventListener('pointerup', up);
|
|
715
|
+
window.addEventListener('pointercancel', up);
|
|
716
|
+
window.addEventListener('blur', up);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
syncCap(); // 首拉前按窗口高度定 CAP(feed 此时已可量,clientHeight 会强制布局)
|
|
721
|
+
fetchFeed(); fetchStats(); connect();
|
|
722
|
+
</script>
|
|
723
|
+
</body>
|
|
724
|
+
</html>
|