@happycastle/openclaw-channel-talk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,55 @@
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
9
- "properties": {}
9
+ "properties": {
10
+ "enabled": {
11
+ "type": "boolean",
12
+ "default": true,
13
+ "description": "Enable/disable the Channel Talk plugin"
14
+ },
15
+ "accessKey": {
16
+ "type": "string",
17
+ "description": "Channel Talk access key for API authentication"
18
+ },
19
+ "accessSecret": {
20
+ "type": "string",
21
+ "description": "Channel Talk access secret for API authentication"
22
+ },
23
+ "webhook": {
24
+ "type": "object",
25
+ "properties": {
26
+ "port": {
27
+ "type": "number",
28
+ "default": 3979,
29
+ "description": "Port for webhook server"
30
+ },
31
+ "path": {
32
+ "type": "string",
33
+ "default": "/api/channel-talk",
34
+ "description": "Path for webhook endpoint"
35
+ }
36
+ }
37
+ },
38
+ "botName": {
39
+ "type": "string",
40
+ "description": "Bot display name for sent messages"
41
+ },
42
+ "groupPolicy": {
43
+ "type": "string",
44
+ "enum": ["open", "closed"],
45
+ "default": "open",
46
+ "description": "Group chat policy"
47
+ },
48
+ "allowedGroups": {
49
+ "type": "array",
50
+ "items": { "type": "string" },
51
+ "description": "List of group chatIds to respond to"
52
+ },
53
+ "mentionOnly": {
54
+ "type": "boolean",
55
+ "default": false,
56
+ "description": "Only respond when the bot is mentioned"
57
+ }
58
+ }
10
59
  }
11
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/openclaw-channel-talk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OpenClaw Channel Talk (채널톡) Team Chat channel plugin",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/channel.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk';
2
- import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk';
2
+ import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk';
3
3
  import type { ChannelTalkCredentials } from './types.js';
4
4
  import { ChannelTalkConfigSchema } from './config-schema.js';
5
5
  import { channelTalkOutbound } from './send.js';
