ML Intern commited on
Commit
ee1a7da
·
1 Parent(s): 12abe10

Triệt để: web LUÔN có mã KH + link chiết khấu — /api/kh-link mint mã KH + signed link cho khách vào thẳng web; initDiscountLink fallback qua kh-link + lưu vas_signed_kh; greeting/mọi reply hiện header

Browse files
Files changed (3) hide show
  1. index.html +1 -1
  2. index.ts +60 -0
  3. src/app.js +62 -20
index.html CHANGED
@@ -553,7 +553,7 @@
553
  <script src="./src/vaix-rag.js?gc=46"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
554
  <script src="./src/greeting-news.js?gc=22"></script> <!-- Greeting HOT news cards + source links -->
555
  <script src="./src/order-sync.js?gc=14"></script> <!-- Order sync with V.AISTUDIO backend -->
556
- <script type="module" src="./src/app.js?gc=51"></script>
557
  <script src="./src/greeting-source-cards.js?gc=14"></script> <!-- FIX: Nguồn tin cards under greeting -->
558
  <script src="./src/avatar-picker.js?gc=12"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
559
 
 
553
  <script src="./src/vaix-rag.js?gc=46"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
554
  <script src="./src/greeting-news.js?gc=22"></script> <!-- Greeting HOT news cards + source links -->
555
  <script src="./src/order-sync.js?gc=14"></script> <!-- Order sync with V.AISTUDIO backend -->
556
+ <script type="module" src="./src/app.js?gc=52"></script>
557
  <script src="./src/greeting-source-cards.js?gc=14"></script> <!-- FIX: Nguồn tin cards under greeting -->
558
  <script src="./src/avatar-picker.js?gc=12"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
559
 
index.ts CHANGED
@@ -1285,6 +1285,66 @@ const server = Bun.serve({
1285
  }
1286
  },
1287
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1288
  "/api/customers": {
1289
  GET: async () => {
1290
  try {
 
1285
  }
1286
  },
1287
  },
