fix: restore WebShell batch probe completion

This commit is contained in:
Ed1s0nZ committed 2026-10-31 02:26:46 +08:00
1 parent bc0b288d40
commit 6ad8d47c74
3 files changed
+80 -16

No files matched your search

+2
View File
@@ -10,6 +10,8 @@
### Fixed
- 修复 WebShell 批量探活因请求构建异常一直停留在“探活中”的问题;使用连接自身 ID 读取已保存凭据,并在探活失败后恢复按钮、继续处理其他连接。
- 修复审计 Agent 向不支持 thinking 字段的兼容网关发起请求失败的问题;支持独立兼容 profile,并按模型和地址识别默认行为。
- 修复已知支持 `none` 的官方 OpenAI 模型在关闭推理后仍使用服务端默认推理的问题(#366):按提供商、端点和模型发送并保留显式关闭参数,覆盖 Planner 和强制工具选择;保留 DeepSeek 与第三方兼容网关行为,并明确提示需要 Responses 的工具调用限制。
+60
View File
@@ -0,0 +1,60 @@
const fs = require('node:fs');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
const source = fs.readFileSync('web/static/js/webshell.js', 'utf8');
function harness(reply) {
const requests = [];
const button = {};
const ctx = {
webshellConnections: Array.from({length: 6}, (_, i) => ({id: `conn-${i}`, url: 'http://example.invalid', password: '********'})),
webshellBatchProbeRunning: false, webshellProbeStatusById: {},
wsT: key => key, renderWebshellList() {},
document: {getElementById: id => id === 'webshell-batch-probe-btn' ? button : {value: 'unrelated-edit-id'}},
webshellConnEncoding: () => 'utf-8', webshellConnOS: () => 'linux',
apiFetch: (url, options) => {
const body = JSON.parse(options.body);
requests.push(body);
return reply(body);
}
};
vm.createContext(ctx);
for (const name of ['buildWebshellProbeToken', 'buildWebshellProbeCommand', 'isWebshellProbeOutputMatched', 'probeWebshellConnection', 'updateWebshellBatchProbeButton', 'runBatchProbeWebshellConnections']) {
const start = source.indexOf(`function ${name}(`);
const end = source.indexOf('\nfunction ', start + 1);
assert.ok(start >= 0 && end > start);
vm.runInContext(source.slice(start, end), ctx);
}
return {ctx, requests, button};
}
function success(body) {
return Promise.resolve({json: async () => ({ok: true, output: body.command.replace(/^echo /, '')})});
}
test('saved masked credentials use each connection ID and complete a batch, then allow retry', async () => {
const {ctx, requests, button} = harness(success);
await ctx.runBatchProbeWebshellConnections();
assert.deepEqual(requests.map(r => r.connection_id).sort(), Array.from({length: 6}, (_, i) => `conn-${i}`));
assert.ok(Object.values(ctx.webshellProbeStatusById).every(s => s.state === 'ok'));
assert.equal(ctx.webshellBatchProbeRunning, false);
assert.equal(button.disabled, false);
await ctx.runBatchProbeWebshellConnections();
assert.equal(requests.length, 12);
});
for (const failure of ['sync', 'network', 'json', 'server', 'mismatch']) {
test(`${failure} failure settles offline, completes other connections and restores button`, async () => {
const {ctx, button, requests} = harness(body => {
if (body.connection_id !== 'conn-0') return success(body);
if (failure === 'sync') throw new Error('sync failure');
if (failure === 'network') return Promise.reject(new Error('network failure'));
if (failure === 'json') return Promise.resolve({json: () => Promise.reject(new Error('invalid JSON'))});
return Promise.resolve({json: async () => ({ok: failure === 'mismatch', output: '', error: 'probe failed'})});
});
await ctx.runBatchProbeWebshellConnections();
assert.equal(requests.length, 6);
assert.equal(ctx.webshellProbeStatusById['conn-0'].state, 'fail');
assert.ok(ctx.webshellProbeStatusById['conn-0'].message);
assert.equal(Object.values(ctx.webshellProbeStatusById).filter(s => s.state === 'ok').length, 5);
assert.equal(ctx.webshellBatchProbeRunning, false);
assert.equal(button.disabled, false);
});
}
+18 -16
View File
@@ -1232,21 +1232,23 @@ function probeWebshellConnection(conn) {
return Promise.resolve({ ok: false, message: wsT('webshell.testFailed') || '连通性测试失败' });
}
var probeToken = buildWebshellProbeToken();
return apiFetch('/api/webshell/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: conn.url,
password: conn.password || '',
type: conn.type || 'php',
method: ((conn.method || 'post').toLowerCase() === 'get') ? 'get' : 'post',
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: buildWebshellProbeCommand(probeToken),
connection_id: password === '********' ? (document.getElementById('webshell-edit-id')?.value || '') : ''
})
// Include request construction and synchronous transport errors in the failure path.
return Promise.resolve().then(function () {
return apiFetch('/api/webshell/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: conn.url,
password: conn.password || '',
type: conn.type || 'php',
method: ((conn.method || 'post').toLowerCase() === 'get') ? 'get' : 'post',
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: buildWebshellProbeCommand(probeToken)
})
});
})
.then(function (r) { return r.json(); })
.then(function (data) {
@@ -1322,7 +1324,7 @@ function runBatchProbeWebshellConnections() {
var workers = [];
for (var i = 0; i < concurrency; i++) workers.push(runOne());
Promise.all(workers).finally(function () {
return Promise.all(workers).finally(function () {
webshellBatchProbeRunning = false;
updateWebshellBatchProbeButton(done, total, okCount);
});