Skip to content

Commit d94dea2

Browse files
committed
feat: add auth file mappings and update related queries to support credential names
1 parent ed3ac0a commit d94dea2

12 files changed

Lines changed: 282 additions & 27 deletions

File tree

app/api/overview/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ const OVERVIEW_CACHE_TTL_MS = 30_000;
2020
const OVERVIEW_CACHE_MAX_ENTRIES = 100;
2121
const overviewCache = new Map<string, CachedOverview>();
2222

23-
function makeCacheKey(input: { days?: number; model?: string | null; route?: string | null; source?: string | null; page?: number; pageSize?: number; start?: string | null; end?: string | null }) {
23+
function makeCacheKey(input: { days?: number; model?: string | null; route?: string | null; source?: string | null; name?: string | null; page?: number; pageSize?: number; start?: string | null; end?: string | null }) {
2424
return JSON.stringify({
2525
days: input.days ?? null,
2626
model: input.model ?? null,
2727
route: input.route ?? null,
2828
source: input.source ?? null,
29+
name: input.name ?? null,
2930
page: input.page ?? null,
3031
pageSize: input.pageSize ?? null,
3132
start: input.start ?? null,
@@ -65,6 +66,7 @@ export async function GET(request: Request) {
6566
const model = searchParams.get("model");
6667
const route = searchParams.get("route");
6768
const source = searchParams.get("source");
69+
const name = searchParams.get("name");
6870
const pageParam = searchParams.get("page");
6971
const pageSizeParam = searchParams.get("pageSize");
7072
const start = searchParams.get("start");
@@ -74,7 +76,7 @@ export async function GET(request: Request) {
7476
const pageSize = pageSizeParam ? Number.parseInt(pageSizeParam, 10) : undefined;
7577
const skipCacheParam = searchParams.get("skipCache");
7678
const skipCache = skipCacheParam === "1" || skipCacheParam === "true";
77-
const cacheKey = makeCacheKey({ days, model, route, source, page, pageSize, start, end });
79+
const cacheKey = makeCacheKey({ days, model, route, source, name, page, pageSize, start, end });
7880
if (!skipCache) {
7981
const cached = getCached(cacheKey);
8082
if (cached) {
@@ -86,6 +88,7 @@ export async function GET(request: Request) {
8688
model: model || undefined,
8789
route: route || undefined,
8890
source: source || undefined,
91+
name: name || undefined,
8992
page,
9093
pageSize,
9194
start,

app/api/sync/route.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { cookies } from "next/headers";
33
import { eq, sql } from "drizzle-orm";
44
import { config, assertEnv } from "@/lib/config";
55
import { db } from "@/lib/db/client";
6-
import { usageRecords } from "@/lib/db/schema";
6+
import { authFileMappings, usageRecords } from "@/lib/db/schema";
7+
import { toAuthFileMappings } from "@/lib/auth-files";
78
import { parseUsagePayload, toUsageRecords } from "@/lib/usage";
89

910
export const runtime = "nodejs";
@@ -47,6 +48,44 @@ async function isAuthorized(request: Request) {
4748
return false;
4849
}
4950

51+
async function syncAuthFileMappings(pulledAt: Date) {
52+
const authFilesUrl = `${config.cliproxy.baseUrl.replace(/\/$/, "")}/auth-files`;
53+
54+
const response = await fetch(authFilesUrl, {
55+
headers: {
56+
Authorization: `Bearer ${config.cliproxy.apiKey}`,
57+
"Content-Type": "application/json"
58+
},
59+
cache: "no-store"
60+
});
61+
62+
if (!response.ok) {
63+
throw new Error(`Failed to fetch auth-files: ${response.status} ${response.statusText}`);
64+
}
65+
66+
const json = await response.json();
67+
const rows = toAuthFileMappings(json, pulledAt);
68+
if (rows.length === 0) return 0;
69+
70+
await db
71+
.insert(authFileMappings)
72+
.values(rows)
73+
.onConflictDoUpdate({
74+
target: authFileMappings.authId,
75+
set: {
76+
name: sql`coalesce(nullif(excluded.name, ''), ${authFileMappings.name})`,
77+
label: sql`coalesce(nullif(excluded.label, ''), ${authFileMappings.label})`,
78+
provider: sql`coalesce(nullif(excluded.provider, ''), ${authFileMappings.provider})`,
79+
source: sql`coalesce(nullif(excluded.source, ''), ${authFileMappings.source})`,
80+
email: sql`coalesce(nullif(excluded.email, ''), ${authFileMappings.email})`,
81+
updatedAt: sql`coalesce(excluded.updated_at, ${authFileMappings.updatedAt})`,
82+
syncedAt: pulledAt
83+
}
84+
});
85+
86+
return rows.length;
87+
}
88+
5089
async function performSync(request: Request) {
5190
if (!config.password && !config.cronSecret && !PASSWORD) return missingPassword();
5291
if (!(await isAuthorized(request))) return unauthorized();
@@ -89,8 +128,23 @@ async function performSync(request: Request) {
89128

90129
const rows = toUsageRecords(payload, pulledAt);
91130

131+
let authFilesSynced = 0;
132+
let authFilesWarning: string | undefined;
133+
try {
134+
authFilesSynced = await syncAuthFileMappings(pulledAt);
135+
} catch (error) {
136+
authFilesWarning = "auth-files sync failed";
137+
console.warn("/api/sync auth-files sync failed:", error);
138+
}
139+
92140
if (rows.length === 0) {
93-
return NextResponse.json({ status: "ok", inserted: 0, message: "No usage data" });
141+
return NextResponse.json({
142+
status: "ok",
143+
inserted: 0,
144+
message: "No usage data",
145+
authFilesSynced,
146+
...(authFilesWarning ? { authFilesWarning } : {})
147+
});
94148
}
95149

96150
let insertedRows: Array<{ id: number }>;
@@ -119,7 +173,13 @@ async function performSync(request: Request) {
119173
inserted = Number(fallback?.[0]?.count ?? 0);
120174
}
121175

122-
return NextResponse.json({ status: "ok", inserted, attempted: rows.length });
176+
return NextResponse.json({
177+
status: "ok",
178+
inserted,
179+
attempted: rows.length,
180+
authFilesSynced,
181+
...(authFilesWarning ? { authFilesWarning } : {})
182+
});
123183
}
124184

125185
export async function POST(request: Request) {

app/page.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ const PIE_COLORS = [
2323
];
2424

2525
type OverviewMeta = { page: number; pageSize: number; totalModels: number; totalPages: number };
26-
type OverviewAPIResponse = { overview: UsageOverview | null; empty: boolean; days: number; timezone?: string; meta?: OverviewMeta; filters?: { models: string[]; routes: string[]; sources: string[] } };
26+
type OverviewAPIResponse = {
27+
overview: UsageOverview | null;
28+
empty: boolean;
29+
days: number;
30+
timezone?: string;
31+
meta?: OverviewMeta;
32+
filters?: { models: string[]; routes: string[]; sources: string[]; names: string[] };
33+
};
2734

2835
type PriceForm = {
2936
model: string;
@@ -185,12 +192,15 @@ export default function DashboardPage() {
185192
const [modelOptions, setModelOptions] = useState<string[]>([]);
186193
const [routeOptions, setRouteOptions] = useState<string[]>([]);
187194
const [sourceOptions, setSourceOptions] = useState<string[]>([]);
195+
const [nameOptions, setNameOptions] = useState<string[]>([]);
188196
const [filterModelInput, setFilterModelInput] = useState("");
189197
const [filterRouteInput, setFilterRouteInput] = useState("");
190198
const [filterSourceInput, setFilterSourceInput] = useState("");
199+
const [filterNameInput, setFilterNameInput] = useState("");
191200
const [filterModel, setFilterModel] = useState<string | undefined>(undefined);
192201
const [filterRoute, setFilterRoute] = useState<string | undefined>(undefined);
193202
const [filterSource, setFilterSource] = useState<string | undefined>(undefined);
203+
const [filterName, setFilterName] = useState<string | undefined>(undefined);
194204
const [page, setPage] = useState(1);
195205
const [form, setForm] = useState<PriceForm>({ model: "", inputPricePer1M: "", cachedInputPricePer1M: "", outputPricePer1M: "" });
196206
const [status, setStatus] = useState<string | null>(null);
@@ -720,6 +730,7 @@ export default function DashboardPage() {
720730
if (filterModel) params.set("model", filterModel);
721731
if (filterRoute) params.set("route", filterRoute);
722732
if (filterSource) params.set("source", filterSource);
733+
if (filterName) params.set("name", filterName);
723734
params.set("page", String(page));
724735
params.set("pageSize", "500");
725736

@@ -747,6 +758,7 @@ export default function DashboardPage() {
747758
setModelOptions(Array.from(new Set(data.filters?.models ?? [])));
748759
setRouteOptions(Array.from(new Set(data.filters?.routes ?? [])));
749760
setSourceOptions(Array.from(new Set(data.filters?.sources ?? [])));
761+
setNameOptions(Array.from(new Set(data.filters?.names ?? [])));
750762
setAppliedDays(data.days ?? rangeDays);
751763
} catch (err) {
752764
if (!active) return;
@@ -763,7 +775,7 @@ export default function DashboardPage() {
763775
active = false;
764776
controller.abort();
765777
};
766-
}, [rangeMode, customStart, customEnd, rangeDays, filterModel, filterRoute, filterSource, page, refreshTrigger, ready]);
778+
}, [rangeMode, customStart, customEnd, rangeDays, filterModel, filterRoute, filterSource, filterName, page, refreshTrigger, ready]);
767779

768780
const overviewData = overview;
769781
const showEmpty = overviewEmpty || !overview;
@@ -882,6 +894,7 @@ export default function DashboardPage() {
882894
setFilterModel(filterModelInput.trim() || undefined);
883895
setFilterRoute(filterRouteInput.trim() || undefined);
884896
setFilterSource(filterSourceInput.trim() || undefined);
897+
setFilterName(filterNameInput.trim() || undefined);
885898
};
886899

887900
const applyModelOption = (val: string) => {
@@ -902,6 +915,12 @@ export default function DashboardPage() {
902915
setPage(1);
903916
};
904917

918+
const applyNameOption = (val: string) => {
919+
setFilterNameInput(val);
920+
setFilterName(val.trim() || undefined);
921+
setPage(1);
922+
};
923+
905924
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
906925
event.preventDefault();
907926
setStatus(null);
@@ -1269,21 +1288,36 @@ export default function DashboardPage() {
12691288
setPage(1);
12701289
}}
12711290
/>
1291+
<ComboBox
1292+
value={filterNameInput}
1293+
onChange={setFilterNameInput}
1294+
options={nameOptions}
1295+
placeholder="按凭证名过滤"
1296+
darkMode={darkMode}
1297+
onSelectOption={applyNameOption}
1298+
onClear={() => {
1299+
setFilterNameInput("");
1300+
setFilterName(undefined);
1301+
setPage(1);
1302+
}}
1303+
/>
12721304
<button
12731305
onClick={applyFilters}
12741306
className={`rounded-lg border px-3 py-1.5 text-sm font-semibold transition ${darkMode ? "border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-500" : "border-slate-300 bg-white text-slate-700 hover:border-slate-400"}`}
12751307
>
12761308
应用筛选
12771309
</button>
1278-
{(filterModel || filterRoute || filterSource) ? (
1310+
{(filterModel || filterRoute || filterSource || filterName) ? (
12791311
<button
12801312
onClick={() => {
12811313
setFilterModelInput("");
12821314
setFilterRouteInput("");
12831315
setFilterSourceInput("");
1316+
setFilterNameInput("");
12841317
setFilterModel(undefined);
12851318
setFilterRoute(undefined);
12861319
setFilterSource(undefined);
1320+
setFilterName(undefined);
12871321
setPage(1);
12881322
}}
12891323
className={`rounded-lg border px-3 py-1.5 text-sm transition ${darkMode ? "border-slate-700 bg-slate-800 text-slate-400 hover:border-slate-500" : "border-slate-300 bg-white text-slate-600 hover:border-slate-400"}`}

app/records/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type UsageRecord = {
1212
occurredAt: string;
1313
route: string;
1414
source: string;
15+
credentialName: string;
1516
model: string;
1617
totalTokens: number;
1718
inputTokens: number;
@@ -787,8 +788,8 @@ export default function RecordsPage() {
787788
</div>
788789
</td>
789790
<td className="px-3 py-3 first:rounded-l-lg last:rounded-r-lg">
790-
<div className="max-w-[220px] truncate text-slate-300" title={hideRouteValue ? "-" : row.source || "-"}>
791-
{hideRouteValue ? "-" : row.source || "-"}
791+
<div className="max-w-[220px] truncate text-slate-300" title={hideRouteValue ? "-" : row.credentialName || "-"}>
792+
{hideRouteValue ? "-" : row.credentialName || "-"}
792793
</div>
793794
</td>
794795
<td className="px-3 py-3 first:rounded-l-lg last:rounded-r-lg">
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE "usage_records"
2+
ADD COLUMN "auth_index" text;
3+
4+
-- 回填历史数据中的 auth_index(从 raw.detail.auth_index 提取)
5+
UPDATE "usage_records"
6+
SET "auth_index" = NULLIF(("raw"::jsonb -> 'detail' ->> 'auth_index'), '')
7+
WHERE "auth_index" IS NULL;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CREATE TABLE "auth_file_mappings" (
2+
"auth_id" text PRIMARY KEY NOT NULL,
3+
"name" text NOT NULL DEFAULT '',
4+
"label" text,
5+
"provider" text,
6+
"source" text,
7+
"email" text,
8+
"updated_at" timestamp with time zone,
9+
"synced_at" timestamp with time zone NOT NULL DEFAULT now()
10+
);
11+
12+
CREATE INDEX "auth_file_mappings_name_idx"
13+
ON "auth_file_mappings" USING btree ("name");

drizzle/meta/_journal.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@
2222
"when": 1771065600000,
2323
"tag": "0002_add_source_to_usage_records",
2424
"breakpoints": true
25+
},
26+
{
27+
"idx": 3,
28+
"version": "7",
29+
"when": 1771069200000,
30+
"tag": "0003_add_auth_index_to_usage_records",
31+
"breakpoints": true
32+
},
33+
{
34+
"idx": 4,
35+
"version": "7",
36+
"when": 1771072800000,
37+
"tag": "0004_add_auth_file_mappings",
38+
"breakpoints": true
2539
}
2640
]
2741
}

0 commit comments

Comments
 (0)