@teamix-evo/skills 0.3.0 → 0.4.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 (66) hide show
  1. package/manifest.json +61 -45
  2. package/package.json +2 -2
  3. package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/SKILL.md +18 -18
  4. package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/checklist.md +2 -2
  5. package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/reuse-first.md +25 -17
  6. package/src/teamix-evo-code-uni-manager/SKILL.md +95 -0
  7. package/src/teamix-evo-code-uni-manager/api-layering.md +370 -0
  8. package/src/teamix-evo-code-uni-manager/checklist.md +193 -0
  9. package/src/teamix-evo-code-uni-manager/error-and-loading.md +389 -0
  10. package/src/teamix-evo-code-uni-manager/file-structure.md +339 -0
  11. package/src/teamix-evo-code-uni-manager/forms-and-validation.md +459 -0
  12. package/src/teamix-evo-code-uni-manager/reuse-first.md +188 -0
  13. package/src/teamix-evo-code-uni-manager/routing-and-codesplit.md +450 -0
  14. package/src/teamix-evo-code-uni-manager/testing.md +396 -0
  15. package/src/teamix-evo-design-opentrek/SKILL.md +71 -0
  16. package/src/teamix-evo-design-opentrek/boundaries.md +513 -0
  17. package/src/teamix-evo-design-opentrek/brand.md +154 -0
  18. package/src/teamix-evo-design-opentrek/checklist.md +83 -0
  19. package/src/teamix-evo-design-opentrek/components.md +245 -0
  20. package/src/teamix-evo-design-opentrek/flows.md +51 -0
  21. package/src/teamix-evo-design-opentrek/foundations.md +271 -0
  22. package/src/teamix-evo-design-opentrek/generation-flow.md +185 -0
  23. package/src/teamix-evo-design-opentrek/patterns/dashboard.md +31 -0
  24. package/src/teamix-evo-design-opentrek/patterns/detail-page.md +202 -0
  25. package/src/teamix-evo-design-opentrek/patterns/form-page.md +289 -0
  26. package/src/teamix-evo-design-opentrek/patterns/list-page.md +334 -0
  27. package/src/teamix-evo-design-opentrek/patterns/page-types.md +154 -0
  28. package/src/teamix-evo-design-opentrek/philosophy.md +96 -0
  29. package/src/teamix-evo-design-opentrek/rules/README.md +39 -0
  30. package/src/teamix-evo-design-opentrek/rules/boundaries.rules.json +391 -0
  31. package/src/teamix-evo-design-uni-manager/SKILL.md +73 -0
  32. package/src/teamix-evo-design-uni-manager/boundaries.md +564 -0
  33. package/src/teamix-evo-design-uni-manager/brand.md +202 -0
  34. package/src/teamix-evo-design-uni-manager/checklist.md +115 -0
  35. package/src/teamix-evo-design-uni-manager/components.md +254 -0
  36. package/src/teamix-evo-design-uni-manager/flows.md +63 -0
  37. package/src/teamix-evo-design-uni-manager/foundations.md +258 -0
  38. package/src/teamix-evo-design-uni-manager/generation-flow.md +194 -0
  39. package/src/teamix-evo-design-uni-manager/patterns/dashboard.md +95 -0
  40. package/src/teamix-evo-design-uni-manager/patterns/detail-page.md +224 -0
  41. package/src/teamix-evo-design-uni-manager/patterns/form-page.md +329 -0
  42. package/src/teamix-evo-design-uni-manager/patterns/list-page.md +356 -0
  43. package/src/teamix-evo-design-uni-manager/patterns/page-types.md +165 -0
  44. package/src/teamix-evo-design-uni-manager/philosophy.md +106 -0
  45. package/src/teamix-evo-design-uni-manager/rules/README.md +49 -0
  46. package/src/teamix-evo-design-uni-manager/rules/boundaries.rules.json +418 -0
  47. package/src/teamix-evo-manage/SKILL.md +281 -0
  48. package/skills/teamix-evo-design-rules/SKILL.md +0 -86
  49. package/skills/teamix-evo-design-rules/boundaries.md +0 -89
  50. package/skills/teamix-evo-design-rules/checklist.md +0 -108
  51. package/skills/teamix-evo-design-rules/generation-flow.md +0 -142
  52. package/skills/teamix-evo-design-rules/prompts/page-design.md +0 -148
  53. package/skills/teamix-evo-design-rules-opentrek/SKILL.md +0 -48
  54. package/skills/teamix-evo-design-rules-opentrek/brand-rules.md +0 -74
  55. package/skills/teamix-evo-design-rules-uni-manager/SKILL.md +0 -51
  56. package/skills/teamix-evo-design-rules-uni-manager/ai-scenarios.md +0 -51
  57. package/skills/teamix-evo-design-rules-uni-manager/command-center.md +0 -108
  58. package/skills/teamix-evo-design-rules-uni-manager/danger-ops.md +0 -87
  59. package/skills/teamix-evo-manage/SKILL.md +0 -178
  60. package/skills/teamix-evo-ui-upgrade/SKILL.md +0 -75
  61. /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/api-layering.md +0 -0
  62. /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/error-and-loading.md +0 -0
  63. /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/file-structure.md +0 -0
  64. /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/forms-and-validation.md +0 -0
  65. /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/routing-and-codesplit.md +0 -0
  66. /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/testing.md +0 -0
