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