@pactor-app/skills 1.33.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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/pactor-prd/SKILL.md +20 -23
  3. package/pactor-prd/references/acceptance-checklist.md +1 -1
  4. package/pactor-prd/references/data-api.md +35 -0
  5. package/pactor-prd/references/incremental-wireframe-delta.md +8 -9
  6. package/pactor-prd/references/page-inventory-and-files.md +2 -2
  7. package/pactor-prd/references/page-templates.md +59 -12
  8. package/pactor-prd/references/project-structure.md +1 -1
  9. package/pactor-prd/template/README.md +4 -4
  10. package/pactor-prd/template/app/api/d/[...parts]/route.ts +80 -0
  11. package/pactor-prd/template/app/layout.tsx +1 -3
  12. package/pactor-prd/template/next-env.d.ts +2 -2
  13. package/pactor-prd/template/next.config.js +2 -2
  14. package/pactor-prd/template/package.json +4 -4
  15. package/pactor-prd/template/pnpm-lock.yaml +97 -97
  16. package/pactor-prd/template/src/pactor/App.tsx +3 -2
  17. package/pactor-prd/template/src/pactor/actions.ts +5 -431
  18. package/pactor-prd/template/src/pactor/config.ts +12 -4
  19. package/pactor-prd/template/src/server/yaml/common/dashboard.yaml +1 -1
  20. package/pactor-prd/template/src/server/yaml/common/login.yaml +46 -75
  21. package/pactor-prd/template/src/server/yaml/models/data-list.yaml +344 -0
  22. package/pactor-prd/template/src/server/yaml/provider/provider-detail.yaml +2 -2
  23. package/pactor-prd/template/src/server/yaml/provider/provider-edit.yaml +1 -1
  24. package/pactor-prd/template/src/server/yaml/provider/provider-list.yaml +1 -1
  25. package/pactor-prd/template/src/server/yaml/route/route-detail.yaml +1 -1
  26. package/pactor-prd/template/src/server/yaml/route/route-edit.yaml +1 -1
  27. package/pactor-prd/template/src/server/yaml/route/route-list.yaml +1 -1
  28. package/pactor-prd/template/src/server/yaml/sitemap.yaml +23 -6
  29. package/pactor-prd/references/mock-data-routes.md +0 -66
  30. package/pactor-prd/template/app/api/data/[...parts]/route.ts +0 -118
  31. package/pactor-prd/template/app/login.css +0 -122
  32. package/pactor-prd/template/src/pactor/mocks/audit-mock.ts +0 -403
  33. package/pactor-prd/template/src/pactor/mocks/call-logs-mock.ts +0 -875
  34. package/pactor-prd/template/src/pactor/mocks/client-debug-mock.ts +0 -188
  35. package/pactor-prd/template/src/pactor/mocks/client-templates-mock.ts +0 -497
  36. package/pactor-prd/template/src/pactor/mocks/clients-mock.ts +0 -483
  37. package/pactor-prd/template/src/pactor/mocks/costs-mock.ts +0 -406
  38. package/pactor-prd/template/src/pactor/mocks/dashboard-mock.ts +0 -192
  39. package/pactor-prd/template/src/pactor/mocks/model-debug-mock.ts +0 -144
  40. package/pactor-prd/template/src/pactor/mocks/models-mock.ts +0 -799
  41. package/pactor-prd/template/src/pactor/mocks/ops-usage-mock.ts +0 -806
  42. package/pactor-prd/template/src/pactor/mocks/providers-mock.ts +0 -424
  43. package/pactor-prd/template/src/pactor/mocks/routes-mock.ts +0 -604
  44. package/pactor-prd/template/src/pactor/services.ts +0 -185
