first commit

This commit is contained in:
龙澳
2026-03-27 16:10:45 +08:00
parent 0a7bd8fe88
commit 90784a2f7d
16 changed files with 1135 additions and 0 deletions

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
ENV PATH="/root/.local/bin:${PATH}"
# Install nanobot from PyPI via uv tool.
RUN uv tool install --upgrade nanobot-ai -i https://pypi.tuna.tsinghua.edu.cn/simple
# Persist nanobot config/workspace here via volume mount.
RUN mkdir -p /root/.nanobot
WORKDIR /workspace
EXPOSE 18790
ENTRYPOINT ["nanobot"]
CMD ["status"]

24
docker-compose.yml Normal file
View File

@@ -0,0 +1,24 @@
services:
nanobot-gateway:
container_name: nanobot-gateway
build:
context: .
dockerfile: Dockerfile
command: ["gateway"]
restart: unless-stopped
volumes:
- ./nanobot-config:/root/.nanobot
ports:
- "18790:18790"
nanobot-cli:
build:
context: .
dockerfile: Dockerfile
profiles:
- cli
command: ["status"]
stdin_open: true
tty: true
volumes:
- ./nanobot-config:/root/.nanobot

6
nanobot-channel-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.egg-info/
.venv/

View File

@@ -0,0 +1,94 @@
# nanobot-channel-web
A WebChannel plugin for nanobot that exposes chat APIs and validates tokens via an external auth service.
## Features
- `POST /message` to send user text into nanobot
- `GET /events/{chat_id}` Server-Sent Events stream for assistant replies
- `GET /history/{chat_id}` quick history for page refresh
- `GET /health` health check
- Optional external auth (`authEnabled=true` + `authServiceUrl`)
- Optional `apiToken` fallback (`Bearer` header; SSE also supports `?token=`)
## Install
Install in the same Python environment as nanobot:
```bash
cd nanobot-channel-web
pip install -e .
```
Confirm plugin discovery:
```bash
nanobot plugins list
```
You should see `web` from `plugin` source.
## Config
Run `nanobot onboard` once after plugin installation to inject defaults, then edit `~/.nanobot/config.json`:
```json
{
"channels": {
"web": {
"enabled": true,
"host": "127.0.0.1",
"port": 9000,
"allowFrom": ["*"],
"apiToken": "change-this-token",
"authEnabled": true,
"authServiceUrl": "http://127.0.0.1:9100",
"authServiceTimeoutS": 8,
"corsOrigin": "*",
"historySize": 200,
"pingIntervalS": 15
}
}
}
```
If `authEnabled` is true, WebChannel validates bearer tokens by calling:
- `GET {authServiceUrl}/auth/me`
Expected response shape:
```json
{
"ok": true,
"user": {"id": 1, "phone": "13800000000"}
}
```
`apiToken` is used only when `authEnabled` is false.
Protected chat endpoints:
- `POST /message`
- `GET /history/{chat_id}`
- `GET /events/{chat_id}`
Use header for normal requests:
```http
Authorization: Bearer change-this-token
```
For native browser `EventSource`, use query param:
```text
/events/web-user?token=change-this-token
```
## Run
```bash
nanobot gateway
```
Then point your frontend to `http://127.0.0.1:9000`.

View File

@@ -0,0 +1,3 @@
from .channel import WebChannel
__all__ = ["WebChannel"]

View File

