B2B网络软件

标题: 无碍模式auto淘宝千牛自动发货系统 [打印本页]

作者: YYPOST群发软件    时间: 2 小时前
标题: 无碍模式auto淘宝千牛自动发货系统

无碍模式auto淘宝千牛自动发货系统
无碍模式auto淘宝千牛自动发货系统 B2B网络软件

  1. // =============================================================================
  2. //  淘宝千牛自动发货系统  v3.0(精简版)
  3. //  仅支持无碍模式(auto),使用完整商品标题精准匹配
  4. //  功能:24小时监控千牛待发货列表 → 匹配商品标题 → 发送卡密/网盘链接
  5. //  适用:AIWROK 安卓自动化平台,ES5/Rhino 引擎
  6. // =============================================================================

  7. importClass(java.io.File);
  8. importClass(java.io.FileWriter);
  9. importClass(java.io.BufferedReader);
  10. importClass(java.io.InputStreamReader);
  11. importClass(java.io.FileInputStream);
  12. importClass(java.io.FileOutputStream);
  13. importClass(java.io.OutputStreamWriter);

  14. // =============================================================================
  15. //  第一部分:全局配置
  16. // =============================================================================

  17. var CONFIG = {
  18.     千牛包名: "com.taobao.qianniu",
  19.     轮询间隔: 30,          // 检查新订单间隔(秒)
  20.     最大重试: 3,
  21.     操作延时: 2,            // 每步操作后等待(秒)

  22.     // 卡密服务器API(留空则用本地文件)
  23.     服务器地址: "",
  24.     服务器密钥: "",
  25.     使用服务器: false,

  26.     卡密目录: "/sdcard/卡密仓库/",
  27.     发货记录文件: "/sdcard/发货记录.txt",

  28.     启用保活: true,
  29.     保活间隔: 300,

  30.     启用悬浮窗: true,

  31.     // debug=只输出节点摘要不发货; test=处理一个真实订单测试; false=正常监控
  32.     调试模式: "debug",
  33. };

  34. // =============================================================================
  35. //  第二部分:商品配置(用完整商品标题,精准匹配)
  36. // =============================================================================

  37. /**
  38. * 配置说明:
  39. *   商品标题:  淘宝商品的完整标题,用于在订单列表中精准匹配
  40. *   发货类型:  "卡密" / "网盘" / "卡密+网盘"
  41. *   卡密文件:  本地卡密文件路径(每行一个卡密,用过的自动删除)
  42. *   网盘链接:  该商品对应的网盘链接
  43. *   发货话术:  发送给买家的话术,{卡密}和{网盘}自动替换
  44. */
  45. var 商品配置 = {
  46.     "二年级数学必刷题上册人教版口算题卡天天练": {
  47.         发货类型: "卡密",
  48.         卡密文件: "/sdcard/卡密仓库/二年级数学必刷题.txt",
  49.         网盘链接: "",
  50.         发货话术: "您好,您购买的【二年级数学必刷题】已发货!\n卡密:{卡密}\n请打开APP输入卡密激活使用,有问题随时联系~"
  51.     },
  52.     "小学单位换算练习元角分长度单位专项训练": {
  53.         发货类型: "网盘",
  54.         卡密文件: "",
  55.         网盘链接: "https://pan.baidu.com/s/1aBcDeFgH 提取码: x1y2",
  56.         发货话术: "您好,您购买的【小学单位换算练习】已发货!\n网盘下载链接:{网盘}\n请复制链接到浏览器下载,有问题随时联系~"
  57.     },
  58.     "一年级加减口算练习册10以内20以内凑十法": {
  59.         发货类型: "卡密+网盘",
  60.         卡密文件: "/sdcard/卡密仓库/一年级加减口算.txt",
  61.         网盘链接: "https://pan.baidu.com/s/2bCdEfGhI 提取码: a3b4",
  62.         发货话术: "您好,您购买的【一年级加减口算】已发货!\n激活卡密:{卡密}\n网盘链接:{网盘}\n请先下载APP,输入卡密激活使用~"
  63.     }
  64.     // 继续添加商品...把完整的淘宝商品标题填进来即可
  65. };

  66. // =============================================================================
  67. //  第三部分:无碍模式操作封装(仅auto,无模式分支)
  68. // =============================================================================

  69. var ActionAdapter = {

  70.     // ================================================================
  71.     //  基础操作(直接调用 auto 对象)
  72.     // ================================================================

  73.     init: function() {
  74.         if (typeof auto !== 'undefined') {
  75.             logger.log("无碍模式已就绪");
  76.             return true;
  77.         }
  78.         logger.err("auto对象不存在,请开启无障碍服务");
  79.         return false;
  80.     },

  81.     click: function(x, y) {
  82.         try {
  83.             auto.clickPoint(x, y);
  84.             return true;
  85.         } catch (e) {
  86.             logger.err("点击失败: " + e);
  87.             return false;
  88.         }
  89.     },

  90.     clickPercent: function(px, py) {
  91.         try {
  92.             auto.clickPercent(px, py);
  93.             return true;
  94.         } catch (e) {
  95.             logger.err("百分比点击失败: " + e);
  96.             return false;
  97.         }
  98.     },

  99.     swipe: function(x1, y1, x2, y2, duration) {
  100.         duration = duration || 500;
  101.         try {
  102.             auto.swip(x1, y1, x2, y2, duration, 0);
  103.             return true;
  104.         } catch (e) {
  105.             logger.err("滑动失败: " + e);
  106.             return false;
  107.         }
  108.     },

  109.     swipePercent: function(px1, py1, px2, py2, duration) {
  110.         duration = duration || 500;
  111.         var w = screen.getScreenWidth();
  112.         var h = screen.getScreenHeight();
  113.         return this.swipe(
  114.             Math.floor(px1 * w), Math.floor(py1 * h),
  115.             Math.floor(px2 * w), Math.floor(py2 * h), duration
  116.         );
  117.     },

  118.     back: function() {
  119.         try { auto.back(); return true; } catch (e) { return false; }
  120.     },

  121.     home: function() {
  122.         try { auto.home(); return true; } catch (e) { return false; }
  123.     },

  124.     // ================================================================
  125.     //  核心:auto.getXml() 节点解析
  126.     //  千牛很多按钮文字在 desc 属性里,必须同时匹配 text 和 desc
  127.     // ================================================================

  128.     getXml: function() {
  129.         try {
  130.             if (typeof auto !== 'undefined' && typeof auto.getXml === 'function') {
  131.                 return auto.getXml();
  132.             }
  133.         } catch (e) {
  134.             logger.err("getXml失败: " + e);
  135.         }
  136.         return "";
  137.     },

  138.     /**
  139.      * 从XML中查找包含指定文字的节点(同时匹配 text 和 desc)
  140.      */
  141.     findNodes: function(xml, keyword) {
  142.         var results = [];
  143.         if (!xml || !keyword) return results;

  144.         var pattern = /<node\s+([^>]+?)(?:\/>|>)/g;
  145.         var match;
  146.         while ((match = pattern.exec(xml)) !== null) {
  147.             var attrs = match[1];
  148.             var textVal = "";
  149.             var descVal = "";
  150.             var tm = attrs.match(/text="([^"]*)"/);
  151.             if (tm) textVal = tm[1];
  152.             var dm = attrs.match(/desc="([^"]*)"/);
  153.             if (dm) descVal = dm[1];

  154.             if (textVal.indexOf(keyword) !== -1 || descVal.indexOf(keyword) !== -1) {
  155.                 results.push(this._parseNode(attrs, textVal, descVal));
  156.             }
  157.         }
  158.         return results;
  159.     },

  160.     /**
  161.      * 解析XML中所有节点(不过滤关键词,用于调试和父节点查找)
  162.      */
  163.     findAllNodes: function(xml) {
  164.         var results = [];
  165.         if (!xml) return results;

  166.         var pattern = /<node\s+([^>]+?)(?:\/>|>)/g;
  167.         var match;
  168.         while ((match = pattern.exec(xml)) !== null) {
  169.             var attrs = match[1];
  170.             var tm = attrs.match(/text="([^"]*)"/);
  171.             var dm = attrs.match(/desc="([^"]*)"/);
  172.             results.push(this._parseNode(attrs, tm ? tm[1] : "", dm ? dm[1] : ""));
  173.         }
  174.         return results;
  175.     },

  176.     /**
  177.      * 解析单个节点属性(内部方法)
  178.      */
  179.     _parseNode: function(attrs, textVal, descVal) {
  180.         var cpm = attrs.match(/centerPercent="\[([0-9.]+),([0-9.]+)\]"/);
  181.         var cm = attrs.match(/center="\[(\d+),(\d+)\]"/);
  182.         var clm = attrs.match(/clickable="([^"]*)"/);
  183.         var idm = attrs.match(/id="([^"]*)"/);
  184.         var bm = attrs.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  185.         var clsm = attrs.match(/class="([^"]*)"/);

  186.         return {
  187.             text: textVal,
  188.             desc: descVal,
  189.             x: cm ? parseInt(cm[1]) : 0,
  190.             y: cm ? parseInt(cm[2]) : 0,
  191.             px: cpm ? parseFloat(cpm[1]) : 0,
  192.             py: cpm ? parseFloat(cpm[2]) : 0,
  193.             clickable: clm ? clm[1] : "false",
  194.             id: idm ? idm[1] : "",
  195.             bounds: bm ? [parseInt(bm[1]), parseInt(bm[2]), parseInt(bm[3]), parseInt(bm[4])] : null,
  196.             class: clsm ? clsm[1] : ""
  197.         };
  198.     },

  199.     /**
  200.      * 查找不可点击节点的最近可点击父节点
  201.      */
  202.     findClickableParent: function(node, xml) {
  203.         if (!node || !node.bounds) return null;
  204.         var allNodes = this.findAllNodes(xml);
  205.         var nodeArea = (node.bounds[2] - node.bounds[0]) * (node.bounds[3] - node.bounds[1]);
  206.         var bestParent = null;
  207.         var bestArea = 999999999;

  208.         for (var i = 0; i < allNodes.length; i++) {
  209.             var p = allNodes[i];
  210.             if (p.clickable !== "true" || !p.bounds) continue;
  211.             // 父节点必须包含目标节点
  212.             if (p.bounds[0] <= node.bounds[0] && p.bounds[1] <= node.bounds[1] &&
  213.                 p.bounds[2] >= node.bounds[2] && p.bounds[3] >= node.bounds[3]) {
  214.                 var pArea = (p.bounds[2] - p.bounds[0]) * (p.bounds[3] - p.bounds[1]);
  215.                 // 选面积最小的可点击父节点(最近的)
  216.                 if (pArea < bestArea && pArea >= nodeArea) {
  217.                     bestArea = pArea;
  218.                     bestParent = p;
  219.                 }
  220.             }
  221.         }
  222.         return bestParent;
  223.     },

  224.     /**
  225.      * 找文字并点击(核心方法)
  226.      * 1. 查找包含文字的节点
  227.      * 2. 优先点击 clickable=true 的节点
  228.      * 3. 不可点击的找父节点点击
  229.      */
  230.     findTextAndClick: function(text, timeout) {
  231.         timeout = timeout || 3000;
  232.         var startTime = new Date().getTime();

  233.         while (new Date().getTime() - startTime < timeout) {
  234.             var xml = this.getXml();
  235.             if (!xml) { sleep.millisecond(毫秒 = 300); continue; }

  236.             var result = this._findBestNode(xml, text);
  237.             if (result) {
  238.                 if (result.clickable === "true") {
  239.                     this.clickPercent(result.px, result.py);
  240.                 } else {
  241.                     // 不可点击 → 找最近可点击父节点
  242.                     var parent = this.findClickableParent(result, xml);
  243.                     if (parent) {
  244.                         logger.log("节点不可点击,使用父节点: [" + result.text + "|" + result.desc + "] -> px=" + parent.px + " py=" + parent.py);
  245.                         this.clickPercent(parent.px, parent.py);
  246.                     } else {
  247.                         this.clickPercent(result.px, result.py);
  248.                     }
  249.                 }
  250.                 sleep.millisecond(毫秒 = 500);
  251.                 logger.log("找到并点击: " + text + " (text=" + result.text + " desc=" + result.desc + ")", true);
  252.                 return true;
  253.             }
  254.             sleep.millisecond(毫秒 = 300);
  255.         }

  256.         logger.warn("未找到文字: " + text);
  257.         return false;
  258.     },

  259.     /**
  260.      * 查找最佳节点(优先可点击的)
  261.      */
  262.     _findBestNode: function(xml, text) {
  263.         var nodes = this.findNodes(xml, text);
  264.         if (nodes.length === 0) return null;
  265.         // 优先返回 clickable=true
  266.         for (var i = 0; i < nodes.length; i++) {
  267.             if (nodes[i].clickable === "true") return nodes[i];
  268.         }
  269.         return nodes[0];
  270.     },

  271.     /**
  272.      * 找文字(返回节点信息,不点击)
  273.      */
  274.     findText: function(text) {
  275.         var xml = this.getXml();
  276.         if (!xml) return null;
  277.         return this._findBestNode(xml, text);
  278.     },

  279.     hasText: function(text) {
  280.         return this.findText(text) !== null;
  281.     },

  282.     hasAnyText: function(texts) {
  283.         for (var i = 0; i < texts.length; i++) {
  284.             if (this.findText(texts[i])) return true;
  285.         }
  286.         return false;
  287.     },

  288.     /**
  289.      * 输出当前页面所有有意义节点的摘要(调试用)
  290.      */
  291.     dumpPageSummary: function() {
  292.         var xml = this.getXml();
  293.         if (!xml) { logger.warn("无法获取页面XML"); return; }

  294.         var nodes = this.findAllNodes(xml);
  295.         var count = 0;
  296.         logger.log("====== 页面节点摘要 ======");
  297.         for (var i = 0; i < nodes.length; i++) {
  298.             var n = nodes[i];
  299.             // 过滤掉空白节点
  300.             if (n.text === "" && n.desc === "") continue;
  301.             logger.log("  [" + (n.clickable === "true" ? "可点击" : "  不可") + "] text=\"" + n.text + "\" desc=\"" + n.desc + "\" px=" + n.px + " py=" + n.py);
  302.             count++;
  303.         }
  304.         logger.log("====== 共 " + count + " 个有意义节点 ======");
  305.     },

  306.     /**
  307.      * 输入文字(通过剪贴板粘贴,兼容中文)
  308.      */
  309.     inputText: function(text) {
  310.         try {
  311.             if (typeof Clipboard !== 'undefined' && Clipboard.copy) {
  312.                 Clipboard.copy(text);
  313.                 sleep.millisecond(毫秒 = 300);
  314.             }

  315.             // 尝试粘贴
  316.             try {
  317.                 var node = auto.nodeSearch(3000).focused(true);
  318.                 if (node && typeof node.getNode === 'function') {
  319.                     var n = node.getNode(0);
  320.                     if (n && typeof n.paste === 'function') {
  321.                         n.paste();
  322.                     }
  323.                 }
  324.             } catch (pe) {}

  325.             sleep.second(秒 = 1);
  326.             logger.log("已输入文字: " + text.substring(0, 20) + (text.length > 20 ? "..." : ""));
  327.             return true;
  328.         } catch (e) {
  329.             logger.err("输入文字失败: " + e);
  330.             return false;
  331.         }
  332.     },

  333.     /**
  334.      * 等待指定文字出现
  335.      */
  336.     backToText: function(text, timeout) {
  337.         timeout = timeout || 10000;
  338.         var startTime = new Date().getTime();
  339.         while (new Date().getTime() - startTime < timeout) {
  340.             if (this.hasText(text)) return true;
  341.             sleep.millisecond(毫秒 = 500);
  342.         }
  343.         return false;
  344.     }
  345. };

  346. // =============================================================================
  347. //  第四部分:日志系统(悬浮窗 + Toast + print.log/err/warn)
  348. // =============================================================================

  349. var logger = {
  350.     悬浮窗实例: null,
  351.     标题控件: null,
  352.     内容控件: null,
  353.     状态控件: null,
  354.     日志缓冲: [],
  355.     最大行数: 12,
  356.     统计: { 轮次: 0, 发货: 0, 错误: 0, 启动时间: null },

  357.     init: function() {
  358.         this.统计.启动时间 = new Date();
  359.         if (!CONFIG.启用悬浮窗) return;
  360.         try {
  361.             this.悬浮窗实例 = new floatUI();
  362.             this.悬浮窗实例.loadXML(
  363.                 '<LinearLayout orientation="vertical" w="400" h="280" gravity="top" id="log_main" background="#CC000000">' +
  364.                 '  <LinearLayout orientation="horizontal" w="400" h="30" gravity="center_vertical" background="#FF1a1a2e">' +
  365.                 '    <TextView id="log_title" textColor="#FFD700" textSize="13sp" padding="8dp" layout_weight="1" text="&#128230; 淘宝自动发货v3" />' +
  366.                 '  </LinearLayout>' +
  367.                 '  <LinearLayout orientation="horizontal" w="400" h="24" gravity="center_vertical" background="#FF16213e">' +
  368.                 '    <TextView id="log_status" textColor="#00FFFF" textSize="10sp" padding="6dp" layout_weight="1" text="状态: 初始化中..." />' +
  369.                 '  </LinearLayout>' +
  370.                 '  <TextView id="log_content" textColor="#00FF00" textSize="10sp" padding="6dp" layout_weight="1" text="等待启动...\n" />' +
  371.                 '</LinearLayout>'
  372.             );
  373.             this.标题控件 = this.悬浮窗实例.findViewById('log_title');
  374.             this.状态控件 = this.悬浮窗实例.findViewById('log_status');
  375.             this.内容控件 = this.悬浮窗实例.findViewById('log_content');

  376.             var self = this;
  377.             setTimeout(function() {
  378.                 if (self.悬浮窗实例) self.悬浮窗实例.setPosition(5, 5);
  379.             }, 200);
  380.         } catch (e) {}
  381.     },

  382.     _time: function() {
  383.         var now = new Date();
  384.         var h = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
  385.         return (h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
  386.     },

  387.     _更新状态: function() {
  388.         if (!this.状态控件) return;
  389.         try {
  390.             var runtime = "";
  391.             if (this.统计.启动时间) {
  392.                 var diff = Math.floor((new Date() - this.统计.启动时间) / 1000);
  393.                 var h = Math.floor(diff / 3600);
  394.                 var m = Math.floor((diff % 3600) / 60);
  395.                 var s = diff % 60;
  396.                 runtime = " 运行" + (h > 0 ? h + "h" : "") + m + "m" + s + "s";
  397.             }
  398.             this.状态控件.setText("轮次:" + this.统计.轮次 + " | 发货:" + this.统计.发货 + " | 错误:" + this.统计.错误 + runtime);
  399.         } catch (e) {}
  400.     },

  401.     _toast: function(msg) {
  402.         try {
  403.             if (typeof toast !== 'undefined' && toast.show) toast.show("" + msg);
  404.         } catch (e) {}
  405.     },

  406.     _output: function(level, msg, showToast) {
  407.         var timeStr = this._time();
  408.         var levelTag = "";
  409.         if (level === "信息") { levelTag = "ℹ"; }
  410.         else if (level === "成功") { levelTag = "✅"; }
  411.         else if (level === "警告") { levelTag = "⚠"; }
  412.         else if (level === "错误") { levelTag = "❌"; this.统计.错误++; }

  413.         var line = timeStr + " " + levelTag + " " + msg;

  414.         // 按级别调用 AIWROK 平台原生方法
  415.         try {
  416.             if (level === "错误" && typeof print !== 'undefined' && print.err) {
  417.                 print.err(line);
  418.             } else if (level === "警告" && typeof print !== 'undefined' && print.warn) {
  419.                 print.warn(line);
  420.             } else if (typeof print !== 'undefined' && print.log) {
  421.                 print.log(line);
  422.             } else if (typeof printl === 'function') {
  423.                 printl(line);
  424.             }
  425.         } catch (e) {}

  426.         // 悬浮窗
  427.         this.日志缓冲.push(line);
  428.         if (this.日志缓冲.length > this.最大行数) this.日志缓冲.shift();
  429.         if (this.内容控件) {
  430.             try { this.内容控件.setText(this.日志缓冲.join("\n")); } catch (e) {}
  431.         }
  432.         this._更新状态();

  433.         if (showToast) this._toast(levelTag + " " + msg);
  434.     },

  435.     log: function(msg, toast) { this._output("信息", msg, toast); },
  436.     warn: function(msg, toast) { this._output("警告", msg, toast); },
  437.     err: function(msg, toast) { this._output("错误", msg, toast); },
  438.     success: function(msg, toast) { this._output("成功", msg, toast); },

  439.     addShip: function() { this.统计.发货++; this._更新状态(); },
  440.     addRound: function() { this.统计.轮次++; this._更新状态(); }
  441. };

  442. // =============================================================================
  443. //  第五部分:卡密管理
  444. // =============================================================================

  445. var 卡密管理 = {

  446.     取本地卡密: function(文件路径) {
  447.         try {
  448.             var file = new File(文件路径);
  449.             if (!file.exists()) {
  450.                 logger.err("卡密文件不存在: " + 文件路径);
  451.                 return null;
  452.             }
  453.             var reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
  454.             var lines = [], line;
  455.             while ((line = reader.readLine()) !== null) {
  456.                 line = line.trim();
  457.                 if (line !== "" && line.indexOf("#") !== 0) lines.push(line);
  458.             }
  459.             reader.close();

  460.             if (lines.length === 0) {
  461.                 logger.warn("卡密文件已空: " + 文件路径);
  462.                 return null;
  463.             }
  464.             var 卡密 = lines[0];
  465.             logger.log("取出卡密: " + 卡密 + " (剩余 " + (lines.length - 1) + " 个)");

  466.             var remaining = lines.slice(1);
  467.             var writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
  468.             for (var i = 0; i < remaining.length; i++) writer.write(remaining[i] + "\n");
  469.             writer.close();
  470.             return 卡密;
  471.         } catch (e) {
  472.             logger.err("读取卡密失败: " + e);
  473.             return null;
  474.         }
  475.     },

  476.     取服务器卡密: function(商品ID) {
  477.         try {
  478.             var url = CONFIG.服务器地址 + "?action=get&product=" + 商品ID + "&key=" + CONFIG.服务器密钥;
  479.             logger.log("从服务器获取卡密: " + url);
  480.             var response = http.get(url);
  481.             if (!response) { logger.err("服务器无响应"); return null; }

  482.             try {
  483.                 var json = JSON.parse(response);
  484.                 if (json.code === 0 && json.data) {
  485.                     logger.log("服务器返回卡密: " + json.data);
  486.                     return json.data;
  487.                 }
  488.                 logger.err("服务器返回错误: " + (json.msg || json.message || response));
  489.             } catch (e) {
  490.                 if (response && response.length > 0) {
  491.                     logger.log("服务器返回卡密: " + response);
  492.                     return response;
  493.                 }
  494.             }
  495.         } catch (e) {
  496.             logger.err("服务器获取卡密失败: " + e);
  497.         }
  498.         return null;
  499.     },

  500.     上报已使用: function(商品ID, 卡密, 订单号) {
  501.         try {
  502.             var url = CONFIG.服务器地址 + "?action=report&product=" + 商品ID + "&key=" + 卡密 + "&order=" + 订单号;
  503.             var response = http.get(url);
  504.             logger.log("服务器上报结果: " + response);
  505.         } catch (e) {
  506.             logger.err("服务器上报失败: " + e);
  507.         }
  508.     },

  509.     取卡密: function(商品) {
  510.         if (CONFIG.使用服务器) return this.取服务器卡密(商品.名称 || "");
  511.         return this.取本地卡密(商品.卡密文件);
  512.     },

  513.     检查库存: function(商品) {
  514.         if (CONFIG.使用服务器) return -1;
  515.         try {
  516.             var file = new File(商品.卡密文件);
  517.             if (!file.exists()) return 0;
  518.             var reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
  519.             var count = 0, line;
  520.             while ((line = reader.readLine()) !== null) {
  521.                 line = line.trim();
  522.                 if (line !== "" && line.indexOf("#") !== 0) count++;
  523.             }
  524.             reader.close();
  525.             return count;
  526.         } catch (e) { return -1; }
  527.     }
  528. };

  529. // =============================================================================
  530. //  第六部分:发货记录
  531. // =============================================================================

  532. var 发货记录 = {

  533.     已发货: function(订单号) {
  534.         try {
  535.             var file = new File(CONFIG.发货记录文件);
  536.             if (!file.exists()) return false;
  537.             var reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
  538.             var line;
  539.             while ((line = reader.readLine()) !== null) {
  540.                 if (line.indexOf(订单号) !== -1) { reader.close(); return true; }
  541.             }
  542.             reader.close();
  543.         } catch (e) {}
  544.         return false;
  545.     },

  546.     记录: function(订单号, 商品名, 发货内容) {
  547.         try {
  548.             var writer = new OutputStreamWriter(new FileOutputStream(CONFIG.发货记录文件, true), "UTF-8");
  549.             var now = new Date();
  550.             var dateStr = now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate() + " " + logger._time();
  551.             writer.write(dateStr + " | " + 订单号 + " | " + 商品名 + " | " + 发货内容 + "\n");
  552.             writer.close();
  553.             logger.log("已记录发货: " + 订单号 + " -> " + 商品名);
  554.         } catch (e) {
  555.             logger.err("记录发货失败: " + e);
  556.         }
  557.     },

  558.     今日数量: function() {
  559.         try {
  560.             var file = new File(CONFIG.发货记录文件);
  561.             if (!file.exists()) return 0;
  562.             var reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
  563.             var now = new Date();
  564.             var today = now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
  565.             var count = 0, line;
  566.             while ((line = reader.readLine()) !== null) {
  567.                 if (line.indexOf(today) !== -1) count++;
  568.             }
  569.             reader.close();
  570.             return count;
  571.         } catch (e) { return 0; }
  572.     }
  573. };

  574. // =============================================================================
  575. //  第七部分:千牛自动化操作
  576. // =============================================================================

  577. var 千牛操作 = {

  578.     启动千牛: function() {
  579.         logger.log("启动千牛...", true);
  580.         try {
  581.             app.openApp(CONFIG.千牛包名);
  582.             for (var i = 8; i > 0; i--) {
  583.                 sleep.second(秒 = 1);
  584.             }
  585.             logger.success("千牛已启动");
  586.             return true;
  587.         } catch (e) {
  588.             logger.err("启动千牛失败: " + e);
  589.             return false;
  590.         }
  591.     },

  592.     确保运行: function() {
  593.         // 无碍模式没有shell,直接用app.openApp确保运行
  594.         app.openApp(CONFIG.千牛包名);
  595.         sleep.second(秒 = 3);
  596.     },

  597.     /**
  598.      * 检查是否在订单管理页面
  599.      */
  600.     在订单管理页: function() {
  601.         return ActionAdapter.hasText("待发货") || ActionAdapter.hasText("待付款") ||
  602.                ActionAdapter.hasText("已发货") || ActionAdapter.hasText("催发货");
  603.     },

  604.     /**
  605.      * 检查待发货标签是否已选中
  606.      */
  607.     待发货已选中: function() {
  608.         var xml = ActionAdapter.getXml();
  609.         if (!xml) return false;
  610.         var nodes = ActionAdapter.findNodes(xml, "待发货");
  611.         for (var i = 0; i < nodes.length; i++) {
  612.             // selected=true 表示已选中
  613.             var pattern = /<node\s+([^>]+?)(?:\/>|>)/g;
  614.             var match;
  615.             while ((match = pattern.exec(xml)) !== null) {
  616.                 var attrs = match[1];
  617.                 if ((attrs.indexOf("待发货") !== -1) && attrs.indexOf('selected="true"') !== -1) {
  618.                     return true;
  619.                 }
  620.             }
  621.         }
  622.         return false;
  623.     },

  624.     /**
  625.      * 进入待发货订单列表
  626.      * 多条路径:直接找"待发货"标签 → 找"订单管理"入口 → 首页重试
  627.      */
  628.     进入待发货列表: function() {
  629.         logger.log("进入待发货订单列表...", true);

  630.         // 路径1:直接点击"待发货"标签
  631.         if (ActionAdapter.findTextAndClick("待发货", 3000)) {
  632.             sleep.second(秒 = CONFIG.操作延时);
  633.             if (this.在订单管理页()) {
  634.                 logger.log("已在待发货列表页面");
  635.                 return true;
  636.             }
  637.         }

  638.         // 路径2:找"订单管理"入口
  639.         if (ActionAdapter.findTextAndClick("订单管理", 2000)) {
  640.             sleep.second(秒 = CONFIG.操作延时);
  641.             if (ActionAdapter.findTextAndClick("待发货", 3000)) {
  642.                 sleep.second(秒 = CONFIG.操作延时);
  643.                 if (this.在订单管理页()) return true;
  644.             }
  645.         }

  646.         // 路径3:从首页找订单入口
  647.         ActionAdapter.home();
  648.         sleep.second(秒 = 2);
  649.         if (ActionAdapter.findTextAndClick("订单", 3000)) {
  650.             sleep.second(秒 = CONFIG.操作延时);
  651.             ActionAdapter.findTextAndClick("待发货", 3000);
  652.             sleep.second(秒 = CONFIG.操作延时);
  653.             if (this.在订单管理页()) return true;
  654.         }

  655.         logger.warn("未能进入待发货列表");
  656.         return false;
  657.     },

  658.     /**
  659.      * 检查是否有新订单(空状态消失 = 有订单)
  660.      */
  661.     有新订单: function() {
  662.         // 空状态提示
  663.         if (ActionAdapter.hasText("还没有相关订单") || ActionAdapter.hasText("暂无")) {
  664.             return false;
  665.         }

  666.         // 有订单时的特征文字
  667.         var 订单特征 = ["立即发货", "去发货", "买家已付款", "联系买家"];
  668.         for (var i = 0; i < 订单特征.length; i++) {
  669.             if (ActionAdapter.hasText(订单特征[i])) {
  670.                 logger.log("检测到待发货订单: " + 订单特征[i], true);
  671.                 return true;
  672.             }
  673.         }

  674.         // 检查是否有订单号格式的数字
  675.         var xml = ActionAdapter.getXml();
  676.         if (xml) {
  677.             var allNodes = ActionAdapter.findAllNodes(xml);
  678.             for (var j = 0; j < allNodes.length; j++) {
  679.                 if (allNodes[j].text && /^\d{15,20}$/.test(allNodes[j].text)) {
  680.                     logger.log("检测到订单号: " + allNodes[j].text, true);
  681.                     return true;
  682.                 }
  683.             }
  684.         }
  685.         return false;
  686.     },

  687.     /**
  688.      * 匹配商品(用完整商品标题精准匹配)
  689.      * 遍历商品配置,在页面XML中查找哪个商品标题出现了
  690.      * @return {object|null} {名称, 配置, 节点}
  691.      */
  692.     匹配商品: function() {
  693.         var xml = ActionAdapter.getXml();
  694.         if (!xml) xml = "";

  695.         for (var 商品标题 in 商品配置) {
  696.             // 直接用完整标题去XML里找
  697.             var nodes = ActionAdapter.findNodes(xml, 商品标题);
  698.             if (nodes.length > 0) {
  699.                 logger.success("精准匹配到商品: " + 商品标题, true);
  700.                 logger.log("  节点: text=[" + nodes[0].text + "] desc=[" + nodes[0].desc + "] px=" + nodes[0].px + " py=" + nodes[0].py);
  701.                 return {
  702.                     名称: 商品标题,
  703.                     配置: 商品配置[商品标题],
  704.                     节点: nodes[0]
  705.                 };
  706.             }
  707.         }

  708.         // 如果完整标题没匹配到,尝试模糊匹配(标题前6个字)
  709.         for (var 标题 in 商品配置) {
  710.             var 短标题 = 标题.substring(0, 6);
  711.             var nodes2 = ActionAdapter.findNodes(xml, 短标题);
  712.             if (nodes2.length > 0) {
  713.                 logger.log("模糊匹配到商品: " + 标题 + " (关键词: " + 短标题 + ")", true);
  714.                 return {
  715.                     名称: 标题,
  716.                     配置: 商品配置[标题],
  717.                     节点: nodes2[0]
  718.                 };
  719.             }
  720.         }

  721.         logger.warn("未匹配到任何商品");
  722.         return null;
  723.     },

  724.     /**
  725.      * 获取当前订单号
  726.      */
  727.     获取订单号: function() {
  728.         var xml = ActionAdapter.getXml();
  729.         if (xml) {
  730.             var patterns = [
  731.                 /订单号[::]\s*(\d{10,20})/,
  732.                 /订单编号[::]\s*(\d{10,20})/,
  733.                 /"(\d{15,20})"/
  734.             ];
  735.             for (var i = 0; i < patterns.length; i++) {
  736.                 var m = xml.match(patterns[i]);
  737.                 if (m && m[1]) {
  738.                     logger.log("提取到订单号: " + m[1]);
  739.                     return m[1];
  740.                 }
  741.             }
  742.         }
  743.         var 临时号 = "TEMP" + new Date().getTime();
  744.         logger.warn("未能提取订单号,使用临时号: " + 临时号);
  745.         return 临时号;
  746.     },

  747.     /**
  748.      * 点击订单进入详情页
  749.      */
  750.     点击进入订单: function(商品节点) {
  751.         if (!商品节点) {
  752.             ActionAdapter.clickPercent(0.5, 0.4);
  753.             sleep.second(秒 = CONFIG.操作延时);
  754.         } else {
  755.             var node = 商品节点.节点 || 商品节点;
  756.             if (node.px > 0 && node.py > 0) {
  757.                 ActionAdapter.clickPercent(node.px, node.py);
  758.             } else {
  759.                 ActionAdapter.clickPercent(0.5, 0.4);
  760.             }
  761.             sleep.second(秒 = CONFIG.操作延时);
  762.         }

  763.         // 验证是否进入订单详情
  764.         if (ActionAdapter.hasText("订单详情") || ActionAdapter.hasText("订单号") ||
  765.             ActionAdapter.hasText("联系买家") || ActionAdapter.hasText("发货") ||
  766.             ActionAdapter.hasText("阿里旺旺") || ActionAdapter.hasText("旺旺")) {
  767.             logger.success("已进入订单详情");
  768.             return true;
  769.         }
  770.         if (ActionAdapter.hasText("确认发货") || ActionAdapter.hasText("物流")) {
  771.             logger.success("已进入发货页面");
  772.             return true;
  773.         }

  774.         logger.warn("未能进入订单详情");
  775.         return false;
  776.     },

  777.     /**
  778.      * 进入买家聊天界面(点阿里旺旺)
  779.      */
  780.     进入聊天: function() {
  781.         logger.log("进入买家聊天界面...");
  782.         var 按钮文字 = ["阿里旺旺", "旺旺", "联系买家", "发消息", "联系", "留言"];
  783.         for (var i = 0; i < 按钮文字.length; i++) {
  784.             if (ActionAdapter.findTextAndClick(按钮文字[i], 2000)) {
  785.                 sleep.second(秒 = CONFIG.操作延时);
  786.                 logger.success("已进入聊天界面: " + 按钮文字[i]);
  787.                 return true;
  788.             }
  789.         }
  790.         logger.warn("未能进入聊天界面");
  791.         return false;
  792.     },

  793.     /**
  794.      * 在聊天界面发送消息
  795.      */
  796.     发送消息: function(消息内容) {
  797.         logger.log("准备发送消息...", true);

  798.         // 步骤1:点击聊天输入框
  799.         ActionAdapter.clickPercent(0.5, 0.92);
  800.         sleep.second(秒 = 1);

  801.         // 步骤2:通过剪贴板粘贴
  802.         ActionAdapter.inputText(消息内容);
  803.         sleep.second(秒 = 1);

  804.         // 步骤3:点击发送按钮
  805.         var 发送按钮文字 = ["发送", "Send", "发送消息"];
  806.         var 发送成功 = false;
  807.         for (var i = 0; i < 发送按钮文字.length; i++) {
  808.             if (ActionAdapter.findTextAndClick(发送按钮文字[i], 2000)) {
  809.                 sleep.second(秒 = 1);
  810.                 发送成功 = true;
  811.                 break;
  812.             }
  813.         }

  814.         // 百分比点击发送按钮(通常在右下角)
  815.         if (!发送成功) {
  816.             ActionAdapter.clickPercent(0.95, 0.92);
  817.             sleep.second(秒 = 1);
  818.             发送成功 = true;
  819.         }

  820.         if (发送成功) logger.success("消息已发送", true);
  821.         return 发送成功;
  822.     },

  823.     /**
  824.      * 返回待发货订单列表
  825.      */
  826.     返回列表: function() {
  827.         logger.log("返回订单列表...");
  828.         ActionAdapter.back();
  829.         sleep.second(秒 = CONFIG.操作延时);
  830.         ActionAdapter.back();
  831.         sleep.second(秒 = CONFIG.操作延时);

  832.         if (!this.在订单管理页()) {
  833.             sleep.second(秒 = 1);
  834.             if (!this.在订单管理页()) {
  835.                 logger.warn("返回后不在订单管理页,重新进入");
  836.                 this.进入待发货列表();
  837.             }
  838.         }
  839.     },

  840.     /**
  841.      * 执行发货(完整流程)
  842.      * 在订单详情页点击"发货" → 进入发货页面 → 选"无需物流" → 填写发货留言 → 确认发货
  843.      * @param {object} 发货内容 - {消息, 摘要, 卡密}
  844.      * @return {boolean} 是否发货成功
  845.      */
  846.     执行发货: function(发货内容) {
  847.         logger.log("===== 开始执行发货操作 =====", true);

  848.         // 步骤1:在订单详情页点击"发货"按钮
  849.         var 发货按钮 = ["立即发货", "去发货", "发货"];
  850.         var 点击成功 = false;
  851.         for (var i = 0; i < 发货按钮.length; i++) {
  852.             if (ActionAdapter.findTextAndClick(发货按钮[i], 3000)) {
  853.                 sleep.second(秒 = CONFIG.操作延时);
  854.                 logger.log("已点击发货按钮: " + 发货按钮[i]);
  855.                 点击成功 = true;
  856.                 break;
  857.             }
  858.         }

  859.         if (!点击成功) {
  860.             // 可能已经在发货页面了
  861.             if (ActionAdapter.hasText("确认发货") || ActionAdapter.hasText("无需物流")) {
  862.                 logger.log("已在发货页面");
  863.                 点击成功 = true;
  864.             } else {
  865.                 logger.err("未找到发货按钮");
  866.                 return false;
  867.             }
  868.         }

  869.         // 步骤2:等待发货页面加载,检查页面特征
  870.         sleep.second(秒 = 2);
  871.         var 在发货页 = ActionAdapter.hasText("确认发货") ||
  872.                        ActionAdapter.hasText("无需物流") ||
  873.                        ActionAdapter.hasText("物流公司") ||
  874.                        ActionAdapter.hasText("发货方式");
  875.         if (!在发货页) {
  876.             logger.log("发货页面未加载,再等一下...");
  877.             sleep.second(秒 = 2);
  878.             在发货页 = ActionAdapter.hasText("确认发货") ||
  879.                        ActionAdapter.hasText("无需物流") ||
  880.                        ActionAdapter.hasText("物流公司");
  881.         }

  882.         if (在发货页) {
  883.             logger.log("已进入发货页面");
  884.             ActionAdapter.dumpPageSummary();
  885.         }

  886.         // 步骤3:选择"无需物流"(虚拟商品不需要快递)
  887.         var 物流选项 = ["无需物流", "自己联系物流", "虚拟商品", "电子凭证", "不需要物流"];
  888.         var 选了物流 = false;
  889.         for (var j = 0; j < 物流选项.length; j++) {
  890.             if (ActionAdapter.findTextAndClick(物流选项[j], 2000)) {
  891.                 sleep.second(秒 = 1);
  892.                 logger.log("已选择: " + 物流选项[j]);
  893.                 选了物流 = true;
  894.                 break;
  895.             }
  896.         }

  897.         if (!选了物流) {
  898.             logger.log("未找到'无需物流'选项,可能默认已选或页面不同");
  899.             // 输出页面节点帮助排查
  900.             ActionAdapter.dumpPageSummary();
  901.         }

  902.         // 步骤4:填写发货留言/备注(把卡密/网盘信息填进去)
  903.         var 留言字段 = ["发货留言", "卖家发货备注", "发货备注", "留言", "备注", "给买家留言"];
  904.         var 填了留言 = false;
  905.         for (var k = 0; k < 留言字段.length; k++) {
  906.             if (ActionAdapter.findTextAndClick(留言字段[k], 1500)) {
  907.                 sleep.second(秒 = 1);
  908.                 // 点击后可能出现输入框,填入发货内容
  909.                 ActionAdapter.inputText(发货内容.消息);
  910.                 sleep.second(秒 = 1);
  911.                 logger.log("已填写发货留言: " + 留言字段[k]);
  912.                 填了留言 = true;
  913.                 break;
  914.             }
  915.         }

  916.         if (!填了留言) {
  917.             // 尝试找输入框直接填写
  918.             logger.log("未找到留言字段,尝试直接找输入框...");
  919.             // 发货页面的输入框通常在中部
  920.             ActionAdapter.clickPercent(0.5, 0.5);
  921.             sleep.second(秒 = 1);
  922.             ActionAdapter.inputText(发货内容.消息);
  923.             sleep.second(秒 = 1);
  924.         }

  925.         // 步骤5:点击"确认发货"按钮
  926.         logger.log("寻找确认发货按钮...");
  927.         var 确认按钮 = ["确认发货", "确定发货", "提交", "确定"];
  928.         var 确认成功 = false;
  929.         for (var m = 0; m < 确认按钮.length; m++) {
  930.             if (ActionAdapter.findTextAndClick(确认按钮[m], 3000)) {
  931.                 sleep.second(秒 = CONFIG.操作延时);
  932.                 logger.log("已点击: " + 确认按钮[m]);
  933.                 确认成功 = true;
  934.                 break;
  935.             }
  936.         }

  937.         if (!确认成功) {
  938.             // 尝试点击右下角的确认按钮(常见位置)
  939.             logger.log("未找到确认按钮文字,尝试点击右下角...");
  940.             ActionAdapter.clickPercent(0.85, 0.9);
  941.             sleep.second(秒 = CONFIG.操作延时);
  942.         }

  943.         // 步骤6:验证发货是否成功
  944.         sleep.second(秒 = 2);
  945.         var 发货成功 = ActionAdapter.hasText("发货成功") ||
  946.                        ActionAdapter.hasText("已发货") ||
  947.                        ActionAdapter.hasText("发货完成") ||
  948.                        ActionAdapter.hasText("提交成功");

  949.         // 如果没有明确的成功提示,检查是否回到了订单详情或列表
  950.         if (!发货成功) {
  951.             if (ActionAdapter.hasText("订单详情") || ActionAdapter.hasText("待发货") ||
  952.                 ActionAdapter.hasText("已发货") || ActionAdapter.hasText("催发货")) {
  953.                 logger.log("已返回订单页面,推断发货成功");
  954.                 发货成功 = true;
  955.             }
  956.         }

  957.         // 处理可能的二次确认弹窗
  958.         if (ActionAdapter.hasText("确定") || ActionAdapter.hasText("确认")) {
  959.             var 弹窗按钮 = ["确定", "确认", "是"];
  960.             for (var n = 0; n < 弹窗按钮.length; n++) {
  961.                 if (ActionAdapter.findTextAndClick(弹窗按钮[n], 1500)) {
  962.                     sleep.second(秒 = 1);
  963.                     logger.log("已确认弹窗: " + 弹窗按钮[n]);
  964.                     break;
  965.                 }
  966.             }
  967.             sleep.second(秒 = 2);
  968.             // 再次检查
  969.             if (!发货成功) {
  970.                 发货成功 = ActionAdapter.hasText("发货成功") ||
  971.                            ActionAdapter.hasText("已发货") ||
  972.                            ActionAdapter.hasText("发货完成");
  973.             }
  974.         }

  975.         if (发货成功) {
  976.             logger.success("发货操作完成!", true);
  977.         } else {
  978.             logger.warn("未能确认发货是否成功,请手动检查");
  979.             ActionAdapter.dumpPageSummary();
  980.         }

  981.         return 发货成功;
  982.     },

  983.     /**
  984.      * 标记发货(旧方法保留,仅作为后备)
  985.      */
  986.     标记发货: function() {
  987.         logger.log("尝试标记订单发货...");
  988.         var 发货按钮 = ["发货", "确认发货", "一键发货"];
  989.         for (var i = 0; i < 发货按钮.length; i++) {
  990.             if (ActionAdapter.findTextAndClick(发货按钮[i], 2000)) {
  991.                 sleep.second(秒 = CONFIG.操作延时);
  992.                 logger.success("已标记发货");
  993.                 return true;
  994.             }
  995.         }
  996.         logger.warn("未找到发货按钮");
  997.         return false;
  998.     }
  999. };

  1000. // =============================================================================
  1001. //  第八部分:发货核心流程
  1002. // =============================================================================

  1003. var 发货流程 = {

  1004.     /**
  1005.      * 处理一个订单的完整流程
  1006.      * 匹配商品 → 进详情 → 执行发货(选无需物流+确认) → 点旺旺 → 发消息 → 返回列表
  1007.      */
  1008.     处理订单: function() {
  1009.         logger.log("========== 开始处理订单 ==========");

  1010.         // 步骤1:匹配商品
  1011.         var 商品 = 千牛操作.匹配商品();
  1012.         if (!商品) {
  1013.             logger.warn("未匹配到商品,跳过");
  1014.             return false;
  1015.         }

  1016.         // 步骤2:获取订单号
  1017.         var 订单号 = 千牛操作.获取订单号();

  1018.         // 步骤3:检查是否已发货
  1019.         if (发货记录.已发货(订单号)) {
  1020.             logger.log("订单 " + 订单号 + " 已发过货,跳过");
  1021.             ActionAdapter.swipePercent(0.5, 0.6, 0.5, 0.3, 500);
  1022.             sleep.second(秒 = 1);
  1023.             return false;
  1024.         }

  1025.         // 步骤4:准备发货内容
  1026.         var 发货内容 = this.准备发货内容(商品);
  1027.         if (!发货内容) {
  1028.             logger.err("准备发货内容失败");
  1029.             return false;
  1030.         }

  1031.         // 步骤5:点击订单进入详情
  1032.         if (!千牛操作.点击进入订单(商品.节点)) {
  1033.             logger.err("进入订单详情失败");
  1034.             return false;
  1035.         }

  1036.         // 步骤6:执行发货(点发货 → 选无需物流 → 填留言 → 确认发货)
  1037.         var 发货结果 = 千牛操作.执行发货(发货内容);
  1038.         if (发货结果) {
  1039.             logger.success("订单已发货: " + 订单号, true);
  1040.         } else {
  1041.             logger.warn("发货操作未能确认成功,继续发旺旺消息");
  1042.         }

  1043.         // 步骤7:进入聊天界面(点阿里旺旺,给买家发卡密/网盘信息)
  1044.         // 此时可能需要返回到订单详情页
  1045.         if (!ActionAdapter.hasText("订单详情") && !ActionAdapter.hasText("阿里旺旺")) {
  1046.             // 如果发货后不在订单详情,尝试返回
  1047.             ActionAdapter.back();
  1048.             sleep.second(秒 = CONFIG.操作延时);
  1049.         }

  1050.         if (千牛操作.进入聊天()) {
  1051.             // 步骤8:发送消息
  1052.             if (千牛操作.发送消息(发货内容.消息)) {
  1053.                 logger.success("旺旺消息已发送", true);
  1054.                 // 步骤9:返回订单详情
  1055.                 ActionAdapter.back();
  1056.                 sleep.second(秒 = CONFIG.操作延时);
  1057.             } else {
  1058.                 logger.err("发送旺旺消息失败");
  1059.             }
  1060.         } else {
  1061.             logger.warn("未能进入旺旺聊天(发货已完成,消息发送失败)");
  1062.         }

  1063.         // 步骤10:记录发货
  1064.         发货记录.记录(订单号, 商品.名称, 发货内容.摘要);

  1065.         // 步骤11:上报服务器
  1066.         if (CONFIG.使用服务器 && 发货内容.卡密) {
  1067.             卡密管理.上报已使用(商品.名称, 发货内容.卡密, 订单号);
  1068.         }

  1069.         logger.success("订单 " + 订单号 + " 处理完成: " + 商品.名称, true);
  1070.         logger.success("今日已发货: " + 发货记录.今日数量() + " 单");

  1071.         // 步骤12:返回列表
  1072.         千牛操作.返回列表();
  1073.         return true;
  1074.     },

  1075.     准备发货内容: function(商品) {
  1076.         var 配置 = 商品.配置;
  1077.         var 话术 = 配置.发货话术;
  1078.         var 卡密 = "";
  1079.         var 网盘 = 配置.网盘链接 || "";
  1080.         var 摘要 = "";

  1081.         if (配置.发货类型 === "卡密") {
  1082.             卡密 = 卡密管理.取卡密(配置);
  1083.             if (!卡密) { logger.err("无可用卡密!商品: " + 商品.名称); return null; }
  1084.             话术 = 话术.replace("{卡密}", 卡密).replace("{网盘}", 网盘);
  1085.             摘要 = "卡密:" + 卡密;
  1086.         } else if (配置.发货类型 === "网盘") {
  1087.             话术 = 话术.replace("{网盘}", 网盘).replace("{卡密}", "");
  1088.             摘要 = "网盘:" + 网盘.substring(0, 20);
  1089.         } else if (配置.发货类型 === "卡密+网盘") {
  1090.             卡密 = 卡密管理.取卡密(配置);
  1091.             if (!卡密) { logger.err("无可用卡密!商品: " + 商品.名称); return null; }
  1092.             话术 = 话术.replace("{卡密}", 卡密).replace("{网盘}", 网盘);
  1093.             摘要 = "卡密:" + 卡密 + " 网盘:" + 网盘.substring(0, 15);
  1094.         }
  1095.         return { 消息: 话术, 摘要: 摘要, 卡密: 卡密 };
  1096.     },

  1097.     /**
  1098.      * 处理当前所有可见的待发货订单
  1099.      */
  1100.     处理当前订单: function() {
  1101.         var 处理数量 = 0;
  1102.         var 连续无订单 = 0;

  1103.         for (var i = 0; i < 50; i++) {
  1104.             if (!千牛操作.有新订单()) {
  1105.                 连续无订单++;
  1106.                 if (连续无订单 >= 2) {
  1107.                     logger.log("没有更多待发货订单");
  1108.                     break;
  1109.                 }
  1110.                 ActionAdapter.swipePercent(0.5, 0.6, 0.5, 0.3, 500);
  1111.                 sleep.second(秒 = 1);
  1112.                 continue;
  1113.             }
  1114.             连续无订单 = 0;
  1115.             logger.log("--- 处理第 " + (i + 1) + " 个订单 ---");

  1116.             if (this.处理订单()) 处理数量++;
  1117.             sleep.second(秒 = 1);
  1118.         }

  1119.         logger.success("本轮处理完成,共发货 " + 处理数量 + " 单", true);
  1120.         return 处理数量;
  1121.     }
  1122. };

  1123. // =============================================================================
  1124. //  第九部分:保活心跳(无碍版,不用shell)
  1125. // =============================================================================

  1126. var 保活系统 = {
  1127.     线程: null,
  1128.     运行中: false,

  1129.     start: function() {
  1130.         if (!CONFIG.启用保活) return;
  1131.         this.运行中 = true;
  1132.         this.线程 = new thread().runJsCode(function() {
  1133.             while (保活系统.运行中) {
  1134.                 try {
  1135.                     // 无碍模式没有shell,用getXml检查千牛是否在前台
  1136.                     var xml = ActionAdapter.getXml();
  1137.                     if (!xml || xml === "") {
  1138.                         logger.warn("千牛可能未运行,尝试重启...");
  1139.                         app.openApp(CONFIG.千牛包名);
  1140.                         sleep.second(秒 = 8);
  1141.                     }
  1142.                     logger.log("保活心跳: 今日发货 " + 发货记录.今日数量() + " 单");
  1143.                 } catch (e) {}
  1144.                 sleep.second(秒 = CONFIG.保活间隔);
  1145.             }
  1146.         });
  1147.         logger.log("保活线程已启动");
  1148.     },

  1149.     stop: function() {
  1150.         this.运行中 = false;
  1151.         logger.log("保活线程已停止");
  1152.     }
  1153. };

  1154. // =============================================================================
  1155. //  第十部分:库存预警
  1156. // =============================================================================

  1157. var 库存预警 = {
  1158.     检查全部: function() {
  1159.         var 预警商品 = [];
  1160.         for (var 商品名 in 商品配置) {
  1161.             var 配置 = 商品配置[商品名];
  1162.             if (配置.发货类型 === "卡密" || 配置.发货类型 === "卡密+网盘") {
  1163.                 var 库存 = 卡密管理.检查库存(配置);
  1164.                 if (库存 >= 0 && 库存 < 5) {
  1165.                     预警商品.push(商品名 + " (剩余" + 库存 + "个)");
  1166.                     logger.warn("库存预警: " + 商品名 + " 仅剩 " + 库存 + " 个卡密!");
  1167.                 }
  1168.             }
  1169.         }
  1170.         return 预警商品;
  1171.     }
  1172. };

  1173. // =============================================================================
  1174. //  第十一部分:主控制流程(持续监控模式)
  1175. // =============================================================================

  1176. var 主控 = {
  1177.     运行中: false,
  1178.     统计: { 总轮次: 0, 总发货: 0, 开始时间: null, 最后检查: null },

  1179.     初始化: function() {
  1180.         logger.log("========================================");
  1181.         logger.log("  淘宝千牛自动发货系统 v3.0");
  1182.         logger.log("  精简版 · 仅无碍模式 · 完整标题匹配");
  1183.         logger.log("========================================");

  1184.         logger.init();

  1185.         if (!ActionAdapter.init()) {
  1186.             logger.err("无碍模式初始化失败,系统退出");
  1187.             return false;
  1188.         }

  1189.         try {
  1190.             var dir = new File(CONFIG.卡密目录);
  1191.             if (!dir.exists()) dir.mkdirs();
  1192.         } catch (e) {}

  1193.         var 预警 = 库存预警.检查全部();
  1194.         if (预警.length > 0) logger.warn("库存预警: " + 预警.join(", "));

  1195.         logger.log("商品数量: " + Object.keys(商品配置).length);
  1196.         logger.log("监控间隔: " + CONFIG.轮询间隔 + "秒");

  1197.         this.统计.开始时间 = new Date();
  1198.         logger.success("系统初始化完成", true);
  1199.         return true;
  1200.     },

  1201.     进入监控状态: function() {
  1202.         千牛操作.确保运行();
  1203.         sleep.second(秒 = 2);

  1204.         if (!千牛操作.进入待发货列表()) {
  1205.             logger.err("进入待发货列表失败!", true);
  1206.             return false;
  1207.         }

  1208.         if (!千牛操作.待发货已选中()) {
  1209.             ActionAdapter.findTextAndClick("待发货", 3000);
  1210.             sleep.second(秒 = CONFIG.操作延时);
  1211.         }

  1212.         logger.success("已进入待发货列表,开始持续监控", true);
  1213.         return true;
  1214.     },

  1215.     监控一轮: function() {
  1216.         this.统计.总轮次++;
  1217.         logger.addRound();

  1218.         try {
  1219.             if (!千牛操作.在订单管理页()) {
  1220.                 logger.warn("不在订单管理页面,重新进入");
  1221.                 this.进入监控状态();
  1222.                 return;
  1223.             }

  1224.             if (千牛操作.有新订单()) {
  1225.                 logger.success("检测到新订单!开始处理...", true);
  1226.                 var 发货数 = 发货流程.处理当前订单();
  1227.                 this.统计.总发货 += 发货数;
  1228.                 for (var i = 0; i < 发货数; i++) logger.addShip();

  1229.                 if (发货数 > 0) {
  1230.                     logger.success("本轮发货 " + 发货数 + " 单,累计 " + this.统计.总发货 + " 单", true);
  1231.                 }

  1232.                 if (!千牛操作.在订单管理页()) this.进入监控状态();
  1233.             } else {
  1234.                 if (this.统计.总轮次 % 10 === 0) {
  1235.                     logger.log("监控中... 暂无新订单 (轮次:" + this.统计.总轮次 + ")");
  1236.                 }
  1237.                 if (this.统计.总轮次 % 20 === 0) 库存预警.检查全部();
  1238.             }
  1239.         } catch (e) {
  1240.             logger.err("监控异常: " + e, true);
  1241.             logger.log("异常恢复:重新进入待发货列表...", true);
  1242.             this.进入监控状态();
  1243.         }
  1244.         this.统计.最后检查 = new Date();
  1245.     },

  1246.     主循环: function() {
  1247.         this.运行中 = true;
  1248.         logger.success("系统进入持续监控模式", true);
  1249.         logger.log("停在待发货列表页面,有人下单自动发货", true);

  1250.         while (this.运行中) {
  1251.             try { this.监控一轮(); } catch (e) { logger.err("主循环异常: " + e, true); }

  1252.             var 等待秒数 = CONFIG.轮询间隔;
  1253.             for (var i = 等待秒数; i > 0; i--) {
  1254.                 if (!this.运行中) break;
  1255.                 if (i % 10 === 0 || i <= 3) { try { logger._更新状态(); } catch (e2) {} }
  1256.                 sleep.second(秒 = 1);
  1257.             }
  1258.         }
  1259.     },

  1260.     start: function() {
  1261.         if (!this.初始化()) return;
  1262.         保活系统.start();

  1263.         if (!this.进入监控状态()) {
  1264.             logger.err("无法进入监控状态,5秒后重试...", true);
  1265.             sleep.second(秒 = 5);
  1266.             if (!this.进入监控状态()) {
  1267.                 logger.err("重试失败,系统退出", true);
  1268.                 return;
  1269.             }
  1270.         }
  1271.         this.主循环();
  1272.     },

  1273.     stop: function() {
  1274.         this.运行中 = false;
  1275.         保活系统.stop();
  1276.         logger.log("系统已停止");
  1277.     }
  1278. };

  1279. // =============================================================================
  1280. //  第十二部分:启动入口
  1281. // =============================================================================

  1282. function 确保目录存在(路径) {
  1283.     try {
  1284.         var dir = new File(路径);
  1285.         if (!dir.exists()) dir.mkdirs();
  1286.     } catch (e) {}
  1287. }

  1288. function 创建卡密示例() {
  1289.     try {
  1290.         for (var 商品名 in 商品配置) {
  1291.             var 配置 = 商品配置[商品名];
  1292.             if (配置.卡密文件 && 配置.卡密文件 !== "") {
  1293.                 var file = new File(配置.卡密文件);
  1294.                 if (!file.exists()) {
  1295.                     var writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
  1296.                     writer.write("# " + 商品名 + " 卡密文件\n");
  1297.                     writer.write("# 每行一个卡密,用过的自动删除\n\n");
  1298.                     writer.write("Q5J3HHT8-NJMF\n");
  1299.                     writer.write("KA8CPLJP-DNCM\n");
  1300.                     writer.write("FYYEGC6D-KPCA\n");
  1301.                     writer.close();
  1302.                     logger.log("已创建卡密示例文件: " + 配置.卡密文件);
  1303.                 }
  1304.             }
  1305.         }
  1306.     } catch (e) {
  1307.         logger.err("创建卡密示例失败: " + e);
  1308.     }
  1309. }

  1310. // =============================================================================
  1311. //  启动
  1312. // =============================================================================

  1313. 确保目录存在(CONFIG.卡密目录);
  1314. 确保目录存在("/sdcard/");
  1315. 创建卡密示例();

  1316. if (CONFIG.调试模式 === "debug") {
  1317.     // ===== 调试模式:输出页面节点,验证XML解析 =====
  1318.     logger.init();
  1319.     logger.log("========================================");
  1320.     logger.log("  调试模式:验证XML解析");
  1321.     logger.log("========================================");

  1322.     if (!ActionAdapter.init()) {
  1323.         logger.err("无碍模式初始化失败");
  1324.     } else {
  1325.         千牛操作.启动千牛();
  1326.         sleep.second(秒 = 3);

  1327.         logger.log("===== 当前页面节点摘要 =====");
  1328.         ActionAdapter.dumpPageSummary();

  1329.         logger.log("===== 测试查找'待发货'节点 =====");
  1330.         var xml = ActionAdapter.getXml();
  1331.         var nodes = ActionAdapter.findNodes(xml, "待发货");
  1332.         logger.log("查找'待发货'结果: " + nodes.length + " 个节点");
  1333.         for (var i = 0; i < nodes.length; i++) {
  1334.             logger.log("  节点" + i + ": text=[" + nodes[i].text + "] desc=[" + nodes[i].desc + "] px=" + nodes[i].px + " py=" + nodes[i].py + " clickable=" + nodes[i].clickable);
  1335.             if (nodes[i].clickable === "false") {
  1336.                 var parent = ActionAdapter.findClickableParent(nodes[i], xml);
  1337.                 if (parent) {
  1338.                     logger.log("    -> 父节点: px=" + parent.px + " py=" + parent.py + " clickable=" + parent.clickable);
  1339.                 }
  1340.             }
  1341.         }

  1342.         logger.log("===== 测试商品标题匹配 =====");
  1343.         var 商品结果 = 千牛操作.匹配商品();
  1344.         if (商品结果) {
  1345.             logger.success("匹配到商品: " + 商品结果.名称);
  1346.         } else {
  1347.             logger.log("当前页面未匹配到商品(正常,列表为空时无商品)");
  1348.         }

  1349.         logger.success("调试完成!", true);
  1350.         logger.log("如需测试真实发货,请将 CONFIG.调试模式 设为 'test'");
  1351.         logger.log("如需正常监控,请将 CONFIG.调试模式 设为 false");
  1352.     }

  1353. } else if (CONFIG.调试模式 === "test") {
  1354.     // ===== 测试模式:处理一个真实订单,验证完整发货流程 =====
  1355.     logger.init();
  1356.     logger.log("========================================");
  1357.     logger.log("  测试模式:处理一个真实订单");
  1358.     logger.log("========================================");

  1359.     if (!ActionAdapter.init()) {
  1360.         logger.err("无碍模式初始化失败");
  1361.     } else {
  1362.         // 启动千牛
  1363.         千牛操作.启动千牛();
  1364.         sleep.second(秒 = 3);

  1365.         // 进入待发货列表
  1366.         if (!千牛操作.进入待发货列表()) {
  1367.             logger.err("进入待发货列表失败!", true);
  1368.         } else {
  1369.             logger.log("===== 检查是否有待发货订单 =====");
  1370.             if (千牛操作.有新订单()) {
  1371.                 logger.success("检测到订单!开始处理...", true);

  1372.                 // 输出页面节点,看看订单列表长什么样
  1373.                 logger.log("===== 订单列表节点摘要 =====");
  1374.                 ActionAdapter.dumpPageSummary();

  1375.                 // 尝试匹配商品
  1376.                 logger.log("===== 尝试匹配商品 =====");
  1377.                 var 商品 = 千牛操作.匹配商品();
  1378.                 if (商品) {
  1379.                     logger.success("匹配到商品: " + 商品.名称, true);

  1380.                     // 处理这个订单
  1381.                     logger.log("===== 开始处理订单 =====");
  1382.                     var 结果 = 发货流程.处理订单();
  1383.                     if (结果) {
  1384.                         logger.success("测试发货成功!", true);
  1385.                     } else {
  1386.                         logger.err("测试发货失败,请检查日志", true);
  1387.                     }
  1388.                 } else {
  1389.                     logger.warn("未匹配到商品");
  1390.                     logger.log("请检查商品配置中的标题是否与淘宝订单中的商品标题一致");
  1391.                     logger.log("===== 当前页面所有节点 =====");
  1392.                     ActionAdapter.dumpPageSummary();
  1393.                 }
  1394.             } else {
  1395.                 logger.log("当前没有待发货订单,请先在淘宝下一单测试");
  1396.                 logger.log("===== 当前页面节点 =====");
  1397.                 ActionAdapter.dumpPageSummary();
  1398.             }
  1399.         }

  1400.         logger.log("========================================");
  1401.         logger.log("测试完成!");
  1402.         logger.log("如需正常监控,请将 CONFIG.调试模式 设为 false");
  1403.         logger.log("========================================");
  1404.     }

  1405. } else {
  1406.     // ===== 正常模式:24小时持续监控 =====
  1407.     主控.start();
  1408. }
复制代码







欢迎光临 B2B网络软件 (http://bbs.niubt.cn/) Powered by Discuz! X3.2