@m1heng-clawd/feishu 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  Feishu/Lark (飞书) channel plugin for [OpenClaw](https://github.com/openclaw/openclaw).
4
4
 
5
5
  > **中文社区资料** - 配置教程、常见问题、使用技巧:[Wiki](https://github.com/m1heng/clawdbot-feishu/wiki)
6
+ >
7
+ > **Contributing / 贡献指南**: [CONTRIBUTING.md](./CONTRIBUTING.md)
6
8
 
7
9
  [English](#english) | [中文](#中文)
8
10
 
@@ -16,6 +18,19 @@ Feishu/Lark (飞书) channel plugin for [OpenClaw](https://github.com/openclaw/o
16
18
  openclaw plugins install @m1heng-clawd/feishu
17
19
  ```
18
20
 
21
+ > [!IMPORTANT]
22
+ > **Windows Troubleshooting (`spawn npm ENOENT`)**
23
+ >
24
+ > If `openclaw plugins install` fails, install manually:
25
+ >
26
+ > ```bash
27
+ > # 1. Download the package
28
+ > curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
29
+ >
30
+ > # 2. Install from local file
31
+ > openclaw plugins install ./feishu-0.1.3.tgz
32
+ > ```
33
+
19
34
  ### Upgrade
20
35
 
21
36
  ```bash
@@ -61,6 +76,7 @@ openclaw plugins update feishu
61
76
  | `drive:drive:readonly` | `feishu_drive` | List folders, get file info |
62
77
  | `wiki:wiki:readonly` | `feishu_wiki` | List spaces, list nodes, get node info, search |
63
78
  | `bitable:app:readonly` | `feishu_bitable` | Read bitable records and fields |
79
+ | `task:task:read` | `feishu_task_get` | Get task details |
64
80
 
65
81
  **Read-write** (optional, for create/edit/delete operations):
66
82
 
@@ -70,7 +86,10 @@ openclaw plugins update feishu
70
86
  | `docx:document.block:convert` | `feishu_doc` | Markdown to blocks conversion (required for write/append) |
71
87
  | `drive:drive` | `feishu_doc`, `feishu_drive` | Upload images to documents, create folders, move/delete files |
72
88
  | `wiki:wiki` | `feishu_wiki` | Create/move/rename wiki nodes |
73
- | `bitable:app` | `feishu_bitable` | Create/update bitable records |
89
+ | `bitable:app` | `feishu_bitable` | Create/update/delete bitable records and manage fields |
90
+ | `task:task:write` | `feishu_task_create`, `feishu_task_update`, `feishu_task_delete` | Create/update/delete tasks |
91
+
92
+ > Task scope names may vary slightly in Feishu console UI. If needed, search for Task-related permissions and grant read/write accordingly.
74
93
 
75
94
  #### Drive Access ⚠️
76
95
 
@@ -260,7 +279,8 @@ session:
260
279
  - **Document tools**: Read, create, and write Feishu documents with markdown (tables not supported due to API limitations)
261
280
  - **Wiki tools**: Navigate knowledge bases, list spaces, get node details, search, create/move/rename nodes
262
281
  - **Drive tools**: List folders, get file info, create folders, move/delete files
263
- - **Bitable tools**: Read/write bitable (多维表格) records, supports both `/base/` and `/wiki/` URLs
282
+ - **Bitable tools**: Manage bitable (多维表格) fields and records (read/create/update/delete), supports both `/base/` and `/wiki/` URLs
283
+ - **Task tools**: Create, get details, update, and delete tasks via Feishu Task v2 API
264
284
  - **@mention forwarding**: When you @mention someone in your message, the bot's reply will automatically @mention them too
265
285
  - **Permission error notification**: When the bot encounters a Feishu API permission error, it automatically notifies the user with the permission grant URL
266
286
  - **Dynamic agent creation**: Each DM user can have their own isolated agent instance with dedicated workspace (optional)
@@ -299,18 +319,6 @@ Send `/new` command in the chat.
299
319
 
300
320
  Feishu API has rate limits. Streaming updates can easily trigger throttling. We use complete-then-send approach for stability.
301
321
 
302
- #### Windows install error `spawn npm ENOENT`
303
-
304
- If `openclaw plugins install` fails, install manually:
305
-
306
- ```bash
307
- # 1. Download the package
308
- curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
309
-
310
- # 2. Install from local file
311
- openclaw plugins install ./feishu-0.1.3.tgz
312
- ```
313
-
314
322
  #### Cannot find the bot in Feishu
315
323
 
316
324
  1. Ensure the app is published (at least to test version)
@@ -327,6 +335,19 @@ openclaw plugins install ./feishu-0.1.3.tgz
327
335
  openclaw plugins install @m1heng-clawd/feishu
328
336
  ```
329
337
 
338
+ > [!IMPORTANT]
339
+ > **Windows 排错(`spawn npm ENOENT`)**
340
+ >
341
+ > 如果 `openclaw plugins install` 失败,可以手动安装:
342
+ >
343
+ > ```bash
344
+ > # 1. 下载插件包
345
+ > curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
346
+ >
347
+ > # 2. 从本地安装
348
+ > openclaw plugins install ./feishu-0.1.3.tgz
349
+ > ```
350
+
330
351
  ### 升级
331
352
 
332
353
  ```bash
@@ -372,6 +393,7 @@ openclaw plugins update feishu
372
393
  | `drive:drive:readonly` | `feishu_drive` | 列出文件夹、获取文件信息 |
373
394
  | `wiki:wiki:readonly` | `feishu_wiki` | 列出空间、列出节点、获取节点详情、搜索 |
374
395
  | `bitable:app:readonly` | `feishu_bitable` | 读取多维表格记录和字段 |
396
+ | `task:task:read` | `feishu_task_get` | 获取任务详情 |
375
397
 
376
398
  **读写权限**(可选,用于创建/编辑/删除操作):
377
399
 
@@ -381,7 +403,10 @@ openclaw plugins update feishu
381
403
  | `docx:document.block:convert` | `feishu_doc` | Markdown 转 blocks(write/append 必需) |
382
404
  | `drive:drive` | `feishu_doc`, `feishu_drive` | 上传图片到文档、创建文件夹、移动/删除文件 |
383
405
  | `wiki:wiki` | `feishu_wiki` | 创建/移动/重命名知识库节点 |
384
- | `bitable:app` | `feishu_bitable` | 创建/更新多维表格记录 |
406
+ | `bitable:app` | `feishu_bitable` | 创建/更新/删除多维表格记录并管理字段 |
407
+ | `task:task:write` | `feishu_task_create`, `feishu_task_update`, `feishu_task_delete` | 创建/更新/删除任务 |
408
+
409
+ > 飞书控制台中任务权限的显示名称可能略有差异,必要时可按关键字 `task` 搜索并授予对应读写权限。
385
410
 
386
411
  #### 云空间访问权限 ⚠️
387
412
 
@@ -571,7 +596,8 @@ session:
571
596
  - **文档工具**:读取、创建、用 Markdown 写入飞书文档(表格因 API 限制不支持)
572
597
  - **知识库工具**:浏览知识库、列出空间、获取节点详情、搜索、创建/移动/重命名节点
573
598
  - **云空间工具**:列出文件夹、获取文件信息、创建文件夹、移动/删除文件
574
- - **多维表格工具**:读写多维表格记录,支持 `/base/` 和 `/wiki/` 两种链接格式
599
+ - **多维表格工具**:支持多维表格字段与记录的读取/创建/更新/删除,支持 `/base/` 和 `/wiki/` 两种链接格式
600
+ - **任务工具**:基于 Task v2 API 支持任务创建、获取详情、更新和删除
575
601
  - **@ 转发功能**:在消息中 @ 某人,机器人的回复会自动 @ 该用户
576
602
  - **权限错误提示**:当机器人遇到飞书 API 权限错误时,会自动通知用户并提供权限授权链接
577
603
  - **动态 Agent 创建**:每个私聊用户可拥有独立的 agent 实例和专属 workspace(可选)
@@ -610,18 +636,6 @@ session:
610
636
 
611
637
  飞书 API 有请求频率限制,流式更新消息很容易触发限流。当前采用完整回复后一次性发送的方式,以保证稳定性。
612
638
 
613
- #### Windows 安装报错 `spawn npm ENOENT`
614
-
615
- 如果 `openclaw plugins install` 失败,可以手动安装:
616
-
617
- ```bash
618
- # 1. 下载插件包
619
- curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
620
-
621
- # 2. 从本地安装
622
- openclaw plugins install ./feishu-0.1.3.tgz
623
- ```
624
-
625
639
  #### 在飞书里找不到机器人
626
640
 
627
641
  1. 确保应用已发布(至少发布到测试版本)
package/index.ts CHANGED
@@ -7,6 +7,7 @@ import { registerFeishuWikiTools } from "./src/wiki.js";
7
7
  import { registerFeishuDriveTools } from "./src/drive.js";
8
8
  import { registerFeishuPermTools } from "./src/perm.js";
9
9
  import { registerFeishuBitableTools } from "./src/bitable.js";
10
+ import { registerFeishuTaskTools } from "./src/task.js";
10
11
 
11
12
  export { monitorFeishuProvider } from "./src/monitor.js";
12
13
  export {
@@ -23,7 +24,7 @@ export {
23
24
  sendFileFeishu,
24
25
  sendMediaFeishu,
25
26
  } from "./src/media.js";
26
- export { probeFeishu } from "./src/probe.js";
27
+ export { probeFeishu, clearProbeCache } from "./src/probe.js";
27
28
  export {
28
29
  addReactionFeishu,
29
30
  removeReactionFeishu,
@@ -57,6 +58,7 @@ const plugin = {
57
58
  registerFeishuDriveTools(api);
58
59
  registerFeishuPermTools(api);
59
60
  registerFeishuBitableTools(api);
61
+ registerFeishuTaskTools(api);
60
62
  },
61
63
  };
62
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m1heng-clawd/feishu",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
@@ -46,17 +46,17 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "@larksuiteoapi/node-sdk": "^1.58.0",
49
+ "@larksuiteoapi/node-sdk": "^1.59.0",
50
50
  "@sinclair/typebox": "0.34.48",
51
51
  "zod": "^4.3.6"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^25.0.10",
55
- "openclaw": "2026.1.29",
55
+ "openclaw": "2026.2.13",
56
56
  "tsx": "^4.21.0",
57
57
  "typescript": "^5.7.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "openclaw": ">=2026.1.29"
60
+ "openclaw": ">=2026.2.13"
61
61
  }
62
62
  }
@@ -0,0 +1,199 @@
1
+ import type {
2
+ BitableClient,
3
+ BitableFieldCreateData,
4
+ BitableFieldUpdateData,
5
+ } from "./common.js";
6
+ import { formatField, runBitableApiCall } from "./common.js";
7
+
8
+ // -------- Field operations --------
9
+
10
+ export async function listFields(client: BitableClient, appToken: string, tableId: string) {
11
+ const res = await runBitableApiCall("bitable.appTableField.list", () =>
12
+ client.bitable.appTableField.list({
13
+ path: { app_token: appToken, table_id: tableId },
14
+ }),
15
+ );
16
+
17
+ const fields = res.data?.items ?? [];
18
+ return {
19
+ fields: fields.map((f) => formatField(f)),
20
+ total: fields.length,
21
+ };
22
+ }
23
+
24
+ export async function createField(
25
+ client: BitableClient,
26
+ appToken: string,
27
+ tableId: string,
28
+ field: BitableFieldCreateData,
29
+ ) {
30
+ const res = await runBitableApiCall("bitable.appTableField.create", () =>
31
+ client.bitable.appTableField.create({
32
+ path: { app_token: appToken, table_id: tableId },
33
+ data: field,
34
+ }),
35
+ );
36
+
37
+ return {
38
+ field: res.data?.field ? formatField(res.data.field) : undefined,
39
+ };
40
+ }
41
+
42
+ export async function updateField(
43
+ client: BitableClient,
44
+ appToken: string,
45
+ tableId: string,
46
+ fieldId: string,
47
+ field: BitableFieldUpdateData,
48
+ ) {
49
+ const res = await runBitableApiCall("bitable.appTableField.update", () =>
50
+ client.bitable.appTableField.update({
51
+ path: { app_token: appToken, table_id: tableId, field_id: fieldId },
52
+ data: field,
53
+ }),
54
+ );
55
+
56
+ return {
57
+ field: res.data?.field ? formatField(res.data.field) : undefined,
58
+ };
59
+ }
60
+
61
+ export async function deleteField(
62
+ client: BitableClient,
63
+ appToken: string,
64
+ tableId: string,
65
+ fieldId: string,
66
+ ) {
67
+ const res = await runBitableApiCall("bitable.appTableField.delete", () =>
68
+ client.bitable.appTableField.delete({
69
+ path: { app_token: appToken, table_id: tableId, field_id: fieldId },
70
+ }),
71
+ );
72
+
73
+ return {
74
+ success: res.data?.deleted ?? true,
75
+ field_id: res.data?.field_id ?? fieldId,
76
+ deleted: res.data?.deleted ?? true,
77
+ };
78
+ }
79
+
80
+ // -------- Record operations --------
81
+
82
+ export async function listRecords(
83
+ client: BitableClient,
84
+ appToken: string,
85
+ tableId: string,
86
+ pageSize?: number,
87
+ pageToken?: string,
88
+ ) {
89
+ const res = await runBitableApiCall("bitable.appTableRecord.list", () =>
90
+ client.bitable.appTableRecord.list({
91
+ path: { app_token: appToken, table_id: tableId },
92
+ params: {
93
+ page_size: pageSize ?? 100,
94
+ ...(pageToken && { page_token: pageToken }),
95
+ },
96
+ }),
97
+ );
98
+
99
+ return {
100
+ records: res.data?.items ?? [],
101
+ has_more: res.data?.has_more ?? false,
102
+ page_token: res.data?.page_token,
103
+ total: res.data?.total,
104
+ };
105
+ }
106
+
107
+ export async function getRecord(
108
+ client: BitableClient,
109
+ appToken: string,
110
+ tableId: string,
111
+ recordId: string,
112
+ ) {
113
+ const res = await runBitableApiCall("bitable.appTableRecord.get", () =>
114
+ client.bitable.appTableRecord.get({
115
+ path: { app_token: appToken, table_id: tableId, record_id: recordId },
116
+ }),
117
+ );
118
+
119
+ return {
120
+ record: res.data?.record,
121
+ };
122
+ }
123
+
124
+ export async function createRecord(
125
+ client: BitableClient,
126
+ appToken: string,
127
+ tableId: string,
128
+ fields: Record<string, unknown>,
129
+ ) {
130
+ const res = await runBitableApiCall("bitable.appTableRecord.create", () =>
131
+ client.bitable.appTableRecord.create({
132
+ path: { app_token: appToken, table_id: tableId },
133
+ data: { fields },
134
+ }),
135
+ );
136
+
137
+ return {
138
+ record: res.data?.record,
139
+ };
140
+ }
141
+
142
+ export async function updateRecord(
143
+ client: BitableClient,
144
+ appToken: string,
145
+ tableId: string,
146
+ recordId: string,
147
+ fields: Record<string, unknown>,
148
+ ) {
149
+ const res = await runBitableApiCall("bitable.appTableRecord.update", () =>
150
+ client.bitable.appTableRecord.update({
151
+ path: { app_token: appToken, table_id: tableId, record_id: recordId },
152
+ data: { fields },
153
+ }),
154
+ );
155
+
156
+ return {
157
+ record: res.data?.record,
158
+ };
159
+ }
160
+
161
+ export async function deleteRecord(
162
+ client: BitableClient,
163
+ appToken: string,
164
+ tableId: string,
165
+ recordId: string,
166
+ ) {
167
+ const res = await runBitableApiCall("bitable.appTableRecord.delete", () =>
168
+ client.bitable.appTableRecord.delete({
169
+ path: { app_token: appToken, table_id: tableId, record_id: recordId },
170
+ }),
171
+ );
172
+
173
+ return {
174
+ success: res.data?.deleted ?? true,
175
+ record_id: res.data?.record_id ?? recordId,
176
+ deleted: res.data?.deleted ?? true,
177
+ };
178
+ }
179
+
180
+ export async function batchDeleteRecords(
181
+ client: BitableClient,
182
+ appToken: string,
183
+ tableId: string,
184
+ recordIds: string[],
185
+ ) {
186
+ const res = await runBitableApiCall("bitable.appTableRecord.batchDelete", () =>
187
+ client.bitable.appTableRecord.batchDelete({
188
+ path: { app_token: appToken, table_id: tableId },
189
+ data: { records: recordIds },
190
+ }),
191
+ );
192
+
193
+ const results = res.data?.records ?? [];
194
+ return {
195
+ results,
196
+ requested: recordIds.length,
197
+ deleted: results.filter((r) => r.deleted).length,
198
+ };
199
+ }
@@ -0,0 +1,90 @@
1
+ import { createFeishuClient } from "../client.js";
2
+ import {
3
+ errorResult,
4
+ json,
5
+ runFeishuApiCall,
6
+ type FeishuApiResponse,
7
+ } from "../tools-common/feishu-api.js";
8
+
9
+ export type BitableClient = ReturnType<typeof createFeishuClient>;
10
+
11
+ // SDK-derived payload types keep tool input strongly typed and aligned with upstream API changes.
12
+ type AppTableFieldCreatePayload = NonNullable<
13
+ Parameters<BitableClient["bitable"]["appTableField"]["create"]>[0]
14
+ >;
15
+ type AppTableFieldUpdatePayload = NonNullable<
16
+ Parameters<BitableClient["bitable"]["appTableField"]["update"]>[0]
17
+ >;
18
+
19
+ export type BitableFieldCreateData = AppTableFieldCreatePayload["data"];
20
+ export type BitableFieldUpdateData = AppTableFieldUpdatePayload["data"];
21
+ export type BitableFieldDescription = NonNullable<BitableFieldCreateData["description"]>;
22
+
23
+ export { json, errorResult };
24
+
25
+ const RETRYABLE_BITABLE_ERROR_CODES = new Set<number>([
26
+ 1254607, // Data not ready
27
+ 1255040, // Request timeout
28
+ 1254290, // Too many requests
29
+ 1254291, // Write conflict
30
+ ]);
31
+
32
+ const RETRY_BACKOFF_MS = [350, 900, 1800];
33
+
34
+ export async function runBitableApiCall<T extends FeishuApiResponse>(
35
+ context: string,
36
+ fn: () => Promise<T>,
37
+ ): Promise<T> {
38
+ // Bitable APIs can briefly return "Data not ready" after writes.
39
+ // Retry only known transient codes with short backoff.
40
+ return runFeishuApiCall(context, fn, {
41
+ retryableCodes: RETRYABLE_BITABLE_ERROR_CODES,
42
+ backoffMs: RETRY_BACKOFF_MS,
43
+ });
44
+ }
45
+
46
+ const FIELD_TYPE_NAMES: Record<number, string> = {
47
+ 1: "Text",
48
+ 2: "Number",
49
+ 3: "SingleSelect",
50
+ 4: "MultiSelect",
51
+ 5: "DateTime",
52
+ 7: "Checkbox",
53
+ 11: "User",
54
+ 13: "Phone",
55
+ 15: "URL",
56
+ 17: "Attachment",
57
+ 18: "SingleLink",
58
+ 19: "Lookup",
59
+ 20: "Formula",
60
+ 21: "DuplexLink",
61
+ 22: "Location",
62
+ 23: "GroupChat",
63
+ 1001: "CreatedTime",
64
+ 1002: "ModifiedTime",
65
+ 1003: "CreatedUser",
66
+ 1004: "ModifiedUser",
67
+ 1005: "AutoNumber",
68
+ };
69
+
70
+ export function formatField(field: {
71
+ field_id?: string;
72
+ field_name?: string;
73
+ type?: number;
74
+ is_primary?: boolean;
75
+ property?: unknown;
76
+ ui_type?: string;
77
+ is_hidden?: boolean;
78
+ }) {
79
+ const typeName = field.type != null ? FIELD_TYPE_NAMES[field.type] || `type_${field.type}` : undefined;
80
+ return {
81
+ field_id: field.field_id,
82
+ field_name: field.field_name,
83
+ type: field.type,
84
+ ...(typeName && { type_name: typeName }),
85
+ is_primary: field.is_primary,
86
+ ui_type: field.ui_type,
87
+ is_hidden: field.is_hidden,
88
+ ...(field.property && { property: field.property }),
89
+ };
90
+ }
@@ -0,0 +1,80 @@
1
+ import { runBitableApiCall, type BitableClient } from "./common.js";
2
+
3
+ // Accept both native bitable URLs and wiki-embedded bitable URLs.
4
+ function parseBitableUrl(url: string): { token: string; tableId?: string; isWiki: boolean } | null {
5
+ try {
6
+ const u = new URL(url);
7
+ const tableId = u.searchParams.get("table") ?? undefined;
8
+
9
+ const wikiMatch = u.pathname.match(/\/wiki\/([A-Za-z0-9]+)/);
10
+ if (wikiMatch) {
11
+ return { token: wikiMatch[1], tableId, isWiki: true };
12
+ }
13
+
14
+ const baseMatch = u.pathname.match(/\/base\/([A-Za-z0-9]+)/);
15
+ if (baseMatch) {
16
+ return { token: baseMatch[1], tableId, isWiki: false };
17
+ }
18
+
19
+ return null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ async function getAppTokenFromWiki(client: BitableClient, nodeToken: string): Promise<string> {
26
+ // Wiki links expose a wiki node token; bitable APIs require app_token.
27
+ const res = await runBitableApiCall("wiki.space.getNode", () =>
28
+ client.wiki.space.getNode({
29
+ params: { token: nodeToken },
30
+ }),
31
+ );
32
+
33
+ const node = res.data?.node;
34
+ if (!node) throw new Error("Node not found");
35
+ if (node.obj_type !== "bitable") {
36
+ throw new Error(`Node is not a bitable (type: ${node.obj_type})`);
37
+ }
38
+
39
+ return node.obj_token!;
40
+ }
41
+
42
+ export async function getBitableMeta(client: BitableClient, url: string) {
43
+ const parsed = parseBitableUrl(url);
44
+ if (!parsed) {
45
+ throw new Error("Invalid URL format. Expected /base/XXX or /wiki/XXX URL");
46
+ }
47
+
48
+ const appToken = parsed.isWiki ? await getAppTokenFromWiki(client, parsed.token) : parsed.token;
49
+
50
+ const res = await runBitableApiCall("bitable.app.get", () =>
51
+ client.bitable.app.get({
52
+ path: { app_token: appToken },
53
+ }),
54
+ );
55
+
56
+ let tables: { table_id: string; name: string }[] = [];
57
+ if (!parsed.tableId) {
58
+ // If table is not specified in URL, return available tables to guide the next call.
59
+ const tablesRes = await runBitableApiCall("bitable.appTable.list", () =>
60
+ client.bitable.appTable.list({
61
+ path: { app_token: appToken },
62
+ }),
63
+ );
64
+ tables = (tablesRes.data?.items ?? []).map((t) => ({
65
+ table_id: t.table_id!,
66
+ name: t.name!,
67
+ }));
68
+ }
69
+
70
+ return {
71
+ app_token: appToken,
72
+ table_id: parsed.tableId,
73
+ name: res.data?.app?.name,
74
+ url_type: parsed.isWiki ? "wiki" : "base",
75
+ ...(tables.length > 0 && { tables }),
76
+ hint: parsed.tableId
77
+ ? `Use app_token="${appToken}" and table_id="${parsed.tableId}" for other bitable tools`
78
+ : `Use app_token="${appToken}" for other bitable tools. Select a table_id from the tables list.`,
79
+ };
80
+ }