feat(agentRegistry): support agent download directory with multi-source

* feat(docs): 更新数据库支持文档,增加新数据库和下载链接说明

- 在数据库支持列表中添加 ChromaDB、Cloudflare D1、Apache Spark、Dremio 和 OSCAR
- 更新文档中关于下载来源的描述,增加对新数据库的支持说明
- 增加下载链接功能,支持 GitHub、CNB 和 AtomGit 下载

* feat(agentRegistry): 增加代理下载目录功能,支持从多个源获取下载资产

- 添加新的接口和类型以支持代理下载
- 实现从 GitHub、CNB 和 AtomGit 获取下载资产的功能
- 更新下载链接格式化逻辑
This commit is contained in:
二丫讲梵 2026-07-16 16:48:10 +08:00 committed by GitHub
parent b0a20e8364
commit df1d31e74f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 294 additions and 70 deletions

View File

@ -20,6 +20,8 @@ const databaseSupport = [
{ name: "Qdrant", icon: "/icons/database/qdrant.svg", tone: "#dc244c" },
{ name: "Milvus", icon: "/icons/database/milvus.png", tone: "#00a1ea" },
{ name: "Weaviate", icon: "/icons/database/weaviate.png", tone: "#00b894" },
{ name: "ChromaDB", shortLabel: "CH", tone: "#ff7a59" },
{ name: "Cloudflare D1", shortLabel: "D1", tone: "#f6821f" },
{ name: "MariaDB", icon: "/icons/database/mariadb.svg", tone: "#003545" },
{ name: "Doris", icon: "/icons/database/doris.svg", tone: "#5b7cfa" },
{ name: "StarRocks", icon: "/icons/database/starrocks.svg", tone: "#6750ff" },
@ -47,6 +49,7 @@ const databaseSupport = [
{ name: "Trino", icon: "/icons/database/trino.svg", tone: "#dd00a1" },
{ name: "PrestoSQL", icon: "/icons/database/presto.svg", tone: "#5890ff" },
{ name: "Hive", icon: "/icons/database/hive.svg", tone: "#fdcb00" },
{ name: "Apache Spark", shortLabel: "SP", tone: "#e25a1c" },
{ name: "DB2", icon: "/icons/database/db2.svg", tone: "#054ada" },
{ name: "SAP HANA", icon: "/icons/database/saphana.webp", tone: "#008fd3" },
{ name: "Teradata", icon: "/icons/database/teradata.webp", tone: "#f37440" },
@ -57,6 +60,8 @@ const databaseSupport = [
{ name: "Neo4j", icon: "/icons/database/neo4j.svg", tone: "#018bff" },
{ name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" },
{ name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" },
{ name: "Dremio", shortLabel: "DR", tone: "#30bdbe" },
{ name: "OSCAR", shortLabel: "OS", tone: "#1b8dff" },
{ name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" },
{ name: "QuestDB", icon: "/icons/database/questdb.svg", tone: "#dc2626" },
{ name: "IoTDB", icon: "/icons/database/iotdb.svg", tone: "#3cb371" },
@ -72,6 +77,7 @@ const databaseSupport = [
{ name: "Etcd", icon: "/icons/database/etcd.svg", tone: "#419eda" },
{ name: "ZooKeeper", icon: "/icons/database/zookeeper.svg", tone: "#3b82f6" },
{ name: "Pulsar", icon: "/icons/database/pulsar.svg", tone: "#188fff" },
{ name: "Apache Kafka", shortLabel: "KF", tone: "#231f20" },
{ name: "Nacos", icon: "/icons/database/nacos.png", tone: "#2f80ed" },
{ name: "IRIS", icon: "/icons/database/iris.png", tone: "#0085ca" },
{ name: "JDBC", icon: "/icons/database/jdbc.svg", tone: "#6ea8ff" },
@ -150,7 +156,7 @@ export default async function DatabasesPage({ params }: { params: Promise<{ lang
) : db.icon ? (
<img src={db.icon} alt="" width={38} height={38} className="block w-[38px] h-[38px] object-contain" />
) : (
<span className="grid place-items-center min-w-[46px] h-8 rounded-lg px-2 text-white text-xs font-[780]">{db.name.slice(0, 2).toUpperCase()}</span>
<span className="grid place-items-center min-w-[46px] h-8 rounded-lg px-2 text-xs font-[780] text-white" style={{ backgroundColor: db.tone }}>{db.shortLabel ?? db.name.slice(0, 2).toUpperCase()}</span>
)}
</div>
<strong className={`text-sm font-[650] leading-[1.2] text-center ${isCta ? "text-landing-blue" : "text-[color-mix(in_srgb,var(--color-landing-ink)_92%,var(--color-landing-muted))]"}`}>{db.name}</strong>

View File

@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useParams } from "next/navigation";
import { LandingNav } from "@/components/landing/LandingNav";
import { fetchAgentDownloadCatalog, formatSize, type AgentDownloadCatalog, type JreDisplayEntry, type NativeAgentDisplayEntry, type OfflineBundleEntry } from "@/lib/agentRegistry";
import { downloadLinksFor, fetchAgentDownloadCatalog, formatSize, type AgentDownloadCatalog, type DownloadSource, type JreDisplayEntry, type NativeAgentDisplayEntry, type OfflineBundleEntry } from "@/lib/agentRegistry";
import { AlertTriangle, Archive, Cpu, Database, Download, Loader2, Plug, Search, Terminal, X } from "lucide-react";
const i18n = {
@ -37,6 +37,14 @@ const i18n = {
showing: "Showing",
of: "of",
clearSearch: "Clear search",
mirrorHint: "CNB and AtomGit mirrors are available for GitHub release assets on mainland China networks.",
downloadSources: "Download source",
sources: {
github: "GitHub",
cnb: "CNB",
atomgit: "AtomGit",
official: "Official",
},
downloadHint: "For air-gapped environments: download the bundle for your platform on an internet-connected machine, then transfer it to the offline machine and import it in DBX from Settings > Driver Manager. Use the driver and JRE tabs only when you need individual artifacts.",
},
cn: {
@ -69,6 +77,14 @@ const i18n = {
showing: "显示",
of: "/",
clearSearch: "清空搜索",
mirrorHint: "中国大陆网络可选择 CNB 或 AtomGit 镜像下载GitHub Release 资源保持同步。",
downloadSources: "下载来源",
sources: {
github: "GitHub",
cnb: "CNB",
atomgit: "AtomGit",
official: "官方下载",
},
downloadHint: "内网环境使用说明:在有网的电脑上下载对应平台的整包,然后传输到内网机器,在 DBX 的“设置 > 驱动管理”中导入。只有需要单个产物时再使用驱动和 JRE 标签页。",
},
};
@ -94,6 +110,27 @@ type NativeAgentGroup = {
options: NativeAgentDisplayEntry[];
};
type DriverTranslations = (typeof i18n)["en"];
function DownloadLinks({ url, t }: { url: string; t: DriverTranslations }) {
return (
<div className="flex shrink-0 flex-nowrap justify-end gap-1.5 max-[760px]:flex-wrap max-[760px]:justify-start">
{downloadLinksFor(url).map((link) => (
<a
key={link.source}
href={link.url}
download
className={`landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 text-xs font-medium transition-colors hover:border-landing-blue ${link.source === "cnb" || link.source === "atomgit" ? "border-landing-blue/45 bg-landing-blue/10 text-landing-sky" : "border-landing-line"}`}
aria-label={`${t.download}: ${t.sources[link.source as DownloadSource]}`}
>
<Download size={13} />
{t.sources[link.source as DownloadSource]}
</a>
))}
</div>
);
}
function matchesSearch(values: Array<string | number | undefined>, query: string): boolean {
if (!query) return true;
return values.filter(Boolean).join(" ").toLowerCase().includes(query);
@ -227,6 +264,10 @@ export function DriversClient() {
{catalog && !loading && (
<>
<div className="mb-3 flex items-center gap-2 rounded-[7px] border border-landing-blue/30 bg-landing-blue/10 px-3 py-2 text-xs leading-[1.55] text-landing-sky">
<Download size={14} className="shrink-0" />
<span>{t.mirrorHint}</span>
</div>
<div className="landing-glass-card mb-12 overflow-hidden rounded-[10px]">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-landing-line bg-landing-panel/70 p-3">
<div className="inline-flex shrink-0 rounded-[8px] border border-landing-line bg-black/10 p-1 max-[760px]:grid max-[760px]:w-full max-[760px]:grid-cols-2">
@ -284,13 +325,13 @@ export function DriversClient() {
{activeTab === "jdbcPlugin" && (
<>
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.jdbcPluginDesc}</p>
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
<table className="w-full table-fixed border-collapse text-sm max-[760px]:block">
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
<tr className="border-b border-landing-line">
<th className="px-5 py-2.5 text-left font-medium">{t.jdbcPluginFile}</th>
<th className="w-[22%] px-5 py-2.5 text-left font-medium">{t.jdbcPluginFile}</th>
<th className="px-5 py-2.5 text-left font-medium">{t.filename}</th>
<th className="px-5 py-2.5 text-left font-medium">{t.installMethod}</th>
<th className="w-24 px-5 py-2.5" />
<th className="w-[340px] px-5 py-2.5 text-right font-medium">{t.downloadSources}</th>
</tr>
</thead>
<tbody className="max-[760px]:block">
@ -303,13 +344,10 @@ export function DriversClient() {
</div>
<p className="mt-1 hidden text-xs leading-[1.55] text-landing-muted max-[760px]:block">{t.jdbcPluginInstallHint}</p>
</td>
<td className="px-5 py-3 font-mono text-[11px] text-landing-muted max-[760px]:hidden">{plugin.filename}</td>
<td className="px-5 py-3 font-mono text-[11px] text-landing-muted max-[760px]:hidden"><span className="block truncate" title={plugin.filename}>{plugin.filename}</span></td>
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{t.jdbcPluginInstallHint}</td>
<td className="px-5 py-3 text-right max-[760px]:px-0">
<a href={plugin.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
<Download size={13} />
{t.download}
</a>
<td className="px-5 py-3 text-right max-[760px]:col-span-2 max-[760px]:px-0 max-[760px]:pt-0">
<DownloadLinks url={plugin.url} t={t} />
</td>
</tr>
))}
@ -322,13 +360,13 @@ export function DriversClient() {
{activeTab === "bundles" && (
<>
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.bundlesDesc}</p>
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
<table className="w-full table-fixed border-collapse text-sm max-[760px]:block">
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
<tr className="border-b border-landing-line">
<th className="px-5 py-2.5 text-left font-medium">{t.platform}</th>
<th className="w-[22%] px-5 py-2.5 text-left font-medium">{t.platform}</th>
<th className="px-5 py-2.5 text-left font-medium">{t.filename}</th>
<th className="px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-24 px-5 py-2.5" />
<th className="w-[116px] px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-[340px] px-5 py-2.5 text-right font-medium">{t.downloadSources}</th>
</tr>
</thead>
<tbody className="max-[760px]:block">
@ -340,13 +378,10 @@ export function DriversClient() {
<span className="hidden shrink-0 rounded-[5px] border border-landing-blue/35 bg-landing-blue/10 px-1.5 py-0.5 font-mono text-[11px] text-landing-sky max-[760px]:inline">ZIP</span>
</div>
</td>
<td className="px-5 py-3 font-mono text-[11px] text-landing-muted max-[760px]:hidden">{bundle.filename}</td>
<td className="px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(bundle.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:px-0">
<a href={bundle.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
<Download size={13} />
{t.download}
</a>
<td className="px-5 py-3 font-mono text-[11px] text-landing-muted max-[760px]:hidden"><span className="block truncate" title={bundle.filename}>{bundle.filename}</span></td>
<td className="whitespace-nowrap px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(bundle.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:col-span-2 max-[760px]:px-0 max-[760px]:pt-0">
<DownloadLinks url={bundle.url} t={t} />
</td>
</tr>
))}
@ -359,15 +394,15 @@ export function DriversClient() {
{activeTab === "drivers" && (
<>
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.driversDesc}</p>
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
<table className="w-full table-fixed border-collapse text-sm max-[760px]:block">
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
<tr className="border-b border-landing-line">
<th className="px-5 py-2.5 text-left font-medium">Driver</th>
<th className="px-5 py-2.5 text-left font-medium">Key</th>
<th className="w-[18%] px-5 py-2.5 text-left font-medium">Driver</th>
<th className="w-[14%] px-5 py-2.5 text-left font-medium">Key</th>
<th className="px-5 py-2.5 text-left font-medium">{t.version}</th>
<th className="px-5 py-2.5 text-left font-medium">{t.requiresJre}</th>
<th className="px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-24 px-5 py-2.5" />
<th className="w-[116px] px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-[340px] px-5 py-2.5 text-right font-medium">{t.downloadSources}</th>
</tr>
</thead>
<tbody className="max-[760px]:block">
@ -384,12 +419,9 @@ export function DriversClient() {
</td>
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{d.version}</td>
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{d.jre}</td>
<td className="px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(d.jar.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:px-0">
<a href={d.jar.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
<Download size={13} />
{t.download}
</a>
<td className="whitespace-nowrap px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(d.jar.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:col-span-2 max-[760px]:px-0 max-[760px]:pt-0">
<DownloadLinks url={d.jar.url} t={t} />
</td>
</tr>
))}
@ -402,14 +434,14 @@ export function DriversClient() {
{activeTab === "native" && (
<>
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.nativeAgentsDesc}</p>
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
<table className="w-full table-fixed border-collapse text-sm max-[760px]:block">
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
<tr className="border-b border-landing-line">
<th className="px-5 py-2.5 text-left font-medium">Agent</th>
<th className="w-[22%] px-5 py-2.5 text-left font-medium">Agent</th>
<th className="px-5 py-2.5 text-left font-medium">{t.platform}</th>
<th className="px-5 py-2.5 text-left font-medium">{t.version}</th>
<th className="px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-24 px-5 py-2.5" />
<th className="w-[116px] px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-[340px] px-5 py-2.5 text-right font-medium">{t.downloadSources}</th>
</tr>
</thead>
<tbody className="max-[760px]:block">
@ -439,12 +471,9 @@ export function DriversClient() {
</select>
</td>
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{group.version}</td>
<td className="px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(selectedAgent.info.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:px-0">
<a href={selectedAgent.info.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
<Download size={13} />
{t.download}
</a>
<td className="whitespace-nowrap px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(selectedAgent.info.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:col-span-2 max-[760px]:px-0 max-[760px]:pt-0">
<DownloadLinks url={selectedAgent.info.url} t={t} />
</td>
</tr>
);
@ -458,14 +487,14 @@ export function DriversClient() {
{activeTab === "jre" && (
<>
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.jreDesc}</p>
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
<table className="w-full table-fixed border-collapse text-sm max-[760px]:block">
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
<tr className="border-b border-landing-line">
<th className="px-5 py-2.5 text-left font-medium">{t.platform}</th>
<th className="w-[24%] px-5 py-2.5 text-left font-medium">{t.platform}</th>
<th className="px-5 py-2.5 text-left font-medium">JRE</th>
<th className="px-5 py-2.5 text-left font-medium">{t.version}</th>
<th className="px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-24 px-5 py-2.5" />
<th className="w-[116px] px-5 py-2.5 text-right font-medium">{t.size}</th>
<th className="w-[340px] px-5 py-2.5 text-right font-medium">{t.downloadSources}</th>
</tr>
</thead>
<tbody className="max-[760px]:block">
@ -483,12 +512,9 @@ export function DriversClient() {
<span className="inline-flex rounded-[5px] border border-landing-green/35 bg-landing-green/10 px-1.5 py-0.5 font-mono text-[11px] text-landing-green">JRE {j.jreKey}</span>
</td>
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{j.jreVersion}</td>
<td className="px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(j.info.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:px-0">
<a href={j.info.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
<Download size={13} />
{t.download}
</a>
<td className="whitespace-nowrap px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(j.info.size)}</td>
<td className="px-5 py-3 text-right max-[760px]:col-span-2 max-[760px]:px-0 max-[760px]:pt-0">
<DownloadLinks url={j.info.url} t={t} />
</td>
</tr>
);

View File

@ -73,6 +73,8 @@ const databaseSupport = [
{ name: "Qdrant", icon: "/icons/database/qdrant.svg", tone: "#dc244c" },
{ name: "Milvus", icon: "/icons/database/milvus.png", tone: "#00a1ea" },
{ name: "Weaviate", icon: "/icons/database/weaviate.png", tone: "#00b894" },
{ name: "ChromaDB", shortLabel: "CH", tone: "#ff7a59" },
{ name: "Cloudflare D1", shortLabel: "D1", tone: "#f6821f" },
{ name: "MariaDB", icon: "/icons/database/mariadb.svg", tone: "#003545" },
{ name: "Doris", icon: "/icons/database/doris.svg", tone: "#5b7cfa" },
{ name: "StarRocks", icon: "/icons/database/starrocks.svg", tone: "#6750ff" },
@ -100,6 +102,7 @@ const databaseSupport = [
{ name: "Trino", icon: "/icons/database/trino.svg", tone: "#dd00a1" },
{ name: "PrestoSQL", icon: "/icons/database/presto.svg", tone: "#5890ff" },
{ name: "Hive", icon: "/icons/database/hive.svg", tone: "#fdcb00" },
{ name: "Apache Spark", shortLabel: "SP", tone: "#e25a1c" },
{ name: "DB2", icon: "/icons/database/db2.svg", tone: "#054ada" },
{ name: "SAP HANA", icon: "/icons/database/saphana.webp", tone: "#008fd3" },
{ name: "Teradata", icon: "/icons/database/teradata.webp", tone: "#f37440" },
@ -110,6 +113,8 @@ const databaseSupport = [
{ name: "Neo4j", icon: "/icons/database/neo4j.svg", tone: "#018bff" },
{ name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" },
{ name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" },
{ name: "Dremio", shortLabel: "DR", tone: "#30bdbe" },
{ name: "OSCAR", shortLabel: "OS", tone: "#1b8dff" },
{ name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" },
{ name: "QuestDB", icon: "/icons/database/questdb.svg", tone: "#dc2626" },
{ name: "IoTDB", icon: "/icons/database/iotdb.svg", tone: "#3cb371" },
@ -125,6 +130,7 @@ const databaseSupport = [
{ name: "Etcd", icon: "/icons/database/etcd.svg", tone: "#419eda" },
{ name: "ZooKeeper", icon: "/icons/database/zookeeper.svg", tone: "#3b82f6" },
{ name: "Pulsar", icon: "/icons/database/pulsar.svg", tone: "#188fff" },
{ name: "Apache Kafka", shortLabel: "KF", tone: "#231f20" },
{ name: "Nacos", icon: "/icons/database/nacos.png", tone: "#2f80ed" },
{ name: "IRIS", icon: "/icons/database/iris.png", tone: "#0085ca" },
{ name: "JDBC", icon: "/icons/database/jdbc.svg", tone: "#6ea8ff" },
@ -549,7 +555,7 @@ export default async function LandingPage({ params }: { params: Promise<{ lang:
) : db.icon ? (
<img src={db.icon} alt="" width={38} height={38} className="block w-[38px] h-[38px] object-contain" />
) : (
<span className="grid place-items-center min-w-[46px] h-8 rounded-lg px-2 text-white text-xs font-[780]">{db.name.slice(0, 2).toUpperCase()}</span>
<span className="grid place-items-center min-w-[46px] h-8 rounded-lg px-2 text-xs font-[780] text-white" style={{ backgroundColor: db.tone }}>{db.shortLabel ?? db.name.slice(0, 2).toUpperCase()}</span>
)}
</div>
<strong className={`text-sm font-[650] leading-[1.2] text-center ${isCta ? "text-landing-blue" : "text-[color-mix(in_srgb,var(--color-landing-ink)_92%,var(--color-landing-muted))]"}`}>{db.name}</strong>

View File

@ -12,12 +12,13 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能
| 分组 | 类型 | 说明 |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| 核心 SQL 数据库 | MySQL、PostgreSQL、SQLite、DuckDB、ClickHouse、SQL Server、Oracle、Redshift、DM、GaussDB、KWDB、Teradata、Vertica、Exasol、Firebird、SAP HANA、崖山 YashanDB、GBase | 直接出现在 DBX 连接选择器中 |
| 文档、键值与配置服务 | Redis、MongoDB、Elasticsearch、etcd、Nacos | 根据数据模型使用专门浏览器、管理控制台或查询流程,不完全等同于关系型表格 |
| 文档、键值、向量与配置服务 | Redis、MongoDB、Elasticsearch、Qdrant、Milvus、Weaviate、ChromaDB、etcd、Apache ZooKeeper、Nacos | 根据数据模型使用专门浏览器、管理控制台或查询流程,不完全等同于关系型表格 |
| MySQL 兼容类型 | MariaDB、TiDB、OceanBase、Doris、SelectDB、StarRocks、Manticore Search、GoldenDB、TDSQL、PolarDB、GreatSQL、自定义 MySQL | 复用 MySQL 风格连接能力 |
| PostgreSQL 兼容类型 | openGauss、KingBase、HighGo、Vastbase、CockroachDB、自定义 PostgreSQL | 复用 PostgreSQL 风格连接能力 |
| 文件型数据库 | SQLite、DuckDB、Microsoft Access、RQLite、Turso | 选择本地数据库文件或 HTTP 端点,不填写主机和端口 |
| 时序与边缘数据库 | Cloudflare D1、InfluxDB、IoTDB、TDengine、Databend、QuestDB | 托管 SQLite、时序、IoT 或边缘场景 |
| Agent/JDBC 扩展类型 | H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、XuguDB、Databricks、IRIS、JDBC | 功能覆盖取决于对应驱动路径 |
| 分析与消息服务 | Apache Spark、Dremio、Apache Pulsar、Apache Kafka | 用于分布式分析、消息和流管理场景 |
| Agent/JDBC 扩展类型 | H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、OSCAR、XuguDB、Databricks、IRIS、JDBC | 功能覆盖取决于对应驱动路径 |
## 默认端口
@ -31,6 +32,10 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能
| SQL Server | 1433 |
| Oracle | 1521 |
| Elasticsearch | 9200 |
| Qdrant | 6333 |
| Milvus | 19530 |
| Weaviate | 8080 |
| ChromaDB | 8000 |
| TiDB | 4000 |
| OceanBase | 2881 |
| Doris / SelectDB / StarRocks / Databend | 9030 |
@ -53,6 +58,7 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能
| InfluxDB | 8086 |
| IoTDB | 6667 |
| etcd | 2379 |
| Apache ZooKeeper | 2181 |
| Nacos | 8848 |
| Nacos 控制台/admin API | 8085 |
| IRIS | 1972 |
@ -62,16 +68,21 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能
| Trino | 8080 |
| PrestoSQL | 8080 |
| Hive | 10000 |
| Apache Spark | 10015 |
| DB2 | 50000 |
| Informix | 9088 |
| Dremio | 31010 |
| Neo4j | 7687 |
| Cassandra | 9042 |
| BigQuery | 443 |
| Kylin | 7070 |
| SunDB | 22000 |
| OSCAR | 2003 |
| TDengine | 6041 |
| XuguDB | 5138 |
| QuestDB | 8812 |
| Apache Pulsar Admin API | 8080 |
| Apache Kafka | 9092 |
<Callout type="info">SQLite、DuckDB、Access 和 JDBC 在不需要网络端口时,会在连接模型中保存为端口 `0`。</Callout>

View File

@ -12,12 +12,13 @@ The connection dialog exposes ready-to-use profiles with default ports and drive
| Group | Profiles | Notes |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Core SQL engines | MySQL, PostgreSQL, SQLite, DuckDB, ClickHouse, SQL Server, Oracle, Redshift, DM, GaussDB, KWDB, Teradata, Vertica, Exasol, Firebird, SAP HANA, YashanDB, GBase | Available directly in the DBX connection picker |
| Document, key-value, and config services | Redis, MongoDB, Elasticsearch, etcd, Nacos | Use dedicated browsers, admin consoles, or query flows instead of the generic relational grid where appropriate |
| Document, key-value, vector, and config services | Redis, MongoDB, Elasticsearch, Qdrant, Milvus, Weaviate, ChromaDB, etcd, Apache ZooKeeper, Nacos | Use dedicated browsers, admin consoles, or query flows instead of the generic relational grid where appropriate |
| MySQL-compatible profiles | MariaDB, TiDB, OceanBase, Doris, SelectDB, StarRocks, Manticore Search, GoldenDB, TDSQL, PolarDB, GreatSQL, custom MySQL | Reuse MySQL-style connection handling where the engine speaks a compatible protocol |
| PostgreSQL-compatible profiles | openGauss, KingBase, HighGo, Vastbase, CockroachDB, custom PostgreSQL | Reuse PostgreSQL-style connection handling where the engine speaks a compatible protocol |
| File-based engines | SQLite, DuckDB, Microsoft Access, RQLite, Turso | Choose a local database file or HTTP endpoint instead of host and port |
| Time-series and edge | Cloudflare D1, InfluxDB, IoTDB, TDengine, Databend, QuestDB | Managed SQLite, time-series, IoT, or edge workloads |
| Agent/JDBC-oriented engines | H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, XuguDB, Databricks, IRIS, JDBC | Feature coverage depends on the driver path used by that engine |
| Analytics and messaging services | Apache Spark, Dremio, Apache Pulsar, Apache Kafka | Distributed analytics, messaging, and stream-management workloads |
| Agent/JDBC-oriented engines | H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, OSCAR, XuguDB, Databricks, IRIS, JDBC | Feature coverage depends on the driver path used by that engine |
## Default Ports
@ -31,6 +32,10 @@ The connection dialog exposes ready-to-use profiles with default ports and drive
| SQL Server | 1433 |
| Oracle | 1521 |
| Elasticsearch | 9200 |
| Qdrant | 6333 |
| Milvus | 19530 |
| Weaviate | 8080 |
| ChromaDB | 8000 |
| TiDB | 4000 |
| OceanBase | 2881 |
| Doris / SelectDB / StarRocks / Databend | 9030 |
@ -53,6 +58,7 @@ The connection dialog exposes ready-to-use profiles with default ports and drive
| InfluxDB | 8086 |
| IoTDB | 6667 |
| etcd | 2379 |
| Apache ZooKeeper | 2181 |
| Nacos | 8848 |
| Nacos console/admin API | 8085 |
| IRIS | 1972 |
@ -62,16 +68,21 @@ The connection dialog exposes ready-to-use profiles with default ports and drive
| Trino | 8080 |
| PrestoSQL | 8080 |
| Hive | 10000 |
| Apache Spark | 10015 |
| DB2 | 50000 |
| Informix | 9088 |
| Dremio | 31010 |
| Neo4j | 7687 |
| Cassandra | 9042 |
| BigQuery | 443 |
| Kylin | 7070 |
| SunDB | 22000 |
| OSCAR | 2003 |
| TDengine | 6041 |
| XuguDB | 5138 |
| QuestDB | 8812 |
| Apache Pulsar Admin API | 8080 |
| Apache Kafka | 9092 |
<Callout type="info">SQLite, DuckDB, Access, and JDBC profiles use port `0` in the saved connection model when a network port is not part of the connection.</Callout>

View File

@ -1,6 +1,10 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { buildAgentDownloadCatalog } from "./agentRegistry";
import { afterEach, test, vi } from "vitest";
import { buildAgentDownloadCatalog, downloadLinksFor, fetchAgentDownloadCatalog, formatSize } from "./agentRegistry";
afterEach(() => {
vi.restoreAllMocks();
});
test("offline download catalog includes the JDBC plugin ZIP", () => {
const catalog = buildAgentDownloadCatalog([]);
@ -11,3 +15,53 @@ test("offline download catalog includes the JDBC plugin ZIP", () => {
url: "https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip",
});
});
test("release assets expose GitHub, CNB, and AtomGit download links", () => {
assert.deepEqual(downloadLinksFor("https://github.com/t8y2/dbx/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip"), [
{ source: "github", url: "https://github.com/t8y2/dbx/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip" },
{ source: "cnb", url: "https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip" },
{ source: "atomgit", url: "https://atomgit.com/t8y2/dbx/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip" },
]);
});
test("non-release assets retain their official download link", () => {
assert.deepEqual(downloadLinksFor("https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip"), [
{ source: "official", url: "https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip" },
]);
});
test("catalog falls back from GitHub to CNB and then AtomGit", async () => {
const requestedUrls: string[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
requestedUrls.push(url);
if (url.includes("api.github.com")) return new Response("rate limited", { status: 403 });
if (url.includes("cnb.cool")) throw new TypeError("CORS blocked");
return Response.json({
tag_name: "agents-latest",
assets: [
{ name: "dbx-agent-access.jar", type: "attach" },
{ name: "dbx-jre-21-macos-aarch64.tar.gz", type: "attach" },
{ name: "dbx-agents-offline-macos-aarch64.zip", type: "attach" },
],
});
}),
);
const catalog = await fetchAgentDownloadCatalog();
assert.deepEqual(requestedUrls, [
"https://api.github.com/repos/t8y2/dbx/releases/tags/agents-latest",
"https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/agent-registry.json",
"https://api.atomgit.com/api/v5/repos/t8y2/dbx/releases/agents-latest",
]);
assert.equal(catalog?.drivers[0]?.key, "access");
assert.equal(catalog?.jres[0]?.platformKey, "macos-aarch64");
assert.equal(catalog?.bundles[0]?.platformKey, "macos-aarch64");
});
test("unknown fallback asset sizes render as unavailable", () => {
assert.equal(formatSize(0), "—");
});

View File

@ -5,6 +5,13 @@ export interface ArtifactInfo {
size: number;
}
export type DownloadSource = "github" | "cnb" | "atomgit" | "official";
export interface DownloadLink {
source: DownloadSource;
url: string;
}
export interface GitHubReleaseAsset {
name: string;
size: number;
@ -15,6 +22,26 @@ interface GitHubRelease {
assets?: GitHubReleaseAsset[];
}
interface AgentRegistryArtifact {
url: string;
size: number;
}
interface AgentRegistryDriver {
jar?: AgentRegistryArtifact;
native?: Record<string, AgentRegistryArtifact>;
}
interface AgentRegistry {
jres?: Record<string, { platforms?: Record<string, AgentRegistryArtifact> }>;
drivers?: Record<string, AgentRegistryDriver>;
}
interface AtomGitRelease {
tag_name?: string;
assets?: Array<{ name?: string; type?: string }>;
}
export interface OfflineBundleEntry {
platformKey: string;
platformLabel: string;
@ -65,7 +92,12 @@ export interface AgentDownloadCatalog {
}
const AGENTS_LATEST_RELEASE_API_URL = "https://api.github.com/repos/t8y2/dbx/releases/tags/agents-latest";
const CNB_AGENT_REGISTRY_URL = "https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/agent-registry.json";
const ATOMGIT_LATEST_RELEASE_API_URL = "https://api.atomgit.com/api/v5/repos/t8y2/dbx/releases/agents-latest";
const JDBC_PLUGIN_DOWNLOAD_URL = "https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip";
const GITHUB_RELEASE_DOWNLOAD_PREFIX = "https://github.com/t8y2/dbx/releases/download/";
const CNB_RELEASE_DOWNLOAD_PREFIX = "https://cnb.cool/dbxio.com/dbx/-/releases/download/";
const ATOMGIT_RELEASE_DOWNLOAD_PREFIX = "https://atomgit.com/t8y2/dbx/releases/download/";
const MIN_APP_VERSION = "0.6.0";
const driverVersionMap = driverVersions as Record<string, string>;
const nativeDriverKeys = new Set(["oracle", "xugu"]);
@ -136,23 +168,100 @@ function assetInfo(asset: GitHubReleaseAsset): ArtifactInfo {
};
}
function releaseAssetName(url: string, fallback: string): string {
try {
return decodeURIComponent(new URL(url).pathname.split("/").pop() || fallback);
} catch {
return fallback;
}
}
function githubReleaseAsset(name: string, size: number, tag = "agents-latest"): GitHubReleaseAsset {
return {
name,
size,
browser_download_url: `${GITHUB_RELEASE_DOWNLOAD_PREFIX}${tag}/${name}`,
};
}
function registryReleaseAssets(registry: AgentRegistry): GitHubReleaseAsset[] {
const assets = new Map<string, GitHubReleaseAsset>();
const addArtifact = (artifact: AgentRegistryArtifact | undefined, fallbackName: string) => {
if (!artifact?.url) return;
const name = releaseAssetName(artifact.url, fallbackName);
assets.set(name, { name, size: artifact.size || 0, browser_download_url: artifact.url });
};
for (const [jreKey, jre] of Object.entries(registry.jres ?? {})) {
for (const [platformKey, artifact] of Object.entries(jre.platforms ?? {})) {
addArtifact(artifact, `dbx-jre-${jreKey}-${platformKey}.tar.gz`);
}
}
for (const [driverKey, driver] of Object.entries(registry.drivers ?? {})) {
addArtifact(driver.jar, `dbx-agent-${driverKey}.jar`);
for (const [platformKey, artifact] of Object.entries(driver.native ?? {})) {
addArtifact(artifact, `dbx-agent-${driverKey}-${platformKey}`);
}
}
for (const platformKey of Object.keys(registry.jres?.["21"]?.platforms ?? {})) {
const name = `dbx-agents-offline-${platformKey}.zip`;
assets.set(name, githubReleaseAsset(name, 0));
}
return Array.from(assets.values());
}
function atomGitReleaseAssets(release: AtomGitRelease): GitHubReleaseAsset[] {
const tag = release.tag_name || "agents-latest";
return (release.assets ?? [])
.filter((asset): asset is { name: string; type?: string } => Boolean(asset.name) && asset.type !== "source")
.map((asset) => githubReleaseAsset(asset.name, 0, tag));
}
function hasDownloadAssets(catalog: AgentDownloadCatalog): boolean {
return catalog.bundles.length > 0 || catalog.drivers.length > 0 || catalog.jres.length > 0 || catalog.nativeAgents.length > 0;
}
async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
const response = await fetch(url, { cache: "no-store", headers });
if (!response.ok) throw new Error(`Download catalog request failed with ${response.status}`);
return response.json() as Promise<T>;
}
export function downloadLinksFor(url: string): DownloadLink[] {
const releasePath = url.startsWith(GITHUB_RELEASE_DOWNLOAD_PREFIX) ? url.slice(GITHUB_RELEASE_DOWNLOAD_PREFIX.length) : null;
if (!releasePath) return [{ source: "official", url }];
return [
{ source: "github", url },
{ source: "cnb", url: `${CNB_RELEASE_DOWNLOAD_PREFIX}${releasePath}` },
{ source: "atomgit", url: `${ATOMGIT_RELEASE_DOWNLOAD_PREFIX}${releasePath}` },
];
}
function assetMap(assets: GitHubReleaseAsset[]): Map<string, GitHubReleaseAsset> {
return new Map(assets.map((asset) => [asset.name, asset]));
}
export async function fetchAgentDownloadCatalog(): Promise<AgentDownloadCatalog | null> {
try {
const res = await fetch(AGENTS_LATEST_RELEASE_API_URL, {
cache: "no-store",
headers: { Accept: "application/vnd.github+json" },
});
if (!res.ok) return null;
const loaders: Array<() => Promise<GitHubReleaseAsset[]>> = [
async () => (await fetchJson<GitHubRelease>(AGENTS_LATEST_RELEASE_API_URL, { Accept: "application/vnd.github+json" })).assets ?? [],
async () => registryReleaseAssets(await fetchJson<AgentRegistry>(CNB_AGENT_REGISTRY_URL)),
async () => atomGitReleaseAssets(await fetchJson<AtomGitRelease>(ATOMGIT_LATEST_RELEASE_API_URL, { Accept: "application/json" })),
];
const release = (await res.json()) as GitHubRelease;
return buildAgentDownloadCatalog(release.assets ?? []);
} catch {
return null;
for (const loadAssets of loaders) {
try {
const catalog = buildAgentDownloadCatalog(await loadAssets());
if (hasDownloadAssets(catalog)) return catalog;
} catch {
// Try the next synchronized release source.
}
}
return null;
}
export function buildAgentDownloadCatalog(assets: GitHubReleaseAsset[]): AgentDownloadCatalog {
@ -255,6 +364,7 @@ export function buildOfflineBundleEntries(assets: GitHubReleaseAsset[]): Offline
}
export function formatSize(bytes: number): string {
if (bytes <= 0) return "—";
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}