基于AstrBot+米家实现自然语言控制夜灯设备

准备 我自己有一台接入米家的夜灯,型号是米家床头灯二代,官方已经给出了产品规格与各项控制参数 感谢Do1e开源的的mijiaAPI,这个仓库向调用者提供了傻瓜式的米家电器控制调用接口,只需要调用相应方法、传入对应于设备控制意图的参数,即可通过云端控制米家设备 AstrBot部署于阿里云服务器,已经接入微信的ClawBot。为了插件开发方便,也在本地部署了AstrBot 环境配置 首先在AstrBot的根目录下安装依赖,官方提供了uv依赖管理: cd AstrBot uv sync uv pip install mijiaAPI AstrBot为插件开发者提供了模板仓库,只需使用此模板,然后克隆到本地的AstrBot插件文件夹即可开始开发 cd AstrBot/data/plugins git clone <template-registry> 为了保证插件的正常运行,之后在插件开发调试中,Python环境都应使用外层目录的astrbot,而不应在插件目录新建环境 登录米家 mijiaAPI提供了便捷的登陆方式: import mijiaAPI from mijiaAPI api = mijiaAPI("auth.json") api.login() 执行login()时,程序会在终端打印登录二维码,使用登陆了米家账号的设备扫描后即可完成登录,此后程序不再阻塞,继续执行后面的脚本 不过,如果要将登录逻辑引入AstrBot机器人聊天环境存在一些问题: 登陆时的阻塞会导致机器人无法回复 登陆二维码在终端打印,需要某种方法将登陆手段以机器人回复的形式向用户呈现 在满足以上条件的基础上,还要维持原mijiaAPI中的轮询用户登录逻辑,并在用户扫码登录成功后使机器人回复“登陆成功”提示用户 关于阻塞问题的解决方案,我参考了已有的在AstrBot中调用mijiaAPI的仓库,由674537331开发,核心思路是创建一个独立的进程沙盒,在沙盒中运行login()从而避免主进程被阻塞 在超时时间内每隔0.1秒不断读取进程的输出,如果读取到了二维码URL,则立即向用户回复该URL,回复后不结束登录流程,而是继续循环读取输出,直到超时或读取到登陆成功或失败 在子进程中执行登录的脚本_login_worker.py: #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 登录工作脚本,在子进程中执行登录流程 """ import sys import os import time from pathlib import Path import logging # 配置日志 logging.basicConfig(level=logging.INFO, encoding='utf-8') logging.getLogger("mijiaAPI").setLevel(logging.INFO) # 强制设置 stdout 编码为 UTF-8 if sys.stdout.encoding != 'utf-8': sys.stdout.reconfigure(encoding='utf-8') if sys.stderr.encoding != 'utf-8': sys.stderr.reconfigure(encoding='utf-8') from mijiaAPI import mijiaAPI def main(): if len(sys.argv) != 2: print("Usage: python _login_worker.py <auth_path>", flush=True) sys.exit(1) auth_path = Path(sys.argv[1]) print("[WORKER] 开始初始化认证环境。", flush=True) try: # 确保目录存在 auth_path.parent.mkdir(parents=True, exist_ok=True) api = mijiaAPI(auth_path) print("[WORKER] API 实例已创建,正在请求小米服务器...", flush=True) # 执行登录 result = api.login() print("[WORKER] 登录成功", flush=True) print("[WORKER_SUCCESS] 授权完毕。", flush=True) sys.exit(0) except Exception as e: print(f"[WORKER_ERROR] 登录流程失败: {type(e).__name__}: {e}", flush=True) sys.exit(1) if __name__ == "__main__": main() 主文件: ...

April 20, 2026

为Kite开发集群内基于exec参数启动kubelogin的OIDC认证功能

