@boyingliu01/xp-gate 0.8.21 → 0.9.1

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 (79) hide show
  1. package/lib/__tests__/check-version.test.js +4 -4
  2. package/mock-policy/AGENTS.md +3 -3
  3. package/mutation/AGENTS.md +3 -3
  4. package/package.json +1 -1
  5. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  6. package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +6 -797
  7. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-1.md +91 -0
  8. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-2.md +155 -0
  9. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-3.md +70 -0
  10. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-4.md +154 -0
  11. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-5.md +126 -0
  12. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-6.md +180 -0
  13. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  14. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -361
  15. package/plugins/claude-code/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  16. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  17. package/plugins/claude-code/skills/ralph-loop/SKILL.md +87 -428
  18. package/plugins/claude-code/skills/ralph-loop/references/components.md +113 -0
  19. package/plugins/claude-code/skills/ralph-loop/references/workflow-details.md +99 -0
  20. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  21. package/plugins/claude-code/skills/sprint-flow/references/phase-2-build.md +34 -0
  22. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  23. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +97 -599
  24. package/plugins/opencode/package.json +1 -1
  25. package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +6 -797
  26. package/plugins/opencode/skills/admin-template-guidelines/references/rule-1.md +91 -0
  27. package/plugins/opencode/skills/admin-template-guidelines/references/rule-2.md +155 -0
  28. package/plugins/opencode/skills/admin-template-guidelines/references/rule-3.md +70 -0
  29. package/plugins/opencode/skills/admin-template-guidelines/references/rule-4.md +154 -0
  30. package/plugins/opencode/skills/admin-template-guidelines/references/rule-5.md +126 -0
  31. package/plugins/opencode/skills/admin-template-guidelines/references/rule-6.md +180 -0
  32. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  33. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -361
  34. package/plugins/opencode/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  35. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  36. package/plugins/opencode/skills/ralph-loop/SKILL.md +87 -428
  37. package/plugins/opencode/skills/ralph-loop/references/components.md +113 -0
  38. package/plugins/opencode/skills/ralph-loop/references/workflow-details.md +99 -0
  39. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  40. package/plugins/opencode/skills/sprint-flow/references/phase-2-build.md +34 -0
  41. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +97 -599
  43. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  44. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  45. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  46. package/principles/AGENTS.md +3 -3
  47. package/skills/admin-template-guidelines/SKILL.md +6 -797
  48. package/skills/admin-template-guidelines/references/rule-1.md +91 -0
  49. package/skills/admin-template-guidelines/references/rule-2.md +155 -0
  50. package/skills/admin-template-guidelines/references/rule-3.md +70 -0
  51. package/skills/admin-template-guidelines/references/rule-4.md +154 -0
  52. package/skills/admin-template-guidelines/references/rule-5.md +126 -0
  53. package/skills/admin-template-guidelines/references/rule-6.md +180 -0
  54. package/skills/delphi-review/AGENTS.md +3 -3
  55. package/skills/delphi-review/SKILL.md +59 -361
  56. package/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  57. package/skills/delphi-review/references/round-templates.md +67 -0
  58. package/skills/ralph-loop/SKILL.md +87 -428
  59. package/skills/ralph-loop/references/components.md +113 -0
  60. package/skills/ralph-loop/references/workflow-details.md +99 -0
  61. package/skills/sprint-flow/AGENTS.md +3 -3
  62. package/skills/sprint-flow/references/phase-2-build.md +34 -0
  63. package/skills/test-specification-alignment/AGENTS.md +3 -3
  64. package/skills/test-specification-alignment/SKILL.md +97 -599
  65. package/plugins/claude-code/skills/ralph-loop/references/components/memory.md +0 -55
  66. package/plugins/claude-code/skills/ralph-loop/references/components/middleware.md +0 -54
  67. package/plugins/claude-code/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  68. package/plugins/claude-code/skills/ralph-loop/references/components/system-prompt.md +0 -24
  69. package/plugins/claude-code/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
  70. package/plugins/opencode/skills/ralph-loop/references/components/memory.md +0 -55
  71. package/plugins/opencode/skills/ralph-loop/references/components/middleware.md +0 -54
  72. package/plugins/opencode/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  73. package/plugins/opencode/skills/ralph-loop/references/components/system-prompt.md +0 -24
  74. package/plugins/opencode/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
  75. package/skills/ralph-loop/references/components/memory.md +0 -55
  76. package/skills/ralph-loop/references/components/middleware.md +0 -54
  77. package/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  78. package/skills/ralph-loop/references/components/system-prompt.md +0 -24
  79. package/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
