AstrBot 4.12.3__py3-none-any.whl → 4.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
  2. astrbot/builtin_stars/builtin_commands/commands/__init__.py +0 -2
  3. astrbot/builtin_stars/builtin_commands/commands/persona.py +68 -6
  4. astrbot/builtin_stars/builtin_commands/main.py +0 -26
  5. astrbot/cli/__init__.py +1 -1
  6. astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
  7. astrbot/core/agent/tool.py +61 -20
  8. astrbot/core/astr_agent_hooks.py +3 -1
  9. astrbot/core/astr_agent_run_util.py +243 -1
  10. astrbot/core/astr_agent_tool_exec.py +2 -2
  11. astrbot/core/{sandbox → computer}/booters/base.py +4 -4
  12. astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
  13. astrbot/core/computer/booters/local.py +234 -0
  14. astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
  15. astrbot/core/computer/computer_client.py +102 -0
  16. astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
  17. astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
  18. astrbot/core/computer/tools/python.py +94 -0
  19. astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
  20. astrbot/core/config/default.py +90 -9
  21. astrbot/core/db/__init__.py +94 -1
  22. astrbot/core/db/po.py +46 -0
  23. astrbot/core/db/sqlite.py +248 -0
  24. astrbot/core/message/components.py +2 -2
  25. astrbot/core/persona_mgr.py +162 -2
  26. astrbot/core/pipeline/context_utils.py +2 -2
  27. astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
  28. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +73 -6
  29. astrbot/core/pipeline/process_stage/utils.py +31 -4
  30. astrbot/core/pipeline/scheduler.py +1 -1
  31. astrbot/core/pipeline/waking_check/stage.py +0 -1
  32. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
  33. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +32 -14
  34. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +61 -2
  35. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +57 -11
  36. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
  37. astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
  38. astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
  39. astrbot/core/provider/manager.py +38 -0
  40. astrbot/core/provider/provider.py +54 -0
  41. astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
  42. astrbot/core/provider/sources/gemini_source.py +12 -9
  43. astrbot/core/provider/sources/genie_tts.py +128 -0
  44. astrbot/core/provider/sources/openai_embedding_source.py +1 -1
  45. astrbot/core/skills/__init__.py +3 -0
  46. astrbot/core/skills/skill_manager.py +237 -0
  47. astrbot/core/star/command_management.py +1 -1
  48. astrbot/core/star/config.py +1 -1
  49. astrbot/core/star/context.py +9 -8
  50. astrbot/core/star/filter/command.py +1 -1
  51. astrbot/core/star/filter/custom_filter.py +2 -2
  52. astrbot/core/star/register/star_handler.py +2 -4
  53. astrbot/core/utils/astrbot_path.py +6 -0
  54. astrbot/dashboard/routes/__init__.py +2 -0
  55. astrbot/dashboard/routes/config.py +236 -2
  56. astrbot/dashboard/routes/live_chat.py +423 -0
  57. astrbot/dashboard/routes/persona.py +265 -1
  58. astrbot/dashboard/routes/skills.py +148 -0
  59. astrbot/dashboard/routes/util.py +102 -0
  60. astrbot/dashboard/server.py +21 -5
  61. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
  62. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/RECORD +69 -63
  63. astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
  64. astrbot/core/sandbox/sandbox_client.py +0 -52
  65. astrbot/core/sandbox/tools/python.py +0 -74
  66. /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
  67. /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
  68. /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
  69. /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
  70. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
  71. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
  72. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -221,11 +221,65 @@ class TTSProvider(AbstractProvider):
221
221
  self.provider_config = provider_config
222
222
  self.provider_settings = provider_settings
223
223
 
224
+ def support_stream(self) -> bool:
225
+ """是否支持流式 TTS
226
+
227
+ Returns:
228
+ bool: True 表示支持流式处理,False 表示不支持(默认)
229
+
230
+ Notes:
231
+ 子类可以重写此方法返回 True 来启用流式 TTS 支持
232
+ """
233
+ return False
234
+
224
235
  @abc.abstractmethod
225
236
  async def get_audio(self, text: str) -> str:
