@nextclaw/channel-extension-feishu 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NextClaw contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @nextclaw/channel-extension-feishu
2
+
3
+ Lightweight Feishu/Lark channel extension for NextClaw.
4
+
5
+ This extension uses the new NextClaw extension process model and supports QR
6
+ scan-to-create onboarding inspired by Hermes Agent:
7
+
8
+ 1. Start channel auth from the UI or service API.
9
+ 2. Scan the Feishu/Lark QR code.
10
+ 3. The platform creates a bot application and returns app credentials.
11
+ 4. Credentials are stored under `NEXTCLAW_HOME/channels/feishu`.
12
+ 5. The extension connects through the Lark WebSocket client.
13
+
14
+ This extension replaces the removed legacy Feishu channel plugin. It contributes
15
+ the built-in `feishu` channel id through the extension mechanism, so QR
16
+ onboarding, WebSocket inbound messages, NCP replies, and Feishu processing
17
+ reactions now live in one lightweight channel owner.
@@ -0,0 +1,6 @@
1
+ import { FeishuAccountConfig, FeishuChannelConfig, FeishuDomain, FeishuInboundMessage, FeishuRuntimeAccount } from "./types/feishu-extension.types.js";
2
+ import { FeishuRegistrationService } from "./services/feishu-registration.service.js";
3
+ import { FeishuAuthCapability } from "./services/feishu-auth-capability.service.js";
4
+ import { FeishuChannelAdapter } from "./services/feishu-channel-adapter.service.js";
5
+ import { FeishuExtensionRuntime } from "./services/feishu-extension-runtime.service.js";
6
+ export { type FeishuAccountConfig, FeishuAuthCapability, FeishuChannelAdapter, type FeishuChannelConfig, type FeishuDomain, FeishuExtensionRuntime, type FeishuInboundMessage, FeishuRegistrationService, type FeishuRuntimeAccount };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { FeishuRegistrationService } from "./services/feishu-registration.service.js";
2
+ import { FeishuAuthCapability } from "./services/feishu-auth-capability.service.js";
3
+ import { FeishuChannelAdapter } from "./services/feishu-channel-adapter.service.js";
4
+ import { FeishuExtensionRuntime } from "./services/feishu-extension-runtime.service.js";
5
+ export { FeishuAuthCapability, FeishuChannelAdapter, FeishuExtensionRuntime, FeishuRegistrationService };
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/main.js ADDED
@@ -0,0 +1,12 @@
1
+ import { FeishuAuthCapability } from "./services/feishu-auth-capability.service.js";
2
+ import { FeishuChannelAdapter } from "./services/feishu-channel-adapter.service.js";
3
+ import { FeishuExtensionRuntime } from "./services/feishu-extension-runtime.service.js";
4
+ import { NextClawExtension } from "@nextclaw/extension-sdk";
5
+ //#region src/main.ts
6
+ const extension = new NextClawExtension();
7
+ const feishu = extension.channels.use("feishu");
8
+ const runtime = new FeishuExtensionRuntime(feishu, new FeishuChannelAdapter());
9
+ extension.capabilities.provide("channel.auth", new FeishuAuthCapability({ channel: feishu }));
10
+ await runtime.start();
11
+ //#endregion
12
+ export {};
@@ -0,0 +1,20 @@
1
+ import { FeishuRegistrationService } from "./feishu-registration.service.js";
2
+ import { ExtensionCapabilityPayload, ExtensionChannel } from "@nextclaw/extension-sdk";
3
+
4
+ //#region src/services/feishu-auth-capability.service.d.ts
5
+ type FeishuAuthCapabilityDeps = {
6
+ channel: ExtensionChannel;
7
+ registrationService?: Pick<FeishuRegistrationService, "start" | "poll">;
8
+ };
9
+ declare class FeishuAuthCapability {
10
+ private readonly channel;
11
+ private readonly registrationService;
12
+ constructor(deps: FeishuAuthCapabilityDeps);
13
+ readonly start: (request: ExtensionCapabilityPayload) => Promise<unknown>;
14
+ readonly poll: (request: ExtensionCapabilityPayload) => Promise<unknown>;
15
+ private readonly readCurrentConfig;
16
+ private readonly readString;
17
+ private readonly readDomain;
18
+ }
19
+ //#endregion
20
+ export { FeishuAuthCapability };
@@ -0,0 +1,32 @@
1
+ import { FeishuRegistrationService } from "./feishu-registration.service.js";
2
+ //#region src/services/feishu-auth-capability.service.ts
3
+ var FeishuAuthCapability = class {
4
+ channel;
5
+ registrationService;
6
+ constructor(deps) {
7
+ this.channel = deps.channel;
8
+ this.registrationService = deps.registrationService ?? new FeishuRegistrationService();
9
+ }
10
+ start = async (request) => await this.registrationService.start({
11
+ pluginConfig: await this.readCurrentConfig(),
12
+ requestedAccountId: this.readString(request.accountId),
13
+ domain: this.readDomain(request.domain),
14
+ verbose: request.verbose === true
15
+ });
16
+ poll = async (request) => {
17
+ const sessionId = this.readString(request.sessionId);
18
+ if (!sessionId) throw new Error("sessionId is required");
19
+ return await this.registrationService.poll({ sessionId });
20
+ };
21
+ readCurrentConfig = async () => {
22
+ const config = await this.channel.config.get();
23
+ return config && typeof config === "object" && !Array.isArray(config) ? config : {};
24
+ };
25
+ readString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
26
+ readDomain = (value) => {
27
+ const text = this.readString(value);
28
+ return text === "feishu" || text === "lark" ? text : null;
29
+ };
30
+ };
31
+ //#endregion
32
+ export { FeishuAuthCapability };
@@ -0,0 +1,46 @@
1
+ import { FeishuChannelAdapterContract, FeishuChannelConfig, FeishuInboundMessage } from "../types/feishu-extension.types.js";
2
+ import { FeishuAccountStore } from "../stores/feishu-account.store.js";
3
+ import { FeishuSdkService } from "./feishu-sdk.service.js";
4
+ import { NcpEndpointEvent } from "@nextclaw/ncp";
5
+
6
+ //#region src/services/feishu-channel-adapter.service.d.ts
7
+ type FeishuAdapterDeps = {
8
+ sdk?: FeishuSdkService;
9
+ store?: FeishuAccountStore;
10
+ logger?: Pick<typeof console, "warn" | "log">;
11
+ };
12
+ declare class FeishuChannelAdapter implements FeishuChannelAdapterContract {
13
+ private messageHandler;
14
+ private readonly sdk;
15
+ private readonly store;
16
+ private readonly logger;
17
+ private readonly replyConsumer;
18
+ private readonly wsClients;
19
+ private readonly replySessions;
20
+ private readonly canonicalRoutes;
21
+ private readonly processingReactions;
22
+ private running;
23
+ private config;
24
+ constructor(deps?: FeishuAdapterDeps);
25
+ readonly configure: (config: FeishuChannelConfig) => Promise<void>;
26
+ readonly start: () => Promise<void>;
27
+ readonly stop: () => Promise<void>;
28
+ readonly onMessage: (handler: (message: FeishuInboundMessage) => void | Promise<void>) => (() => void);
29
+ readonly sendNcpEvent: (event: NcpEndpointEvent) => Promise<void>;
30
+ readonly emitMessageForTest: (message: FeishuInboundMessage) => Promise<void>;
31
+ private readonly startAccount;
32
+ private readonly listAvailableAccountIds;
33
+ private readonly resolveRuntimeAccount;
34
+ private readonly resolveSelectedAccountId;
35
+ private readonly resolveSendAccount;
36
+ private readonly resolveReplySession;
37
+ private readonly resolveCanonicalRoute;
38
+ private readonly handleInboundEvent;
39
+ private readonly startProcessingReaction;
40
+ private readonly finalizeProcessingReaction;
41
+ private readonly deleteProcessingReaction;
42
+ private readonly addFailedReaction;
43
+ private readonly sendText;
44
+ }
45
+ //#endregion
46
+ export { FeishuChannelAdapter };
@@ -0,0 +1,251 @@
1
+ import { FileFeishuAccountStore } from "../stores/feishu-account.store.js";
2
+ import { FeishuReplyChat, NcpEventQueue, TERMINAL_NCP_EVENT_TYPES } from "./feishu-reply-chat.service.js";
3
+ import { FeishuSdkService } from "./feishu-sdk.service.js";
4
+ import { isFeishuBotMentioned, isFeishuGroupChat, parseFeishuInboundMessage } from "../utils/feishu-message.utils.js";
5
+ import { readFeishuEventSessionId, resolveFeishuSessionRoute } from "../utils/feishu-session-route.utils.js";
6
+ import { NcpEventType } from "@nextclaw/ncp";
7
+ import { NcpReplyConsumer } from "@nextclaw/ncp-toolkit";
8
+ //#region src/services/feishu-channel-adapter.service.ts
9
+ const FEISHU_PROCESSING_REACTION = "Typing";
10
+ const FEISHU_FAILED_REACTION = "CrossMark";
11
+ const FAILED_NCP_EVENT_TYPES = new Set([NcpEventType.MessageFailed, NcpEventType.RunError]);
12
+ function readStringArray(value) {
13
+ if (!Array.isArray(value)) return [];
14
+ return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
15
+ }
16
+ function normalizeRouteKey(conversationId) {
17
+ return conversationId.toLowerCase();
18
+ }
19
+ function isAllowedSender(allowFrom, senderId) {
20
+ return allowFrom.length === 0 || allowFrom.includes(senderId);
21
+ }
22
+ var FeishuChannelAdapter = class {
23
+ messageHandler = null;
24
+ sdk;
25
+ store;
26
+ logger;
27
+ replyConsumer;
28
+ wsClients = /* @__PURE__ */ new Map();
29
+ replySessions = /* @__PURE__ */ new Map();
30
+ canonicalRoutes = /* @__PURE__ */ new Map();
31
+ processingReactions = /* @__PURE__ */ new Map();
32
+ running = false;
33
+ config = {};
34
+ constructor(deps = {}) {
35
+ this.sdk = deps.sdk ?? new FeishuSdkService();
36
+ this.store = deps.store ?? new FileFeishuAccountStore();
37
+ this.logger = deps.logger ?? console;
38
+ this.replyConsumer = new NcpReplyConsumer(new FeishuReplyChat({
39
+ resolveAccount: this.resolveSendAccount,
40
+ sendText: this.sendText
41
+ }));
42
+ }
43
+ configure = async (config) => {
44
+ this.config = config;
45
+ if (!this.running) return;
46
+ await this.stop();
47
+ await this.start();
48
+ };
49
+ start = async () => {
50
+ if (this.running) return;
51
+ this.running = true;
52
+ for (const accountId of this.listAvailableAccountIds()) {
53
+ const account = this.resolveRuntimeAccount(accountId);
54
+ if (account?.enabled) this.startAccount(account);
55
+ }
56
+ };
57
+ stop = async () => {
58
+ if (!this.running) return;
59
+ this.running = false;
60
+ for (const client of this.wsClients.values()) {
61
+ client.close?.();
62
+ client.stop?.();
63
+ }
64
+ this.wsClients.clear();
65
+ for (const session of this.replySessions.values()) session.queue.close();
66
+ this.replySessions.clear();
67
+ this.processingReactions.clear();
68
+ };
69
+ onMessage = (handler) => {
70
+ this.messageHandler = handler;
71
+ return () => {
72
+ if (this.messageHandler === handler) this.messageHandler = null;
73
+ };
74
+ };
75
+ sendNcpEvent = async (event) => {
76
+ if (!this.running) return;
77
+ const route = resolveFeishuSessionRoute(event);
78
+ if (!route) return;
79
+ const sessionId = readFeishuEventSessionId(event);
80
+ if (!sessionId) return;
81
+ const canonicalRoute = this.resolveCanonicalRoute(route);
82
+ const session = this.resolveReplySession(sessionId, canonicalRoute);
83
+ session.queue.push(event);
84
+ if (TERMINAL_NCP_EVENT_TYPES.has(event.type)) {
85
+ session.queue.close();
86
+ this.replySessions.delete(sessionId);
87
+ await session.consuming;
88
+ await this.finalizeProcessingReaction(canonicalRoute, FAILED_NCP_EVENT_TYPES.has(event.type));
89
+ }
90
+ };
91
+ emitMessageForTest = async (message) => {
92
+ await this.messageHandler?.(message);
93
+ };
94
+ startAccount = (account) => {
95
+ const dispatcher = this.sdk.createEventDispatcher();
96
+ dispatcher.register({ "im.message.receive_v1": async (data) => {
97
+ await this.handleInboundEvent(account, data);
98
+ } });
99
+ const wsClient = this.sdk.createWsClient(account);
100
+ wsClient.start({ eventDispatcher: dispatcher });
101
+ this.wsClients.set(account.accountId, wsClient);
102
+ this.logger.log?.(`[feishu] websocket started for ${account.accountId}`);
103
+ };
104
+ listAvailableAccountIds = () => {
105
+ return Array.from(new Set([
106
+ ...this.config.defaultAccountId ? [this.config.defaultAccountId] : [],
107
+ ...Object.keys(this.config.accounts ?? {}),
108
+ ...this.store.listAccountIds()
109
+ ]));
110
+ };
111
+ resolveRuntimeAccount = (accountId) => {
112
+ const stored = this.store.loadAccount(accountId);
113
+ if (!stored?.appId || !stored.appSecret) return null;
114
+ const accountConfig = this.config.accounts?.[accountId] ?? {};
115
+ return {
116
+ accountId,
117
+ appId: stored.appId,
118
+ appSecret: stored.appSecret,
119
+ domain: accountConfig.domain ?? stored.domain ?? this.config.domain ?? "feishu",
120
+ enabled: accountConfig.enabled !== false && this.config.enabled !== false,
121
+ name: accountConfig.name ?? stored.botName,
122
+ botOpenId: stored.botOpenId,
123
+ allowFrom: Array.from(new Set([...readStringArray(this.config.allowFrom), ...readStringArray(accountConfig.allowFrom)])),
124
+ groupPolicy: accountConfig.groupPolicy ?? this.config.groupPolicy ?? "open",
125
+ requireMention: accountConfig.requireMention ?? this.config.requireMention ?? true
126
+ };
127
+ };
128
+ resolveSelectedAccountId = (requestedAccountId) => {
129
+ if (requestedAccountId) return requestedAccountId;
130
+ if (this.config.defaultAccountId) return this.config.defaultAccountId;
131
+ const accountIds = this.listAvailableAccountIds();
132
+ if (accountIds.length === 1 && accountIds[0]) return accountIds[0];
133
+ throw new Error("feishu send failed: accountId is required when multiple accounts are configured");
134
+ };
135
+ resolveSendAccount = (target) => {
136
+ const account = this.resolveRuntimeAccount(this.resolveSelectedAccountId(target.accountId));
137
+ if (!account?.enabled) throw new Error(`feishu send failed: account "${target.accountId ?? this.config.defaultAccountId ?? ""}" is not connected`);
138
+ return account;
139
+ };
140
+ resolveReplySession = (sessionId, route) => {
141
+ const existing = this.replySessions.get(sessionId);
142
+ if (existing) return existing;
143
+ const queue = new NcpEventQueue();
144
+ const session = {
145
+ queue,
146
+ consuming: this.replyConsumer.consume({
147
+ target: {
148
+ conversationId: route.conversationId,
149
+ ...route.accountId ? { accountId: route.accountId } : {}
150
+ },
151
+ eventStream: queue
152
+ })
153
+ };
154
+ this.replySessions.set(sessionId, session);
155
+ return session;
156
+ };
157
+ resolveCanonicalRoute = (route) => {
158
+ const remembered = this.canonicalRoutes.get(normalizeRouteKey(route.conversationId));
159
+ if (remembered) return {
160
+ ...remembered,
161
+ ...route.accountId ? { accountId: route.accountId } : {}
162
+ };
163
+ return route;
164
+ };
165
+ handleInboundEvent = async (account, rawEvent) => {
166
+ const parsed = parseFeishuInboundMessage(rawEvent);
167
+ if (!parsed || !isAllowedSender(account.allowFrom, parsed.senderOpenId)) return;
168
+ if (isFeishuGroupChat(parsed.chatType)) {
169
+ if (account.groupPolicy === "disabled") return;
170
+ if (account.groupPolicy === "allowlist" && !account.allowFrom.includes(parsed.chatId)) return;
171
+ if (account.requireMention && !isFeishuBotMentioned({
172
+ botOpenId: account.botOpenId,
173
+ mentionedOpenIds: parsed.mentionedOpenIds
174
+ })) return;
175
+ }
176
+ this.canonicalRoutes.set(normalizeRouteKey(parsed.chatId), {
177
+ accountId: account.accountId,
178
+ conversationId: parsed.chatId
179
+ });
180
+ await this.startProcessingReaction(account, parsed.messageId, parsed.chatId);
181
+ await this.messageHandler?.({
182
+ conversationId: parsed.chatId,
183
+ senderId: parsed.senderOpenId,
184
+ text: parsed.text,
185
+ accountId: account.accountId,
186
+ peerKind: isFeishuGroupChat(parsed.chatType) ? "group" : "direct",
187
+ messageId: parsed.messageId,
188
+ raw: rawEvent
189
+ });
190
+ };
191
+ startProcessingReaction = async (account, messageId, conversationId) => {
192
+ if (!messageId) return;
193
+ const routeKey = normalizeRouteKey(conversationId);
194
+ try {
195
+ const reactionId = await this.sdk.addReaction({
196
+ account,
197
+ messageId,
198
+ emojiType: FEISHU_PROCESSING_REACTION
199
+ });
200
+ this.processingReactions.set(routeKey, {
201
+ accountId: account.accountId,
202
+ messageId,
203
+ reactionId
204
+ });
205
+ } catch (error) {
206
+ this.logger.warn?.(`[feishu] failed to add processing reaction for ${messageId}: ${String(error)}`);
207
+ }
208
+ };
209
+ finalizeProcessingReaction = async (route, failed) => {
210
+ const routeKey = normalizeRouteKey(route.conversationId);
211
+ const state = this.processingReactions.get(routeKey);
212
+ if (!state) return;
213
+ this.processingReactions.delete(routeKey);
214
+ const account = this.resolveRuntimeAccount(state.accountId);
215
+ if (!account?.enabled) return;
216
+ await this.deleteProcessingReaction(account, state);
217
+ if (failed) await this.addFailedReaction(account, state.messageId);
218
+ };
219
+ deleteProcessingReaction = async (account, state) => {
220
+ if (!state.reactionId) return;
221
+ try {
222
+ await this.sdk.deleteReaction({
223
+ account,
224
+ messageId: state.messageId,
225
+ reactionId: state.reactionId
226
+ });
227
+ } catch (error) {
228
+ this.logger.warn?.(`[feishu] failed to remove processing reaction for ${state.messageId}: ${String(error)}`);
229
+ }
230
+ };
231
+ addFailedReaction = async (account, messageId) => {
232
+ try {
233
+ await this.sdk.addReaction({
234
+ account,
235
+ messageId,
236
+ emojiType: FEISHU_FAILED_REACTION
237
+ });
238
+ } catch (error) {
239
+ this.logger.warn?.(`[feishu] failed to add failure reaction for ${messageId}: ${String(error)}`);
240
+ }
241
+ };
242
+ sendText = async (params) => {
243
+ await this.sdk.sendText({
244
+ account: params.account,
245
+ chatId: params.conversationId,
246
+ text: params.text
247
+ });
248
+ };
249
+ };
250
+ //#endregion
251
+ export { FeishuChannelAdapter };
@@ -0,0 +1,19 @@
1
+ import { FeishuChannelAdapterContract } from "../types/feishu-extension.types.js";
2
+ import { ExtensionChannel } from "@nextclaw/extension-sdk";
3
+
4
+ //#region src/services/feishu-extension-runtime.service.d.ts
5
+ declare class FeishuExtensionRuntime {
6
+ private readonly channel;
7
+ private readonly adapter;
8
+ private unsubscribeConfig;
9
+ private unsubscribeMessages;
10
+ private unsubscribeNcpEvents;
11
+ constructor(channel: ExtensionChannel, adapter: FeishuChannelAdapterContract);
12
+ readonly start: () => Promise<void>;
13
+ readonly stop: () => Promise<void>;
14
+ private readonly applyConfig;
15
+ private readonly submitMessage;
16
+ private readonly sendNcpEvent;
17
+ }
18
+ //#endregion
19
+ export { FeishuExtensionRuntime };
@@ -0,0 +1,66 @@
1
+ //#region src/services/feishu-extension-runtime.service.ts
2
+ var FeishuExtensionRuntime = class {
3
+ unsubscribeConfig = null;
4
+ unsubscribeMessages = null;
5
+ unsubscribeNcpEvents = null;
6
+ constructor(channel, adapter) {
7
+ this.channel = channel;
8
+ this.adapter = adapter;
9
+ }
10
+ start = async () => {
11
+ this.unsubscribeMessages = this.adapter.onMessage(this.submitMessage);
12
+ this.unsubscribeNcpEvents = this.channel.onNcpEvent(this.sendNcpEvent);
13
+ this.unsubscribeConfig = this.channel.config.onChange(async () => {
14
+ await this.applyConfig();
15
+ });
16
+ await this.applyConfig();
17
+ };
18
+ stop = async () => {
19
+ this.unsubscribeConfig?.();
20
+ this.unsubscribeMessages?.();
21
+ this.unsubscribeNcpEvents?.();
22
+ this.unsubscribeConfig = null;
23
+ this.unsubscribeMessages = null;
24
+ this.unsubscribeNcpEvents = null;
25
+ await this.adapter.stop();
26
+ };
27
+ applyConfig = async () => {
28
+ const config = await this.channel.config.get();
29
+ await this.adapter.configure(config);
30
+ if (config.enabled === false) {
31
+ await this.adapter.stop();
32
+ return;
33
+ }
34
+ await this.adapter.start();
35
+ };
36
+ submitMessage = async (message) => {
37
+ await this.channel.submitMessage({
38
+ conversationId: message.conversationId,
39
+ senderId: message.senderId,
40
+ content: {
41
+ type: "text",
42
+ text: message.text
43
+ },
44
+ metadata: {
45
+ accountId: message.accountId,
46
+ account_id: message.accountId,
47
+ peerId: message.conversationId,
48
+ peer_id: message.conversationId,
49
+ peerKind: message.peerKind,
50
+ peer_kind: message.peerKind,
51
+ ...message.messageId ? { message_id: message.messageId } : {},
52
+ ...message.raw === void 0 ? {} : { raw: message.raw }
53
+ }
54
+ });
55
+ };
56
+ sendNcpEvent = async (event) => {
57
+ try {
58
+ await this.adapter.sendNcpEvent(event);
59
+ } catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ console.warn(`[feishu] failed to send NCP event: ${message}`);
62
+ }
63
+ };
64
+ };
65
+ //#endregion
66
+ export { FeishuExtensionRuntime };
@@ -0,0 +1,54 @@
1
+ import { FeishuDomain } from "../types/feishu-extension.types.js";
2
+ import { FeishuAccountStore } from "../stores/feishu-account.store.js";
3
+
4
+ //#region src/services/feishu-registration.service.d.ts
5
+ type FeishuRegistrationStartParams = {
6
+ pluginConfig?: Record<string, unknown>;
7
+ requestedAccountId?: string | null;
8
+ domain?: FeishuDomain | null;
9
+ verbose?: boolean;
10
+ };
11
+ type FeishuAuthStartResult = {
12
+ channel: string;
13
+ kind: "qr_code";
14
+ sessionId: string;
15
+ qrCode: string;
16
+ qrCodeUrl: string;
17
+ expiresAt: string;
18
+ intervalMs: number;
19
+ note?: string;
20
+ };
21
+ type FeishuAuthPollResult = {
22
+ channel: string;
23
+ status: "pending" | "scanned" | "authorized" | "expired" | "error";
24
+ message?: string;
25
+ nextPollMs?: number;
26
+ accountId?: string | null;
27
+ notes?: string[];
28
+ pluginConfig?: Record<string, unknown>;
29
+ };
30
+ type FeishuRegistrationServiceDeps = {
31
+ store?: FeishuAccountStore;
32
+ fetchImpl?: typeof fetch;
33
+ };
34
+ declare class FeishuRegistrationService {
35
+ private readonly store;
36
+ private readonly fetchImpl;
37
+ private readonly sessions;
38
+ constructor(deps?: FeishuRegistrationServiceDeps);
39
+ readonly start: (params: FeishuRegistrationStartParams) => Promise<FeishuAuthStartResult>;
40
+ readonly poll: ({
41
+ sessionId
42
+ }: {
43
+ sessionId: string;
44
+ }) => Promise<FeishuAuthPollResult | null>;
45
+ private readonly assertRegistrationSupported;
46
+ private readonly beginRegistration;
47
+ private readonly postRegistration;
48
+ private readonly confirmRegistration;
49
+ private readonly probeBot;
50
+ private readonly readRecord;
51
+ private readonly cleanupExpiredSessions;
52
+ }
53
+ //#endregion
54
+ export { FeishuRegistrationService };