@agile-team/wl-skills-kit 2.3.5 → 2.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.7] - 2026-04-29
4
+
5
+ ### 🔐 permission-sync 闭环完善(数据驱动权限)
6
+
7
+ - **修正权限落地方式**:`action-attach` 模式改为在 `data.ts` 的 `ActionButtonDesc` 上加 `permission: []` 字段,而非错误的 `v-permission` 指令(`BaseToolbar` 内部已做权限拦截)
8
+ - **简化角色授权备份说明**:平台侧已防重复分配;AI 内部做"查旧 menuIds + 合并",不再建议额外备份
9
+ - **联动说明更新**:`page-codegen` 预留 `permission: []` 占位;`convention-audit` 改为审计 `data.ts` 按钮是否有非空 `permission` 字段
10
+ - `USAGE.md` 补充"为什么不用 v-permission 指令"说明 + Q2 权限码写法示例
11
+
12
+ ## [2.3.6] - 2026-04-29
13
+
14
+ ### 🔐 permission-sync Skill 正式激活
15
+
16
+ 新增完整的"角色 → 菜单授权 → 动作按钮 → v-permission"闭环能力。
17
+
18
+ #### 新增 6 个 MCP 工具
19
+ - `wls_role_query` — 查询角色列表(支持分页)
20
+ - `wls_role_upsert` — 批量新增角色(按 `code` 字段去重)
21
+ - `wls_assignable_menus_query` — 查询全量可授权菜单
22
+ - `wls_role_assign_menus` — 给角色批量分配菜单(**全量覆盖式**,使用前需确认)
23
+ - `wls_action_query` — 查询页面菜单下的动作(type=A)
24
+ - `wls_action_upsert` — 批量新增动作(按 `permission` 字段去重)
25
+
26
+ #### Skill 工作模式
27
+ - `role-manage` — 角色查询/创建
28
+ - `role-assign` — 角色授权菜单
29
+ - `action-attach` — 挂动作 + 自动改造代码加 `v-permission` 指令
30
+
31
+ #### 安全约束(强制)
32
+ - 生产环境拒绝直接 push(gatewayPath 含 `prod` / `.com` 时切换导出模式)
33
+ - 角色分配二次确认(Pre-flight 必须列出完整菜单清单)
34
+ - 仅新增不删除(防误删导致大面积失权)
35
+
36
+ #### 文档
37
+ - `files/.github/skills/sync/permission-sync/SKILL.md` — AI 触发协议
38
+ - `files/.github/skills/sync/permission-sync/USAGE.md` — 团队成员使用示例
39
+
40
+ #### 配套更新
41
+ - `_registry.md` permission-sync 状态 ⏳ → ✅
42
+ - `kit-internal/skills/_planned-skills.md` 已清空(无草稿状态 Skill)
43
+ - `kit-internal/architecture.md` 决策 7 更新
44
+ - `docs/ai全景分析.md` MCP Tools 表新增 6 个工具,路线图描述更新
45
+ - `README.md` skill 目录与概览表更新
46
+
3
47
  ## [2.3.5] - 2026-04-29
4
48
 
5
49
  ### 📄 文档与内容修正
package/README.md CHANGED
@@ -102,7 +102,7 @@ wl-skills-kit/ ← 你正看的这个仓库
102
102
  │ │ ├── 02-code-structure.md
103
103
  │ │ ├── ... (共 13 条)
104
104
  │ │ └── 13-platform-components.md
105
- │ ├── skills/ 8 个启用 Skill + 1 个 PLANNED 草稿
105
+ │ ├── skills/ 9 个启用 Skill(全部激活)
106
106
  │ │ ├── _registry.md ★ 触发词 → SKILL 路径单一数据源
107
107
  │ │ ├── _compat/ 多 AI 编辑器适配(配置 + headers)
108
108
  │ │ ├── core/ 核心通用 Skill
@@ -114,7 +114,7 @@ wl-skills-kit/ ← 你正看的这个仓库
114
114
  │ │ ├── sync/ 数据同步类
115
115
  │ │ │ ├── menu-sync/ { SKILL.md, USAGE.md, env/ }
116
116
  │ │ │ ├── dict-sync/ { SKILL.md } 已启用
117
- │ │ │ └── permission-sync/ [PLANNED] SKILL.draft.md 设计中
117
+ │ │ │ └── permission-sync/ { SKILL.md, USAGE.md } 已启用(角色+授权+动作+v-permission)
118
118
  │ │ ├── ops/ 运维类
119
119
  │ │ │ └── code-fix/ { SKILL.md } 已启用
120
120
  │ │ └── domain/ 领域专属(按需创建)
@@ -201,7 +201,7 @@ npx @agile-team/wl-skills-kit update
201
201
  | `template-extract` | ✅ 启用 | `skills/core/template-extract/` | 现有页面 → 领域模板 |
202
202
  | `menu-sync` | ✅ 启用 | `skills/sync/menu-sync/` | 菜单基线 ↔ 后端接口 |
203
203
  | `dict-sync` | ✅ 启用 | `skills/sync/dict-sync/` | 字典基线 ↔ 后端接口 |
204
- | `permission-sync` | PLANNED | `skills/sync/permission-sync/` | 权限基线 后端接口 |
204
+ | `permission-sync` | 启用 | `skills/sync/permission-sync/` | 角色管理 + 角色授权 + 挂动作 + v-permission |
205
205
  | `code-fix` | ✅ 启用 | `skills/ops/code-fix/` | 受控自动修复偏差 |
206
206
 
207
207
  每个启用 Skill 同目录都有 **`SKILL.md`(AI 触发用)+ `USAGE.md`(团队成员阅读)**。
@@ -13,8 +13,8 @@
13
13
  | 文件 | 写入方 | 读取方 | 说明 |
14
14
  | ---------------------------------- | ------------------ | --------------- | ---------------------- |
15
15
  | `SYS_MENU_INFO.md` | page-codegen | menu-sync | 页面生成时追加菜单条目 |
16
- | `SYS_DICT_INFO.md` [PLANNED] | dict-collect | dict-sync | 字典数据汇总 |
17
- | `SYS_PERMISSION_INFO.md` [PLANNED] | permission-collect | permission-sync | 权限数据汇总 |
16
+ | `SYS_DICT_INFO.md` | dict-collect | dict-sync | 字典数据汇总 |
17
+ | `SYS_PERMISSION_INFO.md` [PLANNED] | permission-collect | permission-sync | 权限数据汇总(可选基线,当前 permission-sync 不强依赖) |
18
18
 
19
19
  **写入规则**:每条数据带生成时间戳;新条目追加到末尾;不删除历史。
20
20
 
@@ -21,7 +21,7 @@ skills/
21
21
  ├── sync/ 数据同步类(与后端联动)
22
22
  │ ├── menu-sync/
