diff --git a/apps/desktop/src/components/settings/ChangelogPanel.vue b/apps/desktop/src/components/settings/ChangelogPanel.vue
new file mode 100644
index 0000000000..ec9ae9c8e4
--- /dev/null
+++ b/apps/desktop/src/components/settings/ChangelogPanel.vue
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
{{ t("settings.changelogDescription") }}
+
+
+
+
+
+
+
+
+
+ {{ t("settings.changelogLoading") }}
+
+
+
+
{{ t("settings.changelogLoadFailed") }}
+
+
+
+
+
+
+
{{ t("settings.changelogEmpty") }}
+
+
+
+
+
+
+
+
+
{{ sectionLabel(section.type, section.title) }}
+
+ -
+
+
+ {{ item.title }} — {{ item.desc }}
+ {{ item.title }}
+
+
+
+
+
+
{{ t("settings.changelogNoDetails") }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 31debdf7cd..56d2c8d2e0 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -3632,6 +3632,23 @@ export default {
project: "Project",
openSource: "Open-source repository",
officialDocs: "Official docs",
+ changelogTitle: "Changelog",
+ changelogDescription: "Browse what's new in each release.",
+ changelogLoading: "Loading changelog...",
+ changelogLoadFailed: "Failed to load changelog.",
+ changelogEmpty: "No changelog entries yet.",
+ changelogPublishedOn: "Published on {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "Load more",
+ changelogOpenWebsite: "View on website",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "See GitHub Release for details.",
+ changelogRetry: "Retry",
+ changelogSectionAdded: "New Features",
+ changelogSectionImproved: "Improvements",
+ changelogSectionFixed: "Bug Fixes",
+ changelogSectionChanged: "Changes",
+ changelogSectionRemoved: "Removed",
shortcutUppercaseSelection: "Convert selection to uppercase",
shortcutLowercaseSelection: "Convert selection to lowercase",
shortcutExPasteSqlInCondition: "ExPaste: paste as IN condition",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index b56f88b196..58a9c20d3f 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -3422,6 +3422,23 @@ export default withEnglishFallback({
project: "Proyecto",
openSource: "Repositorio de código abierto",
officialDocs: "Documentación oficial",
+ changelogTitle: "Registro de cambios",
+ changelogDescription: "Consulta las novedades de cada versión.",
+ changelogLoading: "Cargando el registro de cambios...",
+ changelogLoadFailed: "No se pudo cargar el registro de cambios.",
+ changelogEmpty: "Aún no hay entradas en el registro de cambios.",
+ changelogPublishedOn: "Publicado el {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "Cargar más",
+ changelogOpenWebsite: "Ver en el sitio web",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "Consulta GitHub Release para más detalles.",
+ changelogRetry: "Reintentar",
+ changelogSectionAdded: "Novedades",
+ changelogSectionImproved: "Mejoras",
+ changelogSectionFixed: "Correcciones",
+ changelogSectionChanged: "Cambios",
+ changelogSectionRemoved: "Eliminado",
shortcutUppercaseSelection: "Convertir selección a mayúsculas",
shortcutLowercaseSelection: "Convertir selección a minúsculas",
shortcutExPasteSqlInCondition: "ExPaste: pegar como condición IN",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index f59ddc2183..05209b8082 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -3420,6 +3420,23 @@ export default withEnglishFallback({
project: "Progetto",
openSource: "Repository open-source",
officialDocs: "Documenti ufficiali",
+ changelogTitle: "Registro modifiche",
+ changelogDescription: "Consulta le novità di ogni versione.",
+ changelogLoading: "Caricamento del registro modifiche...",
+ changelogLoadFailed: "Impossibile caricare il registro modifiche.",
+ changelogEmpty: "Nessuna voce nel registro modifiche.",
+ changelogPublishedOn: "Pubblicato il {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "Carica altro",
+ changelogOpenWebsite: "Apri sul sito",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "Vedi GitHub Release per i dettagli.",
+ changelogRetry: "Riprova",
+ changelogSectionAdded: "Nuove funzionalità",
+ changelogSectionImproved: "Miglioramenti",
+ changelogSectionFixed: "Correzioni",
+ changelogSectionChanged: "Modifiche",
+ changelogSectionRemoved: "Rimossi",
shortcutUppercaseSelection: "Converti selezione in maiuscolo",
shortcutLowercaseSelection: "Converti selezione in minuscolo",
shortcutExPasteSqlInCondition: "ExPaste: incolla come condizione IN",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 2ee097296a..4ee32ee6eb 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -3421,6 +3421,23 @@ export default withEnglishFallback({
project: "プロジェクト",
openSource: "オープンソースリポジトリ",
officialDocs: "公式ドキュメント",
+ changelogTitle: "更新履歴",
+ changelogDescription: "各バージョンの更新内容を確認できます。",
+ changelogLoading: "更新履歴を読み込み中...",
+ changelogLoadFailed: "更新履歴の読み込みに失敗しました。",
+ changelogEmpty: "更新履歴はまだありません。",
+ changelogPublishedOn: "公開日 {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "さらに読み込む",
+ changelogOpenWebsite: "公式サイトで見る",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "詳細は GitHub Release を参照してください。",
+ changelogRetry: "再試行",
+ changelogSectionAdded: "新機能",
+ changelogSectionImproved: "改善",
+ changelogSectionFixed: "不具合修正",
+ changelogSectionChanged: "変更",
+ changelogSectionRemoved: "削除",
shortcutUppercaseSelection: "選択範囲を大文字に変換",
shortcutLowercaseSelection: "選択範囲を小文字に変換",
shortcutExPasteSqlInCondition: "ExPaste: IN条件として貼り付け",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 3d90e77b27..9a23ba2373 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -3422,6 +3422,23 @@ export default withEnglishFallback({
project: "Projeto",
openSource: "Repositório de código aberto",
officialDocs: "Documentação oficial",
+ changelogTitle: "Registro de alterações",
+ changelogDescription: "Veja o que há de novo em cada versão.",
+ changelogLoading: "Carregando o registro de alterações...",
+ changelogLoadFailed: "Falha ao carregar o registro de alterações.",
+ changelogEmpty: "Ainda não há entradas no registro de alterações.",
+ changelogPublishedOn: "Publicado em {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "Carregar mais",
+ changelogOpenWebsite: "Ver no site",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "Veja o GitHub Release para detalhes.",
+ changelogRetry: "Tentar novamente",
+ changelogSectionAdded: "Novidades",
+ changelogSectionImproved: "Melhorias",
+ changelogSectionFixed: "Correções",
+ changelogSectionChanged: "Alterações",
+ changelogSectionRemoved: "Removido",
shortcutUppercaseSelection: "Converter seleção em maiúsculas",
shortcutLowercaseSelection: "Converter seleção em minúsculas",
shortcutExPasteSqlInCondition: "ExPaste: colar como condição IN",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index a02b10f860..c09067a5d3 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -3634,6 +3634,23 @@ export default withEnglishFallback({
project: "项目",
openSource: "开源仓库",
officialDocs: "官方文档",
+ changelogTitle: "更新日志",
+ changelogDescription: "查看各版本的更新内容。",
+ changelogLoading: "正在加载更新日志...",
+ changelogLoadFailed: "加载更新日志失败。",
+ changelogEmpty: "暂无更新日志。",
+ changelogPublishedOn: "发布于 {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "加载更多",
+ changelogOpenWebsite: "在官网查看",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "详见 GitHub Release。",
+ changelogRetry: "重试",
+ changelogSectionAdded: "新功能",
+ changelogSectionImproved: "改进",
+ changelogSectionFixed: "问题修复",
+ changelogSectionChanged: "变更",
+ changelogSectionRemoved: "移除",
},
driverStore: {
progressJreExtract: "解压 JRE...",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 805069df16..a61c69298c 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -3221,6 +3221,23 @@ export default withEnglishFallback({
project: "專案",
openSource: "開源倉庫",
officialDocs: "官方文件",
+ changelogTitle: "更新日誌",
+ changelogDescription: "查看各版本的更新內容。",
+ changelogLoading: "正在載入更新日誌...",
+ changelogLoadFailed: "載入更新日誌失敗。",
+ changelogEmpty: "暫無更新日誌。",
+ changelogPublishedOn: "發佈於 {date}",
+ changelogNew: "NEW",
+ changelogLoadMore: "載入更多",
+ changelogOpenWebsite: "在官網查看",
+ changelogOpenGitHub: "GitHub Release",
+ changelogNoDetails: "詳見 GitHub Release。",
+ changelogRetry: "重試",
+ changelogSectionAdded: "新功能",
+ changelogSectionImproved: "改進",
+ changelogSectionFixed: "問題修復",
+ changelogSectionChanged: "變更",
+ changelogSectionRemoved: "移除",
mcpTab: "MCP",
mcpTitle: "MCP Server",
mcpDescription: "檢查 Claude Code、Cursor 等程式助理使用的 DBX MCP Server 安裝與版本狀態。",
diff --git a/apps/desktop/src/lib/app/changelog.ts b/apps/desktop/src/lib/app/changelog.ts
new file mode 100644
index 0000000000..51ace0eaff
--- /dev/null
+++ b/apps/desktop/src/lib/app/changelog.ts
@@ -0,0 +1,76 @@
+export type ChangelogItem = {
+ title: string;
+ desc: string;
+};
+
+export type ChangelogSection = {
+ type: string;
+ title: string;
+ items: ChangelogItem[];
+};
+
+export type ChangelogRelease = {
+ tag: string;
+ name: string;
+ date: string;
+ sections: ChangelogSection[];
+};
+
+export type ChangelogData = {
+ updatedAt: string;
+ releases: ChangelogRelease[];
+};
+
+export type ChangelogLang = "en" | "cn";
+
+const cache = new Map
>();
+
+export function createLatestRequestGuard() {
+ let latestRequestId = 0;
+ return {
+ begin: () => ++latestRequestId,
+ isCurrent: (requestId: number) => requestId === latestRequestId,
+ };
+}
+
+export function changelogLangFromLocale(locale: string): ChangelogLang {
+ return locale === "zh-CN" || locale === "zh-TW" ? "cn" : "en";
+}
+
+export function changelogWebsiteUrl(lang: ChangelogLang): string {
+ return `https://dbxio.com/${lang}/changelog`;
+}
+
+export function changelogReleaseUrl(tag: string): string {
+ return `https://github.com/t8y2/dbx/releases/tag/${encodeURIComponent(tag)}`;
+}
+
+export async function fetchChangelog(lang: ChangelogLang, options: { force?: boolean } = {}): Promise {
+ if (options.force) {
+ cache.delete(lang);
+ }
+
+ let pending = cache.get(lang);
+ if (!pending) {
+ pending = loadChangelog(lang).catch((error) => {
+ cache.delete(lang);
+ throw error;
+ });
+ cache.set(lang, pending);
+ }
+
+ return pending;
+}
+
+async function loadChangelog(lang: ChangelogLang): Promise {
+ const { fetchChangelog: fetchChangelogViaBackend } = await import("@/lib/backend/api");
+ const data = await fetchChangelogViaBackend(lang);
+ if (!data || !Array.isArray(data.releases)) {
+ throw new Error("Invalid changelog payload");
+ }
+
+ return {
+ updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "",
+ releases: data.releases.filter((release) => release && typeof release.tag === "string"),
+ };
+}
diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts
index a33b09a0ba..e3f3dfeb81 100644
--- a/apps/desktop/src/lib/backend/api.ts
+++ b/apps/desktop/src/lib/backend/api.ts
@@ -466,6 +466,7 @@ export const deleteHistoryEntry = forward("deleteHistoryEntry");
export const checkMcpServerStatus = forward("checkMcpServerStatus");
export const installMcpServer = forward("installMcpServer");
export const checkForUpdates = forward("checkForUpdates");
+export const fetchChangelog = forward("fetchChangelog");
export const getSystemProxyUrl = forward("getSystemProxyUrl");
export const downloadAndInstallUpdate = forward("downloadAndInstallUpdate");
export const getAppVersion = forward("getAppVersion");
diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts
index 8a09cac17a..a0a3216b21 100644
--- a/apps/desktop/src/lib/backend/http.ts
+++ b/apps/desktop/src/lib/backend/http.ts
@@ -2166,6 +2166,11 @@ export async function checkForUpdates(locale?: string): Promise {
return get(`/api/update/check${query}`);
}
+export async function fetchChangelog(lang?: string): Promise {
+ const query = lang ? `?lang=${encodeURIComponent(lang)}` : "";
+ return get(`/api/changelog${query}`);
+}
+
export async function checkMcpServerStatus(): Promise {
return {
installed: false,
diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts
index 79a5e1be52..57297e2bf4 100644
--- a/apps/desktop/src/lib/backend/tauri.ts
+++ b/apps/desktop/src/lib/backend/tauri.ts
@@ -1392,6 +1392,10 @@ export async function checkForUpdates(locale?: string): Promise {
return invoke("check_for_updates", { locale });
}
+export async function fetchChangelog(lang?: string): Promise {
+ return invoke("fetch_changelog", { lang });
+}
+
export async function getSystemProxyUrl(): Promise {
return invoke("get_system_proxy_url");
}
diff --git a/crates/dbx-core/src/changelog.rs b/crates/dbx-core/src/changelog.rs
new file mode 100644
index 0000000000..6fc108367a
--- /dev/null
+++ b/crates/dbx-core/src/changelog.rs
@@ -0,0 +1,99 @@
+use serde::{Deserialize, Serialize};
+
+const CHANGELOG_R2_PREFIX: &str = "changelog/releases-";
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChangelogItem {
+ pub title: String,
+ #[serde(default)]
+ pub desc: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ChangelogSection {
+ #[serde(default)]
+ pub r#type: String,
+ #[serde(default)]
+ pub title: String,
+ #[serde(default)]
+ pub items: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ChangelogRelease {
+ pub tag: String,
+ #[serde(default)]
+ pub name: String,
+ #[serde(default)]
+ pub date: String,
+ #[serde(default)]
+ pub sections: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChangelogData {
+ #[serde(default)]
+ pub updated_at: String,
+ #[serde(default)]
+ pub releases: Vec,
+}
+
+/// Normalize UI locale / short lang into `cn` or `en` (matches R2 file naming).
+pub fn normalize_changelog_lang(lang: &str) -> &'static str {
+ let trimmed = lang.trim();
+ if trimmed.eq_ignore_ascii_case("cn")
+ || trimmed.eq_ignore_ascii_case("zh")
+ || trimmed.eq_ignore_ascii_case("zh-CN")
+ || trimmed.eq_ignore_ascii_case("zh-TW")
+ {
+ "cn"
+ } else {
+ "en"
+ }
+}
+
+pub async fn fetch_changelog(lang: &str) -> Result {
+ let lang = normalize_changelog_lang(lang);
+ let client = build_changelog_http_client()?;
+ let url = format!("{}{CHANGELOG_R2_PREFIX}{lang}.json", crate::R2_CDN_BASE);
+
+ let resp = client
+ .get(&url)
+ .header(reqwest::header::USER_AGENT, "dbx-changelog")
+ .send()
+ .await
+ .and_then(|r| r.error_for_status())
+ .map_err(|e| format!("Failed to fetch changelog: {e}"))?;
+
+ let mut data: ChangelogData = resp.json().await.map_err(|e| format!("Failed to parse changelog: {e}"))?;
+ data.releases.retain(|release| !release.tag.trim().is_empty());
+ Ok(data)
+}
+
+fn build_changelog_http_client() -> Result {
+ let mut builder =
+ reqwest::Client::builder().timeout(std::time::Duration::from_secs(15)).user_agent("dbx-changelog");
+
+ if let Some(proxy_url) = crate::update::system_proxy_url() {
+ let proxy = reqwest::Proxy::all(&proxy_url).map_err(|e| format!("Invalid system proxy URL: {e}"))?;
+ builder = builder.proxy(proxy);
+ }
+
+ builder.build().map_err(|e| format!("Failed to create HTTP client: {e}"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::normalize_changelog_lang;
+
+ #[test]
+ fn normalizes_changelog_lang() {
+ assert_eq!(normalize_changelog_lang("cn"), "cn");
+ assert_eq!(normalize_changelog_lang("zh-CN"), "cn");
+ assert_eq!(normalize_changelog_lang("zh-TW"), "cn");
+ assert_eq!(normalize_changelog_lang("en"), "en");
+ assert_eq!(normalize_changelog_lang("ja"), "en");
+ }
+}
diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs
index 407143b89a..f153fe0575 100644
--- a/crates/dbx-core/src/lib.rs
+++ b/crates/dbx-core/src/lib.rs
@@ -11,6 +11,7 @@ pub mod agent_tools;
pub mod ai;
pub mod ai_cli_agent;
pub mod ai_codex_cli;
+pub mod changelog;
pub mod cloud_sync;
pub mod connection;
pub mod connection_secrets;
diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs
index 28a615d099..502f004951 100644
--- a/crates/dbx-web/src/main.rs
+++ b/crates/dbx-web/src/main.rs
@@ -556,6 +556,7 @@ async fn main() {
// Update
.route("/version", get(routes::update::get_version))
.route("/update/check", get(routes::update::check_for_updates))
+ .route("/changelog", get(routes::update::fetch_changelog))
// Layout
.route("/layout/sidebar", post(routes::layout::save_sidebar_layout).get(routes::layout::load_sidebar_layout))
// App settings
diff --git a/crates/dbx-web/src/routes/update.rs b/crates/dbx-web/src/routes/update.rs
index e3ca0bd4ee..1a3f261201 100644
--- a/crates/dbx-web/src/routes/update.rs
+++ b/crates/dbx-web/src/routes/update.rs
@@ -1,5 +1,5 @@
use axum::{extract::Query, Json};
-use dbx_core::update;
+use dbx_core::{changelog, update};
use crate::error::AppError;
@@ -19,3 +19,17 @@ pub async fn check_for_updates(Query(params): Query) -> Resul
let info = update::build_update_info(release, env!("CARGO_PKG_VERSION"));
Ok(Json(serde_json::to_value(info).map_err(|e| AppError(e.to_string()))?))
}
+
+#[derive(serde::Deserialize)]
+pub struct ChangelogParams {
+ #[serde(default)]
+ pub lang: Option,
+}
+
+pub async fn fetch_changelog(
+ Query(params): Query,
+) -> Result, AppError> {
+ let lang = params.lang.unwrap_or_else(|| "en".to_string());
+ let data = changelog::fetch_changelog(&lang).await.map_err(AppError)?;
+ Ok(Json(data))
+}
diff --git a/packages/app-tests/changelog.test.ts b/packages/app-tests/changelog.test.ts
new file mode 100644
index 0000000000..00a9431cd2
--- /dev/null
+++ b/packages/app-tests/changelog.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "vitest";
+import { createLatestRequestGuard } from "../../apps/desktop/src/lib/app/changelog";
+
+describe("latest changelog request guard", () => {
+ test("invalidates older requests when a new load starts", () => {
+ const guard = createLatestRequestGuard();
+ const englishRequest = guard.begin();
+ const chineseRequest = guard.begin();
+
+ expect(guard.isCurrent(englishRequest)).toBe(false);
+ expect(guard.isCurrent(chineseRequest)).toBe(true);
+ });
+});
diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs
index 222e8b444b..223394daa0 100644
--- a/src-tauri/src/commands/update.rs
+++ b/src-tauri/src/commands/update.rs
@@ -118,6 +118,12 @@ pub async fn check_for_updates(locale: Option) -> Result) -> Result {
+ let lang = lang.unwrap_or_else(|| "en".to_string());
+ dbx_core::changelog::fetch_changelog(&lang).await
+}
+
#[tauri::command]
pub async fn get_system_proxy_url() -> Option {
tauri::async_runtime::spawn_blocking(dbx_core::update::system_proxy_url).await.ok().flatten()
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 6af956c05e..542af0a21d 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1235,6 +1235,7 @@ pub fn run() {
commands::mcp::check_mcp_server_status,
commands::mcp::install_mcp_server,
commands::update::check_for_updates,
+ commands::update::fetch_changelog,
commands::update::get_system_proxy_url,
commands::update::download_and_install_update,
commands::transfer::start_transfer,