first commit

This commit is contained in:
hero920103 committed 2026-10-24 13:02:19 +08:00
commit 761b95080f
404 files changed
+312608

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
namespace anjianjingling.Lua
{
/// <summary>
/// Lua 脚本能力宿主:由主窗体实现,向 Lua 引擎提供系统级操作能力。
/// 所有方法均在后台执行线程上被调用。
/// </summary>
public interface ILuaHost
{
void Log(string msg);
/// <summary>是否启用拟人化(落点随机偏移 + 随机微延时)。</summary>
bool JitterEnabled { get; }
// ---- 鼠标 ----
void MouseMove(int x, int y);
void MouseMoveR(int dx, int dy);
void MouseClick(string button, int? x, int? y);
void MouseDoubleClick(int? x, int? y);
void MouseDown(string button);
void MouseUp(string button);
void MouseWheel(int delta);
void MouseDrag(int x1, int y1, int x2, int y2, int durationMs);
(int X, int Y) MousePosition();
// ---- 键盘 ----
void KeyPress(string key);
void KeyDown(string key);
void KeyUp(string key);
void HotKey(string expression);
void TypeText(string text);
// ---- 屏幕与视觉 ----
(int W, int H) ScreenSize();
string ColorAt(int x, int y);
(int X, int Y) FindColor(int x1, int y1, int x2, int y2, string hex, int tolerance);
(int X, int Y, double Score) FindImage(int x1, int y1, int x2, int y2, string templatePath, double similarity);
void ScreenShot(string path);
/// <summary>弹出框选截图,返回保存路径(用户取消返回 null)。</summary>
string? PickRegion(string? savePath);
// ---- 窗口 / 系统 ----
void Launch(string path, string? arguments);
bool ActivateWindow(string titleKeyword);
string[] ListWindows();
// ---- 剪贴板 ----
string ClipGet();
void ClipSet(string text);
// ---- 内嵌浏览器 ----
void BrowserOpen(string url);
bool BrowserClick(string selector);
bool BrowserType(string selector, string text);
bool BrowserWaitFor(string selector, int timeoutMs);
string BrowserGetText(string selector);
string BrowserExecJs(string js);
void BrowserShot(string path);
void BrowserBack();
void BrowserForward();
void BrowserReload();
}
}
+655
View File
@@ -0,0 +1,655 @@
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace anjianjingling.Lua
{
public enum ArgKind
{
/// <summary>数字,原样输出。</summary>
Number,
/// <summary>字符串,生成时自动加引号转义。</summary>
Text,
/// <summary>键名 / 选择项,加引号。</summary>
Key,
/// <summary>表达式,原样输出。</summary>
Expr,
/// <summary>原样文本(变量名、注释),不加引号。</summary>
Raw
}
public enum BlockShape
{
/// <summary>单行命令。</summary>
Simple,
/// <summary>if <条件> then … end(可切换 else 分支)。</summary>
If,
/// <summary>如果屏幕上存在图片 … then … end。</summary>
IfExists,
/// <summary>for i = 1, N do … end。</summary>
LoopN,
/// <summary>while <条件> do … end。</summary>
LoopWhile,
/// <summary>break。</summary>
Break
}
public sealed record BlockArg(string Name, string Label, ArgKind Kind, string Default, string[]? Options = null);
public sealed class BlockDef
{
public required string Key { get; init; }
public required string Category { get; init; }
/// <summary>显示标题,含 {0} {1} 参数占位。</summary>
public required string Title { get; init; }
/// <summary>Lua 模板,含 {0} {1} 参数占位;容器块为起始行。</summary>
public string Template { get; init; } = "";
public BlockShape Shape { get; init; } = BlockShape.Simple;
public BlockArg[] Args { get; init; } = Array.Empty<BlockArg>();
public string Help { get; init; } = "";
public bool IsContainer => Shape is BlockShape.If or BlockShape.IfExists
or BlockShape.LoopN or BlockShape.LoopWhile;
}
/// <summary>画布上的一个积木实例。</summary>
public sealed class LuaBlock
{
public string Key { get; set; } = "";
public List<string> Values { get; set; } = new();
/// <summary>容器块的子块。</summary>
public List<LuaBlock> Children { get; set; } = new();
/// <summary>If 块是否有 else 分支。</summary>
public bool HasElse { get; set; }
public List<LuaBlock> ElseChildren { get; set; } = new();
public LuaBlock Clone()
{
var b = new LuaBlock { Key = Key, HasElse = HasElse, Values = new List<string>(Values) };
foreach (var c in Children) b.Children.Add(c.Clone());
foreach (var c in ElseChildren) b.ElseChildren.Add(c.Clone());
return b;
}
public string Value(int i)
{
var def = BlockCatalog.Find(Key);
if (def == null || i >= def.Args.Length) return "";
return i < Values.Count && !string.IsNullOrEmpty(Values[i]) ? Values[i] : def.Args[i].Default;
}
}
/// <summary>积木目录:定义表 + Lua 生成 + JSON 读写。</summary>
public static class BlockCatalog
{
public static readonly List<BlockDef> All = new();
private static readonly Dictionary<string, BlockDef> ByKey = new();
private static void Add(BlockDef d) { All.Add(d); ByKey[d.Key] = d; }
public static BlockDef? Find(string key) => ByKey.TryGetValue(key, out var d) ? d : null;
public static IEnumerable<string> Categories => All.Select(d => d.Category).Distinct();
static BlockCatalog()
{
// ==================== 流程 ====================
Add(new() { Key = "delay", Category = "流程", Title = "等待 {0} 毫秒", Template = "delay({0})",
Args = new[] { new BlockArg("ms", "毫秒", ArgKind.Number, "500") }, Help = "暂停指定时间" });
Add(new() { Key = "delayRandom", Category = "流程", Title = "随机等待 {0} ~ {1} 毫秒",
Template = "delayRandom({0}, {1})",
Args = new[] { new BlockArg("min", "最小", ArgKind.Number, "300"),
new BlockArg("max", "最大", ArgKind.Number, "800") } });
Add(new() { Key = "loopN", Category = "流程", Title = "循环 {0} 次", Shape = BlockShape.LoopN,
Args = new[] { new BlockArg("n", "次数", ArgKind.Number, "10") } });
Add(new() { Key = "loopWhile", Category = "流程", Title = "当 {0} 时循环", Shape = BlockShape.LoopWhile,
Args = new[] { new BlockArg("cond", "条件", ArgKind.Expr, "i < 10") } });
Add(new() { Key = "loopForever", Category = "流程", Title = "无限循环", Shape = BlockShape.LoopWhile });
Add(new() { Key = "if", Category = "流程", Title = "如果 {0}", Shape = BlockShape.If,
Args = new[] { new BlockArg("cond", "条件", ArgKind.Expr, "x >= 0") } });
Add(new() { Key = "break", Category = "流程", Title = "跳出循环", Shape = BlockShape.Break });
Add(new() { Key = "varSet", Category = "流程", Title = "设置变量 {0} = {1}", Template = "{0} = {1}",
Args = new[] { new BlockArg("name", "变量名", ArgKind.Raw, "x"),
new BlockArg("val", "值", ArgKind.Expr, "0") } });
Add(new() { Key = "varAdd", Category = "流程", Title = "变量 {0} 增加 {1}", Template = "{0} = {0} + {1}",
Args = new[] { new BlockArg("name", "变量名", ArgKind.Raw, "x"),
new BlockArg("val", "增量", ArgKind.Number, "1") } });
Add(new() { Key = "comment", Category = "流程", Title = "注释:{0}", Template = "-- {0}",
Args = new[] { new BlockArg("text", "内容", ArgKind.Raw, "说明") } });
// Lua 里无法识别的行,原样保留(脚本 ⇄ 积木互转时不丢代码)
Add(new() { Key = "lua", Category = "流程", Title = "自定义代码 {0}", Template = "{0}",
Args = new[] { new BlockArg("code", "代码", ArgKind.Raw, "-- 原生 Lua") },
Help = "脚本转换时无法识别的语句会变成此块,原样执行" });
// ==================== 鼠标 ====================
Add(new() { Key = "moveTo", Category = "鼠标", Title = "移动鼠标到 ({0}, {1})", Template = "moveTo({0}, {1})",
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
new BlockArg("y", "Y", ArgKind.Number, "300") } });
Add(new() { Key = "moveR", Category = "鼠标", Title = "相对移动 ({0}, {1})", Template = "moveR({0}, {1})",
Args = new[] { new BlockArg("dx", "X 偏移", ArgKind.Number, "10"),
new BlockArg("dy", "Y 偏移", ArgKind.Number, "10") } });
Add(new() { Key = "leftClickXY", Category = "鼠标", Title = "左键点击 ({0}, {1})",
Template = "leftClick({0}, {1})",
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
new BlockArg("y", "Y", ArgKind.Number, "300") } });
Add(new() { Key = "leftClick", Category = "鼠标", Title = "左键点击(当前位置)", Template = "leftClick()" });
Add(new() { Key = "rightClickXY", Category = "鼠标", Title = "右键点击 ({0}, {1})",
Template = "rightClick({0}, {1})",
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
new BlockArg("y", "Y", ArgKind.Number, "300") } });
Add(new() { Key = "rightClick", Category = "鼠标", Title = "右键点击(当前位置)", Template = "rightClick()" });
Add(new() { Key = "middleClick", Category = "鼠标", Title = "中键点击(当前位置)", Template = "middleClick()" });
Add(new() { Key = "doubleClick", Category = "鼠标", Title = "双击 ({0}, {1})", Template = "doubleClick({0}, {1})",
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
new BlockArg("y", "Y", ArgKind.Number, "300") } });
Add(new() { Key = "mouseDown", Category = "鼠标", Title = "按下鼠标 {0}", Template = "mouse.down({0})",
Args = new[] { new BlockArg("btn", "按键", ArgKind.Key, "left", new[] { "left", "right", "middle" }) } });
Add(new() { Key = "mouseUp", Category = "鼠标", Title = "弹起鼠标 {0}", Template = "mouse.up({0})",
Args = new[] { new BlockArg("btn", "按键", ArgKind.Key, "left", new[] { "left", "right", "middle" }) } });
Add(new() { Key = "wheel", Category = "鼠标", Title = "滚轮 {0} 格(正上负下)", Template = "wheel({0})",
Args = new[] { new BlockArg("n", "格数", ArgKind.Number, "-1") } });
Add(new() { Key = "drag", Category = "鼠标", Title = "拖拽 ({0},{1}) → ({2},{3}) 用时 {4} 毫秒",
Template = "drag({0}, {1}, {2}, {3}, {4})",
Args = new[] { new BlockArg("x1", "起点X", ArgKind.Number, "100"),
new BlockArg("y1", "起点Y", ArgKind.Number, "100"),
new BlockArg("x2", "终点X", ArgKind.Number, "600"),
new BlockArg("y2", "终点Y", ArgKind.Number, "400"),
new BlockArg("ms", "时长", ArgKind.Number, "500") } });
// ==================== 键盘 ====================
Add(new() { Key = "keyPress", Category = "键盘", Title = "按键 {0}", Template = "keyPress({0})",
Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "ENTER") },
Help = "ENTER / SPACE / TAB / ESC / F1~F12 / A~Z / LEFT / UP …" });
Add(new() { Key = "hotkey", Category = "键盘", Title = "组合键 {0}", Template = "hotkey({0})",
Args = new[] { new BlockArg("k", "组合", ArgKind.Key, "Ctrl+A") } });
Add(new() { Key = "typeText", Category = "键盘", Title = "输入文本 {0}", Template = "typeText({0})",
Args = new[] { new BlockArg("t", "文本", ArgKind.Text, "你好,世界") },
Help = "支持中文,使用 Unicode 注入,不受输入法影响" });
Add(new() { Key = "keyDown", Category = "键盘", Title = "按下键 {0}", Template = "keyDown({0})",
Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "SHIFT") } });
Add(new() { Key = "keyUp", Category = "键盘", Title = "弹起键 {0}", Template = "keyUp({0})",
Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "SHIFT") } });
// ==================== 视觉 ====================
Add(new() { Key = "findImage", Category = "视觉", Title = "找图 {0},相似度 {1} → 变量 {2}, {3}",
Template = "{2}, {3} = findImage({0}, {1})",
Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"),
new BlockArg("sim", "相似度", ArgKind.Number, "0.85"),
new BlockArg("vx", "X 变量", ArgKind.Raw, "x"),
new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") },
Help = "找不到时变量为 -1;模板图片放程序目录 images/ 下" });
Add(new() { Key = "waitImage", Category = "视觉", Title = "等待图片 {0},超时 {1} 毫秒 → 变量 {2}, {3}",
Template = "{2}, {3} = waitImage({0}, {1})",
Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"),
new BlockArg("ms", "超时", ArgKind.Number, "10000"),
new BlockArg("vx", "X 变量", ArgKind.Raw, "x"),
new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") } });
Add(new() { Key = "findColor", Category = "视觉", Title = "找色 {0},容差 {1} → 变量 {2}, {3}",
Template = "{2}, {3} = findColor({0}, {1})",
Args = new[] { new BlockArg("hex", "颜色", ArgKind.Text, "FF0000"),
new BlockArg("tol", "容差", ArgKind.Number, "10"),
new BlockArg("vx", "X 变量", ArgKind.Raw, "x"),
new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") } });
Add(new() { Key = "ifExists", Category = "视觉", Title = "如果屏幕上有图片 {0}(相似度 {1})",
Shape = BlockShape.IfExists,
Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"),
new BlockArg("sim", "相似度", ArgKind.Number, "0.85") } });
Add(new() { Key = "screenshot", Category = "视觉", Title = "截图保存到 {0}", Template = "screenshot({0})",
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "shot.png") } });
Add(new() { Key = "pickRegion", Category = "视觉", Title = "框选截图存为 {0}", Template = "screen.pick({0})",
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "images/btn.png") },
Help = "运行时弹窗框选一块屏幕区域,直接存成模板图" });
// ==================== 系统 ====================
Add(new() { Key = "run", Category = "系统", Title = "启动程序 {0}", Template = "run({0})",
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "notepad.exe") } });
Add(new() { Key = "activate", Category = "系统", Title = "激活窗口 {0}", Template = "activate({0})",
Args = new[] { new BlockArg("title", "标题关键字", ArgKind.Text, "记事本") } });
Add(new() { Key = "log", Category = "系统", Title = "日志 {0}", Template = "log({0})",
Args = new[] { new BlockArg("msg", "内容", ArgKind.Text, "提示信息") } });
Add(new() { Key = "clipSet", Category = "系统", Title = "复制到剪贴板 {0}", Template = "clipSet({0})",
Args = new[] { new BlockArg("text", "内容", ArgKind.Text, "") } });
Add(new() { Key = "stop", Category = "系统", Title = "停止脚本", Template = "stop()" });
// ==================== 浏览器 ====================
Add(new() { Key = "browserOpen", Category = "浏览器", Title = "打开网页 {0}", Template = "browser.open({0})",
Args = new[] { new BlockArg("url", "网址", ArgKind.Text, "https://www.baidu.com") } });
Add(new() { Key = "browserClick", Category = "浏览器", Title = "点击网页元素 {0}", Template = "browser.click({0})",
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, "#btn") } });
Add(new() { Key = "browserType", Category = "浏览器", Title = "网页元素 {0} 输入 {1}",
Template = "browser.type({0}, {1})",
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, "#input"),
new BlockArg("text", "文本", ArgKind.Text, "内容") } });
Add(new() { Key = "browserWait", Category = "浏览器", Title = "等待网页元素 {0},超时 {1} 毫秒",
Template = "browser.wait({0}, {1})",
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, ".loaded"),
new BlockArg("ms", "超时", ArgKind.Number, "10000") } });
Add(new() { Key = "browserText", Category = "浏览器", Title = "读取网页元素 {0} → 变量 {1}",
Template = "{1} = browser.text({0})",
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, ".result"),
new BlockArg("v", "变量", ArgKind.Raw, "text") } });
Add(new() { Key = "browserJs", Category = "浏览器", Title = "执行 JS {0}", Template = "browser.js({0})",
Args = new[] { new BlockArg("js", "JS 代码", ArgKind.Text, "document.title") } });
Add(new() { Key = "browserShot", Category = "浏览器", Title = "网页截图 {0}", Template = "browser.shot({0})",
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "web.png") } });
}
// ==================== 标题渲染 ====================
public static string RenderTitle(LuaBlock b)
{
var def = Find(b.Key);
if (def == null) return "未知积木:" + b.Key;
var args = new object[def.Args.Length];
for (int i = 0; i < def.Args.Length; i++)
{
string v = b.Value(i);
if (def.Args[i].Kind == ArgKind.Text) v = "\"" + v + "\"";
args[i] = v;
}
try { return string.Format(def.Title, args); }
catch { return def.Title; }
}
// ==================== Lua 生成 ====================
public static string ToLua(IEnumerable<LuaBlock> blocks)
{
var sb = new StringBuilder();
Emit(blocks, 0, sb);
return sb.ToString();
}
private static void Emit(IEnumerable<LuaBlock> blocks, int level, StringBuilder sb)
{
string pad = new(' ', level * 4);
foreach (var b in blocks)
{
var def = Find(b.Key);
if (def == null) continue;
switch (def.Shape)
{
case BlockShape.If:
sb.AppendLine($"{pad}if {ArgText(def, b, 0)} then");
Emit(b.Children, level + 1, sb);
if (b.HasElse)
{
sb.AppendLine($"{pad}else");
Emit(b.ElseChildren, level + 1, sb);
}
sb.AppendLine($"{pad}end");
break;
case BlockShape.IfExists:
sb.AppendLine($"{pad}do");
sb.AppendLine($"{pad} local __ix, __iy = findImage({ArgText(def, b, 0)}, {ArgText(def, b, 1)})");
sb.AppendLine($"{pad} if __ix >= 0 then");
Emit(b.Children, level + 2, sb);
if (b.HasElse)
{
sb.AppendLine($"{pad} else");
Emit(b.ElseChildren, level + 2, sb);
}
sb.AppendLine($"{pad} end");
sb.AppendLine($"{pad}end");
break;
case BlockShape.LoopN:
sb.AppendLine($"{pad}for i = 1, {ArgText(def, b, 0)} do");
Emit(b.Children, level + 1, sb);
sb.AppendLine($"{pad}end");
break;
case BlockShape.LoopWhile:
string cond = def.Args.Length == 0 ? "true" : ArgText(def, b, 0);
sb.AppendLine($"{pad}while {cond} do");
Emit(b.Children, level + 1, sb);
sb.AppendLine($"{pad}end");
break;
case BlockShape.Break:
sb.AppendLine($"{pad}break");
break;
default:
if (string.IsNullOrEmpty(def.Template)) break;
var vals = new object[def.Args.Length];
for (int i = 0; i < def.Args.Length; i++) vals[i] = ArgText(def, b, i);
string line;
try { line = string.Format(def.Template, vals); }
catch { line = def.Template; }
sb.AppendLine(pad + line);
break;
}
}
}
private static string ArgText(BlockDef def, LuaBlock b, int i)
{
if (i >= def.Args.Length) return "";
var arg = def.Args[i];
string v = b.Value(i);
return arg.Kind switch
{
ArgKind.Number => string.IsNullOrWhiteSpace(v) ? "0" : v.Trim(),
ArgKind.Expr or ArgKind.Raw => v,
_ => Quote(v)
};
}
private static string Quote(string s)
=> "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"")
.Replace("\r", "").Replace("\n", "\\n") + "\"";
// ==================== Lua → 积木(双向转换) ====================
private sealed class Frame
{
public LuaBlock Block = null!;
public List<LuaBlock> ParentList = null!;
public int Ends; // 还差几个 end 才结束此块
}
private sealed class TplInfo
{
public BlockDef Def = null!;
public string Call = "";
public int RetCount;
public List<int> ArgIdx = new();
}
private static List<TplInfo>? _tplCache;
private static List<TplInfo> TplCache => _tplCache ??= BuildTplCache();
private static List<TplInfo> BuildTplCache()
{
var list = new List<TplInfo>();
foreach (var def in All)
{
if (def.Shape != BlockShape.Simple || def.Template.Length == 0) continue;
string callPart = def.Template, retPart = "";
int eq = def.Template.IndexOf(" = ", StringComparison.Ordinal);
if (eq >= 0)
{
retPart = def.Template.Substring(0, eq);
callPart = def.Template.Substring(eq + 3);
}
var m = Regex.Match(callPart, @"^([A-Za-z_][\w\.]*)\s*\((.*)\)$");
if (!m.Success) continue;
// 左侧只允许纯占位符({2}, {3}),右侧只允许 "a, b" 形式的占位符列表
string retFixed = Regex.Replace(retPart, @"\{\d+\}", "").Trim().Replace(",", "").Replace(" ", "");
if (retPart.Length > 0 && retFixed.Length > 0) continue;
string argFixed = Regex.Replace(m.Groups[2].Value, @"\{\d+\}", "").Trim().Replace(",", "").Replace(" ", "");
if (argFixed.Length > 0) continue;
if (retPart.Length > 0 && !Regex.IsMatch(retPart, @"\{\d+\}")) continue;
var info = new TplInfo { Def = def, Call = m.Groups[1].Value };
if (retPart.Length > 0)
info.RetCount = Regex.Matches(retPart, @"\{\d+\}").Count;
foreach (Match ph in Regex.Matches(m.Groups[2].Value, @"\{(\d+)\}"))
info.ArgIdx.Add(int.Parse(ph.Groups[1].Value));
list.Add(info);
}
return list;
}
/// <summary>把 Lua 脚本解析成积木。认识目录里的全部命令,认不出的行进「自定义代码」块。</summary>
public static List<LuaBlock> FromLua(string code)
{
var root = new List<LuaBlock>();
var stack = new Stack<Frame>();
var cur = root;
string[] lines = (code ?? "").Replace("\r\n", "\n").Split('\n');
for (int i = 0; i < lines.Length; i++)
{
string raw = lines[i].Trim();
if (raw.Length == 0) continue;
if (raw.StartsWith("--"))
{
cur.Add(new LuaBlock { Key = "comment", Values = { raw.Substring(2).Trim() } });
continue;
}
string line = StripComment(raw).Trim();
if (line.Length == 0) continue;
// ifExists 生成的三行结构:do / local __ix,__iy = findImage(…) / if __ix >= 0 then
if (line == "do" && i + 2 < lines.Length)
{
string l1 = StripComment(lines[i + 1]).Trim();
string l2 = StripComment(lines[i + 2]).Trim();
var m1 = Regex.Match(l1, @"^local\s+__\w+\s*,\s*__\w+\s*=\s*findImage\s*\((.*)\)$");
if (m1.Success && Regex.IsMatch(l2, @"^if\s+__\w+\s*>=\s*0\s+then$"))
{
var b = new LuaBlock { Key = "ifExists" };
FillArgs(b, SplitArgs(m1.Groups[1].Value), new List<int> { 0, 1 });
cur.Add(b);
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 2 });
cur = b.Children;
i += 2;
continue;
}
}
if (line == "end")
{
if (stack.Count > 0)
{
stack.Peek().Ends--;
while (stack.Count > 0 && stack.Peek().Ends <= 0)
cur = stack.Pop().ParentList;
}
continue;
}
if (line == "else" || line.StartsWith("elseif "))
{
bool handled = false;
if (stack.Count > 0)
{
var top = stack.Peek();
if (top.Block.Key is "if" or "ifExists")
{
if (line == "else" && !top.Block.HasElse)
{
top.Block.HasElse = true;
cur = top.Block.ElseChildren;
handled = true;
}
else if (line.StartsWith("elseif "))
{
// elseif → else 里嵌一个 if,共用原来的 end
string cond = Regex.Match(line, @"^elseif\s+(.+?)\s+then$").Groups[1].Value;
if (cond.Length > 0)
{
top.Ends--;
top.Block.HasElse = true;
var nested = new LuaBlock { Key = "if", Values = { cond.Trim() } };
cur.Add(nested);
stack.Push(new Frame { Block = nested, ParentList = cur, Ends = 1 });
cur = nested.Children;
handled = true;
}
}
}
}
if (!handled) cur.Add(Custom(raw));
continue;
}
var mFor = Regex.Match(line, @"^for\s+\w+\s*=\s*([^,]+?)\s*,\s*(.+?)\s+do$");
if (mFor.Success)
{
var b = new LuaBlock { Key = "loopN", Values = { mFor.Groups[2].Value.Trim() } };
cur.Add(b);
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 });
cur = b.Children;
continue;
}
var mWhile = Regex.Match(line, @"^while\s+(.+?)\s+do$");
if (mWhile.Success)
{
string cond = mWhile.Groups[1].Value.Trim();
var b = cond == "true"
? new LuaBlock { Key = "loopForever" }
: new LuaBlock { Key = "loopWhile", Values = { cond } };
cur.Add(b);
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 });
cur = b.Children;
continue;
}
var mIf = Regex.Match(line, @"^if\s+(.+?)\s+then$");
if (mIf.Success)
{
var b = new LuaBlock { Key = "if", Values = { mIf.Groups[1].Value.Trim() } };
cur.Add(b);
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 });
cur = b.Children;
continue;
}
if (line == "break") { cur.Add(new LuaBlock { Key = "break" }); continue; }
var tpl = MatchTemplate(line);
if (tpl != null) { cur.Add(tpl); continue; }
var mAdd = Regex.Match(line, @"^(\w+)\s*=\s*\1\s*\+\s*(.+)$");
if (mAdd.Success)
{
cur.Add(new LuaBlock { Key = "varAdd", Values = { mAdd.Groups[1].Value, mAdd.Groups[2].Value.Trim() } });
continue;
}
var mSet = Regex.Match(line, @"^([A-Za-z_]\w*)\s*=\s*(.+)$");
if (mSet.Success)
{
cur.Add(new LuaBlock { Key = "varSet", Values = { mSet.Groups[1].Value, mSet.Groups[2].Value.Trim() } });
continue;
}
cur.Add(Custom(raw));
}
return root;
}
private static LuaBlock Custom(string line) => new() { Key = "lua", Values = { line.Trim() } };
/// <summary>去掉行尾注释(不切字符串里的 --)。</summary>
private static string StripComment(string s)
{
bool inStr = false;
for (int i = 0; i < s.Length - 1; i++)
{
char c = s[i];
if (c == '\\' && inStr) { i++; continue; }
if (c == '"') inStr = !inStr;
else if (!inStr && c == '-' && i + 1 < s.Length && s[i + 1] == '-')
return s.Substring(0, i);
}
return s;
}
private static LuaBlock? MatchTemplate(string line)
{
foreach (var info in TplCache)
{
string callPat = Regex.Escape(info.Call).Replace(@"\.", @"\.");
string pat = info.RetCount switch
{
0 => "^(?:local\\s+)?" + callPat + @"\s*\((.*)\)$",
1 => "^(?:local\\s+)?([A-Za-z_][\\w\\.]*)\\s*=\\s*" + callPat + @"\s*\((.*)\)$",
_ => "^(?:local\\s+)?([A-Za-z_][\\w\\.]*\\s*,\\s*[A-Za-z_][\\w\\.]*)\\s*=\\s*" + callPat + @"\s*\((.*)\)$"
};
var m = Regex.Match(line, pat);
if (!m.Success) continue;
var args = SplitArgs(info.RetCount == 0 ? m.Groups[1].Value : m.Groups[2].Value);
if (args.Count != info.ArgIdx.Count) continue;
// 返回值变量数必须一致
if (info.RetCount > 0)
{
int lhs = m.Groups[1].Value.Split(',').Length;
if (lhs != info.RetCount) continue;
}
var b = new LuaBlock { Key = info.Def.Key };
FillArgs(b, args, info.ArgIdx);
return b;
}
return null;
}
private static void FillArgs(LuaBlock b, List<string> args, List<int> slotIdx)
{
var def = Find(b.Key)!;
for (int k = 0; k < slotIdx.Count && k < args.Count; k++)
{
int slot = slotIdx[k];
while (b.Values.Count < def.Args.Length) b.Values.Add("");
b.Values[slot] = def.Args[slot].Kind is ArgKind.Text or ArgKind.Key
? Unquote(args[k]) : args[k].Trim();
}
}
/// <summary>按顶层逗号拆参数(跳过字符串 / 括号内的逗号)。</summary>
private static List<string> SplitArgs(string s)
{
var list = new List<string>();
if (string.IsNullOrWhiteSpace(s)) return list;
int depth = 0;
bool inStr = false;
var sb = new StringBuilder();
foreach (var c in s)
{
if (inStr)
{
sb.Append(c);
if (c == '"') inStr = false;
else if (c == '\\') { sb.Length--; /* 下一个字符原样进 */ }
continue;
}
switch (c)
{
case '"': inStr = true; sb.Append(c); break;
case '(' or '[' or '{': depth++; sb.Append(c); break;
case ')' or ']' or '}': depth--; sb.Append(c); break;
case ',' when depth == 0:
list.Add(sb.ToString().Trim()); sb.Clear(); break;
default: sb.Append(c); break;
}
}
if (sb.Length > 0) list.Add(sb.ToString().Trim());
return list;
}
private static string Unquote(string s)
{
s = s.Trim();
if (s.Length >= 2 && ((s[0] == '"' && s[^1] == '"') || (s[0] == '\'' && s[^1] == '\'')))
{
s = s.Substring(1, s.Length - 2);
s = s.Replace("\\\"", "\"").Replace("\\'", "'")
.Replace("\\\\", "\\").Replace("\\n", "\n").Replace("\\t", "\t");
}
return s;
}
// ==================== JSON ====================
private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true };
public static string ToJson(IEnumerable<LuaBlock> blocks) => JsonSerializer.Serialize(blocks, JsonOpt);
public static List<LuaBlock> FromJson(string json)
=> JsonSerializer.Deserialize<List<LuaBlock>>(json) ?? new();
}
}
+276
View File
@@ -0,0 +1,276 @@
using MoonSharp.Interpreter;
using System.Diagnostics;
namespace anjianjingling.Lua
{
public enum LuaState { Idle, Running, Paused }
/// <summary>脚本中止信号(停止 / 急停 / 主动 stop)。</summary>
internal sealed class AbortSignal : Exception
{
public bool ByPanic { get; }
public AbortSignal(bool byPanic) : base(byPanic ? "鼠标触及屏幕左上角" : "脚本已停止")
=> ByPanic = byPanic;
}
/// <summary>
/// Lua 脚本执行器:MoonSharp 宿主 + 暂停/停止/急停 + 任务队列。
/// </summary>
public sealed class LuaEngine
{
private readonly ILuaHost _host;
private CancellationTokenSource? _cts;
private readonly ManualResetEventSlim _pauseGate = new(true);
private Task? _task;
private volatile bool _paused;
private volatile bool _byPanic;
private long _lastPanicCheckTick;
public LuaState State { get; private set; } = LuaState.Idle;
public event Action<LuaState>? StateChanged;
/// <summary>执行速度倍率:只影响 delay 等待时长。</summary>
public double Speed { get; set; } = 1.0;
public LuaEngine(ILuaHost host) => _host = host;
// ===================== 语法检查 =====================
/// <summary>只编译不执行;返回 null 表示语法通过。</summary>
public string? Validate(string code)
{
try
{
var script = CreateScript();
script.LoadString(code, null, "脚本");
return null;
}
catch (SyntaxErrorException ex) { return Localize(ex.Message); }
catch (Exception ex) { return ex.Message; }
}
// ===================== 运行 =====================
/// <summary>运行单段脚本(一次)。</summary>
public void Run(string code)
{
if (State != LuaState.Idle) { _host.Log("已有脚本在运行"); return; }
DynValue chunk;
try
{
var script = CreateScript();
chunk = script.LoadString(code, null, "脚本");
}
catch (SyntaxErrorException ex)
{
_host.Log("语法错误:" + Localize(ex.Message));
return;
}
RunBackground(() =>
{
chunk.Function.Call();
_host.Log("脚本执行完毕");
});
}
/// <summary>按顺序执行勾选的任务;每个任务按自身循环设置重复。</summary>
public void RunQueue(IReadOnlyList<LuaTask> tasks)
{
if (State != LuaState.Idle) { _host.Log("已有脚本在运行"); return; }
var enabled = tasks.Where(t => t.Enabled).ToList();
if (enabled.Count == 0) { _host.Log("没有勾选的任务"); return; }
// 启动前统一编译,提前暴露语法错误
var prepared = new List<(LuaTask Task, DynValue Chunk)>();
foreach (var t in enabled)
{
try
{
var script = CreateScript();
prepared.Add((t, script.LoadString(t.Script, null, t.Name)));
}
catch (SyntaxErrorException ex)
{
_host.Log($"任务「{t.Name}」语法错误:{Localize(ex.Message)}");
return;
}
}
RunBackground(() =>
{
for (int i = 0; i < prepared.Count; i++)
{
var (task, chunk) = prepared[i];
_host.Log($"▶ 开始任务 {i + 1}/{prepared.Count}:{task.Name}({task.ModeText})");
int round = 0;
while (true)
{
Tick();
if (!task.Infinite && round >= task.LoopCount) break;
chunk.Function.Call();
round++;
bool more = task.Infinite || round < task.LoopCount;
if (more && task.IntervalMs > 0) Sleep(task.IntervalMs);
}
_host.Log($"✔ 任务完成:{task.Name}({round} 轮)");
}
_host.Log("★ 任务队列全部执行完毕");
});
}
private void RunBackground(Action body)
{
_paused = false;
_byPanic = false;
_pauseGate.Set();
_cts = new CancellationTokenSource();
SetState(LuaState.Running);
_task = Task.Run(() =>
{
try { body(); }
catch (AbortSignal ex)
{
_host.Log(ex.ByPanic ? "■ 鼠标触及屏幕左上角,紧急停止" : "■ 脚本已停止");
}
catch (SyntaxErrorException ex) { _host.Log("语法错误:" + Localize(ex.Message)); }
catch (ScriptRuntimeException ex) { _host.Log("运行错误:" + Localize(ex.Message)); }
catch (InterpreterException ex) { _host.Log("运行错误:" + Localize(ex.Message)); }
catch (Exception ex) { _host.Log("运行错误:" + ex.Message); }
finally
{
_cts?.Dispose();
_cts = null;
_pauseGate.Set();
_paused = false;
SetState(LuaState.Idle);
}
});
}
private Script CreateScript()
{
var script = new Script(CoreModules.Preset_HardSandbox
| CoreModules.String | CoreModules.Math
| CoreModules.Table | CoreModules.Coroutine);
script.Options.DebugPrint = s => _host.Log(s);
ScriptApi.Bind(script, this, _host);
return script;
}
// ===================== 控制 =====================
public void PauseOrResume()
{
if (State == LuaState.Running)
{
_paused = true;
_pauseGate.Reset();
_host.Log("‖ 已暂停(F9 继续)");
SetState(LuaState.Paused);
}
else if (State == LuaState.Paused)
{
_paused = false;
_pauseGate.Set();
_host.Log("▶ 继续执行");
SetState(LuaState.Running);
}
}
public void Stop()
{
if (State == LuaState.Idle) return;
_byPanic = false;
_paused = false;
_pauseGate.Set();
_cts?.Cancel();
}
private void SetState(LuaState s)
{
State = s;
StateChanged?.Invoke(s);
}
// ===================== 中断点(API 回调内调用)=====================
internal void Tick()
{
var cts = _cts;
if (cts is { IsCancellationRequested: true }) throw new AbortSignal(_byPanic);
if (_paused)
{
_pauseGate.Wait();
if (cts is { IsCancellationRequested: true }) throw new AbortSignal(_byPanic);
}
// 急停检查节流到 100ms 一次,避免每条命令都调系统 API
long now = Environment.TickCount64;
if (now - _lastPanicCheckTick < 100) return;
_lastPanicCheckTick = now;
Native.Win32.GetCursorPos(out var p);
if (p.X > 0 || p.Y > 0) return;
Thread.Sleep(120);
Native.Win32.GetCursorPos(out var p2);
if (p2.X <= 0 && p2.Y <= 0)
{
_byPanic = true;
_cts?.Cancel();
throw new AbortSignal(true);
}
}
/// <summary>可中断、可暂停的延时(分片 20ms,支持倍速)。</summary>
internal void Sleep(double ms)
{
ms = Math.Max(0, ms / Math.Max(0.05, Speed));
var sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalMilliseconds < ms)
{
Tick();
int slice = (int)Math.Min(20, Math.Max(1, ms - sw.Elapsed.TotalMilliseconds));
_pauseGate.Wait(slice);
}
}
// ===================== 错误信息中文化 =====================
private static readonly (string From, string To)[] Dict =
{
("unexpected symbol near '<eof>'", "语句未写完(意外到达文件结尾)"),
("unexpected symbol", "意外的符号"),
("'end' expected", "缺少 end"),
("'=' expected", "缺少 ="),
("'then' expected", "缺少 then"),
("'do' expected", "缺少 do"),
("unfinished string", "字符串未闭合"),
("malformed number", "数字格式错误"),
("attempt to index a nil value", "访问了空值(nil)"),
("attempt to call a nil value", "调用了不存在的函数"),
("attempt to perform arithmetic", "算术运算类型错误"),
("attempt to compare", "比较运算类型错误"),
("index out of range", "索引越界"),
("cannot convert", "类型转换失败"),
("invalid arguments", "参数不合法"),
};
private static string Localize(string msg)
{
if (string.IsNullOrEmpty(msg)) return msg;
foreach (var (from, to) in Dict)
if (msg.Contains(from, StringComparison.OrdinalIgnoreCase))
msg = msg.Replace(from, to, StringComparison.OrdinalIgnoreCase);
return msg.Trim();
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System.Text.Json;
namespace anjianjingling.Lua
{
/// <summary>任务列表中的一个脚本任务。</summary>
public sealed class LuaTask
{
public string Name { get; set; } = "新任务";
public bool Enabled { get; set; } = true;
/// <summary>true=无限循环(会阻塞后续任务,需手动停止)。</summary>
public bool Infinite { get; set; } = false;
public int LoopCount { get; set; } = 1;
public int IntervalMs { get; set; } = 1000;
public string Script { get; set; } = "";
/// <summary>积木画布 JSON(有内容时优先于脚本)。</summary>
public string Blocks { get; set; } = "";
/// <summary>启动热键(如 F8),空表示不绑定。</summary>
public string Hotkey { get; set; } = "";
public string Source { get; set; } = "";
public string ModeText => Infinite ? "无限循环" : $"{LoopCount} 次";
public override string ToString() => Name;
}
/// <summary>任务列表 JSON 读写(.ajt)。</summary>
public static class LuaTaskFile
{
private static readonly JsonSerializerOptions Opt = new() { WriteIndented = true };
public static string ToJson(IEnumerable<LuaTask> tasks) => JsonSerializer.Serialize(tasks, Opt);
public static List<LuaTask> FromJson(string json)
=> JsonSerializer.Deserialize<List<LuaTask>>(json) ?? new();
}
}
+280
View File
@@ -0,0 +1,280 @@
using MoonSharp.Interpreter;
namespace anjianjingling.Lua
{
/// <summary>
/// 把自动化能力注册进 Lua 全局环境。
/// 两套写法并存:
/// 1) 全局简写(按键精灵风格) moveTo(100,200) leftClick() delay(500)
/// 2) 命名空间写法 mouse.moveTo(100,200) mouse.click() key.press("ENTER")
/// </summary>
internal static class ScriptApi
{
private const double DefaultSim = 0.85;
private const int DefaultTolerance = 10;
public static void Bind(Script script, LuaEngine engine, ILuaHost host)
{
var g = script.Globals;
// ===================== 基础 =====================
g["log"] = (Action<string>)(m => host.Log(m));
g["delay"] = (Action<double>)(ms => engine.Sleep(ms));
g["sleep"] = (Action<double>)(ms => engine.Sleep(ms));
g["delayRandom"] = (Action<double, double>)((a, b) =>
{
int lo = (int)Math.Min(a, b), hi = (int)Math.Max(a, b);
engine.Sleep(lo >= hi ? lo : Random.Shared.Next(lo, hi + 1));
});
g["random"] = (Func<double, double, double>)((a, b) =>
{
int lo = (int)Math.Min(a, b), hi = (int)Math.Max(a, b);
return lo >= hi ? lo : Random.Shared.Next(lo, hi + 1);
});
g["now"] = (Func<double>)(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
g["stop"] = (Action)(() => throw new AbortSignal(false));
// ===================== 鼠标 =====================
var mouse = new Table(script);
mouse["moveTo"] = (Action<int, int>)((x, y) => { engine.Tick(); host.MouseMove(x, y); });
mouse["moveR"] = (Action<int, int>)((dx, dy) => { engine.Tick(); host.MouseMoveR(dx, dy); });
mouse["down"] = (Action<string>)(b => { engine.Tick(); host.MouseDown(NormBtn(b)); });
mouse["up"] = (Action<string>)(b => { engine.Tick(); host.MouseUp(NormBtn(b)); });
mouse["wheel"] = (Action<int>)(d => { engine.Tick(); host.MouseWheel(d); });
mouse["drag"] = (Action<int, int, int, int, int>)((x1, y1, x2, y2, ms) =>
{
engine.Tick();
host.MouseDrag(x1, y1, x2, y2, ms <= 0 ? 400 : ms);
});
mouse["pos"] = (Func<DynValue>)(() =>
{
engine.Tick();
var p = host.MousePosition();
return DynValue.NewTuple(DynValue.NewNumber(p.X), DynValue.NewNumber(p.Y));
});
// click 支持多种参数形态
mouse["click"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
string btn = "left";
int? x = null, y = null;
if (a.Count >= 3 && a[0].Type == DataType.Number)
{
x = (int)Math.Round(a[0].Number);
y = (int)Math.Round(a[1].Number);
btn = a[2].Type == DataType.String ? a[2].String : "left";
}
else if (a.Count >= 2 && a[0].Type == DataType.Number)
{
x = (int)Math.Round(a[0].Number);
y = (int)Math.Round(a[1].Number);
}
else if (a.Count >= 1 && a[0].Type == DataType.String)
{
btn = a[0].String;
}
host.MouseClick(NormBtn(btn), x, y);
return DynValue.Nil;
});
mouse["doubleClick"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
int? x = a.Count >= 2 && a[0].Type == DataType.Number ? (int)Math.Round(a[0].Number) : null;
int? y = a.Count >= 2 && a[0].Type == DataType.Number ? (int)Math.Round(a[1].Number) : null;
host.MouseDoubleClick(x, y);
return DynValue.Nil;
});
g["mouse"] = mouse;
// ===================== 键盘 =====================
var key = new Table(script);
key["press"] = (Action<string>)(k => { engine.Tick(); host.KeyPress(k); });
key["down"] = (Action<string>)(k => { engine.Tick(); host.KeyDown(k); });
key["up"] = (Action<string>)(k => { engine.Tick(); host.KeyUp(k); });
key["hotkey"] = (Action<string>)(k => { engine.Tick(); host.HotKey(k); });
key["type"] = (Action<string>)(t => { engine.Tick(); host.TypeText(t); });
g["key"] = key;
// ===================== 屏幕 / 视觉 =====================
var screen = new Table(script);
screen["size"] = (Func<DynValue>)(() =>
{
engine.Tick();
var (w, h) = host.ScreenSize();
return DynValue.NewTuple(DynValue.NewNumber(w), DynValue.NewNumber(h));
});
screen["color"] = (Func<int, int, string>)((x, y) => { engine.Tick(); return host.ColorAt(x, y); });
screen["shot"] = (Action<string>)(p => { engine.Tick(); host.ScreenShot(p); });
screen["pick"] = (Func<string, string>)(p =>
{
engine.Tick();
return host.PickRegion(string.IsNullOrWhiteSpace(p) ? null : p) ?? "";
});
screen["findColor"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
string hex = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "FF0000";
int tol = a.Count > 1 && a[1].Type == DataType.Number ? (int)Math.Round(a[1].Number) : DefaultTolerance;
var (w, h) = host.ScreenSize();
int x1 = a.Count > 2 ? (int)Math.Round(a[2].Number) : 0;
int y1 = a.Count > 3 ? (int)Math.Round(a[3].Number) : 0;
int x2 = a.Count > 4 ? (int)Math.Round(a[4].Number) : w - 1;
int y2 = a.Count > 5 ? (int)Math.Round(a[5].Number) : h - 1;
var p = host.FindColor(x1, y1, x2, y2, hex, tol);
return DynValue.NewTuple(DynValue.NewNumber(p.X), DynValue.NewNumber(p.Y));
});
screen["findImage"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
double sim = a.Count > 1 && a[1].Type == DataType.Number ? a[1].Number : DefaultSim;
var (w, h) = host.ScreenSize();
int x1 = a.Count > 2 ? (int)Math.Round(a[2].Number) : 0;
int y1 = a.Count > 3 ? (int)Math.Round(a[3].Number) : 0;
int x2 = a.Count > 4 ? (int)Math.Round(a[4].Number) : w - 1;
int y2 = a.Count > 5 ? (int)Math.Round(a[5].Number) : h - 1;
var r = host.FindImage(x1, y1, x2, y2, path, sim);
return DynValue.NewTuple(
DynValue.NewNumber(r.X), DynValue.NewNumber(r.Y), DynValue.NewNumber(r.Score));
});
screen["waitImage"] = DynValue.NewCallback((ctx, a) =>
{
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
int timeout = a.Count > 1 && a[1].Type == DataType.Number ? (int)Math.Round(a[1].Number) : 10000;
double sim = a.Count > 2 && a[2].Type == DataType.Number ? a[2].Number : DefaultSim;
var (w, h) = host.ScreenSize();
var sw = System.Diagnostics.Stopwatch.StartNew();
int rx = -1, ry = -1;
double best = 0;
while (sw.ElapsedMilliseconds < timeout)
{
engine.Tick();
var r = host.FindImage(0, 0, w - 1, h - 1, path, sim);
best = r.Score;
if (r.X >= 0) { rx = r.X; ry = r.Y; break; }
engine.Sleep(300);
}
host.Log(rx >= 0
? $"等待图片出现:找到 ({rx},{ry}) 相似度 {best:0.00}"
: $"等待图片超时:{path}(最高 {best:0.00})");
return DynValue.NewTuple(
DynValue.NewNumber(rx), DynValue.NewNumber(ry), DynValue.NewNumber(best));
});
screen["exists"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
double sim = a.Count > 1 && a[1].Type == DataType.Number ? a[1].Number : DefaultSim;
var (w, h) = host.ScreenSize();
var r = host.FindImage(0, 0, w - 1, h - 1, path, sim);
return DynValue.NewBoolean(r.X >= 0);
});
g["screen"] = screen;
// ===================== 窗口 / 系统 =====================
var win = new Table(script);
win["launch"] = (Action<string, string>)((p, args) => { engine.Tick(); host.Launch(p, args); });
win["activate"] = (Func<string, bool>)(t => { engine.Tick(); return host.ActivateWindow(t); });
win["list"] = (Func<DynValue>)(() =>
{
engine.Tick();
var list = new Table(script);
var arr = host.ListWindows();
for (int i = 0; i < arr.Length; i++) list[i + 1] = arr[i];
return DynValue.NewTable(list);
});
g["win"] = win;
var sys = new Table(script);
sys["time"] = (Func<double>)(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
sys["sleep"] = (Action<double>)(ms => engine.Sleep(ms));
sys["speed"] = (Func<double, double>)(s => { engine.Speed = s <= 0 ? 1 : s; return engine.Speed; });
g["sys"] = sys;
// ===================== 剪贴板 =====================
var clip = new Table(script);
clip["get"] = (Func<string>)(() => host.ClipGet());
clip["set"] = (Action<string>)(t => host.ClipSet(t));
g["clip"] = clip;
// ===================== 浏览器 =====================
var browser = new Table(script);
browser["open"] = (Action<string>)(u => { engine.Tick(); host.BrowserOpen(u); });
browser["click"] = (Func<string, bool>)(s => { engine.Tick(); return host.BrowserClick(s); });
browser["type"] = (Func<string, string, bool>)((s, t) => { engine.Tick(); return host.BrowserType(s, t); });
browser["wait"] = (Func<string, int, bool>)((s, ms) => { engine.Tick(); return host.BrowserWaitFor(s, ms <= 0 ? 10000 : ms); });
browser["text"] = (Func<string, string>)(s => { engine.Tick(); return host.BrowserGetText(s); });
browser["js"] = (Func<string, string>)(js => { engine.Tick(); return host.BrowserExecJs(js); });
browser["shot"] = (Action<string>)(p => { engine.Tick(); host.BrowserShot(p); });
browser["back"] = (Action)(() => { engine.Tick(); host.BrowserBack(); });
browser["forward"] = (Action)(() => { engine.Tick(); host.BrowserForward(); });
browser["reload"] = (Action)(() => { engine.Tick(); host.BrowserReload(); });
g["browser"] = browser;
// ===================== 全局简写 =====================
g["moveTo"] = mouse["moveTo"];
g["moveR"] = mouse["moveR"];
g["click"] = mouse["click"];
g["leftClick"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
host.MouseClick("left", x, y);
return DynValue.Nil;
});
g["rightClick"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
host.MouseClick("right", x, y);
return DynValue.Nil;
});
g["middleClick"] = DynValue.NewCallback((ctx, a) =>
{
engine.Tick();
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
host.MouseClick("middle", x, y);
return DynValue.Nil;
});
g["doubleClick"] = mouse["doubleClick"];
g["wheel"] = mouse["wheel"];
g["drag"] = mouse["drag"];
g["keyPress"] = key["press"];
g["keyDown"] = key["down"];
g["keyUp"] = key["up"];
g["hotkey"] = key["hotkey"];
g["typeText"] = key["type"];
g["sayText"] = key["type"];
g["findImage"] = screen["findImage"];
g["findColor"] = screen["findColor"];
g["waitImage"] = screen["waitImage"];
g["exists"] = screen["exists"];
g["colorAt"] = screen["color"];
g["screenshot"] = screen["shot"];
g["run"] = win["launch"];
g["activate"] = win["activate"];
g["clipGet"] = clip["get"];
g["clipSet"] = clip["set"];
}
private static string NormBtn(string b) => b.Trim().ToLowerInvariant() switch
{
"右" or "right" or "r" or "rightclick" => "right",
"中" or "middle" or "m" => "middle",
_ => "left"
};
}
}