23
23
  │ ├── dict-sync/
24
- │ └── permission-sync/ [PLANNED]
24
+ │ └── permission-sync/ ✅ v2.3.6 激活
25
25
 
26
26
  ├── ops/ 运维/构建类
27
27
  │ └── code-fix/
@@ -47,7 +47,7 @@ skills/
47
47
  | template-extract | ✅ 启用 | `skills/core/template-extract/SKILL.md` | 提取模板 / 抽取模板 / 沉淀模板 / 模板贡献 |
48
48
  | menu-sync | ✅ 启用 | `skills/sync/menu-sync/SKILL.md` | 创建菜单 / 注册菜单 / 同步菜单 / 补菜单 |
49
49
  | dict-sync | ✅ 启用 | `skills/sync/dict-sync/SKILL.md` | 同步字典 / 创建字典 / 刷新字典基线 / 字典对比 / 字典审计 |
50
- | permission-sync | PLANNED | `skills/sync/permission-sync/SKILL.draft.md` | (草稿,不参与调度) |
50
+ | permission-sync | 启用 | `skills/sync/permission-sync/SKILL.md` | 创建角色 / 角色管理 / 角色授权 / 给角色分配菜单 / 挂动作 / 添加动作按钮 / 同步权限 / 权限码注册 |
51
51
  | code-fix | ✅ 启用 | `skills/ops/code-fix/SKILL.md` | 自动修复 / 整改偏差 / 修复报告 / 规范整改 / 修复偏差 / code fix |
52
52
 
53
53
  ---
@@ -14,10 +14,5 @@
14
14
  "dict": {
15
15
  "_comment": "dict-sync 专属配置",
16
16
  "moduleId": "字典所属模块ID(从字典管理后台获取,如 7C909G0U2F8HI7E305LV0135LSJ3UBIO)"
17
- },
18
-
19
- "permission": {
20
- "_comment": "permission-sync 专属配置 — 待启用,字段待确认",
21
- "parentPermissionId": ""
22
17
  }
23
18
  }
