166 lines
6.4 KiB
Python
166 lines
6.4 KiB
Python
"""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 <access_token>`
|
||
|
||
### 常见排查
|
||
|
||
- `未找到凭证`:确认 `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)
|