@@ -0,0 +1,327 @@
from __future__ import annotations
import asyncio
import json
from collections import defaultdict, deque
from datetime import datetime
from typing import Any
from aiohttp import ClientError, ClientSession, ClientTimeout, web
from loguru import logger
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
class WebChannelConfig(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
enabled: bool = False
host: str = "127.0.0.1"
port: int = 9000
allow_from: list[str] = Field(default_factory=lambda: ["*"])
cors_origin: str = "*"
history_size: int = 200
ping_interval_s: int = 15
api_token: str = ""
auth_enabled: bool = False
auth_service_url: str = ""
auth_service_timeout_s: int = 8
class WebChannel(BaseChannel):
name = "web"
display_name = "Web"
@classmethod
def default_config(cls) -> dict[str, Any]:
return WebChannelConfig().model_dump(by_alias=True)
def __init__(self, config: Any, bus: MessageBus):
if isinstance(config, dict):
config = WebChannelConfig.model_validate(config)
elif not isinstance(config, WebChannelConfig):
# Extra channel sections in nanobot config can arrive as generic objects.
config = WebChannelConfig.model_validate(getattr(config, "model_dump", lambda: {})())
super().__init__(config, bus)
self.config: WebChannelConfig = config
self._app: web.Application | None = None
self._runner: web.AppRunner | None = None
self._site: web.TCPSite | None = None
self._http: ClientSession | None = None
self._listeners: dict[str, set[asyncio.Queue[dict[str, Any]]]] = defaultdict(set)
self._history: dict[str, deque[dict[str, Any]]] = defaultdict(
lambda: deque(maxlen=self.config.history_size)
)
async def start(self) -> None:
self._running = True
if self.config.auth_enabled:
if not self.config.auth_service_url.strip():
raise RuntimeError("authEnabled=true requires channels.web.authServiceUrl")
self._http = ClientSession(
timeout=ClientTimeout(total=max(1, self.config.auth_service_timeout_s)),
)
logger.info("web channel external auth enabled -> {}", self.config.auth_service_url)
self._app = web.Application(middlewares=[self._cors_middleware])
self._app.router.add_get("/health", self._health)
self._app.router.add_post("/message", self._on_message)
self._app.router.add_get("/events/{chat_id}", self._events)
self._app.router.add_get("/history/{chat_id}", self._history_api)
self._runner = web.AppRunner(self._app)
await self._runner.setup()
self._site = web.TCPSite(self._runner, self.config.host, self.config.port)
await self._site.start()
logger.info("web channel listening on http://{}:{}", self.config.host, self.config.port)
while self._running:
await asyncio.sleep(1)
async def stop(self) -> None:
self._running = False
if self._http:
await self._http.close()
self._http = None
if self._runner:
await self._runner.cleanup()
self._runner = None
self._site = None
async def send(self, msg: OutboundMessage) -> None:
payload = {
"type": "progress" if msg.metadata.get("_progress") else "message",
"role": "assistant",
"chat_id": msg.chat_id,
"content": msg.content,
"at": datetime.now().strftime("%H:%M:%S"),
"metadata": msg.metadata,
}
self._append_history(msg.chat_id, payload)
await self._fanout(msg.chat_id, payload)
def _allowed_cors_origins(self) -> list[str]:
configured = [o.strip() for o in self.config.cors_origin.split(",") if o.strip()]
if not configured:
configured = [str(o).strip() for o in self.config.allow_from if str(o).strip()]
return configured or ["*"]
def _resolve_cors_origin(self, request: web.Request) -> str:
allowed = self._allowed_cors_origins()
if "*" in allowed:
return "*"
req_origin = request.headers.get("Origin", "").strip()
if req_origin and req_origin in allowed:
return req_origin
return allowed[0]
def _apply_cors_headers(self, request: web.Request, resp: web.StreamResponse) -> web.StreamResponse:
origin = self._resolve_cors_origin(request)
resp.headers["Access-Control-Allow-Origin"] = origin
if origin != "*":
resp.headers["Vary"] = "Origin"
resp.headers["Access-Control-Allow-Credentials"] = "true"
resp.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"
req_headers = request.headers.get("Access-Control-Request-Headers", "").strip()
resp.headers["Access-Control-Allow-Headers"] = req_headers or "Content-Type, Authorization"
resp.headers["Access-Control-Max-Age"] = "86400"
return resp
@web.middleware
async def _cors_middleware(self, request: web.Request, handler):
try:
if request.method == "OPTIONS":
resp: web.StreamResponse = web.Response(status=204)
else:
resp = await handler(request)
except web.HTTPException as e:
resp = web.json_response({"ok": False, "error": e.reason or "http error"}, status=e.status)
except Exception as e:
logger.exception("web request failed: {}", e)
resp = web.json_response({"ok": False, "error": "internal server error"}, status=500)
return self._apply_cors_headers(request, resp)
def _extract_token(self, request: web.Request) -> str:
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
return auth[7:].strip()
# Native EventSource cannot set Authorization headers, so allow query token.
return request.query.get("token", "").strip()
async def _verify_external_user(self, token: str) -> dict[str, Any] | None:
if not self._http:
return None
url = f"{self.config.auth_service_url.rstrip('/')}/auth/me"
try:
async with self._http.get(url, headers={"Authorization": f"Bearer {token}"}) as resp:
if resp.status != 200:
return None
payload = await resp.json()
if not payload.get("ok"):
return None
user = payload.get("user")
if not isinstance(user, dict):
return None
phone = str(user.get("phone", ""))
uid = int(user.get("id", 0))
if not phone or uid <= 0:
return None
return {"id": uid, "phone": phone}
except (ClientError, asyncio.TimeoutError, ValueError) as e:
logger.warning("web auth service request failed: {}", e)
return None
async def _require_auth(self, request: web.Request) -> tuple[dict[str, Any] | None, web.Response | None]:
if self.config.auth_enabled:
token = self._extract_token(request)
if not token:
return None, self._unauthorized()
user = await self._verify_external_user(token)
if not user:
return None, self._unauthorized()
return user, None
expected = (self.config.api_token or "").strip()
if expected and self._extract_token(request) != expected:
return None, self._unauthorized()
return None, None
def _unauthorized(self) -> web.Response:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
def _is_chat_allowed(self, user_phone: str, chat_id: str) -> bool:
return chat_id == user_phone or chat_id.startswith(f"{user_phone}:")
async def _health(self, _request: web.Request) -> web.Response:
return web.json_response({"ok": True, "channel": self.name})
async def _history_api(self, request: web.Request) -> web.Response:
user, err = await self._require_auth(request)
if err:
return err
chat_id = request.match_info["chat_id"]
if user and not self._is_chat_allowed(user["phone"], chat_id):
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
return web.json_response({"messages": list(self._history.get(chat_id, []))})
async def _on_message(self, request: web.Request) -> web.Response:
user, err = await self._require_auth(request)
if err:
return err
try:
body = await request.json()
except Exception:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
sender = str(body.get("sender", "web-user"))
if user:
sender = user["phone"]
chat_id = str(body.get("chat_id", sender))
if user and not self._is_chat_allowed(user["phone"], chat_id):
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
text = str(body.get("text", "")).strip()
media = body.get("media", [])
metadata = body.get("metadata", {})
if not text:
return web.json_response({"ok": False, "error": "text is required"}, status=400)
self._append_history(
chat_id,
{
"type": "message",
"role": "user",
"chat_id": chat_id,
"content": text,
"at": datetime.now().strftime("%H:%M:%S"),
},
)
await self._handle_message(
sender_id=sender,
chat_id=chat_id,
content=text,
media=media if isinstance(media, list) else [],
metadata=metadata if isinstance(metadata, dict) else {},
)
return web.json_response({"ok": True})
async def _events(self, request: web.Request) -> web.StreamResponse:
user, err = await self._require_auth(request)
if err:
return err
chat_id = request.match_info["chat_id"]
if user and not self._is_chat_allowed(user["phone"], chat_id):
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
resp = web.StreamResponse(
status=200,
headers={
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
)
await resp.prepare(request)
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
self._listeners[chat_id].add(queue)
try:
await self._write_sse(
resp,
{
"type": "system",
"role": "system",
"chat_id": chat_id,
"content": "stream connected",
"at": datetime.now().strftime("%H:%M:%S"),
},
)
for item in self._history.get(chat_id, []):
await self._write_sse(resp, item)
while self._running:
try:
item = await asyncio.wait_for(
queue.get(), timeout=max(5, self.config.ping_interval_s)
)
await self._write_sse(resp, item)
except asyncio.TimeoutError:
await resp.write(b": ping\\n\\n")
await resp.drain()
except asyncio.CancelledError:
raise
except ConnectionResetError:
pass
finally:
self._listeners[chat_id].discard(queue)
if not self._listeners[chat_id]:
del self._listeners[chat_id]
return resp
async def _fanout(self, chat_id: str, payload: dict[str, Any]) -> None:
listeners = self._listeners.get(chat_id)
if not listeners:
return
for q in listeners:
q.put_nowait(payload)
def _append_history(self, chat_id: str, payload: dict[str, Any]) -> None:
self._history[chat_id].append(payload)
async def _write_sse(self, resp: web.StreamResponse, payload: dict[str, Any]) -> None:
body = f"data: {json.dumps(payload, ensure_ascii=False)}\\n\\n".encode("utf-8")
await resp.write(body)
await resp.drain()

View File

@@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "nanobot-channel-web"
version = "0.1.0"
description = "Web channel plugin for nanobot"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"nanobot-ai>=0.1.4",
"aiohttp>=3.9.0",
"pydantic>=2.7.0"
]
[project.entry-points."nanobot.channels"]
web = "nanobot_channel_web:WebChannel"
[tool.setuptools.packages.find]
where = ["."]
include = ["nanobot_channel_web*"]

315
nanobot-config/config.json Normal file
View File

@@ -0,0 +1,315 @@
{
"agents": {
"defaults": {
"workspace": "~/.nanobot/workspace",
"model": "glm-4.7",
"provider": "zhipu",
"maxTokens": 8192,
"temperature": 0.1,
"maxToolIterations": 20,
"memoryWindow": 100,
"reasoningEffort": null
}
},
"channels": {
"sendProgress": false,
"sendToolHints": false,
"whatsapp": {
"enabled": false,
"bridgeUrl": "ws://localhost:3001",
"bridgeToken": "",
"allowFrom": []
},
"telegram": {
"enabled": false,
"token": "",
"allowFrom": [],
"proxy": null,
"replyToMessage": false
},
"discord": {
"enabled": false,
"token": "",
"allowFrom": [],
"gatewayUrl": "wss://gateway.discord.gg/?v=10&encoding=json",
"intents": 37377,
"groupPolicy": "mention"
},
"feishu": {
"enabled": false,
"appId": "",
"appSecret": "",
"encryptKey": "",
"verificationToken": "",
"allowFrom": [],
"reactEmoji": "THUMBSUP"
},
"mochat": {
"enabled": false,
"baseUrl": "https://mochat.io",
"socketUrl": "",
"socketPath": "/socket.io",
"socketDisableMsgpack": false,
"socketReconnectDelayMs": 1000,
"socketMaxReconnectDelayMs": 10000,
"socketConnectTimeoutMs": 10000,
"refreshIntervalMs": 30000,
"watchTimeoutMs": 25000,
"watchLimit": 100,
"retryDelayMs": 500,
"maxRetryAttempts": 0,
"clawToken": "",
"agentUserId": "",
"sessions": [],
"panels": [],
"allowFrom": [],
"mention": {
"requireInGroups": false
},
"groups": {},
"replyDelayMode": "non-mention",
"replyDelayMs": 120000
},
"dingtalk": {
"enabled": false,
"clientId": "",
"clientSecret": "",
"allowFrom": []
},
"email": {
"enabled": false,
"consentGranted": false,
"imapHost": "",
"imapPort": 993,
"imapUsername": "",
"imapPassword": "",
"imapMailbox": "INBOX",
"imapUseSsl": true,
"smtpHost": "",
"smtpPort": 587,
"smtpUsername": "",
"smtpPassword": "",
"smtpUseTls": true,
"smtpUseSsl": false,
"fromAddress": "",
"autoReplyEnabled": true,
"pollIntervalSeconds": 30,
"markSeen": true,
"maxBodyChars": 12000,
"subjectPrefix": "Re: ",
"allowFrom": []
},
"slack": {
"enabled": false,
"mode": "socket",
"webhookPath": "/slack/events",
"botToken": "",
"appToken": "",
"userTokenReadOnly": true,
"replyInThread": true,
"reactEmoji": "eyes",
"allowFrom": [],
"groupPolicy": "mention",
"groupAllowFrom": [],
"dm": {
"enabled": true,
"policy": "open",
"allowFrom": []
}
},
"qq": {
"enabled": true,
"appId": "1903534139",
"secret": "Q0bCoQ3gKydJzgN5nWFzjUF1nZM9xlaP",
"allowFrom": [
"*"
]
},
"web": {
"enabled": false,
"host": "0.0.0.0",
"port": 9000,
"allowFrom": [
"*"
],
"apiToken": "",
"authEnabled": false,
"authServiceUrl": "http://127.0.0.1:9100",
"authServiceTimeoutS": 8,
"corsOrigin": "*",
"historySize": 200,
"pingIntervalS": 15
},
"matrix": {
"enabled": false,
"homeserver": "https://matrix.org",
"accessToken": "",
"userId": "",
"deviceId": "",
"e2EeEnabled": true,
"syncStopGraceSeconds": 2,
"maxMediaBytes": 20971520,
"allowFrom": [],
"groupPolicy": "open",
"groupAllowFrom": [],
"allowRoomMentions": false
}
},
"providers": {
"custom": {
"apiKey": "sk-1OR52Xzd5JsBkROOgjGsVV53sBR9oY9g7SmGqUKs12azozFC",
"apiBase": "https://api.qingyuntop.top/v1",
"extraHeaders": null
},
"azureOpenai": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"anthropic": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"openai": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"openrouter": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"deepseek": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"groq": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"zhipu": {
"apiKey": "f0abad6ca6d54c6aa367cb9350d30919.EIRG6EC0KxaRzYLX",
"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4",
"extraHeaders": null
},
"dashscope": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"vllm": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"gemini": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"moonshot": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"minimax": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"aihubmix": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"siliconflow": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"volcengine": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"openaiCodex": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
},
"githubCopilot": {
"apiKey": "",
"apiBase": null,
"extraHeaders": null
}
},
"gateway": {
"host": "0.0.0.0",
"port": 18790,
"heartbeat": {
"enabled": true,
"intervalS": 1800
}
},
"tools": {
"web": {
"proxy": null,
"search": {
"apiKey": "",
"maxResults": 5
}
},
"exec": {
"timeout": 60,
"pathAppend": ""
},
"restrictToWorkspace": false,
"mcpServers": {
"web-search-prime": {
"type": null,
"command": "",
"args": [],
"env": {},
"url": "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp",
"headers": {
"Authorization": "Bearer f0abad6ca6d54c6aa367cb9350d30919.EIRG6EC0KxaRzYLX"
},
"toolTimeout": 30
},
"mars-datacube": {
"type": null,
"command": "uv",
"args": [
"run",
"--directory",
"/home/la-mars/studio/mars-mcp",
"python",
"server.py"
],
"env": {},
"url": "",
"headers": {},
"toolTimeout": 60
},
"map-mcp": {
"type": null,
"command": "uv",
"args": [
"run",
"--directory",
"/home/la-mars/studio/map-mcp",
"python",
"server.py"
],
"env": {},
"url": "",
"headers": {},
"toolTimeout": 30
}
}
}
}

