first commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
.git
|
||||||
|
README.md
|
||||||
24
.env.example
Normal file
24
.env.example
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 核心凭证配置 (必填)
|
||||||
|
APP_ID=your_app_id
|
||||||
|
APP_SECRET=your_app_secret
|
||||||
|
LARK_HOST=https://open.feishu.cn
|
||||||
|
|
||||||
|
# 单表查询配置 (get_records, batch_get_records)
|
||||||
|
DEFAULT_APP_TOKEN=your_default_app_token
|
||||||
|
DEFAULT_TABLE_ID=your_default_table_id
|
||||||
|
BATCH_TABLE_ID=your_batch_table_id
|
||||||
|
|
||||||
|
# 导出网页视图配置 (严格 JSON 数组格式)
|
||||||
|
WEB_VIEW_TABS=[{"name": "默认分组", "app_token": "your_app_token", "table_id": "your_table_id"}]
|
||||||
|
|
||||||
|
# AI 总结配置 (可选,用于 outline_view.html 生成摘要)
|
||||||
|
AI_API_KEY=your_ai_api_key
|
||||||
|
AI_BASE_URL=https://api.openai.com/v1
|
||||||
|
AI_MODEL=gpt-4o
|
||||||
|
|
||||||
|
# 多表合并功能配置 (可选)
|
||||||
|
MERGE_SOURCE_APP_TOKEN_1=source_token_1
|
||||||
|
MERGE_SOURCE_TABLE_ID_1=source_id_1
|
||||||
|
MERGE_SOURCE_APP_TOKEN_2=source_token_2
|
||||||
|
MERGE_SOURCE_TABLE_ID_2=source_id_2
|
||||||
|
MERGE_TARGET_APP_TOKEN=target_token
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
.env
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
WORKDIR /home/app
|
||||||
|
|
||||||
|
# If we add the requirements and install dependencies first, docker can use cache if requirements don't change
|
||||||
|
ADD requirements.txt /home/app
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
ADD . /home/app
|
||||||
|
|
||||||
|
# Run the command on container startup
|
||||||
|
CMD python3 bitable_calendar.py
|
||||||
|
|
||||||
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# 基于多维表格的敏捷项目周期管理
|
||||||
|
|
||||||
|
[使用说明](https://open.feishu.cn/document/home/agile-project-cycle-management-based-on-bitable/introduction)
|
||||||
|
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
本项目依赖环境变量来进行飞书应用授权和数据表定位。你需要根据 `.env.example`(或直接新建一个 `.env` 文件),配置以下环境变量:
|
||||||
|
|
||||||
|
### 核心凭证配置 (必填)
|
||||||
|
* `APP_ID`: 你的飞书应用的 App ID。
|
||||||
|
* `APP_SECRET`: 你的飞书应用的 App Secret。
|
||||||
|
* `LARK_HOST`: 飞书开放平台 API 地址,默认为 `https://open.feishu.cn`。
|
||||||
|
|
||||||
|
### 单表查询配置 (get_records, batch_get_records)
|
||||||
|
* `DEFAULT_APP_TOKEN`: 默认读取的飞书多维表格的 App Token。
|
||||||
|
* `DEFAULT_TABLE_ID`: `get_records.py` 默认查询的 Table ID。
|
||||||
|
* `BATCH_TABLE_ID`: `batch_get_records.py` 默认批量查询的 Table ID。
|
||||||
|
|
||||||
|
### 导出可视化视图配置 (export_web_view)
|
||||||
|
* `WEB_VIEW_TABS`: 导出 HTML 时使用的多标签页配置,必须为严格的 **JSON 数组字符串**。例如:
|
||||||
|
`[{"name": "开发组", "app_token": "token1", "table_id": "id1"}, {"name": "测试组", "app_token": "token2", "table_id": "id2"}]`
|
||||||
|
* **AI 总结配置**(可选,用于网页自带的 AI 分板):
|
||||||
|
* `AI_API_KEY`: 大模型 API Key。
|
||||||
|
* `AI_BASE_URL`: 大模型 API 请求 Base URL。
|
||||||
|
* `AI_MODEL`: 大模型模型名称(如 `gpt-4o`, `MiniMaxAI` 等)。
|
||||||
|
|
||||||
|
### 多表合并功能配置 (merge_bitables, append_bitables)
|
||||||
|
* `MERGE_SOURCE_APP_TOKEN_1` / `MERGE_SOURCE_TABLE_ID_1`: 第一个数据源。
|
||||||
|
* `MERGE_SOURCE_APP_TOKEN_2` / `MERGE_SOURCE_TABLE_ID_2`: 第二个数据源。
|
||||||
|
* `MERGE_TARGET_APP_TOKEN`: 数据合并后的目标 App Token。
|
||||||
|
|
||||||
|
## 快速开始模板 (`.env` 文件配置)
|
||||||
|
你可以直接在项目根目录创建一个 `.env` 文件,然后复制以下内容并替换成你的真实配置:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 核心凭证配置 (必填)
|
||||||
|
APP_ID=your_app_id
|
||||||
|
APP_SECRET=your_app_secret
|
||||||
|
LARK_HOST=https://open.feishu.cn
|
||||||
|
|
||||||
|
# 单表查询配置 (get_records, batch_get_records)
|
||||||
|
DEFAULT_APP_TOKEN=your_default_app_token
|
||||||
|
DEFAULT_TABLE_ID=your_default_table_id
|
||||||
|
BATCH_TABLE_ID=your_batch_table_id
|
||||||
|
|
||||||
|
# 导出网页视图配置 (严格 JSON 数组格式)
|
||||||
|
WEB_VIEW_TABS=[{"name": "默认分组", "app_token": "your_app_token", "table_id": "your_table_id"}]
|
||||||
|
|
||||||
|
# AI 总结配置 (可选,用于 outline_view.html 生成摘要)
|
||||||
|
AI_API_KEY=your_ai_api_key
|
||||||
|
AI_BASE_URL=https://api.openai.com/v1
|
||||||
|
AI_MODEL=gpt-4o
|
||||||
|
|
||||||
|
# 多表合并功能配置 (可选)
|
||||||
|
MERGE_SOURCE_APP_TOKEN_1=source_token_1
|
||||||
|
MERGE_SOURCE_TABLE_ID_1=source_id_1
|
||||||
|
MERGE_SOURCE_APP_TOKEN_2=source_token_2
|
||||||
|
MERGE_SOURCE_TABLE_ID_2=source_id_2
|
||||||
|
MERGE_TARGET_APP_TOKEN=target_token
|
||||||
|
```
|
||||||
|
## 快速开始
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
uv run export_web_view.py
|
||||||
|
uv run server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
服务启动后,可以通过浏览器直接访问查看交互式数据看板:
|
||||||
|
🔗 **[http://localhost:18080](http://localhost:18080)**
|
||||||
237
api.py
Normal file
237
api.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
from utils import request
|
||||||
|
import sys
|
||||||
|
if sys.version_info.minor >= 8:
|
||||||
|
from typing import Literal
|
||||||
|
else:
|
||||||
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
|
||||||
|
class Client(object):
|
||||||
|
def __init__(self, lark_host):
|
||||||
|
self._host = lark_host
|
||||||
|
|
||||||
|
def get_tenant_access_token(self, app_id, app_secret):
|
||||||
|
url = self._host+"/open-apis/auth/v3/app_access_token/internal/"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
'app_id': app_id,
|
||||||
|
'app_secret': app_secret
|
||||||
|
}
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['tenant_access_token']
|
||||||
|
|
||||||
|
def get_records(self, access_token: str, app_token: str, table_id: str, view_id: str=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if view_id: params['view_id'] = view_id
|
||||||
|
resp = request("GET", url, headers, params=params)
|
||||||
|
return resp['data']['items']
|
||||||
|
|
||||||
|
|
||||||
|
def create_calendar(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
summary: str=None,
|
||||||
|
description: str=None,
|
||||||
|
permissions: Literal["private", "show_only_free_busy", "public"]=None,
|
||||||
|
color: int=None,
|
||||||
|
summary_alias: str=None):
|
||||||
|
url = f"{self._host}/open-apis/calendar/v4/calendars"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {}
|
||||||
|
if summary: payload['summary'] = summary
|
||||||
|
if description: payload['description'] = description
|
||||||
|
if permissions: payload['permissions'] = permissions
|
||||||
|
if color: payload['color'] = color
|
||||||
|
if summary_alias: payload['summary_alias'] = summary_alias
|
||||||
|
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['data']['calendar']
|
||||||
|
|
||||||
|
def create_event(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
calendar_id: str,
|
||||||
|
start_time: datetime.datetime,
|
||||||
|
end_time: datetime.datetime,
|
||||||
|
vchat=None,
|
||||||
|
location=None,
|
||||||
|
reminders=None,
|
||||||
|
schemas=None,
|
||||||
|
summary: str=None,
|
||||||
|
description: str=None,
|
||||||
|
need_notification: bool=None,
|
||||||
|
visibility: Literal['default', 'public', 'private']=None,
|
||||||
|
attendee_ability: Literal['none', 'can_see_others', 'can_invite_others', 'can_modify_event']=None,
|
||||||
|
free_busy_status: Literal['busy', 'free']=None,
|
||||||
|
color: int=None,
|
||||||
|
recurrence: str=None,
|
||||||
|
):
|
||||||
|
url = f"{self._host}/open-apis/calendar/v4/calendars/{calendar_id}/events"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
'start_time': {'timestamp': int(start_time.timestamp())},
|
||||||
|
'end_time': {'timestamp': int(end_time.timestamp())}
|
||||||
|
}
|
||||||
|
if vchat: payload['vchat'] = vchat
|
||||||
|
if location: payload['location'] = location
|
||||||
|
if reminders: payload['reminders'] = reminders
|
||||||
|
if schemas: payload['schemas'] = schemas
|
||||||
|
if summary: payload['summary'] = summary
|
||||||
|
if description: payload['description'] = description
|
||||||
|
if need_notification: payload['need_notification'] = need_notification
|
||||||
|
if visibility: payload['visibility'] = visibility
|
||||||
|
if attendee_ability: payload['attendee_ability'] = attendee_ability
|
||||||
|
if free_busy_status: payload['free_busy_status'] = free_busy_status
|
||||||
|
if color: payload['color'] = color
|
||||||
|
if recurrence: payload['recurrence'] = recurrence
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['data']['event']
|
||||||
|
|
||||||
|
def batch_create_records(self, access_token: str, app_token: str, table_id: str, records: List[dict]):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
'records': records
|
||||||
|
}
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['data']['records']
|
||||||
|
|
||||||
|
def get_fields_list(self, access_token: str, app_token: str, table_id: str, view_id: str=None, page_token: str=None, page_size: int=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
params = {}
|
||||||
|
if view_id: params['view_id'] = view_id
|
||||||
|
if page_token: params['page_token'] = page_token
|
||||||
|
if page_size: params['page_size'] = page_size
|
||||||
|
resp = request("GET", url, headers, params=params)
|
||||||
|
return resp['data']
|
||||||
|
|
||||||
|
def update_field(self, access_token: str, app_token: str, table_id: str, field_id: str, field_name: str, type: int, property: dict=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields/{field_id}"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
'field_name': field_name,
|
||||||
|
'type': type,
|
||||||
|
}
|
||||||
|
if property: payload['property'] = property
|
||||||
|
resp = request("PUT", url, headers, payload)
|
||||||
|
return resp['data']['field']
|
||||||
|
|
||||||
|
def get_records_list(self, access_token: str, app_token: str, table_id: str, view_id: str=None, page_token: str=None, page_size: int=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
params = {}
|
||||||
|
if view_id: params['view_id'] = view_id
|
||||||
|
if page_token: params['page_token'] = page_token
|
||||||
|
if page_size: params['page_size'] = page_size
|
||||||
|
resp = request("GET", url, headers, params=params)
|
||||||
|
return resp['data']
|
||||||
|
|
||||||
|
def get_tables_list(self, access_token: str, app_token: str, page_token: str=None, page_size: int=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
params = {}
|
||||||
|
if page_token: params['page_token'] = page_token
|
||||||
|
if page_size: params['page_size'] = page_size
|
||||||
|
resp = request("GET", url, headers, params=params)
|
||||||
|
return resp.get('data', {})
|
||||||
|
|
||||||
|
|
||||||
|
def batch_update_records(self, access_token: str, app_token: str, table_id: str, records: List[dict], user_id_type: str=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
'records': records
|
||||||
|
}
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['data']['records']
|
||||||
|
|
||||||
|
def create_table(self, access_token: str, app_token: str, table_name: str=None) -> str:
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {}
|
||||||
|
if table_name: payload['table'] = {'name': table_name}
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['data']['table_id']
|
||||||
|
|
||||||
|
def add_field(self, access_token: str, app_token: str, table_id: str, field_name: str, type: int, property: dict=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
'field_name': field_name,
|
||||||
|
'type': type
|
||||||
|
}
|
||||||
|
if property: payload['property'] = property
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
return resp['data']['field']
|
||||||
|
|
||||||
|
def batch_get_records(self, access_token: str, app_token: str, table_id: str, record_ids: List[str], user_id_type: str=None, with_shared_url: bool=None, automatic_fields: bool=None):
|
||||||
|
url = f"{self._host}/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_get"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Authorization': 'Bearer '+access_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
all_records = []
|
||||||
|
# The API supports a maximum of 100 record IDs per request.
|
||||||
|
for i in range(0, len(record_ids), 100):
|
||||||
|
chunk = record_ids[i:i + 100]
|
||||||
|
payload = {
|
||||||
|
'record_ids': chunk
|
||||||
|
}
|
||||||
|
if user_id_type is not None: payload['user_id_type'] = user_id_type
|
||||||
|
if with_shared_url is not None: payload['with_shared_url'] = with_shared_url
|
||||||
|
if automatic_fields is not None: payload['automatic_fields'] = automatic_fields
|
||||||
|
|
||||||
|
resp = request("POST", url, headers, payload)
|
||||||
|
if resp and 'data' in resp and 'records' in resp['data']:
|
||||||
|
all_records.extend(resp['data']['records'])
|
||||||
|
|
||||||
|
return all_records
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
130
append_bitables.py
Normal file
130
append_bitables.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
import utils
|
||||||
|
import copy
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
||||||
|
|
||||||
|
def get_all_records(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
records = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_records_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
records.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_fields(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
fields = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_fields_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
fields.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def append_to_existing_table(client: api.Client, access_token: str, source_tables: list, target_app_token: str, target_table_id: str):
|
||||||
|
"""
|
||||||
|
Appends data from multiple source tables into one existing target table.
|
||||||
|
Missing fields in the target table will be automatically added.
|
||||||
|
"""
|
||||||
|
logging.info(f"Appending records from {len(source_tables)} source tables to target {target_app_token} / {target_table_id}")
|
||||||
|
|
||||||
|
# 1. Read existing Target Table schema
|
||||||
|
target_fields = get_all_fields(client, access_token, target_app_token, target_table_id)
|
||||||
|
target_field_map = {f['field_name']: f['field_id'] for f in target_fields}
|
||||||
|
|
||||||
|
# Check if "Source" field exists, if not, create it
|
||||||
|
if "Source" not in target_field_map:
|
||||||
|
try:
|
||||||
|
resp = client.add_field(access_token, target_app_token, target_table_id, "Source", 1, None)
|
||||||
|
target_field_map["Source"] = resp['field_id']
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to create 'Source' field: {e}")
|
||||||
|
|
||||||
|
for source_app_token, source_table_id in source_tables:
|
||||||
|
logging.info(f"Processing source table {source_app_token} / {source_table_id}...")
|
||||||
|
|
||||||
|
# 2. Merge Missing Fields
|
||||||
|
source_fields = get_all_fields(client, access_token, source_app_token, source_table_id)
|
||||||
|
for f in source_fields:
|
||||||
|
name = f['field_name']
|
||||||
|
if name not in target_field_map:
|
||||||
|
ftype = f['type']
|
||||||
|
fprop = copy.deepcopy(f.get('property'))
|
||||||
|
# Clean up option IDs to prevent insertion errors in target table
|
||||||
|
if fprop and 'options' in fprop:
|
||||||
|
for opt in fprop['options']:
|
||||||
|
if 'id' in opt: del opt['id']
|
||||||
|
# Sometimes a field might be a reference to another table that isn't copied, so we might want to catch creation errors
|
||||||
|
try:
|
||||||
|
resp = client.add_field(access_token, target_app_token, target_table_id, name, ftype, fprop)
|
||||||
|
target_field_map[name] = resp['field_id']
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to create field '{name}' in target: {e}")
|
||||||
|
|
||||||
|
# 3. Read & Insert Records
|
||||||
|
records = get_all_records(client, access_token, source_app_token, source_table_id)
|
||||||
|
batch = []
|
||||||
|
source_name = f"{source_app_token}/{source_table_id}"
|
||||||
|
for r in records:
|
||||||
|
original_fields = r.get('fields', {})
|
||||||
|
new_fields = {}
|
||||||
|
for fname, fvalue in original_fields.items():
|
||||||
|
if fname in target_field_map:
|
||||||
|
new_fields[fname] = fvalue
|
||||||
|
new_fields['Source'] = source_name
|
||||||
|
batch.append({'fields': new_fields})
|
||||||
|
|
||||||
|
# Feishu limit is usually 500 per request
|
||||||
|
CHUNK_SIZE = 500
|
||||||
|
for i in range(0, len(batch), CHUNK_SIZE):
|
||||||
|
chunk = batch[i:i + CHUNK_SIZE]
|
||||||
|
if chunk:
|
||||||
|
try:
|
||||||
|
client.batch_create_records(access_token, target_app_token, target_table_id, chunk)
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to insert chunk of records: {e}")
|
||||||
|
|
||||||
|
logging.info(f"Appended records from {source_name} successfully.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not get access token: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
logging.info(f"Using App ID: {config.APP_ID}")
|
||||||
|
|
||||||
|
SOURCE_TABLES = [
|
||||||
|
(config.MERGE_SOURCE_APP_TOKEN_1, config.MERGE_SOURCE_TABLE_ID_1),
|
||||||
|
(config.MERGE_SOURCE_APP_TOKEN_2, config.MERGE_SOURCE_TABLE_ID_2),
|
||||||
|
# Add more source tables as needed: ("app_token", "table_id")
|
||||||
|
]
|
||||||
|
|
||||||
|
TARGET_APP_TOKEN = config.MERGE_TARGET_APP_TOKEN
|
||||||
|
TARGET_TABLE_ID = "target_table_id_to_append_to" # PLEASE UPDATE THIS TO THE EXISTING MERGED TABLE ID
|
||||||
|
|
||||||
|
is_placeholder = (config.MERGE_SOURCE_APP_TOKEN_1 == "source_app_token_1")
|
||||||
|
if is_placeholder:
|
||||||
|
logging.warning("Please update config.py or environment variables with valid MERGE_* tokens to execute.")
|
||||||
|
else:
|
||||||
|
append_to_existing_table(client, access_token, SOURCE_TABLES, TARGET_APP_TOKEN, TARGET_TABLE_ID)
|
||||||
58
batch_get_records.py
Normal file
58
batch_get_records.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
# Get tenant access token
|
||||||
|
try:
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not get access token: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use parameters from config or environment
|
||||||
|
APP_TOKEN = config.DEFAULT_APP_TOKEN
|
||||||
|
TABLE_ID = config.BATCH_TABLE_ID
|
||||||
|
|
||||||
|
# First, let's get some record IDs to demonstrate batch retrieval
|
||||||
|
# In a real scenario, you might already have these IDs.
|
||||||
|
logging.info(f"Fetching some record IDs from App Token: {APP_TOKEN}, Table ID: {TABLE_ID}")
|
||||||
|
try:
|
||||||
|
resp = client.get_records_list(access_token, APP_TOKEN, TABLE_ID, page_size=10)
|
||||||
|
records = resp.get('items', [])
|
||||||
|
record_ids = [r['record_id'] for r in records]
|
||||||
|
|
||||||
|
if not record_ids:
|
||||||
|
logging.warning("No records found in the table to perform batch retrieval.")
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info(f"Found {len(record_ids)} record IDs. Performing batch retrieval...")
|
||||||
|
|
||||||
|
# Performance batch retrieval
|
||||||
|
batch_records = client.batch_get_records(
|
||||||
|
access_token,
|
||||||
|
APP_TOKEN,
|
||||||
|
TABLE_ID,
|
||||||
|
record_ids,
|
||||||
|
with_shared_url=True,
|
||||||
|
automatic_fields=True
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info(f"Successfully retrieved {len(batch_records)} records via batch_get.")
|
||||||
|
|
||||||
|
if batch_records:
|
||||||
|
print("\nExample Batch Retrieved Record (First item):")
|
||||||
|
print(json.dumps(batch_records[0], ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error during batch record retrieval: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
108
bitable_calendar.py
Normal file
108
bitable_calendar.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
import utils
|
||||||
|
from mock import schema, raw_data, name_map
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.ERROR)
|
||||||
|
|
||||||
|
import os
|
||||||
|
logging.info(os.getcwd())
|
||||||
|
|
||||||
|
def rgb(r: int, g: int, b: int): return (r<<16) + (g<<8) + b
|
||||||
|
|
||||||
|
|
||||||
|
def sync_records_to_calendar(client: api.Client, access_token, calendar_id, app_token, table_id):
|
||||||
|
'''generate calendar events from bitable records'''
|
||||||
|
|
||||||
|
records = client.get_records(access_token, app_token, table_id)
|
||||||
|
for record in records:
|
||||||
|
try:
|
||||||
|
fields = record.get('fields')
|
||||||
|
version = fields.get(config.KEY_VERSION)
|
||||||
|
proposal_date = datetime.fromtimestamp(fields.get(config.KEY_STARTUP) / 1000)
|
||||||
|
delivery_date = datetime.fromtimestamp(fields.get(config.KEY_SUBMIT) / 1000)
|
||||||
|
expected_online_time = datetime.fromtimestamp(fields.get(config.KEY_GREY) / 1000)
|
||||||
|
online_time = datetime.fromtimestamp(fields.get(config.KEY_GA) / 1000)
|
||||||
|
print(version, proposal_date, delivery_date, expected_online_time, online_time)
|
||||||
|
print(client.create_event(access_token, calendar_id, start_time=proposal_date, end_time=delivery_date, summary=config.FORMATTER_DEV.format(version), color=rgb(221, 88, 24)))
|
||||||
|
print(client.create_event(access_token, calendar_id, start_time=delivery_date, end_time=expected_online_time, summary=config.FORMATTER_TEST.format(version), color=rgb(85, 220, 101)))
|
||||||
|
print(client.create_event(access_token, calendar_id, start_time=expected_online_time, end_time=online_time, summary=config.FORMATTER_GREY.format(version), color=rgb(118, 244, 244)))
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_bitable(client: api.Client, access_token, raw_data, schema, name_map) -> str:
|
||||||
|
'''write project release date info to bitable'''
|
||||||
|
table_id = client.create_table(access_token, config.APP_TOKEN, config.TABLE_NAME)
|
||||||
|
resp = client.get_fields_list(access_token, config.APP_TOKEN, table_id)
|
||||||
|
current_fields = resp['items']
|
||||||
|
|
||||||
|
for index, (field_name, field_type) in enumerate(schema.items()):
|
||||||
|
try:
|
||||||
|
if index >= len(current_fields):
|
||||||
|
print(client.add_field(access_token, config.APP_TOKEN, table_id, name_map[field_name], field_type))
|
||||||
|
else:
|
||||||
|
print(client.update_field(
|
||||||
|
access_token,
|
||||||
|
config.APP_TOKEN,
|
||||||
|
table_id,
|
||||||
|
current_fields[index]['field_id'],
|
||||||
|
name_map[field_name],
|
||||||
|
field_type))
|
||||||
|
except utils.LarkException as e:
|
||||||
|
if e.code == 1254606: # DataNotChange
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
resp = client.get_records_list(access_token, config.APP_TOKEN, table_id)
|
||||||
|
current_records = resp['items']
|
||||||
|
if current_records is None:
|
||||||
|
current_records = []
|
||||||
|
|
||||||
|
updated_records = []
|
||||||
|
created_records = []
|
||||||
|
|
||||||
|
for index, version_info in enumerate(raw_data):
|
||||||
|
fields = {}
|
||||||
|
for field_name, field_value in version_info.items():
|
||||||
|
if schema[field_name] == 5:
|
||||||
|
field_value = datetime.strptime(field_value, '%Y-%m-%d').timestamp() * 1000
|
||||||
|
fields[name_map[field_name]] = field_value
|
||||||
|
if index >= len(current_records):
|
||||||
|
created_records.append({'fields': fields})
|
||||||
|
else:
|
||||||
|
updated_records.append({'record_id': current_records[index]['record_id'], 'fields': fields})
|
||||||
|
if updated_records:
|
||||||
|
resp = client.batch_update_records(access_token, config.APP_TOKEN, table_id, updated_records)
|
||||||
|
if created_records:
|
||||||
|
resp = client.batch_create_records(access_token, config.APP_TOKEN, table_id, created_records)
|
||||||
|
return table_id
|
||||||
|
|
||||||
|
def bitable_to_calendar():
|
||||||
|
'''write project release date info to bitable and sync it to calendar'''
|
||||||
|
# init api client
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
# get tenant access token
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
|
||||||
|
# create a new table and push local json-like version iteration data to it
|
||||||
|
table_id = write_bitable(client, access_token, raw_data, schema, name_map)
|
||||||
|
|
||||||
|
# create new calendar and get its id
|
||||||
|
calendar = client.create_calendar(access_token, permissions='public', summary=config.SUMMARY)
|
||||||
|
print(calendar)
|
||||||
|
calendar_id = calendar['calendar_id']
|
||||||
|
|
||||||
|
# sync records to calendar
|
||||||
|
sync_records_to_calendar(client, access_token, calendar_id, config.APP_TOKEN, table_id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bitable_to_calendar()
|
||||||
54
config.py
Normal file
54
config.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
|
||||||
|
# load from env
|
||||||
|
APP_ID = os.getenv("APP_ID")
|
||||||
|
APP_SECRET = os.getenv("APP_SECRET")
|
||||||
|
LARK_HOST = os.getenv("LARK_HOST")
|
||||||
|
APP_TOKEN=os.getenv("APP_TOKEN")
|
||||||
|
|
||||||
|
# AI API Configurations
|
||||||
|
AI_API_KEY = os.getenv("AI_API_KEY", "sk-orez64WkG1NkfksB5j_hGA")
|
||||||
|
AI_BASE_URL = os.getenv("AI_BASE_URL", "https://oai.bwgdi.com/v1")
|
||||||
|
AI_MODEL = os.getenv("AI_MODEL", "MiniMaxAI")
|
||||||
|
|
||||||
|
# merge source and target configs (placeholders)
|
||||||
|
MERGE_SOURCE_APP_TOKEN_1 = os.getenv("MERGE_SOURCE_APP_TOKEN_1", "source_app_token_1")
|
||||||
|
MERGE_SOURCE_TABLE_ID_1 = os.getenv("MERGE_SOURCE_TABLE_ID_1", "source_table_id_1")
|
||||||
|
MERGE_SOURCE_APP_TOKEN_2 = os.getenv("MERGE_SOURCE_APP_TOKEN_2", "source_app_token_2")
|
||||||
|
MERGE_SOURCE_TABLE_ID_2 = os.getenv("MERGE_SOURCE_TABLE_ID_2", "source_table_id_2")
|
||||||
|
MERGE_TARGET_APP_TOKEN = os.getenv("MERGE_TARGET_APP_TOKEN", "target_app_token")
|
||||||
|
|
||||||
|
# localization
|
||||||
|
SUMMARY = '项目版本迭代计划日历'
|
||||||
|
TABLE_NAME = '项目版本迭代计划表'
|
||||||
|
KEY_VERSION='迭代版本'
|
||||||
|
KEY_STARTUP='启动时间'
|
||||||
|
KEY_SUBMIT='提测时间'
|
||||||
|
KEY_GREY='灰度时间'
|
||||||
|
KEY_GA='全量时间'
|
||||||
|
|
||||||
|
FORMATTER_DEV = '版本{}开工'
|
||||||
|
FORMATTER_TEST = '版本{}测试'
|
||||||
|
FORMATTER_GREY = '版本{}灰度'
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Web View Tabs Configuration
|
||||||
|
WEB_VIEW_TABS_ENV = os.getenv("WEB_VIEW_TABS")
|
||||||
|
if WEB_VIEW_TABS_ENV:
|
||||||
|
try:
|
||||||
|
WEB_VIEW_TABS = json.loads(WEB_VIEW_TABS_ENV)
|
||||||
|
except Exception:
|
||||||
|
WEB_VIEW_TABS = []
|
||||||
|
else:
|
||||||
|
WEB_VIEW_TABS = []
|
||||||
|
|
||||||
|
# Default table configuration for single table operations
|
||||||
|
DEFAULT_APP_TOKEN = os.getenv("DEFAULT_APP_TOKEN", "")
|
||||||
|
DEFAULT_TABLE_ID = os.getenv("DEFAULT_TABLE_ID", "")
|
||||||
|
BATCH_TABLE_ID = os.getenv("BATCH_TABLE_ID", "")
|
||||||
2
exec.ps1
Normal file
2
exec.ps1
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
docker build -t bitable-calendar .
|
||||||
|
docker run --env-file .env -it bitable-calendar
|
||||||
2
exec.sh
Normal file
2
exec.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
docker build -t bitable-calendar .
|
||||||
|
docker run --env-file .env -it bitable-calendar
|
||||||
971
export_web_view.py
Normal file
971
export_web_view.py
Normal file
@ -0,0 +1,971 @@
|
|||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from get_records import get_all_records
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
|
||||||
|
TABS = config.WEB_VIEW_TABS
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
||||||
|
|
||||||
|
HTML_TEMPLATE = r"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bitable Records - Interactive View</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #f8fafc;
|
||||||
|
--text-color: #334155;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--accent-color: #3b82f6;
|
||||||
|
--hover-bg: #f1f5f9;
|
||||||
|
--tag-bg: #e0e7ff;
|
||||||
|
--tag-text: #3730a3;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 30px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.toolbar input, .toolbar select, .toolbar button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.toolbar input {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.toolbar input:focus, .toolbar select:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px #dbeafe;
|
||||||
|
}
|
||||||
|
.toolbar button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toolbar button:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tab:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
.tab.active {
|
||||||
|
color: var(--accent-color);
|
||||||
|
border-bottom-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.tree {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ul.tree ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 28px;
|
||||||
|
border-left: 2px solid #cbd5e1;
|
||||||
|
margin-left: 14px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
li { margin: 6px 0; }
|
||||||
|
.node-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
|
||||||
|
}
|
||||||
|
.node-content:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.dimmed .node-content {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
details { margin-bottom: 6px; }
|
||||||
|
summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
summary::-webkit-details-marker { display: none; }
|
||||||
|
.toggle-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #94a3b8;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
details[open] > summary .toggle-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
.task-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.task-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1e293b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
.task-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e2e8f0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
background: var(--tag-bg);
|
||||||
|
color: var(--tag-text);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.empty-leaf {
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
.unnamed-task {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>ALL IN AI</h1>
|
||||||
|
|
||||||
|
<div id="globalAiSummaryResult" style="display:none;margin-bottom:20px;padding:16px;background:#ffffff;border:1px solid #c4b5fd;border-left:4px solid #8b5cf6;border-radius:8px;font-size:14px;color:#4c1d95;white-space:pre-wrap;line-height:1.6;max-height:500px;overflow-y:auto;box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);"></div>
|
||||||
|
<div class="tabs" id="tabContainer"></div>
|
||||||
|
|
||||||
|
<div class="toolbar" id="toolbarFilters">
|
||||||
|
<input type="text" id="searchInput" placeholder="Search tasks...">
|
||||||
|
<div style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<span style="font-size:14px;color:#64748b;">Date:</span>
|
||||||
|
<input type="date" id="dateFrom" title="Start Date">
|
||||||
|
<span style="color:#cbd5e1;">-</span>
|
||||||
|
<input type="date" id="dateTo" title="End Date">
|
||||||
|
</div>
|
||||||
|
<select id="statusFilter">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
</select>
|
||||||
|
<select id="leaderFilter">
|
||||||
|
<option value="">All Leaders</option>
|
||||||
|
</select>
|
||||||
|
<select id="sortOrder">
|
||||||
|
<option value="default">Sort: Default</option>
|
||||||
|
<option value="startDate">Sort: Start Date</option>
|
||||||
|
<option value="priority">Sort: Priority</option>
|
||||||
|
</select>
|
||||||
|
<button id="toggleExpand">Toggle Expand All</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats" id="statsDisplay"></div>
|
||||||
|
<div id="summaryDisplay"></div>
|
||||||
|
<div id="aiSummaryResult" style="display:none;margin-bottom:16px;padding:16px;background:#ffffff;border:1px solid #bbf7d0;border-radius:8px;font-size:14px;color:#334155;white-space:pre-wrap;line-height:1.6;max-height:500px;overflow-y:auto;box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);"></div>
|
||||||
|
<div id="tree-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const tabData = {RECORDS_JSON}; // Array of {name, records}
|
||||||
|
let rawRecords = [];
|
||||||
|
let records = [];
|
||||||
|
|
||||||
|
function extractText(val) {
|
||||||
|
if (Array.isArray(val) && val.length > 0) return val[0].text || '';
|
||||||
|
return val ? String(val) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(ts) {
|
||||||
|
if (!ts) return '';
|
||||||
|
const d = new Date(Number(ts));
|
||||||
|
const mo = String(d.getMonth()+1).padStart(2,'0');
|
||||||
|
const da = String(d.getDate()).padStart(2,'0');
|
||||||
|
return `${mo}-${da}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecords() {
|
||||||
|
records = rawRecords.map(r => {
|
||||||
|
const fields = r.fields || {};
|
||||||
|
const taskNameRaw = fields['Task'] || fields['Task Description'];
|
||||||
|
const taskName = extractText(taskNameRaw) || 'Untitled Task';
|
||||||
|
const status = extractText(fields['Status']).trim();
|
||||||
|
const priority = extractText(fields['Priority']).trim();
|
||||||
|
const overdue = extractText(fields['Overdue']).trim();
|
||||||
|
const manday = fields['Manday count'] || '';
|
||||||
|
const progressNotes = extractText(fields['Progress notes']).trim();
|
||||||
|
|
||||||
|
let tcRaw = fields['Target Customers'];
|
||||||
|
let targetCustomers = '';
|
||||||
|
if (Array.isArray(tcRaw)) {
|
||||||
|
targetCustomers = tcRaw.map(x => typeof x === 'object' ? (x.text || '') : String(x)).join(', ').trim();
|
||||||
|
} else if (tcRaw) {
|
||||||
|
targetCustomers = String(tcRaw).trim();
|
||||||
|
}
|
||||||
|
if (targetCustomers) {
|
||||||
|
console.log("Parsed Target Customers for [" + taskName + "]:", targetCustomers);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentRaw = fields['父记录'];
|
||||||
|
let parentId = null;
|
||||||
|
let parentName = null;
|
||||||
|
|
||||||
|
if (Array.isArray(parentRaw) && parentRaw.length > 0) {
|
||||||
|
if (parentRaw[0].record_ids && parentRaw[0].record_ids.length > 0) {
|
||||||
|
parentId = parentRaw[0].record_ids[0];
|
||||||
|
} else {
|
||||||
|
parentName = parentRaw[0].text;
|
||||||
|
}
|
||||||
|
} else if (typeof parentRaw === 'string') {
|
||||||
|
parentName = parentRaw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDateRaw = fields['Start date'];
|
||||||
|
const endDateRaw = fields['End Date'];
|
||||||
|
const startDate = startDateRaw ? Number(startDateRaw) : Infinity;
|
||||||
|
const endDate = endDateRaw ? Number(endDateRaw) : Infinity;
|
||||||
|
|
||||||
|
const leadersRaw = Array.isArray(fields['Task leader']) ? fields['Task leader'] : [];
|
||||||
|
const leaders = leadersRaw.filter(x => x && typeof x === 'object').map(x => ({name: x.name, avatar: x.avatar_url}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: r.record_id,
|
||||||
|
taskName,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
overdue,
|
||||||
|
manday,
|
||||||
|
progressNotes,
|
||||||
|
targetCustomers,
|
||||||
|
parentId,
|
||||||
|
parentName,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
startDateStr: formatDate(startDateRaw),
|
||||||
|
endDateStr: formatDate(endDateRaw),
|
||||||
|
leaders
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build hierarchy mapping
|
||||||
|
const nameMap = {};
|
||||||
|
records.forEach(r => nameMap[r.taskName] = r.id);
|
||||||
|
records.forEach(r => {
|
||||||
|
if (!r.parentId && r.parentName && nameMap[r.parentName]) {
|
||||||
|
r.parentId = nameMap[r.parentName];
|
||||||
|
}
|
||||||
|
if (r.parentId === r.id) r.parentId = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate dropdowns
|
||||||
|
const statuses = new Set();
|
||||||
|
const leaderNames = new Set();
|
||||||
|
records.forEach(r => {
|
||||||
|
if(r.status) statuses.add(r.status);
|
||||||
|
r.leaders.forEach(l => leaderNames.add(l.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusSelect = document.getElementById('statusFilter');
|
||||||
|
statusSelect.innerHTML = '<option value="">All Statuses</option>';
|
||||||
|
Array.from(statuses).sort().forEach(s => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = opt.textContent = s;
|
||||||
|
statusSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
const leaderSelect = document.getElementById('leaderFilter');
|
||||||
|
leaderSelect.innerHTML = '<option value="">All Leaders</option>';
|
||||||
|
Array.from(leaderNames).sort().forEach(l => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = opt.textContent = l;
|
||||||
|
leaderSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(index) {
|
||||||
|
if (!tabData[index]) return;
|
||||||
|
rawRecords = tabData[index].records;
|
||||||
|
|
||||||
|
document.querySelectorAll('.tab').forEach((t, i) => {
|
||||||
|
if (i === index) t.classList.add('active');
|
||||||
|
else t.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset filters visual
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
document.getElementById('sortOrder').value = 'default';
|
||||||
|
|
||||||
|
parseRecords();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTabs() {
|
||||||
|
const container = document.getElementById('tabContainer');
|
||||||
|
tabData.forEach((tab, index) => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'tab';
|
||||||
|
btn.textContent = tab.name;
|
||||||
|
btn.onclick = () => switchTab(index);
|
||||||
|
container.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryBtn = document.createElement('button');
|
||||||
|
summaryBtn.className = 'tab';
|
||||||
|
summaryBtn.style.marginLeft = 'auto'; // Push to the right
|
||||||
|
summaryBtn.style.color = '#8b5cf6';
|
||||||
|
summaryBtn.style.fontWeight = 'bold';
|
||||||
|
summaryBtn.innerHTML = '🌟 全局大总结';
|
||||||
|
summaryBtn.onclick = window.requestGlobalSummary;
|
||||||
|
container.appendChild(summaryBtn);
|
||||||
|
|
||||||
|
if (tabData.length > 0) {
|
||||||
|
switchTab(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
let isExpandAll = false;
|
||||||
|
|
||||||
|
document.getElementById('searchInput').addEventListener('input', render);
|
||||||
|
document.getElementById('statusFilter').addEventListener('change', render);
|
||||||
|
document.getElementById('leaderFilter').addEventListener('change', render);
|
||||||
|
document.getElementById('sortOrder').addEventListener('change', render);
|
||||||
|
document.getElementById('dateFrom').addEventListener('change', render);
|
||||||
|
document.getElementById('dateTo').addEventListener('change', render);
|
||||||
|
document.getElementById('toggleExpand').addEventListener('click', () => {
|
||||||
|
isExpandAll = !isExpandAll;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
function sortNodes(nodes, sortKey) {
|
||||||
|
if (sortKey === 'default') return nodes;
|
||||||
|
nodes.sort((a, b) => {
|
||||||
|
if (sortKey === 'startDate') return a.startDate - b.startDate;
|
||||||
|
if (sortKey === 'priority') {
|
||||||
|
const getP = p => p.includes('High') || p.includes('Important') ? 3 : p.includes('Normal') ? 2 : 1;
|
||||||
|
return getP(b.priority) - getP(a.priority);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
nodes.forEach(n => sortNodes(n.children, sortKey));
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNodes(nodes, search, statusFilter, leaderFilter, dateFrom, dateTo) {
|
||||||
|
let result = [];
|
||||||
|
nodes.forEach(n => {
|
||||||
|
let matchSearch = n.taskName.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
(n.targetCustomers && n.targetCustomers.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
let matchStatus = statusFilter === '' || n.status === statusFilter;
|
||||||
|
let matchLeader = leaderFilter === '' || n.leaders.some(l => l.name === leaderFilter);
|
||||||
|
|
||||||
|
let matchDate = true;
|
||||||
|
if (dateFrom !== -Infinity || dateTo !== Infinity) {
|
||||||
|
let s = n.startDate !== Infinity ? n.startDate : (n.endDate !== Infinity ? n.endDate : Infinity);
|
||||||
|
let e = n.endDate !== Infinity ? n.endDate : (n.startDate !== Infinity ? n.startDate : Infinity);
|
||||||
|
|
||||||
|
if (s !== Infinity && e !== Infinity) {
|
||||||
|
matchDate = (s <= dateTo) && (e >= dateFrom);
|
||||||
|
} else {
|
||||||
|
matchDate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filteredChildren = filterNodes(n.children, search, statusFilter, leaderFilter, dateFrom, dateTo);
|
||||||
|
|
||||||
|
let selfMatch = matchSearch && matchStatus && matchLeader && matchDate;
|
||||||
|
|
||||||
|
if (selfMatch || filteredChildren.length > 0) {
|
||||||
|
let cloned = {...n, children: filteredChildren};
|
||||||
|
cloned.isMatch = selfMatch;
|
||||||
|
result.push(cloned);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genTags(n) {
|
||||||
|
let html = '<div class="tags">';
|
||||||
|
if (n.status) {
|
||||||
|
let bg = '#f1f5f9', fg = '#475569';
|
||||||
|
if (n.status.includes('Ongoing') || n.status.includes('进行中')) { bg='#dbeafe'; fg='#1e40af'; }
|
||||||
|
else if (n.status.includes('Completed') || n.status.includes('完成')) { bg='#d1fae5'; fg='#065f46'; }
|
||||||
|
html += `<span class="tag" style="background:${bg};color:${fg}">${n.status}</span>`;
|
||||||
|
}
|
||||||
|
if (n.priority) {
|
||||||
|
if (n.priority.includes('High') || n.priority.includes('Important')) {
|
||||||
|
html += `<span class="tag" style="background:#ffedd5;color:#c2410c">${n.priority}</span>`;
|
||||||
|
} else if (!n.priority.includes('Normal')) {
|
||||||
|
html += `<span class="tag" style="background:#f1f5f9;color:#475569">${n.priority}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n.overdue && !n.overdue.includes('✅')) {
|
||||||
|
html += `<span class="tag" style="background:#fee2e2;color:#991b1b">${n.overdue}</span>`;
|
||||||
|
}
|
||||||
|
if (n.targetCustomers) {
|
||||||
|
html += `<span class="tag" style="background:#fce7f3;color:#9d174d">🎯 ${n.targetCustomers}</span>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genMeta(n) {
|
||||||
|
let html = '<div class="task-meta">';
|
||||||
|
if (n.leaders.length > 0) {
|
||||||
|
html += '<div class="meta-item">';
|
||||||
|
n.leaders.forEach(l => {
|
||||||
|
if (l.avatar) html += `<img src="${l.avatar}" class="avatar" title="${l.name}">`;
|
||||||
|
else html += `<span>${l.name}</span>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
if (n.startDateStr || n.endDateStr) {
|
||||||
|
html += `<div class="meta-item">🗓️ ${n.startDateStr} ~ ${n.endDateStr}</div>`;
|
||||||
|
}
|
||||||
|
if (n.manday) {
|
||||||
|
html += `<div class="meta-item">⏱️ ${n.manday}d</div>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toggleNotes = function(event, id) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
const el = document.getElementById('notes-' + id);
|
||||||
|
if (!el) return;
|
||||||
|
if (el.style.display === 'none') {
|
||||||
|
el.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderHTML(nodes, isRoot) {
|
||||||
|
if (nodes.length === 0) return '';
|
||||||
|
let html = isRoot ? '<ul class="tree">' : '<ul>';
|
||||||
|
nodes.forEach(n => {
|
||||||
|
let dimClass = n.isMatch === false ? 'dimmed' : '';
|
||||||
|
let tags = genTags(n);
|
||||||
|
let meta = genMeta(n);
|
||||||
|
|
||||||
|
// Hide details by default, show toggle button if notes exist
|
||||||
|
let toggleNotesBtn = '';
|
||||||
|
let notesHtml = '';
|
||||||
|
if (n.progressNotes) {
|
||||||
|
toggleNotesBtn = `<span title="Toggle Progress Notes" style="cursor:pointer; font-size:14px; margin-right:6px;" onclick="window.toggleNotes(event, '${n.id}')">📝</span>`;
|
||||||
|
notesHtml = `<div id="notes-${n.id}" style="display:none; font-size:13px; color:#64748b; margin-top:8px; padding:8px; background:#f8fafc; border-radius:6px; border:1px dashed #cbd5e1; font-family: monospace; white-space: pre-wrap;">${n.progressNotes}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<li class="${dimClass}">`;
|
||||||
|
if (n.children.length > 0) {
|
||||||
|
let openAttr = isExpandAll ? 'open' : '';
|
||||||
|
html += `<details ${openAttr}>
|
||||||
|
<summary>
|
||||||
|
<div class="node-content" style="flex-wrap: wrap;">
|
||||||
|
<div style="width:100%; display:flex; align-items:center; justify-content:space-between;">
|
||||||
|
<div class="task-left">
|
||||||
|
<span class="toggle-icon">▶</span>
|
||||||
|
${toggleNotesBtn}
|
||||||
|
<span class="task-title">${n.taskName}</span>
|
||||||
|
${tags}
|
||||||
|
</div>
|
||||||
|
${meta}
|
||||||
|
</div>
|
||||||
|
${notesHtml ? '<div style="width:100%; padding-left:24px;">' + notesHtml + '</div>' : ''}
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
${renderHTML(n.children, false)}
|
||||||
|
</details>`;
|
||||||
|
} else {
|
||||||
|
let unnamedClass = n.taskName === 'Untitled Task' ? 'unnamed-task' : '';
|
||||||
|
html += `<div class="node-content empty-leaf" style="flex-wrap: wrap;">
|
||||||
|
<div style="width:100%; display:flex; align-items:center; justify-content:space-between;">
|
||||||
|
<div class="task-left">
|
||||||
|
${toggleNotesBtn}
|
||||||
|
<span class="task-title ${unnamedClass}">${n.taskName}</span>
|
||||||
|
${tags}
|
||||||
|
</div>
|
||||||
|
${meta}
|
||||||
|
</div>
|
||||||
|
${notesHtml ? '<div style="width:100%; padding-left:8px;">' + notesHtml + '</div>' : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
html += '</li>';
|
||||||
|
});
|
||||||
|
html += '</ul>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countNodes(nodes) {
|
||||||
|
return nodes.reduce((sum, n) => sum + 1 + countNodes(n.children), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMatchedNodes(nodes) {
|
||||||
|
let acc = [];
|
||||||
|
nodes.forEach(n => {
|
||||||
|
if (n.isMatch) acc.push(n);
|
||||||
|
acc = acc.concat(collectMatchedNodes(n.children));
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSummary(nodes, status, leader) {
|
||||||
|
let matched = collectMatchedNodes(nodes);
|
||||||
|
if (matched.length === 0) return '';
|
||||||
|
|
||||||
|
let totalMandays = 0;
|
||||||
|
let completed = 0;
|
||||||
|
let ongoing = 0;
|
||||||
|
let taskNames = [];
|
||||||
|
|
||||||
|
matched.forEach(r => {
|
||||||
|
if (r.manday) {
|
||||||
|
let num = parseFloat(r.manday);
|
||||||
|
if (!isNaN(num)) totalMandays += num;
|
||||||
|
}
|
||||||
|
if (r.status && (r.status.includes('Completed') || r.status.includes('完成'))) completed++;
|
||||||
|
else if (r.status && (r.status.includes('Ongoing') || r.status.includes('进行中'))) ongoing++;
|
||||||
|
|
||||||
|
if (r.taskName && r.taskName !== 'Untitled Task') {
|
||||||
|
taskNames.push(r.taskName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '<div style="margin-bottom:16px; padding:12px 16px; background:#f0fdf4; border: 1px solid #bbf7d0; border-left:4px solid #22c55e; border-radius:6px; font-size:14px; color:#166534; box-shadow: 0 1px 2px rgba(0,0,0,0.05);">';
|
||||||
|
html += '<div style="font-weight:600; margin-bottom:6px; display:flex; align-items:center; gap:6px;"><span>💡</span>智能总结 (Smart Summary)</div>';
|
||||||
|
|
||||||
|
let parts = [];
|
||||||
|
if (!status && !leader) {
|
||||||
|
let title = document.getElementById('searchInput') && document.getElementById('searchInput').value ? '搜索结果' : '当前全局';
|
||||||
|
parts.push(`${title}共有 <strong>${matched.length}</strong> 项任务 (已完成 ${completed} 项,进行中 ${ongoing} 项)。`);
|
||||||
|
if (totalMandays > 0) parts.push(`预计总人力:<strong>${Math.round(totalMandays*10)/10}</strong> 人日。`);
|
||||||
|
parts.push(`核心事项概览:`);
|
||||||
|
} else if (leader && !status) {
|
||||||
|
parts.push(`<strong>${leader}</strong> 共有 <strong>${matched.length}</strong> 项任务 (已完成 ${completed} 项,进行中 ${ongoing} 项)。`);
|
||||||
|
if (totalMandays > 0) parts.push(`预计总人力:<strong>${Math.round(totalMandays*10)/10}</strong> 人日。`);
|
||||||
|
parts.push(`Ta 负责的主要事项有:`);
|
||||||
|
} else if (status && !leader) {
|
||||||
|
let isDone = status.includes('Completed') || status.includes('完成');
|
||||||
|
if (isDone) {
|
||||||
|
parts.push(`✅ 已经干完了 <strong>${matched.length}</strong> 项任务!`);
|
||||||
|
if (totalMandays > 0) parts.push(`共计消耗人力:<strong>${Math.round(totalMandays*10)/10}</strong> 人日。`);
|
||||||
|
parts.push(`具体完成了这些东西:`);
|
||||||
|
} else {
|
||||||
|
parts.push(`有 <strong>${matched.length}</strong> 项任务处于 "<strong>${status}</strong>" 状态。`);
|
||||||
|
if (totalMandays > 0) parts.push(`关联的人力评估:<strong>${Math.round(totalMandays*10)/10}</strong> 人日。`);
|
||||||
|
parts.push(`具体包含:`);
|
||||||
|
}
|
||||||
|
} else if (leader && status) {
|
||||||
|
parts.push(`<strong>${leader}</strong> 有 <strong>${matched.length}</strong> 项状态为 "<strong>${status}</strong>" 的任务。`);
|
||||||
|
if (totalMandays > 0) parts.push(`相关人力评估:<strong>${Math.round(totalMandays*10)/10}</strong> 人日。`);
|
||||||
|
parts.push(`相关任务:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskNames.length > 0) {
|
||||||
|
let displayNames = taskNames.slice(0, 10).map(n => `「${n}」`).join('、');
|
||||||
|
if (taskNames.length > 10) displayNames += ` 等等 (共${taskNames.length}个)`;
|
||||||
|
parts.push(`${displayNames}。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div style="line-height: 1.6;">' + parts.join(' ') + '</div>';
|
||||||
|
|
||||||
|
window._lastMatchedNodes = matched;
|
||||||
|
|
||||||
|
html += `<div style="margin-top:12px;display:flex;gap:8px;align-items:center;">
|
||||||
|
<button onclick="window.requestAISummary()" style="background:#22c55e;color:white;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-weight:500;font-size:13px;transition:background 0.2s;" onmouseover="this.style.background='#16a34a'" onmouseout="this.style.background='#22c55e'">🤖 请求 AI 深度总结</button>
|
||||||
|
<button onclick="window.configAI()" style="background:transparent;border:1px solid #bbf7d0;color:#166534;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:13px;transition:background 0.2s;" onmouseover="this.style.background='#dcfce7'" onmouseout="this.style.background='transparent'">⚙️ 配置 API</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.configAI = function() {
|
||||||
|
let currentKey = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}';
|
||||||
|
let currentUrl = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}';
|
||||||
|
let currentModel = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}';
|
||||||
|
|
||||||
|
let key = prompt('请输入您的 API Key (无需则留空或取消):', currentKey);
|
||||||
|
if (key === null) return;
|
||||||
|
let url = prompt('请输入 API Base URL (例如 https://api.openai.com/v1 或兼容地址):', currentUrl);
|
||||||
|
if (url === null) return;
|
||||||
|
let model = prompt('请输入模型名称 (例如 gpt-4o, deepseek-chat):', currentModel);
|
||||||
|
if (model === null) return;
|
||||||
|
|
||||||
|
localStorage.setItem('ai_api_key', key);
|
||||||
|
localStorage.setItem('ai_base_url', url);
|
||||||
|
localStorage.setItem('ai_model', model);
|
||||||
|
alert('AI 配置已成功保存在浏览器本地!');
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchAndStreamAI(promptText, box) {
|
||||||
|
let key = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}';
|
||||||
|
let url = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}';
|
||||||
|
let model = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}';
|
||||||
|
|
||||||
|
if (!key && url.includes('openai.com')) {
|
||||||
|
alert('系统检测到您未配置 API Key,请先进行配置!');
|
||||||
|
window.configAI();
|
||||||
|
key = localStorage.getItem('ai_api_key');
|
||||||
|
if (!key) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.style.display = 'block';
|
||||||
|
box.textContent = '🚀 正在请求 AI,这可能需要一点时间,请稍等...\n如果长时间不响应,请检查浏览器控制台报错或网络代理。';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let fetchUrl = url;
|
||||||
|
if (fetchUrl.endsWith('/')) fetchUrl = fetchUrl.slice(0, -1);
|
||||||
|
if (!fetchUrl.endsWith('/chat/completions')) fetchUrl += '/chat/completions';
|
||||||
|
|
||||||
|
const response = await fetch(fetchUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + key
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: '你是一位资深项目负责人,擅长做简洁、高价值的工作汇报,突出结果与业务价值,而非技术细节。' },
|
||||||
|
{ role: 'user', content: promptText }
|
||||||
|
],
|
||||||
|
stream: true,
|
||||||
|
temperature: 0.7
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errBox = await response.text();
|
||||||
|
box.textContent = `网络请求失败 (状态码 ${response.status}): \n${errBox}\n\n请确认提供的 API Base URL(${url}) 和 API Key 是否正确。`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
let content = '';
|
||||||
|
box.textContent = '思考中...';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().startsWith('data:') && line.trim() !== 'data: [DONE]') {
|
||||||
|
try {
|
||||||
|
let jsonStr = line.replace(/^data:\s*/, '');
|
||||||
|
const parsed = JSON.parse(jsonStr);
|
||||||
|
if (parsed.choices && parsed.choices[0].delta && parsed.choices[0].delta.content) {
|
||||||
|
content += parsed.choices[0].delta.content;
|
||||||
|
|
||||||
|
let displayContent = content.replace(/<think>[\s\S]*?(?:<\/think>\n*|$)/gi, '');
|
||||||
|
if (content.toLowerCase().includes('<think>') && !content.toLowerCase().includes('</think>')) {
|
||||||
|
displayContent = "🤔 正在深度思考中...\n\n" + displayContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.textContent = displayContent;
|
||||||
|
box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
box.textContent = `发生异常: ${e.message}\n请检查网络连通性或 API 跨域(CORS)限制问题。`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAISummary = function() {
|
||||||
|
const matched = window._lastMatchedNodes || [];
|
||||||
|
if (matched.length === 0) {
|
||||||
|
alert("当前视图没有任何任务记录,无需总结。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasksForAI = matched.map(m => ({
|
||||||
|
TaskName: m.taskName,
|
||||||
|
Status: m.status,
|
||||||
|
Priority: m.priority,
|
||||||
|
Leaders: m.leaders.map(l => l.name).join(','),
|
||||||
|
ProgressNotes: m.progressNotes || "无",
|
||||||
|
Manday: m.manday || "0",
|
||||||
|
TargetCustomers: m.targetCustomers || "无"
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promptText = `请将以下多个任务整理汇报摘要:
|
||||||
|
要求:
|
||||||
|
1. 每个任务用3-4行总结(须明确提及该任务相关的 TargetCustomers 目标客户信息)
|
||||||
|
2. 最后增加【整体情况总结】:
|
||||||
|
- 完成情况(完成/进行中)
|
||||||
|
- 当前重点方向(包含核心目标客户的总体进展)
|
||||||
|
|
||||||
|
输出结构:
|
||||||
|
(多个任务总结)
|
||||||
|
|
||||||
|
【整体情况总结】
|
||||||
|
- 数据如下:
|
||||||
|
${JSON.stringify(tasksForAI, null, 2)}`;
|
||||||
|
|
||||||
|
const box = document.getElementById('aiSummaryResult');
|
||||||
|
fetchAndStreamAI(promptText, box);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.requestGlobalSummary = function() {
|
||||||
|
let allTasks = [];
|
||||||
|
tabData.forEach(tab => {
|
||||||
|
tab.records.forEach(r => {
|
||||||
|
const fields = r.fields || {};
|
||||||
|
const taskNameRaw = fields['Task'] || fields['Task Description'];
|
||||||
|
let taskName = '';
|
||||||
|
if (Array.isArray(taskNameRaw) && taskNameRaw.length > 0) taskName = taskNameRaw[0].text || '';
|
||||||
|
else taskName = taskNameRaw ? String(taskNameRaw) : 'Untitled Task';
|
||||||
|
|
||||||
|
let statusRaw = fields['Status'];
|
||||||
|
let status = '';
|
||||||
|
if (Array.isArray(statusRaw) && statusRaw.length > 0) status = statusRaw[0].text || '';
|
||||||
|
else status = statusRaw ? String(statusRaw) : '';
|
||||||
|
|
||||||
|
let targetCustomersRaw = fields['Target Customers'];
|
||||||
|
let targetCustomers = '';
|
||||||
|
if (Array.isArray(targetCustomersRaw)) {
|
||||||
|
targetCustomers = targetCustomersRaw.map(x => typeof x === 'object' ? (x.text || '') : String(x)).join(', ').trim();
|
||||||
|
} else if (targetCustomersRaw) {
|
||||||
|
targetCustomers = String(targetCustomersRaw).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
allTasks.push({
|
||||||
|
TabName: tab.name,
|
||||||
|
TaskName: taskName,
|
||||||
|
Status: status.trim(),
|
||||||
|
TargetCustomers: targetCustomers.trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allTasks.length === 0) {
|
||||||
|
alert("所有视图均没有任务记录,无需总结。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptText = `请将以下来自多个部门/维度的表格任务进行全局大总结:
|
||||||
|
要求:
|
||||||
|
1. 按照不同的 Tab (即数据来源) 独立提取核心进展与问题。
|
||||||
|
2. 特别留意并提炼出 TargetCustomers (目标客户) 的主要推行情况。
|
||||||
|
3. 最后给出一个【全局统筹结论】,指出哪些方向正常,哪些可能存在延期或需要重点关注。
|
||||||
|
4. 尽量言简意赅,不要只是把每一条数据罗列一遍,要有跨表分析和高层次的提炼。
|
||||||
|
|
||||||
|
- 数据如下:
|
||||||
|
${JSON.stringify(allTasks, null, 2)}`;
|
||||||
|
|
||||||
|
const box = document.getElementById('globalAiSummaryResult');
|
||||||
|
fetchAndStreamAI(promptText, box);
|
||||||
|
};
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const search = document.getElementById('searchInput').value;
|
||||||
|
const status = document.getElementById('statusFilter').value;
|
||||||
|
const leader = document.getElementById('leaderFilter').value;
|
||||||
|
const sortKey = document.getElementById('sortOrder').value;
|
||||||
|
|
||||||
|
let dfRaw = document.getElementById('dateFrom').value;
|
||||||
|
let dtRaw = document.getElementById('dateTo').value;
|
||||||
|
const dateFrom = dfRaw ? new Date(dfRaw).getTime() : -Infinity;
|
||||||
|
const dateTo = dtRaw ? new Date(dtRaw).getTime() + 86400000 - 1 : Infinity;
|
||||||
|
|
||||||
|
let nodeMap = {};
|
||||||
|
records.forEach(r => { nodeMap[r.id] = {...r, children: []}; });
|
||||||
|
|
||||||
|
let roots = [];
|
||||||
|
records.forEach(r => {
|
||||||
|
let n = nodeMap[r.id];
|
||||||
|
if (r.parentId && nodeMap[r.parentId]) nodeMap[r.parentId].children.push(n);
|
||||||
|
else roots.push(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort logic inside roots recursively helps visual grouping
|
||||||
|
roots.sort((a,b) => b.children.length - a.children.length); // roots with children on top by default
|
||||||
|
|
||||||
|
let filtered = filterNodes(roots, search, status, leader, dateFrom, dateTo);
|
||||||
|
let sorted = sortNodes(filtered, sortKey);
|
||||||
|
|
||||||
|
let renderedHtml = renderHTML(sorted, true);
|
||||||
|
document.getElementById('tree-container').innerHTML = renderedHtml || '<p style="text-align:center;color:#64748b;margin-top:40px;">No matching records found.</p>';
|
||||||
|
|
||||||
|
let visibleCount = countNodes(sorted);
|
||||||
|
let isFilterActive = search !== '' || status !== '' || leader !== '';
|
||||||
|
let msg = `Showing ${visibleCount} / ${records.length} tasks`;
|
||||||
|
if (isFilterActive) {
|
||||||
|
msg += ' (Filtered)';
|
||||||
|
}
|
||||||
|
document.getElementById('statsDisplay').textContent = msg;
|
||||||
|
document.getElementById('summaryDisplay').innerHTML = generateSummary(filtered, status, leader);
|
||||||
|
|
||||||
|
// Clear AI Summary on any filter change so it doesn't show stale info
|
||||||
|
let aiBox = document.getElementById('aiSummaryResult');
|
||||||
|
if (aiBox) {
|
||||||
|
aiBox.style.display = 'none';
|
||||||
|
aiBox.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize tabs and first render
|
||||||
|
initTabs();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def build_html(client, access_token):
|
||||||
|
tab_data_list = []
|
||||||
|
|
||||||
|
for tab in TABS:
|
||||||
|
logging.info(f"Fetching records for tab [{tab['name']}]...")
|
||||||
|
records = get_all_records(client, access_token, tab['app_token'], tab['table_id'])
|
||||||
|
logging.info(f"Tab [{tab['name']}] retrieved {len(records)} records.")
|
||||||
|
tab_data_list.append({
|
||||||
|
"name": tab['name'],
|
||||||
|
"records": records
|
||||||
|
})
|
||||||
|
|
||||||
|
# Dump records to json
|
||||||
|
records_json = json.dumps(tab_data_list, ensure_ascii=False)
|
||||||
|
|
||||||
|
final_html = HTML_TEMPLATE.replace('{RECORDS_JSON}', records_json)
|
||||||
|
final_html = final_html.replace('{DEFAULT_AI_KEY}', config.AI_API_KEY or '')
|
||||||
|
final_html = final_html.replace('{DEFAULT_AI_URL}', config.AI_BASE_URL or 'https://api.openai.com/v1')
|
||||||
|
final_html = final_html.replace('{DEFAULT_AI_MODEL}', config.AI_MODEL or 'gpt-4o')
|
||||||
|
return final_html
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not get access token: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
final_html = build_html(client, access_token)
|
||||||
|
|
||||||
|
output_file = "outline_view.html"
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(final_html)
|
||||||
|
|
||||||
|
logging.info(f"Successfully generated HTML view at {output_file}")
|
||||||
|
print(f"\n✅ HTML view generated! You can open '/home/verachen/Workspace/feishu/bitable_calendar/python/{output_file}' in your browser.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
59
get_records.py
Normal file
59
get_records.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
||||||
|
|
||||||
|
def get_all_records(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
"""Retrieve all records from a given table, handling pagination if necessary."""
|
||||||
|
records = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_records_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
records.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
# Get tenant access token
|
||||||
|
try:
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not get access token: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
logging.info(f"Using App ID: {config.APP_ID}")
|
||||||
|
|
||||||
|
# Please replace these with your actual app_token and table_id
|
||||||
|
APP_TOKEN = config.DEFAULT_APP_TOKEN
|
||||||
|
TABLE_ID = config.DEFAULT_TABLE_ID
|
||||||
|
|
||||||
|
if APP_TOKEN == "your_app_token_here" or TABLE_ID == "your_table_id_here":
|
||||||
|
logging.warning("Please specify APP_TOKEN and TABLE_ID in the script to get records.")
|
||||||
|
logging.info("For example, you can edit this script to set APP_TOKEN and TABLE_ID, then run it again.")
|
||||||
|
else:
|
||||||
|
logging.info(f"Fetching records for App Token: {APP_TOKEN}, Table ID: {TABLE_ID}")
|
||||||
|
records = get_all_records(client, access_token, APP_TOKEN, TABLE_ID)
|
||||||
|
logging.info(f"Successfully retrieved {len(records)} records.")
|
||||||
|
|
||||||
|
# Print the first record as an example of the structure
|
||||||
|
if records:
|
||||||
|
print("\nExample Record (First item):")
|
||||||
|
print(json.dumps(records[0], ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
# Example to use batch_get_records if you have specific record IDs:
|
||||||
|
# record_ids = [r['record_id'] for r in records[:5]] # get first 5 ids
|
||||||
|
# if record_ids:
|
||||||
|
# batch_records = client.batch_get_records(access_token, APP_TOKEN, TABLE_ID, record_ids)
|
||||||
|
# logging.info(f"Batch GET returned {len(batch_records)} records.")
|
||||||
158
import_bitables.py
Normal file
158
import_bitables.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
import utils
|
||||||
|
import copy
|
||||||
|
import time
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
||||||
|
|
||||||
|
def get_all_records(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
records = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_records_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
records.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_fields(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
fields = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_fields_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
fields.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_tables(client: api.Client, access_token: str, app_token: str):
|
||||||
|
tables = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_tables_list(access_token, app_token, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
tables.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def copy_single_table(client: api.Client, access_token: str, source_app_token: str, source_table_id: str, target_app_token: str, table_name: str):
|
||||||
|
logging.info(f"Copying table '{table_name}' ({source_table_id}) from {source_app_token} to {target_app_token}")
|
||||||
|
|
||||||
|
fields = get_all_fields(client, access_token, source_app_token, source_table_id)
|
||||||
|
|
||||||
|
# Create Target Table
|
||||||
|
target_table_id = client.create_table(access_token, target_app_token, table_name)
|
||||||
|
|
||||||
|
# Get initial default fields set by Feishu
|
||||||
|
target_initial_fields = get_all_fields(client, access_token, target_app_token, target_table_id)
|
||||||
|
initial_field_names = {f['field_name']: f['field_id'] for f in target_initial_fields}
|
||||||
|
|
||||||
|
target_field_map = {}
|
||||||
|
|
||||||
|
for f in fields:
|
||||||
|
name = f['field_name']
|
||||||
|
ftype = f['type']
|
||||||
|
fprop = copy.deepcopy(f.get('property'))
|
||||||
|
|
||||||
|
# Clean up IDs from options to avoid errors
|
||||||
|
if fprop and 'options' in fprop:
|
||||||
|
for opt in fprop['options']:
|
||||||
|
if 'id' in opt:
|
||||||
|
del opt['id']
|
||||||
|
|
||||||
|
if name in initial_field_names:
|
||||||
|
field_id = initial_field_names[name]
|
||||||
|
try:
|
||||||
|
client.update_field(access_token, target_app_token, target_table_id, field_id, name, ftype, fprop)
|
||||||
|
target_field_map[name] = field_id
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to update default field '{name}': {e}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = client.add_field(access_token, target_app_token, target_table_id, name, ftype, fprop)
|
||||||
|
target_field_map[name] = resp['field_id']
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to create field '{name}': {e}")
|
||||||
|
|
||||||
|
# Read Records from Source Table
|
||||||
|
records = get_all_records(client, access_token, source_app_token, source_table_id)
|
||||||
|
|
||||||
|
target_records = []
|
||||||
|
for r in records:
|
||||||
|
original_fields = r.get('fields', {})
|
||||||
|
new_fields = {}
|
||||||
|
for fname, fvalue in original_fields.items():
|
||||||
|
if fname in target_field_map:
|
||||||
|
new_fields[fname] = fvalue
|
||||||
|
target_records.append({'fields': new_fields})
|
||||||
|
|
||||||
|
CHUNK_SIZE = 500
|
||||||
|
for i in range(0, len(target_records), CHUNK_SIZE):
|
||||||
|
chunk = target_records[i:i + CHUNK_SIZE]
|
||||||
|
if chunk:
|
||||||
|
try:
|
||||||
|
client.batch_create_records(access_token, target_app_token, target_table_id, chunk)
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to insert chunk of records: {e}")
|
||||||
|
|
||||||
|
logging.info(f"Successfully copied table '{table_name}' to {target_table_id}!")
|
||||||
|
return target_table_id
|
||||||
|
|
||||||
|
|
||||||
|
def import_all_tables(client: api.Client, access_token: str, source_app_tokens: list, target_app_token: str):
|
||||||
|
"""
|
||||||
|
Imports all tables from a list of source bitable Apps into the target App.
|
||||||
|
"""
|
||||||
|
for source_app_token in source_app_tokens:
|
||||||
|
logging.info(f"Fetching tables from source app: {source_app_token}")
|
||||||
|
try:
|
||||||
|
tables = get_all_tables(client, access_token, source_app_token)
|
||||||
|
for table in tables:
|
||||||
|
source_table_id = table['table_id']
|
||||||
|
table_name = table['name']
|
||||||
|
copy_single_table(client, access_token, source_app_token, source_table_id, target_app_token, table_name)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to copy tables from {source_app_token}: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not get access token: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
logging.info(f"Using App ID: {config.APP_ID}")
|
||||||
|
|
||||||
|
# Setup source and target tokens. You can put multiple source app tokens in a list.
|
||||||
|
SOURCE_APP_TOKENS = [
|
||||||
|
config.MERGE_SOURCE_APP_TOKEN_1, # Or replace with an actual App token string
|
||||||
|
# "another_source_app_token",
|
||||||
|
]
|
||||||
|
TARGET_APP_TOKEN = config.MERGE_TARGET_APP_TOKEN
|
||||||
|
|
||||||
|
is_placeholder = (config.MERGE_SOURCE_APP_TOKEN_1 == "source_app_token_1")
|
||||||
|
if is_placeholder:
|
||||||
|
logging.warning("Please update config.py or environment variables with valid MERGE_* tokens to execute.")
|
||||||
|
else:
|
||||||
|
import_all_tables(client, access_token, SOURCE_APP_TOKENS, TARGET_APP_TOKEN)
|
||||||
39
list_test.py
Normal file
39
list_test.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import lark_oapi as lark
|
||||||
|
from lark_oapi.api.bitable.v1 import *
|
||||||
|
|
||||||
|
APP_ID="cli_a92c04f105f8dcb0"
|
||||||
|
APP_SECRET="5ogsQbtBQ1fPDvwdsykdecpVLT2EyXZc"
|
||||||
|
|
||||||
|
# SDK 使用说明: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/server-side-sdk/python--sdk/preparations-before-development
|
||||||
|
# 以下示例代码默认根据文档示例值填充,如果存在代码问题,请在 API 调试台填上相关必要参数后再复制代码使用
|
||||||
|
# 复制该 Demo 后, 需要将 "YOUR_APP_ID", "YOUR_APP_SECRET" 替换为自己应用的 APP_ID, APP_SECRET.
|
||||||
|
def main():
|
||||||
|
# 创建client
|
||||||
|
client = lark.Client.builder() \
|
||||||
|
.app_id("APP_ID") \
|
||||||
|
.app_secret("APP_SECRET") \
|
||||||
|
.log_level(lark.LogLevel.DEBUG) \
|
||||||
|
.build()
|
||||||
|
|
||||||
|
# 构造请求对象
|
||||||
|
request: ListAppTableRequest = ListAppTableRequest.builder() \
|
||||||
|
.page_size(20) \
|
||||||
|
.build()
|
||||||
|
|
||||||
|
# 发起请求
|
||||||
|
response: ListAppTableResponse = client.bitable.v1.app_table.list(request)
|
||||||
|
|
||||||
|
# 处理失败返回
|
||||||
|
if not response.success():
|
||||||
|
lark.logger.error(
|
||||||
|
f"client.bitable.v1.app_table.list failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 处理业务结果
|
||||||
|
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
6
main.py
Normal file
6
main.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from python!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
221
merge_bitables.py
Normal file
221
merge_bitables.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import logging
|
||||||
|
import utils
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
||||||
|
|
||||||
|
def get_all_records(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
"""Retrieve all records from a given table, handling pagination if necessary."""
|
||||||
|
records = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_records_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
records.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_fields(client: api.Client, access_token: str, app_token: str, table_id: str):
|
||||||
|
"""Retrieve all fields from a given table, handling pagination if necessary."""
|
||||||
|
fields = []
|
||||||
|
page_token = None
|
||||||
|
while True:
|
||||||
|
resp = client.get_fields_list(access_token, app_token, table_id, page_token=page_token)
|
||||||
|
items = resp.get('items', [])
|
||||||
|
if items:
|
||||||
|
fields.extend(items)
|
||||||
|
if resp.get('has_more'):
|
||||||
|
page_token = resp.get('page_token')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def merge_schema(fields1: list, fields2: list):
|
||||||
|
"""
|
||||||
|
Merge two lists of field definitions into one unified schema list.
|
||||||
|
If fields have the same name, we assume they are the same field.
|
||||||
|
We'll keep the definition from fields1 if there's a collision.
|
||||||
|
"""
|
||||||
|
merged_fields = {}
|
||||||
|
|
||||||
|
# Process fields from first table
|
||||||
|
for f in fields1:
|
||||||
|
# We use a deep copy-like approach or just assign, but we will mutate properties later
|
||||||
|
import copy
|
||||||
|
merged_fields[f['field_name']] = {
|
||||||
|
'field_name': f['field_name'],
|
||||||
|
'type': f['type'],
|
||||||
|
'property': copy.deepcopy(f.get('property'))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process fields from second table (add if not exists)
|
||||||
|
for f in fields2:
|
||||||
|
import copy
|
||||||
|
if f['field_name'] not in merged_fields:
|
||||||
|
merged_fields[f['field_name']] = {
|
||||||
|
'field_name': f['field_name'],
|
||||||
|
'type': f['type'],
|
||||||
|
'property': copy.deepcopy(f.get('property'))
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Merge options if both are SingleSelect (3) or MultiSelect (4)
|
||||||
|
existing = merged_fields[f['field_name']]
|
||||||
|
if existing['type'] in (3, 4) and existing.get('property', {}).get('options') and f.get('property', {}).get('options'):
|
||||||
|
existing_names = {opt['name'] for opt in existing['property']['options']}
|
||||||
|
for opt in f['property']['options']:
|
||||||
|
if opt['name'] not in existing_names:
|
||||||
|
existing['property']['options'].append(copy.deepcopy(opt))
|
||||||
|
existing_names.add(opt['name'])
|
||||||
|
|
||||||
|
# Clean up option IDs to prevent insertion errors in the target table
|
||||||
|
for f_name, f_def in merged_fields.items():
|
||||||
|
if f_def.get('property') and 'options' in f_def['property']:
|
||||||
|
for opt in f_def['property']['options']:
|
||||||
|
if 'id' in opt:
|
||||||
|
del opt['id']
|
||||||
|
|
||||||
|
# Add a custom "Source" field
|
||||||
|
merged_fields['Source'] = {
|
||||||
|
'field_name': 'Source',
|
||||||
|
'type': 1, # Text type
|
||||||
|
'property': None
|
||||||
|
}
|
||||||
|
|
||||||
|
return list(merged_fields.values())
|
||||||
|
|
||||||
|
|
||||||
|
def merge_bitables(
|
||||||
|
client: api.Client,
|
||||||
|
access_token: str,
|
||||||
|
source_app_token_1: str,
|
||||||
|
source_table_id_1: str,
|
||||||
|
source_app_token_2: str,
|
||||||
|
source_table_id_2: str,
|
||||||
|
target_app_token: str,
|
||||||
|
target_table_name: str = "Merged Table"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Core function to merge two bitables into a single one.
|
||||||
|
"""
|
||||||
|
logging.info(f"Merging {source_app_token_1}/{source_table_id_1} and {source_app_token_2}/{source_table_id_2} into {target_app_token}")
|
||||||
|
|
||||||
|
# 1. Read schema from both tables
|
||||||
|
logging.info("Reading schema from source tables...")
|
||||||
|
fields1 = get_all_fields(client, access_token, source_app_token_1, source_table_id_1)
|
||||||
|
fields2 = get_all_fields(client, access_token, source_app_token_2, source_table_id_2)
|
||||||
|
|
||||||
|
# 2. Merge schema
|
||||||
|
unified_schema = merge_schema(fields1, fields2)
|
||||||
|
|
||||||
|
# 3. Create Target Table
|
||||||
|
logging.info(f"Creating target table '{target_table_name}'...")
|
||||||
|
target_table_id = client.create_table(access_token, target_app_token, target_table_name)
|
||||||
|
|
||||||
|
# The new table will have some default columns.
|
||||||
|
# Let's get them, so we can update them or ignore them.
|
||||||
|
target_initial_fields = client.get_fields_list(access_token, target_app_token, target_table_id).get('items', [])
|
||||||
|
initial_field_names = {f['field_name']: f['field_id'] for f in target_initial_fields}
|
||||||
|
|
||||||
|
# 4. Create Fields in Target Table
|
||||||
|
logging.info("Creating fields in target table...")
|
||||||
|
target_field_map = {} # Maps field name to its new field_id in the target table
|
||||||
|
|
||||||
|
for field_def in unified_schema:
|
||||||
|
name = field_def['field_name']
|
||||||
|
ftype = field_def['type']
|
||||||
|
fprop = field_def['property']
|
||||||
|
|
||||||
|
# If the default table creation already made this field (e.g. initial '多行文本' / Text), we can update it or skip
|
||||||
|
if name in initial_field_names:
|
||||||
|
field_id = initial_field_names[name]
|
||||||
|
# Optionally update it the type is different (often default is just 'Text')
|
||||||
|
client.update_field(access_token, target_app_token, target_table_id, field_id, name, ftype, fprop)
|
||||||
|
target_field_map[name] = field_id
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = client.add_field(access_token, target_app_token, target_table_id, name, ftype, fprop)
|
||||||
|
target_field_map[name] = resp['field_id']
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to create field '{name}': {e}")
|
||||||
|
|
||||||
|
# 5. Read Records from Source Tables
|
||||||
|
logging.info("Reading records from source tables...")
|
||||||
|
records1 = get_all_records(client, access_token, source_app_token_1, source_table_id_1)
|
||||||
|
records2 = get_all_records(client, access_token, source_app_token_2, source_table_id_2)
|
||||||
|
|
||||||
|
# 6. Map and batch insert records
|
||||||
|
logging.info("Mapping and inserting records...")
|
||||||
|
|
||||||
|
def process_records(source_records, source_name):
|
||||||
|
batch = []
|
||||||
|
for r in source_records:
|
||||||
|
original_fields = r.get('fields', {})
|
||||||
|
new_fields = {}
|
||||||
|
for fname, fvalue in original_fields.items():
|
||||||
|
if fname in target_field_map:
|
||||||
|
# In bitable API, it's safer to use the new field_names (or their ids depending on the specific endpoint).
|
||||||
|
# The get_records endpoint returns field names as keys. The batch_create_records also accepts field names as keys if no 'user_id_type' strictly enforces IDs.
|
||||||
|
# We'll use the mapped names to be safe.
|
||||||
|
new_fields[fname] = fvalue
|
||||||
|
|
||||||
|
# Add Source
|
||||||
|
new_fields['Source'] = source_name
|
||||||
|
batch.append({'fields': new_fields})
|
||||||
|
return batch
|
||||||
|
|
||||||
|
target_records = []
|
||||||
|
target_records.extend(process_records(records1, f"{source_app_token_1}/{source_table_id_1}"))
|
||||||
|
target_records.extend(process_records(records2, f"{source_app_token_2}/{source_table_id_2}"))
|
||||||
|
|
||||||
|
# Feishu batch insert has a limit of 500 per request usually, so we chunk it just in case
|
||||||
|
CHUNK_SIZE = 500
|
||||||
|
for i in range(0, len(target_records), CHUNK_SIZE):
|
||||||
|
chunk = target_records[i:i + CHUNK_SIZE]
|
||||||
|
if chunk:
|
||||||
|
try:
|
||||||
|
client.batch_create_records(access_token, target_app_token, target_table_id, chunk)
|
||||||
|
except utils.LarkException as e:
|
||||||
|
logging.error(f"Failed to insert chunk of records: {e}")
|
||||||
|
|
||||||
|
logging.info(f"Successfully merged into {target_table_id}!")
|
||||||
|
return target_table_id
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example execution using vars from config.py
|
||||||
|
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
|
||||||
|
# Get tenant access token
|
||||||
|
try:
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not get access token: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
logging.info(f"Using App ID: {config.APP_ID}")
|
||||||
|
|
||||||
|
# These need to be valid app_tokens and table_ids for this script to actually work
|
||||||
|
is_placeholder = (config.MERGE_SOURCE_APP_TOKEN_1 == "source_app_token_1")
|
||||||
|
|
||||||
|
if is_placeholder:
|
||||||
|
logging.warning("Please update config.py or environment variables with valid MERGE_* tokens to execute.")
|
||||||
|
else:
|
||||||
|
merge_bitables(
|
||||||
|
client=client,
|
||||||
|
access_token=access_token,
|
||||||
|
source_app_token_1=config.MERGE_SOURCE_APP_TOKEN_1,
|
||||||
|
source_table_id_1=config.MERGE_SOURCE_TABLE_ID_1,
|
||||||
|
source_app_token_2=config.MERGE_SOURCE_APP_TOKEN_2,
|
||||||
|
source_table_id_2=config.MERGE_SOURCE_TABLE_ID_2,
|
||||||
|
target_app_token=config.MERGE_TARGET_APP_TOKEN,
|
||||||
|
target_table_name="Merged Data"
|
||||||
|
)
|
||||||
40
mock.py
Executable file
40
mock.py
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
from config import KEY_SUBMIT, KEY_GREY, KEY_GA, KEY_STARTUP, KEY_VERSION
|
||||||
|
schema = {
|
||||||
|
"version": 1,
|
||||||
|
"proposal": 5,
|
||||||
|
"delivery": 5,
|
||||||
|
"expected": 5,
|
||||||
|
"online": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
name_map = {
|
||||||
|
"version": KEY_VERSION,
|
||||||
|
"proposal": KEY_STARTUP,
|
||||||
|
"delivery": KEY_SUBMIT,
|
||||||
|
"expected": KEY_GREY,
|
||||||
|
"online": KEY_GA,
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_data = [
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"proposal": '2022-07-20',
|
||||||
|
"delivery": '2022-7-27',
|
||||||
|
"expected": '2022-07-28',
|
||||||
|
"online": '2022-07-29',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.1.0",
|
||||||
|
"proposal": '2022-07-30',
|
||||||
|
"delivery": '2022-08-04',
|
||||||
|
"expected": '2022-08-05',
|
||||||
|
"online": '2022-08-07',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.0",
|
||||||
|
"proposal": '2022-08-09',
|
||||||
|
"delivery": '2022-08-16',
|
||||||
|
"expected": '2022-08-16',
|
||||||
|
"online": '2022-08-16',
|
||||||
|
}
|
||||||
|
]
|
||||||
915
outline_view.html
Normal file
915
outline_view.html
Normal file
File diff suppressed because one or more lines are too long
10
pyproject.toml
Normal file
10
pyproject.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[project]
|
||||||
|
name = "python"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"python-dotenv>=1.2.2",
|
||||||
|
"requests>=2.32.5",
|
||||||
|
]
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
76
server.py
Normal file
76
server.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Import functions from existing scripts
|
||||||
|
from export_web_view import HTML_TEMPLATE, build_html
|
||||||
|
from get_records import get_all_records
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
|
||||||
|
PORT = int(os.getenv("PORT", 18080))
|
||||||
|
CACHE_DURATION = 60 # Cache data for 60 seconds
|
||||||
|
|
||||||
|
# Globals to store cached HTML
|
||||||
|
cached_html = None
|
||||||
|
last_fetch_time = 0
|
||||||
|
|
||||||
|
class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||||
|
daemon_threads = True
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
class FeishuHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
global cached_html, last_fetch_time
|
||||||
|
|
||||||
|
# Serve dynamic HTML at root or outline_view.html
|
||||||
|
if self.path == '/' or self.path == '/outline_view.html':
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
if cached_html is None or (current_time - last_fetch_time) > CACHE_DURATION:
|
||||||
|
logging.info("Fetching fresh records from API (Cache expired or missing)...")
|
||||||
|
try:
|
||||||
|
client = api.Client(config.LARK_HOST)
|
||||||
|
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||||
|
|
||||||
|
final_html = build_html(client, access_token)
|
||||||
|
|
||||||
|
cached_html = final_html.encode('utf-8')
|
||||||
|
last_fetch_time = current_time
|
||||||
|
logging.info(f"Successfully generated new HTML view. Cached for {CACHE_DURATION}s.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching data: {e}")
|
||||||
|
if cached_html is None:
|
||||||
|
error_msg = f"<h1>Internal Server Error: Could not fetch data</h1><p>{e}</p>".encode('utf-8')
|
||||||
|
self.wfile.write(error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write response
|
||||||
|
self.wfile.write(cached_html)
|
||||||
|
else:
|
||||||
|
# Fallback to serving static files for any other paths
|
||||||
|
super().do_GET()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
server_address = ("0.0.0.0", PORT)
|
||||||
|
try:
|
||||||
|
httpd = ThreadingHTTPServer(server_address, FeishuHandler)
|
||||||
|
logging.info(f"🚀 Server successfully started at http://localhost:{PORT}")
|
||||||
|
logging.info("You can now access the viewer directly through the browser.")
|
||||||
|
logging.info("Press Ctrl+C to stop the server.")
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info("Shutting down the server...")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not start server on port {PORT}: {e}")
|
||||||
44
test_batch_get.py
Normal file
44
test_batch_get.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
import api
|
||||||
|
|
||||||
|
class TestBatchGetChunking(unittest.TestCase):
|
||||||
|
@patch('api.request')
|
||||||
|
def test_batch_get_records_chunking(self, mock_request):
|
||||||
|
# Setup mock client
|
||||||
|
client = api.Client("https://open.feishu.cn")
|
||||||
|
access_token = "fake_token"
|
||||||
|
app_token = "fake_app"
|
||||||
|
table_id = "fake_table"
|
||||||
|
|
||||||
|
# Create 250 record IDs (should result in 3 chunks: 100, 100, 50)
|
||||||
|
record_ids = [f"rec_{i}" for i in range(250)]
|
||||||
|
|
||||||
|
# Mock responses
|
||||||
|
mock_request.side_effect = [
|
||||||
|
{'code': 0, 'data': {'records': [{'record_id': f'rec_{i}'} for i in range(0, 100)]}},
|
||||||
|
{'code': 0, 'data': {'records': [{'record_id': f'rec_{i}'} for i in range(100, 200)]}},
|
||||||
|
{'code': 0, 'data': {'records': [{'record_id': f'rec_{i}'} for i in range(200, 250)]}},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Call the method
|
||||||
|
results = client.batch_get_records(access_token, app_token, table_id, record_ids)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
self.assertEqual(len(results), 250)
|
||||||
|
self.assertEqual(mock_request.call_count, 3)
|
||||||
|
|
||||||
|
# Verify the chunks were passed correctly
|
||||||
|
first_call_payload = mock_request.call_args_list[0][0][3] # payload is the 4th positional arg
|
||||||
|
self.assertEqual(len(first_call_payload['record_ids']), 100)
|
||||||
|
self.assertEqual(first_call_payload['record_ids'][0], "rec_0")
|
||||||
|
self.assertEqual(first_call_payload['record_ids'][99], "rec_99")
|
||||||
|
|
||||||
|
last_call_payload = mock_request.call_args_list[2][0][3]
|
||||||
|
self.assertEqual(len(last_call_payload['record_ids']), 50)
|
||||||
|
self.assertEqual(last_call_payload['record_ids'][0], "rec_200")
|
||||||
|
self.assertEqual(last_call_payload['record_ids'][49], "rec_249")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
34
utils.py
Normal file
34
utils.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
import json
|
||||||
|
import logging as logger
|
||||||
|
import requests
|
||||||
|
class LarkException(Exception):
|
||||||
|
def __init__(self, code=0, msg=None):
|
||||||
|
self.code = code
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "{}:{}".format(self.code, self.msg)
|
||||||
|
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
def request(method, url, headers, payload={}, params=None):
|
||||||
|
response = requests.request(method, url, headers=headers, json=payload, params=params)
|
||||||
|
logger.info("URL: " + url)
|
||||||
|
logger.info("X-Tt-Logid: " + response.headers['X-Tt-Logid'])
|
||||||
|
logger.info("headers:\n"+json.dumps(headers,indent=2, ensure_ascii=False))
|
||||||
|
logger.info("payload:\n"+json.dumps(payload,indent=2, ensure_ascii=False))
|
||||||
|
resp = {}
|
||||||
|
if response.text[0] == '{':
|
||||||
|
resp = response.json()
|
||||||
|
logger.info("response:\n"+json.dumps(resp,indent=2, ensure_ascii=False))
|
||||||
|
else:
|
||||||
|
logger.info("response:\n"+response.text)
|
||||||
|
code = resp.get("code", -1)
|
||||||
|
if code == -1:
|
||||||
|
code = resp.get("StatusCode", -1)
|
||||||
|
if code == -1 and response.status_code != 200:
|
||||||
|
response.raise_for_status()
|
||||||
|
if code != 0:
|
||||||
|
raise LarkException(code=code, msg=resp.get("msg", ""))
|
||||||
|
return resp
|
||||||
110
uv.lock
generated
Normal file
110
uv.lock
generated
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.2.25"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
|
]
|
||||||
117
列出数据表.md
Normal file
117
列出数据表.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# 列出数据表
|
||||||
|
|
||||||
|
列出多维表格中的所有数据表,包括其 ID、版本号和名称。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token/tables
|
||||||
|
HTTP Method | GET
|
||||||
|
接口频率限制 | [20 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 获取数据表信息(base:table:read)<br>查看、评论、编辑和管理多维表格(bitable:app)<br>查看、评论和导出多维表格(bitable:app:readonly)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 多维表格 App 的唯一标识。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。<br>**示例值**:"appbcbWCzen6D8dezhoCH2RpMAh"
|
||||||
|
|
||||||
|
### 查询参数
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
page_token | string | 否 | 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果<br>**示例值**:tblsRc9GRRXKqhvW
|
||||||
|
page_size | int | 否 | 分页大小<br>**示例值**:10<br>**默认值**:`20`<br>**数据校验规则**:<br>- 最大值:`100`
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
has_more | boolean | 是否还有更多项
|
||||||
|
page_token | string | 分页标记,当 has_more 为 true 时,会同时返回新的 page_token,否则不返回 page_token
|
||||||
|
total | int | 总数
|
||||||
|
items | app.table\[\] | 数据表信息
|
||||||
|
table_id | string | 数据表 ID
|
||||||
|
revision | int | 数据表的版本号。对数据表进行修改时更新,如新增、删除记录,修改数据表名称等,初始为 1,每次更新+1
|
||||||
|
name | string | 数据表名称
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"has_more": false,
|
||||||
|
"page_token": "tblKz5D60T4JlfcT",
|
||||||
|
"total": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"table_id": "tblKz5D60T4JlfcT",
|
||||||
|
"revision": 1,
|
||||||
|
"name": "数据表1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
200 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
200 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
200 | 1254002 | Fail | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
200 | 1254004 | WrongTableId | table_id 错误
|
||||||
|
200 | 1254005 | WrongViewId | view_id 错误
|
||||||
|
200 | 1254006 | WrongRecordId | 检查 record_id
|
||||||
|
200 | 1254007 | EmptyValue | 空值
|
||||||
|
200 | 1254008 | EmptyView | 空视图
|
||||||
|
200 | 1254009 | WrongFieldId | 字段 id 错误
|
||||||
|
200 | 1254010 | ReqConvError | 请求错误
|
||||||
|
400 | 1254011 | Page size must greater than 0. | 确认page_size参数的值符合要求。
|
||||||
|
200 | 1254030 | TooLargeResponse | 响应体过大
|
||||||
|
400 | 1254036 | Base is copying, please try again later. | 多维表格副本复制中,稍后重试
|
||||||
|
200 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
200 | 1254041 | TableIdNotFound | table_id 不存在
|
||||||
|
200 | 1254042 | ViewIdNotFound | view_id 不存在
|
||||||
|
200 | 1254043 | RecordIdNotFound | record_id 不存在
|
||||||
|
200 | 1254044 | FieldIdNotFound | field_id 不存在
|
||||||
|
200 | 1254060 | TextFieldConvFail | 多行文本字段错误
|
||||||
|
200 | 1254061 | NumberFieldConvFail | 数字字段错误
|
||||||
|
200 | 1254062 | SingleSelectFieldConvFail | 单选字段错误
|
||||||
|
200 | 1254063 | MultiSelectFieldConvFail | 多选字段错误
|
||||||
|
200 | 1254064 | DatetimeFieldConvFail | 日期字段错误
|
||||||
|
200 | 1254065 | CheckboxFieldConvFail | 复选框字段错误
|
||||||
|
200 | 1254066 | UserFieldConvFail | 人员字段有误。原因可能是:<br>- `user_id_type` 参数指定的 ID 类型与传入的 ID 类型不匹配<br>- 传入了不识别的类型或结构,目前只支持填写 `id` 参数,且需要传入数组<br>- 跨应用传入了 `open_id`。如果跨应用传入 ID,建议使用 `user_id`。不同应用获取的 `open_id` 不能交叉使用
|
||||||
|
200 | 1254067 | LinkFieldConvFail | 关联字段错误
|
||||||
|
200 | 1254100 | TableExceedLimit | 数据表或仪表盘数量超限。每个多维表格中,数据表加仪表盘的数量最多为 100 个
|
||||||
|
200 | 1254101 | ViewExceedLimit | 视图数量超限, 限制200个
|
||||||
|
200 | 1254102 | FileExceedLimit | 超限
|
||||||
|
200 | 1254103 | RecordExceedLimit | 记录数量超限, 限制20,000条
|
||||||
|
200 | 1254104 | RecordAddOnceExceedLimit | 单次添加记录数量超限, 限制500条
|
||||||
|
200 | 1254130 | TooLargeCell | 格子内容过大
|
||||||
|
200 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
200 | 1254291 | Write conflict | 同一个数据表(table) 不支持并发调用写接口,请检查是否存在并发调用写接口。写接口包括:新增、修改、删除记录;新增、修改、删除字段;修改表单;修改视图等。
|
||||||
|
200 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
403 | 1254302 | The role has no permissions. | 无访问权限, 常由表格开启了高级权限造成, 如果是用应用请求的话,目前有两种方法对应用赋予高级权限,第一种方法为在表格中添加应用为协作者并将应用设置为管理员,第二种方法为在一个用户群中将应用添加为机器人, 并在高级权限的角色中添加该用户群,从而赋予对应的权限。
|
||||||
|
200 | 1255001 | InternalError | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1255002 | RpcError | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1255003 | MarshalError | 序列化错误,有疑问可咨询客服
|
||||||
|
200 | 1255004 | UmMarshalError | 反序列化错误
|
||||||
|
200 | 1255005 | ConvError | 内部错误,有疑问可咨询客服处
|
||||||
|
504 | 1255040 | Request timed out, please try again later. | 请求超时,请进行重试
|
||||||
124
创建多维表格.md
Normal file
124
创建多维表格.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# 创建多维表格
|
||||||
|
|
||||||
|
在指定文件夹中创建一个多维表格,包含一个空白的数据表。
|
||||||
|
|
||||||
|
**注意事项**:要基于模板创建多维表格,可先获取模板多维表格 `app_token` 作为文件 token,再调用[复制文件](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/drive-v1/file/copy)接口创建多维表格。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps
|
||||||
|
HTTP Method | POST
|
||||||
|
接口频率限制 | [20 次/分钟](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 创建多维表格(base:app:create)<br>查看、评论、编辑和管理多维表格(bitable:app)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
name | string | 否 | 多维表格 App 名称。最长为 255 个字符。<br>**示例值**:"一篇新的多维表格"
|
||||||
|
folder_token | string | 否 | 多维表格 App 归属文件夹。默认为空,表示多维表格将被创建在云空间根目录。了解如何获取文件夹 Token,参考[如何获取云文档资源相关 Token](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#08bb5df6)。<br>**注意**:<br>请确保调用身份拥有在该文件夹中的编辑权限。若应用使用的是 `tenant_access_token` 权限,此处仅可指定应用创建的文件夹。详情参考[如何为应用开通云文档相关资源的权限](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-add-permissions-to-app)。<br>**示例值**:"fldcnqquW1svRIYVT2Np6Iabcef"
|
||||||
|
time_zone | string | 否 | 文档时区,详情参考[文档时区介绍](https://feishu.feishu.cn/docx/YKRndTM7VoyDqpxqqeEcd67MnEf)。<br>**示例值**:"Asia/Macau"
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name":"一篇新的多维表格",
|
||||||
|
"folder_token": "fldcnqquW1svRIYVT2Np6Iabcef"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
app | app | 返回响应体
|
||||||
|
app_token | string | 多维表格的唯一标识 app_token
|
||||||
|
name | string | 多维表格的名称
|
||||||
|
folder_token | string | 多维表格 App 归属文件夹
|
||||||
|
url | string | 多维表格 App 的 URL 链接
|
||||||
|
default_table_id | string | 默认创建的数据表 ID
|
||||||
|
time_zone | string | 文档时区
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"app": {
|
||||||
|
"app_token": "S404b*****e9PQsYDWYcNryFn0g",
|
||||||
|
"default_table_id": "tbl********oumSQ",
|
||||||
|
"folder_token": "fldcnqquW1svRIYVT2Np6Iabcef",
|
||||||
|
"name": "一篇新的多维表格",
|
||||||
|
"url": "https://example.feishu.cn/base/S404b*****e9PQsYDWYcNryFn0g"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"msg": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
200 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
200 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
200 | 1254002 | Fail | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
200 | 1254004 | WrongTableId | table_id 错误
|
||||||
|
200 | 1254005 | WrongViewId | view_id 错误
|
||||||
|
200 | 1254006 | WrongRecordId | 检查 record_id
|
||||||
|
200 | 1254007 | EmptyValue | 空值
|
||||||
|
200 | 1254008 | EmptyView | 空视图
|
||||||
|
200 | 1254009 | WrongFieldId | 字段 id 错误
|
||||||
|
200 | 1254010 | ReqConvError | 请求错误
|
||||||
|
200 | 1254025 | InvalidCopyTypes | Copy Type参数无效
|
||||||
|
200 | 1254030 | TooLargeResponse | 响应体过大
|
||||||
|
400 | 1254031 | InvalidAppName | 名称不能超过255个字符
|
||||||
|
400 | 1254036 | Base is copying, please try again later. | 复制多维表格为异步操作,该错误码表示当前多维表格仍在复制中,在复制期间无法操作当前多维表格。需要等待复制完成后再操作
|
||||||
|
200 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
200 | 1254041 | TableIdNotFound | table_id 不存在
|
||||||
|
200 | 1254042 | ViewIdNotFound | view_id 不存在
|
||||||
|
200 | 1254043 | RecordIdNotFound | record_id 不存在
|
||||||
|
200 | 1254044 | FieldIdNotFound | field_id 不存在
|
||||||
|
200 | 1254060 | TextFieldConvFail | 多行文本字段错误
|
||||||
|
200 | 1254061 | NumberFieldConvFail | 数字字段错误
|
||||||
|
200 | 1254062 | SingleSelectFieldConvFail | 单选字段错误
|
||||||
|
200 | 1254063 | MultiSelectFieldConvFail | 多选字段错误
|
||||||
|
200 | 1254064 | DatetimeFieldConvFail | 日期字段错误
|
||||||
|
200 | 1254065 | CheckboxFieldConvFail | 复选框字段错误
|
||||||
|
200 | 1254066 | UserFieldConvFail | 人员字段有误。原因可能是:<br>- `user_id_type` 参数指定的 ID 类型与传入的 ID 类型不匹配<br>- 传入了不识别的类型或结构,目前只支持填写 `id` 参数,且需要传入数组<br>- 跨应用传入了 `open_id`。如果跨应用传入 ID,建议使用 `user_id`。不同应用获取的 `open_id` 不能交叉使用
|
||||||
|
200 | 1254067 | LinkFieldConvFail | 关联字段错误
|
||||||
|
200 | 1254100 | TableExceedLimit | 数据表或仪表盘数量超限。每个多维表格中,数据表加仪表盘的数量最多为 100 个
|
||||||
|
200 | 1254101 | ViewExceedLimit | 视图数量超限,限制 200 个,包括公共视图、锁定视图和个人视图(即只有视图所有者才能看到的视图)
|
||||||
|
200 | 1254102 | FileExceedLimit | 文档数量超限
|
||||||
|
200 | 1254103 | RecordExceedLimit | 记录数量超限,限制 20,000 条
|
||||||
|
200 | 1254104 | RecordAddOnceExceedLimit | 单次添加记录数量超限,限制 500 条
|
||||||
|
200 | 1254130 | TooLargeCell | 格子内容过大
|
||||||
|
200 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
200 | 1254291 | Write conflict | 同一个数据表 (table) 不支持并发调用写接口,请检查是否存在并发调用写接口。写接口包括:新增、修改、删除记录;新增、修改、删除字段;修改表单;修改视图等。
|
||||||
|
200 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
200 | 1254304 | PermNotAllow | 仅企业版和旗舰版飞书支持行列权限
|
||||||
|
403 | 1254701 | DriveNodePermNotAllow | 对目标云空间节点没有权限
|
||||||
|
404 | 1254702 | DriveNodeNotExist | 云空间节点不存在,检查参数正确性
|
||||||
|
400 | 1254800 | InvalidParameter | 参数错误,请根据msg修正后重试
|
||||||
|
200 | 1255001 | InternalError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255002 | RpcError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255003 | MarshalError | 序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255004 | UmMarshalError | 反序列化错误
|
||||||
|
200 | 1255005 | ConvError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
504 | 1255040 | 请求超时 | 进行重试
|
||||||
108
复制多维表格.md
Normal file
108
复制多维表格.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# 复制多维表格
|
||||||
|
|
||||||
|
复制一个多维表格,可以指定复制到某个有权限的文件夹下。
|
||||||
|
|
||||||
|
**注意事项**:当多维表格记录数超 50,000 条可复制上限时,仅可复制多维表格结构。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
调用此接口前,请确保当前调用身份(tenant_access_token 或 user_access_token)已有多维表格和目标文件夹的阅读、编辑等文档权限,否则接口将返回 HTTP 403 或 400 状态码。了解更多,参考[如何为应用或用户开通云文档权限](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#16c6475a)。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token/copy
|
||||||
|
HTTP Method | POST
|
||||||
|
接口频率限制 | [20 次/分钟](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 复制多维表格(base:app:copy)<br>查看、评论、编辑和管理多维表格(bitable:app)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 要复制的多维表格 App 的唯一标识。不同形态的多维表格,其 app_token 的获取方式不同,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview)获取。<br>**示例值**:"AW3Qbtr2cakCnesXzXVbbsrIcVT "
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
name | string | 否 | 多维表格 App 的名称<br>**示例值**:"一篇新的多维表格"
|
||||||
|
folder_token | string | 否 | 了解如何获取文件夹 Token,参考[如何获取云文档资源相关 Token](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#08bb5df6)。<br>**注意**:<br>请确保调用身份拥有在该文件夹中的编辑权限。若应用使用的是 `tenant_access_token` 权限,此处仅可指定应用创建的文件夹。详情参考[如何为应用开通云文档相关资源的权限](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-add-permissions-to-app)。<br>**示例值**:"fldcnqquW1svRIYVT2Np6Iabcef"
|
||||||
|
without_content | boolean | 否 | 是否复制多维表格中的内容,默认 false,即复制多维表格中的内容。可取值:<br>* true:不复制<br>* false:复制<br>**示例值**:false
|
||||||
|
time_zone | string | 否 | 文档时区,详情参考[文档时区介绍](https://feishu.feishu.cn/docx/YKRndTM7VoyDqpxqqeEcd67MnEf)。<br>**示例值**:"Asia/Shanghai"
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "一篇新的多维表格",
|
||||||
|
"folder_token": "fldcnqquW1svRIYVT2Np6Iabcef",
|
||||||
|
"without_content": false,
|
||||||
|
"time_zone": "Asia/Shanghai"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
app | app | 返回响应体
|
||||||
|
app_token | string | 多维表格的唯一标识 app_token
|
||||||
|
name | string | 多维表格的名称
|
||||||
|
folder_token | string | 多维表格 App 归属文件夹
|
||||||
|
url | string | 多维表格 App 的 URL 链接
|
||||||
|
time_zone | string | 文档时区
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"app": {
|
||||||
|
"app_token": "S404b*****e9PQsYDWYcNryFn0g",
|
||||||
|
"name": "一篇新的多维表格",
|
||||||
|
"folder_token": "fldbco*****CIMltVc",
|
||||||
|
"url": "https://example.feishu.cn/base/S404b*****e9PQsYDWYcNryFn0g",
|
||||||
|
"time_zone": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
400 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
400 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
400 | 1254002 | Fail | 内部错误,有疑问可咨询客服
|
||||||
|
400 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
400 | 1254031 | InvalidAppName | 多维表格名称格式错误,长度不超过 100 个字符,不能包含 ? / \ * : [ ]
|
||||||
|
400 | 1254036 | Base is copying, please try again later. | 多维表格副本复制中,稍后重试
|
||||||
|
404 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
400 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
400 | 1254291 | Write conflict | 同一个数据表(table) 不支持并发调用写接口,请检查是否存在并发调用写接口。写接口包括:新增、修改、删除记录;新增、修改、删除字段;修改表单;修改视图等。
|
||||||
|
403 | 1254304 | PermNotAllow | 仅企业版和旗舰版飞书支持行列权限
|
||||||
|
403 | 1254701 | DriveNodePermNotAllow | 目标文件夹没有权限
|
||||||
|
404 | 1254702 | DriveNodeNotExist | 目标文件夹不存在
|
||||||
|
400 | 1254800 | InvalidParameter | 参数错误,请根据msg修正后重试
|
||||||
|
500 | 1255001 | InternalError | 内部错误,有疑问可咨询客服
|
||||||
|
500 | 1255002 | RpcError | 内部错误,有疑问可咨询客服
|
||||||
|
500 | 1255003 | MarshalError | 序列化错误,有疑问可咨询客服
|
||||||
|
500 | 1255004 | UmMarshalError | 反序列化错误
|
||||||
|
500 | 1255005 | ConvError | 内部错误,有疑问可咨询客服处
|
||||||
|
504 | 1255040 | 请求超时 | 进行重试
|
||||||
194
多维表格常见问题.md
Normal file
194
多维表格常见问题.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# 多维表格常见问题
|
||||||
|
|
||||||
|
## 1. 如何在多维表格中上传附件?
|
||||||
|
|
||||||
|
**第 1 步**:调用[上传素材](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/drive-v1/media/upload_all)或[分片上传素材](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/drive-v1/media/upload_prepare)接口将附件上传至多维表格,获取文件的 file_token。
|
||||||
|
|
||||||
|
**第 2 步**:调用[新增记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/create)或[更新记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/update)接口,将 file_token 的值传入多维表格中。请求示例如下所示:
|
||||||
|
file_token 仅支持在当前多维表格中使用,要上传附件至其他多维表格,你仍需要重新上传素材获取新的 file_token。
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"附件": [
|
||||||
|
{
|
||||||
|
"file_token": "boxbcCFb2dBwMK9S8kDILk1tayh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_token": "boxbcCFb2dBwMK9S8kDILk1tayh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"附件": [
|
||||||
|
{
|
||||||
|
"file_token": "boxbcCFb2dBwMK9S8kDILk1tayh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_token": "boxbcCFb2dBwMK9S8kDILk1tayh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 如何下载多维表格中的附件?
|
||||||
|
|
||||||
|
**第 1 步**:调用[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)获取多维表格中附件的 file_token。
|
||||||
|
|
||||||
|
**第 2 步**:调用[下载素材](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/drive-v1/media/download)或者[获取素材临时下载链接](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/drive-v1/media/batch_get_tmp_download_url)接口下载附件。
|
||||||
|
|
||||||
|
若多维表格开启了高级权限,你需要添加 extra 参数作为 URL 查询参数鉴权。你可通过以下方式获取 extra 参数:
|
||||||
|
|
||||||
|
1. 调用[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口,响应示例中会返回附件的下载链接,如下所示:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"has_more": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"附件": [
|
||||||
|
{
|
||||||
|
"file_token": "RSkabsaphoy6yVxK0mGcggabcef",
|
||||||
|
"name": "74d2c703524489dbabcef.png",
|
||||||
|
"size": 87123,
|
||||||
|
"tmp_url": "https://open.feishu.cn/open-apis/drive/v1/medias/batch_get_tmp_download_url?file_tokens=RSkabsaphoy6yVxK0mGcggabcef&extra={"bitablePerm":{"tableId":"tblz8ExGaVhuiSD1","rev":32}}", // 素材临时下载链接对应的 URL 值,需要对此字符串进行 URL 编码
|
||||||
|
"type": "image/png",
|
||||||
|
"url": "https://open.feishu.cn/open-apis/drive/v1/medias/RSkabsaphoy6yVxK0mGcggabcef/download?extra={"bitablePerm":{"tableId":"tblz8ExGaVhuiSD1","rev":32}}" // 下载素材对应的 URL 值,需要对此字符串进行 URL 编码
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"record_id": "recbMzD0zT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"msg": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 构建示例如下所示:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{"bitablePerm":{"tableId":"tblO6OeNZxfabcef","attachments":{"fld32zZi5I":{"rec0BuOHq":["boxbcsQNT0JsmrztOnX530abcef"]}}}}
|
||||||
|
// 转义后
|
||||||
|
https://open.feishu.cn/open-apis/drive/v1/medias/boxbcsQNT0JsmrztOnX530abcef/download?extra=%7B%22bitablePerm%22%3A%7B%22tableId%22%3A%22tblO6OeNZxfabcef%22%2C%22attachments%22%3A%7B%22fld32zZi5I%22%3A%7B%22rec0BuOHq%22%3A%5B%22boxbcsQNT0JsmrztOnX530abcef%22%5D%7D%7D%7D%7D
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 调用多维表格相关接口,返回 1254040、1254041、1254042、1254043、1254044 等 NotFound 错误码,该如何解决?
|
||||||
|
|
||||||
|
上述这些错误码表示未找到到标识各类 ID 对应的资源。你可参考以下方法解决:
|
||||||
|
|
||||||
|
1. 确认 ID 是否正确。你可参考[多维表格概述](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview)标获取各类资源的 ID
|
||||||
|
2. 如果 ID 对应的资源刚创建完成后出现此类报错,可能是遇到了服务器的主从延迟问题。你可尝试等待几秒之后重试。
|
||||||
|
|
||||||
|
## 4. 调用查询记录接口成功,数据返回为空,但实际多维表格存在数据,该如何解决?
|
||||||
|
|
||||||
|
这一般是由于多维表格开通了高级权限,导致调用身份权限不足。你需给予调用身份数据表的 **可管理** 权限或多维表格的 **可管理** 等权限,再重新调用。具体步骤如下所示:
|
||||||
|
|
||||||
|
- 对用户授予可管理权限,你可在 **多维表格高级权限设置** 中添加用户,为用户开通足够权限。具体可参考飞书帮助中心文档[使用多维表格高级权限](https://www.feishu.cn/hc/zh-CN/articles/588604550568-%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BB%B4%E8%A1%A8%E6%A0%BC%E9%AB%98%E7%BA%A7%E6%9D%83%E9%99%90)。
|
||||||
|
|
||||||
|
- 对应用授予可管理权限,你需通过多维表格页面右上方 **「...」** -> **「...更多」** ->**「添加文档应用」** 入口为应用添加可管理权限。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
在 **添加文档应用** 前,你需确保目标应用至少开通了一个多维表格的 [API 权限](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/scope-list)。否则你将无法在文档应用窗口搜索到目标应用。
|
||||||
|
|
||||||
|
- 你也可以在 **多维表格高级权限设置** 中添加用户或一个包含应用的群组, 给予这个群自定义的读写等权限。
|
||||||
|
|
||||||
|
## 5. 调用查询记录接口,复选框返回数据为空,如何解决?
|
||||||
|
|
||||||
|
在多维表格中,如果字段为空值,则查询记录接口不返回数据。相应地,如果复选框字段为空值(即用户没有选中和取消选中过该复选框),则查询记录接口也不返回数据。在此场景下,由于空值效果与复选框为 `false` 效果相同,开发者需自行兼容该空值场景。
|
||||||
|
## 6. 如何筛选自定义编号类型的自动编号字段?
|
||||||
|
|
||||||
|
要筛选含有固定字符的自动编号字段,需将自定义的固定字符去除,再筛选,否则将返回空数据。如下图,自定义的固定字符为 2024,在调用查询记录接口时,需确保仅 value 的值为自增部分数字,不包含自定义的 2024。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 7. 调用查询记录接口,如何筛选人员字段?
|
||||||
|
|
||||||
|
对于人员字段,你需在 value 中传入人员的用户 ID。用户 ID 的类型与查询记录接口中查询参数 `user_id_type` 指定的类型一致,默认为 Open ID 类型。以下为筛选人员 ID 为 `ou_00d9ea2d7bcd6b6aa7d71dd88deabcef` 和 `ou_5fdfc3d312b24d28224769baf52abcef` 的请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"view_id": "vewfrk8iX4",
|
||||||
|
"field_names": [
|
||||||
|
"创建人"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"conjunction": "and",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"field_name": "创建人",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": [
|
||||||
|
"ou_00d9ea2d7bcd6b6aa7d71dd88deabcef",
|
||||||
|
"ou_5fdfc3d312b24d28224769baf52abcef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## 8. 查询记录接口是否支持查询特定行,如数据表第 10 行~20 行的数据?
|
||||||
|
|
||||||
|
1. 在多维表格中新增一个自动编号字段,编号类型选择自增数字。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. 筛选该自动编号。例如要查询第 10~20 行数据,则筛选小于 21、大于 9 的编号,然后调用[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口,请求体如下所示:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"view_id": "vewfrk8iX4",
|
||||||
|
"field_names": [],
|
||||||
|
"filter": {
|
||||||
|
"conjunction": "and",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"field_name": "编号",
|
||||||
|
"operator": "isGreater",
|
||||||
|
"value": [
|
||||||
|
"9"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field_name": "编号",
|
||||||
|
"operator": "isLess",
|
||||||
|
"value": [
|
||||||
|
"21"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
你也可以调用[批量获取记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/batch_get)接口,使用记录的 Record ID 来查询,获取多条记录的数据。
|
||||||
|
|
||||||
|
## 9. 如何获取多维表格指定数据表的总记录数(或总行数)?
|
||||||
|
|
||||||
|
你可以调用[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口,在请求体中将 `conditions` 字段设为空或不设置,以下为请求体示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"view_id": "vewfrk8iX4", // 请替换为实际的 view_id
|
||||||
|
"filter": {
|
||||||
|
"conjunction": "and",
|
||||||
|
"conditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
若请求成功,响应体中将返回 `total` 字段,即为记录总数。
|
||||||
272
多维表格概述.md
Normal file
272
多维表格概述.md
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
# 多维表格概述
|
||||||
|
多维表格(Base)是全新的业务管理工具,帮助用户重构工作应用和团队协同模式,高效在线协同数据,随心构建个性化应用,轻松掌控全盘业务数据,和团队一起创造效率的无限可能。多维表格可以是一个表格,也可以是无数个应用。它拥有强大的底层开放能力,你可以通过多维表格 API 轻松打通内部其他业务系统,让业务数据通畅流转,实时同步。
|
||||||
|
## 典型案例
|
||||||
|
|
||||||
|
开放平台提供了集成多维表格能力的客户实践案例:
|
||||||
|
|
||||||
|
- [芝麻开门,在飞书五天打造门禁管理系统](https://open.feishu.cn/solutions/detail/wiseasy)
|
||||||
|
- [飞书集成平台,让企业服务行业项目管理与销售体验升级](https://open.feishu.cn/solutions/detail/cloudwise)
|
||||||
|
## 接入流程
|
||||||
|
|
||||||
|
接入多维表格 OpenAPI 的流程如下图所示。了解更多,参考[云文档概述](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/docs-overview) 的 **接入流程** 一节。
|
||||||
|

|
||||||
|
|
||||||
|
## 开发教程
|
||||||
|
体验以下多维表格相关教程,了解如何运用多维表格 API 助力企业高效协作。
|
||||||
|
|
||||||
|
- [快速调用一个服务端 API(以创建多维表格接口为例)](https://open.feishu.cn/document/uAjLw4CM/uMzNwEjLzcDMx4yM3ATM/call-a-server-api-base-example/introduction)
|
||||||
|
- [快速接入多维表格](https://open.feishu.cn/document/home/quick-access-to-base/preparation)
|
||||||
|
- [多维表格管理敏捷项目](https://open.feishu.cn/document/home/agile-project-cycle-management-based-on-bitable/introduction)
|
||||||
|
|
||||||
|
## 鉴权说明
|
||||||
|
使用 tenant_access_token 访问多维表格资源之前,请确保你的应用已经是多维表格的所有者或者协作者,否则会调用失败。
|
||||||
|
你可通过添加文档应用的方式将应用添加为协作者,详情参考[开通文档、电子表格等其它云文档资源权限](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-add-permissions-to-app#223459af);或通过应用身份创建一篇多维表格,再使用 tenant_access_token 来调用接口。
|
||||||
|
|
||||||
|
## 使用限制
|
||||||
|
|
||||||
|
使用多维表格接口,整体有以下限制或说明:
|
||||||
|
- 对于接口的批量操作,单次最高为 1,000 条记录,且响应状态是全部成功或者失败,不存在部分成功或失败的结果。
|
||||||
|
- 为保证稳定性,建议对单一多维表格同时只请求**一次** API 写操作。
|
||||||
|
单一多维表格中,各个资源的数量限制如下所示:
|
||||||
|
|
||||||
|
| **资源** | **最大支持数量** |
|
||||||
|
| --------- | ---------- |
|
||||||
|
| 记录 | 不同租户的最大支持数量不同,开放平台没有额外限制。你可以在多维表格数据表 UI 中点击查看。  |
|
||||||
|
| 字段 | 300,对于公式类型的字段,最多支持 100 个 | |
|
||||||
|
| 视图 | 200 |
|
||||||
|
| 数据表+仪表盘 | 100 |
|
||||||
|
| 高级权限自定义角色 | 30 |
|
||||||
|
| 高级权限协作者 | 200 |
|
||||||
|
|
||||||
|
## 资源介绍
|
||||||
|
|
||||||
|
多维表格开放了多维表格 App、数据表、视图、记录、字段、仪表盘、高级权限等多种资源的接口。本小节介绍这几类资源的含义。了解更多多维表格的概念和使用说明,可参考飞书帮助中心文档[快速上手多维表格](https://www.feishu.cn/hc/zh-CN/articles/697278684206-%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E5%A4%9A%E7%BB%B4%E8%A1%A8%E6%A0%BC)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 多维表格 app
|
||||||
|
|
||||||
|
一个多维表格可以理解成是一个应用(app,但不是在开发者后台创建的应用),标记该应用的唯一标识为 `app_token`。作为一个应用,多维表格有多种形态:可以作为一个独立应用,也可以作为一个模块(block)与文档、电子表格结合。
|
||||||
|
|
||||||
|
#### 多维表格形态
|
||||||
|
|
||||||
|
| **多维表格形态** | **资源定义** | **含义** |
|
||||||
|
| ---------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 文件夹中的多维表格 | Base app | 存储在飞书云空间(云盘)文件夹中的多维表格。URL 以 **feishu.cn/base** 开头 |
|
||||||
|
| 知识库下的多维表格 | Base app 和 wiki node | 放置在知识库中的多维表格。URL 以 **feishu.cn/wiki** 开头 |
|
||||||
|
| 文档嵌入多维表格 | Base docx block | 即在"文档"中插入的多维表格,URL 以 **feishu.cn/docx** 开头 |
|
||||||
|
| 电子表格嵌入多维表格 | Base sheet block | 在电子表格中嵌入的多维表格,URL 以 **feishu.cn/sheets** 开头 |
|
||||||
|
|
||||||
|
#### 多维表格 app_token 获取方式
|
||||||
|
|
||||||
|
不同形态的多维表格,其 `app_token` 的获取方式不同,具体如下所示。
|
||||||
|
|
||||||
|
##### **文件夹中的多维表格**
|
||||||
|
|
||||||
|
该类多维表格的 app_token 为 URL 下图高亮部分:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### **知识库下的多维表格**
|
||||||
|
|
||||||
|
需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取该类多维表格的 app_token。如下返回示例,当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值 `AW3Qbtr2cakCnesXzXVbbsrIcVT` 是多维表格的 `app_token`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"node":{
|
||||||
|
"space_id":"6946843325487912356",
|
||||||
|
"node_token":"wikcnKQ1k3p******8Vabcef",
|
||||||
|
"obj_token":"AW3Qbtr2cakCnesXzXVbbsrIcVT", // 多维表格的 app_token
|
||||||
|
"obj_type":"bitable",
|
||||||
|
"parent_node_token":"wikcnKQ1k3p******8Vabcef",
|
||||||
|
"node_type":"origin",
|
||||||
|
"origin_node_token":"wikcnKQ1k3p******8Vabcef",
|
||||||
|
"origin_space_id":"6946843325487912356",
|
||||||
|
"has_child":false,
|
||||||
|
"title":"标题",
|
||||||
|
"obj_create_time":"1642402428",
|
||||||
|
"obj_edit_time":"1642402428",
|
||||||
|
"node_create_time":"1642402428",
|
||||||
|
"creator":"ou_xxxxx",
|
||||||
|
"owner":"ou_xxxxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### **文档嵌入多维表格**
|
||||||
|
|
||||||
|
文档中嵌入的多维表格,需要调用文档相关接口获取多维表格的 `app_token`。
|
||||||
|
调用[获取文档所有块](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document-block/list),在返回结果中检索,其中 `bitable.token` 字段的值 `AW3Qbtr2cakCnesXzXVbbsrIcVT_tblkIYhz52o6G5nx`是用 `_` 隔开的 `app_token` 和 `table_id`;
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bitable": {
|
||||||
|
"token": "AW3Qbtr2cakCnesXzXVbbsrIcVT_tblkIYhz52o6G5nx"
|
||||||
|
},
|
||||||
|
"block_id": "Mgeadqo4CoeoOGxI7Lgb4GNicEd",
|
||||||
|
"block_type": 18,
|
||||||
|
"parent_id": "YUqpdO2eLo7xJdxy5RQbuQBdctf"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### **电子表格嵌入多维表格**
|
||||||
|
|
||||||
|
电子表格中嵌入的多维表格,需要调用电子表格相关接口获取多维表格的 `app_token`。
|
||||||
|
若电子表格中嵌有多维表格,需调用[获取表格元数据](https://open.feishu.cn/document/ukTMukTMukTM/uETMzUjLxEzM14SMxMTN),在返回结果中将返回 `blockInfo`。其中,当 `blockType` 的值为 `BITABLE_BLOCK` 时,blockToken 字段的值`AW3Qbtr2cakCnesXzXVbbsrIcVT_tblkIYhz52o6G5nx` 是用 `_` 隔开的 `app_token` 和 `table_id`。
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"blockInfo": {
|
||||||
|
"blockToken": "AW3Qbtr2cakCnesXzXVbbsrIcVT_tblkIYhz52o6G5nx",
|
||||||
|
"blockType": "BITABLE_BLOCK"
|
||||||
|
},
|
||||||
|
"columnCount": 0,
|
||||||
|
"frozenColCount": 0,
|
||||||
|
"frozenRowCount": 0,
|
||||||
|
"index": 0,
|
||||||
|
"rowCount": 0,
|
||||||
|
"sheetId": "***",
|
||||||
|
"title": "*** "
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据表 table
|
||||||
|
|
||||||
|
多维表格的数据容器,一个多维表格中至少有一个数据表(table),也可能有多个数据表。每个数据表都有唯一标识 `table_id`。`table_id` 在一个多维表格 App 中唯一,在全局不一定唯一。
|
||||||
|
你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的唯一标识。你也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 视图 view
|
||||||
|
|
||||||
|
指数据的汇总和展现形式。视图有多种类型,包括表格视图、看板视图、画册视图、甘特视图和表单视图等,可参考飞书帮助中心文档[视图类型](https://www.feishu.cn/hc/zh-CN/articles/360049067931-%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BB%B4%E8%A1%A8%E6%A0%BC%E8%A7%86%E5%9B%BE#tabs0|lineguid-6kjqG)。一个数据表至少有一个视图,可能有多个视图。每个视图都有唯一标识 `view_id`,`view_id` 在一个多维表格中唯一,在全局不一定唯一。
|
||||||
|
你可通过多维表格 URL 获取 `view_id`,下图高亮部分即为当前视图的唯一标识。你也可通过[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)接口获取 `view_id`。暂时无法获取到嵌入到文档中的多维表格的 `view_id`。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **表单视图 form**
|
||||||
|
|
||||||
|
表单视图是多维表格的一种视图类型,形式类似于问卷,可以用来收集信息和数据。每个表单都有唯一标识 `form_id`,即当前视图的 `view_id`。`form_id` 的获取方式和 `view_id` 的获取方式相同。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 记录 record
|
||||||
|
|
||||||
|
数据表中的每一行数据都是一条记录(record)。每条记录都有唯一标识 `record_id`,`record_id` 在一个多维表格中唯一,在全局不一定唯一。`record_id` 需要通过[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口获取。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 字段 field
|
||||||
|
|
||||||
|
即多维表格的“列”,多维表格提供丰富的字段类型。每个字段都有唯一标识 `field_id`,`field_id` 在一个多维表格内唯一,在全局不一定唯一。`field_id` 需要通过[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取。了解更多字段说明,参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 仪表盘 block
|
||||||
|
|
||||||
|
仪表盘与数据看板类似,可以从不同的维度统计对多维表格中的数据进行统计。了解更多,参考飞书帮助中心文档[使用多维表格仪表盘](https://www.feishu.cn/hc/zh-CN/articles/161059314076-%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BB%B4%E8%A1%A8%E6%A0%BC%E4%BB%AA%E8%A1%A8%E7%9B%98)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
仪表盘的唯一标识为 `block_id`,以 `blk` 开头,你可通过多维表格 URL 获取 `block_id`,下图高亮部分即为当前仪表盘的唯一标识,也可通过[列出仪表盘](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-dashboard/list)接口获取。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 高级权限
|
||||||
|
|
||||||
|
高级权限允许用户针对单一数据表设置哪些用户可以查看、编辑指定的行,或是设置针对某用户可以编辑的列。高级权限接口分为 **自定义角色** 和 **协作者** 两部分,多维表格的 **所有者** 或者 **有可管理权限** 的用户可通过接口设置高级权限,管理高级权限的协作者。了解更多,参考[高级权限概述](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/advanced-permission-guide)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **自定义角色 role**
|
||||||
|
|
||||||
|
在高级权限中添加角色并设置权限,该角色即为自定义角色。每个自定义角色都有唯一标识 `role_id`。`role_id` 需要通过[列出自定义角色](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/list)接口获取。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **协作者 member**
|
||||||
|
|
||||||
|
高级权限设置中,一个自定义角色(role)中的成员,即为协作者(member)。每个协作者都有唯一标识 `member_id`。`member_id` 需要通过[列出协作者](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role-member/list)接口获取。
|
||||||
|
|
||||||
|
### 自动化流程 workflows
|
||||||
|
|
||||||
|
自动化流程是用户给多维表格设定的自动运行规则。设定“触发条件”和“执行操作”以后,多维表格会根据数据变更,自动执行下一步操作。你可通过[列出自动化流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-workflow/list)获取自动化流程的 ID,即 `workflow_id`。
|
||||||
|
|
||||||
|
## 方法列表
|
||||||
|
以下为多维表格的方法列表。其中,“商店”代表[商店应用](https://open.feishu.cn/document/home/app-types-introduction/overview);“自建”代表[企业自建应用](https://open.feishu.cn/document/home/app-types-introduction/overview),了解更多应用相关信息,参考[应用类型简介](https://open.feishu.cn/document/home/app-types-introduction/overview)。了解调用服务端 API 的流程,参考[流程概述](https://open.feishu.cn/document/uMzNwEjLzcDMx4yM3ATM/ugzNwEjL4cDMx4CO3ATM)。
|
||||||
|
|
||||||
|
### 多维表格 app
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[创建多维表格](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app/create)<br>`POST` /open-apis/bitable/v1/apps | 创建多维表格(base:app:create)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[复制多维表格](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app/copy)<br>`POST` /open-apis/bitable/v1/apps/:app_token/copy | 复制多维表格(base:app:copy)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[获取多维表格元数据](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app/get)<br>`GET` /open-apis/bitable/v1/apps/:app_token | 查看、评论和导出多维表格(bitable:app:readonly)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[更新多维表格元数据](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app/update)<br>`PUT` /open-apis/bitable/v1/apps/:app_token | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 数据表 table
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/tables | 查看、评论和导出多维表格(bitable:app:readonly)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增多个数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/batch_create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/batch_create | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/tables/:table_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除多个数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/batch_delete)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/batch_delete | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 视图 view
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/views | 查看、评论和导出多维表格(bitable:app:readonly)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/views | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[更新视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/patch)<br>`PATCH` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/views/:view_id | 查看、评论和导出多维表格(bitable:app:readonly)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[检索视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/get)<br>`GET` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/views/:view_id | 检索视图(base:view:read)<br>查看、评论、编辑和管理多维表格(bitable:app)<br>查看、评论和导出多维表格(bitable:app:readonly) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/views/:view_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 记录 record
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[列出记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records | 查看、评论和导出多维表格(bitable:app:readonly)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[检索记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/get)<br>`GET` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/:record_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增多条记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/batch_create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/batch_create | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[更新记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/update)<br>`PUT` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/:record_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[更新多条记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/batch_update)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/batch_update | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/:record_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除多条记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/batch_delete)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/batch_delete | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 字段 field
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/fields | 查看、评论和导出多维表格(bitable:app:readonly)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/fields | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[更新字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/update)<br>`PUT` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/fields/:field_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/tables/:table_id/fields/:field_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 仪表盘 dashboard
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[复制仪表盘](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-dashboard/copy)<br>`POST` /open-apis/bitable/v1/apps/:app_token/dashboards/:block_id/copy | 复制仪表盘(base:dashboard:copy)<br>查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[列出仪表盘](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-dashboard/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/dashboards | 获取仪表盘信息(base:dashboard:read)<br>查看、评论、编辑和管理多维表格(bitable:app)<br>查看、评论和导出多维表格(bitable:app:readonly) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 自定义角色 role
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[列出自定义权限](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/roles | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增自定义权限](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/roles/:role_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[更新自定义权限](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/update)<br>`PUT` /open-apis/bitable/v1/apps/:app_token/roles/:role_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除自定义权限](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/roles/:role_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
|
||||||
|
### 协作者 member
|
||||||
|
|
||||||
|
高级权限下的协作者。
|
||||||
|
|
||||||
|
**[方法 (API)](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)** | 权限要求(满足任一) | **[访问凭证](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM)(选择其一)** | 商店 | 自建
|
||||||
|
---|---|---|---|---
|
||||||
|
[列出协作者](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role-member/list)<br>`GET` /open-apis/bitable/v1/apps/:app_token/roles/:role_id/members | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[新增协作者](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role-member/create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/roles/:role_id/members | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[删除协作者](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role-member/delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/roles/:role_id/members/:member_id | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[批量新增协作者](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role-member/batch_create)<br>`POST` /open-apis/bitable/v1/apps/:app_token/roles/:role_id/members/batch_create | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
|
[批量删除协作者](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role-member/batch_delete)<br>`DELETE` /open-apis/bitable/v1/apps/:app_token/roles/:role_id/members/batch_delete | 查看、评论、编辑和管理多维表格(bitable:app) | `tenant_access_token`<br>`user_access_token` | **✓** | **✓**
|
||||||
296
批量获取记录.md
Normal file
296
批量获取记录.md
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
# 批量获取记录
|
||||||
|
|
||||||
|
通过多个记录 ID 查询记录信息。该接口最多支持查询 100 条记录。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
若多维表格开启了高级权限,你需确保调用身份拥有多维表格的可管理权限,否则可能出现调用成功但返回数据为空的情况。了解具体步骤,参考[如何为应用或用户开通文档权限](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#16c6475a)。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token/tables/:table_id/records/batch_get
|
||||||
|
HTTP Method | POST
|
||||||
|
接口频率限制 | [20 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 检索特定记录(base:record:read)<br>查看、评论、编辑和管理多维表格(bitable:app)<br>查看、评论和导出多维表格(bitable:app:readonly)
|
||||||
|
字段权限要求 | **注意事项**:该接口返回体中存在下列敏感字段,仅当开启对应的权限后才会返回;如果无需获取这些字段,则不建议申请<br>获取用户基本信息(contact:user.base:readonly)<br>以应用身份访问通讯录(contact:contact:access_as_app)<br>读取通讯录(contact:contact:readonly)<br>以应用身份读取通讯录(contact:contact:readonly_as_app)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 多维表格 App 的唯一标识。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。<br>**示例值**:"NQRxbRkBMa6OnZsjtERcxhabcef"<br>**数据校验规则**:<br>- 长度范围:`0` ~ `100` 字符
|
||||||
|
table_id | string | 多维表格数据表的唯一标识。获取方式:<br>- 你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的 `table_id`<br>- 也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`<br><br>**示例值**:"tbl0xe5g8PPabcef"<br>**数据校验规则**:<br>- 长度范围:`0` ~ `50` 字符
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
record_ids | string\[\] | 是 | 记录 ID 列表。调用[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)获取。<br>**示例值**:["recyO3299N"]<br>**数据校验规则**:<br>- 长度范围:`1` ~ `100`
|
||||||
|
user_id_type | string | 否 | 此次调用中使用的用户 id 的类型<br>**示例值**:"open_id"<br>**可选值有**:<br>- user_id:以user_id来识别用户<br>- union_id:以union_id来识别用户<br>- open_id:以open_id来识别用户
|
||||||
|
with_shared_url | boolean | 否 | 是否返回记录的分享链接。可选值:<br>- true:返回分享链接<br>- false:不返回分享链接<br>**默认值**:false<br>**示例值**:true
|
||||||
|
automatic_fields | boolean | 否 | 是否返回自动计算的字段。可选值:<br>- true:返回自动计算的字段<br>- false:不返回自动计算的字段<br>**默认值**:false<br>**示例值**:true
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"record_ids": [
|
||||||
|
"recyOaMB2F",
|
||||||
|
"rec111111",
|
||||||
|
"recyOaMB2F"
|
||||||
|
],
|
||||||
|
"user_id_type": "open_id",
|
||||||
|
"with_shared_url": true,
|
||||||
|
"automatic_fields": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
records | app.table.record\[\] | 记录列表
|
||||||
|
fields | map<string, union> | 记录字段
|
||||||
|
record_id | string | 记录 ID
|
||||||
|
created_by | person | 创建人信息
|
||||||
|
id | string | 创建人 ID。ID 类型与请求体中的 `user_id_type` 指定的类型一致。
|
||||||
|
name | string | 创建人中文姓名
|
||||||
|
en_name | string | 创建人英文姓名
|
||||||
|
email | string | 创建人邮箱
|
||||||
|
avatar_url | string | 创建人头像链接<br>**字段权限要求(满足任一)**:<br>获取用户基本信息(contact:user.base:readonly)<br>以应用身份访问通讯录(contact:contact:access_as_app)<br>读取通讯录(contact:contact:readonly)<br>以应用身份读取通讯录(contact:contact:readonly_as_app)
|
||||||
|
created_time | int | 创建时间。毫秒级时间戳。
|
||||||
|
last_modified_by | person | 修改人信息
|
||||||
|
id | string | 修改人 ID。ID 类型与请求体中的 `user_id_type` 指定的类型一致。
|
||||||
|
name | string | 修改人中文姓名
|
||||||
|
en_name | string | 修改人英文姓名
|
||||||
|
email | string | 修改人邮箱
|
||||||
|
avatar_url | string | 修改人头像链接<br>**字段权限要求(满足任一)**:<br>获取用户基本信息(contact:user.base:readonly)<br>以应用身份访问通讯录(contact:contact:access_as_app)<br>读取通讯录(contact:contact:readonly)<br>以应用身份读取通讯录(contact:contact:readonly_as_app)
|
||||||
|
last_modified_time | int | 最近更新时间。毫秒级时间戳。
|
||||||
|
shared_url | string | 记录分享链接
|
||||||
|
record_url | string | 记录链接(检索记录接口将返回该字段,本接口不返回)
|
||||||
|
forbidden_record_ids | string\[\] | 禁止访问的记录列表(针对开启了高级权限的多维表格)
|
||||||
|
absent_record_ids | string\[\] | 不存在的记录列表
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"forbidden_record_ids": [
|
||||||
|
"recyOaMB2F"
|
||||||
|
],
|
||||||
|
"absent_record_ids": [
|
||||||
|
"rec111111"
|
||||||
|
],
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"created_by": {
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu.cn/static-resource/v1/06d568cb-f464-4c2e-bd03-76512c545c5j~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"email": "",
|
||||||
|
"en_name": "Min Zhang",
|
||||||
|
"id": "ou_92945f86a98bba075174776959c90eda",
|
||||||
|
"name": "张敏"
|
||||||
|
},
|
||||||
|
"created_time": 1691049973000,
|
||||||
|
"fields": {
|
||||||
|
"人员": [
|
||||||
|
{
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu.cn/static-resource/v1/b2-7619-4b8a-b27b-c72d90b06a2j~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"email": "minzhang.leben@bytedance.com",
|
||||||
|
"en_name": "Min Zhang",
|
||||||
|
"id": "ou_2910013f1e6456f16a0ce75ede950a0a",
|
||||||
|
"name": "张敏"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu.cn/static-resource/v1/v2_q86-fcb6-4f18-85c7-87ca8881e50j~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"email": "minzhang.00@bytedance.com",
|
||||||
|
"en_name": "Min Zhang",
|
||||||
|
"id": "ou_e04138c9633dd0d2ea166d79f548ab5d",
|
||||||
|
"name": "张敏"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"修改人": [
|
||||||
|
{
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu.cn/static-resource/v1/06d568cb-f464-4c2e-bd03-76512c545c5j~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"email": "",
|
||||||
|
"en_name": "Min Zhang",
|
||||||
|
"id": "ou_92945f86a98bba075174776959c90eda",
|
||||||
|
"name": "张敏"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"创建人": [
|
||||||
|
{
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu.cn/static-resource/v1/06d568cb-f464-4c2e-bd03-76512c545c5j~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"email": "",
|
||||||
|
"en_name": "Min Zhang",
|
||||||
|
"id": "ou_92945f86a98bba075174776959c90eda",
|
||||||
|
"name": "张敏"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"创建时间": 1691049973000,
|
||||||
|
"单向关联": {
|
||||||
|
"link_record_ids": [
|
||||||
|
"recnVYsuqV"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"单选": "选项1",
|
||||||
|
"双向关联": {
|
||||||
|
"link_record_ids": [
|
||||||
|
"recqLvMaXT",
|
||||||
|
"recrdld32q"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"地理位置": {
|
||||||
|
"address": "东长安街",
|
||||||
|
"adname": "东城区",
|
||||||
|
"cityname": "北京市",
|
||||||
|
"full_address": "天安门广场,北京市东城区东长安街",
|
||||||
|
"location": "116.397755,39.903179",
|
||||||
|
"name": "天安门广场",
|
||||||
|
"pname": "北京市"
|
||||||
|
},
|
||||||
|
"复选框": true,
|
||||||
|
"多行文本": [
|
||||||
|
{
|
||||||
|
"text": "多行文本内容1",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mentionNotify": false,
|
||||||
|
"mentionType": "User",
|
||||||
|
"name": "张敏",
|
||||||
|
"text": "@张敏",
|
||||||
|
"token": "ou_2910013f1e6456f16a0ce75ede950a0a",
|
||||||
|
"type": "mention"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"多选": [
|
||||||
|
"选项1",
|
||||||
|
"选项2"
|
||||||
|
],
|
||||||
|
"数字": 2323.2323,
|
||||||
|
"日期": 1690992000000,
|
||||||
|
"最后更新时间": 1702455191000,
|
||||||
|
"条码": [
|
||||||
|
{
|
||||||
|
"text": "123",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"电话号码": "131xxxx6666",
|
||||||
|
"自动编号": "17",
|
||||||
|
"群组": [
|
||||||
|
{
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu-boe.cn/static-resource/v1/v2_c8d2cd50-ba29-476f-b7f1-5b5917cb18ej~?image_size=72x72&cut_type=&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"id": "oc_cd07f55f14d6f4a4f1b51504e7e97f48",
|
||||||
|
"name": "武侠聊天组"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"评分": 3,
|
||||||
|
"货币": 1,
|
||||||
|
"超链接": {
|
||||||
|
"link": "https://bitable.feishu.cn",
|
||||||
|
"text": "飞书多维表格官网"
|
||||||
|
},
|
||||||
|
"进度": 0.66,
|
||||||
|
"附件": [
|
||||||
|
{
|
||||||
|
"file_token": "Vl3FbVkvnowlgpxpqsAbBrtFcrd",
|
||||||
|
"name": "飞书.jpeg",
|
||||||
|
"size": 32975,
|
||||||
|
"tmp_url": "https://open.feishu.cn/open-apis/drive/v1/medias/batch_get_tmp_download_url?file_tokens=Vl3FbVk11owlgpxpqsAbBrtFcrd&extra=%7B%22bitablePerm%22%3A%7B%22tableId%22%3A%22tblBJyX6jZteblYv%22%2C%22rev%22%3A90%7D%7D",
|
||||||
|
"type": "image/jpeg",
|
||||||
|
"url": "https://open.feishu.cn/open-apis/drive/v1/medias/Vl3FbVk11owlgpxpqsAbBrtFcrd/download?extra=%7B%22bitablePerm%22%3A%7B%22tableId%22%3A%22tblBJyX6jZteblYv%22%2C%22rev%22%3A90%7D%7D"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"last_modified_by": {
|
||||||
|
"avatar_url": "https://internal-api-lark-file.feishu.cn/static-resource/v1/06d568cb-f464-4c2e-bd03-76512c545c5j~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
|
||||||
|
"email": "",
|
||||||
|
"en_name": "Min Zhang",
|
||||||
|
"id": "ou_92945f86a98bba075174776959c90eda",
|
||||||
|
"name": "张敏"
|
||||||
|
},
|
||||||
|
"last_modified_time": 1702455191000,
|
||||||
|
"record_id": "recyOaMB2F",
|
||||||
|
"shared_url": "https://example.feishu.cn/record/KBcNrNtpWePAlscCvdmb6ZcSc5b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
400 | 1254000 | WrongRequestJson | 请求体错误。请检查请求参数
|
||||||
|
400 | 1254001 | WrongRequestBody | 请求体错误。请检查请求参数
|
||||||
|
500 | 1254002 | Fail | 导致报 1254002 错误码的场景较多,请参考以下建议排查:<br>- 如果单次操作的内容变更较大,请尝试在单次操作中减少数据量<br>- 如果你并发调用了接口,请尝试控制请求间隔,稍后重试<br>- 如果在知识库(wiki)中创建多维表格,请检查你是否使用了知识库[创建知识空间节点](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space-node/create)接口创建多维表格。在此场景下不能使用[创建多维表格](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app/create)接口<br>- 请检查接口参数是否有误。例如,在分页查询多维表格时,传递了无效的 page_token,或传递了错误的数据表的 table_id<br>- 如果该报错偶尔发生,可能是服务器超时或不稳定,请重试解决
|
||||||
|
400 | 1254003 | WrongBaseToken | app_token 错误。app_token 是多维表格 App 的唯一标识。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。
|
||||||
|
400 | 1254004 | WrongTableId | table_id 错误。table_id 是多维表格数据表的唯一标识。获取方式:<br>- 你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的 `table_id`<br>- 也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`<br>
|
||||||
|
400 | 1254005 | WrongViewId | view_id 错误。view_id 是多维表格中视图的唯一标识。获取方式:<br>- 在多维表格的 URL 地址栏中,`view_id` 是下图中高亮部分:<br><br>- 通过[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)接口获取。暂时无法获取到嵌入到云文档中的多维表格的 `view_id`。<br>**注意**:<br>当 `filter` 参数 或 `sort` 参数不为空时,请求视为对数据表中的全部数据做条件过滤,指定的 `view_id` 会被忽略。
|
||||||
|
400 | 1254006 | WrongRecordId | record_id 错误。record_id 是数据表中一条记录的唯一标识。通过[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口获取
|
||||||
|
400 | 1254007 | EmptyValue | 空值
|
||||||
|
400 | 1254008 | EmptyView | 空视图
|
||||||
|
400 | 1254009 | WrongFieldId | field_id 错误。field_id 是数据表中一个字段的唯一标识。通过[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取
|
||||||
|
400 | 1254010 | ReqConvError | 请求错误
|
||||||
|
400 | 1254011 | Page size must greater than 0. | page_size参数非法
|
||||||
|
400 | 1254016 | InvalidSort | Sort参数错误
|
||||||
|
400 | 1254018 | InvalidFilter | filter 参数错误。请参考[记录过滤参数填写指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide)了解如何填写 filter 参数。
|
||||||
|
400 | 1254024 | InvalidFieldNames | field_names 参数错误。请检查接口中字段名称和多维表格中的字段名称是否完全匹配。如果难以排查,建议你调用[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取字段名称,因为根据表格页面的 UI 名称可能会忽略空格、换行或特殊符号等差异
|
||||||
|
400 | 1254030 | InvalidPageToken | 分页游标错误
|
||||||
|
400 | 1254036 | Bitable is copying, please try again later. | 复制多维表格为异步操作,该错误码表示当前多维表格仍在复制中,在复制期间无法操作当前多维表格。需要等待复制完成后再操作
|
||||||
|
404 | 1254040 | BaseTokenNotFound | app_token 不存在。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。
|
||||||
|
404 | 1254041 | TableIdNotFound | table_id 不存在。获取方式:<br>- 你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的 `table_id`<br>- 也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`<br>
|
||||||
|
404 | 1254042 | ViewIdNotFound | view_id 不存在。获取方式:<br>- 在多维表格的 URL 地址栏中,`view_id` 是下图中高亮部分:<br><br>- 通过[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)接口获取。暂时无法获取到嵌入到云文档中的多维表格的 `view_id`。<br>**注意**:<br>当 `filter` 参数 或 `sort` 参数不为空时,请求视为对数据表中的全部数据做条件过滤,指定的 `view_id` 会被忽略。
|
||||||
|
404 | 1254043 | RecordIdNotFound | record_id 不存在。record_id 是数据表中一条记录的唯一标识。请通过[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口获取。
|
||||||
|
404 | 1254044 | FieldIdNotFound | field_id 不存在。field_id 是数据表中一个字段的唯一标识。通过[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取。
|
||||||
|
404 | 1254045 | FieldNameNotFound | 字段名称不存在。请检查:<br>- 接口中字段名称和多维表格中的字段名称是否完全匹配。如果难以排查,建议你调用[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取字段名称,因为根据表格页面的 UI 名称可能会忽略空格、换行或特殊符号等差异。<br>- 表格是否开启了高级权限但调用身份缺少对应字段的权限。你需要为调用身份授予高级权限:<br>- 对用户授予高级权限,你需要在多维表格页面右上方 **分享** 入口为当前用户添加可管理权限。<br>- 对应用授予高级权限,你需通过多维表格页面右上方 **「...」** -> **「...更多」** ->**「添加文档应用」** 入口为应用添加可管理权限。<br><br><br>**注意**:<br>在 **添加文档应用** 前,你需确保目标应用至少开通了一个多维表格的 [API 权限](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/scope-list)。否则你将无法在文档应用窗口搜索到目标应用。 <br>- 你也可以在 **多维表格高级权限设置** 中添加用户或一个包含应用的群组, 给予这个群自定义的读写等权限。
|
||||||
|
500 | 1254060 | TextFieldConvFail | 多行文本字段错误
|
||||||
|
500 | 1254061 | NumberFieldConvFail | 数字字段错误
|
||||||
|
500 | 1254062 | SingleSelectFieldConvFail | 单选字段错误
|
||||||
|
500 | 1254063 | MultiSelectFieldConvFail | 多选字段错误
|
||||||
|
500 | 1254064 | DatetimeFieldConvFail | 日期字段错误
|
||||||
|
500 | 1254065 | CheckboxFieldConvFail | 复选框字段错误
|
||||||
|
500 | 1254066 | UserFieldConvFail | 人员字段有误。原因可能是:<br>- `user_id_type` 参数指定的 ID 类型与传入的 ID 类型不匹配<br>- 传入了不识别的类型或结构,目前只支持填写 `id` 参数,且需要传入数组<br>- 跨应用传入了 `open_id`。如果跨应用传入 ID,建议使用 `user_id`。不同应用获取的 `open_id` 不能交叉使用<br>- 若想对人员字段传空,可传 null
|
||||||
|
500 | 1254067 | LinkFieldConvFail | 关联字段错误
|
||||||
|
500 | 1254068 | URLFieldConvFail | 超链接字段错误
|
||||||
|
500 | 1254069 | AttachFieldConvFail | 附件字段错误
|
||||||
|
400 | 1254072 | Failed to convert phone field, please make sure it is correct. | 电话字段错误
|
||||||
|
400 | 1254100 | TableExceedLimit | 数据表或仪表盘数量超限。每个多维表格中,数据表加仪表盘的数量最多为 100 个
|
||||||
|
400 | 1254101 | ViewExceedLimit | 视图数量超限, 限制 200 个
|
||||||
|
400 | 1254102 | FileExceedLimit | 文件数量超限
|
||||||
|
400 | 1254103 | RecordExceedLimit | 记录数量超限, 限制 20,000 条
|
||||||
|
400 | 1254104 | RecordAddOnceExceedLimit | 单次添加记录数量超限, 单次调用最多更新 1,000 条记录
|
||||||
|
400 | 1254107 | FilterLengthExceedLimit | Filter长度超限, 限制 2,000 个字符
|
||||||
|
400 | 1254108 | SortLengthExceedLimit | Sort 长度超限, 限制 1,000 个字符
|
||||||
|
400 | 1254109 | FormulaTableSizeExceedLimit | 公式表大小超限
|
||||||
|
400 | 1254130 | TooLargeCell | 格子内容过大
|
||||||
|
400 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
400 | 1254291 | LockNotObtainedError | 在同一个数据表中,并发调用了读写接口或请求过快,出现冲突。请参考以下建议解决:<br>- 确保没有并发调用多维表格读写相关接口<br>- 若操作量较大,建议在接口与接口之间增加 0.5 或 1 秒的延迟,也可在报错中增加重试逻辑,确保业务的稳定性<br>- 对于写接口,可以将接口中的查询参数 `ignore_consistency_check` 设置为 true,表示在读写操作时,暂时忽略一致性检查,以提高性能
|
||||||
|
400 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
400 | 1254302 | RolePermNotAllow | 无访问权限,常由表格开启了高级权限造成。请确保当前调用身份具有高级权限或多维表格的管理权限:<br>- 对于应用身份,你需要通过云文档网页页面右上方 「**...**」->「**...更多**」-> 「**添加文档应用**」入口添加并授予应用可管理权限,或在高级权限设置中添加一个包含应用的群组,给予这个群读写权限<br>- 对于用户身份,你需要通过云文档网页的「**分享**」入口授予用户管理权限<br>了解更多,参考[如何为应用或用户开通云文档权限](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#16c6475a)。
|
||||||
|
400 | 1254303 | AttachPermNotAllow | 附件无权限
|
||||||
|
500 | 1255001 | InternalError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
500 | 1255002 | RpcError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
500 | 1255003 | MarshalError | 序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
500 | 1255004 | UmMarshalError | 反序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
500 | 1255005 | ConvError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
504 | 1255040 | Request timed out, please try again later | 请求超时
|
||||||
|
500 | 1254607 | Data not ready, please try again later | 该报错一般是由于前置操作未执行完成,或本次操作数据太大,服务器计算超时导致。遇到该错误码时,建议等待一段时间后重试。通常有以下几种原因:<br>- **编辑操作频繁**:开发者对多维表格的编辑操作非常频繁。可能会导致由于等待前置操作处理完成耗时过长而超时的情况。多维表格底层对数据表的处理基于版本维度的串行方式,不支持并发。因此,并发请求时容易出现此类错误,不建议开发者对单个数据表进行并发请求。<br>- **批量操作负载重**:开发者在多维表格中进行批量新增、删除等操作时,如果数据表的数据量非常大,可能会导致单次请求耗时过长,最终导致请求超时。建议开发者适当降低批量请求的 page_size 以减少请求耗时。<br>- **资源分配与计算开销**:资源分配是基于单文档维度的,如果读接口涉及公式计算、排序等计算逻辑,会占用较多资源。例如,并发读取一个文档下的多个数据表也可能导致该文档阻塞。
|
||||||
159
批量获取评论.md
Normal file
159
批量获取评论.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# 批量获取评论
|
||||||
|
|
||||||
|
该接口用于根据评论 ID 列表批量获取云文档评论信息,包括评论和回复 ID、回复的内容、评论人和回复人的用户 ID 等。支持返回全局评论以及局部评论(可通过 is_whole 字段区分)。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/drive/v1/files/:file_token/comments/batch_query
|
||||||
|
HTTP Method | POST
|
||||||
|
接口频率限制 | [100 次/分钟](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 获取云文档中的评论(docs:document.comment:read)<br>查看、评论、编辑和管理云空间中所有文件(drive:drive)<br>查看、评论和下载云空间中所有文件(drive:drive:readonly)
|
||||||
|
字段权限要求 | **注意事项**:该接口返回体中存在下列敏感字段,仅当开启对应的权限后才会返回;如果无需获取这些字段,则不建议申请<br>获取用户 user ID(contact:user.employee_id:readonly)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
file_token | string | 文档 Token<br>**示例值**:"doxbcdl03Vsxhm7Qmnj110abcef"
|
||||||
|
|
||||||
|
### 查询参数
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
file_type | string | 是 | 云文档类型<br>**示例值**:docx<br>**可选值有**:<br>- doc:旧版文档类型,已不推荐使用<br>- docx:新版文档类型<br>- sheet:电子表格类型<br>- file:文件类型<br>- slides:幻灯片
|
||||||
|
user_id_type | string | 否 | 用户 ID 类型<br>**示例值**:open_id<br>**可选值有**:<br>- open_id:标识一个用户在某个应用中的身份。同一个用户在不同应用中的 Open ID 不同。[了解更多:如何获取 Open ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid)<br>- union_id:标识一个用户在某个应用开发商下的身份。同一用户在同一开发商下的应用中的 Union ID 是相同的,在不同开发商下的应用中的 Union ID 是不同的。通过 Union ID,应用开发商可以把同个用户在多个应用中的身份关联起来。[了解更多:如何获取 Union ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-union-id)<br>- user_id:标识一个用户在某个租户内的身份。同一个用户在租户 A 和租户 B 内的 User ID 是不同的。在同一个租户内,一个用户的 User ID 在所有应用(包括商店应用)中都保持一致。User ID 主要用于在不同的应用间打通用户数据。[了解更多:如何获取 User ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-user-id)<br>**默认值**:`open_id`<br>**当值为 `user_id`,字段权限要求**:<br>获取用户 user ID(contact:user.employee_id:readonly)
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
comment_ids | string\[\] | 是 | 需要获取数据的评论 ID ,可通过调用获取云文档所有评论接口获取 comment_id<br>**示例值**:["1654857036541812356"]
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"comment_ids": [
|
||||||
|
"1654857036541812356"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
items | file.comment\[\] | 评论的相关信息、回复的信息、回复分页的信息
|
||||||
|
comment_id | string | 评论 ID
|
||||||
|
user_id | string | 用户 ID
|
||||||
|
create_time | int | 创建时间
|
||||||
|
update_time | int | 更新时间
|
||||||
|
is_solved | boolean | 是否已解决
|
||||||
|
solved_time | int | 解决评论时间
|
||||||
|
solver_user_id | string | 解决评论者的用户 ID
|
||||||
|
has_more | boolean | 是否有更多回复
|
||||||
|
page_token | string | 回复分页标记
|
||||||
|
is_whole | boolean | 是否是全文评论
|
||||||
|
quote | string | 局部评论的引用字段
|
||||||
|
reply_list | reply_list | 评论里的回复列表
|
||||||
|
replies | file.comment.reply\[\] | 回复列表
|
||||||
|
content | reply_content | 回复内容
|
||||||
|
elements | reply_element\[\] | 回复的内容
|
||||||
|
type | string | 回复的内容元素<br>**可选值有**:<br>- text_run:普通文本<br>- docs_link:at 云文档链接<br>- person:at 联系人
|
||||||
|
text_run | text_run | 文本内容
|
||||||
|
text | string | 回复 普通文本
|
||||||
|
docs_link | docs_link | 添加云文档链接
|
||||||
|
url | string | 回复 at 云文档
|
||||||
|
person | person | 添加用户的 user_id
|
||||||
|
user_id | string | 添加用户的 user_id 以@用户
|
||||||
|
reply_id | string | 回复 ID
|
||||||
|
user_id | string | 用户 ID
|
||||||
|
create_time | int | 创建时间
|
||||||
|
update_time | int | 更新时间
|
||||||
|
extra | reply_extra | 回复的其他内容,图片 Token 等
|
||||||
|
image_list | string\[\] | 评论中的图片 Token list
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"comment_id": "6916106822734512356",
|
||||||
|
"user_id": "ou_cc19b2bfb93f8a44db4b4d6eababcef",
|
||||||
|
"create_time": 1610281603,
|
||||||
|
"update_time": 1610281603,
|
||||||
|
"is_solved": false,
|
||||||
|
"solved_time": 1610281603,
|
||||||
|
"solver_user_id": "null",
|
||||||
|
"has_more": false,
|
||||||
|
"page_token": "6916106822734512356",
|
||||||
|
"is_whole": true,
|
||||||
|
"quote": "划词评论引用内容",
|
||||||
|
"reply_list": {
|
||||||
|
"replies": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "text_run",
|
||||||
|
"text_run": {
|
||||||
|
"text": "comment text"
|
||||||
|
},
|
||||||
|
"docs_link": {
|
||||||
|
"url": "https://example.feishu.cn/docs/doccnHh7U87HOFpii5u5Gabcef"
|
||||||
|
},
|
||||||
|
"person": {
|
||||||
|
"user_id": "ou_cc19b2bfb93f8a44db4b4d6eababcef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reply_id": "6916106822734512356",
|
||||||
|
"user_id": "ou_cc19b2bfb93f8a44db4b4d6eab2abcef",
|
||||||
|
"create_time": 1610281603,
|
||||||
|
"update_time": 1610281603,
|
||||||
|
"extra": {
|
||||||
|
"image_list": [
|
||||||
|
"xfsfseewewabcef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
400 | 1069301 | fail | 重试,若稳定失败请联系相关业务方oncall人员
|
||||||
|
400 | 1069302 | param error | 检查参数有效性
|
||||||
|
403 | 1069303 | forbidden | 检查是否有待评论云文档的评论权限
|
||||||
|
400 | 1069304 | docs had been deleted | 检查待评论云文档是否已被删除
|
||||||
|
400 | 1069305 | docs not exist | 检查待评论云文档是否能正常访问
|
||||||
|
400 | 1069306 | content review not pass | 排查评论内容是否存在不合法内容
|
||||||
|
404 | 1069307 | not exist | 检查待评论云文档是否能正常访问、检查评论内容at人或云文档是否存在
|
||||||
|
400 | 1069308 | exceeded limit | 评论数据超过上限限制,详情请咨询客服
|
||||||
|
400 | 1069399 | internal error | 重试,若稳定失败请联系相关业务方oncall人员
|
||||||
|
400 | 1064230 | locked for data migration | 数据迁移中,暂时无法上传。
|
||||||
80
数据结构概述.md
Normal file
80
数据结构概述.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# 数据结构概述
|
||||||
|
|
||||||
|
本文档介绍多维表格数据表中记录、字段和视图等的数据结构。多维表格中的数据表由记录(record)和字段(field)组成, 同时可以拥有多个视图(view)。
|
||||||
|
|
||||||
|
## 记录
|
||||||
|
记录由 record 和 fields 两个结构组成。
|
||||||
|
|
||||||
|
### record 结构
|
||||||
|
|
||||||
|
record 是一个 object 结构类型。
|
||||||
|
| 参数 | 数据类型 | 描述 |
|
||||||
|
| --------- | ------- | --------- |
|
||||||
|
|`record_id`| string | 记录的 ID |
|
||||||
|
|`fields`| map | 记录的字段 |
|
||||||
|
|
||||||
|
### fields 结构
|
||||||
|
|
||||||
|
fields 字段为 map 型,由字段名称和其具体内容的键值对组成。了解 fields 详细结构和参数描述,参考[多维表格记录数据结构](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/bitable-record-data-structure-overview)。
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"任务情况总结": [
|
||||||
|
{
|
||||||
|
"text": "网站更新任务由黄泡泡负责,正在进行中",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
参数 | 数据类型 | 描述 | 示例值
|
||||||
|
---|---|---|---
|
||||||
|
key | string | 多维表格数据表中的字段名称。 | "任务情况总结"
|
||||||
|
value | union | 某个字段的具体内容,其结构可以是数字、字符串、布尔型、字符串列表或对象列表。详情参考下文。 | 该示例值为对象列表,更多示例,参考[多维表格记录数据结构](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/bitable-record-data-structure-overview)。<br>```json<br>[<br>{<br>"text": "网站更新任务正在进行中",<br>"type": "text"<br>}<br>]<br>```
|
||||||
|
|
||||||
|
## 字段
|
||||||
|
|
||||||
|
字段即多维表格数据表中的“列”,是一个`object`结构类型。字段的基本结构如下所示。了解字段详细结构和参数描述,参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"field_id": "fldYWaldeW", // 字段的 ID
|
||||||
|
"field_name": "文本", // 字段名称
|
||||||
|
"type": 1, // 字段的类型,详情参考下文
|
||||||
|
"description": "字段的描述", // 对字段的更多说明
|
||||||
|
"is_primary": true, // 该字段是否是初始的索引字段
|
||||||
|
"property": null, // 字段的属性,详情参考下文
|
||||||
|
"ui_type": "Text", // 字段在界面上的展示类型,例如进度字段是数字的一种展示形态
|
||||||
|
"is_hidden": false // 字段是否是隐藏字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
参数描述如下所示:
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
field_id | string | 字段 ID
|
||||||
|
field_name | string | 字段名称
|
||||||
|
type | int | 字段类型:相同类型用ui_type区分:<br>- 1:文本(默认值)、条码(需声明 <code>"ui_type": "Barcode"</code>)、邮箱(需声明<code>"ui_type": "Email"</code>)<br>- 2:数字(默认值)、进度(需声明 <code>"ui_type": "Progress"</code>)、货币(需声明 <code>"ui_type": "Currency"</code>)、评分(需声明 <code>"ui_type": "Rating"</code>)<br>- 3:单选<br>- 4:多选<br>- 5:日期<br>- 7:复选框<br>- 11:人员<br>- 13:电话号码<br>- 15:超链接<br>- 17:附件<br>- 18:单向关联<br>- 19:查找引用<br>- 20:公式<br>- 21:双向关联<br>- 22:地理位置<br>- 23:群组<br>- 24:流程(不支持通过写接口新增或编辑,仅支持读接口) <br>- 1001:创建时间<br>- 1002:最后更新时间<br>- 1003:创建人<br>- 1004:修改人<br>- 1005:自动编号<br>- 3001:按钮(不支持通过写接口新增或编辑,仅支持读接口)
|
||||||
|
description | 字段的描述 | 对字段的更多说明。
|
||||||
|
is_primary | true/false | 该字段是否是初始的索引字段。
|
||||||
|
property | object | 字段属性,因字段类型而异。详情参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。
|
||||||
|
ui_type | string | 字段的 UI 类型:<br>- "Text":文本<br>- "Email":邮箱<br>- "Barcode":条码<br>- "Number":数字<br>- "Progress":进度<br>- "Currency":货币<br>- "Rating":评分<br>- "SingleSelect":单选<br>- "MultiSelect":多选<br>- "DateTime":日期<br>- "Checkbox":复选框<br>- "User":人员<br>- "GroupChat":群组<br>- "Phone":电话号码<br>- "Url":超链接<br>- "Attachment":附件<br>- "SingleLink":单向关联<br>- "Formula":公式<br>- "Lookup": 查找引用<br>- "DuplexLink":双向关联<br>- "Location":地理位置<br>- "CreatedTime":创建时间<br>- "ModifiedTime":最后更新时间<br>- "CreatedUser":创建人<br>- "ModifiedUser":修改人<br>- "AutoNumber":自动编号<br>- "Button":按钮
|
||||||
|
is_hidden | true/false | 字段是否是隐藏字段。
|
||||||
|
|
||||||
|
## 视图
|
||||||
|
|
||||||
|
视图是一个 object 结构类型。
|
||||||
|
|
||||||
|
参数 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
view_id | string | 视图 ID。`view_id` 在一个多维表格中唯一,在全局不一定唯一。获取方式:<br>- 你可通过多维表格 URL 获取 `view_id`,下图高亮部分即为当前视图的唯一标识。<br><br>- 你也可通过[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)接口获取 `view_id`。暂时无法获取到嵌入到文档中的多维表格的 `view_id`。
|
||||||
|
view_name | string | 视图名称
|
||||||
|
view_type | string | 视图类型,支持以下类型,默认为 grid 类型。<br>- grid:表格视图<br>- kanban:看板视图<br>- gallery:画册视图<br>- gantt:甘特视图<br>- form:表单视图
|
||||||
|
|
||||||
|
## 自定义数据结构
|
||||||
|
|
||||||
|
### delete_record
|
||||||
|
| 参数 | 数据类型 | 描述 |
|
||||||
|
| --------- | --------------- | ----------- |
|
||||||
|
|`deleted` | `boolean` | 是否删除成功 |
|
||||||
|
|`record_id` | `string` | 单条记录的 ID |
|
||||||
189
新增一个数据表.md
Normal file
189
新增一个数据表.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# 新增一个数据表
|
||||||
|
|
||||||
|
新增一个数据表,支持传入数据表名称、视图名称和字段。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
调用此接口前,请确保当前调用身份(tenant_access_token 或 user_access_token)已有多维表格的编辑等文档权限,否则接口将返回 HTTP 403 或 400 状态码。了解更多,参考[如何为应用或用户开通文档权限](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#16c6475a)。
|
||||||
|
|
||||||
|
## 使用限制
|
||||||
|
|
||||||
|
每个多维表格中,数据表与仪表盘的总数量上限为 100。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token/tables
|
||||||
|
HTTP Method | POST
|
||||||
|
接口频率限制 | [10 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 新增数据表(base:table:create)<br>查看、评论、编辑和管理多维表格(bitable:app)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 多维表格 App 的唯一标识。不同形态的多维表格,其 app_token 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 app_token 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 obj_type 的值为 bitable 时,obj_token 字段的值才是多维表格的 app_token。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。<br>**示例值**:"appbcbWCzen6D8dezhoCH2RpMAh"<br>**数据校验规则**:<br>- 最小长度:`1` 字符
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
table | req_table | 否 | 数据表
|
||||||
|
name | string | 否 | 数据表名称。该字段必填。<br>**注意**:<br>- 名称中的首尾空格将会被默认去除<br>- 数据表名称不可以包含 `/ \ ? * : [ ]` 等特殊字符<br>**示例值**:"一个新的数据表"<br>**数据校验规则**:<br>- 长度范围:`1` ~ `100` 字符
|
||||||
|
default_view_name | string | 否 | 默认表格视图的名称。<br>注意:<br>- 名称中的首尾空格将会被去除<br>- 名称中不允许包含 [ ] 两个字符<br>**示例值**:"表格视图"
|
||||||
|
fields | app.table.create_header\[\] | 否 | 数据表的初始字段。了解如何填写字段,参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。<br>**注意**:<br>- 如果传入了 `default_view_name` 字段,则必须传入 `fields` 字段<br>- 如果不传 `default_view_name` 字段,则 `fields` 字段为可选字段<br>- 若 `default_view_name` 字段和 `fields` 字段都不传,将会创建一个仅包含索引字段的空数据表。<br>- 数据表的第一个字段为索引字段。索引字段仅支持以下类型:<br>- 1:多行文本<br>- 2:数字<br>- 5:日期<br>- 13:电话号码<br>- 15:超链接<br>- 20:公式<br>- 22:地理位置 <br>**数据校验规则**:<br>- 长度范围:`1` ~ `300`
|
||||||
|
field_name | string | 是 | 字段名称<br>**示例值**:"问题描述"
|
||||||
|
type | int | 是 | 字段类型。不支持新增 19 查找引用字段类型。<br>**示例值**:1<br>**可选值有**:<br>- 1:文本<br>- 2:数字<br>- 3:单选<br>- 4:多选<br>- 5:日期<br>- 7:复选框<br>- 11:人员<br>- 13:电话号码<br>- 15:超链接<br>- 17:附件<br>- 18:单向关联<br>- 20:公式<br>- 21:双向关联<br>- 22:地理位置<br>- 23:群组<br>- 1001:创建时间<br>- 1002:最后更新时间<br>- 1003:创建人<br>- 1004:修改人<br>- 1005:自动编号
|
||||||
|
ui_type | string | 否 | 字段在界面上的展示类型,例如 Progress 进度字段是数字的一种展示形态<br>**示例值**:"Progress"<br>**可选值有**:<br>- Text:文本<br>- Barcode:条码<br>- Number:数字<br>- Progress:进度<br>- Currency:货币<br>- Rating:评分<br>- SingleSelect:单选<br>- MultiSelect:多选<br>- DateTime:日期<br>- Checkbox:复选框<br>- User:人员<br>- GroupChat:群组<br>- Phone:电话号码<br>- Url:超链接<br>- Attachment:附件<br>- SingleLink:单向关联<br>- Formula:公式<br>- DuplexLink:双向关联<br>- Location:地理位置<br>- CreatedTime:创建时间<br>- ModifiedTime:最后更新时间<br>- CreatedUser:创建人<br>- ModifiedUser:修改人<br>- AutoNumber:自动编号
|
||||||
|
property | app.table.field.property | 否 | 字段属性
|
||||||
|
options | app.table.field.property.option\[\] | 否 | 单选、多选字段的选项信息
|
||||||
|
name | string | 否 | 选项名<br>**示例值**:"红色"
|
||||||
|
id | string | 否 | 选项 ID,创建时不可指定 ID<br>**示例值**:"optKl35lnG"
|
||||||
|
color | int | 否 | 选项颜色,详情参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。<br>**示例值**:0<br>**数据校验规则**:<br>- 取值范围:`0` ~ `54`
|
||||||
|
formatter | string | 否 | 数字、公式字段的显示格式。详情参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。<br>**示例值**:"0"
|
||||||
|
date_formatter | string | 否 | 日期、创建时间、最后更新时间字段的显示格式。默认为 "yyyy/MM/dd",详情参考[字段编辑指南](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/guide)。<br>**示例值**:"2021/01/30"
|
||||||
|
auto_fill | boolean | 否 | 日期字段中新纪录自动填写创建时间。默认为 false<br>**示例值**:false
|
||||||
|
multiple | boolean | 否 | 人员字段中允许添加多个成员,单向关联、双向关联中允许添加多个记录<br>**示例值**:false
|
||||||
|
table_id | string | 否 | 单向关联、双向关联字段中关联的数据表的id<br>**示例值**:"tblsRc9GRRXKqhvW"
|
||||||
|
table_name | string | 否 | 单向关联、双向关联字段中关联的数据表的名字<br>**示例值**:"table2"
|
||||||
|
back_field_name | string | 否 | 双向关联字段中关联的数据表中对应的双向关联字段的名字<br>**示例值**:"table1-双向关联"
|
||||||
|
auto_serial | app.field.property.auto_serial | 否 | 自动编号类型
|
||||||
|
type | string | 是 | 自动编号类型<br>**示例值**:"auto_increment_number"<br>**可选值有**:<br>- custom:自定义编号<br>- auto_increment_number:自增数字
|
||||||
|
options | app.field.property.auto_serial.options\[\] | 否 | 自动编号规则列表
|
||||||
|
type | string | 是 | 自动编号的可选规则项类型<br>**示例值**:"created_time"<br>**可选值有**:<br>- system_number:自增数字位,value范围1-9<br>- fixed_text:固定字符,最大长度:20<br>- created_time:创建时间,支持格式 "yyyyMMdd"、"yyyyMM"、"yyyy"、"MMdd"、"MM"、"dd"
|
||||||
|
value | string | 是 | 与自动编号的可选规则项类型相对应的取值<br>**示例值**:"yyyyMMdd"
|
||||||
|
location | app.field.property.location | 否 | 地理位置输入方式
|
||||||
|
input_type | string | 是 | 地理位置输入限制<br>**示例值**:"not_limit"<br>**可选值有**:<br>- only_mobile:只允许移动端上传<br>- not_limit:无限制
|
||||||
|
formula_expression | string | 否 | 公式字段的表达式<br>**示例值**:"bitable::$table[tblNj92WQBAasdEf].$field[fldMV60rYs]*2"
|
||||||
|
allowed_edit_modes | allowed_edit_modes | 否 | 字段支持的编辑模式
|
||||||
|
manual | boolean | 否 | 是否允许手动录入<br>**示例值**:true
|
||||||
|
scan | boolean | 否 | 是否允许移动端录入<br>**示例值**:true
|
||||||
|
min | number(float) | 否 | 进度、评分等字段的数据范围最小值<br>**示例值**:0
|
||||||
|
max | number(float) | 否 | 进度、评分等字段的数据范围最大值<br>**示例值**:10
|
||||||
|
range_customize | boolean | 否 | 进度等字段是否支持自定义范围<br>**示例值**:true
|
||||||
|
currency_code | string | 否 | 货币币种<br>**示例值**:"CNY"
|
||||||
|
rating | rating | 否 | 评分字段的相关设置
|
||||||
|
symbol | string | 否 | 评分字段的符号展示<br>**示例值**:"star"
|
||||||
|
description | app.table.field.description | 否 | 字段的描述
|
||||||
|
disable_sync | boolean | 否 | 是否禁止同步,如果为true,表示禁止同步该描述内容到表单的问题描述<br>**示例值**:true<br>**默认值**:`true`
|
||||||
|
text | string | 否 | 字段描述内容,支持换行\n<br>**示例值**:"请按 name_id 格式填写\n例如:“Alice_20202020”"
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"table": {
|
||||||
|
"name": "数据表名称",
|
||||||
|
"default_view_name": "默认的表格视图",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"field_name": "索引字段",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field_name": "单选",
|
||||||
|
"type": 3,
|
||||||
|
"ui_type": "SingleSelect",
|
||||||
|
"property": {
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"name": "Enabled",
|
||||||
|
"color": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Disabled",
|
||||||
|
"color": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Draft",
|
||||||
|
"color": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
table_id | string | 多维表格数据表的 ID
|
||||||
|
default_view_id | string | 默认表格视图的 ID。该字段仅在请求参数中填写了`default_view_name` 或 `fields` 字段才会返回
|
||||||
|
field_id_list | string\[\] | 数据表初始字段的 ID 列表,该字段仅在请求参数中填写了 `fields` 才会返回
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"table_id": "tblDBTWm6Es84d8c",
|
||||||
|
"default_view_id": "vewUuKOz2R",
|
||||||
|
"field_id_list": [
|
||||||
|
"fldhr2hBEA"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
200 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
200 | 1254001 | WrongRequestBody | 请求体错误。请检查请求体中是否已传入所有必填参数
|
||||||
|
200 | 1254002 | Fail | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
200 | 1254004 | WrongTableId | table_id 错误
|
||||||
|
200 | 1254007 | EmptyValue | 空值
|
||||||
|
200 | 1254008 | EmptyView | 空视图
|
||||||
|
200 | 1254009 | WrongFieldId | 字段 id 错误
|
||||||
|
200 | 1254010 | ReqConvError | 请求错误
|
||||||
|
400 | 1254012 | NotSupportFieldOrView | 不支持的字段或视图。注意数据表的初始索引字段仅支持以下类型:<br>- 1:多行文本<br>- 2:数字<br>- 5:日期<br>- 13:电话号码<br>- 15:超链接<br>- 20:公式<br>- 22:地理位置
|
||||||
|
200 | 1254013 | TableNameDuplicated | 表名重复
|
||||||
|
400 | 1254014 | FieldNameDuplicated | 字段名重复
|
||||||
|
400 | 1254021 | EmptyViewName | 视图名为空
|
||||||
|
400 | 1254022 | InvalidViewName | 视图名无效
|
||||||
|
400 | 1254029 | InvalidFieldName | 字段名无效
|
||||||
|
200 | 1254030 | TooLargeResponse | 响应体过大
|
||||||
|
200 | 1254036 | Base is copying, please try again later. | 复制多维表格为异步操作,该错误码表示当前多维表格仍在复制中,在复制期间无法操作当前多维表格。需要等待复制完成后再操作
|
||||||
|
200 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
200 | 1254041 | TableIdNotFound | table_id 不存在
|
||||||
|
200 | 1254044 | FieldIdNotFound | field_id 不存在
|
||||||
|
200 | 1254060 | TextFieldConvFail | 多行文本字段错误
|
||||||
|
200 | 1254061 | NumberFieldConvFail | 数字字段错误
|
||||||
|
200 | 1254062 | SingleSelectFieldConvFail | 单选字段错误
|
||||||
|
200 | 1254063 | MultiSelectFieldConvFail | 多选字段错误
|
||||||
|
200 | 1254064 | DatetimeFieldConvFail | 日期字段错误
|
||||||
|
200 | 1254065 | CheckboxFieldConvFail | 复选框字段错误
|
||||||
|
200 | 1254066 | UserFieldConvFail | 人员字段有误。原因可能是:<br>- `user_id_type` 参数指定的 ID 类型与传入的 ID 类型不匹配<br>- 传入了不识别的类型或结构,目前只支持填写 `id` 参数,且需要传入数组<br>- 跨应用传入了 `open_id`。如果跨应用传入 ID,建议使用 `user_id`。不同应用获取的 `open_id` 不能交叉使用<br>- 若想对人员字段传空,可传 null
|
||||||
|
200 | 1254067 | LinkFieldConvFail | 关联字段错误
|
||||||
|
200 | 1254100 | TableExceedLimit | 数据表或仪表盘数量超限。每个多维表格中,数据表加仪表盘的数量最多为 100 个
|
||||||
|
200 | 1254101 | ViewExceedLimit | 视图数量超限, 限制200个
|
||||||
|
200 | 1254130 | TooLargeCell | 格子内容过大
|
||||||
|
200 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
200 | 1254291 | Write conflict | 同一个数据表(table) 不支持并发调用写接口,请检查是否存在并发调用写接口。写接口包括:新增、修改、删除记录;新增、修改、删除字段;修改表单;修改视图等。
|
||||||
|
200 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
403 | 1254302 | Permission denied. | 调用身份缺少多维表格的高级权限。你需要为调用身份授予高级权限:<br>- 对用户授予高级权限,你需要在多维表格页面右上方 **分享** 入口为当前用户添加可管理权限。<br>- 对应用授予高级权限,你需通过多维表格页面右上方 **「...」** -> **「...更多」** ->**「添加文档应用」** 入口为应用添加可管理权限。<br><br><br>**注意**:<br>在 **添加文档应用** 前,你需确保目标应用至少开通了一个多维表格的 [API 权限](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/scope-list)。否则你将无法在文档应用窗口搜索到目标应用。 <br>- 你也可以在 **多维表格高级权限设置** 中添加用户或一个包含应用的群组, 给予这个群自定义的读写等权限。
|
||||||
|
400 | 1254607 | Data not ready, please try again later | 该报错一般是由于前置操作未执行完成,或本次操作数据太大,服务器计算超时导致。遇到该错误码时,建议等待一段时间后重试。通常有以下几种原因:<br>- **编辑操作频繁**:开发者对多维表格的编辑操作非常频繁。可能会导致由于等待前置操作处理完成耗时过长而超时的情况。多维表格底层对数据表的处理基于版本维度的串行方式,不支持并发。因此,并发请求时容易出现此类错误,不建议开发者对单个数据表进行并发请求。<br>- **批量操作负载重**:开发者在多维表格中进行批量新增、删除等操作时,如果数据表的数据量非常大,可能会导致单次请求耗时过长,最终导致请求超时。建议开发者适当降低批量请求的 page_size 以减少请求耗时。<br>- **资源分配与计算开销**:资源分配是基于单文档维度的,如果读接口涉及公式计算、排序等计算逻辑,会占用较多资源。例如,并发读取一个文档下的多个数据表也可能导致该文档阻塞。
|
||||||
|
200 | 1255001 | InternalError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255002 | RpcError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255003 | MarshalError | 序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255004 | UmMarshalError | 反序列化错误
|
||||||
|
200 | 1255005 | ConvError | 服务内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
131
新增多个数据表.md
Normal file
131
新增多个数据表.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# 新增多个数据表
|
||||||
|
|
||||||
|
新增多个数据表,仅可指定数据表名称。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
调用此接口前,请确保当前调用身份(tenant_access_token 或 user_access_token)已有多维表格的编辑等文档权限,否则接口将返回 HTTP 403 或 400 状态码。了解更多,参考[如何为应用或用户开通文档权限](https://open.feishu.cn/document/ukTMukTMukTM/uczNzUjL3czM14yN3MTN#16c6475a)。
|
||||||
|
|
||||||
|
## 使用限制
|
||||||
|
|
||||||
|
每个多维表格中,数据表与仪表盘的总数量上限为 100。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token/tables/batch_create
|
||||||
|
HTTP Method | POST
|
||||||
|
接口频率限制 | [10 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 新增数据表(base:table:create)<br>查看、评论、编辑和管理多维表格(bitable:app)
|
||||||
|
字段权限要求 | **注意事项**:该接口返回体中存在下列敏感字段,仅当开启对应的权限后才会返回;如果无需获取这些字段,则不建议申请<br>获取用户 user ID(contact:user.employee_id:readonly)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 多维表格 App 的唯一标识。不同形态的多维表格,其 app_token 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 app_token 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 obj_type 的值为 bitable 时,obj_token 字段的值才是多维表格的 app_token。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。<br>**示例值**:"appbcbWCzen6D8dezhoCH2RpMAh"
|
||||||
|
|
||||||
|
### 查询参数
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
user_id_type | string | 否 | 用户 ID 类型<br>**示例值**:open_id<br>**可选值有**:<br>- open_id:标识一个用户在某个应用中的身份。同一个用户在不同应用中的 Open ID 不同。[了解更多:如何获取 Open ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid)<br>- union_id:标识一个用户在某个应用开发商下的身份。同一用户在同一开发商下的应用中的 Union ID 是相同的,在不同开发商下的应用中的 Union ID 是不同的。通过 Union ID,应用开发商可以把同个用户在多个应用中的身份关联起来。[了解更多:如何获取 Union ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-union-id)<br>- user_id:标识一个用户在某个租户内的身份。同一个用户在租户 A 和租户 B 内的 User ID 是不同的。在同一个租户内,一个用户的 User ID 在所有应用(包括商店应用)中都保持一致。User ID 主要用于在不同的应用间打通用户数据。[了解更多:如何获取 User ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-user-id)<br>**默认值**:`open_id`<br>**当值为 `user_id`,字段权限要求**:<br>获取用户 user ID(contact:user.employee_id:readonly)
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
tables | req_table\[\] | 否 | tables
|
||||||
|
name | string | 否 | 数据表名称。该字段必填。<br>**注意**:<br>- 名称中的首尾空格将会被默认去除。<br>- 数据表名称不可以包含 `/ \ ? * : [ ]` 等特殊字符。<br>**示例值**:"一个新的数据表"<br>**数据校验规则**:<br>- 长度范围:`1` ~ `100` 字符
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"name": "一个新的数据表"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
table_ids | string\[\] | table ids
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"table_ids": [
|
||||||
|
"tblIovTTN2eIW2hn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
200 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
200 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
200 | 1254002 | Fail | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
200 | 1254004 | WrongTableId | table_id 错误
|
||||||
|
200 | 1254005 | WrongViewId | view_id 错误
|
||||||
|
200 | 1254006 | WrongRecordId | 检查 record_id
|
||||||
|
200 | 1254007 | EmptyValue | 空值
|
||||||
|
200 | 1254008 | EmptyView | 空视图
|
||||||
|
200 | 1254009 | WrongFieldId | 字段 id 错误
|
||||||
|
200 | 1254010 | ReqConvError | 请求错误
|
||||||
|
400 | 1254013 | TableNameDuplicated | 表名重复
|
||||||
|
200 | 1254030 | TooLargeResponse | 响应体过大
|
||||||
|
400 | 1254036 | Base is copying, please try again later. | 复制多维表格为异步操作,该错误码表示当前多维表格仍在复制中,在复制期间无法操作当前多维表格。需要等待复制完成后再操作
|
||||||
|
200 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
200 | 1254041 | TableIdNotFound | table_id 不存在
|
||||||
|
200 | 1254042 | ViewIdNotFound | view_id 不存在
|
||||||
|
200 | 1254043 | RecordIdNotFound | record_id 不存在
|
||||||
|
200 | 1254044 | FieldIdNotFound | field_id 不存在
|
||||||
|
200 | 1254060 | TextFieldConvFail | 多行文本字段错误
|
||||||
|
200 | 1254061 | NumberFieldConvFail | 数字字段错误
|
||||||
|
200 | 1254062 | SingleSelectFieldConvFail | 单选字段错误
|
||||||
|
200 | 1254063 | MultiSelectFieldConvFail | 多选字段错误
|
||||||
|
200 | 1254064 | DatetimeFieldConvFail | 日期字段错误
|
||||||
|
200 | 1254065 | CheckboxFieldConvFail | 复选框字段错误
|
||||||
|
200 | 1254066 | UserFieldConvFail | 人员字段有误。原因可能是:<br>- `user_id_type` 参数指定的 ID 类型与传入的 ID 类型不匹配<br>- 传入了不识别的类型或结构,目前只支持填写 `id` 参数,且需要传入数组<br>- 跨应用传入了 `open_id`。如果跨应用传入 ID,建议使用 `user_id`。不同应用获取的 `open_id` 不能交叉使用
|
||||||
|
200 | 1254067 | LinkFieldConvFail | 关联字段错误
|
||||||
|
200 | 1254100 | TableExceedLimit | 数据表或仪表盘数量超限。每个多维表格中,数据表加仪表盘的数量最多为 100 个
|
||||||
|
200 | 1254101 | ViewExceedLimit | 视图数量超限, 限制200个
|
||||||
|
200 | 1254102 | FileExceedLimit | 超限
|
||||||
|
200 | 1254103 | RecordExceedLimit | 记录数量超限, 限制20,000条
|
||||||
|
200 | 1254104 | RecordAddOnceExceedLimit | 单次添加记录数量超限, 限制500条
|
||||||
|
200 | 1254130 | TooLargeCell | 格子内容过大
|
||||||
|
200 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
200 | 1254291 | Write conflict | 同一个数据表(table) 不支持并发调用写接口,请检查是否存在并发调用写接口。写接口包括:新增、修改、删除记录;新增、修改、删除字段;修改表单;修改视图等。
|
||||||
|
200 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
403 | 1254302 | The role has no permissions. | 调用身份缺少多维表格的高级权限。你需要为调用身份授予高级权限:<br>- 对用户授予高级权限,你需要在多维表格页面右上方 **分享** 入口为当前用户添加可管理权限。<br>- 对应用授予高级权限,你需通过多维表格页面右上方 **「...」** -> **「...更多」** ->**「添加文档应用」** 入口为应用添加可管理权限。<br><br><br>**注意**:<br>在 **添加文档应用** 前,你需确保目标应用至少开通了一个多维表格的 [API 权限](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/scope-list)。否则你将无法在文档应用窗口搜索到目标应用。 <br>- 你也可以在 **多维表格高级权限设置** 中添加用户或一个包含应用的群组, 给予这个群自定义的读写等权限。
|
||||||
|
200 | 1255001 | InternalError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255002 | RpcError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255003 | MarshalError | 序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255004 | UmMarshalError | 反序列化错误
|
||||||
|
200 | 1255005 | ConvError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
504 | 1255040 | 请求超时 | 进行重试
|
||||||
107
更新多维表格元数据.md
Normal file
107
更新多维表格元数据.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# 更新多维表格元数据
|
||||||
|
|
||||||
|
更新多维表格元数据,包括多维表格的名称、是否开启高级权限。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 在线文档和电子表格中嵌入的多维表格、知识库中的多维表格不支持开启高级权限。
|
||||||
|
- 此接口非原子操作,先修改多维表格名称,后开关高级权限,可能存在部分成功的情况。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token
|
||||||
|
HTTP Method | PUT
|
||||||
|
接口频率限制 | [10 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 更新多维表格(base:app:update)<br>查看、评论、编辑和管理多维表格(bitable:app)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 目标多维表格的 App token。该接口仅支持存储在云空间文件夹中的多维表格,即 URL 以 **feishu.cn/base** 开头的多维表格形态。该类多维表格的 app_token 为 URL 下图高亮部分:<br><br>**示例值**:"appbcbWCzen6D8dezhoCH2RpMAh"
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
name | string | 否 | 新的多维表格名称,不传则不更新名称。<br>**示例值**:"新的多维表格名称"
|
||||||
|
is_advanced | boolean | 否 | 多维表格是否开启高级权限。不传则不更新设置。可选值:<br>- true:开启高级权限<br>- false:关闭高级权限<br>**示例值**:true
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "新的多维表格名称",
|
||||||
|
"is_advanced": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
app | display_app_v2 | 多维表格元数据
|
||||||
|
app_token | string | 多维表格的唯一标识 app_token
|
||||||
|
name | string | 多维表格的名称
|
||||||
|
is_advanced | boolean | 多维表格是否已开启高级权限
|
||||||
|
time_zone | string | 文档时区
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"app": {
|
||||||
|
"app_token": "appbcbWCzen6D8dezhoCH2RpMAh",
|
||||||
|
"name": "新的多维表格名字",
|
||||||
|
"is_advanced": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
200 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
200 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
200 | 1254002 | Fail | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
200 | 1254010 | ReqConvError | 请求错误
|
||||||
|
200 | 1254031 | InvalidAppName | 多维表格名称格式错误,长度不超过 100 个字符,不能包含 ? / \ * : [ ]
|
||||||
|
400 | 1254036 | Base is copying, please try again later. | 多维表格副本复制中,稍后重试
|
||||||
|
200 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
200 | 1254043 | RecordIdNotFound | record_id 不存在
|
||||||
|
200 | 1254200 | internal error | 内部错误
|
||||||
|
200 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
200 | 1254291 | Write conflict | 同一个数据表(table) 不支持并发调用写接口,请检查是否存在并发调用写接口。写接口包括:新增、修改、删除记录;新增、修改、删除字段;修改表单;修改视图等。
|
||||||
|
400 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
403 | 1254302 | Permission denied. | 无访问权限, 常由表格开启了高级权限造成, 请在高级权限设置中添加一个包含应用的群, 给予这个群读写权限
|
||||||
|
403 | 1254304 | The role has no permissions. | 无权限
|
||||||
|
200 | 1255001 | InternalError | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1255002 | RpcError | 内部错误,有疑问可咨询客服
|
||||||
|
200 | 1255003 | MarshalError | 序列化错误,有疑问可咨询客服
|
||||||
|
200 | 1255004 | UmMarshalError | 反序列化错误
|
||||||
|
504 | 1255040 | 请求超时 | 进行重试
|
||||||
|
|
||||||
|
## 补充错误码
|
||||||
|
|
||||||
|
**错误码** | **原因** | **排查建议** |
|
||||||
|
| ------- | ------- | ----------------- |
|
||||||
|
| 1254061 | 字段格式错误。 | 确认对应字段类型参数格式是否正确。
|
||||||
78
更新数据表.md
Normal file
78
更新数据表.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# 更新数据表
|
||||||
|
|
||||||
|
更新数据表的名称。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token/tables/:table_id
|
||||||
|
HTTP Method | PATCH
|
||||||
|
接口频率限制 | [10 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 更新数据表(base:table:update)<br>查看、评论、编辑和管理多维表格(bitable:app)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 多维表格 App 的唯一标识。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。<br>**示例值**:"XrgTb4y1haKYnasu0xXb1g7lcSg"<br>**数据校验规则**:<br>- 最小长度:`1` 字符
|
||||||
|
table_id | string | 多维表格数据表的唯一标识。获取方式:<br>- 你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的 `table_id`<br>- 也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`<br><br>**示例值**:"tbl1TkhyTWDkSoZ3"
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
name | string | 否 | 数据表的新名称。<br>**注意**:<br>- 名称中的首尾空格将会被去除。<br>- 数据表名称不可以包含 `/ \ ? * : [ ]` 等特殊字符。<br>- 如果名称为空或和旧名称相同,接口仍然会返回成功,但是名称不会被更改。<br>**示例值**:"新的数据表名称"<br>**数据校验规则**:<br>- 长度范围:`1` ~ `100` 字符<br>- 正则校验:`^[^\[\]\:\\\/\?\*]+$`
|
||||||
|
|
||||||
|
### 请求体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "新的数据表名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
name | string | 新的数据表名称
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"name": "新的数据表名称"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
400 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
400 | 1254002 | Fail | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
400 | 1254003 | WrongBaseToken | app_token 错误
|
||||||
|
400 | 1254004 | WrongTableId | table_id 错误
|
||||||
|
400 | 1254013 | TableNameDuplicated | 表名重复
|
||||||
|
403 | 1254036 | Base is copying, please try again later. | 复制多维表格为异步操作,该错误码表示当前多维表格仍在复制中,在复制期间无法操作当前多维表格。需要等待复制完成后再操作
|
||||||
|
404 | 1254040 | BaseTokenNotFound | app_token 不存在
|
||||||
|
404 | 1254041 | TableIdNotFound | table_id 不存在
|
||||||
|
429 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
403 | 1254302 | Permission denied. | 调用身份缺少多维表格的高级权限。你需要为调用身份授予高级权限:<br>- 对用户授予高级权限,你需要在多维表格页面右上方 **分享** 入口为当前用户添加可管理权限。<br>- 对应用授予高级权限,你需通过多维表格页面右上方 **「...」** -> **「...更多」** ->**「添加文档应用」** 入口为应用添加可管理权限。<br><br><br>**注意**:<br>在 **添加文档应用** 前,你需确保目标应用至少开通了一个多维表格的 [API 权限](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/scope-list)。否则你将无法在文档应用窗口搜索到目标应用。 <br>- 你也可以在 **多维表格高级权限设置** 中添加用户或一个包含应用的群组, 给予这个群自定义的读写等权限。
|
||||||
|
500 | 1255001 | InternalError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
108
获取多维表格元数据.md
Normal file
108
获取多维表格元数据.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# 获取多维表格元数据
|
||||||
|
|
||||||
|
获取指定多维表格的元数据信息,包括多维表格名称、多维表格版本号、多维表格是否开启高级权限等。
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
基本 |
|
||||||
|
---|---
|
||||||
|
HTTP URL | https://open.feishu.cn/open-apis/bitable/v1/apps/:app_token
|
||||||
|
HTTP Method | GET
|
||||||
|
接口频率限制 | [20 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
|
||||||
|
支持的应用类型 | Custom App、Store App
|
||||||
|
权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用**<br>开启任一权限即可 | 获取多维表格信息(base:app:read)<br>查看、评论、编辑和管理多维表格(bitable:app)<br>查看、评论和导出多维表格(bitable:app:readonly)
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
名称 | 类型 | 必填 | 描述
|
||||||
|
---|---|---|---
|
||||||
|
Authorization | string | 是 | `tenant_access_token`<br>或<br>`user_access_token`<br>**值格式**:"Bearer `access_token`"<br>**示例值**:"Bearer u-7f1bcd13fc57d46bac21793a18e560"<br>[了解更多:如何选择与获取 access token](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-choose-which-type-of-token-to-use)
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
app_token | string | 多维表格 App 的唯一标识。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。<br>**示例值**:"appbcbWCzen6D8dezhoCH2RpMAh"
|
||||||
|
|
||||||
|
## 响应
|
||||||
|
|
||||||
|
### 响应体
|
||||||
|
|
||||||
|
名称 | 类型 | 描述
|
||||||
|
---|---|---
|
||||||
|
code | int | 错误码,非 0 表示失败
|
||||||
|
msg | string | 错误描述
|
||||||
|
data | \- | \-
|
||||||
|
app | display_app | 多维表格元数据
|
||||||
|
app_token | string | 多维表格的唯一标识 app_token
|
||||||
|
name | string | 多维表格的名称
|
||||||
|
revision | int | 多维表格的版本号。对多维表格进行修改时更新,如新增、删除数据表,修改数据表名等,初始为 1,每次更新+1
|
||||||
|
is_advanced | boolean | 多维表格是否开启了高级权限。取值包括:<br>- true:开启了高级权限<br>- false:关闭了高级权限<br>了解更多参考飞书帮助中心文档[使用多维表格高级权限](https://www.feishu.cn/hc/zh-CN/articles/588604550568)。
|
||||||
|
time_zone | string | 多维表格的时区
|
||||||
|
formula_type | int | 多维表格的公式字段类型。可结合[字段相关 API](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/create)使用。<br>**可选值有**:<br>- 1:不支持指定公式字段类型<br>- 2:支持指定公式字段类型
|
||||||
|
advance_version | string | 文档高级权限版本。可结合[自定义角色 API](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-role/create)使用。<br>**可选值有**:<br>- v1:v1版本<br>- v2:v2版本
|
||||||
|
|
||||||
|
### 响应体示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"app": {
|
||||||
|
"app_token": "appbcbWCzen6D8dezhoCH2RpMAh",
|
||||||
|
"name": "mybase",
|
||||||
|
"revision": 1,
|
||||||
|
"is_advanced": false,
|
||||||
|
"time_zone": "Asia/Beijing",
|
||||||
|
"formula_type": 1,
|
||||||
|
"advance_version": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
HTTP状态码 | 错误码 | 描述 | 排查建议
|
||||||
|
---|---|---|---
|
||||||
|
200 | 1254000 | WrongRequestJson | 请求体错误
|
||||||
|
200 | 1254001 | WrongRequestBody | 请求体错误
|
||||||
|
200 | 1254002 | Fail | 导致报 1254002 错误码的场景较多,请参考以下建议排查:<br>- 如果单次操作的内容变更较大,请尝试在单次操作中减少数据量<br>- 如果你并发调用了接口,请尝试控制请求间隔,稍后重试<br>- 如果在知识库(wiki)中创建多维表格,请检查你是否使用了知识库[创建知识空间节点](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space-node/create)接口创建多维表格。在此场景下不能使用[创建多维表格](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app/create)接口<br>- 请检查接口参数是否有误。例如,在分页查询多维表格时,传递了无效的 page_token,或传递了错误的数据表的 table_id<br>- 如果该报错偶尔发生,可能是服务器超时或不稳定,请重试解决
|
||||||
|
200 | 1254003 | WrongBaseToken | app_token 错误。app_token 是多维表格 App 的唯一标识。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。
|
||||||
|
200 | 1254004 | WrongTableId | table_id 错误。table_id 是多维表格数据表的唯一标识。获取方式:<br>- 你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的 `table_id`<br>- 也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`<br>
|
||||||
|
200 | 1254005 | WrongViewId | view_id 错误。view_id 是多维表格中视图的唯一标识。获取方式:<br>- 在多维表格的 URL 地址栏中,`view_id` 是下图中高亮部分:<br><br>- 通过[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)接口获取。暂时无法获取到嵌入到云文档中的多维表格的 `view_id`。<br>**注意**:<br>当 `filter` 参数 或 `sort` 参数不为空时,请求视为对数据表中的全部数据做条件过滤,指定的 `view_id` 会被忽略。
|
||||||
|
200 | 1254006 | WrongRecordId | record_id 错误。record_id 是数据表中一条记录的唯一标识。通过[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口获取
|
||||||
|
200 | 1254007 | EmptyValue | 空值
|
||||||
|
200 | 1254008 | EmptyView | 空视图
|
||||||
|
200 | 1254009 | WrongFieldId | field_id 错误。field_id 是数据表中一个字段的唯一标识。通过[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取
|
||||||
|
200 | 1254010 | ReqConvError | 请求错误
|
||||||
|
200 | 1254030 | TooLargeResponse | 响应体过大
|
||||||
|
400 | 1254036 | Base is copying, please try again later. | 复制多维表格为异步操作,该错误码表示当前多维表格仍在复制中,在复制期间无法操作当前多维表格。需要等待复制完成后再操作
|
||||||
|
200 | 1254040 | BaseTokenNotFound | app_token 不存在。不同形态的多维表格,其 `app_token` 的获取方式不同:<br>- 如果多维表格的 URL 以 ==**feishu.cn/base**== 开头,该多维表格的 `app_token` 是下图高亮部分:<br><br>- 如果多维表格的 URL 以 ==**feishu.cn/wiki**== 开头,你需调用知识库相关[获取知识空间节点信息](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/wiki-v2/space/get_node)接口获取多维表格的 app_token。当 `obj_type` 的值为 `bitable` 时,`obj_token` 字段的值才是多维表格的 `app_token`。<br>了解更多,参考[多维表格 app_token 获取方式](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/bitable-overview#-752212c)。
|
||||||
|
200 | 1254041 | TableIdNotFound | table_id 不存在。获取方式:<br>- 你可通过多维表格 URL 获取 `table_id`,下图高亮部分即为当前数据表的 `table_id`<br>- 也可通过[列出数据表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table/list)接口获取 `table_id`<br>
|
||||||
|
200 | 1254042 | ViewIdNotFound | view_id 不存在。获取方式:<br>- 在多维表格的 URL 地址栏中,`view_id` 是下图中高亮部分:<br><br>- 通过[列出视图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-view/list)接口获取。暂时无法获取到嵌入到云文档中的多维表格的 `view_id`。<br>**注意**:<br>当 `filter` 参数 或 `sort` 参数不为空时,请求视为对数据表中的全部数据做条件过滤,指定的 `view_id` 会被忽略。
|
||||||
|
200 | 1254043 | RecordIdNotFound | record_id 不存在。record_id 是数据表中一条记录的唯一标识。请通过[查询记录](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search)接口获取。
|
||||||
|
200 | 1254044 | FieldIdNotFound | field_id 不存在。field_id 是数据表中一个字段的唯一标识。通过[列出字段](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-field/list)接口获取。
|
||||||
|
200 | 1254060 | TextFieldConvFail | 文本字段错误
|
||||||
|
200 | 1254061 | NumberFieldConvFail | 数字字段错误
|
||||||
|
200 | 1254062 | SingleSelectFieldConvFail | 单选字段错误
|
||||||
|
200 | 1254063 | MultiSelectFieldConvFail | 多选字段错误
|
||||||
|
200 | 1254064 | DatetimeFieldConvFail | 日期字段错误
|
||||||
|
200 | 1254065 | CheckboxFieldConvFail | 复选框字段错误
|
||||||
|
200 | 1254066 | UserFieldConvFail | 人员字段有误。原因可能是:<br>- `user_id_type` 参数指定的 ID 类型与传入的 ID 类型不匹配<br>- 传入了不识别的类型或结构,目前只支持填写 `id` 参数,且需要传入数组<br>- 跨应用传入了 `open_id`。如果跨应用传入 ID,建议使用 `user_id`。不同应用获取的 `open_id` 不能交叉使用<br>- 若想对人员字段传空,可传 null
|
||||||
|
200 | 1254067 | LinkFieldConvFail | 关联字段错误
|
||||||
|
200 | 1254100 | TableExceedLimit | 数据表或仪表盘数量超限。每个多维表格中,数据表加仪表盘的数量最多为 100 个
|
||||||
|
200 | 1254101 | ViewExceedLimit | 视图数量超限, 限制200个
|
||||||
|
200 | 1254102 | FileExceedLimit | 文件数量超限
|
||||||
|
200 | 1254103 | RecordExceedLimit | 记录数量超限, 限制20,000条
|
||||||
|
200 | 1254104 | RecordAddOnceExceedLimit | 单次添加记录数量超限, 单次调用最多更新 1,000 条记录
|
||||||
|
200 | 1254130 | TooLargeCell | 格子内容过大
|
||||||
|
200 | 1254290 | TooManyRequest | 请求过快,稍后重试
|
||||||
|
200 | 1254291 | Write conflict | 在同一个数据表中,并发调用了读写接口或请求过快,出现冲突。请参考以下建议解决:<br>- 确保没有并发调用多维表格读写相关接口<br>- 若操作量较大,建议在接口与接口之间增加 0.5 或 1 秒的延迟,也可在报错中增加重试逻辑,确保业务的稳定性<br>- 对于写接口,可以将接口中的查询参数 `ignore_consistency_check` 设置为 true,表示在读写操作时,暂时忽略一致性检查,以提高性能
|
||||||
|
200 | 1254301 | OperationTypeError | 多维表格未开启高级权限或不支持开启高级权限
|
||||||
|
200 | 1255001 | InternalError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255002 | RpcError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255003 | MarshalError | 序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255004 | UmMarshalError | 反序列化错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
200 | 1255005 | ConvError | 内部错误,请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
|
||||||
|
504 | 1255040 | Request timed out, please try again later | 请求超时,进行重试
|
||||||
Reference in New Issue
Block a user