From 2aaa384a05539b4adf3c6e04669949408efe89c6 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Mon, 6 Jul 2026 22:19:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4=EF=BC=9AOp?= =?UTF-8?q?enClaw=20Extra=20=E7=AE=A1=E7=90=86=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能: - 📋 智能体列表和管理 - 💬 会话内容查看 - 🤖 模型列表 - 📁 工作区文件浏览 - 🌐 Web 管理面板(端口 16026) 包含: - FastAPI Web 后端 - 网页前端界面 - CLI 命令行工具 - Python API 客户端 --- .gitignore | 41 ++ README.md | 92 ++++ pyproject.toml | 49 ++ src/openclaw_extra/__init__.py | 9 + src/openclaw_extra/cli.py | 585 +++++++++++++++++++++ src/openclaw_extra/client.py | 386 ++++++++++++++ src/openclaw_extra/exceptions.py | 33 ++ src/openclaw_extra/models.py | 199 +++++++ src/openclaw_extra/web/__init__.py | 1 + src/openclaw_extra/web/app.py | 259 +++++++++ src/openclaw_extra/web/static/agent.html | 263 +++++++++ src/openclaw_extra/web/static/index.html | 142 +++++ src/openclaw_extra/web/static/models.html | 170 ++++++ src/openclaw_extra/web/static/session.html | 103 ++++ 14 files changed, 2332 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/openclaw_extra/__init__.py create mode 100644 src/openclaw_extra/cli.py create mode 100644 src/openclaw_extra/client.py create mode 100644 src/openclaw_extra/exceptions.py create mode 100644 src/openclaw_extra/models.py create mode 100644 src/openclaw_extra/web/__init__.py create mode 100644 src/openclaw_extra/web/app.py create mode 100644 src/openclaw_extra/web/static/agent.html create mode 100644 src/openclaw_extra/web/static/index.html create mode 100644 src/openclaw_extra/web/static/models.html create mode 100644 src/openclaw_extra/web/static/session.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7438f7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +ENV/ +env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcd6ac0 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# openclaw-extra + +OpenClaw extra utilities - manage agents, sessions, models, and workspaces. + +## 功能 + +- 📋 **智能体列表**: 列出所有 OpenClaw 智能体及其状态 +- 💬 **对话内容**: 查看各智能体的对话历史(包含使用的模型信息) +- 🤖 **模型列表**: 查看 OpenClaw 可用的大模型列表 +- 📁 **工作区文件**: 浏览各智能体的工作区文件 +- 🌐 **Web 面板**: 网页端管理界面,直观易用 + +## 安装 + +```bash +pip install openclaw-extra +``` + +## 使用 + +### Web 管理面板(推荐) + +```bash +# 启动 Web 服务 +openclaw-extra web + +# 自定义端口和地址 +openclaw-extra web --port 9000 --host 127.0.0.1 + +默认端口 16026,访问 http://localhost:16026 +``` + +访问 `http://localhost:8080` 即可看到网页管理界面。 + +### 基本命令 + +```bash +# 显示帮助 +openclaw-extra --help + +# 列出所有智能体 +openclaw-extra agents + +# 列出可用模型 +openclaw-extra models + +# 查看智能体详情 +openclaw-extra agent + +# 查看会话列表 +openclaw-extra sessions --agent + +# 查看工作区文件 +openclaw-extra workspace +``` + +### 环境配置 + +可以通过环境变量或配置文件设置 Gateway 连接: + +```bash +# 设置 Gateway URL (默认: http://127.0.0.1:18789) +export OPENCLAW_GATEWAY_URL="http://127.0.0.1:18789" + +# 设置 Gateway Token (如果需要认证) +export OPENCLAW_GATEWAY_TOKEN="your-token" +``` + +## API 使用 + +```python +from openclaw_extra import OpenClawClient + +# 创建客户端 +client = OpenClawClient() + +# 获取智能体列表 +agents = client.get_agents() + +# 获取模型列表 +models = client.get_models() + +# 获取会话列表 +sessions = client.get_sessions(agent_id="hz4th_coder") + +# 获取工作区信息 +workspace = client.get_workspace(agent_id="hz4th_coder") +``` + +## License + +MIT \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1380d97 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "openclaw-extra" +version = "1.0.0" +description = "OpenClaw extra utilities - manage agents, sessions, models, and workspaces" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.8" +authors = [ + {name = "hz4th_coder", email = "hz4th_coder@tphai.com"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "requests>=2.28.0", + "websocket-client>=1.4.0", + "rich>=13.0.0", + "typer>=0.9.0", + "fastapi>=0.100.0", + "uvicorn>=0.23.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", +] + +[project.scripts] +openclaw-extra = "openclaw_extra.cli:app" + +[project.urls] +Homepage = "http://121.40.164.32:12007/hz4th_coder/openclaw-extra" +Repository = "http://121.40.164.32:12007/hz4th_coder/openclaw-extra.git" + +[tool.setuptools.packages.find] +where = ["src"] \ No newline at end of file diff --git a/src/openclaw_extra/__init__.py b/src/openclaw_extra/__init__.py new file mode 100644 index 0000000..95871fb --- /dev/null +++ b/src/openclaw_extra/__init__.py @@ -0,0 +1,9 @@ +""" +OpenClaw Extra - OpenClaw management utilities +""" + +from .client import OpenClawClient +from .models import AgentInfo, SessionInfo, ModelInfo, WorkspaceInfo + +__version__ = "1.0.0" +__all__ = ["OpenClawClient", "AgentInfo", "SessionInfo", "ModelInfo", "WorkspaceInfo"] \ No newline at end of file diff --git a/src/openclaw_extra/cli.py b/src/openclaw_extra/cli.py new file mode 100644 index 0000000..8860f4d --- /dev/null +++ b/src/openclaw_extra/cli.py @@ -0,0 +1,585 @@ +""" +OpenClaw Extra CLI +""" + +import os +import sys +import typer +from typing import Optional, List +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich.tree import Tree +from rich.text import Text +from rich import print as rprint + +from .client import OpenClawClient +from .models import AgentInfo, SessionInfo, ModelInfo, WorkspaceInfo, TranscriptMessage + +app = typer.Typer( + name="openclaw-extra", + help="OpenClaw extra utilities - manage agents, sessions, models, and workspaces", + add_completion=False, +) + +console = Console() + + +def get_client() -> OpenClawClient: + """获取 OpenClaw 客户端""" + gateway_url = os.environ.get("OPENCLAW_GATEWAY_URL") + gateway_token = os.environ.get("OPENCLAW_GATEWAY_TOKEN") + return OpenClawClient(gateway_url=gateway_url, gateway_token=gateway_token) + + +@app.command("agents") +def list_agents(): + """列出所有智能体""" + client = get_client() + + try: + agents = client.get_agents() + except Exception as e: + console.print(f"[red]错误: 无法连接到 Gateway[/red]") + console.print(f"[yellow]详情: {e}[/yellow]") + raise typer.Exit(1) + + if not agents: + console.print("[yellow]没有找到智能体[/yellow]") + return + + table = Table(title="OpenClaw 智能体列表", show_header=True, header_style="bold cyan") + table.add_column("ID", style="cyan") + table.add_column("名称", style="green") + table.add_column("默认", style="yellow") + table.add_column("心跳", style="blue") + table.add_column("会话数", style="magenta") + table.add_column("模型", style="white") + table.add_column("工作区", style="dim") + + for agent in agents: + heartbeat_str = f"{agent.heartbeat_every}" if agent.heartbeat_enabled else "-" + workspace_short = os.path.basename(agent.workspace or "") if agent.workspace else "-" + + table.add_row( + agent.agent_id, + agent.name or agent.agent_id, + "✓" if agent.is_default else "", + heartbeat_str, + str(agent.sessions_count), + agent.model or "-", + workspace_short, + ) + + console.print(table) + client.close() + + +@app.command("agent") +def show_agent(agent_id: str): + """显示智能体详情 + + Args: + agent_id: 智能体 ID + """ + client = get_client() + + try: + agent = client.get_agent(agent_id) + except Exception as e: + console.print(f"[red]错误: 无法连接到 Gateway[/red]") + raise typer.Exit(1) + + if not agent: + console.print(f"[red]未找到智能体: {agent_id}[/red]") + raise typer.Exit(1) + + # 显示基本信息 + info_text = f""" +[bold cyan]智能体 ID:[/bold cyan] {agent.agent_id} +[bold green]名称:[/bold green] {agent.name or agent.agent_id} +[bold yellow]默认智能体:[/bold yellow] {"是" if agent.is_default else "否"} +[bold blue]模型:[/bold blue] {agent.model or "未设置"} +[bold magenta]心跳:[/bold magenta] {"启用" if agent.heartbeat_enabled else "禁用"} ({agent.heartbeat_every or "-"}) +[bold white]会话数:[/bold white] {agent.sessions_count} +""" + console.print(Panel(info_text, title=f"[bold]{agent.agent_id}[/bold]", border_style="cyan")) + + # 显示路径信息 + if agent.workspace or agent.agent_dir: + paths_table = Table(title="路径", show_header=True, header_style="bold") + paths_table.add_column("类型", style="cyan") + paths_table.add_column("路径", style="green") + + if agent.workspace: + paths_table.add_row("工作区", agent.workspace) + if agent.agent_dir: + paths_table.add_row("智能体目录", agent.agent_dir) + if agent.sessions_path: + paths_table.add_row("会话存储", agent.sessions_path) + + console.print(paths_table) + + # 显示最近会话 + if agent.recent_sessions: + recent_table = Table(title="最近会话", show_header=True, header_style="bold") + recent_table.add_column("Key", style="cyan") + recent_table.add_column("更新时间", style="green") + recent_table.add_column("年龄(秒)", style="yellow") + + for session in agent.recent_sessions[:5]: + from datetime import datetime + updated = datetime.fromtimestamp(session["updatedAt"] / 1000).strftime("%Y-%m-%d %H:%M:%S") + recent_table.add_row(session["key"], updated, str(session["age"])) + + console.print(recent_table) + + client.close() + + +@app.command("models") +def list_models( + provider: Optional[str] = typer.Option(None, "--provider", "-p", help="筛选提供商"), + detailed: bool = typer.Option(False, "--detailed", "-d", help="显示详细信息"), +): + """列出所有可用模型 + + Args: + provider: 筛选指定提供商 + detailed: 显示详细信息 + """ + client = get_client() + + try: + models = client.get_models() + except Exception as e: + console.print(f"[red]错误: 无法连接到 Gateway[/red]") + raise typer.Exit(1) + + if not models: + console.print("[yellow]没有找到模型[/yellow]") + return + + # 筛选提供商 + if provider: + models = [m for m in models if m.provider == provider] + + if detailed: + table = Table(title="OpenClaw 模型列表 (详细)", show_header=True, header_style="bold cyan") + table.add_column("完整 ID", style="cyan") + table.add_column("别名", style="green") + table.add_column("推理", style="yellow") + table.add_column("输入类型", style="blue") + table.add_column("上下文窗口", style="magenta") + table.add_column("最大 Tokens", style="white") + table.add_column("成本 (输入/输出)", style="dim") + + for model in models: + input_types = ", ".join(model.input_types) + context = str(model.context_window) if model.context_window else "-" + max_tokens = str(model.max_tokens) if model.max_tokens else "-" + cost = f"{model.cost_input}/{model.cost_output}" + + table.add_row( + model.full_id, + model.alias or "-", + "✓" if model.reasoning else "", + input_types, + context, + max_tokens, + cost, + ) + else: + table = Table(title="OpenClaw 模型列表", show_header=True, header_style="bold cyan") + table.add_column("完整 ID", style="cyan") + table.add_column("别名", style="green") + table.add_column("推理", style="yellow") + table.add_column("输入类型", style="blue") + + for model in models: + input_types = ", ".join(model.input_types) + table.add_row( + model.full_id, + model.alias or "-", + "✓" if model.reasoning else "", + input_types, + ) + + console.print(table) + + # 显示统计 + console.print(f"\n[dim]总计: {len(models)} 个模型[/dim]") + + # 显示提供商统计 + providers = set(m.provider for m in models) + console.print(f"[dim]提供商: {', '.join(sorted(providers))}[/dim]") + + client.close() + + +@app.command("sessions") +def list_sessions( + agent_id: Optional[str] = typer.Option(None, "--agent", "-a", help="指定智能体"), + active: Optional[int] = typer.Option(None, "--active", help="只显示最近 N 分钟内活跃的会话"), + detailed: bool = typer.Option(False, "--detailed", "-d", help="显示详细信息"), +): + """列出会话 + + Args: + agent_id: 指定智能体 ID + active: 只显示最近活跃的会话(分钟) + detailed: 显示详细信息 + """ + client = get_client() + + try: + sessions = client.get_sessions(agent_id=agent_id) + except Exception as e: + console.print(f"[red]错误: 无法连接到 Gateway[/red]") + raise typer.Exit(1) + + if not sessions: + console.print("[yellow]没有找到会话[/yellow]") + return + + # 筛选活跃会话 + if active: + import time + now = time.time() * 1000 + threshold = now - active * 60 * 1000 + sessions = [s for s in sessions if s.updated_at and s.updated_at.timestamp() * 1000 >= threshold] + + if detailed: + table = Table(title="OpenClaw 会话列表 (详细)", show_header=True, header_style="bold cyan") + table.add_column("Key", style="cyan", max_width=40) + table.add_column("显示名称", style="green") + table.add_column("状态", style="yellow") + table.add_column("模型", style="blue") + table.add_column("Tokens (输入/输出)", style="magenta") + table.add_column("更新时间", style="white") + + for session in sessions: + key_short = session.key[:40] + "..." if len(session.key) > 40 else session.key + updated = session.updated_at.strftime("%Y-%m-%d %H:%M") if session.updated_at else "-" + tokens = f"{session.input_tokens}/{session.output_tokens}" + + table.add_row( + key_short, + session.display_name or "-", + session.status or "-", + session.model_provider or "-", + tokens, + updated, + ) + else: + table = Table(title="OpenClaw 会话列表", show_header=True, header_style="bold cyan") + table.add_column("Key", style="cyan", max_width=50) + table.add_column("显示名称", style="green") + table.add_column("类型", style="yellow") + table.add_column("模型", style="blue") + + for session in sessions: + key_short = session.key[:50] + "..." if len(session.key) > 50 else session.key + table.add_row( + key_short, + session.display_name or "-", + session.chat_type or session.kind or "-", + session.model_provider or "-", + ) + + console.print(table) + console.print(f"\n[dim]总计: {len(sessions)} 个会话[/dim]") + + client.close() + + +@app.command("transcript") +def show_transcript( + session_key: str, + limit: int = typer.Option(20, "--limit", "-l", help="显示消息数量限制"), +): + """显示会话对话内容 + + Args: + session_key: 会话 key + limit: 显示消息数量限制 + """ + client = get_client() + + try: + messages = client.get_session_transcript(session_key) + except Exception as e: + console.print(f"[red]错误: 无法获取对话内容[/red]") + console.print(f"[yellow]详情: {e}[/yellow]") + raise typer.Exit(1) + + if not messages: + console.print("[yellow]没有找到对话内容[/yellow]") + return + + # 显示最近的消息 + messages = messages[-limit:] + + console.print(Panel(f"[bold]会话: {session_key}[/bold]", border_style="cyan")) + + for msg in messages: + # 用户消息 + if msg.role == "user": + console.print(f"\n[bold blue]用户:[/bold blue]") + console.print(f"[blue]{msg.content[:500]}{'...' if len(msg.content) > 500 else ''}[/blue]") + + # 助手消息 + elif msg.role == "assistant": + model_str = f" ({msg.model})" if msg.model else "" + console.print(f"\n[bold green]助手{model_str}:[/bold green]") + console.print(f"[green]{msg.content[:500]}{'...' if len(msg.content) > 500 else ''}[/green]") + + # 时间戳 + if msg.timestamp: + console.print(f"[dim]{msg.timestamp.strftime('%Y-%m-%d %H:%M:%S')}[/dim]") + + console.print(f"\n[dim]显示最近 {len(messages)} 条消息[/dim]") + + client.close() + + +@app.command("workspace") +def show_workspace( + agent_id: str, + path: Optional[str] = typer.Option(None, "--path", "-p", help="查看特定文件"), + tree: bool = typer.Option(False, "--tree", "-t", help="以树形结构显示"), +): + """显示智能体工作区 + + Args: + agent_id: 挅能体 ID + path: 查看特定文件路径 + tree: 以树形结构显示 + """ + client = get_client() + + try: + workspace = client.get_workspace(agent_id) + except Exception as e: + console.print(f"[red]错误: 无法获取工作区[/red]") + raise typer.Exit(1) + + # 查看特定文件 + if path: + content = client.get_workspace_file(agent_id, path) + if content is None: + console.print(f"[red]文件不存在: {path}[/red]") + raise typer.Exit(1) + + console.print(Panel(f"[bold]{path}[/bold]", border_style="cyan")) + console.print(content) + return + + # 显示工作区信息 + info_text = f""" +[bold cyan]智能体:[/bold cyan] {workspace.agent_id} +[bold green]工作区路径:[/bold green] {workspace.workspace_path or "未设置"} +[bold yellow]智能体目录:[/bold yellow] {workspace.agent_dir or "未设置"} +[bold blue]会话存储:[/bold blue] {workspace.sessions_path or "未设置"} +[bold magenta]文件总数:[/bold magenta] {workspace.total_files} +[bold white]总大小:[/bold white] {workspace.total_size / 1024:.1f} KB +""" + console.print(Panel(info_text, title=f"[bold]{agent_id} 工作区[/bold]", border_style="cyan")) + + if not workspace.files: + console.print("[yellow]工作区为空[/yellow]") + return + + # 树形显示 + if tree: + root = Tree(f"[bold cyan]{workspace.workspace_path}[/bold cyan]") + + # 按目录组织 + dirs = {} + for file_info in workspace.files[:50]: # 限制显示数量 + parts = file_info["path"].split(os.sep) + if len(parts) > 1: + dir_name = parts[0] + if dir_name not in dirs: + dirs[dir_name] = root.add(f"[bold green]{dir_name}/[/bold green]") + dirs[dir_name].add(f"[white]{parts[-1]}[/white] ({file_info['size']} bytes)") + else: + root.add(f"[white]{file_info['name']}[/white] ({file_info['size']} bytes)") + + console.print(root) + else: + # 表格显示 + table = Table(title="工作区文件", show_header=True, header_style="bold cyan") + table.add_column("名称", style="cyan") + table.add_column("路径", style="green") + table.add_column("类型", style="yellow") + table.add_column("大小", style="blue") + + for file_info in workspace.files[:30]: # 限制显示数量 + size_kb = file_info["size"] / 1024 + table.add_row( + file_info["name"], + file_info["path"], + file_info["type"], + f"{size_kb:.1f} KB", + ) + + console.print(table) + console.print(f"\n[dim]显示 {min(30, len(workspace.files))}/{workspace.total_files} 个文件[/dim]") + + client.close() + + +@app.command("health") +def show_health(): + """显示 Gateway 健康状态""" + client = get_client() + + try: + # 直接调用 health endpoint + import requests + url = f"{client.gateway_url}/health" + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + except Exception as e: + console.print(f"[red]错误: 无法连接到 Gateway[/red]") + console.print(f"[yellow]URL: {client.gateway_url}[/yellow]") + console.print(f"[yellow]详情: {e}[/yellow]") + raise typer.Exit(1) + + # 基本信息 + console.print(Panel( + f""" +[bold cyan]Gateway 状态:[/bold cyan] {'[green]运行中[/green]' if data.get('ok') else '[red]异常[/red]'} +[bold green]URL:[/bold green] {client.gateway_url} +[bold yellow]默认智能体:[/bold yellow] {data.get('defaultAgentId', '-')} +[bold blue]心跳间隔:[/bold blue] {data.get('heartbeatSeconds', 0)} 秒 +""", + title="[bold]Gateway 健康状态[/bold]", + border_style="cyan", + )) + + # 插件状态 + plugins = data.get("plugins", {}) + if plugins.get("loaded"): + console.print(f"\n[bold green]已加载插件:[/bold green] {', '.join(plugins['loaded'])}") + if plugins.get("errors"): + console.print(f"[bold red]插件错误:[/bold red] {plugins['errors']}") + + # 通道状态 + channels = data.get("channels", {}) + if channels: + channel_table = Table(title="通道状态", show_header=True, header_style="bold") + channel_table.add_column("通道", style="cyan") + channel_table.add_column("状态", style="green") + channel_table.add_column("账号", style="yellow") + + for channel_id, channel_data in channels.items(): + status = "运行" if channel_data.get("running") else "停止" + accounts = list(channel_data.get("accounts", {}).keys()) + channel_table.add_row(channel_id, status, ", ".join(accounts[:3])) + + console.print(channel_table) + + client.close() + + +@app.command("config") +def show_config( + section: Optional[str] = typer.Option(None, "--section", "-s", help="显示特定配置部分"), +): + """显示 OpenClaw 配置 + + Args: + section: 显示特定配置部分 (如 agents, gateway, models 等) + """ + client = get_client() + + try: + config = client.get_config() + except Exception as e: + console.print(f"[red]错误: 无法获取配置[/red]") + raise typer.Exit(1) + + if section: + data = config.get(section) + if data is None: + console.print(f"[red]配置部分不存在: {section}[/red]") + raise typer.Exit(1) + + import json + console.print(Panel(json.dumps(data, indent=2, ensure_ascii=False), title=f"[bold]{section}[/bold]", border_style="cyan")) + else: + # 显示主要配置部分 + sections = ["agents", "gateway", "models", "session", "tools", "browser"] + for sec in sections: + if sec in config: + import json + data = config[sec] + # 简化显示 + summary = {} + if sec == "agents": + summary["defaults.model"] = data.get("defaults", {}).get("model", {}).get("primary") + summary["agents count"] = len(data.get("list", [])) + elif sec == "gateway": + summary["port"] = data.get("port") + summary["bind"] = data.get("bind") + summary["auth mode"] = data.get("auth", {}).get("mode") + elif sec == "models": + summary["mode"] = data.get("mode") + summary["providers count"] = len(data.get("providers", {})) + else: + summary = {k: str(v)[:50] for k, v in list(data.items())[:5]} + + console.print(Panel( + json.dumps(summary, indent=2, ensure_ascii=False), + title=f"[bold]{sec}[/bold]", + border_style="cyan", + )) + + client.close() + + +@app.command("web") +def start_web( + port: int = typer.Option(16026, "--port", "-p", help="Web 服务端口"), + host: str = typer.Option("0.0.0.0", "--host", "-h", help="Web 服务地址"), +): + """启动 Web 管理面板 + + Args: + port: Web 服务端口 + host: Web 服务地址 + """ + try: + import uvicorn + from .web.app import app as web_app + + console.print(f"[bold cyan]启动 OpenClaw Extra Web[/bold cyan]") + console.print(f"[green]地址: http://{host}:{port}[/green]") + console.print(f"[dim]按 Ctrl+C 停止[/dim]") + + uvicorn.run(web_app, host=host, port=port, log_level="info") + except ImportError: + console.print(f"[red]错误: 需要安装 FastAPI 和 uvicorn[/red]") + console.print(f"[yellow]pip install fastapi uvicorn[/yellow]") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]启动失败: {e}[/red]") + raise typer.Exit(1) + + +@app.callback() +def main( + version: bool = typer.Option(False, "--version", "-v", help="显示版本"), +): + """OpenClaw Extra - OpenClaw 管理工具""" + if version: + from . import __version__ + console.print(f"[bold cyan]openclaw-extra[/bold cyan] version {__version__}") + raise typer.Exit() + + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/src/openclaw_extra/client.py b/src/openclaw_extra/client.py new file mode 100644 index 0000000..02f6428 --- /dev/null +++ b/src/openclaw_extra/client.py @@ -0,0 +1,386 @@ +""" +OpenClaw Gateway API Client + +通过调用 openclaw CLI 命令获取数据(简单可靠的方式) +""" + +import os +import json +import re +import subprocess +from typing import Optional, List, Dict, Any + +from .models import AgentInfo, SessionInfo, ModelInfo, WorkspaceInfo, TranscriptMessage + + +class OpenClawClient: + """OpenClaw Gateway API 客户端""" + + def __init__( + self, + gateway_url: Optional[str] = None, + gateway_token: Optional[str] = None, + ): + """ + 初始化客户端 + + Args: + gateway_url: Gateway URL(用于环境变量) + gateway_token: Gateway Token(用于环境变量) + """ + self.gateway_url = gateway_url or os.environ.get("OPENCLAW_GATEWAY_URL") + self.gateway_token = gateway_token or os.environ.get("OPENCLAW_GATEWAY_TOKEN") + + # 设置环境变量用于 CLI 调用 + self._env = os.environ.copy() + if self.gateway_url: + self._env["OPENCLAW_GATEWAY_URL"] = self.gateway_url + if self.gateway_token: + self._env["OPENCLAW_GATEWAY_TOKEN"] = self.gateway_token + + def _run_cli(self, args: List[str], timeout: float = 30) -> Dict[str, Any]: + """ + 运行 openclaw CLI 命令 + + Args: + args: 命令参数 + timeout: 超时时间 + + Returns: + JSON 输出 + """ + cmd = ["openclaw"] + args + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + env=self._env, + ) + + if result.returncode != 0: + raise Exception(f"CLI error: {result.stderr}") + + # 解析 JSON 输出(跳过警告) + lines = result.stdout.strip().split("\n") + json_lines = [l for l in lines if l.startswith("{") or l.startswith("[")] + + if json_lines: + return json.loads(json_lines[-1]) + + return {} + except subprocess.TimeoutExpired: + raise Exception(f"Timeout running: {cmd}") + except json.JSONDecodeError as e: + raise Exception(f"JSON parse error: {e}") + + def get_health(self) -> Dict[str, Any]: + """ + 获取 Gateway 健康状态 + + Returns: + 健康状态数据 + """ + return self._run_cli(["gateway", "call", "health", "--json"]) + + def get_agents(self) -> List[AgentInfo]: + """ + 获取所有智能体列表 + + Returns: + 智能体信息列表 + """ + health_data = self.get_health() + config_data = self.get_config() + + agents = [] + for agent_data in health_data.get("agents", []): + agent = AgentInfo.from_dict(agent_data, config_data) + agents.append(agent) + + return agents + + def get_agent(self, agent_id: str) -> Optional[AgentInfo]: + """ + 获取单个智能体信息 + + Args: + agent_id: 智能体 ID + + Returns: + 智能体信息 + """ + agents = self.get_agents() + for agent in agents: + if agent.agent_id == agent_id: + return agent + return None + + def get_config(self) -> Dict[str, Any]: + """ + 获取 OpenClaw 配置 + + Returns: + 配置字典 + """ + try: + result = self._run_cli(["gateway", "call", "config.get", "--json"]) + return result.get("parsed", {}) + except Exception: + return self._read_config_file() + + def _read_config_file(self) -> Dict[str, Any]: + """直接读取配置文件""" + config_path = os.environ.get( + "OPENCLAW_CONFIG_PATH", + os.path.expanduser("~/.openclaw/openclaw.json") + ) + + if not os.path.exists(config_path): + return {} + + try: + with open(config_path, "r", encoding="utf-8") as f: + content = f.read() + + # 处理 JSON5 格式 + content = re.sub(r'//.*$', '', content, flags=re.MULTILINE) + content = re.sub(r',(\s*[}\]])', r'\1', content) + + return json.loads(content) + except Exception: + return {} + + def get_models(self) -> List[ModelInfo]: + """ + 获取所有可用模型列表 + + Returns: + 模型信息列表 + """ + config = self.get_config() + models = [] + + providers = config.get("models", {}).get("providers", {}) + for provider_id, provider_data in providers.items(): + for model_data in provider_data.get("models", []): + model_id = model_data.get("id", "") + if model_id: + model = ModelInfo.from_dict(provider_id, model_id, model_data) + models.append(model) + + # 从 agents.defaults.models 获取别名 + defaults_models = config.get("agents", {}).get("defaults", {}).get("models", {}) + for full_id, model_config in defaults_models.items(): + alias = model_config.get("alias") + parts = full_id.split("/") + if len(parts) == 2: + provider, model_id = parts + for model in models: + if model.provider == provider and model.model_id == model_id: + model.alias = alias + break + + return models + + def get_sessions(self, agent_id: Optional[str] = None) -> List[SessionInfo]: + """ + 获取会话列表 + + Args: + agent_id: 指定智能体 ID + + Returns: + 会话信息列表 + """ + params = {} + if agent_id: + params["agentId"] = agent_id + + result = self._run_cli([ + "gateway", "call", "sessions.list", + "--params", json.dumps(params), + "--json" + ]) + + sessions = [] + for session_data in result.get("sessions", []): + session = SessionInfo.from_dict(session_data) + sessions.append(session) + + return sessions + + def get_session_transcript(self, session_key: str) -> List[TranscriptMessage]: + """ + 获取会话对话内容 + + Args: + session_key: 会话 key + + Returns: + 对话消息列表 + """ + try: + result = self._run_cli([ + "gateway", "call", "sessions.history", + "--params", json.dumps({ + "sessionKey": session_key, + "includeTools": True, + }), + "--json" + ]) + + messages = [] + for msg_data in result.get("messages", []): + role = msg_data.get("role") + if role in ("user", "assistant"): + msg = TranscriptMessage.from_dict(msg_data) + messages.append(msg) + + return messages + except Exception: + return self._read_transcript_file(session_key) + + def _read_transcript_file(self, session_key: str) -> List[TranscriptMessage]: + """直接读取 transcript 文件""" + parts = session_key.split(":") + if len(parts) < 2: + return [] + + agent_id = parts[1] if parts[0] == "agent" else "main" + + config = self.get_config() + agents_list = config.get("agents", {}).get("list", []) + for agent_config in agents_list: + if agent_config.get("id") == agent_id: + agent_dir = agent_config.get("agentDir") + if agent_dir: + sessions_dir = os.path.dirname(agent_dir) + transcripts_dir = os.path.join(sessions_dir, "sessions", "transcripts") + if os.path.exists(transcripts_dir): + filename = session_key.replace(":", "_").replace("/", "_") + transcript_path = os.path.join(transcripts_dir, f"{filename}.jsonl") + if os.path.exists(transcript_path): + return self._parse_jsonl(transcript_path) + break + + return [] + + def _parse_jsonl(self, path: str) -> List[TranscriptMessage]: + """解析 JSONL 文件""" + messages = [] + try: + with open(path, "r", encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + msg = TranscriptMessage.from_dict(data) + messages.append(msg) + except Exception: + pass + return messages + + def get_workspace(self, agent_id: str) -> WorkspaceInfo: + """ + 获取智能体工作区信息 + + Args: + agent_id: 智能体 ID + + Returns: + 工作区信息 + """ + agent = self.get_agent(agent_id) + if not agent: + return WorkspaceInfo(agent_id=agent_id) + + workspace_path = agent.workspace + if not workspace_path: + return WorkspaceInfo(agent_id=agent_id) + + workspace_path = os.path.expanduser(workspace_path) + + if not os.path.exists(workspace_path): + return WorkspaceInfo(agent_id=agent_id, workspace_path=workspace_path) + + files = [] + for root, dirs, filenames in os.walk(workspace_path): + dirs[:] = [d for d in dirs if not d.startswith(".") + and d not in ("node_modules", "__pycache__", ".git", "dist", "build")] + + for filename in filenames: + if filename.startswith("."): + continue + + filepath = os.path.join(root, filename) + try: + size = os.path.getsize(filepath) + relpath = os.path.relpath(filepath, workspace_path) + files.append({ + "name": filename, + "path": relpath, + "size": size, + "type": self._get_file_type(filename), + }) + except Exception: + pass + + return WorkspaceInfo( + agent_id=agent_id, + workspace_path=workspace_path, + agent_dir=agent.agent_dir, + sessions_path=agent.sessions_path, + files=files, + total_files=len(files), + total_size=sum(f.get("size", 0) for f in files), + ) + + def _get_file_type(self, filename: str) -> str: + """获取文件类型""" + ext = os.path.splitext(filename)[1].lower() + type_map = { + ".py": "Python", + ".js": "JavaScript", + ".ts": "TypeScript", + ".md": "Markdown", + ".json": "JSON", + ".yaml": "YAML", + ".yml": "YAML", + ".txt": "Text", + ".html": "HTML", + ".css": "CSS", + ".sh": "Shell", + } + return type_map.get(ext, ext.lstrip(".") or "Other") + + def get_workspace_file(self, agent_id: str, filepath: str) -> Optional[str]: + """ + 读取工作区文件内容 + + Args: + agent_id: 智能体 ID + filepath: 相对文件路径 + + Returns: + 文件内容 + """ + workspace = self.get_workspace(agent_id) + if not workspace.workspace_path: + return None + + full_path = os.path.join(workspace.workspace_path, filepath) + + if not os.path.exists(full_path): + return None + + try: + with open(full_path, "r", encoding="utf-8") as f: + return f.read() + except Exception: + return None + + def close(self): + """关闭客户端""" + pass \ No newline at end of file diff --git a/src/openclaw_extra/exceptions.py b/src/openclaw_extra/exceptions.py new file mode 100644 index 0000000..266c3c9 --- /dev/null +++ b/src/openclaw_extra/exceptions.py @@ -0,0 +1,33 @@ +""" +OpenClaw Extra exceptions +""" + + +class OpenClawError(Exception): + """Base exception for OpenClaw Extra""" + pass + + +class GatewayConnectionError(OpenClawError): + """Gateway connection error""" + pass + + +class GatewayAuthError(OpenClawError): + """Gateway authentication error""" + pass + + +class AgentNotFoundError(OpenClawError): + """Agent not found""" + pass + + +class SessionNotFoundError(OpenClawError): + """Session not found""" + pass + + +class FileNotFoundError(OpenClawError): + """File not found in workspace""" + pass \ No newline at end of file diff --git a/src/openclaw_extra/models.py b/src/openclaw_extra/models.py new file mode 100644 index 0000000..295f673 --- /dev/null +++ b/src/openclaw_extra/models.py @@ -0,0 +1,199 @@ +""" +Data models for OpenClaw entities +""" + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from datetime import datetime + + +@dataclass +class AgentInfo: + """智能体信息""" + agent_id: str + name: Optional[str] = None + is_default: bool = False + workspace: Optional[str] = None + agent_dir: Optional[str] = None + heartbeat_enabled: bool = False + heartbeat_every: Optional[str] = None + sessions_count: int = 0 + sessions_path: Optional[str] = None + model: Optional[str] = None + recent_sessions: List[Dict[str, Any]] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: Dict[str, Any], config_data: Optional[Dict] = None) -> "AgentInfo": + """从 API 响应创建实例""" + heartbeat = data.get("heartbeat", {}) + + # 从 config 获取额外信息 + agent_config = {} + if config_data: + for agent in config_data.get("agents", {}).get("list", []): + if agent.get("id") == data.get("agentId"): + agent_config = agent + break + + return cls( + agent_id=data.get("agentId", ""), + name=data.get("name") or agent_config.get("name") or data.get("agentId"), + is_default=data.get("isDefault", False), + workspace=agent_config.get("workspace"), + agent_dir=agent_config.get("agentDir"), + heartbeat_enabled=heartbeat.get("enabled", False), + heartbeat_every=heartbeat.get("every"), + sessions_count=data.get("sessions", {}).get("count", 0), + sessions_path=data.get("sessions", {}).get("path"), + model=data.get("modelProvider") or (config_data.get("agents", {}).get("defaults", {}).get("model", {}).get("primary") if config_data else None), + recent_sessions=data.get("sessions", {}).get("recent", []), + ) + + +@dataclass +class SessionInfo: + """会话信息""" + key: str + kind: Optional[str] = None + display_name: Optional[str] = None + chat_type: Optional[str] = None + status: Optional[str] = None + model: Optional[str] = None + model_provider: Optional[str] = None + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + estimated_cost_usd: float = 0.0 + updated_at: Optional[datetime] = None + started_at: Optional[datetime] = None + session_id: Optional[str] = None + thinking_level: Optional[str] = None + origin: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SessionInfo": + """从 API 响应创建实例""" + updated_at = None + if data.get("updatedAt"): + updated_at = datetime.fromtimestamp(data["updatedAt"] / 1000) + + started_at = None + if data.get("startedAt"): + started_at = datetime.fromtimestamp(data["startedAt"] / 1000) + + return cls( + key=data.get("key", ""), + kind=data.get("kind"), + display_name=data.get("displayName"), + chat_type=data.get("chatType"), + status=data.get("status"), + model=data.get("model"), + model_provider=data.get("modelProvider"), + input_tokens=data.get("inputTokens", 0), + output_tokens=data.get("outputTokens", 0), + total_tokens=data.get("totalTokens", 0), + estimated_cost_usd=data.get("estimatedCostUsd", 0.0), + updated_at=updated_at, + started_at=started_at, + session_id=data.get("sessionId"), + thinking_level=data.get("thinkingLevel"), + origin=data.get("origin", {}), + ) + + +@dataclass +class ModelInfo: + """模型信息""" + provider: str + model_id: str + full_id: str # provider/model_id + name: Optional[str] = None + alias: Optional[str] = None + reasoning: bool = False + input_types: List[str] = field(default_factory=list) + context_window: Optional[int] = None + max_tokens: Optional[int] = None + cost_input: float = 0.0 + cost_output: float = 0.0 + + @classmethod + def from_dict(cls, provider: str, model_id: str, data: Dict[str, Any]) -> "ModelInfo": + """从 API 响应创建实例""" + cost = data.get("cost", {}) + return cls( + provider=provider, + model_id=model_id, + full_id=f"{provider}/{model_id}", + name=data.get("name") or model_id, + alias=data.get("alias"), + reasoning=data.get("reasoning", False), + input_types=data.get("input", ["text"]), + context_window=data.get("contextWindow"), + max_tokens=data.get("maxTokens"), + cost_input=cost.get("input", 0.0), + cost_output=cost.get("output", 0.0), + ) + + +@dataclass +class WorkspaceInfo: + """工作区信息""" + agent_id: str + workspace_path: Optional[str] = None + agent_dir: Optional[str] = None + sessions_path: Optional[str] = None + files: List[Dict[str, Any]] = field(default_factory=list) + total_files: int = 0 + total_size: int = 0 + + @classmethod + def from_dict(cls, agent_id: str, data: Dict[str, Any], files: List[Dict[str, Any]] = None) -> "WorkspaceInfo": + """创建实例""" + return cls( + agent_id=agent_id, + workspace_path=data.get("workspace"), + agent_dir=data.get("agentDir"), + sessions_path=data.get("sessions_path"), + files=files or [], + total_files=len(files or []), + total_size=sum(f.get("size", 0) for f in files or []), + ) + + +@dataclass +class TranscriptMessage: + """对话消息""" + role: str # user, assistant, system + content: str + timestamp: Optional[datetime] = None + model: Optional[str] = None + tokens: Optional[int] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TranscriptMessage": + """从 API 响应创建实例""" + timestamp = None + if data.get("ts"): + timestamp = datetime.fromtimestamp(data["ts"] / 1000) + + # 处理 content 可能是数组的情况 + content = data.get("content", "") + if isinstance(content, list): + content_parts = [] + for part in content: + if isinstance(part, dict): + if part.get("type") == "text": + content_parts.append(part.get("text", "")) + elif part.get("type") == "image": + content_parts.append("[图片]") + else: + content_parts.append(str(part)) + content = "\n".join(content_parts) + + return cls( + role=data.get("role", ""), + content=content, + timestamp=timestamp, + model=data.get("model"), + tokens=data.get("tokens"), + ) \ No newline at end of file diff --git a/src/openclaw_extra/web/__init__.py b/src/openclaw_extra/web/__init__.py new file mode 100644 index 0000000..6c02143 --- /dev/null +++ b/src/openclaw_extra/web/__init__.py @@ -0,0 +1 @@ +# Web module for openclaw-extra \ No newline at end of file diff --git a/src/openclaw_extra/web/app.py b/src/openclaw_extra/web/app.py new file mode 100644 index 0000000..dd3ae41 --- /dev/null +++ b/src/openclaw_extra/web/app.py @@ -0,0 +1,259 @@ +""" +FastAPI Web Application for OpenClaw Extra +""" + +from fastapi import FastAPI, HTTPException, Query +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +from fastapi.middleware.cors import CORSMiddleware +from pathlib import Path +from typing import Optional + +from ..client import OpenClawClient + +# 创建 FastAPI 应用 +app = FastAPI( + title="OpenClaw Extra", + description="OpenClaw 管理面板", + version="0.1.0", +) + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 静态文件目录 +STATIC_DIR = Path(__file__).parent / "static" +STATIC_DIR.mkdir(exist_ok=True) + + +# 全局客户端实例 +_client: Optional[OpenClawClient] = None + + +def get_client() -> OpenClawClient: + """获取 OpenClaw 客户端""" + global _client + if _client is None: + _client = OpenClawClient() + return _client + + +# ============== API 路由 ============== + +@app.get("/api/health") +async def api_health(): + """健康检查""" + try: + client = get_client() + health = client.get_health() + return {"status": "ok", "gateway": health} + except Exception as e: + return {"status": "error", "message": str(e)} + + +@app.get("/api/agents") +async def api_agents(): + """获取所有智能体""" + try: + client = get_client() + agents = client.get_agents() + return { + "agents": [ + { + "agent_id": a.agent_id, + "name": a.name, + "is_default": a.is_default, + "workspace": a.workspace, + "heartbeat_enabled": a.heartbeat_enabled, + "heartbeat_every": a.heartbeat_every, + "sessions_count": a.sessions_count, + "model": a.model, + } + for a in agents + ] + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/agents/{agent_id}") +async def api_agent(agent_id: str): + """获取单个智能体详情""" + try: + client = get_client() + agent = client.get_agent(agent_id) + if not agent: + raise HTTPException(status_code=404, detail="Agent not found") + + return { + "agent_id": agent.agent_id, + "name": agent.name, + "is_default": agent.is_default, + "workspace": agent.workspace, + "agent_dir": agent.agent_dir, + "heartbeat_enabled": agent.heartbeat_enabled, + "heartbeat_every": agent.heartbeat_every, + "sessions_count": agent.sessions_count, + "sessions_path": agent.sessions_path, + "model": agent.model, + "recent_sessions": agent.recent_sessions, + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/models") +async def api_models(): + """获取所有模型""" + try: + client = get_client() + models = client.get_models() + return { + "models": [ + { + "provider": m.provider, + "model_id": m.model_id, + "full_id": m.full_id, + "name": m.name, + "alias": m.alias, + "reasoning": m.reasoning, + "input_types": m.input_types, + "context_window": m.context_window, + "max_tokens": m.max_tokens, + "cost_input": m.cost_input, + "cost_output": m.cost_output, + } + for m in models + ] + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/sessions") +async def api_sessions(agent_id: Optional[str] = Query(None)): + """获取会话列表""" + try: + client = get_client() + sessions = client.get_sessions(agent_id) + return { + "sessions": [ + { + "key": s.key, + "kind": s.kind, + "display_name": s.display_name, + "chat_type": s.chat_type, + "status": s.status, + "model": s.model, + "model_provider": s.model_provider, + "input_tokens": s.input_tokens, + "output_tokens": s.output_tokens, + "total_tokens": s.total_tokens, + "estimated_cost_usd": s.estimated_cost_usd, + "updated_at": s.updated_at.isoformat() if s.updated_at else None, + "started_at": s.started_at.isoformat() if s.started_at else None, + "thinking_level": s.thinking_level, + } + for s in sessions + ] + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/sessions/{session_key}/transcript") +async def api_session_transcript(session_key: str): + """获取会话对话内容""" + try: + client = get_client() + messages = client.get_session_transcript(session_key) + return { + "messages": [ + { + "role": m.role, + "content": m.content, + "timestamp": m.timestamp.isoformat() if m.timestamp else None, + "model": m.model, + "tokens": m.tokens, + } + for m in messages + ] + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/workspaces/{agent_id}") +async def api_workspace(agent_id: str): + """获取工作区信息""" + try: + client = get_client() + workspace = client.get_workspace(agent_id) + return { + "agent_id": workspace.agent_id, + "workspace_path": workspace.workspace_path, + "agent_dir": workspace.agent_dir, + "sessions_path": workspace.sessions_path, + "files": workspace.files, + "total_files": workspace.total_files, + "total_size": workspace.total_size, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/workspaces/{agent_id}/files/{filepath:path}") +async def api_workspace_file(agent_id: str, filepath: str): + """读取工作区文件内容""" + try: + client = get_client() + content = client.get_workspace_file(agent_id, filepath) + if content is None: + raise HTTPException(status_code=404, detail="File not found") + return {"content": content} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ============== 页面路由 ============== + +@app.get("/", response_class=HTMLResponse) +async def page_index(): + """主页 - 智能体列表""" + html = (STATIC_DIR / "index.html").read_text(encoding="utf-8") + return HTMLResponse(content=html) + + +@app.get("/agents/{agent_id}", response_class=HTMLResponse) +async def page_agent(agent_id: str): + """智能体详情页""" + html = (STATIC_DIR / "agent.html").read_text(encoding="utf-8") + return HTMLResponse(content=html) + + +@app.get("/sessions/{session_key:path}", response_class=HTMLResponse) +async def page_session(session_key: str): + """会话详情页""" + html = (STATIC_DIR / "session.html").read_text(encoding="utf-8") + return HTMLResponse(content=html) + + +@app.get("/models", response_class=HTMLResponse) +async def page_models(): + """模型列表页""" + html = (STATIC_DIR / "models.html").read_text(encoding="utf-8") + return HTMLResponse(content=html) + + +# 挂载静态文件 +app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") \ No newline at end of file diff --git a/src/openclaw_extra/web/static/agent.html b/src/openclaw_extra/web/static/agent.html new file mode 100644 index 0000000..7214bd6 --- /dev/null +++ b/src/openclaw_extra/web/static/agent.html @@ -0,0 +1,263 @@ + + + + + + 智能体详情 - OpenClaw Extra + + + + + +
+ +
+ +
+
+ +
+
+

