@modesto_ma/dsh-channels 0.5.0-beta.2 → 0.5.0-beta.3
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 +6 -6
- package/docs/attachment-pipeline-v2.md +2 -2
- package/docs/feishu-onboarding.md +4 -3
- package/lib/index.js +1606 -248
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +26 -14
- package/src/feishu/scopes.ts +13 -1
- package/src/feishu/wiki/a1.ts +97 -0
- package/src/feishu/wiki/api.ts +21 -6
- package/src/feishu/wiki/base-api.ts +267 -0
- package/src/feishu/wiki/base-tools.ts +423 -0
- package/src/feishu/wiki/shared.ts +284 -0
- package/src/feishu/wiki/sheet-api.ts +250 -0
- package/src/feishu/wiki/sheet-tools.ts +493 -0
- package/src/feishu/wiki/tools.ts +102 -189
- package/src/feishu-channel.ts +14 -1
- package/src/settings/surface.ts +6 -6
package/lib/index.js
CHANGED
|
@@ -3400,26 +3400,72 @@ const SEND_FILE_REQUIREMENT = {
|
|
|
3400
3400
|
level: "optional",
|
|
3401
3401
|
purpose: "上传图片/文件,让模型用 feishu_send_file 把生成的文件发给用户"
|
|
3402
3402
|
};
|
|
3403
|
-
/**
|
|
3404
|
-
const
|
|
3405
|
-
anyOf: [
|
|
3403
|
+
/** Spreadsheet and Base read needs, shared by the wiki read and write sets. */
|
|
3404
|
+
const SHEET_BASE_READ_REQUIREMENTS = [{
|
|
3405
|
+
anyOf: [
|
|
3406
|
+
"sheets:spreadsheet",
|
|
3407
|
+
"sheets:spreadsheet:readonly",
|
|
3408
|
+
"sheets:spreadsheet:read"
|
|
3409
|
+
],
|
|
3406
3410
|
level: "optional",
|
|
3407
|
-
purpose: "
|
|
3411
|
+
purpose: "读取知识库电子表格(feishu_wiki_sheet_read)"
|
|
3408
3412
|
}, {
|
|
3409
|
-
anyOf: [
|
|
3413
|
+
anyOf: [
|
|
3414
|
+
"bitable:app",
|
|
3415
|
+
"bitable:app:readonly",
|
|
3416
|
+
"base:record:read"
|
|
3417
|
+
],
|
|
3410
3418
|
level: "optional",
|
|
3411
|
-
purpose: "
|
|
3419
|
+
purpose: "读取知识库多维表格(feishu_wiki_base_read)"
|
|
3412
3420
|
}];
|
|
3421
|
+
/** Wiki read needs (`feishu_wiki_list` / `feishu_wiki_read` and the sheet/base read tools). */
|
|
3422
|
+
const WIKI_READ_REQUIREMENTS = [
|
|
3423
|
+
{
|
|
3424
|
+
anyOf: ["wiki:wiki", "wiki:wiki:readonly"],
|
|
3425
|
+
level: "optional",
|
|
3426
|
+
purpose: "浏览知识库节点(feishu_wiki_list / feishu_wiki_read)"
|
|
3427
|
+
},
|
|
3428
|
+
{
|
|
3429
|
+
anyOf: ["docx:document", "docx:document:readonly"],
|
|
3430
|
+
level: "optional",
|
|
3431
|
+
purpose: "读取知识库中的文档内容(feishu_wiki_read)"
|
|
3432
|
+
},
|
|
3433
|
+
...SHEET_BASE_READ_REQUIREMENTS
|
|
3434
|
+
];
|
|
3413
3435
|
/** Wiki write needs (`feishu_wiki_create` / `feishu_wiki_edit`). */
|
|
3414
|
-
const WIKI_WRITE_REQUIREMENTS = [
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3436
|
+
const WIKI_WRITE_REQUIREMENTS = [
|
|
3437
|
+
{
|
|
3438
|
+
anyOf: ["wiki:wiki"],
|
|
3439
|
+
level: "optional",
|
|
3440
|
+
purpose: "在知识库中新建节点(feishu_wiki_create)"
|
|
3441
|
+
},
|
|
3442
|
+
{
|
|
3443
|
+
anyOf: ["docx:document"],
|
|
3444
|
+
level: "optional",
|
|
3445
|
+
purpose: "写入与修改知识库文档内容(feishu_wiki_create / feishu_wiki_edit)"
|
|
3446
|
+
},
|
|
3447
|
+
{
|
|
3448
|
+
anyOf: ["sheets:spreadsheet", "sheets:spreadsheet:write_only"],
|
|
3449
|
+
level: "optional",
|
|
3450
|
+
purpose: "写入知识库电子表格(feishu_wiki_sheet_edit)"
|
|
3451
|
+
},
|
|
3452
|
+
{
|
|
3453
|
+
anyOf: ["bitable:app", "base:record:create"],
|
|
3454
|
+
level: "optional",
|
|
3455
|
+
purpose: "新增知识库多维表格记录(feishu_wiki_base_edit)"
|
|
3456
|
+
},
|
|
3457
|
+
{
|
|
3458
|
+
anyOf: ["bitable:app", "base:record:update"],
|
|
3459
|
+
level: "optional",
|
|
3460
|
+
purpose: "修改本应用创建的多维表格中的记录(feishu_wiki_base_edit)"
|
|
3461
|
+
},
|
|
3462
|
+
{
|
|
3463
|
+
anyOf: ["bitable:app", "base:record:delete"],
|
|
3464
|
+
level: "optional",
|
|
3465
|
+
purpose: "删除本应用创建的多维表格中的记录(feishu_wiki_base_edit)"
|
|
3466
|
+
},
|
|
3467
|
+
...SHEET_BASE_READ_REQUIREMENTS
|
|
3468
|
+
];
|
|
3423
3469
|
/** Group receive needs while ordinary group messages require an @ mention. */
|
|
3424
3470
|
const MENTION_GROUP_REQUIREMENTS = [{
|
|
3425
3471
|
anyOf: ["im:message.group_at_msg:readonly", "im:message.group_msg"],
|
|
@@ -3548,6 +3594,14 @@ function thrownError(error) {
|
|
|
3548
3594
|
if (code === void 0 && msg === void 0) return new FeishuApiError(error instanceof Error ? error.message : String(error));
|
|
3549
3595
|
return new FeishuApiError([code === void 0 ? void 0 : `code=${code}`, msg].filter(Boolean).join(" "), code);
|
|
3550
3596
|
}
|
|
3597
|
+
/**
|
|
3598
|
+
* One Feishu OpenAPI call: unwraps `data` and maps a transport error or a
|
|
3599
|
+
* non-zero `code` to {@link FeishuApiError}.
|
|
3600
|
+
*
|
|
3601
|
+
* @param request The SDK's generic request.
|
|
3602
|
+
* @param input Method, URL, query, and body.
|
|
3603
|
+
* @returns The response `data`.
|
|
3604
|
+
*/
|
|
3551
3605
|
async function call(request, input) {
|
|
3552
3606
|
let response;
|
|
3553
3607
|
try {
|
|
@@ -3574,6 +3628,12 @@ function wikiNode(raw, where) {
|
|
|
3574
3628
|
...raw.url === void 0 || raw.url === "" ? {} : { url: raw.url }
|
|
3575
3629
|
};
|
|
3576
3630
|
}
|
|
3631
|
+
/**
|
|
3632
|
+
* Encode one URL path segment.
|
|
3633
|
+
*
|
|
3634
|
+
* @param value Raw segment.
|
|
3635
|
+
* @returns The encoded segment.
|
|
3636
|
+
*/
|
|
3577
3637
|
function segment(value) {
|
|
3578
3638
|
return encodeURIComponent(value);
|
|
3579
3639
|
}
|
|
@@ -3618,13 +3678,13 @@ function createWikiApi(request) {
|
|
|
3618
3678
|
...data.has_more === true && data.page_token !== void 0 && data.page_token !== "" ? { pageToken: data.page_token } : {}
|
|
3619
3679
|
};
|
|
3620
3680
|
},
|
|
3621
|
-
async
|
|
3681
|
+
async createNode(spaceId, parentNodeToken, title, objType) {
|
|
3622
3682
|
const url = `/open-apis/wiki/v2/spaces/${segment(spaceId)}/nodes`;
|
|
3623
3683
|
return wikiNode((await call(request, {
|
|
3624
3684
|
method: "POST",
|
|
3625
3685
|
url,
|
|
3626
3686
|
data: {
|
|
3627
|
-
obj_type:
|
|
3687
|
+
obj_type: objType,
|
|
3628
3688
|
node_type: "origin",
|
|
3629
3689
|
title,
|
|
3630
3690
|
...parentNodeToken === void 0 ? {} : { parent_node_token: parentNodeToken }
|
|
@@ -3695,6 +3755,292 @@ function createWikiApi(request) {
|
|
|
3695
3755
|
};
|
|
3696
3756
|
}
|
|
3697
3757
|
//#endregion
|
|
3758
|
+
//#region src/feishu/wiki/base-api.ts
|
|
3759
|
+
/**
|
|
3760
|
+
* Feishu Base (多维表格) calls used by the wiki base tools, made with the app
|
|
3761
|
+
* identity (tenant access token).
|
|
3762
|
+
*
|
|
3763
|
+
* Everything goes through `base/v3/bases/:base_token/...` (the legacy
|
|
3764
|
+
* `bitable/v1` API is not used). Record reads return a column matrix that
|
|
3765
|
+
* this module turns into one field map per record; record writes take flat
|
|
3766
|
+
* maps keyed by field name or field id. Writes carry no precondition, are
|
|
3767
|
+
* applied asynchronously (a read straight after a write may not show it), and
|
|
3768
|
+
* must not run in parallel on one table.
|
|
3769
|
+
*
|
|
3770
|
+
* @module dsh-channels/feishu/wiki/base-api
|
|
3771
|
+
*/
|
|
3772
|
+
function stringArray(value) {
|
|
3773
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
3774
|
+
}
|
|
3775
|
+
function ignoredFieldsOf(value) {
|
|
3776
|
+
if (!Array.isArray(value)) return [];
|
|
3777
|
+
return value.flatMap((item) => {
|
|
3778
|
+
if (typeof item !== "object" || item === null) return [];
|
|
3779
|
+
const { name, id, reason } = item;
|
|
3780
|
+
const label = typeof name === "string" && name !== "" ? name : typeof id === "string" ? id : "?";
|
|
3781
|
+
return [typeof reason === "string" && reason !== "" ? `${label}: ${reason}` : label];
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
function recordPage(raw, where) {
|
|
3785
|
+
const names = stringArray(raw.fields) ?? [];
|
|
3786
|
+
const ids = stringArray(raw.record_id_list) ?? [];
|
|
3787
|
+
const rows = Array.isArray(raw.data) ? raw.data : [];
|
|
3788
|
+
if (rows.length !== ids.length) throw new FeishuApiError(`${where}: record ids and rows differ in length`);
|
|
3789
|
+
return {
|
|
3790
|
+
records: ids.map((recordId, index) => {
|
|
3791
|
+
const row = rows[index];
|
|
3792
|
+
if (!Array.isArray(row) || row.length !== names.length) throw new FeishuApiError(`${where}: row ${index} does not match the field list`);
|
|
3793
|
+
return {
|
|
3794
|
+
recordId,
|
|
3795
|
+
fields: Object.fromEntries(names.map((name, column) => [name, row[column] ?? null]))
|
|
3796
|
+
};
|
|
3797
|
+
}),
|
|
3798
|
+
hasMore: raw.has_more === true,
|
|
3799
|
+
...typeof raw.rev === "number" ? { rev: raw.rev } : {},
|
|
3800
|
+
...typeof raw.timezone === "string" && raw.timezone !== "" ? { timezone: raw.timezone } : {},
|
|
3801
|
+
ignoredFields: ignoredFieldsOf(raw.ignored_fields)
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
/**
|
|
3805
|
+
* {@link BaseApi} over the SDK's generic authenticated request.
|
|
3806
|
+
*
|
|
3807
|
+
* @param request `Client.request` of the connected SDK client.
|
|
3808
|
+
* @returns The API.
|
|
3809
|
+
*/
|
|
3810
|
+
function createBaseApi(request) {
|
|
3811
|
+
const tablePath = (baseToken, tableId) => `/open-apis/base/v3/bases/${segment(baseToken)}/tables/${segment(tableId)}`;
|
|
3812
|
+
return {
|
|
3813
|
+
async listTables(baseToken) {
|
|
3814
|
+
const data = await call(request, {
|
|
3815
|
+
method: "GET",
|
|
3816
|
+
url: `/open-apis/base/v3/bases/${segment(baseToken)}/tables`,
|
|
3817
|
+
params: {
|
|
3818
|
+
offset: 0,
|
|
3819
|
+
limit: 300
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
return (Array.isArray(data.tables) ? data.tables : Array.isArray(data.items) ? data.items : []).flatMap((item) => {
|
|
3823
|
+
if (typeof item !== "object" || item === null) return [];
|
|
3824
|
+
const { id, table_id, name, table_name } = item;
|
|
3825
|
+
const tableId = typeof id === "string" ? id : typeof table_id === "string" ? table_id : void 0;
|
|
3826
|
+
if (tableId === void 0) return [];
|
|
3827
|
+
return [{
|
|
3828
|
+
tableId,
|
|
3829
|
+
name: typeof name === "string" ? name : typeof table_name === "string" ? table_name : ""
|
|
3830
|
+
}];
|
|
3831
|
+
});
|
|
3832
|
+
},
|
|
3833
|
+
async listFields(baseToken, tableId) {
|
|
3834
|
+
const data = await call(request, {
|
|
3835
|
+
method: "GET",
|
|
3836
|
+
url: `${tablePath(baseToken, tableId)}/fields`,
|
|
3837
|
+
params: {
|
|
3838
|
+
offset: 0,
|
|
3839
|
+
limit: 300
|
|
3840
|
+
}
|
|
3841
|
+
});
|
|
3842
|
+
return (Array.isArray(data.fields) ? data.fields : Array.isArray(data.items) ? data.items : []).flatMap((item) => {
|
|
3843
|
+
if (typeof item !== "object" || item === null) return [];
|
|
3844
|
+
const { id, field_id, name, field_name, type, ...properties } = item;
|
|
3845
|
+
const fieldId = typeof id === "string" ? id : typeof field_id === "string" ? field_id : void 0;
|
|
3846
|
+
if (fieldId === void 0) return [];
|
|
3847
|
+
return [{
|
|
3848
|
+
fieldId,
|
|
3849
|
+
name: typeof name === "string" ? name : typeof field_name === "string" ? field_name : "",
|
|
3850
|
+
type: typeof type === "string" ? type : "not_support",
|
|
3851
|
+
properties
|
|
3852
|
+
}];
|
|
3853
|
+
});
|
|
3854
|
+
},
|
|
3855
|
+
async listRecords(baseToken, tableId, query) {
|
|
3856
|
+
const url = `${tablePath(baseToken, tableId)}/records`;
|
|
3857
|
+
const params = new URLSearchParams({
|
|
3858
|
+
offset: String(query.offset),
|
|
3859
|
+
limit: String(query.limit)
|
|
3860
|
+
});
|
|
3861
|
+
for (const field of query.fields ?? []) params.append("field_id", field);
|
|
3862
|
+
if (query.viewId !== void 0) params.set("view_id", query.viewId);
|
|
3863
|
+
if (query.filter !== void 0) params.set("filter", JSON.stringify(query.filter));
|
|
3864
|
+
if (query.sort !== void 0) params.set("sort", JSON.stringify(query.sort));
|
|
3865
|
+
return recordPage(await call(request, {
|
|
3866
|
+
method: "GET",
|
|
3867
|
+
url: `${url}?${params.toString()}`
|
|
3868
|
+
}), url);
|
|
3869
|
+
},
|
|
3870
|
+
async searchRecords(baseToken, tableId, search) {
|
|
3871
|
+
const url = `${tablePath(baseToken, tableId)}/records/search`;
|
|
3872
|
+
return recordPage(await call(request, {
|
|
3873
|
+
method: "POST",
|
|
3874
|
+
url,
|
|
3875
|
+
data: {
|
|
3876
|
+
keyword: search.keyword,
|
|
3877
|
+
search_fields: search.searchFields,
|
|
3878
|
+
...search.fields === void 0 ? {} : { select_fields: search.fields },
|
|
3879
|
+
...search.viewId === void 0 ? {} : { view_id: search.viewId },
|
|
3880
|
+
...search.filter === void 0 ? {} : { filter: search.filter },
|
|
3881
|
+
...search.sort === void 0 ? {} : { sort: search.sort },
|
|
3882
|
+
offset: search.offset,
|
|
3883
|
+
limit: search.limit
|
|
3884
|
+
}
|
|
3885
|
+
}), url);
|
|
3886
|
+
},
|
|
3887
|
+
async createRecords(baseToken, tableId, records) {
|
|
3888
|
+
return stringArray((await call(request, {
|
|
3889
|
+
method: "POST",
|
|
3890
|
+
url: `${tablePath(baseToken, tableId)}/records/batch_create`,
|
|
3891
|
+
data: { create_records: records }
|
|
3892
|
+
})).record_id_list) ?? [];
|
|
3893
|
+
},
|
|
3894
|
+
async updateRecords(baseToken, tableId, updates) {
|
|
3895
|
+
return ignoredFieldsOf((await call(request, {
|
|
3896
|
+
method: "POST",
|
|
3897
|
+
url: `${tablePath(baseToken, tableId)}/records/batch_update`,
|
|
3898
|
+
data: { update_records: updates }
|
|
3899
|
+
})).ignored_fields);
|
|
3900
|
+
},
|
|
3901
|
+
async deleteRecords(baseToken, tableId, recordIds) {
|
|
3902
|
+
await call(request, {
|
|
3903
|
+
method: "POST",
|
|
3904
|
+
url: `${tablePath(baseToken, tableId)}/records/batch_delete`,
|
|
3905
|
+
data: { record_id_list: recordIds }
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
//#endregion
|
|
3911
|
+
//#region src/feishu/wiki/sheet-api.ts
|
|
3912
|
+
/**
|
|
3913
|
+
* Feishu spreadsheet (电子表格) calls used by the wiki sheet tools, made with
|
|
3914
|
+
* the app identity (tenant access token).
|
|
3915
|
+
*
|
|
3916
|
+
* Everything goes through the `sheet_ai/v2` tool endpoints: `POST
|
|
3917
|
+
* spreadsheets/:token/tools/invoke_read` (or `invoke_write`) with body
|
|
3918
|
+
* `{tool_name, input}`, where `input` is a JSON string that also carries
|
|
3919
|
+
* `excel_id`, and the result is the JSON string `data.output`. Read tools sent
|
|
3920
|
+
* to `invoke_write` (or the reverse) are rejected by the gateway. Writes carry
|
|
3921
|
+
* no base revision and no idempotency key; callers check the workbook
|
|
3922
|
+
* revision before writing.
|
|
3923
|
+
*
|
|
3924
|
+
* @module dsh-channels/feishu/wiki/sheet-api
|
|
3925
|
+
*/
|
|
3926
|
+
function invokePath(token, kind) {
|
|
3927
|
+
return `/open-apis/sheet_ai/v2/spreadsheets/${segment(token)}/tools/invoke_${kind}`;
|
|
3928
|
+
}
|
|
3929
|
+
async function invoke(request, token, kind, toolName, input) {
|
|
3930
|
+
const data = await call(request, {
|
|
3931
|
+
method: "POST",
|
|
3932
|
+
url: invokePath(token, kind),
|
|
3933
|
+
data: {
|
|
3934
|
+
tool_name: toolName,
|
|
3935
|
+
input: JSON.stringify({
|
|
3936
|
+
excel_id: token,
|
|
3937
|
+
...input
|
|
3938
|
+
})
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
if (typeof data.output !== "string" || data.output === "") return {};
|
|
3942
|
+
let output;
|
|
3943
|
+
try {
|
|
3944
|
+
output = JSON.parse(data.output);
|
|
3945
|
+
} catch (error) {
|
|
3946
|
+
throw new FeishuApiError(`${toolName}: output is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
3947
|
+
}
|
|
3948
|
+
if (Array.isArray(output)) return { sheets: output };
|
|
3949
|
+
if (typeof output !== "object" || output === null) throw new FeishuApiError(`${toolName}: output is not an object`);
|
|
3950
|
+
return output;
|
|
3951
|
+
}
|
|
3952
|
+
function optionalString(value) {
|
|
3953
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
3954
|
+
}
|
|
3955
|
+
function optionalNumber(value) {
|
|
3956
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3957
|
+
}
|
|
3958
|
+
function sheetInfo(raw, position) {
|
|
3959
|
+
const sheetId = optionalString(raw.sheet_id);
|
|
3960
|
+
if (sheetId === void 0) return void 0;
|
|
3961
|
+
const rowCount = optionalNumber(raw.row_count);
|
|
3962
|
+
const columnCount = optionalNumber(raw.column_count);
|
|
3963
|
+
return {
|
|
3964
|
+
sheetId,
|
|
3965
|
+
title: optionalString(raw.title) ?? optionalString(raw.sheet_name) ?? "",
|
|
3966
|
+
index: optionalNumber(raw.index) ?? position,
|
|
3967
|
+
resourceType: optionalString(raw.resource_type) ?? "sheet",
|
|
3968
|
+
hidden: raw.hidden === true || raw.is_hidden === true,
|
|
3969
|
+
...rowCount === void 0 ? {} : { rowCount },
|
|
3970
|
+
...columnCount === void 0 ? {} : { columnCount }
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3973
|
+
function cellOf(value) {
|
|
3974
|
+
if (value === null) return {};
|
|
3975
|
+
if (typeof value === "string" && value.startsWith("=")) return { formula: value };
|
|
3976
|
+
return { value };
|
|
3977
|
+
}
|
|
3978
|
+
/**
|
|
3979
|
+
* {@link SheetApi} over the SDK's generic authenticated request.
|
|
3980
|
+
*
|
|
3981
|
+
* @param request `Client.request` of the connected SDK client.
|
|
3982
|
+
* @returns The API.
|
|
3983
|
+
*/
|
|
3984
|
+
function formulaError(raw) {
|
|
3985
|
+
if (typeof raw !== "object" || raw === null) return [];
|
|
3986
|
+
const entry = raw;
|
|
3987
|
+
const cell = optionalString(entry.cell);
|
|
3988
|
+
if (cell === void 0) return [];
|
|
3989
|
+
const formula = optionalString(entry.formula);
|
|
3990
|
+
const errorType = optionalString(entry.error_type);
|
|
3991
|
+
const detail = optionalString(entry.detail);
|
|
3992
|
+
return [{
|
|
3993
|
+
cell,
|
|
3994
|
+
...formula === void 0 ? {} : { formula },
|
|
3995
|
+
...errorType === void 0 ? {} : { errorType },
|
|
3996
|
+
...detail === void 0 ? {} : { detail }
|
|
3997
|
+
}];
|
|
3998
|
+
}
|
|
3999
|
+
function createSheetApi(request) {
|
|
4000
|
+
return {
|
|
4001
|
+
async workbook(token) {
|
|
4002
|
+
const output = await invoke(request, token, "read", "get_workbook_structure", {});
|
|
4003
|
+
const sheets = (Array.isArray(output.sheets) ? output.sheets : []).flatMap((raw, position) => {
|
|
4004
|
+
const info = typeof raw === "object" && raw !== null ? sheetInfo(raw, position) : void 0;
|
|
4005
|
+
return info === void 0 ? [] : [info];
|
|
4006
|
+
});
|
|
4007
|
+
const revision = optionalNumber(output.revision);
|
|
4008
|
+
return {
|
|
4009
|
+
...revision === void 0 ? {} : { revision },
|
|
4010
|
+
sheets
|
|
4011
|
+
};
|
|
4012
|
+
},
|
|
4013
|
+
async readCsv(token, read) {
|
|
4014
|
+
const output = await invoke(request, token, "read", "get_range_as_csv", {
|
|
4015
|
+
sheet_id: read.sheetId,
|
|
4016
|
+
range: read.range,
|
|
4017
|
+
max_chars: read.maxChars,
|
|
4018
|
+
max_rows: 1e9,
|
|
4019
|
+
skip_hidden: false
|
|
4020
|
+
});
|
|
4021
|
+
const actualRange = optionalString(output.actual_range);
|
|
4022
|
+
const currentRegion = optionalString(output.current_region);
|
|
4023
|
+
const warning = optionalString(output.warning_message);
|
|
4024
|
+
return {
|
|
4025
|
+
csv: typeof output.annotated_csv === "string" ? output.annotated_csv : "",
|
|
4026
|
+
...actualRange === void 0 ? {} : { actualRange },
|
|
4027
|
+
...currentRegion === void 0 ? {} : { currentRegion },
|
|
4028
|
+
hasMore: output.has_more === true,
|
|
4029
|
+
...warning === void 0 ? {} : { warning }
|
|
4030
|
+
};
|
|
4031
|
+
},
|
|
4032
|
+
async writeCells(token, write) {
|
|
4033
|
+
const output = await invoke(request, token, "write", "set_cell_range", {
|
|
4034
|
+
sheet_id: write.sheetId,
|
|
4035
|
+
range: write.range,
|
|
4036
|
+
cells: write.rows.map((row) => row.map(cellOf)),
|
|
4037
|
+
...write.allowOverwrite === false ? { allow_overwrite: false } : {}
|
|
4038
|
+
});
|
|
4039
|
+
return { formulaErrors: (Array.isArray(output.formula_errors) ? output.formula_errors : []).flatMap(formulaError) };
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
//#endregion
|
|
3698
4044
|
//#region src/feishu-channel.ts
|
|
3699
4045
|
/**
|
|
3700
4046
|
* Thin wrapper over the official SDK's `createLarkChannel` high-level module.
|
|
@@ -3888,6 +4234,8 @@ var LarkChannelAdapter = class {
|
|
|
3888
4234
|
dmMode;
|
|
3889
4235
|
slashCommands;
|
|
3890
4236
|
wiki;
|
|
4237
|
+
sheets;
|
|
4238
|
+
bases;
|
|
3891
4239
|
constructor(options) {
|
|
3892
4240
|
this.channel = createLarkChannel({
|
|
3893
4241
|
appId: options.appId,
|
|
@@ -3906,7 +4254,10 @@ var LarkChannelAdapter = class {
|
|
|
3906
4254
|
this.dmMode = options.dmMode;
|
|
3907
4255
|
const rawClient = this.channel.rawClient;
|
|
3908
4256
|
this.slashCommands = slashCommandApi(async (input) => await rawClient.request(input));
|
|
3909
|
-
|
|
4257
|
+
const wikiRequest = async (input) => await rawClient.request(input);
|
|
4258
|
+
this.wiki = createWikiApi(wikiRequest);
|
|
4259
|
+
this.sheets = createSheetApi(wikiRequest);
|
|
4260
|
+
this.bases = createBaseApi(wikiRequest);
|
|
3910
4261
|
}
|
|
3911
4262
|
onMessage(handler) {
|
|
3912
4263
|
return this.channel.on("message", (message) => {
|
|
@@ -9921,6 +10272,112 @@ var WikiConsent = class {
|
|
|
9921
10272
|
})) entry.resolve(outcome);
|
|
9922
10273
|
}
|
|
9923
10274
|
};
|
|
10275
|
+
const FAILURE_REASONS = {
|
|
10276
|
+
TOOL_DISABLED: "知识库工具已在插件配置中关闭",
|
|
10277
|
+
ROUTE_NOT_FOUND: "当前会话没有绑定飞书话题,无法确定授权人",
|
|
10278
|
+
NOT_AUTHORIZED: "当前话题的发起人不在知识库白名单中;请管理员发送 /auth wiki add @该用户 后重试",
|
|
10279
|
+
CHANNEL_UNAVAILABLE: "飞书连接当前不可用,请稍后重试",
|
|
10280
|
+
INVALID_REFERENCE: "无法识别的知识库引用:请传知识库节点链接 https://<租户>.feishu.cn/wiki/<node_token>、node_token,或知识空间链接 https://<租户>.feishu.cn/wiki/space/<space_id>",
|
|
10281
|
+
INVALID_OPERATION: "操作参数不正确",
|
|
10282
|
+
OUTSIDE_ROOTS: "该位置不在插件配置的知识库范围(wikiRoots)内,不能访问",
|
|
10283
|
+
NOT_FOUND: "知识库节点不存在或已被删除",
|
|
10284
|
+
NO_ACCESS: "机器人对该知识库节点没有访问权限;请知识库管理员把应用(或包含机器人的群)加为该空间成员",
|
|
10285
|
+
UNSUPPORTED_TYPE: "这个工具不支持该类型的节点:飞书文档(docx)用 feishu_wiki_read / feishu_wiki_edit,电子表格(sheet)用 feishu_wiki_sheet_read / feishu_wiki_sheet_edit,多维表格(bitable)用 feishu_wiki_base_read / feishu_wiki_base_edit;其他类型暂不支持",
|
|
10286
|
+
NOT_OWNED: "只能修改由本应用创建(所有者是本应用)的文档",
|
|
10287
|
+
REVISION_CONFLICT: "文档在你读取之后被修改过;请重新用 feishu_wiki_read 读取最新内容,基于新的 revision_id 再修改",
|
|
10288
|
+
CONSENT_DENIED: "话题发起人拒绝了这次知识库操作",
|
|
10289
|
+
CONSENT_EXPIRED: "话题发起人没有在授权卡有效期内确认,已自动拒绝",
|
|
10290
|
+
CONSENT_UNAVAILABLE: "授权卡发送失败,无法征得话题发起人同意,已取消操作",
|
|
10291
|
+
PERMISSION_DENIED: "飞书拒绝调用:应用缺少知识库或文档权限(wiki:wiki、docx:document),请管理员在开发者后台开通并发布版本",
|
|
10292
|
+
CONTENT_FAILED: "文档已创建,但写入内容失败",
|
|
10293
|
+
EDIT_FAILED: "修改失败",
|
|
10294
|
+
EDIT_PARTIAL: "飞书报告操作只部分生效;已停止后续操作,请用 feishu_wiki_read 读取最新内容核验后再继续",
|
|
10295
|
+
API_FAILED: "飞书接口调用失败",
|
|
10296
|
+
ABORTED: "已取消"
|
|
10297
|
+
};
|
|
10298
|
+
function failure(code, reason) {
|
|
10299
|
+
return {
|
|
10300
|
+
ok: false,
|
|
10301
|
+
code,
|
|
10302
|
+
reason: reason ?? FAILURE_REASONS[code]
|
|
10303
|
+
};
|
|
10304
|
+
}
|
|
10305
|
+
function parseArgs(tool, schema, rawArgs) {
|
|
10306
|
+
const parsed = schema.safeParse(rawArgs);
|
|
10307
|
+
if (parsed.success) return parsed.data;
|
|
10308
|
+
const issue = parsed.error.issues[0];
|
|
10309
|
+
const where = issue === void 0 || issue.path.length === 0 ? "arguments" : issue.path.join(".");
|
|
10310
|
+
throw new Error(`${tool}: invalid ${where}: ${issue?.message ?? "invalid arguments"}`);
|
|
10311
|
+
}
|
|
10312
|
+
/** Run one gated call body, mapping thrown Feishu errors to failures. */
|
|
10313
|
+
async function guarded$1(deps, tool, body) {
|
|
10314
|
+
try {
|
|
10315
|
+
return await body();
|
|
10316
|
+
} catch (error) {
|
|
10317
|
+
const mapped = failureOf(error);
|
|
10318
|
+
if (mapped.code === "API_FAILED") deps.warn("dsh-channels: %s failed: %s", tool, mapped.reason);
|
|
10319
|
+
return mapped;
|
|
10320
|
+
}
|
|
10321
|
+
}
|
|
10322
|
+
/** Feishu codes: node missing/deleted, no resource access, missing app scope. */
|
|
10323
|
+
const NOT_FOUND_CODES = /* @__PURE__ */ new Set([
|
|
10324
|
+
131005,
|
|
10325
|
+
131012,
|
|
10326
|
+
131013,
|
|
10327
|
+
131014,
|
|
10328
|
+
131016
|
|
10329
|
+
]);
|
|
10330
|
+
const NO_ACCESS_CODES = /* @__PURE__ */ new Set([
|
|
10331
|
+
131006,
|
|
10332
|
+
1770032,
|
|
10333
|
+
1770002
|
|
10334
|
+
]);
|
|
10335
|
+
const PERMISSION_CODES = /* @__PURE__ */ new Set([99991672, 99991679]);
|
|
10336
|
+
function failureOf(error) {
|
|
10337
|
+
const detail = describeError(error);
|
|
10338
|
+
if (error instanceof FeishuApiError && error.code !== void 0) {
|
|
10339
|
+
if (NOT_FOUND_CODES.has(error.code)) return failure("NOT_FOUND", `${FAILURE_REASONS.NOT_FOUND}(${detail})`);
|
|
10340
|
+
if (NO_ACCESS_CODES.has(error.code)) return failure("NO_ACCESS", `${FAILURE_REASONS.NO_ACCESS}(${detail})`);
|
|
10341
|
+
if (PERMISSION_CODES.has(error.code)) return failure("PERMISSION_DENIED", `${FAILURE_REASONS.PERMISSION_DENIED}(${detail})`);
|
|
10342
|
+
}
|
|
10343
|
+
return failure("API_FAILED", `${FAILURE_REASONS.API_FAILED}:${detail}`);
|
|
10344
|
+
}
|
|
10345
|
+
function consentFailure(outcome) {
|
|
10346
|
+
switch (outcome) {
|
|
10347
|
+
case "allowed": return;
|
|
10348
|
+
case "denied": return failure("CONSENT_DENIED");
|
|
10349
|
+
case "expired": return failure("CONSENT_EXPIRED");
|
|
10350
|
+
case "cancelled": return failure("ABORTED");
|
|
10351
|
+
case "unavailable": return failure("CONSENT_UNAVAILABLE");
|
|
10352
|
+
}
|
|
10353
|
+
}
|
|
10354
|
+
/** Whether the app owns a node: Feishu reports the bot open_id as the owner. */
|
|
10355
|
+
function ownedByApp(node, botOpenId) {
|
|
10356
|
+
return botOpenId !== void 0 && botOpenId !== "" && node.owner === botOpenId;
|
|
10357
|
+
}
|
|
10358
|
+
/** A node's web link; falls back to the first root's origin. */
|
|
10359
|
+
function nodeUrl(roots, node) {
|
|
10360
|
+
if (node.url !== void 0) return node.url;
|
|
10361
|
+
const first = roots[0];
|
|
10362
|
+
return `${first === void 0 ? "https://feishu.cn" : new URL(first.link).origin}/wiki/${node.nodeToken}`;
|
|
10363
|
+
}
|
|
10364
|
+
/** One-line clipped preview with Markdown control characters neutralized. */
|
|
10365
|
+
function preview(value, max = 80) {
|
|
10366
|
+
const flat = value.replace(/\s+/gu, " ").trim().replace(/([[\]()*_`~<>])/gu, "\\$1");
|
|
10367
|
+
return flat.length <= max ? flat : `${flat.slice(0, max)}…`;
|
|
10368
|
+
}
|
|
10369
|
+
/** A fenced preview of the first characters of a content. */
|
|
10370
|
+
function previewBlock(content) {
|
|
10371
|
+
return `\`\`\`\n${(content.length <= 300 ? content : `${content.slice(0, 300)}\n…`).replace(/```/gu, "```")}\n\`\`\``;
|
|
10372
|
+
}
|
|
10373
|
+
function display(value) {
|
|
10374
|
+
return typeof value === "string" ? preview(value, 120) : "?";
|
|
10375
|
+
}
|
|
10376
|
+
/** Readable, bounded text for a thrown value. */
|
|
10377
|
+
function describeError(error) {
|
|
10378
|
+
const cleaned = (error instanceof FeishuApiError ? error.detail : error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f]+/gu, " ").trim();
|
|
10379
|
+
return cleaned.length <= 300 ? cleaned : `${cleaned.slice(0, 300)}…`;
|
|
10380
|
+
}
|
|
9924
10381
|
//#endregion
|
|
9925
10382
|
//#region src/feishu/wiki/links.ts
|
|
9926
10383
|
/**
|
|
@@ -9985,99 +10442,1040 @@ function parseWikiRootLink(link) {
|
|
|
9985
10442
|
}
|
|
9986
10443
|
};
|
|
9987
10444
|
return {
|
|
9988
|
-
ok: false,
|
|
9989
|
-
error: "not-a-wiki-link"
|
|
10445
|
+
ok: false,
|
|
10446
|
+
error: "not-a-wiki-link"
|
|
10447
|
+
};
|
|
10448
|
+
}
|
|
10449
|
+
/**
|
|
10450
|
+
* Parse every configured root. Callers validate the config first, so an
|
|
10451
|
+
* invalid entry here is a programming error.
|
|
10452
|
+
*
|
|
10453
|
+
* @param links Validated `wikiRoots` entries.
|
|
10454
|
+
* @returns The parsed roots, in configuration order.
|
|
10455
|
+
*/
|
|
10456
|
+
function parseWikiRoots(links) {
|
|
10457
|
+
return links.map((link) => {
|
|
10458
|
+
const parsed = parseWikiRootLink(link);
|
|
10459
|
+
if (!parsed.ok) throw new Error(`dsh-channels: invalid wikiRoots entry ${link}: ${WIKI_ROOT_LINK_ERRORS[parsed.error]}`);
|
|
10460
|
+
return parsed.root;
|
|
10461
|
+
});
|
|
10462
|
+
}
|
|
10463
|
+
/**
|
|
10464
|
+
* Parse a model-supplied reference: a wiki node link, a docx link, a space
|
|
10465
|
+
* link, or a bare node token.
|
|
10466
|
+
*
|
|
10467
|
+
* @param input Raw tool argument.
|
|
10468
|
+
* @returns The reference, or `undefined` when it names nothing recognizable.
|
|
10469
|
+
*/
|
|
10470
|
+
function parseWikiRef(input) {
|
|
10471
|
+
const trimmed = input.trim();
|
|
10472
|
+
if (TOKEN_PATTERN.test(trimmed)) return {
|
|
10473
|
+
kind: "node",
|
|
10474
|
+
token: trimmed
|
|
10475
|
+
};
|
|
10476
|
+
let url;
|
|
10477
|
+
try {
|
|
10478
|
+
url = new URL(trimmed);
|
|
10479
|
+
} catch {
|
|
10480
|
+
return;
|
|
10481
|
+
}
|
|
10482
|
+
if (url.protocol !== "https:") return void 0;
|
|
10483
|
+
const segments = url.pathname.split("/").filter((segment) => segment !== "");
|
|
10484
|
+
if (segments[0] === "wiki" && segments.length === 3 && segments[1] === "space" && SPACE_ID_PATTERN.test(segments[2])) return {
|
|
10485
|
+
kind: "space",
|
|
10486
|
+
spaceId: segments[2]
|
|
10487
|
+
};
|
|
10488
|
+
if (segments.length === 2 && TOKEN_PATTERN.test(segments[1])) {
|
|
10489
|
+
if (segments[0] === "wiki" && segments[1] !== "space") return {
|
|
10490
|
+
kind: "node",
|
|
10491
|
+
token: segments[1]
|
|
10492
|
+
};
|
|
10493
|
+
if (segments[0] === "docx") return {
|
|
10494
|
+
kind: "docx",
|
|
10495
|
+
token: segments[1]
|
|
10496
|
+
};
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
10499
|
+
//#endregion
|
|
10500
|
+
//#region src/feishu/wiki/scope.ts
|
|
10501
|
+
/**
|
|
10502
|
+
* Longest parent chain walked for a node root. Feishu limits wiki depth well
|
|
10503
|
+
* below this; a longer chain is treated as outside.
|
|
10504
|
+
*/
|
|
10505
|
+
const MAX_PARENT_DEPTH = 64;
|
|
10506
|
+
/**
|
|
10507
|
+
* Find the configured root containing `node`.
|
|
10508
|
+
*
|
|
10509
|
+
* @param api Wiki endpoints (for parent lookups).
|
|
10510
|
+
* @param roots Configured roots.
|
|
10511
|
+
* @param node The node to check.
|
|
10512
|
+
* @returns The containing root, or `undefined` when none contains it.
|
|
10513
|
+
*/
|
|
10514
|
+
async function containingRoot(api, roots, node) {
|
|
10515
|
+
for (const root of roots) if (root.kind === "space" && root.spaceId === node.spaceId) return { root };
|
|
10516
|
+
const nodeRoots = roots.filter((root) => root.kind === "node");
|
|
10517
|
+
if (nodeRoots.length === 0) return void 0;
|
|
10518
|
+
const byToken = new Map(nodeRoots.map((root) => [root.nodeToken, root]));
|
|
10519
|
+
let current = node;
|
|
10520
|
+
for (let depth = 0; depth <= MAX_PARENT_DEPTH; depth += 1) {
|
|
10521
|
+
const match = byToken.get(current.nodeToken);
|
|
10522
|
+
if (match !== void 0) return { root: match };
|
|
10523
|
+
if (current.parentNodeToken === void 0) return void 0;
|
|
10524
|
+
current = await api.getNode(current.parentNodeToken, "wiki");
|
|
10525
|
+
}
|
|
10526
|
+
}
|
|
10527
|
+
/**
|
|
10528
|
+
* Whether a whole space (its top level) is inside the roots: only a space
|
|
10529
|
+
* root covers a space's top level.
|
|
10530
|
+
*
|
|
10531
|
+
* @param roots Configured roots.
|
|
10532
|
+
* @param spaceId The space.
|
|
10533
|
+
* @returns True when a space root names it.
|
|
10534
|
+
*/
|
|
10535
|
+
function spaceIsRoot(roots, spaceId) {
|
|
10536
|
+
return roots.some((root) => root.kind === "space" && root.spaceId === spaceId);
|
|
10537
|
+
}
|
|
10538
|
+
//#endregion
|
|
10539
|
+
//#region src/feishu/wiki/base-tools.ts
|
|
10540
|
+
/**
|
|
10541
|
+
* Wiki Base (多维表格) tools: `feishu_wiki_base_read` (switch
|
|
10542
|
+
* `feishuTools.wikiRead`) and `feishu_wiki_base_edit` (switch
|
|
10543
|
+
* `feishuTools.wikiWrite`).
|
|
10544
|
+
*
|
|
10545
|
+
* Both run through the same gates as the docx tools (whitelist, `wikiRoots`
|
|
10546
|
+
* containment, consent cards). Reading shares the docx read grant per Session
|
|
10547
|
+
* and document. Editing changes records only (no table or field structure):
|
|
10548
|
+
* on a base the app owns it may create, update, and delete records; on any
|
|
10549
|
+
* other base it may only create records, so existing data written by people
|
|
10550
|
+
* is never changed. Every edit asks on a consent card. Feishu offers no
|
|
10551
|
+
* revision precondition for record writes, and applies them asynchronously.
|
|
10552
|
+
*
|
|
10553
|
+
* @module dsh-channels/feishu/wiki/base-tools
|
|
10554
|
+
*/
|
|
10555
|
+
/** Model-facing tool names; stable across releases (session history references them). */
|
|
10556
|
+
const WIKI_BASE_READ_TOOL_NAME = "feishu_wiki_base_read";
|
|
10557
|
+
const WIKI_BASE_EDIT_TOOL_NAME = "feishu_wiki_base_edit";
|
|
10558
|
+
/** Record page size bounds of `feishu_wiki_base_read`. */
|
|
10559
|
+
const READ_DEFAULT_LIMIT = 50;
|
|
10560
|
+
const READ_MAX_LIMIT = 200;
|
|
10561
|
+
/** Records of each operation shown on a consent card. */
|
|
10562
|
+
const CARD_PREVIEW_RECORDS = 3;
|
|
10563
|
+
const refSchema$2 = z.string().trim().min(1).max(2048);
|
|
10564
|
+
const nameSchema = z.string().trim().min(1).max(200);
|
|
10565
|
+
const recordIdSchema = z.string().trim().min(1).max(64);
|
|
10566
|
+
const fieldMapSchema = z.record(z.string().min(1).max(200), z.unknown()).refine((map) => Object.keys(map).length > 0, "至少包含一个字段");
|
|
10567
|
+
const wikiBaseReadArgsSchema = z.object({
|
|
10568
|
+
doc: refSchema$2,
|
|
10569
|
+
table: nameSchema.optional(),
|
|
10570
|
+
filter: z.object({
|
|
10571
|
+
logic: z.enum(["and", "or"]).optional(),
|
|
10572
|
+
conditions: z.array(z.array(z.unknown()).min(2).max(3)).min(1).max(50)
|
|
10573
|
+
}).strict().optional(),
|
|
10574
|
+
sort: z.array(z.object({
|
|
10575
|
+
field: nameSchema,
|
|
10576
|
+
desc: z.boolean().optional()
|
|
10577
|
+
}).strict()).min(1).max(10).optional(),
|
|
10578
|
+
keyword: z.string().trim().min(1).max(200).optional(),
|
|
10579
|
+
search_fields: z.array(nameSchema).min(1).max(20).optional(),
|
|
10580
|
+
fields: z.array(nameSchema).min(1).max(100).optional(),
|
|
10581
|
+
view_id: nameSchema.optional(),
|
|
10582
|
+
offset: z.number().int().min(0).optional(),
|
|
10583
|
+
limit: z.number().int().min(1).max(READ_MAX_LIMIT).optional()
|
|
10584
|
+
}).strict();
|
|
10585
|
+
const baseOperationSchema = z.discriminatedUnion("command", [
|
|
10586
|
+
z.object({
|
|
10587
|
+
command: z.literal("create"),
|
|
10588
|
+
records: z.array(fieldMapSchema).min(1).max(200)
|
|
10589
|
+
}).strict(),
|
|
10590
|
+
z.object({
|
|
10591
|
+
command: z.literal("update"),
|
|
10592
|
+
records: z.array(z.object({
|
|
10593
|
+
record_id: recordIdSchema,
|
|
10594
|
+
fields: fieldMapSchema
|
|
10595
|
+
}).strict()).min(1).max(200)
|
|
10596
|
+
}).strict(),
|
|
10597
|
+
z.object({
|
|
10598
|
+
command: z.literal("delete"),
|
|
10599
|
+
record_ids: z.array(recordIdSchema).min(1).max(200)
|
|
10600
|
+
}).strict()
|
|
10601
|
+
]);
|
|
10602
|
+
const wikiBaseEditArgsSchema = z.object({
|
|
10603
|
+
doc: refSchema$2,
|
|
10604
|
+
table: nameSchema,
|
|
10605
|
+
operations: z.array(baseOperationSchema).min(1).max(10)
|
|
10606
|
+
}).strict();
|
|
10607
|
+
const READ_DESCRIPTION$2 = `读取知识库中的飞书多维表格(bitable)。doc 传知识库节点链接或 node_token。第一次读取某个多维表格会在话题里弹出授权卡,由话题发起人确认,确认后本话题内再次读取不再询问;等待期间不要重复调用。不传 table 时只返回数据表列表;传 table(table_id 或表名)时返回字段列表和记录。filter 形如 {"logic":"and","conditions":[["状态","intersects",["进行中"]],["金额",">=",100],["备注","non_empty"]]},运算符有 == != > >= < <= intersects(包含)disjoint(不包含)empty non_empty;日期条件用 "ExactDate(2026-08-07)",日期不支持 >=。sort 形如 [{"field":"更新时间","desc":true}];keyword 为关键词搜索,search_fields 指定搜索的字段(默认主字段);fields 只返回指定字段。分页用 offset/limit(limit 默认 ${READ_DEFAULT_LIMIT},最多 ${READ_MAX_LIMIT}),has_more 为 true 时用 next_offset 继续。writable 说明能做哪些修改:all 表示可增改删记录(本应用创建的表),create 表示只能新增记录,none 表示不能修改。`;
|
|
10608
|
+
const EDIT_DESCRIPTION$2 = `修改知识库中飞书多维表格(bitable)某个数据表的记录。先用 feishu_wiki_base_read 读取字段列表,按字段名写入。operations 按顺序执行(最多 10 个,每个最多 200 条记录):create(records 为字段名→值的对象数组)、update(records 为 {record_id, fields} 数组,只改给出的字段)、delete(record_ids)。本应用创建的多维表格可以增改删;其他多维表格只能新增记录,不能修改或删除已有记录。不会修改数据表和字段结构。单元格取值:文本为字符串;数字为数字;单选/多选为选项名数组(如 ["进行中"],单选也是数组);日期为 "2026-03-24 10:00" 或毫秒时间戳;复选框为布尔;人员为 [{"id":"ou_xxx"}];关联为 [{"id":"rec_xxx"}];null 清空。公式、查找引用、创建时间等字段只读。写入是异步生效的,立刻读取可能还看不到。每次修改都会在话题里弹出授权卡,由话题发起人确认后才执行;等待期间不要重复调用。`;
|
|
10609
|
+
const REF_DESCRIPTION$2 = "知识库节点链接 https://<租户>.feishu.cn/wiki/<node_token> 或 node_token";
|
|
10610
|
+
const TABLE_PROPERTY = {
|
|
10611
|
+
type: "string",
|
|
10612
|
+
description: "数据表 table_id 或表名"
|
|
10613
|
+
};
|
|
10614
|
+
const FIELD_MAP_PROPERTY = {
|
|
10615
|
+
type: "object",
|
|
10616
|
+
description: "字段名(或字段 id)→ 值",
|
|
10617
|
+
additionalProperties: true
|
|
10618
|
+
};
|
|
10619
|
+
const READ_PARAMETERS$2 = {
|
|
10620
|
+
type: "object",
|
|
10621
|
+
properties: {
|
|
10622
|
+
doc: {
|
|
10623
|
+
type: "string",
|
|
10624
|
+
description: REF_DESCRIPTION$2
|
|
10625
|
+
},
|
|
10626
|
+
table: {
|
|
10627
|
+
...TABLE_PROPERTY,
|
|
10628
|
+
description: "数据表 table_id 或表名;不传只列出数据表"
|
|
10629
|
+
},
|
|
10630
|
+
filter: {
|
|
10631
|
+
type: "object",
|
|
10632
|
+
properties: {
|
|
10633
|
+
logic: {
|
|
10634
|
+
type: "string",
|
|
10635
|
+
enum: ["and", "or"]
|
|
10636
|
+
},
|
|
10637
|
+
conditions: {
|
|
10638
|
+
type: "array",
|
|
10639
|
+
items: {
|
|
10640
|
+
type: "array",
|
|
10641
|
+
description: "[字段, 运算符, 值];empty / non_empty 不带值"
|
|
10642
|
+
}
|
|
10643
|
+
}
|
|
10644
|
+
},
|
|
10645
|
+
required: ["conditions"],
|
|
10646
|
+
additionalProperties: false
|
|
10647
|
+
},
|
|
10648
|
+
sort: {
|
|
10649
|
+
type: "array",
|
|
10650
|
+
items: {
|
|
10651
|
+
type: "object",
|
|
10652
|
+
properties: {
|
|
10653
|
+
field: { type: "string" },
|
|
10654
|
+
desc: { type: "boolean" }
|
|
10655
|
+
},
|
|
10656
|
+
required: ["field"],
|
|
10657
|
+
additionalProperties: false
|
|
10658
|
+
}
|
|
10659
|
+
},
|
|
10660
|
+
keyword: {
|
|
10661
|
+
type: "string",
|
|
10662
|
+
description: "关键词搜索"
|
|
10663
|
+
},
|
|
10664
|
+
search_fields: {
|
|
10665
|
+
type: "array",
|
|
10666
|
+
items: { type: "string" },
|
|
10667
|
+
description: "keyword 搜索的字段,默认主字段"
|
|
10668
|
+
},
|
|
10669
|
+
fields: {
|
|
10670
|
+
type: "array",
|
|
10671
|
+
items: { type: "string" },
|
|
10672
|
+
description: "只返回这些字段"
|
|
10673
|
+
},
|
|
10674
|
+
view_id: {
|
|
10675
|
+
type: "string",
|
|
10676
|
+
description: "按视图读取(应用视图的筛选与排序)"
|
|
10677
|
+
},
|
|
10678
|
+
offset: {
|
|
10679
|
+
type: "integer",
|
|
10680
|
+
minimum: 0
|
|
10681
|
+
},
|
|
10682
|
+
limit: {
|
|
10683
|
+
type: "integer",
|
|
10684
|
+
minimum: 1,
|
|
10685
|
+
maximum: READ_MAX_LIMIT
|
|
10686
|
+
}
|
|
10687
|
+
},
|
|
10688
|
+
required: ["doc"],
|
|
10689
|
+
additionalProperties: false
|
|
10690
|
+
};
|
|
10691
|
+
const EDIT_PARAMETERS$2 = {
|
|
10692
|
+
type: "object",
|
|
10693
|
+
properties: {
|
|
10694
|
+
doc: {
|
|
10695
|
+
type: "string",
|
|
10696
|
+
description: REF_DESCRIPTION$2
|
|
10697
|
+
},
|
|
10698
|
+
table: TABLE_PROPERTY,
|
|
10699
|
+
operations: {
|
|
10700
|
+
type: "array",
|
|
10701
|
+
minItems: 1,
|
|
10702
|
+
maxItems: 10,
|
|
10703
|
+
items: {
|
|
10704
|
+
type: "object",
|
|
10705
|
+
properties: {
|
|
10706
|
+
command: {
|
|
10707
|
+
type: "string",
|
|
10708
|
+
enum: [
|
|
10709
|
+
"create",
|
|
10710
|
+
"update",
|
|
10711
|
+
"delete"
|
|
10712
|
+
]
|
|
10713
|
+
},
|
|
10714
|
+
records: {
|
|
10715
|
+
type: "array",
|
|
10716
|
+
maxItems: 200,
|
|
10717
|
+
description: "create:字段映射数组;update:{record_id, fields} 数组",
|
|
10718
|
+
items: {
|
|
10719
|
+
type: "object",
|
|
10720
|
+
properties: {
|
|
10721
|
+
record_id: { type: "string" },
|
|
10722
|
+
fields: FIELD_MAP_PROPERTY
|
|
10723
|
+
},
|
|
10724
|
+
additionalProperties: true
|
|
10725
|
+
}
|
|
10726
|
+
},
|
|
10727
|
+
record_ids: {
|
|
10728
|
+
type: "array",
|
|
10729
|
+
maxItems: 200,
|
|
10730
|
+
items: { type: "string" },
|
|
10731
|
+
description: "delete 的记录 id"
|
|
10732
|
+
}
|
|
10733
|
+
},
|
|
10734
|
+
required: ["command"],
|
|
10735
|
+
additionalProperties: false
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
},
|
|
10739
|
+
required: [
|
|
10740
|
+
"doc",
|
|
10741
|
+
"table",
|
|
10742
|
+
"operations"
|
|
10743
|
+
],
|
|
10744
|
+
additionalProperties: false
|
|
10745
|
+
};
|
|
10746
|
+
async function baseTarget(kit, context, doc) {
|
|
10747
|
+
const bases = context.bases;
|
|
10748
|
+
if (bases === void 0) return failure("CHANNEL_UNAVAILABLE");
|
|
10749
|
+
const ref = parseWikiRef(doc);
|
|
10750
|
+
if (ref === void 0) return failure("INVALID_REFERENCE");
|
|
10751
|
+
const node = await kit.containedNode(context, ref);
|
|
10752
|
+
if ("ok" in node) return node;
|
|
10753
|
+
if (node.objType !== "bitable") return failure("UNSUPPORTED_TYPE", `${failure("UNSUPPORTED_TYPE").reason}(该节点类型:${node.objType})`);
|
|
10754
|
+
return {
|
|
10755
|
+
context,
|
|
10756
|
+
bases,
|
|
10757
|
+
node,
|
|
10758
|
+
url: nodeUrl(context.roots, node)
|
|
10759
|
+
};
|
|
10760
|
+
}
|
|
10761
|
+
function writableOf(kit, target) {
|
|
10762
|
+
if (!kit.writeEnabled()) return "none";
|
|
10763
|
+
return ownedByApp(target.node, target.context.botOpenId) ? "all" : "create";
|
|
10764
|
+
}
|
|
10765
|
+
function pickTable(tables, wanted) {
|
|
10766
|
+
const match = tables.find((table) => table.tableId === wanted) ?? tables.find((table) => table.name === wanted);
|
|
10767
|
+
if (match !== void 0) return match;
|
|
10768
|
+
return `找不到数据表「${wanted}」;可用数据表:${tables.map((table) => `${table.name}(${table.tableId})`).join("、") || "无"}`;
|
|
10769
|
+
}
|
|
10770
|
+
function fieldView(field) {
|
|
10771
|
+
return {
|
|
10772
|
+
...field.properties,
|
|
10773
|
+
field_id: field.fieldId,
|
|
10774
|
+
name: field.name,
|
|
10775
|
+
type: field.type
|
|
10776
|
+
};
|
|
10777
|
+
}
|
|
10778
|
+
async function read$1(kit, sessionId, rawArgs, signal) {
|
|
10779
|
+
const args = parseArgs(WIKI_BASE_READ_TOOL_NAME, wikiBaseReadArgsSchema, rawArgs);
|
|
10780
|
+
const gate = kit.gate(sessionId, WIKI_BASE_READ_TOOL_NAME);
|
|
10781
|
+
if (!gate.ok) return gate;
|
|
10782
|
+
return await guarded$1(kit, WIKI_BASE_READ_TOOL_NAME, async () => {
|
|
10783
|
+
const target = await baseTarget(kit, gate.context, args.doc);
|
|
10784
|
+
if ("ok" in target) return target;
|
|
10785
|
+
const denied = await kit.readConsent(sessionId, target.context, target.node, signal);
|
|
10786
|
+
if (denied !== void 0) return denied;
|
|
10787
|
+
if (signal.aborted) return failure("ABORTED");
|
|
10788
|
+
const token = target.node.objToken;
|
|
10789
|
+
const tables = await target.bases.listTables(token);
|
|
10790
|
+
const base = {
|
|
10791
|
+
ok: true,
|
|
10792
|
+
title: target.node.title,
|
|
10793
|
+
node_token: target.node.nodeToken,
|
|
10794
|
+
url: target.url,
|
|
10795
|
+
tables: tables.map((table) => ({
|
|
10796
|
+
table_id: table.tableId,
|
|
10797
|
+
name: table.name
|
|
10798
|
+
})),
|
|
10799
|
+
writable: writableOf(kit, target)
|
|
10800
|
+
};
|
|
10801
|
+
if (args.table === void 0) return base;
|
|
10802
|
+
const table = pickTable(tables, args.table);
|
|
10803
|
+
if (typeof table === "string") return failure("INVALID_OPERATION", table);
|
|
10804
|
+
const fields = await target.bases.listFields(token, table.tableId);
|
|
10805
|
+
const offset = args.offset ?? 0;
|
|
10806
|
+
const query = {
|
|
10807
|
+
...args.filter === void 0 ? {} : { filter: args.filter },
|
|
10808
|
+
...args.sort === void 0 ? {} : { sort: args.sort },
|
|
10809
|
+
...args.fields === void 0 ? {} : { fields: args.fields },
|
|
10810
|
+
...args.view_id === void 0 ? {} : { viewId: args.view_id },
|
|
10811
|
+
offset,
|
|
10812
|
+
limit: args.limit ?? READ_DEFAULT_LIMIT
|
|
10813
|
+
};
|
|
10814
|
+
let page;
|
|
10815
|
+
if (args.keyword === void 0) page = await target.bases.listRecords(token, table.tableId, query);
|
|
10816
|
+
else {
|
|
10817
|
+
const primary = fields[0]?.name;
|
|
10818
|
+
const searchFields = args.search_fields ?? (primary === void 0 ? [] : [primary]);
|
|
10819
|
+
if (searchFields.length === 0) return failure("INVALID_OPERATION", "keyword 搜索需要 search_fields");
|
|
10820
|
+
page = await target.bases.searchRecords(token, table.tableId, {
|
|
10821
|
+
...query,
|
|
10822
|
+
keyword: args.keyword,
|
|
10823
|
+
searchFields
|
|
10824
|
+
});
|
|
10825
|
+
}
|
|
10826
|
+
return {
|
|
10827
|
+
...base,
|
|
10828
|
+
table_id: table.tableId,
|
|
10829
|
+
fields: fields.map(fieldView),
|
|
10830
|
+
records: page.records.map((record) => ({
|
|
10831
|
+
record_id: record.recordId,
|
|
10832
|
+
fields: record.fields
|
|
10833
|
+
})),
|
|
10834
|
+
has_more: page.hasMore,
|
|
10835
|
+
...page.hasMore ? { next_offset: offset + page.records.length } : {},
|
|
10836
|
+
...page.timezone === void 0 ? {} : { timezone: page.timezone },
|
|
10837
|
+
...page.ignoredFields.length === 0 ? {} : { ignored_fields: page.ignoredFields }
|
|
10838
|
+
};
|
|
10839
|
+
});
|
|
10840
|
+
}
|
|
10841
|
+
function json(value) {
|
|
10842
|
+
return preview(JSON.stringify(value) ?? "null", 120);
|
|
10843
|
+
}
|
|
10844
|
+
function cardLines$1(operations, tableName) {
|
|
10845
|
+
const lines = operations.map((operation, index) => {
|
|
10846
|
+
const n = `${index + 1}.`;
|
|
10847
|
+
switch (operation.command) {
|
|
10848
|
+
case "create": {
|
|
10849
|
+
const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${json(record)}`);
|
|
10850
|
+
const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : [];
|
|
10851
|
+
return [
|
|
10852
|
+
`${n} 新增 ${operation.records.length} 条记录`,
|
|
10853
|
+
...sample,
|
|
10854
|
+
...more
|
|
10855
|
+
].join("\n");
|
|
10856
|
+
}
|
|
10857
|
+
case "update": {
|
|
10858
|
+
const sample = operation.records.slice(0, CARD_PREVIEW_RECORDS).map((record) => ` ${preview(record.record_id, 40)}:${json(record.fields)}`);
|
|
10859
|
+
const more = operation.records.length > CARD_PREVIEW_RECORDS ? [` …(共 ${operation.records.length} 条)`] : [];
|
|
10860
|
+
return [
|
|
10861
|
+
`${n} 修改 ${operation.records.length} 条记录`,
|
|
10862
|
+
...sample,
|
|
10863
|
+
...more
|
|
10864
|
+
].join("\n");
|
|
10865
|
+
}
|
|
10866
|
+
case "delete": return `${n} ⚠️ 删除 ${operation.record_ids.length} 条记录:${preview(operation.record_ids.join("、"), 160)}`;
|
|
10867
|
+
}
|
|
10868
|
+
});
|
|
10869
|
+
return [
|
|
10870
|
+
`**数据表**:${preview(tableName, 60)}`,
|
|
10871
|
+
`**将执行 ${operations.length} 个操作**`,
|
|
10872
|
+
...lines
|
|
10873
|
+
];
|
|
10874
|
+
}
|
|
10875
|
+
async function edit$1(kit, sessionId, rawArgs, signal) {
|
|
10876
|
+
const args = parseArgs(WIKI_BASE_EDIT_TOOL_NAME, wikiBaseEditArgsSchema, rawArgs);
|
|
10877
|
+
const gate = kit.gate(sessionId, WIKI_BASE_EDIT_TOOL_NAME);
|
|
10878
|
+
if (!gate.ok) return gate;
|
|
10879
|
+
return await guarded$1(kit, WIKI_BASE_EDIT_TOOL_NAME, async () => {
|
|
10880
|
+
const target = await baseTarget(kit, gate.context, args.doc);
|
|
10881
|
+
if ("ok" in target) return target;
|
|
10882
|
+
const { node, bases } = target;
|
|
10883
|
+
if (writableOf(kit, target) !== "all") {
|
|
10884
|
+
const changing = args.operations.find((operation) => operation.command !== "create");
|
|
10885
|
+
if (changing !== void 0) return failure("NOT_OWNED", `这个多维表格不是本应用创建的,只能新增记录,不能${changing.command === "update" ? "修改" : "删除"}已有记录(所有者:${node.owner ?? "未知"})`);
|
|
10886
|
+
}
|
|
10887
|
+
const table = pickTable(await bases.listTables(node.objToken), args.table);
|
|
10888
|
+
if (typeof table === "string") return failure("INVALID_OPERATION", table);
|
|
10889
|
+
const denied = await kit.consent(sessionId, target.context, "edit", {
|
|
10890
|
+
title: node.title,
|
|
10891
|
+
url: target.url,
|
|
10892
|
+
location: await kit.locationOf(target.context.api, node),
|
|
10893
|
+
details: cardLines$1(args.operations, table.name)
|
|
10894
|
+
}, signal);
|
|
10895
|
+
if (denied !== void 0) return denied;
|
|
10896
|
+
const created = [];
|
|
10897
|
+
const ignored = [];
|
|
10898
|
+
let updated = 0;
|
|
10899
|
+
let deleted = 0;
|
|
10900
|
+
for (const [index, operation] of args.operations.entries()) {
|
|
10901
|
+
if (signal.aborted) return {
|
|
10902
|
+
...failure("ABORTED", `已取消;前 ${index} 个操作已执行`),
|
|
10903
|
+
applied: index
|
|
10904
|
+
};
|
|
10905
|
+
try {
|
|
10906
|
+
switch (operation.command) {
|
|
10907
|
+
case "create":
|
|
10908
|
+
created.push(...await bases.createRecords(node.objToken, table.tableId, operation.records));
|
|
10909
|
+
break;
|
|
10910
|
+
case "update":
|
|
10911
|
+
ignored.push(...await bases.updateRecords(node.objToken, table.tableId, Object.fromEntries(operation.records.map((record) => [record.record_id, record.fields]))));
|
|
10912
|
+
updated += operation.records.length;
|
|
10913
|
+
break;
|
|
10914
|
+
case "delete":
|
|
10915
|
+
await bases.deleteRecords(node.objToken, table.tableId, operation.record_ids);
|
|
10916
|
+
deleted += operation.record_ids.length;
|
|
10917
|
+
break;
|
|
10918
|
+
}
|
|
10919
|
+
} catch (error) {
|
|
10920
|
+
const reason = failureOf(error).reason;
|
|
10921
|
+
return {
|
|
10922
|
+
...failure("EDIT_FAILED", `第 ${index + 1} 个操作失败:${reason};前 ${index} 个操作已执行`),
|
|
10923
|
+
applied: index
|
|
10924
|
+
};
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
return {
|
|
10928
|
+
ok: true,
|
|
10929
|
+
title: node.title,
|
|
10930
|
+
node_token: node.nodeToken,
|
|
10931
|
+
url: target.url,
|
|
10932
|
+
table_id: table.tableId,
|
|
10933
|
+
applied: args.operations.length,
|
|
10934
|
+
created_record_ids: created,
|
|
10935
|
+
updated,
|
|
10936
|
+
deleted,
|
|
10937
|
+
ignored_fields: [...new Set(ignored)]
|
|
10938
|
+
};
|
|
10939
|
+
});
|
|
10940
|
+
}
|
|
10941
|
+
/** The Base tool family. */
|
|
10942
|
+
const BASE_TOOL_SPECS = [{
|
|
10943
|
+
name: WIKI_BASE_READ_TOOL_NAME,
|
|
10944
|
+
access: "read",
|
|
10945
|
+
description: READ_DESCRIPTION$2,
|
|
10946
|
+
parameters: READ_PARAMETERS$2,
|
|
10947
|
+
execute: read$1,
|
|
10948
|
+
presentCall: (args) => `读取知识库多维表格:${display(args.doc)}`
|
|
10949
|
+
}, {
|
|
10950
|
+
name: WIKI_BASE_EDIT_TOOL_NAME,
|
|
10951
|
+
access: "write",
|
|
10952
|
+
description: EDIT_DESCRIPTION$2,
|
|
10953
|
+
parameters: EDIT_PARAMETERS$2,
|
|
10954
|
+
execute: edit$1,
|
|
10955
|
+
presentCall: (args) => `修改知识库多维表格:${display(args.doc)}`
|
|
10956
|
+
}];
|
|
10957
|
+
//#endregion
|
|
10958
|
+
//#region src/feishu/wiki/a1.ts
|
|
10959
|
+
/**
|
|
10960
|
+
* A1 notation helpers for the wiki sheet tools: column letters, single cells,
|
|
10961
|
+
* and rectangle end rows. Ranges never carry a sheet prefix (`Sheet1!`); the
|
|
10962
|
+
* sub-sheet is selected separately.
|
|
10963
|
+
*
|
|
10964
|
+
* @module dsh-channels/feishu/wiki/a1
|
|
10965
|
+
*/
|
|
10966
|
+
/** Largest column the helpers accept (`ZZZ`). */
|
|
10967
|
+
const MAX_COLUMN = 18278;
|
|
10968
|
+
/** Largest row the helpers accept. */
|
|
10969
|
+
const MAX_ROW = 1e7;
|
|
10970
|
+
/**
|
|
10971
|
+
* Column letters to a 1-based index (`A` → 1, `AA` → 27).
|
|
10972
|
+
*
|
|
10973
|
+
* @param letters Column letters, any case.
|
|
10974
|
+
* @returns The index, or undefined when invalid or beyond `ZZZ`.
|
|
10975
|
+
*/
|
|
10976
|
+
function columnIndex(letters) {
|
|
10977
|
+
if (!/^[A-Za-z]{1,3}$/u.test(letters)) return void 0;
|
|
10978
|
+
let index = 0;
|
|
10979
|
+
for (const char of letters.toUpperCase()) index = index * 26 + (char.charCodeAt(0) - 64);
|
|
10980
|
+
return index <= MAX_COLUMN ? index : void 0;
|
|
10981
|
+
}
|
|
10982
|
+
/**
|
|
10983
|
+
* A 1-based column index to letters (1 → `A`, 27 → `AA`).
|
|
10984
|
+
*
|
|
10985
|
+
* @param index Column index, at least 1.
|
|
10986
|
+
* @returns The letters.
|
|
10987
|
+
*/
|
|
10988
|
+
function columnLetters(index) {
|
|
10989
|
+
let letters = "";
|
|
10990
|
+
let rest = index;
|
|
10991
|
+
while (rest > 0) {
|
|
10992
|
+
const digit = (rest - 1) % 26;
|
|
10993
|
+
letters = String.fromCharCode(65 + digit) + letters;
|
|
10994
|
+
rest = Math.floor((rest - 1) / 26);
|
|
10995
|
+
}
|
|
10996
|
+
return letters;
|
|
10997
|
+
}
|
|
10998
|
+
/**
|
|
10999
|
+
* Parse one cell (`B3`).
|
|
11000
|
+
*
|
|
11001
|
+
* @param value Cell reference.
|
|
11002
|
+
* @returns The position, or undefined when it is not a single valid cell.
|
|
11003
|
+
*/
|
|
11004
|
+
function parseCell(value) {
|
|
11005
|
+
const match = /^([A-Za-z]{1,3})([1-9]\d{0,7})$/u.exec(value.trim());
|
|
11006
|
+
if (match === null) return void 0;
|
|
11007
|
+
const column = columnIndex(match[1] ?? "");
|
|
11008
|
+
const row = Number(match[2]);
|
|
11009
|
+
return column === void 0 || row > MAX_ROW ? void 0 : {
|
|
11010
|
+
column,
|
|
11011
|
+
row
|
|
11012
|
+
};
|
|
11013
|
+
}
|
|
11014
|
+
/**
|
|
11015
|
+
* The rectangle that starts at `start` and spans `rows` × `columns` cells.
|
|
11016
|
+
*
|
|
11017
|
+
* @param start Top-left cell.
|
|
11018
|
+
* @param rows Row count, at least 1.
|
|
11019
|
+
* @param columns Column count, at least 1.
|
|
11020
|
+
* @returns The range (`B3:D10`).
|
|
11021
|
+
*/
|
|
11022
|
+
function rectangle(start, rows, columns) {
|
|
11023
|
+
return `${columnLetters(start.column)}${start.row}:${columnLetters(start.column + columns - 1)}${start.row + rows - 1}`;
|
|
11024
|
+
}
|
|
11025
|
+
/**
|
|
11026
|
+
* Last row of a range (`A1:B5` → 5, `C7` → 7); undefined for whole-column ranges (`A:C`).
|
|
11027
|
+
*
|
|
11028
|
+
* @param range A1 range.
|
|
11029
|
+
* @returns The row, or undefined.
|
|
11030
|
+
*/
|
|
11031
|
+
function rangeEndRow(range) {
|
|
11032
|
+
const end = range.split(":").at(-1) ?? "";
|
|
11033
|
+
const match = /(\d+)$/u.exec(end.trim());
|
|
11034
|
+
return match === null ? void 0 : Number(match[1]);
|
|
11035
|
+
}
|
|
11036
|
+
/**
|
|
11037
|
+
* Whether a range is plain A1 (`A1`, `A1:F30`, `A:C`, `3:5`) without a sheet prefix.
|
|
11038
|
+
*
|
|
11039
|
+
* @param range Candidate range.
|
|
11040
|
+
* @returns True when acceptable.
|
|
11041
|
+
*/
|
|
11042
|
+
function isA1Range(range) {
|
|
11043
|
+
return /^(?:[A-Za-z]{1,3}\d{0,8}|\d{1,8})(?::(?:[A-Za-z]{1,3}\d{0,8}|\d{1,8}))?$/u.test(range);
|
|
11044
|
+
}
|
|
11045
|
+
//#endregion
|
|
11046
|
+
//#region src/feishu/wiki/sheet-tools.ts
|
|
11047
|
+
/**
|
|
11048
|
+
* Wiki spreadsheet (电子表格) tools: `feishu_wiki_sheet_read` (switch
|
|
11049
|
+
* `feishuTools.wikiRead`) and `feishu_wiki_sheet_edit` (switch
|
|
11050
|
+
* `feishuTools.wikiWrite`).
|
|
11051
|
+
*
|
|
11052
|
+
* Both run through the same gates as the docx tools (whitelist, `wikiRoots`
|
|
11053
|
+
* containment, consent cards). Reading shares the docx read grant per Session
|
|
11054
|
+
* and document. Editing writes cell values and formulas only (no styles,
|
|
11055
|
+
* charts, or row/column structure), only on spreadsheets the app owns, and
|
|
11056
|
+
* only when the workbook revision still equals the one the model read, checked
|
|
11057
|
+
* before and after the consent card; Feishu offers no revision precondition on
|
|
11058
|
+
* the write itself.
|
|
11059
|
+
*
|
|
11060
|
+
* @module dsh-channels/feishu/wiki/sheet-tools
|
|
11061
|
+
*/
|
|
11062
|
+
/** Model-facing tool names; stable across releases (session history references them). */
|
|
11063
|
+
const WIKI_SHEET_READ_TOOL_NAME = "feishu_wiki_sheet_read";
|
|
11064
|
+
const WIKI_SHEET_EDIT_TOOL_NAME = "feishu_wiki_sheet_edit";
|
|
11065
|
+
/** Most cells one `feishu_wiki_sheet_edit` call may write in total. */
|
|
11066
|
+
const WIKI_SHEET_EDIT_MAX_CELLS = 5e4;
|
|
11067
|
+
/** Read size bounds (characters of CSV returned to the model). */
|
|
11068
|
+
const READ_DEFAULT_CHARS = 2e4;
|
|
11069
|
+
const READ_MAX_CHARS = 1e5;
|
|
11070
|
+
/** CSV characters requested when only the data extent of a sheet is needed. */
|
|
11071
|
+
const EXTENT_PROBE_CHARS = 2e3;
|
|
11072
|
+
/** CSV characters per page while scanning below the first data region for later rows. */
|
|
11073
|
+
const EXTENT_SCAN_CHARS = 2e5;
|
|
11074
|
+
/** Pages the append row scan reads before giving up. */
|
|
11075
|
+
const EXTENT_SCAN_PAGES = 20;
|
|
11076
|
+
/** Scan bounds when the workbook does not report the grid size. */
|
|
11077
|
+
const MAX_SCAN_COLUMN = 18278;
|
|
11078
|
+
const MAX_SCAN_ROW = 1e7;
|
|
11079
|
+
/** Rows of each written block shown on a consent card. */
|
|
11080
|
+
const CARD_PREVIEW_ROWS = 3;
|
|
11081
|
+
const refSchema$1 = z.string().trim().min(1).max(2048);
|
|
11082
|
+
const sheetSchema = z.string().trim().min(1).max(200);
|
|
11083
|
+
const cellValueSchema = z.union([
|
|
11084
|
+
z.string().max(5e4),
|
|
11085
|
+
z.number().finite(),
|
|
11086
|
+
z.boolean(),
|
|
11087
|
+
z.null()
|
|
11088
|
+
]);
|
|
11089
|
+
const valuesSchema = z.array(z.array(cellValueSchema).min(1).max(1e3)).min(1).max(1e4);
|
|
11090
|
+
const wikiSheetReadArgsSchema = z.object({
|
|
11091
|
+
doc: refSchema$1,
|
|
11092
|
+
sheet: sheetSchema.optional(),
|
|
11093
|
+
range: z.string().trim().min(1).max(40).refine(isA1Range, "需要不带子表前缀的 A1 范围,例如 A1:F30").optional(),
|
|
11094
|
+
max_chars: z.number().int().min(1e3).max(READ_MAX_CHARS).optional()
|
|
11095
|
+
}).strict();
|
|
11096
|
+
const sheetOperationSchema = z.discriminatedUnion("command", [z.object({
|
|
11097
|
+
command: z.literal("write"),
|
|
11098
|
+
sheet: sheetSchema.optional(),
|
|
11099
|
+
start_cell: z.string().trim().min(2).max(12),
|
|
11100
|
+
values: valuesSchema
|
|
11101
|
+
}).strict(), z.object({
|
|
11102
|
+
command: z.literal("append"),
|
|
11103
|
+
sheet: sheetSchema.optional(),
|
|
11104
|
+
start_column: z.string().trim().min(1).max(3).optional(),
|
|
11105
|
+
values: valuesSchema
|
|
11106
|
+
}).strict()]);
|
|
11107
|
+
const wikiSheetEditArgsSchema = z.object({
|
|
11108
|
+
doc: refSchema$1,
|
|
11109
|
+
revision_id: z.number().int().min(0),
|
|
11110
|
+
operations: z.array(sheetOperationSchema).min(1).max(20)
|
|
11111
|
+
}).strict();
|
|
11112
|
+
const READ_DESCRIPTION$1 = `读取知识库中的飞书电子表格(sheet)。doc 传知识库节点链接或 node_token。第一次读取某个表格会在话题里弹出授权卡,由话题发起人确认,确认后本话题内再次读取不再询问;等待期间不要重复调用。返回 sheets(子表列表);sheet 传子表 sheet_id 或标题选择子表,只有一个可见子表时可省略,有多个而未指定时只返回子表列表。range 为不带子表前缀的 A1 范围(如 A1:F30、A:C),省略时读整张子表。内容为 CSV(显示值),每行前缀 [row=N] 是真实行号,引用行号时以它为准,不要自己数行。max_chars 默认 ${READ_DEFAULT_CHARS};has_more 为 true 时表示被截断,从 actual_range 末行之后接着读。返回的 revision_id 用于 feishu_wiki_sheet_edit;editable 表示能否修改(只有本应用创建的表格可以修改)。`;
|
|
11113
|
+
const EDIT_DESCRIPTION$1 = `修改知识库中由本应用创建的飞书电子表格(sheet)的单元格值。先用 feishu_wiki_sheet_read 读取,把读到的 revision_id 原样传入;表格在此之后被改过会被拒绝,需要重新读取。operations 按顺序执行(最多 20 个,合计不超过 ${WIKI_SHEET_EDIT_MAX_CELLS} 个单元格):write(从 start_cell 如 B3 开始写入二维数组 values)、append(在子表现有数据的最后一行之后追加 values,start_column 默认 A;目标单元格非空时失败,不会覆盖)。返回 formula_errors 时,这些公式没有编译成功、已按文本写入,需要修正后重写。values 每个元素是一行;单元格可以是字符串、数字、布尔或 null(null 表示保持原值不变);以 = 开头的字符串作为公式写入。数字、金额、百分比(0.4 表示 40%)写成数字;编号、前导零、身份证号写成字符串。不会修改样式、图表、行列结构。sheet 传子表 sheet_id 或标题,只有一个可见子表时可省略。每次修改都会在话题里弹出授权卡,由话题发起人确认后才执行;等待期间不要重复调用。`;
|
|
11114
|
+
const REF_DESCRIPTION$1 = "知识库节点链接 https://<租户>.feishu.cn/wiki/<node_token> 或 node_token";
|
|
11115
|
+
const SHEET_PROPERTY = {
|
|
11116
|
+
type: "string",
|
|
11117
|
+
description: "子表 sheet_id 或标题;只有一个可见子表时可省略"
|
|
11118
|
+
};
|
|
11119
|
+
const VALUES_PROPERTY = {
|
|
11120
|
+
type: "array",
|
|
11121
|
+
description: "二维数组,每个元素是一行;单元格为字符串、数字、布尔或 null(保持原值),以 = 开头的字符串是公式",
|
|
11122
|
+
items: {
|
|
11123
|
+
type: "array",
|
|
11124
|
+
items: { type: [
|
|
11125
|
+
"string",
|
|
11126
|
+
"number",
|
|
11127
|
+
"boolean",
|
|
11128
|
+
"null"
|
|
11129
|
+
] }
|
|
11130
|
+
}
|
|
11131
|
+
};
|
|
11132
|
+
const READ_PARAMETERS$1 = {
|
|
11133
|
+
type: "object",
|
|
11134
|
+
properties: {
|
|
11135
|
+
doc: {
|
|
11136
|
+
type: "string",
|
|
11137
|
+
description: REF_DESCRIPTION$1
|
|
11138
|
+
},
|
|
11139
|
+
sheet: SHEET_PROPERTY,
|
|
11140
|
+
range: {
|
|
11141
|
+
type: "string",
|
|
11142
|
+
description: "不带子表前缀的 A1 范围,如 A1:F30;省略读整张子表"
|
|
11143
|
+
},
|
|
11144
|
+
max_chars: {
|
|
11145
|
+
type: "integer",
|
|
11146
|
+
minimum: 1e3,
|
|
11147
|
+
maximum: READ_MAX_CHARS,
|
|
11148
|
+
description: `返回 CSV 的字符上限,默认 ${READ_DEFAULT_CHARS}`
|
|
11149
|
+
}
|
|
11150
|
+
},
|
|
11151
|
+
required: ["doc"],
|
|
11152
|
+
additionalProperties: false
|
|
11153
|
+
};
|
|
11154
|
+
const EDIT_PARAMETERS$1 = {
|
|
11155
|
+
type: "object",
|
|
11156
|
+
properties: {
|
|
11157
|
+
doc: {
|
|
11158
|
+
type: "string",
|
|
11159
|
+
description: REF_DESCRIPTION$1
|
|
11160
|
+
},
|
|
11161
|
+
revision_id: {
|
|
11162
|
+
type: "integer",
|
|
11163
|
+
description: "feishu_wiki_sheet_read 返回的 revision_id"
|
|
11164
|
+
},
|
|
11165
|
+
operations: {
|
|
11166
|
+
type: "array",
|
|
11167
|
+
minItems: 1,
|
|
11168
|
+
maxItems: 20,
|
|
11169
|
+
items: {
|
|
11170
|
+
type: "object",
|
|
11171
|
+
properties: {
|
|
11172
|
+
command: {
|
|
11173
|
+
type: "string",
|
|
11174
|
+
enum: ["write", "append"]
|
|
11175
|
+
},
|
|
11176
|
+
sheet: SHEET_PROPERTY,
|
|
11177
|
+
start_cell: {
|
|
11178
|
+
type: "string",
|
|
11179
|
+
description: "write 的起始单元格,如 B3"
|
|
11180
|
+
},
|
|
11181
|
+
start_column: {
|
|
11182
|
+
type: "string",
|
|
11183
|
+
description: "append 的起始列,默认 A"
|
|
11184
|
+
},
|
|
11185
|
+
values: VALUES_PROPERTY
|
|
11186
|
+
},
|
|
11187
|
+
required: ["command", "values"],
|
|
11188
|
+
additionalProperties: false
|
|
11189
|
+
}
|
|
11190
|
+
}
|
|
11191
|
+
},
|
|
11192
|
+
required: [
|
|
11193
|
+
"doc",
|
|
11194
|
+
"revision_id",
|
|
11195
|
+
"operations"
|
|
11196
|
+
],
|
|
11197
|
+
additionalProperties: false
|
|
11198
|
+
};
|
|
11199
|
+
async function sheetTarget(kit, context, doc) {
|
|
11200
|
+
const sheets = context.sheets;
|
|
11201
|
+
if (sheets === void 0) return failure("CHANNEL_UNAVAILABLE");
|
|
11202
|
+
const ref = parseWikiRef(doc);
|
|
11203
|
+
if (ref === void 0) return failure("INVALID_REFERENCE");
|
|
11204
|
+
const node = await kit.containedNode(context, ref);
|
|
11205
|
+
if ("ok" in node) return node;
|
|
11206
|
+
if (node.objType !== "sheet") return failure("UNSUPPORTED_TYPE", `${failure("UNSUPPORTED_TYPE").reason}(该节点类型:${node.objType})`);
|
|
11207
|
+
return {
|
|
11208
|
+
context,
|
|
11209
|
+
sheets,
|
|
11210
|
+
node,
|
|
11211
|
+
url: nodeUrl(context.roots, node)
|
|
11212
|
+
};
|
|
11213
|
+
}
|
|
11214
|
+
function sheetView(sheet) {
|
|
11215
|
+
return {
|
|
11216
|
+
sheet_id: sheet.sheetId,
|
|
11217
|
+
title: sheet.title,
|
|
11218
|
+
index: sheet.index,
|
|
11219
|
+
resource_type: sheet.resourceType,
|
|
11220
|
+
hidden: sheet.hidden,
|
|
11221
|
+
...sheet.rowCount === void 0 ? {} : { row_count: sheet.rowCount },
|
|
11222
|
+
...sheet.columnCount === void 0 ? {} : { column_count: sheet.columnCount }
|
|
9990
11223
|
};
|
|
9991
11224
|
}
|
|
9992
|
-
/**
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
11225
|
+
/** The named grid sheet, the only visible grid sheet, or why neither applies. */
|
|
11226
|
+
function pickSheet(workbook, wanted) {
|
|
11227
|
+
const grids = workbook.sheets.filter((sheet) => sheet.resourceType === "sheet");
|
|
11228
|
+
if (wanted !== void 0) {
|
|
11229
|
+
const match = workbook.sheets.find((sheet) => sheet.sheetId === wanted) ?? workbook.sheets.find((sheet) => sheet.title === wanted);
|
|
11230
|
+
if (match === void 0) return `找不到子表「${wanted}」;可用子表:${sheetList(workbook)}`;
|
|
11231
|
+
if (match.resourceType !== "sheet") return `子表「${match.title}」是 ${match.resourceType},不是单元格表格,不能按单元格读写`;
|
|
11232
|
+
return match;
|
|
11233
|
+
}
|
|
11234
|
+
const visible = grids.filter((sheet) => !sheet.hidden);
|
|
11235
|
+
if (visible.length === 1 && visible[0] !== void 0) return visible[0];
|
|
11236
|
+
return `表格有多个子表,请用 sheet 指定:${sheetList(workbook)}`;
|
|
11237
|
+
}
|
|
11238
|
+
function sheetList(workbook) {
|
|
11239
|
+
return workbook.sheets.map((sheet) => `${sheet.title}(${sheet.sheetId})`).join("、") || "无";
|
|
11240
|
+
}
|
|
11241
|
+
function wholeSheetRange(sheet) {
|
|
11242
|
+
return sheet.rowCount !== void 0 && sheet.columnCount !== void 0 && sheet.rowCount > 0 && sheet.columnCount > 0 ? `A1:${columnLetters(sheet.columnCount)}${sheet.rowCount}` : "A:ZZZ";
|
|
11243
|
+
}
|
|
11244
|
+
async function read(kit, sessionId, rawArgs, signal) {
|
|
11245
|
+
const args = parseArgs(WIKI_SHEET_READ_TOOL_NAME, wikiSheetReadArgsSchema, rawArgs);
|
|
11246
|
+
const gate = kit.gate(sessionId, WIKI_SHEET_READ_TOOL_NAME);
|
|
11247
|
+
if (!gate.ok) return gate;
|
|
11248
|
+
return await guarded$1(kit, WIKI_SHEET_READ_TOOL_NAME, async () => {
|
|
11249
|
+
const target = await sheetTarget(kit, gate.context, args.doc);
|
|
11250
|
+
if ("ok" in target) return target;
|
|
11251
|
+
const denied = await kit.readConsent(sessionId, target.context, target.node, signal);
|
|
11252
|
+
if (denied !== void 0) return denied;
|
|
11253
|
+
if (signal.aborted) return failure("ABORTED");
|
|
11254
|
+
const workbook = await target.sheets.workbook(target.node.objToken);
|
|
11255
|
+
const base = {
|
|
11256
|
+
ok: true,
|
|
11257
|
+
title: target.node.title,
|
|
11258
|
+
node_token: target.node.nodeToken,
|
|
11259
|
+
url: target.url,
|
|
11260
|
+
...workbook.revision === void 0 ? {} : { revision_id: workbook.revision },
|
|
11261
|
+
sheets: workbook.sheets.map(sheetView),
|
|
11262
|
+
editable: kit.writeEnabled() && ownedByApp(target.node, target.context.botOpenId)
|
|
11263
|
+
};
|
|
11264
|
+
const sheet = pickSheet(workbook, args.sheet);
|
|
11265
|
+
if (typeof sheet === "string") {
|
|
11266
|
+
if (args.sheet !== void 0) return failure("INVALID_OPERATION", sheet);
|
|
11267
|
+
return {
|
|
11268
|
+
...base,
|
|
11269
|
+
warning: sheet,
|
|
11270
|
+
content: ""
|
|
11271
|
+
};
|
|
11272
|
+
}
|
|
11273
|
+
const csv = await target.sheets.readCsv(target.node.objToken, {
|
|
11274
|
+
sheetId: sheet.sheetId,
|
|
11275
|
+
range: args.range ?? wholeSheetRange(sheet),
|
|
11276
|
+
maxChars: args.max_chars ?? READ_DEFAULT_CHARS
|
|
11277
|
+
});
|
|
11278
|
+
return {
|
|
11279
|
+
...base,
|
|
11280
|
+
sheet_id: sheet.sheetId,
|
|
11281
|
+
...csv.actualRange === void 0 ? {} : { actual_range: csv.actualRange },
|
|
11282
|
+
...csv.currentRegion === void 0 ? {} : { current_region: csv.currentRegion },
|
|
11283
|
+
has_more: csv.hasMore,
|
|
11284
|
+
...csv.warning === void 0 ? {} : { warning: csv.warning },
|
|
11285
|
+
content: csv.csv
|
|
11286
|
+
};
|
|
10004
11287
|
});
|
|
10005
11288
|
}
|
|
10006
|
-
/**
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
*/
|
|
10013
|
-
function parseWikiRef(input) {
|
|
10014
|
-
const trimmed = input.trim();
|
|
10015
|
-
if (TOKEN_PATTERN.test(trimmed)) return {
|
|
10016
|
-
kind: "node",
|
|
10017
|
-
token: trimmed
|
|
11289
|
+
/** Rows padded with `null` (unchanged) to one rectangle. */
|
|
11290
|
+
function rectangular(values) {
|
|
11291
|
+
const columns = Math.max(...values.map((row) => row.length));
|
|
11292
|
+
return {
|
|
11293
|
+
rows: values.map((row) => [...row, ...Array(columns - row.length).fill(null)]),
|
|
11294
|
+
columns
|
|
10018
11295
|
};
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
11296
|
+
}
|
|
11297
|
+
function plan(workbook, operations) {
|
|
11298
|
+
const planned = [];
|
|
11299
|
+
let cells = 0;
|
|
11300
|
+
for (const [index, operation] of operations.entries()) {
|
|
11301
|
+
const where = `第 ${index + 1} 个操作(${operation.command})`;
|
|
11302
|
+
const sheet = pickSheet(workbook, operation.sheet);
|
|
11303
|
+
if (typeof sheet === "string") return failure("INVALID_OPERATION", `${where}:${sheet}`);
|
|
11304
|
+
let start;
|
|
11305
|
+
if (operation.command === "write") {
|
|
11306
|
+
start = parseCell(operation.start_cell);
|
|
11307
|
+
if (start === void 0) return failure("INVALID_OPERATION", `${where}:start_cell 需要单个单元格,例如 B3`);
|
|
11308
|
+
} else {
|
|
11309
|
+
const column = parseCell(`${operation.start_column ?? "A"}1`);
|
|
11310
|
+
if (column === void 0) return failure("INVALID_OPERATION", `${where}:start_column 需要列字母,例如 A`);
|
|
11311
|
+
start = column;
|
|
11312
|
+
}
|
|
11313
|
+
const { rows, columns } = rectangular(operation.values);
|
|
11314
|
+
cells += rows.length * columns;
|
|
11315
|
+
if (cells > 5e4) return failure("INVALID_OPERATION", `一次最多写入 ${WIKI_SHEET_EDIT_MAX_CELLS} 个单元格,请拆成多次调用`);
|
|
11316
|
+
planned.push({
|
|
11317
|
+
operation,
|
|
11318
|
+
sheet,
|
|
11319
|
+
start,
|
|
11320
|
+
rows,
|
|
11321
|
+
columns
|
|
11322
|
+
});
|
|
10024
11323
|
}
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
11324
|
+
return planned;
|
|
11325
|
+
}
|
|
11326
|
+
function cardLines(planned) {
|
|
11327
|
+
const lines = planned.map((write, index) => {
|
|
11328
|
+
const size = `${write.rows.length} 行 × ${write.columns} 列`;
|
|
11329
|
+
const where = write.operation.command === "write" ? `写入子表「${preview(write.sheet.title, 40)}」的 ${rectangle(write.start, write.rows.length, write.columns)}(${size})` : `在子表「${preview(write.sheet.title, 40)}」末尾追加 ${size}(从 ${columnLetters(write.start.column)} 列开始)`;
|
|
11330
|
+
const sample = write.rows.slice(0, CARD_PREVIEW_ROWS).map((row) => ` ${preview(row.map((cell) => cell === null ? "" : String(cell)).join(" | "), 80)}`);
|
|
11331
|
+
const more = write.rows.length > CARD_PREVIEW_ROWS ? [` …(共 ${write.rows.length} 行)`] : [];
|
|
11332
|
+
return [
|
|
11333
|
+
`${index + 1}. ${where}`,
|
|
11334
|
+
...sample,
|
|
11335
|
+
...more
|
|
11336
|
+
].join("\n");
|
|
11337
|
+
});
|
|
11338
|
+
return [`**将执行 ${planned.length} 个操作**`, ...lines];
|
|
11339
|
+
}
|
|
11340
|
+
/** First empty row after the sheet's data (1 for an empty sheet), or undefined when it cannot be determined. */
|
|
11341
|
+
async function appendRow(sheets, token, sheet) {
|
|
11342
|
+
const probe = await sheets.readCsv(token, {
|
|
11343
|
+
sheetId: sheet.sheetId,
|
|
11344
|
+
range: wholeSheetRange(sheet),
|
|
11345
|
+
maxChars: EXTENT_PROBE_CHARS
|
|
11346
|
+
});
|
|
11347
|
+
const region = probe.currentRegion ?? (probe.hasMore ? void 0 : probe.actualRange);
|
|
11348
|
+
let last = region === void 0 ? probe.csv.trim() === "" && !probe.hasMore ? 0 : void 0 : rangeEndRow(region);
|
|
11349
|
+
if (last === void 0) return void 0;
|
|
11350
|
+
const lastColumn = columnLetters(sheet.columnCount !== void 0 && sheet.columnCount > 0 ? sheet.columnCount : MAX_SCAN_COLUMN);
|
|
11351
|
+
const lastRow = sheet.rowCount !== void 0 && sheet.rowCount > 0 ? sheet.rowCount : MAX_SCAN_ROW;
|
|
11352
|
+
let from = last + 1;
|
|
11353
|
+
for (let page = 0; from <= lastRow; page++) {
|
|
11354
|
+
if (page === EXTENT_SCAN_PAGES) return void 0;
|
|
11355
|
+
const scan = await sheets.readCsv(token, {
|
|
11356
|
+
sheetId: sheet.sheetId,
|
|
11357
|
+
range: `A${from}:${lastColumn}${lastRow}`,
|
|
11358
|
+
maxChars: EXTENT_SCAN_CHARS
|
|
11359
|
+
});
|
|
11360
|
+
last = Math.max(last, lastFilledRow(scan.csv));
|
|
11361
|
+
if (!scan.hasMore) break;
|
|
11362
|
+
const end = scan.actualRange === void 0 ? void 0 : rangeEndRow(scan.actualRange);
|
|
11363
|
+
if (end === void 0 || end < from) return void 0;
|
|
11364
|
+
from = end + 1;
|
|
11365
|
+
}
|
|
11366
|
+
return last + 1;
|
|
11367
|
+
}
|
|
11368
|
+
/** Largest `[row=N]` whose record has a non-empty cell, or 0. */
|
|
11369
|
+
function lastFilledRow(csv) {
|
|
11370
|
+
let last = 0;
|
|
11371
|
+
for (const match of csv.matchAll(/^\[row=(\d+)\] ?(.*)$/gmu)) if (/[^,\s"]/u.test(match[2] ?? "")) last = Math.max(last, Number(match[1]));
|
|
11372
|
+
return last;
|
|
11373
|
+
}
|
|
11374
|
+
async function edit(kit, sessionId, rawArgs, signal) {
|
|
11375
|
+
const args = parseArgs(WIKI_SHEET_EDIT_TOOL_NAME, wikiSheetEditArgsSchema, rawArgs);
|
|
11376
|
+
const gate = kit.gate(sessionId, WIKI_SHEET_EDIT_TOOL_NAME);
|
|
11377
|
+
if (!gate.ok) return gate;
|
|
11378
|
+
return await guarded$1(kit, WIKI_SHEET_EDIT_TOOL_NAME, async () => {
|
|
11379
|
+
const target = await sheetTarget(kit, gate.context, args.doc);
|
|
11380
|
+
if ("ok" in target) return target;
|
|
11381
|
+
const { node, sheets } = target;
|
|
11382
|
+
if (!ownedByApp(node, target.context.botOpenId)) return failure("NOT_OWNED", `${failure("NOT_OWNED").reason}(该表格所有者:${node.owner ?? "未知"})`);
|
|
11383
|
+
const workbook = await sheets.workbook(node.objToken);
|
|
11384
|
+
if (workbook.revision !== void 0 && workbook.revision !== args.revision_id) return {
|
|
11385
|
+
...failure("REVISION_CONFLICT", `${failure("REVISION_CONFLICT").reason}(读取时 ${args.revision_id},当前 ${workbook.revision})`),
|
|
11386
|
+
revision_id: workbook.revision
|
|
10035
11387
|
};
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
11388
|
+
const planned = plan(workbook, args.operations);
|
|
11389
|
+
if (!Array.isArray(planned)) return planned;
|
|
11390
|
+
const denied = await kit.consent(sessionId, target.context, "edit", {
|
|
11391
|
+
title: node.title,
|
|
11392
|
+
url: target.url,
|
|
11393
|
+
location: await kit.locationOf(target.context.api, node),
|
|
11394
|
+
details: cardLines(planned)
|
|
11395
|
+
}, signal);
|
|
11396
|
+
if (denied !== void 0) return denied;
|
|
11397
|
+
const after = await sheets.workbook(node.objToken);
|
|
11398
|
+
if (after.revision !== void 0 && after.revision !== args.revision_id) return {
|
|
11399
|
+
...failure("REVISION_CONFLICT", `${failure("REVISION_CONFLICT").reason}(等待授权期间表格被修改)`),
|
|
11400
|
+
revision_id: after.revision
|
|
10039
11401
|
};
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
11402
|
+
const written = [];
|
|
11403
|
+
const formulaErrors = [];
|
|
11404
|
+
for (const [index, write] of planned.entries()) {
|
|
11405
|
+
if (signal.aborted) return {
|
|
11406
|
+
...failure("ABORTED", `已取消;前 ${index} 个操作已执行`),
|
|
11407
|
+
applied: index
|
|
11408
|
+
};
|
|
11409
|
+
try {
|
|
11410
|
+
let start = write.start;
|
|
11411
|
+
if (write.operation.command === "append") {
|
|
11412
|
+
const row = await appendRow(sheets, node.objToken, write.sheet);
|
|
11413
|
+
if (row === void 0) return {
|
|
11414
|
+
...failure("EDIT_FAILED", `第 ${index + 1} 个操作失败:无法确定子表「${write.sheet.title}」的数据末行,请改用 write 指定 start_cell;前 ${index} 个操作已执行`),
|
|
11415
|
+
applied: index
|
|
11416
|
+
};
|
|
11417
|
+
start = {
|
|
11418
|
+
column: write.start.column,
|
|
11419
|
+
row
|
|
11420
|
+
};
|
|
11421
|
+
}
|
|
11422
|
+
const range = rectangle(start, write.rows.length, write.columns);
|
|
11423
|
+
const result = await sheets.writeCells(node.objToken, {
|
|
11424
|
+
sheetId: write.sheet.sheetId,
|
|
11425
|
+
range,
|
|
11426
|
+
rows: write.rows,
|
|
11427
|
+
...write.operation.command === "append" ? { allowOverwrite: false } : {}
|
|
11428
|
+
});
|
|
11429
|
+
written.push({
|
|
11430
|
+
sheet_id: write.sheet.sheetId,
|
|
11431
|
+
range
|
|
11432
|
+
});
|
|
11433
|
+
for (const error of result.formulaErrors) formulaErrors.push({
|
|
11434
|
+
sheet_id: write.sheet.sheetId,
|
|
11435
|
+
cell: error.cell,
|
|
11436
|
+
...error.formula === void 0 ? {} : { formula: error.formula },
|
|
11437
|
+
...error.errorType === void 0 ? {} : { error_type: error.errorType },
|
|
11438
|
+
...error.detail === void 0 ? {} : { detail: error.detail }
|
|
11439
|
+
});
|
|
11440
|
+
} catch (error) {
|
|
11441
|
+
const reason = failureOf(error).reason;
|
|
11442
|
+
return {
|
|
11443
|
+
...failure("EDIT_FAILED", `第 ${index + 1} 个操作失败:${reason};前 ${index} 个操作已执行`),
|
|
11444
|
+
applied: index
|
|
11445
|
+
};
|
|
11446
|
+
}
|
|
11447
|
+
}
|
|
11448
|
+
const revision = await sheets.workbook(node.objToken).then((final) => final.revision, (error) => {
|
|
11449
|
+
kit.warn("dsh-channels: reading the revision after %s failed: %s", WIKI_SHEET_EDIT_TOOL_NAME, describeError(error));
|
|
11450
|
+
});
|
|
11451
|
+
return {
|
|
11452
|
+
ok: true,
|
|
11453
|
+
title: node.title,
|
|
11454
|
+
node_token: node.nodeToken,
|
|
11455
|
+
url: target.url,
|
|
11456
|
+
applied: planned.length,
|
|
11457
|
+
written,
|
|
11458
|
+
...revision === void 0 ? {} : { revision_id: revision },
|
|
11459
|
+
...formulaErrors.length === 0 ? {} : { formula_errors: formulaErrors }
|
|
11460
|
+
};
|
|
11461
|
+
});
|
|
10080
11462
|
}
|
|
11463
|
+
/** The spreadsheet tool family. */
|
|
11464
|
+
const SHEET_TOOL_SPECS = [{
|
|
11465
|
+
name: WIKI_SHEET_READ_TOOL_NAME,
|
|
11466
|
+
access: "read",
|
|
11467
|
+
description: READ_DESCRIPTION$1,
|
|
11468
|
+
parameters: READ_PARAMETERS$1,
|
|
11469
|
+
execute: read,
|
|
11470
|
+
presentCall: (args) => `读取知识库表格:${display(args.doc)}`
|
|
11471
|
+
}, {
|
|
11472
|
+
name: WIKI_SHEET_EDIT_TOOL_NAME,
|
|
11473
|
+
access: "write",
|
|
11474
|
+
description: EDIT_DESCRIPTION$1,
|
|
11475
|
+
parameters: EDIT_PARAMETERS$1,
|
|
11476
|
+
execute: edit,
|
|
11477
|
+
presentCall: (args) => `修改知识库表格:${display(args.doc)}`
|
|
11478
|
+
}];
|
|
10081
11479
|
//#endregion
|
|
10082
11480
|
//#region src/feishu/wiki/tools.ts
|
|
10083
11481
|
/**
|
|
@@ -10110,8 +11508,6 @@ const WIKI_EDIT_TOOL_NAME = "feishu_wiki_edit";
|
|
|
10110
11508
|
const MAX_TITLE_CHARS = 200;
|
|
10111
11509
|
/** Longest content one create/edit operation may carry (characters). */
|
|
10112
11510
|
const MAX_CONTENT_CHARS = 2e5;
|
|
10113
|
-
/** Characters of each content shown on a consent card. */
|
|
10114
|
-
const PREVIEW_CHARS = 300;
|
|
10115
11511
|
const refSchema = z.string().trim().min(1).max(2048);
|
|
10116
11512
|
const blockIdSchema = z.string().trim().min(1).max(128);
|
|
10117
11513
|
const contentSchema = z.string().max(MAX_CONTENT_CHARS);
|
|
@@ -10138,6 +11534,11 @@ const wikiReadArgsSchema = z.object({
|
|
|
10138
11534
|
const wikiCreateArgsSchema = z.object({
|
|
10139
11535
|
parent: refSchema,
|
|
10140
11536
|
title: z.string().trim().min(1).max(MAX_TITLE_CHARS),
|
|
11537
|
+
type: z.enum([
|
|
11538
|
+
"docx",
|
|
11539
|
+
"sheet",
|
|
11540
|
+
"bitable"
|
|
11541
|
+
]).optional(),
|
|
10141
11542
|
content: contentSchema.optional(),
|
|
10142
11543
|
format: formatSchema.optional()
|
|
10143
11544
|
}).strict();
|
|
@@ -10181,29 +11582,6 @@ const wikiEditArgsSchema = z.object({
|
|
|
10181
11582
|
format: formatSchema.optional(),
|
|
10182
11583
|
operations: z.array(wikiEditOperationSchema).min(1).max(20)
|
|
10183
11584
|
}).strict();
|
|
10184
|
-
const FAILURE_REASONS = {
|
|
10185
|
-
TOOL_DISABLED: "知识库工具已在插件配置中关闭",
|
|
10186
|
-
ROUTE_NOT_FOUND: "当前会话没有绑定飞书话题,无法确定授权人",
|
|
10187
|
-
NOT_AUTHORIZED: "当前话题的发起人不在知识库白名单中;请管理员发送 /auth wiki add @该用户 后重试",
|
|
10188
|
-
CHANNEL_UNAVAILABLE: "飞书连接当前不可用,请稍后重试",
|
|
10189
|
-
INVALID_REFERENCE: "无法识别的知识库引用:请传知识库节点链接 https://<租户>.feishu.cn/wiki/<node_token>、node_token,或知识空间链接 https://<租户>.feishu.cn/wiki/space/<space_id>",
|
|
10190
|
-
INVALID_OPERATION: "编辑操作参数不完整",
|
|
10191
|
-
OUTSIDE_ROOTS: "该位置不在插件配置的知识库范围(wikiRoots)内,不能访问",
|
|
10192
|
-
NOT_FOUND: "知识库节点不存在或已被删除",
|
|
10193
|
-
NO_ACCESS: "机器人对该知识库节点没有访问权限;请知识库管理员把应用(或包含机器人的群)加为该空间成员",
|
|
10194
|
-
UNSUPPORTED_TYPE: "目前只支持读取和修改飞书文档(docx);表格、多维表格等类型暂不支持",
|
|
10195
|
-
NOT_OWNED: "只能修改由本应用创建(所有者是本应用)的文档",
|
|
10196
|
-
REVISION_CONFLICT: "文档在你读取之后被修改过;请重新用 feishu_wiki_read 读取最新内容,基于新的 revision_id 再修改",
|
|
10197
|
-
CONSENT_DENIED: "话题发起人拒绝了这次知识库操作",
|
|
10198
|
-
CONSENT_EXPIRED: "话题发起人没有在授权卡有效期内确认,已自动拒绝",
|
|
10199
|
-
CONSENT_UNAVAILABLE: "授权卡发送失败,无法征得话题发起人同意,已取消操作",
|
|
10200
|
-
PERMISSION_DENIED: "飞书拒绝调用:应用缺少知识库或文档权限(wiki:wiki、docx:document),请管理员在开发者后台开通并发布版本",
|
|
10201
|
-
CONTENT_FAILED: "文档已创建,但写入内容失败",
|
|
10202
|
-
EDIT_FAILED: "修改失败",
|
|
10203
|
-
EDIT_PARTIAL: "飞书报告操作只部分生效;已停止后续操作,请用 feishu_wiki_read 读取最新内容核验后再继续",
|
|
10204
|
-
API_FAILED: "飞书接口调用失败",
|
|
10205
|
-
ABORTED: "已取消"
|
|
10206
|
-
};
|
|
10207
11585
|
const OUTPUT_SCHEMA = {
|
|
10208
11586
|
type: "object",
|
|
10209
11587
|
properties: {
|
|
@@ -10214,9 +11592,16 @@ const OUTPUT_SCHEMA = {
|
|
|
10214
11592
|
required: ["ok"]
|
|
10215
11593
|
};
|
|
10216
11594
|
const LIST_DESCRIPTION = "浏览飞书知识库(只能访问插件配置的知识库范围 wikiRoots)。不传 parent 时列出可访问的根(知识空间或节点);传 parent(知识库节点链接、node_token,或知识空间链接)时列出它的直接子节点,has_child 为 true 的节点可以继续往下浏览。结果分页,带 page_token 时用它取下一页。返回的 node_token 可直接传给其他知识库工具。";
|
|
10217
|
-
const READ_DESCRIPTION = "读取知识库中的一篇飞书文档(docx)。doc 传知识库节点链接或 node_token。第一次读取某篇文档会在话题里弹出授权卡,由话题发起人确认,确认后本话题内再次读取同一篇不再询问;等待期间不要重复调用。scope 默认 full(整篇);大文档先用 outline 看目录,再用 section(start_block_id 为标题块 id)、range(start/end_block_id)或 keyword(关键词,支持 a|b)读取局部;max_depth 在 outline 中表示标题层级上限。format 默认 markdown;xml 为飞书文档 XML(类 HTML 标签:<p> <h1>-<h6> <ul>/<ol><li> <pre><code> <table> <callout> 等),只有 xml 读取会给每个块带上 id 属性(block id),需要按块修改时用 xml 读取。返回的 revision_id 用于 feishu_wiki_edit;editable 表示这篇文档能否被修改(只有本应用创建的文档可以修改)。";
|
|
10218
|
-
const CREATE_DESCRIPTION = "在知识库中新建一篇飞书文档(docx)。parent 传父节点(知识库节点链接或 node_token
|
|
11595
|
+
const READ_DESCRIPTION = "读取知识库中的一篇飞书文档(docx;电子表格用 feishu_wiki_sheet_read,多维表格用 feishu_wiki_base_read)。doc 传知识库节点链接或 node_token。第一次读取某篇文档会在话题里弹出授权卡,由话题发起人确认,确认后本话题内再次读取同一篇不再询问;等待期间不要重复调用。scope 默认 full(整篇);大文档先用 outline 看目录,再用 section(start_block_id 为标题块 id)、range(start/end_block_id)或 keyword(关键词,支持 a|b)读取局部;max_depth 在 outline 中表示标题层级上限。format 默认 markdown;xml 为飞书文档 XML(类 HTML 标签:<p> <h1>-<h6> <ul>/<ol><li> <pre><code> <table> <callout> 等),只有 xml 读取会给每个块带上 id 属性(block id),需要按块修改时用 xml 读取。返回的 revision_id 用于 feishu_wiki_edit;editable 表示这篇文档能否被修改(只有本应用创建的文档可以修改)。";
|
|
11596
|
+
const CREATE_DESCRIPTION = "在知识库中新建一篇飞书文档(docx,默认)、电子表格(type=sheet)或多维表格(type=bitable)。parent 传父节点(知识库节点链接或 node_token)或知识空间链接(建在空间顶层),必须位于插件配置的知识库范围内;新节点本身也可以作为目录,之后把它的 node_token 作为 parent 在下面继续新建子节点。title 为标题;content 为文档正文(仅 docx,可选,不要在正文里重复标题),电子表格和多维表格建好后是空白的,再用 feishu_wiki_sheet_edit / feishu_wiki_base_edit 写入。format 默认 markdown;xml 为飞书文档 XML(类 HTML 标签:<p> <h1>-<h6> <ul>/<ol><li> <pre><code> <table> <callout> 等),只有 xml 读取会给每个块带上 id 属性(block id),需要按块修改时用 xml 读取。每次新建都会在话题里弹出授权卡,由话题发起人确认后才执行;等待期间不要重复调用。";
|
|
10219
11597
|
const EDIT_DESCRIPTION = `修改知识库中由本应用创建的飞书文档(docx)。先用 feishu_wiki_read 读取(按块修改时用 format=xml 拿到 block id),把读到的 revision_id 原样传入;文档在此之后被他人改过会被拒绝,需要重新读取。operations 按顺序执行(最多 20 个):append(文末追加 content)、insert_after(在 block_id 之后插入 content)、replace(用 content 替换 block_id,或同一父块下 start_block_id 到 end_block_id 的连续块)、delete(删除 block_id,或 start_block_id 到 end_block_id)、str_replace(把文本 pattern 替换为 content,content 为空表示删除)、overwrite(清空后用 content 重写整篇,会丢失评论与不支持的内容,尽量少用)。替换和删除会使涉及的旧 block id 失效,同一个块的多处修改请合并成一次 replace。format 默认 markdown;xml 为飞书文档 XML(类 HTML 标签:<p> <h1>-<h6> <ul>/<ol><li> <pre><code> <table> <callout> 等),只有 xml 读取会给每个块带上 id 属性(block id),需要按块修改时用 xml 读取。每次修改都会在话题里弹出授权卡,由话题发起人确认后才执行;等待期间不要重复调用。`;
|
|
11598
|
+
const CREATE_TYPE_LABELS = {
|
|
11599
|
+
docx: "飞书文档",
|
|
11600
|
+
sheet: "电子表格",
|
|
11601
|
+
bitable: "多维表格"
|
|
11602
|
+
};
|
|
11603
|
+
/** The spreadsheet and Base tool families, mounted with the docx tools under the same switches. */
|
|
11604
|
+
const FAMILY_SPECS = [...SHEET_TOOL_SPECS, ...BASE_TOOL_SPECS];
|
|
10220
11605
|
/**
|
|
10221
11606
|
* The wiki tool runtime: mounts the enabled tools into Agent scopes and runs
|
|
10222
11607
|
* each call through the gates in the module contract.
|
|
@@ -10234,7 +11619,16 @@ var WikiToolRuntime = class {
|
|
|
10234
11619
|
enabledTools() {
|
|
10235
11620
|
const config = this.deps.config();
|
|
10236
11621
|
if (config.wikiRoots.length === 0) return [];
|
|
10237
|
-
|
|
11622
|
+
const family = (access) => FAMILY_SPECS.filter((spec) => spec.access === access).map((spec) => spec.name);
|
|
11623
|
+
return [...config.feishuTools.wikiRead ? [
|
|
11624
|
+
WIKI_LIST_TOOL_NAME,
|
|
11625
|
+
WIKI_READ_TOOL_NAME,
|
|
11626
|
+
...family("read")
|
|
11627
|
+
] : [], ...config.feishuTools.wikiWrite ? [
|
|
11628
|
+
WIKI_CREATE_TOOL_NAME,
|
|
11629
|
+
WIKI_EDIT_TOOL_NAME,
|
|
11630
|
+
...family("write")
|
|
11631
|
+
] : []];
|
|
10238
11632
|
}
|
|
10239
11633
|
/**
|
|
10240
11634
|
* Register the enabled tools inside one Agent scope; same mount policy as
|
|
@@ -10291,6 +11685,16 @@ var WikiToolRuntime = class {
|
|
|
10291
11685
|
},
|
|
10292
11686
|
presentResult: (_args, value) => presentWikiResult(value)
|
|
10293
11687
|
};
|
|
11688
|
+
const spec = FAMILY_SPECS.find((candidate) => candidate.name === name);
|
|
11689
|
+
if (spec !== void 0) return {
|
|
11690
|
+
...base,
|
|
11691
|
+
name,
|
|
11692
|
+
description: spec.description,
|
|
11693
|
+
parameters: spec.parameters,
|
|
11694
|
+
execute: async (rawArgs, exec) => await spec.execute(this, sessionId, rawArgs, exec.signal),
|
|
11695
|
+
presentCall: (args) => spec.presentCall(args),
|
|
11696
|
+
isConcurrencySafe: () => false
|
|
11697
|
+
};
|
|
10294
11698
|
switch (name) {
|
|
10295
11699
|
case WIKI_LIST_TOOL_NAME: return {
|
|
10296
11700
|
...base,
|
|
@@ -10343,6 +11747,8 @@ var WikiToolRuntime = class {
|
|
|
10343
11747
|
context: {
|
|
10344
11748
|
route,
|
|
10345
11749
|
api,
|
|
11750
|
+
...channel.sheets === void 0 ? {} : { sheets: channel.sheets },
|
|
11751
|
+
...channel.bases === void 0 ? {} : { bases: channel.bases },
|
|
10346
11752
|
roots: parseWikiRoots(this.deps.config().wikiRoots),
|
|
10347
11753
|
botOpenId: channel.botOpenId()
|
|
10348
11754
|
}
|
|
@@ -10372,6 +11778,26 @@ var WikiToolRuntime = class {
|
|
|
10372
11778
|
return label;
|
|
10373
11779
|
}
|
|
10374
11780
|
/** `空间 / 父节点` for a node (one parent lookup). */
|
|
11781
|
+
async readConsent(sessionId, context, node, signal) {
|
|
11782
|
+
if (this.deps.consent.hasReadGrant(sessionId, node.objToken)) return void 0;
|
|
11783
|
+
const denied = await this.consent(sessionId, context, "read", {
|
|
11784
|
+
title: node.title,
|
|
11785
|
+
url: nodeUrl(context.roots, node),
|
|
11786
|
+
location: await this.locationOf(context.api, node),
|
|
11787
|
+
details: []
|
|
11788
|
+
}, signal);
|
|
11789
|
+
if (denied === void 0) this.deps.consent.grantRead(sessionId, node.objToken);
|
|
11790
|
+
return denied;
|
|
11791
|
+
}
|
|
11792
|
+
grantRead(sessionId, documentId) {
|
|
11793
|
+
this.deps.consent.grantRead(sessionId, documentId);
|
|
11794
|
+
}
|
|
11795
|
+
writeEnabled() {
|
|
11796
|
+
return this.deps.config().feishuTools.wikiWrite;
|
|
11797
|
+
}
|
|
11798
|
+
warn(message, ...args) {
|
|
11799
|
+
this.deps.warn(message, ...args);
|
|
11800
|
+
}
|
|
10375
11801
|
async locationOf(api, node) {
|
|
10376
11802
|
const space = await this.spaceName(api, node.spaceId);
|
|
10377
11803
|
if (node.parentNodeToken === void 0) return space;
|
|
@@ -10466,16 +11892,8 @@ var WikiToolRuntime = class {
|
|
|
10466
11892
|
if ("ok" in node) return node;
|
|
10467
11893
|
if (node.objType !== "docx") return failure("UNSUPPORTED_TYPE", `${FAILURE_REASONS.UNSUPPORTED_TYPE}(该节点类型:${node.objType})`);
|
|
10468
11894
|
const url = nodeUrl(context.roots, node);
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
title: node.title,
|
|
10472
|
-
url,
|
|
10473
|
-
location: await this.locationOf(context.api, node),
|
|
10474
|
-
details: []
|
|
10475
|
-
}, exec.signal);
|
|
10476
|
-
if (denied !== void 0) return denied;
|
|
10477
|
-
this.deps.consent.grantRead(sessionId, node.objToken);
|
|
10478
|
-
}
|
|
11895
|
+
const denied = await this.readConsent(sessionId, context, node, exec.signal);
|
|
11896
|
+
if (denied !== void 0) return denied;
|
|
10479
11897
|
if (exec.signal.aborted) return failure("ABORTED");
|
|
10480
11898
|
const format = args.format ?? "markdown";
|
|
10481
11899
|
const document = await context.api.fetchDocument(node.objToken, {
|
|
@@ -10523,9 +11941,11 @@ var WikiToolRuntime = class {
|
|
|
10523
11941
|
parentUrl = nodeUrl(context.roots, parent);
|
|
10524
11942
|
location = `${await this.spaceName(context.api, spaceId)} / ${parent.title === "" ? parent.nodeToken : parent.title}`;
|
|
10525
11943
|
}
|
|
11944
|
+
const type = args.type ?? "docx";
|
|
10526
11945
|
const format = args.format ?? "markdown";
|
|
10527
11946
|
const content = args.content === void 0 || args.content.trim() === "" ? void 0 : args.content;
|
|
10528
|
-
|
|
11947
|
+
if (type !== "docx" && content !== void 0) return failure("INVALID_OPERATION", "content 只用于飞书文档;新建电子表格或多维表格后,用 feishu_wiki_sheet_edit / feishu_wiki_base_edit 写入数据");
|
|
11948
|
+
const details = type !== "docx" ? [`**类型**:${CREATE_TYPE_LABELS[type]}(空白)`] : content === void 0 ? ["**内容**:空文档"] : [`**内容预览**(${format === "xml" ? "XML" : "Markdown"},共 ${content.length} 字)\n${previewBlock(content)}`];
|
|
10529
11949
|
const denied = await this.consent(sessionId, context, "create", {
|
|
10530
11950
|
title: args.title,
|
|
10531
11951
|
...parentUrl === void 0 ? {} : { url: parentUrl },
|
|
@@ -10534,9 +11954,9 @@ var WikiToolRuntime = class {
|
|
|
10534
11954
|
}, exec.signal);
|
|
10535
11955
|
if (denied !== void 0) return denied;
|
|
10536
11956
|
if (exec.signal.aborted) return failure("ABORTED");
|
|
10537
|
-
const node = await context.api.
|
|
11957
|
+
const node = await context.api.createNode(spaceId, parentToken, args.title, type);
|
|
10538
11958
|
const url = nodeUrl(context.roots, node);
|
|
10539
|
-
this.
|
|
11959
|
+
this.grantRead(sessionId, node.objToken);
|
|
10540
11960
|
if (content === void 0) return {
|
|
10541
11961
|
ok: true,
|
|
10542
11962
|
title: node.title || args.title,
|
|
@@ -10656,72 +12076,6 @@ var WikiToolRuntime = class {
|
|
|
10656
12076
|
});
|
|
10657
12077
|
}
|
|
10658
12078
|
};
|
|
10659
|
-
function failure(code, reason) {
|
|
10660
|
-
return {
|
|
10661
|
-
ok: false,
|
|
10662
|
-
code,
|
|
10663
|
-
reason: reason ?? FAILURE_REASONS[code]
|
|
10664
|
-
};
|
|
10665
|
-
}
|
|
10666
|
-
function parseArgs(tool, schema, rawArgs) {
|
|
10667
|
-
const parsed = schema.safeParse(rawArgs);
|
|
10668
|
-
if (parsed.success) return parsed.data;
|
|
10669
|
-
const issue = parsed.error.issues[0];
|
|
10670
|
-
const where = issue === void 0 || issue.path.length === 0 ? "arguments" : issue.path.join(".");
|
|
10671
|
-
throw new Error(`${tool}: invalid ${where}: ${issue?.message ?? "invalid arguments"}`);
|
|
10672
|
-
}
|
|
10673
|
-
/** Run one gated call body, mapping thrown Feishu errors to failures. */
|
|
10674
|
-
async function guarded$1(deps, tool, body) {
|
|
10675
|
-
try {
|
|
10676
|
-
return await body();
|
|
10677
|
-
} catch (error) {
|
|
10678
|
-
const mapped = failureOf(error);
|
|
10679
|
-
if (mapped.code === "API_FAILED") deps.warn("dsh-channels: %s failed: %s", tool, mapped.reason);
|
|
10680
|
-
return mapped;
|
|
10681
|
-
}
|
|
10682
|
-
}
|
|
10683
|
-
/** Feishu codes: node missing/deleted, no resource access, missing app scope. */
|
|
10684
|
-
const NOT_FOUND_CODES = /* @__PURE__ */ new Set([
|
|
10685
|
-
131005,
|
|
10686
|
-
131012,
|
|
10687
|
-
131013,
|
|
10688
|
-
131014,
|
|
10689
|
-
131016
|
|
10690
|
-
]);
|
|
10691
|
-
const NO_ACCESS_CODES = /* @__PURE__ */ new Set([
|
|
10692
|
-
131006,
|
|
10693
|
-
1770032,
|
|
10694
|
-
1770002
|
|
10695
|
-
]);
|
|
10696
|
-
const PERMISSION_CODES = /* @__PURE__ */ new Set([99991672, 99991679]);
|
|
10697
|
-
function failureOf(error) {
|
|
10698
|
-
const detail = describeError(error);
|
|
10699
|
-
if (error instanceof FeishuApiError && error.code !== void 0) {
|
|
10700
|
-
if (NOT_FOUND_CODES.has(error.code)) return failure("NOT_FOUND", `${FAILURE_REASONS.NOT_FOUND}(${detail})`);
|
|
10701
|
-
if (NO_ACCESS_CODES.has(error.code)) return failure("NO_ACCESS", `${FAILURE_REASONS.NO_ACCESS}(${detail})`);
|
|
10702
|
-
if (PERMISSION_CODES.has(error.code)) return failure("PERMISSION_DENIED", `${FAILURE_REASONS.PERMISSION_DENIED}(${detail})`);
|
|
10703
|
-
}
|
|
10704
|
-
return failure("API_FAILED", `${FAILURE_REASONS.API_FAILED}:${detail}`);
|
|
10705
|
-
}
|
|
10706
|
-
function consentFailure(outcome) {
|
|
10707
|
-
switch (outcome) {
|
|
10708
|
-
case "allowed": return;
|
|
10709
|
-
case "denied": return failure("CONSENT_DENIED");
|
|
10710
|
-
case "expired": return failure("CONSENT_EXPIRED");
|
|
10711
|
-
case "cancelled": return failure("ABORTED");
|
|
10712
|
-
case "unavailable": return failure("CONSENT_UNAVAILABLE");
|
|
10713
|
-
}
|
|
10714
|
-
}
|
|
10715
|
-
/** Whether the app owns a node: Feishu reports the bot open_id as the owner. */
|
|
10716
|
-
function ownedByApp(node, botOpenId) {
|
|
10717
|
-
return botOpenId !== void 0 && botOpenId !== "" && node.owner === botOpenId;
|
|
10718
|
-
}
|
|
10719
|
-
/** A node's web link; falls back to the first root's origin. */
|
|
10720
|
-
function nodeUrl(roots, node) {
|
|
10721
|
-
if (node.url !== void 0) return node.url;
|
|
10722
|
-
const first = roots[0];
|
|
10723
|
-
return `${first === void 0 ? "https://feishu.cn" : new URL(first.link).origin}/wiki/${node.nodeToken}`;
|
|
10724
|
-
}
|
|
10725
12079
|
/** Map one validated operation to a `docs_ai` update, or the reason it is incomplete. */
|
|
10726
12080
|
function updateRequestOf(operation, format) {
|
|
10727
12081
|
switch (operation.command) {
|
|
@@ -10796,23 +12150,6 @@ function operationLines(operations) {
|
|
|
10796
12150
|
function blockRangeLabel(operation) {
|
|
10797
12151
|
return operation.block_id !== void 0 ? `块 \`${operation.block_id}\`` : `块 \`${operation.start_block_id ?? "?"}\` 到 \`${operation.end_block_id ?? "?"}\``;
|
|
10798
12152
|
}
|
|
10799
|
-
/** One-line clipped preview with Markdown control characters neutralized. */
|
|
10800
|
-
function preview(value, max = 80) {
|
|
10801
|
-
const flat = value.replace(/\s+/gu, " ").trim().replace(/([[\]()*_`~<>])/gu, "\\$1");
|
|
10802
|
-
return flat.length <= max ? flat : `${flat.slice(0, max)}…`;
|
|
10803
|
-
}
|
|
10804
|
-
/** A fenced preview of the first characters of a content. */
|
|
10805
|
-
function previewBlock(content) {
|
|
10806
|
-
return `\`\`\`\n${(content.length <= PREVIEW_CHARS ? content : `${content.slice(0, PREVIEW_CHARS)}\n…`).replace(/```/gu, "```")}\n\`\`\``;
|
|
10807
|
-
}
|
|
10808
|
-
function display(value) {
|
|
10809
|
-
return typeof value === "string" ? preview(value, 120) : "?";
|
|
10810
|
-
}
|
|
10811
|
-
/** Readable, bounded text for a thrown value. */
|
|
10812
|
-
function describeError(error) {
|
|
10813
|
-
const cleaned = (error instanceof FeishuApiError ? error.detail : error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f]+/gu, " ").trim();
|
|
10814
|
-
return cleaned.length <= 300 ? cleaned : `${cleaned.slice(0, 300)}…`;
|
|
10815
|
-
}
|
|
10816
12153
|
function isValue(value) {
|
|
10817
12154
|
return typeof value === "object" && value !== null && typeof value.ok === "boolean";
|
|
10818
12155
|
}
|
|
@@ -10850,6 +12187,8 @@ function presentWikiResult(value) {
|
|
|
10850
12187
|
if (!isValue(value)) return "知识库工具结果不可用";
|
|
10851
12188
|
if (!value.ok) return `失败(${value.code}):${value.reason}`;
|
|
10852
12189
|
if ("items" in value) return `列出 ${value.items.length} 项`;
|
|
12190
|
+
if ("records" in value && value.records !== void 0) return `已读取《${value.title}》(${value.records.length} 条记录)`;
|
|
12191
|
+
if ("tables" in value) return `已读取《${value.title}》(${value.tables.length} 个数据表)`;
|
|
10853
12192
|
if ("content" in value) return `已读取《${value.title}》(${value.content.length} 字)`;
|
|
10854
12193
|
if ("applied" in value) return `已修改《${value.title}》(${value.applied} 个操作)`;
|
|
10855
12194
|
return `已新建《${value.title}》`;
|
|
@@ -10926,11 +12265,20 @@ const CREATE_PARAMETERS = {
|
|
|
10926
12265
|
},
|
|
10927
12266
|
title: {
|
|
10928
12267
|
type: "string",
|
|
10929
|
-
description: "
|
|
12268
|
+
description: "标题"
|
|
12269
|
+
},
|
|
12270
|
+
type: {
|
|
12271
|
+
type: "string",
|
|
12272
|
+
enum: [
|
|
12273
|
+
"docx",
|
|
12274
|
+
"sheet",
|
|
12275
|
+
"bitable"
|
|
12276
|
+
],
|
|
12277
|
+
description: "节点类型:docx 飞书文档(默认)、sheet 电子表格、bitable 多维表格"
|
|
10930
12278
|
},
|
|
10931
12279
|
content: {
|
|
10932
12280
|
type: "string",
|
|
10933
|
-
description: "
|
|
12281
|
+
description: "文档正文(仅 docx,可选)"
|
|
10934
12282
|
},
|
|
10935
12283
|
format: FORMAT_PROPERTY
|
|
10936
12284
|
},
|
|
@@ -13514,6 +14862,8 @@ const ATTACHMENT_STORE_MODES = ["native", "archive"];
|
|
|
13514
14862
|
const DEFAULT_ATTACHMENT_FETCH_MAX_BYTES = 100 * 1024 * 1024;
|
|
13515
14863
|
/** Default whole-call timeout of `feishu_attachment_fetch` (120 s). */
|
|
13516
14864
|
const DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS = 12e4;
|
|
14865
|
+
/** Largest delay Node's `setTimeout` honors (2^31 − 1 ms, about 24.8 days); longer delays fire at once. */
|
|
14866
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
13517
14867
|
/** Default per-image byte cap for trigger-message prefetch (20 MiB). */
|
|
13518
14868
|
const DEFAULT_PREFETCH_IMAGE_MAX_BYTES = 20 * 1024 * 1024;
|
|
13519
14869
|
/** Default total byte budget for one trigger-message image prefetch (50 MiB). */
|
|
@@ -13712,10 +15062,12 @@ const configSchema = z.object({
|
|
|
13712
15062
|
* fetch (1–50). A message with more attachments is refused with a readable
|
|
13713
15063
|
* reason so the model can retry per `file_key`.
|
|
13714
15064
|
* - `attachmentFetchMaxBytes`: total download budget of one tool call
|
|
13715
|
-
* (default 100 MiB). Files stream to the native attachment
|
|
13716
|
-
* limit is enforced while streaming rather than buffered
|
|
13717
|
-
* -
|
|
13718
|
-
*
|
|
15065
|
+
* (default 100 MiB, no upper bound). Files stream to the native attachment
|
|
15066
|
+
* library, so the limit is enforced while streaming rather than buffered
|
|
15067
|
+
* first, and multi-GiB files need only disk space.
|
|
15068
|
+
* - `attachmentFetchTimeoutMs`: whole-call timeout (default 120 s, at most
|
|
15069
|
+
* {@link MAX_TIMER_DELAY_MS}). On expiry the in-flight download is
|
|
15070
|
+
* cancelled and reported per attachment.
|
|
13719
15071
|
* - `topicMessages` (default on): register `feishu_topic_messages` in
|
|
13720
15072
|
* channel sessions so the model can read the quoted message / earlier
|
|
13721
15073
|
* topic messages on demand and authorize their attachments for fetch.
|
|
@@ -13735,14 +15087,21 @@ const configSchema = z.object({
|
|
|
13735
15087
|
* `im:resource` (or `im:resource:upload`) scope; without it a call returns
|
|
13736
15088
|
* a readable failure and the turn continues.
|
|
13737
15089
|
*
|
|
13738
|
-
* - `wikiRead` (default off): register `feishu_wiki_list
|
|
13739
|
-
* `feishu_wiki_read`
|
|
13740
|
-
* `
|
|
13741
|
-
* `
|
|
13742
|
-
*
|
|
13743
|
-
* `
|
|
13744
|
-
* `
|
|
13745
|
-
*
|
|
15090
|
+
* - `wikiRead` (default off): register `feishu_wiki_list`,
|
|
15091
|
+
* `feishu_wiki_read`, `feishu_wiki_sheet_read`, and
|
|
15092
|
+
* `feishu_wiki_base_read` so the model can browse the wiki nodes under
|
|
15093
|
+
* `wikiRoots` and read docx documents, spreadsheets, and Bases there.
|
|
15094
|
+
* Needs `wiki:wiki` (or `wiki:wiki:readonly`), `docx:document` (or
|
|
15095
|
+
* `docx:document:readonly`), `sheets:spreadsheet` (or a read-only
|
|
15096
|
+
* spreadsheet scope), and `bitable:app` (or a read-only Base scope).
|
|
15097
|
+
* - `wikiWrite` (default off): register `feishu_wiki_create`,
|
|
15098
|
+
* `feishu_wiki_edit`, `feishu_wiki_sheet_edit`, and
|
|
15099
|
+
* `feishu_wiki_base_edit` so the model can create docx documents,
|
|
15100
|
+
* spreadsheets, and Bases under `wikiRoots`, edit documents and
|
|
15101
|
+
* spreadsheets this app owns, and change records of Bases this app owns
|
|
15102
|
+
* (other Bases accept new records only). Needs `wiki:wiki`,
|
|
15103
|
+
* `docx:document`, `sheets:spreadsheet`, and `bitable:app` (or the
|
|
15104
|
+
* matching write-only scopes).
|
|
13746
15105
|
*
|
|
13747
15106
|
* The wiki tools act with the app identity and are mounted only while
|
|
13748
15107
|
* `wikiRoots` is non-empty. Each call is limited to topics whose owner is
|
|
@@ -13755,8 +15114,8 @@ const configSchema = z.object({
|
|
|
13755
15114
|
feishuTools: z.object({
|
|
13756
15115
|
attachmentFetch: z.boolean().default(true),
|
|
13757
15116
|
attachmentFetchMaxResources: z.number().int().min(1).max(50).default(10),
|
|
13758
|
-
attachmentFetchMaxBytes: z.number().int().min(1).
|
|
13759
|
-
attachmentFetchTimeoutMs: z.number().int().min(1e3).max(
|
|
15117
|
+
attachmentFetchMaxBytes: z.number().int().min(1).default(DEFAULT_ATTACHMENT_FETCH_MAX_BYTES),
|
|
15118
|
+
attachmentFetchTimeoutMs: z.number().int().min(1e3).max(MAX_TIMER_DELAY_MS).default(DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS),
|
|
13760
15119
|
topicMessages: z.boolean().default(true),
|
|
13761
15120
|
topicMessagesTimeoutMs: z.number().int().min(1e3).max(12e4).default(DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS),
|
|
13762
15121
|
sendFile: z.boolean().default(true),
|
|
@@ -17467,16 +18826,15 @@ const FIELD_META = {
|
|
|
17467
18826
|
},
|
|
17468
18827
|
"feishuTools.attachmentFetchMaxBytes": {
|
|
17469
18828
|
label: "单次取回字节预算(字节)",
|
|
17470
|
-
help: "一次工具调用的总下载字节预算(默认 104857600 = 100 MiB
|
|
18829
|
+
help: "一次工具调用的总下载字节预算(默认 104857600 = 100 MiB,不设上限);文件流式写入原生库,不整文件缓冲,几 GiB 的文件只占磁盘。",
|
|
17471
18830
|
min: 1,
|
|
17472
|
-
max: 1024 * 1024 * 1024,
|
|
17473
18831
|
step: 1
|
|
17474
18832
|
},
|
|
17475
18833
|
"feishuTools.attachmentFetchTimeoutMs": {
|
|
17476
18834
|
label: "单次取回超时(ms)",
|
|
17477
|
-
help: "一次 feishu_attachment_fetch 的整段超时(默认 120000 ms
|
|
18835
|
+
help: "一次 feishu_attachment_fetch 的整段超时(默认 120000 ms,最小 1000,最大 2147483647 ≈ 24.8 天);到期取消下载并逐项报告可读原因。大文件请按带宽同步调大。",
|
|
17478
18836
|
min: 1e3,
|
|
17479
|
-
max:
|
|
18837
|
+
max: MAX_TIMER_DELAY_MS,
|
|
17480
18838
|
step: 1e3
|
|
17481
18839
|
},
|
|
17482
18840
|
"feishuTools.topicMessages": {
|
|
@@ -17496,11 +18854,11 @@ const FIELD_META = {
|
|
|
17496
18854
|
},
|
|
17497
18855
|
"feishuTools.wikiRead": {
|
|
17498
18856
|
label: "启用知识库读取工具",
|
|
17499
|
-
help: "
|
|
18857
|
+
help: "让模型浏览「知识库范围」内的知识库节点,并读取 docx 文档、电子表格和多维表格(feishu_wiki_list / feishu_wiki_read / feishu_wiki_sheet_read / feishu_wiki_base_read),以应用身份访问。需开通 wiki:wiki、docx:document、sheets:spreadsheet、bitable:app 权限(或对应的只读权限)。只有话题所有者是管理员或在知识库白名单(/auth wiki add @成员)中才能使用;打开文档前会在授权卡上征得话题所有者同意,每个话题一次。知识库范围为空时不挂载。对新会话生效。"
|
|
17500
18858
|
},
|
|
17501
18859
|
"feishuTools.wikiWrite": {
|
|
17502
18860
|
label: "启用知识库写入工具",
|
|
17503
|
-
help: "让模型在「知识库范围」内新建 docx
|
|
18861
|
+
help: "让模型在「知识库范围」内新建 docx 文档、电子表格和多维表格,编辑本应用拥有的文档和电子表格,增改删本应用拥有的多维表格记录、向其他多维表格新增记录(feishu_wiki_create / feishu_wiki_edit / feishu_wiki_sheet_edit / feishu_wiki_base_edit),以应用身份访问;每次新建或编辑都需话题所有者在审批卡上同意。需开通 wiki:wiki、docx:document、sheets:spreadsheet、bitable:app 权限;使用条件同读取工具(管理员或知识库白名单)。知识库范围为空时不挂载。对新会话生效。"
|
|
17504
18862
|
},
|
|
17505
18863
|
wikiRoots: {
|
|
17506
18864
|
label: "知识库范围(wikiRoots)",
|