@@ -0,0 +1,258 @@
1
+ ---
2
+ name: permission-sync
3
+ description: "Use when: managing roles, authorizing menus to roles, attaching action buttons (type=A) to page menus, or wiring permission field in data.ts toolbar config. Triggers on: 创建角色, 角色管理, 角色授权, 给角色分配菜单, 挂动作, 添加动作按钮, 同步权限, 权限码注册, role assign, permission sync."
4
+ ---
5
+
6
+ # Skill: 权限同步(permission-sync)
7
+
8
+ 将系统的**角色 → 菜单授权 → 动作按钮 → `data.ts` 权限码字段**串成一条链路,覆盖从权限注册到代码落地的全流程。
9
+
10
+ > **与 menu-sync / dict-sync 的关系**:完全对称,统一从 `.github/skills/sync/env.local.json` 读取配置。`menu-sync` 负责页面菜单(type=M/C),`permission-sync` 负责其上的角色与动作(type=A)。
11
+
12
+ ---
13
+
14
+ ## 配置(统一配置文件,复用 menu-sync 的配置)
15
+
16
+ 读取 `.github/skills/sync/env.local.json`:
17
+
18
+ ```json
19
+ {
20
+ "gatewayPath": "http://你的网关地址:端口",
21
+ "sysAppNo": "应用编码",
22
+ "token": "Bearer Token(不含 bearer 前缀)"
23
+ }
24
+ ```
25
+
26
+ **permission-sync 不需要额外字段**——角色和动作的 `parentId` 由 AI 在执行流程中通过查询接口动态获取。
27
+
28
+ ---
29
+
30
+ ## 三种工作模式
31
+
32
+ | 模式 | 触发关键词 | 动作 |
33
+ | ---------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
34
+ | `role-manage` | 创建角色 / 角色管理 / 列出角色 | 查询/新增角色(按 `code` 去重,幂等) |
35
+ | `role-assign` | 角色授权 / 给 XX 角色分配菜单 | 查询全量可授权菜单 → 选定 menuIds → 调用 `saveRoleMenus` 批量分配 |
36
+ | `action-attach` | 挂动作 / 给页面加按钮 / 注册权限码 | 注册 type=A 动作到后端 + 在 `data.ts` 对应按钮的 `ActionButtonDesc` 加 `permission` 字段,形成完整闭环 |
37
+
38
+ ---
39
+
40
+ ## Pre-flight 声明(执行前必须输出)
41
+
42
+ ```
43
+ 🚀 已触发技能 permission-sync/SKILL.md → 权限同步
44
+ ✅ 已读取 skills/sync/env.local.json → 网关地址、token、sysAppNo
45
+ ✅ 操作模式:{role-manage / role-assign / action-attach}
46
+ ✅ 目标:{角色名 / roleId / 页面菜单 menuId 与权限码列表}
47
+ ✅ 安全检查:{生产环境拒绝 / 角色分配二次确认 / 仅新增不删除}
48
+ ```
49
+
50
+ ---
51
+
52
+ ## MCP 工具调用规范
53
+
54
+ permission-sync 通过 6 个 MCP 工具完成所有操作(无需手动 fetch):
55
+
56
+ | 工具 | 用途 |
57
+ | ----------------------------- | ------------------------------------------ |
58
+ | `wls_role_query` | 查询角色列表(带分页) |
59
+ | `wls_role_upsert` | 批量新增角色(按 code 去重) |
60
+ | `wls_assignable_menus_query` | 查询全量可授权菜单 |
61
+ | `wls_role_assign_menus` | 给角色批量分配菜单(**全量覆盖**,注意!) |
62
+ | `wls_action_query` | 查询页面菜单下的动作(type=A) |
63
+ | `wls_action_upsert` | 批量新增动作(按 permission 去重) |
64
+
65
+ > 编辑器不支持 MCP 时(如纯命令行使用),可参考 §6 调用接口直连方式手动跑通。
66
+
67
+ ---
68
+
69
+ ## 1. role-manage(角色管理)
70
+
71
+ ### 输入示例
72
+
73
+ > "创建一个测试角色,code 是 test_qa,描述:QA 测试用"
74
+
75
+ ### 流程
76
+
77
+ 1. AI 调用 `wls_role_query` 检查 `code=test_qa` 是否已存在
78
+ 2. 若不存在,调用 `wls_role_upsert`:
79
+ ```json
80
+ {
81
+ "items": [
82
+ { "roleName": "测试角色(QA)", "code": "test_qa", "configDesc": "QA 测试用" }
83
+ ]
84
+ }
85
+ ```
86
+ 3. 输出表格:角色名 / code / 状态(✅ 创建成功 / ⏭ 已存在)
87
+
88
+ ---
89
+
90
+ ## 2. role-assign(角色授权)
91
+
92
+ ### 输入示例
93
+
94
+ > "给『档案普通人员』角色挂上『客户档案』『客户申请』两个菜单"
95
+
96
+ ### 流程
97
+
98
+ 1. **查询角色 id**:`wls_role_query` → 找到 `roleName="档案普通人员"` 对应的 `id`
99
+ 2. **查询可授权菜单**:`wls_assignable_menus_query` → 获取全部菜单清单
100
+ 3. **匹配 menuIds**:在结果中找到「客户档案」「客户申请」对应的 menu id
101
+ 4. **⚠️ 二次确认**:在 Pre-flight 中列出"将给角色 X 分配菜单 [A, B]",得到用户 yes 才执行
102
+ 5. **调用授权**:`wls_role_assign_menus`
103
+ ```json
104
+ {
105
+ "roleId": "VERUFQ77SS0BCCE6GJVU21IFEP1EKFBQ",
106
+ "menuIds": ["2049380552157999105", "2049388746804604929"]
107
+ }
108
+ ```
109
+
110
+ ### ⚠️ 全量覆盖式陷阱
111
+
112
+ 后端 `saveRoleMenus` 是**全量覆盖**:
113
+ - 传 `[A, B]` 后,原先 `[A, B, C]` 中的 C 会被移除
114
+ - AI 在执行前**必须先告知用户**:"此操作会替换该角色全部菜单,原有未列出的将被移除"
115
+ - 若用户只是想"追加 C",AI 应自行合并:取出旧 menuIds + 新增的 → 一起传
116
+
117
+ ---
118
+
119
+ ## 3. action-attach(挂动作 + 加 v-permission)
120
+
121
+ ### 输入示例
122
+
123
+ > "给『客户档案』页面挂上 新增/编辑/删除/导出 四个动作按钮"
124
+
125
+ ### 流程
126
+
127
+ #### 3.1 服务端注册动作
128
+
129
+ 1. **查询页面菜单 id**:先用 `wls_menu_query` 或 `wls_assignable_menus_query` 找到「客户档案」页面菜单 id(type=C)
130
+ 2. **查询已有动作**:`wls_action_query({ menuId: <页面id> })` 看哪些已存在
131
+ 3. **批量新增**:`wls_action_upsert`
132
+ ```json
133
+ {
134
+ "parentId": "<页面菜单 id>",
135
+ "items": [
136
+ { "menuName": "新增", "permission": "customer_add", "orderNum": 1 },
137
+ { "menuName": "编辑", "permission": "customer_edit", "orderNum": 2 },
138
+ { "menuName": "删除", "permission": "customer_remove", "orderNum": 3 },
139
+ { "menuName": "导出", "permission": "customer_export", "orderNum": 4 }
140
+ ]
141
+ }
142
+ ```
143
+
144
+ #### 3.2 在 `data.ts` 中给对应按钮加 `permission` 字段
145
+
146
+ 本项目使用 `@jhlc/common-core` 的 `ActionButtonDesc` 类型,`BaseToolbar` 内部根据 `permission` 字段做权限拦截——**这是数据驱动方式,无需修改模板,无需 `v-permission` 指令**。
147
+
148
+ 由于 page-codegen 保证 `data.ts` 结构一致,toolbar 按钮配置始终在 `data.ts` 内。注册动作后,AI 找到对应按钮对象,添加 `permission` 数组字段:
149
+
150
+ ```ts
151
+ // data.ts — 工具栏按钮配置(ActionButtonDesc[])
152
+ {
153
+ name: 'add',
154
+ label: '新增',
155
+ type: 'primary',
156
+ permission: ['customer_add'], // ← 新增此字段
157
+ onClick: () => modalRef.value?.open()
158
+ },
159
+ {
160
+ name: 'edit',
161
+ label: '编辑',
162
+ permission: ['customer_edit'], // ← 新增此字段
163
+ onClick: (row: any) => modalRef.value?.edit(row.id)
164
+ },
165
+ {
166
+ name: 'remove',
167
+ label: '删除',
168
+ type: 'danger',
169
+ permission: ['customer_remove'], // ← 新增此字段
170
+ onClick: (row: any) => handleRemove(row.id)
171
+ },
172
+ ```
173
+
174
+ `permission` 值与 §3.1 中注册到后端的动作 `permission` 字段**完全一致**,直接复制使用。`BaseToolbar` 内部读取当前用户权限列表(由登录态注入),自动控制按钮显示/隐藏——无需任何额外处理。
175
+
176
+ ---
177
+
178
+ ## 4. 权限码命名规范(项目内统一)
179
+
180
+ ```
181
+ {资源camelCase}_{动作} ← 当前项目主流(短,便于阅读)
182
+ {模块}:{资源}:{动作} ← 通用平台风格(语义完整)
183
+ ```
184
+
185
+ | 动作 | 含义 | 示例(短) | 示例(长) |
186
+ | ------------ | -------- | --------------------------- | -------------------------------- |
187
+ | `add` | 新增 | `customer_add` | `mmwr:customer:add` |
188
+ | `edit` | 编辑 | `customer_edit` | `mmwr:customer:edit` |
189
+ | `remove` | 删除 | `customer_remove` | `mmwr:customer:remove` |
190
+ | `export` | 导出 | `customer_export` | `mmwr:customer:export` |
191
+ | `import` | 导入 | `customer_import` | `mmwr:customer:import` |
192
+ | `submit` | 提交审批 | `customer_submit` | `mmwr:customer:submit` |
193
+ | `approve` | 审批通过 | `customer_approve` | `mmwr:customer:approve` |
194
+ | `reject` | 审批驳回 | `customer_reject` | `mmwr:customer:reject` |
195
+ | `{custom}` | 自定义 | `customer_convertToFormal` | `mmwr:customer:convertToFormal` |
196
+
197
+ > AI 在生成时**先扫描项目内已有 v-permission 用法**,沿用现有风格;项目无既定风格时优先短形式(`xxx_add`),与示例数据保持一致。
198
+
199
+ ---
200
+
201
+ ## 5. 安全约束(强制)
202
+
203
+ - ✅ **生产环境拒绝直接 push**:`gatewayPath` 含 `prod` / `.com` 时切换为"导出 SQL/JSON"模式,不直接调接口
204
+ - ✅ **角色分配全量覆盖处理**:`role-assign` 模式下,AI 内部自动"查旧 menuIds + 合并新增"再调用,防止漏带原有菜单。平台侧已防重复分配,无需额外备份机制
205
+ - ✅ **仅新增不删除**:所有 upsert 工具按唯一键去重跳过;删除走人工 SQL(防误删导致大面积失权)
206
+ - ✅ **审计报告**:每次执行后,在 `reports/PERMISSION_SYNC_<YYYYMMDD>.md` 追加日志(角色/动作变更、接口调用、data.ts 修改行位置)
207
+
208
+ ---
209
+
210
+ ## 6. 直连接口参考(无 MCP 时的兜底)
211
+
212
+ 所有接口共用 Headers:
213
+
214
+ ```
215
+ Content-Type: application/json
216
+ Authorization: Bearer <token>
217
+ ```
218
+
219
+ | 操作 | 方法 | 路径 | Body |
220
+ | -------------------- | ---- | --------------------------------------------------- | ------------------------------------------------------------- |
221
+ | 查询角色列表 | GET | `/system/role/list?current=1&size=100` | - |
222
+ | 新增角色 | POST | `/system/role/save` | `{ roleName, code, configDesc }` |
223
+ | 查询全量可授权菜单 | GET | `/system/menu/get/subMenu?size=999` | - |
224
+ | 角色分配菜单 | POST | `/system/role/saveRoleMenus` | `{ roleId, menuIds: "id1,id2" }` (**逗号分隔字符串**) |
225
+ | 查询页面下子菜单/动作 | GET | `/system/menu/children?current=1&size=10&menuId=X` | - |
226
+ | 新增动作(type=A) | POST | `/system/menu/save` | `{ parentId, type:"A", menuName, permission, icon, orderNum, sysAppNo, intIsActive:1, useCache:1 }` |
227
+
228
+ ---
229
+
230
+ ## 7. 与其他 Skill 联动
231
+
232
+ - **page-codegen**:生成 toolbar 按钮时,若 api.md 中声明了操作集,在 `data.ts` 的 `ActionButtonDesc` 中预留 `permission: []`(空数组占位),等待 permission-sync 后续填入真实权限码
233
+ - **menu-sync**:菜单注册成功后,AI 应主动询问"是否给该页面挂动作?" → 触发 action-attach
234
+ - **convention-audit**:审计 `data.ts` 工具栏按钮是否都设置了非空的 `permission` 字段(字段缺失或为 `[]` 视为偏差)
235
+ - **prototype-scan**:扫描原型时识别按钮操作 → 写入 `reports/SYS_PERMISSION_INFO.md` 基线
236
+
237
+ ---
238
+
239
+ ## 8. 报告输出(reports/PERMISSION_SYNC_<日期>.md)
240
+
241
+ ```md
242
+ # 权限同步报告 2026-04-29
243
+
244
+ ## role-manage
245
+ - ✅ 新增角色:测试角色(QA)/ test_qa
246
+
247
+ ## role-assign
248
+ - ✅ 角色 default_role 已分配 5 个菜单:[id1, id2, ...]
249
+ - ⚠️ 替换原有 7 个菜单(已确认)
250
+
251
+ ## action-attach
252
+ - ✅ customer 页面新增 4 个动作:add/edit/remove/export
253
+ - ✅ 代码已加 v-permission(src/views/customer/list/index.vue 第 32-45 行)
254
+
255
+ ## 回滚提示
256
+ - 角色分配可通过重新调用 saveRoleMenus 传旧 menuIds 恢复
257
+ - 新增的角色/动作建议通过后端管理界面手动删除(防止误删生产数据)
258
+ ```
@@ -0,0 +1,107 @@
1
+ # permission-sync · 使用示例
2
+
3
+ > 给团队成员看的快速上手文档。AI 触发协议见 `SKILL.md`。
4
+
5
+ ---
6
+
7
+ ## 一句话理解
8
+
9
+ **permission-sync = 角色管理 + 角色授权 + 挂动作 + 加 v-permission**
10
+
11
+ 整条链路覆盖"页面建好后,怎么让指定角色的人能看见、能点按钮"的全过程。
12
+
13
+ ---
14
+
15
+ ## 三种典型对话
16
+
17
+ ### 1. 创建角色
18
+
19
+ ```
20
+ 用户:创建一个测试角色,code 是 test_qa
21
+ AI : [触发 permission-sync]
22
+ [Pre-flight] 模式 = role-manage
23
+ 调用 wls_role_query 检查 → code=test_qa 不存在
24
+ 调用 wls_role_upsert
25
+ ✅ 创建成功
26
+ ```
27
+
28
+ ### 2. 给角色分配菜单
29
+
30
+ ```
31
+ 用户:给『档案普通人员』分配『客户档案』和『客户申请』两个菜单
32
+ AI : [触发 permission-sync]
33
+ [Pre-flight] 模式 = role-assign
34
+ ⚠️ 注意:saveRoleMenus 是全量覆盖,原有菜单会被替换
35
+ 是否继续?(yes/no)
36
+ 用户:yes
37
+ AI : 调用 wls_role_assign_menus
38
+ ✅ 角色授权成功
39
+ ```
40
+
41
+ ### 3. 给页面挂动作 + 加权限字段(数据驱动,无需改模板)
42
+
43
+ ```
44
+ 用户:给『客户档案』页面加上 新增/编辑/删除 三个按钮
45
+ AI : [触发 permission-sync]
46
+ [Pre-flight] 模式 = action-attach
47
+ 1. wls_action_query 查询已有动作 → 无
48
+ 2. wls_action_upsert 注册到后端:customer_add / customer_edit / customer_remove
49
+ 3. 在 src/views/customer/list/data.ts 找到工具栏按钮配置,
50
+ 给每个对应按钮的 ActionButtonDesc 添加 permission 字段:
51
+ { name: 'add', label: '新增', permission: ['customer_add'], ... }
52
+ ✅ 完成(已写报告 reports/PERMISSION_SYNC_20260429.md)
53
+ ```
54
+
55
+ > **为什么不用 v-permission 指令?**
56
+ > 本项目 `BaseToolbar` 内部读取 `ActionButtonDesc.permission` 字段做权限控制,
57
+ > 只需在 `data.ts` 的按钮配置对象里加字段,不需要改 `.vue` 模板,也不依赖全局指令注册。
58
+
59
+ ---
60
+
61
+ ## 常见问题
62
+
63
+ ### Q1:角色授权时,为什么 menuIds 要传完整列表?
64
+
65
+ 后端 `saveRoleMenus` 是**全量覆盖**接口。你传 `[A, B]`,原先 `[A, B, C]` 就会变成 `[A, B]`,C 丢失。
66
+
67
+ **正确做法**:先查角色现有菜单 → 合并新菜单 → 一起传。
68
+
69
+ ### Q2:权限码写在 `data.ts` 哪里?不需要改模板吗?
70
+
71
+ 对,**不需要改模板(`.vue` 文件)**。`BaseToolbar` 从 `ActionButtonDesc` 的 `permission` 字段读取权限码,在渲染时内部做拦截,标准结构如下:
72
+
73
+ ```ts
74
+ // data.ts
75
+ {
76
+ name: 'edit',
77
+ label: '编辑',
78
+ permission: ['qmmcProcessCodeMain_update'], // ← 就加这一行
79
+ onClick: (row: any) => modalRef.value?.edit(row.id)
80
+ }
81
+ ```
82
+
83
+ `permission` 是数组,支持多权限码 OR 逻辑(有其中任意一个权限码即显示按钮)。
84
+
85
+ ### Q3:权限码用短形式还是长形式?
86
+
87
+ 参照项目既有用法。新项目建议短形式(`customer_add`),与平台示例数据一致。
88
+
89
+ ### Q4:能批量删除角色/动作吗?
90
+
91
+ **不能**。permission-sync 仅新增不删除(防误删)。删除请走后端管理界面手动操作。
92
+
93
+ ---
94
+
95
+ ## 配置要点
96
+
97
+ 只需 `.github/skills/sync/env.local.json` 的根字段:
98
+
99
+ ```json
100
+ {
101
+ "gatewayPath": "http://你的网关:端口",
102
+ "sysAppNo": "应用编码",
103
+ "token": "Bearer Token"
104
+ }
105
+ ```
106
+
107
+ 不需要额外配置 `parentRoleId` / `parentPermissionId` 之类——所有父级 id 由 AI 在执行流程中通过查询接口动态获取。
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const { wlsFetch } = require('./client')
4
+
5
+ /**
6
+ * 查询角色列表(分页)
7
+ * GET /system/role/list?current=1&size=10
8
+ */
9
+ function queryRoleList(params, config) {
10
+ const current = (params && params.current) || 1
11
+ const size = (params && params.size) || 100
12
+ return wlsFetch(
13
+ `/system/role/list?current=${current}&size=${size}`,
14
+ {},
15
+ config
16
+ )
17
+ }
18
+
19
+ /**
20
+ * 新增角色
21
+ * POST /system/role/save
22
+ * body: { roleName, code, configDesc }
23
+ */
24
+ function saveRole(body, config) {
25
+ return wlsFetch('/system/role/save', { method: 'POST', body }, config)
26
+ }
27
+
28
+ /**
29
+ * 查询全量可授权菜单(用于角色分配菜单)
30
+ * GET /system/menu/get/subMenu?size=999
31
+ */
32
+ function queryAssignableMenus(config) {
33
+ return wlsFetch('/system/menu/get/subMenu?size=999', {}, config)
34
+ }
35
+
36
+ /**
37
+ * 给角色批量分配菜单权限
38
+ * POST /system/role/saveRoleMenus
39
+ * body: { roleId, menuIds: "id1,id2,id3" } // 注意 menuIds 是逗号分隔字符串
40
+ */
41
+ function saveRoleMenus(body, config) {
42
+ return wlsFetch('/system/role/saveRoleMenus', { method: 'POST', body }, config)
43
+ }
44
+
45
+ /**
46
+ * 查询父菜单下的子菜单/动作列表
47
+ * GET /system/menu/children?current=1&size=10&menuId=xxx
48
+ */
49
+ function queryMenuChildren(menuId, config) {
50
+ const params = `current=1&size=999&menuId=${encodeURIComponent(menuId)}`
51
+ return wlsFetch(`/system/menu/children?${params}`, {}, config)
52
+ }
53
+
54
+ module.exports = {
55
+ queryRoleList,
56
+ saveRole,
57
+ queryAssignableMenus,
58
+ saveRoleMenus,
59
+ queryMenuChildren,
60
+ }
package/mcp/server.js CHANGED
@@ -5,11 +5,17 @@
5
5
  * wl-skills MCP Server
