8900 commited on
Commit
cec8f4e
·
verified ·
1 Parent(s): bfb0ac8

Update hf-sync-manager.mjs

Browse files
Files changed (1) hide show
  1. hf-sync-manager.mjs +76 -299
hf-sync-manager.mjs CHANGED
@@ -1,324 +1,101 @@
1
- // ============================================================
2
- // HF Sync Manager - Full Mirror Edition
3
- //
4
- // Mirrors ~/.openclaw/ to HF Dataset every 30 min.
5
- // Boot restore: Dataset -> local (container restart only).
6
- // Gateway restart detection via /tmp flag file.
7
- //
8
- // Excluded: agents/*/sessions, agents/*/qmd, canvas, .bak, .tmp
9
- // ============================================================
10
-
11
- import fs from "node:fs";
12
  import path from "node:path";
13
  import { execSync } from "node:child_process";
14
 
15
- var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
16
- var OPENCLAW_DIR = path.join(HOME, ".openclaw");
17
- var WORKSPACE = path.join(OPENCLAW_DIR, "workspace");
18
- var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
19
- var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
20
- var REPO_DIR = "/tmp/oc-dataset";
21
- var INTERVAL = 30 * 60 * 1000;
22
- var RUNNING_FLAG = "/tmp/.hf-sync-running";
23
-
24
- var EXCLUDE = ["sessions", "qmd", "canvas", ".git"];
25
- var EXCLUDE_EXT = [".bak", ".tmp", ".log"];
26
-
27
- // –––––––––––––––––––––––––––––
28
-
29
- function log(msg) { console.log("[hf-sync] " + msg); }
30
-
31
- function sleep(ms) {
32
- return new Promise(function(resolve) { setTimeout(resolve, ms); });
33
- }
34
-
35
- function repoUrl() {
36
- return "https://user:" + HF_TOKEN +
37
- "@huggingface.co/datasets/" + DATASET_ID;
38
  }
39
 
40
- function git(args) {
41
- return execSync("git " + args, {
42
- cwd: REPO_DIR,
43
- stdio: "pipe",
44
- timeout: 120000,
45
- env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
46
- }).toString().trim();
47
  }
48
 
49
- function shouldExclude(name) {
50
- var i;
51
- for (i = 0; i < EXCLUDE.length; i++) {
52
- if (name === EXCLUDE[i]) return true;
53
- }
54
- for (i = 0; i < EXCLUDE_EXT.length; i++) {
55
- if (name.indexOf(EXCLUDE_EXT[i]) >= 0) return true;
56
- }
57
- return false;
58
- }
59
-
60
- // –––––––––––––––––––––––––––––
61
-
62
- function ensureRepo() {
63
- if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
64
- execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
65
- try {
66
- execSync("git clone --depth 1 " + repoUrl() + " " + REPO_DIR, {
67
- stdio: "pipe", timeout: 120000,
68
- env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
69
- });
70
- log("Repo cloned");
71
- } catch (e) {
72
- log("Clone failed, init empty: " + e.message);
73
- fs.mkdirSync(REPO_DIR, { recursive: true });
74
- execSync("git init " + REPO_DIR, { stdio: "pipe" });
75
- git("remote add origin " + repoUrl());
76
- }
77
- try {
78
- git("config user.email openclaw@hf.space");
79
- git("config user.name OpenClaw-Sync");
80
- git("config pull.rebase false");
81
- } catch (e) { /* non-fatal */ }
82
- }
83
-
84
- function pull() {
85
- try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
86
- try { git("pull --quiet --no-rebase origin main"); }
87
- catch (e) {
88
- try { git("pull --quiet --no-rebase origin master"); }
89
- catch (e2) { /* empty repo */ }
90
- }
91
- }
92
-
93
- function push() {
94
- try {
95
  git("add -A");
96
  var changed = git("diff --cached --name-only");
97
- if (!changed) return 0;
98
- var n = changed.split("\n").filter(Boolean).length;
99
- var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
100
- git('commit -m "sync ' + ts + '"');
101
- try { git("push --quiet origin HEAD:main"); }
102
- catch (e) { git("push --quiet origin HEAD:master"); }
103
- return n;
104
- } catch (e) {
105
- log("Push failed: " + e.message);
106
- return 0;
107
- }
108
  }
