@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.2
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/dist/index.d.ts +23 -0
- package/dist/index.js +45 -0
- package/dist/src/accounts.js +141 -0
- package/dist/src/app-scope-checker.js +36 -0
- package/dist/src/async.js +34 -0
- package/dist/src/auth-errors.js +72 -0
- package/dist/src/bitable.js +495 -0
- package/dist/src/bot.d.ts +35 -0
- package/dist/src/bot.js +941 -0
- package/dist/src/calendar-calendar.js +54 -0
- package/dist/src/calendar-event-attendee.js +98 -0
- package/dist/src/calendar-event.js +193 -0
- package/dist/src/calendar-freebusy.js +40 -0
- package/dist/src/calendar-shared.js +23 -0
- package/dist/src/calendar.js +16 -0
- package/dist/src/card-action.js +49 -0
- package/dist/src/channel.d.ts +7 -0
- package/dist/src/channel.js +413 -0
- package/dist/src/chat-schema.js +25 -0
- package/dist/src/chat.js +87 -0
- package/dist/src/client.d.ts +16 -0
- package/dist/src/client.js +112 -0
- package/dist/src/config-schema.d.ts +357 -0
- package/dist/src/dedup.js +126 -0
- package/dist/src/device-flow.js +109 -0
- package/dist/src/directory.js +101 -0
- package/dist/src/doc-schema.js +148 -0
- package/dist/src/docx-batch-insert.js +104 -0
- package/dist/src/docx-color-text.js +80 -0
- package/dist/src/docx-table-ops.js +197 -0
- package/dist/src/docx.js +858 -0
- package/dist/src/domains.js +14 -0
- package/dist/src/drive-schema.js +41 -0
- package/dist/src/drive.js +126 -0
- package/dist/src/dynamic-agent.js +93 -0
- package/dist/src/external-keys.js +13 -0
- package/dist/src/feishu-fetch.js +12 -0
- package/dist/src/identity.js +92 -0
- package/dist/src/lark-ticket.js +11 -0
- package/dist/src/media.d.ts +75 -0
- package/dist/src/media.js +304 -0
- package/dist/src/mention.d.ts +52 -0
- package/dist/src/mention.js +82 -0
- package/dist/src/monitor.account.d.ts +1 -0
- package/dist/src/monitor.account.js +393 -0
- package/dist/src/monitor.d.ts +11 -0
- package/dist/src/monitor.js +58 -0
- package/dist/src/monitor.startup.js +24 -0
- package/dist/src/monitor.state.d.ts +1 -0
- package/dist/src/monitor.state.js +80 -0
- package/dist/src/monitor.transport.js +167 -0
- package/dist/src/nextclaw-sdk/account-id.js +15 -0
- package/dist/src/nextclaw-sdk/core-channel.js +150 -0
- package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
- package/dist/src/nextclaw-sdk/dedupe.js +164 -0
- package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
- package/dist/src/nextclaw-sdk/feishu.js +14 -0
- package/dist/src/nextclaw-sdk/history.js +69 -0
- package/dist/src/nextclaw-sdk/network-body.js +180 -0
- package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
- package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
- package/dist/src/nextclaw-sdk/network.js +4 -0
- package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
- package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
- package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
- package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
- package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets.js +4 -0
- package/dist/src/nextclaw-sdk/types.d.ts +242 -0
- package/dist/src/oauth.js +171 -0
- package/dist/src/onboarding.js +381 -0
- package/dist/src/outbound.js +150 -0
- package/dist/src/perm-schema.js +49 -0
- package/dist/src/perm.js +90 -0
- package/dist/src/policy.js +61 -0
- package/dist/src/post.js +160 -0
- package/dist/src/probe.d.ts +11 -0
- package/dist/src/probe.js +85 -0
- package/dist/src/raw-request.js +24 -0
- package/dist/src/reactions.d.ts +67 -0
- package/dist/src/reactions.js +91 -0
- package/dist/src/reply-dispatcher.js +250 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/secret-input.js +3 -0
- package/dist/src/send-result.js +12 -0
- package/dist/src/send-target.js +22 -0
- package/dist/src/send.d.ts +51 -0
- package/dist/src/send.js +265 -0
- package/dist/src/sheets-shared.js +193 -0
- package/dist/src/sheets.js +95 -0
- package/dist/src/streaming-card.js +263 -0
- package/dist/src/targets.js +39 -0
- package/dist/src/task-comment.js +76 -0
- package/dist/src/task-shared.js +13 -0
- package/dist/src/task-subtask.js +79 -0
- package/dist/src/task-task.js +144 -0
- package/dist/src/task-tasklist.js +136 -0
- package/dist/src/task.js +16 -0
- package/dist/src/token-store.js +154 -0
- package/dist/src/tool-account.js +65 -0
- package/dist/src/tool-result.js +18 -0
- package/dist/src/tool-scopes.js +62 -0
- package/dist/src/tools-config.js +30 -0
- package/dist/src/types.d.ts +43 -0
- package/dist/src/typing.js +145 -0
- package/dist/src/uat-client.js +102 -0
- package/dist/src/user-tool-client.js +132 -0
- package/dist/src/user-tool-helpers.js +110 -0
- package/dist/src/user-tool-result.js +10 -0
- package/dist/src/wiki-schema.js +45 -0
- package/dist/src/wiki.js +144 -0
- package/package.json +8 -4
- package/index.ts +0 -75
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { applyBasicWebhookRequestGuards } from "./nextclaw-sdk/network-webhook.js";
|
|
2
|
+
import { installRequestBodyLimitGuard, readJsonBodyWithLimit } from "./nextclaw-sdk/network-body.js";
|
|
3
|
+
import "./nextclaw-sdk/feishu.js";
|
|
4
|
+
import { createFeishuWSClient } from "./client.js";
|
|
5
|
+
import { FEISHU_WEBHOOK_BODY_TIMEOUT_MS, FEISHU_WEBHOOK_MAX_BODY_BYTES, botNames, botOpenIds, feishuWebhookRateLimiter, httpServers, recordWebhookStatus, wsClients } from "./monitor.state.js";
|
|
6
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import * as http from "http";
|
|
9
|
+
//#region src/monitor.transport.ts
|
|
10
|
+
function isFeishuWebhookPayload(value) {
|
|
11
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function buildFeishuWebhookEnvelope(req, payload) {
|
|
14
|
+
return Object.assign(Object.create({ headers: req.headers }), payload);
|
|
15
|
+
}
|
|
16
|
+
function isFeishuWebhookSignatureValid(params) {
|
|
17
|
+
const encryptKey = params.encryptKey?.trim();
|
|
18
|
+
if (!encryptKey) return true;
|
|
19
|
+
const timestampHeader = params.headers["x-lark-request-timestamp"];
|
|
20
|
+
const nonceHeader = params.headers["x-lark-request-nonce"];
|
|
21
|
+
const signatureHeader = params.headers["x-lark-signature"];
|
|
22
|
+
const timestamp = Array.isArray(timestampHeader) ? timestampHeader[0] : timestampHeader;
|
|
23
|
+
const nonce = Array.isArray(nonceHeader) ? nonceHeader[0] : nonceHeader;
|
|
24
|
+
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
25
|
+
if (!timestamp || !nonce || !signature) return false;
|
|
26
|
+
return crypto.createHash("sha256").update(timestamp + nonce + encryptKey + JSON.stringify(params.payload)).digest("hex") === signature;
|
|
27
|
+
}
|
|
28
|
+
function respondText(res, statusCode, body) {
|
|
29
|
+
res.statusCode = statusCode;
|
|
30
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
31
|
+
res.end(body);
|
|
32
|
+
}
|
|
33
|
+
async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher }) {
|
|
34
|
+
const log = runtime?.log ?? console.log;
|
|
35
|
+
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
|
36
|
+
const wsClient = createFeishuWSClient(account);
|
|
37
|
+
wsClients.set(accountId, wsClient);
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
wsClients.delete(accountId);
|
|
41
|
+
botOpenIds.delete(accountId);
|
|
42
|
+
botNames.delete(accountId);
|
|
43
|
+
};
|
|
44
|
+
const handleAbort = () => {
|
|
45
|
+
log(`feishu[${accountId}]: abort signal received, stopping`);
|
|
46
|
+
cleanup();
|
|
47
|
+
resolve();
|
|
48
|
+
};
|
|
49
|
+
if (abortSignal?.aborted) {
|
|
50
|
+
cleanup();
|
|
51
|
+
resolve();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
55
|
+
try {
|
|
56
|
+
wsClient.start({ eventDispatcher });
|
|
57
|
+
log(`feishu[${accountId}]: WebSocket client started`);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
cleanup();
|
|
60
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
61
|
+
reject(err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher }) {
|
|
66
|
+
const log = runtime?.log ?? console.log;
|
|
67
|
+
const error = runtime?.error ?? console.error;
|
|
68
|
+
const port = account.config.webhookPort ?? 3e3;
|
|
69
|
+
const path = account.config.webhookPath ?? "/feishu/events";
|
|
70
|
+
const host = account.config.webhookHost ?? "127.0.0.1";
|
|
71
|
+
log(`feishu[${accountId}]: starting Webhook server on ${host}:${port}, path ${path}...`);
|
|
72
|
+
const server = http.createServer();
|
|
73
|
+
server.on("request", (req, res) => {
|
|
74
|
+
res.on("finish", () => {
|
|
75
|
+
recordWebhookStatus(runtime, accountId, path, res.statusCode);
|
|
76
|
+
});
|
|
77
|
+
if (!applyBasicWebhookRequestGuards({
|
|
78
|
+
req,
|
|
79
|
+
res,
|
|
80
|
+
rateLimiter: feishuWebhookRateLimiter,
|
|
81
|
+
rateLimitKey: `${accountId}:${path}:${req.socket.remoteAddress ?? "unknown"}`,
|
|
82
|
+
nowMs: Date.now(),
|
|
83
|
+
requireJsonContentType: true
|
|
84
|
+
})) return;
|
|
85
|
+
const guard = installRequestBodyLimitGuard(req, res, {
|
|
86
|
+
maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
87
|
+
timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
|
|
88
|
+
responseFormat: "text"
|
|
89
|
+
});
|
|
90
|
+
if (guard.isTripped()) return;
|
|
91
|
+
(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const bodyResult = await readJsonBodyWithLimit(req, {
|
|
94
|
+
maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
95
|
+
timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS
|
|
96
|
+
});
|
|
97
|
+
if (guard.isTripped() || res.writableEnded) return;
|
|
98
|
+
if (!bodyResult.ok) {
|
|
99
|
+
if (bodyResult.code === "INVALID_JSON") respondText(res, 400, "Invalid JSON");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!isFeishuWebhookPayload(bodyResult.value)) {
|
|
103
|
+
respondText(res, 400, "Invalid JSON");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!isFeishuWebhookSignatureValid({
|
|
107
|
+
headers: req.headers,
|
|
108
|
+
payload: bodyResult.value,
|
|
109
|
+
encryptKey: account.encryptKey
|
|
110
|
+
})) {
|
|
111
|
+
respondText(res, 401, "Invalid signature");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const { isChallenge, challenge } = Lark.generateChallenge(bodyResult.value, { encryptKey: account.encryptKey ?? "" });
|
|
115
|
+
if (isChallenge) {
|
|
116
|
+
res.statusCode = 200;
|
|
117
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
118
|
+
res.end(JSON.stringify(challenge));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const value = await eventDispatcher.invoke(buildFeishuWebhookEnvelope(req, bodyResult.value), { needCheck: false });
|
|
122
|
+
if (!res.headersSent) {
|
|
123
|
+
res.statusCode = 200;
|
|
124
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
125
|
+
res.end(JSON.stringify(value));
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (!guard.isTripped()) {
|
|
129
|
+
error(`feishu[${accountId}]: webhook handler error: ${String(err)}`);
|
|
130
|
+
if (!res.headersSent) respondText(res, 500, "Internal Server Error");
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
guard.dispose();
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
});
|
|
137
|
+
httpServers.set(accountId, server);
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
server.close();
|
|
141
|
+
httpServers.delete(accountId);
|
|
142
|
+
botOpenIds.delete(accountId);
|
|
143
|
+
botNames.delete(accountId);
|
|
144
|
+
};
|
|
145
|
+
const handleAbort = () => {
|
|
146
|
+
log(`feishu[${accountId}]: abort signal received, stopping Webhook server`);
|
|
147
|
+
cleanup();
|
|
148
|
+
resolve();
|
|
149
|
+
};
|
|
150
|
+
if (abortSignal?.aborted) {
|
|
151
|
+
cleanup();
|
|
152
|
+
resolve();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
156
|
+
server.listen(port, host, () => {
|
|
157
|
+
log(`feishu[${accountId}]: Webhook server listening on ${host}:${port}`);
|
|
158
|
+
});
|
|
159
|
+
server.on("error", (err) => {
|
|
160
|
+
error(`feishu[${accountId}]: Webhook server error: ${err}`);
|
|
161
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { monitorWebSocket, monitorWebhook };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/account-id.ts
|
|
2
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
3
|
+
function normalizeAccountId(accountId) {
|
|
4
|
+
return accountId?.trim() || "default";
|
|
5
|
+
}
|
|
6
|
+
const VALID_AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
7
|
+
const INVALID_AGENT_CHARS_RE = /[^a-z0-9_-]+/g;
|
|
8
|
+
function normalizeAgentId(value) {
|
|
9
|
+
const trimmed = value?.trim();
|
|
10
|
+
if (!trimmed) return "main";
|
|
11
|
+
if (VALID_AGENT_ID_RE.test(trimmed)) return trimmed.toLowerCase();
|
|
12
|
+
return trimmed.toLowerCase().replace(INVALID_AGENT_CHARS_RE, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 64) || "main";
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeAgentId };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/core-channel.ts
|
|
2
|
+
function emptyPluginConfigSchema() {
|
|
3
|
+
return {
|
|
4
|
+
type: "object",
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
properties: {}
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
const warnedMissingProviderGroupPolicy = /* @__PURE__ */ new Set();
|
|
10
|
+
function resolveDefaultGroupPolicy(cfg) {
|
|
11
|
+
return cfg.channels?.defaults?.groupPolicy;
|
|
12
|
+
}
|
|
13
|
+
function resolveOpenProviderRuntimeGroupPolicy(params) {
|
|
14
|
+
return {
|
|
15
|
+
groupPolicy: params.providerConfigPresent ? params.groupPolicy ?? params.defaultGroupPolicy ?? "open" : params.groupPolicy ?? "allowlist",
|
|
16
|
+
providerMissingFallbackApplied: !params.providerConfigPresent && params.groupPolicy === void 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function warnMissingProviderGroupPolicyFallbackOnce(params) {
|
|
20
|
+
if (!params.providerMissingFallbackApplied) return false;
|
|
21
|
+
const key = `${params.providerKey}:${params.accountId ?? "*"}`;
|
|
22
|
+
if (warnedMissingProviderGroupPolicy.has(key)) return false;
|
|
23
|
+
warnedMissingProviderGroupPolicy.add(key);
|
|
24
|
+
const blockedLabel = params.blockedLabel?.trim() || "group messages";
|
|
25
|
+
params.log(`${params.providerKey}: channels.${params.providerKey} is missing; defaulting groupPolicy to "allowlist" (${blockedLabel} blocked until explicitly configured).`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function evaluateSenderGroupAccessForPolicy(params) {
|
|
29
|
+
if (params.groupPolicy === "disabled") return {
|
|
30
|
+
allowed: false,
|
|
31
|
+
groupPolicy: params.groupPolicy,
|
|
32
|
+
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
|
33
|
+
reason: "disabled"
|
|
34
|
+
};
|
|
35
|
+
if (params.groupPolicy === "allowlist") {
|
|
36
|
+
if (params.groupAllowFrom.length === 0) return {
|
|
37
|
+
allowed: false,
|
|
38
|
+
groupPolicy: params.groupPolicy,
|
|
39
|
+
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
|
40
|
+
reason: "empty_allowlist"
|
|
41
|
+
};
|
|
42
|
+
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) return {
|
|
43
|
+
allowed: false,
|
|
44
|
+
groupPolicy: params.groupPolicy,
|
|
45
|
+
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
|
46
|
+
reason: "sender_not_allowlisted"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
allowed: true,
|
|
51
|
+
groupPolicy: params.groupPolicy,
|
|
52
|
+
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
|
53
|
+
reason: "allowed"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createDefaultChannelRuntimeState(accountId, extra) {
|
|
57
|
+
return {
|
|
58
|
+
accountId,
|
|
59
|
+
running: false,
|
|
60
|
+
lastStartAt: null,
|
|
61
|
+
lastStopAt: null,
|
|
62
|
+
lastError: null,
|
|
63
|
+
...extra ?? {}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function buildProbeChannelStatusSummary(snapshot, extra) {
|
|
67
|
+
return {
|
|
68
|
+
configured: snapshot.configured ?? false,
|
|
69
|
+
running: snapshot.running ?? false,
|
|
70
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
71
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
72
|
+
lastError: snapshot.lastError ?? null,
|
|
73
|
+
...extra ?? {},
|
|
74
|
+
probe: snapshot.probe,
|
|
75
|
+
lastProbeAt: snapshot.lastProbeAt ?? null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildRuntimeAccountStatusSnapshot(params) {
|
|
79
|
+
return {
|
|
80
|
+
running: params.runtime?.running ?? false,
|
|
81
|
+
lastStartAt: params.runtime?.lastStartAt ?? null,
|
|
82
|
+
lastStopAt: params.runtime?.lastStopAt ?? null,
|
|
83
|
+
lastError: params.runtime?.lastError ?? null,
|
|
84
|
+
probe: params.probe
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function mapAllowFromEntries(allowFrom) {
|
|
88
|
+
return (allowFrom ?? []).map((entry) => String(entry));
|
|
89
|
+
}
|
|
90
|
+
function formatAllowFromLowercase(params) {
|
|
91
|
+
return params.allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => params.stripPrefixRe ? entry.replace(params.stripPrefixRe, "") : entry).map((entry) => entry.toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
function applyDirectoryQueryAndLimit(ids, params) {
|
|
94
|
+
const query = params.query?.trim().toLowerCase() || "";
|
|
95
|
+
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : void 0;
|
|
96
|
+
const filtered = ids.filter((id) => query ? id.toLowerCase().includes(query) : true);
|
|
97
|
+
return typeof limit === "number" ? filtered.slice(0, limit) : filtered;
|
|
98
|
+
}
|
|
99
|
+
function dedupeIds(ids) {
|
|
100
|
+
return Array.from(new Set(ids));
|
|
101
|
+
}
|
|
102
|
+
function collectEntryIds(params) {
|
|
103
|
+
return (params.entries ?? []).map((entry) => String(entry).trim()).filter((entry) => Boolean(entry) && entry !== "*").map((entry) => {
|
|
104
|
+
const normalized = params.normalizeId ? params.normalizeId(entry) : entry;
|
|
105
|
+
return typeof normalized === "string" ? normalized.trim() : "";
|
|
106
|
+
}).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
function collectMapIds(params) {
|
|
109
|
+
return collectEntryIds({
|
|
110
|
+
entries: Object.keys(params.map ?? {}),
|
|
111
|
+
normalizeId: params.normalizeId
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function listDirectoryUserEntriesFromAllowFromAndMapKeys(params) {
|
|
115
|
+
return applyDirectoryQueryAndLimit(dedupeIds([...collectEntryIds({
|
|
116
|
+
entries: params.allowFrom,
|
|
117
|
+
normalizeId: params.normalizeAllowFromId
|
|
118
|
+
}), ...collectMapIds({
|
|
119
|
+
map: params.map,
|
|
120
|
+
normalizeId: params.normalizeMapKeyId
|
|
121
|
+
})]), params).map((id) => ({
|
|
122
|
+
kind: "user",
|
|
123
|
+
id
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
function listDirectoryGroupEntriesFromMapKeysAndAllowFrom(params) {
|
|
127
|
+
return applyDirectoryQueryAndLimit(dedupeIds([...collectMapIds({
|
|
128
|
+
map: params.groups,
|
|
129
|
+
normalizeId: params.normalizeMapKeyId
|
|
130
|
+
}), ...collectEntryIds({
|
|
131
|
+
entries: params.allowFrom,
|
|
132
|
+
normalizeId: params.normalizeAllowFromId
|
|
133
|
+
})]), params).map((id) => ({
|
|
134
|
+
kind: "group",
|
|
135
|
+
id
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function collectAllowlistProviderRestrictSendersWarnings(params) {
|
|
139
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
|
|
140
|
+
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
|
|
141
|
+
providerConfigPresent: params.providerConfigPresent,
|
|
142
|
+
groupPolicy: params.configuredGroupPolicy ?? void 0,
|
|
143
|
+
defaultGroupPolicy
|
|
144
|
+
});
|
|
145
|
+
if (groupPolicy !== "open") return [];
|
|
146
|
+
const mentionSuffix = params.mentionGated === false ? "" : " (mention-gated)";
|
|
147
|
+
return [`- ${params.surface}: groupPolicy="open" allows ${params.openScope} to trigger${mentionSuffix}. Set ${params.groupPolicyPath}="allowlist" + ${params.groupAllowFromPath} to restrict senders.`];
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { buildProbeChannelStatusSummary, buildRuntimeAccountStatusSnapshot, collectAllowlistProviderRestrictSendersWarnings, createDefaultChannelRuntimeState, emptyPluginConfigSchema, evaluateSenderGroupAccessForPolicy, formatAllowFromLowercase, listDirectoryGroupEntriesFromMapKeysAndAllowFrom, listDirectoryUserEntriesFromAllowFromAndMapKeys, mapAllowFromEntries, resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { normalizeAccountId } from "./account-id.js";
|
|
2
|
+
//#region src/nextclaw-sdk/core-pairing.ts
|
|
3
|
+
const PAIRING_APPROVED_MESSAGE = "NextClaw access approved. Send a message to start chatting.";
|
|
4
|
+
const NEXTCLAW_DOCS_ROOT = "https://docs.nextclaw.io";
|
|
5
|
+
function buildAgentMediaPayload(mediaList) {
|
|
6
|
+
const first = mediaList[0];
|
|
7
|
+
const mediaPaths = mediaList.map((media) => media.path);
|
|
8
|
+
const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean);
|
|
9
|
+
return {
|
|
10
|
+
MediaPath: first?.path,
|
|
11
|
+
MediaType: first?.contentType ?? void 0,
|
|
12
|
+
MediaUrl: first?.path,
|
|
13
|
+
MediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
14
|
+
MediaUrls: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
15
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function formatDocsLink(path, label) {
|
|
19
|
+
const trimmed = path.trim();
|
|
20
|
+
const url = trimmed.startsWith("http") ? trimmed : `${NEXTCLAW_DOCS_ROOT}${trimmed.startsWith("/") ? trimmed : `/${trimmed}`}`;
|
|
21
|
+
return label ? `${label} (${url})` : url;
|
|
22
|
+
}
|
|
23
|
+
function buildPairingReply(params) {
|
|
24
|
+
return [
|
|
25
|
+
"NextClaw: access not configured.",
|
|
26
|
+
"",
|
|
27
|
+
params.idLine,
|
|
28
|
+
"",
|
|
29
|
+
`Pairing code: ${params.code}`,
|
|
30
|
+
"",
|
|
31
|
+
"Ask the bot owner to approve with:",
|
|
32
|
+
`nextclaw pairing approve ${params.channel} ${params.code}`
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
async function issuePairingChallenge(params) {
|
|
36
|
+
const { code, created } = await params.upsertPairingRequest({
|
|
37
|
+
id: params.senderId,
|
|
38
|
+
meta: params.meta
|
|
39
|
+
});
|
|
40
|
+
if (!created) return { created: false };
|
|
41
|
+
params.onCreated?.({ code });
|
|
42
|
+
const replyText = params.buildReplyText?.({
|
|
43
|
+
code,
|
|
44
|
+
senderIdLine: params.senderIdLine
|
|
45
|
+
}) ?? buildPairingReply({
|
|
46
|
+
channel: params.channel,
|
|
47
|
+
idLine: params.senderIdLine,
|
|
48
|
+
code
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
await params.sendPairingReply(replyText);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
params.onReplyError?.(error);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
created: true,
|
|
57
|
+
code
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createScopedPairingAccess(params) {
|
|
61
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
62
|
+
return {
|
|
63
|
+
accountId,
|
|
64
|
+
readAllowFromStore: () => params.core.channel.pairing.readAllowFromStore({
|
|
65
|
+
channel: params.channel,
|
|
66
|
+
accountId
|
|
67
|
+
}),
|
|
68
|
+
readStoreForDmPolicy: (provider, providerAccountId) => params.core.channel.pairing.readAllowFromStore({
|
|
69
|
+
channel: provider,
|
|
70
|
+
accountId: normalizeAccountId(providerAccountId)
|
|
71
|
+
}),
|
|
72
|
+
upsertPairingRequest: (input) => params.core.channel.pairing.upsertPairingRequest({
|
|
73
|
+
channel: params.channel,
|
|
74
|
+
accountId,
|
|
75
|
+
...input
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createReplyPrefixContext() {
|
|
80
|
+
const prefixContext = {};
|
|
81
|
+
return {
|
|
82
|
+
prefixContext,
|
|
83
|
+
responsePrefix: void 0,
|
|
84
|
+
enableSlackInteractiveReplies: void 0,
|
|
85
|
+
responsePrefixContextProvider: () => prefixContext,
|
|
86
|
+
onModelSelected: (_ctx) => {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function logTypingFailure(params) {
|
|
90
|
+
const target = params.target ? ` target=${params.target}` : "";
|
|
91
|
+
const action = params.action ? ` action=${params.action}` : "";
|
|
92
|
+
params.log(`${params.channel} typing${action} failed${target}: ${String(params.error)}`);
|
|
93
|
+
}
|
|
94
|
+
function createTypingCallbacks(params) {
|
|
95
|
+
const keepaliveIntervalMs = params.keepaliveIntervalMs ?? 3e3;
|
|
96
|
+
const maxConsecutiveFailures = Math.max(1, params.maxConsecutiveFailures ?? 2);
|
|
97
|
+
const maxDurationMs = params.maxDurationMs ?? 6e4;
|
|
98
|
+
let interval;
|
|
99
|
+
let timeout;
|
|
100
|
+
let closed = false;
|
|
101
|
+
let stopSent = false;
|
|
102
|
+
let consecutiveFailures = 0;
|
|
103
|
+
const cleanupTimers = () => {
|
|
104
|
+
if (interval) {
|
|
105
|
+
clearInterval(interval);
|
|
106
|
+
interval = void 0;
|
|
107
|
+
}
|
|
108
|
+
if (timeout) {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
timeout = void 0;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const fireStop = () => {
|
|
114
|
+
cleanupTimers();
|
|
115
|
+
closed = true;
|
|
116
|
+
if (!params.stop || stopSent) return;
|
|
117
|
+
stopSent = true;
|
|
118
|
+
params.stop().catch((error) => (params.onStopError ?? params.onStartError)(error));
|
|
119
|
+
};
|
|
120
|
+
const fireStart = async () => {
|
|
121
|
+
if (closed) return;
|
|
122
|
+
try {
|
|
123
|
+
await params.start();
|
|
124
|
+
consecutiveFailures = 0;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
consecutiveFailures += 1;
|
|
127
|
+
params.onStartError(error);
|
|
128
|
+
if (consecutiveFailures >= maxConsecutiveFailures) fireStop();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
onReplyStart: async () => {
|
|
133
|
+
closed = false;
|
|
134
|
+
stopSent = false;
|
|
135
|
+
consecutiveFailures = 0;
|
|
136
|
+
cleanupTimers();
|
|
137
|
+
await fireStart();
|
|
138
|
+
if (closed) return;
|
|
139
|
+
interval = setInterval(() => {
|
|
140
|
+
fireStart();
|
|
141
|
+
}, keepaliveIntervalMs);
|
|
142
|
+
if (maxDurationMs > 0) timeout = setTimeout(() => {
|
|
143
|
+
fireStop();
|
|
144
|
+
}, maxDurationMs);
|
|
145
|
+
},
|
|
146
|
+
onIdle: fireStop,
|
|
147
|
+
onCleanup: fireStop
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { PAIRING_APPROVED_MESSAGE, buildAgentMediaPayload, createReplyPrefixContext, createScopedPairingAccess, createTypingCallbacks, formatDocsLink, issuePairingChallenge, logTypingFailure };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/nextclaw-sdk/dedupe.ts
|
|
4
|
+
function pruneMapToMaxSize(map, maxSize) {
|
|
5
|
+
while (map.size > maxSize) {
|
|
6
|
+
const firstKey = map.keys().next().value;
|
|
7
|
+
if (firstKey === void 0) break;
|
|
8
|
+
map.delete(firstKey);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function createDedupeCache(options) {
|
|
12
|
+
const ttlMs = Math.max(0, options.ttlMs);
|
|
13
|
+
const maxSize = Math.max(0, Math.floor(options.maxSize));
|
|
14
|
+
const cache = /* @__PURE__ */ new Map();
|
|
15
|
+
const touch = (key, now) => {
|
|
16
|
+
cache.delete(key);
|
|
17
|
+
cache.set(key, now);
|
|
18
|
+
};
|
|
19
|
+
const prune = (now) => {
|
|
20
|
+
const cutoff = ttlMs > 0 ? now - ttlMs : void 0;
|
|
21
|
+
if (cutoff !== void 0) {
|
|
22
|
+
for (const [key, seenAt] of cache) if (seenAt < cutoff) cache.delete(key);
|
|
23
|
+
}
|
|
24
|
+
if (maxSize <= 0) {
|
|
25
|
+
cache.clear();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
pruneMapToMaxSize(cache, maxSize);
|
|
29
|
+
};
|
|
30
|
+
const hasUnexpired = (key, now, touchOnRead) => {
|
|
31
|
+
const seenAt = cache.get(key);
|
|
32
|
+
if (seenAt === void 0) return false;
|
|
33
|
+
if (ttlMs > 0 && now - seenAt >= ttlMs) {
|
|
34
|
+
cache.delete(key);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (touchOnRead) touch(key, now);
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
check: (key, now = Date.now()) => {
|
|
42
|
+
if (!key) return false;
|
|
43
|
+
if (hasUnexpired(key, now, true)) return true;
|
|
44
|
+
touch(key, now);
|
|
45
|
+
prune(now);
|
|
46
|
+
return false;
|
|
47
|
+
},
|
|
48
|
+
peek: (key, now = Date.now()) => {
|
|
49
|
+
if (!key) return false;
|
|
50
|
+
return hasUnexpired(key, now, false);
|
|
51
|
+
},
|
|
52
|
+
delete: (key) => {
|
|
53
|
+
if (key) cache.delete(key);
|
|
54
|
+
},
|
|
55
|
+
clear: () => cache.clear(),
|
|
56
|
+
size: () => cache.size
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function readJsonFileWithFallback(filePath, fallback) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await fs.promises.readFile(filePath, "utf-8");
|
|
62
|
+
return {
|
|
63
|
+
value: JSON.parse(raw),
|
|
64
|
+
exists: true
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === "ENOENT") return {
|
|
68
|
+
value: fallback,
|
|
69
|
+
exists: false
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
value: fallback,
|
|
73
|
+
exists: false
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function writeJsonFileAtomically(filePath, value) {
|
|
78
|
+
await fs.promises.mkdir(path.dirname(filePath), {
|
|
79
|
+
recursive: true,
|
|
80
|
+
mode: 448
|
|
81
|
+
});
|
|
82
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
83
|
+
await fs.promises.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
84
|
+
await fs.promises.rename(tempPath, filePath);
|
|
85
|
+
}
|
|
86
|
+
function createPersistentDedupe(options) {
|
|
87
|
+
const ttlMs = Math.max(0, Math.floor(options.ttlMs));
|
|
88
|
+
const fileMaxEntries = Math.max(1, Math.floor(options.fileMaxEntries));
|
|
89
|
+
const memory = createDedupeCache({
|
|
90
|
+
ttlMs,
|
|
91
|
+
maxSize: Math.max(0, Math.floor(options.memoryMaxSize))
|
|
92
|
+
});
|
|
93
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
94
|
+
const sanitize = (value) => {
|
|
95
|
+
if (!value || typeof value !== "object") return {};
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [key, timestamp] of Object.entries(value)) if (typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0) out[key] = timestamp;
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
const pruneData = (data, now) => {
|
|
101
|
+
if (ttlMs > 0) {
|
|
102
|
+
for (const [key, timestamp] of Object.entries(data)) if (now - timestamp >= ttlMs) delete data[key];
|
|
103
|
+
}
|
|
104
|
+
const keys = Object.keys(data);
|
|
105
|
+
if (keys.length > fileMaxEntries) keys.toSorted((left, right) => data[left] - data[right]).slice(0, keys.length - fileMaxEntries).forEach((key) => {
|
|
106
|
+
delete data[key];
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const checkAndRecordInner = async (key, namespace, scopedKey, now, onDiskError) => {
|
|
110
|
+
if (memory.check(scopedKey, now)) return false;
|
|
111
|
+
const filePath = options.resolveFilePath(namespace);
|
|
112
|
+
try {
|
|
113
|
+
const { value } = await readJsonFileWithFallback(filePath, {});
|
|
114
|
+
const data = sanitize(value);
|
|
115
|
+
const seenAt = data[key];
|
|
116
|
+
if (seenAt != null && (ttlMs <= 0 || now - seenAt < ttlMs)) return false;
|
|
117
|
+
data[key] = now;
|
|
118
|
+
pruneData(data, now);
|
|
119
|
+
await writeJsonFileAtomically(filePath, data);
|
|
120
|
+
return true;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
onDiskError?.(error);
|
|
123
|
+
memory.check(scopedKey, now);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
async checkAndRecord(key, dedupeOptions) {
|
|
129
|
+
const trimmed = key.trim();
|
|
130
|
+
if (!trimmed) return true;
|
|
131
|
+
const namespace = dedupeOptions?.namespace?.trim() || "global";
|
|
132
|
+
const scopedKey = `${namespace}:${trimmed}`;
|
|
133
|
+
if (inflight.has(scopedKey)) return false;
|
|
134
|
+
const work = checkAndRecordInner(trimmed, namespace, scopedKey, dedupeOptions?.now ?? Date.now(), dedupeOptions?.onDiskError ?? options.onDiskError);
|
|
135
|
+
inflight.set(scopedKey, work);
|
|
136
|
+
try {
|
|
137
|
+
return await work;
|
|
138
|
+
} finally {
|
|
139
|
+
inflight.delete(scopedKey);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
async warmup(namespace = "global", onError) {
|
|
143
|
+
const filePath = options.resolveFilePath(namespace);
|
|
144
|
+
try {
|
|
145
|
+
const { value } = await readJsonFileWithFallback(filePath, {});
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
let loaded = 0;
|
|
148
|
+
for (const [key, timestamp] of Object.entries(sanitize(value))) {
|
|
149
|
+
if (ttlMs > 0 && now - timestamp >= ttlMs) continue;
|
|
150
|
+
memory.check(`${namespace}:${key}`, timestamp);
|
|
151
|
+
loaded += 1;
|
|
152
|
+
}
|
|
153
|
+
return loaded;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
onError?.(error);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
clearMemory: () => memory.clear(),
|
|
160
|
+
memorySize: () => memory.size()
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { createDedupeCache, createPersistentDedupe, readJsonFileWithFallback };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { AnyAgentTool, BaseProbeResult, ChannelMeta, ChannelOnboardingAdapter, ChannelOnboardingDmPolicy, ChannelPlugin, ClawdbotConfig, DmPolicy, OpenClawConfig, OpenClawPluginApi, PluginRuntime, RuntimeEnv, WizardPrompter } from "./types.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "./account-id.js";
|
|
2
|
+
import "./core-channel.js";
|
|
3
|
+
import "./core-pairing.js";
|
|
4
|
+
import "./dedupe.js";
|
|
5
|
+
import "./history.js";
|
|
6
|
+
import "./network-fetch.js";
|
|
7
|
+
import "./network-webhook.js";
|
|
8
|
+
import "./network-body.js";
|
|
9
|
+
import "./network.js";
|
|
10
|
+
import "./secrets-core.js";
|
|
11
|
+
import "./secrets-config.js";
|
|
12
|
+
import "./secrets-prompt.js";
|
|
13
|
+
import "./secrets.js";
|
|
14
|
+
export {};
|