Skip to content

Commit 083045f

Browse files
committed
feat(FishNew): 新增钓鱼预期收益识别
1 parent d8922c1 commit 083045f

4 files changed

Lines changed: 566 additions & 11 deletions

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""
2+
钓鱼收益统计 Custom Action
3+
- 在钓鱼结果界面截图,OCR 识别鱼名
4+
- 模糊匹配价格表,容忍OCR错别字
5+
- 查表获取贝壳价值,累计统计
6+
- 通过 focus 推送到前端日志
7+
"""
8+
9+
import json
10+
from pathlib import Path
11+
12+
from maa.agent.agent_server import AgentServer
13+
from maa.custom_action import CustomAction
14+
from maa.context import Context
15+
16+
17+
# 价格表路径
18+
_PRICE_TABLE_PATH = Path(__file__).parents[4] / "assets" / "resource" / "base" / "fish_price_table.json"
19+
if not _PRICE_TABLE_PATH.exists():
20+
_PRICE_TABLE_PATH = Path(__file__).parents[4] / "resource" / "base" / "fish_price_table.json"
21+
22+
# 加载价格表
23+
def _load_price_table() -> dict:
24+
"""加载价格表,返回 {鱼名: avg价格} 的扁平dict"""
25+
if _PRICE_TABLE_PATH.exists():
26+
with open(_PRICE_TABLE_PATH, "r", encoding="utf-8") as f:
27+
raw = json.load(f)
28+
result = {}
29+
for name, val in raw.items():
30+
if isinstance(val, dict):
31+
result[name] = val.get("avg", 0)
32+
else:
33+
result[name] = val
34+
return result
35+
return {}
36+
37+
38+
def _edit_distance(s1: str, s2: str) -> int:
39+
"""计算两个字符串的编辑距离"""
40+
m, n = len(s1), len(s2)
41+
dp = list(range(n + 1))
42+
for i in range(1, m + 1):
43+
prev = dp[0]
44+
dp[0] = i
45+
for j in range(1, n + 1):
46+
temp = dp[j]
47+
if s1[i-1] == s2[j-1]:
48+
dp[j] = prev
49+
else:
50+
dp[j] = 1 + min(prev, dp[j], dp[j-1])
51+
prev = temp
52+
return dp[n]
53+
54+
55+
def _fuzzy_match(ocr_text: str, name_list: list, max_dist: int = 2) -> str | None:
56+
"""模糊匹配:在鱼名列表中找编辑距离最小的,超过阈值则返回None"""
57+
if not ocr_text:
58+
return None
59+
# 精确匹配优先
60+
if ocr_text in name_list:
61+
return ocr_text
62+
# 模糊匹配
63+
best_name = None
64+
best_dist = max_dist + 1
65+
for name in name_list:
66+
# 长度差太大直接跳过
67+
if abs(len(name) - len(ocr_text)) > max_dist:
68+
continue
69+
dist = _edit_distance(ocr_text, name)
70+
if dist < best_dist:
71+
best_dist = dist
72+
best_name = name
73+
return best_name if best_dist <= max_dist else None
74+
75+
76+
@AgentServer.custom_action("fish_catch_logger")
77+
class FishCatchLogger(CustomAction):
78+
# 类变量:跨轮次累计
79+
_total_count: int = 0
80+
_total_shells: int = 0
81+
_catch_log: dict = {} # {鱼名: 数量}
82+
_price_table: dict = _load_price_table()
83+
_initialized: bool = False # 标记本轮是否已初始化
84+
85+
def run(
86+
self, context: Context, argv: CustomAction.RunArg
87+
) -> CustomAction.RunResult:
88+
"""在钓鱼结果界面,OCR识别鱼名并累计统计"""
89+
90+
# 首次调用时 reset(确保每次启动任务从0开始)
91+
if not FishCatchLogger._initialized:
92+
FishCatchLogger.reset()
93+
FishCatchLogger._initialized = True
94+
95+
# 截图
96+
image = context.tasker.controller.post_screencap().wait().get()
97+
98+
# OCR 识别鱼名
99+
fish_name = self._recognize_fish_name(context, image)
100+
101+
if fish_name:
102+
price = self._price_table.get(fish_name, 0)
103+
FishCatchLogger._total_count += 1
104+
FishCatchLogger._total_shells += price
105+
FishCatchLogger._catch_log[fish_name] = FishCatchLogger._catch_log.get(fish_name, 0) + 1
106+
msg = f"第{FishCatchLogger._total_count}{fish_name},累计预期收益{FishCatchLogger._total_shells}贝壳"
107+
else:
108+
FishCatchLogger._total_count += 1
109+
msg = f"第{FishCatchLogger._total_count}条(识别失败),累计预期收益{FishCatchLogger._total_shells}贝壳"
110+
111+
# 通过 focus 推送到前端
112+
try:
113+
context.override_pipeline({
114+
"FishCatchLogger_Notify": {
115+
"recognition": "DirectHit",
116+
"action": "DoNothing",
117+
"focus": {
118+
"Node.Action.Succeeded": {
119+
"content": msg,
120+
"display": ["log", "toast"]
121+
}
122+
}
123+
}
124+
})
125+
context.run_task("FishCatchLogger_Notify")
126+
except Exception as e:
127+
print(f"[FishLog] focus推送失败: {e}")
128+
129+
# 按 Esc 关闭结果界面
130+
context.tasker.controller.post_click_key(27).wait()
131+
132+
return CustomAction.RunResult(success=True)
133+
134+
def _recognize_fish_name(self, context: Context, image) -> str:
135+
"""OCR 识别鱼名,并模糊匹配到价格表"""
136+
reco_detail = context.run_recognition(
137+
"FishCatchLogger_OCR_FishName",
138+
image,
139+
pipeline_override={
140+
"FishCatchLogger_OCR_FishName": {
141+
"recognition": "OCR",
142+
"roi": [440, 120, 400, 50],
143+
"expected": [],
144+
"only_rec": False
145+
}
146+
}
147+
)
148+
149+
if reco_detail and reco_detail.all_results:
150+
best = reco_detail.all_results[0]
151+
ocr_text = best.text.strip() if hasattr(best, 'text') else ""
152+
if not ocr_text:
153+
return None
154+
# 模糊匹配价格表中的鱼名
155+
matched = _fuzzy_match(ocr_text, list(self._price_table.keys()))
156+
return matched
157+
return None
158+
159+
@classmethod
160+
def get_summary(cls) -> str:
161+
"""获取当前累计汇总文本"""
162+
if cls._total_count == 0:
163+
return "本轮未钓到鱼"
164+
summary = f"🎣 本轮钓鱼: {cls._total_count}条"
165+
if cls._total_shells > 0:
166+
summary += f" | 预计收益: {cls._total_shells}贝壳"
167+
return summary
168+
169+
@classmethod
170+
def reset(cls):
171+
"""重置统计"""
172+
cls._total_count = 0
173+
cls._total_shells = 0
174+
cls._catch_log = {}
175+
176+
177+
@AgentServer.custom_action("fish_catch_summary")
178+
class FishCatchSummary(CustomAction):
179+
"""任务结束时上报钓鱼收益汇总到前端"""
180+
181+
def run(
182+
self, context: Context, argv: CustomAction.RunArg
183+
) -> CustomAction.RunResult:
184+
summary = FishCatchLogger.get_summary()
185+
186+
# 推送汇总到前端
187+
try:
188+
context.override_pipeline({
189+
"FishCatchSummary_Notify": {
190+
"recognition": "DirectHit",
191+
"action": "DoNothing",
192+
"focus": {
193+
"Node.Action.Succeeded": {
194+
"content": summary,
195+
"display": ["log", "toast"]
196+
}
197+
}
198+
}
199+
})
200+
context.run_task("FishCatchSummary_Notify")
201+
except Exception as e:
202+
print(f"[FishLog] 汇总推送失败: {e}")
203+
204+
# 重置计数(为下一轮准备)
205+
FishCatchLogger.reset()
206+
FishCatchLogger._initialized = False
207+
208+
return CustomAction.RunResult(success=True)

agent/custom/action/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .AutoFish.auto_fish import *
22
from .AutoFish.auto_buy_fish_bait import *
33
from .AutoFish.auto_sell_fish import *
4+
from .AutoFish.fish_catch_logger import *
45
from .auto_make_coffee import *
56
from .rhythm.feats.play import *
67
from .rhythm.feats.repeat_decision import *
@@ -27,6 +28,8 @@
2728
"AutoFish",
2829
"AutoBuyFishBait",
2930
"AutoSellFish",
31+
"FishCatchLogger",
32+
"FishCatchSummary",
3033
"ClickOverride",
3134
"AutoTetris",
3235
"AutoRhythmPlay",

0 commit comments

Comments
 (0)