@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.
- package/dist/index.d.ts +16 -0
- package/dist/index.js +21 -0
- package/dist/src/bot.d.ts +17 -0
- package/dist/src/bot.js +228 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +83 -0
- package/dist/src/client.d.ts +20 -0
- package/dist/src/client.js +52 -0
- package/dist/src/config-schema.d.ts +54 -0
- package/dist/src/config-schema.js +55 -0
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +45 -0
- package/dist/src/file-download.d.ts +17 -0
- package/dist/src/file-download.js +53 -0
- package/dist/src/file-upload.d.ts +23 -0
- package/dist/src/file-upload.js +129 -0
- package/dist/src/formatter.d.ts +77 -0
- package/dist/src/formatter.js +215 -0
- package/dist/src/heartbeat.d.ts +38 -0
- package/dist/src/heartbeat.js +94 -0
- package/dist/src/monitor.d.ts +12 -0
- package/dist/src/monitor.js +119 -0
- package/dist/src/onboarding.d.ts +6 -0
- package/dist/src/onboarding.js +173 -0
- package/dist/src/outbound.d.ts +6 -0
- package/dist/src/outbound.js +143 -0
- package/dist/src/parser.d.ts +44 -0
- package/dist/src/parser.js +84 -0
- package/dist/src/push.d.ts +17 -0
- package/dist/src/push.js +55 -0
- package/dist/src/reply-dispatcher.d.ts +15 -0
- package/dist/src/reply-dispatcher.js +162 -0
- package/dist/src/runtime.d.ts +11 -0
- package/dist/src/runtime.js +18 -0
- package/dist/src/tools/location-tool.d.ts +5 -0
- package/dist/src/tools/location-tool.js +97 -0
- package/dist/src/tools/session-manager.d.ts +29 -0
- package/dist/src/tools/session-manager.js +35 -0
- package/dist/src/tools/tool-context.d.ts +16 -0
- package/dist/src/tools/tool-context.js +7 -0
- package/dist/src/types.d.ts +171 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/crypto.d.ts +8 -0
- package/dist/src/utils/crypto.js +14 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +34 -0
- package/dist/src/utils/session.d.ts +34 -0
- package/dist/src/utils/session.js +50 -0
- package/dist/src/websocket.d.ts +83 -0
- package/dist/src/websocket.js +366 -0
- package/openclaw.plugin.json +10 -0
- 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,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
|
+
}
|
package/dist/src/push.js
ADDED
|
@@ -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;
|