iopenclawwx 0.0.1 → 0.0.2

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.
@@ -2,7 +2,7 @@
2
2
  "id": "iopenclawwx",
3
3
  "name": "iOpenClaw Relay",
4
4
  "description": "Channel plugin for iOpenClaw relay service",
5
- "version": "0.0.1",
5
+ "version": "0.0.2",
6
6
  "type": "module",
7
7
  "main": "index.ts",
8
8
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iopenclawwx",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "OpenClaw channel plugin for iOpenClaw relay",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/constants.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const PLUGIN_VERSION = "0.0.1";
1
+ export const PLUGIN_VERSION = "0.0.2";
2
2
  export const PLUGIN_ID = "iopenclawwx";
3
3
  export const CHANNEL_ID = "iopenclawwx";
4
4
 
@@ -14,23 +14,118 @@ export interface InjectConfig {
14
14
  apiKey: string;
15
15
  }
16
16
 
17
- function extractReplyText(payload: unknown): string {
17
+ function asRecord(value: unknown): Record<string, unknown> | null {
18
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
19
+ return null;
20
+ }
21
+ return value as Record<string, unknown>;
22
+ }
23
+
24
+ function pushText(bucket: string[], value: unknown): void {
25
+ if (typeof value !== "string") {
26
+ return;
27
+ }
28
+ const text = value.trim();
29
+ if (!text) {
30
+ return;
31
+ }
32
+ bucket.push(text);
33
+ }
34
+
35
+ function extractReplyTextFromAny(payload: unknown, depth = 0): string {
36
+ if (depth > 4) {
37
+ return "";
38
+ }
39
+
18
40
  if (typeof payload === "string") {
19
41
  return payload.trim();
20
42
  }
21
43
 
22
- if (payload && typeof payload === "object") {
23
- const row = payload as Record<string, unknown>;
24
- if (typeof row.text === "string") return row.text.trim();
25
- if (typeof row.content === "string") return row.content.trim();
26
- if (typeof row.message === "string") return row.message.trim();
44
+ if (Array.isArray(payload)) {
45
+ const chunks = payload
46
+ .map((item) => extractReplyTextFromAny(item, depth + 1))
47
+ .filter(Boolean);
48
+ return chunks.join("\n").trim();
27
49
  }
28
50
 
29
- try {
30
- return JSON.stringify(payload);
31
- } catch {
51
+ const row = asRecord(payload);
52
+ if (!row) {
32
53
  return "";
33
54
  }
55
+
56
+ const chunks: string[] = [];
57
+
58
+ // Common text fields used by different providers / dispatchers.
59
+ pushText(chunks, row.text);
60
+ pushText(chunks, row.content);
61
+ pushText(chunks, row.message);
62
+ pushText(chunks, row.output_text);
63
+ pushText(chunks, row.final_text);
64
+ pushText(chunks, row.finalText);
65
+ pushText(chunks, row.answer);
66
+ pushText(chunks, row.deltaText);
67
+
68
+ const listFields = ["content", "parts", "blocks", "messages", "choices", "outputs"];
69
+ for (const field of listFields) {
70
+ const value = row[field];
71
+ if (Array.isArray(value)) {
72
+ for (const item of value) {
73
+ const nestedText = extractReplyTextFromAny(item, depth + 1);
74
+ if (nestedText) {
75
+ chunks.push(nestedText);
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ const objFields = ["message", "delta", "output", "result", "response", "data"];
82
+ for (const field of objFields) {
83
+ const value = row[field];
84
+ const nestedText = extractReplyTextFromAny(value, depth + 1);
85
+ if (nestedText) {
86
+ chunks.push(nestedText);
87
+ }
88
+ }
89
+
90
+ const deduped = Array.from(new Set(chunks.map((item) => item.trim()).filter(Boolean)));
91
+ return deduped.join("\n").trim();
92
+ }
93
+
94
+ function extractReplyText(payload: unknown): string {
95
+ return extractReplyTextFromAny(payload);
96
+ }
97
+
98
+ function mergeStreamBuffer(current: string, next: string): string {
99
+ const a = current.trim();
100
+ const b = next.trim();
101
+ if (!b) {
102
+ return a;
103
+ }
104
+ if (!a) {
105
+ return b;
106
+ }
107
+ if (a === b) {
108
+ return a;
109
+ }
110
+ if (b.includes(a)) {
111
+ return b;
112
+ }
113
+ if (a.includes(b)) {
114
+ return a;
115
+ }
116
+ return `${a}\n${b}`;
117
+ }
118
+
119
+ function pickFinalText(finalText: string, streamText: string): string {
120
+ const a = finalText.trim();
121
+ const b = streamText.trim();
122
+
123
+ if (a && b) {
124
+ if (a.includes(b)) return a;
125
+ if (b.includes(a)) return b;
126
+ return a;
127
+ }
128
+ return a || b;
34
129
  }
35
130
 
36
131
  export async function injectRelayMessage(
@@ -101,7 +196,8 @@ export async function injectRelayMessage(
101
196
  }
102
197
 
103
198
  let replied = false;
104
- let firstReplyId = true;
199
+ let streamBuffer = "";
200
+ const sentFinals = new Set<string>();
105
201
 
106
202
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
107
203
  ctx: msgContext,
@@ -112,22 +208,35 @@ export async function injectRelayMessage(
112
208
  ? String((info as Record<string, unknown>).kind || "")
113
209
  : "";
114
210
 
211
+ const fromPayload = extractReplyText(payload);
212
+ const fromInfo = extractReplyText(info);
213
+ const candidate = fromPayload || fromInfo;
214
+
215
+ if (candidate) {
216
+ streamBuffer = mergeStreamBuffer(streamBuffer, candidate);
217
+ }
218
+
115
219
  if (kind !== "final") {
116
220
  return;
117
221
  }
118
222
 
119
- const text = extractReplyText(payload);
223
+ const text = pickFinalText(candidate, streamBuffer);
120
224
  if (!text) {
225
+ log?.warn?.(`[${config.accountId}] final reply arrived but text is empty (msg=${message.id})`);
121
226
  return;
122
227
  }
123
228
 
229
+ if (sentFinals.has(text)) {
230
+ return;
231
+ }
232
+ sentFinals.add(text);
233
+
124
234
  await relayPushReply({
125
235
  relayBaseUrl: config.relayBaseUrl,
126
236
  apiKey: config.apiKey,
127
- replyToMessageId: firstReplyId ? Number(message.id) : Number(message.id),
237
+ replyToMessageId: Number(message.id),
128
238
  content: text,
129
239
  });
130
- firstReplyId = false;
131
240
  replied = true;
132
241
  },
133
242
  onError: (error: unknown, info: unknown) => {
@@ -140,6 +249,17 @@ export async function injectRelayMessage(
140
249
  });
141
250
 
142
251
  if (!replied) {
252
+ const fallbackText = streamBuffer.trim();
253
+ if (fallbackText) {
254
+ await relayPushReply({
255
+ relayBaseUrl: config.relayBaseUrl,
256
+ apiKey: config.apiKey,
257
+ replyToMessageId: Number(message.id),
258
+ content: fallbackText,
259
+ });
260
+ return;
261
+ }
262
+
143
263
  await relayPushReply({
144
264
  relayBaseUrl: config.relayBaseUrl,
145
265
  apiKey: config.apiKey,