@uzqw/shared 0.1.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/dist/index.d.ts +708 -0
- package/dist/index.js +493 -0
- package/package.json +25 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
interface RecolxConfig {
|
|
4
|
+
/** OAuth client id(developer 客户端注册名) */
|
|
5
|
+
clientId?: string;
|
|
6
|
+
/** OAuth 回调地址(本地 stdio 自收) */
|
|
7
|
+
redirectUri?: string;
|
|
8
|
+
/** token 落盘文件名(~/.recolx/ 内;默认 tokens-mcp.json 与 CLI 分离) */
|
|
9
|
+
tokenFile?: string;
|
|
10
|
+
/** token 落盘目录(测试注入用),默认 ~/.recolx */
|
|
11
|
+
tokenDir?: string;
|
|
12
|
+
/** API base,默认 http://127.0.0.1:8005(本地 dev) */
|
|
13
|
+
apiBase?: string;
|
|
14
|
+
/** 授权页地址;缺省 = `${apiBase}/open/oauth/authorize` */
|
|
15
|
+
authorizationUrl?: string;
|
|
16
|
+
/** token/refresh 端点;缺省 = `${apiBase}/open/oauth/token`(grant_type 区分) */
|
|
17
|
+
tokenUrl?: string;
|
|
18
|
+
/** 附加请求头 */
|
|
19
|
+
extraHeaders?: Record<string, string>;
|
|
20
|
+
/** fetch 实现(测试注入用),默认全局 fetch */
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
declare const DEFAULT_API_BASE: string;
|
|
24
|
+
declare const DEFAULT_CLIENT_ID: string;
|
|
25
|
+
declare const DEFAULT_REDIRECT_URI = "http://localhost:8199/auth/callback";
|
|
26
|
+
declare function resolveConfig(config: RecolxConfig): Required<Pick<RecolxConfig, "clientId" | "redirectUri" | "tokenFile" | "apiBase" | "authorizationUrl" | "tokenUrl">> & Pick<RecolxConfig, "extraHeaders" | "fetch" | "tokenDir">;
|
|
27
|
+
|
|
28
|
+
interface TokenSet {
|
|
29
|
+
access_token: string;
|
|
30
|
+
refresh_token?: string;
|
|
31
|
+
token_type?: string;
|
|
32
|
+
/** 过期时间(epoch ms),由 expires_in 换算 */
|
|
33
|
+
expires_at?: number;
|
|
34
|
+
scope?: string;
|
|
35
|
+
}
|
|
36
|
+
declare class TokenStore {
|
|
37
|
+
readonly dir: string;
|
|
38
|
+
readonly path: string;
|
|
39
|
+
constructor(filename?: string, dir?: string);
|
|
40
|
+
save(tokenSet: TokenSet): Promise<void>;
|
|
41
|
+
load(): Promise<TokenSet | null>;
|
|
42
|
+
clear(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/** 默认 MCP token 落盘绝对路径(与 issue #5 的 ~/.recolx/tokens-mcp.json 一致) */
|
|
45
|
+
declare const TOKENS_MCP_PATH: string;
|
|
46
|
+
|
|
47
|
+
declare function generateCodeVerifier(): string;
|
|
48
|
+
declare function generateCodeChallenge(verifier: string): string;
|
|
49
|
+
declare function generateState(): string;
|
|
50
|
+
interface AuthorizationRequest {
|
|
51
|
+
url: string;
|
|
52
|
+
codeVerifier: string;
|
|
53
|
+
state: string;
|
|
54
|
+
}
|
|
55
|
+
declare class OAuth {
|
|
56
|
+
readonly config: ReturnType<typeof resolveConfig>;
|
|
57
|
+
readonly tokenStore: TokenStore;
|
|
58
|
+
readonly authorizationUrl: string;
|
|
59
|
+
readonly tokenUrl: string;
|
|
60
|
+
private readonly fetchImpl;
|
|
61
|
+
constructor(config: RecolxConfig);
|
|
62
|
+
/** 构造授权请求(PKCE S256)。extraParams 用于本地联调注入 user_id(env=local 才接受)。 */
|
|
63
|
+
createAuthorizationRequest(extraParams?: Record<string, string>): AuthorizationRequest;
|
|
64
|
+
/** 授权码换 token 并落盘;失败抛 Error(含状态码与响应体)。 */
|
|
65
|
+
exchangeCode(code: string, codeVerifier: string): Promise<TokenSet>;
|
|
66
|
+
/** 用 refresh_token 换新 token(后端滚动轮换),失败返回 null。 */
|
|
67
|
+
refresh(refreshToken: string): Promise<TokenSet | null>;
|
|
68
|
+
/** 取可用 access token:未过期直接用;过期则滚动刷新;失败返回 null。 */
|
|
69
|
+
getAccessToken(): Promise<string | null>;
|
|
70
|
+
/** 服务端撤销指定 token(RFC 7009:未知 token 也返回成功)。 */
|
|
71
|
+
revoke(token: string, tokenTypeHint?: string): Promise<void>;
|
|
72
|
+
/** 清空本地 token(logout 本地侧)。 */
|
|
73
|
+
logout(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare class RecolxClient {
|
|
77
|
+
readonly oauth: OAuth;
|
|
78
|
+
readonly apiBase: string;
|
|
79
|
+
readonly extraHeaders: Record<string, string>;
|
|
80
|
+
/** 测试/联调用:设置后不再走 OAuth token 获取 */
|
|
81
|
+
staticToken?: string;
|
|
82
|
+
private readonly fetchImpl;
|
|
83
|
+
constructor(config: RecolxConfig);
|
|
84
|
+
get auth(): OAuth;
|
|
85
|
+
request(path: string, init?: RequestInit & {
|
|
86
|
+
token?: string;
|
|
87
|
+
}): Promise<unknown>;
|
|
88
|
+
/** GET /open/third-party/users/current */
|
|
89
|
+
getCurrentUser(): Promise<unknown>;
|
|
90
|
+
/** POST /open/third-party/users/current/revoke(撤销当前用户全部 MCP token) */
|
|
91
|
+
revokeCurrentUser(): Promise<unknown>;
|
|
92
|
+
/** GET /open/third-party/files?page=&page_size= */
|
|
93
|
+
listFiles(page?: number, pageSize?: number): Promise<unknown>;
|
|
94
|
+
/** GET /open/third-party/files/{id}[?include=transcript] */
|
|
95
|
+
getFile(fileId: string, opts?: {
|
|
96
|
+
includeTranscript?: boolean;
|
|
97
|
+
}): Promise<unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type RecolxErrorType = "auth" | "permission" | "not_found" | "timeout" | "network" | "server_error" | "client_error" | "unknown";
|
|
101
|
+
declare function errorTypeFromStatus(status: number): RecolxErrorType;
|
|
102
|
+
declare function classifyError(err: unknown): RecolxErrorType;
|
|
103
|
+
|
|
104
|
+
declare const SubscriptionSchema: z.ZodObject<{
|
|
105
|
+
tier: z.ZodEnum<["free", "trial", "vip"]>;
|
|
106
|
+
is_trial: z.ZodBoolean;
|
|
107
|
+
subscribed: z.ZodBoolean;
|
|
108
|
+
vip_seconds_left: z.ZodNumber;
|
|
109
|
+
vip_seconds_total: z.ZodNumber;
|
|
110
|
+
pack_seconds_left: z.ZodNumber;
|
|
111
|
+
pack_seconds_total: z.ZodNumber;
|
|
112
|
+
device_seconds_left: z.ZodNumber;
|
|
113
|
+
device_seconds_total: z.ZodNumber;
|
|
114
|
+
space_total: z.ZodNumber;
|
|
115
|
+
}, "strip", z.ZodTypeAny, {
|
|
116
|
+
tier: "free" | "trial" | "vip";
|
|
117
|
+
is_trial: boolean;
|
|
118
|
+
subscribed: boolean;
|
|
119
|
+
vip_seconds_left: number;
|
|
120
|
+
vip_seconds_total: number;
|
|
121
|
+
pack_seconds_left: number;
|
|
122
|
+
pack_seconds_total: number;
|
|
123
|
+
device_seconds_left: number;
|
|
124
|
+
device_seconds_total: number;
|
|
125
|
+
space_total: number;
|
|
126
|
+
}, {
|
|
127
|
+
tier: "free" | "trial" | "vip";
|
|
128
|
+
is_trial: boolean;
|
|
129
|
+
subscribed: boolean;
|
|
130
|
+
vip_seconds_left: number;
|
|
131
|
+
vip_seconds_total: number;
|
|
132
|
+
pack_seconds_left: number;
|
|
133
|
+
pack_seconds_total: number;
|
|
134
|
+
device_seconds_left: number;
|
|
135
|
+
device_seconds_total: number;
|
|
136
|
+
space_total: number;
|
|
137
|
+
}>;
|
|
138
|
+
type RecolxSubscription = z.infer<typeof SubscriptionSchema>;
|
|
139
|
+
declare const CurrentUserSchema: z.ZodObject<{
|
|
140
|
+
user_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
|
|
141
|
+
nickname: z.ZodString;
|
|
142
|
+
subscription: z.ZodObject<{
|
|
143
|
+
tier: z.ZodEnum<["free", "trial", "vip"]>;
|
|
144
|
+
is_trial: z.ZodBoolean;
|
|
145
|
+
subscribed: z.ZodBoolean;
|
|
146
|
+
vip_seconds_left: z.ZodNumber;
|
|
147
|
+
vip_seconds_total: z.ZodNumber;
|
|
148
|
+
pack_seconds_left: z.ZodNumber;
|
|
149
|
+
pack_seconds_total: z.ZodNumber;
|
|
150
|
+
device_seconds_left: z.ZodNumber;
|
|
151
|
+
device_seconds_total: z.ZodNumber;
|
|
152
|
+
space_total: z.ZodNumber;
|
|
153
|
+
}, "strip", z.ZodTypeAny, {
|
|
154
|
+
tier: "free" | "trial" | "vip";
|
|
155
|
+
is_trial: boolean;
|
|
156
|
+
subscribed: boolean;
|
|
157
|
+
vip_seconds_left: number;
|
|
158
|
+
vip_seconds_total: number;
|
|
159
|
+
pack_seconds_left: number;
|
|
160
|
+
pack_seconds_total: number;
|
|
161
|
+
device_seconds_left: number;
|
|
162
|
+
device_seconds_total: number;
|
|
163
|
+
space_total: number;
|
|
164
|
+
}, {
|
|
165
|
+
tier: "free" | "trial" | "vip";
|
|
166
|
+
is_trial: boolean;
|
|
167
|
+
subscribed: boolean;
|
|
168
|
+
vip_seconds_left: number;
|
|
169
|
+
vip_seconds_total: number;
|
|
170
|
+
pack_seconds_left: number;
|
|
171
|
+
pack_seconds_total: number;
|
|
172
|
+
device_seconds_left: number;
|
|
173
|
+
device_seconds_total: number;
|
|
174
|
+
space_total: number;
|
|
175
|
+
}>;
|
|
176
|
+
}, "strip", z.ZodTypeAny, {
|
|
177
|
+
user_id: string | number;
|
|
178
|
+
nickname: string;
|
|
179
|
+
subscription: {
|
|
180
|
+
tier: "free" | "trial" | "vip";
|
|
181
|
+
is_trial: boolean;
|
|
182
|
+
subscribed: boolean;
|
|
183
|
+
vip_seconds_left: number;
|
|
184
|
+
vip_seconds_total: number;
|
|
185
|
+
pack_seconds_left: number;
|
|
186
|
+
pack_seconds_total: number;
|
|
187
|
+
device_seconds_left: number;
|
|
188
|
+
device_seconds_total: number;
|
|
189
|
+
space_total: number;
|
|
190
|
+
};
|
|
191
|
+
}, {
|
|
192
|
+
user_id: string | number;
|
|
193
|
+
nickname: string;
|
|
194
|
+
subscription: {
|
|
195
|
+
tier: "free" | "trial" | "vip";
|
|
196
|
+
is_trial: boolean;
|
|
197
|
+
subscribed: boolean;
|
|
198
|
+
vip_seconds_left: number;
|
|
199
|
+
vip_seconds_total: number;
|
|
200
|
+
pack_seconds_left: number;
|
|
201
|
+
pack_seconds_total: number;
|
|
202
|
+
device_seconds_left: number;
|
|
203
|
+
device_seconds_total: number;
|
|
204
|
+
space_total: number;
|
|
205
|
+
};
|
|
206
|
+
}>;
|
|
207
|
+
type RecolxCurrentUser = z.infer<typeof CurrentUserSchema>;
|
|
208
|
+
declare const DeviceSchema: z.ZodObject<{
|
|
209
|
+
name: z.ZodString;
|
|
210
|
+
serial: z.ZodString;
|
|
211
|
+
/** 型号:earphone/recorder/recolx_fit/card/unknown(后端按 SN 白名单解析) */
|
|
212
|
+
model: z.ZodString;
|
|
213
|
+
}, "strip", z.ZodTypeAny, {
|
|
214
|
+
name: string;
|
|
215
|
+
serial: string;
|
|
216
|
+
model: string;
|
|
217
|
+
}, {
|
|
218
|
+
name: string;
|
|
219
|
+
serial: string;
|
|
220
|
+
model: string;
|
|
221
|
+
}>;
|
|
222
|
+
type RecolxDevice = z.infer<typeof DeviceSchema>;
|
|
223
|
+
declare const FileSchema: z.ZodObject<{
|
|
224
|
+
file_id: z.ZodString;
|
|
225
|
+
title: z.ZodString;
|
|
226
|
+
/** Unix 秒 */
|
|
227
|
+
recorded_at: z.ZodNumber;
|
|
228
|
+
/** 秒 */
|
|
229
|
+
duration: z.ZodNumber;
|
|
230
|
+
/** 字节 */
|
|
231
|
+
file_size: z.ZodNumber;
|
|
232
|
+
device: z.ZodObject<{
|
|
233
|
+
name: z.ZodString;
|
|
234
|
+
serial: z.ZodString;
|
|
235
|
+
/** 型号:earphone/recorder/recolx_fit/card/unknown(后端按 SN 白名单解析) */
|
|
236
|
+
model: z.ZodString;
|
|
237
|
+
}, "strip", z.ZodTypeAny, {
|
|
238
|
+
name: string;
|
|
239
|
+
serial: string;
|
|
240
|
+
model: string;
|
|
241
|
+
}, {
|
|
242
|
+
name: string;
|
|
243
|
+
serial: string;
|
|
244
|
+
model: string;
|
|
245
|
+
}>;
|
|
246
|
+
}, "strip", z.ZodTypeAny, {
|
|
247
|
+
file_id: string;
|
|
248
|
+
title: string;
|
|
249
|
+
recorded_at: number;
|
|
250
|
+
duration: number;
|
|
251
|
+
file_size: number;
|
|
252
|
+
device: {
|
|
253
|
+
name: string;
|
|
254
|
+
serial: string;
|
|
255
|
+
model: string;
|
|
256
|
+
};
|
|
257
|
+
}, {
|
|
258
|
+
file_id: string;
|
|
259
|
+
title: string;
|
|
260
|
+
recorded_at: number;
|
|
261
|
+
duration: number;
|
|
262
|
+
file_size: number;
|
|
263
|
+
device: {
|
|
264
|
+
name: string;
|
|
265
|
+
serial: string;
|
|
266
|
+
model: string;
|
|
267
|
+
};
|
|
268
|
+
}>;
|
|
269
|
+
type RecolxFile = z.infer<typeof FileSchema>;
|
|
270
|
+
declare const FileListSchema: z.ZodObject<{
|
|
271
|
+
files: z.ZodArray<z.ZodObject<{
|
|
272
|
+
file_id: z.ZodString;
|
|
273
|
+
title: z.ZodString;
|
|
274
|
+
/** Unix 秒 */
|
|
275
|
+
recorded_at: z.ZodNumber;
|
|
276
|
+
/** 秒 */
|
|
277
|
+
duration: z.ZodNumber;
|
|
278
|
+
/** 字节 */
|
|
279
|
+
file_size: z.ZodNumber;
|
|
280
|
+
device: z.ZodObject<{
|
|
281
|
+
name: z.ZodString;
|
|
282
|
+
serial: z.ZodString;
|
|
283
|
+
/** 型号:earphone/recorder/recolx_fit/card/unknown(后端按 SN 白名单解析) */
|
|
284
|
+
model: z.ZodString;
|
|
285
|
+
}, "strip", z.ZodTypeAny, {
|
|
286
|
+
name: string;
|
|
287
|
+
serial: string;
|
|
288
|
+
model: string;
|
|
289
|
+
}, {
|
|
290
|
+
name: string;
|
|
291
|
+
serial: string;
|
|
292
|
+
model: string;
|
|
293
|
+
}>;
|
|
294
|
+
}, "strip", z.ZodTypeAny, {
|
|
295
|
+
file_id: string;
|
|
296
|
+
title: string;
|
|
297
|
+
recorded_at: number;
|
|
298
|
+
duration: number;
|
|
299
|
+
file_size: number;
|
|
300
|
+
device: {
|
|
301
|
+
name: string;
|
|
302
|
+
serial: string;
|
|
303
|
+
model: string;
|
|
304
|
+
};
|
|
305
|
+
}, {
|
|
306
|
+
file_id: string;
|
|
307
|
+
title: string;
|
|
308
|
+
recorded_at: number;
|
|
309
|
+
duration: number;
|
|
310
|
+
file_size: number;
|
|
311
|
+
device: {
|
|
312
|
+
name: string;
|
|
313
|
+
serial: string;
|
|
314
|
+
model: string;
|
|
315
|
+
};
|
|
316
|
+
}>, "many">;
|
|
317
|
+
total: z.ZodNumber;
|
|
318
|
+
page: z.ZodNumber;
|
|
319
|
+
page_size: z.ZodNumber;
|
|
320
|
+
}, "strip", z.ZodTypeAny, {
|
|
321
|
+
files: {
|
|
322
|
+
file_id: string;
|
|
323
|
+
title: string;
|
|
324
|
+
recorded_at: number;
|
|
325
|
+
duration: number;
|
|
326
|
+
file_size: number;
|
|
327
|
+
device: {
|
|
328
|
+
name: string;
|
|
329
|
+
serial: string;
|
|
330
|
+
model: string;
|
|
331
|
+
};
|
|
332
|
+
}[];
|
|
333
|
+
total: number;
|
|
334
|
+
page: number;
|
|
335
|
+
page_size: number;
|
|
336
|
+
}, {
|
|
337
|
+
files: {
|
|
338
|
+
file_id: string;
|
|
339
|
+
title: string;
|
|
340
|
+
recorded_at: number;
|
|
341
|
+
duration: number;
|
|
342
|
+
file_size: number;
|
|
343
|
+
device: {
|
|
344
|
+
name: string;
|
|
345
|
+
serial: string;
|
|
346
|
+
model: string;
|
|
347
|
+
};
|
|
348
|
+
}[];
|
|
349
|
+
total: number;
|
|
350
|
+
page: number;
|
|
351
|
+
page_size: number;
|
|
352
|
+
}>;
|
|
353
|
+
type RecolxFileList = z.infer<typeof FileListSchema>;
|
|
354
|
+
declare const SegmentSchema: z.ZodObject<{
|
|
355
|
+
start: z.ZodNumber;
|
|
356
|
+
end: z.ZodNumber;
|
|
357
|
+
text: z.ZodString;
|
|
358
|
+
speaker: z.ZodString;
|
|
359
|
+
language: z.ZodString;
|
|
360
|
+
}, "strip", z.ZodTypeAny, {
|
|
361
|
+
start: number;
|
|
362
|
+
end: number;
|
|
363
|
+
text: string;
|
|
364
|
+
speaker: string;
|
|
365
|
+
language: string;
|
|
366
|
+
}, {
|
|
367
|
+
start: number;
|
|
368
|
+
end: number;
|
|
369
|
+
text: string;
|
|
370
|
+
speaker: string;
|
|
371
|
+
language: string;
|
|
372
|
+
}>;
|
|
373
|
+
type RecolxSegment = z.infer<typeof SegmentSchema>;
|
|
374
|
+
declare const SummarySchema: z.ZodObject<{
|
|
375
|
+
task_id: z.ZodString;
|
|
376
|
+
text: z.ZodString;
|
|
377
|
+
prompt_id: z.ZodString;
|
|
378
|
+
prompt_name: z.ZodString;
|
|
379
|
+
}, "strip", z.ZodTypeAny, {
|
|
380
|
+
text: string;
|
|
381
|
+
task_id: string;
|
|
382
|
+
prompt_id: string;
|
|
383
|
+
prompt_name: string;
|
|
384
|
+
}, {
|
|
385
|
+
text: string;
|
|
386
|
+
task_id: string;
|
|
387
|
+
prompt_id: string;
|
|
388
|
+
prompt_name: string;
|
|
389
|
+
}>;
|
|
390
|
+
type RecolxSummary = z.infer<typeof SummarySchema>;
|
|
391
|
+
/** 转写/总结(?include=transcript 时附加;空数据后端返回 null,按 null 与 [] 双兼容处理) */
|
|
392
|
+
declare const TranscriptSchema: z.ZodObject<{
|
|
393
|
+
language: z.ZodString;
|
|
394
|
+
segments: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
395
|
+
start: z.ZodNumber;
|
|
396
|
+
end: z.ZodNumber;
|
|
397
|
+
text: z.ZodString;
|
|
398
|
+
speaker: z.ZodString;
|
|
399
|
+
language: z.ZodString;
|
|
400
|
+
}, "strip", z.ZodTypeAny, {
|
|
401
|
+
start: number;
|
|
402
|
+
end: number;
|
|
403
|
+
text: string;
|
|
404
|
+
speaker: string;
|
|
405
|
+
language: string;
|
|
406
|
+
}, {
|
|
407
|
+
start: number;
|
|
408
|
+
end: number;
|
|
409
|
+
text: string;
|
|
410
|
+
speaker: string;
|
|
411
|
+
language: string;
|
|
412
|
+
}>, "many">>;
|
|
413
|
+
summary: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
414
|
+
task_id: z.ZodString;
|
|
415
|
+
text: z.ZodString;
|
|
416
|
+
prompt_id: z.ZodString;
|
|
417
|
+
prompt_name: z.ZodString;
|
|
418
|
+
}, "strip", z.ZodTypeAny, {
|
|
419
|
+
text: string;
|
|
420
|
+
task_id: string;
|
|
421
|
+
prompt_id: string;
|
|
422
|
+
prompt_name: string;
|
|
423
|
+
}, {
|
|
424
|
+
text: string;
|
|
425
|
+
task_id: string;
|
|
426
|
+
prompt_id: string;
|
|
427
|
+
prompt_name: string;
|
|
428
|
+
}>, "many">>;
|
|
429
|
+
}, "strip", z.ZodTypeAny, {
|
|
430
|
+
language: string;
|
|
431
|
+
segments: {
|
|
432
|
+
start: number;
|
|
433
|
+
end: number;
|
|
434
|
+
text: string;
|
|
435
|
+
speaker: string;
|
|
436
|
+
language: string;
|
|
437
|
+
}[] | null;
|
|
438
|
+
summary: {
|
|
439
|
+
text: string;
|
|
440
|
+
task_id: string;
|
|
441
|
+
prompt_id: string;
|
|
442
|
+
prompt_name: string;
|
|
443
|
+
}[] | null;
|
|
444
|
+
}, {
|
|
445
|
+
language: string;
|
|
446
|
+
segments: {
|
|
447
|
+
start: number;
|
|
448
|
+
end: number;
|
|
449
|
+
text: string;
|
|
450
|
+
speaker: string;
|
|
451
|
+
language: string;
|
|
452
|
+
}[] | null;
|
|
453
|
+
summary: {
|
|
454
|
+
text: string;
|
|
455
|
+
task_id: string;
|
|
456
|
+
prompt_id: string;
|
|
457
|
+
prompt_name: string;
|
|
458
|
+
}[] | null;
|
|
459
|
+
}>;
|
|
460
|
+
type RecolxTranscript = z.infer<typeof TranscriptSchema>;
|
|
461
|
+
declare const FileDetailSchema: z.ZodObject<{
|
|
462
|
+
file_id: z.ZodString;
|
|
463
|
+
title: z.ZodString;
|
|
464
|
+
/** Unix 秒 */
|
|
465
|
+
recorded_at: z.ZodNumber;
|
|
466
|
+
/** 秒 */
|
|
467
|
+
duration: z.ZodNumber;
|
|
468
|
+
/** 字节 */
|
|
469
|
+
file_size: z.ZodNumber;
|
|
470
|
+
device: z.ZodObject<{
|
|
471
|
+
name: z.ZodString;
|
|
472
|
+
serial: z.ZodString;
|
|
473
|
+
/** 型号:earphone/recorder/recolx_fit/card/unknown(后端按 SN 白名单解析) */
|
|
474
|
+
model: z.ZodString;
|
|
475
|
+
}, "strip", z.ZodTypeAny, {
|
|
476
|
+
name: string;
|
|
477
|
+
serial: string;
|
|
478
|
+
model: string;
|
|
479
|
+
}, {
|
|
480
|
+
name: string;
|
|
481
|
+
serial: string;
|
|
482
|
+
model: string;
|
|
483
|
+
}>;
|
|
484
|
+
} & {
|
|
485
|
+
/** 24h 音频下载地址(presign JWT 在 query) */
|
|
486
|
+
presigned_url: z.ZodString;
|
|
487
|
+
/** Unix 秒 */
|
|
488
|
+
presigned_url_expires_at: z.ZodNumber;
|
|
489
|
+
transcript: z.ZodOptional<z.ZodObject<{
|
|
490
|
+
language: z.ZodString;
|
|
491
|
+
segments: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
492
|
+
start: z.ZodNumber;
|
|
493
|
+
end: z.ZodNumber;
|
|
494
|
+
text: z.ZodString;
|
|
495
|
+
speaker: z.ZodString;
|
|
496
|
+
language: z.ZodString;
|
|
497
|
+
}, "strip", z.ZodTypeAny, {
|
|
498
|
+
start: number;
|
|
499
|
+
end: number;
|
|
500
|
+
text: string;
|
|
501
|
+
speaker: string;
|
|
502
|
+
language: string;
|
|
503
|
+
}, {
|
|
504
|
+
start: number;
|
|
505
|
+
end: number;
|
|
506
|
+
text: string;
|
|
507
|
+
speaker: string;
|
|
508
|
+
language: string;
|
|
509
|
+
}>, "many">>;
|
|
510
|
+
summary: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
511
|
+
task_id: z.ZodString;
|
|
512
|
+
text: z.ZodString;
|
|
513
|
+
prompt_id: z.ZodString;
|
|
514
|
+
prompt_name: z.ZodString;
|
|
515
|
+
}, "strip", z.ZodTypeAny, {
|
|
516
|
+
text: string;
|
|
517
|
+
task_id: string;
|
|
518
|
+
prompt_id: string;
|
|
519
|
+
prompt_name: string;
|
|
520
|
+
}, {
|
|
521
|
+
text: string;
|
|
522
|
+
task_id: string;
|
|
523
|
+
prompt_id: string;
|
|
524
|
+
prompt_name: string;
|
|
525
|
+
}>, "many">>;
|
|
526
|
+
}, "strip", z.ZodTypeAny, {
|
|
527
|
+
language: string;
|
|
528
|
+
segments: {
|
|
529
|
+
start: number;
|
|
530
|
+
end: number;
|
|
531
|
+
text: string;
|
|
532
|
+
speaker: string;
|
|
533
|
+
language: string;
|
|
534
|
+
}[] | null;
|
|
535
|
+
summary: {
|
|
536
|
+
text: string;
|
|
537
|
+
task_id: string;
|
|
538
|
+
prompt_id: string;
|
|
539
|
+
prompt_name: string;
|
|
540
|
+
}[] | null;
|
|
541
|
+
}, {
|
|
542
|
+
language: string;
|
|
543
|
+
segments: {
|
|
544
|
+
start: number;
|
|
545
|
+
end: number;
|
|
546
|
+
text: string;
|
|
547
|
+
speaker: string;
|
|
548
|
+
language: string;
|
|
549
|
+
}[] | null;
|
|
550
|
+
summary: {
|
|
551
|
+
text: string;
|
|
552
|
+
task_id: string;
|
|
553
|
+
prompt_id: string;
|
|
554
|
+
prompt_name: string;
|
|
555
|
+
}[] | null;
|
|
556
|
+
}>>;
|
|
557
|
+
}, "strip", z.ZodTypeAny, {
|
|
558
|
+
file_id: string;
|
|
559
|
+
title: string;
|
|
560
|
+
recorded_at: number;
|
|
561
|
+
duration: number;
|
|
562
|
+
file_size: number;
|
|
563
|
+
device: {
|
|
564
|
+
name: string;
|
|
565
|
+
serial: string;
|
|
566
|
+
model: string;
|
|
567
|
+
};
|
|
568
|
+
presigned_url: string;
|
|
569
|
+
presigned_url_expires_at: number;
|
|
570
|
+
transcript?: {
|
|
571
|
+
language: string;
|
|
572
|
+
segments: {
|
|
573
|
+
start: number;
|
|
574
|
+
end: number;
|
|
575
|
+
text: string;
|
|
576
|
+
speaker: string;
|
|
577
|
+
language: string;
|
|
578
|
+
}[] | null;
|
|
579
|
+
summary: {
|
|
580
|
+
text: string;
|
|
581
|
+
task_id: string;
|
|
582
|
+
prompt_id: string;
|
|
583
|
+
prompt_name: string;
|
|
584
|
+
}[] | null;
|
|
585
|
+
} | undefined;
|
|
586
|
+
}, {
|
|
587
|
+
file_id: string;
|
|
588
|
+
title: string;
|
|
589
|
+
recorded_at: number;
|
|
590
|
+
duration: number;
|
|
591
|
+
file_size: number;
|
|
592
|
+
device: {
|
|
593
|
+
name: string;
|
|
594
|
+
serial: string;
|
|
595
|
+
model: string;
|
|
596
|
+
};
|
|
597
|
+
presigned_url: string;
|
|
598
|
+
presigned_url_expires_at: number;
|
|
599
|
+
transcript?: {
|
|
600
|
+
language: string;
|
|
601
|
+
segments: {
|
|
602
|
+
start: number;
|
|
603
|
+
end: number;
|
|
604
|
+
text: string;
|
|
605
|
+
speaker: string;
|
|
606
|
+
language: string;
|
|
607
|
+
}[] | null;
|
|
608
|
+
summary: {
|
|
609
|
+
text: string;
|
|
610
|
+
task_id: string;
|
|
611
|
+
prompt_id: string;
|
|
612
|
+
prompt_name: string;
|
|
613
|
+
}[] | null;
|
|
614
|
+
} | undefined;
|
|
615
|
+
}>;
|
|
616
|
+
type RecolxFileDetail = z.infer<typeof FileDetailSchema>;
|
|
617
|
+
declare const LoginInput: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>;
|
|
618
|
+
declare const LogoutInput: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>;
|
|
619
|
+
declare const GetCurrentUserInput: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>;
|
|
620
|
+
declare const ListFilesInput: z.ZodObject<{
|
|
621
|
+
page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
622
|
+
page_size: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
623
|
+
query: z.ZodOptional<z.ZodString>;
|
|
624
|
+
date_from: z.ZodOptional<z.ZodString>;
|
|
625
|
+
date_to: z.ZodOptional<z.ZodString>;
|
|
626
|
+
}, "strip", z.ZodTypeAny, {
|
|
627
|
+
page: number;
|
|
628
|
+
page_size: number;
|
|
629
|
+
query?: string | undefined;
|
|
630
|
+
date_from?: string | undefined;
|
|
631
|
+
date_to?: string | undefined;
|
|
632
|
+
}, {
|
|
633
|
+
page?: number | undefined;
|
|
634
|
+
page_size?: number | undefined;
|
|
635
|
+
query?: string | undefined;
|
|
636
|
+
date_from?: string | undefined;
|
|
637
|
+
date_to?: string | undefined;
|
|
638
|
+
}>;
|
|
639
|
+
declare const GetFileInput: z.ZodObject<{
|
|
640
|
+
file_id: z.ZodString;
|
|
641
|
+
}, "strip", z.ZodTypeAny, {
|
|
642
|
+
file_id: string;
|
|
643
|
+
}, {
|
|
644
|
+
file_id: string;
|
|
645
|
+
}>;
|
|
646
|
+
declare const GetNoteInput: z.ZodObject<{
|
|
647
|
+
file_id: z.ZodString;
|
|
648
|
+
template: z.ZodOptional<z.ZodString>;
|
|
649
|
+
}, "strip", z.ZodTypeAny, {
|
|
650
|
+
file_id: string;
|
|
651
|
+
template?: string | undefined;
|
|
652
|
+
}, {
|
|
653
|
+
file_id: string;
|
|
654
|
+
template?: string | undefined;
|
|
655
|
+
}>;
|
|
656
|
+
declare const GetTranscriptInput: z.ZodObject<{
|
|
657
|
+
file_id: z.ZodString;
|
|
658
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
659
|
+
language: z.ZodOptional<z.ZodString>;
|
|
660
|
+
speaker: z.ZodOptional<z.ZodString>;
|
|
661
|
+
mode: z.ZodOptional<z.ZodEnum<["raw", "polish"]>>;
|
|
662
|
+
}, "strip", z.ZodTypeAny, {
|
|
663
|
+
file_id: string;
|
|
664
|
+
speaker?: string | undefined;
|
|
665
|
+
language?: string | undefined;
|
|
666
|
+
cursor?: string | undefined;
|
|
667
|
+
mode?: "raw" | "polish" | undefined;
|
|
668
|
+
}, {
|
|
669
|
+
file_id: string;
|
|
670
|
+
speaker?: string | undefined;
|
|
671
|
+
language?: string | undefined;
|
|
672
|
+
cursor?: string | undefined;
|
|
673
|
+
mode?: "raw" | "polish" | undefined;
|
|
674
|
+
}>;
|
|
675
|
+
|
|
676
|
+
/** 解析 API 时间戳(秒/ISO)→ epoch ms;无效返回 null。 */
|
|
677
|
+
declare function parseApiTimestamp(value: string | undefined | null): number | null;
|
|
678
|
+
/** 某日 00:00:00(本地时区)→ epoch ms;无效日期返回 null。 */
|
|
679
|
+
declare function localDayStart(value: string | undefined | null): number | null;
|
|
680
|
+
/** 某日 23:59:59.999 → epoch ms;无效日期返回 null。 */
|
|
681
|
+
declare function localDayEnd(value: string | undefined | null): number | null;
|
|
682
|
+
|
|
683
|
+
type OAuthCallbackResult = {
|
|
684
|
+
status: "success";
|
|
685
|
+
} | {
|
|
686
|
+
status: "denied";
|
|
687
|
+
error: Error;
|
|
688
|
+
} | {
|
|
689
|
+
status: "exchange-failed";
|
|
690
|
+
error: Error;
|
|
691
|
+
} | {
|
|
692
|
+
status: "timeout";
|
|
693
|
+
} | {
|
|
694
|
+
status: "listen-failed";
|
|
695
|
+
error: Error;
|
|
696
|
+
};
|
|
697
|
+
interface RunOAuthCallbackOpts {
|
|
698
|
+
port: number;
|
|
699
|
+
expectedState: string;
|
|
700
|
+
exchangeCode: (code: string) => Promise<unknown>;
|
|
701
|
+
timeoutMs?: number;
|
|
702
|
+
onListening?: () => void;
|
|
703
|
+
/** 成功后延时关服,保证页面渲染完(默认 1.5s) */
|
|
704
|
+
postSuccessDelayMs?: number;
|
|
705
|
+
}
|
|
706
|
+
declare function runOAuthCallback(opts: RunOAuthCallbackOpts): Promise<OAuthCallbackResult>;
|
|
707
|
+
|
|
708
|
+
export { type AuthorizationRequest, CurrentUserSchema, DEFAULT_API_BASE, DEFAULT_CLIENT_ID, DEFAULT_REDIRECT_URI, DeviceSchema, FileDetailSchema, FileListSchema, FileSchema, GetCurrentUserInput, GetFileInput, GetNoteInput, GetTranscriptInput, ListFilesInput, LoginInput, LogoutInput, OAuth, type OAuthCallbackResult, RecolxClient, type RecolxConfig, type RecolxCurrentUser, type RecolxDevice, type RecolxErrorType, type RecolxFile, type RecolxFileDetail, type RecolxFileList, type RecolxSegment, type RecolxSubscription, type RecolxSummary, type RecolxTranscript, type RunOAuthCallbackOpts, SegmentSchema, SubscriptionSchema, SummarySchema, TOKENS_MCP_PATH, type TokenSet, TokenStore, TranscriptSchema, classifyError, errorTypeFromStatus, generateCodeChallenge, generateCodeVerifier, generateState, localDayEnd, localDayStart, parseApiTimestamp, resolveConfig, runOAuthCallback };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var DEFAULT_API_BASE = process.env.RECOLX_API_BASE ?? "http://127.0.0.1:8005";
|
|
3
|
+
var DEFAULT_CLIENT_ID = process.env.RECOLX_MCP_CLIENT_ID ?? "recolx-mcp";
|
|
4
|
+
var DEFAULT_REDIRECT_URI = "http://localhost:8199/auth/callback";
|
|
5
|
+
function resolveConfig(config) {
|
|
6
|
+
const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
7
|
+
return {
|
|
8
|
+
clientId: config.clientId ?? DEFAULT_CLIENT_ID,
|
|
9
|
+
redirectUri: config.redirectUri ?? DEFAULT_REDIRECT_URI,
|
|
10
|
+
tokenFile: config.tokenFile ?? "tokens-mcp.json",
|
|
11
|
+
apiBase,
|
|
12
|
+
authorizationUrl: config.authorizationUrl ?? `${apiBase}/open/oauth/authorize`,
|
|
13
|
+
tokenUrl: config.tokenUrl ?? `${apiBase}/open/oauth/token`,
|
|
14
|
+
extraHeaders: config.extraHeaders ?? {},
|
|
15
|
+
fetch: config.fetch,
|
|
16
|
+
tokenDir: config.tokenDir
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/token-store.ts
|
|
21
|
+
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
var TokenStore = class {
|
|
25
|
+
dir;
|
|
26
|
+
path;
|
|
27
|
+
constructor(filename = "tokens-mcp.json", dir = join(homedir(), ".recolx")) {
|
|
28
|
+
this.dir = dir;
|
|
29
|
+
this.path = join(dir, filename);
|
|
30
|
+
}
|
|
31
|
+
async save(tokenSet) {
|
|
32
|
+
await mkdir(this.dir, { recursive: true });
|
|
33
|
+
await writeFile(this.path, JSON.stringify(tokenSet, null, 2) + "\n", "utf-8");
|
|
34
|
+
}
|
|
35
|
+
async load() {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(await readFile(this.path, "utf-8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async clear() {
|
|
43
|
+
try {
|
|
44
|
+
await rm(this.path);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var TOKENS_MCP_PATH = join(homedir(), ".recolx", "tokens-mcp.json");
|
|
50
|
+
|
|
51
|
+
// src/oauth.ts
|
|
52
|
+
import { createHash, randomBytes } from "crypto";
|
|
53
|
+
function generateCodeVerifier() {
|
|
54
|
+
return randomBytes(32).toString("base64url");
|
|
55
|
+
}
|
|
56
|
+
function generateCodeChallenge(verifier) {
|
|
57
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
58
|
+
}
|
|
59
|
+
function generateState() {
|
|
60
|
+
return randomBytes(16).toString("base64url");
|
|
61
|
+
}
|
|
62
|
+
function toTokenSet(data) {
|
|
63
|
+
return {
|
|
64
|
+
access_token: String(data.access_token ?? ""),
|
|
65
|
+
refresh_token: data.refresh_token ? String(data.refresh_token) : void 0,
|
|
66
|
+
token_type: data.token_type ? String(data.token_type) : "Bearer",
|
|
67
|
+
expires_at: typeof data.expires_in === "number" ? Date.now() + data.expires_in * 1e3 : void 0,
|
|
68
|
+
scope: data.scope ? String(data.scope) : void 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var OAuth = class {
|
|
72
|
+
config;
|
|
73
|
+
tokenStore;
|
|
74
|
+
authorizationUrl;
|
|
75
|
+
tokenUrl;
|
|
76
|
+
fetchImpl;
|
|
77
|
+
constructor(config) {
|
|
78
|
+
this.config = resolveConfig(config);
|
|
79
|
+
this.tokenStore = new TokenStore(this.config.tokenFile, this.config.tokenDir);
|
|
80
|
+
this.authorizationUrl = this.config.authorizationUrl;
|
|
81
|
+
this.tokenUrl = this.config.tokenUrl;
|
|
82
|
+
this.fetchImpl = this.config.fetch ?? fetch;
|
|
83
|
+
}
|
|
84
|
+
/** 构造授权请求(PKCE S256)。extraParams 用于本地联调注入 user_id(env=local 才接受)。 */
|
|
85
|
+
createAuthorizationRequest(extraParams = {}) {
|
|
86
|
+
const codeVerifier = generateCodeVerifier();
|
|
87
|
+
const params = new URLSearchParams({
|
|
88
|
+
client_id: this.config.clientId,
|
|
89
|
+
redirect_uri: this.config.redirectUri,
|
|
90
|
+
response_type: "code",
|
|
91
|
+
scope: "read",
|
|
92
|
+
code_challenge: generateCodeChallenge(codeVerifier),
|
|
93
|
+
code_challenge_method: "S256",
|
|
94
|
+
state: generateState(),
|
|
95
|
+
...extraParams
|
|
96
|
+
});
|
|
97
|
+
return { url: `${this.authorizationUrl}?${params.toString()}`, codeVerifier, state: params.get("state") };
|
|
98
|
+
}
|
|
99
|
+
/** 授权码换 token 并落盘;失败抛 Error(含状态码与响应体)。 */
|
|
100
|
+
async exchangeCode(code, codeVerifier) {
|
|
101
|
+
const res = await this.fetchImpl(this.tokenUrl, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
104
|
+
body: new URLSearchParams({ grant_type: "authorization_code", code, code_verifier: codeVerifier })
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(`Token exchange failed: ${res.status} ${await res.text()}`);
|
|
108
|
+
}
|
|
109
|
+
const tokenSet = toTokenSet(await res.json());
|
|
110
|
+
await this.tokenStore.save(tokenSet);
|
|
111
|
+
return tokenSet;
|
|
112
|
+
}
|
|
113
|
+
/** 用 refresh_token 换新 token(后端滚动轮换),失败返回 null。 */
|
|
114
|
+
async refresh(refreshToken) {
|
|
115
|
+
const res = await this.fetchImpl(this.tokenUrl, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
118
|
+
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken })
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new Error(`Token refresh failed: ${res.status} ${await res.text()}`);
|
|
122
|
+
}
|
|
123
|
+
const tokenSet = toTokenSet(await res.json());
|
|
124
|
+
await this.tokenStore.save(tokenSet);
|
|
125
|
+
return tokenSet;
|
|
126
|
+
}
|
|
127
|
+
/** 取可用 access token:未过期直接用;过期则滚动刷新;失败返回 null。 */
|
|
128
|
+
async getAccessToken() {
|
|
129
|
+
const tokenSet = await this.tokenStore.load();
|
|
130
|
+
if (!tokenSet?.access_token) return null;
|
|
131
|
+
const expired = tokenSet.expires_at !== void 0 && Date.now() > tokenSet.expires_at - 6e4;
|
|
132
|
+
if (expired) {
|
|
133
|
+
if (!tokenSet.refresh_token) return null;
|
|
134
|
+
try {
|
|
135
|
+
return (await this.refresh(tokenSet.refresh_token))?.access_token ?? null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return tokenSet.access_token;
|
|
141
|
+
}
|
|
142
|
+
/** 服务端撤销指定 token(RFC 7009:未知 token 也返回成功)。 */
|
|
143
|
+
async revoke(token, tokenTypeHint) {
|
|
144
|
+
const body = { token };
|
|
145
|
+
if (tokenTypeHint) body.token_type_hint = tokenTypeHint;
|
|
146
|
+
await this.fetchImpl(`${this.config.apiBase}/open/oauth/revoke`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
149
|
+
body: new URLSearchParams(body)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** 清空本地 token(logout 本地侧)。 */
|
|
153
|
+
async logout() {
|
|
154
|
+
await this.tokenStore.clear();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// src/client.ts
|
|
159
|
+
var RecolxClient = class {
|
|
160
|
+
oauth;
|
|
161
|
+
apiBase;
|
|
162
|
+
extraHeaders;
|
|
163
|
+
/** 测试/联调用:设置后不再走 OAuth token 获取 */
|
|
164
|
+
staticToken;
|
|
165
|
+
fetchImpl;
|
|
166
|
+
constructor(config) {
|
|
167
|
+
const resolved = resolveConfig(config);
|
|
168
|
+
this.oauth = new OAuth(resolved);
|
|
169
|
+
this.apiBase = resolved.apiBase;
|
|
170
|
+
this.extraHeaders = resolved.extraHeaders ?? {};
|
|
171
|
+
this.fetchImpl = resolved.fetch ?? fetch;
|
|
172
|
+
}
|
|
173
|
+
get auth() {
|
|
174
|
+
return this.oauth;
|
|
175
|
+
}
|
|
176
|
+
async request(path, init) {
|
|
177
|
+
const token = init?.token ?? this.staticToken ?? await this.oauth.getAccessToken();
|
|
178
|
+
if (!token) {
|
|
179
|
+
throw new Error("\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u8FD0\u884C recolx-mcp login");
|
|
180
|
+
}
|
|
181
|
+
const url = `${this.apiBase}${path}`;
|
|
182
|
+
const res = await this.fetchImpl(url, {
|
|
183
|
+
...init,
|
|
184
|
+
headers: {
|
|
185
|
+
Authorization: `Bearer ${token}`,
|
|
186
|
+
Accept: "application/json",
|
|
187
|
+
...this.extraHeaders,
|
|
188
|
+
...init?.headers ?? {}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
const body = await res.text();
|
|
193
|
+
const detail = body.length > 0 ? `: ${body.slice(0, 300)}` : "";
|
|
194
|
+
throw new Error(`API error: ${res.status} ${res.statusText}${detail}`);
|
|
195
|
+
}
|
|
196
|
+
return res.json();
|
|
197
|
+
}
|
|
198
|
+
/** GET /open/third-party/users/current */
|
|
199
|
+
getCurrentUser() {
|
|
200
|
+
return this.request("/open/third-party/users/current");
|
|
201
|
+
}
|
|
202
|
+
/** POST /open/third-party/users/current/revoke(撤销当前用户全部 MCP token) */
|
|
203
|
+
revokeCurrentUser() {
|
|
204
|
+
return this.request("/open/third-party/users/current/revoke", { method: "POST" });
|
|
205
|
+
}
|
|
206
|
+
/** GET /open/third-party/files?page=&page_size= */
|
|
207
|
+
listFiles(page = 1, pageSize = 20) {
|
|
208
|
+
return this.request(`/open/third-party/files?page=${page}&page_size=${pageSize}`);
|
|
209
|
+
}
|
|
210
|
+
/** GET /open/third-party/files/{id}[?include=transcript] */
|
|
211
|
+
getFile(fileId, opts = {}) {
|
|
212
|
+
const qs = opts.includeTranscript ? "?include=transcript" : "";
|
|
213
|
+
return this.request(`/open/third-party/files/${encodeURIComponent(fileId)}${qs}`);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// src/errors.ts
|
|
218
|
+
function errorTypeFromStatus(status) {
|
|
219
|
+
if (status === 401) return "auth";
|
|
220
|
+
if (status === 403) return "permission";
|
|
221
|
+
if (status === 404) return "not_found";
|
|
222
|
+
if (status >= 500) return "server_error";
|
|
223
|
+
if (status >= 400) return "client_error";
|
|
224
|
+
return "unknown";
|
|
225
|
+
}
|
|
226
|
+
function classifyError(err) {
|
|
227
|
+
const e = err instanceof Error ? err : void 0;
|
|
228
|
+
const msg = e ? e.message : String(err ?? "");
|
|
229
|
+
const name = e ? e.name : "";
|
|
230
|
+
if (name === "AbortError" || /\btimeout\b|ETIMEDOUT/i.test(msg)) return "timeout";
|
|
231
|
+
if (err instanceof TypeError || /fetch failed|ECONNREFUSED|ENOTFOUND|ECONNRESET|EAI_AGAIN|network/i.test(msg)) return "network";
|
|
232
|
+
if (/\b401\b|not authenticated|unauthorized|未登录/i.test(msg)) return "auth";
|
|
233
|
+
if (/\b403\b|forbidden/i.test(msg)) return "permission";
|
|
234
|
+
if (/\b404\b|not found/i.test(msg)) return "not_found";
|
|
235
|
+
if (/\b5\d\d\b|internal server error|bad gateway|service unavailable|gateway timeout/i.test(msg)) return "server_error";
|
|
236
|
+
if (/\b4\d\d\b/.test(msg)) return "client_error";
|
|
237
|
+
return "unknown";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/schemas.ts
|
|
241
|
+
import { z } from "zod";
|
|
242
|
+
var SubscriptionSchema = z.object({
|
|
243
|
+
tier: z.enum(["free", "trial", "vip"]),
|
|
244
|
+
is_trial: z.boolean(),
|
|
245
|
+
subscribed: z.boolean(),
|
|
246
|
+
vip_seconds_left: z.number(),
|
|
247
|
+
vip_seconds_total: z.number(),
|
|
248
|
+
pack_seconds_left: z.number(),
|
|
249
|
+
pack_seconds_total: z.number(),
|
|
250
|
+
device_seconds_left: z.number(),
|
|
251
|
+
device_seconds_total: z.number(),
|
|
252
|
+
space_total: z.number()
|
|
253
|
+
});
|
|
254
|
+
var CurrentUserSchema = z.object({
|
|
255
|
+
// 后端输出 string(雪花 ID 防 JS 精度丢失);兼容历史 number 输出。
|
|
256
|
+
user_id: z.union([z.string(), z.number()]),
|
|
257
|
+
nickname: z.string(),
|
|
258
|
+
subscription: SubscriptionSchema
|
|
259
|
+
});
|
|
260
|
+
var DeviceSchema = z.object({
|
|
261
|
+
name: z.string(),
|
|
262
|
+
serial: z.string(),
|
|
263
|
+
/** 型号:earphone/recorder/recolx_fit/card/unknown(后端按 SN 白名单解析) */
|
|
264
|
+
model: z.string()
|
|
265
|
+
});
|
|
266
|
+
var FileSchema = z.object({
|
|
267
|
+
file_id: z.string(),
|
|
268
|
+
title: z.string(),
|
|
269
|
+
/** Unix 秒 */
|
|
270
|
+
recorded_at: z.number(),
|
|
271
|
+
/** 秒 */
|
|
272
|
+
duration: z.number(),
|
|
273
|
+
/** 字节 */
|
|
274
|
+
file_size: z.number(),
|
|
275
|
+
device: DeviceSchema
|
|
276
|
+
});
|
|
277
|
+
var FileListSchema = z.object({
|
|
278
|
+
files: z.array(FileSchema),
|
|
279
|
+
total: z.number(),
|
|
280
|
+
page: z.number(),
|
|
281
|
+
page_size: z.number()
|
|
282
|
+
});
|
|
283
|
+
var SegmentSchema = z.object({
|
|
284
|
+
start: z.number(),
|
|
285
|
+
end: z.number(),
|
|
286
|
+
text: z.string(),
|
|
287
|
+
speaker: z.string(),
|
|
288
|
+
language: z.string()
|
|
289
|
+
});
|
|
290
|
+
var SummarySchema = z.object({
|
|
291
|
+
task_id: z.string(),
|
|
292
|
+
text: z.string(),
|
|
293
|
+
prompt_id: z.string(),
|
|
294
|
+
prompt_name: z.string()
|
|
295
|
+
});
|
|
296
|
+
var TranscriptSchema = z.object({
|
|
297
|
+
language: z.string(),
|
|
298
|
+
segments: z.array(SegmentSchema).nullable(),
|
|
299
|
+
summary: z.array(SummarySchema).nullable()
|
|
300
|
+
});
|
|
301
|
+
var FileDetailSchema = FileSchema.extend({
|
|
302
|
+
/** 24h 音频下载地址(presign JWT 在 query) */
|
|
303
|
+
presigned_url: z.string(),
|
|
304
|
+
/** Unix 秒 */
|
|
305
|
+
presigned_url_expires_at: z.number(),
|
|
306
|
+
transcript: TranscriptSchema.optional()
|
|
307
|
+
});
|
|
308
|
+
var LoginInput = z.object({}).optional();
|
|
309
|
+
var LogoutInput = z.object({}).optional();
|
|
310
|
+
var GetCurrentUserInput = z.object({}).optional();
|
|
311
|
+
var ListFilesInput = z.object({
|
|
312
|
+
page: z.number().int().min(1).optional().default(1),
|
|
313
|
+
page_size: z.number().int().min(1).max(100).optional().default(20),
|
|
314
|
+
query: z.string().optional().describe("\u6807\u9898\u5927\u5C0F\u5199\u4E0D\u654F\u611F\u5B50\u4E32\u8FC7\u6EE4\uFF08MCP \u4FA7\u5BA2\u6237\u7AEF\u8FC7\u6EE4\uFF09"),
|
|
315
|
+
date_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("\u5F00\u59CB\u65E5\u671F\uFF08\u542B\uFF09\uFF0CYYYY-MM-DD\uFF0C\u6309\u670D\u52A1\u7AEF\u65F6\u533A"),
|
|
316
|
+
date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("\u7ED3\u675F\u65E5\u671F\uFF08\u542B\uFF09\uFF0CYYYY-MM-DD\uFF0C\u6309\u670D\u52A1\u7AEF\u65F6\u533A")
|
|
317
|
+
});
|
|
318
|
+
var GetFileInput = z.object({
|
|
319
|
+
file_id: z.string().min(1).describe("\u5F55\u97F3\u6587\u4EF6 ID")
|
|
320
|
+
});
|
|
321
|
+
var GetNoteInput = z.object({
|
|
322
|
+
file_id: z.string().min(1).describe("\u5F55\u97F3\u6587\u4EF6 ID"),
|
|
323
|
+
template: z.string().optional().describe("\u6A21\u677F\u540D/\u6A21\u677F id\uFF08\u6309\u6A21\u677F tab \u5B9A\u4F4D\uFF1B\u7F3A\u7701\u8FD4\u56DE\u5168\u90E8\u6458\u8981\uFF09")
|
|
324
|
+
});
|
|
325
|
+
var GetTranscriptInput = z.object({
|
|
326
|
+
file_id: z.string().min(1).describe("\u5F55\u97F3\u6587\u4EF6 ID"),
|
|
327
|
+
cursor: z.string().optional().describe("\u4E0A\u4E00\u9875\u8FD4\u56DE\u7684\u7FFB\u9875\u6E38\u6807"),
|
|
328
|
+
language: z.string().optional().describe("\u6309\u8BED\u8A00\u8FC7\u6EE4\uFF08\u5982 zh\u3001en\uFF09"),
|
|
329
|
+
speaker: z.string().optional().describe("\u6309\u8BF4\u8BDD\u4EBA\u8FC7\u6EE4"),
|
|
330
|
+
mode: z.enum(["raw", "polish"]).optional().describe("\u9ED8\u8BA4 raw \u539F\u6587\uFF1Bpolish \u6DA6\u8272")
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// src/time.ts
|
|
334
|
+
var HAS_TIMEZONE = /(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;
|
|
335
|
+
var HAS_TIME = /\d{2}:\d{2}/;
|
|
336
|
+
var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
337
|
+
function parseApiTimestamp(value) {
|
|
338
|
+
if (!value) return null;
|
|
339
|
+
const trimmed = value.trim();
|
|
340
|
+
if (!trimmed) return null;
|
|
341
|
+
const normalised = HAS_TIME.test(trimmed) && !HAS_TIMEZONE.test(trimmed) ? `${trimmed}Z` : trimmed;
|
|
342
|
+
const ms = new Date(normalised).getTime();
|
|
343
|
+
return Number.isNaN(ms) ? null : ms;
|
|
344
|
+
}
|
|
345
|
+
function parseDateOnly(value) {
|
|
346
|
+
if (!value) return null;
|
|
347
|
+
const match = DATE_ONLY.exec(value.trim());
|
|
348
|
+
if (!match) return null;
|
|
349
|
+
const y = Number(match[1]);
|
|
350
|
+
const m = Number(match[2]);
|
|
351
|
+
const d = Number(match[3]);
|
|
352
|
+
const probe = new Date(y, m - 1, d);
|
|
353
|
+
if (probe.getFullYear() !== y || probe.getMonth() !== m - 1 || probe.getDate() !== d) return null;
|
|
354
|
+
return [y, m, d];
|
|
355
|
+
}
|
|
356
|
+
function localDayStart(value) {
|
|
357
|
+
const parts = parseDateOnly(value);
|
|
358
|
+
if (!parts) return null;
|
|
359
|
+
const [y, m, d] = parts;
|
|
360
|
+
return new Date(y, m - 1, d, 0, 0, 0, 0).getTime();
|
|
361
|
+
}
|
|
362
|
+
function localDayEnd(value) {
|
|
363
|
+
const parts = parseDateOnly(value);
|
|
364
|
+
if (!parts) return null;
|
|
365
|
+
const [y, m, d] = parts;
|
|
366
|
+
return new Date(y, m - 1, d + 1, 0, 0, 0, 0).getTime() - 1;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/callback.ts
|
|
370
|
+
import { createServer } from "http";
|
|
371
|
+
var SUCCESS_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Recolx</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>\u6388\u6743\u6210\u529F\uFF01</h1><p>\u4F60\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\uFF0C\u56DE\u5230\u539F\u7A97\u53E3\u7EE7\u7EED\u3002</p></body></html>';
|
|
372
|
+
var NEUTRAL_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Recolx</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>\u8BF7\u56DE\u5230\u539F\u7A97\u53E3\u7EE7\u7EED\u6388\u6743\u3002</h1><p>\u6B64\u9875\u9762\u53EF\u5173\u95ED\u3002</p></body></html>';
|
|
373
|
+
var ESC = { "&": "&", "<": "<", ">": ">" };
|
|
374
|
+
function errorHtml(message) {
|
|
375
|
+
const escaped = (message ?? "").replace(/[&<>]/g, (c) => ESC[c] ?? c);
|
|
376
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Recolx</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>\u6388\u6743\u5931\u8D25</h1><pre style="white-space:pre-wrap;">${escaped}</pre></body></html>`;
|
|
377
|
+
}
|
|
378
|
+
function runOAuthCallback(opts) {
|
|
379
|
+
const { port, expectedState, exchangeCode, timeoutMs = 12e4, postSuccessDelayMs = 1500, onListening } = opts;
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
let settled = false;
|
|
382
|
+
let exchangeStarted = false;
|
|
383
|
+
let exchangeSucceeded = false;
|
|
384
|
+
let timeoutId = null;
|
|
385
|
+
let closeTimeoutId = null;
|
|
386
|
+
const finalize = (result, immediate = false) => {
|
|
387
|
+
if (settled) return;
|
|
388
|
+
settled = true;
|
|
389
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
390
|
+
const close = () => {
|
|
391
|
+
try {
|
|
392
|
+
server.closeAllConnections?.();
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
server.close(() => resolve(result));
|
|
396
|
+
};
|
|
397
|
+
if (immediate || result.status !== "success") close();
|
|
398
|
+
else {
|
|
399
|
+
closeTimeoutId = setTimeout(close, postSuccessDelayMs);
|
|
400
|
+
closeTimeoutId.unref?.();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const server = createServer((req, res) => {
|
|
404
|
+
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
405
|
+
if (reqUrl.pathname !== "/auth/callback") {
|
|
406
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
407
|
+
res.end(errorHtml("404 Not Found"));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const params = reqUrl.searchParams;
|
|
411
|
+
const error = params.get("error");
|
|
412
|
+
const state = params.get("state");
|
|
413
|
+
const code = params.get("code");
|
|
414
|
+
const respond = (html, status = 200) => {
|
|
415
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
416
|
+
res.end(html);
|
|
417
|
+
};
|
|
418
|
+
if (error) {
|
|
419
|
+
const desc = params.get("error_description") ?? error;
|
|
420
|
+
respond(errorHtml(`\u6388\u6743\u88AB\u62D2\u7EDD\uFF1A${desc}`), 400);
|
|
421
|
+
finalize({ status: "denied", error: new Error(desc) });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!state || state !== expectedState) {
|
|
425
|
+
respond(NEUTRAL_HTML);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (exchangeSucceeded) {
|
|
429
|
+
respond(SUCCESS_HTML);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!code || exchangeStarted) {
|
|
433
|
+
respond(NEUTRAL_HTML);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
exchangeStarted = true;
|
|
437
|
+
exchangeCode(code).then(
|
|
438
|
+
() => {
|
|
439
|
+
exchangeSucceeded = true;
|
|
440
|
+
respond(SUCCESS_HTML);
|
|
441
|
+
finalize({ status: "success" });
|
|
442
|
+
},
|
|
443
|
+
(err) => {
|
|
444
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
445
|
+
respond(errorHtml(e.message), 500);
|
|
446
|
+
finalize({ status: "exchange-failed", error: e });
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
server.on("error", (err) => {
|
|
451
|
+
if (settled) return;
|
|
452
|
+
const message = err.code === "EADDRINUSE" ? `\u7AEF\u53E3 ${port} \u88AB\u5360\u7528 \u2014\u2014 \u53EF\u80FD\u5DF2\u6709\u53E6\u4E00\u4E2A recolx login \u5728\u8FD0\u884C\u3002\u7A0D\u7B49\u51E0\u79D2\u91CD\u8BD5\u3002` : `\u56DE\u8C03\u670D\u52A1\u5668\u9519\u8BEF\uFF1A${err.message}`;
|
|
453
|
+
finalize({ status: "listen-failed", error: new Error(message) }, true);
|
|
454
|
+
});
|
|
455
|
+
timeoutId = setTimeout(() => finalize({ status: "timeout" }, true), timeoutMs);
|
|
456
|
+
server.listen(port, () => onListening?.());
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
export {
|
|
460
|
+
CurrentUserSchema,
|
|
461
|
+
DEFAULT_API_BASE,
|
|
462
|
+
DEFAULT_CLIENT_ID,
|
|
463
|
+
DEFAULT_REDIRECT_URI,
|
|
464
|
+
DeviceSchema,
|
|
465
|
+
FileDetailSchema,
|
|
466
|
+
FileListSchema,
|
|
467
|
+
FileSchema,
|
|
468
|
+
GetCurrentUserInput,
|
|
469
|
+
GetFileInput,
|
|
470
|
+
GetNoteInput,
|
|
471
|
+
GetTranscriptInput,
|
|
472
|
+
ListFilesInput,
|
|
473
|
+
LoginInput,
|
|
474
|
+
LogoutInput,
|
|
475
|
+
OAuth,
|
|
476
|
+
RecolxClient,
|
|
477
|
+
SegmentSchema,
|
|
478
|
+
SubscriptionSchema,
|
|
479
|
+
SummarySchema,
|
|
480
|
+
TOKENS_MCP_PATH,
|
|
481
|
+
TokenStore,
|
|
482
|
+
TranscriptSchema,
|
|
483
|
+
classifyError,
|
|
484
|
+
errorTypeFromStatus,
|
|
485
|
+
generateCodeChallenge,
|
|
486
|
+
generateCodeVerifier,
|
|
487
|
+
generateState,
|
|
488
|
+
localDayEnd,
|
|
489
|
+
localDayStart,
|
|
490
|
+
parseApiTimestamp,
|
|
491
|
+
resolveConfig,
|
|
492
|
+
runOAuthCallback
|
|
493
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uzqw/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared types, zod schemas, token store and API client for recolx MCP/CLI",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": ["dist"],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
20
|
+
"test": "tsx --test test/*.test.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"zod": "^3.23.8"
|
|
24
|
+
}
|
|
25
|
+
}
|