| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- /* ============================== 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: "已是最新" };
- };
|