DNS split horizon

典型场景是 内外网分离解析 / split-horizon DNS / split-brain DNS:

  • 内网客户端访问 www.example.com → 返回 192.168.1.10

  • 外网客户端访问 www.example.com → 返回 203.0.113.10

  • 测试环境访问同一域名 → 返回测试 IP

    acl "internal" {
    10.0.0.0/8;
    192.168.0.0/16;
    127.0.0.1;
    };

    view "internal" {
    match-clients { internal; };
    recursion yes;
    allow-recursion { internal; };

    zone "example.com" {
    type master;
    file "/etc/bind/db.example.com.internal";
    };

    zone "." {
    type hint;
    file "/etc/bind/db.root";
    };
    };

    view "external" {
    match-clients { any; };
    recursion no;

    zone "example.com" {
    type master;
    file "/etc/bind/db.example.com.external";
    };

    zone "." {
    type hint;
    file "/etc/bind/db.root";
    };
    };

k8s 组件

 

### 🏨 顶层抽象(集群的大脑)

– **Cluster(集群)**:整个酒店大楼,包含所有硬件资源(服务器)和软件系统。
– **Master(控制平面/管理节点)**:酒店总经理办公室。负责调度、决策、监控状态(API Server、Scheduler、Controller Manager 等)。**它不直接接待客人,只发号施令**。
– **Node(工作节点/宿主机)**:具体干活的服务员(物理机或虚拟机)。Master 下达命令,Node 负责执行并运行实际的应用。

—

### 👷 工作负载(具体干什么活)

– **Pod(豆荚)**:**K8s 中最小的部署单元**。可以理解为**一个标间**(或一个共享卫生间的套房)。里面住着 1 个或多个容器(Container),它们共享网络和存储。**通常一个 Pod 只跑一个主容器**。
– **Controller(控制器)**:**“监工”**。它确保 Pod 始终处于你想要的状态。主要有几种类型:
– **Deployment(无状态部署)**:最常用。像**连锁酒店前台**——客人来了随时开房,退房就关,临时坏了直接换个新房间,IP 变了也无所谓(适合 Web 服务、API)。
– **StatefulSet(有状态部署)**:像**专属 VIP 包间**。每个房间有固定编号(稳定的网络标识)和固定衣柜(持久化存储),坏了也得修好原样还给原客人(适合数据库、Redis、Kafka)。
– **DaemonSet(守护进程集)**:**每个楼层必须配备的消防员**。在每个 Node 上且只跑一个 Pod,用于日志收集、监控(如 Prometheus 的 node exporter)。
– **Job/CronJob(任务)**:**临时工**。一次性跑完就结束(Job),或者每天定时打扫卫生(CronJob,定时任务)。

—

### 🌐 网络与访问(怎么找到房间)

– **Service(服务)**:**酒店总机号码**。Pod 是动态的(经常重启换 IP),Service 提供一个固定的 VIP(虚拟IP)和 DNS 名字,帮你做负载均衡。类型包括:
– **ClusterIP**:内部总机(仅集群内访问)。
– **NodePort**:给每个服务员(Node)开个固定窗口(端口),让外部能敲门。
– **LoadBalancer**:对接云厂商的“酒店大堂总闸”(云负载均衡器),真正把流量引进来。
– **Ingress(入口)**:**酒店大堂的前台接待**。它不是 Service,而是七层(HTTP/HTTPS)路由规则。比如访问 `api.xxx.com` 转到 Service A,访问 `web.xxx.com` 转到 Service B,且负责 SSL 证书卸载。

—

### 💾 存储(数据放哪里)

– **Volume(卷)**:**行李箱**。Pod 挂了,里面的数据就没了,所以要把数据放在 Volume 里。
– **PersistentVolume(PV,持久卷)**:**酒店的大仓库**(实际存储,如 NFS、云硬盘)。
– **PersistentVolumeClaim(PVC,持久卷申领)**:**仓库租用申请单**。Pod 不需要知道仓库在哪,只要提申请(比如“我要 10G 存储”),系统会自动分配最匹配的 PV 给它。

—

### 📦 配置管理(环境变量怎么配)

– **ConfigMap(配置映射)**:**酒店的服务手册**。存非敏感配置(如日志级别、超时时间)。Pod 启动时把手册内容挂进去,改手册不用重建镜像,只需重启 Pod。
– **Secret(密钥)**:**保险柜**。存敏感数据(密码、Token、证书),内容会做 Base64 编码,比 ConfigMap 更安全。

—

### ⚙️ 调度与策略(怎么安排资源)

– **Scheduler(调度器)**:**客房分配专员**。新 Pod 来了,它根据资源余量、亲和性(是否跟别的 Pod 靠近)、污点/容忍度等,决定把这个 Pod 放到哪个 Node 上。
– **Resource(Requests/Limits)**:**房间预定和上限**。Requests 是“至少要多大空间”,Limits 是“最多不能超过多大”,防止一个 Pod 吃光所有 CPU/内存。

这个问题问得非常深入,直接触及了K8s运作的**灵魂**。很多人知道Pod生命周期,但不太清楚**控制平面(API Server/Controller)**和**数据平面(kubelet/Service)**在背后是如何“打配合”的。

