Skip to content
Merged
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
72 changes: 71 additions & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,29 @@ jobs:
exit 1
}

- name: "Smoke test"
shell: pwsh
run: |
# 冒烟测试:用临时配置启动产物,确认可以正常运行
$env:PYTHONUTF8 = "1"
'{"account_token":"smoke_test_token","system":{"proxy":null,"logger":{"level":20}},"servers":[]}' | Out-File -Encoding utf8 config.json
$exe = "build\OneDisc_Windows_${{ matrix.arch }}.exe"
$p = Start-Process -FilePath $exe -PassThru -RedirectStandardOutput smoke.out.log -RedirectStandardError smoke.err.log
Start-Sleep -Seconds 25
if (-not $p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
Start-Sleep -Seconds 5
Get-Process | Where-Object { $_.ProcessName -like "OneDisc*" } | Stop-Process -Force -ErrorAction SilentlyContinue
$out = ""
foreach ($f in @("smoke.out.log", "smoke.err.log")) {
if (Test-Path $f) { $out += (Get-Content $f -Raw) + "`n" }
}
Write-Host "===== smoke log ====="
Write-Host $out
if ($out -notmatch "OneDisc \(By: IT Craft Development Team\)" -or $out -notmatch "logging in using static token") {
Write-Error "冒烟测试失败:程序未能正常启动"
exit 1
}

- name: "Upload Release Asset"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -220,6 +243,28 @@ jobs:
exit 1
fi

- name: "Smoke test"
run: |
# 冒烟测试:用临时配置启动产物,确认可以正常运行
cat > config.json <<'EOF'
{
"account_token": "smoke_test_token",
"system": {"proxy": null, "logger": {"level": 20}},
"servers": []
}
EOF
./build/OneDisc_Linux_${{ matrix.arch }} > smoke.log 2>&1 &
PID=$!
sleep 20
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
echo "===== smoke.log ====="
cat smoke.log
if ! grep -q "OneDisc (By: IT Craft Development Team)" smoke.log || ! grep -q "logging in using static token" smoke.log; then
echo "::error::冒烟测试失败:程序未能正常启动"
exit 1
fi

- name: "Upload Release Asset"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -228,15 +273,17 @@ jobs:

build-macos:
needs: get-version-number
runs-on: macos-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
python-arch: x64
os: macos-26-intel
- arch: arm64
python-arch: arm64
os: macos-latest
steps:
- name: "Checkout"
uses: actions/checkout@v4
Expand All @@ -245,6 +292,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
architecture: ${{ matrix.python-arch }}

- name: "Setup Poetry"
uses: snok/install-poetry@v1
Expand Down Expand Up @@ -307,6 +355,28 @@ jobs:
exit 1
fi

- name: "Smoke test"
run: |
# 冒烟测试:用临时配置启动产物,确认可以正常运行
cat > config.json <<'EOF'
{
"account_token": "smoke_test_token",
"system": {"proxy": null, "logger": {"level": 20}},
"servers": []
}
EOF
./build/OneDisc_macOS_${{ matrix.arch }} > smoke.log 2>&1 &
PID=$!
sleep 20
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
echo "===== smoke.log ====="
cat smoke.log
if ! grep -q "OneDisc (By: IT Craft Development Team)" smoke.log || ! grep -q "logging in using static token" smoke.log; then
echo "::error::冒烟测试失败:程序未能正常启动"
exit 1
fi

- name: "Upload Release Asset"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
72 changes: 71 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,29 @@ jobs:
exit 1
}

