@imweapp/openclaw-imwe 2026.4.12-alpha.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/index.ts +16 -0
  4. package/openclaw.plugin.json +58 -0
  5. package/package.json +73 -0
  6. package/proto/PbBoxPullProto.proto +43 -0
  7. package/proto/PbChatAudioContent.proto +23 -0
  8. package/proto/PbChatDeliverMsg.proto +38 -0
  9. package/proto/PbChatFileMeta.proto +34 -0
  10. package/proto/PbChatMsg.proto +93 -0
  11. package/proto/PbChatRichMediaContent.proto +31 -0
  12. package/proto/PbChatTextContent.proto +38 -0
  13. package/proto/PbMarkdownContent.proto +18 -0
  14. package/proto/PbMsgReadStampContent.proto +11 -0
  15. package/proto/PbPacket.proto +61 -0
  16. package/proto/PbSingleChatMsg.proto +60 -0
  17. package/setup-entry.ts +17 -0
  18. package/src/accounts.ts +109 -0
  19. package/src/api-client.ts +740 -0
  20. package/src/bot-info-cache.ts +49 -0
  21. package/src/channel.runtime.ts +29 -0
  22. package/src/channel.ts +456 -0
  23. package/src/config-schema.ts +26 -0
  24. package/src/e2ee/api.ts +261 -0
  25. package/src/e2ee/canonical.ts +59 -0
  26. package/src/e2ee/errors.ts +103 -0
  27. package/src/e2ee/index.ts +8 -0
  28. package/src/e2ee/proper-lockfile.d.ts +61 -0
  29. package/src/e2ee/service.ts +1273 -0
  30. package/src/e2ee/store.ts +174 -0
  31. package/src/e2ee/types.ts +113 -0
  32. package/src/e2ee/vodozemac.ts +373 -0
  33. package/src/file-transfer/api.ts +364 -0
  34. package/src/file-transfer/concurrency.ts +77 -0
  35. package/src/file-transfer/download.ts +261 -0
  36. package/src/file-transfer/file-crypto.ts +93 -0
  37. package/src/file-transfer/index.ts +18 -0
  38. package/src/file-transfer/scheduler.ts +185 -0
  39. package/src/file-transfer/types.ts +195 -0
  40. package/src/file-transfer/upload.ts +656 -0
  41. package/src/markdown-detect.ts +119 -0
  42. package/src/media-upload.ts +338 -0
  43. package/src/media-utils.ts +110 -0
  44. package/src/monitor.ts +838 -0
  45. package/src/proto/codec.ts +54 -0
  46. package/src/proto/inbound.codec.ts +624 -0
  47. package/src/proto/proto-types.ts +291 -0
  48. package/src/proto/registry.ts +226 -0
  49. package/src/proto/send.codec.ts +535 -0
  50. package/src/recent-message-cache.ts +350 -0
  51. package/src/send.ts +792 -0
  52. package/src/setup-core.ts +62 -0
  53. package/src/types.ts +153 -0
  54. package/src/vodozemackit/index.ts +297 -0
  55. package/src/vodozemackit/pkg/vodozemackit_wasm.d.ts +138 -0
  56. package/src/vodozemackit/pkg/vodozemackit_wasm.js +24 -0
  57. package/src/vodozemackit/pkg/vodozemackit_wasm_bg.js +1172 -0
  58. package/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm +0 -0
  59. package/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm.d.ts +109 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * e2ee/api.ts — E2EE HTTP API(首期:payload 构造工厂)
