@casatwy/deyo 0.1.9 → 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/deyo.mjs CHANGED
@@ -6,12 +6,15 @@ import { createRequire } from 'node:module'
6
6
  import { basename, dirname, extname, isAbsolute, resolve } from 'node:path'
7
7
  import { homedir } from 'node:os'
8
8
  import process from 'node:process'
9
+ import { handleSkillCommand } from './skill-manager.mjs'
9
10
 
10
11
  const CLI_VERSION = createRequire(import.meta.url)('./package.json').version
11
12
 
12
13
  const DEFAULT_BASE_URL = 'https://deyo.miaobi.fun'
13
14
  const DEFAULT_LANGUAGE = 'zh'
14
15
  const POLL_INTERVAL_MS = 1500
16
+ const SSE_RECONNECT_DELAYS_MS = [1000, 2000, 4000]
17
+ const SSE_RECOVERY_INTERVAL_MS = 15000
15
18
  const PROGRESS_FORMATS = new Set(['auto', 'text', 'jsonl'])
16
19
  const RESULT_FORMATS = new Set(['text', 'srt', 'vtt', 'json', 'verbose_json'])
17
20
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled'])
@@ -80,6 +83,7 @@ function printUsage() {
80
83
  deyo [选项] <url|file>
81
84
  deyo [选项] --file <file>
82
85
  deyo auth <login|logout|status> [选项]
86
+ deyo skill <install|status> [选项]
83
87
 
84
88
  转写选项:
85
89
  --api-key <key> 可选,Deyo API key
@@ -90,6 +94,7 @@ function printUsage() {
90
94
  --language <value> 可选,转写语言,默认 ${DEFAULT_LANGUAGE}
91
95
  --format <value> 可选,text | srt | vtt | json | verbose_json
92
96
  --progress-format <v> 可选,auto | text | jsonl,默认 auto
97
+ --stream-transcript 可选,通过 stderr JSONL 输出稳定正文;仅支持 text 且必须搭配 --progress-format jsonl
93
98
  --base-url <url> 可选,默认 ${DEFAULT_BASE_URL}
94
99
  -h, --help 显示帮助
95
100
  -v, --version 显示 CLI 版本
@@ -99,6 +104,10 @@ auth 子命令:
99
104
  deyo auth logout
100
105
  deyo auth status
101
106
 
107
+ Skill 管理:
108
+ deyo skill install --platform codex|claude|gemini|openclaw [--scope user|workspace|global]
109
+ deyo skill status [--platform codex|claude|gemini|openclaw] [--json]
110
+
102
111
  API key 读取优先级:
103
112
  1. --api-key
104
113
  2. 环境变量 DEYO_API_KEY
@@ -133,6 +142,7 @@ function parseTranscriptionArgs(argv) {
133
142
  language: '',
134
143
  format: '',
135
144
  progressFormat: 'auto',
145
+ streamTranscript: false,
136
146
  baseUrl: '',
137
147
  url: '',
138
148
  positional: [],
@@ -205,6 +215,11 @@ function parseTranscriptionArgs(argv) {
205
215
  continue
206
216
  }
207
217
 
218
+ if (current === '--stream-transcript') {
219
+ args.streamTranscript = true
220
+ continue
221
+ }
222
+
208
223
  if (current === '--base-url') {
209
224
  args.baseUrl = argv[index + 1] || ''
210
225
  index += 1
@@ -299,6 +314,20 @@ function inferFormat(outputPath, explicitFormat) {
299
314
  return 'text'
300
315
  }
301
316
 
317
+ function validateTranscriptStreaming(args, format) {
318
+ if (!args.streamTranscript) {
319
+ return
320
+ }
321
+
322
+ if (format !== 'text') {
323
+ fail('--stream-transcript 仅支持 text 结果格式')
324
+ }
325
+
326
+ if (args.progressFormat !== 'jsonl') {
327
+ fail('--stream-transcript 必须搭配 --progress-format jsonl')
328
+ }
329
+ }
330
+
302
331
  function isAbortError(error) {
303
332
  return error?.name === 'AbortError' || error?.code === 'ABORT_ERR'
304
333
  }
@@ -823,6 +852,39 @@ async function requestResult(baseUrl, apiKey, taskId, format, options = {}) {
823
852
  return response.text()
824
853
  }
825
854
 
855
+ async function requestTranscriptPreview(baseUrl, apiKey, taskId, options = {}) {
856
+ const response = await fetch(`${baseUrl}/api/cli/transcriptions/${encodeURIComponent(taskId)}/preview`, {
857
+ method: 'GET',
858
+ signal: options.signal,
859
+ headers: {
860
+ Authorization: `Bearer ${apiKey}`,
861
+ },
862
+ })
863
+
864
+ if (response.status === 204) {
865
+ return {
866
+ available: true,
867
+ text: null,
868
+ }
869
+ }
870
+
871
+ if (response.status === 404 || response.status === 405 || response.status === 501) {
872
+ return {
873
+ available: false,
874
+ text: null,
875
+ }
876
+ }
877
+
878
+ if (!response.ok) {
879
+ throw new HttpCliError(await readErrorMessage(response), response.status)
880
+ }
881
+
882
+ return {
883
+ available: true,
884
+ text: await response.text(),
885
+ }
886
+ }
887
+
826
888
  function sleep(ms) {
827
889
  return new Promise(resolvePromise => setTimeout(resolvePromise, ms))
828
890
  }
@@ -1262,6 +1324,50 @@ function createProgressReporter(progressFormat) {
1262
1324
  error: errorMessage || null,
1263
1325
  })
1264
1326
  },
1327
+ reportTranscriptDelta(input) {
1328
+ if (resolvedMode !== 'jsonl') {
1329
+ return
1330
+ }
1331
+
1332
+ writeJsonEvent({
1333
+ event: 'task.transcript.delta',
1334
+ taskId: input.taskId,
1335
+ sequence: input.sequence,
1336
+ source: input.source,
1337
+ startOffset: input.startOffset,
1338
+ endOffset: input.endOffset,
1339
+ text: input.text,
1340
+ })
1341
+ },
1342
+ reportTranscriptReset(input) {
1343
+ if (resolvedMode !== 'jsonl') {
1344
+ return
1345
+ }
1346
+
1347
+ writeJsonEvent({
1348
+ event: 'task.transcript.reset',
1349
+ taskId: input.taskId,
1350
+ sequence: input.sequence,
1351
+ source: input.source,
1352
+ reason: input.reason,
1353
+ text: input.text,
1354
+ characterCount: input.characterCount,
1355
+ })
1356
+ },
1357
+ reportTranscriptCompleted(input) {
1358
+ if (resolvedMode !== 'jsonl') {
1359
+ return
1360
+ }
1361
+
1362
+ writeJsonEvent({
1363
+ event: 'task.transcript.completed',
1364
+ taskId: input.taskId,
1365
+ sequence: input.sequence,
1366
+ source: input.source,
1367
+ characterCount: input.characterCount,
1368
+ sha256: input.sha256,
1369
+ })
1370
+ },
1265
1371
  reportResultWritten(outputPath, format) {
1266
1372
  emitTextOrJson(`结果已写入:${outputPath}`, {
1267
1373
  event: 'task.result_written',
@@ -1306,7 +1412,114 @@ function parseSseBlock(block) {
1306
1412
  }
1307
1413
  }
1308
1414
 
1309
- async function consumeTaskEvents(baseUrl, apiKey, taskId, initialTask, reporter, signal) {
1415
+ function countCodePoints(value) {
1416
+ return Array.from(String(value || '')).length
1417
+ }
1418
+
1419
+ function longestCommonCodePointPrefix(first, second) {
1420
+ const firstCodePoints = Array.from(String(first || ''))
1421
+ const secondCodePoints = Array.from(String(second || ''))
1422
+ const length = Math.min(firstCodePoints.length, secondCodePoints.length)
1423
+ let index = 0
1424
+
1425
+ while (index < length && firstCodePoints[index] === secondCodePoints[index]) {
1426
+ index += 1
1427
+ }
1428
+
1429
+ return firstCodePoints.slice(0, index).join('')
1430
+ }
1431
+
1432
+ function createTranscriptStreamState(taskId, reporter) {
1433
+ let stableText = ''
1434
+ let stableCharacterCount = 0
1435
+ let previousPreviewSnapshot = null
1436
+ let sequence = 0
1437
+
1438
+ function nextSequence() {
1439
+ sequence += 1
1440
+ return sequence
1441
+ }
1442
+
1443
+ function applyStableSnapshot(snapshot, source, resetReason = 'non_prefix_snapshot') {
1444
+ if (typeof snapshot !== 'string' || snapshot === stableText) {
1445
+ return
1446
+ }
1447
+
1448
+ if (snapshot.startsWith(stableText)) {
1449
+ const appendedText = snapshot.slice(stableText.length)
1450
+ const startOffset = stableCharacterCount
1451
+
1452
+ stableText = snapshot
1453
+ stableCharacterCount += countCodePoints(appendedText)
1454
+
1455
+ if (appendedText) {
1456
+ reporter.reportTranscriptDelta({
1457
+ taskId,
1458
+ sequence: nextSequence(),
1459
+ source,
1460
+ startOffset,
1461
+ endOffset: stableCharacterCount,
1462
+ text: appendedText,
1463
+ })
1464
+ }
1465
+ return
1466
+ }
1467
+
1468
+ stableText = snapshot
1469
+ stableCharacterCount = countCodePoints(stableText)
1470
+ reporter.reportTranscriptReset({
1471
+ taskId,
1472
+ sequence: nextSequence(),
1473
+ source,
1474
+ reason: resetReason,
1475
+ text: stableText,
1476
+ characterCount: stableCharacterCount,
1477
+ })
1478
+ }
1479
+
1480
+ return {
1481
+ applySseSnapshot(snapshot) {
1482
+ if (typeof snapshot === 'string') {
1483
+ previousPreviewSnapshot = null
1484
+ }
1485
+ applyStableSnapshot(snapshot, 'sse')
1486
+ },
1487
+ applyPreviewSnapshot(snapshot) {
1488
+ if (typeof snapshot !== 'string') {
1489
+ return
1490
+ }
1491
+
1492
+ if (previousPreviewSnapshot === null) {
1493
+ previousPreviewSnapshot = snapshot
1494
+ return
1495
+ }
1496
+
1497
+ const stablePreviewText = longestCommonCodePointPrefix(previousPreviewSnapshot, snapshot)
1498
+ previousPreviewSnapshot = snapshot
1499
+
1500
+ if (
1501
+ stablePreviewText.length < stableText.length
1502
+ && stableText.startsWith(stablePreviewText)
1503
+ ) {
1504
+ return
1505
+ }
1506
+
1507
+ applyStableSnapshot(stablePreviewText, 'preview')
1508
+ },
1509
+ complete(authoritativeText) {
1510
+ applyStableSnapshot(authoritativeText, 'result', 'final_result_mismatch')
1511
+ reporter.reportTranscriptCompleted({
1512
+ taskId,
1513
+ sequence: nextSequence(),
1514
+ source: 'result',
1515
+ characterCount: stableCharacterCount,
1516
+ sha256: createHash('sha256').update(authoritativeText, 'utf8').digest('hex'),
1517
+ })
1518
+ },
1519
+ }
1520
+ }
1521
+
1522
+ async function consumeTaskEvents(baseUrl, apiKey, taskId, initialTask, reporter, signal, transcriptState = null) {
1310
1523
  const response = await fetch(`${baseUrl}/api/cli/transcriptions/${encodeURIComponent(taskId)}/events`, {
1311
1524
  method: 'GET',
1312
1525
  signal,
@@ -1316,12 +1529,13 @@ async function consumeTaskEvents(baseUrl, apiKey, taskId, initialTask, reporter,
1316
1529
  })
1317
1530
 
1318
1531
  if (!response.ok || !response.body) {
1319
- throw new Error(await readErrorMessage(response))
1532
+ throw new HttpCliError(await readErrorMessage(response), response.status)
1320
1533
  }
1321
1534
 
1322
1535
  const reader = response.body.getReader()
1323
1536
  const decoder = new TextDecoder()
1324
1537
  let buffer = ''
1538
+ let streamDone = false
1325
1539
  let task = {
1326
1540
  ...initialTask,
1327
1541
  }
@@ -1365,68 +1579,117 @@ async function consumeTaskEvents(baseUrl, apiKey, taskId, initialTask, reporter,
1365
1579
  reporter.reportProgress(data)
1366
1580
  }
1367
1581
 
1368
- while (true) {
1369
- const { value, done } = await reader.read()
1582
+ function processSseBlock(block) {
1583
+ const normalizedBlock = block.replace(/\r\n?/g, '\n').trim()
1370
1584
 
1371
- if (done) {
1372
- break
1585
+ if (!normalizedBlock) {
1586
+ return false
1373
1587
  }
1374
1588
 
1375
- buffer += decoder.decode(value, { stream: true })
1376
- buffer = buffer.replace(/\r\n/g, '\n')
1589
+ const { event, data } = parseSseBlock(normalizedBlock)
1377
1590
 
1378
- let separatorIndex = buffer.indexOf('\n\n')
1591
+ if (event === 'job.snapshot') {
1592
+ transcriptState?.applySseSnapshot(data?.committedText)
1379
1593
 
1380
- while (separatorIndex >= 0) {
1381
- const block = buffer.slice(0, separatorIndex).trim()
1382
- buffer = buffer.slice(separatorIndex + 2)
1383
- separatorIndex = buffer.indexOf('\n\n')
1384
-
1385
- if (!block) {
1386
- continue
1594
+ if (data?.status === 'transcribing') {
1595
+ renderProgress(data)
1596
+ } else if (TERMINAL_STATUSES.has(data?.status)) {
1597
+ applyTerminal(data.status, data?.error || data?.message || null)
1598
+ } else {
1599
+ applyStatus(data?.status)
1387
1600
  }
1601
+ return TERMINAL_STATUSES.has(data?.status)
1602
+ }
1388
1603
 
1389
- const { event, data } = parseSseBlock(block)
1604
+ if (event === 'job.status_changed') {
1605
+ applyStatus(data?.status)
1606
+ return TERMINAL_STATUSES.has(data?.status)
1607
+ }
1390
1608
 
1391
- if (event === 'job.snapshot') {
1392
- if (data?.status === 'transcribing') {
1393
- renderProgress(data)
1394
- } else if (TERMINAL_STATUSES.has(data?.status)) {
1395
- applyTerminal(data.status, data?.error || data?.message || null)
1396
- } else {
1397
- applyStatus(data?.status)
1398
- }
1399
- continue
1400
- }
1609
+ if (event === 'job.progress') {
1610
+ renderProgress(data)
1611
+ return false
1612
+ }
1401
1613
 
1402
- if (event === 'job.status_changed') {
1403
- applyStatus(data?.status)
1404
- continue
1405
- }
1614
+ if (event === 'transcript.delta') {
1615
+ transcriptState?.applySseSnapshot(data?.committedText)
1616
+ return false
1617
+ }
1406
1618
 
1407
- if (event === 'job.progress') {
1408
- renderProgress(data)
1409
- continue
1619
+ if (event === 'job.completed') {
1620
+ applyTerminal('completed', null)
1621
+ return true
1622
+ }
1623
+
1624
+ if (event === 'job.failed') {
1625
+ applyTerminal('failed', data?.error || data?.message || '转写失败')
1626
+ return true
1627
+ }
1628
+
1629
+ if (event === 'job.cancelled') {
1630
+ applyTerminal('cancelled', '转写任务已取消')
1631
+ return true
1632
+ }
1633
+
1634
+ return false
1635
+ }
1636
+
1637
+ function drainSseBuffer(flush = false) {
1638
+ while (true) {
1639
+ const separator = /\r\n\r\n|\n\n|\r\r/.exec(buffer)
1640
+
1641
+ if (!separator || typeof separator.index !== 'number') {
1642
+ break
1410
1643
  }
1411
1644
 
1412
- if (event === 'job.completed') {
1413
- applyTerminal('completed', null)
1414
- return task
1645
+ const block = buffer.slice(0, separator.index)
1646
+ buffer = buffer.slice(separator.index + separator[0].length)
1647
+
1648
+ if (processSseBlock(block)) {
1649
+ return true
1415
1650
  }
1651
+ }
1416
1652
 
1417
- if (event === 'job.failed') {
1418
- applyTerminal('failed', data?.error || data?.message || '转写失败')
1419
- return task
1653
+ if (flush && buffer.trim()) {
1654
+ const tail = buffer
1655
+ buffer = ''
1656
+ return processSseBlock(tail)
1657
+ }
1658
+
1659
+ return false
1660
+ }
1661
+
1662
+ try {
1663
+ while (true) {
1664
+ const { value, done } = await reader.read()
1665
+
1666
+ if (done) {
1667
+ streamDone = true
1668
+ buffer += decoder.decode()
1669
+ if (drainSseBuffer(true)) {
1670
+ return task
1671
+ }
1672
+ break
1420
1673
  }
1421
1674
 
1422
- if (event === 'job.cancelled') {
1423
- applyTerminal('cancelled', '转写任务已取消')
1675
+ buffer += decoder.decode(value, { stream: true })
1676
+
1677
+ if (drainSseBuffer()) {
1424
1678
  return task
1425
1679
  }
1426
1680
  }
1681
+ } finally {
1682
+ if (!streamDone) {
1683
+ await reader.cancel().catch(() => {})
1684
+ }
1685
+
1686
+ try {
1687
+ reader.releaseLock()
1688
+ } catch {
1689
+ // The stream may already have released its lock while aborting.
1690
+ }
1427
1691
  }
1428
1692
 
1429
- reporter.close()
1430
1693
  return task
1431
1694
  }
1432
1695
 
@@ -1953,11 +2216,36 @@ async function createCliTask(baseUrl, apiKey, createPayload, reporter, runContex
1953
2216
  return created
1954
2217
  }
1955
2218
 
1956
- async function waitForTaskAndOutput(baseUrl, apiKey, task, args, format, reporter, runContext) {
1957
- let currentTask = task
2219
+ function reportTaskStatusTransition(reporter, previousStatus, task) {
2220
+ if (task.status === previousStatus) {
2221
+ return task.status
2222
+ }
2223
+
2224
+ if (TERMINAL_STATUSES.has(task.status)) {
2225
+ reporter.reportTerminal(task.status, task.error || null)
2226
+ } else {
2227
+ reporter.reportStatusChanged(task.status)
2228
+ }
2229
+
2230
+ return task.status
2231
+ }
2232
+
2233
+ async function waitForTranscriptionTask(baseUrl, apiKey, initialTask, reporter, runContext, transcriptState) {
2234
+ let currentTask = initialTask
1958
2235
  let lastStatus = currentTask.status
2236
+ let lastSseError = null
2237
+
2238
+ for (let attempt = 0; attempt <= SSE_RECONNECT_DELAYS_MS.length; attempt += 1) {
2239
+ if (attempt > 0) {
2240
+ const delayMs = SSE_RECONNECT_DELAYS_MS[attempt - 1]
2241
+ reporter.reportNotice(`事件流中断,${delayMs / 1000} 秒后重连${lastSseError ? `:${lastSseError}` : ''}`)
2242
+ await sleep(delayMs)
2243
+ }
2244
+
2245
+ if (runContext.state.interrupting) {
2246
+ throw new ReportedCliError('已中断', 130)
2247
+ }
1959
2248
 
1960
- if (currentTask.mode === 'transcription' && !TERMINAL_STATUSES.has(currentTask.status)) {
1961
2249
  try {
1962
2250
  currentTask = await consumeTaskEvents(
1963
2251
  baseUrl,
@@ -1966,17 +2254,33 @@ async function waitForTaskAndOutput(baseUrl, apiKey, task, args, format, reporte
1966
2254
  currentTask,
1967
2255
  reporter,
1968
2256
  runContext.signal,
2257
+ transcriptState,
1969
2258
  )
1970
2259
  lastStatus = currentTask.status
2260
+
2261
+ if (TERMINAL_STATUSES.has(currentTask.status)) {
2262
+ return currentTask
2263
+ }
2264
+
2265
+ lastSseError = '服务端在任务结束前关闭了事件流'
1971
2266
  } catch (error) {
1972
2267
  if (runContext.state.interrupting || isAbortError(error)) {
1973
2268
  throw new ReportedCliError('已中断', 130)
1974
2269
  }
1975
2270
 
1976
- reporter.reportNotice(`事件流中断,回退到轮询状态:${error instanceof Error ? error.message : String(error)}`)
2271
+ if (!isRetryableRequestError(error)) {
2272
+ throw error
2273
+ }
2274
+
2275
+ lastSseError = error instanceof Error ? error.message : String(error)
1977
2276
  }
1978
2277
  }
1979
2278
 
2279
+ reporter.reportNotice(`事件流连续中断,回退到状态轮询${transcriptState ? '和正文预览' : ''}`)
2280
+
2281
+ let previewAvailable = !!transcriptState
2282
+ let lastSseRecoveryAt = Date.now()
2283
+
1980
2284
  while (!TERMINAL_STATUSES.has(currentTask.status)) {
1981
2285
  await sleep(POLL_INTERVAL_MS)
1982
2286
 
@@ -1984,26 +2288,115 @@ async function waitForTaskAndOutput(baseUrl, apiKey, task, args, format, reporte
1984
2288
  throw new ReportedCliError('已中断', 130)
1985
2289
  }
1986
2290
 
1987
- currentTask = await requestJson(
1988
- baseUrl,
1989
- apiKey,
1990
- `/api/cli/transcriptions/${encodeURIComponent(currentTask.taskId)}`,
1991
- {
1992
- signal: runContext.signal,
1993
- retry: true,
1994
- },
1995
- )
2291
+ try {
2292
+ currentTask = await requestJson(
2293
+ baseUrl,
2294
+ apiKey,
2295
+ `/api/cli/transcriptions/${encodeURIComponent(currentTask.taskId)}`,
2296
+ {
2297
+ signal: runContext.signal,
2298
+ retry: true,
2299
+ },
2300
+ )
2301
+ lastStatus = reportTaskStatusTransition(reporter, lastStatus, currentTask)
2302
+ } catch (error) {
2303
+ if (runContext.state.interrupting || isAbortError(error)) {
2304
+ throw new ReportedCliError('已中断', 130)
2305
+ }
1996
2306
 
1997
- if (currentTask.status !== lastStatus) {
1998
- if (TERMINAL_STATUSES.has(currentTask.status)) {
1999
- reporter.reportTerminal(currentTask.status, currentTask.error || null)
2000
- } else {
2001
- reporter.reportStatusChanged(currentTask.status)
2307
+ if (!isRetryableRequestError(error)) {
2308
+ throw error
2002
2309
  }
2310
+
2311
+ reporter.reportNotice(`状态轮询失败,将继续重试:${error instanceof Error ? error.message : String(error)}`)
2312
+ }
2313
+
2314
+ if (TERMINAL_STATUSES.has(currentTask.status)) {
2315
+ break
2316
+ }
2317
+
2318
+ if (previewAvailable && transcriptState) {
2319
+ try {
2320
+ const preview = await requestTranscriptPreview(baseUrl, apiKey, currentTask.taskId, {
2321
+ signal: runContext.signal,
2322
+ })
2323
+
2324
+ if (!preview.available) {
2325
+ previewAvailable = false
2326
+ reporter.reportNotice('当前服务端不支持 CLI 正文预览,将继续等待最终结果')
2327
+ } else if (preview.text !== null) {
2328
+ transcriptState.applyPreviewSnapshot(preview.text)
2329
+ }
2330
+ } catch (error) {
2331
+ if (runContext.state.interrupting || isAbortError(error)) {
2332
+ throw new ReportedCliError('已中断', 130)
2333
+ }
2334
+
2335
+ if (!isRetryableRequestError(error)) {
2336
+ throw error
2337
+ }
2338
+
2339
+ reporter.reportNotice(`正文预览读取失败,将继续重试:${error instanceof Error ? error.message : String(error)}`)
2340
+ }
2341
+ }
2342
+
2343
+ if (Date.now() - lastSseRecoveryAt < SSE_RECOVERY_INTERVAL_MS) {
2344
+ continue
2345
+ }
2346
+
2347
+ lastSseRecoveryAt = Date.now()
2348
+
2349
+ try {
2350
+ reporter.reportNotice('正在尝试恢复事件流')
2351
+ currentTask = await consumeTaskEvents(
2352
+ baseUrl,
2353
+ apiKey,
2354
+ currentTask.taskId,
2355
+ currentTask,
2356
+ reporter,
2357
+ runContext.signal,
2358
+ transcriptState,
2359
+ )
2003
2360
  lastStatus = currentTask.status
2361
+
2362
+ if (TERMINAL_STATUSES.has(currentTask.status)) {
2363
+ return currentTask
2364
+ }
2365
+
2366
+ reporter.reportNotice('事件流再次提前结束,将继续轮询')
2367
+ } catch (error) {
2368
+ if (runContext.state.interrupting || isAbortError(error)) {
2369
+ throw new ReportedCliError('已中断', 130)
2370
+ }
2371
+
2372
+ if (!isRetryableRequestError(error)) {
2373
+ throw error
2374
+ }
2375
+
2376
+ reporter.reportNotice(`事件流恢复失败,将继续轮询:${error instanceof Error ? error.message : String(error)}`)
2004
2377
  }
2005
2378
  }
2006
2379
 
2380
+ return currentTask
2381
+ }
2382
+
2383
+ async function waitForTaskAndOutput(baseUrl, apiKey, task, args, format, reporter, runContext) {
2384
+ let currentTask = task
2385
+ const transcriptState = args.streamTranscript
2386
+ ? createTranscriptStreamState(currentTask.taskId, reporter)
2387
+ : null
2388
+
2389
+ if (currentTask.mode === 'transcription' && !TERMINAL_STATUSES.has(currentTask.status)) {
2390
+ currentTask = await waitForTranscriptionTask(
2391
+ baseUrl,
2392
+ apiKey,
2393
+ currentTask,
2394
+ reporter,
2395
+ runContext,
2396
+ transcriptState,
2397
+ )
2398
+ }
2399
+
2007
2400
  reporter.reportTerminal(currentTask.status, currentTask.error || null)
2008
2401
 
2009
2402
  if (currentTask.status !== 'completed') {
@@ -2016,10 +2409,12 @@ async function waitForTaskAndOutput(baseUrl, apiKey, task, args, format, reporte
2016
2409
 
2017
2410
  if (args.output) {
2018
2411
  const outputPath = await writeOutput(args.output, result)
2412
+ transcriptState?.complete(result)
2019
2413
  reporter.reportResultWritten(outputPath, format)
2020
2414
  return
2021
2415
  }
2022
2416
 
2417
+ transcriptState?.complete(result)
2023
2418
  reporter.close()
2024
2419
  process.stdout.write(result)
2025
2420
 
@@ -2030,6 +2425,8 @@ async function waitForTaskAndOutput(baseUrl, apiKey, task, args, format, reporte
2030
2425
 
2031
2426
  async function handleTranscription(argv) {
2032
2427
  const args = parseTranscriptionArgs(argv)
2428
+ const format = inferFormat(args.output, args.format)
2429
+ validateTranscriptStreaming(args, format)
2033
2430
  const reporter = createProgressReporter(args.progressFormat)
2034
2431
  let runContext = null
2035
2432
 
@@ -2055,7 +2452,6 @@ async function handleTranscription(argv) {
2055
2452
  fail('缺少内容链接或本地文件')
2056
2453
  }
2057
2454
 
2058
- const format = inferFormat(args.output, args.format)
2059
2455
  runContext = createRunContext(baseUrl, resolvedApiKey.apiKey, reporter)
2060
2456
  let createPayload
2061
2457
  let created
@@ -2132,10 +2528,19 @@ async function main() {
2132
2528
  return
2133
2529
  }
2134
2530
 
2531
+ if (primaryCommand === 'skill') {
2532
+ await handleSkillCommand(argv, CLI_VERSION)
2533
+ return
2534
+ }
2535
+
2135
2536
  await handleTranscription(argv)
2136
2537
  }
2137
2538
 
2138
2539
  main().catch((error) => {
2540
+ if (error?.name === 'SkillCommandReportedError') {
2541
+ process.exit(error.exitCode || 1)
2542
+ }
2543
+
2139
2544
  if (error instanceof ReportedCliError) {
2140
2545
  process.exit(error.exitCode || 1)
2141
2546
  }