@bobotu/feishu-fork 0.1.0
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/LICENSE +21 -0
- package/README.md +922 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +72 -0
- package/skills/feishu-doc/SKILL.md +161 -0
- package/skills/feishu-doc/references/block-types.md +102 -0
- package/skills/feishu-drive/SKILL.md +96 -0
- package/skills/feishu-perm/SKILL.md +90 -0
- package/skills/feishu-task/SKILL.md +210 -0
- package/skills/feishu-wiki/SKILL.md +96 -0
- package/src/accounts.ts +140 -0
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bot.ts +1125 -0
- package/src/channel.ts +334 -0
- package/src/client.ts +114 -0
- package/src/config-schema.ts +237 -0
- package/src/dedup.ts +54 -0
- package/src/directory.ts +165 -0
- package/src/doc-tools/actions.ts +341 -0
- package/src/doc-tools/common.ts +33 -0
- package/src/doc-tools/index.ts +2 -0
- package/src/doc-tools/register.ts +90 -0
- package/src/doc-tools/schemas.ts +85 -0
- package/src/doc-write-service.ts +711 -0
- package/src/drive-tools/actions.ts +182 -0
- package/src/drive-tools/common.ts +18 -0
- package/src/drive-tools/index.ts +2 -0
- package/src/drive-tools/register.ts +71 -0
- package/src/drive-tools/schemas.ts +67 -0
- package/src/dynamic-agent.ts +135 -0
- package/src/external-keys.ts +19 -0
- package/src/media.ts +510 -0
- package/src/mention.ts +121 -0
- package/src/monitor.ts +323 -0
- package/src/onboarding.ts +449 -0
- package/src/outbound.ts +40 -0
- package/src/perm-tools/actions.ts +111 -0
- package/src/perm-tools/common.ts +18 -0
- package/src/perm-tools/index.ts +2 -0
- package/src/perm-tools/register.ts +65 -0
- package/src/perm-tools/schemas.ts +52 -0
- package/src/policy.ts +117 -0
- package/src/probe.ts +147 -0
- package/src/reactions.ts +160 -0
- package/src/reply-dispatcher.ts +240 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +391 -0
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +58 -0
- package/src/task-tools/actions.ts +590 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/constants.ts +13 -0
- package/src/task-tools/index.ts +1 -0
- package/src/task-tools/register.ts +263 -0
- package/src/task-tools/schemas.ts +567 -0
- package/src/text/markdown-links.ts +104 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +79 -0
- package/src/typing.ts +75 -0
- package/src/wiki-tools/actions.ts +166 -0
- package/src/wiki-tools/common.ts +18 -0
- package/src/wiki-tools/index.ts +2 -0
- package/src/wiki-tools/register.ts +66 -0
- package/src/wiki-tools/schemas.ts +55 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal response shape shared by Feishu OpenAPI endpoints.
|
|
3
|
+
* Most endpoints return success when `code` is `0` (or omitted).
|
|
4
|
+
*/
|
|
5
|
+
export type FeishuApiResponse = {
|
|
6
|
+
code?: number;
|
|
7
|
+
msg?: string;
|
|
8
|
+
log_id?: string;
|
|
9
|
+
logId?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type FeishuErrorInfo = {
|
|
13
|
+
code?: number;
|
|
14
|
+
msg?: string;
|
|
15
|
+
logId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type RunFeishuApiCallOptions = {
|
|
19
|
+
/** Feishu error codes that should be treated as transient and retried. */
|
|
20
|
+
retryableCodes?: Iterable<number>;
|
|
21
|
+
/** Retry delays in milliseconds. Number of entries controls retry attempts. */
|
|
22
|
+
backoffMs?: number[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard tool result payload:
|
|
27
|
+
* - `content` for model-visible text output
|
|
28
|
+
* - `details` for structured downstream access
|
|
29
|
+
*/
|
|
30
|
+
export function json(data: unknown) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
33
|
+
details: data,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Convert any thrown value into the standard JSON error envelope. */
|
|
38
|
+
export function errorResult(err: unknown) {
|
|
39
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Small async sleep utility used by retry backoff. */
|
|
43
|
+
function sleep(ms: number) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract Feishu error fields (`code`, `msg`, `log_id`) from different throw shapes.
|
|
49
|
+
* Handles nested SDK error arrays and axios-style `response.data`.
|
|
50
|
+
*/
|
|
51
|
+
function extractFeishuErrorInfo(err: unknown): FeishuErrorInfo | null {
|
|
52
|
+
if (!err) return null;
|
|
53
|
+
|
|
54
|
+
// Feishu SDK may throw nested array structures like:
|
|
55
|
+
// [axiosError, { code, msg, log_id, ... }]
|
|
56
|
+
if (Array.isArray(err)) {
|
|
57
|
+
for (let i = err.length - 1; i >= 0; i -= 1) {
|
|
58
|
+
const info = extractFeishuErrorInfo(err[i]);
|
|
59
|
+
if (info) return info;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof err !== "object") return null;
|
|
65
|
+
|
|
66
|
+
const obj = err as Record<string, unknown>;
|
|
67
|
+
const codeValue = obj.code;
|
|
68
|
+
const msgValue = obj.msg ?? obj.message;
|
|
69
|
+
const logIdValue = obj.log_id ?? obj.logId;
|
|
70
|
+
|
|
71
|
+
const hasCode = typeof codeValue === "number";
|
|
72
|
+
const hasMsg = typeof msgValue === "string";
|
|
73
|
+
const hasLogId = typeof logIdValue === "string";
|
|
74
|
+
|
|
75
|
+
if (hasCode || hasMsg || hasLogId) {
|
|
76
|
+
return {
|
|
77
|
+
code: hasCode ? codeValue : undefined,
|
|
78
|
+
msg: hasMsg ? (msgValue as string) : undefined,
|
|
79
|
+
logId: hasLogId ? (logIdValue as string) : undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const responseData = (obj.response as { data?: unknown } | undefined)?.data;
|
|
84
|
+
if (responseData) return extractFeishuErrorInfo(responseData);
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertFeishuOk<T extends FeishuApiResponse>(response: T, context: string): T {
|
|
90
|
+
if (response.code === undefined || response.code === 0) return response;
|
|
91
|
+
|
|
92
|
+
const message = response.msg || `code ${response.code}`;
|
|
93
|
+
const detail = response.log_id ?? response.logId;
|
|
94
|
+
const error = new Error(
|
|
95
|
+
detail
|
|
96
|
+
? `${context} failed: ${message}, code=${response.code}, log_id=${detail}`
|
|
97
|
+
: `${context} failed: ${message}, code=${response.code}`,
|
|
98
|
+
) as Error & { code?: number; log_id?: string; logId?: string };
|
|
99
|
+
error.code = response.code;
|
|
100
|
+
if (detail) {
|
|
101
|
+
error.log_id = detail;
|
|
102
|
+
error.logId = detail;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize unknown errors to a readable, context-aware Error message.
|
|
109
|
+
* Preserves Feishu `code/log_id` details when available.
|
|
110
|
+
*/
|
|
111
|
+
function toError(err: unknown, context: string): Error {
|
|
112
|
+
if (err instanceof Error) {
|
|
113
|
+
const info = extractFeishuErrorInfo(err);
|
|
114
|
+
if (!info) return err;
|
|
115
|
+
const details = [
|
|
116
|
+
info.msg || `code ${info.code}`,
|
|
117
|
+
info.code !== undefined ? `code=${info.code}` : undefined,
|
|
118
|
+
info.logId ? `log_id=${info.logId}` : undefined,
|
|
119
|
+
]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join(", ");
|
|
122
|
+
return new Error(`${context} failed: ${details}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const info = extractFeishuErrorInfo(err);
|
|
126
|
+
if (info) {
|
|
127
|
+
const details = [
|
|
128
|
+
info.msg || `code ${info.code}`,
|
|
129
|
+
info.code !== undefined ? `code=${info.code}` : undefined,
|
|
130
|
+
info.logId ? `log_id=${info.logId}` : undefined,
|
|
131
|
+
]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(", ");
|
|
134
|
+
return new Error(`${context} failed: ${details}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new Error(`${context} failed: ${String(err)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execute a Feishu API call with shared success/error handling.
|
|
142
|
+
*
|
|
143
|
+
* Behavior:
|
|
144
|
+
* - Treats `code === 0` (or undefined) as success.
|
|
145
|
+
* - Converts non-zero responses and thrown values into normalized Errors.
|
|
146
|
+
* - Optionally retries only for configured transient error codes.
|
|
147
|
+
*
|
|
148
|
+
* Retry model:
|
|
149
|
+
* - Attempts = `backoffMs.length + 1`
|
|
150
|
+
* - Delay before each retry uses the corresponding `backoffMs` entry.
|
|
151
|
+
*/
|
|
152
|
+
export async function runFeishuApiCall<T extends FeishuApiResponse>(
|
|
153
|
+
context: string,
|
|
154
|
+
fn: () => Promise<T>,
|
|
155
|
+
options?: RunFeishuApiCallOptions,
|
|
156
|
+
): Promise<T> {
|
|
157
|
+
const retryableCodes = new Set(options?.retryableCodes ?? []);
|
|
158
|
+
const backoffMs = options?.backoffMs ?? [];
|
|
159
|
+
const maxAttempts = backoffMs.length + 1;
|
|
160
|
+
let attempt = 0;
|
|
161
|
+
let lastErr: unknown = null;
|
|
162
|
+
|
|
163
|
+
while (attempt < maxAttempts) {
|
|
164
|
+
try {
|
|
165
|
+
const response = await fn();
|
|
166
|
+
return assertFeishuOk(response, context);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
lastErr = err;
|
|
169
|
+
const info = extractFeishuErrorInfo(err);
|
|
170
|
+
const retryable =
|
|
171
|
+
retryableCodes.size > 0 && info?.code !== undefined && retryableCodes.has(info.code);
|
|
172
|
+
const exhausted = attempt >= maxAttempts - 1;
|
|
173
|
+
if (!retryable || exhausted) {
|
|
174
|
+
throw toError(err, context);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)];
|
|
178
|
+
await sleep(waitMs);
|
|
179
|
+
attempt += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw toError(lastErr, context);
|
|
184
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
export type FeishuToolContext = {
|
|
4
|
+
channel: "feishu";
|
|
5
|
+
accountId: string;
|
|
6
|
+
sessionKey?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const toolContextStorage = new AsyncLocalStorage<FeishuToolContext>();
|
|
10
|
+
|
|
11
|
+
export function runWithFeishuToolContext<T>(
|
|
12
|
+
context: FeishuToolContext,
|
|
13
|
+
fn: () => T,
|
|
14
|
+
): T {
|
|
15
|
+
// Propagate the active Feishu account through async boundaries so tool execution
|
|
16
|
+
// can resolve the correct account without changing OpenClaw core APIs.
|
|
17
|
+
return toolContextStorage.run(context, fn);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCurrentFeishuToolContext(): FeishuToolContext | undefined {
|
|
21
|
+
// Returns undefined when execution is outside a message-dispatch context.
|
|
22
|
+
return toolContextStorage.getStore();
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import type { ClawdbotConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import {
|
|
4
|
+
listEnabledFeishuAccounts,
|
|
5
|
+
resolveDefaultFeishuAccountId,
|
|
6
|
+
resolveFeishuAccount,
|
|
7
|
+
} from "../accounts.js";
|
|
8
|
+
import { createFeishuClient } from "../client.js";
|
|
9
|
+
import { resolveToolsConfig } from "../tools-config.js";
|
|
10
|
+
import { getCurrentFeishuToolContext } from "./tool-context.js";
|
|
11
|
+
import type { FeishuToolsConfig, ResolvedFeishuAccount } from "../types.js";
|
|
12
|
+
|
|
13
|
+
export type FeishuToolFlag = keyof Required<FeishuToolsConfig>;
|
|
14
|
+
|
|
15
|
+
export function hasFeishuToolEnabledForAnyAccount(
|
|
16
|
+
cfg: ClawdbotConfig,
|
|
17
|
+
requiredTool?: FeishuToolFlag,
|
|
18
|
+
): boolean {
|
|
19
|
+
// Tool registration is global (one definition), so we only need to know whether
|
|
20
|
+
// at least one enabled account can use the tool.
|
|
21
|
+
const accounts = listEnabledFeishuAccounts(cfg);
|
|
22
|
+
if (accounts.length === 0) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (!requiredTool) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return accounts.some((account) => resolveToolsConfig(account.config.tools)[requiredTool]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveToolAccount(cfg: ClawdbotConfig): ResolvedFeishuAccount {
|
|
32
|
+
const context = getCurrentFeishuToolContext();
|
|
33
|
+
if (context?.channel === "feishu" && context.accountId) {
|
|
34
|
+
// Message-driven path: use the account from AsyncLocalStorage context.
|
|
35
|
+
return resolveFeishuAccount({ cfg, accountId: context.accountId });
|
|
36
|
+
}
|
|
37
|
+
// Non-session path (e.g. background/manual invocation): fall back to default account.
|
|
38
|
+
return resolveFeishuAccount({ cfg, accountId: resolveDefaultFeishuAccountId(cfg) });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function withFeishuToolClient<T>(params: {
|
|
42
|
+
api: OpenClawPluginApi;
|
|
43
|
+
toolName: string;
|
|
44
|
+
requiredTool?: FeishuToolFlag;
|
|
45
|
+
run: (args: { client: Lark.Client; account: ResolvedFeishuAccount }) => Promise<T>;
|
|
46
|
+
}): Promise<T> {
|
|
47
|
+
if (!params.api.config) {
|
|
48
|
+
throw new Error("Feishu config is not available");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Resolve account at execution time (not registration time).
|
|
52
|
+
const account = resolveToolAccount(params.api.config);
|
|
53
|
+
|
|
54
|
+
if (!account.enabled) {
|
|
55
|
+
throw new Error(`Feishu account "${account.accountId}" is disabled`);
|
|
56
|
+
}
|
|
57
|
+
if (!account.configured) {
|
|
58
|
+
throw new Error(`Feishu account "${account.accountId}" is not configured`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (params.requiredTool) {
|
|
62
|
+
// Enforce per-account tool toggles, even though the tool is registered globally.
|
|
63
|
+
const toolsCfg = resolveToolsConfig(account.config.tools);
|
|
64
|
+
if (!toolsCfg[params.requiredTool]) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Feishu tool "${params.toolName}" is disabled for account "${account.accountId}"`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const client = createFeishuClient(account);
|
|
72
|
+
return params.run({ client, account });
|
|
73
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FeishuToolsConfig } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default tool configuration.
|
|
5
|
+
* - doc, wiki, drive, scopes, task: enabled by default
|
|
6
|
+
* - perm: disabled by default (sensitive operation)
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
|
9
|
+
doc: true,
|
|
10
|
+
wiki: true,
|
|
11
|
+
drive: true,
|
|
12
|
+
perm: false,
|
|
13
|
+
scopes: true,
|
|
14
|
+
task: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve tools config with defaults.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveToolsConfig(cfg?: FeishuToolsConfig): Required<FeishuToolsConfig> {
|
|
21
|
+
return { ...DEFAULT_TOOLS_CONFIG, ...cfg };
|
|
22
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { FeishuConfigSchema, FeishuGroupSchema, FeishuAccountConfigSchema, z } from "./config-schema.js";
|
|
2
|
+
import type { MentionTarget } from "./mention.js";
|
|
3
|
+
|
|
4
|
+
export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
|
|
5
|
+
export type FeishuGroupConfig = z.infer<typeof FeishuGroupSchema>;
|
|
6
|
+
export type FeishuAccountConfig = z.infer<typeof FeishuAccountConfigSchema>;
|
|
7
|
+
|
|
8
|
+
export type FeishuDomain = "feishu" | "lark" | (string & {});
|
|
9
|
+
export type FeishuConnectionMode = "websocket" | "webhook";
|
|
10
|
+
|
|
11
|
+
export type ResolvedFeishuAccount = {
|
|
12
|
+
accountId: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
configured: boolean;
|
|
15
|
+
name?: string;
|
|
16
|
+
appId?: string;
|
|
17
|
+
appSecret?: string;
|
|
18
|
+
encryptKey?: string;
|
|
19
|
+
verificationToken?: string;
|
|
20
|
+
domain: FeishuDomain;
|
|
21
|
+
/** Merged config (top-level defaults + account-specific overrides) */
|
|
22
|
+
config: FeishuConfig;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type FeishuIdType = "open_id" | "user_id" | "union_id" | "chat_id";
|
|
26
|
+
|
|
27
|
+
export type FeishuMessageContext = {
|
|
28
|
+
chatId: string;
|
|
29
|
+
messageId: string;
|
|
30
|
+
senderId: string;
|
|
31
|
+
senderOpenId: string;
|
|
32
|
+
senderName?: string;
|
|
33
|
+
chatType: "p2p" | "group";
|
|
34
|
+
mentionedBot: boolean;
|
|
35
|
+
hasAnyMention?: boolean;
|
|
36
|
+
rootId?: string;
|
|
37
|
+
parentId?: string;
|
|
38
|
+
content: string;
|
|
39
|
+
contentType: string;
|
|
40
|
+
/** Mention forward targets (excluding the bot itself) */
|
|
41
|
+
mentionTargets?: MentionTarget[];
|
|
42
|
+
/** Extracted message body (after removing @ placeholders) */
|
|
43
|
+
mentionMessageBody?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type FeishuSendResult = {
|
|
47
|
+
messageId: string;
|
|
48
|
+
chatId: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type FeishuProbeResult = {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
error?: string;
|
|
54
|
+
appId?: string;
|
|
55
|
+
botName?: string;
|
|
56
|
+
botOpenId?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type FeishuMediaInfo = {
|
|
60
|
+
path: string;
|
|
61
|
+
contentType?: string;
|
|
62
|
+
placeholder: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type FeishuToolsConfig = {
|
|
66
|
+
doc?: boolean;
|
|
67
|
+
wiki?: boolean;
|
|
68
|
+
drive?: boolean;
|
|
69
|
+
perm?: boolean;
|
|
70
|
+
scopes?: boolean;
|
|
71
|
+
task?: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type DynamicAgentCreationConfig = {
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
workspaceTemplate?: string;
|
|
77
|
+
agentDirTemplate?: string;
|
|
78
|
+
maxAgents?: number;
|
|
79
|
+
};
|
package/src/typing.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { createFeishuClient } from "./client.js";
|
|
3
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
|
+
|
|
5
|
+
// Feishu emoji types for typing indicator
|
|
6
|
+
// See: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
|
|
7
|
+
// Full list: https://github.com/go-lark/lark/blob/main/emoji.go
|
|
8
|
+
const TYPING_EMOJI = "Typing"; // Typing indicator emoji
|
|
9
|
+
|
|
10
|
+
export type TypingIndicatorState = {
|
|
11
|
+
messageId: string;
|
|
12
|
+
reactionId: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a typing indicator (reaction) to a message
|
|
17
|
+
*/
|
|
18
|
+
export async function addTypingIndicator(params: {
|
|
19
|
+
cfg: ClawdbotConfig;
|
|
20
|
+
messageId: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
}): Promise<TypingIndicatorState> {
|
|
23
|
+
const { cfg, messageId, accountId } = params;
|
|
24
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
25
|
+
if (!account.configured) {
|
|
26
|
+
return { messageId, reactionId: null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const client = createFeishuClient(account);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await client.im.messageReaction.create({
|
|
33
|
+
path: { message_id: messageId },
|
|
34
|
+
data: {
|
|
35
|
+
reaction_type: { emoji_type: TYPING_EMOJI },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const reactionId = (response as any)?.data?.reaction_id ?? null;
|
|
40
|
+
return { messageId, reactionId };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Silently fail - typing indicator is not critical
|
|
43
|
+
console.log(`[feishu] failed to add typing indicator: ${err}`);
|
|
44
|
+
return { messageId, reactionId: null };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove a typing indicator (reaction) from a message
|
|
50
|
+
*/
|
|
51
|
+
export async function removeTypingIndicator(params: {
|
|
52
|
+
cfg: ClawdbotConfig;
|
|
53
|
+
state: TypingIndicatorState;
|
|
54
|
+
accountId?: string;
|
|
55
|
+
}): Promise<void> {
|
|
56
|
+
const { cfg, state, accountId } = params;
|
|
57
|
+
if (!state.reactionId) return;
|
|
58
|
+
|
|
59
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
60
|
+
if (!account.configured) return;
|
|
61
|
+
|
|
62
|
+
const client = createFeishuClient(account);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await client.im.messageReaction.delete({
|
|
66
|
+
path: {
|
|
67
|
+
message_id: state.messageId,
|
|
68
|
+
reaction_id: state.reactionId,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Silently fail - cleanup is not critical
|
|
73
|
+
console.log(`[feishu] failed to remove typing indicator: ${err}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { runWikiApiCall, type WikiClient } from "./common.js";
|
|
2
|
+
import type { FeishuWikiParams } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
type ObjType = "doc" | "sheet" | "mindnote" | "bitable" | "file" | "docx" | "slides";
|
|
5
|
+
|
|
6
|
+
const WIKI_ACCESS_HINT =
|
|
7
|
+
"To grant wiki access: Open wiki space -> Settings -> Members -> Add the bot. " +
|
|
8
|
+
"See: https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa#a40ad4ca";
|
|
9
|
+
|
|
10
|
+
async function listSpaces(client: WikiClient) {
|
|
11
|
+
const res = await runWikiApiCall("wiki.space.list", () => client.wiki.space.list({}));
|
|
12
|
+
const spaces =
|
|
13
|
+
res.data?.items?.map((s) => ({
|
|
14
|
+
space_id: s.space_id,
|
|
15
|
+
name: s.name,
|
|
16
|
+
description: s.description,
|
|
17
|
+
visibility: s.visibility,
|
|
18
|
+
})) ?? [];
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
spaces,
|
|
22
|
+
...(spaces.length === 0 && { hint: WIKI_ACCESS_HINT }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function listNodes(client: WikiClient, spaceId: string, parentNodeToken?: string) {
|
|
27
|
+
const res = await runWikiApiCall("wiki.spaceNode.list", () =>
|
|
28
|
+
client.wiki.spaceNode.list({
|
|
29
|
+
path: { space_id: spaceId },
|
|
30
|
+
params: { parent_node_token: parentNodeToken },
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
nodes:
|
|
36
|
+
res.data?.items?.map((n) => ({
|
|
37
|
+
node_token: n.node_token,
|
|
38
|
+
obj_token: n.obj_token,
|
|
39
|
+
obj_type: n.obj_type,
|
|
40
|
+
title: n.title,
|
|
41
|
+
has_child: n.has_child,
|
|
42
|
+
})) ?? [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getNode(client: WikiClient, token: string) {
|
|
47
|
+
const res = await runWikiApiCall("wiki.space.getNode", () =>
|
|
48
|
+
client.wiki.space.getNode({
|
|
49
|
+
params: { token },
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const node = res.data?.node;
|
|
54
|
+
return {
|
|
55
|
+
node_token: node?.node_token,
|
|
56
|
+
space_id: node?.space_id,
|
|
57
|
+
obj_token: node?.obj_token,
|
|
58
|
+
obj_type: node?.obj_type,
|
|
59
|
+
title: node?.title,
|
|
60
|
+
parent_node_token: node?.parent_node_token,
|
|
61
|
+
has_child: node?.has_child,
|
|
62
|
+
creator: node?.creator,
|
|
63
|
+
create_time: node?.node_create_time,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createNode(
|
|
68
|
+
client: WikiClient,
|
|
69
|
+
spaceId: string,
|
|
70
|
+
title: string,
|
|
71
|
+
objType?: string,
|
|
72
|
+
parentNodeToken?: string,
|
|
73
|
+
) {
|
|
74
|
+
const res = await runWikiApiCall("wiki.spaceNode.create", () =>
|
|
75
|
+
client.wiki.spaceNode.create({
|
|
76
|
+
path: { space_id: spaceId },
|
|
77
|
+
data: {
|
|
78
|
+
obj_type: (objType as ObjType) || "docx",
|
|
79
|
+
node_type: "origin" as const,
|
|
80
|
+
title,
|
|
81
|
+
parent_node_token: parentNodeToken,
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const node = res.data?.node;
|
|
87
|
+
return {
|
|
88
|
+
node_token: node?.node_token,
|
|
89
|
+
obj_token: node?.obj_token,
|
|
90
|
+
obj_type: node?.obj_type,
|
|
91
|
+
title: node?.title,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function moveNode(
|
|
96
|
+
client: WikiClient,
|
|
97
|
+
spaceId: string,
|
|
98
|
+
nodeToken: string,
|
|
99
|
+
targetSpaceId?: string,
|
|
100
|
+
targetParentToken?: string,
|
|
101
|
+
) {
|
|
102
|
+
const res = await runWikiApiCall("wiki.spaceNode.move", () =>
|
|
103
|
+
client.wiki.spaceNode.move({
|
|
104
|
+
path: { space_id: spaceId, node_token: nodeToken },
|
|
105
|
+
data: {
|
|
106
|
+
target_space_id: targetSpaceId || spaceId,
|
|
107
|
+
target_parent_token: targetParentToken,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
node_token: res.data?.node?.node_token,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function renameNode(
|
|
119
|
+
client: WikiClient,
|
|
120
|
+
spaceId: string,
|
|
121
|
+
nodeToken: string,
|
|
122
|
+
title: string,
|
|
123
|
+
) {
|
|
124
|
+
await runWikiApiCall("wiki.spaceNode.updateTitle", () =>
|
|
125
|
+
client.wiki.spaceNode.updateTitle({
|
|
126
|
+
path: { space_id: spaceId, node_token: nodeToken },
|
|
127
|
+
data: { title },
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
node_token: nodeToken,
|
|
134
|
+
title,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function runWikiAction(client: WikiClient, params: FeishuWikiParams) {
|
|
139
|
+
switch (params.action) {
|
|
140
|
+
case "spaces":
|
|
141
|
+
return listSpaces(client);
|
|
142
|
+
case "nodes":
|
|
143
|
+
return listNodes(client, params.space_id, params.parent_node_token);
|
|
144
|
+
case "get":
|
|
145
|
+
return getNode(client, params.token);
|
|
146
|
+
case "search":
|
|
147
|
+
return {
|
|
148
|
+
error:
|
|
149
|
+
"Search is not available. Use feishu_wiki with action: 'nodes' to browse or action: 'get' to lookup by token.",
|
|
150
|
+
};
|
|
151
|
+
case "create":
|
|
152
|
+
return createNode(client, params.space_id, params.title, params.obj_type, params.parent_node_token);
|
|
153
|
+
case "move":
|
|
154
|
+
return moveNode(
|
|
155
|
+
client,
|
|
156
|
+
params.space_id,
|
|
157
|
+
params.node_token,
|
|
158
|
+
params.target_space_id,
|
|
159
|
+
params.target_parent_token,
|
|
160
|
+
);
|
|
161
|
+
case "rename":
|
|
162
|
+
return renameNode(client, params.space_id, params.node_token, params.title);
|
|
163
|
+
default:
|
|
164
|
+
return { error: `Unknown action: ${(params as any).action}` };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFeishuClient } from "../client.js";
|
|
2
|
+
import {
|
|
3
|
+
errorResult,
|
|
4
|
+
json,
|
|
5
|
+
runFeishuApiCall,
|
|
6
|
+
type FeishuApiResponse,
|
|
7
|
+
} from "../tools-common/feishu-api.js";
|
|
8
|
+
|
|
9
|
+
export type WikiClient = ReturnType<typeof createFeishuClient>;
|
|
10
|
+
|
|
11
|
+
export { json, errorResult };
|
|
12
|
+
|
|
13
|
+
export async function runWikiApiCall<T extends FeishuApiResponse>(
|
|
14
|
+
context: string,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return runFeishuApiCall(context, fn);
|
|
18
|
+
}
|