Skip to content

Commit f06be42

Browse files
committed
feat: move custom nodes into settings
1 parent 58b54cd commit f06be42

6 files changed

Lines changed: 78 additions & 30 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ npm run build:windows
5353

5454
Windows 需要先安装 Git for Windows。安装包默认按当前用户安装;WebView2 缺失时由安装器静默引导安装。产物位于 `src-tauri/target/x86_64-pc-windows-msvc/release/bundle/`。
5555

56-
系统节点目录源文件为仓库根目录的 `nodes.json`,发布地址为 `https://cdn.jsdelivr.net/gh/DiscoverBox/gitboost@main/nodes.json`。文件只包含代理 URL 字符串,不包含 ID、名称、类型或转换规则。
56+
系统节点目录源文件为仓库根目录的 `nodes.json`,主发布地址为 `https://cdn.jsdelivr.net/gh/DiscoverBox/gitboost@main/nodes.json`,主地址不可用时回退到 `https://cdn.jsdmirror.com/gh/DiscoverBox/gitboost@main/nodes.json`。文件只包含代理 URL 字符串,不包含 ID、名称、类型或转换规则。
5757

5858
数据保存在系统的应用数据目录 `pro.gitboost.desktop` 下。`system-nodes.json` 保存最近一次有效的系统节点目录,`nodes.json` 只保存用户自定义节点。恢复操作只删除 GitBoost 自己注册的 `include.path` 并清空自己的重写规则,不修改任何仓库的 remote。
5959

