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

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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.6] - 2026-04-29
4
+
5
+ ### 🔐 permission-sync Skill 正式激活
6
+
7
+ 新增完整的"角色 → 菜单授权 → 动作按钮 → v-permission"闭环能力。
8
+
9
+ #### 新增 6 个 MCP 工具
10
+ - `wls_role_query` — 查询角色列表(支持分页)
11
+ - `wls_role_upsert` — 批量新增角色(按 `code` 字段去重)
12
+ - `wls_assignable_menus_query` — 查询全量可授权菜单
13
+ - `wls_role_assign_menus` — 给角色批量分配菜单(**全量覆盖式**,使用前需确认)
14
+ - `wls_action_query` — 查询页面菜单下的动作(type=A)
15
+ - `wls_action_upsert` — 批量新增动作(按 `permission` 字段去重)
16
+
17
+ #### Skill 工作模式
18
+ - `role-manage` — 角色查询/创建
19
+ - `role-assign` — 角色授权菜单
20
+ - `action-attach` — 挂动作 + 自动改造代码加 `v-permission` 指令
21
+
22
+ #### 安全约束(强制)
23
+ - 生产环境拒绝直接 push(gatewayPath 含 `prod` / `.com` 时切换导出模式)
24
+ - 角色分配二次确认(Pre-flight 必须列出完整菜单清单)
25
+ - 仅新增不删除(防误删导致大面积失权)
26
+
27
+ #### 文档
28
+ - `files/.github/skills/sync/permission-sync/SKILL.md` — AI 触发协议
29
+ - `files/.github/skills/sync/permission-sync/USAGE.md` — 团队成员使用示例
30
+
31
+ #### 配套更新
32
+ - `_registry.md` permission-sync 状态 ⏳ → ✅
33
+ - `kit-internal/skills/_planned-skills.md` 已清空(无草稿状态 Skill)
34
+ - `kit-internal/architecture.md` 决策 7 更新
35
+ - `docs/ai全景分析.md` MCP Tools 表新增 6 个工具,路线图描述更新
36
+ - `README.md` skill 目录与概览表更新
37
+
3
38
  ## [2.3.5] - 2026-04-29
4
39
 
