diff --git a/docs/app/[lang]/databases/page.tsx b/docs/app/[lang]/databases/page.tsx
index 89d2d7b03..ca03b0440 100644
--- a/docs/app/[lang]/databases/page.tsx
+++ b/docs/app/[lang]/databases/page.tsx
@@ -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 ? (
) : (
- {db.name.slice(0, 2).toUpperCase()}
+ {db.shortLabel ?? db.name.slice(0, 2).toUpperCase()}
)}
{db.name}
diff --git a/docs/app/[lang]/drivers/DriversClient.tsx b/docs/app/[lang]/drivers/DriversClient.tsx
index 222670b7a..07545968e 100644
--- a/docs/app/[lang]/drivers/DriversClient.tsx
+++ b/docs/app/[lang]/drivers/DriversClient.tsx
@@ -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 (
+
+ );
+}
+
function matchesSearch(values: Array, query: string): boolean {
if (!query) return true;
return values.filter(Boolean).join(" ").toLowerCase().includes(query);
@@ -227,6 +264,10 @@ export function DriversClient() {
{catalog && !loading && (
<>
+
+
+ {t.mirrorHint}
+
@@ -284,13 +325,13 @@ export function DriversClient() {
{activeTab === "jdbcPlugin" && (
<>
{t.jdbcPluginDesc}
-
+
- | {t.jdbcPluginFile} |
+ {t.jdbcPluginFile} |
{t.filename} |
{t.installMethod} |
- |
+ {t.downloadSources} |
@@ -303,13 +344,10 @@ export function DriversClient() {
{t.jdbcPluginInstallHint}
- {plugin.filename} |
+ {plugin.filename} |
{t.jdbcPluginInstallHint} |
-
-
-
- {t.download}
-
+ |
+
|
))}
@@ -322,13 +360,13 @@ export function DriversClient() {
{activeTab === "bundles" && (
<>
{t.bundlesDesc}
-
+
- | {t.platform} |
+ {t.platform} |
{t.filename} |
- {t.size} |
- |
+ {t.size} |
+ {t.downloadSources} |
@@ -340,13 +378,10 @@ export function DriversClient() {
ZIP
- {bundle.filename} |
- {formatSize(bundle.size)} |
-
-
-
- {t.download}
-
+ | {bundle.filename} |
+ {formatSize(bundle.size)} |
+
+
|
))}
@@ -359,15 +394,15 @@ export function DriversClient() {
{activeTab === "drivers" && (
<>
{t.driversDesc}
-
+
- | Driver |
- Key |
+ Driver |
+ Key |
{t.version} |
{t.requiresJre} |
- {t.size} |
- |
+ {t.size} |
+ {t.downloadSources} |
@@ -384,12 +419,9 @@ export function DriversClient() {
{d.version} |
{d.jre} |
- {formatSize(d.jar.size)} |
-
-
-
- {t.download}
-
+ | {formatSize(d.jar.size)} |
+
+
|
))}
@@ -402,14 +434,14 @@ export function DriversClient() {
{activeTab === "native" && (
<>
{t.nativeAgentsDesc}
-
+
- | Agent |
+ Agent |
{t.platform} |
{t.version} |
- {t.size} |
- |
+ {t.size} |
+ {t.downloadSources} |
@@ -439,12 +471,9 @@ export function DriversClient() {
{group.version} |
- {formatSize(selectedAgent.info.size)} |
-
-
-
- {t.download}
-
+ | {formatSize(selectedAgent.info.size)} |
+
+
|
);
@@ -458,14 +487,14 @@ export function DriversClient() {
{activeTab === "jre" && (
<>
{t.jreDesc}
-
+
- | {t.platform} |
+ {t.platform} |
JRE |
{t.version} |
- {t.size} |
- |
+ {t.size} |
+ {t.downloadSources} |
@@ -483,12 +512,9 @@ export function DriversClient() {
JRE {j.jreKey}
{j.jreVersion} |
- {formatSize(j.info.size)} |
-
-
-
- {t.download}
-
+ | {formatSize(j.info.size)} |
+
+
|
);
diff --git a/docs/app/[lang]/page.tsx b/docs/app/[lang]/page.tsx
index 85073c2be..1da8213d6 100644
--- a/docs/app/[lang]/page.tsx
+++ b/docs/app/[lang]/page.tsx
@@ -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 ? (
) : (
- {db.name.slice(0, 2).toUpperCase()}
+ {db.shortLabel ?? db.name.slice(0, 2).toUpperCase()}
)}
{db.name}
diff --git a/docs/content/docs/databases.cn.mdx b/docs/content/docs/databases.cn.mdx
index f5ae9babc..bdcb57fa9 100644
--- a/docs/content/docs/databases.cn.mdx
+++ b/docs/content/docs/databases.cn.mdx
@@ -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 |
SQLite、DuckDB、Access 和 JDBC 在不需要网络端口时,会在连接模型中保存为端口 `0`。
diff --git a/docs/content/docs/databases.mdx b/docs/content/docs/databases.mdx
index ca77869a3..8ad5147a8 100644
--- a/docs/content/docs/databases.mdx
+++ b/docs/content/docs/databases.mdx
@@ -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 |
SQLite, DuckDB, Access, and JDBC profiles use port `0` in the saved connection model when a network port is not part of the connection.
diff --git a/docs/lib/agentRegistry.test.ts b/docs/lib/agentRegistry.test.ts
index ec6b4cf94..9370d41ff 100644
--- a/docs/lib/agentRegistry.test.ts
+++ b/docs/lib/agentRegistry.test.ts
@@ -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), "—");
+});
diff --git a/docs/lib/agentRegistry.ts b/docs/lib/agentRegistry.ts
index 223d526a2..78fcf01bd 100644
--- a/docs/lib/agentRegistry.ts
+++ b/docs/lib/agentRegistry.ts
@@ -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;
+}
+
+interface AgentRegistry {
+ jres?: Record }>;
+ drivers?: Record;
+}
+
+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;
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();
+ 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(url: string, headers?: HeadersInit): Promise {
+ 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;
+}
+
+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 {
return new Map(assets.map((asset) => [asset.name, asset]));
}
export async function fetchAgentDownloadCatalog(): Promise {
- 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> = [
+ async () => (await fetchJson(AGENTS_LATEST_RELEASE_API_URL, { Accept: "application/vnd.github+json" })).assets ?? [],
+ async () => registryReleaseAssets(await fetchJson(CNB_AGENT_REGISTRY_URL)),
+ async () => atomGitReleaseAssets(await fetchJson(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`;
}