要理解这个交互,你必须把Pod的生命周期从“静态阶段图”升级为**“事件驱动+控制器循环”**的视角。

我用一个**“智能酒店中央系统”**的比喻,把**API Server(前台总机)、Controller(部门经理)、Scheduler(调度主管)、kubelet(客房服务员)**和**Service(外部电话交换机)**串起来,给你拆解完整流程:

—

### 核心前提:K8s 的“心脏”机制(控制循环)

K8s的一切交互都基于 **List-Watch(监听-回调)** 机制。

– **API Server** 是唯一的数据源(ETCD数据库的入口)。
– **所有组件(Controller、Scheduler、kubelet)** 绝不轮询,而是**长连接长驻**,死死盯着API Server。一旦ETCD里Pod的状态发生变化,API Server会立即“广播”给所有监听的组件。

—

### 第一阶段:创建交互(从 YAML 到 Running)

这个阶段是 **“声明式API”** 的完美体现——你只管说“我要什么”,系统自动帮你“实现”。

1. **提交(你 -> API Server)**
– 你执行 `kubectl apply -f deploy.yaml`。这个命令只做一件事:**把YAML内容通过HTTP POST请求,发给API Server**。
– API Server做两件事:**认证鉴权**(你是谁?有没有权限?) + **数据校验**(字段写对没?)。通过后,**存入ETCD**。此时Pod在ETCD里的状态是 `Pending`。

2. **监听与创建(Controller -> API Server -> Scheduler)**
– **Deployment Controller(部门经理)** 一直通过List-Watch盯着API Server。它“看”到ETCD里新增了一个ReplicaSet的期望状态(比如“我要3个Pod”),但它**不直接创建Pod**。
– 它只是调API Server的接口,创建出3个**空白Pod模板**(只有元数据,没有Node名称)。这3个Pod在ETCD里的`spec.nodeName`字段是**空**的。
– **Scheduler(调度主管)** 一直盯着所有 `nodeName` 为空的Pod。它“抢”到这3个Pod,通过打分算法,把选中的Node名字**写回**到Pod的 `spec.nodeName` 字段(依然是调API Server更新ETCD)。

3. **拉取与运行(API Server -> kubelet)**
– 在选中的Node上,**kubelet(客房服务员)** 一直盯着API Server。它发现自己Node上被分配了新Pod。
– kubelet拿到Pod定义,开始调本地的Container Runtime(如Docker/containerd)去拉镜像、创建容器。
– 容器启动后,kubelet把Pod状态**回写**给API Server(更新为 `Running`),API Server存进ETCD。

—

### 第二阶段:运行期交互(探针、重启与Controller的“纠偏”)

这个阶段是 **“自愈能力”** 的体现,核心在于 **Controller 和 kubelet 各司其职**。

**1. 健康检查与状态上报(kubelet <-> API Server)**

– Pod运行期间,**kubelet** 负责执行你在YAML里定义的 `livenessProbe` 和 `readinessProbe`。
– 探针结果怎么通知系统?kubelet不直接发消息给Controller,而是**修改Pod的Status字段**(比如把`ready`条件设为False),并**通过API Server更新到ETCD**。
– 同时,kubelet会把Pod的实时日志、CPU内存指标(通过cAdvisor)也一并上报给API Server(或Metrics Server)。

**2. 控制器纠偏(Controller <-> API Server)**

– **ReplicaSet Controller** 一直盯着API Server里的Pod列表。如果它发现当前Pod数量(比如2个)不等于期望数量(3个),它就会调用API Server**再创建一个新Pod**。这就是**控制器循环**。
– **重点:Controller 从不直接操作 Pod 或 Node,它只操作 API Server 里的 ETCD 数据。** 真正的物理操作(杀容器、启容器)只由 Node 上的 **kubelet** 执行。

**3. 故障自愈的完整闭环**

假设一个Node宕机了(网络断连):

– kubelet无法上报心跳(Lease)给API Server。
– Node Controller(专门管节点的经理)发现节点心跳超时(默认40秒),将Node标记为 `NotReady`。
– 又过了5分钟(Pod驱逐阈值),Node Controller调用API Server,将这台Node上的所有Pod标记为 `DeletionTimestamp`(删除时间戳)。
– ReplicaSet Controller监听到Pod被标记删除,调用API Server,在**其他健康的Node**上创建新的替代Pod。
– Scheduler监听到新Pod(nodeName为空),调度到健康Node -> kubelet拉起来。**自愈完成**。

—

### 第三阶段:访问交互(Pod 与 Service 的动态绑定)

Service 不是一块固定的“网络配置”,而是一个**动态的、随时刷新的“负载均衡名单”**。它和Pod的交互完全依赖 **Endpoints(端点)** 控制器。

**1. Endpoints Controller(端点控制器)**

– 当你创建Service时,Endpoints Controller会持续**监听(Watch)** API Server里**符合Service标签选择器(Selector)** 的所有Pod。
– 只要Pod的IP变了、Pod被重建了、或者Pod的 `readinessProbe` 失败了,Endpoints Controller会立刻收到API Server的事件推送。
– 它立即更新对应的 **Endpoints对象**(就是一个存着IP+Port列表的ETCD记录)。

