ysagc-agent 0.2.0 → 0.4.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.
- package/README.md +36 -66
- package/dist/agent.d.ts +17 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +293 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +267 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +135 -0
- package/dist/config.js.map +1 -0
- package/dist/executor.d.ts +46 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +397 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +37 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +121 -0
- package/dist/logger.js.map +1 -0
- package/dist/path-utils.d.ts +44 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +206 -0
- package/dist/path-utils.js.map +1 -0
- package/dist/protocol.d.ts +77 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +94 -0
- package/dist/protocol.js.map +1 -0
- package/dist/security.d.ts +32 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +228 -0
- package/dist/security.js.map +1 -0
- package/dist/tools.d.ts +36 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +498 -0
- package/dist/tools.js.map +1 -0
- package/package.json +36 -21
- package/src/config.js +0 -94
- package/src/http-fs.js +0 -178
- package/src/index.js +0 -439
- package/src/logger.js +0 -84
- package/src/methods/dialog.js +0 -84
- package/src/methods/fs.js +0 -674
- package/src/methods/git.js +0 -391
- package/src/methods/project.js +0 -57
- package/src/methods/scaffold.js +0 -131
- package/src/methods/skill.js +0 -347
- package/src/methods/system.js +0 -82
- package/src/methods/terminal.js +0 -234
- package/src/origin.js +0 -38
- package/src/pairing.js +0 -168
- package/src/rpc.js +0 -62
- package/src/sandbox.js +0 -178
package/src/index.js
DELETED
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* ysagc-agent 入口
|
|
4
|
-
* - WS 服务仅绑定 127.0.0.1(浏览器可信源)
|
|
5
|
-
* - 端口占用自动 +1(最多 5 次)
|
|
6
|
-
* - 控制通道 JSON-RPC 2.0;终端二进制分帧
|
|
7
|
-
* - 鉴权握手 3s 超时;心跳 15s、3 次无响应断开
|
|
8
|
-
* - 同设备最多 2 个控制连接(T-M0-15)
|
|
9
|
-
* 用法:npx ysagc-agent [--init-token <token>] [--port N]
|
|
10
|
-
*/
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
const http = require('http');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
const url = require('url');
|
|
16
|
-
const { WebSocketServer } = require('ws');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
|
|
20
|
-
const { loadConfig, saveConfig, CONFIG_DIR } = require('./config');
|
|
21
|
-
const logger = require('./logger');
|
|
22
|
-
const { register, handleRequest } = require('./rpc');
|
|
23
|
-
const { isOriginTrusted } = require('./origin');
|
|
24
|
-
const { registerSystemMethods } = require('./methods/system');
|
|
25
|
-
const { createTerminalMethods } = require('./methods/terminal');
|
|
26
|
-
const { createProjectMethods } = require('./methods/project');
|
|
27
|
-
const { createFsMethods } = require('./methods/fs');
|
|
28
|
-
const { createSkillMethods } = require('./methods/skill');
|
|
29
|
-
const { createGitMethods } = require('./methods/git');
|
|
30
|
-
const { createScaffoldMethods } = require('./methods/scaffold');
|
|
31
|
-
const { createDialogMethods } = require('./methods/dialog');
|
|
32
|
-
const { createHttpFsHandlers } = require('./http-fs');
|
|
33
|
-
const { Sandbox } = require('./sandbox');
|
|
34
|
-
const { startPairing, stopPairing, agentId } = require('./pairing');
|
|
35
|
-
|
|
36
|
-
const DEFAULT_PORT = 37890;
|
|
37
|
-
const MAX_PORT_TRIES = 5;
|
|
38
|
-
const AUTH_TIMEOUT_MS = 3000;
|
|
39
|
-
const HEARTBEAT_INTERVAL_MS = 15000;
|
|
40
|
-
const HEARTBEAT_MISS_LIMIT = 3;
|
|
41
|
-
const MAX_CONNS_PER_DEVICE = 2;
|
|
42
|
-
const RPC_RATE_LIMIT = 60; // RPC ≤ 60/min/设备(5.20.3)
|
|
43
|
-
const RPC_RATE_WINDOW = 60000;
|
|
44
|
-
|
|
45
|
-
// ---------------- CLI ----------------
|
|
46
|
-
const argv = process.argv.slice(2);
|
|
47
|
-
function getArg(name) {
|
|
48
|
-
const i = argv.indexOf(name);
|
|
49
|
-
return i >= 0 ? argv[i + 1] : null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const AGENT_VERSION = require('../package.json').version;
|
|
53
|
-
if (argv.includes('--help') || argv.includes('-h')) {
|
|
54
|
-
console.log(`ysagc-agent v${AGENT_VERSION} — 代码工作台本机代理
|
|
55
|
-
用法: npx ysagc-agent [选项]
|
|
56
|
-
|
|
57
|
-
选项:
|
|
58
|
-
--port N 指定监听端口(默认 37890,被占用自动 +1)
|
|
59
|
-
--init-token <t> 把指定 token 加入白名单(兼容旧版配对,一般无需使用)
|
|
60
|
-
--help, -h 显示帮助
|
|
61
|
-
--version, -v 显示版本
|
|
62
|
-
|
|
63
|
-
启动后无需配对码:浏览器打开网站「工作台」即可自动识别本机代理并连接。
|
|
64
|
-
代理仅监听 127.0.0.1,替浏览器在本机执行文件 / Git / 终端操作。`);
|
|
65
|
-
process.exit(0);
|
|
66
|
-
}
|
|
67
|
-
if (argv.includes('--version') || argv.includes('-v')) {
|
|
68
|
-
console.log(AGENT_VERSION);
|
|
69
|
-
process.exit(0);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---------------- 配置 ----------------
|
|
73
|
-
let config = loadConfig();
|
|
74
|
-
logger.setLevel(config.logLevel);
|
|
75
|
-
|
|
76
|
-
const initToken = getArg('--init-token');
|
|
77
|
-
if (initToken) {
|
|
78
|
-
const hash = crypto.createHash('sha256').update(String(initToken)).digest('hex');
|
|
79
|
-
if (!config.deviceTokenWhitelist.includes(hash)) {
|
|
80
|
-
config = saveConfig({ deviceTokenWhitelist: [...config.deviceTokenWhitelist, hash] });
|
|
81
|
-
logger.info(`[init-token] 已加入设备令牌白名单(sha256: ${hash.slice(0, 16)}...)`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (getArg('--port')) {
|
|
85
|
-
config = saveConfig({ port: Number(getArg('--port')) || DEFAULT_PORT });
|
|
86
|
-
}
|
|
87
|
-
const configRef = { config }; // 供方法层读取最新配置
|
|
88
|
-
const sandbox = new Sandbox(config.allowedRoots || []);
|
|
89
|
-
configRef.sandbox = sandbox;
|
|
90
|
-
|
|
91
|
-
// ---------------- RPC 注册 ----------------
|
|
92
|
-
registerSystemMethods(register, () => configRef);
|
|
93
|
-
const terminal = createTerminalMethods(() => configRef);
|
|
94
|
-
terminal.register(register);
|
|
95
|
-
const projectMethods = createProjectMethods(() => configRef);
|
|
96
|
-
projectMethods.register(register);
|
|
97
|
-
const fsMethods = createFsMethods(() => configRef);
|
|
98
|
-
fsMethods.register(register);
|
|
99
|
-
const skillMethods = createSkillMethods(() => configRef);
|
|
100
|
-
skillMethods.register(register);
|
|
101
|
-
const gitMethods = createGitMethods(() => configRef);
|
|
102
|
-
gitMethods.register(register);
|
|
103
|
-
const scaffoldMethods = createScaffoldMethods(() => configRef);
|
|
104
|
-
scaffoldMethods.register(register);
|
|
105
|
-
const dialogMethods = createDialogMethods();
|
|
106
|
-
dialogMethods.register(register);
|
|
107
|
-
|
|
108
|
-
// ---------------- WS 服务 ----------------
|
|
109
|
-
const state = {
|
|
110
|
-
/** deviceId -> Set<ws> 控制连接 */
|
|
111
|
-
deviceConns: new Map(),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
function sha256(s) {
|
|
115
|
-
return crypto.createHash('sha256').update(String(s)).digest('hex');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function checkAuthToken(token) {
|
|
119
|
-
if (!token || typeof token !== 'string') return false;
|
|
120
|
-
return configRef.config.deviceTokenWhitelist.includes(sha256(token));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 客户端 → Agent 的二进制帧解析(终端输入):
|
|
125
|
-
* 4 字节大端头长 + JSON 头 {channel:'terminal', sessionId, type} + payload
|
|
126
|
-
*/
|
|
127
|
-
function parseBinaryFrame(buffer) {
|
|
128
|
-
if (buffer.length < 5) return null;
|
|
129
|
-
const headerLen = buffer.readUInt32BE(0);
|
|
130
|
-
if (buffer.length < 4 + headerLen) return null;
|
|
131
|
-
let header;
|
|
132
|
-
try {
|
|
133
|
-
header = JSON.parse(buffer.slice(4, 4 + headerLen).toString('utf8'));
|
|
134
|
-
} catch {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
return { header, payload: buffer.slice(4 + headerLen) };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function buildBinaryFrame(header, payload) {
|
|
141
|
-
const h = Buffer.from(JSON.stringify(header), 'utf8');
|
|
142
|
-
const len = Buffer.alloc(4);
|
|
143
|
-
len.writeUInt32BE(h.length, 0);
|
|
144
|
-
return Buffer.concat([len, h, payload]);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function sendJson(ws, obj) {
|
|
148
|
-
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(obj));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function closeWith(ws, code, reason) {
|
|
152
|
-
try {
|
|
153
|
-
ws.close(code, reason);
|
|
154
|
-
} catch {
|
|
155
|
-
ws.terminate();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function setupConnection(ws, req) {
|
|
160
|
-
ws.isAlive = true;
|
|
161
|
-
ws.authed = false;
|
|
162
|
-
ws.deviceId = null;
|
|
163
|
-
// 免配对码直连:Origin 可信的浏览器连接无需 deviceToken(v0.2.0)
|
|
164
|
-
ws.originTrusted = isOriginTrusted(req.headers.origin, configRef.config);
|
|
165
|
-
ws.authTimer = setTimeout(() => {
|
|
166
|
-
if (!ws.authed) closeWith(ws, 4401, 'auth timeout');
|
|
167
|
-
}, AUTH_TIMEOUT_MS);
|
|
168
|
-
|
|
169
|
-
ws.on('pong', () => {
|
|
170
|
-
ws.isAlive = true;
|
|
171
|
-
ws.missed = 0;
|
|
172
|
-
});
|
|
173
|
-
ws.missed = 0;
|
|
174
|
-
|
|
175
|
-
ws.on('message', async (data, isBinary) => {
|
|
176
|
-
if (isBinary) {
|
|
177
|
-
const frame = parseBinaryFrame(data);
|
|
178
|
-
if (!frame || !ws.authed) return;
|
|
179
|
-
if (frame.header && frame.header.channel === 'terminal' && frame.header.sessionId) {
|
|
180
|
-
// 终端输入 → 对应 pty 会话
|
|
181
|
-
const s = terminal.sessions.get(frame.header.sessionId);
|
|
182
|
-
if (s && frame.payload.length > 0) s.pty.write(frame.payload.toString('utf8'));
|
|
183
|
-
}
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
let msg;
|
|
187
|
-
try {
|
|
188
|
-
msg = JSON.parse(data.toString('utf8'));
|
|
189
|
-
} catch {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ---- 鉴权握手 ----
|
|
194
|
-
if (msg && msg.type === 'auth') {
|
|
195
|
-
if (ws.authed) return;
|
|
196
|
-
const tokenOk = checkAuthToken(msg.token);
|
|
197
|
-
const autoOk = ws.originTrusted && (msg.mode === 'auto' || !msg.token);
|
|
198
|
-
if (!tokenOk && !autoOk) {
|
|
199
|
-
logger.warn(`[ws] 鉴权失败:token 不在白名单且 Origin 不受信任(origin=${req.headers.origin || 'none'} remote=${req.socket.remoteAddress})`);
|
|
200
|
-
closeWith(ws, 4401, 'unauthorized');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
ws.authed = true;
|
|
204
|
-
ws.deviceId = msg.deviceId || 'dev-default';
|
|
205
|
-
if (ws.authTimer) clearTimeout(ws.authTimer);
|
|
206
|
-
// ---- 多连接限制(T-M0-15)----
|
|
207
|
-
let set = state.deviceConns.get(ws.deviceId);
|
|
208
|
-
if (!set) {
|
|
209
|
-
set = new Set();
|
|
210
|
-
state.deviceConns.set(ws.deviceId, set);
|
|
211
|
-
}
|
|
212
|
-
const existing = [...set].filter((c) => c !== ws && c.readyState === c.OPEN);
|
|
213
|
-
if (existing.length >= MAX_CONNS_PER_DEVICE) {
|
|
214
|
-
sendJson(ws, { jsonrpc: '2.0', method: 'event.connection.limit', params: { max: MAX_CONNS_PER_DEVICE, code: 'E_CONNECTION_LIMIT' } });
|
|
215
|
-
closeWith(ws, 4403, 'connection limit');
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
set.add(ws);
|
|
219
|
-
if (existing.length > 0) {
|
|
220
|
-
// 旧连接收到提示
|
|
221
|
-
sendJson(existing[0], { jsonrpc: '2.0', method: 'event.connection.duplicated', params: { hint: '已在其他窗口打开' } });
|
|
222
|
-
}
|
|
223
|
-
sendJson(ws, {
|
|
224
|
-
jsonrpc: '2.0',
|
|
225
|
-
method: 'event.auth.ok',
|
|
226
|
-
params: {
|
|
227
|
-
agentVersion: require('../package.json').version,
|
|
228
|
-
protocolVersion: 1,
|
|
229
|
-
port: configRef.config.port,
|
|
230
|
-
platform: `${process.platform} ${process.arch}`,
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
logger.info(`[ws] 设备 ${ws.deviceId} 已连接(当前 ${existing.length + 1} 连接)`);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!ws.authed) {
|
|
238
|
-
// 未鉴权连接仅放行配对握手(新设备尚无 deviceToken,用配对码换取令牌)
|
|
239
|
-
if (msg && msg.jsonrpc === '2.0' && msg.method === 'pairing.handshake') {
|
|
240
|
-
const resp = await handleRequest(msg, {
|
|
241
|
-
socket: req.socket,
|
|
242
|
-
ws,
|
|
243
|
-
deviceId: null,
|
|
244
|
-
send: (obj) => sendJson(ws, obj),
|
|
245
|
-
sendBinary: null,
|
|
246
|
-
});
|
|
247
|
-
if (resp) sendJson(ws, resp);
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
// ---- JSON-RPC(5.20.3 频率限制:RPC ≤60/min/设备)----
|
|
252
|
-
if (msg && msg.jsonrpc === '2.0' && msg.method) {
|
|
253
|
-
const now = Date.now();
|
|
254
|
-
if (!ws.rpcWindow) {
|
|
255
|
-
ws.rpcWindow = now;
|
|
256
|
-
ws.rpcCount = 0;
|
|
257
|
-
}
|
|
258
|
-
if (now - ws.rpcWindow > RPC_RATE_WINDOW) {
|
|
259
|
-
ws.rpcWindow = now;
|
|
260
|
-
ws.rpcCount = 0;
|
|
261
|
-
}
|
|
262
|
-
ws.rpcCount++;
|
|
263
|
-
if (ws.rpcCount > RPC_RATE_LIMIT) {
|
|
264
|
-
logger.warn(`[ws] RPC 频率超限(${ws.rpcCount}/${RPC_RATE_LIMIT} per min),拒绝: ${msg.method}`);
|
|
265
|
-
sendJson(ws, {
|
|
266
|
-
jsonrpc: '2.0',
|
|
267
|
-
id: msg.id ?? null,
|
|
268
|
-
error: { code: -32603, message: '请求过于频繁,请稍后再试', data: { code: 'E_RATE_LIMIT' } },
|
|
269
|
-
});
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const resp = await handleRequest(msg, {
|
|
273
|
-
socket: req.socket,
|
|
274
|
-
ws,
|
|
275
|
-
deviceId: ws.deviceId,
|
|
276
|
-
send: (obj) => sendJson(ws, obj),
|
|
277
|
-
sendBinary: (buf, opts) => {
|
|
278
|
-
if (ws.readyState === ws.OPEN) ws.send(buf, { binary: true });
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
if (resp) sendJson(ws, resp);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
ws.on('close', () => {
|
|
286
|
-
if (ws.authTimer) clearTimeout(ws.authTimer);
|
|
287
|
-
if (ws.deviceId) {
|
|
288
|
-
const set = state.deviceConns.get(ws.deviceId);
|
|
289
|
-
if (set) {
|
|
290
|
-
set.delete(ws);
|
|
291
|
-
if (set.size === 0) state.deviceConns.delete(ws.deviceId);
|
|
292
|
-
}
|
|
293
|
-
logger.info(`[ws] 设备 ${ws.deviceId} 连接断开(剩余 ${set ? set.size : 0} 连接)`);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
ws.on('error', () => {
|
|
298
|
-
/* 连接错误由 close 统一处理 */
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function tryListen(port, triesLeft) {
|
|
303
|
-
const httpFs = createHttpFsHandlers(() => configRef);
|
|
304
|
-
const server = http.createServer((req, res) => {
|
|
305
|
-
const parsed = url.parse(req.url, true);
|
|
306
|
-
const p = parsed.pathname || '';
|
|
307
|
-
const origin = req.headers.origin || '';
|
|
308
|
-
|
|
309
|
-
// CORS 预检(浏览器跨源 fetch 到 http://127.0.0.1 需要)
|
|
310
|
-
if (req.method === 'OPTIONS') {
|
|
311
|
-
res.writeHead(204, {
|
|
312
|
-
'Access-Control-Allow-Origin': origin || '*',
|
|
313
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
314
|
-
'Access-Control-Allow-Headers': 'Content-Type, X-Offset, X-Total, Authorization',
|
|
315
|
-
'Access-Control-Max-Age': '86400',
|
|
316
|
-
});
|
|
317
|
-
res.end();
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
const corsHeaders = (h) => {
|
|
321
|
-
h['Access-Control-Allow-Origin'] = origin || '*';
|
|
322
|
-
return h;
|
|
323
|
-
};
|
|
324
|
-
try {
|
|
325
|
-
// 免配对码直连:浏览器探测本机 Agent 信息(v0.2.0)
|
|
326
|
-
if (req.method === 'GET' && p === '/api/bootstrap') {
|
|
327
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
328
|
-
res.end(JSON.stringify({
|
|
329
|
-
agentId: agentId(),
|
|
330
|
-
agentVersion: require('../package.json').version,
|
|
331
|
-
protocolVersion: configRef.config.protocolVersion || 2,
|
|
332
|
-
port: configRef.config.port,
|
|
333
|
-
platform: `${process.platform} ${process.arch}`,
|
|
334
|
-
nodeVersion: process.version,
|
|
335
|
-
hostname: require('os').hostname(),
|
|
336
|
-
paired: (configRef.config.deviceTokenWhitelist || []).length > 0,
|
|
337
|
-
allowAuto: true,
|
|
338
|
-
}));
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (req.method === 'POST' && p === '/api/fs/upload') return httpFs.handleUpload(req, res, parsed.query);
|
|
342
|
-
if (req.method === 'GET' && p === '/api/fs/download') return httpFs.handleDownload(req, res, parsed.query);
|
|
343
|
-
if (req.method === 'GET' && p === '/api/fs/zip') return httpFs.handleZip(req, res, parsed.query);
|
|
344
|
-
} catch (e) {
|
|
345
|
-
logger.error(`[http] 处理 ${p} 失败: ${e && e.message}`);
|
|
346
|
-
res.writeHead(500, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
347
|
-
res.end(JSON.stringify({ code: 'E_INTERNAL', msg: e && e.message }));
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
res.writeHead(404, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
351
|
-
res.end(JSON.stringify({ error: 'not found' }));
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const wss = new WebSocketServer({ server, maxPayload: 1024 * 1024 });
|
|
355
|
-
wss.on('connection', setupConnection);
|
|
356
|
-
// 广播通道(fs 监听等事件推送到所有已鉴权连接)
|
|
357
|
-
configRef.broadcast = (obj) => {
|
|
358
|
-
for (const c of wss.clients) {
|
|
359
|
-
if (c.authed && c.readyState === c.OPEN) sendJson(c, obj);
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
server.on('error', (err) => {
|
|
364
|
-
if (err.code === 'EADDRINUSE' && triesLeft > 0) {
|
|
365
|
-
logger.warn(`[ws] 端口 ${port} 被占用,尝试 ${port + 1}`);
|
|
366
|
-
tryListen(port + 1, triesLeft - 1);
|
|
367
|
-
} else {
|
|
368
|
-
logger.error(`[ws] 启动失败: ${err.message}`);
|
|
369
|
-
process.exit(1);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
server.listen(port, '127.0.0.1', () => {
|
|
374
|
-
configRef.config = saveConfig({ port });
|
|
375
|
-
logger.info(`[ws] Agent 已启动 ws://127.0.0.1:${port}`);
|
|
376
|
-
console.log(`ysagc-agent 运行中: ws://127.0.0.1:${port}`);
|
|
377
|
-
// ---- 心跳 ----
|
|
378
|
-
const heartbeat = setInterval(() => {
|
|
379
|
-
for (const ws of wss.clients) {
|
|
380
|
-
if (ws.isAlive === false) {
|
|
381
|
-
ws.terminate();
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
ws.isAlive = false;
|
|
385
|
-
ws.missed = (ws.missed || 0) + 1;
|
|
386
|
-
if (ws.missed > HEARTBEAT_MISS_LIMIT) {
|
|
387
|
-
ws.terminate();
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
try {
|
|
391
|
-
ws.ping();
|
|
392
|
-
} catch {
|
|
393
|
-
/* ignore */
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
397
|
-
heartbeat.unref();
|
|
398
|
-
// ---- 配对码 ----
|
|
399
|
-
startPairing(() => configRef.config);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
return server;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
console.log(`ysagc-agent v${AGENT_VERSION} 启动中(配置目录: ${CONFIG_DIR},端口: ${config.port})`);
|
|
406
|
-
console.log(`免配对码直连已启用:打开网站「工作台」即可自动识别本机代理(agentId: ${agentId().slice(0, 8)}...)`);
|
|
407
|
-
console.log('提示: 输入 --help 查看完整选项。');
|
|
408
|
-
tryListen(config.port, MAX_PORT_TRIES);
|
|
409
|
-
|
|
410
|
-
process.on('SIGINT', () => {
|
|
411
|
-
stopPairing();
|
|
412
|
-
logger.info('[agent] 收到 SIGINT,退出');
|
|
413
|
-
process.exit(0);
|
|
414
|
-
});
|
|
415
|
-
process.on('SIGTERM', () => {
|
|
416
|
-
stopPairing();
|
|
417
|
-
logger.info('[agent] 收到 SIGTERM,退出');
|
|
418
|
-
process.exit(0);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Agent 守护进程韧性:单个会话/pty 的异步错误不得打崩整个 Agent。
|
|
422
|
-
// 快速连续崩溃(如 10 秒内 5 次)视为致命,退出。
|
|
423
|
-
let crashCount = 0;
|
|
424
|
-
let crashWindowStart = 0;
|
|
425
|
-
function guardCrash(level, err) {
|
|
426
|
-
const now = Date.now();
|
|
427
|
-
if (now - crashWindowStart > 10000) {
|
|
428
|
-
crashWindowStart = now;
|
|
429
|
-
crashCount = 0;
|
|
430
|
-
}
|
|
431
|
-
crashCount++;
|
|
432
|
-
logger.error(`[guard:${level}]`, err);
|
|
433
|
-
if (crashCount >= 5) {
|
|
434
|
-
logger.error('[guard] 短时间多次致命异常,Agent 退出');
|
|
435
|
-
process.exit(1);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
process.on('uncaughtException', (err) => guardCrash('uncaughtException', err));
|
|
439
|
-
process.on('unhandledRejection', (reason) => guardCrash('unhandledRejection', reason));
|
package/src/logger.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 滚动日志:~/.ysagc-agent/logs/agent.log(按日滚动,保留 7 份)
|
|
3
|
-
* 红线:永不记录密钥文件内容、Git 凭证、密码类数据
|
|
4
|
-
*/
|
|
5
|
-
'use strict';
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const { CONFIG_DIR } = require('./config');
|
|
10
|
-
|
|
11
|
-
const LOG_DIR = path.join(CONFIG_DIR, 'logs');
|
|
12
|
-
const LOG_FILE = path.join(LOG_DIR, 'agent.log');
|
|
13
|
-
const MAX_KEEP = 7;
|
|
14
|
-
const MAX_SIZE = 5 * 1024 * 1024; // 5MB 滚动
|
|
15
|
-
|
|
16
|
-
let levelRank = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
17
|
-
let currentLevel = 'info';
|
|
18
|
-
|
|
19
|
-
function ensureLogDir() {
|
|
20
|
-
try {
|
|
21
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
22
|
-
} catch {
|
|
23
|
-
/* 日志目录创建失败时降级为 console */
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function rotateIfNeeded() {
|
|
28
|
-
try {
|
|
29
|
-
if (!fs.existsSync(LOG_FILE)) return;
|
|
30
|
-
const stat = fs.statSync(LOG_FILE);
|
|
31
|
-
if (stat.size < MAX_SIZE) return;
|
|
32
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
33
|
-
fs.renameSync(LOG_FILE, path.join(LOG_DIR, `agent.log.${stamp}`));
|
|
34
|
-
// 清理旧日志,保留最近 MAX_KEEP 份
|
|
35
|
-
const backups = fs
|
|
36
|
-
.readdirSync(LOG_DIR)
|
|
37
|
-
.filter((f) => f.startsWith('agent.log.') && f !== LOG_FILE)
|
|
38
|
-
.map((f) => ({ f, t: fs.statSync(path.join(LOG_DIR, f)).mtimeMs }))
|
|
39
|
-
.sort((a, b) => b.t - a.t);
|
|
40
|
-
for (const b of backups.slice(MAX_KEEP)) {
|
|
41
|
-
try {
|
|
42
|
-
fs.unlinkSync(path.join(LOG_DIR, b.f));
|
|
43
|
-
} catch {
|
|
44
|
-
/* ignore */
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
/* ignore */
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sanitize(msg) {
|
|
53
|
-
// 脱敏:常见的密钥/凭证形态(预防性兜底,正常流程不应写入)
|
|
54
|
-
return String(msg).replace(
|
|
55
|
-
/(sk-[A-Za-z0-9_\-]{8,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[bap]-[A-Za-z0-9\-]{10,}|password\s*[:=]\s*\S+)/gi,
|
|
56
|
-
'[REDACTED]'
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function write(level, args) {
|
|
61
|
-
if (levelRank[level] < levelRank[currentLevel]) return;
|
|
62
|
-
ensureLogDir();
|
|
63
|
-
rotateIfNeeded();
|
|
64
|
-
const line = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${args
|
|
65
|
-
.map((a) => (a instanceof Error ? a.stack || a.message : typeof a === 'object' ? JSON.stringify(a) : String(a)))
|
|
66
|
-
.join(' ')}`;
|
|
67
|
-
try {
|
|
68
|
-
fs.appendFileSync(LOG_FILE, sanitize(line) + '\n');
|
|
69
|
-
} catch {
|
|
70
|
-
/* ignore */
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const logger = {
|
|
75
|
-
setLevel(level) {
|
|
76
|
-
if (levelRank[level]) currentLevel = level;
|
|
77
|
-
},
|
|
78
|
-
debug: (...a) => write('debug', a),
|
|
79
|
-
info: (...a) => write('info', a),
|
|
80
|
-
warn: (...a) => write('warn', a),
|
|
81
|
-
error: (...a) => write('error', a),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
module.exports = logger;
|
package/src/methods/dialog.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dialog.* RPC 方法:本机原生对话框(T-M3-xx)
|
|
3
|
-
* - dialog.pickFolder {} → { path } 打开系统文件夹选择器,返回绝对路径(取消返回 E_DIALOG_CANCELED)
|
|
4
|
-
* 实现:
|
|
5
|
-
* Windows: PowerShell FolderBrowserDialog(Agent 运行在用户本机,可弹原生窗口)
|
|
6
|
-
* macOS: osascript choose folder
|
|
7
|
-
* Linux: zenity / kdialog(依次尝试)
|
|
8
|
-
* 用途:添加项目时"点击选择文件夹"替代手输绝对路径(需求 4.3.1 方式 A)
|
|
9
|
-
*/
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const { execFile } = require('child_process');
|
|
13
|
-
const { RpcError } = require('../rpc');
|
|
14
|
-
|
|
15
|
-
const DIALOG_TIMEOUT_MS = 5 * 60 * 1000; // 用户可能挑很久,5 分钟上限
|
|
16
|
-
|
|
17
|
-
function run(cmd, args, timeoutMs) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
execFile(
|
|
20
|
-
cmd,
|
|
21
|
-
args,
|
|
22
|
-
{ timeout: timeoutMs || DIALOG_TIMEOUT_MS, windowsHide: true, maxBuffer: 4 * 1024 * 1024 },
|
|
23
|
-
(err, stdout) => {
|
|
24
|
-
if (err) return reject(err);
|
|
25
|
-
resolve(String(stdout || ''));
|
|
26
|
-
}
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Windows:PowerShell FolderBrowserDialog */
|
|
32
|
-
async function pickFolderWindows() {
|
|
33
|
-
const ps = [
|
|
34
|
-
'Add-Type -AssemblyName System.Windows.Forms',
|
|
35
|
-
"$f = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
36
|
-
"$f.Description = '选择要添加到工作台的项目文件夹'",
|
|
37
|
-
'$f.ShowNewFolderButton = $true',
|
|
38
|
-
"if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.SelectedPath }",
|
|
39
|
-
].join('; ');
|
|
40
|
-
const out = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-STA', '-Command', ps]);
|
|
41
|
-
const p = String(out || '').trim().split(/\r?\n/).filter(Boolean).pop() || '';
|
|
42
|
-
return p;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** macOS:osascript choose folder */
|
|
46
|
-
async function pickFolderMac() {
|
|
47
|
-
const out = await run('osascript', ['-e', 'POSIX path of (choose folder with prompt "选择要添加到工作台的项目文件夹")']);
|
|
48
|
-
return String(out || '').trim();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Linux:zenity → kdialog 依次尝试 */
|
|
52
|
-
async function pickFolderLinux() {
|
|
53
|
-
for (const [cmd, args] of [
|
|
54
|
-
['zenity', ['--file-selection', '--directory', '--title=选择要添加到工作台的项目文件夹']],
|
|
55
|
-
['kdialog', ['--getexistingdirectory', '选择要添加到工作台的项目文件夹']],
|
|
56
|
-
]) {
|
|
57
|
-
try {
|
|
58
|
-
const out = await run(cmd, args, 30000);
|
|
59
|
-
const p = String(out || '').trim();
|
|
60
|
-
if (p) return p;
|
|
61
|
-
} catch {
|
|
62
|
-
/* 尝试下一种 */
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
throw new RpcError('E_DIALOG_UNSUPPORTED', '当前系统未安装文件夹选择工具(zenity/kdialog),请手动输入路径');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function createDialogMethods() {
|
|
69
|
-
return {
|
|
70
|
-
register(reg) {
|
|
71
|
-
reg('dialog.pickFolder', async () => {
|
|
72
|
-
let p = '';
|
|
73
|
-
if (process.platform === 'win32') p = await pickFolderWindows();
|
|
74
|
-
else if (process.platform === 'darwin') p = await pickFolderMac();
|
|
75
|
-
else p = await pickFolderLinux();
|
|
76
|
-
p = String(p || '').trim();
|
|
77
|
-
if (!p) throw new RpcError('E_DIALOG_CANCELED', '已取消选择文件夹');
|
|
78
|
-
return { path: p.replace(/\\/g, '/') };
|
|
79
|
-
});
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
module.exports = { createDialogMethods };
|