YYPOST群发软件 发表于 7 天前

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

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


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

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

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

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

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

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

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

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

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

// ========== 工具函数 ==========

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

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

// HTML实体解码(千问TextView中的换行符等会被编码为
' 等)
function 解码HTML实体(s) {
    var str = "" + s;
    str = str.replace(/
/g, "\n");
    str = str.replace(/
/g, "\r");
    str = str.replace(/        /g, "\t");
    str = str.replace(/'/g, "'");
    str = str.replace(/"/g, '"');
    str = str.replace(/"/g, '"');
    str = str.replace(/'/g, "'");
    str = str.replace(/&/g, "&");
    str = str.replace(/</g, "<");
    str = str.replace(/>/g, ">");
    return str;
}

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

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

    while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
      var rx1 = parseInt(rvMatch);
      var ry1 = parseInt(rvMatch);
      var rx2 = parseInt(rvMatch);
      var ry2 = parseInt(rvMatch);
      var 高度 = ry2 - ry1;
      var 宽度 = rx2 - rx1;

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

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

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

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

    while ((tvMatch = tvPattern.exec(xmlStr)) !== null) {
      var tvNode = tvMatch;

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

      // 从节点中提取边界
      var boundsMatch = tvNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (!boundsMatch) continue;
      var tx1 = parseInt(boundsMatch);
      var ty1 = parseInt(boundsMatch);
      var tx2 = parseInt(boundsMatch);
      var ty2 = parseInt(boundsMatch);

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

      if (!在区域内) continue;

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

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

    return 回答列表;
}

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

    var inputIdx = xmlStr.indexOf('text="发消息或按住说话..."');
    if (inputIdx === -1) {
      inputIdx = xmlStr.indexOf("text='发消息或按住说话...'");
    }
    if (inputIdx !== -1) {
      var 段 = xmlStr.substring(inputIdx);
      var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (m) {
            print.log("✓ 输入框定位:hint文本匹配");
            return {
                x: (parseInt(m) + parseInt(m)) / 2,
                y: (parseInt(m) + parseInt(m)) / 2
            };
      }
    }

    var editTextPattern = /<node[^>]*?class="android\.widget\.EditText"[^>]*?\/?>/g;
    var etMatch;
    while ((etMatch = editTextPattern.exec(xmlStr)) !== null) {
      var etNode = etMatch;
      var boundsMatch = etNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (boundsMatch) {
            var ex1 = parseInt(boundsMatch);
            var ey1 = parseInt(boundsMatch);
            var ex2 = parseInt(boundsMatch);
            var ey2 = parseInt(boundsMatch);
            
            if (ey1 > 屏幕高 * 0.8) {
                print.log("✓ 输入框定位:EditText匹配");
                return {
                  x: (ex1 + ex2) / 2,
                  y: (ey1 + ey2) / 2
                };
            }
      }
    }

    var viewGroupPattern = /<node[^>]*?class="android\.view\.ViewGroup"[^>]*?clickable="true"[^>]*?\/?>/g;
    var vgMatch;
    while ((vgMatch = viewGroupPattern.exec(xmlStr)) !== null) {
      var vgNode = vgMatch;
      var boundsMatch = vgNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (boundsMatch) {
            var vx1 = parseInt(boundsMatch);
            var vy1 = parseInt(boundsMatch);
            var vx2 = parseInt(boundsMatch);
            var vy2 = parseInt(boundsMatch);
            
            var vWidth = vx2 - vx1;
            var vHeight = vy2 - vy1;
            
            if (vy1 > 屏幕高 * 0.8 && vWidth > 屏幕宽 * 0.4 && vWidth < 屏幕宽 * 0.8) {
                print.log("✓ 输入框定位:ViewGroup匹配");
                return {
                  x: (vx1 + vx2) / 2,
                  y: (vy1 + vy2) / 2
                };
            }
      }
    }

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

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

    // 1. 优先查找文字"发送"
    var sendTextIdx = xmlStr.indexOf('text="发送"');
    if (sendTextIdx === -1) {
      sendTextIdx = xmlStr.indexOf("text='发送'");
    }
    if (sendTextIdx !== -1) {
      var 段 = xmlStr.substring(sendTextIdx);
      var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (m) {
            print.log("✓ 发送按钮定位:文本匹配");
            return {
                x: (parseInt(m) + parseInt(m)) / 2,
                y: (parseInt(m) + parseInt(m)) / 2
            };
      }
    }

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

    while ((clickMatch = clickablePattern.exec(xmlStr)) !== null) {
      var cx1 = parseInt(clickMatch);
      var cy1 = parseInt(clickMatch);
      var cx2 = parseInt(clickMatch);
      var cy2 = parseInt(clickMatch);

      if (cy1 > 屏幕高 * 0.85) {
            candidates.push({
                x1: cx1, y1: cy1, x2: cx2, y2: cy2,
                x: (cx1 + cx2) / 2,
                y: (cy1 + cy2) / 2
            });
      }
    }

    if (candidates.length > 0) {
      candidates.sort(function(a, b) {
            return b.x1 - a.x1;
      });
      print.log("✓ 找到" + candidates.length + "个候选按钮,选择最右侧的,坐标(" + Math.round(candidates.x) + "," + Math.round(candidates.y) + ")");
      return {
            x: candidates.x,
            y: candidates.y
      };
    }

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

    while ((flMatch = flPattern.exec(xmlStr)) !== null) {
      var flNode = flMatch;
      
      if (flNode.indexOf('clickable="true"') === -1) continue;
      
      var boundsMatch = flNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (!boundsMatch) continue;
      
      var fx1 = parseInt(boundsMatch);
      var fy1 = parseInt(boundsMatch);
      var fx2 = parseInt(boundsMatch);
      var fy2 = parseInt(boundsMatch);

      if (fy1 > 屏幕高 * 0.85) {
            flCandidates.push({
                x1: fx1, y1: fy1, x2: fx2, y2: fy2,
                x: (fx1 + fx2) / 2,
                y: (fy1 + fy2) / 2
            });
      }
    }

    if (flCandidates.length > 0) {
      flCandidates.sort(function(a, b) {
            return b.x1 - a.x1;
      });
      print.log("✓ 找到" + flCandidates.length + "个FrameLayout候选,选择最右侧的,坐标(" + Math.round(flCandidates.x) + "," + Math.round(flCandidates.y) + ")");
      return {
            x: flCandidates.x,
            y: flCandidates.y
      };
    }

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

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

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

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

// 查找并点击AI回答底部的展开按钮(向下箭头)
// 千问长回答会折叠显示,需要点击底部向下箭头才能看到完整内容
function 查找并点击展开按钮(xml) {
    var xmlStr = "" + xml;
   
    // 1. 查找消息列表底部中间的小区域clickable节点(向下箭头特征)
    var rvPattern = /<node[^>]*?class="androidx\.recyclerview\.widget\.RecyclerView"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
    var rvMatch;
    while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
      var rx1 = parseInt(rvMatch);
      var ry1 = parseInt(rvMatch);
      var rx2 = parseInt(rvMatch);
      var ry2 = parseInt(rvMatch);
      var 高度 = ry2 - ry1;
      if (高度 > 500) {
            var viewPattern = /<node[^>]*?clickable="true"[^>]*?\/?>/g;
            var viewMatch;
            while ((viewMatch = viewPattern.exec(xmlStr)) !== null) {
                var vNode = viewMatch;
                var boundsMatch = vNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
                if (!boundsMatch) continue;
               
                var vx1 = parseInt(boundsMatch);
                var vy1 = parseInt(boundsMatch);
                var vx2 = parseInt(boundsMatch);
                var vy2 = parseInt(boundsMatch);
               
                var vWidth = vx2 - vx1;
                var vHeight = vy2 - vy1;
               
                var 在区域内 = (vx1 >= rx1 && vx2 <= rx2 && vy1 >= ry1 && vy2 <= ry2);
                var 是小按钮 = (vWidth < 200 && vHeight < 200);
                var 在底部 = (vy1 > ry1 + 高度 * 0.7);
               
                if (在区域内 && 是小按钮 && 在底部) {
                  var centerX = (vx1 + vx2) / 2;
                  var centerY = (vy1 + vy2) / 2;
                  agent.click(centerX, centerY);
                  print.log("✓ 已点击展开箭头(消息列表底部小按钮),坐标(" + Math.round(centerX) + "," + Math.round(centerY) + ")");
                  sleep.second(秒 = 2);
                  return true;
                }
            }
            break;
      }
    }

    // 2. 查找文本"查看全部"的按钮
    var textExpandIdx = xmlStr.indexOf('text="查看全部"');
    if (textExpandIdx === -1) {
      textExpandIdx = xmlStr.indexOf("text='查看全部'");
    }
    if (textExpandIdx !== -1) {
      var 段 = xmlStr.substring(textExpandIdx);
      var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (m) {
            var x = (parseInt(m) + parseInt(m)) / 2;
            var y = (parseInt(m) + parseInt(m)) / 2;
            agent.click(x, y);
            print.log("✓ 已点击查看全部按钮,坐标(" + Math.round(x) + "," + Math.round(y) + ")");
            sleep.second(秒 = 2);
            return true;
      }
    }

    // 3. 查找 content-desc 含"展开"或"更多"的按钮
    var expandIdx = xmlStr.indexOf('content-desc="展开"');
    if (expandIdx === -1) {
      expandIdx = xmlStr.indexOf('content-desc="更多"');
    }
    if (expandIdx !== -1) {
      var 段2 = xmlStr.substring(expandIdx);
      var m2 = 段2.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (m2) {
            var x2 = (parseInt(m2) + parseInt(m2)) / 2;
            var y2 = (parseInt(m2) + parseInt(m2)) / 2;
            agent.click(x2, y2);
            print.log("✓ 已点击展开按钮(content-desc),坐标(" + Math.round(x2) + "," + Math.round(y2) + ")");
            sleep.second(秒 = 1);
            return true;
      }
    }

    // 4. 查找文本"展开"或"更多"的按钮
    var textExpandIdx2 = xmlStr.indexOf('text="展开"');
    if (textExpandIdx2 === -1) {
      textExpandIdx2 = xmlStr.indexOf('text="更多"');
    }
    if (textExpandIdx2 !== -1) {
      var 段3 = xmlStr.substring(textExpandIdx2);
      var m3 = 段3.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (m3) {
            var x3 = (parseInt(m3) + parseInt(m3)) / 2;
            var y3 = (parseInt(m3) + parseInt(m3)) / 2;
            agent.click(x3, y3);
            print.log("✓ 已点击展开按钮(文本匹配),坐标(" + Math.round(x3) + "," + Math.round(y3) + ")");
            sleep.second(秒 = 1);
            return true;
      }
    }

    // 5. 查找消息列表底部区域内的ImageView(箭头图标)
    var ivPattern = /<node[^>]*?class="android\.widget\.ImageView"[^>]*?clickable="true"[^>]*?\/?>/g;
    var ivMatch;
    while ((ivMatch = ivPattern.exec(xmlStr)) !== null) {
      var ivNode = ivMatch;
      var boundsMatch = ivNode.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
      if (!boundsMatch) continue;
      
      var ivx1 = parseInt(boundsMatch);
      var ivy1 = parseInt(boundsMatch);
      var ivx2 = parseInt(boundsMatch);
      var ivy2 = parseInt(boundsMatch);
      
      var 在底部 = (ivy1 > 屏幕高 * 0.5);
      var 在中间 = (ivx1 > 屏幕宽 * 0.3 && ivx2 < 屏幕宽 * 0.7);
      
      if (在底部 && 在中间) {
            var centerX = (ivx1 + ivx2) / 2;
            var centerY = (ivy1 + ivy2) / 2;
            agent.click(centerX, centerY);
            print.log("✓ 已点击ImageView(箭头图标),坐标(" + Math.round(centerX) + "," + Math.round(centerY) + ")");
            sleep.second(秒 = 2);
            return true;
      }
    }

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

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

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

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

    while ((rvMatch = rvPattern.exec(xmlStr)) !== null) {
      var rx1 = parseInt(rvMatch);
      var ry1 = parseInt(rvMatch);
      var rx2 = parseInt(rvMatch);
      var ry2 = parseInt(rvMatch);
      var 高度 = ry2 - ry1;

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

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

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

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

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

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

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

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

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

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

            // 检查文本是否停止变化
            if (当前文本 === 上次文本) {
                文本不变次数++;
            } else {
                文本不变次数 = 0;
                上次文本 = 当前文本;
            }

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

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

    // 超时兜底:尽量返回当前能采集到的最后一条回答
    print.log("⚠ 等待AI回答超时(" + 等待回答超时秒 + "秒),尝试返回当前内容");
    var xml = 获取UI();
    var 兜底列表 = 采集AI回答列表(xml, true);
    if (兜底列表.length > 0) {
      var 兜底文本 = 兜底列表[兜底列表.length - 1];
      if (兜底文本 !== 发送前的最后文本) {
            print.log("✓ 超时兜底:返回当前采集到的回答");
            return 兜底文本;
      }
    }
    var 兜底宽松 = 采集AI回答列表(xml, false);
    if (兜底宽松.length > 0) {
      var 兜底文本2 = 兜底宽松[兜底宽松.length - 1];
      if (兜底文本2 !== 发送前的最后文本 && 字符串长度(兜底文本2) > 5) {
            print.log("✓ 超时兜底(宽松):返回当前采集到的回答");
            return 兜底文本2;
      }
    }
    return null;
}

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

// 格式化对话内容输出(TXT格式)
function 格式化对话(对话列表) {
    var result = "========== 千问对话记录 ==========\n";
    result += "采集时间:" + new Date().toString() + "\n";
    result += "对话轮数:" + 对话列表.length + "\n";
    result += "==================================\n\n";
   
    for (var i = 0; i < 对话列表.length; i++) {
      result += "【" + 对话列表.角色 + "】\n";
      result += 对话列表.内容 + "\n";
      result += "----------------------------------\n";
      if (i < 对话列表.length - 1) {
            result += "\n";
      }
    }
   
    return result;
}

// 写入文件(TXT格式,GBK编码)
function 写入文件(文件路径, 内容) {
    var javaStr = new java.lang.String(内容);
   
    try {
      var fos = new java.io.FileOutputStream(文件路径);
      fos.write(javaStr.getBytes("GBK"));
      fos.close();
      
      print.log("✓ 已写入文件(TXT格式,GBK编码):" + 文件路径);
      
      var catResult = agent.shell("cat " + 文件路径 + " | head -2");
      print.log("✓ cat验证结果:" + catResult);
      
      return true;
    } catch (e) {
      print.log("⚠ FileOutputStream写入异常:" + e.message);
    }

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

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

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

// ========== 主流程开始 ==========

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

页: [1]
查看完整版本: 示例千问自动采集文章保存记事本