"""Credential setup guides for collector integrations.""" from __future__ import annotations from dataclasses import dataclass from typing import Any from sqlalchemy import select from app.models.system_setting import SystemSetting from app.schemas.ai import SituationalAnalysisRequest from app.services.ai_client import AIProviderClient CREDENTIAL_GUIDES_CATEGORY = "collector_credential_guides" @dataclass(frozen=True) class CredentialGuideDefault: provider: str title: str prompt: str markdown: str BARENTSWATCH_DEFAULT_GUIDE = CredentialGuideDefault( provider="barentswatch", title="BarentsWatch AIS 凭证获取教程", prompt=( "请生成一份中文教程,指导开发者获取 BarentsWatch Live AIS API 的 " "OAuth client credentials。教程要面向已经有本地开发环境的人,包含注册/登录、" "创建 client、申请或确认 ais scope、复制 client id 和 client secret、" "在系统设置中填写并验证连接、常见失败排查。不要编造具体页面按钮文案," "必须参考官方 tutorial:https://developer.barentswatch.no/docs/tutorial 。" "必须强调 Live AIS 要选择 AIS-client / AIS - API,而不是普通 API-client。" "如果步骤可能变化,要提醒以 BarentsWatch developer portal 当前页面为准。" ), markdown="""## BarentsWatch AIS 凭证获取 官方教程:https://developer.barentswatch.no/docs/tutorial 1. 先打开上面的 BarentsWatch 官方 tutorial,按官方流程登录或注册开发者账号。 2. 在 Developer access 页面选择 `AIS - API`,不要选择普通的 `BarentsWatch - API`。 3. 在 `AIS - API` 下创建用于 Planet 的 AIS client。 4. 创建时记下你设置的 password / client secret。 5. 回到 My Page 复制完整 `Client ID`。它通常长得像 `your.email@example.com:client-name`。 6. 回到 Planet 的 `设置 -> 采集器设置 -> BarentsWatch AIS`,填入 `Client ID` 和 `Client Secret`。 7. 点击 `连接` 验证 token 和 AIS endpoint 是否可访问。 8. 连接成功后保存凭证。 ### 请求规则 - Token 地址:`https://id.barentswatch.no/connect/token` - 请求方式:`POST` - Content-Type:`application/x-www-form-urlencoded` - Body 必须包含:`grant_type=client_credentials`、`client_id`、`client_secret`、`scope=ais` - `client_id`、`client_secret`、`scope`、`grant_type` 都要放在 body,不要放在 header。 - AIS 数据请求使用 header:`Authorization: Bearer ` ### 常见排查 - `未找到凭证`:确认 `Client ID` 和 `Client Secret` 已填写,或已经写入 `~/.zshrc`。 - `HTTP 401/403`:通常是选成了普通 `BarentsWatch - API` client、client secret 错误,或 token 请求没有使用 `scope=ais`。 - `network` 错误:检查本机是否能访问 `id.barentswatch.no` 和 `live.ais.barentswatch.no`。 - Endpoint 建议保持默认:`https://live.ais.barentswatch.no/v1/latest/combined`。 """, ) DEFAULT_CREDENTIAL_GUIDES = { BARENTSWATCH_DEFAULT_GUIDE.provider: BARENTSWATCH_DEFAULT_GUIDE, } async def _get_guide_store(db) -> tuple[SystemSetting | None, dict[str, Any]]: result = await db.execute( select(SystemSetting).where(SystemSetting.category == CREDENTIAL_GUIDES_CATEGORY) ) record = result.scalar_one_or_none() payload = dict(record.payload or {}) if record and isinstance(record.payload, dict) else {} return record, payload async def get_credential_guide(db, provider: str) -> dict[str, Any]: default = DEFAULT_CREDENTIAL_GUIDES.get(provider) if default is None: raise ValueError(f"Unsupported credential guide provider: {provider}") _record, store = await _get_guide_store(db) custom = store.get(provider) if isinstance(store.get(provider), dict) else None return { "provider": provider, "title": custom.get("title") if custom else default.title, "markdown": custom.get("markdown") if custom else default.markdown, "prompt": default.prompt, "source": "ai" if custom else "default", } async def save_credential_guide(db, provider: str, title: str, markdown: str) -> dict[str, Any]: default = DEFAULT_CREDENTIAL_GUIDES.get(provider) if default is None: raise ValueError(f"Unsupported credential guide provider: {provider}") record, store = await _get_guide_store(db) store[provider] = { "title": title or default.title, "markdown": markdown, } if record is None: db.add(SystemSetting(category=CREDENTIAL_GUIDES_CATEGORY, payload=store)) else: record.payload = store await db.commit() return await get_credential_guide(db, provider) async def reset_credential_guide(db, provider: str) -> dict[str, Any]: default = DEFAULT_CREDENTIAL_GUIDES.get(provider) if default is None: raise ValueError(f"Unsupported credential guide provider: {provider}") record, store = await _get_guide_store(db) if provider in store: store.pop(provider, None) if record is not None: record.payload = store await db.commit() return await get_credential_guide(db, provider) async def generate_credential_guide( db, provider: str, ai_client: AIProviderClient, ) -> dict[str, Any]: default = DEFAULT_CREDENTIAL_GUIDES.get(provider) if default is None: raise ValueError(f"Unsupported credential guide provider: {provider}") response = await ai_client.analyze( SituationalAnalysisRequest( title=f"Generate credential guide for {provider}", objective=default.prompt, context={ "provider": provider, "current_default_guide": default.markdown, "product_context": "Planet collector credential settings", }, observations=[ "Use concise Chinese markdown.", "Prefer stable concepts over brittle UI labels.", "Include verification and troubleshooting steps.", ], constraints=[ "Do not ask the user for secrets.", "Do not include fabricated screenshots.", "Return markdown only.", ], ) ) markdown = response.content.strip() if not markdown: markdown = default.markdown return await save_credential_guide(db, provider, default.title, markdown)