ForcePilot/backend/package/yuxi/channels/adapters/dingding/tools/bitable.py
Kris 9c01d480cc feat(dingding): 完整实现钉钉适配器功能模块
- 新增钉钉工具调用审计日志功能
- 新增参数校验装饰器与基础工具集(文档、审批、多维表格)
- 完善事件类型映射与会话ID生成逻辑
- 新增消息去重、访问策略匹配、配置校验与UI提示
- 新增酷应用卡片、目录管理、表情反应支持
- 优化消息发送逻辑,添加401重试机制
- 新增配置化的权限控制与流式输出支持
2026-05-13 16:06:10 +08:00

274 lines
9.1 KiB
Python

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)