@qinsilk/scm-agent-tools 0.0.0-stage → 0.2.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.
- package/README.md +48 -2
- package/cli/api.js +114 -0
- package/cli/commands/data.js +112 -0
- package/cli/commands/docs.js +121 -0
- package/cli/commands/log.js +152 -0
- package/cli/commands/query.js +285 -0
- package/cli/commands/report.js +213 -0
- package/cli/commands/staff.js +505 -0
- package/cli/config.js +46 -0
- package/cli/datasets.js +541 -0
- package/cli/scm-agent.js +110 -0
- package/cli/skills.js +89 -0
- package/cli/table.js +43 -0
- package/docs/api.md +2067 -0
- package/docs/cli.md +252 -0
- package/package.json +27 -4
- package/reports/excel.js +107 -0
- package/reports/factoryEvaluation.js +128 -0
- package/reports/goodsBom.js +648 -0
- package/reports/goodsFirstOrder.js +409 -0
- package/reports/goodsInventory.js +220 -0
- package/reports/goodsSize.js +440 -0
- package/reports/image.js +216 -0
- package/reports/leakCheck.js +694 -0
- package/reports/materialPurchaseExport.js +393 -0
- package/reports/materialTransit.js +593 -0
- package/reports/processFollowup.js +312 -0
- package/reports/produceBoard.js +353 -0
- package/reports/produceCost.js +366 -0
- package/reports/produceCostDetail.js +622 -0
- package/reports/produceDaily.js +485 -0
- package/reports/storeInGoods.js +276 -0
- package/reports/supplierBalance.js +671 -0
- package/reports/ticketStatistics.js +289 -0
- package/reports/util.js +83 -0
- package/skills/bom/345/225/206/345/223/201/344/277/241/346/201/257/SKILL.md +40 -0
- package/skills/qinsilk-scm/SKILL.md +126 -0
- package/skills//344/276/233/345/272/224/345/225/206/345/257/271/350/264/246/345/257/274/345/207/272/SKILL.md +38 -0
- package/skills//345/225/206/345/223/201/345/260/272/345/257/270/344/277/241/346/201/257/SKILL.md +40 -0
- package/skills//345/225/206/345/223/201/351/246/226/345/215/225/351/235/242/346/226/231/345/267/245/345/272/217/345/215/225/344/273/267/SKILL.md +39 -0
- package/skills//345/267/245/345/216/202/350/257/204/344/274/260/346/212/245/350/241/250/SKILL.md +39 -0
- package/skills//346/210/220/345/223/201/345/205/245/345/272/223/346/212/245/350/241/250/SKILL.md +40 -0
- package/skills//346/234/215/350/243/205/346/210/220/345/223/201/347/233/230/345/255/230/350/241/250/SKILL.md +39 -0
- package/skills//346/274/217/346/226/231/346/243/200/346/265/213/346/227/245/346/212/245/SKILL.md +40 -0
- package/skills//347/211/251/346/226/231/345/234/250/351/200/224/347/212/266/346/200/201/346/212/245/350/241/250/SKILL.md +40 -0
- package/skills//347/211/251/346/226/231/351/207/207/350/264/255/345/257/274/345/207/272/SKILL.md +39 -0
- package/skills//347/224/237/344/272/247/345/215/225/345/267/245/345/272/217/350/267/237/350/277/233/350/241/250/SKILL.md +39 -0
- package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/SKILL.md +41 -0
- package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/346/230/216/347/273/206/SKILL.md +40 -0
- package/skills//347/224/237/344/272/247/346/227/245/346/212/245/SKILL.md +42 -0
- package/skills//347/224/237/344/272/247/347/212/266/345/206/265/347/273/237/350/256/241/350/241/250/SKILL.md +39 -0
- package/skills//347/224/237/344/272/247/350/212/202/347/202/271/347/234/213/346/235/277/SKILL.md +39 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// 员工与权限管理命令族:创建员工、查用户角色、角色与权限菜单的查询、角色权限增减。
|
|
2
|
+
// 写命令(user add、role add-permission、role remove-permission)默认只做查询与解析并
|
|
3
|
+
// 输出预览,不发出任何写请求;加 --confirm 才真正执行——这是「改动前必须让用户确认」
|
|
4
|
+
// 的机制,调用方(AI)必须先把预览展示给用户并取得同意。
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { call } = require('../api');
|
|
7
|
+
const { printTable } = require('../table');
|
|
8
|
+
|
|
9
|
+
/** 与网页一致的密码转换:明文 md5 后大写。服务端在此基础上二次加密存库 */
|
|
10
|
+
function md5Upper(text) {
|
|
11
|
+
return crypto.createHash('md5').update(String(text), 'utf8').digest('hex').toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function splitNames(value) {
|
|
15
|
+
if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter(Boolean);
|
|
16
|
+
return String(value || '').split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 权限的展示名:备注优先(网页权限不足提示引号内的就是它) */
|
|
20
|
+
function menuLabel(menu) {
|
|
21
|
+
return menu.remark || menu.label || `id=${menu.id}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 权限的完整描述,用于需要区分同名项的场合。
|
|
26
|
+
* 备注会重名:同一个「商品管理的查看」对应按设计师/跟单员/品牌/设计助理判断四个变体,
|
|
27
|
+
* 只显示备注时用户无法分辨,因此连同功能标题、权限 key 与菜单 id 一起给出。
|
|
28
|
+
*/
|
|
29
|
+
function describeMenu(menu) {
|
|
30
|
+
const parts = [menuLabel(menu)];
|
|
31
|
+
if (menu.label && menu.label !== menu.remark) {
|
|
32
|
+
parts.push(`标题:${menu.label}`);
|
|
33
|
+
}
|
|
34
|
+
if (menu.permission) {
|
|
35
|
+
parts.push(menu.permission);
|
|
36
|
+
}
|
|
37
|
+
parts.push(`id=${menu.id}`);
|
|
38
|
+
return parts.join('|');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function describeRole(role) {
|
|
42
|
+
return `${role.name}(id=${role.id}${role.isSystem ? ',系统角色' : ''})`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ------------------------------------------------------------------ 服务端查询与解析
|
|
46
|
+
|
|
47
|
+
async function fetchRoles(name) {
|
|
48
|
+
const params = {};
|
|
49
|
+
if (name) params.name = name;
|
|
50
|
+
return call('GET', '/api/agent/v1/role/list', params);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 按名称解析角色:先精确匹配,再唯一前缀匹配;匹配不到或有歧义时报错并列出候选。
|
|
55
|
+
* id 优先于名称。
|
|
56
|
+
*/
|
|
57
|
+
async function resolveRole({ id, name }) {
|
|
58
|
+
if (id) {
|
|
59
|
+
return { id, name: null };
|
|
60
|
+
}
|
|
61
|
+
if (!name) {
|
|
62
|
+
throw new Error('请用 --id 指定角色 id,或用 --name 指定角色名');
|
|
63
|
+
}
|
|
64
|
+
const roles = await fetchRoles(name);
|
|
65
|
+
const exact = roles.filter((r) => r.name === name);
|
|
66
|
+
if (exact.length === 1) {
|
|
67
|
+
return exact[0];
|
|
68
|
+
}
|
|
69
|
+
if (exact.length > 1) {
|
|
70
|
+
throw new Error(`存在多个同名角色「${name}」,请用 --id 指定:${exact.map(describeRole).join(';')}`);
|
|
71
|
+
}
|
|
72
|
+
const prefix = roles.filter((r) => r.name.startsWith(name));
|
|
73
|
+
if (prefix.length === 1) {
|
|
74
|
+
return prefix[0];
|
|
75
|
+
}
|
|
76
|
+
const candidates = roles.slice(0, 8).map(describeRole).join(';') || '(无)';
|
|
77
|
+
throw new Error(`没有找到角色「${name}」。相近角色:${candidates}。可执行 scm-agent role list --name ${name} 查看`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 把用户给的多个角色名解析为角色行,全部可解析才继续 */
|
|
81
|
+
async function resolveRoles(names) {
|
|
82
|
+
const all = await fetchRoles();
|
|
83
|
+
const rows = [];
|
|
84
|
+
for (const name of names) {
|
|
85
|
+
let hit = all.filter((r) => r.name === name);
|
|
86
|
+
if (hit.length === 0) {
|
|
87
|
+
hit = all.filter((r) => r.name.startsWith(name));
|
|
88
|
+
}
|
|
89
|
+
if (hit.length !== 1) {
|
|
90
|
+
const candidates = all.filter((r) => r.name.includes(name)).slice(0, 8).map(describeRole).join(';') || '(无)';
|
|
91
|
+
throw new Error(`角色「${name}」没有唯一匹配(${hit.length} 个)。候选:${candidates}`);
|
|
92
|
+
}
|
|
93
|
+
if (!rows.some((r) => r.id === hit[0].id)) {
|
|
94
|
+
rows.push(hit[0]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return rows;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 把用户给的多个权限名解析为菜单行。
|
|
102
|
+
*
|
|
103
|
+
* 每项依次尝试:①按菜单 id(纯数字)②精确匹配权限 key ③精确匹配备注
|
|
104
|
+
* ④唯一包含匹配。备注会重名(例如「商品管理的查看」有按设计师、跟单员、
|
|
105
|
+
* 品牌、设计助理判断四个变体),命中多项时不猜,把带标题与权限 key 的候选
|
|
106
|
+
* 列出来让调用方去问用户,用户可用更明确的标题或权限 key 重新提交。
|
|
107
|
+
*/
|
|
108
|
+
async function resolvePermissions(spec) {
|
|
109
|
+
const items = splitNames(spec);
|
|
110
|
+
if (!items.length) {
|
|
111
|
+
throw new Error('请用 --permissions 指定权限(逗号分隔,可用中文名、英文权限 key 或菜单 id)');
|
|
112
|
+
}
|
|
113
|
+
const menus = (await call('GET', '/api/agent/v1/permission/list')).filter((m) => m.state === 1);
|
|
114
|
+
const rows = [];
|
|
115
|
+
const problems = [];
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
const hits = resolveOnePermission(menus, item);
|
|
118
|
+
if (hits.length === 1) {
|
|
119
|
+
if (!rows.some((m) => m.id === hits[0].id)) {
|
|
120
|
+
rows.push(hits[0]);
|
|
121
|
+
}
|
|
122
|
+
} else if (hits.length === 0) {
|
|
123
|
+
problems.push(`「${item}」没有匹配`);
|
|
124
|
+
} else {
|
|
125
|
+
problems.push(`「${item}」有 ${hits.length} 个候选,需指定其中一个:${hits.slice(0, 6).map(describeMenu).join(';')}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (problems.length) {
|
|
129
|
+
throw new Error(`权限解析失败:${problems.join(';')}。可执行 scm-agent permission list --keyword <词> 查看全部候选,再用带标题的完整名称、权限 key 或菜单 id 作为 --permissions`);
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 单项解析:id → 权限 key → 备注精确 → 唯一包含匹配。返回命中列表(0、1 或多条)。 */
|
|
135
|
+
function resolveOnePermission(menus, item) {
|
|
136
|
+
if (/^\d+$/.test(item)) {
|
|
137
|
+
const byId = menus.filter((m) => String(m.id) === item);
|
|
138
|
+
if (byId.length) {
|
|
139
|
+
return byId;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const byKey = menus.filter((m) => m.permission === item);
|
|
143
|
+
if (byKey.length) {
|
|
144
|
+
return byKey;
|
|
145
|
+
}
|
|
146
|
+
const byRemark = menus.filter((m) => m.remark === item);
|
|
147
|
+
if (byRemark.length) {
|
|
148
|
+
return byRemark;
|
|
149
|
+
}
|
|
150
|
+
return menus.filter((m) => (m.remark || '').includes(item) || (m.label || '').includes(item) || (m.permission || '').includes(item));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ------------------------------------------------------------------ 输出
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 预览后的确认提示。这段文字是写给 Agent 读的:CLI 在 Agent 的子进程里运行,
|
|
157
|
+
* 没有交互终端,无法自己弹确认框;确认必须由 Agent 用宿主提供的交互式提问工具完成。
|
|
158
|
+
*/
|
|
159
|
+
function printConfirmHint() {
|
|
160
|
+
console.log('\n以上为将要提交的改动,当前没有修改任何数据。');
|
|
161
|
+
console.log('请调用 AskUserQuestion(Codex 用 request_user_input;宿主没有该工具时改为文字提问),'
|
|
162
|
+
+ '给出「确认」与「取消」两个选项并附上改动摘要;用户想改参数会自己用对话框自带的「Other」输入文字。');
|
|
163
|
+
console.log('选「确认」后加 --confirm 重新执行本命令;选「取消」不发写请求;'
|
|
164
|
+
+ '「Other」里写了新要求则按它重新预览并再次确认。');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function printAffectedUsers(detail) {
|
|
168
|
+
const affected = detail.affectedUsers || [];
|
|
169
|
+
if (affected.length) {
|
|
170
|
+
console.log(`影响范围:使用该角色的 ${affected.length} 个用户——${affected.map((u) => u.name || u.uid).join('、')}`);
|
|
171
|
+
} else {
|
|
172
|
+
console.log('影响范围:当前没有用户使用该角色。');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ------------------------------------------------------------------ 写命令实现
|
|
177
|
+
|
|
178
|
+
/** 创建员工。无 --confirm 时输出预览;有 --confirm 时提交(密码先做与网页一致的 md5 大写) */
|
|
179
|
+
async function runUserAdd(argv) {
|
|
180
|
+
const passwordMd5 = md5Upper(argv.password);
|
|
181
|
+
const roleRows = argv.roles ? await resolveRoles(splitNames(argv.roles)) : [];
|
|
182
|
+
const preview = {
|
|
183
|
+
action: '创建员工(子账号)',
|
|
184
|
+
name: argv.name,
|
|
185
|
+
loginName: argv.loginName,
|
|
186
|
+
passwordMd5,
|
|
187
|
+
roles: roleRows.map((r) => ({ id: r.id, name: r.name })),
|
|
188
|
+
phone: argv.phone,
|
|
189
|
+
departmentId: argv.departmentId,
|
|
190
|
+
workSn: argv.workSn,
|
|
191
|
+
};
|
|
192
|
+
if (!argv.confirm) {
|
|
193
|
+
if (argv.json) {
|
|
194
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
195
|
+
} else {
|
|
196
|
+
console.log('【预览】将创建员工(子账号):');
|
|
197
|
+
console.log(` 姓名:${argv.name}`);
|
|
198
|
+
console.log(` 登录账号:${argv.loginName}`);
|
|
199
|
+
console.log(' 密码:********(已按网页规则加密,不再显示)');
|
|
200
|
+
console.log(` 角色:${roleRows.map((r) => `${r.name}(id=${r.id})`).join('、') || '(不关联角色)'}`);
|
|
201
|
+
if (argv.phone) console.log(` 手机号:${argv.phone}`);
|
|
202
|
+
if (argv.departmentId) console.log(` 部门 id:${argv.departmentId}`);
|
|
203
|
+
if (argv.workSn) console.log(` 工号:${argv.workSn}`);
|
|
204
|
+
}
|
|
205
|
+
printConfirmHint();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const body = {
|
|
209
|
+
name: argv.name,
|
|
210
|
+
loginName: argv.loginName,
|
|
211
|
+
password: passwordMd5,
|
|
212
|
+
roleIds: roleRows.length ? roleRows.map((r) => r.id) : undefined,
|
|
213
|
+
phone: argv.phone,
|
|
214
|
+
departmentId: argv.departmentId,
|
|
215
|
+
workSn: argv.workSn,
|
|
216
|
+
};
|
|
217
|
+
Object.keys(body).forEach((key) => body[key] === undefined && delete body[key]);
|
|
218
|
+
const data = await call('POST', '/api/agent/v1/user/add', body);
|
|
219
|
+
if (argv.json) {
|
|
220
|
+
console.log(JSON.stringify(data, null, 2));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.log(`✔ 已创建员工:${data.name}(uid=${data.uid}),登录账号 ${data.loginName}${data.roleNames ? `,角色:${data.roleNames.join('、')}` : ''}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 角色权限增减。mode 为 add 或 remove;无 --confirm 时输出预览(含受影响用户) */
|
|
227
|
+
async function runRolePermission(argv, mode) {
|
|
228
|
+
const role = await resolveRole({ id: argv.id, name: argv.name });
|
|
229
|
+
const detail = await call('GET', '/api/agent/v1/role/detail', { roleId: role.id });
|
|
230
|
+
if (detail.isSystem) {
|
|
231
|
+
throw new Error(`「${detail.name}」是系统内置角色,不能修改权限。可建议用户复制一个自定义角色再调整`);
|
|
232
|
+
}
|
|
233
|
+
const permissions = await resolvePermissions(argv.permissions);
|
|
234
|
+
const current = detail.permissions || [];
|
|
235
|
+
const verb = mode === 'add' ? '增加' : '移除';
|
|
236
|
+
if (mode === 'add') {
|
|
237
|
+
const already = permissions.filter((p) => current.some((c) => c.menuId === p.id));
|
|
238
|
+
if (already.length) {
|
|
239
|
+
throw new Error(`该角色已拥有:${already.map(describeMenu).join(';')}`);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
const notOwned = permissions.filter((p) => !current.some((c) => c.menuId === p.id));
|
|
243
|
+
if (notOwned.length) {
|
|
244
|
+
throw new Error(`该角色没有这些权限:${notOwned.map(describeMenu).join(';')}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const after = mode === 'add' ? current.length + permissions.length : current.length - permissions.length;
|
|
248
|
+
if (!argv.confirm) {
|
|
249
|
+
if (argv.json) {
|
|
250
|
+
console.log(JSON.stringify({
|
|
251
|
+
action: `${verb}角色权限`,
|
|
252
|
+
role: { id: detail.roleId, name: detail.name },
|
|
253
|
+
permissions: permissions.map((p) => ({ menuId: p.id, name: menuLabel(p), label: p.label, permission: p.permission })),
|
|
254
|
+
currentCount: current.length,
|
|
255
|
+
afterCount: after,
|
|
256
|
+
affectedUsers: detail.affectedUsers,
|
|
257
|
+
}, null, 2));
|
|
258
|
+
} else {
|
|
259
|
+
console.log(`【预览】将给角色「${detail.name}」${verb}以下权限:`);
|
|
260
|
+
permissions.forEach((p) => console.log(` - ${describeMenu(p)}`));
|
|
261
|
+
console.log(`该角色现有权限 ${current.length} 项,改动后 ${after} 项。`);
|
|
262
|
+
printAffectedUsers(detail);
|
|
263
|
+
}
|
|
264
|
+
printConfirmHint();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const body = { roleId: detail.roleId };
|
|
268
|
+
body[mode === 'add' ? 'addMenuIds' : 'removeMenuIds'] = permissions.map((p) => p.id);
|
|
269
|
+
const data = await call('POST', '/api/agent/v1/role/update-permission', body);
|
|
270
|
+
if (argv.json) {
|
|
271
|
+
console.log(JSON.stringify(data, null, 2));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const changed = (mode === 'add' ? data.added : data.removed) || [];
|
|
275
|
+
console.log(`✔ 已${verb}角色「${data.roleName}」的 ${changed.length} 项权限(${changed.map((p) => p.remark || p.label).join('、')}),该角色现有 ${data.totalPermissions} 项权限。`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ------------------------------------------------------------------ 命令定义
|
|
279
|
+
|
|
280
|
+
const user = {
|
|
281
|
+
command: 'user',
|
|
282
|
+
describe: '员工管理(创建子账号、搜员工、查员工角色)',
|
|
283
|
+
builder(yargs) {
|
|
284
|
+
return yargs
|
|
285
|
+
.command({
|
|
286
|
+
command: 'add',
|
|
287
|
+
describe: '创建员工(子账号)。默认只预览,加 --confirm 才创建',
|
|
288
|
+
builder: (y) => y
|
|
289
|
+
.option('name', { describe: '姓名', demandOption: true, type: 'string' })
|
|
290
|
+
.option('login-name', { describe: '登录账号(字母或数字,不能是 11 位手机号)', demandOption: true, type: 'string' })
|
|
291
|
+
.option('password', { describe: '密码(明文,命令内部加密后提交)', demandOption: true, type: 'string' })
|
|
292
|
+
.option('roles', { describe: '角色名,逗号分隔(如:设计师,版师)', type: 'string' })
|
|
293
|
+
.option('phone', { describe: '手机号', type: 'string' })
|
|
294
|
+
.option('department-id', { describe: '部门 id', type: 'number' })
|
|
295
|
+
.option('work-sn', { describe: '工号', type: 'string' })
|
|
296
|
+
.option('confirm', { describe: '确认执行(不加则只预览)', type: 'boolean', default: false }),
|
|
297
|
+
handler: (argv) => runStaff(argv, () => runUserAdd(argv)),
|
|
298
|
+
})
|
|
299
|
+
.command({
|
|
300
|
+
command: 'search',
|
|
301
|
+
describe: '按姓名/手机号/工号搜员工(用于查 uid,再交给 user detail)',
|
|
302
|
+
builder: (y) => y.option('name', { describe: '姓名、手机号或工号的一部分', demandOption: true, type: 'string' }),
|
|
303
|
+
handler: (argv) => runStaff(argv, async () => {
|
|
304
|
+
const data = await call('GET', '/api/agent/v1/user-list', { searchKey: argv.name, page: argv.page, limit: argv.limit });
|
|
305
|
+
if (argv.json) {
|
|
306
|
+
return { json: data };
|
|
307
|
+
}
|
|
308
|
+
const rows = data.list.map((u) => ({
|
|
309
|
+
name: u.name,
|
|
310
|
+
uid: u.uid,
|
|
311
|
+
department: u.departmentName || '-',
|
|
312
|
+
state: u.status === 1 ? '正常' : '停用',
|
|
313
|
+
phone: u.phone || '-',
|
|
314
|
+
workSn: u.workSn || '-',
|
|
315
|
+
}));
|
|
316
|
+
if (!rows.length) {
|
|
317
|
+
console.log('(无数据)');
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
console.log(`共 ${data.total} 个员工(本页 ${rows.length} 条)`);
|
|
321
|
+
printTable(rows, [
|
|
322
|
+
{ field: 'name', label: '姓名' },
|
|
323
|
+
{ field: 'uid', label: '用户id' },
|
|
324
|
+
{ field: 'department', label: '部门' },
|
|
325
|
+
{ field: 'state', label: '状态' },
|
|
326
|
+
{ field: 'phone', label: '手机号' },
|
|
327
|
+
{ field: 'workSn', label: '工号' },
|
|
328
|
+
]);
|
|
329
|
+
return {};
|
|
330
|
+
}),
|
|
331
|
+
})
|
|
332
|
+
.command({
|
|
333
|
+
command: 'detail',
|
|
334
|
+
describe: '员工详情(基本信息与拥有的角色)',
|
|
335
|
+
builder: (y) => y.option('uid', { describe: '员工用户 id', demandOption: true, type: 'number' }),
|
|
336
|
+
handler: (argv) => runStaff(argv, async () => {
|
|
337
|
+
const data = await call('GET', '/api/agent/v1/user/detail', { uid: argv.uid });
|
|
338
|
+
if (argv.json) {
|
|
339
|
+
return { json: data };
|
|
340
|
+
}
|
|
341
|
+
console.log(`员工:${data.name}(uid=${data.uid}),登录账号 ${data.loginName},状态 ${data.status === 1 ? '正常' : data.status}`);
|
|
342
|
+
const roles = (data.roles || []).map((r) => ({ name: r.name, id: r.id, _row: r }));
|
|
343
|
+
if (roles.length) {
|
|
344
|
+
console.log(`拥有 ${roles.length} 个角色:`);
|
|
345
|
+
printTable(roles, [
|
|
346
|
+
{ field: 'name', label: '角色名' },
|
|
347
|
+
{ field: 'id', label: '角色id' },
|
|
348
|
+
]);
|
|
349
|
+
console.log('给「某个用户」调整权限时,改的是他其中一个角色的权限;多个角色时必须先让用户选择改哪一个。');
|
|
350
|
+
} else {
|
|
351
|
+
console.log('该员工没有角色。');
|
|
352
|
+
}
|
|
353
|
+
return {};
|
|
354
|
+
}),
|
|
355
|
+
})
|
|
356
|
+
.demandCommand(1, '请指定子命令:add、search 或 detail');
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const role = {
|
|
361
|
+
command: 'role',
|
|
362
|
+
describe: '角色与权限管理(查角色、给角色增加/移除权限)',
|
|
363
|
+
builder(yargs) {
|
|
364
|
+
return yargs
|
|
365
|
+
.command({
|
|
366
|
+
command: 'list',
|
|
367
|
+
describe: '角色列表(含系统内置角色)',
|
|
368
|
+
builder: (y) => y.option('name', { describe: '按角色名模糊过滤', type: 'string' }),
|
|
369
|
+
handler: (argv) => runStaff(argv, async () => {
|
|
370
|
+
const data = await fetchRoles(argv.name);
|
|
371
|
+
if (argv.json) {
|
|
372
|
+
return { json: data };
|
|
373
|
+
}
|
|
374
|
+
const rows = data.map((r) => ({
|
|
375
|
+
name: r.name,
|
|
376
|
+
type: r.isSystem ? '系统' : '自定义',
|
|
377
|
+
state: r.state === 1 ? '有效' : '无效',
|
|
378
|
+
id: r.id,
|
|
379
|
+
remark: r.remark || '-',
|
|
380
|
+
}));
|
|
381
|
+
if (!rows.length) {
|
|
382
|
+
console.log('(无数据)');
|
|
383
|
+
return {};
|
|
384
|
+
}
|
|
385
|
+
console.log(`共 ${rows.length} 个角色`);
|
|
386
|
+
printTable(rows, [
|
|
387
|
+
{ field: 'name', label: '角色名' },
|
|
388
|
+
{ field: 'type', label: '类型' },
|
|
389
|
+
{ field: 'state', label: '状态' },
|
|
390
|
+
{ field: 'id', label: '角色id' },
|
|
391
|
+
{ field: 'remark', label: '备注' },
|
|
392
|
+
]);
|
|
393
|
+
return {};
|
|
394
|
+
}),
|
|
395
|
+
})
|
|
396
|
+
.command({
|
|
397
|
+
command: 'detail',
|
|
398
|
+
describe: '角色详情(现有权限与使用该角色的用户)',
|
|
399
|
+
builder: (y) => y
|
|
400
|
+
.option('id', { describe: '角色 id', type: 'number' })
|
|
401
|
+
.option('name', { describe: '角色名(精确或唯一前缀)', type: 'string' }),
|
|
402
|
+
handler: (argv) => runStaff(argv, async () => {
|
|
403
|
+
const roleRow = await resolveRole(argv);
|
|
404
|
+
const data = await call('GET', '/api/agent/v1/role/detail', { roleId: roleRow.id });
|
|
405
|
+
if (argv.json) {
|
|
406
|
+
return { json: data };
|
|
407
|
+
}
|
|
408
|
+
console.log(`角色:${data.name}(id=${data.roleId}),${data.isSystem ? '系统内置角色(不能修改)' : '自定义角色'},现有权限 ${(data.permissions || []).length} 项`);
|
|
409
|
+
const affected = data.affectedUsers || [];
|
|
410
|
+
console.log(`使用该角色的用户(${affected.length} 个):${affected.map((u) => u.name || u.uid).join('、') || '(无)'}`);
|
|
411
|
+
const rows = (data.permissions || []).map((p) => ({
|
|
412
|
+
name: p.remark || p.label || '-',
|
|
413
|
+
permission: p.permission || '-',
|
|
414
|
+
menuId: p.menuId,
|
|
415
|
+
}));
|
|
416
|
+
if (rows.length) {
|
|
417
|
+
printTable(rows, [
|
|
418
|
+
{ field: 'name', label: '权限' },
|
|
419
|
+
{ field: 'permission', label: '权限key' },
|
|
420
|
+
{ field: 'menuId', label: '菜单id' },
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
return {};
|
|
424
|
+
}),
|
|
425
|
+
})
|
|
426
|
+
.command({
|
|
427
|
+
command: 'add-permission',
|
|
428
|
+
describe: '给角色增加权限。默认只预览,加 --confirm 才执行',
|
|
429
|
+
builder: rolePermissionOptions(),
|
|
430
|
+
handler: (argv) => runStaff(argv, () => runRolePermission(argv, 'add')),
|
|
431
|
+
})
|
|
432
|
+
.command({
|
|
433
|
+
command: 'remove-permission',
|
|
434
|
+
describe: '给角色移除权限。默认只预览,加 --confirm 才执行',
|
|
435
|
+
builder: rolePermissionOptions(),
|
|
436
|
+
handler: (argv) => runStaff(argv, () => runRolePermission(argv, 'remove')),
|
|
437
|
+
})
|
|
438
|
+
.demandCommand(1, '请指定子命令:list、detail、add-permission 或 remove-permission');
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
function rolePermissionOptions() {
|
|
443
|
+
return (y) => y
|
|
444
|
+
.option('id', { describe: '角色 id', type: 'number' })
|
|
445
|
+
.option('name', { describe: '角色名(精确或唯一前缀;与 --id 二选一)', type: 'string' })
|
|
446
|
+
.option('permissions', { describe: '权限,逗号分隔;可用中文备注(权限不足提示引号内的文字)或英文权限 key', demandOption: true, type: 'string' })
|
|
447
|
+
.option('confirm', { describe: '确认执行(不加则只预览)', type: 'boolean', default: false });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const permission = {
|
|
451
|
+
command: 'permission',
|
|
452
|
+
describe: '权限菜单查询(中文名/权限 key/菜单 id 的对照)',
|
|
453
|
+
builder(yargs) {
|
|
454
|
+
return yargs
|
|
455
|
+
.command({
|
|
456
|
+
command: 'list',
|
|
457
|
+
describe: '权限菜单列表(可用 keyword 过滤)',
|
|
458
|
+
builder: (y) => y.option('keyword', { describe: '按标题/备注/权限 key 过滤', type: 'string' }),
|
|
459
|
+
handler: (argv) => runStaff(argv, async () => {
|
|
460
|
+
const params = {};
|
|
461
|
+
if (argv.keyword) params.keyword = argv.keyword;
|
|
462
|
+
const data = await call('GET', '/api/agent/v1/permission/list', params);
|
|
463
|
+
if (argv.json) {
|
|
464
|
+
return { json: data };
|
|
465
|
+
}
|
|
466
|
+
const rows = data.map((m) => ({
|
|
467
|
+
name: m.remark || m.label || '-',
|
|
468
|
+
label: m.label || '-',
|
|
469
|
+
permission: m.permission || '-',
|
|
470
|
+
type: m.type === 1 ? '目录' : m.type === 2 ? '页面' : '操作',
|
|
471
|
+
id: m.id,
|
|
472
|
+
}));
|
|
473
|
+
if (!rows.length) {
|
|
474
|
+
console.log('(无数据)');
|
|
475
|
+
return {};
|
|
476
|
+
}
|
|
477
|
+
console.log(`共 ${rows.length} 项`);
|
|
478
|
+
printTable(rows, [
|
|
479
|
+
{ field: 'name', label: '权限备注' },
|
|
480
|
+
{ field: 'label', label: '标题' },
|
|
481
|
+
{ field: 'permission', label: '权限key' },
|
|
482
|
+
{ field: 'type', label: '类型' },
|
|
483
|
+
{ field: 'id', label: '菜单id' },
|
|
484
|
+
]);
|
|
485
|
+
return {};
|
|
486
|
+
}),
|
|
487
|
+
})
|
|
488
|
+
.demandCommand(1, '请指定子命令:list');
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/** 统一执行壳:任务返回 { json } 时按 --json 决定输出;异常输出可读文案并以非 0 退出 */
|
|
493
|
+
async function runStaff(argv, task) {
|
|
494
|
+
try {
|
|
495
|
+
const result = await task();
|
|
496
|
+
if (result && result.json !== undefined && argv.json) {
|
|
497
|
+
console.log(JSON.stringify(result.json, null, 2));
|
|
498
|
+
}
|
|
499
|
+
} catch (e) {
|
|
500
|
+
console.error(`错误:${e.message}`);
|
|
501
|
+
process.exitCode = 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
module.exports = { user, role, permission, md5Upper, resolveRole, resolveRoles, resolvePermissions, menuLabel, describeMenu, resolveOnePermission };
|
package/cli/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// 服务器地址默认值与本机配置文件路径。
|
|
2
|
+
// 配置文件保存 AI Key 与访问令牌,权限 600,任何情况下不进入仓库与对话输出。
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SERVER = 'https://scm.qinsilk.com';
|
|
8
|
+
|
|
9
|
+
/** 配置根目录:默认用户主目录;SCM_AGENT_HOME 环境变量可覆盖(多环境调试用) */
|
|
10
|
+
function homeDir() {
|
|
11
|
+
return process.env.SCM_AGENT_HOME || os.homedir();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function configDir(home = homeDir()) {
|
|
15
|
+
return path.join(home, '.scm-agent');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function configPath(home = homeDir()) {
|
|
19
|
+
return path.join(configDir(home), 'config.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readConfig(home = homeDir()) {
|
|
23
|
+
const file = configPath(home);
|
|
24
|
+
if (!fs.existsSync(file)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 保存配置。文件权限设为 600(仅本用户可读写),目录不存在时先创建。
|
|
32
|
+
*/
|
|
33
|
+
function writeConfig(config, home = homeDir()) {
|
|
34
|
+
const dir = configDir(home);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
const file = configPath(home);
|
|
37
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
38
|
+
// Windows 上 writeFileSync 的 mode 只在新建时生效,已是其他权限时需要显式收紧
|
|
39
|
+
try {
|
|
40
|
+
fs.chmodSync(file, 0o600);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// 部分文件系统不支持 chmod,忽略
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { DEFAULT_SERVER, configDir, configPath, readConfig, writeConfig };
|