@pactor-app/skills 1.24.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/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +26 -0
- package/pactor/SKILL.md +373 -0
- package/pactor/references/dsl-authoring.md +448 -0
- package/prd-to-app/SKILL.md +75 -0
- package/prd-to-app/references/acceptance-checklist.md +47 -0
- package/prd-to-app/references/component-mapping.md +41 -0
- package/prd-to-app/references/mock-data-routes.md +66 -0
- package/prd-to-app/references/page-templates.md +274 -0
- package/prd-to-app/references/project-structure.md +89 -0
- package/prd-to-app/references/theme-and-style.md +41 -0
- package/prd-to-app/template/README.md +26 -0
- package/prd-to-app/template/app/[[...slug]]/page.tsx +7 -0
- package/prd-to-app/template/app/api/auth/login/route.ts +32 -0
- package/prd-to-app/template/app/api/auth/logout/route.ts +22 -0
- package/prd-to-app/template/app/api/data/[...parts]/route.ts +118 -0
- package/prd-to-app/template/app/api/public/pages/[id]/route.ts +40 -0
- package/prd-to-app/template/app/api/sitemap/route.ts +21 -0
- package/prd-to-app/template/app/api/users/route.ts +57 -0
- package/prd-to-app/template/app/favicon.ico +0 -0
- package/prd-to-app/template/app/globals.css +248 -0
- package/prd-to-app/template/app/layout.tsx +49 -0
- package/prd-to-app/template/app/login.css +122 -0
- package/prd-to-app/template/eslint.config.mjs +18 -0
- package/prd-to-app/template/next-env.d.ts +7 -0
- package/prd-to-app/template/next.config.js +32 -0
- package/prd-to-app/template/package.json +34 -0
- package/prd-to-app/template/pnpm-lock.yaml +6854 -0
- package/prd-to-app/template/pnpm-workspace.yaml +7 -0
- package/prd-to-app/template/postcss.config.mjs +7 -0
- package/prd-to-app/template/public/file.svg +1 -0
- package/prd-to-app/template/public/globe.svg +1 -0
- package/prd-to-app/template/public/next.svg +1 -0
- package/prd-to-app/template/public/skins/chenwunaifu.css +111 -0
- package/prd-to-app/template/public/skins/taijingshenlin.css +109 -0
- package/prd-to-app/template/public/skins/xuanzhimoyun.css +110 -0
- package/prd-to-app/template/public/vercel.svg +1 -0
- package/prd-to-app/template/public/window.svg +1 -0
- package/prd-to-app/template/scripts/build.mjs +35 -0
- package/prd-to-app/template/src/pactor/App.tsx +55 -0
- package/prd-to-app/template/src/pactor/actions.ts +438 -0
- package/prd-to-app/template/src/pactor/components/card-links.tsx +86 -0
- package/prd-to-app/template/src/pactor/components/chip-filter.tsx +89 -0
- package/prd-to-app/template/src/pactor/components/chip-node.tsx +46 -0
- package/prd-to-app/template/src/pactor/components/debug-chat.tsx +189 -0
- package/prd-to-app/template/src/pactor/components/index.ts +28 -0
- package/prd-to-app/template/src/pactor/components/log-status.tsx +64 -0
- package/prd-to-app/template/src/pactor/components/status-badge.tsx +46 -0
- package/prd-to-app/template/src/pactor/components/theme-switcher.tsx +136 -0
- package/prd-to-app/template/src/pactor/config.ts +28 -0
- package/prd-to-app/template/src/pactor/mocks/audit-mock.ts +403 -0
- package/prd-to-app/template/src/pactor/mocks/call-logs-mock.ts +875 -0
- package/prd-to-app/template/src/pactor/mocks/client-debug-mock.ts +188 -0
- package/prd-to-app/template/src/pactor/mocks/client-templates-mock.ts +497 -0
- package/prd-to-app/template/src/pactor/mocks/clients-mock.ts +483 -0
- package/prd-to-app/template/src/pactor/mocks/costs-mock.ts +406 -0
- package/prd-to-app/template/src/pactor/mocks/dashboard-mock.ts +192 -0
- package/prd-to-app/template/src/pactor/mocks/model-debug-mock.ts +144 -0
- package/prd-to-app/template/src/pactor/mocks/models-mock.ts +799 -0
- package/prd-to-app/template/src/pactor/mocks/ops-usage-mock.ts +806 -0
- package/prd-to-app/template/src/pactor/mocks/providers-mock.ts +424 -0
- package/prd-to-app/template/src/pactor/mocks/routes-mock.ts +604 -0
- package/prd-to-app/template/src/pactor/services.ts +185 -0
- package/prd-to-app/template/src/pactor/skin.ts +52 -0
- package/prd-to-app/template/src/server/accounts.ts +63 -0
- package/prd-to-app/template/src/server/cors.ts +34 -0
- package/prd-to-app/template/src/server/sitemap.ts +176 -0
- package/prd-to-app/template/src/server/yaml/common/dashboard.yaml +498 -0
- package/prd-to-app/template/src/server/yaml/common/login.yaml +84 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-create.yaml +340 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-detail.yaml +780 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-edit.yaml +366 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-list.yaml +1989 -0
- package/prd-to-app/template/src/server/yaml/route/route-create.yaml +511 -0
- package/prd-to-app/template/src/server/yaml/route/route-detail.yaml +610 -0
- package/prd-to-app/template/src/server/yaml/route/route-edit.yaml +809 -0
- package/prd-to-app/template/src/server/yaml/route/route-list.yaml +1793 -0
- package/prd-to-app/template/src/server/yaml/sitemap.yaml +250 -0
- package/prd-to-app/template/tsconfig.json +34 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 成本明细(运行管理)演示数据服务(MVP mock,静态页面)。
|
|
3
|
+
* cost.list / cost.detail 注册为 Pactor service;接入真实后端后替换为
|
|
4
|
+
* http / service 数据源,DSL 绑定路径保持不变。
|
|
5
|
+
* 数据形态对齐 PRD《运行管理/06-成本明细》:按单次请求展示实际成本与
|
|
6
|
+
* pricing_snapshot(命中档位 + 时段倍率);计费公式
|
|
7
|
+
* actual_price = tier_price × multiplier
|
|
8
|
+
* Cost = Σ(usage × actual_price / 1M)
|
|
9
|
+
* 金额、档位文案、计算过程说明一律在此预计算为字符串(表达式引擎不支持
|
|
10
|
+
* 数组方法 / 函数调用)。模型 / 供应商 / 客户端 / AppKey / Alias 名称与
|
|
11
|
+
* models-mock / providers-mock / clients-mock / routes-mock 保持一致。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface CostRecord {
|
|
15
|
+
/** 与调用日志成功(已计费)记录的 request id 一致,如 req-9f2a1c4d */
|
|
16
|
+
id: string;
|
|
17
|
+
/** 距当前时间的分钟数(time/shortTime 由此派生,随运行日漂移,
|
|
18
|
+
* 保证默认时间范围(近 7 天)始终命中数据) */
|
|
19
|
+
minutesAgo: number;
|
|
20
|
+
/** 完整时间,如 2026-09-19 08:05:12(由 minutesAgo 派生) */
|
|
21
|
+
time: string;
|
|
22
|
+
/** 列表短时间(MM-DD HH:mm,由 minutesAgo 派生) */
|
|
23
|
+
shortTime: string;
|
|
24
|
+
application: string;
|
|
25
|
+
clientName: string;
|
|
26
|
+
appKeyId: string;
|
|
27
|
+
alias: string;
|
|
28
|
+
modelCode: string;
|
|
29
|
+
modelName: string;
|
|
30
|
+
providerCode: string;
|
|
31
|
+
providerName: string;
|
|
32
|
+
currency: string;
|
|
33
|
+
// ── 用量 ──
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
/** 0 表示无缓存用量(列表 Cache 列展示 —) */
|
|
37
|
+
cacheReadTokens: number;
|
|
38
|
+
// ── 定价快照(pricing_snapshot,只读) ──
|
|
39
|
+
priceVersion: string;
|
|
40
|
+
priceEffective: string;
|
|
41
|
+
/** 命中档位 UPTO 上限;null = 无上限档(∞) */
|
|
42
|
+
tierUpto: number | null;
|
|
43
|
+
/** 档位单价($/1M tokens) */
|
|
44
|
+
tierInputPrice: number;
|
|
45
|
+
tierOutputPrice: number;
|
|
46
|
+
tierCacheReadPrice: number | null;
|
|
47
|
+
/** 命中的时段倍率(未命中为 1.0) */
|
|
48
|
+
multiplier: number;
|
|
49
|
+
/** 时段倍率规则文案,如「每天 00:00–08:00 ×0.8」;未命中为说明文字 */
|
|
50
|
+
multiplierRule: string;
|
|
51
|
+
// ── 预计算展示字段 ──
|
|
52
|
+
/** 档位短标签,如 ≤32K / ≤128K / ∞ */
|
|
53
|
+
tierLabel: string;
|
|
54
|
+
/** 倍率短标签,如 0.8x */
|
|
55
|
+
multiplierText: string;
|
|
56
|
+
inputCost: string;
|
|
57
|
+
outputCost: string;
|
|
58
|
+
/** 无缓存用量时为 — */
|
|
59
|
+
cacheCost: string;
|
|
60
|
+
totalCost: string;
|
|
61
|
+
/** Header 摘要:时间 · 客户端(AppKey)· 模型 · 供应商 */
|
|
62
|
+
contextSummary: string;
|
|
63
|
+
/** 档位完整文案,如 UPTO ≤ 32,000 / UPTO ∞(无上限档) */
|
|
64
|
+
tierDetail: string;
|
|
65
|
+
/** 档位单价摘要:输入 $0.2/M · 输出 $1.6/M · 缓存读 — */
|
|
66
|
+
tierPriceText: string;
|
|
67
|
+
inputTokensText: string;
|
|
68
|
+
outputTokensText: string;
|
|
69
|
+
cacheReadTokensText: string;
|
|
70
|
+
/** 费用计算过程,如 2,400 × $0.2 × 0.8 ÷ 1M = $0.0004 */
|
|
71
|
+
inputCalcText: string;
|
|
72
|
+
outputCalcText: string;
|
|
73
|
+
cacheCalcText: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CostListParams {
|
|
77
|
+
/** Application 模糊 */
|
|
78
|
+
application?: string;
|
|
79
|
+
/** AppKey ID 模糊 */
|
|
80
|
+
appKey?: string;
|
|
81
|
+
/** Alias 模糊 */
|
|
82
|
+
alias?: string;
|
|
83
|
+
/** all | Model Code */
|
|
84
|
+
model?: string;
|
|
85
|
+
/** all | Provider Code */
|
|
86
|
+
provider?: string;
|
|
87
|
+
/** 时间范围(ISO 日期对,含端点;缺省不过滤) */
|
|
88
|
+
start?: string;
|
|
89
|
+
end?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CostListResult {
|
|
93
|
+
items: CostRecord[];
|
|
94
|
+
total: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pad2 = (n: number) => String(n).padStart(2, "0");
|
|
98
|
+
|
|
99
|
+
/** 相对当前时间的 "YYYY-MM-DD HH:mm:ss" 时间戳(mock 时间随运行日漂移) */
|
|
100
|
+
function timeAgo(minutes: number): string {
|
|
101
|
+
const d = new Date(Date.now() - minutes * 60000);
|
|
102
|
+
return (
|
|
103
|
+
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ` +
|
|
104
|
+
`${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Token 档位的短写法:32000 → ≤32K,128000 → ≤128K,null → ∞ */
|
|
109
|
+
function tierShortLabel(upto: number | null): string {
|
|
110
|
+
if (upto == null) return "∞";
|
|
111
|
+
if (upto >= 1000 && upto % 1000 === 0) return `≤${upto / 1000}K`;
|
|
112
|
+
return `≤${upto.toLocaleString("en-US")}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function money(v: number): string {
|
|
116
|
+
return `$${v.toFixed(4)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function num(n: number): string {
|
|
120
|
+
return n.toLocaleString("en-US");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 单价展示:$0.2(/M tokens 单价,保留原始精度) */
|
|
124
|
+
function price(p: number): string {
|
|
125
|
+
return `$${p}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 列表/详情展示字段派生(费用按 PRD 公式计算并格式化为字符串) */
|
|
129
|
+
function derive(
|
|
130
|
+
record: Omit<
|
|
131
|
+
CostRecord,
|
|
132
|
+
| "time"
|
|
133
|
+
| "shortTime"
|
|
134
|
+
| "tierLabel"
|
|
135
|
+
| "multiplierText"
|
|
136
|
+
| "inputCost"
|
|
137
|
+
| "outputCost"
|
|
138
|
+
| "cacheCost"
|
|
139
|
+
| "totalCost"
|
|
140
|
+
| "contextSummary"
|
|
141
|
+
| "tierDetail"
|
|
142
|
+
| "tierPriceText"
|
|
143
|
+
| "inputTokensText"
|
|
144
|
+
| "outputTokensText"
|
|
145
|
+
| "cacheReadTokensText"
|
|
146
|
+
| "inputCalcText"
|
|
147
|
+
| "outputCalcText"
|
|
148
|
+
| "cacheCalcText"
|
|
149
|
+
>,
|
|
150
|
+
): CostRecord {
|
|
151
|
+
// actual_price = tier_price × multiplier;Cost = usage × actual_price / 1M
|
|
152
|
+
const inputCost =
|
|
153
|
+
(record.inputTokens * record.tierInputPrice * record.multiplier) / 1e6;
|
|
154
|
+
const outputCost =
|
|
155
|
+
(record.outputTokens * record.tierOutputPrice * record.multiplier) / 1e6;
|
|
156
|
+
const cacheCost =
|
|
157
|
+
record.cacheReadTokens > 0 && record.tierCacheReadPrice != null
|
|
158
|
+
? (record.cacheReadTokens * record.tierCacheReadPrice * record.multiplier) /
|
|
159
|
+
1e6
|
|
160
|
+
: 0;
|
|
161
|
+
const hasCache = record.cacheReadTokens > 0 && record.tierCacheReadPrice != null;
|
|
162
|
+
const calc = (tokens: number, unitPrice: number, cost: number) =>
|
|
163
|
+
`${num(tokens)} × ${price(unitPrice)} × ${record.multiplier} ÷ 1M = ${money(cost)}`;
|
|
164
|
+
const time = timeAgo(record.minutesAgo);
|
|
165
|
+
return {
|
|
166
|
+
...record,
|
|
167
|
+
time,
|
|
168
|
+
shortTime: time.slice(5, 16),
|
|
169
|
+
tierLabel: tierShortLabel(record.tierUpto),
|
|
170
|
+
multiplierText: `${record.multiplier}x`,
|
|
171
|
+
inputCost: money(inputCost),
|
|
172
|
+
outputCost: money(outputCost),
|
|
173
|
+
cacheCost: hasCache ? money(cacheCost) : "—",
|
|
174
|
+
totalCost: money(inputCost + outputCost + cacheCost),
|
|
175
|
+
contextSummary: `${time} · ${record.clientName}(${record.appKeyId})· ${record.modelName} · ${record.providerName}`,
|
|
176
|
+
tierDetail:
|
|
177
|
+
record.tierUpto == null
|
|
178
|
+
? "UPTO ∞(无上限档)"
|
|
179
|
+
: `UPTO ≤ ${num(record.tierUpto)}`,
|
|
180
|
+
tierPriceText: `输入 ${price(record.tierInputPrice)}/M · 输出 ${price(record.tierOutputPrice)}/M · 缓存读 ${record.tierCacheReadPrice != null ? `${price(record.tierCacheReadPrice)}/M` : "—"}`,
|
|
181
|
+
inputTokensText: num(record.inputTokens),
|
|
182
|
+
outputTokensText: num(record.outputTokens),
|
|
183
|
+
cacheReadTokensText: hasCache ? num(record.cacheReadTokens) : "—",
|
|
184
|
+
inputCalcText: calc(record.inputTokens, record.tierInputPrice, inputCost),
|
|
185
|
+
outputCalcText: calc(record.outputTokens, record.tierOutputPrice, outputCost),
|
|
186
|
+
cacheCalcText: hasCache
|
|
187
|
+
? calc(record.cacheReadTokens, record.tierCacheReadPrice!, cacheCost)
|
|
188
|
+
: "—(无缓存用量)",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const COSTS: CostRecord[] = [
|
|
193
|
+
derive({
|
|
194
|
+
id: "req-9f2a1c4d",
|
|
195
|
+
minutesAgo: 25,
|
|
196
|
+
application: "agent-platform",
|
|
197
|
+
clientName: "agent-app",
|
|
198
|
+
appKeyId: "gw-ak-7f3a2b1c",
|
|
199
|
+
alias: "reasoning",
|
|
200
|
+
modelCode: "qwen3-vl-plus",
|
|
201
|
+
modelName: "Qwen3-VL Plus",
|
|
202
|
+
providerCode: "dashscope",
|
|
203
|
+
providerName: "阿里云百炼",
|
|
204
|
+
currency: "USD",
|
|
205
|
+
inputTokens: 2400,
|
|
206
|
+
outputTokens: 1800,
|
|
207
|
+
cacheReadTokens: 0,
|
|
208
|
+
priceVersion: "v3",
|
|
209
|
+
priceEffective: "2026-09-01",
|
|
210
|
+
tierUpto: 32000,
|
|
211
|
+
tierInputPrice: 0.2,
|
|
212
|
+
tierOutputPrice: 1.6,
|
|
213
|
+
tierCacheReadPrice: null,
|
|
214
|
+
multiplier: 0.8,
|
|
215
|
+
multiplierRule: "每天 00:00–08:00 ×0.8",
|
|
216
|
+
}),
|
|
217
|
+
derive({
|
|
218
|
+
id: "req-7b3e5f08",
|
|
219
|
+
minutesAgo: 180,
|
|
220
|
+
application: "web-portal",
|
|
221
|
+
clientName: "web-portal",
|
|
222
|
+
appKeyId: "gw-ak-9c1d4e5f",
|
|
223
|
+
alias: "general",
|
|
224
|
+
modelCode: "qwen3.5-flash",
|
|
225
|
+
modelName: "Qwen3.5 Flash",
|
|
226
|
+
providerCode: "dashscope",
|
|
227
|
+
providerName: "阿里云百炼",
|
|
228
|
+
currency: "USD",
|
|
229
|
+
inputTokens: 1200,
|
|
230
|
+
outputTokens: 640,
|
|
231
|
+
cacheReadTokens: 0,
|
|
232
|
+
priceVersion: "v2",
|
|
233
|
+
priceEffective: "2026-08-15",
|
|
234
|
+
tierUpto: 128000,
|
|
235
|
+
tierInputPrice: 0.1,
|
|
236
|
+
tierOutputPrice: 0.4,
|
|
237
|
+
tierCacheReadPrice: null,
|
|
238
|
+
multiplier: 1.0,
|
|
239
|
+
multiplierRule: "—(未命中时段倍率,按 1.0 计)",
|
|
240
|
+
}),
|
|
241
|
+
derive({
|
|
242
|
+
id: "req-1e6f0b35",
|
|
243
|
+
minutesAgo: 900,
|
|
244
|
+
application: "batch-jobs",
|
|
245
|
+
clientName: "internal-batch",
|
|
246
|
+
appKeyId: "gw-ak-2b8e6a0d",
|
|
247
|
+
alias: "embedding",
|
|
248
|
+
modelCode: "text-embedding-3-large",
|
|
249
|
+
modelName: "Text Embedding 3 Large",
|
|
250
|
+
providerCode: "openai",
|
|
251
|
+
providerName: "OpenAI",
|
|
252
|
+
currency: "USD",
|
|
253
|
+
inputTokens: 56000,
|
|
254
|
+
outputTokens: 0,
|
|
255
|
+
cacheReadTokens: 0,
|
|
256
|
+
priceVersion: "v1",
|
|
257
|
+
priceEffective: "2026-07-01",
|
|
258
|
+
tierUpto: null,
|
|
259
|
+
tierInputPrice: 0.1,
|
|
260
|
+
tierOutputPrice: 0,
|
|
261
|
+
tierCacheReadPrice: null,
|
|
262
|
+
multiplier: 1.0,
|
|
263
|
+
multiplierRule: "—(未命中时段倍率,按 1.0 计)",
|
|
264
|
+
}),
|
|
265
|
+
derive({
|
|
266
|
+
id: "req-2d9b4e77",
|
|
267
|
+
minutesAgo: 3000,
|
|
268
|
+
application: "agent-platform",
|
|
269
|
+
clientName: "agent-app",
|
|
270
|
+
appKeyId: "gw-ak-7f3a2b1c",
|
|
271
|
+
alias: "coding",
|
|
272
|
+
modelCode: "gpt-5",
|
|
273
|
+
modelName: "GPT-5",
|
|
274
|
+
providerCode: "openai",
|
|
275
|
+
providerName: "OpenAI",
|
|
276
|
+
currency: "USD",
|
|
277
|
+
inputTokens: 3200,
|
|
278
|
+
outputTokens: 900,
|
|
279
|
+
cacheReadTokens: 12000,
|
|
280
|
+
priceVersion: "v1",
|
|
281
|
+
priceEffective: "2026-09-10",
|
|
282
|
+
tierUpto: null,
|
|
283
|
+
tierInputPrice: 0.4,
|
|
284
|
+
tierOutputPrice: 1.8,
|
|
285
|
+
tierCacheReadPrice: 0.1,
|
|
286
|
+
multiplier: 1.0,
|
|
287
|
+
multiplierRule: "—(未命中时段倍率,按 1.0 计)",
|
|
288
|
+
}),
|
|
289
|
+
derive({
|
|
290
|
+
id: "req-3e7c9a54",
|
|
291
|
+
minutesAgo: 5000,
|
|
292
|
+
application: "web-portal",
|
|
293
|
+
clientName: "web-portal",
|
|
294
|
+
appKeyId: "gw-ak-9c1d4e5f",
|
|
295
|
+
alias: "general",
|
|
296
|
+
modelCode: "gemini-3-flash",
|
|
297
|
+
modelName: "Gemini 3 Flash",
|
|
298
|
+
providerCode: "gemini",
|
|
299
|
+
providerName: "Google Gemini",
|
|
300
|
+
currency: "USD",
|
|
301
|
+
inputTokens: 8000,
|
|
302
|
+
outputTokens: 2200,
|
|
303
|
+
cacheReadTokens: 0,
|
|
304
|
+
priceVersion: "v1",
|
|
305
|
+
priceEffective: "2026-08-30",
|
|
306
|
+
tierUpto: 200000,
|
|
307
|
+
tierInputPrice: 0.15,
|
|
308
|
+
tierOutputPrice: 0.6,
|
|
309
|
+
tierCacheReadPrice: null,
|
|
310
|
+
multiplier: 1.0,
|
|
311
|
+
multiplierRule: "—(未命中时段倍率,按 1.0 计)",
|
|
312
|
+
}),
|
|
313
|
+
derive({
|
|
314
|
+
id: "req-0c4e2b89",
|
|
315
|
+
minutesAgo: 8200,
|
|
316
|
+
application: "mobile-app",
|
|
317
|
+
clientName: "legacy-mobile",
|
|
318
|
+
appKeyId: "gw-ak-5d7c9b2e",
|
|
319
|
+
alias: "chat-lite",
|
|
320
|
+
modelCode: "qwen3.5-flash",
|
|
321
|
+
modelName: "Qwen3.5 Flash",
|
|
322
|
+
providerCode: "dashscope",
|
|
323
|
+
providerName: "阿里云百炼",
|
|
324
|
+
currency: "USD",
|
|
325
|
+
inputTokens: 40000,
|
|
326
|
+
outputTokens: 3000,
|
|
327
|
+
cacheReadTokens: 0,
|
|
328
|
+
priceVersion: "v2",
|
|
329
|
+
priceEffective: "2026-08-15",
|
|
330
|
+
tierUpto: 128000,
|
|
331
|
+
tierInputPrice: 0.1,
|
|
332
|
+
tierOutputPrice: 0.4,
|
|
333
|
+
tierCacheReadPrice: null,
|
|
334
|
+
multiplier: 0.9,
|
|
335
|
+
multiplierRule: "每天 00:00–08:00 ×0.9",
|
|
336
|
+
}),
|
|
337
|
+
derive({
|
|
338
|
+
id: "req-d8b1e473",
|
|
339
|
+
minutesAgo: 13000,
|
|
340
|
+
application: "agent-platform",
|
|
341
|
+
clientName: "agent-app",
|
|
342
|
+
appKeyId: "gw-ak-7f3a2b1c",
|
|
343
|
+
alias: "reasoning",
|
|
344
|
+
modelCode: "qwen3-vl-plus",
|
|
345
|
+
modelName: "Qwen3-VL Plus",
|
|
346
|
+
providerCode: "dashscope",
|
|
347
|
+
providerName: "阿里云百炼",
|
|
348
|
+
currency: "USD",
|
|
349
|
+
inputTokens: 45000,
|
|
350
|
+
outputTokens: 6000,
|
|
351
|
+
cacheReadTokens: 0,
|
|
352
|
+
priceVersion: "v3",
|
|
353
|
+
priceEffective: "2026-09-01",
|
|
354
|
+
tierUpto: 128000,
|
|
355
|
+
tierInputPrice: 0.4,
|
|
356
|
+
tierOutputPrice: 3.2,
|
|
357
|
+
tierCacheReadPrice: null,
|
|
358
|
+
multiplier: 1.2,
|
|
359
|
+
multiplierRule: "工作日 09:00–18:00 ×1.2",
|
|
360
|
+
}),
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
/** Model 筛选选项:全部 + 数据中出现过的模型(按名称去重) */
|
|
364
|
+
export const COST_MODEL_OPTIONS: { value: string; label: string }[] = [
|
|
365
|
+
{ value: "all", label: "全部模型" },
|
|
366
|
+
...[...new Map(COSTS.map((c) => [c.modelCode, c.modelName])).entries()].map(
|
|
367
|
+
([value, label]) => ({ value, label }),
|
|
368
|
+
),
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
/** Provider 筛选选项:全部 + 数据中出现过的供应商 */
|
|
372
|
+
export const COST_PROVIDER_OPTIONS: { value: string; label: string }[] = [
|
|
373
|
+
{ value: "all", label: "全部供应商" },
|
|
374
|
+
...[
|
|
375
|
+
...new Map(COSTS.map((c) => [c.providerCode, c.providerName])).entries(),
|
|
376
|
+
].map(([value, label]) => ({ value, label })),
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
/** cost.list:时间区间(按日期含端点)+ Application/AppKey/Alias 模糊 +
|
|
380
|
+
* Model/Provider 单选,按时间倒序 */
|
|
381
|
+
export function queryCosts(params: CostListParams = {}): CostListResult {
|
|
382
|
+
const application = (params.application ?? "").trim().toLowerCase();
|
|
383
|
+
const appKey = (params.appKey ?? "").trim().toLowerCase();
|
|
384
|
+
const alias = (params.alias ?? "").trim().toLowerCase();
|
|
385
|
+
const model = params.model ?? "all";
|
|
386
|
+
const provider = params.provider ?? "all";
|
|
387
|
+
const items = COSTS.filter(
|
|
388
|
+
(c) =>
|
|
389
|
+
(!params.start || c.time.slice(0, 10) >= params.start) &&
|
|
390
|
+
(!params.end || c.time.slice(0, 10) <= params.end) &&
|
|
391
|
+
(application === "" || c.application.toLowerCase().includes(application)) &&
|
|
392
|
+
(appKey === "" || c.appKeyId.toLowerCase().includes(appKey)) &&
|
|
393
|
+
(alias === "" || c.alias.toLowerCase().includes(alias)) &&
|
|
394
|
+
(model === "all" || c.modelCode === model) &&
|
|
395
|
+
(provider === "all" || c.providerCode === provider),
|
|
396
|
+
).sort((a, b) => b.time.localeCompare(a.time));
|
|
397
|
+
return { items, total: COSTS.length };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** cost.detail:按请求 id 取详情;未命中返回 undefined(页面做空态) */
|
|
401
|
+
export function getCost(id: string): CostRecord | undefined {
|
|
402
|
+
return COSTS.find((c) => c.id === id);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 全量请求 id(静态导出 generateStaticParams 用) */
|
|
406
|
+
export const COST_IDS: string[] = COSTS.map((c) => c.id);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard 演示数据服务(MVP mock)。
|
|
3
|
+
* queryDashboard(start, end) 按任意日期区间确定性生成看板数据,
|
|
4
|
+
* 注册为 Pactor service(dashboard.query);接入真实后端后替换为
|
|
5
|
+
* http / service 数据源,DSL 绑定路径保持不变。
|
|
6
|
+
*
|
|
7
|
+
* 时间桶规则(对齐 PRD §2):≤2 天 → 2h 桶;3 天 → 6h 桶;
|
|
8
|
+
* ≤16 天 → 天桶;更长 → 约 15 个多天桶。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface TrendPoint {
|
|
12
|
+
t: string;
|
|
13
|
+
requests: number;
|
|
14
|
+
tokens: number;
|
|
15
|
+
cost: number;
|
|
16
|
+
latency: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DistributionItem {
|
|
20
|
+
name: string;
|
|
21
|
+
percent: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DashboardStats {
|
|
25
|
+
requests: string;
|
|
26
|
+
activeClients: number;
|
|
27
|
+
tokens: string;
|
|
28
|
+
successRate: string;
|
|
29
|
+
failures: string;
|
|
30
|
+
errorRate: string;
|
|
31
|
+
avgLatency: number;
|
|
32
|
+
cost: string;
|
|
33
|
+
avgCost: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DashboardData {
|
|
37
|
+
realtime: { rpm: number; tpm: string };
|
|
38
|
+
/** Chart yKeys 需要数组,而表达式不支持数组字面量 → 预置映射按指标取 */
|
|
39
|
+
metricKeys: Record<string, string[]>;
|
|
40
|
+
stats: DashboardStats;
|
|
41
|
+
trend: TrendPoint[];
|
|
42
|
+
models: DistributionItem[];
|
|
43
|
+
clients: DistributionItem[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DAY_MS = 86400000;
|
|
47
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
48
|
+
|
|
49
|
+
/** 本地日期 → ISO yyyy-MM-dd(与 DatePicker 的 format 约定一致) */
|
|
50
|
+
export function isoDate(d: Date): string {
|
|
51
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 默认分析范围:最近 7 天(含今天) */
|
|
55
|
+
export function defaultRange(): { start: string; end: string } {
|
|
56
|
+
const end = new Date();
|
|
57
|
+
const start = new Date(end.getTime() - 6 * DAY_MS);
|
|
58
|
+
return { start: isoDate(start), end: isoDate(end) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function monthDay(d: Date): string {
|
|
62
|
+
return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 确定性伪随机(同一区间多次调用结果一致) */
|
|
66
|
+
function seededRandom(seedText: string): () => number {
|
|
67
|
+
let a = 0;
|
|
68
|
+
for (const ch of seedText) a = (a * 31 + ch.charCodeAt(0)) | 0;
|
|
69
|
+
return () => {
|
|
70
|
+
a |= 0;
|
|
71
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
72
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
73
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
74
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fmt(n: number): string {
|
|
79
|
+
return n.toLocaleString("en-US");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const MODEL_SHARES: Array<[string, number]> = [
|
|
83
|
+
["gpt-4o", 38],
|
|
84
|
+
["reasoning-v2", 28],
|
|
85
|
+
["claude-3.5", 18],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const CLIENT_SHARES: Array<[string, number]> = [
|
|
89
|
+
["app-mobile", 42],
|
|
90
|
+
["app-web", 31],
|
|
91
|
+
["internal-api", 16],
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function buildDistribution(
|
|
95
|
+
base: Array<[string, number]>,
|
|
96
|
+
rand: () => number,
|
|
97
|
+
): DistributionItem[] {
|
|
98
|
+
const items = base.map(([name, share]) => ({
|
|
99
|
+
name,
|
|
100
|
+
percent: Math.max(1, Math.round(share * (0.85 + rand() * 0.3))),
|
|
101
|
+
}));
|
|
102
|
+
items.sort((a, b) => b.percent - a.percent);
|
|
103
|
+
const top = items.reduce((sum, it) => sum + it.percent, 0);
|
|
104
|
+
items.push({ name: "其他", percent: Math.max(0, 100 - top) });
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 时间桶标签序列:按区间长度选择粒度(PRD:1~3 天小时桶,7~14 天 6h/天,30 天/周/月 天) */
|
|
109
|
+
function bucketLabels(start: Date, days: number): string[] {
|
|
110
|
+
if (days <= 2) {
|
|
111
|
+
return Array.from({ length: 12 }, (_, i) => `${pad(i * 2)}:00`);
|
|
112
|
+
}
|
|
113
|
+
if (days === 3) {
|
|
114
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
115
|
+
const d = new Date(start.getTime() + Math.floor(i / 4) * DAY_MS);
|
|
116
|
+
return `${monthDay(d)} ${(i % 4) * 6}:00`;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (days <= 16) {
|
|
120
|
+
return Array.from({ length: days }, (_, i) =>
|
|
121
|
+
monthDay(new Date(start.getTime() + i * DAY_MS)),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const step = Math.ceil(days / 15);
|
|
125
|
+
const labels: string[] = [];
|
|
126
|
+
for (let i = 0; i < days; i += step) {
|
|
127
|
+
labels.push(monthDay(new Date(start.getTime() + i * DAY_MS)));
|
|
128
|
+
}
|
|
129
|
+
return labels;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function queryDashboard(start: string, end: string): DashboardData {
|
|
133
|
+
const rand = seededRandom(`${start}|${end}`);
|
|
134
|
+
const startDate = new Date(`${start}T00:00:00`);
|
|
135
|
+
const endDate = new Date(`${end}T00:00:00`);
|
|
136
|
+
const valid =
|
|
137
|
+
!Number.isNaN(startDate.getTime()) && !Number.isNaN(endDate.getTime());
|
|
138
|
+
const days = valid
|
|
139
|
+
? Math.min(
|
|
140
|
+
62,
|
|
141
|
+
Math.max(1, Math.round((endDate.getTime() - startDate.getTime()) / DAY_MS) + 1),
|
|
142
|
+
)
|
|
143
|
+
: 7;
|
|
144
|
+
const base = valid ? startDate : new Date(Date.now() - 6 * DAY_MS);
|
|
145
|
+
|
|
146
|
+
const labels = bucketLabels(base, days);
|
|
147
|
+
const bucketBase = (days * 18000) / labels.length;
|
|
148
|
+
|
|
149
|
+
const trend: TrendPoint[] = labels.map((t) => {
|
|
150
|
+
const requests = Math.round(bucketBase * (0.6 + rand() * 0.8));
|
|
151
|
+
return {
|
|
152
|
+
t,
|
|
153
|
+
requests,
|
|
154
|
+
tokens: Number(((requests * (320 + rand() * 60)) / 1_000_000).toFixed(2)),
|
|
155
|
+
cost: Number((requests * 0.0069 * (0.9 + rand() * 0.2)).toFixed(2)),
|
|
156
|
+
latency: Math.round(280 + rand() * 140),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const totalRequests = trend.reduce((s, p) => s + p.requests, 0);
|
|
161
|
+
const totalTokens = trend.reduce((s, p) => s + p.tokens, 0);
|
|
162
|
+
const totalCost = trend.reduce((s, p) => s + p.cost, 0);
|
|
163
|
+
const avgLatency = Math.round(
|
|
164
|
+
trend.reduce((s, p) => s + p.latency, 0) / trend.length,
|
|
165
|
+
);
|
|
166
|
+
const successRate = 97 + rand() * 2.8;
|
|
167
|
+
const failures = Math.round(totalRequests * (1 - successRate / 100));
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
realtime: { rpm: 128, tpm: "2.1K" },
|
|
171
|
+
metricKeys: {
|
|
172
|
+
requests: ["requests"],
|
|
173
|
+
tokens: ["tokens"],
|
|
174
|
+
cost: ["cost"],
|
|
175
|
+
latency: ["latency"],
|
|
176
|
+
},
|
|
177
|
+
stats: {
|
|
178
|
+
requests: fmt(totalRequests),
|
|
179
|
+
activeClients: 8 + Math.floor(rand() * 10),
|
|
180
|
+
tokens: `${totalTokens.toFixed(1)}M`,
|
|
181
|
+
successRate: successRate.toFixed(1),
|
|
182
|
+
failures: fmt(failures),
|
|
183
|
+
errorRate: (100 - successRate).toFixed(2),
|
|
184
|
+
avgLatency,
|
|
185
|
+
cost: `$${totalCost.toFixed(4)}`,
|
|
186
|
+
avgCost: `$${(totalCost / totalRequests).toFixed(4)}`,
|
|
187
|
+
},
|
|
188
|
+
trend,
|
|
189
|
+
models: buildDistribution(MODEL_SHARES, rand),
|
|
190
|
+
clients: buildDistribution(CLIENT_SHARES, rand),
|
|
191
|
+
};
|
|
192
|
+
}
|