109
 
110
- // –––––––––––––––––––––––––––––
 
111
 
112
- function mirrorDirToRepo(srcDir, dstDir) {
113
- if (!fs.existsSync(srcDir)) return 0;
114
- fs.mkdirSync(dstDir, { recursive: true });
115
- var count = 0;
116
- fs.readdirSync(srcDir).forEach(function(name) {
117
- if (shouldExclude(name)) return;
118
- var src = path.join(srcDir, name);
119
- var dst = path.join(dstDir, name);
120
- try {
121
- if (fs.statSync(src).isDirectory()) {
122
- count += mirrorDirToRepo(src, dst);
123
- } else {
124
- fs.copyFileSync(src, dst);
125
- count++;
126
- }
127
- } catch (e) {
128
- log("Copy error [" + name + "]: " + e.message);
129
- }
130
- });
131
- return count;
132
- }
133
 
134
- function mirrorDirFromRepo(srcDir, dstDir) {
135
- if (!fs.existsSync(srcDir)) return 0;
136
- fs.mkdirSync(dstDir, { recursive: true });
137
- var count = 0;
138
- fs.readdirSync(srcDir).forEach(function(name) {
139
- var src = path.join(srcDir, name);
140
- var dst = path.join(dstDir, name);
141
- try {
142
- if (fs.statSync(src).isDirectory()) {
143
- count += mirrorDirFromRepo(src, dst);
144
- } else {
145
- fs.copyFileSync(src, dst);
146
- count++;
147
- }
148
- } catch (e) {
149
- log("Restore error [" + name + "]: " + e.message);
150
- }
151
- });
152
- return count;
153
  }
154
 
155
- // –––––––––––––––––––––––––––––
156
-
157
- function seedWorkspaceIfNeeded() {
158
- fs.mkdirSync(WORKSPACE, { recursive: true });
159
- fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true });
160
-
161
- var soul = path.join(WORKSPACE, "SOUL.md");
162
- if (!fs.existsSync(soul)) {
163
- fs.writeFileSync(soul, [
164
- "# Soul",
165
- "",
166
- "You are a helpful, warm, concise AI assistant.",
167
- "",
168
- "## Language",
169
- "",
170
- "Default language: Simplified Chinese.",
171
- "Always reply in Chinese unless the user writes in another language first.",
172
- "",
173
- "## Tone",
174
- "",
175
- "- Natural and friendly, not overly formal",
176
- "- Concise and to the point",
177
- "- Ask one clarifying question at a time when needed"
178
- ].join("\n") + "\n", "utf-8");
179
- log("Seeded workspace/SOUL.md");
180
- }
181
-
182
- var agents = path.join(WORKSPACE, "AGENTS.md");
183
- if (!fs.existsSync(agents)) {
184
- fs.writeFileSync(agents, [
185
- "# Agent Instructions",
186
- "",
187
- "## Boot sequence",
188
- "",
189
- "1. Read SOUL.md - language and persona",
190
- "2. Read USER.md if present - user profile",
191
- "3. Read MEMORY.md - long-term facts and rules",
192
- "4. Read today and yesterday memory/YYYY-MM-DD.md if present",
193
- "",
194
- "## Memory rules",
195
- "",
196
- "- Write important facts to MEMORY.md when asked",
197
- "- Log daily context to memory/YYYY-MM-DD.md",
198
- "- Do not ask for information already provided"
199
- ].join("\n") + "\n", "utf-8");
200
- log("Seeded workspace/AGENTS.md");
201
- }
202
-
203
- var mem = path.join(WORKSPACE, "MEMORY.md");
204
- if (!fs.existsSync(mem)) {
205
- fs.writeFileSync(mem, [
206
- "# Long-term Memory",
207
- "",
208
- "<!-- OpenClaw writes important facts here. -->",
209
- "<!-- Loaded at the start of every session. -->"
210
- ].join("\n") + "\n", "utf-8");
211
- log("Seeded workspace/MEMORY.md");
212
- }
213
  }
