@pactor-app/skills 1.34.0 → 1.40.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/package.json +1 -1
- package/pactor-dsl/SKILL.md +28 -1
- package/pactor-dsl/references/dsl-authoring.md +174 -36
- package/pactor-prd/SKILL.md +20 -21
- package/pactor-prd/references/acceptance-checklist.md +1 -1
- package/pactor-prd/references/component-mapping.md +1 -1
- package/pactor-prd/references/data-api.md +37 -0
- package/pactor-prd/references/incremental-wireframe-delta.md +8 -9
- package/pactor-prd/references/page-inventory-and-files.md +2 -2
- package/pactor-prd/references/page-templates.md +59 -12
- package/pactor-prd/references/project-structure.md +1 -1
- package/pactor-prd/template/README.md +4 -4
- package/pactor-prd/template/app/api/d/[...parts]/route.ts +80 -0
- package/pactor-prd/template/app/layout.tsx +1 -3
- package/pactor-prd/template/next.config.js +2 -2
- package/pactor-prd/template/pnpm-lock.yaml +97 -97
- package/pactor-prd/template/src/pactor/App.tsx +3 -2
- package/pactor-prd/template/src/pactor/actions.ts +5 -431
- package/pactor-prd/template/src/pactor/components/index.ts +1 -4
- package/pactor-prd/template/src/pactor/config.ts +12 -4
- package/pactor-prd/template/src/server/yaml/common/dashboard.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/common/login.yaml +46 -75
- package/pactor-prd/template/src/server/yaml/models/data-list.yaml +4 -4
- package/pactor-prd/template/src/server/yaml/provider/provider-detail.yaml +2 -2
- package/pactor-prd/template/src/server/yaml/provider/provider-edit.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/provider/provider-list.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/route/route-detail.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/route/route-edit.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/route/route-list.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/sitemap.yaml +2 -6
- package/pactor-prd/references/mock-data-routes.md +0 -66
- package/pactor-prd/template/app/api/data/[...parts]/route.ts +0 -160
- package/pactor-prd/template/app/login.css +0 -122
- package/pactor-prd/template/src/pactor/components/chip-filter.tsx +0 -89
- package/pactor-prd/template/src/pactor/components/chip-node.tsx +0 -46
- package/pactor-prd/template/src/pactor/mocks/audit-mock.ts +0 -403
- package/pactor-prd/template/src/pactor/mocks/call-logs-mock.ts +0 -875
- package/pactor-prd/template/src/pactor/mocks/client-debug-mock.ts +0 -188
- package/pactor-prd/template/src/pactor/mocks/client-templates-mock.ts +0 -497
- package/pactor-prd/template/src/pactor/mocks/clients-mock.ts +0 -483
- package/pactor-prd/template/src/pactor/mocks/costs-mock.ts +0 -406
- package/pactor-prd/template/src/pactor/mocks/dashboard-mock.ts +0 -192
- package/pactor-prd/template/src/pactor/mocks/data-list-mock.ts +0 -84
- package/pactor-prd/template/src/pactor/mocks/model-debug-mock.ts +0 -144
- package/pactor-prd/template/src/pactor/mocks/models-mock.ts +0 -799
- package/pactor-prd/template/src/pactor/mocks/ops-usage-mock.ts +0 -806
- package/pactor-prd/template/src/pactor/mocks/providers-mock.ts +0 -424
- package/pactor-prd/template/src/pactor/mocks/routes-mock.ts +0 -604
- package/pactor-prd/template/src/pactor/services.ts +0 -200
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 客户端调试(调试台)演示数据与调用模拟(MVP mock,静态页面)。
|
|
3
|
-
* 对齐 PRD《调试台/01-客户端调试》:以指定客户端走 AppKey → Route → Runtime
|
|
4
|
-
* 完整业务链路,返回请求/返回报文与运行摘要(状态码、延迟、TTFT、命中路由组、
|
|
5
|
-
* Token 用量、配额扣减预览、限流检查结果)。
|
|
6
|
-
* 身份选项直接复用访问治理 mock(clients-mock / routes-mock);接入真实
|
|
7
|
-
* Gateway 后替换为 http 调用,组件 props 契约不变。
|
|
8
|
-
* 安全约束(PRD 交互规则 4):不展示、不持久化 AppKey Secret / Provider Credential 明文。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { getClient, queryClients } from "./clients-mock";
|
|
12
|
-
import { getRoute } from "./routes-mock";
|
|
13
|
-
|
|
14
|
-
export interface DebugIdentity {
|
|
15
|
-
clientCode: string;
|
|
16
|
-
appKeyId: string;
|
|
17
|
-
routeCode: string;
|
|
18
|
-
channel: string;
|
|
19
|
-
routeTag: string;
|
|
20
|
-
user: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 一轮调用的完整执行明细(右侧「执行明细」面板数据源) */
|
|
24
|
-
export interface ClientDebugRound {
|
|
25
|
-
id: string;
|
|
26
|
-
/** 第 N 轮(从 1 起) */
|
|
27
|
-
seq: number;
|
|
28
|
-
/** 实际发往 Gateway 的 HTTP Body(pretty JSON 文本) */
|
|
29
|
-
requestJson: string;
|
|
30
|
-
/** 请求报文压缩形态(「压缩」切换) */
|
|
31
|
-
requestCompact: string;
|
|
32
|
-
/** Gateway 返回的完整 Response Body(流式合并后的最终 JSON,pretty 文本) */
|
|
33
|
-
responseJson: string;
|
|
34
|
-
/** 返回报文压缩形态 */
|
|
35
|
-
responseCompact: string;
|
|
36
|
-
status: number;
|
|
37
|
-
latencyMs: number;
|
|
38
|
-
ttftMs: number;
|
|
39
|
-
/** 命中路由组(RouteTag 命中;未指定时取路由首个标签) */
|
|
40
|
-
routeGroup: string;
|
|
41
|
-
/** 命中 Target(模型 · 供应商摘要) */
|
|
42
|
-
target: string;
|
|
43
|
-
tokensIn: number;
|
|
44
|
-
tokensOut: number;
|
|
45
|
-
/** 配额扣减预览文案 */
|
|
46
|
-
quotaPreview: string;
|
|
47
|
-
/** 限流检查结果文案 */
|
|
48
|
-
rateLimitResult: string;
|
|
49
|
-
requestId: string;
|
|
50
|
-
/** assistant 回复全文(流式模拟的目标文本) */
|
|
51
|
-
reply: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 客户端选项:仅启用中的客户端可调试 */
|
|
55
|
-
export const DEBUG_CLIENT_OPTIONS: { value: string; label: string }[] =
|
|
56
|
-
queryClients({ status: "enabled" }).items.map((c) => ({
|
|
57
|
-
value: c.code,
|
|
58
|
-
label: `${c.name}(${c.code})`,
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
/** 客户端下的 AppKey 选项(mock 每客户端一把;Secret 不持久化,仅会话内使用) */
|
|
62
|
-
export function appKeyOptionsOf(clientCode: string) {
|
|
63
|
-
const c = getClient(clientCode);
|
|
64
|
-
return c ? [{ value: c.appKeyId, label: c.appKeyId }] : [];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 客户端已授权路由选项 */
|
|
68
|
-
export function routeOptionsOf(clientCode: string) {
|
|
69
|
-
const c = getClient(clientCode);
|
|
70
|
-
return (c?.authRoutes ?? []).map((r) => ({
|
|
71
|
-
value: r.code,
|
|
72
|
-
label: `${r.name}(${r.alias})`,
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Channel 调用上下文选项 */
|
|
77
|
-
export const CHANNEL_OPTIONS = [
|
|
78
|
-
{ value: "WEB", label: "WEB" },
|
|
79
|
-
{ value: "APP", label: "APP" },
|
|
80
|
-
{ value: "SERVER", label: "SERVER" },
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
/** RouteTag 选项:所选路由的标签(参与路由策略匹配,可为空) */
|
|
84
|
-
export function routeTagOptionsOf(routeCode: string) {
|
|
85
|
-
const r = getRoute(routeCode);
|
|
86
|
-
return (r?.tags ?? []).map((t) => ({ value: t, label: t }));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function hexId(prefix: string) {
|
|
90
|
-
return `${prefix}-${Math.random().toString(16).slice(2, 10).padEnd(8, "0")}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** 估算 token(演示口径:按字符数近似) */
|
|
94
|
-
function roughTokens(text: string) {
|
|
95
|
-
return Math.max(8, Math.round(text.length / 2));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 模拟一次完整链路调用:鉴权 → 路由命中 → 配额/限流检查 → 上游生成。
|
|
100
|
-
* 返回该轮的请求/返回报文与运行摘要;reply 供左侧流式展示。
|
|
101
|
-
*/
|
|
102
|
-
export function simulateClientCall(
|
|
103
|
-
identity: DebugIdentity,
|
|
104
|
-
userText: string,
|
|
105
|
-
seq: number,
|
|
106
|
-
): ClientDebugRound {
|
|
107
|
-
const client = getClient(identity.clientCode);
|
|
108
|
-
const route = getRoute(identity.routeCode);
|
|
109
|
-
const target = route?.models.find((m) => m.status === "enabled") ??
|
|
110
|
-
route?.models[0];
|
|
111
|
-
const routeGroup =
|
|
112
|
-
identity.routeTag || route?.tags[0] || route?.name || "default";
|
|
113
|
-
const tokensIn = roughTokens(userText) + 12;
|
|
114
|
-
const reply = [
|
|
115
|
-
`已收到你的请求(via ${client?.name ?? identity.clientCode} → ${route?.name ?? identity.routeCode})。`,
|
|
116
|
-
"",
|
|
117
|
-
`要点如下:`,
|
|
118
|
-
`1. 鉴权通过:AppKey ${identity.appKeyId} 有效;`,
|
|
119
|
-
`2. 路由命中:路由组 ${routeGroup},Target ${target?.modelName ?? "—"};`,
|
|
120
|
-
`3. 配额与限流检查通过,本次调用已计入用量。`,
|
|
121
|
-
"",
|
|
122
|
-
`你刚才说的是:「${userText.length > 40 ? `${userText.slice(0, 40)}…` : userText}」`,
|
|
123
|
-
].join("\n");
|
|
124
|
-
const tokensOut = roughTokens(reply);
|
|
125
|
-
const ttftMs = 120 + Math.floor(Math.random() * 120);
|
|
126
|
-
const latencyMs = ttftMs + 100 + Math.floor(Math.random() * 200);
|
|
127
|
-
const created = Math.floor(Date.now() / 1000);
|
|
128
|
-
|
|
129
|
-
const requestObj = {
|
|
130
|
-
model: route?.alias || route?.name || identity.routeCode,
|
|
131
|
-
messages: [{ role: "user", content: userText }],
|
|
132
|
-
stream: true,
|
|
133
|
-
user: identity.user || undefined,
|
|
134
|
-
metadata: {
|
|
135
|
-
app_key_id: identity.appKeyId,
|
|
136
|
-
channel: identity.channel || undefined,
|
|
137
|
-
route_tag: identity.routeTag || undefined,
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const responseObj = {
|
|
142
|
-
id: `chatcmpl-${hexId("").slice(1)}`,
|
|
143
|
-
object: "chat.completion",
|
|
144
|
-
created,
|
|
145
|
-
model: route?.alias || route?.name || identity.routeCode,
|
|
146
|
-
choices: [
|
|
147
|
-
{
|
|
148
|
-
index: 0,
|
|
149
|
-
message: { role: "assistant", content: reply },
|
|
150
|
-
finish_reason: "stop",
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
usage: {
|
|
154
|
-
prompt_tokens: tokensIn,
|
|
155
|
-
completion_tokens: tokensOut,
|
|
156
|
-
total_tokens: tokensIn + tokensOut,
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const quota = client?.quotas.find((q) => q.status === "enabled");
|
|
161
|
-
const rate = client?.rateLimits.find((r) => r.status === "enabled");
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
id: hexId("round"),
|
|
165
|
-
seq,
|
|
166
|
-
requestJson: JSON.stringify(requestObj, null, 2),
|
|
167
|
-
requestCompact: JSON.stringify(requestObj),
|
|
168
|
-
responseJson: JSON.stringify(responseObj, null, 2),
|
|
169
|
-
responseCompact: JSON.stringify(responseObj),
|
|
170
|
-
status: 200,
|
|
171
|
-
latencyMs,
|
|
172
|
-
ttftMs,
|
|
173
|
-
routeGroup,
|
|
174
|
-
target: target
|
|
175
|
-
? `${target.modelName} · ${target.providerSummary}`
|
|
176
|
-
: "—",
|
|
177
|
-
tokensIn,
|
|
178
|
-
tokensOut,
|
|
179
|
-
quotaPreview: quota
|
|
180
|
-
? `${quota.quotaTypeLabel}「${quota.name}」扣减 ${tokensIn + tokensOut} ${quota.metricLabel}(${quota.modeLabel})`
|
|
181
|
-
: "未配置配额策略",
|
|
182
|
-
rateLimitResult: rate
|
|
183
|
-
? `通过(${rate.limitTypeLabel} 上限 ${rate.limitText}${rate.burst ? `,Burst ${rate.burst}` : ""})`
|
|
184
|
-
: "未配置限流策略",
|
|
185
|
-
requestId: hexId("req"),
|
|
186
|
-
reply,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 客户端模板(访问治理)演示数据服务(MVP mock,静态页面)。
|
|
3
|
-
* clientTemplate.list / clientTemplate.detail 注册为 Pactor service;接入真实后端后
|
|
4
|
-
* 替换为 http / service 数据源,DSL 绑定路径保持不变。
|
|
5
|
-
* 数据形态对齐 PRD《访问治理/Client-Template/01-列表页》《02-详情页》与
|
|
6
|
-
* 《Client/02-客户端详情页》③路由授权 / ④配额策略 / ⑤限流策略 线框图。
|
|
7
|
-
* 共享子类型(AuthRoute / QuotaRule / RateLimitRule)在本文件定义并导出,
|
|
8
|
-
* clients-mock 从这里 import(避免与 routes-mock 形成循环依赖)。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { getRoute } from "./routes-mock";
|
|
12
|
-
|
|
13
|
-
export type ClientTemplateStatus = "enabled" | "disabled";
|
|
14
|
-
export type ValidityType = "days" | "fixed" | "never";
|
|
15
|
-
export type QuotaType = "lifetime" | "cyclic";
|
|
16
|
-
export type QuotaMetric =
|
|
17
|
-
| "REQUEST"
|
|
18
|
-
| "INPUT_TOKEN"
|
|
19
|
-
| "OUTPUT_TOKEN"
|
|
20
|
-
| "TOTAL_TOKEN"
|
|
21
|
-
| "COST"
|
|
22
|
-
| "CONCURRENT";
|
|
23
|
-
export type QuotaMode = "soft" | "hard";
|
|
24
|
-
export type CycleUnit = "hour" | "day" | "month";
|
|
25
|
-
export type RateLimitType = "RPM" | "TPM" | "CONCURRENT";
|
|
26
|
-
export type RuleStatus = "enabled" | "disabled";
|
|
27
|
-
|
|
28
|
-
/** 授权路由快照:引用 routes-mock 中已启用路由的展示字段(code/name/alias/typeLabel/path) */
|
|
29
|
-
export interface AuthRoute {
|
|
30
|
-
code: string;
|
|
31
|
-
name: string;
|
|
32
|
-
alias: string;
|
|
33
|
-
typeLabel: string;
|
|
34
|
-
path: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 配额规则(模板仅配置规则;客户端另带用量,见 clients-mock ClientQuotaRule) */
|
|
38
|
-
export interface QuotaRule {
|
|
39
|
-
/** 规则名称,策略内唯一 */
|
|
40
|
-
name: string;
|
|
41
|
-
quotaType: QuotaType;
|
|
42
|
-
/** 总配额 / 周期循环配额 */
|
|
43
|
-
quotaTypeLabel: string;
|
|
44
|
-
metric: QuotaMetric;
|
|
45
|
-
metricLabel: string;
|
|
46
|
-
limit: number;
|
|
47
|
-
/** 千分位上限文案(表达式不适合做数字格式化,derive 预计算) */
|
|
48
|
-
limitText: string;
|
|
49
|
-
mode: QuotaMode;
|
|
50
|
-
/** Soft Limit / Hard Limit */
|
|
51
|
-
modeLabel: string;
|
|
52
|
-
/** 循环周期(仅 quotaType=cyclic):每 X 小时 / 每 X 天 / 每月(自然月) */
|
|
53
|
-
cycleUnit?: CycleUnit;
|
|
54
|
-
cycleValue?: number;
|
|
55
|
-
/** 如「每 24 小时」「每 7 天」「每月(自然月)」 */
|
|
56
|
-
cycleLabel?: string;
|
|
57
|
-
status: RuleStatus;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 限流规则 */
|
|
61
|
-
export interface RateLimitRule {
|
|
62
|
-
name: string;
|
|
63
|
-
limitType: RateLimitType;
|
|
64
|
-
/** 如「RPM(每分钟请求数)」 */
|
|
65
|
-
limitTypeLabel: string;
|
|
66
|
-
limit: number;
|
|
67
|
-
/** 千分位上限文案 */
|
|
68
|
-
limitText: string;
|
|
69
|
-
/** 突发容量(RPM/TPM 可选;Concurrent 无) */
|
|
70
|
-
burst?: number;
|
|
71
|
-
/** true = 全部授权路由;false 时 scopeRoutes 为指定路由 code 列表 */
|
|
72
|
-
scopeAll: boolean;
|
|
73
|
-
scopeRoutes?: string[];
|
|
74
|
-
/** 「全部授权路由」或路由名列表 */
|
|
75
|
-
scopeLabel: string;
|
|
76
|
-
status: RuleStatus;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface ClientTemplateRecord {
|
|
80
|
-
code: string;
|
|
81
|
-
name: string;
|
|
82
|
-
tags: string[];
|
|
83
|
-
description: string;
|
|
84
|
-
status: ClientTemplateStatus;
|
|
85
|
-
statusLabel: string;
|
|
86
|
-
/** 有效期规则:创建后 N 天 / 固定日期 / 永不过期 */
|
|
87
|
-
validityType: ValidityType;
|
|
88
|
-
validityDays?: number;
|
|
89
|
-
validityFixedDate?: string;
|
|
90
|
-
/** 如「创建后 365 天」「固定至 2027-12-31」「永不过期」 */
|
|
91
|
-
validityLabel: string;
|
|
92
|
-
authRoutes: AuthRoute[];
|
|
93
|
-
quotas: QuotaRule[];
|
|
94
|
-
rateLimits: RateLimitRule[];
|
|
95
|
-
/** 基于本模板创建的客户端数 */
|
|
96
|
-
createdClientCount: number;
|
|
97
|
-
createdAt: string;
|
|
98
|
-
updatedAt: string;
|
|
99
|
-
operator: string;
|
|
100
|
-
// ── 预计算摘要(表达式不适合拼接复合文案) ──
|
|
101
|
-
/** 授权路由数 */
|
|
102
|
-
routeCount: number;
|
|
103
|
-
/** 配额摘要,如「总+日循环」 */
|
|
104
|
-
quotaSummary: string;
|
|
105
|
-
/** 限流摘要,如「1000 RPM」 */
|
|
106
|
-
rateLimitSummary: string;
|
|
107
|
-
tagsText: string;
|
|
108
|
-
/** 策略摘要,如「3 路由 · 总+日循环 · 1000 RPM」 */
|
|
109
|
-
policySummary: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface ClientTemplateListParams {
|
|
113
|
-
/** all | enabled | disabled */
|
|
114
|
-
status?: string;
|
|
115
|
-
/** 名称 / 标识模糊匹配 */
|
|
116
|
-
keyword?: string;
|
|
117
|
-
/** 标签多选(取交集) */
|
|
118
|
-
tags?: string[];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 快筛 Chip 计数(数据构成口径,不随筛选变化) */
|
|
122
|
-
export interface ClientTemplateCounts {
|
|
123
|
-
all: { enabled: number; total: number };
|
|
124
|
-
status: Record<ClientTemplateStatus, number>;
|
|
125
|
-
/** 键为标签的表达式安全形式(连字符转下划线,如 team-default → team_default) */
|
|
126
|
-
tags: Record<string, number>;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 快筛 Chip 选项(ChipFilter 组件入项;文案在接口侧预计算) */
|
|
130
|
-
export interface ClientTemplateChipOption {
|
|
131
|
-
value: string;
|
|
132
|
-
text: string;
|
|
133
|
-
dot?: string;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface ClientTemplateListResult {
|
|
137
|
-
items: ClientTemplateRecord[];
|
|
138
|
-
total: number;
|
|
139
|
-
counts: ClientTemplateCounts;
|
|
140
|
-
/** 快筛 Chip 组:status 单选(含「全部」),tags 多选(含「全部」,点击清空) */
|
|
141
|
-
filters: {
|
|
142
|
-
status: ClientTemplateChipOption[];
|
|
143
|
-
tags: ClientTemplateChipOption[];
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export const TEMPLATE_STATUS_LABELS: Record<ClientTemplateStatus, string> = {
|
|
148
|
-
enabled: "启用",
|
|
149
|
-
disabled: "停用",
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
export const QUOTA_TYPE_LABELS: Record<QuotaType, string> = {
|
|
153
|
-
lifetime: "总配额",
|
|
154
|
-
cyclic: "周期循环配额",
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
export const QUOTA_METRIC_LABELS: Record<QuotaMetric, string> = {
|
|
158
|
-
REQUEST: "Request",
|
|
159
|
-
INPUT_TOKEN: "Input Token",
|
|
160
|
-
OUTPUT_TOKEN: "Output Token",
|
|
161
|
-
TOTAL_TOKEN: "Total Token",
|
|
162
|
-
COST: "Cost",
|
|
163
|
-
CONCURRENT: "Concurrent",
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
export const QUOTA_MODE_LABELS: Record<QuotaMode, string> = {
|
|
167
|
-
soft: "Soft Limit",
|
|
168
|
-
hard: "Hard Limit",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
export const RATE_LIMIT_TYPE_LABELS: Record<RateLimitType, string> = {
|
|
172
|
-
RPM: "RPM(每分钟请求数)",
|
|
173
|
-
TPM: "TPM(每分钟 Token 数)",
|
|
174
|
-
CONCURRENT: "Concurrent(并发连接数)",
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/** 千分位数字文案(10,000,000) */
|
|
178
|
-
export function thousands(n: number): string {
|
|
179
|
-
return n.toLocaleString("en-US");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** 授权路由快照:按 code 引用 routes-mock 的已启用路由(clients-mock 复用) */
|
|
183
|
-
export function authRouteSnapshot(code: string): AuthRoute {
|
|
184
|
-
const r = getRoute(code);
|
|
185
|
-
if (!r) throw new Error(`client-templates-mock: 未知路由 ${code}`);
|
|
186
|
-
return {
|
|
187
|
-
code: r.code,
|
|
188
|
-
name: r.name,
|
|
189
|
-
alias: r.alias,
|
|
190
|
-
typeLabel: r.typeLabel,
|
|
191
|
-
path: r.path,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** 循环周期文案(仅周期循环配额) */
|
|
196
|
-
function cycleLabel(unit: CycleUnit, value?: number): string {
|
|
197
|
-
if (unit === "month") return "每月(自然月)";
|
|
198
|
-
if (unit === "day") return `每 ${value ?? 1} 天`;
|
|
199
|
-
return `每 ${value ?? 24} 小时`;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** 配额规则展示字段派生 */
|
|
203
|
-
function deriveQuota(
|
|
204
|
-
rule: Omit<
|
|
205
|
-
QuotaRule,
|
|
206
|
-
"quotaTypeLabel" | "metricLabel" | "limitText" | "modeLabel" | "cycleLabel"
|
|
207
|
-
>,
|
|
208
|
-
): QuotaRule {
|
|
209
|
-
return {
|
|
210
|
-
...rule,
|
|
211
|
-
quotaTypeLabel: QUOTA_TYPE_LABELS[rule.quotaType],
|
|
212
|
-
metricLabel: QUOTA_METRIC_LABELS[rule.metric],
|
|
213
|
-
limitText: thousands(rule.limit),
|
|
214
|
-
modeLabel: QUOTA_MODE_LABELS[rule.mode],
|
|
215
|
-
cycleLabel:
|
|
216
|
-
rule.quotaType === "cyclic" && rule.cycleUnit
|
|
217
|
-
? cycleLabel(rule.cycleUnit, rule.cycleValue)
|
|
218
|
-
: undefined,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** 限流规则展示字段派生 */
|
|
223
|
-
function deriveRateLimit(
|
|
224
|
-
rule: Omit<RateLimitRule, "limitTypeLabel" | "limitText" | "scopeLabel"> & {
|
|
225
|
-
scopeRouteNames?: string[];
|
|
226
|
-
},
|
|
227
|
-
): RateLimitRule {
|
|
228
|
-
return {
|
|
229
|
-
...rule,
|
|
230
|
-
limitTypeLabel: RATE_LIMIT_TYPE_LABELS[rule.limitType],
|
|
231
|
-
limitText: thousands(rule.limit),
|
|
232
|
-
scopeLabel: rule.scopeAll
|
|
233
|
-
? "全部授权路由"
|
|
234
|
-
: (rule.scopeRouteNames ?? []).join("、") || "—",
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** 配额摘要:总+日循环 / 总+周循环 / 每7天循环 …… */
|
|
239
|
-
export function quotaSummaryOf(quotas: QuotaRule[]): string {
|
|
240
|
-
const parts = quotas.map((q) => {
|
|
241
|
-
if (q.quotaType === "lifetime") return "总";
|
|
242
|
-
if (q.cycleUnit === "month") return "月循环";
|
|
243
|
-
if (q.cycleUnit === "day") return q.cycleValue === 7 ? "周循环" : "日循环";
|
|
244
|
-
return "日循环";
|
|
245
|
-
});
|
|
246
|
-
return parts.length > 0 ? parts.join("+") : "—";
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** 限流摘要:取第一条启用规则,如「1000 RPM」「50 并发」 */
|
|
250
|
-
export function rateLimitSummaryOf(rateLimits: RateLimitRule[]): string {
|
|
251
|
-
const r = rateLimits.find((x) => x.status === "enabled") ?? rateLimits[0];
|
|
252
|
-
if (!r) return "—";
|
|
253
|
-
if (r.limitType === "CONCURRENT") return `${r.limit} 并发`;
|
|
254
|
-
return `${r.limit} ${r.limitType}`;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** 列表卡/详情页摘要字段派生 */
|
|
258
|
-
function derive(
|
|
259
|
-
record: Omit<
|
|
260
|
-
ClientTemplateRecord,
|
|
261
|
-
| "statusLabel"
|
|
262
|
-
| "validityLabel"
|
|
263
|
-
| "routeCount"
|
|
264
|
-
| "quotaSummary"
|
|
265
|
-
| "rateLimitSummary"
|
|
266
|
-
| "tagsText"
|
|
267
|
-
| "policySummary"
|
|
268
|
-
>,
|
|
269
|
-
): ClientTemplateRecord {
|
|
270
|
-
const validityLabel =
|
|
271
|
-
record.validityType === "days"
|
|
272
|
-
? `创建后 ${record.validityDays} 天`
|
|
273
|
-
: record.validityType === "fixed"
|
|
274
|
-
? `固定至 ${record.validityFixedDate}`
|
|
275
|
-
: "永不过期";
|
|
276
|
-
const quotaSummary = quotaSummaryOf(record.quotas);
|
|
277
|
-
const rateLimitSummary = rateLimitSummaryOf(record.rateLimits);
|
|
278
|
-
return {
|
|
279
|
-
...record,
|
|
280
|
-
statusLabel: TEMPLATE_STATUS_LABELS[record.status],
|
|
281
|
-
validityLabel,
|
|
282
|
-
routeCount: record.authRoutes.length,
|
|
283
|
-
quotaSummary,
|
|
284
|
-
rateLimitSummary,
|
|
285
|
-
tagsText: record.tags.join(" · "),
|
|
286
|
-
policySummary: `${record.authRoutes.length} 路由 · ${quotaSummary} · ${rateLimitSummary}`,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const TEMPLATES: ClientTemplateRecord[] = [
|
|
291
|
-
derive({
|
|
292
|
-
code: "agent-default",
|
|
293
|
-
name: "agent-default",
|
|
294
|
-
tags: ["agent", "team-default"],
|
|
295
|
-
description: "Agent 团队标准访问模板",
|
|
296
|
-
status: "enabled",
|
|
297
|
-
validityType: "days",
|
|
298
|
-
validityDays: 365,
|
|
299
|
-
authRoutes: [
|
|
300
|
-
authRouteSnapshot("reasoning-default"),
|
|
301
|
-
authRouteSnapshot("general-chat"),
|
|
302
|
-
authRouteSnapshot("mobile-lite"),
|
|
303
|
-
],
|
|
304
|
-
quotas: [
|
|
305
|
-
deriveQuota({
|
|
306
|
-
name: "lifetime-total",
|
|
307
|
-
quotaType: "lifetime",
|
|
308
|
-
metric: "TOTAL_TOKEN",
|
|
309
|
-
limit: 10000000,
|
|
310
|
-
mode: "hard",
|
|
311
|
-
status: "enabled",
|
|
312
|
-
}),
|
|
313
|
-
deriveQuota({
|
|
314
|
-
name: "daily-token-cap",
|
|
315
|
-
quotaType: "cyclic",
|
|
316
|
-
metric: "TOTAL_TOKEN",
|
|
317
|
-
limit: 500000,
|
|
318
|
-
mode: "soft",
|
|
319
|
-
cycleUnit: "hour",
|
|
320
|
-
cycleValue: 24,
|
|
321
|
-
status: "enabled",
|
|
322
|
-
}),
|
|
323
|
-
],
|
|
324
|
-
rateLimits: [
|
|
325
|
-
deriveRateLimit({
|
|
326
|
-
name: "app-rpm",
|
|
327
|
-
limitType: "RPM",
|
|
328
|
-
limit: 1000,
|
|
329
|
-
burst: 100,
|
|
330
|
-
scopeAll: true,
|
|
331
|
-
status: "enabled",
|
|
332
|
-
}),
|
|
333
|
-
deriveRateLimit({
|
|
334
|
-
name: "concurrent-cap",
|
|
335
|
-
limitType: "CONCURRENT",
|
|
336
|
-
limit: 50,
|
|
337
|
-
scopeAll: true,
|
|
338
|
-
status: "enabled",
|
|
339
|
-
}),
|
|
340
|
-
],
|
|
341
|
-
createdClientCount: 12,
|
|
342
|
-
createdAt: "2026-06-05 10:00",
|
|
343
|
-
updatedAt: "2026-09-12 14:20",
|
|
344
|
-
operator: "张三",
|
|
345
|
-
}),
|
|
346
|
-
derive({
|
|
347
|
-
code: "web-portal-base",
|
|
348
|
-
name: "web-portal-base",
|
|
349
|
-
tags: ["web"],
|
|
350
|
-
description: "Web 门户前端调用基础模板",
|
|
351
|
-
status: "enabled",
|
|
352
|
-
validityType: "fixed",
|
|
353
|
-
validityFixedDate: "2027-12-31",
|
|
354
|
-
authRoutes: [authRouteSnapshot("general-chat")],
|
|
355
|
-
quotas: [
|
|
356
|
-
deriveQuota({
|
|
357
|
-
name: "weekly-request-cap",
|
|
358
|
-
quotaType: "cyclic",
|
|
359
|
-
metric: "REQUEST",
|
|
360
|
-
limit: 100000,
|
|
361
|
-
mode: "soft",
|
|
362
|
-
cycleUnit: "day",
|
|
363
|
-
cycleValue: 7,
|
|
364
|
-
status: "enabled",
|
|
365
|
-
}),
|
|
366
|
-
],
|
|
367
|
-
rateLimits: [
|
|
368
|
-
deriveRateLimit({
|
|
369
|
-
name: "web-rpm",
|
|
370
|
-
limitType: "RPM",
|
|
371
|
-
limit: 500,
|
|
372
|
-
scopeAll: true,
|
|
373
|
-
status: "enabled",
|
|
374
|
-
}),
|
|
375
|
-
],
|
|
376
|
-
createdClientCount: 5,
|
|
377
|
-
createdAt: "2026-07-01 09:30",
|
|
378
|
-
updatedAt: "2026-09-06 11:10",
|
|
379
|
-
operator: "李四",
|
|
380
|
-
}),
|
|
381
|
-
derive({
|
|
382
|
-
code: "partner-readonly",
|
|
383
|
-
name: "partner-readonly",
|
|
384
|
-
tags: ["partner"],
|
|
385
|
-
description: "合作方只读接入模板(当前停用)",
|
|
386
|
-
status: "disabled",
|
|
387
|
-
validityType: "never",
|
|
388
|
-
authRoutes: [authRouteSnapshot("embeddings-std")],
|
|
389
|
-
quotas: [
|
|
390
|
-
deriveQuota({
|
|
391
|
-
name: "lifetime-request",
|
|
392
|
-
quotaType: "lifetime",
|
|
393
|
-
metric: "REQUEST",
|
|
394
|
-
limit: 1000000,
|
|
395
|
-
mode: "hard",
|
|
396
|
-
status: "enabled",
|
|
397
|
-
}),
|
|
398
|
-
],
|
|
399
|
-
rateLimits: [
|
|
400
|
-
deriveRateLimit({
|
|
401
|
-
name: "partner-rpm",
|
|
402
|
-
limitType: "RPM",
|
|
403
|
-
limit: 60,
|
|
404
|
-
scopeAll: true,
|
|
405
|
-
status: "enabled",
|
|
406
|
-
}),
|
|
407
|
-
],
|
|
408
|
-
createdClientCount: 2,
|
|
409
|
-
createdAt: "2026-06-20 16:00",
|
|
410
|
-
updatedAt: "2026-08-28 10:45",
|
|
411
|
-
operator: "王五",
|
|
412
|
-
}),
|
|
413
|
-
];
|
|
414
|
-
|
|
415
|
-
/** 快筛选项常量:状态(单选)与标签(多选) */
|
|
416
|
-
export const CLIENT_TEMPLATE_STATUS_OPTIONS: { value: string; label: string }[] =
|
|
417
|
-
[
|
|
418
|
-
{ value: "all", label: "全部" },
|
|
419
|
-
{ value: "enabled", label: "已启用" },
|
|
420
|
-
{ value: "disabled", label: "已停用" },
|
|
421
|
-
];
|
|
422
|
-
|
|
423
|
-
/** 标签快筛(按数据中出现顺序展示;多选取交集) */
|
|
424
|
-
export const CLIENT_TEMPLATE_TAG_FILTERS: { value: string; label: string }[] = [
|
|
425
|
-
...new Set(TEMPLATES.flatMap((t) => t.tags)),
|
|
426
|
-
].map((t) => ({ value: t, label: t }));
|
|
427
|
-
|
|
428
|
-
/** 快筛计数:基于全量数据(构成口径,与当前筛选无关) */
|
|
429
|
-
function computeCounts(): ClientTemplateCounts {
|
|
430
|
-
return {
|
|
431
|
-
all: {
|
|
432
|
-
enabled: TEMPLATES.filter((t) => t.status === "enabled").length,
|
|
433
|
-
total: TEMPLATES.length,
|
|
434
|
-
},
|
|
435
|
-
status: {
|
|
436
|
-
enabled: TEMPLATES.filter((t) => t.status === "enabled").length,
|
|
437
|
-
disabled: TEMPLATES.filter((t) => t.status === "disabled").length,
|
|
438
|
-
},
|
|
439
|
-
tags: CLIENT_TEMPLATE_TAG_FILTERS.reduce<Record<string, number>>(
|
|
440
|
-
(acc, t) => {
|
|
441
|
-
const key = t.value.replace(/-/g, "_");
|
|
442
|
-
acc[key] = TEMPLATES.filter((r) => r.tags.includes(t.value)).length;
|
|
443
|
-
return acc;
|
|
444
|
-
},
|
|
445
|
-
{},
|
|
446
|
-
),
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** 快筛 Chip 选项:由计数派生,文案在此预计算(表达式不支持复合文案拼接) */
|
|
451
|
-
function computeFilters(
|
|
452
|
-
counts: ClientTemplateCounts,
|
|
453
|
-
): ClientTemplateListResult["filters"] {
|
|
454
|
-
return {
|
|
455
|
-
status: [
|
|
456
|
-
{ value: "all", text: `全部 ${counts.all.enabled}/${counts.all.total}` },
|
|
457
|
-
{ value: "enabled", text: `已启用 ${counts.status.enabled}`, dot: "#22c55e" },
|
|
458
|
-
{ value: "disabled", text: `已停用 ${counts.status.disabled}`, dot: "#9ca3af" },
|
|
459
|
-
],
|
|
460
|
-
tags: [
|
|
461
|
-
{ value: "all", text: `全部 ${counts.all.total}` },
|
|
462
|
-
...CLIENT_TEMPLATE_TAG_FILTERS.map((t) => ({
|
|
463
|
-
value: t.value,
|
|
464
|
-
text: `${t.label} ${counts.tags[t.value.replace(/-/g, "_")]}`,
|
|
465
|
-
})),
|
|
466
|
-
],
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/** clientTemplate.list:状态单选 + 关键字模糊 + 标签多选取交集,按 Updated At 降序 */
|
|
471
|
-
export function queryClientTemplates(
|
|
472
|
-
params: ClientTemplateListParams = {},
|
|
473
|
-
): ClientTemplateListResult {
|
|
474
|
-
const status = params.status ?? "all";
|
|
475
|
-
const keyword = (params.keyword ?? "").trim().toLowerCase();
|
|
476
|
-
const tags = params.tags ?? [];
|
|
477
|
-
const items = TEMPLATES.filter(
|
|
478
|
-
(t) =>
|
|
479
|
-
(status === "all" || t.status === status) &&
|
|
480
|
-
(keyword === "" ||
|
|
481
|
-
t.name.toLowerCase().includes(keyword) ||
|
|
482
|
-
t.code.toLowerCase().includes(keyword)) &&
|
|
483
|
-
(tags.length === 0 || tags.every((tag) => t.tags.includes(tag))),
|
|
484
|
-
).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
485
|
-
const counts = computeCounts();
|
|
486
|
-
return { items, total: TEMPLATES.length, counts, filters: computeFilters(counts) };
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/** clientTemplate.detail:按模板标识取详情;未命中返回 undefined(页面做空态) */
|
|
490
|
-
export function getClientTemplate(
|
|
491
|
-
code: string,
|
|
492
|
-
): ClientTemplateRecord | undefined {
|
|
493
|
-
return TEMPLATES.find((t) => t.code === code);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/** 全量模板标识(静态导出 generateStaticParams 用) */
|
|
497
|
-
export const CLIENT_TEMPLATE_CODES: string[] = TEMPLATES.map((t) => t.code);
|