Skip to content

Commit add8670

Browse files
authored
Merge branch 'Mai-with-u:dev' into dev
2 parents 47f32e4 + 52bf27b commit add8670

10 files changed

Lines changed: 171 additions & 8 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,14 @@ MaiSaka 不仅仅是一个机器人,不仅仅是一个可以帮你完成任务
115115

116116
| 类别 / Category | 群组 / Group | 说明 / Description |
117117
| :--- | :--- | :--- |
118-
| **技术交流**<br><sub><sup>Technical</sup></sub> | [麦麦脑电图](https://qm.qq.com/q/RzmCiRtHEW)<br><sub><sup>MaiBrain EEG</sup></sub> | 技术交流 / 答疑<br><sub><sup>Technical discussion / Q&A</sup></sub> |
119-
| **技术交流**<br><sub><sup>Technical</sup></sub> | [麦麦大脑磁共振](https://qm.qq.com/q/VQ3XZrWgMs)<br><sub><sup>MaiBrain MRI</sup></sub> | 技术交流 / 答疑<br><sub><sup>Technical discussion / Q&A</sup></sub> |
118+
| **技术交流**<br><sub><sup>Technical</sup></sub> | 麦麦脑电图:571780722
119+
<br><sub><sup>MaiBrain EEG</sup></sub> | 技术交流 / 答疑<br><sub><sup>Technical discussion / Q&A</sup></sub> |
120+
| **技术交流**<br><sub><sup>Technical</sup></sub> | 麦麦大脑磁共振:766798517
121+
<br><sub><sup>MaiBrain MRI</sup></sub> | 技术交流 / 答疑<br><sub><sup>Technical discussion / Q&A</sup></sub> |
120122
| **技术交流**<br><sub><sup>Technical</sup></sub> | [麦麦要当 VTB](https://qm.qq.com/q/wGePTl1UyY)<br><sub><sup>Mai Wants to Be a VTuber</sup></sub> | 技术交流 / 答疑<br><sub><sup>Technical discussion / Q&A</sup></sub> |
121123
| **闲聊吹水**<br><sub><sup>Casual Chat</sup></sub> | [麦麦之闲聊群](https://qm.qq.com/q/JxvHZnxyec)<br><sub><sup>Mai Casual Chat Group</sup></sub> | 仅限闲聊,不答疑<br><sub><sup>Casual chat only, no support</sup></sub> |
122-
| **插件开发**<br><sub><sup>Plugin Development</sup></sub> | [插件开发群](https://qm.qq.com/q/1036092828)<br><sub><sup>Plugin Dev Group</sup></sub> | 进阶开发与测试<br><sub><sup>Advanced development and testing</sup></sub> |
124+
| **插件开发**<br><sub><sup>Plugin Development</sup></sub> | 插件开发群:1036092828
125+
<br><sub><sup>Plugin Dev Group</sup></sub> | 进阶开发与测试<br><sub><sup>Advanced development and testing</sup></sub> |
123126

124127
---
125128

dashboard/src/i18n/locales/zh.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@
518518
},
519519
"header": {
520520
"title": "首次配置向导",
521-
"description": "让我们一起完成 {{appName}} 的初始配置"
521+
"description": "让我们一起完成麦麦的初始配置"
522522
},
523523
"progress": {
524524
"stepCounter": "步骤 {{current}} / {{total}}"
@@ -735,7 +735,7 @@
735735
"restartingTip": "🔄 配置已保存,正在重启主程序...",
736736
"checking": "检查服务状态",
737737
"checkingDesc": "等待服务恢复... ({{current}}/{{max}})",
738-
"checkingTip": "⏳ 正在等待服务恢复,请勿关闭页面...",
738+
"checkingTip": "重启中,请勿关闭页面...",
739739
"success": "重启成功",
740740
"successDesc": "正在跳转到登录页面...",
741741
"successTip": "✅ 配置已生效,服务运行正常",

dashboard/src/routes/config/model.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ function unwrapModelConfig(data: unknown): Record<string, unknown> {
8585
return data as Record<string, unknown>
8686
}
8787

88+
const ADVANCED_MODEL_TASK_NAMES = new Set(['memory', 'learner', 'emoji', 'voice'])
89+
8890
function getRequiredTaskNames(schema: ConfigSchema | null): Set<string> {
8991
return new Set(
9092
(schema?.fields ?? [])
91-
.filter((field) => field.type === 'object' && !field.advanced)
93+
.filter((field) => field.type === 'object' && !field.advanced && !ADVANCED_MODEL_TASK_NAMES.has(field.name))
9294
.map((field) => field.name)
9395
)
9496
}

dashboard/src/routes/setup/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ function SetupPageContent() {
220220
description: t('setupPage.toast.saveSuccessDescription', {
221221
step: steps[currentStep].title,
222222
}),
223+
duration: 1000,
223224
})
224225
return true
225226
} catch (error) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest
2+
3+
from src.maisaka import chat_loop_service as chat_loop_service_module
4+
from src.maisaka.chat_loop_service import MaisakaChatLoopService
5+
6+
7+
def test_expression_selector_uses_text_context() -> None:
8+
assert MaisakaChatLoopService._resolve_enable_visual_message("expression_selector") is False
9+
10+
11+
def test_reply_effect_judge_uses_text_context() -> None:
12+
assert MaisakaChatLoopService._resolve_enable_visual_message("reply_effect_judge") is False
13+
14+
15+
@pytest.mark.parametrize("request_kind", ["planner", "timing_gate"])
16+
def test_planner_requests_follow_planner_visual_mode(
17+
monkeypatch: pytest.MonkeyPatch,
18+
request_kind: str,
19+
) -> None:
20+
monkeypatch.setattr(chat_loop_service_module, "resolve_enable_visual_planner", lambda: False)
21+
22+
assert MaisakaChatLoopService._resolve_enable_visual_message(request_kind) is False
23+
24+
25+
def test_visual_sub_agent_requests_keep_visual_context() -> None:
26+
assert MaisakaChatLoopService._resolve_enable_visual_message("emotion") is True

pytests/webui/test_plugin_management_routes.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,67 @@ async def clone_repository(self, **kwargs):
134134
assert plugin_path is not None
135135
manifest = json.loads((plugin_path / "_manifest.json").read_text(encoding="utf-8"))
136136
assert manifest["id"] == "market.legacy"
137+
138+
139+
def test_install_plugin_cleans_config_only_residue(client: TestClient, monkeypatch):
140+
residue_path, _ = support_module.get_plugin_candidate_paths("market.residue")
141+
residue_path.mkdir(parents=True)
142+
(residue_path / "config.toml").write_text("[plugin]\nenabled = true\n", encoding="utf-8")
143+
144+
class FakeGitMirrorService:
145+
async def clone_repository(self, **kwargs):
146+
target_path = kwargs["target_path"]
147+
assert target_path == residue_path
148+
assert not (target_path / "config.toml").exists()
149+
target_path.mkdir(parents=True, exist_ok=True)
150+
(target_path / "_manifest.json").write_text(
151+
json.dumps(
152+
{
153+
"manifest_version": 2,
154+
"id": "market.residue",
155+
"name": "Residue Plugin",
156+
"version": "1.0.0",
157+
"author": {"name": "market"},
158+
}
159+
),
160+
encoding="utf-8",
161+
)
162+
return {"success": True}
163+
164+
monkeypatch.setattr(management_module, "get_git_mirror_service", lambda: FakeGitMirrorService())
165+
166+
response = client.post(
167+
"/api/webui/plugins/install",
168+
json={
169+
"plugin_id": "market.residue",
170+
"repository_url": "https://github.com/market/residue",
171+
"branch": "main",
172+
},
173+
)
174+
175+
assert response.status_code == 200
176+
assert (residue_path / "_manifest.json").exists()
177+
assert not (residue_path / "config.toml").exists()
178+
179+
180+
def test_uninstall_plugin_releases_runtime_before_delete(client: TestClient, monkeypatch):
181+
from src.plugin_runtime import integration as integration_module
182+
183+
plugin_path = support_module.resolve_installed_plugin_path("test.demo")
184+
assert plugin_path is not None
185+
reload_calls = []
186+
187+
class FakeRuntimeManager:
188+
async def reload_plugins_globally(self, plugin_ids, reason="manual"):
189+
reload_calls.append((list(plugin_ids), reason))
190+
config_text = (plugin_path / "config.toml").read_text(encoding="utf-8")
191+
assert "enabled = false" in config_text
192+
return True
193+
194+
monkeypatch.setattr(integration_module, "get_plugin_runtime_manager", lambda: FakeRuntimeManager())
195+
196+
response = client.post("/api/webui/plugins/uninstall", json={"plugin_id": "test.demo"})
197+
198+
assert response.status_code == 200
199+
assert reload_calls == [(["test.demo"], "uninstall")]
200+
assert not plugin_path.exists()

src/maisaka/chat_loop_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,5 +1016,7 @@ def _filter_history_for_request_kind(
10161016
def _resolve_enable_visual_message(request_kind: str) -> bool:
10171017
if request_kind in {"planner", "timing_gate"}:
10181018
return resolve_enable_visual_planner()
1019+
if request_kind in {"expression_selector", "reply_effect_judge"}:
1020+
return False
10191021
return True
10201022

src/plugin_runtime/component_query.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
from src.core.types import ActionActivationType, ActionInfo, CommandInfo, ComponentInfo, ComponentType, ToolInfo
2222
from src.llm_models.payload_content.tool_option import normalize_tool_option
23+
from src.plugin_runtime.host.message_utils import PluginMessageUtils
2324

2425
if TYPE_CHECKING:
2526
from src.plugin_runtime.host.component_registry import ActionEntry, CommandEntry, ComponentEntry, ToolEntry
@@ -503,6 +504,10 @@ async def _executor(**kwargs: Any) -> tuple[bool, Optional[str], bool]:
503504
"user_id": str(getattr(user_info, "user_id", "") or ""),
504505
"matched_groups": matched_groups if isinstance(matched_groups, dict) else {},
505506
}
507+
if message is not None:
508+
invoke_args["message"] = dict(
509+
PluginMessageUtils._session_message_to_dict(message, include_binary_data=False)
510+
)
506511
if isinstance(plugin_config, dict):
507512
invoke_args["plugin_config"] = plugin_config
508513

src/webui/routers/plugin/management.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
find_plugin_path_by_id,
1515
get_plugin_candidate_paths,
1616
get_plugin_config_path,
17+
is_plugin_install_residue,
1718
iter_plugin_directories,
1819
load_manifest_json,
1920
parse_repository_url,
@@ -99,6 +100,36 @@ def _get_runtime_plugin_load_statuses() -> Dict[str, str]:
99100
return {}
100101

101102

103+
def _write_plugin_disabled_for_uninstall(plugin_path: Path) -> None:
104+
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
105+
if config_path.exists():
106+
with open(config_path, "r", encoding="utf-8") as file_obj:
107+
config_doc = tomlkit.load(file_obj)
108+
else:
109+
config_doc = tomlkit.document()
110+
111+
plugin_section = config_doc.get("plugin")
112+
if not isinstance(plugin_section, dict):
113+
plugin_section = tomlkit.table()
114+
config_doc["plugin"] = plugin_section
115+
plugin_section["enabled"] = False
116+
117+
with open(config_path, "w", encoding="utf-8") as file_obj:
118+
file_obj.write(tomlkit.dumps(config_doc))
119+
120+
121+
async def _release_plugin_runtime_before_delete(plugin_id: str, plugin_path: Path) -> bool:
122+
try:
123+
_write_plugin_disabled_for_uninstall(plugin_path)
124+
125+
from src.plugin_runtime.integration import get_plugin_runtime_manager
126+
127+
return await get_plugin_runtime_manager().reload_plugins_globally([plugin_id], reason="uninstall")
128+
except Exception as exc:
129+
logger.warning(f"插件 {plugin_id} 删除前运行时卸载失败,将继续尝试删除文件: {exc}")
130+
return False
131+
132+
102133
@router.post("/install")
103134
async def install_plugin(request: InstallPluginRequest, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
104135
require_plugin_token(maibot_session)
@@ -121,6 +152,10 @@ async def install_plugin(request: InstallPluginRequest, maibot_session: Optional
121152
)
122153

123154
target_path, old_format_path = get_plugin_candidate_paths(plugin_id)
155+
for candidate_path in (target_path, old_format_path):
156+
if is_plugin_install_residue(candidate_path):
157+
logger.warning(f"检测到插件安装残留目录,安装前自动清理: {candidate_path}")
158+
remove_tree(candidate_path)
124159
if target_path.exists() or old_format_path.exists():
125160
await update_progress(
126161
stage="error",
@@ -257,15 +292,24 @@ async def uninstall_plugin(
257292
)
258293
raise HTTPException(status_code=404, detail="插件未安装")
259294

295+
manifest = load_manifest_json(resolve_plugin_file_path(plugin_path, "_manifest.json"))
296+
plugin_name = str(manifest.get("name", plugin_id)) if manifest is not None else plugin_id
297+
runtime_plugin_id = str(manifest.get("id", plugin_id)) if manifest is not None else plugin_id
260298
await update_progress(
261299
stage="loading",
262300
progress=30,
301+
message=f"正在卸载运行中的插件: {plugin_name}",
302+
operation="uninstall",
303+
plugin_id=plugin_id,
304+
)
305+
await _release_plugin_runtime_before_delete(runtime_plugin_id, plugin_path)
306+
await update_progress(
307+
stage="loading",
308+
progress=45,
263309
message=f"正在删除插件文件: {plugin_path}",
264310
operation="uninstall",
265311
plugin_id=plugin_id,
266312
)
267-
manifest = load_manifest_json(resolve_plugin_file_path(plugin_path, "_manifest.json"))
268-
plugin_name = str(manifest.get("name", plugin_id)) if manifest is not None else plugin_id
269313
await update_progress(
270314
stage="loading",
271315
progress=50,

src/webui/routers/plugin/support.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,22 @@ def get_plugin_candidate_paths(plugin_id: str) -> Tuple[Path, Path]:
205205
return validate_safe_path(folder_name, plugins_dir), validate_safe_path(plugin_id, plugins_dir)
206206

207207

208+
def is_plugin_install_residue(plugin_path: Path) -> bool:
209+
"""判断目录是否只是卸载失败留下的插件安装残留。"""
210+
if not plugin_path.exists() or not plugin_path.is_dir() or plugin_path.is_symlink():
211+
return False
212+
213+
manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json")
214+
if manifest_path.exists():
215+
return False
216+
217+
allowed_residue_files = {"config.toml"}
218+
try:
219+
return all(entry.is_file() and entry.name in allowed_residue_files for entry in plugin_path.iterdir())
220+
except OSError:
221+
return False
222+
223+
208224
def resolve_installed_plugin_path(plugin_id: str) -> Optional[Path]:
209225
new_format_path, old_format_path = get_plugin_candidate_paths(plugin_id)
210226
plugins_dir = get_plugins_dir()

0 commit comments

Comments
 (0)