Merge pull request #6666 from Aqr-K/feat/plugin-instance-target-enablement

feat(plugin): 插件实例默认调用目标与启停,未指定实例时不再兜底取第一个
This commit is contained in:
Aqr-K
2026-09-13 16:37:27 +08:00
committed by GitHub
parent 30d6d9e146
commit d44480d739
54 changed files with 2853 additions and 73 deletions

View File

@@ -0,0 +1,110 @@
"""3.0.38 插件实例增加启用位,并把本体的装载判据搬到这一列上。
Revision ID: b7d1e4a9c206
Revises: e0e68cbd5756
Create Date: 2026-09-13
"""
import json
from datetime import datetime, timezone
import sqlalchemy as sa
from alembic import op
revision = "b7d1e4a9c206"
down_revision = "e0e68cbd5756"
branch_labels = None
depends_on = None
_TABLE = "plugininstance"
_INSTALLED_KEY = "UserInstalledPlugins"
def _table_names(connection) -> set:
"""读取当前数据库已有表名。"""
return set(sa.inspect(connection).get_table_names())
def _column_names(connection) -> set:
"""读取实例表已有列名;表不存在时为空集。"""
if _TABLE not in _table_names(connection):
return set()
return {column["name"] for column in sa.inspect(connection).get_columns(_TABLE)}
def _installed_plugin_ids(connection) -> list:
"""读取安装清单里的插件 ID,载荷不是字符串数组时按空清单处理。"""
if "systemconfig" not in _table_names(connection):
return []
row = connection.execute(
sa.text("SELECT value FROM systemconfig WHERE key = :key"),
{"key": _INSTALLED_KEY},
).fetchone()
if row is None or row[0] is None:
return []
value = row[0]
if isinstance(value, (str, bytes, bytearray)):
try:
value = json.loads(value)
except (TypeError, ValueError):
return []
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, str) and item]
def upgrade() -> None:
"""加上启用位,并把存量的分身与已安装插件本体一并登记为启用。
存量行必须显式补成启用:这一列同时是本体的装载判据,列建出来默认为假而不回填,
升级后全部插件都会变成「装着但永不加载」。分身按「行存在即在册」的旧语义一律置真;
本体按安装清单补行并置真——清单在此之前一直兼任运行开关,两者在升级这一刻等价。
"""
connection = op.get_bind()
columns = _column_names(connection)
if not columns:
return
if "is_enabled" not in columns:
op.add_column(
_TABLE,
sa.Column(
"is_enabled",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
)
# 布尔列写 Python 布尔而不是整数字面量:PostgreSQL 会拒绝 `= 1`,报
# column is of type boolean but expression is of type integer
connection.execute(
sa.text(f"UPDATE {_TABLE} SET is_enabled = :enabled"),
{"enabled": True},
)
now = datetime.now(timezone.utc).isoformat()
existing = {
row[0]
for row in connection.execute(
sa.text(f"SELECT instance_id FROM {_TABLE}")
).fetchall()
}
for plugin_id in _installed_plugin_ids(connection):
if plugin_id in existing:
continue
connection.execute(
sa.text(
f"INSERT INTO {_TABLE} "
"(instance_id, source_plugin_id, is_enabled, created_at, updated_at) "
"VALUES (:plugin_id, :plugin_id, :enabled, :now, :now)"
),
{"plugin_id": plugin_id, "enabled": True, "now": now},
)
existing.add(plugin_id)
def downgrade() -> None:
"""删除启用位;装载判据随之回落安装清单,停用状态无处表达因而丢弃。"""
if "is_enabled" in _column_names(op.get_bind()):
op.drop_column(_TABLE, "is_enabled")

View File

@@ -0,0 +1,64 @@
"""3.0.37 插件实例描述符增加默认调用目标标记。
Revision ID: e0e68cbd5756
Revises: 487f7e681955
Create Date: 2026-09-13
"""
import sqlalchemy as sa
from alembic import op
revision = "e0e68cbd5756"
down_revision = "487f7e681955"
branch_labels = None
depends_on = None
_TABLE = "plugininstance"
_DEFAULT_TARGET_INDEX = "ux_plugininstance_default_target"
def _column_names() -> set[str]:
"""读取当前表已有列名,兼容重复升级和已由 create_all 建出当前模型的场景。"""
return {column["name"] for column in sa.inspect(op.get_bind()).get_columns(_TABLE)}
def _index_names() -> set[str]:
"""读取当前表已有索引名,兼容重复升级和已由 create_all 建出当前模型的场景。"""
return {index["name"] for index in sa.inspect(op.get_bind()).get_indexes(_TABLE)}
def upgrade() -> None:
"""增加默认调用目标标记列,并建出「同一源插件至多一个默认调用目标」的条件唯一索引。
条件谓词按方言分别给出:布尔列与 ``True`` 比较,SQLite 编译为 ``IS 1``,
PostgreSQL 编译为 ``IS true``;谓词整个丢失会退化成「每个源插件只能有一行
实例」,把插件分身整个锁死,因此必须两个方言各给一份,不能只给一份共用。
"""
columns = _column_names()
if "is_default_target" not in columns:
op.add_column(
_TABLE,
sa.Column(
"is_default_target",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
)
if _DEFAULT_TARGET_INDEX not in _index_names():
op.create_index(
_DEFAULT_TARGET_INDEX,
_TABLE,
["source_plugin_id"],
unique=True,
sqlite_where=sa.column("is_default_target", sa.Boolean()).is_(True),
postgresql_where=sa.column("is_default_target", sa.Boolean()).is_(True),
)
def downgrade() -> None:
"""删除条件唯一索引与默认调用目标标记列。"""
if _DEFAULT_TARGET_INDEX in _index_names():
op.drop_index(_DEFAULT_TARGET_INDEX, table_name=_TABLE)
if "is_default_target" in _column_names():
op.drop_column(_TABLE, "is_default_target")