xshell 1.0.82 → 1.0.83

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/apps.d.ts CHANGED
@@ -41,14 +41,15 @@ export declare let oss: {
41
41
  access_id: string;
42
42
  access_key: string;
43
43
  }): Promise<void>;
44
- /** 上传文件到 OSS,返回文件 URL
45
- - fp: 上传到 OSS 的路径, 如 assets/web.latest.zip
44
+ /** 上传文件到 oss,返回文件 url
45
+ - fp: 上传到 oss 的路径, 如 assets/web.latest.zip
46
46
  - data: 文件 (Uint8Array) | 本地文件完整路径 (string) | ReadableStream
47
47
  - options?:
48
48
  - private?: `false` 设置为私有,需要通过 URL 签名参数访问
49
+ - print?: `true` 打印消息
49
50
  - cdn?: `false` false 时返回 oss 链接,true 时返回 cdn 链接
50
- - print?: `true` 打印消息 */
51
- upload(fp: string, data: Uint8Array | string | Readable, { private: _private, print, cdn, }?: {
51
+ - copy?: `false` 是否复制到剪贴板 */
52
+ upload(fp_remote: string, data: Uint8Array | string | Readable, { private: _private, print, cdn, }?: {
52
53
  private?: boolean;
53
54
  print?: boolean;
54
55
  cdn?: boolean;
package/apps.js CHANGED
@@ -59,31 +59,32 @@ export let oss = {
59
59
  secure: true
60
60
  });
61
61
  },
62
- /** 上传文件到 OSS,返回文件 URL
63
- - fp: 上传到 OSS 的路径, 如 assets/web.latest.zip
62
+ /** 上传文件到 oss,返回文件 url
63
+ - fp: 上传到 oss 的路径, 如 assets/web.latest.zip
64
64
  - data: 文件 (Uint8Array) | 本地文件完整路径 (string) | ReadableStream
65
65
  - options?:
66
66
  - private?: `false` 设置为私有,需要通过 URL 签名参数访问
67
+ - print?: `true` 打印消息
67
68
  - cdn?: `false` false 时返回 oss 链接,true 时返回 cdn 链接
68
- - print?: `true` 打印消息 */
69
- async upload(fp, data, { private: _private = false, print = true, cdn = false, } = {}) {
69
+ - copy?: `false` 是否复制到剪贴板 */
70
+ async upload(fp_remote, data, { private: _private = false, print = true, cdn = false, } = {}) {
70
71
  assert(this.client, 'OSS 应该已经初始化了');
71
- assert(!fp.startsWith('/'), 'fp 不能以 / 开头');
72
- assert(!fp.isdir, '不能使用 oss.upload 上传文件夹,请使用 oss.upload_dir');
72
+ assert(!fp_remote.startsWith('/'), 'fp 不能以 / 开头');
73
+ assert(!fp_remote.isdir, '不能使用 oss.upload 上传文件夹,请使用 oss.upload_dir');
73
74
  if (typeof data === 'string')
74
- assert(data[1] === ':' && path.isAbsolute(data), 'oss.upload 传入 data 参数类型为 string 时,必须为本地文件完整路径');
75
+ assert(path.isAbsolute(data), 'oss.upload 传入 data 参数类型为 string 时,必须为本地文件完整路径');
75
76
  if (data instanceof Uint8Array && !Buffer.isBuffer(data))
76
77
  data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
77
- await this.client.put(fp, data);
78
+ await this.client.put(fp_remote, data);
78
79
  let url;
79
80
  if (_private) {
80
- await this.client.putACL(fp, 'private');
81
- url = await this.get_url(fp);
81
+ await this.client.putACL(fp_remote, 'private');
82
+ url = await this.get_url(fp_remote);
82
83
  }
83
84
  else
84
- url = new URL(`${cdn ? this.fpd_cdn : this.fpd_oss}${fp}`).toString();
85
+ url = new URL(`${cdn ? this.fpd_cdn : this.fpd_oss}${fp_remote}`).toString();
85
86
  if (print)
86
- console.log(`已上传到 OSS${_private ? ' (私有)' : ''}:`, url);
87
+ console.log(`已上传到 oss${_private ? ' (私有)' : ''}:`, url);
87
88
  return url;
88
89
  },
89
90
  /** 获取经过签名后可访问的 url */
package/file.d.ts CHANGED
@@ -51,6 +51,9 @@ export declare function fread_json<T = any>(fp: string, options?: {
51
51
  encoding?: Encoding;
52
52
  print?: boolean;
53
53
  }): Promise<T>;
54
+ interface FWriteOptions {
55
+ print?: boolean;
56
+ }
54
57
  /** 写入 data 到 fp 路径所指的文件
55
58
  会在因不存在父文件夹导致写入失败时,自动创建父文件夹,并再次尝试写入
56
59
  最好预先创建父文件夹,减少文件系统操作,提升性能
@@ -62,10 +65,8 @@ export declare function fread_json<T = any>(fp: string, options?: {
62
65
  - options?:
63
66
  - dir?: 文件夹
64
67
  - print?: `true` */
65
- export declare function fwrite(fp: string | FileHandle, data: string | Uint8Array | any, { dir, print, }?: {
66
- dir?: string;
67
- print?: boolean;
68
- }): Promise<void>;
68
+ export declare function fwrite(fp: string, data: string | Uint8Array | any, options?: FWriteOptions): Promise<string>;
69
+ export declare function fwrite(fp: FileHandle, data: string | Uint8Array | any, options?: FWriteOptions): Promise<FileHandle>;
69
70
  export declare function fappend(fp: string, data: string | Uint8Array, { dir, print }?: {
70
71
  dir?: string;
71
72
  print?: boolean;
@@ -98,17 +99,18 @@ export declare function flist(fpd: string, options?: FListOptions): Promise<stri
98
99
  export declare function fstat(fp: string): Promise<FStats>;
99
100
  export declare function flstat(fp: string): Promise<FStats>;
100
101
  export declare function ffstat(handle: fsp.FileHandle): Promise<fs.BigIntStats>;
101
- /** 删除文件或文件夹 delete files or folders
102
- - fp: 文件或文件夹的完整路径 The full path to the file or folder
102
+ /** 删除文件或文件夹,返回是否实际进行了删除操作
103
+ - fp: 文件或文件夹的完整路径
103
104
  - options?:
104
- - print?: `true`
105
-
106
- 返回是否实际进行了删除操作
107
- Returns whether the delete operation actually took place */
105
+ - print?: `true` */
108
106
  export declare function fdelete(fp: string, { print }?: {
109
107
  print?: boolean;
110
108
  red?: boolean;
111
109
  }): Promise<boolean>;
110
+ /** 清空文件夹中的内容,实际为删除该文件夹后新建 */
111
+ export declare function fdclear(fpd: string, { print }?: {
112
+ print?: boolean;
113
+ }): Promise<void>;
112
114
  /** 复制文件或文件夹
113
115
  会在因不存在父文件夹导致复制失败时,自动创建父文件夹,并再次尝试复制
114
116
  最好预先创建父文件夹,减少文件系统操作,提升性能
package/file.js CHANGED
@@ -55,28 +55,13 @@ export async function fread_lines(fp, options = {}) {
55
55
  export async function fread_json(fp, options = {}) {
56
56
  return JSON.parse(await fread(fp, options));
57
57
  }
58
- /** 写入 data fp 路径所指的文件
59
- 会在因不存在父文件夹导致写入失败时,自动创建父文件夹,并再次尝试写入
60
- 最好预先创建父文件夹,减少文件系统操作,提升性能
61
- - fp: 目标文件完整路径
62
- - data: 支持下面几种类型
63
- - string: 写入文本
64
- - Uint8Array, Buffer: 写入二进制 buffer
65
- - any: 通过 JSON.stringify 转为文本后写入文件
66
- - options?:
67
- - dir?: 文件夹
68
- - print?: `true` */
69
- export async function fwrite(fp, data, { dir, print = true, } = {}) {
58
+ export async function fwrite(fp, data, { print = true } = {}) {
70
59
  const is_handle = typeof fp === 'object' && fp && 'fd' in fp;
71
60
  if (is_handle) {
72
61
  if (print)
73
62
  console.log(t('写入'), fp.fp);
74
63
  }
75
64
  else {
76
- if (dir) {
77
- assert(dir.isdir, t('dir 必须以 / 结尾'));
78
- fp = dir + fp;
79
- }
80
65
  assert(path.isAbsolute(fp), `${t('fp 必须是绝对路径,当前为:')} ${fp}`);
81
66
  if (print)
82
67
  console.log(t('写入'), fp);
@@ -94,6 +79,7 @@ export async function fwrite(fp, data, { dir, print = true, } = {}) {
94
79
  else
95
80
  throw error;
96
81
  }
82
+ return fp;
97
83
  }
98
84
  export async function fappend(fp, data, { dir, print = true } = {}) {
99
85
  if (dir) {
@@ -187,13 +173,10 @@ export async function ffstat(handle) {
187
173
  });
188
174
  });
189
175
  }
190
- /** 删除文件或文件夹 delete files or folders
191
- - fp: 文件或文件夹的完整路径 The full path to the file or folder
176
+ /** 删除文件或文件夹,返回是否实际进行了删除操作
177
+ - fp: 文件或文件夹的完整路径
192
178
  - options?:
193
- - print?: `true`
194
-
195
- 返回是否实际进行了删除操作
196
- Returns whether the delete operation actually took place */
179
+ - print?: `true` */
197
180
  export async function fdelete(fp, { print = true } = {}) {
198
181
  assert(fp.length >= 6, `fp: ${fp} ${t('不能太短,防止误删文件')}`);
199
182
  assert(path.isAbsolute(fp), t('fp 必须是绝对路径'));
@@ -213,6 +196,13 @@ export async function fdelete(fp, { print = true } = {}) {
213
196
  throw error;
214
197
  }
215
198
  }
199
+ /** 清空文件夹中的内容,实际为删除该文件夹后新建 */
200
+ export async function fdclear(fpd, { print = true } = {}) {
201
+ await fdelete(fpd, { print: false });
202
+ await fmkdir(fpd, { print: false });
203
+ if (print)
204
+ console.log(`清空了文件夹 ${fpd}`);
205
+ }
216
206
  /** 复制文件或文件夹
217
207
  会在因不存在父文件夹导致复制失败时,自动创建父文件夹,并再次尝试复制
218
208
  最好预先创建父文件夹,减少文件系统操作,提升性能
package/i18n/dict.json CHANGED
@@ -170,8 +170,8 @@
170
170
  "所有模块全部加载": {
171
171
  "en": "all modules were loaded"
172
172
  },
173
- "状态码 {{code}}, 非 2xx": {
174
- "en": "status code {{code}} is not 2xx"
173
+ "状态码 {{status}}, 非 2xx: {{url}}": {
174
+ "en": "status code {{code}} is not 2xx: {{url}}"
175
175
  },
176
176
  "请求参数:": {
177
177
  "en": "request.params:"
package/net.browser.js CHANGED
@@ -105,7 +105,7 @@ export async function request(url, options = {}) {
105
105
  try {
106
106
  response = await fetch_retry(url, fetch_options, timeout, retries);
107
107
  if (!response.ok)
108
- throw Object.assign(new Error(t('状态码 {{status}}, 非 2xx', { status: response.status })), { name: 'StatusCodeError' });
108
+ throw Object.assign(new Error(t('状态码 {{status}}, 非 2xx: {{url}}', { status: response.status, url })), { name: 'StatusCodeError' });
109
109
  }
110
110
  catch (error) {
111
111
  ;
package/net.js CHANGED
@@ -198,7 +198,7 @@ export async function request(url, options = {}) {
198
198
  body
199
199
  };
200
200
  if (!((200 <= status && status <= 299) || (redirect === 'manual' && 300 <= status && status < 400)))
201
- throw Object.assign(new Error(t('状态码 {{status}} 非 2xx', { status: response.status })), { name: 'StatusCodeError' });
201
+ throw Object.assign(new Error(t('状态码 {{status}}, 非 2xx: {{url}}', { status: response.status, url })), { name: 'StatusCodeError' });
202
202
  }
203
203
  catch (error) {
204
204
  ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.0.82",
3
+ "version": "1.0.83",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -53,12 +53,12 @@
53
53
  ]
54
54
  },
55
55
  "dependencies": {
56
- "@babel/core": "^7.23.7",
57
- "@babel/parser": "^7.23.6",
58
- "@babel/traverse": "^7.23.7",
56
+ "@babel/core": "^7.23.9",
57
+ "@babel/parser": "^7.23.9",
58
+ "@babel/traverse": "^7.23.9",
59
59
  "@koa/cors": "^5.0.0",
60
60
  "@types/ws": "^8.5.10",
61
- "ali-oss": "^6.19.0",
61
+ "ali-oss": "^6.20.0",
62
62
  "archiver": "^6.0.1",
63
63
  "byte-size": "^8.1.1",
64
64
  "chalk": "^5.3.0",
@@ -66,11 +66,11 @@
66
66
  "cli-table3": "^0.6.3",
67
67
  "cli-truncate": "^4.0.0",
68
68
  "colors": "^1.4.0",
69
- "commander": "^11.1.0",
69
+ "commander": "^12.0.0",
70
70
  "emoji-regex": "^10.3.0",
71
71
  "gulp-sort": "^2.0.0",
72
72
  "hash-string": "^1.0.0",
73
- "i18next": "^23.7.16",
73
+ "i18next": "^23.9.0",
74
74
  "i18next-scanner": "^4.4.0",
75
75
  "js-cookie": "^3.0.5",
76
76
  "koa": "^2.15.0",
@@ -80,8 +80,8 @@
80
80
  "mime-types": "^2.1.35",
81
81
  "ora": "^8.0.1",
82
82
  "react": "^18.2.0",
83
- "react-i18next": "^14.0.0",
84
- "react-object-model": "^1.2.1",
83
+ "react-i18next": "^14.0.5",
84
+ "react-object-model": "^1.2.2",
85
85
  "resolve-path": "^1.4.0",
86
86
  "strip-ansi": "^7.1.0",
87
87
  "through2": "^4.0.2",
@@ -89,7 +89,7 @@
89
89
  "tslib": "^2.6.2",
90
90
  "typescript": "^5.3.3",
91
91
  "ua-parser-js": "2.0.0-alpha.2",
92
- "undici": "^6.3.0",
92
+ "undici": "^6.6.2",
93
93
  "vinyl": "^3.0.0",
94
94
  "vinyl-fs": "^4.0.0",
95
95
  "ws": "^8.16.0",
@@ -99,7 +99,7 @@
99
99
  "xterm-addon-webgl": "^0.16.0"
100
100
  },
101
101
  "devDependencies": {
102
- "@babel/types": "^7.23.6",
102
+ "@babel/types": "^7.23.9",
103
103
  "@types/ali-oss": "^6.16.11",
104
104
  "@types/archiver": "^6.0.2",
105
105
  "@types/babel__traverse": "^7.20.5",
@@ -111,13 +111,13 @@
111
111
  "@types/koa-compress": "^4.0.6",
112
112
  "@types/lodash": "^4.14.202",
113
113
  "@types/mime-types": "^2.1.4",
114
- "@types/node": "^20.11.0",
115
- "@types/react": "^18.2.47",
114
+ "@types/node": "^20.11.19",
115
+ "@types/react": "^18.2.57",
116
116
  "@types/through2": "^2.0.41",
117
117
  "@types/tough-cookie": "^4.0.5",
118
118
  "@types/ua-parser-js": "^0.7.39",
119
119
  "@types/vinyl-fs": "^3.0.5",
120
- "@types/vscode": "^1.85.0"
120
+ "@types/vscode": "^1.86.0"
121
121
  },
122
122
  "pnpm": {
123
123
  "patchedDependencies": {
package/process.d.ts CHANGED
@@ -115,13 +115,15 @@ export interface TermOptions {
115
115
  /** `true` 打印参数 */
116
116
  print?: boolean;
117
117
  title?: string;
118
+ /** `process.env` 覆盖/添加到 process.env 的环境变量 */
119
+ envs?: Record<string, string>;
118
120
  }
119
121
  export declare const exe_winterm: string;
120
122
  /** 新建 terminal 窗口执行进程 New Terminal window execution process
121
123
  - exe: exe 文件路径 exe file path
122
124
  - args: 调用参数 call parameter
123
125
  - options?: WinTermOptions */
124
- export declare function term(exe: string, args?: string[], { cwd, print, title, }?: TermOptions): Promise<ChildProcess>;
126
+ export declare function term(exe: string, args?: string[], { cwd, print, title, envs }?: TermOptions): Promise<ChildProcess>;
125
127
  export interface TermNodeOptions extends TermOptions {
126
128
  /** nodejs debugger port */
127
129
  inspect?: number;
package/process.js CHANGED
@@ -37,10 +37,10 @@ export async function start(exe, args = [], { cwd, encoding = 'utf-8', print = t
37
37
  if (typeof stdio === 'string')
38
38
  stdio = [stdio, stdio, stdio];
39
39
  if (print.command)
40
- console.log(((short_exe_names[exe] || (exe.includes(' ') ? exe.quote() : exe)) +
40
+ console.log(((short_exe_names[exe] || exe.quote_if_space()) +
41
41
  (args.length ?
42
42
  ' ' + args.filter(arg => arg !== '--suppressApplicationTitle')
43
- .map(arg => arg.includes(' ') ? arg.quote() : arg)
43
+ .map(arg => arg.quote_if_space())
44
44
  .join(' ')
45
45
  :
46
46
  '')).blue);
@@ -97,8 +97,8 @@ export async function call(exe, args = [], options = {}) {
97
97
  };
98
98
  if (typeof stdio === 'string')
99
99
  stdio = [stdio, stdio, stdio];
100
- const cmd = (short_exe_names[exe] || (exe.includes(' ') ? exe.quote() : exe)) +
101
- (args.length ? (' ' + args.map(arg => arg.includes(' ') ? arg.quote() : arg)
100
+ const cmd = (short_exe_names[exe] || exe.quote_if_space()) +
101
+ (args.length ? (' ' + args.map(arg => arg.quote_if_space())
102
102
  .join(' '))
103
103
  :
104
104
  '');
@@ -191,9 +191,7 @@ export const exe_winterm = `C:/Users/${username}/AppData/Local/Microsoft/Windows
191
191
  - exe: exe 文件路径 exe file path
192
192
  - args: 调用参数 call parameter
193
193
  - options?: WinTermOptions */
194
- export async function term(exe, args = [], { cwd = process.cwd().fpd, print = true, title,
195
- // env
196
- } = {}) {
194
+ export async function term(exe, args = [], { cwd = process.cwd().fpd, print = true, title, envs } = {}) {
197
195
  return start(exe_winterm, [
198
196
  'new-tab',
199
197
  '-d', cwd,
@@ -204,7 +202,7 @@ export async function term(exe, args = [], { cwd = process.cwd().fpd, print = tr
204
202
  print,
205
203
  detached: true,
206
204
  cwd,
207
- // env
205
+ envs
208
206
  });
209
207
  }
210
208
  /** 在 term tab 中创建 node.exe 进程 create the node.exe process in term tab */
package/prototype.d.ts CHANGED
@@ -56,11 +56,12 @@ declare global {
56
56
 
57
57
  - preservations?: `''` 保留的正则表达式字符
58
58
  - flags?: `''` 正则匹配选项
59
- - pattern_placeholder?: `/\{.*?\}/g`
60
- */
59
+ - pattern_placeholder?: `/\{.*?\}/g` */
61
60
  find(this: string, pattern: string, preservations?: string, flags?: string, pattern_placeholder?: RegExp): Record<string, string>;
62
- /** - type?: `'single'` */
61
+ /** - type?: `'single'` 引号类型 */
63
62
  quote(this: string, type?: keyof typeof quotes | 'psh'): string;
63
+ /** - type?: `'single'` 引号类型 */
64
+ quote_if_space(this: string, type?: keyof typeof quotes | 'psh'): string;
64
65
  /** - shape?: `'parenthesis'` */
65
66
  bracket(this: string, shape?: keyof typeof brackets): string;
66
67
  surround(this: string, left: string, right?: string): string;
package/prototype.js CHANGED
@@ -219,6 +219,9 @@ if (!globalThis.my_prototype_defined) {
219
219
  return `& ${this.quote()}`;
220
220
  return this.surround(quotes[type]);
221
221
  },
222
+ quote_if_space(type = 'single') {
223
+ return this.includes(' ') ? this.quote(type) : this;
224
+ },
222
225
  bracket(shape = 'round') {
223
226
  return this.surround(...brackets[shape]);
224
227
  },
package/server.d.ts CHANGED
@@ -92,7 +92,9 @@ export declare class Server {
92
92
  logger(ctx: Context): void;
93
93
  process_ua(ctx: Context): string;
94
94
  format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
95
- proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>, redirect?: RequestOptions['redirect']): Promise<void>;
95
+ proxy(ctx: Context,
96
+ /** 只含 host, path, 不含 queries 的 url */
97
+ path_url: string | URL, headers_?: Record<string, string>, redirect?: RequestOptions['redirect']): Promise<void>;
96
98
  static filter_response_headers(headers: RawResponse['headers']): Record<string, string>;
97
99
  /** 提供静态文件
98
100
  @example
package/server.js CHANGED
@@ -436,13 +436,14 @@ export class Server {
436
436
  }
437
437
  return s;
438
438
  }
439
- async proxy(ctx, url, headers_ = {}, redirect = 'manual') {
440
- const { request: { method, headers, query, body, ip } } = ctx;
439
+ async proxy(ctx,
440
+ /** 只含 host, path, 不含 queries url */
441
+ path_url, headers_ = {}, redirect = 'manual') {
442
+ const { request: { method, headers, body, ip } } = ctx;
441
443
  let { response } = ctx;
442
444
  try {
443
- const response_ = await _request(url, {
445
+ const response_ = await _request(path_url, {
444
446
  method: method,
445
- queries: query,
446
447
  body,
447
448
  headers: {
448
449
  ...headers,
@@ -463,7 +464,7 @@ export class Server {
463
464
  if (error.response) {
464
465
  const { status, headers, body } = error.response;
465
466
  if (status === 404)
466
- console.log(method, '404:', url);
467
+ console.log(method, '404:', path_url);
467
468
  else
468
469
  console.log(error);
469
470
  response.status = status;
@@ -56,7 +56,7 @@ export declare function encode(str: string): Uint8Array;
56
56
  在流式处理 (buffer 可能不完整) 时,应使用独立的 TextDecoder 实例调用 decode(buffer, { stream: true }) */
57
57
  export declare function decode(buffer: Uint8Array): string;
58
58
  /** 字符串字典序比较 */
59
- export declare function strcmp(l: string, r: string): 1 | 0 | -1;
59
+ export declare function strcmp(l: string, r: string): 0 | 1 | -1;
60
60
  /** 比较 1.10.02 这种版本号 */
61
61
  export declare function vercmp(l: string, r: string): number;
62
62
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
package/utils.d.ts CHANGED
@@ -29,10 +29,12 @@ export declare function map_values<TValue, TNewValue>(obj: {
29
29
  }, mapper: (value: TValue, key: string) => TNewValue): {
30
30
  [k: string]: TNewValue;
31
31
  };
32
- /** 映射对象中的 keys, 返回新对象 */
32
+ /** 过滤对象中的 keys, 返回新对象 */
33
33
  export declare function filter_keys<TObj>(obj: TObj, filter: (key: string) => any): TObj;
34
+ /** 忽略对象中的 keys, 返回新对象 */
35
+ export declare function omit<TObj>(obj: TObj, omit_keys: string[]): TObj;
34
36
  /** 字符串字典序比较 */
35
- export declare function strcmp(l: string, r: string): 1 | 0 | -1;
37
+ export declare function strcmp(l: string, r: string): 0 | 1 | -1;
36
38
  /** 比较 1.10.02 这种版本号 */
37
39
  export declare function vercmp(l: string, r: string): number;
38
40
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
package/utils.js CHANGED
@@ -92,10 +92,15 @@ export function map_values(obj, mapper) {
92
92
  return Object.fromEntries(Object.entries(obj)
93
93
  .map(([key, value]) => [key, mapper(value, key)]));
94
94
  }
95
- /** 映射对象中的 keys, 返回新对象 */
95
+ /** 过滤对象中的 keys, 返回新对象 */
96
96
  export function filter_keys(obj, filter) {
97
97
  return Object.fromEntries(Object.entries(obj)
98
- .filter(([key, value]) => filter(key)));
98
+ .filter(([key]) => filter(key)));
99
+ }
100
+ /** 忽略对象中的 keys, 返回新对象 */
101
+ export function omit(obj, omit_keys) {
102
+ const omit_keys_ = new Set(omit_keys);
103
+ return filter_keys(obj, key => !omit_keys_.has(key));
99
104
  }
100
105
  /** 字符串字典序比较 */
101
106
  export function strcmp(l, r) {