@nextclaw/channel-extension-feishu 0.1.7 → 0.1.9-beta.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/README.md CHANGED
@@ -11,7 +11,7 @@ scan-to-create onboarding inspired by Hermes Agent:
11
11
  4. Credentials are stored under `NEXTCLAW_HOME/channels/feishu`.
12
12
  5. The extension connects through the Lark WebSocket client.
13
13
 
14
- This extension replaces the removed legacy Feishu channel plugin. It contributes
14
+ This extension replaces the removed legacy Feishu channel package. It contributes
15
15
  the built-in `feishu` channel id through the extension mechanism, so QR
16
16
  onboarding, WebSocket inbound messages, NCP replies, and Feishu processing
17
17
  reactions now live in one lightweight channel owner.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { FeishuAccountConfig, FeishuChannelConfig, FeishuDomain, FeishuInboundMessage, FeishuRuntimeAccount } from "./types/feishu-extension.types.js";
2
+ import { FeishuAccountConnectionService } from "./services/feishu-account-connection.service.js";
2
3
  import { FeishuRegistrationService } from "./services/feishu-registration.service.js";
3
4
  import { FeishuAuthCapability } from "./services/feishu-auth-capability.service.js";
4
5
  import { FeishuChannelAdapter } from "./services/feishu-channel-adapter.service.js";
5
- export { type FeishuAccountConfig, FeishuAuthCapability, FeishuChannelAdapter, type FeishuChannelConfig, type FeishuDomain, type FeishuInboundMessage, FeishuRegistrationService, type FeishuRuntimeAccount };
6
+ export { type FeishuAccountConfig, FeishuAccountConnectionService, FeishuAuthCapability, FeishuChannelAdapter, type FeishuChannelConfig, type FeishuDomain, type FeishuInboundMessage, FeishuRegistrationService, type FeishuRuntimeAccount };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
+ import { FeishuAccountConnectionService } from "./services/feishu-account-connection.service.js";
1
2
  import { FeishuRegistrationService } from "./services/feishu-registration.service.js";
2
3
  import { FeishuAuthCapability } from "./services/feishu-auth-capability.service.js";
3
4
  import { FeishuChannelAdapter } from "./services/feishu-channel-adapter.service.js";
4
- export { FeishuAuthCapability, FeishuChannelAdapter, FeishuRegistrationService };
5
+ export { FeishuAccountConnectionService, FeishuAuthCapability, FeishuChannelAdapter, FeishuRegistrationService };
@@ -0,0 +1,35 @@
1
+ import { FeishuDomain } from "../types/feishu-extension.types.js";
2
+ import { FeishuAccountStore } from "../stores/feishu-account.store.js";
3
+
4
+ //#region src/services/feishu-account-connection.service.d.ts
5
+ type FeishuAccountConnectionServiceDeps = {
6
+ store?: FeishuAccountStore;
7
+ fetchImpl?: typeof fetch;
8
+ };
9
+ type FeishuAccountConnectionParams = {
10
+ channelConfig?: Record<string, unknown>;
11
+ requestedAccountId?: string | null;
12
+ domain?: FeishuDomain | null;
13
+ appId: string;
14
+ appSecret: string;
15
+ ownerOpenId?: string;
16
+ };
17
+ type FeishuAccountConnectionResult = {
18
+ channel: string;
19
+ status: "authorized";
20
+ message: string;
21
+ accountId: string;
22
+ notes: string[];
23
+ channelConfig: Record<string, unknown>;
24
+ };
25
+ declare class FeishuAccountConnectionService {
26
+ private readonly store;
27
+ private readonly fetchImpl;
28
+ constructor(deps?: FeishuAccountConnectionServiceDeps);
29
+ readonly connect: (params: FeishuAccountConnectionParams) => Promise<FeishuAccountConnectionResult>;
30
+ private readonly resolveReplacementAccountIds;
31
+ private readonly probeBot;
32
+ }
33
+ //#endregion
34
+ export { FeishuAccountConnectionService };
35
+ //# sourceMappingURL=feishu-account-connection.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feishu-account-connection.service.d.ts","names":[],"sources":["../../src/services/feishu-account-connection.service.ts"],"mappings":";;;;KAYK,kCAAA;EACH,KAAA,GAAQ,kBAAA;EACR,SAAA,UAAmB,KAAA;AAAA;AAAA,KAGT,6BAAA;EACV,aAAA,GAAgB,MAAA;EAChB,kBAAA;EACA,MAAA,GAAS,YAAA;EACT,KAAA;EACA,SAAA;EACA,WAAA;AAAA;AAAA,KAGU,6BAAA;EACV,OAAA;EACA,MAAA;EACA,OAAA;EACA,SAAA;EACA,KAAA;EACA,aAAA,EAAe,MAAA;AAAA;AAAA,cAkBJ,8BAAA;EAAA,iBACM,KAAA;EAAA,iBACA,SAAA;cAEL,IAAA,GAAM,kCAAA;EAAA,SAKT,OAAA,GAAiB,MAAA,EAAQ,6BAAA,KAAgC,OAAA,CAAQ,6BAAA;EAAA,iBAqDzD,4BAAA;EAAA,iBAyBA,QAAA;AAAA"}
@@ -0,0 +1,112 @@
1
+ import { FEISHU_CHANNEL_ID, buildRegisteredFeishuChannelConfig, normalizeFeishuChannelConfig } from "../utils/feishu-config.utils.js";
2
+ import { FileFeishuAccountStore } from "../stores/feishu-account.store.js";
3
+ //#region src/services/feishu-account-connection.service.ts
4
+ const FEISHU_OPEN_BASE_URLS = {
5
+ feishu: "https://open.feishu.cn",
6
+ lark: "https://open.larksuite.com"
7
+ };
8
+ function readString(value) {
9
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
10
+ }
11
+ function readRecord(value) {
12
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
13
+ }
14
+ var FeishuAccountConnectionService = class {
15
+ store;
16
+ fetchImpl;
17
+ constructor(deps = {}) {
18
+ this.store = deps.store ?? new FileFeishuAccountStore();
19
+ this.fetchImpl = deps.fetchImpl ?? fetch;
20
+ }
21
+ connect = async (params) => {
22
+ const { appId, appSecret, channelConfig, domain: requestedDomain, ownerOpenId, requestedAccountId } = params;
23
+ const currentConfig = normalizeFeishuChannelConfig(channelConfig);
24
+ const domain = requestedDomain ?? currentConfig.domain ?? "feishu";
25
+ const botInfo = await this.probeBot({
26
+ appId,
27
+ appSecret,
28
+ domain
29
+ });
30
+ const accountId = requestedAccountId?.trim() || appId;
31
+ const replacementAccountIds = this.resolveReplacementAccountIds({
32
+ accountId,
33
+ botOpenId: botInfo.botOpenId,
34
+ currentConfig,
35
+ requestedAccountId
36
+ });
37
+ for (const replacementAccountId of replacementAccountIds) this.store.deleteAccount(replacementAccountId);
38
+ this.store.saveAccount({
39
+ accountId,
40
+ appId,
41
+ appSecret,
42
+ domain,
43
+ botName: botInfo.botName,
44
+ botOpenId: botInfo.botOpenId,
45
+ ownerOpenId,
46
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
47
+ });
48
+ return {
49
+ channel: FEISHU_CHANNEL_ID,
50
+ status: "authorized",
51
+ message: "飞书智能体已连接。",
52
+ accountId,
53
+ notes: [
54
+ ...botInfo.botName ? [`Connected bot: ${botInfo.botName}`] : [],
55
+ ...replacementAccountIds.map((replacementAccountId) => `Replaced previous Feishu agent: ${replacementAccountId}`),
56
+ ...ownerOpenId ? [`Authorized initial user: ${ownerOpenId}`] : []
57
+ ],
58
+ channelConfig: buildRegisteredFeishuChannelConfig({
59
+ config: currentConfig,
60
+ accountId,
61
+ domain,
62
+ botName: botInfo.botName,
63
+ allowOpenId: ownerOpenId,
64
+ replaceAccountIds: replacementAccountIds
65
+ })
66
+ };
67
+ };
68
+ resolveReplacementAccountIds = (params) => {
69
+ const { accountId, botOpenId, currentConfig, requestedAccountId: requestedAccountIdRaw } = params;
70
+ const replacementIds = /* @__PURE__ */ new Set();
71
+ const defaultAccountId = currentConfig.defaultAccountId?.trim();
72
+ if (!requestedAccountIdRaw?.trim() && defaultAccountId && defaultAccountId !== accountId) replacementIds.add(defaultAccountId);
73
+ if (botOpenId) {
74
+ for (const candidateAccountId of this.store.listAccountIds()) if (candidateAccountId !== accountId && this.store.loadAccount(candidateAccountId)?.botOpenId === botOpenId) replacementIds.add(candidateAccountId);
75
+ }
76
+ return [...replacementIds];
77
+ };
78
+ probeBot = async ({ appId, appSecret, domain }) => {
79
+ const tokenResponse = await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/auth/v3/tenant_access_token/internal`, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({
83
+ app_id: appId,
84
+ app_secret: appSecret
85
+ })
86
+ });
87
+ const tokenData = await tokenResponse.json();
88
+ const accessToken = readString(tokenData.tenant_access_token);
89
+ if (!tokenResponse.ok || !accessToken) {
90
+ const message = readString(tokenData.msg) ?? readString(tokenData.error) ?? `HTTP ${tokenResponse.status}`;
91
+ throw new Error(`Feishu credentials could not be verified: ${message}`);
92
+ }
93
+ const botResponse = await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/bot/v3/info`, { headers: {
94
+ Authorization: `Bearer ${accessToken}`,
95
+ "Content-Type": "application/json"
96
+ } });
97
+ const botData = await botResponse.json();
98
+ if (!botResponse.ok) {
99
+ const message = readString(botData.msg) ?? readString(botData.error) ?? `HTTP ${botResponse.status}`;
100
+ throw new Error(`Feishu bot info could not be loaded: ${message}`);
101
+ }
102
+ const bot = readRecord(botData.bot) ?? readRecord(readRecord(botData.data)?.bot);
103
+ return {
104
+ botName: readString(bot?.app_name) ?? readString(bot?.bot_name),
105
+ botOpenId: readString(bot?.open_id)
106
+ };
107
+ };
108
+ };
109
+ //#endregion
110
+ export { FeishuAccountConnectionService };
111
+
112
+ //# sourceMappingURL=feishu-account-connection.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feishu-account-connection.service.js","names":[],"sources":["../../src/services/feishu-account-connection.service.ts"],"sourcesContent":["import {\n buildRegisteredFeishuChannelConfig,\n DEFAULT_FEISHU_DOMAIN,\n FEISHU_CHANNEL_ID,\n normalizeFeishuChannelConfig,\n} from \"../utils/feishu-config.utils.js\";\nimport {\n FileFeishuAccountStore,\n type FeishuAccountStore,\n} from \"../stores/feishu-account.store.js\";\nimport type { FeishuChannelConfig, FeishuDomain } from \"../types/feishu-extension.types.js\";\n\ntype FeishuAccountConnectionServiceDeps = {\n store?: FeishuAccountStore;\n fetchImpl?: typeof fetch;\n};\n\nexport type FeishuAccountConnectionParams = {\n channelConfig?: Record<string, unknown>;\n requestedAccountId?: string | null;\n domain?: FeishuDomain | null;\n appId: string;\n appSecret: string;\n ownerOpenId?: string;\n};\n\nexport type FeishuAccountConnectionResult = {\n channel: string;\n status: \"authorized\";\n message: string;\n accountId: string;\n notes: string[];\n channelConfig: Record<string, unknown>;\n};\n\nconst FEISHU_OPEN_BASE_URLS: Record<FeishuDomain, string> = {\n feishu: \"https://open.feishu.cn\",\n lark: \"https://open.larksuite.com\",\n};\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction readRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined;\n}\n\nexport class FeishuAccountConnectionService {\n private readonly store: FeishuAccountStore;\n private readonly fetchImpl: typeof fetch;\n\n constructor(deps: FeishuAccountConnectionServiceDeps = {}) {\n this.store = deps.store ?? new FileFeishuAccountStore();\n this.fetchImpl = deps.fetchImpl ?? fetch;\n }\n\n readonly connect = async (params: FeishuAccountConnectionParams): Promise<FeishuAccountConnectionResult> => {\n const { appId, appSecret, channelConfig, domain: requestedDomain, ownerOpenId, requestedAccountId } = params;\n const currentConfig = normalizeFeishuChannelConfig(channelConfig);\n const domain = requestedDomain ?? currentConfig.domain ?? DEFAULT_FEISHU_DOMAIN;\n const botInfo = await this.probeBot({\n appId,\n appSecret,\n domain,\n });\n const accountId = requestedAccountId?.trim() || appId;\n const replacementAccountIds = this.resolveReplacementAccountIds({\n accountId,\n botOpenId: botInfo.botOpenId,\n currentConfig,\n requestedAccountId,\n });\n for (const replacementAccountId of replacementAccountIds) {\n this.store.deleteAccount(replacementAccountId);\n }\n\n this.store.saveAccount({\n accountId,\n appId,\n appSecret,\n domain,\n botName: botInfo.botName,\n botOpenId: botInfo.botOpenId,\n ownerOpenId,\n savedAt: new Date().toISOString(),\n });\n\n const notes = [\n ...(botInfo.botName ? [`Connected bot: ${botInfo.botName}`] : []),\n ...replacementAccountIds.map((replacementAccountId) => `Replaced previous Feishu agent: ${replacementAccountId}`),\n ...(ownerOpenId ? [`Authorized initial user: ${ownerOpenId}`] : []),\n ];\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"authorized\",\n message: \"飞书智能体已连接。\",\n accountId,\n notes,\n channelConfig: buildRegisteredFeishuChannelConfig({\n config: currentConfig as FeishuChannelConfig,\n accountId,\n domain,\n botName: botInfo.botName,\n allowOpenId: ownerOpenId,\n replaceAccountIds: replacementAccountIds,\n }) as Record<string, unknown>,\n };\n };\n\n private readonly resolveReplacementAccountIds = (params: {\n accountId: string;\n botOpenId?: string;\n currentConfig: FeishuChannelConfig;\n requestedAccountId?: string | null;\n }): string[] => {\n const { accountId, botOpenId, currentConfig, requestedAccountId: requestedAccountIdRaw } = params;\n const replacementIds = new Set<string>();\n const defaultAccountId = currentConfig.defaultAccountId?.trim();\n const requestedAccountId = requestedAccountIdRaw?.trim();\n\n if (!requestedAccountId && defaultAccountId && defaultAccountId !== accountId) {\n replacementIds.add(defaultAccountId);\n }\n if (botOpenId) {\n for (const candidateAccountId of this.store.listAccountIds()) {\n if (candidateAccountId !== accountId && this.store.loadAccount(candidateAccountId)?.botOpenId === botOpenId) {\n replacementIds.add(candidateAccountId);\n }\n }\n }\n\n return [...replacementIds];\n };\n\n private readonly probeBot = async ({\n appId,\n appSecret,\n domain,\n }: {\n appId: string;\n appSecret: string;\n domain: FeishuDomain;\n }): Promise<{ botName?: string; botOpenId?: string }> => {\n const tokenResponse = await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/auth/v3/tenant_access_token/internal`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ app_id: appId, app_secret: appSecret }),\n });\n const tokenData = await tokenResponse.json() as Record<string, unknown>;\n const accessToken = readString(tokenData.tenant_access_token);\n if (!tokenResponse.ok || !accessToken) {\n const message = readString(tokenData.msg) ?? readString(tokenData.error) ?? `HTTP ${tokenResponse.status}`;\n throw new Error(`Feishu credentials could not be verified: ${message}`);\n }\n const botResponse = await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/bot/v3/info`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n const botData = await botResponse.json() as Record<string, unknown>;\n if (!botResponse.ok) {\n const message = readString(botData.msg) ?? readString(botData.error) ?? `HTTP ${botResponse.status}`;\n throw new Error(`Feishu bot info could not be loaded: ${message}`);\n }\n const bot = readRecord(botData.bot) ?? readRecord(readRecord(botData.data)?.bot);\n return {\n botName: readString(bot?.app_name) ?? readString(bot?.bot_name),\n botOpenId: readString(bot?.open_id),\n };\n };\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { FeishuAccountConnectionService } from "./feishu-account-connection.service.js";
1
2
  import { FeishuRegistrationService } from "./feishu-registration.service.js";