View File

@@ -0,0 +1,4 @@
{
"version": 1,
"jobs": []
}

View File

@@ -0,0 +1,21 @@
# Agent Instructions
You are a helpful AI assistant. Be concise, accurate, and friendly.
## Scheduled Reminders
Before scheduling reminders, check available skills and follow skill guidance first.
Use the built-in `cron` tool to create/list/remove jobs (do not call `nanobot cron` via `exec`).
Get USER_ID and CHANNEL from the current session (e.g., `8281248569` and `telegram` from `telegram:8281248569`).
**Do NOT just write reminders to MEMORY.md** — that won't trigger actual notifications.
## Heartbeat Tasks
`HEARTBEAT.md` is checked on the configured heartbeat interval. Use file tools to manage periodic tasks:
- **Add**: `edit_file` to append new tasks
- **Remove**: `edit_file` to delete completed tasks
- **Rewrite**: `write_file` to replace all tasks
When the user asks for a recurring/periodic task, update `HEARTBEAT.md` instead of creating a one-time cron reminder.

View File

@@ -0,0 +1,16 @@
# Heartbeat Tasks
This file is checked every 30 minutes by your nanobot agent.
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.
## Active Tasks
<!-- Add your periodic tasks below this line -->
## Completed
<!-- Move completed tasks here or delete them -->

