8900 commited on
Update hf-sync-manager.mjs
Browse files- hf-sync-manager.mjs +146 -232
hf-sync-manager.mjs
CHANGED
|
@@ -1,277 +1,191 @@
|
|
| 1 |
// ============================================================
|
| 2 |
-
// HF Memory Sync Manager -
|
| 3 |
//
|
| 4 |
-
//
|
| 5 |
-
// Boot: repo/settings/openclaw.json -> local disk
|
| 6 |
-
// ONLY if repo file passes JSON validation
|
| 7 |
-
// If invalid: keep local file, log warning
|
| 8 |
-
// Upload: local disk -> repo/settings/openclaw.json
|
| 9 |
-
// ONLY if local file passes JSON validation
|
| 10 |
-
// Single copy, always overwrite, never accumulate
|
| 11 |
//
|
| 12 |
-
//
|
| 13 |
-
//
|
| 14 |
-
//
|
| 15 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
// ============================================================
|
| 17 |
|
| 18 |
import fs from "node:fs";
|
| 19 |
import path from "node:path";
|
| 20 |
import { execSync } from "node:child_process";
|
| 21 |
|
| 22 |
-
// -- config ----------------------------------
|
| 23 |
-
|
| 24 |
-
var HOME
|
| 25 |
-
var OPENCLAW_DIR
|
| 26 |
-
var HF_TOKEN
|
| 27 |
-
var DATASET_ID
|
| 28 |
-
var REPO_DIR
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
var
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
];
|
| 42 |
|
|
|
|
| 43 |
var NEVER_SYNC = [
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
"config.yaml",
|
| 49 |
-
"config.json",
|
| 50 |
-
".env",
|
| 51 |
-
"node_modules",
|
| 52 |
-
".git",
|
| 53 |
-
"canvas"
|
| 54 |
];
|
| 55 |
|
| 56 |
-
// -- helpers ----------------------------------
|
| 57 |
|
| 58 |
function log(msg) {
|
| 59 |
-
|
| 60 |
}
|
| 61 |
|
| 62 |
function sleep(ms) {
|
| 63 |
-
|
| 64 |
}
|
| 65 |
|
| 66 |
function repoUrl() {
|
| 67 |
-
|
| 68 |
-
|
| 69 |
}
|
| 70 |
|
| 71 |
function git(args) {
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
}
|
| 79 |
|
| 80 |
-
function isMemoryTarget(name) {
|
| 81 |
-
var i;
|
| 82 |
-
for (i = 0; i < NEVER_SYNC.length; i++) {
|
| 83 |
-
if (name === NEVER_SYNC[i]) return false;
|
| 84 |
-
}
|
| 85 |
-
for (i = 0; i < MEMORY_TARGETS.length; i++) {
|
| 86 |
-
if (name === MEMORY_TARGETS[i]) return true;
|
| 87 |
-
}
|
| 88 |
-
return false;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
// FIX: validate JSON before any config file operation
|
| 92 |
-
// Returns true if file exists and contains valid JSON
|
| 93 |
function isValidJson(filePath) {
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
}
|
| 104 |
|
| 105 |
function copyItem(src, dst) {
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
}
|
| 113 |
|
| 114 |
-
// -- git repo setup --------------------------
|
| 115 |
|
| 116 |
function ensureRepo() {
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
}
|
| 127 |
-
);
|
| 128 |
-
} catch (e) {
|
| 129 |
-
fs.mkdirSync(REPO_DIR, { recursive: true });
|
| 130 |
-
execSync("git init " + REPO_DIR, { stdio: "pipe" });
|
| 131 |
-
git("remote add origin " + repoUrl());
|
| 132 |
-
}
|
| 133 |
-
try {
|
| 134 |
-
git("config user.email openclaw@hf.space");
|
| 135 |
-
git("config user.name OpenClaw-Memory-Sync");
|
| 136 |
-
git("config pull.rebase false");
|
| 137 |
-
} catch (e) { /* non-fatal */ }
|
| 138 |
}
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
} catch (e) {
|
| 145 |
-
try { git("pull --quiet --no-rebase origin master"); } catch (e2) { /* no remote yet */ }
|
| 146 |
-
}
|
| 147 |
}
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
if (!changed) return 0;
|
| 154 |
-
var count = changed.split("\n").filter(Boolean).length;
|
| 155 |
-
var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
|
| 156 |
-
git("commit -m \"" + label + " " + ts + "\"");
|
| 157 |
-
try {
|
| 158 |
-
git("push --quiet origin HEAD:main");
|
| 159 |
-
} catch (e) {
|
| 160 |
-
git("push --quiet origin HEAD:master");
|
| 161 |
-
}
|
| 162 |
-
return count;
|
| 163 |
-
} catch (e) {
|
| 164 |
-
log("Commit/push failed: " + e.message);
|
| 165 |
-
return 0;
|
| 166 |
-
}
|
| 167 |
}
|
| 168 |
|
| 169 |
-
|
| 170 |
-
/
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
|
| 176 |
-
|
| 177 |
-
if (!fs.existsSync(repoFile)) {
|
| 178 |
-
log("[settings] No settings in repo yet, keeping local defaults");
|
| 179 |
-
return false;
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
// FIX: validate repo file before overwriting local config
|
| 183 |
-
if (!isValidJson(repoFile)) {
|
| 184 |
-
log("[settings] WARN: repo openclaw.json is invalid JSON, skipping restore to protect local config");
|
| 185 |
-
log("[settings] Corrupted repo file will be overwritten on next upload");
|
| 186 |
-
return false;
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
try {
|
| 190 |
-
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
| 191 |
-
// Backup local before overwriting
|
| 192 |
-
if (fs.existsSync(localFile)) {
|
| 193 |
-
fs.copyFileSync(localFile, localFile + ".pre-restore-bak");
|
| 194 |
-
}
|
| 195 |
-
fs.copyFileSync(repoFile, localFile);
|
| 196 |
-
log("[settings] Boot restore done: openclaw.json <- repo (validated OK)");
|
| 197 |
-
return true;
|
| 198 |
-
} catch (e) {
|
| 199 |
-
log("[settings] Boot restore failed: " + e.message);
|
| 200 |
-
return false;
|
| 201 |
-
}
|
| 202 |
}
|
| 203 |
-
|
| 204 |
-
function settingsUpload() {
|
| 205 |
-
var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
|
| 206 |
-
|
| 207 |
-
// FIX: validate local file before uploading to repo
|
| 208 |
-
if (!isValidJson(localFile)) {
|
| 209 |
-
log("[settings] WARN: local openclaw.json is invalid JSON, skipping upload");
|
| 210 |
-
return 0;
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
var repoDir = path.join(REPO_DIR, REPO_SETTINGS);
|
| 214 |
-
var repoFile = path.join(repoDir, SETTINGS_FILE);
|
| 215 |
-
|
| 216 |
-
fs.mkdirSync(repoDir, { recursive: true });
|
| 217 |
-
// Single copy, direct overwrite - no version accumulation
|
| 218 |
-
fs.copyFileSync(localFile, repoFile);
|
| 219 |
-
log("[settings] Uploaded openclaw.json -> repo (validated OK)");
|
| 220 |
-
return 1;
|
| 221 |
}
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
var d = path.join(OPENCLAW_DIR, name);
|
| 237 |
-
try {
|
| 238 |
-
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
| 239 |
-
copyItem(s, d);
|
| 240 |
-
copied++;
|
| 241 |
-
} catch (e) {
|
| 242 |
-
log("[memory] Restore error [" + name + "]: " + e.message);
|
| 243 |
-
}
|
| 244 |
-
});
|
| 245 |
-
|
| 246 |
-
return copied;
|
| 247 |
}
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
fs.mkdirSync(dest, { recursive: true });
|
| 253 |
-
|
| 254 |
-
if (!fs.existsSync(OPENCLAW_DIR)) return 0;
|
| 255 |
-
|
| 256 |
-
var items = fs.readdirSync(OPENCLAW_DIR);
|
| 257 |
-
var copied = 0;
|
| 258 |
-
|
| 259 |
-
items.forEach(function(name) {
|
| 260 |
-
if (!isMemoryTarget(name)) return;
|
| 261 |
-
var src = path.join(OPENCLAW_DIR, name);
|
| 262 |
-
var dst = path.join(dest, name);
|
| 263 |
-
try {
|
| 264 |
-
copyItem(src, dst);
|
| 265 |
-
copied++;
|
| 266 |
-
} catch (e) {
|
| 267 |
-
log("[memory] Copy error [" + name + "]: " + e.message);
|
| 268 |
-
}
|
| 269 |
-
});
|
| 270 |
-
|
| 271 |
-
return copied;
|
| 272 |
}
|
| 273 |
-
|
| 274 |
-
function scanMemoryDisk() {
|
| 275 |
-
if (!fs.existsSync(OPENCLAW_DIR)) return [];
|
| 276 |
-
return fs.readdirSync(OPENCLAW_DIR).filter(isMemoryTarget);
|
| 277 |
}
|
|
|
|
| 1 |
// ============================================================
|
| 2 |
+
// HF Memory Sync Manager - Definitive Edition
|
| 3 |
//
|
| 4 |
+
// Based on actual OpenClaw disk layout (from official docs):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
//
|
| 6 |
+
// ~/.openclaw/openclaw.json <- gateway config
|
| 7 |
+
// ~/.openclaw/workspace/ <- agent brain (THIS is memory)
|
| 8 |
+
// AGENTS.md <- operating instructions
|
| 9 |
+
// SOUL.md <- persona / tone
|
| 10 |
+
// USER.md <- user profile
|
| 11 |
+
// IDENTITY.md <- name / emoji
|
| 12 |
+
// TOOLS.md <- env conventions
|
| 13 |
+
// HEARTBEAT.md <- periodic tasks
|
| 14 |
+
// MEMORY.md <- long-term memory (iron rules)
|
| 15 |
+
// memory/YYYY-MM-DD.md <- daily logs
|
| 16 |
+
// memory/archive/ <- logs older than 30 days
|
| 17 |
+
// checklists/ <- op checklists
|
| 18 |
+
// docs/ <- on-demand reference docs
|
| 19 |
+
// ~/.openclaw/agents/<id>/sessions/ <- DO NOT sync (session transcripts)
|
| 20 |
+
// ~/.openclaw/agents/<id>/qmd/ <- DO NOT sync (sqlite index, rebuilt on boot)
|
| 21 |
+
//
|
| 22 |
+
// Repo structure:
|
| 23 |
+
// settings/openclaw.json <- single copy, validated, overwrite
|
| 24 |
+
// workspace/ <- full workspace mirror
|
| 25 |
+
// AGENTS.md / SOUL.md / MEMORY.md / memory/ …
|
| 26 |
+
// README.md <- auto-generated index
|
| 27 |
+
//
|
| 28 |
+
// Rules:
|
| 29 |
+
// TRACK 1 - settings:
|
| 30 |
+
// Boot: repo -> local (skip if repo JSON invalid)
|
| 31 |
+
// Upload: local -> repo (skip if local JSON invalid)
|
| 32 |
+
// One file, one copy, always overwrite
|
| 33 |
+
//
|
| 34 |
+
// TRACK 2 - workspace:
|
| 35 |
+
// Boot: repo/workspace/ -> ~/.openclaw/workspace/ (full restore)
|
| 36 |
+
// Upload: ~/.openclaw/workspace/ -> repo/workspace/ every 3h
|
| 37 |
+
// Never downloads during runtime (prevents memory flooding)
|
| 38 |
+
//
|
| 39 |
+
// Multi-bot:
|
| 40 |
+
// Set OPENCLAW_PROFILE env var in HF Space Variables.
|
| 41 |
+
// OpenClaw uses ~/.openclaw/workspace-<profile>/ per profile.
|
| 42 |
+
// Repo stores each profile under workspace-<profile>/.
|
| 43 |
// ============================================================
|
| 44 |
|
| 45 |
import fs from "node:fs";
|
| 46 |
import path from "node:path";
|
| 47 |
import { execSync } from "node:child_process";
|
| 48 |
|
| 49 |
+
// -- config ----------------------------------
|
| 50 |
+
|
| 51 |
+
var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
|
| 52 |
+
var OPENCLAW_DIR = path.join(HOME, ".openclaw");
|
| 53 |
+
var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
|
| 54 |
+
var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
|
| 55 |
+
var REPO_DIR = "/tmp/hf-memory-sync";
|
| 56 |
+
|
| 57 |
+
// OPENCLAW_PROFILE: if set, workspace is ~/.openclaw/workspace-<profile>/
|
| 58 |
+
// This is OpenClaw's native multi-bot mechanism
|
| 59 |
+
var PROFILE = (process.env.OPENCLAW_PROFILE || "").trim();
|
| 60 |
+
var WORKSPACE_DIR = PROFILE
|
| 61 |
+
? path.join(OPENCLAW_DIR, "workspace-" + PROFILE)
|
| 62 |
+
: path.join(OPENCLAW_DIR, "workspace");
|
| 63 |
+
|
| 64 |
+
// Repo folder name for this profile's workspace
|
| 65 |
+
var REPO_WORKSPACE_FOLDER = PROFILE ? "workspace-" + PROFILE : "workspace";
|
| 66 |
+
|
| 67 |
+
var UPLOAD_INTERVAL = 3 * 60 * 60 * 1000; // 3 hours
|
| 68 |
+
|
| 69 |
+
// -- workspace files OpenClaw actually uses -----------------
|
| 70 |
+
var WORKSPACE_SYNC = [
|
| 71 |
+
"AGENTS.md",
|
| 72 |
+
"SOUL.md",
|
| 73 |
+
"USER.md",
|
| 74 |
+
"IDENTITY.md",
|
| 75 |
+
"TOOLS.md",
|
| 76 |
+
"HEARTBEAT.md",
|
| 77 |
+
"BOOT.md",
|
| 78 |
+
"MEMORY.md",
|
| 79 |
+
"memory", // directory: memory/YYYY-MM-DD.md + memory/archive/
|
| 80 |
+
"checklists", // directory: op checklists
|
| 81 |
+
"docs" // directory: on-demand reference docs
|
| 82 |
];
|
| 83 |
|
| 84 |
+
// Never touch these (sessions and search index are runtime-only)
|
| 85 |
var NEVER_SYNC = [
|
| 86 |
+
"agents", // ~/.openclaw/agents/<id>/sessions/ and qmd/ - runtime only
|
| 87 |
+
"canvas", // canvas UI files - runtime only
|
| 88 |
+
".git",
|
| 89 |
+
"node_modules"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
];
|
| 91 |
|
| 92 |
+
// -- helpers ----------------------------------
|
| 93 |
|
| 94 |
function log(msg) {
|
| 95 |
+
console.log("[hf-sync] " + msg);
|
| 96 |
}
|
| 97 |
|
| 98 |
function sleep(ms) {
|
| 99 |
+
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
| 100 |
}
|
| 101 |
|
| 102 |
function repoUrl() {
|
| 103 |
+
return "https://user:" + HF_TOKEN +
|
| 104 |
+
"@huggingface.co/datasets/" + DATASET_ID;
|
| 105 |
}
|
| 106 |
|
| 107 |
function git(args) {
|
| 108 |
+
return execSync("git " + args, {
|
| 109 |
+
cwd: REPO_DIR,
|
| 110 |
+
stdio: "pipe",
|
| 111 |
+
timeout: 120000,
|
| 112 |
+
env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
|
| 113 |
+
}).toString().trim();
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
function isValidJson(filePath) {
|
| 117 |
+
try {
|
| 118 |
+
if (!fs.existsSync(filePath)) return false;
|
| 119 |
+
var content = fs.readFileSync(filePath, "utf-8").trim();
|
| 120 |
+
if (!content) return false;
|
| 121 |
+
JSON.parse(content);
|
| 122 |
+
return true;
|
| 123 |
+
} catch (e) {
|
| 124 |
+
return false;
|
| 125 |
+
}
|
| 126 |
}
|
| 127 |
|
| 128 |
function copyItem(src, dst) {
|
| 129 |
+
if (fs.statSync(src).isDirectory()) {
|
| 130 |
+
execSync("cp -a " + src + " " + dst, { stdio: "pipe" });
|
| 131 |
+
} else {
|
| 132 |
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
| 133 |
+
fs.copyFileSync(src, dst);
|
| 134 |
+
}
|
| 135 |
}
|
| 136 |
|
| 137 |
+
// -- git repo setup --------------------------
|
| 138 |
|
| 139 |
function ensureRepo() {
|
| 140 |
+
if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
|
| 141 |
+
execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
|
| 142 |
+
try {
|
| 143 |
+
execSync(
|
| 144 |
+
"git clone --depth 1 " + repoUrl() + " " + REPO_DIR,
|
| 145 |
+
{
|
| 146 |
+
stdio: "pipe",
|
| 147 |
+
timeout: 120000,
|
| 148 |
+
env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
| 150 |
+
);
|
| 151 |
+
} catch (e) {
|
| 152 |
+
fs.mkdirSync(REPO_DIR, { recursive: true });
|
| 153 |
+
execSync("git init " + REPO_DIR, { stdio: "pipe" });
|
| 154 |
+
git("remote add origin " + repoUrl());
|
|
|
|
|
|
|
|
|
|
| 155 |
}
|
| 156 |
+
try {
|
| 157 |
+
git("config user.email openclaw@hf.space");
|
| 158 |
+
git("config user.name OpenClaw-Sync");
|
| 159 |
+
git("config pull.rebase false");
|
| 160 |
+
} catch (e) { /* non-fatal */ }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
|
| 163 |
+
function pullLatest() {
|
| 164 |
+
try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
|
| 165 |
+
try {
|
| 166 |
+
git("pull --quiet --no-rebase origin main");
|
| 167 |
+
} catch (e) {
|
| 168 |
+
try { git("pull --quiet --no-rebase origin master"); } catch (e2) { /* no remote yet */ }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
}
|
| 171 |
|
| 172 |
+
function commitAndPush(label) {
|
| 173 |
+
try {
|
| 174 |
+
git("add -A");
|
| 175 |
+
var changed = git("diff --cached --name-only");
|
| 176 |
+
if (!changed) return 0;
|
| 177 |
+
var count = changed.split("\n").filter(Boolean).length;
|
| 178 |
+
var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
|
| 179 |
+
var tag = PROFILE ? "[" + PROFILE + "]" : "";
|
| 180 |
+
git("commit -m \"sync" + tag + " " + ts + "\"");
|
| 181 |
+
try {
|
| 182 |
+
git("push --quiet origin HEAD:main");
|
| 183 |
+
} catch (e) {
|
| 184 |
+
git("push --quiet origin HEAD:master");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
}
|
| 186 |
+
return count;
|
| 187 |
+
} catch (e) {
|
| 188 |
+
log("Commit/push failed: " + e.message);
|
| 189 |
+
return 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
}
|