2
3
  import { ExtensionCapabilityPayload, ExtensionChannel } from "@nextclaw/extension-sdk";
3
4
 
@@ -5,16 +6,20 @@ import { ExtensionCapabilityPayload, ExtensionChannel } from "@nextclaw/extensio
5
6
  type FeishuAuthCapabilityDeps = {
6
7
  channel: ExtensionChannel;
7
8
  registrationService?: Pick<FeishuRegistrationService, "start" | "poll">;
9
+ accountConnection?: Pick<FeishuAccountConnectionService, "connect">;
8
10
  };
9
11
  declare class FeishuAuthCapability {
10
12
  private readonly channel;
11
13
  private readonly registrationService;
14
+ private readonly accountConnection;
12
15
  constructor(deps: FeishuAuthCapabilityDeps);
13
16
  readonly start: (request: ExtensionCapabilityPayload) => Promise<unknown>;
14
17
  readonly poll: (request: ExtensionCapabilityPayload) => Promise<unknown>;
18
+ readonly connect: (request: ExtensionCapabilityPayload) => Promise<unknown>;
15
19
  private readonly readCurrentConfig;
16
20
  private readonly readString;
17
21
  private readonly readDomain;
22
+ private readonly readRecord;
18
23
  }
19
24
  //#endregion
20
25
  export { FeishuAuthCapability };
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-auth-capability.service.d.ts","names":[],"sources":["../../src/services/feishu-auth-capability.service.ts"],"mappings":";;;;KAOK,wBAAA;EACH,OAAA,EAAS,gBAAA;EACT,mBAAA,GAAsB,IAAA,CAAK,yBAAA;AAAA;AAAA,cAGhB,oBAAA;EAAA,iBACM,OAAA;EAAA,iBACA,mBAAA;cAEL,IAAA,EAAM,wBAAA;EAAA,SAKT,KAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,SAQM,IAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,iBAQc,iBAAA;EAAA,iBAOA,UAAA;EAAA,iBAGA,UAAA;AAAA"}
1
+ {"version":3,"file":"feishu-auth-capability.service.d.ts","names":[],"sources":["../../src/services/feishu-auth-capability.service.ts"],"mappings":";;;;;KAQK,wBAAA;EACH,OAAA,EAAS,gBAAA;EACT,mBAAA,GAAsB,IAAA,CAAK,yBAAA;EAC3B,iBAAA,GAAoB,IAAA,CAAK,8BAAA;AAAA;AAAA,cAGd,oBAAA;EAAA,iBACM,OAAA;EAAA,iBACA,mBAAA;EAAA,iBACA,iBAAA;cAEL,IAAA,EAAM,wBAAA;EAAA,SAMT,KAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,SAQM,IAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,SAQM,OAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,iBAgBc,iBAAA;EAAA,iBAOA,UAAA;EAAA,iBAGA,UAAA;EAAA,iBAKA,UAAA;AAAA"}
@@ -1,14 +1,17 @@
1
+ import { FeishuAccountConnectionService } from "./feishu-account-connection.service.js";
1
2
  import { FeishuRegistrationService } from "./feishu-registration.service.js";
2
3
  //#region src/services/feishu-auth-capability.service.ts
