Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions astrbot/core/star/command_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescri
async def rename_command(
handler_full_name: str,
new_fragment: str,
aliases: list[str] | None = None,
) -> CommandDescriptor:
descriptor = _build_descriptor_by_full_name(handler_full_name)
if not descriptor:
Expand All @@ -99,9 +100,24 @@ async def rename_command(
if not new_fragment:
raise ValueError("指令名不能为空。")

# 校验主指令名
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
if _is_command_in_use(handler_full_name, candidate_full):
raise ValueError("新的指令名已被其他指令占用,请换一个名称。")
raise ValueError(f"指令名 '{candidate_full}' 已被其他指令占用。")

# 校验别名
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 建议抽取用于别名规范化和 extra_data 访问的共享辅助函数,这样别名处理和过滤器更新的逻辑只在一个地方定义,而不是在多个函数中重复。

你可以通过集中处理别名规范化和 extra_data 访问,并复用描述符/过滤器的别名路径,来降低新增的复杂度。

1. 将别名规范化逻辑集中到一处

目前,同样的逻辑分别出现在 _bind_descriptor_with_config_apply_config_to_runtimerename_command(校验步骤)中。可以用一个小的辅助函数把规则集中起来:

def _normalize_aliases(raw_aliases: Any) -> list[str]:
    if not isinstance(raw_aliases, list):
        return []
    result: list[str] = []
    for x in raw_aliases:
        s = str(x).strip()
        if s:
            result.append(s)
    return result

然后在各处复用它:

# rename_command – 校验 aliases
if aliases:
    normalized_aliases = _normalize_aliases(aliases)
    for alias in normalized_aliases:
        alias_full = _compose_command(descriptor.parent_signature, alias)
        if _is_command_in_use(handler_full_name, alias_full):
            raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。")

existing_cfg = await db_helper.get_command_config(handler_full_name)
merged_extra = dict(existing_cfg.extra_data or {}) if existing_cfg else {}
merged_extra["resolved_aliases"] = _normalize_aliases(aliases or [])
# _bind_descriptor_with_config
extra = config.extra_data or {}
descriptor.aliases = _normalize_aliases(extra.get("resolved_aliases"))
# _apply_config_to_runtime
descriptor.handler.enabled = config.enabled
if descriptor.filter_ref:
    if descriptor.current_fragment:
        _set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
    aliases = _normalize_aliases((config.extra_data or {}).get("resolved_aliases"))
    _set_filter_aliases(descriptor.filter_ref, aliases)

这样就可以去掉重复的“转为字符串 / 去空格 / 过滤空字符串 / 只接受列表”逻辑。

2. 为别名抽象 extra_data 的访问

可以新增一些小的辅助函数,而不是让每个调用点都知道 "resolved_aliases" 这个 key:

def _get_resolved_aliases(config: CommandConfig) -> list[str]:
    extra = config.extra_data or {}
    return _normalize_aliases(extra.get("resolved_aliases"))

def _set_resolved_aliases(
    base_extra: dict[str, Any] | None,
    aliases: list[str] | None,
) -> dict[str, Any]:
    extra = dict(base_extra or {})
    extra["resolved_aliases"] = _normalize_aliases(aliases or [])
    return extra

然后在调用处使用:

# rename_command
existing_cfg = await db_helper.get_command_config(handler_full_name)
merged_extra = _set_resolved_aliases(
    existing_cfg.extra_data if existing_cfg else None,
    aliases,
)
config = await db_helper.upsert_command_config(
    # ...
    extra_data=merged_extra,
    # ...
)
# _bind_descriptor_with_config
descriptor.aliases = _get_resolved_aliases(config)
# _apply_config_to_runtime
if descriptor.filter_ref:
    if descriptor.current_fragment:
        _set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
    _set_filter_aliases(descriptor.filter_ref, _get_resolved_aliases(config))

这样一来,未来如果 extra_data 的结构或别名规则发生变化,只需要修改这些辅助函数即可。

3. 明确过滤器别名在列表 ↔ 集合之间的转换

descriptor.aliasesextra_data["resolved_aliases"] 使用的是列表,而过滤器期望的是 set。可以把这层转换封装到 _set_filter_aliases 中,这样调用点就不需要关心差异:

def _set_filter_aliases(
    filter_ref: CommandFilter | CommandGroupFilter,
    aliases: list[str],
) -> None:
    new_aliases = set(aliases)
    current_aliases = getattr(filter_ref, "alias", set())
    if new_aliases == current_aliases:
        return
    setattr(filter_ref, "alias", new_aliases)
    if hasattr(filter_ref, "_cmpl_cmd_names"):
        filter_ref._cmpl_cmd_names = None

结合 _get_resolved_aliases,这可以让描述符/过滤器的逻辑路径保持一致,并且把“为什么这里用 set 而不是 list”这类细节集中到一个地方管理。

Original comment in English

issue (complexity): Consider extracting shared helpers for alias normalization and extra_data access so alias handling and filter updates are defined once instead of repeated in multiple functions.

You can reduce the new complexity by centralizing alias normalization and extra_data access, and by sharing the descriptor/filter alias paths.

1. Centralize alias normalization

Right now the same logic appears in _bind_descriptor_with_config, _apply_config_to_runtime, and rename_command (validation step). A small helper keeps the rules in one place:

def _normalize_aliases(raw_aliases: Any) -> list[str]:
    if not isinstance(raw_aliases, list):
        return []
    result: list[str] = []
    for x in raw_aliases:
        s = str(x).strip()
        if s:
            result.append(s)
    return result

Then reuse it:

# rename_command – validate aliases
if aliases:
    normalized_aliases = _normalize_aliases(aliases)
    for alias in normalized_aliases:
        alias_full = _compose_command(descriptor.parent_signature, alias)
        if _is_command_in_use(handler_full_name, alias_full):
            raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。")

existing_cfg = await db_helper.get_command_config(handler_full_name)
merged_extra = dict(existing_cfg.extra_data or {}) if existing_cfg else {}
merged_extra["resolved_aliases"] = _normalize_aliases(aliases or [])
# _bind_descriptor_with_config
extra = config.extra_data or {}
descriptor.aliases = _normalize_aliases(extra.get("resolved_aliases"))
# _apply_config_to_runtime
descriptor.handler.enabled = config.enabled
if descriptor.filter_ref:
    if descriptor.current_fragment:
        _set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
    aliases = _normalize_aliases((config.extra_data or {}).get("resolved_aliases"))
    _set_filter_aliases(descriptor.filter_ref, aliases)

This removes repeated “cast to str / strip / non-empty / list-only” logic.

2. Abstract extra_data access for aliases

Instead of each call site knowing about "resolved_aliases", add tiny helpers:

def _get_resolved_aliases(config: CommandConfig) -> list[str]:
    extra = config.extra_data or {}
    return _normalize_aliases(extra.get("resolved_aliases"))

def _set_resolved_aliases(
    base_extra: dict[str, Any] | None,
    aliases: list[str] | None,
) -> dict[str, Any]:
    extra = dict(base_extra or {})
    extra["resolved_aliases"] = _normalize_aliases(aliases or [])
    return extra

Then:

# rename_command
existing_cfg = await db_helper.get_command_config(handler_full_name)
merged_extra = _set_resolved_aliases(
    existing_cfg.extra_data if existing_cfg else None,
    aliases,
)
config = await db_helper.upsert_command_config(
    # ...
    extra_data=merged_extra,
    # ...
)
# _bind_descriptor_with_config
descriptor.aliases = _get_resolved_aliases(config)
# _apply_config_to_runtime
if descriptor.filter_ref:
    if descriptor.current_fragment:
        _set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
    _set_filter_aliases(descriptor.filter_ref, _get_resolved_aliases(config))

Now future changes to extra_data shape or alias rules only touch these helpers.

3. Clarify list ↔ set conversion for filter aliases

descriptor.aliases and extra_data["resolved_aliases"] are lists, while the filter expects a set. Wrap this in _set_filter_aliases so call sites don’t worry about the difference:

def _set_filter_aliases(
    filter_ref: CommandFilter | CommandGroupFilter,
    aliases: list[str],
) -> None:
    new_aliases = set(aliases)
    current_aliases = getattr(filter_ref, "alias", set())
    if new_aliases == current_aliases:
        return
    setattr(filter_ref, "alias", new_aliases)
    if hasattr(filter_ref, "_cmpl_cmd_names"):
        filter_ref._cmpl_cmd_names = None

Combined with _get_resolved_aliases, this makes the descriptor/filter paths consistent and keeps the “why set vs list” detail in one place.

if aliases:
for alias in aliases:
alias = alias.strip()
if not alias:
continue
alias_full = _compose_command(descriptor.parent_signature, alias)
if _is_command_in_use(handler_full_name, alias_full):
raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。")

existing_cfg = await db_helper.get_command_config(handler_full_name)
merged_extra = dict(existing_cfg.extra_data or {}) if existing_cfg else {}
merged_extra["resolved_aliases"] = aliases or []

config = await db_helper.upsert_command_config(
handler_full_name=handler_full_name,
Expand All @@ -114,7 +130,7 @@ async def rename_command(
conflict_key=descriptor.original_command,
resolution_strategy="manual_rename",
note=None,
extra_data=None,
extra_data=merged_extra,
auto_managed=False,
)
_bind_descriptor_with_config(descriptor, config)
Expand Down Expand Up @@ -363,14 +379,27 @@ def _apply_config_to_descriptor(
new_fragment,
)

extra = config.extra_data or {}
resolved_aliases = extra.get("resolved_aliases")
if isinstance(resolved_aliases, list):
descriptor.aliases = [str(x) for x in resolved_aliases if str(x).strip()]


def _apply_config_to_runtime(
descriptor: CommandDescriptor,
config: CommandConfig,
) -> None:
descriptor.handler.enabled = config.enabled
if descriptor.filter_ref and descriptor.current_fragment:
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
if descriptor.filter_ref:
if descriptor.current_fragment:
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
extra = config.extra_data or {}
resolved_aliases = extra.get("resolved_aliases")
if isinstance(resolved_aliases, list):
_set_filter_aliases(
descriptor.filter_ref,
[str(x) for x in resolved_aliases if str(x).strip()],
)


def _bind_configs_to_descriptors(
Expand Down Expand Up @@ -409,6 +438,18 @@ def _set_filter_fragment(
filter_ref._cmpl_cmd_names = None


def _set_filter_aliases(
filter_ref: CommandFilter | CommandGroupFilter,
aliases: list[str],
) -> None:
current_aliases = getattr(filter_ref, "alias", set())
if set(aliases) == current_aliases:
return
setattr(filter_ref, "alias", set(aliases))
if hasattr(filter_ref, "_cmpl_cmd_names"):
filter_ref._cmpl_cmd_names = None


def _is_command_in_use(
target_handler_full_name: str,
candidate_full_command: str,
Expand Down
3 changes: 2 additions & 1 deletion astrbot/dashboard/routes/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ async def rename_command(self):
data = await request.get_json()
handler_full_name = data.get("handler_full_name")
new_name = data.get("new_name")
aliases = data.get("aliases")

if not handler_full_name or not new_name:
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__

try:
await rename_command_service(handler_full_name, new_name)
await rename_command_service(handler_full_name, new_name, aliases=aliases)
except ValueError as exc:
return Response().error(str(exc)).__dict__

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import type { CommandItem } from '../types';

const { tm } = useModuleI18n('features/command');

// Props
defineProps<{
const props = defineProps<{
show: boolean;
command: CommandItem | null;
newName: string;
aliases: string[];
loading: boolean;
}>();

// Emits
const emit = defineEmits<{
(e: 'update:show', value: boolean): void;
(e: 'update:newName', value: string): void;
(e: 'update:aliases', value: string[]): void;
(e: 'confirm'): void;
}>();

const addAlias = () => {
emit('update:aliases', [...props.aliases, '']);
};

const removeAlias = (index: number) => {
const newAliases = [...props.aliases];
newAliases.splice(index, 1);
emit('update:aliases', newAliases);
};

const updateAlias = (index: number, value: string) => {
const newAliases = [...props.aliases];
newAliases[index] = value;
emit('update:aliases', newAliases);
};

const hasAliases = computed(() => (props.aliases || []).some(a => (a ?? '').toString().trim()));
const showAliasEditor = ref(false);
const aliasEditorEverOpened = ref(false);

watch(
() => props.show,
(open) => {
if (!open) return;
// 如果已有别名则默认展开,否则默认收起
showAliasEditor.value = hasAliases.value;
},
);

watch(showAliasEditor, (open) => {
if (open) aliasEditorEverOpened.value = true;
});
</script>

<template>
Expand All @@ -32,7 +68,49 @@ const emit = defineEmits<{
variant="outlined"
density="compact"
autofocus
class="mb-2"
/>

<v-card variant="outlined" class="mt-2" elevation="0">
<div
class="d-flex align-center justify-space-between px-4 py-3"
role="button"
tabindex="0"
@click="showAliasEditor = !showAliasEditor"
@keydown.enter.prevent="showAliasEditor = !showAliasEditor"
@keydown.space.prevent="showAliasEditor = !showAliasEditor"
>
<div class="text-subtitle-1">{{ tm('dialogs.rename.aliases') }}</div>
<v-icon size="20">{{ showAliasEditor ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</div>
<v-divider v-if="showAliasEditor" />
<v-slide-y-transition>
<div v-if="aliasEditorEverOpened" v-show="showAliasEditor" class="px-4 py-3">
<div v-for="(alias, index) in aliases" :key="index" class="d-flex align-center mb-2">
<v-text-field
:model-value="alias"
@update:model-value="updateAlias(index, $event)"
variant="outlined"
density="compact"
hide-details
class="flex-grow-1 mr-2"
/>
<v-btn icon="mdi-delete" variant="text" color="error" density="compact" @click="removeAlias(index)" />
</div>
<v-btn
prepend-icon="mdi-plus"
variant="outlined"
color="primary"
block
size="small"
class="mt-2"
@click="addAlias"
>
{{ tm('dialogs.rename.addAlias') }}
</v-btn>
</div>
</v-slide-y-transition>
</v-card>
</v-card-text>
<v-card-actions>
<v-spacer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function useCommandActions(
show: false,
command: null,
newName: '',
aliases: [],
loading: false
});

Expand Down Expand Up @@ -53,6 +54,7 @@ export function useCommandActions(
const openRenameDialog = (cmd: CommandItem) => {
renameDialog.command = cmd;
renameDialog.newName = cmd.current_fragment || '';
renameDialog.aliases = [...(cmd.aliases || [])];
renameDialog.show = true;
};

Expand All @@ -66,7 +68,8 @@ export function useCommandActions(
try {
const res = await axios.post('/api/commands/rename', {
handler_full_name: renameDialog.command.handler_full_name,
new_name: renameDialog.newName.trim()
new_name: renameDialog.newName.trim(),
aliases: renameDialog.aliases.filter(a => a.trim())
});
if (res.data.status === 'ok') {
toast(successMessage, 'success');
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/components/extension/componentPanel/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ watch(viewMode, async (mode) => {
@update:show="renameDialog.show = $event"
:new-name="renameDialog.newName"
@update:new-name="renameDialog.newName = $event"
:aliases="renameDialog.aliases"
@update:aliases="renameDialog.aliases = $event"
:command="renameDialog.command"
:loading="renameDialog.loading"
@confirm="handleConfirmRename"
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/components/extension/componentPanel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface RenameDialogState {
show: boolean;
command: CommandItem | null;
newName: string;
aliases: string[];
loading: boolean;
}

Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/command.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"rename": {
"title": "Rename Command",
"newName": "New command name",
"aliases": "Manage aliases",
"addAlias": "Add alias",
"cancel": "Cancel",
"confirm": "Confirm"
},
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/command.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"rename": {
"title": "重命名指令",
"newName": "新指令名",
"aliases": "管理别名",
"addAlias": "添加别名",
"cancel": "取消",
"confirm": "确认"
},
Expand Down