226
237
  """获取文本的音频,返回音频文件路径"""
227
238
  raise NotImplementedError
228
239
 
240
+ async def get_audio_stream(
241
+ self,
242
+ text_queue: asyncio.Queue[str | None],
243
+ audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
244
+ ) -> None:
245
+ """流式 TTS 处理方法。
246
+
247
+ 从 text_queue 中读取文本片段,将生成的音频数据(WAV 格式的 in-memory bytes)放入 audio_queue。
248
+ 当 text_queue 收到 None 时,表示文本输入结束,此时应该处理完所有剩余文本并向 audio_queue 发送 None 表示结束。
249
+
250
+ Args:
251
+ text_queue: 输入文本队列,None 表示输入结束
252
+ audio_queue: 输出音频队列(bytes 或 (text, bytes)),None 表示输出结束
253
+
254
+ Notes:
255
+ - 默认实现会将文本累积后一次性调用 get_audio 生成完整音频
256
+ - 子类可以重写此方法实现真正的流式 TTS
257
+ - 音频数据应该是 WAV 格式的 bytes
258
+ """
259
+ accumulated_text = ""
260
+
261
+ while True:
262
+ text_part = await text_queue.get()
263
+
264
+ if text_part is None:
265
+ # 输入结束,处理累积的文本
266
+ if accumulated_text:
267
+ try:
268
+ # 调用原有的 get_audio 方法获取音频文件路径
269
+ audio_path = await self.get_audio(accumulated_text)
270
+ # 读取音频文件内容
271
+ with open(audio_path, "rb") as f:
272
+ audio_data = f.read()
273
+ await audio_queue.put((accumulated_text, audio_data))
274
+ except Exception:
275
+ # 出错时也要发送 None 结束标记
276
+ pass
277
+ # 发送结束标记
278
+ await audio_queue.put(None)
279
+ break
280
+
281
+ accumulated_text += text_part
282
+
229
283
  async def test(self):
230
284
  await self.get_audio("hi")
231
285
 
@@ -68,4 +68,4 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
68
68
 
69
69
  def get_dim(self) -> int:
70
70
  """获取向量的维度"""
71
- return self.provider_config.get("embedding_dimensions", 768)
71
+ return int(self.provider_config.get("embedding_dimensions", 768))
@@ -382,15 +382,18 @@ class ProviderGoogleGenAI(Provider):
382
382
  append_or_extend(gemini_contents, parts, types.ModelContent)
383
383
 
384
384
  elif role == "tool" and not native_tool_enabled:
385
- parts = [
386
- types.Part.from_function_response(
387
- name=message["tool_call_id"],
388
- response={
389
- "name": message["tool_call_id"],
390
- "content": message["content"],
391
- },
392
- ),
393
- ]
385
+ func_name = message.get("name", message["tool_call_id"])
386
+ part = types.Part.from_function_response(
387
+ name=func_name,
388
+ response={
389
+ "name": func_name,
390
+ "content": message["content"],
391
+ },
392
+ )
393
+ if part.function_response:
394
+ part.function_response.id = message["tool_call_id"]
395
+
396
+ parts = [part]
394
397
  append_or_extend(gemini_contents, parts, types.UserContent)
395
398
 
396
399
  if gemini_contents and isinstance(gemini_contents[0], types.ModelContent):
