@forsion/tangu-computer-use 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/UPSTREAM.md +3 -1
- package/icon.png +0 -0
- package/main.js +15 -0
- package/manifest.json +3 -3
- package/native/macos/bridge.swift +171 -7
- package/native/windows/bridge-rs/Cargo.lock +1 -0
- package/native/windows/bridge-rs/Cargo.toml +11 -0
- package/native/windows/bridge-rs/src/agent_marker.rs +97 -0
- package/native/windows/bridge-rs/src/lib.rs +4 -0
- package/native/windows/bridge-rs/src/main.rs +32 -0
- package/native/windows/bridge-rs/src/recorder.rs +1865 -0
- package/native/windows/bridge-rs/src/recorder_logic.rs +851 -0
- package/native/windows/bridge-rs/tests/recorder_logic_tests.rs +314 -0
- package/package.json +1 -1
- package/prebuilt/linux/x64/linux-bridge +0 -0
- package/prebuilt/macos/arm64/bridge +0 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.json +4 -4
- package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/macos/x64/bridge +0 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.json +4 -4
- package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/windows/windows-bridge.exe +0 -0
- package/scripts/no-foreground.check.mjs +14 -1
- package/scripts/probe-recorder-windows.mjs +356 -0
- package/skills/computer-use/SKILL.md +4 -0
- package/src/foregroundNote.ts +16 -0
- package/src/index.ts +2 -0
- package/src/tools.ts +4 -3
- package/src/vendor/platform/macos/helper.ts +1 -1
- package/tangu-plugins/computer-use/dist/foregroundNote.js +10 -1
- package/tangu-plugins/computer-use/dist/index.js +13 -2
- package/tangu-plugins/computer-use/tangu-plugin.json +1 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
//! 电脑历史采集器纯逻辑(src/recorder_logic.rs)的单测 —— 与 macOS activity_recorder_tests.swift 同一批用例,
|
|
2
|
+
//! 任何平台都能跑(`cargo test`)。
|
|
3
|
+
|
|
4
|
+
use serde_json::{json, Value};
|
|
5
|
+
use windows_bridge::recorder_logic::*;
|
|
6
|
+
|
|
7
|
+
fn ctx(name: &str, bundle: &str, title: Option<&str>, url: Option<&str>) -> Context {
|
|
8
|
+
Context {
|
|
9
|
+
name: name.into(),
|
|
10
|
+
bundle_id: bundle.into(),
|
|
11
|
+
title: title.map(Into::into),
|
|
12
|
+
url: url.map(Into::into),
|
|
13
|
+
..Context::default()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn excluded(body: &Option<serde_json::Map<String, Value>>) -> bool {
|
|
18
|
+
body.as_ref()
|
|
19
|
+
.and_then(|b| b.get("app"))
|
|
20
|
+
.and_then(|a| a.get("excluded"))
|
|
21
|
+
.and_then(Value::as_bool)
|
|
22
|
+
== Some(true)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn field<'a>(body: &'a Option<serde_json::Map<String, Value>>, key: &str) -> Option<&'a str> {
|
|
26
|
+
body.as_ref().and_then(|b| b.get(key)).and_then(Value::as_str)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn text_diff_matches_macos() {
|
|
31
|
+
assert_eq!(text_diff("abc", "abc"), None);
|
|
32
|
+
assert_eq!(text_diff("", "hello"), Some(TextDiff { inserted: "hello".into(), deleted: 0 }));
|
|
33
|
+
assert_eq!(text_diff("hello world", "hello brave world"), Some(TextDiff { inserted: "brave ".into(), deleted: 0 }));
|
|
34
|
+
assert_eq!(text_diff("hello world", "hello"), Some(TextDiff { inserted: "".into(), deleted: 6 }));
|
|
35
|
+
assert_eq!(text_diff("cat", "cot"), Some(TextDiff { inserted: "o".into(), deleted: 1 }));
|
|
36
|
+
assert_eq!(text_diff("aa", "aaa"), Some(TextDiff { inserted: "a".into(), deleted: 0 }));
|
|
37
|
+
assert_eq!(text_diff("你好", "你们好"), Some(TextDiff { inserted: "们".into(), deleted: 0 }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn settle_text_matches_macos() {
|
|
42
|
+
assert_eq!(settle_text("", Some("hello"), ""), Some(TextDiff { inserted: "hello".into(), deleted: 0 }), "cleared after send");
|
|
43
|
+
assert_eq!(settle_text("draft", Some("draft more"), "draft more!"), Some(TextDiff { inserted: " more!".into(), deleted: 0 }));
|
|
44
|
+
assert_eq!(settle_text("abc", None, "ab"), Some(TextDiff { inserted: "".into(), deleted: 1 }));
|
|
45
|
+
assert_eq!(settle_text("x", Some("x"), "x"), None);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn text_fields_cap_and_deletions() {
|
|
50
|
+
let long = text_fields(&TextDiff { inserted: "x".repeat(900), deleted: 3 });
|
|
51
|
+
assert_eq!(long["text"].as_str().unwrap().chars().count(), limit::TEXT);
|
|
52
|
+
assert_eq!(long["truncated"], json!(true));
|
|
53
|
+
assert_eq!(long["deleted"], json!(3));
|
|
54
|
+
let deletion = text_fields(&TextDiff { inserted: "".into(), deleted: 12 });
|
|
55
|
+
assert!(deletion.get("text").is_none());
|
|
56
|
+
assert_eq!(deletion["deleted"], json!(12));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn strings_are_collapsed_and_capped() {
|
|
61
|
+
assert_eq!(truncate("abc", 5), ("abc".into(), false));
|
|
62
|
+
assert_eq!(truncate("abcdef", 3), ("abc".into(), true));
|
|
63
|
+
assert_eq!(truncate("你好世界", 2), ("你好".into(), true));
|
|
64
|
+
assert_eq!(label(" Send\n message "), Some("Send message".into()));
|
|
65
|
+
assert_eq!(label(" "), None);
|
|
66
|
+
assert_eq!(label(&"x".repeat(200)).unwrap().len(), limit::LABEL);
|
|
67
|
+
assert_eq!(title(&"t".repeat(300)).unwrap().len(), limit::TITLE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn private_markers() {
|
|
72
|
+
assert!(has_private_marker("Example — Mozilla Firefox Private Browsing"));
|
|
73
|
+
assert!(has_private_marker("New Tab - Google Chrome (Incognito)"));
|
|
74
|
+
assert!(has_private_marker("[InPrivate] Bing - Microsoft Edge"));
|
|
75
|
+
assert!(has_private_marker("新标签页 - 无痕模式"));
|
|
76
|
+
assert!(!has_private_marker("Pull requests · Private repo"));
|
|
77
|
+
assert!(!has_private_marker("Inbox (3) - Gmail"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn private_verdicts_fail_closed() {
|
|
82
|
+
assert!(title_marks_private("MSEdge.exe") && title_marks_private("firefox.exe"));
|
|
83
|
+
assert!(!title_marks_private("chrome.exe"), "Chrome incognito titles carry no marker on Windows");
|
|
84
|
+
assert_eq!(private_verdict(false, true, false), PrivateVerdict::Private, "a toolbar hint wins even mid-walk");
|
|
85
|
+
assert_eq!(private_verdict(true, true, true), PrivateVerdict::Private);
|
|
86
|
+
assert_eq!(private_verdict(true, false, false), PrivateVerdict::Normal, "title-trusted browsers need no complete walk");
|
|
87
|
+
assert_eq!(private_verdict(false, false, true), PrivateVerdict::Normal, "a complete walk without a hint proves normal");
|
|
88
|
+
assert_eq!(private_verdict(false, false, false), PrivateVerdict::Unknown, "an incomplete walk proves nothing");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn urls_are_sanitized() {
|
|
93
|
+
assert_eq!(sanitize_url("https://example.com/path?token=abc#frag").as_deref(), Some("https://example.com/path"));
|
|
94
|
+
assert_eq!(sanitize_url("https://user:pw@example.com/a").as_deref(), Some("https://example.com/a"));
|
|
95
|
+
assert_eq!(sanitize_url("example.com/docs").as_deref(), Some("https://example.com/docs"));
|
|
96
|
+
assert_eq!(sanitize_url("github.com").as_deref(), Some("https://github.com"));
|
|
97
|
+
assert_eq!(sanitize_url("HTTP://Example.com:8080/a?b").as_deref(), Some("http://Example.com:8080/a"));
|
|
98
|
+
assert_eq!(sanitize_url("how to cook rice"), None);
|
|
99
|
+
assert_eq!(sanitize_url("githu"), None);
|
|
100
|
+
assert_eq!(sanitize_url("file:///C:/Users/me/secret.pdf"), None);
|
|
101
|
+
assert_eq!(sanitize_url("chrome://newtab"), None);
|
|
102
|
+
assert_eq!(sanitize_url("edge://settings"), None);
|
|
103
|
+
assert_eq!(sanitize_url(&format!("https://example.com/{}", "a".repeat(400))).unwrap().chars().count(), limit::URL);
|
|
104
|
+
assert_eq!(url_host("https://Docs.Example.com/x").as_deref(), Some("docs.example.com"));
|
|
105
|
+
assert_eq!(url_host("https://user@Example.com:8443/x").as_deref(), Some("example.com"));
|
|
106
|
+
assert_eq!(url_host("http://[::1]:3000/").as_deref(), Some("::1"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[test]
|
|
110
|
+
fn address_bar_values() {
|
|
111
|
+
assert_eq!(address_bar_url(""), (None, true), "new tab page: resolved, no site");
|
|
112
|
+
assert_eq!(address_bar_url("github.com/forsion"), (Some("https://github.com/forsion".into()), true));
|
|
113
|
+
assert_eq!(address_bar_url("chrome://settings/privacy"), (None, true));
|
|
114
|
+
assert_eq!(address_bar_url("edge://newtab"), (None, true));
|
|
115
|
+
assert_eq!(address_bar_url("about:blank"), (None, true));
|
|
116
|
+
assert_eq!(address_bar_url("mybank"), (None, false), "half-typed text is unresolved");
|
|
117
|
+
assert_eq!(address_bar_url("how to cook rice"), (None, false));
|
|
118
|
+
assert_eq!(address_bar_url("view-source:https://x.com"), (None, false), "wrapping schemes are not internal pages");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn address_field_detection() {
|
|
123
|
+
assert!(is_address_field("Address and search bar", "OmniboxViewViews", ""));
|
|
124
|
+
assert!(is_address_field("", "OmniboxViewViews", ""));
|
|
125
|
+
assert!(is_address_field("Search with Google or enter address", "", "urlbar-input"));
|
|
126
|
+
assert!(is_address_field("地址和搜索栏", "", ""));
|
|
127
|
+
assert!(!is_address_field("Message", "", ""));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn domains_and_bundles() {
|
|
132
|
+
assert_eq!(normalize_domain("https://www.Example.com/path").as_deref(), Some("www.example.com"));
|
|
133
|
+
assert_eq!(normalize_domain("*.example.com").as_deref(), Some("example.com"));
|
|
134
|
+
assert_eq!(normalize_domain(".example.com.").as_deref(), Some("example.com"));
|
|
135
|
+
assert_eq!(normalize_domain("example.com:8443").as_deref(), Some("example.com"));
|
|
136
|
+
assert_eq!(normalize_domain(" "), None);
|
|
137
|
+
assert!(host_matches("example.com", "example.com"));
|
|
138
|
+
assert!(host_matches("mail.example.com", "example.com"));
|
|
139
|
+
assert!(host_matches("MAIL.example.com.", "example.com"));
|
|
140
|
+
assert!(!host_matches("badexample.com", "example.com"));
|
|
141
|
+
assert!(!host_matches("example.com.evil.net", "example.com"));
|
|
142
|
+
assert!(bundle_matches("com.forsion.desktop", "com.forsion.*"));
|
|
143
|
+
assert!(!bundle_matches("com.forsionx.desktop", "com.forsion.*"));
|
|
144
|
+
assert!(bundle_matches("Chrome.EXE", "chrome.exe"));
|
|
145
|
+
assert!(!bundle_matches("chrome.exe.bak", "chrome.exe"));
|
|
146
|
+
assert!(is_hard_excluded("KeePassXC.exe"));
|
|
147
|
+
assert!(is_hard_excluded("1Password.exe"));
|
|
148
|
+
assert!(!is_hard_excluded("notepad.exe"));
|
|
149
|
+
assert!(is_browser("MSEdge.exe") && is_browser("firefox.exe") && !is_browser("thunderbird.exe"));
|
|
150
|
+
assert!(is_ignored("SearchHost.exe", "") && is_ignored("explorer.exe", "Shell_TrayWnd"));
|
|
151
|
+
assert!(!is_ignored("explorer.exe", "CabinetWClass"));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn controls() {
|
|
156
|
+
assert_eq!(text_mode(CT_EDIT, false, Some(true), None), Some(TextMode::Value));
|
|
157
|
+
assert_eq!(text_mode(CT_EDIT, true, Some(true), None), None, "password fields are never text");
|
|
158
|
+
assert_eq!(text_mode(CT_EDIT, false, Some(false), Some(true)), None, "read-only value means not editable");
|
|
159
|
+
assert_eq!(text_mode(CT_DOCUMENT, false, None, Some(true)), Some(TextMode::Text), "rich text documents");
|
|
160
|
+
assert_eq!(text_mode(CT_DOCUMENT, false, None, Some(false)), None, "a read-only web page is not a field");
|
|
161
|
+
assert_eq!(text_mode(CT_GROUP, false, None, Some(true)), Some(TextMode::Text), "web contenteditable");
|
|
162
|
+
assert_eq!(text_mode(CT_BUTTON, false, Some(true), None), None, "buttons with a value are not text");
|
|
163
|
+
assert_eq!(text_mode(CT_COMBOBOX, false, None, Some(true)), None);
|
|
164
|
+
assert_eq!(text_mode(CT_BUTTON, false, None, None), None);
|
|
165
|
+
assert!(click_label_allowed(CT_BUTTON) && click_label_allowed(CT_TABITEM) && click_label_allowed(CT_HYPERLINK));
|
|
166
|
+
assert!(!click_label_allowed(CT_EDIT), "text fields never carry a label");
|
|
167
|
+
assert!(!click_label_allowed(CT_DOCUMENT));
|
|
168
|
+
assert_eq!(control_type_name(CT_MENUITEM), "MenuItem");
|
|
169
|
+
assert_eq!(control_type_name(CT_RADIOBUTTON), "RadioButton");
|
|
170
|
+
assert_eq!(control_type_name(1), "Unknown");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn shortcuts() {
|
|
175
|
+
assert_eq!(key_combo(true, false, true, false, "P").as_deref(), Some("Ctrl+Shift+P"));
|
|
176
|
+
assert_eq!(key_combo(false, false, true, true, "S").as_deref(), Some("Win+Shift+S"));
|
|
177
|
+
assert_eq!(key_combo(false, true, true, false, "A"), None, "no Ctrl or Win means no shortcut");
|
|
178
|
+
assert_eq!(key_name(0x41, None).as_deref(), Some("A"));
|
|
179
|
+
assert_eq!(key_name(0x35, None).as_deref(), Some("5"));
|
|
180
|
+
assert_eq!(key_name(0x0D, None).as_deref(), Some("Enter"));
|
|
181
|
+
assert_eq!(key_name(0x70, None).as_deref(), Some("F1"));
|
|
182
|
+
assert_eq!(key_name(0x7B, None).as_deref(), Some("F12"));
|
|
183
|
+
assert_eq!(key_name(0xBF, Some('/')).as_deref(), Some("/"));
|
|
184
|
+
assert_eq!(key_name(0xBA, Some('\u{3}')), None, "control characters are not key names");
|
|
185
|
+
assert_eq!(key_name(0xBA, None), None);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn event_bodies() {
|
|
190
|
+
let open = Policy::default();
|
|
191
|
+
let chrome = ctx("Google Chrome", "chrome.exe", Some("Docs"), Some("https://docs.example.com/a"));
|
|
192
|
+
let full = event_body(Kind::Window, Some(&chrome), &open);
|
|
193
|
+
assert_eq!(field(&full, "title"), Some("Docs"));
|
|
194
|
+
assert_eq!(field(&full, "url"), Some("https://docs.example.com/a"));
|
|
195
|
+
|
|
196
|
+
let domain_off = Policy::from_json(Some(&json!({ "excludeDomains": ["example.com"] })));
|
|
197
|
+
let switched = event_body(Kind::App, Some(&chrome), &domain_off);
|
|
198
|
+
assert!(excluded(&switched) && field(&switched, "title").is_none() && field(&switched, "url").is_none());
|
|
199
|
+
assert!(event_body(Kind::Text, Some(&chrome), &domain_off).is_none());
|
|
200
|
+
assert!(event_body(Kind::Click, Some(&chrome), &domain_off).is_none());
|
|
201
|
+
let marker = event_body(Kind::Window, Some(&chrome), &domain_off);
|
|
202
|
+
assert!(excluded(&marker) && field(&marker, "title").is_none());
|
|
203
|
+
|
|
204
|
+
let app_off = Policy::from_json(Some(&json!({ "excludeBundleIds": ["Chrome.exe"] })));
|
|
205
|
+
let app_switch = event_body(Kind::App, Some(&chrome), &app_off);
|
|
206
|
+
assert!(excluded(&app_switch) && field(&app_switch, "title").is_none());
|
|
207
|
+
assert!(event_body(Kind::Window, Some(&chrome), &app_off).is_none());
|
|
208
|
+
assert!(event_body(Kind::Key, Some(&chrome), &app_off).is_none());
|
|
209
|
+
|
|
210
|
+
let title_only = Policy::from_json(Some(&json!({ "titleOnlyBundleIds": ["windowsterminal.exe", "com.forsion.*"] })));
|
|
211
|
+
let terminal = ctx("Windows Terminal", "windowsterminal.exe", Some("pwsh"), None);
|
|
212
|
+
assert_eq!(field(&event_body(Kind::Window, Some(&terminal), &title_only), "title"), Some("pwsh"));
|
|
213
|
+
assert!(event_body(Kind::Text, Some(&terminal), &title_only).is_none());
|
|
214
|
+
|
|
215
|
+
let mut incognito = ctx("Google Chrome", "chrome.exe", None, None);
|
|
216
|
+
incognito.is_private = true;
|
|
217
|
+
let private_app = event_body(Kind::App, Some(&incognito), &open);
|
|
218
|
+
assert!(private_app.is_some() && field(&private_app, "title").is_none() && !excluded(&private_app));
|
|
219
|
+
assert!(event_body(Kind::Text, Some(&incognito), &open).is_none());
|
|
220
|
+
assert!(event_body(Kind::Window, Some(&incognito), &open).is_none());
|
|
221
|
+
|
|
222
|
+
let mut vault = ctx("KeePassXC", "keepassxc.exe", None, None);
|
|
223
|
+
vault.hard_excluded = true;
|
|
224
|
+
assert!(excluded(&event_body(Kind::App, Some(&vault), &open)), "hard exclusion beats an open policy");
|
|
225
|
+
assert!(event_body(Kind::Window, Some(&vault), &open).is_none());
|
|
226
|
+
|
|
227
|
+
let no_text = Policy::from_json(Some(&json!({ "text": false, "clicks": true, "keys": false })));
|
|
228
|
+
assert!(event_body(Kind::Text, Some(&chrome), &no_text).is_none());
|
|
229
|
+
assert!(event_body(Kind::Key, Some(&chrome), &no_text).is_none());
|
|
230
|
+
assert!(event_body(Kind::Click, Some(&chrome), &no_text).is_some());
|
|
231
|
+
assert_eq!(field(&event_body(Kind::System, None, &open), "kind"), Some("system"));
|
|
232
|
+
|
|
233
|
+
let garbage = Policy::from_json(Some(&json!({ "excludeBundleIds": "not-a-list", "text": "yes" })));
|
|
234
|
+
assert!(garbage.exclude_bundle_ids.is_empty() && garbage.text);
|
|
235
|
+
assert_eq!(Policy::from_json(Some(&json!({ "excludeDomains": ["Example.com"] }))).exclude_domains, vec!["example.com"]);
|
|
236
|
+
|
|
237
|
+
let mut unknown = ctx("Google Chrome", "chrome.exe", Some("Chase — Accounts"), None);
|
|
238
|
+
unknown.url_unknown = true;
|
|
239
|
+
let fail_closed = event_body(Kind::Window, Some(&unknown), &domain_off);
|
|
240
|
+
assert!(excluded(&fail_closed) && field(&fail_closed, "title").is_none(), "unknown URL fails closed when sites are excluded");
|
|
241
|
+
assert_eq!(field(&event_body(Kind::Window, Some(&unknown), &open), "title"), Some("Chase — Accounts"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn private_context_delivery() {
|
|
246
|
+
let docs = ctx("Google Chrome", "chrome.exe", Some("Docs"), Some("https://docs.example.com/a"));
|
|
247
|
+
let mail = ctx("Google Chrome", "chrome.exe", Some("Inbox"), Some("https://mail.example.com/"));
|
|
248
|
+
let mut incognito = ctx("Google Chrome", "chrome.exe", None, None);
|
|
249
|
+
incognito.is_private = true;
|
|
250
|
+
let open = Policy::default();
|
|
251
|
+
let mut state = DeliveryState::default();
|
|
252
|
+
let mut deliver = |kind, c: &Context, dedupe| context_delivery(kind, Some(c), &open, &mut state, dedupe);
|
|
253
|
+
assert_eq!(field(&deliver(Kind::App, &docs, false), "title"), Some("Docs"));
|
|
254
|
+
assert!(deliver(Kind::Window, &docs, true).is_none(), "identical window event is deduped");
|
|
255
|
+
assert!(deliver(Kind::Window, &incognito, true).is_none(), "entering private sends nothing");
|
|
256
|
+
let back = deliver(Kind::Window, &docs, true);
|
|
257
|
+
assert_eq!(field(&back, "title"), Some("Docs"), "returning to the same normal window is sent again");
|
|
258
|
+
assert_eq!(back.as_ref().unwrap()["resumed"], json!(true), "and carries the resumed marker");
|
|
259
|
+
assert!(deliver(Kind::Window, &docs, true).is_none());
|
|
260
|
+
let _ = deliver(Kind::Window, &incognito, true);
|
|
261
|
+
assert!(deliver(Kind::App, &incognito, true).is_none(), "re-activation while private sends no second switch");
|
|
262
|
+
assert_eq!(field(&deliver(Kind::Window, &mail, true), "title"), Some("Inbox"));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn resumed_marker_is_per_policy_and_once() {
|
|
267
|
+
let docs = ctx("Google Chrome", "chrome.exe", Some("Docs"), Some("https://docs.example.com/a"));
|
|
268
|
+
let bank = ctx("Google Chrome", "chrome.exe", Some("Accounts"), Some("https://mybank.com/accounts"));
|
|
269
|
+
let site_off = Policy::from_json(Some(&json!({ "excludeDomains": ["mybank.com"] })));
|
|
270
|
+
let mut state = DeliveryState::default();
|
|
271
|
+
assert!(context_delivery(Kind::App, Some(&docs), &site_off, &mut state, false).unwrap().get("resumed").is_none());
|
|
272
|
+
let marker = context_delivery(Kind::Window, Some(&bank), &site_off, &mut state, true);
|
|
273
|
+
assert!(excluded(&marker) && marker.as_ref().unwrap().get("resumed").is_none(), "the excluded marker itself never carries resumed");
|
|
274
|
+
let back = context_delivery(Kind::Window, Some(&docs), &site_off, &mut state, true).unwrap();
|
|
275
|
+
assert_eq!(back["resumed"], json!(true));
|
|
276
|
+
let again = context_delivery(Kind::Window, Some(&bank), &site_off, &mut state, true);
|
|
277
|
+
assert!(excluded(&again));
|
|
278
|
+
// 另一个不排除这个站点的订阅者:同一串情境没有断点
|
|
279
|
+
let mut open_state = DeliveryState::default();
|
|
280
|
+
let open = Policy::default();
|
|
281
|
+
let _ = context_delivery(Kind::App, Some(&docs), &open, &mut open_state, false);
|
|
282
|
+
let _ = context_delivery(Kind::Window, Some(&bank), &open, &mut open_state, true);
|
|
283
|
+
assert!(context_delivery(Kind::Window, Some(&docs), &open, &mut open_state, true).unwrap().get("resumed").is_none());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
fn pending_actions() {
|
|
288
|
+
assert_eq!(pending_on_refresh(false, false, false), PendingAction::Drop);
|
|
289
|
+
assert_eq!(pending_on_refresh(true, true, false), PendingAction::Flush);
|
|
290
|
+
assert_eq!(pending_on_refresh(true, false, true), PendingAction::Flush);
|
|
291
|
+
assert_eq!(pending_on_refresh(true, false, false), PendingAction::Keep);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn pipe_names() {
|
|
296
|
+
assert!(pipe_name_valid(r"\\.\pipe\tangu-computer-use-recorder-1a2b3c4d-0123456789ab"));
|
|
297
|
+
assert!(!pipe_name_valid(r"\\server\pipe\x"));
|
|
298
|
+
assert!(!pipe_name_valid(r"\\.\pipe\"));
|
|
299
|
+
assert!(!pipe_name_valid(r"\\.\pipe\a\b"));
|
|
300
|
+
assert!(!pipe_name_valid(r"C:\temp\x"));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[test]
|
|
304
|
+
fn context_keys_match_desktop() {
|
|
305
|
+
let open = Policy::default();
|
|
306
|
+
let chrome = ctx("Google Chrome", "chrome.exe", Some("Docs"), Some("https://docs.example.com/a"));
|
|
307
|
+
let a = context_key(&event_body(Kind::App, Some(&chrome), &open).unwrap());
|
|
308
|
+
let b = context_key(&event_body(Kind::Window, Some(&chrome), &open).unwrap());
|
|
309
|
+
let mut retitled = chrome.clone();
|
|
310
|
+
retitled.title = Some("Other".into());
|
|
311
|
+
assert_eq!(a, b);
|
|
312
|
+
assert_ne!(a, context_key(&event_body(Kind::Window, Some(&retitled), &open).unwrap()));
|
|
313
|
+
assert_eq!(a, "chrome.exe\u{1F}\u{1F}Docs\u{1F}https://docs.example.com/a");
|
|
314
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forsion/tangu-computer-use",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Tangu engine plugin: let agents observe and control macOS, Windows, and Linux desktop apps. Fork of injaneity/pi-computer-use (MIT).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bundleId": "com.forsion.tangu-computer-use",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"archiveSha256": "
|
|
3
|
+
"version": "0.6.2",
|
|
4
|
+
"archiveSha256": "d6c815f307a2f3119ba14b4821b7e972449c7eff4e056ad39cce12381f8f3061",
|
|
5
5
|
"files": {
|
|
6
|
-
"Contents/MacOS/bridge": "
|
|
7
|
-
"Contents/Info.plist": "
|
|
6
|
+
"Contents/MacOS/bridge": "bb497e3f4a3d03141f2fb14323111fc2c1a27dd3f526c1170a3ebb1bd0d9a431",
|
|
7
|
+
"Contents/Info.plist": "b09a226bd57436b8e6e1ca6447c590820f46222b87b1a96084660a28f81c2cd7",
|
|
8
8
|
"Contents/_CodeSignature/CodeResources": "6686de10a28a2fe11b36cbb86dcbacc827cfc4ea116b4dabf1845e5aee629e9b"
|
|
9
9
|
}
|
|
10
10
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bundleId": "com.forsion.tangu-computer-use",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"archiveSha256": "
|
|
3
|
+
"version": "0.6.2",
|
|
4
|
+
"archiveSha256": "3dba34fdc2ae8c7ac9870f50c11c04c6c2080d73b148c3dfbeeac160cf4d49a9",
|
|
5
5
|
"files": {
|
|
6
|
-
"Contents/MacOS/bridge": "
|
|
7
|
-
"Contents/Info.plist": "
|
|
6
|
+
"Contents/MacOS/bridge": "2477f7f95065592cfa60c11928b5622837a66bfe5f2ee540f88417e774769874",
|
|
7
|
+
"Contents/Info.plist": "b09a226bd57436b8e6e1ca6447c590820f46222b87b1a96084660a28f81c2cd7",
|
|
8
8
|
"Contents/_CodeSignature/CodeResources": "6686de10a28a2fe11b36cbb86dcbacc827cfc4ea116b4dabf1845e5aee629e9b"
|
|
9
9
|
}
|
|
10
10
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -20,7 +20,7 @@ const execFileP = promisify(execFile);
|
|
|
20
20
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
|
|
22
22
|
async function unitTests() {
|
|
23
|
-
const { foregroundNote } = await import(path.join(here, '..', 'tangu-plugins', 'computer-use', 'dist', 'foregroundNote.js'));
|
|
23
|
+
const { foregroundNote, selfWindowNote } = await import(path.join(here, '..', 'tangu-plugins', 'computer-use', 'dist', 'foregroundNote.js'));
|
|
24
24
|
const has = (s) => s.includes('[foreground]');
|
|
25
25
|
|
|
26
26
|
// 无信号 → 空串
|
|
@@ -47,6 +47,19 @@ async function unitTests() {
|
|
|
47
47
|
assert.ok(withReason.includes('foreground_required'), 'reason surfaced');
|
|
48
48
|
|
|
49
49
|
console.log('✓ unit: foregroundNote 判据 9 项全过');
|
|
50
|
+
|
|
51
|
+
// 自家窗口提示(Forsion 反馈 6a239e58):目标 pid = 拉起引擎的桌面应用 → 提示改用界面命令;别的 App 不提示。
|
|
52
|
+
const own = (s) => s.includes('[forsion]');
|
|
53
|
+
assert.ok(own(selfWindowNote({ target: { pid: 80732 } }, 80732)), 'observe/act on the host app → note');
|
|
54
|
+
assert.ok(own(selfWindowNote({ windows: [{ pid: 501 }, { pid: 80732 }] }, 80732)), 'find_roots listing a host window → note');
|
|
55
|
+
assert.ok(selfWindowNote({ target: { pid: 80732 } }, 80732).includes('run_ui_command'), 'note names the UI tools');
|
|
56
|
+
assert.equal(selfWindowNote({ target: { pid: 501 } }, 80732), '', 'another app → empty');
|
|
57
|
+
assert.equal(selfWindowNote({ windows: [{ pid: 501 }] }, 80732), '', 'find_roots without host windows → empty');
|
|
58
|
+
assert.equal(selfWindowNote({ windows: [{ pid: 0 }] }, 0), '', 'browser pages carry pid 0; no host pid → empty');
|
|
59
|
+
assert.equal(selfWindowNote({ target: { pid: 1 } }, 1), '', 'orphaned engine (ppid 1) → empty');
|
|
60
|
+
assert.equal(selfWindowNote(undefined, 80732), '', 'no details → empty');
|
|
61
|
+
assert.equal(selfWindowNote({ tool: 'read_text' }, 80732), '', 'details without a target → empty');
|
|
62
|
+
console.log('✓ unit: selfWindowNote 判据 9 项全过');
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
async function frontmostBundleId() {
|