@ondevplann/remotetype 0.1.2 → 0.1.3
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/bin/index.js +732 -732
- package/binaries/linux-x64/input-agent +0 -0
- package/lib/agent.js +382 -382
- package/lib/config.js +148 -148
- package/lib/index.js +27 -27
- package/lib/logger.js +95 -95
- package/lib/queue.js +141 -141
- package/lib/sanitize.js +71 -71
- package/lib/store.js +75 -75
- package/lib/ws-client.js +217 -217
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 配置解析:默认值 < 配置文件 < 环境变量 < 命令行参数。
|
|
5
|
-
*
|
|
6
|
-
* 配置文件位置:~/.remotetype/config.json
|
|
7
|
-
* 环境变量前缀:REMOTETYPE_
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const { resolveTarget } = require('./platform');
|
|
14
|
-
|
|
15
|
-
/** 内置默认公网服务地址(用户可用 --url 或环境变量覆盖)。 */
|
|
16
|
-
const DEFAULT_SERVE_URL = 'wss://remotetype.example.com/ws';
|
|
17
|
-
|
|
18
|
-
/** 配置目录:~/.remotetype */
|
|
19
|
-
function configDir() {
|
|
20
|
-
const base =
|
|
21
|
-
process.env.REMOTETYPE_HOME ||
|
|
22
|
-
(process.platform === 'win32'
|
|
23
|
-
? path.join(process.env.APPDATA || os.homedir(), 'remotetype')
|
|
24
|
-
: path.join(os.homedir(), '.remotetype'));
|
|
25
|
-
return base;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function configFile() {
|
|
29
|
-
return path.join(configDir(), 'config.json');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readConfigFile() {
|
|
33
|
-
const file = configFile();
|
|
34
|
-
try {
|
|
35
|
-
if (!fs.existsSync(file)) return {};
|
|
36
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
37
|
-
} catch {
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 生成/持久化 clientId:首次运行按 主机名-随机串 生成并落盘,保证稳定路由。 */
|
|
43
|
-
function ensureClientId() {
|
|
44
|
-
const dir = configDir();
|
|
45
|
-
const file = path.join(dir, 'client-id');
|
|
46
|
-
try {
|
|
47
|
-
if (fs.existsSync(file)) {
|
|
48
|
-
const id = fs.readFileSync(file, 'utf8').trim();
|
|
49
|
-
if (id) return id;
|
|
50
|
-
}
|
|
51
|
-
} catch { /* 忽略,继续生成 */ }
|
|
52
|
-
|
|
53
|
-
const host = (os.hostname() || 'pc').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 20);
|
|
54
|
-
const rand = Math.random().toString(36).slice(2, 8);
|
|
55
|
-
const id = `${host}-${rand}`;
|
|
56
|
-
try {
|
|
57
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
58
|
-
fs.writeFileSync(file, id, 'utf8');
|
|
59
|
-
} catch { /* 不可写则每次生成,仅影响路由稳定性 */ }
|
|
60
|
-
return id;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 解析 CLI 参数:支持 --key value 与 --key=value,以及 -h/--help 等布尔位。 */
|
|
64
|
-
function parseArgs(argv) {
|
|
65
|
-
const flags = {};
|
|
66
|
-
const positional = [];
|
|
67
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
68
|
-
const arg = argv[i];
|
|
69
|
-
if (arg.startsWith('--')) {
|
|
70
|
-
const [rawKey, inline] = arg.slice(2).split('=');
|
|
71
|
-
// --client-id → clientId:loadConfig 按驼峰读取,
|
|
72
|
-
// 曾因不做映射导致所有多词参数(--client-id/--log-level/…)静默失效
|
|
73
|
-
const key = rawKey.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
74
|
-
if (inline !== undefined) {
|
|
75
|
-
flags[key] = inline;
|
|
76
|
-
} else {
|
|
77
|
-
const next = argv[i + 1];
|
|
78
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
79
|
-
flags[key] = next;
|
|
80
|
-
i += 1;
|
|
81
|
-
} else {
|
|
82
|
-
flags[key] = true;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} else if (arg.startsWith('-') && arg.length > 1) {
|
|
86
|
-
const short = arg.slice(1);
|
|
87
|
-
const map = { h: 'help', v: 'version' };
|
|
88
|
-
flags[map[short] || short] = true;
|
|
89
|
-
} else {
|
|
90
|
-
positional.push(arg);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { flags, positional };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 组装最终配置。 */
|
|
97
|
-
function loadConfig(flags = {}) {
|
|
98
|
-
const file = readConfigFile();
|
|
99
|
-
const env = process.env;
|
|
100
|
-
const target = resolveTarget();
|
|
101
|
-
|
|
102
|
-
const num = (v, d) => {
|
|
103
|
-
const n = Number.parseInt(v, 10);
|
|
104
|
-
return Number.isFinite(n) ? n : d;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const url =
|
|
108
|
-
flags.url ?? env.REMOTETYPE_URL ?? file.url ?? DEFAULT_SERVE_URL;
|
|
109
|
-
|
|
110
|
-
const cfg = {
|
|
111
|
-
url,
|
|
112
|
-
clientId: flags.clientId ?? env.REMOTETYPE_CLIENT_ID ?? file.clientId ?? ensureClientId(),
|
|
113
|
-
logLevel: flags.logLevel ?? env.REMOTETYPE_LOG_LEVEL ?? file.logLevel ?? 'info',
|
|
114
|
-
logDir: flags.logDir ?? env.REMOTETYPE_LOG_DIR ?? file.logDir ?? path.join(configDir(), 'logs'),
|
|
115
|
-
recordFile:
|
|
116
|
-
flags.recordFile ?? env.REMOTETYPE_RECORD_FILE ?? file.recordFile ??
|
|
117
|
-
path.join(configDir(), 'data', 'records.jsonl'),
|
|
118
|
-
// 队列与预处理
|
|
119
|
-
maxQueueSize: num(flags.maxQueueSize ?? env.REMOTETYPE_MAX_QUEUE ?? file.maxQueueSize, 100),
|
|
120
|
-
maxTextLength: num(flags.maxTextLength ?? env.REMOTETYPE_MAX_TEXT ?? file.maxTextLength, 5000),
|
|
121
|
-
// 子进程
|
|
122
|
-
restartDelayMs: num(flags.restartDelayMs ?? env.REMOTETYPE_RESTART_DELAY ?? file.restartDelayMs, 2000),
|
|
123
|
-
heartbeatMs: num(flags.heartbeatMs ?? env.REMOTETYPE_HEARTBEAT ?? file.heartbeatMs, 15000),
|
|
124
|
-
requestTimeoutMs: num(flags.requestTimeoutMs ?? env.REMOTETYPE_REQUEST_TIMEOUT ?? file.requestTimeoutMs, 30000),
|
|
125
|
-
// 二进制
|
|
126
|
-
agentPath: flags.agent ?? env.REMOTETYPE_AGENT ?? file.agentPath ?? null,
|
|
127
|
-
// 对齐模式(--align 开关:加密 + 输入实时对齐;--key 传配对 key)
|
|
128
|
-
align: flags.align === true,
|
|
129
|
-
alignKey: flags.key ?? env.REMOTETYPE_KEY ?? file.alignKey ?? null,
|
|
130
|
-
// 注册门禁 token(服务器设 RT_TOKEN 后必须携带同一值才能注册)
|
|
131
|
-
token: flags.token ?? env.REMOTETYPE_TOKEN ?? env.RT_TOKEN ?? file.token ?? null,
|
|
132
|
-
configDir: configDir(),
|
|
133
|
-
configFile: configFile(),
|
|
134
|
-
target,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
return cfg;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
module.exports = {
|
|
141
|
-
loadConfig,
|
|
142
|
-
parseArgs,
|
|
143
|
-
configDir,
|
|
144
|
-
configFile,
|
|
145
|
-
ensureClientId,
|
|
146
|
-
readConfigFile,
|
|
147
|
-
DEFAULT_SERVE_URL,
|
|
148
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 配置解析:默认值 < 配置文件 < 环境变量 < 命令行参数。
|
|
5
|
+
*
|
|
6
|
+
* 配置文件位置:~/.remotetype/config.json
|
|
7
|
+
* 环境变量前缀:REMOTETYPE_
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { resolveTarget } = require('./platform');
|
|
14
|
+
|
|
15
|
+
/** 内置默认公网服务地址(用户可用 --url 或环境变量覆盖)。 */
|
|
16
|
+
const DEFAULT_SERVE_URL = 'wss://remotetype.example.com/ws';
|
|
17
|
+
|
|
18
|
+
/** 配置目录:~/.remotetype */
|
|
19
|
+
function configDir() {
|
|
20
|
+
const base =
|
|
21
|
+
process.env.REMOTETYPE_HOME ||
|
|
22
|
+
(process.platform === 'win32'
|
|
23
|
+
? path.join(process.env.APPDATA || os.homedir(), 'remotetype')
|
|
24
|
+
: path.join(os.homedir(), '.remotetype'));
|
|
25
|
+
return base;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function configFile() {
|
|
29
|
+
return path.join(configDir(), 'config.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readConfigFile() {
|
|
33
|
+
const file = configFile();
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(file)) return {};
|
|
36
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 生成/持久化 clientId:首次运行按 主机名-随机串 生成并落盘,保证稳定路由。 */
|
|
43
|
+
function ensureClientId() {
|
|
44
|
+
const dir = configDir();
|
|
45
|
+
const file = path.join(dir, 'client-id');
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(file)) {
|
|
48
|
+
const id = fs.readFileSync(file, 'utf8').trim();
|
|
49
|
+
if (id) return id;
|
|
50
|
+
}
|
|
51
|
+
} catch { /* 忽略,继续生成 */ }
|
|
52
|
+
|
|
53
|
+
const host = (os.hostname() || 'pc').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 20);
|
|
54
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
55
|
+
const id = `${host}-${rand}`;
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
58
|
+
fs.writeFileSync(file, id, 'utf8');
|
|
59
|
+
} catch { /* 不可写则每次生成,仅影响路由稳定性 */ }
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 解析 CLI 参数:支持 --key value 与 --key=value,以及 -h/--help 等布尔位。 */
|
|
64
|
+
function parseArgs(argv) {
|
|
65
|
+
const flags = {};
|
|
66
|
+
const positional = [];
|
|
67
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
68
|
+
const arg = argv[i];
|
|
69
|
+
if (arg.startsWith('--')) {
|
|
70
|
+
const [rawKey, inline] = arg.slice(2).split('=');
|
|
71
|
+
// --client-id → clientId:loadConfig 按驼峰读取,
|
|
72
|
+
// 曾因不做映射导致所有多词参数(--client-id/--log-level/…)静默失效
|
|
73
|
+
const key = rawKey.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
74
|
+
if (inline !== undefined) {
|
|
75
|
+
flags[key] = inline;
|
|
76
|
+
} else {
|
|
77
|
+
const next = argv[i + 1];
|
|
78
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
79
|
+
flags[key] = next;
|
|
80
|
+
i += 1;
|
|
81
|
+
} else {
|
|
82
|
+
flags[key] = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (arg.startsWith('-') && arg.length > 1) {
|
|
86
|
+
const short = arg.slice(1);
|
|
87
|
+
const map = { h: 'help', v: 'version' };
|
|
88
|
+
flags[map[short] || short] = true;
|
|
89
|
+
} else {
|
|
90
|
+
positional.push(arg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { flags, positional };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 组装最终配置。 */
|
|
97
|
+
function loadConfig(flags = {}) {
|
|
98
|
+
const file = readConfigFile();
|
|
99
|
+
const env = process.env;
|
|
100
|
+
const target = resolveTarget();
|
|
101
|
+
|
|
102
|
+
const num = (v, d) => {
|
|
103
|
+
const n = Number.parseInt(v, 10);
|
|
104
|
+
return Number.isFinite(n) ? n : d;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const url =
|
|
108
|
+
flags.url ?? env.REMOTETYPE_URL ?? file.url ?? DEFAULT_SERVE_URL;
|
|
109
|
+
|
|
110
|
+
const cfg = {
|
|
111
|
+
url,
|
|
112
|
+
clientId: flags.clientId ?? env.REMOTETYPE_CLIENT_ID ?? file.clientId ?? ensureClientId(),
|
|
113
|
+
logLevel: flags.logLevel ?? env.REMOTETYPE_LOG_LEVEL ?? file.logLevel ?? 'info',
|
|
114
|
+
logDir: flags.logDir ?? env.REMOTETYPE_LOG_DIR ?? file.logDir ?? path.join(configDir(), 'logs'),
|
|
115
|
+
recordFile:
|
|
116
|
+
flags.recordFile ?? env.REMOTETYPE_RECORD_FILE ?? file.recordFile ??
|
|
117
|
+
path.join(configDir(), 'data', 'records.jsonl'),
|
|
118
|
+
// 队列与预处理
|
|
119
|
+
maxQueueSize: num(flags.maxQueueSize ?? env.REMOTETYPE_MAX_QUEUE ?? file.maxQueueSize, 100),
|
|
120
|
+
maxTextLength: num(flags.maxTextLength ?? env.REMOTETYPE_MAX_TEXT ?? file.maxTextLength, 5000),
|
|
121
|
+
// 子进程
|
|
122
|
+
restartDelayMs: num(flags.restartDelayMs ?? env.REMOTETYPE_RESTART_DELAY ?? file.restartDelayMs, 2000),
|
|
123
|
+
heartbeatMs: num(flags.heartbeatMs ?? env.REMOTETYPE_HEARTBEAT ?? file.heartbeatMs, 15000),
|
|
124
|
+
requestTimeoutMs: num(flags.requestTimeoutMs ?? env.REMOTETYPE_REQUEST_TIMEOUT ?? file.requestTimeoutMs, 30000),
|
|
125
|
+
// 二进制
|
|
126
|
+
agentPath: flags.agent ?? env.REMOTETYPE_AGENT ?? file.agentPath ?? null,
|
|
127
|
+
// 对齐模式(--align 开关:加密 + 输入实时对齐;--key 传配对 key)
|
|
128
|
+
align: flags.align === true,
|
|
129
|
+
alignKey: flags.key ?? env.REMOTETYPE_KEY ?? file.alignKey ?? null,
|
|
130
|
+
// 注册门禁 token(服务器设 RT_TOKEN 后必须携带同一值才能注册)
|
|
131
|
+
token: flags.token ?? env.REMOTETYPE_TOKEN ?? env.RT_TOKEN ?? file.token ?? null,
|
|
132
|
+
configDir: configDir(),
|
|
133
|
+
configFile: configFile(),
|
|
134
|
+
target,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return cfg;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
loadConfig,
|
|
142
|
+
parseArgs,
|
|
143
|
+
configDir,
|
|
144
|
+
configFile,
|
|
145
|
+
ensureClientId,
|
|
146
|
+
readConfigFile,
|
|
147
|
+
DEFAULT_SERVE_URL,
|
|
148
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 模块导出:把内部能力暴露出来,便于单元测试与二次开发。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { AgentManager, STATE } = require('./agent');
|
|
8
|
-
const { MessageQueue } = require('./queue');
|
|
9
|
-
const { ServeClient } = require('./ws-client');
|
|
10
|
-
const { LocalStore } = require('./store');
|
|
11
|
-
const { Logger } = require('./logger');
|
|
12
|
-
const sanitize = require('./sanitize');
|
|
13
|
-
const platform = require('./platform');
|
|
14
|
-
const config = require('./config');
|
|
15
|
-
|
|
16
|
-
module.exports = {
|
|
17
|
-
AgentManager,
|
|
18
|
-
STATE,
|
|
19
|
-
MessageQueue,
|
|
20
|
-
ServeClient,
|
|
21
|
-
LocalStore,
|
|
22
|
-
Logger,
|
|
23
|
-
sanitize,
|
|
24
|
-
platform,
|
|
25
|
-
config,
|
|
26
|
-
version: require('../package.json').version,
|
|
27
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 模块导出:把内部能力暴露出来,便于单元测试与二次开发。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { AgentManager, STATE } = require('./agent');
|
|
8
|
+
const { MessageQueue } = require('./queue');
|
|
9
|
+
const { ServeClient } = require('./ws-client');
|
|
10
|
+
const { LocalStore } = require('./store');
|
|
11
|
+
const { Logger } = require('./logger');
|
|
12
|
+
const sanitize = require('./sanitize');
|
|
13
|
+
const platform = require('./platform');
|
|
14
|
+
const config = require('./config');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
AgentManager,
|
|
18
|
+
STATE,
|
|
19
|
+
MessageQueue,
|
|
20
|
+
ServeClient,
|
|
21
|
+
LocalStore,
|
|
22
|
+
Logger,
|
|
23
|
+
sanitize,
|
|
24
|
+
platform,
|
|
25
|
+
config,
|
|
26
|
+
version: require('../package.json').version,
|
|
27
|
+
};
|
package/lib/logger.js
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 客户端日志器:控制台(带时间戳与级别色)+ 本地文件。
|
|
5
|
-
* 与 server/src/logger.js 行为保持一致,便于两端日志对照排查。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
|
|
11
|
-
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
12
|
-
const COLORS = {
|
|
13
|
-
debug: '\x1b[90m',
|
|
14
|
-
info: '\x1b[36m',
|
|
15
|
-
warn: '\x1b[33m',
|
|
16
|
-
error: '\x1b[31m',
|
|
17
|
-
reset: '\x1b[0m',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function safeStringify(value) {
|
|
21
|
-
if (value === undefined) return undefined;
|
|
22
|
-
try {
|
|
23
|
-
const s = JSON.stringify(value);
|
|
24
|
-
if (s === undefined) return String(value);
|
|
25
|
-
return s.length > 2000 ? `${s.slice(0, 2000)}...(truncated)` : s;
|
|
26
|
-
} catch {
|
|
27
|
-
return String(value);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
class Logger {
|
|
32
|
-
/**
|
|
33
|
-
* @param {object} opts
|
|
34
|
-
* @param {string} [opts.level]
|
|
35
|
-
* @param {string|null} [opts.logDir]
|
|
36
|
-
* @param {string} [opts.name]
|
|
37
|
-
* @param {boolean} [opts.console]
|
|
38
|
-
*/
|
|
39
|
-
constructor(opts = {}) {
|
|
40
|
-
this.level = LEVELS[opts.level] ?? LEVELS.info;
|
|
41
|
-
this.name = opts.name || 'client';
|
|
42
|
-
this.printToConsole = opts.console !== false;
|
|
43
|
-
this.stream = null;
|
|
44
|
-
this.currentDay = null;
|
|
45
|
-
|
|
46
|
-
if (opts.logDir) {
|
|
47
|
-
this.logDir = opts.logDir;
|
|
48
|
-
try {
|
|
49
|
-
fs.mkdirSync(this.logDir, { recursive: true });
|
|
50
|
-
this._rotate();
|
|
51
|
-
} catch {
|
|
52
|
-
// 日志目录不可写时降级为纯控制台,不影响主流程
|
|
53
|
-
this.stream = null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
_rotate() {
|
|
59
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
60
|
-
if (this.currentDay === day && this.stream) return;
|
|
61
|
-
if (this.stream) this.stream.end();
|
|
62
|
-
this.currentDay = day;
|
|
63
|
-
this.stream = fs.createWriteStream(path.join(this.logDir, `${this.name}-${day}.log`), {
|
|
64
|
-
flags: 'a',
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
_write(level, msg, meta) {
|
|
69
|
-
if (LEVELS[level] < this.level) return;
|
|
70
|
-
const ts = new Date().toISOString();
|
|
71
|
-
const metaStr = safeStringify(meta);
|
|
72
|
-
const line = `${ts} [${level.toUpperCase().padEnd(5)}] ${msg}${metaStr ? ` ${metaStr}` : ''}`;
|
|
73
|
-
if (this.printToConsole) {
|
|
74
|
-
process.stdout.write(`${COLORS[level] || ''}${line}${COLORS.reset}\n`);
|
|
75
|
-
}
|
|
76
|
-
if (this.stream) {
|
|
77
|
-
this._rotate();
|
|
78
|
-
this.stream.write(`${line}\n`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
debug(msg, meta) { this._write('debug', msg, meta); }
|
|
83
|
-
info(msg, meta) { this._write('info', msg, meta); }
|
|
84
|
-
warn(msg, meta) { this._write('warn', msg, meta); }
|
|
85
|
-
error(msg, meta) { this._write('error', msg, meta); }
|
|
86
|
-
|
|
87
|
-
close() {
|
|
88
|
-
if (this.stream) {
|
|
89
|
-
this.stream.end();
|
|
90
|
-
this.stream = null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
module.exports = { Logger, LEVELS };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 客户端日志器:控制台(带时间戳与级别色)+ 本地文件。
|
|
5
|
+
* 与 server/src/logger.js 行为保持一致,便于两端日志对照排查。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
12
|
+
const COLORS = {
|
|
13
|
+
debug: '\x1b[90m',
|
|
14
|
+
info: '\x1b[36m',
|
|
15
|
+
warn: '\x1b[33m',
|
|
16
|
+
error: '\x1b[31m',
|
|
17
|
+
reset: '\x1b[0m',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function safeStringify(value) {
|
|
21
|
+
if (value === undefined) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const s = JSON.stringify(value);
|
|
24
|
+
if (s === undefined) return String(value);
|
|
25
|
+
return s.length > 2000 ? `${s.slice(0, 2000)}...(truncated)` : s;
|
|
26
|
+
} catch {
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Logger {
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {string} [opts.level]
|
|
35
|
+
* @param {string|null} [opts.logDir]
|
|
36
|
+
* @param {string} [opts.name]
|
|
37
|
+
* @param {boolean} [opts.console]
|
|
38
|
+
*/
|
|
39
|
+
constructor(opts = {}) {
|
|
40
|
+
this.level = LEVELS[opts.level] ?? LEVELS.info;
|
|
41
|
+
this.name = opts.name || 'client';
|
|
42
|
+
this.printToConsole = opts.console !== false;
|
|
43
|
+
this.stream = null;
|
|
44
|
+
this.currentDay = null;
|
|
45
|
+
|
|
46
|
+
if (opts.logDir) {
|
|
47
|
+
this.logDir = opts.logDir;
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
50
|
+
this._rotate();
|
|
51
|
+
} catch {
|
|
52
|
+
// 日志目录不可写时降级为纯控制台,不影响主流程
|
|
53
|
+
this.stream = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_rotate() {
|
|
59
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
60
|
+
if (this.currentDay === day && this.stream) return;
|
|
61
|
+
if (this.stream) this.stream.end();
|
|
62
|
+
this.currentDay = day;
|
|
63
|
+
this.stream = fs.createWriteStream(path.join(this.logDir, `${this.name}-${day}.log`), {
|
|
64
|
+
flags: 'a',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_write(level, msg, meta) {
|
|
69
|
+
if (LEVELS[level] < this.level) return;
|
|
70
|
+
const ts = new Date().toISOString();
|
|
71
|
+
const metaStr = safeStringify(meta);
|
|
72
|
+
const line = `${ts} [${level.toUpperCase().padEnd(5)}] ${msg}${metaStr ? ` ${metaStr}` : ''}`;
|
|
73
|
+
if (this.printToConsole) {
|
|
74
|
+
process.stdout.write(`${COLORS[level] || ''}${line}${COLORS.reset}\n`);
|
|
75
|
+
}
|
|
76
|
+
if (this.stream) {
|
|
77
|
+
this._rotate();
|
|
78
|
+
this.stream.write(`${line}\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
debug(msg, meta) { this._write('debug', msg, meta); }
|
|
83
|
+
info(msg, meta) { this._write('info', msg, meta); }
|
|
84
|
+
warn(msg, meta) { this._write('warn', msg, meta); }
|
|
85
|
+
error(msg, meta) { this._write('error', msg, meta); }
|
|
86
|
+
|
|
87
|
+
close() {
|
|
88
|
+
if (this.stream) {
|
|
89
|
+
this.stream.end();
|
|
90
|
+
this.stream = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { Logger, LEVELS };
|