@@ -0,0 +1,344 @@
1
+ # 数据列表标准骨架 —— 「搜索 + 表格 + 抽屉新建/编辑」数据列表页的复制起点。
2
+ # 本页已注册进 sitemap.yaml(菜单「页面骨架 / 数据列表」,路径 /models/data-list),
3
+ # 在模板工程中可直接运行预览;生成业务列表页时复制本文件到 yaml/<模块>/ 下,
4
+ # 替换页面信息、字段、接口名(data-list → 业务资源名)与文案。
5
+ # 结构与交互约定见 skills/pactor-prd/references/page-templates.md「数据列表标准模型」。
6
+ dsl: "1.0"
7
+ page:
8
+ id: data-list
9
+ title: 数据列表
10
+ name: dataList
11
+ state:
12
+ # 搜索条件镜像:Form submit 后写入,驱动列表请求参数
13
+ keyword: ""
14
+ status: all
15
+ editingId: ""
16
+ # 表单条件显隐镜像(onValuesChange 写入;按页面需要增减,可多个)
17
+ fType: ""
18
+ dataSources:
19
+ list:
20
+ type: http
21
+ autoLoad: true
22
+ request:
23
+ method: GET
24
+ url: /api/d/data-list
25
+ params:
26
+ keyword: ${state.keyword}
27
+ status: ${state.status}
28
+ editing: # 编辑预填;autoLoad: false,打开抽屉前手动 reload
29
+ type: http
30
+ autoLoad: false
31
+ request:
32
+ method: GET
33
+ url: /api/d/data-list/${state.editingId}
34
+ body:
35
+ type: Page
36
+ children:
37
+ - type: Breadcrumb
38
+ props:
39
+ items:
40
+ - label: 页面骨架
41
+ - label: 数据列表
42
+ # 标题行:左标题 + 副标题,右新建按钮
43
+ - type: Flex
44
+ props:
45
+ justify: space-between
46
+ align: center
47
+ children:
48
+ - type: Flex
49
+ props:
50
+ direction: vertical
51
+ gap: 2
52
+ children:
53
+ - type: Typography
54
+ props:
55
+ variant: h3
56
+ text: 数据列表
57
+ - type: Typography
58
+ props:
59
+ variant: muted
60
+ text: 标准列表示例:搜索 + 表格 + 抽屉新建/编辑,复制本页生成业务列表。
61
+ - type: Button
62
+ props:
63
+ text: + 新建记录
64
+ variant: primary
65
+ events:
66
+ onClick:
67
+ - action: setState # 清空编辑态与显隐镜像
68
+ params:
69
+ editingId: ""
70
+ fType: ""
71
+ - action: reload # 清空 editing 数据(editingId 为空 → 空对象)
72
+ params:
73
+ dataSource: editing
74
+ - action: overlay.open # 锚点 &entityDrawer 在此定义,表格行内用 *entityDrawer 引用
75
+ params:
76
+ overlay: &entityDrawer
77
+ type: drawer
78
+ title: "${state.editingId == '' ? '新建记录' : '编辑记录'}"
79
+ width: 720
80
+ content:
81
+ type: Form
82
+ props:
83
+ name: entityForm
84
+ layout: vertical
85
+ initialValues: ${data.editing}
86
+ events:
87
+ # 条件显隐字段镜像到 state(其他字段变化时保持原值)
88
+ onValuesChange:
89
+ - action: setState
90
+ params:
91
+ fType: "${event.type != null ? event.type : state.fType}"
92
+ onSubmit:
93
+ - action: request
94
+ params:
95
+ method: POST
96
+ url: /api/d/ops/data-list-save
97
+ body:
98
+ id: ${state.editingId}
99
+ name: ${event.name}
100
+ code: ${event.code}
101
+ type: ${event.type}
102
+ extra: ${event.extra}
103
+ sort: ${event.sort}
104
+ status: ${event.status}
105
+ - action: message
106
+ params:
107
+ messageType: success
108
+ text: 已保存(演示)
109
+ - action: overlay.close
110
+ children:
111
+ # 分区结构:标题 → 分割线 → 字段组(不用 Card);多个分区重复此结构
112
+ - type: Flex
113
+ props:
114
+ direction: vertical
115
+ gap: middle
116
+ children:
117
+ - type: Text
118
+ props:
119
+ text: 基本信息
120
+ strong: true
121
+ - type: Separator
122
+ - type: Flex
123
+ props:
124
+ direction: vertical
125
+ gap: small
126
+ children:
127
+ - type: Field
128
+ props:
129
+ label: 名称
130
+ required: true
131
+ children:
132
+ - type: Input
133
+ props:
134
+ name: name
135
+ placeholder: 请输入名称
136
+ rules:
137
+ - required: true
138
+ message: 请输入名称
139
+ # 编辑时禁用关键字段
140
+ - type: Field
141
+ props:
142
+ label: 编码
143
+ required: true
144
+ children:
145
+ - type: Input
146
+ props:
147
+ name: code
148
+ placeholder: 大写字母 + 下划线,如 DEMO_ITEM
149
+ disabled: ${state.editingId != ''}
150
+ rules:
151
+ - required: true
152
+ message: 请输入编码
153
+ # 成对字段用 Grid 两列
154
+ - type: Grid
155
+ props:
156
+ columns: 2
157
+ gap: 16
158
+ children:
159
+ - type: Field
160
+ props:
161
+ label: 类型
162
+ required: true
163
+ children:
164
+ - type: Select
165
+ props:
166
+ name: type
167
+ placeholder: 请选择类型
168
+ options: ${data.list.filters.type}
169
+ rules:
170
+ - required: true
171
+ message: 请选择类型
172
+ - type: Field
173
+ props:
174
+ label: 状态
175
+ children:
176
+ - type: Select
177
+ props:
178
+ name: status
179
+ value: 启用
180
+ options: ${data.list.filters.status}
181
+ - type: Grid
182
+ props:
183
+ columns: 2
184
+ gap: 16
185
+ children:
186
+ - type: Field
187
+ props:
188
+ label: 排序
189
+ children:
190
+ - type: NumberInput
191
+ props:
192
+ name: sort
193
+ className: w-full
194
+ min: 0
195
+ precision: 0
196
+ # 条件显隐字段:依赖镜像 state
197
+ - when: ${state.fType == '高级'}
198
+ type: Field
199
+ props:
200
+ label: 扩展说明
201
+ children:
202
+ - type: Input
203
+ props:
204
+ name: extra
205
+ placeholder: 高级类型的扩展配置说明
206
+ # 底部按钮:取消关抽屉,保存触发 Form onSubmit
207
+ - type: Flex
208
+ props:
209
+ justify: end
210
+ gap: small
211
+ className: mt-4
212
+ children:
213
+ - type: Button
214
+ props:
215
+ text: 取消
216
+ events:
217
+ onClick:
218
+ - action: overlay.close
219
+ - type: Button
220
+ props:
221
+ text: 保存
222
+ variant: primary
223
+ submit: true
224
+ htmlType: submit
225
+ - action: reload # overlay.open 阻塞到关闭,关闭后刷新列表
226
+ params:
227
+ dataSource: list
228
+ # 搜索栏:Form(layout: inline) + Field 包裹控件
229
+ - type: Card
230
+ children:
231
+ - type: Form
232
+ props:
233
+ name: search
234
+ layout: inline
235
+ events:
236
+ onSubmit:
237
+ - action: setState
238
+ params:
239
+ keyword: ${event.keyword}
240
+ status: ${event.status}
241
+ - action: reload
242
+ params:
243
+ dataSource: list
244
+ children:
245
+ - type: Field
246
+ props:
247
+ label: 编码/名称
248
+ children:
249
+ - type: Input
250
+ props:
251
+ name: keyword
252
+ placeholder: 支持模糊查询
253
+ - type: Field
254
+ props:
255
+ label: 状态
256
+ children:
257
+ - type: Select
258
+ props:
259
+ name: status
260
+ placeholder: 请选择状态
261
+ options: ${data.list.filters.status} # 选项由接口下发
262
+ # 按钮也放进 Field:label 为 U+00A0 不间断空格(普通空格会被压缩),与左侧控件基线对齐
263
+ - type: Field
264
+ props:
265
+ label: " "
266
+ children:
267
+ - type: Flex
268
+ props:
269
+ gap: small
270
+ align: center
271
+ children:
272
+ - type: Button
273
+ props:
274
+ text: 查询
275
+ submit: true
276
+ htmlType: submit
277
+ - type: Button
278
+ props:
279
+ text: 重置
280
+ events:
281
+ onClick:
282
+ - action: form.setValues
283
+ params:
284
+ form: search
285
+ values:
286
+ keyword: ""
287
+ status: all
288
+ - action: setState
289
+ params:
290
+ keyword: ""
291
+ status: all
292
+ - action: reload
293
+ params:
294
+ dataSource: list
295
+ # 表格:名称列/操作列按钮统一 variant: link size: small
296
+ - type: Table
297
+ props:
298
+ dataSource: ${data.list.items}
299
+ rowKey: id
300
+ pagination: false
301
+ className: mt-4
302
+ columns:
303
+ - title: 名称
304
+ cell:
305
+ type: Button
306
+ props:
307
+ text: ${row.name}
308
+ variant: link
309
+ size: small
310
+ events:
311
+ onClick: &editRow # 行点击 = 编辑,锚点在此定义
312
+ - action: setState # 写入编辑 id + 显隐镜像(取自行数据,不等 editing 接口)
313
+ params:
314
+ editingId: ${row.id}
315
+ fType: ${row.type}
316
+ - action: reload
317
+ params:
318
+ dataSource: editing
319
+ - action: overlay.open
320
+ params:
321
+ overlay: *entityDrawer
322
+ - action: reload
323
+ params:
324
+ dataSource: list
325
+ - title: 编码
326
+ dataIndex: code
327
+ - title: 类型
328
+ dataIndex: type
329
+ - title: 状态
330
+ dataIndex: status
331
+ - title: 操作
332
+ cell:
333
+ type: Button
334
+ props:
335
+ text: 编辑
336
+ variant: link
337
+ size: small
338
+ events:
339
+ onClick: *editRow # 与名称列同一动作
340
+ - type: Text
341
+ props:
342
+ text: 共 ${data.list.total} 条
343
+ type: secondary
344
+ className: mt-3
@@ -12,12 +12,12 @@ dataSources:
12
12
  autoLoad: true