5
40
  ### 📄 文档与内容修正
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,239 @@
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 v-permission directives in Vue code. Triggers on: 创建角色, 角色管理, 角色授权, 给角色分配菜单, 挂动作, 添加动作按钮, 同步权限, 权限码注册, role assign, permission sync."
4
+ ---
5
+
6
+ # Skill: 权限同步(permission-sync)
7
+
8
+ 将系统的**角色 → 菜单授权 → 动作按钮 → 业务代码 v-permission**串成一条链路,覆盖从权限注册到代码落地的全流程。
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 动作(按 `permission` 去重)+ 在代码加 v-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 在 Vue 代码中加 v-permission 指令
145
+
146
+ 注册动作后,AI 自动改造对应页面的 toolbar/操作列,加上 `v-permission`:
147
+
148
+ ```vue
149
+ <el-button v-permission="'customer_add'" @click="onAdd">新增</el-button>
150
+ <el-button v-permission="'customer_edit'" @click="onEdit">编辑</el-button>
151
+ <el-button v-permission="'customer_remove'" @click="onRemove">删除</el-button>
152
+ <el-button v-permission="'customer_export'" @click="onExport">导出</el-button>
153
+ ```
154
+
155
+ > `v-permission` 指令通常由项目基座(如 `@jhlc/common-core` 或登录鉴权模块)注册。若项目未注册,AI 应在 Pre-flight 提示用户"未检测到 v-permission 指令实现,仅完成服务端注册,代码侧指令请由架构组补全"。
156
+
157
+ ---
158
+
159
+ ## 4. 权限码命名规范(项目内统一)
160
+
161
+ ```
162
+ {资源camelCase}_{动作} ← 当前项目主流(短,便于阅读)
163
+ {模块}:{资源}:{动作} ← 通用平台风格(语义完整)
164
+ ```
165
+
166
+ | 动作 | 含义 | 示例(短) | 示例(长) |
167
+ | ------------ | -------- | --------------------------- | -------------------------------- |
168
+ | `add` | 新增 | `customer_add` | `mmwr:customer:add` |
169
+ | `edit` | 编辑 | `customer_edit` | `mmwr:customer:edit` |
170
+ | `remove` | 删除 | `customer_remove` | `mmwr:customer:remove` |
171
+ | `export` | 导出 | `customer_export` | `mmwr:customer:export` |
172
+ | `import` | 导入 | `customer_import` | `mmwr:customer:import` |
173
+ | `submit` | 提交审批 | `customer_submit` | `mmwr:customer:submit` |
174
+ | `approve` | 审批通过 | `customer_approve` | `mmwr:customer:approve` |
175
+ | `reject` | 审批驳回 | `customer_reject` | `mmwr:customer:reject` |
176
+ | `{custom}` | 自定义 | `customer_convertToFormal` | `mmwr:customer:convertToFormal` |
177
+
178
+ > AI 在生成时**先扫描项目内已有 v-permission 用法**,沿用现有风格;项目无既定风格时优先短形式(`xxx_add`),与示例数据保持一致。
179
+
180
+ ---
181
+
182
+ ## 5. 安全约束(强制)
183
+
184
+ - ✅ **生产环境拒绝直接 push**:`gatewayPath` 含 `prod` / `.com` 时切换为"导出 SQL/JSON"模式,不直接调接口
185
+ - ✅ **角色分配二次确认**:`role-assign` 模式下,必须在 Pre-flight 中列出"角色 → 完整菜单清单"得到用户 yes 才执行
186
+ - ✅ **仅新增不删除**:所有 upsert 工具按唯一键去重跳过;删除走人工 SQL(防误删导致大面积失权)
187
+ - ✅ **审计报告**:每次执行后,在 `reports/PERMISSION_SYNC_<YYYYMMDD>.md` 追加日志(角色/动作变更、调用接口、响应、回滚提示)
188
+
189
+ ---
190
+
191
+ ## 6. 直连接口参考(无 MCP 时的兜底)
192
+
193
+ 所有接口共用 Headers:
194
+
195
+ ```
196
+ Content-Type: application/json
197
+ Authorization: Bearer <token>
198
+ ```
199
+
200
+ | 操作 | 方法 | 路径 | Body |
201
+ | -------------------- | ---- | --------------------------------------------------- | ------------------------------------------------------------- |
202
+ | 查询角色列表 | GET | `/system/role/list?current=1&size=100` | - |
203
+ | 新增角色 | POST | `/system/role/save` | `{ roleName, code, configDesc }` |
204
+ | 查询全量可授权菜单 | GET | `/system/menu/get/subMenu?size=999` | - |
205
+ | 角色分配菜单 | POST | `/system/role/saveRoleMenus` | `{ roleId, menuIds: "id1,id2" }` (**逗号分隔字符串**) |
206
+ | 查询页面下子菜单/动作 | GET | `/system/menu/children?current=1&size=10&menuId=X` | - |
207
+ | 新增动作(type=A) | POST | `/system/menu/save` | `{ parentId, type:"A", menuName, permission, icon, orderNum, sysAppNo, intIsActive:1, useCache:1 }` |
208
+
209
+ ---
210
+
211
+ ## 7. 与其他 Skill 联动
212
+
213
+ - **page-codegen**:生成 toolbar 时根据 `api.md` 的操作集自动加 `v-permission` 指令(在动作 upsert 完成后)
214
+ - **menu-sync**:菜单注册成功后,AI 应主动询问"是否给该页面挂动作?" → 触发 action-attach
215
+ - **convention-audit**:审计页面 toolbar 是否所有按钮都有 `v-permission`(缺失视为偏差)
216
+ - **prototype-scan**:扫描原型时识别按钮 → 写入 `reports/SYS_PERMISSION_INFO.md` 基线
217
+
218
+ ---
219
+
220
+ ## 8. 报告输出(reports/PERMISSION_SYNC_<日期>.md)
221
+
222
+ ```md
223
+ # 权限同步报告 2026-04-29
224
+
225
+ ## role-manage
226
+ - ✅ 新增角色:测试角色(QA)/ test_qa
227
+
228
+ ## role-assign
229
+ - ✅ 角色 default_role 已分配 5 个菜单:[id1, id2, ...]
230
+ - ⚠️ 替换原有 7 个菜单(已确认)
231
+
232
+ ## action-attach
233
+ - ✅ customer 页面新增 4 个动作:add/edit/remove/export
234
+ - ✅ 代码已加 v-permission(src/views/customer/list/index.vue 第 32-45 行)
235
+
236
+ ## 回滚提示
237
+ - 角色分配可通过重新调用 saveRoleMenus 传旧 menuIds 恢复
238
+ - 新增的角色/动作建议通过后端管理界面手动删除(防止误删生产数据)
239
+ ```
@@ -0,0 +1,93 @@
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. 给页面挂动作 + 加 v-permission
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/index.vue toolbar,加 v-permission
50
+ ✅ 完成(已写报告 reports/PERMISSION_SYNC_20260429.md)
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 常见问题
56
+
57
+ ### Q1:角色授权时,为什么 menuIds 要传完整列表?
58
+
59
+ 后端 `saveRoleMenus` 是**全量覆盖**接口。你传 `[A, B]`,原先 `[A, B, C]` 就会变成 `[A, B]`,C 丢失。
60
+
61
+ **正确做法**:先查角色现有菜单 → 合并新菜单 → 一起传。
62
+
63
+ ### Q2:v-permission 指令在哪里实现?
64
+
65
+ 通常由项目基座(`@jhlc/common-core` 或登录 store)注册全局指令。检查方式:
66
+ ```bash
67
+ grep -r "v-permission\|app.directive('permission'" src/
68
+ ```
69
+ 如未找到,请联系架构组确认。
70
+
71
+ ### Q3:权限码用短形式还是长形式?
72
+
73
+ 参照项目既有用法。新项目建议短形式(`customer_add`),与平台示例数据一致。
74
+
75
+ ### Q4:能批量删除角色/动作吗?
76
+
77
+ **不能**。permission-sync 仅新增不删除(防误删)。删除请走后端管理界面手动操作。
78
+
79
+ ---
80
+
81
+ ## 配置要点
82
+
83
+ 只需 `.github/skills/sync/env.local.json` 的根字段:
84
+
85
+ ```json
86
+ {
87
+ "gatewayPath": "http://你的网关:端口",
88
+ "sysAppNo": "应用编码",
89
+ "token": "Bearer Token"
90
+ }
91
+ ```
92
+
93
+ 不需要额外配置 `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.6",
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(嘉为蓝鲸)权限同步策略