 | |  |  | 豆包对话采集示例
- // AIWROK 自动化流程:启动豆包 → 输入问题 → 点击发送 → 等待AI回答 → 采集回答内容 → 保存到文件
- // 全程使用 agent.getXml() 获取界面源码
- // 包名:com.larus.nova(豆包)
- //
- // 使用说明:
- // 1. 修改下方【配置区】中的"要发送的问题列表"
- // 2. 采集结果保存到 /sdcard/豆包对话记录.txt
- print.log("===== 豆包对话采集流程启动 =====");
- print.time();
- // ========== 配置区 ==========
- var 豆包包名 = "com.larus.nova";
- // 输出文件路径
- var 输出文件路径 = "/sdcard/豆包对话记录.txt";
- // 要发送的问题列表(按顺序发送)
- var 要发送的问题列表 = [
- "你好,请用一句话介绍你自己",
- "中国平安股票最近走势如何?",
- "招商银行持有3年卖出需要交股息税吗?"
- ];
- // 发送后等待AI回答的最大秒数(整体超时,最长10秒)
- var 等待回答超时秒 = 10;
- // 检测到新回答出现后,再等待多少秒让回答尽量输出完整(然后直接返回)
- var 回答输出等待秒 = 8;
- // 每个问题之间的间隔秒数
- 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;
- }
- // 采集豆包 message_list 中所有 content_view 下的 TextView 文本(即AI回答)
- // 参数 严格过滤: true=排除短文本按钮等, false=不过滤(用于检测新回答出现,更灵敏)
- function 采集AI回答列表(xml, 严格过滤) {
- if (严格过滤 === undefined) 严格过滤 = true;
- var xmlStr = "" + xml;
- var 回答列表 = [];
- // 定位 message_list 节点
- var listIdx = xmlStr.indexOf('id/message_list"');
- if (listIdx === -1) {
- print.log("⚠ 未找到消息列表节点,可能不在豆包对话页面");
- return 回答列表;
- }
- // 截取 message_list 之后的部分
- var 后面 = xmlStr.substring(listIdx);
- // 遍历所有 content_view 节点
- var 位置 = 0;
- while (true) {
- var cvIdx = 后面.indexOf('id/content_view"', 位置);
- if (cvIdx === -1) break;
- // 从 content_view 起始位置截取一段,找它内部第一个有非空 text 的 TextView
- var 段 = 后面.substring(cvIdx);
- // 找到 content_view 节点开始标签的结束位置 ">"
- var 标签结束 = 段.indexOf(">");
- if (标签结束 === -1) break;
- var 子段 = 段.substring(标签结束 + 1);
- // 在子段中查找第一个有非空 text 的 TextView 节点
- var 文本匹配 = 子段.match(/<node[^>]*?text="([^"]+)"[^>]*?class="android\.widget\.TextView"[^>]*?\/?>/);
- if (!文本匹配) {
- // 备选顺序:先 class 再 text
- 文本匹配 = 子段.match(/<node[^>]*?class="android\.widget\.TextView"[^>]*?text="([^"]+)"[^>]*?\/?>/);
- }
- if (文本匹配 && 文本匹配[1]) {
- var 原始文本 = 文本匹配[1];
- var 解码文本 = 解码HTML实体(原始文本);
- if (严格过滤) {
- // 严格模式:排除按钮文案(复制、分享等),AI回答特征:含换行 或 长度>30
- if (解码文本.indexOf("\n") !== -1 || 字符串长度(解码文本) > 30) {
- 回答列表.push(解码文本);
- }
- } else {
- // 宽松模式:只要有非空文本就算,用于灵敏检测新回答出现
- if (字符串长度(解码文本) > 0) {
- 回答列表.push(解码文本);
- }
- }
- }
- 位置 = cvIdx + 30;
- }
- return 回答列表;
- }
- // 查找输入框中心坐标(resource-id 含 input_text 的 EditText)
- function 查找输入框中心(xml) {
- var xmlStr = "" + xml;
- var idx = xmlStr.indexOf('id/input_text"');
- if (idx === -1) return null;
- var 段 = xmlStr.substring(idx);
- var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (!m) return null;
- var x1 = parseInt(m[1]);
- var y1 = parseInt(m[2]);
- var x2 = parseInt(m[3]);
- var y2 = parseInt(m[4]);
- return {
- x: (x1 + x2) / 2,
- y: (y1 + y2) / 2
- };
- }
- // 查找发送按钮坐标
- // 豆包输入框右侧的 action_speak 节点,输入文字后会变成发送按钮
- // 也查找文字"发送"或 resource-id 含 action_send 的节点
- function 查找发送按钮中心(xml) {
- var xmlStr = "" + xml;
- // 1. 优先查找文字"发送"
- var sendTextIdx = xmlStr.indexOf('text="发送"');
- if (sendTextIdx !== -1) {
- var 段 = xmlStr.substring(sendTextIdx);
- var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (m) {
- return {
- x: (parseInt(m[1]) + parseInt(m[3])) / 2,
- y: (parseInt(m[2]) + parseInt(m[4])) / 2
- };
- }
- }
- // 2. 查找 resource-id 含 action_send
- var sendIdIdx = xmlStr.indexOf('id/action_send"');
- if (sendIdIdx !== -1) {
- var 段2 = xmlStr.substring(sendIdIdx);
- var m2 = 段2.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (m2) {
- return {
- x: (parseInt(m2[1]) + parseInt(m2[3])) / 2,
- y: (parseInt(m2[2]) + parseInt(m2[4])) / 2
- };
- }
- }
- // 3. 兜底:使用 action_speak 节点坐标(输入文字后该按钮变为发送功能)
- var speakIdx = xmlStr.indexOf('id/action_speak"');
- if (speakIdx !== -1) {
- var 段3 = xmlStr.substring(speakIdx);
- var m3 = 段3.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (m3) {
- return {
- x: (parseInt(m3[1]) + parseInt(m3[3])) / 2,
- y: (parseInt(m3[2]) + parseInt(m3[4])) / 2
- };
- }
- }
- return null;
- }
- // 粘贴文本到当前焦点输入框(剪贴板法,兼容中文)
- 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回答底部的展开按钮(向下箭头)
- // 豆包长回答会折叠显示,需要点击展开箭头才能看到完整内容
- // 从XML中动态定位,不依赖硬编码坐标
- function 查找并点击展开按钮(xml) {
- var xmlStr = "" + xml;
-
- // 1. 查找 content-desc 含"展开"或"更多"的按钮
- var expandIdx = xmlStr.indexOf('content-desc="展开"');
- if (expandIdx === -1) {
- expandIdx = xmlStr.indexOf('content-desc="更多"');
- }
- if (expandIdx === -1) {
- expandIdx = xmlStr.indexOf('content-desc="收起"');
- }
-
- if (expandIdx !== -1) {
- var 段 = xmlStr.substring(expandIdx);
- var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (m) {
- var x = (parseInt(m[1]) + parseInt(m[3])) / 2;
- var y = (parseInt(m[2]) + parseInt(m[4])) / 2;
- agent.click(x, y);
- print.log("✓ 已点击展开按钮(content-desc),坐标(" + Math.round(x) + "," + Math.round(y) + ")");
- sleep.second(秒 = 1);
- return true;
- }
- }
- // 2. 查找文本"展开"或"更多"的按钮
- var textExpandIdx = xmlStr.indexOf('text="展开"');
- if (textExpandIdx === -1) {
- textExpandIdx = xmlStr.indexOf('text="更多"');
- }
- if (textExpandIdx !== -1) {
- var 段2 = xmlStr.substring(textExpandIdx);
- var m2 = 段2.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (m2) {
- var x2 = (parseInt(m2[1]) + parseInt(m2[3])) / 2;
- var y2 = (parseInt(m2[2]) + parseInt(m2[4])) / 2;
- agent.click(x2, y2);
- print.log("✓ 已点击展开按钮(文本匹配),坐标(" + Math.round(x2) + "," + Math.round(y2) + ")");
- sleep.second(秒 = 1);
- return true;
- }
- }
- // 3. 兜底:从XML解析最后一个 content_view 的底部中心位置(展开箭头通常在回答底部中间)
- var lastCVIdx = xmlStr.lastIndexOf('id/content_view"');
- if (lastCVIdx !== -1) {
- var cv段 = xmlStr.substring(lastCVIdx);
- var cvM = cv段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (cvM) {
- var cvX1 = parseInt(cvM[1]);
- var cvY1 = parseInt(cvM[2]);
- var cvX2 = parseInt(cvM[3]);
- var cvY2 = parseInt(cvM[4]);
- // 点击 content_view 底部中间位置(展开箭头通常在这里)
- var centerX = (cvX1 + cvX2) / 2;
- var bottomY = cvY2 - 50; // 稍微往上一点,避开边缘
- agent.click(centerX, bottomY);
- print.log("✓ 已点击content_view底部(兜底展开),坐标(" + Math.round(centerX) + "," + Math.round(bottomY) + ")");
- sleep.second(秒 = 1);
- return true;
- }
- }
- // 4. 最终兜底:使用屏幕百分比坐标(仅作为最后备选)
- var pctX = 屏幕宽 / 2;
- var pctY = 屏幕高 * 0.85;
- agent.click(pctX, pctY);
- print.log("✓ 已点击屏幕底部(最终兜底),坐标(" + Math.round(pctX) + "," + Math.round(pctY) + ")");
- sleep.second(秒 = 1);
- return true;
- }
- // 从XML解析 message_list 的边界,然后在其中滑动到底部
- // 不依赖硬编码坐标,换手机也能运行
- function 滑动消息列表到底部(xml) {
- var xmlStr = "" + xml;
- var listIdx = xmlStr.indexOf('id/message_list"');
- if (listIdx === -1) {
- print.log("⚠ 未找到message_list,使用屏幕百分比滑动");
- agent.swip(屏幕宽 / 2, 屏幕高 * 0.3, 屏幕宽 / 2, 屏幕高 * 0.8, 500);
- return;
- }
- var 段 = xmlStr.substring(listIdx);
- var m = 段.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
- if (!m) {
- print.log("⚠ 未找到message_list边界,使用屏幕百分比滑动");
- agent.swip(屏幕宽 / 2, 屏幕高 * 0.3, 屏幕宽 / 2, 屏幕高 * 0.8, 500);
- return;
- }
- var x1 = parseInt(m[1]);
- var y1 = parseInt(m[2]);
- var x2 = parseInt(m[3]);
- var y2 = parseInt(m[4]);
- // 在 message_list 区域内滑动:从上半部分滑到下半部分
- var startX = (x1 + x2) / 2;
- var startY = y1 + (y2 - y1) * 0.2; // 区域上方20%位置
- var endX = startX;
- var endY = y1 + (y2 - y1) * 0.8; // 区域下方20%位置
- agent.swip(startX, startY, endX, endY, 500);
- print.log("✓ 已在message_list内滑动,区域[" + x1 + "," + y1 + "][" + x2 + "," + y2 + "]");
- }
- // 等待AI回答完成
- // 逻辑:
- // 阶段1:宽松检测 - 等待最后一条回答文本变化(新回答出现)
- // 阶段2:检测到新回答后,先点击展开按钮,再等待回答输出等待秒,然后直接返回
- // 超时兜底:若整体超时但能采到非空文本,也返回当前内容
- // 参数:发送前的最后一条AI回答文本(用于对比检测新回答)
- // 返回:新回答的文本内容
- function 等待AI回答完成(发送前的最后文本) {
- var 已耗时 = 0;
- var 检查间隔秒 = 2;
- var 已检测到新回答 = false;
- var 检测到的时间点 = 0;
- var 已点击展开按钮 = false;
- while (已耗时 < 等待回答超时秒) {
- sleep.second(秒 = 检查间隔秒);
- 已耗时 += 检查间隔秒;
- var xml = 获取UI();
- if (!已检测到新回答) {
- // 阶段1:宽松模式检测新回答是否出现
- var 宽松列表 = 采集AI回答列表(xml, false);
- if (宽松列表.length === 0) {
- continue;
- }
- var 当前最后文本 = 宽松列表[宽松列表.length - 1];
- if (当前最后文本 !== 发送前的最后文本 && 字符串长度(当前最后文本) > 0) {
- print.log("✓ 检测到新回答出现(" + 已耗时 + "秒),等待" + 回答输出等待秒 + "秒后采集");
- 已检测到新回答 = true;
- 检测到的时间点 = 已耗时;
- }
- } else {
- // 阶段2:已检测到新回答
- var 已等待 = 已耗时 - 检测到的时间点;
-
- // 先点击展开按钮(只点击一次)
- if (!已点击展开按钮 && 已等待 >= 2) {
- 查找并点击展开按钮(xml);
- 已点击展开按钮 = true;
- }
-
- // 再等回答输出等待秒后直接返回
- if (已等待 >= 回答输出等待秒) {
- var 严格列表 = 采集AI回答列表(xml, true);
- if (严格列表.length > 0) {
- var 最终回答 = 严格列表[严格列表.length - 1];
- if (最终回答 !== 发送前的最后文本 && 字符串长度(最终回答) > 0) {
- print.log("✓ 已等待" + 已等待 + "秒,返回采集结果");
- return 最终回答;
- }
- }
- // 严格模式没采到,用宽松模式
- var 宽松列表2 = 采集AI回答列表(xml, false);
- if (宽松列表2.length > 0) {
- var 最终回答2 = 宽松列表2[宽松列表2.length - 1];
- if (最终回答2 !== 发送前的最后文本 && 字符串长度(最终回答2) > 5) {
- print.log("✓ 已等待" + 已等待 + "秒(宽松模式),返回采集结果");
- return 最终回答2;
- }
- }
- }
- }
- }
- // 超时兜底:尽量返回当前能采集到的最后一条回答
- 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];
- }
- // 格式化对话内容输出
- function 格式化对话(对话列表) {
- var 输出 = "";
- 输出 += "========================================\n";
- 输出 += "豆包对话采集记录\n";
- 输出 += "采集时间:" + new Date().toString() + "\n";
- 输出 += "对话轮数:" + 对话列表.length + "\n";
- 输出 += "========================================\n\n";
- for (var i = 0; i < 对话列表.length; i++) {
- var 对话 = 对话列表[i];
- if (对话.角色 === "用户") {
- 输出 += "【用户提问 " + (i + 1) + "】\n";
- } else {
- 输出 += "【豆包回答 " + (i + 1) + "】\n";
- }
- 输出 += 对话.内容 + "\n";
- 输出 += "----------------------------------------\n\n";
- }
- 输出 += "\n===== 采集结束 =====\n";
- return 输出;
- }
- // 写入文件(UTF-8 BOM格式,兼容Windows记事本)
- function 写入文件(文件路径, 内容) {
- try {
- var fos = new java.io.FileOutputStream(文件路径);
- var osw = new java.io.OutputStreamWriter(fos, "UTF-8");
- osw.write('\ufeff');
- osw.write(内容);
- osw.flush();
- osw.close();
- fos.close();
- print.log("✓ 已写入文件(UTF-8 BOM):" + 文件路径);
-
- var catResult = agent.shell("cat " + 文件路径 + " | head -3");
- print.log("✓ cat验证结果:" + catResult);
-
- var fileSize = agent.shell("ls -la " + 文件路径);
- print.log("✓ 文件信息:" + fileSize);
-
- return true;
- } catch (e) {
- print.log("⚠ UTF-8 BOM写入异常:" + e.message);
- }
- try {
- var fos2 = new java.io.FileOutputStream(文件路径);
- fos2.write(239);
- fos2.write(187);
- fos2.write(191);
- fos2.write(内容.getBytes("UTF-8"));
- fos2.close();
- print.log("✓ 已写入文件(字节写入BOM):" + 文件路径);
- return true;
- } catch (e2) {
- print.log("⚠ 字节写入异常:" + e2.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();
- if (xml.indexOf("message_list") === -1) {
- print.log("⚠ 当前页面可能不是豆包对话页,再次等待");
- sleep.second(秒 = 3);
- xml = 获取UI();
- }
- if (xml.indexOf("message_list") !== -1) {
- print.log("✓ 已进入豆包对话页面");
- } else {
- print.log("⚠ 未检测到消息列表,继续尝试");
- }
- // 步骤3:循环发送问题并采集回答
- print.log("\n========== 步骤3:循环发送问题并采集回答 ==========");
- // 完整对话列表(包含用户问题和AI回答)
- var 完整对话 = [];
- // 先采集当前页面已有的AI回答
- var 已有回答 = 采集AI回答列表(xml);
- print.log("✓ 当前页面已有AI回答数量:" + 已有回答.length);
- for (var i = 0; i < 已有回答.length; i++) {
- 完整对话.push({
- 角色: "AI",
- 内容: 已有回答[i]
- });
- }
- // 开始发送问题
- for (var q = 0; q < 要发送的问题列表.length; q++) {
- var 当前问题 = 要发送的问题列表[q];
- 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===== 豆包对话采集流程结束 =====");
复制代码
| |  | |  |
|