+
+
+
+
+ + +
+ + +
+ + +
+ + + +
+ + +
+
+
+

会话列表

+ +
+
+ +
+ 暂无会话 +
+
+
+
+ + +
+
+
+

工作区文件

+ +
+
+ +
+ 工作区为空或无法访问 +
+
+
+
+ + +
+
+

智能体信息

+
+
+
+
Agent ID
+
+
+
+
名称
+
+
+
+
模型
+
+
+
+
默认智能体
+
+
+
+
+
+
工作区路径
+
+
+
+
心跳
+
+
+
+
会话数量
+
+
+
+
+
+
+ + +
+
+
+
加载中...
+
+
+
+ + + + \ No newline at end of file diff --git a/src/openclaw_extra/web/static/index.html b/src/openclaw_extra/web/static/index.html new file mode 100644 index 0000000..930ff15 --- /dev/null +++ b/src/openclaw_extra/web/static/index.html @@ -0,0 +1,142 @@ + + + + + + OpenClaw Extra - 智能体管理 + + + + + +
+ +
+
+
+

🤖 OpenClaw Extra

+

智能体管理面板

+
+ +
+
+ + +
+ + +
+ + +
+
+
智能体总数
+
+
+
+
活跃会话
+
+
+
+
Gateway 状态
+
+ ✓ 在线 + ✗ 离线 +
+
+
+ + +
+
+

