Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/memos-local-openclaw/src/storage/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
25 changes: 25 additions & 0 deletions apps/memos-local-openclaw/src/viewer/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,13 @@ input,textarea,select{font-family:inherit;font-size:inherit}
<button class="btn btn-ghost" onclick="loadConfig()" data-i18n="settings.reset">Reset</button>
<button class="btn btn-primary" onclick="saveGeneralConfig()" data-i18n="settings.save">Save Settings</button>
</div>
<div class="settings-card-divider"></div>
<div class="settings-card-subtitle" data-i18n="settings.export">\u{1F4E4} Export Data</div>
<div class="field-hint" style="margin-bottom:10px" data-i18n="settings.export.hint">Download all your memories, tasks, and skills as a backup file. Choose JSON for full data or CSV for memories only.</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-ghost" onclick="exportData('json')" data-i18n="settings.export.json">\u2B07 Export JSON (full backup)</button>
<button class="btn btn-ghost" onclick="exportData('csv')" data-i18n="settings.export.csv">\u2B07 Export CSV (memories only)</button>
</div>
</div>
</div>

Expand Down Expand Up @@ -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)',
Expand Down Expand Up @@ -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':'任务自动完结(小时)',
Expand Down Expand Up @@ -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');
Expand Down
37 changes: 37 additions & 0 deletions apps/memos-local-openclaw/src/viewer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }));
Expand Down Expand Up @@ -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();
Expand Down