示例千问自动采集文章保存记事本

380

主题

193

回帖

2397

积分

管理员

积分
2397
示例千问自动采集文章保存记事本

1.png

2.png

3.png

4.png

  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. function 解码HTML实体(s) {
  41.     var str = "" + s;
  42.     str = str.replace(/
/g, "\n");
  43.     str = str.replace(/
/g, "\r");
  44.     str = str.replace(/	/g, "\t");
  45.     str = str.replace(/'/g, "'");
  46.     str = str.replace(/"/g, '"');
  47.     str = str.replace(/"/g, '"');
  48.     str = str.replace(/'/g, "'");
  49.     str = str.replace(/&/g, "&");
  50.     str = str.replace(/&lt;/g, "<");
  51.     str = str.replace(/&gt;/g, ">");
  52.     return str;
  53. }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  131.     return 回答列表;
  132. }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  497.         var xml = 获取UI();
  498.         var xmlStr = "" + xml;

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

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

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

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

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

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

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

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

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

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

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

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

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

  644.     print.log("✗ 写入文件失败:" + 文件路径);
  645.     return false;
  646. }

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

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

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

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

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

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

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

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

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

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

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

  697.     // 3.4 粘贴问题文本
  698.     粘贴文本(当前问题);

  699.     // 3.5 重新获取UI,查找发送按钮
  700.     xml = 获取UI();
  701.     var 发送按钮 = 查找发送按钮中心(xml);

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

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

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

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

  730.     sleep.second(秒 = 问题间隔秒);
  731. }

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

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

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

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


举报 回复