mn-ws 0.1.0 → 0.2.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.
Files changed (178) hide show
  1. package/assets/groups.json +10 -0
  2. package/assets/mn-ws/SKILL.md +55 -0
  3. package/assets/mn-ws/hooks.json +14 -0
  4. package/assets/mn-ws/install/inject-root.mjs +26 -0
  5. package/bin/ws.mjs +13 -0
  6. package/dist/core/auth.d.ts +194 -0
  7. package/dist/core/auth.d.ts.map +1 -0
  8. package/dist/core/auth.js +331 -0
  9. package/dist/core/auth.js.map +1 -0
  10. package/dist/core/brand.d.ts +11 -0
  11. package/dist/core/brand.d.ts.map +1 -0
  12. package/dist/core/brand.js +7 -0
  13. package/dist/core/brand.js.map +1 -0
  14. package/dist/core/bridge.d.ts +106 -0
  15. package/dist/core/bridge.d.ts.map +1 -0
  16. package/dist/core/bridge.js +308 -0
  17. package/dist/core/bridge.js.map +1 -0
  18. package/dist/core/config.d.ts +174 -0
  19. package/dist/core/config.d.ts.map +1 -0
  20. package/dist/core/config.js +417 -0
  21. package/dist/core/config.js.map +1 -0
  22. package/dist/core/controller.d.ts +64 -0
  23. package/dist/core/controller.d.ts.map +1 -0
  24. package/dist/core/controller.js +100 -0
  25. package/dist/core/controller.js.map +1 -0
  26. package/dist/core/errors/index.d.ts +30 -0
  27. package/dist/core/errors/index.d.ts.map +1 -0
  28. package/dist/core/errors/index.js +88 -0
  29. package/dist/core/errors/index.js.map +1 -0
  30. package/dist/core/open.d.ts +3 -0
  31. package/dist/core/open.d.ts.map +1 -0
  32. package/dist/core/open.js +27 -0
  33. package/dist/core/open.js.map +1 -0
  34. package/dist/core/paths.d.ts +15 -0
  35. package/dist/core/paths.d.ts.map +1 -0
  36. package/dist/core/paths.js +50 -0
  37. package/dist/core/paths.js.map +1 -0
  38. package/dist/core/serve-port.d.ts +73 -0
  39. package/dist/core/serve-port.d.ts.map +1 -0
  40. package/dist/core/serve-port.js +134 -0
  41. package/dist/core/serve-port.js.map +1 -0
  42. package/dist/core/session.d.ts +78 -0
  43. package/dist/core/session.d.ts.map +1 -0
  44. package/dist/core/session.js +124 -0
  45. package/dist/core/session.js.map +1 -0
  46. package/dist/core/skills/bus.d.ts +29 -0
  47. package/dist/core/skills/bus.d.ts.map +1 -0
  48. package/dist/core/skills/bus.js +114 -0
  49. package/dist/core/skills/bus.js.map +1 -0
  50. package/dist/core/skills/get.d.ts +17 -0
  51. package/dist/core/skills/get.d.ts.map +1 -0
  52. package/dist/core/skills/get.js +87 -0
  53. package/dist/core/skills/get.js.map +1 -0
  54. package/dist/core/skills/handlers.d.ts +22 -0
  55. package/dist/core/skills/handlers.d.ts.map +1 -0
  56. package/dist/core/skills/handlers.js +86 -0
  57. package/dist/core/skills/handlers.js.map +1 -0
  58. package/dist/core/skills/index.d.ts +47 -0
  59. package/dist/core/skills/index.d.ts.map +1 -0
  60. package/dist/core/skills/index.js +268 -0
  61. package/dist/core/skills/index.js.map +1 -0
  62. package/dist/core/skills/install.d.ts +18 -0
  63. package/dist/core/skills/install.d.ts.map +1 -0
  64. package/dist/core/skills/install.js +123 -0
  65. package/dist/core/skills/install.js.map +1 -0
  66. package/dist/core/skills/text-util.d.ts +2 -0
  67. package/dist/core/skills/text-util.d.ts.map +1 -0
  68. package/dist/core/skills/text-util.js +14 -0
  69. package/dist/core/skills/text-util.js.map +1 -0
  70. package/dist/core/store.d.ts +29 -0
  71. package/dist/core/store.d.ts.map +1 -0
  72. package/dist/core/store.js +100 -0
  73. package/dist/core/store.js.map +1 -0
  74. package/dist/core/views.d.ts +11 -0
  75. package/dist/core/views.d.ts.map +1 -0
  76. package/dist/core/views.js +20 -0
  77. package/dist/core/views.js.map +1 -0
  78. package/dist/index.d.ts +6 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +8 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/modules/auth/index.d.ts +73 -0
  83. package/dist/modules/auth/index.d.ts.map +1 -0
  84. package/dist/modules/auth/index.js +64 -0
  85. package/dist/modules/auth/index.js.map +1 -0
  86. package/dist/modules/auth/service.d.ts +98 -0
  87. package/dist/modules/auth/service.d.ts.map +1 -0
  88. package/dist/modules/auth/service.js +200 -0
  89. package/dist/modules/auth/service.js.map +1 -0
  90. package/dist/modules/bridge/index.d.ts +73 -0
  91. package/dist/modules/bridge/index.d.ts.map +1 -0
  92. package/dist/modules/bridge/index.js +89 -0
  93. package/dist/modules/bridge/index.js.map +1 -0
  94. package/dist/modules/bridge/service.d.ts +75 -0
  95. package/dist/modules/bridge/service.d.ts.map +1 -0
  96. package/dist/modules/bridge/service.js +134 -0
  97. package/dist/modules/bridge/service.js.map +1 -0
  98. package/dist/modules/home/index.d.ts +95 -0
  99. package/dist/modules/home/index.d.ts.map +1 -0
  100. package/dist/modules/home/index.js +123 -0
  101. package/dist/modules/home/index.js.map +1 -0
  102. package/dist/modules/home/service.d.ts +48 -0
  103. package/dist/modules/home/service.d.ts.map +1 -0
  104. package/dist/modules/home/service.js +195 -0
  105. package/dist/modules/home/service.js.map +1 -0
  106. package/dist/modules/skill/index.d.ts +78 -0
  107. package/dist/modules/skill/index.d.ts.map +1 -0
  108. package/dist/modules/skill/index.js +132 -0
  109. package/dist/modules/skill/index.js.map +1 -0
  110. package/dist/runtime/api.d.ts +24 -0
  111. package/dist/runtime/api.d.ts.map +1 -0
  112. package/dist/runtime/api.js +140 -0
  113. package/dist/runtime/api.js.map +1 -0
  114. package/dist/runtime/cli.d.ts +23 -0
  115. package/dist/runtime/cli.d.ts.map +1 -0
  116. package/dist/runtime/cli.js +410 -0
  117. package/dist/runtime/cli.js.map +1 -0
  118. package/dist/runtime/help.d.ts +29 -0
  119. package/dist/runtime/help.d.ts.map +1 -0
  120. package/dist/runtime/help.js +89 -0
  121. package/dist/runtime/help.js.map +1 -0
  122. package/dist/runtime/registry.d.ts +318 -0
  123. package/dist/runtime/registry.d.ts.map +1 -0
  124. package/dist/runtime/registry.js +75 -0
  125. package/dist/runtime/registry.js.map +1 -0
  126. package/dist/runtime/server.d.ts +10 -0
  127. package/dist/runtime/server.d.ts.map +1 -0
  128. package/dist/runtime/server.js +127 -0
  129. package/dist/runtime/server.js.map +1 -0
  130. package/dist/runtime/spec.d.ts +32 -0
  131. package/dist/runtime/spec.d.ts.map +1 -0
  132. package/dist/runtime/spec.js +180 -0
  133. package/dist/runtime/spec.js.map +1 -0
  134. package/dist/runtime/types/action.d.ts +101 -0
  135. package/dist/runtime/types/action.d.ts.map +1 -0
  136. package/dist/runtime/types/action.js +19 -0
  137. package/dist/runtime/types/action.js.map +1 -0
  138. package/dist/runtime/types/client.d.ts +39 -0
  139. package/dist/runtime/types/client.d.ts.map +1 -0
  140. package/dist/runtime/types/client.js +2 -0
  141. package/dist/runtime/types/client.js.map +1 -0
  142. package/dist/runtime/types/http.d.ts +22 -0
  143. package/dist/runtime/types/http.d.ts.map +1 -0
  144. package/dist/runtime/types/http.js +6 -0
  145. package/dist/runtime/types/http.js.map +1 -0
  146. package/dist/runtime/types/module.d.ts +72 -0
  147. package/dist/runtime/types/module.d.ts.map +1 -0
  148. package/dist/runtime/types/module.js +15 -0
  149. package/dist/runtime/types/module.js.map +1 -0
  150. package/dist/web/public/assets/CliHints-BOxU-Duc.js +1 -0
  151. package/dist/web/public/assets/index-BTRKdMIW.js +50 -0
  152. package/dist/web/public/assets/index-BjGe8ggI.css +1 -0
  153. package/dist/web/public/assets/view-CE4tnCuy.js +1 -0
  154. package/dist/web/public/assets/view-CEQ2uPxE.js +1 -0
  155. package/dist/web/public/favicon-16.png +0 -0
  156. package/dist/web/public/favicon-32.png +0 -0
  157. package/dist/web/public/favicon-48.png +0 -0
  158. package/dist/web/public/favicon.ico +0 -0
  159. package/dist/web/public/index.html +19 -0
  160. package/dist/web/public/logo-rounded.png +0 -0
  161. package/dist/web/public/logo-rounded.svg +6 -0
  162. package/dist/web/public/logo.png +0 -0
  163. package/dist/web/public/logo.svg +6 -0
  164. package/package.json +52 -31
  165. package/README.md +0 -251
  166. package/bin/index.js +0 -714
  167. package/client/bridge-client.js +0 -160
  168. package/client/web.js +0 -234
  169. package/client/ws-client.js +0 -191
  170. package/lib/apps.js +0 -237
  171. package/lib/auth.js +0 -319
  172. package/lib/config.js +0 -534
  173. package/lib/index.js +0 -15
  174. package/lib/logger.js +0 -63
  175. package/lib/protocol.js +0 -208
  176. package/lib/store.js +0 -66
  177. package/server/auth.js +0 -274
  178. package/server/server.js +0 -752
