@brierb/brier-cli 0.0.6 → 0.0.8
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/dist/commands/log.js +28 -0
- package/dist/commands/restart.js +9 -4
- package/dist/commands/run.js +13 -0
- package/dist/commands/start.js +2 -2
- package/dist/commands/status.js +11 -16
- package/dist/commands/stop.js +1 -1
- package/dist/config/credentials.js +25 -0
- package/dist/config/index.js +16 -0
- package/dist/config/load.js +26 -0
- package/dist/config/paths.js +14 -0
- package/dist/config/version.js +13 -0
- package/dist/core/index.js +11 -0
- package/dist/core/runtimes.js +74 -0
- package/dist/daemon/DaemonManager.js +65 -66
- package/dist/daemon/DaemonRunner.js +97 -39
- package/dist/daemon/OutputBatcher.js +77 -0
- package/dist/daemon/TaskExecutor.js +79 -10
- package/dist/daemon/index.js +13 -0
- package/dist/daemon/state.js +100 -0
- package/dist/definitions/daemon.js +5 -0
- package/dist/definitions/index.js +12 -0
- package/dist/definitions/tunnel.js +9 -0
- package/dist/index.js +12 -31
- package/dist/tunnel/backoff.js +21 -0
- package/dist/tunnel/dispatcher.js +35 -0
- package/dist/tunnel/heartbeat.js +55 -0
- package/dist/tunnel/index.js +220 -0
- package/dist/tunnel/transport.js +112 -0
- package/dist/tunnel/url.js +10 -0
- package/package.json +3 -3
- package/types/commands/log.d.ts +5 -0
- package/types/commands/run.d.ts +5 -0
- package/types/commands/status.d.ts +1 -0
- package/types/config/credentials.d.ts +8 -0
- package/types/config/index.d.ts +16 -0
- package/types/config/load.d.ts +7 -0
- package/types/config/paths.d.ts +12 -0
- package/types/config/version.d.ts +2 -0
- package/types/core/index.d.ts +11 -0
- package/types/core/runtimes.d.ts +29 -0
- package/types/daemon/DaemonManager.d.ts +1 -1
- package/types/daemon/OutputBatcher.d.ts +39 -0
- package/types/daemon/TaskExecutor.d.ts +4 -2
- package/types/daemon/index.d.ts +13 -0
- package/types/daemon/state.d.ts +19 -0
- package/types/definitions/daemon.d.ts +58 -0
- package/types/definitions/index.d.ts +17 -0
- package/types/definitions/task.d.ts +25 -0
- package/types/definitions/tunnel.d.ts +112 -0
- package/types/tunnel/backoff.d.ts +32 -0
- package/types/tunnel/dispatcher.d.ts +26 -0
- package/types/tunnel/heartbeat.d.ts +36 -0
- package/types/tunnel/index.d.ts +38 -0
- package/types/tunnel/transport.d.ts +29 -0
- package/types/tunnel/url.d.ts +5 -0
- package/dist/config.js +0 -81
- package/dist/runtimes.js +0 -29
- package/dist/tunnel/TunnelClient.js +0 -218
- package/types/config.d.ts +0 -9
- package/types/runtimes.d.ts +0 -14
- package/types/tunnel/TunnelClient.d.ts +0 -10
- package/types/types.d.ts +0 -78
- /package/dist/{logger.js → core/logger.js} +0 -0
- /package/dist/{types.js → definitions/task.js} +0 -0
- /package/types/{logger.d.ts → core/logger.d.ts} +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { toWsUrl } from './url.js';
|
|
2
|
+
import { logger } from '../core/index.js';
|
|
3
|
+
import { createBackoff } from './backoff.js';
|
|
4
|
+
import { dispatchServerMessage } from './dispatcher.js';
|
|
5
|
+
import { createHeartbeat } from './heartbeat.js';
|
|
6
|
+
import { createWebSocketTransport } from './transport.js';
|
|
7
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
8
|
+
const HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
9
|
+
/** auth 消息发出后等待 auth-ok/auth-failed 的最长时间,超时视为握手失败并重连 */
|
|
10
|
+
const AUTH_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
11
|
+
const BASE_RECONNECT_DELAY_MS = 1_000;
|
|
12
|
+
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
13
|
+
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
14
|
+
/**
|
|
15
|
+
* 隧道骨架:连接生命周期编排与状态仲裁的唯一入口。
|
|
16
|
+
*
|
|
17
|
+
* 本文件只做“编排”,不接触 ws 细节、不算退避、不解析消息:
|
|
18
|
+
* - transport:负责底层连接与收发帧
|
|
19
|
+
* - heartbeat:负责心跳节奏(只上报不决策)
|
|
20
|
+
* - backoff:负责重连延迟计算(纯逻辑)
|
|
21
|
+
* - dispatcher:负责服务端消息解析与分发
|
|
22
|
+
*
|
|
23
|
+
* 关键时序不变式(重构后保持与原实现一致):
|
|
24
|
+
* 1. 心跳仅在 auth-ok 后启动;
|
|
25
|
+
* 2. close 后仅当 running 才退避重连,stop() 后绝不再连;
|
|
26
|
+
* 3. 仅 auth-ok 归零退避计数;
|
|
27
|
+
* 4. stop 时先优雅 close,3s 兜底 terminate。
|
|
28
|
+
*/
|
|
29
|
+
export const createTunnelClient = (config, messageHandlers) => {
|
|
30
|
+
const transport = createWebSocketTransport();
|
|
31
|
+
const backoff = createBackoff({
|
|
32
|
+
baseMs: BASE_RECONNECT_DELAY_MS,
|
|
33
|
+
maxMs: MAX_RECONNECT_DELAY_MS,
|
|
34
|
+
maxAttempts: MAX_RECONNECT_ATTEMPTS,
|
|
35
|
+
});
|
|
36
|
+
let state = 'disconnected';
|
|
37
|
+
let running = false;
|
|
38
|
+
let reconnectTimer = null;
|
|
39
|
+
/** auth 握手超时计时器(open 后启动,auth-ok/auth-failed/close 时清除) */
|
|
40
|
+
let handshakeTimer = null;
|
|
41
|
+
/**
|
|
42
|
+
* 本次运行是否以“错误”终止(auth 失败 / 重连耗尽)。
|
|
43
|
+
* 区分于 stop() 的正常停止:保证随后的 close 事件不把 error 覆盖成 disconnected。
|
|
44
|
+
*/
|
|
45
|
+
let exitAsError = false;
|
|
46
|
+
const listeners = new Set();
|
|
47
|
+
const notifyStateChange = (newState) => {
|
|
48
|
+
state = newState;
|
|
49
|
+
for (const callback of listeners) {
|
|
50
|
+
callback(newState);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const onStateChange = (callback) => {
|
|
54
|
+
listeners.add(callback);
|
|
55
|
+
return () => {
|
|
56
|
+
listeners.delete(callback);
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const getState = () => state;
|
|
60
|
+
const send = (message) => transport.send(JSON.stringify(message));
|
|
61
|
+
const sendHeartbeat = () => send({ type: 'heartbeat', timestamp: Date.now() });
|
|
62
|
+
const clearHandshakeTimer = () => {
|
|
63
|
+
if (handshakeTimer) {
|
|
64
|
+
clearTimeout(handshakeTimer);
|
|
65
|
+
handshakeTimer = null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* 启动握手超时:若服务端不回 auth-ok/auth-failed,10s 后主动断开走重连,
|
|
70
|
+
* 避免永久停留在 connecting(此时心跳尚未启动,无其他自愈路径)。
|
|
71
|
+
*/
|
|
72
|
+
const armHandshakeTimeout = () => {
|
|
73
|
+
clearHandshakeTimer();
|
|
74
|
+
handshakeTimer = setTimeout(() => {
|
|
75
|
+
logger.warn('Auth handshake timed out, reconnecting');
|
|
76
|
+
transport.close(4000, 'auth timeout');
|
|
77
|
+
}, AUTH_HANDSHAKE_TIMEOUT_MS);
|
|
78
|
+
};
|
|
79
|
+
const heartbeat = createHeartbeat({
|
|
80
|
+
intervalMs: HEARTBEAT_INTERVAL_MS,
|
|
81
|
+
timeoutMs: HEARTBEAT_TIMEOUT_MS,
|
|
82
|
+
onBeat: sendHeartbeat,
|
|
83
|
+
onTimeout: () => {
|
|
84
|
+
logger.warn('Heartbeat timeout, forcing reconnect');
|
|
85
|
+
transport.close(4000, 'heartbeat timeout');
|
|
86
|
+
},
|
|
87
|
+
onBeatError: (error) => {
|
|
88
|
+
logger.error('Heartbeat send failed:', error);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const connect = () => {
|
|
92
|
+
logger.info('Connecting to', toWsUrl(config.serverUrl));
|
|
93
|
+
notifyStateChange('connecting');
|
|
94
|
+
transport.connect(toWsUrl(config.serverUrl), {
|
|
95
|
+
Authorization: `Bearer ${config.token}`,
|
|
96
|
+
'X-Brier-Hostname': config.hostname,
|
|
97
|
+
'X-Brier-OS': config.os,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const scheduleReconnect = () => {
|
|
101
|
+
if (!running)
|
|
102
|
+
return;
|
|
103
|
+
const delay = backoff.next();
|
|
104
|
+
if (delay < 0) {
|
|
105
|
+
logger.error('Max reconnect attempts reached, stopping');
|
|
106
|
+
exitAsError = true;
|
|
107
|
+
running = false;
|
|
108
|
+
notifyStateChange('error');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
logger.info(`Reconnecting in ${(delay / 1000).toFixed(0)}s (attempt ${backoff.attempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
112
|
+
notifyStateChange('reconnecting');
|
|
113
|
+
reconnectTimer = setTimeout(() => {
|
|
114
|
+
if (running) {
|
|
115
|
+
connect();
|
|
116
|
+
}
|
|
117
|
+
}, delay);
|
|
118
|
+
};
|
|
119
|
+
const dispatchHandlers = {
|
|
120
|
+
onAuthOk: (computerId) => {
|
|
121
|
+
clearHandshakeTimer();
|
|
122
|
+
logger.info('Tunnel authenticated, computerId:', computerId);
|
|
123
|
+
backoff.reset();
|
|
124
|
+
notifyStateChange('connected');
|
|
125
|
+
heartbeat.start();
|
|
126
|
+
},
|
|
127
|
+
onAuthFailed: (reason) => {
|
|
128
|
+
clearHandshakeTimer();
|
|
129
|
+
logger.error('Authentication failed:', reason);
|
|
130
|
+
exitAsError = true;
|
|
131
|
+
running = false;
|
|
132
|
+
notifyStateChange('error');
|
|
133
|
+
},
|
|
134
|
+
onHeartbeatAck: () => {
|
|
135
|
+
heartbeat.ack();
|
|
136
|
+
},
|
|
137
|
+
onTaskStart: (message) => {
|
|
138
|
+
logger.info('Task start:', message.taskId, message.command);
|
|
139
|
+
messageHandlers.onTaskStart(message);
|
|
140
|
+
},
|
|
141
|
+
onTaskCancel: (taskId) => {
|
|
142
|
+
logger.info('Task cancel:', taskId);
|
|
143
|
+
messageHandlers.onTaskCancel(taskId);
|
|
144
|
+
},
|
|
145
|
+
onQueryRuntimes: () => {
|
|
146
|
+
send({ type: 'runtime-info', runtimes: config.runtimes });
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
// 传输事件 → 骨架仲裁(订阅一次,跨多次重连保持有效)
|
|
150
|
+
transport.on('open', () => {
|
|
151
|
+
logger.info('WebSocket connected, authenticating...');
|
|
152
|
+
send({
|
|
153
|
+
type: 'auth',
|
|
154
|
+
token: config.token,
|
|
155
|
+
hostname: config.hostname,
|
|
156
|
+
os: config.os,
|
|
157
|
+
runtimes: config.runtimes,
|
|
158
|
+
version: config.version,
|
|
159
|
+
});
|
|
160
|
+
armHandshakeTimeout();
|
|
161
|
+
});
|
|
162
|
+
transport.on('message', (data) => dispatchServerMessage(data, dispatchHandlers));
|
|
163
|
+
transport.on('close', (code, reason) => {
|
|
164
|
+
const reasonStr = reason || `code ${code}`;
|
|
165
|
+
logger.warn(`WebSocket closed: ${reasonStr}`);
|
|
166
|
+
clearHandshakeTimer();
|
|
167
|
+
heartbeat.stop();
|
|
168
|
+
if (running) {
|
|
169
|
+
scheduleReconnect();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// 失败停机(auth 失败/重连耗尽)保持 error,正常 stop 才是 disconnected
|
|
173
|
+
notifyStateChange(exitAsError ? 'error' : 'disconnected');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
transport.on('error', (err) => {
|
|
177
|
+
logger.error('WebSocket error:', err);
|
|
178
|
+
});
|
|
179
|
+
const start = () => {
|
|
180
|
+
if (running) {
|
|
181
|
+
logger.warn('Tunnel is already running');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
running = true;
|
|
185
|
+
exitAsError = false;
|
|
186
|
+
backoff.reset();
|
|
187
|
+
connect();
|
|
188
|
+
};
|
|
189
|
+
const stop = async () => {
|
|
190
|
+
running = false;
|
|
191
|
+
heartbeat.stop();
|
|
192
|
+
clearHandshakeTimer();
|
|
193
|
+
// 取消运行中任务由宿主(DaemonRunner)在自身 shutdown 里处理,隧道只负责关闭连接
|
|
194
|
+
if (reconnectTimer) {
|
|
195
|
+
clearTimeout(reconnectTimer);
|
|
196
|
+
reconnectTimer = null;
|
|
197
|
+
}
|
|
198
|
+
// 连接已完全关闭(从未连接/已断开/已 teardown):close 事件不会再触发,
|
|
199
|
+
// 直接返回,避免空等 waitClosed 的 3s 兜底超时。
|
|
200
|
+
if (transport.isClosed()) {
|
|
201
|
+
notifyStateChange('disconnected');
|
|
202
|
+
logger.info('Tunnel stopped');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
transport.close(1000, 'client shutdown');
|
|
206
|
+
await transport.waitClosed(3000);
|
|
207
|
+
if (transport.isOpen() || transport.isConnecting()) {
|
|
208
|
+
transport.terminate();
|
|
209
|
+
}
|
|
210
|
+
notifyStateChange('disconnected');
|
|
211
|
+
logger.info('Tunnel stopped');
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
start,
|
|
215
|
+
stop,
|
|
216
|
+
send,
|
|
217
|
+
getState,
|
|
218
|
+
onStateChange,
|
|
219
|
+
};
|
|
220
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket 传输层封装:屏蔽 ws 库细节,向骨架暴露统一事件与命令。
|
|
4
|
+
*
|
|
5
|
+
* 职责边界:
|
|
6
|
+
* - connect() 每次调用会先清理上一个连接(移除监听 + terminate),再建立新连接;
|
|
7
|
+
* - 事件(open/message/close/error)通过 on() 订阅,订阅一次后跨多次重连保持有效;
|
|
8
|
+
* - 协议层 ping 自动回 pong,调用方无需感知;
|
|
9
|
+
* - send() 返回是否已入队:未连接或发送缓冲积压超限(背压)时返回 false,
|
|
10
|
+
* 由上层决定丢弃或重试,本模块不抛错。
|
|
11
|
+
*/
|
|
12
|
+
/** 发送缓冲上限(字节):超过该值 send() 返回 false 触发背压,防止慢网时内存无上限增长 */
|
|
13
|
+
const MAX_BUFFERED_AMOUNT_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
export const createWebSocketTransport = () => {
|
|
15
|
+
let ws = null;
|
|
16
|
+
const listeners = {
|
|
17
|
+
open: new Set(),
|
|
18
|
+
close: new Set(),
|
|
19
|
+
message: new Set(),
|
|
20
|
+
error: new Set(),
|
|
21
|
+
};
|
|
22
|
+
const emit = (event, ...args) => {
|
|
23
|
+
// 泛型下 Parameters 的元组关系无法直接展开,统一按 rest 参数调用
|
|
24
|
+
for (const callback of listeners[event]) {
|
|
25
|
+
callback(...args);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const teardown = () => {
|
|
29
|
+
if (ws) {
|
|
30
|
+
ws.removeAllListeners();
|
|
31
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
32
|
+
ws.terminate();
|
|
33
|
+
}
|
|
34
|
+
ws = null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const readyState = () => (ws ? ws.readyState : -1);
|
|
38
|
+
return {
|
|
39
|
+
connect(url, headers) {
|
|
40
|
+
teardown();
|
|
41
|
+
const socket = new WebSocket(url, { headers });
|
|
42
|
+
ws = socket;
|
|
43
|
+
socket.on('open', () => emit('open'));
|
|
44
|
+
socket.on('message', (data) => emit('message', data.toString()));
|
|
45
|
+
socket.on('close', (code, reason) => emit('close', code, reason.toString()));
|
|
46
|
+
socket.on('error', (err) => emit('error', err.message));
|
|
47
|
+
socket.on('ping', () => socket.pong());
|
|
48
|
+
},
|
|
49
|
+
send(data) {
|
|
50
|
+
if (ws === null || ws.readyState !== WebSocket.OPEN) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (ws.bufferedAmount > MAX_BUFFERED_AMOUNT_BYTES) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
ws.send(data);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
close(code, reason) {
|
|
65
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
66
|
+
ws.close(code, reason);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
terminate() {
|
|
70
|
+
ws?.terminate();
|
|
71
|
+
},
|
|
72
|
+
isOpen() {
|
|
73
|
+
return readyState() === WebSocket.OPEN;
|
|
74
|
+
},
|
|
75
|
+
isConnecting() {
|
|
76
|
+
return readyState() === WebSocket.CONNECTING;
|
|
77
|
+
},
|
|
78
|
+
isClosed() {
|
|
79
|
+
return ws === null || ws.readyState === WebSocket.CLOSED;
|
|
80
|
+
},
|
|
81
|
+
waitClosed(timeoutMs) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
let settled = false;
|
|
84
|
+
const onClose = () => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
cleanup();
|
|
89
|
+
resolve();
|
|
90
|
+
};
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
if (settled)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
cleanup();
|
|
96
|
+
resolve();
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
listeners.close.delete(onClose);
|
|
101
|
+
};
|
|
102
|
+
listeners.close.add(onClose);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
on(event, callback) {
|
|
106
|
+
listeners[event].add(callback);
|
|
107
|
+
return () => {
|
|
108
|
+
listeners[event].delete(callback);
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 隧道接入端点工具:把服务端 http(s) 地址转换为 WebSocket 隧道地址。
|
|
3
|
+
* 纯字符串转换(wss/ws://host/tunnel),无状态、无副作用。
|
|
4
|
+
*/
|
|
5
|
+
export const toWsUrl = (serverUrl) => {
|
|
6
|
+
return (serverUrl
|
|
7
|
+
.replace(/^https:\/\//, 'wss://')
|
|
8
|
+
.replace(/^http:\/\//, 'ws://')
|
|
9
|
+
.replace(/\/$/, '') + '/tunnel');
|
|
10
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brierb/brier-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Brier 平台接入命令行工具:通过加密隧道将本机接入 Brier 远程编码池,接收并执行远程任务",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brier",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"types": "./types/index.d.ts"
|
|
20
20
|
},
|
|
21
21
|
"./tunnel": {
|
|
22
|
-
"import": "./dist/tunnel/
|
|
23
|
-
"types": "./types/tunnel/
|
|
22
|
+
"import": "./dist/tunnel/index.js",
|
|
23
|
+
"types": "./types/tunnel/index.d.ts"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 读取持久化的接入令牌;文件缺失或损坏时返回 undefined。 */
|
|
2
|
+
export declare const readStoredToken: () => string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* 持久化接入令牌到 ~/.brier/credentials.json。
|
|
5
|
+
* 写入使用 mode 0600,并对已存在的文件强制收紧权限(防止先前被宽权限创建)。
|
|
6
|
+
* 失败会抛错,由调用方决定是否阻断启动。
|
|
7
|
+
*/
|
|
8
|
+
export declare const writeCredentials: (token: string) => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置域统一出口:提供 daemon 装配所需的全部环境信息。
|
|
3
|
+
*
|
|
4
|
+
* 目录内文件职责:
|
|
5
|
+
* - load.ts: loadConfig(),从环境变量装配 DaemonConfig
|
|
6
|
+
* - paths.ts: 应用路径常量(~/.brier 下的 PID/日志文件)
|
|
7
|
+
* - version.ts:CLI 版本读取(package.json)
|
|
8
|
+
*/
|
|
9
|
+
/** daemon 装配配置(BRIER_SERVER_URL / BRIER_TOKEN → DaemonConfig) */
|
|
10
|
+
export { loadConfig } from './load.js';
|
|
11
|
+
/** 应用路径常量:BRIER_DIR / DAEMON_STATE_FILE / LOG_FILE / CREDENTIALS_FILE */
|
|
12
|
+
export { BRIER_DIR, DAEMON_STATE_FILE, LOG_FILE, CREDENTIALS_FILE } from './paths.js';
|
|
13
|
+
/** CLI 版本读取 */
|
|
14
|
+
export { readCliVersion } from './version.js';
|
|
15
|
+
/** 接入令牌持久化(credentials.json,0600) */
|
|
16
|
+
export { readStoredToken, writeCredentials } from './credentials.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DaemonConfig } from '../definitions/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* daemon 装配配置加载:从 BRIER_SERVER_URL / BRIER_TOKEN 环境变量
|
|
4
|
+
* 读取 serverUrl / token,并组合本机信息(hostname/os/runtimes/version)。
|
|
5
|
+
* 校验失败(缺少必填项)时抛出带说明的错误。
|
|
6
|
+
*/
|
|
7
|
+
export declare const loadConfig: () => DaemonConfig;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用文件路径常量。
|
|
3
|
+
* 集中到 ~/.brier/ 下,保证与 daemon 启动时的工作目录无关。
|
|
4
|
+
*/
|
|
5
|
+
/** Brier 用户数据目录(~/.brier) */
|
|
6
|
+
export declare const BRIER_DIR: string;
|
|
7
|
+
/** daemon 运行状态文件(JSON,结构见 definitions/daemon.ts 的 DaemonState) */
|
|
8
|
+
export declare const DAEMON_STATE_FILE: string;
|
|
9
|
+
/** daemon 运行日志文件 */
|
|
10
|
+
export declare const LOG_FILE: string;
|
|
11
|
+
/** daemon 接入令牌持久化文件(0600,供 `daemon restart` 无参回退) */
|
|
12
|
+
export declare const CREDENTIALS_FILE: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 公共底座统一出口:无业务依赖的基础能力,供 config/daemon/tunnel/commands 各域使用。
|
|
3
|
+
*
|
|
4
|
+
* 目录内文件职责:
|
|
5
|
+
* - logger.ts: 文件日志(configureLogger / logger)
|
|
6
|
+
* - runtimes.ts:AI runtime 注册表、可执行文件解析与已安装探测
|
|
7
|
+
*/
|
|
8
|
+
/** 文件日志 */
|
|
9
|
+
export { configureLogger, logger } from './logger.js';
|
|
10
|
+
/** AI runtime 注册表 / 命令 / prompt 参数 / 解析 / 探测 */
|
|
11
|
+
export * from './runtimes.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RuntimeEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
command: string;
|
|
4
|
+
/** 命令不在 PATH 时的兜底可执行文件(~ 开头表示用户主目录),存在即视为已安装 */
|
|
5
|
+
fallbacks?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const RUNTIME_REGISTRY: readonly RuntimeEntry[];
|
|
8
|
+
export declare const RUNTIME_COMMANDS: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* AI runtime 单次(非交互)执行参数模板:命令前缀 flags,后面紧跟 prompt。
|
|
11
|
+
* 服务端 task-start 携带 prompt 时,CLI 用它拼出
|
|
12
|
+
* `spawn(<command>, [...flags, prompt])`;未列出的 runtime 按裸参数执行。
|
|
13
|
+
*/
|
|
14
|
+
export declare const RUNTIME_PROMPT_FLAGS: Record<string, readonly string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* 解析 runtime 的可执行文件**绝对路径**:
|
|
17
|
+
* 1. PATH 中查找(排除系统目录误报,如 /usr/sbin/gpt)
|
|
18
|
+
* 2. 未命中则检查官方安装目录(如 opencode → ~/.opencode/bin/opencode)
|
|
19
|
+
*
|
|
20
|
+
* 探测(runtimes 上报)与执行(spawn 任务)都走这里,保证"探测到"的
|
|
21
|
+
* runtime 一定可被执行,不依赖 daemon 进程的 PATH。
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolveRuntimeExecutable: (runtime: string) => string | null;
|
|
24
|
+
/**
|
|
25
|
+
* 探测本机已安装的 AI runtime 名称列表。
|
|
26
|
+
* 过滤注册表:只保留 resolveRuntimeExecutable 能解析出可执行文件的项
|
|
27
|
+
* (探测与执行共用同一解析,保证“探测到”的 runtime 一定能被 spawn)。
|
|
28
|
+
*/
|
|
29
|
+
export declare const detectInstalledRuntimes: () => string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StreamType } from '../definitions/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 任务输出批量发送器(micro-batching)。
|
|
4
|
+
*
|
|
5
|
+
* 动机:task-output 是高频小消息,逐块上行会在传输、序列化与服务端逐帧 DB 写入上
|
|
6
|
+
* 放大开销。本模块按 (taskId, stream) 缓冲,满足以下任一条件即 flush:
|
|
7
|
+
* - 单缓冲累计达到 maxBatchBytes(大输出即时发送,不等时间窗);
|
|
8
|
+
* - 到达 flushIntervalMs(低流量输出不会滞留过久,保证可感知延迟有上界)。
|
|
9
|
+
*
|
|
10
|
+
* 语义保证:
|
|
11
|
+
* - 同一 (taskId, stream) 严格保序,合并为一条消息发出;
|
|
12
|
+
* - 不同 task/stream 互不阻塞,各自独立 flush;
|
|
13
|
+
* - 只聚合、不截断、不背压(背压仍由上层 safeSend/连接层负责)。
|
|
14
|
+
*/
|
|
15
|
+
export interface OutputChunk {
|
|
16
|
+
taskId: string;
|
|
17
|
+
stream: StreamType;
|
|
18
|
+
/** 该缓冲项合并后的完整数据 */
|
|
19
|
+
data: string;
|
|
20
|
+
}
|
|
21
|
+
export interface OutputBatcherOptions {
|
|
22
|
+
/** 单个 (taskId, stream) 缓冲的字节阈值(按字符串 length 近似),达到即立即 flush */
|
|
23
|
+
maxBatchBytes?: number;
|
|
24
|
+
/** 周期 flush 时间窗(ms),保证低流量也有延迟上界 */
|
|
25
|
+
flushIntervalMs?: number;
|
|
26
|
+
/** flush 回调:一次回调可含多个缓冲项 */
|
|
27
|
+
onFlush: (chunks: OutputChunk[]) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface OutputBatcher {
|
|
30
|
+
/** 追加一块输出;达到字节阈值会立即触发 flush */
|
|
31
|
+
push: (taskId: string, stream: StreamType, data: string) => void;
|
|
32
|
+
/** 立即 flush 指定 task 的全部缓冲(发送终态消息前调用,避免尾输出丢失) */
|
|
33
|
+
flushTask: (taskId: string) => void;
|
|
34
|
+
/** 立即 flush 全部缓冲(停止前调用) */
|
|
35
|
+
flushAll: () => void;
|
|
36
|
+
/** 清理定时器与残留缓冲(调用前应先 flushAll) */
|
|
37
|
+
dispose: () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare const createOutputBatcher: (options: OutputBatcherOptions) => OutputBatcher;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskInfo } from '../
|
|
1
|
+
import type { TaskInfo } from '../definitions/index.js';
|
|
2
2
|
export interface TaskExecutorCallbacks {
|
|
3
3
|
onOutput: (taskId: string, stream: 'stdout' | 'stderr', data: string) => void;
|
|
4
4
|
onComplete: (taskId: string, exitCode: number) => void;
|
|
@@ -6,7 +6,9 @@ export interface TaskExecutorCallbacks {
|
|
|
6
6
|
}
|
|
7
7
|
export interface TaskExecutor {
|
|
8
8
|
execute: (task: TaskInfo) => void;
|
|
9
|
+
/** 取消单个任务:SIGTERM,宽限期后 SIGKILL */
|
|
9
10
|
cancel: (taskId: string) => void;
|
|
10
|
-
|
|
11
|
+
/** 停止并收尾:对全部运行中任务 SIGTERM,等待退出,未退出的 SIGKILL */
|
|
12
|
+
dispose: () => Promise<void>;
|
|
11
13
|
}
|
|
12
14
|
export declare const createTaskExecutor: (callbacks: TaskExecutorCallbacks) => TaskExecutor;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon 域统一出口。
|
|
3
|
+
*
|
|
4
|
+
* 说明:DaemonRunner.ts 是 daemon 子进程的入口脚本(模块顶层即执行 main(),
|
|
5
|
+
* 并校验 BRIER_DAEMON_MODE),由 DaemonManager 以文件路径 spawn,
|
|
6
|
+
* 不应被 import,故不在此导出。本目录其余模块均无副作用,可安全经此导入。
|
|
7
|
+
*/
|
|
8
|
+
/** daemon 进程生命周期管理(前台 CLI 使用) */
|
|
9
|
+
export { createDaemonManager, type DaemonManager } from './DaemonManager.js';
|
|
10
|
+
/** 任务执行器(daemon 子进程内 spawn runtime) */
|
|
11
|
+
export { createTaskExecutor, type TaskExecutor, type TaskExecutorCallbacks, } from './TaskExecutor.js';
|
|
12
|
+
/** daemon 运行状态读取(status/restart 展示与回退) */
|
|
13
|
+
export { readDaemonState } from './state.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DaemonState } from '../definitions/index.js';
|
|
2
|
+
/** 仅判断 pid 是否存活(kill 0)。 */
|
|
3
|
+
export declare const isProcessAlive: (pid: number) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* 校验状态记录对应的 daemon 是否存活:
|
|
6
|
+
* pid 存活 + 命令行含 DaemonRunner(防止 PID 复用后指向无关进程)。
|
|
7
|
+
* ps 不可用时退化为仅存活判断,避免跨平台误伤。
|
|
8
|
+
*/
|
|
9
|
+
export declare const isDaemonRunning: (state: DaemonState) => boolean;
|
|
10
|
+
/** 读取运行状态;文件缺失或字段不全(含旧版格式)时返回 null。 */
|
|
11
|
+
export declare const readDaemonState: () => DaemonState | null;
|
|
12
|
+
/** 写入完整运行状态(Manager 启动时调用)。 */
|
|
13
|
+
export declare const writeDaemonState: (state: DaemonState) => void;
|
|
14
|
+
/** 合并更新状态(Runner 侧置 ready/bootError/tunnelState,保留其余字段)。 */
|
|
15
|
+
export declare const updateDaemonState: (patch: Partial<DaemonState>) => void;
|
|
16
|
+
/** 清理运行状态文件(不存在时静默)。 */
|
|
17
|
+
export declare const removeDaemonState: () => void;
|
|
18
|
+
/** 轮询等待 pid 退出;超时返回 false。 */
|
|
19
|
+
export declare const waitForProcessExit: (pid: number, timeoutMs?: number) => Promise<boolean>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon 的装配配置、生命周期状态与进程间通信文件。
|
|
3
|
+
* 本文件三个类型均为“daemon 进程自身/前台 CLI 管理 daemon”使用,不参与隧道线协议。
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* daemon 后台服务运行状态(前台 CLI 使用,如 `brier daemon status`)。
|
|
7
|
+
*
|
|
8
|
+
* 当前实现实际只产出 running | stopped(进程不在即清理状态文件并返回 stopped);
|
|
9
|
+
* error 为预留值,尚无产出点。
|
|
10
|
+
*/
|
|
11
|
+
export type DaemonStatus = 'running' | 'stopped' | 'error';
|
|
12
|
+
/**
|
|
13
|
+
* daemon 装配配置。
|
|
14
|
+
*
|
|
15
|
+
* 由 config.loadConfig() 从 BRIER_SERVER_URL / BRIER_TOKEN 环境变量加载,
|
|
16
|
+
* DaemonRunner 启动后整体注入 TunnelClient;其中 hostname/os/runtimes/version
|
|
17
|
+
* 在握手时随 auth 消息上报服务端用于展示与任务路由。
|
|
18
|
+
*/
|
|
19
|
+
export interface DaemonConfig {
|
|
20
|
+
/** Brier 服务端地址(http/https,连接时由 toWsUrl 转换为 ws/wss 并拼接 /tunnel) */
|
|
21
|
+
serverUrl: string;
|
|
22
|
+
/** 接入令牌(BRIER_TOKEN),同时用于 wss Authorization 头与 auth 消息 */
|
|
23
|
+
token: string;
|
|
24
|
+
/** 本机主机名(同时作为服务端按用户维度 upsert 工作电脑的标识键) */
|
|
25
|
+
hostname: string;
|
|
26
|
+
/** 操作系统描述(`${type} ${platform} ${arch}`,仅供展示) */
|
|
27
|
+
os: string;
|
|
28
|
+
/** 探测到的已安装 AI runtime 列表(探测与执行共用 resolveRuntimeExecutable) */
|
|
29
|
+
runtimes: string[];
|
|
30
|
+
/** CLI 自身版本(读取 package.json),供服务端展示客户端版本 */
|
|
31
|
+
version: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* daemon 运行状态文件内容(~/.brier/daemon.json)。
|
|
35
|
+
*
|
|
36
|
+
* 前台 CLI 与 daemon 子进程之间没有 IPC 通道,二者通过该文件 + 信号协作:
|
|
37
|
+
* - 前台 DaemonManager:启动时写入(ready=false),轮询 ready 做就绪握手;
|
|
38
|
+
* - daemon 子进程(Runner):配置加载成功进入运行后置 ready=true,
|
|
39
|
+
* 启动早期失败时写 bootError,隧道状态变化时更新 tunnelState;
|
|
40
|
+
* - 前台 status/restart:读取展示与回退(serverUrl)。
|
|
41
|
+
*
|
|
42
|
+
* 身份校验:以 pid + 进程命令行(含 DaemonRunner)双重匹配,
|
|
43
|
+
* 避免 PID 被系统复用后误判/误杀其他进程。
|
|
44
|
+
*/
|
|
45
|
+
export interface DaemonState {
|
|
46
|
+
/** daemon 子进程 PID */
|
|
47
|
+
pid: number;
|
|
48
|
+
/** 启动时间戳(ms) */
|
|
49
|
+
startTime: number;
|
|
50
|
+
/** 启动时连接的服务端地址,restart 未传 --server-url 时回退使用 */
|
|
51
|
+
serverUrl: string;
|
|
52
|
+
/** 子进程是否已完成启动(Runner 进入运行循环后置 true) */
|
|
53
|
+
ready: boolean;
|
|
54
|
+
/** 启动早期失败原因(仅失败时写入) */
|
|
55
|
+
bootError?: string;
|
|
56
|
+
/** 最近一次隧道状态(Runner 随状态机更新,供 status 展示) */
|
|
57
|
+
tunnelState?: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 类型契约统一出口。
|
|
3
|
+
*
|
|
4
|
+
* 目录按功能域拆分:
|
|
5
|
+
* - tunnel.ts:隧道域(线协议 StreamType / ClientMessage / ServerMessage + 连接状态机 TunnelState)
|
|
6
|
+
* - task.ts: 本地任务执行上下文(消息 → 子进程执行)
|
|
7
|
+
* - daemon.ts:daemon 装配配置 / 生命周期状态 / PID 文件
|
|
8
|
+
*
|
|
9
|
+
* 约定:业务模块一律从本文件('../definitions/index.js')导入,不要跨层直接引用子文件,
|
|
10
|
+
* 以便后续调整目录结构时只需改这里,不动各调用方。
|
|
11
|
+
*/
|
|
12
|
+
/** 隧道域:输出流来源、线协议上行/下行消息、连接状态机 */
|
|
13
|
+
export type { StreamType, ClientMessage, ServerMessage, TunnelState } from './tunnel.js';
|
|
14
|
+
/** 本地任务执行上下文(TaskExecutor 入参) */
|
|
15
|
+
export type { TaskInfo } from './task.js';
|
|
16
|
+
/** daemon 生命周期相关:DaemonStatus / DaemonConfig / DaemonState */
|
|
17
|
+
export type { DaemonStatus, DaemonConfig, DaemonState } from './daemon.js';
|