Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/plugins/core/runtime/configuration.py
T
insistence 4bd83bc7cd feat: 支持 v2 插件、独立前端和签名制品发布 (#130)
* feat: 支持 v2 插件、原生扩展与签名发布

* fix: 修复测试

* feat: 增强宿主 SDK、运行观测与制品维护能力

* feat: 增强文件传输、配置观测与本地开发能力

* feat: 支持 SSE 实时事件流并完善开发脚手架

* feat: 完善长连接鉴权复核与关闭回收

* feat: 增强生命周期与任务关闭可靠性

* feat: 收紧健康检查契约并补齐前端与真实网络验收

* feat: 增加SDK溯源与离线更新工具并验证跨版本兼容

* feat: 增加持久化业务示例与受控 Python 交付

* feat: 增加统一运行诊断与多worker状态观测

* feat: 增加固定目标发布等待与自动验收

* fix: 修复多环境插件部署前缀与代理配置不一致

* fix: 修复测试
2026-10-09 23:49:45 +08:00

143 lines
5.3 KiB
Python

import hashlib
import hmac
import json
import time
from collections.abc import Callable, Mapping
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from config.env import JwtConfig
from plugins.core.discovery.scanner import DiscoveredPlugin
from plugins.core.sdk.config import PluginConfigSnapshot
def config_revision(plugin_id: str, values: Mapping[str, Any]) -> str:
"""
生成绑定插件和宿主密钥的不透明版本,避免公开敏感配置的可枚举裸哈希。
:param plugin_id: 当前插件标识
:param values: 按清单合并默认值后的配置明文
:return: 可比较但不可据此还原配置的 HMAC 摘要
"""
payload = json.dumps(
['plugin-config-v1', plugin_id, dict(values)],
ensure_ascii=False,
allow_nan=False,
sort_keys=True,
separators=(',', ':'),
).encode('utf-8')
return hmac.new(JwtConfig.jwt_secret_key.encode('utf-8'), payload, hashlib.sha256).hexdigest()
class PluginConfigObservation(BaseModel):
"""
当前 worker 的启动配置和最近一次按需读取版本,不包含配置值。
"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, extra='forbid', strict=True)
plugin_id: str = Field(pattern=r'^[a-z][a-z0-9_]{1,63}$')
version: str = Field(max_length=128)
digest: str | None = Field(default=None, pattern=r'^[0-9a-f]{64}$')
generation: str | None = Field(default=None, pattern=r'^[0-9a-f]{32}$')
active: bool = False
startup_revision: str = Field(pattern=r'^[0-9a-f]{64}$')
last_read_revision: str | None = Field(default=None, pattern=r'^[0-9a-f]{64}$')
last_read_at: float | None = Field(default=None, ge=0, allow_inf_nan=False)
class PluginConfigReader:
"""
绑定已加载清单的配置读取器,每次读取使用自己的数据库会话。
"""
def __init__(
self,
plugin: DiscoveredPlugin,
session_factory: Callable[..., Any],
observation: PluginConfigObservation,
) -> None:
"""
固定插件身份,调用方不能提供其他插件 ID。
:param plugin: 当前进程实际加载的插件
:param session_factory: 宿主提供的数据库会话工厂
:param observation: 不包含敏感值的运行观测记录
:return: None
"""
self.plugin = plugin
self.session_factory = session_factory
self.observation = observation
async def read(self) -> PluginConfigSnapshot:
"""
读取已提交配置并记录读取版本,数据库错误直接交由调用方处理。
:return: 独立的插件配置快照,不代表业务代码已完成应用
"""
from plugins.core.management.service.service import PluginService # noqa: PLC0415
async with self.session_factory() as db:
configs = await PluginService.get_plugin_config_services(db, self.plugin, reveal_secret=True)
values = {item.key: item.value for item in configs}
snapshot = PluginConfigSnapshot(values, config_revision(self.plugin.manifest.id, values))
self.observation.last_read_revision = snapshot.revision
self.observation.last_read_at = time.time()
return snapshot
def build_config_status(plugin_id: str, desired_revision: str, report: Mapping[str, Any]) -> dict[str, Any]:
"""
比较已观测进程的启动快照,不把按需读取或局部观测当成全局生效。
:param plugin_id: 当前插件标识
:param desired_revision: 当前数据库配置版本
:param report: 指标报告器返回的进程快照与范围信息
:return: 包含待重启数量和明确采样范围的配置状态
"""
workers = [
{
'workerId': worker['workerId'],
'collectedAt': worker['collectedAt'],
**config,
'matchesDesired': config['active'] and config['startupRevision'] == desired_revision,
}
for worker in report.get('workers', [])
for config in worker.get('configurations', [])
if config['pluginId'] == plugin_id
]
pending = sum(worker['active'] and not worker['matchesDesired'] for worker in workers)
matched = sum(worker['matchesDesired'] for worker in workers)
unknown = sum(
any(series['pluginId'] == plugin_id for series in worker.get('series', []))
and not any(config['pluginId'] == plugin_id for config in worker.get('configurations', []))
for worker in report.get('workers', [])
)
return {
'ok': True,
'pluginId': plugin_id,
'desiredRevision': desired_revision,
'activationMode': 'startup_snapshot',
'state': 'restart_required' if pending else 'observed_match' if matched else 'unobserved',
'observedWorkers': len(workers),
'matchedWorkers': matched,
'pendingWorkers': pending,
'unknownWorkers': unknown,
'workers': workers,
**{
key: report.get(key)
for key in (
'scope',
'currentWorkerId',
'clusterAvailable',
'sampleIntervalSeconds',
'sampleTtlSeconds',
'invalidSnapshots',
'staleSnapshots',
'workerLimitReached',
)
},
}