@@ -0,0 +1,128 @@
1
+ import asyncio
2
+ import os
3
+ import uuid
4
+
5
+ from astrbot.core import logger
6
+ from astrbot.core.provider.entities import ProviderType
7
+ from astrbot.core.provider.provider import TTSProvider
8
+ from astrbot.core.provider.register import register_provider_adapter
9
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
10
+
11
+ try:
12
+ import genie_tts as genie # type: ignore
13
+ except ImportError:
14
+ genie = None
15
+
16
+
17
+ @register_provider_adapter(
18
+ "genie_tts",
19
+ "Genie TTS",
20
+ provider_type=ProviderType.TEXT_TO_SPEECH,
21
+ )
22
+ class GenieTTSProvider(TTSProvider):
23
+ def __init__(
24
+ self,
25
+ provider_config: dict,
26
+ provider_settings: dict,
27
+ ) -> None:
28
+ super().__init__(provider_config, provider_settings)
29
+ if not genie:
30
+ raise ImportError("Please install genie_tts first.")
31
+
32
+ self.character_name = provider_config.get("genie_character_name", "mika")
33
+ language = provider_config.get("genie_language", "Japanese")
34
+ model_dir = provider_config.get("genie_onnx_model_dir", "")
35
+ refer_audio_path = provider_config.get("genie_refer_audio_path", "")
36
+ refer_text = provider_config.get("genie_refer_text", "")
37
+
38
+ try:
39
+ genie.load_character(
40
+ character_name=self.character_name,
41
+ language=language,
42
+ onnx_model_dir=model_dir,
43
+ )
44
+ genie.set_reference_audio(
45
+ character_name=self.character_name,
46
+ audio_path=refer_audio_path,
47
+ audio_text=refer_text,
48
+ language=language,
49
+ )
50
+ except Exception as e:
51
+ raise RuntimeError(f"Failed to load character {self.character_name}: {e}")
52
+
53
+ def support_stream(self) -> bool:
54
+ return True
55
+
56
+ async def get_audio(self, text: str) -> str:
57
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
58
+ os.makedirs(temp_dir, exist_ok=True)
59
+ filename = f"genie_tts_{uuid.uuid4()}.wav"
60
+ path = os.path.join(temp_dir, filename)
61
+
62
+ loop = asyncio.get_event_loop()
63
+
64
+ def _generate(save_path: str):
65
+ assert genie is not None
66
+ genie.tts(
67
+ character_name=self.character_name,
68
+ text=text,
69
+ save_path=save_path,
70
+ )
71
+
72
+ try:
73
+ await loop.run_in_executor(None, _generate, path)
74
+
75
+ if os.path.exists(path):
76
+ return path
77
+
78
+ raise RuntimeError("Genie TTS did not save to file.")
79
+
80
+ except Exception as e:
81
+ raise RuntimeError(f"Genie TTS generation failed: {e}")
82
+
83
+ async def get_audio_stream(
84
+ self,
85
+ text_queue: asyncio.Queue[str | None],
86
+ audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
87
+ ) -> None:
88
+ loop = asyncio.get_event_loop()
89
+
90
+ while True:
91
+ text = await text_queue.get()
92
+ if text is None:
93
+ await audio_queue.put(None)
94
+ break
95
+
96
+ try:
97
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
98
+ os.makedirs(temp_dir, exist_ok=True)
99
+ filename = f"genie_tts_{uuid.uuid4()}.wav"
100
+ path = os.path.join(temp_dir, filename)
101
+
102
+ def _generate(save_path: str, t: str):
103
+ assert genie is not None
104
+ genie.tts(
105
+ character_name=self.character_name,
106
+ text=t,
107
+ save_path=save_path,
108
+ )
109
+
110
+ await loop.run_in_executor(None, _generate, path, text)
111
+
112
+ if os.path.exists(path):
113
+ with open(path, "rb") as f:
114
+ audio_data = f.read()
115
+
116
+ # Put (text, bytes) into queue so frontend can display text
117
+ await audio_queue.put((text, audio_data))
118
+
119
+ # Clean up
120
+ try:
121
+ os.remove(path)
122
+ except OSError:
123
+ pass
124
+ else:
125
+ logger.error(f"Genie TTS failed to generate audio for: {text}")
126
+
127
+ except Exception as e:
128
+ logger.error(f"Genie TTS stream error: {e}")
@@ -37,4 +37,4 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
37
37
 
38
38
  def get_dim(self) -> int:
39
39
  """获取向量的维度"""
