first commit
This commit is contained in:
16
Dockerfile
Normal file
16
Dockerfile
Normal 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
24
docker-compose.yml
Normal 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
6
nanobot-channel-web/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info/
|
||||
.venv/
|
||||
94
nanobot-channel-web/README.md
Normal file
94
nanobot-channel-web/README.md
Normal 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`.
|
||||
3
nanobot-channel-web/nanobot_channel_web/__init__.py
Normal file
3
nanobot-channel-web/nanobot_channel_web/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .channel import WebChannel
|
||||
|
||||
__all__ = ["WebChannel"]
|
||||
327
nanobot-channel-web/nanobot_channel_web/channel.py
Normal file
327
nanobot-channel-web/nanobot_channel_web/channel.py
Normal 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()
|
||||
22
nanobot-channel-web/pyproject.toml
Normal file
22
nanobot-channel-web/pyproject.toml
Normal 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
315
nanobot-config/config.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
nanobot-config/cron/jobs.json
Normal file
4
nanobot-config/cron/jobs.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 1,
|
||||
"jobs": []
|
||||
}
|
||||
21
nanobot-config/workspace/AGENTS.md
Normal file
21
nanobot-config/workspace/AGENTS.md
Normal 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.
|
||||
16
nanobot-config/workspace/HEARTBEAT.md
Normal file
16
nanobot-config/workspace/HEARTBEAT.md
Normal 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 -->
|
||||
|
||||
61
nanobot-config/workspace/SOUL.md
Normal file
61
nanobot-config/workspace/SOUL.md
Normal 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.
|
||||
15
nanobot-config/workspace/TOOLS.md
Normal file
15
nanobot-config/workspace/TOOLS.md
Normal 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.
|
||||
49
nanobot-config/workspace/USER.md
Normal file
49
nanobot-config/workspace/USER.md
Normal 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.*
|
||||
16
nanobot-config/workspace/memory/HISTORY.md
Normal file
16
nanobot-config/workspace/memory/HISTORY.md
Normal 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.
|
||||
|
||||
146
nanobot-config/workspace/memory/MEMORY.md
Normal file
146
nanobot-config/workspace/memory/MEMORY.md
Normal 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.*
|
||||
Reference in New Issue
Block a user