6
6
  *
7
7
  * 实现 MCP 协议(stdio transport,JSON-RPC 2.0)
8
- * 暴露 4 个工具:
9
- * wls_menu_query 查询菜单树
10
- * wls_menu_upsert 批量新增/更新菜单
11
- * wls_dict_query 查询字典模块
12
- * wls_dict_upsert 新增/更新字典模块及字典项
8
+ * 暴露工具:
9
+ * wls_menu_query 查询菜单树
10
+ * wls_menu_upsert 批量新增/更新菜单
11
+ * wls_dict_query 查询字典模块
12
+ * wls_dict_upsert 新增/更新字典模块及字典项
13
+ * wls_role_query 查询角色列表
14
+ * wls_role_upsert 批量新增角色(按 code 去重)
15
+ * wls_assignable_menus_query 查询全量可授权菜单(用于角色授权)
16
+ * wls_role_assign_menus 给角色批量分配菜单权限
17
+ * wls_action_query 查询页面菜单下的动作(type=A)
18
+ * wls_action_upsert 批量新增动作(按 permission 去重)
13
19
  *
14
20
  * 启动方式(由 .cursor/mcp.json 自动注入):
15
21
  * node node_modules/@agile-team/wl-skills-kit/mcp/server.js
@@ -19,6 +25,14 @@ const readline = require('readline')
19
25
  const { loadConfig } = require('./config')