214
 
215
- // –––––––––––––––––––––––––––––
216
-
217
- function bootRestore() {
218
- if (!HF_TOKEN || !DATASET_ID) {
219
- log("No HF_TOKEN/HF_DATASET_ID - skipping Dataset restore");
220
- seedWorkspaceIfNeeded();
221
- return;
222
- }
223
-
224
- log("Boot restore from " + DATASET_ID + "…");
225
- try {
226
- ensureRepo();
227
- pull();
228
- } catch (e) {
229
- log("Repo init failed (" + e.message + ") - starting fresh");
230
- seedWorkspaceIfNeeded();
231
- return;
232
- }
233
-
234
- var repoOC = path.join(REPO_DIR, "openclaw");
235
- if (fs.existsSync(repoOC)) {
236
- var n = mirrorDirFromRepo(repoOC, OPENCLAW_DIR);
237
- log("Boot restore done: " + n + " file(s) -> " + OPENCLAW_DIR);
238
- } else {
239
- log("No data in Dataset yet - starting fresh");
240
- }
241
-
242
- seedWorkspaceIfNeeded();
243
- log("Boot restore complete");
244
  }
245
 
246
- function runSync() {
247
- if (!HF_TOKEN || !DATASET_ID) return;
248
-
249
- var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
250
- log("Sync at " + ts + "…");
251
-
252
- try {
253
- ensureRepo();
254
- pull();
255
-
256
- var repoOC = path.join(REPO_DIR, "openclaw");
257
- if (fs.existsSync(repoOC)) {
258
- execSync("rm -rf " + repoOC, { stdio: "pipe" });
259
- }
260
- fs.mkdirSync(repoOC, { recursive: true });
261
-
262
- var n = mirrorDirToRepo(OPENCLAW_DIR, repoOC);
263
- log("Mirrored " + n + " file(s)");
264
-
265
- var pushed = push();
266
- if (pushed > 0) {
267
- log("Sync done: " + pushed + " file(s) pushed");
268
- } else {
269
- log("Sync done: no changes");
270
- }
271
- } catch (e) {
272
- log("Sync failed (retry in 30 min): " + e.message);
273
  }
274
  }
275
 
276
- // –––––––––––––––––––––––––––––
277
- // Main - use IIFE to avoid any hoisting issues
278
- // –––––––––––––––––––––––––––––
279
-
280
- (async function() {
281
- log("Starting…");
282
- log("OPENCLAW_DIR : " + OPENCLAW_DIR);
283
- log("DATASET : " + (DATASET_ID || "NOT SET"));
284
-
285
- fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
286
- fs.mkdirSync(WORKSPACE, { recursive: true });
287
-
288
- try {
289
- execSync("git config --global user.email openclaw@hf.space", { stdio: "pipe" });
290
- execSync("git config --global user.name OpenClaw-Sync", { stdio: "pipe" });
291
- execSync("git config --global http.postBuffer 52428800", { stdio: "pipe" });
292
- execSync("git config --global pull.rebase false", { stdio: "pipe" });
293
- } catch (e) { /* non-fatal */ }
294
-
295
- var isGatewayRestart = fs.existsSync(RUNNING_FLAG);
296
-
297
- if (isGatewayRestart) {
298
- log("Gateway restart - skipping boot restore (data on disk)");
299
- } else {
300
- log("Container start - running full boot restore");
301
- bootRestore();
302
  }
303
 
304
- fs.writeFileSync(RUNNING_FLAG, String(process.pid), "utf-8");
305
-
306
- process.on("exit", function() { try { fs.unlinkSync(RUNNING_FLAG); } catch (e) { /* ok */ } });
307
- process.on("SIGTERM", function() { try { fs.unlinkSync(RUNNING_FLAG); } catch (e) { /* ok */ } process.exit(0); });
308
- process.on("SIGINT", function() { try { fs.unlinkSync(RUNNING_FLAG); } catch (e) { /* ok */ } process.exit(0); });
309
-
310
- if (!HF_TOKEN || !DATASET_ID) {
311
- log("Sync disabled: set HF_TOKEN and HF_DATASET_ID in Secrets");
312
- return;
313
- }
314
-
315
- log("Sync loop starting (first run in 10 min)…");
316
- await sleep(10 * 60 * 1000);
317
 
318
- while (true) {
319
- runSync();
320
- await sleep(INTERVAL);
321
- }
322
- })().catch(function(e) {
323
- console.error("[hf-sync] Fatal: " + e.message);
324
- });
 
