mn-ws 0.1.0 → 0.2.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/assets/groups.json +10 -0
- package/assets/mn-ws/SKILL.md +55 -0
- package/assets/mn-ws/hooks.json +14 -0
- package/assets/mn-ws/install/inject-root.mjs +26 -0
- package/bin/ws.mjs +13 -0
- package/dist/core/auth.d.ts +194 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +331 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/brand.d.ts +11 -0
- package/dist/core/brand.d.ts.map +1 -0
- package/dist/core/brand.js +7 -0
- package/dist/core/brand.js.map +1 -0
- package/dist/core/bridge.d.ts +106 -0
- package/dist/core/bridge.d.ts.map +1 -0
- package/dist/core/bridge.js +308 -0
- package/dist/core/bridge.js.map +1 -0
- package/dist/core/config.d.ts +174 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +417 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/controller.d.ts +64 -0
- package/dist/core/controller.d.ts.map +1 -0
- package/dist/core/controller.js +100 -0
- package/dist/core/controller.js.map +1 -0
- package/dist/core/errors/index.d.ts +30 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js +88 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/open.d.ts +3 -0
- package/dist/core/open.d.ts.map +1 -0
- package/dist/core/open.js +27 -0
- package/dist/core/open.js.map +1 -0
- package/dist/core/paths.d.ts +15 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +50 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/serve-port.d.ts +73 -0
- package/dist/core/serve-port.d.ts.map +1 -0
- package/dist/core/serve-port.js +134 -0
- package/dist/core/serve-port.js.map +1 -0
- package/dist/core/session.d.ts +78 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +124 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/skills/bus.d.ts +29 -0
- package/dist/core/skills/bus.d.ts.map +1 -0
- package/dist/core/skills/bus.js +114 -0
- package/dist/core/skills/bus.js.map +1 -0
- package/dist/core/skills/get.d.ts +17 -0
- package/dist/core/skills/get.d.ts.map +1 -0
- package/dist/core/skills/get.js +87 -0
- package/dist/core/skills/get.js.map +1 -0
- package/dist/core/skills/handlers.d.ts +22 -0
- package/dist/core/skills/handlers.d.ts.map +1 -0
- package/dist/core/skills/handlers.js +86 -0
- package/dist/core/skills/handlers.js.map +1 -0
- package/dist/core/skills/index.d.ts +47 -0
- package/dist/core/skills/index.d.ts.map +1 -0
- package/dist/core/skills/index.js +268 -0
- package/dist/core/skills/index.js.map +1 -0
- package/dist/core/skills/install.d.ts +18 -0
- package/dist/core/skills/install.d.ts.map +1 -0
- package/dist/core/skills/install.js +123 -0
- package/dist/core/skills/install.js.map +1 -0
- package/dist/core/skills/text-util.d.ts +2 -0
- package/dist/core/skills/text-util.d.ts.map +1 -0
- package/dist/core/skills/text-util.js +14 -0
- package/dist/core/skills/text-util.js.map +1 -0
- package/dist/core/store.d.ts +29 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +100 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/views.d.ts +11 -0
- package/dist/core/views.d.ts.map +1 -0
- package/dist/core/views.js +20 -0
- package/dist/core/views.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/auth/index.d.ts +73 -0
- package/dist/modules/auth/index.d.ts.map +1 -0
- package/dist/modules/auth/index.js +64 -0
- package/dist/modules/auth/index.js.map +1 -0
- package/dist/modules/auth/service.d.ts +98 -0
- package/dist/modules/auth/service.d.ts.map +1 -0
- package/dist/modules/auth/service.js +200 -0
- package/dist/modules/auth/service.js.map +1 -0
- package/dist/modules/bridge/index.d.ts +73 -0
- package/dist/modules/bridge/index.d.ts.map +1 -0
- package/dist/modules/bridge/index.js +89 -0
- package/dist/modules/bridge/index.js.map +1 -0
- package/dist/modules/bridge/service.d.ts +75 -0
- package/dist/modules/bridge/service.d.ts.map +1 -0
- package/dist/modules/bridge/service.js +134 -0
- package/dist/modules/bridge/service.js.map +1 -0
- package/dist/modules/home/index.d.ts +95 -0
- package/dist/modules/home/index.d.ts.map +1 -0
- package/dist/modules/home/index.js +123 -0
- package/dist/modules/home/index.js.map +1 -0
- package/dist/modules/home/service.d.ts +48 -0
- package/dist/modules/home/service.d.ts.map +1 -0
- package/dist/modules/home/service.js +195 -0
- package/dist/modules/home/service.js.map +1 -0
- package/dist/modules/skill/index.d.ts +78 -0
- package/dist/modules/skill/index.d.ts.map +1 -0
- package/dist/modules/skill/index.js +132 -0
- package/dist/modules/skill/index.js.map +1 -0
- package/dist/runtime/api.d.ts +24 -0
- package/dist/runtime/api.d.ts.map +1 -0
- package/dist/runtime/api.js +140 -0
- package/dist/runtime/api.js.map +1 -0
- package/dist/runtime/cli.d.ts +23 -0
- package/dist/runtime/cli.d.ts.map +1 -0
- package/dist/runtime/cli.js +410 -0
- package/dist/runtime/cli.js.map +1 -0
- package/dist/runtime/help.d.ts +29 -0
- package/dist/runtime/help.d.ts.map +1 -0
- package/dist/runtime/help.js +89 -0
- package/dist/runtime/help.js.map +1 -0
- package/dist/runtime/registry.d.ts +318 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/registry.js +75 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime/server.d.ts +10 -0
- package/dist/runtime/server.d.ts.map +1 -0
- package/dist/runtime/server.js +127 -0
- package/dist/runtime/server.js.map +1 -0
- package/dist/runtime/spec.d.ts +32 -0
- package/dist/runtime/spec.d.ts.map +1 -0
- package/dist/runtime/spec.js +180 -0
- package/dist/runtime/spec.js.map +1 -0
- package/dist/runtime/types/action.d.ts +101 -0
- package/dist/runtime/types/action.d.ts.map +1 -0
- package/dist/runtime/types/action.js +19 -0
- package/dist/runtime/types/action.js.map +1 -0
- package/dist/runtime/types/client.d.ts +39 -0
- package/dist/runtime/types/client.d.ts.map +1 -0
- package/dist/runtime/types/client.js +2 -0
- package/dist/runtime/types/client.js.map +1 -0
- package/dist/runtime/types/http.d.ts +22 -0
- package/dist/runtime/types/http.d.ts.map +1 -0
- package/dist/runtime/types/http.js +6 -0
- package/dist/runtime/types/http.js.map +1 -0
- package/dist/runtime/types/module.d.ts +72 -0
- package/dist/runtime/types/module.d.ts.map +1 -0
- package/dist/runtime/types/module.js +15 -0
- package/dist/runtime/types/module.js.map +1 -0
- package/dist/web/public/assets/CliHints-BOxU-Duc.js +1 -0
- package/dist/web/public/assets/index-BTRKdMIW.js +50 -0
- package/dist/web/public/assets/index-BjGe8ggI.css +1 -0
- package/dist/web/public/assets/view-CE4tnCuy.js +1 -0
- package/dist/web/public/assets/view-CEQ2uPxE.js +1 -0
- package/dist/web/public/favicon-16.png +0 -0
- package/dist/web/public/favicon-32.png +0 -0
- package/dist/web/public/favicon-48.png +0 -0
- package/dist/web/public/favicon.ico +0 -0
- package/dist/web/public/index.html +19 -0
- package/dist/web/public/logo-rounded.png +0 -0
- package/dist/web/public/logo-rounded.svg +6 -0
- package/dist/web/public/logo.png +0 -0
- package/dist/web/public/logo.svg +6 -0
- package/package.json +56 -35
- package/README.md +0 -251
- package/bin/index.js +0 -714
- package/client/bridge-client.js +0 -160
- package/client/web.js +0 -234
- package/client/ws-client.js +0 -191
- package/lib/apps.js +0 -237
- package/lib/auth.js +0 -319
- package/lib/config.js +0 -534
- package/lib/index.js +0 -15
- package/lib/logger.js +0 -63
- package/lib/protocol.js +0 -208
- package/lib/store.js +0 -66
- package/server/auth.js +0 -274
- package/server/server.js +0 -752
package/bin/index.js
DELETED
|
@@ -1,714 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* mn-ws CLI 入口。
|
|
6
|
-
*
|
|
7
|
-
* 子命令(命令名与 mn-rt 的 client 保持同一套形状):
|
|
8
|
-
* mn-ws server 启动中继服务(公网半,HTTP + WS 同端口)
|
|
9
|
-
* mn-ws client 启动本机转发(连 server + 注册可桥接应用)
|
|
10
|
-
* mn-ws client web 本地 Web 面板(只监听 127.0.0.1)
|
|
11
|
-
* mn-ws client apps 列出可桥接的应用(白名单 + 端口存活)
|
|
12
|
-
* mn-ws client status 查看桥接连接状态
|
|
13
|
-
* mn-ws client catalog 拉取远端元信息目录(登录后看哪些能路由)
|
|
14
|
-
* mn-ws auth login 统一登录(go-ah auth-hub):免掉人工复制令牌
|
|
15
|
-
* mn-ws auth list|switch 多人共用一台机器:查看 / 切换本机已存账号
|
|
16
|
-
* mn-ws auth status|logout 查看 / 清除本机登录凭据
|
|
17
|
-
* mn-ws doctor 环境自检(端口 / 鉴权 / 应用可达性)
|
|
18
|
-
*
|
|
19
|
-
* 全局参数:
|
|
20
|
-
* --url <ws://...> server 半地址(client 用)
|
|
21
|
-
* --server-http <url> 统一登录用的 HTTP 基址(默认从 --url 推导)
|
|
22
|
-
* --host <h> 监听地址(server 默认 0.0.0.0;本地面板固定 127.0.0.1)
|
|
23
|
-
* --port <n> server 监听端口(默认 7777)
|
|
24
|
-
* --web-port <n> 本地面板端口(默认 7777)
|
|
25
|
-
* --token <TOKEN> 注册门禁(与 server 侧 MN_WS_TOKEN 一致)
|
|
26
|
-
* --apps <a,b,c> 只桥接这些应用(**白名单**,不给就一个都不桥)
|
|
27
|
-
* --device-name <名> 桥的显示名
|
|
28
|
-
* --no-open auth login 只打印授权地址、不弹浏览器
|
|
29
|
-
* --insecure server 允许无鉴权启动(**仅限本机自测**)
|
|
30
|
-
* --auth-enabled <bool> 显式开/关统一登录(默认跟随「配没配 MN_WS_JWT_SECRET」)
|
|
31
|
-
* --log-level <level> debug | info | warn | error
|
|
32
|
-
* --help / --version
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
const path = require('node:path');
|
|
36
|
-
|
|
37
|
-
const { Logger } = require('../lib/logger');
|
|
38
|
-
const { scanApps, isPortFree, KNOWN_APPS } = require('../lib/apps');
|
|
39
|
-
const {
|
|
40
|
-
DEFAULT_SERVE_URL, configDir, parseArgs, loadConfig, placeholderUrlReason,
|
|
41
|
-
readConfigFile, ensureClientId, withAccounts, listAccounts, labelOf, entryKeyOf,
|
|
42
|
-
saveAuthAccount, switchActiveAccount, clearAuthCredentials, saveConnectionConfig,
|
|
43
|
-
} = require('../lib/config');
|
|
44
|
-
const { authLogin, getJson } = require('../lib/auth');
|
|
45
|
-
const { createServer } = require('../server/server');
|
|
46
|
-
const { WsBridgeClient } = require('../client/ws-client');
|
|
47
|
-
const { startWebPanel } = require('../client/web');
|
|
48
|
-
|
|
49
|
-
const pkg = require('../package.json');
|
|
50
|
-
|
|
51
|
-
const C = {
|
|
52
|
-
reset: '\x1b[0m',
|
|
53
|
-
bold: '\x1b[1m',
|
|
54
|
-
dim: '\x1b[90m',
|
|
55
|
-
red: '\x1b[31m',
|
|
56
|
-
green: '\x1b[32m',
|
|
57
|
-
yellow: '\x1b[33m',
|
|
58
|
-
cyan: '\x1b[36m',
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
function say(msg = '') {
|
|
62
|
-
process.stdout.write(`${msg}\n`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function printHelp() {
|
|
66
|
-
say(`
|
|
67
|
-
${C.bold}mn-ws${C.reset} v${pkg.version} — HTTP ⇄ WebSocket 双向桥(把本机 nx 服务桥给远端客户端)
|
|
68
|
-
|
|
69
|
-
${C.bold}用法${C.reset}
|
|
70
|
-
npx mn-ws <command> [options]
|
|
71
|
-
|
|
72
|
-
${C.bold}命令${C.reset}
|
|
73
|
-
${C.cyan}(无命令)${C.reset} 显示本帮助
|
|
74
|
-
${C.cyan}server${C.reset} 启动中继服务(公网半):HTTP 与 WS 同端口共存
|
|
75
|
-
${C.cyan}client${C.reset} 启动本机转发:连上 server 并注册可桥接的应用
|
|
76
|
-
${C.cyan}client web${C.reset} 本地 Web 面板(只监听 127.0.0.1)
|
|
77
|
-
${C.cyan}client apps${C.reset} 列出可桥接的应用(白名单 + 本地端口存活探测)
|
|
78
|
-
${C.cyan}client status${C.reset} 查看桥接连接状态
|
|
79
|
-
${C.cyan}client catalog${C.reset} 拉取远端元信息目录(登录后看哪些能路由)
|
|
80
|
-
${C.cyan}auth login${C.reset} 统一登录(弹浏览器):换回设备令牌,之后免手填
|
|
81
|
-
${C.cyan}auth login --new-account${C.reset} 换人登录:强制认证中心重新登录
|
|
82
|
-
${C.cyan}auth list${C.reset} 列出本机已存账号
|
|
83
|
-
${C.cyan}auth switch${C.reset} <账号> 切换当前生效账号
|
|
84
|
-
${C.cyan}auth status${C.reset} 查看当前身份、令牌剩余有效期
|
|
85
|
-
${C.cyan}auth logout${C.reset} 清除当前账号的登录凭据(--all 清空全部)
|
|
86
|
-
${C.cyan}doctor${C.reset} 环境自检:端口 / 鉴权 / 应用可达性
|
|
87
|
-
|
|
88
|
-
${C.bold}选项${C.reset}
|
|
89
|
-
--url <ws://...> server 半地址(默认 ${DEFAULT_SERVE_URL})
|
|
90
|
-
--server-http <url> 统一登录用的 HTTP 基址(默认从 --url 推导)
|
|
91
|
-
--host <h> 监听地址(server 默认 0.0.0.0;本地面板固定 127.0.0.1)
|
|
92
|
-
--port <n> server 监听端口(默认 7777)
|
|
93
|
-
--web-port <n> 本地面板端口(默认 7777)
|
|
94
|
-
--token <TOKEN> 注册门禁(server 侧 MN_WS_TOKEN 一致)
|
|
95
|
-
--apps <a,b,c> 只桥接这些应用(白名单;不给就一个都不桥)
|
|
96
|
-
--ports <p,q> 只桥接这些本地端口(从已知表反查应用名)
|
|
97
|
-
--app-map <a=url,...> 指定应用的本地基址(如 nx-kv=http://127.0.0.1:7999)
|
|
98
|
-
--device-name <名> 桥的显示名
|
|
99
|
-
--no-open auth login 只打印授权地址、不弹浏览器
|
|
100
|
-
--insecure server 允许无鉴权启动(仅限本机自测)
|
|
101
|
-
--auth-enabled <bool> 显式开/关统一登录(1/true/yes/on 与 0/false/no/off)
|
|
102
|
-
--log-level <level> debug | info | warn | error(默认 info)
|
|
103
|
-
--help 显示帮助
|
|
104
|
-
--version 显示版本
|
|
105
|
-
|
|
106
|
-
${C.bold}示例${C.reset}
|
|
107
|
-
npx mn-ws server --port 7777 --token SECRET # 公网侧起中继
|
|
108
|
-
npx mn-ws auth login --url ws://1.94.101.189:7777/ws # 统一登录
|
|
109
|
-
npx mn-ws client --apps nx-kv,nx-rh # 本机转发这两个应用
|
|
110
|
-
npx mn-ws client web # 本地面板看看哪些能桥
|
|
111
|
-
npx mn-ws doctor # 上线前自检
|
|
112
|
-
`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function printVersion() {
|
|
116
|
-
say(`mn-ws v${pkg.version} (${process.platform}-${process.arch}, node ${process.version})`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* 把白名单解析成 { app -> base };不给就空表(= 一个都不桥)。
|
|
121
|
-
*
|
|
122
|
-
* 两种指定方式:
|
|
123
|
-
* · --apps nx-kv,nx-rh 按应用名
|
|
124
|
-
* · --ports 7877,7800 按端口(从已知表反查应用名 —— 用户看到的是端口)
|
|
125
|
-
* · --app-map nx-kv=http://… 自定义基址(端口改了、容器映射等)
|
|
126
|
-
*/
|
|
127
|
-
function resolveApps(cfg) {
|
|
128
|
-
const wanted = new Set(cfg.apps || []);
|
|
129
|
-
|
|
130
|
-
// 按端口反查应用名
|
|
131
|
-
for (const port of cfg.ports || []) {
|
|
132
|
-
const hit = Object.entries(KNOWN_APPS).find(([, info]) => info.port === port);
|
|
133
|
-
if (hit) wanted.add(hit[0]);
|
|
134
|
-
else say(`${C.yellow}! 端口 ${port} 不在已知服务表里${C.reset}${C.dim}(用 --app-map 指定应用名)${C.reset}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const apps = {};
|
|
138
|
-
for (const app of wanted) {
|
|
139
|
-
const base = (cfg.appOverrides && cfg.appOverrides[app] && cfg.appOverrides[app].base)
|
|
140
|
-
|| (KNOWN_APPS[app] ? `http://127.0.0.1:${KNOWN_APPS[app].port}` : null);
|
|
141
|
-
if (base) apps[app] = base;
|
|
142
|
-
else {
|
|
143
|
-
say(`${C.yellow}! 应用 ${app} 既不在已知表里,也没给 --app-map${C.reset}`);
|
|
144
|
-
say(`${C.dim} 形如:--app-map ${app}=http://127.0.0.1:<端口>${C.reset}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return apps;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ============================ server ============================
|
|
151
|
-
|
|
152
|
-
async function cmdServer(cfg) {
|
|
153
|
-
// ── 安全闸:没有鉴权就不许起公网监听 ──
|
|
154
|
-
const hasGate = !!cfg.token;
|
|
155
|
-
const hasJwt = !!cfg.jwtSecret;
|
|
156
|
-
|
|
157
|
-
// 统一登录的**有效状态**:显式开关优先,没表态就跟随「配没配 JWT secret」。
|
|
158
|
-
//
|
|
159
|
-
// 为什么要有这个显式开关:部署脚本里把 MN_WS_AUTH_ENABLED 写成 1 而实际
|
|
160
|
-
// 生效成「关」,服务照常起、/health 照样绿,而整套按用户隔离的属主判据
|
|
161
|
-
// 静默不存在 —— CI 全绿、人以为开着。开关必须能表达「意图」,
|
|
162
|
-
// 部署侧才有得断言。
|
|
163
|
-
const wantAuth = cfg.authEnabled === undefined ? hasJwt : cfg.authEnabled;
|
|
164
|
-
|
|
165
|
-
if (wantAuth && !hasJwt) {
|
|
166
|
-
say(`${C.red}✗ 拒绝启动:统一登录已开启,但没有 MN_WS_JWT_SECRET${C.reset}`);
|
|
167
|
-
say(`${C.dim}签不出设备令牌 = 统一登录等于没开。给一个随机串,或把 MN_WS_AUTH_ENABLED 关掉。${C.reset}`);
|
|
168
|
-
process.exitCode = 1;
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (!wantAuth && hasJwt) {
|
|
172
|
-
say(`${C.yellow}! 统一登录已显式关闭${C.reset}${C.dim},MN_WS_JWT_SECRET 不会生效${C.reset}`);
|
|
173
|
-
}
|
|
174
|
-
if (!hasGate && !wantAuth && !cfg.insecure) {
|
|
175
|
-
say(`${C.red}✗ 拒绝启动:没有任何鉴权配置${C.reset}`);
|
|
176
|
-
say(`${C.dim}mn-ws server 会把本机的 nx 服务桥到公网,没有鉴权等于把整台机器挂在网上。${C.reset}`);
|
|
177
|
-
say('');
|
|
178
|
-
say(` ${C.cyan}--token <TOKEN>${C.reset} 与客户端配同一个静态门禁`);
|
|
179
|
-
say(` ${C.cyan}MN_WS_JWT_SECRET=<随机>${C.reset} 启用统一登录(推荐,可按用户隔离)`);
|
|
180
|
-
say(` ${C.cyan}--insecure${C.reset} 仅限本机自测,明知风险才用`);
|
|
181
|
-
process.exitCode = 1;
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (!hasGate && !wantAuth && cfg.insecure) {
|
|
185
|
-
say(`${C.yellow}! 已用 --insecure:无鉴权启动${C.reset}${C.dim} —— 任何人都能通过它调用本机服务${C.reset}`);
|
|
186
|
-
}
|
|
187
|
-
if (cfg.host === '0.0.0.0' && !hasGate && !wantAuth) {
|
|
188
|
-
say(`${C.red}✗ 无鉴权时不允许绑 0.0.0.0${C.reset}:请用 --host 127.0.0.1 自测`);
|
|
189
|
-
process.exitCode = 1;
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ── 端口预检:被占就明确报错,不静默顺延 ──
|
|
194
|
-
const free = await isPortFree(cfg.port, cfg.host === '0.0.0.0' ? '0.0.0.0' : cfg.host);
|
|
195
|
-
if (!free.free) {
|
|
196
|
-
say(`${C.red}✗ 端口 ${cfg.port} 已被占用${C.reset}${C.dim}(${free.error && free.error.code || ''})${C.reset}`);
|
|
197
|
-
say(`${C.dim}换端口:--port <n>。不静默顺延 —— 客户端还按 ${cfg.port} 连,顺延只会让两边对不上账。${C.reset}`);
|
|
198
|
-
process.exitCode = 1;
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const server = createServer({
|
|
203
|
-
host: cfg.host,
|
|
204
|
-
port: cfg.port,
|
|
205
|
-
wsPath: cfg.wsPath,
|
|
206
|
-
token: cfg.token,
|
|
207
|
-
issuer: cfg.issuer,
|
|
208
|
-
idpClientId: cfg.idpClientId,
|
|
209
|
-
redirectUri: cfg.redirectUri || `http://127.0.0.1:${cfg.port}/auth/callback`,
|
|
210
|
-
jwtSecret: cfg.jwtSecret,
|
|
211
|
-
authEnabled: wantAuth,
|
|
212
|
-
insecure: cfg.insecure,
|
|
213
|
-
requestTimeoutMs: cfg.requestTimeoutMs,
|
|
214
|
-
dataDir: path.join(configDir(), 'data'),
|
|
215
|
-
logLevel: cfg.logLevel,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const actual = await server.listen();
|
|
219
|
-
say(`${C.green}✓ mn-ws server 已启动${C.reset} http://${cfg.host}:${actual} ws=${cfg.wsPath}`);
|
|
220
|
-
say(`${C.dim} 鉴权:${wantAuth ? '统一登录(设备令牌)' : ''}${wantAuth && hasGate ? ' + ' : ''}${hasGate ? '静态门禁' : ''}${!wantAuth && !hasGate ? '无(--insecure)' : ''}${C.reset}`);
|
|
221
|
-
say(`${C.dim} 配置目录:${configDir()}${C.reset}`);
|
|
222
|
-
|
|
223
|
-
const shutdown = async (sig) => {
|
|
224
|
-
say(`\n${C.dim}收到 ${sig},正在关闭…${C.reset}`);
|
|
225
|
-
await server.close();
|
|
226
|
-
process.exit(0);
|
|
227
|
-
};
|
|
228
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
229
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ============================ client ============================
|
|
233
|
-
|
|
234
|
-
async function cmdClient(cfg, rest) {
|
|
235
|
-
const sub = (rest && rest[0]) || '';
|
|
236
|
-
|
|
237
|
-
if (sub === 'web') return cmdClientWeb(cfg);
|
|
238
|
-
if (sub === 'apps') return cmdClientApps(cfg);
|
|
239
|
-
if (sub === 'status') return cmdClientStatus(cfg);
|
|
240
|
-
if (sub === 'catalog') return cmdClientCatalog(cfg);
|
|
241
|
-
|
|
242
|
-
// ── 启动转发 ──
|
|
243
|
-
const apps = resolveApps(cfg);
|
|
244
|
-
if (!Object.keys(apps).length) {
|
|
245
|
-
say(`${C.yellow}没有指定可桥接的应用${C.reset}:一个都不桥。`);
|
|
246
|
-
say(`${C.dim}用 --apps 指定白名单,例如:npx mn-ws client --apps nx-kv,nx-rh${C.reset}`);
|
|
247
|
-
say(`${C.dim}先看看本机有哪些在跑:npx mn-ws client apps${C.reset}`);
|
|
248
|
-
process.exitCode = 1;
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const token = cfg.token || cfg.deviceToken || '';
|
|
253
|
-
if (!token) {
|
|
254
|
-
say(`${C.yellow}没有注册令牌${C.reset}:server 半会拒绝注册。`);
|
|
255
|
-
say(`${C.dim}先 npx mn-ws auth login(推荐),或用 --token 传静态门禁${C.reset}`);
|
|
256
|
-
process.exitCode = 1;
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const logger = new Logger({
|
|
261
|
-
level: cfg.logLevel,
|
|
262
|
-
dir: path.join(configDir(), 'data'),
|
|
263
|
-
name: 'mn-ws-client',
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const client = new WsBridgeClient({
|
|
267
|
-
url: cfg.url,
|
|
268
|
-
token,
|
|
269
|
-
bridgeId: cfg.clientId,
|
|
270
|
-
apps,
|
|
271
|
-
logger,
|
|
272
|
-
onStatus: (s) => {
|
|
273
|
-
if (s.state === 'registered') {
|
|
274
|
-
say(`${C.green}✓ 桥接已注册${C.reset} 应用:${(s.apps || []).join(', ')}`);
|
|
275
|
-
} else if (s.state === 'reconnecting') {
|
|
276
|
-
say(`${C.yellow}…${C.reset} ${s.reason || '重连中'}(${Math.round((s.retryInMs || 0) / 1000)}s 后重试)`);
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
client.start();
|
|
282
|
-
say(`${C.bold}mn-ws client${C.reset} → ${cfg.url}`);
|
|
283
|
-
say(`${C.dim} 可桥接:${Object.keys(apps).join(', ')}${C.reset}`);
|
|
284
|
-
say(`${C.dim} 日志目录:${configDir()}${C.reset}`);
|
|
285
|
-
say(`${C.dim} Ctrl+C 退出(退出即断开,鼠标的蓝牙主机位还给电脑)${C.reset}`);
|
|
286
|
-
|
|
287
|
-
process.on('SIGINT', () => {
|
|
288
|
-
client.stop();
|
|
289
|
-
logger.close();
|
|
290
|
-
process.exit(0);
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function cmdClientWeb(cfg) {
|
|
295
|
-
const apps = resolveApps(cfg);
|
|
296
|
-
const free = await isPortFree(cfg.webPort, cfg.webHost);
|
|
297
|
-
if (!free.free) {
|
|
298
|
-
say(`${C.red}✗ 面板端口 ${cfg.webPort} 已被占用${C.reset}${C.dim}(${free.error && free.error.code || ''})${C.reset}`);
|
|
299
|
-
say(`${C.dim}换端口:--web-port <n>${C.reset}`);
|
|
300
|
-
say(`${C.dim}同机同时跑 server 和面板时最容易撞 —— server 默认也占 7777,面板请显式换端口。${C.reset}`);
|
|
301
|
-
process.exitCode = 1;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const logger = new Logger({ level: cfg.logLevel, dir: path.join(configDir(), 'data'), name: 'mn-ws-web' });
|
|
305
|
-
const panel = await startWebPanel({
|
|
306
|
-
port: cfg.webPort,
|
|
307
|
-
host: cfg.webHost,
|
|
308
|
-
apps,
|
|
309
|
-
appOverrides: cfg.appOverrides,
|
|
310
|
-
logger,
|
|
311
|
-
});
|
|
312
|
-
say(`${C.green}✓ 本地面板已启动${C.reset} http://${cfg.webHost}:${panel.port}/`);
|
|
313
|
-
say(`${C.dim} 白名单:${Object.keys(apps).join(', ') || '(空 —— 用 --apps 指定)'}${C.reset}`);
|
|
314
|
-
say(`${C.dim} 面板只监听 ${cfg.webHost},不对外暴露${C.reset}`);
|
|
315
|
-
process.on('SIGINT', async () => {
|
|
316
|
-
await panel.close();
|
|
317
|
-
logger.close();
|
|
318
|
-
process.exit(0);
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function cmdClientApps(cfg) {
|
|
323
|
-
const configured = resolveApps(cfg);
|
|
324
|
-
const names = Object.keys(configured);
|
|
325
|
-
if (!names.length) {
|
|
326
|
-
say(`${C.yellow}当前没有白名单${C.reset}:用 --apps 指定要桥接的应用。`);
|
|
327
|
-
say(`${C.dim}本机已知的 nx 服务(端口来自各仓 core/paths.js):${C.reset}`);
|
|
328
|
-
for (const [app, info] of Object.entries(KNOWN_APPS)) {
|
|
329
|
-
say(` ${C.cyan}${app.padEnd(10)}${C.reset}${info.port} ${info.title}`);
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
const list = await scanApps({
|
|
334
|
-
host: '127.0.0.1',
|
|
335
|
-
apps: names,
|
|
336
|
-
overrides: cfg.appOverrides,
|
|
337
|
-
});
|
|
338
|
-
say(`${C.bold}可桥接的应用${C.reset}`);
|
|
339
|
-
for (const a of list) {
|
|
340
|
-
const mark = a.alive ? `${C.green}✓ 在监听${C.reset}` : `${C.yellow}✗ 未运行${C.reset}`;
|
|
341
|
-
say(` ${C.cyan}${a.app.padEnd(10)}${C.reset}${String(a.port).padEnd(7)}${mark} ${a.base} ${a.title}`);
|
|
342
|
-
}
|
|
343
|
-
const dead = list.filter((a) => !a.alive);
|
|
344
|
-
if (dead.length) {
|
|
345
|
-
say(`${C.dim}未运行的应用桥过去也会 502(bridge_offline)—— 先把对应服务起起来。${C.reset}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function cmdClientCatalog(cfg) {
|
|
350
|
-
const base = (cfg.serverHttp || '').replace(/\/+$/, '');
|
|
351
|
-
const token = cfg.token || cfg.deviceToken || '';
|
|
352
|
-
if (!base) {
|
|
353
|
-
say(`${C.red}缺少服务端地址${C.reset}:--url 或配置文件里给一个`);
|
|
354
|
-
process.exitCode = 1;
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
if (!token) {
|
|
358
|
-
say(`${C.red}缺少凭据${C.reset}:npx mn-ws auth login,或用 --token 传门禁`);
|
|
359
|
-
process.exitCode = 1;
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
let res;
|
|
363
|
-
try {
|
|
364
|
-
res = await getJson(`${base}/api/catalog`, { headers: { 'X-Mn-Ws-Token': token } });
|
|
365
|
-
} catch (e) {
|
|
366
|
-
say(`${C.red}✗ 连不上 ${base}${C.reset}:${e.message}`);
|
|
367
|
-
process.exitCode = 1;
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (!res.status || res.status >= 400 || !res.json || !res.json.ok) {
|
|
371
|
-
say(`${C.red}✗ 拉目录失败${C.reset}:${(res.json && res.json.error) || `HTTP ${res.status}`}`);
|
|
372
|
-
process.exitCode = 1;
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
const list = res.json.apps || [];
|
|
376
|
-
say(`${C.bold}元信息目录${C.reset}(${base}/api/catalog,共 ${list.length} 个可路由)`);
|
|
377
|
-
if (!list.length) {
|
|
378
|
-
say(`${C.dim}当前没有可路由的应用 —— 检查 client 侧的 --apps / --ports 白名单。${C.reset}`);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
for (const a of list) {
|
|
382
|
-
say(` ${C.cyan}${a.app.padEnd(10)}${C.reset}${String(a.port ?? '—').padEnd(7)}${a.title} ${C.dim}${a.bridgeId}${C.reset}`);
|
|
383
|
-
}
|
|
384
|
-
say('');
|
|
385
|
-
say(`${C.dim}调用方式(二选一):${C.reset}`);
|
|
386
|
-
say(`${C.dim} 路径前缀:curl -H 'X-Mn-Ws-Token: $TOKEN' '${base}/${list[0].app}/api/...${C.reset}`);
|
|
387
|
-
say(`${C.dim} 请求头: curl -H 'X-Mn-Ws-Token: $TOKEN' -H 'X-Nx-App: ${list[0].app}' '${base}/api/...${C.reset}`);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function cmdClientStatus(cfg) {
|
|
391
|
-
const raw = readConfigFile();
|
|
392
|
-
const view = withAccounts(raw, { defaultClientId: ensureClientId() });
|
|
393
|
-
const list = listAccounts(view);
|
|
394
|
-
say(`${C.bold}mn-ws 状态${C.reset}`);
|
|
395
|
-
say(` server ${cfg.url}`);
|
|
396
|
-
say(` 标识 ${cfg.clientId}`);
|
|
397
|
-
say(` 白名单 ${(cfg.apps || []).join(', ') || '(空)'}`);
|
|
398
|
-
if (!cfg.token && !cfg.deviceToken) {
|
|
399
|
-
say(` ${C.yellow}未登录:无注册令牌,server 半会拒绝注册${C.reset}`);
|
|
400
|
-
} else if (cfg.deviceToken) {
|
|
401
|
-
const days = cfg.tokenExpiresAt
|
|
402
|
-
? Math.round((cfg.tokenExpiresAt - Date.now()) / 86400000)
|
|
403
|
-
: null;
|
|
404
|
-
say(` 登录态 设备令牌${days !== null ? `(约 ${days} 天后过期)` : ''}`);
|
|
405
|
-
} else {
|
|
406
|
-
say(` 登录态 静态门禁 token`);
|
|
407
|
-
}
|
|
408
|
-
if (list.length) {
|
|
409
|
-
say(` 已存账号 ${list.length} 个(当前:${labelOf((view.accounts[view.activeAccountKey] || {}))})`);
|
|
410
|
-
}
|
|
411
|
-
say(`${C.dim} 配置目录:${configDir()}${C.reset}`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// ============================ auth ============================
|
|
415
|
-
|
|
416
|
-
async function cmdAuth(cfg, rest, flags = {}) {
|
|
417
|
-
const action = (rest && rest[0]) || 'status';
|
|
418
|
-
const target = rest && rest[1];
|
|
419
|
-
|
|
420
|
-
if (action === 'login') {
|
|
421
|
-
if (!cfg.serverHttp) {
|
|
422
|
-
say(`${C.red}缺少服务端地址${C.reset}:先用 --url ws://<host>:<port>/ws 连一次,或设 MN_WS_SERVER_HTTP`);
|
|
423
|
-
process.exitCode = 1;
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const badUrl = placeholderUrlReason(cfg.serverHttp) || placeholderUrlReason(cfg.url);
|
|
427
|
-
if (badUrl) {
|
|
428
|
-
say(`${C.red}✗ 服务端地址无效${C.reset}:${badUrl}`);
|
|
429
|
-
say(`${C.dim}带上真实地址重试:npx mn-ws auth login --url ws://<服务器>:7777/ws${C.reset}`);
|
|
430
|
-
process.exitCode = 1;
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
say(`${C.dim}服务端:${cfg.serverHttp}${C.reset}`);
|
|
434
|
-
|
|
435
|
-
const rawCfg = readConfigFile();
|
|
436
|
-
const view = withAccounts(rawCfg, { defaultClientId: ensureClientId() });
|
|
437
|
-
|
|
438
|
-
let res;
|
|
439
|
-
try {
|
|
440
|
-
res = await authLogin({
|
|
441
|
-
serverHttp: cfg.serverHttp,
|
|
442
|
-
clientId: cfg.clientId,
|
|
443
|
-
deviceName: cfg.deviceName,
|
|
444
|
-
noOpen: cfg.noOpen === true,
|
|
445
|
-
promptLogin: flags.newAccount === true,
|
|
446
|
-
onUrl: (url) => {
|
|
447
|
-
say('');
|
|
448
|
-
say(`${C.yellow}请在弹出的浏览器里完成统一登录${C.reset}(未自动弹出就手动打开下面地址)`);
|
|
449
|
-
if (flags.newAccount === true) {
|
|
450
|
-
say(`${C.dim}已带 prompt=login:认证中心会要求重新登录账号${C.reset}`);
|
|
451
|
-
} else if (listAccounts(view).length >= 1) {
|
|
452
|
-
say(`${C.dim}换个人登录:改用 npx mn-ws auth login --new-account${C.reset}`);
|
|
453
|
-
}
|
|
454
|
-
say(`${C.dim}${url}${C.reset}`);
|
|
455
|
-
say('');
|
|
456
|
-
say(`${C.dim}等待授权回调…(5 分钟超时)${C.reset}`);
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
} catch (e) {
|
|
460
|
-
say(`${C.red}✗ 登录失败${C.reset}:${e.message}`);
|
|
461
|
-
process.exitCode = 1;
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const accountEntry = {
|
|
466
|
-
userId: res.userId,
|
|
467
|
-
email: res.email,
|
|
468
|
-
username: res.username,
|
|
469
|
-
nickname: res.nickname,
|
|
470
|
-
deviceToken: res.token,
|
|
471
|
-
expiresAt: res.tokenExpiresAt,
|
|
472
|
-
url: cfg.url,
|
|
473
|
-
serverHttp: cfg.serverHttp,
|
|
474
|
-
deviceName: res.deviceName || cfg.deviceName,
|
|
475
|
-
clientId: res.clientId || cfg.clientId,
|
|
476
|
-
loginAt: Date.now(),
|
|
477
|
-
};
|
|
478
|
-
const sameAsBefore = Boolean(
|
|
479
|
-
view.activeAccountKey && view.activeAccountKey === entryKeyOf(accountEntry));
|
|
480
|
-
const saved = saveAuthAccount({ entry: accountEntry, setActive: true });
|
|
481
|
-
const storedCount = listAccounts(withAccounts(readConfigFile(), {})).length;
|
|
482
|
-
|
|
483
|
-
say(`${C.green}✓ 登录成功${C.reset}:${labelOf(accountEntry)}`);
|
|
484
|
-
if (accountEntry.email) say(` 邮箱 ${accountEntry.email}`);
|
|
485
|
-
say(` 本机标识 ${accountEntry.clientId} 设备名 ${accountEntry.deviceName}`);
|
|
486
|
-
if (res.tokenExpiresAt) {
|
|
487
|
-
const days = Math.round((res.tokenExpiresAt - Date.now()) / 86400000);
|
|
488
|
-
say(`${C.dim}设备令牌有效期约 ${days} 天,过期后重新执行 npx mn-ws auth login${C.reset}`);
|
|
489
|
-
}
|
|
490
|
-
say(`${C.dim}已保存 → ${saved.file}${C.reset}`);
|
|
491
|
-
if (sameAsBefore) {
|
|
492
|
-
say(`${C.yellow}! 认证中心那边仍登录着同一个账号${C.reset}`
|
|
493
|
-
+ `${C.dim}(浏览器里有会话,授权是自动过的)。本意是换个人登录的话:先在浏览器里退出该账号${C.reset}`);
|
|
494
|
-
}
|
|
495
|
-
if (storedCount >= 2) {
|
|
496
|
-
say(`${C.dim}本机已存 ${storedCount} 个账号:npx mn-ws auth list 查看,npx mn-ws auth switch <账号> 切换${C.reset}`);
|
|
497
|
-
}
|
|
498
|
-
say(`${C.green}现在直接 npx mn-ws client --apps <应用> 即可启动${C.reset}`);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (action === 'list') {
|
|
503
|
-
const view = withAccounts(readConfigFile(), { defaultClientId: ensureClientId() });
|
|
504
|
-
const list = listAccounts(view);
|
|
505
|
-
if (!list.length) {
|
|
506
|
-
say(`${C.yellow}本机还没有已存账号${C.reset}:先执行 npx mn-ws auth login`);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
say(`${C.bold}本机已存账号${C.reset}(共 ${list.length} 个)`);
|
|
510
|
-
list.forEach((a, i) => printAccountLine(i + 1, a, a.key === view.activeAccountKey));
|
|
511
|
-
say(`${C.dim}切换:npx mn-ws auth switch <序号|邮箱|用户名|昵称>${C.reset}`);
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (action === 'switch') {
|
|
516
|
-
const view = withAccounts(readConfigFile(), { defaultClientId: ensureClientId() });
|
|
517
|
-
const list = listAccounts(view);
|
|
518
|
-
if (!list.length) {
|
|
519
|
-
say(`${C.yellow}本机还没有已存账号${C.reset}:先执行 npx mn-ws auth login`);
|
|
520
|
-
process.exitCode = 1;
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
if (!target) {
|
|
524
|
-
say(`${C.yellow}未指定要切换到的账号${C.reset}。本机已存:`);
|
|
525
|
-
list.forEach((a, i) => printAccountLine(i + 1, a, a.key === view.activeAccountKey));
|
|
526
|
-
say(`${C.dim}用法:npx mn-ws auth switch <序号|邮箱|用户名|昵称>${C.reset}`);
|
|
527
|
-
process.exitCode = 1;
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
let r;
|
|
531
|
-
try {
|
|
532
|
-
r = switchActiveAccount(target, { defaultClientId: ensureClientId() });
|
|
533
|
-
} catch (e) {
|
|
534
|
-
say(`${C.red}✗ ${e.message}${C.reset}`);
|
|
535
|
-
process.exitCode = 1;
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (!r.changed) {
|
|
539
|
-
say(`${C.dim}当前已经就是这个账号${C.reset}:${labelOf(r.entry)}`);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
say(`${C.green}✓ 已切换到${C.reset}:${labelOf(r.entry)}`);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (action === 'status') {
|
|
547
|
-
const raw = readConfigFile();
|
|
548
|
-
const view = withAccounts(raw, { defaultClientId: ensureClientId() });
|
|
549
|
-
const cur = view.accounts[view.activeAccountKey];
|
|
550
|
-
if (!cur) {
|
|
551
|
-
say(`${C.yellow}未登录${C.reset}:执行 npx mn-ws auth login`);
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
say(`${C.bold}当前身份${C.reset}:${labelOf(cur)}`);
|
|
555
|
-
if (cur.email) say(` 邮箱 ${cur.email}`);
|
|
556
|
-
say(` 标识 ${cur.clientId || '(无)'}`);
|
|
557
|
-
if (cur.expiresAt) {
|
|
558
|
-
const days = Math.round((cur.expiresAt - Date.now()) / 86400000);
|
|
559
|
-
say(` 令牌 ${days >= 0 ? `约 ${days} 天后过期` : '已过期'}`);
|
|
560
|
-
}
|
|
561
|
-
say(` server ${cur.url || '(无)'}`);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (action === 'logout') {
|
|
566
|
-
const all = flags.all === true;
|
|
567
|
-
const r = clearAuthCredentials({ all });
|
|
568
|
-
say(`${C.green}✓ 已清除${C.reset}${all ? '本机全部账号' : '当前账号的登录凭据'}`);
|
|
569
|
-
if (r.remaining) say(`${C.dim}本机还剩 ${r.remaining} 个账号${C.reset}`);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
say(`${C.yellow}未知 auth 子命令:${action}${C.reset}`);
|
|
574
|
-
say(`${C.dim}可用:login | list | switch | status | logout${C.reset}`);
|
|
575
|
-
process.exitCode = 1;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function printAccountLine(i, a, active) {
|
|
579
|
-
const e = a.entry || {};
|
|
580
|
-
const mark = active ? `${C.green}★${C.reset}` : ' ';
|
|
581
|
-
say(` ${mark} ${i}. ${labelOf(e)}${e.email && e.email !== labelOf(e) ? ` ${C.dim}${e.email}${C.reset}` : ''}`);
|
|
582
|
-
say(` ${C.dim}clientId ${e.clientId || '(无)'}${C.reset}`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// ============================ doctor ============================
|
|
586
|
-
|
|
587
|
-
async function cmdDoctor(cfg) {
|
|
588
|
-
const logger = new Logger({ level: 'error' });
|
|
589
|
-
let problems = 0;
|
|
590
|
-
|
|
591
|
-
say(`${C.bold}mn-ws doctor${C.reset} ${C.dim}v${pkg.version}${C.reset}`);
|
|
592
|
-
say(`${C.dim}node ${process.version} · ${process.platform}-${process.arch}${C.reset}`);
|
|
593
|
-
say('');
|
|
594
|
-
|
|
595
|
-
// ① 端口占用预检
|
|
596
|
-
const serverPort = await isPortFree(cfg.port, '0.0.0.0');
|
|
597
|
-
const webPort = await isPortFree(cfg.webPort, '127.0.0.1');
|
|
598
|
-
say(`${C.bold}端口${C.reset}`);
|
|
599
|
-
say(` server :${cfg.port} ${serverPort.free ? `${C.green}空闲${C.reset}` : `${C.yellow}已被占用${C.reset}`}`);
|
|
600
|
-
say(` 面板 :${cfg.webPort} ${webPort.free ? `${C.green}空闲${C.reset}` : `${C.yellow}已被占用${C.reset}`}`);
|
|
601
|
-
|
|
602
|
-
// ② 鉴权
|
|
603
|
-
say('');
|
|
604
|
-
say(`${C.bold}鉴权${C.reset}`);
|
|
605
|
-
const hasGate = !!cfg.token;
|
|
606
|
-
const hasJwt = !!cfg.jwtSecret;
|
|
607
|
-
if (hasJwt) say(` ${C.green}✓${C.reset} 统一登录(设备令牌,按用户隔离)`);
|
|
608
|
-
else say(` ${C.yellow}!${C.reset} 未配置统一登录密钥(MN_WS_JWT_SECRET)`);
|
|
609
|
-
if (hasGate) say(` ${C.green}✓${C.reset} 静态门禁 token(共享密钥,等价全量访问)`);
|
|
610
|
-
else say(` ${C.yellow}!${C.reset} 未配置静态门禁(MN_WS_TOKEN)`);
|
|
611
|
-
if (!hasGate && !hasJwt) {
|
|
612
|
-
problems += 1;
|
|
613
|
-
say(` ${C.red}✗ 没有任何鉴权${C.reset} —— server 侧会拒绝启动(除非 --insecure)`);
|
|
614
|
-
}
|
|
615
|
-
if (cfg.host === '0.0.0.0' && !hasGate && !hasJwt) {
|
|
616
|
-
problems += 1;
|
|
617
|
-
say(` ${C.red}✗ 绑 0.0.0.0 且无鉴权${C.reset} —— 绝不能这样上公网`);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ③ 白名单与端口存活
|
|
621
|
-
say('');
|
|
622
|
-
say(`${C.bold}可桥接的应用${C.reset}`);
|
|
623
|
-
const configured = resolveApps(cfg);
|
|
624
|
-
const names = Object.keys(configured);
|
|
625
|
-
if (!names.length) {
|
|
626
|
-
say(` ${C.yellow}!${C.reset} 白名单为空 —— 一个都不桥(用 --apps 指定)`);
|
|
627
|
-
} else {
|
|
628
|
-
const list = await scanApps({ host: '127.0.0.1', apps: names, overrides: cfg.appOverrides });
|
|
629
|
-
for (const a of list) {
|
|
630
|
-
const mark = a.alive ? `${C.green}✓ 在监听${C.reset}` : `${C.yellow}✗ 未运行${C.reset}`;
|
|
631
|
-
say(` ${C.cyan}${a.app.padEnd(10)}${C.reset}${String(a.port).padEnd(7)}${mark} ${a.base}`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// ④ server 连通性
|
|
636
|
-
say('');
|
|
637
|
-
say(`${C.bold}server 连通性${C.reset}`);
|
|
638
|
-
const httpBase = cfg.serverHttp || cfg.url.replace(/^ws/, 'http').replace(/\/ws\/?$/, '');
|
|
639
|
-
try {
|
|
640
|
-
const r = await getJson(`${httpBase.replace(/\/+$/, '')}/health`);
|
|
641
|
-
if (r.status === 200 && r.json && r.json.ok) {
|
|
642
|
-
say(` ${C.green}✓${C.reset} ${httpBase}/health → ${r.json.service} · 桥接数 ${r.json.bridges}`);
|
|
643
|
-
} else {
|
|
644
|
-
say(` ${C.yellow}!${C.reset} ${httpBase}/health 返回 HTTP ${r.status}`);
|
|
645
|
-
}
|
|
646
|
-
} catch (e) {
|
|
647
|
-
say(` ${C.yellow}!${C.reset} 连不上 ${httpBase}/health(${e.message})`);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// ⑤ 登录态
|
|
651
|
-
say('');
|
|
652
|
-
say(`${C.bold}本机登录态${C.reset}`);
|
|
653
|
-
const raw = readConfigFile();
|
|
654
|
-
const view = withAccounts(raw, { defaultClientId: ensureClientId() });
|
|
655
|
-
const cur = view.accounts[view.activeAccountKey];
|
|
656
|
-
if (cur && cur.deviceToken) {
|
|
657
|
-
const days = cur.expiresAt ? Math.round((cur.expiresAt - Date.now()) / 86400000) : null;
|
|
658
|
-
say(` ${C.green}✓${C.reset} ${labelOf(cur)}${days !== null ? ` · 令牌约 ${days} 天后过期` : ''}`);
|
|
659
|
-
} else if (cfg.token) {
|
|
660
|
-
say(` ${C.green}✓${C.reset} 使用静态门禁 token`);
|
|
661
|
-
} else {
|
|
662
|
-
say(` ${C.yellow}!${C.reset} 未登录(npx mn-ws auth login)`);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
say('');
|
|
666
|
-
if (problems) {
|
|
667
|
-
say(`${C.red}✗ ${problems} 项需要处理${C.reset}`);
|
|
668
|
-
process.exitCode = 1;
|
|
669
|
-
} else {
|
|
670
|
-
say(`${C.green}✓ 自检通过${C.reset}`);
|
|
671
|
-
}
|
|
672
|
-
logger.close();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ============================ main ============================
|
|
676
|
-
|
|
677
|
-
async function main() {
|
|
678
|
-
const argv = process.argv.slice(2);
|
|
679
|
-
const { flags, positional } = parseArgs(argv);
|
|
680
|
-
const command = positional[0];
|
|
681
|
-
const rest = positional.slice(1);
|
|
682
|
-
|
|
683
|
-
if (flags.version || command === 'version') return printVersion();
|
|
684
|
-
if (flags.help && !command) return printHelp();
|
|
685
|
-
if (!command) return printHelp();
|
|
686
|
-
|
|
687
|
-
const cfg = loadConfig(flags);
|
|
688
|
-
|
|
689
|
-
switch (command) {
|
|
690
|
-
case 'server':
|
|
691
|
-
return cmdServer(cfg);
|
|
692
|
-
case 'client':
|
|
693
|
-
return cmdClient(cfg, rest);
|
|
694
|
-
case 'web':
|
|
695
|
-
return cmdClientWeb(cfg);
|
|
696
|
-
case 'auth':
|
|
697
|
-
return cmdAuth(cfg, rest, flags);
|
|
698
|
-
case 'doctor':
|
|
699
|
-
return cmdDoctor(cfg);
|
|
700
|
-
case 'help':
|
|
701
|
-
return printHelp();
|
|
702
|
-
default:
|
|
703
|
-
say(`${C.red}未知命令:${command}${C.reset}\n`);
|
|
704
|
-
printHelp();
|
|
705
|
-
process.exitCode = 1;
|
|
706
|
-
return undefined;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
main().catch((e) => {
|
|
711
|
-
process.stderr.write(`${C.red}致命错误:${e.message}${C.reset}\n`);
|
|
712
|
-
if (process.env.MN_WS_DEBUG) process.stderr.write(`${e.stack}\n`);
|
|
713
|
-
process.exit(1);
|
|
714
|
-
});
|