1use async_trait::async_trait;
2use serde_json::{json, Value};
3use synaptic_core::{SynapticError, Tool};
4
5use crate::{api::bitable::BitableApi, LarkConfig};
6
7pub struct LarkBitableTool {
63 api: BitableApi,
64}
65
66impl LarkBitableTool {
67 pub fn new(config: LarkConfig) -> Self {
69 Self {
70 api: BitableApi::new(config),
71 }
72 }
73}
74
75#[async_trait]
76impl Tool for LarkBitableTool {
77 fn name(&self) -> &'static str {
78 "lark_bitable"
79 }
80
81 fn description(&self) -> &'static str {
82 "Interact with a Feishu/Lark Bitable (multi-dimensional table). \
83 Supports search (with filter operators: is/is_not/contains/does_not_contain/is_empty/is_not_empty), \
84 create, update, delete, batch_update, batch_delete, list_tables, list_fields, \
85 create_table, delete_table, create_field, update_field, delete_field."
86 }
87
88 fn parameters(&self) -> Option<Value> {
89 Some(json!({
90 "type": "object",
91 "properties": {
92 "action": {
93 "type": "string",
94 "description": "Operation to perform.",
95 "enum": [
96 "search", "create", "update", "delete",
97 "batch_update", "batch_delete",
98 "list_tables", "list_fields",
99 "create_table", "delete_table",
100 "create_field", "update_field", "delete_field"
101 ]
102 },
103 "app_token": {
104 "type": "string",
105 "description": "Bitable app token (bascnXxx)"
106 },
107 "table_id": {
108 "type": "string",
109 "description": "Table ID (tblXxx). Required for most actions except create_table and list_tables."
110 },
111 "filter": {
112 "type": "object",
113 "description": "For 'search': {\"field\": \"FieldName\", \"operator\": \"is\", \"value\": \"Val\"}. operator defaults to 'is'; for is_empty/is_not_empty omit value.",
114 "properties": {
115 "field": { "type": "string" },
116 "operator": {
117 "type": "string",
118 "enum": ["is", "is_not", "contains", "does_not_contain", "is_empty", "is_not_empty"]
119 },
120 "value": {}
121 }
122 },
123 "records": {
124 "type": "array",
125 "description": "For 'create': [{\"FieldName\": value}]. For 'batch_update': [{\"record_id\": \"recXxx\", \"fields\": {\"FieldName\": value}}].",
126 "items": { "type": "object" }
127 },
128 "record_id": {
129 "type": "string",
130 "description": "For 'update'/'delete': the record ID (recXxx)"
131 },
132 "record_ids": {
133 "type": "array",
134 "description": "For 'batch_delete': list of record IDs to delete",
135 "items": { "type": "string" }
136 },
137 "fields": {
138 "type": "object",
139 "description": "For 'update': fields to update {\"FieldName\": newValue}"
140 },
141 "table_name": {
142 "type": "string",
143 "description": "For 'create_table': the name for the new table"
144 },
145 "field_name": {
146 "type": "string",
147 "description": "For 'create_field'/'update_field': the field name"
148 },
149 "field_type": {
150 "type": "integer",
151 "description": "For 'create_field': Feishu field type integer (1=text, 2=number, 3=single-select, etc.). Defaults to 1."
152 },
153 "field_id": {
154 "type": "string",
155 "description": "For 'update_field'/'delete_field': the field ID (fldXxx)"
156 }
157 },
158 "required": ["action", "app_token"]
159 }))
160 }
161
162 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
163 let action = args["action"]
164 .as_str()
165 .ok_or_else(|| SynapticError::Tool("missing 'action'".to_string()))?;
166 let app_token = args["app_token"]
167 .as_str()
168 .ok_or_else(|| SynapticError::Tool("missing 'app_token'".to_string()))?;
169
170 let require_table_id = || {
172 args["table_id"]
173 .as_str()
174 .ok_or_else(|| SynapticError::Tool("missing 'table_id'".to_string()))
175 };
176
177 match action {
178 "search" => {
180 let table_id = require_table_id()?;
181 let filter = args.get("filter");
182 let body = build_search_body(filter);
183 let items = self.api.search_records(app_token, table_id, body).await?;
184 Ok(json!({ "records": items }))
185 }
186
187 "create" => {
188 let table_id = require_table_id()?;
189 let raw = args["records"]
190 .as_array()
191 .ok_or_else(|| SynapticError::Tool("missing 'records' array".to_string()))?;
192 let records: Vec<Value> = raw.iter().map(|r| json!({ "fields": r })).collect();
193 let created = self
194 .api
195 .batch_create_records(app_token, table_id, records)
196 .await?;
197 Ok(json!({ "created": created }))
198 }
199
200 "update" => {
201 let table_id = require_table_id()?;
202 let record_id = args["record_id"]
203 .as_str()
204 .ok_or_else(|| SynapticError::Tool("missing 'record_id'".to_string()))?;
205 let fields = args
206 .get("fields")
207 .cloned()
208 .ok_or_else(|| SynapticError::Tool("missing 'fields'".to_string()))?;
209 self.api
210 .update_record(app_token, table_id, record_id, fields)
211 .await?;
212 Ok(json!({ "record_id": record_id, "status": "updated" }))
213 }
214
215 "delete" => {
216 let table_id = require_table_id()?;
217 let record_id = args["record_id"]
218 .as_str()
219 .ok_or_else(|| SynapticError::Tool("missing 'record_id'".to_string()))?;
220 self.api
221 .delete_record(app_token, table_id, record_id)
222 .await?;
223 Ok(json!({ "record_id": record_id, "status": "deleted" }))
224 }
225
226 "batch_update" => {
227 let table_id = require_table_id()?;
228 let records = args["records"]
229 .as_array()
230 .ok_or_else(|| SynapticError::Tool("missing 'records' array".to_string()))?
231 .clone();
232 self.api
233 .batch_update_records(app_token, table_id, records)
234 .await?;
235 Ok(json!({ "status": "updated" }))
236 }
237
238 "batch_delete" => {
239 let table_id = require_table_id()?;
240 let ids: Vec<String> = args["record_ids"]
241 .as_array()
242 .ok_or_else(|| SynapticError::Tool("missing 'record_ids' array".to_string()))?
243 .iter()
244 .filter_map(|v| v.as_str().map(String::from))
245 .collect();
246 if ids.is_empty() {
247 return Err(SynapticError::Tool(
248 "'record_ids' must be a non-empty array".to_string(),
249 ));
250 }
251 self.api
252 .batch_delete_records(app_token, table_id, ids)
253 .await?;
254 Ok(json!({ "status": "deleted" }))
255 }
256
257 "list_tables" => {
259 let tables = self.api.list_tables(app_token).await?;
260 Ok(json!({ "tables": tables }))
261 }
262
263 "create_table" => {
264 let name = args["table_name"]
265 .as_str()
266 .ok_or_else(|| SynapticError::Tool("missing 'table_name'".to_string()))?;
267 let table_id = self.api.create_table(app_token, name).await?;
268 Ok(json!({ "table_id": table_id, "status": "created" }))
269 }
270
271 "delete_table" => {
272 let table_id = require_table_id()?;
273 self.api.delete_table(app_token, table_id).await?;
274 Ok(json!({ "table_id": table_id, "status": "deleted" }))
275 }
276
277 "list_fields" => {
279 let table_id = require_table_id()?;
280 let fields = self.api.list_fields(app_token, table_id).await?;
281 Ok(json!({ "fields": fields }))
282 }
283
284 "create_field" => {
285 let table_id = require_table_id()?;
286 let name = args["field_name"]
287 .as_str()
288 .ok_or_else(|| SynapticError::Tool("missing 'field_name'".to_string()))?;
289 let field_type = args["field_type"].as_u64().unwrap_or(1) as u32;
290 let field_id = self
291 .api
292 .create_field(app_token, table_id, name, field_type)
293 .await?;
294 Ok(json!({ "field_id": field_id, "status": "created" }))
295 }
296
297 "update_field" => {
298 let table_id = require_table_id()?;
299 let field_id = args["field_id"]
300 .as_str()
301 .ok_or_else(|| SynapticError::Tool("missing 'field_id'".to_string()))?;
302 let name = args["field_name"]
303 .as_str()
304 .ok_or_else(|| SynapticError::Tool("missing 'field_name'".to_string()))?;
305 self.api
306 .update_field(app_token, table_id, field_id, name)
307 .await?;
308 Ok(json!({ "field_id": field_id, "status": "updated" }))
309 }
310
311 "delete_field" => {
312 let table_id = require_table_id()?;
313 let field_id = args["field_id"]
314 .as_str()
315 .ok_or_else(|| SynapticError::Tool("missing 'field_id'".to_string()))?;
316 self.api.delete_field(app_token, table_id, field_id).await?;
317 Ok(json!({ "field_id": field_id, "status": "deleted" }))
318 }
319
320 other => Err(SynapticError::Tool(format!(
321 "unknown action '{other}': expected search | create | update | delete | \
322 batch_update | batch_delete | list_tables | list_fields | \
323 create_table | delete_table | create_field | update_field | delete_field"
324 ))),
325 }
326 }
327}
328
329fn build_search_body(filter: Option<&Value>) -> Value {
334 let f = match filter {
335 None => return json!({ "page_size": 20 }),
336 Some(f) => f,
337 };
338
339 let field = f["field"].as_str().unwrap_or("");
340 let operator = f.get("operator").and_then(|v| v.as_str()).unwrap_or("is");
341
342 let condition = match operator {
343 "is_empty" | "is_not_empty" => json!({
344 "field_name": field,
345 "operator": operator
346 }),
347 _ => {
348 let value = &f["value"];
349 json!({
350 "field_name": field,
351 "operator": operator,
352 "value": [value]
353 })
354 }
355 };
356
357 json!({
358 "page_size": 20,
359 "filter": {
360 "conjunction": "and",
361 "conditions": [condition]
362 }
363 })
364}