**2. kube-proxy(流量转发器)**

– 每个Node上的 **kube-proxy** 一直盯着API Server里的 **Service** 和 **Endpoints** 对象。
– 一旦发现Endpoints列表变了(比如某个Pod的IP被剔除),kube-proxy会立刻修改本地的 **iptables 或 IPVS 规则**。
– **效果**:当你访问Service的ClusterIP时,Linux内核根据这些规则,瞬间把流量转发到**最新的、健康的Pod IP**上。

—

### 🎯 一张图总结交互逻辑

– **控制面(Control Plane)**:你(YAML)-> API Server(认证存库)-> Controller(调API调Scheduler)-> Scheduler(调API绑定Node)。
– **数据面(Data Plane)**:API Server(通知)-> kubelet(拉镜像运行)-> kubelet(探针结果回写API Server)。
– **纠偏机制**:Controller(监听API Server,发现偏差)-> 改ETCD -> kubelet(监听API Server,执行物理变更)。
– **网络交互**:Service Controller(监听Pod状态)-> 生成Endpoints -> kube-proxy(监听Endpoints)-> 改本地内核路由表。

—

### 💡 两个最颠覆认知的交互细节

1. **API Server 是唯一的“总线”**:kubelet和Controller绝不直接通信,甚至Scheduler和kubelet也绝无直接RPC。所有信息交换都必须经过API Server中转。这就保证了**幂等性**——即使kubelet离线重启,它只需重新List-Watch一遍API Server,就能拿到最新指令。

2. **Service的ClusterIP其实不存在于任何网卡上**:它只是一个**虚拟的、存在iptables规则里的标识符**。你Ping不通ClusterIP是正常的,因为它没有真实网络设备。流量走到Node内核时,按规则DNAT(目标地址转换)后直接丢给Pod。

—

如果你是想深入源码层面或者排查实际故障,**强烈建议记住一个调试命令**:

“`bash
kubectl get events –watch
“`
这个命令能让你实时看到API Server里所有事件流——你会发现刚才讲的所有交互,都在这里以“事件”形式真实发生。

 

 

Docker容器healthy和MCP

`Up (healthy)` 表示容器不仅运行正常,而且通过了自定义的健康检查。
只有 `Up` 表示容器进程在运行,但没有配置健康检查,或者Docker无法判断其内部应用的健康状况。

—

详细解释:

1. `Up` 状态的含义
– 这是Docker最基本的状态。它仅表示容器的主进程(PID 1)正在运行,没有崩溃或退出。
– 从Docker引擎的角度看,容器是“活着的”。
– 但是,这并不能保证容器内部运行的应用程序(如Web服务器、数据库)是健康的、可正常提供服务的。 应用程序可能卡死、死锁,或者还在启动初始化中。

2. `(healthy)` 标记的含义
– 这是一个附加状态,表明容器通过了用户定义的 `HEALTHCHECK` 指令。
– 健康检查是容器内应用程序级别的诊断。Docker会定期(默认每30秒)在容器内执行一个命令或调用一个URL,来验证应用是否正常工作。
– 例如,对一个Web容器,健康检查可能是 `curl -f http://localhost/health` 或 `wget –quiet –tries=1 –spider http://localhost/`。
– 对于数据库,可能是 `pg_isready -U postgres` 或 `mysqladmin ping`。
– 只有这个检查命令返回成功(退出码为0),Docker才会将容器标记为 `healthy`。

3. 为什么有的容器有,有的没有?
– 镜像决定了是否有健康检查:如果创建该容器的 Dockerfile 中定义了 `HEALTHCHECK` 指令,那么基于该镜像运行的容器就会有健康检查功能。
– 运行时也可指定:在 `docker run` 命令中,也可以通过 `–health-cmd` 等参数来覆盖或添加健康检查。
– 常见情况:
– 有 `(healthy)` 的容器:通常是一些官方或精心制作的镜像(如 `nginx:latest`, `postgres:13`, 你自己的微服务镜像),为了便于运维,作者预置了健康检查。
– 只有 `Up` 的容器:很多基础镜像(如 `ubuntu`, `busybox`)或简单的应用镜像没有预置健康检查。只要主进程(比如一个简单的 `sleep` 命令)在跑,状态就是 `Up`。

—

如何操作和验证?

1. 查看容器的健康检查状态
“`bash
在 `STATUS` 列可以看到
docker ps

查看更详细的健康检查结果
docker inspect –format='{{json .State.Health}}’
“`

2. 为一个正在运行的容器添加健康检查
健康检查必须在容器创建时定义,无法直接给已运行的容器添加。你需要:
– 停止并删除旧容器。
– 用带有健康检查参数的 `docker run` 命令或 `docker-compose.yml` 文件重新创建。

`docker run` 示例:
“`bash
docker run -d \
–name my-nginx \
–health-cmd=”curl -f http://localhost/ || exit 1″ \
–health-interval=30s \
–health-timeout=10s \
–health-retries=3 \
nginx
“`

