diff --git a/README.md b/README.md index b2d7ffd1..bff11f3c 100644 --- a/README.md +++ b/README.md @@ -73,35 +73,36 @@ After a few weeks, your agent instance will have a skill tree no one else in the ## 🚀 Quick Start -#### Method 1: Standard Installation +### Method 1: One-line install (recommended) -```bash -# 1. Clone the repo -git clone https://github.com/lsdefine/GenericAgent.git -cd GenericAgent +This installs GenericAgent with an isolated Python environment and Git, then downloads a ready-to-run package. -# 2. Install dependencies -pip install requests streamlit pywebview # Desktop GUI (launch.pyw) -pip install requests textual # Terminal UI (tuiapp.py) +**Windows PowerShell** -# 3. Configure API Key -cp mykey_template.py mykey.py -# Edit mykey.py and fill in your LLM API Key - -# 4. Launch -python launch.pyw +```powershell +powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" ``` -#### Method 2: uv (for experienced Python users) +**Linux / macOS** -If you prefer a modern Python workflow, GenericAgent also provides a minimal `pyproject.toml`: +```bash +GLOBAL=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +After installation, launch the desktop app from: + +```text +frontends/GenericAgent.exe +``` + +### Method 2: Python install (for developers) ```bash git clone https://github.com/lsdefine/GenericAgent.git cd GenericAgent uv venv -uv pip install -e ".[ui]" # Core + GUI dependencies -cp mykey_template.py mykey.py +uv pip install -e ".[ui]" # Core + UI dependencies +cp mykey_template.py mykey.py # Fill in your LLM API key python launch.pyw ``` @@ -111,65 +112,55 @@ Full guide: [GETTING_STARTED.md](GETTING_STARTED.md) --- -## 🖥️ Desktop Frontends +## 🖥️ Frontends + +### Desktop App + +For one-line installs on Windows, double-click: + +```text +frontends/GenericAgent.exe +``` ### Terminal UI -A lightweight, keyboard-driven interface built on [Textual](https://github.com/Textualize/textual). Supports multiple concurrent sessions, real-time streaming, and runs anywhere a terminal does — no browser needed. +A lightweight, keyboard-driven interface built on [Textual](https://github.com/Textualize/textual). Supports multiple concurrent sessions and real-time streaming. ```bash -python frontends/tuiapp.py +python frontends/tuiapp_v2.py ``` -### Other Desktop Frontends +### Streamlit UI ```bash -python frontends/qtapp.py # Qt-based desktop app -streamlit run frontends/stapp2.py # Alternative Streamlit UI +python launch.pyw ``` -### Codeg - - - - -
- -[Codeg](https://github.com/yiqi-017/codeg) (`feat/genericagent-integration` branch) is a desktop/web UI that connects GenericAgent alongside other agents (Claude Code, Gemini, Codex, etc.) in a unified interface with a polished, modern UI. - -> This integration is usable now. Some features are still being refined — feedback welcome. - -Place your GenericAgent directory alongside the codeg project. Codeg will auto-detect `frontends/genericagent_acp_bridge.py` and launch GenericAgent as a local ACP agent. - - -Codeg Demo -
- --- ## 💬 Bot Interface (IM) -### Telegram Bot +GenericAgent also supports IM frontends such as Telegram, WeChat, QQ, Feishu / Lark, WeCom, and DingTalk. -```python -# mykey.py -tg_bot_token = 'YOUR_BOT_TOKEN' -tg_allowed_users = [YOUR_USER_ID] -``` +Typical usage: ```bash -python frontends/tgapp.py +python frontends/tgapp.py # Telegram +python frontends/wechatapp.py # WeChat +python frontends/qqapp.py # QQ +python frontends/fsapp.py # Feishu / Lark +python frontends/wecomapp.py # WeCom +python frontends/dingtalkapp.py # DingTalk ``` -### Common Chat Commands +For detailed setup, ask GenericAgent itself. -The default Streamlit desktop UI started by `python launch.pyw`, plus the QQ / Telegram / Feishu / WeCom / DingTalk frontends, support these chat commands: +Common chat commands: - `/new` - start a fresh conversation and clear the current context - `/continue` - list recoverable conversation snapshots - `/continue N` - restore the `N`th recoverable conversation - ## 📊 Comparison with Similar Tools | Feature | GenericAgent | OpenClaw | Claude Code | @@ -345,35 +336,36 @@ MIT License — see [LICENSE](LICENSE) ## 🚀 快速开始 -#### 方法一:标准安装 +### 方法一:一键安装(推荐) -```bash -# 1. 克隆仓库 -git clone https://github.com/lsdefine/GenericAgent.git -cd GenericAgent +一键安装会自动准备独立 Python 环境、Git、项目文件和桌面端,不污染系统环境。 -# 2. 安装依赖 -pip install requests streamlit pywebview # 桌面 GUI (launch.pyw) -pip install requests textual # 终端 UI (tuiapp.py) +**Windows PowerShell** -# 3. 配置 API Key -cp mykey_template.py mykey.py -# 编辑 mykey.py,填入你的 LLM API Key -# 或使用交互式向导:python assets/configure_mykey.py - -# 4. 启动 -python launch.pyw +```powershell +powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" ``` -#### 方法二:uv 快速安装(熟悉 Python 的用户) +**Linux / macOS** -如果你习惯现代 Python 工作流,GenericAgent 也提供了一个最小化的 `pyproject.toml`: +```bash +curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash +``` + +安装完成后,双击启动: + +```text +frontends/GenericAgent.exe +``` + +### 方法二:Python 安装(开发者) ```bash git clone https://github.com/lsdefine/GenericAgent.git cd GenericAgent -uv pip install -e ".[ui]" # 核心 + GUI 依赖 -cp mykey_template.py mykey.py +uv venv +uv pip install -e ".[ui]" # 核心 + UI 依赖 +cp mykey_template.py mykey.py # 填入你的 LLM API Key python launch.pyw ``` @@ -387,133 +379,55 @@ python launch.pyw --- -## 🖥️ 桌面前端 +## 🖥️ 前端启动方式 + +### 桌面端 + +一键安装自带桌面端,双击: + +```text +frontends/GenericAgent.exe +``` ### 终端 UI -基于 [Textual](https://github.com/Textualize/textual) 的轻量键盘驱动界面。支持多会话并发、实时流式输出,有终端就能跑,无需浏览器。 +基于 [Textual](https://github.com/Textualize/textual) 的轻量键盘驱动界面。支持多会话并发、实时流式输出,有终端就能跑。 ```bash -python frontends/tuiapp.py +python frontends/tuiapp_v2.py ``` -### 其他桌面前端 +### Streamlit UI ```bash -python frontends/qtapp.py # 基于 Qt 的桌面应用 -streamlit run frontends/stapp2.py # 另一种 Streamlit 风格 UI +python launch.pyw ``` -### Codeg前端 - - - - -
- -[Codeg](https://github.com/yiqi-017/codeg)(`feat/genericagent-integration` 分支)是一个桌面/Web UI,可以将 GenericAgent 与其他代理(Claude Code、Gemini、Codex 等)在统一界面中并行使用,UI 更加精美。 - -> 此集成已可使用,部分功能仍在完善中,欢迎体验反馈。 - -将 GenericAgent 目录放在 codeg 项目同级目录下,Codeg 会自动检测 `frontends/genericagent_acp_bridge.py` 并将 GenericAgent 作为本地 ACP 代理启动。 - - -Codeg Demo -
- --- ## 💬 Bot 接口(IM) -### 微信 Bot(个人微信) +GenericAgent 支持 Telegram、微信、QQ、飞书 / Lark、企业微信、钉钉等 IM 前端。 -无需额外配置,扫码登录即可: +常用启动方式: ```bash -pip install pycryptodome qrcode requests -python frontends/wechatapp.py +python frontends/tgapp.py # Telegram +python frontends/wechatapp.py # 微信 +python frontends/qqapp.py # QQ +python frontends/fsapp.py # 飞书 / Lark +python frontends/wecomapp.py # 企业微信 +python frontends/dingtalkapp.py # 钉钉 ``` -> 首次启动会弹出二维码,用微信扫码完成绑定。之后通过微信消息与 Agent 交互。 +详细配置直接问 GenericAgent。 -### QQ Bot - -使用 `qq-botpy` WebSocket 长连接,**无需公网 webhook**: - -```bash -pip install qq-botpy -``` - -在 `mykey.py` 中补充: - -```python -qq_app_id = "YOUR_APP_ID" -qq_app_secret = "YOUR_APP_SECRET" -qq_allowed_users = ["YOUR_USER_OPENID"] # 或 ['*'] 公开访问 -``` - -```bash -python frontends/qqapp.py -``` - -> 在 [QQ 开放平台](https://q.qq.com) 创建机器人获取 AppID / AppSecret。首次消息后,用户 openid 记录于 `temp/qqapp.log`。 - -### 飞书(Lark) - -```bash -pip install lark-oapi -python frontends/fsapp.py -``` - -```python -fs_app_id = "cli_xxx" -fs_app_secret = "xxx" -fs_allowed_users = ["ou_xxx"] # 或 ['*'] -``` - -**入站支持**:文本、富文本 post、图片、文件、音频、media、交互卡片 / 分享卡片 -**出站支持**:流式进度卡片、图片回传、文件 / media 回传 -**视觉模型**:图片首轮以真正的多模态输入发送给兼容 OpenAI Vision 的后端 - -详细配置见 [assets/SETUP_FEISHU.md](assets/SETUP_FEISHU.md) - - -### 企业微信(WeCom) - -```bash -pip install wecom_aibot_sdk -python frontends/wecomapp.py -``` - -```python -wecom_bot_id = "your_bot_id" -wecom_secret = "your_bot_secret" -wecom_allowed_users = ["your_user_id"] -wecom_welcome_message = "你好,我在线上。" -``` - -### 钉钉(DingTalk) - -```bash -pip install dingtalk-stream -python frontends/dingtalkapp.py -``` - -```python -dingtalk_client_id = "your_app_key" -dingtalk_client_secret = "your_app_secret" -dingtalk_allowed_users = ["your_staff_id"] # 或 ['*'] -``` - -### 通用聊天命令 - -默认通过 `python launch.pyw` 启动的 Streamlit 桌面 UI,以及 QQ / Telegram / 飞书 / 企业微信 / 钉钉前端,都支持以下命令: +通用聊天命令: - `/new` - 开启新对话并清空当前上下文 - `/continue` - 列出可恢复会话快照 - `/continue N` - 恢复第 `N` 个可恢复会话 - ## 📊 与同类产品对比 | 特性 | GenericAgent | OpenClaw | Claude Code | diff --git a/frontends/desktop/src-tauri/src/lib.rs b/frontends/desktop/src-tauri/src/lib.rs index adbc6b9f..318546fb 100644 --- a/frontends/desktop/src-tauri/src/lib.rs +++ b/frontends/desktop/src-tauri/src/lib.rs @@ -10,15 +10,62 @@ use std::os::windows::process::CommandExt; static BRIDGE_PROCESS: Mutex> = Mutex::new(None); +/// Get project root (parent of frontends/) +fn project_root() -> PathBuf { + std::env::current_exe() + .expect("cannot get exe path") + .parent().expect("cannot get exe dir") // frontends/ + .parent().expect("cannot get project root") // project root + .to_path_buf() +} + fn find_bridge_script() -> PathBuf { // exe is at frontends/GenericAgent.exe // bridge is at frontends/desktop_bridge.py - let exe_dir = std::env::current_exe() + std::env::current_exe() .expect("cannot get exe path") - .parent() - .expect("cannot get exe dir") - .to_path_buf(); - exe_dir.join("desktop_bridge.py") + .parent().expect("cannot get exe dir") + .join("desktop_bridge.py") +} + +/// Find python executable: +/// 1. .portable/uv-python/ 下找 python.exe (Windows) 或 python3 (Unix) +/// 2. Fallback to system PATH +fn find_python() -> String { + let root = project_root(); + let portable_python_dir = root.join(".portable").join("uv-python"); + + if portable_python_dir.exists() { + // uv installs python like: uv-python/cpython-3.12.x-windows-x86_64/python.exe + // We need to search for python.exe inside subdirectories + if let Ok(entries) = std::fs::read_dir(&portable_python_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + #[cfg(windows)] + { + let py = path.join("python.exe"); + if py.exists() { + return py.to_string_lossy().to_string(); + } + } + #[cfg(not(windows))] + { + let py = path.join("bin").join("python3"); + if py.exists() { + return py.to_string_lossy().to_string(); + } + } + } + } + } + } + + // Fallback: system PATH + #[cfg(windows)] + { "python".to_string() } + #[cfg(not(windows))] + { "python3".to_string() } } fn wait_for_port(port: u16, timeout: Duration) -> bool { @@ -39,9 +86,12 @@ fn start_bridge() { return; } + let python = find_python(); + eprintln!("[tauri] using python: {}", python); + let show_console = std::env::args().any(|a| a == "--console"); - let mut cmd = Command::new("python"); + let mut cmd = Command::new(&python); cmd.arg(&script) .current_dir(script.parent().unwrap()); @@ -51,10 +101,16 @@ fn start_bridge() { cmd.creation_flags(CREATE_NO_WINDOW); } - let child = cmd.spawn().expect("failed to start desktop_bridge.py"); - - eprintln!("[tauri] started bridge PID={}", child.id()); - *BRIDGE_PROCESS.lock().unwrap() = Some(child); + match cmd.spawn() { + Ok(child) => { + eprintln!("[tauri] started bridge PID={}", child.id()); + *BRIDGE_PROCESS.lock().unwrap() = Some(child); + } + Err(e) => { + eprintln!("[tauri] failed to start bridge: {} (python={})", e, python); + return; + } + } if !wait_for_port(14168, Duration::from_secs(15)) { eprintln!("[tauri] WARNING: bridge did not become ready within 15s"); diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index e5a3b2b3..729be447 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -309,8 +309,11 @@ function formatTaskElapsed(ms, ended) { if (hours) parts.push(`${hours}h`); if (hours || minutes) parts.push(`${minutes}min`); parts.push(`${seconds}s`); - const text = `已运行 ${parts.join(' ')}`; - return ended ? `${text}. 已完成!` : text; + const elapsed = parts.join(' '); + if (ended) return `Done ✓ ${elapsed}`; + const spinner = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'; + const frame = spinner[Math.floor(Date.now() / 1000) % spinner.length]; + return `${frame} ${elapsed}`; } function getSessionRuntime(sess) { @@ -420,8 +423,8 @@ function ensureAssistantTaskElapsed(wrap, startedAt, endedAt) { function turnLabelForSegment(seg, index) { const summary = summarizeStructuredBlock(seg.kind, seg.text); if (seg.kind === 'LLM_RUNNING') return summary || `Turn ${index + 1}`; - if (seg.kind === 'TOOL_CALL') return '工具调用'; - if (seg.kind === 'TOOL_RESULT') return '工具结果'; + if (seg.kind === 'TOOL_CALL') return 'Tool'; + if (seg.kind === 'TOOL_RESULT') return 'Result'; return summary || seg.kind || `Turn ${index + 1}`; } @@ -501,7 +504,7 @@ function renderTurnTreeInto(container, turn) { const header = document.createElement('button'); header.type = 'button'; header.className = 'turn-header'; - header.innerHTML = `▼${escapeHtml(turnHeaderLabel(turn.index, `已处理. ${turn.label}`))}`; + header.innerHTML = `▼${escapeHtml(turnHeaderLabel(turn.index, `Done. ${turn.label}`))}`; header.addEventListener('click', () => node.classList.toggle('collapsed')); const body = document.createElement('div'); body.className = 'turn-body md';