Spaces:
Running
Running
ML Intern commited on
Commit ·
f2f1ae7
1
Parent(s): 72ca41a
fix(web): persist customers/CK to vaistudio-data dataset — no more Space rebuild on every save
Browse files- /api/customers GET+POST and /api/kh-link now read/write
bep40/vaistudio-data/customers.json (durable DATASET) instead of committing
customers.json to the vai-avatar2 Space repo.
- Old behavior: every customer/CK save committed to the Space repo → triggered a
Space rebuild, and concurrent writers (Zalo bot, admin saves) raced and
clobbered each other's records (the recurring NBV2fc/CK-loss bug).
- New: dataset commit = no rebuild, safe merge from live dataset on every write.
index.ts
CHANGED
|
@@ -1262,11 +1262,26 @@ const server = Bun.serve({
|
|
| 1262 |
return Response.json({ ok: false, expired: !!expired, error: expired ? "link hết hạn" : "sai token" }, { status: 200 });
|
| 1263 |
}
|
| 1264 |
// Load customer ck (server-side, own data) so the client can apply it.
|
|
|
|
|
|
|
| 1265 |
let ck: any = null; let maKh = kh; let custName = "";
|
| 1266 |
try {
|
| 1267 |
-
const
|
| 1268 |
let data: any = {};
|
| 1269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1270 |
if (data && typeof data === "object") {
|
| 1271 |
for (const k in data) {
|
| 1272 |
const c = data[k];
|
|
@@ -1299,9 +1314,23 @@ const server = Bun.serve({
|
|
| 1299 |
const cid = (url.searchParams.get("cid") || "").trim().slice(0, 40);
|
| 1300 |
const name = (url.searchParams.get("name") || "").trim().slice(0, 60);
|
| 1301 |
if (!cid) return Response.json({ ok: false, error: "missing cid" }, { status: 400 });
|
| 1302 |
-
const
|
|
|
|
| 1303 |
let data: any = {};
|
| 1304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
if (!data || typeof data !== "object") data = {};
|
| 1306 |
let rec: any = null;
|
| 1307 |
for (const k in data) {
|
|
@@ -1317,15 +1346,16 @@ const server = Bun.serve({
|
|
| 1317 |
data[ma_kh] = rec;
|
| 1318 |
// Persist locally (best-effort; mirrors the bot's own persistence).
|
| 1319 |
try { writeFileSync(f, JSON.stringify(data, null, 2), "utf-8"); } catch (_e) {}
|
| 1320 |
-
// Also push to the
|
|
|
|
|
|
|
| 1321 |
try {
|
| 1322 |
-
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1323 |
if (token) {
|
| 1324 |
const payload = [
|
| 1325 |
-
{ key: "header", value: { summary: "Add web visitor " + ma_kh + " via /api/kh-link", repo: { type: "
|
| 1326 |
{ key: "file", value: { path: "customers.json", content: JSON.stringify(data, null, 2) } },
|
| 1327 |
].map((o) => JSON.stringify(o)).join("\n") + "\n";
|
| 1328 |
-
await fetch("https://huggingface.co/api/
|
| 1329 |
method: "POST", headers: { "Content-Type": "application/x-ndjson", Authorization: "Bearer " + token },
|
| 1330 |
body: payload, signal: AbortSignal.timeout(15000),
|
| 1331 |
}).catch(() => {});
|
|
@@ -1350,11 +1380,11 @@ const server = Bun.serve({
|
|
| 1350 |
try {
|
| 1351 |
const f = join("/app", "customers.json");
|
| 1352 |
let data: any = {};
|
| 1353 |
-
// Prefer LIVE
|
| 1354 |
// admin saves) appear immediately without waiting for a rebuild.
|
| 1355 |
try {
|
| 1356 |
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1357 |
-
const r = await fetch("https://huggingface.co/
|
| 1358 |
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1359 |
signal: AbortSignal.timeout(6000),
|
| 1360 |
});
|
|
@@ -1388,7 +1418,22 @@ const server = Bun.serve({
|
|
| 1388 |
|
| 1389 |
const f = join("/app", "customers.json");
|
| 1390 |
let data: any = {};
|
| 1391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1392 |
if (!data || typeof data !== "object") data = {};
|
| 1393 |
|
| 1394 |
let changed = 0;
|
|
@@ -1401,14 +1446,16 @@ const server = Bun.serve({
|
|
| 1401 |
if (!changed) return Response.json({ error: "No valid customers" }, { status: 400 });
|
| 1402 |
|
| 1403 |
const content = JSON.stringify(data, null, 2);
|
| 1404 |
-
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1405 |
if (!token) return Response.json({ error: "No write token on server" }, { status: 500 });
|
| 1406 |
|
|
|
|
|
|
|
|
|
|
| 1407 |
const payload = [
|
| 1408 |
-
{ key: "header", value: { summary: "Update customers via api", repo: { type: "
|
| 1409 |
{ key: "file", value: { path: "customers.json", content } },
|
| 1410 |
].map((x) => JSON.stringify(x)).join("\n");
|
| 1411 |
-
const commit = await fetch("https://huggingface.co/api/
|
| 1412 |
method: "POST",
|
| 1413 |
headers: { Authorization: "Bearer " + token, "Content-Type": "application/x-ndjson" },
|
| 1414 |
body: payload,
|
|
@@ -1417,10 +1464,8 @@ const server = Bun.serve({
|
|
| 1417 |
const errText = await commit.text().catch(() => "");
|
| 1418 |
return Response.json({ error: "Commit failed: HTTP " + commit.status + " " + errText.slice(0, 300) }, { status: 502 });
|
| 1419 |
}
|
| 1420 |
-
// Persist locally too
|
| 1421 |
-
//
|
| 1422 |
-
// and a write shouldn't depend on one). Keep local file in sync so
|
| 1423 |
-
// subsequent GET /api/customers reflect the save immediately.
|
| 1424 |
try { writeFileSync(f, content); } catch (_e) {}
|
| 1425 |
return Response.json({ ok: true, updated: changed, customers: data });
|
| 1426 |
} catch (e: any) {
|
|
|
|
| 1262 |
return Response.json({ ok: false, expired: !!expired, error: expired ? "link hết hạn" : "sai token" }, { status: 200 });
|
| 1263 |
}
|
| 1264 |
// Load customer ck (server-side, own data) so the client can apply it.
|
| 1265 |
+
// Source of truth = bep40/vaistudio-data/customers.json (durable dataset,
|
| 1266 |
+
// no Space rebuild on write).
|
| 1267 |
let ck: any = null; let maKh = kh; let custName = "";
|
| 1268 |
try {
|
| 1269 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1270 |
let data: any = {};
|
| 1271 |
+
try {
|
| 1272 |
+
const r = await fetch("https://huggingface.co/datasets/bep40/vaistudio-data/resolve/main/customers.json", {
|
| 1273 |
+
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1274 |
+
signal: AbortSignal.timeout(6000),
|
| 1275 |
+
});
|
| 1276 |
+
if (r.ok) {
|
| 1277 |
+
const raw = await r.text();
|
| 1278 |
+
if (raw && raw.trim()) { try { data = JSON.parse(raw); } catch (_e) {} }
|
| 1279 |
+
}
|
| 1280 |
+
} catch (_e) {}
|
| 1281 |
+
if (!data || typeof data !== "object" || !Object.keys(data).length) {
|
| 1282 |
+
const f = join("/app", "customers.json");
|
| 1283 |
+
if (existsSync(f)) { try { data = JSON.parse(readFileSync(f, "utf-8")); } catch (_e2) { data = {}; } }
|
| 1284 |
+
}
|
| 1285 |
if (data && typeof data === "object") {
|
| 1286 |
for (const k in data) {
|
| 1287 |
const c = data[k];
|
|
|
|
| 1314 |
const cid = (url.searchParams.get("cid") || "").trim().slice(0, 40);
|
| 1315 |
const name = (url.searchParams.get("name") || "").trim().slice(0, 60);
|
| 1316 |
if (!cid) return Response.json({ ok: false, error: "missing cid" }, { status: 400 });
|
| 1317 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1318 |
+
// Source of truth = bep40/vaistudio-data/customers.json (dataset).
|
| 1319 |
let data: any = {};
|
| 1320 |
+
try {
|
| 1321 |
+
const r = await fetch("https://huggingface.co/datasets/bep40/vaistudio-data/resolve/main/customers.json", {
|
| 1322 |
+
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1323 |
+
signal: AbortSignal.timeout(6000),
|
| 1324 |
+
});
|
| 1325 |
+
if (r.ok) {
|
| 1326 |
+
const raw = await r.text();
|
| 1327 |
+
if (raw && raw.trim()) { try { data = JSON.parse(raw); } catch (_e) {} }
|
| 1328 |
+
}
|
| 1329 |
+
} catch (_e) {}
|
| 1330 |
+
const f = join("/app", "customers.json");
|
| 1331 |
+
if (!data || typeof data !== "object" || !Object.keys(data).length) {
|
| 1332 |
+
if (existsSync(f)) { try { data = JSON.parse(readFileSync(f, "utf-8")) || {}; } catch (_e2) { data = {}; } }
|
| 1333 |
+
}
|
| 1334 |
if (!data || typeof data !== "object") data = {};
|
| 1335 |
let rec: any = null;
|
| 1336 |
for (const k in data) {
|
|
|
|
| 1346 |
data[ma_kh] = rec;
|
| 1347 |
// Persist locally (best-effort; mirrors the bot's own persistence).
|
| 1348 |
try { writeFileSync(f, JSON.stringify(data, null, 2), "utf-8"); } catch (_e) {}
|
| 1349 |
+
// Also push to the durable DATASET so the Zalo bot/KETOAN see it —
|
| 1350 |
+
// dataset commits do NOT trigger a Space rebuild (the old commit to
|
| 1351 |
+
// the vai-avatar2 Space repo rebuilt the Space on every new visitor).
|
| 1352 |
try {
|
|
|
|
| 1353 |
if (token) {
|
| 1354 |
const payload = [
|
| 1355 |
+
{ key: "header", value: { summary: "Add web visitor " + ma_kh + " via /api/kh-link", repo: { type: "dataset", id: "bep40/vaistudio-data" } } },
|
| 1356 |
{ key: "file", value: { path: "customers.json", content: JSON.stringify(data, null, 2) } },
|
| 1357 |
].map((o) => JSON.stringify(o)).join("\n") + "\n";
|
| 1358 |
+
await fetch("https://huggingface.co/api/datasets/bep40/vaistudio-data/commit/main", {
|
| 1359 |
method: "POST", headers: { "Content-Type": "application/x-ndjson", Authorization: "Bearer " + token },
|
| 1360 |
body: payload, signal: AbortSignal.timeout(15000),
|
| 1361 |
}).catch(() => {});
|
|
|
|
| 1380 |
try {
|
| 1381 |
const f = join("/app", "customers.json");
|
| 1382 |
let data: any = {};
|
| 1383 |
+
// Prefer the LIVE durable dataset so edits from ANY writer (Zalo bot,
|
| 1384 |
// admin saves) appear immediately without waiting for a rebuild.
|
| 1385 |
try {
|
| 1386 |
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1387 |
+
const r = await fetch("https://huggingface.co/datasets/bep40/vaistudio-data/resolve/main/customers.json", {
|
| 1388 |
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1389 |
signal: AbortSignal.timeout(6000),
|
| 1390 |
});
|
|
|
|
| 1418 |
|
| 1419 |
const f = join("/app", "customers.json");
|
| 1420 |
let data: any = {};
|
| 1421 |
+
// Merge base = LIVE durable dataset (not the possibly-stale local file),
|
| 1422 |
+
// so concurrent writes from the bot / other sessions are preserved.
|
| 1423 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1424 |
+
try {
|
| 1425 |
+
const r = await fetch("https://huggingface.co/datasets/bep40/vaistudio-data/resolve/main/customers.json", {
|
| 1426 |
+
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1427 |
+
signal: AbortSignal.timeout(6000),
|
| 1428 |
+
});
|
| 1429 |
+
if (r.ok) {
|
| 1430 |
+
const raw = await r.text();
|
| 1431 |
+
if (raw && raw.trim()) { try { data = JSON.parse(raw); } catch (_e) {} }
|
| 1432 |
+
}
|
| 1433 |
+
} catch (_e) {}
|
| 1434 |
+
if (!data || typeof data !== "object" || !Object.keys(data).length) {
|
| 1435 |
+
if (existsSync(f)) { try { data = JSON.parse(readFileSync(f, "utf-8")); } catch (_e) { data = {}; } }
|
| 1436 |
+
}
|
| 1437 |
if (!data || typeof data !== "object") data = {};
|
| 1438 |
|
| 1439 |
let changed = 0;
|
|
|
|
| 1446 |
if (!changed) return Response.json({ error: "No valid customers" }, { status: 400 });
|
| 1447 |
|
| 1448 |
const content = JSON.stringify(data, null, 2);
|
|
|
|
| 1449 |
if (!token) return Response.json({ error: "No write token on server" }, { status: 500 });
|
| 1450 |
|
| 1451 |
+
// Commit to the durable DATASET — NOT the Space repo — so saving a
|
| 1452 |
+
// customer / CK never triggers a vai-avatar2 rebuild (the old commit
|
| 1453 |
+
// rebuilt the Space on every save and concurrent writers lost data).
|
| 1454 |
const payload = [
|
| 1455 |
+
{ key: "header", value: { summary: "Update customers via api", repo: { type: "dataset", id: "bep40/vaistudio-data" } } },
|
| 1456 |
{ key: "file", value: { path: "customers.json", content } },
|
| 1457 |
].map((x) => JSON.stringify(x)).join("\n");
|
| 1458 |
+
const commit = await fetch("https://huggingface.co/api/datasets/bep40/vaistudio-data/commit/main", {
|
| 1459 |
method: "POST",
|
| 1460 |
headers: { Authorization: "Bearer " + token, "Content-Type": "application/x-ndjson" },
|
| 1461 |
body: payload,
|
|
|
|
| 1464 |
const errText = await commit.text().catch(() => "");
|
| 1465 |
return Response.json({ error: "Commit failed: HTTP " + commit.status + " " + errText.slice(0, 300) }, { status: 502 });
|
| 1466 |
}
|
| 1467 |
+
// Persist locally too so the live GET keeps reflecting the save
|
| 1468 |
+
// immediately (no rebuild dependency).
|
|
|
|
|
|
|
| 1469 |
try { writeFileSync(f, content); } catch (_e) {}
|
| 1470 |
return Response.json({ ok: true, updated: changed, customers: data });
|
| 1471 |
} catch (e: any) {
|