feat: 重命名项目为Boardware Genius并添加运行时环境同步功能

- 将项目品牌从nanobot重命名为Boardware Genius,更新所有相关文档、注释和日志输出
- 在web服务器中添加运行时环境变量同步功能,支持授权和后端身份配置
- 更新create-instance脚本以生成运行时环境文件
- 添加实例后端绑定功能到部署控制服务
- 修改入口脚本以加载运行时环境变量
- 更新前端和认证门户的相关描述文本
This commit is contained in:
2026-03-18 15:45:42 +08:00
parent b6dd0c1623
commit 4e45f8b717
36 changed files with 315 additions and 76 deletions

View File

@ -271,7 +271,7 @@ docker run -d \
- 注册接口超时 - 注册接口超时
- `app-instance` 容器反复重启 - `app-instance` 容器反复重启
- 日志里出现 `Missing nanobot config: /root/.nanobot/config.json` - 日志里出现 `Missing Boardware Genius config: /root/.nanobot/config.json`
当前版本里,新实例的默认大模型配置就是从这里分发的: 当前版本里,新实例的默认大模型配置就是从这里分发的:

View File

@ -1,6 +1,6 @@
# nanobot-backend # Boardware Genius Backend
基于 `nanobot` 的后端服务仓库当前重点不是上游通用介绍,而是这套实际可运行的后端能力 这是 `Boardware Genius` 的后端服务仓库当前技术命令和包名仍沿用 `nanobot`,但产品品牌按 `Boardware Genius` 表述
- `nanobot web`:单用户 FastAPI 后端,供独立前端或 `/docs` 调试使用 - `nanobot web`:单用户 FastAPI 后端,供独立前端或 `/docs` 调试使用
- `nanobot gateway`:常驻 worker负责渠道接入、cron、heartbeat - `nanobot gateway`:常驻 worker负责渠道接入、cron、heartbeat
@ -183,7 +183,7 @@ pip install -e .
bw-outlook-mcp --help bw-outlook-mcp --help
``` ```
这样 nanobot 就会直接用 PATH 里的 `bw-outlook-mcp`,不依赖额外挂载路径。 这样 Boardware Genius 就会直接用 PATH 里的 `bw-outlook-mcp`,不依赖额外挂载路径。
#### 方案 B把 `BW_Outlook_Mcp` 作为外部目录挂进来 #### 方案 B把 `BW_Outlook_Mcp` 作为外部目录挂进来
@ -204,7 +204,7 @@ python3 -m venv .venv
pip install -e . pip install -e .
``` ```
然后给 nanobot 设置: 然后给 Boardware Genius 设置:
```bash ```bash
export NANOBOT_OUTLOOK_MCP_ROOT=/srv/BW_Outlook_Mcp export NANOBOT_OUTLOOK_MCP_ROOT=/srv/BW_Outlook_Mcp

View File

@ -2,7 +2,7 @@
## Reporting a Vulnerability ## Reporting a Vulnerability
If you discover a security vulnerability in nanobot, please report it by: If you discover a security vulnerability in Boardware Genius, please report it by:
1. **DO NOT** open a public GitHub issue 1. **DO NOT** open a public GitHub issue
2. Create a private security advisory on GitHub or contact the repository maintainers (xubinrencs@gmail.com) 2. Create a private security advisory on GitHub or contact the repository maintainers (xubinrencs@gmail.com)
@ -67,7 +67,7 @@ The `exec` tool can execute shell commands. While dangerous command patterns are
- ✅ Review all tool usage in agent logs - ✅ Review all tool usage in agent logs
- ✅ Understand what commands the agent is running - ✅ Understand what commands the agent is running
- ✅ Use a dedicated user account with limited privileges - ✅ Use a dedicated user account with limited privileges
- ✅ Never run nanobot as root - ✅ Never run Boardware Genius as root
- ❌ Don't disable security checks - ❌ Don't disable security checks
- ❌ Don't run on systems with sensitive data without careful review - ❌ Don't run on systems with sensitive data without careful review
@ -82,7 +82,7 @@ The `exec` tool can execute shell commands. While dangerous command patterns are
File operations have path traversal protection, but: File operations have path traversal protection, but:
- ✅ Run nanobot with a dedicated user account - ✅ Run Boardware Genius with a dedicated user account
- ✅ Use filesystem permissions to protect sensitive directories - ✅ Use filesystem permissions to protect sensitive directories
- ✅ Regularly audit file operations in logs - ✅ Regularly audit file operations in logs
- ❌ Don't give unrestricted access to sensitive files - ❌ Don't give unrestricted access to sensitive files
@ -123,7 +123,7 @@ npm audit fix
- Keep `litellm` updated to the latest version for security fixes - Keep `litellm` updated to the latest version for security fixes
- We've updated `ws` to `>=8.17.1` to fix DoS vulnerability - We've updated `ws` to `>=8.17.1` to fix DoS vulnerability
- Run `pip-audit` or `npm audit` regularly - Run `pip-audit` or `npm audit` regularly
- Subscribe to security advisories for nanobot and its dependencies - Subscribe to security advisories for Boardware Genius and its dependencies
### 7. Production Deployment ### 7. Production Deployment
@ -238,7 +238,7 @@ If you suspect a security breach:
## Security Checklist ## Security Checklist
Before deploying nanobot: Before deploying Boardware Genius:
- [ ] API keys stored securely (not in code) - [ ] API keys stored securely (not in code)
- [ ] Config file permissions set to 0600 - [ ] Config file permissions set to 0600

View File

