tirtc-devtools-cli 0.0.5 → 0.0.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.
Files changed (35) hide show
  1. package/README.md +3 -1
  2. package/USAGE.md +151 -366
  3. package/dist/devtools/cli/src/config.d.ts +4 -13
  4. package/dist/devtools/cli/src/config.js +7 -19
  5. package/dist/devtools/cli/src/facade.d.ts +39 -154
  6. package/dist/devtools/cli/src/facade.js +9 -23
  7. package/dist/devtools/cli/src/guide.js +7 -9
  8. package/dist/devtools/cli/src/index.js +135 -255
  9. package/dist/devtools/cli/src/token_command.d.ts +7 -0
  10. package/dist/devtools/cli/src/token_command.js +139 -0
  11. package/package.json +1 -1
  12. package/vendor/app-server/bin/native/macos-arm64/credential_napi.node +0 -0
  13. package/vendor/app-server/bin/native/macos-arm64/runtime_host_napi.node +0 -0
  14. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/trp.h +30 -17
  15. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_audio.a +0 -0
  16. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_credential.a +0 -0
  17. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_facade.a +0 -0
  18. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_http.a +0 -0
  19. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_logging.a +0 -0
  20. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_media.a +0 -0
  21. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_transport.a +0 -0
  22. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_video.a +0 -0
  23. package/vendor/app-server/bin/runtime/macos-arm64/manifest.txt +10 -10
  24. package/vendor/app-server/dist/host/HostProtocol.d.ts +7 -5
  25. package/vendor/app-server/dist/host/HostProtocol.js +21 -10
  26. package/vendor/app-server/dist/host/HostServer.d.ts +2 -2
  27. package/vendor/app-server/dist/host/HostServer.js +93 -35
  28. package/vendor/app-server/dist/host/HostState.d.ts +0 -5
  29. package/vendor/app-server/dist/host/HostState.js +0 -1
  30. package/vendor/app-server/dist/host/RuntimeAdapter.d.ts +12 -0
  31. package/vendor/app-server/dist/host/RuntimeAdapter.js +42 -3
  32. package/vendor/app-server/dist/host/RuntimeSendWorker.d.ts +4 -0
  33. package/vendor/app-server/dist/host/RuntimeSendWorker.js +17 -0
  34. package/vendor/app-server/dist/protocol/contract.d.ts +72 -14
  35. package/vendor/app-server/dist/protocol/contract.js +11 -8
@@ -1,19 +1,22 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const commander_1 = require("commander");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
4
9
  const protocol_client_1 = require("../../../app-server/protocol-client");
5
10
  const facade_1 = require("./facade");
6
11
  const config_1 = require("./config");
7
12
  const guide_1 = require("./guide");
8
13
  const session_manager_1 = require("./session_manager");
9
14
  const media_assets_1 = require("./media_assets");
15
+ const token_command_1 = require("./token_command");
10
16
  const transport_1 = require("./transport");
11
- const token_tool_1 = require("./token_tool");
12
- const CLI_VERSION = '0.0.5';
17
+ const CLI_VERSION = '0.0.6';
13
18
  const HOST_VERSION = '1.0.0';
14
19
  const PROTOCOL_VERSION = '1.0.0';
15
- const kTokenIssueAccessIdEnvVar = 'TIRTC_CONN_ACCESS_ID';
16
- const kTokenIssueSecretKeyEnvVar = 'TIRTC_CONN_SECRET_KEY';
17
20
  if (process.argv.includes('--version') || process.argv.includes('-V')) {
18
21
  console.log('CLI Version: ' + CLI_VERSION);
19
22
  console.log('Host Version: ' + HOST_VERSION);
@@ -103,57 +106,91 @@ function toResultObject(result) {
103
106
  }
104
107
  return { result };
105
108
  }