- name: "Smoke test"
shell: pwsh
run: |
# 冒烟测试:用临时配置启动产物,确认可以正常运行
$env:PYTHONUTF8 = "1"
'{"account_token":"smoke_test_token","system":{"proxy":null,"logger":{"level":20}},"servers":[]}' | Out-File -Encoding utf8 config.json
$exe = "build\onedisc-windows-${{ matrix.arch }}.exe"
$p = Start-Process -FilePath $exe -PassThru -RedirectStandardOutput smoke.out.log -RedirectStandardError smoke.err.log
Start-Sleep -Seconds 25
if (-not $p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
Start-Sleep -Seconds 5
Get-Process | Where-Object { $_.ProcessName -like "onedisc*" } | Stop-Process -Force -ErrorAction SilentlyContinue
$out = ""
foreach ($f in @("smoke.out.log", "smoke.err.log")) {
if (Test-Path $f) { $out += (Get-Content $f -Raw) + "`n" }
}
Write-Host "===== smoke log ====="
Write-Host $out
if ($out -notmatch "OneDisc \(By: IT Craft Development Team\)" -or $out -notmatch "logging in using static token") {
Write-Error "冒烟测试失败:程序未能正常启动"
exit 1
}

- name: "Upload build"
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -235,6 +258,28 @@ jobs:
exit 1
fi

- name: "Smoke test"
run: |
# 冒烟测试:用临时配置启动产物,确认可以正常运行
cat > config.json <<'EOF'
{
"account_token": "smoke_test_token",
"system": {"proxy": null, "logger": {"level": 20}},
"servers": []
}
EOF
./build/onedisc-linux-${{ matrix.arch }} > smoke.log 2>&1 &
PID=$!
sleep 20
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
echo "===== smoke.log ====="
cat smoke.log
if ! grep -q "OneDisc (By: IT Craft Development Team)" smoke.log || ! grep -q "logging in using static token" smoke.log; then
echo "::error::冒烟测试失败:程序未能正常启动"
exit 1
fi

- name: "Upload build"
uses: actions/upload-artifact@v4
with:
Expand All @@ -243,15 +288,17 @@ jobs:

build-macos:
needs: get-version-number
runs-on: macos-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
python-arch: x64
os: macos-26-intel
- arch: arm64
python-arch: arm64
os: macos-latest
steps:
- name: "Checkout"
uses: actions/checkout@v4
Expand All @@ -260,6 +307,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
architecture: ${{ matrix.python-arch }}

- name: "Setup Poetry"
uses: snok/install-poetry@v1
Expand Down Expand Up @@ -330,6 +378,28 @@ jobs:
exit 1
fi

- name: "Smoke test"
run: |
# 冒烟测试:用临时配置启动产物,确认可以正常运行
cat > config.json <<'EOF'
{
"account_token": "smoke_test_token",
"system": {"proxy": null, "logger": {"level": 20}},
"servers": []
}
EOF
./build/onedisc-macos-${{ matrix.arch }} > smoke.log 2>&1 &
PID=$!
sleep 20
kill $PID 2>/dev/null || true
wait $PID 2>/dev/null || true
echo "===== smoke.log ====="
cat smoke.log
if ! grep -q "OneDisc (By: IT Craft Development Team)" smoke.log || ! grep -q "logging in using static token" smoke.log; then
echo "::error::冒烟测试失败:程序未能正常启动"
exit 1
fi

- name: "Upload build"
uses: actions/upload-artifact@v4
with:
Expand Down
78 changes: 78 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# CLAUDE.md

本文件为在 OneDisc 仓库中工作的 AI 编程助手(及人类开发者)提供指南。

## 项目简介

OneDisc 是一个基于 Discord 的 OneBot 实现,支持 OneBot V11 / V12 协议,可对接
NoneBot2、koishi 等 OneBot 框架。v1.0 起使用 Poetry 管理依赖,并使用 Nuitka
将程序编译为各平台可执行文件发布。

## 环境要求

- Python >= 3.12
- Poetry(`poetry install --all-groups` 安装全部依赖,含 dev 组的 nuitka)

## 仓库结构

```
main.py 程序入口(读取配置 → 初始化日志 → 导入插件 → 连接 Discord)
version.py 从 pyproject.toml 读取版本号
pyproject.toml 项目元数据 + 依赖(版本号唯一权威来源,发版前必须修改)
call_action.py 动作分发(根据 OneBot 协议类型调用对应 action)
actions/ OneBot 动作实现
v11/ OneBot V11 动作
v12/ OneBot V12 动作
network/ 各协议版本的网络服务(HTTP / WS / WS 反向)
utils/ 工具模块(配置、日志、数据库、事件、消息解析等)
docs/ VitePress 文档(独立发布到 GitHub Pages)
.github/workflows/ CI 定义(见下文)
```

## 常用命令

```bash
poetry install --all-groups # 安装全部依赖
python main.py # 运行(需先有 config.json,缺失时进入创建向导)
poetry run python -m nuitka ... # 编译(参数见 .github/workflows/ci.yml)
```

## 编码规范与注意事项

1. **日志**:使用 `utils/logger.py` 的 `get_logger()`,每个模块在模块级调用
`logger = get_logger()` 即可获得以本模块命名的 logger。
⚠️ **禁止在 get_logger 中使用 `inspect.stack()`/`inspect.getmodule()` 获取调用方**:
程序使用 Nuitka 编译发布,编译模块没有真实源码文件,inspect 会抛
`AttributeError: 'dict' object has no attribute 'endswith'` 导致程序无法启动
(见 issue #106)。现在通过 `sys._getframe(1).f_globals["__name__"]` 实现。
2. **版本号**:唯一权威来源是 `pyproject.toml` 的 `project.version`。`version.py`
运行时读取它(打包后从可执行文件同目录的数据文件中读取)。
**发版前必须先修改 pyproject.toml 中的版本号**,否则发布的可执行文件会显示
旧版本号(v0.2.10 发布时显示 0.2.9.0 的错误曾真实发生过)。
3. **配置文件**:程序从当前工作目录读取 `config.json`,缺失时进入创建向导并退出。
4. **类型标注**:项目使用较新的类型标注语法(`str | None`),保持风格一致。

## CI / 构建

- `.github/workflows/ci.yml`(Build (Dev)):push 到 master 或手动触发,构建
Windows / Linux / macOS 三平台 onefile 可执行文件并上传 artifact,同时构建并
部署文档到 GitHub Pages。
- `.github/workflows/build-release.yml`(Build (Release)):创建 Release 时触发,
构建并直接把可执行文件上传到 Release 资产。
- 每个构建任务都包含**冒烟测试**:用临时 config.json 启动产物,检查是否打印
「OneDisc (By: IT Craft Development Team)」和「当前版本」后正常存活,防止发布
无法启动的产物(如缺少 websockets 等库)。
- 本地验证构建产物:`./build/onedisc --version` 不可用,请按冒烟测试步骤运行并
观察启动日志。

## 发版流程

1. 修改 `pyproject.toml` 中的 `version`(如 `1.0.0`)。
2. 合并 PR 到 master。
3. 创建 tag(`v1.0.0`)并发布 Release → 自动触发 build-release.yml 构建并上传资产。
4. 下载 Release 资产做冒烟测试确认可运行。

## 其他

- Docker 部署见 `Dockerfile`(python:3.12-slim + poetry + wkhtmltopdf)。
- 文档使用 VitePress(`docs/`),本地预览:`npm install && npm run docs:dev`。
19 changes: 17 additions & 2 deletions utils/logger.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import json
import logging
import sys
import httpx
import discord
import inspect

BASIC_CONFIG = {
"level": 20,
Expand All @@ -18,6 +18,13 @@ def init_logger(logger_config: dict) -> None:
Args:
logger_config (dict): 配置(`config.json->system.logger`)
"""
# 强制 stdout/stderr 使用 UTF-8:在非 UTF-8 locale(如英文 Windows 重定向输出、
# POSIX 环境)下,避免日志中的中文被转义成 \\uXXXX(Nuitka 打包环境同样生效)
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8")
except Exception:
pass
config = BASIC_CONFIG.copy()
config.update(logger_config)
logging.basicConfig(**config)
Expand All @@ -33,7 +40,15 @@ def get_logger(name: str | None = None) -> logging.Logger:
Returns:
logging.Logger: 日志记录器
"""
return logging.getLogger(name or inspect.getmodule(inspect.stack()[1][0]).__name__)
if name is None:
try:
# 使用 sys._getframe 获取调用方模块名,避免使用 inspect。
# 在 Nuitka 编译环境下,编译模块没有真实源码文件,
# inspect.stack()/getmodule() 会抛 AttributeError 导致程序无法启动
name = sys._getframe(1).f_globals.get("__name__")
except Exception:
name = None
return logging.getLogger(name or __name__)


logger = get_logger()
Expand Down
38 changes: 29 additions & 9 deletions version.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
import os
import toml


def _pyproject_candidates() -> list[str]:
"""
返回 pyproject.toml 的候选路径

源码运行时:优先使用当前文件所在目录(仓库根目录)
Nuitka 打包运行时:pyproject.toml 作为数据文件被解压到可执行文件/临时目录中,
此时 __file__ 指向解压目录,因此优先使用 __file__ 所在目录
"""
candidates = [
os.path.join(os.path.dirname(os.path.abspath(__file__)), "pyproject.toml"),
"pyproject.toml",
]
return candidates


def get_version_from_pyproject() -> str:
# 读取 pyproject.toml 文件
with open('pyproject.toml', 'r') as file:
data = toml.load(file)
# 依次尝试所有候选路径,读取 pyproject.toml 文件
for path in _pyproject_candidates():
try:
with open(path, "r", encoding="utf-8") as file:
data = toml.load(file)

# 提取版本号
version = data.get("project", {}).get("version", None)

# 提取版本号
version = data.get('project', {}).get('version', None)
if version:
return version
except Exception:
continue
return "Failed to read version number."

if version:
return version
else:
return "Failed to read version number."

try:
VERSION = get_version_from_pyproject()
Expand Down
Loading