@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/LOGFORMATS.md +960 -0
  3. package/README.md +238 -0
  4. package/REFERENCE.md +1428 -0
  5. package/agent-acta-page.html +1998 -0
  6. package/agent-acta-server.mjs +1066 -0
  7. package/agent-acta-widget.html +724 -0
  8. package/archive.mjs +335 -0
  9. package/core/log.mjs +20 -0
  10. package/core/service.mjs +4285 -0
  11. package/hooks/cursor-usage-hook.mjs +89 -0
  12. package/mcp-server.mjs +372 -0
  13. package/package.json +68 -0
  14. package/page/dialog-add-agent.js +65 -0
  15. package/page/dialog-alert.js +159 -0
  16. package/page/dialog-archive.js +342 -0
  17. package/page/dialog-compare.js +238 -0
  18. package/page/dialog-ctxw.js +101 -0
  19. package/page/dialog-diag.js +72 -0
  20. package/page/dialog-heatmap.js +215 -0
  21. package/page/dialog-selftest.js +202 -0
  22. package/page/dialog-sess-timeline.js +108 -0
  23. package/page/dialog-stats.js +694 -0
  24. package/page/dialog-usage.js +158 -0
  25. package/page/log-card.js +277 -0
  26. package/page/log-view.js +273 -0
  27. package/page/sess-view.js +382 -0
  28. package/page/shared.js +408 -0
  29. package/page/sidebar.js +129 -0
  30. package/page/style.css +973 -0
  31. package/page/topbar.js +61 -0
  32. package/parsers/atomcode.mjs +321 -0
  33. package/parsers/buddy.mjs +272 -0
  34. package/parsers/buddyext.mjs +582 -0
  35. package/parsers/claude.mjs +496 -0
  36. package/parsers/cline.mjs +468 -0
  37. package/parsers/codearts.mjs +96 -0
  38. package/parsers/codex.mjs +188 -0
  39. package/parsers/comate.mjs +195 -0
  40. package/parsers/copilot.mjs +308 -0
  41. package/parsers/cursor.mjs +493 -0
  42. package/parsers/devindb.mjs +358 -0
  43. package/parsers/discovery.mjs +1230 -0
  44. package/parsers/doubao.mjs +406 -0
  45. package/parsers/dsh.mjs +377 -0
  46. package/parsers/gemini.mjs +267 -0
  47. package/parsers/generic.mjs +494 -0
  48. package/parsers/hermes.mjs +398 -0
  49. package/parsers/kilo.mjs +42 -0
  50. package/parsers/kimi.mjs +249 -0
  51. package/parsers/mimocode.mjs +531 -0
  52. package/parsers/minimax.mjs +503 -0
  53. package/parsers/openclaw.mjs +490 -0
  54. package/parsers/opencode.mjs +465 -0
  55. package/parsers/shared.mjs +418 -0
  56. package/parsers/trace.mjs +66 -0
  57. package/parsers/tracecode.mjs +348 -0
  58. package/parsers/traedb.mjs +528 -0
  59. package/parsers/zcode.mjs +356 -0
  60. package/plugin/client.js +51 -0
  61. package/plugin/cordis.patch.yml +5 -0
  62. package/plugin/plugin.mjs +220 -0
  63. package/search.mjs +554 -0
  64. package/tools/grab-trae-key.ps1 +265 -0
  65. package/vendor/element-plus-icons.js +3 -0
  66. package/vendor/element-plus.css +1 -0
  67. package/vendor/element-plus.dark.css +1 -0
  68. package/vendor/element-plus.full.min.js +65 -0
  69. package/vendor/element-plus.zh-cn.js +2 -0
  70. package/vendor/svg/antgroup-color.svg +1 -0
  71. package/vendor/svg/bytedance-color.svg +1 -0
  72. package/vendor/svg/chatglm-color.svg +1 -0
  73. package/vendor/svg/claude-color.svg +1 -0
  74. package/vendor/svg/claudecode-color.svg +1 -0
  75. package/vendor/svg/cline.svg +1 -0
  76. package/vendor/svg/codebuddy-color.svg +1 -0
  77. package/vendor/svg/codebuddy.svg +1 -0
  78. package/vendor/svg/codex-color.svg +1 -0
  79. package/vendor/svg/copilot.svg +1 -0
  80. package/vendor/svg/cursor.svg +1 -0
  81. package/vendor/svg/deepseek-color.svg +1 -0
  82. package/vendor/svg/deepseek-harness-whale.svg +8 -0
  83. package/vendor/svg/doubao-color.svg +1 -0
  84. package/vendor/svg/gemini-color.svg +1 -0
  85. package/vendor/svg/glmv-color.svg +1 -0
  86. package/vendor/svg/grok.svg +1 -0
  87. package/vendor/svg/hermesagent.svg +1 -0
  88. package/vendor/svg/huawei-color.svg +1 -0
  89. package/vendor/svg/hunyuan-color.svg +1 -0
  90. package/vendor/svg/kilocode-avatar.svg +1 -0
  91. package/vendor/svg/kilocode.svg +1 -0
  92. package/vendor/svg/kimi-color.svg +1 -0
  93. package/vendor/svg/kimi.svg +1 -0
  94. package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
  95. package/vendor/svg/logo-AtomGit-dark.svg +4 -0
  96. package/vendor/svg/logo-AtomGit-light.svg +4 -0
  97. package/vendor/svg/meta-color.svg +1 -0
  98. package/vendor/svg/mimo.svg +66 -0
  99. package/vendor/svg/minimax-code-logo.svg +8 -0
  100. package/vendor/svg/minimax-color.svg +1 -0
  101. package/vendor/svg/moonshot.svg +1 -0
  102. package/vendor/svg/nex-color.svg +1 -0
  103. package/vendor/svg/nex.svg +1 -0
  104. package/vendor/svg/nvidia-color.svg +1 -0
  105. package/vendor/svg/openai.svg +1 -0
  106. package/vendor/svg/openclaw-color.svg +1 -0
  107. package/vendor/svg/openclaw.svg +1 -0
  108. package/vendor/svg/opencode.svg +1 -0
  109. package/vendor/svg/openrouter-color.svg +1 -0
  110. package/vendor/svg/qoder-color.svg +1 -0
  111. package/vendor/svg/qwen-color.svg +1 -0
  112. package/vendor/svg/sensenova-color.svg +1 -0
  113. package/vendor/svg/stepfun.svg +1 -0
  114. package/vendor/svg/trae-color.svg +1 -0
  115. package/vendor/svg/traework-v2.svg +1 -0
  116. package/vendor/svg/wenxin-color.svg +1 -0
  117. package/vendor/svg/windsurf.svg +1 -0
  118. package/vendor/svg/workbuddy-color.svg +29 -0
  119. package/vendor/svg/xiaomimimo.svg +1 -0
  120. package/vendor/svg/yuanbao-color.svg +1 -0
  121. package/vendor/svg/yuanbao.svg +1 -0
  122. package/vendor/svg/zai.svg +1 -0
  123. package/vendor/svg/zhipu-color.svg +1 -0
  124. package/vendor/vue.global.prod.js +9 -0
  125. package/widget/main.js +202 -0
  126. package/widget/package.json +7 -0
  127. package/widget/preload.js +11 -0
