Compare commits

..

1 Commits

Author SHA1 Message Date
Soulter
4d00309d70 fix: remove python-ripgrep dependency, use system rg/grep instead
python-ripgrep 0.0.9 does not support Python 3.13 (requires <3.13,>=3.10).
This change replaces the python-ripgrep library with direct subprocess calls
to system ripgrep (rg) with fallback to grep.

Changes:
- Remove python-ripgrep import from local.py
- Rewrite search_files() to use shutil.which() to detect rg/grep availability
- Support ripgrep first, fallback to grep if not available
- Handle proper exit codes (0=success, 1=no matches for grep)
- Remove python-ripgrep from requirements.txt and pyproject.toml

Fixes #7496
2026-04-13 16:15:59 +08:00
6 changed files with 92 additions and 187 deletions

View File

@@ -13,23 +13,10 @@ on:
jobs:
smoke-test:
name: Smoke test (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
name: Run smoke tests
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
python-version:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
- '3.14'
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -39,21 +26,33 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install uv
python-version: '3.12'
- name: Install UV package manager
run: |
python -m pip install --upgrade pip
python -m pip install uv
pip install uv
- name: Install dependencies
run: |
uv pip install --system -r requirements.txt
uv sync
timeout-minutes: 15
- name: Run smoke tests
run: |
python scripts/smoke_startup_check.py
uv run main.py &
APP_PID=$!
echo "Waiting for application to start..."
for i in {1..60}; do
if curl -f http://localhost:6185 > /dev/null 2>&1; then
echo "Application started successfully!"
kill $APP_PID
exit 0
fi
sleep 1
done
echo "Application failed to start within 30 seconds"
kill $APP_PID 2>/dev/null || true
exit 1
timeout-minutes: 2

View File

@@ -9,8 +9,6 @@ import sys
from dataclasses import dataclass
from typing import Any
from python_ripgrep import search
from astrbot.api import logger
from astrbot.core.computer.file_read_utils import (
detect_text_encoding,
@@ -221,15 +219,57 @@ class LocalFileSystemComponent(FileSystemComponent):
before_context: int | None = None,
) -> dict[str, Any]:
def _run() -> dict[str, Any]:
results = search(
patterns=[pattern],
paths=[path] if path else None,
globs=[glob] if glob else None,
after_context=after_context,
before_context=before_context,
line_number=True,
)
return {"success": True, "content": _truncate_long_lines("".join(results))}
search_path = path if path else "."
# Try ripgrep first, fallback to grep
if shutil.which("rg"):
cmd = ["rg", "--line-number", "--color=never"]
if glob:
cmd.extend(["--glob", glob])
if after_context:
cmd.extend(["--after-context", str(after_context)])
if before_context:
cmd.extend(["--before-context", str(before_context)])
cmd.extend([pattern, search_path])
elif shutil.which("grep"):
cmd = ["grep", "-rn", "--color=never"]
if after_context:
cmd.extend(["-A", str(after_context)])
if before_context:
cmd.extend(["-B", str(before_context)])
# grep doesn't support glob directly, use include if available
if glob and shutil.which("grep"):
# Try to use --include if grep supports it (GNU grep)
cmd.extend(["--include", glob])
cmd.extend([pattern, search_path])
else:
return {
"success": False,
"error": "Neither ripgrep (rg) nor grep is available on the system",
}
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
)
# grep returns exit code 1 when no matches found, which is not an error
if result.returncode not in (0, 1):
return {
"success": False,
"error": f"Search command failed with exit code {result.returncode}: {result.stderr}",
}
output = result.stdout if result.stdout else ""
return {"success": True, "content": _truncate_long_lines(output)}
except subprocess.TimeoutExpired:
return {
"success": False,
"error": "Search command timed out after 30 seconds",
}
except Exception as e:
return {"success": False, "error": f"Search failed: {str(e)}"}
return await asyncio.to_thread(_run)

View File

