feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

View File

@@ -0,0 +1,381 @@
"""
Soul 控制技能 - Agent 端实现
功能模块:
1. 瞬间管理 - 发布瞬间(文字/图片/视频)
2. 私聊管理 - 发送消息、获取消息列表
3. 好友/关注 - 获取联系人列表
安卓包名cn.soulapp.android
UI 文案与 resourceId 需根据真机 Soul 版本微调。
@author 卡若
@version 3.0.0
"""
import time
import logging
from typing import Dict, Any, List, Optional
import sys
import os
_agent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if _agent_dir not in sys.path:
sys.path.insert(0, _agent_dir)
from skills.base import BaseSkill
logger = logging.getLogger(__name__)
class SoulSkill(BaseSkill):
"""
Soul 控制技能
支持的操作:
- post_moments: 发布瞬间(文字/图片/视频)
- comment_moments: 评论某条瞬间(如回复 123
- send_message: 发送私聊消息
- get_messages: 获取私聊消息列表
- get_contacts: 获取好友/关注列表
"""
PACKAGE = "cn.soulapp.android"
NAME = "Soul"
# Soul 常见 UI 文案(随版本可能变化,真机可调)
TEXT_PUBLISH = "发布"
TEXT_MOMENT = "瞬间"
TEXT_MESSAGE = "消息"
TEXT_ME = "我的"
TEXT_SEND = "发送"
TEXT_SEARCH = "搜索"
TEXT_VIDEO = "视频"
TEXT_PHOTO = "图片"
TEXT_TEXT = "文字"
# ========== 发瞬间 ==========
def post_moments(
self,
content: str,
images: Optional[List[str]] = None,
video_url: Optional[str] = None,
location: Optional[str] = None,
visible_list: Optional[List[str]] = None,
invisible_list: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
发布 Soul 瞬间(文字/图片/视频)
Args:
content: 文案
images: 图片 URL 或本地路径列表(可选)
video_url: 视频 URL 或本地路径(可选)
location: 位置(可选)
visible_list / invisible_list: 可见/不可见名单可选Soul 若支持)
Returns:
执行结果
"""
try:
self.launch()
self.wait_for_app_ready(timeout=8)
self.sleep(1.5)
# 点击发布入口:常见为底部「+」或「发布」
published = False
for hint in ["+", "发布", "发瞬间", "写瞬间", "加号"]:
if self.click_text(hint) or self.click_contains(hint):
self.sleep(1)
published = True
break
if not published:
# 尝试点击屏幕中央偏下(常见发布按钮位置)
info = self.d.info
self.d.click(info["displayWidth"] // 2, info["displayHeight"] * 4 // 5)
self.sleep(1)
# 选择类型:优先视频 > 图片 > 文字
if video_url:
if self.click_text(self.TEXT_VIDEO) or self.click_contains("视频"):
self.sleep(1)
# TODO: 将 video_url 推到手机并选择文件
logger.warning("Soul post_moments: 视频选择需真机适配路径")
if images:
if self.click_text(self.TEXT_PHOTO) or self.click_contains("图片") or self.click_contains("相册"):
self.sleep(1)
# TODO: 将 images 推到手机并多选
logger.warning("Soul post_moments: 多图选择需真机适配路径")
# 输入文案
if content:
self.sleep(0.5)
self.input_text(content, clear=True)
self.sleep(0.3)
# 点击发布/发送
for send_btn in ["发布", "发送", "发瞬间", "完成", "下一步"]:
if self.click_text(send_btn) or self.click_contains(send_btn):
self.sleep(1.5)
break
logger.info("Soul 瞬间发布流程已执行")
return {
"success": True,
"message_id": f"soul_m_{int(time.time() * 1000)}",
"message": "发布流程已执行,请以真机实际界面为准核对",
}
except Exception as e:
logger.error(f"Soul 发布瞬间失败: {e}")
return {"success": False, "error": str(e)}
# ========== 评论瞬间 ==========
def comment_moments(
self,
user_id: str = "",
comment: str = "",
post_index: int = 0,
reply_to: Optional[str] = None,
) -> Dict[str, Any]:
"""
在 Soul 广场找一条瞬间并评论(如回复 123
Args:
user_id: 可选发瞬间的用户标识Soul 上可不传,按 post_index 选条数)
comment: 评论内容,如 "123"
post_index: 第几条瞬间0 表示第一条
reply_to: 可选,回复某人
Returns:
执行结果
"""
try:
self.launch()
self.wait_for_app_ready(timeout=8)
self.sleep(1.5)
# 进入广场/发现(首页往往是广场流)
for tab in ["广场", "发现", "首页", "推荐"]:
if self.click_text(tab) or self.click_contains(tab):
self.sleep(2)
break
else:
# 默认已在首页,稍等加载
self.sleep(2)
# 点第 post_index 条瞬间0=第一条):通过多次下滑再点,或直接点第一条
for _ in range(post_index):
self.swipe_up(scale=0.4)
self.sleep(0.8)
self.sleep(0.5)
# 点击第一条瞬间卡片进入详情(常见:点击卡片区域或「评论」入口)
info = self.d.info
self.d.click(info["displayWidth"] // 2, info["displayHeight"] // 3)
self.sleep(1.5)
# 点评论按钮
commented = False
for btn in ["评论", "写评论", "说点什么", "留言"]:
if self.click_text(btn) or self.click_contains(btn):
self.sleep(1)
commented = True
break
if not commented:
# 尝试点击屏幕下方评论图标区域
self.d.click(info["displayWidth"] // 2, info["displayHeight"] * 4 // 5)
self.sleep(1)
# 输入评论内容
if comment:
self.input_text(comment, clear=True)
self.sleep(0.3)
# 发送
for send_btn in ["发送", "发布", "评论", "完成"]:
if self.click_text(send_btn) or self.click_contains(send_btn):
self.sleep(1)
break
logger.info(f"Soul 评论瞬间已执行: {comment}")
return {
"success": True,
"message": f"已对第 {post_index + 1} 条瞬间评论: {comment}",
}
except Exception as e:
logger.error(f"Soul 评论瞬间失败: {e}")
return {"success": False, "error": str(e)}
# ========== 私聊 ==========
def send_message(
self,
to_id: str = None,
content: str = "",
msg_type: str = "text",
media_url: Optional[str] = None,
to: str = None,
**kwargs,
) -> Dict[str, Any]:
"""
发送 Soul 私聊消息
Args:
to_id: 对方昵称或 Soul ID
to: to_id 的别名AI Brain 兼容)
content: 消息内容
msg_type: text / image / video
media_url: 图片/视频 URL可选
Returns:
执行结果
"""
if to and not to_id:
to_id = to
if not to_id:
return {"success": False, "error": "缺少 to_id 参数"}
try:
self.launch()
self.wait_for_app_ready(timeout=8)
self.sleep(1)
# 进入消息
if not (self.click_text(self.TEXT_MESSAGE) or self.click_contains("消息")):
self.sleep(0.5)
self.click_text(self.TEXT_MESSAGE) or self.click_contains("消息")
self.sleep(1)
# 搜索或找到对方
if self.click_text(self.TEXT_SEARCH) or self.click_desc(self.TEXT_SEARCH):
self.sleep(0.5)
self.input_text(to_id, clear=True)
self.sleep(1.5)
if self.click_contains(to_id) or self.click_text(to_id):
self.sleep(1)
else:
return {"success": False, "error": f"未找到用户: {to_id}"}
# 输入并发送
self.input_text(content, clear=False)
self.sleep(0.3)
if self.click_text(self.TEXT_SEND) or self.click_contains("发送"):
self.sleep(0.5)
logger.info(f"Soul 私聊已发送: {to_id}")
return {
"success": True,
"message_id": f"soul_{int(time.time() * 1000)}",
}
except Exception as e:
logger.error(f"Soul 发送消息失败: {e}")
return {"success": False, "error": str(e)}
def get_messages(
self,
limit: int = 20,
conversation_id: Optional[str] = None,
count: int = None,
**kwargs,
) -> Dict[str, Any]:
"""
获取 Soul 私聊消息列表(当前会话或列表)
Args:
limit: 条数
count: limit 的别名AI Brain 兼容)
conversation_id: 会话对象(对方昵称或 ID
Returns:
消息列表
"""
if count is not None and limit == 20:
limit = count
try:
self.launch()
self.wait_for_app_ready(timeout=8)
self.sleep(1)
self.click_text(self.TEXT_MESSAGE) or self.click_contains("消息")
self.sleep(1)
if conversation_id:
if self.click_text(self.TEXT_SEARCH) or self.click_desc(self.TEXT_SEARCH):
self.sleep(0.5)
self.input_text(conversation_id, clear=True)
self.sleep(1.5)
self.click_contains(conversation_id) or self.click_text(conversation_id)
self.sleep(1)
messages = []
ui_tree = self.d.dump_hierarchy()
import xml.etree.ElementTree as ET
try:
root = ET.fromstring(ui_tree)
for elem in root.iter():
text = elem.get("text", "")
if text and "发送" not in text and "输入" not in text:
messages.append({"text": text, "timestamp": int(time.time() * 1000)})
except Exception:
pass
return {
"success": True,
"messages": messages[:limit],
"count": len(messages[:limit]),
}
except Exception as e:
logger.error(f"Soul 获取消息失败: {e}")
return {"success": False, "error": str(e), "messages": []}
# ========== 好友/联系人 ==========
def get_contacts(self, limit: int = 200) -> Dict[str, Any]:
"""
获取 Soul 好友/关注列表(从消息列表或「我的」-「关注」解析)
Args:
limit: 数量上限
Returns:
联系人列表
"""
try:
self.launch()
self.wait_for_app_ready(timeout=8)
self.sleep(1)
# 从消息页获取会话列表作为「联系人」近似
self.click_text(self.TEXT_MESSAGE) or self.click_contains("消息")
self.sleep(1.5)
contacts = []
ui_tree = self.d.dump_hierarchy()
import xml.etree.ElementTree as ET
try:
root = ET.fromstring(ui_tree)
for elem in root.iter():
text = elem.get("text", "")
if text and len(text) > 0:
if "消息" not in text and "搜索" not in text and "发送" not in text:
contacts.append({"name": text, "user_id": text})
except Exception:
pass
# 去重并限制数量
seen = set()
unique = []
for c in contacts:
k = c.get("name") or c.get("user_id", "")
if k and k not in seen:
seen.add(k)
unique.append(c)
if len(unique) >= limit:
break
return {
"success": True,
"contacts": unique,
"count": len(unique),
}
except Exception as e:
logger.error(f"Soul 获取联系人失败: {e}")
return {"success": False, "error": str(e), "contacts": []}