40
- return self.provider_config.get("embedding_dimensions", 1024)
40
+ return int(self.provider_config.get("embedding_dimensions", 1024))
@@ -0,0 +1,3 @@
1
+ from .skill_manager import SkillInfo, SkillManager, build_skills_prompt
2
+
3
+ __all__ = ["SkillInfo", "SkillManager", "build_skills_prompt"]
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import tempfile
8
+ import zipfile
9
+ from dataclasses import dataclass
10
+ from pathlib import Path, PurePosixPath
11
+
12
+ from astrbot.core.utils.astrbot_path import (
13
+ get_astrbot_data_path,
14
+ get_astrbot_skills_path,
15
+ get_astrbot_temp_path,
16
+ )
17
+
18
+ SKILLS_CONFIG_FILENAME = "skills.json"
19
+ DEFAULT_SKILLS_CONFIG: dict[str, dict] = {"skills": {}}
20
+ SANDBOX_SKILLS_ROOT = "/home/shared/skills"
21
+
22
+ _SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
23
+
24
+
25
+ @dataclass
26
+ class SkillInfo:
27
+ name: str
28
+ description: str
29
+ path: str
30
+ active: bool
31
+
32
+
33
+ def _parse_frontmatter_description(text: str) -> str:
34
+ if not text.startswith("---"):
35
+ return ""
36
+ lines = text.splitlines()
37
+ if not lines or lines[0].strip() != "---":
38
+ return ""
39
+ end_idx = None
40
+ for i in range(1, len(lines)):
41
+ if lines[i].strip() == "---":
42
+ end_idx = i
43
+ break
44
+ if end_idx is None:
45
+ return ""
46
+ for line in lines[1:end_idx]:
47
+ if ":" not in line:
48
+ continue
49
+ key, value = line.split(":", 1)
50
+ if key.strip().lower() == "description":
51
+ return value.strip().strip('"').strip("'")
52
+ return ""
53
+
54
+
55
+ def build_skills_prompt(skills: list[SkillInfo]) -> str:
56
+ skills_lines = []
57
+ for skill in skills:
58
+ description = skill.description or "No description"
59
+ skills_lines.append(f"- {skill.name}: {description} (file: {skill.path})")
60
+ skills_block = "\n".join(skills_lines)
61
+ # Based on openai/codex
62
+ return (
63
+ "## Skills\n"
64
+ "A skill is a set of local instructions stored in a `SKILL.md` file.\n"
65
+ "### Available skills\n"
66
+ f"{skills_block}\n"
67
+ "### Skill Rules\n"
68
+ "\n"
69
+ "- Discovery: The list above shows all skills available in this session. Full instructions live in the referenced `SKILL.md`.\n"
70
+ "- Trigger rules: Use a skill if the user names it or the task matches its description. Do not carry skills across turns unless re-mentioned\n"
71
+ "- Unavailable: If a skill is missing or unreadable, say so and fallback.\n"
72
+ "### How to use a skill (progressive disclosure):\n"
73
+ " 1) After deciding to use a skill, open its `SKILL.md` and read only what is necessary to follow the workflow.\n"
74
+ " 2) Load only directly referenced files, DO NOT bulk-load everything.\n"
75
+ " 3) If `scripts/` exist, prefer running or patching them instead of retyping large blocks of code.\n"
76
+ " 4) If `assets/` or templates exist, reuse them rather than recreating everything from scratch.\n"
77
+ "- Coordination:\n"
78
+ " - If multiple skills apply, choose the minimal set that covers the request and state the order in which you will use them.\n"
79
+ " - Announce which skill(s) you are using and why (one short line). If you skip an obvious skill, explain why.\n"
80
+ " - Prefer to use `astrbot_*` tools to perform skills that need to run scripts.\n"
81
+ "- Context hygiene:\n"
82
+ " - Keep context small: summarize long sections instead of pasting them, and load extra files only when necessary.\n"
83
+ " - Avoid deep reference chasing: unless blocked, open only files that are directly linked from `SKILL.md`.\n"
84
+ " - When variants exist (frameworks, providers, domains), select only the relevant reference file(s) and note that choice.\n"
85
+ "- Failure handling: If a skill cannot be applied, state the issue and continue with the best alternative."
86
+ )
87
+
88
+
89
+ class SkillManager:
90
+ def __init__(self, skills_root: str | None = None) -> None:
91
+ self.skills_root = skills_root or get_astrbot_skills_path()
92
+ self.config_path = os.path.join(get_astrbot_data_path(), SKILLS_CONFIG_FILENAME)
93
+ os.makedirs(self.skills_root, exist_ok=True)
94
+ os.makedirs(get_astrbot_temp_path(), exist_ok=True)
95
+
96
+ def _load_config(self) -> dict:
97
+ if not os.path.exists(self.config_path):
98
+ self._save_config(DEFAULT_SKILLS_CONFIG.copy())
99
+ return DEFAULT_SKILLS_CONFIG.copy()
100
+ with open(self.config_path, encoding="utf-8") as f:
101
+ data = json.load(f)
102
+ if not isinstance(data, dict) or "skills" not in data:
103
+ return DEFAULT_SKILLS_CONFIG.copy()
104
+ return data
105
+
106
+ def _save_config(self, config: dict) -> None:
107
+ with open(self.config_path, "w", encoding="utf-8") as f:
108
+ json.dump(config, f, ensure_ascii=False, indent=4)
109
+
110
+ def list_skills(
111
+ self,
112
+ *,
113
+ active_only: bool = False,
114
+ runtime: str = "local",
115
+ show_sandbox_path: bool = True,
116
+ ) -> list[SkillInfo]:
117
+ """List all skills.
118
+
119
+ show_sandbox_path: If True and runtime is "sandbox",
120
+ return the path as it would appear in the sandbox environment,
121
+ otherwise return the local filesystem path.
122
+ """
123
+ config = self._load_config()
124
+ skill_configs = config.get("skills", {})
125
+ modified = False
126
+ skills: list[SkillInfo] = []
127
+
128
+ for entry in sorted(Path(self.skills_root).iterdir()):
129
+ if not entry.is_dir():
130
+ continue
131
+ skill_name = entry.name
132
+ skill_md = entry / "SKILL.md"
133
+ if not skill_md.exists():
134
+ continue
135
+ active = skill_configs.get(skill_name, {}).get("active", True)
136
+ if skill_name not in skill_configs:
137
+ skill_configs[skill_name] = {"active": active}
138
+ modified = True
139
+ if active_only and not active:
140
+ continue
141
+ description = ""
142
+ try:
143
+ content = skill_md.read_text(encoding="utf-8")
144
+ description = _parse_frontmatter_description(content)
145
+ except Exception:
146
+ description = ""
147
+ if runtime == "sandbox" and show_sandbox_path:
148
+ path_str = f"{SANDBOX_SKILLS_ROOT}/{skill_name}/SKILL.md"
149
+ else:
150
+ path_str = str(skill_md)
151
+ path_str = path_str.replace("\\", "/")
152
+ skills.append(
153
+ SkillInfo(
154
+ name=skill_name,
155
+ description=description,
156
+ path=path_str,
157
+ active=active,
158
+ )
159
+ )
160
+
161
+ if modified:
162
+ config["skills"] = skill_configs
163
+ self._save_config(config)
164
+
165
+ return skills
166
+
167
+ def set_skill_active(self, name: str, active: bool) -> None:
168
+ config = self._load_config()
169
+ config.setdefault("skills", {})
170
+ config["skills"][name] = {"active": bool(active)}
171
+ self._save_config(config)
172
+
173
+ def delete_skill(self, name: str) -> None:
174
+ skill_dir = Path(self.skills_root) / name
175
+ if skill_dir.exists():
176
+ shutil.rmtree(skill_dir)
177
+ config = self._load_config()
178
+ if name in config.get("skills", {}):
179
+ config["skills"].pop(name, None)
180
+ self._save_config(config)
181
+
182
+ def install_skill_from_zip(self, zip_path: str, *, overwrite: bool = True) -> str:
183
+ zip_path_obj = Path(zip_path)
184
+ if not zip_path_obj.exists():
185
+ raise FileNotFoundError(f"Zip file not found: {zip_path}")
186
+ if not zipfile.is_zipfile(zip_path):
187
+ raise ValueError("Uploaded file is not a valid zip archive.")
188
+
189
+ with zipfile.ZipFile(zip_path) as zf:
190
+ names = [name.replace("\\", "/") for name in zf.namelist()]
191
+ file_names = [name for name in names if name and not name.endswith("/")]
192
+ if not file_names:
193
+ raise ValueError("Zip archive is empty.")
194
+
195
+ top_dirs = {
196
+ PurePosixPath(name).parts[0] for name in file_names if name.strip()
197
+ }
198
+ print(top_dirs)
199
+ if len(top_dirs) != 1:
200
+ raise ValueError("Zip archive must contain a single top-level folder.")
201
+ skill_name = next(iter(top_dirs))
202
+ if skill_name in {".", "..", ""} or not _SKILL_NAME_RE.match(skill_name):
203
+ raise ValueError("Invalid skill folder name.")
204
+
205
+ for name in names:
206
+ if not name:
207
+ continue
208
+ if name.startswith("/") or re.match(r"^[A-Za-z]:", name):
209
+ raise ValueError("Zip archive contains absolute paths.")
210
+ parts = PurePosixPath(name).parts
211
+ if ".." in parts:
212
+ raise ValueError("Zip archive contains invalid relative paths.")
213
+ if parts and parts[0] != skill_name:
214
+ raise ValueError(
215
+ "Zip archive contains unexpected top-level entries."
216
+ )
217
+
218
+ if (
219
+ f"{skill_name}/SKILL.md" not in file_names
220
+ and f"{skill_name}/skill.md" not in file_names
221
+ ):
222
+ raise ValueError("SKILL.md not found in the skill folder.")
223
+
224
+ with tempfile.TemporaryDirectory(dir=get_astrbot_temp_path()) as tmp_dir:
225
+ zf.extractall(tmp_dir)
226
+ src_dir = Path(tmp_dir) / skill_name
227
+ if not src_dir.exists():
228
+ raise ValueError("Skill folder not found after extraction.")
229
+ dest_dir = Path(self.skills_root) / skill_name
230
+ if dest_dir.exists():
231
+ if not overwrite:
232
+ raise FileExistsError("Skill already exists.")
233
+ shutil.rmtree(dest_dir)
234
+ shutil.move(str(src_dir), str(dest_dir))
235
+
236
+ self.set_skill_active(skill_name, True)
237
+ return skill_name
@@ -303,7 +303,7 @@ def _locate_primary_filter(
303
303
  handler: StarHandlerMetadata,
304
304
  ) -> CommandFilter | CommandGroupFilter | None:
305
305
  for filter_ref in handler.event_filters:
306
- if isinstance(filter_ref, (CommandFilter, CommandGroupFilter)):
306
+ if isinstance(filter_ref, CommandFilter | CommandGroupFilter):
307
307
  return filter_ref
308
308
  return None
309
309
 
@@ -38,7 +38,7 @@ def put_config(namespace: str, name: str, key: str, value, description: str):
38
38
  raise ValueError("namespace 不能以 internal_ 开头。")
39
39
  if not isinstance(key, str):
40
40
  raise ValueError("key 只支持 str 类型。")
41
- if not isinstance(value, (str, int, float, bool, list)):
41
+ if not isinstance(value, str | int | float | bool | list):
42
42
  raise ValueError("value 只支持 str, int, float, bool, list 类型。")
43
43
 
44
44
  config_dir = os.path.join(get_astrbot_data_path(), "config")
@@ -328,28 +328,29 @@ class Context:
328
328
  """获取所有用于 Embedding 任务的 Provider。"""
329
329
  return self.provider_manager.embedding_provider_insts
330
330
 
331
- def get_using_provider(self, umo: str | None = None) -> Provider:
331
+ def get_using_provider(self, umo: str | None = None) -> Provider | None:
332
332
  """获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。
333
333
 
334
334
  Args:
335
335
  umo: unified_message_origin 值,如果传入并且用户启用了提供商会话隔离,
336
- 则使用该会话偏好的提供商。
336
+ 则使用该会话偏好的对话模型(提供商)。
337
337
 
338
338
  Returns:
339
- 当前使用的文本生成提供者。
339
+ 当前使用的对话模型(提供商),如果未设置则返回 None。
340
340
 
341
341
  Raises:
342
- ValueError: 返回的提供者不是 Provider 类型。
343
-
344
- Note:
345
- 通过 /provider 指令可以切换提供者。
342
+ ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。
346
343
  """
