fix(cua): 保留相同文字的值语义并压缩结构容器
This commit is contained in:
1 parent
6ff1825139
commit
4e5234ac9c
5 files changed
+32
-11
No files matched your search
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 7b30b973a849247d
|
||||
/deepseek-ai-dsh-cua/use-this-package:
|
||||
en: af80087c0e83d91f
|
||||
zh: 855204f287465147
|
||||
en: d8c3e57d3f825b2d
|
||||
zh: 9dc16af1e8b738b5
|
||||
/deepseek-ai-dsh-cua/use-this-package/connect-an-external-execution-environment:
|
||||
en: a9a65a2953478d19
|
||||
zh: ef06dc75607e1724
|
||||
|
||||
@@ -62,7 +62,7 @@ Raw `getState` exposes validated state fields and the complete text for the requ
|
||||
|
||||
AX observations show menu-bar entries by default. Use `app.getAXState({ includeMenus: true })` to expand menu contents and `app.getAXState({ details: true })` for identifiers, global-point bounds and complete action names. These options also apply to raw `getState` and combined AX/screenshot observations. Every observation replaces that scope's indices; obtain fresh indices from the expanded output. Details do not bypass the configured node and depth limits. Truncation identifies the reached limit.
|
||||
|
||||
Values and placeholders are separate. Named, stateful or actionable groups and scroll areas retain their own rows. Equal fields on one node are shown once; body text shared by distinct nodes remains intact. Writability is explicit on relevant controls, including `AXIncrementor`; `unknown` means the system did not report it. Missing optional state fields do not imply false. UTF-16 ranges outside the observed value are marked unavailable. Custom action aliases omit AppKit target/selector diagnostics; ambiguous aliases require a complete name from `details:true`.
|
||||
Values and placeholders are separate. Named, stateful or actionable groups and scroll areas retain their own rows. Repeated names, descriptions and help text on one node are shown once. Values, placeholders and URLs keep their distinct meanings even when their text matches; body text shared by distinct nodes remains intact. Writability is explicit on relevant controls, including `AXIncrementor`; `unknown` means the system did not report it. Missing optional state fields do not imply false. UTF-16 ranges outside the observed value are marked unavailable. Custom action aliases omit AppKit target/selector diagnostics; ambiguous aliases require a complete name from `details:true`.
|
||||
|
||||
Observations emit by default and also return their value. In persistent PTC, use `void await app.getAXState()` for one emitted copy, or `emit:false` and one explicit print. Capturing a screenshot after an emitted tree can fail independently; retain the successful AX output.
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ CUA 工具在读取 AX、窗口标题、截图、激活和输入之前检查应
|
||||
|
||||
AX 观察默认只展示菜单栏入口。使用 `app.getAXState({ includeMenus: true })` 展开菜单内容,使用 `app.getAXState({ details: true })` 读取标识符、全局 point 坐标和完整动作名。原始 `getState` 和 AX/截图组合观察也接受这两个选项。每次观察都会替换该范围的元素编号,展开后应使用新输出中的编号。详情不会绕过配置的节点数和深度上限;截断提示会指明触发的上限。
|
||||
|
||||
实际值与占位文字分别展示。有名称、状态或可用动作的分组和滚动区域保留独立行。同一节点的相同字段只显示一次,不删除不同节点共有的正文。相关控件明确展示可写性,包括 `AXIncrementor`;`unknown` 表示系统未报告。缺少可选状态字段不代表 false。超出当前值长度的 UTF-16 选区标为不可用。自定义动作简称省略 AppKit 的 target/selector 诊断信息;同名动作需要使用 `details:true` 返回的完整名称。
|
||||
实际值与占位文字分别展示。有名称、状态或可用动作的分组和滚动区域保留独立行。同一节点中重复的名称、描述和帮助只显示一次。实际值、占位文字和 URL 即使文字相同也保留各自含义,不删除不同节点共有的正文。相关控件明确展示可写性,包括 `AXIncrementor`;`unknown` 表示系统未报告。缺少可选状态字段不代表 false。超出当前值长度的 UTF-16 选区标为不可用。自定义动作简称省略 AppKit 的 target/selector 诊断信息;同名动作需要使用 `details:true` 返回的完整名称。
|
||||
|
||||
观察方法默认自动输出,同时返回结果。在 persistent PTC 中使用 `void await app.getAXState()` 输出一次,或指定 `emit:false` 后自行打印一次。AX 输出后的截图可能单独失败,已成功取得的 AX 输出仍保留。
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ struct AXTextNode {
|
||||
}
|
||||
|
||||
var hasMeaning: Bool {
|
||||
[title, value, description, placeholder, help, identifier, url, subrole == "AXUnknown" ? nil : subrole].contains { !($0?.isEmpty ?? true) }
|
||||
[title, value, description, placeholder, help, identifier, url, ["AXUnknown", "AXHostingView"].contains(subrole ?? "") ? nil : subrole].contains { !($0?.isEmpty ?? true) }
|
||||
|| enabled == false || selected == true || focused == true || expanded != nil
|
||||
}
|
||||
var transparent: Bool {
|
||||
@@ -62,7 +62,8 @@ enum AXTextFormatter {
|
||||
var line = String(repeating: " ", count: level) + "- "
|
||||
if let index = node.index { line += "[\(index)] " }
|
||||
line += node.role
|
||||
if let subrole = node.subrole, !subrole.isEmpty, subrole != "AXUnknown", subrole != node.role { line += " subrole=\(subrole)" }
|
||||
if let subrole = node.subrole, !subrole.isEmpty, subrole != "AXUnknown", subrole != node.role,
|
||||
details || !["AXStandardWindow", "AXHostingView"].contains(subrole) { line += " subrole=\(subrole)" }
|
||||
if node.enabled == false { line += " disabled" }
|
||||
if node.focused == true { line += " focused" }
|
||||
if let expanded = node.expanded { line += " expanded=\(expanded)" }
|
||||
@@ -75,19 +76,22 @@ enum AXTextFormatter {
|
||||
line += " selectionWritable=\(node.selectionWritable.map(String.init) ?? "unknown")"
|
||||
}
|
||||
var seen: Set<String> = []
|
||||
func field(_ value: String?, prefix: String) {
|
||||
guard let value, !value.isEmpty, seen.insert(value).inserted else { return }
|
||||
func field(_ value: String?, prefix: String, deduplicate: Bool = true) {
|
||||
guard let value, !value.isEmpty else { return }
|
||||
let unique = seen.insert(value).inserted
|
||||
guard !deduplicate || unique else { return }
|
||||
line += prefix + String(reflecting: value)
|
||||
}
|
||||
field(node.title, prefix: " ")
|
||||
if node.title?.isEmpty != false && !details { field(node.description, prefix: " ") }
|
||||
// Empty values distinguish an unfilled input from its placeholder.
|
||||
if let value = node.value, value.isEmpty, AXTextNode.valueRoles.contains(node.role) { line += " value=\"\"" }
|
||||
else if node.value == nil && AXTextNode.valueRoles.contains(node.role) { line += " value=unavailable" }
|
||||
else { field(node.value, prefix: " = ") }
|
||||
field(node.placeholder, prefix: " placeholder=")
|
||||
else { field(node.value, prefix: " = ", deduplicate: false) }
|
||||
field(node.placeholder, prefix: " placeholder=", deduplicate: false)
|
||||
field(node.description, prefix: " description=")
|
||||
field(node.help, prefix: " help=")
|
||||
field(node.url, prefix: " url=")
|
||||
field(node.url, prefix: " url=", deduplicate: false)
|
||||
if details || seen.isEmpty { field(node.identifier, prefix: " id=") }
|
||||
if node.invalidSelection { line += " selection=unavailable" }
|
||||
else if let range = node.selectedRange { line += " selection=[\(range.location),\(range.length)]" }
|
||||
|
||||
@@ -109,3 +109,20 @@ private func node(_ role: String = "AXButton") -> AXTextNode {
|
||||
#expect(AXTextFormatter.truncation(maxElements: 2000, maxDepth: nil).contains("(maxElements=2000)"))
|
||||
#expect(AXTextFormatter.truncation(maxElements: 1, maxDepth: 1).contains("(maxElements=1, maxDepth=1)"))
|
||||
}
|
||||
|
||||
@Test func repeatedWordsDoNotConflateLabelsValuesAndPlaceholders() {
|
||||
var field = node("AXTextField")
|
||||
field.title = "Example"
|
||||
field.description = "Example"
|
||||
field.value = "Example"
|
||||
field.placeholder = "Example"
|
||||
let line = AXTextFormatter.line(field, level: 0)
|
||||
#expect(line.contains("\"Example\" = \"Example\" placeholder=\"Example\""))
|
||||
#expect(!line.contains("description="))
|
||||
var button = node()
|
||||
button.description = "Delete"
|
||||
#expect(AXTextFormatter.line(button, level: 0) == "- [7] AXButton \"Delete\"")
|
||||
var wrapper = node("AXGroup")
|
||||
wrapper.subrole = "AXHostingView"
|
||||
#expect(wrapper.transparent)
|
||||
}
|
||||
Reference in new issue
Block a user