dsh-session-ops 0.1.4 → 0.2.1
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 +16 -11
- package/README.zh.md +28 -40
- package/cordis.patch.yml +1 -1
- package/docs/DETAIL.md +9 -7
- package/docs/DETAIL.zh.md +20 -14
- package/lib/admin.js +723 -774
- package/lib/client.js +1767 -2065
- package/lib/engine.js +181 -182
- package/lib/index.js +125 -166
- package/lib/memory.js +137 -132
- package/lib/move.js +630 -644
- package/lib/sessionlog.js +2 -18
- package/lib/store/frames.js +183 -170
- package/lib/store/host.js +41 -42
- package/lib/store/http.js +64 -59
- package/lib/store/index.js +5 -6
- package/lib/store/paths.js +79 -76
- package/lib/store/registry.js +100 -89
- package/lib/store/util.js +19 -0
- package/lib/types/admin.d.ts +187 -0
- package/lib/types/engine.d.ts +136 -0
- package/lib/types/index.d.ts +281 -0
- package/lib/types/memory.d.ts +106 -0
- package/lib/types/move.d.ts +272 -0
- package/lib/types/sessionlog.d.ts +14 -0
- package/lib/types/store/frames.d.ts +128 -0
- package/lib/types/store/host.d.ts +88 -0
- package/lib/types/store/http.d.ts +105 -0
- package/lib/types/store/index.d.ts +30 -0
- package/lib/types/store/paths.d.ts +100 -0
- package/lib/types/store/registry.d.ts +206 -0
- package/lib/types/store/util.d.ts +16 -0
- package/package.json +13 -3
package/lib/sessionlog.js
CHANGED
|
@@ -10,21 +10,5 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module dsh-session-ops/sessionlog
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
CHECKSUM_OPTIONS,
|
|
16
|
-
ZSTD_MAGIC,
|
|
17
|
-
hasZstd,
|
|
18
|
-
readHeaderFromBuffer,
|
|
19
|
-
rewriteHeaderCwd,
|
|
20
|
-
scanZstdFrames,
|
|
21
|
-
} from './store/frames.js'
|
|
22
|
-
|
|
23
|
-
export {
|
|
24
|
-
compressionSuffix,
|
|
25
|
-
encodeSegment,
|
|
26
|
-
generationLogFilename,
|
|
27
|
-
logPath,
|
|
28
|
-
projectKey,
|
|
29
|
-
sessionDir,
|
|
30
|
-
} from './store/paths.js'
|
|
13
|
+
export { CHECKSUM_OPTIONS, ZSTD_MAGIC, hasZstd, readHeaderFromBuffer, rewriteHeaderCwd, scanZstdFrames, } from './store/frames.js';
|
|
14
|
+
export { compressionSuffix, encodeSegment, generationLogFilename, logPath, projectKey, sessionDir, } from './store/paths.js';
|
package/lib/store/frames.js
CHANGED
|
@@ -28,29 +28,21 @@
|
|
|
28
28
|
*
|
|
29
29
|
* @module dsh-session-ops/store/frames
|
|
30
30
|
*/
|
|
31
|
-
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
import { isSessionLogFile } from './paths.js'
|
|
37
|
-
|
|
31
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import zlib from 'node:zlib';
|
|
34
|
+
import { isSessionLogFile } from './paths.js';
|
|
35
|
+
import { messageOf } from './util.js';
|
|
38
36
|
/** zstd 帧魔数(小端)。 */
|
|
39
|
-
export const ZSTD_MAGIC = 0xfd2fb528
|
|
40
|
-
|
|
37
|
+
export const ZSTD_MAGIC = 0xfd2fb528;
|
|
41
38
|
/** 与 DSH 一致的压缩选项:带内容校验和。 */
|
|
42
|
-
export const CHECKSUM_OPTIONS =
|
|
43
|
-
zlib.constants?.ZSTD_c_checksumFlag === undefined
|
|
39
|
+
export const CHECKSUM_OPTIONS = zlib.constants?.ZSTD_c_checksumFlag === undefined
|
|
44
40
|
? {}
|
|
45
|
-
: { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } }
|
|
46
|
-
|
|
41
|
+
: { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } };
|
|
47
42
|
/** `zstdCompressSync` 需要 Node ≥22.15 / ≥23.8;缺失时插件拒绝搬迁而不是写坏日志。 */
|
|
48
|
-
export const hasZstd = typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function'
|
|
49
|
-
|
|
43
|
+
export const hasZstd = typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function';
|
|
50
44
|
/** `zstdDecompressSync` 同样需要 Node ≥22.15;缺失时降级为「读不到会话头」。 */
|
|
51
|
-
const zstdDecompress =
|
|
52
|
-
typeof zlib.zstdDecompressSync === 'function' ? zlib.zstdDecompressSync : undefined
|
|
53
|
-
|
|
45
|
+
const zstdDecompress = typeof zlib.zstdDecompressSync === 'function' ? zlib.zstdDecompressSync : undefined;
|
|
54
46
|
/**
|
|
55
47
|
* 走一遍拼接帧,给出每帧的字节范围。
|
|
56
48
|
*
|
|
@@ -62,49 +54,58 @@ const zstdDecompress =
|
|
|
62
54
|
* @returns `{frames, incomplete, reason}`;`frames` 每项含 `{start, end}`。
|
|
63
55
|
*/
|
|
64
56
|
export function scanZstdFrames(buffer) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
const frames = [];
|
|
58
|
+
let offset = 0;
|
|
59
|
+
while (offset < buffer.length) {
|
|
60
|
+
const start = offset;
|
|
61
|
+
if (buffer.length - offset < 4)
|
|
62
|
+
return { frames, incomplete: true, reason: `尾部剩 ${buffer.length - offset} 字节,不足以构成帧魔数` };
|
|
63
|
+
if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) {
|
|
64
|
+
return { frames, incomplete: true, reason: `偏移 ${offset} 处不是 zstd 魔数` };
|
|
65
|
+
}
|
|
66
|
+
offset += 4;
|
|
67
|
+
if (offset >= buffer.length)
|
|
68
|
+
return { frames, incomplete: true, reason: '帧头被截断(缺少描述符)' };
|
|
69
|
+
const descriptor = buffer.readUInt8(offset);
|
|
70
|
+
offset += 1;
|
|
71
|
+
const fcsFlag = descriptor >> 6;
|
|
72
|
+
const singleSegment = (descriptor >> 5) & 1;
|
|
73
|
+
const checksum = (descriptor >> 2) & 1;
|
|
74
|
+
const dictionaryFlag = descriptor & 3;
|
|
75
|
+
if (singleSegment === 0)
|
|
76
|
+
offset += 1; // Window_Descriptor
|
|
77
|
+
offset += dictionaryFlag === 0 ? 0 : dictionaryFlag === 1 ? 1 : dictionaryFlag === 2 ? 2 : 4;
|
|
78
|
+
const fcsSize = fcsFlag === 0 ? (singleSegment === 1 ? 1 : 0) : fcsFlag === 1 ? 2 : fcsFlag === 2 ? 4 : 8;
|
|
79
|
+
offset += fcsSize;
|
|
80
|
+
if (offset > buffer.length)
|
|
81
|
+
return { frames, incomplete: true, reason: '帧头字段越过文件尾' };
|
|
82
|
+
for (;;) {
|
|
83
|
+
if (offset + 3 > buffer.length)
|
|
84
|
+
return { frames, incomplete: true, reason: '块头被截断' };
|
|
85
|
+
const header = buffer.readUInt8(offset) | (buffer.readUInt8(offset + 1) << 8) | (buffer.readUInt8(offset + 2) << 16);
|
|
86
|
+
offset += 3;
|
|
87
|
+
const lastBlock = header & 1;
|
|
88
|
+
const blockType = (header >> 1) & 3;
|
|
89
|
+
const blockSize = header >> 3;
|
|
90
|
+
if (blockType === 0)
|
|
91
|
+
offset += blockSize;
|
|
92
|
+
else if (blockType === 1)
|
|
93
|
+
offset += 1;
|
|
94
|
+
else if (blockType === 2)
|
|
95
|
+
offset += blockSize;
|
|
96
|
+
else
|
|
97
|
+
return { frames, incomplete: true, reason: `偏移 ${offset - 3} 处出现保留块类型` };
|
|
98
|
+
if (lastBlock === 1)
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (checksum === 1)
|
|
102
|
+
offset += 4;
|
|
103
|
+
if (offset > buffer.length)
|
|
104
|
+
return { frames, incomplete: true, reason: '帧体越过文件尾' };
|
|
105
|
+
frames.push({ start, end: offset });
|
|
72
106
|
}
|
|
73
|
-
|
|
74
|
-
if (offset >= buffer.length) return { frames, incomplete: true, reason: '帧头被截断(缺少描述符)' }
|
|
75
|
-
const descriptor = buffer.readUInt8(offset)
|
|
76
|
-
offset += 1
|
|
77
|
-
const fcsFlag = descriptor >> 6
|
|
78
|
-
const singleSegment = (descriptor >> 5) & 1
|
|
79
|
-
const checksum = (descriptor >> 2) & 1
|
|
80
|
-
const dictionaryFlag = descriptor & 3
|
|
81
|
-
|
|
82
|
-
if (singleSegment === 0) offset += 1 // Window_Descriptor
|
|
83
|
-
offset += dictionaryFlag === 0 ? 0 : dictionaryFlag === 1 ? 1 : dictionaryFlag === 2 ? 2 : 4
|
|
84
|
-
const fcsSize = fcsFlag === 0 ? (singleSegment === 1 ? 1 : 0) : fcsFlag === 1 ? 2 : fcsFlag === 2 ? 4 : 8
|
|
85
|
-
offset += fcsSize
|
|
86
|
-
if (offset > buffer.length) return { frames, incomplete: true, reason: '帧头字段越过文件尾' }
|
|
87
|
-
|
|
88
|
-
for (;;) {
|
|
89
|
-
if (offset + 3 > buffer.length) return { frames, incomplete: true, reason: '块头被截断' }
|
|
90
|
-
const header = buffer.readUInt8(offset) | (buffer.readUInt8(offset + 1) << 8) | (buffer.readUInt8(offset + 2) << 16)
|
|
91
|
-
offset += 3
|
|
92
|
-
const lastBlock = header & 1
|
|
93
|
-
const blockType = (header >> 1) & 3
|
|
94
|
-
const blockSize = header >> 3
|
|
95
|
-
if (blockType === 0) offset += blockSize
|
|
96
|
-
else if (blockType === 1) offset += 1
|
|
97
|
-
else if (blockType === 2) offset += blockSize
|
|
98
|
-
else return { frames, incomplete: true, reason: `偏移 ${offset - 3} 处出现保留块类型` }
|
|
99
|
-
if (lastBlock === 1) break
|
|
100
|
-
}
|
|
101
|
-
if (checksum === 1) offset += 4
|
|
102
|
-
if (offset > buffer.length) return { frames, incomplete: true, reason: '帧体越过文件尾' }
|
|
103
|
-
frames.push({ start, end: offset })
|
|
104
|
-
}
|
|
105
|
-
return { frames, incomplete: false }
|
|
107
|
+
return { frames, incomplete: false };
|
|
106
108
|
}
|
|
107
|
-
|
|
108
109
|
/**
|
|
109
110
|
* 读出日志第一帧里的 header 对象(只解析,不改写)。
|
|
110
111
|
* @param buffer - 日志内容。
|
|
@@ -112,30 +113,32 @@ export function scanZstdFrames(buffer) {
|
|
|
112
113
|
* @returns header 对象;不是合法会话日志时 `undefined`。
|
|
113
114
|
*/
|
|
114
115
|
export function readHeaderFromBuffer(buffer, compressed = true) {
|
|
115
|
-
|
|
116
|
+
if (!compressed) {
|
|
117
|
+
try {
|
|
118
|
+
const text = buffer.toString('utf8');
|
|
119
|
+
const newlineIndex = text.indexOf('\n');
|
|
120
|
+
const parsed = JSON.parse(newlineIndex === -1 ? text : text.slice(0, newlineIndex));
|
|
121
|
+
return parsed !== null && typeof parsed === 'object' && parsed.type === 'session' ? parsed : undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const scan = scanZstdFrames(buffer);
|
|
128
|
+
if (scan.frames.length === 0)
|
|
129
|
+
return undefined;
|
|
116
130
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
const first = scan.frames[0];
|
|
132
|
+
const inflated = zlib.zstdDecompressSync(buffer.subarray(first.start, first.end));
|
|
133
|
+
const text = Buffer.from(inflated).toString('utf8');
|
|
134
|
+
const newlineIndex = text.indexOf('\n');
|
|
135
|
+
const parsed = JSON.parse(newlineIndex === -1 ? text : text.slice(0, newlineIndex));
|
|
136
|
+
return parsed !== null && typeof parsed === 'object' && parsed.type === 'session' ? parsed : undefined;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
123
140
|
}
|
|
124
|
-
}
|
|
125
|
-
const scan = scanZstdFrames(buffer)
|
|
126
|
-
if (scan.frames.length === 0) return undefined
|
|
127
|
-
try {
|
|
128
|
-
const first = scan.frames[0]
|
|
129
|
-
const inflated = zlib.zstdDecompressSync(buffer.subarray(first.start, first.end))
|
|
130
|
-
const text = Buffer.from(inflated).toString('utf8')
|
|
131
|
-
const newlineIndex = text.indexOf('\n')
|
|
132
|
-
const parsed = JSON.parse(newlineIndex === -1 ? text : text.slice(0, newlineIndex))
|
|
133
|
-
return parsed !== null && typeof parsed === 'object' && parsed.type === 'session' ? parsed : undefined
|
|
134
|
-
} catch {
|
|
135
|
-
return undefined
|
|
136
|
-
}
|
|
137
141
|
}
|
|
138
|
-
|
|
139
142
|
/**
|
|
140
143
|
* 把一个会话目录里的会话头读出来(**宽松**读法:扫目录找日志文件,取解压后的第一行)。
|
|
141
144
|
*
|
|
@@ -149,42 +152,46 @@ export function readHeaderFromBuffer(buffer, compressed = true) {
|
|
|
149
152
|
* @returns 头部对象,或 undefined。
|
|
150
153
|
*/
|
|
151
154
|
export function readSessionHeader(dir, cache = new Map()) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
stat
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const log = entries.find((e) => e.isFile() && isSessionLogFile(e.name));
|
|
163
|
+
if (log === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
const path = join(dir, log.name);
|
|
166
|
+
let stat;
|
|
167
|
+
try {
|
|
168
|
+
stat = statSync(path);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const key = `${path}:${stat.mtimeMs}:${stat.size}`;
|
|
174
|
+
if (cache.has(key))
|
|
175
|
+
return cache.get(key);
|
|
176
|
+
let header;
|
|
177
|
+
try {
|
|
178
|
+
const raw = readFileSync(path);
|
|
179
|
+
const text = log.name.endsWith('.zstd')
|
|
180
|
+
? zstdDecompress === undefined
|
|
181
|
+
? undefined
|
|
182
|
+
: zstdDecompress(raw).toString('utf8')
|
|
183
|
+
: raw.toString('utf8');
|
|
184
|
+
if (text !== undefined) {
|
|
185
|
+
const end = text.indexOf('\n');
|
|
186
|
+
header = JSON.parse(end === -1 ? text : text.slice(0, end));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
header = undefined;
|
|
180
191
|
}
|
|
181
|
-
|
|
182
|
-
header
|
|
183
|
-
}
|
|
184
|
-
cache.set(key, header)
|
|
185
|
-
return header
|
|
192
|
+
cache.set(key, header);
|
|
193
|
+
return header;
|
|
186
194
|
}
|
|
187
|
-
|
|
188
195
|
/**
|
|
189
196
|
* 把会话头的 `cwd` 改写成另一条绝对路径。
|
|
190
197
|
*
|
|
@@ -198,64 +205,70 @@ export function readSessionHeader(dir, cache = new Map()) {
|
|
|
198
205
|
* `rewritten: false` 并原样返回。
|
|
199
206
|
*/
|
|
200
207
|
export function rewriteHeaderCwd(buffer, newCwd, options = {}) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
const compressed = options.compressed !== false;
|
|
209
|
+
if (typeof newCwd !== 'string' || newCwd.length === 0) {
|
|
210
|
+
return { buffer, rewritten: false, reason: 'empty target cwd' };
|
|
211
|
+
}
|
|
212
|
+
if (!compressed) {
|
|
213
|
+
const text = buffer.toString('utf8');
|
|
214
|
+
const newlineIndex = text.indexOf('\n');
|
|
215
|
+
const firstLine = newlineIndex === -1 ? text : text.slice(0, newlineIndex);
|
|
216
|
+
const rest = newlineIndex === -1 ? '' : text.slice(newlineIndex);
|
|
217
|
+
let header;
|
|
218
|
+
try {
|
|
219
|
+
header = JSON.parse(firstLine);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { buffer, rewritten: false, reason: 'header line is not JSON' };
|
|
223
|
+
}
|
|
224
|
+
if (header?.type !== 'session' || typeof header.cwd !== 'string') {
|
|
225
|
+
return { buffer, rewritten: false, reason: 'header has no cwd' };
|
|
226
|
+
}
|
|
227
|
+
if (header.cwd === newCwd)
|
|
228
|
+
return { buffer, rewritten: false, header };
|
|
229
|
+
const rebuilt = {};
|
|
230
|
+
for (const [key, value] of Object.entries(header))
|
|
231
|
+
rebuilt[key] = key === 'cwd' ? newCwd : value;
|
|
232
|
+
return { buffer: Buffer.from(JSON.stringify(rebuilt) + rest, 'utf8'), rewritten: true, header: rebuilt };
|
|
233
|
+
}
|
|
234
|
+
if (!hasZstd)
|
|
235
|
+
return { buffer, rewritten: false, reason: 'this node build lacks zlib zstd support' };
|
|
236
|
+
const scan = scanZstdFrames(buffer);
|
|
237
|
+
if (scan.incomplete || scan.frames.length === 0) {
|
|
238
|
+
return { buffer, rewritten: false, reason: scan.reason ?? 'no complete frame' };
|
|
239
|
+
}
|
|
240
|
+
const firstFrame = scan.frames[0];
|
|
241
|
+
let header;
|
|
242
|
+
let rest;
|
|
211
243
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
const inflated = zlib.zstdDecompressSync(buffer.subarray(firstFrame.start, firstFrame.end));
|
|
245
|
+
const text = Buffer.from(inflated).toString('utf8');
|
|
246
|
+
const newlineIndex = text.indexOf('\n');
|
|
247
|
+
header = JSON.parse(newlineIndex === -1 ? text : text.slice(0, newlineIndex));
|
|
248
|
+
rest = newlineIndex === -1 ? '' : text.slice(newlineIndex);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
return { buffer, rewritten: false, reason: `cannot decode header frame: ${messageOf(error)}` };
|
|
215
252
|
}
|
|
216
253
|
if (header?.type !== 'session' || typeof header.cwd !== 'string') {
|
|
217
|
-
|
|
254
|
+
return { buffer, rewritten: false, reason: 'header has no cwd' };
|
|
255
|
+
}
|
|
256
|
+
if (header.cwd === newCwd)
|
|
257
|
+
return { buffer, rewritten: false, header, frames: scan.frames.length };
|
|
258
|
+
const rebuilt = {};
|
|
259
|
+
for (const [key, value] of Object.entries(header))
|
|
260
|
+
rebuilt[key] = key === 'cwd' ? newCwd : value;
|
|
261
|
+
let newFrame;
|
|
262
|
+
try {
|
|
263
|
+
newFrame = zlib.zstdCompressSync(Buffer.from(JSON.stringify(rebuilt) + rest, 'utf8'), CHECKSUM_OPTIONS);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return { buffer, rewritten: false, reason: `cannot recompress header frame: ${messageOf(error)}` };
|
|
218
267
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (!hasZstd) return { buffer, rewritten: false, reason: 'this node build lacks zlib zstd support' }
|
|
226
|
-
const scan = scanZstdFrames(buffer)
|
|
227
|
-
if (scan.incomplete || scan.frames.length === 0) {
|
|
228
|
-
return { buffer, rewritten: false, reason: scan.reason ?? 'no complete frame' }
|
|
229
|
-
}
|
|
230
|
-
const firstFrame = scan.frames[0]
|
|
231
|
-
let header
|
|
232
|
-
let rest
|
|
233
|
-
try {
|
|
234
|
-
const inflated = zlib.zstdDecompressSync(buffer.subarray(firstFrame.start, firstFrame.end))
|
|
235
|
-
const text = Buffer.from(inflated).toString('utf8')
|
|
236
|
-
const newlineIndex = text.indexOf('\n')
|
|
237
|
-
header = JSON.parse(newlineIndex === -1 ? text : text.slice(0, newlineIndex))
|
|
238
|
-
rest = newlineIndex === -1 ? '' : text.slice(newlineIndex)
|
|
239
|
-
} catch (error) {
|
|
240
|
-
return { buffer, rewritten: false, reason: `cannot decode header frame: ${String(error?.message ?? error)}` }
|
|
241
|
-
}
|
|
242
|
-
if (header?.type !== 'session' || typeof header.cwd !== 'string') {
|
|
243
|
-
return { buffer, rewritten: false, reason: 'header has no cwd' }
|
|
244
|
-
}
|
|
245
|
-
if (header.cwd === newCwd) return { buffer, rewritten: false, header, frames: scan.frames.length }
|
|
246
|
-
|
|
247
|
-
const rebuilt = {}
|
|
248
|
-
for (const [key, value] of Object.entries(header)) rebuilt[key] = key === 'cwd' ? newCwd : value
|
|
249
|
-
let newFrame
|
|
250
|
-
try {
|
|
251
|
-
newFrame = zlib.zstdCompressSync(Buffer.from(JSON.stringify(rebuilt) + rest, 'utf8'), CHECKSUM_OPTIONS)
|
|
252
|
-
} catch (error) {
|
|
253
|
-
return { buffer, rewritten: false, reason: `cannot recompress header frame: ${String(error?.message ?? error)}` }
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
buffer: Buffer.concat([newFrame, buffer.subarray(firstFrame.end)]),
|
|
257
|
-
rewritten: true,
|
|
258
|
-
header: rebuilt,
|
|
259
|
-
frames: scan.frames.length,
|
|
260
|
-
}
|
|
268
|
+
return {
|
|
269
|
+
buffer: Buffer.concat([newFrame, buffer.subarray(firstFrame.end)]),
|
|
270
|
+
rewritten: true,
|
|
271
|
+
header: rebuilt,
|
|
272
|
+
frames: scan.frames.length,
|
|
273
|
+
};
|
|
261
274
|
}
|
package/lib/store/host.js
CHANGED
|
@@ -14,31 +14,26 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module dsh-session-ops/store/host
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { join } from 'node:path'
|
|
21
|
-
|
|
17
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
22
20
|
/** `$DSH_HOME`:环境变量优先,默认 `~/.dsh`。 */
|
|
23
21
|
export function dshHome() {
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const fromEnv = process.env.DSH_HOME;
|
|
23
|
+
return fromEnv !== undefined && fromEnv !== '' ? fromEnv : join(homedir(), '.dsh');
|
|
26
24
|
}
|
|
27
|
-
|
|
28
25
|
/**
|
|
29
26
|
* 文件日志行格式之一:`[ISO 时间] 级别 正文`(`dsh-session-admin` 在用)。
|
|
30
27
|
* @param entry - `{iso, level, message}`。
|
|
31
28
|
* @returns 一整行(含结尾换行)。
|
|
32
29
|
*/
|
|
33
|
-
export const LOG_LINE_LEVEL_FIRST = ({ iso, level, message }) => `[${iso}] ${level} ${message}\n
|
|
34
|
-
|
|
30
|
+
export const LOG_LINE_LEVEL_FIRST = ({ iso, level, message }) => `[${iso}] ${level} ${message}\n`;
|
|
35
31
|
/**
|
|
36
32
|
* 文件日志行格式之一:`ISO 时间 [级别] 正文`(`dsh-session-move` 在用)。
|
|
37
33
|
* @param entry - `{iso, level, message}`。
|
|
38
34
|
* @returns 一整行(含结尾换行)。
|
|
39
35
|
*/
|
|
40
|
-
export const LOG_LINE_TIME_FIRST = ({ iso, level, message }) => `${iso} [${level}] ${message}\n
|
|
41
|
-
|
|
36
|
+
export const LOG_LINE_TIME_FIRST = ({ iso, level, message }) => `${iso} [${level}] ${message}\n`;
|
|
42
37
|
/**
|
|
43
38
|
* 尽力而为的文件日志:写 `$DSH_HOME/logs/<name>.log`,并顺带喂宿主 logger。
|
|
44
39
|
*
|
|
@@ -50,56 +45,60 @@ export const LOG_LINE_TIME_FIRST = ({ iso, level, message }) => `${iso} [${level
|
|
|
50
45
|
* @returns `(level, message) => void`,即插件里的 `log`。
|
|
51
46
|
*/
|
|
52
47
|
export function createFileLogger(ctx, options = {}) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const dir = join(dshHome(), 'logs')
|
|
58
|
-
mkdirSync(dir, { recursive: true })
|
|
59
|
-
path = join(dir, `${name}.log`)
|
|
60
|
-
} catch {
|
|
61
|
-
path = undefined
|
|
62
|
-
}
|
|
63
|
-
return (level, message) => {
|
|
48
|
+
const name = typeof options.name === 'string' && options.name.length > 0 ? options.name : 'dsh-session-ops';
|
|
49
|
+
const formatLine = typeof options.formatLine === 'function' ? options.formatLine : LOG_LINE_LEVEL_FIRST;
|
|
50
|
+
let path;
|
|
64
51
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
const dir = join(dshHome(), 'logs');
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
path = join(dir, `${name}.log`);
|
|
68
55
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
appendFileSync(path, formatLine({ iso: new Date().toISOString(), level, message }), 'utf8')
|
|
72
|
-
} catch {
|
|
73
|
-
/* 日志写不进去就算了 */
|
|
56
|
+
catch {
|
|
57
|
+
path = undefined;
|
|
74
58
|
}
|
|
75
|
-
|
|
59
|
+
return (level, message) => {
|
|
60
|
+
try {
|
|
61
|
+
ctx?.logger?.[level]?.(message);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* 宿主 logger 不可用不影响文件日志 */
|
|
65
|
+
}
|
|
66
|
+
if (path === undefined)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
appendFileSync(path, formatLine({ iso: new Date().toISOString(), level, message }), 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* 日志写不进去就算了 */
|
|
73
|
+
}
|
|
74
|
+
};
|
|
76
75
|
}
|
|
77
|
-
|
|
78
76
|
/**
|
|
79
77
|
* 解包 entry 配置里的 `.volatile()` 活引用。
|
|
80
78
|
* @param raw - entry 配置或普通配置对象。
|
|
81
79
|
* @returns 值已解包的浅拷贝。
|
|
82
80
|
*/
|
|
83
81
|
export function plainConfig(raw = {}) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
const source = raw !== null && typeof raw === 'object' ? raw : {};
|
|
83
|
+
const plain = {};
|
|
84
|
+
for (const [key, value] of Object.entries(source)) {
|
|
85
|
+
plain[key] = value !== null && typeof value === 'object' && typeof value.get === 'function'
|
|
86
|
+
? value.get()
|
|
87
|
+
: value;
|
|
88
|
+
}
|
|
89
|
+
return plain;
|
|
90
90
|
}
|
|
91
|
-
|
|
92
91
|
/**
|
|
93
92
|
* 解析一份 entry 配置:先解包输入的活引用,再交给 schema 校验并补默认值,最后**再解包
|
|
94
93
|
* 一次**——schemastery 对 `.volatile()` 字段输出的也是活引用,不收敛的话读到的就是
|
|
95
94
|
* `{ get() }` 对象(HTTP 面上会变成一个空对象)。
|
|
96
95
|
*
|
|
97
|
-
* schema
|
|
96
|
+
* schema 由调用方传入(本包只把 `@deepseek-ai/schemastery` 当类型来源),传进来的只要是
|
|
98
97
|
* 「可调用、收普通对象、返回带默认值的普通对象」即可。
|
|
99
98
|
* @param schema - 插件自己的 `Config`(schemastery 对象,可调用)。
|
|
100
99
|
* @param raw - entry 配置或普通配置对象。
|
|
101
100
|
* @returns 普通值配置。
|
|
102
101
|
*/
|
|
103
102
|
export function resolveVolatileConfig(schema, raw = {}) {
|
|
104
|
-
|
|
103
|
+
return plainConfig(schema(plainConfig(raw)));
|
|
105
104
|
}
|