@openclaw/feishu 2026.3.2 → 2026.3.7
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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
import { vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export function createFeishuClientMockModule(): {
|
|
4
|
+
createFeishuWSClient: () => { start: () => void };
|
|
5
|
+
createEventDispatcher: () => { register: () => void };
|
|
6
|
+
} {
|
|
7
|
+
return {
|
|
8
|
+
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
|
|
9
|
+
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
export function createFeishuRuntimeMockModule(): {
|
|
14
|
+
getFeishuRuntime: () => {
|
|
15
|
+
channel: {
|
|
16
|
+
debounce: {
|
|
17
|
+
resolveInboundDebounceMs: () => number;
|
|
18
|
+
createInboundDebouncer: () => {
|
|
19
|
+
enqueue: () => Promise<void>;
|
|
20
|
+
flushKey: () => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
text: {
|
|
24
|
+
hasControlCommand: () => boolean;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
} {
|
|
29
|
+
return {
|
|
30
|
+
getFeishuRuntime: () => ({
|
|
31
|
+
channel: {
|
|
32
|
+
debounce: {
|
|
33
|
+
resolveInboundDebounceMs: () => 0,
|
|
34
|
+
createInboundDebouncer: () => ({
|
|
35
|
+
enqueue: async () => {},
|
|
36
|
+
flushKey: async () => {},
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
text: {
|
|
40
|
+
hasControlCommand: () => false,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
package/src/monitor.transport.ts
CHANGED
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
applyBasicWebhookRequestGuards,
|
|
5
5
|
type RuntimeEnv,
|
|
6
6
|
installRequestBodyLimitGuard,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
7
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
8
8
|
import { createFeishuWSClient } from "./client.js";
|
|
9
9
|
import {
|
|
10
|
+
botNames,
|
|
10
11
|
botOpenIds,
|
|
11
12
|
FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
|
|
12
13
|
FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
@@ -42,6 +43,7 @@ export async function monitorWebSocket({
|
|
|
42
43
|
const cleanup = () => {
|
|
43
44
|
wsClients.delete(accountId);
|
|
44
45
|
botOpenIds.delete(accountId);
|
|
46
|
+
botNames.delete(accountId);
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
const handleAbort = () => {
|
|
@@ -134,6 +136,7 @@ export async function monitorWebhook({
|
|
|
134
136
|
server.close();
|
|
135
137
|
httpServers.delete(accountId);
|
|
136
138
|
botOpenIds.delete(accountId);
|
|
139
|
+
botNames.delete(accountId);
|
|
137
140
|
};
|
|
138
141
|
|
|
139
142
|
const handleAbort = () => {
|
package/src/monitor.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js";
|
|
3
3
|
import {
|
|
4
4
|
monitorSingleAccount,
|
|
5
5
|
resolveReactionSyntheticEvent,
|
|
6
6
|
type FeishuReactionCreatedEvent,
|
|
7
7
|
} from "./monitor.account.js";
|
|
8
|
-
import {
|
|
8
|
+
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
|
|
9
9
|
import {
|
|
10
10
|
clearFeishuWebhookRateLimitStateForTest,
|
|
11
11
|
getFeishuWebhookRateLimitStateSizeForTest,
|
|
@@ -66,7 +66,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Probe sequentially so large multi-account startups do not burst Feishu's bot-info endpoint.
|
|
69
|
-
const botOpenId = await
|
|
69
|
+
const { botOpenId, botName } = await fetchBotIdentityForMonitor(account, {
|
|
70
70
|
runtime: opts.runtime,
|
|
71
71
|
abortSignal: opts.abortSignal,
|
|
72
72
|
});
|
|
@@ -82,7 +82,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
82
82
|
account,
|
|
83
83
|
runtime: opts.runtime,
|
|
84
84
|
abortSignal: opts.abortSignal,
|
|
85
|
-
botOpenIdSource: { kind: "prefetched", botOpenId },
|
|
85
|
+
botOpenIdSource: { kind: "prefetched", botOpenId, botName },
|
|
86
86
|
}),
|
|
87
87
|
);
|
|
88
88
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import type { AddressInfo } from "node:net";
|
|
3
|
-
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
createFeishuClientMockModule,
|
|
7
|
+
createFeishuRuntimeMockModule,
|
|
8
|
+
} from "./monitor.test-mocks.js";
|
|
5
9
|
|
|
6
10
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
7
11
|
|
|
@@ -9,27 +13,8 @@ vi.mock("./probe.js", () => ({
|
|
|
9
13
|
probeFeishu: probeFeishuMock,
|
|
10
14
|
}));
|
|
11
15
|
|
|
12
|
-
vi.mock("./client.js", () => (
|
|
13
|
-
|
|
14
|
-
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock("./runtime.js", () => ({
|
|
18
|
-
getFeishuRuntime: () => ({
|
|
19
|
-
channel: {
|
|
20
|
-
debounce: {
|
|
21
|
-
resolveInboundDebounceMs: () => 0,
|
|
22
|
-
createInboundDebouncer: () => ({
|
|
23
|
-
enqueue: async () => {},
|
|
24
|
-
flushKey: async () => {},
|
|
25
|
-
}),
|
|
26
|
-
},
|
|
27
|
-
text: {
|
|
28
|
-
hasControlCommand: () => false,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
32
|
-
}));
|
|
16
|
+
vi.mock("./client.js", () => createFeishuClientMockModule());
|
|
17
|
+
vi.mock("./runtime.js", () => createFeishuRuntimeMockModule());
|
|
33
18
|
|
|
34
19
|
vi.mock("@larksuiteoapi/node-sdk", () => ({
|
|
35
20
|
adaptDefault: vi.fn(
|
|
@@ -88,7 +73,7 @@ function buildConfig(params: {
|
|
|
88
73
|
[params.accountId]: {
|
|
89
74
|
enabled: true,
|
|
90
75
|
appId: "cli_test",
|
|
91
|
-
appSecret: "secret_test",
|
|
76
|
+
appSecret: "secret_test", // pragma: allowlist secret
|
|
92
77
|
connectionMode: "webhook",
|
|
93
78
|
webhookHost: "127.0.0.1",
|
|
94
79
|
webhookPort: params.port,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./probe.js", () => ({
|
|
4
|
+
probeFeishu: vi.fn(async () => ({ ok: false, error: "mocked" })),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
8
|
+
|
|
9
|
+
const baseConfigureContext = {
|
|
10
|
+
runtime: {} as never,
|
|
11
|
+
accountOverrides: {},
|
|
12
|
+
shouldPromptAccountIds: false,
|
|
13
|
+
forceAllowFrom: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const baseStatusContext = {
|
|
17
|
+
accountOverrides: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function withEnvVars(values: Record<string, string | undefined>, run: () => Promise<void>) {
|
|
21
|
+
const previous = new Map<string, string | undefined>();
|
|
22
|
+
for (const [key, value] of Object.entries(values)) {
|
|
23
|
+
previous.set(key, process.env[key]);
|
|
24
|
+
if (value === undefined) {
|
|
25
|
+
delete process.env[key];
|
|
26
|
+
} else {
|
|
27
|
+
process.env[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await run();
|
|
33
|
+
} finally {
|
|
34
|
+
for (const [key, prior] of previous.entries()) {
|
|
35
|
+
if (prior === undefined) {
|
|
36
|
+
delete process.env[key];
|
|
37
|
+
} else {
|
|
38
|
+
process.env[key] = prior;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: string }) {
|
|
45
|
+
return await feishuOnboardingAdapter.getStatus({
|
|
46
|
+
cfg: {
|
|
47
|
+
channels: {
|
|
48
|
+
feishu: {
|
|
49
|
+
appId: { source: "env", id: params.appIdKey, provider: "default" },
|
|
50
|
+
appSecret: { source: "env", id: params.appSecretKey, provider: "default" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
} as never,
|
|
54
|
+
...baseStatusContext,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("feishuOnboardingAdapter.configure", () => {
|
|
59
|
+
it("does not throw when config appId/appSecret are SecretRef objects", async () => {
|
|
60
|
+
const text = vi
|
|
61
|
+
.fn()
|
|
62
|
+
.mockResolvedValueOnce("cli_from_prompt")
|
|
63
|
+
.mockResolvedValueOnce("secret_from_prompt")
|
|
64
|
+
.mockResolvedValueOnce("oc_group_1");
|
|
65
|
+
|
|
66
|
+
const prompter = {
|
|
67
|
+
note: vi.fn(async () => undefined),
|
|
68
|
+
text,
|
|
69
|
+
confirm: vi.fn(async () => true),
|
|
70
|
+
select: vi.fn(
|
|
71
|
+
async ({ initialValue }: { initialValue?: string }) => initialValue ?? "allowlist",
|
|
72
|
+
),
|
|
73
|
+
} as never;
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
feishuOnboardingAdapter.configure({
|
|
77
|
+
cfg: {
|
|
78
|
+
channels: {
|
|
79
|
+
feishu: {
|
|
80
|
+
appId: { source: "env", id: "FEISHU_APP_ID", provider: "default" },
|
|
81
|
+
appSecret: { source: "env", id: "FEISHU_APP_SECRET", provider: "default" },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
} as never,
|
|
85
|
+
prompter,
|
|
86
|
+
...baseConfigureContext,
|
|
87
|
+
}),
|
|
88
|
+
).resolves.toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("feishuOnboardingAdapter.getStatus", () => {
|
|
93
|
+
it("does not fallback to top-level appId when account explicitly sets empty appId", async () => {
|
|
94
|
+
const status = await feishuOnboardingAdapter.getStatus({
|
|
95
|
+
cfg: {
|
|
96
|
+
channels: {
|
|
97
|
+
feishu: {
|
|
98
|
+
appId: "top_level_app",
|
|
99
|
+
accounts: {
|
|
100
|
+
main: {
|
|
101
|
+
appId: "",
|
|
102
|
+
appSecret: "sample-app-credential", // pragma: allowlist secret
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
} as never,
|
|
108
|
+
...baseStatusContext,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(status.configured).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("treats env SecretRef appId as not configured when env var is missing", async () => {
|
|
115
|
+
const appIdKey = "FEISHU_APP_ID_STATUS_MISSING_TEST";
|
|
116
|
+
const appSecretKey = "FEISHU_APP_CREDENTIAL_STATUS_MISSING_TEST"; // pragma: allowlist secret
|
|
117
|
+
await withEnvVars(
|
|
118
|
+
{
|
|
119
|
+
[appIdKey]: undefined,
|
|
120
|
+
[appSecretKey]: "env-credential-456", // pragma: allowlist secret
|
|
121
|
+
},
|
|
122
|
+
async () => {
|
|
123
|
+
const status = await getStatusWithEnvRefs({ appIdKey, appSecretKey });
|
|
124
|
+
expect(status.configured).toBe(false);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("treats env SecretRef appId/appSecret as configured in status", async () => {
|
|
130
|
+
const appIdKey = "FEISHU_APP_ID_STATUS_TEST";
|
|
131
|
+
const appSecretKey = "FEISHU_APP_CREDENTIAL_STATUS_TEST"; // pragma: allowlist secret
|
|
132
|
+
await withEnvVars(
|
|
133
|
+
{
|
|
134
|
+
[appIdKey]: "cli_env_123",
|
|
135
|
+
[appSecretKey]: "env-credential-456", // pragma: allowlist secret
|
|
136
|
+
},
|
|
137
|
+
async () => {
|
|
138
|
+
const status = await getStatusWithEnvRefs({ appIdKey, appSecretKey });
|
|
139
|
+
expect(status.configured).toBe(true);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
package/src/onboarding.ts
CHANGED
|
@@ -5,56 +5,47 @@ import type {
|
|
|
5
5
|
DmPolicy,
|
|
6
6
|
SecretInput,
|
|
7
7
|
WizardPrompter,
|
|
8
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
buildSingleChannelSecretPromptState,
|
|
11
11
|
DEFAULT_ACCOUNT_ID,
|
|
12
12
|
formatDocsLink,
|
|
13
13
|
hasConfiguredSecretInput,
|
|
14
|
+
mergeAllowFromEntries,
|
|
14
15
|
promptSingleChannelSecretInput,
|
|
15
|
-
|
|
16
|
+
setTopLevelChannelAllowFrom,
|
|
17
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
18
|
+
setTopLevelChannelGroupPolicy,
|
|
19
|
+
splitOnboardingEntries,
|
|
20
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
16
21
|
import { resolveFeishuCredentials } from "./accounts.js";
|
|
17
22
|
import { probeFeishu } from "./probe.js";
|
|
18
23
|
import type { FeishuConfig } from "./types.js";
|
|
19
24
|
|
|
20
25
|
const channel = "feishu" as const;
|
|
21
26
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
-
...cfg,
|
|
29
|
-
channels: {
|
|
30
|
-
...cfg.channels,
|
|
31
|
-
feishu: {
|
|
32
|
-
...cfg.channels?.feishu,
|
|
33
|
-
dmPolicy,
|
|
34
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
27
|
+
function normalizeString(value: unknown): string | undefined {
|
|
28
|
+
if (typeof value !== "string") {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed || undefined;
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
function
|
|
41
|
-
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
...cfg.channels?.feishu,
|
|
47
|
-
allowFrom,
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
};
|
|
35
|
+
function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
36
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
37
|
+
cfg,
|
|
38
|
+
channel: "feishu",
|
|
39
|
+
dmPolicy,
|
|
40
|
+
}) as ClawdbotConfig;
|
|
51
41
|
}
|
|
52
42
|
|
|
53
|
-
function
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
44
|
+
return setTopLevelChannelAllowFrom({
|
|
45
|
+
cfg,
|
|
46
|
+
channel: "feishu",
|
|
47
|
+
allowFrom,
|
|
48
|
+
}) as ClawdbotConfig;
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
async function promptFeishuAllowFrom(params: {
|
|
@@ -80,18 +71,13 @@ async function promptFeishuAllowFrom(params: {
|
|
|
80
71
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
81
72
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
82
73
|
});
|
|
83
|
-
const parts =
|
|
74
|
+
const parts = splitOnboardingEntries(String(entry));
|
|
84
75
|
if (parts.length === 0) {
|
|
85
76
|
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
86
77
|
continue;
|
|
87
78
|
}
|
|
88
79
|
|
|
89
|
-
const unique =
|
|
90
|
-
...new Set([
|
|
91
|
-
...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
|
|
92
|
-
...parts,
|
|
93
|
-
]),
|
|
94
|
-
];
|
|
80
|
+
const unique = mergeAllowFromEntries(existing, parts);
|
|
95
81
|
return setFeishuAllowFrom(params.cfg, unique);
|
|
96
82
|
}
|
|
97
83
|
}
|
|
@@ -129,17 +115,12 @@ function setFeishuGroupPolicy(
|
|
|
129
115
|
cfg: ClawdbotConfig,
|
|
130
116
|
groupPolicy: "open" | "allowlist" | "disabled",
|
|
131
117
|
): ClawdbotConfig {
|
|
132
|
-
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
enabled: true,
|
|
139
|
-
groupPolicy,
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
};
|
|
118
|
+
return setTopLevelChannelGroupPolicy({
|
|
119
|
+
cfg,
|
|
120
|
+
channel: "feishu",
|
|
121
|
+
groupPolicy,
|
|
122
|
+
enabled: true,
|
|
123
|
+
}) as ClawdbotConfig;
|
|
143
124
|
}
|
|
144
125
|
|
|
145
126
|
function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
@@ -169,20 +150,43 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
169
150
|
channel,
|
|
170
151
|
getStatus: async ({ cfg }) => {
|
|
171
152
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
153
|
+
|
|
154
|
+
const isAppIdConfigured = (value: unknown): boolean => {
|
|
155
|
+
const asString = normalizeString(value);
|
|
156
|
+
if (asString) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (!value || typeof value !== "object") {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const rec = value as Record<string, unknown>;
|
|
163
|
+
const source = normalizeString(rec.source)?.toLowerCase();
|
|
164
|
+
const id = normalizeString(rec.id);
|
|
165
|
+
if (source === "env" && id) {
|
|
166
|
+
return Boolean(normalizeString(process.env[id]));
|
|
167
|
+
}
|
|
168
|
+
return hasConfiguredSecretInput(value);
|
|
169
|
+
};
|
|
170
|
+
|
|
172
171
|
const topLevelConfigured = Boolean(
|
|
173
|
-
feishuCfg?.appId
|
|
172
|
+
isAppIdConfigured(feishuCfg?.appId) && hasConfiguredSecretInput(feishuCfg?.appSecret),
|
|
174
173
|
);
|
|
174
|
+
|
|
175
175
|
const accountConfigured = Object.values(feishuCfg?.accounts ?? {}).some((account) => {
|
|
176
176
|
if (!account || typeof account !== "object") {
|
|
177
177
|
return false;
|
|
178
178
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
const hasOwnAppId = Object.prototype.hasOwnProperty.call(account, "appId");
|
|
180
|
+
const hasOwnAppSecret = Object.prototype.hasOwnProperty.call(account, "appSecret");
|
|
181
|
+
const accountAppIdConfigured = hasOwnAppId
|
|
182
|
+
? isAppIdConfigured((account as Record<string, unknown>).appId)
|
|
183
|
+
: isAppIdConfigured(feishuCfg?.appId);
|
|
184
|
+
const accountSecretConfigured = hasOwnAppSecret
|
|
185
|
+
? hasConfiguredSecretInput((account as Record<string, unknown>).appSecret)
|
|
186
|
+
: hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
187
|
+
return Boolean(accountAppIdConfigured && accountSecretConfigured);
|
|
185
188
|
});
|
|
189
|
+
|
|
186
190
|
const configured = topLevelConfigured || accountConfigured;
|
|
187
191
|
const resolvedCredentials = resolveFeishuCredentials(feishuCfg, {
|
|
188
192
|
allowUnresolvedSecretRef: true,
|
|
@@ -224,10 +228,15 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
224
228
|
allowUnresolvedSecretRef: true,
|
|
225
229
|
});
|
|
226
230
|
const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
227
|
-
const hasConfigCreds = Boolean(
|
|
228
|
-
|
|
229
|
-
!hasConfigCreds && process.env.FEISHU_APP_ID?.trim() && process.env.FEISHU_APP_SECRET?.trim(),
|
|
231
|
+
const hasConfigCreds = Boolean(
|
|
232
|
+
typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret,
|
|
230
233
|
);
|
|
234
|
+
const appSecretPromptState = buildSingleChannelSecretPromptState({
|
|
235
|
+
accountConfigured: Boolean(resolved),
|
|
236
|
+
hasConfigToken: hasConfigSecret,
|
|
237
|
+
allowEnv: !hasConfigCreds && Boolean(process.env.FEISHU_APP_ID?.trim()),
|
|
238
|
+
envValue: process.env.FEISHU_APP_SECRET,
|
|
239
|
+
});
|
|
231
240
|
|
|
232
241
|
let next = cfg;
|
|
233
242
|
let appId: string | null = null;
|
|
@@ -243,9 +252,9 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
243
252
|
prompter,
|
|
244
253
|
providerHint: "feishu",
|
|
245
254
|
credentialLabel: "App Secret",
|
|
246
|
-
accountConfigured:
|
|
247
|
-
canUseEnv,
|
|
248
|
-
hasConfigToken:
|
|
255
|
+
accountConfigured: appSecretPromptState.accountConfigured,
|
|
256
|
+
canUseEnv: appSecretPromptState.canUseEnv,
|
|
257
|
+
hasConfigToken: appSecretPromptState.hasConfigToken,
|
|
249
258
|
envPrompt: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
250
259
|
keepPrompt: "Feishu App Secret already configured. Keep it?",
|
|
251
260
|
inputPrompt: "Enter Feishu App Secret",
|
|
@@ -265,7 +274,8 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
265
274
|
appSecretProbeValue = appSecretResult.resolvedValue;
|
|
266
275
|
appId = await promptFeishuAppId({
|
|
267
276
|
prompter,
|
|
268
|
-
initialValue:
|
|
277
|
+
initialValue:
|
|
278
|
+
normalizeString(feishuCfg?.appId) ?? normalizeString(process.env.FEISHU_APP_ID),
|
|
269
279
|
});
|
|
270
280
|
}
|
|
271
281
|
|
|
@@ -330,14 +340,19 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
330
340
|
if (connectionMode === "webhook") {
|
|
331
341
|
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
|
|
332
342
|
?.verificationToken;
|
|
343
|
+
const verificationTokenPromptState = buildSingleChannelSecretPromptState({
|
|
344
|
+
accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
|
|
345
|
+
hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
|
|
346
|
+
allowEnv: false,
|
|
347
|
+
});
|
|
333
348
|
const verificationTokenResult = await promptSingleChannelSecretInput({
|
|
334
349
|
cfg: next,
|
|
335
350
|
prompter,
|
|
336
351
|
providerHint: "feishu-webhook",
|
|
337
352
|
credentialLabel: "verification token",
|
|
338
|
-
accountConfigured:
|
|
339
|
-
canUseEnv:
|
|
340
|
-
hasConfigToken:
|
|
353
|
+
accountConfigured: verificationTokenPromptState.accountConfigured,
|
|
354
|
+
canUseEnv: verificationTokenPromptState.canUseEnv,
|
|
355
|
+
hasConfigToken: verificationTokenPromptState.hasConfigToken,
|
|
341
356
|
envPrompt: "",
|
|
342
357
|
keepPrompt: "Feishu verification token already configured. Keep it?",
|
|
343
358
|
inputPrompt: "Enter Feishu verification token",
|
|
@@ -421,7 +436,7 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
421
436
|
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
422
437
|
});
|
|
423
438
|
if (entry) {
|
|
424
|
-
const parts =
|
|
439
|
+
const parts = splitOnboardingEntries(String(entry));
|
|
425
440
|
if (parts.length > 0) {
|
|
426
441
|
next = setFeishuGroupAllowFrom(next, parts);
|
|
427
442
|
}
|