package/lib/config.js DELETED
@@ -1,534 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * 配置解析与账号存储。
5
- *
6
- * 优先级:**默认值 < 配置文件 < 环境变量 < 命令行参数**(与 mn-rt 同一套)。
7
- *
8
- * 配置文件位置:~/.mn-ws/config.json(环境变量 MN_WS_HOME 可覆盖)。
9
- *
10
- * 账号模型与 mn-rt 对齐:一个账号 = 一条完整登录态(含它自己的 clientId),
11
- * 多个账号并存,用 `auth switch` 切当前生效的那个;`auth logout` 默认只清
12
- * 当前账号(--all 清空全部)。这样多人共用一台机器时各登各的。
13
- */
14
-
15
- const fs = require('node:fs');
16
- const os = require('node:os');
17
- const path = require('node:path');
18
-
19
- const { Logger } = require('./logger');
20
- const { KNOWN_APPS } = require('./apps');
21
-
22
- /**
23
- * 内置默认地址。
24
- *
25
- * 指向真实部署,与 mn-rt 同机不同端口(mn-rt 占 8790,mn-ws 用 7777)。
26
- *
27
- * 为什么是 7777:7777 在 nx 系列里是「对外那台机器的入口端口」的语义位,
28
- * 而 mn-rt 的 8790 已经被 remoteserver 占着;给 mn-ws 一个独立且好记的号,
29
- * 免得两套中继的端口在文档、docker、Actions secrets 里混成一锅。
30
- */
31
- const DEFAULT_SERVE_URL = 'ws://1.94.101.189:7777/ws';
32
- const DEFAULT_SERVER_HTTP = 'http://1.94.101.189:7777';
33
- const DEFAULT_SERVER_PORT = 7777;
34
-
35
- /**
36
- * 本地面板默认端口。
37
- *
38
- * 也用 7777 —— 面板只在 client 半那台机器上跑,而 client 半连的是**远端**
39
- * server(1.94.101.189:7777),本机不会同时有 7777 的 server 在监听,
40
- * 所以默认不冲突。真在同一台机上既起 server 又开面板时,
41
- * 端口预检会明确报错让人换 --web-port,不静默顺延。
42
- */
43
- const DEFAULT_WEB_PORT = 7777;
44
-
45
- /** 脚手架时代的占位域名 —— 遇到就当场说清,不拿一句 DNS 错糊用户。 */
46
- const PLACEHOLDER_HOSTS = ['mn-ws.example.com'];
47
-
48
- function configDir() {
49
- return process.env.MN_WS_HOME || path.join(os.homedir(), '.mn-ws');
50
- }
51
-
52
- function configFile() {
53
- return path.join(configDir(), 'config.json');
54
- }
55
-
56
- function readConfigFile() {
57
- try {
58
- const f = configFile();
59
- if (!fs.existsSync(f)) return {};
60
- return JSON.parse(fs.readFileSync(f, 'utf8'));
61
- } catch {
62
- return {};
63
- }
64
- }
65
-
66
- /**
67
- * 原子写整份配置文件:同目录 tmp(0600)→ fsync → rename。
68
- *
69
- * 必须整份替换而不是就地改:写入方不止一个(serve 会在注册成功后回写配置,
70
- * 用户随时可能在另一个终端跑 auth login),半截文件被下一次读当成空配置,
71
- * 等于把凭据静默清空。
72
- */
73
- function writeConfigFile(nextRaw) {
74
- const f = configFile();
75
- fs.mkdirSync(path.dirname(f), { recursive: true });
76
- const tmp = `${f}.${process.pid}.tmp`;
77
- let fd = null;
78
- try {
79
- fd = fs.openSync(tmp, 'w', 0o600);
80
- fs.writeFileSync(fd, `${JSON.stringify(nextRaw, null, 2)}\n`, 'utf8');
81
- fs.fsyncSync(fd);
82
- fs.closeSync(fd);
83
- fd = null;
84
- renameWithRetry(tmp, f);
85
- } catch (e) {
86
- if (fd !== null) { try { fs.closeSync(fd); } catch { /* ignore */ } }
87
- try { fs.unlinkSync(tmp); } catch { /* ignore */ }
88
- throw e;
89
- }
90
- if (process.platform !== 'win32') {
91
- try { fs.chmodSync(f, 0o600); } catch { /* ignore */ }
92
- }
93
- return { file: f };
94
- }
95
-
96
- /** rename 在 Windows 上可能被扫描器占住(EPERM/EBUSY),带几次短重试。 */
97
- function renameWithRetry(from, to) {
98
- let lastErr = null;
99
- for (let i = 0; i < 4; i += 1) {
100
- try {
101
- fs.renameSync(from, to);
102
- return;
103
- } catch (e) {
104
- lastErr = e;
105
- if (e.code !== 'EPERM' && e.code !== 'EBUSY' && e.code !== 'EACCES') throw e;
106
- const until = Date.now() + 20;
107
- while (Date.now() < until) { /* spin */ }
108
- }
109
- }
110
- throw lastErr;
111
- }
112
-
113
- /**
114
- * 解析命令行:--key value / --flag / 位置参数。
115
- *
116
- * 与 mn-rt 的 parseArgs 同语义:`--no-open` 记为 noOpen=false,
117
- * `--flag` 记为 true,未给值记为 true。
118
- */
119
- function parseArgs(argv) {
120
- const flags = {};
121
- const positional = [];
122
- for (let i = 0; i < argv.length; i += 1) {
123
- const a = argv[i];
124
- if (a === '--') {
125
- positional.push(...argv.slice(i + 1));
126
- break;
127
- }
128
- if (a.startsWith('--')) {
129
- const eq = a.indexOf('=');
130
- let key;
131
- let value;
132
- if (eq >= 0) {
133
- key = a.slice(2, eq);
134
- value = a.slice(eq + 1);
135
- } else {
136
- key = a.slice(2);
137
- const next = argv[i + 1];
138
- if (next !== undefined && !next.startsWith('--')) {
139
- value = next;
140
- i += 1;
141
- } else {
142
- value = true;
143
- }
144
- }
145
- if (key.startsWith('no-')) {
146
- flags[camel(key.slice(3))] = false;
147
- } else {
148
- flags[camel(key)] = value;
149
- }
150
- } else {
151
- positional.push(a);
152
- }
153
- }
154
- return { flags, positional };
155
- }
156
-
157
- function camel(s) {
158
- return s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
159
- }
160
-
161
- /** 地址里的占位域名 → 一句人话原因。 */
162
- function placeholderUrlReason(url) {
163
- if (!url) return null;
164
- const hit = PLACEHOLDER_HOSTS.find((h) => String(url).includes(h));
165
- return hit ? `地址里还带着脚手架占位域名 ${hit}` : null;
166
- }
167
-
168
- /** 从 ws://h:p/ws 推导 http://h:p。 */
169
- function httpBaseFromWs(wsUrl) {
170
- try {
171
- const u = new URL(String(wsUrl));
172
- const proto = u.protocol === 'wss:' ? 'https:' : 'http:';
173
- const port = u.port ? `:${u.port}` : '';
174
- return `${proto}//${u.hostname}${port}`;
175
- } catch {
176
- return '';
177
- }
178
- }
179
-
180
- /**
181
- * 合并出最终配置。
182
- *
183
- * 环境变量前缀 MN_WS_(REMOTETYPE_ 那种大写下划线风格)。
184
- */
185
- function loadConfig(flags = {}) {
186
- const file = readConfigFile();
187
- const env = process.env;
188
-
189
- const cfg = {
190
- // ---- 连接 ----
191
- url: pick(flags.url, env.MN_WS_URL, file.url, DEFAULT_SERVE_URL),
192
- serverHttp: pick(flags.serverHttp, env.MN_WS_SERVER_HTTP, file.serverHttp, ''),
193
- token: pick(flags.token, env.MN_WS_TOKEN, file.token, ''),
194
- clientId: pick(flags.clientId, env.MN_WS_CLIENT_ID, file.clientId, ''),
195
- deviceName: pick(flags.deviceName, env.MN_WS_DEVICE_NAME, file.deviceName, os.hostname()),
196
- account: pick(flags.account, env.MN_WS_ACCOUNT, file.account, ''),
197
-
198
- // ---- 服务端 ----
199
- host: pick(flags.host, env.MN_WS_HOST, file.host, '0.0.0.0'),
200
- port: Number(pick(flags.port, env.MN_WS_PORT, file.port, DEFAULT_SERVER_PORT)),
201
- wsPath: pick(flags.wsPath, env.MN_WS_WS_PATH, file.wsPath, '/ws'),
202
- insecure: flags.insecure === true || env.MN_WS_INSECURE === '1',
203
- jwtSecret: pick(flags.jwtSecret, env.MN_WS_JWT_SECRET, file.jwtSecret, ''),
204
- // 显式开关:undefined = 没表态(跟随「配没配 JWT secret」),
205
- // true/false = 部署方明确说了要开/要关。解析非法值直接报错,不猜。
206
- authEnabled: parseBoolFlag(pick(flags.authEnabled, env.MN_WS_AUTH_ENABLED, file.authEnabled, '')),
207
- issuer: pick(flags.issuer, env.MN_WS_ISSUER, file.issuer, 'http://47.110.80.47:8082'),
208
- idpClientId: pick(flags.idpClientId, env.MN_WS_IDP_CLIENT_ID, file.idpClientId, 'mn-ws'),
209
- redirectUri: pick(flags.redirectUri, env.MN_WS_REDIRECT_URI, file.redirectUri, ''),
210
-
211
- // ---- 客户端 ----
212
- apps: parseApps(pick(flags.apps, env.MN_WS_APPS, file.apps, '')),
213
- ports: parsePorts(pick(flags.ports, env.MN_WS_PORTS, file.ports, '')),
214
- appOverrides: parseAppMap(pick(flags.appMap, env.MN_WS_APP_MAP, '', '')),
215
- webPort: Number(pick(flags.webPort, env.MN_WS_WEB_PORT, file.webPort, DEFAULT_WEB_PORT)),
216
- webHost: pick(flags.webHost, env.MN_WS_WEB_HOST, file.webHost, '127.0.0.1'),
217
-
218
- // ---- 杂项 ----
219
- noOpen: flags.noOpen === true || env.MN_WS_NO_OPEN === '1',
220
- logLevel: pick(flags.logLevel, env.MN_WS_LOG_LEVEL, file.logLevel, 'info'),
221
- requestTimeoutMs: Number(pick(flags.requestTimeout, env.MN_WS_REQUEST_TIMEOUT, file.requestTimeoutMs, 30000)),
222
- };
223
-
224
- // serverHttp 缺省从 url 推导(ws://h:p/ws → http://h:p)
225
- if (!cfg.serverHttp) cfg.serverHttp = httpBaseFromWs(cfg.url);
226
-
227
- // clientId 缺省自动生成并持久化
228
- if (!cfg.clientId) {
229
- cfg.clientId = ensureClientId();
230
- file.clientId = cfg.clientId;
231
- try { writeConfigFile(file); } catch { /* ignore */ }
232
- }
233
-
234
- return cfg;
235
- }
236
-
237
- function pick(...vals) {
238
- for (const v of vals) {
239
- if (v !== undefined && v !== null && v !== '') return v;
240
- }
241
- return undefined;
242
- }
243
-
244
- /**
245
- * 布尔开关的严格解析。
246
- *
247
- * 空 → undefined(**没表态**,让上层按「跟随其他配置」处理);
248
- * 1/true/yes/on → true;0/false/no/off → false;
249
- * 其他值 → **直接抛错**。
250
- *
251
- * 为什么要严格:部署脚本里 `MN_WS_AUTH_ENABLED=1` 被当成「关闭」时,
252
- * 服务照常启动、/health 照样绿,而整套按用户隔离的属主判据**静默不存在** ——
253
- * CI 全绿、人以为开着。这类「配了等于没配」必须在解析层就炸掉。
254
- */
255
- function parseBoolFlag(v) {
256
- if (v === undefined || v === null || v === '') return undefined;
257
- if (v === true || v === false) return v;
258
- const s = String(v).trim().toLowerCase();
259
- if (['1', 'true', 'yes', 'on'].includes(s)) return true;
260
- if (['0', 'false', 'no', 'off'].includes(s)) return false;
261
- throw new Error(`布尔开关取值无法识别:${JSON.stringify(v)}(认 1/true/yes/on 与 0/false/no/off)`);
262
- }
263
-
264
- /** --apps "nx-kv,nx-rh" → ['nx-kv','nx-rh'];空 → [](= 不限,但实际仍需显式配置才开桥)。 */
265
- function parseApps(v) {
266
- if (Array.isArray(v)) return v.map((x) => String(x).trim()).filter(Boolean);
267
- if (!v) return [];
268
- return String(v).split(',').map((x) => x.trim()).filter(Boolean);
269
- }
270
-
271
- /**
272
- * --ports "7877,7800" → [7877, 7800]
273
- *
274
- * 「本地 client 端选择可以开放的端口」——按端口选比按应用名选更贴近
275
- * 用户的心智:他看到的是「nx-kv 跑在 7877」,不是「nx-kv 这个名字」。
276
- */
277
- function parsePorts(v) {
278
- if (Array.isArray(v)) return v.map((x) => Number(x)).filter((n) => Number.isFinite(n));
279
- if (!v) return [];
280
- return String(v).split(',')
281
- .map((x) => Number(x.trim()))
282
- .filter((n) => Number.isFinite(n) && n > 0);
283
- }
284
-
285
- /**
286
- * --app-map "nx-kv=http://127.0.0.1:7877,nx-rh=http://127.0.0.1:7800"
287
- * → { 'nx-kv': {base:'http://127.0.0.1:7877'}, 'nx-rh': {...} }
288
- *
289
- * 为什么需要它:已知端口表只是**兜底默认值**,真实部署常改端口
290
- * (换机、端口冲突、容器映射)。让用户能显式覆盖,比让程序去猜端口靠谱。
291
- */
292
- function parseAppMap(v) {
293
- const out = {};
294
- if (!v) return out;
295
- for (const pair of String(v).split(',')) {
296
- const s = pair.trim();
297
- if (!s) continue;
298
- const eq = s.indexOf('=');
299
- if (eq <= 0) continue;
300
- const app = s.slice(0, eq).trim();
301
- const base = s.slice(eq + 1).trim();
302
- if (app && base) out[app] = { base };
303
- }
304
- return out;
305
- }
306
-
307
- // ───────────────────────── 账号表 ─────────────────────────
308
-
309
- /** 账号条目的稳定 key:优先 userId,其次邮箱/用户名/昵称。 */
310
- function entryKeyOf(entry) {
311
- const e = entry || {};
312
- return String(e.userId || e.email || e.username || e.nickname || e.clientId || '');
313
- }
314
-
315
- function labelOf(entry) {
316
- const e = entry || {};
317
- return e.nickname || e.username || e.email || e.userId || e.clientId || '(未知账号)';
318
- }
319
-
320
- /**
321
- * 视图:老配置的 flat 凭据被合成成首个条目。
322
- *
323
- * 这样「第二个人登录」不会把第一个人的登录态挤掉 —— 两个账号并存。
324
- */
325
- function withAccounts(raw, { defaultClientId = '' } = {}) {
326
- const base = raw || {};
327
- const accounts = { ...(base.accounts && typeof base.accounts === 'object' ? base.accounts : {}) };
328
- let activeAccountKey = base.activeAccount || '';
329
-
330
- const flat = {
331
- userId: base.userId,
332
- email: base.email,
333
- username: base.username,
334
- nickname: base.nickname,
335
- deviceToken: base.deviceToken,
336
- expiresAt: base.expiresAt,
337
- deviceName: base.deviceName,
338
- clientId: base.clientId,
339
- };
340
- const hasFlat = Object.values(flat).some((v) => v !== undefined && v !== '');
341
-
342
- if (hasFlat) {
343
- const flatEntry = { ...flat };
344
- let flatKey = entryKeyOf(flatEntry);
345
- if (!flatKey) {
346
- flatKey = 'local';
347
- flatEntry.clientId = flatEntry.clientId || defaultClientId;
348
- }
349
- // flat 里没有的账号才补进去(flat 优先:它是「当前激活账号」的镜像)
350
- if (!accounts[flatKey]) accounts[flatKey] = flatEntry;
351
- if (!activeAccountKey) activeAccountKey = flatKey;
352
- }
353
-
354
- return { accounts, activeAccountKey };
355
- }
356
-
357
- function listAccounts(view) {
358
- return Object.entries(view.accounts || {}).map(([key, entry]) => ({ key, entry }));
359
- }
360
-
361
- /** 把账号条目的运行期字段并进 flat(不动身份字段)。 */
362
- function buildFlatSyncPatch(entry) {
363
- const patch = {};
364
- for (const k of ['url', 'deviceToken', 'expiresAt', 'userId', 'email', 'username', 'nickname', 'deviceName', 'clientId', 'serverHttp']) {
365
- if (entry && entry[k] !== undefined) patch[k] = entry[k];
366
- }
367
- return patch;
368
- }
369
-
370
- /**
371
- * 保存一次成功登录的账号,并激活它。
372
- * @returns {{changed: boolean, key: string, file: string}}
373
- */
374
- function saveAuthAccount({ entry, setActive = true }) {
375
- const file = configFile();
376
- const existing = readConfigFile();
377
- const view = withAccounts(existing, { defaultClientId: ensureClientId() });
378
- const key = entryKeyOf(entry);
379
- const entryWithId = { ...entry, clientId: entry.clientId || ensureClientId() };
380
- const accounts = { ...view.accounts, [key]: entryWithId };
381
- const next = { ...existing, accounts };
382
- if (setActive) {
383
- next.activeAccount = key;
384
- Object.assign(next, buildFlatSyncPatch(entryWithId));
385
- }
386
- const changed = JSON.stringify(existing) !== JSON.stringify(next);
387
- if (changed) writeConfigFile(next);
388
- return { changed, key, file };
389
- }
390
-
391
- /**
392
- * 切换激活账号:设指针 + 把该账号条目打平成 flat。
393
- * 跨部署切换时 url/serverHttp 必须一起走,否则会拿 A 服务器的令牌去连 B。
394
- */
395
- function switchActiveAccount(target, { defaultClientId = '' } = {}) {
396
- const file = configFile();
397
- const existing = readConfigFile();
398
- const view = withAccounts(existing, { defaultClientId });
399
- const list = listAccounts(view);
400
- if (!list.length) throw new Error('本机还没有已存账号:先执行 npx mn-ws auth login');
401
-
402
- let picked = null;
403
- const t = String(target || '').trim();
404
- if (t) {
405
- const byKey = list.find((a) => a.key === t);
406
- const byIdx = /^\d+$/.test(t) ? list[Number(t) - 1] : null;
407
- const byField = list.find((a) =>
408
- [a.entry.email, a.entry.username, a.entry.nickname].some((v) => v && String(v) === t));
409
- picked = byKey || byIdx || byField || null;
410
- if (!picked) {
411
- throw new Error(`找不到账号「${t}」。本机已存:${list.map((a) => a.key).join(', ')}`);
412
- }
413
- } else {
414
- // 不带参数且只有一个账号:没有「切换」可言,直接说清楚
415
- if (list.length === 1) {
416
- const only = list[0];
417
- const err = new Error(`本机只有 1 个账号:${labelOf(only.entry)}(clientId ${only.entry.clientId || '(无)'})`);
418
- err.onlyOne = true;
419
- err.entry = only.entry;
420
- throw err;
421
- }
422
- return { changed: false, needsChoice: true, list, view };
423
- }
424
-
425
- const entry = { ...picked.entry, clientId: picked.entry.clientId || defaultClientId };
426
- const next = {
427
- ...existing,
428
- accounts: { ...view.accounts, [picked.key]: entry },
429
- activeAccount: picked.key,
430
- ...buildFlatSyncPatch(entry),
431
- };
432
- const changed = JSON.stringify(existing) !== JSON.stringify(next);
433
- if (changed) writeConfigFile(next);
434
- return { changed, key: picked.key, entry, file };
435
- }
436
-
437
- /**
438
- * 清除登录凭据。
439
- * @param {{all?: boolean}} opts
440
- */
441
- function clearAuthCredentials({ all = false } = {}) {
442
- const file = configFile();
443
- const existing = readConfigFile();
444
- const view = withAccounts(existing, { defaultClientId: ensureClientId() });
445
- const next = { ...existing };
446
-
447
- if (all) {
448
- delete next.accounts;
449
- delete next.activeAccount;
450
- } else if (view.activeAccountKey && view.accounts[view.activeAccountKey]) {
451
- const accounts = { ...view.accounts };
452
- delete accounts[view.activeAccountKey];
453
- if (Object.keys(accounts).length) next.accounts = accounts;
454
- else delete next.accounts;
455
- if (next.activeAccount === view.activeAccountKey) delete next.activeAccount;
456
- }
457
-
458
- for (const k of ['userId', 'email', 'username', 'nickname', 'deviceToken', 'expiresAt', 'clientId']) {
459
- delete next[k];
460
- }
461
-
462
- const changed = JSON.stringify(existing) !== JSON.stringify(next);
463
- if (changed) writeConfigFile(next);
464
- const remaining = next.accounts ? Object.keys(next.accounts).length : 0;
465
- return { changed, file, remaining };
466
- }
467
-
468
- /** 本机标识:没有就生成一个并落盘。 */
469
- function ensureClientId() {
470
- const file = readConfigFile();
471
- if (file.clientId) return file.clientId;
472
- const id = generateClientId();
473
- file.clientId = id;
474
- try { writeConfigFile(file); } catch { /* ignore */ }
475
- return id;
476
- }
477
-
478
- /** 「主机名-6 位随机」生成 clientId。 */
479
- function generateClientId(exclude) {
480
- const host = (os.hostname() || 'pc').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 20);
481
- const skip = exclude instanceof Set ? exclude : new Set(exclude || []);
482
- for (let i = 0; i < 20; i += 1) {
483
- const id = `${host}-${Math.random().toString(36).slice(2, 8)}`;
484
- if (!skip.has(id)) return id;
485
- }
486
- return `${host}-${Math.random().toString(36).slice(2, 8)}`;
487
- }
488
-
489
- /** 保存本次连接用的参数(url / token 等)到配置文件。 */
490
- function saveConnectionConfig(cfg) {
491
- const file = configFile();
492
- const existing = readConfigFile();
493
- const relevant = {};
494
- for (const k of ['url', 'serverHttp', 'token', 'deviceName', 'clientId']) {
495
- if (cfg[k]) relevant[k] = cfg[k];
496
- }
497
- const next = { ...existing, ...relevant };
498
- const changed = JSON.stringify(existing) !== JSON.stringify(next);
499
- if (changed) writeConfigFile(next);
500
- return { changed, file };
501
- }
502
-
503
- module.exports = {
504
- DEFAULT_SERVE_URL,
505
- DEFAULT_SERVER_HTTP,
506
- DEFAULT_SERVER_PORT,
507
- DEFAULT_WEB_PORT,
508
- PLACEHOLDER_HOSTS,
509
- KNOWN_APPS,
510
- configDir,
511
- configFile,
512
- readConfigFile,
513
- writeConfigFile,
514
- parseArgs,
515
- parseBoolFlag,
516
- parseApps,
517
- parsePorts,
518
- parseAppMap,
519
- placeholderUrlReason,
520
- httpBaseFromWs,
521
- loadConfig,
522
- ensureClientId,
523
- generateClientId,
524
- withAccounts,
525
- listAccounts,
526
- entryKeyOf,
527
- labelOf,
528
- buildFlatSyncPatch,
529
- saveAuthAccount,
530
- switchActiveAccount,
531
- clearAuthCredentials,
532
- saveConnectionConfig,
533
- Logger,
534
- };
package/lib/index.js DELETED
@@ -1,15 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- protocol: require('./protocol'),
5
- apps: require('./apps'),
6
- config: require('./config'),
7
- logger: require('./logger'),
8
- store: require('./store'),
9
- auth: require('./auth'),
10
- server: require('../server/server'),
11
- serverAuth: require('../server/auth'),
12
- bridgeClient: require('../client/bridge-client'),
13
- wsClient: require('../client/ws-client'),
14
- web: require('../client/web'),
15
- };
package/lib/logger.js DELETED
@@ -1,63 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * 极简分级日志器。
5
- *
6
- * 与 mn-rt 的 client/lib/logger.js 同款:写文件 + 控制台,
7
- * 不引第三方依赖(这个包要能被 npx 一把跑起来,依赖越少越好)。
8
- */
9
-
10
- const fs = require('node:fs');
11
- const path = require('node:path');
12
-
13
- const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
14
-
15
- class Logger {
16
- constructor({ level = 'info', dir = null, name = 'mn-ws' } = {}) {
17
- this.level = LEVELS[level] || LEVELS.info;
18
- this.levelName = level;
19
- this.dir = dir;
20
- this.name = name;
21
- this.stream = null;
22
-
23
- if (dir) {
24
- try {
25
- fs.mkdirSync(dir, { recursive: true });
26
- const file = path.join(dir, `${name}.log`);
27
- this.stream = fs.createWriteStream(file, { flags: 'a' });
28
- } catch {
29
- this.stream = null; // 写不了日志不是致命错误,降级成只打控制台
30
- }
31
- }
32
- }
33
-
34
- _log(level, msg, extra) {
35
- if ((LEVELS[level] || 0) < this.level) return;
36
- const ts = new Date().toISOString();
37
- const line = extra
38
- ? `${ts} [${level.toUpperCase()}] ${msg} ${safeJson(extra)}`
39
- : `${ts} [${level.toUpperCase()}] ${msg}`;
40
- try { process.stdout.write(`${line}\n`); } catch { /* ignore */ }
41
- if (this.stream) {
42
- try { this.stream.write(`${line}\n`); } catch { /* ignore */ }
43
- }
44
- }
45
-
46
- debug(msg, extra) { this._log('debug', msg, extra); }
47
- info(msg, extra) { this._log('info', msg, extra); }
48
- warn(msg, extra) { this._log('warn', msg, extra); }
49
- error(msg, extra) { this._log('error', msg, extra); }
50
-
51
- close() {
52
- if (this.stream) {
53
- try { this.stream.end(); } catch { /* ignore */ }
54
- this.stream = null;
55
- }
56
- }
57
- }
58
-
59
- function safeJson(obj) {
60
- try { return JSON.stringify(obj); } catch { return '[unserializable]'; }
61
- }
62
-
63
- module.exports = { Logger, LEVELS };