AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
// 保險庫 / 1Password / 安全表單: the bot can sign in to a site in AnyBot's browser without ever
// seeing the password (the original keeps logins in a vault and fills them through a secure
// form; it also connects to 1Password). The login comes from the macOS Keychain (AnyBot's own
// saved logins or any internet password there) or the 1Password CLI (`op`), and is typed into
// the page by AnyBot; the model only hears that it was filled.
use serde_json ::Value ;
use std ::process ::Command ;
pub const TOOL : & str = " browser_fill_login " ;
/// Keychain entries AnyBot saves are marked with this label.
const LABEL : & str = " AnyBot 登入 " ;
pub const LIST_TOOL : & str = " vault_list " ;
pub const CARD_TOOL : & str = " browser_fill_card " ;
pub const CONTACT_TOOL : & str = " browser_fill_contact " ;
/// The vault tools the bot gets (with AnyBot's browser).
pub fn specs ( ) -> Vec < Value > {
vec! [
serde_json ::json! ( { " name " : LIST_TOOL , " description " : " List what the person saved in AnyBot's 保險庫 (vault): logins (site, username, notes — never the password), payment cards (nickname, brand, last 4 digits, expiry — never the number or security code) and contact info (name, address, phones, emails). Notes say when to use an item. Use the ids with browser_fill_login / browser_fill_card / browser_fill_contact. " ,
" parameters " : { " type " : " object " , " properties " : { " kind " : { " type " : " string " , " enum " : [ " all " , " login " , " card " , " contact " ] } , " query " : { " type " : " string " , " description " : " Words to search for " } } } } ) ,
serde_json ::json! ( { " name " : TOOL , " description " : " Sign in on the current tab of AnyBot's browser with a login from the person's vault (or the macOS Keychain / 1Password). You never see the password; the person approves each use. Open the login page with browser_open first; after filling, press the page's sign-in button (computer_use or ask the person) and check with browser_read. " ,
" parameters " : { " type " : " object " , " properties " : { " login_id " : { " type " : " string " , " description " : " From vault_list (default: the saved login for the current site) " } , " site " : { " type " : " string " , " description " : " Domain to use (default: the current tab's) " } , " submit " : { " type " : " boolean " , " description " : " Also submit the form " } } } } ) ,
serde_json ::json! ( { " name " : CARD_TOOL , " description " : " Fill a saved payment card into the checkout form on the current tab of AnyBot's browser (card number, expiry, security code, name). You never see the number or security code; the person approves every use. Card fields inside a payment provider's frame may need the person's help. Never submit a payment without the person saying so. " ,
" parameters " : { " type " : " object " , " properties " : { " card_id " : { " type " : " string " , " description " : " From vault_list " } } , " required " : [ " card_id " ] } } ) ,
serde_json ::json! ( { " name " : CONTACT_TOOL , " description " : " Fill saved contact info (name, address, phone, email) into the form on the current tab of AnyBot's browser, e.g. a shipping or billing address. " ,
" parameters " : { " type " : " object " , " properties " : { " contact_id " : { " type " : " string " , " description " : " From vault_list " } } , " required " : [ " contact_id " ] } } ) ,
]
}
/// vault_list: what the bot may know (no passwords, card numbers or security codes).
pub fn list_for_bot ( args : & str ) -> String {
use crate ::vault_store ::Kind ;
let v : Value = serde_json ::from_str ( if args . trim ( ) . is_empty ( ) { " {} " } else { args } ) . unwrap_or_default ( ) ;
let kind = v [ " kind " ] . as_str ( ) . unwrap_or ( " all " ) ;
let q = v [ " query " ] . as_str ( ) . unwrap_or ( " " ) . trim ( ) . to_lowercase ( ) ;
let items = match crate ::vault_store ::list ( ) {
Ok ( i ) = > i ,
Err ( e ) = > return format! ( " 讀不到保險庫: {e} " ) ,
} ;
let rows : Vec < Value > = items
. iter ( )
. filter ( | i | match kind {
" login " = > i . kind = = Kind ::Login ,
" card " = > i . kind = = Kind ::Card ,
" contact " = > i . kind = = Kind ::Contact ,
_ = > true ,
} )
. filter ( | i | q . is_empty ( ) | | i . haystack ( ) . contains ( & q ) )
. map ( | i | match i . kind {
Kind ::Login = > serde_json ::json! ( { " id " : i . id , " kind " : " login " , " title " : i . display_title ( ) , " sites " : i . sites , " username " : i . username , " notes " : i . notes } ) ,
Kind ::Card = > serde_json ::json! ( { " id " : i . id , " kind " : " card " , " title " : i . display_title ( ) , " brand " : i . brand , " last4 " : i . last4 , " expiry " : format ! ( " {:02}/{} " , i . exp_month , i . exp_year % 100 ) , " holder " : i . holder , " billing_address " : i . billing . one_line ( ) , " notes " : i . notes } ) ,
Kind ::Contact = > serde_json ::json! ( { " id " : i . id , " kind " : " contact " , " label " : i . title , " name " : i . name , " address " : { " line1 " : i . address . line1 , " line2 " : i . address . line2 , " city " : i . address . city , " region " : i . address . region , " postal_code " : i . address . postal , " country " : i . address . country } , " phones " : i . phones , " emails " : i . emails , " notes " : i . notes } ) ,
} )
. collect ( ) ;
if rows . is_empty ( ) {
return " 保險庫裡沒有符合的項目。請使用者到「設定 → 保險庫」新增,或改問使用者。 " . into ( ) ;
}
serde_json ::to_string_pretty ( & rows ) . unwrap_or_default ( )
}
/// "https://accounts.example.com/login?x" → "accounts.example.com"
pub fn host_of ( url : & str ) -> String {
let rest = url . split ( " :// " ) . nth ( 1 ) . unwrap_or ( url ) ;
rest . split ( [ '/' , '?' , '#' ] ) . next ( ) . unwrap_or ( " " ) . split ( '@' ) . last ( ) . unwrap_or ( " " ) . split ( ':' ) . next ( ) . unwrap_or ( " " ) . to_lowercase ( )
}
/// accounts.google.com → [accounts.google.com, google.com]
pub fn candidates ( host : & str ) -> Vec < String > {
let parts : Vec < & str > = host . split ( '.' ) . collect ( ) ;
let mut v = vec! [ host . to_string ( ) ] ;
for i in 1 .. parts . len ( ) . saturating_sub ( 1 ) {
v . push ( parts [ i .. ] . join ( " . " ) ) ;
}
v
}
pub struct Login {
pub username : String ,
pub password : String ,
pub source : & 'static str ,
}
// ---------- Linux: the desktop keyring (GNOME Keyring / KWallet) through Secret Service ----------
// Entries are stored with attributes application=anybot, server=<host>, user=<name>.
fn secret_tool ( args : & [ & str ] , input : Option < & str > ) -> Option < std ::process ::Output > {
use std ::io ::Write ;
let mut c = Command ::new ( " secret-tool " ) ;
// The label has Chinese in it: make sure secret-tool reads its arguments as UTF-8 (C.UTF-8
// exists everywhere; the person's own locale might not be installed).
c . env ( " LC_ALL " , " C.UTF-8 " ) ;
c . args ( args ) . stdout ( std ::process ::Stdio ::piped ( ) ) . stderr ( std ::process ::Stdio ::piped ( ) ) . stdin ( if input . is_some ( ) { std ::process ::Stdio ::piped ( ) } else { std ::process ::Stdio ::null ( ) } ) ;
let mut child = c . spawn ( ) . ok ( ) ? ;
if let ( Some ( text ) , Some ( mut w ) ) = ( input , child . stdin . take ( ) ) {
let _ = w . write_all ( text . as_bytes ( ) ) ;
}
child . wait_with_output ( ) . ok ( )
}
/// `secret-tool search --all` blocks → (attributes, secret).
pub fn parse_secret_search ( out : & str ) -> Vec < ( std ::collections ::HashMap < String , String > , String ) > {
let mut v = vec! [ ] ;
for block in out . split ( " \n [ " ) . map ( | b | b . trim_start_matches ( '[' ) ) {
let mut attrs = std ::collections ::HashMap ::new ( ) ;
let mut secret = String ::new ( ) ;
for l in block . lines ( ) {
if let Some ( ( k , val ) ) = l . split_once ( " = " ) {
match k . trim ( ) {
" secret " = > secret = val . to_string ( ) ,
k if k . starts_with ( " attribute. " ) = > {
attrs . insert ( k . trim_start_matches ( " attribute. " ) . to_string ( ) , val . to_string ( ) ) ;
}
_ = > { }
}
}
}
if ! attrs . is_empty ( ) {
v . push ( ( attrs , secret ) ) ;
}
}
v
}
fn linux_find ( host : & str ) -> Option < Login > {
let o = secret_tool ( & [ " search " , " --all " , " --unlock " , " application " , " anybot " , " server " , host ] , None ) ? ;
// secret-tool prints the matches on stdout, or (older versions) on stderr.
let text = format! ( " {} \n {} " , String ::from_utf8_lossy ( & o . stdout ) , String ::from_utf8_lossy ( & o . stderr ) ) ;
let ( attrs , secret ) = parse_secret_search ( & text ) . into_iter ( ) . find ( | ( _ , s ) | ! s . is_empty ( ) ) ? ;
Some ( Login { username : attrs . get ( " user " ) . cloned ( ) . unwrap_or_default ( ) , password : secret , source : " 系統鑰匙圈 " } )
}
/// One Keychain login by site and account (bringing old AnyBot logins into the vault file).
pub fn keychain_login ( site : & str , account : & str ) -> Option < Login > {
if ! cfg! ( target_os = " macos " ) {
return linux_find ( site ) . filter ( | l | l . username = = account ) ;
}
let out = Command ::new ( " security " ) . args ( [ " find-internet-password " , " -s " , site , " -a " , account , " -g " ] ) . output ( ) . ok ( ) ? ;
if ! out . status . success ( ) {
return None ;
}
Some ( Login { username : account . to_string ( ) , password : parse_g_password ( & String ::from_utf8_lossy ( & out . stderr ) ) ? , source : " 鑰匙圈 " } )
}
fn keychain ( host : & str ) -> Option < Login > {
if ! cfg! ( target_os = " macos " ) {
return linux_find ( host ) ;
}
// `-g` prints the attributes on stdout and the password on stderr, either as `"text"` or,
// when it isn't plain ASCII, as `0x<hex> "escaped"` (`-w` gives bare hex then, which
// can't be told apart from a password that looks like hex).
let out = Command ::new ( " security " ) . args ( [ " find-internet-password " , " -s " , host , " -g " ] ) . output ( ) . ok ( ) ? ;
if ! out . status . success ( ) {
return None ;
}
let meta = String ::from_utf8_lossy ( & out . stdout ) . to_string ( ) ;
let account = meta . lines ( ) . find ( | l | l . contains ( " \" acct \" " ) ) . and_then ( | l | l . split ( " = \" " ) . nth ( 1 ) ) . map ( | s | s . trim_end_matches ( '"' ) . to_string ( ) ) . unwrap_or_default ( ) ;
let password = parse_g_password ( & String ::from_utf8_lossy ( & out . stderr ) ) ? ;
Some ( Login { username : account , password , source : " 鑰匙圈 " } )
}
/// The password line of `security find-…-password -g`.
pub fn parse_g_password ( stderr : & str ) -> Option < String > {
let line = stderr . lines ( ) . find_map ( | l | l . strip_prefix ( " password: " ) . or_else ( | | ( l = = " password: " ) . then_some ( " " ) ) ) ? ;
if let Some ( hex ) = line . strip_prefix ( " 0x " ) {
let hex : String = hex . chars ( ) . take_while ( | c | c . is_ascii_hexdigit ( ) ) . collect ( ) ;
let bytes : Option < Vec < u8 > > = ( 0 .. hex . len ( ) / 2 ) . map ( | i | u8 ::from_str_radix ( & hex [ i * 2 .. i * 2 + 2 ] , 16 ) . ok ( ) ) . collect ( ) ;
return String ::from_utf8 ( bytes ? ) . ok ( ) ;
}
Some ( line . strip_prefix ( '"' ) . and_then ( | l | l . strip_suffix ( '"' ) ) . unwrap_or ( line ) . to_string ( ) )
}
fn onepassword ( host : & str ) -> Option < Login > {
let path = crate ::cloudbox ::tool_path ( ) ;
let list = Command ::new ( " op " ) . env ( " PATH " , & path ) . args ( [ " item " , " list " , " --categories " , " Login " , " --format " , " json " ] ) . output ( ) . ok ( ) ? ;
if ! list . status . success ( ) {
return None ;
}
let items : Value = serde_json ::from_slice ( & list . stdout ) . ok ( ) ? ;
2026-11-05 07:48:20 +00:00
// Match the host exactly or as a subdomain (with the dot): "notgoogle.com" must not
// answer for "google.com" (a plain ends_with fills the wrong login on a lookalike domain).
let host_match = | h : & str , want : & str | h = = want | | h . ends_with ( & format! ( " . {want} " ) ) ;
let id = items . as_array ( ) ? . iter ( ) . find ( | i | i [ " urls " ] . as_array ( ) . into_iter ( ) . flatten ( ) . any ( | u | host_match ( & host_of ( u [ " href " ] . as_str ( ) . unwrap_or ( " " ) ) , host ) ) ) ? [ " id " ] . as_str ( ) ? . to_string ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let got = Command ::new ( " op " ) . env ( " PATH " , & path ) . args ( [ " item " , " get " , & id , " --fields " , " label=username,label=password " , " --reveal " , " --format " , " json " ] ) . output ( ) . ok ( ) ? ;
let fields : Value = serde_json ::from_slice ( & got . stdout ) . ok ( ) ? ;
let arr = fields . as_array ( ) ? ;
let get = | l : & str | arr . iter ( ) . find ( | f | f [ " label " ] . as_str ( ) = = Some ( l ) ) . and_then ( | f | f [ " value " ] . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
Some ( Login { username : get ( " username " ) , password : get ( " password " ) , source : " 1Password " } )
}
pub fn find ( host : & str ) -> Option < Login > {
for h in candidates ( host ) {
if let Some ( l ) = keychain ( & h ) {
return Some ( l ) ;
}
}
for h in candidates ( host ) {
if let Some ( l ) = onepassword ( & h ) {
return Some ( l ) ;
}
}
None
}
/// Save a login in the macOS Keychain (the person types it in AnyBot's settings).
/// (Kept for the live tests and older callers.)
#[ cfg_attr(not(test), allow(dead_code)) ]
pub fn save ( site : & str , username : & str , password : & str ) -> Result < ( ) , String > {
let host = host_of ( site ) ;
let host = if host . is_empty ( ) { site . trim ( ) . to_lowercase ( ) } else { host } ;
if host . is_empty ( ) | | password . is_empty ( ) {
return Err ( " 請填網站和密碼 " . into ( ) ) ;
}
if ! cfg! ( target_os = " macos " ) {
let label = format! ( " {LABEL} : {host} " ) ;
let o = secret_tool ( & [ " store " , " --label " , & label , " application " , " anybot " , " server " , & host , " user " , username ] , Some ( password ) )
. ok_or ( " 找不到 secret-tool: 請安裝 libsecret-tools( 系統鑰匙圈的指令) 。 " ) ? ;
return if o . status . success ( ) { Ok ( ( ) ) } else { Err ( format! ( " 存不進系統鑰匙圈: {} " , String ::from_utf8_lossy ( & o . stderr ) . trim ( ) ) ) } ;
}
let out = Command ::new ( " security " ) . args ( [ " add-internet-password " , " -U " , " -s " , & host , " -a " , username , " -l " , LABEL , " -w " , password ] ) . output ( ) . map_err ( | e | e . to_string ( ) ) ? ;
if out . status . success ( ) {
index_add ( & host , username ) ;
Ok ( ( ) )
} else {
Err ( String ::from_utf8_lossy ( & out . stderr ) . trim ( ) . to_string ( ) )
}
}
/// AnyBot's list of the logins it saved: site and account only (the passwords stay in the
/// keychain). macOS prints a non-ASCII label like 「AnyBot 登入」 as hex bytes, so the keychain
/// can't be searched by label reliably; this list is the source of truth for what to show.
fn index_path ( ) -> std ::path ::PathBuf {
crate ::model ::State ::path ( ) . parent ( ) . map ( | d | d . join ( " vault-index.json " ) ) . unwrap_or_default ( )
}
fn read_index ( ) -> Option < Vec < ( String , String ) > > {
let mut list : Vec < ( String , String ) > = std ::fs ::read_to_string ( index_path ( ) ) . ok ( ) . and_then ( | t | serde_json ::from_str ( & t ) . ok ( ) ) . unwrap_or_default ( ) ;
// Once: bring in logins AnyBot saved before the list existed.
let marker = index_path ( ) . with_file_name ( " vault-index-imported " ) ;
if cfg! ( target_os = " macos " ) & & ! marker . exists ( ) {
for l in legacy_keychain_logins ( ) {
if ! list . contains ( & l ) {
list . push ( l ) ;
}
}
write_index ( & list ) ;
let _ = std ::fs ::write ( & marker , " 1 " ) ;
}
Some ( list )
}
fn write_index ( list : & [ ( String , String ) ] ) {
let _ = std ::fs ::write ( index_path ( ) , serde_json ::to_string_pretty ( list ) . unwrap_or_default ( ) ) ;
}
/// (Kept for the live tests and older callers.)
#[ cfg_attr(not(test), allow(dead_code)) ]
fn index_add ( site : & str , account : & str ) {
let mut list = read_index ( ) . unwrap_or_default ( ) ;
if ! list . iter ( ) . any ( | ( s , a ) | s = = site & & a = = account ) {
list . push ( ( site . to_string ( ) , account . to_string ( ) ) ) ;
write_index ( & list ) ;
}
}
/// The keychain has this login (no password is read, so no prompt).
fn keychain_has ( site : & str , account : & str ) -> bool {
Command ::new ( " security " ) . args ( [ " find-internet-password " , " -s " , site , " -a " , account ] ) . output ( ) . map ( | o | o . status . success ( ) ) . unwrap_or ( false )
}
/// Logins saved before the index existed: keychain items with AnyBot's label, which
/// `security dump-keychain` prints either as text or as hex bytes.
fn legacy_keychain_logins ( ) -> Vec < ( String , String ) > {
let Ok ( out ) = Command ::new ( " security " ) . args ( [ " dump-keychain " ] ) . output ( ) else { return vec! [ ] } ;
let text = String ::from_utf8_lossy ( & out . stdout ) ;
let hex : String = LABEL . bytes ( ) . map ( | b | format! ( " {b:02X} " ) ) . collect ( ) ;
let mut v = vec! [ ] ;
for block in text . split ( " keychain: " ) {
if ! block . contains ( & format! ( " = \" {LABEL} \" " ) ) & & ! block . to_uppercase ( ) . contains ( & format! ( " 0X {hex} " ) ) {
continue ;
}
let get = | k : & str | block . lines ( ) . find ( | l | l . contains ( & format! ( " \" {k} \" " ) ) ) . and_then ( | l | l . split ( " = \" " ) . nth ( 1 ) ) . map ( | s | s . trim_end_matches ( '"' ) . to_string ( ) ) . unwrap_or_default ( ) ;
let ( s , a ) = ( get ( " srvr " ) , get ( " acct " ) ) ;
if ! s . is_empty ( ) {
v . push ( ( s , a ) ) ;
}
}
v
}
/// Sites AnyBot saved (names only).
pub fn saved_sites ( ) -> Vec < ( String , String ) > {
if ! cfg! ( target_os = " macos " ) {
let Some ( o ) = secret_tool ( & [ " search " , " --all " , " application " , " anybot " ] , None ) else { return vec! [ ] } ;
let text = format! ( " {} \n {} " , String ::from_utf8_lossy ( & o . stdout ) , String ::from_utf8_lossy ( & o . stderr ) ) ;
return parse_secret_search ( & text ) . into_iter ( ) . filter_map ( | ( a , _ ) | Some ( ( a . get ( " server " ) ? . clone ( ) , a . get ( " user " ) . cloned ( ) . unwrap_or_default ( ) ) ) ) . collect ( ) ;
}
let list = read_index ( ) . unwrap_or_default ( ) ;
// Drop entries someone deleted in 鑰匙圈存取 meanwhile.
let kept : Vec < _ > = list . iter ( ) . filter ( | ( s , a ) | keychain_has ( s , a ) ) . cloned ( ) . collect ( ) ;
if kept . len ( ) ! = list . len ( ) {
write_index ( & kept ) ;
}
kept
}
/// (Kept for the live tests and older callers.)
#[ cfg_attr(not(test), allow(dead_code)) ]
pub fn remove ( site : & str , account : & str ) {
if ! cfg! ( target_os = " macos " ) {
let _ = secret_tool ( & [ " clear " , " application " , " anybot " , " server " , site , " user " , account ] , None ) ;
return ;
}
let _ = Command ::new ( " security " ) . args ( [ " delete-internet-password " , " -s " , site , " -a " , account ] ) . output ( ) ;
if let Some ( mut list ) = read_index ( ) {
list . retain ( | ( s , a ) | ! ( s = = site & & a = = account ) ) ;
write_index ( & list ) ;
}
}
/// The JavaScript that types the login into the page (as a JSON-encoded string literal).
pub fn fill_script ( username : & str , password : & str , submit : bool ) -> String {
let u = serde_json ::to_string ( username ) . unwrap_or_default ( ) ;
let p = serde_json ::to_string ( password ) . unwrap_or_default ( ) ;
format! (
r #" (() => {{
const set = ( el , v ) = > { { const d = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , ' value ' ) ; d . set . call ( el , v ) ; el . dispatchEvent ( new Event ( ' input ' , { { bubbles : true } } ) ) ; el . dispatchEvent ( new Event ( ' change ' , { { bubbles : true } } ) ) ; } } ;
const vis = el = > el . offsetParent ! = = null & & ! el . disabled ;
const pw = [ .. . document . querySelectorAll ( ' input [ type = password ] ' ) ] . find ( vis ) ;
const users = [ .. . document . querySelectorAll ( ' input [ type = email ] , input [ type = text ] , input [ autocomplete = username ] , input [ name * = user i ] , input [ name * = mail i ] , input [ id * = user i ] , input [ id * = mail i ] ' ) ] . filter ( vis ) ;
let user = null ;
if ( pw ) { { for ( const el of users ) { { if ( el . compareDocumentPosition ( pw ) & Node . DOCUMENT_POSITION_FOLLOWING ) user = el ; } } } } else { { user = users [ 0 ] | | null ; } }
if ( user & & { u } ) set ( user , { u } ) ;
if ( pw ) set ( pw , { p } ) ;
let submitted = false ;
if ( { submit } & & ( pw | | user ) ) { {
const field = pw | | user ;
const f = field . form ;
// The form's own button; else (pages without a <form>, like many shops) the nearest visible
// button that says 登入 / Log in / Sign in / 繼續 / 下一步.
let b = f ? ( f . querySelector ( ' button [ type = submit ] , input [ type = submit ] ' ) | | f . querySelector ( ' button ' ) ) : null ;
if ( ! b ) { {
const words = / 登 入 | 登 录 | log ? in | sign ? in | 繼 續 | 继 续 | 下 一 步 | next | continue / i ;
const cands = [ .. . document . querySelectorAll ( ' button , input [ type = submit ] , [ role = button ] , a ' ) ] . filter ( vis ) . filter ( el = > words . test ( ( el . innerText | | el . value | | el . getAttribute ( ' aria - label ' ) | | ' ' ) . trim ( ) ) ) ;
const fr = field . getBoundingClientRect ( ) ;
cands . sort ( ( a , c ) = > { { const ra = a . getBoundingClientRect ( ) , rc = c . getBoundingClientRect ( ) ; return ( Math . abs ( ra . top - fr . top ) + Math . abs ( ra . left - fr . left ) ) - ( Math . abs ( rc . top - fr . top ) + Math . abs ( rc . left - fr . left ) ) ; } } ) ;
b = cands [ 0 ] | | null ;
} }
if ( b & & ! b . disabled ) { { b . click ( ) ; submitted = true ; } }
else if ( f ) { { f . requestSubmit ? f . requestSubmit ( ) : f . submit ( ) ; submitted = true ; } }
} }
return JSON . stringify ( { { user : ! ! user , password : ! ! pw , submitted } } ) ;
} } ) ( ) " #
)
}
fn args_of ( args : & str ) -> Value {
serde_json ::from_str ( if args . trim ( ) . is_empty ( ) { " {} " } else { args } ) . unwrap_or_default ( )
}
/// The current tab's host in AnyBot's browser.
fn current_host ( ) -> Result < String , String > {
let ( url , _ ) = crate ::botbrowser ::eval_current ( None , " 1 " ) ? ;
Ok ( host_of ( & url ) )
}
/// What browser_fill_login will use, worked out before asking the person.
pub struct LoginPlan {
pub host : String ,
/// A vault login (public fields only).
pub item : Option < crate ::vault_store ::Item > ,
/// The vault login is saved for this site (else the person is warned before allowing it).
pub matched : bool ,
/// A Keychain / 1Password login (when the vault has none).
other : Option < Login > ,
}
impl LoginPlan {
/// The approval question.
pub fn question ( & self ) -> String {
match & self . item {
Some ( i ) if self . matched = > format! ( " 用保險庫的登入資訊( {} {} )登入 {} ( Bot 看不到密碼)" , i . display_title ( ) , if i . username . is_empty ( ) { String ::new ( ) } else { format! ( " · {} " , i . username ) } , self . host ) ,
Some ( i ) = > format! (
" 也要在 {} 上使用 {} 嗎?(已為 {} 儲存) \n 只有在它與已儲存的網站屬於同一服務時才允許。仿冒網站可能會竊取你的密碼。 " ,
self . host ,
i . display_title ( ) ,
i . sites . join ( " 、 " )
) ,
None = > format! ( " 用 {} 的登入資料( {} )登入 {} ( Bot 看不到密碼)" , self . other . as_ref ( ) . map ( | l | l . source ) . unwrap_or ( " 已儲存 " ) , self . other . as_ref ( ) . map ( | l | l . username . as_str ( ) ) . unwrap_or ( " " ) , self . host ) ,
}
}
}
pub fn plan_login ( args : & str ) -> Result < LoginPlan , String > {
let v = args_of ( args ) ;
let tab = current_host ( ) ? ;
let host = v [ " site " ] . as_str ( ) . filter ( | s | ! s . trim ( ) . is_empty ( ) ) . map ( host_of ) . filter ( | h | ! h . is_empty ( ) ) . unwrap_or ( tab ) ;
if let Some ( id ) = v [ " login_id " ] . as_str ( ) . filter ( | s | ! s . is_empty ( ) ) {
let item = crate ::vault_store ::get ( id ) . filter ( | i | i . kind = = crate ::vault_store ::Kind ::Login ) . ok_or ( " 保險庫裡沒有這筆登入資訊(先用 vault_list 看 id) " ) ? ;
let matched = item . fills_on ( & host ) ;
return Ok ( LoginPlan { host , item : Some ( item ) , matched , other : None } ) ;
}
if let Some ( item ) = crate ::vault_store ::logins_for ( & host ) . into_iter ( ) . next ( ) {
return Ok ( LoginPlan { host , item : Some ( item ) , matched : true , other : None } ) ;
}
match find ( & host ) {
Some ( l ) = > Ok ( LoginPlan { host , item : None , matched : true , other : Some ( l ) } ) ,
None = > Err ( format! ( " 找不到「 {host} 」的登入資料。請使用者在「設定 → 保險庫」新增一筆登入資訊(或存在 macOS 鑰匙圈/ 1Password) 。 " ) ) ,
}
}
/// Type the planned login into the page (after the person allowed it).
pub fn fill_login ( plan : & LoginPlan , submit : bool ) -> Result < String , String > {
let ( username , password , source ) = match ( & plan . item , & plan . other ) {
( Some ( i ) , _ ) = > {
let ( u , p ) = crate ::vault_store ::login_secret ( & i . id ) . ok_or ( " 保險庫裡沒有這筆登入資訊 " ) ? ;
( u , p , " 保險庫 " )
}
( None , Some ( l ) ) = > ( l . username . clone ( ) , l . password . clone ( ) , l . source ) ,
_ = > return Err ( " 沒有可用的登入資料 " . into ( ) ) ,
} ;
let ( _ , r ) = crate ::botbrowser ::eval_current ( None , & fill_script ( & username , & password , submit ) ) ? ;
let got : Value = r . as_str ( ) . and_then ( | s | serde_json ::from_str ( s ) . ok ( ) ) . unwrap_or_default ( ) ;
if got [ " password " ] . as_bool ( ) ! = Some ( true ) & & got [ " user " ] . as_bool ( ) ! = Some ( true ) {
return Err ( " 這一頁找不到帳號或密碼欄位。先打開登入頁(有些網站要先按「登入」或先填帳號、按下一步才出現密碼欄)。 " . into ( ) ) ;
}
2026-11-05 07:48:20 +00:00
// Remember the domain only after the fill worked: adding it on a failed (or lookalike)
// page made the next visit show matched:true and skip the anti-phishing warning.
if ! plan . matched {
if let Some ( i ) = & plan . item {
crate ::vault_store ::add_site ( & i . id , & plan . host ) ? ;
}
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
Ok ( format! (
" 已在 {} 填入{}{}(來自{source},你看不到密碼)。{} " ,
plan . host ,
if got [ " user " ] . as_bool ( ) = = Some ( true ) { format! ( " 帳號 {username} " ) } else { String ::new ( ) } ,
if got [ " password " ] . as_bool ( ) = = Some ( true ) { " 和密碼 " } else { " (這頁沒有密碼欄,可能要先按下一步) " } ,
if submit & & got [ " submitted " ] . as_bool ( ) = = Some ( true ) {
" 也已按下登入,請用 browser_read 確認是否登入成功。如果還在登入頁(常見原因:需要驗證碼、簡訊碼或人機驗證),請使用者在 AnyBot 瀏覽器視窗裡完成,之後會記住登入。 "
} else if submit {
" 但頁面上找不到登入按鈕,沒有送出。請用 computer_use 按登入按鈕,或請使用者按。 "
} else {
" 接著按頁面上的登入按鈕。 "
}
) )
}
/// JavaScript that fills a form's fields by their autocomplete hints and common names.
/// `values` maps a field role to its value; the page gets them as one JSON string literal.
pub fn form_fill_script ( values : & Value ) -> String {
let data = serde_json ::to_string ( & values . to_string ( ) ) . unwrap_or_default ( ) ;
format! (
r #" (() => {{
const V = JSON . parse ( { data } ) ;
const vis = el = > el . offsetParent ! = = null & & ! el . disabled & & ! el . readOnly ;
const set = ( el , v ) = > { {
if ( el . tagName = = = ' SELECT ' ) { {
const want = String ( v ) . toLowerCase ( ) ;
const opt = [ .. . el . options ] . find ( o = > o . value . toLowerCase ( ) = = = want | | o . text . trim ( ) . toLowerCase ( ) = = = want | | ( want . length > 1 & & o . text . trim ( ) . toLowerCase ( ) . startsWith ( want ) ) ) ;
if ( ! opt ) return false ;
el . value = opt . value ;
} } else { {
const proto = el . tagName = = = ' TEXTAREA ' ? HTMLTextAreaElement . prototype : HTMLInputElement . prototype ;
Object . getOwnPropertyDescriptor ( proto , ' value ' ) . set . call ( el , String ( v ) ) ;
} }
el . dispatchEvent ( new Event ( ' input ' , { { bubbles : true } } ) ) ;
el . dispatchEvent ( new Event ( ' change ' , { { bubbles : true } } ) ) ;
return true ;
} } ;
const rules = { {
' cc - number ' : [ / card . ? num | cc . ? num | cardnumber | ccnumber | 卡 號 / i ] ,
' cc - exp ' : [ / ^ ( cc - ? ) ? exp ( iry | iration ) ? ( - ? date ) ? $ | mm . ? \ / ? . ? yy | 有 效 期 / i ] ,
' cc - exp - month ' : [ / exp . * month | cc - ? month | card . ? month | 月 / i ] ,
' cc - exp - year ' : [ / exp . * year | cc - ? year | card . ? year | 年 / i ] ,
' cc - csc ' : [ / cvc | cvv | csc | security . ? code | card . ? code | 安 全 碼 | 驗 證 碼 / i ] ,
' cc - name ' : [ / name . ? on . ? card | card . ? holder | holder . ? name | cc - ? name | 持 卡 人 / i ] ,
' name ' : [ / ^ ( full . ? ) ? name $ | 姓 名 | 收 件 人 / i ] ,
' address - line1 ' : [ / address . ? ( line ) ? . ? 1 | street | ^ address $ | 地 址 / i ] ,
' address - line2 ' : [ / address . ? ( line ) ? . ? 2 | apt | suite | unit / i ] ,
' address - level2 ' : [ / city | town | 城 市 | 縣 市 / i ] ,
' address - level1 ' : [ / state | province | region | county | 州 | 省 / i ] ,
' postal - code ' : [ / zip | postal | postcode | 郵 遞 區 號 | 郵 遞 / i ] ,
' country ' : [ / country | 國 家 / i ] ,
' tel ' : [ / phone | mobile | tel | 手 機 | 電 話 / i ] ,
' email ' : [ / e - ? mail | 電 子 郵 件 / i ] ,
} } ;
const fields = [ .. . document . querySelectorAll ( ' input :not ( [ type = hidden ] ) :not ( [ type = checkbox ] ) :not ( [ type = radio ] ) :not ( [ type = password ] ) , select , textarea ' ) ] . filter ( vis ) ;
const used = new Set ( ) ;
const filled = [ ] ;
for ( const role of Object . keys ( V ) ) { {
if ( V [ role ] = = = ' ' | | V [ role ] = = null ) continue ;
let el = fields . find ( f = > ! used . has ( f ) & & ( f . autocomplete | | ' ' ) . toLowerCase ( ) . split ( / \ s + / ) . includes ( role ) ) ;
if ( ! el ) { {
const pats = rules [ role ] | | [ ] ;
el = fields . find ( f = > { { const ac = ( f . autocomplete | | ' ' ) . toLowerCase ( ) ; return ! used . has ( f ) & & ( ac = = = ' ' | | ac = = = ' on ' | | ac = = = ' off ' | | ac . startsWith ( ' cc - ' ) = = = role . startsWith ( ' cc - ' ) ) & & pats . some ( p = > p . test ( [ f . type = = = ' text ' ? ' ' : f . type , f . name , f . id , f . placeholder , f . getAttribute ( ' aria - label ' ) | | ' ' , ( f . labels & & f . labels [ 0 ] ? f . labels [ 0 ] . innerText : ' ' ) ] . join ( ' ' ) ) ) ; } } ) ;
} }
if ( el & & set ( el , V [ role ] ) ) { { used . add ( el ) ; filled . push ( role ) ; } }
} }
const frames = document . querySelectorAll ( ' iframe ' ) . length ;
return JSON . stringify ( { { filled , frames } } ) ;
} } ) ( ) " #
)
}
/// Names for the roles, as the bot is told what was filled.
fn role_name ( r : & str ) -> & 'static str {
match r {
" cc-number " = > " 卡號 " ,
" cc-exp " | " cc-exp-month " | " cc-exp-year " = > " 有效期限 " ,
" cc-csc " = > " 安全碼 " ,
" cc-name " = > " 持卡人姓名 " ,
" name " = > " 姓名 " ,
" address-line1 " | " address-line2 " = > " 地址 " ,
" address-level2 " = > " 城市 " ,
" address-level1 " = > " 州/省/地區 " ,
" postal-code " = > " 郵遞區號 " ,
" country " = > " 國家/地區 " ,
" tel " = > " 電話 " ,
" email " = > " 電子郵件 " ,
_ = > " 欄位 " ,
}
}
fn filled_summary ( r : & Value ) -> ( Vec < & 'static str > , u64 ) {
let mut names : Vec < & 'static str > = r [ " filled " ] . as_array ( ) . into_iter ( ) . flatten ( ) . filter_map ( | x | x . as_str ( ) ) . map ( role_name ) . collect ( ) ;
names . dedup ( ) ;
( names , r [ " frames " ] . as_u64 ( ) . unwrap_or ( 0 ) )
}
/// For the approval question: (current host, card label) without secrets.
pub fn plan_card ( args : & str ) -> Result < ( String , crate ::vault_store ::Item ) , String > {
let id = args_of ( args ) [ " card_id " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let card = crate ::vault_store ::get ( & id ) . filter ( | i | i . kind = = crate ::vault_store ::Kind ::Card ) . ok_or ( " 保險庫裡沒有這張卡片(先用 vault_list 看 id) " ) ? ;
Ok ( ( current_host ( ) ? , card ) )
}
pub fn fill_card ( args : & str ) -> Result < String , String > {
let id = args_of ( args ) [ " card_id " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let c = crate ::vault_store ::card_secret ( & id ) . ok_or ( " 保險庫裡沒有這張卡片 " ) ? ;
let values = serde_json ::json! ( {
" cc-number " : c . number ,
" cc-exp " : format ! ( " {:02}/{:02} " , c . exp_month , c . exp_year % 100 ) ,
" cc-exp-month " : format ! ( " {:02} " , c . exp_month ) ,
" cc-exp-year " : c . exp_year . to_string ( ) ,
" cc-csc " : c . csc ,
" cc-name " : c . holder ,
} ) ;
let ( _ , r ) = crate ::botbrowser ::eval_current ( None , & form_fill_script ( & values ) ) ? ;
let got : Value = r . as_str ( ) . and_then ( | s | serde_json ::from_str ( s ) . ok ( ) ) . unwrap_or_default ( ) ;
let ( names , frames ) = filled_summary ( & got ) ;
if names . is_empty ( ) {
return Err ( if frames > 0 {
" 這一頁的付款欄位在付款服務的框架裡, AnyBot 填不進去。請使用者自己填卡號,或改用網站提供的其他付款方式。 " . into ( )
} else {
" 這一頁找不到付款欄位。先打開結帳頁的付款步驟。 " . into ( )
} ) ;
}
Ok ( format! ( " 已填入 {} •••• {} 的 {} (你看不到卡號和安全碼)。送出付款前一定要先問使用者。 " , c . display_title ( ) , c . last4 , names . join ( " 、 " ) ) )
}
pub fn plan_contact ( args : & str ) -> Result < ( String , crate ::vault_store ::Item ) , String > {
let id = args_of ( args ) [ " contact_id " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let c = crate ::vault_store ::get ( & id ) . filter ( | i | i . kind = = crate ::vault_store ::Kind ::Contact ) . ok_or ( " 保險庫裡沒有這筆聯絡資訊(先用 vault_list 看 id) " ) ? ;
Ok ( ( current_host ( ) ? , c ) )
}
pub fn fill_contact ( args : & str ) -> Result < String , String > {
let ( _ , c ) = plan_contact ( args ) ? ;
let values = serde_json ::json! ( {
" name " : c . name , " address-line1 " : c . address . line1 , " address-line2 " : c . address . line2 , " address-level2 " : c . address . city ,
" address-level1 " : c . address . region , " postal-code " : c . address . postal , " country " : c . address . country ,
" tel " : c . phones . first ( ) . cloned ( ) . unwrap_or_default ( ) , " email " : c . emails . first ( ) . cloned ( ) . unwrap_or_default ( ) ,
} ) ;
let ( _ , r ) = crate ::botbrowser ::eval_current ( None , & form_fill_script ( & values ) ) ? ;
let got : Value = r . as_str ( ) . and_then ( | s | serde_json ::from_str ( s ) . ok ( ) ) . unwrap_or_default ( ) ;
let ( names , _ ) = filled_summary ( & got ) ;
if names . is_empty ( ) {
return Err ( " 這一頁找不到地址或聯絡欄位。 " . into ( ) ) ;
}
Ok ( format! ( " 已填入「 {} 」的 {} 。請用 browser_read 確認。 " , c . display_title ( ) , names . join ( " 、 " ) ) )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn hosts_and_candidates ( ) {
assert_eq! ( host_of ( " https://accounts.Google.com/signin?x=1 " ) , " accounts.google.com " ) ;
assert_eq! ( host_of ( " https://me:pw@example.com:8443/a " ) , " example.com " ) ;
assert_eq! ( candidates ( " a.b.example.com " ) , vec! [ " a.b.example.com " , " b.example.com " , " example.com " ] ) ;
let found = parse_secret_search ( " [/org/freedesktop/secrets/collection/login/7] \n label = AnyBot 登入: x.com \n secret = hunter2 \n created = 2026-11-04 \n attribute.application = anybot \n attribute.server = x.com \n attribute.user = me \n [/org/freedesktop/secrets/collection/login/8] \n secret = pw2 \n attribute.server = y.com \n " ) ;
assert_eq! ( found . len ( ) , 2 ) ;
assert_eq! ( found [ 0 ] . 1 , " hunter2 " ) ;
assert_eq! ( found [ 0 ] . 0 [ " user " ] , " me " ) ;
assert_eq! ( found [ 1 ] . 0 [ " server " ] , " y.com " ) ;
}
#[ test ]
fn keychain_password_lines ( ) {
assert_eq! ( parse_g_password ( " password: \" hunter2 \" \n " ) . unwrap ( ) , " hunter2 " ) ;
assert_eq! ( parse_g_password ( " password: \" a \" b \" \n " ) . unwrap ( ) , " a \" b " ) ;
assert_eq! ( parse_g_password ( " password: 0x706122737320E4BDA0E5A5BD \" pa \\ \" ss \\ 344 \\ 275 \\ 240 \\ 345 \\ 245 \\ 275 \" \n " ) . unwrap ( ) , " pa \" ss 你好 " ) ;
assert_eq! ( parse_g_password ( " password: \n " ) . unwrap ( ) , " " ) ;
assert! ( parse_g_password ( " nothing " ) . is_none ( ) ) ;
}
#[ test ]
fn the_script_keeps_the_password_in_a_string ( ) {
let js = fill_script ( " me@x.com " , " p \" w'</script> " , true ) ;
assert! ( js . contains ( r # ""p\"w'</script>""# ) ) ;
assert! ( js . contains ( " input[type=password] " ) & & js . contains ( " if (true && (pw || user)) " ) ) ;
}
/// Needs a keyring: on Linux run under `dbus-run-session` with gnome-keyring unlocked.
#[ test ]
#[ ignore ]
fn keyring_live ( ) {
save ( " https://login.example.org/x " , " me@example.org " , " s3cret pass " ) . unwrap ( ) ;
let l = find ( " login.example.org " ) . expect ( " found " ) ;
assert_eq! ( ( l . username . as_str ( ) , l . password . as_str ( ) ) , ( " me@example.org " , " s3cret pass " ) ) ;
assert! ( saved_sites ( ) . iter ( ) . any ( | ( s , a ) | s = = " login.example.org " & & a = = " me@example.org " ) ) ;
// Logins from before the list existed (label printed as hex by `security`) show up too.
if cfg! ( target_os = " macos " ) {
assert! ( saved_sites ( ) . iter ( ) . any ( | ( s , _ ) | s = = " legacy.example.org " ) , " legacy entry: {:?} " , saved_sites ( ) ) ;
}
remove ( " login.example.org " , " me@example.org " ) ;
assert! ( find ( " login.example.org " ) . is_none ( ) ) ;
}
/// The whole bot-side flow on this computer: local pages in AnyBot's browser (a throwaway
/// profile), items in a scratch vault, and the fill tools. Run with ANYBOT_DATA_DIR set to a
/// scratch folder; opens a browser window for a moment.
#[ test ]
#[ ignore ]
fn fill_live ( ) {
use crate ::vault_store as store ;
use std ::io ::{ Read , Write } ;
let dir = crate ::model ::State ::path ( ) . parent ( ) . unwrap ( ) . to_path_buf ( ) ;
std ::env ::set_var ( " ANYBOT_VAULT_DIR " , & dir ) ;
std ::env ::set_var ( " ANYBOT_VAULT_KEY_FILE " , dir . join ( " vault-test.key " ) ) ;
let listener = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = listener . local_addr ( ) . unwrap ( ) . port ( ) ;
std ::thread ::spawn ( move | | {
for mut c in listener . incoming ( ) . flatten ( ) {
let mut buf = [ 0 u8 ; 2048 ] ;
let n = c . read ( & mut buf ) . unwrap_or ( 0 ) ;
let req = String ::from_utf8_lossy ( & buf [ .. n ] ) ;
let page = if req . starts_with ( " GET /checkout " ) {
" <html><body><form><input autocomplete=name id=n><input name=street id=a1><input placeholder=City id=c><input name=zip id=z><input type=tel id=t><input id=cc name=cardnumber><input id=ex placeholder='MM / YY'><input id=cv name=cvc><input id=hn placeholder='Name on card'></form></body></html> "
} else {
" <html><body><form><input type=email name=email id=e><input type=password id=p><button type=submit>Sign in</button></form></body></html> "
} ;
let _ = write! ( c , " HTTP/1.1 200 OK \r \n Content-Type: text/html; charset=utf-8 \r \n Content-Length: {} \r \n Connection: close \r \n \r \n {page} " , page . len ( ) ) ;
}
} ) ;
crate ::botbrowser ::add ( " vault-test " ) . unwrap ( ) ;
let run = | | -> Result < ( ) , String > {
let login = store ::put_login ( store ::LoginInput { id : None , sites : vec ! [ " 127.0.0.1 " . into ( ) ] , username : " tester@example.org " . into ( ) , password : " pa \" ss 你好 " . into ( ) , notes : String ::new ( ) } ) ? ;
let card = store ::put_card ( store ::CardInput { id : None , number : " 4111111111111111 " . into ( ) , expiry : " 08/39 " . into ( ) , csc : " 123 " . into ( ) , holder : " WANG XIAOMING " . into ( ) , nickname : " 測試 " . into ( ) , billing : store ::Address ::default ( ) , notes : String ::new ( ) } ) ? ;
let contact = store ::put_contact ( store ::ContactInput { id : None , label : " 住家 " . into ( ) , name : " 王小明 " . into ( ) , address : store ::Address { line1 : " 信義路五段 7 號 " . into ( ) , city : " 台北市 " . into ( ) , postal : " 110 " . into ( ) , .. Default ::default ( ) } , phones : vec ! [ " 0912345678 " . into ( ) ] , emails : vec ! [ ] , notes : String ::new ( ) } ) ? ;
// What the bot sees has no secrets.
let seen = list_for_bot ( " {} " ) ;
assert! ( seen . contains ( " tester@example.org " ) & & seen . contains ( " 1111 " ) & & ! seen . contains ( " 4111111111111111 " ) & & ! seen . contains ( " pa \\ \" ss " ) & & ! seen . contains ( " 123 \" " ) , " {seen} " ) ;
crate ::botbrowser ::open ( & format! ( " http://127.0.0.1: {port} /login " ) , Some ( " vault-test " ) ) ? ;
let plan = plan_login ( " {} " ) ? ;
assert! ( plan . matched & & plan . item . as_ref ( ) . map ( | i | i . id = = login ) . unwrap_or ( false ) ) ;
let said = fill_login ( & plan , false ) ? ;
assert! ( ! said . contains ( " pa \" ss " ) , " {said} " ) ;
let ( _ , got ) = crate ::botbrowser ::eval_current ( None , " JSON.stringify([e.value, p.value]) " ) ? ;
assert_eq! ( got . as_str ( ) . unwrap ( ) , r # "["tester@example.org","pa\"ss 你好"]"# ) ;
crate ::botbrowser ::open ( & format! ( " http://127.0.0.1: {port} /checkout " ) , Some ( " vault-test " ) ) ? ;
let said = fill_card ( & format! ( " {{ \" card_id \" : \" {card} \" }} " ) ) ? ;
assert! ( ! said . contains ( " 4111111111111111 " ) & & ! said . contains ( " 123 " ) , " {said} " ) ;
fill_contact ( & format! ( " {{ \" contact_id \" : \" {contact} \" }} " ) ) ? ;
let ( _ , got ) = crate ::botbrowser ::eval_current ( None , " JSON.stringify([n.value,a1.value,c.value,z.value,t.value,cc.value,ex.value,cv.value,hn.value]) " ) ? ;
assert_eq! ( got . as_str ( ) . unwrap ( ) , r # "["王小明","信義路五段 7 號","台北市","110","0912345678","4111111111111111","08/39","123","WANG XIAOMING"]"# ) ;
Ok ( ( ) )
} ;
let r = run ( ) ;
let _ = crate ::botbrowser ::remove ( " vault-test " ) ;
let _ = std ::fs ::remove_file ( dir . join ( " vault-test.key " ) ) ;
let _ = std ::fs ::remove_file ( store ::file_path ( ) ) ;
r . unwrap ( ) ;
}
}