初次创建仓库提交代码

1. 已经构建好了架子了。
2. 添加了示例的插件
This commit is contained in:
2025-04-21 06:32:47 +00:00
parent 567bcbc0d0
commit 0a0f6a6054
107 changed files with 48783 additions and 2 deletions

214
core/behavior_tree.py Normal file
View File

@ -0,0 +1,214 @@
import os, sys
from abc import ABC, abstractmethod
from enum import Enum
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from core.defines import ReleaseErr
from thirdparty.py_trees.behaviour import Behaviour
from thirdparty.py_trees.common import Access
from thirdparty.py_trees.composites import Selector, Parallel, Sequence, Composite
# from thirdparty.py_trees.decorators import Decorator
from thirdparty.py_trees import blackboard
from thirdparty.py_trees.common import ParallelPolicy
from thirdparty.py_trees.behaviour import Behaviour
import typing
from core.logger import get_logger
log = get_logger()
class ReleaseFlowBlackboard(blackboard.Client):
"""
重载blackboard.Client用于适配一些我们的需求
每个节点访问其所需要的数据的时候都需要先进行注册,注册之后方可访问
"""
def register_key(self, key: str, access: Access = Access.WRITE, required: bool=False, remap_to: str=None):
"""
重写register_key方法用于注册key,默认具备可读可写的属性
"""
super().register_key(key=key, access=access, required=required, remap_to=remap_to)
def require_key(self, key: str):
"""
强制要求key存在
"""
self.register_key(key=key, access=Access.WRITE, required=True)
class ReleaseFlowAction(Behaviour, ABC):
"""
Release Flow 中的最小单元其本质是一个Behaviour Tree的节点
继承了py_trees.behaviour.Behaviour的接口
同时针对我们的Release Flow添加了一些实现的细节
- setup() 方法用于初始化节点。需要主动调用
- process() 方法在每次节点开始执行时调用。
- update() 方法是节点的核心逻辑返回节点的状态SUCCESS, FAILURE, RUNNING
- shutdown() 方法用于清理节点。和setup()成对使用
"""
def __init__(self, name: str, description: str = ""):
Behaviour.__init__(self, name)
# blackboard 是所有 flow action 共享的每个action自己注册自己需要的key
self.blackboard = self.attach_blackboard_client(name=name)
self.description = description
# 重写attach_blackboard_client方法用于创建ReleaseFlowBlackboard实例
def attach_blackboard_client(self, name: str=None, namespace: str=None
) -> ReleaseFlowBlackboard:
if name is None:
count = len(self.blackboards)
name = self.name if (count == 0) else self.name + "-{}".format(count)
new_blackboard = ReleaseFlowBlackboard(
name=name,
namespace=namespace
)
self.blackboards.append(new_blackboard)
return new_blackboard
# 子类可以重写setup方法用于初始化
def setup(self):
pass
@abstractmethod
def update(self):
pass
def initialise(self):
"""
Behaviour 基类中每次tick都会call该函数执行实际的业务逻辑我们为了方便理解将名字改为process
只要子类实现了process方法就可以在Release Flow中使用
"""
try:
self.process()
except Exception as e:
from thirdparty.py_trees.common import Status
self.status = Status.FAILURE
log.error(f"Action {self.name} 执行失败: {str(e)}")
@abstractmethod
def process(self):
"""
子类实现process以用于实际的业务逻辑
"""
pass
def ReleaseFlowActionDecorator(cls):
"""
装饰器用于自动生成行为树节点的名称并自动处理__init__函数
目的就是为了使得自动生成一个name而不需要手动去写
使用方法:
@ReleaseFlowActionDecorator
class MyAction(ReleaseFlowAction):
pass
"""
# 保存原始的__init__如果存在的话
original_init = cls.__init__ if '__init__' in cls.__dict__ else None
# 如果不是ReleaseFlowAction的子类则不进行装饰
if not issubclass(cls, ReleaseFlowAction):
return cls
def new_init(self, name:str=None, description:str=''):
if name is None:
name = f"{self.__module__}.{self.__class__.__name__}"
# 如果原始类有自定义的__init__则调用它
if original_init:
original_init(self, name, description)
else:
super(cls, self).__init__(name, description)
cls.__init__ = new_init
return cls
class ReleaseFlowBuilder:
"""行为树构建器"""
def __init__(self):
self.current_node = None
self.root = None
self.node_stack = []
def sequence(self, name: str, description: str = "") -> 'ReleaseFlowBuilder':
"""创建序列节点"""
if isinstance(name, Enum):
name = name.value
node = Sequence(name, description)
self._add_node(node)
self.node_stack.append(node)
return self
def selector(self, name: str, description: str = "") -> 'ReleaseFlowBuilder':
"""创建选择节点"""
if isinstance(name, Enum):
name = name.value
node = Selector(name, description)
self._add_node(node)
self.node_stack.append(node)
return self
def parallel(self, name: str, description: str = "",
policy: ParallelPolicy.Base = ParallelPolicy.SuccessOnAll(),
children: typing.List[Behaviour] = None) -> 'ReleaseFlowBuilder':
"""创建并行节点"""
if isinstance(name, Enum):
name = name.value
node = Parallel(name, policy, children)
self._add_node(node)
self.node_stack.append(node)
return self
def action(self, action: ReleaseFlowAction) -> 'ReleaseFlowBuilder':
"""创建动作节点"""
if isinstance(action, list):
for a in action:
self._add_node(a)
else:
self._add_node(action)
return self
def end(self) -> 'ReleaseFlowBuilder':
"""结束当前复合节点或装饰器节点"""
if self.node_stack:
self.node_stack.pop()
return self
def build(self) -> Behaviour:
"""构建并返回行为树根节点"""
if not self.root:
raise ReleaseErr("No nodes added to behavior tree")
# 确保所有节点都已关闭
if self.node_stack:
raise ReleaseErr(f"Unclosed nodes: {[node.name for node in self.node_stack]}")
return self.root
def _add_node(self, node: Behaviour):
"""添加节点到当前位置"""
if not self.root:
# 第一个节点作为根节点
self.root = node
self.current_node = node
return
if not self.node_stack:
# 如果没有开放的节点,直接替换根节点
self.root = node
self.current_node = node
return
parent = self.node_stack[-1]
# 将节点添加到父节点
if isinstance(parent, Composite):
parent.add_child(node)
else:
raise ReleaseErr(f"Parent node {parent.name} is not a Composite")
# elif isinstance(parent, Decorator):
# parent.set_child(node)
# # 装饰器只能有一个子节点,所以自动结束
# if isinstance(node, (Composite, Decorator)):
# # 但如果子节点是复合节点或装饰器,保持它开放
# pass
# else:
# self.node_stack.pop()
# 添加如下magic函数以用于支持with语句可简化代码编写依靠缩进使得层次关系更加清晰
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.end() # 结束序列

