@xinhe-oa/cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -2
- package/bin/xinhe-oa-cli.js +13 -0
- package/package.json +40 -4
- package/src/commands/application.js +244 -0
- package/src/commands/approval.js +275 -0
- package/src/commands/attachment.js +202 -0
- package/src/commands/bitable.js +3040 -0
- package/src/commands/config.js +186 -0
- package/src/commands/dashboard.js +132 -0
- package/src/commands/doctor.js +975 -0
- package/src/commands/event.js +151 -0
- package/src/commands/file.js +1474 -0
- package/src/commands/index.js +78 -0
- package/src/commands/investor.js +179 -0
- package/src/commands/member.js +1341 -0
- package/src/commands/org.js +399 -0
- package/src/commands/report.js +908 -0
- package/src/commands/schema.js +75 -0
- package/src/commands/session.js +516 -0
- package/src/commands/skills.js +1117 -0
- package/src/commands/tenant.js +202 -0
- package/src/commands/workflow.js +176 -0
- package/src/core/args.js +901 -0
- package/src/core/auth.js +349 -0
- package/src/core/checkpoint.js +226 -0
- package/src/core/config.js +407 -0
- package/src/core/error.js +217 -0
- package/src/core/http.js +903 -0
- package/src/core/import.js +753 -0
- package/src/core/output.js +300 -0
- package/src/core/runtime.js +302 -0
- package/src/core/util.js +195 -0
- package/src/core/values.js +278 -0
- package/src/core/xlsx.js +321 -0
- package/src/main.js +205 -0
- package/src/spec/agent_guide.md +55 -0
- package/src/spec/command_surface.json +27073 -0
- package/src/spec/embedded_skills.json +19 -0
- package/src/version.js +16 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// 审批:`approval --list|--show|--approve|--reject|--cancel|--revert`(docs/34 §5)
|
|
2
|
+
// —— 第 15 个顶层域。
|
|
3
|
+
//
|
|
4
|
+
// 通用审批(`base.approval`)今天只服务两个 kind:`skill_share`(技能分享到企业)与
|
|
5
|
+
// `file_permission`(文件读写权申请,FILE-006)。设计成通用类型是为了后面的报销 / 合同 ——
|
|
6
|
+
// 那些只加一个 `kind` 与一条策略映射,不再动表结构,也不再加命令。
|
|
7
|
+
//
|
|
8
|
+
// 四条刻意的行为:
|
|
9
|
+
//
|
|
10
|
+
// 1. **`--approve` / `--reject` / `--revert` 是 `high-risk-write`**:没有 `--yes`
|
|
11
|
+
// 就本地退出码 2、**不发请求**(审批是不可撤销的组织动作,误点一次要有人去擦屁股)。
|
|
12
|
+
// 2. **`--reject` / `--revert` 的 `--note` 必填**:驳回与收回权限都要说清理由
|
|
13
|
+
// (写进 `approval_event`,事后能复盘)。理由先校验、`--yes` 后校验 ——
|
|
14
|
+
// 缺理由时连"确认"都不该鼓励先敲:敲了 `--yes` 就会以为自己已经想清楚了。
|
|
15
|
+
// 3. **`--list --to-review` 在非审批人身份下返回空列表是正常的**,不是错误、更不是 403
|
|
16
|
+
// (docs/34 §6.7:他确实没有待审项)。CLI 把它渲染成"你现在没有待审项"并**退出 0** ——
|
|
17
|
+
// 把它当错误会让"我确实没有待审的"变成一条吓人的红线,而它只是今天的实情。
|
|
18
|
+
// 4. **`--revert` 只收回"能反向收回"的 subject**:`file_permission` 批准时写的 ACL
|
|
19
|
+
// 会被撤销;`skill_share` 这类没有可撤销 ACL 的审批会回 400
|
|
20
|
+
// `approval.revert_not_supported` —— CLI 不替服务端表态,原样透传。
|
|
21
|
+
|
|
22
|
+
import { CliError, exactlyOne } from '../core/error.js';
|
|
23
|
+
import { flagOn, flagValue } from '../core/args.js';
|
|
24
|
+
import { approvalStatusFilter } from '../core/values.js';
|
|
25
|
+
import { dataOf, requestSpec } from '../core/runtime.js';
|
|
26
|
+
|
|
27
|
+
/** `approval` 的分发(动作按旗标判定,理由同 `skills.js` 的说明)。 */
|
|
28
|
+
export async function handleApproval({ args, runtime }) {
|
|
29
|
+
const action = exactlyOne(
|
|
30
|
+
[
|
|
31
|
+
['--list', flagOn(args, '--list')],
|
|
32
|
+
['--show', flagOn(args, '--show')],
|
|
33
|
+
['--approve', flagOn(args, '--approve')],
|
|
34
|
+
['--reject', flagOn(args, '--reject')],
|
|
35
|
+
['--cancel', flagOn(args, '--cancel')],
|
|
36
|
+
['--revert', flagOn(args, '--revert')],
|
|
37
|
+
],
|
|
38
|
+
'approval',
|
|
39
|
+
'例:xinhe-oa-cli approval --list --to-review --json',
|
|
40
|
+
);
|
|
41
|
+
switch (action) {
|
|
42
|
+
case '--list':
|
|
43
|
+
return list(runtime, args);
|
|
44
|
+
case '--show':
|
|
45
|
+
return show(runtime, args, requiredValue(args, '--show', '--show <APPROVAL_ID>'));
|
|
46
|
+
case '--approve':
|
|
47
|
+
return approve(runtime, args, requiredValue(args, '--approve', '--approve <APPROVAL_ID>'));
|
|
48
|
+
case '--reject':
|
|
49
|
+
return reject(runtime, args, requiredValue(args, '--reject', '--reject <APPROVAL_ID>'));
|
|
50
|
+
case '--cancel':
|
|
51
|
+
return cancel(runtime, args, requiredValue(args, '--cancel', '--cancel <APPROVAL_ID>'));
|
|
52
|
+
default:
|
|
53
|
+
return revert(runtime, args, requiredValue(args, '--revert', '--revert <APPROVAL_ID>'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `approval --list [--to-review | --mine] [--status ...] [--kind ...]`。 */
|
|
58
|
+
async function list(runtime, args) {
|
|
59
|
+
const toReview = flagOn(args, '--to-review');
|
|
60
|
+
const mine = flagOn(args, '--mine');
|
|
61
|
+
if (toReview && mine) {
|
|
62
|
+
throw CliError.usage('`--to-review`(待我审)与 `--mine`(我提交的)只能给一个').withHint(
|
|
63
|
+
'两个视角是两份清单,不能混在一起看',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const role = toReview ? 'reviewer' : mine ? 'requester' : null;
|
|
67
|
+
const status = statusFilter(flagValue(args, '--status'));
|
|
68
|
+
const kind = nonEmpty(flagValue(args, '--kind'));
|
|
69
|
+
|
|
70
|
+
const query = [];
|
|
71
|
+
if (role !== null) query.push(['role', role]);
|
|
72
|
+
if (status !== null) query.push(['status', status]);
|
|
73
|
+
if (kind !== null) query.push(['kind', kind]);
|
|
74
|
+
const spec = requestSpec({ query });
|
|
75
|
+
if (runtime.dryRun) {
|
|
76
|
+
runtime.dryRunReport('GET', '/approvals', spec);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const response = await runtime.sendAuthed('GET', '/approvals', spec);
|
|
80
|
+
const data = dataOf(response);
|
|
81
|
+
const approvals = Array.isArray(data?.approvals)
|
|
82
|
+
? data.approvals
|
|
83
|
+
: Array.isArray(data?.items)
|
|
84
|
+
? data.items
|
|
85
|
+
: [];
|
|
86
|
+
const total = data?.total ?? approvals.length;
|
|
87
|
+
const view =
|
|
88
|
+
role === 'reviewer'
|
|
89
|
+
? 'to-review(待我审)'
|
|
90
|
+
: role === 'requester'
|
|
91
|
+
? 'mine(我提交的)'
|
|
92
|
+
: 'all(服务端默认视角)';
|
|
93
|
+
let note;
|
|
94
|
+
if (approvals.length === 0) {
|
|
95
|
+
if (role === 'reviewer') {
|
|
96
|
+
// docs/34 §6.7:业务员看到空列表是设计如此,不是权限问题。
|
|
97
|
+
note =
|
|
98
|
+
'你现在没有待审项(这是正常的:只有企业负责人(admin:member 持有者)才有待审项,不是权限错误)';
|
|
99
|
+
} else if (role === 'requester') {
|
|
100
|
+
note = '你还没有提交过审批:xinhe-oa-cli skills --publish <SKILL> --visibility tenant';
|
|
101
|
+
} else {
|
|
102
|
+
note = '这里现在什么都没有(换 --to-review / --mine 看另一个视角)';
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
note = '待审的用 xinhe-oa-cli approval --approve <ID> --yes 或 --reject <ID> --note "..." --yes';
|
|
106
|
+
}
|
|
107
|
+
runtime.finishFrom(response, {
|
|
108
|
+
approvals,
|
|
109
|
+
total,
|
|
110
|
+
view,
|
|
111
|
+
role,
|
|
112
|
+
status,
|
|
113
|
+
kind,
|
|
114
|
+
note,
|
|
115
|
+
next_step: '看一条的详情与轨迹:xinhe-oa-cli approval --show <APPROVAL_ID> --json',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `approval --show <id>`:详情 + 轨迹(`submit` / `approve` / `reject` …)。 */
|
|
120
|
+
async function show(runtime, args, id) {
|
|
121
|
+
rejectFilters(args, '--show <APPROVAL_ID>');
|
|
122
|
+
const target = `/approvals/${id}`;
|
|
123
|
+
const spec = requestSpec();
|
|
124
|
+
if (runtime.dryRun) {
|
|
125
|
+
runtime.dryRunReport('GET', target, spec);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const response = await runtime.sendAuthed('GET', target, spec);
|
|
129
|
+
const data = dataOf(response);
|
|
130
|
+
const status = typeof data?.status === 'string' ? data.status : '';
|
|
131
|
+
const out = data;
|
|
132
|
+
if (isPlainObject(out)) {
|
|
133
|
+
out.approval_id = id;
|
|
134
|
+
out.next_step =
|
|
135
|
+
status === 'pending'
|
|
136
|
+
? '批准:xinhe-oa-cli approval --approve <ID> --yes;驳回:xinhe-oa-cli approval --reject <ID> --note "理由" --yes;撤回:xinhe-oa-cli approval --cancel <ID>'
|
|
137
|
+
: status === 'approved'
|
|
138
|
+
? '要收回这份批准:xinhe-oa-cli approval --revert <ID> --note "理由" --yes(只对 file_permission 这类批准时写了权限的有效)'
|
|
139
|
+
: '看清单:xinhe-oa-cli approval --list --to-review --json';
|
|
140
|
+
out.note = '轨迹(events)就是复盘用的:谁提交、谁批、什么时候、什么理由';
|
|
141
|
+
}
|
|
142
|
+
runtime.finishFrom(response, out);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `approval --approve <id> [--note "..."]`:**高风险写操作,必须 `--yes`**。 */
|
|
146
|
+
async function approve(runtime, args, id) {
|
|
147
|
+
runtime.requireYes(
|
|
148
|
+
'approval --approve(批准后技能立刻对企业内所有人可见)',
|
|
149
|
+
`xinhe-oa-cli approval --approve ${id}`,
|
|
150
|
+
);
|
|
151
|
+
const note = nonEmpty(flagValue(args, '--note'));
|
|
152
|
+
const body = note === null ? {} : { note };
|
|
153
|
+
return decide(runtime, args, 'approve', id, body);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `approval --reject <id> --note "..."`:**高风险写操作,必须 `--yes`;`--note` 必填**。 */
|
|
157
|
+
async function reject(runtime, args, id) {
|
|
158
|
+
// note 先校验:缺理由的驳回连 --yes 都不该鼓励先敲。
|
|
159
|
+
const note = requireNote(args, '--reject', '名字和企业已有技能重了');
|
|
160
|
+
runtime.requireYes(
|
|
161
|
+
'approval --reject(驳回后技能退回草稿/私有)',
|
|
162
|
+
`xinhe-oa-cli approval --reject ${id} --note "${note}"`,
|
|
163
|
+
);
|
|
164
|
+
return decide(runtime, args, 'reject', id, { note });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `approval --cancel <id>`:提交人撤回(不是高风险:撤的是自己提交的东西)。 */
|
|
168
|
+
async function cancel(runtime, args, id) {
|
|
169
|
+
return decide(runtime, args, 'cancel', id, {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `approval --revert <id> --note "..."`:**高风险写操作,必须 `--yes`;`--note` 必填**。
|
|
174
|
+
*
|
|
175
|
+
* FILE-006:只对批准时确实写出权限的 kind(`file_permission`)有意义 ——
|
|
176
|
+
* 收回后不可再改回,要重新授权只能提一条新申请。对没有可撤销 subject 的审批,
|
|
177
|
+
* 服务端回 400 `approval.revert_not_supported`,CLI 原样透传(不替服务端表态)。
|
|
178
|
+
*/
|
|
179
|
+
async function revert(runtime, args, id) {
|
|
180
|
+
// note 先校验:缺理由的收回连 --yes 都不该鼓励先敲(与 reject 同序)。
|
|
181
|
+
const note = requireNote(args, '--revert', '跨部门授权不符合归档规则');
|
|
182
|
+
runtime.requireYes(
|
|
183
|
+
'approval --revert(撤销已批准的权限,收回后不可再改回)',
|
|
184
|
+
`xinhe-oa-cli approval --revert ${id} --note "${note}"`,
|
|
185
|
+
);
|
|
186
|
+
return decide(runtime, args, 'revert', id, { note });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function decide(runtime, args, action, id, body) {
|
|
190
|
+
rejectFilters(args, `--${action} <APPROVAL_ID>`);
|
|
191
|
+
const target = `/approvals/${id}/${action}`;
|
|
192
|
+
const spec = requestSpec({ body });
|
|
193
|
+
if (runtime.dryRun) {
|
|
194
|
+
runtime.dryRunReport('POST', target, spec);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const response = await runtime.sendAuthed('POST', target, spec);
|
|
198
|
+
const data = dataOf(response);
|
|
199
|
+
const status = typeof data?.status === 'string' ? data.status : '';
|
|
200
|
+
const out = data;
|
|
201
|
+
if (isPlainObject(out)) {
|
|
202
|
+
out.approval_id = id;
|
|
203
|
+
out.action = action;
|
|
204
|
+
out.next_step =
|
|
205
|
+
action === 'approve'
|
|
206
|
+
? '企业内其他人现在才读得到:xinhe-oa-cli skills --read <SKILL> --json'
|
|
207
|
+
: action === 'reject'
|
|
208
|
+
? '提交人改完可以重新提交:xinhe-oa-cli skills --publish <SKILL> --visibility tenant'
|
|
209
|
+
: action === 'revert'
|
|
210
|
+
? '权限已收回;要重新授权只能提一条新申请:xinhe-oa-cli file --permission-request <FILE> --action read|write --json'
|
|
211
|
+
: '看还剩什么:xinhe-oa-cli approval --list --mine --json';
|
|
212
|
+
// `status` **永远在**(Revert 的响应把状态包在 `approval` 里,顶层没有 status)——
|
|
213
|
+
// 形状固定的字段比"有时有有时没有"好判:脚本不需要先判断键在不在。
|
|
214
|
+
out.status = status.length === 0 ? null : status;
|
|
215
|
+
}
|
|
216
|
+
runtime.finishFrom(response, out);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// 本地校验(与 Rust `ApprovalArgs` 逐条对齐)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
/** `--to-review` / `--mine` / `--status` / `--kind` 只对 `--list` 有意义。 */
|
|
223
|
+
function rejectFilters(args, what) {
|
|
224
|
+
if (flagOn(args, '--to-review') || flagOn(args, '--mine')) {
|
|
225
|
+
throw CliError.usage(
|
|
226
|
+
`--to-review / --mine 只对 \`approval --list\` 有意义,不属于 \`${what}\``,
|
|
227
|
+
).withHint('看清单:xinhe-oa-cli approval --list --to-review --json');
|
|
228
|
+
}
|
|
229
|
+
if (flagOn(args, '--status') || flagOn(args, '--kind')) {
|
|
230
|
+
throw CliError.usage(
|
|
231
|
+
`--status / --kind 只对 \`approval --list\` 有意义,不属于 \`${what}\``,
|
|
232
|
+
).withHint('看清单:xinhe-oa-cli approval --list --status pending --json');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function requireNote(args, action, sample) {
|
|
237
|
+
const note = nonEmpty(flagValue(args, '--note'));
|
|
238
|
+
if (note === null) {
|
|
239
|
+
throw CliError.usage(`\`approval ${action}\` 必须给 --note <理由>`).withHint(
|
|
240
|
+
`例:xinhe-oa-cli approval ${action} apr_123 --note "${sample}" --yes`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return note;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* `--status` 的取值面。
|
|
248
|
+
*
|
|
249
|
+
* core 的 `approvalStatusFilter` 认的是最常写的那几个;Rust 的口径还多三条:
|
|
250
|
+
* `all` / 空串 = 不带 status 参数、`canceled` 是 `cancelled` 的别拼、`expired` 合法。
|
|
251
|
+
* 这里补齐三条再交给 core,报错口径与验收面都保持一处实现。
|
|
252
|
+
*/
|
|
253
|
+
function statusFilter(raw) {
|
|
254
|
+
const value = String(raw ?? '').trim().toLowerCase();
|
|
255
|
+
if (value.length === 0 || value === 'all') return null;
|
|
256
|
+
if (value === 'canceled') return 'cancelled';
|
|
257
|
+
if (value === 'expired') return 'expired';
|
|
258
|
+
return approvalStatusFilter(value);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function requiredValue(args, flag, what) {
|
|
262
|
+
const value = nonEmpty(flagValue(args, flag));
|
|
263
|
+
if (value === null) throw CliError.usage(`\`approval\` 需要 ${what}`);
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function nonEmpty(raw) {
|
|
268
|
+
if (raw === null || raw === undefined) return null;
|
|
269
|
+
const text = String(raw).trim();
|
|
270
|
+
return text.length > 0 ? text : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isPlainObject(value) {
|
|
274
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
275
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// 附件:`attachment --upload|--show|--download|--link|--unlink|--delete`(契约 §2)。
|
|
2
|
+
//
|
|
3
|
+
// 三件事在这里收口:
|
|
4
|
+
//
|
|
5
|
+
// - **上传**:`POST /attachments`(multipart)——`--tenant` 不传就用当前唯一可见企业;
|
|
6
|
+
// - **下载**:先 `POST /attachments/{id}/download-url` 拿签名 URL,再用它**匿名**
|
|
7
|
+
// GET(签名即授权),落盘时校验服务端给的 `content_hash`(sha256)——
|
|
8
|
+
// 传输坏了要当场发现。这一段与 `file --download` 共用(`file.js` 的
|
|
9
|
+
// `downloadWithSignedUrl`):文件与附件是同一条版本链,各写一遍迟早漂移;
|
|
10
|
+
// - **关联**:`POST|DELETE /attachments/{id}/links`(`record` 类型)——
|
|
11
|
+
// 关联留在 attachment 域(`file` 域刻意不做 `--link` / `--unlink`)。
|
|
12
|
+
//
|
|
13
|
+
// 本地判定的顺序照抄 Rust:**先**校验必填/读文件,**再**解析企业(后者才可能联网)。
|
|
14
|
+
|
|
15
|
+
import { CliError, exactlyOne } from '../core/error.js';
|
|
16
|
+
import { flagOn, flagValue } from '../core/args.js';
|
|
17
|
+
import { multipartBody, requestSpec } from '../core/http.js';
|
|
18
|
+
import { dataOf, idempotencyKey, readFileBytes } from '../core/runtime.js';
|
|
19
|
+
import { fileNameOf, queryPairs } from '../core/values.js';
|
|
20
|
+
import { downloadWithSignedUrl, resolveTenant } from './file.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `attachment` 的动作分发。
|
|
24
|
+
*
|
|
25
|
+
* 与 Rust 的 clap `ArgGroup(action, multiple=false)` 同义:六个动作**恰好一个**。
|
|
26
|
+
* 规格驱动的解析器不替我们做这件事(`--upload` 这类动作旗标只在 `extra_flags` 里,
|
|
27
|
+
* 不在它的互斥检查范围内),所以 `attachment --upload --delete att_1` 这种命令行
|
|
28
|
+
* 在这里必须自己拒掉 —— Rust 会拒(退出码 2、不发请求),不能替调用方挑一个执行。
|
|
29
|
+
*/
|
|
30
|
+
export async function handleAttachment({ args, runtime }) {
|
|
31
|
+
const picked = exactlyOne(
|
|
32
|
+
[
|
|
33
|
+
['--upload', flagOn(args, '--upload')],
|
|
34
|
+
['--download', args['--download'] !== undefined],
|
|
35
|
+
['--link', flagOn(args, '--link')],
|
|
36
|
+
['--unlink', flagOn(args, '--unlink')],
|
|
37
|
+
['--delete', args['--delete'] !== undefined],
|
|
38
|
+
['--show', args['--show'] !== undefined],
|
|
39
|
+
],
|
|
40
|
+
'attachment',
|
|
41
|
+
'例:xinhe-oa-cli attachment --upload --file ./单据.csv --tenant kashu',
|
|
42
|
+
);
|
|
43
|
+
if (picked === '--upload') return upload(runtime, args);
|
|
44
|
+
if (picked === '--download') return download(runtime, args);
|
|
45
|
+
if (picked === '--link') return link(runtime, args);
|
|
46
|
+
if (picked === '--unlink') return unlink(runtime, args);
|
|
47
|
+
if (picked === '--delete') return remove(runtime, args);
|
|
48
|
+
return show(runtime, args);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 上传一个本地文件并返回服务端的 `data`(`bitable record --attach` 也用它)。
|
|
53
|
+
*
|
|
54
|
+
* `--name` 缺省时用文件名;`--tenant` 缺省时**才**去问 `GET /me` 挑唯一可见企业
|
|
55
|
+
* (多企业会明确报错,不替调用方猜)。
|
|
56
|
+
*/
|
|
57
|
+
export async function uploadFile(runtime, sourcePath, tenantRaw, nameRaw) {
|
|
58
|
+
const bytes = readFileBytes(sourcePath);
|
|
59
|
+
const uploadName = fileNameOf(sourcePath);
|
|
60
|
+
let tenant = tenantRaw === null || tenantRaw === undefined ? '' : String(tenantRaw).trim();
|
|
61
|
+
if (tenant.length === 0) tenant = await resolveTenant(runtime, null, '上传附件');
|
|
62
|
+
const form = multipartBody(
|
|
63
|
+
[
|
|
64
|
+
['tenant', tenant],
|
|
65
|
+
['name', nameRaw === null || nameRaw === undefined ? uploadName : nameRaw],
|
|
66
|
+
],
|
|
67
|
+
['file', uploadName, bytes],
|
|
68
|
+
);
|
|
69
|
+
if (runtime.dryRun) {
|
|
70
|
+
runtime.dryRunReport('POST', '/attachments', requestSpec({ multipart: form }));
|
|
71
|
+
return { dry_run: true };
|
|
72
|
+
}
|
|
73
|
+
const response = await runtime.sendUpload(
|
|
74
|
+
'/attachments',
|
|
75
|
+
form,
|
|
76
|
+
idempotencyKey('attachment-upload'),
|
|
77
|
+
);
|
|
78
|
+
return dataOf(response);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function upload(runtime, args) {
|
|
82
|
+
const sourcePath = flagValue(args, '--file');
|
|
83
|
+
if (sourcePath === null) {
|
|
84
|
+
throw CliError.usage('`attachment --upload` 需要 --file <路径>').withHint(
|
|
85
|
+
'例:xinhe-oa-cli attachment --upload --file ./公账流水.csv --tenant kashu',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const data = await uploadFile(
|
|
89
|
+
runtime,
|
|
90
|
+
sourcePath,
|
|
91
|
+
flagValue(args, '--tenant'),
|
|
92
|
+
flagValue(args, '--name'),
|
|
93
|
+
);
|
|
94
|
+
if (runtime.dryRun) return;
|
|
95
|
+
// 与 Rust 一致:这里的 `meta.request_id` **不**取服务端回显(`finish` 而不是
|
|
96
|
+
// `finish_from`)—— 上传的幂等键才是这条命令的重放凭据。
|
|
97
|
+
runtime.finish({
|
|
98
|
+
attachment: data,
|
|
99
|
+
next_step: '挂到记录上:xinhe-oa-cli attachment --link <attachment_id> --resource-id <record_id>',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `attachment --show <ATTACHMENT_ID>`:元数据(含 versions / links)。
|
|
105
|
+
*
|
|
106
|
+
* **刻意没有 `--dry-run` 分支**(与 Rust 逐条一致):Rust 侧漏了这一支,
|
|
107
|
+
* Node 版照抄而不是"顺手修好"—— 两边行为必须一样。
|
|
108
|
+
*/
|
|
109
|
+
async function show(runtime, args) {
|
|
110
|
+
const attachmentId = String(args['--show'] ?? '');
|
|
111
|
+
const response = await runtime.sendAuthed(
|
|
112
|
+
'GET',
|
|
113
|
+
`/attachments/${attachmentId}`,
|
|
114
|
+
requestSpec(),
|
|
115
|
+
);
|
|
116
|
+
runtime.finishFrom(response, dataOf(response));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `attachment --download <ATTACHMENT_ID> [--output <PATH>]`。 */
|
|
120
|
+
async function download(runtime, args) {
|
|
121
|
+
const attachmentId = String(args['--download'] ?? '');
|
|
122
|
+
if (runtime.dryRun) {
|
|
123
|
+
runtime.dryRunReport('POST', `/attachments/${attachmentId}/download-url`, requestSpec());
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const { response, data } = await downloadWithSignedUrl(
|
|
127
|
+
runtime,
|
|
128
|
+
attachmentId,
|
|
129
|
+
null,
|
|
130
|
+
flagValue(args, '--output'),
|
|
131
|
+
);
|
|
132
|
+
runtime.finishFrom(response, data);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `attachment --link --id <附件 id> --resource-id <记录 id> [--field <字段>]`。 */
|
|
136
|
+
async function link(runtime, args) {
|
|
137
|
+
const attachmentId = flagValue(args, '--id');
|
|
138
|
+
if (attachmentId === null) {
|
|
139
|
+
throw CliError.usage('`attachment --link` 需要 --id <附件 id>').withHint(
|
|
140
|
+
'例:xinhe-oa-cli attachment --link --id att_123 --resource-id rec_123',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const resourceId = flagValue(args, '--resource-id');
|
|
144
|
+
if (resourceId === null) {
|
|
145
|
+
throw CliError.usage('`attachment --link` 需要 --resource-id <记录 id>');
|
|
146
|
+
}
|
|
147
|
+
// `--resource-type` 的缺省是 `record`(Rust 侧是 clap 的 `default_value`)。
|
|
148
|
+
const body = {
|
|
149
|
+
resource_type: flagValue(args, '--resource-type') ?? 'record',
|
|
150
|
+
resource_id: resourceId,
|
|
151
|
+
field_id: flagValue(args, '--field'),
|
|
152
|
+
};
|
|
153
|
+
const spec = requestSpec({ body });
|
|
154
|
+
const requestPath = `/attachments/${attachmentId}/links`;
|
|
155
|
+
if (runtime.dryRun) {
|
|
156
|
+
runtime.dryRunReport('POST', requestPath, spec);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const response = await runtime.sendAuthed('POST', requestPath, spec);
|
|
160
|
+
runtime.finishFrom(response, dataOf(response));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `attachment --unlink --id <附件 id> --resource-id <记录 id>`。 */
|
|
164
|
+
async function unlink(runtime, args) {
|
|
165
|
+
const attachmentId = flagValue(args, '--id');
|
|
166
|
+
if (attachmentId === null) {
|
|
167
|
+
throw CliError.usage('`attachment --unlink` 需要 --id <附件 id>').withHint(
|
|
168
|
+
'例:xinhe-oa-cli attachment --unlink --id att_123 --resource-id rec_123',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const resourceId = flagValue(args, '--resource-id');
|
|
172
|
+
if (resourceId === null) {
|
|
173
|
+
throw CliError.usage('`attachment --unlink` 需要 --resource-id <记录 id>');
|
|
174
|
+
}
|
|
175
|
+
const spec = requestSpec({
|
|
176
|
+
query: [
|
|
177
|
+
['resource_type', flagValue(args, '--resource-type') ?? 'record'],
|
|
178
|
+
['resource_id', resourceId],
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
const requestPath = `/attachments/${attachmentId}/links`;
|
|
182
|
+
if (runtime.dryRun) {
|
|
183
|
+
runtime.dryRunReport('DELETE', requestPath, spec);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const response = await runtime.sendAuthed('DELETE', requestPath, spec);
|
|
187
|
+
runtime.finishFrom(response, dataOf(response));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** `attachment --delete <ATTACHMENT_ID> [--reason <TEXT>]`(高风险:必须 `--yes`)。 */
|
|
191
|
+
async function remove(runtime, args) {
|
|
192
|
+
const attachmentId = String(args['--delete'] ?? '');
|
|
193
|
+
runtime.requireYes('attachment --delete', `xinhe-oa-cli attachment --delete ${attachmentId}`);
|
|
194
|
+
const spec = requestSpec({ query: queryPairs([['reason', flagValue(args, '--reason')]]) });
|
|
195
|
+
const requestPath = `/attachments/${attachmentId}`;
|
|
196
|
+
if (runtime.dryRun) {
|
|
197
|
+
runtime.dryRunReport('DELETE', requestPath, spec);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const response = await runtime.sendAuthed('DELETE', requestPath, spec);
|
|
201
|
+
runtime.finishFrom(response, dataOf(response));
|
|
202
|
+
}
|