"""浏览器开发调试入口:用已安装版的同一份数据启动 Web 服务。
与桌面版(desktop.py)的区别:
- 不启动 pywebview 窗口,直接在浏览器访问(原生对话框不可用:任务下发的选目录
走后端列目录的内置弹窗,技能迁移/本地模型仍退化为手动输入路径)
- 默认共享已安装版的数据目录(%APPDATA%\\a4agent),配置方案 / 供应商 /
本地模型设置全部实时同步
- 跑的是仓库源码最新代码,改完前端刷新浏览器即可,无需重新打包安装
服务就绪后会自动打开浏览器,无需手动刷新等待。
用法:
uv run python dev_server.py # http://127.0.0.1:18900
uv run python dev_server.py 18901 # 自定义端口
"""
import json
import os
import subprocess
import sys
import threading
import time
import webbrowser
from pathlib import Path
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
os.environ.setdefault(
"A4AGENT_DATA_DIR",
str(Path(os.environ.get("APPDATA", Path.home())) / "a4agent"),
)
DEFAULT_PORT = 18900
def _write_instance(port: int) -> None:
"""记录运行端口,供进程内插件发现本服务。
dsh 的 a4agent-dsh-hook 靠这个文件找到回调地址(它跑在 dsh 进程内,
无法像外部 hook 那样直接调 a4agent hook 子命令)。桌面版由 desktop.py
的 write_instance 负责;开发版不写的话,插件会判定「a4agent 未运行」
而静默跳过通知——测试全绿、真机不响的典型来源。
"""
try:
from backend.app.database import get_data_dir
path = get_data_dir() / "desktop.json"
path.write_text(json.dumps({"port": port, "pid": os.getpid()}),
encoding="utf-8")
except Exception as exc:
print(f"⚠ 无法写入 desktop.json(dsh hook 将无法回调本服务):{exc}")
def _clear_instance() -> None:
try:
from backend.app.database import get_data_dir
(get_data_dir() / "desktop.json").unlink(missing_ok=True)
except OSError:
pass
def _warn_if_desktop_running() -> None:
"""已安装版与调试版同时写同一份数据可能偶发 SQLite 锁冲突,仅提示不阻止。
改名过渡期新旧可执行文件并存:a4agent.exe(新版)与 a4api.exe(≤v0.3.3)都查。
"""
try:
out = subprocess.run(
["tasklist"],
capture_output=True, text=True, timeout=10,
encoding="utf-8", errors="replace",
).stdout
running = [name for name in ("a4agent.exe", "a4api.exe") if name in out]
if running:
print(f"⚠ 检测到已安装版({' / '.join(running)})正在运行。")
print(" 两边共享同一份数据,同时改动可能偶发写入冲突;建议先关闭已安装版再调试。")
except Exception:
pass
def _open_browser_when_ready(port: int) -> None:
"""轮询直到服务真正可访问,再打开浏览器(避免页面先于服务打开报拒绝连接)。"""
import urllib.request
url = f"http://127.0.0.1:{port}"
for _ in range(120):
try:
urllib.request.urlopen(url, timeout=1)
print(f"✓ 服务已就绪,正在打开浏览器:{url}")
webbrowser.open(url)
return
except Exception:
time.sleep(0.5)
def main() -> None:
port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PORT
import uvicorn
from backend.app.main import app
print(f"a4agent 浏览器调试版数据目录(与已安装版共享):{os.environ['A4AGENT_DATA_DIR']}")
print("Ctrl+C 停止服务")
_warn_if_desktop_running()
threading.Thread(
target=_open_browser_when_ready, args=(port,), daemon=True
).start()
_write_instance(port)
try:
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")
uvicorn.Server(config).run()
finally:
_clear_instance()
if __name__ == "__main__":
main()