feat: 重命名项目为Boardware Genius并添加运行时环境同步功能
- 将项目品牌从nanobot重命名为Boardware Genius,更新所有相关文档、注释和日志输出 - 在web服务器中添加运行时环境变量同步功能,支持授权和后端身份配置 - 更新create-instance脚本以生成运行时环境文件 - 添加实例后端绑定功能到部署控制服务 - 修改入口脚本以加载运行时环境变量 - 更新前端和认证门户的相关描述文本
This commit is contained in:
@ -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`
|
||||||
|
|
||||||
当前版本里,新实例的默认大模型配置就是从这里分发的:
|
当前版本里,新实例的默认大模型配置就是从这里分发的:
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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__ = ""
|
||||||
|
|||||||
@ -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})
|
||||||
|
|||||||
@ -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."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
"""CLI module for nanobot."""
|
"""CLI module for Boardware Genius."""
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 -->
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Soul
|
# Soul
|
||||||
|
|
||||||
I am nanobot 🐈, a personal AI assistant.
|
I am Boardware Genius, a personal AI assistant.
|
||||||
|
|
||||||
## Personality
|
## Personality
|
||||||
|
|
||||||
|
|||||||
@ -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.*
|
||||||
|
|||||||
@ -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.*
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
"""Web interface for nanobot."""
|
"""Web interface for Boardware Genius."""
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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() -> 程序退出
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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"><workspace>/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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 |
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 |
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user