@qihoo/tuitui-openclaw-channel 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
@@ -18,7 +18,6 @@
18
18
  "description": "TuiTui channel plugin for OpenClaw",
19
19
  "type": "module",
20
20
  "dependencies": {
21
- "zod": "^4.3.6",
22
21
  "ws": "^8.13.0"
23
22
  },
24
23
  "openclaw": {
package/src/channel.ts CHANGED
@@ -5,12 +5,10 @@
5
5
  * Supports single chat (text, image, voice, file) and group chat with @mentions.
6
6
  */
7
7
  import WebSocket from 'ws';
8
- import { z } from 'zod';
9
8
  import {
10
9
  DEFAULT_ACCOUNT_ID,
11
10
  setAccountEnabledInConfigSection,
12
11
  deleteAccountFromConfigSection,
13
- buildChannelConfigSchema,
14
12
  } from 'openclaw/plugin-sdk';
15
13
  import type { TuiTuiInboundMessage, TuiTuiOutboundDeliverOptions } from './types';
16
14
  import {
@@ -24,86 +22,33 @@ import {
24
22
  sendMediaMsg,
25
23
  } from "./utils";
26
24
 
27
- function resolveAccount(cfg: any, accountId?: string | null) {
25
+ import { capabilities, configSchema } from './confs';
26
+
27
+ const isEnabled = (val: any) => val === undefined || !!val;
28
+ const isConfigured = (account: any)=> !!(account?.appId && account?.appSecret);
29
+ const resolveAccount = (cfg: any, accountId?: string | null) => {
28
30
  const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
29
31
  const targetId = accountId || DEFAULT_ACCOUNT_ID;
30
32
  const acct = targetId === DEFAULT_ACCOUNT_ID ? channelConfig : channelConfig.accounts?.[targetId];
31
33
  return {
32
34
  accountId: targetId,
33
- enabled: acct?.enabled || false,
35
+ enabled: isEnabled(acct?.enabled),
34
36
  appId: acct?.appId as string | undefined,
35
37
  appSecret: acct?.appSecret as string | undefined,
36
38
  dmPolicy: acct?.dmPolicy || 'pairing',
37
39
  allowFrom: acct?.allowFrom || [],
38
40
  // 群组策略与白名单、群组级覆盖
39
41
  groupPolicy: (acct?.groupPolicy as string | undefined) ?? 'allowlist',
40
- groupAllowFrom: acct.groupAllowFrom || [],
42
+ groupAllowFrom: acct?.groupAllowFrom || [],
41
43
  groups: (acct?.groups as Record<string, { requireMention?: boolean, shouldReply?: boolean }> | undefined) ?? {},
42
44
  };
43
- }
44
-
45
- const isConfigured = (account: any)=> Boolean(
46
- String(account?.appId ?? '').trim() &&
47
- String(account?.appSecret ?? '').trim()
48
- );
49
-
45
+ };
50
46
  const isToGroup = (chatId: string) => /^\d+$/.test(chatId);
51
- const arrLowerCaseTrim = (arr: any[]) => arr.filter((v: any) => !!v).map((v: any) => String(v).toLowerCase().trim())
52
- const configSchema = buildChannelConfigSchema(
53
- z
54
- .object({
55
- enabled: z.boolean().optional().describe('开启或关闭'),
56
- appId: z.string().min(1).describe('推推机器人身份 AppId(你可以推推搜索【推推机器人助手】,和它聊天自助申请推推机器人)'),
57
- appSecret: z.string().min(1).describe('推推机器人密钥 Secret'),
58
- // 私聊策略(默认 pairing)
59
- dmPolicy: z
60
- .enum(['pairing', 'allowlist', 'open', 'disabled'])
61
- .optional()
62
- .default('pairing')
63
- .describe('私聊策略:pairing=配对(默认);allowlist=白名单;open=允许所有(不安全);disabled=禁用私聊',),
64
- // 私聊允许列表(当 dmPolicy=allowlist 生效;pairing 下也可显式允许)
65
- allowFrom: z
66
- .array(z.string())
67
- .optional()
68
- .describe('私聊白名单(dmPolicy=allowlist 时生效;pairing 下可用于显式放行用户)'),
69
-
70
- // 群组策略
71
- groupPolicy: z
72
- .enum(['allowlist', 'disabled'])
73
- .default('allowlist')
74
- .describe('群聊策略:allowlist=白名单;disabled=禁用群聊',),
75
- // 仅在 allowlist 生效的群组 ID 列表
76
- groupAllowFrom: z
77
- .array(z.string())
78
- .optional()
79
- .describe('群组白名单(仅在 groupPolicy=allowlist 生效)'),
80
- // 每个群组的覆盖配置
81
- /*
82
- groups: z
83
- .record(
84
- z.string(),
85
- z
86
- .object({
87
- requireMention: z
88
- .boolean()
89
- .optional()
90
- .describe('该群组是否需要 @ 机器人才触发 Agent(默认 true)'),
91
- shouldReply: z
92
- .boolean()
93
- .optional()
94
- .describe('该群组是否允许 Agent 回复(默认 true)'),
95
- })
96
- .passthrough(),
97
- )
98
- .optional()
99
- .describe('群组级覆盖配置'),
100
- */
101
- }) as any,
102
- );
47
+ const arrLowerCaseTrim = (arr: any[]) => arr.filter((v: any) => !!v).map((v: any) => String(v).toLowerCase().trim());
48
+
103
49
  export function createTuiTuiChannelPlugin(apiRuntime: any) {
104
50
  return {
105
51
  id: CHANNEL_ID,
106
-
107
52
  meta: {
108
53
  id: CHANNEL_ID,
109
54
  label: CHANNEL_NAME,
@@ -113,29 +58,16 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
113
58
  blurb: `Connect to ${CHANNEL_NAME} bot via WebSocket`,
114
59
  order: 100,
115
60
  },
116
-
117
- capabilities: {
118
- chatTypes: ['direct' as const, 'group' as const],
119
- media: true,
120
- threads: false,
121
- reactions: false,
122
- edit: false,
123
- unsend: false,
124
- reply: true,
125
- effects: false,
126
- blockStreaming: false,
127
- },
128
-
129
- reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
130
-
61
+ capabilities,
131
62
  configSchema,
63
+ reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
132
64
 
133
65
  config: {
134
66
  listAccountIds: (cfg: any) => {
135
67
  const base = cfg?.channels?.[CHANNEL_ID];
136
68
  if (!base) return [];
137
69
  const ids = new Set<string>();
138
- if (base.enabled) ids.add(DEFAULT_ACCOUNT_ID);
70
+ if (isEnabled(base.enabled)) ids.add(DEFAULT_ACCOUNT_ID);
139
71
  if (base.accounts) for (const k in base.accounts) ids.add(k);
140
72
  return Array.from(ids);
141
73
  },
@@ -144,13 +76,13 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
144
76
 
145
77
  defaultAccountId: (_cfg: any) => DEFAULT_ACCOUNT_ID,
146
78
 
147
- isEnabled: (account: any) => account?.enabled !== false,
79
+ isEnabled,
148
80
 
149
81
  isConfigured,
150
82
 
151
83
  describeAccount: (account: any) => ({
152
84
  accountId: account?.accountId || DEFAULT_ACCOUNT_ID,
153
- enabled: account?.enabled !== false,
85
+ enabled: isEnabled(account?.enabled),
154
86
  configured: isConfigured(account),
155
87
  }),
156
88
 
@@ -180,7 +112,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
180
112
  ...cfg.channels,
181
113
  [CHANNEL_ID]: {
182
114
  ...(cfg?.channels?.[CHANNEL_ID] ?? {}),
183
- enabled: false,
115
+ enabled: true,
184
116
  appId: undefined,
185
117
  appSecret: undefined,
186
118
  },
@@ -215,7 +147,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
215
147
  return {
216
148
  ...params.runtime,
217
149
  accountId: params.accountId ?? account?.accountId ?? DEFAULT_ACCOUNT_ID,
218
- enabled: account?.enabled !== false,
150
+ enabled: isEnabled(account?.enabled),
219
151
  configured: isConfigured(account),
220
152
  };
221
153
  },
@@ -314,7 +246,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
314
246
  };
315
247
  const account = resolveAccount(cfg, accountId);
316
248
 
317
- if (!account.enabled) {
249
+ if (!isEnabled(account.enabled)) {
318
250
  log?.info?.(`[${CHANNEL_ID}] account ${accountId} is disabled, skipping`);
319
251
  return _setStatus();
320
252
  }
@@ -576,8 +508,24 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
576
508
  // 确保 accountId 有有效值,优先使用 account.accountId,其次使用 ctx 中的 accountId,最后使用 DEFAULT_ACCOUNT_ID
577
509
 
578
510
  const effectiveAccountId = String(account.accountId || accountId || DEFAULT_ACCOUNT_ID || 'default');
579
- const sessionKey = `${CHANNEL_ID}:${effectiveAccountId}:${chatTypeIsGroup ? groupId : userAccount}`;
580
- console.log(`[${CHANNEL_ID}] effectiveAccountId infos: account.accountId : ${account.accountId}, accountId: ${accountId}, DEFAULT_ACCOUNT_ID: ${DEFAULT_ACCOUNT_ID}, chatType: ${chatType}, sessionKey: ${sessionKey}, effectiveAccountId: ${effectiveAccountId}`);
511
+ const chatId = chatTypeIsGroup ? String(groupId) : String(senderId);
512
+
513
+ // 关于 sessionKey 格式的解释
514
+ // https://docs.openclaw.ai/channels/channel-routing
515
+ // 但 sessionKey 一般不要自己拼字符串,需要用系统api识别(根据 bindings)
516
+ const route = apiRuntime.channel.routing.resolveAgentRoute({
517
+ cfg: await apiRuntime.config.loadConfig(),
518
+ channel: CHANNEL_ID,
519
+ accountId: account.accountId,
520
+ peer: {
521
+ kind: chatTypeIsGroup ? 'group' : 'direct',
522
+ id: chatId,
523
+ },
524
+ });
525
+
526
+ const sessionKey = route.sessionKey;
527
+ console.log(`[${CHANNEL_ID}] chatType: ${chatType}, chatId ${chatId}, senderId: ${senderId}, accountId: ${accountId}, DEFAULT_ACCOUNT_ID: ${DEFAULT_ACCOUNT_ID}, effectiveAccountId: ${effectiveAccountId}`);
528
+ console.log(`[${CHANNEL_ID}] dispatching to agent session=${sessionKey}`);
581
529
 
582
530
  const msgCtx: any = {
583
531
  Body: text || ' ',
@@ -587,11 +535,13 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
587
535
  SessionKey: String(sessionKey).replace(/\//g, '_'),
588
536
  AccountId: effectiveAccountId,
589
537
  OriginatingChannel: CHANNEL_ID,
590
- OriginatingTo: chatTypeIsGroup ? String(groupId) : String(senderId),
538
+ OriginatingTo: chatId,
591
539
  ChatType: chatType,
592
540
  Surface: CHANNEL_ID,
593
541
  Provider: CHANNEL_ID,
594
542
  SenderName: msgUname || String(senderId),
543
+ MsgUname: msgUname,
544
+ UserAccount: userAccount,
595
545
  };
596
546
 
597
547
  // Add group-specific fields
@@ -666,7 +616,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
666
616
  _sendMsg = defSendMsg;
667
617
  if (!abortSignal?.aborted) {
668
618
  log?.warn?.(`[${CHANNEL_ID}] WebSocket Restart`);
669
- startWebSocket(); // 重启
619
+ setTimeout(startWebSocket, 10e3); // 10秒后尝试重启
670
620
  }
671
621
  };
672
622
  ws.on('close', () => {
package/src/confs.ts ADDED
@@ -0,0 +1,90 @@
1
+ /*** 一些配置字段设定信息 ***/
2
+ const fieldsConf = {
3
+ enabled: { type: 'boolean', default: true },
4
+ appId: { type: 'string' } ,
5
+ appSecret: { type: 'string' },
6
+ dmPolicy: {
7
+ type: 'string',
8
+ default: 'pairing',
9
+ enum: ['pairing', 'allowlist', 'open', 'disabled'],
10
+ },
11
+ allowFrom: {
12
+ type: 'array',
13
+ items: { type: 'string' }
14
+ },
15
+ groupPolicy: {
16
+ type: 'string',
17
+ default: 'allowlist',
18
+ enum: ['allowlist', 'disabled']
19
+ },
20
+ groupAllowFrom: {
21
+ type: 'array',
22
+ items: { type: 'string' }
23
+ },
24
+ };
25
+
26
+ export const configSchema = {
27
+ schema: {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ properties: {
31
+ ...fieldsConf,
32
+ /* 基础配置信息中,准备 accounts 配置支持多账户管理,以备后续允许用户配置多个推推机器人账号
33
+ accounts: {
34
+ type: 'object',
35
+ additionalProperties: {
36
+ type: 'object',
37
+ additionalProperties: false,
38
+ properties: { ...fieldsConf },
39
+ },
40
+ },*/
41
+ },
42
+ },
43
+ uiHints: {
44
+ // 基础配置(优先显示)
45
+ enabled: { order: 1, help: '开启或关闭' },
46
+ appId: {
47
+ help: '推推机器人身份 AppId(你可以推推搜索【推推机器人助手】,和它聊天自助申请推推机器人)',
48
+ order: 2,
49
+ },
50
+ appSecret: {
51
+ help: '推推机器人密钥 Secret(推推机器人的 App Secret)',
52
+ order: 3,
53
+ },
54
+ // 高级配置
55
+ dmPolicy: {
56
+ help: '私聊策略(pairing=配对(默认);allowlist=白名单;open=允许所有(不安全);disabled=禁用私聊)',
57
+ order: 10,
58
+ },
59
+ allowFrom: {
60
+ help: '私聊白名单(dmPolicy=allowlist 时生效;pairing 下可用于显式放行用户)',
61
+ order: 11,
62
+ },
63
+ groupPolicy: {
64
+ help: '群聊策略(allowlist=白名单;disabled=禁用群聊)',
65
+ order: 12,
66
+ },
67
+ groupAllowFrom: {
68
+ help: '群组白名单(仅在 groupPolicy=allowlist 生效)',
69
+ order: 13,
70
+ },
71
+ /* 基础配置信息中,准备 accounts 配置支持多账户管理,以备后续允许用户配置多个推推机器人账号
72
+ accounts: {
73
+ help: 'Accounts(多账户配置)',
74
+ order: 30,
75
+ advanced: true
76
+ },*/
77
+ },
78
+ };
79
+
80
+ export const capabilities = {
81
+ chatTypes: ['direct' as const, 'group' as const],
82
+ media: true,
83
+ threads: false,
84
+ reactions: false,
85
+ edit: false,
86
+ unsend: false,
87
+ reply: true,
88
+ effects: false,
89
+ blockStreaming: false,
90
+ };