3
4
  var FeishuAuthCapability = class {
4
5
  channel;
5
6
  registrationService;
7
+ accountConnection;
6
8
  constructor(deps) {
7
9
  this.channel = deps.channel;
8
10
  this.registrationService = deps.registrationService ?? new FeishuRegistrationService();
11
+ this.accountConnection = deps.accountConnection ?? new FeishuAccountConnectionService();
9
12
  }
10
13
  start = async (request) => await this.registrationService.start({
11
- pluginConfig: await this.readCurrentConfig(),
14
+ channelConfig: await this.readCurrentConfig(),
12
15
  requestedAccountId: this.readString(request.accountId),
13
16
  domain: this.readDomain(request.domain),
14
17
  verbose: request.verbose === true
@@ -18,6 +21,19 @@ var FeishuAuthCapability = class {
18
21
  if (!sessionId) throw new Error("sessionId is required");
19
22
  return await this.registrationService.poll({ sessionId });
20
23
  };
24
+ connect = async (request) => {
25
+ const fields = this.readRecord(request.fields);
26
+ const appId = this.readString(fields.appId);
27
+ const appSecret = this.readString(fields.appSecret);
28
+ if (!appId || !appSecret) throw new Error("appId and appSecret are required");
29
+ return await this.accountConnection.connect({
30
+ channelConfig: await this.readCurrentConfig(),
31
+ requestedAccountId: this.readString(request.accountId),
32
+ domain: this.readDomain(request.domain),
33
+ appId,
34
+ appSecret
35
+ });
36
+ };
21
37
  readCurrentConfig = async () => {
22
38
  const config = await this.channel.config.get();
23
39
  return config && typeof config === "object" && !Array.isArray(config) ? config : {};
@@ -27,6 +43,9 @@ var FeishuAuthCapability = class {
27
43
  const text = this.readString(value);
28
44
  return text === "feishu" || text === "lark" ? text : null;
29
45
  };
46
+ readRecord = (value) => {
47
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
48
+ };
30
49
  };
31
50
  //#endregion
32
51
  export { FeishuAuthCapability };
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-auth-capability.service.js","names":[],"sources":["../../src/services/feishu-auth-capability.service.ts"],"sourcesContent":["import type {\n ExtensionChannel,\n ExtensionCapabilityPayload,\n} from \"@nextclaw/extension-sdk\";\nimport { FeishuRegistrationService } from \"./feishu-registration.service.js\";\nimport type { FeishuDomain } from \"../types/feishu-extension.types.js\";\n\ntype FeishuAuthCapabilityDeps = {\n channel: ExtensionChannel;\n registrationService?: Pick<FeishuRegistrationService, \"start\" | \"poll\">;\n};\n\nexport class FeishuAuthCapability {\n private readonly channel: ExtensionChannel;\n private readonly registrationService: Pick<FeishuRegistrationService, \"start\" | \"poll\">;\n\n constructor(deps: FeishuAuthCapabilityDeps) {\n this.channel = deps.channel;\n this.registrationService = deps.registrationService ?? new FeishuRegistrationService();\n }\n\n readonly start = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> =>\n await this.registrationService.start({\n pluginConfig: await this.readCurrentConfig(),\n requestedAccountId: this.readString(request.accountId),\n domain: this.readDomain(request.domain),\n verbose: request.verbose === true,\n });\n\n readonly poll = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> => {\n const sessionId = this.readString(request.sessionId);\n if (!sessionId) {\n throw new Error(\"sessionId is required\");\n }\n return await this.registrationService.poll({ sessionId });\n };\n\n private readonly readCurrentConfig = async (): Promise<Record<string, unknown>> => {\n const config = await this.channel.config.get();\n return config && typeof config === \"object\" && !Array.isArray(config)\n ? config as Record<string, unknown>\n : {};\n };\n\n private readonly readString = (value: unknown): string | null =>\n typeof value === \"string\" && value.trim() ? value.trim() : null;\n\n private readonly readDomain = (value: unknown): FeishuDomain | null => {\n const text = this.readString(value);\n return text === \"feishu\" || text === \"lark\" ? text : null;\n };\n}\n"],"mappings":";;AAYA,IAAa,uBAAb,MAAkC;CAChC;CACA;CAEA,YAAY,MAAgC;AAC1C,OAAK,UAAU,KAAK;AACpB,OAAK,sBAAsB,KAAK,uBAAuB,IAAI,2BAA2B;;CAGxF,QAAiB,OACf,YAEA,MAAM,KAAK,oBAAoB,MAAM;EACnC,cAAc,MAAM,KAAK,mBAAmB;EAC5C,oBAAoB,KAAK,WAAW,QAAQ,UAAU;EACtD,QAAQ,KAAK,WAAW,QAAQ,OAAO;EACvC,SAAS,QAAQ,YAAY;EAC9B,CAAC;CAEJ,OAAgB,OACd,YACqB;EACrB,MAAM,YAAY,KAAK,WAAW,QAAQ,UAAU;AACpD,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wBAAwB;AAE1C,SAAO,MAAM,KAAK,oBAAoB,KAAK,EAAE,WAAW,CAAC;;CAG3D,oBAAqC,YAA8C;EACjF,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,KAAK;AAC9C,SAAO,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,OAAO,GACjE,SACA,EAAE;;CAGR,cAA+B,UAC7B,OAAO,UAAU,YAAY,MAAM,MAAM,GAAG,MAAM,MAAM,GAAG;CAE7D,cAA+B,UAAwC;EACrE,MAAM,OAAO,KAAK,WAAW,MAAM;AACnC,SAAO,SAAS,YAAY,SAAS,SAAS,OAAO"}
1
+ {"version":3,"file":"feishu-auth-capability.service.js","names":[],"sources":["../../src/services/feishu-auth-capability.service.ts"],"sourcesContent":["import type {\n ExtensionChannel,\n ExtensionCapabilityPayload,\n} from \"@nextclaw/extension-sdk\";\nimport { FeishuRegistrationService } from \"./feishu-registration.service.js\";\nimport { FeishuAccountConnectionService } from \"./feishu-account-connection.service.js\";\nimport type { FeishuDomain } from \"../types/feishu-extension.types.js\";\n\ntype FeishuAuthCapabilityDeps = {\n channel: ExtensionChannel;\n registrationService?: Pick<FeishuRegistrationService, \"start\" | \"poll\">;\n accountConnection?: Pick<FeishuAccountConnectionService, \"connect\">;\n};\n\nexport class FeishuAuthCapability {\n private readonly channel: ExtensionChannel;\n private readonly registrationService: Pick<FeishuRegistrationService, \"start\" | \"poll\">;\n private readonly accountConnection: Pick<FeishuAccountConnectionService, \"connect\">;\n\n constructor(deps: FeishuAuthCapabilityDeps) {\n this.channel = deps.channel;\n this.registrationService = deps.registrationService ?? new FeishuRegistrationService();\n this.accountConnection = deps.accountConnection ?? new FeishuAccountConnectionService();\n }\n\n readonly start = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> =>\n await this.registrationService.start({\n channelConfig: await this.readCurrentConfig(),\n requestedAccountId: this.readString(request.accountId),\n domain: this.readDomain(request.domain),\n verbose: request.verbose === true,\n });\n\n readonly poll = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> => {\n const sessionId = this.readString(request.sessionId);\n if (!sessionId) {\n throw new Error(\"sessionId is required\");\n }\n return await this.registrationService.poll({ sessionId });\n };\n\n readonly connect = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> => {\n const fields = this.readRecord(request.fields);\n const appId = this.readString(fields.appId);\n const appSecret = this.readString(fields.appSecret);\n if (!appId || !appSecret) {\n throw new Error(\"appId and appSecret are required\");\n }\n return await this.accountConnection.connect({\n channelConfig: await this.readCurrentConfig(),\n requestedAccountId: this.readString(request.accountId),\n domain: this.readDomain(request.domain),\n appId,\n appSecret,\n });\n };\n\n private readonly readCurrentConfig = async (): Promise<Record<string, unknown>> => {\n const config = await this.channel.config.get();\n return config && typeof config === \"object\" && !Array.isArray(config)\n ? config as Record<string, unknown>\n : {};\n };\n\n private readonly readString = (value: unknown): string | null =>\n typeof value === \"string\" && value.trim() ? value.trim() : null;\n\n private readonly readDomain = (value: unknown): FeishuDomain | null => {\n const text = this.readString(value);\n return text === \"feishu\" || text === \"lark\" ? text : null;\n };\n\n private readonly readRecord = (value: unknown): Record<string, unknown> => {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n };\n}\n"],"mappings":";;;AAcA,IAAa,uBAAb,MAAkC;CAChC;CACA;CACA;CAEA,YAAY,MAAgC;AAC1C,OAAK,UAAU,KAAK;AACpB,OAAK,sBAAsB,KAAK,uBAAuB,IAAI,2BAA2B;AACtF,OAAK,oBAAoB,KAAK,qBAAqB,IAAI,gCAAgC;;CAGzF,QAAiB,OACf,YAEA,MAAM,KAAK,oBAAoB,MAAM;EACnC,eAAe,MAAM,KAAK,mBAAmB;EAC7C,oBAAoB,KAAK,WAAW,QAAQ,UAAU;EACtD,QAAQ,KAAK,WAAW,QAAQ,OAAO;EACvC,SAAS,QAAQ,YAAY;EAC9B,CAAC;CAEJ,OAAgB,OACd,YACqB;EACrB,MAAM,YAAY,KAAK,WAAW,QAAQ,UAAU;AACpD,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wBAAwB;AAE1C,SAAO,MAAM,KAAK,oBAAoB,KAAK,EAAE,WAAW,CAAC;;CAG3D,UAAmB,OACjB,YACqB;EACrB,MAAM,SAAS,KAAK,WAAW,QAAQ,OAAO;EAC9C,MAAM,QAAQ,KAAK,WAAW,OAAO,MAAM;EAC3C,MAAM,YAAY,KAAK,WAAW,OAAO,UAAU;AACnD,MAAI,CAAC,SAAS,CAAC,UACb,OAAM,IAAI,MAAM,mCAAmC;AAErD,SAAO,MAAM,KAAK,kBAAkB,QAAQ;GAC1C,eAAe,MAAM,KAAK,mBAAmB;GAC7C,oBAAoB,KAAK,WAAW,QAAQ,UAAU;GACtD,QAAQ,KAAK,WAAW,QAAQ,OAAO;GACvC;GACA;GACD,CAAC;;CAGJ,oBAAqC,YAA8C;EACjF,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,KAAK;AAC9C,SAAO,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,OAAO,GACjE,SACA,EAAE;;CAGR,cAA+B,UAC7B,OAAO,UAAU,YAAY,MAAM,MAAM,GAAG,MAAM,MAAM,GAAG;CAE7D,cAA+B,UAAwC;EACrE,MAAM,OAAO,KAAK,WAAW,MAAM;AACnC,SAAO,SAAS,YAAY,SAAS,SAAS,OAAO;;CAGvD,cAA+B,UAA4C;AACzE,SAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,GAC9D,QACA,EAAE"}
@@ -30,6 +30,7 @@ declare class FeishuChannelAdapter implements FeishuChannelAdapterContract {
30
30
  readonly emitMessageForTest: (message: FeishuInboundMessage) => Promise<void>;
31
31
  private readonly startAccount;
32
32
  private readonly listAvailableAccountIds;
33
+ private readonly listConfiguredAccountIds;
33
34
  private readonly resolveRuntimeAccount;
34
35
  private readonly resolveSelectedAccountId;
35
36
  private readonly resolveSendAccount;
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-channel-adapter.service.d.ts","names":[],"sources":["../../src/services/feishu-channel-adapter.service.ts"],"mappings":";;;;;;KAgDK,iBAAA;EACH,GAAA,GAAM,gBAAA;EACN,KAAA,GAAQ,kBAAA;EACR,MAAA,GAAS,IAAA,QAAY,OAAA;AAAA;AAAA,cA0BV,oBAAA,YAAgC,4BAAA;EAAA,QACnC,cAAA;EAAA,iBACS,GAAA;EAAA,iBACA,KAAA;EAAA,iBACA,MAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;EAAA,iBACA,aAAA;EAAA,iBACA,eAAA;EAAA,iBACA,mBAAA;EAAA,QACT,OAAA;EAAA,QACA,MAAA;cAEI,IAAA,GAAM,iBAAA;EAAA,SAYT,SAAA,GAAmB,MAAA,EAAQ,mBAAA,KAAsB,OAAA;EAAA,SASjD,KAAA,QAAkB,OAAA;EAAA,SAalB,IAAA,QAAiB,OAAA;EAAA,SAiBjB,SAAA,GACP,OAAA,GAAU,OAAA,EAAS,oBAAA,YAAgC,OAAA;EAAA,SAU5C,YAAA,GAAsB,KAAA,EAAO,gBAAA,KAAmB,OAAA;EAAA,SAuBhD,kBAAA,GAA4B,OAAA,EAAS,oBAAA,KAAuB,OAAA;EAAA,iBAIpD,YAAA;EAAA,iBAaA,uBAAA;EAAA,iBAQA,qBAAA;EAAA,iBAuBA,wBAAA;EAAA,iBAcA,kBAAA;EAAA,iBAQA,mBAAA;EAAA,iBAqBA,qBAAA;EAAA,iBAaA,kBAAA;EAAA,iBAsCA,uBAAA;EAAA,iBA2BA,0BAAA;EAAA,iBAoBA,wBAAA;EAAA,iBAoBA,iBAAA;EAAA,iBAiBA,QAAA;EAAA,SAYR,gBAAA,GAA0B,MAAA;IACjC,EAAA;IACA,IAAA;IACA,SAAA;EAAA,MACE,OAAA;AAAA"}
1
+ {"version":3,"file":"feishu-channel-adapter.service.d.ts","names":[],"sources":["../../src/services/feishu-channel-adapter.service.ts"],"mappings":";;;;;;KAgDK,iBAAA;EACH,GAAA,GAAM,gBAAA;EACN,KAAA,GAAQ,kBAAA;EACR,MAAA,GAAS,IAAA,QAAY,OAAA;AAAA;AAAA,cA0BV,oBAAA,YAAgC,4BAAA;EAAA,QACnC,cAAA;EAAA,iBACS,GAAA;EAAA,iBACA,KAAA;EAAA,iBACA,MAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;EAAA,iBACA,aAAA;EAAA,iBACA,eAAA;EAAA,iBACA,mBAAA;EAAA,QACT,OAAA;EAAA,QACA,MAAA;cAEI,IAAA,GAAM,iBAAA;EAAA,SAYT,SAAA,GAAmB,MAAA,EAAQ,mBAAA,KAAsB,OAAA;EAAA,SASjD,KAAA,QAAkB,OAAA;EAAA,SAalB,IAAA,QAAiB,OAAA;EAAA,SAiBjB,SAAA,GACP,OAAA,GAAU,OAAA,EAAS,oBAAA,YAAgC,OAAA;EAAA,SAU5C,YAAA,GAAsB,KAAA,EAAO,gBAAA,KAAmB,OAAA;EAAA,SAuBhD,kBAAA,GAA4B,OAAA,EAAS,oBAAA,KAAuB,OAAA;EAAA,iBAIpD,YAAA;EAAA,iBAaA,uBAAA;EAAA,iBAQA,wBAAA;EAAA,iBAOA,qBAAA;EAAA,iBAuBA,wBAAA;EAAA,iBAcA,kBAAA;EAAA,iBAQA,mBAAA;EAAA,iBAqBA,qBAAA;EAAA,iBAaA,kBAAA;EAAA,iBAsCA,uBAAA;EAAA,iBA2BA,0BAAA;EAAA,iBAoBA,wBAAA;EAAA,iBAoBA,iBAAA;EAAA,iBAiBA,QAAA;EAAA,SAYR,gBAAA,GAA0B,MAAA;IACjC,EAAA;IACA,IAAA;IACA,SAAA;EAAA,MACE,OAAA;AAAA"}
@@ -102,11 +102,12 @@ var FeishuChannelAdapter = class {
102
102
  this.logger.log?.(`[feishu] websocket started for ${account.accountId}`);
103
103
  };
104
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
- ]));
105
+ const configuredAccountIds = this.listConfiguredAccountIds();
106
+ if (configuredAccountIds.length > 0) return configuredAccountIds;
107
+ return this.store.listAccountIds();
108
+ };
109
+ listConfiguredAccountIds = () => {
110
+ return Array.from(new Set([...this.config.defaultAccountId ? [this.config.defaultAccountId] : [], ...Object.keys(this.config.accounts ?? {})]));
110
111
  };
