Skip to content
Open
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
2 changes: 1 addition & 1 deletion astrbot/dashboard/routes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _validate_template_list(value, meta, path_key, errors, validate_fn) -> None:
validate_fn(
item,
template_meta.get("items", {}),
path=f"{item_path}.",
path=f"{path_key}.templates.{template_key}.",
)


Expand Down
21 changes: 18 additions & 3 deletions astrbot/dashboard/routes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,39 @@ def get_schema_item(schema: dict | None, key_path: str) -> dict | None:
同时支持:
- 扁平 schema(直接 key 命中)
- 嵌套 object schema({type: "object", items: {...}})
- template_list schema(<field>.templates.<template>.items)
"""

if not isinstance(schema, dict) or not key_path:
return None
if key_path in schema:
return schema.get(key_path)

current = schema
parts = key_path.split(".")
for idx, part in enumerate(parts):
current = schema
idx = 0
while idx < len(parts):
part = parts[idx]
if part not in current:
return None
meta = current.get(part)
if idx == len(parts) - 1:
return meta
if not isinstance(meta, dict) or meta.get("type") != "object":
return None
if not isinstance(meta, dict) or meta.get("type") != "template_list":
return None
if idx + 2 >= len(parts) or parts[idx + 1] != "templates":
return None
template_meta = meta.get("templates", {}).get(parts[idx + 2])
if not isinstance(template_meta, dict):
return None
Comment on lines +42 to +44
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation of get_schema_item is not fully defensive when handling template_list types. If the templates field in the schema exists but is not a dictionary (e.g., it is None or a string due to a malformed schema), meta.get("templates", {}) will return that non-dictionary value, and the subsequent .get() call will raise an AttributeError. It is safer to explicitly verify that templates is a dictionary before attempting to access its keys.

Suggested change
template_meta = meta.get("templates", {}).get(parts[idx + 2])
if not isinstance(template_meta, dict):
return None
templates = meta.get("templates")
if not isinstance(templates, dict):
return None
template_meta = templates.get(parts[idx + 2])
if not isinstance(template_meta, dict):
return None

if idx + 2 == len(parts) - 1:
return template_meta
current = template_meta.get("items", {})
idx += 3
continue
current = meta.get("items", {})
idx += 1
return None


Expand Down
50 changes: 43 additions & 7 deletions dashboard/src/components/shared/AstrBotConfigV4.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,25 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ''
},
pluginName: {
type: String,
default: ''
},
pluginI18n: {
type: Object,
default: () => ({})
},
pathPrefix: {
type: String,
default: ''
}
})

const { t } = useI18n()
const { getRaw } = useModuleI18n('features/config-metadata')
const { tm: tmConfig } = useModuleI18n('features/config')
const { translateIfKey } = useConfigTextResolver()
const { translateIfKey, resolveConfigText } = useConfigTextResolver(props)

const hintMarkdown = new MarkdownIt({
linkify: true,
Expand Down Expand Up @@ -114,6 +126,18 @@ function createSelectorModel(selector) {
})
}

function getItemPath(key) {
return props.pathPrefix ? `${props.pathPrefix}.${key}` : key
}

function getItemDescription(itemKey, itemMeta) {
return resolveConfigText(getItemPath(itemKey), 'description', itemMeta?.description) || itemKey
}

function getItemHint(itemKey, itemMeta) {
return resolveConfigText(getItemPath(itemKey), 'hint', itemMeta?.hint)
}

function openEditorDialog(key, value, theme, language) {
currentEditingKey.value = key
currentEditingLanguage.value = language || 'json'
Expand Down Expand Up @@ -143,8 +167,8 @@ function shouldShowItem(itemMeta, itemKey) {

const searchableText = [
itemKey,
translateIfKey(itemMeta?.description || ''),
translateIfKey(itemMeta?.hint || '')
getItemDescription(itemKey, itemMeta),
getItemHint(itemKey, itemMeta)
].join(' ').toLowerCase()

return searchableText.includes(keyword)
Expand Down Expand Up @@ -259,13 +283,13 @@ function getSpecialSubtype(value) {
<v-col cols="12" sm="6" class="property-info">
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ translateIfKey(itemMeta?.description) || itemKey }}
{{ getItemDescription(itemKey, itemMeta) }}
<span class="property-key">({{ itemKey }})</span>
</v-list-item-title>

<v-list-item-subtitle class="property-hint">
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint">‼️</span>
<span v-html="renderHint(itemMeta?.hint)"></span>
<span v-html="renderHint(getItemHint(itemKey, itemMeta))"></span>
</v-list-item-subtitle>
</v-list-item>
</v-col>
Expand All @@ -274,12 +298,18 @@ function getSpecialSubtype(value) {
v-if="itemMeta?.type === 'template_list'"
v-model="createSelectorModel(itemKey).value"
:templates="itemMeta?.templates || {}"
:plugin-name="pluginName"
:plugin-i18n="pluginI18n"
:config-path="getItemPath(itemKey)"
class="config-field"
/>
<ConfigItemRenderer
v-else
v-model="createSelectorModel(itemKey).value"
:item-meta="itemMeta || null"
:plugin-name="pluginName"
:plugin-i18n="pluginI18n"
:config-key="getItemPath(itemKey)"
:show-fullscreen-btn="!!itemMeta?.editor_mode"
@open-fullscreen="openEditorDialog(itemKey, iterable, itemMeta?.editor_theme, itemMeta?.editor_language)"
/>
Expand Down Expand Up @@ -339,13 +369,13 @@ function getSpecialSubtype(value) {
<v-col cols="12" sm="6" class="property-info">
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ translateIfKey(itemMeta?.description) || itemKey }}
{{ getItemDescription(itemKey, itemMeta) }}
<span class="property-key">({{ itemKey }})</span>
</v-list-item-title>

<v-list-item-subtitle class="property-hint">
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint">‼️</span>
<span v-html="renderHint(itemMeta?.hint)"></span>
<span v-html="renderHint(getItemHint(itemKey, itemMeta))"></span>
</v-list-item-subtitle>
</v-list-item>
</v-col>
Expand All @@ -354,12 +384,18 @@ function getSpecialSubtype(value) {
v-if="itemMeta?.type === 'template_list'"
v-model="createSelectorModel(itemKey).value"
:templates="itemMeta?.templates || {}"
:plugin-name="pluginName"
:plugin-i18n="pluginI18n"
:config-path="getItemPath(itemKey)"
class="config-field"
/>
<ConfigItemRenderer
v-else
v-model="createSelectorModel(itemKey).value"
:item-meta="itemMeta || null"
:plugin-name="pluginName"
:plugin-i18n="pluginI18n"
:config-key="getItemPath(itemKey)"
:show-fullscreen-btn="!!itemMeta?.editor_mode"
@open-fullscreen="openEditorDialog(itemKey, iterable, itemMeta?.editor_theme, itemMeta?.editor_language)"
/>
Expand Down
56 changes: 54 additions & 2 deletions dashboard/src/components/shared/TemplateListEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@
</v-btn>
<div class="d-flex flex-column">
<v-list-item-title class="property-name">{{ templateLabel(entry.__template_key) }}</v-list-item-title>
<v-list-item-subtitle class="property-hint" v-if="getTemplate(entry)?.hint || getTemplate(entry)?.description">
{{ templateText(entry.__template_key, 'hint', getTemplate(entry)?.hint || getTemplate(entry)?.description) }}
<v-list-item-subtitle class="property-hint entry-display-text" v-if="templateDisplayText(entry)">
{{ templateDisplayText(entry) }}
</v-list-item-subtitle>
<v-list-item-subtitle class="property-hint" v-if="templateHintText(entry)">
{{ templateHintText(entry) }}
</v-list-item-subtitle>
</div>
</div>
Expand Down Expand Up @@ -201,6 +204,7 @@ const defaultValueMap = {
string: '',
text: '',
list: [],
file: [],
object: {},
template_list: []
}
Expand Down Expand Up @@ -348,6 +352,49 @@ function getTemplate(entry) {
return props.templates?.[key] || null
}

function templateHintText(entry) {
const template = getTemplate(entry)
if (!template || template.hide_hint_in_list) return ''
return templateText(entry.__template_key, 'hint', template.hint || template.description || '')
}

function getItemMetaBySelector(itemsMeta = {}, selector = '') {
const keys = selector.split('.').filter(Boolean)
let currentItems = itemsMeta
let currentMeta = null

for (let i = 0; i < keys.length; i++) {
currentMeta = currentItems?.[keys[i]]
if (!currentMeta) return null
if (i < keys.length - 1) {
if (currentMeta.type !== 'object') return null
currentItems = currentMeta.items || {}
}
}

return currentMeta
}

function templateDisplayText(entry) {
const template = getTemplate(entry)
const displayItem = template?.display_item
if (!template || typeof displayItem !== 'string' || !displayItem) return ''

const displayMeta = getItemMetaBySelector(template.items || {}, displayItem)
if (displayMeta?.type !== 'string') return ''

const value = getValueBySelector(entry, displayItem)
if (typeof value !== 'string' || !value.trim()) return ''

const label = templateItemText(
entry.__template_key,
displayItem,
'description',
displayMeta.description || displayItem,
)
return `${label}: ${value.trim()}`
}

function getValueBySelector(obj, selector) {
const keys = selector.split('.')
let current = obj
Expand Down Expand Up @@ -450,6 +497,11 @@ function hasVisibleItemsAfter(entries, currentIndex, entry) {
margin-top: 2px;
}

.entry-display-text {
color: var(--v-theme-primary);
font-weight: 500;
}

.property-key {
font-size: 0.85em;
opacity: 0.7;
Expand Down
13 changes: 13 additions & 0 deletions docs/en/dev/star/guides/plugin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,14 @@ Plugin developers can add a template-style configuration to `_conf_schema` in th
"template_1": {
"name": "Template One",
"hint":"hint",
"display_item": "attr_name",
"hide_hint_in_list": true,
"items": {
"attr_name": {
"description": "Attribute Name",
"type": "string",
"default": ""
},
"attr_a": {
"description": "Attribute A",
"type": "int",
Expand Down Expand Up @@ -187,6 +194,7 @@ Saved config example:
"field_id": [
{
"__template_key": "template_1",
"attr_name": "",
"attr_a": 10,
"attr_b": true
},
Expand All @@ -198,6 +206,11 @@ Saved config example:
]
```

Templates also support these optional fields:

- `display_item`: Specifies the key of a `string` item inside the template `items`. When set, the WebUI shows that field's current value in the collapsed list of added template entries, for example `Attribute Name: my-adapter`, making it easier to distinguish multiple entries created from the same template. Dot paths are supported for fields inside nested objects, for example `meta.name`.
- `hide_hint_in_list`: When set to `true`, the WebUI hides the template `hint` in the collapsed list of added template entries. The template selection dropdown still shows the `hint`, and hints for fields inside the expanded entry are not affected.

<img width="1000" alt="image" src="https://github.com/user-attachments/assets/74876d30-11a4-491b-a7a0-8ebe8d603782" />


Expand Down
13 changes: 13 additions & 0 deletions docs/zh/dev/star/guides/plugin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,14 @@ AstrBot 提供了“强大”的配置解析和可视化功能。能够让用户
"template_1": {
"name": "Template One",
"hint":"hint",
"display_item": "attr_name",
"hide_hint_in_list": true,
"items": {
"attr_name": {
"description": "Attribute Name",
"type": "string",
"default": ""
},
"attr_a": {
"description": "Attribute A",
"type": "int",
Expand Down Expand Up @@ -187,6 +194,7 @@ AstrBot 提供了“强大”的配置解析和可视化功能。能够让用户
"field_id": [
{
"__template_key": "template_1",
"attr_name": "",
"attr_a": 10,
"attr_b": true
},
Expand All @@ -198,6 +206,11 @@ AstrBot 提供了“强大”的配置解析和可视化功能。能够让用户
]
```

模板本身还支持以下可选字段:

- `display_item`: 指定模板 `items` 中一个 `string` 类型字段的 key。设置后,WebUI 会在已添加模板条目的折叠列表中显示该字段当前值,例如 `Attribute Name: my-adapter`,便于添加多个同类型模板时快速区分。支持用点号选择嵌套 object 中的字段,例如 `meta.name`。
- `hide_hint_in_list`: 设置为 `true` 时,WebUI 会在已添加模板条目的折叠列表中隐藏该模板的 `hint`。添加模板时的下拉菜单仍会显示 `hint`,展开条目后各配置项自己的 `hint` 也不受影响。

<img width="1000" alt="image" src="https://github.com/user-attachments/assets/74876d30-11a4-491b-a7a0-8ebe8d603782" />

## 在插件中使用配置
Expand Down
Loading
Loading