shine-code-submit 1.0.1 → 1.0.2
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/.claude-plugin/plugin.json +1 -1
- package/dist/install.cjs +1 -1
- package/package.json +1 -1
- package/src/daemon/git.ts +10 -0
- package/src/daemon/server.ts +15 -19
- package/src/daemon/settings.ts +11 -3
- package/src/daemon/ui-assets.ts +1 -1
- package/src/shared/types.ts +1 -13
- package/ui/.build/app.js +42 -42
- package/ui/components/OverviewModule.tsx +2 -2
- package/ui/components/ReportModule.tsx +58 -25
- package/ui/components/SessionDetail.tsx +3 -3
- package/ui/components/SessionTree.tsx +3 -6
- package/ui/components/StatsModule.tsx +3 -3
- package/ui/components/SummaryView.tsx +3 -3
- package/ui/lib/aggregate.ts +2 -1
- package/ui/lib/util.ts +30 -9
|
@@ -3,7 +3,7 @@ import { useApi } from "../hooks/useApi";
|
|
|
3
3
|
import { useEvents } from "../hooks/useEvents";
|
|
4
4
|
import { useAllCommits } from "../hooks/useAllCommits";
|
|
5
5
|
import { useApp } from "../state/AppContext";
|
|
6
|
-
import { fmtDateTime, fmtTokens, shortDir, sumTokenUsage } from "../lib/util";
|
|
6
|
+
import { fmtDateTime, fmtTokens, realInput, shortDir, sumTokenUsage } from "../lib/util";
|
|
7
7
|
|
|
8
8
|
interface TimelineItem {
|
|
9
9
|
ts: number;
|
|
@@ -41,7 +41,7 @@ export function OverviewModule() {
|
|
|
41
41
|
<div className="kpi-grid">
|
|
42
42
|
<div className="kpi-card">
|
|
43
43
|
<span className="kpi-label">Token 输入</span>
|
|
44
|
-
<b className="kpi-value">{fmtTokens(tokenSum.total
|
|
44
|
+
<b className="kpi-value">{fmtTokens(realInput(tokenSum.total))}</b>
|
|
45
45
|
</div>
|
|
46
46
|
<div className="kpi-card">
|
|
47
47
|
<span className="kpi-label">Token 输出</span>
|
|
@@ -7,7 +7,7 @@ import { useApp } from "../state/AppContext";
|
|
|
7
7
|
import { Icon } from "./Icon";
|
|
8
8
|
import { Splitter } from "./Splitter";
|
|
9
9
|
import type { ReportProject, ReportResponse } from "../types";
|
|
10
|
-
import { fmtDateTime, fmtTokens, fmtUsageFull, shortDir } from "../lib/util";
|
|
10
|
+
import { fmtDateTime, fmtTokens, fmtUsageFull, fmtUsageLabeled, realInput, shortDir } from "../lib/util";
|
|
11
11
|
|
|
12
12
|
const PAGE = 20; // 每页 session 数
|
|
13
13
|
|
|
@@ -17,6 +17,8 @@ export function ReportModule() {
|
|
|
17
17
|
const [data, setData] = useState<ReportResponse | null>(null);
|
|
18
18
|
const [err, setErr] = useState<string | null>(null);
|
|
19
19
|
const [selCwd, setSelCwd] = useState<string | null>(null);
|
|
20
|
+
const [uploading, setUploading] = useState(false);
|
|
21
|
+
const [uploadResult, setUploadResult] = useState<{ ok: boolean; text: string } | null>(null);
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
22
24
|
let cancelled = false;
|
|
@@ -48,24 +50,42 @@ export function ReportModule() {
|
|
|
48
50
|
<b>报表</b>
|
|
49
51
|
<span title="软件版本">v{data.version}</span>
|
|
50
52
|
<span title="git 用户">👤 {data.gitUser ?? "—"}</span>
|
|
51
|
-
<span>{data.totals.projects} 项目</span>
|
|
52
|
-
<span>{data.totals.sessions} 会话</span>
|
|
53
|
-
<span title={fmtUsageFull(data.totals.tokens)}>
|
|
54
|
-
token {fmtTokens(data.totals.tokens.input + data.totals.tokens.output)}
|
|
55
|
-
</span>
|
|
56
53
|
<span>
|
|
57
|
-
{data.totals.
|
|
54
|
+
{data.totals.projects} 项目 · {data.totals.sessions} 会话
|
|
58
55
|
</span>
|
|
59
|
-
{/* 占位:后期接「上报到服务器」(POST 本报告到远端)。现在禁用。 */}
|
|
60
56
|
<button
|
|
61
57
|
type="button"
|
|
62
58
|
className="tab"
|
|
63
|
-
disabled
|
|
64
|
-
title="
|
|
59
|
+
disabled={uploading}
|
|
60
|
+
title="手动上报报表到服务器(需先在「设置」配置上报地址)"
|
|
65
61
|
style={{ marginLeft: "auto" }}
|
|
62
|
+
onClick={async () => {
|
|
63
|
+
setUploading(true);
|
|
64
|
+
setUploadResult(null);
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch(`${location.origin}/api/report/upload`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
69
|
+
});
|
|
70
|
+
if (r.ok) {
|
|
71
|
+
setUploadResult({ ok: true, text: "上报成功" });
|
|
72
|
+
} else {
|
|
73
|
+
const j = await r.json().catch(() => ({}));
|
|
74
|
+
setUploadResult({ ok: false, text: j.error ?? `HTTP ${r.status}` });
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
setUploadResult({ ok: false, text: e instanceof Error ? e.message : String(e) });
|
|
78
|
+
} finally {
|
|
79
|
+
setUploading(false);
|
|
80
|
+
setTimeout(() => setUploadResult(null), 3000);
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
66
83
|
>
|
|
67
|
-
☁ 上报
|
|
84
|
+
☁ {uploading ? "上报中…" : "上报"}
|
|
68
85
|
</button>
|
|
86
|
+
{uploadResult && (
|
|
87
|
+
<span className={uploadResult.ok ? "field-ok" : "field-err"}>{uploadResult.text}</span>
|
|
88
|
+
)}
|
|
69
89
|
</>
|
|
70
90
|
) : (
|
|
71
91
|
<span>{err ? `加载失败:${err}` : "加载中…"}</span>
|
|
@@ -91,8 +111,12 @@ export function ReportModule() {
|
|
|
91
111
|
className={p.cwd === selCwd ? "active" : undefined}
|
|
92
112
|
title={p.cwd}
|
|
93
113
|
onClick={() => setSelCwd(p.cwd)}
|
|
114
|
+
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: "0.5rem" }}
|
|
94
115
|
>
|
|
95
|
-
{shortDir(p.cwd) || p.cwd}
|
|
116
|
+
<span>{shortDir(p.cwd) || p.cwd}</span>
|
|
117
|
+
<span className="nav-tokens" title={fmtUsageFull(p.totalTokens)}>
|
|
118
|
+
{fmtTokens(realInput(p.totalTokens) + p.totalTokens.output)}
|
|
119
|
+
</span>
|
|
96
120
|
</li>
|
|
97
121
|
))}
|
|
98
122
|
</ul>
|
|
@@ -116,12 +140,12 @@ export function ReportModule() {
|
|
|
116
140
|
);
|
|
117
141
|
}
|
|
118
142
|
|
|
119
|
-
/** 右侧详情:标题(
|
|
143
|
+
/** 右侧详情:标题(仓库地址 + 用户 + token 汇总) + session 表格(分页,隐藏 0 token)。
|
|
120
144
|
* key=sel.cwd:换项目时重挂载,分页回到第 1 页。 */
|
|
121
145
|
function ProjectDetail({ p }: { p: ReportProject }) {
|
|
122
146
|
const [page, setPage] = useState(1);
|
|
123
|
-
// 过滤掉 0 token 的 session(tokenTotal 为 null 或
|
|
124
|
-
const rows = p.sessions.filter((s) => s.tokenTotal && s.tokenTotal
|
|
147
|
+
// 过滤掉 0 token 的 session(tokenTotal 为 null 或 真实输入+输出=0)
|
|
148
|
+
const rows = p.sessions.filter((s) => s.tokenTotal && realInput(s.tokenTotal) + s.tokenTotal.output > 0);
|
|
125
149
|
const pageCount = Math.max(1, Math.ceil(rows.length / PAGE));
|
|
126
150
|
const cur = Math.min(page, pageCount);
|
|
127
151
|
const pageRows = rows.slice((cur - 1) * PAGE, cur * PAGE);
|
|
@@ -129,15 +153,22 @@ function ProjectDetail({ p }: { p: ReportProject }) {
|
|
|
129
153
|
return (
|
|
130
154
|
<>
|
|
131
155
|
<div className="report-title">
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
{p.gitRemote && (
|
|
157
|
+
<span
|
|
158
|
+
className="rt-sum"
|
|
159
|
+
title={p.gitRemote}
|
|
160
|
+
style={{
|
|
161
|
+
flex: "0 0 60ch",
|
|
162
|
+
whiteSpace: "nowrap",
|
|
163
|
+
overflow: "hidden",
|
|
164
|
+
textOverflow: "ellipsis",
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
🔗 {p.gitRemote}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
<span className="rt-sum" title={fmtUsageFull(p.totalTokens)} style={{ marginLeft: "auto", color: "#9dbefe" }}>
|
|
171
|
+
{fmtUsageLabeled(p.totalTokens)}
|
|
141
172
|
</span>
|
|
142
173
|
</div>
|
|
143
174
|
|
|
@@ -150,6 +181,7 @@ function ProjectDetail({ p }: { p: ReportProject }) {
|
|
|
150
181
|
<th>时间</th>
|
|
151
182
|
<th className="rt-num">输入 token</th>
|
|
152
183
|
<th className="rt-num">输出 token</th>
|
|
184
|
+
<th className="rt-num">总数</th>
|
|
153
185
|
</tr>
|
|
154
186
|
</thead>
|
|
155
187
|
<tbody>
|
|
@@ -160,8 +192,9 @@ function ProjectDetail({ p }: { p: ReportProject }) {
|
|
|
160
192
|
{s.sessionId.slice(0, 8)}
|
|
161
193
|
</td>
|
|
162
194
|
<td>{fmtDateTime(s.lastActive)}</td>
|
|
163
|
-
<td className="rt-num">{fmtTokens(s.tokenTotal
|
|
195
|
+
<td className="rt-num">{fmtTokens(realInput(s.tokenTotal!))}</td>
|
|
164
196
|
<td className="rt-num">{fmtTokens(s.tokenTotal!.output)}</td>
|
|
197
|
+
<td className="rt-num">{fmtTokens(realInput(s.tokenTotal!) + s.tokenTotal!.output)}</td>
|
|
165
198
|
</tr>
|
|
166
199
|
))}
|
|
167
200
|
</tbody>
|
|
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { useApi } from "../hooks/useApi";
|
|
3
3
|
import { useConversation } from "../hooks/useConversation";
|
|
4
4
|
import { useApp } from "../state/AppContext";
|
|
5
|
-
import {
|
|
5
|
+
import { fmtUsageFull, fmtUsageLabeled, realInput, shortDir } from "../lib/util";
|
|
6
6
|
import { Conversation } from "./Conversation";
|
|
7
7
|
|
|
8
8
|
/** 会话详情(右侧):顶部摘要(sid/cwd/token)+ 该会话对话。
|
|
@@ -27,9 +27,9 @@ export function SessionDetail({ sessionId }: { sessionId: string }) {
|
|
|
27
27
|
{shortDir(session.cwd)}
|
|
28
28
|
</span>
|
|
29
29
|
)}
|
|
30
|
-
{tokenTotal && (tokenTotal
|
|
30
|
+
{tokenTotal && (realInput(tokenTotal) > 0 || tokenTotal.output > 0) && (
|
|
31
31
|
<span className="detail-token" title={fmtUsageFull(tokenTotal)}>
|
|
32
|
-
{
|
|
32
|
+
{fmtUsageLabeled(tokenTotal)}
|
|
33
33
|
</span>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { useApp } from "../state/AppContext";
|
|
3
|
-
import { fmtDateTime,
|
|
3
|
+
import { fmtDateTime, fmtTokens, fmtUsageFull, realInput } from "../lib/util";
|
|
4
4
|
import type { SessionSummary } from "../types";
|
|
5
5
|
|
|
6
6
|
/** 会话树(按 cwd 分组、可折叠、token 角标、时间点)。纯展示 + onSelect 回调。
|
|
@@ -72,15 +72,12 @@ export function SessionTree({
|
|
|
72
72
|
<div className="sess-row">
|
|
73
73
|
<span className="sess-time">{fmtDateTime(s.lastActive)}</span>
|
|
74
74
|
<span className="sess-sid">{s.sessionId.slice(0, 8)}</span>
|
|
75
|
-
{s.tokenTotal && (s.tokenTotal
|
|
75
|
+
{s.tokenTotal && (realInput(s.tokenTotal) > 0 || s.tokenTotal.output > 0) && (
|
|
76
76
|
<span className="sess-tokens" title={fmtUsageFull(s.tokenTotal)}>
|
|
77
|
-
{
|
|
77
|
+
{fmtTokens(realInput(s.tokenTotal) + s.tokenTotal.output)}
|
|
78
78
|
</span>
|
|
79
79
|
)}
|
|
80
80
|
</div>
|
|
81
|
-
<div className="sess-sub">
|
|
82
|
-
{s.lastType ?? "?"} · {s.eventCount}
|
|
83
|
-
</div>
|
|
84
81
|
</li>
|
|
85
82
|
))}
|
|
86
83
|
</ul>
|
|
@@ -3,7 +3,7 @@ import { useApi } from "../hooks/useApi";
|
|
|
3
3
|
import { useAllCommits } from "../hooks/useAllCommits";
|
|
4
4
|
import { useApp } from "../state/AppContext";
|
|
5
5
|
import { aggregateCommitsByProject, aggregateTokenByProject, bucketByDay } from "../lib/aggregate";
|
|
6
|
-
import { fmtUsage, shortDir } from "../lib/util";
|
|
6
|
+
import { fmtUsage, realInput, shortDir } from "../lib/util";
|
|
7
7
|
|
|
8
8
|
type StatTab = "token" | "commits" | "day";
|
|
9
9
|
|
|
@@ -18,7 +18,7 @@ export function StatsModule() {
|
|
|
18
18
|
const projCommits = useMemo(() => aggregateCommitsByProject(commits), [commits]);
|
|
19
19
|
const commitsByDay = useMemo(() => bucketByDay(commits.map((c) => c.time), 7), [commits]);
|
|
20
20
|
|
|
21
|
-
const maxToken = Math.max(1, ...projTokens.map((r) => r.token
|
|
21
|
+
const maxToken = Math.max(1, ...projTokens.map((r) => realInput(r.token) + r.token.output));
|
|
22
22
|
const maxCommitCount = Math.max(1, ...projCommits.map((r) => r.count));
|
|
23
23
|
const maxDayCount = Math.max(1, ...commitsByDay.map((b) => b.count));
|
|
24
24
|
|
|
@@ -60,7 +60,7 @@ export function StatsModule() {
|
|
|
60
60
|
) : (
|
|
61
61
|
<div className="bar-list">
|
|
62
62
|
{projTokens.map((r) => {
|
|
63
|
-
const v = r.token
|
|
63
|
+
const v = realInput(r.token) + r.token.output;
|
|
64
64
|
return (
|
|
65
65
|
<div className="bar-row" key={r.cwd}>
|
|
66
66
|
<span className="bar-label" title={r.cwd}>
|
|
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import { useApi } from "../hooks/useApi";
|
|
3
3
|
import { useAllCommits } from "../hooks/useAllCommits";
|
|
4
4
|
import { useApp } from "../state/AppContext";
|
|
5
|
-
import { fmtDateTime, fmtUsage, fmtUsageFull, shortDir, sumTokenUsage } from "../lib/util";
|
|
5
|
+
import { fmtDateTime, fmtUsage, fmtUsageFull, realInput, shortDir, sumTokenUsage } from "../lib/util";
|
|
6
6
|
|
|
7
7
|
/** 汇总视图(system 模块临时占位,Step 5 替换为 SystemModule):
|
|
8
8
|
* Token 按会话 + 代码提交按时间。提交拉取改用 useAllCommits(与 Overview/Stats 共用)。 */
|
|
@@ -13,7 +13,7 @@ export function SummaryView() {
|
|
|
13
13
|
const tokenRows = useMemo(
|
|
14
14
|
() =>
|
|
15
15
|
sessions
|
|
16
|
-
.filter((s) => s.tokenTotal && (s.tokenTotal
|
|
16
|
+
.filter((s) => s.tokenTotal && (realInput(s.tokenTotal) > 0 || s.tokenTotal.output > 0))
|
|
17
17
|
.slice()
|
|
18
18
|
.sort((a, b) => b.lastActive - a.lastActive),
|
|
19
19
|
[sessions],
|
|
@@ -24,7 +24,7 @@ export function SummaryView() {
|
|
|
24
24
|
const commitCount = commits.length;
|
|
25
25
|
const added = commits.reduce((n, c) => n + (c.added || 0), 0);
|
|
26
26
|
const deleted = commits.reduce((n, c) => n + (c.deleted || 0), 0);
|
|
27
|
-
const hasTokens = tokenSum.total
|
|
27
|
+
const hasTokens = realInput(tokenSum.total) > 0 || tokenSum.total.output > 0;
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<div id="summary-view" className="summary-view">
|
package/ui/lib/aggregate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 纯聚合函数(统计模块用):按项目/按天分组,无副作用。
|
|
2
2
|
import type { SessionSummary, TokenUsage } from "../types";
|
|
3
|
+
import { realInput } from "./util";
|
|
3
4
|
|
|
4
5
|
export interface ProjTokenRow {
|
|
5
6
|
cwd: string;
|
|
@@ -28,7 +29,7 @@ export function aggregateTokenByProject(sessions: SessionSummary[]): ProjTokenRo
|
|
|
28
29
|
m.set(s.cwd, r);
|
|
29
30
|
}
|
|
30
31
|
return [...m.values()].sort(
|
|
31
|
-
(a, b) => b.token
|
|
32
|
+
(a, b) => realInput(b.token) + b.token.output - (realInput(a.token) + a.token.output),
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
|
package/ui/lib/util.ts
CHANGED
|
@@ -41,27 +41,48 @@ export function safeJson(v: unknown): string {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** 紧凑数字:1234 → "1.2k",1234567 → "1.2M",<1000 原样。 */
|
|
44
|
+
/** 紧凑数字:1234 → "1.2k",1234567 → "1.2M",1.03e9 → "1B",<1000 原样。 */
|
|
45
45
|
export function fmtTokens(n: number): string {
|
|
46
46
|
if (!Number.isFinite(n) || n < 1000) return String(n);
|
|
47
|
-
if (n < 1_000_000) return trimZero((n /
|
|
48
|
-
return trimZero((n / 1_000_000).toFixed(1)) + "M";
|
|
47
|
+
if (n < 1_000_000) return trimZero((n / 1_000).toFixed(1)) + "k";
|
|
48
|
+
if (n < 1_000_000_000) return trimZero((n / 1_000_000).toFixed(1)) + "M";
|
|
49
|
+
if (n < 1_000_000_000_000) return trimZero((n / 1_000_000_000).toFixed(2)) + "B";
|
|
50
|
+
return trimZero((n / 1_000_000_000_000).toFixed(2)) + "T";
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/** token
|
|
52
|
-
|
|
53
|
+
/** 真实输入 token = 未缓存输入 + 缓存写 + 缓存读(每次 API 请求的完整 prompt 上下文)。
|
|
54
|
+
* 直接累加 Anthropic API 返回的原始字段,不乘任何系数。 */
|
|
55
|
+
export function realInput(u?: TokenUsage | null): number {
|
|
56
|
+
if (!u) return 0;
|
|
57
|
+
return u.input + u.cacheCreation + u.cacheRead;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** token 用量简写:↑真实输入 ↓输出(真实输入 = 未缓存 + 缓存写 + 缓存读)。无值返回空串。 */
|
|
61
|
+
export function fmtUsage(u?: TokenUsage | null): string {
|
|
62
|
+
if (!u) return "";
|
|
63
|
+
return `↑${fmtTokens(realInput(u))} ↓${fmtTokens(u.output)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** token 用量三段式:↑真实输入 ↓输出 · 总数(真实输入+输出)。报表/会话导航与详情用。 */
|
|
67
|
+
export function fmtUsageTotal(u?: TokenUsage | null): string {
|
|
53
68
|
if (!u) return "";
|
|
54
|
-
return `↑${fmtTokens(u
|
|
69
|
+
return `↑${fmtTokens(realInput(u))} ↓${fmtTokens(u.output)} · ${fmtTokens(realInput(u) + u.output)}`;
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
/** 完整 token
|
|
72
|
+
/** 完整 token 用量,用于 title 提示:真实输入合计 + 输出 + 四字段明细(均原始值,不加权)。 */
|
|
58
73
|
export function fmtUsageFull(u?: TokenUsage | null): string {
|
|
59
74
|
if (!u) return "";
|
|
60
|
-
return
|
|
75
|
+
return `真实输入 ${realInput(u)} · 输出 ${u.output} · (未缓存 ${u.input} · 缓存写 ${u.cacheCreation} · 缓存读 ${u.cacheRead})`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** token 用量带标签:输入 X · 输出 Y · 总数 Z(真实输入+输出)。会话详情/报表标题用。 */
|
|
79
|
+
export function fmtUsageLabeled(u?: TokenUsage | null): string {
|
|
80
|
+
if (!u) return "";
|
|
81
|
+
return `输入 ${fmtTokens(realInput(u))} · 输出 ${fmtTokens(u.output)} · 总数 ${fmtTokens(realInput(u) + u.output)}`;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
function trimZero(s: string): string {
|
|
64
|
-
return s.replace(
|
|
85
|
+
return s.replace(/\.?0+$/, "");
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
/** 累加 assistant 消息的 usage(对话视图会话级汇总)。 */
|