@@ -860,15 +860,17 @@ export default {
// 过滤出属于该平台的路由,并保持顺序
const routes = [];
for (const [umop, confId] of Object.entries(routingTable)) {
const parsedUmop = this.parseUmop(umop);
if (this.isParsedUmopMatchPlatform(parsedUmop, platformId)) {
routes.push({
umop: umop,
originalUmop: umop, // 保存原始 UMOP 用于更新时查找
messageType: parsedUmop.messageType || '*',
sessionId: parsedUmop.sessionId || '*',
configId: confId
});
if (this.isUmopMatchPlatform(umop, platformId)) {
const parts = umop.split(':');
if (parts.length === 3) {
routes.push({
umop: umop,
originalUmop: umop, // 保存原始 UMOP 用于更新时查找
messageType: parts[1] === '' || parts[1] === '*' ? '*' : parts[1],
sessionId: parts[2] === '' || parts[2] === '*' ? '*' : parts[2],
configId: confId
});
}
}
}
@@ -990,29 +992,11 @@ export default {
},
isUmopMatchPlatform(umop, platformId) {
const parsedUmop = this.parseUmop(umop);
return this.isParsedUmopMatchPlatform(parsedUmop, platformId);
},
isParsedUmopMatchPlatform(parsedUmop, platformId) {
if (!parsedUmop) return false;
return parsedUmop.platform === platformId || parsedUmop.platform === '' || parsedUmop.platform === '*';
},
parseUmop(umop) {
if (!umop) return null;
const firstSeparatorIndex = umop.indexOf(':');
if (firstSeparatorIndex === -1) return null;
const secondSeparatorIndex = umop.indexOf(':', firstSeparatorIndex + 1);
if (secondSeparatorIndex === -1) return null;
return {
platform: umop.slice(0, firstSeparatorIndex),
messageType: umop.slice(firstSeparatorIndex + 1, secondSeparatorIndex),
sessionId: umop.slice(secondSeparatorIndex + 1)
};
if (!umop) return false;
const parts = umop.split(':');
if (parts.length !== 3) return false;
const platform = parts[0];
return platform === platformId || platform === '' || platform === '*';
},
// 获取消息类型标签

View File

@@ -64,7 +64,6 @@ dependencies = [
"python-socks>=2.8.0",
"pysocks>=1.7.1",
"packaging>=24.2",
"python-ripgrep==0.0.8",
]
[dependency-groups]

View File

@@ -52,5 +52,4 @@ tenacity>=9.1.2
shipyard-python-sdk>=0.2.4
shipyard-neo-sdk>=0.2.0
packaging>=24.2
qrcode>=8.2
python-ripgrep==0.0.8
qrcode>=8.2

View File

@@ -1,116 +0,0 @@
"""Cross-platform startup smoke check for AstrBot."""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
HEALTH_URL = "http://127.0.0.1:6185"
STARTUP_TIMEOUT_SECONDS = 60
REQUEST_TIMEOUT_SECONDS = 2
def _tail(path: Path, lines: int = 80) -> str:
try:
content = path.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError as exc:
return f"Unable to read smoke log: {exc}"
return "\n".join(content[-lines:])
def _is_ready() -> bool:
try:
with urllib.request.urlopen( # noqa: S310
HEALTH_URL,
timeout=REQUEST_TIMEOUT_SECONDS,
) as response:
return response.status < 400
except (OSError, urllib.error.URLError):
return False
def _stop_process(proc: subprocess.Popen[bytes]) -> None:
if proc.poll() is not None:
return
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=10)
def main() -> int:
env = os.environ.copy()
env.setdefault("PYTHONUTF8", "1")
env.setdefault("TESTING", "true")
smoke_root = Path(tempfile.mkdtemp(prefix="astrbot-smoke-root-"))
env["ASTRBOT_ROOT"] = str(smoke_root)
log_path = smoke_root / "smoke.log"
webui_dir = smoke_root / "webui"
webui_dir.mkdir()
(webui_dir / "index.html").write_text(
"<!doctype html><title>AstrBot</title>",
encoding="utf-8",
)
with log_path.open("wb") as log_file:
proc = subprocess.Popen(
[
sys.executable,
str(REPO_ROOT / "main.py"),
"--webui-dir",
str(webui_dir),
],
cwd=REPO_ROOT,
stdout=log_file,
stderr=subprocess.STDOUT,
env=env,
)
print(f"Starting smoke test on {HEALTH_URL}")
deadline = time.monotonic() + STARTUP_TIMEOUT_SECONDS
try:
while time.monotonic() < deadline:
if _is_ready():
print("Smoke test passed")
return 0
return_code = proc.poll()
if return_code is not None:
print(
f"AstrBot exited before becoming healthy. Exit code: {return_code}",
file=sys.stderr,
)
print(_tail(log_path), file=sys.stderr)
return 1
time.sleep(1)
print(
"Smoke test failed: health endpoint did not become ready in time.",
file=sys.stderr,
)
print(_tail(log_path), file=sys.stderr)
return 1
finally:
_stop_process(proc)
try:
log_path.unlink()
except OSError:
pass
shutil.rmtree(smoke_root, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())