@modesto_ma/dsh-channels 0.5.0-beta.2 → 0.5.0-beta.4

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.
Files changed (46) hide show
  1. package/README.md +72 -15
  2. package/docs/attachment-pipeline-v2.md +2 -2
  3. package/docs/feishu-onboarding.md +4 -3
  4. package/lib/client.js +369 -94
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +7847 -5293
  7. package/lib/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/access/controller.ts +32 -1
  10. package/src/analyze/contract.ts +121 -0
  11. package/src/analyze/launcher.ts +353 -0
  12. package/src/analyze/server.ts +201 -0
  13. package/src/cards/analyze.ts +116 -0
  14. package/src/cards/index.ts +1 -0
  15. package/src/cards/streaming.ts +58 -0
  16. package/src/client/api.ts +15 -0
  17. package/src/client/panel/access-card.tsx +96 -70
  18. package/src/client/panel/analyze-token-card.tsx +70 -0
  19. package/src/client/panel/index.tsx +72 -10
  20. package/src/client/panel/sessions-tab.tsx +7 -4
  21. package/src/client/panel/shared.tsx +12 -0
  22. package/src/client/panel/wiki-tab.tsx +62 -0
  23. package/src/config.ts +52 -14
  24. package/src/domain.ts +55 -0
  25. package/src/feishu/scopes.ts +19 -1
  26. package/src/feishu/tools.ts +3 -1
  27. package/src/feishu/wiki/a1.ts +97 -0
  28. package/src/feishu/wiki/api.ts +21 -6
  29. package/src/feishu/wiki/base-api.ts +267 -0
  30. package/src/feishu/wiki/base-tools.ts +423 -0
  31. package/src/feishu/wiki/shared.ts +284 -0
  32. package/src/feishu/wiki/sheet-api.ts +250 -0
  33. package/src/feishu/wiki/sheet-tools.ts +493 -0
  34. package/src/feishu/wiki/tools.ts +102 -189
  35. package/src/feishu-channel.ts +14 -1
  36. package/src/health.ts +12 -0
  37. package/src/host/agent.ts +5 -1
  38. package/src/index.ts +1 -1
  39. package/src/interactions/pending.ts +11 -2
  40. package/src/routing/inbound.ts +57 -14
  41. package/src/service.ts +161 -28
  42. package/src/settings/contract.ts +37 -6
  43. package/src/settings/routes.ts +66 -14
  44. package/src/settings/surface.ts +69 -20
  45. package/src/streamer.ts +156 -7
  46. package/src/workspaces.ts +31 -0
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Wiki Base (多维表格) tools: `feishu_wiki_base_read` (switch
3
+ * `feishuTools.wikiRead`) and `feishu_wiki_base_edit` (switch
4
+ * `feishuTools.wikiWrite`).
5
+ *
6
+ * Both run through the same gates as the docx tools (whitelist, `wikiRoots`
7
+ * containment, consent cards). Reading shares the docx read grant per Session
8
+ * and document. Editing changes records only (no table or field structure):
9
+ * on a base the app owns it may create, update, and delete records; on any
10
+ * other base it may only create records, so existing data written by people
11
+ * is never changed. Every edit asks on a consent card. Feishu offers no
12
+ * revision precondition for record writes, and applies them asynchronously.
13
+ *
14
+ * @module dsh-channels/feishu/wiki/base-tools
15
+ */
16
+
17
+ import { z } from 'zod'
18
+ import type { WikiNode } from './api.ts'
19
+ import { BASE_BATCH_MAX_RECORDS } from './base-api.ts'
20
+ import type { BaseApi, BaseField, BaseRecordFields, BaseTable } from './base-api.ts'
21
+ import { parseWikiRef } from './links.ts'
22
+ import { display, failure, failureOf, guarded, nodeUrl, ownedByApp, parseArgs, preview } from './shared.ts'
23
+ import type { WikiCallContext, WikiFailure, WikiToolKit, WikiToolSpec } from './shared.ts'
24
+
25
+ /** Model-facing tool names; stable across releases (session history references them). */
26
+ export const WIKI_BASE_READ_TOOL_NAME = 'feishu_wiki_base_read'
27
+ export const WIKI_BASE_EDIT_TOOL_NAME = 'feishu_wiki_base_edit'
28
+
29
+ /** Most operations one `feishu_wiki_base_edit` call may apply. */
30
+ export const WIKI_BASE_EDIT_MAX_OPERATIONS = 10
31
+
32
+ /** Record page size bounds of `feishu_wiki_base_read`. */
33
+ const READ_DEFAULT_LIMIT = 50
34
+ const READ_MAX_LIMIT = 200
35
+
36
+ /** Records of each operation shown on a consent card. */
37
+ const CARD_PREVIEW_RECORDS = 3
38
+
39
+ // ---- Arguments ----------------------------------------------------------
40
+
41
+ const refSchema = z.string().trim().min(1).max(2048)
42
+ const nameSchema = z.string().trim().min(1).max(200)
43
+ const recordIdSchema = z.string().trim().min(1).max(64)
44
+ const fieldMapSchema = z.record(z.string().min(1).max(200), z.unknown())
45
+ .refine(map => Object.keys(map).length > 0, '至少包含一个字段')
46
+
47
+ export const wikiBaseReadArgsSchema = z.object({
48
+ doc: refSchema,
49
+ table: nameSchema.optional(),
50
+ filter: z.object({
51
+ logic: z.enum(['and', 'or']).optional(),
52
+ conditions: z.array(z.array(z.unknown()).min(2).max(3)).min(1).max(50),
53
+ }).strict().optional(),
54
+ sort: z.array(z.object({ field: nameSchema, desc: z.boolean().optional() }).strict()).min(1).max(10).optional(),
55
+ keyword: z.string().trim().min(1).max(200).optional(),
56
+ search_fields: z.array(nameSchema).min(1).max(20).optional(),
57
+ fields: z.array(nameSchema).min(1).max(100).optional(),
58
+ view_id: nameSchema.optional(),
59
+ offset: z.number().int().min(0).optional(),
60
+ limit: z.number().int().min(1).max(READ_MAX_LIMIT).optional(),
61
+ }).strict()
62
+
63
+ const baseOperationSchema = z.discriminatedUnion('command', [
64
+ z.object({ command: z.literal('create'), records: z.array(fieldMapSchema).min(1).max(BASE_BATCH_MAX_RECORDS) }).strict(),
65
+ z.object({
66
+ command: z.literal('update'),
67
+ records: z.array(z.object({ record_id: recordIdSchema, fields: fieldMapSchema }).strict()).min(1).max(BASE_BATCH_MAX_RECORDS),
68
+ }).strict(),
69
+ z.object({ command: z.literal('delete'), record_ids: z.array(recordIdSchema).min(1).max(BASE_BATCH_MAX_RECORDS) }).strict(),
70
+ ])
71
+
72
+ export type WikiBaseOperation = z.infer<typeof baseOperationSchema>
73
+
74
+ export const wikiBaseEditArgsSchema = z.object({
75
+ doc: refSchema,
76
+ table: nameSchema,
77
+ operations: z.array(baseOperationSchema).min(1).max(WIKI_BASE_EDIT_MAX_OPERATIONS),
78
+ }).strict()
79
+
80
+ // ---- Results ------------------------------------------------------------
81
+
82
+ /** One field as the tools report it: id, name, type, and Feishu's remaining properties. */
83
+ export type WikiBaseFieldView = Readonly<Record<string, unknown>> & {
84
+ readonly field_id: string
85
+ readonly name: string
86
+ readonly type: string
87
+ }
88
+
89
+ /** Successful `feishu_wiki_base_read`. */
90
+ export interface WikiBaseReadValue {
91
+ readonly ok: true
92
+ readonly title: string
93
+ readonly node_token: string
94
+ readonly url: string
95
+ readonly tables: ReadonlyArray<{ readonly table_id: string; readonly name: string }>
96
+ /** The table read; absent when the call only listed the tables. */
97
+ readonly table_id?: string
98
+ readonly fields?: readonly WikiBaseFieldView[]
99
+ readonly records?: ReadonlyArray<{ readonly record_id: string; readonly fields: Readonly<Record<string, unknown>> }>
100
+ readonly has_more?: boolean
101
+ /** Offset of the next page when `has_more`. */
102
+ readonly next_offset?: number
103
+ readonly timezone?: string
104
+ readonly ignored_fields?: readonly string[]
105
+ /** What `feishu_wiki_base_edit` may do here: `all` (owned by the app), `create` (add records only), or `none`. */
106
+ readonly writable: 'all' | 'create' | 'none'
107
+ readonly warning?: string
108
+ }
109
+
110
+ /** Successful `feishu_wiki_base_edit`. */
111
+ export interface WikiBaseEditValue {
112
+ readonly ok: true
113
+ readonly title: string
114
+ readonly node_token: string
115
+ readonly url: string
116
+ readonly table_id: string
117
+ readonly applied: number
118
+ readonly created_record_ids: readonly string[]
119
+ readonly updated: number
120
+ readonly deleted: number
121
+ readonly ignored_fields: readonly string[]
122
+ }
123
+
124
+ // ---- Descriptions -------------------------------------------------------
125
+
126
+ const VALUE_NOTE = '单元格取值:文本为字符串;数字为数字;单选/多选为选项名数组(如 ["进行中"],单选也是数组);日期为 "2026-03-24 10:00" 或毫秒时间戳;'
127
+ + '复选框为布尔;人员为 [{"id":"ou_xxx"}];关联为 [{"id":"rec_xxx"}];null 清空。公式、查找引用、创建时间等字段只读。'
128
+
129
+ const READ_DESCRIPTION =
130
+ '读取知识库中的飞书多维表格(bitable)。doc 传知识库节点链接或 node_token。'
131
+ + '第一次读取某个多维表格会在话题里弹出授权卡,由话题发起人确认,确认后本话题内再次读取不再询问;等待期间不要重复调用。'
132
+ + '不传 table 时只返回数据表列表;传 table(table_id 或表名)时返回字段列表和记录。'
133
+ + 'filter 形如 {"logic":"and","conditions":[["状态","intersects",["进行中"]],["金额",">=",100],["备注","non_empty"]]},'
134
+ + '运算符有 == != > >= < <= intersects(包含)disjoint(不包含)empty non_empty;日期条件用 "ExactDate(2026-08-07)",日期不支持 >=。'
135
+ + 'sort 形如 [{"field":"更新时间","desc":true}];keyword 为关键词搜索,search_fields 指定搜索的字段(默认主字段);fields 只返回指定字段。'
136
+ + `分页用 offset/limit(limit 默认 ${READ_DEFAULT_LIMIT},最多 ${READ_MAX_LIMIT}),has_more 为 true 时用 next_offset 继续。`
137
+ + 'writable 说明能做哪些修改:all 表示可增改删记录(本应用创建的表),create 表示只能新增记录,none 表示不能修改。'
138
+
139
+ const EDIT_DESCRIPTION =
140
+ '修改知识库中飞书多维表格(bitable)某个数据表的记录。先用 feishu_wiki_base_read 读取字段列表,按字段名写入。'
141
+ + `operations 按顺序执行(最多 ${WIKI_BASE_EDIT_MAX_OPERATIONS} 个,每个最多 ${BASE_BATCH_MAX_RECORDS} 条记录):`
142
+ + 'create(records 为字段名→值的对象数组)、update(records 为 {record_id, fields} 数组,只改给出的字段)、delete(record_ids)。'
143
+ + '本应用创建的多维表格可以增改删;其他多维表格只能新增记录,不能修改或删除已有记录。不会修改数据表和字段结构。'
144
+ + VALUE_NOTE
145
+ + '写入是异步生效的,立刻读取可能还看不到。每次修改都会在话题里弹出授权卡,由话题发起人确认后才执行;等待期间不要重复调用。'
146
+
147
+ // ---- Parameters ---------------------------------------------------------
148
+
149
+ const REF_DESCRIPTION = '知识库节点链接 https://<租户>.feishu.cn/wiki/<node_token> 或 node_token'
150
+ const TABLE_PROPERTY = { type: 'string', description: '数据表 table_id 或表名' }
151
+ const FIELD_MAP_PROPERTY = { type: 'object', description: '字段名(或字段 id)→ 值', additionalProperties: true }
152
+
153
+ const READ_PARAMETERS: Record<string, unknown> = {
154
+ type: 'object',
155
+ properties: {
156
+ doc: { type: 'string', description: REF_DESCRIPTION },
157
+ table: { ...TABLE_PROPERTY, description: '数据表 table_id 或表名;不传只列出数据表' },
158
+ filter: {
159
+ type: 'object',
160
+ properties: {
161
+ logic: { type: 'string', enum: ['and', 'or'] },
162
+ conditions: { type: 'array', items: { type: 'array', description: '[字段, 运算符, 值];empty / non_empty 不带值' } },
163
+ },
164
+ required: ['conditions'],
165
+ additionalProperties: false,
166
+ },
167
+ sort: {
168
+ type: 'array',
169
+ items: { type: 'object', properties: { field: { type: 'string' }, desc: { type: 'boolean' } }, required: ['field'], additionalProperties: false },
170
+ },
171
+ keyword: { type: 'string', description: '关键词搜索' },
172
+ search_fields: { type: 'array', items: { type: 'string' }, description: 'keyword 搜索的字段,默认主字段' },
173
+ fields: { type: 'array', items: { type: 'string' }, description: '只返回这些字段' },
174
+ view_id: { type: 'string', description: '按视图读取(应用视图的筛选与排序)' },
175
+ offset: { type: 'integer', minimum: 0 },
176
+ limit: { type: 'integer', minimum: 1, maximum: READ_MAX_LIMIT },
177
+ },
178
+ required: ['doc'],
179
+ additionalProperties: false,
180
+ }
181
+
182
+ const EDIT_PARAMETERS: Record<string, unknown> = {
183
+ type: 'object',
184
+ properties: {
185
+ doc: { type: 'string', description: REF_DESCRIPTION },
186
+ table: TABLE_PROPERTY,
187
+ operations: {
188
+ type: 'array',
189
+ minItems: 1,
190
+ maxItems: WIKI_BASE_EDIT_MAX_OPERATIONS,
191
+ items: {
192
+ type: 'object',
193
+ properties: {
194
+ command: { type: 'string', enum: ['create', 'update', 'delete'] },
195
+ records: {
196
+ type: 'array',
197
+ maxItems: BASE_BATCH_MAX_RECORDS,
198
+ description: 'create:字段映射数组;update:{record_id, fields} 数组',
199
+ items: {
200
+ type: 'object',
201
+ properties: { record_id: { type: 'string' }, fields: FIELD_MAP_PROPERTY },
202
+ additionalProperties: true,
203
+ },
204
+ },
205
+ record_ids: { type: 'array', maxItems: BASE_BATCH_MAX_RECORDS, items: { type: 'string' }, description: 'delete 的记录 id' },
206
+ },
207
+ required: ['command'],
208
+ additionalProperties: false,
209
+ },
210
+ },
211
+ },
212
+ required: ['doc', 'table', 'operations'],
213
+ additionalProperties: false,
214
+ }
215
+
216
+ // ---- Shared steps -------------------------------------------------------
217
+
218
+ interface BaseTarget {
219
+ readonly context: WikiCallContext
220
+ readonly bases: BaseApi
221
+ readonly node: WikiNode
222
+ readonly url: string
223
+ }
224
+
225
+ async function baseTarget(kit: WikiToolKit, context: WikiCallContext, doc: string): Promise<BaseTarget | WikiFailure> {
226
+ const bases = context.bases
227
+ if (bases === undefined) return failure('CHANNEL_UNAVAILABLE')
228
+ const ref = parseWikiRef(doc)
229
+ if (ref === undefined) return failure('INVALID_REFERENCE')
230
+ const node = await kit.containedNode(context, ref)
231
+ if ('ok' in node) return node
232
+ if (node.objType !== 'bitable') return failure('UNSUPPORTED_TYPE', `${failure('UNSUPPORTED_TYPE').reason}(该节点类型:${node.objType})`)
233
+ return { context, bases, node, url: nodeUrl(context.roots, node) }
234
+ }
235
+
236
+ function writableOf(kit: WikiToolKit, target: BaseTarget): 'all' | 'create' | 'none' {
237
+ if (!kit.writeEnabled()) return 'none'
238
+ return ownedByApp(target.node, target.context.botOpenId) ? 'all' : 'create'
239
+ }
240
+
241
+ function pickTable(tables: readonly BaseTable[], wanted: string): BaseTable | string {
242
+ const match = tables.find(table => table.tableId === wanted) ?? tables.find(table => table.name === wanted)
243
+ if (match !== undefined) return match
244
+ const list = tables.map(table => `${table.name}(${table.tableId})`).join('、') || '无'
245
+ return `找不到数据表「${wanted}」;可用数据表:${list}`
246
+ }
247
+
248
+ function fieldView(field: BaseField): WikiBaseFieldView {
249
+ return { ...field.properties, field_id: field.fieldId, name: field.name, type: field.type }
250
+ }
251
+
252
+ // ---- feishu_wiki_base_read ---------------------------------------------
253
+
254
+ async function read(kit: WikiToolKit, sessionId: string, rawArgs: Record<string, unknown>, signal: AbortSignal): Promise<object> {
255
+ const args = parseArgs(WIKI_BASE_READ_TOOL_NAME, wikiBaseReadArgsSchema, rawArgs)
256
+ const gate = kit.gate(sessionId, WIKI_BASE_READ_TOOL_NAME)
257
+ if (!gate.ok) return gate
258
+ return await guarded<WikiBaseReadValue>(kit, WIKI_BASE_READ_TOOL_NAME, async () => {
259
+ const target = await baseTarget(kit, gate.context, args.doc)
260
+ if ('ok' in target) return target
261
+ const denied = await kit.readConsent(sessionId, target.context, target.node, signal)
262
+ if (denied !== undefined) return denied
263
+ if (signal.aborted) return failure('ABORTED')
264
+ const token = target.node.objToken
265
+ const tables = await target.bases.listTables(token)
266
+ const base = {
267
+ ok: true as const,
268
+ title: target.node.title,
269
+ node_token: target.node.nodeToken,
270
+ url: target.url,
271
+ tables: tables.map(table => ({ table_id: table.tableId, name: table.name })),
272
+ writable: writableOf(kit, target),
273
+ }
274
+ if (args.table === undefined) return base
275
+ const table = pickTable(tables, args.table)
276
+ if (typeof table === 'string') return failure('INVALID_OPERATION', table)
277
+ const fields = await target.bases.listFields(token, table.tableId)
278
+ const offset = args.offset ?? 0
279
+ const query = {
280
+ ...(args.filter === undefined ? {} : { filter: args.filter }),
281
+ ...(args.sort === undefined ? {} : { sort: args.sort }),
282
+ ...(args.fields === undefined ? {} : { fields: args.fields }),
283
+ ...(args.view_id === undefined ? {} : { viewId: args.view_id }),
284
+ offset,
285
+ limit: args.limit ?? READ_DEFAULT_LIMIT,
286
+ }
287
+ let page
288
+ if (args.keyword === undefined) {
289
+ page = await target.bases.listRecords(token, table.tableId, query)
290
+ } else {
291
+ const primary = fields[0]?.name
292
+ const searchFields = args.search_fields ?? (primary === undefined ? [] : [primary])
293
+ if (searchFields.length === 0) return failure('INVALID_OPERATION', 'keyword 搜索需要 search_fields')
294
+ page = await target.bases.searchRecords(token, table.tableId, { ...query, keyword: args.keyword, searchFields })
295
+ }
296
+ return {
297
+ ...base,
298
+ table_id: table.tableId,
299
+ fields: fields.map(fieldView),
300
+ records: page.records.map(record => ({ record_id: record.recordId, fields: record.fields })),
301
+ has_more: page.hasMore,
302
+ ...(page.hasMore ? { next_offset: offset + page.records.length } : {}),
303
+ ...(page.timezone === undefined ? {} : { timezone: page.timezone }),
304
+ ...(page.ignoredFields.length === 0 ? {} : { ignored_fields: page.ignoredFields }),
305
+ }
306
+ })
307
+ }
308
+
309
+ // ---- feishu_wiki_base_edit ---------------------------------------------
310
+
311
+ function json(value: unknown): string {
312
+ return preview(JSON.stringify(value) ?? 'null', 120)
313
+ }
314
+
315
+ function cardLines(operations: readonly WikiBaseOperation[], tableName: string): string[] {
316
+ const lines = operations.map((operation, index) => {
317
+ const n = `${index + 1}.`
318
+ switch (operation.command) {
319
+ case 'create': {
320
+ const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map(record => ` ${json(record)}`)
321
+ const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : []
322
+ return [`${n} 新增 ${operation.records.length} 条记录`, ...sample, ...more].join('\n')
323
+ }
324
+ case 'update': {
325
+ const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map(record => ` ${preview(record.record_id, 40)}:${json(record.fields)}`)
326
+ const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : []
327
+ return [`${n} 修改 ${operation.records.length} 条记录`, ...sample, ...more].join('\n')
328
+ }
329
+ case 'delete':
330
+ return `${n} ⚠️ 删除 ${operation.record_ids.length} 条记录:${preview(operation.record_ids.join('、'), 160)}`
331
+ }
332
+ })
333
+ return [`**数据表**:${preview(tableName, 60)}`, `**将执行 ${operations.length} 个操作**`, ...lines]
334
+ }
335
+
336
+ async function edit(kit: WikiToolKit, sessionId: string, rawArgs: Record<string, unknown>, signal: AbortSignal): Promise<object> {
337
+ const args = parseArgs(WIKI_BASE_EDIT_TOOL_NAME, wikiBaseEditArgsSchema, rawArgs)
338
+ const gate = kit.gate(sessionId, WIKI_BASE_EDIT_TOOL_NAME)
339
+ if (!gate.ok) return gate
340
+ return await guarded<WikiBaseEditValue>(kit, WIKI_BASE_EDIT_TOOL_NAME, async () => {
341
+ const target = await baseTarget(kit, gate.context, args.doc)
342
+ if ('ok' in target) return target
343
+ const { node, bases } = target
344
+ if (writableOf(kit, target) !== 'all') {
345
+ const changing = args.operations.find(operation => operation.command !== 'create')
346
+ if (changing !== undefined) {
347
+ return failure('NOT_OWNED', `这个多维表格不是本应用创建的,只能新增记录,不能${changing.command === 'update' ? '修改' : '删除'}已有记录(所有者:${node.owner ?? '未知'})`)
348
+ }
349
+ }
350
+ const table = pickTable(await bases.listTables(node.objToken), args.table)
351
+ if (typeof table === 'string') return failure('INVALID_OPERATION', table)
352
+ const denied = await kit.consent(sessionId, target.context, 'edit', {
353
+ title: node.title,
354
+ url: target.url,
355
+ location: await kit.locationOf(target.context.api, node),
356
+ details: cardLines(args.operations, table.name),
357
+ }, signal)
358
+ if (denied !== undefined) return denied
359
+ const created: string[] = []
360
+ const ignored: string[] = []
361
+ let updated = 0
362
+ let deleted = 0
363
+ for (const [index, operation] of args.operations.entries()) {
364
+ if (signal.aborted) return { ...failure('ABORTED', `已取消;前 ${index} 个操作已执行`), applied: index }
365
+ try {
366
+ switch (operation.command) {
367
+ case 'create':
368
+ created.push(...await bases.createRecords(node.objToken, table.tableId, operation.records as BaseRecordFields[]))
369
+ break
370
+ case 'update':
371
+ ignored.push(...await bases.updateRecords(
372
+ node.objToken,
373
+ table.tableId,
374
+ Object.fromEntries(operation.records.map(record => [record.record_id, record.fields as BaseRecordFields])),
375
+ ))
376
+ updated += operation.records.length
377
+ break
378
+ case 'delete':
379
+ await bases.deleteRecords(node.objToken, table.tableId, operation.record_ids)
380
+ deleted += operation.record_ids.length
381
+ break
382
+ }
383
+ } catch (error) {
384
+ const reason = failureOf(error).reason
385
+ return { ...failure('EDIT_FAILED', `第 ${index + 1} 个操作失败:${reason};前 ${index} 个操作已执行`), applied: index }
386
+ }
387
+ }
388
+ return {
389
+ ok: true,
390
+ title: node.title,
391
+ node_token: node.nodeToken,
392
+ url: target.url,
393
+ table_id: table.tableId,
394
+ applied: args.operations.length,
395
+ created_record_ids: created,
396
+ updated,
397
+ deleted,
398
+ ignored_fields: [...new Set(ignored)],
399
+ }
400
+ })
401
+ }
402
+
403
+ // ---- Specs --------------------------------------------------------------
404
+
405
+ /** The Base tool family. */
406
+ export const BASE_TOOL_SPECS: readonly WikiToolSpec[] = [
407
+ {
408
+ name: WIKI_BASE_READ_TOOL_NAME,
409
+ access: 'read',
410
+ description: READ_DESCRIPTION,
411
+ parameters: READ_PARAMETERS,
412
+ execute: read,
413
+ presentCall: args => `读取知识库多维表格:${display(args.doc)}`,
414
+ },
415
+ {
416
+ name: WIKI_BASE_EDIT_TOOL_NAME,
417
+ access: 'write',
418
+ description: EDIT_DESCRIPTION,
419
+ parameters: EDIT_PARAMETERS,
420
+ execute: edit,
421
+ presentCall: args => `修改知识库多维表格:${display(args.doc)}`,
422
+ },
423
+ ]