dsh-remote-bridge 2.7.1 → 2.8.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.en.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.7.1-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.8.0-087f8c">
13
13
  <img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
14
14
  <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
15
15
  <img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
@@ -402,7 +402,7 @@ inside the zero-change set, and 4 slots retained with 0 removed.
402
402
 
403
403
  ## Project
404
404
 
405
- - Version: `v2.7.1`
405
+ - Version: `v2.8.0`
406
406
  - **v2.7.1 is a defect-fix release**: the kernel's `sessionController.resolveAgent()` returns an
407
407
  `{ agent }` / `{ error }` **envelope**, not the Agent itself (`dsh-api-session-controller/lib/index.js:365-368`
408
408
  `liveAgent()` and `:221-246` `resolve()` both have that shape). The bridge used to hand the envelope
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.7.1-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.8.0-087f8c">
13
13
  <img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
14
14
  <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
15
15
  <img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
@@ -433,7 +433,7 @@ DSH **0.2.1-rc.3** 隔离实例上**实跑验过**,不是静态推断:插件
433
433
 
434
434
  ## 项目信息
435
435
 
436
- - 当前版本:`v2.7.1`
436
+ - 当前版本:`v2.8.0`
437
437
  - **v2.7.1 是缺陷修复版**:内核 `sessionController.resolveAgent()` 返回的是 `{ agent }` / `{ error }`
438
438
  **包装对象**而非 Agent 本体(`dsh-api-session-controller/lib/index.js:365-368` 的 `liveAgent()` 与
439
439
  `:221-246` 的 `resolve()` 都是这个形状),桥此前把包装对象直接当 Agent 传给 `fileUploads.upload()`,
@@ -1,7 +1,7 @@
1
1
  asyncapi: 3.0.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile Events
4
- version: 2.7.1
4
+ version: 2.8.0
5
5
  description: |
6
6
  Live file and authorized DSH chat events. Reconnect clients must refresh
7
7
  REST state; unbounded replay is not provided.
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile API
4
- version: 2.7.1
4
+ version: 2.8.0
5
5
  description: Versioned file and chat BFF for mobile and third-party DSH clients. HTTP and HTTPS are both supported.
6
6
  servers:
7
7
  - url: http://127.0.0.1:3090/api/v1
@@ -24,7 +24,7 @@ paths:
24
24
  properties:
25
25
  ok: { type: boolean, const: true }
26
26
  version: { type: string, const: v1 }
27
- pluginVersion: { type: string, const: 2.7.1 }
27
+ pluginVersion: { type: string, const: 2.8.0 }
28
28
  /debug/host-services:
29
29
  get:
30
30
  operationId: hostServices
@@ -1319,6 +1319,332 @@ paths:
1319
1319
  schema: { type: string }
1320
1320
  responses:
1321
1321
  '202': { description: Cancellation accepted. }