106
- async function applyConfiguredStreamBootstrap(client, config) {
107
- const sendStreams = [];
108
- const mode = config.stream?.request_policy?.mode;
109
- if (mode === 'manual' || mode === 'auto-if-bound') {
110
- await client.sendRequest('stream/requestPolicy/set', { mode });
111
- }
112
- const configuredSend = config.streams?.send;
113
- const assetsDir = configuredSend?.assets_dir;
114
- const audioStreamId = configuredSend?.audio_stream_id;
115
- const videoStreamId = configuredSend?.video_stream_id;
116
- if (assetsDir && assetsDir.length > 0) {
117
- if (Number.isFinite(audioStreamId)) {
118
- const stream = await client.sendRequest('stream/sendStart', {
119
- streamId: audioStreamId,
120
- media: 'audio',
121
- source: {
122
- mode: 'local_assets',
123
- local_assets: {
124
- assets_dir: assetsDir,
125
- },
126
- },
127
- }, 180_000);
128
- sendStreams.push({
129
- streamId: audioStreamId,
130
- assetsDir,
131
- media: 'audio',
132
- stream,
133
- });
134
- }
135
- if (Number.isFinite(videoStreamId)) {
136
- const stream = await client.sendRequest('stream/sendStart', {
137
- streamId: videoStreamId,
138
- media: 'video',
139
- source: {
140
- mode: 'local_assets',
141
- local_assets: {
142
- assets_dir: assetsDir,
143
- },
144
- },
145
- }, 180_000);
146
- sendStreams.push({
147
- streamId: videoStreamId,
148
- assetsDir,
149
- media: 'video',
150
- stream,
151
- });
152
- }
109
+ function requireConfigPath(configPath) {
110
+ if (!configPath || configPath.trim().length === 0) {
111
+ throw new Error('server-only service start requires --config <path>');
112
+ }
113
+ return configPath;
114
+ }
115
+ function validateServerConfig(config) {
116
+ const server = config.server;
117
+ const license = server?.license?.trim() ?? '';
118
+ const mp4Path = server?.mp4_path?.trim() ?? '';
119
+ const audioStreamIdValue = server?.audio_stream_id;
120
+ const videoStreamIdValue = server?.video_stream_id;
121
+ if (license.length === 0) {
122
+ throw new Error('server-only config requires [server].license');
123
+ }
124
+ if (mp4Path.length === 0) {
125
+ throw new Error('server-only config requires [server].mp4_path');
126
+ }
127
+ if (typeof audioStreamIdValue !== 'number') {
128
+ throw new Error('server-only config requires [server].audio_stream_id as non-negative integer');
129
+ }
130
+ if (!Number.isInteger(audioStreamIdValue) || audioStreamIdValue < 0) {
131
+ throw new Error('server-only config requires [server].audio_stream_id as non-negative integer');
132
+ }
133
+ if (typeof videoStreamIdValue !== 'number') {
134
+ throw new Error('server-only config requires [server].video_stream_id as non-negative integer');
135
+ }
136
+ if (!Number.isInteger(videoStreamIdValue) || videoStreamIdValue < 0) {
137
+ throw new Error('server-only config requires [server].video_stream_id as non-negative integer');
138
+ }
139
+ const audioStreamId = audioStreamIdValue;
140
+ const videoStreamId = videoStreamIdValue;
141
+ if (audioStreamId === videoStreamId) {
142
+ throw new Error('server-only config requires distinct audio_stream_id and video_stream_id');
143
+ }
144
+ const resolvedMp4Path = path_1.default.resolve(mp4Path);
145
+ let stats;
146
+ try {
147
+ stats = fs_1.default.statSync(resolvedMp4Path);
153
148
  }
149
+ catch {
150
+ throw new Error('server-only config mp4_path does not exist: ' + resolvedMp4Path);
151
+ }
152
+ if (!stats.isFile()) {
153
+ throw new Error('server-only config mp4_path must be a readable file: ' + resolvedMp4Path);
154
+ }
155
+ const serviceEntry = server?.service_entry?.trim();
154
156
  return {
155
- requestPolicyMode: mode,
156
- sendStreams,
157
+ serviceEntry: serviceEntry && serviceEntry.length > 0 ? serviceEntry : undefined,
158
+ license,
159
+ mp4Path: resolvedMp4Path,
160
+ audioStreamId,
161
+ videoStreamId,
162
+ };
163
+ }
164
+ function resolvePrepareOutputRoot(config) {
165
+ const loggingRoot = config.logging?.root_dir?.trim();
166
+ if (loggingRoot && loggingRoot.length > 0) {
167
+ return path_1.default.resolve(loggingRoot, '..', 'prepared-assets');
168
+ }
169
+ return path_1.default.resolve(process.cwd(), '.tmp', 'tirtc-devtools-cli', 'prepared-assets');
170
+ }
171
+ async function applyConfiguredServerBootstrap(client, config) {
172
+ const server = validateServerConfig(config);
173
+ const prepared = await (0, media_assets_1.prepareMediaAssets)({
174
+ source: server.mp4Path,
175
+ outputRoot: resolvePrepareOutputRoot(config),
176
+ });
177
+ const bootstrapSendStreams = [
178
+ { streamId: server.audioStreamId, media: 'audio' },
179
+ { streamId: server.videoStreamId, media: 'video' },
180
+ ].map(({ streamId, media }) => ({
181
+ streamId,
182
+ media,
183
+ source: {
184
+ mode: 'local_assets',
185
+ local_assets: {
186
+ assets_dir: prepared.assets_dir,
187
+ },
188
+ },
189
+ }));
190
+ return {
191
+ preparedAssetsDir: prepared.assets_dir,
192
+ mediaSendPolicy: 'AUTO_ON_CONNECTED',
193
+ bootstrapSendStreams,
157
194
  };
158
195
  }
159
196
  async function runCommand(cmdName, params, options) {
@@ -168,26 +205,25 @@ async function runCommand(cmdName, params, options) {
168
205
  if (!method) {
169
206
  throw new Error('Unknown command mapping for: ' + cmdName);
170
207
  }
171
- const commandParams = cmdName === 'stream request-policy set' ? (() => {
172
- const mode = String(params.mode ?? '');
173
- if (mode !== 'manual' && mode !== 'auto-if-bound') {
174
- throw new Error('Invalid mode, expected manual|auto-if-bound');
175
- }
176
- return { mode };
177
- })() : params;
178
- const result = await client.sendRequest(method, commandParams);
179
- let resultForPrint = result;
208
+ let resultForPrint;
180
209
  if (cmdName === 'service start') {
181
- const autoApplied = await applyConfiguredStreamBootstrap(client, config);
182
- const hasAutoApplied = autoApplied.requestPolicyMode !== undefined || autoApplied.sendStreams.length > 0;
183
- if (hasAutoApplied) {
184
- const resultObject = toResultObject(result);
185
- resultForPrint = {
186
- ...resultObject,
187
- autoApplied,
188
- };
189
- }
190
- }
210
+ const autoApplied = await applyConfiguredServerBootstrap(client, config);
211
+ const requestParams = {
212
+ ...params,
213
+ bootstrapSendStreams: autoApplied.bootstrapSendStreams,
214
+ };
215
+ const result = await client.sendRequest(method, requestParams, 180_000);
216
+ const resultObject = toResultObject(result);
217
+ resultForPrint = {
218
+ ...resultObject,
219
+ autoApplied,
220
+ };
221
+ printSuccess(options, resultForPrint, session, cmdName);
222
+ transport.stop();
223
+ return 0;
224
+ }
225
+ const result = await client.sendRequest(method, params);
226
+ resultForPrint = result;
191
227
  printSuccess(options, resultForPrint, session, cmdName);
192
228
  transport.stop();
193
229
  return 0;
@@ -290,123 +326,11 @@ async function runMediaAssetsPrepare(source, outputRoot, outputDir, overwrite, o
290
326
  return exitCode;
291
327
  }
292
328
  }
293
- async function runTokenIssue(params, options) {
294
- try {
295
- const result = await (0, token_tool_1.issueTokenWithQrcode)(params);
296
- if (options.json) {
297
- console.log(JSON.stringify({
298
- code: 0,
299
- message: 'OK',
300
- data: {
301
- payload: result.payload,
302
- payloadJson: result.payloadJson,
303
- token: result.token,
304
- qrCodePngPath: result.qrCodePngPath,
305
- },
306
- }));
307
- }
308
- else {
309
- console.log((0, token_tool_1.formatTokenIssueConsoleOutput)(result));
310
- }
311
- return 0;
312
- }
313
- catch (error) {
314
- const normalized = normalizeError(error);
315
- const reasonCode = normalized.reasonCode;
316
- const exitCode = facade_1.ErrorReasonCodeMapping[reasonCode] ?? 1;
317
- if (options.json) {
318
- console.log(JSON.stringify({
319
- code: exitCode,
320
- message: normalized.message,
321
- data: normalized.data,
322
- }));
323
- }
324
- else {
325
- console.error('Error (' + reasonCode + '): ' + normalized.message);
326
- }
327
- return exitCode;
328
- }
329
- }
330
- async function runTokenIssueFromCli(peerId, commandOptions, options) {
331
- const parsePositiveInt = (name, raw) => {
332
- if (raw === undefined) {
333
- return undefined;
334
- }
335
- const parsed = Number.parseInt(raw, 10);
336
- if (!Number.isInteger(parsed) || parsed <= 0) {
337
- throw new Error(name + ' must be a positive integer');
338
- }
339
- return parsed;
340
- };
341
- const parseQrErrorCorrectionLevel = (raw) => {
342
- if (raw === undefined) {
343
- return undefined;
344
- }
345
- const normalized = raw.trim().toUpperCase();
346
- if (normalized !== 'L' && normalized !== 'M' && normalized !== 'Q' && normalized !== 'H') {
347
- throw new Error('qr-error-correction-level must be one of: L, M, Q, H');
348
- }
349
- return normalized;
350
- };
351
- try {
352
- const accessId = resolveTokenIssueCredential(commandOptions.accessId, kTokenIssueAccessIdEnvVar, '--access-id');
353
- const secretKey = resolveTokenIssueCredential(commandOptions.secretKey, kTokenIssueSecretKeyEnvVar, '--secret-key');
354
- return await runTokenIssue({
355
- accessId,
356
- secretKey,
357
- localId: commandOptions.localId?.trim() || peerId,
358
- peerId,
359
- openapiEntry: commandOptions.openapiEntry,
360
- serviceEntry: commandOptions.serviceEntry,
361
- userTtlSeconds: parsePositiveInt('user-ttl-seconds', commandOptions.userTtlSeconds),
362
- channelTtlSeconds: parsePositiveInt('channel-ttl-seconds', commandOptions.channelTtlSeconds),
363
- qrErrorCorrectionLevel: parseQrErrorCorrectionLevel(commandOptions.qrErrorCorrectionLevel),
364
- asciiMaxColumns: parsePositiveInt('ascii-max-columns', commandOptions.asciiMaxColumns),
365
- }, options);
366
- }
367
- catch (error) {
368
- const normalized = normalizeError(error);
369
- const reasonCode = normalized.reasonCode;
370
- const exitCode = facade_1.ErrorReasonCodeMapping[reasonCode] ?? 1;
371
- if (options.json) {
372
- console.log(JSON.stringify({
373
- code: exitCode,
374
- message: normalized.message,
375
- data: normalized.data,
376
- }));
377
- }
378
- else {
379
- console.error('Error (' + reasonCode + '): ' + normalized.message);
380
- }
381
- return exitCode;
382
- }
383
- }
384
329
  function runAndExit(promise) {
385
330
  promise.then((code) => {
386
331
  process.exit(code);
387
332
  });
388
333
  }
389
- function mustParseJsonPayload(input) {
390
- try {
391
- const parsed = JSON.parse(input);
392
- return JSON.stringify(parsed);
393
- }
394
- catch {
395
- throw new Error('payloadJson must be valid JSON text');
396
- }
397
- }
398
- function resolveTokenIssueCredential(explicitValue, envVarName, optionName) {
399
- const normalizedExplicit = explicitValue?.trim();
400
- if (normalizedExplicit) {
401
- return normalizedExplicit;
402
- }
403
- const normalizedEnv = process.env[envVarName]?.trim();
404
- if (normalizedEnv) {
405
- return normalizedEnv;
406
- }
407
- throw new Error('missing credential: set environment variable ' + envVarName +
408
- ' or pass ' + optionName + ' explicitly');
409
- }
410
334
  program.name('tirtc-devtools-cli')
411
335
  .description('TiRTC DevTools CLI')
412
336
  .option('--config <path>', '配置文件路径(TOML)')
@@ -551,22 +475,18 @@ mediaAssets.command('prepare')
551
475
  .action((commandOptions) => {
552
476
  runAndExit(runMediaAssetsPrepare(commandOptions.source, commandOptions.outputRoot, commandOptions.outputDir, commandOptions.overwrite === true, getCliOptions()));
553
477
  });
554
- // Backward-compatible alias.
555
478
  const hostConfig = host.command('config').description('配置辅助(兼容别名,推荐用顶层 config)');
556
479
  hostConfig.command('init [outputPath]').description('生成配置模板(等价于 config init)').action((outputPath) => {
557
480
  handleConfigInit(outputPath);
558
481
  });
559
482
  const service = program.command('service').description('服务端能力:启动/停止服务,等待远端接入');
560
- service.command('start [service_entry] [license]')
561
- .description('启动服务端会话(license 必填,service_entry 可省略走默认)')
562
- .action((service_entry, license) => {
563
- const config = (0, config_1.loadConfig)(getCliOptions().config);
564
- const finalServiceEntry = service_entry ?? config.service?.service_entry;
565
- const finalLicense = license ?? config.service?.license;
566
- if (!finalLicense) {
567
- throw new Error('service start requires license (service_entry can be omitted)');
568
- }
569
- runAndExit(runCommand('service start', { serviceEntry: finalServiceEntry, license: finalLicense, timeoutMs: 5000 }, getCliOptions()));
483
+ service.command('start')
484
+ .description('启动 server-only 会话(必须显式提供 --config)')
485
+ .action(() => {
486
+ const options = getCliOptions();
487
+ const config = (0, config_1.loadConfig)(requireConfigPath(options.config));
488
+ const server = validateServerConfig(config);
489
+ runAndExit(runCommand('service start', { serviceEntry: server.serviceEntry, license: server.license, timeoutMs: 5000 }, options));
570
490
  });
571
491
  service.command('stop').description('停止服务端监听').action(() => {
572
492
  runAndExit(runCommand('service stop', {}, getCliOptions()));
@@ -577,9 +497,10 @@ connection.command('show').description('查看连接状态').action(() => {
577
497
  });
578
498
  connection.command('connect [service_entry] <peer_id> [token]')
579
499
  .description('作为客户端连接远端(总是新建 session;不传 token 时自动签发)')
580
- .action((service_entry, peer_id, token) => {
500
+ .action((serviceEntry, peerId, token) => {
581
501
  const config = (0, config_1.loadConfig)(getCliOptions().config);
582
- const finalServiceEntry = [service_entry, config.connection?.service_entry, config.service?.service_entry].find((value) => typeof value === "string" && value.trim().length > 0);
502
+ const finalServiceEntry = [serviceEntry, config.connection?.service_entry, config.server?.service_entry]
503
+ .find((value) => typeof value === 'string' && value.trim().length > 0);
583
504
  const configuredToken = config.connection?.token?.trim();
584
505
  const manualToken = (token ?? configuredToken)?.trim();
585
506
  const autoTokenCfg = config.connection?.auto_token;
@@ -589,7 +510,7 @@ connection.command('connect [service_entry] <peer_id> [token]')
589
510
  }
590
511
  const params = {
591
512
  serviceEntry: finalServiceEntry,
592
- peerId: peer_id,
513
+ peerId,
593
514
  timeoutMs: 5000,
594
515
  };
595
516
  if (manualToken && manualToken.length > 0) {
@@ -602,7 +523,7 @@ connection.command('connect [service_entry] <peer_id> [token]')
602
523
  openapiEntry: autoTokenCfg?.openapi_entry,
603
524
  accessId: autoTokenCfg?.access_id,
604
525
  secretKey: autoTokenCfg?.secret_key,
605
- localId: autoTokenCfg?.local_id ?? peer_id,
526
+ localId: autoTokenCfg?.local_id ?? peerId,
606
527
  userTtlSeconds: autoTokenCfg?.user_ttl_seconds,
607
528
  channelTtlSeconds: autoTokenCfg?.channel_ttl_seconds,
608
529
  };
@@ -612,36 +533,22 @@ connection.command('connect [service_entry] <peer_id> [token]')
612
533
  connection.command('disconnect').description('断开当前连接').action(() => {
613
534
  runAndExit(runCommand('connection disconnect', {}, getCliOptions()));
614
535
  });
615
- const token = program.command('token').description('Token 工具:签发 token,并输出可直接使用的 JSON 与本地二维码 PNG');
616
- token.command('issue <peer_id>')
617
- .description('默认从环境变量读取 access/secret,并以 peer_id 作为默认 local_id 来签发 token')
618
- .option('--access-id <accessId>', '显式 access id;不传时读取 ' + kTokenIssueAccessIdEnvVar)
619
- .option('--secret-key <secretKey>', '显式 secret key;不传时读取 ' + kTokenIssueSecretKeyEnvVar)
620
- .option('--local-id <localId>', '显式 local id;不传时默认使用 peer_id')
621
- .option('--openapi-entry <url>', '可选 openapi entry;留空时走 runtime 默认值')
622
- .option('--service-entry <entry>', '可选 service entry;用于组合连接 payload 与二维码 PNG')
623
- .option('--user-ttl-seconds <seconds>', '可选 user token ttl(秒)')
624
- .option('--channel-ttl-seconds <seconds>', '可选 channel token ttl(秒)')
625
- .option('--qr-error-correction-level <level>', '二维码纠错级别:L/M/Q/H;默认 M')
626
- .option('--ascii-max-columns <columns>', 'ASCII 二维码最大宽度;不传时优先读取当前终端宽度或 COLUMNS')
627
- .action((peer_id, commandOptions) => {
628
- runAndExit(runTokenIssueFromCli(peer_id, commandOptions, getCliOptions()));
629
- });
630
- const stream = program.command('stream').description('流控制:发送/接收/请求策略');
536
+ (0, token_command_1.registerTokenCommands)(program, getCliOptions, runAndExit);
537
+ const stream = program.command('stream').description('流控制:发送/接收/消息');
631
538
  stream.command('list').description('查看流快照列表').action(() => {
632
539
  runAndExit(runCommand('stream list', {}, getCliOptions()));
633
540
  });
634
541
  const streamSend = stream.command('send').description('发送流:绑定 prepared local assets 并发起上行');
635
542
  streamSend.command('start <streamId> <media> <assets_dir>')
636
543
  .description('开始发送流(绑定 source.local_assets 并触发上行)')
637
- .action((streamId, media, assets_dir) => {
544
+ .action((streamId, media, assetsDir) => {
638
545
  runAndExit(runCommand('stream send start', {
639
546
  streamId: Number.parseInt(streamId, 10),
640
547
  media,
641
548
  source: {
642
549
  mode: 'local_assets',
643
550
  local_assets: {
644
- assets_dir,
551
+ assets_dir: assetsDir,
645
552
  },
646
553
  },
647
554
  }, getCliOptions()));
@@ -663,18 +570,13 @@ streamReceive.command('stop <streamId>')
663
570
  .action((streamId) => {
664
571
  runAndExit(runCommand('stream receive stop', { streamId: Number.parseInt(streamId, 10) }, getCliOptions()));
665
572
  });
666
- const streamRequestPolicy = stream.command('request-policy').description('远端请求策略:manual/auto-if-bound');
667
- streamRequestPolicy.command('get').description('查看远端请求策略').action(() => {
668
- runAndExit(runCommand('stream request-policy get', {}, getCliOptions()));
669
- });
670
- streamRequestPolicy.command('set <mode>')
671
- .description('设置远端请求策略(manual|auto-if-bound)')
672
- .action((mode) => {
673
- runAndExit(runCommand('stream request-policy set', { mode }, getCliOptions()));
573
+ stream.command('message send <streamId> <payload>')
574
+ .description('发送 UTF-8 字符串型 stream message')
575
+ .action((streamId, payload) => {
576
+ runAndExit(runCommand('stream message send', { streamId: Number.parseInt(streamId, 10), payload }, getCliOptions()));
674
577
  });
675
578
  const output = program.command('output').description('输出挂载:文件等消费者 attach/detach');
676
- output
677
- .command('attach <streamId> <consumer> <mediaView> <format> <delivery> [targetPath] [maxFiles]')
579
+ output.command('attach <streamId> <consumer> <mediaView> <format> <delivery> [targetPath] [maxFiles]')
678
580
  .description('挂载输出消费者(如 file_sink)')
679
581
  .action((streamId, consumer, mediaView, format, delivery, targetPath, maxFiles) => {
680
582
  const parsed = facade_1.OutputAttachCliParamsSchema.safeParse({
@@ -705,7 +607,7 @@ output.command('detach <outputId>').description('卸载输出消费者').action(
705
607
  });
706
608
  const command = program.command('command').description('命令通道:发送命令与事件跟踪');
707
609
  command.command('send <commandId> <kind> <payloadEncoding> <payload>')
708
- .description('发送命令并等待响应')
610
+ .description('发送字符串型命令并等待响应')
709
611
  .action((commandId, kind, payloadEncoding, payload) => {
710
612
  runAndExit(runCommand('command send', {
711
613
  commandId: Number.parseInt(commandId, 10),
@@ -716,28 +618,6 @@ command.command('send <commandId> <kind> <payloadEncoding> <payload>')
716
618
  timeoutMs: 5000,
717
619
  }, getCliOptions()));
718
620
  });
719
- command.command('send-json <commandId> <kind> <payloadJson>')
720
- .description('发送 JSON 命令并等待响应(JSON-first)')
721
- .action((commandId, kind, payloadJson) => {
722
- const parsed = facade_1.CommandSendJsonParamsSchema.safeParse({
723
- commandId: Number.parseInt(commandId, 10),
724
- kind,
725
- payloadJson,
726
- });
727
- if (!parsed.success) {
728
- const msg = parsed.error.issues.map((issue) => issue.message).join('; ');
729
- throw new Error('invalid command send-json params: ' + msg);
730
- }
731
- const payload = mustParseJsonPayload(payloadJson);
732
- runAndExit(runCommand('command send', {
733
- commandId: parsed.data.commandId,
734
- kind: parsed.data.kind,
735
- payloadEncoding: 'json',
736
- payload,
737
- awaitResponse: true,
738
- timeoutMs: 5000,
739
- }, getCliOptions()));
740
- });
741
621
  command.command('tail').description('持续监听命令相关事件').action(() => {
742
622
  runAndExit(tailEvents('command tail', ['command'], getCliOptions()));
743
623
  });
@@ -745,14 +625,14 @@ const debug = program.command('debug').description('调试工具:连接 bootst
745
625
  const debugBootstrap = debug.command('bootstrap').description('连接 bootstrap(不在 App Server 侧签发 token)');
746
626
  debugBootstrap.command('qrcode <access_id> <secret_key> <peer_id> [service_entry]')
747
627
  .description('生成客户端连接 bootstrap 二维码(payload 包含 access_id/secret_key/peer_id)')
748
- .action((access_id, secret_key, peer_id, service_entry) => {
628
+ .action((accessId, secretKey, peerId, serviceEntry) => {
749
629
  const payload = JSON.stringify({
750
630
  version: 1,
751
631
  type: 'tirtc-connect-bootstrap',
752
- access_id,
753
- secret_key,
754
- peer_id,
755
- service_entry: service_entry ?? '',
632
+ access_id: accessId,
633
+ secret_key: secretKey,
634
+ peer_id: peerId,
635
+ service_entry: serviceEntry ?? '',
756
636
  generated_at: new Date().toISOString(),
757
637
  });
758
638
  runAndExit(runCommand('debug bootstrap qrcode', { payload, outputStem: 'connect-bootstrap' }, getCliOptions()));
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ export type CliOptions = {
3
+ config?: string;
4
+ json?: boolean;
5
+ session?: string;
6
+ };
7
+ export declare function registerTokenCommands(program: Command, getCliOptions: () => CliOptions, runAndExit: (promise: Promise<number>) => void): void;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTokenCommands = registerTokenCommands;
4
+ const facade_1 = require("./facade");
5
+ const token_tool_1 = require("./token_tool");
6
+ const kTokenIssueAccessIdEnvVar = 'TIRTC_CONN_ACCESS_ID';
7
+ const kTokenIssueSecretKeyEnvVar = 'TIRTC_CONN_SECRET_KEY';
8
+ function normalizeTokenCommandError(error) {
9
+ if (error instanceof Error) {
10
+ return {
11
+ reasonCode: 'internal_error',
12
+ message: error.message,
13
+ data: undefined,
14
+ };
15
+ }
16
+ if (typeof error === 'object' && error !== null) {
17
+ const typed = error;
18
+ return {
19
+ reasonCode: typed.reasonCode ?? 'internal_error',
20
+ message: typed.message ?? 'Internal error',
21
+ data: typed.data,
22
+ };
23
+ }
24
+ return {
25
+ reasonCode: 'internal_error',
26
+ message: 'Internal error',
27
+ data: undefined,
28
+ };
29
+ }
30
+ function printTokenCommandError(error, options) {
31
+ const normalized = normalizeTokenCommandError(error);
32
+ const reasonCode = normalized.reasonCode;
33
+ const exitCode = facade_1.ErrorReasonCodeMapping[reasonCode] ?? 1;
34
+ if (options.json) {
35
+ console.log(JSON.stringify({
36
+ code: exitCode,
37
+ message: normalized.message,
38
+ data: normalized.data,
39
+ }));
40
+ }
41
+ else {
42
+ console.error('Error (' + reasonCode + '): ' + normalized.message);
43
+ }
44
+ return exitCode;
45
+ }
46
+ async function runTokenIssue(params, options) {
47
+ try {
48
+ const result = await (0, token_tool_1.issueTokenWithQrcode)(params);
49
+ if (options.json) {
50
+ console.log(JSON.stringify({
51
+ code: 0,
52
+ message: 'OK',
53
+ data: {
54
+ payload: result.payload,
55
+ payloadJson: result.payloadJson,
56
+ token: result.token,
57
+ qrCodePngPath: result.qrCodePngPath,
58
+ },
59
+ }));
60
+ }
61
+ else {
62
+ console.log((0, token_tool_1.formatTokenIssueConsoleOutput)(result));
63
+ }
64
+ return 0;
65
+ }
66
+ catch (error) {
67
+ return printTokenCommandError(error, options);
68
+ }
69
+ }
70
+ function resolveTokenIssueCredential(explicitValue, envVarName, optionName) {
71
+ const normalizedExplicit = explicitValue?.trim();
72
+ if (normalizedExplicit) {
73
+ return normalizedExplicit;
74
+ }
75
+ const normalizedEnv = process.env[envVarName]?.trim();
76
+ if (normalizedEnv) {
77
+ return normalizedEnv;
78
+ }
79
+ throw new Error('missing credential: set environment variable ' + envVarName +
80
+ ' or pass ' + optionName + ' explicitly');
81
+ }
82
+ async function runTokenIssueFromCli(peerId, commandOptions, options) {
83
+ const parsePositiveInt = (name, raw) => {
84
+ if (raw === undefined) {
85
+ return undefined;
86
+ }
87
+ const parsed = Number.parseInt(raw, 10);
88
+ if (!Number.isInteger(parsed) || parsed <= 0) {
89
+ throw new Error(name + ' must be a positive integer');
90
+ }
91
+ return parsed;
92
+ };
93
+ const parseQrErrorCorrectionLevel = (raw) => {
94
+ if (raw === undefined) {
95
+ return undefined;
96
+ }
97
+ const normalized = raw.trim().toUpperCase();
98
+ if (normalized !== 'L' && normalized !== 'M' && normalized !== 'Q' && normalized !== 'H') {
99
+ throw new Error('qr-error-correction-level must be one of: L, M, Q, H');
100
+ }
101
+ return normalized;
102
+ };
103
+ try {
104
+ const accessId = resolveTokenIssueCredential(commandOptions.accessId, kTokenIssueAccessIdEnvVar, '--access-id');
105
+ const secretKey = resolveTokenIssueCredential(commandOptions.secretKey, kTokenIssueSecretKeyEnvVar, '--secret-key');
106
+ return await runTokenIssue({
107
+ accessId,
108
+ secretKey,
109
+ localId: commandOptions.localId?.trim() || peerId,
110
+ peerId,
111
+ openapiEntry: commandOptions.openapiEntry,
112
+ serviceEntry: commandOptions.serviceEntry,
113
+ userTtlSeconds: parsePositiveInt('user-ttl-seconds', commandOptions.userTtlSeconds),
114
+ channelTtlSeconds: parsePositiveInt('channel-ttl-seconds', commandOptions.channelTtlSeconds),
115
+ qrErrorCorrectionLevel: parseQrErrorCorrectionLevel(commandOptions.qrErrorCorrectionLevel),
116
+ asciiMaxColumns: parsePositiveInt('ascii-max-columns', commandOptions.asciiMaxColumns),
117
+ }, options);
118
+ }
119
+ catch (error) {
120
+ return printTokenCommandError(error, options);
121
+ }
122
+ }
123
+ function registerTokenCommands(program, getCliOptions, runAndExit) {
124
+ const token = program.command('token').description('Token 工具:签发 token,并输出可直接使用的 JSON 与本地二维码 PNG');
125
+ token.command('issue <peer_id>')
126
+ .description('默认从环境变量读取 access/secret,并以 peer_id 作为默认 local_id 来签发 token')
127
+ .option('--access-id <accessId>', '显式 access id;不传时读取 ' + kTokenIssueAccessIdEnvVar)
128
+ .option('--secret-key <secretKey>', '显式 secret key;不传时读取 ' + kTokenIssueSecretKeyEnvVar)
129
+ .option('--local-id <localId>', '显式 local id;不传时默认使用 peer_id')
130
+ .option('--openapi-entry <url>', '可选 openapi entry;留空时走 runtime 默认值')
131
+ .option('--service-entry <entry>', '可选 service entry;用于组合连接 payload 与二维码 PNG')
132
+ .option('--user-ttl-seconds <seconds>', '可选 user token ttl(秒)')
133
+ .option('--channel-ttl-seconds <seconds>', '可选 channel token ttl(秒)')
134
+ .option('--qr-error-correction-level <level>', '二维码纠错级别:L/M/Q/H;默认 M')
135
+ .option('--ascii-max-columns <columns>', 'ASCII 二维码最大宽度;不传时优先读取当前终端宽度或 COLUMNS')
136
+ .action((peerId, commandOptions) => {
137
+ runAndExit(runTokenIssueFromCli(peerId, commandOptions, getCliOptions()));
138
+ });
139
+ }