@@ -0,0 +1,389 @@
1
+ # 错误与加载态规范(uni-manager)
2
+
3
+ > **核心约定**:渲染错误用 `ErrorBoundary`,异步加载用 `react-query` 的 `isPending/isError`(必要时叠 `Suspense`),全局兜底必须存在。**不允许**白屏 / 红屏直出给用户。**uni-manager 额外提供跨云资源专属 fallback。**
4
+
5
+ ---
6
+
7
+ ## 三层兜底
8
+
9
+ ```
10
+ ┌──────────────────────────────────────────┐
11
+ │ App 根:全局 ErrorBoundary + Toaster │ 最后一道:防白屏 / 兜未捕获
12
+ └──────────────┬───────────────────────────┘
13
+
14
+ ┌──────────────────────────────────────────┐
15
+ │ 路由 / 关键 widget:页面级 ErrorBoundary │ 范围隔离:一页崩不带垮全站
16
+ └──────────────┬───────────────────────────┘
17
+
18
+ ┌──────────────────────────────────────────┐
19
+ │ 数据 hook:isPending / isError │ 细粒度:每块数据自己的 loading
20
+ └──────────────────────────────────────────┘
21
+ ```
22
+
23
+ ---
24
+
25
+ ## §1 · 全局 ErrorBoundary
26
+
27
+ 每个 teamix-evo uni-manager 业务应用**必须**在根挂一个全局 ErrorBoundary,以及一个 toast 容器。
28
+
29
+ ### 推荐实现:`react-error-boundary`
30
+
31
+ 业界主流(2024+,Vercel / shadcn 模板默认),比手写 class component 更易组合。
32
+
33
+ ```tsx
34
+ // src/main.tsx
35
+ import { ErrorBoundary } from 'react-error-boundary';
36
+ import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
37
+ import { Toaster } from '@/components/ui';
38
+ import { GlobalErrorFallback } from '@/components/GlobalErrorFallback';
39
+ import { TenantProvider } from '@/contexts/TenantContext';
40
+ import { RegionProvider } from '@/contexts/RegionContext';
41
+ import { reportError } from '@/lib/monitor';
42
+
43
+ const queryClient = new QueryClient({
44
+ defaultOptions: {
45
+ queries: { staleTime: 30_000, retry: 1 },
46
+ mutations: { retry: 0 },
47
+ },
48
+ });
49
+
50
+ createRoot(document.getElementById('root')!).render(
51
+ <ErrorBoundary
52
+ FallbackComponent={GlobalErrorFallback}
53
+ onError={(err, info) => reportError(err, info)}
54
+ >
55
+ <QueryClientProvider client={queryClient}>
56
+ <TenantProvider>
57
+ <RegionProvider>
58
+ <App />
59
+ <Toaster />
60
+ </RegionProvider>
61
+ </TenantProvider>
62
+ </QueryClientProvider>
63
+ </ErrorBoundary>,
64
+ );
65
+ ```
66
+
67
+ ```tsx
68
+ // src/components/GlobalErrorFallback.tsx
69
+ import { Button } from '@teamix-evo/ui';
70
+
71
+ export function GlobalErrorFallback({
72
+ resetErrorBoundary,
73
+ }: {
74
+ resetErrorBoundary: () => void;
75
+ }) {
76
+ return (
77
+ <div className="flex min-h-screen items-center justify-center">
78
+ <div className="text-center">
79
+ <h1 className="text-2xl font-semibold">页面出错了</h1>
80
+ <p className="text-muted-foreground mt-2">
81
+ 已记录,可尝试刷新或返回首页
82
+ </p>
83
+ <Button className="mt-4" onClick={resetErrorBoundary}>
84
+ 重新加载
85
+ </Button>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+ ```
91
+
92
+ 要点:
93
+
94
+ - `onError` **必须**调 `reportError` 上报(Sentry / 自家日志)
95
+ - fallback **必须**给用户可执行的下一步动作(重试 / 返回 / 联系支持)
96
+ - 上报 extra **必须**带当前 `tenantId / regionId / cloudProvider`(便于排障)
97
+ - **不要**只显示 "Something went wrong" —— 用户看到等于白屏
98
+
99
+ ---
100
+
101
+ ## §2 · 路由 / 页面级 ErrorBoundary
102
+
103
+ 跨路由的崩溃不应连带全站。在 router 配置或 Layout 里再嵌一层:
104
+
105
+ ```tsx
106
+ // src/routes/index.tsx
107
+ {
108
+ path: '/instances/:id',
109
+ element: (
110
+ <ErrorBoundary FallbackComponent={PageErrorFallback}>
111
+ <Suspense fallback={<PageSkeleton />}>
112
+ <InstanceDetailPage />
113
+ </Suspense>
114
+ </ErrorBoundary>
115
+ ),
116
+ }
117
+ ```
118
+
119
+ 或使用 react-router v6.4+ 的 `errorElement`(推荐,内置):
120
+
121
+ ```tsx
122
+ {
123
+ path: '/instances/:id',
124
+ element: <InstanceDetailPage />,
125
+ errorElement: <PageErrorFallback />,
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## §3 · 数据加载态(react-query 范式)
132
+
133
+ **不要**在组件里用 `useState<boolean>` + `useEffect` 自己管 loading。所有异步数据走 react-query:
134
+
135
+ ```tsx
136
+ function InstanceListPage() {
137
+ const { data, isPending, isError, error, refetch } = useInstanceList(params);
138
+
139
+ if (isPending) return <TableSkeleton rows={10} />;
140
+ if (isError) return <ErrorPanel message={error.message} onRetry={refetch} />;
141
+
142
+ return <InstanceTable data={data} />;
143
+ }
144
+ ```
145
+
146
+ ### 三种状态都要处理
147
+
148
+ | 状态 | 必须输出 |
149
+ | ---------------------- | ------------------------------------------ |
150
+ | `isPending`(首次加载) | Skeleton / Spinner,**不允许**显示空 div |
151
+ | `isError` | 错误面板 + 重试按钮,**不允许**直接 `throw` |
152
+ | `isFetching`(后台刷新) | 表格右上角小转圈即可,不要遮罩 |
153
+
154
+ ### Skeleton vs Spinner 怎么选
155
+
156
+ | 场景 | 用 |
157
+ | ------------------------------ | ---------------------------------- |
158
+ | 列表 / 卡片 / 详情 | Skeleton(撑住布局,避免 CLS) |
159
+ | 短动作(按钮提交、< 500ms 加载) | Spinner |
160
+ | 全屏切换 | PageSkeleton 或 logo loading |
161
+ | 切换租户/区域(invalidate 全表) | 表格遮罩 Skeleton + topbar Spinner |
162
+
163
+ ---
164
+
165
+ ## §4 · Mutation 反馈
166
+
167
+ ```tsx
168
+ const mutation = useCreateInstance();
169
+
170
+ mutation.mutate(values, {
171
+ onSuccess: (instance) => {
172
+ toast.success(`实例 ${instance.id} 已创建`);
173
+ navigate(`/instances/${instance.id}`);
174
+ },
175
+ onError: (err) => {
176
+ toast.error(err.message); // 已被 lib/http.ts 归一化
177
+ },
178
+ });
179
+ ```
180
+
181
+ 约束:
182
+
183
+ - 成功 → `toast.success` + 跳转 / invalidate
184
+ - 失败 → `toast.error`,**不要** `alert()`
185
+ - 长操作(> 1s) → 按钮 `disabled` + 文案改"提交中…",**不要**双击重复提交
186
+ - **不要**在 mutation 内部弹 toast —— hook 是数据通道,toast 由组件触发
187
+ - **危险操作(删除 / 释放 / 销毁)走 `useDangerConfirm`**,见 [`forms-and-validation.md`](forms-and-validation.md) §10
188
+
189
+ ---
190
+
191
+ ## §5 · Suspense 数据边界(可选,渐进采用)
192
+
193
+ react-query v5 支持 `useSuspenseQuery`,可把 loading 转给 `<Suspense>`:
194
+
195
+ ```tsx
196
+ function InstanceListPage() {
197
+ const { data } = useSuspenseQuery({
198
+ queryKey: ['instances', tenantId, regionId],
199
+ queryFn: () => listInstances(),
200
+ });
201
+ return <InstanceTable data={data} />; // 不需要 isPending 分支
202
+ }
203
+
204
+ // 父级:
205
+ <Suspense fallback={<TableSkeleton rows={10} />}>
206
+ <InstanceListPage />
207
+ </Suspense>;
208
+ ```
209
+
210
+ **何时用**:
211
+
212
+ - 多个 query 需要"全部 ready 才渲染"时,Suspense 比手写 `if (a.isPending || b.isPending)` 简洁
213
+ - 与 `React.lazy` 配合做代码分包(见 [`routing-and-codesplit.md`](routing-and-codesplit.md))
214
+
215
+ **何时不用**:
216
+
217
+ - 单一 query 的简单页面,直接 `isPending` 分支更直观
218
+ - 团队对 Suspense 还不熟 → 先用 `isPending`,慢慢迁
219
+
220
+ ---
221
+
222
+ ## §6 · 全局错误上报
223
+
224
+ ```ts
225
+ // src/lib/monitor.ts
226
+ import * as Sentry from '@sentry/react';
227
+ import {
228
+ getActiveTenant,
229
+ getActiveRegion,
230
+ getActiveCloudProvider,
231
+ } from '@/lib/active-context';
232
+
233
+ if (import.meta.env.PROD) {
234
+ Sentry.init({
235
+ dsn: import.meta.env.VITE_SENTRY_DSN,
236
+ tracesSampleRate: 0.1,
237
+ });
238
+ }
239
+
240
+ export function reportError(err: unknown, extra?: Record<string, unknown>) {
241
+ const ctx = {
242
+ tenantId: getActiveTenant()?.id,
243
+ regionId: getActiveRegion()?.id,
244
+ cloudProvider: getActiveCloudProvider(),
245
+ ...extra,
246
+ };
247
+ if (import.meta.env.DEV) console.error('[reportError]', err, ctx);
248
+ Sentry.captureException(err, { extra: ctx });
249
+ }
250
+
251
+ // window 级未捕获
252
+ window.addEventListener('unhandledrejection', (e) => reportError(e.reason));
253
+ window.addEventListener('error', (e) => reportError(e.error));
254
+ ```
255
+
256
+ 要点:
257
+
258
+ - 所有 ErrorBoundary 的 `onError` **必须**调 `reportError`
259
+ - 上报必须**自动带上当前 tenant/region/cloud 上下文**(不需要 component 显式传)
260
+ - mutation `onError` 在归一化 toast 之外也应调 `reportError`
261
+ - **不要**在 component 里直接 `import * as Sentry` —— 走 `monitor.ts` 这一层包装,便于替换
262
+
263
+ ---
264
+
265
+ ## §7 · 跨云资源专属 fallback(uni-manager 增量)
266
+
267
+ 跨云资源(实例、对象存储桶、网络等)有独有的失败场景:
268
+
269
+ | 场景 | HTTP 行为 | 推荐 UI |
270
+ | --------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------- |
271
+ | 资源被云厂商侧异步删除 | 200 + `{ status: 'NotFound' }` 或 404 | `CloudResourceNotFound`:展示"资源已被删除/迁移" + 当前 cloud/region badge |
272
+ | 跨云聚合查询某 cloud 失败 | 部分成功(其他 cloud 仍返数据) | 表格保留可用数据 + 顶部黄条提示"AWS region us-east-1 暂时不可达,显示部分结果" |
273
+ | 切换租户后旧 URL 资源不可见 | 403 + `{ code: 'TENANT_RESOURCE_NOT_VISIBLE' }` | `CrossTenantNotice`:提示"该资源属于其他租户" + 提供"切换回上一租户"快捷动作 |
274
+ | 区域 metadata 加载失败 | 500 + `{ code: 'REGION_METADATA_UNAVAILABLE' }` | `RegionDegraded`:用上次成功的 region 元数据 + 顶部红条提示"实时数据不可用" |
275
+
276
+ ### 实现示例
277
+
278
+ ```tsx
279
+ // src/components/cloud/CloudResourceNotFound.tsx
280
+ import { Button } from '@teamix-evo/ui';
281
+ import { CloudBadge } from './CloudBadge';
282
+ import { useNavigate } from 'react-router-dom';
283
+ import { useTenant } from '@/hooks/useTenant';
284
+ import { useRegion } from '@/hooks/useRegion';
285
+
286
+ interface Props {
287
+ resourceType: string; // 实例 / 桶 / VPC ...
288
+ resourceId: string;
289
+ }
290
+
291
+ export function CloudResourceNotFound({ resourceType, resourceId }: Props) {
292
+ const navigate = useNavigate();
293
+ const { tenant } = useTenant();
294
+ const { region } = useRegion();
295
+ return (
296
+ <div className="flex flex-col items-center gap-3 py-12">
297
+ <h2 className="text-xl font-semibold">{resourceType} 不存在或已被删除</h2>
298
+ <p className="text-muted-foreground text-sm">
299
+ ID: <code>{resourceId}</code>
300
+ </p>
301
+ <div className="flex items-center gap-2">
302
+ <CloudBadge tenant={tenant} region={region} />
303
+ </div>
304
+ <p className="text-muted-foreground max-w-md text-center text-sm">
305
+ 资源可能已在云厂商侧被释放或迁移。请检查租户/区域上下文,或返回列表查看其他资源。
306
+ </p>
307
+ <div className="mt-2 flex gap-2">
308
+ <Button variant="outline" onClick={() => navigate(-1)}>
309
+ 返回
310
+ </Button>
311
+ <Button onClick={() => navigate(`/${resourceType}s`)}>返回列表</Button>
312
+ </div>
313
+ </div>
314
+ );
315
+ }
316
+ ```
317
+
318
+ ### 详情页用法
319
+
320
+ ```tsx
321
+ function InstanceDetailPage() {
322
+ const { id } = useParams();
323
+ const { data, isPending, isError, error } = useInstance(id!);
324
+
325
+ if (isPending) return <PageSkeleton />;
326
+ if (isError && isCloudResourceNotFound(error)) {
327
+ return <CloudResourceNotFound resourceType="实例" resourceId={id!} />;
328
+ }
329
+ if (isError) return <ErrorPanel message={error.message} />;
330
+ return <InstanceDetail data={data} />;
331
+ }
332
+ ```
333
+
334
+ 要点:
335
+
336
+ - `isCloudResourceNotFound(err)` 在 `lib/http.ts` 定义,统一识别 `{ status: 'NotFound' }` / 404
337
+ - 跨云聚合查询的部分失败,在 service 层就要返回 `{ ok: [], failed: [{ cloud, region, error }] }` 结构,UI 层只展示
338
+ - **禁止**用通用 `<ErrorPanel>` 兜底跨云资源 404 —— 必须给"上下文相关"提示
339
+
340
+ ---
341
+
342
+ ## 反模式速查
343
+
344
+ | 反模式 | 为什么禁 | 应该 |
345
+ | -------------------------------------------- | --------------------------------- | ------------------------------------------------- |
346
+ | 没有全局 ErrorBoundary | 异常直接白屏 | App 根挂一层 |
347
+ | `useState(false)` + `useEffect` 管 loading | 重复造轮,易漏 cancel | 走 react-query / useSuspenseQuery |
348
+ | 只判 `isLoading` 不判 `isError` | 错误态显示空白 | 三态都处理 |
349
+ | `if (data) return <>...</> else return null` | 无 skeleton,布局抖动 + CLS | 加 Skeleton |
350
+ | `alert('保存失败')` | 阻断用户、丑、不可样式化 | `toast.error()` |
351
+ | ErrorBoundary 不上报 | 错过线上 bug | `onError` 接 `reportError` |
352
+ | reportError 不带 tenant/region 上下文 | 排障无法定位 | `monitor.ts` 自动注入 active context |
353
+ | service 里 `try/catch` 吞错 | 上层失去判断依据 | 让 error 抛上来,hook / boundary 决定 |
354
+ | 跨云资源 404 用通用 ErrorPanel 兜底 | 用户看不出是上下文问题 | 用 `CloudResourceNotFound` + cloud/region badge |
355
+ | 跨云聚合查询全或全无 | 一个 region 失败,其他可用结果丢失 | service 层返结构化结果,UI 显示部分成功 + 失败提示 |
356
+
357
+ ---
358
+
359
+ ## 与 ui 包的边界
360
+
361
+ `@teamix-evo/ui` 提供:
362
+
363
+ - `Skeleton` / `TableSkeleton` / `PageSkeleton`
364
+ - `Toaster` + `toast()` API
365
+ - `Spinner`(短动作)
366
+ - `EmptyState`(无数据态,区别于错误态)
367
+
368
+ **优先用 ui 包**,不要自己撸 spinner / toast。
369
+
370
+ uni-manager 工程内**额外**提供(后续抽到 biz-ui/uni-manager):
371
+
372
+ - `CloudResourceNotFound`(跨云资源 404)
373
+ - `CrossTenantNotice`(跨租户访问提示)
374
+ - `RegionDegraded`(区域降级提示)
375
+
376
+ ---
377
+
378
+ ## AI 必须输出的错误处理日志
379
+
380
+ ```
381
+ ## 错误 / 加载态
382
+
383
+ - 全局: ✅ App 根 ErrorBoundary + reportError 已存在(自动带 tenant/region/cloud)
384
+ - 页面: ✅ /instances/:id 加 errorElement + Suspense fallback
385
+ - 数据: ✅ useInstanceList 三态都处理(Skeleton / ErrorPanel / Table)
386
+ - Mutation: ✅ onSuccess toast + navigate;onError toast(已归一化)
387
+ - 跨云 404: ✅ /instances/:id 区分 isCloudResourceNotFound,显示 CloudResourceNotFound
388
+ - 危险操作: ✅ 释放/删除走 useDangerConfirm
389
+ ```