@pactor-app/skills 1.34.0 → 1.35.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-prd/SKILL.md +18 -21
- package/pactor-prd/references/acceptance-checklist.md +1 -1
- package/pactor-prd/references/data-api.md +35 -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/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/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
|
@@ -255,15 +255,11 @@ layouts:
|
|
|
255
255
|
props: { className: shell-content }
|
|
256
256
|
children:
|
|
257
257
|
- type: PageOutlet
|
|
258
|
-
#
|
|
258
|
+
# 登录页外壳:纯页面出口(登录页由 Login 业务组件自带整页品牌视觉与背景装饰)
|
|
259
259
|
login:
|
|
260
260
|
dsl: "1.0"
|
|
261
261
|
body:
|
|
262
|
-
type:
|
|
263
|
-
props:
|
|
264
|
-
className: login-page
|
|
265
|
-
children:
|
|
266
|
-
- type: PageOutlet
|
|
262
|
+
type: PageOutlet
|
|
267
263
|
# 裸布局:无外壳,页面铺满
|
|
268
264
|
blank:
|
|
269
265
|
dsl: "1.0"
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# mock 数据路由模板
|
|
2
|
-
|
|
3
|
-
页面 DSL 的 http 数据源统一入口:`app/api/data/[...path]/route.ts`。目标:原型阶段零后端依赖;接真实后端时只改这一层,页面 yaml 不动。
|
|
4
|
-
|
|
5
|
-
## 命名约定
|
|
6
|
-
|
|
7
|
-
- **单段路径**:`/api/data/projects`、`/api/data/project-members`——列表/选项类一律单段 kebab-case
|
|
8
|
-
- **禁止** `/api/data/project/members` 这种两段写法:`[...path]` 会把第二段当详情 id 解析(`project/detail?id=members`),踩过的坑
|
|
9
|
-
- 详情:`/api/data/<resource>/<code|id>`;子资源:`/api/data/<resource>/<code>/<sub>`
|
|
10
|
-
- 数据源 url 与页面 id 语义对应:`project-list` 页 → `/api/data/projects`
|
|
11
|
-
|
|
12
|
-
## 路由模板
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
// app/api/data/[...path]/route.ts
|
|
16
|
-
/**
|
|
17
|
-
* 页面数据接口(mock):页面 DSL 的 http 数据源统一入口。
|
|
18
|
-
* 接入真实后端时改写 ROUTE_TABLE 的实现(或调整 baseUrl 直连后端)。
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** 单段路径 → mock handler */
|
|
22
|
-
const ROUTE_TABLE: Record<string, (params: Record<string, unknown>) => unknown> = {
|
|
23
|
-
projects: (params) => filterProjects(params), // mock 函数就近定义或放 src/server/mocks/
|
|
24
|
-
"project-members": () => PROJECT_MEMBERS,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** 详情:<resource>/<id> */
|
|
28
|
-
const DETAIL_TABLE: Record<string, (id: string) => unknown> = {
|
|
29
|
-
projects: (code) => PROJECTS.find((p) => p.code === code) ?? null,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export async function GET(
|
|
33
|
-
request: Request,
|
|
34
|
-
context: { params: Promise<{ path: string[] }> },
|
|
35
|
-
) {
|
|
36
|
-
const { path } = await context.params;
|
|
37
|
-
const params = Object.fromEntries(new URL(request.url).searchParams.entries());
|
|
38
|
-
|
|
39
|
-
if (path.length === 1) {
|
|
40
|
-
const handler = ROUTE_TABLE[path[0]!];
|
|
41
|
-
if (!handler) return Response.json({ error: `未知数据接口: /api/data/${path.join("/")}` }, { status: 404 });
|
|
42
|
-
return Response.json(handler(params) ?? null);
|
|
43
|
-
}
|
|
44
|
-
if (path.length === 2) {
|
|
45
|
-
const handler = DETAIL_TABLE[path[0]!];
|
|
46
|
-
if (!handler) return Response.json({ error: "未知详情接口" }, { status: 404 });
|
|
47
|
-
return Response.json(handler(path[1]!) ?? null);
|
|
48
|
-
}
|
|
49
|
-
return Response.json({ error: "不支持的接口路径" }, { status: 404 });
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
注意:`context.params` 的具体类型以目标项目的 Next 版本为准(Next 15+ 为 Promise)。
|
|
54
|
-
|
|
55
|
-
## mock 数据要求
|
|
56
|
-
|
|
57
|
-
- 字段名与线框图「区域说明/字段说明」逐一对齐,不臆造字段
|
|
58
|
-
- 每类数据至少覆盖:正常多条、触发告警/特殊态的条目(如「待填密钥」)、以及空态可被演示(如筛选到无结果)
|
|
59
|
-
- 筛选参数在 handler 内真实过滤(列表页筛选闭环依赖它),不是返回全量
|
|
60
|
-
- 选项类接口(如下拉的操作人列表)单独注册单段路径,页面 `options: ${data.xxx}` 绑定
|
|
61
|
-
|
|
62
|
-
## 替换为真实接口
|
|
63
|
-
|
|
64
|
-
1. 直接把 ROUTE_TABLE handler 改为 `fetch` 真实后端(服务端转发,带鉴权头)
|
|
65
|
-
2. 或调整 pactor 数据源 baseUrl 直连后端,`/api/data/*` 层整体退役
|
|
66
|
-
页面 yaml 的 dataSources 声明两种方案下都无需改动——这就是必须 `type: http` 的原因。
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { cookies } from "next/headers";
|
|
2
|
-
|
|
3
|
-
import { findUserBySession, SESSION_COOKIE } from "@/server/accounts";
|
|
4
|
-
import { corsHeaders, corsPreflight } from "@/server/cors";
|
|
5
|
-
import { pactorServices } from "@/pactor/services";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 页面数据接口(mock):页面 DSL 的 http 数据源统一入口。
|
|
9
|
-
* URL 路径段 → 服务名映射(scripts/to-http-dsl.mjs 中的同一张表);
|
|
10
|
-
* 接入真实后端时改写本表(或在 src/pactor/config.ts 调整 baseUrl 直连后端)。
|
|
11
|
-
*
|
|
12
|
-
* 查询参数原为 service 包装层的入参(布尔快筛在 DSL 侧是一族一个参数),
|
|
13
|
-
* 经 HTTP 序列化为字符串,此处把 'true'/'false' 还原为布尔后再复用
|
|
14
|
-
* 与客户端注册表同一套包装层(src/pactor/services.ts)。
|
|
15
|
-
* 接口侧鉴权:匿名一律 401。
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** 路径段 → 服务名 */
|
|
19
|
-
const ROUTE_TABLE: Record<string, string> = {
|
|
20
|
-
dashboard: "dashboard.query",
|
|
21
|
-
providers: "provider.list",
|
|
22
|
-
models: "model.list",
|
|
23
|
-
routes: "route.list",
|
|
24
|
-
clients: "client.list",
|
|
25
|
-
"client-templates": "clientTemplate.list",
|
|
26
|
-
"call-logs": "callLog.list",
|
|
27
|
-
costs: "cost.list",
|
|
28
|
-
audit: "audit.list",
|
|
29
|
-
"audit-operators": "audit.operators",
|
|
30
|
-
"data-list": "dataList.list",
|
|
31
|
-
"ops/data-list-save": "dataList.save",
|
|
32
|
-
"ops/model-usage": "ops.modelUsage",
|
|
33
|
-
"ops/provider-usage": "ops.providerUsage",
|
|
34
|
-
"ops/client-usage": "ops.clientUsage",
|
|
35
|
-
"ops/provider-availability": "ops.availability",
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** 详情 / 子资源:<res>/<code|id> 或 <res>/<code>/<sub> */
|
|
39
|
-
const DETAIL_TABLE: Record<string, string> = {
|
|
40
|
-
providers: "provider.detail",
|
|
41
|
-
models: "model.detail",
|
|
42
|
-
routes: "route.detail",
|
|
43
|
-
clients: "client.detail",
|
|
44
|
-
"client-templates": "clientTemplate.detail",
|
|
45
|
-
"call-logs": "callLog.detail",
|
|
46
|
-
costs: "cost.detail",
|
|
47
|
-
audit: "audit.detail",
|
|
48
|
-
"data-list": "dataList.detail",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const SUB_TABLE: Record<string, string> = {
|
|
52
|
-
"providers/pull-models": "provider.pullModels",
|
|
53
|
-
"client-templates/created-clients": "clientTemplate.createdClients",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/** 详情类服务的标识参数名(code / id) */
|
|
57
|
-
const ID_PARAM: Record<string, string> = {
|
|
58
|
-
"callLog.detail": "id",
|
|
59
|
-
"cost.detail": "id",
|
|
60
|
-
"audit.detail": "id",
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
function resolveService(parts: string[]): { service: string; id?: string } | null {
|
|
64
|
-
if (parts.length === 0 || parts.length > 3) return null;
|
|
65
|
-
if (parts.length === 1 || parts.length === 2 && parts[0] === "ops") {
|
|
66
|
-
const service = ROUTE_TABLE[parts.join("/")];
|
|
67
|
-
return service ? { service } : null;
|
|
68
|
-
}
|
|
69
|
-
const [resource, id, sub] = parts;
|
|
70
|
-
if (parts.length === 3) {
|
|
71
|
-
const service = SUB_TABLE[`${resource}/${sub}`];
|
|
72
|
-
return service ? { service, id } : null;
|
|
73
|
-
}
|
|
74
|
-
const service = DETAIL_TABLE[resource];
|
|
75
|
-
return service ? { service, id } : null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** query string → 服务入参:'true'/'false' 还原为布尔,其余保持字符串 */
|
|
79
|
-
function coerceParams(searchParams: URLSearchParams): Record<string, unknown> {
|
|
80
|
-
const params: Record<string, unknown> = {};
|
|
81
|
-
for (const [key, value] of searchParams.entries()) {
|
|
82
|
-
params[key] = value === "true" ? true : value === "false" ? false : value;
|
|
83
|
-
}
|
|
84
|
-
return params;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function GET(
|
|
88
|
-
request: Request,
|
|
89
|
-
context: RouteContext<"/api/data/[...parts]">,
|
|
90
|
-
) {
|
|
91
|
-
const store = await cookies();
|
|
92
|
-
const user = findUserBySession(store.get(SESSION_COOKIE)?.value);
|
|
93
|
-
if (!user) {
|
|
94
|
-
return Response.json(
|
|
95
|
-
{ error: "未登录" },
|
|
96
|
-
{ status: 401, headers: corsHeaders(request) },
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const { parts } = await context.params;
|
|
101
|
-
const resolved = resolveService(parts);
|
|
102
|
-
if (!resolved) {
|
|
103
|
-
return Response.json(
|
|
104
|
-
{ error: `未知数据接口: /api/data/${parts.join("/")}` },
|
|
105
|
-
{ status: 404, headers: corsHeaders(request) },
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const params = coerceParams(new URL(request.url).searchParams);
|
|
110
|
-
if (resolved.id !== undefined) {
|
|
111
|
-
params[ID_PARAM[resolved.service] ?? "code"] = resolved.id;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const result = await pactorServices[resolved.service](params);
|
|
115
|
-
return Response.json(result ?? null, {
|
|
116
|
-
headers: { "cache-control": "no-store", ...corsHeaders(request) },
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** 写操作(ops 前缀的保存类接口,如骨架示例的新建/编辑提交) */
|
|
121
|
-
export async function POST(
|
|
122
|
-
request: Request,
|
|
123
|
-
context: RouteContext<"/api/data/[...parts]">,
|
|
124
|
-
) {
|
|
125
|
-
const store = await cookies();
|
|
126
|
-
const user = findUserBySession(store.get(SESSION_COOKIE)?.value);
|
|
127
|
-
if (!user) {
|
|
128
|
-
return Response.json(
|
|
129
|
-
{ error: "未登录" },
|
|
130
|
-
{ status: 401, headers: corsHeaders(request) },
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const { parts } = await context.params;
|
|
135
|
-
if (parts[0] !== "ops") {
|
|
136
|
-
return Response.json(
|
|
137
|
-
{ error: "仅 ops 接口支持 POST" },
|
|
138
|
-
{ status: 405, headers: corsHeaders(request) },
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
const resolved = resolveService(parts);
|
|
142
|
-
if (!resolved) {
|
|
143
|
-
return Response.json(
|
|
144
|
-
{ error: `未知数据接口: /api/data/${parts.join("/")}` },
|
|
145
|
-
{ status: 404, headers: corsHeaders(request) },
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const body = (await request.json().catch(() => ({}))) as Record<
|
|
150
|
-
string,
|
|
151
|
-
unknown
|
|
152
|
-
>;
|
|
153
|
-
const result = await pactorServices[resolved.service](body);
|
|
154
|
-
return Response.json(result ?? null, {
|
|
155
|
-
headers: { "cache-control": "no-store", ...corsHeaders(request) },
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** 预检(跨源带凭据 / 自定义头时) */
|
|
160
|
-
export const OPTIONS = corsPreflight;
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/* 登录页视觉优化:居中渐变背景 + 卡片/表单细节打磨。
|
|
2
|
-
作用域限定 .login-page;DSL 不支持自定义 className,
|
|
3
|
-
故经 Pactor 组件的 data-slot 钩子定位(须在 @pactor-app/ui/styles.css 之后加载)。 */
|
|
4
|
-
|
|
5
|
-
.login-page {
|
|
6
|
-
flex: 1;
|
|
7
|
-
min-height: 100%;
|
|
8
|
-
display: flex;
|
|
9
|
-
align-items: center;
|
|
10
|
-
justify-content: center;
|
|
11
|
-
padding: 3rem 1rem;
|
|
12
|
-
background: radial-gradient(circle at 20% 15%, rgba(128, 165, 160, .2), transparent 32%), radial-gradient(circle at 85% 88%, rgba(223, 127, 71, .1), transparent 34%), #fff;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.login-card {
|
|
16
|
-
width: min(100%, 420px);
|
|
17
|
-
padding: 32px 28px;
|
|
18
|
-
border-radius: 16px;
|
|
19
|
-
background: rgba(250, 250, 249, .78);
|
|
20
|
-
backdrop-filter: blur(18px);
|
|
21
|
-
-webkit-backdrop-filter: blur(18px);
|
|
22
|
-
box-shadow: 0 24px 70px rgba(35, 42, 39, .14);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@media (prefers-color-scheme: dark) {
|
|
26
|
-
.login-page {
|
|
27
|
-
background:
|
|
28
|
-
radial-gradient(circle at 20% 15%, rgb(128 165 160 / 0.16), transparent 34%),
|
|
29
|
-
radial-gradient(circle at 85% 88%, rgb(223 127 71 / 0.08), transparent 36%),
|
|
30
|
-
#0e1211;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/* 登录卡片:磨砂底转为深色半透明,边框按暗场调整 */
|
|
34
|
-
.login-card {
|
|
35
|
-
background: rgb(22 27 25 / 0.72);
|
|
36
|
-
border-color: rgb(255 255 255 / 0.08);
|
|
37
|
-
box-shadow: 0 24px 70px rgba(35, 42, 39, .14);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.login-page [data-slot="button"]:hover {
|
|
41
|
-
box-shadow: 0 6px 16px -6px rgb(0 0 0 / 0.6);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* 登录卡片:更圆、入场动画(全站 Card 无阴影,见 theme.css) */
|
|
46
|
-
.login-page [data-slot="card"] {
|
|
47
|
-
width: 100%;
|
|
48
|
-
max-width: 26rem;
|
|
49
|
-
border-radius: calc(var(--radius) * 2);
|
|
50
|
-
animation: login-card-in 0.45s cubic-bezier(0.21, 1.02, 0.73, 1) both;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@keyframes login-card-in {
|
|
54
|
-
from {
|
|
55
|
-
opacity: 0;
|
|
56
|
-
transform: translateY(12px) scale(0.98);
|
|
57
|
-
}
|
|
58
|
-
to {
|
|
59
|
-
opacity: 1;
|
|
60
|
-
transform: none;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* 标题区三行文字(RESTRICTED ACCESS / 管理后台登录 / 副标题),
|
|
65
|
-
Text 组件经 className 透传定位;卡片内容 gap 较宽,用负 margin 收紧组内间距 */
|
|
66
|
-
.login-page .login-eyebrow {
|
|
67
|
-
display: block;
|
|
68
|
-
font-family: var(--font-geist-mono), ui-monospace, monospace;
|
|
69
|
-
font-size: 0.75rem;
|
|
70
|
-
letter-spacing: 0.18em;
|
|
71
|
-
text-transform: uppercase;
|
|
72
|
-
color: var(--primary);
|
|
73
|
-
margin-bottom: 12px;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.login-page .login-heading {
|
|
77
|
-
display: block;
|
|
78
|
-
margin-top: -0.375rem;
|
|
79
|
-
font-size: 1.5rem;
|
|
80
|
-
font-weight: 700;
|
|
81
|
-
letter-spacing: 0.01em;
|
|
82
|
-
margin-bottom: 24px;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.login-page .login-subtitle {
|
|
86
|
-
display: block;
|
|
87
|
-
margin-top: -0.375rem;
|
|
88
|
-
font-size: 0.875rem;
|
|
89
|
-
margin-bottom: 28px;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.login-page .login-demo-hint {
|
|
93
|
-
display: block;
|
|
94
|
-
font-size: 0.8125rem;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* 提示条圆角跟随卡片 */
|
|
98
|
-
.login-page [data-slot="alert"] {
|
|
99
|
-
border-radius: calc(var(--radius) * 1.2);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/* 输入框加高 */
|
|
103
|
-
.login-page [data-slot="input"] {
|
|
104
|
-
height: 2.75rem;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/* 登录按钮:加高、加粗、悬浮/按压反馈 */
|
|
108
|
-
.login-page [data-slot="button"] {
|
|
109
|
-
height: 2.75rem;
|
|
110
|
-
font-weight: 600;
|
|
111
|
-
transition:
|
|
112
|
-
transform 0.15s ease,
|
|
113
|
-
box-shadow 0.15s ease;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.login-page [data-slot="button"]:hover {
|
|
117
|
-
box-shadow: 0 6px 16px -6px rgb(15 23 42 / 0.35);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.login-page [data-slot="button"]:active {
|
|
121
|
-
transform: translateY(1px);
|
|
122
|
-
}
|