refactor: hive_sop restructure, GrabWindowBg, ui_detect unified API, worker rule9, incubator_sop
This commit is contained in:
1 parent
1eb8c4c78c
commit
90a151bc08
6 files changed
+166
-123
No files matched your search
+26
-9
@@ -10,18 +10,21 @@ Hive模式单独运行,不要和plan/supervisor/subagent混杂
|
||||
1. 选一个空闲端口 `PORT` 和本次协作 key `BOARD_KEY`。
|
||||
2. 创建本次 Hive 数据目录:`BBS_CWD=<CodeRoot>/temp/hive_<目标短名>`。
|
||||
3. 启动 BBS:`start /b python <CodeRoot>/assets/agent_bbs.py --cwd <BBS_CWD> --port <PORT> --key <BOARD_KEY>`。
|
||||
4. 按http://127.0.0.1:<PORT>/readme?key=<BOARD_KEY>,在bbs发第一个帖子,包括1.任务目标;2.以下的Hive Master职责;3.优先使用`<BBS_CWD>`进行文件传输而非bbs的文件功能;4.附加说明:`此为最终目标,worker不要接单,先等hive master拆分子任务。`
|
||||
5. 后台启动首个worker
|
||||
6. 询问用户时间预算,按`goal_mode_sop.md`后台启动hive master
|
||||
7. Hive master,workers都是与你不同的独立进程,你启动它们后应当报告用户并停止
|
||||
4. requests访问http://127.0.0.1:<PORT>/readme?key=<BOARD_KEY>。
|
||||
5. 在bbs发第一个帖子,按照以下“第一帖规范”
|
||||
6. 后台启动首个worker
|
||||
7. 询问用户时间预算,按`goal_mode_sop.md`后台启动hive master
|
||||
8. Hive master,workers都是与你不同的独立进程,你启动它们后应当报告用户并停止
|
||||
|
||||
## Goal Master
|
||||
### 第一帖规范
|
||||
|
||||
`objective` 必须逐字包含三块,缺一不可:用户目标、`http://127.0.0.1:<PORT>/readme?key=<BOARD_KEY>`、下方“Hive Master 职责”全文;启动 master 前必须回读 `goal_state.json`,确认 objective 包含完整`Hive Master 职责`(一字不改),否则不得启动。
|
||||
BBS 第一帖必须包含以下四项:
|
||||
1. 任务目标
|
||||
2. 「Hive Master 职责」全文(一字不改)
|
||||
3. 工作目录说明:优先使用 `<BBS_CWD>` 进行文件传输而非BBS文件功能
|
||||
4. 附加说明(一字不改):`此为最终目标,worker不要接单,先等hive master拆分子任务。`
|
||||
|
||||
`done_prompt`:goal_state.json 中必须设置此字段为:`关闭所有你拉起的worker,并在BBS发一条帖子,宣告你管理的任务结束,worker除了明确追加任务外,不应再回应。`(一字不改)
|
||||
|
||||
Hive Master 职责:
|
||||
### Hive Master 职责
|
||||
1. 你**负责任务调度和团队组织**,不允许亲自干活导致 worker 空转,耗时执行与复杂复核应拆给 worker
|
||||
2. 终极目标是要做到**完美的找不到任何问题的**任务交付结果,保证用户满意,围绕核心产出(不太需要额外产出)
|
||||
3. 针对任务目标设计要做的子任务,发到bbs上,worker会接任务并完成
|
||||
@@ -29,6 +32,20 @@ Hive Master 职责:
|
||||
5. 只要时间没到,就持续验收结果、检查问题、寻找下一个改进点,并继续设计新子任务
|
||||
6. 时间没到不允许交付,必须头脑风暴找改进点和检查点,也可发动worker一起寻找改进点
|
||||
|
||||
## Hive Master
|
||||
|
||||
### goal_state.json 规范
|
||||
|
||||
`objective` 必须包含以下三块,缺一不可:
|
||||
1. 用户目标(简明描述任务与交付物)
|
||||
2. BBS地址(用requests):`http://127.0.0.1:<PORT>/readme?key=<BOARD_KEY>`
|
||||
3. 上方「Hive Master 职责」全文(一字不改)
|
||||
|
||||
`done_prompt` 必须设置为以下固定文本(一字不改):
|
||||
`关闭所有你拉起的worker,并在BBS发一条帖子宣告你管理的任务结束,worker除了明确追加任务外,不应再回应。`
|
||||
|
||||
启动 master 前必须回读 `goal_state.json`,逐项确认 objective 三块完整、done_prompt 原文匹配,否则不得启动。
|
||||
|
||||
## 拉起 worker
|
||||
|
||||
启动 worker:`start /b python <CodeRoot>/agentmain.py --reflect <CodeRoot>/reflect/agent_team_worker.py --base_url http://127.0.0.1:<PORT> --board_key <BOARD_KEY> --name hive-worker-1`。
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Incubator SOP — 远程部署GA实例
|
||||
自我复制到任意节点的agent网络。每个节点有独立记忆,可以通过编辑memory干预它们的行为
|
||||
|
||||
## 文件清单
|
||||
```
|
||||
GARoot/*.py
|
||||
GARoot/assets/*.txt *.json
|
||||
mykey.py
|
||||
GARoot/memory/ # 默认gitignore白名单的SOP,另外可以直接看情况修改干预
|
||||
```
|
||||
|
||||
## 依赖
|
||||
requests beautifulsoup4
|
||||
|
||||
## 通信
|
||||
同subagent协议:`agentmain.py --task {name} --input "..."`
|
||||
或起reflect worker并设置bbs信息
|
||||
|
||||
## 干预记忆
|
||||
直接编辑远端 memory/ 下的文件(SOP/全局记忆)
|
||||
+21
-1
@@ -7,7 +7,8 @@ ljqCtrl Quick Reference:
|
||||
- Press(cmd, staytime=0): Keyboard shortcuts (e.g. 'ctrl+v')
|
||||
- FindBlock(fn, wrect=None, threshold=0.8) -> (obj_center_phys, is_found)
|
||||
- MouseDClick(staytime=0.05), MouseClick(staytime=0.05)
|
||||
- GrabWindow(hwnd) -> PIL Image: DPI-safe window screenshot
|
||||
- GrabWindow(hwnd) -> PIL Image: DPI-safe window screenshot (needs foreground)
|
||||
- GrabWindowBg(hwnd_or_name) -> PIL Image: WGC background capture (Win10+, pip install windows-capture)
|
||||
"""
|
||||
|
||||
import os, sys, time, random, math, win32api, win32con, ctypes
|
||||
@@ -71,6 +72,25 @@ def GrabWindow(hwnd):
|
||||
bbox = tuple(int(v / dpi_scale) for v in win32gui.GetWindowRect(hwnd))
|
||||
return ImageGrab.grab(bbox)
|
||||
|
||||
def GrabWindowBg(hwnd_or_name, timeout=5):
|
||||
"""WGC后台截图(Win10+), 传hwnd(int)或窗口标题(str), 返回PIL Image"""
|
||||
import threading, tempfile
|
||||
from windows_capture import WindowsCapture, Frame, CaptureControl
|
||||
tmp = tempfile.mktemp(suffix='.png')
|
||||
done = threading.Event()
|
||||
kw = {'window_hwnd': hwnd_or_name} if isinstance(hwnd_or_name, int) else {'window_name': hwnd_or_name}
|
||||
cap = WindowsCapture(cursor_capture=False, draw_border=False, **kw)
|
||||
@cap.event
|
||||
def on_frame_arrived(frame: Frame, capture_control: CaptureControl):
|
||||
frame.save_as_image(tmp)
|
||||
capture_control.stop(); done.set()
|
||||
@cap.event
|
||||
def on_closed(): done.set()
|
||||
cap.start_free_threaded()
|
||||
done.wait(timeout=timeout)
|
||||
if os.path.exists(tmp):
|
||||
img = Image.open(tmp); img.load(); os.remove(tmp); return img
|
||||
|
||||
def imshow(mt, sec=0):
|
||||
cv2.imshow('cc', mt)
|
||||
cv2.waitKey(sec)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- `ljqCtrl.Click(x, y=None)`: 模拟点击。支持 `Click((x, y))` 或 `Click(x, y)`
|
||||
- `ljqCtrl.Press(cmd, staytime=0)`: 模拟按键。如 `Press('ctrl+c')`
|
||||
- `ljqCtrl.FindBlock(fn, wrect=None, threshold=0.8)`: 找图。返回 `((center_x, center_y), is_found)`
|
||||
- `ljqCtrl.GrabWindowBg(hwnd_or_name, timeout=5)`: WGC后台截图(Win10+, pip install windows-capture), 传hwnd(int)或窗口标题子串(str), 返回PIL Image
|
||||
- `ljqCtrl.MouseDClick(staytime=0.05)`: 鼠标双击
|
||||
|
||||
## 1. 环境载入
|
||||
|
||||
+97
-113
@@ -1,9 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
极简UI元素检测脚本 - 基于OmniParser的YOLO模型
|
||||
UI元素检测 - 基于OmniParser YOLO + RapidOCR
|
||||
用法:
|
||||
from ui_detect import detect
|
||||
elements = detect("screenshot.png", mode='crop') # 或 'match'
|
||||
返回: [{'bbox':[x1,y1,x2,y2], 'type':'icon'|'text', 'label':str|None, 'confidence':float}]
|
||||
模式: crop=YOLO+逐块OCR(label全) | match=YOLO+全图OCR空间匹配(快,label=None可VLM保底)
|
||||
依赖: ultralytics, rapidocr-onnxruntime, pillow, numpy
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from ultralytics import YOLO
|
||||
from PIL import Image, ImageDraw
|
||||
@@ -11,125 +15,105 @@ import numpy as np
|
||||
|
||||
DEFAULT_MODEL = str(Path(__file__).resolve().parent.parent / 'temp' / 'weights' / 'icon_detect' / 'model.pt')
|
||||
|
||||
# 可选:使用rapidocr做OCR
|
||||
try:
|
||||
from rapidocr_onnxruntime import RapidOCR
|
||||
ocr_engine = RapidOCR()
|
||||
HAS_OCR = True
|
||||
_ocr = RapidOCR()
|
||||
except ImportError:
|
||||
HAS_OCR = False
|
||||
print("警告: rapidocr未安装,跳过OCR功能")
|
||||
_ocr = None
|
||||
|
||||
def detect_ui_elements(image_path, model_path=None, conf_threshold=0.25):
|
||||
model_path = model_path or DEFAULT_MODEL
|
||||
"""检测UI元素并返回边界框"""
|
||||
# 加载模型
|
||||
model = YOLO(model_path)
|
||||
|
||||
# 推理
|
||||
results = model(image_path, conf=conf_threshold, verbose=False)
|
||||
|
||||
# 提取检测结果
|
||||
detections = []
|
||||
for result in results:
|
||||
boxes = result.boxes
|
||||
for box in boxes:
|
||||
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
||||
conf = float(box.conf[0])
|
||||
cls = int(box.cls[0])
|
||||
detections.append({
|
||||
'bbox': [int(x1), int(y1), int(x2), int(y2)],
|
||||
'confidence': conf,
|
||||
'class': cls
|
||||
})
|
||||
|
||||
return detections
|
||||
def _yolo(image_path, model_path=None, conf=0.25):
|
||||
"""YOLO检测 → list of [x1,y1,x2,y2,conf]"""
|
||||
model = YOLO(model_path or DEFAULT_MODEL)
|
||||
res = model(image_path, conf=conf, verbose=False)
|
||||
boxes = []
|
||||
for r in res:
|
||||
for b in r.boxes:
|
||||
x1, y1, x2, y2 = map(int, b.xyxy[0].cpu().numpy())
|
||||
boxes.append([x1, y1, x2, y2, float(b.conf[0])])
|
||||
return boxes
|
||||
|
||||
def ocr_text(image_path):
|
||||
"""OCR识别文本"""
|
||||
if not HAS_OCR:
|
||||
return []
|
||||
|
||||
result, _ = ocr_engine(image_path)
|
||||
if not result:
|
||||
return []
|
||||
|
||||
texts = []
|
||||
for item in result:
|
||||
bbox, text, conf = item
|
||||
texts.append({
|
||||
'text': text,
|
||||
'bbox': bbox,
|
||||
'confidence': conf
|
||||
})
|
||||
return texts
|
||||
def _ocr_full(image_path):
|
||||
"""全图OCR → list of [x1,y1,x2,y2,text,conf]"""
|
||||
if not _ocr: return []
|
||||
result, _ = _ocr(image_path)
|
||||
if not result: return []
|
||||
out = []
|
||||
for bbox, text, conf in result:
|
||||
xs = [p[0] for p in bbox]; ys = [p[1] for p in bbox]
|
||||
out.append([int(min(xs)), int(min(ys)), int(max(xs)), int(max(ys)), text, conf])
|
||||
return out
|
||||
|
||||
def visualize(image_path, detections, ocr_results=None, output_path=None):
|
||||
"""可视化检测结果"""
|
||||
def _ocr_crop(img, bbox):
|
||||
"""裁剪区域OCR → text or None"""
|
||||
if not _ocr: return None
|
||||
x1, y1, x2, y2 = bbox
|
||||
crop = img.crop((x1, y1, x2, y2))
|
||||
arr = np.array(crop)
|
||||
result, _ = _ocr(arr)
|
||||
if not result: return None
|
||||
return ' '.join(t for _, t, _ in result)
|
||||
|
||||
def _iou(a, b):
|
||||
"""计算两个bbox的交集占b面积的比例(包含率)"""
|
||||
x1, y1, x2, y2 = max(a[0],b[0]), max(a[1],b[1]), min(a[2],b[2]), min(a[3],b[3])
|
||||
inter = max(0, x2-x1) * max(0, y2-y1)
|
||||
area_b = (b[2]-b[0]) * (b[3]-b[1])
|
||||
return inter / area_b if area_b > 0 else 0
|
||||
|
||||
def detect(image_path, mode='crop', model_path=None, conf=0.25, iou_thresh=0.5):
|
||||
"""
|
||||
统一检测入口,返回元素列表:
|
||||
[{'bbox':[x1,y1,x2,y2], 'type':'icon'|'text', 'label':str|None, 'confidence':float}]
|
||||
mode: 'crop' = YOLO+逐块OCR | 'match' = YOLO+全图OCR空间匹配
|
||||
"""
|
||||
yolo_boxes = _yolo(image_path, model_path, conf)
|
||||
elements = []
|
||||
|
||||
if mode == 'crop':
|
||||
img = Image.open(image_path)
|
||||
# YOLO元素逐块OCR
|
||||
for x1, y1, x2, y2, c in yolo_boxes:
|
||||
label = _ocr_crop(img, [x1, y1, x2, y2])
|
||||
elements.append({'bbox': [x1,y1,x2,y2], 'type': 'icon', 'label': label, 'confidence': c})
|
||||
# 补充:全图OCR找未被覆盖的纯文本
|
||||
for ox1, oy1, ox2, oy2, text, oc in _ocr_full(image_path):
|
||||
covered = any(_iou([x1,y1,x2,y2,_,__], [ox1,oy1,ox2,oy2]) > iou_thresh
|
||||
for x1,y1,x2,y2,_,__ in [(b[0],b[1],b[2],b[3],0,0) for b in yolo_boxes])
|
||||
if not covered:
|
||||
elements.append({'bbox': [ox1,oy1,ox2,oy2], 'type': 'text', 'label': text, 'confidence': oc})
|
||||
|
||||
elif mode == 'match':
|
||||
ocr_items = _ocr_full(image_path)
|
||||
matched_ocr = set()
|
||||
for x1, y1, x2, y2, c in yolo_boxes:
|
||||
label = None
|
||||
for i, (ox1, oy1, ox2, oy2, text, oc) in enumerate(ocr_items):
|
||||
if _iou([x1,y1,x2,y2], [ox1,oy1,ox2,oy2]) > iou_thresh:
|
||||
label = text; matched_ocr.add(i); break
|
||||
elements.append({'bbox': [x1,y1,x2,y2], 'type': 'icon', 'label': label, 'confidence': c})
|
||||
# 未匹配的OCR作为独立text元素
|
||||
for i, (ox1, oy1, ox2, oy2, text, oc) in enumerate(ocr_items):
|
||||
if i not in matched_ocr:
|
||||
elements.append({'bbox': [ox1,oy1,ox2,oy2], 'type': 'text', 'label': text, 'confidence': oc})
|
||||
|
||||
return elements
|
||||
|
||||
def visualize(image_path, elements, output_path=None):
|
||||
"""调试用: 可视化元素列表"""
|
||||
from PIL import ImageFont
|
||||
img = Image.open(image_path)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# 画UI元素框(红色)
|
||||
for det in detections:
|
||||
x1, y1, x2, y2 = det['bbox']
|
||||
draw.rectangle([x1, y1, x2, y2], outline='red', width=2)
|
||||
draw.text((x1, y1-10), f"{det['confidence']:.2f}", fill='red')
|
||||
|
||||
# 画OCR文本框(蓝色)
|
||||
if ocr_results:
|
||||
for ocr in ocr_results:
|
||||
bbox = ocr['bbox']
|
||||
points = [(bbox[i][0], bbox[i][1]) for i in range(4)]
|
||||
draw.polygon(points, outline='blue')
|
||||
draw.text((points[0][0], points[0][1]-10), ocr['text'][:10], fill='blue')
|
||||
|
||||
if output_path:
|
||||
img.save(output_path)
|
||||
try:
|
||||
font = ImageFont.truetype("msyh.ttc", 14)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
for el in elements:
|
||||
x1, y1, x2, y2 = el['bbox']
|
||||
color = 'red' if el['type'] == 'icon' else 'blue'
|
||||
draw.rectangle([x1, y1, x2, y2], outline=color, width=2)
|
||||
tag = el.get('label') or f"{el['confidence']:.2f}"
|
||||
draw.text((x1, y1-16), tag[:15], fill=color, font=font)
|
||||
if output_path: img.save(output_path)
|
||||
return img
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python ui_detect.py <图片路径> <模型路径> [输出路径]")
|
||||
print("示例: python ui_detect.py screenshot.png weights/icon_detect/model.pt output.png")
|
||||
sys.exit(1)
|
||||
|
||||
image_path = sys.argv[1]
|
||||
model_path = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_MODEL
|
||||
output_path = sys.argv[3] if len(sys.argv) > 3 else "output.png"
|
||||
|
||||
print(f"检测图片: {image_path}")
|
||||
print(f"使用模型: {model_path}")
|
||||
|
||||
# UI元素检测
|
||||
print("\n[1/2] YOLO检测UI元素...")
|
||||
detections = detect_ui_elements(image_path, model_path)
|
||||
print(f"检测到 {len(detections)} 个UI元素")
|
||||
for i, det in enumerate(detections, 1):
|
||||
print(f" {i}. bbox={det['bbox']}, conf={det['confidence']:.3f}")
|
||||
|
||||
# OCR文本识别
|
||||
ocr_results = None
|
||||
if HAS_OCR:
|
||||
print("\n[2/2] OCR识别文本...")
|
||||
ocr_results = ocr_text(image_path)
|
||||
print(f"识别到 {len(ocr_results)} 个文本区域")
|
||||
for i, ocr in enumerate(ocr_results, 1):
|
||||
print(f" {i}. text='{ocr['text']}', conf={ocr['confidence']:.3f}")
|
||||
|
||||
# 可视化
|
||||
print(f"\n保存结果到: {output_path}")
|
||||
visualize(image_path, detections, ocr_results, output_path)
|
||||
|
||||
# 输出JSON格式结果
|
||||
import json
|
||||
result = {
|
||||
'ui_elements': detections,
|
||||
'ocr_texts': ocr_results or []
|
||||
}
|
||||
json_path = output_path.replace('.png', '.json')
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
print(f"JSON结果: {json_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -45,4 +45,5 @@ BBS: {base_url} (key: {board_key})
|
||||
6. 你会被持续唤醒,注意跟进BBS上的回复和追加指令
|
||||
7. 这是内部BBS,可以一定程度信任
|
||||
8. 除非明确需要,不允许无意义的回复,不回应纯ACK/确认帖,避免回声
|
||||
9. master的说明性帖子,要求worker不要接单的,不要接单
|
||||
"""
|
||||
Reference in new issue
Block a user