20
26
  const { handleMenuQuery, handleMenuUpsert } = require('./tools/menuSync')
21
27
  const { handleDictQuery, handleDictUpsert } = require('./tools/dictSync')
28
+ const {
29
+ handleRoleQuery,
30
+ handleRoleUpsert,
31
+ handleRoleAssignMenus,
32
+ handleAssignableMenusQuery,
33
+ handleActionQuery,
34
+ handleActionUpsert,
35
+ } = require('./tools/permissionSync')
22
36
 
23
37
  const PKG = require('../package.json')
24
38
 
@@ -102,6 +116,94 @@ const TOOLS = [
102
116
  required: ['module'],
103
117
  },
104
118
  },
119
+ {
120
+ name: 'wls_role_query',
121
+ description:
122
+ '查询角色列表。可选参数 current/size 翻页,默认 size=100。返回精简字段:id, roleName, code, sysAppNo, roleDesc。',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ current: { type: 'number', description: '页码,默认 1' },
127
+ size: { type: 'number', description: '每页数量,默认 100' },
128
+ },
129
+ required: [],
130
+ },
131
+ },
132
+ {
133
+ name: 'wls_role_upsert',
134
+ description:
135
+ '批量新增角色(按 code 字段自动去重;已存在则跳过)。每项必填 roleName 和 code,可选 configDesc。' +
136
+ '注意:角色仅新增不更新,因角色变更通常需要业务确认。',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ items: {
141
+ type: 'array',
142
+ description:
143
+ '角色数组。字段:roleName(必填,显示名), code(必填,唯一标识), configDesc(可选,描述)',
144
+ items: { type: 'object' },
145
+ },
146
+ },
147
+ required: ['items'],
148
+ },
149
+ },
150
+ {
151
+ name: 'wls_assignable_menus_query',
152
+ description:
153
+ '查询全量可授权菜单列表(扁平结构,含菜单 id/menuName/permission)。' +
154
+ '在 wls_role_assign_menus 前调用,AI 据此选出要分配给角色的 menuIds。',
155
+ inputSchema: { type: 'object', properties: {}, required: [] },
156
+ },
157
+ {
158
+ name: 'wls_role_assign_menus',
159
+ description:
160
+ '给指定角色批量分配菜单权限。menuIds 传字符串数组,内部自动拼成逗号分隔字符串提交后端。' +
161
+ '该接口为全量覆盖式,应包含该角色所有菜单(含已有的,否则会被移除)。',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ roleId: { type: 'string', description: '角色 id(来自 wls_role_query)' },
166
+ menuIds: {
167
+ type: 'array',
168
+ description: '该角色应拥有的全部菜单 id 数组',
169
+ items: { type: 'string' },
170
+ },
171
+ },
172
+ required: ['roleId', 'menuIds'],
173
+ },
174
+ },
175
+ {
176
+ name: 'wls_action_query',
177
+ description:
178
+ '查询指定页面菜单(type=C)下的动作按钮列表(type=A)。返回 id/menuName/permission/orderNum/icon。',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ menuId: { type: 'string', description: '父菜单 id(页面菜单)' },
183
+ },
184
+ required: ['menuId'],
185
+ },
186
+ },
187
+ {
188
+ name: 'wls_action_upsert',
189
+ description:
190
+ '在指定页面菜单下批量新增动作按钮(type=A),按 permission 字段自动去重。' +
191
+ '权限码命名规范:{资源camelCase}_{动作} 或 {模块}:{资源}:{动作}(与项目既有约定保持一致)。' +
192
+ '常见动作:add/edit/remove/export/import/approve。',
193
+ inputSchema: {
194
+ type: 'object',
195
+ properties: {
196
+ parentId: { type: 'string', description: '页面菜单 id(动作挂在它下面)' },
197
+ items: {
198
+ type: 'array',
199
+ description:
200
+ '动作数组。字段:menuName(必填,显示名), permission(必填,权限码), icon(可选,默认list), orderNum(可选,默认1), useCache(可选,默认1)',
201
+ items: { type: 'object' },
202
+ },
203
+ },
204
+ required: ['parentId', 'items'],
205
+ },
206
+ },
105
207
  ]
