diff --git a/apps/memos-local-openclaw/src/storage/sqlite.ts b/apps/memos-local-openclaw/src/storage/sqlite.ts
index 09f9c2bf..31969233 100644
--- a/apps/memos-local-openclaw/src/storage/sqlite.ts
+++ b/apps/memos-local-openclaw/src/storage/sqlite.ts
@@ -2873,6 +2873,39 @@ export class SqliteStore {
return result;
}
+ // ─── Export ───
+
+ exportAll(): { memories: unknown[]; tasks: unknown[]; skills: unknown[] } {
+ const memories = this.db.prepare(
+ "SELECT * FROM chunks ORDER BY created_at ASC",
+ ).all();
+
+ const tasks = this.db.prepare(
+ "SELECT * FROM tasks ORDER BY started_at ASC",
+ ).all();
+
+ const skills = this.db.prepare(
+ "SELECT id, name, description, content, version, status, visibility, owner, created_at, updated_at FROM skills ORDER BY created_at ASC",
+ ).all();
+
+ return { memories, tasks, skills };
+ }
+
+ exportMemoriesAsCsv(): string {
+ const rows = this.db.prepare(
+ "SELECT id, session_key, role, summary, content, created_at FROM chunks ORDER BY created_at ASC",
+ ).all() as Array<{ id: string; session_key: string; role: string; summary: string; content: string; created_at: number }>;
+
+ const escape = (v: string) => `"${String(v ?? "").replace(/"/g, '""')}"`;
+ const header = ["id", "session_key", "role", "summary", "content", "created_at"].join(",");
+ const lines = rows.map((r) =>
+ [r.id, r.session_key, r.role, r.summary, r.content, new Date(r.created_at).toISOString()]
+ .map(escape)
+ .join(","),
+ );
+ return [header, ...lines].join("\n");
+ }
+
close(): void {
this.db.close();
}
diff --git a/apps/memos-local-openclaw/src/viewer/html.ts b/apps/memos-local-openclaw/src/viewer/html.ts
index 5e4456e7..3a2398d5 100644
--- a/apps/memos-local-openclaw/src/viewer/html.ts
+++ b/apps/memos-local-openclaw/src/viewer/html.ts
@@ -1837,6 +1837,13 @@ input,textarea,select{font-family:inherit;font-size:inherit}
+
+ \u{1F4E4} Export Data
+ Download all your memories, tasks, and skills as a backup file. Choose JSON for full data or CSV for memories only.
+
+
+
+
@@ -2327,6 +2334,10 @@ const I18N={
'settings.telemetry':'Telemetry',
'settings.telemetry.enabled':'Enable Anonymous Telemetry',
'settings.telemetry.hint':'Only collects tool names, latencies and version info. No memory content or personal data.',
+ 'settings.export':'Export Data',
+ 'settings.export.hint':'Download all your memories, tasks, and skills as a backup file.',
+ 'settings.export.json':'Export JSON (full backup)',
+ 'settings.export.csv':'Export CSV (memories only)',
'settings.viewerport':'Viewer Port',
'settings.viewerport.hint':'Requires restart to take effect',
'settings.taskAutoFinalize':'Task Auto-Finalize (hours)',
@@ -3100,6 +3111,10 @@ const I18N={
'settings.telemetry':'数据统计',
'settings.telemetry.enabled':'启用匿名数据统计',
'settings.telemetry.hint':'仅收集工具名称、响应时间和版本号,不涉及任何记忆内容或个人数据。',
+ 'settings.export':'导出数据',
+ 'settings.export.hint':'将所有记忆、任务和技能下载为备份文件。',
+ 'settings.export.json':'导出 JSON(完整备份)',
+ 'settings.export.csv':'导出 CSV(仅记忆)',
'settings.viewerport':'Viewer 端口',
'settings.viewerport.hint':'修改后需重启网关生效',
'settings.taskAutoFinalize':'任务自动完结(小时)',
@@ -7493,6 +7508,16 @@ async function saveHubConfig(){
}
}
+function exportData(format){
+ var url='/api/export?format='+encodeURIComponent(format);
+ var a=document.createElement('a');
+ a.href=url;
+ a.style.display='none';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+}
+
async function saveGeneralConfig(){
var card=document.querySelector('.card-general');
var saveBtn=card.querySelector('.settings-actions .btn-primary');
diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts
index 852f3eda..5ef1acef 100644
--- a/apps/memos-local-openclaw/src/viewer/server.ts
+++ b/apps/memos-local-openclaw/src/viewer/server.ts
@@ -395,6 +395,7 @@ export class ViewerServer {
else if (p === "/api/migrate/postprocess/stream" && req.method === "GET") this.handlePostprocessStream(res);
else if (p === "/api/migrate/postprocess/stop" && req.method === "POST") this.handlePostprocessStop(res);
else if (p === "/api/migrate/postprocess/status" && req.method === "GET") this.handlePostprocessStatus(res);
+ else if (p === "/api/export" && req.method === "GET") this.handleExport(res, url);
else {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
@@ -3014,6 +3015,42 @@ export class ViewerServer {
res.end(JSON.stringify({ ips }));
}
+ // ─── Export ───
+
+ private handleExport(res: http.ServerResponse, url: URL): void {
+ const format = url.searchParams.get("format") ?? "json";
+ const now = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
+
+ try {
+ if (format === "csv") {
+ const csv = this.store.exportMemoriesAsCsv();
+ const filename = `memos-memories-${now}.csv`;
+ res.writeHead(200, {
+ "Content-Type": "text/csv; charset=utf-8",
+ "Content-Disposition": `attachment; filename="${filename}"`,
+ });
+ res.end(csv);
+ } else {
+ const data = this.store.exportAll();
+ const payload = JSON.stringify(
+ { exportedAt: new Date().toISOString(), version: 1, ...data },
+ null,
+ 2,
+ );
+ const filename = `memos-export-${now}.json`;
+ res.writeHead(200, {
+ "Content-Type": "application/json",
+ "Content-Disposition": `attachment; filename="${filename}"`,
+ });
+ res.end(payload);
+ }
+ } catch (err) {
+ this.log.error(`Export failed: ${err}`);
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ error: String(err) }));
+ }
+ }
+
private serveConfig(res: http.ServerResponse): void {
try {
const cfgPath = this.getOpenClawConfigPath();