@@ -0,0 +1,91 @@
1
+ # Rule 1: Route Splitting — Examples
2
+
3
+ ## ❌ 错误示例:单文件膨胀(800+ 行)
4
+
5
+ ```typescript
6
+ // admin/routes/index.ts — 反模式
7
+ import { FastifyInstance } from 'fastify';
8
+
9
+ export async function adminRoutes(fastify: FastifyInstance) {
10
+ // 模板列表 — 第 10 行
11
+ fastify.get('/admin/templates', async (req, reply) => { ... });
12
+ // 模板创建 — 第 45 行
13
+ fastify.post('/admin/templates', async (req, reply) => { ... });
14
+ // 模板编辑 — 第 80 行
15
+ fastify.get('/admin/templates/:id/edit', async (req, reply) => { ... });
16
+ // 模板删除 — 第 115 行
17
+ fastify.delete('/admin/templates/:id', async (req, reply) => { ... });
18
+ // 计划列表 — 第 150 行
19
+ fastify.get('/admin/plans', async (req, reply) => { ... });
20
+ // 计划创建 — 第 190 行
21
+ fastify.post('/admin/plans', async (req, reply) => { ... });
22
+ // ... 持续膨胀到 800+ 行 ...
23
+ }
24
+ ```
25
+
26
+ ## ✅ 正确示例:按模块拆分
27
+
28
+ ```typescript
29
+ // admin/routes/admin-shared.ts
30
+ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
31
+
32
+ export function registerAdminShared(fastify: FastifyInstance) {
33
+ // 共享中间件:认证
34
+ fastify.addHook('onRequest', authenticateAdmin);
35
+
36
+ // 共享 helper:渲染带布局的页面
37
+ fastify.decorate('renderAdmin', async function (
38
+ this: FastifyInstance,
39
+ req: FastifyRequest,
40
+ reply: FastifyReply,
41
+ template: string,
42
+ data: Record<string, unknown>
43
+ ) {
44
+ return reply.view(`admin/layouts/main.njk`, {
45
+ ...data,
46
+ _content: await reply.view(`admin/${template}.njk`, data),
47
+ });
48
+ });
49
+ }
50
+
51
+ // admin/routes/templates.ts
52
+ import { FastifyInstance } from 'fastify';
53
+
54
+ export async function registerTemplateRoutes(fastify: FastifyInstance) {
55
+ fastify.get('/admin/templates', { onRequest: [authenticateAdmin] }, listTemplates);
56
+ fastify.get('/admin/templates/new', { onRequest: [authenticateAdmin] }, showNewTemplate);
57
+ fastify.post('/admin/templates', { onRequest: [authenticateAdmin] }, createTemplate);
58
+ fastify.get('/admin/templates/:id/edit', { onRequest: [authenticateAdmin] }, showEditTemplate);
59
+ fastify.put('/admin/templates/:id', { onRequest: [authenticateAdmin] }, updateTemplate);
60
+ fastify.delete('/admin/templates/:id', { onRequest: [authenticateAdmin] }, deleteTemplate);
61
+ }
62
+
63
+ // admin/routes/plans.ts
64
+ import { FastifyInstance } from 'fastify';
65
+
66
+ export async function registerPlanRoutes(fastify: FastifyInstance) {
67
+ fastify.get('/admin/plans', { onRequest: [authenticateAdmin] }, listPlans);
68
+ fastify.post('/admin/plans', { onRequest: [authenticateAdmin] }, createPlan);
69
+ fastify.post('/admin/plans/:id/start', { onRequest: [authenticateAdmin] }, startPlan);
70
+ fastify.post('/admin/plans/:id/pause', { onRequest: [authenticateAdmin] }, pausePlan);
71
+ fastify.post('/admin/plans/:id/complete', { onRequest: [authenticateAdmin] }, completePlan);
72
+ }
73
+
74
+ // admin/routes/index.ts — 仅负责聚合注册
75
+ import { FastifyInstance } from 'fastify';
76
+ import { registerAdminShared } from './admin-shared';
77
+ import { registerTemplateRoutes } from './templates';
78
+ import { registerPlanRoutes } from './plans';
79
+ import { registerReportRoutes } from './reports';
80
+ import { registerAnalyticsRoutes } from './analytics';
81
+ import { registerTreeRoutes } from './tree';
82
+
83
+ export async function registerAllAdminRoutes(fastify: FastifyInstance) {
84
+ registerAdminShared(fastify);
85
+ await registerTemplateRoutes(fastify);
86
+ await registerPlanRoutes(fastify);
87
+ await registerReportRoutes(fastify);
88
+ await registerAnalyticsRoutes(fastify);
89
+ await registerTreeRoutes(fastify);
90
+ }
91
+ ```
@@ -0,0 +1,155 @@
1
+ # Rule 2: Test Consistency — Examples
2
+
3
+ ## ❌ 错误示例:每个测试文件自建实例
4
+
5
+ ```typescript
6
+ // test/admin/templates.test.ts — 反模式
7
+ import Fastify from 'fastify';
8
+ import { registerTemplateRoutes } from '../../admin/routes/templates';
9
+
10
+ describe('Template Routes', () => {
11
+ let fastify: any;
12
+
13
+ beforeEach(async () => {
14
+ fastify = Fastify();
15
+ await fastify.register(prismaPlugin, { datasource: {} }); // 每次自建
16
+ await fastify.register(registerTemplateRoutes);
17
+ await fastify.ready();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await fastify.close(); // 每个文件重复 teardown 逻辑
22
+ });
23
+
24
+ it('should list templates', async () => {
25
+ const res = await fastify.inject({ method: 'GET', url: '/admin/templates' });
26
+ expect(res.statusCode).toBe(200);
27
+ });
28
+ });
29
+ ```
30
+
31
+ ```typescript
32
+ // test/admin/plans.test.ts — 同样重复,但细节不同
33
+ import Fastify from 'fastify';
34
+ import { registerPlanRoutes } from '../../admin/routes/plans';
35
+
36
+ describe('Plan Routes', () => {
37
+ let app: any;
38
+
39
+ beforeAll(async () => {
40
+ app = Fastify({ logger: false }); // 不一致的配置
41
+ await app.register(prismaPlugin, { datasource: {}, skipMigrate: true });
42
+ await app.register(registerPlanRoutes);
43
+ await app.listen({ port: 0 });
44
+ });
45
+
46
+ afterAll(async () => {
47
+ await app.server.close(); // 与 templates.test.ts teardown 方式不同
48
+ });
49
+ });
50
+ ```
51
+
52
+ ## ✅ 正确示例:共享 Helper
53
+
54
+ ```typescript
55
+ // test/helpers/create-admin-test-app.ts
56
+ import Fastify, { FastifyInstance } from 'fastify';
57
+ import { prisma } from '@prisma/client';
58
+ import { registerAllAdminRoutes } from '../../admin/routes';
59
+
60
+ export interface AdminTestApp {
61
+ fastify: FastifyInstance;
62
+ inject: FastifyInstance['inject'];
63
+ close: () => Promise<void>;
64
+ }
65
+
66
+ export async function createAdminTestApp(): Promise<AdminTestApp> {
67
+ const fastify = Fastify({
68
+ logger: false,
69
+ trustProxy: true,
70
+ });
71
+
72
+ // 注册 Prisma 插件
73
+ await fastify.register(async (instance) => {
74
+ instance.decorate('prisma', prisma);
75
+ });
76
+
77
+ // 注册所有 Admin 路由
78
+ await registerAllAdminRoutes(fastify);
79
+
80
+ await fastify.ready();
81
+
82
+ return {
83
+ fastify,
84
+ inject: fastify.inject.bind(fastify),
85
+ close: async () => {
86
+ await fastify.close();
87
+ },
88
+ };
89
+ }
90
+ ```
91
+
92
+ ```typescript
93
+ // test/admin/templates.test.ts
94
+ import { createAdminTestApp, AdminTestApp } from '../helpers/create-admin-test-app';
95
+
96
+ describe('Template Routes', () => {
97
+ let app: AdminTestApp;
98
+
99
+ beforeEach(async () => {
100
+ app = await createAdminTestApp();
101
+ });
102
+
103
+ afterEach(async () => {
104
+ await app.close();
105
+ });
106
+
107
+ it('should list templates', async () => {
108
+ const res = await app.inject({
109
+ method: 'GET',
110
+ url: '/admin/templates',
111
+ });
112
+ expect(res.statusCode).toBe(200);
113
+ const body = JSON.parse(res.body);
114
+ expect(Array.isArray(body.templates)).toBe(true);
115
+ });
116
+
117
+ it('should create a template', async () => {
118
+ const res = await app.inject({
119
+ method: 'POST',
120
+ url: '/admin/templates',
121
+ payload: {
122
+ name: 'New Template',
123
+ content: '<h1>Hello</h1>',
124
+ },
125
+ });
126
+ expect(res.statusCode).toBe(302);
127
+ expect(res.headers.location).toContain('/admin/templates');
128
+ });
129
+ });
130
+ ```
131
+
132
+ ```typescript
133
+ // test/admin/plans.test.ts — 使用同一个 helper,零重复
134
+ import { createAdminTestApp, AdminTestApp } from '../helpers/create-admin-test-app';
135
+
136
+ describe('Plan Routes', () => {
137
+ let app: AdminTestApp;
138
+
139
+ beforeEach(async () => {
140
+ app = await createAdminTestApp();
141
+ });
142
+
143
+ afterEach(async () => {
144
+ await app.close();
145
+ });
146
+
147
+ it('should list plans', async () => {
148
+ const res = await app.inject({
149
+ method: 'GET',
150
+ url: '/admin/plans',
151
+ });
152
+ expect(res.statusCode).toBe(200);
153
+ });
154
+ });
155
+ ```
@@ -0,0 +1,70 @@
1
+ # Rule 3: Nunjucks Parentheses — Examples
2
+
3
+ ## 优先级规则
4
+
5
+ ```
6
+ 表达式: A <= B | lower
7
+ 实际解析: A <= (B | lower) ← filter 先执行,比较的是 A 和 "lower" 后的 B
8
+ 预期语义: (A <= B) | lower ← 先比较,结果再经 filter
9
+ ```
10
+
11
+ ## ❌ 错误示例
12
+
13
+ ```nunjucks
14
+ {# admin/views/plans.njk — 反模式 #}
15
+
16
+ {# 比较在 filter 之后执行 → 逻辑错误 #}
17
+ {% if plan._interviews.length <= 15 | lower %}
18
+ <span class="badge badge-small">Small Plan</span>
19
+ {% endif %}
20
+
21
+ {# 实际等价于: plan._interviews.length <= "lower" → 始终 true #}
22
+
23
+ {# 条件渲染中的陷阱 #}
24
+ {% if plan.status == 'completed' | title %}
25
+ <span class="status-done">Done</span>
26
+ {% endif %}
27
+
28
+ {# 字符串格式化错误 #}
29
+ {{ plan.completionRate * 100 | round | default(0) }}
30
+ {# 当 completionRate 为 null 时,null * 100 = NaN → round(NaN) → 错误 #}
31
+ ```
32
+
33
+ ## ✅ 正确示例
34
+
35
+ ```nunjucks
36
+ {# admin/views/plans.njk #}
37
+
38
+ {# 括号确保比较先执行 #}
39
+ {% if (plan._interviews.length <= 15) | lower %}
40
+ <span class="badge badge-small">Small Plan</span>
41
+ {% endif %}
42
+
43
+ {# 条件判断中的正确使用 #}
44
+ {% if (plan.status == 'completed') | title %}
45
+ <span class="status-done">Done</span>
46
+ {% endif %}
47
+
48
+ {# 数值计算中先处理 null #}
49
+ {{ (plan.completionRate ?? 0) * 100 | round(1) | default('0.0') }}%
50
+
51
+ {# 多层比较与 filter 组合 #}
52
+ {% if (plan._interviews.length >= 15 and plan._interviews.length <= 60) | bool %}
53
+ <span class="badge badge-medium">Medium Plan</span>
54
+ {% elif (plan._interviews.length > 60) | bool %}
55
+ <span class="badge badge-large">Large Plan</span>
56
+ {% endif %}
57
+
58
+ {# 安全渲染可选字段 #}
59
+ {% if (plan.description | length) > 0 %}
60
+ <p class="description">{{ plan.description | truncate(120) }}</p>
61
+ {% else %}
62
+ <p class="description text-muted">No description provided.</p>
63
+ {% endif %}
64
+ ```
65
+
66
+ ## 验证清单
67
+
68
+ - [ ] 所有 `{{ A <= B | filter }}` 改为 `{{ (A <= B) | filter }}`
69
+ - [ ] 所有 `{% if A == B | filter %}` 改为 `{% if (A == B) | filter %}`
70
+ - [ ] 使用 `| bool` 而非裸布尔表达式配合逻辑运算符
@@ -0,0 +1,154 @@
1
+ # Rule 4: View Model Mapper — Examples
2
+
3
+ ## ❌ 错误示例:路由内联转换
4
+
5
+ ```typescript
6
+ // admin/routes/plans.ts — 反模式
7
+ fastify.get('/admin/plans', async (req, reply) => {
8
+ const plans = await prisma.plan.findMany({
9
+ include: { _count: { select: { interviews: true } } },
10
+ });
11
+
12
+ // 每个路由都在做同样的转换
13
+ const viewModels = plans.map((plan) => ({
14
+ id: plan.id,
15
+ name: plan.name,
16
+ status: plan.status,
17
+ interviewCount: plan._count?.interviews ?? 0,
18
+ completedCount: plan.interviews?.filter((i) => i.status === 'COMPLETED').length ?? 0,
19
+ completionRate: plan.interviews?.filter((i) => i.status === 'COMPLETED').length ?? 0
20
+ / (plan._count?.interviews ?? 1) * 100,
21
+ createdAt: plan.createdAt.toISOString().split('T')[0],
22
+ }));
23
+
24
+ return reply.view('admin/plans/list.njk', { plans: viewModels });
25
+ });
26
+ ```
27
+
28
+ ## ✅ 正确示例:提取 ViewModel 模块
29
+
30
+ ```typescript
31
+ // admin/view-models.ts
32
+ import { Plan, Interview, Template } from '@prisma/client';
33
+
34
+ export interface PlanViewModel {
35
+ id: string;
36
+ name: string;
37
+ status: string;
38
+ interviewCount: number;
39
+ completedCount: number;
40
+ completionRate: number;
41
+ createdAt: string;
42
+ }
43
+
44
+ export interface TemplateViewModel {
45
+ id: string;
46
+ name: string;
47
+ content: string;
48
+ interviewCount: number;
49
+ lastUsed: string | null;
50
+ }
51
+
52
+ /**
53
+ * 将 Prisma Plan 记录转换为视图模型
54
+ */
55
+ export function mapPlanToViewModel(
56
+ plan: Plan & { _count?: { interviews: number } },
57
+ interviews?: Interview[]
58
+ ): PlanViewModel {
59
+ const completedCount = interviews?.filter((i) => i.status === 'COMPLETED').length ?? 0;
60
+ const interviewCount = plan._count?.interviews ?? interviews?.length ?? 0;
61
+
62
+ return {
63
+ id: plan.id,
64
+ name: plan.name,
65
+ status: plan.status,
66
+ interviewCount,
67
+ completedCount,
68
+ completionRate: interviewCount > 0 ? (completedCount / interviewCount) * 100 : 0,
69
+ createdAt: plan.createdAt.toISOString().split('T')[0],
70
+ };
71
+ }
72
+
73
+ /**
74
+ * 批量转换 Plan 列表
75
+ */
76
+ export function mapPlansToViewModels(
77
+ plans: (Plan & { _count?: { interviews: number } })[],
78
+ interviewsMap?: Map<string, Interview[]>
79
+ ): PlanViewModel[] {
80
+ return plans.map((plan) =>
81
+ mapPlanToViewModel(plan, interviewsMap?.get(plan.id))
82
+ );
83
+ }
84
+
85
+ /**
86
+ * 将 Prisma Template 记录转换为视图模型
87
+ */
88
+ export function mapTemplateToViewModel(
89
+ template: Template & { _count?: { interviews: number }; interviews?: Interview[] }
90
+ ): TemplateViewModel {
91
+ const interviewCount = template._count?.interviews ?? template.interviews?.length ?? 0;
92
+ const lastUsed = template.interviews?.length
93
+ ? Math.max(...template.interviews.map((i) => i.createdAt.getTime()))
94
+ : null;
95
+
96
+ return {
97
+ id: template.id,
98
+ name: template.name,
99
+ content: template.content,
100
+ interviewCount,
101
+ lastUsed: lastUsed ? new Date(lastUsed).toISOString().split('T')[0] : null,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * 过滤出已完成的计划
107
+ */
108
+ export function filterCompletedPlans(plans: PlanViewModel[]): PlanViewModel[] {
109
+ return plans.filter((p) => p.status === 'COMPLETED');
110
+ }
111
+
112
+ /**
113
+ * 计算统计摘要
114
+ */
115
+ export function calculatePlanStats(plans: PlanViewModel[]) {
116
+ const total = plans.length;
117
+ const completed = plans.filter((p) => p.status === 'COMPLETED').length;
118
+ const inProgress = plans.filter((p) => p.status === 'IN_PROGRESS').length;
119
+ const avgCompletionRate =
120
+ total > 0 ? plans.reduce((sum, p) => sum + p.completionRate, 0) / total : 0;
121
+
122
+ return { total, completed, inProgress, avgCompletionRate: Math.round(avgCompletionRate) };
123
+ }
124
+ ```
125
+
126
+ ```typescript
127
+ // admin/routes/plans.ts — 使用 ViewModel
128
+ import { mapPlansToViewModels, filterCompletedPlans, calculatePlanStats } from '../view-models';
129
+
130
+ fastify.get('/admin/plans', async (req, reply) => {
131
+ const plans = await prisma.plan.findMany({
132
+ include: { _count: { select: { interviews: true } } },
133
+ });
134
+
135
+ const viewModels = mapPlansToViewModels(plans);
136
+
137
+ return reply.view('admin/plans/list.njk', {
138
+ plans: viewModels,
139
+ stats: calculatePlanStats(viewModels),
140
+ });
141
+ });
142
+
143
+ fastify.get('/admin/plans/completed', async (req, reply) => {
144
+ const plans = await prisma.plan.findMany({
145
+ where: { status: 'COMPLETED' },
146
+ include: { _count: { select: { interviews: true } } },
147
+ });
148
+
149
+ const viewModels = mapPlansToViewModels(plans);
150
+ const completed = filterCompletedPlans(viewModels);
151
+
152
+ return reply.view('admin/plans/completed.njk', { plans: completed });
153
+ });
154
+ ```
@@ -0,0 +1,126 @@
1
+ # Rule 5: Auth Protection — Examples
2
+
3
+ ## ❌ 错误示例:GET 路由未认证
4
+
5
+ ```typescript
6
+ // admin/routes/plans.ts — 反模式
7
+ import { FastifyInstance } from 'fastify';
8
+
9
+ export async function registerPlanRoutes(fastify: FastifyInstance) {
10
+ // GET 路由缺少认证 — 任何人都可查看所有计划
11
+ fastify.get('/admin/plans', async (req, reply) => {
12
+ const plans = await prisma.plan.findMany();
13
+ return reply.view('admin/plans/list.njk', { plans });
14
+ });
15
+
16
+ // 只有 POST 有认证
17
+ fastify.post('/admin/plans', { onRequest: [authenticateAdmin] }, async (req, reply) => {
18
+ // ...
19
+ });
20
+ }
21
+ ```
22
+
23
+ ```typescript
24
+ // admin/routes/analytics.ts — 使用不安全的 API Key
25
+ import { FastifyInstance } from 'fastify';
26
+
27
+ export async function registerAnalyticsRoutes(fastify: FastifyInstance) {
28
+ fastify.get('/admin/analytics', {
29
+ onRequest: [
30
+ // 通过 Header 传递 API Key — 易泄露、不安全
31
+ async (req: FastifyRequest, reply: FastifyReply) => {
32
+ const apiKey = req.headers['x-api-key'];
33
+ if (apiKey !== process.env.ADMIN_API_KEY) {
34
+ return reply.code(401).send({ error: 'Unauthorized' });
35
+ }
36
+ },
37
+ ],
38
+ }, async (req, reply) => {
39
+ // ...
40
+ });
41
+ }
42
+ ```
43
+
44
+ ## ✅ 正确示例:所有路由统一认证
45
+
46
+ ```typescript
47
+ // admin/middleware/authenticate-admin.ts
48
+ import { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
49
+
50
+ export async function authenticateAdmin(
51
+ req: FastifyRequest,
52
+ reply: FastifyReply,
53
+ done: HookHandlerDoneFunction
54
+ ) {
55
+ const session = req.session;
56
+
57
+ if (!session?.userId) {
58
+ return reply
59
+ .code(302)
60
+ .header('Location', '/login?redirect=' + encodeURIComponent(req.url))
61
+ .send();
62
+ }
63
+
64
+ // 验证用户具有 Admin 角色
65
+ const user = await prisma.user.findUnique({
66
+ where: { id: session.userId },
67
+ select: { role: true },
68
+ });
69
+
70
+ if (user?.role !== 'ADMIN') {
71
+ return reply.code(403).view('admin/errors/forbidden.njk', {
72
+ message: 'You do not have permission to access the admin panel.',
73
+ });
74
+ }
75
+
76
+ // 将用户信息附加到请求
77
+ req.user = user;
78
+ done();
79
+ }
80
+ ```
81
+
82
+ ```typescript
83
+ // admin/routes/admin-shared.ts — 全局注册认证中间件
84
+ import { FastifyInstance } from 'fastify';
85
+ import { authenticateAdmin } from '../middleware/authenticate-admin';
86
+
87
+ export function registerAdminShared(fastify: FastifyInstance) {
88
+ // 为所有 /admin/* 路由注册认证 hook
89
+ fastify.addHook('onRequest', authenticateAdmin);
90
+ }
91
+ ```
92
+
93
+ ```typescript
94
+ // admin/routes/plans.ts — 继承全局认证
95
+ import { FastifyInstance } from 'fastify';
96
+
97
+ export async function registerPlanRoutes(fastify: FastifyInstance) {
98
+ // 所有路由自动继承 authenticateAdmin
99
+ // 无需在每个 route 上重复声明
100
+ fastify.get('/admin/plans', async (req, reply) => {
101
+ const plans = await prisma.plan.findMany();
102
+ return reply.view('admin/plans/list.njk', { plans });
103
+ });
104
+
105
+ fastify.post('/admin/plans', async (req, reply) => {
106
+ await prisma.plan.create({ data: req.body as any });
107
+ return reply.redirect('/admin/plans');
108
+ });
109
+ }
110
+ ```
111
+
112
+ ```typescript
113
+ // admin/routes/analytics.ts — 同样保护
114
+ import { FastifyInstance } from 'fastify';
115
+
116
+ export async function registerAnalyticsRoutes(fastify: FastifyInstance) {
117
+ // GET 路由同样受认证保护
118
+ fastify.get('/admin/analytics', async (req, reply) => {
119
+ const stats = await prisma.plan.aggregate({
120
+ _count: { id: true },
121
+ _avg: { completionRate: true },
122
+ });
123
+ return reply.view('admin/analytics/dashboard.njk', { stats });
124
+ });
125
+ }
126
+ ```