Skip to content

Commit d9b76b4

Browse files
committed
Merge remote-tracking branch 'upstream/master' into feat/totp
2 parents b7af106 + 094c2de commit d9b76b4

43 files changed

Lines changed: 2009 additions & 345 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

astrbot/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.24.3"
1+
__version__ = "4.24.5"

astrbot/cli/commands/cmd_init.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import asyncio
2+
import os
23
from pathlib import Path
34

45
import click
56
from filelock import FileLock, Timeout
67

78
from ..utils import check_dashboard, get_astrbot_root
89

10+
DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD"
11+
12+
13+
def _initialize_config_from_env(astrbot_root: Path) -> None:
14+
if DASHBOARD_INITIAL_PASSWORD_ENV not in os.environ:
15+
return
16+
17+
from astrbot.core.config.astrbot_config import AstrBotConfig
18+
19+
AstrBotConfig(config_path=str(astrbot_root / "data" / "cmd_config.json"))
20+
click.echo("Initialized data/cmd_config.json with dashboard initial password.")
21+
922

1023
async def initialize_astrbot(astrbot_root: Path) -> None:
1124
"""Execute AstrBot initialization logic"""
@@ -31,6 +44,8 @@ async def initialize_astrbot(astrbot_root: Path) -> None:
3144
path.mkdir(parents=True, exist_ok=True)
3245
click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}")
3346

47+
_initialize_config_from_env(astrbot_root)
48+
3449
await check_dashboard(astrbot_root / "data")
3550

3651

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,9 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
10221022
func_tool = req.func_tool.get_tool(func_tool_name)
10231023
available_tools = req.func_tool.names()
10241024

1025+
# Some API may return None for tools with no parameters
1026+
if func_tool_args is None:
1027+
func_tool_args = {}
10251028
logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
10261029

10271030
if not func_tool:

astrbot/core/computer/booters/shipyard_neo.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,12 @@ def __init__(
353353
self,
354354
endpoint_url: str,
355355
access_token: str,
356-
profile: str = DEFAULT_PROFILE,
356+
profile: str = "",
357357
ttl: int = 3600,
358358
) -> None:
359359
self._endpoint_url = endpoint_url
360360
self._access_token = access_token
361-
self._profile = profile
361+
self._profile = profile.strip() if profile else ""
362362
self._ttl = ttl
363363
self._client: BayClient | None = None
364364
self._sandbox: Sandbox | None = None
@@ -431,7 +431,9 @@ async def boot(self, session_id: str) -> None:
431431
)
432432
await self._client.__aenter__()
433433

434-
# Resolve profile: user-specified > smart selection > default
434+
# Resolve profile: user-specified > smart selection > default.
435+
# An empty profile means auto-select; any non-empty profile must be
436+
# honoured as an explicit choice, including "python-default".
435437
resolved_profile = await self._resolve_profile(self._client)
436438

437439
self._sandbox = await self._client.create_sandbox(
@@ -535,7 +537,7 @@ async def _resolve_profile(self, client: Any) -> str:
535537
"""Pick the best profile for this session.
536538
537539
Resolution order:
538-
1. User-specified profile (non-empty, non-default) → use as-is.
540+
1. User-specified profile (non-empty) → use as-is.
539541
2. Query ``GET /v1/profiles`` and pick the profile with the most
540542
capabilities, preferring profiles that include ``"browser"``.
541543
3. Fall back to :attr:`DEFAULT_PROFILE`.
@@ -544,8 +546,8 @@ async def _resolve_profile(self, client: Any) -> str:
544546
misconfigured token, and silently falling back would just delay the
545547
real failure to ``create_sandbox``.
546548
"""
547-
# User explicitly set a profile → honour it
548-
if self._profile and self._profile != self.DEFAULT_PROFILE:
549+
# User explicitly set a profile → honour it.
550+
if self._profile:
549551
logger.info("[Computer] Using user-specified profile: %s", self._profile)
550552
return self._profile
551553

astrbot/core/config/astrbot_config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
generate_dashboard_password,
99
hash_dashboard_password,
1010
hash_legacy_dashboard_password,
11+
validate_dashboard_password,
1112
)
1213

1314
from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
1415

1516
ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json")
17+
DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD"
1618
logger = logging.getLogger("astrbot")
1719

1820

@@ -97,7 +99,7 @@ def __init__(
9799
self.update(conf)
98100

99101
def _reset_generated_dashboard_password(self, conf: dict) -> None:
100-
generated_password = generate_dashboard_password()
102+
generated_password = self._resolve_initial_dashboard_password()
101103
conf["dashboard"]["pbkdf2_password"] = hash_dashboard_password(
102104
generated_password
103105
)
@@ -117,6 +119,14 @@ def _reset_generated_dashboard_password(self, conf: dict) -> None:
117119
True,
118120
)
119121

