@ynhcj/xiaoyi-channel 0.0.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 (52) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.js +21 -0
  3. package/dist/src/bot.d.ts +17 -0
  4. package/dist/src/bot.js +228 -0
  5. package/dist/src/channel.d.ts +6 -0
  6. package/dist/src/channel.js +83 -0
  7. package/dist/src/client.d.ts +20 -0
  8. package/dist/src/client.js +52 -0
  9. package/dist/src/config-schema.d.ts +54 -0
  10. package/dist/src/config-schema.js +55 -0
  11. package/dist/src/config.d.ts +17 -0
  12. package/dist/src/config.js +45 -0
  13. package/dist/src/file-download.d.ts +17 -0
  14. package/dist/src/file-download.js +53 -0
  15. package/dist/src/file-upload.d.ts +23 -0
  16. package/dist/src/file-upload.js +129 -0
  17. package/dist/src/formatter.d.ts +77 -0
  18. package/dist/src/formatter.js +215 -0
  19. package/dist/src/heartbeat.d.ts +38 -0
  20. package/dist/src/heartbeat.js +94 -0
  21. package/dist/src/monitor.d.ts +12 -0
  22. package/dist/src/monitor.js +119 -0
  23. package/dist/src/onboarding.d.ts +6 -0
  24. package/dist/src/onboarding.js +173 -0
  25. package/dist/src/outbound.d.ts +6 -0
  26. package/dist/src/outbound.js +143 -0
  27. package/dist/src/parser.d.ts +44 -0
  28. package/dist/src/parser.js +84 -0
  29. package/dist/src/push.d.ts +17 -0
  30. package/dist/src/push.js +55 -0
  31. package/dist/src/reply-dispatcher.d.ts +15 -0
  32. package/dist/src/reply-dispatcher.js +162 -0
  33. package/dist/src/runtime.d.ts +11 -0
  34. package/dist/src/runtime.js +18 -0
  35. package/dist/src/tools/location-tool.d.ts +5 -0
  36. package/dist/src/tools/location-tool.js +97 -0
  37. package/dist/src/tools/session-manager.d.ts +29 -0
  38. package/dist/src/tools/session-manager.js +35 -0
  39. package/dist/src/tools/tool-context.d.ts +16 -0
  40. package/dist/src/tools/tool-context.js +7 -0
  41. package/dist/src/types.d.ts +171 -0
  42. package/dist/src/types.js +2 -0
  43. package/dist/src/utils/crypto.d.ts +8 -0
  44. package/dist/src/utils/crypto.js +14 -0
  45. package/dist/src/utils/logger.d.ts +6 -0
  46. package/dist/src/utils/logger.js +34 -0
  47. package/dist/src/utils/session.d.ts +34 -0
  48. package/dist/src/utils/session.js +50 -0
  49. package/dist/src/websocket.d.ts +83 -0
  50. package/dist/src/websocket.js +366 -0
  51. package/openclaw.plugin.json +10 -0
  52. package/package.json +66 -0