现状 我在集群中使用Keycloak + kubelgoin + Webhook的认证模式,具体流程如下: 在API-Server中将认证模式设置为Webhook模式,由Webhook手动校验OIDC 执行kubectl时,exec命令启动kubelogin,以device-code形式唤起Keycloak进行认证 认证完成后Keycloak给出token,由kubectl将id_token添加到GET请求的Authorization字段发送至API-Server API-Server向Webhook POST一个TokenReview,Webhook解析token、验证后根据规则组装成UserInfo发往后续准入环节等 现在项目中引入了Kite作为集群管理面板 Kite的底层是通过K8s-Client向API-Server发送HTTP请求执行相应操作,本质上与kubectl相似,且都需要读取预先配置的kubeconfig文件获取配置上下文,如集群地址、当前集群用户、用户证书等 不足 Kite本身不负责集群身份认证,而是将安全性转移至进入Kite面板的认证。因此Kite虽然原生支持OIDC,但只能支持进入Kite管理界面时的OIDC认证,而非集群内身份认证 也正因这个原因,Kite无法处理kubeconfig中配置了exec参数动态获取认证凭证的用户,在界面中将该用户上下文显示为不可用状态 为了解决这个问题,Kite创建了一个专用的Service Account,并使用其token通过集群认证(将token放入kubeconfig然后被Kite正常读取使用) 需求 我需要二次开发Kite,使Kite支持进行集群内OIDC device-code模式认证 用户只需要指定Issuer、client_secret等必要信息,即可提供Keycloak认证界面供用户进行认证 认证通过后,需要能够接收Keycloak响应的各种token 记录该token,禁用原kubeconfig配置 之后调用K8s-Client发送API请求时都在Authorization字段附加token 如果token过期,则在前端提示用户需要重新进行OIDC认证 如果请求被准入环节拦截,需要提示用户根据准入规定重新认证 / 增量认证 / 拒绝请求 准备 使用Postman测试不带Authorization的API请求响应,返回403: { "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "nodes is forbidden: User \"system:anonymous\" cannot list resource \"nodes\" in API group \"\" at the cluster scope", "reason": "Forbidden", "details": { "kind": "nodes" }, "code": 403 } 使用Postman附带Keycloak客户端ID、客户端Secret向$base_url发送认证请求: { "device_code": "vfpIiDEl8-M7euodTglJTkbUnKr99l7GGDNQK7If0EM", "user_code": "PSWY-TSRS", "verification_uri": "https://192.168.5.96:31443/realms/k8s-auth/device", "verification_uri_complete": "https://192.168.5.96:31443/realms/k8s-auth/device?user_code=PSWY-TSRS", "expires_in": 600, "interval": 5 } 认证完成后尝试拉取Token: ...

April 9, 2026

RPC框架简单实践

