@xgjktech/xg_cwork_im 1.0.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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * XG-IM 群聊历史记录工具(供 AI 按需调用)
3
+ *
4
+ * 通过 OpenClaw 的 api.registerTool() 注入,让 AI 在感知到需要上下文时
5
+ * 主动拉取群聊最近 N 条历史消息,而不是每次收到 @ 都强行拉取。
6
+ */
7
+
8
+ import axios from "axios";
9
+ import { Type, type Static } from "@sinclair/typebox";
10
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
11
+ import { jsonResult } from "openclaw/plugin-sdk";
12
+ import { getToken } from "./auth.js";
13
+ import type { BotIdentity, WsMessageParams, XgImConfig } from "./types.js";
14
+
15
+ // ─── 工具参数 Schema ─────────────────────────────────────────────────────────
16
+
17
+ const GroupHistoryParams = Type.Object({
18
+ groupId: Type.String({
19
+ description: "群聊 ID,即当前消息来自的群组唯一标识符",
20
+ }),
21
+ limit: Type.Optional(
22
+ Type.Number({
23
+ description: "需要获取的消息条数(5~30),默认 10",
24
+ minimum: 1,
25
+ maximum: 30,
26
+ default: 10,
27
+ }),
28
+ ),
29
+ });
30
+
31
+ // ─── 内部 HTTP 工具 ──────────────────────────────────────────────────────────
32
+
33
+ /** 从 OpenClaw 配置中提取 xg_cwork_im 配置(支持多账户取第一个) */
34
+ export function extractXgCworkImConfig(cfg: unknown): XgImConfig | null {
35
+ const raw = (cfg as Record<string, Record<string, unknown>>)?.channels?.xg_cwork_im as XgImConfig | undefined;
36
+ if (!raw?.baseUrl) return null;
37
+
38
+ // 如果有多账户,把 accounts[0] 的 appKey 合并到顶层(用于查询历史)
39
+ if (raw.accounts && raw.accounts.length > 0) {
40
+ const first = raw.accounts[0];
41
+ return { ...raw, ...first };
42
+ }
43
+ return raw;
44
+ }
45
+
46
+ /** 调用 IM 接口拉取群聊最近 N 条消息 */
47
+ async function fetchGroupHistory(
48
+ config: XgImConfig,
49
+ token: string,
50
+ groupId: string,
51
+ limit: number,
52
+ ): Promise<WsMessageParams[]> {
53
+ const url = `${config.baseUrl}/im/message/getLatestMsgListForAI`;
54
+
55
+ const res = await axios.get<{
56
+ resultCode: number;
57
+ message?: string;
58
+ data?: WsMessageParams[];
59
+ }>(url, {
60
+ params: { groupId, msgCount: limit },
61
+ headers: { "access-token": token },
62
+ timeout: 8_000,
63
+ });
64
+
65
+ const body = res.data;
66
+ if ((body.resultCode !== 1 && body.resultCode !== 200) || !Array.isArray(body.data)) {
67
+ return [];
68
+ }
69
+ return body.data;
70
+ }
71
+
72
+ // ─── 工具构建函数(直接传 config,而非 factory 模式)──────────────────────────
73
+
74
+ /**
75
+ * 构建群聊历史工具对象(使用注册时已知的 config,绕开 factory 工厂模式的命名限制)。
76
+ * 由 index.ts 在 register() 里调用,并将构建好的工具对象静态地传给 api.registerTool(tool)。
77
+ */
78
+ export function buildGroupHistoryTool(config: XgImConfig): AnyAgentTool {
79
+ return {
80
+ name: "xg_cwork_im_get_group_chat_history",
81
+ label: "获取群聊历史记录",
82
+ description: [
83
+ "【必须遵守】获取此 IM 群聊的最近消息历史,用于理解对话上下文背景。",
84
+ "",
85
+ "以下情况你必须先调用此工具,不得直接向用户反问或要求澄清:",
86
+ "- 用户使用了指代词却没说清楚指的是什么(如'这'、'那'、'上面说的'、'刚才提到的'、'这两家公司'等)",
87
+ "- 用户的问题依赖之前的对话才能正确理解(如'对吗'、'怎么查的'、'分析一下'但没给出主语)",
88
+ "- 你不确定用户问题的背景或主语时",
89
+ "",
90
+ "调用此工具后你会获得最近 N 条消息,用于还原上下文再给出回答。",
91
+ "groupId 从会话元数据 group_channel 字段中解析(格式:agent:main:xgim:group:<groupId>)。",
92
+ "",
93
+ "【只有在以下情况才可以不调用】:用户的问题明确且自洽,不依赖任何历史对话。",
94
+ ].join("\n"),
95
+ parameters: GroupHistoryParams,
96
+ async execute(_toolCallId: string, params: Static<typeof GroupHistoryParams>) {
97
+ const limit = params.limit ?? 10;
98
+ const { groupId } = params;
99
+
100
+ console.log(`[cwork_im][tool] xg_cwork_im_get_group_chat_history called: groupId=${groupId} limit=${limit}`);
101
+
102
+ let identity: BotIdentity;
103
+ try {
104
+ identity = await getToken(config);
105
+ console.log(`[cwork_im][tool] token acquired for userId=${identity.userId}`);
106
+ } catch (err: unknown) {
107
+ return jsonResult({
108
+ ok: false,
109
+ error: `Failed to obtain access token: ${String(err)}`,
110
+ messages: [],
111
+ });
112
+ }
113
+
114
+ let messages: WsMessageParams[];
115
+ try {
116
+ messages = await fetchGroupHistory(config, identity.token, groupId, limit);
117
+ console.log(`[cwork_im][tool] fetched ${messages.length} messages from group ${groupId}`);
118
+ } catch (err: unknown) {
119
+ return jsonResult({
120
+ ok: false,
121
+ error: `Failed to fetch group history: ${String(err)}`,
122
+ messages: [],
123
+ });
124
+ }
125
+
126
+ // 格式化成对话摘要,方便 AI 理解
127
+ const formatted = messages
128
+ .filter((m) => m.msgContent?.type === "text")
129
+ .map((m) => {
130
+ const isBot = m.fromUserId === identity.userId;
131
+ const sender = isBot ? "[AI]" : m.fromUserName || m.fromUserId;
132
+ const ts = m.timestamp ?? m.msgSendTime ?? 0;
133
+ const time = new Date(ts).toLocaleTimeString("zh-CN", { hour12: false });
134
+ return `[${time}] ${sender}: ${m.msgContent.text}`;
135
+ });
136
+
137
+ return jsonResult({
138
+ ok: true,
139
+ groupId,
140
+ count: formatted.length,
141
+ messages: formatted,
142
+ });
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * XG-IM 发送群消息工具(供 AI 按需调用)
3
+ *
4
+ * 适用场景:
5
+ * - Cron 定时任务主动推送通知
6
+ * - AI 分析后需要主动发起提醒(无需用户先 @ 机器人)
7
+ *
8
+ * 通过 api.registerTool() 注入,工具内部自动获取机器人 token,AI 无需感知鉴权细节。
9
+ */
10
+
11
+ import { Type, type Static } from "@sinclair/typebox";
12
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
13
+ import { jsonResult } from "openclaw/plugin-sdk";
14
+ import { getToken } from "./auth.js";
15
+ import { sendTextMessage } from "./send-service.js";
16
+ import type { BotIdentity, XgImConfig } from "./types.js";
17
+
18
+ // ─── 工具参数 Schema ─────────────────────────────────────────────────────────
19
+
20
+ const SendGroupMessageParams = Type.Object({
21
+ groupId: Type.String({
22
+ description: "目标群聊 ID(IM 系统的群组唯一标识符,即 groupId / gid)",
23
+ }),
24
+ text: Type.String({
25
+ description: "要发送的消息内容(纯文本)",
26
+ }),
27
+ atUserIds: Type.Optional(
28
+ Type.Array(Type.String(), {
29
+ description: "需要 @ 的用户 ID 列表,可为空数组",
30
+ }),
31
+ ),
32
+ });
33
+
34
+ // ─── 工具构建函数 ─────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * 构建发送群消息工具对象。
38
+ * 由 index.ts 在 register() 里调用,config 在注册时捕获进闭包,
39
+ * 工具执行时自动调用 getToken(config) 获取机器人 token。
40
+ */
41
+ export function buildSendGroupMessageTool(config: XgImConfig): AnyAgentTool {
42
+ return {
43
+ name: "xg_cwork_im_send_group_message",
44
+ label: "发送群聊消息",
45
+ description: [
46
+ "向指定 IM 群聊发送一条消息。",
47
+ "适用于定时任务主动推送通知、AI 分析后发起提醒等场景。",
48
+ "消息将以机器人身份发出,可选择 @ 特定用户。",
49
+ "groupId 为 IM 群组的唯一 ID(不是群名称)。",
50
+ ].join("\n"),
51
+ parameters: SendGroupMessageParams,
52
+ async execute(_toolCallId: string, params: Static<typeof SendGroupMessageParams>) {
53
+ const { groupId, text, atUserIds = [] } = params;
54
+
55
+ console.log(`[cwork_im][tool] xg_cwork_im_send_group_message called: groupId=${groupId} atUsers=${JSON.stringify(atUserIds)}`);
56
+
57
+ let identity: BotIdentity;
58
+ try {
59
+ identity = await getToken(config);
60
+ console.log(`[cwork_im][tool] xg_cwork_im_send_group_message token acquired for userId=${identity.userId}`);
61
+ } catch (err: unknown) {
62
+ return jsonResult({
63
+ ok: false,
64
+ error: `Failed to obtain access token: ${String(err)}`,
65
+ });
66
+ }
67
+
68
+ try {
69
+ await sendTextMessage(config, identity.token, groupId, text, atUserIds);
70
+ console.log(`[cwork_im][tool] xg_cwork_im_send_group_message success: groupId=${groupId}`);
71
+ } catch (err: unknown) {
72
+ return jsonResult({
73
+ ok: false,
74
+ error: `Failed to send message to group ${groupId}: ${String(err)}`,
75
+ });
76
+ }
77
+
78
+ return jsonResult({
79
+ ok: true,
80
+ groupId,
81
+ sentBy: identity.userId,
82
+ });
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * XG-IM 消息发送模块
3
+ *
4
+ * 调用 IM 接口将 AI 回复发送到指定群聊,并 @ 原始发送者。
5
+ */
6
+
7
+ import axios from "axios";
8
+ import type { Log } from "./auth.js";
9
+ import type { GetLatestMsgListResponse, SendMessageBody, WsMessageParams, XgImConfig } from "./types.js";
10
+
11
+ /**
12
+ * 发送文本消息到指定 IM 群聊。
13
+ *
14
+ * @param config 插件配置
15
+ * @param token 鉴权 token
16
+ * @param groupId 目标群聊 ID(gid / groupId)
17
+ * @param content 消息内容文本
18
+ * @param atUserIds 需要 @ 的用户 ID 列表(可为空)
19
+ * @param log 日志接口
20
+ */
21
+ export async function sendTextMessage(
22
+ config: XgImConfig,
23
+ token: string,
24
+ groupId: string,
25
+ content: string,
26
+ atUserIds: string[] = [],
27
+ log?: Log,
28
+ ): Promise<void> {
29
+ const url = `${config.baseUrl}/im/message/send`;
30
+
31
+ const body: SendMessageBody = {
32
+ type: "TEXT",
33
+ groupId,
34
+ text: content,
35
+ ...(atUserIds.length > 0 ? { atUserIds } : {}),
36
+ };
37
+
38
+ log?.info(`[cwork_im:send] POST ${url} groupId=${groupId} atUsers=${JSON.stringify(atUserIds)}`);
39
+ if (config.debug) {
40
+ log?.debug?.(`[cwork_im:send] Request body: ${JSON.stringify(body)}`);
41
+ }
42
+
43
+ try {
44
+ const res = await axios.post(url, body, {
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ "access-token": token,
48
+ },
49
+ timeout: 10_000,
50
+ });
51
+
52
+ log?.info(`[cwork_im:send] Response status=${res.status}`);
53
+ if (config.debug) {
54
+ log?.debug?.(`[cwork_im:send] Response body: ${JSON.stringify(res.data)}`);
55
+ }
56
+ } catch (err: unknown) {
57
+ const msg = err instanceof Error ? err.message : String(err);
58
+ log?.error(`[cwork_im:send] Failed to send message to groupId=${groupId}: ${msg}`);
59
+ throw err;
60
+ }
61
+ }
62
+
package/src/types.ts ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * XG-IM Channel Plugin — 类型定义
3
+ */
4
+
5
+ import type {
6
+ OpenClawConfig,
7
+ OpenClawPluginApi,
8
+ ChannelLogSink as SDKChannelLogSink,
9
+ ChannelAccountSnapshot as SDKChannelAccountSnapshot,
10
+ ChannelGatewayContext as SDKChannelGatewayContext,
11
+ ChannelPlugin as SDKChannelPlugin,
12
+ PluginRuntime,
13
+ } from "openclaw/plugin-sdk";
14
+
15
+ // ─── 插件模块 ───────────────────────────────────────────────────────────────
16
+
17
+ export interface XgImPluginModule {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ configSchema?: unknown;
22
+ register?: (api: OpenClawPluginApi) => void | Promise<void>;
23
+ }
24
+
25
+ /** 与 openclaw.plugin.json 中 id: xg_cwork_im 对应 */
26
+ export type XgCworkImPluginModule = XgImPluginModule;
27
+
28
+ // ─── Channel 配置 ────────────────────────────────────────────────────────────
29
+
30
+ /** 单个机器人账户配置 */
31
+ export interface XgImAccountConfig {
32
+ /** 机器人 appKey,从 IM 后台注册获取 */
33
+ appKey: string;
34
+ /** 对应 OpenClaw 的 Agent ID,默认为 'main' */
35
+ agentId?: string;
36
+ /** 账户显示名称 */
37
+ name?: string;
38
+ /** 群聊策略:open = 不需要 @,mention = 必须 @ 机器人才触发 */
39
+ groupPolicy?: "open" | "mention";
40
+ }
41
+
42
+ export interface XgImConfig extends OpenClawConfig {
43
+ /** 多账户列表 */
44
+ accounts?: XgImAccountConfig[];
45
+
46
+ /** 机器人 appKey(单账户模式) */
47
+ appKey?: string;
48
+ /** 对应 OpenClaw 的 Agent ID(单账户模式) */
49
+ agentId?: string;
50
+ /** IM 服务域名,如 https://test.xgjktech.com.cn */
51
+ baseUrl: string;
52
+ /** WebSocket 服务域名,如 wss://test.xgjktech.com.cn */
53
+ wsBaseUrl?: string;
54
+ /** 是否启用 */
55
+ enabled?: boolean;
56
+ /** 账户显示名称(单账户模式使用) */
57
+ name?: string;
58
+ /** 群聊策略:open = 不需要 @,mention = 必须 @ 机器人才触发 */
59
+ groupPolicy?: "open" | "mention";
60
+ /** 允许的发送者 userId 白名单(空表示全部允许) */
61
+ allowFrom?: string[];
62
+ /** 是否开启调试日志 */
63
+ debug?: boolean;
64
+ /** 最大重连次数(默认 10) */
65
+ maxConnectionAttempts?: number;
66
+ /** 初始重连延迟 ms(默认 1000) */
67
+ initialReconnectDelay?: number;
68
+ /** 最大重连延迟 ms(默认 60000) */
69
+ maxReconnectDelay?: number;
70
+ /** 重连延迟抖动因子 0-1(默认 0.3) */
71
+ reconnectJitter?: number;
72
+ }
73
+
74
+ // ─── IM 接口 Request / Response ─────────────────────────────────────────────
75
+
76
+ /** GET /user/login/appkey 的响应 */
77
+ export interface GetTokenResponse {
78
+ data?: {
79
+ xgToken: string;
80
+ empId: string;
81
+ userName?: string;
82
+ avatar?: string;
83
+ corpId?: string;
84
+ deptList?: unknown[];
85
+ appCode?: string;
86
+ telephone?: string;
87
+ personId?: string;
88
+ };
89
+ resultCode?: number;
90
+ resultMsg?: string | null;
91
+ }
92
+
93
+ /** 机器人身份信息(认证成功后缓存) */
94
+ export interface BotIdentity {
95
+ token: string;
96
+ userId: string;
97
+ name: string;
98
+ }
99
+
100
+ // ─── WebSocket 消息 ──────────────────────────────────────────────────────────
101
+
102
+ /** WebSocket 消息通知(cmd = robotMention) */
103
+ export interface WsMessage {
104
+ cmd: string;
105
+ params: WsMessageParams;
106
+ ts: number;
107
+ }
108
+
109
+ /** robotMention 消息的 params */
110
+ export interface WsMessageParams {
111
+ msgId: string;
112
+ groupId: string;
113
+ fromUserId: string;
114
+ fromUserName: string;
115
+ msgContent: {
116
+ text: string;
117
+ type: string;
118
+ url?: string;
119
+ ext?: Record<string, any>;
120
+ };
121
+ /** 服务端可能返回 msgSendTime 或 timestamp */
122
+ msgSendTime?: number;
123
+ timestamp?: number;
124
+ /** 被 @ 的人员 ID 列表 */
125
+ mentions?: string[] | null;
126
+ }
127
+
128
+ // ─── IM 发送消息 ─────────────────────────────────────────────────────────────
129
+
130
+ /** POST /im/message/send 请求体 */
131
+ export interface SendMessageBody {
132
+ type: "TEXT" | "RICH_TEXT" | "VOICE";
133
+ groupId?: string;
134
+ toUserId?: string;
135
+ text: string;
136
+ atUserIds?: string[];
137
+ }
138
+
139
+ /** GET /im/message/getLatestMsgListForAI 响应 */
140
+ export interface GetLatestMsgListResponse {
141
+ data?: WsMessageParams[];
142
+ resultCode?: number;
143
+ message?: string;
144
+ }
145
+
146
+ // ─── OpenClaw 插件 SDK 类型别名 ───────────────────────────────────────────────
147
+
148
+ export type ChannelLogSink = SDKChannelLogSink;
149
+ export type ChannelAccountSnapshot = SDKChannelAccountSnapshot;
150
+
151
+ export interface ResolvedAccount {
152
+ accountId: string;
153
+ config: XgImConfig;
154
+ enabled: boolean;
155
+ configured: boolean;
156
+ name?: string | null;
157
+ }
158
+
159
+ export type GatewayStartContext = SDKChannelGatewayContext<ResolvedAccount>;
160
+ export type XgImChannelPlugin = SDKChannelPlugin<ResolvedAccount & { configured: boolean }>;
161
+
162
+ /** PluginRuntime 的类型(内部 API 丰富,用 any 表示) */
163
+ export type { PluginRuntime };