@@ -0,0 +1,119 @@
1
+ import { resolveXYConfig } from "./config.js";
2
+ import { getXYWebSocketManager } from "./client.js";
3
+ import { getXYRuntime } from "./runtime.js";
4
+ import { handleXYMessage } from "./bot.js";
5
+ /**
6
+ * Per-session serial queue that ensures messages from the same session are processed
7
+ * in arrival order while allowing different sessions to run concurrently.
8
+ * Following feishu/monitor.account.ts pattern.
9
+ */
10
+ function createSessionQueue() {
11
+ const queues = new Map();
12
+ return (sessionId, task) => {
13
+ const prev = queues.get(sessionId) ?? Promise.resolve();
14
+ const next = prev.then(task, task);
15
+ queues.set(sessionId, next);
16
+ void next.finally(() => {
17
+ if (queues.get(sessionId) === next) {
18
+ queues.delete(sessionId);
19
+ }
20
+ });
21
+ return next;
22
+ };
23
+ }
24
+ /**
25
+ * Monitor XY channel WebSocket connections.
26
+ * Keeps the connection alive until abortSignal is triggered.
27
+ */
28
+ export async function monitorXYProvider(opts = {}) {
29
+ const cfg = opts.config;
30
+ if (!cfg) {
31
+ throw new Error("Config is required for XY monitor");
32
+ }
33
+ // Validate runtime early - fail fast if plugin not registered
34
+ // Following feishu/monitor.account.ts pattern
35
+ const core = getXYRuntime();
36
+ const runtime = opts.runtime;
37
+ const log = runtime?.log ?? console.log;
38
+ const error = runtime?.error ?? console.error;
39
+ const account = resolveXYConfig(cfg);
40
+ if (!account.enabled) {
41
+ throw new Error(`XY account is disabled`);
42
+ }
43
+ const accountId = opts.accountId ?? "default";
44
+ // Get WebSocket manager (cached)
45
+ const wsManager = getXYWebSocketManager(account);
46
+ // Track logged servers to avoid duplicate logs
47
+ const loggedServers = new Set();
48
+ // Create session queue for ordered message processing
49
+ const enqueue = createSessionQueue();
50
+ return new Promise((resolve, reject) => {
51
+ const cleanup = () => {
52
+ log("XY gateway: cleaning up...");
53
+ wsManager.disconnect();
54
+ loggedServers.clear();
55
+ };
56
+ const handleAbort = () => {
57
+ log("XY gateway: abort signal received, stopping");
58
+ cleanup();
59
+ log("XY gateway stopped");
60
+ resolve();
61
+ };
62
+ if (opts.abortSignal?.aborted) {
63
+ cleanup();
64
+ resolve();
65
+ return;
66
+ }
67
+ opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
68
+ // Setup event handlers
69
+ const messageHandler = (message, sessionId, serverId) => {
70
+ const task = async () => {
71
+ try {
72
+ await handleXYMessage({
73
+ cfg,
74
+ runtime,
75
+ message,
76
+ accountId, // ✅ Pass accountId ("default")
77
+ });
78
+ }
79
+ catch (err) {
80
+ error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
81
+ throw err;
82
+ }
83
+ };
84
+ void enqueue(sessionId, task).catch((err) => {
85
+ // Error already logged in task, this is for queue failures
86
+ error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
87
+ });
88
+ };
89
+ const connectedHandler = (serverId) => {
90
+ if (!loggedServers.has(serverId)) {
91
+ log(`XY gateway: ${serverId} connected`);
92
+ loggedServers.add(serverId);
93
+ }
94
+ };
95
+ const disconnectedHandler = (serverId) => {
96
+ console.warn(`XY gateway: ${serverId} disconnected`);
97
+ loggedServers.delete(serverId);
98
+ };
99
+ const errorHandler = (err, serverId) => {
100
+ error(`XY gateway: ${serverId} error: ${String(err)}`);
101
+ };
102
+ // Register event handlers
103
+ wsManager.on("message", messageHandler);
104
+ wsManager.on("connected", connectedHandler);
105
+ wsManager.on("disconnected", disconnectedHandler);
106
+ wsManager.on("error", errorHandler);
107
+ // Connect to WebSocket servers
108
+ wsManager.connect()
109
+ .then(() => {
110
+ log("XY gateway: started successfully");
111
+ })
112
+ .catch((err) => {
113
+ // Connection failed but don't reject - continue monitoring for reconnection
114
+ error(`XY gateway: initial connection failed: ${String(err)}`);
115
+ // Still resolve successfully so plugin starts
116
+ resolve();
117
+ });
118
+ });
119
+ }
@@ -0,0 +1,6 @@
1
+ import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
2
+ /**
3
+ * XY Channel Onboarding Adapter
4
+ * Implements the ChannelOnboardingAdapter interface for OpenClaw's onboarding system
5
+ */
6
+ export declare const xyOnboardingAdapter: ChannelOnboardingAdapter;
@@ -0,0 +1,173 @@
1
+ const channel = "xy";
2
+ /**
3
+ * Check if XY channel is properly configured with required fields
4
+ */
5
+ function isXYConfigured(cfg) {
6
+ try {
7
+ const xyConfig = cfg.channels?.xy;
8
+ if (!xyConfig) {
9
+ return false;
10
+ }
11
+ // Check required fields
12
+ const requiredFields = ["apiKey", "agentId", "uid", "apiId", "pushId"];
13
+ for (const field of requiredFields) {
14
+ if (!xyConfig[field] || (typeof xyConfig[field] === "string" && !xyConfig[field].trim())) {
15
+ return false;
16
+ }
17
+ }
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ /**
25
+ * Get current status of XY channel configuration
26
+ */
27
+ async function getStatus({ cfg }) {
28
+ const configured = isXYConfigured(cfg);
29
+ const xyConfig = cfg.channels?.xy;
30
+ const statusLines = [];
31
+ if (configured) {
32
+ const wsUrl1 = xyConfig?.wsUrl1 || "ws://localhost:8765/ws/link";
33
+ const wsUrl2 = xyConfig?.wsUrl2 || "ws://localhost:8768/ws/link";
34
+ statusLines.push(`XY: configured (双 WebSocket: ${wsUrl1}, ${wsUrl2})`);
35
+ }
36
+ else {
37
+ statusLines.push("XY: 需要配置 (需要 apiKey, agentId, uid, apiId, pushId)");
38
+ }
39
+ return {
40
+ channel,
41
+ configured,
42
+ statusLines,
43
+ selectionHint: configured ? "configured" : "需要配置",
44
+ quickstartScore: configured ? 5 : 0,
45
+ };
46
+ }
47
+ /**
48
+ * Configure XY channel through interactive prompts
49
+ */
50
+ async function configure({ cfg, prompter, }) {
51
+ // Note current configuration status
52
+ const currentConfig = cfg.channels?.xy;
53
+ const isUpdate = Boolean(currentConfig);
54
+ await prompter.note([
55
+ "XY Channel - 小艺 A2A 协议配置",
56
+ "",
57
+ "XY 是小艺智能助手的 A2A (Agent-to-Agent) 协议集成,",
58
+ "需要配置双 WebSocket 连接和相关认证信息。",
59
+ "",
60
+ isUpdate ? "当前配置将被更新。" : "首次配置 XY channel。",
61
+ ].join("\n"), "XY Channel 配置");
62
+ // Prompt for WebSocket URLs
63
+ const wsUrl1 = await prompter.text({
64
+ message: "WebSocket URL 1 (主连接)",
65
+ initialValue: currentConfig?.wsUrl1 || "ws://localhost:8765/ws/link",
66
+ placeholder: "ws://localhost:8765/ws/link",
67
+ });
68
+ const wsUrl2 = await prompter.text({
69
+ message: "WebSocket URL 2 (辅助连接)",
70
+ initialValue: currentConfig?.wsUrl2 || "ws://localhost:8768/ws/link",
71
+ placeholder: "ws://localhost:8768/ws/link",
72
+ });
73
+ // Prompt for required authentication fields
74
+ const apiKey = await prompter.text({
75
+ message: "API Key (必需)",
76
+ initialValue: currentConfig?.apiKey || "",
77
+ placeholder: "输入小艺 API Key",
78
+ validate: (value) => (value.trim() ? undefined : "API Key 不能为空"),
79
+ });
80
+ const uid = await prompter.text({
81
+ message: "UID - 用户ID (必需)",
82
+ initialValue: currentConfig?.uid || "",
83
+ placeholder: "输入用户 ID",
84
+ validate: (value) => (value.trim() ? undefined : "UID 不能为空"),
85
+ });
86
+ const agentId = await prompter.text({
87
+ message: "Agent ID - 智能体ID (必需)",
88
+ initialValue: currentConfig?.agentId || "",
89
+ placeholder: "agent5336cca603f941ee9b112f711805e866",
90
+ validate: (value) => (value.trim() ? undefined : "Agent ID 不能为空"),
91
+ });
92
+ const apiId = await prompter.text({
93
+ message: "API ID (必需)",
94
+ initialValue: currentConfig?.apiId || "",
95
+ placeholder: "输入 API ID",
96
+ validate: (value) => (value.trim() ? undefined : "API ID 不能为空"),
97
+ });
98
+ const pushId = await prompter.text({
99
+ message: "Push ID (必需)",
100
+ initialValue: currentConfig?.pushId || "",
101
+ placeholder: "输入 Push ID",
102
+ validate: (value) => (value.trim() ? undefined : "Push ID 不能为空"),
103
+ });
104
+ // Optional fields
105
+ const fileUploadUrl = await prompter.text({
106
+ message: "File Upload URL (文件上传服务)",
107
+ initialValue: currentConfig?.fileUploadUrl || "http://localhost:8767",
108
+ placeholder: "http://localhost:8767",
109
+ });
110
+ const pushUrl = await prompter.text({
111
+ message: "Push URL (推送服务,可选)",
112
+ initialValue: currentConfig?.pushUrl || "",
113
+ placeholder: "留空使用默认值",
114
+ });
115
+ // Update configuration
116
+ const updatedConfig = {
117
+ ...cfg,
118
+ channels: {
119
+ ...cfg.channels,
120
+ xy: {
121
+ enabled: true,
122
+ wsUrl1: wsUrl1.trim(),
123
+ wsUrl2: wsUrl2.trim(),
124
+ apiKey: apiKey.trim(),
125
+ uid: uid.trim(),
126
+ agentId: agentId.trim(),
127
+ apiId: apiId.trim(),
128
+ pushId: pushId.trim(),
129
+ fileUploadUrl: fileUploadUrl.trim(),
130
+ ...(pushUrl?.trim() ? { pushUrl: pushUrl.trim() } : {}),
131
+ },
132
+ },
133
+ };
134
+ // Show confirmation
135
+ await prompter.note([
136
+ "✅ XY Channel 配置完成",
137
+ "",
138
+ `主连接: ${wsUrl1}`,
139
+ `辅助连接: ${wsUrl2}`,
140
+ `Agent ID: ${agentId}`,
141
+ `UID: ${uid}`,
142
+ "",
143
+ "运行以下命令启动 gateway:",
144
+ " openclaw gateway restart",
145
+ "",
146
+ "查看日志:",
147
+ " openclaw logs --follow",
148
+ ].join("\n"), "配置成功");
149
+ return {
150
+ cfg: updatedConfig,
151
+ accountId: "default",
152
+ };
153
+ }
154
+ /**
155
+ * XY Channel Onboarding Adapter
156
+ * Implements the ChannelOnboardingAdapter interface for OpenClaw's onboarding system
157
+ */
158
+ export const xyOnboardingAdapter = {
159
+ channel,
160
+ getStatus,
161
+ configure,
162
+ // Optional: disable the channel
163
+ disable: (cfg) => ({
164
+ ...cfg,
165
+ channels: {
166
+ ...cfg.channels,
167
+ xy: {
168
+ ...(cfg.channels?.xy || {}),
169
+ enabled: false,
170
+ },
171
+ },
172
+ }),
173
+ };
@@ -0,0 +1,6 @@
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
2
+ /**
3
+ * Outbound adapter for sending messages from OpenClaw to XY.
4
+ * Uses Push service for direct message delivery.
5
+ */
6
+ export declare const xyOutbound: ChannelOutboundAdapter;
@@ -0,0 +1,143 @@
1
+ import { resolveXYConfig } from "./config.js";
2
+ import { XYFileUploadService } from "./file-upload.js";
3
+ import { XYPushService } from "./push.js";
4
+ // Special marker for default push delivery when no target is specified
5
+ const DEFAULT_PUSH_MARKER = "default";
6
+ /**
7
+ * Outbound adapter for sending messages from OpenClaw to XY.
8
+ * Uses Push service for direct message delivery.
9
+ */
10
+ export const xyOutbound = {
11
+ deliveryMode: "direct",
12
+ textChunkLimit: 4000,
13
+ /**
14
+ * Resolve delivery target for XY channel.
15
+ * When no target is specified (e.g., in cron jobs with announce mode),
16
+ * returns a default marker that will be handled by sendText.
17
+ */
18
+ resolveTarget: ({ cfg, to, accountId, mode }) => {
19
+ // If no target provided, use default marker for push delivery
20
+ if (!to || to.trim() === "") {
21
+ console.log(`[xyOutbound.resolveTarget] No target specified, using default push marker`);
22
+ return {
23
+ ok: true,
24
+ to: DEFAULT_PUSH_MARKER,
25
+ };
26
+ }
27
+ // Otherwise, use the provided target
28
+ console.log(`[xyOutbound.resolveTarget] Using provided target:`, to);
29
+ return {
30
+ ok: true,
31
+ to: to.trim(),
32
+ };
33
+ },
34
+ sendText: async ({ cfg, to, text, accountId }) => {
35
+ // Log parameters
36
+ console.log(`[xyOutbound.sendText] Called with:`, {
37
+ to,
38
+ accountId,
39
+ textLength: text?.length || 0,
40
+ textPreview: text?.slice(0, 100),
41
+ });
42
+ // Resolve configuration
43
+ const config = resolveXYConfig(cfg);
44
+ // Handle default push marker (for cron jobs without explicit target)
45
+ let actualTo = to;
46
+ if (to === DEFAULT_PUSH_MARKER) {
47
+ console.log(`[xyOutbound.sendText] Using default push delivery (no specific target)`);
48
+ // For push notifications, we don't need a specific target
49
+ // The push service will handle it based on config
50
+ actualTo = config.defaultSessionId || "";
51
+ }
52
+ // Create push service
53
+ const pushService = new XYPushService(config);
54
+ // Extract title (first 57 chars or first line)
55
+ const title = text.split("\n")[0].slice(0, 57);
56
+ // Send push message
57
+ await pushService.sendPush(text, title, actualTo);
58
+ console.log(`[xyOutbound.sendText] Completed successfully`);
59
+ // Return message info
60
+ return {
61
+ channel: "xy",
62
+ messageId: Date.now().toString(),
63
+ chatId: actualTo,
64
+ };
65
+ },
66
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
67
+ // Log parameters
68
+ console.log(`[xyOutbound.sendMedia] Called with:`, {
69
+ to,
70
+ accountId,
71
+ text,
72
+ mediaUrl,
73
+ mediaLocalRoots,
74
+ });
75
+ // Parse to: "sessionId::taskId"
76
+ const parts = to.split("::");
77
+ if (parts.length !== 2) {
78
+ throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
79
+ }
80
+ const [sessionId, taskId] = parts;
81
+ // Resolve configuration
82
+ const config = resolveXYConfig(cfg);
83
+ // Create upload service
84
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
85
+ // Validate mediaUrl
86
+ if (!mediaUrl) {
87
+ throw new Error("mediaUrl is required for sendMedia");
88
+ }
89
+ // Upload file
90
+ const fileId = await uploadService.uploadFile(mediaUrl);
91
+ console.log(`[xyOutbound.sendMedia] File uploaded:`, {
92
+ fileId,
93
+ sessionId,
94
+ taskId,
95
+ });
96
+ // Get filename and mime type from mediaUrl
97
+ // mediaUrl may be a local file path or URL
98
+ const fileName = mediaUrl.split("/").pop() || "unknown";
99
+ const mimeType = text?.match(/\[ MediaType: ([^\]]+)\]/)?.[1] || "application/octet-stream";
100
+ // Build agent_response message
101
+ const agentResponse = {
102
+ msgType: "agent_response",
103
+ agentId: config.agentId,
104
+ sessionId: sessionId,
105
+ taskId: taskId,
106
+ msgDetail: JSON.stringify({
107
+ jsonrpc: "2.0",
108
+ id: taskId,
109
+ result: {
110
+ kind: "artifact-update",
111
+ append: true,
112
+ lastChunk: false,
113
+ final: false,
114
+ artifact: {
115
+ artifactId: taskId,
116
+ parts: [
117
+ {
118
+ kind: "file",
119
+ file: {
120
+ name: fileName,
121
+ mimeType: mimeType,
122
+ fileId: fileId,
123
+ },
124
+ },
125
+ ],
126
+ },
127
+ },
128
+ error: { code: 0 },
129
+ }),
130
+ };
131
+ // Get WebSocket manager and send message
132
+ const { getXYWebSocketManager } = await import("./client.js");
133
+ const wsManager = getXYWebSocketManager(config);
134
+ await wsManager.sendMessage(sessionId, agentResponse);
135
+ console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
136
+ // Return message info
137
+ return {
138
+ channel: "xy",
139
+ messageId: fileId,
140
+ chatId: to,
141
+ };
142
+ },
143
+ };
@@ -0,0 +1,44 @@
1
+ import type { A2AJsonRpcRequest, A2AMessagePart, A2ADataEvent } from "./types.js";
2
+ /**
3
+ * Parsed message information extracted from A2A request.
4
+ * Note: agentId is not extracted from message - it should come from config.
5
+ */
6
+ export interface ParsedA2AMessage {
7
+ sessionId: string;
8
+ taskId: string;
9
+ messageId: string;
10
+ parts: A2AMessagePart[];
11
+ method: string;
12
+ }
13
+ /**
14
+ * Parse an A2A JSON-RPC request into structured message data.
15
+ */
16
+ export declare function parseA2AMessage(request: A2AJsonRpcRequest): ParsedA2AMessage;
17
+ /**
18
+ * Extract text content from message parts.
19
+ */
20
+ export declare function extractTextFromParts(parts: A2AMessagePart[]): string;
21
+ /**
22
+ * Extract file parts from message parts.
23
+ */
24
+ export declare function extractFileParts(parts: A2AMessagePart[]): Array<{
25
+ name: string;
26
+ mimeType: string;
27
+ uri: string;
28
+ }>;
29
+ /**
30
+ * Extract data events from message parts (for tool responses).
31
+ */
32
+ export declare function extractDataEvents(parts: A2AMessagePart[]): A2ADataEvent[];
33
+ /**
34
+ * Check if message is a clearContext request.
35
+ */
36
+ export declare function isClearContextMessage(method: string): boolean;
37
+ /**
38
+ * Check if message is a tasks/cancel request.
39
+ */
40
+ export declare function isTasksCancelMessage(method: string): boolean;
41
+ /**
42
+ * Validate A2A request structure.
43
+ */
44
+ export declare function validateA2ARequest(request: any): request is A2AJsonRpcRequest;
@@ -0,0 +1,84 @@
1
+ import { logger } from "./utils/logger.js";
2
+ /**
3
+ * Parse an A2A JSON-RPC request into structured message data.
4
+ */
5
+ export function parseA2AMessage(request) {
6
+ const { method, params, id } = request;
7
+ if (!params) {
8
+ throw new Error("A2A request missing params");
9
+ }
10
+ const { sessionId, message, id: paramsId } = params;
11
+ if (!sessionId || !message) {
12
+ throw new Error("A2A request params missing required fields");
13
+ }
14
+ return {
15
+ sessionId,
16
+ taskId: paramsId, // Task ID from params (对话唯一标识)
17
+ messageId: id, // Global unique message sequence ID from top-level request
18
+ parts: message.parts || [],
19
+ method,
20
+ };
21
+ }
22
+ /**
23
+ * Extract text content from message parts.
24
+ */
25
+ export function extractTextFromParts(parts) {
26
+ const textParts = parts
27
+ .filter((part) => part.kind === "text")
28
+ .map((part) => part.text);
29
+ return textParts.join("\n").trim();
30
+ }
31
+ /**
32
+ * Extract file parts from message parts.
33
+ */
34
+ export function extractFileParts(parts) {
35
+ return parts
36
+ .filter((part) => part.kind === "file")
37
+ .map((part) => part.file);
38
+ }
39
+ /**
40
+ * Extract data events from message parts (for tool responses).
41
+ */
42
+ export function extractDataEvents(parts) {
43
+ return parts
44
+ .filter((part) => part.kind === "data")
45
+ .map((part) => part.data.event)
46
+ .filter((event) => event !== undefined);
47
+ }
48
+ /**
49
+ * Check if message is a clearContext request.
50
+ */
51
+ export function isClearContextMessage(method) {
52
+ return method === "clearContext" || method === "clear_context";
53
+ }
54
+ /**
55
+ * Check if message is a tasks/cancel request.
56
+ */
57
+ export function isTasksCancelMessage(method) {
58
+ return method === "tasks/cancel" || method === "tasks_cancel";
59
+ }
60
+ /**
61
+ * Validate A2A request structure.
62
+ */
63
+ export function validateA2ARequest(request) {
64
+ if (!request || typeof request !== "object") {
65
+ return false;
66
+ }
67
+ if (request.jsonrpc !== "2.0") {
68
+ logger.warn("Invalid JSON-RPC version:", request.jsonrpc);
69
+ return false;
70
+ }
71
+ if (!request.method || typeof request.method !== "string") {
72
+ logger.warn("Missing or invalid method");
73
+ return false;
74
+ }
75
+ if (!request.id) {
76
+ logger.warn("Missing request id");
77
+ return false;
78
+ }
79
+ if (!request.params || typeof request.params !== "object") {
80
+ logger.warn("Missing or invalid params");
81
+ return false;
82
+ }
83
+ return true;
84
+ }
@@ -0,0 +1,17 @@
1
+ import type { XYChannelConfig } from "./types.js";
2
+ /**
3
+ * Service for sending push messages to users.
4
+ * Used for outbound messages and scheduled tasks.
5
+ */
6
+ export declare class XYPushService {
7
+ private config;
8
+ constructor(config: XYChannelConfig);
9
+ /**
10
+ * Send a push message to a user session.
11
+ */
12
+ sendPush(content: string, title: string, sessionId?: string): Promise<void>;
13
+ /**
14
+ * Send a push message with file attachments.
15
+ */
16
+ sendPushWithFiles(content: string, title: string, fileIds: string[], sessionId?: string): Promise<void>;
17
+ }
@@ -0,0 +1,55 @@
1
+ // Push message service for scheduled tasks
2
+ import fetch from "node-fetch";
3
+ import { logger } from "./utils/logger.js";
4
+ /**
5
+ * Service for sending push messages to users.
6
+ * Used for outbound messages and scheduled tasks.
7
+ */
8
+ export class XYPushService {
9
+ config;
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ /**
14
+ * Send a push message to a user session.
15
+ */
16
+ async sendPush(content, title, sessionId) {
17
+ const pushUrl = this.config.pushUrl || `${this.config.fileUploadUrl}/push`;
18
+ logger.debug(`Sending push message: title="${title}"`);
19
+ try {
20
+ const request = {
21
+ apiKey: this.config.apiKey,
22
+ apiId: this.config.apiId,
23
+ pushId: this.config.pushId,
24
+ sessionId: sessionId || "default",
25
+ title,
26
+ content,
27
+ };
28
+ const response = await fetch(pushUrl, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ "x-api-key": this.config.apiKey,
33
+ "x-request-from": "openclaw",
34
+ },
35
+ body: JSON.stringify(request),
36
+ });
37
+ if (!response.ok) {
38
+ throw new Error(`Push failed: HTTP ${response.status}`);
39
+ }
40
+ logger.log(`Push message sent successfully: "${title}"`);
41
+ }
42
+ catch (error) {
43
+ logger.error("Failed to send push message:", error);
44
+ throw error;
45
+ }
46
+ }
47
+ /**
48
+ * Send a push message with file attachments.
49
+ */
50
+ async sendPushWithFiles(content, title, fileIds, sessionId) {
51
+ // Build content with file references
52
+ const contentWithFiles = `${content}\n\n[文件: ${fileIds.join(", ")}]`;
53
+ await this.sendPush(contentWithFiles, title, sessionId);
54
+ }
55
+ }
@@ -0,0 +1,15 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ export interface CreateXYReplyDispatcherParams {
3
+ cfg: ClawdbotConfig;
4
+ runtime: RuntimeEnv;
5
+ sessionId: string;
6
+ taskId: string;
7
+ messageId: string;
8
+ accountId: string;
9
+ }
10
+ /**
11
+ * Create a reply dispatcher for XY channel messages.
12
+ * Follows feishu pattern with status updates and streaming support.
13
+ * Runtime is expected to be validated before calling this function.
14
+ */
15
+ export declare function createXYReplyDispatcher(params: CreateXYReplyDispatcherParams): any;