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/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';
@@ -28,29 +28,21 @@
28
28
  *
29
29
  * @module dsh-session-ops/store/frames
30
30
  */
31
-
32
- import { readFileSync, readdirSync, statSync } from 'node:fs'
33
- import { join } from 'node:path'
34
- import zlib from 'node:zlib'
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
- const frames = []
66
- let offset = 0
67
- while (offset < buffer.length) {
68
- const start = offset
69
- if (buffer.length - offset < 4) return { frames, incomplete: true, reason: `尾部剩 ${buffer.length - offset} 字节,不足以构成帧魔数` }
70
- if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) {
71
- return { frames, incomplete: true, reason: `偏移 ${offset} 处不是 zstd 魔数` }
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
- offset += 4
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
- if (!compressed) {
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
- const text = buffer.toString('utf8')
118
- const newlineIndex = text.indexOf('\n')
119
- const parsed = JSON.parse(newlineIndex === -1 ? text : text.slice(0, newlineIndex))
120
- return parsed !== null && typeof parsed === 'object' && parsed.type === 'session' ? parsed : undefined
121
- } catch {
122
- return undefined
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
- let entries
153
- try {
154
- entries = readdirSync(dir, { withFileTypes: true })
155
- } catch {
156
- return undefined
157
- }
158
- const log = entries.find((e) => e.isFile() && isSessionLogFile(e.name))
159
- if (log === undefined) return undefined
160
- const path = join(dir, log.name)
161
- let stat
162
- try {
163
- stat = statSync(path)
164
- } catch {
165
- return undefined
166
- }
167
- const key = `${path}:${stat.mtimeMs}:${stat.size}`
168
- if (cache.has(key)) return cache.get(key)
169
- let header
170
- try {
171
- const raw = readFileSync(path)
172
- const text = log.name.endsWith('.zstd')
173
- ? zstdDecompress === undefined
174
- ? undefined
175
- : zstdDecompress(raw).toString('utf8')
176
- : raw.toString('utf8')
177
- if (text !== undefined) {
178
- const end = text.indexOf('\n')
179
- header = JSON.parse(end === -1 ? text : text.slice(0, end))
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
- } catch {
182
- header = undefined
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
- const compressed = options.compressed !== false
202
- if (typeof newCwd !== 'string' || newCwd.length === 0) {
203
- return { buffer, rewritten: false, reason: 'empty target cwd' }
204
- }
205
- if (!compressed) {
206
- const text = buffer.toString('utf8')
207
- const newlineIndex = text.indexOf('\n')
208
- const firstLine = newlineIndex === -1 ? text : text.slice(0, newlineIndex)
209
- const rest = newlineIndex === -1 ? '' : text.slice(newlineIndex)
210
- let header
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
- header = JSON.parse(firstLine)
213
- } catch {
214
- return { buffer, rewritten: false, reason: 'header line is not JSON' }
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
- return { buffer, rewritten: false, reason: 'header has no cwd' }
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
- if (header.cwd === newCwd) return { buffer, rewritten: false, header }
220
- const rebuilt = {}
221
- for (const [key, value] of Object.entries(header)) rebuilt[key] = key === 'cwd' ? newCwd : value
222
- return { buffer: Buffer.from(JSON.stringify(rebuilt) + rest, 'utf8'), rewritten: true, header: rebuilt }
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 { appendFileSync, mkdirSync } from 'node:fs'
19
- import { homedir } from 'node:os'
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
- const fromEnv = process.env.DSH_HOME
25
- return fromEnv !== undefined && fromEnv !== '' ? fromEnv : join(homedir(), '.dsh')
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
- const name = typeof options.name === 'string' && options.name.length > 0 ? options.name : 'dsh-session-ops'
54
- const formatLine = typeof options.formatLine === 'function' ? options.formatLine : LOG_LINE_LEVEL_FIRST
55
- let path
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
- ctx?.logger?.[level]?.(message)
66
- } catch {
67
- /* 宿主 logger 不可用不影响文件日志 */
52
+ const dir = join(dshHome(), 'logs');
53
+ mkdirSync(dir, { recursive: true });
54
+ path = join(dir, `${name}.log`);
68
55
  }
69
- if (path === undefined) return
70
- try {
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
- const source = raw !== null && typeof raw === 'object' ? raw : {}
85
- const plain = {}
86
- for (const [key, value] of Object.entries(source)) {
87
- plain[key] = value !== null && typeof value === 'object' && typeof value.get === 'function' ? value.get() : value
88
- }
89
- return plain
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 由调用方传入(本包不依赖 `@deepseek-ai/schemastery`),传进来的只要是
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
- return plainConfig(schema(plainConfig(raw)))
103
+ return plainConfig(schema(plainConfig(raw)));
105
104
  }