@ -1,7 +1,7 @@
{ {
"name": "nanobot-whatsapp-bridge", "name": "nanobot-whatsapp-bridge",
"version": "0.1.0", "version": "0.1.0",
"description": "WhatsApp bridge for nanobot using Baileys", "description": "WhatsApp bridge for Boardware Genius using Baileys",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* nanobot WhatsApp Bridge * Boardware Genius WhatsApp Bridge
* *
* This bridge connects WhatsApp Web to nanobot's Python backend * This bridge connects WhatsApp Web to the Boardware Genius Python backend
* via WebSocket. It handles authentication, message forwarding, * via WebSocket. It handles authentication, message forwarding,
* and reconnection logic. * and reconnection logic.
* *
@ -27,7 +27,7 @@ const PORT = parseInt(process.env.BRIDGE_PORT || '3001', 10);
const AUTH_DIR = process.env.AUTH_DIR || join(homedir(), '.nanobot', 'whatsapp-auth'); const AUTH_DIR = process.env.AUTH_DIR || join(homedir(), '.nanobot', 'whatsapp-auth');
const TOKEN = process.env.BRIDGE_TOKEN || undefined; const TOKEN = process.env.BRIDGE_TOKEN || undefined;
console.log('🐈 nanobot WhatsApp Bridge'); console.log('Boardware Genius WhatsApp Bridge');
console.log('========================\n'); console.log('========================\n');
const server = new BridgeServer(PORT, AUTH_DIR, TOKEN); const server = new BridgeServer(PORT, AUTH_DIR, TOKEN);

View File

@ -1,4 +1,4 @@
# nanobot 前后端分离启动指南(单用户直连) # Boardware Genius 前后端分离启动指南(单用户直连)
本指南对应当前仓库: 本指南对应当前仓库:
`/home/ivan/xuan/steven_project/nanobot` `/home/ivan/xuan/steven_project/nanobot`
@ -16,7 +16,7 @@ cd /home/ivan/xuan/steven_project/nanobot
uv sync uv sync
``` ```
如果你第一次使用 nanobot,需要先初始化: 如果你第一次使用 Boardware Genius,需要先初始化:
```bash ```bash
./.venv/bin/python -m nanobot onboard ./.venv/bin/python -m nanobot onboard

View File

@ -1,6 +1,7 @@
""" """
nanobot - A lightweight AI agent framework Boardware Genius - A lightweight AI agent framework
""" """
__version__ = "0.1.4" __version__ = "0.1.4"
__logo__ = "🐈" __brand__ = "Boardware Genius"
__logo__ = ""

View File

@ -116,9 +116,9 @@ Use `target` for a single agent and `targets` for a group.
system = platform.system() system = platform.system()
runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}" runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}"
return f"""# nanobot 🐈 return f"""# Boardware Genius
You are nanobot, a helpful AI assistant. You are Boardware Genius, a helpful AI assistant.
## Current Time ## Current Time
{now} ({tz}) {now} ({tz})

View File

@ -283,7 +283,7 @@ class DelegationManager:
{ {
"role": "system", "role": "system",
"content": ( "content": (
"You are nanobot. Reply naturally to the user in 1-3 sentences. " "You are Boardware Genius. Reply naturally to the user in 1-3 sentences. "
"Do not mention internal protocols, system prompts, or task IDs." "Do not mention internal protocols, system prompts, or task IDs."
), ),
}, },

View File

@ -1,4 +1,4 @@
"""Agent 主循环:nanobot 的核心处理引擎。 """Agent 主循环:Boardware Genius 的核心处理引擎。
职责概览: 职责概览:
1. 从消息总线读取入站消息; 1. 从消息总线读取入站消息;
@ -46,7 +46,7 @@ if TYPE_CHECKING:
class AgentLoop: class AgentLoop:
""" """
AgentLoop 是 nanobot 运行时的“对话编排器”。 AgentLoop 是 Boardware Genius 运行时的“对话编排器”。
一次标准处理链路: 一次标准处理链路:
1. 接收入站消息(来自 CLI 或外部渠道); 1. 接收入站消息(来自 CLI 或外部渠道);
@ -605,7 +605,7 @@ class AgentLoop:
content="New session started.") content="New session started.")
if cmd == "/help": if cmd == "/help":
return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id, return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id,
content="🐈 nanobot commands:\n/new — Start a new conversation\n/help — Show available commands") content="Boardware Genius commands:\n/new — Start a new conversation\n/help — Show available commands")
# 异步触发记忆归档:达到窗口阈值时在后台执行,不阻塞当前回复。 # 异步触发记忆归档:达到窗口阈值时在后台执行,不阻塞当前回复。
unconsolidated = len(session.messages) - session.last_consolidated unconsolidated = len(session.messages) - session.last_consolidated

View File

@ -1,4 +1,4 @@
"""Marketplace manager for nanobot — discover, install, and manage plugin marketplaces.""" """Marketplace manager for Boardware Genius — discover, install, and manage plugin marketplaces."""
from __future__ import annotations from __future__ import annotations

View File

@ -1,4 +1,4 @@
"""Plugin system for nanobot - load agents, commands, and skills from plugin directories.""" """Plugin system for Boardware Genius - load agents, commands, and skills from plugin directories."""
from __future__ import annotations from __future__ import annotations

View File

@ -122,7 +122,7 @@ class EmailChannel(BaseChannel):
logger.warning("Email channel missing recipient address") logger.warning("Email channel missing recipient address")
return return
base_subject = self._last_subject_by_chat.get(to_addr, "nanobot reply") base_subject = self._last_subject_by_chat.get(to_addr, "Boardware Genius reply")
subject = self._reply_subject(base_subject) subject = self._reply_subject(base_subject)
if msg.metadata and isinstance(msg.metadata.get("subject"), str): if msg.metadata and isinstance(msg.metadata.get("subject"), str):
override = msg.metadata["subject"].strip() override = msg.metadata["subject"].strip()
@ -397,7 +397,7 @@ class EmailChannel(BaseChannel):
return html.unescape(text) return html.unescape(text)
def _reply_subject(self, base_subject: str) -> str: def _reply_subject(self, base_subject: str) -> str:
subject = (base_subject or "").strip() or "nanobot reply" subject = (base_subject or "").strip() or "Boardware Genius reply"
prefix = self.config.subject_prefix or "Re: " prefix = self.config.subject_prefix or "Re: "
if subject.lower().startswith("re:"): if subject.lower().startswith("re:"):
return subject return subject

