xshell 1.2.89 → 1.3.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/repl.js CHANGED
@@ -41,7 +41,11 @@ export async function start_repl() {
41
41
  name: 'repl',
42
42
  http: true,
43
43
  http_port: 8421,
44
- funcs: {}
44
+ funcs: {
45
+ exit() {
46
+ setTimeout(stop);
47
+ }
48
+ }
45
49
  });
46
50
  await server.start();
47
51
  })(),
package/server.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { type Server as HttpServer, type IncomingMessage } from 'http';
2
- import { type Http2SecureServer } from 'http2';
1
+ import { type Server as HttpServer, type IncomingHttpHeaders, type IncomingMessage } from 'http';
2
+ import { type Http2SecureServer, type IncomingHttpHeaders as IncomingHttp2Headers } from 'http2';
3
3
  import type { Duplex } from 'stream';
4
4
  import type { WebSocketServer } from 'ws';
5
5
  import { default as Koa, type Context, type Next } from 'koa';
6
- import { Remote, type RequestOptions, type RawResponse } from './net.ts';
6
+ import { Remote, type RequestOptions, type RawResponse, type MessageHandler } from './net.ts';
7
7
  declare module 'http' {
8
8
  interface IncomingMessage {
9
9
  body?: Buffer;
@@ -37,6 +37,15 @@ export declare class Server {
37
37
  static logger_ignore_fexts: Set<string>;
38
38
  static empty_body_statuses: Set<number>;
39
39
  static empty_body_methods: Set<string>;
40
+ static windows_platform_versions: {
41
+ legacy: {
42
+ 0.1: string;
43
+ 0.2: string;
44
+ 0.3: string;
45
+ };
46
+ /** platform_version (1 ~ 8) - 1 作为索引下标 */
47
+ win10: string[];
48
+ };
40
49
  app: Koa;
41
50
  handler: ReturnType<Koa['callback']>;
42
51
  /** 启用 http server */
@@ -52,19 +61,16 @@ export declare class Server {
52
61
  http2_server?: Http2SecureServer;
53
62
  websocket_server?: WebSocketServer;
54
63
  /** 设置后会启用 websocket rpc */
55
- remote?: Remote;
64
+ funcs?: Record<string, MessageHandler>;
56
65
  /** 输出日志时包含日期 */
57
66
  log_date: boolean;
58
67
  /** 启用后增加 stdio 订阅相关的 remote.funcs */
59
68
  stdio_subscribable?: boolean;
60
- stdio_subscribers: (((chunk: Uint8Array | string) => Promise<void>) & {
61
- id: number;
62
- close: () => void;
63
- })[];
69
+ stdio_subscribers: StdioSubscriber[];
64
70
  /** 原始 process.stdout.write 函数 bind 后的备份 */
65
71
  stdout_write: Function;
66
72
  stderr_write: Function;
67
- constructor({ name, print, http, http2, http_port, http2_port, fpd_certs, default_hostnames, remote, funcs, stdio_subscribable, log_date }: {
73
+ constructor({ name, print, http, http2, http_port, http2_port, fpd_certs, default_hostnames, funcs, stdio_subscribable, log_date }: {
68
74
  name: string;
69
75
  print?: boolean | {
70
76
  info?: boolean;
@@ -77,13 +83,13 @@ export declare class Server {
77
83
  http2_port?: number;
78
84
  fpd_certs?: string;
79
85
  default_hostnames?: string[];
80
- remote?: Remote;
81
86
  funcs?: Remote['funcs'];
82
- stdio_subscribable?: boolean;
87
+ stdio_subscribable?: true;
83
88
  log_date?: boolean;
84
89
  });
85
90
  /** start http server and listen */
86
91
  start(): Promise<void>;
92
+ try_write_stdio_subscribers(chunk: string | Uint8Array): void;
87
93
  stop(): void;
88
94
  /** 可被子类重写定义错误处理逻辑 */
89
95
  on_error(error: Error & {
@@ -105,6 +111,9 @@ export declare class Server {
105
111
  - code: 301 (永久重定向) | 302 (临时重定向) | 307 (非 GET 请求保持原有方法和 body 的临时重定向) */
106
112
  redirect(ctx: Context, url: string, code: 301 | 302 | 307): true;
107
113
  logger(ctx: Context): void;
114
+ /** model? platform platform_version / mobile browser */
115
+ get_client_info(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
116
+ get_client_platform_and_version(platform: string, version: string): any;
108
117
  /** 转发请求到其他 http 服务,实现网关的功能
109
118
  - ctx
110
119
  - path_url: 只含 host, path, 不含 queries 的 url, 如 http://localhost:8080/api/get-user
@@ -156,6 +165,11 @@ export declare class Server {
156
165
  - reverse?: `false` 在 range 内从后往前尝试 */
157
166
  static get_available_port(range: string, reverse?: boolean): Promise<number>;
158
167
  }
168
+ interface StdioSubscriber {
169
+ remote: Remote;
170
+ id: number;
171
+ close(): void;
172
+ }
159
173
  export interface ServerRequestOptions {
160
174
  headers?: Record<string, string | null>;
161
175
  queries?: Record<string, string>;
@@ -165,3 +179,4 @@ export interface ServerRequestOptions {
165
179
  proxy?: RequestOptions['proxy'];
166
180
  }
167
181
  export declare const text_plain: "text/plain; charset=utf-8";
182
+ export {};
package/server.js CHANGED
@@ -39,6 +39,24 @@ export class Server {
39
39
  static logger_ignore_fexts = new Set(['js', 'css', 'wasm', 'map', 'png', 'jpg', 'svg', 'ico', 'json', 'woff2', 'ttf', 'php']);
40
40
  static empty_body_statuses = new Set([304, 204, 205]);
41
41
  static empty_body_methods = new Set(['HEAD', 'OPTIONS']);
42
+ static windows_platform_versions = {
43
+ legacy: {
44
+ 0.1: 'win7',
45
+ 0.2: 'win8',
46
+ 0.3: 'win8.1'
47
+ },
48
+ /** platform_version (1 ~ 8) - 1 作为索引下标 */
49
+ win10: [
50
+ 'win10 1507',
51
+ 'win10 1511',
52
+ 'win10 1607',
53
+ 'win10 1703',
54
+ 'win10 1709',
55
+ 'win10 1803',
56
+ 'win10 1809',
57
+ 'win10 1909',
58
+ ]
59
+ };
42
60
  app;
43
61
  handler;
44
62
  /** 启用 http server */
@@ -54,7 +72,7 @@ export class Server {
54
72
  http2_server;
55
73
  websocket_server;
56
74
  /** 设置后会启用 websocket rpc */
57
- remote;
75
+ funcs;
58
76
  /** 输出日志时包含日期 */
59
77
  log_date = false;
60
78
  /** 启用后增加 stdio 订阅相关的 remote.funcs */
@@ -63,7 +81,7 @@ export class Server {
63
81
  /** 原始 process.stdout.write 函数 bind 后的备份 */
64
82
  stdout_write;
65
83
  stderr_write;
66
- constructor({ name, print, http, http2, http_port, http2_port, fpd_certs, default_hostnames, remote, funcs, stdio_subscribable, log_date }) {
84
+ constructor({ name, print, http, http2, http_port, http2_port, fpd_certs, default_hostnames, funcs, stdio_subscribable, log_date }) {
67
85
  this.name = name;
68
86
  if (print !== undefined) {
69
87
  if (typeof print === 'boolean')
@@ -85,12 +103,10 @@ export class Server {
85
103
  this.default_hostnames = default_hostnames;
86
104
  if (log_date !== undefined)
87
105
  this.log_date = log_date;
88
- if (remote)
89
- this.remote = remote;
90
- else if (funcs)
91
- this.remote = new Remote({ funcs, print: this.print.errors });
92
- if (stdio_subscribable !== undefined) {
93
- check(remote || funcs);
106
+ if (funcs)
107
+ this.funcs = funcs;
108
+ if (stdio_subscribable) {
109
+ check(funcs);
94
110
  this.stdio_subscribable = stdio_subscribable;
95
111
  }
96
112
  }
@@ -122,7 +138,7 @@ export class Server {
122
138
  app.use(this._router.bind(this));
123
139
  this.app = app;
124
140
  this.handler = this.app.callback();
125
- this.http_server = http_create_server(this.handler);
141
+ this.http_server = http_create_server({ optimizeEmptyRequests: true }, this.handler);
126
142
  const { http, http2 } = this;
127
143
  if (http2) {
128
144
  const { fpd_certs } = this;
@@ -153,7 +169,7 @@ export class Server {
153
169
  }, this.handler);
154
170
  }
155
171
  // websocket rpc
156
- if (this.remote) {
172
+ if (this.funcs) {
157
173
  const { WebSocketServer } = await import('ws');
158
174
  this.websocket_server = new WebSocketServer({
159
175
  noServer: true,
@@ -162,78 +178,51 @@ export class Server {
162
178
  allowSynchronousEvents: true,
163
179
  maxPayload: 2 ** 30 // 1 GB
164
180
  });
165
- this.websocket_server.on('connection', (ws, request) => {
166
- ws.addEventListener('message', ({ data }) => {
167
- this.remote.handle(new Uint8Array(data), ws);
168
- });
181
+ this.websocket_server.on('connection', (websocket, request) => {
182
+ new Remote({ websocket, funcs: this.funcs });
169
183
  });
170
184
  const on_upgrade = this.on_upgrade.bind(this);
171
185
  this.http_server.on('upgrade', on_upgrade);
172
186
  this.http2_server?.on('upgrade', on_upgrade);
173
187
  // 将输出到 stdout, stderr 的内容 copy 一份通过 websocket 发到 web shell
174
188
  if (this.stdio_subscribable) {
175
- this.remote.funcs = {
176
- ...this.remote.funcs,
177
- subscribe_stdio: ({ id }, websocket) => {
178
- const subscriber = async (chunk) => {
179
- // send 时有可能 websocket 连接断开,抛异常,为防止循环调用 (console.error -> stdio subscriber -> throw error -> console.error)
180
- // 这里只能忽略错误
181
- try {
182
- await this.remote.send({
183
- id,
184
- data: typeof chunk === 'string' ? encode(chunk) : chunk
185
- }, websocket);
186
- }
187
- catch { }
188
- };
189
- // 让后续可以通过 unsubscribe_stdio 取消订阅
190
- subscriber.id = id;
191
- const close = subscriber.close = () => {
192
- const length = this.stdio_subscribers.length;
193
- const stdio_subscribers_ = this.stdio_subscribers.filter(s => s !== subscriber);
194
- if (stdio_subscribers_.length !== length)
189
+ this.funcs = {
190
+ ...this.funcs,
191
+ subscribe_stdio: ({ id }, remote) => {
192
+ let websocket = remote.lwebsocket.resource;
193
+ const close = () => {
194
+ const stdio_subscribers_ = this.stdio_subscribers.filter(s => s.remote !== remote);
195
+ if (stdio_subscribers_.length !== this.stdio_subscribers.length)
195
196
  this.stdio_subscribers = stdio_subscribers_;
196
197
  };
197
- this.stdio_subscribers.push(subscriber);
198
+ this.stdio_subscribers.push({
199
+ remote,
200
+ id,
201
+ close
202
+ });
203
+ websocket.addEventListener('error', close, { once: true });
198
204
  websocket.addEventListener('close', close, { once: true });
199
205
  },
200
206
  /** 主动取消订阅,需要清理 stdio listener, websocket close lisener */
201
- unsubscribe_stdio: ({ data: [id] }, websocket) => {
207
+ unsubscribe_stdio: (message, remote) => {
208
+ let websocket = remote.lwebsocket.resource;
202
209
  this.stdio_subscribers = this.stdio_subscribers.filter(s => {
203
- if (s.id === id) {
204
- websocket.removeEventListener('close', s.close);
205
- return false;
206
- }
207
- else
210
+ if (s.remote !== remote)
208
211
  return true;
212
+ websocket.removeEventListener('error', s.close);
213
+ websocket.removeEventListener('close', s.close);
214
+ return false;
209
215
  });
210
- return [];
211
- },
216
+ }
212
217
  };
213
218
  this.stdout_write = process.stdout.write.bind(process.stdout);
214
219
  this.stderr_write = process.stderr.write.bind(process.stderr);
215
220
  process.stdout.write = (...args) => {
216
- if (this.stdio_subscribers.length)
217
- (async () => {
218
- try {
219
- await Promise.all(this.stdio_subscribers.map(async (subscriber) => subscriber(args[0])));
220
- }
221
- catch {
222
- this.stdout_write('stdio_subscriber error\n');
223
- }
224
- })();
221
+ this.try_write_stdio_subscribers(args[0]);
225
222
  return this.stdout_write(...args);
226
223
  };
227
224
  process.stderr.write = (...args) => {
228
- if (this.stdio_subscribers.length)
229
- (async () => {
230
- try {
231
- await Promise.all(this.stdio_subscribers.map(async (subscriber) => subscriber(args[0])));
232
- }
233
- catch {
234
- this.stderr_write('stderr_subscriber error\n');
235
- }
236
- })();
225
+ this.try_write_stdio_subscribers(args[0]);
237
226
  return this.stderr_write(...args);
238
227
  };
239
228
  }
@@ -256,6 +245,17 @@ export class Server {
256
245
  ].join(', ')
257
246
  }));
258
247
  }
248
+ try_write_stdio_subscribers(chunk) {
249
+ if (!this.stdio_subscribers.length)
250
+ return;
251
+ for (let i = 0; i < this.stdio_subscribers.length; ++i) {
252
+ let { remote, id } = this.stdio_subscribers[i];
253
+ remote.try_send({
254
+ id,
255
+ data: typeof chunk === 'string' ? encode(chunk) : chunk
256
+ });
257
+ }
258
+ }
259
259
  stop() {
260
260
  this.http_server.close();
261
261
  if (this.http2_server)
@@ -290,15 +290,19 @@ export class Server {
290
290
  s = s.pad(url_width);
291
291
  // ip
292
292
  s += ` <- ${request.socket.remoteAddress.strip_if_start('::ffff:')}`;
293
+ // 客户端信息
294
+ const client_info = this.get_client_info(headers);
295
+ if (client_info)
296
+ s += `/${client_info}`;
293
297
  console.log(s);
294
298
  }
295
299
  switch (url) {
296
300
  case '/':
297
- this.websocket_server.handleUpgrade(request, socket, head, ws => {
298
- ws.binaryType = 'arraybuffer';
299
- this.websocket_server.emit('connection', ws, request);
301
+ this.websocket_server.handleUpgrade(request, socket, head, websocket => {
302
+ websocket.binaryType = 'arraybuffer';
303
+ this.websocket_server.emit('connection', websocket, request);
300
304
  });
301
- return;
305
+ break;
302
306
  default:
303
307
  if (this.print.logs)
304
308
  console.log(`未知路径的 upgrade 请求: ${url}`.red);
@@ -426,12 +430,146 @@ export class Server {
426
430
  s = s.pad(url_width);
427
431
  // ip
428
432
  s += ` <- ${ip}`;
433
+ // 客户端信息
434
+ const client_info = this.get_client_info(headers);
435
+ if (client_info)
436
+ s += `/${client_info}`;
429
437
  // body
430
438
  if (body)
431
439
  s += '\n' + inspect(body);
432
440
  // 打印日志
433
441
  console.log(s);
434
442
  }
443
+ /** model? platform platform_version / mobile browser */
444
+ get_client_info(headers) {
445
+ const { 'user-agent': user_agent, 'sec-ch-ua': ua, 'sec-ch-ua-mobile': mobile, 'sec-ch-ua-model': _model, 'sec-ch-ua-platform': _platform, 'sec-ch-ua-platform-version': _platform_version } = headers;
446
+ // --- client hints
447
+ let model = _model?.slice(1, -1).toLowerCase();
448
+ if (model === '22127rk46c')
449
+ model = 'redmi k60 pro';
450
+ else if (model === '23013rk75c')
451
+ model = 'redmi k60';
452
+ else if (model === '24129pn74c')
453
+ model = 'xiaomi 15';
454
+ const ch_platform = [
455
+ model,
456
+ mobile === '?1' ? 'mobile' : '',
457
+ this.get_client_platform_and_version(_platform?.slice(1, -1).toLowerCase(), _platform_version?.slice(1, -1))
458
+ ].filter(Boolean).join(' ');
459
+ let ch_browser;
460
+ if (ua) {
461
+ const items = ua.toLowerCase().split(',');
462
+ const i_not_a_brand = items.findIndex(item => item.includes('no') && item.includes('brand'));
463
+ let browsers = [];
464
+ for (let i = 0; i < items.length; ++i) {
465
+ const item = items[i];
466
+ if (i === i_not_a_brand ||
467
+ // 有除了 chromium 的其他浏览器,那么这个是垃圾
468
+ item.includes('"chromium"') && items.length > 2 && i_not_a_brand !== -1)
469
+ continue;
470
+ const matches = /"(.*)";v="(.*)"/.exec(item);
471
+ if (!matches) {
472
+ console.log('奇怪的 ua item:', item);
473
+ continue;
474
+ }
475
+ let [, name, version] = matches;
476
+ if (name === 'google chrome')
477
+ name = 'chrome';
478
+ else if (name === 'microsoft edge')
479
+ name = 'edge';
480
+ else if (name === 'android webview' && ch_platform.includes('android'))
481
+ name = 'webview';
482
+ browsers.push(`${name} ${version}`);
483
+ }
484
+ ch_browser = browsers.join(' ');
485
+ }
486
+ // --- user-agent
487
+ let ua_platform;
488
+ let ua_browser;
489
+ if (user_agent) {
490
+ // 按照空格(非括号内, 非后面跟 (, 非 / 前) 划分多个部分
491
+ let parts = [];
492
+ let i = 0, j = 0, in_parenthesis = false;
493
+ for (let slash = false; i < user_agent.length; ++i) {
494
+ const c = user_agent[i];
495
+ if (c === ')' && in_parenthesis) {
496
+ in_parenthesis = false;
497
+ continue;
498
+ }
499
+ if (c === '/') {
500
+ slash = true;
501
+ continue;
502
+ }
503
+ if (c === ' ') {
504
+ if (user_agent[i + 1] === '(') {
505
+ in_parenthesis = true;
506
+ continue;
507
+ }
508
+ if (!in_parenthesis && slash) {
509
+ parts.push(user_agent.slice(j, i));
510
+ j = i + 1;
511
+ continue;
512
+ }
513
+ }
514
+ }
515
+ const remaining = user_agent.slice(j, i);
516
+ if (remaining)
517
+ parts.push(remaining);
518
+ let has_mozilla = false;
519
+ const part0 = parts[0];
520
+ if (part0 && part0.startsWith('Mozilla/5.0 (') && part0.endsWith(')')) {
521
+ // 只取括号中间的
522
+ ua_platform = part0.slice(13, -1).toLowerCase()
523
+ .replace('22127rk46c', 'redmi k60 pro')
524
+ .replace('23013rk75c', 'redmi k60')
525
+ .replace('24129pn74c', 'xiaomi 15')
526
+ .replace('macintosh', 'mac')
527
+ .replace('linux; android', 'android')
528
+ .replace(' build/tkq1.220905.001', '');
529
+ // 没有多余的信息,屏蔽
530
+ if (ua_platform === 'windows nt 10.0; win64; x64' && ch_platform.includes('win1'))
531
+ ua_platform = '';
532
+ if (ch_platform && ua_platform)
533
+ ua_platform = ua_platform.bracket();
534
+ has_mozilla = true;
535
+ }
536
+ const has_chrome = parts.find(part => part.includes('Chrome'));
537
+ let parts_ = [];
538
+ for (let i = has_mozilla ? 1 : 0; i < parts.length; ++i) {
539
+ const part = parts[i];
540
+ if ((part.startsWith('AppleWebKit/') && part.endsWith(' (KHTML, like Gecko)')) ||
541
+ (part.includes('Safari/') && (has_chrome || i !== parts.length - 1)) ||
542
+ (part.startsWith('Chrome/') && ch_browser && (ch_browser.includes('chrome ') || ch_browser.includes('webview ')))) // 垃圾
543
+ continue;
544
+ parts_.push(part);
545
+ }
546
+ ua_browser = parts_.join(' ').toLowerCase();
547
+ if (!ua_browser.includes('://'))
548
+ ua_browser.replaceAll('/', ' ');
549
+ if (ch_browser && ua_browser)
550
+ ua_browser = ua_browser.bracket();
551
+ }
552
+ return [
553
+ [ch_platform, ua_platform].filter(Boolean).join(' '),
554
+ [ch_browser, ua_browser].filter(Boolean).join(' ')
555
+ ].filter(Boolean).join('/');
556
+ }
557
+ get_client_platform_and_version(platform, version) {
558
+ if (platform === 'windows' && version) {
559
+ const v = Number(version.slice_to('.', { optional: true }));
560
+ if (v === 19)
561
+ return 'win11 24h2';
562
+ else if (v > 13)
563
+ return `win11 (${v})`;
564
+ else if (v === 10)
565
+ return 'win10 21h2';
566
+ else if (v >= 1)
567
+ return Server.windows_platform_versions.win10[v - 1];
568
+ else
569
+ return Server.windows_platform_versions.legacy[version] || `win ${version}?`;
570
+ }
571
+ return [platform, version?.strip_if_end('.0.0')].filter(Boolean).join(' ');
572
+ }
435
573
  /** 转发请求到其他 http 服务,实现网关的功能
436
574
  - ctx
437
575
  - path_url: 只含 host, path, 不含 queries 的 url, 如 http://localhost:8080/api/get-user
@@ -18,4 +18,4 @@ export declare function to_option(value: string): {
18
18
  };
19
19
  export declare function download_url(name: string, url: string): void;
20
20
  export declare function download(name: string, data: string | Uint8Array, mime_type?: string): void;
21
- export declare function load_script(url: string): Promise<unknown>;
21
+ export declare function load_script(url: string, type?: HTMLScriptElement['type']): Promise<Event>;
package/utils.browser.js CHANGED
@@ -41,13 +41,15 @@ export function download_url(name, url) {
41
41
  export function download(name, data, mime_type) {
42
42
  download_url(name, URL.createObjectURL(new Blob([data], { type: mime_type })));
43
43
  }
44
- export async function load_script(url) {
44
+ export async function load_script(url, type) {
45
45
  return new Promise((resolve, reject) => {
46
46
  let $script = document.createElement('script');
47
47
  $script.src = url;
48
48
  $script.async = true;
49
49
  $script.onload = resolve;
50
50
  $script.onerror = reject;
51
+ if (type)
52
+ $script.type = type;
51
53
  document.head.appendChild($script);
52
54
  });
53
55
  }
package/utils.common.d.ts CHANGED
@@ -24,7 +24,7 @@ export declare function strcmp(l: string, r: string): 0 | 1 | -1;
24
24
  export declare function sort_keys<TObj>(obj: TObj): TObj;
25
25
  /** 比较 1.10.02 这种版本号
26
26
  - l, r: 两个版本号字符串
27
- - loose?: 宽松模式,允许两个版本号格式(位数)不一致 */
27
+ - loose?: `false` 宽松模式,允许两个版本号格式(位数)不一致 */
28
28
  export declare function vercmp(l: string, r: string, loose?: boolean): number;
29
29
  /** 将 keys, values 数组按对应的顺序组合成一个对象 */
30
30
  export declare function zip_object<TValue>(keys: (string | number)[], values: TValue[]): Record<string, TValue>;
@@ -67,20 +67,20 @@ export declare function buffer_equals(left: Uint8Array, right: Uint8Array | stri
67
67
  - milliseconds: 限时毫秒数
68
68
  - action?: 要等待运行的任务, async function 或 promise
69
69
  - on_timeout?: 超时后调用的函数
70
- - 如果传入了 on_timeout 参数: 调用 on_timeout,然后 timeout 函数正常返回 null
71
- - 如果没传入 on_timeout 参数: 抛出 TimeoutError
72
- - print?: 打印已超时任务的错误 */
73
- export declare function timeout<TReturn>(milliseconds: number, action: Promise<TReturn> | (() => Promise<TReturn>), on_timeout?: () => void | Promise<void>, print?: boolean): Promise<TReturn>;
70
+ - 若传: 调用 on_timeout,参数为 TimeoutError,然后 timeout 函数正常返回 null
71
+ - 若不传: 抛出 TimeoutError
72
+ - print?: `true` 打印已超时任务的错误 */
73
+ export declare function timeout<TReturn>(milliseconds: number, action: Promise<TReturn> | (() => Promise<TReturn>), on_timeout?: (error: TimeoutError) => void | Promise<void>, print?: boolean): Promise<TReturn>;
74
74
  /** 轮询尝试 action 共 times 次,每次间隔 duration
75
75
  action 返回 trusy 值时认为成功,返回 action 的结果
76
76
  如果次数用尽仍然失败,返回 null */
77
77
  export declare function poll<TResult>(duration: number, times: number, action: (breaker: () => void) => Promise<TResult>): Promise<TResult>;
78
78
  /** 模糊过滤字符串列表或对象列表,常用于根据用户输入补全或搜索过滤
79
- 如果有完全匹配的,只返回那一项的数组
79
+ 如果有完全匹配关键词的,只返回完全匹配关键词的候选项
80
80
  - query: 查询字符串,要求为全小写
81
- - list: 要过滤的列表
82
- - list_lower?: 要过滤的列表对应的全小写字符串列表形式,传入时可复用缓存,加快搜索速度 */
83
- export declare function fuzzyfilter<TItem = string>(query: string, list: TItem[], mapper?: keyof TItem | Mapper<TItem>, list_lower?: string[], single_char_startswith?: boolean): TItem[];
81
+ - items: TItem[], 要过滤的列表
82
+ - keywords: string[][] 每个 item 对应一个全小写字符串的关键词数组,用于实际筛选匹配 */
83
+ export declare function fuzzyfilter<TItem>(query: string, items: TItem[], keywords: string[][], single_char_startswith?: boolean): TItem[];
84
84
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
85
85
  export declare function global_get<TReturn = any>(keypath: string): TReturn;
86
86
  export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
@@ -164,3 +164,5 @@ export declare function throttle(duration: number, func: Function, delay_first?:
164
164
  export declare function debounce(duration: number, func: Function): (this: any, ...args: any[]) => void;
165
165
  export declare function tomorrow(date?: Date | string | number): Date;
166
166
  export declare function to_csv_field(str: string): string;
167
+ /** 设置 error.message 同时更新 error.stack */
168
+ export declare function set_error_message(error: Error, message: string): Error;