13
13
  request:
14
14
  method: GET
15
- url: /api/data/providers/${route.params.code}
15
+ url: /api/d/providers/${route.params.code}
16
16
  upstream:
17
17
  type: http
18
18
  request:
19
19
  method: GET
20
- url: /api/data/providers/${route.params.code}/pull-models
20
+ url: /api/d/providers/${route.params.code}/pull-models
21
21
  body:
22
22
  type: Page
23
23
  children:
@@ -10,7 +10,7 @@ dataSources:
10
10
  autoLoad: true
11
11
  request:
12
12
  method: GET
13
- url: /api/data/providers/${route.params.code}
13
+ url: /api/d/providers/${route.params.code}
14
14
  body:
15
15
  type: Page
16
16
  children:
@@ -14,7 +14,7 @@ dataSources:
14
14
  autoLoad: true
15
15
  request:
16
16
  method: GET
17
- url: /api/data/providers
17
+ url: /api/d/providers
18
18
  params:
19
19
  keyword: ${state.keyword}
20
20
  status: ${state.statusChip}
@@ -10,7 +10,7 @@ dataSources:
10
10
  autoLoad: true
11
11
  request:
12
12
  method: GET
13
- url: /api/data/routes/${route.params.code}
13
+ url: /api/d/routes/${route.params.code}
14
14
  body:
