dsh-annual-activity 1.1.0

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/lib/client.js ADDED
@@ -0,0 +1,836 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-annual-activity",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ const React = require("react");
8
+
9
+ const name = "dsh-annual-activity";
10
+ // 依赖必须显式声明:boot 阶段各插件并发激活,inject: [] 的插件可能在 slots
11
+ // 服务就绪前 apply,导致 ctx.get("slots") 为空而静默失效(无报错、无 UI)。
12
+ const inject = ["slots"];
13
+
14
+ // ==================== 常量 / 工具 ====================
15
+
16
+ // 活跃等级:与 Host 半区同源(Host 通过 /activity/pull 的 levels 字段下发,
17
+ // 且色块颜色随数据里自带的 level 走;这里的兜底表仅用于 Host 不可用时)。
18
+ const FALLBACK_LEVELS = [
19
+ { level: 0, label: '未活跃', min: 0, max: 0 },
20
+ { level: 1, label: '轻', min: 1, max: 1 },
21
+ { level: 2, label: '中', min: 2, max: 5 },
22
+ { level: 3, label: '高', min: 6, max: 15 },
23
+ { level: 4, label: '极高', min: 16, max: null },
24
+ ];
25
+ const MONTH_NAMES = ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'];
26
+ const WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
27
+ const LS_YEAR = 'dsh-annual-activity:year';
28
+ const LS_GEAR = 'dsh-annual-activity:setup-open';
29
+
30
+ const pad2 = (n) => (n < 10 ? '0' + n : '' + n);
31
+ const dayKeyOf = (y, m, d) => y + '-' + pad2(m + 1) + '-' + pad2(d);
32
+ const isLeap = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
33
+ const daysInYear = (y) => (isLeap(y) ? 366 : 365);
34
+
35
+ function fmtInt(n) {
36
+ return String(Math.round(Number(n) || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
37
+ }
38
+ function fmtCompact(n) {
39
+ n = Number(n) || 0;
40
+ if (n < 1000) return String(n);
41
+ if (n < 1e6) return (n / 1e3).toFixed(n < 1e4 ? 1 : 0) + 'K';
42
+ if (n < 1e9) return (n / 1e6).toFixed(2) + 'M';
43
+ return (n / 1e9).toFixed(2) + 'B';
44
+ }
45
+ function fmtPercent(rate, digits) {
46
+ const v = (Number(rate) || 0) * 100;
47
+ const d = typeof digits === 'number' ? digits : v > 0 && v < 1 ? 1 : 0;
48
+ return (v > 0 && v < 0.1 ? '<0.1' : v.toFixed(d)) + '%';
49
+ }
50
+ function fmtClock(ts) {
51
+ if (!ts) return '—';
52
+ const d = new Date(ts);
53
+ return pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes());
54
+ }
55
+ function weekdayOf(day) {
56
+ const [y, m, d] = String(day).split('-').map(Number);
57
+ return (new Date(y, m - 1, d).getDay() + 6) % 7; // 0 = 周一
58
+ }
59
+ function todayKey() {
60
+ const d = new Date();
61
+ return dayKeyOf(d.getFullYear(), d.getMonth(), d.getDate());
62
+ }
63
+ /** 相对今天偏移 n 天的本地日期键(n 为负表示过去)。 */
64
+ function dayKeyOffset(n) {
65
+ const d = new Date();
66
+ d.setDate(d.getDate() + n);
67
+ return dayKeyOf(d.getFullYear(), d.getMonth(), d.getDate());
68
+ }
69
+
70
+ /**
71
+ * 入口按钮里的小热力图字形:最近 14 天,7 列 × 2 行(每列一周、周一在顶)。
72
+ * 让按钮本身就是「活跃记录」的缩影,而不是一个与内容无关的图标。
73
+ */
74
+ function glyphCells(byDay) {
75
+ const out = [];
76
+ const todayDow = weekdayOf(dayKeyOffset(0)); // 0 = 周一
77
+ for (let row = 0; row < 2; row++) {
78
+ for (let col = 0; col < 7; col++) {
79
+ const offset = -13 + row * 7 + col + (6 - todayDow);
80
+ const day = offset <= 0 ? dayKeyOffset(offset) : null;
81
+ out.push({ day, level: day ? levelOfDay(byDay[day]) : -1 });
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ /**
88
+ * 把一年的数据排成 GitHub 风格的热力图网格:列 = 周(周一在顶),最多 53 列。
89
+ * 首列前用 null 占位,使 1 月 1 日落在它真实的星期行上。
90
+ */
91
+ function buildWeeks(year) {
92
+ const total = daysInYear(year);
93
+ const lead = weekdayOf(dayKeyOf(year, 0, 1));
94
+ const cells = [];
95
+ for (let i = 0; i < lead; i++) cells.push(null);
96
+ for (let i = 0; i < total; i++) {
97
+ const d = new Date(year, 0, 1 + i);
98
+ cells.push(dayKeyOf(year, d.getMonth(), d.getDate()));
99
+ }
100
+ while (cells.length % 7 !== 0) cells.push(null);
101
+ const weeks = [];
102
+ for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
103
+ return weeks;
104
+ }
105
+
106
+ /** 月份轴:某列含有该月 1 日时,在这一列顶上标出月份(与设计图一致)。 */
107
+ function monthLabels(weeks) {
108
+ const labels = new Array(weeks.length).fill(null);
109
+ const seen = new Set();
110
+ for (let c = 0; c < weeks.length; c++) {
111
+ for (const day of weeks[c]) {
112
+ if (!day) continue;
113
+ const m = Number(day.slice(5, 7));
114
+ if (Number(day.slice(8, 10)) === 1 && !seen.has(m)) {
115
+ seen.add(m);
116
+ labels[c] = MONTH_NAMES[m - 1];
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ return labels;
122
+ }
123
+
124
+ function levelOfDay(rec) {
125
+ if (!rec) return 0;
126
+ return typeof rec.level === 'number' ? rec.level : 0;
127
+ }
128
+
129
+ /**
130
+ * 把一次 pull 负载整理成「日 → 记录」查表。
131
+ * 负载形状:`days` 是**当前选中年**的完整明细(有界),`byDay` 是全部年份的
132
+ * 汇总(host 序列化时会剔除,只有本地直连时才可能带);两者合并后,无论
133
+ * 选中哪一年,「今天」的色块和计数都能查到。
134
+ */
135
+ function indexDays(data) {
136
+ const map = {};
137
+ if (!data) return map;
138
+ if (data.byDay) for (const day of Object.keys(data.byDay)) map[day] = data.byDay[day];
139
+ for (const rec of data.days || []) map[rec.day] = rec;
140
+ return map;
141
+ }
142
+
143
+ // ==================== 迷你 store:轮询 host 的只读接口 ====================
144
+
145
+ function createStore() {
146
+ // 绝对 URL:不依赖文档 base(DSH Web 面板可能与页面不在同一路径前缀下),
147
+ // 也让 host 路由在任何挂载点都命中(服务只监听回环地址)。
148
+ const api = (p) => window.location.origin + p;
149
+ let state = {
150
+ data: null, // 最近一次成功的 /activity/pull 负载
151
+ loading: true,
152
+ error: '',
153
+ lastOkAt: 0,
154
+ panelOpen: false,
155
+ preferredYear: null, // 用户手选的年份(null = 跟随 host 的当前年)
156
+ };
157
+ const listeners = new Set();
158
+ const emit = () => { for (const fn of Array.from(listeners)) { try { fn() } catch (e) { /* ignore */ } } };
159
+ let timer = null;
160
+ let inFlight = null;
161
+
162
+ function pull() {
163
+ if (inFlight) return inFlight;
164
+ inFlight = (async () => {
165
+ try {
166
+ const year = state.preferredYear;
167
+ // 绝对 URL:不依赖文档 base,也让 host 路由在任何挂载点都命中
168
+ const res = await fetch(api('/activity/pull' + (year ? '?year=' + year : '')), { headers: { accept: 'application/json' } });
169
+ if (!res.ok) throw new Error('HTTP ' + res.status);
170
+ const data = await res.json();
171
+ state = { ...state, data, loading: false, error: '', lastOkAt: Date.now() };
172
+ } catch (err) {
173
+ state = { ...state, loading: false, error: String((err && err.message) || err) };
174
+ } finally {
175
+ inFlight = null;
176
+ emit();
177
+ }
178
+ return state;
179
+ })();
180
+ return inFlight;
181
+ }
182
+
183
+ async function refresh() {
184
+ try { await fetch(api('/activity/refresh'), { method: 'POST' }); } catch (err) { /* host 未就绪时静默 */ }
185
+ return pull();
186
+ }
187
+
188
+ function schedule() {
189
+ if (timer) return;
190
+ // 面板打开时 20s 一次(今天的新活动很快可见),关闭时 3 分钟一次保活
191
+ const tick = () => { void pull(); timer = setTimeout(tick, state.panelOpen ? 20000 : 180000); };
192
+ timer = setTimeout(tick, state.panelOpen ? 1000 : 8000);
193
+ }
194
+
195
+ return {
196
+ getState: () => state,
197
+ subscribe(fn) { listeners.add(fn); schedule(); return () => listeners.delete(fn); },
198
+ /** 只在值真正变化时替换快照:否则 emit → setState → 重渲染会自激成环 */
199
+ setPanelOpen(open) {
200
+ const next = !!open;
201
+ if (next === state.panelOpen) return Promise.resolve(state);
202
+ state = { ...state, panelOpen: next };
203
+ emit();
204
+ return next ? pull() : Promise.resolve(state);
205
+ },
206
+ setYear(year) {
207
+ const next = year || null;
208
+ if (next === state.preferredYear) return inFlight || Promise.resolve(state);
209
+ state = { ...state, preferredYear: next, loading: true };
210
+ emit();
211
+ return pull();
212
+ },
213
+ refresh,
214
+ pull,
215
+ };
216
+ }
217
+
218
+ const store = createStore();
219
+
220
+ /**
221
+ * 版本 / 升级 store:与活动数据分开,只在需要时拉取(打开面板、点检测更新),
222
+ * 避免每次 20s 轮询都多打一次无关请求;升级进行中则自动加快轮询以便看到结果。
223
+ */
224
+ function createMetaStore() {
225
+ const api = (p) => window.location.origin + p;
226
+ let state = { meta: null, checking: false, upgrading: false, error: '', notice: '' };
227
+ const listeners = new Set();
228
+ const emit = () => { for (const fn of Array.from(listeners)) { try { fn() } catch (e) { /* ignore */ } } };
229
+ let inFlight = null;
230
+ let timer = null;
231
+
232
+ function load() {
233
+ if (inFlight) return inFlight;
234
+ inFlight = (async () => {
235
+ try {
236
+ const res = await fetch(api('/activity/meta'), { headers: { accept: 'application/json' } });
237
+ if (!res.ok) throw new Error('HTTP ' + res.status);
238
+ const meta = await res.json();
239
+ state = { ...state, meta, error: '' };
240
+ } catch (err) {
241
+ state = { ...state, error: String((err && err.message) || err) };
242
+ } finally {
243
+ inFlight = null;
244
+ emit();
245
+ }
246
+ return state;
247
+ })();
248
+ return inFlight;
249
+ }
250
+
251
+ async function post(path, patch) {
252
+ state = { ...state, ...patch, notice: '' };
253
+ emit();
254
+ try {
255
+ const res = await fetch(api(path), { method: 'POST', headers: { accept: 'application/json' } });
256
+ if (!res.ok) throw new Error('HTTP ' + res.status);
257
+ const meta = await res.json();
258
+ state = { ...state, meta, error: '', notice: (meta && meta.upgrade && meta.upgrade.message) || '' };
259
+ } catch (err) {
260
+ state = { ...state, error: String((err && err.message) || err) };
261
+ } finally {
262
+ state = { ...state, checking: false, upgrading: false };
263
+ emit();
264
+ }
265
+ return state;
266
+ }
267
+
268
+ return {
269
+ getState: () => state,
270
+ subscribe(fn) { listeners.add(fn); schedule(); return () => listeners.delete(fn); },
271
+ load,
272
+ /** 手动检测更新:强制查一次 registry(忽略 host 的 6h 周期) */
273
+ check: () => { state = { ...state, checking: true }; return post('/activity/check-update', {}); },
274
+ /** 执行在线升级;本地 link/file 安装会被 host 拒绝并在 notice 里说明 */
275
+ upgrade: () => { state = { ...state, upgrading: true }; return post('/activity/upgrade', {}); },
276
+ dismissNotice: () => { state = { ...state, notice: '' }; emit(); },
277
+ };
278
+
279
+ function schedule() {
280
+ if (timer) return;
281
+ const tick = () => {
282
+ // 升级进行中 3s 一次(等结果),否则 5 分钟一次(保持 updateAvailable 新鲜)
283
+ const busy = state.upgrading || (state.meta && state.meta.upgrade && state.meta.upgrade.running);
284
+ void load();
285
+ timer = setTimeout(tick, busy ? 3000 : 300000);
286
+ };
287
+ timer = setTimeout(tick, state.meta ? 300000 : 5000);
288
+ }
289
+ }
290
+
291
+ const metaStore = createMetaStore();
292
+ function useMetaStore() {
293
+ const [snapshot, setSnapshot] = React.useState(metaStore.getState());
294
+ React.useEffect(() => metaStore.subscribe(() => setSnapshot(metaStore.getState())), []);
295
+ return snapshot;
296
+ }
297
+
298
+ /**
299
+ * 订阅 store:快照对象引用变化才 setState。
300
+ * 若这里用「计数器 +1」强制重渲染,emit 后 setState 会让宿主重渲染,宿主里
301
+ * `useEffect(..., [open])` 依赖没变不会重跑,但任何依赖数组缺失的 effect
302
+ * (或父级重渲染)会再次 emit,从而形成渲染循环——引用比较天然避免这一点。
303
+ */
304
+ function useStore() {
305
+ const [snapshot, setSnapshot] = React.useState(store.getState());
306
+ React.useEffect(() => store.subscribe(() => setSnapshot(store.getState())), []);
307
+ return snapshot;
308
+ }
309
+
310
+ // ==================== 小组件 ====================
311
+
312
+ function HeatCell(props) {
313
+ const { day, rec, isToday, size, onHover } = props;
314
+ const ref = React.useRef(null);
315
+
316
+ if (!day) {
317
+ return React.createElement('div', { className: 'daa-cell daa-cell-pad', style: { width: size, height: size } });
318
+ }
319
+ const level = levelOfDay(rec);
320
+ const cls = 'daa-cell daa-l' + level + (isToday ? ' daa-today' : '');
321
+ const onEnter = () => {
322
+ const el = ref.current;
323
+ if (!el || !onHover) return;
324
+ const r = el.getBoundingClientRect();
325
+ onHover({ x: r.left + r.width / 2, y: r.top, day, rec });
326
+ };
327
+ return React.createElement('div', {
328
+ ref,
329
+ className: cls,
330
+ style: { width: size, height: size },
331
+ onMouseEnter: onEnter,
332
+ onMouseLeave: () => { if (onHover) onHover(null); },
333
+ 'aria-label': day + (rec ? ' 活跃 ' + (rec.turns || 0) + ' 轮' : ' 未活跃'),
334
+ });
335
+ }
336
+
337
+ function CellTip({ tip }) {
338
+ const { day, rec, x, y } = tip;
339
+ const d = new Date(Number(day.slice(0, 4)), Number(day.slice(5, 7)) - 1, Number(day.slice(8, 10)));
340
+ const head = d.getFullYear() + '年' + (d.getMonth() + 1) + '月' + d.getDate() + '日 ' + WEEKDAY_NAMES[weekdayOf(day)];
341
+ const rows = rec
342
+ ? [
343
+ ['活跃等级', (FALLBACK_LEVELS[levelOfDay(rec)] || {}).label || '—'],
344
+ ['轮次', fmtInt(rec.turns)],
345
+ ['工具调用', fmtInt(rec.toolCalls)],
346
+ ['Token', fmtCompact(rec.tokens) + '(' + fmtCompact(rec.inputTokens) + ' 入 / ' + fmtCompact(rec.outputTokens) + ' 出)'],
347
+ ['涉及会话', fmtInt(rec.activeSessions)],
348
+ ]
349
+ : [['状态', '未活跃']];
350
+ // 贴近单元格上方,但不越出视口
351
+ const top = Math.max(8, Math.round(y) - 8);
352
+ const left = Math.min(Math.max(Math.round(x), 110), Math.max(110, (typeof window !== 'undefined' ? window.innerWidth : 1200) - 110));
353
+ const style = { left: left + 'px', top: top + 'px' };
354
+ return React.createElement('div', { className: 'daa-tip', style },
355
+ React.createElement('div', { className: 'daa-tip-head' }, head),
356
+ rows.map(([k, v]) => React.createElement('div', { className: 'daa-tip-row', key: k },
357
+ React.createElement('span', { className: 'daa-tip-k' }, k),
358
+ React.createElement('span', { className: 'daa-tip-v' }, v))));
359
+ }
360
+
361
+ function Legend({ levels }) {
362
+ const list = levels && levels.length ? levels : FALLBACK_LEVELS;
363
+ return React.createElement('div', { className: 'daa-legend' },
364
+ React.createElement('span', { className: 'daa-legend-cap' }, '未活跃'),
365
+ list.map((l) => React.createElement('span', {
366
+ key: l.level,
367
+ className: 'daa-legend-cell daa-l' + l.level,
368
+ title: l.level === 0 ? '未活跃' : l.label + '(' + (l.max == null ? l.min + '+ 轮' : l.min === l.max ? l.min + ' 轮' : l.min + '-' + l.max + ' 轮') + ')',
369
+ })),
370
+ React.createElement('span', { className: 'daa-legend-cap' }, '活跃'));
371
+ }
372
+
373
+ function YearSwitch({ years, year, onPick, currentYear, onClose }) {
374
+ const idx = years.indexOf(year);
375
+ const hasPrev = idx > 0;
376
+ const hasNext = idx >= 0 && idx < years.length - 1;
377
+ const [open, setOpen] = React.useState(false);
378
+ return React.createElement('div', { className: 'daa-year' },
379
+ React.createElement('button', {
380
+ type: 'button', className: 'daa-year-btn', disabled: !hasPrev,
381
+ title: hasPrev ? '上一年 ' + years[idx - 1] : '没有更早的记录',
382
+ onClick: () => hasPrev && onPick(years[idx - 1]),
383
+ }, '‹'),
384
+ React.createElement('button', {
385
+ type: 'button', className: 'daa-year-cur' + (year === currentYear ? ' daa-year-now' : ''),
386
+ title: years.length > 1 ? '切换年份' : '仅记录到这一年',
387
+ onClick: () => years.length > 1 && setOpen(!open),
388
+ },
389
+ React.createElement('span', null, year + ' 年'),
390
+ years.length > 1 ? React.createElement('span', { className: 'daa-year-caret' }, '▾') : null),
391
+ React.createElement('button', {
392
+ type: 'button', className: 'daa-year-btn', disabled: !hasNext,
393
+ title: hasNext ? '下一年 ' + years[idx + 1] : '已经是最新的一年',
394
+ onClick: () => hasNext && onPick(years[idx + 1]),
395
+ }, '›'),
396
+ open ? React.createElement('div', { className: 'daa-year-menu' },
397
+ years.map((y) => React.createElement('button', {
398
+ key: y, type: 'button',
399
+ className: 'daa-year-item' + (y === year ? ' on' : ''),
400
+ onClick: () => { setOpen(false); onPick(y); },
401
+ }, y + ' 年', y === currentYear ? React.createElement('span', { className: 'daa-year-badge' }, '今年') : null))) : null,
402
+ React.createElement('button', { type: 'button', className: 'daa-close', title: '关闭 (Esc)', onClick: onClose }, '✕'));
403
+ }
404
+
405
+ function Stat(props) {
406
+ const { value, unit, label, accent } = props;
407
+ return React.createElement('div', { className: 'daa-stat' },
408
+ React.createElement('span', { className: 'daa-stat-v' + (accent ? ' daa-accent' : '') }, value, unit ? React.createElement('span', null, ' ' + unit) : null),
409
+ React.createElement('span', { className: 'daa-stat-l' }, label));
410
+ }
411
+
412
+ // ==================== 面板 ====================
413
+
414
+ function ActivityPanel(props) {
415
+ const { onClose } = props;
416
+ const state = useStore();
417
+ const data = state.data;
418
+ const today = (data && data.today) || todayKey();
419
+
420
+ const years = (data && data.years && data.years.length ? data.years : [Number(today.slice(0, 4))]).slice();
421
+ const hostYear = (data && data.currentYear) || Number(today.slice(0, 4));
422
+ const [year, setYear] = React.useState(() => {
423
+ try {
424
+ const saved = Number(localStorage.getItem(LS_YEAR));
425
+ if (saved >= 1970) return saved;
426
+ } catch (e) { /* ignore */ }
427
+ return hostYear;
428
+ });
429
+ // 用户选的年份若 host 尚未返回该年数据,则回落到 host 的当前年
430
+ const shownYear = years.indexOf(year) >= 0 ? year : hostYear;
431
+ const stats = (data && data.yearStats && data.yearStats[shownYear]) || null;
432
+
433
+ React.useEffect(() => {
434
+ try { localStorage.setItem(LS_YEAR, String(shownYear)); } catch (e) { /* ignore */ }
435
+ }, [shownYear]);
436
+
437
+ // 首开时若本地记住的年份不是 host 的当前年,按该年拉一次(host 只回该年明细)
438
+ const firstPull = React.useRef(true);
439
+ React.useEffect(() => {
440
+ if (!data) return;
441
+ if (firstPull.current) {
442
+ firstPull.current = false;
443
+ if (year !== hostYear && years.indexOf(year) >= 0) store.setYear(year);
444
+ }
445
+ }, [data, hostYear, year, years]);
446
+
447
+ React.useEffect(() => {
448
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
449
+ document.addEventListener('keydown', onKey);
450
+ return () => document.removeEventListener('keydown', onKey);
451
+ }, [onClose]);
452
+
453
+ const weeks = React.useMemo(() => buildWeeks(shownYear), [shownYear]);
454
+ const labels = React.useMemo(() => monthLabels(weeks), [weeks]);
455
+ const byDay = React.useMemo(() => indexDays(data), [data]);
456
+
457
+ const levels = (data && data.levels) || FALLBACK_LEVELS;
458
+ const gridStyle = { '--daa-cols': String(weeks.length) };
459
+ const pickYear = (y) => { setYear(y); store.setYear(y); };
460
+ // 悬浮明细提到面板层级:避免每个格子各自挂一个 fixed 提示(同时只可能出现一个)
461
+ const [tip, setTip] = React.useState(null);
462
+ // 设置/升级抽屉:开合状态持久化,避免每次打开面板都要重新点一次
463
+ const [gear, setGear] = React.useState(() => {
464
+ try { return localStorage.getItem(LS_GEAR) === '1' } catch (e) { return false }
465
+ });
466
+ const metaState = useMetaStore();
467
+ React.useEffect(() => {
468
+ try { localStorage.setItem(LS_GEAR, gear ? '1' : '0') } catch (e) { /* ignore */ }
469
+ }, [gear]);
470
+ // 面板打开即刷新一次版本信息(保持「有新版本」提示及时)
471
+ React.useEffect(() => { void metaStore.load() }, []);
472
+
473
+ const stats0 = stats || { activeDays: 0, totalDays: daysInYear(shownYear), rate: 0, weekStreak: 0, longestDayStreak: 0, longestWeekStreak: 0 };
474
+
475
+ return React.createElement('div', {
476
+ className: 'daa-backdrop',
477
+ onMouseDown: (e) => { if (e.target === e.currentTarget) onClose(); },
478
+ },
479
+ React.createElement('div', { className: 'daa-card', role: 'dialog', 'aria-label': '全年活跃记录' },
480
+ // ---- 头部 ----
481
+ React.createElement('div', { className: 'daa-head' },
482
+ React.createElement('div', { className: 'daa-head-l' },
483
+ React.createElement('span', { className: 'daa-title' }, '全年活跃记录'),
484
+ React.createElement('span', { className: 'daa-sub' },
485
+ fmtInt(stats0.activeDays) + ' 天活跃 · 活跃率 ' + fmtPercent(stats0.rate))),
486
+ React.createElement('div', { className: 'daa-head-r' },
487
+ YearSwitch({ years, year: shownYear, onPick: pickYear, currentYear: hostYear, onClose }))),
488
+
489
+ // ---- 统计行 + 图例 ----
490
+ React.createElement('div', { className: 'daa-toolbar' },
491
+ React.createElement('div', { className: 'daa-stats' },
492
+ React.createElement(Stat, { value: fmtInt(stats0.activeDays), unit: '天', label: '活跃' }),
493
+ React.createElement(Stat, { value: fmtPercent(stats0.rate), label: '活跃率', accent: true }),
494
+ React.createElement(Stat, { value: fmtInt(stats0.weekStreak), unit: '周', label: '连登' }),
495
+ React.createElement(Stat, { value: fmtInt(stats0.longestDayStreak), unit: '天', label: '最长连续' })),
496
+ React.createElement(Legend, { levels })),
497
+
498
+ // ---- 月份轴 + 热力图 ----
499
+ React.createElement('div', { className: 'daa-heat' },
500
+ React.createElement('div', { className: 'daa-months', style: gridStyle },
501
+ labels.map((l, i) => React.createElement('span', { key: i, className: 'daa-month' }, l || ''))),
502
+ React.createElement('div', { className: 'daa-grid', style: gridStyle },
503
+ weeks.map((week, ci) => React.createElement('div', { className: 'daa-col', key: ci },
504
+ week.map((day, ri) => React.createElement(HeatCell, {
505
+ key: ri, day, rec: day ? byDay[day] : null, size: 11, isToday: day === today, onHover: setTip,
506
+ })))))),
507
+
508
+ // ---- 底部 ----
509
+ React.createElement('div', { className: 'daa-foot' },
510
+ React.createElement('span', { className: 'daa-foot-txt' },
511
+ '数据更新于 ' + fmtClock(data && data.scannedAt) + ' · 每 20 秒自动刷新 · 本机 ' + ((data && data.timeZone) || '本地时区') + ' 自然日口径'),
512
+ React.createElement('span', { className: 'daa-foot-txt daa-foot-right' },
513
+ data && data.totalTurns ? ('累计 ' + fmtInt(data.totalTurns) + ' 轮 · ' + fmtCompact(data.totalTokens) + ' tokens · ' + fmtInt(data.sessionCount) + ' 个会话') : ''),
514
+ React.createElement('button', {
515
+ type: 'button', className: 'daa-refresh' + (state.loading ? ' spin' : ''),
516
+ title: '重新扫描本机会话',
517
+ onClick: () => store.refresh(),
518
+ }, '⟳'),
519
+ React.createElement('button', {
520
+ type: 'button',
521
+ className: 'daa-refresh' + (gear ? ' gear-on' : '') + (metaState.meta && metaState.meta.updateAvailable ? ' has-update' : ''),
522
+ title: metaState.meta && metaState.meta.updateAvailable ? '有新版本 v' + metaState.meta.latest + '(点此查看/升级)' : '设置与版本',
523
+ onClick: () => setGear(!gear),
524
+ }, '⚙')),
525
+ gear ? React.createElement(SetupPanel, {
526
+ onClose: () => setGear(false),
527
+ metaState,
528
+ onLoad: () => metaStore.load(),
529
+ }) : null,
530
+ state.error ? React.createElement('div', { className: 'daa-error' }, 'host 未就绪:' + state.error + '(重试中…)') : null,
531
+ !data && state.loading ? React.createElement('div', { className: 'daa-loading' }, '正在扫描本机会话历史…') : null),
532
+ tip ? React.createElement(CellTip, { tip }) : null);
533
+ }
534
+
535
+ // ==================== 设置 / 版本升级抽屉 ====================
536
+
537
+ function SetupPanel(props) {
538
+ const { onClose, metaState, onLoad } = props;
539
+ const meta = metaState.meta;
540
+ const up = (meta && meta.upgrade) || {};
541
+ const busy = metaState.upgrading || up.running;
542
+ const updateAvailable = !!(meta && meta.updateAvailable);
543
+
544
+ React.useEffect(() => { if (onLoad) void onLoad() }, []);
545
+
546
+ const buttonLabel = busy ? '升级中…'
547
+ : metaState.checking ? '检测中…'
548
+ : updateAvailable ? '升级到 v' + meta.latest
549
+ : '检测更新';
550
+ const onClick = () => {
551
+ if (busy || metaState.checking) return;
552
+ if (updateAvailable) void metaStore.upgrade();
553
+ else void metaStore.check();
554
+ };
555
+
556
+ return React.createElement('div', { className: 'daa-setup' },
557
+ React.createElement('div', { className: 'daa-setup-head' },
558
+ React.createElement('span', { className: 'daa-setup-title' }, '插件设置'),
559
+ React.createElement('button', {
560
+ type: 'button', className: 'daa-setup-close', title: '收起', onClick: onClose,
561
+ }, '✕')),
562
+ React.createElement('div', { className: 'daa-setup-row' },
563
+ React.createElement('span', { className: 'daa-setup-k' }, '当前版本'),
564
+ React.createElement('span', { className: 'daa-setup-v' }, meta ? 'v' + meta.version : (metaState.error ? '—' : '读取中…'))),
565
+ React.createElement('div', { className: 'daa-setup-row' },
566
+ React.createElement('span', { className: 'daa-setup-k' }, '最新版本'),
567
+ React.createElement('span', { className: 'daa-setup-v' },
568
+ meta && meta.latest
569
+ ? ('v' + meta.latest + (updateAvailable ? ' · 有更新' : ' · 已是最新'))
570
+ : (meta && meta.updateChecked ? '未发布 / 离线' : '未检测'))),
571
+ meta && meta.localInstall ? React.createElement('div', { className: 'daa-setup-note' },
572
+ '本地安装(' + meta.localInstall + '):直接改源码即可,在线升级已自动跳过。') : null,
573
+ React.createElement('div', { className: 'daa-setup-actions' },
574
+ React.createElement('button', {
575
+ type: 'button',
576
+ className: 'daa-setup-btn' + (updateAvailable ? ' primary' : ''),
577
+ disabled: busy || metaState.checking || !meta,
578
+ onClick,
579
+ }, buttonLabel)),
580
+ up.message || metaState.notice ? React.createElement('div', { className: 'daa-setup-notice' }, up.message || metaState.notice) : null,
581
+ up.ok === 'ok' ? React.createElement('div', { className: 'daa-setup-note' }, '升级只替换安装文件:重启 dsh web(或执行 scripts/restart-web.sh)后生效。') : null,
582
+ metaState.error ? React.createElement('div', { className: 'daa-error' }, 'host 未就绪:' + metaState.error) : null,
583
+ React.createElement('div', { className: 'daa-setup-foot' },
584
+ '数据全部来自本机会话日志,不联网、不上报;仅「检测更新 / 升级」会访问 npm registry。'));
585
+ }
586
+
587
+ // ==================== 入口(会话头部右侧,紧邻「打开右侧边栏」)====================
588
+
589
+ function HeaderEntry() {
590
+ const state = useStore();
591
+ const metaState = useMetaStore();
592
+ const [open, setOpen] = React.useState(false);
593
+ const [tip, setTip] = React.useState(null);
594
+ const btnRef = React.useRef(null);
595
+
596
+ // 面板开合驱动轮询频率(打开时 20s,关闭时 3 分钟)
597
+ React.useEffect(() => {
598
+ store.setPanelOpen(open);
599
+ }, [open]);
600
+
601
+ const data = state.data;
602
+ const byDay = React.useMemo(() => indexDays(data), [data]);
603
+ const today = (data && data.today) || todayKey();
604
+ const rec = byDay[today] || null;
605
+ const curStats = data && data.yearStats ? data.yearStats[data.currentYear] : null;
606
+ const meta = metaState.meta;
607
+ const updateAvailable = !!(meta && meta.updateAvailable);
608
+
609
+ const title = '全年活跃记录 (dsh-annual-activity)'
610
+ + (data
611
+ ? ':' + fmtInt(curStats ? curStats.activeDays : 0) + ' 天活跃 · 活跃率 ' + fmtPercent(curStats ? curStats.rate : 0)
612
+ + ' · 今日 ' + (rec ? fmtInt(rec.turns) + ' 轮' : '未活跃')
613
+ + (meta ? ' · v' + meta.version : '')
614
+ : ':加载中…')
615
+ + '(点击查看年度面板)';
616
+
617
+ const onEnter = () => {
618
+ const el = btnRef.current;
619
+ if (!el) return;
620
+ const r = el.getBoundingClientRect();
621
+ setTip({ x: r.left + r.width / 2, y: r.bottom, today: rec, stats: curStats });
622
+ };
623
+
624
+ return React.createElement('div', { className: 'daa-host' },
625
+ React.createElement('button', {
626
+ ref: btnRef,
627
+ type: 'button',
628
+ className: 'daa-hbtn' + (open ? ' on' : ''),
629
+ 'aria-label': '全年活跃记录',
630
+ title,
631
+ onClick: () => setOpen(!open),
632
+ onMouseEnter: onEnter,
633
+ onMouseLeave: () => setTip(null),
634
+ },
635
+ React.createElement('span', { className: 'daa-glyph', 'aria-hidden': 'true' },
636
+ glyphCells(byDay).map((c, i) => React.createElement('span', {
637
+ key: i, className: 'daa-glyph-cell' + (c.level < 0 ? ' daa-glyph-void' : ' daa-l' + c.level),
638
+ }))),
639
+ updateAvailable ? React.createElement('span', { className: 'daa-hbtn-dot', title: '有新版本 v' + (meta && meta.latest) }) : null),
640
+ tip ? React.createElement(EntryTip, { tip }) : null,
641
+ open ? React.createElement(ActivityPanel, { onClose: () => setOpen(false) }) : null);
642
+ }
643
+
644
+ /** 入口悬浮说明(fixed 定位在按钮下方)。 */
645
+ function EntryTip({ tip }) {
646
+ const { today: rec, stats, x, y } = tip;
647
+ const rows = [
648
+ ['今日', rec ? fmtInt(rec.turns) + ' 轮' : '未活跃'],
649
+ ['今年', (stats ? fmtInt(stats.activeDays) : '0') + ' 天活跃 · ' + fmtPercent(stats ? stats.rate : 0)],
650
+ ['连登', fmtInt(stats ? stats.weekStreak : 0) + ' 周'],
651
+ ];
652
+ const left = Math.min(Math.max(Math.round(x), 110), Math.max(110, window.innerWidth - 130));
653
+ return React.createElement('div', { className: 'daa-tip daa-tip-below', style: { left: left + 'px', top: Math.round(y) + 8 + 'px' } },
654
+ rows.map(([k, v]) => React.createElement('div', { className: 'daa-tip-row', key: k },
655
+ React.createElement('span', { className: 'daa-tip-k' }, k),
656
+ React.createElement('span', { className: 'daa-tip-v' }, v))));
657
+ }
658
+
659
+ // ==================== 样式 ====================
660
+
661
+ const CSS = `
662
+ .daa-host{position:relative;display:flex;align-items:center;
663
+ font-family:'Inter','PingFang SC','Microsoft YaHei',ui-sans-serif,system-ui,sans-serif;
664
+ color:var(--dsh-text-1,#1f2937);}
665
+ /* ---------- 头部入口按钮(对齐「打开右侧边栏」:28×28 / radius 28 / 16px 图标)---------- */
666
+ .daa-hbtn{position:relative;box-sizing:border-box;width:28px;height:28px;flex:none;padding:6px;
667
+ display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
668
+ background:transparent;border:none;border-radius:28px;
669
+ color:var(--dsw-alias-label-secondary,#4b5563);transition:background .14s ease,color .14s ease;}
670
+ .daa-hbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(15,23,42,.06));
671
+ color:var(--dsw-alias-label-primary,#111827);}
672
+ .daa-hbtn.on{background:var(--dsw-alias-interactive-bg-active,rgba(15,23,42,.10));
673
+ color:var(--dsw-alias-label-primary,#111827);}
674
+ .daa-glyph{display:grid;grid-template-columns:repeat(7,2px);grid-template-rows:repeat(2,2px);gap:1px;}
675
+ .daa-glyph-cell{width:2px;height:2px;border-radius:.5px;}
676
+ .daa-glyph-void{background:transparent;}
677
+ .daa-hbtn-dot{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;
678
+ background:var(--dsw-alias-state-business-primary,#f59e0b);
679
+ box-shadow:0 0 0 1.5px var(--dsw-alias-bg-base,#fff);}
680
+ .daa-tip-below{transform:translate(-50%,0);}
681
+ /* ---------- 遮罩 + 卡片 ---------- */
682
+ .daa-backdrop{position:fixed;inset:0;pointer-events:auto;background:rgba(15,23,42,.42);
683
+ display:flex;align-items:center;justify-content:center;padding:24px;animation:daa-fade .14s ease-out;}
684
+ @keyframes daa-fade{from{opacity:0}to{opacity:1}}
685
+ .daa-card{position:relative;width:min(828px,calc(100vw - 40px));max-height:calc(100vh - 48px);overflow:auto;
686
+ background:var(--dsh-surface-1,#fff);border:1px solid var(--dsh-border,#e8eaee);border-radius:14px;
687
+ box-shadow:0 24px 64px rgba(15,23,42,.26);padding:16px 20px 12px;
688
+ animation:daa-pop .16s cubic-bezier(.2,.9,.3,1.2);}
689
+ @keyframes daa-pop{from{opacity:0;transform:translateY(8px) scale(.985)}to{opacity:1;transform:none}}
690
+ /* ---------- 头部 ---------- */
691
+ .daa-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px;}
692
+ .daa-head-l{display:flex;align-items:baseline;gap:8px;min-width:0;}
693
+ .daa-title{font-size:16px;font-weight:600;letter-spacing:.2px;color:var(--dsh-text-1,#111827);white-space:nowrap;}
694
+ .daa-sub{font-size:12px;color:var(--dsh-text-3,#9aa1ab);white-space:nowrap;}
695
+ .daa-head-r{display:flex;align-items:center;gap:8px;}
696
+ .daa-year{position:relative;display:flex;align-items:center;gap:2px;}
697
+ .daa-year-btn{width:24px;height:24px;border-radius:7px;border:1px solid var(--dsh-border,#e5e7eb);
698
+ background:var(--dsh-surface-1,#fff);color:var(--dsh-text-2,#4b5563);cursor:pointer;font-size:14px;line-height:1;
699
+ display:flex;align-items:center;justify-content:center;padding:0;}
700
+ .daa-year-btn:hover:not(:disabled){background:var(--dsh-surface-2,#f3f4f6);color:var(--dsh-text-1,#111827);}
701
+ .daa-year-btn:disabled{opacity:.35;cursor:default;}
702
+ .daa-year-cur{height:24px;padding:0 9px;border-radius:7px;border:1px solid var(--dsh-border,#e5e7eb);
703
+ background:var(--dsh-surface-1,#fff);color:var(--dsh-text-1,#111827);cursor:pointer;font-size:12.5px;font-weight:600;
704
+ display:flex;align-items:center;gap:5px;}
705
+ .daa-year-cur:hover{background:var(--dsh-surface-2,#f3f4f6);}
706
+ .daa-year-now{color:#15803d;border-color:#bbe3c6;}
707
+ .daa-year-caret{font-size:9px;color:var(--dsh-text-3,#9aa1ab);}
708
+ .daa-year-menu{position:absolute;top:28px;right:0;z-index:5;min-width:104px;padding:4px;border-radius:10px;
709
+ background:var(--dsh-surface-1,#fff);border:1px solid var(--dsh-border,#e5e7eb);box-shadow:0 10px 28px rgba(15,23,42,.16);
710
+ display:flex;flex-direction:column;gap:2px;}
711
+ .daa-year-item{display:flex;align-items:center;justify-content:space-between;gap:8px;height:26px;padding:0 8px;
712
+ border:0;border-radius:7px;background:transparent;color:var(--dsh-text-2,#4b5563);cursor:pointer;font-size:12.5px;text-align:left;}
713
+ .daa-year-item:hover{background:var(--dsh-surface-2,#f3f4f6);color:var(--dsh-text-1,#111827);}
714
+ .daa-year-item.on{color:#15803d;font-weight:600;}
715
+ .daa-year-badge{font-size:10px;color:#15803d;background:#e8f6ec;border-radius:5px;padding:1px 4px;}
716
+ .daa-close{width:24px;height:24px;border-radius:999px;border:1px solid var(--dsh-border,#e5e7eb);
717
+ background:var(--dsh-surface-1,#fff);color:var(--dsh-text-3,#9aa1ab);cursor:pointer;font-size:11px;line-height:1;
718
+ display:flex;align-items:center;justify-content:center;padding:0;margin-left:2px;}
719
+ .daa-close:hover{background:var(--dsh-surface-2,#f3f4f6);color:var(--dsh-text-1,#111827);}
720
+ /* ---------- 统计行 + 图例 ---------- */
721
+ .daa-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px;}
722
+ .daa-stats{display:flex;align-items:baseline;gap:22px;}
723
+ .daa-stat{display:flex;align-items:baseline;gap:5px;}
724
+ .daa-stat-v{font-size:15px;font-weight:700;color:var(--dsh-text-1,#111827);letter-spacing:.2px;}
725
+ .daa-stat-v.daa-accent{color:#16a34a;}
726
+ .daa-stat-l{font-size:12.5px;color:var(--dsh-text-3,#9aa1ab);}
727
+ .daa-legend{display:flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsh-text-3,#9aa1ab);}
728
+ .daa-legend-cap{padding:0 3px;}
729
+ .daa-legend-cell{width:11px;height:11px;border-radius:3px;display:inline-block;}
730
+ /* ---------- 热力图 ---------- */
731
+ .daa-heat{overflow-x:auto;overflow-y:hidden;padding-bottom:2px;}
732
+ .daa-months{display:grid;grid-template-columns:repeat(var(--daa-cols,53),11px);gap:3px;margin-bottom:6px;min-width:max-content;}
733
+ .daa-month{font-size:11.5px;color:var(--dsh-text-3,#9aa1ab);white-space:nowrap;line-height:1;overflow:visible;}
734
+ .daa-grid{display:grid;grid-template-columns:repeat(var(--daa-cols,53),11px);gap:3px;min-width:max-content;}
735
+ .daa-col{display:flex;flex-direction:column;gap:3px;}
736
+ .daa-cell{border-radius:3px;position:relative;background:#eef0f2;transition:transform .1s ease,box-shadow .1s ease;}
737
+ .daa-cell-pad{background:transparent;}
738
+ .daa-cell:not(.daa-cell-pad):hover{transform:scale(1.18);box-shadow:0 0 0 1.5px rgba(22,163,74,.45);}
739
+ .daa-today{box-shadow:inset 0 0 0 1.5px #16a34a;}
740
+ /* 5 级色块:0 未活跃 → 4 极高(GitHub 风格绿色梯度) */
741
+ .daa-l0{background:#eef0f2;}
742
+ .daa-l1{background:#c6e8cf;}
743
+ .daa-l2{background:#7fce97;}
744
+ .daa-l3{background:#34a853;}
745
+ .daa-l4{background:#0f7a37;}
746
+ /* ---------- 悬浮明细 ---------- */
747
+ .daa-tip{position:fixed;transform:translate(-50%,-100%);z-index:20;pointer-events:none;
748
+ background:var(--dsh-surface-1,#fff);border:1px solid var(--dsh-border,#e5e7eb);border-radius:9px;
749
+ box-shadow:0 8px 26px rgba(15,23,42,.18);padding:8px 10px;min-width:184px;}
750
+ .daa-tip-head{font-size:12px;font-weight:600;color:var(--dsh-text-1,#111827);margin-bottom:5px;}
751
+ .daa-tip-row{display:flex;justify-content:space-between;gap:12px;font-size:11.5px;line-height:1.7;}
752
+ .daa-tip-k{color:var(--dsh-text-3,#9aa1ab);}
753
+ .daa-tip-v{color:var(--dsh-text-1,#111827);font-variant-numeric:tabular-nums;}
754
+ /* ---------- 底部 ---------- */
755
+ .daa-foot{display:flex;align-items:center;gap:10px;margin-top:12px;padding-top:9px;
756
+ border-top:1px solid var(--dsh-border,#f0f1f3);font-size:11.5px;color:var(--dsh-text-3,#9aa1ab);}
757
+ .daa-foot-txt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
758
+ .daa-foot-right{margin-left:auto;text-align:right;}
759
+ .daa-refresh{flex:none;width:22px;height:22px;border-radius:6px;border:1px solid var(--dsh-border,#e5e7eb);
760
+ background:var(--dsh-surface-1,#fff);color:var(--dsh-text-3,#9aa1ab);cursor:pointer;font-size:12px;line-height:1;
761
+ display:flex;align-items:center;justify-content:center;padding:0;}
762
+ .daa-refresh:hover{background:var(--dsh-surface-2,#f3f4f6);color:var(--dsh-text-1,#111827);}
763
+ .daa-refresh.spin{animation:daa-spin 1s linear infinite;}
764
+ @keyframes daa-spin{to{transform:rotate(360deg)}}
765
+ .daa-refresh.gear-on{background:var(--dsh-surface-2,#f3f4f6);color:var(--dsh-text-1,#111827);}
766
+ .daa-refresh.has-update{color:var(--dsw-alias-state-business-primary,#f59e0b);border-color:currentColor;}
767
+ /* ---------- 设置 / 版本升级抽屉 ---------- */
768
+ .daa-setup{margin-top:10px;padding:11px 12px;border:1px solid var(--dsh-border,#e8eaee);border-radius:10px;
769
+ background:var(--dsh-surface-2,#fafbfc);}
770
+ .daa-setup-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;}
771
+ .daa-setup-title{font-size:12.5px;font-weight:600;color:var(--dsh-text-1,#111827);}
772
+ .daa-setup-close{width:20px;height:20px;border-radius:6px;border:1px solid transparent;background:transparent;
773
+ color:var(--dsh-text-3,#9aa1ab);cursor:pointer;font-size:10px;line-height:1;padding:0;
774
+ display:flex;align-items:center;justify-content:center;}
775
+ .daa-setup-close:hover{background:var(--dsh-surface-1,#fff);border-color:var(--dsh-border,#e5e7eb);color:var(--dsh-text-1,#111827);}
776
+ .daa-setup-row{display:flex;justify-content:space-between;gap:12px;font-size:12px;line-height:1.9;}
777
+ .daa-setup-k{color:var(--dsh-text-3,#9aa1ab);}
778
+ .daa-setup-v{color:var(--dsh-text-1,#111827);font-variant-numeric:tabular-nums;}
779
+ .daa-setup-actions{display:flex;gap:8px;margin-top:9px;}
780
+ .daa-setup-btn{height:26px;padding:0 12px;border-radius:8px;cursor:pointer;font-size:12px;
781
+ border:1px solid var(--dsh-border,#e5e7eb);background:var(--dsh-surface-1,#fff);color:var(--dsh-text-2,#4b5563);}
782
+ .daa-setup-btn:hover:not(:disabled){background:var(--dsh-surface-2,#f3f4f6);color:var(--dsh-text-1,#111827);}
783
+ .daa-setup-btn:disabled{opacity:.55;cursor:default;}
784
+ .daa-setup-btn.primary{border-color:transparent;background:#16a34a;color:#fff;}
785
+ .daa-setup-btn.primary:hover:not(:disabled){background:#15803d;color:#fff;}
786
+ .daa-setup-notice{margin-top:8px;font-size:11.5px;line-height:1.6;color:var(--dsh-text-2,#4b5563);
787
+ background:var(--dsh-surface-1,#fff);border:1px solid var(--dsh-border,#e8eaee);border-radius:8px;padding:6px 9px;}
788
+ .daa-setup-note{margin-top:7px;font-size:11.5px;line-height:1.6;color:var(--dsh-text-3,#9aa1ab);}
789
+ .daa-setup-foot{margin-top:8px;padding-top:7px;border-top:1px solid var(--dsh-border,#eef0f2);
790
+ font-size:11px;line-height:1.6;color:var(--dsh-text-3,#9aa1ab);}
791
+ .daa-error{margin-top:8px;font-size:11.5px;color:#b45309;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:6px 9px;}
792
+ .daa-loading{margin-top:10px;font-size:12px;color:var(--dsh-text-3,#9aa1ab);}
793
+ /* 深色主题兜底(DSH 未提供变量时按 prefers-color-scheme 走) */
794
+ @media (prefers-color-scheme:dark){
795
+ .daa-host{color:var(--dsh-text-1,#e5e7eb);}
796
+ .daa-card,.daa-year-cur,.daa-year-btn,.daa-close,.daa-refresh,.daa-tip,.daa-year-menu,
797
+ .daa-setup-btn,.daa-setup-notice{
798
+ background:var(--dsh-surface-1,#1b1e24);border-color:var(--dsh-border,#2f343d);}
799
+ .daa-setup{background:var(--dsh-surface-2,#171a1f);border-color:var(--dsh-border,#2f343d);}
800
+ .daa-hbtn-dot{box-shadow:0 0 0 1.5px var(--dsh-surface-1,#1b1e24);}
801
+ .daa-l0{background:#2a2f37;}
802
+ .daa-foot,.daa-toolbar,.daa-setup-foot{border-color:var(--dsh-border,#2f343d);}
803
+ .daa-error{background:#3a2a12;border-color:#7c5310;color:#fbbf24;}
804
+ }
805
+ `;
806
+
807
+ // ==================== 注册 ====================
808
+
809
+ function apply(ctx) {
810
+ const slots = ctx.get("slots");
811
+ if (!slots) return;
812
+
813
+ // 注入样式(随插件卸载自动移除):返回值才是清理函数
814
+ const styleEl = document.createElement("style");
815
+ styleEl.setAttribute("data-plugin", "dsh-annual-activity");
816
+ styleEl.textContent = CSS;
817
+ document.head.appendChild(styleEl);
818
+ ctx.effect(() => () => { try { styleEl.remove(); } catch (e) { /* ignore */ } });
819
+
820
+ // 注册到会话头部右侧「utility」列表槽:该槽在 DOM 中先于右上角
821
+ // corner 槽(「打开右侧边栏」按钮所在处)渲染,所以这个入口会落在
822
+ // 那个按钮的左边、同一行内。按钮尺寸/圆角/色板都与 corner 里的
823
+ // 官方按钮保持一致(28×28、图标 16px、interactive-bg-hover 悬停底色)。
824
+ slots.inject('conversation.session.header.utilities', () => slots.register(
825
+ { name: 'conversation.session.header.utilities', id: 'annual-activity', order: 50 },
826
+ () => React.createElement(HeaderEntry, null)
827
+ ));
828
+ }
829
+
830
+ exports.name = name;
831
+ exports.inject = inject;
832
+ exports.apply = apply;
833
+ exports.__internal = { buildWeeks, monthLabels, fmtPercent, fmtCompact, levelOfDay, indexDays, glyphCells, FALLBACK_LEVELS, store, metaStore };
834
+ return module.exports;
835
+ }
836
+ });