openclaw-sc 5.38.0
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/.env.example +13 -0
- package/INSTALL.md +13 -0
- package/LICENSE +233 -0
- package/README.md +123 -0
- package/SECURITY.md +27 -0
- package/index.js +3479 -0
- package/lib/code-review-shared.js +164 -0
- package/lib/config.js +164 -0
- package/lib/constants.js +438 -0
- package/lib/decomposer.js +896 -0
- package/lib/dialog-recall.js +389 -0
- package/lib/env.js +59 -0
- package/lib/level-rules.js +72 -0
- package/lib/log-manager.js +258 -0
- package/lib/preemption.js +278 -0
- package/lib/priority-calc.js +134 -0
- package/lib/prompt-injection.js +748 -0
- package/lib/route-evidence.js +701 -0
- package/lib/shared-fs.js +244 -0
- package/lib/steward-rules.js +648 -0
- package/lib/task-center.js +602 -0
- package/lib/task-chain.js +713 -0
- package/lib/task-checker.js +317 -0
- package/lib/task-profiles.js +255 -0
- package/lib/tcell.js +411 -0
- package/lib/tool-auto-discover.js +522 -0
- package/lib/tool-enrich-subagent.js +375 -0
- package/lib/tool-handlers.js +178 -0
- package/lib/tool-selector.js +459 -0
- package/openclaw.plugin.json +55 -0
- package/package.json +63 -0
- package/security.js +141 -0
- package/tools/bridge.js +3288 -0
- package/tools/dashboard/tk-dashboard.py +262 -0
- package/tools/hippocampus-multi-search.js +1038 -0
- package/tools/hippocampus-sqlite.js +738 -0
- package/tools/hippocampus-store.js +262 -0
- package/tools/mcp-tools.config.json +653 -0
- package/tools/pipeline-engine.js +667 -0
- package/tools/sidecar/_check_tools.py +34 -0
- package/tools/sidecar/sidecar-server.cjs +1360 -0
- package/tools/sidecar/subagent-runner.cjs +1471 -0
- package/tools/system-tools.js +619 -0
- package/vector/README.md +100 -0
- package/vector/usearch-bridge.js +639 -0
- package/vector/usearch-http.js +74 -0
- package/vector/usearch-serve.js +156 -0
- package/workers/worker.js +1419 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦞 sc — 系统运维工具模块
|
|
3
|
+
*
|
|
4
|
+
* 🧠 设计决策:子 agent 专用工具。
|
|
5
|
+
* 为什么主 agent 不能直接调:系统运维操作(下载/安装/端口/服务/磁盘/Ollama/Git/网络)
|
|
6
|
+
* 涉及阻塞调用(spawnSync、execSync)、长超时(120s-300s)、系统级副作用(注册表、
|
|
7
|
+
* 防火墙、服务启停)。放在子 agent 执行可利用独立超时和熔断,不阻塞主线程。
|
|
8
|
+
* 主 agent 调会被 Steward 拦截(force_delegate 级),需 core_allowDirectCall 放行。
|
|
9
|
+
*
|
|
10
|
+
* 12种操作聚合在一个 switch-case:不是冗余,是故意统一对外暴露单一工具名,
|
|
11
|
+
* 内部分发。比12个独立工具更简洁,参数 schema 也在一个地方维护。
|
|
12
|
+
*
|
|
13
|
+
* 🧠 设计决策:内部用 spawnSync/execSync 同步调用。
|
|
14
|
+
* 子 agent 专用上下文,阻塞是安全的——子 agent 有独立超时和熔断机制。
|
|
15
|
+
* 如果主 agent 调这些同步函数才会卡主线程,所以子 agent 专用是保护性设计。
|
|
16
|
+
*
|
|
17
|
+
* 下载/安装/端口/服务/磁盘/Ollama/Git/网络等运维操作
|
|
18
|
+
* 独立于 index.js,只通过 import 注册 2 行代码
|
|
19
|
+
*
|
|
20
|
+
* ⚡ 安全改造:2026-06-01
|
|
21
|
+
* 所有 run() 替换为 spawnRun(),消除 shell pipe 注入。
|
|
22
|
+
* netstat | findstr → spawnSync('netstat', ['-ano']) + JS 过滤
|
|
23
|
+
* powershell -Command 字符串注入 → spawnSync 数组参数
|
|
24
|
+
* taskkill /pid ${pid} → spawnSync('taskkill', ['/pid', pid, '/f'])
|
|
25
|
+
* tar/powershell Expand-Archive → spawnSync 数组参数
|
|
26
|
+
* env echo %VAR% → process.env 直接读取
|
|
27
|
+
* setx 字符串注入 → spawnSync('setx', [name, value]) 数组参数
|
|
28
|
+
*
|
|
29
|
+
* ⚡ 安全改造:2026-06-08 (fix-08)
|
|
30
|
+
* handleSystemDownload 路径遍历漏洞修复:加入路径归一化校验,确保 destPath 在 workspace 内
|
|
31
|
+
* handleSystemDisk 磁盘百分比公式修复:$_.Free/$_.Used → $_.Free/$_.Total
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { writeFile, unlink, mkdir, stat, readdir } from 'fs/promises';
|
|
35
|
+
import { join, resolve, normalize } from 'path';
|
|
36
|
+
import { spawnSync } from 'child_process';
|
|
37
|
+
import { homedir, freemem, totalmem, platform } from 'os';
|
|
38
|
+
import { createRequire } from 'module';
|
|
39
|
+
const require = createRequire(import.meta.url);
|
|
40
|
+
|
|
41
|
+
// ====== 常量 ======
|
|
42
|
+
const PROXY_PORT = 7892;
|
|
43
|
+
const PROXY_HOST = '127.0.0.1';
|
|
44
|
+
const DOWNLOAD_TIMEOUT_MS = 120000;
|
|
45
|
+
const MAX_RETRIES = 2;
|
|
46
|
+
const RETRY_DELAY_MS = 3000;
|
|
47
|
+
const WORKSPACE_DIR = resolve(homedir(), '.openclaw', 'workspace');
|
|
48
|
+
const OLLAMA_DIR = resolve(homedir(), 'AppData', 'Local', 'Programs', 'Ollama');
|
|
49
|
+
const OLLAMA_EXE = join(OLLAMA_DIR, 'ollama.exe');
|
|
50
|
+
|
|
51
|
+
// ====== 内部工具 ======
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 安全执行系统命令 — 使用 spawnSync 数组参数,无 shell 注入风险。
|
|
55
|
+
* 替代旧的 run()(使用 execSync + 字符串拼接,易注入)。
|
|
56
|
+
*/
|
|
57
|
+
function spawnRun(cmd, args, timeout = 30000) {
|
|
58
|
+
try {
|
|
59
|
+
const result = spawnSync(cmd, args, {
|
|
60
|
+
timeout, stdio: 'pipe', encoding: 'utf-8',
|
|
61
|
+
env: { ...process.env, NO_PROXY: '*' }
|
|
62
|
+
});
|
|
63
|
+
return result.stdout || '';
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function psCmd(script) {
|
|
70
|
+
return spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
71
|
+
timeout: 30000, stdio: 'pipe', encoding: 'utf-8',
|
|
72
|
+
env: { ...process.env, NO_PROXY: '*' }
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ====== 网络下载 ======
|
|
77
|
+
|
|
78
|
+
async function downloadFile(url, destPath, useProxy) {
|
|
79
|
+
const { default: https } = await import('https');
|
|
80
|
+
const { default: http } = await import('http');
|
|
81
|
+
const { createWriteStream } = await import('fs');
|
|
82
|
+
|
|
83
|
+
const parsedUrl = new URL(url);
|
|
84
|
+
const mod = parsedUrl.protocol === 'https:' ? https : http;
|
|
85
|
+
|
|
86
|
+
const options = {
|
|
87
|
+
hostname: parsedUrl.hostname,
|
|
88
|
+
port: parsedUrl.port,
|
|
89
|
+
path: parsedUrl.pathname + parsedUrl.search
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (useProxy) {
|
|
93
|
+
options.hostname = PROXY_HOST;
|
|
94
|
+
options.port = PROXY_PORT;
|
|
95
|
+
options.path = url;
|
|
96
|
+
options.headers = { Host: parsedUrl.hostname };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const file = createWriteStream(destPath);
|
|
101
|
+
mod.get(options, (response) => {
|
|
102
|
+
if (response.statusCode !== 200) {
|
|
103
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
response.pipe(file);
|
|
107
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
108
|
+
}).on('error', (err) => {
|
|
109
|
+
file.close();
|
|
110
|
+
reject(err);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateFileSize(filePath) {
|
|
116
|
+
const { size } = require('fs').statSync(filePath);
|
|
117
|
+
if (size < 1024) throw new Error(`文件太小 (${size}B)`);
|
|
118
|
+
if (size > 500 * 1024 * 1024) throw new Error(`文件过大 (${(size/1024/1024).toFixed(1)}MB)`);
|
|
119
|
+
return size;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ====== Handler: download ======
|
|
123
|
+
|
|
124
|
+
export async function handleSystemDownload(params) {
|
|
125
|
+
const { url, destPath, proxy: proxyOpt } = params;
|
|
126
|
+
if (!url) throw new Error('missing url 参数');
|
|
127
|
+
|
|
128
|
+
const destDir = resolve(WORKSPACE_DIR, 'temp');
|
|
129
|
+
const fileName = url.split('/').pop() || `download-${Date.now()}`;
|
|
130
|
+
// ⚡ 安全校验:路径归一化 + 检查是否在 workspace 目录内,防止路径遍历
|
|
131
|
+
let outPath;
|
|
132
|
+
if (destPath) {
|
|
133
|
+
const normalized = resolve(normalize(destPath));
|
|
134
|
+
// 检查归一化后的路径是否以 WORKSPACE_DIR 开头(确保在允许目录内)
|
|
135
|
+
if (!normalized.startsWith(WORKSPACE_DIR + '\\') && normalized !== WORKSPACE_DIR) {
|
|
136
|
+
throw new Error(`路径越权: destPath (${destPath}) 不在允许的 workspace 目录内`);
|
|
137
|
+
}
|
|
138
|
+
outPath = normalized;
|
|
139
|
+
} else {
|
|
140
|
+
outPath = join(destDir, fileName);
|
|
141
|
+
}
|
|
142
|
+
await mkdir(destDir, { recursive: true }).catch(() => {});
|
|
143
|
+
|
|
144
|
+
let useProxy = false;
|
|
145
|
+
if (proxyOpt === 'auto' || proxyOpt === 'force') {
|
|
146
|
+
useProxy = proxyOpt === 'force' || await checkProxy();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let lastErr;
|
|
150
|
+
for (let i = 0; i <= MAX_RETRIES; i++) {
|
|
151
|
+
if (i > 0) { await new Promise(r => setTimeout(r, RETRY_DELAY_MS)); useProxy = !useProxy; }
|
|
152
|
+
try {
|
|
153
|
+
await downloadFile(url, outPath, useProxy);
|
|
154
|
+
const size = validateFileSize(outPath);
|
|
155
|
+
return { status: 'success', data: { file: outPath, size, proxy: useProxy, attempts: i + 1 } };
|
|
156
|
+
} catch (err) { lastErr = err; }
|
|
157
|
+
}
|
|
158
|
+
return { status: 'error', errorCode: 'DOWNLOAD_FAILED', errorDetail: lastErr?.message, recoveryHint: '手动下载' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ====== Handler: install ======
|
|
162
|
+
|
|
163
|
+
export function handleSystemInstall(params) {
|
|
164
|
+
const { exePath, args, silent } = params;
|
|
165
|
+
if (!exePath) throw new Error('missing exePath');
|
|
166
|
+
const installArgs = silent !== false ? [...(args || []), '/S'] : (args || []);
|
|
167
|
+
const result = spawnSync(exePath, installArgs, { timeout: 300000, stdio: silent !== false ? 'ignore' : 'inherit' });
|
|
168
|
+
if (result.error) return { status: 'error', errorCode: 'INSTALL_FAILED', errorDetail: result.error.message, recoveryHint: '手动安装' };
|
|
169
|
+
return { status: 'success', data: { exitCode: result.status } };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ====== Handler: check (进程/服务/端口检查) ======
|
|
173
|
+
|
|
174
|
+
export function handleSystemCheck(params) {
|
|
175
|
+
const { name, port } = params;
|
|
176
|
+
// 查端口占用 — spawnSync 数组参数 + JS 过滤,消除 pipe 注入
|
|
177
|
+
if (port) {
|
|
178
|
+
const netstatResult = spawnSync('netstat', ['-ano'], { timeout: 10000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
179
|
+
const out = netstatResult.stdout || '';
|
|
180
|
+
// JS 过滤而非 findstr pipe,杜绝 shell 注入
|
|
181
|
+
const lines = out.split('\\n').filter(l => l.includes(`:${port}`) && l.includes('LISTENING'));
|
|
182
|
+
if (lines.length === 0) return { status: 'success', data: { port, inUse: false } };
|
|
183
|
+
const pids = [...new Set(lines.map(l => l.trim().split(/\\s+/).pop()).filter(Boolean))];
|
|
184
|
+
const procs = pids.map(pid => {
|
|
185
|
+
// tasklist 用数组参数,无 shell 介入
|
|
186
|
+
const taskResult = spawnSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], { timeout: 5000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
187
|
+
const p = (taskResult.stdout || '').trim();
|
|
188
|
+
return { pid, name: p.split(',')[0]?.replace(/\"/g, '') || 'unknown' };
|
|
189
|
+
});
|
|
190
|
+
return { status: 'success', data: { port, inUse: true, processes: procs } };
|
|
191
|
+
}
|
|
192
|
+
// 查进程名 — spawnSync 数组参数
|
|
193
|
+
if (name) {
|
|
194
|
+
const taskResult = spawnSync('tasklist', ['/fi', `ImageName eq ${name}.exe`, '/fo', 'csv', '/nh'], { timeout: 5000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
195
|
+
const out = (taskResult.stdout || '').trim();
|
|
196
|
+
const pids = out.split('\\n').filter(l => l.trim()).map(l => l.split(',')[1]?.replace(/\"/g, '')).filter(Boolean);
|
|
197
|
+
return { status: 'success', data: { name, running: pids.length > 0, pids } };
|
|
198
|
+
}
|
|
199
|
+
// 查系统信息 — 原生 os 模块,无命令调用
|
|
200
|
+
return {
|
|
201
|
+
status: 'success',
|
|
202
|
+
data: {
|
|
203
|
+
os: platform(),
|
|
204
|
+
cpu: require('os').cpus().length + '核',
|
|
205
|
+
memory: `${(freemem() / 1024 / 1024 / 1024).toFixed(1)}GB/${(totalmem() / 1024 / 1024 / 1024).toFixed(1)}GB`,
|
|
206
|
+
uptime: `${Math.round(require('os').uptime() / 60)}min`,
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ====== Handler: port (端口释放) ======
|
|
212
|
+
|
|
213
|
+
export function handleSystemPort(params) {
|
|
214
|
+
const { action, port } = params;
|
|
215
|
+
if (action === 'check') {
|
|
216
|
+
return handleSystemCheck({ port });
|
|
217
|
+
}
|
|
218
|
+
if (action === 'free' && port) {
|
|
219
|
+
// netstat 用 spawnSync 数组参数 + JS 过滤
|
|
220
|
+
const netstatResult = spawnSync('netstat', ['-ano'], { timeout: 10000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
221
|
+
const out = netstatResult.stdout || '';
|
|
222
|
+
const lines = out.split('\\n').filter(l => l.includes(`:${port}`) && l.includes('LISTENING'));
|
|
223
|
+
const pids = [...new Set(lines.map(l => l.trim().split(/\\s+/).pop()).filter(Boolean))];
|
|
224
|
+
if (pids.length === 0) return { status: 'success', data: { port, freed: 0, message: '端口未被占用' } };
|
|
225
|
+
const killed = [];
|
|
226
|
+
for (const pid of pids) {
|
|
227
|
+
// taskkill 用数组参数,eliminate pid injection
|
|
228
|
+
spawnSync('taskkill', ['/pid', pid, '/f'], { timeout: 5000, stdio: 'pipe', env: { ...process.env, NO_PROXY: '*' } });
|
|
229
|
+
killed.push(pid);
|
|
230
|
+
}
|
|
231
|
+
return { status: 'success', data: { port, freed: killed.length, pids: killed } };
|
|
232
|
+
}
|
|
233
|
+
// 列出所有监听端口 — spawnSync 数组 + JS 过滤
|
|
234
|
+
const netstatResult = spawnSync('netstat', ['-ano'], { timeout: 10000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
235
|
+
const out = netstatResult.stdout || '';
|
|
236
|
+
const lines = out.split('\\n').filter(l => l.includes('LISTENING'));
|
|
237
|
+
const ports = lines.filter(l => l.trim()).map(l => {
|
|
238
|
+
const parts = l.trim().split(/\\s+/);
|
|
239
|
+
return { proto: parts[0], address: parts[1], port: parts[1]?.split(':').pop(), pid: parts.pop() };
|
|
240
|
+
});
|
|
241
|
+
return { status: 'success', data: { total: ports.length, ports } };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ====== Handler: disk (磁盘空间) ======
|
|
245
|
+
|
|
246
|
+
export function handleSystemDisk(params) {
|
|
247
|
+
const { path: checkPath } = params;
|
|
248
|
+
const target = checkPath || process.cwd().substring(0, 2);
|
|
249
|
+
// PS 命令用 spawnSync 数组参数;命令字符串为硬编码无用户输入,安全
|
|
250
|
+
// ⚡ fix-08: $_.Free/$_.Used → $_.Free/$_.Total (正确的磁盘百分比)
|
|
251
|
+
const result = spawnSync('powershell', [
|
|
252
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
253
|
+
"Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -gt 0 } | Select-Object Name, @{N='TotalGB';E={[math]::Round($_.Used/1GB,1)}}, @{N='FreeGB';E={[math]::Round($_.Free/1GB,1)}}, @{N='Pct';E={[math]::Round($_.Free/$_.Total*100,1)}} | ConvertTo-Json"
|
|
254
|
+
], { timeout: 15000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
255
|
+
const out = result.stdout || '';
|
|
256
|
+
try {
|
|
257
|
+
const drives = JSON.parse(out.trim());
|
|
258
|
+
const arr = Array.isArray(drives) ? drives : [drives];
|
|
259
|
+
const targetDrive = arr.find(d => d.Name === target[0].toUpperCase());
|
|
260
|
+
return { status: 'success', data: { drives: arr, warning: targetDrive && targetDrive.FreeGB < 5 ? '磁盘空间不足' : null } };
|
|
261
|
+
} catch {
|
|
262
|
+
return { status: 'success', data: { raw: out.substring(0, 500) } };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ====== Handler: service (服务管理) ======
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 用 sc.exe 替代 PowerShell 管理服务。
|
|
270
|
+
* sc.exe 接受数组参数,避免 PowerShell 字符串注入。
|
|
271
|
+
* sc 输出为英文格式(SERVICE_NAME / STATE / START_TYPE),直接解析。
|
|
272
|
+
*/
|
|
273
|
+
function parseScState(raw) {
|
|
274
|
+
const name = raw.match(/SERVICE_NAME\\s*:\\s*(.+)/)?.[1] || '';
|
|
275
|
+
const stateMatch = raw.match(/STATE\\s*:\\s*\\d+\\s+(\\w+)/);
|
|
276
|
+
const startTypeMatch = raw.match(/START_TYPE\\s*:\\s*\\d+\\s+(\\w+)/);
|
|
277
|
+
return {
|
|
278
|
+
Name: name.trim(),
|
|
279
|
+
Status: stateMatch?.[1] || 'UNKNOWN',
|
|
280
|
+
StartType: startTypeMatch?.[1] || 'UNKNOWN'
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function handleSystemService(params) {
|
|
285
|
+
const { action, name } = params;
|
|
286
|
+
if (!name) throw new Error('missing name');
|
|
287
|
+
|
|
288
|
+
// 用 sc.exe 替代 PowerShell(sc 接受数组参数,无注入风险)
|
|
289
|
+
if (action === 'status') {
|
|
290
|
+
const result = spawnSync('sc', ['query', name], { timeout: 30000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
291
|
+
const raw = result.stdout || '';
|
|
292
|
+
try {
|
|
293
|
+
return { status: 'success', data: parseScState(raw) };
|
|
294
|
+
} catch {
|
|
295
|
+
return { status: 'success', data: { raw: raw.substring(0, 300) } };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (action === 'start') {
|
|
300
|
+
const result = spawnSync('sc', ['start', name], { timeout: 30000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
301
|
+
return { status: 'success', data: { raw: (result.stdout || result.stderr || '').substring(0, 300) } };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (action === 'stop') {
|
|
305
|
+
spawnSync('sc', ['stop', name], { timeout: 30000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
306
|
+
return { status: 'success', data: { action: 'stop', name } };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (action === 'restart') {
|
|
310
|
+
spawnSync('sc', ['stop', name], { timeout: 30000, stdio: 'pipe', env: { ...process.env, NO_PROXY: '*' } });
|
|
311
|
+
// 等 1 s让服务完全停止
|
|
312
|
+
const startResult = spawnSync('sc', ['start', name], { timeout: 30000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
313
|
+
return { status: 'success', data: { action: 'restart', name, output: (startResult.stdout || '').substring(0, 200) } };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
throw new Error(`未知操作: ${action}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ====== Handler: ollama (Ollama 模型管理) ======
|
|
320
|
+
|
|
321
|
+
export function handleSystemOllama(params) {
|
|
322
|
+
const { action, model } = params;
|
|
323
|
+
|
|
324
|
+
if (action === 'status') {
|
|
325
|
+
const ver = spawnRun(OLLAMA_EXE, ['--version'], 10000).trim();
|
|
326
|
+
const list = spawnRun(OLLAMA_EXE, ['list'], 15000);
|
|
327
|
+
const models = list.split('\\n').filter(l => l.includes('GB') || l.includes('MB')).map(l => {
|
|
328
|
+
const parts = l.trim().split(/\\s+/);
|
|
329
|
+
return { name: parts[0], id: parts[1], size: parts[2], modified: parts.slice(3).join(' ') };
|
|
330
|
+
});
|
|
331
|
+
return { status: 'success', data: { version: ver, models, count: models.length } };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (action === 'pull' && model) {
|
|
335
|
+
const out = spawnRun(OLLAMA_EXE, ['pull', model], 300000);
|
|
336
|
+
return { status: 'success', data: { model, result: out.substring(0, 500) } };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (action === 'rm' && model) {
|
|
340
|
+
spawnRun(OLLAMA_EXE, ['rm', model], 30000);
|
|
341
|
+
return { status: 'success', data: { model, removed: true } };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
throw new Error(`ollama action 需为 status/pull/rm`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ====== Handler: git (简易Git操作) ======
|
|
348
|
+
|
|
349
|
+
export function handleSystemGit(params) {
|
|
350
|
+
const { action, repo, dest, message } = params;
|
|
351
|
+
|
|
352
|
+
if (action === 'clone' && repo) {
|
|
353
|
+
const target = dest || join(WORKSPACE_DIR, 'temp', repo.split('/').pop().replace('.git', ''));
|
|
354
|
+
spawnRun('git', ['clone', repo, target], 120000);
|
|
355
|
+
return { status: 'success', data: { repo, dest: target } };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (action === 'pull') {
|
|
359
|
+
const cwd = dest || WORKSPACE_DIR;
|
|
360
|
+
const out = spawnRun('git', ['-C', cwd, 'pull'], 60000);
|
|
361
|
+
return { status: 'success', data: { dir: cwd, result: out.substring(0, 500) } };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (action === 'commit' && message) {
|
|
365
|
+
const cwd = dest || WORKSPACE_DIR;
|
|
366
|
+
spawnRun('git', ['-C', cwd, 'add', '-A'], 30000);
|
|
367
|
+
spawnRun('git', ['-C', cwd, 'commit', '-m', message], 30000);
|
|
368
|
+
return { status: 'success', data: { dir: cwd, message } };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (action === 'status') {
|
|
372
|
+
const cwd = dest || WORKSPACE_DIR;
|
|
373
|
+
const out = spawnRun('git', ['-C', cwd, 'status', '--short'], 30000);
|
|
374
|
+
return { status: 'success', data: { dir: cwd, dirty: out.trim().length > 0, changes: out.substring(0, 1000) } };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
throw new Error('git action 需为 clone/pull/commit/status');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ====== Handler: unzip (解压缩) ======
|
|
381
|
+
|
|
382
|
+
export function handleSystemUnzip(params) {
|
|
383
|
+
const { file, dest } = params;
|
|
384
|
+
if (!file) throw new Error('missing file');
|
|
385
|
+
const targetDir = dest || join(WORKSPACE_DIR, 'temp', `extract-${Date.now()}`);
|
|
386
|
+
mkdir(targetDir, { recursive: true }).catch(() => {});
|
|
387
|
+
|
|
388
|
+
if (file.endsWith('.zip')) {
|
|
389
|
+
// Expand-Archive 用 spawnSync 数组参数,避免 PS 字符串注入
|
|
390
|
+
spawnSync('powershell', [
|
|
391
|
+
'-NoProfile', '-NonInteractive',
|
|
392
|
+
'Expand-Archive', '-Path', file, '-DestinationPath', targetDir, '-Force'
|
|
393
|
+
], { timeout: 60000, stdio: 'pipe', env: { ...process.env, NO_PROXY: '*' } });
|
|
394
|
+
} else if (file.endsWith('.tar.gz') || file.endsWith('.tgz')) {
|
|
395
|
+
spawnRun('tar', ['-xzf', file, '-C', targetDir], 60000);
|
|
396
|
+
} else if (file.endsWith('.tar')) {
|
|
397
|
+
spawnRun('tar', ['-xf', file, '-C', targetDir], 60000);
|
|
398
|
+
} else throw new Error(`不支持的解压格式: ${file.split('.').pop()}`);
|
|
399
|
+
|
|
400
|
+
return { status: 'success', data: { file, dest: targetDir } };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ====== Handler: env (环境变量) ======
|
|
404
|
+
|
|
405
|
+
export function handleSystemEnv(params) {
|
|
406
|
+
const { action, name, value } = params;
|
|
407
|
+
|
|
408
|
+
if (action === 'get') {
|
|
409
|
+
// 用 process.env 直接读取,完全消除命令调用
|
|
410
|
+
if (name) {
|
|
411
|
+
return { status: 'success', data: { var: name, value: process.env[name] || '' } };
|
|
412
|
+
}
|
|
413
|
+
// 无 name 时返回所有环境变量摘要
|
|
414
|
+
const envVars = Object.entries(process.env).slice(0, 100);
|
|
415
|
+
const summary = envVars.map(([k, v]) => `${k}=${v?.substring(0, 200)}`).join('\\n');
|
|
416
|
+
return { status: 'success', data: { var: null, value: summary.substring(0, 2000) } };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (action === 'set' && name && value) {
|
|
420
|
+
// setx 用数组参数,无 shell 注入
|
|
421
|
+
spawnSync('setx', [name, value], { timeout: 10000, stdio: 'pipe', env: { ...process.env, NO_PROXY: '*' } });
|
|
422
|
+
return { status: 'success', data: { name, value, note: '环境变量已设置,新终端生效' } };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (action === 'proxy') {
|
|
426
|
+
// 用 process.env 直接读取代理配置,零命令调用
|
|
427
|
+
return {
|
|
428
|
+
status: 'success',
|
|
429
|
+
data: {
|
|
430
|
+
HTTP_PROXY: getEnv('HTTP_PROXY', ''),
|
|
431
|
+
HTTPS_PROXY: getEnv('HTTPS_PROXY', ''),
|
|
432
|
+
NO_PROXY: getEnv('NO_PROXY', '')
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
throw new Error('env action 需为 get/set/proxy');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ====== Handler: network (网络诊断) ======
|
|
441
|
+
|
|
442
|
+
export function handleSystemNetwork(params) {
|
|
443
|
+
const { target, action } = params;
|
|
444
|
+
|
|
445
|
+
if (action === 'ping') {
|
|
446
|
+
const host = target || '8.8.8.8';
|
|
447
|
+
// ping 用数组参数,无注入
|
|
448
|
+
const out = spawnRun('ping', ['-n', '3', host], 15000);
|
|
449
|
+
const avg = out.match(/平均\\s*=\\s*(\\d+)/)?.[1] || out.match(/Average\\s*=\\s*(\\d+)/)?.[1] || '超时';
|
|
450
|
+
return { status: 'success', data: { target: host, avgMs: avg, raw: out.substring(0, 500) } };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (action === 'dns') {
|
|
454
|
+
const host = target || 'github.com';
|
|
455
|
+
// nslookup 用数组参数
|
|
456
|
+
const out = spawnRun('nslookup', [host], 10000);
|
|
457
|
+
return { status: 'success', data: { target: host, result: out.substring(0, 500) } };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 默认:查网络整体状态
|
|
461
|
+
const out = spawnRun('ping', ['-n', '1', '8.8.8.8'], 10000);
|
|
462
|
+
const online = !out.includes('请求超时') && !out.includes('Destination host unreachable');
|
|
463
|
+
return { status: 'success', data: { online, gateway: PROXY_HOST, proxyPort: PROXY_PORT } };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ====== 围魏救赵 - 自动检上游依赖 ======
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* ⚡ 围魏救赵 — 自动诊断上游依赖
|
|
470
|
+
* timeout/API失败 → 自动查代理是否在线
|
|
471
|
+
* disk/ENOSPC → 自动查磁盘空间
|
|
472
|
+
* 返回结构化诊断报告
|
|
473
|
+
*/
|
|
474
|
+
export async function handleSystemDiagnose(params) {
|
|
475
|
+
const { type } = params;
|
|
476
|
+
const result = { timestamp: new Date().toISOString(), diagnostics: {} };
|
|
477
|
+
|
|
478
|
+
// 代理检查 — net 模块原生探测,零命令调用
|
|
479
|
+
result.diagnostics.proxy = await (async () => {
|
|
480
|
+
try {
|
|
481
|
+
const net = require('net');
|
|
482
|
+
const ok = await new Promise(resolve => {
|
|
483
|
+
const sock = new net.Socket();
|
|
484
|
+
sock.setTimeout(2000);
|
|
485
|
+
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
486
|
+
sock.on('error', () => { sock.destroy(); resolve(false); });
|
|
487
|
+
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
488
|
+
sock.connect(PROXY_PORT, PROXY_HOST);
|
|
489
|
+
});
|
|
490
|
+
return { status: ok ? 'online' : 'offline', host: PROXY_HOST, port: PROXY_PORT };
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return { status: 'check_failed', error: e.message };
|
|
493
|
+
}
|
|
494
|
+
})();
|
|
495
|
+
|
|
496
|
+
// 磁盘空间检查 — spawnSync 数组参数,硬编码 PS 脚本(无用户输入)
|
|
497
|
+
result.diagnostics.disk = await (async () => {
|
|
498
|
+
try {
|
|
499
|
+
const psCmd = [
|
|
500
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
501
|
+
"Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -gt 0 } | Select-Object Name, @{N='TotalGB';E={[math]::Round($_.Used/1GB,1)}}, @{N='FreeGB';E={[math]::Round($_.Free/1GB,1)}}, @{N='FreePct';E={[math]::Round($_.Free/$_.Total*100,1)}} | ConvertTo-Json -Compress"
|
|
502
|
+
];
|
|
503
|
+
const psResult = spawnSync('powershell', psCmd, { timeout: 15000, stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, NO_PROXY: '*' } });
|
|
504
|
+
const out = psResult.stdout || '';
|
|
505
|
+
const drives = JSON.parse(out.trim());
|
|
506
|
+
const arr = Array.isArray(drives) ? drives : [drives];
|
|
507
|
+
result.diagnostics.disk = { drives: arr };
|
|
508
|
+
const critical = arr.find(d => d.FreeGB < 1);
|
|
509
|
+
const warning = arr.find(d => d.FreeGB < 5);
|
|
510
|
+
if (critical) {
|
|
511
|
+
result.diagnostics.disk.critical = `${critical.Name}盘仅剩${critical.FreeGB}GB`;
|
|
512
|
+
result.diagnostics.disk.action = '立即清理磁盘或扩容';
|
|
513
|
+
} else if (warning) {
|
|
514
|
+
result.diagnostics.disk.warning = `${warning.Name}盘不足5GB`;
|
|
515
|
+
result.diagnostics.disk.action = '考虑清理磁盘';
|
|
516
|
+
} else {
|
|
517
|
+
result.diagnostics.disk.status = 'ok';
|
|
518
|
+
}
|
|
519
|
+
return result.diagnostics.disk;
|
|
520
|
+
} catch (e) {
|
|
521
|
+
return { status: 'check_failed', error: e.message };
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
524
|
+
|
|
525
|
+
// 网络可达性 — ping 用数组参数
|
|
526
|
+
if (!type || type === 'full' || type === 'network') {
|
|
527
|
+
result.diagnostics.network = await (async () => {
|
|
528
|
+
const out = spawnRun('ping', ['-n', '2', '8.8.8.8'], 10000);
|
|
529
|
+
const avgMatch = out.match(/平均\\s*=\\s*(\\d+)/) || out.match(/Average\\s*=\\s*(\\d+)/);
|
|
530
|
+
const lossMatch = out.match(/(\\d+)%\\s*(丢失|loss)/i);
|
|
531
|
+
return {
|
|
532
|
+
reachable: !out.includes('请求超时') && !out.includes('Destination host unreachable'),
|
|
533
|
+
avgMs: avgMatch ? parseInt(avgMatch[1]) : null,
|
|
534
|
+
packetLoss: lossMatch ? parseInt(lossMatch[1]) : null,
|
|
535
|
+
};
|
|
536
|
+
})();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 内存 — os 模块原生读取
|
|
540
|
+
if (!type || type === 'full' || type === 'memory') {
|
|
541
|
+
result.diagnostics.memory = {
|
|
542
|
+
freeGB: parseFloat((freemem() / 1024 / 1024 / 1024).toFixed(2)),
|
|
543
|
+
totalGB: parseFloat((totalmem() / 1024 / 1024 / 1024).toFixed(2)),
|
|
544
|
+
usagePercent: Math.round((1 - freemem() / totalmem()) * 100),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 总体判断
|
|
549
|
+
const issues = [];
|
|
550
|
+
if (result.diagnostics.proxy?.status === 'offline') issues.push('代理离线(127.0.0.1:7892)');
|
|
551
|
+
if (result.diagnostics.memory?.freeGB < 2) issues.push('内存不足');
|
|
552
|
+
if (result.diagnostics.disk?.critical) issues.push(result.diagnostics.disk.critical);
|
|
553
|
+
result.summary = issues.length > 0
|
|
554
|
+
? `检测到${issues.length}个问题: ${issues.join('; ')}`
|
|
555
|
+
: '上游依赖全部正常';
|
|
556
|
+
result.hasIssues = issues.length > 0;
|
|
557
|
+
|
|
558
|
+
return { status: 'success', data: result };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ====== 代理检测 ======
|
|
562
|
+
|
|
563
|
+
function checkProxy() {
|
|
564
|
+
try {
|
|
565
|
+
const net = require('net');
|
|
566
|
+
return new Promise(resolve => {
|
|
567
|
+
const sock = new net.Socket();
|
|
568
|
+
sock.setTimeout(2000);
|
|
569
|
+
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
570
|
+
sock.on('error', () => { sock.destroy(); resolve(false); });
|
|
571
|
+
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
572
|
+
sock.connect(PROXY_PORT, PROXY_HOST);
|
|
573
|
+
});
|
|
574
|
+
} catch { return false; }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ====== 主入口 ======
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* cpu_systemRun 统一调度
|
|
581
|
+
* 根据 action 路由到对应 handler
|
|
582
|
+
*/
|
|
583
|
+
export async function handleSystemRun(params) {
|
|
584
|
+
if (!params) throw new Error('[system-tools] missing params');
|
|
585
|
+
const { action } = params;
|
|
586
|
+
if (!action) throw new Error('missing action 参数');
|
|
587
|
+
|
|
588
|
+
switch (action) {
|
|
589
|
+
case 'download': return await handleSystemDownload(params);
|
|
590
|
+
case 'install': return handleSystemInstall(params);
|
|
591
|
+
case 'check': return handleSystemCheck(params);
|
|
592
|
+
case 'port': return handleSystemPort(params);
|
|
593
|
+
case 'disk': return handleSystemDisk(params);
|
|
594
|
+
case 'service': return handleSystemService(params);
|
|
595
|
+
case 'ollama': return handleSystemOllama(params);
|
|
596
|
+
case 'git': return handleSystemGit(params);
|
|
597
|
+
case 'unzip': return handleSystemUnzip(params);
|
|
598
|
+
case 'env': return handleSystemEnv(params);
|
|
599
|
+
case 'network': return handleSystemNetwork(params);
|
|
600
|
+
case 'diagnose': return await handleSystemDiagnose(params);
|
|
601
|
+
default: throw new Error(`未知操作: ${action},支持: download/install/check/port/disk/service/ollama/git/unzip/env/network/diagnose`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export default {
|
|
606
|
+
handleSystemRun,
|
|
607
|
+
handleSystemDownload,
|
|
608
|
+
handleSystemInstall,
|
|
609
|
+
handleSystemCheck,
|
|
610
|
+
handleSystemPort,
|
|
611
|
+
handleSystemDisk,
|
|
612
|
+
handleSystemService,
|
|
613
|
+
handleSystemOllama,
|
|
614
|
+
handleSystemGit,
|
|
615
|
+
handleSystemUnzip,
|
|
616
|
+
handleSystemEnv,
|
|
617
|
+
handleSystemNetwork,
|
|
618
|
+
handleSystemDiagnose,
|
|
619
|
+
};
|