‎src-tauri/src/core.rs‎

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ use std::{
2323
use tempfile::NamedTempFile;
2424
use uuid::Uuid;
2525

26-
const SYSTEM_NODE_CATALOG_URL: &str =
27-
"https://cdn.jsdelivr.net/gh/DiscoverBox/gitboost@main/nodes.json";
26+
const SYSTEM_NODE_CATALOG_URLS: [&str; 2] = [
27+
"https://cdn.jsdelivr.net/gh/DiscoverBox/gitboost@main/nodes.json",
28+
"https://cdn.jsdmirror.com/gh/DiscoverBox/gitboost@main/nodes.json",
29+
];
2830
const MAX_SYSTEM_NODES: usize = 100;
2931
const CATALOG_MAX_BYTES: &str = "262144";
3032
const CATALOG_TIMEOUT_SECONDS: &str = "10";
@@ -1154,6 +1156,24 @@ fn parse_system_catalog(bytes: &[u8]) -> Result<Vec<String>, String> {
11541156
}
11551157

11561158
fn fetch_system_catalog() -> Result<Vec<u8>, String> {
1159+
fetch_system_catalog_from(&SYSTEM_NODE_CATALOG_URLS, fetch_system_catalog_url)
1160+
}
1161+
1162+
fn fetch_system_catalog_from<F>(urls: &[&str], mut fetch: F) -> Result<Vec<u8>, String>
1163+
where
1164+
F: FnMut(&str) -> Result<Vec<u8>, String>,
1165+
{
1166+
let mut errors = Vec::with_capacity(urls.len());
1167+
for url in urls {
1168+
match fetch(url) {
1169+
Ok(output) => return Ok(output),
1170+
Err(error) => errors.push(error),
1171+
}
1172+
}
1173+
Err(format!("无法更新系统节点:{}", errors.join(";")))
1174+
}
1175+
1176+
fn fetch_system_catalog_url(url: &str) -> Result<Vec<u8>, String> {
11571177
let mut command = Command::new("curl");
11581178
command.args([
11591179
"--location",
@@ -1170,16 +1190,16 @@ fn fetch_system_catalog() -> Result<Vec<u8>, String> {
11701190
CATALOG_TIMEOUT_SECONDS,
11711191
"--max-filesize",
11721192
CATALOG_MAX_BYTES,
1173-
SYSTEM_NODE_CATALOG_URL,
1193+
url,
11741194
]);
11751195
hide_catalog_console(&mut command);
11761196
let output = command
11771197
.output()
1178-
.map_err(|error| format!("无法更新系统节点:{error}"))?;
1198+
.map_err(|error| format!("{url}: {error}"))?;
11791199
if !output.status.success() {
11801200
let detail = String::from_utf8_lossy(&output.stderr);
11811201
return Err(format!(
1182-
"无法更新系统节点:{}",
1202+
"{url}: {}",
11831203
detail.lines().next().unwrap_or("请求失败")
11841204
));
11851205
}
@@ -1232,6 +1252,23 @@ mod tests {
12321252
);
12331253
}
12341254

1255+
#[test]
1256+
fn system_catalog_uses_the_mirror_when_the_primary_fails() {
1257+
let mut requested = Vec::new();
1258+
let output = fetch_system_catalog_from(&SYSTEM_NODE_CATALOG_URLS, |url| {
1259+
requested.push(url.to_string());
1260+
if url.contains("cdn.jsdelivr.net") {
1261+
Err("primary unavailable".into())
1262+
} else {
1263+
Ok(br#"["https://fastgit.cc"]"#.to_vec())
1264+
}
1265+
})
1266+
.unwrap();
1267+
1268+
assert_eq!(output, br#"["https://fastgit.cc"]"#);
1269+
assert_eq!(requested, SYSTEM_NODE_CATALOG_URLS);
1270+
}
1271+
12351272
#[test]
12361273
fn removing_current_system_node_immediately_writes_direct_git_config() {
12371274
let directory = tempfile::tempdir().unwrap();

‎src/App.tsx‎

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { currentNode, formatLatency, formatRelativeTime, lineStatusLabel, status
88

99
const navItems: { key: PageKey; label: string }[] = [
1010
{ key: "overview", label: "总览" },
11-
{ key: "nodes", label: "自定义节点" },
1211
{ key: "routes", label: "路由清单" },
1312
{ key: "downloads", label: "文件下载" },
1413
{ key: "usage", label: "使用日志" },
@@ -142,12 +141,11 @@ export default function App() {
142141
</aside>
143142
<main className="main-content">
144143
{page === "overview" && <Overview snapshot={snapshot} busy={busy} run={run} go={setPage} />}
145-
{page === "nodes" && <Nodes snapshot={snapshot} busy={busy} run={run} onImport={() => setImportOpen(true)} />}
146144
{page === "routes" && <Routes snapshot={snapshot} busy={busy} run={run} />}
147145
{page === "downloads" && <Downloads busy={busy} run={run} />}
148146
{page === "usage" && <UsageLogs snapshot={snapshot} busy={busy} run={run} />}
149147
{page === "diagnostics" && <Diagnostics snapshot={snapshot} busy={busy} />}
150-
{page === "settings" && <Settings snapshot={snapshot} busy={busy} run={run} />}
148+
{page === "settings" && <Settings snapshot={snapshot} busy={busy} run={run} onImport={() => setImportOpen(true)} />}
151149
</main>
152150
{toast && <div className={`toast toast--${toast.kind}`}>{toast.text}</div>}
153151
{importOpen && <ImportNodes busy={busy} run={run} onClose={() => setImportOpen(false)} />}
@@ -200,30 +198,34 @@ function Overview({ snapshot, busy, run, go }: { snapshot: AppSnapshot; busy: st
200198
);
201199
}
202200

203-
function Nodes({ snapshot, busy, run, onImport }: { snapshot: AppSnapshot; busy: string | null; run: Runner; onImport: () => void }) {
201+
function CustomNodeSettings({ snapshot, busy, run, onImport }: { snapshot: AppSnapshot; busy: string | null; run: Runner; onImport: () => void }) {
204202
const [editing, setEditing] = useState<NodeEntry | null>(null);
205203
const customNodes = snapshot.nodes.filter((node) => !node.builtIn);
206204
const systemCount = snapshot.nodes.length - customNodes.length;
207205
return (
208-
<div className="page">
209-
<PageHeader eyebrow="可选线路" title="自定义节点" description="系统线路由 GitBoost 自动维护;这里只管理你手工添加的代理地址。" actions={<><Button onClick={onImport}>添加节点</Button><Button tone="primary" busy={busy === "test-all"} onClick={() => run("test-all", api.testAllNodes, "全部线路检测完成").catch(() => undefined)}>检测全部线路</Button></>} />
210-
<div className="inline-explainer"><strong>系统线路</strong><span>{systemCount} 个,由远程目录和本地缓存自动维护。</span><span>所有线路都会经过本机 git ls-remote 检测后再参与选择。</span></div>
211-
<section className="table-shell">
212-
<div className="table-head node-grid"><span>节点</span><span>状态</span><span>成功率</span><span>中位耗时</span><span>最近检测</span><span /></div>
206+
<section className="section-block node-settings" aria-labelledby="custom-nodes-title">
207+
<div className="section-title">
208+
<div><h2 id="custom-nodes-title">自定义节点</h2><p>添加并管理自己的代理地址;系统节点仍由 GitBoost 自动维护。</p></div>
209+
<div className="section-actions"><Button onClick={onImport}>添加节点</Button><Button busy={busy === "test-all"} onClick={() => run("test-all", api.testAllNodes, "全部线路检测完成").catch(() => undefined)}>检测全部</Button></div>
210+
</div>
211+
<div className="node-settings-list">
213212
{customNodes.map((node) => (
214-
<div className={`table-row node-grid ${!node.enabled ? "is-disabled" : ""}`} key={node.id}>
213+
<div className={`node-setting-row ${!node.enabled ? "is-disabled" : ""}`} key={node.id}>
215214
<div className="node-name"><StatusDot status={node.health.status} /><div><strong>{node.name}</strong><code>{node.rewriteBase}</code></div></div>
216-
<span className={`status-text status-text--${statusTone(node.health.status)}`}>{statusLabel[node.health.status]}</span>
217-
<span>{successRate(node.health)}</span><span>{formatLatency(node.health.medianLatencyMs)}</span><span>{formatRelativeTime(node.health.checkedAt)}</span>
215+
<div className="node-health">
216+
<strong className={`status-text status-text--${statusTone(node.health.status)}`}>{statusLabel[node.health.status]}</strong>
217+
<span>{successRate(node.health)} 成功 · {formatLatency(node.health.medianLatencyMs)}</span>
218+
<span>{formatRelativeTime(node.health.checkedAt)}</span>
219+
</div>
218220
<div className="row-actions"><Button tone="quiet" busy={busy === `test-${node.id}`} onClick={() => run(`test-${node.id}`, () => api.testNode(node.id), `${node.name} 检测完成`).catch(() => undefined)}>检测</Button><button className="more-button" onClick={() => setEditing(node)} aria-label={`管理 ${node.name}`}>•••</button></div>
219221
{node.health.failureReason && <p className="row-detail">{node.health.failureReason}</p>}
220222
</div>
221223
))}
222-
{!customNodes.length && <div className="empty-state"><strong>还没有自定义节点</strong><p>GitBoost 会自动使用系统线路;也可以添加自己的代理地址。</p></div>}
223-
</section>
224-
<div className="inline-explainer"><strong>检测仓库</strong><code>https://github.com/octocat/Hello-World.git</code><span>检测隔离运行,不修改你的全局 Git 配置。</span></div>
224+
{!customNodes.length && <div className="empty-state compact"><strong>还没有自定义节点</strong><p>通常无需添加,GitBoost 会自动使用系统线路。</p></div>}
225+
</div>
226+
<p className="node-settings-note">系统线路 {systemCount} 个 · 使用公开仓库进行隔离检测,不修改全局 Git 配置</p>
225227
{editing && <ManageNode node={editing} busy={busy} run={run} onClose={() => setEditing(null)} />}
226-
</div>
228+
</section>
227229
);
228230
}
229231

@@ -401,7 +403,7 @@ function UsageLogs({ snapshot, busy, run }: { snapshot: AppSnapshot; busy: strin
401403
);
402404
}
403405

404-
function Settings({ snapshot, busy, run }: { snapshot: AppSnapshot; busy: string | null; run: Runner }) {
406+
function Settings({ snapshot, busy, run, onImport }: { snapshot: AppSnapshot; busy: string | null; run: Runner; onImport: () => void }) {
405407
const [minutes, setMinutes] = useState(snapshot.settings.healthCheckMinutes);
406408
const [logLevel, setLogLevel] = useState(snapshot.settings.logLevel);
407409
const [launchAtLogin, setLaunchAtLogin] = useState(snapshot.settings.launchAtLogin);
@@ -420,6 +422,7 @@ function Settings({ snapshot, busy, run }: { snapshot: AppSnapshot; busy: string
420422
<div className="setting-row"><div><strong>登录时启动</strong><p>保持托盘运行,以便节点失效后重新选路。</p></div><Switch label="登录时启动" checked={launchAtLogin} onChange={(enabled) => setAutostart(enabled).catch(() => undefined)} /></div>
421423
<div className="setting-row"><div><strong>日志级别</strong><p>日志会移除凭据、查询参数和命令环境。</p></div><select value={logLevel} onChange={(event) => setLogLevel(event.target.value as "error" | "info" | "debug")}><option value="error">仅错误</option><option value="info">信息</option><option value="debug">调试</option></select></div>
422424
</section>
425+
<CustomNodeSettings snapshot={snapshot} busy={busy} run={run} onImport={onImport} />
423426
<section className="section-block maintenance"><div className="section-title"><div><h2>数据与恢复</h2><p>恢复只清空 GitBoost 自己的重写规则,不修改仓库 remote。</p></div></div><div className="maintenance-actions"><Button busy={busy === "export"} onClick={() => saveNodes().catch(() => undefined)}>导出自定义节点</Button><Button busy={busy === "clear-logs"} onClick={() => run("clear-logs", api.clearLogs, "本地日志已清理").catch(() => undefined)}>清理日志</Button><Button tone="danger" busy={busy === "restore"} onClick={() => run("restore", api.restoreGitConfig, "GitBoost 配置已恢复为直连").catch(() => undefined)}>恢复 Git 配置</Button></div></section>
424427
<div className="about-line"><span>GitBoost 0.1.0 · macOS / Windows</span><span>本地运行 · 无账号 · 无遥测</span></div>
425428
</div>

‎src/styles.css‎

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type PageKey = "overview" | "nodes" | "routes" | "downloads" | "usage" | "diagnostics" | "settings";
1+
export type PageKey = "overview" | "routes" | "downloads" | "usage" | "diagnostics" | "settings";
22
export type RouteScope = "allowlist" | "global";
33
export type LineMode = "automatic" | "fixed" | "direct";
44
export type NodeStatus = "untested" | "available" | "slow" | "incompatible" | "unavailable";

‎tests/app.spec.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ test("core desktop workflow is navigable", async ({ page }) => {
1010
await expect(dragRegion).toBeVisible();
1111
await expect(dragRegion).toHaveCSS("height", "29px");
1212

13-
await page.getByRole("button", { name: "自定义节点", exact: true }).click();
13+
await expect(page.getByRole("navigation", { name: "主要导航" }).getByText("自定义节点", { exact: true })).toHaveCount(0);
14+
await page.getByRole("button", { name: "设置", exact: true }).click();
1415
await expect(page.getByRole("heading", { name: "自定义节点", exact: true })).toBeVisible();
15-
await expect(page.getByText("1 个,由远程目录和本地缓存自动维护。", { exact: true })).toBeVisible();
16+
await expect(page.getByText("系统线路 1 个 · 使用公开仓库进行隔离检测,不修改全局 Git 配置", { exact: true })).toBeVisible();
1617
await expect(page.getByText("https://fastgit.cc/https://github.com/", { exact: true })).toBeHidden();
1718

1819
await page.getByRole("button", { name: "添加节点" }).click();
@@ -39,7 +40,7 @@ test("core desktop workflow is navigable", async ({ page }) => {
3940

4041
test("node import keeps failures open and reports actual results", async ({ page }) => {
4142
await page.goto("/");
42-
await page.getByRole("button", { name: "自定义节点", exact: true }).click();
43+
await page.getByRole("button", { name: "设置", exact: true }).click();
4344
await page.getByRole("button", { name: "添加节点" }).click();
4445

4546
await page.evaluate(() => {

0 commit comments

Comments
 (0)