View File

@ -287,7 +287,7 @@ class TelegramChannel(BaseChannel):
user = update.effective_user user = update.effective_user
await update.message.reply_text( await update.message.reply_text(
f"👋 Hi {user.first_name}! I'm nanobot.\n\n" f"👋 Hi {user.first_name}! I'm Boardware Genius.\n\n"
"Send me a message and I'll respond!\n" "Send me a message and I'll respond!\n"
"Type /help to see available commands." "Type /help to see available commands."
) )
@ -297,7 +297,7 @@ class TelegramChannel(BaseChannel):
if not update.message: if not update.message:
return return
await update.message.reply_text( await update.message.reply_text(
"🐈 nanobot commands:\n" "Boardware Genius commands:\n"
"/new — Start a new conversation\n" "/new — Start a new conversation\n"
"/help — Show available commands" "/help — Show available commands"
) )

View File

@ -1 +1 @@
"""CLI module for nanobot.""" """CLI module for Boardware Genius."""

View File

@ -1,4 +1,4 @@
"""nanobot 命令行入口。 """Boardware Genius 命令行入口。
本文件职责: 本文件职责:
1. 定义所有 CLI 命令onboard / agent / gateway / cron / channels / provider 1. 定义所有 CLI 命令onboard / agent / gateway / cron / channels / provider
@ -29,12 +29,12 @@ from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from nanobot import __version__, __logo__ from nanobot import __brand__, __version__
from nanobot.config.schema import Config from nanobot.config.schema import Config
app = typer.Typer( app = typer.Typer(
name="nanobot", name="nanobot",
help=f"{__logo__} nanobot - Personal AI Assistant", help=f"{__brand__} - Personal AI Assistant",
no_args_is_help=True, no_args_is_help=True,
) )
@ -122,7 +122,7 @@ def _print_agent_response(response: str, render_markdown: bool) -> None:
content = response or "" content = response or ""
body = Markdown(content) if render_markdown else Text(content) body = Markdown(content) if render_markdown else Text(content)
console.print() console.print()
console.print(f"[cyan]{__logo__} nanobot[/cyan]") console.print(f"[cyan]{__brand__}[/cyan]")
console.print(body) console.print(body)
console.print() console.print()
@ -158,7 +158,7 @@ async def _read_interactive_input_async() -> str:
def version_callback(value: bool): def version_callback(value: bool):
"""处理 --version/-v 选项并立即退出。""" """处理 --version/-v 选项并立即退出。"""
if value: if value:
console.print(f"{__logo__} nanobot v{__version__}") console.print(f"{__brand__} v{__version__}")
raise typer.Exit() raise typer.Exit()
@ -168,7 +168,7 @@ def main(
None, "--version", "-v", callback=version_callback, is_eager=True None, "--version", "-v", callback=version_callback, is_eager=True
), ),
): ):
"""nanobot - Personal AI Assistant.""" """Boardware Genius - Personal AI Assistant."""
pass pass
@ -179,7 +179,7 @@ def main(
@app.command() @app.command()
def onboard(): def onboard():
"""Initialize nanobot configuration and workspace.""" """Initialize Boardware Genius configuration and workspace."""
from nanobot.config.loader import get_config_path, load_config, save_config from nanobot.config.loader import get_config_path, load_config, save_config
from nanobot.config.schema import Config from nanobot.config.schema import Config
from nanobot.utils.helpers import get_workspace_path from nanobot.utils.helpers import get_workspace_path
@ -225,11 +225,11 @@ def onboard():
_create_workspace_templates(workspace) _create_workspace_templates(workspace)
# 第 5 步:输出下一步操作提示,指导用户继续配置 API Key 并开始对话。 # 第 5 步:输出下一步操作提示,指导用户继续配置 API Key 并开始对话。
console.print(f"\n{__logo__} nanobot is ready!") console.print(f"\n{__brand__} is ready!")
console.print("\nNext steps:") console.print("\nNext steps:")
console.print(" 1. Add your API key to [cyan]~/.nanobot/config.json[/cyan]") console.print(" 1. Add your API key to [cyan]~/.nanobot/config.json[/cyan]")
console.print(" Get one at: https://openrouter.ai/keys") console.print(" Get one at: https://openrouter.ai/keys")
console.print(" 2. Chat: [cyan]nanobot agent -m \"Hello!\"[/cyan]") console.print(" 2. Chat with Boardware Genius: [cyan]nanobot agent -m \"Hello!\"[/cyan]")
console.print("\n[dim]Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot#-chat-apps[/dim]") console.print("\n[dim]Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot#-chat-apps[/dim]")
@ -324,7 +324,7 @@ def gateway(
port: int = typer.Option(18790, "--port", "-p", help="Gateway port"), port: int = typer.Option(18790, "--port", "-p", help="Gateway port"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
): ):
"""启动 nanobot 网关常驻服务。 """启动 Boardware Genius 网关常驻服务。
这是“生产运行入口”之一,主要职责: 这是“生产运行入口”之一,主要职责:
1. 初始化配置、总线、模型提供方、会话管理、Agent 主循环; 1. 初始化配置、总线、模型提供方、会话管理、Agent 主循环;
@ -352,7 +352,7 @@ def gateway(
import logging import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
console.print(f"{__logo__} Starting nanobot gateway on port {port}...") console.print(f"{__brand__}: starting gateway on port {port}...")
# 运行时核心对象初始化顺序: # 运行时核心对象初始化顺序:
# config -> bus -> provider -> sessions -> cron -> agent -> channels -> heartbeat # config -> bus -> provider -> sessions -> cron -> agent -> channels -> heartbeat
@ -525,7 +525,7 @@ def web(
config = load_config() config = load_config()
_create_workspace_templates(config.workspace_path) _create_workspace_templates(config.workspace_path)
console.print(f"{__logo__} Starting nanobot web backend on {host}:{port}...") console.print(f"{__brand__}: starting web backend on {host}:{port}...")
web_app = create_app(config=config) web_app = create_app(config=config)
uvicorn.run(web_app, host=host, port=port) uvicorn.run(web_app, host=host, port=port)
@ -541,7 +541,7 @@ def agent(
message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"), message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
session_id: str = typer.Option("cli:direct", "--session", "-s", help="Session ID"), session_id: str = typer.Option("cli:direct", "--session", "-s", help="Session ID"),
markdown: bool = typer.Option(True, "--markdown/--no-markdown", help="Render assistant output as Markdown"), markdown: bool = typer.Option(True, "--markdown/--no-markdown", help="Render assistant output as Markdown"),
logs: bool = typer.Option(False, "--logs/--no-logs", help="Show nanobot runtime logs during chat"), logs: bool = typer.Option(False, "--logs/--no-logs", help="Show Boardware Genius runtime logs during chat"),
): ):
"""直接与 agent 交互(单轮模式或交互模式)。 """直接与 agent 交互(单轮模式或交互模式)。
@ -614,7 +614,7 @@ def agent(
# 空上下文:进入/退出都不做事,仅用于统一 with 接口。 # 空上下文:进入/退出都不做事,仅用于统一 with 接口。
return nullcontext() return nullcontext()
# 非日志模式下启用转圈动画,提升等待期间的交互感知。 # 非日志模式下启用转圈动画,提升等待期间的交互感知。
return console.status("[dim]nanobot is thinking...[/dim]", spinner="dots") return console.status(f"[dim]{__brand__} is thinking...[/dim]", spinner="dots")
async def _cli_progress(content: str, *, tool_hint: bool = False) -> None: async def _cli_progress(content: str, *, tool_hint: bool = False) -> None:
"""CLI 进度回调:按 channels 配置过滤后渲染中间态输出。""" """CLI 进度回调:按 channels 配置过滤后渲染中间态输出。"""
@ -643,7 +643,7 @@ def agent(
# 初始化 prompt_toolkit 会话(历史记录、编辑能力、粘贴兼容等)。 # 初始化 prompt_toolkit 会话(历史记录、编辑能力、粘贴兼容等)。
_init_prompt_session() _init_prompt_session()
# 打印一次交互模式提示,告知退出方式。 # 打印一次交互模式提示,告知退出方式。
console.print(f"{__logo__} Interactive mode (type [bold]exit[/bold] or [bold]Ctrl+C[/bold] to quit)\n") console.print(f"{__brand__} interactive mode (type [bold]exit[/bold] or [bold]Ctrl+C[/bold] to quit)\n")
# session_id 解析规则: # session_id 解析规则:
# 1) 传入 "channel:chat_id" 时,显式使用对应渠道与会话; # 1) 传入 "channel:chat_id" 时,显式使用对应渠道与会话;
@ -945,7 +945,7 @@ def _get_bridge_dir() -> Path:
console.print("Try reinstalling: pip install --force-reinstall nanobot") console.print("Try reinstalling: pip install --force-reinstall nanobot")
raise typer.Exit(1) raise typer.Exit(1)
console.print(f"{__logo__} Setting up bridge...") console.print(f"{__brand__}: setting up bridge...")
# 重新复制并构建,确保 bridge 资源与当前版本同步。 # 重新复制并构建,确保 bridge 资源与当前版本同步。
user_bridge.parent.mkdir(parents=True, exist_ok=True) user_bridge.parent.mkdir(parents=True, exist_ok=True)
@ -980,7 +980,7 @@ def channels_login():
config = load_config() config = load_config()
bridge_dir = _get_bridge_dir() bridge_dir = _get_bridge_dir()
console.print(f"{__logo__} Starting bridge...") console.print(f"{__brand__}: starting bridge...")
console.print("Scan the QR code to connect.\n") console.print("Scan the QR code to connect.\n")
# 可选注入 BRIDGE_TOKEN 做 bridge 鉴权。 # 可选注入 BRIDGE_TOKEN 做 bridge 鉴权。
@ -1259,14 +1259,14 @@ def cron_run(
@app.command() @app.command()
def status(): def status():
"""展示 nanobot 运行配置与 provider 状态概览。""" """展示 Boardware Genius 运行配置与 provider 状态概览。"""
from nanobot.config.loader import load_config, get_config_path from nanobot.config.loader import load_config, get_config_path
config_path = get_config_path() config_path = get_config_path()
config = load_config() config = load_config()
workspace = config.workspace_path workspace = config.workspace_path
console.print(f"{__logo__} nanobot Status\n") console.print(f"{__brand__} Status\n")
console.print(f"Config: {config_path} {'[green]✓[/green]' if config_path.exists() else '[red]✗[/red]'}") console.print(f"Config: {config_path} {'[green]✓[/green]' if config_path.exists() else '[red]✗[/red]'}")
console.print(f"Workspace: {workspace} {'[green]✓[/green]' if workspace.exists() else '[red]✗[/red]'}") console.print(f"Workspace: {workspace} {'[green]✓[/green]' if workspace.exists() else '[red]✗[/red]'}")
@ -1346,7 +1346,7 @@ def provider_login(
console.print(f"[red]Login not implemented for {spec.label}[/red]") console.print(f"[red]Login not implemented for {spec.label}[/red]")
raise typer.Exit(1) raise typer.Exit(1)
console.print(f"{__logo__} OAuth Login - {spec.label}\n") console.print(f"{__brand__} OAuth Login - {spec.label}\n")
handler() handler()

View File

@ -1,4 +1,4 @@
"""Configuration module for nanobot.""" """Configuration module for Boardware Genius."""
from nanobot.config.loader import load_config, get_config_path from nanobot.config.loader import load_config, get_config_path
from nanobot.config.schema import Config from nanobot.config.schema import Config

View File

@ -1,6 +1,6 @@
# Heartbeat Tasks # Heartbeat Tasks
This file is checked every 30 minutes by your nanobot agent. This file is checked every 30 minutes by your Boardware Genius agent.
Add tasks below that you want the agent to work on periodically. Add tasks below that you want the agent to work on periodically.
If this file has no tasks (only headers and comments), the agent will skip the heartbeat. If this file has no tasks (only headers and comments), the agent will skip the heartbeat.
@ -13,4 +13,3 @@ If this file has no tasks (only headers and comments), the agent will skip the h
## Completed ## Completed
<!-- Move completed tasks here or delete them --> <!-- Move completed tasks here or delete them -->

View File

@ -1,6 +1,6 @@
# Soul # Soul
I am nanobot 🐈, a personal AI assistant. I am Boardware Genius, a personal AI assistant.
## Personality ## Personality

View File

@ -46,4 +46,4 @@ Information about the user to help personalize interactions.
--- ---
*Edit this file to customize nanobot's behavior for your needs.* *Edit this file to customize Boardware Genius behavior for your needs.*

View File

@ -20,4 +20,4 @@ This file stores important information that should persist across sessions.
--- ---
*This file is automatically updated by nanobot when important information should be remembered.* *This file is automatically updated by Boardware Genius when important information should be remembered.*

View File

@ -1,4 +1,4 @@
"""Utility functions for nanobot.""" """Utility functions for Boardware Genius."""
from nanobot.utils.helpers import ( from nanobot.utils.helpers import (
ensure_dir, ensure_dir,

View File

@ -1 +1 @@
"""Web interface for nanobot.""" """Web interface for Boardware Genius."""

View File

@ -1,4 +1,4 @@
"""FastAPI web server for nanobot frontend.""" """FastAPI web server for the Boardware Genius frontend."""
from __future__ import annotations from __future__ import annotations
@ -8,6 +8,7 @@ import json
import os import os
import re import re
import secrets import secrets
import shlex
import shutil import shutil
import time import time
import zipfile import zipfile
@ -676,6 +677,8 @@ def create_app(
app.state.config = config app.state.config = config
app.state.config_path = get_config_path() app.state.config_path = get_config_path()
app.state.runtime_env_path = _get_runtime_env_file_path(app.state.config_path)
_sync_authz_runtime_env(app.state.config, app.state.runtime_env_path)
app.state.session_manager = session_manager app.state.session_manager = session_manager
app.state.cron_service = cron_service app.state.cron_service = cron_service
app.state.bus = bus app.state.bus = bus
@ -766,6 +769,60 @@ def _get_auth_file_path() -> Path:
return Path(__file__).resolve().parents[2] / "web_auth_users.json" return Path(__file__).resolve().parents[2] / "web_auth_users.json"
_AUTHZ_RUNTIME_ENV_KEYS = (
"NANOBOT_AUTHZ__ENABLED",
"NANOBOT_AUTHZ__BASE_URL",
"NANOBOT_AUTHZ__OUTLOOK_MCP_URL",
"NANOBOT_BACKEND_IDENTITY__BACKEND_ID",
"NANOBOT_BACKEND_IDENTITY__CLIENT_ID",
"NANOBOT_BACKEND_IDENTITY__CLIENT_SECRET",
"NANOBOT_BACKEND_IDENTITY__NAME",
"NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL",
)
def _get_runtime_env_file_path(config_path: Path | None = None) -> Path:
env = os.getenv("NANOBOT_RUNTIME_ENV_FILE", "").strip()
if env:
return Path(env).expanduser()
base_path = config_path or get_config_path()
return base_path.parent / "runtime.env"
def _authz_runtime_env_values(config: Config) -> dict[str, str]:
return {
"NANOBOT_AUTHZ__ENABLED": "1" if config.authz.enabled and config.authz.base_url.strip() else "0",
"NANOBOT_AUTHZ__BASE_URL": config.authz.base_url.strip(),
"NANOBOT_AUTHZ__OUTLOOK_MCP_URL": config.authz.outlook_mcp_url.strip(),
"NANOBOT_BACKEND_IDENTITY__BACKEND_ID": config.backend_identity.backend_id.strip(),
"NANOBOT_BACKEND_IDENTITY__CLIENT_ID": config.backend_identity.client_id.strip(),
"NANOBOT_BACKEND_IDENTITY__CLIENT_SECRET": config.backend_identity.client_secret.strip(),
"NANOBOT_BACKEND_IDENTITY__NAME": config.backend_identity.name.strip(),
"NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL": config.backend_identity.public_base_url.strip(),
}
def _sync_authz_runtime_env(config: Config, target_path: Path) -> None:
values = _authz_runtime_env_values(config)
target_path.parent.mkdir(parents=True, exist_ok=True)
lines: list[str] = []
for key in _AUTHZ_RUNTIME_ENV_KEYS:
value = values.get(key, "")
if value:
os.environ[key] = value
lines.append(f"export {key}={shlex.quote(value)}")
continue
if key == "NANOBOT_AUTHZ__ENABLED":
os.environ[key] = "0"
lines.append("export NANOBOT_AUTHZ__ENABLED=0")
continue
os.environ.pop(key, None)
lines.append(f"unset {key}")
target_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def _load_auth_users(path: Path) -> dict[str, str]: def _load_auth_users(path: Path) -> dict[str, str]:
"""Load users from local JSON file. """Load users from local JSON file.
@ -1129,6 +1186,7 @@ def _register_routes(app: FastAPI) -> None:
if authz_enabled: if authz_enabled:
config.authz.enabled = True config.authz.enabled = True
_save_app_config(config) _save_app_config(config)
_sync_authz_runtime_env(config, app.state.runtime_env_path)
return _local_backend_view(config) return _local_backend_view(config)
def _authz_client(config: Config): def _authz_client(config: Config):

View File

@ -1,6 +1,6 @@
# nanobot Workflow # Boardware Genius Workflow
本文按当前仓库代码,整理 nanobot 的主要运行链路,重点说明: 本文按当前仓库代码,整理 Boardware Genius 的主要运行链路。下文的技术命令名仍沿用 `nanobot`,重点说明:
1. 用户执行 `nanobot agent -m "你好"`CLI 单轮模式到底走了什么路径。 1. 用户执行 `nanobot agent -m "你好"`CLI 单轮模式到底走了什么路径。
2. `nanobot gateway` 常驻模式下外部渠道、cron、heartbeat 如何进入同一套工作流。 2. `nanobot gateway` 常驻模式下外部渠道、cron、heartbeat 如何进入同一套工作流。
@ -106,7 +106,7 @@
│ │ └─ NO -> return OutboundMessage(final_content) │ │ └─ NO -> return OutboundMessage(final_content)
├─ process_direct() 拿到 OutboundMessage.content ├─ process_direct() 拿到 OutboundMessage.content
├─ console.print("🐈 ...") ├─ console.print("Boardware Genius ...")
└─ await agent_loop.close_mcp() -> 程序退出 └─ await agent_loop.close_mcp() -> 程序退出
``` ```

View File

@ -48,7 +48,7 @@ Required:
--instance-id <id> Unique instance id. --instance-id <id> Unique instance id.
--auth-username <name> Initial web login username. --auth-username <name> Initial web login username.
--auth-password <password> Initial web login password. --auth-password <password> Initial web login password.
--api-key <key> Provider API key for nanobot. --api-key <key> Provider API key for Boardware Genius.
Optional: Optional:
--image <name> Docker image tag. Default: nano/app-instance:latest --image <name> Docker image tag. Default: nano/app-instance:latest
@ -248,6 +248,57 @@ target.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encodin
PY PY
} }
render_runtime_env_file() {
local target_path="$1"
TARGET_PATH="$target_path" \
AUTHZ_BASE_URL="$AUTHZ_BASE_URL" \
AUTHZ_OUTLOOK_MCP_URL="$AUTHZ_OUTLOOK_MCP_URL" \
BACKEND_ID="$BACKEND_ID" \
CLIENT_ID="$CLIENT_ID" \
CLIENT_SECRET="$CLIENT_SECRET" \
BACKEND_NAME="$BACKEND_NAME" \
PUBLIC_URL="$PUBLIC_URL" \
python3 - <<'PY'
import os
import shlex
from pathlib import Path
target = Path(os.environ["TARGET_PATH"])
values = {
"NANOBOT_AUTHZ__ENABLED": "1" if os.environ["AUTHZ_BASE_URL"].strip() else "0",
"NANOBOT_AUTHZ__BASE_URL": os.environ["AUTHZ_BASE_URL"].strip(),
"NANOBOT_AUTHZ__OUTLOOK_MCP_URL": os.environ["AUTHZ_OUTLOOK_MCP_URL"].strip(),
"NANOBOT_BACKEND_IDENTITY__BACKEND_ID": os.environ["BACKEND_ID"].strip(),
"NANOBOT_BACKEND_IDENTITY__CLIENT_ID": os.environ["CLIENT_ID"].strip(),
"NANOBOT_BACKEND_IDENTITY__CLIENT_SECRET": os.environ["CLIENT_SECRET"].strip(),
"NANOBOT_BACKEND_IDENTITY__NAME": os.environ["BACKEND_NAME"].strip(),
"NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL": os.environ["PUBLIC_URL"].strip(),
}
ordered_keys = [
"NANOBOT_AUTHZ__ENABLED",
"NANOBOT_AUTHZ__BASE_URL",
"NANOBOT_AUTHZ__OUTLOOK_MCP_URL",
"NANOBOT_BACKEND_IDENTITY__BACKEND_ID",
"NANOBOT_BACKEND_IDENTITY__CLIENT_ID",
"NANOBOT_BACKEND_IDENTITY__CLIENT_SECRET",
"NANOBOT_BACKEND_IDENTITY__NAME",
"NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL",
]
lines: list[str] = []
for key in ordered_keys:
value = values.get(key, "")
if value:
lines.append(f"export {key}={shlex.quote(value)}")
continue
if key == "NANOBOT_AUTHZ__ENABLED":
lines.append("export NANOBOT_AUTHZ__ENABLED=0")
else:
lines.append(f"unset {key}")
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
PY
}
image_exists() { image_exists() {
docker image inspect "$IMAGE_NAME" >/dev/null 2>&1 docker image inspect "$IMAGE_NAME" >/dev/null 2>&1
} }
@ -457,12 +508,14 @@ INSTANCE_ROOT="${INSTANCES_ROOT}/${INSTANCE_SLUG}"
NANOBOT_HOME="${INSTANCE_ROOT}/nanobot-home" NANOBOT_HOME="${INSTANCE_ROOT}/nanobot-home"
CONFIG_PATH="${NANOBOT_HOME}/config.json" CONFIG_PATH="${NANOBOT_HOME}/config.json"
AUTH_USERS_PATH="${NANOBOT_HOME}/web_auth_users.json" AUTH_USERS_PATH="${NANOBOT_HOME}/web_auth_users.json"
RUNTIME_ENV_PATH="${NANOBOT_HOME}/runtime.env"
WORKSPACE_PATH="${NANOBOT_HOME}/workspace" WORKSPACE_PATH="${NANOBOT_HOME}/workspace"
mkdir -p "$NANOBOT_HOME" "$WORKSPACE_PATH" mkdir -p "$NANOBOT_HOME" "$WORKSPACE_PATH"
render_config_json "$CONFIG_PATH" render_config_json "$CONFIG_PATH"
render_auth_users_json "$AUTH_USERS_PATH" render_auth_users_json "$AUTH_USERS_PATH"
render_runtime_env_file "$RUNTIME_ENV_PATH"
if [[ "$FORCE_BUILD" -eq 1 ]] || ! image_exists; then if [[ "$FORCE_BUILD" -eq 1 ]] || ! image_exists; then
log "building image ${IMAGE_NAME}" log "building image ${IMAGE_NAME}"
@ -539,6 +592,7 @@ instance_root=${INSTANCE_ROOT}
nanobot_home=${NANOBOT_HOME} nanobot_home=${NANOBOT_HOME}
config_path=${CONFIG_PATH} config_path=${CONFIG_PATH}
auth_users_path=${AUTH_USERS_PATH} auth_users_path=${AUTH_USERS_PATH}
runtime_env_path=${RUNTIME_ENV_PATH}
username=${USERNAME} username=${USERNAME}
email=${EMAIL} email=${EMAIL}
instance_host=${INSTANCE_HOST} instance_host=${INSTANCE_HOST}

View File

@ -6,6 +6,7 @@ APP_FRONTEND_PORT="${APP_FRONTEND_PORT:-3000}"
APP_BACKEND_PORT="${APP_BACKEND_PORT:-18080}" APP_BACKEND_PORT="${APP_BACKEND_PORT:-18080}"
NANOBOT_HOME="${NANOBOT_HOME:-/root/.nanobot}" NANOBOT_HOME="${NANOBOT_HOME:-/root/.nanobot}"
NANOBOT_AUTH_FILE="${NANOBOT_AUTH_FILE:-$NANOBOT_HOME/web_auth_users.json}" NANOBOT_AUTH_FILE="${NANOBOT_AUTH_FILE:-$NANOBOT_HOME/web_auth_users.json}"
NANOBOT_RUNTIME_ENV_FILE="${NANOBOT_RUNTIME_ENV_FILE:-$NANOBOT_HOME/runtime.env}"
log() { log() {
printf '[app-instance] %s\n' "$*" printf '[app-instance] %s\n' "$*"
@ -41,10 +42,17 @@ trap cleanup EXIT INT TERM
mkdir -p "$NANOBOT_HOME" "$NANOBOT_HOME/workspace" mkdir -p "$NANOBOT_HOME" "$NANOBOT_HOME/workspace"
require_file "$NANOBOT_HOME/config.json" "Missing nanobot config" if [[ -f "$NANOBOT_RUNTIME_ENV_FILE" ]]; then
set -a
. "$NANOBOT_RUNTIME_ENV_FILE"
set +a
fi
require_file "$NANOBOT_HOME/config.json" "Missing Boardware Genius config"
require_file "$NANOBOT_AUTH_FILE" "Missing web auth users file" require_file "$NANOBOT_AUTH_FILE" "Missing web auth users file"
export NANOBOT_AUTH_FILE export NANOBOT_AUTH_FILE
export NANOBOT_RUNTIME_ENV_FILE
export PORT="$APP_FRONTEND_PORT" export PORT="$APP_FRONTEND_PORT"
export HOSTNAME="127.0.0.1" export HOSTNAME="127.0.0.1"
@ -64,4 +72,3 @@ nginx -c /opt/app/nginx.conf -g 'daemon off;' &
NGINX_PID=$! NGINX_PID=$!
wait -n "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID" wait -n "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID"

View File

@ -60,10 +60,7 @@ export default function PluginsPage() {
</h1> </h1>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{' '} workspace <code className="text-xs bg-muted px-1 py-0.5 rounded">plugins/</code>
<code className="text-xs bg-muted px-1 py-0.5 rounded">~/.nanobot/plugins/</code>
{' '}{' '}
<code className="text-xs bg-muted px-1 py-0.5 rounded">&lt;workspace&gt;/plugins/</code>
</p> </p>
</div> </div>
<Button onClick={load} variant="outline" size="sm"> <Button onClick={load} variant="outline" size="sm">
@ -91,7 +88,7 @@ export default function PluginsPage() {
<Blocks className="w-12 h-12 mx-auto mb-4 opacity-30" /> <Blocks className="w-12 h-12 mx-auto mb-4 opacity-30" />
<p className="font-medium"></p> <p className="font-medium"></p>
<p className="text-sm mt-2 max-w-sm mx-auto"> <p className="text-sm mt-2 max-w-sm mx-auto">
<code className="text-xs bg-muted px-1 py-0.5 rounded">~/.nanobot/plugins/</code> workspace <code className="text-xs bg-muted px-1 py-0.5 rounded">plugins/</code>
Boardware Agent Sandbox Boardware Agent Sandbox
</p> </p>
</CardContent> </CardContent>

View File

@ -60,7 +60,7 @@ export default function StatusPage() {
<p className="font-medium"> Boardware Agent Sandbox </p> <p className="font-medium"> Boardware Agent Sandbox </p>
<p className="text-sm text-muted-foreground mt-1">{error}</p> <p className="text-sm text-muted-foreground mt-1">{error}</p>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
<code className="bg-muted px-1 rounded">nanobot web</code> 访
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
# 单用户后端地址(nanobot web # 单用户后端地址(Boardware Genius Web 后端
NEXT_PUBLIC_API_URL=http://127.0.0.1:10000 NEXT_PUBLIC_API_URL=http://127.0.0.1:10000
NEXT_PUBLIC_WS_URL=wss://127.0.0.1:10000 NEXT_PUBLIC_WS_URL=wss://127.0.0.1:10000
NEXT_PUBLIC_AUTH_PORTAL_URL=http://127.0.0.1:3081 NEXT_PUBLIC_AUTH_PORTAL_URL=http://127.0.0.1:3081

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -1,6 +1,6 @@
# nanobot-auth-portal # Boardware Genius Auth Portal
Dedicated login/register frontend for nanobot containers. Dedicated login/register frontend for Boardware Genius containers.
## Docs ## Docs

View File

@ -3,7 +3,7 @@ import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Boardware Agent Sandbox Auth Portal', title: 'Boardware Agent Sandbox Auth Portal',
description: 'Dedicated login and registration portal for nanobot containers.', description: 'Dedicated login and registration portal for Boardware Genius containers.',
icons: { icons: {
icon: '/boardware-logo.jpg', icon: '/boardware-logo.jpg',
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -153,6 +153,28 @@ async def _call_instance_api(base_url: str, path: str, payload: dict[str, Any])
) )
async def _sync_instance_backend_binding(username: str, response: dict[str, Any]) -> dict[str, Any] | None:
local_backend = _as_object(response.get("local_backend"))
backend_id = _as_string(local_backend.get("backend_id"))
if not username.strip() or not backend_id:
return None
payload: dict[str, Any] = {
"username": username.strip(),
"backend_id": backend_id,
"authz_base_url": ISSUER,
}
backend_name = _as_string(local_backend.get("name"))
if backend_name:
payload["backend_name"] = backend_name
try:
return await _call_deploy_control("/api/instances/bind-backend", payload)
except HTTPException:
# Registration should not fail only because registry metadata sync failed.
return None
def _normalize_portal_token_response( def _normalize_portal_token_response(
response: dict[str, Any], response: dict[str, Any],
routing: dict[str, Any], routing: dict[str, Any],
@ -438,6 +460,11 @@ async def portal_register(req: PortalRegisterRequest) -> dict[str, Any]:
instance_payload["backend_name"] = backend_name instance_payload["backend_name"] = backend_name
response = await _call_instance_api(api_base_url, "/api/auth/register", instance_payload) response = await _call_instance_api(api_base_url, "/api/auth/register", instance_payload)
bound_instance = await _sync_instance_backend_binding(username, response)
if isinstance(bound_instance, dict):
synced_record = _as_object(bound_instance.get("instance"))
if synced_record:
routing["instance"] = synced_record
return _normalize_portal_token_response(response, routing) return _normalize_portal_token_response(response, routing)

View File

@ -282,6 +282,98 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
} }
def _upsert_registry_record(record: dict[str, Any]) -> dict[str, Any]:
instance_id = str(record.get("instance_id", "") or "").strip()
if not instance_id:
raise ApiError(HTTPStatus.BAD_GATEWAY, "registry record is missing instance_id")
command = [
str(REGISTRY_TOOL),
"--registry",
str(REGISTRY_PATH),
"upsert",
"--instance-id",
instance_id,
"--instance-slug",
str(record.get("instance_slug", "") or "").strip(),
"--container-name",
str(record.get("container_name", "") or "").strip(),
"--image-name",
str(record.get("image_name", "") or "").strip(),
"--host-port",
str(int(record.get("host_port", 0) or 0)),
"--public-url",
str(record.get("public_url", "") or "").strip(),
"--instance-root",
str(record.get("instance_root", "") or "").strip(),
"--nanobot-home",
str(record.get("nanobot_home", "") or "").strip(),
"--config-path",
str(record.get("config_path", "") or "").strip(),
"--auth-users-path",
str(record.get("auth_users_path", "") or "").strip(),
"--network-name",
str(record.get("network_name", "") or "").strip(),
"--backend-id",
str(record.get("backend_id", "") or "").strip(),
"--backend-name",
str(record.get("backend_name", "") or "").strip(),
"--authz-base-url",
str(record.get("authz_base_url", "") or "").strip(),
"--username",
str(record.get("username", "") or "").strip(),
"--email",
str(record.get("email", "") or "").strip(),
"--instance-host",
str(record.get("instance_host", "") or "").strip(),
"--frontend-base-url",
str(record.get("frontend_base_url", "") or "").strip(),
"--api-base-url",
str(record.get("api_base_url", "") or "").strip(),
"--created-at",
str(record.get("created_at", "") or "").strip(),
]
run_command(command, cwd=APP_INSTANCE_DIR)
updated = get_registry_record(instance_id=instance_id)
if updated is None:
raise ApiError(HTTPStatus.BAD_GATEWAY, "registry record update did not persist")
return updated
def bind_instance_backend(payload: dict[str, Any]) -> dict[str, Any]:
instance_id = str(payload.get("instance_id", "") or "").strip()
username = str(payload.get("username", "") or "").strip()
backend_id = str(payload.get("backend_id", "") or "").strip()
backend_name = str(payload.get("backend_name", "") or "").strip()
authz_base_url = str(payload.get("authz_base_url", "") or "").strip()
if not backend_id:
raise ApiError(HTTPStatus.BAD_REQUEST, "backend_id is required")
if not instance_id and not username:
raise ApiError(HTTPStatus.BAD_REQUEST, "instance_id or username is required")
record = None
if instance_id:
record = get_registry_record(instance_id=instance_id)
if record is None and username:
record = get_registry_record(username=username)
if record is None:
raise ApiError(HTTPStatus.NOT_FOUND, "instance not found")
updated_record = dict(record)
updated_record["backend_id"] = backend_id
updated_record["backend_name"] = backend_name or str(record.get("backend_name", "") or "").strip() or backend_id
if authz_base_url:
updated_record["authz_base_url"] = authz_base_url
updated = _upsert_registry_record(updated_record)
return {
"instance": updated,
"public_url": str(updated.get("public_url", "") or ""),
"frontend_base_url": str(updated.get("frontend_base_url", "") or updated.get("public_url", "") or ""),
"api_base_url": build_internal_api_base_url(updated),
}
def resolve_instance(payload: dict[str, Any]) -> dict[str, Any]: def resolve_instance(payload: dict[str, Any]) -> dict[str, Any]:
username = str(payload.get("username", "") or "").strip() username = str(payload.get("username", "") or "").strip()
if not username: if not username:
@ -367,6 +459,10 @@ class Handler(BaseHTTPRequestHandler):
payload = self._read_json_body() payload = self._read_json_body()
self._json_response(HTTPStatus.OK, create_or_get_instance(payload)) self._json_response(HTTPStatus.OK, create_or_get_instance(payload))
return return
if self.path == "/api/instances/bind-backend":
payload = self._read_json_body()
self._json_response(HTTPStatus.OK, bind_instance_backend(payload))
return
if self.path == "/api/instances/resolve": if self.path == "/api/instances/resolve":
payload = self._read_json_body() payload = self._read_json_body()
self._json_response(HTTPStatus.OK, resolve_instance(payload)) self._json_response(HTTPStatus.OK, resolve_instance(payload))