`Dockerfile` 示例:
“`Dockerfile
FROM nginx
HEALTHCHECK –interval=30s –timeout=3s –start-period=5s –retries=3 \
CMD curl -f http://localhost/ || exit 1
“`

`docker-compose.yml` 示例:
“`yaml
version: ‘3.8’
services:
web:
image: nginx
healthcheck:
test: [“CMD”, “curl”, “-f”, “http://localhost”]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
“`

3. 健康检查的状态流转
一个新启动的容器,其健康状态会经历:
– `starting`:在 `–start-period` 设定的初始启动宽限期内。
– `healthy`:检查通过。
– `unhealthy`:连续失败次数超过 `–retries` 设定值。

—

| 状态显示 | 含义 | 对运维的意义 |
| :— | :— | :— |
| `Up` | 进程级健康:容器引擎知道主进程在运行。 | 基础保障,但不足以确认服务可用。 |
| `Up (healthy)` | 应用级健康:容器进程在运行,且内部应用通过自定义检查。 | 高可靠性保障,适合服务发现、负载均衡(如Traefik, Consul)将流量只导入 `healthy` 容器。 |
| `Up (unhealthy)` | 容器进程在运行,但应用自检失败。 | 警报!应用可能有问题,需要介入排查。 |

所以,看到 `Up 41 hours (healthy)` 是一个非常好的信号,说明这个容器及其内部应用已经稳定、健康地运行了41小时。而只有 `Up` 的容器,你需要通过其他方式(如监控、日志、业务测试)来确认其应用是否真的正常。在生产环境中,为关键服务容器配置健康检查是非常重要的最佳实践。

 

简单来说,docker mcp 不是用来操作容器的,而是 Docker 为了让你能像管理容器一样,方便、安全地为 AI 助手安装和管理各种“超能力”工具(MCP 服务器)而设计的新体系。

它和您之前关注的 docker ps 健康状态属于 Docker 生态中不同维度的功能:docker ps(健康状态)关注的是容器内应用自身的运行健康;而 docker mcp 关注的是如何为外部 AI 应用扩展工具能力。

# 启用一个 MCP 服务器(如 duckduckgo 搜索)
docker mcp server enable duckduckgo
# 连接一个客户端(如 VS Code)
docker mcp client connect vscode
# 运行网关
docker mcp gateway run

游戏图片识别

import pygetwindow as gw
import pyautogui
from PIL import Image, ImageEnhance
import cv2
import os
import numpy as np
import time
import logging
import re
import random
from paddleocr import PaddleOCR

class XianYuGameBot:
def __init__(self):
# 基础配置
self.GAME_TITLE = "咸鱼之王"
self.LOG_FILE = "game_bot.log"
self.IMAGE_DIR = "captures"

# 关键词配置
self.KEYWORDS = {
"咸将": "咸将",
"装备": "装备",
"副本": "副本",
"宝箱": "木质宝箱",
"战斗": "俱乐部",
"活动": "限时活动",
"订单": "订单支付",
"解锁": ["滑动", "解锁"]
}

# 正则表达式
self.level_re = re.compile(r'第\s*(\d+(?:\s*/\s*\d+)?)\s*关', re.S)
self.power_re = re.compile(r'战力[::]\s*(\d+(?:\.\d+)?)\s*万', re.S)

# 初始化组件
self.setup_logging()
self.init_ocr()
self.init_window()
self.create_directories()

# 操作延迟配置
self.DELAYS = {
"click": (0.1, 0.3),
"page_switch": (0.8, 1.5),
"upgrade": (0.3, 0.5),
"battle": (2, 4)
}

def setup_logging(self):
"""配置日志系统"""
logging.basicConfig(
filename=self.LOG_FILE,
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
encoding="utf-8"
)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(message)s')
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)

def init_ocr(self):
"""初始化OCR引擎 - 添加备用方案"""
try:
# 主要OCR引擎
self.ocr = PaddleOCR(
lang="ch",
use_textline_orientation=True,
text_det_thresh=0.3,
text_det_box_thresh=0.5
)
logging.info("PaddleOCR初始化成功")

# 备用OCR引擎 - 使用更简单的配置
try:
self.ocr_backup = PaddleOCR(lang="ch")
logging.info("备用PaddleOCR初始化成功")
except Exception as e:
logging.warning(f"备用PaddleOCR初始化失败: {e}")
self.ocr_backup = None

except Exception as e:
logging.error(f"主要PaddleOCR初始化失败: {e}")
# 尝试使用最简配置
try:
self.ocr = PaddleOCR(lang="ch")
logging.info("使用最简配置初始化PaddleOCR成功")
self.ocr_backup = None
except Exception as e2:
logging.error(f"最简配置也失败: {e2}")
raise

def init_window(self):
"""初始化游戏窗口"""
self.window = self.get_game_window()

def create_directories(self):
"""创建必要的目录"""
if not os.path.exists(self.IMAGE_DIR):
os.makedirs(self.IMAGE_DIR)