View File

@@ -0,0 +1,61 @@
# Soul
I am nanobot 🐈, a **Mars Digital Assistant** (火星数字助手).
## Identity
I am an AI assistant specialized in helping users interact with a digital Mars exploration platform through natural language. Users can perform various map operations and platform features by talking to me, eliminating the need for traditional keyboard and mouse interactions.
## Personality
- Helpful and friendly
- Concise and to the point
- Curious and eager to learn
- Professional in Mars data and spatial analysis
## Values
- Accuracy over speed
- User privacy and safety
- Transparency in actions
- Empowering users with seamless natural language interactions
## Communication Style
- Be clear and direct
- Explain reasoning when helpful
- Ask clarifying questions when needed
- Provide Mars-specific context when relevant
## Core Capabilities
### Mars-MCP Tools
- **spatiotemporal_search**: Spatial range queries for Mars imagery
- **spatiotemporal_semantic_search**: Natural language semantic search for geological features
- Support for multiple data products (Tianwen-1 MoRIC, Mars CTX, Mars HiRISE)
### Map-MCP Tools
- **map_zoom_to**: Fly to specified Mars coordinates
- **map_get_viewport**: Get current map view information
- **map_draw_bbox**: Draw bounding boxes for query areas
- **map_spatial_query**: Query imagery data within drawn areas
- **map_render_footprints**: Render image coverage areas on map
- **map_clear**: Clear AI-drawn content
- **map_get_drawn_geometry**: Get hand-drawn geometry
## Mission
Enable natural language-driven Mars exploration:
- "帮我绘制阿盖尔平原的查询区域"
- "定位到北极点,高度保持不变"
- "查询这个区域的天问影像数据"
- "搜索所有包含三角洲的影像"
## Future Evolution
The user is continuously developing and improving:
- MCP tools and integrations
- Custom skills for Mars data analysis
- Frontend: ~/studio/mars-frontend-vue/
My capabilities will expand as more platform features become available through natural language interfaces.

