@openclaw/twitch 2026.5.7 → 2026.5.10-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.
- package/dist/api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{markdown-MRdI1sR7.js → markdown-CLNO0I2v.js} +1 -1
- package/dist/{monitor-DS0YTAPB.js → monitor-DvbgvvL4.js} +136 -132
- package/dist/{plugin-BQX9GiIn.js → plugin-BLMSYnzs.js} +117 -14
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-yArVgckI.js → setup-surface-CDhELE-4.js} +1 -1
- package/dist/{twitch-CklAMZL5.js → twitch-D0A1Bjui.js} +1 -3
- package/package.json +4 -4
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as twitchPlugin } from "./plugin-
|
|
1
|
+
import { t as twitchPlugin } from "./plugin-BLMSYnzs.js";
|
|
2
2
|
export { twitchPlugin };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as normalizeToken, o as resolveTwitchToken } from "./twitch-
|
|
1
|
+
import { i as normalizeToken, o as resolveTwitchToken } from "./twitch-D0A1Bjui.js";
|
|
2
2
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
3
|
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
|
4
4
|
import { ChatClient, LogLevel } from "@twurple/chat";
|
|
@@ -1,128 +1,132 @@
|
|
|
1
|
-
import { i as getOrCreateClientManager, n as stripMarkdownForTwitch } from "./markdown-
|
|
1
|
+
import { i as getOrCreateClientManager, n as stripMarkdownForTwitch } from "./markdown-CLNO0I2v.js";
|
|
2
2
|
import { t as getTwitchRuntime } from "./runtime-QZ5I3GlI.js";
|
|
3
3
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
4
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
5
|
-
import {
|
|
5
|
+
import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
6
6
|
//#region extensions/twitch/src/access-control.ts
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
7
|
+
const twitchUserIdentity = defineStableChannelIngressIdentity({
|
|
8
|
+
key: "sender-id",
|
|
9
|
+
entryIdPrefix: "twitch-user-entry"
|
|
10
|
+
});
|
|
11
|
+
const twitchRoleIdentity = defineStableChannelIngressIdentity({
|
|
12
|
+
key: "role-moderator",
|
|
13
|
+
kind: "role",
|
|
14
|
+
normalizeEntry: normalizeTwitchRole,
|
|
15
|
+
normalizeSubject: normalizeTwitchRole,
|
|
16
|
+
aliases: [
|
|
17
|
+
"owner",
|
|
18
|
+
"vip",
|
|
19
|
+
"subscriber"
|
|
20
|
+
].map((role) => ({
|
|
21
|
+
key: `role-${role}`,
|
|
22
|
+
kind: "role",
|
|
23
|
+
normalizeEntry: () => null,
|
|
24
|
+
normalizeSubject: normalizeTwitchRole
|
|
25
|
+
})),
|
|
26
|
+
isWildcardEntry: (entry) => normalizeTwitchRole(entry) === "all",
|
|
27
|
+
resolveEntryId: ({ entryIndex }) => `twitch-role-entry-${entryIndex + 1}`
|
|
28
|
+
});
|
|
29
|
+
async function checkTwitchAccessControl(params) {
|
|
29
30
|
const { message, account, botUsername } = params;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
const policyKind = resolveTwitchPolicyKind(account);
|
|
32
|
+
const decision = (await createChannelIngressResolver({
|
|
33
|
+
channelId: "twitch",
|
|
34
|
+
accountId: "default",
|
|
35
|
+
identity: policyKind === "role" ? twitchRoleIdentity : twitchUserIdentity
|
|
36
|
+
}).message({
|
|
37
|
+
subject: policyKind === "role" ? twitchRoleSubject(message) : { stableId: message.userId },
|
|
38
|
+
conversation: {
|
|
39
|
+
kind: "group",
|
|
40
|
+
id: message.channel
|
|
41
|
+
},
|
|
42
|
+
event: { mayPair: false },
|
|
43
|
+
mentionFacts: {
|
|
44
|
+
canDetectMention: true,
|
|
45
|
+
wasMentioned: mentionsBot(message.message, botUsername)
|
|
46
|
+
},
|
|
47
|
+
dmPolicy: "open",
|
|
48
|
+
groupPolicy: policyKind === "open" ? "open" : "allowlist",
|
|
49
|
+
policy: { activation: {
|
|
50
|
+
requireMention: account.requireMention ?? true,
|
|
51
|
+
allowTextCommands: false,
|
|
52
|
+
order: "before-sender"
|
|
53
|
+
} },
|
|
54
|
+
groupAllowFrom: policyKind === "allowFrom" ? account.allowFrom : policyKind === "role" ? account.allowedRoles : void 0
|
|
55
|
+
})).ingress;
|
|
56
|
+
if (decision.decisiveGateId === "activation" && decision.admission !== "dispatch") return {
|
|
57
|
+
allowed: false,
|
|
58
|
+
reason: "message does not mention the bot (requireMention is enabled)"
|
|
59
|
+
};
|
|
60
|
+
if (decision.admission === "dispatch") {
|
|
61
|
+
if (policyKind === "allowFrom") return {
|
|
48
62
|
allowed: true,
|
|
49
|
-
matchKey:
|
|
63
|
+
matchKey: params.message.userId,
|
|
50
64
|
matchSource: "allowlist"
|
|
51
65
|
};
|
|
52
|
-
return {
|
|
53
|
-
allowed: false,
|
|
54
|
-
reason: "sender is not in allowFrom allowlist"
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (account.allowedRoles && account.allowedRoles.length > 0) {
|
|
58
|
-
const allowedRoles = account.allowedRoles;
|
|
59
|
-
if (allowedRoles.includes("all")) return {
|
|
66
|
+
if (policyKind === "role") return {
|
|
60
67
|
allowed: true,
|
|
61
|
-
matchKey: "
|
|
68
|
+
matchKey: params.account.allowedRoles?.join(","),
|
|
62
69
|
matchSource: "role"
|
|
63
70
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
return { allowed: true };
|
|
72
|
+
}
|
|
73
|
+
if (policyKind === "allowFrom") {
|
|
74
|
+
if (!params.message.userId) return {
|
|
68
75
|
allowed: false,
|
|
69
|
-
reason:
|
|
76
|
+
reason: "sender user ID not available for allowlist check"
|
|
70
77
|
};
|
|
71
78
|
return {
|
|
72
|
-
allowed:
|
|
73
|
-
|
|
74
|
-
matchSource: "role"
|
|
79
|
+
allowed: false,
|
|
80
|
+
reason: "sender is not in allowFrom allowlist"
|
|
75
81
|
};
|
|
76
82
|
}
|
|
77
|
-
|
|
83
|
+
if (policyKind === "role") return {
|
|
84
|
+
allowed: false,
|
|
85
|
+
reason: `sender does not have any of the required roles: ${params.account.allowedRoles?.join(", ") ?? ""}`
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
allowed: false,
|
|
89
|
+
reason: reasonForTwitchIngressDecision(decision)
|
|
90
|
+
};
|
|
78
91
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
function resolveTwitchPolicyKind(account) {
|
|
93
|
+
if (account.allowFrom !== void 0) return "allowFrom";
|
|
94
|
+
if (account.allowedRoles && account.allowedRoles.length > 0) return "role";
|
|
95
|
+
return "open";
|
|
96
|
+
}
|
|
97
|
+
function twitchRoleSubject(message) {
|
|
98
|
+
return {
|
|
99
|
+
stableId: message.isMod ? "moderator" : void 0,
|
|
100
|
+
aliases: {
|
|
101
|
+
"role-owner": message.isOwner ? "owner" : void 0,
|
|
102
|
+
"role-vip": message.isVip ? "vip" : void 0,
|
|
103
|
+
"role-subscriber": message.isSub ? "subscriber" : void 0
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function normalizeTwitchRole(value) {
|
|
108
|
+
const role = normalizeLowercaseStringOrEmpty(value);
|
|
109
|
+
if (role === "*") return "all";
|
|
110
|
+
return role === "moderator" || role === "owner" || role === "vip" || role === "subscriber" || role === "all" ? role : null;
|
|
111
|
+
}
|
|
112
|
+
function reasonForTwitchIngressDecision(decision) {
|
|
113
|
+
switch (decision.reasonCode) {
|
|
114
|
+
case "activation_skipped": return "message does not mention the bot (requireMention is enabled)";
|
|
115
|
+
case "group_policy_empty_allowlist":
|
|
116
|
+
case "group_policy_not_allowlisted": return "sender is not in allowFrom allowlist";
|
|
117
|
+
default: return decision.reasonCode;
|
|
98
118
|
}
|
|
99
|
-
return false;
|
|
100
119
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
*
|
|
104
|
-
* Returns a list of lowercase usernames that were mentioned in the message.
|
|
105
|
-
* Twitch mentions are in the format @username.
|
|
106
|
-
*/
|
|
107
|
-
function extractMentions(message) {
|
|
120
|
+
function mentionsBot(message, botUsername) {
|
|
121
|
+
const expected = normalizeLowercaseStringOrEmpty(botUsername);
|
|
108
122
|
const mentionRegex = /@(\w+)/g;
|
|
109
|
-
const mentions = [];
|
|
110
123
|
let match;
|
|
111
|
-
while ((match = mentionRegex.exec(message)) !== null)
|
|
112
|
-
|
|
113
|
-
if (username) mentions.push(normalizeLowercaseStringOrEmpty(username));
|
|
114
|
-
}
|
|
115
|
-
return mentions;
|
|
124
|
+
while ((match = mentionRegex.exec(message)) !== null) if ((match[1] ? normalizeLowercaseStringOrEmpty(match[1]) : "") === expected) return true;
|
|
125
|
+
return false;
|
|
116
126
|
}
|
|
117
127
|
//#endregion
|
|
118
128
|
//#region extensions/twitch/src/monitor.ts
|
|
119
129
|
/**
|
|
120
|
-
* Twitch message monitor - processes incoming messages and routes to agents.
|
|
121
|
-
*
|
|
122
|
-
* This monitor connects to the Twitch client manager, processes incoming messages,
|
|
123
|
-
* resolves agent routes, and handles replies.
|
|
124
|
-
*/
|
|
125
|
-
/**
|
|
126
130
|
* Process an incoming Twitch message and dispatch to agent.
|
|
127
131
|
*/
|
|
128
132
|
async function processTwitchMessage(params) {
|
|
@@ -203,12 +207,6 @@ async function processTwitchMessage(params) {
|
|
|
203
207
|
channel: "twitch",
|
|
204
208
|
accountId
|
|
205
209
|
});
|
|
206
|
-
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
|
|
207
|
-
cfg,
|
|
208
|
-
agentId: route.agentId,
|
|
209
|
-
channel: "twitch",
|
|
210
|
-
accountId
|
|
211
|
-
});
|
|
212
210
|
return {
|
|
213
211
|
cfg,
|
|
214
212
|
channel: "twitch",
|
|
@@ -220,24 +218,26 @@ async function processTwitchMessage(params) {
|
|
|
220
218
|
recordInboundSession: core.channel.session.recordInboundSession,
|
|
221
219
|
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
222
220
|
delivery: {
|
|
221
|
+
durable: () => ({ to: `twitch:channel:${message.channel}` }),
|
|
223
222
|
deliver: async (payload) => {
|
|
224
|
-
await deliverTwitchReply({
|
|
223
|
+
return await deliverTwitchReply({
|
|
225
224
|
payload,
|
|
226
225
|
channel: message.channel,
|
|
227
226
|
account,
|
|
228
227
|
accountId,
|
|
229
228
|
config,
|
|
230
229
|
tableMode,
|
|
231
|
-
runtime
|
|
232
|
-
statusSink
|
|
230
|
+
runtime
|
|
233
231
|
});
|
|
234
232
|
},
|
|
233
|
+
onDelivered: (_payload, _info, result) => {
|
|
234
|
+
if (result?.visibleReplySent !== false) statusSink?.({ lastOutboundAt: Date.now() });
|
|
235
|
+
},
|
|
235
236
|
onError: (err, info) => {
|
|
236
237
|
runtime.error?.(`Twitch ${info.kind} reply failed: ${String(err)}`);
|
|
237
238
|
}
|
|
238
239
|
},
|
|
239
|
-
|
|
240
|
-
replyOptions: { onModelSelected },
|
|
240
|
+
replyPipeline: {},
|
|
241
241
|
record: { onRecordError: (err) => {
|
|
242
242
|
runtime.error?.(`Failed updating session meta: ${String(err)}`);
|
|
243
243
|
} }
|
|
@@ -250,7 +250,7 @@ async function processTwitchMessage(params) {
|
|
|
250
250
|
* Deliver a reply to Twitch chat.
|
|
251
251
|
*/
|
|
252
252
|
async function deliverTwitchReply(params) {
|
|
253
|
-
const { payload, channel, account, accountId, config, runtime
|
|
253
|
+
const { payload, channel, account, accountId, config, runtime } = params;
|
|
254
254
|
try {
|
|
255
255
|
const client = await getOrCreateClientManager(accountId, {
|
|
256
256
|
info: (msg) => runtime.log?.(msg),
|
|
@@ -260,17 +260,18 @@ async function deliverTwitchReply(params) {
|
|
|
260
260
|
}).getClient(account, config, accountId);
|
|
261
261
|
if (!client) {
|
|
262
262
|
runtime.error?.(`No client available for sending reply`);
|
|
263
|
-
return;
|
|
263
|
+
return { visibleReplySent: false };
|
|
264
264
|
}
|
|
265
265
|
if (!payload.text) {
|
|
266
266
|
runtime.error?.(`No text to send in reply payload`);
|
|
267
|
-
return;
|
|
267
|
+
return { visibleReplySent: false };
|
|
268
268
|
}
|
|
269
269
|
const textToSend = stripMarkdownForTwitch(payload.text);
|
|
270
270
|
await client.say(channel, textToSend);
|
|
271
|
-
|
|
271
|
+
return { visibleReplySent: true };
|
|
272
272
|
} catch (err) {
|
|
273
273
|
runtime.error?.(`Failed to send reply: ${String(err)}`);
|
|
274
|
+
return { visibleReplySent: false };
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
/**
|
|
@@ -302,23 +303,26 @@ async function monitorTwitchProvider(options) {
|
|
|
302
303
|
}
|
|
303
304
|
const unregisterHandler = clientManager.onMessage(account, (message) => {
|
|
304
305
|
if (stopped) return;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
306
|
+
(async () => {
|
|
307
|
+
const botUsername = normalizeLowercaseStringOrEmpty(account.username);
|
|
308
|
+
if (normalizeLowercaseStringOrEmpty(message.username) === botUsername) return;
|
|
309
|
+
const access = await checkTwitchAccessControl({
|
|
310
|
+
message,
|
|
311
|
+
account,
|
|
312
|
+
botUsername
|
|
313
|
+
});
|
|
314
|
+
if (stopped || !access.allowed) return;
|
|
315
|
+
statusSink?.({ lastInboundAt: Date.now() });
|
|
316
|
+
await processTwitchMessage({
|
|
317
|
+
message,
|
|
318
|
+
account,
|
|
319
|
+
accountId,
|
|
320
|
+
config,
|
|
321
|
+
runtime,
|
|
322
|
+
core,
|
|
323
|
+
statusSink
|
|
324
|
+
});
|
|
325
|
+
})().catch((err) => {
|
|
322
326
|
runtime.error?.(`Message processing failed: ${String(err)}`);
|
|
323
327
|
});
|
|
324
328
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as normalizeTwitchChannel, i as normalizeToken, n as isAccountConfigured, o as resolveTwitchToken, r as missingTargetError, t as generateMessageId } from "./twitch-
|
|
2
|
-
import { a as getAccountConfig, c as resolveTwitchAccountContext, i as DEFAULT_ACCOUNT_ID, l as resolveTwitchSnapshotAccountId, o as listAccountIds, r as twitchSetupWizard, s as resolveDefaultTwitchAccountId, t as twitchSetupAdapter } from "./setup-surface-
|
|
3
|
-
import { a as removeClientManager, n as stripMarkdownForTwitch, r as getClientManager, t as chunkTextForTwitch } from "./markdown-
|
|
1
|
+
import { a as normalizeTwitchChannel, i as normalizeToken, n as isAccountConfigured, o as resolveTwitchToken, r as missingTargetError, t as generateMessageId } from "./twitch-D0A1Bjui.js";
|
|
2
|
+
import { a as getAccountConfig, c as resolveTwitchAccountContext, i as DEFAULT_ACCOUNT_ID, l as resolveTwitchSnapshotAccountId, o as listAccountIds, r as twitchSetupWizard, s as resolveDefaultTwitchAccountId, t as twitchSetupAdapter } from "./setup-surface-CDhELE-4.js";
|
|
3
|
+
import { a as removeClientManager, n as stripMarkdownForTwitch, r as getClientManager, t as chunkTextForTwitch } from "./markdown-CLNO0I2v.js";
|
|
4
4
|
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
5
5
|
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
6
6
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
@@ -8,14 +8,33 @@ import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from
|
|
|
8
8
|
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
|
|
9
9
|
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
10
10
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
11
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
12
|
+
import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
|
|
11
13
|
import { StaticAuthProvider } from "@twurple/auth";
|
|
12
14
|
import { ChatClient } from "@twurple/chat";
|
|
13
15
|
import { MarkdownConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives";
|
|
14
16
|
import { z } from "openclaw/plugin-sdk/zod";
|
|
15
17
|
import { ApiClient } from "@twurple/api";
|
|
16
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
17
18
|
//#region extensions/twitch/src/send.ts
|
|
18
19
|
/**
|
|
20
|
+
* Twitch message sending functions with dependency injection support.
|
|
21
|
+
*
|
|
22
|
+
* These functions are the primary interface for sending messages to Twitch.
|
|
23
|
+
* They support dependency injection via the `deps` parameter for testability.
|
|
24
|
+
*/
|
|
25
|
+
function createTwitchSendReceipt(params) {
|
|
26
|
+
const messageId = params.messageId.trim();
|
|
27
|
+
const conversationId = params.channel?.trim();
|
|
28
|
+
return createMessageReceiptFromOutboundResults({
|
|
29
|
+
results: params.visible === true && messageId && messageId !== "skipped" ? [{
|
|
30
|
+
channel: "twitch",
|
|
31
|
+
messageId,
|
|
32
|
+
...conversationId ? { conversationId } : {}
|
|
33
|
+
}] : [],
|
|
34
|
+
kind: "text"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
19
38
|
* Internal send function used by the outbound adapter.
|
|
20
39
|
*
|
|
21
40
|
* This function has access to the full OpenClaw config and handles
|
|
@@ -44,47 +63,93 @@ async function sendMessageTwitchInternal(channel, text, cfg, accountId, stripMar
|
|
|
44
63
|
if (!account) return {
|
|
45
64
|
ok: false,
|
|
46
65
|
messageId: generateMessageId(),
|
|
66
|
+
receipt: createTwitchSendReceipt({
|
|
67
|
+
messageId: "",
|
|
68
|
+
channel,
|
|
69
|
+
visible: false
|
|
70
|
+
}),
|
|
47
71
|
error: `Account not found: ${accountId ?? "(default)"}. Available accounts: ${availableAccountIds.join(", ") || "none"}`
|
|
48
72
|
};
|
|
49
73
|
if (!configured) return {
|
|
50
74
|
ok: false,
|
|
51
75
|
messageId: generateMessageId(),
|
|
76
|
+
receipt: createTwitchSendReceipt({
|
|
77
|
+
messageId: "",
|
|
78
|
+
channel,
|
|
79
|
+
visible: false
|
|
80
|
+
}),
|
|
52
81
|
error: `Account ${resolvedAccountId} is not properly configured. Required: username, clientId, and token (config or env for default account).`
|
|
53
82
|
};
|
|
54
83
|
const normalizedChannel = channel || account.channel;
|
|
55
84
|
if (!normalizedChannel) return {
|
|
56
85
|
ok: false,
|
|
57
86
|
messageId: generateMessageId(),
|
|
87
|
+
receipt: createTwitchSendReceipt({
|
|
88
|
+
messageId: "",
|
|
89
|
+
channel: normalizedChannel,
|
|
90
|
+
visible: false
|
|
91
|
+
}),
|
|
58
92
|
error: "No channel specified and no default channel in account config"
|
|
59
93
|
};
|
|
94
|
+
const deliveryChannel = normalizeTwitchChannel(normalizedChannel);
|
|
60
95
|
const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text;
|
|
61
96
|
if (!cleanedText) return {
|
|
62
97
|
ok: true,
|
|
63
|
-
messageId: "skipped"
|
|
98
|
+
messageId: "skipped",
|
|
99
|
+
receipt: createTwitchSendReceipt({
|
|
100
|
+
messageId: "skipped",
|
|
101
|
+
channel: deliveryChannel,
|
|
102
|
+
visible: false
|
|
103
|
+
})
|
|
64
104
|
};
|
|
65
105
|
const clientManager = getClientManager(resolvedAccountId);
|
|
66
106
|
if (!clientManager) return {
|
|
67
107
|
ok: false,
|
|
68
108
|
messageId: generateMessageId(),
|
|
109
|
+
receipt: createTwitchSendReceipt({
|
|
110
|
+
messageId: "",
|
|
111
|
+
channel: deliveryChannel,
|
|
112
|
+
visible: false
|
|
113
|
+
}),
|
|
69
114
|
error: `Client manager not found for account: ${resolvedAccountId}. Please start the Twitch gateway first.`
|
|
70
115
|
};
|
|
71
116
|
try {
|
|
72
|
-
const result = await clientManager.sendMessage(account,
|
|
73
|
-
if (!result.ok)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
const result = await clientManager.sendMessage(account, deliveryChannel, cleanedText, cfg, resolvedAccountId);
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
const messageId = result.messageId ?? generateMessageId();
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
messageId,
|
|
123
|
+
receipt: createTwitchSendReceipt({
|
|
124
|
+
messageId,
|
|
125
|
+
channel: deliveryChannel,
|
|
126
|
+
visible: false
|
|
127
|
+
}),
|
|
128
|
+
error: result.error ?? "Send failed"
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const messageId = result.messageId ?? generateMessageId();
|
|
78
132
|
return {
|
|
79
133
|
ok: true,
|
|
80
|
-
messageId
|
|
134
|
+
messageId,
|
|
135
|
+
receipt: createTwitchSendReceipt({
|
|
136
|
+
messageId,
|
|
137
|
+
channel: deliveryChannel,
|
|
138
|
+
visible: true
|
|
139
|
+
})
|
|
81
140
|
};
|
|
82
141
|
} catch (error) {
|
|
83
142
|
const errorMsg = formatErrorMessage(error);
|
|
143
|
+
const messageId = generateMessageId();
|
|
84
144
|
logger.error(`Failed to send message: ${errorMsg}`);
|
|
85
145
|
return {
|
|
86
146
|
ok: false,
|
|
87
|
-
messageId
|
|
147
|
+
messageId,
|
|
148
|
+
receipt: createTwitchSendReceipt({
|
|
149
|
+
messageId,
|
|
150
|
+
channel: deliveryChannel,
|
|
151
|
+
visible: false
|
|
152
|
+
}),
|
|
88
153
|
error: errorMsg
|
|
89
154
|
};
|
|
90
155
|
}
|
|
@@ -106,6 +171,11 @@ async function sendMessageTwitchInternal(channel, text, cfg, accountId, stripMar
|
|
|
106
171
|
const twitchOutbound = {
|
|
107
172
|
/** Direct delivery mode - messages are sent immediately */
|
|
108
173
|
deliveryMode: "direct",
|
|
174
|
+
deliveryCapabilities: { durableFinal: {
|
|
175
|
+
text: true,
|
|
176
|
+
media: true,
|
|
177
|
+
messageSendingHooks: true
|
|
178
|
+
} },
|
|
109
179
|
/** Twitch chat message limit is 500 characters */
|
|
110
180
|
textChunkLimit: 500,
|
|
111
181
|
/** Word-boundary chunker with markdown stripping */
|
|
@@ -184,6 +254,7 @@ const twitchOutbound = {
|
|
|
184
254
|
return {
|
|
185
255
|
channel: "twitch",
|
|
186
256
|
messageId: result.messageId,
|
|
257
|
+
receipt: result.receipt,
|
|
187
258
|
timestamp: Date.now()
|
|
188
259
|
};
|
|
189
260
|
},
|
|
@@ -216,6 +287,37 @@ const twitchOutbound = {
|
|
|
216
287
|
});
|
|
217
288
|
}
|
|
218
289
|
};
|
|
290
|
+
function toTwitchMessageSendResult(result, kind) {
|
|
291
|
+
const receipt = result.receipt ?? createMessageReceiptFromOutboundResults({
|
|
292
|
+
results: result.messageId ? [{
|
|
293
|
+
channel: "twitch",
|
|
294
|
+
messageId: result.messageId
|
|
295
|
+
}] : [],
|
|
296
|
+
kind
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
messageId: result.messageId || receipt.primaryPlatformMessageId,
|
|
300
|
+
receipt
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const twitchMessageAdapter = defineChannelMessageAdapter({
|
|
304
|
+
id: "twitch",
|
|
305
|
+
durableFinal: { capabilities: {
|
|
306
|
+
text: true,
|
|
307
|
+
media: true,
|
|
308
|
+
messageSendingHooks: true
|
|
309
|
+
} },
|
|
310
|
+
send: {
|
|
311
|
+
text: async (ctx) => {
|
|
312
|
+
if (!twitchOutbound.sendText) throw new Error("Twitch text sending is not available.");
|
|
313
|
+
return toTwitchMessageSendResult(await twitchOutbound.sendText(ctx), "text");
|
|
314
|
+
},
|
|
315
|
+
media: async (ctx) => {
|
|
316
|
+
if (!twitchOutbound.sendMedia) throw new Error("Twitch media sending is not available.");
|
|
317
|
+
return toTwitchMessageSendResult(await twitchOutbound.sendMedia(ctx), "media");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
219
321
|
//#endregion
|
|
220
322
|
//#region extensions/twitch/src/actions.ts
|
|
221
323
|
/**
|
|
@@ -779,6 +881,7 @@ const twitchPlugin = createChatChannelPlugin({
|
|
|
779
881
|
setup: twitchSetupAdapter,
|
|
780
882
|
setupWizard: twitchSetupWizard,
|
|
781
883
|
capabilities: { chatTypes: ["group"] },
|
|
884
|
+
message: twitchMessageAdapter,
|
|
782
885
|
configSchema: buildChannelConfigSchema(TwitchConfigSchema),
|
|
783
886
|
config: {
|
|
784
887
|
listAccountIds: (cfg) => listAccountIds(cfg),
|
|
@@ -851,7 +954,7 @@ const twitchPlugin = createChatChannelPlugin({
|
|
|
851
954
|
lastError: null
|
|
852
955
|
});
|
|
853
956
|
ctx.log?.info(`Starting Twitch connection for ${account.username}`);
|
|
854
|
-
const { monitorTwitchProvider } = await import("./monitor-
|
|
957
|
+
const { monitorTwitchProvider } = await import("./monitor-DvbgvvL4.js");
|
|
855
958
|
await monitorTwitchProvider({
|
|
856
959
|
account,
|
|
857
960
|
accountId,
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as twitchSetupPlugin } from "./setup-surface-
|
|
1
|
+
import { n as twitchSetupPlugin } from "./setup-surface-CDhELE-4.js";
|
|
2
2
|
export { twitchSetupPlugin };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as isAccountConfigured, o as resolveTwitchToken } from "./twitch-
|
|
1
|
+
import { n as isAccountConfigured, o as resolveTwitchToken } from "./twitch-D0A1Bjui.js";
|
|
2
2
|
import { listCombinedAccountIds, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
|
|
3
3
|
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
|
4
4
|
import { getChatChannelMeta } from "openclaw/plugin-sdk/core";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
3
4
|
//#region extensions/twitch/src/token.ts
|
|
4
5
|
/**
|
|
5
6
|
* Twitch access token resolution with environment variable support.
|
|
@@ -61,9 +62,6 @@ function resolveTwitchToken(cfg, opts = {}) {
|
|
|
61
62
|
/**
|
|
62
63
|
* Twitch-specific utility functions
|
|
63
64
|
*/
|
|
64
|
-
function normalizeLowercaseStringOrEmpty(value) {
|
|
65
|
-
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
66
|
-
}
|
|
67
65
|
/**
|
|
68
66
|
* Normalize Twitch channel names.
|
|
69
67
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/twitch",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.10-beta.1",
|
|
4
4
|
"description": "OpenClaw Twitch channel plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"minHostVersion": ">=2026.4.10"
|
|
27
27
|
},
|
|
28
28
|
"compat": {
|
|
29
|
-
"pluginApi": ">=2026.5.
|
|
29
|
+
"pluginApi": ">=2026.5.10-beta.1"
|
|
30
30
|
},
|
|
31
31
|
"build": {
|
|
32
|
-
"openclawVersion": "2026.5.
|
|
32
|
+
"openclawVersion": "2026.5.10-beta.1"
|
|
33
33
|
},
|
|
34
34
|
"channel": {
|
|
35
35
|
"id": "twitch",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"README.md"
|
|
57
57
|
],
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"openclaw": ">=2026.5.
|
|
59
|
+
"openclaw": ">=2026.5.10-beta.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"openclaw": {
|