From 4641fa1a156faa11b03b693c7e6df99f3af6145b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 5 May 2026 00:32:49 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8Dqq=E5=8F=B7?= =?UTF-8?q?=E4=B8=BAint=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=83=A8=E5=88=86=E5=A4=9A=E8=A1=8C=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80prompt=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 ++- .../dynamic-form/DynamicConfigForm.tsx | 44 +++++++++++++-- .../components/dynamic-form/DynamicField.tsx | 54 +++++++++++++++---- dashboard/src/routes/config/bot.tsx | 25 +++++---- dashboard/src/routes/config/model.tsx | 2 +- dashboard/src/routes/resource/emoji/index.tsx | 2 +- dashboard/src/routes/setup/StepForms.tsx | 8 ++- dashboard/src/routes/setup/api.ts | 5 +- dashboard/src/routes/setup/index.tsx | 4 +- dashboard/src/routes/setup/types.ts | 2 +- dashboard/src/types/config-schema.ts | 2 + prompts/en-US/maisaka_replyer.prompt | 4 +- prompts/ja-JP/maisaka_replyer.prompt | 4 +- src/config/config.py | 2 +- src/config/legacy_migration.py | 6 +-- src/config/official_configs.py | 44 +++++++++++++-- src/webui/routers/config.py | 5 ++ 17 files changed, 164 insertions(+), 55 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a66d71ca..f164dc95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,8 +32,7 @@ # 运行/调试/构建/测试/依赖 优先使用uv -依赖项以 pyproject.toml 为准 - +依赖项以 pyproject.toml 为准,要同步更新requirements.txt # 语言规范 项目的首选语言为简体中文,无论是注释语言,日志展示语言,还是 WebUI 展示语言都首要以简体中文为首要实现目标 @@ -52,3 +51,6 @@ # maibot插件开发文档 https://github.com/Mai-with-u/maibot-plugin-sdk/blob/main/docs/guide.md + +# 如何提交maibot插件 +https://github.com/Mai-with-u/plugin-repo/blob/main/CONTRIBUTING.md \ No newline at end of file diff --git a/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx b/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx index b057e6f4..44704bda 100644 --- a/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx +++ b/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx @@ -215,12 +215,50 @@ export const DynamicConfigForm: React.FC = ({ ? [...normalFields, ...advancedFields] : normalFields + const groupFieldsByRow = (fields: FieldSchema[]) => { + const rows: FieldSchema[][] = [] + let currentRow: FieldSchema[] = [] + let currentRowKey: string | undefined + + for (const field of fields) { + const rowKey = field['x-row'] + if (rowKey && rowKey === currentRowKey) { + currentRow.push(field) + continue + } + + if (currentRow.length > 0) { + rows.push(currentRow) + } + + currentRow = [field] + currentRowKey = rowKey + } + + if (currentRow.length > 0) { + rows.push(currentRow) + } + + return rows + } + const renderFieldList = (fields: FieldSchema[]) => ( <> - {fields.map((field, index) => ( - + {groupFieldsByRow(fields).map((row, index) => ( + field.name).join('|')}> {index > 0 && } -
{renderField(field)}
+ {row.length > 1 ? ( +
+ {row.map((field) => ( +
{renderField(field)}
+ ))} +
+ ) : ( +
{renderField(row[0])}
+ )}
))} diff --git a/dashboard/src/components/dynamic-form/DynamicField.tsx b/dashboard/src/components/dynamic-form/DynamicField.tsx index 139165b2..27039cbe 100644 --- a/dashboard/src/components/dynamic-form/DynamicField.tsx +++ b/dashboard/src/components/dynamic-form/DynamicField.tsx @@ -8,6 +8,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Slider } from "@/components/ui/slider" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import type { FieldSchema } from "@/types/config-schema" @@ -115,11 +121,9 @@ export const DynamicField: React.FC = ({ return } - const isRuleTypeSelect = - schema.name === 'rule_type' && - (schema.type === 'select' || schema['x-widget'] === 'select') - const inlineDescription = isRuleTypeSelect ? '' : schema.description - const selectHoverDescription = isRuleTypeSelect ? schema.description : undefined + const optionDescriptions = schema['x-option-descriptions'] ?? {} + const hasOptionDescriptions = Object.keys(optionDescriptions).length > 0 + const inlineDescription = hasOptionDescriptions ? '' : schema.description const renderFieldHeader = () => (
@@ -352,15 +356,43 @@ export const DynamicField: React.FC = ({ return ( ) diff --git a/dashboard/src/routes/config/bot.tsx b/dashboard/src/routes/config/bot.tsx index fe5dce5a..4a6110eb 100644 --- a/dashboard/src/routes/config/bot.tsx +++ b/dashboard/src/routes/config/bot.tsx @@ -490,10 +490,20 @@ function BotConfigPageContent() { const saveSourceCode = async () => { try { setSaving(true) + // 编辑器展示时会把 basic string 内的 \n 展开成真实换行;保存前先转回 TOML 转义序列。 + const escapedSourceCode = sourceCode.replace(/"([^"]*)"/g, (_match, content) => { + const encoded = content + .replace(/\\/g, '\\\\') // 反斜杠必须先转义,避免 \s 等序列被 TOML 当作非法转义 + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + .replace(/\r/g, '\\r') + return `"${encoded}"` + }) // 前端验证 TOML 格式 try { - parseToml(sourceCode) + parseToml(escapedSourceCode) } catch (error) { const errorMsg = error instanceof Error ? error.message : 'TOML 格式错误' const translatedMsg = translateTomlError(errorMsg) @@ -508,18 +518,7 @@ function BotConfigPageContent() { return } - // 将双引号字符串中的实际字符转换回 TOML 转义序列 - // 使用正则表达式只处理双引号字符串内的内容,不影响单引号字符串 - const escaped = sourceCode.replace(/"([^"]*)"/g, (_match, content) => { - const encoded = content - .replace(/\\/g, '\\\\') // 反斜杠(必须放在最前) - .replace(/"/g, '\\"') // 双引号 - .replace(/\n/g, '\\n') // 换行符 - .replace(/\t/g, '\\t') // 制表符 - .replace(/\r/g, '\\r') // 回车符 - return `"${encoded}"` - }) - const result = await updateBotConfigRaw(escaped) + const result = await updateBotConfigRaw(escapedSourceCode) if (!result.success) { setHasTomlError(true) const errorMsg = result.error diff --git a/dashboard/src/routes/config/model.tsx b/dashboard/src/routes/config/model.tsx index f6fb5b57..278dc7bc 100644 --- a/dashboard/src/routes/config/model.tsx +++ b/dashboard/src/routes/config/model.tsx @@ -889,7 +889,7 @@ function ModelConfigPageContent() { 开始引导
diff --git a/dashboard/src/routes/resource/emoji/index.tsx b/dashboard/src/routes/resource/emoji/index.tsx index 8a604ee2..3f3ee92b 100644 --- a/dashboard/src/routes/resource/emoji/index.tsx +++ b/dashboard/src/routes/resource/emoji/index.tsx @@ -63,7 +63,7 @@ export function EmojiManagementPage() { const [page, setPage] = useState(1) const [total, setTotal] = useState(0) const [pageSize, setPageSize] = useState(20) - const [registeredFilter, setRegisteredFilter] = useState('all') + const [registeredFilter, setRegisteredFilter] = useState('registered') const [bannedFilter, setBannedFilter] = useState('all') const [formatFilter, setFormatFilter] = useState('all') const [sortBy, setSortBy] = useState('usage_count') diff --git a/dashboard/src/routes/setup/StepForms.tsx b/dashboard/src/routes/setup/StepForms.tsx index 833d3085..6ae55cf6 100644 --- a/dashboard/src/routes/setup/StepForms.tsx +++ b/dashboard/src/routes/setup/StepForms.tsx @@ -44,7 +44,7 @@ function normalizePlatform(raw: string): string { function deriveSelectedPlatform(config: BotBasicConfig): { selected: string; customName: string } { const platform = config.platform // Legacy: no platform set but has QQ account - if (!platform && config.qq_account > 0) { + if (!platform && config.qq_account.trim()) { return { selected: 'qq', customName: '' } } if (!platform) { @@ -96,9 +96,7 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) { const customPlatformName = customPlatformNameOverride ?? derived.customName const primaryAccount = selectedPlatform === 'qq' - ? config.qq_account > 0 - ? String(config.qq_account) - : '' + ? config.qq_account.trim() : config.platform ? getPrimaryAccount(config.platforms, config.platform) : '' @@ -141,7 +139,7 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) { if (normalized === 'qq') { onChange({ ...config, - qq_account: Number(accountId) || 0, + qq_account: accountId.trim(), platform: 'qq', }) } else { diff --git a/dashboard/src/routes/setup/api.ts b/dashboard/src/routes/setup/api.ts index 5332b7fe..ae793f9b 100644 --- a/dashboard/src/routes/setup/api.ts +++ b/dashboard/src/routes/setup/api.ts @@ -61,10 +61,11 @@ export async function loadBotBasicConfig(): Promise { ) const data = throwIfError(result) const botConfig = (data.config.bot || {}) as Partial + const qqAccount = String(botConfig.qq_account ?? '').trim() return { - platform: botConfig.platform || (botConfig.qq_account ? 'qq' : ''), - qq_account: botConfig.qq_account || 0, + platform: botConfig.platform || (qqAccount ? 'qq' : ''), + qq_account: qqAccount, platforms: botConfig.platforms || [], nickname: botConfig.nickname || '', alias_names: botConfig.alias_names || [], diff --git a/dashboard/src/routes/setup/index.tsx b/dashboard/src/routes/setup/index.tsx index 787d6854..87eaed02 100644 --- a/dashboard/src/routes/setup/index.tsx +++ b/dashboard/src/routes/setup/index.tsx @@ -106,7 +106,7 @@ function SetupPageContent() { // 步骤1:Bot基础信息 const [botBasic, setBotBasic] = useState({ platform: '', - qq_account: 0, + qq_account: '', platforms: [], nickname: '', alias_names: [], @@ -239,7 +239,7 @@ function SetupPageContent() { if (!config.platform) return t('setupPage.validation.selectPlatform') if (!config.nickname.trim()) return t('setupPage.validation.enterNickname') if (config.platform === 'qq') { - if (!config.qq_account || config.qq_account <= 0) { + if (!config.qq_account.trim()) { return t('setupPage.validation.enterQqAccount') } } else { diff --git a/dashboard/src/routes/setup/types.ts b/dashboard/src/routes/setup/types.ts index 91414358..da650a68 100644 --- a/dashboard/src/routes/setup/types.ts +++ b/dashboard/src/routes/setup/types.ts @@ -10,7 +10,7 @@ export interface SetupStep { // 步骤1:Bot基础信息 export interface BotBasicConfig { platform: string // Primary platform name (normalized, lowercase) - qq_account: number // QQ account (preserved always for webui compat) + qq_account: string // QQ account (preserved always for webui compat) platforms: string[] // Other platform accounts "platform:account" nickname: string alias_names: string[] diff --git a/dashboard/src/types/config-schema.ts b/dashboard/src/types/config-schema.ts index 6eb17b71..7a9c29b8 100644 --- a/dashboard/src/types/config-schema.ts +++ b/dashboard/src/types/config-schema.ts @@ -40,6 +40,8 @@ export interface FieldSchema { 'x-icon'?: string 'x-layout'?: 'inline-right' 'x-input-width'?: string + 'x-option-descriptions'?: Record + 'x-row'?: string 'x-textarea-min-height'?: number 'x-textarea-rows'?: number advanced?: boolean diff --git a/prompts/en-US/maisaka_replyer.prompt b/prompts/en-US/maisaka_replyer.prompt index 4e712a75..ad6265fd 100644 --- a/prompts/en-US/maisaka_replyer.prompt +++ b/prompts/en-US/maisaka_replyer.prompt @@ -1,7 +1,5 @@ You are chatting in a QQ group. Below is the content currently being discussed in the group, including chat records and images in the chat. -Messages marked with {bot_name} (you) are your own messages, so please distinguish them carefully: - -{time_block} +Messages marked as your own messages should be distinguished carefully: {identity} You are chatting in the group now. Please read the previous chat records, grasp the current topic, and then give a natural, colloquial reply. diff --git a/prompts/ja-JP/maisaka_replyer.prompt b/prompts/ja-JP/maisaka_replyer.prompt index 743cb6e9..69a6b150 100644 --- a/prompts/ja-JP/maisaka_replyer.prompt +++ b/prompts/ja-JP/maisaka_replyer.prompt @@ -1,7 +1,5 @@ あなたは QQ グループで会話しています。以下はグループ内で現在話されている内容で、チャット記録とチャット中の画像が含まれます。 -{bot_name}(あなた) と記された発言はあなた自身の発言です。区別に注意してください: - -{time_block} +あなた自身の発言として記された発言は、区別に注意してください: {identity} 今あなたはグループ内で会話しています。これまでのチャット記録を読み、現在の話題を把握したうえで、日常的で口語的な返信をしてください。 diff --git a/src/config/config.py b/src/config/config.py index 63e1fcc3..042515dd 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -56,7 +56,7 @@ BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute() MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute() LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute() A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute() -MMC_VERSION: str = "1.0.0-pre.10" +MMC_VERSION: str = "1.0.0-pre.11" CONFIG_VERSION: str = "8.10.6" MODEL_CONFIG_VERSION: str = "1.15.3" diff --git a/src/config/legacy_migration.py b/src/config/legacy_migration.py index fad11ac7..05714afd 100644 --- a/src/config/legacy_migration.py +++ b/src/config/legacy_migration.py @@ -343,10 +343,10 @@ def try_migrate_legacy_bot_config_dict(data: dict[str, Any]) -> MigrationResult: reasons: list[str] = [] bot = _as_dict(data.get("bot")) - if bot is not None and isinstance(bot.get("qq_account"), str) and not bot["qq_account"].strip(): - bot["qq_account"] = 0 + if bot is not None and isinstance(bot.get("qq_account"), int): + bot["qq_account"] = str(bot["qq_account"]) if bot["qq_account"] > 0 else "" migrated_any = True - reasons.append("bot.qq_account_empty") + reasons.append("bot.qq_account_int_to_string") chat = _as_dict(data.get("chat")) if chat is not None and _migrate_chat_talk_value_rules(chat): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 2ff199dd..148dc157 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -4,6 +4,17 @@ import re from .config_base import ConfigBase, Field +RULE_TYPE_OPTION_DESCRIPTIONS = { + "group": "群聊聊天流,item_id 填群号或群聊 ID", + "private": "私聊聊天流,item_id 填用户 ID", +} + +VISUAL_MODE_OPTION_DESCRIPTIONS = { + "auto": "根据模型信息自动选择文本或多模态模式", + "text": "纯文本模式,不向模型发送视觉输入", + "multimodal": "多模态模式,会向模型发送视觉输入", +} + """ 须知: 1. 本文件中记录了所有的配置项 @@ -33,8 +44,8 @@ class BotConfig(ConfigBase): ) """平台""" - qq_account: int = Field( - default=0, + qq_account: str = Field( + default="", json_schema_extra={ "x-widget": "input", "x-icon": "user", @@ -141,6 +152,8 @@ class VisualConfig(ConfigBase): json_schema_extra={ "x-widget": "select", "x-icon": "git-branch", + "x-option-descriptions": VISUAL_MODE_OPTION_DESCRIPTIONS, + "x-row": "visual-modes", }, ) """规划器模式,auto根据模型信息自动选择,text为纯文本模式,multimodal为多模态模式""" @@ -150,6 +163,8 @@ class VisualConfig(ConfigBase): json_schema_extra={ "x-widget": "select", "x-icon": "git-branch", + "x-option-descriptions": VISUAL_MODE_OPTION_DESCRIPTIONS, + "x-row": "visual-modes", }, ) """回复器模式,auto根据模型信息自动选择,text为纯文本模式,multimodal为多模态模式""" @@ -162,7 +177,13 @@ class TalkRulesItem(ConfigBase): item_id: str = "" """用户ID,与平台一起留空表示全局""" - rule_type: Literal["group", "private"] = "group" + rule_type: Literal["group", "private"] = Field( + default="group", + json_schema_extra={ + "x-widget": "select", + "x-option-descriptions": RULE_TYPE_OPTION_DESCRIPTIONS, + }, + ) """聊天流类型,group(群聊)或private(私聊)""" time: str = "" @@ -185,6 +206,7 @@ class ChatConfig(ConfigBase): json_schema_extra={ "x-widget": "slider", "x-icon": "message-circle", + "x-row": "talk-values", "step": 0.1, }, ) @@ -197,6 +219,7 @@ class ChatConfig(ConfigBase): json_schema_extra={ "x-widget": "slider", "x-icon": "message-circle", + "x-row": "talk-values", "step": 0.1, }, ) @@ -207,11 +230,19 @@ class ChatConfig(ConfigBase): json_schema_extra={ "x-widget": "switch", "x-icon": "at-sign", + "x-row": "reply-switches", }, ) """是否启用提及必回复""" - inevitable_at_reply: bool = Field(default=True) + inevitable_at_reply: bool = Field( + default=True, + json_schema_extra={ + "x-widget": "switch", + "x-icon": "at-sign", + "x-row": "reply-switches", + }, + ) """是否启用at必回复""" enable_at: bool = Field( @@ -241,6 +272,7 @@ class ChatConfig(ConfigBase): "x-icon": "layers", "x-layout": "inline-right", "x-input-width": "12rem", + "x-row": "context-sizes", }, ) """上下文长度""" @@ -252,6 +284,7 @@ class ChatConfig(ConfigBase): "x-icon": "layers", "x-layout": "inline-right", "x-input-width": "12rem", + "x-row": "context-sizes", }, ) """私聊上下文长度""" @@ -395,6 +428,7 @@ class TargetItem(ConfigBase): json_schema_extra={ "x-widget": "select", "x-icon": "users", + "x-option-descriptions": RULE_TYPE_OPTION_DESCRIPTIONS, }, ) """聊天流类型,group(群聊)或private(私聊)""" @@ -1049,6 +1083,7 @@ class LearningItem(ConfigBase): json_schema_extra={ "x-widget": "select", "x-icon": "users", + "x-option-descriptions": RULE_TYPE_OPTION_DESCRIPTIONS, }, ) """聊天流类型,group(群聊)或private(私聊)""" @@ -1751,6 +1786,7 @@ class ExtraPromptItem(ConfigBase): json_schema_extra={ "x-widget": "select", "x-icon": "users", + "x-option-descriptions": RULE_TYPE_OPTION_DESCRIPTIONS, }, ) """聊天流类型,group(群聊)或private(私聊)""" diff --git a/src/webui/routers/config.py b/src/webui/routers/config.py index 8aa36c97..86ec34a0 100644 --- a/src/webui/routers/config.py +++ b/src/webui/routers/config.py @@ -132,6 +132,11 @@ def _toml_to_plain_dict(obj: Any) -> Any: def _coerce_numeric_value(value: Any, target_type: Any) -> Any: """根据配置字段类型,把旧 WebUI 可能写入的数字字符串还原为数字。""" + if target_type is str: + if isinstance(value, (int, float)): + return str(value) + return value + if target_type is int: if isinstance(value, str): try: