xshell 1.2.88 → 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
@@ -3,7 +3,7 @@ import { type Http2SecureServer, type IncomingHttpHeaders as IncomingHttp2Header
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,8 +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;
108
- process_ua(ctx: Context): string;
109
- format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
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;
110
117
  /** 转发请求到其他 http 服务,实现网关的功能
111
118
  - ctx
112
119
  - path_url: 只含 host, path, 不含 queries 的 url, 如 http://localhost:8080/api/get-user
@@ -158,6 +165,11 @@ export declare class Server {
158
165
  - reverse?: `false` 在 range 内从后往前尝试 */
159
166
  static get_available_port(range: string, reverse?: boolean): Promise<number>;
160
167
  }
168
+ interface StdioSubscriber {
169
+ remote: Remote;
170
+ id: number;
171
+ close(): void;
172
+ }
161
173
  export interface ServerRequestOptions {
162
174
  headers?: Record<string, string | null>;
163
175
  queries?: Record<string, string>;
@@ -167,3 +179,4 @@ export interface ServerRequestOptions {
167
179
  proxy?: RequestOptions['proxy'];
168
180
  }
169
181
  export declare const text_plain: "text/plain; charset=utf-8";
182
+ export {};
package/server.js CHANGED
@@ -8,7 +8,6 @@ import node_sea from 'node:sea';
8
8
  import { default as Koa } from 'koa';
9
9
  import KoaCors from '@koa/cors';
10
10
  import KoaCompress from 'koa-compress';
11
- import { UAParser } from 'ua-parser-js';
12
11
  import resolve_safely from 'resolve-path';
13
12
  import { contentType as get_content_type } from 'mime-types';
14
13
  // --- my libs
@@ -40,6 +39,24 @@ export class Server {
40
39
  static logger_ignore_fexts = new Set(['js', 'css', 'wasm', 'map', 'png', 'jpg', 'svg', 'ico', 'json', 'woff2', 'ttf', 'php']);
41
40
  static empty_body_statuses = new Set([304, 204, 205]);
42
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
+ };
43
60
  app;
44
61
  handler;
45
62
  /** 启用 http server */
@@ -55,7 +72,7 @@ export class Server {
55
72
  http2_server;
56
73
  websocket_server;
57
74
  /** 设置后会启用 websocket rpc */
58
- remote;
75
+ funcs;
59
76
  /** 输出日志时包含日期 */
60
77
  log_date = false;
61
78
  /** 启用后增加 stdio 订阅相关的 remote.funcs */
@@ -64,7 +81,7 @@ export class Server {
64
81
  /** 原始 process.stdout.write 函数 bind 后的备份 */
65
82
  stdout_write;
66
83
  stderr_write;
67
- 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 }) {
68
85
  this.name = name;
69
86
  if (print !== undefined) {
70
87
  if (typeof print === 'boolean')
@@ -86,12 +103,10 @@ export class Server {
86
103
  this.default_hostnames = default_hostnames;
87
104
  if (log_date !== undefined)
88
105
  this.log_date = log_date;
89
- if (remote)
90
- this.remote = remote;
91
- else if (funcs)
92
- this.remote = new Remote({ funcs, print: this.print.errors });
93
- if (stdio_subscribable !== undefined) {
94
- check(remote || funcs);
106
+ if (funcs)
107
+ this.funcs = funcs;
108
+ if (stdio_subscribable) {
109
+ check(funcs);
95
110
  this.stdio_subscribable = stdio_subscribable;
96
111
  }
97
112
  }
@@ -123,7 +138,7 @@ export class Server {
123
138
  app.use(this._router.bind(this));
124
139
  this.app = app;
125
140
  this.handler = this.app.callback();
126
- this.http_server = http_create_server(this.handler);
141
+ this.http_server = http_create_server({ optimizeEmptyRequests: true }, this.handler);
127
142
  const { http, http2 } = this;
128
143
  if (http2) {
129
144
  const { fpd_certs } = this;
@@ -154,7 +169,7 @@ export class Server {
154
169
  }, this.handler);
155
170
  }
156
171
  // websocket rpc
157
- if (this.remote) {
172
+ if (this.funcs) {
158
173
  const { WebSocketServer } = await import('ws');
159
174
  this.websocket_server = new WebSocketServer({
160
175
  noServer: true,
@@ -163,78 +178,51 @@ export class Server {
163
178
  allowSynchronousEvents: true,
164
179
  maxPayload: 2 ** 30 // 1 GB
165
180
  });
166
- this.websocket_server.on('connection', (ws, request) => {
167
- ws.addEventListener('message', ({ data }) => {
168
- this.remote.handle(new Uint8Array(data), ws);
169
- });
181
+ this.websocket_server.on('connection', (websocket, request) => {
182
+ new Remote({ websocket, funcs: this.funcs });
170
183
  });
171
184
  const on_upgrade = this.on_upgrade.bind(this);
172
185
  this.http_server.on('upgrade', on_upgrade);
173
186
  this.http2_server?.on('upgrade', on_upgrade);
174
187
  // 将输出到 stdout, stderr 的内容 copy 一份通过 websocket 发到 web shell
175
188
  if (this.stdio_subscribable) {
176
- this.remote.funcs = {
177
- ...this.remote.funcs,
178
- subscribe_stdio: ({ id }, websocket) => {
179
- const subscriber = async (chunk) => {
180
- // send 时有可能 websocket 连接断开,抛异常,为防止循环调用 (console.error -> stdio subscriber -> throw error -> console.error)
181
- // 这里只能忽略错误
182
- try {
183
- await this.remote.send({
184
- id,
185
- data: typeof chunk === 'string' ? encode(chunk) : chunk
186
- }, websocket);
187
- }
188
- catch { }
189
- };
190
- // 让后续可以通过 unsubscribe_stdio 取消订阅
191
- subscriber.id = id;
192
- const close = subscriber.close = () => {
193
- const length = this.stdio_subscribers.length;
194
- const stdio_subscribers_ = this.stdio_subscribers.filter(s => s !== subscriber);
195
- 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)
196
196
  this.stdio_subscribers = stdio_subscribers_;
197
197
  };
198
- this.stdio_subscribers.push(subscriber);
198
+ this.stdio_subscribers.push({
199
+ remote,
200
+ id,
201
+ close
202
+ });
203
+ websocket.addEventListener('error', close, { once: true });
199
204
  websocket.addEventListener('close', close, { once: true });
200
205
  },
201
206
  /** 主动取消订阅,需要清理 stdio listener, websocket close lisener */
202
- unsubscribe_stdio: ({ data: [id] }, websocket) => {
207
+ unsubscribe_stdio: (message, remote) => {
208
+ let websocket = remote.lwebsocket.resource;
203
209
  this.stdio_subscribers = this.stdio_subscribers.filter(s => {
204
- if (s.id === id) {
205
- websocket.removeEventListener('close', s.close);
206
- return false;
207
- }
208
- else
210
+ if (s.remote !== remote)
209
211
  return true;
212
+ websocket.removeEventListener('error', s.close);
213
+ websocket.removeEventListener('close', s.close);
214
+ return false;
210
215
  });
211
- return [];
212
- },
216
+ }
213
217
  };
214
218
  this.stdout_write = process.stdout.write.bind(process.stdout);
215
219
  this.stderr_write = process.stderr.write.bind(process.stderr);
216
220
  process.stdout.write = (...args) => {
217
- if (this.stdio_subscribers.length)
218
- (async () => {
219
- try {
220
- await Promise.all(this.stdio_subscribers.map(async (subscriber) => subscriber(args[0])));
221
- }
222
- catch {
223
- this.stdout_write('stdio_subscriber error\n');
224
- }
225
- })();
221
+ this.try_write_stdio_subscribers(args[0]);
226
222
  return this.stdout_write(...args);
227
223
  };
228
224
  process.stderr.write = (...args) => {
229
- if (this.stdio_subscribers.length)
230
- (async () => {
231
- try {
232
- await Promise.all(this.stdio_subscribers.map(async (subscriber) => subscriber(args[0])));
233
- }
234
- catch {
235
- this.stderr_write('stderr_subscriber error\n');
236
- }
237
- })();
225
+ this.try_write_stdio_subscribers(args[0]);
238
226
  return this.stderr_write(...args);
239
227
  };
240
228
  }
@@ -257,6 +245,17 @@ export class Server {
257
245
  ].join(', ')
258
246
  }));
259
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
+ }
260
259
  stop() {
261
260
  this.http_server.close();
262
261
  if (this.http2_server)
@@ -291,19 +290,19 @@ export class Server {
291
290
  s = s.pad(url_width);
292
291
  // ip
293
292
  s += ` <- ${request.socket.remoteAddress.strip_if_start('::ffff:')}`;
294
- // ua
295
- const ua = this.format_ua(headers);
296
- if (ua)
297
- s += `/${ua}`;
293
+ // 客户端信息
294
+ const client_info = this.get_client_info(headers);
295
+ if (client_info)
296
+ s += `/${client_info}`;
298
297
  console.log(s);
299
298
  }
300
299
  switch (url) {
301
300
  case '/':
302
- this.websocket_server.handleUpgrade(request, socket, head, ws => {
303
- ws.binaryType = 'arraybuffer';
304
- 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);
305
304
  });
306
- return;
305
+ break;
307
306
  default:
308
307
  if (this.print.logs)
309
308
  console.log(`未知路径的 upgrade 请求: ${url}`.red);
@@ -431,62 +430,145 @@ export class Server {
431
430
  s = s.pad(url_width);
432
431
  // ip
433
432
  s += ` <- ${ip}`;
434
- // ua
435
- const ua = this.process_ua(ctx);
436
- if (ua)
437
- s += `/${ua}`;
433
+ // 客户端信息
434
+ const client_info = this.get_client_info(headers);
435
+ if (client_info)
436
+ s += `/${client_info}`;
438
437
  // body
439
438
  if (body)
440
439
  s += '\n' + inspect(body);
441
440
  // 打印日志
442
441
  console.log(s);
443
442
  }
444
- process_ua(ctx) {
445
- const { headers, headers: { 'user-agent': ua_str } } = ctx.request;
446
- if (!ua_str)
447
- return '';
448
- ctx.response.set('accept-ch', 'Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, RTT, Device-Memory');
449
- return this.format_ua(headers);
450
- }
451
- format_ua(headers) {
452
- const { rtt, 'device-memory': memory } = headers;
453
- const { device, os, browser } = UAParser({ ...headers }).withClientHints();
454
- const vendor = device.vendor?.toLowerCase();
455
- const model = device.model?.toLowerCase();
456
- const osname = os.name?.toLowerCase();
457
- const browser_name = browser.name?.toLowerCase();
458
- let s = '';
459
- if (vendor && vendor !== 'apple')
460
- s += vendor;
461
- if (model && model !== 'k' && model !== 'macintosh') {
462
- if (s)
463
- s += ' ';
464
- s += model.replace('22127rk46c', 'redmi k60 pro');
465
- }
466
- if (osname) {
467
- if (s)
468
- s += '/';
469
- s += osname;
470
- if (os.version)
471
- s += ` ${os.version.split('.')[0].toLowerCase()}`;
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(' ');
472
485
  }
473
- if (browser_name) {
474
- if (s)
475
- s += '/';
476
- s += browser_name.replace('mobile chrome', 'chrome');
477
- if (browser.version) {
478
- const [major, minor] = browser.version.toLowerCase().split('.');
479
- s += ` ${major}`;
480
- if (minor && minor !== '0')
481
- s += `.${minor}`;
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);
482
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();
483
551
  }
484
- if (memory) {
485
- s += `/${memory}g`;
486
- if (rtt && rtt !== '0')
487
- s += ` ${rtt}ms`;
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}?`;
488
570
  }
489
- return s;
571
+ return [platform, version?.strip_if_end('.0.0')].filter(Boolean).join(' ');
490
572
  }
491
573
  /** 转发请求到其他 http 服务,实现网关的功能
492
574
  - ctx
@@ -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;