122+
@staticmethod
123+
def _resolve_initial_dashboard_password() -> str:
124+
env_password = os.environ.get(DASHBOARD_INITIAL_PASSWORD_ENV)
125+
if env_password is None:
126+
return generate_dashboard_password()
127+
validate_dashboard_password(env_password)
128+
return env_password
129+
120130
def _config_schema_to_default_config(self, schema: dict) -> dict:
121131
"""将 Schema 转换成 Config"""
122132
conf = {}

astrbot/core/config/default.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
66
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
77

8-
VERSION = "4.24.3"
8+
VERSION = "4.24.5"
99
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
1010
PERSONAL_WECHAT_CONFIG_METADATA = {
1111
"weixin_oc_base_url": {
@@ -1808,6 +1808,34 @@
18081808
"timeout": 20,
18091809
"proxy": "",
18101810
},
1811+
"NVIDIA Embedding": {
1812+
"id": "nvidia_embedding",
1813+
"type": "nvidia_embedding",
1814+
"provider": "nvidia",
1815+
"provider_type": "embedding",
1816+
"hint": "provider_group.provider.nvidia_embedding.hint",
1817+
"enable": True,
1818+
"embedding_api_key": "",
1819+
"embedding_api_base": "https://integrate.api.nvidia.com/v1",
1820+
"embedding_model": "nvidia/llama-nemotron-embed-1b-v2",
1821+
"input_type": "passage",
1822+
"embedding_dimensions": 1024,
1823+
"timeout": 20,
1824+
"proxy": "",
1825+
},
1826+
"Ollama Embedding": {
1827+
"id": "ollama_embedding",
1828+
"type": "ollama_embedding",
1829+
"provider": "ollama",
1830+
"provider_type": "embedding",
1831+
"hint": "provider_group.provider.ollama_embedding.hint",
1832+
"enable": True,
1833+
"embedding_api_base": "http://localhost:11434",
1834+
"embedding_model": "nomic-embed-text",
1835+
"embedding_dimensions": 768,
1836+
"timeout": 60,
1837+
"proxy": "",
1838+
},
18111839
"vLLM Rerank": {
18121840
"id": "vllm_rerank",
18131841
"type": "vllm_rerank",
@@ -3320,7 +3348,7 @@
33203348
"provider_settings.sandbox.shipyard_neo_profile": {
33213349
"description": "Shipyard Neo Profile",
33223350
"type": "string",
3323-
"hint": "Shipyard Neo 沙箱 profile,如 python-default。",
3351+
"hint": "Shipyard Neo 沙箱 profile,如 python-default。留空时自动选择能力更完整的 profile。",
33243352
"condition": {
33253353
"provider_settings.computer_use_runtime": "sandbox",
33263354
"provider_settings.sandbox.booter": "shipyard_neo",

astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,12 @@ async def _send_media_segment(
993993
file_name,
994994
)
995995
except Exception as e:
996-
logger.error("weixin_oc(%s): prepare media failed: %s", self.meta().id, e)
996+
logger.error(
997+
"weixin_oc(%s): prepare media failed: %s",
998+
self.meta().id,
999+
e,
1000+
exc_info=True,
1001+
)
9971002
return False
9981003

9991004
if text:
@@ -1608,18 +1613,21 @@ async def send_by_session(
16081613
target_user = session.session_id
16091614
pending_text = ""
16101615
has_supported_segment = False
1616+
failed_segments = 0
16111617
for segment in message_chain.chain:
16121618
if isinstance(segment, Plain):
16131619
pending_text += segment.text
16141620
continue
16151621

16161622
if isinstance(segment, (Image, Video, File)):
16171623
has_supported_segment = True
1618-
await self._send_media_segment(
1624+
sent = await self._send_media_segment(
16191625
target_user,
16201626
segment,
16211627
text=pending_text.strip() or None,
16221628
)
1629+
if not sent:
1630+
failed_segments += 1
16231631
pending_text = ""
16241632
continue
16251633

@@ -1631,13 +1639,20 @@ async def send_by_session(
16311639

16321640
if pending_text:
16331641
has_supported_segment = True
1634-
await self._send_to_session(target_user, pending_text.strip())
1642+
sent = await self._send_to_session(target_user, pending_text.strip())
1643+
if not sent:
1644+
failed_segments += 1
16351645

16361646
if not has_supported_segment:
16371647
logger.warning(
16381648
"weixin_oc(%s): outbound message ignored, no supported segments",
16391649
self.meta().id,
16401650
)
1651+
if failed_segments:
1652+
raise RuntimeError(
1653+
f"weixin_oc({self.meta().id}, target_user={target_user}) "
1654+
f"failed to send {failed_segments} message segment(s)"
1655+
)
16411656
await super().send_by_session(session, message_chain)
16421657

16431658
def meta(self) -> PlatformMetadata:

astrbot/core/provider/manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,14 @@ def dynamic_import_provider(self, type: str) -> None:
469469
from .sources.gemini_embedding_source import (
470470
GeminiEmbeddingProvider as GeminiEmbeddingProvider,
471471
)
472+
case "nvidia_embedding":
473+
from .sources.nvidia_embedding_source import (
474+
NvidiaEmbeddingProvider as NvidiaEmbeddingProvider,
475+
)
476+
case "ollama_embedding":
477+
from .sources.ollama_embedding_source import (
478+
OllamaEmbeddingProvider as OllamaEmbeddingProvider,
479+
)
472480
case "vllm_rerank":
473481
from .sources.vllm_rerank_source import (
474482
VLLMRerankProvider as VLLMRerankProvider,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import aiohttp
2+
3+
from astrbot import logger
4+
5+
from ..entities import ProviderType
6+
from ..provider import EmbeddingProvider
7+
from ..register import register_provider_adapter
8+
9+
10+
@register_provider_adapter(
11+
"nvidia_embedding",
12+
"NVIDIA NIM Embedding 提供商适配器",
13+
provider_type=ProviderType.EMBEDDING,
14+
)
15+
class NvidiaEmbeddingProvider(EmbeddingProvider):
16+
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
17+
super().__init__(provider_config, provider_settings)
18+
self.provider_config = provider_config
19+
self.provider_settings = provider_settings
20+
21+
self.api_key = provider_config.get("embedding_api_key", "")
22+
self.base_url = (
23+
provider_config.get(
24+
"embedding_api_base", "https://integrate.api.nvidia.com/v1"
25+
)
26+
.rstrip("/")
27+
.removesuffix("/embeddings")
28+
)
29+
self.timeout = int(provider_config.get("timeout", 20))
30+
self.model = provider_config.get(
31+
"embedding_model", "nvidia/llama-nemotron-embed-1b-v2"
32+
)
33+
self.input_type = provider_config.get("input_type", "passage")
34+
35+
proxy = provider_config.get("proxy", "")
36+
self.proxy = proxy
37+
if proxy:
38+
logger.info(f"[NVIDIA Embedding] Using proxy: {proxy}")
39+
40+
self.client = None
41+
self.set_model(self.model)
42+
43+
async def _get_client(self):
44+
if self.client is None or self.client.closed:
45+
headers = {
46+
"Authorization": f"Bearer {self.api_key}",
47+
"Content-Type": "application/json",
48+
"Accept": "application/json",
49+
}
50+
timeout = aiohttp.ClientTimeout(total=self.timeout)
51+
self.client = aiohttp.ClientSession(
52+
headers=headers,
53+
timeout=timeout,
54+
)
55+
return self.client
56+
57+
def _build_payload(self, text: str | list[str]) -> dict:
58+
if isinstance(text, str):
59+
input_text = [text]
60+
else:
61+
input_text = text
62+
63+
return {
64+
"input": input_text,
65+
"model": self.model,
66+
"input_type": self.input_type,
67+
"encoding_format": "float",
68+
}
69+
70+
def _parse_response(self, response_data: dict) -> list[list[float]]:
71+
data = response_data.get("data", [])
72+
embeddings = []
73+
for item in data:
74+
embedding = item.get("embedding", [])
75+
embeddings.append(embedding)
76+
return embeddings
77+
78+
async def get_embedding(self, text: str) -> list[float]:
79+
embeddings = await self.get_embeddings([text])
80+
return embeddings[0] if embeddings else []
81+
82+
async def get_embeddings(self, text: list[str]) -> list[list[float]]:
83+
client = await self._get_client()
84+
if not client or client.closed:
85+
raise Exception("[NVIDIA Embedding] Client session not initialized")
86+
87+
payload = self._build_payload(text)
88+
request_url = f"{self.base_url}/embeddings"
89+
90+
try:
91+
async with client.post(
92+
request_url, json=payload, proxy=self.proxy or None
93+
) as response:
94+
if response.status != 200:
95+
error_text = await response.text()
96+
logger.error(
97+
f"[NVIDIA Embedding] API Error: {response.status} - {error_text}"
98+
)
99+
raise Exception(
100+
f"NVIDIA Embedding API request failed: HTTP {response.status} - {error_text}"
101+
)
102+
103+
response_data = await response.json()
104+
embeddings = self._parse_response(response_data)
105+
106+
usage = response_data.get("usage", {})
107+
total_tokens = usage.get("total_tokens", 0)
108+
if total_tokens > 0:
109+
logger.debug(f"[NVIDIA Embedding] Token usage: {total_tokens}")
110+
111+
return embeddings
112+
113+
except aiohttp.ClientError as e:
114+
logger.error(f"[NVIDIA Embedding] Network error: {e}")
115+
raise
116+
except Exception as e:
117+
logger.error(f"[NVIDIA Embedding] Error: {e}", exc_info=True)
118+
raise
119+
120+
def get_dim(self) -> int:
121+
if "embedding_dimensions" in self.provider_config:
122+
try:
123+
return int(self.provider_config["embedding_dimensions"])
124+
except (ValueError, TypeError):
125+
logger.warning(
126+
f"embedding_dimensions in embedding configs is not a valid integer: "
127+
f"'{self.provider_config['embedding_dimensions']}', ignored."
128+
)
129+
return 0
130+
131+
async def terminate(self):
132+
if self.client and not self.client.closed:
133+
await self.client.close()
134+
self.client = None

0 commit comments

Comments
 (0)