@@ -0,0 +1,238 @@
1
+ // 两轮对比弹层(候选池 I9):选中主列表里两条轮,并排对比模型 / token / 工具链 / 耗时,
2
+ // 回答「同一个 prompt 为什么这次慢」。入口 = 每张卡片顶行的「对比」按钮(选两条自动弹开)。
3
+ // 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue / Element Plus 的 API 一律写全名。
4
+ //
5
+ // 数据进出:
6
+ // · visible ← 根(卡片「对比」选满两条 / 弹层关闭时回写)
7
+ // · req ← 根:{ a, b } —— 两条条目本体(快照 / 搜索命中集里现有的对象,带模型 / token / 耗时等统计字段)
8
+ // · getJSON ← 根 provide(接口 404 要点亮版本红条)
9
+ // · 正文(用户输入 / AI 输出 / 工具链)仍走 /api/entry 懒加载:点开对比才取,两条各自独立缓存。
10
+ //
11
+ // 对比口径:统计行用的是条目**本体**(快照就带,不必等详情),正文与工具链用的是 /api/entry 详情。
12
+ // 每条指标行给一个「谁大」的标记 —— 这一条是 diff 的意义所在,光把两列摆一起不标出差值不算对比。
13
+ // qoder 不落盘 token(tin/tout/total 恒 0,只有最后一次调用的 context_usage_ratio):
14
+ // 它的格子一律走「积分」与「占比」两行,绝不在 Token 行印 0(印了会被读成「这轮没花钱」),
15
+ // 两侧单位不同(一积分一 token)时不做差 —— 与卡片 page/log-card.js 的 isQoder 分支同一口径。
16
+ window.AACTA = window.AACTA || {};
17
+ AACTA.DialogCompare = {
18
+ name: 'aa-dialog-compare',
19
+ template: `
20
+ <el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="两轮对比" width="min(1100px, 96vw)" append-to-body class="cmp-dialog" top="4vh">
21
+ <div v-if="!reqA || !reqB" class="empty">还没有选够两条轮,先回列表里点卡片顶行的「对比」。</div>
22
+ <div v-else>
23
+ <!-- 概览:两列头部(agent / 模型 / 时间 / 状态)+ 耗时条相对比例 -->
24
+ <div class="cmp-head">
25
+ <div class="cmp-col cmp-col-b">
26
+ <div class="cmp-hdr">
27
+ <span class="cmp-agent"><b>{{ titleOf(reqA) }}</b></span>
28
+ <span class="mini-tag">{{ reqA.agent }}</span>
29
+ <span :class="'tag ' + (reqA.status==='ok' ? 'tag-ok' : 'tag-err')">{{ reqA.status==='ok' ? 'OK' : 'ERR ⚠' }}</span>
30
+ </div>
31
+ <div class="cmp-meta">{{ fmtTime(reqA.time) }} · {{ reqA.project }}</div>
32
+ </div>
33
+ <div class="cmp-vs">VS</div>
34
+ <div class="cmp-col">
35
+ <div class="cmp-hdr">
36
+ <span class="cmp-agent"><b>{{ titleOf(reqB) }}</b></span>
37
+ <span class="mini-tag">{{ reqB.agent }}</span>
38
+ <span :class="'tag ' + (reqB.status==='ok' ? 'tag-ok' : 'tag-err')">{{ reqB.status==='ok' ? 'OK' : 'ERR ⚠' }}</span>
39
+ </div>
40
+ <div class="cmp-meta">{{ fmtTime(reqB.time) }} · {{ reqB.project }}</div>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="cmp-table">
45
+ <!-- 耗时:并排进度条,一眼看出谁慢 -->
46
+ <div class="cmp-row cmp-dur">
47
+ <span class="cmp-label">耗时<b v-if="durDiffOn" class="cmp-hot">差 {{ cmpDurText }}</b></span>
48
+ <div class="cmp-barwrap">
49
+ <div class="cmp-bar" :style="{width: durPctOf(reqA) + '%'}"><span>{{ fmtDur(reqA.dur) }}</span></div>
50
+ </div>
51
+ <div class="cmp-barwrap">
52
+ <div class="cmp-bar bar-b" :style="{width: durPctOf(reqB) + '%'}"><span>{{ fmtDur(reqB.dur) }}</span></div>
53
+ </div>
54
+ </div>
55
+ <!-- 指标行全在 setup 的 cmpRows 里算:显示文本、「谁大」、「差多少」用同一份数,不在模板里各算一遍 -->
56
+ <div class="cmp-row" v-for="r in cmpRows" :key="r.label">
57
+ <span class="cmp-label">{{ r.label }}<b v-if="r.hot" class="cmp-hot">{{ r.hot }}</b></span>
58
+ <span class="cmp-val" :class="{big: r.big === 'a'}" :title="r.at">{{ r.a }}</span>
59
+ <span class="cmp-val" :class="{big: r.big === 'b'}" :title="r.bt">{{ r.b }}</span>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="cmp-loading" v-if="loading">正在读取两条正文…</div>
64
+ <div v-else-if="err" class="cmp-err">{{ err }} <el-button size="small" @click="load">重试</el-button></div>
65
+ <!-- 正文对比 -->
66
+ <div v-else class="cmp-body">
67
+ <div class="cmp-sec">
68
+ <div class="cmp-sec-head"><h4>用户输入</h4></div>
69
+ <div class="cmp-cols">
70
+ <div class="cmp-col"><pre class="cmp-pre">{{ detA && detA.user ? detA.user : '(空)' }}</pre></div>
71
+ <div class="cmp-col"><pre class="cmp-pre">{{ detB && detB.user ? detB.user : '(空)' }}</pre></div>
72
+ </div>
73
+ </div>
74
+ <div class="cmp-sec" v-if="(detA && detA.assistant) || (detB && detB.assistant)">
75
+ <div class="cmp-sec-head"><h4>AI 输出</h4></div>
76
+ <div class="cmp-cols">
77
+ <div class="cmp-col"><div v-if="detA && detA.assistant" class="md-body" v-html="detA._assistantMd"></div><span v-else class="cmp-none">(无)</span></div>
78
+ <div class="cmp-col"><div v-if="detB && detB.assistant" class="md-body" v-html="detB._assistantMd"></div><span v-else class="cmp-none">(无)</span></div>
79
+ </div>
80
+ </div>
81
+ <div class="cmp-sec" v-if="(detA && detA.tools && detA.tools.length) || (detB && detB.tools && detB.tools.length)">
82
+ <div class="cmp-sec-head"><h4>工具调用</h4></div>
83
+ <!-- 入参/返回一律先读 prepDetail 缓存的 _inPretty / _outPretty(缓存缺失才现算):
84
+ 一对长会话能并到 90 多个工具项,每条重渲染都全量 prettyJson 就是把卡片当年踩过的坑再踩一遍。 -->
85
+ <div class="cmp-cols">
86
+ <div class="cmp-col">
87
+ <div v-for="(t, i) in (detA ? detA.tools : [])" :key="i" class="tool-item cmp-tool">
88
+ <div class="tool-head"><span class="tname">{{ t.name }}</span><span v-if="t.error" class="terr">✗ {{ t.error }}</span><span v-if="t.dur!=null" class="tdur">{{ fmtDur(t.dur) }}</span></div>
89
+ <pre v-if="t.input" class="cmp-pre">{{ t._inPretty || prettyJson(t.input) }}</pre>
90
+ <pre v-if="t.output" class="cmp-pre">{{ t._outPretty || prettyJson(t.output) }}</pre>
91
+ </div>
92
+ <span v-if="!detA || !detA.tools || !detA.tools.length" class="cmp-none">(没有工具调用)</span>
93
+ </div>
94
+ <div class="cmp-col">
95
+ <div v-for="(t, i) in (detB ? detB.tools : [])" :key="i" class="tool-item cmp-tool">
96
+ <div class="tool-head"><span class="tname">{{ t.name }}</span><span v-if="t.error" class="terr">✗ {{ t.error }}</span><span v-if="t.dur!=null" class="tdur">{{ fmtDur(t.dur) }}</span></div>
97
+ <pre v-if="t.input" class="cmp-pre">{{ t._inPretty || prettyJson(t.input) }}</pre>
98
+ <pre v-if="t.output" class="cmp-pre">{{ t._outPretty || prettyJson(t.output) }}</pre>
99
+ </div>
100
+ <span v-if="!detB || !detB.tools || !detB.tools.length" class="cmp-none">(没有工具调用)</span>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ <template #footer>
107
+ <span class="cmp-foot">两条轮对比的是「同一件事这次为什么慢」—— 先看耗时条,再比对模型 / token / 工具链。</span>
108
+ <el-button @click="$emit('update:visible', false)">关闭</el-button>
109
+ </template>
110
+ </el-dialog>`,
111
+ props: {
112
+ visible: Boolean,
113
+ // 被对比的两条轮:{ a, b },条目本体(快照 / 搜索命中集中的对象)。
114
+ req: { type: Object, default: null },
115
+ },
116
+ emits: ['update:visible'],
117
+ setup(props) {
118
+ const getJSON = Vue.inject('getJSON');
119
+ const detA = Vue.ref(null), detB = Vue.ref(null); // /api/entry 的详情(含正文 / 工具)
120
+ const loading = Vue.ref(false), err = Vue.ref('');
121
+ let loadedKey = ''; // 'a.id + "_" + b.id':别拿旧条目的缓存去配新条目
122
+
123
+ async function load() {
124
+ const a = props.req && props.req.a, b = props.req && props.req.b;
125
+ if (!a || !b) { detA.value = detB.value = null; return; }
126
+ const key = a.id + '_' + b.id;
127
+ loading.value = true; err.value = '';
128
+ try {
129
+ const [ja, jb] = await Promise.all([
130
+ getJSON('/api/entry?id=' + encodeURIComponent(a.id)),
131
+ getJSON('/api/entry?id=' + encodeURIComponent(b.id)),
132
+ ]);
133
+ if (key !== loadedKey) return; // 打开期间换代了,丢弃旧响应
134
+ detA.value = prepDetail(ja); detB.value = prepDetail(jb);
135
+ } catch (e) {
136
+ if (key === loadedKey) err.value = '读取详情失败:' + e.message;
137
+ } finally { if (key === loadedKey) loading.value = false; }
138
+ }
139
+ // labels 复用 set 前后序:loadedKey 要跟当前对比对一致才复用 / 触发加载
140
+ Vue.watch(() => props.req, r => {
141
+ err.value = '';
142
+ const k = (r && r.a && r.b) ? r.a.id + '_' + r.b.id : '';
143
+ if (k === loadedKey) return; // 同两条:已有的详情直接可看,不重拉
144
+ loadedKey = k;
145
+ detA.value = detB.value = null;
146
+ if (k) load();
147
+ });
148
+
149
+ const reqA = Vue.computed(() => (props.req && props.req.a) || null);
150
+ const reqB = Vue.computed(() => (props.req && props.req.b) || null);
151
+ // 指标行:两侧同单位才作差、才标「谁大」;一侧没这个单位(qoder 无 token / 别家无积分)就给 '—'。
152
+ const cmpRows = Vue.computed(() => {
153
+ const a = reqA.value, b = reqB.value;
154
+ if (!a || !b) return [];
155
+ const DASH = '—';
156
+ const qa = isQoder(a), qb = isQoder(b);
157
+ const nOf = v => (typeof v === 'number' && isFinite(v) ? v : null);
158
+ const rows = [];
159
+ // ka/kb 是两格各自要挂的 title:'—' 有两种意思(这条根本没数据 / 这一行对它压根不适用),
160
+ // 不分开写清就会让人以为「另一条没花 token」也是缺数据。
161
+ const push = (label, ta, tb, na, nb, fmtDiff, ka, kb) => {
162
+ let hot = '', big = '';
163
+ if (na != null && nb != null && na !== nb) {
164
+ big = na > nb ? 'a' : 'b';
165
+ hot = '差 ' + (fmtDiff ? fmtDiff(Math.abs(na - nb)) : fmtN(Math.abs(na - nb)));
166
+ }
167
+ rows.push({ label, a: ta, b: tb, hot, big, at: ka || '', bt: kb || '' });
168
+ };
169
+ const NO_CREDIT = '这条不按积分计费(按 token),见下面 Token 行';
170
+ const NO_TOKEN = 'qoder 不落盘 token(恒 0),它的花费在「积分」行';
171
+ const NO_CTX = '日志没给这轮的占用与窗口容量';
172
+ push('模型', modelText(a.models) || '(无模型信息)', modelText(b.models) || '(无模型信息)');
173
+ push('LLM 调用', a.calls != null ? String(a.calls) : DASH, b.calls != null ? String(b.calls) : DASH, nOf(a.calls), nOf(b.calls));
174
+ push('工具', a.tools != null ? String(a.tools) : DASH, b.tools != null ? String(b.tools) : DASH, nOf(a.tools), nOf(b.tools));
175
+ // 积分:只要有一条是 qoder 就出这一行,非 qoder 那侧给 '—'(单位不同,不作差)
176
+ if (qa || qb) {
177
+ const cr = e => (isQoder(e) && e.credits != null ? fmtCredits(e.credits) : DASH);
178
+ const crn = e => (isQoder(e) && e.credits != null ? nOf(e.credits) : null);
179
+ push('积分', cr(a), cr(b), crn(a), crn(b), v => v.toFixed(2),
180
+ isQoder(a) ? '' : NO_CREDIT, isQoder(b) ? '' : NO_CREDIT);
181
+ }
182
+ // Token 四行:qoder 那侧一律 '—' —— 它的 tin/tout/total 恒 0,印 0 会被读成「这轮没花钱」
183
+ if (!qa || !qb) {
184
+ const tk = (label, f) => push(label,
185
+ isQoder(a) ? DASH : fmtN(f(a)), isQoder(b) ? DASH : fmtN(f(b)),
186
+ isQoder(a) ? null : nOf(f(a)), isQoder(b) ? null : nOf(f(b)), null,
187
+ isQoder(a) ? NO_TOKEN : '', isQoder(b) ? NO_TOKEN : '');
188
+ tk('Token 输入', e => e.tin);
189
+ tk('Token 输出', e => e.tout);
190
+ tk('缓存命中', e => e.tcache);
191
+ tk('Token 合计', sumTok);
192
+ }
193
+ // 上下文占用:qoder 只有占比(日志不落窗口容量),别家给「占用 / 容量(百分比)」;
194
+ // 作差用的是同一把尺(0–1 占用率),所以两侧单位不同也能比。
195
+ const ctxText = e => isQoder(e) ? (e.ctxRatio > 0 ? fmtPct(e.ctxRatio) : DASH)
196
+ : (e.ctxUsed != null || e.ctx)
197
+ ? fmtN(ctxUsed(e)) + (e.ctx ? ' / ' + fmtN(e.ctx) + '(' + ctxPct(e) + '%)' : '')
198
+ : DASH;
199
+ const ctxNum = e => isQoder(e) ? (e.ctxRatio > 0 ? Math.min(1, e.ctxRatio > 1 ? e.ctxRatio / 100 : e.ctxRatio) : null)
200
+ : (e.ctx ? ctxPct(e) / 100 : null);
201
+ push('上下文占用', ctxText(a), ctxText(b), ctxNum(a), ctxNum(b), v => Math.round(v * 100) + '%',
202
+ ctxNum(a) == null ? NO_CTX : '', ctxNum(b) == null ? NO_CTX : '');
203
+ if (a.compacts || b.compacts) {
204
+ // I12:压缩对比也带上「自动/手动」「丢多少」「等多久」——两侧各自缺哪一位就不说哪一位。
205
+ const cp = e => {
206
+ if (!e.compacts) return DASH;
207
+ let s = e.compacts + ' 次';
208
+ if (e.compAuto || e.compManual) s += '(自动' + (e.compAuto || 0) + ' / 手动' + (e.compManual || 0) + ')';
209
+ if (e.compPre) s += '(' + fmtN(e.compPre) + '→' + fmtN(e.compPost) + ')';
210
+ if (e.compDrop) s += ' 丢 ' + fmtN(e.compDrop);
211
+ if (e.compMs) s += ' 等 ' + fmtDur(e.compMs);
212
+ return s;
213
+ };
214
+ push('压缩', cp(a), cp(b));
215
+ }
216
+ return rows;
217
+ });
218
+ // 耗时条:以两条里较大者为满刻度,谁慢谁就顶满
219
+ const durMax = Vue.computed(() => Math.max((reqA.value && reqA.value.dur) || 0, (reqB.value && reqB.value.dur) || 0));
220
+ const durPctOf = e => durMax.value ? Math.max(2, Math.round(((e && e.dur) || 0) / durMax.value * 100)) : 0;
221
+ const durDiffOn = Vue.computed(() => durMax.value > 0 && reqA.value && reqB.value && reqA.value.dur !== reqB.value.dur);
222
+ const cmpDurText = Vue.computed(() => {
223
+ if (!durDiffOn.value) return '';
224
+ const a = (reqA.value && reqA.value.dur) || 0, b = (reqB.value && reqB.value.dur) || 0;
225
+ const abs = Math.abs(a - b);
226
+ const fast = a < b ? 'A 快' : 'B 快';
227
+ return fast + ' ' + fmtDur(abs);
228
+ });
229
+ const titleOf = e => {
230
+ const mt = modelText(e && e.models);
231
+ if (mt) return mt;
232
+ if (e && e.name) return e.name;
233
+ return '(无模型信息)';
234
+ };
235
+ return { reqA, reqB, detA, detB, loading, err, load, cmpRows,
236
+ durPctOf, durDiffOn, cmpDurText, titleOf, fmtDur, fmtTime, prettyJson };
237
+ }
238
+ };
@@ -0,0 +1,101 @@
1
+ // 上下文窗口弹层(R25 批 1 第三个组件)。
2
+ // 契约见 page/shared.js 头部;顶层只许有那一处 AACTA 赋值,Vue / Element Plus 的 API 一律写全名。
3
+ //
4
+ // 数据进出 —— 这个弹层与别处不同:**表数据不属于它**。
5
+ // 改前 ctxw.rows/unknown 就是根里的一个 reactive,侧栏那枚「待填 N」徽章(ctxUnfilled)吃的是
6
+ // 同一份 unknown.length。搬的时候如果把表整个端进组件,根就得在徽章和服务之间来回同步两份状态。
7
+ // 所以定成:**弹层自己拉、自己改(编辑态是它的),拉回来的结论上报给根**(emit('unfilled', n)),
8
+ // 根只留一个数字给徽章。保存成功后同样上报 —— 服务端重算了卡片,徽章要跟着变。
9
+ // · visible ← 根(侧栏「上下文窗口」项点开、弹层自己关闭时回写)
10
+ // · getJSON ← 根 provide(404 要点亮版本红条)
11
+ window.AACTA = window.AACTA || {};
12
+ AACTA.DialogCtxw = {
13
+ name: 'aa-dialog-ctxw',
14
+ template: `
15
+ <el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="上下文窗口" width="640px" append-to-body>
16
+ <div class="diag-hint">
17
+ 卡片上「上下文」那根进度条需要两个数:<b>分子</b>是这一轮的占用(从日志里的 usage 算,已经有了),
18
+ <b>分母</b>是模型窗口容量。分母**只有 claude 和 kimi 拿不到** —— 其余 agent 自带这个字段;
19
+ claude 的请求体不落盘、kimi 的 wire.jsonl 只记用量,都只能按模型名查这张表。
20
+ 查不到的模型就只显示占用、不画条:宁可少一根条,也不拿一个假分母凑出一根看着像真的。
21
+ <div style="margin-top:6px">表里的名字是**日志里的模型名**(网关转发后的名字,如 deepseek-flash / kimi-k3)。可以写全名,也可以写前缀(按最长命中)。填 <b>0</b> 表示停用这一条。</div>
22
+ </div>
23
+ <el-skeleton v-if="ctxw.loading" :rows="4" animated></el-skeleton>
24
+ <template v-else>
25
+ <div class="ctxw-t">已配置</div>
26
+ <div v-for="(r, i) in ctxw.rows" :key="i" class="ctxw-row" :class="{off: !(r.value > 0)}">
27
+ <el-input v-model="r.name" size="small" placeholder="模型名或前缀" class="ctxw-name"></el-input>
28
+ <el-input-number v-model="r.value" size="small" :min="0" :step="1024" :controls="false" class="ctxw-val"></el-input-number>
29
+ <span class="ctxw-src">{{ r.builtin ? '内置' : '自定义' }}{{ !(r.value > 0) ? ' · 停用' : '' }}</span>
30
+ <el-icon class="del-btn" title="删掉这一行(保存后生效)" @click="ctxw.rows.splice(i, 1)"><Close></Close></el-icon>
31
+ </div>
32
+ <el-button size="small" style="margin-top:6px" @click="ctxw.rows.push({ name: '', value: 200000, builtin: false })">加一行</el-button>
33
+
34
+ <template v-if="ctxw.unknown.length">
35
+ <div class="ctxw-t" style="margin-top:14px">日志里出现过、表里没有的模型(点一下加进上表)</div>
36
+ <div class="ctxw-chips">
37
+ <span v-for="u in ctxw.unknown" :key="u.model" class="ctxw-chip" @click="addCtxRow(u.model)">
38
+ {{ u.model }} <small>{{ u.n }} 轮</small>
39
+ </span>
40
+ </div>
41
+ </template>
42
+ <div v-else class="ctxw-t" style="margin-top:14px;font-weight:400;color:var(--placeholder)">
43
+ 日志里的模型都有窗口值了。
44
+ </div>
45
+ </template>
46
+ <template #footer>
47
+ <el-button @click="$emit('update:visible', false)">关闭</el-button>
48
+ <el-button type="primary" :loading="ctxw.saving" @click="saveCtxWindows">保存</el-button>
49
+ </template>
50
+ </el-dialog>`,
51
+ props: {
52
+ visible: Boolean,
53
+ },
54
+ emits: ['update:visible', 'unfilled'],
55
+ setup(props, { emit }) {
56
+ const getJSON = Vue.inject('getJSON');
57
+ // visible 不在这个对象里:开关归根(与别的弹层一致),放这儿会有两份真相
58
+ const ctxw = Vue.reactive({ loading: false, saving: false, rows: [], unknown: [] });
59
+ // 服务端每次都整表回吐(含刚刚保存后的重算结果),落地与上报只有这一处
60
+ function apply(j) {
61
+ ctxw.rows = (j.windows || []).map(r => ({ name: r.name, value: r.value, builtin: !!r.builtin }));
62
+ ctxw.unknown = j.unknown || [];
63
+ emit('unfilled', ctxw.unknown.length); // 侧栏徽章在根上,只有它需要知道这个数
64
+ }
65
+ async function load() {
66
+ ctxw.loading = true;
67
+ try {
68
+ const j = await getJSON('/api/ctxwindows');
69
+ if (!j.ok) throw new Error(j.error || '读取失败');
70
+ apply(j);
71
+ } catch (e) {
72
+ ElementPlus.ElMessage.error('读取上下文窗口表失败:' + e.message);
73
+ } finally { ctxw.loading = false; }
74
+ }
75
+ function addCtxRow(model) {
76
+ if (ctxw.rows.some(r => r.name === model)) { ElementPlus.ElMessage.info('这个模型已经在表里了'); return; }
77
+ ctxw.rows.push({ name: model, value: 200000, builtin: false });
78
+ ctxw.unknown = ctxw.unknown.filter(u => u.model !== model);
79
+ }
80
+ async function saveCtxWindows() {
81
+ ctxw.saving = true;
82
+ try {
83
+ // 整表提交:空名字的行丢掉,0/空 = 停用(照常提交,否则内置那条默认删不掉)
84
+ const windows = ctxw.rows
85
+ .map(r => ({ name: String(r.name || '').trim(), value: Number(r.value) > 0 ? Math.round(Number(r.value)) : 0 }))
86
+ .filter(r => r.name);
87
+ const j = await getJSON('/api/ctxwindows', {
88
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({ windows }),
90
+ });
91
+ if (!j.ok) throw new Error(j.error || '保存失败');
92
+ apply(j);
93
+ ElementPlus.ElMessage.success(j.changed ? '已保存,重算了 ' + j.changed + ' 张卡片' : '已保存');
94
+ } catch (e) {
95
+ ElementPlus.ElMessage.error('保存失败:' + e.message);
96
+ } finally { ctxw.saving = false; }
97
+ }
98
+ Vue.watch(() => props.visible, v => { if (v) load(); });
99
+ return { ctxw, addCtxRow, saveCtxWindows };
100
+ }
101
+ };
@@ -0,0 +1,72 @@
1
+ // 环境诊断弹层(R25 批 1 从 agent-acta-page.html 搬出的第一个组件)。
2
+ // 契约见 page/shared.js 头部的「R25 组件文件契约」;本文件顶层只许有那一处 AACTA 赋值 ——
3
+ // Vue 的 API 一律写全名(Vue.reactive / Vue.watch / Vue.inject),**不要解构**:
4
+ // 顶层 const 与根内联 script 共享同一个全局词法作用域,重名就是整页 SyntaxError。
5
+ //
6
+ // 数据进出(搬的时候逐个对过):
7
+ // · visible ← 根(侧栏「环境诊断」项点开、弹层关掉时回写)—— 开关状态留根,与改前一样
8
+ // · pageBuild ← 根(底部那行「服务自报指纹 ≠ 页面注入指纹」的比对基准)
9
+ // · getJSON ← 根 provide:接口 404 要点亮顶部版本红条,那段逻辑只有根里一份,别在这儿重写
10
+ // · 自己拉 /api/diagnose(改前就是独立 fetch,不依赖根的任何状态)
11
+ window.AACTA = window.AACTA || {};
12
+ AACTA.DialogDiag = {
13
+ name: 'aa-dialog-diag',
14
+ template: `
15
+ <el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="环境诊断" width="720px" append-to-body>
16
+ <div class="diag-hint">
17
+ 下面逐个列出候选 agent 的探测结果:<b>已接入</b>是正常在扫;<b>目录在但未识别</b>说明这个 agent
18
+ 的日志目录就在本机、但格式还没写过解析器(顺着给出的路径补一个即可);
19
+ <b>已由 xxx 接入</b>是同一份目录被另一个 agent 接走了(避免重复扫描,不是故障)。
20
+ </div>
21
+ <el-skeleton v-if="diag.loading" :rows="5" animated></el-skeleton>
22
+ <el-alert v-else-if="diag.error" :title="diag.error" type="error" :closable="false" show-icon></el-alert>
23
+ <div v-else>
24
+ <div v-for="a in diag.rows" :key="a.name" class="diag-row">
25
+ <span class="dn">{{ a.name }}</span>
26
+ <span class="ds" :class="a.state==='ok' ? 'ok' : (a.state==='absent' ? 'off' : 'warn')">{{ a.stateText }}</span>
27
+ <div class="db">
28
+ <div class="dnote">{{ a.note }}</div>
29
+ <div v-for="c in a.candidates" :key="c.path">
30
+ <div class="dpath">{{ c.exists ? '✓' : '·' }} {{ c.path }}<template v-if="c.reason"> —— {{ c.reason }}</template></div>
31
+ <div v-if="c.hint" class="dhint"> {{ c.hint }}</div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="diag-hint" style="margin-top:10px">
36
+ 平台:{{ diag.platform || '?' }} | 数据目录:{{ diag.dataDir }} | 端口:{{ diag.port }} | 服务版本:{{ diag.version || '?' }}(指纹 {{ diag.build ? diag.build.slice(0, 12) : '无' }})
37
+ <!-- 指纹是「正在跑的服务」自报的代码指纹,与页面顶部注入的那份比对不上就是新旧错配。
38
+ 「我装的是新版、怎么没有新功能」这类问题,第一步就看这一行。 -->
39
+ <span v-if="diag.build && diag.build !== pageBuild" style="color:var(--red)"> ⚠ 与页面不一致,请重启服务</span>
40
+ </div>
41
+ </div>
42
+ <template #footer>
43
+ <el-button @click="$emit('update:visible', false)">关闭</el-button>
44
+ <el-button type="primary" :loading="diag.loading" @click="run">重新探测</el-button>
45
+ </template>
46
+ </el-dialog>`,
47
+ props: {
48
+ visible: Boolean,
49
+ pageBuild: { type: String, default: '' },
50
+ },
51
+ emits: ['update:visible'],
52
+ setup(props) {
53
+ const getJSON = Vue.inject('getJSON');
54
+ // 改前这个对象还带 visible 字段(弹层开关),现在开关归根 —— 放这儿会有两份真相
55
+ const diag = Vue.reactive({ loading: false, error: '', rows: [], platform: '', dataDir: '', port: '', version: '', build: '' });
56
+ async function run() {
57
+ diag.loading = true; diag.error = '';
58
+ try {
59
+ const j = await getJSON('/api/diagnose');
60
+ if (!j.ok) throw new Error(j.error || '诊断失败');
61
+ diag.rows = j.agents || [];
62
+ diag.platform = j.platform || ''; // 截图发回来时能自证是哪台机器(四平台验证靠它)
63
+ diag.dataDir = j.dataDir || ''; diag.port = j.port || '';
64
+ diag.version = j.version || ''; diag.build = j.build || '';
65
+ } catch (e) { diag.error = '诊断失败:' + e.message; }
66
+ finally { diag.loading = false; }
67
+ }
68
+ // 侧栏点开就探一次(改前是 openDiagnose 顺手把 visible 置 true;现在根只置 visible,探测挂在这儿)
69
+ Vue.watch(() => props.visible, v => { if (v) run(); });
70
+ return { diag, run };
71
+ }
72
+ };
@@ -0,0 +1,215 @@
1
+ // R27 活跃热力图弹层:全年活跃热力(GitHub contributions 风格)+ 当月月历热力。
2
+ // 契约见 page/shared.js 头部「R25 组件文件契约」;Vue / Element Plus 的 API 写全名。
3
+ //
4
+ // 数据进出:
5
+ // · visible ← 根(侧栏「活跃热力」项 / 弹层自己关闭时回写)
6
+ // · getJSON ← 根 provide
7
+ // · 天桶 ← /api/daily?range=all(不带任何筛选)——口径是需求书 R27 定死的:
8
+ // **任一 agent 当天有日志记录即算活跃**,颜色深浅按当天记录条数分五档。
9
+ // 所以这张图**刻意不跟随**左侧栏的 agent/项目/时间范围筛选:它回答的是「这一年我出没出现过」,
10
+ // 跟「用量统计」那种「筛出来的这一段烧了多少 token」是两个问题,别接 req。
11
+ // · 聚合全在前端做(天桶只有 day+count 两个字段要,服务端不新开端点)。
12
+ //
13
+ // 分档:取当年全部活跃日的条数,按**去重后的值**均匀映射到 1..5 档(不是按最大值线性切——
14
+ // 一个爆量的日子会把其余全压成最浅档)。档位在全年与月历之间共用,两块的深浅才可比。
15
+ // 周连登:从当周(周日为首,与月历同口径)往前回溯连续活跃周数,需求书 R27 定义。
16
+ //
17
+ // 悬浮提示(2026-09-22 按用户补发的 Qoder 新任务面板截图对齐「内容系统」):
18
+ // 第一行「M月D日周X」+ 右对齐「N 个任务」,第二行灰字「单日活跃任务」。
19
+ // 原生 title 排不出这个两行版式,所以是一块全局单例的自绘提示(事件委托在两块网格上,
20
+ // 不给 400 个格子各挂监听)。无记录/未到/非本月三种日子给各自的第二行文案,不冒充「0 个任务」。
21
+ window.AACTA = window.AACTA || {};
22
+ AACTA.DialogHeatmap = {
23
+ name: 'aa-dialog-heatmap',
24
+ template: `
25
+ <el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="活跃热力图" width="min(1080px, 94vw)" append-to-body class="hm-dialog">
26
+ <el-skeleton v-if="hm.loading && !hm.days.length" :rows="8" animated></el-skeleton>
27
+ <el-alert v-else-if="hm.error" :title="hm.error" type="error" :closable="false" show-icon></el-alert>
28
+ <div v-else-if="!hm.days.length" class="empty">暂无日志数据</div>
29
+ <template v-else>
30
+ <div class="hm-sec">
31
+ <div class="hm-head">
32
+ <span class="hm-h1">全年活跃记录</span>
33
+ <span class="hm-stat"><b>{{ hmActive }}</b> 天活跃 · <b class="rate">{{ hmRate }}%</b> 活跃率 · <b>{{ hmStreak }}</b> 周连登</span>
34
+ <span class="hm-legend">未活跃<i v-for="lv in hmLvs" :key="lv" class="hm-chip" :class="'lv' + lv"></i>活跃</span>
35
+ </div>
36
+ <div class="hm-year" @mouseover="onHover" @mouseleave="hideTip">
37
+ <div v-for="mo in yearMonths" :key="mo.label" class="hm-month">
38
+ <div class="hm-m-label">{{ mo.label }}</div>
39
+ <div class="hm-grid">
40
+ <template v-for="c in mo.cells" :key="c.k">
41
+ <i v-if="c.pad" class="hm-cell hm-pad"></i>
42
+ <i v-else class="hm-cell" :class="['lv' + c.lv, {fut: c.future}]" :data-d="c.k" :data-n="c.n" :data-f="c.future ? '1' : ''"></i>
43
+ </template>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <div class="hm-foot">{{ hmFoot }}</div>
48
+ </div>
49
+ <div class="hm-sec">
50
+ <div class="hm-head">
51
+ <span class="hm-h1">{{ monthTitle }}</span>
52
+ <span class="hm-stat"><b>{{ monthActive }}</b> 天活跃 · 色阶与上方全年热力同一档位</span>
53
+ </div>
54
+ <div class="hm-cal" @mouseover="onHover" @mouseleave="hideTip">
55
+ <span v-for="w in weekLabels" :key="w" class="hm-wd">{{ w }}</span>
56
+ <div v-for="c in monthCal" :key="c.k" class="hm-day" :class="['lv' + c.lv, {out: c.out, today: c.today}]" :data-d="c.k" :data-n="c.n" :data-out="c.out ? '1' : ''">{{ c.today ? '今日' : c.d }}</div>
57
+ </div>
58
+ </div>
59
+ <!-- 全局单例悬浮提示:position:fixed 按格子 rect 落位(见文件头注释) -->
60
+ <div v-if="tip.show" class="hm-tip" :class="{below: tip.below}" :style="{left:tip.x+'px', top:tip.y+'px'}">
61
+ <div class="hm-tip-1"><b>{{ tip.date }}</b><span v-if="!tip.fut && !tip.out">{{ tip.n }} 个任务</span></div>
62
+ <div class="hm-tip-2">{{ tip.sub }}</div>
63
+ </div>
64
+ </template>
65
+ <template #footer>
66
+ <el-button @click="$emit('update:visible', false)">关闭</el-button>
67
+ <el-button type="primary" :loading="hm.loading" @click="load">刷新</el-button>
68
+ </template>
69
+ </el-dialog>`,
70
+ props: { visible: Boolean },
71
+ emits: ['update:visible'],
72
+ setup(props) {
73
+ const getJSON = Vue.inject('getJSON');
74
+ const hm = Vue.reactive({ loading: false, error: '', days: [] });
75
+ // 打开时重新取数、也重新认「今天」(挂着的服务跨了午夜再开弹层,今日格不能留在昨天)
76
+ const now = Vue.ref(new Date());
77
+
78
+ async function load() {
79
+ if (hm.loading) return;
80
+ hm.loading = true;
81
+ hm.error = '';
82
+ now.value = new Date();
83
+ try {
84
+ const j = await getJSON('/api/daily?range=all');
85
+ if (!j.ok) throw new Error(j.error || '聚合失败');
86
+ hm.days = j.days || [];
87
+ } catch (e) { hm.error = '活跃热力图加载失败:' + e.message; }
88
+ finally { hm.loading = false; }
89
+ }
90
+ Vue.watch(() => props.visible, v => { if (v) load(); });
91
+
92
+ const pad2 = n => (n < 10 ? '0' : '') + n;
93
+ const dk = d => d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
94
+ const todayKey = Vue.computed(() => dk(now.value));
95
+ const year = Vue.computed(() => now.value.getFullYear());
96
+ // 只收有记录的天:热力图只问「有没有、多少条」,token/耗时一概不用
97
+ const cnt = Vue.computed(() => {
98
+ const m = new Map();
99
+ for (const d of hm.days) if (d.count) m.set(d.day, d.count);
100
+ return m;
101
+ });
102
+ // 五档:当年活跃日条数去重后均匀铺到 1..5(见文件头注释)。只有一个档位时给中间档,
103
+ // 不至于「全年就一天有记录」反而画成最深绿。
104
+ const levelMap = Vue.computed(() => {
105
+ const ys = String(year.value);
106
+ const uniq = [...new Set([...cnt.value].filter(([k]) => k.slice(0, 4) === ys).map(([, n]) => n))].sort((a, b) => a - b);
107
+ const m = new Map();
108
+ uniq.forEach((v, i) => m.set(v, uniq.length === 1 ? 3 : 1 + Math.round(i / (uniq.length - 1) * 4)));
109
+ return m;
110
+ });
111
+ const lvOf = (n) => !n ? 0 : (levelMap.value.get(n) || 1);
112
+
113
+ // ---- 悬浮提示(内容系统对齐 Qoder 新任务面板,见文件头)----
114
+ const WD = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
115
+ const tip = Vue.reactive({ show: false, below: false, x: 0, y: 0, date: '', n: 0, fut: false, out: false, sub: '' });
116
+ // 事件委托:监听挂在两块网格容器上,格子只带 data-*
117
+ function onHover(ev) {
118
+ const el = ev.target && ev.target.closest ? ev.target.closest('.hm-cell, .hm-day') : null;
119
+ const d = el && el.dataset.d;
120
+ if (!d) { tip.show = false; return; }
121
+ const n = +el.dataset.n || 0;
122
+ const dt = new Date(+d.slice(0, 4), +d.slice(5, 7) - 1, +d.slice(8, 10));
123
+ tip.date = (dt.getMonth() + 1) + '月' + dt.getDate() + '日' + WD[dt.getDay()];
124
+ tip.n = n;
125
+ tip.fut = el.dataset.f === '1';
126
+ tip.out = el.dataset.out === '1';
127
+ tip.sub = tip.fut ? '未到' : tip.out ? '非本月' : n ? '单日活跃任务' : '无活跃记录';
128
+ const r = el.getBoundingClientRect();
129
+ tip.below = r.top < 76; // 第一行格子的提示放不下头顶,翻到下方
130
+ tip.x = Math.round(r.left + r.width / 2);
131
+ tip.y = Math.round(tip.below ? r.bottom + 8 : r.top - 8);
132
+ tip.show = true;
133
+ }
134
+ function hideTip() { tip.show = false; }
135
+
136
+ const yearMonths = Vue.computed(() => {
137
+ const Y = year.value, tk = todayKey.value, out = [];
138
+ for (let m = 0; m < 12; m++) {
139
+ const off = new Date(Y, m, 1).getDay(); // 周日为首(与月历同一口径)
140
+ const dim = new Date(Y, m + 1, 0).getDate();
141
+ const cells = [];
142
+ for (let i = 0; i < off; i++) cells.push({ pad: true, k: Y + '-' + m + '-p' + i });
143
+ for (let d = 1; d <= dim; d++) {
144
+ const key = Y + '-' + pad2(m + 1) + '-' + pad2(d);
145
+ const n = cnt.value.get(key) || 0;
146
+ cells.push({ pad: false, k: key, n, lv: lvOf(n), future: key > tk });
147
+ }
148
+ out.push({ label: (m + 1) + '月', cells });
149
+ }
150
+ return out;
151
+ });
152
+ // 活跃率 = 活跃天数 / 当年已过天数(需求书 R27 口径;1 月 1 日当天算已过 1 天)
153
+ const hmActive = Vue.computed(() => {
154
+ const ys = String(year.value);
155
+ let n = 0;
156
+ for (const [k] of cnt.value) if (k.slice(0, 4) === ys) n++;
157
+ return n;
158
+ });
159
+ const hmRate = Vue.computed(() => {
160
+ const elapsed = Math.floor((now.value - new Date(year.value, 0, 1)) / 86400000) + 1;
161
+ return elapsed > 0 ? Math.round(hmActive.value / elapsed * 100) : 0;
162
+ });
163
+ const hmStreak = Vue.computed(() => {
164
+ const cur = new Date(now.value);
165
+ cur.setHours(0, 0, 0, 0);
166
+ cur.setDate(cur.getDate() - cur.getDay()); // 本周日
167
+ let s = 0;
168
+ for (let g = 0; g < 110; g++) { // 上限只是防病态回溯(数据最老也就 400 天)
169
+ let hit = false;
170
+ for (let i = 0; i < 7 && !hit; i++) {
171
+ const d = new Date(cur);
172
+ d.setDate(d.getDate() + i);
173
+ if (cnt.value.has(dk(d))) hit = true;
174
+ }
175
+ if (!hit) break;
176
+ s++;
177
+ cur.setDate(cur.getDate() - 7);
178
+ }
179
+ return s;
180
+ });
181
+
182
+ const monthTitle = Vue.computed(() => year.value + '年' + (now.value.getMonth() + 1) + '月');
183
+ const monthActive = Vue.computed(() => {
184
+ const p = year.value + '-' + pad2(now.value.getMonth() + 1);
185
+ let n = 0;
186
+ for (const [k] of cnt.value) if (k.slice(0, 7) === p) n++;
187
+ return n;
188
+ });
189
+ const monthCal = Vue.computed(() => {
190
+ const Y = year.value, M = now.value.getMonth(), tk = todayKey.value;
191
+ const off = new Date(Y, M, 1).getDay();
192
+ const dim = new Date(Y, M + 1, 0).getDate();
193
+ const cells = [];
194
+ for (let i = 0; i < Math.ceil((off + dim) / 7) * 7; i++) {
195
+ const dt = new Date(Y, M, 1 - off + i);
196
+ const key = dk(dt);
197
+ const inM = dt.getMonth() === M;
198
+ const n = inM ? (cnt.value.get(key) || 0) : 0;
199
+ cells.push({
200
+ k: key, d: dt.getDate(), n, out: !inM, today: key === tk, lv: lvOf(n),
201
+ });
202
+ }
203
+ return cells;
204
+ });
205
+
206
+ const hmLvs = [0, 1, 2, 3, 4, 5];
207
+ const weekLabels = ['日', '一', '二', '三', '四', '五', '六'];
208
+ // 参考截图底部那行写的是「数据更新于每日 02:00」——本服务没有每日定点任务,
209
+ // 数据随扫描实时进桶,照抄那句就是假话,改成如实的口径说明。
210
+ const hmFoot = '活跃口径:任一 agent 当天有日志记录即算活跃 · 色阶 = 当天条数按全年活跃日分五档 · 数据随日志扫描实时更新';
211
+
212
+ // 模板只认 props ∪ setup return(page-check.mjs 盯住),用到的名字必须全列出来
213
+ return { hm, load, yearMonths, monthCal, hmLvs, weekLabels, hmActive, hmRate, hmStreak, monthTitle, monthActive, hmFoot, tip, onHover, hideTip };
214
+ }
215
+ };