@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,202 @@
|
|
|
1
|
+
// 企业:`tenant --list|--create|--show`(契约 §2、§6.2)。
|
|
2
|
+
//
|
|
3
|
+
// 对应服务端 `GET|POST /admin/tenants`。三件事在这里定:
|
|
4
|
+
//
|
|
5
|
+
// 1. **动作由旗标判**(`--list` / `--create` / `--show`),不靠命令名 —— Node 的解析器
|
|
6
|
+
// 是"按参数面挑最合适的那条候选命令",`investor --endorse --reason r` 这种组合
|
|
7
|
+
// 甚至会被判成兄弟命令的候选(见 investor.js 的说明)。Rust 侧同样是**一个扁平
|
|
8
|
+
// struct + `action()`**,所以两条实现都以 args 里的动作旗标为准。
|
|
9
|
+
// 2. **缺必填、多动作、空门店编码都在本地判死**(退出码 2、一个请求都不发)——
|
|
10
|
+
// 与 Rust 的 `CliError::usage` 逐条对齐。
|
|
11
|
+
// 3. **接口没上线时不伪装成功**:`POST /admin/tenants` 在旧服务端上是 404/500,
|
|
12
|
+
// 这时给"服务端未就绪 + 替代路径",而不是让客户以为自己的参数写错了。
|
|
13
|
+
|
|
14
|
+
import { CliError, Kind, exactlyOne } from '../core/error.js';
|
|
15
|
+
import { flagList, flagOn, flagValue } from '../core/args.js';
|
|
16
|
+
import { dataOf, idempotencyKey, requestSpec } from '../core/runtime.js';
|
|
17
|
+
|
|
18
|
+
/** `tenant --list|--create|--show` 的分发。 */
|
|
19
|
+
export async function handleTenant({ args, positional, runtime, command }) {
|
|
20
|
+
const action = exactlyOne(
|
|
21
|
+
[
|
|
22
|
+
['--list', flagOn(args, '--list')],
|
|
23
|
+
['--create', flagOn(args, '--create')],
|
|
24
|
+
['--show', flagOn(args, '--show')],
|
|
25
|
+
],
|
|
26
|
+
'tenant',
|
|
27
|
+
'例:xinhe-oa-cli tenant --list --json',
|
|
28
|
+
);
|
|
29
|
+
if (action === '--list') return list(runtime);
|
|
30
|
+
if (action === '--create') return create(runtime, args);
|
|
31
|
+
return show(runtime, flagValue(args, '--show') ?? '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `POST /admin/tenants` 还没有时的说明(Agent B 的接口)。
|
|
36
|
+
*
|
|
37
|
+
* 判据照抄 Rust:**404**,或 **500 + `internal.error`** —— 后者是"旧服务端没挂这条
|
|
38
|
+
* 路由、框架兜底报 500"的常见样子。命中时保留原 code 与 request_id(排障要),
|
|
39
|
+
* 但换掉 message/hint:这不是调用方的参数错,而是服务端版本不对。
|
|
40
|
+
*/
|
|
41
|
+
function notReady(err) {
|
|
42
|
+
if (
|
|
43
|
+
err instanceof CliError &&
|
|
44
|
+
(err.httpStatus === 404 || (err.httpStatus === 500 && err.code === 'internal.error'))
|
|
45
|
+
) {
|
|
46
|
+
const out = new CliError(
|
|
47
|
+
Kind.Server,
|
|
48
|
+
err.code,
|
|
49
|
+
'服务端还没有 `POST /admin/tenants`(建企业接口未就绪)',
|
|
50
|
+
);
|
|
51
|
+
out.hint =
|
|
52
|
+
'先让服务端升级到带队建企业接口的版本;或在服务端机器上跑 ' +
|
|
53
|
+
'`python scripts/add_tenant.py` 建企业,再用 `xinhe-oa-cli tenant --list` 确认';
|
|
54
|
+
out.requestId = err.requestId ?? null;
|
|
55
|
+
out.httpStatus = err.httpStatus;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
return err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 已认证请求 + 未就绪说明(`tenant` 三个动作共用)。 */
|
|
62
|
+
async function sendTenant(runtime, method, path, spec) {
|
|
63
|
+
try {
|
|
64
|
+
return await runtime.sendAuthed(method, path, spec);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw notReady(err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function list(runtime) {
|
|
71
|
+
const response = await sendTenant(runtime, 'GET', '/admin/tenants', requestSpec());
|
|
72
|
+
const data = dataOf(response);
|
|
73
|
+
// 服务端可能回 `{data: {tenants: [...]}}`,也可能把清单直接塞在 `data` 里:
|
|
74
|
+
// Rust 是 `tenants → items → 整个 data` 三级兜底,这里逐级照抄(顺序不能换,
|
|
75
|
+
// `items` 只是旧版形状的兼容)。
|
|
76
|
+
const tenants = hasKey(data, 'tenants')
|
|
77
|
+
? data.tenants
|
|
78
|
+
: hasKey(data, 'items')
|
|
79
|
+
? data.items
|
|
80
|
+
: data;
|
|
81
|
+
// `total` 优先信服务端;它没给就数一数(数不出来算 0)——
|
|
82
|
+
// "列表被截断了"这件事只有服务端知道,本地数字不能反过来覆盖它。
|
|
83
|
+
const total = hasKey(data, 'total')
|
|
84
|
+
? data.total
|
|
85
|
+
: Array.isArray(tenants)
|
|
86
|
+
? tenants.length
|
|
87
|
+
: 0;
|
|
88
|
+
runtime.finishFrom(response, { tenants, total });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function create(runtime, args) {
|
|
92
|
+
// `--id` 与 `--name` 都在本地判:它们是"这条命令能不能成立"的前提,
|
|
93
|
+
// 交给服务端去报 422 只会把一次用法错误变成一次网络往返。
|
|
94
|
+
const tenantId = flagValue(args, '--id');
|
|
95
|
+
if (tenantId === null) {
|
|
96
|
+
throw CliError.usage('`tenant --create` 需要 --id <TENANT_ID>').withHint(
|
|
97
|
+
'例:xinhe-oa-cli tenant --create --id kashu --name 开叔企业 --store S01:总院',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const name = flagValue(args, '--name');
|
|
101
|
+
if (name === null) {
|
|
102
|
+
throw CliError.usage('`tenant --create` 需要 --name <显示名>').withHint(
|
|
103
|
+
'例:xinhe-oa-cli tenant --create --id kashu --name 开叔企业',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// `--store S01:总院` → `[{"code": "S01", "name": "总院"}]`;**不带冒号就只给 code**
|
|
108
|
+
// (显示名由服务端兜底)。
|
|
109
|
+
//
|
|
110
|
+
// 为什么按**第一个冒号**拆:门店编码是 `S01` 这种大写字母+数字的短码,里面不会
|
|
111
|
+
// 有冒号;而显示名(`总院:东区`)里可能有 —— 所以 `split_once(':')`(JS 里就是
|
|
112
|
+
// 第一个 `indexOf(':')`)才是对的,从右边拆会把名字切碎。
|
|
113
|
+
//
|
|
114
|
+
// 字段名是 **`code` 不是 `id`**:服务端拿它拼门店 id `<tenant>-<CODE>`
|
|
115
|
+
// (`services/tenants.py` 的 `normalize_stores` / `store_id_for`)。发 `id` 会得到
|
|
116
|
+
// 一个空 code 的 `request.invalid`,而错误信息只说"门店编码只能是大写字母…",
|
|
117
|
+
// 看不出是字段名错了 —— 这是两条并行线真实踩过的一次集成失败。
|
|
118
|
+
//
|
|
119
|
+
// `--store` 可重复(clap 的 `ArgAction::Append`):一家企业开业就有多个门店,
|
|
120
|
+
// 一次建企业要把它们一起写进去,否则得再补一次调用(而 `--create` 幂等键会变)。
|
|
121
|
+
const stores = [];
|
|
122
|
+
for (const raw of flagList(args, '--store')) {
|
|
123
|
+
const text = String(raw);
|
|
124
|
+
const colon = text.indexOf(':');
|
|
125
|
+
const code = (colon >= 0 ? text.slice(0, colon) : text).trim();
|
|
126
|
+
const label = colon >= 0 ? text.slice(colon + 1).trim() : null;
|
|
127
|
+
// 空编码等于让服务端去拼一个 `<tenant>-` 出来 —— 本地就拦,别让它变成脏数据。
|
|
128
|
+
if (code.length === 0) {
|
|
129
|
+
throw CliError.usage(`--store 里的门店编码不能为空(${JSON.stringify(text)})`);
|
|
130
|
+
}
|
|
131
|
+
stores.push(label !== null && label.length > 0 ? { code, name: label } : { code });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const body = {
|
|
135
|
+
id: tenantId,
|
|
136
|
+
name,
|
|
137
|
+
stores,
|
|
138
|
+
note: flagValue(args, '--description'),
|
|
139
|
+
};
|
|
140
|
+
// 写操作必带幂等键(契约 §3.4):网络抖动重发不会建出第二家企业。
|
|
141
|
+
// `--idempotency-key` 只是覆盖口(当前命令面还没登记这个旗标,取到的恒为 null)。
|
|
142
|
+
const spec = requestSpec({
|
|
143
|
+
body,
|
|
144
|
+
idempotencyKey: idempotencyKey('tenant --create', flagValue(args, '--idempotency-key')),
|
|
145
|
+
});
|
|
146
|
+
if (runtime.dryRun) return runtime.dryRunReport('POST', '/admin/tenants', spec);
|
|
147
|
+
|
|
148
|
+
const response = await sendTenant(runtime, 'POST', '/admin/tenants', spec);
|
|
149
|
+
const data = dataOf(response);
|
|
150
|
+
runtime.finishFrom(response, {
|
|
151
|
+
tenant: hasKey(data, 'tenant') ? data.tenant : data,
|
|
152
|
+
// 建企业只是第一步:没人有权限的话,这家企业谁都看不见。
|
|
153
|
+
// 下一步写成可直接照抄的一条命令,省掉一次翻文档。
|
|
154
|
+
next_step:
|
|
155
|
+
'给企业负责人赋角:xinhe-oa-cli member --create --phone <号码> --name <姓名> ' +
|
|
156
|
+
`--role tenant_owner --tenant ${tenantId}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function show(runtime, tenantId) {
|
|
161
|
+
// 与 Rust 一致:**不做 URL 编码**。租户 id 的字符集由服务端校验
|
|
162
|
+
// (小写字母/数字/下划线),本地擅自转义只会让路径与服务端审计里的那一行对不上。
|
|
163
|
+
const response = await sendTenant(runtime, 'GET', `/admin/tenants/${tenantId}`, requestSpec());
|
|
164
|
+
runtime.finishFrom(response, dataOf(response));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 把 `--tenant` 的缺省值处理成"你的唯一可见企业":先 `GET /me` 拿 `scope.tenants`。
|
|
169
|
+
*
|
|
170
|
+
* 为什么要有它:`member` / `file` / `investor --endorse` 这些命令都要求"给哪家企业办事",
|
|
171
|
+
* 而绝大多数用户只有一家可见企业 —— 每次都逼人写 `--tenant` 是纯粹的摩擦。
|
|
172
|
+
* 但**多企业时必须报错**,不能替用户挑一个:挑错了就是往别人的企业里写数据。
|
|
173
|
+
*
|
|
174
|
+
* 三种结果:显式 `--tenant`(trim 后非空)> 唯一可见企业 > `null`(没有可见企业)。
|
|
175
|
+
* 多个可见企业是**用法错误**(退出码 2、不发写请求),并把清单塞进 `details`
|
|
176
|
+
* 让调用方(或 LLM)能直接挑一个。
|
|
177
|
+
*/
|
|
178
|
+
export async function resolveTenant(runtime, requested) {
|
|
179
|
+
const trimmed = requested === null || requested === undefined ? '' : String(requested).trim();
|
|
180
|
+
if (trimmed.length > 0) return trimmed;
|
|
181
|
+
|
|
182
|
+
const response = await runtime.sendAuthed('GET', '/me', requestSpec());
|
|
183
|
+
const data = dataOf(response);
|
|
184
|
+
const tenants = Array.isArray(data?.scope?.tenants) ? data.scope.tenants : [];
|
|
185
|
+
if (tenants.length === 1) {
|
|
186
|
+
const id = tenants[0]?.id;
|
|
187
|
+
return typeof id === 'string' ? id : null;
|
|
188
|
+
}
|
|
189
|
+
if (tenants.length === 0) return null;
|
|
190
|
+
throw CliError.usage('你有多个可见企业,请用 --tenant <TENANT_ID> 指明').withDetails({
|
|
191
|
+
visible_tenants: tenants,
|
|
192
|
+
hint: 'xinhe-oa-cli tenant --list',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 键存在就取(`null` 也算存在)—— 对齐 Rust `Value::get`:**不能**用 `??` 兜底,
|
|
197
|
+
* 否则服务端显式回的 `null` 会被当成"没这个键"而套上默认值。 */
|
|
198
|
+
function hasKey(value, key) {
|
|
199
|
+
return (
|
|
200
|
+
value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// 工作流:`workflow --list|--runs|--create|--run`(契约 §2)。
|
|
2
|
+
//
|
|
3
|
+
// `--run` 是 `high-risk-write`:它**按触发人(就是你)的身份真的执行动作** ——
|
|
4
|
+
// 建记录、改记录、发通知、跑 Skill,都会留下真实的写入与审计(docs/26 §3)。
|
|
5
|
+
// 定时/事件触发的运行有 `(workflow_id, trigger_event_id)` 唯一约束兜底,手动触发没有
|
|
6
|
+
// "事件"可去重,所以必须 `--yes`;不给就本地退出码 2,一个请求都不发。
|
|
7
|
+
//
|
|
8
|
+
// 另外两条刻意如此的口径:
|
|
9
|
+
//
|
|
10
|
+
// 1. **触发器与动作只收 JSON 字面量,CLI 不发明 DSL**:服务端登记的就是五类触发器
|
|
11
|
+
// (record_created / record_updated / status_changed / manual_confirmed / schedule)
|
|
12
|
+
// 与四类动作(create_record / update_record / notify / run_skill),CLI 自己再抄一份
|
|
13
|
+
// 枚举只会随时间漂移。合法性在**本地**只判两件事:是不是合法 JSON、清单空不空。
|
|
14
|
+
// 2. `--run` 的执行身份是**自然人**,不是"CLI 这个程序":越权会记 `failed` 且没有任何
|
|
15
|
+
// 写入(`workflow.permission_denied`)—— 这条写进响应的 note,因为调用方(尤其是
|
|
16
|
+
// Agent)需要知道"失败了不等于什么都没发生"的边界在哪。
|
|
17
|
+
|
|
18
|
+
import { CliError, exactlyOne } from '../core/error.js';
|
|
19
|
+
import { flagList, flagOn, flagValue } from '../core/args.js';
|
|
20
|
+
import { dataOf, idempotencyKey, requestSpec } from '../core/runtime.js';
|
|
21
|
+
import { randomHex, sortKeysDeep } from '../core/util.js';
|
|
22
|
+
import { queryPairs } from '../core/values.js';
|
|
23
|
+
|
|
24
|
+
/** `workflow --list|--runs|--create|--run` 的分发。 */
|
|
25
|
+
export async function handleWorkflow({ args, runtime }) {
|
|
26
|
+
const action = exactlyOne(
|
|
27
|
+
[
|
|
28
|
+
['--list', flagOn(args, '--list')],
|
|
29
|
+
['--runs', flagValue(args, '--runs') !== null],
|
|
30
|
+
['--run', flagValue(args, '--run') !== null],
|
|
31
|
+
['--create', flagOn(args, '--create')],
|
|
32
|
+
],
|
|
33
|
+
'workflow',
|
|
34
|
+
'例:xinhe-oa-cli workflow --list --json',
|
|
35
|
+
);
|
|
36
|
+
if (action === '--list') return list(runtime, args);
|
|
37
|
+
if (action === '--runs') return runs(runtime, flagValue(args, '--runs'));
|
|
38
|
+
if (action === '--run') return runOnce(runtime, args, flagValue(args, '--run'));
|
|
39
|
+
return create(runtime, args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function list(runtime, args) {
|
|
43
|
+
// `--base` 空值不进 query,非空值原样发(与 Rust 的 `query_pair` 同义)。
|
|
44
|
+
const query = queryPairs([['base_id', flagValue(args, '--base')]]);
|
|
45
|
+
const response = await runtime.sendAuthed('GET', '/workflows', requestSpec({ query }));
|
|
46
|
+
const data = dataOf(response);
|
|
47
|
+
runtime.finishFrom(response, {
|
|
48
|
+
workflows: hasKey(data, 'workflows') ? data.workflows : [],
|
|
49
|
+
total: hasKey(data, 'total') ? data.total : null,
|
|
50
|
+
next_step: '看运行历史:xinhe-oa-cli workflow --runs <WORKFLOW_ID> --json',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runs(runtime, workflowId) {
|
|
55
|
+
const response = await runtime.sendAuthed(
|
|
56
|
+
'GET',
|
|
57
|
+
`/workflows/${workflowId}/runs`,
|
|
58
|
+
requestSpec(),
|
|
59
|
+
);
|
|
60
|
+
const data = dataOf(response);
|
|
61
|
+
runtime.finishFrom(response, {
|
|
62
|
+
runs: hasKey(data, 'runs') ? data.runs : [],
|
|
63
|
+
total: hasKey(data, 'total') ? data.total : null,
|
|
64
|
+
// 失败原因在每条 run 的 `error` 里,最常见的两条语义差别很大,所以写在 note 里:
|
|
65
|
+
// `workflow.permission_denied` 是**触发人没权限**(没有任何写入副作用)。
|
|
66
|
+
note: '失败的原因在 error 里:workflow.permission_denied = 触发人对目标表没写权限(**没有任何写入副作用**)',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 手动跑一次(高风险:必须 `--yes`)。 */
|
|
71
|
+
async function runOnce(runtime, args, workflowId) {
|
|
72
|
+
runtime.requireYes(
|
|
73
|
+
'workflow --run(会按你的身份真的执行工作流动作)',
|
|
74
|
+
`xinhe-oa-cli workflow --run ${workflowId}`,
|
|
75
|
+
);
|
|
76
|
+
const path = `/workflows/${workflowId}/run`;
|
|
77
|
+
// 写操作必带幂等键:手动触发重发两次不会跑出两条运行(服务端按幂等键去重)。
|
|
78
|
+
const spec = requestSpec({
|
|
79
|
+
idempotencyKey: idempotencyKey('workflow --run', flagValue(args, '--idempotency-key')),
|
|
80
|
+
});
|
|
81
|
+
if (runtime.dryRun) return runtime.dryRunReport('POST', path, spec);
|
|
82
|
+
|
|
83
|
+
const response = await runtime.sendAuthed('POST', path, spec);
|
|
84
|
+
runtime.finishFrom(response, {
|
|
85
|
+
run: dataOf(response),
|
|
86
|
+
note: '工作流用**触发它的自然人**的身份执行;越权会记 failed 且没有任何写入',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function create(runtime, args) {
|
|
91
|
+
const baseId = flagValue(args, '--base');
|
|
92
|
+
if (baseId === null) {
|
|
93
|
+
throw CliError.usage('`workflow --create` 需要 --base <BASE_ID>');
|
|
94
|
+
}
|
|
95
|
+
const name = flagValue(args, '--name');
|
|
96
|
+
if (name === null) {
|
|
97
|
+
throw CliError.usage('`workflow --create` 需要 --name <工作流名>');
|
|
98
|
+
}
|
|
99
|
+
const triggers = flagList(args, '--trigger');
|
|
100
|
+
if (triggers.length === 0) {
|
|
101
|
+
throw CliError.usage('`workflow --create` 需要至少一个 --trigger <JSON>').withHint(
|
|
102
|
+
'触发器五类之一:{"type":"record_created","table_id":"tbl_1"} / {"type":"record_updated",...} / {"type":"status_changed",...} / {"type":"manual_confirmed"} / {"type":"schedule","cron":"0 9 * * *","timezone":"Asia/Shanghai"}',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const actions = flagList(args, '--action-json');
|
|
106
|
+
if (actions.length === 0) {
|
|
107
|
+
throw CliError.usage('`workflow --create` 需要至少一个 --action-json <JSON>').withHint(
|
|
108
|
+
'动作四类之一:create_record / update_record / notify / run_skill(都是 {"type":...} 的对象)',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
// 两个清单都先**整体**校验再发请求:触发器合法、动作非法时不该产生半个工作流。
|
|
112
|
+
const parsedTriggers = parseJsonList(triggers, '--trigger');
|
|
113
|
+
const parsedActions = parseJsonList(actions, '--action-json');
|
|
114
|
+
// 机器名创建后不可变:显式 `--key` 优先,否则从显示名推导,推不出来用 `w<8 位 hex>`。
|
|
115
|
+
const key = flagValue(args, '--key') ?? deriveKey(name) ?? `w${randomHex().slice(0, 8)}`;
|
|
116
|
+
// 键序按字典序(serde_json 是 BTreeMap),连请求体的字节面都与 Rust 对齐。
|
|
117
|
+
const body = {
|
|
118
|
+
actions: parsedActions,
|
|
119
|
+
base_id: baseId,
|
|
120
|
+
key,
|
|
121
|
+
name,
|
|
122
|
+
triggers: parsedTriggers,
|
|
123
|
+
};
|
|
124
|
+
const spec = requestSpec({
|
|
125
|
+
body,
|
|
126
|
+
idempotencyKey: idempotencyKey('workflow --create', flagValue(args, '--idempotency-key')),
|
|
127
|
+
});
|
|
128
|
+
if (runtime.dryRun) return runtime.dryRunReport('POST', '/workflows', spec);
|
|
129
|
+
|
|
130
|
+
const response = await runtime.sendAuthed('POST', '/workflows', spec);
|
|
131
|
+
runtime.finishFrom(response, { workflow: dataOf(response) });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `--trigger` / `--action-json` 的 JSON 字面量解析(Rust `parse_json_list` 同义)。
|
|
136
|
+
*
|
|
137
|
+
* 只在这里判"是不是 JSON":**任何**合法 JSON 都收(对象、字符串、数字……),
|
|
138
|
+
* 因为它们原样进请求体,形状合不合法是服务端的事(五类触发器/四类动作是它的枚举)。
|
|
139
|
+
*/
|
|
140
|
+
function parseJsonList(items, flag) {
|
|
141
|
+
return items.map((item) => {
|
|
142
|
+
let value;
|
|
143
|
+
try {
|
|
144
|
+
value = JSON.parse(String(item));
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw CliError.usage(`${flag} 里不是合法 JSON:${err.message}`).withHint(
|
|
147
|
+
`例:${flag} '{"type":"record_created","table_id":"tbl_1"}'`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
// 键序与 Rust 的 serde_json 一致(它反序列化成 BTreeMap,再序列化就是字典序)。
|
|
151
|
+
return sortKeysDeep(value);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 由显示名推导机器名 —— 与 Rust `bitable::derive_key` 逐条同义(详见 dashboard.js 的同名函数:
|
|
157
|
+
* 那边已经解释了"为什么中文名推不出来时宁可随机也不硬编")。
|
|
158
|
+
*/
|
|
159
|
+
function deriveKey(name) {
|
|
160
|
+
let key = '';
|
|
161
|
+
for (const ch of String(name ?? '')) {
|
|
162
|
+
if (/^[A-Za-z0-9]$/.test(ch)) key += ch.toLowerCase();
|
|
163
|
+
else if (ch.codePointAt(0) < 128) key += '_';
|
|
164
|
+
}
|
|
165
|
+
key = key.replace(/_{2,}/g, '_').replace(/^_+|_+$/g, '');
|
|
166
|
+
if (key.length > 48) key = key.slice(0, 48);
|
|
167
|
+
if (key.length === 0 || !/^[a-z]/.test(key)) return null;
|
|
168
|
+
return key;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 键存在就取(`null` 也算存在)—— 对齐 Rust `Value::get`。 */
|
|
172
|
+
function hasKey(value, key) {
|
|
173
|
+
return (
|
|
174
|
+
value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key)
|
|
175
|
+
);
|
|
176
|
+
}
|