111
112
  resolveRuntimeAccount = (accountId) => {
112
113
  const stored = this.store.loadAccount(accountId);
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-channel-adapter.service.js","names":[],"sources":["../../src/services/feishu-channel-adapter.service.ts"],"sourcesContent":["import type * as Lark from \"@larksuiteoapi/node-sdk\";\nimport { NcpEventType, type NcpEndpointEvent } from \"@nextclaw/ncp\";\nimport { NcpReplyConsumer, type ChatTarget } from \"@nextclaw/ncp-toolkit\";\nimport {\n FileFeishuAccountStore,\n type FeishuAccountStore,\n} from \"../stores/feishu-account.store.js\";\nimport {\n FeishuReplyChat,\n NcpEventQueue,\n TERMINAL_NCP_EVENT_TYPES,\n type FeishuReplySession,\n} from \"./feishu-reply-chat.service.js\";\nimport { FeishuSdkService } from \"./feishu-sdk.service.js\";\nimport {\n isFeishuBotMentioned,\n isFeishuGroupChat,\n parseFeishuInboundMessage,\n} from \"../utils/feishu-message.utils.js\";\nimport {\n readFeishuEventSessionId,\n resolveFeishuSessionRoute,\n} from \"../utils/feishu-session-route.utils.js\";\nimport type {\n FeishuChannelAdapterContract,\n FeishuChannelConfig,\n FeishuInboundMessage,\n FeishuRuntimeAccount,\n} from \"../types/feishu-extension.types.js\";\n\ntype FeishuCanonicalRoute = {\n conversationId: string;\n accountId?: string;\n};\n\ntype FeishuProcessingReactionState = {\n accountId: string;\n messageId: string;\n reactionId: string | null;\n};\n\nconst FEISHU_PROCESSING_REACTION = \"Typing\";\nconst FEISHU_FAILED_REACTION = \"CrossMark\";\nconst FAILED_NCP_EVENT_TYPES = new Set<NcpEndpointEvent[\"type\"]>([\n NcpEventType.MessageFailed,\n NcpEventType.RunError,\n]);\n\ntype FeishuAdapterDeps = {\n sdk?: FeishuSdkService;\n store?: FeishuAccountStore;\n logger?: Pick<typeof console, \"warn\" | \"log\">;\n};\n\ntype FeishuWsHandle = {\n start: (params: { eventDispatcher: Lark.EventDispatcher }) => void;\n close?: () => void;\n stop?: () => void;\n};\n\nfunction readStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value\n .map((entry) => (typeof entry === \"string\" ? entry.trim() : \"\"))\n .filter(Boolean);\n}\n\nfunction normalizeRouteKey(conversationId: string): string {\n return conversationId.toLowerCase();\n}\n\nfunction isAllowedSender(allowFrom: string[], senderId: string): boolean {\n return allowFrom.length === 0 || allowFrom.includes(senderId);\n}\n\nexport class FeishuChannelAdapter implements FeishuChannelAdapterContract {\n private messageHandler: ((message: FeishuInboundMessage) => void | Promise<void>) | null = null;\n private readonly sdk: FeishuSdkService;\n private readonly store: FeishuAccountStore;\n private readonly logger: Pick<typeof console, \"warn\" | \"log\">;\n private readonly replyConsumer: NcpReplyConsumer;\n private readonly wsClients = new Map<string, FeishuWsHandle>();\n private readonly replySessions = new Map<string, FeishuReplySession>();\n private readonly canonicalRoutes = new Map<string, FeishuCanonicalRoute>();\n private readonly processingReactions = new Map<string, FeishuProcessingReactionState>();\n private running = false;\n private config: FeishuChannelConfig = {};\n\n constructor(deps: FeishuAdapterDeps = {}) {\n this.sdk = deps.sdk ?? new FeishuSdkService();\n this.store = deps.store ?? new FileFeishuAccountStore();\n this.logger = deps.logger ?? console;\n this.replyConsumer = new NcpReplyConsumer(\n new FeishuReplyChat({\n resolveAccount: this.resolveSendAccount,\n sendText: this.sendText,\n }),\n );\n }\n\n readonly configure = async (config: FeishuChannelConfig): Promise<void> => {\n this.config = config;\n if (!this.running) {\n return;\n }\n await this.stop();\n await this.start();\n };\n\n readonly start = async (): Promise<void> => {\n if (this.running) {\n return;\n }\n this.running = true;\n for (const accountId of this.listAvailableAccountIds()) {\n const account = this.resolveRuntimeAccount(accountId);\n if (account?.enabled) {\n this.startAccount(account);\n }\n }\n };\n\n readonly stop = async (): Promise<void> => {\n if (!this.running) {\n return;\n }\n this.running = false;\n for (const client of this.wsClients.values()) {\n client.close?.();\n client.stop?.();\n }\n this.wsClients.clear();\n for (const session of this.replySessions.values()) {\n session.queue.close();\n }\n this.replySessions.clear();\n this.processingReactions.clear();\n };\n\n readonly onMessage = (\n handler: (message: FeishuInboundMessage) => void | Promise<void>,\n ): (() => void) => {\n this.messageHandler = handler;\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n }\n };\n };\n\n readonly sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (!this.running) {\n return;\n }\n const route = resolveFeishuSessionRoute(event);\n if (!route) {\n return;\n }\n const sessionId = readFeishuEventSessionId(event);\n if (!sessionId) {\n return;\n }\n const canonicalRoute = this.resolveCanonicalRoute(route);\n const session = this.resolveReplySession(sessionId, canonicalRoute);\n session.queue.push(event);\n if (TERMINAL_NCP_EVENT_TYPES.has(event.type)) {\n session.queue.close();\n this.replySessions.delete(sessionId);\n await session.consuming;\n await this.finalizeProcessingReaction(canonicalRoute, FAILED_NCP_EVENT_TYPES.has(event.type));\n }\n };\n\n readonly emitMessageForTest = async (message: FeishuInboundMessage): Promise<void> => {\n await this.messageHandler?.(message);\n };\n\n private readonly startAccount = (account: FeishuRuntimeAccount): void => {\n const dispatcher = this.sdk.createEventDispatcher();\n dispatcher.register({\n \"im.message.receive_v1\": async (data: unknown) => {\n await this.handleInboundEvent(account, data);\n },\n });\n const wsClient = this.sdk.createWsClient(account) as unknown as FeishuWsHandle;\n wsClient.start({ eventDispatcher: dispatcher });\n this.wsClients.set(account.accountId, wsClient);\n this.logger.log?.(`[feishu] websocket started for ${account.accountId}`);\n };\n\n private readonly listAvailableAccountIds = (): string[] => {\n return Array.from(new Set([\n ...(this.config.defaultAccountId ? [this.config.defaultAccountId] : []),\n ...Object.keys(this.config.accounts ?? {}),\n ...this.store.listAccountIds(),\n ]));\n };\n\n private readonly resolveRuntimeAccount = (accountId: string): FeishuRuntimeAccount | null => {\n const stored = this.store.loadAccount(accountId);\n if (!stored?.appId || !stored.appSecret) {\n return null;\n }\n const accountConfig = this.config.accounts?.[accountId] ?? {};\n return {\n accountId,\n appId: stored.appId,\n appSecret: stored.appSecret,\n domain: accountConfig.domain ?? stored.domain ?? this.config.domain ?? \"feishu\",\n enabled: accountConfig.enabled !== false && this.config.enabled !== false,\n name: accountConfig.name ?? stored.botName,\n botOpenId: stored.botOpenId,\n allowFrom: Array.from(new Set([\n ...readStringArray(this.config.allowFrom),\n ...readStringArray(accountConfig.allowFrom),\n ])),\n groupPolicy: accountConfig.groupPolicy ?? this.config.groupPolicy ?? \"open\",\n requireMention: accountConfig.requireMention ?? this.config.requireMention ?? true,\n };\n };\n\n private readonly resolveSelectedAccountId = (requestedAccountId?: string): string => {\n if (requestedAccountId) {\n return requestedAccountId;\n }\n if (this.config.defaultAccountId) {\n return this.config.defaultAccountId;\n }\n const accountIds = this.listAvailableAccountIds();\n if (accountIds.length === 1 && accountIds[0]) {\n return accountIds[0];\n }\n throw new Error(\"feishu send failed: accountId is required when multiple accounts are configured\");\n };\n\n private readonly resolveSendAccount = (target: ChatTarget): FeishuRuntimeAccount => {\n const account = this.resolveRuntimeAccount(this.resolveSelectedAccountId(target.accountId));\n if (!account?.enabled) {\n throw new Error(`feishu send failed: account \"${target.accountId ?? this.config.defaultAccountId ?? \"\"}\" is not connected`);\n }\n return account;\n };\n\n private readonly resolveReplySession = (\n sessionId: string,\n route: FeishuCanonicalRoute,\n ): FeishuReplySession => {\n const existing = this.replySessions.get(sessionId);\n if (existing) {\n return existing;\n }\n const queue = new NcpEventQueue();\n const consuming = this.replyConsumer.consume({\n target: {\n conversationId: route.conversationId,\n ...(route.accountId ? { accountId: route.accountId } : {}),\n },\n eventStream: queue,\n });\n const session = { queue, consuming };\n this.replySessions.set(sessionId, session);\n return session;\n };\n\n private readonly resolveCanonicalRoute = (\n route: FeishuCanonicalRoute,\n ): FeishuCanonicalRoute => {\n const remembered = this.canonicalRoutes.get(normalizeRouteKey(route.conversationId));\n if (remembered) {\n return {\n ...remembered,\n ...(route.accountId ? { accountId: route.accountId } : {}),\n };\n }\n return route;\n };\n\n private readonly handleInboundEvent = async (\n account: FeishuRuntimeAccount,\n rawEvent: unknown,\n ): Promise<void> => {\n const parsed = parseFeishuInboundMessage(rawEvent);\n if (!parsed || !isAllowedSender(account.allowFrom, parsed.senderOpenId)) {\n return;\n }\n if (isFeishuGroupChat(parsed.chatType)) {\n if (account.groupPolicy === \"disabled\") {\n return;\n }\n if (account.groupPolicy === \"allowlist\" && !account.allowFrom.includes(parsed.chatId)) {\n return;\n }\n if (account.requireMention && !isFeishuBotMentioned({\n botOpenId: account.botOpenId,\n mentionedOpenIds: parsed.mentionedOpenIds,\n })) {\n return;\n }\n }\n this.canonicalRoutes.set(normalizeRouteKey(parsed.chatId), {\n accountId: account.accountId,\n conversationId: parsed.chatId,\n });\n await this.startProcessingReaction(account, parsed.messageId, parsed.chatId);\n await this.messageHandler?.({\n conversationId: parsed.chatId,\n senderId: parsed.senderOpenId,\n text: parsed.text,\n accountId: account.accountId,\n peerKind: isFeishuGroupChat(parsed.chatType) ? \"group\" : \"direct\",\n messageId: parsed.messageId,\n raw: rawEvent,\n });\n };\n\n private readonly startProcessingReaction = async (\n account: FeishuRuntimeAccount,\n messageId: string | undefined,\n conversationId: string,\n ): Promise<void> => {\n if (!messageId) {\n return;\n }\n const routeKey = normalizeRouteKey(conversationId);\n try {\n const reactionId = await this.sdk.addReaction({\n account,\n messageId,\n emojiType: FEISHU_PROCESSING_REACTION,\n });\n this.processingReactions.set(routeKey, {\n accountId: account.accountId,\n messageId,\n reactionId,\n });\n } catch (error) {\n this.logger.warn?.(\n `[feishu] failed to add processing reaction for ${messageId}: ${String(error)}`,\n );\n }\n };\n\n private readonly finalizeProcessingReaction = async (\n route: FeishuCanonicalRoute,\n failed: boolean,\n ): Promise<void> => {\n const routeKey = normalizeRouteKey(route.conversationId);\n const state = this.processingReactions.get(routeKey);\n if (!state) {\n return;\n }\n this.processingReactions.delete(routeKey);\n const account = this.resolveRuntimeAccount(state.accountId);\n if (!account?.enabled) {\n return;\n }\n await this.deleteProcessingReaction(account, state);\n if (failed) {\n await this.addFailedReaction(account, state.messageId);\n }\n };\n\n private readonly deleteProcessingReaction = async (\n account: FeishuRuntimeAccount,\n state: FeishuProcessingReactionState,\n ): Promise<void> => {\n if (!state.reactionId) {\n return;\n }\n try {\n await this.sdk.deleteReaction({\n account,\n messageId: state.messageId,\n reactionId: state.reactionId,\n });\n } catch (error) {\n this.logger.warn?.(\n `[feishu] failed to remove processing reaction for ${state.messageId}: ${String(error)}`,\n );\n }\n };\n\n private readonly addFailedReaction = async (\n account: FeishuRuntimeAccount,\n messageId: string,\n ): Promise<void> => {\n try {\n await this.sdk.addReaction({\n account,\n messageId,\n emojiType: FEISHU_FAILED_REACTION,\n });\n } catch (error) {\n this.logger.warn?.(\n `[feishu] failed to add failure reaction for ${messageId}: ${String(error)}`,\n );\n }\n };\n\n private readonly sendText = async (params: {\n account: FeishuRuntimeAccount;\n conversationId: string;\n text: string;\n }): Promise<void> => {\n await this.sdk.sendText({\n account: params.account,\n chatId: params.conversationId,\n text: params.text,\n });\n };\n\n readonly sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n }): Promise<void> => {\n const { accountId, text, to } = params;\n const account = this.resolveSendAccount({\n conversationId: to,\n ...(accountId ? { accountId } : {}),\n });\n await this.sendText({\n account,\n conversationId: to,\n text,\n });\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"feishu-channel-adapter.service.js","names":[],"sources":["../../src/services/feishu-channel-adapter.service.ts"],"sourcesContent":["import type * as Lark from \"@larksuiteoapi/node-sdk\";\nimport { NcpEventType, type NcpEndpointEvent } from \"@nextclaw/ncp\";\nimport { NcpReplyConsumer, type ChatTarget } from \"@nextclaw/ncp-toolkit\";\nimport {\n FileFeishuAccountStore,\n type FeishuAccountStore,\n} from \"../stores/feishu-account.store.js\";\nimport {\n FeishuReplyChat,\n NcpEventQueue,\n TERMINAL_NCP_EVENT_TYPES,\n type FeishuReplySession,\n} from \"./feishu-reply-chat.service.js\";\nimport { FeishuSdkService } from \"./feishu-sdk.service.js\";\nimport {\n isFeishuBotMentioned,\n isFeishuGroupChat,\n parseFeishuInboundMessage,\n} from \"../utils/feishu-message.utils.js\";\nimport {\n readFeishuEventSessionId,\n resolveFeishuSessionRoute,\n} from \"../utils/feishu-session-route.utils.js\";\nimport type {\n FeishuChannelAdapterContract,\n FeishuChannelConfig,\n FeishuInboundMessage,\n FeishuRuntimeAccount,\n} from \"../types/feishu-extension.types.js\";\n\ntype FeishuCanonicalRoute = {\n conversationId: string;\n accountId?: string;\n};\n\ntype FeishuProcessingReactionState = {\n accountId: string;\n messageId: string;\n reactionId: string | null;\n};\n\nconst FEISHU_PROCESSING_REACTION = \"Typing\";\nconst FEISHU_FAILED_REACTION = \"CrossMark\";\nconst FAILED_NCP_EVENT_TYPES = new Set<NcpEndpointEvent[\"type\"]>([\n NcpEventType.MessageFailed,\n NcpEventType.RunError,\n]);\n\ntype FeishuAdapterDeps = {\n sdk?: FeishuSdkService;\n store?: FeishuAccountStore;\n logger?: Pick<typeof console, \"warn\" | \"log\">;\n};\n\ntype FeishuWsHandle = {\n start: (params: { eventDispatcher: Lark.EventDispatcher }) => void;\n close?: () => void;\n stop?: () => void;\n};\n\nfunction readStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value\n .map((entry) => (typeof entry === \"string\" ? entry.trim() : \"\"))\n .filter(Boolean);\n}\n\nfunction normalizeRouteKey(conversationId: string): string {\n return conversationId.toLowerCase();\n}\n\nfunction isAllowedSender(allowFrom: string[], senderId: string): boolean {\n return allowFrom.length === 0 || allowFrom.includes(senderId);\n}\n\nexport class FeishuChannelAdapter implements FeishuChannelAdapterContract {\n private messageHandler: ((message: FeishuInboundMessage) => void | Promise<void>) | null = null;\n private readonly sdk: FeishuSdkService;\n private readonly store: FeishuAccountStore;\n private readonly logger: Pick<typeof console, \"warn\" | \"log\">;\n private readonly replyConsumer: NcpReplyConsumer;\n private readonly wsClients = new Map<string, FeishuWsHandle>();\n private readonly replySessions = new Map<string, FeishuReplySession>();\n private readonly canonicalRoutes = new Map<string, FeishuCanonicalRoute>();\n private readonly processingReactions = new Map<string, FeishuProcessingReactionState>();\n private running = false;\n private config: FeishuChannelConfig = {};\n\n constructor(deps: FeishuAdapterDeps = {}) {\n this.sdk = deps.sdk ?? new FeishuSdkService();\n this.store = deps.store ?? new FileFeishuAccountStore();\n this.logger = deps.logger ?? console;\n this.replyConsumer = new NcpReplyConsumer(\n new FeishuReplyChat({\n resolveAccount: this.resolveSendAccount,\n sendText: this.sendText,\n }),\n );\n }\n\n readonly configure = async (config: FeishuChannelConfig): Promise<void> => {\n this.config = config;\n if (!this.running) {\n return;\n }\n await this.stop();\n await this.start();\n };\n\n readonly start = async (): Promise<void> => {\n if (this.running) {\n return;\n }\n this.running = true;\n for (const accountId of this.listAvailableAccountIds()) {\n const account = this.resolveRuntimeAccount(accountId);\n if (account?.enabled) {\n this.startAccount(account);\n }\n }\n };\n\n readonly stop = async (): Promise<void> => {\n if (!this.running) {\n return;\n }\n this.running = false;\n for (const client of this.wsClients.values()) {\n client.close?.();\n client.stop?.();\n }\n this.wsClients.clear();\n for (const session of this.replySessions.values()) {\n session.queue.close();\n }\n this.replySessions.clear();\n this.processingReactions.clear();\n };\n\n readonly onMessage = (\n handler: (message: FeishuInboundMessage) => void | Promise<void>,\n ): (() => void) => {\n this.messageHandler = handler;\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n }\n };\n };\n\n readonly sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (!this.running) {\n return;\n }\n const route = resolveFeishuSessionRoute(event);\n if (!route) {\n return;\n }\n const sessionId = readFeishuEventSessionId(event);\n if (!sessionId) {\n return;\n }\n const canonicalRoute = this.resolveCanonicalRoute(route);\n const session = this.resolveReplySession(sessionId, canonicalRoute);\n session.queue.push(event);\n if (TERMINAL_NCP_EVENT_TYPES.has(event.type)) {\n session.queue.close();\n this.replySessions.delete(sessionId);\n await session.consuming;\n await this.finalizeProcessingReaction(canonicalRoute, FAILED_NCP_EVENT_TYPES.has(event.type));\n }\n };\n\n readonly emitMessageForTest = async (message: FeishuInboundMessage): Promise<void> => {\n await this.messageHandler?.(message);\n };\n\n private readonly startAccount = (account: FeishuRuntimeAccount): void => {\n const dispatcher = this.sdk.createEventDispatcher();\n dispatcher.register({\n \"im.message.receive_v1\": async (data: unknown) => {\n await this.handleInboundEvent(account, data);\n },\n });\n const wsClient = this.sdk.createWsClient(account) as unknown as FeishuWsHandle;\n wsClient.start({ eventDispatcher: dispatcher });\n this.wsClients.set(account.accountId, wsClient);\n this.logger.log?.(`[feishu] websocket started for ${account.accountId}`);\n };\n\n private readonly listAvailableAccountIds = (): string[] => {\n const configuredAccountIds = this.listConfiguredAccountIds();\n if (configuredAccountIds.length > 0) {\n return configuredAccountIds;\n }\n return this.store.listAccountIds();\n };\n\n private readonly listConfiguredAccountIds = (): string[] => {\n return Array.from(new Set([\n ...(this.config.defaultAccountId ? [this.config.defaultAccountId] : []),\n ...Object.keys(this.config.accounts ?? {}),\n ]));\n };\n\n private readonly resolveRuntimeAccount = (accountId: string): FeishuRuntimeAccount | null => {\n const stored = this.store.loadAccount(accountId);\n if (!stored?.appId || !stored.appSecret) {\n return null;\n }\n const accountConfig = this.config.accounts?.[accountId] ?? {};\n return {\n accountId,\n appId: stored.appId,\n appSecret: stored.appSecret,\n domain: accountConfig.domain ?? stored.domain ?? this.config.domain ?? \"feishu\",\n enabled: accountConfig.enabled !== false && this.config.enabled !== false,\n name: accountConfig.name ?? stored.botName,\n botOpenId: stored.botOpenId,\n allowFrom: Array.from(new Set([\n ...readStringArray(this.config.allowFrom),\n ...readStringArray(accountConfig.allowFrom),\n ])),\n groupPolicy: accountConfig.groupPolicy ?? this.config.groupPolicy ?? \"open\",\n requireMention: accountConfig.requireMention ?? this.config.requireMention ?? true,\n };\n };\n\n private readonly resolveSelectedAccountId = (requestedAccountId?: string): string => {\n if (requestedAccountId) {\n return requestedAccountId;\n }\n if (this.config.defaultAccountId) {\n return this.config.defaultAccountId;\n }\n const accountIds = this.listAvailableAccountIds();\n if (accountIds.length === 1 && accountIds[0]) {\n return accountIds[0];\n }\n throw new Error(\"feishu send failed: accountId is required when multiple accounts are configured\");\n };\n\n private readonly resolveSendAccount = (target: ChatTarget): FeishuRuntimeAccount => {\n const account = this.resolveRuntimeAccount(this.resolveSelectedAccountId(target.accountId));\n if (!account?.enabled) {\n throw new Error(`feishu send failed: account \"${target.accountId ?? this.config.defaultAccountId ?? \"\"}\" is not connected`);\n }\n return account;\n };\n\n private readonly resolveReplySession = (\n sessionId: string,\n route: FeishuCanonicalRoute,\n ): FeishuReplySession => {\n const existing = this.replySessions.get(sessionId);\n if (existing) {\n return existing;\n }\n const queue = new NcpEventQueue();\n const consuming = this.replyConsumer.consume({\n target: {\n conversationId: route.conversationId,\n ...(route.accountId ? { accountId: route.accountId } : {}),\n },\n eventStream: queue,\n });\n const session = { queue, consuming };\n this.replySessions.set(sessionId, session);\n return session;\n };\n\n private readonly resolveCanonicalRoute = (\n route: FeishuCanonicalRoute,\n ): FeishuCanonicalRoute => {\n const remembered = this.canonicalRoutes.get(normalizeRouteKey(route.conversationId));\n if (remembered) {\n return {\n ...remembered,\n ...(route.accountId ? { accountId: route.accountId } : {}),\n };\n }\n return route;\n };\n\n private readonly handleInboundEvent = async (\n account: FeishuRuntimeAccount,\n rawEvent: unknown,\n ): Promise<void> => {\n const parsed = parseFeishuInboundMessage(rawEvent);\n if (!parsed || !isAllowedSender(account.allowFrom, parsed.senderOpenId)) {\n return;\n }\n if (isFeishuGroupChat(parsed.chatType)) {\n if (account.groupPolicy === \"disabled\") {\n return;\n }\n if (account.groupPolicy === \"allowlist\" && !account.allowFrom.includes(parsed.chatId)) {\n return;\n }\n if (account.requireMention && !isFeishuBotMentioned({\n botOpenId: account.botOpenId,\n mentionedOpenIds: parsed.mentionedOpenIds,\n })) {\n return;\n }\n }\n this.canonicalRoutes.set(normalizeRouteKey(parsed.chatId), {\n accountId: account.accountId,\n conversationId: parsed.chatId,\n });\n await this.startProcessingReaction(account, parsed.messageId, parsed.chatId);\n await this.messageHandler?.({\n conversationId: parsed.chatId,\n senderId: parsed.senderOpenId,\n text: parsed.text,\n accountId: account.accountId,\n peerKind: isFeishuGroupChat(parsed.chatType) ? \"group\" : \"direct\",\n messageId: parsed.messageId,\n raw: rawEvent,\n });\n };\n\n private readonly startProcessingReaction = async (\n account: FeishuRuntimeAccount,\n messageId: string | undefined,\n conversationId: string,\n ): Promise<void> => {\n if (!messageId) {\n return;\n }\n const routeKey = normalizeRouteKey(conversationId);\n try {\n const reactionId = await this.sdk.addReaction({\n account,\n messageId,\n emojiType: FEISHU_PROCESSING_REACTION,\n });\n this.processingReactions.set(routeKey, {\n accountId: account.accountId,\n messageId,\n reactionId,\n });\n } catch (error) {\n this.logger.warn?.(\n `[feishu] failed to add processing reaction for ${messageId}: ${String(error)}`,\n );\n }\n };\n\n private readonly finalizeProcessingReaction = async (\n route: FeishuCanonicalRoute,\n failed: boolean,\n ): Promise<void> => {\n const routeKey = normalizeRouteKey(route.conversationId);\n const state = this.processingReactions.get(routeKey);\n if (!state) {\n return;\n }\n this.processingReactions.delete(routeKey);\n const account = this.resolveRuntimeAccount(state.accountId);\n if (!account?.enabled) {\n return;\n }\n await this.deleteProcessingReaction(account, state);\n if (failed) {\n await this.addFailedReaction(account, state.messageId);\n }\n };\n\n private readonly deleteProcessingReaction = async (\n account: FeishuRuntimeAccount,\n state: FeishuProcessingReactionState,\n ): Promise<void> => {\n if (!state.reactionId) {\n return;\n }\n try {\n await this.sdk.deleteReaction({\n account,\n messageId: state.messageId,\n reactionId: state.reactionId,\n });\n } catch (error) {\n this.logger.warn?.(\n `[feishu] failed to remove processing reaction for ${state.messageId}: ${String(error)}`,\n );\n }\n };\n\n private readonly addFailedReaction = async (\n account: FeishuRuntimeAccount,\n messageId: string,\n ): Promise<void> => {\n try {\n await this.sdk.addReaction({\n account,\n messageId,\n emojiType: FEISHU_FAILED_REACTION,\n });\n } catch (error) {\n this.logger.warn?.(\n `[feishu] failed to add failure reaction for ${messageId}: ${String(error)}`,\n );\n }\n };\n\n private readonly sendText = async (params: {\n account: FeishuRuntimeAccount;\n conversationId: string;\n text: string;\n }): Promise<void> => {\n await this.sdk.sendText({\n account: params.account,\n chatId: params.conversationId,\n text: params.text,\n });\n };\n\n readonly sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n }): Promise<void> => {\n const { accountId, text, to } = params;\n const account = this.resolveSendAccount({\n conversationId: to,\n ...(accountId ? { accountId } : {}),\n });\n await this.sendText({\n account,\n conversationId: to,\n text,\n });\n };\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { FeishuDomain } from "../types/feishu-extension.types.js";
2
2
  import { FeishuAccountStore } from "../stores/feishu-account.store.js";
3
+ import { FeishuAccountConnectionService } from "./feishu-account-connection.service.js";
3
4
 
4
5
  //#region src/services/feishu-registration.service.d.ts
5
6
  type FeishuRegistrationStartParams = {
6
- pluginConfig?: Record<string, unknown>;
7
+ channelConfig?: Record<string, unknown>;
7
8
  requestedAccountId?: string | null;
8
9
  domain?: FeishuDomain | null;
9
10
  verbose?: boolean;
@@ -25,14 +26,16 @@ type FeishuAuthPollResult = {
25
26
  nextPollMs?: number;
26
27
  accountId?: string | null;
27
28
  notes?: string[];
28
- pluginConfig?: Record<string, unknown>;
29
+ channelConfig?: Record<string, unknown>;
29
30
  };
30
31
  type FeishuRegistrationServiceDeps = {
31
32
  store?: FeishuAccountStore;
32
33
  fetchImpl?: typeof fetch;
34
+ accountConnection?: Pick<FeishuAccountConnectionService, "connect">;
33
35
  };
34
36
  declare class FeishuRegistrationService {
35
37
  private readonly store;
38
+ private readonly accountConnection;
36
39
  private readonly fetchImpl;
37
40
  private readonly sessions;
38
41
  constructor(deps?: FeishuRegistrationServiceDeps);
@@ -46,7 +49,6 @@ declare class FeishuRegistrationService {
46
49
  private readonly beginRegistration;
47
50
  private readonly postRegistration;
48
51
  private readonly confirmRegistration;
49
- private readonly probeBot;
50
52
  private readonly readRecord;
51
53
  private readonly cleanupExpiredSessions;
52
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-registration.service.d.ts","names":[],"sources":["../../src/services/feishu-registration.service.ts"],"mappings":";;;;KAaY,6BAAA;EACV,YAAA,GAAe,MAAA;EACf,kBAAA;EACA,MAAA,GAAS,YAAA;EACT,OAAA;AAAA;AAAA,KAGU,qBAAA;EACV,OAAA;EACA,IAAA;EACA,SAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,IAAA;AAAA;AAAA,KAGU,oBAAA;EACV,OAAA;EACA,MAAA;EACA,OAAA;EACA,UAAA;EACA,SAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;AAAA;AAAA,KAcZ,6BAAA;EACH,KAAA,GAAQ,kBAAA;EACR,SAAA,UAAmB,KAAA;AAAA;AAAA,cA8BR,yBAAA;EAAA,iBACM,KAAA;EAAA,iBACA,SAAA;EAAA,iBACA,QAAA;cAEL,IAAA,GAAM,6BAAA;EAAA,SAKT,KAAA,GACP,MAAA,EAAQ,6BAAA,KACP,OAAA,CAAQ,qBAAA;EAAA,SA6BF,IAAA;IAAc;EAAA;IAAiB,SAAA;EAAA,MAAsB,OAAA,CAAQ,oBAAA;EAAA,iBAsErD,2BAAA;EAAA,iBAUA,iBAAA;EAAA,iBA2BA,gBAAA;EAAA,iBAgBA,mBAAA;EAAA,iBA6CA,QAAA;EAAA,iBAiCA,UAAA;EAAA,iBAOA,sBAAA;AAAA"}
1
+ {"version":3,"file":"feishu-registration.service.d.ts","names":[],"sources":["../../src/services/feishu-registration.service.ts"],"mappings":";;;;;KAaY,6BAAA;EACV,aAAA,GAAgB,MAAA;EAChB,kBAAA;EACA,MAAA,GAAS,YAAA;EACT,OAAA;AAAA;AAAA,KAGU,qBAAA;EACV,OAAA;EACA,IAAA;EACA,SAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,IAAA;AAAA;AAAA,KAGU,oBAAA;EACV,OAAA;EACA,MAAA;EACA,OAAA;EACA,UAAA;EACA,SAAA;EACA,KAAA;EACA,aAAA,GAAgB,MAAA;AAAA;AAAA,KAcb,6BAAA;EACH,KAAA,GAAQ,kBAAA;EACR,SAAA,UAAmB,KAAA;EACnB,iBAAA,GAAoB,IAAA,CAAK,8BAAA;AAAA;AAAA,cA0Bd,yBAAA;EAAA,iBACM,KAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,SAAA;EAAA,iBACA,QAAA;cAEL,IAAA,GAAM,6BAAA;EAAA,SAST,KAAA,GACP,MAAA,EAAQ,6BAAA,KACP,OAAA,CAAQ,qBAAA;EAAA,SA6BF,IAAA;IAAc;EAAA;IAAiB,SAAA;EAAA,MAAsB,OAAA,CAAQ,oBAAA;EAAA,iBAsErD,2BAAA;EAAA,iBAUA,iBAAA;EAAA,iBA2BA,gBAAA;EAAA,iBAgBA,mBAAA;EAAA,iBA0BA,UAAA;EAAA,iBAOA,sBAAA;AAAA"}
@@ -1,5 +1,6 @@
1
- import { FEISHU_CHANNEL_ID, buildRegisteredFeishuChannelConfig, normalizeFeishuChannelConfig } from "../utils/feishu-config.utils.js";
1
+ import { FEISHU_CHANNEL_ID, normalizeFeishuChannelConfig } from "../utils/feishu-config.utils.js";
2
2
  import { FileFeishuAccountStore } from "../stores/feishu-account.store.js";
3
+ import { FeishuAccountConnectionService } from "./feishu-account-connection.service.js";
3
4
  import { randomUUID } from "node:crypto";
4
5
  //#region src/services/feishu-registration.service.ts
5
6
  const REGISTRATION_PATH = "/oauth/v1/app/registration";
@@ -7,10 +8,6 @@ const FEISHU_ACCOUNTS_BASE_URLS = {
7
8
  feishu: "https://accounts.feishu.cn",
8
9
  lark: "https://accounts.larksuite.com"
9
10
  };
10
- const FEISHU_OPEN_BASE_URLS = {
11
- feishu: "https://open.feishu.cn",
12
- lark: "https://open.larksuite.com"
13
- };
14
11
  const FEISHU_AUTH_POLL_INTERVAL_MS = 5e3;
15
12
  const FEISHU_REGISTRATION_TIMEOUT_MS = 10 * 6e4;
16
13
  function readString(value) {
@@ -25,15 +22,20 @@ function appendHermesQrHints(url) {
25
22
  }
26
23
  var FeishuRegistrationService = class {
27
24
  store;
25
+ accountConnection;
28
26
  fetchImpl;
29
27
  sessions = /* @__PURE__ */ new Map();
30
28
  constructor(deps = {}) {
31
29
  this.store = deps.store ?? new FileFeishuAccountStore();
32
30
  this.fetchImpl = deps.fetchImpl ?? fetch;
31
+ this.accountConnection = deps.accountConnection ?? new FeishuAccountConnectionService({
32
+ store: this.store,
33
+ fetchImpl: this.fetchImpl
34
+ });
33
35
  }
34
36
  start = async (params) => {
35
37
  this.cleanupExpiredSessions();
36
- const currentConfig = normalizeFeishuChannelConfig(params.pluginConfig);
38
+ const currentConfig = normalizeFeishuChannelConfig(params.channelConfig);
37
39
  const domain = params.domain ?? currentConfig.domain ?? "feishu";
38
40
  await this.assertRegistrationSupported(domain);
39
41
  const begin = await this.beginRegistration(domain);
@@ -96,7 +98,7 @@ var FeishuRegistrationService = class {
96
98
  nextPollMs: 0,
97
99
  accountId: result.accountId,
98
100
  notes: result.notes,
99
- pluginConfig: result.pluginConfig
101
+ channelConfig: result.channelConfig
100
102
  };
101
103
  }
102
104
  const error = readString(response.error);
@@ -154,52 +156,18 @@ var FeishuRegistrationService = class {
154
156
  return data;
155
157
  };
156
158
  confirmRegistration = async ({ appId, appSecret, ownerOpenId, session }) => {
157
- const botInfo = await this.probeBot({
158
- appId,
159
- appSecret,
160
- domain: session.domain
161
- });
162
- const accountId = session.requestedAccountId?.trim() || appId;
163
- this.store.saveAccount({
164
- accountId,
159
+ const result = await this.accountConnection.connect({
160
+ channelConfig: session.currentConfig,
161
+ requestedAccountId: session.requestedAccountId,
165
162
  appId,
166
163
  appSecret,
167
164
  domain: session.domain,
168
- botName: botInfo.botName,
169
- botOpenId: botInfo.botOpenId,
170
- ownerOpenId,
171
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
165
+ ownerOpenId
172
166
  });
173
167
  return {
174
- accountId,
175
- notes: [...botInfo.botName ? [`Connected bot: ${botInfo.botName}`] : [], ...ownerOpenId ? [`Authorized initial user: ${ownerOpenId}`] : []],
176
- pluginConfig: buildRegisteredFeishuChannelConfig({
177
- config: session.currentConfig,
178
- accountId,
179
- domain: session.domain,
180
- botName: botInfo.botName,
181
- allowOpenId: ownerOpenId
182
- })
183
- };
184
- };
185
- probeBot = async ({ appId, appSecret, domain }) => {
186
- const accessToken = readString((await (await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/auth/v3/tenant_access_token/internal`, {
187
- method: "POST",
188
- headers: { "Content-Type": "application/json" },
189
- body: JSON.stringify({
190
- app_id: appId,
191
- app_secret: appSecret
192
- })
193
- })).json()).tenant_access_token);
194
- if (!accessToken) return {};
195
- const botData = await (await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/bot/v3/info`, { headers: {
196
- Authorization: `Bearer ${accessToken}`,
197
- "Content-Type": "application/json"
198
- } })).json();
199
- const bot = this.readRecord(botData.bot) ?? this.readRecord(this.readRecord(botData.data)?.bot);
200
- return {
201
- botName: readString(bot?.app_name) ?? readString(bot?.bot_name),
202
- botOpenId: readString(bot?.open_id)
168
+ accountId: result.accountId,
169
+ notes: result.notes,
170
+ channelConfig: result.channelConfig
203
171
  };
204
172
  };
205
173
  readRecord = (value) => {
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-registration.service.js","names":[],"sources":["../../src/services/feishu-registration.service.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n buildRegisteredFeishuChannelConfig,\n DEFAULT_FEISHU_DOMAIN,\n FEISHU_CHANNEL_ID,\n normalizeFeishuChannelConfig,\n} from \"../utils/feishu-config.utils.js\";\nimport {\n FileFeishuAccountStore,\n type FeishuAccountStore,\n} from \"../stores/feishu-account.store.js\";\nimport type { FeishuChannelConfig, FeishuDomain } from \"../types/feishu-extension.types.js\";\n\nexport type FeishuRegistrationStartParams = {\n pluginConfig?: Record<string, unknown>;\n requestedAccountId?: string | null;\n domain?: FeishuDomain | null;\n verbose?: boolean;\n};\n\nexport type FeishuAuthStartResult = {\n channel: string;\n kind: \"qr_code\";\n sessionId: string;\n qrCode: string;\n qrCodeUrl: string;\n expiresAt: string;\n intervalMs: number;\n note?: string;\n};\n\nexport type FeishuAuthPollResult = {\n channel: string;\n status: \"pending\" | \"scanned\" | \"authorized\" | \"expired\" | \"error\";\n message?: string;\n nextPollMs?: number;\n accountId?: string | null;\n notes?: string[];\n pluginConfig?: Record<string, unknown>;\n};\n\ntype FeishuRegistrationSession = {\n currentConfig: FeishuChannelConfig;\n requestedAccountId?: string | null;\n deviceCode: string;\n domain: FeishuDomain;\n intervalMs: number;\n expiresAtMs: number;\n};\n\ntype FeishuRegistrationResponse = Record<string, unknown>;\n\ntype FeishuRegistrationServiceDeps = {\n store?: FeishuAccountStore;\n fetchImpl?: typeof fetch;\n};\n\nconst REGISTRATION_PATH = \"/oauth/v1/app/registration\";\nconst FEISHU_ACCOUNTS_BASE_URLS: Record<FeishuDomain, string> = {\n feishu: \"https://accounts.feishu.cn\",\n lark: \"https://accounts.larksuite.com\",\n};\nconst FEISHU_OPEN_BASE_URLS: Record<FeishuDomain, string> = {\n feishu: \"https://open.feishu.cn\",\n lark: \"https://open.larksuite.com\",\n};\nconst FEISHU_AUTH_POLL_INTERVAL_MS = 5_000;\nconst FEISHU_REGISTRATION_TIMEOUT_MS = 10 * 60_000;\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction readDomain(value: unknown): FeishuDomain | undefined {\n return value === \"feishu\" || value === \"lark\" ? value : undefined;\n}\n\nfunction appendHermesQrHints(url: string): string {\n if (!url.trim()) {\n return url;\n }\n return `${url}${url.includes(\"?\") ? \"&\" : \"?\"}from=nextclaw&tp=nextclaw`;\n}\n\nexport class FeishuRegistrationService {\n private readonly store: FeishuAccountStore;\n private readonly fetchImpl: typeof fetch;\n private readonly sessions = new Map<string, FeishuRegistrationSession>();\n\n constructor(deps: FeishuRegistrationServiceDeps = {}) {\n this.store = deps.store ?? new FileFeishuAccountStore();\n this.fetchImpl = deps.fetchImpl ?? fetch;\n }\n\n readonly start = async (\n params: FeishuRegistrationStartParams,\n ): Promise<FeishuAuthStartResult> => {\n this.cleanupExpiredSessions();\n const currentConfig = normalizeFeishuChannelConfig(params.pluginConfig);\n const domain = params.domain ?? currentConfig.domain ?? DEFAULT_FEISHU_DOMAIN;\n await this.assertRegistrationSupported(domain);\n const begin = await this.beginRegistration(domain);\n const sessionId = randomUUID();\n const expiresAtMs = Date.now() + Math.min(begin.expiresInMs, FEISHU_REGISTRATION_TIMEOUT_MS);\n this.sessions.set(sessionId, {\n currentConfig,\n requestedAccountId: params.requestedAccountId,\n deviceCode: begin.deviceCode,\n domain,\n intervalMs: begin.intervalMs,\n expiresAtMs,\n });\n\n return {\n channel: FEISHU_CHANNEL_ID,\n kind: \"qr_code\",\n sessionId,\n qrCode: begin.qrUrl,\n qrCodeUrl: begin.qrUrl,\n expiresAt: new Date(expiresAtMs).toISOString(),\n intervalMs: begin.intervalMs,\n note: \"请使用飞书或 Lark 扫码授权,NextClaw 会自动创建机器人应用并保存连接信息。\",\n };\n };\n\n readonly poll = async ({ sessionId }: { sessionId: string }): Promise<FeishuAuthPollResult | null> => {\n this.cleanupExpiredSessions();\n const session = this.sessions.get(sessionId);\n if (!session) {\n return null;\n }\n if (session.expiresAtMs <= Date.now()) {\n this.sessions.delete(sessionId);\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"expired\",\n message: \"飞书扫码授权已过期,请重新开始。\",\n };\n }\n\n try {\n const response = await this.postRegistration(session.domain, {\n action: \"poll\",\n device_code: session.deviceCode,\n tp: \"ob_app\",\n });\n const userInfo = this.readRecord(response.user_info) ?? {};\n const switchedDomain = readDomain(userInfo.tenant_brand) ?? session.domain;\n if (switchedDomain !== session.domain) {\n session.domain = switchedDomain;\n }\n const clientId = readString(response.client_id);\n const clientSecret = readString(response.client_secret);\n if (clientId && clientSecret) {\n const result = await this.confirmRegistration({\n session,\n appId: clientId,\n appSecret: clientSecret,\n ownerOpenId: readString(userInfo.open_id),\n });\n this.sessions.delete(sessionId);\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"authorized\",\n message: \"飞书已连接。\",\n nextPollMs: 0,\n accountId: result.accountId,\n notes: result.notes,\n pluginConfig: result.pluginConfig,\n };\n }\n\n const error = readString(response.error);\n if (error === \"access_denied\" || error === \"expired_token\") {\n this.sessions.delete(sessionId);\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"expired\",\n message: error === \"access_denied\" ? \"飞书授权已取消。\" : \"飞书扫码授权已过期。\",\n };\n }\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"pending\",\n nextPollMs: session.intervalMs,\n };\n } catch (error) {\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n private readonly assertRegistrationSupported = async (domain: FeishuDomain): Promise<void> => {\n const response = await this.postRegistration(domain, { action: \"init\" });\n const methods = Array.isArray(response.supported_auth_methods)\n ? response.supported_auth_methods\n : [];\n if (!methods.includes(\"client_secret\")) {\n throw new Error(`Feishu registration does not support client_secret auth. Supported: ${methods.join(\", \")}`);\n }\n };\n\n private readonly beginRegistration = async (\n domain: FeishuDomain,\n ): Promise<{\n deviceCode: string;\n qrUrl: string;\n intervalMs: number;\n expiresInMs: number;\n }> => {\n const response = await this.postRegistration(domain, {\n action: \"begin\",\n archetype: \"PersonalAgent\",\n auth_method: \"client_secret\",\n request_user_info: \"open_id\",\n });\n const deviceCode = readString(response.device_code);\n const qrUrl = readString(response.verification_uri_complete);\n if (!deviceCode || !qrUrl) {\n throw new Error(\"Feishu registration did not return a device code and QR URL.\");\n }\n return {\n deviceCode,\n qrUrl: appendHermesQrHints(qrUrl),\n intervalMs: Math.max(1, Number(response.interval ?? 5)) * 1000 || FEISHU_AUTH_POLL_INTERVAL_MS,\n expiresInMs: Math.max(60, Number(response.expire_in ?? 600)) * 1000,\n };\n };\n\n private readonly postRegistration = async (\n domain: FeishuDomain,\n body: Record<string, string>,\n ): Promise<FeishuRegistrationResponse> => {\n const response = await this.fetchImpl(`${FEISHU_ACCOUNTS_BASE_URLS[domain]}${REGISTRATION_PATH}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(body).toString(),\n });\n const data = await response.json() as FeishuRegistrationResponse;\n if (!response.ok && !data.error) {\n throw new Error(`Feishu registration failed: HTTP ${response.status}`);\n }\n return data;\n };\n\n private readonly confirmRegistration = async ({\n appId,\n appSecret,\n ownerOpenId,\n session,\n }: {\n session: FeishuRegistrationSession;\n appId: string;\n appSecret: string;\n ownerOpenId?: string;\n }): Promise<{ pluginConfig: Record<string, unknown>; accountId: string; notes: string[] }> => {\n const botInfo = await this.probeBot({\n appId,\n appSecret,\n domain: session.domain,\n });\n const accountId = session.requestedAccountId?.trim() || appId;\n this.store.saveAccount({\n accountId,\n appId,\n appSecret,\n domain: session.domain,\n botName: botInfo.botName,\n botOpenId: botInfo.botOpenId,\n ownerOpenId,\n savedAt: new Date().toISOString(),\n });\n\n const notes = [\n ...(botInfo.botName ? [`Connected bot: ${botInfo.botName}`] : []),\n ...(ownerOpenId ? [`Authorized initial user: ${ownerOpenId}`] : []),\n ];\n return {\n accountId,\n notes,\n pluginConfig: buildRegisteredFeishuChannelConfig({\n config: session.currentConfig,\n accountId,\n domain: session.domain,\n botName: botInfo.botName,\n allowOpenId: ownerOpenId,\n }) as Record<string, unknown>,\n };\n };\n\n private readonly probeBot = async ({\n appId,\n appSecret,\n domain,\n }: {\n appId: string;\n appSecret: string;\n domain: FeishuDomain;\n }): Promise<{ botName?: string; botOpenId?: string }> => {\n const tokenResponse = await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/auth/v3/tenant_access_token/internal`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ app_id: appId, app_secret: appSecret }),\n });\n const tokenData = await tokenResponse.json() as Record<string, unknown>;\n const accessToken = readString(tokenData.tenant_access_token);\n if (!accessToken) {\n return {};\n }\n const botResponse = await this.fetchImpl(`${FEISHU_OPEN_BASE_URLS[domain]}/open-apis/bot/v3/info`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n const botData = await botResponse.json() as Record<string, unknown>;\n const bot = this.readRecord(botData.bot) ?? this.readRecord(this.readRecord(botData.data)?.bot);\n return {\n botName: readString(bot?.app_name) ?? readString(bot?.bot_name),\n botOpenId: readString(bot?.open_id),\n };\n };\n\n private readonly readRecord = (value: unknown): Record<string, unknown> | undefined => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n return value as Record<string, unknown>;\n };\n\n private readonly cleanupExpiredSessions = (now = Date.now()): void => {\n for (const [sessionId, session] of this.sessions.entries()) {\n if (session.expiresAtMs <= now) {\n this.sessions.delete(sessionId);\n }\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"feishu-registration.service.js","names":[],"sources":["../../src/services/feishu-registration.service.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n DEFAULT_FEISHU_DOMAIN,\n FEISHU_CHANNEL_ID,\n normalizeFeishuChannelConfig,\n} from \"../utils/feishu-config.utils.js\";\nimport {\n FileFeishuAccountStore,\n type FeishuAccountStore,\n} from \"../stores/feishu-account.store.js\";\nimport { FeishuAccountConnectionService } from \"./feishu-account-connection.service.js\";\nimport type { FeishuChannelConfig, FeishuDomain } from \"../types/feishu-extension.types.js\";\n\nexport type FeishuRegistrationStartParams = {\n channelConfig?: Record<string, unknown>;\n requestedAccountId?: string | null;\n domain?: FeishuDomain | null;\n verbose?: boolean;\n};\n\nexport type FeishuAuthStartResult = {\n channel: string;\n kind: \"qr_code\";\n sessionId: string;\n qrCode: string;\n qrCodeUrl: string;\n expiresAt: string;\n intervalMs: number;\n note?: string;\n};\n\nexport type FeishuAuthPollResult = {\n channel: string;\n status: \"pending\" | \"scanned\" | \"authorized\" | \"expired\" | \"error\";\n message?: string;\n nextPollMs?: number;\n accountId?: string | null;\n notes?: string[];\n channelConfig?: Record<string, unknown>;\n};\n\ntype FeishuRegistrationSession = {\n currentConfig: FeishuChannelConfig;\n requestedAccountId?: string | null;\n deviceCode: string;\n domain: FeishuDomain;\n intervalMs: number;\n expiresAtMs: number;\n};\n\ntype FeishuRegistrationResponse = Record<string, unknown>;\n\ntype FeishuRegistrationServiceDeps = {\n store?: FeishuAccountStore;\n fetchImpl?: typeof fetch;\n accountConnection?: Pick<FeishuAccountConnectionService, \"connect\">;\n};\n\nconst REGISTRATION_PATH = \"/oauth/v1/app/registration\";\nconst FEISHU_ACCOUNTS_BASE_URLS: Record<FeishuDomain, string> = {\n feishu: \"https://accounts.feishu.cn\",\n lark: \"https://accounts.larksuite.com\",\n};\nconst FEISHU_AUTH_POLL_INTERVAL_MS = 5_000;\nconst FEISHU_REGISTRATION_TIMEOUT_MS = 10 * 60_000;\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction readDomain(value: unknown): FeishuDomain | undefined {\n return value === \"feishu\" || value === \"lark\" ? value : undefined;\n}\n\nfunction appendHermesQrHints(url: string): string {\n if (!url.trim()) {\n return url;\n }\n return `${url}${url.includes(\"?\") ? \"&\" : \"?\"}from=nextclaw&tp=nextclaw`;\n}\n\nexport class FeishuRegistrationService {\n private readonly store: FeishuAccountStore;\n private readonly accountConnection: Pick<FeishuAccountConnectionService, \"connect\">;\n private readonly fetchImpl: typeof fetch;\n private readonly sessions = new Map<string, FeishuRegistrationSession>();\n\n constructor(deps: FeishuRegistrationServiceDeps = {}) {\n this.store = deps.store ?? new FileFeishuAccountStore();\n this.fetchImpl = deps.fetchImpl ?? fetch;\n this.accountConnection = deps.accountConnection ?? new FeishuAccountConnectionService({\n store: this.store,\n fetchImpl: this.fetchImpl,\n });\n }\n\n readonly start = async (\n params: FeishuRegistrationStartParams,\n ): Promise<FeishuAuthStartResult> => {\n this.cleanupExpiredSessions();\n const currentConfig = normalizeFeishuChannelConfig(params.channelConfig);\n const domain = params.domain ?? currentConfig.domain ?? DEFAULT_FEISHU_DOMAIN;\n await this.assertRegistrationSupported(domain);\n const begin = await this.beginRegistration(domain);\n const sessionId = randomUUID();\n const expiresAtMs = Date.now() + Math.min(begin.expiresInMs, FEISHU_REGISTRATION_TIMEOUT_MS);\n this.sessions.set(sessionId, {\n currentConfig,\n requestedAccountId: params.requestedAccountId,\n deviceCode: begin.deviceCode,\n domain,\n intervalMs: begin.intervalMs,\n expiresAtMs,\n });\n\n return {\n channel: FEISHU_CHANNEL_ID,\n kind: \"qr_code\",\n sessionId,\n qrCode: begin.qrUrl,\n qrCodeUrl: begin.qrUrl,\n expiresAt: new Date(expiresAtMs).toISOString(),\n intervalMs: begin.intervalMs,\n note: \"请使用飞书或 Lark 扫码授权,NextClaw 会自动创建机器人应用并保存连接信息。\",\n };\n };\n\n readonly poll = async ({ sessionId }: { sessionId: string }): Promise<FeishuAuthPollResult | null> => {\n this.cleanupExpiredSessions();\n const session = this.sessions.get(sessionId);\n if (!session) {\n return null;\n }\n if (session.expiresAtMs <= Date.now()) {\n this.sessions.delete(sessionId);\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"expired\",\n message: \"飞书扫码授权已过期,请重新开始。\",\n };\n }\n\n try {\n const response = await this.postRegistration(session.domain, {\n action: \"poll\",\n device_code: session.deviceCode,\n tp: \"ob_app\",\n });\n const userInfo = this.readRecord(response.user_info) ?? {};\n const switchedDomain = readDomain(userInfo.tenant_brand) ?? session.domain;\n if (switchedDomain !== session.domain) {\n session.domain = switchedDomain;\n }\n const clientId = readString(response.client_id);\n const clientSecret = readString(response.client_secret);\n if (clientId && clientSecret) {\n const result = await this.confirmRegistration({\n session,\n appId: clientId,\n appSecret: clientSecret,\n ownerOpenId: readString(userInfo.open_id),\n });\n this.sessions.delete(sessionId);\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"authorized\",\n message: \"飞书已连接。\",\n nextPollMs: 0,\n accountId: result.accountId,\n notes: result.notes,\n channelConfig: result.channelConfig,\n };\n }\n\n const error = readString(response.error);\n if (error === \"access_denied\" || error === \"expired_token\") {\n this.sessions.delete(sessionId);\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"expired\",\n message: error === \"access_denied\" ? \"飞书授权已取消。\" : \"飞书扫码授权已过期。\",\n };\n }\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"pending\",\n nextPollMs: session.intervalMs,\n };\n } catch (error) {\n return {\n channel: FEISHU_CHANNEL_ID,\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n private readonly assertRegistrationSupported = async (domain: FeishuDomain): Promise<void> => {\n const response = await this.postRegistration(domain, { action: \"init\" });\n const methods = Array.isArray(response.supported_auth_methods)\n ? response.supported_auth_methods\n : [];\n if (!methods.includes(\"client_secret\")) {\n throw new Error(`Feishu registration does not support client_secret auth. Supported: ${methods.join(\", \")}`);\n }\n };\n\n private readonly beginRegistration = async (\n domain: FeishuDomain,\n ): Promise<{\n deviceCode: string;\n qrUrl: string;\n intervalMs: number;\n expiresInMs: number;\n }> => {\n const response = await this.postRegistration(domain, {\n action: \"begin\",\n archetype: \"PersonalAgent\",\n auth_method: \"client_secret\",\n request_user_info: \"open_id\",\n });\n const deviceCode = readString(response.device_code);\n const qrUrl = readString(response.verification_uri_complete);\n if (!deviceCode || !qrUrl) {\n throw new Error(\"Feishu registration did not return a device code and QR URL.\");\n }\n return {\n deviceCode,\n qrUrl: appendHermesQrHints(qrUrl),\n intervalMs: Math.max(1, Number(response.interval ?? 5)) * 1000 || FEISHU_AUTH_POLL_INTERVAL_MS,\n expiresInMs: Math.max(60, Number(response.expire_in ?? 600)) * 1000,\n };\n };\n\n private readonly postRegistration = async (\n domain: FeishuDomain,\n body: Record<string, string>,\n ): Promise<FeishuRegistrationResponse> => {\n const response = await this.fetchImpl(`${FEISHU_ACCOUNTS_BASE_URLS[domain]}${REGISTRATION_PATH}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(body).toString(),\n });\n const data = await response.json() as FeishuRegistrationResponse;\n if (!response.ok && !data.error) {\n throw new Error(`Feishu registration failed: HTTP ${response.status}`);\n }\n return data;\n };\n\n private readonly confirmRegistration = async ({\n appId,\n appSecret,\n ownerOpenId,\n session,\n }: {\n session: FeishuRegistrationSession;\n appId: string;\n appSecret: string;\n ownerOpenId?: string;\n }): Promise<{ channelConfig: Record<string, unknown>; accountId: string; notes: string[] }> => {\n const result = await this.accountConnection.connect({\n channelConfig: session.currentConfig,\n requestedAccountId: session.requestedAccountId,\n appId,\n appSecret,\n domain: session.domain,\n ownerOpenId,\n });\n return {\n accountId: result.accountId,\n notes: result.notes,\n channelConfig: result.channelConfig,\n };\n };\n\n private readonly readRecord = (value: unknown): Record<string, unknown> | undefined => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n return value as Record<string, unknown>;\n };\n\n private readonly cleanupExpiredSessions = (now = Date.now()): void => {\n for (const [sessionId, session] of this.sessions.entries()) {\n if (session.expiresAtMs <= now) {\n this.sessions.delete(sessionId);\n }\n }\n };\n}\n"],"mappings":"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"}
@@ -50,8 +50,10 @@ function normalizeFeishuChannelConfig(value) {
50
50
  accounts: Object.keys(accounts).length > 0 ? accounts : void 0
51
51
  };
52
52
  }
53
- function buildRegisteredFeishuChannelConfig({ accountId, allowOpenId, botName, config, domain }) {
53
+ function buildRegisteredFeishuChannelConfig({ accountId, allowOpenId, botName, config, domain, replaceAccountIds }) {
54
54
  const current = normalizeFeishuChannelConfig(config);
55
+ const replacementIds = new Set((replaceAccountIds ?? []).map((accountId) => readString(accountId)).filter((replacementAccountId) => Boolean(replacementAccountId) && replacementAccountId !== accountId));
56
+ const accounts = Object.fromEntries(Object.entries(current.accounts ?? {}).filter(([accountId]) => !replacementIds.has(accountId)));
55
57
  const currentAccount = current.accounts?.[accountId] ?? {};
56
58
  const allowFrom = new Set([
57
59
  ...current.allowFrom ?? [],
@@ -64,7 +66,7 @@ function buildRegisteredFeishuChannelConfig({ accountId, allowOpenId, botName, c
64
66
  defaultAccountId: accountId,
65
67
  domain: current.domain ?? domain,
66
68
  accounts: {
67
- ...current.accounts ?? {},
69
+ ...accounts,
68
70
  [accountId]: {
69
71
  ...currentAccount,
70
72
  enabled: true,
@@ -1 +1 @@
1
- {"version":3,"file":"feishu-config.utils.js","names":[],"sources":["../../src/utils/feishu-config.utils.ts"],"sourcesContent":["import type {\n FeishuAccountConfig,\n FeishuChannelConfig,\n FeishuDomain,\n} from \"../types/feishu-extension.types.js\";\n\nexport const FEISHU_CHANNEL_ID = \"feishu\";\nexport const DEFAULT_FEISHU_DOMAIN: FeishuDomain = \"feishu\";\n\nfunction toRecord(value: unknown): Record<string, unknown> | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n return value as Record<string, unknown>;\n}\n\nfunction readString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n\nfunction readStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) {\n return undefined;\n }\n const values = value\n .map((entry) => readString(entry))\n .filter((entry): entry is string => Boolean(entry));\n return values.length > 0 ? values : undefined;\n}\n\nfunction readDomain(value: unknown): FeishuDomain | undefined {\n return value === \"lark\" || value === \"feishu\" ? value : undefined;\n}\n\nfunction readGroupPolicy(value: unknown): FeishuAccountConfig[\"groupPolicy\"] {\n return value === \"open\" || value === \"allowlist\" || value === \"disabled\"\n ? value\n : undefined;\n}\n\nfunction normalizeAccountConfig(value: unknown): FeishuAccountConfig | undefined {\n const record = toRecord(value);\n if (!record) {\n return undefined;\n }\n return {\n enabled: typeof record.enabled === \"boolean\" ? record.enabled : undefined,\n name: readString(record.name),\n domain: readDomain(record.domain),\n allowFrom: readStringArray(record.allowFrom),\n groupPolicy: readGroupPolicy(record.groupPolicy),\n requireMention: typeof record.requireMention === \"boolean\" ? record.requireMention : undefined,\n };\n}\n\nexport function normalizeFeishuChannelConfig(value: unknown): FeishuChannelConfig {\n const record = toRecord(value);\n if (!record) {\n return {};\n }\n\n const accounts: Record<string, FeishuAccountConfig> = {};\n for (const [accountId, rawAccountConfig] of Object.entries(toRecord(record.accounts) ?? {})) {\n const normalized = normalizeAccountConfig(rawAccountConfig);\n if (normalized) {\n accounts[accountId] = normalized;\n }\n }\n\n return {\n enabled: typeof record.enabled === \"boolean\" ? record.enabled : undefined,\n defaultAccountId: readString(record.defaultAccountId),\n domain: readDomain(record.domain),\n allowFrom: readStringArray(record.allowFrom),\n groupPolicy: readGroupPolicy(record.groupPolicy),\n requireMention: typeof record.requireMention === \"boolean\" ? record.requireMention : undefined,\n accounts: Object.keys(accounts).length > 0 ? accounts : undefined,\n };\n}\n\nexport function buildRegisteredFeishuChannelConfig({\n accountId,\n allowOpenId,\n botName,\n config,\n domain,\n}: {\n config: FeishuChannelConfig;\n accountId: string;\n domain: FeishuDomain;\n botName?: string;\n allowOpenId?: string;\n}): FeishuChannelConfig {\n const current = normalizeFeishuChannelConfig(config);\n const currentAccount = current.accounts?.[accountId] ?? {};\n const allowFrom = new Set([\n ...(current.allowFrom ?? []),\n ...(currentAccount.allowFrom ?? []),\n ...(allowOpenId ? [allowOpenId] : []),\n ]);\n\n return {\n ...current,\n enabled: true,\n defaultAccountId: accountId,\n domain: current.domain ?? domain,\n accounts: {\n ...(current.accounts ?? {}),\n [accountId]: {\n ...currentAccount,\n enabled: true,\n domain,\n name: currentAccount.name ?? botName,\n allowFrom: allowFrom.size > 0 ? [...allowFrom] : undefined,\n },\n },\n };\n}\n"],"mappings":";AAMA,MAAa,oBAAoB;AACjC,MAAa,wBAAsC;AAEnD,SAAS,SAAS,OAAqD;AACrE,KAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7D;AAEF,QAAO;;AAGT,SAAS,WAAW,OAAoC;AACtD,KAAI,OAAO,UAAU,SACnB;AAGF,QADgB,MAAM,MAAM,IACV,KAAA;;AAGpB,SAAS,gBAAgB,OAAsC;AAC7D,KAAI,CAAC,MAAM,QAAQ,MAAM,CACvB;CAEF,MAAM,SAAS,MACZ,KAAK,UAAU,WAAW,MAAM,CAAC,CACjC,QAAQ,UAA2B,QAAQ,MAAM,CAAC;AACrD,QAAO,OAAO,SAAS,IAAI,SAAS,KAAA;;AAGtC,SAAS,WAAW,OAA0C;AAC5D,QAAO,UAAU,UAAU,UAAU,WAAW,QAAQ,KAAA;;AAG1D,SAAS,gBAAgB,OAAoD;AAC3E,QAAO,UAAU,UAAU,UAAU,eAAe,UAAU,aAC1D,QACA,KAAA;;AAGN,SAAS,uBAAuB,OAAiD;CAC/E,MAAM,SAAS,SAAS,MAAM;AAC9B,KAAI,CAAC,OACH;AAEF,QAAO;EACL,SAAS,OAAO,OAAO,YAAY,YAAY,OAAO,UAAU,KAAA;EAChE,MAAM,WAAW,OAAO,KAAK;EAC7B,QAAQ,WAAW,OAAO,OAAO;EACjC,WAAW,gBAAgB,OAAO,UAAU;EAC5C,aAAa,gBAAgB,OAAO,YAAY;EAChD,gBAAgB,OAAO,OAAO,mBAAmB,YAAY,OAAO,iBAAiB,KAAA;EACtF;;AAGH,SAAgB,6BAA6B,OAAqC;CAChF,MAAM,SAAS,SAAS,MAAM;AAC9B,KAAI,CAAC,OACH,QAAO,EAAE;CAGX,MAAM,WAAgD,EAAE;AACxD,MAAK,MAAM,CAAC,WAAW,qBAAqB,OAAO,QAAQ,SAAS,OAAO,SAAS,IAAI,EAAE,CAAC,EAAE;EAC3F,MAAM,aAAa,uBAAuB,iBAAiB;AAC3D,MAAI,WACF,UAAS,aAAa;;AAI1B,QAAO;EACL,SAAS,OAAO,OAAO,YAAY,YAAY,OAAO,UAAU,KAAA;EAChE,kBAAkB,WAAW,OAAO,iBAAiB;EACrD,QAAQ,WAAW,OAAO,OAAO;EACjC,WAAW,gBAAgB,OAAO,UAAU;EAC5C,aAAa,gBAAgB,OAAO,YAAY;EAChD,gBAAgB,OAAO,OAAO,mBAAmB,YAAY,OAAO,iBAAiB,KAAA;EACrF,UAAU,OAAO,KAAK,SAAS,CAAC,SAAS,IAAI,WAAW,KAAA;EACzD;;AAGH,SAAgB,mCAAmC,EACjD,WACA,aACA,SACA,QACA,UAOsB;CACtB,MAAM,UAAU,6BAA6B,OAAO;CACpD,MAAM,iBAAiB,QAAQ,WAAW,cAAc,EAAE;CAC1D,MAAM,YAAY,IAAI,IAAI;EACxB,GAAI,QAAQ,aAAa,EAAE;EAC3B,GAAI,eAAe,aAAa,EAAE;EAClC,GAAI,cAAc,CAAC,YAAY,GAAG,EAAE;EACrC,CAAC;AAEF,QAAO;EACL,GAAG;EACH,SAAS;EACT,kBAAkB;EAClB,QAAQ,QAAQ,UAAU;EAC1B,UAAU;GACR,GAAI,QAAQ,YAAY,EAAE;IACzB,YAAY;IACX,GAAG;IACH,SAAS;IACT;IACA,MAAM,eAAe,QAAQ;IAC7B,WAAW,UAAU,OAAO,IAAI,CAAC,GAAG,UAAU,GAAG,KAAA;IAClD;GACF;EACF"}
1
+ {"version":3,"file":"feishu-config.utils.js","names":[],"sources":["../../src/utils/feishu-config.utils.ts"],"sourcesContent":["import type {\n FeishuAccountConfig,\n FeishuChannelConfig,\n FeishuDomain,\n} from \"../types/feishu-extension.types.js\";\n\nexport const FEISHU_CHANNEL_ID = \"feishu\";\nexport const DEFAULT_FEISHU_DOMAIN: FeishuDomain = \"feishu\";\n\nfunction toRecord(value: unknown): Record<string, unknown> | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n return value as Record<string, unknown>;\n}\n\nfunction readString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n\nfunction readStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) {\n return undefined;\n }\n const values = value\n .map((entry) => readString(entry))\n .filter((entry): entry is string => Boolean(entry));\n return values.length > 0 ? values : undefined;\n}\n\nfunction readDomain(value: unknown): FeishuDomain | undefined {\n return value === \"lark\" || value === \"feishu\" ? value : undefined;\n}\n\nfunction readGroupPolicy(value: unknown): FeishuAccountConfig[\"groupPolicy\"] {\n return value === \"open\" || value === \"allowlist\" || value === \"disabled\"\n ? value\n : undefined;\n}\n\nfunction normalizeAccountConfig(value: unknown): FeishuAccountConfig | undefined {\n const record = toRecord(value);\n if (!record) {\n return undefined;\n }\n return {\n enabled: typeof record.enabled === \"boolean\" ? record.enabled : undefined,\n name: readString(record.name),\n domain: readDomain(record.domain),\n allowFrom: readStringArray(record.allowFrom),\n groupPolicy: readGroupPolicy(record.groupPolicy),\n requireMention: typeof record.requireMention === \"boolean\" ? record.requireMention : undefined,\n };\n}\n\nexport function normalizeFeishuChannelConfig(value: unknown): FeishuChannelConfig {\n const record = toRecord(value);\n if (!record) {\n return {};\n }\n\n const accounts: Record<string, FeishuAccountConfig> = {};\n for (const [accountId, rawAccountConfig] of Object.entries(toRecord(record.accounts) ?? {})) {\n const normalized = normalizeAccountConfig(rawAccountConfig);\n if (normalized) {\n accounts[accountId] = normalized;\n }\n }\n\n return {\n enabled: typeof record.enabled === \"boolean\" ? record.enabled : undefined,\n defaultAccountId: readString(record.defaultAccountId),\n domain: readDomain(record.domain),\n allowFrom: readStringArray(record.allowFrom),\n groupPolicy: readGroupPolicy(record.groupPolicy),\n requireMention: typeof record.requireMention === \"boolean\" ? record.requireMention : undefined,\n accounts: Object.keys(accounts).length > 0 ? accounts : undefined,\n };\n}\n\nexport function buildRegisteredFeishuChannelConfig({\n accountId,\n allowOpenId,\n botName,\n config,\n domain,\n replaceAccountIds,\n}: {\n config: FeishuChannelConfig;\n accountId: string;\n domain: FeishuDomain;\n botName?: string;\n allowOpenId?: string;\n replaceAccountIds?: string[];\n}): FeishuChannelConfig {\n const current = normalizeFeishuChannelConfig(config);\n const replacementIds = new Set(\n (replaceAccountIds ?? [])\n .map((accountId) => readString(accountId))\n .filter((replacementAccountId): replacementAccountId is string =>\n Boolean(replacementAccountId) && replacementAccountId !== accountId\n ),\n );\n const accounts = Object.fromEntries(\n Object.entries(current.accounts ?? {}).filter(([accountId]) => !replacementIds.has(accountId)),\n );\n const currentAccount = current.accounts?.[accountId] ?? {};\n const allowFrom = new Set([\n ...(current.allowFrom ?? []),\n ...(currentAccount.allowFrom ?? []),\n ...(allowOpenId ? [allowOpenId] : []),\n ]);\n\n return {\n ...current,\n enabled: true,\n defaultAccountId: accountId,\n domain: current.domain ?? domain,\n accounts: {\n ...accounts,\n [accountId]: {\n ...currentAccount,\n enabled: true,\n domain,\n name: currentAccount.name ?? botName,\n allowFrom: allowFrom.size > 0 ? [...allowFrom] : undefined,\n },\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-extension-feishu",
3
- "version": "0.1.7",
3
+ "version": "0.1.9-beta.0",
4
4
  "private": false,
5
5
  "description": "NextClaw Feishu/Lark lightweight channel extension process.",
6
6
  "type": "module",
@@ -19,9 +19,9 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@larksuiteoapi/node-sdk": "^1.59.0",
22
- "@nextclaw/extension-sdk": "0.1.10",
23
- "@nextclaw/ncp-toolkit": "0.5.21",
24
- "@nextclaw/ncp": "0.5.16"
22
+ "@nextclaw/extension-sdk": "0.1.12-beta.0",
23
+ "@nextclaw/ncp": "0.5.18-beta.0",
24
+ "@nextclaw/ncp-toolkit": "0.5.23-beta.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^20.17.6",