dsh-remote-bridge 2.7.0 → 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 +19 -5
- package/README.md +16 -5
- package/docs/api/asyncapi.yaml +1 -1
- package/docs/api/openapi.yaml +484 -2
- package/lib/index.d.ts +219 -3
- package/lib/index.js +5813 -1211
- package/package.json +2 -2
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.
|
|
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">
|
|
@@ -102,10 +102,10 @@ dsh plugin --profile web add dsh-remote-bridge
|
|
|
102
102
|
dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
|
|
103
103
|
|
|
104
104
|
# 2. tarball from the GitHub release
|
|
105
|
-
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.7.
|
|
105
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.7.1/dsh-remote-bridge-v2.7.1.tgz
|
|
106
106
|
|
|
107
107
|
# 3. tarball from the Gitee release (faster from mainland China)
|
|
108
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.7.
|
|
108
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.7.1/dsh-remote-bridge-v2.7.1.tgz
|
|
109
109
|
|
|
110
110
|
npx @deepseek-ai/dsh web
|
|
111
111
|
```
|
|
@@ -122,7 +122,7 @@ Restart DSH after installation, then open settings — The section is titled **
|
|
|
122
122
|
To install a downloaded release archive:
|
|
123
123
|
|
|
124
124
|
```sh
|
|
125
|
-
dsh plugin --profile web add ./dsh-remote-bridge-v2.7.
|
|
125
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.7.1.tgz
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
## First connection
|
|
@@ -386,6 +386,10 @@ pnpm pack
|
|
|
386
386
|
|
|
387
387
|
v2.5.0 was verified on DSH **0.2.1-rc.2** on real hardware: all 14/14 host services injected, the three jobs endpoints exercised, and the permission catalog returned the kernel's real three presets. The development baseline is upstream `dsh-v0.2.1-rc.2` (commit `687f282e8625c6b8d11ff58211a90524be2e4317`); see `engines.dsh` above for the supported range. CI covers Windows, Ubuntu, and macOS.
|
|
388
388
|
|
|
389
|
+
**v2.7.1 verification status**: 76 test files / **701 cases** green; `typecheck`, `docs:check`, and the
|
|
390
|
+
build all pass. This release touches only `agentFor` in `src/host/dsh-0.2-bridge.ts` plus its regression
|
|
391
|
+
tests; every other byte is identical to v2.7.0.
|
|
392
|
+
|
|
389
393
|
**v2.7.0 verification status**: 76 test files / 698 cases green, plus 8 Playwright browser cases
|
|
390
394
|
(including the 375px narrow-viewport contract); `typecheck`, `docs:check`, and the build all pass.
|
|
391
395
|
All four capabilities were exercised on an isolated DSH **0.2.1-rc.3** instance rather than inferred:
|
|
@@ -398,7 +402,17 @@ inside the zero-change set, and 4 slots retained with 0 removed.
|
|
|
398
402
|
|
|
399
403
|
## Project
|
|
400
404
|
|
|
401
|
-
- Version: `v2.
|
|
405
|
+
- Version: `v2.8.0`
|
|
406
|
+
- **v2.7.1 is a defect-fix release**: the kernel's `sessionController.resolveAgent()` returns an
|
|
407
|
+
`{ agent }` / `{ error }` **envelope**, not the Agent itself (`dsh-api-session-controller/lib/index.js:365-368`
|
|
408
|
+
`liveAgent()` and `:221-246` `resolve()` both have that shape). The bridge used to hand the envelope
|
|
409
|
+
straight to `fileUploads.upload()`, so the kernel reading `agent.session.header.origin` blew up with
|
|
410
|
+
`DSH_TYPEERROR: Cannot read properties of undefined (reading 'header')` — on a real device this showed up
|
|
411
|
+
as "sending a file always fails with 400/500 and the message points nowhere", breaking both attachment
|
|
412
|
+
upload and agent-preset selection. The bridge now detects the envelope, unwraps `.agent` on success, and
|
|
413
|
+
rethrows `.error` as-is (keeping the kernel's stable code, so "session not found" is no longer flattened
|
|
414
|
+
into "session not running"), while still accepting a bare object. Three regression tests pin all three
|
|
415
|
+
return shapes.
|
|
402
416
|
- **v2.7.0 aligns with DSH 0.2.1-rc.3 and lifts four capabilities**: (1) the model catalog now carries
|
|
403
417
|
`contextWindow` / `maxTokens` — the kernel's `ModelCatalogModel` only has id/name/description, so the
|
|
404
418
|
bridge resolves capacity through `llm.resolveModelInfo`, caching per `provider + model` for ten
|
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.
|
|
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">
|
|
@@ -93,10 +93,10 @@ dsh plugin --profile web add dsh-remote-bridge
|
|
|
93
93
|
dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
|
|
94
94
|
|
|
95
95
|
# ② GitHub Release 的 tarball
|
|
96
|
-
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.7.
|
|
96
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.7.1/dsh-remote-bridge-v2.7.1.tgz
|
|
97
97
|
|
|
98
98
|
# ③ Gitee Release 的 tarball(国内访问更快)
|
|
99
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.7.
|
|
99
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.7.1/dsh-remote-bridge-v2.7.1.tgz
|
|
100
100
|
|
|
101
101
|
npx @deepseek-ai/dsh web
|
|
102
102
|
```
|
|
@@ -112,7 +112,7 @@ npx @deepseek-ai/dsh web
|
|
|
112
112
|
从 GitHub Release 下载离线安装包时,可直接安装 tarball:
|
|
113
113
|
|
|
114
114
|
```sh
|
|
115
|
-
dsh plugin --profile web add ./dsh-remote-bridge-v2.7.
|
|
115
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.7.1.tgz
|
|
116
116
|
```
|
|
117
117
|
|
|
118
118
|
## 第一次使用
|
|
@@ -419,6 +419,9 @@ v2.5.0 在 DSH **0.2.1-rc.2** 上完成真机验证:14/14 宿主服务全部
|
|
|
419
419
|
权限档位目录返回内核真实三档。开发基线为上游 `dsh-v0.2.1-rc.2`(commit `687f282e8625c6b8d11ff58211a90524be2e4317`),
|
|
420
420
|
兼容范围见上文 `engines.dsh`。CI 覆盖 Windows、Ubuntu 和 macOS。
|
|
421
421
|
|
|
422
|
+
**v2.7.1 的验证状态**:76 个测试文件 / **701 个用例**全绿、`typecheck`/`docs:check`/构建通过。
|
|
423
|
+
这一版只改 `src/host/dsh-0.2-bridge.ts` 的 `agentFor` 与对应回归测试,其余代码与 v2.7.0 逐字节相同。
|
|
424
|
+
|
|
422
425
|
**v2.7.0 的验证状态**:76 个测试文件 / 698 个用例全绿、8 个 Playwright 浏览器用例
|
|
423
426
|
(含 375px 窄屏契约)、`typecheck`/`docs:check`/构建通过。这一版的四项能力都在
|
|
424
427
|
DSH **0.2.1-rc.3** 隔离实例上**实跑验过**,不是静态推断:插件清单返回
|
|
@@ -430,7 +433,15 @@ DSH **0.2.1-rc.3** 隔离实例上**实跑验过**,不是静态推断:插件
|
|
|
430
433
|
|
|
431
434
|
## 项目信息
|
|
432
435
|
|
|
433
|
-
- 当前版本:`v2.
|
|
436
|
+
- 当前版本:`v2.8.0`
|
|
437
|
+
- **v2.7.1 是缺陷修复版**:内核 `sessionController.resolveAgent()` 返回的是 `{ agent }` / `{ error }`
|
|
438
|
+
**包装对象**而非 Agent 本体(`dsh-api-session-controller/lib/index.js:365-368` 的 `liveAgent()` 与
|
|
439
|
+
`:221-246` 的 `resolve()` 都是这个形状),桥此前把包装对象直接当 Agent 传给 `fileUploads.upload()`,
|
|
440
|
+
内核读 `agent.session.header.origin` 就炸成
|
|
441
|
+
`DSH_TYPEERROR: Cannot read properties of undefined (reading 'header')` —— 真机表现是
|
|
442
|
+
「发文件永远 400/500,报错信息完全指不到原因」,附件上传与 agent preset 选择两条路径都挂在这里。
|
|
443
|
+
现在识别包装形状、成功解出 `.agent`、失败原样抛 `.error`(保留内核稳定 code,「会话不存在」不再
|
|
444
|
+
被压成「会话没在跑」),同时保留裸对象兼容。新增 3 条回归测试钉住三种返回形状。
|
|
434
445
|
- **v2.7.0 是对齐 DSH 0.2.1-rc.3 的能力提升版**:四项 —— ①模型目录补上 `contextWindow` / `maxTokens`
|
|
435
446
|
(内核的 `ModelCatalogModel` 只有 id/name/description,容量得另走 `llm.resolveModelInfo`,桥按
|
|
436
447
|
`provider + model` 缓存 10 分钟、失败不缓存、内核自己给了就不覆盖);②插件清单多了右侧栏形态
|
package/docs/api/asyncapi.yaml
CHANGED
package/docs/api/openapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile API
|
|
4
|
-
version: 2.
|
|
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.
|
|
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 }
|