1
+ import fs from "node:fs";
 
 
 
 
 
 
 
 
 
 
2
  import path from "node:path";
3
  import { execSync } from "node:child_process";
4
 
5
+ var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
6
+ var OPENCLAW_DIR = path.join(HOME,".openclaw");
7
+ var MEMORY_DIR = path.join(OPENCLAW_DIR,"dataset-memory");
8
+ var HF_TOKEN = (process.env.HF_TOKEN||"").trim();
9
+ var DATASET_ID = (process.env.HF_DATASET_ID||"").trim();
10
+ var REPO_DIR = "/tmp/hf-memory-sync";
11
+ var UPLOAD_INTERVAL = 3*60*60*1000;
12
+
13
+ var TRACK_NAMES = ["conversations","memory","logs"];
14
+ var botMemory = { conversations:[], memory:[], logs:[] };
15
+
16
+ function log(msg){ console.log("[hf-sync] "+msg); }
17
+ function repoUrl(){ return "https://user:"+HF_TOKEN+"@huggingface.co/datasets/"+DATASET_ID; }
18
+ function git(args,cwd){ return execSync("git "+args,{ cwd:cwd||REPO_DIR, stdio:"pipe", timeout:120000, env:Object.assign({},process.env,{GIT_TERMINAL_PROMPT:"0"}) }).toString().trim(); }
19
+ function sleep(ms){ return new Promise(resolve=>setTimeout(resolve,ms)); }
20
+
21
+ function ensureRepo(){
22
+ if(fs.existsSync(path.join(REPO_DIR,".git"))) return;
23
+ execSync("rm -rf "+REPO_DIR,{stdio:"pipe"});
24
+ try{ git("clone --depth 1 "+repoUrl()+" "+REPO_DIR,"/tmp"); } catch(e){ git("init",REPO_DIR); git("remote add origin "+repoUrl(),REPO_DIR); }
25
+ try{ git("config user.email openclaw@hf.space"); git("config user.name OpenClaw-Memory-Sync"); git("config pull.rebase false"); } catch(e){ }
 
 
26
  }
27
 
28
+ function pullLatest(){
29
+ try{ git("fetch --quiet origin"); git("pull --quiet --no-rebase origin main"); }
30
+ catch(e){ try{ git("pull --quiet --no-rebase origin master"); } catch(e2){ } }
 
 
 
 
31
  }
32
 
33
+ function commitAndPush(){
34
+ try{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  git("add -A");
36
  var changed = git("diff --cached --name-only");
37
+ if(!changed) return 0;
38
+ var ts = new Date().toISOString().replace("T"," ").substring(0,19);
39
+ var count = changed.split("\n").filter(Boolean).length;
40
+ git('commit -m "memory-sync '+ts+'"');
41
+ try{ git("push --quiet origin HEAD:main"); } catch(e){ git("push --quiet origin HEAD:master"); }
42
+ return count;
43
+ }catch(e){ log("Push failed: "+e.message); return 0; }
 
 
 
 
44
  }
