nx-ce 0.1.3 → 0.1.5
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 +232 -94
- package/package.json +3 -2
- package/src/cli.js +4 -2
- package/src/index.js +9 -1
- package/src/serve.js +553 -198
- package/src/session-store.js +56 -2
- package/src/util.js +124 -0
package/src/session-store.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 会话存储 — 磁盘上的持久化状态
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 参考 happy 的 sessions.json 设计:
|
|
5
|
+
* - 每个命名实例存储完整元数据
|
|
6
|
+
* - lifecycleState 追踪会话生命周期
|
|
7
|
+
* - machineId/host 标识运行环境
|
|
8
|
+
* - usage 追踪 token 消耗
|
|
9
|
+
*
|
|
5
10
|
* 目录:$HOME/.nx-ce/instances/{name}.json
|
|
6
11
|
*/
|
|
7
12
|
|
|
@@ -17,6 +22,51 @@ function ensureDir() {
|
|
|
17
22
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 会话生命周期状态枚举。
|
|
27
|
+
* 参考 happy 的 lifecycleState: "running" | "stopped" | "crashed"
|
|
28
|
+
*/
|
|
29
|
+
export const LifecycleState = Object.freeze({
|
|
30
|
+
RUNNING: 'running',
|
|
31
|
+
STOPPED: 'stopped',
|
|
32
|
+
CRASHED: 'crashed',
|
|
33
|
+
RESUMING: 'resuming',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 创建一个新的状态对象(含默认值)。
|
|
38
|
+
*
|
|
39
|
+
* @param {string} name - 实例名称
|
|
40
|
+
* @param {object} [overrides] - 覆盖字段
|
|
41
|
+
* @returns {object}
|
|
42
|
+
*/
|
|
43
|
+
export function createState(name, overrides = {}) {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
pid: process.pid,
|
|
47
|
+
startedAt: new Date().toISOString(),
|
|
48
|
+
updatedAt: new Date().toISOString(),
|
|
49
|
+
sessionId: null,
|
|
50
|
+
model: 'claude-sonnet-4-6',
|
|
51
|
+
host: '',
|
|
52
|
+
machineId: '',
|
|
53
|
+
claudeVersion: '',
|
|
54
|
+
lifecycleState: LifecycleState.RUNNING,
|
|
55
|
+
lifecycleStateSince: Date.now(),
|
|
56
|
+
startedBy: 'serve',
|
|
57
|
+
port: null,
|
|
58
|
+
usage: {
|
|
59
|
+
inputTokens: 0,
|
|
60
|
+
cacheCreationInputTokens: 0,
|
|
61
|
+
cacheReadInputTokens: 0,
|
|
62
|
+
outputTokens: 0,
|
|
63
|
+
contextWindow: 200000,
|
|
64
|
+
contextTokens: 0,
|
|
65
|
+
},
|
|
66
|
+
...overrides,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
20
70
|
/**
|
|
21
71
|
* 读取指定实例的持久化状态。
|
|
22
72
|
*
|
|
@@ -42,7 +92,11 @@ export function readState(name) {
|
|
|
42
92
|
export function writeState(name, state) {
|
|
43
93
|
ensureDir();
|
|
44
94
|
const path = join(STATE_DIR, sanitize(name));
|
|
45
|
-
|
|
95
|
+
const enriched = {
|
|
96
|
+
...state,
|
|
97
|
+
updatedAt: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
writeFileSync(path, JSON.stringify(enriched, null, 2), 'utf8');
|
|
46
100
|
}
|
|
47
101
|
|
|
48
102
|
/**
|
package/src/util.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函数 — 加密 ID 生成、单调时钟、机器标识
|
|
3
|
+
*
|
|
4
|
+
* 参考 happy 的 cuid2 + monotonic clock + machineId 设计。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
8
|
+
import { hostname, machine, platform, release } from 'node:os';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
// =================================================================
|
|
14
|
+
// ID 生成
|
|
15
|
+
// =================================================================
|
|
16
|
+
|
|
17
|
+
/** 会话 ID 前缀 */
|
|
18
|
+
const SESSION_PREFIX = 'nxce';
|
|
19
|
+
/** 消息 / turn ID 前缀 */
|
|
20
|
+
const MSG_PREFIX = 'msg';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 生成可排序的唯一 ID。
|
|
24
|
+
* 格式: {prefix}_{timestamp}-{randomUUID片段}
|
|
25
|
+
* 前缀保证可读性,时间戳保证近似排序,UUID 保证全局唯一。
|
|
26
|
+
*
|
|
27
|
+
* @param {string} prefix - ID 前缀
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function generateId(prefix = SESSION_PREFIX) {
|
|
31
|
+
const ts = Date.now().toString(36);
|
|
32
|
+
const rand = randomUUID().replace(/-/g, '').slice(0, 12);
|
|
33
|
+
return `${prefix}_${ts}_${rand}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =================================================================
|
|
37
|
+
// 单调时钟
|
|
38
|
+
// =================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 单调时钟 — 保证消息 time 字段严格递增。
|
|
42
|
+
*
|
|
43
|
+
* 参考 happy 的 AcpSessionManager.nextTime():
|
|
44
|
+
* time = max(lastTime + 1, Date.now())
|
|
45
|
+
*
|
|
46
|
+
* 即使是同一毫秒内的多条消息也能保持正确顺序。
|
|
47
|
+
*/
|
|
48
|
+
export class MonotonicClock {
|
|
49
|
+
/** @type {number} */
|
|
50
|
+
#lastTime = 0;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 获取下一个单调递增的时间戳(毫秒级)。
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
next() {
|
|
57
|
+
this.#lastTime = Math.max(this.#lastTime + 1, Date.now());
|
|
58
|
+
return this.#lastTime;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 重置时钟(会话重新初始化时使用)。
|
|
63
|
+
*/
|
|
64
|
+
reset() {
|
|
65
|
+
this.#lastTime = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =================================================================
|
|
70
|
+
// 机器标识
|
|
71
|
+
// =================================================================
|
|
72
|
+
|
|
73
|
+
/** 持久化机器 ID 文件路径 */
|
|
74
|
+
const MACHINE_ID_FILE = join(homedir(), '.nx-ce', 'machine-id');
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取或生成持久的机器 ID。
|
|
78
|
+
* 参考 happy 的 machineId: "418aa05c-377a-4577-b100-fd36ab54c641"
|
|
79
|
+
*
|
|
80
|
+
* 持久化到 ~/.nx-ce/machine-id,首次生成后固定不变。
|
|
81
|
+
*
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function getMachineId() {
|
|
85
|
+
try {
|
|
86
|
+
if (existsSync(MACHINE_ID_FILE)) {
|
|
87
|
+
return readFileSync(MACHINE_ID_FILE, 'utf8').trim();
|
|
88
|
+
}
|
|
89
|
+
} catch { /* 首次运行,文件不存在 */ }
|
|
90
|
+
|
|
91
|
+
// 基于机器特征生成稳定 ID
|
|
92
|
+
const raw = `${hostname()}-${machine()}-${platform()}-${release()}`;
|
|
93
|
+
const id = createHash('sha256').update(raw).digest('hex').slice(0, 24);
|
|
94
|
+
const formatted = [
|
|
95
|
+
id.slice(0, 8),
|
|
96
|
+
id.slice(8, 12),
|
|
97
|
+
id.slice(12, 16),
|
|
98
|
+
id.slice(16, 20),
|
|
99
|
+
id.slice(20, 24),
|
|
100
|
+
].join('-');
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(join(homedir(), '.nx-ce'), { recursive: true });
|
|
104
|
+
writeFileSync(MACHINE_ID_FILE, formatted, 'utf8');
|
|
105
|
+
} catch { /* 持久化失败不阻塞 */ }
|
|
106
|
+
|
|
107
|
+
return formatted;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// =================================================================
|
|
111
|
+
// 编码 / 序列化辅助
|
|
112
|
+
// =================================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 将字节大小格式化为可读字符串。
|
|
116
|
+
* @param {number} bytes
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function formatBytes(bytes) {
|
|
120
|
+
if (bytes === 0 || !bytes) return '0 B';
|
|
121
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
122
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
123
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
124
|
+
}
|