@thuzjq/meteorcloud-device-sdk-node 0.5.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 +45 -0
- package/docs/NODE_INTEGRATION_GUIDE.zh-CN.md +414 -0
- package/index.d.ts +930 -0
- package/index.js +945 -0
- package/lib/artifacts.js +279 -0
- package/lib/camera.js +316 -0
- package/lib/config.js +257 -0
- package/lib/connect.js +718 -0
- package/lib/durable.js +85 -0
- package/lib/errors.js +70 -0
- package/lib/http.js +188 -0
- package/lib/jobs.js +54 -0
- package/lib/jose.js +146 -0
- package/lib/journal.js +116 -0
- package/lib/keystore.js +585 -0
- package/lib/resources.js +408 -0
- package/lib/tokens.js +311 -0
- package/lib/upload.js +188 -0
- package/package.json +44 -0
- package/tools/migrate-key-to-dpapi.js +386 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @thuzjq/meteorcloud-device-sdk-node
|
|
2
|
+
|
|
3
|
+
MeteorCloud SDK for Node.js >=20.10. Version 0.5.1.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @thuzjq/meteorcloud-device-sdk-node@0.5.1
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The SDK has no required runtime dependencies. Windows DPAPI key storage uses
|
|
12
|
+
optional peer dependency `@meteorlive/dpapi`. Install it separately when selecting
|
|
13
|
+
that storage provider. Never log or distribute private keys or access tokens.
|
|
14
|
+
|
|
15
|
+
## Integration
|
|
16
|
+
|
|
17
|
+
Read the [Node integration guide](docs/NODE_INTEGRATION_GUIDE.zh-CN.md).
|
|
18
|
+
The package includes CommonJS exports and TypeScript declarations in `index.d.ts`.
|
|
19
|
+
|
|
20
|
+
Your application authorizes an installation through `MeteorCloud.connect()` and
|
|
21
|
+
reports events through `client.uploadEvent()`. The SDK handles request signing,
|
|
22
|
+
token renewal, chunked uploads, retry handling and resumable upload journals.
|
|
23
|
+
Product client identifiers must be assigned by MeteorCloud.
|
|
24
|
+
|
|
25
|
+
## Station declarations
|
|
26
|
+
|
|
27
|
+
Supply `camera.key` and `camera.station.latitude`, `longitude`, and measured
|
|
28
|
+
`elevationM`. `timezone` is optional with a compatible server. The server resolves
|
|
29
|
+
an IANA zone and applies daylight-saving rules for the observation date.
|
|
30
|
+
Country, province, city and district are server-enriched metadata. They may
|
|
31
|
+
remain pending if the lookup service is unavailable.
|
|
32
|
+
|
|
33
|
+
A camera key represents a fixed observing position. Create and persist a new key
|
|
34
|
+
when the position changes. Existing observations retain their original identity.
|
|
35
|
+
SDK resource deletion is not supported; use the website's available controls.
|
|
36
|
+
|
|
37
|
+
## Verification
|
|
38
|
+
|
|
39
|
+
Validate authorization, upload and resume behavior with an authorized test account
|
|
40
|
+
before integrating into an unattended capture workflow. Upload completion and
|
|
41
|
+
subsequent scientific processing are separate states; see the integration guide.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
UNLICENSED. Public package availability does not grant an open-source license.
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
|
|
2
|
+
# 流星云(MeteorCloud)Node.js SDK 接入指南
|
|
3
|
+
|
|
4
|
+
> 面向:把自己的软件接入流星云的 Node 开发者
|
|
5
|
+
> 版本:`@thuzjq/meteorcloud-device-sdk-node` `0.5.1`,Node 20.10+,**零运行时依赖**
|
|
6
|
+
> `client_id` 由我们分配:`meteormasterai` / `meteorstudio` / `ufocapture-adapter`
|
|
7
|
+
|
|
8
|
+
| | |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| **管什么** | 用户怎么授权、开发者怎么调、SDK 内部怎么工作、要准备什么数据、错误怎么分类、密钥存哪 |
|
|
11
|
+
| **不管什么** | 不讲服务端实现、不讲科学流水线、不讲你自己的采集与配置模型 |
|
|
12
|
+
| **权威来源** | 代码即权威:`index.d.ts`(类型)、`index.js`、`lib/`、`test/`。文档与代码冲突时以代码为准 |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 0. 现在能做什么,不能做什么
|
|
17
|
+
|
|
18
|
+
## 0.5.1
|
|
19
|
+
|
|
20
|
+
机位声明必填 latitude、longitude、elevationM,timezone 可省略。服务端按坐标解析
|
|
21
|
+
IANA 时区并按日期处理夏令时。行政区划查询不可用时,相关字段可能待补全。
|
|
22
|
+
换位置需要新的 camera.key。SDK 不提供删除接口。
|
|
23
|
+
|
|
24
|
+
接入方应使用获授权的测试账号完成授权、上传和恢复验证。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 上报成功与科学配对独立
|
|
29
|
+
|
|
30
|
+
`await client.uploadEvent(...)` 返回 `status: 'completed'` 就是上报成功。立即保存
|
|
31
|
+
`uploadUid` / `jobUid`、显示“上报成功”、关闭上传转圈,无需等待科学配对。
|
|
32
|
+
|
|
33
|
+
科学配对是可选的后续查询。多数单站数据不一定有配对结果;`pending` 可显示
|
|
34
|
+
“暂无配对结果”,`rejected` 表示科学结果未确认,`failed` 表示后续处理异常。
|
|
35
|
+
这些状态以及查询超时均不得覆盖上传成功状态,也不得触发重新上传。
|
|
36
|
+
|
|
37
|
+
只在用户查看科学结果时按需查询 `client.jobStatus(jobUid)`。如使用轮询,应遵守
|
|
38
|
+
`nextPollAfterSeconds`,离开页面或 `resultFinal === true` 时停止;不要让上传队列
|
|
39
|
+
等待科学结果,不要把 `scientific_result_rejected` 显示成“上传失败”。
|
|
40
|
+
|
|
41
|
+
## 1. 用户视角:一次授权,之后无感
|
|
42
|
+
|
|
43
|
+
用户在你的软件里点一次"连接流星云",然后:
|
|
44
|
+
|
|
45
|
+
| 步骤 | 用户看到什么 |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| 1 | 系统默认浏览器自动打开 |
|
|
48
|
+
| 2 | 没登录就是流星云登录页,用已有账号登录(含微信);已登录直接跳过 |
|
|
49
|
+
| 3 | 一屏"是否允许〈你的软件〉访问我的流星云账户",点**允许** |
|
|
50
|
+
| 4 | 浏览器显示"绑定完成,可以关闭",你的软件那边 `connect()` 返回 |
|
|
51
|
+
|
|
52
|
+
**用户不做的事**:不用从管理网页复制任何码,不用勾选相机,不用填写任何 UID。
|
|
53
|
+
确认页上没有相机列表。
|
|
54
|
+
|
|
55
|
+
之后就没有用户的事了。相机和站点由服务端在**第一次上报时自动登记**,用户在网页上
|
|
56
|
+
能看到它们;用户可以在网页上把自动建出的临时站标为固定站,也可以吊销某个安装实例。
|
|
57
|
+
这些都是纯服务端动作,你的上报代码一个字都不用改。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. 开发者视角:最小可用代码
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @thuzjq/meteorcloud-device-sdk-node
|
|
65
|
+
# 仅 Windows、且要用不落盘的密钥档时(可选 peer,见 §7)
|
|
66
|
+
npm install @meteorlive/dpapi
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2.1 首次连接(每个安装一次)
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const { MeteorCloud } = require('@thuzjq/meteorcloud-device-sdk-node')
|
|
73
|
+
|
|
74
|
+
const { client, account } = await MeteorCloud.connect({
|
|
75
|
+
issuer: 'https://cloud.meteorlive.com/cloud', // 必须带 /cloud
|
|
76
|
+
clientId: 'meteormasterai',
|
|
77
|
+
configPath: 'C:/ProgramData/YourApp/meteorcloud.json',
|
|
78
|
+
keyPath: 'C:/ProgramData/YourApp/meteorcloud.key'
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
SDK 会自己生成本机私钥、起环回监听、打开浏览器、等回调、换码、写配置、取 Token、读账户。
|
|
83
|
+
|
|
84
|
+
`issuer` 的 `/cloud` 不是可选的:它同时决定路由、客户端断言的 `aud` 和 DPoP 的 `htu`。
|
|
85
|
+
漏掉它的表现是 404 或 `invalid_client`,不像是 URL 写错。
|
|
86
|
+
|
|
87
|
+
### 2.2 后续启动(不再走浏览器)
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const { MeteorCloudClient } = require('@thuzjq/meteorcloud-device-sdk-node')
|
|
91
|
+
|
|
92
|
+
const client = await MeteorCloudClient.fromConfig('C:/ProgramData/YourApp/meteorcloud.json')
|
|
93
|
+
if (!(await client.isBound())) {
|
|
94
|
+
// 私钥丢了或当前 Windows 用户读不出来,提示用户重新授权
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`fromConfig()` 只读配置、不碰密钥,所以务必用 `isBound()` 探一次:否则最早要等到
|
|
99
|
+
`uploadEvent()` 把几个 G 的文件哈希完才发现签不了名。
|
|
100
|
+
|
|
101
|
+
### 2.3 日常上报
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
await client.uploadEvent({
|
|
105
|
+
camera: {
|
|
106
|
+
key: channel.cloudKey, // 你为这一路生成一次并存进自己配置的稳定键
|
|
107
|
+
name: '北向广角',
|
|
108
|
+
station: {
|
|
109
|
+
latitude: 36.07, longitude: 120.38, elevationM: 45,
|
|
110
|
+
timezone: 'Asia/Shanghai', name: '青岛家里'
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
clientRequestKey: eventId, // 幂等键,你的事件 ID
|
|
114
|
+
manifestJson, // 与 manifest 文件字节完全一致的 JSON 字符串
|
|
115
|
+
artifacts: [
|
|
116
|
+
{ role: 'manifest', path: manifestPath, contentType: 'application/json' },
|
|
117
|
+
{ role: 'ecsv', path: ecsvPath, contentType: 'text/plain' },
|
|
118
|
+
{ role: 'media', path: mp4Path, contentType: 'video/mp4' }
|
|
119
|
+
],
|
|
120
|
+
journalPath: `${workDir}/${eventId}.journal.json`
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`channel.cloudKey` 用 `MeteorCloud.newCameraKey()` 生成一次,存进**你自己的**配置,之后一直用它。
|
|
125
|
+
|
|
126
|
+
就这些。绑定一次,之后每次上报带上机位声明即可。
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 3. 它是怎么工作的
|
|
131
|
+
|
|
132
|
+
### 3.1 三层身份
|
|
133
|
+
|
|
134
|
+
| 层 | 载体 | 寿命 | 能干什么 |
|
|
135
|
+
| --- | --- | --- | --- |
|
|
136
|
+
| 用户 | 浏览器登录态 | 会话级 | 登录、同意或拒绝 |
|
|
137
|
+
| 安装实例 | 本机 P-256 私钥 + `installation_uid` | 长期,可吊销 | 代表该账户换取短期 Token |
|
|
138
|
+
| 操作 Token | 不透明 Token,DPoP 绑定 | 10 分钟 | 查询账户资源、上报、查任务 |
|
|
139
|
+
|
|
140
|
+
**没有 Refresh Token,也没有任何长期 Bearer 凭据落盘。** 续期靠本机私钥现签一份
|
|
141
|
+
RFC 7523 断言换新 Token,SDK 在过期前 60 秒自动做,并发调用共享同一次续期。
|
|
142
|
+
Token 只在内存里;配置文件、journal、日志都写不进去(写入前有拦截)。
|
|
143
|
+
|
|
144
|
+
一年没开机的客户端和一分钟前刚跑过的,行为完全一样。
|
|
145
|
+
|
|
146
|
+
### 3.2 机位声明:客户端不维护任何清单
|
|
147
|
+
|
|
148
|
+
三份数据分属三方,不要混:
|
|
149
|
+
|
|
150
|
+
| 数据 | 谁持有 | SDK 会读吗 |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| 你软件本地的站点、通道、摄像头配置 | 你的软件 | **不会**。SDK 不扫描、不接收、不同步 |
|
|
153
|
+
| 流星云账户下已登记的站点、相机 | 服务端 | 只有你显式调查询方法时 |
|
|
154
|
+
| 本次上报的机位声明 | 你在业务时刻决定 | 作为单次参数接收,用完即忘 |
|
|
155
|
+
|
|
156
|
+
服务端的识别规则(你零参与):
|
|
157
|
+
|
|
158
|
+
- **相机身份** = `(租户, 账户, 产品 client_id, camera.key)`。见过即是,没见过就建。
|
|
159
|
+
- **站点身份 = 位置**。同账户下坐标按列精度取整后相同即同一站;多路填同样坐标自动归到一站;
|
|
160
|
+
没见过的坐标就新建一个**临时站**。
|
|
161
|
+
- **站点不可改,相机可迁移**。已知相机换了坐标就挂到另一个站,旧站保留。
|
|
162
|
+
- **名字**首次建立时采用;之后改名只改相机名,不动站点。
|
|
163
|
+
|
|
164
|
+
`authorize()` 的响应会回 `stationUid` / `cameraUid` / `createdStation` / `createdCamera`,
|
|
165
|
+
**仅作告知**,你不必保存。下次上报凭同一个 `camera` 块就能定位到同一台相机。
|
|
166
|
+
(注意:`uploadEvent()` 返回的是 finalize 响应,不带这四个字段。)
|
|
167
|
+
|
|
168
|
+
已经手里有 `cameraUid` 的,可以用 `cameraUid` 代替 `camera` 块,这是正常用法而非兼容层。
|
|
169
|
+
两者都不给会在**读文件之前**本地报错,不会白哈希一遍几个 G 的视频。
|
|
170
|
+
|
|
171
|
+
### 3.3 一次上报的三段
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
authorize → 分块 PUT → finalize
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
1. **authorize**:本地先校验机位声明,再哈希全部文件,把尺寸和 SHA-256 报给服务端换一个会话。
|
|
178
|
+
2. **分块 PUT**:每块默认 32 MiB,带 `Content-Range` 和该块的 SHA-256,逐块顺序直传到我们自己的服务器。
|
|
179
|
+
3. **finalize**:服务端重新校验全文摘要,落库,返回 `jobUid`。
|
|
180
|
+
|
|
181
|
+
### 3.4 断点续传与幂等
|
|
182
|
+
|
|
183
|
+
- **服务端的 `receivedBytes` 是唯一权威**。任何失败后客户端都会重读它再续传,
|
|
184
|
+
所以"块传到了但 200 丢了"会被识别成进度,而不是重传成冲突。
|
|
185
|
+
- `journalPath` 是本地进度提示,**一个事件一份**,成功前不要删。它只是加速,
|
|
186
|
+
丢了也不影响正确性;磁盘满了写不进去也不会中断上传。
|
|
187
|
+
- `clientRequestKey` 是幂等键。同一个键重报同样内容会拿回同一个会话;
|
|
188
|
+
内容不同则报冲突。**换事件要换键。**
|
|
189
|
+
- 收到 429 时 SDK 会遵守 `Retry-After`。服务端对上传控制面按 IP 限每分钟 20 次,
|
|
190
|
+
一次事件至少占 2 次。
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 4. API 速查
|
|
195
|
+
|
|
196
|
+
必学的只有四个,其余按需。
|
|
197
|
+
|
|
198
|
+
| 方法 | 作用 |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `MeteorCloud.connect(options)` | 首次授权,返回 `{ client, token, account, installation, config, configPath }` |
|
|
201
|
+
| `MeteorCloud.beginConnect(options)` | 两阶段版:先拿 `authorizationUrl` 自己去开(Electron / 内嵌 WebView),再 `await pending.wait()`;`pending.cancel()` 取消 |
|
|
202
|
+
| `MeteorCloud.newCameraKey()` | 生成 32 位十六进制稳定键。纯本地,无网络 |
|
|
203
|
+
| `MeteorCloudClient.fromConfig(path)` | 从配置恢复客户端 |
|
|
204
|
+
| `client.isBound()` | 私钥是否真的可用(会试签一次,不只是看文件在不在) |
|
|
205
|
+
| `client.uploadEvent(request)` | 上报一个事件:authorize → 分块 → finalize |
|
|
206
|
+
| `client.jobStatus(jobUid)` | 查科学任务状态 |
|
|
207
|
+
| `client.getAccount()` | 当前账户摘要 `{ accountId, nickname, tenantId }` |
|
|
208
|
+
| `client.listStations(query?)` / `listCameras(query?)` / `getCamera(uid)` | 读回账户已登记的资源。普通查询,**不缓存**,不要在启动或上传时调 |
|
|
209
|
+
| `client.ensureCamera(block)` | 可选预建。参数是 `camera` 块**本身**(不是 `{camera:{...}}`),行为与上报时的登记完全一致 |
|
|
210
|
+
| `client.authorize(request)` / `uploadAuthorized(...)` / `finalize(uid)` / `abort(uid)` | 拆开用的低层接口 |
|
|
211
|
+
| `client.getAccessToken()` | 取当前 Token(互操作用,不是让你自己管续期) |
|
|
212
|
+
|
|
213
|
+
`ConnectOptions` 常用项:`issuer`、`clientId`、`configPath`、`keyPath`、`keyStoreScheme`
|
|
214
|
+
(`file` / `dpapi` / `cng`)、`browserMode`(`system` 默认 / `manual`)、`onAuthorizeUrl`、
|
|
215
|
+
`signal`(`AbortSignal`)、`timeoutMs`(默认 5 分钟)。
|
|
216
|
+
|
|
217
|
+
进度回调 `onProgress(progress)` 的 `stage` 依次是
|
|
218
|
+
`hashing` → `authorizing` → `uploading`(带 `role` / `transferredBytes` / `totalBytes`)
|
|
219
|
+
→ `finalizing` → `completed`。**回调返回 `false` 即取消上传。**
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 5. 你要准备的数据
|
|
224
|
+
|
|
225
|
+
### 5.1 机位声明字段
|
|
226
|
+
|
|
227
|
+
| 字段 | 必填 | 规则 |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| `camera.key` | 是 | 1–64 字符,`A-Za-z0-9._:-`,首字符为字母或数字。**不含个人信息**。改键 = 换相机 |
|
|
230
|
+
| `camera.name` | 否 | ≤128 字符,不含控制字符。缺省用 key |
|
|
231
|
+
| `station.latitude` | 是 | WGS84 度,−90 ~ 90。必须是数字,字符串会被拒 |
|
|
232
|
+
| `station.longitude` | 是 | WGS84 度,−180 ~ 180 |
|
|
233
|
+
| `station.elevationM` | 是 | 整数米,−500 ~ 10000 |
|
|
234
|
+
| `station.timezone` | 是 | IANA 时区 id 如 `Asia/Shanghai`。**`+08:00` 这种偏移会被拒**,因为它没有夏令时规则 |
|
|
235
|
+
| `station.name` | 否 | ≤128 字符。仅首次建站时采用 |
|
|
236
|
+
| `cameraUid` | 与 `camera` 二选一 | `cam_` + 32 位小写十六进制 |
|
|
237
|
+
|
|
238
|
+
校验在**读任何文件之前**完成,错误消息只说字段名和规则,**从不回显坐标值**(那是别人家的位置)。
|
|
239
|
+
|
|
240
|
+
### 5.2 文件与 manifest
|
|
241
|
+
|
|
242
|
+
必须三个,`preview` 可选作第四个:
|
|
243
|
+
|
|
244
|
+
| 角色 | 内容 | 上限 |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `manifest` | 事件元数据 JSON | 1 MiB |
|
|
247
|
+
| `ecsv` | 测光/位置序列 | 2 GiB |
|
|
248
|
+
| `media` | 视频或图像 | 100 MiB |
|
|
249
|
+
| `preview` | 预览图(可选) | 20 MiB |
|
|
250
|
+
|
|
251
|
+
manifest 的硬性要求:
|
|
252
|
+
|
|
253
|
+
- `schema` 必须是 `"mlc.manifest/2"`;
|
|
254
|
+
- `local_event_id` 必须**逐字等于** `clientRequestKey`;
|
|
255
|
+
- `files.<role>.sha256` 必须与你实际提交的文件摘要一致;没提交的角色要缺省或写 `"-"`;
|
|
256
|
+
- `manifestJson` 字符串与 manifest 文件的字节必须完全一致。
|
|
257
|
+
|
|
258
|
+
这几条 SDK 都在本地先查一遍。不查的话,服务端的摘要不符会以 **401** 返回,
|
|
259
|
+
读起来像"你的凭据错了",而实际是 manifest 内容不对。
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 6. 错误处理
|
|
264
|
+
|
|
265
|
+
SDK 只抛一种错误 `MeteorCloudError`。先看 `kind`:
|
|
266
|
+
|
|
267
|
+
| `kind` | 含义 | 该怎么做 |
|
|
268
|
+
| --- | --- | --- |
|
|
269
|
+
| `validation` | 你的参数不对 | **别重试**,修代码 |
|
|
270
|
+
| `io` | 本地文件读写问题 | 查路径与磁盘 |
|
|
271
|
+
| `transport` | 网络层失败 | 可重试,SDK 内部已退避过 |
|
|
272
|
+
| `device_api` | 服务端拒绝了请求 | 看 `httpStatus` 与 `apiCode` |
|
|
273
|
+
| `auth` | 授权或 Token 问题 | 看 `oauthError` |
|
|
274
|
+
| `keystore` | 密钥不可用 | 多半要重新授权 |
|
|
275
|
+
| `cancelled` | 超时或被取消 | 看 `action` 区分 |
|
|
276
|
+
|
|
277
|
+
配套字段:`httpStatus`、`apiCode`(服务端稳定业务码)、`retryable`、`retryAfterSeconds`、
|
|
278
|
+
`action`。`error.toSafeObject()` 返回可以直接写日志的版本。
|
|
279
|
+
|
|
280
|
+
几个必须认识的 `action`:
|
|
281
|
+
|
|
282
|
+
| `action` | 含义 |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `authorize_timeout` | 用户没在超时内完成授权,可以再来一次 |
|
|
285
|
+
| `authorize_cancelled` / `authorize_aborted` | 你自己取消的 |
|
|
286
|
+
| `account_api_unsupported` | 这台服务端还没上账户级 API(见 §0),**升级服务端,不要重新绑定** |
|
|
287
|
+
| `bind_registered_load_config` | **绑定其实成功了**,只是随后取 Token 或读账户失败。**千万不要再调 `connect()`**,那会生成第二把密钥、注册第二个安装实例,把第一个孤立掉。改用 `fromConfig()` 读错误上的 `configPath` |
|
|
288
|
+
|
|
289
|
+
常见业务码:`1030003035` 没声明机位、`1030003030` 分块顺序错、`1030003026` 幂等键撞了不同内容、
|
|
290
|
+
`1030002005` 摘要不符。
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 7. 密钥存储分档
|
|
295
|
+
|
|
296
|
+
| 档 | `key_reference` | 平台 | 说明 |
|
|
297
|
+
| --- | --- | --- | --- |
|
|
298
|
+
| `cng` | `cng://meteorlive/<名>` | Windows | 最强:私钥不可导出、不落盘。**`@meteorlive/cng` 尚未发布**,Node 侧暂不可用 |
|
|
299
|
+
| `dpapi` | `dpapi://<路径>` | Windows | 默认档。DPAPI 按当前用户加密后落盘。需可选 peer `@meteorlive/dpapi` |
|
|
300
|
+
| `file` | `file://<路径>` | 全平台 | 明文 PKCS#8,权限 0600。**仅开发和受控环境** |
|
|
301
|
+
|
|
302
|
+
不传 `keyStoreScheme` 时:Windows 用 `dpapi`,其他平台用 `file`。
|
|
303
|
+
|
|
304
|
+
三条纪律:SDK 只暴露 `sign()`,永远不提供导出私钥;新建密钥不会覆盖已有密钥;
|
|
305
|
+
绑定失败且服务端尚未登记时,本次新建的密钥会被清理,而**服务端一旦登记就绝不销毁密钥**
|
|
306
|
+
(否则会留下一个永远无法认证、只能人工吊销的安装实例)。
|
|
307
|
+
|
|
308
|
+
配置文件只有五个字段,且**不含任何秘密**:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"schema": "mlc.installation/1",
|
|
313
|
+
"issuer": "https://cloud.meteorlive.com/cloud",
|
|
314
|
+
"client_id": "meteormasterai",
|
|
315
|
+
"installation_uid": "ins_<32位十六进制>",
|
|
316
|
+
"key_reference": "dpapi://C:/ProgramData/YourApp/meteorcloud.key"
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 8. 日志纪律
|
|
323
|
+
|
|
324
|
+
**永远不要记录**:完整 Token、`Authorization` 头、客户端断言、DPoP proof、私钥、
|
|
325
|
+
授权码、PKCE verifier、**完整的授权 URL**。
|
|
326
|
+
|
|
327
|
+
**可以记录**:错误类别、HTTP 状态、业务码、OAuth `error`、Token 指纹
|
|
328
|
+
(`token.fingerprint`,SHA-256 前 16 位)、`installationUid` 脱敏尾段。
|
|
329
|
+
|
|
330
|
+
`error.toSafeObject()` 就是按这条规则设计的,直接用它。
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 9. 已知缺口
|
|
335
|
+
|
|
336
|
+
| 缺口 | 影响 |
|
|
337
|
+
| --- | --- |
|
|
338
|
+
| 未在 Windows 真机跑过 0.5.0 | 上一份真机收据属于 0.4.x |
|
|
339
|
+
| `@meteorlive/cng` 未发布、`@meteorlive/dpapi` 未发公开 npm | 最强密钥档 Node 侧不可用;装不上 dpapi 的机器只能退到明文档 |
|
|
340
|
+
| 失败后调 `abort()` 会烧掉幂等键 | 失败后暂时不要 `abort`,直接重试同一个键 |
|
|
341
|
+
| 没有"列出我上传过什么"的接口 | 事件 ID ↔ `jobUid` 的账本要你自己维护 |
|
|
342
|
+
| 站点/相机没有更新和删除 API | 客户端只能被动登记或预建,其余管理动作在网页上做 |
|
|
343
|
+
| 坐标填错会留下一个空临时站 | 无上报的临时站会自动归档,不影响已受理事件。⚠ 服务端尚未实现该归档任务(`archived` 只有常量定义、没有写入方),当前 `archivedAt` 恒不下发,填错的临时站会一直留着 |
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 10. 完整示例
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
'use strict'
|
|
351
|
+
const { MeteorCloud, MeteorCloudClient, MeteorCloudError } = require('@thuzjq/meteorcloud-device-sdk-node')
|
|
352
|
+
|
|
353
|
+
const CONFIG = 'C:/ProgramData/YourApp/meteorcloud.json'
|
|
354
|
+
const KEY = 'C:/ProgramData/YourApp/meteorcloud.key'
|
|
355
|
+
const ISSUER = 'https://cloud.meteorlive.com/cloud'
|
|
356
|
+
|
|
357
|
+
async function getClient() {
|
|
358
|
+
try {
|
|
359
|
+
const client = await MeteorCloudClient.fromConfig(CONFIG)
|
|
360
|
+
if (await client.isBound()) return client
|
|
361
|
+
} catch (_) { /* 还没绑定过,往下走 */ }
|
|
362
|
+
|
|
363
|
+
const { client, account } = await MeteorCloud.connect({
|
|
364
|
+
issuer: ISSUER, clientId: 'meteormasterai', configPath: CONFIG, keyPath: KEY
|
|
365
|
+
})
|
|
366
|
+
console.log('已连接账户:', account.nickname)
|
|
367
|
+
return client
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function report(client, event, channel) {
|
|
371
|
+
const result = await client.uploadEvent({
|
|
372
|
+
camera: {
|
|
373
|
+
key: channel.cloudKey, // MeteorCloud.newCameraKey() 生成一次并存好
|
|
374
|
+
name: channel.displayName,
|
|
375
|
+
station: {
|
|
376
|
+
latitude: channel.lat, longitude: channel.lon,
|
|
377
|
+
elevationM: channel.elevationM, timezone: channel.timezone
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
clientRequestKey: event.id,
|
|
381
|
+
manifestJson: event.manifestJson,
|
|
382
|
+
artifacts: [
|
|
383
|
+
{ role: 'manifest', path: event.manifestPath, contentType: 'application/json' },
|
|
384
|
+
{ role: 'ecsv', path: event.ecsvPath, contentType: 'text/plain' },
|
|
385
|
+
{ role: 'media', path: event.mediaPath, contentType: 'video/mp4' }
|
|
386
|
+
],
|
|
387
|
+
journalPath: `${event.workDir}/${event.id}.journal.json`,
|
|
388
|
+
onProgress: (p) => { if (p.stage === 'uploading') ui.setProgress(p.transferredBytes / p.totalBytes) }
|
|
389
|
+
})
|
|
390
|
+
return result.jobUid // 存好它,之后用 jobStatus() 查科学结果
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function main(event, channel) {
|
|
394
|
+
const client = await getClient()
|
|
395
|
+
try {
|
|
396
|
+
const jobUid = await report(client, event, channel)
|
|
397
|
+
console.log('已受理:', jobUid)
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (!(error instanceof MeteorCloudError)) throw error
|
|
400
|
+
console.error('上报失败', error.toSafeObject())
|
|
401
|
+
if (error.action === 'bind_registered_load_config') {
|
|
402
|
+
console.error('绑定是成功的,不要重新授权;配置在', error.configPath)
|
|
403
|
+
}
|
|
404
|
+
if (error.kind === 'validation') return // 参数错,重试没意义
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 11. 相关文档
|
|
412
|
+
|
|
413
|
+
- [`../README.md`](../README.md) — 英文速查与 API 索引
|
|
414
|
+
- [`../index.d.ts`](../index.d.ts) — 类型定义,附带每个字段的理由,**最权威**
|