Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions astrbot/core/provider/sources/gemini_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,6 @@ def _process_content_parts(
"""处理内容部分并构建消息链"""
if not candidate.content:
logger.warning(f"收到的 candidate.content 为空: {candidate}")
<<<<<<< HEAD
raise EmptyModelOutputError(
"Gemini candidate content is empty. "
f"finish_reason={candidate.finish_reason}"
Expand All @@ -494,7 +493,6 @@ def _process_content_parts(

if not result_parts:
logger.warning(f"收到的 candidate.content.parts 为空: {candidate}")
<<<<<<< HEAD
raise EmptyModelOutputError(
"Gemini candidate content parts are empty. "
f"finish_reason={candidate.finish_reason}"
Expand Down
114 changes: 35 additions & 79 deletions astrbot/dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,50 +575,42 @@ def get_process_using_port(self, port: int) -> str:
return f"获取进程信息失败: {e!s}"
return "未知进程"

async def run(self) -> None:
"""Run dashboard server (blocking)"""
if self._webui_fallback:
logger.warning(
"前端未内置或未初始化, 回退到仅启动后端. 请访问在线面板: dash.astrbot.men"
)
elif not self.enable_webui:
logger.warning("前端已禁用, 请访问在线面板: dash.astrbot.men")

dashboard_config = self.config.get("dashboard", {})
host_value = os.environ.get("ASTRBOT_HOST") or dashboard_config.get(
"host", "0.0.0.0"
)
host = _resolve_dashboard_value(host_value, field_name="host")
if not isinstance(host, str) or not host:
raise ValueError("Dashboard host must be a non-empty string")
@staticmethod
def _resolve_dashboard_ssl_config(
ssl_config: dict,
) -> tuple[bool, dict[str, str]]:
cert_file = (
os.environ.get("DASHBOARD_SSL_CERT")
or os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT")
or os.environ.get("ASTRBOT_SSL_CERT")
or ssl_config.get("cert_file", "")
)
cert_file = _resolve_dashboard_value(cert_file, field_name="ssl.cert_file")

key_file = (
os.environ.get("DASHBOARD_SSL_KEY")
or os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY")
or os.environ.get("ASTRBOT_SSL_KEY")
or ssl_config.get("key_file", "")
)
key_file = _resolve_dashboard_value(key_file, field_name="ssl.key_file")

ca_certs = (
os.environ.get("DASHBOARD_SSL_CA_CERTS")
or os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS")
or os.environ.get("ASTRBOT_SSL_CA_CERTS")
or ssl_config.get("ca_certs", "")
)
ca_certs = _resolve_dashboard_value(ca_certs, field_name="ssl.ca_certs")

if not cert_file or not key_file:
logger.warning(
"dashboard.ssl.enable 已启用,但未同时配置 cert_file 和 key_file,SSL 配置将不会生效。",
)
return False, {}

cert_path = Path(cert_file).expanduser()
key_path = Path(key_file).expanduser()
cert_path = Path(str(cert_file)).expanduser()
key_path = Path(str(key_file)).expanduser()
if not cert_path.is_file():
logger.warning(
f"dashboard.ssl.enable 已启用,但 SSL 证书文件不存在: {cert_path},SSL 配置将不会生效。",
Expand All @@ -636,7 +628,7 @@ def _resolve_dashboard_ssl_config(
}

if ca_certs:
ca_path = Path(ca_certs).expanduser()
ca_path = Path(str(ca_certs)).expanduser()
if not ca_path.is_file():
logger.warning(
f"dashboard.ssl.enable 已启用,但 SSL CA 证书文件不存在: {ca_path},SSL 配置将不会生效。",
Expand All @@ -646,35 +638,22 @@ def _resolve_dashboard_ssl_config(

return True, resolved_ssl_config

def run(self):
ip_addr = []
dashboard_config = self.core_lifecycle.astrbot_config.get("dashboard", {})
port = (
os.environ.get("DASHBOARD_PORT")
or os.environ.get("ASTRBOT_DASHBOARD_PORT")
or dashboard_config.get("port", 6185)
)
host = (
os.environ.get("DASHBOARD_HOST")
or os.environ.get("ASTRBOT_DASHBOARD_HOST")
or dashboard_config.get("host", "0.0.0.0")
)
enable = dashboard_config.get("enable", True)
ssl_config = dashboard_config.get("ssl", {})
if not isinstance(ssl_config, dict):
ssl_config = {}
ssl_enable = _parse_env_bool(
os.environ.get("DASHBOARD_SSL_ENABLE")
or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE"),
bool(ssl_config.get("enable", False)),
)
resolved_ssl_config: dict[str, str] = {}
if ssl_enable:
ssl_enable, resolved_ssl_config = self._resolve_dashboard_ssl_config(
ssl_config,
async def run(self) -> None:
"""Run dashboard server (blocking)"""
if self._webui_fallback:
logger.warning(
"前端未内置或未初始化, 回退到仅启动后端. 请访问在线面板: dash.astrbot.men"
)
scheme = "https" if ssl_enable else "http"
>>>>>>> origin/master
elif not self.enable_webui:
logger.warning("前端已禁用, 请访问在线面板: dash.astrbot.men")

dashboard_config = self.config.get("dashboard", {})
host_value = os.environ.get("ASTRBOT_HOST") or dashboard_config.get(
"host", "0.0.0.0"
)
host = _resolve_dashboard_value(host_value, field_name="host")
if not isinstance(host, str) or not host:
raise ValueError("Dashboard host must be a non-empty string")

# Port priority: ASTRBOT_PORT env var > cmd_config.json dashboard.port > default 6185
env_port = os.environ.get("ASTRBOT_PORT")
Expand All @@ -697,9 +676,16 @@ def run(self):
port = int(resolved_port)
ssl_config = dashboard_config.get("ssl", {})
ssl_enable = _parse_env_bool(
os.environ.get("ASTRBOT_SSL_ENABLE"),
ssl_config.get("enable", False),
os.environ.get("DASHBOARD_SSL_ENABLE")
or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE")
or os.environ.get("ASTRBOT_SSL_ENABLE"),
bool(ssl_config.get("enable", False)),
)
resolved_ssl_config: dict[str, str] = {}
if ssl_enable:
ssl_enable, resolved_ssl_config = self._resolve_dashboard_ssl_config(
ssl_config,
)

scheme = "https" if ssl_enable else "http"
binds: list[str] = [self._build_bind(host, port)]
Expand Down Expand Up @@ -732,36 +718,6 @@ def run(self):
config.bind = binds

if ssl_enable:
<<<<<<< HEAD
cert_file = os.environ.get("ASTRBOT_SSL_CERT") or ssl_config.get(
"cert_file", ""
)
cert_file = _resolve_dashboard_value(cert_file, field_name="ssl.cert_file")
key_file = os.environ.get("ASTRBOT_SSL_KEY") or ssl_config.get(
"key_file", ""
)
key_file = _resolve_dashboard_value(key_file, field_name="ssl.key_file")
ca_certs = os.environ.get("ASTRBOT_SSL_CA_CERTS") or ssl_config.get(
"ca_certs", ""
)
ca_certs = _resolve_dashboard_value(ca_certs, field_name="ssl.ca_certs")

if cert_file and key_file:
cert_path = await anyio.Path(str(cert_file)).expanduser()
key_path = await anyio.Path(str(key_file)).expanduser()
if not await cert_path.is_file():
raise ValueError(f"SSL 证书文件不存在: {cert_path}")
if not await key_path.is_file():
raise ValueError(f"SSL 私钥文件不存在: {key_path}")

config.certfile = str(await cert_path.resolve())
config.keyfile = str(await key_path.resolve())

if ca_certs:
ca_path = await anyio.Path(str(ca_certs)).expanduser()
if not await ca_path.is_file():
raise ValueError(f"SSL CA 证书文件不存在: {ca_path}")
config.ca_certs = str(await ca_path.resolve())
config.certfile = resolved_ssl_config["certfile"]
config.keyfile = resolved_ssl_config["keyfile"]
if "ca_certs" in resolved_ssl_config:
Expand Down
113 changes: 113 additions & 0 deletions docs/冲突留案/2026-03-30_dev分支残留冲突修复留案.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# dev 分支残留冲突修复留案(2026-03-30)

## 背景
- 近期多个 PR 对 `dev` 分支的检查失败,表现为 `Unit Tests` 与 `Smoke Test` 在 Python import 阶段直接报 `SyntaxError`。
- 失败日志显示 `astrbot/dashboard/server.py` 与 `astrbot/core/provider/sources/gemini_source.py` 中存在残留的 Git conflict markers。
- 本次留案目标:记录冲突来源、两侧差异、合并原则、最终取舍与验证方式,避免只在对话里口头说明。

## 受影响文件
1. `astrbot/dashboard/server.py`
2. `astrbot/core/provider/sources/gemini_source.py`

## 冲突来源定位
- `dev` 分支头包含残留冲突标记。
- 重点可疑 merge 提交:
- `efa999c2`:`merge: origin/master into dev`
- `1faeee37`:`merge: pull latest master into dev`
- 其中 `server.py` 不只是残留标记,还混入了重复 `run()`、同步/异步签名错位、`await` 位置错误等二次损伤。

## 合并原则
1. **必须留案**:每个冲突文件都记录来源、两侧差异、合并理由。
2. **优先合并两侧逻辑**:能兼容就不回滚;仅在局部明显损坏且无法安全拼接时,才退回较稳的一侧。
3. **最小修复范围**:本次只修复阻塞 CI 的冲突与因冲突导致的语法/结构损坏,不顺手做无关重构。
4. **保留可验证证据**:至少保留语法校验与差异说明;后续补充 CI 运行结果。

## 文件一:`astrbot/core/provider/sources/gemini_source.py`

### 冲突形态
- `candidate.content` 为空分支前残留 `<<<<<<< HEAD`
- `candidate.content.parts` 为空分支前残留 `<<<<<<< HEAD`

### 两侧逻辑对比
- 两边实质逻辑接近,核心差异不在业务行为,而在是否残留冲突文本。
- 较新的版本使用 `EmptyModelOutputError`,错误语义更清晰。

### 最终合并结果
- 保留 `EmptyModelOutputError` 分支处理。
- 删除所有 conflict markers。
- 不扩展其它 Gemini 行为变更。

### 合并理由
- 两侧不存在明显互斥功能,属于“同一逻辑 + 脏标记”型冲突。
- 选择保留更明确的异常类型,同时维持最小修改面。

## 文件二:`astrbot/dashboard/server.py`

### 冲突形态
- 残留 `<<<<<<< HEAD` / `>>>>>>> origin/master`
- 文件被错误拼接后出现:
- 重复 `run()` 实现
- 同步 `def run(self)` 与异步 `async def run(self)` 混杂
- `await serve(...)` 落在同步函数上下文中
- 两套 SSL 配置路径处理逻辑互相覆盖

### 两侧逻辑对比
#### dev 侧(旧 async 版本)保留的能力
- `async def run(self)` + `await serve(...)`
- IPv6 / 多 bind 处理:`_build_bind()`
- `WebUI + API` / `API Server` 两种启动提示
- 端口占用检查更细,按 `host` 与 `127.0.0.1` 检查
- `_print_access_urls()` 统一输出本地/网络访问地址
- 通过 `_resolve_dashboard_value()` 支持配置值中的环境变量占位符

#### master 侧(被 merge 进来的版本)新增/增强的能力
- `_resolve_dashboard_ssl_config()` 把 SSL 文件校验抽成独立方法
- 支持 `DASHBOARD_SSL_*` / `ASTRBOT_DASHBOARD_SSL_*` 这类更明确的环境变量命名
- 当 SSL 文件不存在时,采用 warning + 降级失效,而不是直接在运行期抛异常

### 最终合并结果
本次不再简单回滚为 dev 侧,而是采用**合并版**:
- 以 **dev 侧 async run / bind / 访问地址输出** 为主干;
- 引入并保留 **master 侧 `_resolve_dashboard_ssl_config()`** 思路;
- 在该方法中同时兼容:
- `DASHBOARD_SSL_*`
- `ASTRBOT_DASHBOARD_SSL_*`
- `ASTRBOT_SSL_*`
- 继续保留 `_resolve_dashboard_value()`,确保 SSL 路径仍支持占位符展开;
- `run()` 中不再手写三段证书路径解析,而是调用统一 helper 生成 `resolved_ssl_config`;
- `ssl_enable` 读取兼容:
- `DASHBOARD_SSL_ENABLE`
- `ASTRBOT_DASHBOARD_SSL_ENABLE`
- `ASTRBOT_SSL_ENABLE`

### 合并理由
- 用户要求“尽可能合并两者,而不是回滚”;该文件确实存在可兼容空间。
- dev 侧主干更符合当前类的 async 结构与访问地址展示逻辑。
- master 侧 SSL helper 属于结构化增强,值得吸收。
- 将两边优势合并后,可以同时保住:
- 当前 async/多 bind 运行方式
- 新的 SSL 环境变量与降级策略

## 验证
### 已完成
- 清理冲突标记后执行:
- `python -m py_compile astrbot/dashboard/server.py astrbot/core/provider/sources/gemini_source.py`

### 待补充
- 维护者批准 fork PR 的 GitHub Actions 后,补充:
- `Unit Tests`
- `Smoke Test`
的实际结果
- 如需完全对齐仓库开发规范,还应补跑:
- `uv run ruff format .`
- `uv run ruff check .`

## PR / 分支信息
- 修复分支:`fix/dev-merge-conflict-cleanup`
- PR:`#7203`
- 链接:<https://github.com/AstrBotDevs/AstrBot/pull/7203>

## 后续动作
1. 将本留案摘要同步进 PR 描述,避免 reviewer 只能从 diff 猜原因。
2. 待 Actions 获批后观察 CI 是否恢复。
3. 若 CI 仍失败,再按新报错继续追加留案,而不是覆盖旧记录。