B2B网络软件

标题: 示例千问自动采集文章保存记事本 [打印本页]

作者: YYPOST群发软件    时间: 1 小时前
标题: 示例千问自动采集文章保存记事本
示例千问自动采集文章保存记事本
示例千问自动采集文章保存记事本 B2B网络软件

  1. // AIWROK 自动化流程:启动千问 → 输入问题 → 点击发送 → 等待AI回答 → 采集回答内容 → 保存到文件
  2. // 全程使用 agent.getXml() 获取界面源码
  3. // 包名:com.aliyun.tongyi(千问)

  4. print.log("===== 千问对话采集流程启动 =====");
  5. print.time();

  6. // ========== 配置区 ==========
  7. var 千问包名 = "com.aliyun.tongyi";

  8. // 输出文件路径(JSON格式,UTF-8编码)
  9. var 输出文件路径 = "/sdcard/千问对话记录.txt";

  10. // 要发送的问题列表(按顺序发送)
  11. var 要发送的问题列表 = [
  12.     "你好,请用一句话介绍你自己",
  13.     "中国平安股票最近走势如何?",
  14.     "招商银行持有3年卖出需要交股息税吗?"
  15. ];

  16. // 发送后等待AI回答的最大秒数(整体超时,最长35秒)
  17. var 等待回答超时秒 = 35;

  18. // 检测到新回答出现后,再等待多少秒让回答尽量输出完整(然后直接返回)
  19. var 回答输出等待秒 = 12;

  20. // 每个问题之间的间隔秒数
  21. var 问题间隔秒 = 2;

  22. // ========== 获取设备信息 ==========
  23. var 屏幕宽 = screen.getScreenWidth();
  24. var 屏幕高 = screen.getScreenHeight();
  25. print.log("屏幕尺寸:" + 屏幕宽 + " x " + 屏幕高);
  26. print.log("代理状态:" + (agent.isOpen() ? "已开启" : "未开启"));

  27. // ========== 工具函数 ==========

  28. // 获取 UI XML 字符串(使用 agent.getXml(),强制转为字符串)
  29. function 获取UI() {
  30.     var xml = agent.getXml();
  31.     // agent.getXml() 返回对象,需强制转为字符串
  32.     return "" + xml;
  33. }

  34. // 字符串长度兼容函数(避免 Java String 对象 .length() 方法问题)
  35. function 字符串长度(s) {
  36.     var str = "" + s;
  37.     return str.length;
  38. }

  39. // HTML实体解码(千问TextView中的换行符等会被编码为
  40. ' 等)
  41. function 解码HTML实体(s) {
  42.     var str = "" + s;
  43.     str = str.replace(/
  44. /g, "\n");
  45.     str = str.replace(/
  46. /g, "\r");
  47.     str = str.replace(/        /g, "\t");
  48.     str = str.replace(/'/g, "'");
  49.     str = str.replace(/"/g, '"');
  50.     str = str.replace(/"/g, '"');
  51.     str = str.replace(/'/g, "'");
  52.     str = str.replace(/&/g, "&");
  53.     str = str.replace(/</g, "<");
  54.     str = str.replace(/>/g, ">");
  55.     return str;
  56. }

  57. // 采集千问 RecyclerView 中所有长文本 TextView(即AI回答)
  58. // 参数 严格过滤: true=排除短文本按钮等, false=不过滤(用于检测新回答出现,更灵敏)
  59. function 采集AI回答列表(xml, 严格过滤) {
  60.     if (严格过滤 === undefined) 严格过滤 = true;
  61.     var xmlStr = "" + xml;
  62.     var 回答列表 = [];

  63.     // 定位 RecyclerView 节点(千问消息列表使用 RecyclerView)
  64.     var rvPattern = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
  65.     var rvMatch;
  66.     var 消息列表区域 = null;

  67.     while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
  68.         var rx1 = parseInt(rvMatch[1]);
  69.         var ry1 = parseInt(rvMatch[2]);
  70.         var rx2 = parseInt(rvMatch[3]);
  71.         var ry2 = parseInt(rvMatch[4]);
  72.         var 高度 = ry2 - ry1;
  73.         var 宽度 = rx2 - rx1;

  74.         if (高度 > 500 && 宽度 > 屏幕宽 * 0.8) {
  75.             消息列表区域 = {x1: rx1, y1: ry1, x2: rx2, y2: ry2};
  76.             break;
  77.         }
  78.     }

  79.     if (!消息列表区域) {
  80.         print.log("⚠ 未找到消息列表RecyclerView节点");
  81.         return 回答列表;
  82.     }

  83.     print.log("✓ 找到消息列表区域:[" + 消息列表区域.x1 + "," + 消息列表区域.y1 + "][" + 消息列表区域.x2 + "," + 消息列表区域.y2 + "]");

  84.     // 提取消息列表区域内的所有 TextView(两阶段匹配:先找节点,再提取属性)
  85.     var tvPattern = /<node[^>]*?class="android\.widget\.TextView"[^>]*?\/?>/g;
  86.     var tvMatch;

  87.     while ((tvMatch = tvPattern.exec(xmlStr)) !== null) {
  88.         var tvNode = tvMatch[0];

  89.         // 从节点中提取文本(支持双引号和单引号)
  90.         var textMatch = tvNode.match(/text="([^"]+)"/);
  91.         if (!textMatch) {
  92.             textMatch = tvNode.match(/text='([^']+)'/);
  93.         }
  94.         if (!textMatch) continue;
  95.         var 文本 = textMatch[1];

  96.         // 从节点中提取边界
  97.         var boundsMatch = tvNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  98.         if (!boundsMatch) continue;
  99.         var tx1 = parseInt(boundsMatch[1]);
  100.         var ty1 = parseInt(boundsMatch[2]);
  101.         var tx2 = parseInt(boundsMatch[3]);
  102.         var ty2 = parseInt(boundsMatch[4]);

  103.         // 判断该 TextView 是否在消息列表区域内
  104.         var 在区域内 = (tx1 >= 消息列表区域.x1 && tx2 <= 消息列表区域.x2 &&
  105.                         ty1 >= 消息列表区域.y1 && ty2 <= 消息列表区域.y2);

  106.         if (!在区域内) continue;

  107.         var 解码文本 = 解码HTML实体(文本);

  108.         // 排除用户消息特征:位于消息列表顶部区域(y坐标较小)且是单行短文本
  109.         var 是用户消息 = (ty1 < 消息列表区域.y1 + 500) && (解码文本.indexOf("\n") === -1) && (字符串长度(解码文本) < 100);
  110.         
  111.         // 排除搜索步骤文本(包含"查询"、"检索"、"调研"等关键词的是搜索计划,不是最终回答)
  112.         var 是搜索步骤 = (解码文本.indexOf("查询") !== -1 || 解码文本.indexOf("检索") !== -1 ||
  113.                           解码文本.indexOf("调研") !== -1 || 解码文本.indexOf("确认") !== -1 ||
  114.                           解码文本.indexOf("分析") !== -1 || 解码文本.indexOf("查找") !== -1) &&
  115.                          (字符串长度(解码文本) < 500);
  116.         
  117.         // 排除视频推荐区域的文本(视频推荐通常在AI回答下方,y坐标较大且有特定特征)
  118.         // AI回答通常在消息列表顶部区域,视频推荐在下方
  119.         var 消息列表高度 = 消息列表区域.y2 - 消息列表区域.y1;
  120.         var 是视频推荐 = (ty1 > 消息列表区域.y1 + 消息列表高度 * 0.3) &&
  121.                          (解码文本.indexOf("笔记") !== -1 || 解码文本.indexOf("作者") !== -1 ||
  122.                           字符串长度(解码文本) < 30);
  123.         
  124.         if (严格过滤) {
  125.             if (!是用户消息 && !是搜索步骤 && !是视频推荐 && (解码文本.indexOf("\n") !== -1 || 字符串长度(解码文本) > 50)) {
  126.                 回答列表.push(解码文本);
  127.             }
  128.         } else {
  129.             if (!是用户消息 && !是搜索步骤 && !是视频推荐 && 字符串长度(解码文本) > 0) {
  130.                 回答列表.push(解码文本);
  131.             }
  132.         }
  133.     }

  134.     return 回答列表;
  135. }

  136. // 查找输入框中心坐标(查找 hint 文本为"发消息或按住说话..."的 TextView)
  137. function 查找输入框中心(xml) {
  138.     var xmlStr = "" + xml;

  139.     var inputIdx = xmlStr.indexOf('text="发消息或按住说话..."');
  140.     if (inputIdx === -1) {
  141.         inputIdx = xmlStr.indexOf("text='发消息或按住说话...'");
  142.     }
  143.     if (inputIdx !== -1) {
  144.         var 段 = xmlStr.substring(inputIdx);
  145.         var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  146.         if (m) {
  147.             print.log("✓ 输入框定位:hint文本匹配");
  148.             return {
  149.                 x: (parseInt(m[1]) + parseInt(m[3])) / 2,
  150.                 y: (parseInt(m[2]) + parseInt(m[4])) / 2
  151.             };
  152.         }
  153.     }

  154.     var editTextPattern = /<node[^>]*?class="android\.widget\.EditText"[^>]*?\/?>/g;
  155.     var etMatch;
  156.     while ((etMatch = editTextPattern.exec(xmlStr)) !== null) {
  157.         var etNode = etMatch[0];
  158.         var boundsMatch = etNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  159.         if (boundsMatch) {
  160.             var ex1 = parseInt(boundsMatch[1]);
  161.             var ey1 = parseInt(boundsMatch[2]);
  162.             var ex2 = parseInt(boundsMatch[3]);
  163.             var ey2 = parseInt(boundsMatch[4]);
  164.             
  165.             if (ey1 > 屏幕高 * 0.8) {
  166.                 print.log("✓ 输入框定位:EditText匹配");
  167.                 return {
  168.                     x: (ex1 + ex2) / 2,
  169.                     y: (ey1 + ey2) / 2
  170.                 };
  171.             }
  172.         }
  173.     }

  174.     var viewGroupPattern = /<node[^>]*?class="android\.view\.ViewGroup"[^>]*?clickable="true"[^>]*?\/?>/g;
  175.     var vgMatch;
  176.     while ((vgMatch = viewGroupPattern.exec(xmlStr)) !== null) {
  177.         var vgNode = vgMatch[0];
  178.         var boundsMatch = vgNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  179.         if (boundsMatch) {
  180.             var vx1 = parseInt(boundsMatch[1]);
  181.             var vy1 = parseInt(boundsMatch[2]);
  182.             var vx2 = parseInt(boundsMatch[3]);
  183.             var vy2 = parseInt(boundsMatch[4]);
  184.             
  185.             var vWidth = vx2 - vx1;
  186.             var vHeight = vy2 - vy1;
  187.             
  188.             if (vy1 > 屏幕高 * 0.8 && vWidth > 屏幕宽 * 0.4 && vWidth < 屏幕宽 * 0.8) {
  189.                 print.log("✓ 输入框定位:ViewGroup匹配");
  190.                 return {
  191.                     x: (vx1 + vx2) / 2,
  192.                     y: (vy1 + vy2) / 2
  193.                 };
  194.             }
  195.         }
  196.     }

  197.     print.log("⚠ 未找到输入框,使用兜底坐标");
  198.     return {
  199.         x: 屏幕宽 * 0.4,
  200.         y: 屏幕高 * 0.92
  201.     };
  202. }

  203. // 查找发送按钮坐标
  204. // 千问输入框下方右侧的按钮,输入文字后会变成发送按钮
  205. function 查找发送按钮中心(xml) {
  206.     var xmlStr = "" + xml;

  207.     // 1. 优先查找文字"发送"
  208.     var sendTextIdx = xmlStr.indexOf('text="发送"');
  209.     if (sendTextIdx === -1) {
  210.         sendTextIdx = xmlStr.indexOf("text='发送'");
  211.     }
  212.     if (sendTextIdx !== -1) {
  213.         var 段 = xmlStr.substring(sendTextIdx);
  214.         var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  215.         if (m) {
  216.             print.log("✓ 发送按钮定位:文本匹配");
  217.             return {
  218.                 x: (parseInt(m[1]) + parseInt(m[3])) / 2,
  219.                 y: (parseInt(m[2]) + parseInt(m[4])) / 2
  220.             };
  221.         }
  222.     }

  223.     // 2. 查找屏幕底部区域的点击区域(FrameLayout)
  224.     var clickablePattern = /<node[^>]*?clickable="true"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"[^>]*?\/?>/g;
  225.     var clickMatch;
  226.     var candidates = [];

  227.     while ((clickMatch = clickablePattern.exec(xmlStr)) !== null) {
  228.         var cx1 = parseInt(clickMatch[1]);
  229.         var cy1 = parseInt(clickMatch[2]);
  230.         var cx2 = parseInt(clickMatch[3]);
  231.         var cy2 = parseInt(clickMatch[4]);

  232.         if (cy1 > 屏幕高 * 0.85) {
  233.             candidates.push({
  234.                 x1: cx1, y1: cy1, x2: cx2, y2: cy2,
  235.                 x: (cx1 + cx2) / 2,
  236.                 y: (cy1 + cy2) / 2
  237.             });
  238.         }
  239.     }

  240.     if (candidates.length > 0) {
  241.         candidates.sort(function(a, b) {
  242.             return b.x1 - a.x1;
  243.         });
  244.         print.log("✓ 找到" + candidates.length + "个候选按钮,选择最右侧的,坐标(" + Math.round(candidates[0].x) + "," + Math.round(candidates[0].y) + ")");
  245.         return {
  246.             x: candidates[0].x,
  247.             y: candidates[0].y
  248.         };
  249.     }

  250.     // 3. 查找屏幕底部区域的 FrameLayout(千问发送按钮特征)
  251.     var flPattern = /<node[^>]*?class="android\.widget\.FrameLayout"[^>]*?\/?>/g;
  252.     var flMatch;
  253.     var flCandidates = [];

  254.     while ((flMatch = flPattern.exec(xmlStr)) !== null) {
  255.         var flNode = flMatch[0];
  256.         
  257.         if (flNode.indexOf('clickable="true"') === -1) continue;
  258.         
  259.         var boundsMatch = flNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  260.         if (!boundsMatch) continue;
  261.         
  262.         var fx1 = parseInt(boundsMatch[1]);
  263.         var fy1 = parseInt(boundsMatch[2]);
  264.         var fx2 = parseInt(boundsMatch[3]);
  265.         var fy2 = parseInt(boundsMatch[4]);

  266.         if (fy1 > 屏幕高 * 0.85) {
  267.             flCandidates.push({
  268.                 x1: fx1, y1: fy1, x2: fx2, y2: fy2,
  269.                 x: (fx1 + fx2) / 2,
  270.                 y: (fy1 + fy2) / 2
  271.             });
  272.         }
  273.     }

  274.     if (flCandidates.length > 0) {
  275.         flCandidates.sort(function(a, b) {
  276.             return b.x1 - a.x1;
  277.         });
  278.         print.log("✓ 找到" + flCandidates.length + "个FrameLayout候选,选择最右侧的,坐标(" + Math.round(flCandidates[0].x) + "," + Math.round(flCandidates[0].y) + ")");
  279.         return {
  280.             x: flCandidates[0].x,
  281.             y: flCandidates[0].y
  282.         };
  283.     }

  284.     // 4. 兜底:基于屏幕百分比定位发送按钮(输入框下方右侧)
  285.     print.log("⚠ 使用兜底策略定位发送按钮,坐标(" + Math.round(屏幕宽 * 0.92) + "," + Math.round(屏幕高 * 0.92) + ")");
  286.     return {
  287.         x: 屏幕宽 * 0.92,
  288.         y: 屏幕高 * 0.92
  289.     };
  290. }

  291. // 粘贴文本到当前焦点输入框(剪贴板法,兼容中文)
  292. function 粘贴文本(文本) {
  293.     try {
  294.         Clipboard.copy("" + 文本);
  295.     } catch (e) {
  296.         print.log("⚠ Clipboard.copy 失败:" + e.message);
  297.     }
  298.     sleep.millisecond(毫秒 = 300);

  299.     // 双保险:通过 shell 写剪贴板
  300.     try {
  301.         agent.shell("service call clipboard 3 i32 1 s16 '" + 文本 + "' i32 0");
  302.     } catch (e2) {
  303.         // 忽略错误
  304.     }
  305.     sleep.millisecond(毫秒 = 300);

  306.     // 触发粘贴:KEYCODE_PASTE = 279
  307.     agent.shell("input keyevent 279");
  308.     sleep.second(秒 = 1);
  309.     print.log("✓ 已粘贴文本:" + 文本);
  310. }

  311. // 查找并点击AI回答底部的展开按钮(向下箭头)
  312. // 千问长回答会折叠显示,需要点击底部向下箭头才能看到完整内容
  313. function 查找并点击展开按钮(xml) {
  314.     var xmlStr = "" + xml;
  315.    
  316.     // 1. 查找消息列表底部中间的小区域clickable节点(向下箭头特征)
  317.     var rvPattern = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
  318.     var rvMatch;
  319.     while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
  320.         var rx1 = parseInt(rvMatch[1]);
  321.         var ry1 = parseInt(rvMatch[2]);
  322.         var rx2 = parseInt(rvMatch[3]);
  323.         var ry2 = parseInt(rvMatch[4]);
  324.         var 高度 = ry2 - ry1;
  325.         if (高度 > 500) {
  326.             var viewPattern = /<node[^>]*?clickable="true"[^>]*?\/?>/g;
  327.             var viewMatch;
  328.             while ((viewMatch = viewPattern.exec(xmlStr)) !== null) {
  329.                 var vNode = viewMatch[0];
  330.                 var boundsMatch = vNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  331.                 if (!boundsMatch) continue;
  332.                
  333.                 var vx1 = parseInt(boundsMatch[1]);
  334.                 var vy1 = parseInt(boundsMatch[2]);
  335.                 var vx2 = parseInt(boundsMatch[3]);
  336.                 var vy2 = parseInt(boundsMatch[4]);
  337.                
  338.                 var vWidth = vx2 - vx1;
  339.                 var vHeight = vy2 - vy1;
  340.                
  341.                 var 在区域内 = (vx1 >= rx1 && vx2 <= rx2 && vy1 >= ry1 && vy2 <= ry2);
  342.                 var 是小按钮 = (vWidth < 200 && vHeight < 200);
  343.                 var 在底部 = (vy1 > ry1 + 高度 * 0.7);
  344.                
  345.                 if (在区域内 && 是小按钮 && 在底部) {
  346.                     var centerX = (vx1 + vx2) / 2;
  347.                     var centerY = (vy1 + vy2) / 2;
  348.                     agent.click(centerX, centerY);
  349.                     print.log("✓ 已点击展开箭头(消息列表底部小按钮),坐标(" + Math.round(centerX) + "," + Math.round(centerY) + ")");
  350.                     sleep.second(秒 = 2);
  351.                     return true;
  352.                 }
  353.             }
  354.             break;
  355.         }
  356.     }

  357.     // 2. 查找文本"查看全部"的按钮
  358.     var textExpandIdx = xmlStr.indexOf('text="查看全部"');
  359.     if (textExpandIdx === -1) {
  360.         textExpandIdx = xmlStr.indexOf("text='查看全部'");
  361.     }
  362.     if (textExpandIdx !== -1) {
  363.         var 段 = xmlStr.substring(textExpandIdx);
  364.         var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  365.         if (m) {
  366.             var x = (parseInt(m[1]) + parseInt(m[3])) / 2;
  367.             var y = (parseInt(m[2]) + parseInt(m[4])) / 2;
  368.             agent.click(x, y);
  369.             print.log("✓ 已点击查看全部按钮,坐标(" + Math.round(x) + "," + Math.round(y) + ")");
  370.             sleep.second(秒 = 2);
  371.             return true;
  372.         }
  373.     }

  374.     // 3. 查找 content-desc 含"展开"或"更多"的按钮
  375.     var expandIdx = xmlStr.indexOf('content-desc="展开"');
  376.     if (expandIdx === -1) {
  377.         expandIdx = xmlStr.indexOf('content-desc="更多"');
  378.     }
  379.     if (expandIdx !== -1) {
  380.         var 段2 = xmlStr.substring(expandIdx);
  381.         var m2 = 段2.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  382.         if (m2) {
  383.             var x2 = (parseInt(m2[1]) + parseInt(m2[3])) / 2;
  384.             var y2 = (parseInt(m2[2]) + parseInt(m2[4])) / 2;
  385.             agent.click(x2, y2);
  386.             print.log("✓ 已点击展开按钮(content-desc),坐标(" + Math.round(x2) + "," + Math.round(y2) + ")");
  387.             sleep.second(秒 = 1);
  388.             return true;
  389.         }
  390.     }

  391.     // 4. 查找文本"展开"或"更多"的按钮
  392.     var textExpandIdx2 = xmlStr.indexOf('text="展开"');
  393.     if (textExpandIdx2 === -1) {
  394.         textExpandIdx2 = xmlStr.indexOf('text="更多"');
  395.     }
  396.     if (textExpandIdx2 !== -1) {
  397.         var 段3 = xmlStr.substring(textExpandIdx2);
  398.         var m3 = 段3.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  399.         if (m3) {
  400.             var x3 = (parseInt(m3[1]) + parseInt(m3[3])) / 2;
  401.             var y3 = (parseInt(m3[2]) + parseInt(m3[4])) / 2;
  402.             agent.click(x3, y3);
  403.             print.log("✓ 已点击展开按钮(文本匹配),坐标(" + Math.round(x3) + "," + Math.round(y3) + ")");
  404.             sleep.second(秒 = 1);
  405.             return true;
  406.         }
  407.     }

  408.     // 5. 查找消息列表底部区域内的ImageView(箭头图标)
  409.     var ivPattern = /<node[^>]*?class="android\.widget\.ImageView"[^>]*?clickable="true"[^>]*?\/?>/g;
  410.     var ivMatch;
  411.     while ((ivMatch = ivPattern.exec(xmlStr)) !== null) {
  412.         var ivNode = ivMatch[0];
  413.         var boundsMatch = ivNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  414.         if (!boundsMatch) continue;
  415.         
  416.         var ivx1 = parseInt(boundsMatch[1]);
  417.         var ivy1 = parseInt(boundsMatch[2]);
  418.         var ivx2 = parseInt(boundsMatch[3]);
  419.         var ivy2 = parseInt(boundsMatch[4]);
  420.         
  421.         var 在底部 = (ivy1 > 屏幕高 * 0.5);
  422.         var 在中间 = (ivx1 > 屏幕宽 * 0.3 && ivx2 < 屏幕宽 * 0.7);
  423.         
  424.         if (在底部 && 在中间) {
  425.             var centerX = (ivx1 + ivx2) / 2;
  426.             var centerY = (ivy1 + ivy2) / 2;
  427.             agent.click(centerX, centerY);
  428.             print.log("✓ 已点击ImageView(箭头图标),坐标(" + Math.round(centerX) + "," + Math.round(centerY) + ")");
  429.             sleep.second(秒 = 2);
  430.             return true;
  431.         }
  432.     }

  433.     // 6. 最终兜底:点击消息列表底部中心位置(展开箭头通常在这里)
  434.     var rvPattern2 = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
  435.     var rvMatch2;
  436.     while ((rvMatch2 = rvPattern2.exec(xmlStr)) !== null) {
  437.         var rx1 = parseInt(rvMatch2[1]);
  438.         var ry1 = parseInt(rvMatch2[2]);
  439.         var rx2 = parseInt(rvMatch2[3]);
  440.         var ry2 = parseInt(rvMatch2[4]);
  441.         var 高度 = ry2 - ry1;
  442.         if (高度 > 500) {
  443.             var centerX = (rx1 + rx2) / 2;
  444.             var bottomY = ry2 - 60;
  445.             agent.click(centerX, bottomY);
  446.             print.log("✓ 已点击消息列表底部中心(兜底展开),坐标(" + Math.round(centerX) + "," + Math.round(bottomY) + ")");
  447.             sleep.second(秒 = 2);
  448.             return true;
  449.         }
  450.     }

  451.     var pctX = 屏幕宽 / 2;
  452.     var pctY = 屏幕高 * 0.75;
  453.     agent.click(pctX, pctY);
  454.     print.log("✓ 已点击屏幕底部中间(最终兜底),坐标(" + Math.round(pctX) + "," + Math.round(pctY) + ")");
  455.     sleep.second(秒 = 1);
  456.     return true;
  457. }

  458. // 从XML解析 RecyclerView 的边界,然后在其中滑动到底部
  459. function 滑动消息列表到底部(xml) {
  460.     var xmlStr = "" + xml;

  461.     var rvPattern = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
  462.     var rvMatch;

  463.     while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
  464.         var rx1 = parseInt(rvMatch[1]);
  465.         var ry1 = parseInt(rvMatch[2]);
  466.         var rx2 = parseInt(rvMatch[3]);
  467.         var ry2 = parseInt(rvMatch[4]);
  468.         var 高度 = ry2 - ry1;

  469.         if (高度 > 500) {
  470.             var startX = (rx1 + rx2) / 2;
  471.             var startY = ry1 + (ry2 - ry1) * 0.2;
  472.             var endX = startX;
  473.             var endY = ry1 + (ry2 - ry1) * 0.8;

  474.             agent.swip(startX, startY, endX, endY, 500);
  475.             print.log("✓ 已在RecyclerView内滑动,区域[" + rx1 + "," + ry1 + "][" + rx2 + "," + ry2 + "]");
  476.             return;
  477.         }
  478.     }

  479.     print.log("⚠ 未找到RecyclerView,使用屏幕百分比滑动");
  480.     agent.swip(屏幕宽 / 2, 屏幕高 * 0.3, 屏幕宽 / 2, 屏幕高 * 0.8, 500);
  481. }

  482. // 等待AI回答完成
  483. // 逻辑:
  484. //   阶段1:等待新回答出现(文本变化)
  485. //   阶段2:点击展开按钮(在消息列表底部)
  486. //   阶段3:动态检测回答是否完成(连续2次检查文本不变,则认为完成)
  487. // 参数:发送前的最后一条AI回答文本(用于对比检测新回答)
  488. // 返回:新回答的文本内容
  489. function 等待AI回答完成(发送前的最后文本) {
  490.     var 已耗时 = 0;
  491.     var 检查间隔秒 = 2;
  492.     var 已检测到新回答 = false;
  493.     var 已点击展开按钮 = false;
  494.     var 上次文本 = "";
  495.     var 文本不变次数 = 0;
  496.     var 已检测到思考完成 = false;

  497.     while (已耗时 < 等待回答超时秒) {
  498.         sleep.second(秒 = 检查间隔秒);
  499.         已耗时 += 检查间隔秒;

  500.         var xml = 获取UI();
  501.         var xmlStr = "" + xml;

  502.         if (!已检测到新回答) {
  503.             // 阶段1:等待新回答出现
  504.             var 宽松列表 = 采集AI回答列表(xml, false);
  505.             if (宽松列表.length === 0) {
  506.                 continue;
  507.             }
  508.             var 当前最后文本 = 宽松列表[宽松列表.length - 1];
  509.             if (当前最后文本 !== 发送前的最后文本 && 字符串长度(当前最后文本) > 0) {
  510.                 print.log("✓ 检测到新回答出现(" + 已耗时 + "秒)");
  511.                 已检测到新回答 = true;
  512.                 上次文本 = 当前最后文本;
  513.             }
  514.         } else {
  515.             // 阶段2和3:点击展开按钮并检测完成
  516.             var 当前列表 = 采集AI回答列表(xml, false);
  517.             var 当前文本 = "";
  518.             if (当前列表.length > 0) {
  519.                 当前文本 = 当前列表[当前列表.length - 1];
  520.             }

  521.             // 检查是否完成思考(宽松条件)
  522.             if (!已检测到思考完成 && (xmlStr.indexOf("参考") !== -1)) {
  523.                 print.log("✓ 检测到AI思考完成(" + 已耗时 + "秒)");
  524.                 已检测到思考完成 = true;
  525.             }

  526.             // 点击展开按钮(在检测到思考完成后立即点击)
  527.             if (!已点击展开按钮 && 已检测到思考完成) {
  528.                 查找并点击展开按钮(xml);
  529.                 已点击展开按钮 = true;
  530.                 print.log("✓ 已点击展开按钮");
  531.             }

  532.             // 如果没检测到思考完成,但文本很长,也尝试点击展开
  533.             if (!已点击展开按钮 && !已检测到思考完成 && 字符串长度(当前文本) > 100 && 已耗时 >= 8) {
  534.                 查找并点击展开按钮(xml);
  535.                 已点击展开按钮 = true;
  536.                 print.log("✓ 文本较长,尝试点击展开按钮");
  537.             }

  538.             // 检查文本是否停止变化
  539.             if (当前文本 === 上次文本) {
  540.                 文本不变次数++;
  541.             } else {
  542.                 文本不变次数 = 0;
  543.                 上次文本 = 当前文本;
  544.             }

  545.             // 判断完成条件:
  546.             // 1. 文本连续2次不变
  547.             // 2. 且文本长度大于50
  548.             // 3. 且已等待至少6秒(确保展开后有时间显示完整内容)
  549.             if (文本不变次数 >= 2 && 字符串长度(当前文本) > 50 && 已耗时 >= 6) {
  550.                 print.log("✓ 检测到回答完成(文本连续不变),返回采集结果");
  551.                 return 当前文本;
  552.             }

  553.             // 如果文本很短但停止变化很久,也返回
  554.             if (文本不变次数 >= 4 && 字符串长度(当前文本) > 10 && 已耗时 >= 10) {
  555.                 print.log("✓ 文本停止变化已久,返回采集结果");
  556.                 return 当前文本;
  557.             }
  558.         }
  559.     }

  560.     // 超时兜底:尽量返回当前能采集到的最后一条回答
  561.     print.log("⚠ 等待AI回答超时(" + 等待回答超时秒 + "秒),尝试返回当前内容");
  562.     var xml = 获取UI();
  563.     var 兜底列表 = 采集AI回答列表(xml, true);
  564.     if (兜底列表.length > 0) {
  565.         var 兜底文本 = 兜底列表[兜底列表.length - 1];
  566.         if (兜底文本 !== 发送前的最后文本) {
  567.             print.log("✓ 超时兜底:返回当前采集到的回答");
  568.             return 兜底文本;
  569.         }
  570.     }
  571.     var 兜底宽松 = 采集AI回答列表(xml, false);
  572.     if (兜底宽松.length > 0) {
  573.         var 兜底文本2 = 兜底宽松[兜底宽松.length - 1];
  574.         if (兜底文本2 !== 发送前的最后文本 && 字符串长度(兜底文本2) > 5) {
  575.             print.log("✓ 超时兜底(宽松):返回当前采集到的回答");
  576.             return 兜底文本2;
  577.         }
  578.     }
  579.     return null;
  580. }

  581. // 获取当前页面最后一条AI回答文本(用于发送前记录基线)
  582. function 获取当前最后AI回答(xml) {
  583.     var 回答列表 = 采集AI回答列表(xml);
  584.     if (回答列表.length === 0) return "";
  585.     return 回答列表[回答列表.length - 1];
  586. }

  587. // 格式化对话内容输出(TXT格式)
  588. function 格式化对话(对话列表) {
  589.     var result = "========== 千问对话记录 ==========\n";
  590.     result += "采集时间:" + new Date().toString() + "\n";
  591.     result += "对话轮数:" + 对话列表.length + "\n";
  592.     result += "==================================\n\n";
  593.    
  594.     for (var i = 0; i < 对话列表.length; i++) {
  595.         result += "【" + 对话列表[i].角色 + "】\n";
  596.         result += 对话列表[i].内容 + "\n";
  597.         result += "----------------------------------\n";
  598.         if (i < 对话列表.length - 1) {
  599.             result += "\n";
  600.         }
  601.     }
  602.    
  603.     return result;
  604. }

  605. // 写入文件(TXT格式,GBK编码)
  606. function 写入文件(文件路径, 内容) {
  607.     var javaStr = new java.lang.String(内容);
  608.    
  609.     try {
  610.         var fos = new java.io.FileOutputStream(文件路径);
  611.         fos.write(javaStr.getBytes("GBK"));
  612.         fos.close();
  613.         
  614.         print.log("✓ 已写入文件(TXT格式,GBK编码):" + 文件路径);
  615.         
  616.         var catResult = agent.shell("cat " + 文件路径 + " | head -2");
  617.         print.log("✓ cat验证结果:" + catResult);
  618.         
  619.         return true;
  620.     } catch (e) {
  621.         print.log("⚠ FileOutputStream写入异常:" + e.message);
  622.     }

  623.     try {
  624.         var base64Content = java.util.Base64.getEncoder().encodeToString(javaStr.getBytes("GBK"));
  625.         agent.shell("echo '" + base64Content + "' | base64 -d > " + 文件路径);
  626.         
  627.         print.log("✓ 已通过base64写入文件(GBK):" + 文件路径);
  628.         
  629.         var catResult = agent.shell("cat " + 文件路径 + " | head -2");
  630.         print.log("✓ cat验证结果:" + catResult);
  631.         
  632.         return true;
  633.     } catch (e2) {
  634.         print.log("⚠ base64写入异常:" + e2.message);
  635.     }

  636.     try {
  637.         var fos2 = new java.io.FileOutputStream(文件路径);
  638.         fos2.write(javaStr.getBytes("UTF-8"));
  639.         fos2.close();
  640.         
  641.         print.log("✓ 已通过FileOutputStream写入文件(UTF-8):" + 文件路径);
  642.         
  643.         return true;
  644.     } catch (e3) {
  645.         print.log("⚠ UTF-8写入异常:" + e3.message);
  646.     }

  647.     print.log("✗ 写入文件失败:" + 文件路径);
  648.     return false;
  649. }

  650. // ========== 主流程开始 ==========

  651. // 步骤1:启动千问APP
  652. print.log("\n========== 步骤1:启动千问APP ==========");
  653. try {
  654.     app.openApp(千问包名);
  655.     print.log("✓ 已启动千问");
  656. } catch (e) {
  657.     print.log("⚠ 启动失败:" + e.message);
  658. }
  659. sleep.second(秒 = 5);

  660. // 步骤2:确认进入对话页面
  661. print.log("\n========== 步骤2:确认进入对话页面 ==========");
  662. var xml = 获取UI();
  663. var hasRecyclerView = (xml.indexOf("androidx.recyclerview.widget.RecyclerView") !== -1);
  664. if (!hasRecyclerView) {
  665.     print.log("⚠ 当前页面可能不是千问对话页,再次等待");
  666.     sleep.second(秒 = 3);
  667.     xml = 获取UI();
  668.     hasRecyclerView = (xml.indexOf("androidx.recyclerview.widget.RecyclerView") !== -1);
  669. }

  670. if (hasRecyclerView) {
  671.     print.log("✓ 已进入千问对话页面");
  672. } else {
  673.     print.log("⚠ 未检测到消息列表,继续尝试");
  674. }

  675. // 步骤3:循环发送问题并采集回答
  676. print.log("\n========== 步骤3:循环发送问题并采集回答 ==========");

  677. // 完整对话列表(包含用户问题和AI回答)
  678. var 完整对话 = [];

  679. // 获取发送第一个问题前的最后AI回答文本(作为基线,用于检测新回答)
  680. var 初始最后文本 = 获取当前最后AI回答(xml);
  681. print.log("✓ 初始页面最后一条AI回答长度:" + 字符串长度(初始最后文本));

  682. // 开始发送问题
  683. for (var q = 0; q < 要发送的问题列表.length; q++) {
  684.     var 当前问题 = 要发送的问题列表[q];
  685.     print.log("\n---- 第 " + (q + 1) + " / " + 要发送的问题列表.length + " 个问题 ----");
  686.     print.log("问题:" + 当前问题);

  687.     // 3.1 重新获取UI,定位输入框
  688.     xml = 获取UI();
  689.     var 输入框 = 查找输入框中心(xml);
  690.     if (!输入框) {
  691.         print.log("⚠ 未找到输入框,跳过此问题");
  692.         sleep.second(秒 = 问题间隔秒);
  693.         continue;
  694.     }

  695.     // 3.2 记录发送前的最后一条AI回答文本(作为检测新回答的基线)
  696.     var 发送前最后文本 = 获取当前最后AI回答(xml);

  697.     // 3.3 点击输入框获取焦点
  698.     agent.click(输入框.x, 输入框.y);
  699.     sleep.second(秒 = 1);

  700.     // 3.4 粘贴问题文本
  701.     粘贴文本(当前问题);

  702.     // 3.5 重新获取UI,查找发送按钮
  703.     xml = 获取UI();
  704.     var 发送按钮 = 查找发送按钮中心(xml);

  705.     if (发送按钮) {
  706.         agent.click(发送按钮.x, 发送按钮.y);
  707.         print.log("✓ 已点击发送按钮");
  708.     } else {
  709.         // 兜底:按回车键发送
  710.         agent.shell("input keyevent 66");
  711.         print.log("⚠ 未找到发送按钮,已尝试按回车发送");
  712.     }

  713.     // 3.6 记录用户问题
  714.     完整对话.push({
  715.         角色: "用户",
  716.         内容: 当前问题
  717.     });

  718.     // 3.7 等待AI回答完成(基于"最后一条回答文本变化"判断,不依赖数量递增)
  719.     print.log("等待AI回答,发送前最后文本长度:" + 字符串长度(发送前最后文本));

  720.     var 新回答文本 = 等待AI回答完成(发送前最后文本);
  721.     if (新回答文本 && 字符串长度(新回答文本) > 0) {
  722.         完整对话.push({
  723.             角色: "AI",
  724.             内容: 新回答文本
  725.         });
  726.         print.log("✓ 采集到新回答,当前总对话条数:" + 完整对话.length);
  727.         print.log("--- 采集内容开始 ---");
  728.         print.log(新回答文本);
  729.         print.log("--- 采集内容结束 ---");
  730.     } else {
  731.         print.log("⚠ 未能采集到新回答");
  732.     }

  733.     sleep.second(秒 = 问题间隔秒);
  734. }

  735. // 步骤4:保存对话到文件
  736. print.log("\n========== 步骤4:保存对话到文件 ==========");
  737. var 输出内容 = 格式化对话(完整对话);
  738. var 写入成功 = 写入文件(输出文件路径, 输出内容);

  739. if (写入成功) {
  740.     print.log("✓ 对话已保存到:" + 输出文件路径);
  741.     print.log("✓ 总对话条数:" + 完整对话.length);
  742. } else {
  743.     print.log("✗ 保存失败,请检查存储权限");
  744. }

  745. // 步骤5:打印全部采集到的对话内容
  746. print.log("\n========== 全部采集内容 ==========");
  747. print.log(输出内容);
  748. print.log("========== 内容结束 ==========");

  749. print.log("\n===== 千问对话采集流程结束 =====");
复制代码







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