feat(outlook): 添加 Outlook MCP 集成支持并优化分页功能

- 新增 NANO_OUTLOOK_MCP_URL 和 NANO_OUTLOOK_MCP_SERVER_ID 环境变量配置
- 实现 Outlook 邮件和日历的分页查询功能,添加安全参数验证
- 为 app-instance 创建脚本添加 Outlook MCP 服务器 ID 参数
- 更新前端 Outlook 页面实现邮件列表和日历事件的分页浏览
- 添加 Git 忽略文件配置和 Docker 挂载路径修复

BREAKING CHANGE: Outlook 集成现在需要配置 MCP URL 和服务器 ID 环境变量
This commit is contained in:
2026-03-16 17:01:58 +08:00
parent 04501fea22
commit b3767dd4ab
20 changed files with 1671 additions and 83 deletions

View File

@ -41,6 +41,8 @@ DEFAULT_MODEL = os.environ.get("APP_INSTANCE_MODEL", "openai/gpt-5").strip()
DEFAULT_API_KEY = os.environ.get("APP_INSTANCE_API_KEY", "").strip()
DEFAULT_API_BASE = os.environ.get("APP_INSTANCE_API_BASE", "").strip()
DEFAULT_AUTHZ_BASE_URL = os.environ.get("DEFAULT_AUTHZ_BASE_URL", "").strip()
DEFAULT_AUTHZ_OUTLOOK_MCP_URL = os.environ.get("DEFAULT_AUTHZ_OUTLOOK_MCP_URL", "").strip()
DEFAULT_OUTLOOK_MCP_SERVER_ID = os.environ.get("DEFAULT_OUTLOOK_MCP_SERVER_ID", "outlook_mcp").strip() or "outlook_mcp"
PUBLIC_SCHEME = os.environ.get("DEPLOY_PUBLIC_SCHEME", "http").strip() or "http"
PUBLIC_BASE_DOMAIN = os.environ.get("DEPLOY_PUBLIC_BASE_DOMAIN", "127.0.0.1.nip.io").strip()
PUBLIC_HOST_TEMPLATE = os.environ.get("DEPLOY_PUBLIC_HOST_TEMPLATE", "{slug}.{base_domain}").strip()
@ -48,6 +50,7 @@ PUBLIC_PORT = int(os.environ.get("DEPLOY_PUBLIC_PORT", "8088").strip() or "8088"
AUTO_START_PROXY = os.environ.get("DEPLOY_AUTO_START_PROXY", "1").strip() not in {"0", "false", "False"}
HEALTH_TIMEOUT_SECONDS = float(os.environ.get("DEPLOY_HEALTH_TIMEOUT_SECONDS", "60").strip() or "60")
HEALTH_INTERVAL_SECONDS = float(os.environ.get("DEPLOY_HEALTH_INTERVAL_SECONDS", "1").strip() or "1")
INSTANCE_INTERNAL_PORT = int(os.environ.get("APP_INSTANCE_INTERNAL_PORT", "8080").strip() or "8080")
SERVER_HOST = os.environ.get("DEPLOY_CONTROL_HOST", "0.0.0.0").strip() or "0.0.0.0"
SERVER_PORT = int(os.environ.get("DEPLOY_CONTROL_PORT", "8090").strip() or "8090")
@ -154,22 +157,37 @@ def build_public_url(host: str) -> str:
return f"{PUBLIC_SCHEME}://{netloc}"
def build_internal_api_base_url(record: dict[str, Any]) -> str:
container_name = str(record.get("container_name", "") or "").strip()
if container_name:
return f"http://{container_name}:{INSTANCE_INTERNAL_PORT}"
fallback = str(record.get("api_base_url", "") or record.get("public_url", "") or "").strip()
return fallback
def wait_for_backend(record: dict[str, Any]) -> None:
host_port = int(record.get("host_port", 0) or 0)
if host_port <= 0:
raise ApiError(HTTPStatus.BAD_GATEWAY, "instance host port missing from registry")
container_name = str(record.get("container_name", "") or "").strip()
targets: list[str] = []
if container_name:
targets.append(f"http://{container_name}:{INSTANCE_INTERNAL_PORT}/api/ping")
if host_port > 0:
targets.append(f"http://127.0.0.1:{host_port}/api/ping")
if not targets:
raise ApiError(HTTPStatus.BAD_GATEWAY, "instance health target missing from registry")
deadline = time.time() + HEALTH_TIMEOUT_SECONDS
target = f"http://127.0.0.1:{host_port}/api/ping"
last_error = "backend not ready"
while time.time() < deadline:
try:
with urllib_request.urlopen(target, timeout=5) as response:
payload = json.loads(response.read().decode("utf-8"))
if payload.get("message") == "pong":
return
last_error = f"unexpected ping response from {target}"
except (urllib_error.URLError, TimeoutError, json.JSONDecodeError) as exc:
last_error = str(exc)
for target in targets:
try:
with urllib_request.urlopen(target, timeout=5) as response:
payload = json.loads(response.read().decode("utf-8"))
if payload.get("message") == "pong":
return
last_error = f"unexpected ping response from {target}"
except (urllib_error.URLError, TimeoutError, json.JSONDecodeError) as exc:
last_error = f"{target}: {exc}"
time.sleep(HEALTH_INTERVAL_SECONDS)
raise ApiError(HTTPStatus.BAD_GATEWAY, f"instance health check failed: {last_error}")
@ -197,6 +215,9 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
api_key = str(payload.get("api_key", "") or DEFAULT_API_KEY).strip()
api_base = str(payload.get("api_base", "") or DEFAULT_API_BASE).strip()
authz_base_url = str(payload.get("authz_base_url", "") or DEFAULT_AUTHZ_BASE_URL).strip()
authz_outlook_mcp_url = str(
payload.get("authz_outlook_mcp_url", "") or DEFAULT_AUTHZ_OUTLOOK_MCP_URL
).strip()
backend_name = str(payload.get("backend_name", "") or username).strip() or username
image_name = str(payload.get("image_name", "") or INSTANCE_IMAGE).strip() or INSTANCE_IMAGE
@ -236,6 +257,9 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
command.extend(["--api-base", api_base])
if authz_base_url:
command.extend(["--authz-base-url", authz_base_url])
if authz_outlook_mcp_url:
command.extend(["--authz-outlook-mcp-url", authz_outlook_mcp_url])
command.extend(["--outlook-mcp-server-id", DEFAULT_OUTLOOK_MCP_SERVER_ID])
if payload.get("replace") is True:
command.append("--replace")
@ -254,7 +278,7 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
"instance": existing,
"public_url": str(existing.get("public_url", "") or ""),
"frontend_base_url": str(existing.get("frontend_base_url", "") or existing.get("public_url", "") or ""),
"api_base_url": str(existing.get("api_base_url", "") or existing.get("public_url", "") or ""),
"api_base_url": build_internal_api_base_url(existing),
}
@ -269,7 +293,7 @@ def resolve_instance(payload: dict[str, Any]) -> dict[str, Any]:
"instance": record,
"public_url": str(record.get("public_url", "") or ""),
"frontend_base_url": str(record.get("frontend_base_url", "") or record.get("public_url", "") or ""),
"api_base_url": str(record.get("api_base_url", "") or record.get("public_url", "") or ""),
"api_base_url": build_internal_api_base_url(record),
}