cloudhouse-admin-cli 0.3.3
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 +26 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/SECURITY.md +11 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/bin/ch.mjs +11 -0
- package/docs/00-overview.md +71 -0
- package/docs/01-command-reference.md +1206 -0
- package/docs/02-agent-contract.md +118 -0
- package/docs/03-agent-setup.md +69 -0
- package/docs/04-audit.md +3 -0
- package/docs/05-release.md +5 -0
- package/docs/06-coverage.md +3 -0
- package/docs/07-data-analysis.md +105 -0
- package/docs/08-installation.md +36 -0
- package/docs/security-review.md +26 -0
- package/integrations/codex/.agents/plugins/marketplace.json +20 -0
- package/integrations/codex/.codex-plugin/plugin.json +10 -0
- package/integrations/codex/LICENSE +21 -0
- package/integrations/codex/README.md +5 -0
- package/integrations/codex/package.json +7 -0
- package/integrations/codex/plugin.json +11 -0
- package/integrations/codex/skills/cloudhouse-admin/SKILL.md +32 -0
- package/integrations/codex/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
- package/integrations/codex/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
- package/integrations/dsh/LICENSE +21 -0
- package/integrations/dsh/README.md +14 -0
- package/integrations/dsh/index.mjs +7 -0
- package/integrations/dsh/package.json +12 -0
- package/integrations/dsh/skills/cloudhouse-admin/SKILL.md +32 -0
- package/integrations/dsh/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
- package/integrations/dsh/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
- package/integrations/openclaw/LICENSE +21 -0
- package/integrations/openclaw/README.md +5 -0
- package/integrations/openclaw/package.json +7 -0
- package/integrations/openclaw/skills/cloudhouse-admin/SKILL.md +32 -0
- package/integrations/openclaw/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
- package/integrations/openclaw/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
- package/integrations/opencode/LICENSE +21 -0
- package/integrations/opencode/README.md +5 -0
- package/integrations/opencode/package.json +7 -0
- package/integrations/opencode/skills/cloudhouse-admin/SKILL.md +32 -0
- package/integrations/opencode/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
- package/integrations/opencode/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
- package/integrations/pi/LICENSE +21 -0
- package/integrations/pi/README.md +5 -0
- package/integrations/pi/package.json +15 -0
- package/integrations/pi/skills/cloudhouse-admin/SKILL.md +32 -0
- package/integrations/pi/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
- package/integrations/pi/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
- package/package.json +44 -0
- package/skills/cloudhouse-admin/SKILL.md +32 -0
- package/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
- package/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
- package/src/agent-install.mjs +50 -0
- package/src/analysis-resources.json +1207 -0
- package/src/argv.mjs +88 -0
- package/src/capabilities.mjs +18 -0
- package/src/cli.mjs +180 -0
- package/src/client.mjs +169 -0
- package/src/commands/accounts.mjs +115 -0
- package/src/commands/agent.mjs +13 -0
- package/src/commands/ai.mjs +420 -0
- package/src/commands/analysis.mjs +124 -0
- package/src/commands/api.mjs +63 -0
- package/src/commands/applies.mjs +89 -0
- package/src/commands/assistant.mjs +53 -0
- package/src/commands/auth.mjs +109 -0
- package/src/commands/candidates.mjs +252 -0
- package/src/commands/checkin.mjs +41 -0
- package/src/commands/dashboard.mjs +11 -0
- package/src/commands/evaluations.mjs +27 -0
- package/src/commands/extended.mjs +96 -0
- package/src/commands/feishu.mjs +31 -0
- package/src/commands/groups.mjs +66 -0
- package/src/commands/index.mjs +101 -0
- package/src/commands/interviews.mjs +151 -0
- package/src/commands/load.mjs +27 -0
- package/src/commands/notifications.mjs +131 -0
- package/src/commands/plans.mjs +90 -0
- package/src/commands/profile.mjs +43 -0
- package/src/commands/slots.mjs +95 -0
- package/src/commands/uploads.mjs +169 -0
- package/src/config.mjs +68 -0
- package/src/errors.mjs +156 -0
- package/src/flags.mjs +114 -0
- package/src/idempotent.mjs +75 -0
- package/src/output.mjs +138 -0
- package/src/path.mjs +40 -0
- package/src/payload.mjs +137 -0
- package/src/prompt.mjs +47 -0
- package/src/query.mjs +44 -0
- package/src/sensitive.mjs +50 -0
- package/src/session.mjs +94 -0
- package/src/time.mjs +87 -0
- package/src/version.mjs +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { register } from './index.mjs'
|
|
2
|
+
import { usageError } from '../errors.mjs'
|
|
3
|
+
import { collectQuery, parseNumber } from '../query.mjs'
|
|
4
|
+
import { flagEnum, flagString, flagNumber, payloadFrom } from '../payload.mjs'
|
|
5
|
+
import { requireReason } from '../sensitive.mjs'
|
|
6
|
+
import { idParam } from '../path.mjs'
|
|
7
|
+
|
|
8
|
+
const POSITIONS = ['CAPTAIN', 'LEADER', 'MEMBER']
|
|
9
|
+
|
|
10
|
+
register('applies list', {
|
|
11
|
+
summary: '管理员申请审批列表',
|
|
12
|
+
usage: 'ch applies list [--status 0|1|2] [--pretty]',
|
|
13
|
+
endpoints: ['GET /admin/account-applications'],
|
|
14
|
+
run: async (ctx) => {
|
|
15
|
+
const query = collectQuery(ctx, { status: { parse: parseNumber } })
|
|
16
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/account-applications', query })
|
|
17
|
+
return data
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
register('applies pending-count', {
|
|
22
|
+
summary: '未审批申请数量',
|
|
23
|
+
usage: 'ch applies pending-count',
|
|
24
|
+
endpoints: ['GET /admin/account-applications/pending-count'],
|
|
25
|
+
run: async (ctx) => {
|
|
26
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/account-applications/pending-count' })
|
|
27
|
+
return data
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
register('applies approve', {
|
|
32
|
+
summary: '审批通过(自动开通账号,账号=学号,初始密码=学号)',
|
|
33
|
+
usage: 'ch applies approve <id>',
|
|
34
|
+
endpoints: ['POST /admin/account-applications/{id}/approve'],
|
|
35
|
+
run: async (ctx) => {
|
|
36
|
+
const id = idParam(ctx.positionals[0], { label: '申请 id' })
|
|
37
|
+
const { data } = await ctx.client.request({ method: 'POST', path: `/admin/account-applications/${id}/approve` })
|
|
38
|
+
return data
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
register('applies reject', {
|
|
43
|
+
summary: '驳回申请(原因必填)',
|
|
44
|
+
usage: 'ch applies reject <id> --reason 驳回原因',
|
|
45
|
+
endpoints: ['POST /admin/account-applications/{id}/reject'],
|
|
46
|
+
run: async (ctx) => {
|
|
47
|
+
const id = idParam(ctx.positionals[0], { label: '申请 id' })
|
|
48
|
+
const reason = requireReason(ctx, { label: 'ch applies reject <id> --reason 驳回原因' })
|
|
49
|
+
const payload = payloadFrom(ctx, { reason })
|
|
50
|
+
const { data } = await ctx.client.request({ method: 'POST', path: `/admin/account-applications/${id}/reject`, body: payload })
|
|
51
|
+
return data === null ? { rejected: true, id: Number(id) } : data
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
register('applies status', {
|
|
56
|
+
summary: '按学号查询申请进度(公开接口,免登录)',
|
|
57
|
+
usage: 'ch applies status <studentNo>',
|
|
58
|
+
endpoints: ['GET /account-applications/status'],
|
|
59
|
+
auth: false,
|
|
60
|
+
run: async (ctx) => {
|
|
61
|
+
const studentNo = idParam(ctx.positionals[0], { label: '学号' })
|
|
62
|
+
const { data } = await ctx.client.request({
|
|
63
|
+
method: 'GET',
|
|
64
|
+
path: '/account-applications/status',
|
|
65
|
+
query: [['studentNo', studentNo]],
|
|
66
|
+
noAuth: true,
|
|
67
|
+
})
|
|
68
|
+
return data
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
register('applies submit', {
|
|
73
|
+
summary: '提交管理员账号申请(公开接口,免登录)',
|
|
74
|
+
usage: 'ch applies submit --studentNo 10位学号 --real-name 姓名 --position-code CAPTAIN|LEADER|MEMBER [--apply-group-id N]',
|
|
75
|
+
endpoints: ['POST /account-applications'],
|
|
76
|
+
auth: false,
|
|
77
|
+
run: async (ctx) => {
|
|
78
|
+
const studentNo = flagString(ctx, 'studentNo', { required: true, label: 'ch applies submit --studentNo 2026000001' })
|
|
79
|
+
if (!/^\d{10}$/.test(studentNo)) throw usageError('学号必须是 10 位数字')
|
|
80
|
+
const realName = flagString(ctx, 'real-name', { required: true, label: 'ch applies submit --real-name 姓名' })
|
|
81
|
+
const positionCode = flagEnum(ctx, 'position-code', POSITIONS, { required: true, label: `--position-code ${POSITIONS.join('|')}` })
|
|
82
|
+
const base = { studentNo, realName, positionCode }
|
|
83
|
+
const applyGroupId = flagNumber(ctx, 'apply-group-id')
|
|
84
|
+
if (applyGroupId !== undefined) base.applyGroupId = applyGroupId
|
|
85
|
+
const payload = payloadFrom(ctx, base)
|
|
86
|
+
const { data } = await ctx.client.request({ method: 'POST', path: '/account-applications', body: payload, noAuth: true })
|
|
87
|
+
return data
|
|
88
|
+
},
|
|
89
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { register } from './index.mjs'
|
|
2
|
+
import { usageError } from '../errors.mjs'
|
|
3
|
+
import { flagString, flagNumber, flagBool, payloadFrom } from '../payload.mjs'
|
|
4
|
+
import { idParam } from '../path.mjs'
|
|
5
|
+
|
|
6
|
+
register('assistant search', {
|
|
7
|
+
summary: '面试官工作台:按学号/姓名搜索候选人',
|
|
8
|
+
usage: 'ch assistant search --keyword 张三',
|
|
9
|
+
endpoints: ['GET /admin/assistant/search'],
|
|
10
|
+
run: async (ctx) => {
|
|
11
|
+
const keyword = flagString(ctx, 'keyword', { required: true, label: 'ch assistant search --keyword 张三' })
|
|
12
|
+
const { data } = await ctx.client.request({
|
|
13
|
+
method: 'GET',
|
|
14
|
+
path: '/admin/assistant/search',
|
|
15
|
+
query: [['keyword', keyword]],
|
|
16
|
+
})
|
|
17
|
+
return data
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
register('assistant profile', {
|
|
22
|
+
summary: '面试官工作台:候选人一屏档案(含全部场次与历史评价)',
|
|
23
|
+
usage: 'ch assistant profile <userId>',
|
|
24
|
+
endpoints: ['GET /admin/assistant/candidates/{userId}'],
|
|
25
|
+
run: async (ctx) => {
|
|
26
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
27
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/admin/assistant/candidates/${userId}` })
|
|
28
|
+
return data
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
register('assistant submit', {
|
|
33
|
+
summary: '面试官工作台:现场打分提交(结果+评语必填)',
|
|
34
|
+
usage: 'ch assistant submit --interview-id N --result 1|2|3 --evaluation-content 评语 [--score 85] [--starred true|false]',
|
|
35
|
+
endpoints: ['POST /admin/assistant/submit'],
|
|
36
|
+
run: async (ctx) => {
|
|
37
|
+
const interviewId = flagNumber(ctx, 'interview-id', { required: true, label: 'ch assistant submit --interview-id N' })
|
|
38
|
+
const result = flagNumber(ctx, 'result', { required: true, label: 'ch assistant submit --result 1|2|3' })
|
|
39
|
+
if (![1, 2, 3].includes(result)) throw usageError('--result 必须是 1|2|3')
|
|
40
|
+
const evaluationContent = flagString(ctx, 'evaluation-content', { required: true, label: 'ch assistant submit --evaluation-content 评语' })
|
|
41
|
+
const base = { interviewId, result, evaluationContent }
|
|
42
|
+
const score = flagNumber(ctx, 'score')
|
|
43
|
+
if (score !== undefined) {
|
|
44
|
+
if (!Number.isInteger(score) || score < 0 || score > 100) throw usageError('--score 必须是 0-100 的整数')
|
|
45
|
+
base.score = score
|
|
46
|
+
}
|
|
47
|
+
const starred = flagBool(ctx, 'starred')
|
|
48
|
+
if (starred !== undefined) base.starred = starred
|
|
49
|
+
const payload = payloadFrom(ctx, base)
|
|
50
|
+
const { data } = await ctx.client.request({ method: 'POST', path: '/admin/assistant/submit', body: payload })
|
|
51
|
+
return data
|
|
52
|
+
},
|
|
53
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { register, getRegistry } from './index.mjs'
|
|
2
|
+
import { usageError, EXIT, CliError } from '../errors.mjs'
|
|
3
|
+
import { flagOn } from '../payload.mjs'
|
|
4
|
+
import * as sessionStore from '../session.mjs'
|
|
5
|
+
import { decodeTokenExpiry, sessionExpiryState, promptPassword, promptText } from '../prompt.mjs'
|
|
6
|
+
|
|
7
|
+
register('login', {
|
|
8
|
+
summary: '登录并保存会话(token 落盘 0600,含滚动续期自动换存)',
|
|
9
|
+
usage: 'ch login [--studentNo <学号>] [--password <密码>] [--profile local|prod] [--show-token]',
|
|
10
|
+
endpoints: ['POST /auth/login'],
|
|
11
|
+
auth: false,
|
|
12
|
+
run: async (ctx) => {
|
|
13
|
+
const flagStudentNo = typeof ctx.flags.get('studentNo') === 'string' ? ctx.flags.get('studentNo') : ''
|
|
14
|
+
const envStudentNo = typeof ctx.env.CH_STUDENT_NO === 'string' ? ctx.env.CH_STUDENT_NO.trim() : ''
|
|
15
|
+
let studentNo = (flagStudentNo || envStudentNo).trim()
|
|
16
|
+
if (!studentNo && process.stdin.isTTY) studentNo = (await promptText('学号:')).trim()
|
|
17
|
+
if (!studentNo) {
|
|
18
|
+
throw usageError('缺少学号', '用 --studentNo <学号> 或环境变量 CH_STUDENT_NO 提供')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let password = typeof ctx.flags.get('password') === 'string' ? ctx.flags.get('password') : ''
|
|
22
|
+
if (!password && typeof ctx.env.CH_PASSWORD === 'string') password = ctx.env.CH_PASSWORD
|
|
23
|
+
if (!password) {
|
|
24
|
+
if (process.stdin.isTTY) {
|
|
25
|
+
password = await promptPassword('密码(输入不回显):')
|
|
26
|
+
} else {
|
|
27
|
+
throw usageError('非交互环境缺少密码', '用 --password 或环境变量 CH_PASSWORD 提供(避免写入 shell 历史)')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!password) throw usageError('密码不能为空')
|
|
31
|
+
|
|
32
|
+
const { data } = await ctx.client.request({
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: '/auth/login',
|
|
35
|
+
body: { studentNo, password },
|
|
36
|
+
noAuth: true,
|
|
37
|
+
})
|
|
38
|
+
if (!data || typeof data.token !== 'string') {
|
|
39
|
+
throw new CliError({ code: 'LOGIN_FAILED', message: '登录响应缺少 token', exitCode: EXIT.BUSINESS })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const session = {
|
|
43
|
+
version: 1,
|
|
44
|
+
profile: ctx.config.profile,
|
|
45
|
+
baseUrl: ctx.config.baseUrl,
|
|
46
|
+
token: data.token,
|
|
47
|
+
userId: data.userId ?? null,
|
|
48
|
+
studentNo: data.studentNo ?? studentNo,
|
|
49
|
+
role: data.role ?? null,
|
|
50
|
+
expiresAt: decodeTokenExpiry(data.token),
|
|
51
|
+
savedAt: Date.now(),
|
|
52
|
+
}
|
|
53
|
+
const file = sessionStore.saveSession(ctx.env, session)
|
|
54
|
+
|
|
55
|
+
const result = {
|
|
56
|
+
loggedIn: true,
|
|
57
|
+
userId: session.userId,
|
|
58
|
+
studentNo: session.studentNo,
|
|
59
|
+
role: session.role,
|
|
60
|
+
tokenExpiresAt: session.expiresAt ? new Date(session.expiresAt).toISOString() : null,
|
|
61
|
+
profile: ctx.config.profile,
|
|
62
|
+
baseUrl: ctx.config.baseUrl,
|
|
63
|
+
sessionFile: file,
|
|
64
|
+
}
|
|
65
|
+
if (flagOn(ctx, 'show-token')) result.token = data.token
|
|
66
|
+
ctx.log(`会话已保存至 ${file}(权限 0600;续期由后端 X-Renewed-Token 自动换存)`)
|
|
67
|
+
return result
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
register('whoami', {
|
|
72
|
+
summary: '查看当前会话与管理员权限(adminLevel / authorizedGroups)',
|
|
73
|
+
usage: 'ch whoami',
|
|
74
|
+
endpoints: ['GET /admin/profile'],
|
|
75
|
+
run: async (ctx) => {
|
|
76
|
+
const { data: profile } = await ctx.client.request({ method: 'GET', path: '/admin/profile' })
|
|
77
|
+
const session = ctx.session || sessionStore.loadSession(ctx.env)
|
|
78
|
+
const expiresAt = typeof session?.expiresAt === 'number' ? session.expiresAt : decodeTokenExpiry(ctx.token)
|
|
79
|
+
const state = sessionExpiryState(expiresAt)
|
|
80
|
+
return {
|
|
81
|
+
session: {
|
|
82
|
+
userId: session?.userId ?? null,
|
|
83
|
+
studentNo: session?.studentNo ?? null,
|
|
84
|
+
role: session?.role ?? null,
|
|
85
|
+
profile: ctx.config.profile,
|
|
86
|
+
baseUrl: ctx.config.baseUrl,
|
|
87
|
+
tokenSource: ctx.env.CH_TOKEN ? 'CH_TOKEN 环境变量' : session ? '本地会话文件' : '无',
|
|
88
|
+
tokenExpiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
|
|
89
|
+
tokenState: state,
|
|
90
|
+
},
|
|
91
|
+
profile,
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
register('logout', {
|
|
97
|
+
summary: '删除本地会话文件(不影响服务端 token,直至自然过期)',
|
|
98
|
+
usage: 'ch logout',
|
|
99
|
+
endpoints: [],
|
|
100
|
+
auth: false,
|
|
101
|
+
localWrite: true,
|
|
102
|
+
run: async (ctx) => {
|
|
103
|
+
const cleared = sessionStore.clearSession(ctx.env)
|
|
104
|
+
return { loggedOut: true, sessionFileRemoved: cleared }
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const identity = getRegistry().get('whoami')
|
|
109
|
+
register('auth status', { ...identity, name: 'auth status', usage: 'ch auth status' })
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { register } from './index.mjs'
|
|
2
|
+
import { usageError } from '../errors.mjs'
|
|
3
|
+
import { collectQuery, parseNumber } from '../query.mjs'
|
|
4
|
+
import { flagEnum, flagString, flagNumber, flagBool, payloadFrom, parseBoolFlag, parseIdList } from '../payload.mjs'
|
|
5
|
+
import { requireReason } from '../sensitive.mjs'
|
|
6
|
+
import { idParam } from '../path.mjs'
|
|
7
|
+
import { runWithVersionRetry } from '../idempotent.mjs'
|
|
8
|
+
|
|
9
|
+
register('candidates list', {
|
|
10
|
+
summary: '候选人分页列表(后端过滤分页;全部筛选 AND)',
|
|
11
|
+
usage: 'ch candidates list [--page N] [--size N] [--keyword 关键词] [--status 1|2|3] [--second-round-group-id N] [--third-round-group-id N] [--third-round-choice-state ALL|SELECTED|UNSELECTED] [--starred true|false] [--starred-admin-id N] [--starred-by-me true|false]',
|
|
12
|
+
endpoints: ['GET /admin/users/info/page'],
|
|
13
|
+
run: async (ctx) => {
|
|
14
|
+
const query = collectQuery(ctx, {
|
|
15
|
+
page: { parse: parseNumber },
|
|
16
|
+
size: { parse: parseNumber },
|
|
17
|
+
keyword: {},
|
|
18
|
+
status: { parse: parseNumber },
|
|
19
|
+
'second-round-group-id': { key: 'secondRoundGroupId', parse: parseNumber },
|
|
20
|
+
'third-round-group-id': { key: 'thirdRoundGroupId', parse: parseNumber },
|
|
21
|
+
'third-round-choice-state': { key: 'thirdRoundChoiceState' },
|
|
22
|
+
starred: { parse: (v) => String(parseBoolFlag(v, { flag: 'starred' })) },
|
|
23
|
+
'starred-admin-id': { key: 'starredAdminId', parse: parseNumber },
|
|
24
|
+
'starred-by-me': { key: 'starredByMe', parse: (v) => String(parseBoolFlag(v, { flag: 'starred-by-me' })) },
|
|
25
|
+
})
|
|
26
|
+
const queryMap = new Map(query)
|
|
27
|
+
if (!queryMap.has('page')) query.push(['page', 1])
|
|
28
|
+
if (!queryMap.has('size')) query.push(['size', 20])
|
|
29
|
+
if (!queryMap.has('thirdRoundChoiceState')) query.push(['thirdRoundChoiceState', 'ALL'])
|
|
30
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/users/info/page', query })
|
|
31
|
+
return data
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
register('candidates list-all', {
|
|
36
|
+
summary: '候选人全量列表(旧接口,后端最多返回 200 条)',
|
|
37
|
+
usage: 'ch candidates list-all [--pretty]',
|
|
38
|
+
endpoints: ['GET /admin/users/info'],
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/users/info' })
|
|
41
|
+
return data
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
register('candidates get', {
|
|
46
|
+
summary: '候选人档案详情',
|
|
47
|
+
usage: 'ch candidates get <userId>',
|
|
48
|
+
endpoints: ['GET /users/{userId}/info'],
|
|
49
|
+
run: async (ctx) => {
|
|
50
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
51
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/users/${userId}/info` })
|
|
52
|
+
return data
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
register('candidates application', {
|
|
57
|
+
summary: '候选人报名记录',
|
|
58
|
+
usage: 'ch candidates application <userId>',
|
|
59
|
+
endpoints: ['GET /users/{userId}/application'],
|
|
60
|
+
run: async (ctx) => {
|
|
61
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
62
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/users/${userId}/application` })
|
|
63
|
+
return data
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
register('candidates interviews', {
|
|
68
|
+
summary: '候选人的全部面试记录',
|
|
69
|
+
usage: 'ch candidates interviews <userId>',
|
|
70
|
+
endpoints: ['GET /users/{userId}/interviews'],
|
|
71
|
+
run: async (ctx) => {
|
|
72
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
73
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/users/${userId}/interviews` })
|
|
74
|
+
return data
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
register('candidates evaluations', {
|
|
79
|
+
summary: '候选人的全部面评',
|
|
80
|
+
usage: 'ch candidates evaluations <userId>',
|
|
81
|
+
endpoints: ['GET /admin/candidates/{userId}/evaluations'],
|
|
82
|
+
run: async (ctx) => {
|
|
83
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
84
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/admin/candidates/${userId}/evaluations` })
|
|
85
|
+
return data
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
register('candidates starred-admins', {
|
|
90
|
+
summary: '特别关注面试官列表(含打星统计)',
|
|
91
|
+
usage: 'ch candidates starred-admins [--pretty]',
|
|
92
|
+
endpoints: ['GET /admin/candidates/starred-admins'],
|
|
93
|
+
run: async (ctx) => {
|
|
94
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/candidates/starred-admins' })
|
|
95
|
+
return data
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
register('candidates second-round-groups', {
|
|
100
|
+
summary: '候选人二轮自选组别',
|
|
101
|
+
usage: 'ch candidates second-round-groups <userId>',
|
|
102
|
+
endpoints: ['GET /users/{userId}/second-round-groups'],
|
|
103
|
+
run: async (ctx) => {
|
|
104
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
105
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/users/${userId}/second-round-groups` })
|
|
106
|
+
return data
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
register('candidates third-round-choice', {
|
|
111
|
+
summary: '候选人三面选组上下文',
|
|
112
|
+
usage: 'ch candidates third-round-choice <userId>',
|
|
113
|
+
endpoints: ['GET /admin/users/{userId}/third-round-choice'],
|
|
114
|
+
run: async (ctx) => {
|
|
115
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
116
|
+
const { data } = await ctx.client.request({ method: 'GET', path: `/admin/users/${userId}/third-round-choice` })
|
|
117
|
+
return data
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
register('applications list', {
|
|
122
|
+
summary: '报名记录分页列表',
|
|
123
|
+
usage: 'ch applications list [--page N] [--size N]',
|
|
124
|
+
endpoints: ['GET /admin/applications/page'],
|
|
125
|
+
run: async (ctx) => {
|
|
126
|
+
const query = collectQuery(ctx, { page: { parse: parseNumber }, size: { parse: parseNumber } })
|
|
127
|
+
const queryMap = new Map(query)
|
|
128
|
+
if (!queryMap.has('page')) query.push(['page', 1])
|
|
129
|
+
if (!queryMap.has('size')) query.push(['size', 200])
|
|
130
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/applications/page', query })
|
|
131
|
+
return data
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
register('applications list-all', {
|
|
136
|
+
summary: '报名记录全量列表(旧接口,后端最多返回 200 条)',
|
|
137
|
+
usage: 'ch applications list-all [--pretty]',
|
|
138
|
+
endpoints: ['GET /admin/applications'],
|
|
139
|
+
run: async (ctx) => {
|
|
140
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/applications' })
|
|
141
|
+
return data
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
register('second-round-choices', {
|
|
146
|
+
summary: '二轮考核组别自选全量列表',
|
|
147
|
+
usage: 'ch second-round-choices [--pretty]',
|
|
148
|
+
endpoints: ['GET /admin/second-round-choices'],
|
|
149
|
+
run: async (ctx) => {
|
|
150
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/second-round-choices' })
|
|
151
|
+
return data
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
register('candidates set-third-round-choice', {
|
|
156
|
+
summary: '修改候选人三面最终组别(原因必填;version 自动重读重试)',
|
|
157
|
+
usage: 'ch candidates set-third-round-choice <userId> --group-id N --reason 原因 [--version N]',
|
|
158
|
+
endpoints: ['PUT /admin/users/{userId}/third-round-choice'],
|
|
159
|
+
run: async (ctx) => {
|
|
160
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
161
|
+
const groupId = flagNumber(ctx, 'group-id', { required: true, label: '--group-id N' })
|
|
162
|
+
const reason = requireReason(ctx, { label: '--reason 修改原因(≤200 字)' })
|
|
163
|
+
const explicitVersion = flagNumber(ctx, 'version')
|
|
164
|
+
const path = `/admin/users/${userId}/third-round-choice`
|
|
165
|
+
|
|
166
|
+
return runWithVersionRetry({
|
|
167
|
+
explicitVersion,
|
|
168
|
+
readVersion: async () => {
|
|
169
|
+
const { data } = await ctx.client.request({ method: 'GET', path })
|
|
170
|
+
if (typeof data?.version !== 'number') {
|
|
171
|
+
throw usageError('选组上下文中没有可用 version(候选人可能尚未进入三面阶段)')
|
|
172
|
+
}
|
|
173
|
+
return data.version
|
|
174
|
+
},
|
|
175
|
+
log: (message) => ctx.log(message),
|
|
176
|
+
run: async ({ version }) => {
|
|
177
|
+
const payload = payloadFrom(ctx, { groupId, reason, version })
|
|
178
|
+
const { data } = await ctx.client.request({ method: 'PUT', path, body: payload })
|
|
179
|
+
return data
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
register('candidates star', {
|
|
186
|
+
summary: '切换对候选人的特别关注标记',
|
|
187
|
+
usage: 'ch candidates star <userId> --starred true|false',
|
|
188
|
+
endpoints: ['POST /admin/candidates/{userId}/star'],
|
|
189
|
+
run: async (ctx) => {
|
|
190
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
191
|
+
const starred = flagBool(ctx, 'starred')
|
|
192
|
+
if (starred === undefined) throw usageError('缺少 --starred', '用法:ch candidates star <userId> --starred true|false')
|
|
193
|
+
const payload = payloadFrom(ctx, { starred })
|
|
194
|
+
const { data } = await ctx.client.request({ method: 'POST', path: `/admin/candidates/${userId}/star`, body: payload })
|
|
195
|
+
return data
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
register('candidates update', {
|
|
200
|
+
summary: '手动调整成员资料(仅更新传入字段)',
|
|
201
|
+
usage: 'ch candidates update <userId> -d \'{"realName":"张三","phone":"138..."}\'',
|
|
202
|
+
endpoints: ['PUT /users/{userId}/info'],
|
|
203
|
+
run: async (ctx) => {
|
|
204
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
205
|
+
const payload = payloadFrom(ctx, {})
|
|
206
|
+
if (Object.keys(payload).length === 0) throw usageError('缺少更新字段', '用 -d \'{"字段":"值"}\' 提供')
|
|
207
|
+
const { data } = await ctx.client.request({ method: 'PUT', path: `/users/${userId}/info`, body: payload })
|
|
208
|
+
return data === null ? { updated: true, userId: Number(userId) } : data
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
register('candidates application-status', {
|
|
213
|
+
summary: '调整报名状态(仅未产生面试记录时可用)',
|
|
214
|
+
usage: 'ch candidates application-status <userId> --status 1|2|3',
|
|
215
|
+
endpoints: ['PATCH /admin/applications/{userId}/status'],
|
|
216
|
+
run: async (ctx) => {
|
|
217
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
218
|
+
const status = flagEnum(ctx, 'status', ['1', '2', '3'], { required: true, label: '--status 1|2|3' })
|
|
219
|
+
const payload = payloadFrom(ctx, { status: Number(status) })
|
|
220
|
+
const { data } = await ctx.client.request({ method: 'PATCH', path: `/admin/applications/${userId}/status`, body: payload })
|
|
221
|
+
return data === null ? { updated: true, userId: Number(userId) } : data
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
register('candidates set-second-round-groups', {
|
|
226
|
+
summary: '设置候选人二轮考核组别',
|
|
227
|
+
usage: 'ch candidates set-second-round-groups <userId> --group-ids 1,2',
|
|
228
|
+
endpoints: ['PUT /users/{userId}/second-round-groups'],
|
|
229
|
+
run: async (ctx) => {
|
|
230
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
231
|
+
const base = payloadFrom(ctx, {})
|
|
232
|
+
if (base.groupIds === undefined) {
|
|
233
|
+
const raw = flagString(ctx, 'group-ids', { required: true, label: '--group-ids 1,2(或用 -d \'{"groupIds":[1,2]}\')' })
|
|
234
|
+
base.groupIds = parseIdList(raw, { flag: 'group-ids' })
|
|
235
|
+
}
|
|
236
|
+
const { data } = await ctx.client.request({ method: 'PUT', path: `/users/${userId}/second-round-groups`, body: base })
|
|
237
|
+
return data === null ? { updated: true, userId: Number(userId) } : data
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
register('candidates unbind-wechat', {
|
|
242
|
+
summary: '解除候选人微信绑定(原因必填;后端同时失效其会话)',
|
|
243
|
+
usage: 'ch candidates unbind-wechat <userId> --reason 原因',
|
|
244
|
+
endpoints: ['DELETE /admin/users/{userId}/wechat-binding'],
|
|
245
|
+
run: async (ctx) => {
|
|
246
|
+
const userId = idParam(ctx.positionals[0], { label: 'userId' })
|
|
247
|
+
const reason = requireReason(ctx, { label: '--reason 解绑原因(≤200 字)' })
|
|
248
|
+
const payload = payloadFrom(ctx, { reason })
|
|
249
|
+
const { data } = await ctx.client.request({ method: 'DELETE', path: `/admin/users/${userId}/wechat-binding`, body: payload })
|
|
250
|
+
return data === null ? { unbound: true, userId: Number(userId) } : data
|
|
251
|
+
},
|
|
252
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { register } from './index.mjs'
|
|
2
|
+
import { usageError } from '../errors.mjs'
|
|
3
|
+
import { toArray } from '../argv.mjs'
|
|
4
|
+
import { flagString } from '../payload.mjs'
|
|
5
|
+
|
|
6
|
+
register('checkin launch', {
|
|
7
|
+
summary: '创建签到大屏会话(8 小时有效,screenToken 仅经 header 传递,不落盘)',
|
|
8
|
+
usage: 'ch checkin launch',
|
|
9
|
+
endpoints: ['POST /admin/checkin-screens'],
|
|
10
|
+
run: async (ctx) => {
|
|
11
|
+
const { data } = await ctx.client.request({ method: 'POST', path: '/admin/checkin-screens' })
|
|
12
|
+
return data
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
register('checkin snapshot', {
|
|
17
|
+
summary: '读取签到大屏当前快照(screenToken 经 X-Checkin-Screen-Token header,绝不携带管理员 JWT)',
|
|
18
|
+
usage: 'ch checkin snapshot --screen-token <token> [--group-id N ...]',
|
|
19
|
+
endpoints: ['GET /checkin-screens/current'],
|
|
20
|
+
auth: false,
|
|
21
|
+
run: async (ctx) => {
|
|
22
|
+
const screenToken =
|
|
23
|
+
flagString(ctx, 'screen-token') ||
|
|
24
|
+
(typeof ctx.env.CH_CHECKIN_SCREEN_TOKEN === 'string' ? ctx.env.CH_CHECKIN_SCREEN_TOKEN.trim() : '')
|
|
25
|
+
if (!screenToken) {
|
|
26
|
+
throw usageError('缺少 --screen-token', '大屏 token 来自 ch checkin launch 的响应;仅经 header 传递,不写入任何持久化文件')
|
|
27
|
+
}
|
|
28
|
+
const query = []
|
|
29
|
+
for (const entry of toArray(ctx.multi.get('group-id'))) {
|
|
30
|
+
query.push(['groupId', String(entry)])
|
|
31
|
+
}
|
|
32
|
+
const { data } = await ctx.client.request({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
path: '/checkin-screens/current',
|
|
35
|
+
query,
|
|
36
|
+
headers: { 'X-Checkin-Screen-Token': screenToken },
|
|
37
|
+
noAuth: true,
|
|
38
|
+
})
|
|
39
|
+
return data
|
|
40
|
+
},
|
|
41
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { register } from './index.mjs'
|
|
2
|
+
|
|
3
|
+
register('dashboard', {
|
|
4
|
+
summary: '招新总览统计(报名/录取/待处理/场次占用)',
|
|
5
|
+
usage: 'ch dashboard [--pretty]',
|
|
6
|
+
endpoints: ['GET /admin/dashboard'],
|
|
7
|
+
run: async (ctx) => {
|
|
8
|
+
const { data } = await ctx.client.request({ method: 'GET', path: '/admin/dashboard' })
|
|
9
|
+
return data
|
|
10
|
+
},
|
|
11
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { register } from './index.mjs'
|
|
2
|
+
import { usageError } from '../errors.mjs'
|
|
3
|
+
import { flagString, flagNumber, payloadFrom } from '../payload.mjs'
|
|
4
|
+
import { idParam } from '../path.mjs'
|
|
5
|
+
|
|
6
|
+
register('evaluations add', {
|
|
7
|
+
summary: '提交面试评价(评语必填,评分 0-100 整数选填)',
|
|
8
|
+
usage: 'ch evaluations add <interviewId> --content 评语 [--score 85]',
|
|
9
|
+
endpoints: ['POST /admin/interviews/{interviewId}/evaluations'],
|
|
10
|
+
run: async (ctx) => {
|
|
11
|
+
const interviewId = idParam(ctx.positionals[0], { label: 'interviewId' })
|
|
12
|
+
const content = flagString(ctx, 'content', { required: true, label: 'ch evaluations add <interviewId> --content 评语' })
|
|
13
|
+
const score = flagNumber(ctx, 'score')
|
|
14
|
+
if (score !== undefined && (!Number.isInteger(score) || score < 0 || score > 100)) {
|
|
15
|
+
throw usageError('--score 必须是 0-100 的整数')
|
|
16
|
+
}
|
|
17
|
+
const base = { content }
|
|
18
|
+
if (score !== undefined) base.score = score
|
|
19
|
+
const payload = payloadFrom(ctx, base)
|
|
20
|
+
const { data } = await ctx.client.request({
|
|
21
|
+
method: 'POST',
|
|
22
|
+
path: `/admin/interviews/${interviewId}/evaluations`,
|
|
23
|
+
body: payload,
|
|
24
|
+
})
|
|
25
|
+
return data
|
|
26
|
+
},
|
|
27
|
+
})
|