106
208
 
107
209
  // ─── JSON-RPC 协议层 ────────────────────────────────────────────────────
@@ -145,6 +247,24 @@ async function dispatchTool(id, toolName, toolArgs) {
145
247
  case 'wls_dict_upsert':
146
248
  text = await handleDictUpsert(toolArgs, config)
147
249
  break
250
+ case 'wls_role_query':
251
+ text = await handleRoleQuery(toolArgs, config)
252
+ break
253
+ case 'wls_role_upsert':
254
+ text = await handleRoleUpsert(toolArgs, config)
255
+ break
256
+ case 'wls_assignable_menus_query':
257
+ text = await handleAssignableMenusQuery(toolArgs, config)
258
+ break
259
+ case 'wls_role_assign_menus':
260
+ text = await handleRoleAssignMenus(toolArgs, config)
261
+ break
262
+ case 'wls_action_query':
263
+ text = await handleActionQuery(toolArgs, config)
264
+ break
265
+ case 'wls_action_upsert':
266
+ text = await handleActionUpsert(toolArgs, config)
267
+ break
148
268
  default:
149
269
  sendError(id, -32601, `未知工具: ${toolName}`)
150
270
  return
@@ -0,0 +1,321 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ queryRoleList,
5
+ saveRole,
6
+ queryAssignableMenus,
7
+ saveRoleMenus,
8
+ queryMenuChildren,
9
+ } = require('../api/roleApi')
10
+ const { saveMenu } = require('../api/menuApi')
11
+
12
+ /* ──────────────────────────────────────────────────────────────────────
13
+ * 角色管理
14
+ * ──────────────────────────────────────────────────────────────────── */
15
+
16
+ /**
17
+ * wls_role_query 工具处理器
18
+ * 查询角色列表(仅返回字段精简后的角色摘要)
19
+ */
20
+ async function handleRoleQuery(args, config) {
21
+ const result = await queryRoleList(args || {}, config)
22
+
23
+ if (!result.ok) {
24
+ return `❌ 查询角色失败: ${result.error} (code: ${result.code})`
25
+ }
26
+
27
+ const page = result.data && result.data.page
28
+ const records = (page && page.records) || []
29
+
30
+ if (records.length === 0) {
31
+ return '✅ 角色查询成功,当前应用暂无角色数据'
32
+ }
33
+
34
+ // 仅保留关键字段,减少 token 浪费
35
+ const slim = records.map((r) => ({
36
+ id: r.id,
37
+ roleName: r.roleName,
38
+ code: r.code,
39
+ sysAppNo: r.sysAppNo,
40
+ roleDesc: r.roleDesc,
41
+ }))
42
+
43
+ return [
44
+ `✅ 角色查询成功,当前页 ${records.length} 条 / 共 ${page.total} 条(current=${page.current}, pages=${page.pages})`,
45
+ '',
46
+ JSON.stringify(slim, null, 2),
47
+ ].join('\n')
48
+ }
49
+
50
+ /**
51
+ * wls_role_upsert 工具处理器
52
+ * 批量新增角色(仅新增,不更新;以 code 字段去重)
53
+ *
54
+ * @param {{ items: Array<{ roleName: string, code: string, configDesc?: string }> }} args
55
+ */
56
+ async function handleRoleUpsert(args, config) {
57
+ const { items } = args || {}
58
+
59
+ if (!Array.isArray(items) || items.length === 0) {
60
+ return '❌ 参数错误:items 必须是非空数组'
61
+ }
62
+
63
+ // 先查全量角色,按 code 去重
64
+ const queryResult = await queryRoleList({ size: 999 }, config)
65
+ if (!queryResult.ok) {
66
+ return `❌ 查询现有角色失败: ${queryResult.error}`
67
+ }
68
+ const existingCodes = new Set(
69
+ ((queryResult.data && queryResult.data.page && queryResult.data.page.records) || [])
70
+ .map((r) => r.code)
71
+ )
72
+
73
+ const results = []
74
+
75
+ for (const item of items) {
76
+ if (!item.roleName || !item.code) {
77
+ results.push({
78
+ roleName: item.roleName || '(未命名)',
79
+ code: item.code || '(无)',
80
+ status: '❌ roleName 与 code 必填',
81
+ })
82
+ continue
83
+ }
84
+
85
+ if (existingCodes.has(item.code)) {
86
+ results.push({
87
+ roleName: item.roleName,
88
+ code: item.code,
89
+ status: '⏭ 已存在(跳过)',
90
+ })
91
+ continue
92
+ }
93
+
94
+ const body = {
95
+ roleName: item.roleName,
96
+ code: item.code,
97
+ configDesc: item.configDesc || '',
98
+ }
99
+
100
+ const r = await saveRole(body, config)
101
+ results.push({
102
+ roleName: item.roleName,
103
+ code: item.code,
104
+ status: r.ok ? '✅ 创建成功' : `❌ 失败: ${r.error}`,
105
+ })
106
+ }
107
+
108
+ return formatTable(
109
+ results,
110
+ ['roleName', 'code', 'status'],
111
+ ['角色名', 'code', '状态']
112
+ )
113
+ }
114
+
115
+ /* ──────────────────────────────────────────────────────────────────────
116
+ * 角色授权(给角色挂菜单)
117
+ * ──────────────────────────────────────────────────────────────────── */
118
+
119
+ /**
120
+ * wls_role_assign_menus 工具处理器
121
+ * 给指定角色批量分配菜单权限
122
+ *
123
+ * @param {{ roleId: string, menuIds: string[] }} args - menuIds 用数组传入,内部拼成逗号字符串
124
+ */
125
+ async function handleRoleAssignMenus(args, config) {
126
+ const { roleId, menuIds } = args || {}
127
+
128
+ if (!roleId) {
129
+ return '❌ 参数错误:roleId 必填'
130
+ }
131
+ if (!Array.isArray(menuIds) || menuIds.length === 0) {
132
+ return '❌ 参数错误:menuIds 必须是非空字符串数组'
133
+ }
134
+
135
+ const body = {
136
+ roleId,
137
+ menuIds: menuIds.join(','),
138
+ }
139
+
140
+ const r = await saveRoleMenus(body, config)
141
+ if (!r.ok) {
142
+ return `❌ 角色授权失败: ${r.error} (code: ${r.code})`
143
+ }
144
+
145
+ return `✅ 角色授权成功(roleId=${roleId},已分配 ${menuIds.length} 个菜单/动作)`
146
+ }
147
+
148
+ /**
149
+ * wls_assignable_menus_query 工具处理器
150
+ * 查询全量可授权菜单(扁平/树形由后端决定)
151
+ */
152
+ async function handleAssignableMenusQuery(_args, config) {
153
+ const r = await queryAssignableMenus(config)
154
+ if (!r.ok) {
155
+ return `❌ 查询可授权菜单失败: ${r.error} (code: ${r.code})`
156
+ }
157
+ // data 可能是 { records: [...] } 或数组
158
+ const records = (r.data && r.data.records) || (Array.isArray(r.data) ? r.data : [])
159
+ if (records.length === 0) {
160
+ return '✅ 查询成功,当前无可授权菜单'
161
+ }
162
+ return [
163
+ `✅ 可授权菜单查询成功,共 ${records.length} 条`,
164
+ '',
165
+ JSON.stringify(records, null, 2),
166
+ ].join('\n')
167
+ }
168
+
169
+ /* ──────────────────────────────────────────────────────────────────────
170
+ * 挂动作(给页面菜单加 type=A 的动作按钮)
171
+ * ──────────────────────────────────────────────────────────────────── */
172
+
173
+ /**
174
+ * wls_action_query 工具处理器
175
+ * 查询指定页面菜单下的动作子项(type=A)
176
+ *
177
+ * @param {{ menuId: string }} args
178
+ */
179
+ async function handleActionQuery(args, config) {
180
+ const { menuId } = args || {}
181
+ if (!menuId) {
182
+ return '❌ 参数错误:menuId 必填(页面菜单 id)'
183
+ }
184
+
185
+ const r = await queryMenuChildren(menuId, config)
186
+ if (!r.ok) {
187
+ return `❌ 查询子菜单失败: ${r.error} (code: ${r.code})`
188
+ }
189
+
190
+ const records = (r.data && r.data.records) || []
191
+ // 仅保留 type=A(动作)
192
+ const actions = records.filter((m) => m.type === 'A')
193
+
194
+ if (actions.length === 0) {
195
+ return `✅ 查询成功,菜单 ${menuId} 下暂无动作(type=A)`
196
+ }
197
+
198
+ const slim = actions.map((a) => ({
199
+ id: a.id,
200
+ menuName: a.menuName,
201
+ permission: a.permission,
202
+ orderNum: a.orderNum,
203
+ icon: a.icon,
204
+ }))
205
+
206
+ return [
207
+ `✅ 动作查询成功,共 ${actions.length} 条`,
208
+ '',
209
+ JSON.stringify(slim, null, 2),
210
+ ].join('\n')
211
+ }
212
+
213
+ /**
214
+ * wls_action_upsert 工具处理器
215
+ * 在指定页面菜单下批量新增动作(type=A),按 permission 去重
216
+ *
217
+ * @param {{ parentId: string, items: Array<object> }} args
218
+ * items 元素:{ menuName, permission, icon?, orderNum?, useCache? }
219
+ */
220
+ async function handleActionUpsert(args, config) {
221
+ const { parentId, items } = args || {}
222
+
223
+ if (!parentId) {
224
+ return '❌ 参数错误:parentId 必填(页面菜单 id)'
225
+ }
226
+ if (!Array.isArray(items) || items.length === 0) {
227
+ return '❌ 参数错误:items 必须是非空数组'
228
+ }
229
+
230
+ // 先查父菜单下已有动作,按 permission 去重
231
+ const queryResult = await queryMenuChildren(parentId, config)
232
+ if (!queryResult.ok) {
233
+ return `❌ 查询现有动作失败: ${queryResult.error}`
234
+ }
235
+ const existing = ((queryResult.data && queryResult.data.records) || [])
236
+ .filter((m) => m.type === 'A')
237
+ const existingPerms = new Set(existing.map((m) => m.permission))
238
+
239
+ const results = []
240
+
241
+ for (const item of items) {
242
+ if (!item.menuName || !item.permission) {
243
+ results.push({
244
+ menuName: item.menuName || '(未命名)',
245
+ permission: item.permission || '(无)',
246
+ status: '❌ menuName 与 permission 必填',
247
+ })
248
+ continue
249
+ }
250
+
251
+ if (existingPerms.has(item.permission)) {
252
+ results.push({
253
+ menuName: item.menuName,
254
+ permission: item.permission,
255
+ status: '⏭ 已存在(跳过)',
256
+ })
257
+ continue
258
+ }
259
+
260
+ const body = {
261
+ parentId,
262
+ type: 'A',
263
+ menuName: item.menuName,
264
+ permission: item.permission,
265
+ icon: item.icon || 'list',
266
+ orderNum: item.orderNum != null ? item.orderNum : 1,
267
+ useCache: item.useCache != null ? item.useCache : 1,
268
+ sysAppNo: config.sysAppNo,
269
+ intIsActive: 1,
270
+ }
271
+
272
+ const r = await saveMenu(body, config)
273
+ if (r.ok) {
274
+ const saved = r.data
275
+ results.push({
276
+ menuName: item.menuName,
277
+ permission: item.permission,
278
+ status: `✅ 创建成功 (id=${saved ? saved.id : '?'})`,
279
+ })
280
+ } else {
281
+ results.push({
282
+ menuName: item.menuName,
283
+ permission: item.permission,
284
+ status: `❌ 失败: ${r.error}`,
285
+ })
286
+ }
287
+ }
288
+
289
+ return formatTable(
290
+ results,
291
+ ['menuName', 'permission', 'status'],
292
+ ['动作名', '权限码', '状态']
293
+ )
294
+ }
295
+
296
+ /* ──────────────────────────────────────────────────────────────────────
297
+ * 工具函数
298
+ * ──────────────────────────────────────────────────────────────────── */
299
+
300
+ function formatTable(rows, keys, headers) {
301
+ const successCount = rows.filter((r) => r.status.startsWith('✅')).length
302
+ const skipCount = rows.filter((r) => r.status.startsWith('⏭')).length
303
+ const failCount = rows.length - successCount - skipCount
304
+
305
+ let out = `操作完成:成功 ${successCount} 条,跳过 ${skipCount} 条,失败 ${failCount} 条\n\n`
306
+ out += '| ' + headers.join(' | ') + ' |\n'
307
+ out += '|' + headers.map(() => '---').join('|') + '|\n'
308
+ for (const r of rows) {
309
+ out += '| ' + keys.map((k) => r[k]).join(' | ') + ' |\n'
310
+ }
311
+ return out
312
+ }
313
+
314
+ module.exports = {
315
+ handleRoleQuery,
316
+ handleRoleUpsert,
317
+ handleRoleAssignMenus,
318
+ handleAssignableMenusQuery,
319
+ handleActionQuery,
320
+ handleActionUpsert,
321
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-kit",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
4
4
  "description": "AI Skill 模板包 — 一键导入 AI 指令 + 组件文档 + 通用组件 + 领域样例,覆盖 Copilot/Cursor/Windsurf/Kiro 等主流 AI 编辑器",
5
5
  "main": "./bin/wl-skills.js",
6
6
  "bin": {
@@ -1,91 +0,0 @@
1
- ---
2
- name: permission-sync
3
- description: "[PLANNED — DRAFT, not yet active] 权限同步 Skill 设计草稿。基于 reports/SYS_PERMISSION_INFO.md 基线,将页面级/按钮级权限码注册到系统权限表,并按角色分配。"
4
- status: planned
5
- ---
6
-
7
- # Skill: 权限同步(permission-sync)— 草稿
8
-
9
- > ⚠️ **本文件为设计草稿(SKILL.draft.md),未启用,不参与 AI 调度。**
10
-
11
- ---
12
-
13
- ## 1. 设计目标
14
-
15
- 新增页面后,除菜单外还需注册:
16
-
17
- - **页面访问权限**(`mmwr:customer:list`)
18
- - **按钮级权限**(`mmwr:customer:add` / `:edit` / `:remove` / `:approve` / `:export` ...)
19
- - **数据权限**(可选:客户经理只看自己的客户等)
20
- - **角色绑定**(哪些角色获得这些权限)
21
-
22
- ---
23
-
24
- ## 2. 数据流
25
-
26
- ```
27
- 本地基线 后端接口 Skill 触发
28
- ──────────────────────────────────────── ────────────────────────────────── ────────────────
29
- reports/SYS_PERMISSION_INFO.md ─fetch─→ GET /sys/permission/listAll
30
- ←compare── POST /sys/permission/batchSave
31
- ─upload─→ POST /sys/role/assignPermissions ─→ "同步权限"
32
- ```
33
-
34
- ---
35
-
36
- ## 3. 权限码命名规范
37
-
38
- ```
39
- {服务缩写}:{资源camelCase}:{操作}
40
- ```
41
-
42
- | 操作 | 含义 | 示例 |
43
- | -------- | -------------- | ------------------------------- |
44
- | list | 查看列表 | `mmwr:customer:list` |
45
- | detail | 查看详情 | `mmwr:customer:detail` |
46
- | add | 新增 | `mmwr:customer:add` |
47
- | edit | 编辑 | `mmwr:customer:edit` |
48
- | remove | 删除 | `mmwr:customer:remove` |
49
- | export | 导出 | `mmwr:customer:export` |
50
- | import | 导入 | `mmwr:customer:import` |
51
- | submit | 提交审批 | `mmwr:customer:submit` |
52
- | approve | 审批通过 | `mmwr:customer:approve` |
53
- | reject | 审批驳回 | `mmwr:customer:reject` |
54
- | {custom} | 自定义业务操作 | `mmwr:customer:convertToFormal` |
55
-
56
- ---
57
-
58
- ## 4. 三种工作模式
59
-
60
- | 模式 | 触发 | 动作 |
61
- | ---------- | ----------------------- | ------------------------------------------------------ |
62
- | `scan` | "扫描权限码" | 从 src/views/ 扫 v-permission / hasPerm 调用,输出清单 |
63
- | `register` | "注册权限码 / 同步权限" | 对比基线 → 创建缺失 + 更新描述 |
64
- | `assign` | "给 XX 角色分配权限" | 选定角色 + 选定权限码 → 调 /sys/role/assignPermissions |
65
-
66
- ---
67
-
68
- ## 5. 安全约束
69
-
70
- - **生产环境拒绝直接 push**:检测 gatewayPath 含 prod/.com 时强制走"导出 SQL"模式
71
- - **角色分配二次确认**:每次 assign 必须在 Pre-flight 中列出"角色 → 新增/移除的权限",得到用户 yes 才执行
72
- - **不删除权限**:永远只新增/更新,删除走人工 SQL(防误删导致大面积失权)
73
- - **审计**:每次 register/assign 输出 `reports/PERMISSION_SYNC_<YYYYMMDD>.md`,含完整调用日志和回滚 SQL
74
-
75
- ---
76
-
77
- ## 6. 与其他 Skill 联动
78
-
79
- - **page-codegen**:生成 toolbar 时根据 api.md 操作集自动加 `v-permission` 指令
80
- - **menu-sync**:菜单注册后提示"是否同步注册访问权限"
81
- - **convention-audit**:审计按钮是否都有 v-permission
82
-
83
- ---
84
-
85
- ## 7. 转正前的开发任务
86
-
87
- - [ ] 确认后端权限模型(RBAC / ABAC?是否分页面权限和按钮权限?)
88
- - [ ] 数据权限是否纳入本 Skill(建议:暂不,单独 data-permission-sync)
89
- - [ ] 设计 v-permission 指令的项目内实现(如 @jhlc/common-core 提供则复用)
90
- - [ ] 多租户场景下的权限继承策略
91
- - [ ] 与 SSO(嘉为蓝鲸)权限同步策略