智能体列表

+
+
+ +
+ 暂无智能体 +
+
+
+ + +
+
+
+
加载中...
+
+
+
+ + + + \ No newline at end of file diff --git a/src/openclaw_extra/web/static/models.html b/src/openclaw_extra/web/static/models.html new file mode 100644 index 0000000..deeecfd --- /dev/null +++ b/src/openclaw_extra/web/static/models.html @@ -0,0 +1,170 @@ + + + + + + 模型列表 - OpenClaw Extra + + + + + +
+ +
+ +

📊 模型列表

+

OpenClaw 可用的大模型

+
+ + +
+
+
模型总数
+
+
+
+
支持推理
+
+
+
+
提供商
+
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + +
模型提供商别名上下文推理输入价格输出价格
+
+
+ 暂无模型 +
+
+ + +
+
+
+
加载中...
+
+
+
+ + + + \ No newline at end of file diff --git a/src/openclaw_extra/web/static/session.html b/src/openclaw_extra/web/static/session.html new file mode 100644 index 0000000..59a5cf1 --- /dev/null +++ b/src/openclaw_extra/web/static/session.html @@ -0,0 +1,103 @@ + + + + + + 会话详情 - OpenClaw Extra + + + + + +
+ +
+ ← 返回智能体列表 +

+
+ + +
+ + +
+ + +
+
+

对话内容

+ +
+
+ +
+ 暂无消息 +
+
+
+ + +
+
+
+
加载中...
+
+
+
+ + + + \ No newline at end of file