85
core/defines.py Normal file
View File

@ -0,0 +1,85 @@
from enum import Enum
import json
class ReleasePhase(str, Enum):
ENV = "PhaseEnv"
CONFIG = "PhaseConfig"
BUILD = "PhaseBuild"
CHECK = "PhaseCheck"
PACKAGE = "PhasePackage"
VERIFY = "PhaseVerify"
class ProcessStep(str, Enum):
"""
ReleaseFlow中每个阶段的几个执行步骤顺序执行
"""
PRE = "Pre"
PROCESS = "Process"
POST = "Post"
class ReleaseErr(Exception):
"""Release tool专用异常基类包含错误信息和上下文"""
def __init__(self, message: str, ctx: dict = None):
self.message = message
self.ctx = ctx or {}
super().__init__(message)
def __str__(self):
return f"{self.message} (context: {self.ctx})" if self.ctx else self.message
class Dict(dict):
"""
一个字典支持点号访问从xxx[xxx][xxx] 的方式转化为 xxx.xxx.xxx 的形式访问字典中的值
如果key不存在返回一个空的字典
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 将字典中的所有值递归转换为 Dict
for key, value in self.items():
if isinstance(value, dict):
self[key] = Dict(value)
@classmethod
def from_dict(cls, data: dict) -> "Dict":
"""
从普通字典创建一个 Dict 对象
:param data: 要转换的字典
:return: Dict 对象
"""
return cls(data)
def __getattr__(self, key):
if key in self:
value = self[key]
# 如果值是字典,递归转换为 Dict
if isinstance(value, dict):
return Dict(value)
return value
# 如果key不存在返回None
# return Dict()
raise AttributeError(f"In object not found '{key}'")
def __setattr__(self, key, value):
self[key] = value
def __str__(self):
string = json.dumps(self, indent=4)
return string
def __repr__(self):
return self.__str__()
class List(list):
def __str__(self):
return json.dumps(self, indent=4)
def __repr__(self):
return self.__str__()

82
core/logger.py Normal file
View File

@ -0,0 +1,82 @@
import logging
import sys
import os
TRACE = 5
DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
CRITICAL = logging.CRITICAL
NOTSET = logging.NOTSET
def get_logger(name=None):
if name is None:
name = 'alkaid_release_platform'
# 创建一个 logger
logger = logging.getLogger(name)
if logger.handlers:
return logger
#定义一个新的日志级别用于输出trace信息
logging.TRACE = TRACE
logging.addLevelName(logging.TRACE, "TRACE")
def trace(self, message, *args, **kws):
self._log(logging.TRACE, message, args, **kws)
logging.Logger.trace = trace
logger.setLevel(INFO)
class LogColors:
RESET = "\033[0m"
TRACE = "\033[35m" # 紫色
DEBUG = "\033[34m" # 蓝色
INFO = "\033[32m" # 绿色
WARNING = "\033[33m" # 黄色
ERROR = "\033[31m" # 红色
CRITICAL = "\033[41m" # 红底白字
# 创建一个log的颜色格式化器使得输出的log信息在控制台上有颜色区分
class ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color=True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
level_color = {
logging.TRACE: LogColors.TRACE,
logging.DEBUG: LogColors.DEBUG,
logging.INFO: LogColors.INFO,
logging.WARNING: LogColors.WARNING,
logging.ERROR: LogColors.ERROR,
logging.CRITICAL: LogColors.CRITICAL
}
color = level_color.get(record.levelno, LogColors.RESET)
message = super().format(record)
return f"{color}{message}{LogColors.RESET}" if os.isatty(sys.stdout.fileno()) else message
# 创建一个带颜色的控制台处理器
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(INFO)
# 创建一个带颜色的格式化器并将其添加到处理器
formatter = ColoredFormatter('[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s')
console_handler.setFormatter(formatter)
# 将处理器添加到 logger
logger.addHandler(console_handler)
# 定义一个方法,用于设置日志级别,并将其绑定到 logger 对象
def set_level(self, level):
lv_map = {
"trace":TRACE,
"debug":DEBUG,
"info":INFO,
"warning":WARNING,
"error":ERROR,
"critical":CRITICAL
}
level = lv_map.get(level, DEBUG)
for handler in self.handlers:
handler.setLevel(level)
logger.setLevel(level)
logging.Logger.set_level = set_level
return logger

219
core/plugin.py Normal file
View File

@ -0,0 +1,219 @@
# core/plugin.py
from abc import ABC, abstractmethod
import importlib
from enum import Enum
import os,sys
import pkgutil
import inspect
from typing import Dict, List, Type
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from core.behavior_tree import ReleaseFlowAction
from core.defines import ReleasePhase, ProcessStep, Dict
import core.logger as logger
log = logger.get_logger()
"""
# format: PluginName.ReleaseStep.ProcessStep
ReleaseFlowCommon.StepEnv.Pre
"""
class PluginType(str, Enum):
"""
为插件分类目前仅有一种插件FLOW类型的插件用于执行Release Flow
"""
FLOW = 'flow'
class PluginInterface(ABC):
"""PluginInterface 插件基类"""
def __init__(self, type: PluginType):
self.type = type
def get_type(self) -> PluginType:
return self.type
@abstractmethod
def get_version(self) -> str:
pass
@abstractmethod
def get_description(self) -> str:
"""返回插件描述"""
pass
class ReleaseFlowPlugin(PluginInterface, ABC):
"""ReleaseFlowPlugin 插件基类"""
def __init__(self):
PluginInterface.__init__(self, PluginType.FLOW)
self.btrees:Dict = Dict()
@abstractmethod
def get_version(self) -> str:
"""返回插件版本"""
pass
@abstractmethod
def get_description(self) -> str:
"""返回插件描述"""
pass
# 支持使用.操作符访问btrees
def __getattr__(self, name: str) -> Dict:
return self.btrees[name]
def ReleaseFlowPluginDecorator(cls):
"""装饰器用于自动注册ReleaseFlow插件 每个 ReleaseFlowPlugin 插件上加上这个装饰器, 插件初始化时会自动搜索并注册行为树
Args:
cls: 插件类
Returns:
插件类
"""
original_init = cls.__init__
# 如果不是ReleaseFlowPlugin的子类则不进行装饰
if not issubclass(cls, ReleaseFlowPlugin):
return cls
def behavior_tree_register(self: ReleaseFlowPlugin):
"""
搜索当前目录下的所有行为树节点并根据命名注册到btrees中
"""
def _load_actions_from_package(self, package_name: str):
"""从包中加载插件"""
package = importlib.import_module(package_name)
for _, name, is_pkg in pkgutil.iter_modules(package.__path__, f"{package_name}."):
if not is_pkg:
_load_actions(self, name)
else:
_load_actions_from_package(self, f"{package_name}.{name}")
def _load_actions(self, module_name: str):
"""加载单个behavior action"""
# log.debug(f"Try to check action: {module_name}")
module = importlib.import_module(module_name)
# 遍历模块中的所有属性名称
for attribute_name in dir(module):
attribute = getattr(module, attribute_name)
if not isinstance(attribute, type): # 检查是否是类
continue
if not issubclass(attribute, ReleaseFlowAction): # 检查是否是插件类
continue
if inspect.isabstract(attribute): # 检查是否是抽象类
continue
log.info(f"Loaded action: {attribute.__name__}")
self.sub_actions[attribute.__name__] = attribute
def discover_actions(self:ReleaseFlowPlugin):
module = inspect.getmodule(self)
path = os.path.relpath(os.path.dirname(module.__file__)).replace('/', '.')
# log.debug(f"Try to search action in: {path}")
self.sub_actions = {}
for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(module.__file__)]):
# log.trace(f"Try to load action: {name}")
if is_pkg:
_load_actions_from_package(self, f"{path}.{name}")
else:
_load_actions(self, f"{path}.{name}")
if not isinstance(self, ReleaseFlowPlugin):
log.warning(f"Plugin class {self.__class__.__name__} is not a ReleaseFlowPlugin")
raise ReleaseErr("The plugin class decorator is not a ReleaseFlowPlugin")
discover_actions(self)
for phase in ReleasePhase:
phase = phase.value
self.btrees[phase] = Dict()
for step in ProcessStep:
step = step.value
action_class = f"_{phase}_{step}"
if not self.sub_actions.get(action_class):
log.trace(f"Action {action_class} not found")
continue
log.debug(f"Register ReleaseFlow behavior tree node: {action_class}")
if not self.btrees[phase].get(step):
self.btrees[phase][step] = self.sub_actions[action_class]
else:
log.warning(f"Behavior tree node {action_class} already exists: {self.btrees[phase][step]}")
def new_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
behavior_tree_register(self)
# 添加标记表示该类使用了flow action 装饰器,用于检查是否注册了行为树
cls._is_action_registered = True
cls.__init__ = new_init
return cls
class PluginManager:
"""插件管理器"""
def __init__(self, plugin_dir: str = "plugins"):
self.plugin_dir = plugin_dir
self.plugins: Dict[str, Type[PluginInterface]] = {}
self.loaded_plugins: Dict[str, PluginInterface] = {}
self.discover_plugins()
def discover_plugins(self):
"""发现并加载所有插件"""
for _, name, is_pkg in pkgutil.iter_modules([self.plugin_dir]):
plugin_dir = f"{self.plugin_dir}.{name}"
log.trace(f"Try to search plugin in: {plugin_dir}")
if is_pkg:
self._load_plugins_from_package(plugin_dir)
else:
self._load_plugin(plugin_dir)
def _load_plugins_from_package(self, package_name: str):
"""从包中加载插件"""
package = importlib.import_module(package_name)
for _, name, is_pkg in pkgutil.iter_modules(package.__path__, f"{package_name}."):
if not is_pkg:
self._load_plugin(name)
else:
self._load_plugins_from_package(f"{package_name}.{name}")
def _load_plugin(self, module_name: str):
"""加载单个插件"""
module = importlib.import_module(module_name)
for attribute_name in dir(module):
plugin_class = getattr(module, attribute_name)
if not isinstance(plugin_class, type): # 检查是否是类
continue
if not issubclass(plugin_class, PluginInterface): # 检查是否是插件类
continue
if inspect.isabstract(plugin_class): # 检查是否是抽象类
continue
# 检查插件类是否使用了 ReleaseFlowActionRegister 装饰器
if issubclass(plugin_class, ReleaseFlowPlugin) and not hasattr(plugin_class, '_is_action_registered'):
log.warning(f"Plugin class {plugin_class.__name__} does not have the ReleaseFlowActionRegister decorator.")
continue
log.info(f"Loaded plugin: {plugin_class.__name__}")
self.plugins[plugin_class.__name__] = plugin_class
def get_plugin(self, name: str) -> PluginInterface:
"""获取插件实例, 每个插件实例只初始化一次
Args:
name: 插件名称
Returns:
插件实例
"""
if name not in self.loaded_plugins:
if name in self.plugins:
self.loaded_plugins[name] = self.plugins[name]()
else:
raise ValueError(f"Plugin {name} not found")
return self.loaded_plugins[name]
def get_available_plugins(self) -> List[str]:
"""获取所有可用插件名称"""
return list(self.plugins.keys())
def __str__(self) -> str:
string = ""
string += f"Plugin directory: {self.plugin_dir}\n"
string += "Available plugins:"
string += "[" + " \n".join(self.plugins.keys()) + "]\n"
string += "Loaded plugins:"
string += "[" + " \n".join(self.loaded_plugins.keys()) + "]\n"
return string
def __repr__(self) -> str:
return str(self)
def __getattr__(self, name: str) -> PluginInterface:
# 获取插件实例
plugin = self.get_plugin(name)
return plugin

157
core/release_flow.py Normal file
View File

@ -0,0 +1,157 @@
import functools
import os,sys
import threading
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
import thirdparty.py_trees as py_trees
from thirdparty.py_trees.trees import BehaviourTree
from core.behavior_tree import ReleaseFlowBuilder
from core.plugin import PluginManager
from core.defines import ReleasePhase, ProcessStep, ReleaseErr, Dict
from database import *
import core.logger as logger
log = logger.get_logger()
def draw_release_flow_status(snapshot_visitor, behaviour_tree):
"""保存行为树状态为HTML文件"""
mutex = threading.Lock()
with mutex:
# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"alkaid_release_flow_status.html"
with open(filename, 'w') as f:
f.write('<html><head><title>SStar Alkaid Release Flow Status</title><body>')
f.write(py_trees.display.xhtml_tree(
behaviour_tree.root,
show_status=True,
visited=snapshot_visitor.visited,
previously_visited=snapshot_visitor.visited
))
f.write("</body></html>")
filename = f"alkaid_release_flow_status.dot"
with open(filename, 'w') as f:
f.write(str(py_trees.display.dot_tree(behaviour_tree.root, with_blackboard_variables=True)))
# log.info(f"Behavior tree saved to {filename}")
# 构建环境阶段 (验证配置、拉取code等基础环境的准备)
def _construct_env_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
with builder.sequence(ReleasePhase.ENV):
with builder.sequence(ProcessStep.PRE):
pass
with builder.sequence(ProcessStep.PROCESS):
if not ReleaseOptions.SkipSyncCode:
builder.action(plugins.BaseLine.PhaseEnv.Process())
with builder.sequence(ProcessStep.POST):
pass
# 配置编译文件阶段 defconfig mkfile 等编译控制文件的配置)
def _construct_config_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
with builder.sequence(ReleasePhase.CONFIG):
with builder.sequence(ProcessStep.PRE):
pass
with builder.sequence(ProcessStep.PROCESS):
pass
with builder.sequence(ProcessStep.POST):
pass
# 构建阶段(编译构建sdk根据上一步的配置执行编译构建)
def _construct_build_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
with builder.sequence(ReleasePhase.BUILD):
with builder.sequence(ProcessStep.PRE):
pass
with builder.sequence(ProcessStep.PROCESS):
pass
with builder.sequence(ProcessStep.POST):
pass
# 检查阶段检查SDK编译结果是否符合预期
def _construct_check_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
with builder.sequence(ReleasePhase.CHECK):
with builder.sequence(ProcessStep.PRE):
pass
with builder.sequence(ProcessStep.PROCESS):
pass
with builder.sequence(ProcessStep.POST):
pass
# 打包阶段 (将SDK其他文件诸如doctool等打包)
def _construct_package_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
with builder.sequence(ReleasePhase.PACKAGE):
with builder.sequence(ProcessStep.PRE):
pass
with builder.sequence(ProcessStep.PROCESS):
pass
with builder.sequence(ProcessStep.POST):
pass
# 验证阶段 (验证Release的SDK是否可以正常工作)
def _construct_verify_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
with builder.sequence(ReleasePhase.VERIFY):
with builder.sequence(ProcessStep.PRE):
pass
with builder.sequence(ProcessStep.PROCESS):
pass
with builder.sequence(ProcessStep.POST):
pass
class ReleaseFlow():
def __init__(self):
# 我们的Flow默认是一个串行的流程所以根节点用Sequence
self.builder = ReleaseFlowBuilder()
self.tree = None
self.flow = None
self.timer = None
self.status_update_interval = 1 # 定时器间隔时间,单位秒
self.running = False
# 构建 Alkaid Release流程
def construct(self, plugins: PluginManager):
with self.builder.sequence("AlkaidReleaseFlow") as builder:
_construct_env_phase(builder, plugins)
_construct_config_phase(builder, plugins)
_construct_build_phase(builder, plugins)
_construct_check_phase(builder, plugins)
_construct_package_phase(builder, plugins)
_construct_verify_phase(builder, plugins)
self.flow = self.builder.build()
def _start_update_status(self, snapshot_visitor):
"""启动定时器"""
if self.timer is not None:
return
def timer_callback():
if self.tree is not None:
draw_release_flow_status(snapshot_visitor, self.tree)
# 重新设置定时器
self.timer = threading.Timer(self.status_update_interval, timer_callback)
self.timer.start()
# 启动第一个定时器
self.timer = threading.Timer(self.status_update_interval, timer_callback)
self.tree.add_post_tick_handler(functools.partial(draw_release_flow_status, snapshot_visitor))
self.timer.start()
# 运行Release 流程
def run(self):
if self.flow is None:
raise ReleaseErr("Flow is not built")
# 使用封装好的BehaviourTree去管理流程
self.tree = BehaviourTree(root=self.flow)
snapshot_visitor = py_trees.visitors.SnapshotVisitor()
self.tree.visitors.append(snapshot_visitor)
self.tree.setup() # 初始化树
# 启动定时器
self._start_update_status(snapshot_visitor)
# print(
# py_trees.display.dot_tree(
# self.tree.root
# )
# )
self.tree.tick() # 触发tick进行执行
return self.tree.root.status
def shutdown(self):
if self.timer is not None:
self.timer.cancel()
self.timer = None
if self.tree is not None:
self.tree.shutdown()