feat(outlook): 添加Outlook集成功能支持

添加完整的Outlook MCP集成,包括邮件和日历功能,通过AuthZ模式进行认证和权限管理,
支持邮箱连接、断开、状态检查和数据同步等功能。

fix(config): 统一配置文件路径从.nanobot到.beaver

将配置文件路径从/root/.nanobot统一更改为/root/.beaver,更新Dockerfile中的环境变量定义,
确保所有组件使用一致的配置目录结构。

feat(agent): 添加代理删除功能和助手身份提示

为代理注册表添加delete_agent方法,实现代理的动态删除功能;同时添加海狸助手身份提示,
确保AI助手在交互中保持一致的身份认知。

feat(engine): 增强引擎循环并添加意图决策快照

扩展AgentLoop类,添加intent_agent_decision参数用于意图驱动的代理决策,并在会话中记录
决策快照,便于后续分析和调试。

feat(authz): 扩展认证客户端功能

为AuthzClient添加设置权限、用户注册、后端注册和Outlook设置管理等新方法,增强系统
的认证和授权能力。
This commit is contained in:
2026-05-14 16:01:46 +08:00
parent 30ab74ffb2
commit ebfa242862
35 changed files with 3979 additions and 462 deletions

View File

@ -548,8 +548,13 @@ class AgentService:
provider=router_provider,
model=getattr(router_runtime, "model", None),
recent_messages=session_manager.get_messages_as_conversation(session_id),
intent_skill=self._load_intent_agent_skill(loaded),
thinking_enabled=kwargs.get("thinking_enabled"),
)
kwargs["intent_agent_decision"] = self._intent_decision_payload(
decision,
active_task=active_task,
)
if active_task is not None and decision.short_title and not active_task.metadata.get("short_title"):
active_task.metadata["short_title"] = decision.short_title
task_service.store.upsert_task(active_task)
@ -796,6 +801,28 @@ class AgentService:
raise RuntimeError(f"Engine loader did not provide required dependency {field_name!r}")
return value
@staticmethod
def _load_intent_agent_skill(loaded: Any) -> str | None:
skills_loader = getattr(loaded, "skills_loader", None)
if skills_loader is None:
return None
return skills_loader.load_skill("intent-agent-router")
@staticmethod
def _intent_decision_payload(decision: Any, *, active_task: TaskRecord | None) -> dict[str, Any]:
action = decision.action or ("create_task" if decision.is_task and active_task is None else decision.mode)
return {
"agent": "intent_agent",
"choice": action,
"mode": "task" if decision.is_task else "simple",
"reason": decision.reason,
"active_task_id": active_task.task_id if active_task is not None else None,
"starts_new_task": bool(decision.starts_new_task or (decision.is_task and active_task is None)),
"closes_task": bool(decision.closes_task),
"abandons_task": bool(decision.abandons_task),
"short_title": decision.short_title,
}
@staticmethod
def _skill_names_for_run(loaded: Any, run_id: str) -> list[str]:
store = getattr(loaded, "run_memory_store", None)

View File

@ -68,6 +68,34 @@ class SkillHubService:
files = []
return {"detail": detail, "files": files}
async def versions(self, namespace: str, slug: str) -> dict[str, Any]:
namespace = namespace.removeprefix("@")
payload = _unwrap(await self._get_json(f"/skills/{namespace}/{slug}/versions"))
if not isinstance(payload, dict):
payload = {}
items = payload.get("items")
return {
"items": items if isinstance(items, list) else [],
"total": int(payload.get("total") or len(items or [])),
"page": int(payload.get("page") or 0),
"size": int(payload.get("size") or len(items or [])),
}
async def file_content(self, namespace: str, slug: str, version: str, file_path: str) -> dict[str, Any]:
namespace = namespace.removeprefix("@")
safe_path = _safe_posix_path(file_path)
content = await self._get_text(
f"/skills/{namespace}/{slug}/versions/{version}/file",
params={"path": safe_path},
)
return {
"filePath": safe_path,
"content": content,
"contentType": _guess_content_type(safe_path),
"isBinary": False,
"fileSize": len(content.encode("utf-8")),
}
async def install(self, namespace: str, slug: str, version: str | None = None) -> dict[str, Any]:
namespace = namespace.removeprefix("@")
skill = await self.detail(namespace, slug)
@ -246,3 +274,14 @@ def _render_skill_content(frontmatter: dict[str, Any], body: str) -> str:
lines.append(f"{key}: {value}")
lines.extend(["---", "", body.strip()])
return "\n".join(lines).rstrip() + "\n"
def _guess_content_type(file_path: str) -> str:
lower = file_path.lower()
if lower.endswith(".md"):
return "text/markdown"
if lower.endswith(".json"):
return "application/json"
if lower.endswith((".txt", ".yaml", ".yml", ".toml", ".csv", ".log")):
return "text/plain"
return "text/plain"