1288
+ // ── Issue a mã KH + fresh time-limited discount link for the web visitor ──
1289
+ // The Zalo bot only sends ?kh links in Zalo chat. Web visitors who open the
1290
+ // site directly (no ?kh in URL) still need to SEE their mã KH + a valid
1291
+ // chiết khấu link on every greeting / non-product reply. The client sends a
1292
+ // stable visitor id (localStorage vas_cid) + display name; the server mints
1293
+ // a customer code (persisted to customers.json) and a signed link — so the
1294
+ // greeting always has a working link, not a dead one.
1295
+ "/api/kh-link": {
1296
+ GET: async (req: Request) => {
1297
+ try {
1298
+ const url = new URL(req.url);
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) {
1308
+ const c = data[k];
1309
+ if (c && String(c.cid || "").trim() === cid) { rec = c; break; }
1310
+ }
1311
+ if (!rec) {
1312
+ // Mint a new code: initials + last 3 of cid (mirror bot's rule), fallback "X".
1313
+ const base = (name || "KH").replace(/[^A-Za-zÀ-ỹ\s]/g, " ").trim().split(/\s+/)
1314
+ .map((w: string) => w.charAt(0)).join("").toUpperCase().replace(/[^A-Z]/g, "") || "X";
1315
+ const ma_kh = base + cid.slice(-3).toUpperCase();
1316
+ rec = { cid: cid, name: name || "", ma_kh: ma_kh, created_at: new Date().toISOString() };
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(() => {});
1332
+ }
1333
+ } catch (_e2) {}
1334
+ }
1335
+ const maKh = String(rec.ma_kh || "");
1336
+ const secret = (process.env.DISCOUNT_SECRET || "vai2026ck");
1337
+ const exp = Math.floor(Date.now() / 1000) + 3600;
1338
+ const crypto = await import("node:crypto");
1339
+ const tk = crypto.createHmac("sha256", secret).update(maKh + ":" + exp).digest("hex").slice(0, 24);
1340
+ const link = url.origin + "/?kh=" + encodeURIComponent(maKh) + "&tk=" + tk + "&exp=" + exp;
1341
+ const ck = rec.ck != null ? String(rec.ck) : null;
1342
+ return Response.json({ ok: true, kh: maKh, ck: ck, customer: rec.name || "", link: link });
1343
+ } catch (e: any) {
1344
+ return Response.json({ ok: false, error: e.message }, { status: 500 });
1345
+ }
1346
+ },
1347
+ },
1348
  "/api/customers": {
1349
  GET: async () => {
1350
  try {
src/app.js CHANGED
@@ -1067,34 +1067,76 @@ function cleanAiText(text) {
1067
  .trim();
1068
  }
1069
 
1070
- // ── Time-limited discount link (Zalo bot → web) ─────────────────────────────
1071
- // The bot signs ?kh=<ma_kh>&tk=<hmac>&exp=<unix> with a shared secret. When the
1072
- // page is opened from that link we verify it server-side (/api/verify-kh) and:
1073
- // * remember the customer (vas_selected_customer: ma_kh, ck, name, cid)
1074
- // * remember the raw discount link for the greeting header
1075
- // * auto-apply the customer's ck to the quote + product cards
1076
- // The server response is authoritative: expired/stale links are ignored and the
1077
- // greeting then shows NO mã KH/link (customer must message the bot for a new link).
1078
  let _discountLink = null; // { kh, url } when a verified link is active
1079
  let _discountCustomer = null; // { ma_kh, ck, name } verified customer
 
 
 
 
 
 
 
 
 
 
 
1080
  async function initDiscountLink() {
1081
  try {
1082
  const p = new URLSearchParams(location.search);
1083
  const kh = (p.get("kh") || "").trim();
1084
  const tk = (p.get("tk") || "").trim();
1085
  const exp = (p.get("exp") || "").trim();
1086
- if (!kh || !tk || !exp) return; // no signed link in URL
1087
- let resp;
1088
- try {
1089
- resp = await fetch("/api/verify-kh?kh=" + encodeURIComponent(kh) + "&tk=" + encodeURIComponent(tk) + "&exp=" + encodeURIComponent(exp));
1090
- } catch (e) { return; }
1091
- if (!resp || !resp.ok) return;
1092
- const data = await resp.json();
1093
- if (!data || !data.ok) return; // expired ("link hết hạn") or bad token — ignored
1094
- const maKh = data.kh || kh;
1095
- const ck = data.ck != null ? String(data.ck) : "";
1096
- const custName = data.customer || "";
1097
- _discountLink = { kh: maKh, url: location.origin + location.pathname + "?kh=" + encodeURIComponent(maKh) + "&tk=" + encodeURIComponent(tk) + "&exp=" + encodeURIComponent(exp) };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1098
  _discountCustomer = { ma_kh: maKh, ck: ck, name: custName };
1099
  // Remember the customer for the quote/order exports (customers.js format).
1100
  try {
 
1067
  .trim();
1068
  }
1069
 
1070
+ // ── Mã KH + time-limited discount link (Zalo bot → web, and web standalone) ──
1071
+ // 1) Opened FROM the Zalo bot's signed link (?kh=..&tk=..&exp=..): verify it
1072
+ // server-side (/api/verify-kh); expired/stale links are ignored.
1073
+ // 2) Opened DIRECTLY (no ?kh): mint/load a KH + fresh signed link via
1074
+ // /api/kh-link, so the greeting and every non-product reply STILL show the
1075
+ // customer's KH + a working chiết khấu link.
1076
+ // Both paths remember the customer (vas_selected_customer: ma_kh, ck, name,
1077
+ // cid) and auto-apply the customer's ck to quote + product cards.
1078
  let _discountLink = null; // { kh, url } when a verified link is active
1079
  let _discountCustomer = null; // { ma_kh, ck, name } verified customer
1080
+ let _servedLink = null; // full signed link returned by /api/kh-link
1081
+ let _stableCid = null; // stable visitor id (localStorage vas_cid)
1082
+ async function _ensureStableCid() {
1083
+ if (_stableCid) return _stableCid;
1084
+ try { _stableCid = localStorage.getItem("vas_cid"); } catch (e) {}
1085
+ if (!_stableCid) {
1086
+ _stableCid = "web" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
1087
+ try { localStorage.setItem("vas_cid", _stableCid); } catch (e) {}
1088
+ }
1089
+ return _stableCid;
1090
+ }
1091
  async function initDiscountLink() {
1092
  try {
1093
  const p = new URLSearchParams(location.search);
1094
  const kh = (p.get("kh") || "").trim();
1095
  const tk = (p.get("tk") || "").trim();
1096
  const exp = (p.get("exp") || "").trim();
1097
+ // Store the signed-link info DAILY so a returning visitor keeps a valid
1098
+ // link even after the URL param is gone. Fallback to /api/kh-link below.
1099
+ let signed = null;
1100
+ if (kh && tk && exp) {
1101
+ let resp;
1102
+ try {
1103
+ resp = await fetch("/api/verify-kh?kh=" + encodeURIComponent(kh) + "&tk=" + encodeURIComponent(tk) + "&exp=" + encodeURIComponent(exp));
1104
+ } catch (e) { return; }
1105
+ if (!resp || !resp.ok) return;
1106
+ const data = await resp.json();
1107
+ if (data && data.ok) {
1108
+ signed = { kh: data.kh || kh, tk: tk, exp: exp, ck: data.ck != null ? String(data.ck) : "", customer: data.customer || "" };
1109
+ try { localStorage.setItem("vas_signed_kh", JSON.stringify(signed)); } catch (e2) {}
1110
+ }
1111
+ }
1112
+ // No (valid) signed link in URL → get/mint one from the server so the web
1113
+ // chat ALWAYS has a mã KH + working link (visitor identity from vas_cid).
1114
+ if (!signed) {
1115
+ try { signed = JSON.parse(localStorage.getItem("vas_signed_kh") || "null"); } catch (e3) { signed = null; }
1116
+ if (signed && signed.exp && Number(signed.exp) < Math.floor(Date.now() / 1000)) signed = null; // expired stored link
1117
+ }
1118
+ if (!signed) {
1119
+ const cid = await _ensureStableCid();
1120
+ let name = "";
1121
+ try { name = (localStorage.getItem("vas_name") || "").trim(); } catch (e4) {}
1122
+ try {
1123
+ const r = await fetch("/api/kh-link?cid=" + encodeURIComponent(cid) + "&name=" + encodeURIComponent(name));
1124
+ if (r.ok) {
1125
+ const d = await r.json();
1126
+ if (d && d.ok && d.kh && d.link) {
1127
+ signed = { kh: d.kh, tk: "", exp: String(Math.floor(Date.now() / 1000) + 3600), ck: d.ck != null ? String(d.ck) : "", customer: d.customer || "" };
1128
+ try { localStorage.setItem("vas_signed_kh", JSON.stringify(signed)); } catch (e5) {}
1129
+ _servedLink = d.link;
1130
+ }
1131
+ }
1132
+ } catch (e6) {}
1133
+ }
1134
+ if (!signed || !signed.kh) return;
1135
+ const maKh = signed.kh;
1136
+ const ck = signed.ck != null ? String(signed.ck) : "";
1137
+ const custName = signed.customer || "";
1138
+ const linkUrl = _servedLink || (location.origin + location.pathname + "?kh=" + encodeURIComponent(maKh) + "&tk=" + encodeURIComponent(signed.tk || "") + "&exp=" + encodeURIComponent(signed.exp || ""));
1139
+ _discountLink = { kh: maKh, url: linkUrl };
1140
  _discountCustomer = { ma_kh: maKh, ck: ck, name: custName };
1141
  // Remember the customer for the quote/order exports (customers.js format).
1142
  try {