347
344
  prov = self.provider_manager.get_using_provider(
348
345
  provider_type=ProviderType.CHAT_COMPLETION,
349
346
  umo=umo,
350
347
  )
348
+ if prov is None:
349
+ return None
351
350
  if not isinstance(prov, Provider):
352
- raise ValueError("返回的 Provider 不是 Provider 类型")
351
+ raise ValueError(
352
+ f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}"
353
+ )
353
354
  return prov
354
355
 
355
356
  def get_using_tts_provider(self, umo: str | None = None) -> TTSProvider | None:
@@ -115,7 +115,7 @@ class CommandFilter(HandlerFilter):
115
115
  # 没有 GreedyStr 的情况
116
116
  if i >= len(params):
117
117
  if (
118
- isinstance(param_type_or_default_val, (type, types.UnionType))
118
+ isinstance(param_type_or_default_val, type | types.UnionType)
119
119
  or typing.get_origin(param_type_or_default_val) is typing.Union
120
120
  or param_type_or_default_val is inspect.Parameter.empty
121
121
  ):
@@ -37,7 +37,7 @@ class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
37
37
  class CustomFilterOr(CustomFilter):
38
38
  def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
39
39
  super().__init__()
40
- if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
40
+ if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
41
41
  raise ValueError(
42
42
  "CustomFilter lass can only operate with other CustomFilter.",
43
43
  )
