修改注释及代码重构

This commit is contained in:
zyj
2026-03-10 13:41:18 +08:00
parent 8a7521faff
commit 1be2f4021f
15 changed files with 411 additions and 365 deletions

16
libs/doc.go Normal file
View File

@@ -0,0 +1,16 @@
// Package libs 提供了与 UIAutomator2 服务交互的工具库。
//
// 本包包含以下核心组件:
// - AdbHTTPConnection通过 ADB 隧道发送 HTTP 请求到设备端 UIAutomator2 服务
// - SelectorUI 元素选择器构造器,支持文本、类名、资源 ID 等多种查询条件
// - HTTPResponseHTTP 响应封装
//
// 使用示例:
//
// // 创建 UI 选择器
// selector := libs.MustNew(map[string]interface{}{
// "text": "登录",
// "className": "android.widget.Button",
// })
// jsonData, _ := selector.ToJSON()
package libs

View File

@@ -14,50 +14,61 @@ import (
"time"
)
// ---------- 用于适配的外部接口/类型 ----------
// ---------- 外部接口类型定义 ----------
// AdbDevice 定义了通过 ADB 隧道创建设备连接的接口
type AdbDevice interface {
// CreateConnection 建立到设备的 TCP 连接(通常通过 adb 隧道/port-forward 实现)
// network 常为 "tcp" 或 "tcp4"/"tcp6" port 为设备上服务监听端口
// CreateConnection 建立到设备的 TCP 连接
// network 常为 "tcp"port 为设备上服务监听端口
CreateConnection(network string, port int) (net.Conn, error)
}
// ---------- HTTPResponse 等价类型 ----------
// HTTPResponse 封装 HTTP 响应数据
type HTTPResponse struct {
Content []byte
Status int
Reason string
Content []byte // 响应体内容
Status int // HTTP 状态码
Reason string // 状态描述
}
// JSON 将响应体解析为指定的结构体
func (r *HTTPResponse) JSON(v interface{}) error {
return json.Unmarshal(r.Content, v)
}
// Text 返回响应体的文本内容
func (r *HTTPResponse) Text() string {
return string(r.Content)
}
// ---------- 自定义错误类型 ----------
var (
ErrHTTPTimeout = errors.New("http request timeout")
ErrHTTPFailed = errors.New("http request failed")
// ErrHTTPTimeout 表示 HTTP 请求超时
ErrHTTPTimeout = errors.New("HTTP 请求超时")
// ErrHTTPFailed 表示 HTTP 请求失败
ErrHTTPFailed = errors.New("HTTP 请求失败")
)
// ---------- AdbHTTPConnection 核心:使用 net.Conn 写请求并用 http.ReadResponse 解析 ----------
// ---------- AdbHTTPConnection:通过 ADB 隧道发送 HTTP 请求 ----------
// AdbHTTPConnection 基于 net.Conn 实现的 HTTP 连接
// 通过 ADB 端口转发直接与设备端 UIAutomator2 服务通信
type AdbHTTPConnection struct {
Conn net.Conn
}
// NewAdbHTTPConnection 创建一个新的 ADB HTTP 连接
// dev 为设备接口port 为设备端服务端口timeout 为连接超时
func NewAdbHTTPConnection(dev AdbDevice, port int, timeout time.Duration) (*AdbHTTPConnection, error) {
// 这里 network 使用 "tcp",调用方可依据实际实现调整
conn, err := dev.CreateConnection("tcp", port)
if err != nil {
return nil, fmt.Errorf("unable to connect to uiautomator2 server: %w", err)
return nil, fmt.Errorf("无法连接到 UIAutomator2 服务: %w", err)
}
// 设置默认 deadline调用方可在需要时调整 Conn.SetDeadline
_ = conn.SetDeadline(time.Now().Add(timeout))
return &AdbHTTPConnection{Conn: conn}, nil
}
// Close 关闭底层连接
func (c *AdbHTTPConnection) Close() error {
if c.Conn != nil {
return c.Conn.Close()
@@ -65,124 +76,125 @@ func (c *AdbHTTPConnection) Close() error {
return nil
}
// sendRequest 写入 HTTP 请求并返回 *http.Response
// sendRequest HTTP 请求写入连接并读取响应
// 通过原始 TCP 连接发送 HTTP 报文,避免依赖标准 http.Client
func (c *AdbHTTPConnection) sendRequest(req *http.Request, timeout time.Duration) (*http.Response, error) {
// 确保 deadline
// 设置读写截止时间
if timeout > 0 {
_ = c.Conn.SetDeadline(time.Now().Add(timeout))
} else {
_ = c.Conn.SetDeadline(time.Time{})
}
// 将 http.Request 序列化为原始 HTTP 报文并写到 conn
// 序列化 HTTP 请求为原始报文
var buf bytes.Buffer
// 行: METHOD PATH HTTP/1.1
// 请求行METHOD PATH HTTP/1.1
path := req.URL.RequestURI()
if path == "" {
path = "/"
}
fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", req.Method, path)
// Host 头Python 里使用 "localhost" 但最终是通过 adb 隧道Host 不重要,这里写 localhost
fmt.Fprintf(&buf, "Host: localhost\r\n")
// 写 headers
// 保证 Content-Length 或 Transfer-Encoding 存在
// 设置默认请求头
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "uiautomator2")
}
if req.Header.Get("Accept-Encoding") == "" {
// 与 Python 保持一致,明确禁用压缩
req.Header.Set("Accept-Encoding", "")
}
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
// copy headers into buffer
// 写入请求头
for k, vals := range req.Header {
for _, v := range vals {
fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
}
}
// body
// 处理请求体
var bodyBytes []byte
if req.Body != nil {
var err error
bodyBytes, err = io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("read request body failed: %w", err)
return nil, fmt.Errorf("读取请求体失败: %w", err)
}
// set Content-Length
fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(bodyBytes))
} else {
fmt.Fprintf(&buf, "Content-Length: 0\r\n")
}
// header-body separator
// 请求头与请求体之间的空行
buf.WriteString("\r\n")
// write header+body to conn
// 发送请求头
if _, err := c.Conn.Write(buf.Bytes()); err != nil {
return nil, fmt.Errorf("write request headers failed: %w", err)
return nil, fmt.Errorf("发送请求头失败: %w", err)
}
// 发送请求体
if len(bodyBytes) > 0 {
if _, err := c.Conn.Write(bodyBytes); err != nil {
return nil, fmt.Errorf("write request body failed: %w", err)
return nil, fmt.Errorf("发送请求体失败: %w", err)
}
}
// read response using http.ReadResponse
// 使用标准库解析 HTTP 响应
reader := bufio.NewReader(c.Conn)
// note: http.ReadResponse expects a Request to be passed for RequestURI related logic,
// but if nil it still parses status & headers fine. Provide req for completeness.
resp, err := http.ReadResponse(reader, req)
if err != nil {
return nil, fmt.Errorf("read http response failed: %w", err)
return nil, fmt.Errorf("读取 HTTP 响应失败: %w", err)
}
return resp, nil
}
// ---------- _http_request 的 Go 等价实现 ----------
// ---------- HttpRequest:高层 HTTP 请求封装 ----------
// HttpRequest 向设备端 UIAutomator2 服务发送 HTTP 请求
// ctx 为上下文控制dev 为设备接口devicePort 为设备端服务端口
// method 为 HTTP 方法path 为请求路径
// data 为请求体数据(会被 JSON 编码timeoutSecs 为超时秒数
// printRequest 为 true 时输出 curl 风格的调试信息
func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, path string, data map[string]interface{}, timeoutSecs float64, printRequest bool) (*HTTPResponse, error) {
// 兼容 python 默认 timeout
// 默认超时 10 秒
if timeoutSecs <= 0 {
timeoutSecs = 10.0
}
timeout := time.Duration(timeoutSecs * float64(time.Second))
// debug 打印 curl 样式
// 调试模式:打印 curl 风格的请求信息
if printRequest {
now := time.Now().Format("15:04:05.000")
url := fmt.Sprintf("http://127.0.0.1:%d%s", devicePort, path)
if data != nil {
b, _ := json.Marshal(data)
fmt.Printf("# http timeout=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b))
fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b))
} else {
fmt.Printf("# http timeout=%.3f\n%s $ curl -X %s %s\n", timeoutSecs, now, method, url)
fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s\n", timeoutSecs, now, method, url)
}
}
// 构造 http.Request
// 构造 HTTP 请求
var body io.Reader
if data != nil {
b, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("json marshal data failed: %w", err)
return nil, fmt.Errorf("JSON 编码失败: %w", err)
}
body = bytes.NewReader(b)
}
// URL 可以是任何合法的虚拟 URL因为我们直接写原始请求行到 conn
req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, body)
if err != nil {
return nil, fmt.Errorf("create http request failed: %w", err)
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
}
// 设置 headers 与 Python 保持一致
req.Header.Set("User-Agent", "uiautomator2")
req.Header.Set("Accept-Encoding", "")
req.Header.Set("Content-Type", "application/json")
// 建立到设备的连接AdbHTTPConnection.connect
// 建立到设备的连接
connWrapper, err := NewAdbHTTPConnection(dev, devicePort, timeout)
if err != nil {
return nil, err
@@ -192,7 +204,6 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat
// 发送请求并读取响应
resp, err := connWrapper.sendRequest(req, timeout)
if err != nil {
// 判断是否为超时net.Error with Timeout
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return nil, fmt.Errorf("%w: %v", ErrHTTPTimeout, err)
@@ -201,14 +212,14 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat
}
defer resp.Body.Close()
// 按块读取响应体(与 Python 的循环等效)
// 读取响应体
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
return nil, fmt.Errorf("读取响应体失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http request failed: %d %s", resp.StatusCode, resp.Status)
return nil, fmt.Errorf("HTTP 请求失败: %d %s", resp.StatusCode, resp.Status)
}
response := &HTTPResponse{
@@ -219,7 +230,7 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat
if printRequest {
now := time.Now().Format("15:04:05.000")
fmt.Printf("%s Response >>>\n%s\n<<< END timed_used = %.3f\n\n", now, strings.TrimRight(response.Text(), "\n"), time.Since(time.Now().Add(-timeout)).Seconds())
fmt.Printf("%s 响应 >>>\n%s\n<<< 结束\n\n", now, strings.TrimRight(response.Text(), "\n"))
}
return response, nil

View File

@@ -7,28 +7,30 @@ import (
"strconv"
)
// FieldMeta 保存每个字段对应的 mask 位和默认值nil 表示无默认值)
// FieldMeta 定义选择器字段的掩码位和默认值
// Bit 为该字段对应的掩码位Default 为默认值nil 表示无默认值)
type FieldMeta struct {
Bit uint32
Default interface{}
}
// Selector 表示一个 UiSelector 的构造器
// 用于构建 Android UI 元素的查询条件
type Selector struct {
// 存放字段及其值(只包含显式设置的字段
// 存放设置的字段及其值
fields map[string]interface{}
// mask 值(通过设置/删除字段自动维护)
// 掩码值(通过设置/删除字段自动维护)
mask uint32
// childOrSibling 顺序列表,元素为 "child" 或 "sibling"
// 子/兄弟关系列表,元素为 "child" 或 "sibling"
childOrSibling []string
// 对应的嵌套 Selector 列表,长度与 childOrSibling 相同
childOrSiblingSelector []*Selector
}
// 字段元数据(与 Python 版本一致)
// fieldDefs 定义所有支持的选择器字段及其元数据(与 Python 版 uiautomator2 一致)
var fieldDefs = map[string]FieldMeta{
"text": {Bit: 0x01, Default: nil},
"textContains": {Bit: 0x02, Default: nil},
@@ -57,7 +59,7 @@ var fieldDefs = map[string]FieldMeta{
"instance": {Bit: 0x01000000, Default: 0},
}
// New creates a Selector and可选传入初始字段
// New 创建一个新的 Selector可选传入初始字段
func New(initial map[string]interface{}) (*Selector, error) {
s := &Selector{
fields: make(map[string]interface{}),
@@ -73,7 +75,7 @@ func New(initial map[string]interface{}) (*Selector, error) {
return s, nil
}
// MustNew New 相同,但在错时 panic便于简洁示例
// MustNew New 相同,但在错时 panic适合简洁的初始化场景
func MustNew(initial map[string]interface{}) *Selector {
s, err := New(initial)
if err != nil {
@@ -82,42 +84,43 @@ func MustNew(initial map[string]interface{}) *Selector {
return s
}
// validateValue 对给定字段和值类型校验(布尔字段与整数字段)
// validateValue 对给定字段和值进行类型校验
// 布尔字段要求值为 bool 类型,整数字段要求值为整数类型
func validateValue(key string, val interface{}) error {
meta, ok := fieldDefs[key]
if !ok {
return fmt.Errorf("field %s is not allowed", key)
return fmt.Errorf("不支持的字段: %s", key)
}
if meta.Default == false {
// 期望 bool
// 布尔字段校验
_, ok := val.(bool)
if !ok {
return fmt.Errorf("%s must be bool", key)
return fmt.Errorf("%s 必须是 bool 类型", key)
}
return nil
}
// 整数字段Default 为 int 类型)要求 int
// 整数字段校验
switch d := meta.Default.(type) {
case int:
// 支持 int 和可被转为 int 的数值(如 int64
switch val.(type) {
case int, int8, int16, int32, int64:
return nil
case uint, uint8, uint16, uint32, uint64:
return nil
default:
return fmt.Errorf("%s must be integer type, default=%v", key, d)
return fmt.Errorf("%s 必须是整数类型, 默认值=%v", key, d)
}
default:
// 其字段没有特别要求
// 其字段无特殊类型要求
return nil
}
}
// Set 设置字段并更新 mask若字段非法或类型不对则返回错误
// Set 设置字段并更新掩码
// 如果字段名非法或类型不匹配则返回错误
func (s *Selector) Set(key string, val interface{}) error {
if _, ok := fieldDefs[key]; !ok {
return fmt.Errorf("%s is not allowed", key)
return fmt.Errorf("不支持的字段: %s", key)
}
if err := validateValue(key, val); err != nil {
return err
@@ -127,10 +130,11 @@ func (s *Selector) Set(key string, val interface{}) error {
return nil
}
// Delete 删除字段并更新 mask幂等删除不存在字段不报错
// Delete 删除字段并更新掩码
// 删除不存在的字段不会报错(幂等操作)
func (s *Selector) Delete(key string) error {
if _, ok := fieldDefs[key]; !ok {
return fmt.Errorf("%s is not allowed", key)
return fmt.Errorf("不支持的字段: %s", key)
}
if _, present := s.fields[key]; present {
delete(s.fields, key)
@@ -139,12 +143,12 @@ func (s *Selector) Delete(key string) error {
return nil
}
// Mask 返回当前 mask(只读)
// Mask 返回当前掩码值(只读)
func (s *Selector) Mask() uint32 {
return s.mask
}
// Child 在末尾添加 child
// Child 添加一个子元素选择器
func (s *Selector) Child(initial map[string]interface{}) (*Selector, error) {
child, err := New(initial)
if err != nil {
@@ -155,7 +159,7 @@ func (s *Selector) Child(initial map[string]interface{}) (*Selector, error) {
return s, nil
}
// Sibling 在末尾添加 sibling
// Sibling 添加一个兄弟元素选择器
func (s *Selector) Sibling(initial map[string]interface{}) (*Selector, error) {
child, err := New(initial)
if err != nil {
@@ -166,7 +170,8 @@ func (s *Selector) Sibling(initial map[string]interface{}) (*Selector, error) {
return s, nil
}
// UpdateInstance 更新最后一个 childOrSiblingSelector 的 instance 字段(或根 selector
// UpdateInstance 更新最后一个子/兄弟选择器的 instance 字段
// 如果没有子/兄弟选择器,则更新根选择器的 instance
func (s *Selector) UpdateInstance(i int) error {
n := len(s.childOrSiblingSelector)
if n > 0 {
@@ -175,7 +180,7 @@ func (s *Selector) UpdateInstance(i int) error {
return s.Set("instance", i)
}
// Clone 深拷贝 Selector包括子/兄弟
// Clone 深拷贝当前 Selector包括所有子/兄弟选择器
func (s *Selector) Clone() *Selector {
clone := &Selector{
fields: make(map[string]interface{}, len(s.fields)),
@@ -184,8 +189,6 @@ func (s *Selector) Clone() *Selector {
childOrSiblingSelector: make([]*Selector, 0, len(s.childOrSiblingSelector)),
}
for k, v := range s.fields {
// 简单深拷贝对于常见类型string,bool,int直接赋值即可。
// 若值为复杂结构,调用方应使用 ToMap/ToJSON 再 Parse 得到深拷贝。
clone.fields[k] = v
}
for _, c := range s.childOrSiblingSelector {
@@ -194,7 +197,7 @@ func (s *Selector) Clone() *Selector {
return clone
}
// ToMap 序列化为 map便于 RPC 调用或 JSON 编码
// ToMap 将选择器序列化为 map便于 JSON 编码或 RPC 调用
func (s *Selector) ToMap() map[string]interface{} {
out := make(map[string]interface{}, len(s.fields)+3)
for k, v := range s.fields {
@@ -212,32 +215,29 @@ func (s *Selector) ToMap() map[string]interface{} {
return out
}
// ToJSON 返回 ToMap 的 JSON 编码
// ToJSON 返回选择器的 JSON 编码
func (s *Selector) ToJSON() ([]byte, error) {
return json.Marshal(s.ToMap())
}
// FromMap 从 map 恢复 Selector(简单实现,忽略非法字段)
// FromMap 从 map 恢复 Selector 实例
// 自动解析已知字段、掩码和子/兄弟选择器
func FromMap(data map[string]interface{}) (*Selector, error) {
// 提取根字段
root := &Selector{
fields: make(map[string]interface{}),
childOrSibling: []string{},
childOrSiblingSelector: []*Selector{},
mask: 0,
}
// 读取已知字段
for k, meta := range fieldDefs {
// 恢复已知字段
for k := range fieldDefs {
if v, ok := data[k]; ok {
// 尝试 Set 以做类型校验并设置 mask
if err := root.Set(k, v); err != nil {
return nil, err
}
// 注意Set 已经更新了 mask
_ = meta
}
}
// 恢复 mask如果提供了 mask并且为数值
// 恢复掩码值
if m, ok := data["mask"]; ok {
switch mv := m.(type) {
case float64:
@@ -248,11 +248,9 @@ func FromMap(data map[string]interface{}) (*Selector, error) {
root.mask = uint32(mv)
case int64:
root.mask = uint32(mv)
default:
// 忽略不能解析的 mask
}
}
// 恢复 childOrSibling 列表和对应 selector期望 childOrSiblingSelector 为 []map[string]interface{}
// 恢复子/兄弟关系列表
if cs, ok := data["childOrSibling"]; ok {
if arr, ok := cs.([]interface{}); ok {
for _, e := range arr {
@@ -262,6 +260,7 @@ func FromMap(data map[string]interface{}) (*Selector, error) {
}
}
}
// 恢复子/兄弟选择器
if css, ok := data["childOrSiblingSelector"]; ok {
if arr, ok := css.([]interface{}); ok {
for _, item := range arr {
@@ -278,13 +277,13 @@ func FromMap(data map[string]interface{}) (*Selector, error) {
return root, nil
}
// UpdateAtPath 在指定路径child 索引链)上更新字段
// path: 逐级索引,例如 [0,2] 表示 childOrSiblingSelector[0].childOrSiblingSelector[2]
// UpdateAtPath 在指定路径上更新字段
// path 逐级索引,例如 [0,2] 表示 childOrSiblingSelector[0].childOrSiblingSelector[2]
func (s *Selector) UpdateAtPath(path []int, updates map[string]interface{}) error {
node := s
for _, idx := range path {
if idx < 0 || idx >= len(node.childOrSiblingSelector) {
return errors.New("path out of range")
return errors.New("路径索引越界")
}
node = node.childOrSiblingSelector[idx]
}
@@ -296,10 +295,9 @@ func (s *Selector) UpdateAtPath(path []int, updates map[string]interface{}) erro
return nil
}
// String 实现 fmt.Stringer输出友好可读的 Selector 表示(类似 Python 的 __str__
// String 实现 fmt.Stringer 接口,输出可读的选择器表示
func (s *Selector) String() string {
m := s.ToMap()
// 删除空的 childOrSibling 字段以保持简洁
if _, ok := m["childOrSibling"]; !ok {
delete(m, "childOrSibling")
delete(m, "childOrSiblingSelector")
@@ -308,46 +306,46 @@ func (s *Selector) String() string {
return "Selector " + string(b)
}
// Example 用示例(不是正式测试,仅供快速手动运行
// Example 使用示例(仅供参考,非单元测试
func Example() {
// 初始化根 selector
// 初始化根选择器
root := MustNew(map[string]interface{}{
"className": "android.widget.LinearLayout",
})
// 添加 child
// 添加子元素选择器
root.Child(map[string]interface{}{
"text": "下一步",
"instance": 0,
})
// 更新最后一个 child 的 instance
// 更新最后一个子选择器的 instance
_ = root.UpdateInstance(2)
// 深拷贝
cpy := root.Clone()
// 序列化 JSON
// 序列化 JSON
j, _ := cpy.ToJSON()
fmt.Println(string(j))
}
// 简单测试函数(你可在 package 内使用 testing 包将其改写成真正的单元测试
// SimpleTests 简单测试函数(建议迁移到 _test.go 文件中使用 testing 包
func SimpleTests() {
// set & delete
// 设置与删除字段
s := MustNew(map[string]interface{}{"text": "hello"})
fmt.Println("mask after set:", strconv.FormatUint(uint64(s.Mask()), 10))
fmt.Println("设置后掩码:", strconv.FormatUint(uint64(s.Mask()), 10))
_ = s.Delete("text")
fmt.Println("mask after delete:", strconv.FormatUint(uint64(s.Mask()), 10))
fmt.Println("删除后掩码:", strconv.FormatUint(uint64(s.Mask()), 10))
// bool 类型校验
// 类型校验bool 字段传入非 bool 值应报错
_, err := New(map[string]interface{}{"checkable": "yes"})
fmt.Println("expected error for bad bool:", err != nil)
fmt.Println("非法 bool 值报错:", err != nil)
// clone 深拷贝检查
// 深拷贝独立性验证
s2 := MustNew(map[string]interface{}{"text": "a"})
s2.Child(map[string]interface{}{"text": "b", "instance": 1})
c := s2.Clone()
c.childOrSibling[0] = "sibling"
fmt.Println("original childOrSibling:", s2.childOrSibling[0], "clone childOrSibling:", c.childOrSibling[0])
fmt.Println("原始 childOrSibling:", s2.childOrSibling[0], "克隆 childOrSibling:", c.childOrSibling[0])
}