from __future__ import annotations import logging from typing import TYPE_CHECKING, Any import httpx from ._validate import validate_params if TYPE_CHECKING: from yuxi.channels.adapters.dingding.token import DingDingTokenManager logger = logging.getLogger(__name__) DINGTALK_API_BASE = "https://api.dingtalk.com" @validate_params async def list_bitable_tables( token_manager: DingDingTokenManager, app_token: str, *, http_client: httpx.AsyncClient | None = None, ) -> list[dict[str, Any]]: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/bitable/apps/{app_token}/tables" headers = {"x-acs-dingtalk-access-token": token} try: resp = await _get(http_client, url, headers) if resp.status_code != 200: raise RuntimeError(f"获取多维表列表失败: HTTP {resp.status_code}: {resp.text[:200]}") data = resp.json() items = data.get("items", data.get("tableList", [])) return [ { "table_id": item.get("tableId", item.get("id", "")), "name": item.get("tableName", item.get("name", "")), "revision": item.get("revision", 0), } for item in items ] except Exception as e: logger.error(f"[DingDing Bitable] list_bitable_tables failed: {e}") raise RuntimeError(f"获取多维表列表失败: {e}") from e @validate_params async def get_bitable_fields( token_manager: DingDingTokenManager, app_token: str, table_id: str, *, http_client: httpx.AsyncClient | None = None, ) -> list[dict[str, Any]]: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/bitable/apps/{app_token}/tables/{table_id}/fields" headers = {"x-acs-dingtalk-access-token": token} try: resp = await _get(http_client, url, headers) if resp.status_code != 200: raise RuntimeError(f"获取字段列表失败: HTTP {resp.status_code}: {resp.text[:200]}") data = resp.json() items = data.get("items", data.get("fields", [])) return [ { "field_id": item.get("fieldId", item.get("id", "")), "field_name": item.get("fieldName", item.get("name", "")), "type": item.get("type", item.get("fieldType", "")), "property": item.get("property", {}), "is_primary": item.get("isPrimary", item.get("primary", False)), } for item in items ] except Exception as e: logger.error(f"[DingDing Bitable] get_bitable_fields failed: {e}") raise RuntimeError(f"获取字段列表失败: {e}") from e @validate_params async def list_bitable_records( token_manager: DingDingTokenManager, app_token: str, table_id: str, *, page_size: int = 100, page_token: str = "", filter_formula: str = "", http_client: httpx.AsyncClient | None = None, ) -> dict[str, Any]: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/bitable/apps/{app_token}/tables/{table_id}/records" headers = {"x-acs-dingtalk-access-token": token} params: dict[str, Any] = { "pageSize": min(page_size, 200), } if page_token: params["pageToken"] = page_token if filter_formula: params["filter"] = filter_formula try: resp = await _get(http_client, url, headers, params=params) if resp.status_code != 200: raise RuntimeError(f"获取记录列表失败: HTTP {resp.status_code}: {resp.text[:200]}") data = resp.json() items = data.get("items", data.get("records", [])) records = [ { "record_id": item.get("recordId", item.get("id", "")), "fields": item.get("fields", {}), "created_time": item.get("createTime", item.get("createdTime", "")), "last_modified_time": item.get("updateTime", item.get("lastModifiedTime", "")), } for item in items ] return { "records": records, "has_more": data.get("hasMore", data.get("has_more", False)), "page_token": data.get("nextToken", data.get("pageToken", "")), "total": data.get("totalCount", data.get("total", len(records))), } except Exception as e: logger.error(f"[DingDing Bitable] list_bitable_records failed: {e}") raise RuntimeError(f"获取记录列表失败: {e}") from e @validate_params async def create_bitable_record( token_manager: DingDingTokenManager, app_token: str, table_id: str, fields: dict[str, Any], *, http_client: httpx.AsyncClient | None = None, ) -> dict[str, Any]: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/bitable/apps/{app_token}/tables/{table_id}/records" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload = {"fields": fields} try: resp = await _post(http_client, url, headers, payload) if resp.status_code != 200: raise RuntimeError(f"创建记录失败: HTTP {resp.status_code}: {resp.text[:200]}") data = resp.json() record = data.get("record", data.get("item", data)) return { "record_id": record.get("recordId", record.get("id", "")), "fields": record.get("fields", fields), } except Exception as e: logger.error(f"[DingDing Bitable] create_bitable_record failed: {e}") raise RuntimeError(f"创建记录失败: {e}") from e @validate_params async def update_bitable_record( token_manager: DingDingTokenManager, app_token: str, table_id: str, record_id: str, fields: dict[str, Any], *, http_client: httpx.AsyncClient | None = None, ) -> dict[str, Any]: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/bitable/apps/{app_token}/tables/{table_id}/records/{record_id}" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload = {"fields": fields} try: resp = await _put(http_client, url, headers, payload) if resp.status_code != 200: raise RuntimeError(f"更新记录失败: HTTP {resp.status_code}: {resp.text[:200]}") data = resp.json() record = data.get("record", data.get("item", data)) return { "record_id": record.get("recordId", record.get("id", record_id)), "fields": record.get("fields", fields), } except Exception as e: logger.error(f"[DingDing Bitable] update_bitable_record failed: {e}") raise RuntimeError(f"更新记录失败: {e}") from e @validate_params async def delete_bitable_record( token_manager: DingDingTokenManager, app_token: str, table_id: str, record_id: str, *, http_client: httpx.AsyncClient | None = None, ) -> dict[str, Any]: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/bitable/apps/{app_token}/tables/{table_id}/records/{record_id}" headers = {"x-acs-dingtalk-access-token": token} try: resp = await _delete(http_client, url, headers) if resp.status_code != 200: raise RuntimeError(f"删除记录失败: HTTP {resp.status_code}: {resp.text[:200]}") return { "deleted": True, "record_id": record_id, "app_token": app_token, "table_id": table_id, } except Exception as e: logger.error(f"[DingDing Bitable] delete_bitable_record failed: {e}") raise RuntimeError(f"删除记录失败: {e}") from e async def _get( http_client: httpx.AsyncClient | None, url: str, headers: dict[str, str], params: dict[str, Any] | None = None, ) -> httpx.Response: if http_client is not None: return await http_client.get(url, headers=headers, params=params) async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: return await client.get(url, headers=headers, params=params) async def _post( http_client: httpx.AsyncClient | None, url: str, headers: dict[str, str], payload: dict[str, Any], ) -> httpx.Response: if http_client is not None: return await http_client.post(url, json=payload, headers=headers) async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: return await client.post(url, json=payload, headers=headers) async def _put( http_client: httpx.AsyncClient | None, url: str, headers: dict[str, str], payload: dict[str, Any], ) -> httpx.Response: if http_client is not None: return await http_client.put(url, json=payload, headers=headers) async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: return await client.put(url, json=payload, headers=headers) async def _delete( http_client: httpx.AsyncClient | None, url: str, headers: dict[str, str], ) -> httpx.Response: if http_client is not None: return await http_client.delete(url, headers=headers) async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: return await client.delete(url, headers=headers)