@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. 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 {};