xshell 1.2.89 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/antd.sass +4 -31
- package/apps.js +1 -1
- package/builder.js +12 -17
- package/development.js +1 -1
- package/file.d.ts +11 -7
- package/file.js +8 -4
- 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 +34 -443
- package/net.common.d.ts +178 -0
- package/net.common.js +564 -0
- package/net.d.ts +6 -175
- package/net.js +65 -530
- package/package.json +29 -30
- package/path.d.ts +2 -2
- package/platform.browser.js +9 -1
- package/platform.common.d.ts +10 -1
- package/platform.js +11 -1
- package/process.d.ts +2 -2
- package/process.js +2 -2
- package/prototype.common.d.ts +2 -2
- package/prototype.common.js +8 -8
- package/prototype.js +1 -1
- 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 +7 -3
- package/server.d.ts +27 -12
- package/server.js +209 -71
- package/utils.browser.d.ts +1 -1
- package/utils.browser.js +3 -1
- package/utils.common.d.ts +29 -10
- package/utils.common.js +102 -35
- package/utils.d.ts +2 -2
- package/utils.js +10 -4
- package/i18n/utils.d.ts +0 -1
- package/i18n/utils.js +0 -11
package/repl.js
CHANGED
|
@@ -6,8 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
-
import repl from 'repl';
|
|
10
|
-
import process from 'process';
|
|
9
|
+
import repl from 'node:repl';
|
|
10
|
+
import process from 'node:process';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { path } from "./path.js";
|
|
13
13
|
import { t } from "./i18n/instance.js";
|
|
@@ -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';
|
|
3
|
-
import type { Duplex } from 'stream';
|
|
1
|
+
import { type Server as HttpServer, type IncomingHttpHeaders, type IncomingMessage } from 'node:http';
|
|
2
|
+
import { type Http2SecureServer, type IncomingHttpHeaders as IncomingHttp2Headers } from 'node:http2';
|
|
3
|
+
import type { Duplex } from 'node: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,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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createServer as http_create_server, } from 'http';
|
|
2
|
-
import { createSecureServer as http2_create_server } from 'http2';
|
|
3
|
-
import { createSecureContext } from 'tls';
|
|
4
|
-
import zlib from 'zlib';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import { buffer as stream_to_buffer } from 'stream/consumers';
|
|
1
|
+
import { createServer as http_create_server, } from 'node:http';
|
|
2
|
+
import { createSecureServer as http2_create_server } from 'node:http2';
|
|
3
|
+
import { createSecureContext } from 'node:tls';
|
|
4
|
+
import zlib from 'node:zlib';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { buffer as stream_to_buffer } from 'node:stream/consumers';
|
|
7
7
|
import node_sea from 'node:sea';
|
|
8
8
|
import { default as Koa } from 'koa';
|
|
9
9
|
import KoaCors from '@koa/cors';
|
|
@@ -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
|
-
|
|
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,
|
|
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 (
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
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', (
|
|
166
|
-
|
|
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.
|
|
176
|
-
...this.
|
|
177
|
-
subscribe_stdio: ({ id },
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
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: (
|
|
207
|
+
unsubscribe_stdio: (message, remote) => {
|
|
208
|
+
let websocket = remote.lwebsocket.resource;
|
|
202
209
|
this.stdio_subscribers = this.stdio_subscribers.filter(s => {
|
|
203
|
-
if (s.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
298
|
-
|
|
299
|
-
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);
|
|
300
304
|
});
|
|
301
|
-
|
|
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
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TimerOptions } from 'timers';
|
|
1
|
+
import type { TimerOptions } from 'node:timers';
|
|
2
2
|
import { type Mapper } from './prototype.common.ts';
|
|
3
3
|
export declare function delay(milliseconds: number, options?: TimerOptions): Promise<void>;
|
|
4
4
|
export declare function assert<T>(assertion: T, message?: string): T;
|
|
@@ -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;
|
|
@@ -124,6 +124,13 @@ export interface Deferred<TValue> extends Promise<TValue> {
|
|
|
124
124
|
- initial?: `undefined` 传入非 undefined 值(包括 null)时直接设置为 resolved 状态
|
|
125
125
|
注: 下面的方法不能标记为 aysnc function, 否则会对返回值再做一层 Promise.resolve() 导致 reject, resolve 属性丢失 */
|
|
126
126
|
export declare function defer<TValue>(initial?: TValue): Deferred<TValue>;
|
|
127
|
+
export interface Deferred2<TValue> extends Promise<TValue> {
|
|
128
|
+
resolve(value: TValue | PromiseLike<TValue>): void;
|
|
129
|
+
reject(reason?: Error): void;
|
|
130
|
+
get settled(): boolean;
|
|
131
|
+
}
|
|
132
|
+
/** 有 settled 状态的 defer */
|
|
133
|
+
export declare function defer2<TValue>(initial?: TValue): Deferred2<TValue>;
|
|
127
134
|
export interface LockedAction<TResource, TResult> {
|
|
128
135
|
(resource: TResource): TResult | Promise<TResult>;
|
|
129
136
|
}
|
|
@@ -164,3 +171,15 @@ export declare function throttle(duration: number, func: Function, delay_first?:
|
|
|
164
171
|
export declare function debounce(duration: number, func: Function): (this: any, ...args: any[]) => void;
|
|
165
172
|
export declare function tomorrow(date?: Date | string | number): Date;
|
|
166
173
|
export declare function to_csv_field(str: string): string;
|
|
174
|
+
/** 设置 error.message 同时更新 error.stack */
|
|
175
|
+
export declare function set_error_message(error: Error, message: string): Error;
|
|
176
|
+
/** 比较两个数组中的元素完全相同,数组元素用引用比较 */
|
|
177
|
+
export declare function array_equals(a: any[], b: any[]): boolean;
|
|
178
|
+
export declare function nowstr(with_date?: boolean): string;
|
|
179
|
+
interface MyTimeout {
|
|
180
|
+
clear(): void;
|
|
181
|
+
ref: NodeJS.Timeout | number;
|
|
182
|
+
}
|
|
183
|
+
/** 面向对象的 timeout, callback 在后的 timeout */
|
|
184
|
+
export declare function set_timeout(delay: number, callback: VoidFunction): MyTimeout;
|
|
185
|
+
export {};
|