基于 大佬“空” 对Chrome.ahk优化过的版本修改而来。
因为现在的 Win10、Win11基本上都以及内置了Edge浏览器,所以就进行了一下适配。
源码地址:Edge.ahk
源码:
; Edge.ahk v1.0
; Copyright GeekDude 2021
; https://github.com/G33kDude/Chrome.ahk
; 基于 https://www.autoahk.com/ 一位大佬优化过的 Chrome.ahk 修改而来。
class Edge
{
static DebugPort := 9222
/*
Escape a string in a manner suitable for command line parameters
*/
CliEscape(Param)
{
return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
}
/*
Finds instances of msedge in debug mode and the ports they're running
on. If no instances are found, returns a false value. If one or more
instances are found, returns an associative array where the keys are
the ports, and the values are the full command line texts used to start
the processes.
One example of how this may be used would be to open msedge on a
different port if an instance of msedge is already open on the port
you wanted to used.
```
; If the wanted port is taken, use the largest taken port plus one
DebugPort := 9222
if (Edges := Edge.FindInstances()).HasKey(DebugPort)
DebugPort := Edges.MaxIndex() + 1
EdgeInst := new Edge(ProfilePath,,,, DebugPort)
```
Another use would be to scan for running instances and attach to one
instead of starting a new instance.
```
if (Edges := Edge.FindInstances())
EdgeInst := {"base": Edge, "DebugPort": Edges.MinIndex()}
else
EdgeInst := new Edge(ProfilePath)
```
*/
FindInstances()
{
static Needle := "--remote-debugging-port=(\d+)"
Out := {}
for Item in ComObjGet("winmgmts:")
.ExecQuery("SELECT CommandLine FROM Win32_Process"
. " WHERE Name = 'msedge.exe'")
if RegExMatch(Item.CommandLine, Needle, Match)
Out[Match1] := Item.CommandLine
return Out.MaxIndex() ? Out : False
}
/*
ProfilePath - Path to the user profile directory to use. Will use the standard if left blank.
URLs - The page or array of pages for Edge to load when it opens
Flags - Additional flags for msedge when launching
EdgePath - Path to msedge.exe, will detect from start menu when left blank
DebugPort - What port should Edge's remote debugging server run on
*/
__New(ProfilePath:="EdgeProfile", URLs:="about:blank", Flags:="", EdgePath:="", DebugPort:="")
{
if (ProfilePath == "")
throw Exception("Need a profile directory")
; Verify ProfilePath
if (!InStr(FileExist(ProfilePath), "D"))
{
FileCreateDir, %ProfilePath%
if (ErrorLevel = 1)
throw Exception("Failed to create the profile directory")
}
this.ProfilePath := ProfilePath
; Verify EdgePath
if (EdgePath == "")
; By using winmgmts to get the path of a shortcut file we fix an edge case where the path is retreived incorrectly
; if using the ahk executable with a different architecture than the OS (using 32bit AHK on a 64bit OS for example)
try EdgePath := ComObjGet("winmgmts:").ExecQuery("Select * from Win32_ShortcutFile where Name=""" StrReplace(A_StartMenuCommon "\Programs\Microsoft Edge.lnk", "\", "\\") """").ItemIndex(0).Target
; FileGetShortcut, %A_StartMenuCommon%\Programs\Microsoft Edge.lnk, EdgePath
if (EdgePath == "")
RegRead, EdgePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe
if !FileExist(EdgePath)
throw Exception("Edge could not be found")
this.EdgePath := EdgePath
; Verify DebugPort
if (DebugPort != "")
{
if DebugPort is not integer
throw Exception("DebugPort must be a positive integer")
else if (DebugPort <= 0)
throw Exception("DebugPort must be a positive integer")
this.DebugPort := DebugPort
}
; Escape the URL(s)
for Index, URL in IsObject(URLs) ? URLs : [URLs]
URLString .= " " this.CliEscape(URL)
Run, % this.CliEscape(EdgePath)
. " --remote-debugging-port=" this.DebugPort
. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
. (Flags ? " " Flags : "")
. URLString
,,, OutputVarPID
this.PID := OutputVarPID
}
/*
End Edge by terminating the process.
*/
Kill()
{
Process, Close, % this.PID
}
/*
Queries msedge for a list of pages that expose a debug interface.
In addition to standard tabs, these include pages such as extension
configuration pages.
*/
GetPageList()
{
http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
StartTime := A_TickCount
while (A_TickCount-StartTime < 10*1000)
{
; It is easy to fail here because "new msedge()" takes a long time to execute.
; Therefore, it will be tried again and again within 10 seconds until it succeeds or timeout.
try
{
http.Open("GET", "http://127.0.0.1:" this.DebugPort "/json", true)
http.Send()
http.WaitForResponse(-1)
if (http.Status = 200)
break
}
Sleep, 50
}
return this.Jxon_Load(http.responseText)
}
/*
Returns a connection to the debug interface of a page that matches the
provided criteria. When multiple pages match the criteria, they appear
ordered by how recently the pages were opened.
Key - The key from the page list to search for, such as "url" or "title"
Value - The value to search for in the provided key
MatchMode - What kind of search to use, such as "exact", "contains", "startswith", or "regex"
Index - If multiple pages match the given criteria, which one of them to return
Timeout - Maximum number of seconds to wait for the page connection
fnCallback - A function to be called whenever message is received from the page
*/
GetPageBy(Key, Value, MatchMode:="exact", Index:=1, Timeout:=30, fnCallback:="", fnClose:="")
{
Count := 0
for n, PageData in this.GetPageList()
{
if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive
|| (MatchMode = "contains" && InStr(PageData[Key], Value))
|| (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1)
|| (MatchMode = "regex" && PageData[Key] ~= Value))
&& ++Count == Index)
return new this.Page(PageData.webSocketDebuggerUrl, Timeout, fnCallback, fnClose)
}
}
/*
Shorthand for GetPageBy("url", Value, "startswith")
*/
GetPageByURL(Value, MatchMode:="startswith", Index:=1, Timeout:=30, fnCallback:="", fnClose:="")
{
return this.GetPageBy("url", Value, MatchMode, Index, Timeout, fnCallback, fnClose)
}
/*
Shorthand for GetPageBy("title", Value, "startswith")
*/
GetPageByTitle(Value, MatchMode:="startswith", Index:=1, Timeout:=30, fnCallback:="", fnClose:="")
{
return this.GetPageBy("title", Value, MatchMode, Index, Timeout, fnCallback, fnClose)
}
/*
Shorthand for GetPageBy("type", Type, "exact")
The default type to search for is "page", which is the visible area of
a normal Edge tab.
*/
GetPage(Index:=1, Type:="page", Timeout:=30, fnCallback:="", fnClose:="")
{
return this.GetPageBy("type", Type, "exact", Index, Timeout, fnCallback, fnClose)
}
/*
Connects to the debug interface of a page given its WebSocket URL.
*/
class Page
{
Connected := False
ID := 0
Responses := []
/*
wsurl - The desired page's WebSocket URL
timeout - Maximum number of seconds to wait for the page connection
fnCallback - A function to be called whenever message is received
fnClose - A function to be called whenever the page connection is lost
*/
__New(wsurl, timeout:=30, fnCallback:="", fnClose:="")
{
this.fnCallback := fnCallback
this.fnClose := fnClose
; Here is no waiting for a response so no need to add a timeout
this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
; TODO: Throw exception on invalid objects
if IsObject(wsurl)
wsurl := wsurl.webSocketDebuggerUrl
wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
this.ws.__New(wsurl, timeout)
; The timeout here is perhaps duplicated with the previous line
StartTime := A_TickCount
while !this.Connected
{
if (A_TickCount-StartTime > timeout*1000)
throw Exception("Page connection timeout")
else
Sleep, 50
}
}
/*
Calls the specified endpoint and provides it with the given
parameters.
DomainAndMethod - The endpoint domain and method name for the
endpoint you would like to call. For example:
PageInst.Call("Browser.close")
PageInst.Call("Schema.getDomains")
Params - An associative array of parameters to be provided to the
endpoint. For example:
PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value
, "landscape": Edge.Jxon_True() ; Boolean Value
, "pageRanges: "1-5, 8, 11-13"}) ; String value
PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"})
WaitForResponse - Whether to block until a response is received from
Edge, which is necessary to receive a return value, or whether
to continue on with the script without waiting for a response.
Timeout - Maximum number of seconds to wait for a response.
*/
Call(DomainAndMethod, Params:="", WaitForResponse:=True, Timeout:=30)
{
if !this.Connected
throw Exception("Not connected to tab")
; Use a temporary variable for ID in case more calls are made
; before we receive a response.
ID := this.ID += 1
this.ws.Send(Edge.Jxon_Dump({"id": ID
, "params": Params ? Params : {}
, "method": DomainAndMethod}))
if !WaitForResponse
return
; Wait for the response
this.responses[ID] := False
StartTime := A_TickCount
while !this.responses[ID]
{
if (A_TickCount-StartTime > Timeout*1000)
throw Exception(DomainAndMethod " response timeout")
else
Sleep, 50
}
; Get the response, check if it's an error
response := this.responses.Delete(ID)
if (response.error)
throw Exception("Edge indicated error in response",, Edge.Jxon_Dump(response.error))
return response.result
}
/*
Run some JavaScript on the page. For example:
PageInst.Evaluate("alert(""I can't believe it's not IE!"");")
PageInst.Evaluate("document.getElementsByTagName('button')[0].click();")
*/
Evaluate(JS, Timeout:=30)
{
response := this.Call("Runtime.evaluate",
( LTrim Join
{
"expression": JS,
"objectGroup": "console",
"includeCommandLineAPI": Edge.Jxon_True(),
"silent": Edge.Jxon_False(),
"returnByValue": Edge.Jxon_False(),
"userGesture": Edge.Jxon_True(),
"awaitPromise": Edge.Jxon_False()
}
), Timeout)
if (response.exceptionDetails)
throw Exception(response.result.description, -1
, Edge.Jxon_Dump({"Code": JS
, "exceptionDetails": response.exceptionDetails}))
return response.result
}
/*
Waits for the page's readyState to match the DesiredState.
DesiredState - The state to wait for the page's ReadyState to match
Interval - How often it should check whether the state matches
Timeout - Maximum number of seconds to wait for the page's ReadyState to match
*/
WaitForLoad(DesiredState:="complete", Interval:=100, Timeout:=30)
{
StartTime := A_TickCount
while this.Evaluate("document.readyState").value != DesiredState
{
if (A_TickCount-StartTime > Timeout*1000)
throw Exception("Wait for page " DesiredState " timeout")
else
Sleep, Interval
}
}
/*
Internal function triggered when the script receives a message on
the WebSocket connected to the page.
*/
Event(EventName, Event)
{
; If it was called from the WebSocket adjust the class context
if this.Parent
this := this.Parent
; TODO: Handle Error events
if (EventName == "Open")
{
this.Connected := True
BoundKeepAlive := this.BoundKeepAlive
SetTimer, %BoundKeepAlive%, 15000
}
else if (EventName == "Message")
{
data := Edge.Jxon_Load(Event.data)
; Run the callback routine
fnCallback := this.fnCallback
if (newData := %fnCallback%(data))
data := newData
if this.responses.HasKey(data.ID)
this.responses[data.ID] := data
}
else if (EventName == "Close")
{
this.Disconnect()
fnClose := this.fnClose
%fnClose%(this)
}
}
/*
Disconnect from the page's debug interface, allowing the instance
to be garbage collected.
This method should always be called when you are finished with a
page or else your script will leak memory.
*/
Disconnect()
{
if !this.Connected
return
this.Connected := False
this.ws.Delete("Parent")
this.ws.Disconnect()
BoundKeepAlive := this.BoundKeepAlive
SetTimer, %BoundKeepAlive%, Delete
this.Delete("BoundKeepAlive")
}
class WebSocket
{
__New(WS_URL, Timeout:=30)
{
static wb
; Need IE10+
RegRead, OutputVar, HKLM, Software\Microsoft\Internet Explorer, svcVersion
if (StrSplit(OutputVar, ".")[1] < 10)
throw Exception("Connect to a WebSocket server need IE10+")
; Create an IE instance
Gui, +hWndhOld
Gui, New, +hWndhWnd
this.hWnd := hWnd
Gui, Add, ActiveX, vWB, Shell.Explorer
Gui, %hOld%: Default
; Write an appropriate document
WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
. "content='IE=edge'><body></body>")
StartTime := A_TickCount
while (WB.ReadyState < 4)
{
if (A_TickCount-StartTime > Timeout*1000)
throw Exception("Connect to a WebSocket server timeout")
else
Sleep, 50
}
this.document := WB.document
; Add our handlers to the JavaScript namespace
this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this)
this.document.parentWindow.ahk_event := this._Event.Bind(this)
this.document.parentWindow.ahk_ws_url := WS_URL
; Add some JavaScript to the page to open a socket
; Here is the JS code, no need to add a timeout
Script := this.document.createElement("script")
Script.text := "ws = new WebSocket(ahk_ws_url);`n"
. "ws.onopen = function(event){ ahk_event('Open', event); };`n"
. "ws.onclose = function(event){ ahk_event('Close', event); };`n"
. "ws.onerror = function(event){ ahk_event('Error', event); };`n"
. "ws.onmessage = function(event){ ahk_event('Message', event); };"
this.document.body.appendChild(Script)
}
; Called by the JS in response to WS events
_Event(EventName, Event)
{
this["On" EventName](Event)
}
; Sends data through the WebSocket
Send(Data)
{
this.document.parentWindow.ws.send(Data)
}
; Closes the WebSocket connection
Close(Code:=1000, Reason:="")
{
this.document.parentWindow.ws.close(Code, Reason)
}
; Closes and deletes the WebSocket, removing
; references so the class can be garbage collected
Disconnect()
{
if this.hWnd
{
this.Close()
Gui, % this.hWnd ": Destroy"
this.hWnd := False
}
}
}
}
Jxon_Load(ByRef src, args*)
{
static q := Chr(34)
key := "", is_key := false
stack := [ tree := [] ]
is_arr := { (tree): 1 }
next := q . "{[01234567890-tfn"
pos := 0
while ( (ch := SubStr(src, ++pos, 1)) != "" )
{
if InStr(" `t`n`r", ch)
continue
if !InStr(next, ch, true)
{
ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
msg := Format("{}: line {} col {} (char {})"
, (next == "") ? ["Extra data", ch := SubStr(src, pos)][1]
: (next == "'") ? "Unterminated string starting at"
: (next == "\") ? "Invalid \escape"
: (next == ":") ? "Expecting ':' delimiter"
: (next == q) ? "Expecting object key enclosed in double quotes"
: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
: (next == ",}") ? "Expecting ',' delimiter or object closing '}'"
: (next == ",]") ? "Expecting ',' delimiter or array closing ']'"
: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
, ln, col, pos)
throw Exception(msg, -1, ch)
}
is_array := is_arr[obj := stack[1]]
if i := InStr("{[", ch)
{
val := (proto := args[i]) ? new proto : {}
is_array? ObjPush(obj, val) : obj[key] := val
ObjInsertAt(stack, 1, val)
is_arr[val] := !(is_key := ch == "{")
next := q . (is_key ? "}" : "{[]0123456789-tfn")
}
else if InStr("}]", ch)
{
ObjRemoveAt(stack, 1)
next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
}
else if InStr(",:", ch)
{
is_key := (!is_array && ch == ",")
next := is_key ? q : q . "{[0123456789-tfn"
}
else ; string | number | true | false | null
{
if (ch == q) ; string
{
i := pos
while i := InStr(src, q,, i+1)
{
val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
static end := A_AhkVersion<"2" ? 0 : -1
if (SubStr(val, end) != "\")
break
}
if !i ? (pos--, next := "'") : 0
continue
pos := i ; update pos
val := StrReplace(val, "\/", "/")
, val := StrReplace(val, "\" . q, q)
, val := StrReplace(val, "\b", "`b")
, val := StrReplace(val, "\f", "`f")
, val := StrReplace(val, "\n", "`n")
, val := StrReplace(val, "\r", "`r")
, val := StrReplace(val, "\t", "`t")
i := 0
while i := InStr(val, "\",, i+1)
{
if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
continue 2
; \uXXXX - JSON unicode escape sequence
xxxx := Abs("0x" . SubStr(val, i+2, 4))
if (A_IsUnicode || xxxx < 0x100)
val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
}
if is_key
{
key := val, next := ":"
continue
}
}
else ; number | true | false | null
{
val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
; For numerical values, numerify integers and keep floats as is.
; I'm not yet sure if I should numerify floats in v2.0-a ...
static number := "number", integer := "integer"
if val is %number%
{
if val is %integer%
val += 0
}
; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
; SOMETIMES return strings due to certain optimizations. Since it
; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
else if (val == "true" || val == "false")
val := %value% + 0
; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
; as it would raise an exception in AHK_H(overriding built-in var)
else if (val == "null")
val := ""
; any other values are invalid, continue to trigger error
else if (pos--, next := "#")
continue
pos += i-1
}
is_array? ObjPush(obj, val) : obj[key] := val
next := obj==tree ? "" : is_array ? ",]" : ",}"
}
}
return tree[1]
}
Jxon_Dump(obj, indent:="", lvl:=1)
{
static q := Chr(34)
if IsObject(obj)
{
static Type := Func("Type")
if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
prefix := SubStr(A_ThisFunc, 1, InStr(A_ThisFunc, ".",, 0))
fn_t := prefix "Jxon_True", obj_t := this ? %fn_t%(this) : %fn_t%()
fn_f := prefix "Jxon_False", obj_f := this ? %fn_f%(this) : %fn_f%()
if (&obj == &obj_t)
return "true"
else if (&obj == &obj_f)
return "false"
is_array := 0
for k in obj
is_array := k == A_Index
until !is_array
static integer := "integer"
if indent is %integer%
{
if (indent < 0)
throw Exception("Indent parameter must be a postive integer.", -1, indent)
spaces := indent, indent := ""
Loop % spaces
indent .= " "
}
indt := ""
Loop, % indent ? lvl : 0
indt .= indent
this_fn := this ? Func(A_ThisFunc).Bind(this) : A_ThisFunc
lvl += 1, out := "" ; Make #Warn happy
for k, v in obj
{
if IsObject(k) || (k == "")
throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
if !is_array
out .= ( ObjGetCapacity([k], 1) ? %this_fn%(k) : q . k . q ) ;// key
. ( indent ? ": " : ":" ) ; token + padding
out .= %this_fn%(v, indent, lvl) ; value
. ( indent ? ",`n" . indt : "," ) ; token + indent
}
if (out != "")
{
out := Trim(out, ",`n" . indent)
if (indent != "")
out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
}
return is_array ? "[" . out . "]" : "{" . out . "}"
}
; Number
else if (ObjGetCapacity([obj], 1) == "")
return obj
; String (null -> not supported by AHK)
if (obj != "")
{
obj := StrReplace(obj, "\", "\\")
, obj := StrReplace(obj, "/", "\/")
, obj := StrReplace(obj, q, "\" . q)
, obj := StrReplace(obj, "`b", "\b")
, obj := StrReplace(obj, "`f", "\f")
, obj := StrReplace(obj, "`n", "\n")
, obj := StrReplace(obj, "`r", "\r")
, obj := StrReplace(obj, "`t", "\t")
static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
while RegExMatch(obj, needle, m)
obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
}
return q . obj . q
}
Jxon_True()
{
static obj := {}
return obj
}
Jxon_False()
{
static obj := {}
return obj
}
}
建议把代码也粘一个过来,大家跳转到github,影响体验
好的师傅,马上处理一下