@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/README.md +116 -5
- package/deyo.mjs +467 -62
- package/package.json +7 -4
- package/skill-manager.mjs +1344 -0
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
|
-
|
|
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
|
|
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
|
-
|
|
1369
|
-
const
|
|
1582
|
+
function processSseBlock(block) {
|
|
1583
|
+
const normalizedBlock = block.replace(/\r\n?/g, '\n').trim()
|
|
1370
1584
|
|
|
1371
|
-
if (
|
|
1372
|
-
|
|
1585
|
+
if (!normalizedBlock) {
|
|
1586
|
+
return false
|
|
1373
1587
|
}
|
|
1374
1588
|
|
|
1375
|
-
|
|
1376
|
-
buffer = buffer.replace(/\r\n/g, '\n')
|
|
1589
|
+
const { event, data } = parseSseBlock(normalizedBlock)
|
|
1377
1590
|
|
|
1378
|
-
|
|
1591
|
+
if (event === 'job.snapshot') {
|
|
1592
|
+
transcriptState?.applySseSnapshot(data?.committedText)
|
|
1379
1593
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1604
|
+
if (event === 'job.status_changed') {
|
|
1605
|
+
applyStatus(data?.status)
|
|
1606
|
+
return TERMINAL_STATUSES.has(data?.status)
|
|
1607
|
+
}
|
|
1390
1608
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1614
|
+
if (event === 'transcript.delta') {
|
|
1615
|
+
transcriptState?.applySseSnapshot(data?.committedText)
|
|
1616
|
+
return false
|
|
1617
|
+
}
|
|
1406
1618
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1957
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
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
|
-
|
|
1998
|
-
|
|
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
|
}
|