Add GNOME Shell extension showing sing-box status via the Clash API

This commit is contained in:
av
2026-07-18 15:25:26 +03:00
commit 629e031845
10 changed files with 893 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
// Minimal async client for the sing-box Clash API, built on libsoup 3.
//
// Endpoints used:
// GET /version liveness + version string
// GET /connections snapshot: totals + active connections
// GET /proxies all proxies and selector groups
// PUT /proxies/{group} {name} switch a Selector group's active node
// GET /proxies/{name}/delay?url=... measure latency of a node
import Soup from 'gi://Soup';
import GLib from 'gi://GLib';
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
export class ClashApi {
constructor({ url = 'http://127.0.0.1:9090', secret = '' } = {}) {
this._session = new Soup.Session();
this._session.timeout = 5;
this.setEndpoint(url, secret);
}
setEndpoint(url, secret) {
this._baseUrl = String(url || '').replace(/\/+$/, '');
this._secret = secret || '';
}
_message(method, path, body) {
const msg = Soup.Message.new(method, `${this._baseUrl}${path}`);
const headers = msg.get_request_headers();
headers.append('Accept', 'application/json');
if (this._secret)
headers.append('Authorization', `Bearer ${this._secret}`);
if (body !== undefined) {
const bytes = new GLib.Bytes(textEncoder.encode(JSON.stringify(body)));
msg.set_request_body_from_bytes('application/json', bytes);
}
return msg;
}
_request(method, path, body, cancellable) {
return new Promise((resolve, reject) => {
const msg = this._message(method, path, body);
this._session.send_and_read_async(
msg,
GLib.PRIORITY_DEFAULT,
cancellable ?? null,
(session, res) => {
let bytes;
try {
bytes = session.send_and_read_finish(res);
} catch (e) {
reject(e);
return;
}
const status = msg.get_status();
if (status < 200 || status >= 300) {
reject(new Error(`HTTP ${status} ${msg.get_reason_phrase() ?? ''} (${path})`.trim()));
return;
}
try {
const data = bytes?.get_data();
const text = data && data.length ? textDecoder.decode(data) : '';
resolve(text ? JSON.parse(text) : null);
} catch (e) {
reject(e);
}
}
);
});
}
getVersion(cancellable) {
return this._request('GET', '/version', undefined, cancellable);
}
getConnections(cancellable) {
return this._request('GET', '/connections', undefined, cancellable);
}
getProxies(cancellable) {
return this._request('GET', '/proxies', undefined, cancellable);
}
selectProxy(group, name, cancellable) {
return this._request('PUT', `/proxies/${encodeURIComponent(group)}`, { name }, cancellable);
}
getDelay(name, { url = 'https://www.gstatic.com/generate_204', timeout = 3000 } = {}, cancellable) {
const query = `url=${encodeURIComponent(url)}&timeout=${timeout}`;
return this._request('GET', `/proxies/${encodeURIComponent(name)}/delay?${query}`, undefined, cancellable);
}
}