- 新增钉钉工具调用审计日志功能 - 新增参数校验装饰器与基础工具集(文档、审批、多维表格) - 完善事件类型映射与会话ID生成逻辑 - 新增消息去重、访问策略匹配、配置校验与UI提示 - 新增酷应用卡片、目录管理、表情反应支持 - 优化消息发送逻辑,添加401重试机制 - 新增配置化的权限控制与流式输出支持
274 lines
9.1 KiB
Python
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)
|