15
15
  type: Page
16
16
  children:
@@ -10,7 +10,7 @@ dataSources:
10
10
  autoLoad: true
11
11
  request:
12
12
  method: GET
13
- url: /api/data/routes/${route.params.code}
13
+ url: /api/d/routes/${route.params.code}
14
14
  body:
15
15
  type: Page
16
16
  children:
@@ -14,7 +14,7 @@ dataSources:
14
14
  autoLoad: true
15
15
  request:
16
16
  method: GET
17
- url: /api/data/routes
17
+ url: /api/d/routes
18
18
  params:
19
19
  status: ${state.statusChip}
20
20
  type: ${state.typeChip}
@@ -49,6 +49,14 @@ pages:
49
49
  }
50
50
  # 模块二:模型路由 / 路由(卡片式列表 + 单页纵向详情 + 单页表单)
51
51
  - { id: route-list, path: /routes, title: 路由, version: 1, lazy: true }
52
+ # 页面骨架:数据列表标准模型(搜索 + 表格 + 抽屉新建/编辑),可复制生成业务列表页
53
+ - {
54
+ id: data-list,
55
+ path: /models/data-list,
56
+ title: 数据列表,
57
+ version: 1,
58
+ lazy: true,
59
+ }
52
60
  - {
53
61
  id: route-create,
54
62
  path: /routes/new,
@@ -116,6 +124,19 @@ menus:
116
124
  title: 路由,
117
125
  layout: admin,
118
126
  }
127
+ - id: skeletons
128
+ type: group
129
+ title: 页面骨架
130
+ icon: folder
131
+ order: 4
132
+ children:
133
+ - {
134
+ id: data-list,
135
+ type: page,
136
+ pageId: data-list,
137
+ title: 数据列表,
138
+ layout: admin,
139
+ }
119
140
  # 隐藏页(不出现在菜单,经列表页跳转进入;activeMenu 保持所属菜单高亮)
