AIWROK 实战脚本千问对话采集SQLite/MySQL 双模式自动入库

431

主题

231

回帖

2703

积分

管理员

积分
2703
AIWROK 实战脚本千问对话采集SQLite/MySQL 双模式自动入库

QQ截图20261017123702.png


QQ截图20261017123633.png


QQ截图20261017123619.png




  1. // AIWROK 自动化流程:启动千问 → 输入问题 → 点击发送 → 等待AI回答 → 采集回答内容 → 对接数据库入库
  2. // 全程使用 agent.getXml() 获取界面源码(代理模式,不依赖无障碍服务)
  3. // 输入中文用剪贴板粘贴法: Clipboard.copy + keyevent 279(KEYCODE_PASTE)
  4. // 包名:com.aliyun.tongyi(千问)
  5. //
  6. // ========== 这种"采集+数据库入库"方式的优势 ==========
  7. // 1. 全程自动入库:采集到的问答内容实时写入数据库,无需人工导出、无需翻手机看记录;
  8. // 2. 支持网络数据库:切换"数据库模式"为 mysql 后,数据直接上传到公网数据库或本地局域网数据库,
  9. //    手机采集、电脑存储,一台设备跑脚本、多台设备共享数据;
  10. // 3. 可远程调用:数据入库后任何联网设备都能远程查询、统计、导出,不受手机位置限制;
  11. // 4. API 友好:不同手机、电脑、后端服务都可以通过数据库(或基于数据库的 API)统一读取采集结果,
  12. //    多机集群采集的内容汇聚到同一张表 qa_records,天然去重、可追溯、可扩展;
  13. // 5. 结构化存储:role(角色)/content(内容)/collected_at(采集时间) 分字段存放,
  14. //    比 TXT 文件更好检索、更好做关键词分析和二次加工。

  15. printl("===== 千问对话采集流程启动(数据库入库版) =====");
  16. print.time();

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

  19. // 数据库模式:"sqlite"=设备本地库文件(零配置,默认) | "mysql"=远程 MySQL
  20. var 数据库模式 = "sqlite";
  21. var SQLite路径 = "/sdcard/千问对话.db";        // sqlite 模式:本地库文件路径
  22. var MySQL地址 = "jdbc:mysql://192.168.1.100:3306/aiwork?useSSL=false&characterEncoding=utf8&connectTimeout=5000&socketTimeout=10000";  // mysql 模式(必须带超时,否则连不上会无限卡死)
  23. var MySQL用户 = "root";
  24. var MySQL密码 = "请填写数据库密码";
  25. var 表名 = "qa_records";

  26. // 输出文件路径(JSON格式,UTF-8编码)——保留 TXT 作为备份
  27. var 输出文件路径 = "/sdcard/千问对话记录.txt";

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

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

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

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

  40. // ========== 全局变量放顶部 ==========
  41. importClass(android.database.sqlite.SQLiteDatabase);
  42. importClass(java.io.File);

  43. var 数据库连接 = null;
  44. var 数据库可用 = false;

  45. // ========== 获取设备信息 ==========
  46. var 屏幕宽 = screen.getScreenWidth();
  47. var 屏幕高 = screen.getScreenHeight();
  48. printl("屏幕尺寸:" + 屏幕宽 + " x " + 屏幕高);
  49. printl("代理状态:" + (agent.isOpen() ? "已开启" : "未开启"));

  50. // ========== 数据库函数(sqlite/mysql 双模式) ==========
  51. function dbConnect() {
  52.     if (数据库模式 == "mysql") {
  53.         printl("[DB] 正在连接 MySQL(5 秒超时)...");
  54.         try {
  55.             数据库连接 = mysql.getConnection(MySQL地址, MySQL用户, MySQL密码);
  56.             printl("[DB] ✅ MySQL 连接成功");
  57.             return true;
  58.         } catch (e) {
  59.             printl("[DB] ❌ MySQL 连接失败: " + e);
  60.             return false;
  61.         }
  62.     }
  63.     try {
  64.         数据库连接 = SQLiteDatabase.openOrCreateDatabase(new File(SQLite路径), null);
  65.         printl("[DB] ✅ SQLite 已打开: " + SQLite路径);
  66.         return true;
  67.     } catch (e2) {
  68.         printl("[DB] ❌ SQLite 打开失败: " + e2);
  69.         return false;
  70.     }
  71. }

  72. function dbEnsureTable() {
  73.     var sql;
  74.     if (数据库模式 == "mysql") {
  75.         sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
  76.             "id INT AUTO_INCREMENT PRIMARY KEY," +
  77.             "role VARCHAR(16)," +
  78.             "content LONGTEXT," +
  79.             "collected_at VARCHAR(32)" +
  80.             ") DEFAULT CHARSET=utf8mb4";
  81.     } else {
  82.         sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
  83.             "id INTEGER PRIMARY KEY AUTOINCREMENT," +
  84.             "role TEXT, content TEXT, collected_at TEXT)";
  85.     }
  86.     try {
  87.         数据库连接.execSQL(sql);
  88.         printl("[DB] 表 " + 表名 + " 就绪");
  89.         return true;
  90.     } catch (e) {
  91.         printl("[DB] ❌ 建表失败: " + e);
  92.         return false;
  93.     }
  94. }

  95. // 单条对话入库(角色=用户/AI, 内容=原文, 时间=time.now())
  96. function dbInsert(角色, 内容) {
  97.     try {
  98.         var sql = "INSERT INTO " + 表名 + " (role, content, collected_at) VALUES (?,?,?)";
  99.         var 时间 = time.now();
  100.         if (数据库模式 == "mysql") {
  101.             var stmt = 数据库连接.prepareStatement(sql);
  102.             stmt.setString(1, 角色);
  103.             stmt.setString(2, 内容);
  104.             stmt.setString(3, 时间);
  105.             stmt.executeUpdate();
  106.             stmt.close();
  107.         } else {
  108.             var stmt2 = 数据库连接.compileStatement(sql);
  109.             stmt2.bindString(1, 角色);
  110.             stmt2.bindString(2, 内容);
  111.             stmt2.bindString(3, 时间);
  112.             stmt2.executeInsert();
  113.             stmt2.close();
  114.         }
  115.         printl("[DB] ✅ 入库:【" + 角色 + "】" + 字符串长度(内容) + " 字");
  116.         return true;
  117.     } catch (e) {
  118.         printl("[DB] ❌ 入库失败: " + e);
  119.         return false;
  120.     }
  121. }

  122. function dbClose() {
  123.     try {
  124.         if (数据库连接 != null) { 数据库连接.close(); 数据库连接 = null; printl("[DB] 已断开连接"); }
  125.     } catch (e) {}
  126. }

  127. // ========== 工具函数 ==========

  128. // 获取 UI XML 字符串(使用 agent.getXml(),强制转为字符串)
  129. function 获取UI() {
  130.     var xml = agent.getXml();
  131.     // agent.getXml() 返回对象,需强制转为字符串
  132.     return "" + xml;
  133. }

  134. // 字符串长度兼容函数(避免 Java String 对象 .length() 方法问题)
  135. function 字符串长度(s) {
  136.     var str = "" + s;
  137.     return str.length;
  138. }

  139. // HTML实体解码(千问TextView中的换行符等会被编码为 
 ' 等)
  140. function 解码HTML实体(s) {
  141.     var str = "" + s;
  142.     str = str.replace(/
/g, "\n");
  143.     str = str.replace(/
/g, "\r");
  144.     str = str.replace(/	/g, "\t");
  145.     str = str.replace(/'/g, "'");
  146.     str = str.replace(/"/g, '"');
  147.     str = str.replace(/"/g, '"');
  148.     str = str.replace(/'/g, "'");
  149.     str = str.replace(/&/g, "&");
  150.     str = str.replace(/&lt;/g, "<");
  151.     str = str.replace(/&gt;/g, ">");
  152.     return str;
  153. }

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

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

  164.     while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
  165.         var rx1 = parseInt(rvMatch[1]);
  166.         var ry1 = parseInt(rvMatch[2]);
  167.         var rx2 = parseInt(rvMatch[3]);
  168.         var ry2 = parseInt(rvMatch[4]);
  169.         var 高度 = ry2 - ry1;
  170.         var 宽度 = rx2 - rx1;

  171.         if (高度 > 500 && 宽度 > 屏幕宽 * 0.8) {
  172.             消息列表区域 = {x1: rx1, y1: ry1, x2: rx2, y2: ry2};
  173.             break;
  174.         }
  175.     }

  176.     if (!消息列表区域) {
  177.         printl("⚠ 未找到消息列表RecyclerView节点");
  178.         return 回答列表;
  179.     }

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

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

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

  186.         // 从节点中提取文本(支持双引号和单引号)
  187.         var textMatch = tvNode.match(/text="([^"]+)"/);
  188.         if (!textMatch) {
  189.             textMatch = tvNode.match(/text='([^']+)'/);
  190.         }
  191.         if (!textMatch) continue;
  192.         var 文本 = textMatch[1];

  193.         // 从节点中提取边界
  194.         var boundsMatch = tvNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  195.         if (!boundsMatch) continue;
  196.         var tx1 = parseInt(boundsMatch[1]);
  197.         var ty1 = parseInt(boundsMatch[2]);
  198.         var tx2 = parseInt(boundsMatch[3]);
  199.         var ty2 = parseInt(boundsMatch[4]);

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

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

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

  205.         // 排除用户消息特征:位于消息列表顶部区域(y坐标较小)且是单行短文本
  206.         var 是用户消息 = (ty1 < 消息列表区域.y1 + 500) && (解码文本.indexOf("\n") === -1) && (字符串长度(解码文本) < 100);

  207.         // 排除搜索步骤文本(包含"查询"、"检索"、"调研"等关键词的是搜索计划,不是最终回答)
  208.         var 是搜索步骤 = (解码文本.indexOf("查询") !== -1 || 解码文本.indexOf("检索") !== -1 ||
  209.                           解码文本.indexOf("调研") !== -1 || 解码文本.indexOf("确认") !== -1 ||
  210.                           解码文本.indexOf("分析") !== -1 || 解码文本.indexOf("查找") !== -1) &&
  211.                          (字符串长度(解码文本) < 500);

  212.         // 排除视频推荐区域的文本(视频推荐通常在AI回答下方,y坐标较大且有特定特征)
  213.         // AI回答通常在消息列表顶部区域,视频推荐在下方
  214.         var 消息列表高度 = 消息列表区域.y2 - 消息列表区域.y1;
  215.         var 是视频推荐 = (ty1 > 消息列表区域.y1 + 消息列表高度 * 0.3) &&
  216.                          (解码文本.indexOf("笔记") !== -1 || 解码文本.indexOf("作者") !== -1 ||
  217.                           字符串长度(解码文本) < 30);

  218.         if (严格过滤) {
  219.             if (!是用户消息 && !是搜索步骤 && !是视频推荐 && (解码文本.indexOf("\n") !== -1 || 字符串长度(解码文本) > 50)) {
  220.                 回答列表.push(解码文本);
  221.             }
  222.         } else {
  223.             if (!是用户消息 && !是搜索步骤 && !是视频推荐 && 字符串长度(解码文本) > 0) {
  224.                 回答列表.push(解码文本);
  225.             }
  226.         }
  227.     }

  228.     return 回答列表;
  229. }

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

  233.     var inputIdx = xmlStr.indexOf('text="发消息或按住说话..."');
  234.     if (inputIdx === -1) {
  235.         inputIdx = xmlStr.indexOf("text='发消息或按住说话...'");
  236.     }
  237.     if (inputIdx !== -1) {
  238.         var 段 = xmlStr.substring(inputIdx);
  239.         var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  240.         if (m) {
  241.             printl("✓ 输入框定位:hint文本匹配");
  242.             return {
  243.                 x: (parseInt(m[1]) + parseInt(m[3])) / 2,
  244.                 y: (parseInt(m[2]) + parseInt(m[4])) / 2
  245.             };
  246.         }
  247.     }

  248.     var editTextPattern = /<node[^>]*?class="android\.widget\.EditText"[^>]*?\/?>/g;
  249.     var etMatch;
  250.     while ((etMatch = editTextPattern.exec(xmlStr)) !== null) {
  251.         var etNode = etMatch[0];
  252.         var boundsMatch = etNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  253.         if (boundsMatch) {
  254.             var ex1 = parseInt(boundsMatch[1]);
  255.             var ey1 = parseInt(boundsMatch[2]);
  256.             var ex2 = parseInt(boundsMatch[3]);
  257.             var ey2 = parseInt(boundsMatch[4]);

  258.             if (ey1 > 屏幕高 * 0.8) {
  259.                 printl("✓ 输入框定位:EditText匹配");
  260.                 return {
  261.                     x: (ex1 + ex2) / 2,
  262.                     y: (ey1 + ey2) / 2
  263.                 };
  264.             }
  265.         }
  266.     }

  267.     var viewGroupPattern = /<node[^>]*?class="android\.view\.ViewGroup"[^>]*?clickable="true"[^>]*?\/?>/g;
  268.     var vgMatch;
  269.     while ((vgMatch = viewGroupPattern.exec(xmlStr)) !== null) {
  270.         var vgNode = vgMatch[0];
  271.         var boundsMatch = vgNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  272.         if (boundsMatch) {
  273.             var vx1 = parseInt(boundsMatch[1]);
  274.             var vy1 = parseInt(boundsMatch[2]);
  275.             var vx2 = parseInt(boundsMatch[3]);
  276.             var vy2 = parseInt(boundsMatch[4]);

  277.             var vWidth = vx2 - vx1;
  278.             var vHeight = vy2 - vy1;

  279.             if (vy1 > 屏幕高 * 0.8 && vWidth > 屏幕宽 * 0.4 && vWidth < 屏幕宽 * 0.8) {
  280.                 printl("✓ 输入框定位:ViewGroup匹配");
  281.                 return {
  282.                     x: (vx1 + vx2) / 2,
  283.                     y: (vy1 + vy2) / 2
  284.                 };
  285.             }
  286.         }
  287.     }

  288.     printl("⚠ 未找到输入框,使用兜底坐标");
  289.     return {
  290.         x: 屏幕宽 * 0.4,
  291.         y: 屏幕高 * 0.92
  292.     };
  293. }

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

  298.     // 1. 优先查找文字"发送"
  299.     var sendTextIdx = xmlStr.indexOf('text="发送"');
  300.     if (sendTextIdx === -1) {
  301.         sendTextIdx = xmlStr.indexOf("text='发送'");
  302.     }
  303.     if (sendTextIdx !== -1) {
  304.         var 段 = xmlStr.substring(sendTextIdx);
  305.         var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  306.         if (m) {
  307.             printl("✓ 发送按钮定位:文本匹配");
  308.             return {
  309.                 x: (parseInt(m[1]) + parseInt(m[3])) / 2,
  310.                 y: (parseInt(m[2]) + parseInt(m[4])) / 2
  311.             };
  312.         }
  313.     }

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

  318.     while ((clickMatch = clickablePattern.exec(xmlStr)) !== null) {
  319.         var cx1 = parseInt(clickMatch[1]);
  320.         var cy1 = parseInt(clickMatch[2]);
  321.         var cx2 = parseInt(clickMatch[3]);
  322.         var cy2 = parseInt(clickMatch[4]);

  323.         if (cy1 > 屏幕高 * 0.85) {
  324.             candidates.push({
  325.                 x1: cx1, y1: cy1, x2: cx2, y2: cy2,
  326.                 x: (cx1 + cx2) / 2,
  327.                 y: (cy1 + cy2) / 2
  328.             });
  329.         }
  330.     }

  331.     if (candidates.length > 0) {
  332.         candidates.sort(function(a, b) {
  333.             return b.x1 - a.x1;
  334.         });
  335.         printl("✓ 找到" + candidates.length + "个候选按钮,选择最右侧的,坐标(" + Math.round(candidates[0].x) + "," + Math.round(candidates[0].y) + ")");
  336.         return {
  337.             x: candidates[0].x,
  338.             y: candidates[0].y
  339.         };
  340.     }

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

  345.     while ((flMatch = flPattern.exec(xmlStr)) !== null) {
  346.         var flNode = flMatch[0];

  347.         if (flNode.indexOf('clickable="true"') === -1) continue;

  348.         var boundsMatch = flNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  349.         if (!boundsMatch) continue;

  350.         var fx1 = parseInt(boundsMatch[1]);
  351.         var fy1 = parseInt(boundsMatch[2]);
  352.         var fx2 = parseInt(boundsMatch[3]);
  353.         var fy2 = parseInt(boundsMatch[4]);

  354.         if (fy1 > 屏幕高 * 0.85) {
  355.             flCandidates.push({
  356.                 x1: fx1, y1: fy1, x2: fx2, y2: fy2,
  357.                 x: (fx1 + fx2) / 2,
  358.                 y: (fy1 + fy2) / 2
  359.             });
  360.         }
  361.     }

  362.     if (flCandidates.length > 0) {
  363.         flCandidates.sort(function(a, b) {
  364.             return b.x1 - a.x1;
  365.         });
  366.         printl("✓ 找到" + flCandidates.length + "个FrameLayout候选,选择最右侧的,坐标(" + Math.round(flCandidates[0].x) + "," + Math.round(flCandidates[0].y) + ")");
  367.         return {
  368.             x: flCandidates[0].x,
  369.             y: flCandidates[0].y
  370.         };
  371.     }

  372.     // 4. 兜底:基于屏幕百分比定位发送按钮(输入框下方右侧)
  373.     printl("⚠ 使用兜底策略定位发送按钮,坐标(" + Math.round(屏幕宽 * 0.92) + "," + Math.round(屏幕高 * 0.92) + ")");
  374.     return {
  375.         x: 屏幕宽 * 0.92,
  376.         y: 屏幕高 * 0.92
  377.     };
  378. }

  379. // 粘贴文本到当前焦点输入框(剪贴板法,兼容中文)
  380. function 粘贴文本(文本) {
  381.     try {
  382.         Clipboard.copy("" + 文本);
  383.     } catch (e) {
  384.         printl("⚠ Clipboard.copy 失败:" + e.message);
  385.     }
  386.     sleep.millisecond(毫秒 = 300);

  387.     // 双保险:通过 shell 写剪贴板
  388.     try {
  389.         agent.shell("service call clipboard 3 i32 1 s16 '" + 文本 + "' i32 0");
  390.     } catch (e2) {
  391.         // 忽略错误
  392.     }
  393.     sleep.millisecond(毫秒 = 300);

  394.     // 触发粘贴:KEYCODE_PASTE = 279
  395.     agent.shell("input keyevent 279");
  396.     sleep.second(秒 = 1);
  397.     printl("✓ 已粘贴文本:" + 文本);
  398. }

  399. // 查找并点击AI回答底部的展开按钮(向下箭头)
  400. // 千问长回答会折叠显示,需要点击底部向下箭头才能看到完整内容
  401. function 查找并点击展开按钮(xml) {
  402.     var xmlStr = "" + xml;

  403.     // 1. 查找消息列表底部中间的小区域clickable节点(向下箭头特征)
  404.     var rvPattern = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
  405.     var rvMatch;
  406.     while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
  407.         var rx1 = parseInt(rvMatch[1]);
  408.         var ry1 = parseInt(rvMatch[2]);
  409.         var rx2 = parseInt(rvMatch[3]);
  410.         var ry2 = parseInt(rvMatch[4]);
  411.         var 高度 = ry2 - ry1;
  412.         if (高度 > 500) {
  413.             var viewPattern = /<node[^>]*?clickable="true"[^>]*?\/?>/g;
  414.             var viewMatch;
  415.             while ((viewMatch = viewPattern.exec(xmlStr)) !== null) {
  416.                 var vNode = viewMatch[0];
  417.                 var boundsMatch = vNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  418.                 if (!boundsMatch) continue;

  419.                 var vx1 = parseInt(boundsMatch[1]);
  420.                 var vy1 = parseInt(boundsMatch[2]);
  421.                 var vx2 = parseInt(boundsMatch[3]);
  422.                 var vy2 = parseInt(boundsMatch[4]);

  423.                 var vWidth = vx2 - vx1;
  424.                 var vHeight = vy2 - vy1;

  425.                 var 在区域内 = (vx1 >= rx1 && vx2 <= rx2 && vy1 >= ry1 && vy2 <= ry2);
  426.                 var 是小按钮 = (vWidth < 200 && vHeight < 200);
  427.                 var 在底部 = (vy1 > ry1 + 高度 * 0.7);

  428.                 if (在区域内 && 是小按钮 && 在底部) {
  429.                     var centerX = (vx1 + vx2) / 2;
  430.                     var centerY = (vy1 + vy2) / 2;
  431.                     agent.click(centerX, centerY);
  432.                     printl("✓ 已点击展开箭头(消息列表底部小按钮),坐标(" + Math.round(centerX) + "," + Math.round(centerY) + ")");
  433.                     sleep.second(秒 = 2);
  434.                     return true;
  435.                 }
  436.             }
  437.             break;
  438.         }
  439.     }

  440.     // 2. 查找文本"查看全部"的按钮
  441.     var textExpandIdx = xmlStr.indexOf('text="查看全部"');
  442.     if (textExpandIdx === -1) {
  443.         textExpandIdx = xmlStr.indexOf("text='查看全部'");
  444.     }
  445.     if (textExpandIdx !== -1) {
  446.         var 段 = xmlStr.substring(textExpandIdx);
  447.         var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  448.         if (m) {
  449.             var x = (parseInt(m[1]) + parseInt(m[3])) / 2;
  450.             var y = (parseInt(m[2]) + parseInt(m[4])) / 2;
  451.             agent.click(x, y);
  452.             printl("✓ 已点击查看全部按钮,坐标(" + Math.round(x) + "," + Math.round(y) + ")");
  453.             sleep.second(秒 = 2);
  454.             return true;
  455.         }
  456.     }

  457.     // 3. 查找 content-desc 含"展开"或"更多"的按钮
  458.     var expandIdx = xmlStr.indexOf('content-desc="展开"');
  459.     if (expandIdx === -1) {
  460.         expandIdx = xmlStr.indexOf('content-desc="更多"');
  461.     }
  462.     if (expandIdx !== -1) {
  463.         var 段2 = xmlStr.substring(expandIdx);
  464.         var m2 = 段2.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  465.         if (m2) {
  466.             var x2 = (parseInt(m2[1]) + parseInt(m2[3])) / 2;
  467.             var y2 = (parseInt(m2[2]) + parseInt(m2[4])) / 2;
  468.             agent.click(x2, y2);
  469.             printl("✓ 已点击展开按钮(content-desc),坐标(" + Math.round(x2) + "," + Math.round(y2) + ")");
  470.             sleep.second(秒 = 1);
  471.             return true;
  472.         }
  473.     }

  474.     // 4. 查找文本"展开"或"更多"的按钮
  475.     var textExpandIdx2 = xmlStr.indexOf('text="展开"');
  476.     if (textExpandIdx2 === -1) {
  477.         textExpandIdx2 = xmlStr.indexOf('text="更多"');
  478.     }
  479.     if (textExpandIdx2 !== -1) {
  480.         var 段3 = xmlStr.substring(textExpandIdx2);
  481.         var m3 = 段3.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  482.         if (m3) {
  483.             var x3 = (parseInt(m3[1]) + parseInt(m3[3])) / 2;
  484.             var y3 = (parseInt(m3[2]) + parseInt(m3[4])) / 2;
  485.             agent.click(x3, y3);
  486.             printl("✓ 已点击展开按钮(文本匹配),坐标(" + Math.round(x3) + "," + Math.round(y3) + ")");
  487.             sleep.second(秒 = 1);
  488.             return true;
  489.         }
  490.     }

  491.     // 5. 查找消息列表底部区域内的ImageView(箭头图标)
  492.     var ivPattern = /<node[^>]*?class="android\.widget\.ImageView"[^>]*?clickable="true"[^>]*?\/?>/g;
  493.     var ivMatch;
  494.     while ((ivMatch = ivPattern.exec(xmlStr)) !== null) {
  495.         var ivNode = ivMatch[0];
  496.         var boundsMatch = ivNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
  497.         if (!boundsMatch) continue;

  498.         var ivx1 = parseInt(boundsMatch[1]);
  499.         var ivy1 = parseInt(boundsMatch[2]);
  500.         var ivx2 = parseInt(boundsMatch[3]);
  501.         var ivy2 = parseInt(boundsMatch[4]);

  502.         var 在底部 = (ivy1 > 屏幕高 * 0.5);
  503.         var 在中间 = (ivx1 > 屏幕宽 * 0.3 && ivx2 < 屏幕宽 * 0.7);

  504.         if (在底部 && 在中间) {
  505.             var centerX = (ivx1 + ivx2) / 2;
  506.             var centerY = (ivy1 + ivy2) / 2;
  507.             agent.click(centerX, centerY);
  508.             printl("✓ 已点击ImageView(箭头图标),坐标(" + Math.round(centerX) + "," + Math.round(centerY) + ")");
  509.             sleep.second(秒 = 2);
  510.             return true;
  511.         }
  512.     }

  513.     // 6. 最终兜底:点击消息列表底部中心位置(展开箭头通常在这里)
  514.     var rvPattern2 = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
  515.     var rvMatch2;
  516.     while ((rvMatch2 = rvPattern2.exec(xmlStr)) !== null) {
  517.         var rx1 = parseInt(rvMatch2[1]);
  518.         var ry1 = parseInt(rvMatch2[2]);
  519.         var rx2 = parseInt(rvMatch2[3]);
  520.         var ry2 = parseInt(rvMatch2[4]);
  521.         var 高度 = ry2 - ry1;
  522.         if (高度 > 500) {
  523.             var centerX = (rx1 + rx2) / 2;
  524.             var bottomY = ry2 - 60;
  525.             agent.click(centerX, bottomY);
  526.             printl("✓ 已点击消息列表底部中心(兜底展开),坐标(" + Math.round(centerX) + "," + Math.round(bottomY) + ")");
  527.             sleep.second(秒 = 2);
  528.             return true;
  529.         }
  530.     }

  531.     var pctX = 屏幕宽 / 2;
  532.     var pctY = 屏幕高 * 0.75;
  533.     agent.click(pctX, pctY);
  534.     printl("✓ 已点击屏幕底部中间(最终兜底),坐标(" + Math.round(pctX) + "," + Math.round(pctY) + ")");
  535.     sleep.second(秒 = 1);
  536.     return true;
  537. }

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

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

  543.     while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
  544.         var rx1 = parseInt(rvMatch[1]);
  545.         var ry1 = parseInt(rvMatch[2]);
  546.         var rx2 = parseInt(rvMatch[3]);
  547.         var ry2 = parseInt(rvMatch[4]);
  548.         var 高度 = ry2 - ry1;

  549.         if (高度 > 500) {
  550.             var startX = (rx1 + rx2) / 2;
  551.             var startY = ry1 + (ry2 - ry1) * 0.2;
  552.             var endX = startX;
  553.             var endY = ry1 + (ry2 - ry1) * 0.8;

  554.             agent.swip(startX, startY, endX, endY, 500);
  555.             printl("✓ 已在RecyclerView内滑动,区域[" + rx1 + "," + ry1 + "][" + rx2 + "," + ry2 + "]");
  556.             return;
  557.         }
  558.     }

  559.     printl("⚠ 未找到RecyclerView,使用屏幕百分比滑动");
  560.     agent.swip(屏幕宽 / 2, 屏幕高 * 0.3, 屏幕宽 / 2, 屏幕高 * 0.8, 500);
  561. }

  562. // 等待AI回答完成
  563. // 逻辑:
  564. //   阶段1:等待新回答出现(文本变化)
  565. //   阶段2:点击展开按钮(在消息列表底部)
  566. //   阶段3:动态检测回答是否完成(连续2次检查文本不变,则认为完成)
  567. // 参数:发送前的最后一条AI回答文本(用于对比检测新回答)
  568. // 返回:新回答的文本内容
  569. function 等待AI回答完成(发送前的最后文本) {
  570.     var 已耗时 = 0;
  571.     var 检查间隔秒 = 2;
  572.     var 已检测到新回答 = false;
  573.     var 已点击展开按钮 = false;
  574.     var 上次文本 = "";
  575.     var 文本不变次数 = 0;
  576.     var 已检测到思考完成 = false;

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

  580.         var xml = 获取UI();
  581.         var xmlStr = "" + xml;

  582.         if (!已检测到新回答) {
  583.             // 阶段1:等待新回答出现
  584.             var 宽松列表 = 采集AI回答列表(xml, false);
  585.             if (宽松列表.length === 0) {
  586.                 continue;
  587.             }
  588.             var 当前最后文本 = 宽松列表[宽松列表.length - 1];
  589.             if (当前最后文本 !== 发送前的最后文本 && 字符串长度(当前最后文本) > 0) {
  590.                 printl("✓ 检测到新回答出现(" + 已耗时 + "秒)");
  591.                 已检测到新回答 = true;
  592.                 上次文本 = 当前最后文本;
  593.             }
  594.         } else {
  595.             // 阶段2和3:点击展开按钮并检测完成
  596.             var 当前列表 = 采集AI回答列表(xml, false);
  597.             var 当前文本 = "";
  598.             if (当前列表.length > 0) {
  599.                 当前文本 = 当前列表[当前列表.length - 1];
  600.             }

  601.             // 检查是否完成思考(宽松条件)
  602.             if (!已检测到思考完成 && (xmlStr.indexOf("参考") !== -1)) {
  603.                 printl("✓ 检测到AI思考完成(" + 已耗时 + "秒)");
  604.                 已检测到思考完成 = true;
  605.             }

  606.             // 点击展开按钮(在检测到思考完成后立即点击)
  607.             if (!已点击展开按钮 && 已检测到思考完成) {
  608.                 查找并点击展开按钮(xml);
  609.                 已点击展开按钮 = true;
  610.                 printl("✓ 已点击展开按钮");
  611.             }

  612.             // 如果没检测到思考完成,但文本很长,也尝试点击展开
  613.             if (!已点击展开按钮 && !已检测到思考完成 && 字符串长度(当前文本) > 100 && 已耗时 >= 8) {
  614.                 查找并点击展开按钮(xml);
  615.                 已点击展开按钮 = true;
  616.                 printl("✓ 文本较长,尝试点击展开按钮");
  617.             }

  618.             // 检查文本是否停止变化
  619.             if (当前文本 === 上次文本) {
  620.                 文本不变次数++;
  621.             } else {
  622.                 文本不变次数 = 0;
  623.                 上次文本 = 当前文本;
  624.             }

  625.             // 判断完成条件:
  626.             // 1. 文本连续2次不变
  627.             // 2. 且文本长度大于50
  628.             // 3. 且已等待至少6秒(确保展开后有时间显示完整内容)
  629.             if (文本不变次数 >= 2 && 字符串长度(当前文本) > 50 && 已耗时 >= 6) {
  630.                 printl("✓ 检测到回答完成(文本连续不变),返回采集结果");
  631.                 return 当前文本;
  632.             }

  633.             // 如果文本很短但停止变化很久,也返回
  634.             if (文本不变次数 >= 4 && 字符串长度(当前文本) > 10 && 已耗时 >= 10) {
  635.                 printl("✓ 文本停止变化已久,返回采集结果");
  636.                 return 当前文本;
  637.             }
  638.         }
  639.     }

  640.     // 超时兜底:尽量返回当前能采集到的最后一条回答
  641.     printl("⚠ 等待AI回答超时(" + 等待回答超时秒 + "秒),尝试返回当前内容");
  642.     var xml = 获取UI();
  643.     var 兜底列表 = 采集AI回答列表(xml, true);
  644.     if (兜底列表.length > 0) {
  645.         var 兜底文本 = 兜底列表[兜底列表.length - 1];
  646.         if (兜底文本 !== 发送前的最后文本) {
  647.             printl("✓ 超时兜底:返回当前采集到的回答");
  648.             return 兜底文本;
  649.         }
  650.     }
  651.     var 兜底宽松 = 采集AI回答列表(xml, false);
  652.     if (兜底宽松.length > 0) {
  653.         var 兜底文本2 = 兜底宽松[兜底宽松.length - 1];
  654.         if (兜底文本2 !== 发送前的最后文本 && 字符串长度(兜底文本2) > 5) {
  655.             printl("✓ 超时兜底(宽松):返回当前采集到的回答");
  656.             return 兜底文本2;
  657.         }
  658.     }
  659.     return null;
  660. }

  661. // 获取当前页面最后一条AI回答文本(用于发送前记录基线)
  662. function 获取当前最后AI回答(xml) {
  663.     var 回答列表 = 采集AI回答列表(xml);
  664.     if (回答列表.length === 0) return "";
  665.     return 回答列表[回答列表.length - 1];
  666. }

  667. // 格式化对话内容输出(TXT格式,备份用)
  668. function 格式化对话(对话列表) {
  669.     var result = "========== 千问对话记录 ==========\n";
  670.     result += "采集时间:" + new Date().toString() + "\n";
  671.     result += "对话轮数:" + 对话列表.length + "\n";
  672.     result += "==================================\n\n";

  673.     for (var i = 0; i < 对话列表.length; i++) {
  674.         result += "【" + 对话列表[i].角色 + "】\n";
  675.         result += 对话列表[i].内容 + "\n";
  676.         result += "----------------------------------\n";
  677.         if (i < 对话列表.length - 1) {
  678.             result += "\n";
  679.         }
  680.     }

  681.     return result;
  682. }

  683. // 写入文件(TXT格式,GBK编码)——保留作备份
  684. function 写入文件(文件路径, 内容) {
  685.     var javaStr = new java.lang.String(内容);

  686.     try {
  687.         var fos = new java.io.FileOutputStream(文件路径);
  688.         fos.write(javaStr.getBytes("GBK"));
  689.         fos.close();

  690.         printl("✓ 已写入文件(TXT格式,GBK编码):" + 文件路径);

  691.         var catResult = agent.shell("cat " + 文件路径 + " | head -2");
  692.         printl("✓ cat验证结果:" + catResult);

  693.         return true;
  694.     } catch (e) {
  695.         printl("⚠ FileOutputStream写入异常:" + e.message);
  696.     }

  697.     try {
  698.         var base64Content = java.util.Base64.getEncoder().encodeToString(javaStr.getBytes("GBK"));
  699.         agent.shell("echo '" + base64Content + "' | base64 -d > " + 文件路径);

  700.         printl("✓ 已通过base64写入文件(GBK):" + 文件路径);

  701.         var catResult = agent.shell("cat " + 文件路径 + " | head -2");
  702.         printl("✓ cat验证结果:" + catResult);

  703.         return true;
  704.     } catch (e2) {
  705.         printl("⚠ base64写入异常:" + e2.message);
  706.     }

  707.     try {
  708.         var fos2 = new java.io.FileOutputStream(文件路径);
  709.         fos2.write(javaStr.getBytes("UTF-8"));
  710.         fos2.close();

  711.         printl("✓ 已通过FileOutputStream写入文件(UTF-8):" + 文件路径);

  712.         return true;
  713.     } catch (e3) {
  714.         printl("⚠ UTF-8写入异常:" + e3.message);
  715.     }

  716.     printl("✗ 写入文件失败:" + 文件路径);
  717.     return false;
  718. }

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

  720. // 步骤0:连接数据库(连不上不中止,降级只写TXT备份)
  721. printl("\n========== 步骤0:连接数据库 ==========");
  722. 数据库可用 = dbConnect();
  723. if (数据库可用) 数据库可用 = dbEnsureTable();
  724. if (数据库可用) {
  725.     printl("[DB] 数据存放位置: " + (数据库模式 == "mysql" ? MySQL地址 : SQLite路径));
  726. } else {
  727.     printl("[DB] ⚠️ 数据库不可用,本次只写TXT备份: " + 输出文件路径);
  728. }

  729. // 步骤1:启动千问APP
  730. printl("\n========== 步骤1:启动千问APP ==========");
  731. try {
  732.     app.openApp(千问包名);
  733.     printl("✓ 已启动千问");
  734. } catch (e) {
  735.     printl("⚠ 启动失败:" + e.message);
  736. }
  737. sleep.second(秒 = 5);

  738. // 步骤2:确认进入对话页面
  739. printl("\n========== 步骤2:确认进入对话页面 ==========");
  740. var xml = 获取UI();
  741. var hasRecyclerView = (xml.indexOf("androidx.recyclerview.widget.RecyclerView") !== -1);
  742. if (!hasRecyclerView) {
  743.     printl("⚠ 当前页面可能不是千问对话页,再次等待");
  744.     sleep.second(秒 = 3);
  745.     xml = 获取UI();
  746.     hasRecyclerView = (xml.indexOf("androidx.recyclerview.widget.RecyclerView") !== -1);
  747. }

  748. if (hasRecyclerView) {
  749.     printl("✓ 已进入千问对话页面");
  750. } else {
  751.     printl("⚠ 未检测到消息列表,继续尝试");
  752. }

  753. // 步骤3:循环发送问题并采集回答
  754. printl("\n========== 步骤3:循环发送问题并采集回答 ==========");

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

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

  760. // 开始发送问题
  761. for (var q = 0; q < 要发送的问题列表.length; q++) {
  762.     var 当前问题 = 要发送的问题列表[q];
  763.     printl("\n---- 第 " + (q + 1) + " / " + 要发送的问题列表.length + " 个问题 ----");
  764.     printl("问题:" + 当前问题);

  765.     // 3.1 重新获取UI,定位输入框
  766.     xml = 获取UI();
  767.     var 输入框 = 查找输入框中心(xml);
  768.     if (!输入框) {
  769.         printl("⚠ 未找到输入框,跳过此问题");
  770.         sleep.second(秒 = 问题间隔秒);
  771.         continue;
  772.     }

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

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

  778.     // 3.4 粘贴问题文本
  779.     粘贴文本(当前问题);

  780.     // 3.5 重新获取UI,查找发送按钮
  781.     xml = 获取UI();
  782.     var 发送按钮 = 查找发送按钮中心(xml);

  783.     if (发送按钮) {
  784.         agent.click(发送按钮.x, 发送按钮.y);
  785.         printl("✓ 已点击发送按钮");
  786.     } else {
  787.         // 兜底:按回车键发送
  788.         agent.shell("input keyevent 66");
  789.         printl("⚠ 未找到发送按钮,已尝试按回车发送");
  790.     }

  791.     // 3.6 记录用户问题
  792.     完整对话.push({
  793.         角色: "用户",
  794.         内容: 当前问题
  795.     });

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

  798.     var 新回答文本 = 等待AI回答完成(发送前最后文本);
  799.     if (新回答文本 && 字符串长度(新回答文本) > 0) {
  800.         完整对话.push({
  801.             角色: "AI",
  802.             内容: 新回答文本
  803.         });
  804.         printl("✓ 采集到新回答,当前总对话条数:" + 完整对话.length);
  805.         printl("--- 采集内容开始 ---");
  806.         printl(新回答文本);
  807.         printl("--- 采集内容结束 ---");
  808.     } else {
  809.         printl("⚠ 未能采集到新回答");
  810.     }

  811.     sleep.second(秒 = 问题间隔秒);
  812. }

  813. // 步骤4:入库 + 保存TXT备份
  814. printl("\n========== 步骤4:对话入库 ==========");
  815. var 入库条数 = 0;
  816. if (数据库可用) {
  817.     for (var d = 0; d < 完整对话.length; d++) {
  818.         if (dbInsert(完整对话[d].角色, 完整对话[d].内容)) 入库条数++;
  819.     }
  820.     printl("[DB] ✅ 本次共入库 " + 入库条数 + " 条 | 存放位置: " + (数据库模式 == "mysql" ? MySQL地址 : SQLite路径));
  821.     dbClose();
  822. } else {
  823.     printl("[DB] 跳过入库(数据库不可用)");
  824. }

  825. // TXT 备份照旧
  826. var 输出内容 = 格式化对话(完整对话);
  827. var 写入成功 = 写入文件(输出文件路径, 输出内容);

  828. if (写入成功) {
  829.     printl("✓ TXT备份已保存到:" + 输出文件路径);
  830.     printl("✓ 总对话条数:" + 完整对话.length);
  831. } else {
  832.     printl("✗ TXT备份保存失败,请检查存储权限");
  833. }

  834. // 步骤5:打印全部采集到的对话内容
  835. printl("\n========== 全部采集内容 ==========");
  836. printl(输出内容);
  837. printl("========== 内容结束 ==========");

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


上一篇: 关于找图速度太慢的问题

下一篇: 没有了

举报 回复