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.

Files changed (1) hide show
  1. index.ts +63 -18
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 f = join("/app", "customers.json");
1268
  let data: any = {};
1269
- if (existsSync(f)) { try { data = JSON.parse(readFileSync(f, "utf-8")); } catch (_e) { data = {}; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
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 f = join("/app", "customers.json");
 
1303
  let data: any = {};
1304
- if (existsSync(f)) { try { data = JSON.parse(readFileSync(f, "utf-8")) || {}; } catch (_e) { data = {}; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
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 repo so the Zalo bot/KETOAN see it (best-effort).
 
 
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: "space", id: "bep40/vai-avatar2" } } },
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/spaces/bep40/vai-avatar2/commit/main", {
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 repo HEAD so edits from any writer (Zalo bot upload, new
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/spaces/bep40/vai-avatar2/raw/main/customers.json", {
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
- if (existsSync(f)) { try { data = JSON.parse(readFileSync(f, "utf-8")); } catch (_e) { data = {}; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: "space", id: "bep40/vai-avatar2" } } },
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/spaces/bep40/vai-avatar2/commit/main", {
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 without this, the live GET keeps returning the
1421
- // stale /app/customers.json until the Space rebuilds (rebuilds are slow
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) {