120
141
  - {
121
142
  id: provider-create,
@@ -234,15 +255,11 @@ layouts:
234
255
  props: { className: shell-content }
235
256
  children:
236
257
  - type: PageOutlet
237
- # 登录页外壳:login-page 全屏渐变容器,样式见 app/login.css
258
+ # 登录页外壳:纯页面出口(登录页由 Login 业务组件自带整页品牌视觉与背景装饰)
238
259
  login:
239
260
  dsl: "1.0"
240
261
  body:
241
- type: Flex
242
- props:
243
- className: login-page
244
- children:
245
- - type: PageOutlet
262
+ type: PageOutlet
246
263
  # 裸布局:无外壳,页面铺满
247
264
  blank:
248
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,118 +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
- "ops/model-usage": "ops.modelUsage",
31
- "ops/provider-usage": "ops.providerUsage",
32
- "ops/client-usage": "ops.clientUsage",
33
- "ops/provider-availability": "ops.availability",
34
- };
35
-
36
- /** 详情 / 子资源:<res>/<code|id> 或 <res>/<code>/<sub> */
37
- const DETAIL_TABLE: Record<string, string> = {
38
- providers: "provider.detail",
39
- models: "model.detail",
40
- routes: "route.detail",
41
- clients: "client.detail",
42
- "client-templates": "clientTemplate.detail",
43
- "call-logs": "callLog.detail",
44
- costs: "cost.detail",
45
- audit: "audit.detail",
46
- };
47
-
48
- const SUB_TABLE: Record<string, string> = {
49
- "providers/pull-models": "provider.pullModels",
50
- "client-templates/created-clients": "clientTemplate.createdClients",
51
- };
52
-
53
- /** 详情类服务的标识参数名(code / id) */
54
- const ID_PARAM: Record<string, string> = {
55
- "callLog.detail": "id",
56
- "cost.detail": "id",
57
- "audit.detail": "id",
58
- };
59
-
60
- function resolveService(parts: string[]): { service: string; id?: string } | null {
61
- if (parts.length === 0 || parts.length > 3) return null;
62
- if (parts.length === 1 || parts.length === 2 && parts[0] === "ops") {
63
- const service = ROUTE_TABLE[parts.join("/")];
64
- return service ? { service } : null;
65
- }
66
- const [resource, id, sub] = parts;
67
- if (parts.length === 3) {
68
- const service = SUB_TABLE[`${resource}/${sub}`];
69
- return service ? { service, id } : null;
70
- }
71
- const service = DETAIL_TABLE[resource];
72
- return service ? { service, id } : null;
73
- }
74
-
75
- /** query string → 服务入参:'true'/'false' 还原为布尔,其余保持字符串 */
76
- function coerceParams(searchParams: URLSearchParams): Record<string, unknown> {
77
- const params: Record<string, unknown> = {};
78
- for (const [key, value] of searchParams.entries()) {
79
- params[key] = value === "true" ? true : value === "false" ? false : value;
80
- }
81
- return params;
82
- }
83
-
84
- export async function GET(
85
- request: Request,
86
- context: RouteContext<"/api/data/[...parts]">,
87
- ) {
88
- const store = await cookies();
89
- const user = findUserBySession(store.get(SESSION_COOKIE)?.value);
90
- if (!user) {
91
- return Response.json(
92
- { error: "未登录" },
93
- { status: 401, headers: corsHeaders(request) },
94
- );
95
- }
96
-
97
- const { parts } = await context.params;
98
- const resolved = resolveService(parts);
99
- if (!resolved) {
100
- return Response.json(
101
- { error: `未知数据接口: /api/data/${parts.join("/")}` },
102
- { status: 404, headers: corsHeaders(request) },
103
- );
104
- }
105
-
106
- const params = coerceParams(new URL(request.url).searchParams);
107
- if (resolved.id !== undefined) {
108
- params[ID_PARAM[resolved.service] ?? "code"] = resolved.id;
109
- }
110
-
111
- const result = await pactorServices[resolved.service](params);
112
- return Response.json(result ?? null, {
113
- headers: { "cache-control": "no-store", ...corsHeaders(request) },
114
- });
115
- }
116
-
117
- /** 预检(跨源带凭据 / 自定义头时) */
118
- export const OPTIONS = corsPreflight;