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