@xinhehuimeng/cli 0.2.4 → 0.2.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/README.md +19 -0
- package/package.json +1 -1
- package/src/commands/index.js +4 -2
- package/src/commands/member.js +69 -1
- package/src/commands/project.js +685 -0
- package/src/commands/session.js +59 -6
- package/src/spec/command_surface.json +311 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,25 @@ npx @xinhehuimeng/cli login --phone 13800000000 --code 123456
|
|
|
13
13
|
npx @xinhehuimeng/cli me --json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
管理员为普通成员发放 30 分钟临时口令后,可以改用自己的长期口令:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1) 用临时口令登录
|
|
20
|
+
printf '%s' "$TEMP_PW" | xinhe-oa-cli login --password --account <手机号>
|
|
21
|
+
|
|
22
|
+
# 2) 首次登录必须改密;改密会吊销该账号全部旧会话
|
|
23
|
+
printf '%s\n%s\n%s\n' "$TEMP_PW" "$NEW_PW" "$NEW_PW" | \
|
|
24
|
+
xinhe-oa-cli login --change-password
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
管理员发放临时口令:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
xinhe-oa-cli member --reset-password <USER_ID> --yes --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
临时口令只显示一次,不要发到工单评论或群聊。
|
|
34
|
+
|
|
16
35
|
装一次就够(`npm i -g @xinhehuimeng/cli` 之后直接用 `xinhe-oa-cli`):
|
|
17
36
|
|
|
18
37
|
```bash
|
package/package.json
CHANGED
package/src/commands/index.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// 自己负责把结果写出去(`runtime.finish(...)` / `runtime.finishWithHuman(...)`),
|
|
5
5
|
// 失败就 `throw CliError`。**退出码由 `main.js` 统一收口**(契约 §3.2)。
|
|
6
6
|
//
|
|
7
|
-
// 为什么一张显式的表,而不是按域名动态 import:命令面是**冻结契约**(
|
|
7
|
+
// 为什么一张显式的表,而不是按域名动态 import:命令面是**冻结契约**(160 条),
|
|
8
8
|
// 表里缺一条就是"这条命令在 Node 版上不存在"——显式的表能让这件事一眼可见,
|
|
9
|
-
// 也方便 `test/surface.test.js` 断言"
|
|
9
|
+
// 也方便 `test/surface.test.js` 断言"160 条命令每条都有处理函数"。
|
|
10
10
|
|
|
11
11
|
import { handleLogin, handleLogout, handleMe } from './session.js';
|
|
12
12
|
import { handleTenant } from './tenant.js';
|
|
@@ -14,6 +14,7 @@ import { handleMember } from './member.js';
|
|
|
14
14
|
import { handleInvestor } from './investor.js';
|
|
15
15
|
import { handleOrg } from './org.js';
|
|
16
16
|
import { handleBitable } from './bitable.js';
|
|
17
|
+
import { handleProject } from './project.js';
|
|
17
18
|
import { handleApplication } from './application.js';
|
|
18
19
|
import { handleDashboard } from './dashboard.js';
|
|
19
20
|
import { handleWorkflow } from './workflow.js';
|
|
@@ -42,6 +43,7 @@ export const HANDLERS = new Map([
|
|
|
42
43
|
['bitable record', handleBitable],
|
|
43
44
|
['bitable view', handleBitable],
|
|
44
45
|
['bitable form', handleBitable],
|
|
46
|
+
['project', handleProject],
|
|
45
47
|
['application', handleApplication],
|
|
46
48
|
['dashboard', handleDashboard],
|
|
47
49
|
['workflow', handleWorkflow],
|
package/src/commands/member.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 成员域:`member --list|--search|--create|--role|--revoke-role|--remove
|
|
1
|
+
// 成员域:`member --list|--search|--create|--role|--revoke-role|--remove|--reset-password|--link`,
|
|
2
2
|
// 含 ORG-001 的部门归属三件套(`--set-dept` / `--unset-dept` / `--set-manager|--clear-manager`)、
|
|
3
3
|
// ORG-002 的授权三动作(`--grant|--revoke-grant|--grants`,issue #82)、
|
|
4
4
|
// ORG-004 的离职 / 移交四动作(issue #81)与 MEMBER-001 的 `--search`(issue #99)。
|
|
@@ -81,6 +81,10 @@ export async function handleMember({ args, runtime }) {
|
|
|
81
81
|
return revokeRole(runtime, args, action.role, action.user);
|
|
82
82
|
case 'remove':
|
|
83
83
|
return removeMember(runtime, action.user, args);
|
|
84
|
+
case 'reset-password':
|
|
85
|
+
return resetPassword(runtime, action.user);
|
|
86
|
+
case 'link':
|
|
87
|
+
return portalLink(runtime, args, action.user);
|
|
84
88
|
case 'set-dept':
|
|
85
89
|
return setDept(runtime, args, action.user, action.deptId, action.primary);
|
|
86
90
|
case 'unset-dept':
|
|
@@ -129,6 +133,8 @@ function memberAction(args) {
|
|
|
129
133
|
['--role', given('--role') && !on('--create')],
|
|
130
134
|
['--revoke-role', given('--revoke-role')],
|
|
131
135
|
['--remove', given('--remove')],
|
|
136
|
+
['--reset-password', given('--reset-password')],
|
|
137
|
+
['--link', given('--link')],
|
|
132
138
|
['--set-dept', given('--set-dept')],
|
|
133
139
|
['--unset-dept', given('--unset-dept')],
|
|
134
140
|
['--set-manager', given('--set-manager')],
|
|
@@ -396,11 +402,47 @@ function memberAction(args) {
|
|
|
396
402
|
}
|
|
397
403
|
return { kind: 'transfer-revert', transferId, note: String(note) };
|
|
398
404
|
}
|
|
405
|
+
case '--reset-password':
|
|
406
|
+
return {
|
|
407
|
+
kind: 'reset-password',
|
|
408
|
+
user: requiredId(
|
|
409
|
+
val('--reset-password'),
|
|
410
|
+
'--reset-password',
|
|
411
|
+
'例:xinhe-oa-cli member --reset-password usr_123 --yes --json',
|
|
412
|
+
),
|
|
413
|
+
};
|
|
414
|
+
case '--link': {
|
|
415
|
+
const user = normalizeId(val('--link'));
|
|
416
|
+
if (user === null) {
|
|
417
|
+
throw CliError.usage('`member --link` 需要 --link <USER_ID>').withHint(
|
|
418
|
+
'先看 user_id:xinhe-oa-cli member --list --json',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return { kind: 'link', user };
|
|
422
|
+
}
|
|
399
423
|
default:
|
|
400
424
|
return { kind: 'remove', user: String(val('--remove') ?? '') };
|
|
401
425
|
}
|
|
402
426
|
}
|
|
403
427
|
|
|
428
|
+
async function portalLink(runtime, args, userId) {
|
|
429
|
+
const tenant = await resolveTenant(runtime, flagValue(args, '--tenant'));
|
|
430
|
+
if (tenant === null) {
|
|
431
|
+
throw CliError.usage('签发门户链接必须指明企业').withHint(
|
|
432
|
+
'加 --tenant <TENANT_ID>;用 `xinhe-oa-cli tenant --list` 查看',
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const spec = requestSpec({
|
|
436
|
+
body: { tenant_id: tenant, user_id: userId },
|
|
437
|
+
});
|
|
438
|
+
if (runtime.dryRun) return runtime.dryRunReport('POST', '/portal/auth/links', spec);
|
|
439
|
+
const response = await runtime.sendAuthed('POST', '/portal/auth/links', spec);
|
|
440
|
+
runtime.finishFrom(response, {
|
|
441
|
+
...dataOf(response),
|
|
442
|
+
next_step: '把 url 发给对应成员;链接 10 分钟内有效且只能使用一次',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
404
446
|
/** 0 个或多于 1 个动作都是用法错误(与 Rust `exactly_one` 同文案)。 */
|
|
405
447
|
function exactlyOne(selected, what, hint) {
|
|
406
448
|
const picked = selected.filter(([, active]) => active).map(([name]) => name);
|
|
@@ -779,6 +821,32 @@ async function removeMember(runtime, userId, args) {
|
|
|
779
821
|
});
|
|
780
822
|
}
|
|
781
823
|
|
|
824
|
+
/** 管理员发放 30 分钟一次性口令,并吊销该成员全部旧会话。 */
|
|
825
|
+
async function resetPassword(runtime, userId) {
|
|
826
|
+
runtime.requireYes(
|
|
827
|
+
'重置成员口令',
|
|
828
|
+
`xinhe-oa-cli member --reset-password ${userId}`,
|
|
829
|
+
);
|
|
830
|
+
const path = `/admin/users/${userId}/password/reset`;
|
|
831
|
+
const spec = requestSpec();
|
|
832
|
+
if (runtime.dryRun) return runtime.dryRunReport('POST', path, spec);
|
|
833
|
+
const response = await runtime.sendAuthed('POST', path, spec);
|
|
834
|
+
const data = dataOf(response);
|
|
835
|
+
runtime.finishFrom(response, {
|
|
836
|
+
user: data.user ?? null,
|
|
837
|
+
temporary_password: data.temporary_password ?? null,
|
|
838
|
+
expires_at: data.expires_at ?? null,
|
|
839
|
+
sessions_revoked: data.sessions_revoked ?? null,
|
|
840
|
+
must_change_password: data.must_change_password ?? null,
|
|
841
|
+
next_steps: [
|
|
842
|
+
'通过安全渠道把临时口令单独交给本人,不要发群聊或工单评论',
|
|
843
|
+
"本人登录:printf '%s' \"$TEMP_PW\" | xinhe-oa-cli login --password --account <手机号>",
|
|
844
|
+
"本人立即改密:printf '%s\\n%s\\n%s\\n' \"$TEMP_PW\" \"$NEW_PW\" \"$NEW_PW\" | xinhe-oa-cli login --change-password",
|
|
845
|
+
],
|
|
846
|
+
warning: '旧口令与该成员的全部旧会话已失效;临时口令只显示这一次。',
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
782
850
|
// ---------------------------------------------------------------------------
|
|
783
851
|
// 部门归属(ORG-001):set-dept / unset-dept / set-manager
|
|
784
852
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// 项目应用适配域:`project --list|--show|--create|--update|--context`
|
|
2
|
+
// 与 `project milestone|task|progress`。
|
|
3
|
+
//
|
|
4
|
+
// 项目不是平台核心的新固定业务表:这里只把项目语义翻译到已安装的
|
|
5
|
+
// `project_delivery` Manifest,最终仍走 `/tables/{id}/records` 的同一条
|
|
6
|
+
// 幂等、乐观锁、权限和审计路径。Web 门户读取的也是这些记录。
|
|
7
|
+
|
|
8
|
+
import { CliError, Kind, exactlyOne } from '../core/error.js';
|
|
9
|
+
import { flagList, flagOn, flagValue, intFlag } from '../core/args.js';
|
|
10
|
+
import { dataOf, idempotencyKey, requestSpec } from '../core/runtime.js';
|
|
11
|
+
import { parseSetPairs, valuesObject } from '../core/values.js';
|
|
12
|
+
import { resolveTenant } from './tenant.js';
|
|
13
|
+
|
|
14
|
+
const APP_DEFAULT = 'project_delivery';
|
|
15
|
+
const MAX_PAGE = 1000;
|
|
16
|
+
const MAX_PAGES = 100;
|
|
17
|
+
|
|
18
|
+
const PROJECT_STATUS = ['planning', 'in_progress', 'acceptance', 'done', 'paused'];
|
|
19
|
+
const HEALTH = ['on_track', 'attention', 'at_risk', 'delayed'];
|
|
20
|
+
const MILESTONE_STATUS = ['upcoming', 'in_progress', 'completed', 'overdue'];
|
|
21
|
+
const TASK_STATUS = ['todo', 'in_progress', 'review', 'done', 'blocked'];
|
|
22
|
+
const PRIORITIES = ['high', 'medium', 'low'];
|
|
23
|
+
|
|
24
|
+
export async function handleProject({ args, runtime, command }) {
|
|
25
|
+
const resource = String(command?.name ?? 'project').split(' ')[1] ?? null;
|
|
26
|
+
if (resource === 'milestone') return milestone(runtime, args);
|
|
27
|
+
if (resource === 'task') return task(runtime, args);
|
|
28
|
+
if (resource === 'progress') return progress(runtime, args);
|
|
29
|
+
|
|
30
|
+
const action = exactlyOne(
|
|
31
|
+
[
|
|
32
|
+
['--list', flagOn(args, '--list')],
|
|
33
|
+
['--show', flagValue(args, '--show') !== null],
|
|
34
|
+
['--create', flagOn(args, '--create')],
|
|
35
|
+
['--update', flagValue(args, '--update') !== null],
|
|
36
|
+
['--context', flagOn(args, '--context')],
|
|
37
|
+
],
|
|
38
|
+
'project',
|
|
39
|
+
'例:xinhe-oa-cli project --list --tenant <TENANT_ID> --json',
|
|
40
|
+
);
|
|
41
|
+
if (action === '--list') return list(runtime, args);
|
|
42
|
+
if (action === '--show') return show(runtime, args, required(args, '--show'));
|
|
43
|
+
if (action === '--create') return create(runtime, args);
|
|
44
|
+
if (action === '--update') return update(runtime, args, required(args, '--update'));
|
|
45
|
+
return context(runtime, args);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function list(runtime, args) {
|
|
49
|
+
const payload = {
|
|
50
|
+
all: flagOn(args, '--all'),
|
|
51
|
+
health: flagValue(args, '--health'),
|
|
52
|
+
limit: intFlag(args, '--limit', { fallback: 200 }),
|
|
53
|
+
offset: intFlag(args, '--offset', { fallback: 0 }),
|
|
54
|
+
owner: flagValue(args, '--owner'),
|
|
55
|
+
status: flagValue(args, '--status'),
|
|
56
|
+
};
|
|
57
|
+
if (runtime.dryRun) return dryRun(runtime, 'project.list', args, payload);
|
|
58
|
+
|
|
59
|
+
const binding = await resolveBinding(runtime, args);
|
|
60
|
+
const needAll =
|
|
61
|
+
payload.all || payload.status !== null || payload.health !== null || payload.owner !== null;
|
|
62
|
+
const { records, response } = await fetchRecords(
|
|
63
|
+
runtime,
|
|
64
|
+
table(binding, 'project'),
|
|
65
|
+
payload.limit,
|
|
66
|
+
payload.offset,
|
|
67
|
+
needAll,
|
|
68
|
+
);
|
|
69
|
+
const projects = records.filter(
|
|
70
|
+
(record) =>
|
|
71
|
+
(payload.status === null || recordValue(record, 'project_status') === payload.status) &&
|
|
72
|
+
(payload.health === null || recordValue(record, 'health_status') === payload.health) &&
|
|
73
|
+
(payload.owner === null || recordValue(record, 'project_owner') === payload.owner),
|
|
74
|
+
);
|
|
75
|
+
runtime.finishFrom(response, {
|
|
76
|
+
binding,
|
|
77
|
+
count: projects.length,
|
|
78
|
+
projects,
|
|
79
|
+
next_step:
|
|
80
|
+
'看详情:xinhe-oa-cli project --show <PROJECT_ID|CODE> --tenant <TENANT_ID> --json',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function show(runtime, args, projectRef) {
|
|
85
|
+
if (runtime.dryRun) return dryRun(runtime, 'project.show', args, { project: projectRef });
|
|
86
|
+
const binding = await resolveBinding(runtime, args);
|
|
87
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
88
|
+
const response = await runtime.sendAuthed('GET', `/records/${projectId}`, requestSpec());
|
|
89
|
+
runtime.finishFrom(response, { binding, project: dataOf(response) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function context(runtime, args) {
|
|
93
|
+
if (runtime.dryRun) return dryRun(runtime, 'project.context', args, {});
|
|
94
|
+
const binding = await resolveBinding(runtime, args);
|
|
95
|
+
runtime.finish({
|
|
96
|
+
binding,
|
|
97
|
+
next_step: 'xinhe-oa-cli project --list --tenant <TENANT_ID> --json',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function create(runtime, args) {
|
|
102
|
+
const name = required(args, '--name');
|
|
103
|
+
const owner = required(args, '--owner');
|
|
104
|
+
const due = required(args, '--due');
|
|
105
|
+
const code = flagValue(args, '--code') ?? `PRJ-${timestamp()}`;
|
|
106
|
+
const status = flagValue(args, '--status') ?? 'planning';
|
|
107
|
+
const health = flagValue(args, '--health') ?? 'on_track';
|
|
108
|
+
choice(status, PROJECT_STATUS, '--status');
|
|
109
|
+
choice(health, HEALTH, '--health');
|
|
110
|
+
|
|
111
|
+
const pairs = [
|
|
112
|
+
textPair('project_name', name),
|
|
113
|
+
textPair('project_code', code),
|
|
114
|
+
textPair('project_status', status),
|
|
115
|
+
textPair('health_status', health),
|
|
116
|
+
textPair('project_owner', owner),
|
|
117
|
+
textPair('target_date', due),
|
|
118
|
+
numberPair('progress_percent', percentValue(args, 0)),
|
|
119
|
+
textPair('last_update_at', today()),
|
|
120
|
+
];
|
|
121
|
+
addText(pairs, args, '--start', 'start_date');
|
|
122
|
+
addText(pairs, args, '--description', 'project_description');
|
|
123
|
+
addText(pairs, args, '--current-milestone', 'current_milestone');
|
|
124
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
125
|
+
|
|
126
|
+
if (runtime.dryRun) {
|
|
127
|
+
return dryRun(runtime, 'project.create', args, {
|
|
128
|
+
reason: flagValue(args, '--reason'),
|
|
129
|
+
table: 'project',
|
|
130
|
+
values: valuesObject(pairs),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const binding = await resolveBinding(runtime, args);
|
|
134
|
+
await ensureCodeAvailable(runtime, binding, code, null);
|
|
135
|
+
return createRecord(runtime, table(binding, 'project'), pairs, args, '项目立项');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function update(runtime, args, projectRef) {
|
|
139
|
+
const pairs = [];
|
|
140
|
+
addText(pairs, args, '--name', 'project_name');
|
|
141
|
+
addText(pairs, args, '--code', 'project_code');
|
|
142
|
+
addText(pairs, args, '--owner', 'project_owner');
|
|
143
|
+
addText(pairs, args, '--start', 'start_date');
|
|
144
|
+
addText(pairs, args, '--due', 'target_date');
|
|
145
|
+
|
|
146
|
+
const status = flagValue(args, '--status');
|
|
147
|
+
if (status !== null) {
|
|
148
|
+
choice(status, PROJECT_STATUS, '--status');
|
|
149
|
+
pairs.push(textPair('project_status', status));
|
|
150
|
+
}
|
|
151
|
+
const health = flagValue(args, '--health');
|
|
152
|
+
if (health !== null) {
|
|
153
|
+
choice(health, HEALTH, '--health');
|
|
154
|
+
pairs.push(textPair('health_status', health));
|
|
155
|
+
}
|
|
156
|
+
const progress = flagValue(args, '--progress');
|
|
157
|
+
if (progress !== null) pairs.push(numberPair('progress_percent', percent(progress, '--progress')));
|
|
158
|
+
addText(pairs, args, '--current-milestone', 'current_milestone');
|
|
159
|
+
addText(pairs, args, '--description', 'project_description');
|
|
160
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
161
|
+
if (pairs.length === 0) throw CliError.usage('`project --update` 至少要给一个更新字段或 --set');
|
|
162
|
+
pairs.push(textPair('last_update_at', today()));
|
|
163
|
+
|
|
164
|
+
if (runtime.dryRun) {
|
|
165
|
+
return dryRun(runtime, 'project.update', args, {
|
|
166
|
+
project: projectRef,
|
|
167
|
+
reason: flagValue(args, '--reason'),
|
|
168
|
+
table: 'project',
|
|
169
|
+
values: valuesObject(pairs),
|
|
170
|
+
version: versionFlag(args),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const binding = await resolveBinding(runtime, args);
|
|
174
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
175
|
+
const code = flagValue(args, '--code');
|
|
176
|
+
if (code !== null) await ensureCodeAvailable(runtime, binding, code, projectId);
|
|
177
|
+
return updateRecord(runtime, projectId, pairs, args, '项目进度更新');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function milestone(runtime, args) {
|
|
181
|
+
const action = exactlyOne(
|
|
182
|
+
[
|
|
183
|
+
['--list', flagOn(args, '--list')],
|
|
184
|
+
['--add', flagOn(args, '--add')],
|
|
185
|
+
['--update', flagValue(args, '--update') !== null],
|
|
186
|
+
],
|
|
187
|
+
'project milestone',
|
|
188
|
+
'例:xinhe-oa-cli project milestone --add --project <PROJECT_ID|CODE> --name 灰度发布 --due 2026-11-12 --weight 25 --json',
|
|
189
|
+
);
|
|
190
|
+
if (action === '--list') {
|
|
191
|
+
const projectRef = required(args, '--project');
|
|
192
|
+
if (runtime.dryRun) {
|
|
193
|
+
return dryRun(runtime, 'project.milestone.list', args, { project: projectRef });
|
|
194
|
+
}
|
|
195
|
+
const binding = await resolveBinding(runtime, args);
|
|
196
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
197
|
+
const { records, response } = await fetchRecords(
|
|
198
|
+
runtime,
|
|
199
|
+
table(binding, 'milestone'),
|
|
200
|
+
500,
|
|
201
|
+
0,
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
const milestones = records.filter((item) => mentions(item, 'project_ref', projectId));
|
|
205
|
+
return runtime.finishFrom(response, { count: milestones.length, milestones, project: projectId });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (action === '--add') {
|
|
209
|
+
const projectRef = required(args, '--project');
|
|
210
|
+
const name = required(args, '--name');
|
|
211
|
+
const due = required(args, '--due');
|
|
212
|
+
const weightRaw = required(args, '--weight');
|
|
213
|
+
const weight = number(weightRaw, '--weight');
|
|
214
|
+
const status = flagValue(args, '--status') ?? 'upcoming';
|
|
215
|
+
choice(status, MILESTONE_STATUS, '--status');
|
|
216
|
+
const pairs = [
|
|
217
|
+
jsonPair('project_ref', [projectRef]),
|
|
218
|
+
textPair('milestone_name', name),
|
|
219
|
+
textPair('milestone_status', status),
|
|
220
|
+
textPair('milestone_due_date', due),
|
|
221
|
+
numberPair('milestone_progress', percentValue(args, 0)),
|
|
222
|
+
numberPair('milestone_weight', weight),
|
|
223
|
+
];
|
|
224
|
+
addText(pairs, args, '--owner', 'milestone_owner');
|
|
225
|
+
addText(pairs, args, '--description', 'milestone_description');
|
|
226
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
227
|
+
if (runtime.dryRun) {
|
|
228
|
+
return dryRun(runtime, 'project.milestone.add', args, {
|
|
229
|
+
project: projectRef,
|
|
230
|
+
table: 'milestone',
|
|
231
|
+
values: valuesObject(pairs),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const binding = await resolveBinding(runtime, args);
|
|
235
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
236
|
+
pairs[0] = jsonPair('project_ref', [projectId]);
|
|
237
|
+
return createRecord(runtime, table(binding, 'milestone'), pairs, args, '新增项目里程碑');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const milestoneId = required(args, '--update');
|
|
241
|
+
const pairs = [];
|
|
242
|
+
addText(pairs, args, '--name', 'milestone_name');
|
|
243
|
+
addText(pairs, args, '--owner', 'milestone_owner');
|
|
244
|
+
addText(pairs, args, '--due', 'milestone_due_date');
|
|
245
|
+
addText(pairs, args, '--description', 'milestone_description');
|
|
246
|
+
const status = flagValue(args, '--status');
|
|
247
|
+
if (status !== null) {
|
|
248
|
+
choice(status, MILESTONE_STATUS, '--status');
|
|
249
|
+
pairs.push(textPair('milestone_status', status));
|
|
250
|
+
}
|
|
251
|
+
const progress = flagValue(args, '--progress');
|
|
252
|
+
if (progress !== null) pairs.push(numberPair('milestone_progress', percent(progress, '--progress')));
|
|
253
|
+
const weight = flagValue(args, '--weight');
|
|
254
|
+
if (weight !== null) pairs.push(numberPair('milestone_weight', number(weight, '--weight')));
|
|
255
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
256
|
+
if (pairs.length === 0) {
|
|
257
|
+
throw CliError.usage('`project milestone --update` 至少要给一个更新字段或 --set');
|
|
258
|
+
}
|
|
259
|
+
if (runtime.dryRun) {
|
|
260
|
+
return dryRun(runtime, 'project.milestone.update', args, {
|
|
261
|
+
milestone_id: milestoneId,
|
|
262
|
+
table: 'milestone',
|
|
263
|
+
values: valuesObject(pairs),
|
|
264
|
+
version: versionFlag(args),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return updateRecord(runtime, milestoneId, pairs, args, '更新项目里程碑');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function task(runtime, args) {
|
|
271
|
+
const action = exactlyOne(
|
|
272
|
+
[
|
|
273
|
+
['--list', flagOn(args, '--list')],
|
|
274
|
+
['--create', flagOn(args, '--create')],
|
|
275
|
+
['--update', flagValue(args, '--update') !== null],
|
|
276
|
+
],
|
|
277
|
+
'project task',
|
|
278
|
+
'例:xinhe-oa-cli project task --create --project <PROJECT_ID|CODE> --title 完成灰度名单校验 --json',
|
|
279
|
+
);
|
|
280
|
+
if (action === '--list') {
|
|
281
|
+
const projectRef = required(args, '--project');
|
|
282
|
+
if (runtime.dryRun) return dryRun(runtime, 'project.task.list', args, { project: projectRef });
|
|
283
|
+
const binding = await resolveBinding(runtime, args);
|
|
284
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
285
|
+
const { records, response } = await fetchRecords(
|
|
286
|
+
runtime,
|
|
287
|
+
table(binding, 'work_item'),
|
|
288
|
+
500,
|
|
289
|
+
0,
|
|
290
|
+
true,
|
|
291
|
+
);
|
|
292
|
+
const tasks = records.filter((item) => mentions(item, 'project_ref', projectId));
|
|
293
|
+
return runtime.finishFrom(response, { count: tasks.length, project: projectId, tasks });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (action === '--create') {
|
|
297
|
+
const projectRef = required(args, '--project');
|
|
298
|
+
const title = required(args, '--title');
|
|
299
|
+
const status = flagValue(args, '--status') ?? 'todo';
|
|
300
|
+
const priority = flagValue(args, '--priority') ?? 'medium';
|
|
301
|
+
choice(status, TASK_STATUS, '--status');
|
|
302
|
+
choice(priority, PRIORITIES, '--priority');
|
|
303
|
+
const pairs = [
|
|
304
|
+
jsonPair('project_ref', [projectRef]),
|
|
305
|
+
textPair('work_item_title', title),
|
|
306
|
+
textPair('work_item_status', status),
|
|
307
|
+
textPair('priority', priority),
|
|
308
|
+
];
|
|
309
|
+
addText(pairs, args, '--owner', 'work_item_owner');
|
|
310
|
+
addText(pairs, args, '--due', 'work_item_due_date');
|
|
311
|
+
addText(pairs, args, '--milestone', 'milestone_ref', true);
|
|
312
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
313
|
+
if (runtime.dryRun) {
|
|
314
|
+
return dryRun(runtime, 'project.task.create', args, {
|
|
315
|
+
project: projectRef,
|
|
316
|
+
table: 'work_item',
|
|
317
|
+
values: valuesObject(pairs),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const binding = await resolveBinding(runtime, args);
|
|
321
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
322
|
+
pairs[0] = jsonPair('project_ref', [projectId]);
|
|
323
|
+
return createRecord(runtime, table(binding, 'work_item'), pairs, args, '新增项目任务');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const taskId = required(args, '--update');
|
|
327
|
+
const pairs = [];
|
|
328
|
+
addText(pairs, args, '--title', 'work_item_title');
|
|
329
|
+
addText(pairs, args, '--owner', 'work_item_owner');
|
|
330
|
+
addText(pairs, args, '--due', 'work_item_due_date');
|
|
331
|
+
addText(pairs, args, '--milestone', 'milestone_ref', true);
|
|
332
|
+
const status = flagValue(args, '--status');
|
|
333
|
+
if (status !== null) {
|
|
334
|
+
choice(status, TASK_STATUS, '--status');
|
|
335
|
+
pairs.push(textPair('work_item_status', status));
|
|
336
|
+
}
|
|
337
|
+
const priority = flagValue(args, '--priority');
|
|
338
|
+
if (priority !== null) {
|
|
339
|
+
choice(priority, PRIORITIES, '--priority');
|
|
340
|
+
pairs.push(textPair('priority', priority));
|
|
341
|
+
}
|
|
342
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
343
|
+
if (pairs.length === 0) {
|
|
344
|
+
throw CliError.usage('`project task --update` 至少要给一个更新字段或 --set');
|
|
345
|
+
}
|
|
346
|
+
if (runtime.dryRun) {
|
|
347
|
+
return dryRun(runtime, 'project.task.update', args, {
|
|
348
|
+
table: 'work_item',
|
|
349
|
+
task_id: taskId,
|
|
350
|
+
values: valuesObject(pairs),
|
|
351
|
+
version: versionFlag(args),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return updateRecord(runtime, taskId, pairs, args, '更新项目任务');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function progress(runtime, args) {
|
|
358
|
+
const action = exactlyOne(
|
|
359
|
+
[['--list', flagOn(args, '--list')], ['--submit', flagOn(args, '--submit')]],
|
|
360
|
+
'project progress',
|
|
361
|
+
'例:xinhe-oa-cli project progress --submit --project <PROJECT_ID|CODE> --progress 72 --summary "本周完成联调" --json',
|
|
362
|
+
);
|
|
363
|
+
const projectRef = required(args, '--project');
|
|
364
|
+
if (action === '--list') {
|
|
365
|
+
if (runtime.dryRun) {
|
|
366
|
+
return dryRun(runtime, 'project.progress.list', args, { project: projectRef });
|
|
367
|
+
}
|
|
368
|
+
const binding = await resolveBinding(runtime, args);
|
|
369
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
370
|
+
const { records, response } = await fetchRecords(
|
|
371
|
+
runtime,
|
|
372
|
+
table(binding, 'progress_update'),
|
|
373
|
+
500,
|
|
374
|
+
0,
|
|
375
|
+
true,
|
|
376
|
+
);
|
|
377
|
+
const updates = records.filter((item) => mentions(item, 'project_ref', projectId));
|
|
378
|
+
return runtime.finishFrom(response, { count: updates.length, project: projectId, updates });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const progressValue = percent(required(args, '--progress'), '--progress');
|
|
382
|
+
const summary = required(args, '--summary');
|
|
383
|
+
const reportDate = flagValue(args, '--date') ?? today();
|
|
384
|
+
const author = currentUserId(runtime);
|
|
385
|
+
if (author === null) {
|
|
386
|
+
throw CliError.usage('当前 CLI 会话缺少用户摘要,无法记录进度更新人').withHint(
|
|
387
|
+
'重新登录或先执行 `xinhe-oa-cli me --json`',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const pairs = [
|
|
391
|
+
jsonPair('project_ref', [projectRef]),
|
|
392
|
+
textPair('report_date', reportDate),
|
|
393
|
+
numberPair('reported_progress', progressValue),
|
|
394
|
+
textPair('update_summary', summary),
|
|
395
|
+
textPair('update_author', author),
|
|
396
|
+
];
|
|
397
|
+
addText(pairs, args, '--risk', 'update_risk');
|
|
398
|
+
addText(pairs, args, '--next-step', 'update_next_step');
|
|
399
|
+
pairs.push(...parseSetPairs(flagList(args, '--set'), '--set'));
|
|
400
|
+
if (runtime.dryRun) {
|
|
401
|
+
return dryRun(runtime, 'project.progress.submit', args, {
|
|
402
|
+
project: projectRef,
|
|
403
|
+
table: 'progress_update',
|
|
404
|
+
values: valuesObject(pairs),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const binding = await resolveBinding(runtime, args);
|
|
408
|
+
const projectId = await resolveProjectId(runtime, binding, projectRef);
|
|
409
|
+
pairs[0] = jsonPair('project_ref', [projectId]);
|
|
410
|
+
return createRecord(runtime, table(binding, 'progress_update'), pairs, args, '提交项目周度进度');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function resolveBinding(runtime, args) {
|
|
414
|
+
const appKey = (flagValue(args, '--app') ?? APP_DEFAULT).trim() || APP_DEFAULT;
|
|
415
|
+
const tenantId = await resolveTenant(runtime, flagValue(args, '--tenant'));
|
|
416
|
+
if (tenantId === null) {
|
|
417
|
+
throw CliError.usage('项目命令需要企业上下文').withHint(
|
|
418
|
+
'加 --tenant <TENANT_ID>;用 `xinhe-oa-cli tenant --list` 查看',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const appsResponse = await runtime.sendAuthed(
|
|
422
|
+
'GET',
|
|
423
|
+
'/apps',
|
|
424
|
+
requestSpec({ query: [['tenant', tenantId], ['limit', '200']] }),
|
|
425
|
+
);
|
|
426
|
+
const apps = Array.isArray(dataOf(appsResponse)?.apps) ? dataOf(appsResponse).apps : [];
|
|
427
|
+
const app = apps
|
|
428
|
+
.filter((item) => item?.key === appKey)
|
|
429
|
+
.sort((left, right) => versionKey(right?.version) - versionKey(left?.version))[0];
|
|
430
|
+
if (app === undefined) {
|
|
431
|
+
throw CliError.usage(`企业 ${tenantId} 尚未安装项目应用 ${appKey}`).withHint(
|
|
432
|
+
'安装 Manifest:xinhe-oa-cli application --install --file apps/web/project-app.manifest.json --tenant <TENANT_ID> --plan --yes',
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const appId = app.id;
|
|
436
|
+
const appResponse = await runtime.sendAuthed('GET', `/apps/${appId}`, requestSpec());
|
|
437
|
+
const baseKey = dataOf(appResponse)?.manifest?.bases?.[0]?.key;
|
|
438
|
+
if (typeof baseKey !== 'string') {
|
|
439
|
+
throw CliError.usage('项目 Manifest 没有声明 bases[0].key');
|
|
440
|
+
}
|
|
441
|
+
const basesResponse = await runtime.sendAuthed(
|
|
442
|
+
'GET',
|
|
443
|
+
'/bases',
|
|
444
|
+
requestSpec({ query: [['tenant', tenantId], ['limit', '200']] }),
|
|
445
|
+
);
|
|
446
|
+
const bases = Array.isArray(dataOf(basesResponse)?.bases) ? dataOf(basesResponse).bases : [];
|
|
447
|
+
const base = bases.find((item) => item?.key === baseKey);
|
|
448
|
+
if (base === undefined || typeof base.id !== 'string') {
|
|
449
|
+
throw CliError.usage(`找不到项目应用库 ${baseKey}`).withHint(
|
|
450
|
+
'确认应用已成功应用,并且当前身份有该库的读取权限',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const tablesResponse = await runtime.sendAuthed(
|
|
454
|
+
'GET',
|
|
455
|
+
`/bases/${base.id}/tables`,
|
|
456
|
+
requestSpec(),
|
|
457
|
+
);
|
|
458
|
+
const tableRows = Array.isArray(dataOf(tablesResponse)?.tables)
|
|
459
|
+
? dataOf(tablesResponse).tables
|
|
460
|
+
: [];
|
|
461
|
+
const tables = {};
|
|
462
|
+
for (const key of ['project', 'milestone', 'work_item', 'progress_update']) {
|
|
463
|
+
const row = tableRows.find((item) => item?.key === key);
|
|
464
|
+
if (typeof row?.id === 'string') tables[key] = row.id;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
app_id: appId,
|
|
468
|
+
app_key: appKey,
|
|
469
|
+
app_version: app.version ?? null,
|
|
470
|
+
base_id: base.id,
|
|
471
|
+
tables,
|
|
472
|
+
tenant_id: tenantId,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function table(binding, key) {
|
|
477
|
+
const id = binding.tables[key];
|
|
478
|
+
if (typeof id !== 'string') {
|
|
479
|
+
throw CliError.usage(`项目应用缺少 ${key} 表`).withHint(
|
|
480
|
+
'重新应用项目 Manifest:xinhe-oa-cli application --install --file apps/web/project-app.manifest.json --tenant <TENANT_ID> --yes',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return id;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function resolveProjectId(runtime, binding, projectRef) {
|
|
487
|
+
const wanted = String(projectRef).trim();
|
|
488
|
+
if (wanted.startsWith('rec_')) return wanted;
|
|
489
|
+
const { records } = await fetchRecords(runtime, table(binding, 'project'), 500, 0, true);
|
|
490
|
+
const matches = records.filter((record) => recordValue(record, 'project_code') === wanted);
|
|
491
|
+
if (matches.length > 1) {
|
|
492
|
+
throw CliError.usage(`项目编号 ${JSON.stringify(wanted)} 对应多条记录,无法唯一确定`)
|
|
493
|
+
.withDetails({ project_code: wanted, record_ids: matches.map((item) => item.id) })
|
|
494
|
+
.withHint('改用项目记录 ID,并为已有数据修正重复编号');
|
|
495
|
+
}
|
|
496
|
+
if (typeof matches[0]?.id !== 'string') {
|
|
497
|
+
throw CliError.usage(`找不到项目 ${JSON.stringify(wanted)}`).withHint(
|
|
498
|
+
'用项目记录 ID 或精确项目编号;先跑 `project --list --all --json`',
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return matches[0].id;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function ensureCodeAvailable(runtime, binding, code, excludeId) {
|
|
505
|
+
const { records } = await fetchRecords(runtime, table(binding, 'project'), 500, 0, true);
|
|
506
|
+
const conflict = records.find(
|
|
507
|
+
(record) => recordValue(record, 'project_code') === code && record.id !== excludeId,
|
|
508
|
+
);
|
|
509
|
+
if (conflict !== undefined) {
|
|
510
|
+
throw CliError.usage(`项目编号 ${JSON.stringify(code)} 已存在`)
|
|
511
|
+
.withDetails({
|
|
512
|
+
existing_project_name: conflict?.values?.project_name ?? null,
|
|
513
|
+
existing_record_id: conflict.id,
|
|
514
|
+
project_code: code,
|
|
515
|
+
})
|
|
516
|
+
.withHint('项目编号应稳定且唯一;换一个编号,或更新已有项目');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function fetchRecords(runtime, tableId, limit, startOffset, all) {
|
|
521
|
+
const pageLimit = Math.min(Math.max(limit, 1), MAX_PAGE);
|
|
522
|
+
let offset = startOffset;
|
|
523
|
+
const records = [];
|
|
524
|
+
let lastResponse = null;
|
|
525
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
526
|
+
const response = await runtime.sendAuthed(
|
|
527
|
+
'GET',
|
|
528
|
+
`/tables/${tableId}/records`,
|
|
529
|
+
requestSpec({
|
|
530
|
+
query: [
|
|
531
|
+
['limit', String(pageLimit)],
|
|
532
|
+
['offset', String(offset)],
|
|
533
|
+
['order', 'created_at:desc,id:desc'],
|
|
534
|
+
],
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
const data = dataOf(response);
|
|
538
|
+
const rows = Array.isArray(data?.records) ? data.records : [];
|
|
539
|
+
records.push(...rows);
|
|
540
|
+
offset += rows.length;
|
|
541
|
+
lastResponse = response;
|
|
542
|
+
if (!all || rows.length === 0 || rows.length < pageLimit) break;
|
|
543
|
+
if (typeof data?.total === 'number' && offset >= data.total) break;
|
|
544
|
+
}
|
|
545
|
+
return { records, response: lastResponse };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function createRecord(runtime, tableId, pairs, args, defaultReason) {
|
|
549
|
+
const body = {
|
|
550
|
+
reason: flagValue(args, '--reason') ?? defaultReason,
|
|
551
|
+
source: 'cli:project',
|
|
552
|
+
values: valuesObject(pairs),
|
|
553
|
+
};
|
|
554
|
+
const spec = requestSpec({
|
|
555
|
+
body,
|
|
556
|
+
idempotencyKey: idempotencyKey('project-create', flagValue(args, '--idempotency-key')),
|
|
557
|
+
});
|
|
558
|
+
const response = await runtime.sendAuthed('POST', `/tables/${tableId}/records`, spec);
|
|
559
|
+
runtime.finishFrom(response, { record: dataOf(response) });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function updateRecord(runtime, recordId, pairs, args, defaultReason) {
|
|
563
|
+
const version =
|
|
564
|
+
versionFlag(args) ?? (await currentVersion(runtime, recordId));
|
|
565
|
+
const spec = requestSpec({
|
|
566
|
+
body: {
|
|
567
|
+
reason: flagValue(args, '--reason') ?? defaultReason,
|
|
568
|
+
values: valuesObject(pairs),
|
|
569
|
+
version,
|
|
570
|
+
},
|
|
571
|
+
idempotencyKey: idempotencyKey('project-update', flagValue(args, '--idempotency-key')),
|
|
572
|
+
});
|
|
573
|
+
const response = await runtime.sendAuthed('PATCH', `/records/${recordId}`, spec);
|
|
574
|
+
runtime.finishFrom(response, { record: dataOf(response) });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function currentVersion(runtime, recordId) {
|
|
578
|
+
const response = await runtime.sendAuthed('GET', `/records/${recordId}`, requestSpec());
|
|
579
|
+
const version = dataOf(response)?.version;
|
|
580
|
+
if (typeof version !== 'number' || !Number.isInteger(version)) {
|
|
581
|
+
throw new CliError(Kind.Server, 'cli.bad_response', '读不到记录的 version(乐观锁必须用它)');
|
|
582
|
+
}
|
|
583
|
+
return version;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function dryRun(runtime, operation, args, payload) {
|
|
587
|
+
runtime.finish({
|
|
588
|
+
app_key: flagValue(args, '--app') ?? APP_DEFAULT,
|
|
589
|
+
dry_run: true,
|
|
590
|
+
note: '--dry-run:没有联网解析 ID,也没有写入任何数据',
|
|
591
|
+
operation,
|
|
592
|
+
payload,
|
|
593
|
+
tenant: flagValue(args, '--tenant') ?? '<由唯一可见企业解析>',
|
|
594
|
+
will_resolve: ['app_id', 'base_id', 'project/milestone/work_item/progress_update table_id'],
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function required(args, flag) {
|
|
599
|
+
const raw = flagValue(args, flag);
|
|
600
|
+
if (raw === null || String(raw).trim().length === 0) {
|
|
601
|
+
throw CliError.usage(`缺少 ${flag}`).withHint('先加 --dry-run --json 查看完整参数,再正式执行');
|
|
602
|
+
}
|
|
603
|
+
return String(raw).trim();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function choice(value, choices, flag) {
|
|
607
|
+
if (!choices.includes(value)) {
|
|
608
|
+
throw CliError.usage(`${flag} 只能是 ${choices.join(' | ')},收到 ${JSON.stringify(value)}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function number(raw, flag) {
|
|
613
|
+
const value = Number(raw);
|
|
614
|
+
if (!Number.isFinite(value)) throw CliError.usage(`${flag} 需要一个数字,收到 ${JSON.stringify(raw)}`);
|
|
615
|
+
return value;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function percent(raw, flag) {
|
|
619
|
+
const value = number(raw, flag);
|
|
620
|
+
if (value < 0 || value > 100) throw CliError.usage(`${flag} 必须在 0..100,收到 ${value}`);
|
|
621
|
+
return value;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function percentValue(args, fallback) {
|
|
625
|
+
const raw = flagValue(args, '--progress');
|
|
626
|
+
return raw === null ? fallback : percent(raw, '--progress');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function versionFlag(args) {
|
|
630
|
+
const raw = flagValue(args, '--version');
|
|
631
|
+
if (raw === null) return null;
|
|
632
|
+
const text = String(raw).trim();
|
|
633
|
+
if (!/^[+-]?\d+$/.test(text)) {
|
|
634
|
+
throw CliError.usage(`\`--version\` 要一个整数,收到 ${JSON.stringify(raw)}`);
|
|
635
|
+
}
|
|
636
|
+
return Number.parseInt(text, 10);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function addText(pairs, args, flag, key, relation = false) {
|
|
640
|
+
const value = flagValue(args, flag);
|
|
641
|
+
if (value === null) return;
|
|
642
|
+
pairs.push(relation ? jsonPair(key, [String(value)]) : textPair(key, String(value)));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function textPair(key, value) {
|
|
646
|
+
return { forceString: true, key, value: String(value) };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function numberPair(key, value) {
|
|
650
|
+
return { forceString: false, key, value: String(value) };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function jsonPair(key, value) {
|
|
654
|
+
return { forceString: false, key, value: JSON.stringify(value) };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function recordValue(record, key) {
|
|
658
|
+
const value = record?.values?.[key];
|
|
659
|
+
return value === null || value === undefined ? '' : String(value);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function mentions(record, key, recordId) {
|
|
663
|
+
const value = record?.values?.[key];
|
|
664
|
+
if (Array.isArray(value)) return value.some((item) => item === recordId);
|
|
665
|
+
return value === recordId;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function currentUserId(runtime) {
|
|
669
|
+
const user = runtime.credentials.get(runtime.profile)?.user;
|
|
670
|
+
return typeof user?.id === 'string' ? user.id : null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function versionKey(raw) {
|
|
674
|
+
return String(raw ?? '')
|
|
675
|
+
.split('.')
|
|
676
|
+
.reduce((value, part) => value * 1000 + (Number.parseInt(part, 10) || 0), 0);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function today() {
|
|
680
|
+
return new Date().toISOString().slice(0, 10);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function timestamp() {
|
|
684
|
+
return new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
685
|
+
}
|
package/src/commands/session.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { flagOn, flagValue } from '../core/args.js';
|
|
34
34
|
import {
|
|
35
35
|
adoptSession,
|
|
36
|
+
credentialFromTokens,
|
|
36
37
|
envelopeData,
|
|
37
38
|
loginWithPassword,
|
|
38
39
|
logout as logoutSession,
|
|
@@ -53,8 +54,9 @@ const SCOPE_CAP = 50;
|
|
|
53
54
|
* 顺序不是随手写的:clap 的 `ArgGroup(multiple = false)` 保证四选一,
|
|
54
55
|
* 但"既有 code 又 adopt"这种组合在 Rust 里由 adopt 先赢 —— `meta.command` 要一致。
|
|
55
56
|
*/
|
|
56
|
-
function loginKind({ adopt, password, hasCode }) {
|
|
57
|
+
function loginKind({ adopt, password, changePassword, hasCode }) {
|
|
57
58
|
if (adopt) return '--adopt-session';
|
|
59
|
+
if (changePassword) return '--change-password';
|
|
58
60
|
if (password) return '--password';
|
|
59
61
|
if (hasCode) return '--phone --code';
|
|
60
62
|
return '--phone';
|
|
@@ -64,6 +66,7 @@ function loginKind({ adopt, password, hasCode }) {
|
|
|
64
66
|
export async function handleLogin({ args, runtime }) {
|
|
65
67
|
const adopt = flagOn(args, '--adopt-session');
|
|
66
68
|
const password = flagOn(args, '--password');
|
|
69
|
+
const changePassword = flagOn(args, '--change-password');
|
|
67
70
|
const phone = flagValue(args, '--phone');
|
|
68
71
|
const code = flagValue(args, '--code');
|
|
69
72
|
const account = flagValue(args, '--account');
|
|
@@ -72,15 +75,18 @@ export async function handleLogin({ args, runtime }) {
|
|
|
72
75
|
// (`login --phone` / `login --phone --code` / `login --password` / `login --adopt-session`),
|
|
73
76
|
// 而 Node 的命令表按域名折叠成 `login`。这里在收尾(以及发请求)之前把它校正成
|
|
74
77
|
// Rust 的形态,`--json` 的 `meta.command` 才逐字一致。
|
|
75
|
-
runtime.command = `login ${loginKind({ adopt, password, hasCode: code !== null })}`;
|
|
78
|
+
runtime.command = `login ${loginKind({ adopt, password, changePassword, hasCode: code !== null })}`;
|
|
76
79
|
|
|
77
80
|
// ① 交接会话(install.sh 用):不联网,只把它写进自己的凭据文件。
|
|
78
81
|
if (adopt) return adoptFromStdin(runtime);
|
|
79
82
|
|
|
80
|
-
// ②
|
|
83
|
+
// ② 当前会话改密:先完成临时口令登录,再读取三行口令。
|
|
84
|
+
if (changePassword) return changeCurrentPassword(runtime);
|
|
85
|
+
|
|
86
|
+
// ③ 口令登录(普通成员或引导账号;口令从 stdin 读,**不进 argv**)。
|
|
81
87
|
if (password) return loginByPassword(runtime, account);
|
|
82
88
|
|
|
83
|
-
//
|
|
89
|
+
// ④ 拿验证码换凭据。
|
|
84
90
|
//
|
|
85
91
|
// 判空用 `=== null`(而不是 falsy):`--phone=` 这种显式空串在 Rust 里是
|
|
86
92
|
// `Some("")`,会被原样发给服务端,由服务端回"号码不合法" —— CLI 不替它判。
|
|
@@ -91,12 +97,59 @@ export async function handleLogin({ args, runtime }) {
|
|
|
91
97
|
}
|
|
92
98
|
if (code !== null) return loginByCode(runtime, phone, code);
|
|
93
99
|
|
|
94
|
-
//
|
|
100
|
+
// ⑤ 只给手机号 = 发码。
|
|
95
101
|
return requestOtp(runtime, phone);
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
async function changeCurrentPassword(runtime) {
|
|
105
|
+
const raw = await readStdinText();
|
|
106
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim());
|
|
107
|
+
if (lines.length < 3 || lines.slice(0, 3).some((line) => line.length === 0)) {
|
|
108
|
+
throw CliError.usage('标准输入里需要当前口令、新口令、确认新口令三行').withHint(
|
|
109
|
+
"用法:printf '%s\\n%s\\n%s\\n' \"$OLD_PW\" \"$NEW_PW\" \"$NEW_PW\" | xinhe-oa-cli login --change-password",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const [currentPassword, newPassword, confirmation] = lines;
|
|
113
|
+
if (newPassword !== confirmation) throw CliError.usage('两次输入的新口令不一致');
|
|
114
|
+
if (newPassword.length < 12) throw CliError.usage('新口令至少需要 12 个字符');
|
|
115
|
+
if (newPassword === currentPassword) throw CliError.usage('新口令不能与当前口令相同');
|
|
116
|
+
|
|
117
|
+
const spec = requestSpec({
|
|
118
|
+
body: { current_password: currentPassword, new_password: newPassword },
|
|
119
|
+
});
|
|
120
|
+
if (runtime.dryRun) {
|
|
121
|
+
return runtime.dryRunReport('POST', '/auth/password/change', spec);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const response = await runtime.sendAuthed('POST', '/auth/password/change', spec);
|
|
125
|
+
const body = envelopeData(response);
|
|
126
|
+
const previous = runtime.credentials.get(runtime.profile);
|
|
127
|
+
const phone = previous?.phone ?? runtime.config.profile(runtime.profile).phone ?? null;
|
|
128
|
+
const deviceId = runtime.ensureDeviceId();
|
|
129
|
+
const credential = credentialFromTokens(body, runtime.baseUrl, deviceId, phone);
|
|
130
|
+
runtime.credentials.put(runtime.profile, credential);
|
|
131
|
+
if (typeof phone === 'string' && phone.length > 0) {
|
|
132
|
+
runtime.config.setPhone(phone);
|
|
133
|
+
runtime.config.save();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return runtime.finishFrom(response, {
|
|
137
|
+
profile: runtime.profile,
|
|
138
|
+
base_url: credential.base_url,
|
|
139
|
+
user: credential.user ?? null,
|
|
140
|
+
auth_method: 'password',
|
|
141
|
+
sessions_revoked: body.sessions_revoked ?? null,
|
|
142
|
+
credentials_file: runtime.paths.credentialsFile,
|
|
143
|
+
access_token: maskToken(credential.tokens?.access_token ?? ''),
|
|
144
|
+
refresh_token: maskToken(credential.tokens?.refresh_token ?? ''),
|
|
145
|
+
access_expires_at: credential.tokens?.access_expires_at ?? null,
|
|
146
|
+
next_step: '口令已更新并保存新会话;现在可以执行 `xinhe-oa-cli me --json` 或每日任务',
|
|
147
|
+
note: '旧口令、全部旧会话和临时口令均已失效;新令牌未进入 argv 与日志。',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
/**
|
|
99
|
-
* `login --password --account
|
|
152
|
+
* `login --password --account`:普通成员或引导账号,口令从 stdin 读。
|
|
100
153
|
*
|
|
101
154
|
* 三步的顺序是刻意的(与 Rust 一致):**先落盘 device_id,再读口令,最后才碰网络**。
|
|
102
155
|
* 口令读失败(stdin 是空的)时,device_id 已经存好了 —— 用户重跑一次不会换指纹。
|
|
@@ -299,6 +299,43 @@
|
|
|
299
299
|
"--password"
|
|
300
300
|
]
|
|
301
301
|
},
|
|
302
|
+
{
|
|
303
|
+
"name": "login --change-password",
|
|
304
|
+
"flags": {
|
|
305
|
+
"--change-password": {
|
|
306
|
+
"takes_value": false,
|
|
307
|
+
"required": false,
|
|
308
|
+
"optional_value": false
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"extra_flags": [],
|
|
312
|
+
"allowed_flags": [],
|
|
313
|
+
"params_raw": [
|
|
314
|
+
"--change-password"
|
|
315
|
+
],
|
|
316
|
+
"params": [
|
|
317
|
+
{
|
|
318
|
+
"flag": "--change-password"
|
|
319
|
+
}
|
|
320
|
+
],
|
|
321
|
+
"risk": "write",
|
|
322
|
+
"requires_yes": false,
|
|
323
|
+
"endpoints": [
|
|
324
|
+
"POST /auth/password/change"
|
|
325
|
+
],
|
|
326
|
+
"examples": [
|
|
327
|
+
"printf '%s\\n%s\\n%s\\n' \"$OLD_PW\" \"$NEW_PW\" \"$NEW_PW\" | xinhe-oa-cli login --change-password"
|
|
328
|
+
],
|
|
329
|
+
"output_schema": "{user, auth_method, sessions_revoked, tokens_saved: true}(旧会话与临时口令失效)",
|
|
330
|
+
"synonym_flags": [],
|
|
331
|
+
"rejected_flags": [
|
|
332
|
+
"--phone",
|
|
333
|
+
"--code",
|
|
334
|
+
"--password",
|
|
335
|
+
"--account",
|
|
336
|
+
"--adopt-session"
|
|
337
|
+
]
|
|
338
|
+
},
|
|
302
339
|
{
|
|
303
340
|
"name": "logout",
|
|
304
341
|
"flags": {},
|
|
@@ -26809,6 +26846,38 @@
|
|
|
26809
26846
|
"--primary"
|
|
26810
26847
|
]
|
|
26811
26848
|
},
|
|
26849
|
+
{
|
|
26850
|
+
"name": "member --reset-password",
|
|
26851
|
+
"flags": {
|
|
26852
|
+
"--reset-password": {
|
|
26853
|
+
"takes_value": true,
|
|
26854
|
+
"required": false,
|
|
26855
|
+
"optional_value": false
|
|
26856
|
+
}
|
|
26857
|
+
},
|
|
26858
|
+
"extra_flags": [],
|
|
26859
|
+
"allowed_flags": [],
|
|
26860
|
+
"params_raw": [
|
|
26861
|
+
"--reset-password <USER_ID>"
|
|
26862
|
+
],
|
|
26863
|
+
"params": [
|
|
26864
|
+
{
|
|
26865
|
+
"flag": "--reset-password",
|
|
26866
|
+
"value": "USER_ID"
|
|
26867
|
+
}
|
|
26868
|
+
],
|
|
26869
|
+
"risk": "high-risk-write",
|
|
26870
|
+
"requires_yes": true,
|
|
26871
|
+
"endpoints": [
|
|
26872
|
+
"POST /admin/users/{id}/password/reset"
|
|
26873
|
+
],
|
|
26874
|
+
"examples": [
|
|
26875
|
+
"xinhe-oa-cli member --reset-password usr_123 --yes --json"
|
|
26876
|
+
],
|
|
26877
|
+
"output_schema": "{user, temporary_password, expires_at, sessions_revoked, must_change_password}(临时口令只显示一次,30 分钟有效)",
|
|
26878
|
+
"synonym_flags": [],
|
|
26879
|
+
"rejected_flags": []
|
|
26880
|
+
},
|
|
26812
26881
|
{
|
|
26813
26882
|
"name": "config --list",
|
|
26814
26883
|
"flags": {
|
|
@@ -27048,6 +27117,248 @@
|
|
|
27048
27117
|
"synonym_flags": [],
|
|
27049
27118
|
"rejected_flags": []
|
|
27050
27119
|
},
|
|
27120
|
+
{
|
|
27121
|
+
"name": "project",
|
|
27122
|
+
"flags": {
|
|
27123
|
+
"--context": {"takes_value": false, "required": false, "optional_value": false},
|
|
27124
|
+
"--list": {"takes_value": false, "required": false, "optional_value": false},
|
|
27125
|
+
"--create": {"takes_value": false, "required": false, "optional_value": false},
|
|
27126
|
+
"--show": {"takes_value": true, "required": false, "optional_value": false},
|
|
27127
|
+
"--update": {"takes_value": true, "required": false, "optional_value": false},
|
|
27128
|
+
"--add": {"takes_value": false, "required": false, "optional_value": false},
|
|
27129
|
+
"--submit": {"takes_value": false, "required": false, "optional_value": false},
|
|
27130
|
+
"--app": {"takes_value": true, "required": true, "optional_value": true},
|
|
27131
|
+
"--tenant": {"takes_value": true, "required": false, "optional_value": false},
|
|
27132
|
+
"--name": {"takes_value": true, "required": false, "optional_value": false},
|
|
27133
|
+
"--code": {"takes_value": true, "required": false, "optional_value": false},
|
|
27134
|
+
"--owner": {"takes_value": true, "required": false, "optional_value": false},
|
|
27135
|
+
"--start": {"takes_value": true, "required": false, "optional_value": false},
|
|
27136
|
+
"--due": {"takes_value": true, "required": false, "optional_value": false},
|
|
27137
|
+
"--status": {"takes_value": true, "required": false, "optional_value": false},
|
|
27138
|
+
"--health": {"takes_value": true, "required": false, "optional_value": false},
|
|
27139
|
+
"--progress": {"takes_value": true, "required": false, "optional_value": false},
|
|
27140
|
+
"--current-milestone": {"takes_value": true, "required": false, "optional_value": false},
|
|
27141
|
+
"--description": {"takes_value": true, "required": false, "optional_value": false},
|
|
27142
|
+
"--set": {"takes_value": true, "required": false, "optional_value": false},
|
|
27143
|
+
"--version": {"takes_value": true, "required": false, "optional_value": false},
|
|
27144
|
+
"--reason": {"takes_value": true, "required": false, "optional_value": false},
|
|
27145
|
+
"--idempotency-key": {"takes_value": true, "required": false, "optional_value": false},
|
|
27146
|
+
"--all": {"takes_value": false, "required": false, "optional_value": false},
|
|
27147
|
+
"--limit": {"takes_value": true, "required": true, "optional_value": true},
|
|
27148
|
+
"--offset": {"takes_value": true, "required": false, "optional_value": false},
|
|
27149
|
+
"--project": {"takes_value": true, "required": false, "optional_value": false},
|
|
27150
|
+
"--milestone": {"takes_value": true, "required": false, "optional_value": false},
|
|
27151
|
+
"--weight": {"takes_value": true, "required": false, "optional_value": false},
|
|
27152
|
+
"--title": {"takes_value": true, "required": false, "optional_value": false},
|
|
27153
|
+
"--priority": {"takes_value": true, "required": false, "optional_value": false},
|
|
27154
|
+
"--risk": {"takes_value": true, "required": false, "optional_value": false},
|
|
27155
|
+
"--next-step": {"takes_value": true, "required": false, "optional_value": false},
|
|
27156
|
+
"--date": {"takes_value": true, "required": false, "optional_value": false}
|
|
27157
|
+
},
|
|
27158
|
+
"extra_flags": [
|
|
27159
|
+
"--context", "--list", "--create", "--show", "--update", "--add", "--submit",
|
|
27160
|
+
"--app", "--tenant", "--name", "--code", "--owner", "--start", "--due",
|
|
27161
|
+
"--status", "--health", "--progress", "--current-milestone", "--description",
|
|
27162
|
+
"--set", "--version", "--reason", "--idempotency-key", "--all", "--limit",
|
|
27163
|
+
"--offset", "--project", "--milestone", "--weight", "--title", "--priority",
|
|
27164
|
+
"--risk", "--next-step", "--date"
|
|
27165
|
+
],
|
|
27166
|
+
"allowed_flags": [],
|
|
27167
|
+
"params_raw": [
|
|
27168
|
+
"--list | --show <ID|CODE> | --create | --update <ID|CODE> | --context",
|
|
27169
|
+
"[--tenant <TENANT_ID>]",
|
|
27170
|
+
"[--app <APP_KEY>]"
|
|
27171
|
+
],
|
|
27172
|
+
"params": [
|
|
27173
|
+
{"flag": "--tenant", "value": "TENANT_ID", "optional": true},
|
|
27174
|
+
{"flag": "--app", "value": "APP_KEY", "optional": true}
|
|
27175
|
+
],
|
|
27176
|
+
"risk": "write",
|
|
27177
|
+
"requires_yes": false,
|
|
27178
|
+
"endpoints": [
|
|
27179
|
+
"GET /apps",
|
|
27180
|
+
"GET /apps/{id}",
|
|
27181
|
+
"GET /bases",
|
|
27182
|
+
"GET /bases/{id}/tables",
|
|
27183
|
+
"GET|POST|PATCH /tables/{id}/records",
|
|
27184
|
+
"GET|PATCH /records/{id}"
|
|
27185
|
+
],
|
|
27186
|
+
"examples": [
|
|
27187
|
+
"xinhe-oa-cli project --list --tenant huimeng_xinhe --json",
|
|
27188
|
+
"xinhe-oa-cli project --create --tenant huimeng_xinhe --name 新项目 --owner usr_123 --due 2026-12-31",
|
|
27189
|
+
"xinhe-oa-cli project --update HM-PRJ-001 --tenant huimeng_xinhe --progress 65 --health attention"
|
|
27190
|
+
],
|
|
27191
|
+
"output_schema": "{binding?, project(s), count, next_step?}",
|
|
27192
|
+
"synonym_flags": [],
|
|
27193
|
+
"rejected_flags": []
|
|
27194
|
+
},
|
|
27195
|
+
{
|
|
27196
|
+
"name": "project milestone",
|
|
27197
|
+
"flags": {
|
|
27198
|
+
"--list": {"takes_value": false, "required": false, "optional_value": false},
|
|
27199
|
+
"--add": {"takes_value": false, "required": false, "optional_value": false},
|
|
27200
|
+
"--update": {"takes_value": true, "required": false, "optional_value": false},
|
|
27201
|
+
"--app": {"takes_value": true, "required": true, "optional_value": true},
|
|
27202
|
+
"--tenant": {"takes_value": true, "required": false, "optional_value": false},
|
|
27203
|
+
"--project": {"takes_value": true, "required": true, "optional_value": false},
|
|
27204
|
+
"--name": {"takes_value": true, "required": false, "optional_value": false},
|
|
27205
|
+
"--owner": {"takes_value": true, "required": false, "optional_value": false},
|
|
27206
|
+
"--due": {"takes_value": true, "required": false, "optional_value": false},
|
|
27207
|
+
"--status": {"takes_value": true, "required": false, "optional_value": false},
|
|
27208
|
+
"--progress": {"takes_value": true, "required": false, "optional_value": false},
|
|
27209
|
+
"--weight": {"takes_value": true, "required": false, "optional_value": false},
|
|
27210
|
+
"--description": {"takes_value": true, "required": false, "optional_value": false},
|
|
27211
|
+
"--set": {"takes_value": true, "required": false, "optional_value": false},
|
|
27212
|
+
"--version": {"takes_value": true, "required": false, "optional_value": false},
|
|
27213
|
+
"--reason": {"takes_value": true, "required": false, "optional_value": false},
|
|
27214
|
+
"--idempotency-key": {"takes_value": true, "required": false, "optional_value": false},
|
|
27215
|
+
"--all": {"takes_value": false, "required": false, "optional_value": false}
|
|
27216
|
+
},
|
|
27217
|
+
"extra_flags": [
|
|
27218
|
+
"--list", "--add", "--update", "--app", "--tenant", "--project", "--name",
|
|
27219
|
+
"--owner", "--due", "--status", "--progress", "--weight", "--description",
|
|
27220
|
+
"--set", "--version", "--reason", "--idempotency-key", "--all"
|
|
27221
|
+
],
|
|
27222
|
+
"allowed_flags": [],
|
|
27223
|
+
"params_raw": ["--list | --add | --update <MILESTONE_ID>", "--project <ID|CODE>"],
|
|
27224
|
+
"params": [
|
|
27225
|
+
{"flag": "--project", "value": "ID|CODE"}
|
|
27226
|
+
],
|
|
27227
|
+
"risk": "write",
|
|
27228
|
+
"requires_yes": false,
|
|
27229
|
+
"endpoints": ["GET|POST|PATCH /tables/{id}/records"],
|
|
27230
|
+
"examples": [
|
|
27231
|
+
"xinhe-oa-cli project milestone --list --project HM-PRJ-001 --tenant huimeng_xinhe --json",
|
|
27232
|
+
"xinhe-oa-cli project milestone --add --project HM-PRJ-001 --name MVP交付 --due 2026-12-01 --weight 30"
|
|
27233
|
+
],
|
|
27234
|
+
"output_schema": "{project, milestones, count}",
|
|
27235
|
+
"synonym_flags": [],
|
|
27236
|
+
"rejected_flags": []
|
|
27237
|
+
},
|
|
27238
|
+
{
|
|
27239
|
+
"name": "project task",
|
|
27240
|
+
"flags": {
|
|
27241
|
+
"--list": {"takes_value": false, "required": false, "optional_value": false},
|
|
27242
|
+
"--create": {"takes_value": false, "required": false, "optional_value": false},
|
|
27243
|
+
"--update": {"takes_value": true, "required": false, "optional_value": false},
|
|
27244
|
+
"--app": {"takes_value": true, "required": true, "optional_value": true},
|
|
27245
|
+
"--tenant": {"takes_value": true, "required": false, "optional_value": false},
|
|
27246
|
+
"--project": {"takes_value": true, "required": true, "optional_value": false},
|
|
27247
|
+
"--milestone": {"takes_value": true, "required": false, "optional_value": false},
|
|
27248
|
+
"--title": {"takes_value": true, "required": false, "optional_value": false},
|
|
27249
|
+
"--owner": {"takes_value": true, "required": false, "optional_value": false},
|
|
27250
|
+
"--status": {"takes_value": true, "required": false, "optional_value": false},
|
|
27251
|
+
"--priority": {"takes_value": true, "required": false, "optional_value": false},
|
|
27252
|
+
"--due": {"takes_value": true, "required": false, "optional_value": false},
|
|
27253
|
+
"--set": {"takes_value": true, "required": false, "optional_value": false},
|
|
27254
|
+
"--version": {"takes_value": true, "required": false, "optional_value": false},
|
|
27255
|
+
"--reason": {"takes_value": true, "required": false, "optional_value": false},
|
|
27256
|
+
"--idempotency-key": {"takes_value": true, "required": false, "optional_value": false},
|
|
27257
|
+
"--all": {"takes_value": false, "required": false, "optional_value": false}
|
|
27258
|
+
},
|
|
27259
|
+
"extra_flags": [
|
|
27260
|
+
"--list", "--create", "--update", "--app", "--tenant", "--project",
|
|
27261
|
+
"--milestone", "--title", "--owner", "--status", "--priority", "--due",
|
|
27262
|
+
"--set", "--version", "--reason", "--idempotency-key", "--all"
|
|
27263
|
+
],
|
|
27264
|
+
"allowed_flags": [],
|
|
27265
|
+
"params_raw": ["--list | --create | --update <TASK_ID>", "--project <ID|CODE>"],
|
|
27266
|
+
"params": [
|
|
27267
|
+
{"flag": "--project", "value": "ID|CODE"}
|
|
27268
|
+
],
|
|
27269
|
+
"risk": "write",
|
|
27270
|
+
"requires_yes": false,
|
|
27271
|
+
"endpoints": ["GET|POST|PATCH /tables/{id}/records"],
|
|
27272
|
+
"examples": [
|
|
27273
|
+
"xinhe-oa-cli project task --list --project HM-PRJ-001 --tenant huimeng_xinhe --json",
|
|
27274
|
+
"xinhe-oa-cli project task --create --project HM-PRJ-001 --title 完成联调 --owner usr_123"
|
|
27275
|
+
],
|
|
27276
|
+
"output_schema": "{project, tasks, count}",
|
|
27277
|
+
"synonym_flags": [],
|
|
27278
|
+
"rejected_flags": []
|
|
27279
|
+
},
|
|
27280
|
+
{
|
|
27281
|
+
"name": "project progress",
|
|
27282
|
+
"flags": {
|
|
27283
|
+
"--list": {"takes_value": false, "required": false, "optional_value": false},
|
|
27284
|
+
"--submit": {"takes_value": false, "required": false, "optional_value": false},
|
|
27285
|
+
"--app": {"takes_value": true, "required": true, "optional_value": true},
|
|
27286
|
+
"--tenant": {"takes_value": true, "required": false, "optional_value": false},
|
|
27287
|
+
"--project": {"takes_value": true, "required": true, "optional_value": false},
|
|
27288
|
+
"--progress": {"takes_value": true, "required": false, "optional_value": false},
|
|
27289
|
+
"--summary": {"takes_value": true, "required": false, "optional_value": false},
|
|
27290
|
+
"--risk": {"takes_value": true, "required": false, "optional_value": false},
|
|
27291
|
+
"--next-step": {"takes_value": true, "required": false, "optional_value": false},
|
|
27292
|
+
"--date": {"takes_value": true, "required": false, "optional_value": false},
|
|
27293
|
+
"--set": {"takes_value": true, "required": false, "optional_value": false},
|
|
27294
|
+
"--reason": {"takes_value": true, "required": false, "optional_value": false},
|
|
27295
|
+
"--idempotency-key": {"takes_value": true, "required": false, "optional_value": false},
|
|
27296
|
+
"--all": {"takes_value": false, "required": false, "optional_value": false}
|
|
27297
|
+
},
|
|
27298
|
+
"extra_flags": [
|
|
27299
|
+
"--list", "--submit", "--app", "--tenant", "--project", "--progress",
|
|
27300
|
+
"--summary", "--risk", "--next-step", "--date", "--set", "--reason",
|
|
27301
|
+
"--idempotency-key", "--all"
|
|
27302
|
+
],
|
|
27303
|
+
"allowed_flags": [],
|
|
27304
|
+
"params_raw": ["--list | --submit", "--project <ID|CODE>", "--progress <PERCENT>", "--summary <TEXT>"],
|
|
27305
|
+
"params": [
|
|
27306
|
+
{"flag": "--project", "value": "ID|CODE"}
|
|
27307
|
+
],
|
|
27308
|
+
"risk": "write",
|
|
27309
|
+
"requires_yes": false,
|
|
27310
|
+
"endpoints": ["GET|POST /tables/{id}/records"],
|
|
27311
|
+
"examples": [
|
|
27312
|
+
"xinhe-oa-cli project progress --list --project HM-PRJ-001 --tenant huimeng_xinhe --json",
|
|
27313
|
+
"xinhe-oa-cli project progress --submit --project HM-PRJ-001 --progress 72 --summary 本周完成联调 --risk 等待测试数据"
|
|
27314
|
+
],
|
|
27315
|
+
"output_schema": "{project, updates, count}",
|
|
27316
|
+
"synonym_flags": [],
|
|
27317
|
+
"rejected_flags": []
|
|
27318
|
+
},
|
|
27319
|
+
{
|
|
27320
|
+
"name": "member --link",
|
|
27321
|
+
"flags": {
|
|
27322
|
+
"--link": {
|
|
27323
|
+
"takes_value": true,
|
|
27324
|
+
"required": true,
|
|
27325
|
+
"optional_value": false
|
|
27326
|
+
},
|
|
27327
|
+
"--tenant": {
|
|
27328
|
+
"takes_value": true,
|
|
27329
|
+
"required": false,
|
|
27330
|
+
"optional_value": false
|
|
27331
|
+
}
|
|
27332
|
+
},
|
|
27333
|
+
"extra_flags": [],
|
|
27334
|
+
"allowed_flags": [],
|
|
27335
|
+
"params_raw": [
|
|
27336
|
+
"--link <USER_ID>",
|
|
27337
|
+
"[--tenant <TENANT_ID>]"
|
|
27338
|
+
],
|
|
27339
|
+
"params": [
|
|
27340
|
+
{
|
|
27341
|
+
"flag": "--link",
|
|
27342
|
+
"value": "USER_ID"
|
|
27343
|
+
},
|
|
27344
|
+
{
|
|
27345
|
+
"flag": "--tenant",
|
|
27346
|
+
"value": "TENANT_ID",
|
|
27347
|
+
"optional": true
|
|
27348
|
+
}
|
|
27349
|
+
],
|
|
27350
|
+
"risk": "write",
|
|
27351
|
+
"requires_yes": false,
|
|
27352
|
+
"endpoints": [
|
|
27353
|
+
"POST /portal/auth/links"
|
|
27354
|
+
],
|
|
27355
|
+
"examples": [
|
|
27356
|
+
"xinhe-oa-cli member --link usr_123 --tenant huimeng_xinhe --json"
|
|
27357
|
+
],
|
|
27358
|
+
"output_schema": "{url, ticket, expires_at, user, tenant}",
|
|
27359
|
+
"synonym_flags": [],
|
|
27360
|
+
"rejected_flags": []
|
|
27361
|
+
},
|
|
27051
27362
|
{
|
|
27052
27363
|
"name": "schema",
|
|
27053
27364
|
"flags": {
|
package/src/version.js
CHANGED