@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. package/index.ts +0 -75
@@ -0,0 +1,193 @@
1
+ import { wwwDomain } from "./domains.js";
2
+ import { assertLarkOk, json } from "./user-tool-helpers.js";
3
+ //#region src/sheets-shared.ts
4
+ const MAX_READ_ROWS = 200;
5
+ const MAX_WRITE_ROWS = 5e3;
6
+ const MAX_WRITE_COLS = 100;
7
+ function parseSheetUrl(url) {
8
+ try {
9
+ const parsed = new URL(url);
10
+ const match = parsed.pathname.match(/\/sheets\/([^/?#]+)/);
11
+ if (!match) return null;
12
+ return {
13
+ token: match[1],
14
+ sheetId: parsed.searchParams.get("sheet") || void 0
15
+ };
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function resolveToken(params) {
21
+ if (params.spreadsheet_token) return { token: params.spreadsheet_token };
22
+ if (params.url) return parseSheetUrl(params.url);
23
+ return null;
24
+ }
25
+ function colLetter(n) {
26
+ let result = "";
27
+ let value = n;
28
+ while (value > 0) {
29
+ value -= 1;
30
+ result = String.fromCharCode(65 + value % 26) + result;
31
+ value = Math.floor(value / 26);
32
+ }
33
+ return result;
34
+ }
35
+ function flattenCellValue(cell) {
36
+ if (!Array.isArray(cell)) return cell;
37
+ if (cell.length > 0 && cell.every((segment) => segment && typeof segment === "object" && "text" in segment)) return cell.map((segment) => String(segment.text ?? "")).join("");
38
+ return cell;
39
+ }
40
+ async function resolveSheetRange(client, token, range, sheetId) {
41
+ if (range) return range;
42
+ if (sheetId) return sheetId;
43
+ const response = await client.invoke("feishu_sheet.info", (sdk, opts) => sdk.sheets.spreadsheetSheet.query({ path: { spreadsheet_token: token } }, opts), { as: "user" });
44
+ assertLarkOk(response);
45
+ const firstSheet = response.data?.sheets?.[0];
46
+ if (!firstSheet?.sheet_id) throw new Error("Spreadsheet has no worksheets.");
47
+ return firstSheet.sheet_id;
48
+ }
49
+ async function handleCreate(client, payload) {
50
+ const createResponse = await client.invoke("feishu_sheet.create", (sdk, opts) => sdk.sheets.spreadsheet.create({ data: {
51
+ title: payload.title,
52
+ folder_token: payload.folder_token
53
+ } }, opts), { as: "user" });
54
+ assertLarkOk(createResponse);
55
+ const spreadsheet = createResponse.data?.spreadsheet;
56
+ const token = spreadsheet?.spreadsheet_token;
57
+ if (!token) return json({ error: "创建电子表格失败,未返回 spreadsheet_token。" });
58
+ const allRows = [...payload.headers ? [payload.headers] : [], ...payload.data ?? []];
59
+ if (allRows.length > 0) {
60
+ const sheetsResponse = await client.invoke("feishu_sheet.create", (sdk, opts) => sdk.sheets.spreadsheetSheet.query({ path: { spreadsheet_token: token } }, opts), { as: "user" });
61
+ assertLarkOk(sheetsResponse);
62
+ const firstSheet = sheetsResponse.data?.sheets?.[0];
63
+ if (firstSheet?.sheet_id) {
64
+ const numCols = Math.max(...allRows.map((row) => row.length), 1);
65
+ const range = `${firstSheet.sheet_id}!A1:${colLetter(numCols)}${allRows.length}`;
66
+ await client.invokeByPath("feishu_sheet.create", `/open-apis/sheets/v2/spreadsheets/${token}/values`, {
67
+ method: "PUT",
68
+ body: { valueRange: {
69
+ range,
70
+ values: allRows
71
+ } },
72
+ as: "user"
73
+ });
74
+ }
75
+ }
76
+ return json({
77
+ spreadsheet_token: token,
78
+ title: spreadsheet?.title ?? payload.title,
79
+ url: `${wwwDomain(client.account.domain)}/sheets/${token}`
80
+ });
81
+ }
82
+ async function handleInfo(client, token) {
83
+ const [spreadsheetRes, sheetsRes] = await Promise.all([client.invoke("feishu_sheet.info", (sdk, opts) => sdk.sheets.spreadsheet.get({ path: { spreadsheet_token: token } }, opts), { as: "user" }), client.invoke("feishu_sheet.info", (sdk, opts) => sdk.sheets.spreadsheetSheet.query({ path: { spreadsheet_token: token } }, opts), { as: "user" })]);
84
+ assertLarkOk(spreadsheetRes);
85
+ assertLarkOk(sheetsRes);
86
+ return json({
87
+ title: spreadsheetRes.data?.spreadsheet?.title,
88
+ spreadsheet_token: token,
89
+ url: `${wwwDomain(client.account.domain)}/sheets/${token}`,
90
+ sheets: (sheetsRes.data?.sheets ?? []).map((sheet) => ({
91
+ sheet_id: sheet.sheet_id,
92
+ title: sheet.title,
93
+ index: sheet.index,
94
+ row_count: sheet.grid_properties?.row_count,
95
+ column_count: sheet.grid_properties?.column_count
96
+ }))
97
+ });
98
+ }
99
+ async function handleRead(client, tokenInfo, payload) {
100
+ const range = await resolveSheetRange(client, tokenInfo.token, payload.range, payload.sheet_id ?? tokenInfo.sheetId);
101
+ const response = await client.invokeByPath("feishu_sheet.read", `/open-apis/sheets/v2/spreadsheets/${tokenInfo.token}/values/${encodeURIComponent(range)}`, {
102
+ method: "GET",
103
+ query: {
104
+ valueRenderOption: payload.value_render_option ?? "ToString",
105
+ dateTimeRenderOption: "FormattedString"
106
+ },
107
+ as: "user"
108
+ });
109
+ const values = (response.data?.valueRange?.values ?? []).map((row) => row.map(flattenCellValue));
110
+ return json({
111
+ range: response.data?.valueRange?.range,
112
+ values: values.slice(0, MAX_READ_ROWS),
113
+ ...values.length > MAX_READ_ROWS ? {
114
+ truncated: true,
115
+ total_rows: values.length,
116
+ hint: `结果超过 ${MAX_READ_ROWS} 行,已截断。请缩小 range 后重试。`
117
+ } : {}
118
+ });
119
+ }
120
+ async function handleWrite(client, tokenInfo, payload) {
121
+ if ((payload.values?.length ?? 0) > MAX_WRITE_ROWS) return json({ error: `写入行数超过限制 ${MAX_WRITE_ROWS}` });
122
+ if ((payload.values ?? []).some((row) => row.length > MAX_WRITE_COLS)) return json({ error: `写入列数超过限制 ${MAX_WRITE_COLS}` });
123
+ const range = await resolveSheetRange(client, tokenInfo.token, payload.range, payload.sheet_id ?? tokenInfo.sheetId);
124
+ const response = await client.invokeByPath(payload.action === "write" ? "feishu_sheet.write" : "feishu_sheet.append", `/open-apis/sheets/v2/spreadsheets/${tokenInfo.token}/${payload.action === "write" ? "values" : "values_append"}`, {
125
+ method: payload.action === "write" ? "PUT" : "POST",
126
+ body: { valueRange: {
127
+ range,
128
+ values: payload.values
129
+ } },
130
+ as: "user"
131
+ });
132
+ const updates = response.data?.updates ?? response.data;
133
+ return json({
134
+ updated_range: updates?.updatedRange,
135
+ updated_rows: updates?.updatedRows,
136
+ updated_columns: updates?.updatedColumns,
137
+ updated_cells: updates?.updatedCells,
138
+ revision: updates?.revision
139
+ });
140
+ }
141
+ async function handleFind(client, token, payload) {
142
+ const response = await client.invoke("feishu_sheet.find", (sdk, opts) => sdk.sheets.spreadsheetSheet.find({
143
+ path: {
144
+ spreadsheet_token: token,
145
+ sheet_id: payload.sheet_id
146
+ },
147
+ data: {
148
+ find: payload.find,
149
+ find_condition: {
150
+ range: payload.range ? `${payload.sheet_id}!${payload.range}` : payload.sheet_id,
151
+ ...payload.match_case !== void 0 ? { match_case: !payload.match_case } : {},
152
+ ...payload.match_entire_cell !== void 0 ? { match_entire_cell: payload.match_entire_cell } : {},
153
+ ...payload.search_by_regex !== void 0 ? { search_by_regex: payload.search_by_regex } : {},
154
+ ...payload.include_formulas !== void 0 ? { include_formulas: payload.include_formulas } : {}
155
+ }
156
+ }
157
+ }, opts), { as: "user" });
158
+ assertLarkOk(response);
159
+ return json({
160
+ matched_cells: response.data?.find_result?.matched_cells ?? [],
161
+ matched_formula_cells: response.data?.find_result?.matched_formula_cells ?? []
162
+ });
163
+ }
164
+ async function handleExport(client, token, payload) {
165
+ const exportCreate = await client.invoke("feishu_sheet.export", (sdk, opts) => sdk.drive.exportTask.create({ data: {
166
+ file_extension: payload.file_extension,
167
+ token,
168
+ type: "sheet",
169
+ sub_id: payload.sheet_id
170
+ } }, opts), { as: "user" });
171
+ assertLarkOk(exportCreate);
172
+ const ticket = exportCreate.data?.ticket;
173
+ if (!ticket) return json({ error: "导出任务创建失败,未返回 ticket。" });
174
+ for (let i = 0; i < 30; i += 1) {
175
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
176
+ const exportStatus = await client.invoke("feishu_sheet.export", (sdk, opts) => sdk.drive.exportTask.get({
177
+ path: { ticket },
178
+ params: { token }
179
+ }, opts), { as: "user" });
180
+ assertLarkOk(exportStatus);
181
+ const result = exportStatus.data?.result;
182
+ if (result?.job_status === 0) return json({
183
+ file_token: result.file_token,
184
+ file_name: result.file_name,
185
+ file_size: result.file_size,
186
+ file_extension: payload.file_extension
187
+ });
188
+ if ((result?.job_status ?? 0) >= 3) return json({ error: result?.job_error_msg ?? `导出失败 (status=${result?.job_status})` });
189
+ }
190
+ return json({ error: "导出任务超时,请稍后重试。" });
191
+ }
192
+ //#endregion
193
+ export { handleCreate, handleExport, handleFind, handleInfo, handleRead, handleWrite, resolveToken };
@@ -0,0 +1,95 @@
1
+ import { StringEnum, createToolContext, handleInvokeError, registerTool } from "./user-tool-helpers.js";
2
+ import { resolveRegisteredFeishuToolsConfig } from "./tool-account.js";
3
+ import { handleCreate, handleExport, handleFind, handleInfo, handleRead, handleWrite, resolveToken } from "./sheets-shared.js";
4
+ import { Type } from "@sinclair/typebox";
5
+ //#region src/sheets.ts
6
+ const SheetSchema = Type.Union([
7
+ Type.Object({
8
+ action: Type.Literal("info"),
9
+ url: Type.Optional(Type.String()),
10
+ spreadsheet_token: Type.Optional(Type.String())
11
+ }),
12
+ Type.Object({
13
+ action: Type.Literal("read"),
14
+ url: Type.Optional(Type.String()),
15
+ spreadsheet_token: Type.Optional(Type.String()),
16
+ range: Type.Optional(Type.String()),
17
+ sheet_id: Type.Optional(Type.String()),
18
+ value_render_option: Type.Optional(StringEnum([
19
+ "FormattedValue",
20
+ "UnformattedValue",
21
+ "Formula",
22
+ "ToString"
23
+ ]))
24
+ }),
25
+ Type.Object({
26
+ action: Type.Literal("write"),
27
+ url: Type.Optional(Type.String()),
28
+ spreadsheet_token: Type.Optional(Type.String()),
29
+ range: Type.Optional(Type.String()),
30
+ sheet_id: Type.Optional(Type.String()),
31
+ values: Type.Array(Type.Array(Type.Any()))
32
+ }),
33
+ Type.Object({
34
+ action: Type.Literal("append"),
35
+ url: Type.Optional(Type.String()),
36
+ spreadsheet_token: Type.Optional(Type.String()),
37
+ range: Type.Optional(Type.String()),
38
+ sheet_id: Type.Optional(Type.String()),
39
+ values: Type.Array(Type.Array(Type.Any()))
40
+ }),
41
+ Type.Object({
42
+ action: Type.Literal("find"),
43
+ url: Type.Optional(Type.String()),
44
+ spreadsheet_token: Type.Optional(Type.String()),
45
+ sheet_id: Type.String(),
46
+ range: Type.Optional(Type.String()),
47
+ find: Type.String(),
48
+ match_case: Type.Optional(Type.Boolean()),
49
+ match_entire_cell: Type.Optional(Type.Boolean()),
50
+ search_by_regex: Type.Optional(Type.Boolean()),
51
+ include_formulas: Type.Optional(Type.Boolean())
52
+ }),
53
+ Type.Object({
54
+ action: Type.Literal("create"),
55
+ title: Type.String(),
56
+ folder_token: Type.Optional(Type.String()),
57
+ headers: Type.Optional(Type.Array(Type.Any())),
58
+ data: Type.Optional(Type.Array(Type.Array(Type.Any())))
59
+ }),
60
+ Type.Object({
61
+ action: Type.Literal("export"),
62
+ url: Type.Optional(Type.String()),
63
+ spreadsheet_token: Type.Optional(Type.String()),
64
+ sheet_id: Type.Optional(Type.String()),
65
+ file_extension: StringEnum(["xlsx", "csv"])
66
+ })
67
+ ]);
68
+ function registerFeishuSheetsTools(api) {
69
+ if (!api.config) return;
70
+ if (!resolveRegisteredFeishuToolsConfig(api.config).sheets) return;
71
+ registerTool(api, {
72
+ name: "feishu_sheet",
73
+ label: "Feishu Sheet",
74
+ description: "按本人身份读取、写入、创建、查找和导出飞书电子表格。",
75
+ parameters: SheetSchema,
76
+ async execute(_toolCallId, params) {
77
+ const payload = params;
78
+ try {
79
+ const client = createToolContext(api, "feishu_sheet").toolClient();
80
+ if (payload.action === "create") return handleCreate(client, payload);
81
+ const tokenInfo = resolveToken(payload);
82
+ if (!tokenInfo?.token) return json({ error: "请传 spreadsheet_token 或 /sheets/TOKEN 形式的 url。" });
83
+ if (payload.action === "info") return handleInfo(client, tokenInfo.token);
84
+ if (payload.action === "read") return handleRead(client, tokenInfo, payload);
85
+ if (payload.action === "write" || payload.action === "append") return handleWrite(client, tokenInfo, payload);
86
+ if (payload.action === "find") return handleFind(client, tokenInfo.token, payload);
87
+ return handleExport(client, tokenInfo.token, payload);
88
+ } catch (error) {
89
+ return handleInvokeError(error, api);
90
+ }
91
+ }
92
+ }, { name: "feishu_sheet" });
93
+ }
94
+ //#endregion
95
+ export { registerFeishuSheetsTools };
@@ -0,0 +1,263 @@
1
+ import { fetchWithSsrFGuard } from "./nextclaw-sdk/network-fetch.js";
2
+ import "./nextclaw-sdk/feishu.js";
3
+ //#region src/streaming-card.ts
4
+ const tokenCache = /* @__PURE__ */ new Map();
5
+ function resolveApiBase(domain) {
6
+ if (domain === "lark") return "https://open.larksuite.com/open-apis";
7
+ if (domain && domain !== "feishu" && domain.startsWith("http")) return `${domain.replace(/\/+$/, "")}/open-apis`;
8
+ return "https://open.feishu.cn/open-apis";
9
+ }
10
+ function resolveAllowedHostnames(domain) {
11
+ if (domain === "lark") return ["open.larksuite.com"];
12
+ if (domain && domain !== "feishu" && domain.startsWith("http")) try {
13
+ return [new URL(domain).hostname];
14
+ } catch {
15
+ return [];
16
+ }
17
+ return ["open.feishu.cn"];
18
+ }
19
+ async function getToken(creds) {
20
+ const key = `${creds.domain ?? "feishu"}|${creds.appId}`;
21
+ const cached = tokenCache.get(key);
22
+ if (cached && cached.expiresAt > Date.now() + 6e4) return cached.token;
23
+ const { response, release } = await fetchWithSsrFGuard({
24
+ url: `${resolveApiBase(creds.domain)}/auth/v3/tenant_access_token/internal`,
25
+ init: {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({
29
+ app_id: creds.appId,
30
+ app_secret: creds.appSecret
31
+ })
32
+ },
33
+ policy: { allowedHostnames: resolveAllowedHostnames(creds.domain) },
34
+ auditContext: "feishu.streaming-card.token"
35
+ });
36
+ if (!response.ok) {
37
+ await release();
38
+ throw new Error(`Token request failed with HTTP ${response.status}`);
39
+ }
40
+ const data = await response.json();
41
+ await release();
42
+ if (data.code !== 0 || !data.tenant_access_token) throw new Error(`Token error: ${data.msg}`);
43
+ tokenCache.set(key, {
44
+ token: data.tenant_access_token,
45
+ expiresAt: Date.now() + (data.expire ?? 7200) * 1e3
46
+ });
47
+ return data.tenant_access_token;
48
+ }
49
+ function truncateSummary(text, max = 50) {
50
+ if (!text) return "";
51
+ const clean = text.replace(/\n/g, " ").trim();
52
+ return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";
53
+ }
54
+ function mergeStreamingText(previousText, nextText) {
55
+ const previous = typeof previousText === "string" ? previousText : "";
56
+ const next = typeof nextText === "string" ? nextText : "";
57
+ if (!next) return previous;
58
+ if (!previous || next === previous) return next;
59
+ if (next.startsWith(previous)) return next;
60
+ if (previous.startsWith(next)) return previous;
61
+ if (next.includes(previous)) return next;
62
+ if (previous.includes(next)) return previous;
63
+ const maxOverlap = Math.min(previous.length, next.length);
64
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) if (previous.slice(-overlap) === next.slice(0, overlap)) return `${previous}${next.slice(overlap)}`;
65
+ return `${previous}${next}`;
66
+ }
67
+ function resolveStreamingCardSendMode(options) {
68
+ if (options?.replyToMessageId) return "reply";
69
+ if (options?.rootId) return "root_create";
70
+ return "create";
71
+ }
72
+ /** Streaming card session manager */
73
+ var FeishuStreamingSession = class {
74
+ client;
75
+ creds;
76
+ state = null;
77
+ queue = Promise.resolve();
78
+ closed = false;
79
+ log;
80
+ lastUpdateTime = 0;
81
+ pendingText = null;
82
+ updateThrottleMs = 100;
83
+ constructor(client, creds, log) {
84
+ this.client = client;
85
+ this.creds = creds;
86
+ this.log = log;
87
+ }
88
+ async start(receiveId, receiveIdType = "chat_id", options) {
89
+ if (this.state) return;
90
+ const apiBase = resolveApiBase(this.creds.domain);
91
+ const cardJson = {
92
+ schema: "2.0",
93
+ config: {
94
+ streaming_mode: true,
95
+ summary: { content: "[Generating...]" },
96
+ streaming_config: {
97
+ print_frequency_ms: { default: 50 },
98
+ print_step: { default: 1 }
99
+ }
100
+ },
101
+ body: { elements: [{
102
+ tag: "markdown",
103
+ content: "⏳ Thinking...",
104
+ element_id: "content"
105
+ }] }
106
+ };
107
+ if (options?.header) cardJson.header = {
108
+ title: {
109
+ tag: "plain_text",
110
+ content: options.header.title
111
+ },
112
+ template: options.header.template ?? "blue"
113
+ };
114
+ const { response: createRes, release: releaseCreate } = await fetchWithSsrFGuard({
115
+ url: `${apiBase}/cardkit/v1/cards`,
116
+ init: {
117
+ method: "POST",
118
+ headers: {
119
+ Authorization: `Bearer ${await getToken(this.creds)}`,
120
+ "Content-Type": "application/json"
121
+ },
122
+ body: JSON.stringify({
123
+ type: "card_json",
124
+ data: JSON.stringify(cardJson)
125
+ })
126
+ },
127
+ policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
128
+ auditContext: "feishu.streaming-card.create"
129
+ });
130
+ if (!createRes.ok) {
131
+ await releaseCreate();
132
+ throw new Error(`Create card request failed with HTTP ${createRes.status}`);
133
+ }
134
+ const createData = await createRes.json();
135
+ await releaseCreate();
136
+ if (createData.code !== 0 || !createData.data?.card_id) throw new Error(`Create card failed: ${createData.msg}`);
137
+ const cardId = createData.data.card_id;
138
+ const cardContent = JSON.stringify({
139
+ type: "card",
140
+ data: { card_id: cardId }
141
+ });
142
+ let sendRes;
143
+ const sendOptions = options ?? {};
144
+ const sendMode = resolveStreamingCardSendMode(sendOptions);
145
+ if (sendMode === "reply") sendRes = await this.client.im.message.reply({
146
+ path: { message_id: sendOptions.replyToMessageId },
147
+ data: {
148
+ msg_type: "interactive",
149
+ content: cardContent,
150
+ ...sendOptions.replyInThread ? { reply_in_thread: true } : {}
151
+ }
152
+ });
153
+ else if (sendMode === "root_create") sendRes = await this.client.im.message.create({
154
+ params: { receive_id_type: receiveIdType },
155
+ data: Object.assign({
156
+ receive_id: receiveId,
157
+ msg_type: "interactive",
158
+ content: cardContent
159
+ }, { root_id: sendOptions.rootId })
160
+ });
161
+ else sendRes = await this.client.im.message.create({
162
+ params: { receive_id_type: receiveIdType },
163
+ data: {
164
+ receive_id: receiveId,
165
+ msg_type: "interactive",
166
+ content: cardContent
167
+ }
168
+ });
169
+ if (sendRes.code !== 0 || !sendRes.data?.message_id) throw new Error(`Send card failed: ${sendRes.msg}`);
170
+ this.state = {
171
+ cardId,
172
+ messageId: sendRes.data.message_id,
173
+ sequence: 1,
174
+ currentText: ""
175
+ };
176
+ this.log?.(`Started streaming: cardId=${cardId}, messageId=${sendRes.data.message_id}`);
177
+ }
178
+ async updateCardContent(text, onError) {
179
+ if (!this.state) return;
180
+ const apiBase = resolveApiBase(this.creds.domain);
181
+ this.state.sequence += 1;
182
+ await fetchWithSsrFGuard({
183
+ url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/elements/content/content`,
184
+ init: {
185
+ method: "PUT",
186
+ headers: {
187
+ Authorization: `Bearer ${await getToken(this.creds)}`,
188
+ "Content-Type": "application/json"
189
+ },
190
+ body: JSON.stringify({
191
+ content: text,
192
+ sequence: this.state.sequence,
193
+ uuid: `s_${this.state.cardId}_${this.state.sequence}`
194
+ })
195
+ },
196
+ policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
197
+ auditContext: "feishu.streaming-card.update"
198
+ }).then(async ({ release }) => {
199
+ await release();
200
+ }).catch((error) => onError?.(error));
201
+ }
202
+ async update(text) {
203
+ if (!this.state || this.closed) return;
204
+ const mergedInput = mergeStreamingText(this.pendingText ?? this.state.currentText, text);
205
+ if (!mergedInput || mergedInput === this.state.currentText) return;
206
+ const now = Date.now();
207
+ if (now - this.lastUpdateTime < this.updateThrottleMs) {
208
+ this.pendingText = mergedInput;
209
+ return;
210
+ }
211
+ this.pendingText = null;
212
+ this.lastUpdateTime = now;
213
+ this.queue = this.queue.then(async () => {
214
+ if (!this.state || this.closed) return;
215
+ const mergedText = mergeStreamingText(this.state.currentText, mergedInput);
216
+ if (!mergedText || mergedText === this.state.currentText) return;
217
+ this.state.currentText = mergedText;
218
+ await this.updateCardContent(mergedText, (e) => this.log?.(`Update failed: ${String(e)}`));
219
+ });
220
+ await this.queue;
221
+ }
222
+ async close(finalText) {
223
+ if (!this.state || this.closed) return;
224
+ this.closed = true;
225
+ await this.queue;
226
+ const pendingMerged = mergeStreamingText(this.state.currentText, this.pendingText ?? void 0);
227
+ const text = finalText ? mergeStreamingText(pendingMerged, finalText) : pendingMerged;
228
+ const apiBase = resolveApiBase(this.creds.domain);
229
+ if (text && text !== this.state.currentText) {
230
+ await this.updateCardContent(text);
231
+ this.state.currentText = text;
232
+ }
233
+ this.state.sequence += 1;
234
+ await fetchWithSsrFGuard({
235
+ url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
236
+ init: {
237
+ method: "PATCH",
238
+ headers: {
239
+ Authorization: `Bearer ${await getToken(this.creds)}`,
240
+ "Content-Type": "application/json; charset=utf-8"
241
+ },
242
+ body: JSON.stringify({
243
+ settings: JSON.stringify({ config: {
244
+ streaming_mode: false,
245
+ summary: { content: truncateSummary(text) }
246
+ } }),
247
+ sequence: this.state.sequence,
248
+ uuid: `c_${this.state.cardId}_${this.state.sequence}`
249
+ })
250
+ },
251
+ policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
252
+ auditContext: "feishu.streaming-card.close"
253
+ }).then(async ({ release }) => {
254
+ await release();
255
+ }).catch((e) => this.log?.(`Close failed: ${String(e)}`));
256
+ this.log?.(`Closed streaming: cardId=${this.state.cardId}`);
257
+ }
258
+ isActive() {
259
+ return this.state !== null && !this.closed;
260
+ }
261
+ };
262
+ //#endregion
263
+ export { FeishuStreamingSession, mergeStreamingText };
@@ -0,0 +1,39 @@
1
+ //#region src/targets.ts
2
+ const CHAT_ID_PREFIX = "oc_";
3
+ const OPEN_ID_PREFIX = "ou_";
4
+ function stripProviderPrefix(raw) {
5
+ return raw.replace(/^(feishu|lark):/i, "").trim();
6
+ }
7
+ function normalizeFeishuTarget(raw) {
8
+ const trimmed = raw.trim();
9
+ if (!trimmed) return null;
10
+ const withoutProvider = stripProviderPrefix(trimmed);
11
+ const lowered = withoutProvider.toLowerCase();
12
+ if (lowered.startsWith("chat:")) return withoutProvider.slice(5).trim() || null;
13
+ if (lowered.startsWith("group:")) return withoutProvider.slice(6).trim() || null;
14
+ if (lowered.startsWith("channel:")) return withoutProvider.slice(8).trim() || null;
15
+ if (lowered.startsWith("user:")) return withoutProvider.slice(5).trim() || null;
16
+ if (lowered.startsWith("dm:")) return withoutProvider.slice(3).trim() || null;
17
+ if (lowered.startsWith("open_id:")) return withoutProvider.slice(8).trim() || null;
18
+ return withoutProvider;
19
+ }
20
+ function resolveReceiveIdType(id) {
21
+ const trimmed = id.trim();
22
+ const lowered = trimmed.toLowerCase();
23
+ if (lowered.startsWith("chat:") || lowered.startsWith("group:") || lowered.startsWith("channel:")) return "chat_id";
24
+ if (lowered.startsWith("open_id:")) return "open_id";
25
+ if (lowered.startsWith("user:") || lowered.startsWith("dm:")) return trimmed.replace(/^(user|dm):/i, "").trim().startsWith(OPEN_ID_PREFIX) ? "open_id" : "user_id";
26
+ if (trimmed.startsWith(CHAT_ID_PREFIX)) return "chat_id";
27
+ if (trimmed.startsWith(OPEN_ID_PREFIX)) return "open_id";
28
+ return "user_id";
29
+ }
30
+ function looksLikeFeishuId(raw) {
31
+ const trimmed = stripProviderPrefix(raw.trim());
32
+ if (!trimmed) return false;
33
+ if (/^(chat|group|channel|user|dm|open_id):/i.test(trimmed)) return true;
34
+ if (trimmed.startsWith(CHAT_ID_PREFIX)) return true;
35
+ if (trimmed.startsWith(OPEN_ID_PREFIX)) return true;
36
+ return false;
37
+ }
38
+ //#endregion
39
+ export { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType };
@@ -0,0 +1,76 @@
1
+ import { StringEnum, assertLarkOk, createToolContext, handleInvokeError, json, registerTool } from "./user-tool-helpers.js";
2
+ import { Type } from "@sinclair/typebox";
3
+ //#region src/task-comment.ts
4
+ const CommentSchema = Type.Union([
5
+ Type.Object({
6
+ action: Type.Literal("create"),
7
+ task_guid: Type.String(),
8
+ content: Type.String(),
9
+ reply_to_comment_id: Type.Optional(Type.String())
10
+ }),
11
+ Type.Object({
12
+ action: Type.Literal("list"),
13
+ resource_id: Type.String(),
14
+ direction: Type.Optional(StringEnum(["asc", "desc"])),
15
+ page_size: Type.Optional(Type.Number()),
16
+ page_token: Type.Optional(Type.String())
17
+ }),
18
+ Type.Object({
19
+ action: Type.Literal("get"),
20
+ comment_id: Type.String()
21
+ })
22
+ ]);
23
+ function registerFeishuTaskCommentTool(api) {
24
+ const { toolClient } = createToolContext(api, "feishu_task_comment");
25
+ registerTool(api, {
26
+ name: "feishu_task_comment",
27
+ label: "Feishu Task Comment",
28
+ description: "按本人身份创建、获取、列出任务评论。",
29
+ parameters: CommentSchema,
30
+ async execute(_toolCallId, params) {
31
+ const payload = params;
32
+ try {
33
+ const client = toolClient();
34
+ if (payload.action === "create") {
35
+ const response = await client.invoke("feishu_task_comment.create", (sdk, opts) => sdk.task.v2.comment.create({
36
+ params: { user_id_type: "open_id" },
37
+ data: {
38
+ content: payload.content,
39
+ resource_type: "task",
40
+ resource_id: payload.task_guid,
41
+ reply_to_comment_id: payload.reply_to_comment_id
42
+ }
43
+ }, opts), { as: "user" });
44
+ assertLarkOk(response);
45
+ return json({ comment: response.data?.comment });
46
+ }
47
+ if (payload.action === "get") {
48
+ const response = await client.invoke("feishu_task_comment.get", (sdk, opts) => sdk.task.v2.comment.get({
49
+ path: { comment_id: payload.comment_id },
50
+ params: { user_id_type: "open_id" }
51
+ }, opts), { as: "user" });
52
+ assertLarkOk(response);
53
+ return json({ comment: response.data?.comment });
54
+ }
55
+ const response = await client.invoke("feishu_task_comment.list", (sdk, opts) => sdk.task.v2.comment.list({ params: {
56
+ resource_type: "task",
57
+ resource_id: payload.resource_id,
58
+ direction: payload.direction,
59
+ page_size: payload.page_size,
60
+ page_token: payload.page_token,
61
+ user_id_type: "open_id"
62
+ } }, opts), { as: "user" });
63
+ assertLarkOk(response);
64
+ return json({
65
+ comments: response.data?.items ?? [],
66
+ has_more: response.data?.has_more ?? false,
67
+ page_token: response.data?.page_token
68
+ });
69
+ } catch (error) {
70
+ return handleInvokeError(error, api);
71
+ }
72
+ }
73
+ }, { name: "feishu_task_comment" });
74
+ }
75
+ //#endregion
76
+ export { registerFeishuTaskCommentTool };
@@ -0,0 +1,13 @@
1
+ import { parseTimeToTimestampMs } from "./user-tool-helpers.js";
2
+ //#region src/task-shared.ts
3
+ function normalizeTaskTime(input) {
4
+ if (!input) return void 0;
5
+ const ts = parseTimeToTimestampMs(input.timestamp);
6
+ if (!ts) throw new Error("任务时间字段必须是带时区的 ISO 8601 / RFC 3339 时间。");
7
+ return {
8
+ timestamp: ts,
9
+ is_all_day: input.is_all_day ?? false
10
+ };
11
+ }
12
+ //#endregion
13
+ export { normalizeTaskTime };