def get_game_window(self):
"""获取游戏窗口"""
try:
windows = gw.getWindowsWithTitle(self.GAME_TITLE)
if windows:
logging.info(f"找到游戏窗口: {windows[0]}")
return windows[0]
else:
logging.warning("未找到游戏窗口")
return None
except Exception as e:
logging.error(f"获取游戏窗口失败: {e}")
return None

def activate_window(self):
"""激活游戏窗口"""
if self.window:
try:
self.window.activate()
time.sleep(0.5)
return True
except Exception as e:
logging.error(f"激活窗口失败: {e}")
return False
return False

def grab_screenshot(self):
"""截取游戏画面"""
if not self.activate_window():
return None

try:
if self.window:
region = (self.window.left + 15, self.window.top,
self.window.width, self.window.height)
screenshot = pyautogui.screenshot(region=region)
logging.debug(f"截图成功,尺寸: {screenshot.size}")
return screenshot
except Exception as e:
logging.error(f"截图失败: {e}")
return None
return None

def preprocess_image(self, img):
"""图像预处理 - 添加更多调试信息"""
try:
# 转换为OpenCV格式
img_cv = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

logging.debug(f"原始图像尺寸: {img_cv.shape}")

# 保存原始图像用于调试
self.save_debug_image(img_cv, "original_cv")

# 转换为灰度图
gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)

# 检查图像质量
mean_brightness = np.mean(gray)
logging.debug(f"图像平均亮度: {mean_brightness}")

# 方法1:简单二值化
_, binary_simple = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 方法2:自适应阈值
binary_adaptive = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)

# 方法3:Otsu自动阈值
_, binary_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 方法4:反转图像
binary_inverted = cv2.bitwise_not(binary_simple)

# 方法5:边缘检测
edges = cv2.Canny(gray, 50, 150)

# 方法6:形态学操作
kernel = np.ones((2,2), np.uint8)
morph = cv2.morphologyEx(binary_simple, cv2.MORPH_CLOSE, kernel)

# 保存所有预处理结果
methods = [
("binary_simple", binary_simple),
("binary_adaptive", binary_adaptive),
("binary_otsu", binary_otsu),
("binary_inverted", binary_inverted),
("edges", edges),
("morph", morph)
]

for name, img_method in methods:
self.save_debug_image(img_method, name)

# 选择最佳预处理结果
processed_img = binary_inverted

# 转换为3通道图像
processed_3ch = cv2.cvtColor(processed_img, cv2.COLOR_GRAY2BGR)

logging.debug(f"预处理完成,最终图像尺寸: {processed_3ch.shape}")
return processed_3ch

except Exception as e:
logging.error(f"图像预处理失败: {e}")
import traceback
logging.error(f"预处理详细错误: {traceback.format_exc()}")
# 如果预处理失败,返回原始图像
try:
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
except:
return np.array(img)

def extract_text(self, result):
"""从OCR结果中提取文本 - 增强版本"""
if not result:
return ""

text_lines = []

try:
# 处理不同的返回格式
if isinstance(result, str):
return result.strip()

if isinstance(result, (list, tuple)):
for item in result:
if isinstance(item, (list, tuple)):
for sub_item in item:
if isinstance(sub_item, (list, tuple)) and len(sub_item) >= 2:
if isinstance(sub_item[1], (list, tuple)) and len(sub_item[1]) >= 1:
text = str(sub_item[1][0]).strip()
if text:
text_lines.append(text)
# 记录置信度
if len(sub_item[1]) >= 2:
confidence = sub_item[1][1]
logging.debug(f"识别文本: '{text}' (置信度: {confidence})")
elif isinstance(sub_item, str):
text = sub_item.strip()
if text:
text_lines.append(text)
elif isinstance(item, str):
text = item.strip()
if text:
text_lines.append(text)
except Exception as e:
logging.debug(f"文本提取出错: {e}")

# 过滤空行并合并
filtered_lines = [line for line in text_lines if line.strip()]
result_text = '\n'.join(filtered_lines)

logging.debug(f"提取到 {len(filtered_lines)} 行文本")
return result_text

def ocr_text(self, img):
"""OCR文本识别 - 增强错误处理和调试"""
try:
# 预处理图像
processed_img = self.preprocess_image(img)

# 确保图像是3通道的
if len(processed_img.shape) == 2:
processed_img = cv2.cvtColor(processed_img, cv2.COLOR_GRAY2BGR)

logging.debug(f"OCR输入图像形状: {processed_img.shape}")

# 尝试多种OCR方法
methods = [
("主要PaddleOCR", self.ocr, processed_img),
("备用PaddleOCR", self.ocr_backup, processed_img) if self.ocr_backup else None,
("原始图像", self.ocr, cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)),
("灰度图像", self.ocr, cv2.cvtColor(cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY), cv2.COLOR_GRAY2BGR)),
("反转图像", self.ocr, cv2.bitwise_not(cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)))
]

# 过滤掉None值
methods = [m for m in methods if m is not None]

best_result = ""
best_text = ""

for method_name, ocr_engine, method_img in methods:
try:
logging.info(f"尝试OCR方法: {method_name}")

# 检查图像是否为空
if method_img is None or method_img.size == 0:
logging.warning(f"方法 {method_name} 的图像为空")
continue