3
+ *
4
+ * 对应设计:docs/07-E2EE单聊实现设计.md §2.5 / §3.4;requirements R3、R11。
5
+ *
6
+ * 本文件首期仅承载 `buildE2eeDeviceUploadPayload`,用于 `/e2eeDevice/create`
7
+ * 与 `/e2eeDevice/replenish` 请求体中的 `uploadId / md5Sum / e2eeDeviceJson`
8
+ * 三个字段构造。与 iOS `EncryptionKeysPerIMAccount.toRequestParams` 对齐:
9
+ *
10
+ * encryptionKeysPerIMAccount = {
11
+ * imAcctId,
12
+ * e2eeId,
13
+ * identityKey,
14
+ * oneTimeKeys: string[], // base64
15
+ * fallbackKeys: string[], // base64
16
+ * }
17
+ *
18
+ * md5Sum = hex(md5(sortedJson([encryptionKeysPerIMAccount])))
19
+ * e2eeDeviceJson = sortedJson([encryptionKeysPerIMAccount])
20
+ *
21
+ * 使用 `./canonical.ts` 的 `sortedJsonStringify` / `buildMd5Sum` 保证两个字段
22
+ * 基于同一份规范化字节串产出;相同输入必得相同 md5Sum / e2eeDeviceJson。
23
+ *
24
+ * HTTP 端点封装对齐 open-bot E2EE 接口:create / replenish /
25
+ * querySupplement / getPreKeyBundle。
26
+ */
27
+
28
+ import { randomUUID } from 'node:crypto';
29
+
30
+ import { postJson } from '../api-client.js';
31
+ import { buildMd5Sum, sortedJsonStringify } from './canonical.js';
32
+ import { NoPeerDeviceError } from './errors.js';
33
+ import type { E2eeDeviceStatus } from './types.js';
34
+
35
+ /** `/e2eeDevice/create` 与 `/replenish` 请求体中的上传 payload 输入 */
36
+ export interface BuildE2eeDeviceUploadPayloadParams {
37
+ imAcctId: string;
38
+ e2eeId: string;
39
+ identityKey: string;
40
+ /** base64 编码的 OTK 公钥列表 */
41
+ oneTimeKeys: string[];
42
+ /** base64 编码的 FBK 公钥列表 */
43
+ fallbackKeys: string[];
44
+ }
45
+
46
+ /** 构造出的上传三元组 */
47
+ export interface E2eeDeviceUploadPayload {
48
+ /** 32 字符 UUID hex;幂等键 */
49
+ uploadId: string;
50
+ /** 小写十六进制 MD5(对 [encryptionKeysPerIMAccount] 的 sortedJson 字节串取摘要) */
51
+ md5Sum: string;
52
+ /** 规范化 JSON 字符串;与计算 md5Sum 的字节串完全一致 */
53
+ e2eeDeviceJson: string;
54
+ }
55
+
56
+ /**
57
+ * 构造 `/e2eeDevice/create` 与 `/replenish` 的上传 payload。
58
+ *
59
+ * - `uploadId` 使用 32 字符 UUID hex;每次调用独立生成,用于服务端幂等键。
60
+ * - `md5Sum` = `buildMd5Sum([encryptionKeysPerIMAccount])`,对**数组**做 sortedJson
61
+ * 后取 MD5 小写 hex,与 iOS 保持逐字节一致。
62
+ * - `e2eeDeviceJson` = `sortedJsonStringify([encryptionKeysPerIMAccount])`,
63
+ * 复用同一规范化路径,保证服务端拿到的 JSON 字节与 md5Sum 输入严格对应。
64
+ *
65
+ * 纯函数(除 UUID 随机性外无 I/O);相同输入参数产出确定性 md5Sum / e2eeDeviceJson。
66
+ */
67
+ export function buildE2eeDeviceUploadPayload(
68
+ params: BuildE2eeDeviceUploadPayloadParams,
69
+ ): E2eeDeviceUploadPayload {
70
+ const encryptionKeysPerIMAccount = {
71
+ imAcctId: params.imAcctId,
72
+ e2eeId: params.e2eeId,
73
+ identityKey: params.identityKey,
74
+ oneTimeKeys: [...params.oneTimeKeys],
75
+ fallbackKeys: [...params.fallbackKeys],
76
+ };
77
+ const payloads = [encryptionKeysPerIMAccount];
78
+
79
+ return {
80
+ uploadId: randomUUID().replaceAll('-', ''),
81
+ md5Sum: buildMd5Sum(payloads),
82
+ e2eeDeviceJson: sortedJsonStringify(payloads),
83
+ };
84
+ }
85
+
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ // open-bot E2EE HTTP 端点封装(docs/02-OpenAPI接口设计.md §九~十二)
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+
90
+ /** `/e2eeDevice/create` 与 `/replenish` 请求体(对齐 iOS `EncryptionKeysPerIMAccount.toRequestParams`) */
91
+ export interface CreateDeviceRequest {
92
+ imAcctId: string;
93
+ e2eeId: string;
94
+ identityKey: string;
95
+ oneTimeKeys: string[];
96
+ fallbackKeys: string[];
97
+ }
98
+
99
+ /** getPreKeyBundle 返回的 PreKeyBundle(oneTimeKey 可能承载 OTK 或 fallback key) */
100
+ export interface PreKeyBundle {
101
+ acctId: string;
102
+ e2eeId: string;
103
+ identityKey: string;
104
+ signedPreKey?: string;
105
+ signedPreKeySignature?: string;
106
+ oneTimeKey: string;
107
+ }
108
+
109
+ /** `/e2eeDevice/querySupplement` 响应 */
110
+ export interface QuerySupplementResponse {
111
+ e2eeId: string;
112
+ status: E2eeDeviceStatus;
113
+ identityKey?: string;
114
+ oneTimeKeyCount?: number;
115
+ fallbackKeyCount?: number;
116
+ oneTimeKeyRequireNum?: number;
117
+ fallbackKeyRequireNum?: number;
118
+ needsSupplement?: boolean;
119
+ }
120
+
121
+ /** E2EE HTTP API 对外契约 */
122
+ export interface E2eeApi {
123
+ /** 新建 / 替换本设备。`uploadId` / `md5Sum` / `e2eeDeviceJson` 由内部 `buildE2eeDeviceUploadPayload` 生成。 */
124
+ createDevice(params: CreateDeviceRequest): Promise<void>;
125
+
126
+ /** 补充 OTK / FBK。请求体与 `createDevice` 完全同构,仅路径不同。 */
127
+ replenish(params: CreateDeviceRequest): Promise<void>;
128
+
129
+ /** 查询本机设备密钥状态。 */
130
+ querySupplement(e2eeId: string): Promise<QuerySupplementResponse>;
131
+
132
+ /** 单目标 PreKeyBundle 申请(单聊主路径)。 */
133
+ getPreKeyBundle(params: {
134
+ otherImAcctId: string;
135
+ /** 指定目标设备;为空字符串时由服务端按账号选择。 */
136
+ otherE2eeId: string;
137
+ }): Promise<PreKeyBundle>;
138
+ }
139
+
140
+ /**
141
+ * 构造 E2EE HTTP API 客户端。
142
+ *
143
+ * - 所有端点经 `postJson`(api-client.ts)发出,复用既有 HMAC-SHA256 签名链路。
144
+ * - 错误语义:`postJson` 对非 2xx 已经抛错;本层只额外处理 PreKeyBundle 字段不全。
145
+ */
146
+ export function createE2eeApi(opts: {
147
+ apiBaseUrl: string;
148
+ auth: { apiKey: string; apiSecret: string };
149
+ }): E2eeApi {
150
+ const { apiBaseUrl, auth } = opts;
151
+
152
+ // 构造 create / replenish 的请求体
153
+ const toUploadBody = (
154
+ processType: 'CREATE' | 'REPLENISH',
155
+ params: CreateDeviceRequest,
156
+ ): Record<string, unknown> => {
157
+ const payload = buildE2eeDeviceUploadPayload(params);
158
+ console.log(
159
+ 'Create JSON ',
160
+ JSON.stringify({
161
+ processType,
162
+ deviceType: 'BOT',
163
+ uploadId: payload.uploadId,
164
+ batchCount: 1,
165
+ batchNo: 1,
166
+ md5Sum: payload.md5Sum,
167
+ e2eeDeviceJson: payload.e2eeDeviceJson,
168
+ }),
169
+ );
170
+ return {
171
+ processType,
172
+ deviceType: 'BOT',
173
+ uploadId: payload.uploadId,
174
+ batchCount: 1,
175
+ batchNo: 1,
176
+ md5Sum: payload.md5Sum,
177
+ e2eeDeviceJson: payload.e2eeDeviceJson,
178
+ };
179
+ };
180
+
181
+ return {
182
+ async createDevice(params) {
183
+ await postJson<unknown>(
184
+ apiBaseUrl,
185
+ '/api/im/open/bot/e2eeDevice/create',
186
+ toUploadBody('CREATE', params),
187
+ auth,
188
+ );
189
+ },
190
+
191
+ async replenish(params) {
192
+ await postJson<unknown>(
193
+ apiBaseUrl,
194
+ '/api/im/open/bot/e2eeDevice/replenish',
195
+ toUploadBody('REPLENISH', params),
196
+ auth,
197
+ );
198
+ },
199
+
200
+ async querySupplement(e2eeId) {
201
+ return postJson<QuerySupplementResponse>(
202
+ apiBaseUrl,
203
+ '/api/im/open/bot/e2eeDevice/querySupplement',
204
+ { e2eeId },
205
+ auth,
206
+ );
207
+ },
208
+
209
+ async getPreKeyBundle(params) {
210
+ const raw = await postJson<unknown>(
211
+ apiBaseUrl,
212
+ '/api/im/open/bot/e2eeDevice/getPreKeyBundle',
213
+ { otherImAcctId: params.otherImAcctId, otherE2eeId: params.otherE2eeId },
214
+ auth,
215
+ );
216
+ return parsePreKeyBundle(raw, params.otherImAcctId, params.otherE2eeId);
217
+ },
218
+ };
219
+ }
220
+
221
+ /** 解析 getPreKeyBundle 响应,并把字段不全统一映射为 NoPeerDeviceError。 */
222
+ function parsePreKeyBundle(raw: unknown, otherImAcctId: string, otherE2eeId: string): PreKeyBundle {
223
+ if (raw === null || typeof raw !== 'object') {
224
+ throw new NoPeerDeviceError('getPreKeyBundle 响应不是对象', otherImAcctId);
225
+ }
226
+ const obj = raw as Record<string, unknown>;
227
+ const acctId = pickString(obj, ['acctId', 'imAcctId']);
228
+ const e2eeId = pickString(obj, ['e2eeId']);
229
+ const identityKey = pickString(obj, ['identityKey']);
230
+ const oneTimeKey = pickString(obj, ['oneTimeKey']);
231
+
232
+ if (!acctId || !e2eeId || !identityKey || !oneTimeKey) {
233
+ throw new NoPeerDeviceError(
234
+ `getPreKeyBundle 响应字段不全 (acctId=${!!acctId}, e2eeId=${!!e2eeId}, identityKey=${!!identityKey}, oneTimeKey=${!!oneTimeKey})`,
235
+ otherImAcctId,
236
+ );
237
+ }
238
+ if (otherE2eeId && e2eeId !== otherE2eeId) {
239
+ throw new NoPeerDeviceError('getPreKeyBundle 返回的 e2eeId 与目标设备不匹配', otherImAcctId);
240
+ }
241
+
242
+ return {
243
+ acctId,
244
+ e2eeId,
245
+ identityKey,
246
+ signedPreKey: pickString(obj, ['signedPreKey']) || undefined,
247
+ signedPreKeySignature: pickString(obj, ['signedPreKeySignature']) || undefined,
248
+ oneTimeKey,
249
+ };
250
+ }
251
+
252
+ /** 从对象里按 key 优先级取第一个非空字符串,全部缺失时返回空串。 */
253
+ function pickString(obj: Record<string, unknown>, keys: readonly string[]): string {
254
+ for (const key of keys) {
255
+ const value = obj[key];
256
+ if (typeof value === 'string' && value.length > 0) {
257
+ return value;
258
+ }
259
+ }
260
+ return '';
261
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * canonical.ts — E2EE 规范化序列化与 md5Sum
3
+ *
4
+ * 对应设计:docs/07-E2EE单聊实现设计.md §2.5;requirements R11
5
+ *
6
+ * 为对齐 iOS `JSONSerialization.data(options:.sortedKeys)` + `Data.md5()`,本模块提供:
7
+ * - sortedJsonStringify(value): 递归按 key 字典序排序对象字段,再 UTF-8 JSON 序列化;
8
+ * 对 undefined 字段按 JSON.stringify 原始行为忽略;数组元素保持原顺序(仅递归元素内部)。
9
+ * - buildMd5Sum(payloads): 对数组做 sortedJsonStringify → Node 'md5' 摘要 → 小写 hex。
10
+ *
11
+ * 设计约束:
12
+ * - 使用 MD5(**不是** SHA-256),仅用于上传完整性校验,不承担密码学安全。
13
+ * - 纯函数,无 I/O,无副作用;相同输入必得相同输出。
14
+ */
15
+
16
+ import { createHash } from 'node:crypto';
17
+
18
+ /**
19
+ * 递归规范化:对 object 字段按 key 字典序重排,对 array 保持顺序仅递归元素,
20
+ * 对 null / primitives 原样返回;undefined 字段由下游 JSON.stringify 自然忽略。
21
+ */
22
+ function canonicalize(value: unknown): unknown {
23
+ if (value === null || typeof value !== 'object') {
24
+ return value;
25
+ }
26
+ if (Array.isArray(value)) {
27
+ return value.map(canonicalize);
28
+ }
29
+ const entries = value as Record<string, unknown>;
30
+ const sortedKeys = Object.keys(entries).sort();
31
+ const result: Record<string, unknown> = {};
32
+ for (const key of sortedKeys) {
33
+ const child = entries[key];
34
+ if (child === undefined) continue; // 与 JSON.stringify 一致,忽略 undefined 字段
35
+ result[key] = canonicalize(child);
36
+ }
37
+ return result;
38
+ }
39
+
40
+ /**
41
+ * 对输入递归按 key 字典序排序后 UTF-8 JSON 序列化。
42
+ *
43
+ * - 数组保持原顺序(只对元素递归)
44
+ * - undefined 字段被忽略(与 JSON.stringify 一致)
45
+ * - 跨调用确定性:相同 value 返回相同字符串
46
+ */
47
+ export function sortedJsonStringify(value: unknown): string {
48
+ return JSON.stringify(canonicalize(value));
49
+ }
50
+
51
+ /**
52
+ * 计算 md5Sum:对 payloads 数组先 sortedJsonStringify,再取 MD5 小写 hex。
53
+ *
54
+ * 对齐 iOS `hex(md5(sortedJson([encryptionKeysPerIMAccount])))`。
55
+ */
56
+ export function buildMd5Sum(payloads: unknown[]): string {
57
+ const canonicalJson = sortedJsonStringify(payloads);
58
+ return createHash('md5').update(canonicalJson, 'utf8').digest('hex');
59
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * e2ee/errors.ts — E2EE 子系统错误类型
3
+ *
4
+ * 对应设计:design.md §3 + requirements R4 / R7 / R10。
5
+ *
6
+ * 分类原则:
7
+ * - NoPeerDeviceError:对端未启用 E2EE / apply 响应缺字段 / e2eeId 不匹配 →
8
+ * 视作 ACCOUNT_E2EE_NOT_EXISTS;上层必须"失败不降级",不得以明文发送兜底。
9
+ * - DeviceNotMatchError:服务端识别本端持有的对端设备集合与实际不一致,
10
+ * 附带 device 级(单聊)或账号级(群聊)的差异列表。
11
+ * - E2eeApiError:HTTP 4xx/5xx 的通用兜底。
12
+ * - E2eeAccountNormalizeError:normalizeFreshAccount 超过 4 轮仍未收敛。
13
+ * - SingleNoAvailableRecipientOrSession:encryptSingle peer-must-exist 校验失败。
14
+ * - E2eeEncryptFailedError:所有活跃 session encrypt 全部失败。
15
+ */
16
+
17
+ /** `ACCOUNT_E2EE_NOT_EXISTS` 语义:对端未启用 E2EE / apply 响应无效。 */
18
+ export class NoPeerDeviceError extends Error {
19
+ readonly code = 'ACCOUNT_E2EE_NOT_EXISTS';
20
+
21
+ constructor(
22
+ message: string,
23
+ public readonly peerImAcctId: string,
24
+ ) {
25
+ super(message);
26
+ this.name = 'NoPeerDeviceError';
27
+ }
28
+ }
29
+
30
+ /** DEVICE_NOT_MATCH:服务端返回的本端对端设备集合与实际不一致。 */
31
+ export class DeviceNotMatchError extends Error {
32
+ readonly code = 'E2EE_DEVICE_NOT_MATCH';
33
+
34
+ constructor(
35
+ message: string,
36
+ public readonly data: {
37
+ extraDevices?: Array<{ imAcctId: string; e2eeId: string }>;
38
+ missDevices?: Array<{ imAcctId: string; e2eeId: string }>;
39
+ extraImAcctIds?: string[];
40
+ missImAcctIds?: string[];
41
+ },
42
+ ) {
43
+ super(message);
44
+ this.name = 'DeviceNotMatchError';
45
+ }
46
+ }
47
+
48
+ /** E2EE HTTP 通用错误(4xx / 5xx / 网络异常)。 */
49
+ export class E2eeApiError extends Error {
50
+ readonly code = 'E2EE_API_ERROR';
51
+
52
+ constructor(
53
+ message: string,
54
+ public readonly statusCode?: number,
55
+ public readonly responseBody?: string,
56
+ ) {
57
+ super(message);
58
+ this.name = 'E2eeApiError';
59
+ }
60
+ }
61
+
62
+ /**
63
+ * normalizeFreshAccount 自举 pickle/unpickle >4 轮仍未收敛时抛出。
64
+ * 永远不应把该 Account 的 pickle / curve25519Key 写入 state.json。
65
+ */
66
+ export class E2eeAccountNormalizeError extends Error {
67
+ readonly code = 'E2EE_ACCOUNT_NORMALIZE_FAILED';
68
+
69
+ constructor(message = 'vodozemac Account 归一化失败:curve25519Key 连续 4 轮未收敛') {
70
+ super(message);
71
+ this.name = 'E2eeAccountNormalizeError';
72
+ }
73
+ }
74
+
75
+ /**
76
+ * encryptSingle 的 peer-must-exist 校验失败:
77
+ * 完成 getPreKeyBundle 协商后 `sessions[to]` 仍无任何活跃 session,
78
+ * 立即同步抛错,不得触发 `session.encrypt` 或额外 I/O(R6)。
79
+ */
80
+ export class SingleNoAvailableRecipientOrSession extends Error {
81
+ readonly code = 'SINGLE_NO_AVAILABLE_RECIPIENT_OR_SESSION';
82
+
83
+ constructor(
84
+ message: string,
85
+ public readonly peerImAcctId: string,
86
+ ) {
87
+ super(message);
88
+ this.name = 'SingleNoAvailableRecipientOrSession';
89
+ }
90
+ }
91
+
92
+ /** 所有活跃 session 的 encrypt 都抛错时兜底抛出。 */
93
+ export class E2eeEncryptFailedError extends Error {
94
+ readonly code = 'E2EE_ENCRYPT_FAILED';
95
+
96
+ constructor(
97
+ message: string,
98
+ public readonly peerImAcctId: string,
99
+ ) {
100
+ super(message);
101
+ this.name = 'E2eeEncryptFailedError';
102
+ }
103
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * e2ee/index.ts — E2EE 子系统公共接口聚合导出
3
+ */
4
+
5
+ export { createE2eeService } from './service.js';
6
+ export type { E2eeService, BotInfo } from './service.js';
7
+ export { E2EE_DEVICE_STATUS } from './types.js';
8
+ export type { E2eeDeviceStatus } from './types.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 最小 ambient 声明:`proper-lockfile` 不提供官方 `@types` 包,
3
+ * 这里仅补齐本项目实际使用到的 `lock` / `unlock` / `check` 三个函数签名。
4
+ *
5
+ * 参考:https://github.com/moxystudio/node-proper-lockfile#api
6
+ */
7
+
8
+ declare module 'proper-lockfile' {
9
+ /** `lockfile.lock` 的 retries 选项:数字或 `node-retry` 配置对象。 */
10
+ export interface LockRetryOptions {
11
+ retries?: number;
12
+ factor?: number;
13
+ minTimeout?: number;
14
+ maxTimeout?: number;
15
+ randomize?: boolean;
16
+ }
17
+
18
+ export interface LockOptions {
19
+ /** 重试次数,或完整的 retry 配置对象 */
20
+ retries?: number | LockRetryOptions;
21
+ /** 认为锁已陈旧(被 crash 的持有者遗留)的毫秒阈值,默认 10_000 */
22
+ stale?: number;
23
+ /** 锁心跳更新间隔(ms) */
24
+ update?: number;
25
+ /** 显式指定 lockfile 路径,默认 `${file}.lock` */
26
+ lockfilePath?: string;
27
+ /** 是否通过 `fs.realpath` 解析符号链接,默认 true */
28
+ realpath?: boolean;
29
+ /** 可选注入的 fs 实现(用于测试) */
30
+ fs?: unknown;
31
+ /** 进程退出时的兜底策略,默认 true */
32
+ onCompromised?: (err: Error) => void;
33
+ }
34
+
35
+ export interface UnlockOptions {
36
+ lockfilePath?: string;
37
+ realpath?: boolean;
38
+ fs?: unknown;
39
+ }
40
+
41
+ export interface CheckOptions extends UnlockOptions {
42
+ stale?: number;
43
+ }
44
+
45
+ /** 释放锁的 callback,多次调用仅首次生效。 */
46
+ export type ReleaseLock = () => Promise<void>;
47
+
48
+ /** 异步加锁。返回 release 函数。 */
49
+ export function lock(file: string, options?: LockOptions): Promise<ReleaseLock>;
50
+ /** 异步释放锁。 */
51
+ export function unlock(file: string, options?: UnlockOptions): Promise<void>;
52
+ /** 检查指定路径是否已被加锁。 */
53
+ export function check(file: string, options?: CheckOptions): Promise<boolean>;
54
+
55
+ const _default: typeof lock & {
56
+ lock: typeof lock;
57
+ unlock: typeof unlock;
58
+ check: typeof check;
59
+ };
60
+ export default _default;
61
+ }