45
 
46
+ function readJsonl(filePath,targetArray){ if(!fs.existsSync(filePath)) return; fs.readFileSync(filePath,"utf-8").split("\n").filter(Boolean).forEach(line=>{ try{ targetArray.push(JSON.parse(line)) }catch(e){} }); }
47
+ function writeJsonl(filePath,dataArray){ fs.writeFileSync(filePath,dataArray.map(x=>JSON.stringify(x)).join("\n"),"utf-8"); }
48
 
49
+ function todayDir(){ var d=new Date(); return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); }
50
+ function getRepoDateDir(dateStr){ var dir=path.join(REPO_DIR,"dataset-memory",dateStr); fs.mkdirSync(dir,{recursive:true}); return dir; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ function loadMemoryFromRepo(){
53
+ var repoBase = path.join(REPO_DIR,"dataset-memory");
54
+ if(!fs.existsSync(repoBase)) return;
55
+ var dateDirs = fs.readdirSync(repoBase).filter(f=>fs.statSync(path.join(repoBase,f)).isDirectory());
56
+ dateDirs.forEach(dateDir=>{ TRACK_NAMES.forEach(track=>{ readJsonl(path.join(repoBase,dateDir,track+".jsonl"),botMemory[track]); }); });
57
+ log("Loaded: conversations="+botMemory.conversations.length+" memory="+botMemory.memory.length+" logs="+botMemory.logs.length);
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
+ function saveMemoryToRepo(){
61
+ var dateStr=todayDir(); var destDir=getRepoDateDir(dateStr); TRACK_NAMES.forEach(track=>writeJsonl(path.join(destDir,track+".jsonl"),botMemory[track]));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
 
64
+ function bootRestore(){
65
+ if(!HF_TOKEN || !DATASET_ID){ log("HF_TOKEN or HF_DATASET_ID not set, skipping restore"); return; }
66
+ log("Boot restore starting from "+DATASET_ID+"...");
67
+ try{ ensureRepo(); pullLatest(); loadMemoryFromRepo(); log("Boot restore done."); }catch(e){ log("Boot restore failed (non-fatal): "+e.message); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
+ async function startUploadLoop(){
71
+ while(true){
72
+ await sleep(UPLOAD_INTERVAL);
73
+ try{ log("Uploading memory to repo..."); ensureRepo(); pullLatest(); saveMemoryToRepo(); var count=commitAndPush(); log("Upload done: "+count+" file(s) changed"); }catch(e){ log("Upload error: "+e.message); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
  }
76
 
77
+ async function main(){
78
+ log("Starting...");
79
+ log("OPENCLAW_DIR : "+OPENCLAW_DIR);
80
+ log("HF_DATASET_ID: "+(DATASET_ID||"NOT SET"));
81
+ log("Upload interval: "+(UPLOAD_INTERVAL/3600000)+"h");
82
+
83
+ fs.mkdirSync(MEMORY_DIR,{recursive:true});
84
+ try{
85
+ execSync("git config --global user.email openclaw@hf.space",{stdio:"pipe"});
86
+ execSync("git config --global user.name OpenClaw-Memory-Sync",{stdio:"pipe"});
87
+ execSync("git config --global http.postBuffer 52428800",{stdio:"pipe"});
88
+ execSync("git config --global pull.rebase false",{stdio:"pipe"});
89
+ }catch(e){}
90
+
91
+ bootRestore();
92
+
93
+ if(HF_TOKEN && DATASET_ID){
94
+ log("Upload loop started (first run in "+(UPLOAD_INTERVAL/3600000)+"h)");
95
+ startUploadLoop().catch(e=>{ log("Upload loop error: "+e.message); });
 
 
 
 
 
 
 
96
  }
97
 
98
+ log("Ready.");
99
+ }
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ main().catch(e=>{ console.error("[hf-sync] Fatal: "+e.message); });