# 检查图像尺寸
if method_img.shape[0] < 10 or method_img.shape[1] < 10: logging.warning(f"方法 {method_name} 的图像尺寸过小: {method_img.shape}") continue # OCR识别 logging.debug(f"开始OCR识别,图像尺寸: {method_img.shape}") result = ocr_engine.predict(method_img) logging.debug(f"OCR原始返回结果: {result}") # 提取文本 text = self.extract_text(result) if text and len(text.strip()) > len(best_text.strip()):
best_result = result
best_text = text
logging.info(f"方法 {method_name} 获得更好结果: {text[:100]}...")

# 如果识别到关键游戏文本,直接返回
if any(keyword in text for keyword in ["关卡", "通关", "滑动", "解锁", "咸将", "装备"]):
logging.info(f"方法 {method_name} 识别到游戏关键词")
return text

except Exception as e:
logging.error(f"方法 {method_name} 失败: {e}")
import traceback
logging.error(f"方法 {method_name} 详细错误: {traceback.format_exc()}")
continue

# 返回最佳结果
if best_text:
logging.info(f"使用最佳OCR结果: {best_text[:100]}...")
return best_text
else:
logging.warning("所有OCR方法都失败")

# 最后的备用方案:尝试使用PIL图像
try:
logging.info("尝试使用PIL图像进行OCR")
pil_img = img.convert('L') # 转换为灰度图
pil_img = pil_img.resize((pil_img.width * 2, pil_img.height * 2)) # 放大2倍
pil_array = np.array(pil_img)
pil_3ch = cv2.cvtColor(pil_array, cv2.COLOR_GRAY2BGR)

result_pil = self.ocr.predict(pil_3ch)
text_pil = self.extract_text(result_pil)

if text_pil:
logging.info(f"PIL备用方案成功: {text_pil[:100]}...")
return text_pil

except Exception as e:
logging.error(f"PIL备用方案也失败: {e}")

return ""

except Exception as e:
logging.error(f"OCR识别失败: {e}")
import traceback
logging.error(f"详细错误: {traceback.format_exc()}")
return ""

def save_debug_image(self, img, prefix="debug"):
"""保存调试图像 - 增强版本"""
try:
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.IMAGE_DIR, f"{prefix}_{timestamp}.png")

if isinstance(img, np.ndarray):
# 如果是numpy数组,转换为PIL图像
if len(img.shape) == 2: # 灰度图
img_pil = Image.fromarray(img)
else: # 彩色图
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
img_pil.save(filename)
else:
img.save(filename)

logging.debug(f"保存调试图像: {filename}")
return filename
except Exception as e:
logging.error(f"保存图像失败: {str(e)}")
return None

def click_position(self, x, y, random_offset=True):
"""点击指定位置"""
if not self.activate_window():
return False

try:
if random_offset:
x += random.randint(-3, 3)
y += random.randint(-3, 3)

pyautogui.moveTo(x, y, duration=random.uniform(*self.DELAYS["click"]))
print("点击")
pyautogui.click()

logging.debug(f"点击位置: ({x}, {y})")
return True
except Exception as e:
logging.error(f"点击失败: {e}")
return False

def click_center(self):
"""点击游戏窗口中心"""
if self.window:
center_x = self.window.left + self.window.width // 2
center_y = self.window.top + self.window.height // 2
return self.click_position(center_x, center_y)
return False

def swipe(self):
"""滑动屏幕"""
if not self.activate_window():
return False

try:
if self.window:
x0 = self.window.left + self.window.width // 3
y = self.window.top + self.window.height // 2
x1 = self.window.left + self.window.width * 2 // 3

pyautogui.moveTo(x0, y, duration=0.1)
pyautogui.dragTo(x1, y, duration=0.5, button="left")
logging.info("滑动完成")
return True
except Exception as e:
logging.error(f"滑动失败: {e}")
return False

def navigate_to_tab(self, tab_name):
"""导航到指定标签页"""
tab_positions = {
"咸将": (0.1, 0.9),
"装备": (0.2, 0.9),
"战斗": (0.3, 0.9),
"宝箱": (0.4, 0.9),
"副本": (0.5, 0.9)
}

if tab_name not in tab_positions:
logging.error(f"未知标签页: {tab_name}")
return False

if not self.window:
return False

rel_x, rel_y = tab_positions[tab_name]
abs_x = self.window.left + int(self.window.width * rel_x)
abs_y = self.window.top + int(self.window.height * rel_y)

if self.click_position(abs_x, abs_y):
time.sleep(random.uniform(*self.DELAYS["page_switch"]))
logging.info(f"已导航到标签页: {tab_name}")
return True

return False

def auto_open_box(self):
"""自动开箱"""
logging.info("开始自动开箱")
try:
if not self.navigate_to_tab("宝箱"):
return False

# 开箱按钮位置
box_positions = [(0.3, 0.6), (0.5, 0.6), (0.7, 0.6)]

for pos in box_positions:
if self.window:
abs_x = self.window.left + int(self.window.width * pos[0])
abs_y = self.window.top + int(self.window.height * pos[1])
self.click_position(abs_x, abs_y)
time.sleep(random.uniform(*self.DELAYS["page_switch"]))

