Files
xiaoli-cli/plugins/operit/automatic_xiaohongshu_assistant.ts
T
xiaoli-cli bot dfe21298b9 release: v5.4.2 - terminal模拟器 + Operit桥接
新增插件:
- terminal: 持久交互式终端,纯标准库,全平台,13个测试
- operit_bridge: Operit AI工具格式桥接,47个脚本288个工具,12个测试

其他:
- plugins/operit/: 47个Operit官方脚本
- 更新plugin_registry.json/plugins_config.py
- 更新about.txt/CHANGELOG.md至v5.4.2
2026-06-23 08:50:15 +00:00

1260 lines
51 KiB
TypeScript

/*
METADATA
{
// 小红书智能助手包
name: "Automatic_xiaohongshu_assistant",
display_name: {
zh: "小红书自动化助手"
en: "Xiaohongshu Automation Assistant"
}description: { zh: "高级小红书智能助手,通过UI自动化技术实现小红书应用交互,支持内容浏览、搜索查看、评论互动、内容发布等功能,为AI赋予小红书社交和内容创作能力。适用于内容营销、社交互动、生活分享等场景。", en: "Advanced Xiaohongshu (RED) assistant powered by UI automation. Supports browsing feeds, searching and viewing content, commenting and interactions, and publishing posts, enabling AI-driven social interaction and content creation. Useful for content marketing and social engagement." },
// Tools in this package
category: "Automatic"
tools: [
{
name: "workflow_guide",
description: { zh: "小红书助手工具使用流程指南。工具主要分为几类:\n1. **内容发现**: 使用 `browse_home_feed` 浏览主页,或用 `search_content` 搜索特定内容。\n2. **查看与互动**: 发现帖子后,用 `view_post` 查看详情。在详情页内可执行 `like_post`, `collect_post`, `comment_post`, `follow_user`, `unfollow_user` 等并列互动操作。\n3. **内容发布**: `publish_post` 是一个独立的发布流程。\n4. **导航**: `navigate_to_home`, `back_to_feed` 等工具用于在各页面间跳转。", en: "Workflow guide for the Xiaohongshu assistant:\n1. **Discover**: browse the home feed via `browse_home_feed`, or search via `search_content`.\n2. **View & interact**: after finding a post, open it with `view_post`. On the detail page you can run `like_post`, `collect_post`, `comment_post`, `follow_user`, `unfollow_user`, etc.\n3. **Publish**: `publish_post` is a dedicated publishing flow.\n4. **Navigation**: use tools like `navigate_to_home` and `back_to_feed` to switch between pages." },
parameters: []
},
{
name: "browse_home_feed",
description: { zh: "浏览小红书主页信息流,获取推荐内容", en: "Browse the Xiaohongshu home feed and collect recommended content." },
parameters: [
{
name: "scroll_count",
description: { zh: "滚动次数,控制浏览的内容量,默认为3次", en: "Number of scrolls (controls how much content to browse; default: 3)." },
type: "number",
required: false
},
{
name: "collect_posts",
description: { zh: "是否收集帖子信息,默认为true", en: "Whether to collect post info (default: true)." },
type: "boolean",
required: false
}
]
},
{
name: "search_content",
description: { zh: "在小红书中搜索内容、用户或话题", en: "Search content, users, or topics in Xiaohongshu." },
parameters: [
{
name: "keyword",
description: { zh: "搜索关键词", en: "Search keyword." },
type: "string",
required: true
},
{
name: "search_type",
description: { zh: "搜索类型:comprehensive(综合)、note(笔记)、user(用户)、topic(话题)", en: "Search type: comprehensive, note, user, topic." },
type: "string",
required: false
}
]
},
{
name: "view_post",
description: { zh: "查看指定的帖子详情", en: "View details of a specific post." },
parameters: [
{
name: "post_title",
description: { zh: "要查看的帖子标题关键词", en: "Keyword to match the post title (optional)." },
type: "string",
required: false
},
{
name: "post_index",
description: { zh: "要查看的帖子在列表中的索引位置(从1开始)", en: "Index of the post in the list (1-based, optional)." },
type: "number",
required: false
}
]
},
{
name: "like_post",
description: { zh: "给当前帖子点赞", en: "Like the current post." },
parameters: []
},
{
name: "collect_post",
description: { zh: "收藏当前帖子", en: "Save (collect) the current post." },
parameters: [
{
name: "collection_name",
description: { zh: "收藏夹名称,留空则使用默认收藏夹", en: "Collection name (leave empty to use the default collection)." },
type: "string",
required: false
}
]
},
{
name: "follow_user",
description: { zh: "关注当前帖子的作者", en: "Follow the author of the current post." },
parameters: []
},
{
name: "unfollow_user",
description: { zh: "取消关注当前帖子的作者", en: "Unfollow the author of the current post." },
parameters: []
},
{
name: "comment_post",
description: { zh: "在当前帖子下发表评论", en: "Post a comment under the current post." },
parameters: [
{
name: "comment_text",
description: { zh: "要发表的评论内容", en: "Comment text to post." },
type: "string",
required: true
}
]
},
{
name: "get_post_info",
description: { zh: "获取当前帖子的详细信息,包括标题、作者、点赞数、评论数、发布日期和热门评论等", en: "Get detailed info of the current post: title, author, likes, comments, publish date, hot comments, etc." },
parameters: []
},
{
name: "publish_post",
description: { zh: "发布新的帖子内容", en: "Publish a new post." },
parameters: [
{
name: "content_text",
description: { zh: "帖子文字内容", en: "Post text content." },
type: "string",
required: true
},
{
name: "image_paths",
description: { zh: "图片文件路径列表,用逗号分隔", en: "Comma-separated list of image file paths (optional)." },
type: "string",
required: false
},
{
name: "tags",
description: { zh: "话题标签,用逗号分隔", en: "Comma-separated topic tags (optional)." },
type: "string",
required: false
},
{
name: "location",
description: { zh: "地理位置信息", en: "Location information (optional)." },
type: "string",
required: false
}
]
},
{
name: "navigate_to_home",
description: { zh: "导航到小红书首页", en: "Navigate to the Xiaohongshu home page." },
parameters: []
},
{
name: "navigate_to_profile",
description: { zh: "导航到个人主页", en: "Navigate to the profile page." },
parameters: []
},
{
name: "navigate_to_search",
description: { zh: "导航到搜索页面", en: "Navigate to the search page." },
parameters: []
},
{
name: "navigate_to_publish",
description: { zh: "导航到发布页面", en: "Navigate to the publish page." },
parameters: []
},
{
name: "back_to_feed",
description: { zh: "从帖子详情页返回到信息流列表", en: "Return from the post detail page back to the feed list." },
parameters: []
}
]
}*/
interface Array<T> {
at(index: number): T | undefined;
}
const XiaohongshuAssistant = (function () {
// 添加 Array.prototype.at 支持
Array.prototype.at = function (index: number) {
if (index < 0) {
index = this.length + index;
}
return this[index];
}
interface Post {
title: string;
author: string;
likes: string;
}
interface PostInfo {
index: number;
title: string;
author: string;
likes: string;
element: UINode;
}
interface Comment {
author: string;
content: string;
likes: string;
date: string;
}
interface ToolResponse {
success: boolean;
message: string;
[key: string]: unknown;
}
// 小红书应用包名和主要Activity
const XIAOHONGSHU_PACKAGE = "com.xingin.xhs";
const MAIN_ACTIVITY = "com.xingin.xhs.activity.SplashActivity";
const HOME_ACTIVITY = "com.xingin.xhs.index.v2.IndexActivityV2";
// Helper to create response objects
function createResponse(success: boolean, message: string, data: object | string = {}) {
if (typeof data === 'string') {
return { success: true, message: message, data: data };
}
return { success, message, ...data };
}
// Helper to find a UI element and click it
async function findAndClick(finder: () => Promise<UINode | null | undefined>): Promise<boolean> {
const element = await finder();
if (element) {
await element.click();
await Tools.System.sleep(1000);
return true;
}
return false;
}
// Helper to find a UI element by text and click it
async function findAndClickByText(text: string): Promise<boolean> {
return findAndClick(async () => (await UINode.getCurrentPage()).findByText(text));
}
async function ensureMain(packageName: string = XIAOHONGSHU_PACKAGE): Promise<boolean> {
let pageInfo = await Tools.UI.getPageInfo();
// 1. 如果不在小红书app,则启动
if (pageInfo.packageName !== packageName) {
await Tools.System.startApp(packageName);
await Tools.System.sleep(3000);
pageInfo = await Tools.UI.getPageInfo();
}
// 2. 检查是否在首页
const isAlreadyOnHome = async () => {
const page = await UINode.getCurrentPage();
const followButton = page.findByContentDesc("关注");
const discoverButton = page.findByContentDesc("发现");
const searchButton = page.findByContentDesc("搜索");
return followButton && discoverButton && searchButton;
};
if (await isAlreadyOnHome()) {
console.log("已经在首页。");
return true;
}
// 3. 尝试通过返回回到首页
console.log("尝试返回到首页。");
for (let i = 0; i < 4; i++) {
await Tools.UI.pressKey("KEYCODE_BACK");
await Tools.System.sleep(1000);
if (await isAlreadyOnHome()) {
console.log("成功回到首页。");
return true;
}
pageInfo = await Tools.UI.getPageInfo();
if (pageInfo.packageName !== packageName) {
console.log("返回时退出了app。");
break; // Exit loop if we've exited the app
}
}
// 4. 如果返回失败,重启app
console.log("返回失败,重启app。");
await Tools.System.stopApp(packageName);
await Tools.System.sleep(1000);
await Tools.System.startApp(packageName);
await Tools.System.sleep(5000);
pageInfo = await Tools.UI.getPageInfo();
if (pageInfo.packageName === packageName) {
console.log("App重启成功。");
return true;
}
console.error(`无法导航到 ${packageName} 的首页。`);
return false;
}
// --- 小红书自动化逻辑 ---
let lastBrowseResults: Post[] = [];
async function browse_home_feed(params: { scroll_count?: number, collect_posts?: boolean }) {
const { scroll_count = 5, collect_posts = false } = params;
const posts: Post[] = [];
console.log(`开始浏览, 滑动次数: ${scroll_count}, 是否收集帖子: ${collect_posts}`);
for (let i = 0; i < scroll_count; i++) {
console.log(`第 ${i + 1} 次滑动...`);
if (collect_posts) {
const currentPage = await UINode.getCurrentPage();
const searchScopeNode = currentPage;
console.log("将在整个页面范围内查找帖子。");
// 为了调试,我们先找到所有 FrameLayout 并打印它们的 contentDesc
const allFrameLayouts = searchScopeNode.findAll(n => n.className === 'FrameLayout');
console.log(`[调试] 在查找范围内找到了 ${allFrameLayouts.length} 个 FrameLayout 节点。`);
for (const frame of allFrameLayouts) {
if (frame.contentDesc) {
console.log(`[调试] FrameLayout contentDesc: "${frame.contentDesc}"`);
}
}
// 正式筛选帖子节点
const postNodes = allFrameLayouts.filter(n =>
n.contentDesc && n.contentDesc.includes(' 来自')
);
console.log(`本次查找共发现 ${postNodes.length} 个帖子。`);
for (const postNode of postNodes) {
const desc = postNode.contentDesc;
if (desc) {
try {
const parts = desc.split(' 来自');
if (parts.length > 1) {
const authorAndLikesStr = parts.pop()!.trim();
let title = parts.join(' 来自').trim();
// 移除 "笔记" or "视频" 前缀, 包括中间有空格或换行的情况
title = title.replace(/^(?:笔\s*记|视\s*频)\s*/, '');
const words = authorAndLikesStr.split(/\s+/);
if (words.length > 0) {
const likesWithSuffix = words.pop();
const author = words.join(' ');
if (title && likesWithSuffix && likesWithSuffix.endsWith('赞')) {
const post = {
title,
author: author || "未知作者",
likes: likesWithSuffix.replace('赞', '').trim()
};
if (!posts.some(p => p.title === post.title && p.author === post.author)) {
console.log(` -> 收集到新帖子: ${post.title} | 作者: ${post.author} | 赞: ${post.likes}`);
posts.push(post);
}
}
}
}
} catch (e) {
console.log(`解析时出现异常: "${desc}"`, e);
}
}
}
}
await Tools.UI.swipe(500, 1800, 500, 500); // 从下往上滑动
await Tools.System.sleep(2500); // 等待新内容加载
}
console.log(`浏览结束, 共收集到 ${posts.length} 个独立帖子。`);
return createResponse(true, "成功浏览主页信息流", { posts: posts });
}
async function search_content(params: { keyword: string; search_type?: string }): Promise<ToolResponse> {
const { keyword, search_type = "comprehensive" } = params;
console.log(`搜索内容: ${keyword}, 类型: ${search_type}`);
// 确保小红书已在前台运行
if (!await ensureMain()) {
return createResponse(false, "无法启动或切换到小红书");
}
// 1. 点击主页右上角的搜索图标
let page = await UINode.getCurrentPage();
const searchIcon = page.findByContentDesc("搜索");
if (!searchIcon) {
return createResponse(false, "在主页上找不到带 '搜索' contentDesc 的图标");
}
await searchIcon.click();
await Tools.System.sleep(2000); // 等待搜索页面加载
// 2. 在搜索页面,找到输入框并输入文字
page = await UINode.getCurrentPage();
const searchInput = page.findByClass("EditText");
if (!searchInput) {
return createResponse(false, "在搜索页面找不到输入框");
}
await searchInput.click();
await Tools.System.sleep(1000);
await Tools.UI.setText(keyword);
await Tools.System.sleep(500);
// 3. 点击“搜索”按钮
page = await UINode.getCurrentPage();
const searchButtons = page.findAllByText("搜索");
const searchButton = searchButtons.at(-1); // 通常,实际的搜索按钮是最后一个
if (!searchButton) {
console.log("找不到文本为'搜索'的按钮,尝试按回车键");
await Tools.UI.pressKey("KEYCODE_ENTER");
} else {
await searchButton.click();
}
await Tools.System.sleep(3000); // 等待搜索结果加载
// 应用搜索类型过滤
if (search_type !== "comprehensive") {
await applySearchFilter(search_type);
}
// 获取搜索结果
const results = await getSearchResults();
return createResponse(true, `搜索到 ${results.length} 条相关内容`, {
keyword: keyword,
search_type: search_type,
results: results.map(({ element, ...rest }) => rest), // 移除element属性
result_count: results.length
});
}
async function applySearchFilter(filterType: string): Promise<void> {
console.log(`应用搜索过滤器: ${filterType}`);
const page = await UINode.getCurrentPage();
let filterButton;
switch (filterType) {
case "note":
filterButton = page.findByText("笔记");
break;
case "user":
filterButton = page.findByText("用户");
break;
case "topic":
filterButton = page.findByText("话题");
break;
}
if (filterButton) {
await filterButton.click();
await Tools.System.sleep(2000);
}
}
function extractPostInfoFromNode(node: UINode, index: number): PostInfo | null {
// 改进视频帖子过滤逻辑
const relativeLayout = node.findByClass("RelativeLayout");
if (relativeLayout) {
const directImageViews = relativeLayout.children.filter(child => child.className === "ImageView");
if (directImageViews.length > 1) {
// console.log("检测到视频帖子(多个直接ImageView子节点),已跳过。");
return null;
}
}
const allTextViews = node.findAllByClass("TextView");
// 过滤广告
if (allTextViews.some(tv => tv.text === "广告")) {
// console.log("检测到广告,已跳过。")
return null;
}
if (allTextViews.length < 2) return null; // 需要标题和其他信息
// 1. 寻找标题:通常是当中最长的文本
const sortedByLength = [...allTextViews].sort((a, b) => (b.text?.length || 0) - (a.text?.length || 0));
const titleNode = sortedByLength[0];
if (!titleNode || !titleNode.text || titleNode.text.length < 5) return null;
const title = titleNode.text;
const otherTextViews = allTextViews.filter(tv => tv !== titleNode);
// 2. 寻找点赞数:包含数字,可能带“万”
const likesRegex = /^\s*\d+(\.\d+)?\s*万?\s*$/;
const likesNode = otherTextViews.find(tv => tv.text && likesRegex.test(tv.text.trim()));
const likes = likesNode?.text?.trim() || "未知";
// 3. 寻找作者:不是点赞数,也不是日期
const dateRegex = /^\d{2,4}-\d{2}(-\d{2})?$/;
const authorNode = otherTextViews.find(tv =>
tv.text &&
tv !== likesNode &&
!dateRegex.test(tv.text.trim())
);
const author = authorNode?.text?.trim() || "未知作者";
if (title === author) {
return null;
}
return {
index,
title,
author,
likes,
element: node,
};
}
async function getSearchResults(desiredCount: number = 15): Promise<PostInfo[]> {
const results: PostInfo[] = [];
const seenTitles = new Set<string>();
const maxScrolls = 5;
let lastResultCount = -1;
let scrollCount = 0;
for (let i = 0; i < maxScrolls && results.length < desiredCount; i++) {
const page = await UINode.getCurrentPage();
const resultsContainer = page.findByClass("RecyclerView");
if (!resultsContainer) {
console.log("未找到搜索结果容器 (RecyclerView)");
if (i === 0) return [];
else break;
}
for (const item of resultsContainer.children) {
const postInfo = extractPostInfoFromNode(item, results.length + 1);
if (postInfo && !seenTitles.has(postInfo.title)) {
seenTitles.add(postInfo.title);
results.push(postInfo);
if (results.length >= desiredCount) break;
}
}
if (results.length >= desiredCount) break;
if (results.length === lastResultCount) {
console.log("滚动未产生新结果,停止滚动");
break;
}
lastResultCount = results.length;
await Tools.UI.swipe(540, 1500, 540, 800);
await Tools.System.sleep(2000);
scrollCount++;
}
console.log(`Scrolling back up ${scrollCount} times.`);
for (let i = 0; i < scrollCount; i++) {
await Tools.UI.swipe(540, 800, 540, 1550);
await Tools.System.sleep(1000);
}
return results;
}
async function view_post(params: { post_title?: string; post_index?: number }): Promise<ToolResponse> {
const { post_title, post_index } = params;
if (!post_title && !post_index) {
return createResponse(false, "请提供帖子标题或索引");
}
console.log(`尝试查看帖子 - 标题: "${post_title}", 索引: ${post_index}`);
let targetPost: { title: string; item: UINode } | null = null;
let currentIndex = 0;
const seenTitles = new Set<string>();
const maxScrolls = 5;
for (let i = 0; i < maxScrolls; i++) {
const page = await UINode.getCurrentPage();
const resultsContainer = page.findByClass("RecyclerView");
if (!resultsContainer) {
if (i === 0) return createResponse(false, "未找到帖子列表容器 (RecyclerView)");
else break;
}
for (const item of resultsContainer.children) {
const postInfo = extractPostInfoFromNode(item, 0); // index doesn't matter here
if (postInfo && !seenTitles.has(postInfo.title)) {
seenTitles.add(postInfo.title);
currentIndex++;
let shouldClick = false;
if (post_index && currentIndex === post_index) {
shouldClick = true;
} else if (post_title && postInfo.title.includes(post_title)) {
shouldClick = true;
}
if (shouldClick) {
targetPost = { title: postInfo.title, item: postInfo.element };
break;
}
}
}
if (targetPost) break;
await Tools.UI.swipe(540, 1500, 540, 800);
await Tools.System.sleep(2000);
}
if (targetPost) {
await targetPost.item.click();
await Tools.System.sleep(3000); // 等待帖子详情页加载
let res = await get_post_info({});
return createResponse(true, `成功打开帖子并完成内容加载: "${targetPost.title}"`, res);
} else {
return createResponse(false, "未找到指定的帖子", { post_title, post_index });
}
}
async function isInPostDetail(): Promise<boolean> {
const page = await UINode.getCurrentPage();
// 检测是否在帖子详情页的特征元素
const likeButton = page.findByContentDesc("点赞") ||
page.findByText("赞") ||
page.find(n => n.contentDesc && n.contentDesc.includes("点赞"));
const commentButton = page.findByContentDesc("评论") ||
page.findByText("评论") ||
page.find(n => n.contentDesc && n.contentDesc.includes("评论"));
return !!(likeButton || commentButton);
}
async function findLikeButton(): Promise<UINode | null> {
const page = await UINode.getCurrentPage();
const button = page.findByContentDesc("点赞") ||
page.findByText("赞") ||
page.find(n => n.contentDesc && n.contentDesc.includes("点赞"));
return button || null;
}
async function findCollectButton(): Promise<UINode | null> {
const page = await UINode.getCurrentPage();
const button = page.findByContentDesc("收藏") ||
page.findByText("收藏") ||
page.find(n => n.contentDesc && n.contentDesc.includes("收藏"));
return button || null;
}
async function findCommentButton(): Promise<UINode | null> {
const page = await UINode.getCurrentPage();
const button = page.findByContentDesc("评论") ||
page.findByText("评论") ||
page.find(n => n.contentDesc && n.contentDesc.includes("评论"));
return button || null;
}
async function like_post(params: {}): Promise<ToolResponse> {
if (!await isInPostDetail()) {
return createResponse(false, "当前不在帖子详情页");
}
console.log("给帖子点赞");
const likeButton = await findLikeButton();
if (likeButton) {
await likeButton.click();
await Tools.System.sleep(1000);
return createResponse(true, "点赞成功");
} else {
return createResponse(false, "未找到点赞按钮");
}
}
async function collect_post(params: { collection_name?: string }): Promise<ToolResponse> {
if (!await isInPostDetail()) {
return createResponse(false, "当前不在帖子详情页");
}
const { collection_name } = params;
console.log(`收藏帖子到: ${collection_name || "默认收藏夹"}`);
const collectButton = await findCollectButton();
if (collectButton) {
await collectButton.click();
await Tools.System.sleep(2000);
// 如果指定了收藏夹名称,尝试选择
if (collection_name) {
const collectionPage = await UINode.getCurrentPage();
const targetCollection = collectionPage.findByText(collection_name);
if (targetCollection) {
await targetCollection.click();
await Tools.System.sleep(1000);
}
}
return createResponse(true, "收藏成功", { collection_name });
} else {
return createResponse(false, "未找到收藏按钮", { collection_name });
}
}
async function follow_user(params: {}): Promise<ToolResponse> {
if (!await isInPostDetail()) {
return createResponse(false, "当前不在帖子详情页");
}
console.log("关注用户");
const page = await UINode.getCurrentPage();
const followButton = page.findByText("关注");
if (followButton) {
await followButton.click();
await Tools.System.sleep(1000);
return createResponse(true, "关注成功");
} else {
return createResponse(false, "未找到关注按钮或已经关注");
}
}
async function unfollow_user(params: {}): Promise<ToolResponse> {
if (!await isInPostDetail()) {
return createResponse(false, "当前不在帖子详情页");
}
console.log("不关注用户");
const page = await UINode.getCurrentPage();
const followButton = page.findByText("已关注");
if (followButton) {
await followButton.click();
await Tools.System.sleep(1000);
(await UINode.getCurrentPage()).findByText("不再关注")?.click();
return createResponse(true, "取消关注成功");
} else {
return createResponse(false, "未找到不关注按钮或已经不关注");
}
}
async function comment_post(params: { comment_text: string }): Promise<ToolResponse> {
if (!await isInPostDetail()) {
return createResponse(false, "当前不在帖子详情页");
}
const { comment_text } = params;
console.log(`发表评论: ${comment_text}`);
const page = await UINode.getCurrentPage();
const commentBox = page.findByContentDesc("评论框")
if (commentBox) {
await commentBox.click();
await Tools.System.sleep(1000);
// 输入评论内容
await Tools.UI.setText(comment_text);
await Tools.System.sleep(500);
// 查找发送按钮
const sendButton = (await UINode.getCurrentPage()).findByText("发送")
if (sendButton) {
await sendButton.click();
await Tools.System.sleep(2000);
return createResponse(true, "评论发表成功", { comment_text });
} else {
return createResponse(false, "未找到发送按钮", { comment_text });
}
} else {
return createResponse(false, "未找到评论输入框", { comment_text });
}
}
async function get_post_info(params: GetPostInfoParams): Promise<ToolResponse> {
if (!await isInPostDetail()) {
return createResponse(false, "当前不在帖子详情页");
}
console.log("获取帖子信息...");
// Initial scroll to load content below the media
await Tools.UI.swipe(540, 1600, 540, 1000);
await Tools.System.sleep(1500);
const page = await UINode.getCurrentPage();
const postInfo: {
title: string;
author: string;
content: string;
like_count: string;
comment_count: string;
collect_count: string;
publish_date: string;
comments: Comment[];
} = {
title: "未知",
author: "未知",
content: "",
like_count: "未知",
comment_count: "未知",
collect_count: "未知",
publish_date: "未知",
comments: [],
};
// 1. Parse static information first
const authorButton = page.find(n => n.contentDesc && n.contentDesc.startsWith("作者,"));
let authorTextView: UINode | undefined;
if (authorButton) {
authorTextView = authorButton.findByClass("TextView") ?? undefined;
if (authorTextView && authorTextView.text) {
postInfo.author = authorTextView.text;
} else if (authorButton.contentDesc) {
const parts = authorButton.contentDesc.split(',');
if (parts.length > 1) {
postInfo.author = parts[1].trim();
}
}
}
const allNodesWithDesc = page.findAll(n => !!n.contentDesc);
const dateNode = allNodesWithDesc.find(n => n.contentDesc && /^\d{2}-\d{2}$/.test(n.contentDesc.trim()));
if (dateNode && dateNode.contentDesc) {
postInfo.publish_date = dateNode.contentDesc.trim();
}
const findCountFromButton = (button: UINode | null): string => {
if (!button) return "未知";
if (button.contentDesc) {
const match = button.contentDesc.match(/(\d+(\.\d+)?[万wW]?)/);
if (match && match[1]) return match[1];
}
const countNode = button.findByClass("TextView");
if (countNode && countNode.text && /^\d+(\.\d+)?[万wW]?$/.test(countNode.text.trim())) {
return countNode.text.trim();
}
return "未知";
};
postInfo.like_count = findCountFromButton(await findLikeButton());
postInfo.comment_count = findCountFromButton(await findCommentButton());
postInfo.collect_count = findCountFromButton(await findCollectButton());
// 2. Identify comment section to isolate title/content
const allRecyclerViews = page.findAllByClass("RecyclerView");
const commentListInitial = allRecyclerViews.find(rv => {
const parent = rv.parent;
return !(parent && parent.className === 'FrameLayout' && parent.contentDesc && (parent.contentDesc.includes("图片") || parent.contentDesc.includes("视频")));
});
// 3. Extract title and content
const commentTextViews = new Set(commentListInitial ? commentListInitial.findAllByClass("TextView") : []);
const allTextViews = page.findAllByClass("TextView");
const mainTextViews = allTextViews.filter(tv => {
return tv && !commentTextViews.has(tv);
});
const ignoredTexts = new Set(['关注', '赞', '评论', '收藏', '翻译', '说点什么...', postInfo.author]);
if (postInfo.author === '未知') {
ignoredTexts.delete('未知');
}
const contentTextNodes = mainTextViews.filter(tv => {
if (!tv.text) return false;
const text = tv.text.trim();
return text.length > 0 && !ignoredTexts.has(text) && !/^\d+(\.\d+)?[万wW]?$/.test(text) && tv !== authorTextView;
});
if (contentTextNodes.length > 0) {
postInfo.title = contentTextNodes[0].text!.trim();
if (contentTextNodes.length > 1) {
postInfo.content = contentTextNodes.slice(1).map(tv => tv.text!.trim()).join('\n');
}
}
// 4. Scroll and parse comments iteratively
console.log("开始滚动并解析评论...");
const seenComments = new Set<string>();
let stableScrolls = 0;
const maxScrolls = 10;
for (let i = 0; i < maxScrolls; i++) {
const currentCommentCount = postInfo.comments.length;
const currentPage = await UINode.getCurrentPage();
const commentList = currentPage.findAllByClass("RecyclerView").find(rv => {
const parent = rv.parent;
return !(parent && parent.className === 'FrameLayout' && parent.contentDesc && (parent.contentDesc.includes("图片") || parent.contentDesc.includes("视频")));
});
if (commentList) {
for (const item of commentList.children) {
if (postInfo.comments.length >= 20) break;
const textViews = item.findAllByClass("TextView");
if (textViews.length < 2) continue;
const comment: Comment = { author: "未知", content: "未知", likes: "0", date: "未知" };
const likesRegex = /^\d+(\.\d+)?[万wW]?$/;
const likesNode = item.find(n => n.className === 'TextView' && n.text && likesRegex.test(n.text));
comment.likes = likesNode?.text ?? "0";
const contentCandidates = textViews.filter(tv => tv.text && tv !== likesNode);
if (contentCandidates.length === 0) continue;
contentCandidates.sort((a, b) => (b.text?.length ?? 0) - (a.text?.length ?? 0));
let contentText = contentCandidates[0].text ?? "";
const authorNode = contentCandidates.length > 1 ? contentCandidates[1] : undefined;
comment.author = authorNode?.text?.replace("作者", "").trim() || "未知";
const dateRegex = /(\d{2}-\d{2}( \d{2}:\d{2})?|\d+天前|昨天 \d{2}:\d{2})/;
let dateMatch = contentText.match(dateRegex);
if (dateMatch) {
comment.date = dateMatch[0];
comment.content = contentText.replace(dateRegex, ' ').replace(/回复$/, '').trim();
} else {
comment.content = contentText.replace(/回复$/, '').trim();
}
if (comment.content.startsWith(comment.author)) {
comment.content = comment.content.substring(comment.author.length).trim();
}
if (comment.content === comment.author || !comment.content) continue;
const uniqueKey = `${comment.author}:${comment.content}`;
if (comment.content && comment.content !== "未知" && !seenComments.has(uniqueKey)) {
seenComments.add(uniqueKey);
postInfo.comments.push(comment);
}
}
}
if (postInfo.comments.length >= 20) {
console.log("已收集到20条评论,停止滚动。");
break;
}
if (postInfo.comments.length === currentCommentCount) {
stableScrolls++;
if (stableScrolls >= 2) {
console.log("滚动两次未发现新评论,停止。");
break;
}
} else {
stableScrolls = 0;
}
console.log(`第 ${i + 1}/${maxScrolls} 次滚动... 当前评论数: ${postInfo.comments.length}`);
await Tools.UI.swipe(540, 1600, 540, 800);
await Tools.System.sleep(2000);
}
return createResponse(true, "获取帖子信息成功", { post_info: postInfo });
}
async function publish_post(params: { content_text: string; image_paths?: string; tags?: string; location?: string }): Promise<ToolResponse> {
const { content_text, image_paths, tags, location } = params;
console.log(`发布帖子: ${content_text.substring(0, 20)}...`);
// 确保小红书已在前台运行
if (!await ensureMain()) {
return createResponse(false, "无法启动或切换到小红书");
}
// 点击发布按钮(通常在底部导航栏中间)
const page = await UINode.getCurrentPage();
const publishButton = page.findByText("发布") ||
page.findByText("+") ||
page.findByContentDesc("发布");
if (!publishButton) {
return createResponse(false, "未找到发布按钮");
}
await publishButton.click();
await Tools.System.sleep(3000);
// 选择发布类型(如果需要)
const noteOption = (await UINode.getCurrentPage()).findByText("笔记") ||
(await UINode.getCurrentPage()).findByText("图文");
if (noteOption) {
await noteOption.click();
await Tools.System.sleep(2000);
}
// 如果提供了图片路径,添加图片
if (image_paths) {
const imagePaths = image_paths.split(',');
for (const imagePath of imagePaths) {
// 这里需要根据实际的图片选择界面来实现
console.log(`添加图片: ${imagePath.trim()}`);
// 实际实现中需要打开相册、选择图片等步骤
}
}
// 输入文字内容
const pageForInput = await UINode.getCurrentPage();
const textInput = pageForInput.findByClass("EditText") ||
pageForInput.findByText("分享你的生活...");
if (textInput) {
await textInput.click();
await Tools.System.sleep(1000);
await Tools.UI.setText(content_text);
await Tools.System.sleep(1000);
}
// 添加标签
if (tags) {
const tagList = tags.split(',');
for (const tag of tagList) {
const tagText = tag.trim();
if (tagText) {
// 在文本末尾添加话题标签
await Tools.UI.setText(` #${tagText}`);
await Tools.System.sleep(500);
}
}
}
// 添加位置信息
if (location) {
const locationButton = (await UINode.getCurrentPage()).findByText("添加地点");
if (locationButton) {
await locationButton.click();
await Tools.System.sleep(2000);
// 搜索并选择位置
await Tools.UI.setText(location);
await Tools.System.sleep(1000);
const firstLocation = (await UINode.getCurrentPage()).findByClass("TextView");
if (firstLocation && firstLocation.text && firstLocation.text.includes(location)) {
await firstLocation.click();
await Tools.System.sleep(1000);
}
}
}
// 发布帖子
const finalPublishButton = (await UINode.getCurrentPage()).findByText("发布");
if (finalPublishButton) {
await finalPublishButton.click();
await Tools.System.sleep(5000); // 等待发布完成
return createResponse(true, "帖子发布成功", {
content_text: content_text,
image_paths: image_paths,
tags: tags,
location: location
});
} else {
return createResponse(false, "未找到最终发布按钮", {
content_text: content_text
});
}
}
async function navigate_to_home(params: {}): Promise<ToolResponse> {
console.log("导航到首页");
if (await ensureMain()) {
const page = await UINode.getCurrentPage();
const homeTab = page.findByText("首页");
if (homeTab) {
await homeTab.click();
await Tools.System.sleep(1000);
}
return createResponse(true, "已导航到首页");
} else {
return createResponse(false, "无法导航到首页");
}
}
async function navigate_to_profile(params: {}): Promise<ToolResponse> {
console.log("导航到个人主页");
if (await ensureMain()) {
const page = await UINode.getCurrentPage();
const profileTab = page.findByText("我") ||
page.findByText("个人主页");
if (profileTab) {
await profileTab.click();
await Tools.System.sleep(2000);
return createResponse(true, "已导航到个人主页");
} else {
return createResponse(false, "未找到个人主页标签");
}
} else {
return createResponse(false, "无法导航到个人主页");
}
}
async function navigate_to_search(params: {}): Promise<ToolResponse> {
console.log("导航到搜索页面");
if (await ensureMain()) {
const page = await UINode.getCurrentPage();
const searchTab = page.findByText("搜索") ||
page.findByContentDesc("搜索");
if (searchTab) {
await searchTab.click();
await Tools.System.sleep(1500);
return createResponse(true, "已导航到搜索页面");
} else {
// 尝试点击顶部搜索区域
await Tools.UI.tap(540, 200);
await Tools.System.sleep(1500);
return createResponse(true, "已打开搜索页面");
}
} else {
return createResponse(false, "无法导航到搜索页面");
}
}
async function navigate_to_publish(params: {}): Promise<ToolResponse> {
console.log("导航到发布页面");
if (await ensureMain()) {
const page = await UINode.getCurrentPage();
const publishTab = page.findByText("发布") ||
page.findByText("+");
if (publishTab) {
await publishTab.click();
await Tools.System.sleep(2000);
return createResponse(true, "已导航到发布页面");
} else {
return createResponse(false, "未找到发布按钮");
}
} else {
return createResponse(false, "无法导航到发布页面");
}
}
async function back_to_feed(params: {}): Promise<ToolResponse> {
console.log("从帖子详情页返回到信息流");
if (await isInPostDetail()) {
await Tools.UI.pressKey("KEYCODE_BACK");
await Tools.System.sleep(2000);
return createResponse(true, "已返回到信息流列表");
} else {
return createResponse(true, "当前不在帖子详情页,无需返回");
}
}
async function wrapToolExecution<P>(func: (params: P) => Promise<ToolResponse | void>, params: P) {
try {
const result = await func(params);
if (result) {
complete(result);
} else {
complete({ success: true, message: "操作成功完成但无返回数据。" });
}
} catch (error) {
console.error(`Tool ${func.name} failed unexpectedly`, error);
complete({
success: false,
message: `工具执行时发生意外错误: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
async function main(params: {}): Promise<void> {
console.log("=== 小红书智能助手测试 ===");
console.log("测试搜索功能...");
// let res = await search_content({ keyword: "美食" });
// console.log(JSON.stringify(res, null, 2));
// if (res.success && (res.result_count as number) > 0) {
// console.log("测试查看帖子功能...");
// let view_res = await view_post({ post_index: 10 });
// console.log(JSON.stringify(view_res, null, 2));
// } else {
// console.log("搜索无结果,跳过查看帖子测试。");
// }
console.log(JSON.stringify(await get_post_info({}), null, 2));
}
// Tool parameter interfaces
interface BrowseHomeFeedParams { scroll_count?: number; collect_posts?: boolean; }
interface SearchContentParams { keyword: string; search_type?: string; }
interface ViewPostParams { post_title?: string; post_index?: number; }
interface LikePostParams { }
interface CollectPostParams { collection_name?: string; }
interface FollowUserParams { }
interface UnfollowUserParams { }
interface CommentPostParams { comment_text: string; }
interface GetPostInfoParams { }
interface PublishPostParams { content_text: string; image_paths?: string; tags?: string; location?: string; }
interface NavigateToHomeParams { }
interface NavigateToProfileParams { }
interface NavigateToSearchParams { }
interface NavigateToPublishParams { }
interface BackToFeedParams { }
interface MainParams { }
// 导出所有工具
return {
browse_home_feed: async (params: BrowseHomeFeedParams) => await wrapToolExecution(browse_home_feed, params),
search_content: async (params: SearchContentParams) => await wrapToolExecution(search_content, params),
view_post: async (params: ViewPostParams) => await wrapToolExecution(view_post, params),
like_post: async (params: LikePostParams) => await wrapToolExecution(like_post, params),
collect_post: async (params: CollectPostParams) => await wrapToolExecution(collect_post, params),
follow_user: async (params: FollowUserParams) => await wrapToolExecution(follow_user, params),
unfollow_user: async (params: UnfollowUserParams) => await wrapToolExecution(unfollow_user, params),
comment_post: async (params: CommentPostParams) => await wrapToolExecution(comment_post, params),
get_post_info: async (params: GetPostInfoParams) => await wrapToolExecution(get_post_info, params),
publish_post: async (params: PublishPostParams) => await wrapToolExecution(publish_post, params),
navigate_to_home: async (params: NavigateToHomeParams) => await wrapToolExecution(navigate_to_home, params),
navigate_to_profile: async (params: NavigateToProfileParams) => await wrapToolExecution(navigate_to_profile, params),
navigate_to_search: async (params: NavigateToSearchParams) => await wrapToolExecution(navigate_to_search, params),
navigate_to_publish: async (params: NavigateToPublishParams) => await wrapToolExecution(navigate_to_publish, params),
back_to_feed: async (params: BackToFeedParams) => await wrapToolExecution(back_to_feed, params),
main: async (params: MainParams) => await wrapToolExecution(main, params),
};
})();
// 逐个导出,以便在外部调用
exports.browse_home_feed = XiaohongshuAssistant.browse_home_feed;
exports.search_content = XiaohongshuAssistant.search_content;
exports.view_post = XiaohongshuAssistant.view_post;
exports.like_post = XiaohongshuAssistant.like_post;
exports.collect_post = XiaohongshuAssistant.collect_post;
exports.follow_user = XiaohongshuAssistant.follow_user;
exports.unfollow_user = XiaohongshuAssistant.unfollow_user;
exports.comment_post = XiaohongshuAssistant.comment_post;
exports.get_post_info = XiaohongshuAssistant.get_post_info;
exports.publish_post = XiaohongshuAssistant.publish_post;
exports.navigate_to_home = XiaohongshuAssistant.navigate_to_home;
exports.navigate_to_profile = XiaohongshuAssistant.navigate_to_profile;
exports.navigate_to_search = XiaohongshuAssistant.navigate_to_search;
exports.navigate_to_publish = XiaohongshuAssistant.navigate_to_publish;
exports.back_to_feed = XiaohongshuAssistant.back_to_feed;
exports.main = XiaohongshuAssistant.main;