@@ -51,7 +51,7 @@ class CustomFilterOr(CustomFilter):
51
51
  class CustomFilterAnd(CustomFilter):
52
52
  def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
53
53
  super().__init__()
54
- if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
54
+ if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
55
55
  raise ValueError(
56
56
  "CustomFilter lass can only operate with other CustomFilter.",
57
57
  )
@@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs):
150
150
  if args:
151
151
  raise_error = args[0]
152
152
 
153
- if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
153
+ if not isinstance(custom_filter, CustomFilterAnd | CustomFilterOr):
154
154
  custom_filter = custom_filter(raise_error)
155
155
 
156
156
  def decorator(awaitable):
@@ -452,9 +452,7 @@ def register_on_llm_tool_respond(**kwargs):
452
452
  """
453
453
 
454
454
  def decorator(awaitable):
455
- _ = get_handler_or_create(
456
- awaitable, EventType.OnLLMToolRespondEvent, **kwargs
457
- )
455
+ _ = get_handler_or_create(awaitable, EventType.OnLLMToolRespondEvent, **kwargs)
458
456
  return awaitable
459
457
 
460
458
  return decorator
@@ -9,6 +9,7 @@
9
9
  T2I 模板目录路径:固定为数据目录下的 t2i_templates 目录
10
10
  WebChat 数据目录路径:固定为数据目录下的 webchat 目录
11
11
  临时文件目录路径:固定为数据目录下的 temp 目录
12
+ Skills 目录路径:固定为数据目录下的 skills 目录
12
13
  """
13
14
 
14
15
  import os
@@ -63,6 +64,11 @@ def get_astrbot_temp_path() -> str:
63
64
  return os.path.realpath(os.path.join(get_astrbot_data_path(), "temp"))
64
65
 
65
66
 
67
+ def get_astrbot_skills_path() -> str:
68
+ """获取Astrbot Skills 目录路径"""
69
+ return os.path.realpath(os.path.join(get_astrbot_data_path(), "skills"))
70
+
71
+
66
72
  def get_astrbot_knowledge_base_path() -> str:
67
73
  """获取Astrbot知识库根目录路径"""
68
74
  return os.path.realpath(os.path.join(get_astrbot_data_path(), "knowledge_base"))
@@ -12,6 +12,7 @@ from .persona import PersonaRoute
12
12
  from .platform import PlatformRoute
13
13
  from .plugin import PluginRoute
14
14
  from .session_management import SessionManagementRoute
15
+ from .skills import SkillsRoute
15
16
  from .stat import StatRoute
16
17
  from .static_file import StaticFileRoute
17
18
  from .tools import ToolsRoute
@@ -35,5 +36,6 @@ __all__ = [
35
36
  "StatRoute",
36
37
  "StaticFileRoute",
37
38
  "ToolsRoute",
39
+ "SkillsRoute",
38
40
  "UpdateRoute",
39
41
  ]