@lasting-beauty/chat-hub 0.1.0-beta.1

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/cli/deploy.mjs ADDED
@@ -0,0 +1,569 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
+ import {
3
+ chmodSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ statSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
13
+ import { ipHttpsCaddySite } from './ip-certificate.mjs';
14
+
15
+ export const supportedDeploymentDistributions = Object.freeze([
16
+ 'Ubuntu 24.04 LTS',
17
+ 'Alibaba Cloud Linux 4',
18
+ ]);
19
+
20
+ export const verifiedPlatformCliVersionPatterns = Object.freeze({
21
+ 'lark-cli': /^(?:lark-cli version )?1\.0\.92(?:\s|$)/,
22
+ dws: /^(?:dws version )?v?1\.0\.(?:60|61)(?:\s|$)/,
23
+ });
24
+
25
+ export const alinuxCaddyRelease = Object.freeze({
26
+ version: '2.11.4',
27
+ archiveUrl:
28
+ 'https://github.com/caddyserver/caddy/releases/download/v2.11.4/caddy_2.11.4_linux_amd64.tar.gz',
29
+ archiveSha256: '527fbf917c39189a1e3b31d34fa955601680b2d5c8055d2a87b8b9588dec7bb9',
30
+ binaryPath: '/usr/local/bin/caddy',
31
+ servicePath: '/etc/systemd/system/caddy.service',
32
+ });
33
+
34
+ export const deploymentPaths = Object.freeze({
35
+ data: '/var/lib/chat-hub',
36
+ configuration: '/etc/chat-hub',
37
+ credentials: '/etc/chat-hub/credentials',
38
+ backups: '/var/backups/chat-hub',
39
+ runtime: '/run/chat-hub',
40
+ runtimeConfig: '/etc/chat-hub/runtime.json',
41
+ manifest: '/etc/chat-hub/install.json',
42
+ service: '/etc/systemd/system/chat-hub.service',
43
+ caddyfile: '/etc/caddy/Caddyfile',
44
+ caddySnippet: '/etc/caddy/chat-hub.caddy',
45
+ masterKey: '/etc/chat-hub/credentials/chat-hub-master-key',
46
+ adminBootstrap: '/etc/chat-hub/credentials/chat-hub-admin-bootstrap',
47
+ });
48
+
49
+ const caddyImport = 'import /etc/caddy/chat-hub.caddy';
50
+
51
+ export function createDeploymentPlan(
52
+ publicHost,
53
+ installDependencies,
54
+ platform,
55
+ architecture,
56
+ deploymentHost,
57
+ publicScheme = 'https',
58
+ certificateMode = publicScheme === 'https' ? 'managed-domain' : 'none',
59
+ ) {
60
+ const supported = deploymentHost?.supported ?? (platform === 'linux' && architecture === 'x64');
61
+ const secure = publicScheme === 'https';
62
+ return {
63
+ mode: 'plan',
64
+ applied: false,
65
+ domain: publicHost,
66
+ publicHost,
67
+ publicScheme,
68
+ certificateMode,
69
+ publicUrl: `${publicScheme}://${publicHost}`,
70
+ target: {
71
+ platform: 'linux',
72
+ architecture: 'x64',
73
+ distributions: supportedDeploymentDistributions,
74
+ },
75
+ detectedHost: deploymentHost,
76
+ currentPlatformSupported: supported,
77
+ requiresRootToApply: true,
78
+ installDependencies,
79
+ ports: {
80
+ public: secure ? [80, 443] : [80],
81
+ gateway: '127.0.0.1:8080',
82
+ application: '127.0.0.1:4310',
83
+ },
84
+ paths: deploymentPaths,
85
+ changes: [
86
+ '创建专用 chat-hub 系统用户和受限目录',
87
+ '创建 root 管理的 32 字节主密钥并通过 systemd credential 注入',
88
+ `生成仅显示一次、10 分钟有效的管理员 ${publicScheme.toUpperCase()} 初始化链接`,
89
+ '安装并启用 Chat Hub systemd 服务,应用与安全网关只监听回环地址',
90
+ secure
91
+ ? certificateMode === 'ip-shortlived'
92
+ ? `使用 Certbot shortlived 配置 ${publicHost} 的 IP HTTPS、自动续期并反向代理到安全网关`
93
+ : `配置 Caddy 为 ${publicHost} 申请 HTTPS 并反向代理到安全网关`
94
+ : `配置 Caddy 通过 http://${publicHost} 反向代理到安全网关(不启用 TLS)`,
95
+ ...(installDependencies
96
+ ? [
97
+ '允许在 Ubuntu 通过 apt、在 Alibaba Cloud Linux 4 通过校验过的官方静态资产安装缺失的 Caddy;包内飞书/钉钉 CLI 仍做固定版本校验',
98
+ ]
99
+ : ['只检查 Caddy 和包内固定版本平台 CLI,不自动安装系统依赖']),
100
+ ],
101
+ warnings: secure
102
+ ? certificateMode === 'ip-shortlived'
103
+ ? [
104
+ 'IP 证书约 160 小时有效,续期 timer 和成功后 Caddy reload 必须持续正常',
105
+ '公网 IP 变化会使入口失效;正式实例应绑定稳定 EIP',
106
+ ]
107
+ : []
108
+ : [
109
+ 'HTTP 不加密管理员密码、会话、平台数据和模型配置,只适合临时受控测试',
110
+ '阿里云安全组必须把 TCP 80 来源限制为管理员固定公网 IP,不能开放 0.0.0.0/0',
111
+ '迁移到域名 HTTPS 前,不应处理真实工作数据或配置真实平台与模型凭证',
112
+ ],
113
+ blockers: ['公司许可证与公开 npm 发布许可尚未完成;不影响本地 tarball 内部部署'],
114
+ };
115
+ }
116
+
117
+ export function detectDeploymentHost({
118
+ platform = process.platform,
119
+ architecture = process.arch,
120
+ readOsRelease = () => readFileSync('/etc/os-release', 'utf8'),
121
+ } = {}) {
122
+ if (platform !== 'linux' || architecture !== 'x64') {
123
+ return {
124
+ platform,
125
+ architecture,
126
+ supported: false,
127
+ distribution: 'unsupported',
128
+ label: `${platform}-${architecture}`,
129
+ };
130
+ }
131
+ let values;
132
+ try {
133
+ values = parseOsRelease(readOsRelease());
134
+ } catch (error) {
135
+ return {
136
+ platform,
137
+ architecture,
138
+ supported: false,
139
+ distribution: 'unknown',
140
+ label: '未知 Linux 发行版',
141
+ error: error instanceof Error ? error.message : String(error),
142
+ };
143
+ }
144
+ const common = {
145
+ platform,
146
+ architecture,
147
+ id: values.ID,
148
+ versionId: values.VERSION_ID,
149
+ prettyName: values.PRETTY_NAME,
150
+ };
151
+ if (values.ID === 'ubuntu' && values.VERSION_ID === '24.04') {
152
+ return {
153
+ ...common,
154
+ supported: true,
155
+ distribution: 'ubuntu-24.04',
156
+ label: values.PRETTY_NAME || 'Ubuntu 24.04 LTS',
157
+ caddyInstaller: 'apt',
158
+ };
159
+ }
160
+ if (values.ID === 'alinux' && /^4(?:\.|$)/.test(values.VERSION_ID ?? '')) {
161
+ return {
162
+ ...common,
163
+ supported: true,
164
+ distribution: 'alinux-4',
165
+ label: values.PRETTY_NAME || 'Alibaba Cloud Linux 4',
166
+ caddyInstaller: 'verified-static',
167
+ };
168
+ }
169
+ return {
170
+ ...common,
171
+ supported: false,
172
+ distribution: 'unsupported',
173
+ label: values.PRETTY_NAME || `${values.ID ?? 'linux'} ${values.VERSION_ID ?? ''}`.trim(),
174
+ };
175
+ }
176
+
177
+ export function assertSupportedDeploymentHost({
178
+ commandName = 'deploy --apply',
179
+ getUid = () => (typeof process.getuid === 'function' ? process.getuid() : undefined),
180
+ ...detectionOptions
181
+ } = {}) {
182
+ const host = detectDeploymentHost(detectionOptions);
183
+ if (!host.supported) {
184
+ throw new Error(
185
+ `${commandName} 只支持 Ubuntu 24.04 x64 或 Alibaba Cloud Linux 4 x64;当前为 ${host.label}`,
186
+ );
187
+ }
188
+ if (getUid() !== 0) throw new Error(`${commandName} 需要通过 sudo 或 root 执行`);
189
+ return host;
190
+ }
191
+
192
+ export function parseOsRelease(content) {
193
+ if (typeof content !== 'string') throw new Error('/etc/os-release 内容无效');
194
+ return Object.fromEntries(
195
+ content
196
+ .split(/\r?\n/)
197
+ .filter((line) => line && !line.startsWith('#') && line.includes('='))
198
+ .map((line) => {
199
+ const separator = line.indexOf('=');
200
+ return [line.slice(0, separator), line.slice(separator + 1).replace(/^"|"$/g, '')];
201
+ }),
202
+ );
203
+ }
204
+
205
+ export function alinuxCaddySystemdUnit() {
206
+ return `[Unit]
207
+ Description=Caddy
208
+ Documentation=https://caddyserver.com/docs/
209
+ After=network.target network-online.target
210
+ Requires=network-online.target
211
+
212
+ [Service]
213
+ Type=notify
214
+ User=caddy
215
+ Group=caddy
216
+ Environment=HOME=/var/lib/caddy
217
+ Environment=XDG_DATA_HOME=/var/lib/caddy/.local/share
218
+ Environment=XDG_CONFIG_HOME=/var/lib/caddy/.config
219
+ ExecStart=${alinuxCaddyRelease.binaryPath} run --environ --config /etc/caddy/Caddyfile --adapter caddyfile
220
+ ExecReload=${alinuxCaddyRelease.binaryPath} reload --config /etc/caddy/Caddyfile --adapter caddyfile --force
221
+ TimeoutStopSec=5s
222
+ LimitNOFILE=1048576
223
+ NoNewPrivileges=true
224
+ PrivateTmp=true
225
+ ProtectSystem=full
226
+ ProtectHome=true
227
+ CapabilityBoundingSet=CAP_NET_BIND_SERVICE
228
+ AmbientCapabilities=CAP_NET_BIND_SERVICE
229
+ ReadWritePaths=/var/lib/caddy
230
+
231
+ [Install]
232
+ WantedBy=multi-user.target
233
+ `;
234
+ }
235
+
236
+ export function stageDeployment({
237
+ domain,
238
+ publicHost = domain,
239
+ publicScheme = 'https',
240
+ certificateMode = publicScheme === 'https' ? 'managed-domain' : 'none',
241
+ packageRoot,
242
+ nodePath,
243
+ rootDirectory = '/',
244
+ now = Date.now(),
245
+ bootstrapToken = randomBytes(32).toString('base64url'),
246
+ larkCliPath = '/usr/local/bin/lark-cli',
247
+ dingtalkCliPath = '/usr/local/bin/dws',
248
+ preserveAdministratorCredential = false,
249
+ }) {
250
+ assertSafeDeploymentInput(
251
+ publicHost,
252
+ packageRoot,
253
+ nodePath,
254
+ rootDirectory,
255
+ bootstrapToken,
256
+ larkCliPath,
257
+ dingtalkCliPath,
258
+ );
259
+ const expiresAt = now + 10 * 60_000;
260
+ const outputPath = (absolutePath) => rootedPath(rootDirectory, absolutePath);
261
+ for (const directory of [
262
+ deploymentPaths.data,
263
+ deploymentPaths.configuration,
264
+ deploymentPaths.credentials,
265
+ deploymentPaths.backups,
266
+ dirname(deploymentPaths.service),
267
+ dirname(deploymentPaths.caddyfile),
268
+ ]) {
269
+ mkdirSync(outputPath(directory), {
270
+ recursive: true,
271
+ mode: directory.includes('credentials') ? 0o700 : 0o755,
272
+ });
273
+ }
274
+ chmodSync(outputPath(deploymentPaths.configuration), 0o700);
275
+ chmodSync(outputPath(deploymentPaths.credentials), 0o700);
276
+ chmodSync(outputPath(deploymentPaths.data), 0o700);
277
+ chmodSync(outputPath(deploymentPaths.backups), 0o700);
278
+
279
+ const masterKeyPath = outputPath(deploymentPaths.masterKey);
280
+ if (existsSync(masterKeyPath)) {
281
+ const metadata = statSync(masterKeyPath);
282
+ if (!metadata.isFile() || readFileSync(masterKeyPath).length !== 32) {
283
+ throw new Error('现有 Chat Hub 主密钥不是 32 字节普通文件,已停止部署');
284
+ }
285
+ } else {
286
+ writeFileSync(masterKeyPath, randomBytes(32), { flag: 'wx', mode: 0o600 });
287
+ }
288
+ chmodSync(masterKeyPath, 0o600);
289
+ const administratorCredentialPath = outputPath(deploymentPaths.adminBootstrap);
290
+ if (preserveAdministratorCredential) {
291
+ if (
292
+ !existsSync(administratorCredentialPath) ||
293
+ !statSync(administratorCredentialPath).isFile()
294
+ ) {
295
+ throw new Error('升级要求保留现有管理员凭证,但凭证文件不存在');
296
+ }
297
+ } else {
298
+ writePrivateJson(administratorCredentialPath, {
299
+ token: bootstrapToken,
300
+ expiresAt,
301
+ reset: false,
302
+ });
303
+ }
304
+
305
+ if (!['http', 'https'].includes(publicScheme)) throw new Error('公网协议必须是 http 或 https');
306
+ const runtimeConfig = buildRuntimeConfig(
307
+ publicHost,
308
+ publicScheme,
309
+ packageRoot,
310
+ larkCliPath,
311
+ dingtalkCliPath,
312
+ );
313
+ const artifacts = new Map([
314
+ [deploymentPaths.runtimeConfig, `${JSON.stringify(runtimeConfig, null, 2)}\n`],
315
+ [deploymentPaths.service, systemdUnit(nodePath, packageRoot)],
316
+ [deploymentPaths.caddySnippet, caddySite(publicHost, publicScheme, certificateMode)],
317
+ [
318
+ deploymentPaths.manifest,
319
+ `${JSON.stringify(
320
+ {
321
+ version: 2,
322
+ domain: publicHost,
323
+ publicHost,
324
+ publicScheme,
325
+ certificateMode,
326
+ packageRoot,
327
+ installedAt: new Date(now).toISOString(),
328
+ paths: deploymentPaths,
329
+ },
330
+ null,
331
+ 2,
332
+ )}\n`,
333
+ ],
334
+ ]);
335
+ const caddyPath = outputPath(deploymentPaths.caddyfile);
336
+ const existingCaddyfile = existsSync(caddyPath) ? readFileSync(caddyPath, 'utf8') : '';
337
+ if (!existingCaddyfile.split(/\r?\n/).some((line) => line.trim() === caddyImport)) {
338
+ const separator = existingCaddyfile && !existingCaddyfile.endsWith('\n') ? '\n' : '';
339
+ artifacts.set(deploymentPaths.caddyfile, `${existingCaddyfile}${separator}${caddyImport}\n`);
340
+ }
341
+
342
+ const previous = new Map();
343
+ try {
344
+ for (const [absolutePath, content] of artifacts) {
345
+ const target = outputPath(absolutePath);
346
+ previous.set(
347
+ absolutePath,
348
+ existsSync(target)
349
+ ? { existed: true, content: readFileSync(target), mode: statSync(target).mode & 0o777 }
350
+ : { existed: false },
351
+ );
352
+ writeAtomic(target, content, absolutePath === deploymentPaths.runtimeConfig ? 0o600 : 0o644);
353
+ }
354
+ } catch (error) {
355
+ restoreFiles(previous, outputPath);
356
+ throw error;
357
+ }
358
+
359
+ return {
360
+ domain: publicHost,
361
+ publicHost,
362
+ publicScheme,
363
+ expiresAt,
364
+ initializationUrl: preserveAdministratorCredential
365
+ ? undefined
366
+ : `${publicScheme}://${publicHost}/?token=${encodeURIComponent(bootstrapToken)}`,
367
+ paths: deploymentPaths,
368
+ outputPaths: Object.fromEntries(
369
+ Object.entries(deploymentPaths).map(([name, absolutePath]) => [
370
+ name,
371
+ outputPath(absolutePath),
372
+ ]),
373
+ ),
374
+ rollback: () => restoreFiles(previous, outputPath),
375
+ };
376
+ }
377
+
378
+ export function stageAdministratorReset({
379
+ domain,
380
+ publicHost = domain,
381
+ publicScheme = 'https',
382
+ rootDirectory = '/',
383
+ now = Date.now(),
384
+ bootstrapToken = randomBytes(32).toString('base64url'),
385
+ }) {
386
+ if (typeof publicHost !== 'string' || !publicHost || /[\r\n\0]/.test(publicHost)) {
387
+ throw new Error('管理员重置域名无效');
388
+ }
389
+ if (!['http', 'https'].includes(publicScheme)) throw new Error('管理员重置协议无效');
390
+ if (!isAbsolute(rootDirectory)) throw new Error('部署根目录必须是绝对路径');
391
+ if (typeof bootstrapToken !== 'string' || bootstrapToken.length < 32) {
392
+ throw new Error('管理员重置令牌强度不足');
393
+ }
394
+ const expiresAt = now + 10 * 60_000;
395
+ const target = rootedPath(rootDirectory, deploymentPaths.adminBootstrap);
396
+ if (!existsSync(dirname(target))) throw new Error('Chat Hub 尚未部署,找不到凭证目录');
397
+ writePrivateJson(target, { token: bootstrapToken, expiresAt, reset: true });
398
+ return {
399
+ initializationUrl: `${publicScheme}://${publicHost}/?token=${encodeURIComponent(bootstrapToken)}`,
400
+ expiresAt,
401
+ credentialPath: target,
402
+ };
403
+ }
404
+
405
+ function buildRuntimeConfig(publicHost, publicScheme, packageRoot, larkCliPath, dingtalkCliPath) {
406
+ return {
407
+ id: createHash('sha256')
408
+ .update(`${deploymentPaths.data}:${publicScheme}:${publicHost}`)
409
+ .digest('hex')
410
+ .slice(0, 16),
411
+ runId: randomUUID(),
412
+ mode: 'live',
413
+ entryMode: 'public',
414
+ publicDomain: publicHost,
415
+ publicHost,
416
+ publicScheme,
417
+ packageRoot,
418
+ runtimeDirectory: deploymentPaths.runtime,
419
+ dataDirectory: deploymentPaths.data,
420
+ apiPort: 4310,
421
+ httpPort: 8080,
422
+ frontendPort: 4310,
423
+ phoneProtocol: publicScheme,
424
+ environment: {
425
+ CHAT_HUB_HOST: '127.0.0.1',
426
+ CHAT_HUB_PORT: '4310',
427
+ CHAT_HUB_DATA_DIR: deploymentPaths.data,
428
+ CHAT_HUB_STATIC_DIR: join(packageRoot, 'dist'),
429
+ CHAT_HUB_ACCOUNT_KEY: 'server-admin',
430
+ CHAT_HUB_AUTH_MODE: 'admin',
431
+ CHAT_HUB_PUBLIC_SCHEME: publicScheme,
432
+ LARK_CLI_PATH: larkCliPath,
433
+ DINGTALK_CLI_PATH: dingtalkCliPath,
434
+ },
435
+ };
436
+ }
437
+
438
+ function systemdUnit(nodePath, packageRoot) {
439
+ return `[Unit]
440
+ Description=Chat Hub personal work information service
441
+ After=network-online.target
442
+ Wants=network-online.target
443
+
444
+ [Service]
445
+ Type=simple
446
+ User=chat-hub
447
+ Group=chat-hub
448
+ WorkingDirectory=${deploymentPaths.data}
449
+ RuntimeDirectory=chat-hub
450
+ RuntimeDirectoryMode=0700
451
+ UMask=0077
452
+ Environment=HOME=${deploymentPaths.data}
453
+ Environment=XDG_CONFIG_HOME=${deploymentPaths.data}/.config
454
+ Environment=XDG_DATA_HOME=${deploymentPaths.data}/.local/share
455
+ LoadCredential=chat-hub-master-key:${deploymentPaths.masterKey}
456
+ LoadCredential=chat-hub-admin-bootstrap:${deploymentPaths.adminBootstrap}
457
+ ExecStart=${systemdQuote(nodePath)} ${systemdQuote(join(packageRoot, 'cli', 'runtime.mjs'))} ${systemdQuote(deploymentPaths.runtimeConfig)}
458
+ Restart=on-failure
459
+ RestartSec=5s
460
+ TimeoutStopSec=30s
461
+ NoNewPrivileges=true
462
+ PrivateTmp=true
463
+ ProtectSystem=strict
464
+ ProtectHome=true
465
+ ProtectKernelTunables=true
466
+ ProtectKernelModules=true
467
+ ProtectControlGroups=true
468
+ RestrictSUIDSGID=true
469
+ LockPersonality=true
470
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
471
+ ReadWritePaths=${deploymentPaths.data} ${deploymentPaths.backups}
472
+
473
+ [Install]
474
+ WantedBy=multi-user.target
475
+ `;
476
+ }
477
+
478
+ function caddySite(publicHost, publicScheme, certificateMode) {
479
+ if (certificateMode === 'ip-shortlived') return ipHttpsCaddySite(publicHost);
480
+ const siteAddress = publicScheme === 'http' ? `http://${publicHost}` : publicHost;
481
+ return `${siteAddress} {
482
+ encode zstd gzip
483
+ reverse_proxy 127.0.0.1:8080 {
484
+ header_up Host {host}
485
+ header_up X-Forwarded-Proto ${publicScheme}
486
+ header_up X-Forwarded-For {remote_host}
487
+ }
488
+ header {
489
+ -Server
490
+ X-Content-Type-Options nosniff
491
+ Referrer-Policy no-referrer
492
+ X-Frame-Options DENY
493
+ }
494
+ }
495
+ `;
496
+ }
497
+
498
+ function writePrivateJson(path, value) {
499
+ writeAtomic(path, `${JSON.stringify(value)}\n`, 0o600);
500
+ }
501
+
502
+ function writeAtomic(path, content, mode) {
503
+ const temporary = `${path}.${randomUUID()}.tmp`;
504
+ writeFileSync(temporary, content, { mode });
505
+ chmodSync(temporary, mode);
506
+ renameSync(temporary, path);
507
+ }
508
+
509
+ function restoreFiles(previous, outputPath) {
510
+ for (const [absolutePath, state] of [...previous.entries()].reverse()) {
511
+ const target = outputPath(absolutePath);
512
+ if (!state.existed) {
513
+ if (existsSync(target)) unlinkSync(target);
514
+ continue;
515
+ }
516
+ writeFileSync(target, state.content, { mode: state.mode });
517
+ chmodSync(target, state.mode);
518
+ }
519
+ }
520
+
521
+ function rootedPath(rootDirectory, absolutePath) {
522
+ const root = resolve(rootDirectory);
523
+ if (!isAbsolute(absolutePath)) throw new Error('部署目标路径必须是绝对路径');
524
+ if (root === resolve('/')) return absolutePath;
525
+ const target = resolve(root, absolutePath.replace(/^[/\\]+/, ''));
526
+ const pathFromRoot = relative(root, target);
527
+ if (pathFromRoot.startsWith('..') || isAbsolute(pathFromRoot))
528
+ throw new Error('部署目标越出测试根目录');
529
+ return target;
530
+ }
531
+
532
+ function assertSafeDeploymentInput(
533
+ domain,
534
+ packageRoot,
535
+ nodePath,
536
+ rootDirectory,
537
+ bootstrapToken,
538
+ larkCliPath,
539
+ dingtalkCliPath,
540
+ ) {
541
+ for (const [name, value] of Object.entries({
542
+ domain,
543
+ packageRoot,
544
+ nodePath,
545
+ rootDirectory,
546
+ larkCliPath,
547
+ dingtalkCliPath,
548
+ })) {
549
+ if (typeof value !== 'string' || !value || /[\r\n\0]/.test(value)) {
550
+ throw new Error(`${name} 包含无效字符`);
551
+ }
552
+ }
553
+ if (
554
+ !isAbsolute(packageRoot) ||
555
+ !isAbsolute(nodePath) ||
556
+ !isAbsolute(rootDirectory) ||
557
+ !isAbsolute(larkCliPath) ||
558
+ !isAbsolute(dingtalkCliPath)
559
+ ) {
560
+ throw new Error('部署路径必须是绝对路径');
561
+ }
562
+ if (typeof bootstrapToken !== 'string' || bootstrapToken.length < 32) {
563
+ throw new Error('管理员初始化令牌强度不足');
564
+ }
565
+ }
566
+
567
+ function systemdQuote(value) {
568
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
569
+ }