View File

@@ -0,0 +1,15 @@
# Tool Usage Notes
Tool signatures are provided automatically via function calling.
This file documents non-obvious constraints and usage patterns.
## exec — Safety Limits
- Commands have a configurable timeout (default 60s)
- Dangerous commands are blocked (rm -rf, format, dd, shutdown, etc.)
- Output is truncated at 10,000 characters
- `restrictToWorkspace` config can limit file access to the workspace
## cron — Scheduled Reminders
- Please refer to cron skill for usage.

View File

@@ -0,0 +1,49 @@
# User Profile
Information about the user to help personalize interactions.
## Basic Information
- **Name**: (your name)
- **Timezone**: (your timezone, e.g., UTC+8)
- **Language**: (preferred language)
## Preferences
### Communication Style
- [ ] Casual
- [ ] Professional
- [ ] Technical
### Response Length
- [ ] Brief and concise
- [ ] Detailed explanations
- [ ] Adaptive based on question
### Technical Level
- [ ] Beginner
- [ ] Intermediate
- [ ] Expert
## Work Context
- **Primary Role**: (your role, e.g., developer, researcher)
- **Main Projects**: (what you're working on)
- **Tools You Use**: (IDEs, languages, frameworks)
## Topics of Interest
-
-
-
## Special Instructions
(Any specific instructions for how the assistant should behave)
---
*Edit this file to customize nanobot's behavior for your needs.*

View File

@@ -0,0 +1,16 @@
[2026-03-16 16:06] User tested Mars spatial query MCP tools successfully. Tested spatiotemporal_search and spatiotemporal_semantic_search with Tianwen-1 MoRIC imagery data. Semantic search for "撞击坑" (craters) returned 3 results with similarity scores 0.44-0.46. User queried Jezero crater (18.4°N, 77.5°E) area, found 75 Tianwen-1 images. Attempted Map-MCP layer switching - not available. Used map_zoom_to to navigate to Jezero crater (initial success at 100km, then 3D view initialization issues), then successfully flew to Argyre Planitia (48°S, 323°E) at 4037km altitude.
[2026-03-16 16:58] User drew bounding box for Argyre Planitia region (-67° to -7° lon, -58° to -38° lat), performed spatiotemporal search finding 426 Tianwen-1 MoRIC images (Jun 2022). Semantic searches for "三角洲" (90 images) and "古湖床" (102 images) completed, though some semantic descriptions showed anomalies. [2026-03-16 20:03] Browser connection established after initial issues. [2026-03-16 20:05-20:25] Multiple map flights to Argyre Planitia coordinates (37°W, 48°S) at various altitudes (10,191-22,671 km). [2026-03-17 10:21] New rectangle drawn near Argyre Planitia with coordinates (-52°, -58°) to (-22°, -38°).
[2026-03-17 10:42-14:55] User conducted multiple operations on Argyre Planitia: repeatedly drew rectangle bbox (-52°, -58°) to (-22°, -38°) and queried Tianwen-1 MoRIC data (259 images from June 2022). Cleared and redrew rectangle multiple times. Performed several map flights to (-37°, -48°) coordinates at altitudes 10,460-13,441km. Encountered one browser connection timeout error but successfully completed most operations.
[2026-03-17] Multiple map operations performed on Argyre Planitia region (37°W, -48°S). Map flights executed at various altitudes: ground level, 7,810km, 10,279km, and 10,260km. Primary bbox (-52° to -22° lon, -58° to -38° lat) drawn and queried multiple times, consistently returning 259 Tianwen-1 MoRIC images. Browser timeout errors encountered (15s timeout). Map viewport bbox returned global bounds (-180 to 180) instead of actual view during flights. Final flight to North Pole (0° lon, 90°N) at 4,038km altitude.
[2026-03-17 19:30] User redefined nanobot's identity as a "Mars Digital Assistant" (火星数字助手). Updated SOUL.md and MEMORY.md to reflect new role: enable natural language-driven Mars exploration through digital platform, eliminating need for traditional keyboard/mouse interactions. Key features documented: Mars-MCP (spatiotemporal search, semantic search), Map-MCP (zoom, viewport, draw bbox, query, render footprints, clear). Platform frontend: ~/studio/mars-frontend-vue/ (Vue.js). Goal: Make all platform functions accessible through natural language as user continues expanding MCP tools and custom skills.
[2026-03-17 15:34-18:59] Multiple map operations on Argyre Planitia and North Pole. Argyre bbox (-52° to -22° lon, -58° to -38° lat) drawn and queried 3 times (15:37, 17:18, 18:45) consistently returning 259 Tianwen-1 images. Flights to Argyre at (-37°, -48°) at various altitudes: 1km, 7,810km, 10,279km, 10,260km. Browser timeout error at 17:30 (15s timeout). North Pole flights: 4,038km at 18:44, 19,653km at 18:59. Viewport bbox continues to return global bounds (-180 to 180). North Polar Cap features: water ice and CO2 ice, ~1,000 km diameter.
[2026-03-17 19:29] User updated assistant persona to "Mars Digital Assistant" for digital Mars platform. The assistant helps users perform map operations via natural language instead of keyboard/mouse. Frontend code at ~/studio/mars-frontend-vue/. User plans to continuously improve MCP/Skill tools. [2026-03-17 19:31] SOUL.md, MEMORY.md, and HISTORY.md updated with new persona and platform context. [2026-03-17 19:34-19:35] Multiple map flights performed: South Pole (90°S), North Pole (90°N) at various altitudes (12,007km, 14,987km). Argyre Planitia bbox (-52°,-58°) to (-22°,-38°) drawn again, queried 259 Tianwen-1 MoRIC images and discovered 48 Mars CTX images.
[2026-03-17 19:44] User requested current map information in Chinese. Map-MCP returned global view from 26,673 km altitude at coordinates (-0.395°, -6.3716°). Viewport bbox again returned global bounds (-180 to 180) instead of actual view, confirming persistent bbox issue. This is highest recorded altitude in session.

View File

@@ -0,0 +1,146 @@
# Long-term Memory
This file stores important information that should persist across sessions.
## User Information
- User works with Mars spatial data and imagery analysis
- Interested in Martian geological features (craters, dunes, deltas)
- Uses both spatial queries and semantic search for Mars imagery
- Developer of a digital Mars platform with natural language interface
## Platform Context
- **Assistant Role**: Mars Digital Assistant (火星数字助手) for digital Mars platform
- **Mission**: Enable users to complete map operations and platform functions through natural language instead of traditional keyboard/mouse
- **Frontend Code**: Located at `~/studio/mars-frontend-vue/`
- **Vision**: User plans to continuously improve MCP and Skill tools to enable all platform functions through natural language
## Preferences
- Uses Chinese language for Mars data queries
- Prefers detailed geological analysis and feature descriptions
- Interested in scientific value and geological evolution of Martian features
- Natural language interaction style for all operations
## Project Context
### Available MCP Tools
**Mars-MCP Tools (Data Query):**
- `spatiotemporal_search` - Basic spatial range query for Mars imagery
- Parameters: spatial (lat_min, lat_max, lon_min, lon_max), product type
- Returns: image ID, filename, path, coordinates, capture time
- `spatiotemporal_semantic_search` - Spatial + semantic search using natural language
- Returns images with similarity scores (cosine similarity 0-1) and detailed visual descriptions
- Supports queries like "撞击坑" (craters), "沙丘" (dunes), "三角洲" (deltas)
**Map-MCP Tools (Visualization):**
- `map_zoom_to` - Fly to specified Mars coordinates (lon, lat, altitude)
- `map_get_viewport` - Get current map viewport information
- `map_draw_bbox` - Draw bounding box and fly to region
- `map_render_footprints` - Render GeoJSON footprint data
- `map_clear` - Clear AI-drawn content
- `map_get_drawn_geometry` - Get hand-drawn geometry
- **Note**: No layer switching capability available
### Data Sources
- **Tianwen-1 MoRIC imagery** (`tianwen_moric`): High-resolution Mars imagery from Chinese Tianwen-1 mission
- Resolution: ~230 meters/pixel
- Image path format: `/mars_data/tianwenmoric_stamp/...`
- Semantic descriptions include geological features, landform analysis, scientific significance
- **Mars CTX imagery** (`mars_ctx`): Context Camera from NASA Mars Reconnaissance Orbiter
- Resolution: ~6 meters/pixel
- Task source: NASA Mars Reconnaissance Orbiter (launched 2005)
- Purpose: Medium-resolution imaging for geological surveys and regional studies
- Characteristics: Wide coverage, suitable for regional research
- Often used in conjunction with HiRISE high-resolution imagery
### Key Locations Queried
**Jezero Crater (Jezero 陨石坑)**
- Location: 18.4°N, 77.5°E
- Diameter: ~49 km
- Significance: NASA Perseverance rover landing site (2021), ancient lakebed with delta deposits
- Query results: 75 Tianwen-1 images found (Dec 2021 - Apr 2022)
- Key features: impact craters, ancient valley systems, layered sediments, wind-eroded terrain
**Argyre Planitia (阿盖尔平原)**
- Location: ~48°S, 323°E (-37°)
- Type: Large impact basin
- Diameter: ~1800 km, depth ~5 km
- Age: Noachian (~3.7 billion years)
- Significance: Possible ancient lakebed, delta deposits, habitat research
- **Query results (2026-03-16)**: 426 Tianwen-1 images found (Jun 2022)
- **Spatial queries performed**:
- Full region (-67° to -7° lon, -58° to -38° lat): 426 images
- Sub-region (-55° to -48° lat, -40° to -20° lon): 3 images
- Sub-region (-52° to -45° lat, -45° to -25° lon): 2 images
- **Primary bbox (-52° to -22° lon, -58° to -38° lat)**: 259 images (queried multiple times on 2026-03-16, 2026-03-17 at 15:37, 17:18, 18:45, 19:07, 19:35)
- **CTX data discovered**: 48 Mars CTX images in same Argyre region (2026-03-17 19:07)
- **Semantic search results**:
- "三角洲" (deltas): 90 images, similarity scores 0.3258, 0.3071, 0.3044
- "古湖床" (ancient lakebed): 102 images, similarity scores 0.371, 0.3583
- Note: Some semantic descriptions showed anomalies (orange geometric placeholders)
- **Bounding boxes drawn**:
- First: (-67°, -58°) to (-7°, -38°) - 2026-03-16
- Second: (-52°, -58°) to (-22°, -38°) - 2026-03-17 (drawn and queried 5+ times)
- **Map operations (2026-03-17)**: Multiple flights to coordinates (-37°, -48°) at various altitudes:
- Ground level altitude (~1km) at 15:34
- 7,810 km at 15:36
- 10,279 km at 17:30
- 10,260 km at 17:34
- 13,260 km (failed due to timeout) at 17:30
- 12,007 km (North Pole) at 19:34
- 14,987 km (before North Pole flight) at 19:33
**North Pole (北极点)**
- Location: 0° lon, 90°N
- Query date: 2026-03-17 18:44, 18:59, 19:06, 19:34
- Flight altitudes: 4,038 km, 19,653 km, 9,886 km, 12,007 km
- Features: Mars North Polar Cap (water ice and CO2 ice), ~1,000 km diameter
**South Pole (南极点)**
- Location: 0° lon, 90°S
- Query date: 2026-03-17 19:04
- Flight altitudes: 4,218 km
- Features: Mars South Polar Cap, ~350 km diameter (permanent), up to 1,000 km (seasonal)
**Global View (全球视野)**
- Location: -0.395° lon, -6.3716° lat (equatorial region, southern hemisphere)
- Query date: 2026-03-17 19:44
- Flight altitude: 26,673 km (highest recorded in session)
- Status: Global overhead view of entire Mars surface
### Known Issues
- Map-MCP 3D view initialization can fail intermittently (error: "3D 视图未初始化")
- Semantic search similarity scores vary by feature prevalence in region (e.g., "dunes" scored 0.27, "craters" scored 0.45)
- Browser connection issues can occur intermittently (timeout errors, 15s timeout encountered on 2026-03-17 at 17:30)
- Some semantic descriptions in Argyre Planitia results showed anomalies (orange geometric placeholders instead of geological descriptions)
- **Map-MCP viewport bbox coordinates frequently return global bounds (-180 to 180) instead of actual view** (confirmed multiple times on 2026-03-17, including 19:44)
- Argyre Planitia bbox (-52°, -58°) to (-22°, -38°) drawn and queried 5+ times with consistent 259 results
- Map altitude parameter is not always preserved during flights (actual altitude may differ from requested)
## Important Notes
- Semantic search similarity scores: higher is better (1.0 = perfect match), but scores vary by feature abundance
- Visual descriptions from semantic search include detailed geological analysis (features, landforms, scientific significance)
- Map altitudes used: 100km (detailed view), 4000+km (regional overview), 10,000-23,000km (global/regional)
- Map-MCP viewport bbox coordinates frequently return global bounds (-180 to 180) instead of actual view - persistent issue
- Argyre Planitia data is primarily from June 2022 Tianwen-1 observations
- Some semantic descriptions may be incomplete or anomalous (placeholders)
- Primary Argyre Planitia bbox for repeated queries: (-52°, -58°) to (-22°, -38°), returns 259 Tianwen-1 images + 48 Mars CTX images
- Browser timeout issues can interrupt map operations (15s timeout)
- North Polar Cap composition: water ice and CO2 ice, ~1,000 km diameter
- South Polar Cap: ~350 km diameter permanent, up to 1,000 km seasonal expansion
- Highest recorded altitude: 26,673 km at global view (2026-03-17 19:44)
- Assistant is a Mars Digital Assistant enabling natural language interaction for all platform operations
- Frontend code repository: `~/studio/mars-frontend-vue/`
---
*This file is automatically updated by nanobot when important information should be remembered.*