@xinhehuimeng/cli 0.2.5 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xinhehuimeng/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "心河 OA 客户端 CLI(Node 版):多维表格 / 应用 / 权限的唯一命令行入口",
5
5
  "keywords": [
6
6
  "xinhe-oa",
@@ -4,9 +4,9 @@
4
4
  // 自己负责把结果写出去(`runtime.finish(...)` / `runtime.finishWithHuman(...)`),
5
5
  // 失败就 `throw CliError`。**退出码由 `main.js` 统一收口**(契约 §3.2)。
6
6
  //
7
- // 为什么一张显式的表,而不是按域名动态 import:命令面是**冻结契约**(153 条),
7
+ // 为什么一张显式的表,而不是按域名动态 import:命令面是**冻结契约**(160 条),
8
8
  // 表里缺一条就是"这条命令在 Node 版上不存在"——显式的表能让这件事一眼可见,
9
- // 也方便 `test/surface.test.js` 断言"153 条命令每条都有处理函数"。
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],
@@ -1,4 +1,4 @@
1
- // 成员域:`member --list|--search|--create|--role|--revoke-role|--remove|--reset-password`,
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)。
@@ -83,6 +83,8 @@ export async function handleMember({ args, runtime }) {
83
83
  return removeMember(runtime, action.user, args);
84
84
  case 'reset-password':
85
85
  return resetPassword(runtime, action.user);
86
+ case 'link':
87
+ return portalLink(runtime, args, action.user);
86
88
  case 'set-dept':
87
89
  return setDept(runtime, args, action.user, action.deptId, action.primary);
88
90
  case 'unset-dept':
@@ -132,6 +134,7 @@ function memberAction(args) {
132
134
  ['--revoke-role', given('--revoke-role')],
133
135
  ['--remove', given('--remove')],
134
136
  ['--reset-password', given('--reset-password')],
137
+ ['--link', given('--link')],
135
138
  ['--set-dept', given('--set-dept')],
136
139
  ['--unset-dept', given('--unset-dept')],
137
140
  ['--set-manager', given('--set-manager')],
@@ -408,11 +411,38 @@ function memberAction(args) {
408
411
  '例:xinhe-oa-cli member --reset-password usr_123 --yes --json',
409
412
  ),
410
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
+ }
411
423
  default:
412
424
  return { kind: 'remove', user: String(val('--remove') ?? '') };
413
425
  }
414
426
  }
415
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
+
416
446
  /** 0 个或多于 1 个动作都是用法错误(与 Rust `exactly_one` 同文案)。 */
417
447
  function exactlyOne(selected, what, hint) {
418
448
  const picked = selected.filter(([, active]) => active).map(([name]) => name);
@@ -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
+ }
@@ -27117,6 +27117,248 @@
27117
27117
  "synonym_flags": [],
27118
27118
  "rejected_flags": []
27119
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
+ },
27120
27362
  {
27121
27363
  "name": "schema",
27122
27364
  "flags": {
package/src/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // 这个常量与 `package.json` 的 `version` **必须一致**:`test/version.test.js`
8
8
  // 会逐字比对,`npm version` 之后忘了改这里就会红。
9
- export const VERSION = '0.2.5';
9
+ export const VERSION = '0.2.6';
10
10
 
11
11
  /**
12
12
  * npm 包名(升级指引 `npm i -g <包名>@latest` 与 `doctor` 的 npm 探测都用它)。