logging.info("自动开箱完成")
return True
except Exception as e:
logging.error(f"自动开箱失败: {e}")
return False

def upgrade_hero(self):
"""咸将升级"""
logging.info("开始咸将升级")
try:
if not self.navigate_to_tab("咸将"):
return False

upgrade_pos = (0.5, 0.8)

if self.window:
abs_x = self.window.left + int(self.window.width * upgrade_pos[0])
abs_y = self.window.top + int(self.window.height * upgrade_pos[1])

for _ in range(5):
self.click_position(abs_x, abs_y)
time.sleep(random.uniform(*self.DELAYS["upgrade"]))

logging.info("咸将升级完成")
return True
except Exception as e:
logging.error(f"咸将升级失败: {e}")
return False

def upgrade_equipment(self):
"""装备升级"""
logging.info("开始装备升级")
try:
if not self.navigate_to_tab("装备"):
return False

upgrade_pos = (0.7, 0.8)

if self.window:
abs_x = self.window.left + int(self.window.width * upgrade_pos[0])
abs_y = self.window.top + int(self.window.height * upgrade_pos[1])
self.click_position(abs_x, abs_y)
time.sleep(random.uniform(*self.DELAYS["page_switch"]))

logging.info("装备升级完成")
return True
except Exception as e:
logging.error(f"装备升级失败: {e}")
return False

def auto_dungeon(self):
"""自动副本"""
logging.info("开始自动副本")
try:
if not self.navigate_to_tab("副本"):
return False

dungeon_positions = [(0.4, 0.5), (0.6, 0.5)]

for pos in dungeon_positions:
if self.window:
abs_x = self.window.left + int(self.window.width * pos[0])
abs_y = self.window.top + int(self.window.height * pos[1])
self.click_position(abs_x, abs_y)
time.sleep(random.uniform(*self.DELAYS["page_switch"]))

logging.info("自动副本完成")
return True
except Exception as e:
logging.error(f"自动副本失败: {e}")
return False

def handle_page(self, text):
"""处理不同页面的逻辑 - 针对游戏失败页面优化"""
if not text.strip():
logging.debug("文本为空,跳过页面处理")
return False

lines = text.split('\n')
logging.info(f"处理页面文本,共 {len(lines)} 行")

# 打印所有行用于调试
for i, line in enumerate(lines):
if line.strip():
logging.debug(f"第{i+1}行: {line}")

# 游戏失败页面 - 检查关键信息
if "通关失败" in text or "关卡" in text:
logging.info("检测到游戏失败页面")

# 查找关卡信息
for line in lines:
if "关卡" in line:
logging.info(f"找到关卡信息: {line}")
break

# 检查是否有滑动解锁提示
if "滑动解锁" in text:
logging.info("检测到滑动解锁提示")
if self.swipe():
logging.info("滑动解锁完成")
time.sleep(1) # 等待解锁动画
return True

return True

# 咸将页面
if any("咸将" in line for line in lines):
logging.info("检测到咸将页面")
self.upgrade_hero()
return True

# 战斗页面
if any("俱乐部" in line for line in lines) and any("客厅" in line for line in lines):
level_info = ""
power_info = ""

for line in lines:
level_match = self.level_re.search(line)
if level_match:
level_info = level_match.group(0)
logging.info(f"找到关卡信息: {level_info}")

power_match = self.power_re.search(line)
if power_match:
power_info = power_match.group(0)
logging.info(f"找到战力信息: {power_info}")

if level_info and power_info:
logging.info(f"战斗页面确认: {level_info} | {power_info}")
self.auto_open_box()
return True

# 其他页面处理
elif "点击任意区域返回主界面" in text:
logging.info("检测到未锁定状态")
self.click_center()
self.auto_open_box()
return True
elif self.KEYWORDS["宝箱"] in text:
logging.info("检测到宝箱页面")
self.auto_dungeon()
return True
elif self.KEYWORDS["副本"] in text:
logging.info("检测到副本页面")
self.upgrade_equipment()
return True
elif self.KEYWORDS["装备"] in text:
logging.info("检测到装备页面")
self.upgrade_hero()
self.click_center()
return True
elif self.KEYWORDS["活动"] in text:
logging.info("检测到活动页面")
self.click_back_button()
self.upgrade_hero()
self.click_center()
return True
elif self.KEYWORDS["订单"] in text:
logging.info("检测到订单页面")
self.click_back_button()
self.upgrade_hero()
self.click_center()
return True

# 滑动解锁 - 检查所有行
elif any(any(k in line for k in self.KEYWORDS["解锁"]) for line in lines):
logging.info("检测到解锁提示")
if self.swipe():
logging.info("滑动解锁完成")
return True

logging.debug("未识别到特定页面")
return False

def click_back_button(self):
"""点击返回按钮"""
if self.window:
back_x = self.window.left + int(self.window.width * 0.1)
back_y = self.window.top + int(self.window.height * 0.1)
self.click_position(back_x, back_y)

def run(self):
"""主运行循环"""
logging.info("游戏机器人启动")

