@teamix-evo/skills 0.2.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 (56) hide show
  1. package/README.md +18 -8
  2. package/_template/SKILL.md.hbs +14 -1
  3. package/manifest.json +79 -6
  4. package/package.json +7 -3
  5. package/src/teamix-evo-code-opentrek/SKILL.md +92 -0
  6. package/src/teamix-evo-code-opentrek/api-layering.md +225 -0
  7. package/src/teamix-evo-code-opentrek/checklist.md +173 -0
  8. package/src/teamix-evo-code-opentrek/error-and-loading.md +269 -0
  9. package/src/teamix-evo-code-opentrek/file-structure.md +273 -0
  10. package/src/teamix-evo-code-opentrek/forms-and-validation.md +220 -0
  11. package/src/teamix-evo-code-opentrek/reuse-first.md +130 -0
  12. package/src/teamix-evo-code-opentrek/routing-and-codesplit.md +298 -0
  13. package/src/teamix-evo-code-opentrek/testing.md +313 -0
  14. package/src/teamix-evo-code-uni-manager/SKILL.md +95 -0
  15. package/src/teamix-evo-code-uni-manager/api-layering.md +370 -0
  16. package/src/teamix-evo-code-uni-manager/checklist.md +193 -0
  17. package/src/teamix-evo-code-uni-manager/error-and-loading.md +389 -0
  18. package/src/teamix-evo-code-uni-manager/file-structure.md +339 -0
  19. package/src/teamix-evo-code-uni-manager/forms-and-validation.md +459 -0
  20. package/src/teamix-evo-code-uni-manager/reuse-first.md +188 -0
  21. package/src/teamix-evo-code-uni-manager/routing-and-codesplit.md +450 -0
  22. package/src/teamix-evo-code-uni-manager/testing.md +396 -0
  23. package/src/teamix-evo-design-opentrek/SKILL.md +71 -0
  24. package/src/teamix-evo-design-opentrek/boundaries.md +513 -0
  25. package/src/teamix-evo-design-opentrek/brand.md +154 -0
  26. package/src/teamix-evo-design-opentrek/checklist.md +83 -0
  27. package/src/teamix-evo-design-opentrek/components.md +245 -0
  28. package/src/teamix-evo-design-opentrek/flows.md +51 -0
  29. package/src/teamix-evo-design-opentrek/foundations.md +271 -0
  30. package/src/teamix-evo-design-opentrek/generation-flow.md +185 -0
  31. package/src/teamix-evo-design-opentrek/patterns/dashboard.md +31 -0
  32. package/src/teamix-evo-design-opentrek/patterns/detail-page.md +202 -0
  33. package/src/teamix-evo-design-opentrek/patterns/form-page.md +289 -0
  34. package/src/teamix-evo-design-opentrek/patterns/list-page.md +334 -0
  35. package/src/teamix-evo-design-opentrek/patterns/page-types.md +154 -0
  36. package/src/teamix-evo-design-opentrek/philosophy.md +96 -0
  37. package/src/teamix-evo-design-opentrek/rules/README.md +39 -0
  38. package/src/teamix-evo-design-opentrek/rules/boundaries.rules.json +391 -0
  39. package/src/teamix-evo-design-uni-manager/SKILL.md +73 -0
  40. package/src/teamix-evo-design-uni-manager/boundaries.md +564 -0
  41. package/src/teamix-evo-design-uni-manager/brand.md +202 -0
  42. package/src/teamix-evo-design-uni-manager/checklist.md +115 -0
  43. package/src/teamix-evo-design-uni-manager/components.md +254 -0
  44. package/src/teamix-evo-design-uni-manager/flows.md +63 -0
  45. package/src/teamix-evo-design-uni-manager/foundations.md +258 -0
  46. package/src/teamix-evo-design-uni-manager/generation-flow.md +194 -0
  47. package/src/teamix-evo-design-uni-manager/patterns/dashboard.md +95 -0
  48. package/src/teamix-evo-design-uni-manager/patterns/detail-page.md +224 -0
  49. package/src/teamix-evo-design-uni-manager/patterns/form-page.md +329 -0
  50. package/src/teamix-evo-design-uni-manager/patterns/list-page.md +356 -0
  51. package/src/teamix-evo-design-uni-manager/patterns/page-types.md +165 -0
  52. package/src/teamix-evo-design-uni-manager/philosophy.md +106 -0
  53. package/src/teamix-evo-design-uni-manager/rules/README.md +49 -0
  54. package/src/teamix-evo-design-uni-manager/rules/boundaries.rules.json +418 -0
  55. package/src/teamix-evo-manage/SKILL.md +281 -0
  56. package/skills/teamix-evo-manage/SKILL.md +0 -138
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: teamix-evo-code-uni-manager
3
+ description: |
4
+ Enforce teamix-evo coding conventions in uni-manager (hybrid-cloud / multi-tenant console) consumer apps — reuse-first against `@teamix-evo/ui` registry + `biz-ui/uni-manager` (um-topbar / CloudBadge), API code under `src/services/` with tenant / region context propagation, `src/` layering and import boundaries.
5
+ TRIGGER when: user asks to "新增 / 重构 / 写一个" 组件 / 页面 / 接口 / 钩子 / 工具 / hook in a hybrid-cloud / 专有云 / 多租户 / 多区域 console; phrases like "create a CRUD page"、"add an API call"、"extract a hook"、"refactor this fetch"、"调一下接口"、"加个组件"、"加个按钮"; file write under `src/pages/**`、`src/components/**`、`src/services/**`、`src/hooks/**`; AI is about to write any new `.tsx` / `.ts` file in a teamix-evo-installed uni-manager-variant project.
6
+ SKIP: pure visual / layout design questions about an already-existing screen with no code change (defer to teamix-evo-design-uni-manager); teamix-evo lifecycle (init / update / uninstall) — those go to teamix-evo-manage; changes only to design tokens / styles / theme — those go to ESLint and `tokens.overrides.css`.
7
+ Coordinates with: teamix-evo-design-uni-manager (run alongside when the change also creates a UI screen — design handles the visual side, this skill handles file placement and reuse).
8
+ ---
9
+
10
+ # teamix-evo-code-uni-manager
11
+
12
+ This skill bundles the **AI-readable engineering conventions** for **uni-manager-variant** consumer business apps — products that target hybrid-cloud / 专有云 / 多租户 / 多区域 management consoles. It complements [`teamix-evo-design-uni-manager`](../teamix-evo-design-uni-manager/SKILL.md) — design tells AI _what a screen should look like_; this skill tells AI _where the code should live, what to reuse before writing new code, and how to thread tenant / region / cloud-provider context through the layers_.
13
+
14
+ <!-- teamix-evo:managed:start id="core" -->
15
+
16
+ ## When to use
17
+
18
+ Activate this skill whenever AI is about to **write or refactor code** inside a uni-manager-variant consumer app. Common signals:
19
+
20
+ - "新增一个 xxx 页面 / 列表页 / 详情页(控制台 / 工作台 / 多云资源)"
21
+ - "加一个 xxx 接口 / 调用 xxx API(多租户 / 跨云 / 跨账号)"
22
+ - "写一个 xxx 组件"
23
+ - "把这段重构一下"
24
+ - "create a CRUD page for 多云实例"
25
+ - "add an API call to list instances across regions"
26
+ - Any time AI is about to create a new file under `src/pages/`、`src/components/`、`src/services/`、`src/hooks/` in a hybrid-cloud console
27
+
28
+ If the task is purely about _visual design_ of a screen, use [`teamix-evo-design-uni-manager`](../teamix-evo-design-uni-manager/SKILL.md) instead — or run both in tandem.
29
+
30
+ ## What this skill does
31
+
32
+ Before AI writes or commits code, it performs an **8-step gated flow**. Steps 1-4 are baseline (always run); steps 5-7 are topic gates (run when the task touches that topic); step 8 is the final self-review.
33
+
34
+ 1. **Reuse-first check** — read [`reuse-first.md`](reuse-first.md). Before creating any new component, query the `@teamix-evo/ui` registry (via MCP `list_components` / `find_components`), the **biz-ui/uni-manager** layer (um-topbar / CloudBadge / ContextSwitcher placeholders), and grep the local project. Only write new code when no reuse path exists. **组件兜底铁律**:找不到时优先 ui 原子件组合,**禁止**自撸基础件。
35
+ 2. **Layering check** — read [`api-layering.md`](api-layering.md). Any code that talks to a backend goes under `src/services/<domain>.ts`; multi-tenant / multi-region / multi-cloud context (tenantId / regionId / cloudProvider) is **propagated through `lib/http.ts` interceptors**, not as ad-hoc params per call. Components never call `fetch` / `axios` directly. Data hooks live in `src/hooks/` and consume services.
36
+ 3. **Directory check** — read [`file-structure.md`](file-structure.md). Place the new file under the right top-level folder; uni-manager-specific shells (TenantContext / RegionContext / CloudBadge) have conventional locations.
37
+ 4. **Forms gate** — if the task involves a form, read [`forms-and-validation.md`](forms-and-validation.md). `react-hook-form` + `zod`; schema lives at `src/services/<domain>.schema.ts`. Dangerous operations (delete / release / destroy) **must** route through `useDangerConfirm` (input resource name).
38
+ 5. **Error/loading gate** — if the task adds a page or data hook, read [`error-and-loading.md`](error-and-loading.md). Ensure global ErrorBoundary, page-level fallback, and three-state handling (`isPending` / `isError` / data) are in place; mutations report success/error via `toast`. Cross-cloud resource-not-found has a dedicated fallback.
39
+ 6. **Routing gate** — if the task adds a route or page-entry, read [`routing-and-codesplit.md`](routing-and-codesplit.md). Pages use `React.lazy`; auth/role guards live in `src/routes/guards.tsx`; **list-page filters MUST sync `tenantId` / `regionId` / `cloudProvider` into URL search params**; um-topbar context switch rewrites the current URL.
40
+ 7. **Testing gate** — read [`testing.md`](testing.md). Pure functions and zod schemas are **mandatory** to test; tenant-context-aware http handlers must be covered by msw.
41
+ 8. **Self-review** — run [`checklist.md`](checklist.md). Every item must pass before declaring the change done.
42
+
43
+ ## Inputs the user provides
44
+
45
+ - Intent: "add a page", "add a service call", "refactor X", "extract a hook", etc.
46
+ - Optional: domain entity (`instance`、`database`、`tenant`、`workorder`、`audit-log`)
47
+ - Optional: existing file the change should land near
48
+ - Optional: cloud-provider scope(only-aliyun / multi-cloud / private-only)
49
+
50
+ ## Outputs
51
+
52
+ - Code placed under the conventional path
53
+ - Reuse decisions explicitly logged ("reused `Button` from `@teamix-evo/ui`" / "reused `um-topbar` from `biz-ui/uni-manager`" / "no match found, wrote a new `OrderCard`")
54
+ - Tenant / region / cloud context propagation logged
55
+ - Pass / fail status against [`checklist.md`](checklist.md)
56
+
57
+ ## How to invoke (typical flow)
58
+
59
+ 1. Parse user intent → identify which artifact is being created (page / component / service / hook / util / form / route)
60
+ 2. Read [`reuse-first.md`](reuse-first.md) and run the reuse query (MCP first, then biz-ui/uni-manager, then local grep)
61
+ 3. Read [`file-structure.md`](file-structure.md) to pick the destination directory
62
+ 4. If the change touches network / backend, read [`api-layering.md`](api-layering.md) — confirm tenant / region / cloud headers are injected via interceptor
63
+ 5. If the change involves a form, read [`forms-and-validation.md`](forms-and-validation.md) — for dangerous operations, plug `useDangerConfirm`
64
+ 6. If the change adds a page / data hook, read [`error-and-loading.md`](error-and-loading.md) for fallback / Skeleton / toast wiring
65
+ 7. If the change adds a route or page entry, read [`routing-and-codesplit.md`](routing-and-codesplit.md) — confirm tenant / region URL sync
66
+ 8. Decide test coverage per [`testing.md`](testing.md); write `*.test.ts(x)` next to source
67
+ 9. Write the code; cite each reuse / new-write decision in the response
68
+ 10. Run through [`checklist.md`](checklist.md); list pass / fail explicitly
69
+
70
+ ## Files in this skill
71
+
72
+ | File | Purpose |
73
+ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
74
+ | [`reuse-first.md`](reuse-first.md) | 复用决策流(ui 原子件 + biz-ui/uni-manager + 本项目);组件兜底铁律 |
75
+ | [`file-structure.md`](file-structure.md) | 顶层 `src/` 目录骨架、归属、import 边界、命名、全局态分级(含 TenantContext / RegionContext) |
76
+ | [`api-layering.md`](api-layering.md) | API 三层结构 + 多租户 / 多区域 / 跨云上下文注入 |
77
+ | [`forms-and-validation.md`](forms-and-validation.md) | `react-hook-form` + `zod`;危险操作走 `useDangerConfirm`(输入资源名称确认) |
78
+ | [`error-and-loading.md`](error-and-loading.md) | 全局 / 页面 ErrorBoundary、三态、Suspense、toast、跨云资源专属 fallback |
79
+ | [`routing-and-codesplit.md`](routing-and-codesplit.md) | `React.lazy`、guards、404/403/500、tenant/region/cloud 进 URL search params、um-topbar 切换 URL 改写 |
80
+ | [`testing.md`](testing.md) | `vitest` + RTL + msw、就近 test、tenant-context handler 覆盖 |
81
+ | [`checklist.md`](checklist.md) | 多段自检,含 uni-manager 红线(自建 topbar / 不带租户上下文 / 危险操作未输入名称) |
82
+
83
+ ## Relationship to other skills
84
+
85
+ - [`teamix-evo-design-uni-manager`](../teamix-evo-design-uni-manager/SKILL.md) — visual / interaction rules for uni-manager screen generation. Run **alongside** this skill when the task includes UI; this skill never overrides design on visual concerns.
86
+ - [`teamix-evo-code-opentrek`](../teamix-evo-code-opentrek/SKILL.md) — sister code skill for OpenTrek variant. The two skills share 80% of conventions (file structure / layering / forms / testing); uni-manager-specific deltas are in this skill's SKILL/reuse-first/api-layering/routing/checklist.
87
+ - [`teamix-evo-manage`](../teamix-evo-manage/SKILL.md) — lifecycle (`init` / `update` / `uninstall` / `skills add`). Out of scope here.
88
+
89
+ ## Why these conventions(uni-manager 视角)
90
+
91
+ - **Single source for UI** — `@teamix-evo/ui` 89 原子件 + `biz-ui/uni-manager`(当前 1 实物 um-topbar,其余如 CloudBadge / ContextSwitcher 是概念占位)。Re-implementing 顶部 topbar / 基础件会破坏一致性三件套(UM1/UM2/UM3)。
92
+ - **Stable seams for change** — tenant / region / cloud 上下文集中在 `lib/http.ts` interceptor 与 `contexts/TenantContext.tsx`,新增云厂商或区域不需要改 89 处 service 调用。
93
+ - **Predictable file location** — 所有 uni-manager 项目目录形状一致;新接手的人 5 分钟内能找到「租户切换在哪管」「跨云列表怎么写」「危险操作怎么二次确认」。
94
+
95
+ <!-- teamix-evo:managed:end -->
@@ -0,0 +1,370 @@
1
+ # API 数据层规范(uni-manager)
2
+
3
+ > **核心原则**:组件不直接 fetch。所有与后端通信的代码都落在 `src/services/`,通过 `src/hooks/` 暴露给组件。
4
+ >
5
+ > **uni-manager 增强**:多租户 / 多区域 / 跨云上下文(tenantId / regionId / cloudProvider)通过 **`src/lib/http.ts` 的 request interceptor 自动注入**,service 函数与 hook 不感知;只有"显式跨上下文"的少数 API 才在调用处显式覆盖。
6
+
7
+ ---
8
+
9
+ ## 三层结构
10
+
11
+ ```
12
+ ┌────────────────────────────────────┐
13
+ │ src/pages/, src/components/ │ 组件层:消费 hook,渲染 UI
14
+ │ - 不调用 fetch / axios │
15
+ │ - 不直接读 process.env │
16
+ │ - 不拼 URL │
17
+ │ - 不直接读 tenantId / regionId │ ← uni-manager 强化:通过 useTenant() / useRegion() hook 读
18
+ └──────────────┬─────────────────────┘
19
+ │ 仅通过 hook
20
+
21
+ ┌────────────────────────────────────┐
22
+ │ src/hooks/use<Domain>*.ts │ Hook 层:状态管理 + 缓存
23
+ │ - 包装请求生命周期 │
24
+ │ - 处理缓存 / 重试 / 失效 │
25
+ │ - 不写具体的 URL / payload 拼装 │
26
+ │ - queryKey 含 tenantId / regionId │ ← uni-manager 强化:键值带上下文
27
+ └──────────────┬─────────────────────┘
28
+ │ 调用纯函数
29
+
30
+ ┌────────────────────────────────────┐
31
+ │ src/services/<domain>.ts │ Service 层:请求纯函数
32
+ │ - 输入参数 → 输出 Promise<T> │
33
+ │ - 拼 URL / 序列化 / 反序列化 │
34
+ │ - 不依赖 React │
35
+ │ - 不显式拼 tenantId / regionId │ ← uni-manager:让 interceptor 注入
36
+ └──────────────┬─────────────────────┘
37
+
38
+
39
+ ┌────────────────────────────────────┐
40
+ │ src/lib/http.ts │ 传输层:axios / fetch wrapper
41
+ │ - 鉴权 / baseURL / 错误归一化 │
42
+ │ - **租户 / 区域 / 云厂商 header 注入** ← uni-manager 集中点
43
+ │ - 全局 interceptor │
44
+ └────────────────────────────────────┘
45
+ ```
46
+
47
+ ---
48
+
49
+ ## §1 · `src/services/` 约定
50
+
51
+ ### 文件组织
52
+
53
+ 按**领域**(domain)拆,**不**按 HTTP 动词拆。
54
+
55
+ ```
56
+ src/services/
57
+ ├── instance.ts # 实例领域:list / get / create / start / stop / release
58
+ ├── database.ts # 数据库领域
59
+ ├── tenant.ts # 租户管理(meta,不是上下文)
60
+ ├── region.ts # 区域元数据
61
+ ├── audit-log.ts # 操作日志
62
+ └── index.ts # 统一 re-export(可选)
63
+ ```
64
+
65
+ ❌ 不要按动词拆:`getInstances.ts` / `createInstance.ts` —— 会爆文件数。
66
+
67
+ ### 函数签名约定(uni-manager)
68
+
69
+ ```ts
70
+ // src/services/instance.ts
71
+ import { http } from '@/lib/http';
72
+ import type {
73
+ Instance,
74
+ InstanceListParams,
75
+ CreateInstanceInput,
76
+ } from '@/types/instance';
77
+
78
+ // ✅ 标准:service 不显式拼 tenantId / regionId / cloudProvider
79
+ // interceptor 会从当前 TenantContext 读取并加 header
80
+ export async function listInstances(
81
+ params: InstanceListParams,
82
+ ): Promise<Instance[]> {
83
+ const { data } = await http.get('/api/instances', { params });
84
+ return data.list;
85
+ }
86
+
87
+ export async function getInstance(id: string): Promise<Instance> {
88
+ const { data } = await http.get(`/api/instances/${id}`);
89
+ return data;
90
+ }
91
+
92
+ export async function createInstance(
93
+ input: CreateInstanceInput,
94
+ ): Promise<Instance> {
95
+ const { data } = await http.post('/api/instances', input);
96
+ return data;
97
+ }
98
+
99
+ // ✅ 显式跨上下文:少数场景需要"在 A 租户读 B 租户的数据"
100
+ // 通过 axios config 的 headers 显式覆盖
101
+ export async function listInstancesAcrossTenants(
102
+ params: InstanceListParams,
103
+ contextOverride: { tenantId: string; regionId?: string },
104
+ ): Promise<Instance[]> {
105
+ const { data } = await http.get('/api/instances', {
106
+ params,
107
+ headers: {
108
+ 'X-Tenant-Id': contextOverride.tenantId,
109
+ ...(contextOverride.regionId && {
110
+ 'X-Region-Id': contextOverride.regionId,
111
+ }),
112
+ },
113
+ });
114
+ return data.list;
115
+ }
116
+ ```
117
+
118
+ 要点:
119
+
120
+ - **纯函数**(不依赖 React 上下文,可以在 SSR / Node 测试中单独跑)
121
+ - **类型来自 `src/types/`**,不要在 service 文件里就地定义 domain 类型
122
+ - **错误不在 service 层处理** —— 抛出去给 hook / 全局 interceptor 处理(归一化在 `src/lib/http.ts`)
123
+ - **不写 console.log / 业务弹窗**
124
+ - **不显式拼 tenantId / regionId** —— 让 interceptor 注入;显式覆盖只用于跨上下文场景
125
+
126
+ ### 反模式
127
+
128
+ - ❌ `fetch('https://api.example.com/...')` 写死 host
129
+ - ❌ 每个 service 函数加 `tenantId: string` 参数 —— 应该走 interceptor
130
+ - ❌ service 函数返回 `{ loading, data, error }`(那是 hook 该做的)
131
+ - ❌ 在 service 里 `import { toast } from 'sonner'`
132
+ - ❌ 一个 service 函数同时拼 URL、做业务计算、改全局 store
133
+
134
+ ---
135
+
136
+ ## §2 · `src/hooks/` 约定
137
+
138
+ ### 命名
139
+
140
+ - Query 类:`useInstanceList`、`useInstance(id)`、`useUserProfile`
141
+ - Mutation 类:`useCreateInstance`、`useReleaseInstance`、`useUpdateUser`
142
+
143
+ ### 实现(以 `@tanstack/react-query` 为例 — uni-manager 增强 queryKey)
144
+
145
+ ```ts
146
+ // src/hooks/useInstanceList.ts
147
+ import { useQuery } from '@tanstack/react-query';
148
+ import { listInstances } from '@/services/instance';
149
+ import { useTenant } from '@/contexts/TenantContext';
150
+ import { useRegion } from '@/contexts/RegionContext';
151
+ import type { InstanceListParams } from '@/types/instance';
152
+
153
+ export function useInstanceList(params: InstanceListParams) {
154
+ const { tenantId } = useTenant();
155
+ const { regionId } = useRegion();
156
+ return useQuery({
157
+ // ✅ queryKey 含 tenantId / regionId,租户切换自动失效
158
+ queryKey: ['instances', tenantId, regionId, params],
159
+ queryFn: () => listInstances(params),
160
+ staleTime: 30_000,
161
+ });
162
+ }
163
+ ```
164
+
165
+ ```ts
166
+ // src/hooks/useCreateInstance.ts
167
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
168
+ import { createInstance } from '@/services/instance';
169
+ import { useTenant } from '@/contexts/TenantContext';
170
+
171
+ export function useCreateInstance() {
172
+ const qc = useQueryClient();
173
+ const { tenantId } = useTenant();
174
+ return useMutation({
175
+ mutationFn: createInstance,
176
+ // ✅ 仅失效当前租户的列表,不影响其他租户缓存
177
+ onSuccess: () =>
178
+ qc.invalidateQueries({ queryKey: ['instances', tenantId] }),
179
+ });
180
+ }
181
+ ```
182
+
183
+ ### 反模式
184
+
185
+ - ❌ Hook 里再次拼 URL(违反"service 拼 URL"边界)
186
+ - ❌ queryKey 不包含 tenantId / regionId —— 切换租户后旧数据被复用
187
+ - ❌ 一个 hook 调多个 service 把数据缝合 —— 缝合应该在 service 层
188
+ - ❌ Hook 里写业务跳转 / 路由切换 —— 由组件接 `mutation.onSuccess` callback 处理
189
+ - ❌ 在 hook 里手动加 `tenantId` 到 service 入参 —— 走 interceptor
190
+
191
+ ---
192
+
193
+ ## §3 · `src/lib/http.ts` 约定(uni-manager 关键集中点)
194
+
195
+ **全局只能有一份 http 实例**。提供:
196
+
197
+ - `baseURL`(从 env)
198
+ - 鉴权 header 注入
199
+ - **租户 / 区域 / 云厂商 header 注入**(uni-manager 核心)
200
+ - 错误归一化
201
+ - 请求 ID / trace header
202
+
203
+ ```ts
204
+ // src/lib/http.ts
205
+ import axios from 'axios';
206
+ import {
207
+ getActiveTenant,
208
+ getActiveRegion,
209
+ getActiveCloudProvider,
210
+ } from '@/lib/active-context';
211
+
212
+ export const http = axios.create({
213
+ baseURL: import.meta.env.VITE_API_BASE,
214
+ timeout: 15_000,
215
+ });
216
+
217
+ http.interceptors.request.use((config) => {
218
+ // 鉴权
219
+ const token = localStorage.getItem('token');
220
+ if (token) config.headers.Authorization = `Bearer ${token}`;
221
+
222
+ // ✅ uni-manager 核心:注入上下文 header
223
+ // 若调用方已在 config.headers 里显式覆盖(跨上下文场景),则保留覆盖值
224
+ const tenantId = config.headers['X-Tenant-Id'] ?? getActiveTenant()?.id;
225
+ const regionId = config.headers['X-Region-Id'] ?? getActiveRegion()?.id;
226
+ const cloud = config.headers['X-Cloud-Provider'] ?? getActiveCloudProvider();
227
+
228
+ if (tenantId) config.headers['X-Tenant-Id'] = tenantId;
229
+ if (regionId) config.headers['X-Region-Id'] = regionId;
230
+ if (cloud) config.headers['X-Cloud-Provider'] = cloud;
231
+
232
+ return config;
233
+ });
234
+
235
+ http.interceptors.response.use(
236
+ (res) => res,
237
+ (err) => {
238
+ const message = err.response?.data?.message ?? err.message;
239
+ return Promise.reject(new Error(message));
240
+ },
241
+ );
242
+ ```
243
+
244
+ ```ts
245
+ // src/lib/active-context.ts
246
+ // 在 React 树外读取上下文(vanilla TS)
247
+ // 由 TenantContext / RegionContext 在 mount 时注入
248
+ let activeTenant: { id: string } | null = null;
249
+ let activeRegion: { id: string } | null = null;
250
+ let activeCloud: string | null = null;
251
+
252
+ export function setActiveTenant(t: { id: string } | null) {
253
+ activeTenant = t;
254
+ }
255
+ export function setActiveRegion(r: { id: string } | null) {
256
+ activeRegion = r;
257
+ }
258
+ export function setActiveCloudProvider(c: string | null) {
259
+ activeCloud = c;
260
+ }
261
+
262
+ export function getActiveTenant() {
263
+ return activeTenant;
264
+ }
265
+ export function getActiveRegion() {
266
+ return activeRegion;
267
+ }
268
+ export function getActiveCloudProvider() {
269
+ return activeCloud;
270
+ }
271
+ ```
272
+
273
+ ```tsx
274
+ // src/contexts/TenantContext.tsx
275
+ import { createContext, useContext, useEffect, useState } from 'react';
276
+ import { setActiveTenant } from '@/lib/active-context';
277
+
278
+ const TenantContext = createContext<{
279
+ tenantId: string;
280
+ setTenant: (id: string) => void;
281
+ } | null>(null);
282
+
283
+ export function TenantProvider({ children }: { children: React.ReactNode }) {
284
+ const [tenantId, setTenantIdState] = useState(
285
+ () => localStorage.getItem('activeTenantId') ?? '',
286
+ );
287
+ useEffect(() => {
288
+ setActiveTenant(tenantId ? { id: tenantId } : null);
289
+ localStorage.setItem('activeTenantId', tenantId);
290
+ }, [tenantId]);
291
+ return (
292
+ <TenantContext.Provider value={{ tenantId, setTenant: setTenantIdState }}>
293
+ {children}
294
+ </TenantContext.Provider>
295
+ );
296
+ }
297
+
298
+ export function useTenant() {
299
+ const ctx = useContext(TenantContext);
300
+ if (!ctx) throw new Error('useTenant must be used within TenantProvider');
301
+ return ctx;
302
+ }
303
+ ```
304
+
305
+ ### 反模式
306
+
307
+ - ❌ 在 service 里再 `import axios` 直接用(应该用 `http`)
308
+ - ❌ 多份 `http` 实例
309
+ - ❌ 把 tenantId / regionId 放到每个 service 的 url query 里(应该走 header)
310
+ - ❌ 把 tenantId 当 React state 在每个组件里 prop drilling
311
+ - ❌ tenantId 切换后不清 query cache —— 要么 queryKey 含 tenantId(推荐),要么显式 `qc.clear()`
312
+
313
+ ---
314
+
315
+ ## §4 · 类型分层
316
+
317
+ ```
318
+ src/types/
319
+ ├── instance.ts # Instance、InstanceStatus、InstanceListParams、CreateInstanceInput
320
+ ├── tenant.ts # Tenant、TenantContext
321
+ ├── region.ts # Region、CloudProvider
322
+ └── api.ts # 通用 ApiResponse<T>、Pagination<T>
323
+ ```
324
+
325
+ - **Domain 类型**放 `src/types/<domain>.ts`,被 service / hook / component 共享
326
+ - **API 入参 / 出参类型**:与 domain 类型分开命名
327
+ - **不要**让组件 props 类型直接 `import` service 的内部类型
328
+
329
+ ---
330
+
331
+ ## §5 · 何时可以打破
332
+
333
+ 某些场景**允许**绕过本规范,但需要在代码注释里说明原因:
334
+
335
+ | 场景 | 允许 |
336
+ | -------------------------------------- | ----------------------------------------------- |
337
+ | 静态资源 fetch | 组件直接 `fetch` 可,不用进 service |
338
+ | 第三方 SDK(地图、支付)有自己的客户端 | 包装层放 `src/lib/<sdk>.ts` |
339
+ | 跨租户管理员视图(显式覆盖 header) | service 接 `contextOverride` 参数(见 §1 示例) |
340
+ | 一次性的诊断 / 调试代码 | 临时 fetch 可,但 PR 合并前必须清理或归位 |
341
+
342
+ ---
343
+
344
+ ## §6 · 上下文切换的级联失效
345
+
346
+ 当用户在 um-topbar 切换租户 / 区域 / 云厂商时,**必须**:
347
+
348
+ 1. 写入 `localStorage.activeTenantId / activeRegionId`
349
+ 2. `TenantContext` / `RegionContext` 同步更新
350
+ 3. **react-query cache** 中所有以 `[domain, tenantId, ...]` 开头的 queryKey 自动失效(因为 key 变了)
351
+ 4. URL 同步改写:`?tenantId=xxx&regionId=yyy`(见 [routing-and-codesplit.md](routing-and-codesplit.md) §6)
352
+ 5. 当前页面有未保存表单时,**先弹 AlertDialog 二次确认**(见 design-uni-manager flows.md §1.E)
353
+
354
+ ---
355
+
356
+ ## AI 必须输出的层级日志
357
+
358
+ 完成涉及网络 / 后端的改动时,AI 必须在响应里写明:
359
+
360
+ ```
361
+ ## 数据层改动
362
+
363
+ - src/types/instance.ts: 新增 `InstanceListParams`、`InstanceStatus`
364
+ - src/services/instance.ts: 新增 `listInstances`、`releaseInstance`(纯函数,使用 `@/lib/http`,不显式拼 tenantId)
365
+ - src/hooks/useInstanceList.ts: 包 `useQuery`,queryKey=['instances', tenantId, regionId, params]
366
+ - src/pages/instances/index.tsx: 仅消费 `useInstanceList`,不直接 fetch;URL 同步 tenantId / regionId
367
+ - src/lib/http.ts: 已有 interceptor 注入 X-Tenant-Id / X-Region-Id,无需修改
368
+ ```
369
+
370
+ 每一层都点到名,且**租户 / 区域上下文如何流过**也要说清,才算合规。
@@ -0,0 +1,193 @@
1
+ # 编码自检清单(uni-manager)
2
+
3
+ > AI 写完 / 改完代码后,**必须**逐项核对。任何一项未通过都不应交付,需要先修复或显式告诉用户哪条不通过、为什么。
4
+
5
+ ---
6
+
7
+ ## 1. 复用判定 ✓
8
+
9
+ - [ ] 新建 UI 组件前,查过 `@teamix-evo/ui` 注册表(MCP `list_components` / `find_components`)
10
+ - [ ] 新建 UI 组件前,查过 `biz-ui/uni-manager`(um-topbar 等)+ §2 概念占位拼装表
11
+ - [ ] 新建 UI 组件前,grep 过本项目 `src/components/`
12
+ - [ ] 新建工具函数前,grep 过 `src/utils/` `src/lib/` `src/hooks/`
13
+ - [ ] 没有重复实现 ui 包已经提供的能力(Button、Input、Dialog、DataTable 等)
14
+ - [ ] 没有 fork ui 包源码改样式(改样式应当走 design token)
15
+ - [ ] **组件兜底铁律**:找不到现成实物时优先 ui 原子件组合,未自撸基础件
16
+ - [ ] 响应里**显式列出**复用决策日志
17
+
18
+ ## 2. 数据层分层 ✓
19
+
20
+ - [ ] 组件**没有**直接调 `fetch` / `axios`
21
+ - [ ] 组件**没有**直接读 `import.meta.env` / `process.env`
22
+ - [ ] 所有后端调用都落在 `src/services/<domain>.ts`,且是纯函数
23
+ - [ ] Service 函数**不依赖 React**(没有 `useXxx`、没有 `useState`)
24
+ - [ ] Service 函数**不弹通知 / 不跳路由**
25
+ - [ ] Service 函数**不显式拼 `tenantId` / `regionId` / `cloudProvider`** —— 走 interceptor 注入
26
+ - [ ] 数据 hook 在 `src/hooks/`,使用统一的数据库(react-query / swr,不混用)
27
+ - [ ] 数据 hook 的 `queryKey` 含 `tenantId` / `regionId`(确保切换上下文时 cache 自动失效)
28
+ - [ ] 全局只有一份 http 实例,在 `src/lib/http.ts`
29
+ - [ ] `lib/http.ts` interceptor 已注入 `X-Tenant-Id` / `X-Region-Id` / `X-Cloud-Provider`
30
+
31
+ ## 3. 目录归位 ✓
32
+
33
+ - [ ] 新文件位于约定的顶层目录(`pages/`、`components/`、`services/`、`hooks/`、`stores/`、`types/`、`utils/`、`lib/`、`contexts/`)
34
+ - [ ] **没有**新增同义层(如 `views/`、`api/`、`helpers/`)
35
+ - [ ] 仅在单个页面使用的组件 / hook 放在 `src/pages/<id>/_components/` 或 `_hooks/`(下划线前缀)
36
+ - [ ] 跨页面复用的组件 / hook 才升到 `src/components/` `src/hooks/`
37
+ - [ ] 跨云 / 跨租户专属组件放在 `src/components/cloud/`(CloudBadge / RegionBadge / ContextSwitcher)
38
+ - [ ] 危险操作组件放在 `src/components/danger/` 或 `src/hooks/useDangerConfirm.ts`
39
+ - [ ] React Hook 在 `hooks/`,不在 `utils/`
40
+ - [ ] 类型声明文件没有运行时代码(纯类型)
41
+
42
+ ## 4. 命名 ✓
43
+
44
+ - [ ] 目录 kebab-case(`instance-detail/`)
45
+ - [ ] React 组件文件 PascalCase.tsx,文件名 = 组件名
46
+ - [ ] Hook 文件 camelCase.ts,以 `use` 开头(`useInstanceList.ts`)
47
+ - [ ] Service 文件按 domain 命名(`instance.ts`),不按 HTTP 动词
48
+ - [ ] 名字能让下次复用时被 grep 命中(`InstanceStatusBadge` ✓ / `MyBadge` ❌)
49
+
50
+ ## 5. Import 边界 ✓
51
+
52
+ - [ ] 跨目录 import 走 `@/*` 别名
53
+ - [ ] **未发生反向依赖**:
54
+ - `components/` 没有 import `pages/`
55
+ - `services/` 没有 import `hooks/` `components/` `pages/`
56
+ - `hooks/` 没有 import `pages/`
57
+ - `utils/` `lib/` `types/` 没有 import 业务层
58
+ - [ ] 页面私有目录(`_components/` `_hooks/`)**没有**被其他页面 import
59
+
60
+ ## 6. 类型 ✓
61
+
62
+ - [ ] Domain 类型放 `src/types/<domain>.ts`
63
+ - [ ] Service 函数有显式的入参 / 返回 `Promise<T>` 类型
64
+ - [ ] 没有 `any`
65
+ - [ ] Hook 的返回类型由 `react-query` / `swr` 自动推断
66
+
67
+ ## 7. 依赖卫生 ✓
68
+
69
+ - [ ] 没有引入与项目内现有库**功能重复**的新依赖
70
+ - [ ] 日期 / 数据请求 / 表单 / 状态等核心库**全项目统一**
71
+ - [ ] 没有 `import` 未在 `package.json` 声明的包
72
+
73
+ ## 8. 副作用与可测性 ✓
74
+
75
+ - [ ] Service 函数是纯函数
76
+ - [ ] 工具函数(`src/utils/`)是纯函数
77
+ - [ ] 没有在模块顶层(import 时)发请求 / 弹通知 / 改全局状态
78
+ - [ ] 异步操作有错误处理路径
79
+
80
+ ## 9. 表单与校验 ✓
81
+
82
+ (若改动涉及表单 — 详见 [`forms-and-validation.md`](forms-and-validation.md))
83
+
84
+ - [ ] 表单状态用 `react-hook-form`,**未用** `useState` 拼字段
85
+ - [ ] 校验用 `zod` schema,schema 落 `src/services/<domain>.schema.ts`
86
+ - [ ] 类型从 schema 推导(`z.infer<...>`)
87
+ - [ ] 错误信息写在 schema 里
88
+ - [ ] 提交走 mutation hook
89
+ - [ ] 字段级 / 通用错误分别处理
90
+ - [ ] 表单组件使用 `@teamix-evo/ui` 的 `Form` / `FormField`
91
+ - [ ] **危险操作**(删除 / 释放 / 销毁)走 `useDangerConfirm`,要求**输入资源名称**确认(不接受只输 ID)
92
+
93
+ ## 10. 错误与加载态 ✓
94
+
95
+ (若改动包含 UI / 数据 hook — 详见 [`error-and-loading.md`](error-and-loading.md))
96
+
97
+ - [ ] App 根**存在**全局 `ErrorBoundary` + `Toaster`
98
+ - [ ] 全局 `onError` 调 `reportError`
99
+ - [ ] 数据 hook 三态全处理:`isPending` → Skeleton、`isError` → 错误面板 + 重试、data → 内容
100
+ - [ ] **未用** `useState<boolean>` + `useEffect` 自管 loading
101
+ - [ ] Mutation 成功 → `toast.success` + 跳转 / invalidate;失败 → `toast.error`
102
+ - [ ] Skeleton 撑住布局
103
+ - [ ] 长操作(> 1s)按钮 `disabled`
104
+ - [ ] 跨云资源 404 用专属 fallback("该资源在当前云厂商 / 区域不存在,请检查 um-topbar 上下文")
105
+
106
+ ## 11. 路由与代码分包 ✓
107
+
108
+ (若改动涉及路由 — 详见 [`routing-and-codesplit.md`](routing-and-codesplit.md))
109
+
110
+ - [ ] 新页面用 `React.lazy()` 引入
111
+ - [ ] Lazy 路由有 `<Suspense fallback>` 兜底
112
+ - [ ] 鉴权 / 角色判断在 `src/routes/guards.tsx`
113
+ - [ ] 路由声明集中在 `src/routes/index.tsx`
114
+ - [ ] 404 / 403 / 500 兜底路由存在
115
+ - [ ] 列表筛选 / 分页用 `useSearchParams`
116
+ - [ ] **`tenantId` / `regionId` / `cloudProvider` 同步进 URL search params**(uni-manager 强化)
117
+ - [ ] um-topbar 切换上下文时改写当前 URL,而不是 push 新历史
118
+ - [ ] 跳转用 `<Link>` / `useNavigate`,**未用** `window.location.href`
119
+
120
+ ## 12. 测试覆盖 ✓
121
+
122
+ (详见 [`testing.md`](testing.md))
123
+
124
+ - [ ] 新增纯函数有同名 `*.test.ts`(**必测**)
125
+ - [ ] 新增 zod schema 有 `parse` 成功 / 失败用例(**必测**)
126
+ - [ ] 关键业务路径(创建实例 / 切租户 / 删除资源)有组件 / hook 测试(**必测**)
127
+ - [ ] 测试文件就近放在 `<source>.test.ts(x)`
128
+ - [ ] 用 `msw` mock 后端
129
+ - [ ] **msw handler 验证 X-Tenant-Id / X-Region-Id header**(uni-manager 强化)
130
+ - [ ] 测试名是行为描述
131
+ - [ ] 跳过测试时显式说明原因
132
+
133
+ ## 13. 与 design 规范的协作 ✓
134
+
135
+ (若改动包含 UI)
136
+
137
+ - [ ] 视觉部分按 [`teamix-evo-design-uni-manager`](../teamix-evo-design-uni-manager/SKILL.md) 自检过 token / 间距 / 圆角 / 动效
138
+ - [ ] 没有硬编码颜色 / 间距 / 字号
139
+ - [ ] 一致性三件套(UM1/UM2/UM3)满足:
140
+ - UM1:用 `um-topbar`,未自建顶部栏
141
+ - UM2:PageHeader 结构一致(标题 24px / 面包屑紧贴 / 操作右对齐)
142
+ - UM3:跨云资源在列表 / 详情有 CloudBadge
143
+
144
+ ---
145
+
146
+ ## ⚠️ 红线(一票否决)
147
+
148
+ 任何一条触发即必须重写或回退:
149
+
150
+ 1. **组件直接 fetch / axios** —— 数据层未分层
151
+ 2. **新建 Button / Input / Dialog 等基础组件** —— 重造 ui 包能力
152
+ 3. **services 函数依赖 React** —— 边界穿透
153
+ 4. **services 函数显式拼 tenantId / regionId 在 url query** —— 应走 header interceptor
154
+ 5. **`src/views/` 与 `src/pages/` 并存** —— 同义层污染
155
+ 6. **响应里没有复用决策日志** —— 流程未跑通
156
+ 7. **反向依赖**(service 引 hook、component 引 page)
157
+ 8. **多份 http 实例** —— 鉴权 / 上下文 header 无法统一
158
+ 9. **没有全局 ErrorBoundary** —— 异常直接白屏给用户
159
+ 10. **表单用 `useState` 拼字段** —— 弃 react-hook-form + zod
160
+ 11. **页面顶部 import 不 lazy + 没有 Suspense**
161
+ 12. **纯函数 / zod schema 无单测**
162
+ 13. **`alert()` / `window.location.href` 出现在业务代码**
163
+ 14. **uni-manager 红线 — 自建全局 topbar** —— 违反 UM1,必须用 `um-topbar`
164
+ 15. **uni-manager 红线 — 危险操作只用 ID 确认或不二次确认** —— 必须输入资源名称
165
+ 16. **uni-manager 红线 — 跨云资源列表 / 详情不带 CloudBadge** —— 违反 UM3
166
+ 17. **uni-manager 红线 — queryKey 不含 tenantId / regionId** —— 切租户后旧数据被复用,污染数据
167
+
168
+ ---
169
+
170
+ ## AI 输出格式建议
171
+
172
+ 完成改动时,在响应末尾附:
173
+
174
+ ```markdown
175
+ ## 编码合规自检(uni-manager)
176
+
177
+ - 复用判定: ✅ 复用 ui Button / DataTable + biz-ui/uni-manager um-topbar;🆕 新写 InstanceStatusBadge(业务状态映射)
178
+ - 数据层: ✅ services/instance.ts 纯函数;✅ hooks/useInstanceList.ts queryKey 含 tenantId / regionId;✅ 组件未 fetch;✅ http.ts interceptor 已注入上下文
179
+ - 目录: ✅ 页面在 pages/instances/;✅ 跨云组件在 components/cloud/CloudBadge.tsx
180
+ - 命名: ✅ kebab 目录 / Pascal 组件 / camel hook
181
+ - 边界: ✅ 全部走 @/\*;✅ 无反向依赖
182
+ - 类型: ✅ types/instance.ts 集中声明
183
+ - 表单: ✅ useForm + zodResolver;✅ 删除走 useDangerConfirm(输入名称)
184
+ - 错误/加载: ✅ 三态全处理;✅ ErrorBoundary 已在 App 根;✅ 跨云 404 fallback 已加
185
+ - 路由: ✅ React.lazy + Suspense;✅ tenantId/regionId 进 URL;✅ um-topbar 切换改写 URL
186
+ - 测试: ✅ instance.test.ts msw handler 验证 X-Tenant-Id;✅ schema parse 失败用例已加
187
+ - design 协作: ✅ UM1/UM2/UM3 一致性三件套已自检
188
+ - 红线: ✅ 全过(含 uni-manager UM 4 条红线)
189
+
190
+ 未通过项: 无
191
+ ```
192
+
193
+ 如有未通过项,明确写出"❌ 第 X 条:<原因> → <处理方式>"。