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/antd.sass +4 -31
- package/builder.js +5 -14
- package/file.d.ts +2 -2
- package/file.js +1 -1
- package/i18n/dict.json +12 -0
- package/i18n/scanner/index.d.ts +1 -0
- package/i18n/scanner/index.js +14 -5
- package/net.browser.d.ts +1 -142
- package/net.browser.js +1 -408
- package/net.common.d.ts +178 -0
- package/net.common.js +564 -0
- package/net.d.ts +2 -173
- package/net.js +1 -466
- package/package.json +29 -32
- package/path.d.ts +2 -2
- package/platform.browser.js +9 -1
- package/platform.common.d.ts +9 -0
- package/platform.js +11 -1
- package/prototype.common.d.ts +2 -2
- package/prototype.common.js +8 -8
- package/react.development.js +9199 -6036
- package/react.development.js.map +1 -1
- package/react.production.js +4958 -4306
- package/react.production.js.map +1 -1
- package/repl.js +5 -1
- package/server.d.ts +24 -11
- package/server.js +198 -116
- package/utils.browser.d.ts +1 -1
- package/utils.browser.js +3 -1
- package/utils.common.d.ts +11 -9
- package/utils.common.js +50 -35
- package/utils.js +7 -1
- package/i18n/utils.d.ts +0 -1
- package/i18n/utils.js +0 -11
package/repl.js
CHANGED
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
|
-
|
|
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:
|
|
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,
|
|
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?:
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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', (
|
|
167
|
-
|
|
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.
|
|
177
|
-
...this.
|
|
178
|
-
subscribe_stdio: ({ id },
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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(
|
|
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: (
|
|
207
|
+
unsubscribe_stdio: (message, remote) => {
|
|
208
|
+
let websocket = remote.lwebsocket.resource;
|
|
203
209
|
this.stdio_subscribers = this.stdio_subscribers.filter(s => {
|
|
204
|
-
if (s.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
s += `/${
|
|
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,
|
|
303
|
-
|
|
304
|
-
this.websocket_server.emit('connection',
|
|
301
|
+
this.websocket_server.handleUpgrade(request, socket, head, websocket => {
|
|
302
|
+
websocket.binaryType = 'arraybuffer';
|
|
303
|
+
this.websocket_server.emit('connection', websocket, request);
|
|
305
304
|
});
|
|
306
|
-
|
|
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
|
-
//
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
s += `/${
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
571
|
+
return [platform, version?.strip_if_end('.0.0')].filter(Boolean).join(' ');
|
|
490
572
|
}
|
|
491
573
|
/** 转发请求到其他 http 服务,实现网关的功能
|
|
492
574
|
- ctx
|
package/utils.browser.d.ts
CHANGED
|
@@ -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<
|
|
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
|
-
-
|
|
71
|
-
-
|
|
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
|
-
-
|
|
82
|
-
-
|
|
83
|
-
export declare function fuzzyfilter<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;
|