创建一个简单示例,原理如下: 运行一个RPC服务器,服务器中使用ConcurrentHashMap<String, Class>存储服务实现类的类名和实例 服务提供者Provider将自己的实现类加入该Map中去,实现服务注册 服务消费者Consumer发送HTTP的服务请求,其中包含服务类名、方法名、参数类型、参数列表四个参数 RPC服务器接收到服务请求,根据解析的请求内容到Map中取相对应的实例,通过反射执行对应方法 RPC服务器将执行方法后的结果封装到响应中返回Consumer 关于Consumer发送请求如果每次都手动构造请求体就太麻烦了,有两个实现: 编写静态的代理类,也实现了服务接口,实现内部是构造相应的请求体发送给RPC服务器,获取响应后反序列化结果返回 不过这个静态代理类需要针对每个服务接口方法硬编码请求体,比如.methodName(“getUser”)这种都要手动设置 编写动态代理类,可以基于JDK动态代理实现,这样就不用手动实现每个服务接口,invoke()方法会获取到各种调用参数信息,将这些信息封装好发送就行 共享接口 RPC服务器、服务提供者、服务消费者都需要得到服务接口信息,如此才能实例化或实现功能 用户User: public class User implements Serializable { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } 用户服务接口UserService: public interface UserService { /** * 获取用户 * * @param user * @return */ User getUser(User user); } RPC服务器 核心Handler中,先反序列化请求对象,从中取出请求的服务信息,从注册中心Map获取实现类实例后,通过反射执行方法,然后将执行结果封装为响应返回: public class HttpServerHandler implements Handler<HttpServerRequest> { @Override public void handle(HttpServerRequest request) { // 创建序列化器 final Serializer serializer = new JdkSerializer(); // 记录日志 System.out.println("Received request: " + request.method() + " " + request.uri()); // 异步处理请求 request.bodyHandler(body -> { byte[] bytes = body.getBytes(); RpcRequest rpcRequest = null; try { // 反序列化请求参数为RpcRequest对象 rpcRequest = serializer.deserialize(bytes, RpcRequest.class); } catch (Exception e) { e.printStackTrace(); } RpcResponse rpcResponse = new RpcResponse(); // 如果请求为空则返回错误信息 if (rpcRequest == null) { rpcResponse.setMessage("Invalid request, request is null"); doResponse(request, rpcResponse, serializer); return; } try { // 从请求取服务名,然后从注册中心获取服务实现类 Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName()); // 获取服务实现类的方法并执行 Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes()); Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs()); // 封装响应结果 rpcResponse.setData(result); rpcResponse.setDataType(method.getReturnType()); rpcResponse.setMessage("ok"); } catch (Exception e) { e.printStackTrace(); rpcResponse.setMessage(e.getMessage()); rpcResponse.setException(e); } doResponse(request, rpcResponse, serializer); }); } private void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) { HttpServerResponse httpServerResponse = request.response().putHeader("content-type", "application/json"); try { // 序列化响应结果为字节数组 byte[] bytes = serializer.serialize(rpcResponse); // 发送响应 httpServerResponse.end(Buffer.buffer(bytes)); } catch (IOException e) { e.printStackTrace(); httpServerResponse.end(Buffer.buffer()); } } } 关于注册中心,维护该Map即可: ...

March 22, 2026

K8s误删PV补救措施

背景 K8s中的插件local-path-provisioner能够动态创建PV,使用local-pathPV时,无需手动建立PV,只需建立一个PVC,在其中指定storageClassName: local-path即可,插件会自动为其分配PV和存储目录 这个插件的好处是: 用户无需手动配置PV,只需创建PVC绑定即可; 在非文件共享的场景下,用户也无需关心服务Pod被调度至哪个节点,因为插件会自动根据Pod所在节点分配存储区 现在我误删了所有PV,结果是执行kubectl get pv命令时,看到了状态是Terminating的PV,这说明PV处于正在被删除的状态 但当我执行命令kubectl get pvc -n authen查看PVC时,却发现绑定了PV的PVC是正常Bound的状态 原因分析 之所以被删除时的PV表现为Terminating状态而非直接消失,是因为该PV存在**finalizer字段**,这个字段的作用就是在集群删除PV时,不立刻清除PV,而是将其标记起来,呈现Terminating状态,这是为了确保一些重要的清理动作在资源被物理删除前完成,此时,PVC仍然是可用的 我现在存在两个Terminating状态的PV,它们各自的名称是: pvc-539fd3b7-12ed-488b-9c86-59e07de28b05 pvc-63a03d24-f078-43af-ae6c-0ce2328ba54e 查看其中一个PV的信息: master@master:~/authen/pvc$ kubectl get pv pvc-539fd3b7-12ed-488b-9c86-59e07de28b05 -o yaml apiVersion: v1 kind: PersistentVolume metadata: annotations: local.path.provisioner/selected-node: master pv.kubernetes.io/provisioned-by: rancher.io/local-path creationTimestamp: "2025-10-10T07:46:40Z" deletionGracePeriodSeconds: 0 deletionTimestamp: "2025-10-22T06:34:12Z" finalizers: - kubernetes.io/pv-protection name: pvc-539fd3b7-12ed-488b-9c86-59e07de28b05 resourceVersion: "30449839" uid: bea50dbb-7f29-4e21-bb22-d9e61ce13cec spec: accessModes: - ReadWriteOnce capacity: storage: 10Gi claimRef: apiVersion: v1 kind: PersistentVolumeClaim name: ldap-config-pvc namespace: authen resourceVersion: "27503744" uid: 539fd3b7-12ed-488b-9c86-59e07de28b05 hostPath: path: /opt/local-path-provisioner/pvc-539fd3b7-12ed-488b-9c86-59e07de28b05_authen_ldap-config-pvc type: DirectoryOrCreate nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - master persistentVolumeReclaimPolicy: Delete storageClassName: local-path volumeMode: Filesystem status: phase: Bound 可以看到,在metadata下确实存在finalizers字段,正是它导致了PV始终处于Terminating状态 ...

October 23, 2025

在K8s中部署openLDAP

安装 先创建一个命名空间:kubectl create namespace authen 设置PVC: # vi openldap-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ldap-data-pvc namespace: authen spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: local-path --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ldap-config-pvc namespace: authen spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: local-path 执行kubectl apply -f open-ldap-pvc.yaml创建PVC即可 设置初始化时需要插入的信息: # vi ldap-init.ldif dn: ou=People,dc=example,dc=com ou: People objectClass: organizationalUnit dn: ou=Group,dc=example,dc=com ou: Group objectClass: organizationalUnit 执行kubectl create configmap openldap-init --from-file=./ldap-init.ldif -n authen生成ConfigMap,供之后LDAP初始化时读取 创建部署文件: # vi openldap-deployment.yaml kind: Deployment apiVersion: apps/v1 metadata: name: openldap namespace: authen labels: app: openldap annotations: app.kubernetes.io/alias-name: LDAP app.kubernetes.io/description: 认证中心 spec: replicas: 1 selector: matchLabels: app: openldap template: metadata: labels: app: openldap spec: containers: - name: go-ldap-admin-openldap args: - --copy-service image: 'osixia/openldap:1.5.0' ports: - name: tcp-389 containerPort: 389 protocol: TCP - name: tcp-636 containerPort: 636 protocol: TCP env: - name: TZ value: Asia/Shanghai - name: LDAP_ORGANISATION value: "orgldap" - name: LDAP_DOMAIN value: "example.com" - name: LDAP_ADMIN_PASSWORD value: "123456" - name: LDAP_BACKEND value: mdb resources: limits: cpu: 500m memory: 500Mi requests: cpu: 100m memory: 100Mi volumeMounts: - name: ldap-config-pvc mountPath: /etc/ldap/slapd.d - name: ldap-data-pvc mountPath: /var/lib/ldap - name: openldap-init mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom/init.ldif subPath: init.ldif volumes: - name: ldap-config-pvc persistentVolumeClaim: claimName: ldap-config-pvc - name: ldap-data-pvc persistentVolumeClaim: claimName: ldap-data-pvc - name: openldap-init configMap: name: openldap-init --- apiVersion: v1 kind: Service metadata: name: openldap-svc namespace: authen labels: app: openldap-svc spec: ports: - name: tcp-389 port: 389 protocol: TCP targetPort: 389 - name: tcp-636 port: 636 protocol: TCP targetPort: 636 selector: app: openldap 执行kubectl apply -f ldap-deployment.yaml即可将LDAP部署为一个Pod ...

October 10, 2025