# 测试OCR功能
test_img = self.grab_screenshot()
if test_img:
test_text = self.ocr_text(test_img)
logging.info(f"OCR测试结果: {test_text[:100]}...")

while True:
try:
# 刷新窗口引用
self.window = self.get_game_window()

# 截图并识别
img = self.grab_screenshot()
if img is None:
logging.warning("未找到游戏窗口")
time.sleep(3)
continue

text = self.ocr_text(img)
print(f"识别文本: {text[:100]}...")

# 处理页面
if not self.handle_page(text):
logging.debug("未识别到特定页面")

time.sleep(2)

except KeyboardInterrupt:
logging.info("用户中断程序")
break
except Exception as e:
logging.error(f"主循环出错: {e}")
time.sleep(3)

if __name__ == "__main__":
try:
import paddleocr
print("PaddleOCR已安装")
except ImportError:
print("请先安装PaddleOCR: pip install paddleocr")
exit(1)

try:
bot = XianYuGameBot()
bot.run()
except Exception as e:
logging.error(f"程序运行失败: {e}")
print(f"程序运行失败: {e}")

Generating a billion records in Cassandra

Generating a billion records in Cassandra can be accomplished using various methods, including using scripts, data generation tools, or custom applications. Below are some approaches you can take to generate a large dataset for testing or benchmarking purposes.
Method 1: Using CQLSH with a Python Script
You can use a Python script to generate and insert a billion records into Cassandra. This method uses the cassandra-driver library to connect to your Cassandra cluster.
Prerequisites
Install Cassandra Driver for Python:
Make sure you have the Cassandra driver installed. You can install it using pip:
pip install cassandra-driver
2. Set Up Your Cassandra Keyspace and Table:
Create a keyspace and a table in Cassandra where you will insert the records.
CREATE KEYSPACE test_keyspace WITH REPLICATION = { 'class': 'SimpleStrategy', 'replication_factor': 1 };

CREATE TABLE test_keyspace.test_table (
id UUID PRIMARY KEY,
name TEXT,
age INT
);

Python Script to Generate Records
Here’s a sample Python script that generates and inserts a billion records:
from cassandra.cluster import Cluster
import uuid
import random

# Connect to Cassandra
cluster = Cluster(['127.0.0.1']) # Replace with your Cassandra node IP
session = cluster.connect('test_keyspace')

# Prepare the insert statement
insert_stmt = session.prepare("INSERT INTO test_table (id, name, age) VALUES (?, ?, ?)")

# Generate and insert records
for i in range(1, 1000000001): # 1 billion records
record_id = uuid.uuid4()
name = f"Name_{i}"
age = random.randint(18, 99)

session.execute(insert_stmt, (record_id, name, age))

if i % 100000 == 0: # Print progress every 100,000 records
print(f"Inserted {i} records")

# Close the session and cluster connection
session.shutdown()
cluster.shutdown()

Method 2: Using Apache Spark
If you have Apache Spark set up, you can use it to generate and insert a large number of records into Cassandra efficiently.
Prerequisites
1. Set Up Spark with Cassandra Connector:
Make sure you have the Spark Cassandra Connector. You can include it in your Spark job using the following Maven dependency:

com.datastax.spark
spark-cassandra-connector_2.12
3.1.0

Spark Job to Generate Records
Here’s a sample Spark job in Scala to generate and insert records:

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.cassandra._

object GenerateRecords {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("Generate Records")
.config("spark.cassandra.connection.host", "127.0.0.1") // Replace with your Cassandra node IP
.getOrCreate()

import spark.implicits._

// Generate a DataFrame with 1 billion records
val records = (1 to 1000000000).map(i => (java.util.UUID.randomUUID(), s"Name_$i", scala.util.Random.nextInt(82) + 18))
val df = records.toDF("id", "name", "age")

// Write to Cassandra
df.write
.format("org.apache.spark.sql.cassandra")
.options(Map("keyspace" -> "test_keyspace", "table" -> "test_table"))
.mode("append")
.save()

spark.stop()
}
}

Method 3: Using Data Generation Tools
You can also use data generation tools like:
Apache JMeter: You can create a test plan to generate data and insert it into Cassandra.
Mockaroo: A web-based tool that allows you to generate large datasets in various formats, including CSV, which you can then import into Cassandra.
Conclusion
Generating a billion records in Cassandra can be done using various methods, including Python scripts, Apache Spark, or data generation tools. Choose the method that best fits your environment and requirements. Always ensure that your Cassandra cluster is properly configured to handle the load, and monitor performance during the data generation process.

cisco fpr1140 ftd smart license issue

If you got one notification as follow:

Product Instance Failed to Connect – The device “UDI_PID:FPR-1140; UDI_SN:xxxxxxxxxxxx; ” in the virtual account “DEFAULT” has not connected within its renewal period, and may run in a degraded state if it does not connect within the next 1 day. If the device is not going to connect, you can remove it to immediately release the licenses it is consuming.

try login system run command:

 

root@fw:/home/admin# pmtool restartbyid tomcat
root@fw:/home/admin# pmtool status | grep "tomcat"
then wait a quite long time to the http portal recover to access