1322
+ /chat/sessions/{sessionId}/attachments/uploads:
1323
+ post:
1324
+ operationId: openChunkedAttachmentUpload
1325
+ description: |
1326
+ Opens — or resumes — a chunked attachment upload for one DSH session
1327
+ (`docs/project/BFF-REST-CONTRACT.md` §1).
1328
+
1329
+ `chunkSize` is chosen by the server and must be obeyed by the client.
1330
+ Re-posting the same `name` + `size` while an upload is still alive returns the
1331
+ **same** `uploadId` plus `alreadyReceived` (the chunk indexes the server already
1332
+ holds), which is how a dropped connection resumes without re-sending bytes.
1333
+ Tickets expire one hour after opening; an expired or unknown id answers 404.
1334
+
1335
+ Errors: `400 BODY_INVALID` (bad name/size/contentType), `404 NOT_FOUND` (unknown
1336
+ session), `413 PAYLOAD_TOO_LARGE` (`size` above the 256 MiB ceiling).
1337
+ parameters:
1338
+ - name: sessionId
1339
+ in: path
1340
+ required: true
1341
+ schema: { type: string }
1342
+ requestBody:
1343
+ required: true
1344
+ content:
1345
+ application/json:
1346
+ schema: { $ref: '#/components/schemas/ChunkedUploadOpenRequest' }
1347
+ responses:
1348
+ '201':
1349
+ description: Upload ticket opened (or reused).
1350
+ content:
1351
+ application/json:
1352
+ schema: { $ref: '#/components/schemas/ChunkedUploadTicket' }
1353
+ '400': { $ref: '#/components/responses/Error' }
1354
+ '404': { $ref: '#/components/responses/Error' }
1355
+ '413': { $ref: '#/components/responses/Error' }
1356
+ get:
1357
+ operationId: listChunkedAttachmentUploads
1358
+ description: |
1359
+ Lists the session's unfinished upload tickets so a cold-started client can
1360
+ resume without re-opening. Expired tickets are omitted; `missing` on each item
1361
+ is the same chunk-index list `complete` would report.
1362
+ parameters:
1363
+ - name: sessionId
1364
+ in: path
1365
+ required: true
1366
+ schema: { type: string }
1367
+ responses:
1368
+ '200':
1369
+ description: Unfinished uploads for this session, soonest expiry first.
1370
+ content:
1371
+ application/json:
1372
+ schema:
1373
+ type: object
1374
+ required: [items]
1375
+ properties:
1376
+ items:
1377
+ type: array
1378
+ items: { $ref: '#/components/schemas/ChunkedUploadStatus' }
1379
+ '404': { $ref: '#/components/responses/Error' }
1380
+ /chat/sessions/{sessionId}/attachments/uploads/{uploadId}:
1381
+ get:
1382
+ operationId: getChunkedAttachmentUpload
1383
+ description: |
1384
+ Upload progress. `missing` is the authoritative retry list — the same array
1385
+ `POST …/complete` returns in `details.missing` when chunks are absent.
1386
+ Unknown, expired, or other-session ids all answer `404 NOT_FOUND`
1387
+ (indistinguishable on purpose).
1388
+ parameters:
1389
+ - name: sessionId
1390
+ in: path
1391
+ required: true
1392
+ schema: { type: string }
1393
+ - name: uploadId
1394
+ in: path
1395
+ required: true
1396
+ schema: { type: string }
1397
+ responses:
1398
+ '200':
1399
+ description: Current ticket state.
1400
+ content:
1401
+ application/json:
1402
+ schema: { $ref: '#/components/schemas/ChunkedUploadStatus' }
1403
+ '404': { $ref: '#/components/responses/Error' }
1404
+ delete:
1405
+ operationId: cancelChunkedAttachmentUpload
1406
+ description: Discards the ticket and its spooled chunks. The id is dead afterwards (404).
1407
+ parameters:
1408
+ - name: sessionId
1409
+ in: path
1410
+ required: true
1411
+ schema: { type: string }
1412
+ - name: uploadId
1413
+ in: path
1414
+ required: true
1415
+ schema: { type: string }
1416
+ responses:
1417
+ '204': { description: 'Ticket discarded; no body.' }
1418
+ '404': { $ref: '#/components/responses/Error' }
1419
+ /chat/sessions/{sessionId}/attachments/uploads/{uploadId}/chunks/{index}:
1420
+ put:
1421
+ operationId: putChunkedAttachmentChunk
1422
+ description: |
1423
+ Stores one chunk. `index` is zero-based; repeating a `PUT` on the same index is
1424
+ idempotent (the chunk is overwritten), which makes retries safe.
1425
+
1426
+ Byte length must be **exact**: `chunkSize` for every chunk but the last, and
1427
+ `size - index * chunkSize` for the last one. A body longer than `chunkSize`
1428
+ answers `413 PAYLOAD_TOO_LARGE`; a wrong-but-not-oversized length answers
1429
+ `400 BODY_INVALID`; an index outside `[0, count)` answers `400 BODY_INVALID`.
1430
+ The body is `application/octet-stream`, not JSON and not base64.
1431
+ parameters:
1432
+ - name: sessionId
1433
+ in: path
1434
+ required: true
1435
+ schema: { type: string }
1436
+ - name: uploadId
1437
+ in: path
1438
+ required: true
1439
+ schema: { type: string }
1440
+ - name: index
1441
+ in: path
1442
+ required: true
1443
+ schema: { type: integer, minimum: 0 }
1444
+ requestBody:
1445
+ required: true
1446
+ content:
1447
+ application/octet-stream:
1448
+ schema: { type: string, format: binary }
1449
+ responses:
1450
+ '200':
1451
+ description: Chunk stored.
1452
+ content:
1453
+ application/json:
1454
+ schema: { $ref: '#/components/schemas/ChunkedUploadChunkResult' }
1455
+ '400': { $ref: '#/components/responses/Error' }
1456
+ '404': { $ref: '#/components/responses/Error' }
1457
+ '413': { $ref: '#/components/responses/Error' }
1458
+ /chat/sessions/{sessionId}/attachments/uploads/{uploadId}/complete:
1459
+ post:
1460
+ operationId: completeChunkedAttachmentUpload
1461
+ description: |
1462
+ Assembles the chunks and hands the bytes to the same kernel attachment path the
1463
+ single-shot `POST /chat/sessions/{sessionId}/attachments` uses, so the resulting
1464
+ receipt has the same meaning. On success the ticket is void (a second `complete`
1465
+ answers 404).
1466
+
1467
+ Missing chunks answer `409 CONFLICT` with `details.missing` = the chunk indexes
1468
+ still owed, so the client can send exactly those and retry.
1469
+ parameters:
1470
+ - name: sessionId
1471
+ in: path
1472
+ required: true
1473
+ schema: { type: string }
1474
+ - name: uploadId
1475
+ in: path
1476
+ required: true
1477
+ schema: { type: string }
1478
+ responses:
1479
+ '201':
1480
+ description: Attachment stored in the session.
1481
+ content:
1482
+ application/json:
1483
+ schema: { $ref: '#/components/schemas/ChunkedUploadReceipt' }
1484
+ '404': { $ref: '#/components/responses/Error' }
1485
+ '409': { $ref: '#/components/responses/Error' }
1486
+ /chat/schedules:
1487
+ get:
1488
+ operationId: listSchedules
1489
+ description: |
1490
+ Lists cron-shaped schedules (`docs/project/BFF-REST-CONTRACT.md` §2).
1491
+
1492
+ Every item is a **cron projection** of the kernel record: `daily`/`weekly`
1493
+ records are converted to their equivalent five-field cron; one-shot (`at`) and
1494
+ fixed-interval (`every`) records cannot be expressed that way and are therefore
1495
+ omitted rather than faked. `createdAt` is always `null` because the kernel record
1496
+ carries no creation timestamp.
1497
+ parameters:
1498
+ - name: sessionId
1499
+ in: query
1500
+ required: false
1501
+ schema: { type: string }
1502
+ responses:
1503
+ '200':
1504
+ description: Schedules, optionally filtered to one session.
1505
+ content:
1506
+ application/json:
1507
+ schema:
1508
+ type: object
1509
+ required: [items]
1510
+ properties:
1511
+ items:
1512
+ type: array
1513
+ items: { $ref: '#/components/schemas/Schedule' }
1514
+ '404': { $ref: '#/components/responses/Error' }
1515
+ '503': { $ref: '#/components/responses/Error' }
1516
+ post:
1517
+ operationId: createSchedule
1518
+ description: |
1519
+ Creates a cron schedule for one DSH session.
1520
+
1521
+ `cron` must be a five-field Vixie expression; `timezone` must be `UTC` or a real
1522
+ IANA `Area/Location` name — anything else is `400 BODY_INVALID` before the kernel
1523
+ is involved. An unknown session is `404 NOT_FOUND`.
1524
+
1525
+ `503 UPSTREAM_UNAVAILABLE` (with `details.reason`) means the kernel does not
1526
+ expose schedule creation to the bridge at all — the client must hide the entry
1527
+ point then, not retry.
1528
+ requestBody:
1529
+ required: true
1530
+ content:
1531
+ application/json:
1532
+ schema: { $ref: '#/components/schemas/ScheduleCreateRequest' }
1533
+ responses:
1534
+ '201':
1535
+ description: Schedule created.
1536
+ content:
1537
+ application/json:
1538
+ schema: { $ref: '#/components/schemas/Schedule' }
1539
+ '400': { $ref: '#/components/responses/Error' }
1540
+ '404': { $ref: '#/components/responses/Error' }
1541
+ '503': { $ref: '#/components/responses/Error' }
1542
+ /chat/schedules/{id}:
1543
+ patch:
1544
+ operationId: updateSchedule
1545
+ description: |
1546
+ Updates only the fields present in the body.
1547
+
1548
+ `enabled: false` (pause) answers `400 BODY_INVALID` with
1549
+ `details.reason: ENABLED_UNSUPPORTED` — this kernel has no pause operation, and a
1550
+ silent success would show a paused reminder that keeps firing. Deleting is the
1551
+ supported way to stop one. `enabled: true` on an already-ended schedule answers
1552
+ `409 CONFLICT` (`details.reason: SCHEDULE_ENDED`).
1553
+ parameters:
1554
+ - name: id
1555
+ in: path
1556
+ required: true
1557
+ schema: { type: string }
1558
+ requestBody:
1559
+ required: true
1560
+ content:
1561
+ application/json:
1562
+ schema: { $ref: '#/components/schemas/SchedulePatch' }
1563
+ responses:
1564
+ '200':
1565
+ description: Updated schedule.
1566
+ content:
1567
+ application/json:
1568
+ schema: { $ref: '#/components/schemas/Schedule' }
1569
+ '400': { $ref: '#/components/responses/Error' }
1570
+ '404': { $ref: '#/components/responses/Error' }
1571
+ '409': { $ref: '#/components/responses/Error' }
1572
+ '503': { $ref: '#/components/responses/Error' }
1573
+ delete:
1574
+ operationId: deleteSchedule
1575
+ description: Removes the schedule. Unknown id answers 404.
1576
+ parameters:
1577
+ - name: id
1578
+ in: path
1579
+ required: true
1580
+ schema: { type: string }
1581
+ responses:
1582
+ '204': { description: 'Deleted; no body.' }
1583
+ '404': { $ref: '#/components/responses/Error' }
1584
+ '503': { $ref: '#/components/responses/Error' }
1585
+ /chat/speech/catalog:
1586
+ get:
1587
+ operationId: getSpeechCatalog
1588
+ description: |
1589
+ Official speech input catalog (`docs/project/BFF-REST-CONTRACT.md` §3).
1590
+
1591
+ When the kernel has no speech service this is **not** a 404: it answers `200`
1592
+ with `available: false`, an empty `providers` and a `reason`. That is the same
1593
+ judgement the `/ws/mobile` `speech-catalog` frame makes, and it lets an old
1594
+ client hide the entry point without treating a missing feature as a failure.
1595
+ responses:
1596
+ '200':
1597
+ description: Catalog, or the unavailable view.
1598
+ content:
1599
+ application/json:
1600
+ schema: { $ref: '#/components/schemas/SpeechCatalog' }
1601
+ /chat/speech/transcriptions:
1602
+ post:
1603
+ operationId: createSpeechTranscription
1604
+ description: |
1605
+ Transcribes one base64 WAV clip.
1606
+
1607
+ Decoded audio must be ≤ 4 MiB — the same ceiling `/ws/mobile` enforces, so a clip
1608
+ does not become acceptable merely by taking the REST path; over the limit answers
1609
+ `413 PAYLOAD_TOO_LARGE`. Transcription gives up after 180 s. An empty transcript
1610
+ is a **success** (`text: ""`), not an error: silence is a legal answer.
1611
+
1612
+ Upstream failures answer `503 UPSTREAM_UNAVAILABLE` with `details.reason`; the
1613
+ kernel's raw message is never echoed (it can contain host paths).
1614
+ requestBody:
1615
+ required: true
1616
+ content:
1617
+ application/json:
1618
+ schema: { $ref: '#/components/schemas/SpeechTranscriptionRequest' }
1619
+ responses:
1620
+ '200':
1621
+ description: Transcript (possibly empty).
1622
+ content:
1623
+ application/json:
1624
+ schema: { $ref: '#/components/schemas/SpeechTranscription' }
1625
+ '400': { $ref: '#/components/responses/Error' }
1626
+ '413': { $ref: '#/components/responses/Error' }
1627
+ '503': { $ref: '#/components/responses/Error' }
1628
+ /chat/speech/config:
1629
+ put:
1630
+ operationId: configureSpeech
1631
+ description: |
1632
+ Changes the selected provider and/or language and returns the catalog view
1633
+ afterwards. At least one of `providerId`/`language` is required
1634
+ (`400 BODY_INVALID` otherwise).
1635
+ requestBody:
1636
+ required: true
1637
+ content:
1638
+ application/json:
1639
+ schema: { $ref: '#/components/schemas/SpeechConfigPatch' }
1640
+ responses:
1641
+ '200':
1642
+ description: Catalog after the change.
1643
+ content:
1644
+ application/json:
1645
+ schema: { $ref: '#/components/schemas/SpeechCatalog' }
1646
+ '400': { $ref: '#/components/responses/Error' }
1647
+ '503': { $ref: '#/components/responses/Error' }
1322
1648
  components:
1323
1649
  securitySchemes:
1324
1650
  bearerAuth:
@@ -1889,3 +2215,159 @@ components:
1889
2215
  detail: { type: string, description: 'Terminal reason (`exit code: 3`); a recorded kill reason is merged in.' }
1890
2216
  startedAt: { type: integer, format: epoch-ms }
1891
2217
  finishedAt: { type: integer, format: epoch-ms, description: Absent while live. }
2218
+ ChunkedUploadOpenRequest:
2219
+ type: object
2220
+ description: Opens or resumes a chunked upload. `size` is the exact byte count the client intends to send.
2221
+ required: [name, size]
2222
+ properties:
2223
+ name:
2224
+ type: string
2225
+ minLength: 1
2226
+ maxLength: 255
2227
+ description: 'Leaf file name only — path separators and control characters are refused.'
2228
+ size:
2229
+ type: integer
2230
+ minimum: 1
2231
+ maximum: 268435456
2232
+ description: 'Declared byte count, 1..256 MiB; above the ceiling answers 413.'
2233
+ contentType:
2234
+ type: [string, 'null']
2235
+ description: 'Optional MIME hint. Omitted → derived from the file extension at `complete`.'
2236
+ ChunkedUploadTicket:
2237
+ type: object
2238
+ description: Server-decided ticket for one chunked upload.
2239
+ required: [uploadId, chunkSize, expiresAt, alreadyReceived]
2240
+ properties:
2241
+ uploadId: { type: string, description: 'Opaque id, e.g. `up_9f3a…`; valid for one hour.' }
2242
+ chunkSize: { type: integer, description: 'Fixed server-side slice size (1048576 today); clients must obey it.' }
2243
+ expiresAt: { type: integer, format: epoch-ms }
2244
+ alreadyReceived:
2245
+ type: array
2246
+ description: 'Chunks the server already holds — empty on a fresh open, populated when a resume matched.'
2247
+ items: { type: integer, minimum: 0 }
2248
+ ChunkedUploadStatus:
2249
+ type: object
2250
+ required: [uploadId, name, size, chunkSize, receivedCount, count, expiresAt, missing]
2251
+ properties:
2252
+ uploadId: { type: string }
2253
+ name: { type: string }
2254
+ size: { type: integer }
2255
+ chunkSize: { type: integer }
2256
+ receivedCount: { type: integer, description: 'Distinct chunks held so far.' }
2257
+ count: { type: integer, description: 'Total chunks the declared size needs.' }
2258
+ expiresAt: { type: integer, format: epoch-ms }
2259
+ missing:
2260
+ type: array
2261
+ description: 'Chunk indexes still owed, ascending. Same list `complete` reports in `details.missing`.'
2262
+ items: { type: integer, minimum: 0 }
2263
+ ChunkedUploadChunkResult:
2264
+ type: object
2265
+ required: [uploadId, received, count]
2266
+ properties:
2267
+ uploadId: { type: string }
2268
+ received: { type: integer, description: 'Distinct chunks held after this write.' }
2269
+ count: { type: integer, description: 'Total chunks the declared size needs.' }
2270
+ ChunkedUploadReceipt:
2271
+ type: object
2272
+ description: 'Stored attachment. Same meaning as the single-shot upload receipt; `name`/`contentType` may be server-derived.'
2273
+ required: [id, receiptId, name, size, contentType, uploadedAt]
2274
+ properties:
2275
+ id: { type: string }
2276
+ receiptId: { type: string }
2277
+ name: { type: string }
2278
+ size: { type: integer }
2279
+ contentType: { type: string }
2280
+ uploadedAt: { type: integer, format: epoch-ms }
2281
+ Schedule:
2282
+ type: object
2283
+ description: |
2284
+ Cron projection of one DSH reminder. `createdAt` is always null — the kernel
2285
+ record has no creation timestamp; clients must not sort on it.
2286
+ required: [id, sessionId, text, cron, timezone, enabled, createdAt, lastRunAt, nextRunAt]
2287
+ properties:
2288
+ id: { type: string, description: 'Opaque id, e.g. `sch_…`.' }
2289
+ sessionId: { type: string }
2290
+ text: { type: string, maxLength: 8000, description: 'The reminder body (kernel `prompt`).' }
2291
+ cron: { type: string, description: 'Five-field Vixie expression, `minute hour day-of-month month day-of-week`.' }
2292
+ timezone: { type: string, description: 'IANA zone the expression is evaluated in.' }
2293
+ enabled: { type: boolean, description: 'True while the kernel still schedules it.' }
2294
+ createdAt: { type: [integer, 'null'], format: epoch-ms, description: 'Always null in this kernel generation.' }
2295
+ lastRunAt: { type: [integer, 'null'], format: epoch-ms, description: 'Last delivered occurrence, when the kernel reports one.' }
2296
+ nextRunAt: { type: [integer, 'null'], format: epoch-ms, description: 'Next planned occurrence.' }
2297
+ ScheduleCreateRequest:
2298
+ type: object
2299
+ required: [sessionId, text, cron, timezone]
2300
+ properties:
2301
+ sessionId: { type: string }
2302
+ text: { type: string, minLength: 1, maxLength: 8000 }
2303
+ cron: { type: string, description: 'Five-field Vixie expression; seconds and `@daily`-style macros are refused.' }
2304
+ timezone: { type: string, description: '`UTC` or a real IANA `Area/Location` name.' }
2305
+ SchedulePatch:
2306
+ type: object
2307
+ description: Send only the fields to change; omitted fields keep their stored value.
2308
+ minProperties: 1
2309
+ properties:
2310
+ enabled:
2311
+ type: boolean
2312
+ description: '`false` is refused (400, `details.reason: ENABLED_UNSUPPORTED`) — this kernel cannot pause; delete instead.'
2313
+ text: { type: string, minLength: 1, maxLength: 8000 }
2314
+ cron: { type: string }
2315
+ timezone: { type: string }
2316
+ SpeechCatalog:
2317
+ type: object
2318
+ description: 'Official speech input state. `available: false` is a 200 answer, never a 404 — hide the entry point, do not error.'
2319
+ required: [available, providers, selected, reason, maxAudioBytes, maxDurationSeconds]
2320
+ properties:
2321
+ available: { type: boolean }
2322
+ providers:
2323
+ type: array
2324
+ items: { $ref: '#/components/schemas/SpeechProvider' }
2325
+ selected:
2326
+ oneOf:
2327
+ - $ref: '#/components/schemas/SpeechSelection'
2328
+ - type: 'null'
2329
+ reason: { type: [string, 'null'], description: 'Why the feature is unavailable; null when it is available.' }
2330
+ maxAudioBytes: { type: integer, description: 'Hard ceiling for one clip on this device (4 MiB today).' }
2331
+ maxDurationSeconds: { type: [integer, 'null'], description: 'Kernel-declared clip length ceiling, when it reports one.' }
2332
+ SpeechProvider:
2333
+ type: object
2334
+ required: [id, name, languages, location]
2335
+ properties:
2336
+ id: { type: string, examples: [builtin] }
2337
+ name: { type: string, examples: [内置语音] }
2338
+ languages:
2339
+ type: array
2340
+ items: { type: string }
2341
+ examples: [[zh-CN, en-US]]
2342
+ location: { type: string, enum: [host-local, cloud], description: 'Whether inference runs on the host or in a provider cloud.' }
2343
+ SpeechSelection:
2344
+ type: object
2345
+ required: [providerId, language]
2346
+ properties:
2347
+ providerId: { type: string }
2348
+ language: { type: string }
2349
+ SpeechTranscriptionRequest:
2350
+ type: object
2351
+ required: [audio]
2352
+ properties:
2353
+ audio:
2354
+ type: string
2355
+ format: byte
2356
+ description: 'Base64 WAV (16 kHz PCM16 recommended). Decoded bytes must be ≤ 4 MiB; non-canonical base64 is refused.'
2357
+ providerId: { type: string, description: 'Defaults to the configured provider.' }
2358
+ language: { type: string, description: 'Defaults to the configured language.' }
2359
+ SpeechTranscription:
2360
+ type: object
2361
+ required: [text, providerId, language, durationMs]
2362
+ properties:
2363
+ text: { type: string, description: 'Empty string means "no speech recognised" — a success, not an error.' }
2364
+ providerId: { type: string }
2365
+ language: { type: string }
2366
+ durationMs: { type: integer, description: 'Audio duration reported by the kernel, in milliseconds.' }
2367
+ SpeechConfigPatch:
2368
+ type: object
2369
+ description: At least one field is required.
2370
+ minProperties: 1
2371
+ properties:
2372
+ providerId: { type: string }
2373
+ language: { type: string }