/* ============================== src/service.js ============================== * 需要「服务端」的业务能力:卡密校验、检查更新。 * * 这两件事的共同点是:真实场景必须联网核验,但**离线也必须能给出确定行为**。 * 所以每个能力都是「远端优先 + 本地降级」,且降级模式会在界面上如实标注, * 不会把本地通过伪装成服务端通过(规范:不编造测试结果/接口字段)。 * * ⚠️ 本地模式的定位必须说清楚: * 仅校验格式,不具备防伪能力。要真防伪,把 CONFIG.licenseEndpoint * 指向你自己的校验接口即可——请求/响应结构见 verifyRemote() 注释。 */ var Service = {}; Service.CONFIG = { /* 留空 = 本地模式。填写后 login() 会先请求远端。 */ licenseEndpoint: "", licenseTimeoutMs: 8000, /* 本地模式下的默认有效期(天),仅用于让「到期时间」这一栏有真实含义。 */ localTrialDays: 30, /* 检查更新的清单地址,留空则用内置版本号自比。 */ updateEndpoint: "" }; /** 脚本版本。设计稿的「版本信息卡」显示的就是它;build.js 会用构建日期覆盖。 */ Service.SCRIPT_VERSION = "2026.10.13"; Service.CHANGELOG = [ "2026.10.11 数量类设置全部改为「最小值 ~ 最大值」区间(关注/私信/休息/浏览/单次/停止/间隔)", "2026.10.11 补齐登录页 / 运行状态条 / 拦截弹窗,ID提取页改单选", "2026.10.10 界面层按设计稿重构,配置项全部落盘", "2026.10.03 接入设备真实状态(权限 / TikTok 版本)", "2026.09.28 首个可运行版本" ]; /* ---------------------------------------------------------------- 卡密 */ /** 去掉空格与连字符并转大写,得到比较用的规范形式。 */ Service.normalizeCode = function (code) { return String(code || "").replace(/[\s-]/g, "").toUpperCase(); }; /** * 格式校验:16 位大写字母或数字。 * 只做形态校验——真伪必须靠服务端,本地无法判定一个随机串是不是有效卡密。 */ Service.isWellFormed = function (code) { return /^[A-Z0-9]{16}$/.test(Service.normalizeCode(code)); }; /** 展示用分组:ABCDEFGHIJKLMNOP → ABCDE-FGHI-JKLM-NOPQ 形态的 4 组。 */ Service.formatCode = function (code) { var n = Service.normalizeCode(code); var out = []; for (var i = 0; i < n.length; i += 4) { out.push(n.substring(i, i + 4)); } return out.join("-"); }; function httpPostJson(url, payload) { var body = JSON.stringify(payload); var response = http.post(url, body, { headers: { "Content-Type": "application/json" }, timeout: Service.CONFIG.licenseTimeoutMs }); if (!response || response.statusCode !== 200) { return null; } try { return JSON.parse(response.body.string()); } catch (e) { console.log("[service] 卡密响应不是合法 JSON"); return null; } } /** * 远端校验。 * 请求:POST {endpoint} {"code":"<规范化卡密>"} * 响应:{"ok":true,"expireAt":"2026-11-09"} 或 {"ok":false,"reason":"卡密不存在"} * 返回 null 表示「没问到」(网络/格式问题),调用方据此决定是否降级为本地模式。 */ Service.verifyRemote = function (code) { if (!Service.CONFIG.licenseEndpoint) { return null; } var data = null; try { data = httpPostJson(Service.CONFIG.licenseEndpoint, { code: code }); } catch (e) { console.log("[service] 卡密远端校验异常: " + e); return null; } if (!data) { return null; } return { ok: !!data.ok, expireAt: data.expireAt || "", reason: data.reason || "卡密无效" }; }; /** 今天 + N 天,格式 yyyy-MM-dd。用于本地模式生成到期时间。 */ Service.datePlusDays = function (days) { var now = new Date(); now.setDate(now.getDate() + Number(days || 0)); var m = now.getMonth() + 1; var d = now.getDate(); return now.getFullYear() + "-" + (m < 10 ? "0" + m : m) + "-" + (d < 10 ? "0" + d : d); }; Service.today = function () { return Service.datePlusDays(0); }; /** 过期判定:只比日期字符串(yyyy-MM-dd 字典序即时间序),不引时区计算。 */ Service.isExpired = function (expireAt) { if (!expireAt) { return false; } return Service.today() > String(expireAt); }; /** * 登录。返回 { ok, status, message }。 * status: "active" | "expired" | "none" */ Service.login = function (code) { var normalized = Service.normalizeCode(code); if (!Service.isWellFormed(normalized)) { return { ok: false, status: "none", message: "卡密格式不对,应为 16 位字母或数字" }; } var remote = Service.verifyRemote(normalized); if (remote) { if (!remote.ok) { return { ok: false, status: "none", message: remote.reason }; } if (Service.isExpired(remote.expireAt)) { return { ok: false, status: "expired", message: "该卡密已于 " + remote.expireAt + " 到期" }; } return { ok: true, status: "active", code: normalized, expireAt: remote.expireAt, message: "登录成功(服务端核验)" }; } /* 本地模式:只有格式校验,如实告知,不伪装成服务端通过。 */ return { ok: true, status: "active", code: normalized, expireAt: Service.datePlusDays(Service.CONFIG.localTrialDays), message: "登录成功(本地模式,未联网核验)" }; }; /** 读当前卡密状态,并顺手处理「已过期但状态还写着 active」的情况。 */ Service.licenseStatus = function () { var status = Store.getEnum("license.status", "licenseStatus"); var expireAt = Store.getText("license.expireAt"); var code = Store.getText("license.code"); if (status === "active" && Service.isExpired(expireAt)) { Store.set("license.status", "expired"); status = "expired"; } return { status: status, code: code, expireAt: expireAt }; }; /** * 状态 → 界面文案与配色 key。集中在这里,避免页面各自判断。 * * ⚠️ 返回值里必须带 `status` —— 页面除了文案还要按状态分支 * (已登录才显示「解绑」,否则显示「去登录」)。 * 早先只返回 text/tone/expireAt,页面写 `display.status === "active"` 永远是 false, * 结果是「已登录」旁边挂着「去登录」,自相矛盾但没有任何报错。 */ Service.licenseDisplay = function () { var st = Service.licenseStatus(); if (st.status === "active") { return { status: st.status, text: "已登录", tone: "ok", expireAt: st.expireAt || "—" }; } if (st.status === "expired") { return { status: st.status, text: "已过期", tone: "warn", expireAt: st.expireAt || "—" }; } return { status: st.status, text: "未登录", tone: "warn", expireAt: "—" }; }; Service.logout = function () { Store.set("license.status", "none"); Store.set("license.code", ""); Store.set("license.expireAt", ""); }; /* ---------------------------------------------------------------- 更新 */ /** * 检查更新。远端不可达时退化为「与内置版本号自比」, * 结论同样如实标注来源,避免把「没连上」说成「已是最新」。 */ Service.checkUpdate = function () { var current = Service.SCRIPT_VERSION; if (Service.CONFIG.updateEndpoint) { var data = null; try { var response = http.get(Service.CONFIG.updateEndpoint, { timeout: 8000 }); if (response && response.statusCode === 200) { data = JSON.parse(response.body.string()); } } catch (e) { console.log("[service] 检查更新失败: " + e); } if (data && data.version) { return { latest: String(data.version), hasUpdate: String(data.version) > current, source: "remote", note: data.note || "" }; } return { latest: current, hasUpdate: false, source: "unknown", note: "检查失败,请稍后重试" }; } return { latest: current, hasUpdate: false, source: "local", note: "已是最新" }; };