agent-loft 0.2.32 → 0.2.34
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/README.md +9 -3
- package/package.json +1 -1
- package/src/agents/claude/driver.ts +80 -2
- package/src/agents/claude/session.ts +51 -7
- package/src/agents/codex/session.ts +43 -7
- package/src/channels/feishu/bot.ts +2 -2
- package/src/channels/web/api.ts +84 -2
- package/src/channels/web/conv-pages.ts +68 -0
- package/src/channels/web/server.ts +13 -4
- package/src/core/hub.ts +126 -19
- package/src/core/model-prices.json +103 -0
- package/src/core/pricing.ts +93 -0
- package/src/core/skill-runtime.ts +1 -1
- package/src/core/skills.ts +2 -1
- package/src/core/types.ts +24 -2
- package/src/core/usage.ts +144 -0
- package/web/css/composer.css +0 -25
- package/web/css/messages.css +169 -0
- package/web/css/settings.css +194 -0
- package/web/css/sidebar.css +5 -5
- package/web/css/skills.css +18 -1
- package/web/css/topbar.css +131 -0
- package/web/lib/dom.js +2 -1
- package/web/lib/markdown.js +84 -14
- package/web/lib/ws.js +46 -6
- package/web/settings/index.js +11 -4
- package/web/{views → settings}/skills.js +86 -172
- package/web/settings/usage.js +272 -0
- package/web/state.js +5 -4
- package/web/views/composer.js +2 -1
- package/web/views/interaction.js +12 -4
- package/web/views/layout.js +22 -25
- package/web/views/mention.js +2 -2
- package/web/views/messages.js +239 -81
- package/web/views/msg-nav.js +120 -0
- package/web/views/quota.js +1 -0
- package/web/views/sidebar.js +114 -17
- package/web/views/topbar.js +169 -41
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// 每个 agent 会话累计消耗的 token(页面右上角「更多」里显示分布)。
|
|
2
|
+
//
|
|
3
|
+
// Claude 每轮结束报一次这个会话的累计值(恢复 / 分叉来的会话也接着原来的总数算);Codex 每次模型请求报一次增量。
|
|
4
|
+
// 都按会话 id 记在这里:页面重新加载、服务重启后还在。
|
|
5
|
+
// 按 agent 会话算,不按对话:/new、恢复旧会话、切 agent 之后显示的都是现在这个会话的。
|
|
6
|
+
// Codex 升级前跑过的轮次没有记录,从升级后开始算。
|
|
7
|
+
//
|
|
8
|
+
// 另外按天记一本账(usage-daily.json,设置 →「用量」按日 / 月 / 年统计):每次报上来的增量记到当天、
|
|
9
|
+
// 按「谁的对话 + 哪个 agent + 什么模型」分开。只从有这本账开始算,之前的用量没有日期,不补。
|
|
10
|
+
// 没报费用的(Codex)查的时候按模型单价估一个(见 pricing.ts)。
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { dataDir, readJson, writeJsonAtomic } from "../home.js";
|
|
13
|
+
import { estimateCost } from "./pricing.js";
|
|
14
|
+
import type { TokenUsage } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export type UsageTotals = Required<Omit<TokenUsage, "costUsd">> & { costUsd?: number; at: number };
|
|
17
|
+
type Store = Record<string, UsageTotals>;
|
|
18
|
+
|
|
19
|
+
/** 一天里某个人用某个 agent 的合计 */
|
|
20
|
+
export type DailyTotals = Required<Omit<TokenUsage, "costUsd">> & { costUsd?: number };
|
|
21
|
+
/** 按天的账:日期(服务器本地时间 YYYY-MM-DD)→「owner\tagent\tmodel」→ 合计。没有主人、不知道模型的是空串 */
|
|
22
|
+
type Ledger = Record<string, Record<string, DailyTotals>>;
|
|
23
|
+
/** 查询结果的一行。estimated:costUsd 是按单价估的,不是 agent 报的 */
|
|
24
|
+
export type DailyRow = DailyTotals & { day: string; owner?: string; agent: string; model?: string; estimated?: true };
|
|
25
|
+
|
|
26
|
+
const FILE = () => path.join(dataDir, "usage.json");
|
|
27
|
+
const DAILY_FILE = () => path.join(dataDir, "usage-daily.json");
|
|
28
|
+
const MAX_SESSIONS = 1000;
|
|
29
|
+
/** 按天的账留多久(天) */
|
|
30
|
+
const MAX_DAYS = 3 * 366;
|
|
31
|
+
|
|
32
|
+
let store: Store | null = null;
|
|
33
|
+
let ledger: Ledger | null = null;
|
|
34
|
+
let timer: NodeJS.Timeout | null = null;
|
|
35
|
+
|
|
36
|
+
function load(): Store {
|
|
37
|
+
return (store ??= readJson<Store>(FILE(), {}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadLedger(): Ledger {
|
|
41
|
+
return (ledger ??= readJson<Ledger>(DAILY_FILE(), {}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function write(): void {
|
|
45
|
+
const s = load();
|
|
46
|
+
// 只留最近用过的会话
|
|
47
|
+
const keys = Object.keys(s).sort((a, b) => s[b]!.at - s[a]!.at);
|
|
48
|
+
for (const k of keys.slice(MAX_SESSIONS)) delete s[k];
|
|
49
|
+
writeJsonAtomic(FILE(), s);
|
|
50
|
+
const l = loadLedger();
|
|
51
|
+
const days = Object.keys(l).sort();
|
|
52
|
+
for (const d of days.slice(0, Math.max(0, days.length - MAX_DAYS))) delete l[d];
|
|
53
|
+
writeJsonAtomic(DAILY_FILE(), l);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 本地时间的 YYYY-MM-DD */
|
|
57
|
+
export function dayOf(ms: number): string {
|
|
58
|
+
const d = new Date(ms);
|
|
59
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function persistSoon(): void {
|
|
63
|
+
if (timer) return;
|
|
64
|
+
timer = setTimeout(() => {
|
|
65
|
+
timer = null;
|
|
66
|
+
write();
|
|
67
|
+
}, 1000);
|
|
68
|
+
timer.unref();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 退出前把还没写的落盘 */
|
|
72
|
+
export function flushUsage(): void {
|
|
73
|
+
if (!timer) return;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
timer = null;
|
|
76
|
+
write();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0);
|
|
80
|
+
|
|
81
|
+
const FIELDS = ["input", "cacheRead", "cacheWrite", "output", "reasoning"] as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* cumulative:报的是累计值,每项取较大的(累计值只增不减;恢复的会话万一没从原来的总数接着算,
|
|
85
|
+
* 宁可先少算,也不把之前的再加一遍)。否则是增量,加上去。
|
|
86
|
+
* by:这次用量算谁的、哪个 agent 的(记进按天的账;不给就只记会话的)
|
|
87
|
+
*/
|
|
88
|
+
export function recordUsage(
|
|
89
|
+
sessionId: string | undefined,
|
|
90
|
+
usage: TokenUsage,
|
|
91
|
+
cumulative = false,
|
|
92
|
+
by?: { owner?: string; agent: string; model?: string },
|
|
93
|
+
): void {
|
|
94
|
+
if (!sessionId) return;
|
|
95
|
+
const s = load();
|
|
96
|
+
const t = (s[sessionId] ??= { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, reasoning: 0, at: 0 });
|
|
97
|
+
const delta: DailyTotals = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, reasoning: 0 };
|
|
98
|
+
for (const f of FIELDS) {
|
|
99
|
+
const next = cumulative ? Math.max(t[f], num(usage[f])) : t[f] + num(usage[f]);
|
|
100
|
+
delta[f] = next - t[f];
|
|
101
|
+
t[f] = next;
|
|
102
|
+
}
|
|
103
|
+
const cost = num(usage.costUsd);
|
|
104
|
+
if (cost) {
|
|
105
|
+
const next = cumulative ? Math.max(t.costUsd ?? 0, cost) : (t.costUsd ?? 0) + cost;
|
|
106
|
+
if (next > (t.costUsd ?? 0)) delta.costUsd = next - (t.costUsd ?? 0);
|
|
107
|
+
t.costUsd = next;
|
|
108
|
+
}
|
|
109
|
+
t.at = Date.now();
|
|
110
|
+
if (by) addDaily(t.at, by, delta);
|
|
111
|
+
persistSoon();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function addDaily(at: number, by: { owner?: string; agent: string; model?: string }, delta: DailyTotals): void {
|
|
115
|
+
if (!FIELDS.some((f) => delta[f] > 0) && !delta.costUsd) return;
|
|
116
|
+
const day = (loadLedger()[dayOf(at)] ??= {});
|
|
117
|
+
const d = (day[`${by.owner ?? ""}\t${by.agent}\t${by.model ?? ""}`] ??= { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, reasoning: 0 });
|
|
118
|
+
for (const f of FIELDS) d[f] += delta[f];
|
|
119
|
+
if (delta.costUsd) d.costUsd = (d.costUsd ?? 0) + delta.costUsd;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 按天的账里 from ~ to(含两头,YYYY-MM-DD)的行;visible 按主人过滤 */
|
|
123
|
+
export function dailyUsage(from: string, to: string, visible?: (owner: string | undefined) => boolean): DailyRow[] {
|
|
124
|
+
const rows: DailyRow[] = [];
|
|
125
|
+
for (const [day, buckets] of Object.entries(loadLedger())) {
|
|
126
|
+
if (day < from || day > to) continue;
|
|
127
|
+
for (const [k, v] of Object.entries(buckets)) {
|
|
128
|
+
const [o = "", agent = "", model = ""] = k.split("\t");
|
|
129
|
+
const owner = o || undefined;
|
|
130
|
+
if (visible && !visible(owner)) continue;
|
|
131
|
+
const row: DailyRow = { day, ...(owner ? { owner } : {}), agent, ...(model ? { model } : {}), ...v };
|
|
132
|
+
if (!row.costUsd) {
|
|
133
|
+
const est = estimateCost(model, row);
|
|
134
|
+
if (est) Object.assign(row, { costUsd: est, estimated: true });
|
|
135
|
+
}
|
|
136
|
+
rows.push(row);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return rows.sort((a, b) => a.day.localeCompare(b.day));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function usageOf(sessionId: string | undefined): UsageTotals | undefined {
|
|
143
|
+
return sessionId ? load()[sessionId] : undefined;
|
|
144
|
+
}
|
package/web/css/composer.css
CHANGED
|
@@ -167,31 +167,6 @@ button.ctx-item:hover {
|
|
|
167
167
|
flex-shrink: 0;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
.mode-pill {
|
|
171
|
-
display: inline-flex;
|
|
172
|
-
align-items: center;
|
|
173
|
-
height: 32px;
|
|
174
|
-
padding: 0 10px;
|
|
175
|
-
border-radius: 999px;
|
|
176
|
-
font-size: 13px;
|
|
177
|
-
color: var(--fg-faint);
|
|
178
|
-
white-space: nowrap;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.mode-pill:hover {
|
|
182
|
-
background: var(--bg-hover);
|
|
183
|
-
color: var(--fg);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.mode-pill.chat {
|
|
187
|
-
color: var(--accent);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.topbar .mode-slot {
|
|
191
|
-
display: inline-flex;
|
|
192
|
-
flex-shrink: 0;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
170
|
.topbar .top-more svg {
|
|
196
171
|
width: 20px;
|
|
197
172
|
height: 20px;
|
package/web/css/messages.css
CHANGED
|
@@ -227,6 +227,26 @@ a.att-file:hover {
|
|
|
227
227
|
word-break: break-word;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/* 太长的用户消息:收起到约 10 行,底部淡出,下面一个「展开显示」 */
|
|
231
|
+
.msg-user.clamped {
|
|
232
|
+
max-height: 260px;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 56px), transparent);
|
|
235
|
+
mask-image: linear-gradient(to bottom, #000 calc(100% - 56px), transparent);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.msg-more {
|
|
239
|
+
margin-top: -2px;
|
|
240
|
+
padding: 2px 8px;
|
|
241
|
+
border-radius: 6px;
|
|
242
|
+
font-size: 13px;
|
|
243
|
+
color: var(--fg-faint);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.msg-more:hover {
|
|
247
|
+
color: var(--fg);
|
|
248
|
+
}
|
|
249
|
+
|
|
230
250
|
.msg-user .origin {
|
|
231
251
|
display: block;
|
|
232
252
|
font-size: 12px;
|
|
@@ -732,3 +752,152 @@ a.att-file:hover {
|
|
|
732
752
|
.lightbox-dl:hover {
|
|
733
753
|
background: rgba(255, 255, 255, 0.26);
|
|
734
754
|
}
|
|
755
|
+
|
|
756
|
+
/* ---------- 右边的快速定位:每条用户消息一道横线,鼠标移上去展开成列表 ---------- */
|
|
757
|
+
.msg-nav {
|
|
758
|
+
position: absolute;
|
|
759
|
+
right: 14px;
|
|
760
|
+
top: 45%;
|
|
761
|
+
transform: translateY(-50%);
|
|
762
|
+
z-index: 4;
|
|
763
|
+
display: flex;
|
|
764
|
+
align-items: center;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.msg-nav[hidden],
|
|
768
|
+
.main.tasks-view .msg-nav {
|
|
769
|
+
display: none;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.msg-nav-ticks {
|
|
773
|
+
display: flex;
|
|
774
|
+
flex-direction: column;
|
|
775
|
+
max-height: 50vh;
|
|
776
|
+
overflow-y: auto;
|
|
777
|
+
scrollbar-width: none;
|
|
778
|
+
opacity: 0.55;
|
|
779
|
+
transition: opacity 0.2s;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.msg-nav-ticks::-webkit-scrollbar {
|
|
783
|
+
display: none;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.msg-nav.scrolling .msg-nav-ticks,
|
|
787
|
+
.msg-nav:hover .msg-nav-ticks {
|
|
788
|
+
opacity: 1;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/* 点击范围比横线大:整个 20×10 都能点 */
|
|
792
|
+
.msg-nav-tick {
|
|
793
|
+
flex-shrink: 0;
|
|
794
|
+
width: 20px;
|
|
795
|
+
height: 10px;
|
|
796
|
+
padding: 0;
|
|
797
|
+
display: flex;
|
|
798
|
+
align-items: center;
|
|
799
|
+
justify-content: flex-end;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.msg-nav-tick i {
|
|
803
|
+
display: block;
|
|
804
|
+
width: 10px;
|
|
805
|
+
height: 2px;
|
|
806
|
+
border-radius: 1px;
|
|
807
|
+
background: var(--fg-faint);
|
|
808
|
+
transition: width 0.15s, background 0.15s;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.msg-nav-tick:hover i {
|
|
812
|
+
background: var(--fg-soft);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.msg-nav-tick.on i {
|
|
816
|
+
width: 16px;
|
|
817
|
+
background: var(--fg);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/* 展开的列表:在横线左边,鼠标移开就收起 */
|
|
821
|
+
.msg-nav-panel {
|
|
822
|
+
position: absolute;
|
|
823
|
+
right: calc(100% + 4px);
|
|
824
|
+
top: 50%;
|
|
825
|
+
transform: translateY(-50%);
|
|
826
|
+
width: 260px;
|
|
827
|
+
max-height: 60vh;
|
|
828
|
+
overflow-y: auto;
|
|
829
|
+
padding: 6px;
|
|
830
|
+
display: flex;
|
|
831
|
+
flex-direction: column;
|
|
832
|
+
gap: 1px;
|
|
833
|
+
background: var(--bg);
|
|
834
|
+
border: 1px solid var(--border);
|
|
835
|
+
border-radius: 12px;
|
|
836
|
+
box-shadow: var(--shadow);
|
|
837
|
+
opacity: 0;
|
|
838
|
+
visibility: hidden;
|
|
839
|
+
transition: opacity 0.12s, visibility 0.12s;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* 键盘 Tab 进来时也展开;鼠标点过之后不因为焦点还在就一直开着 */
|
|
843
|
+
.msg-nav:hover .msg-nav-panel,
|
|
844
|
+
.msg-nav:has(:focus-visible) .msg-nav-panel {
|
|
845
|
+
opacity: 1;
|
|
846
|
+
visibility: visible;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.msg-nav-row {
|
|
850
|
+
flex-shrink: 0;
|
|
851
|
+
text-align: left;
|
|
852
|
+
padding: 6px 10px;
|
|
853
|
+
border-radius: 8px;
|
|
854
|
+
font-size: 13px;
|
|
855
|
+
color: var(--fg-soft);
|
|
856
|
+
white-space: nowrap;
|
|
857
|
+
overflow: hidden;
|
|
858
|
+
text-overflow: ellipsis;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.msg-nav-row:hover {
|
|
862
|
+
background: var(--bg-hover);
|
|
863
|
+
color: var(--fg);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.msg-nav-row.on {
|
|
867
|
+
color: var(--fg);
|
|
868
|
+
font-weight: 500;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/* 手机上没地方放,也没有悬停 */
|
|
872
|
+
@media (max-width: 768px) {
|
|
873
|
+
.msg-nav {
|
|
874
|
+
display: none;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/* 排队中的消息:「排队中 · 打断并发送」 */
|
|
879
|
+
.queue-note {
|
|
880
|
+
display: flex;
|
|
881
|
+
align-items: center;
|
|
882
|
+
gap: 10px;
|
|
883
|
+
margin-top: -2px;
|
|
884
|
+
font-size: 12px;
|
|
885
|
+
color: var(--fg-faint);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.queue-note.dropped {
|
|
889
|
+
color: var(--danger);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.queue-jump {
|
|
893
|
+
padding: 2px 8px;
|
|
894
|
+
border-radius: 6px;
|
|
895
|
+
font-size: 12px;
|
|
896
|
+
color: var(--fg-soft);
|
|
897
|
+
border: 1px solid var(--border);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.queue-jump:hover {
|
|
901
|
+
color: var(--fg);
|
|
902
|
+
background: var(--bg-hover);
|
|
903
|
+
}
|
package/web/css/settings.css
CHANGED
|
@@ -333,3 +333,197 @@
|
|
|
333
333
|
background: var(--code-bg);
|
|
334
334
|
font-size: 12px;
|
|
335
335
|
}
|
|
336
|
+
|
|
337
|
+
/* ---------- 用量(按日 / 月 / 年统计 Token 和费用) ---------- */
|
|
338
|
+
|
|
339
|
+
.usage-stats h5 {
|
|
340
|
+
margin: 4px 0 -4px;
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
color: var(--fg-soft);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.us-head {
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
gap: 12px;
|
|
350
|
+
flex-wrap: wrap;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.us-head h4 {
|
|
354
|
+
flex: 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.us-head .seg {
|
|
358
|
+
display: inline-flex;
|
|
359
|
+
padding: 3px;
|
|
360
|
+
border-radius: 10px;
|
|
361
|
+
background: var(--bg-soft);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.us-head .seg button {
|
|
365
|
+
padding: 5px 12px;
|
|
366
|
+
border-radius: 8px;
|
|
367
|
+
font-size: 13px;
|
|
368
|
+
color: var(--fg-soft);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.us-head .seg button.on {
|
|
372
|
+
background: var(--bg);
|
|
373
|
+
color: var(--fg);
|
|
374
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.us-nav {
|
|
378
|
+
display: inline-flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: 6px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.us-title {
|
|
384
|
+
min-width: 7.5em;
|
|
385
|
+
text-align: center;
|
|
386
|
+
font-size: 14px;
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
font-variant-numeric: tabular-nums;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.us-empty {
|
|
392
|
+
padding: 24px 0;
|
|
393
|
+
text-align: center;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.us-tiles {
|
|
397
|
+
display: grid;
|
|
398
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
399
|
+
gap: 8px;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.us-tile {
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
gap: 4px;
|
|
406
|
+
padding: 10px 12px;
|
|
407
|
+
border-radius: 10px;
|
|
408
|
+
background: var(--bg-soft);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.us-tile span {
|
|
412
|
+
font-size: 12px;
|
|
413
|
+
color: var(--fg-soft);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.us-tile b {
|
|
417
|
+
font-size: 20px;
|
|
418
|
+
font-weight: 600;
|
|
419
|
+
font-variant-numeric: tabular-nums;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* 趋势:单一系列,用主题前景色;没用量的格子留空,悬停看数字 */
|
|
423
|
+
.us-trend {
|
|
424
|
+
position: relative;
|
|
425
|
+
padding-top: 16px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.us-bars {
|
|
429
|
+
display: grid;
|
|
430
|
+
gap: 2px;
|
|
431
|
+
height: 120px;
|
|
432
|
+
align-items: end;
|
|
433
|
+
border-bottom: 1px solid var(--border);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.us-col {
|
|
437
|
+
height: 100%;
|
|
438
|
+
display: flex;
|
|
439
|
+
align-items: flex-end;
|
|
440
|
+
border-radius: 4px 4px 0 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.us-col:hover {
|
|
444
|
+
background: var(--bg-soft);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.us-col i {
|
|
448
|
+
width: 100%;
|
|
449
|
+
background: var(--fg);
|
|
450
|
+
opacity: 0.78;
|
|
451
|
+
border-radius: 4px 4px 0 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.us-col:hover i {
|
|
455
|
+
opacity: 1;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.us-axis {
|
|
459
|
+
display: grid;
|
|
460
|
+
gap: 2px;
|
|
461
|
+
margin-top: 4px;
|
|
462
|
+
font-size: 11px;
|
|
463
|
+
color: var(--fg-faint);
|
|
464
|
+
text-align: center;
|
|
465
|
+
font-variant-numeric: tabular-nums;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.us-max {
|
|
469
|
+
position: absolute;
|
|
470
|
+
top: 0;
|
|
471
|
+
right: 0;
|
|
472
|
+
font-size: 11px;
|
|
473
|
+
color: var(--fg-faint);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.usage-stats .usage {
|
|
477
|
+
padding: 0;
|
|
478
|
+
font-size: 13px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.us-list {
|
|
482
|
+
display: flex;
|
|
483
|
+
flex-direction: column;
|
|
484
|
+
gap: 6px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.us-item {
|
|
488
|
+
display: grid;
|
|
489
|
+
grid-template-columns: minmax(5em, 9em) 1fr 4.5em 4.5em;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 10px;
|
|
492
|
+
font-size: 13px;
|
|
493
|
+
font-variant-numeric: tabular-nums;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.us-name {
|
|
497
|
+
overflow: hidden;
|
|
498
|
+
text-overflow: ellipsis;
|
|
499
|
+
white-space: nowrap;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.us-meter {
|
|
503
|
+
height: 6px;
|
|
504
|
+
border-radius: 3px;
|
|
505
|
+
background: var(--bg-soft);
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.us-meter i {
|
|
510
|
+
display: block;
|
|
511
|
+
height: 100%;
|
|
512
|
+
background: var(--fg-soft);
|
|
513
|
+
border-radius: 3px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.us-num,
|
|
517
|
+
.us-cost {
|
|
518
|
+
text-align: right;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.us-cost {
|
|
522
|
+
color: var(--fg-soft);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
@media (max-width: 640px) {
|
|
526
|
+
.us-tiles {
|
|
527
|
+
grid-template-columns: 1fr 1fr;
|
|
528
|
+
}
|
|
529
|
+
}
|
package/web/css/sidebar.css
CHANGED
|
@@ -324,13 +324,13 @@ button.conv-group:hover {
|
|
|
324
324
|
min-height: 48px;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
/* 没有额度可显示:连上面的分隔线一起藏起来 */
|
|
328
|
+
.side-bottom[hidden] {
|
|
329
|
+
display: none;
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
display: none;
|
|
332
|
+
.conv-item .chat-mark {
|
|
333
|
+
color: var(--fg-soft);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
/* ---- 侧栏底部的额度 ---- */
|
package/web/css/skills.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* ----------
|
|
1
|
+
/* ---------- 技能(设置里的一页,卡片沿用定时任务页的 .task-card) ---------- */
|
|
2
2
|
|
|
3
3
|
.skill-head-btns {
|
|
4
4
|
display: flex;
|
|
@@ -366,3 +366,20 @@
|
|
|
366
366
|
flex: 1 1 100%;
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
+
|
|
370
|
+
/* 设置里的技能页 */
|
|
371
|
+
.skills-page {
|
|
372
|
+
display: flex;
|
|
373
|
+
flex-direction: column;
|
|
374
|
+
gap: 14px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.skills-head {
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: flex-start;
|
|
380
|
+
gap: 16px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.skills-head .skill-note {
|
|
384
|
+
flex: 1;
|
|
385
|
+
}
|