mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-02 02:30:16 +08:00
Compare commits
1 Commits
fix/repeat
...
codex/rest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53296fcc73 |
@@ -9,8 +9,6 @@ from filelock import FileLock, Timeout
|
||||
|
||||
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
|
||||
|
||||
DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_RESET_DASHBOARD_PASSWORD"
|
||||
|
||||
|
||||
async def run_astrbot(astrbot_root: Path) -> None:
|
||||
"""Run AstrBot"""
|
||||
@@ -30,13 +28,8 @@ async def run_astrbot(astrbot_root: Path) -> None:
|
||||
|
||||
@click.option("--reload", "-r", is_flag=True, help="Auto-reload plugins")
|
||||
@click.option("--port", "-p", help="AstrBot Dashboard port", required=False, type=str)
|
||||
@click.option(
|
||||
"--reset-password",
|
||||
is_flag=True,
|
||||
help="Reset dashboard initial password on startup",
|
||||
)
|
||||
@click.command()
|
||||
def run(reload: bool, port: str | None, reset_password: bool) -> None:
|
||||
def run(reload: bool, port: str) -> None:
|
||||
"""Run AstrBot"""
|
||||
try:
|
||||
os.environ["ASTRBOT_CLI"] = "1"
|
||||
@@ -57,9 +50,6 @@ def run(reload: bool, port: str | None, reset_password: bool) -> None:
|
||||
click.echo("Plugin auto-reload enabled")
|
||||
os.environ["ASTRBOT_RELOAD"] = "1"
|
||||
|
||||
if reset_password:
|
||||
os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "1"
|
||||
|
||||
lock_file = astrbot_root / "astrbot.lock"
|
||||
lock = FileLock(lock_file, timeout=5)
|
||||
with lock.acquire():
|
||||
|
||||
@@ -16,7 +16,6 @@ from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
|
||||
|
||||
ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json")
|
||||
DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD"
|
||||
DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_RESET_DASHBOARD_PASSWORD"
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
|
||||
@@ -78,11 +77,7 @@ class AstrBotConfig(dict):
|
||||
)
|
||||
# 检查配置完整性,并插入
|
||||
has_new = self.check_config_integrity(default_config, conf)
|
||||
reset_dashboard_password = self._consume_reset_dashboard_password_flag()
|
||||
if reset_dashboard_password and "dashboard" in conf:
|
||||
self._reset_generated_dashboard_password(conf)
|
||||
has_new = True
|
||||
elif (
|
||||
if (
|
||||
"dashboard" in conf
|
||||
and isinstance(conf["dashboard"], dict)
|
||||
and not conf["dashboard"].get("pbkdf2_password")
|
||||
@@ -123,11 +118,6 @@ class AstrBotConfig(dict):
|
||||
True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _consume_reset_dashboard_password_flag() -> bool:
|
||||
raw_value = os.environ.pop(DASHBOARD_RESET_PASSWORD_ENV, "")
|
||||
return raw_value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
@staticmethod
|
||||
def _resolve_initial_dashboard_password() -> str:
|
||||
env_password = os.environ.get(DASHBOARD_INITIAL_PASSWORD_ENV)
|
||||
|
||||
@@ -186,6 +186,7 @@ class ResultDecorateStage(Stage):
|
||||
|
||||
# 流式输出不执行下面的逻辑
|
||||
if is_stream:
|
||||
logger.info("流式输出已启用,跳过结果装饰阶段")
|
||||
return
|
||||
|
||||
# 需要再获取一次。插件可能直接对 chain 进行了替换。
|
||||
|
||||
@@ -90,7 +90,7 @@ class PipelineScheduler:
|
||||
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
||||
await event.send(None)
|
||||
|
||||
logger.debug("pipeline execution completed.")
|
||||
logger.debug("pipeline 执行完毕。")
|
||||
finally:
|
||||
event.cleanup_temporary_local_files()
|
||||
active_event_registry.unregister(event)
|
||||
|
||||
@@ -97,6 +97,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
if proxy:
|
||||
async_client_kwargs["proxy"] = proxy
|
||||
async_client_kwargs["trust_env"] = False
|
||||
logger.info("[Gemini] 使用代理")
|
||||
else:
|
||||
async_client_kwargs["trust_env"] = True
|
||||
|
||||
@@ -136,18 +137,15 @@ class ProviderGoogleGenAI(Provider):
|
||||
keys.remove(self.chosen_api_key)
|
||||
if len(keys) > 0:
|
||||
self.set_key(random.choice(keys))
|
||||
logger.warning(
|
||||
"Retrying with a different API key due to detected key issue: %s. Current key: %s...",
|
||||
e.message,
|
||||
self.chosen_api_key[:12],
|
||||
logger.info(
|
||||
f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}...",
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
return True
|
||||
logger.error(
|
||||
"No valid API keys remaining. Current key: %s...",
|
||||
self.chosen_api_key[:12],
|
||||
f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...",
|
||||
)
|
||||
raise Exception("Gemini API rate limit reached or API key issue detected.")
|
||||
raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
|
||||
|
||||
# 连接错误处理
|
||||
if is_connection_error(e):
|
||||
@@ -174,9 +172,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
self.provider_settings.get("streaming_response", False)
|
||||
and "IMAGE" in modalities
|
||||
):
|
||||
logger.warning(
|
||||
"Streaming responses do not support IMAGE modality, falling back to TEXT modality."
|
||||
)
|
||||
logger.warning("流式输出不支持图片模态,已自动降级为文本模态")
|
||||
modalities = ["TEXT"]
|
||||
|
||||
tool_list: list[types.Tool] | None = []
|
||||
@@ -185,28 +181,60 @@ class ProviderGoogleGenAI(Provider):
|
||||
native_search = self.provider_config.get("gm_native_search", False)
|
||||
url_context = self.provider_config.get("gm_url_context", False)
|
||||
|
||||
if "gemini-2.0-lite" in model_name:
|
||||
if "gemini-2.5" in model_name:
|
||||
if native_coderunner:
|
||||
tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
|
||||
if native_search:
|
||||
logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
|
||||
if url_context:
|
||||
logger.warning(
|
||||
"代码执行工具与URL上下文工具互斥,已忽略URL上下文工具",
|
||||
)
|
||||
else:
|
||||
if native_search:
|
||||
tool_list.append(types.Tool(google_search=types.GoogleSearch()))
|
||||
|
||||
if url_context:
|
||||
if hasattr(types, "UrlContext"):
|
||||
tool_list.append(types.Tool(url_context=types.UrlContext()))
|
||||
else:
|
||||
logger.warning(
|
||||
"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
|
||||
)
|
||||
|
||||
elif "gemini-2.0-lite" in model_name:
|
||||
if native_coderunner or native_search or url_context:
|
||||
logger.warning(
|
||||
"gemini-2.0-lite does not support native code execution, search, or URL context tools. These settings will be ignored.",
|
||||
"gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置",
|
||||
)
|
||||
tool_list = None
|
||||
|
||||
else:
|
||||
if native_coderunner:
|
||||
tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
|
||||
if native_search:
|
||||
if native_search:
|
||||
logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
|
||||
elif native_search:
|
||||
tool_list.append(types.Tool(google_search=types.GoogleSearch()))
|
||||
if url_context:
|
||||
tool_list.append(types.Tool(url_context=types.UrlContext()))
|
||||
|
||||
if tools:
|
||||
func_desc = tools.get_func_desc_google_genai_style()
|
||||
tool_list.append(
|
||||
types.Tool(function_declarations=func_desc["function_declarations"]),
|
||||
)
|
||||
if url_context and not native_coderunner:
|
||||
if hasattr(types, "UrlContext"):
|
||||
tool_list.append(types.Tool(url_context=types.UrlContext()))
|
||||
else:
|
||||
logger.warning(
|
||||
"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
|
||||
)
|
||||
|
||||
if not tool_list:
|
||||
tool_list = None
|
||||
|
||||
if tools and tool_list:
|
||||
logger.warning("已启用原生工具,函数工具将被忽略")
|
||||
elif tools and (func_desc := tools.get_func_desc_google_genai_style()):
|
||||
tool_list = [
|
||||
types.Tool(function_declarations=func_desc["function_declarations"]),
|
||||
]
|
||||
|
||||
tool_config = None
|
||||
has_func_decl = tool_list and any(t.function_declarations for t in tool_list)
|
||||
if has_func_decl:
|
||||
@@ -294,7 +322,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
def create_text_part(text: str) -> types.Part:
|
||||
content_a = text if text else " "
|
||||
if not text:
|
||||
logger.warning("Text content is empty, added a space as placeholder.")
|
||||
logger.warning("文本内容为空,已添加空格占位")
|
||||
return types.Part.from_text(text=content_a)
|
||||
|
||||
def process_image_url(image_url_dict: dict) -> types.Part:
|
||||
@@ -321,6 +349,12 @@ class ProviderGoogleGenAI(Provider):
|
||||
contents.append(content_cls(parts=part))
|
||||
|
||||
gemini_contents: list[types.Content] = []
|
||||
native_tool_enabled = any(
|
||||
[
|
||||
self.provider_config.get("gm_native_coderunner", False),
|
||||
self.provider_config.get("gm_native_search", False),
|
||||
],
|
||||
)
|
||||
for message in payloads["messages"]:
|
||||
role, content = message["role"], message.get("content")
|
||||
|
||||
@@ -343,10 +377,11 @@ class ProviderGoogleGenAI(Provider):
|
||||
append_or_extend(gemini_contents, parts, types.UserContent)
|
||||
|
||||
elif role == "assistant":
|
||||
parts = []
|
||||
if isinstance(content, str):
|
||||
parts.append(types.Part.from_text(text=content))
|
||||
parts = [types.Part.from_text(text=content)]
|
||||
append_or_extend(gemini_contents, parts, types.ModelContent)
|
||||
elif isinstance(content, list):
|
||||
parts = []
|
||||
thinking_signature = None
|
||||
text = ""
|
||||
for part in content:
|
||||
@@ -365,31 +400,16 @@ class ProviderGoogleGenAI(Provider):
|
||||
exc_info=True,
|
||||
)
|
||||
thinking_signature = None
|
||||
|
||||
if (
|
||||
not text
|
||||
and thinking_signature
|
||||
and "tool_calls" in message
|
||||
and any(
|
||||
isinstance(tool, dict)
|
||||
and isinstance(tool.get("extra_content"), dict)
|
||||
and isinstance(tool["extra_content"].get("google"), dict)
|
||||
and tool["extra_content"]["google"].get("thought_signature")
|
||||
for tool in message["tool_calls"]
|
||||
)
|
||||
):
|
||||
# If the main content is empty but tool calls have thought signatures,
|
||||
# skip adding an empty text part to deduplicate the thinking signature in the main content and tool calls.
|
||||
pass
|
||||
else:
|
||||
parts.append(
|
||||
types.Part(
|
||||
text=text,
|
||||
thought_signature=thinking_signature,
|
||||
)
|
||||
parts.append(
|
||||
types.Part(
|
||||
text=text,
|
||||
thought_signature=thinking_signature,
|
||||
)
|
||||
)
|
||||
append_or_extend(gemini_contents, parts, types.ModelContent)
|
||||
|
||||
if "tool_calls" in message:
|
||||
elif not native_tool_enabled and "tool_calls" in message:
|
||||
parts = []
|
||||
for tool in message["tool_calls"]:
|
||||
part = types.Part.from_function_call(
|
||||
name=tool["function"]["name"],
|
||||
@@ -407,13 +427,17 @@ class ProviderGoogleGenAI(Provider):
|
||||
if ts_bs64:
|
||||
part.thought_signature = base64.b64decode(ts_bs64)
|
||||
parts.append(part)
|
||||
|
||||
if not parts:
|
||||
append_or_extend(gemini_contents, parts, types.ModelContent)
|
||||
else:
|
||||
logger.warning("assistant 角色的消息内容为空,已添加空格占位")
|
||||
if native_tool_enabled and "tool_calls" in message:
|
||||
logger.warning(
|
||||
"检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文",
|
||||
)
|
||||
parts = [types.Part.from_text(text=" ")]
|
||||
append_or_extend(gemini_contents, parts, types.ModelContent)
|
||||
|
||||
append_or_extend(gemini_contents, parts, types.ModelContent)
|
||||
|
||||
elif role == "tool":
|
||||
elif role == "tool" and not native_tool_enabled:
|
||||
func_name = message.get("name", message["tool_call_id"])
|
||||
part = types.Part.from_function_response(
|
||||
name=func_name,
|
||||
@@ -477,7 +501,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
) -> MessageChain:
|
||||
"""处理内容部分并构建消息链"""
|
||||
if not candidate.content:
|
||||
logger.warning(f"Gemini candidate.content is empty: {candidate}")
|
||||
logger.warning(f"收到的 candidate.content 为空: {candidate}")
|
||||
if validate_output:
|
||||
raise EmptyModelOutputError(
|
||||
"Gemini candidate content is empty. "
|
||||
@@ -490,22 +514,22 @@ class ProviderGoogleGenAI(Provider):
|
||||
result_parts: list[types.Part] | None = candidate.content.parts
|
||||
|
||||
if finish_reason == types.FinishReason.SAFETY:
|
||||
raise Exception("The model output failed Gemini platform safety checks.")
|
||||
raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
|
||||
|
||||
if finish_reason in {
|
||||
types.FinishReason.PROHIBITED_CONTENT,
|
||||
types.FinishReason.SPII,
|
||||
types.FinishReason.BLOCKLIST,
|
||||
}:
|
||||
raise Exception("The model output violates Gemini platform policy.")
|
||||
raise Exception("模型生成内容违反 Gemini 平台政策")
|
||||
|
||||
# 防止旧版本SDK不存在IMAGE_SAFETY
|
||||
if hasattr(types.FinishReason, "IMAGE_SAFETY"):
|
||||
if finish_reason == types.FinishReason.IMAGE_SAFETY:
|
||||
raise Exception("The model output violates Gemini platform policy.")
|
||||
raise Exception("模型生成内容违反 Gemini 平台政策")
|
||||
|
||||
if not result_parts:
|
||||
logger.warning(f"Gemini candidate.content.parts is empty: {candidate}")
|
||||
logger.warning(f"收到的 candidate.content.parts 为空: {candidate}")
|
||||
if validate_output:
|
||||
raise EmptyModelOutputError(
|
||||
"Gemini candidate content parts are empty. "
|
||||
@@ -612,19 +636,15 @@ class ProviderGoogleGenAI(Provider):
|
||||
logger.debug(f"genai result: {result}")
|
||||
|
||||
if not result.candidates:
|
||||
logger.error(
|
||||
f"Gemini request failed: candidates is empty: {result}"
|
||||
)
|
||||
raise Exception("Gemini request failed: candidates is empty.")
|
||||
logger.error(f"请求失败, 返回的 candidates 为空: {result}")
|
||||
raise Exception("请求失败, 返回的 candidates 为空。")
|
||||
|
||||
if result.candidates[0].finish_reason == types.FinishReason.RECITATION:
|
||||
if temperature > 2:
|
||||
raise Exception(
|
||||
"Temperature exceeded the maximum value of 2, but Gemini recitation still occurred."
|
||||
)
|
||||
raise Exception("温度参数已超过最大值2,仍然发生recitation")
|
||||
temperature += 0.2
|
||||
logger.warning(
|
||||
f"Gemini recitation detected; increasing temperature to {temperature:.1f} and retrying...",
|
||||
f"发生了recitation,正在提高温度至{temperature:.1f}重试...",
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -635,13 +655,11 @@ class ProviderGoogleGenAI(Provider):
|
||||
e.message = ""
|
||||
if "Developer instruction is not enabled" in e.message:
|
||||
logger.warning(
|
||||
f"{model} does not support system prompts; removing it automatically. This may affect persona settings.",
|
||||
f"{model} 不支持 system prompt,已自动去除(影响人格设置)",
|
||||
)
|
||||
system_instruction = None
|
||||
elif "Function calling is not enabled" in e.message:
|
||||
logger.warning(
|
||||
f"{model} does not support function calling; removing tools automatically."
|
||||
)
|
||||
logger.warning(f"{model} 不支持函数调用,已自动去除")
|
||||
tools = None
|
||||
elif (
|
||||
"Multi-modal output is not supported" in e.message
|
||||
@@ -650,7 +668,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
or "only supports text output" in e.message
|
||||
):
|
||||
logger.warning(
|
||||
f"{model} does not support multimodal output; falling back to TEXT modality.",
|
||||
f"{model} 不支持多模态输出,降级为文本模态",
|
||||
)
|
||||
modalities = ["TEXT"]
|
||||
else:
|
||||
@@ -701,13 +719,11 @@ class ProviderGoogleGenAI(Provider):
|
||||
e.message = ""
|
||||
if "Developer instruction is not enabled" in e.message:
|
||||
logger.warning(
|
||||
f"{model} does not support system prompts; removing it automatically. This may affect persona settings.",
|
||||
f"{model} 不支持 system prompt,已自动去除(影响人格设置)",
|
||||
)
|
||||
system_instruction = None
|
||||
elif "Function calling is not enabled" in e.message:
|
||||
logger.warning(
|
||||
f"{model} does not support function calling; removing tools automatically."
|
||||
)
|
||||
logger.warning(f"{model} 不支持函数调用,已自动去除")
|
||||
tools = None
|
||||
else:
|
||||
raise
|
||||
@@ -722,10 +738,10 @@ class ProviderGoogleGenAI(Provider):
|
||||
llm_response = LLMResponse("assistant", is_chunk=True)
|
||||
|
||||
if not chunk.candidates:
|
||||
logger.warning(f"Gemini stream chunk has empty candidates: {chunk}")
|
||||
logger.warning(f"收到的 chunk 中 candidates 为空: {chunk}")
|
||||
continue
|
||||
if not chunk.candidates[0].content:
|
||||
logger.warning(f"Gemini stream chunk has empty content: {chunk}")
|
||||
logger.warning(f"收到的 chunk 中 content 为空: {chunk}")
|
||||
continue
|
||||
|
||||
if chunk.candidates[0].content.parts and any(
|
||||
@@ -856,7 +872,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
continue
|
||||
break
|
||||
|
||||
raise Exception("Gemini request failed.")
|
||||
raise Exception("请求失败。")
|
||||
|
||||
async def text_chat_stream(
|
||||
self,
|
||||
@@ -931,7 +947,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
and m.name
|
||||
]
|
||||
except APIError as e:
|
||||
raise Exception(f"Failed to fetch Gemini model list: {e.message}")
|
||||
raise Exception(f"获取模型列表失败: {e.message}")
|
||||
|
||||
def get_current_key(self) -> str:
|
||||
return self.chosen_api_key
|
||||
@@ -958,7 +974,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
media_type="image",
|
||||
)
|
||||
if not image_data:
|
||||
logger.warning("Image preprocessing returned no data; ignoring it.")
|
||||
logger.warning("图片预处理结果为空,将忽略。")
|
||||
return None
|
||||
return {
|
||||
"type": "image_url",
|
||||
@@ -973,13 +989,11 @@ class ProviderGoogleGenAI(Provider):
|
||||
strict=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Audio preprocessing failed; ignoring it. Error: %s", exc
|
||||
)
|
||||
logger.warning("音频预处理失败,将忽略。错误: %s", exc)
|
||||
return None
|
||||
|
||||
if not audio_data:
|
||||
logger.warning("Audio preprocessing returned no data; ignoring it.")
|
||||
logger.warning("音频预处理结果为空,将忽略。")
|
||||
return None
|
||||
return {
|
||||
"type": "audio_url",
|
||||
@@ -1015,9 +1029,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
if audio_part:
|
||||
content_blocks.append(audio_part)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported extra content part type: {type(part)}"
|
||||
)
|
||||
raise ValueError(f"不支持的额外内容块类型: {type(part)}")
|
||||
|
||||
# 3. 图片内容
|
||||
if image_urls:
|
||||
|
||||
@@ -60,28 +60,12 @@ Common options:
|
||||
| --- | --- |
|
||||
| `-p, --port <PORT>` | Set the WebUI port. |
|
||||
| `-r, --reload` | Enable plugin auto-reload for plugin development. |
|
||||
| `--reset-password` | Reset the WebUI initial password on startup and print the new password in startup logs. |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
astrbot run --port 6185
|
||||
astrbot run --reload
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
If you forget the WebUI login password, run this from the AstrBot working directory:
|
||||
|
||||
```bash
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
AstrBot regenerates the initial password during startup and prints it in startup logs. After logging in, change the password in the WebUI immediately.
|
||||
|
||||
When starting directly from source, you can also run:
|
||||
|
||||
```bash
|
||||
python main.py --reset-password
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
@@ -60,28 +60,12 @@ astrbot run
|
||||
| --- | --- |
|
||||
| `-p, --port <PORT>` | 指定 WebUI 端口。 |
|
||||
| `-r, --reload` | 启用插件自动重载,适合插件开发调试。 |
|
||||
| `--reset-password` | 启动时重置 WebUI 初始密码,并在启动日志中打印新密码。 |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
astrbot run --port 6185
|
||||
astrbot run --reload
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
如果你忘记了 WebUI 登录密码,可以在 AstrBot 工作目录中执行:
|
||||
|
||||
```bash
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
AstrBot 会在启动时重新生成初始密码,并在启动日志中打印。登录后请立即在 WebUI 中修改密码。
|
||||
|
||||
使用源码方式直接启动时,也可以执行:
|
||||
|
||||
```bash
|
||||
python main.py --reset-password
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
30
main.py
30
main.py
@@ -9,28 +9,6 @@ import runtime_bootstrap
|
||||
|
||||
runtime_bootstrap.initialize_runtime_bootstrap()
|
||||
|
||||
DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_RESET_DASHBOARD_PASSWORD"
|
||||
|
||||
|
||||
def _apply_startup_env_flags(argv: list[str]) -> None:
|
||||
"""Apply startup flags that must take effect before core imports.
|
||||
|
||||
Args:
|
||||
argv: Command-line arguments excluding the executable name.
|
||||
"""
|
||||
|
||||
if "-h" in argv or "--help" in argv:
|
||||
return
|
||||
|
||||
startup_parser = argparse.ArgumentParser(add_help=False)
|
||||
startup_parser.add_argument("--reset-password", action="store_true")
|
||||
startup_args, _ = startup_parser.parse_known_args(argv)
|
||||
if startup_args.reset_password:
|
||||
os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "1"
|
||||
|
||||
|
||||
_apply_startup_env_flags(sys.argv[1:])
|
||||
|
||||
from astrbot.core import LogBroker, LogManager, db_helper, logger # noqa: E402
|
||||
from astrbot.core.config.default import VERSION # noqa: E402
|
||||
from astrbot.core.initial_loader import InitialLoader # noqa: E402
|
||||
@@ -163,14 +141,6 @@ if __name__ == "__main__":
|
||||
help="Specify the directory path for WebUI static files",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reset-password",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Reset the dashboard initial password on startup and print it in "
|
||||
"startup logs"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
check_env()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from astrbot.cli.commands import cmd_run
|
||||
|
||||
|
||||
def test_run_reset_password_sets_startup_env(monkeypatch, tmp_path):
|
||||
(tmp_path / ".astrbot").touch()
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv(cmd_run.DASHBOARD_RESET_PASSWORD_ENV, raising=False)
|
||||
original_env = {
|
||||
"ASTRBOT_CLI": os.environ.get("ASTRBOT_CLI"),
|
||||
"ASTRBOT_ROOT": os.environ.get("ASTRBOT_ROOT"),
|
||||
cmd_run.DASHBOARD_RESET_PASSWORD_ENV: os.environ.get(
|
||||
cmd_run.DASHBOARD_RESET_PASSWORD_ENV
|
||||
),
|
||||
}
|
||||
original_sys_path = list(sys.path)
|
||||
|
||||
called = False
|
||||
|
||||
async def fake_run_astrbot(astrbot_root):
|
||||
nonlocal called
|
||||
called = True
|
||||
assert astrbot_root == tmp_path
|
||||
assert os.environ[cmd_run.DASHBOARD_RESET_PASSWORD_ENV] == "1"
|
||||
|
||||
monkeypatch.setattr(cmd_run, "run_astrbot", fake_run_astrbot)
|
||||
|
||||
try:
|
||||
result = CliRunner().invoke(cmd_run.run, ["--reset-password"])
|
||||
finally:
|
||||
for key, value in original_env.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
sys.path[:] = original_sys_path
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert called is True
|
||||
@@ -10,12 +10,7 @@ from unittest import mock
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils.io import should_use_bundled_dashboard_dist
|
||||
from main import (
|
||||
DASHBOARD_RESET_PASSWORD_ENV,
|
||||
_apply_startup_env_flags,
|
||||
check_dashboard_files,
|
||||
check_env,
|
||||
)
|
||||
from main import check_dashboard_files, check_env
|
||||
|
||||
|
||||
class _version_info:
|
||||
@@ -67,30 +62,6 @@ def test_check_env(monkeypatch):
|
||||
check_env()
|
||||
|
||||
|
||||
def test_apply_startup_env_flags_sets_reset_password_env(monkeypatch):
|
||||
monkeypatch.delenv(DASHBOARD_RESET_PASSWORD_ENV, raising=False)
|
||||
|
||||
_apply_startup_env_flags(["--webui-dir", "/tmp/webui", "--reset-password"])
|
||||
|
||||
assert os.environ[DASHBOARD_RESET_PASSWORD_ENV] == "1"
|
||||
|
||||
|
||||
def test_apply_startup_env_flags_ignores_unrelated_args(monkeypatch):
|
||||
monkeypatch.delenv(DASHBOARD_RESET_PASSWORD_ENV, raising=False)
|
||||
|
||||
_apply_startup_env_flags(["--webui-dir", "/tmp/webui"])
|
||||
|
||||
assert DASHBOARD_RESET_PASSWORD_ENV not in os.environ
|
||||
|
||||
|
||||
def test_apply_startup_env_flags_does_not_reset_for_help(monkeypatch):
|
||||
monkeypatch.delenv(DASHBOARD_RESET_PASSWORD_ENV, raising=False)
|
||||
|
||||
_apply_startup_env_flags(["--reset-password", "--help"])
|
||||
|
||||
assert DASHBOARD_RESET_PASSWORD_ENV not in os.environ
|
||||
|
||||
|
||||
def test_check_env_appends_user_site_packages_after_runtime_paths(monkeypatch):
|
||||
astrbot_root = "/tmp/astrbot-root"
|
||||
site_packages_path = "/tmp/astrbot-site-packages"
|
||||
|
||||
@@ -10,8 +10,6 @@ from astrbot.core.config.default import DEFAULT_VALUE_MAP
|
||||
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
|
||||
from astrbot.core.utils.auth_password import (
|
||||
DEFAULT_DASHBOARD_PASSWORD,
|
||||
hash_dashboard_password,
|
||||
hash_md5_dashboard_password,
|
||||
validate_dashboard_password,
|
||||
verify_dashboard_password,
|
||||
)
|
||||
@@ -322,53 +320,6 @@ class TestAstrBotConfigLoad:
|
||||
config["dashboard"]["password"], generated_password
|
||||
)
|
||||
|
||||
def test_reset_dashboard_password_env_rotates_existing_password(
|
||||
self, temp_config_path, monkeypatch
|
||||
):
|
||||
"""Test startup reset flag rotates an already configured dashboard password."""
|
||||
old_password = "OldPassword123"
|
||||
default_config = {
|
||||
"dashboard": {
|
||||
"username": "astrbot",
|
||||
"password": "",
|
||||
"pbkdf2_password": "",
|
||||
},
|
||||
}
|
||||
with open(temp_config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"dashboard": {
|
||||
"username": "astrbot",
|
||||
"password": hash_md5_dashboard_password(old_password),
|
||||
"pbkdf2_password": hash_dashboard_password(old_password),
|
||||
"password_change_required": False,
|
||||
"password_storage_upgraded": True,
|
||||
}
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
monkeypatch.setenv("ASTRBOT_RESET_DASHBOARD_PASSWORD", "1")
|
||||
config = AstrBotConfig(
|
||||
config_path=temp_config_path,
|
||||
default_config=default_config,
|
||||
)
|
||||
generated_password = getattr(config, "_generated_dashboard_password", None)
|
||||
|
||||
assert isinstance(generated_password, str)
|
||||
assert config["dashboard"]["password_change_required"] is True
|
||||
assert config["dashboard"]["password_storage_upgraded"] is True
|
||||
assert "ASTRBOT_RESET_DASHBOARD_PASSWORD" not in os.environ
|
||||
assert verify_dashboard_password(
|
||||
config["dashboard"]["pbkdf2_password"], generated_password
|
||||
)
|
||||
assert not verify_dashboard_password(
|
||||
config["dashboard"]["pbkdf2_password"], old_password
|
||||
)
|
||||
assert verify_dashboard_password(
|
||||
config["dashboard"]["password"], generated_password
|
||||
)
|
||||
|
||||
def test_legacy_astrbot_user_without_change_flag_keeps_legacy_password(
|
||||
self, temp_config_path
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user