@moonquake2004/dsh-security 0.1.0 → 0.1.2
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 +230 -48
- package/package.json +1 -1
- package/src/checks/index.mjs +1 -0
- package/src/checks/sl1-supply-chain.mjs +44 -15
- package/src/checks/sl3-reputation-score.mjs +22 -9
- package/src/checks/sl4-release-compat.mjs +16 -9
- package/src/checks/sp2-secret-scan.mjs +3 -1
- package/src/checks/sp4-entry-poison.mjs +4 -1
- package/src/checks/sp5-permission-model.mjs +24 -6
- package/src/checks/sp6-vuln-match.mjs +41 -12
- package/src/checks/sp7-client-syntax.mjs +150 -0
- package/src/checks/sr1-sandbox-violation.mjs +35 -41
- package/src/checks/sr2-privilege-escalation.mjs +22 -15
- package/src/checks/sr3-data-exfiltration.mjs +27 -50
- package/src/checks/sr4-isolation-verify.mjs +18 -13
- package/src/checks/ss1-credential-leak.mjs +30 -48
- package/src/checks/ss2-pii-exposure.mjs +22 -12
- package/src/checks/ss3-sensitive-output.mjs +17 -14
- package/src/config.mjs +21 -16
- package/src/index.mjs +4 -2
- package/src/integrations/ecosystem.mjs +60 -23
- package/src/integrations/plugin-reducer.mjs +17 -14
- package/src/integrations/poison-guard.mjs +9 -15
- package/src/integrations/sandbox-audit.mjs +10 -11
- package/src/protocol/check.mjs +9 -0
- package/src/registry.mjs +69 -16
- package/src/session-reader.mjs +63 -0
|
@@ -2,22 +2,21 @@
|
|
|
2
2
|
* dsh-plugin-reducer 集成
|
|
3
3
|
*
|
|
4
4
|
* 在检测到故障时,提供最小化故障插件集的能力。
|
|
5
|
-
* 如果 dsh-plugin-reducer
|
|
5
|
+
* 如果 dsh-plugin-reducer 已安装(PATH 中可执行),自动建议运行。
|
|
6
|
+
* 复审修复:不再用 `npx 包名` 探测/执行;执行失败返回 skip 而不是伪装通过。
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
9
10
|
import { Severity } from '../protocol/severity.mjs';
|
|
10
11
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
12
|
+
import { skip } from '../protocol/check.mjs';
|
|
11
13
|
|
|
12
14
|
export function isAvailable() {
|
|
13
15
|
try {
|
|
14
|
-
|
|
16
|
+
execFileSync('which', ['dsh-plugin-reducer'], { encoding: 'utf8', stdio: 'pipe' });
|
|
15
17
|
return true;
|
|
16
18
|
} catch {
|
|
17
|
-
|
|
18
|
-
execSync('npx dsh-plugin-reducer --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
19
|
-
return true;
|
|
20
|
-
} catch { return false; }
|
|
19
|
+
return false;
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -25,15 +24,19 @@ export async function runReducer(profileDir, probeType = 'web') {
|
|
|
25
24
|
const id = 'EXT-RED-1';
|
|
26
25
|
|
|
27
26
|
if (!isAvailable()) {
|
|
28
|
-
return
|
|
27
|
+
return skip(id, Severity.LOW, 'dsh-plugin-reducer 未安装,跳过故障最小化');
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
try {
|
|
32
|
-
const output =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
const output = execFileSync(
|
|
32
|
+
'dsh-plugin-reducer',
|
|
33
|
+
['--profile', String(profileDir), '--probe', String(probeType), '--report', '/tmp/reducer-report.json', '--json'],
|
|
34
|
+
{
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
timeout: 120000,
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
}
|
|
39
|
+
);
|
|
37
40
|
|
|
38
41
|
const result = JSON.parse(output);
|
|
39
42
|
|
|
@@ -50,7 +53,7 @@ export async function runReducer(profileDir, probeType = 'web') {
|
|
|
50
53
|
|
|
51
54
|
return { id, ok: true, severity: Severity.LOW, detail: 'dsh-plugin-reducer 未发现故障插件集' };
|
|
52
55
|
} catch (e) {
|
|
53
|
-
return
|
|
56
|
+
return skip(id, Severity.LOW, `dsh-plugin-reducer 执行失败,跳过:${e.message.slice(0, 80)}`);
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-poison-guard 集成
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 如果 dsh-poison-guard 已安装(PATH 中可执行),自动集成到安全检查流程。
|
|
5
|
+
* 复审修复:不再用 `npx 包名` 探测/执行;执行失败返回 skip 而不是伪装通过。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
10
9
|
import { Severity } from '../protocol/severity.mjs';
|
|
11
10
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
11
|
+
import { skip } from '../protocol/check.mjs';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 检测 dsh-poison-guard 是否可用
|
|
15
15
|
*/
|
|
16
16
|
export function isAvailable() {
|
|
17
17
|
try {
|
|
18
|
-
|
|
18
|
+
execFileSync('which', ['dsh-poison-guard'], { encoding: 'utf8', stdio: 'pipe' });
|
|
19
19
|
return true;
|
|
20
20
|
} catch {
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
execSync('npx dsh-poison-guard --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
24
|
-
return true;
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
21
|
+
return false;
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
|
|
@@ -37,11 +31,11 @@ export async function runScan(targetPath) {
|
|
|
37
31
|
const id = 'EXT-PG-1';
|
|
38
32
|
|
|
39
33
|
if (!isAvailable()) {
|
|
40
|
-
return
|
|
34
|
+
return skip(id, Severity.HIGH, 'dsh-poison-guard 未安装,跳过投毒扫描');
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
try {
|
|
44
|
-
const output =
|
|
38
|
+
const output = execFileSync('dsh-poison-guard', ['scan', String(targetPath), '--json'], {
|
|
45
39
|
encoding: 'utf8',
|
|
46
40
|
timeout: 60000,
|
|
47
41
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -64,7 +58,7 @@ export async function runScan(targetPath) {
|
|
|
64
58
|
fix: '检查相关插件的源码,移除恶意代码',
|
|
65
59
|
};
|
|
66
60
|
} catch (e) {
|
|
67
|
-
return
|
|
61
|
+
return skip(id, Severity.HIGH, `dsh-poison-guard 执行失败,跳过:${e.message.slice(0, 80)}`);
|
|
68
62
|
}
|
|
69
63
|
}
|
|
70
64
|
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-sandbox-audit 集成
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 如果 dsh-sandbox-audit 已安装(PATH 中可执行),自动集成到安全检查流程。
|
|
5
|
+
* 复审修复:不再用 `npx 包名` 探测可用性——那会在用户机器上触发任意包的下载执行;
|
|
6
|
+
* 执行失败现在返回 skip 而不是伪装成通过。
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
9
10
|
import { Severity } from '../protocol/severity.mjs';
|
|
10
11
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
12
|
+
import { skip } from '../protocol/check.mjs';
|
|
11
13
|
|
|
12
14
|
export function isAvailable() {
|
|
13
15
|
try {
|
|
14
|
-
|
|
16
|
+
execFileSync('which', ['dsh-sandbox-audit'], { encoding: 'utf8', stdio: 'pipe' });
|
|
15
17
|
return true;
|
|
16
18
|
} catch {
|
|
17
|
-
|
|
18
|
-
execSync('npx dsh-sandbox-audit --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
19
|
-
return true;
|
|
20
|
-
} catch { return false; }
|
|
19
|
+
return false;
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -25,11 +24,11 @@ export async function runAudit(profileDir) {
|
|
|
25
24
|
const id = 'EXT-SA-1';
|
|
26
25
|
|
|
27
26
|
if (!isAvailable()) {
|
|
28
|
-
return
|
|
27
|
+
return skip(id, Severity.MEDIUM, 'dsh-sandbox-audit 未安装,跳过沙箱策略审计');
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
try {
|
|
32
|
-
const output =
|
|
31
|
+
const output = execFileSync('dsh-sandbox-audit', [String(profileDir), '--json'], {
|
|
33
32
|
encoding: 'utf8',
|
|
34
33
|
timeout: 60000,
|
|
35
34
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -52,7 +51,7 @@ export async function runAudit(profileDir) {
|
|
|
52
51
|
fix: '参考 dsh-sandbox-audit 文档修复沙箱配置',
|
|
53
52
|
};
|
|
54
53
|
} catch (e) {
|
|
55
|
-
return
|
|
54
|
+
return skip(id, Severity.MEDIUM, `dsh-sandbox-audit 执行失败,跳过:${e.message.slice(0, 80)}`);
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
package/src/protocol/check.mjs
CHANGED
|
@@ -47,3 +47,12 @@ export function pass(id, severity, detail) {
|
|
|
47
47
|
export function fail(id, severity, detail, fix, references = []) {
|
|
48
48
|
return createResult(id, false, severity, detail, fix, references);
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 跳过结果——检查因外部条件不满足而未真正执行(如网络不可达、依赖工具缺失)。
|
|
53
|
+
* skipped=true 的结果不计入失败统计,也不影响退出码;
|
|
54
|
+
* detail 必须说明原因(对齐 #1719 r5 词汇表「skip 必须带 reason」)。
|
|
55
|
+
*/
|
|
56
|
+
export function skip(id, severity, detail) {
|
|
57
|
+
return { id, ok: true, skipped: true, severity, detail };
|
|
58
|
+
}
|
package/src/registry.mjs
CHANGED
|
@@ -2,22 +2,41 @@
|
|
|
2
2
|
* DSH Security Framework — Check Registry
|
|
3
3
|
*
|
|
4
4
|
* 管理安全检查的注册、发现和执行。
|
|
5
|
-
*
|
|
5
|
+
* 支持内置检查、外部工具集成(自动探测)和 checks.d JSON 注册(显式启用)。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { maxSeverity, severityToExitCode } from './protocol/severity.mjs';
|
|
10
|
+
import { maxSeverity, severityToExitCode, severityGte } from './protocol/severity.mjs';
|
|
11
|
+
|
|
12
|
+
const VALID_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
11
13
|
|
|
12
14
|
export class SecurityCheckRegistry {
|
|
13
15
|
constructor() {
|
|
14
16
|
/** @type {Map<string, import('./protocol/check.mjs').SecurityCheck>} */
|
|
15
17
|
this.checks = new Map();
|
|
18
|
+
/** @type {object|null} 可选运行配置(见 config.mjs / README 配置节) */
|
|
19
|
+
this.config = null;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
register(check) { this.checks.set(check.id, check); }
|
|
19
23
|
registerAll(checks) { for (const check of checks) this.register(check); }
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 注入运行配置(~/.dsh/security.json):
|
|
27
|
+
* - enabled=false → 全部停用
|
|
28
|
+
* - checks.{ID}.enabled=false → 停用单个检查
|
|
29
|
+
* - severityThreshold → 低于阈值的失败降级为 skipped(不影响退出码)
|
|
30
|
+
*/
|
|
31
|
+
setConfig(config) {
|
|
32
|
+
this.config = config && typeof config === 'object' ? config : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Plugin Interface:从 <securityDir>/checks.d/*.json 加载外部注册的检查。
|
|
37
|
+
* 注意:JSON 中的 command 会被执行——只应加载用户自己放置的文件,
|
|
38
|
+
* 且目录由调用方显式传入(框架不会默认扫描任何位置)。
|
|
39
|
+
*/
|
|
21
40
|
loadExternalChecks(securityDir) {
|
|
22
41
|
const checksDir = join(securityDir, 'checks.d');
|
|
23
42
|
if (!existsSync(checksDir)) return;
|
|
@@ -27,17 +46,20 @@ export class SecurityCheckRegistry {
|
|
|
27
46
|
const reg = JSON.parse(readFileSync(join(checksDir, file), 'utf8'));
|
|
28
47
|
if (reg.checks && Array.isArray(reg.checks)) {
|
|
29
48
|
for (const ext of reg.checks) {
|
|
49
|
+
if (!ext || !ext.id || !ext.command) continue;
|
|
30
50
|
this.register({
|
|
31
|
-
id: ext.id, name: ext.name, severity: ext.severity
|
|
32
|
-
|
|
51
|
+
id: ext.id, name: ext.name || ext.id, severity: ext.severity || 'medium',
|
|
52
|
+
phase: ext.phase || 'post-install',
|
|
53
|
+
description: ext.description || `External check (${reg.source || file})`,
|
|
54
|
+
src: 'external', source: reg.source || file,
|
|
33
55
|
runner: async () => {
|
|
34
56
|
const { execSync } = await import('node:child_process');
|
|
35
57
|
try {
|
|
36
58
|
const output = execSync(ext.command, { encoding: 'utf8', timeout: 30000 });
|
|
37
59
|
const result = JSON.parse(output);
|
|
38
|
-
return { id: ext.id, ok: result.ok ?? true, severity: ext.severity, detail: result.detail || 'External check completed', fix: result.fix, references: result.references };
|
|
60
|
+
return { id: ext.id, ok: result.ok ?? true, severity: ext.severity || 'medium', detail: result.detail || 'External check completed', fix: result.fix, references: result.references };
|
|
39
61
|
} catch (e) {
|
|
40
|
-
return { id: ext.id, ok: false, severity: ext.severity, detail: `External check failed: ${e.message}` };
|
|
62
|
+
return { id: ext.id, ok: false, severity: ext.severity || 'medium', detail: `External check failed: ${e.message}` };
|
|
41
63
|
}
|
|
42
64
|
},
|
|
43
65
|
});
|
|
@@ -56,7 +78,18 @@ export class SecurityCheckRegistry {
|
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
async runAll(contextFn, phase = null) {
|
|
59
|
-
|
|
81
|
+
let checks = phase ? this.getByPhase(phase) : [...this.checks.values()];
|
|
82
|
+
|
|
83
|
+
// 应用配置过滤(未 setConfig 时全部启用)
|
|
84
|
+
const cfg = this.config;
|
|
85
|
+
if (cfg && cfg.enabled === false) {
|
|
86
|
+
checks = [];
|
|
87
|
+
} else if (cfg && cfg.checks) {
|
|
88
|
+
checks = checks.filter(c => !(cfg.checks[c.id] && cfg.checks[c.id].enabled === false));
|
|
89
|
+
}
|
|
90
|
+
const threshold = cfg && cfg.severityThreshold && VALID_SEVERITIES.includes(cfg.severityThreshold)
|
|
91
|
+
? cfg.severityThreshold : null;
|
|
92
|
+
|
|
60
93
|
const results = [];
|
|
61
94
|
for (const check of checks) {
|
|
62
95
|
try {
|
|
@@ -67,20 +100,34 @@ export class SecurityCheckRegistry {
|
|
|
67
100
|
results.push({ id: check.id, ok: false, severity: check.severity, detail: `Check execution failed: ${e.message}` });
|
|
68
101
|
}
|
|
69
102
|
}
|
|
70
|
-
|
|
103
|
+
|
|
104
|
+
// severityThreshold:低于阈值的失败降级为 skip(保留原因),不进失败统计与退出码
|
|
105
|
+
const finalResults = results.map((r) => {
|
|
106
|
+
if (r.ok || r.skipped || !threshold) return r;
|
|
107
|
+
const sev = VALID_SEVERITIES.includes(r.severity) ? r.severity : 'medium';
|
|
108
|
+
if (severityGte(threshold, sev)) {
|
|
109
|
+
return { ...r, ok: true, skipped: true, detail: `${r.detail}\n[severityThreshold=${threshold}:低于阈值,已降级为 skip]` };
|
|
110
|
+
}
|
|
111
|
+
return r;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const summary = { critical: 0, high: 0, medium: 0, low: 0, info: 0, skipped: 0 };
|
|
71
115
|
const failedSeverities = [];
|
|
72
|
-
for (const r of
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
116
|
+
for (const r of finalResults) {
|
|
117
|
+
if (r.skipped) { summary.skipped++; continue; }
|
|
118
|
+
if (!r.ok) {
|
|
119
|
+
// 无效 severity 的失败按 medium 计,避免静默丢失退出码信号
|
|
120
|
+
const sev = VALID_SEVERITIES.includes(r.severity) ? r.severity : 'medium';
|
|
121
|
+
summary[sev]++;
|
|
122
|
+
failedSeverities.push(sev);
|
|
76
123
|
}
|
|
77
124
|
}
|
|
78
125
|
const exitCode = failedSeverities.length > 0 ? severityToExitCode[maxSeverity(failedSeverities)] : 0;
|
|
79
|
-
return { results, exitCode, summary };
|
|
126
|
+
return { results: finalResults, exitCode, summary };
|
|
80
127
|
}
|
|
81
128
|
}
|
|
82
129
|
|
|
83
|
-
export async function createDefaultRegistry(loadExternal = true) {
|
|
130
|
+
export async function createDefaultRegistry(loadExternal = true, options = {}) {
|
|
84
131
|
const registry = new SecurityCheckRegistry();
|
|
85
132
|
const modules = await Promise.all([
|
|
86
133
|
import('./checks/sp1-dependency-audit.mjs'),
|
|
@@ -100,6 +147,7 @@ export async function createDefaultRegistry(loadExternal = true) {
|
|
|
100
147
|
import('./checks/ss1-credential-leak.mjs'),
|
|
101
148
|
import('./checks/ss2-pii-exposure.mjs'),
|
|
102
149
|
import('./checks/ss3-sensitive-output.mjs'),
|
|
150
|
+
import('./checks/sp7-client-syntax.mjs'),
|
|
103
151
|
]);
|
|
104
152
|
for (const mod of modules) {
|
|
105
153
|
for (const val of Object.values(mod)) {
|
|
@@ -109,8 +157,13 @@ export async function createDefaultRegistry(loadExternal = true) {
|
|
|
109
157
|
}
|
|
110
158
|
}
|
|
111
159
|
|
|
112
|
-
//
|
|
113
|
-
|
|
160
|
+
// Plugin Interface:仅在调用方显式给出目录时启用(安全考虑,不默认扫描)
|
|
161
|
+
const extDir = typeof loadExternal === 'object' ? loadExternal.externalChecksDir : options.externalChecksDir;
|
|
162
|
+
if (extDir) registry.loadExternalChecks(extDir);
|
|
163
|
+
|
|
164
|
+
// 自动探测外部工具集成(dsh-poison-guard 等)
|
|
165
|
+
const wantIntegrations = typeof loadExternal === 'object' ? (loadExternal.integrations !== false) : loadExternal;
|
|
166
|
+
if (wantIntegrations) {
|
|
114
167
|
try {
|
|
115
168
|
const { getAvailableIntegrations } = await import('./integrations/index.mjs');
|
|
116
169
|
const integrations = await getAvailableIntegrations();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Security Framework — 会话日志读取
|
|
3
|
+
*
|
|
4
|
+
* SR/SS 系列检查共用:透明支持明文 .jsonl 与 zstd 压缩的 session.jsonl.zstd
|
|
5
|
+
* (真实 DSH 部署中会话日志为 ~/.dsh/sessions/<user>/<session>/session.jsonl.zstd)。
|
|
6
|
+
*
|
|
7
|
+
* 依赖系统 zstd 命令解压(macOS: brew install zstd;Debian: apt install zstd)。
|
|
8
|
+
* 二进制不可用时抛 code='ZSTD_UNAVAILABLE',检查应转为 skip 而非误报通过。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createReadStream } from 'node:fs';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
14
|
+
|
|
15
|
+
export function isZstdFile(sessionFile) {
|
|
16
|
+
return typeof sessionFile === 'string' && (sessionFile.endsWith('.zstd') || sessionFile.endsWith('.zst'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function openStream(sessionFile) {
|
|
20
|
+
if (isZstdFile(sessionFile)) {
|
|
21
|
+
const child = spawn('zstd', ['-dc', sessionFile], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
22
|
+
let stderrTail = '';
|
|
23
|
+
let spawnErr = null;
|
|
24
|
+
child.stderr.on('data', (d) => { stderrTail += String(d); });
|
|
25
|
+
const settled = new Promise((resolve) => {
|
|
26
|
+
child.once('error', (e) => { spawnErr = e; resolve(); });
|
|
27
|
+
child.once('close', () => resolve());
|
|
28
|
+
});
|
|
29
|
+
return { stream: child.stdout, settled, getErr: () => (spawnErr ? spawnErr : stderrTail.trim() || null) };
|
|
30
|
+
}
|
|
31
|
+
return { stream: createReadStream(sessionFile, { encoding: 'utf8' }), settled: Promise.resolve(), getErr: () => null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 逐行扫描会话文件(自动解压 zstd)。
|
|
36
|
+
* @param {string} sessionFile 会话文件路径
|
|
37
|
+
* @param {(line: string, lineNo: number) => void|Promise<void>} onLine 每行回调
|
|
38
|
+
* @returns {Promise<number>} 实际读取的总行数
|
|
39
|
+
*/
|
|
40
|
+
export async function scanSessionLines(sessionFile, onLine) {
|
|
41
|
+
const { stream, settled, getErr } = openStream(sessionFile);
|
|
42
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
43
|
+
let lineCount = 0;
|
|
44
|
+
try {
|
|
45
|
+
for await (const line of rl) {
|
|
46
|
+
lineCount++;
|
|
47
|
+
await onLine(line, lineCount);
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
await settled.catch(() => {});
|
|
51
|
+
if (typeof stream.destroy === 'function') stream.destroy();
|
|
52
|
+
}
|
|
53
|
+
const err = getErr();
|
|
54
|
+
if (err) {
|
|
55
|
+
if (err.code === 'ENOENT') {
|
|
56
|
+
const e = new Error('zstd 命令不可用(PATH 中未找到),无法解压压缩会话日志');
|
|
57
|
+
e.code = 'ZSTD_UNAVAILABLE';
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`会话日志读取失败:${String(err).split('\n').pop().slice(0, 100)}`);
|
|
61
|
+
}
|
|
62
|
+
return lineCount;
|
|
63
|
+
}
|