@@ -56,7 +56,7 @@ export const channelTalkPlugin: ChannelPlugin<ResolvedChannelTalkAccount> = {
56
56
 
57
57
  reload: { configPrefixes: ['channels.channel-talk'] },
58
58
 
59
- configSchema: buildChannelConfigSchema(ChannelTalkConfigSchema as any),
59
+ configSchema: ChannelTalkConfigSchema,
60
60
 
61
61
  config: {
62
62
  listAccountIds: () => [DEFAULT_ACCOUNT_ID],
@@ -1,60 +1,90 @@
1
1
  /**
2
2
  * Channel Talk Plugin Configuration Schema
3
- * Defines the structure and validation for Channel Talk plugin configuration
3
+ * Plain JSON Schema avoids TypeBox compatibility issues with OpenClaw's config validator.
4
4
  */
5
5
 
6
- import { Type } from '@sinclair/typebox';
7
- import type { Static } from '@sinclair/typebox';
8
-
9
- /**
10
- * Channel Talk configuration schema using TypeBox
11
- * Defines required and optional configuration fields for the plugin
12
- */
13
- export const ChannelTalkConfigSchema = Type.Object(
14
- {
6
+ export const ChannelTalkConfigSchema = {
7
+ type: 'object' as const,
8
+ additionalProperties: false,
9
+ properties: {
15
10
  /** Enable/disable the Channel Talk plugin */
16
- enabled: Type.Optional(Type.Boolean({ default: true })),
17
-
11
+ enabled: {
12
+ type: 'boolean' as const,
13
+ default: true,
14
+ description: 'Enable/disable the Channel Talk plugin',
15
+ },
16
+
18
17
  /** Channel Talk API access key (required when enabled) */
19
- accessKey: Type.String({
18
+ accessKey: {
19
+ type: 'string' as const,
20
20
  description: 'Channel Talk access key for API authentication',
21
- }),
22
-
21
+ },
22
+
23
23
  /** Channel Talk API access secret (required when enabled) */
24
- accessSecret: Type.String({
24
+ accessSecret: {
25
+ type: 'string' as const,
25
26
  description: 'Channel Talk access secret for API authentication',
26
- }),
27
-
27
+ },
28
+
28
29
  /** Webhook configuration */
29
- webhook: Type.Optional(
30
- Type.Object({
31
- /** Port for webhook server (default: 3979) */
32
- port: Type.Optional(Type.Number({ default: 3979 })),
33
-
34
- /** Path for webhook endpoint (default: /api/channel-talk) */
35
- path: Type.Optional(Type.String({ default: '/api/channel-talk' })),
36
- })
37
- ),
38
-
30
+ webhook: {
31
+ type: 'object' as const,
32
+ properties: {
33
+ port: {
34
+ type: 'number' as const,
35
+ default: 3979,
36
+ description: 'Port for webhook server (default: 3979)',
37
+ },
38
+ path: {
39
+ type: 'string' as const,
40
+ default: '/api/channel-talk',
41
+ description: 'Path for webhook endpoint (default: /api/channel-talk)',
42
+ },
43
+ },
44
+ },
45
+
39
46
  /** Bot display name for sent messages (optional) */
40
- botName: Type.Optional(
41
- Type.String({
42
- description: 'Bot display name for sent messages',
43
- })
44
- ),
45
-
46
- /** Group chat policy: 'open' = all groups allowed, 'closed' = none allowed */
47
- groupPolicy: Type.Optional(
48
- Type.Enum(['open', 'closed'], { default: 'open' })
49
- ),
47
+ botName: {
48
+ type: 'string' as const,
49
+ description: 'Bot display name for sent messages',
50
+ },
51
+
52
+ /** Group chat policy: "open" = all groups allowed, "closed" = none allowed */
53
+ groupPolicy: {
54
+ type: 'string' as const,
55
+ enum: ['open', 'closed'],
56
+ default: 'open',
57
+ description: 'Group chat policy: "open" = all groups allowed, "closed" = none',
58
+ },
59
+
60
+ /** Allowlist of group chatIds to respond to */
61
+ allowedGroups: {
62
+ type: 'array' as const,
63
+ items: { type: 'string' as const },
64
+ description:
65
+ 'List of group chatIds to respond to. If empty or omitted, all groups are allowed (subject to groupPolicy).',
66
+ },
67
+
68
+ /** Only respond when the bot is mentioned */
69
+ mentionOnly: {
70
+ type: 'boolean' as const,
71
+ default: false,
72
+ description:
73
+ 'Only respond when the bot is mentioned in the message. Similar to Discord mention-only mode.',
74
+ },
50
75
  },
51
- {
52
- additionalProperties: false,
53
- }
54
- );
76
+ };
55
77
 
56
78
  /**
57
- * TypeScript type derived from the schema
58
- * Use this for type-safe configuration handling
79
+ * TypeScript type for the config (manual, not derived from TypeBox)
59
80
  */
60
- export type ChannelTalkConfig = Static<typeof ChannelTalkConfigSchema>;
81
+ export interface ChannelTalkConfig {
82
+ enabled?: boolean;
83
+ accessKey: string;
84
+ accessSecret: string;
85
+ webhook?: { port?: number; path?: string };
86
+ botName?: string;
87
+ groupPolicy?: 'open' | 'closed';
88
+ allowedGroups?: string[];
89
+ mentionOnly?: boolean;
90
+ }
package/src/webhook.ts CHANGED
@@ -8,6 +8,27 @@ const DEFAULT_ACCOUNT_ID = 'default';
8
8
  const DEDUP_TTL_MS = 60_000;
9
9
  const DEDUP_CLEANUP_INTERVAL_MS = 30_000;
10
10
 
11
+ /**
12
+ * Check if a message mentions the bot.
13
+ * Matches @botName or common patterns like "봇이름아", "봇이름아,", etc.
14
+ * If no botName is configured, always returns false (falls through to respond to all).
15
+ */
16
+ function checkMention(text: string, botName?: string): boolean {
17
+ if (!botName) return false;
18
+ const lower = text.toLowerCase();
19
+ const nameLower = botName.toLowerCase();
20
+ // Direct @mention
21
+ if (lower.includes(`@${nameLower}`)) return true;
22
+ // Name appears at start or as a word boundary
23
+ const namePattern = new RegExp(`(?:^|\\s)${escapeRegex(nameLower)}(?:[아야,!?\\s]|$)`, 'i');
24
+ if (namePattern.test(text)) return true;
25
+ return false;
26
+ }
27
+
28
+ function escapeRegex(str: string): string {
29
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ }
31
+
11
32
  export type StartChannelTalkWebhookContext = {
12
33
  cfg: OpenClawConfig;
13
34
  runtime: { log?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
@@ -56,6 +77,8 @@ export async function startChannelTalkWebhook(
56
77
  const webhookPath = webhookCfg?.path ?? '/api/channel-talk';
57
78
  const botName = channelTalkCfg.botName as string | undefined;
58
79
  const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
80
+ const allowedGroups = channelTalkCfg.allowedGroups as string[] | undefined;
81
+ const mentionOnly = channelTalkCfg.mentionOnly as boolean | undefined;
59
82
 
60
83
  const apiClient = createApiClient({ accessKey, accessSecret }, channelTalkCfg.baseUrl as string | undefined);
61
84
 
@@ -151,6 +174,24 @@ export async function startChannelTalkWebhook(
151
174
  return;
152
175
  }
153
176
 
177
+ // --- Group allowlist filtering ---
178
+ if (allowedGroups && allowedGroups.length > 0) {
179
+ if (!allowedGroups.includes(groupId)) {
180
+ log.debug?.('skipping message from non-allowed group', { groupId });
181
+ return;
182
+ }
183
+ }
184
+
185
+ // --- Mention-only filtering ---
186
+ const wasMentioned = mentionOnly
187
+ ? checkMention(plainText, botName)
188
+ : false;
189
+
190
+ if (mentionOnly && !wasMentioned) {
191
+ log.debug?.('skipping non-mentioned message (mentionOnly=true)', { groupId });
192
+ return;
193
+ }
194
+
154
195
  const managerId = refers?.manager?.id ?? entity.personId ?? 'unknown';
155
196
  const managerName = refers?.manager?.name ?? managerId;
156
197
  const timestamp = entity.createdAt ?? Date.now();
@@ -215,7 +256,7 @@ export async function startChannelTalkWebhook(
215
256
  Surface: 'channel-talk' as const,
216
257
  MessageSid: messageId,
217
258
  Timestamp: timestamp,
218
- WasMentioned: false,
259
+ WasMentioned: wasMentioned || !mentionOnly,
219
260
  CommandAuthorized: false,
220
261
  OriginatingChannel: 'channel-talk' as const,
221
262
  OriginatingTo: `group:${groupId}`,