@stagewhisper/stagewhisper 0.37.0 → 0.39.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +21 -15
- package/src/service.ts +152 -64
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
DEFAULT_ACCOUNT_ID,
|
|
5
5
|
} from "openclaw/plugin-sdk/core";
|
|
6
6
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
7
|
-
import { StageWhisperClient } from "./client.js";
|
|
8
7
|
|
|
9
8
|
export type StageWhisperAccount = {
|
|
10
9
|
accountId: string | null;
|
|
@@ -147,24 +146,31 @@ export const stagewhisperPlugin = createChatChannelPlugin<StageWhisperAccount>(
|
|
|
147
146
|
threading: { topLevelReplyToMode: "reply" },
|
|
148
147
|
|
|
149
148
|
outbound: {
|
|
150
|
-
base: {
|
|
149
|
+
base: {
|
|
150
|
+
deliveryMode: "direct",
|
|
151
|
+
resolveTarget: (params: {
|
|
152
|
+
cfg?: OpenClawConfig;
|
|
153
|
+
to?: string;
|
|
154
|
+
allowFrom?: string[];
|
|
155
|
+
accountId?: string | null;
|
|
156
|
+
mode?: string;
|
|
157
|
+
}) => {
|
|
158
|
+
if (!params.to) return { ok: false as const, error: new Error("No delivery target") };
|
|
159
|
+
return { ok: true as const, to: params.to };
|
|
160
|
+
},
|
|
161
|
+
},
|
|
151
162
|
attachedResults: {
|
|
152
163
|
channel: "stagewhisper",
|
|
153
164
|
sendText: async (ctx) => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
account.integrationId,
|
|
158
|
-
account.relayToken,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const taskId = ctx.threadId as string | undefined;
|
|
162
|
-
if (!taskId) {
|
|
163
|
-
return { messageId: `sw-noop-${Date.now()}`, ok: true };
|
|
165
|
+
const target = (ctx as Record<string, unknown>).to as string | undefined ?? "";
|
|
166
|
+
if (target.startsWith("sw-session-")) {
|
|
167
|
+
return { messageId: `sw-relay-ack-${Date.now()}`, ok: true };
|
|
164
168
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
console.warn(
|
|
170
|
+
`[stagewhisper] sendText called for unrecognised target "${target}"; ` +
|
|
171
|
+
`StageWhisper channel is inbound-only — task replies are routed by the relay service`,
|
|
172
|
+
);
|
|
173
|
+
return { messageId: `sw-dropped-${Date.now()}`, ok: false };
|
|
168
174
|
},
|
|
169
175
|
},
|
|
170
176
|
},
|
package/src/service.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function isTestTask(task: TaskPayload): boolean {
|
|
81
|
-
return task.action_type === "
|
|
81
|
+
return task.action_type === "connectivity_test";
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async function updateStatus(
|
|
@@ -90,6 +90,27 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
90
90
|
await client.updateTaskStatus(task.id, status);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function extractContentFromMessage(
|
|
94
|
+
msg: Record<string, unknown>,
|
|
95
|
+
): string | null {
|
|
96
|
+
const content = msg["content"];
|
|
97
|
+
if (typeof content === "string") return content;
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(content)) {
|
|
100
|
+
for (const part of content) {
|
|
101
|
+
if (
|
|
102
|
+
typeof part === "object" &&
|
|
103
|
+
part !== null &&
|
|
104
|
+
(part as Record<string, unknown>)["type"] === "text" &&
|
|
105
|
+
typeof (part as Record<string, unknown>)["text"] === "string"
|
|
106
|
+
) {
|
|
107
|
+
return (part as Record<string, unknown>)["text"] as string;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
function extractAssistantReply(
|
|
94
115
|
messages: unknown[],
|
|
95
116
|
): string | null {
|
|
@@ -98,20 +119,39 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
98
119
|
if (!msg) continue;
|
|
99
120
|
const role = msg["role"];
|
|
100
121
|
if (role !== "assistant" && role !== "model") continue;
|
|
122
|
+
return extractContentFromMessage(msg);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
101
126
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
async function extractReplyForTask(
|
|
128
|
+
sessionKey: string,
|
|
129
|
+
taskId: string,
|
|
130
|
+
maxAttempts: number = 3,
|
|
131
|
+
): Promise<string | null> {
|
|
132
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
133
|
+
if (attempt > 0) {
|
|
134
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
135
|
+
}
|
|
136
|
+
const session = await api.runtime.subagent.getSessionMessages({
|
|
137
|
+
sessionKey,
|
|
138
|
+
limit: 50,
|
|
139
|
+
});
|
|
140
|
+
const messages = session.messages as Record<string, unknown>[];
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < messages.length; i++) {
|
|
143
|
+
const msg = messages[i];
|
|
144
|
+
if (msg["role"] !== "user") continue;
|
|
145
|
+
const text = extractContentFromMessage(msg) ?? "";
|
|
146
|
+
if (!text.includes(`StageWhisper task: ${taskId}`)) continue;
|
|
147
|
+
|
|
148
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
149
|
+
const reply = messages[j];
|
|
150
|
+
const role = reply["role"];
|
|
151
|
+
if (role === "assistant" || role === "model") {
|
|
152
|
+
return extractContentFromMessage(reply);
|
|
114
153
|
}
|
|
154
|
+
if (role === "user") break;
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
}
|
|
@@ -136,71 +176,121 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
136
176
|
return null;
|
|
137
177
|
}
|
|
138
178
|
|
|
139
|
-
async function
|
|
179
|
+
async function handleNormalTask(
|
|
140
180
|
task: TaskPayload,
|
|
141
181
|
client: StageWhisperClient,
|
|
142
182
|
): Promise<void> {
|
|
143
|
-
api.logger.info(`Received task: ${task.title} (${task.id})`);
|
|
144
|
-
|
|
145
183
|
try {
|
|
146
184
|
await updateStatus(client, task, "delivered");
|
|
147
185
|
} catch (err) {
|
|
148
|
-
api.logger.warn(`Failed to mark task as delivered: ${err}`);
|
|
186
|
+
api.logger.warn(`Failed to mark task as delivered, skipping to prevent duplicates: ${err}`);
|
|
187
|
+
return;
|
|
149
188
|
}
|
|
150
189
|
|
|
151
190
|
const messageContent = buildTaskMessage(task);
|
|
191
|
+
const peerId = `sw-session-${task.session_id}`;
|
|
192
|
+
const sessionKey = buildAgentSessionKey({
|
|
193
|
+
agentId: "default",
|
|
194
|
+
channel: "stagewhisper",
|
|
195
|
+
peer: { kind: "direct", id: peerId },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await api.runtime.subagent.run({
|
|
199
|
+
sessionKey,
|
|
200
|
+
message: messageContent,
|
|
201
|
+
deliver: true,
|
|
202
|
+
idempotencyKey: randomUUID(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
api.logger.info(
|
|
206
|
+
`Task ${task.id} dispatched to agent session (runId: ${result.runId})`,
|
|
207
|
+
);
|
|
152
208
|
|
|
153
209
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
210
|
+
await updateStatus(client, task, "running");
|
|
211
|
+
} catch (err) {
|
|
212
|
+
api.logger.warn(`Failed to mark task as running: ${err}`);
|
|
213
|
+
}
|
|
159
214
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
215
|
+
api.runtime.subagent
|
|
216
|
+
.waitForRun({ runId: result.runId, timeoutMs: 120_000 })
|
|
217
|
+
.then(async (waitResult) => {
|
|
218
|
+
if (waitResult.status === "ok") {
|
|
219
|
+
const reply = await extractReplyForTask(sessionKey, task.id);
|
|
220
|
+
if (reply) {
|
|
221
|
+
await client.postReply(task.id, reply);
|
|
222
|
+
api.logger.info(`Task ${task.id} completed with reply`);
|
|
223
|
+
} else {
|
|
224
|
+
api.logger.warn(`Task ${task.id} completed but no reply found`);
|
|
225
|
+
}
|
|
226
|
+
await updateStatus(client, task, "completed").catch(() => {});
|
|
227
|
+
} else {
|
|
228
|
+
api.logger.error(
|
|
229
|
+
`Agent run failed for task ${task.id}: ${waitResult.error}`,
|
|
230
|
+
);
|
|
231
|
+
await updateStatus(client, task, "failed").catch(() => {});
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.catch(async (err) => {
|
|
235
|
+
api.logger.error(`Failed to track task ${task.id}: ${err}`);
|
|
236
|
+
await updateStatus(client, task, "failed").catch(() => {});
|
|
165
237
|
});
|
|
238
|
+
}
|
|
166
239
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
240
|
+
async function handleTestTask(
|
|
241
|
+
task: TaskPayload,
|
|
242
|
+
client: StageWhisperClient,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
const messageContent = buildTaskMessage(task);
|
|
245
|
+
const sessionKey = buildAgentSessionKey({
|
|
246
|
+
agentId: "default",
|
|
247
|
+
channel: "stagewhisper",
|
|
248
|
+
peer: { kind: "direct", id: `sw-test-${task.id}` },
|
|
249
|
+
});
|
|
170
250
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
251
|
+
const result = await api.runtime.subagent.run({
|
|
252
|
+
sessionKey,
|
|
253
|
+
message: messageContent,
|
|
254
|
+
deliver: false,
|
|
255
|
+
idempotencyKey: randomUUID(),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
api.logger.info(
|
|
259
|
+
`Test task ${task.id} dispatched (runId: ${result.runId})`,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
263
|
+
runId: result.runId,
|
|
264
|
+
timeoutMs: 120_000,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (waitResult.status === "ok") {
|
|
268
|
+
const reply = await extractReplyWithRetry(sessionKey);
|
|
269
|
+
if (reply) {
|
|
270
|
+
await client.testReply(task.id, reply);
|
|
271
|
+
api.logger.info(`Test task ${task.id} completed with reply`);
|
|
272
|
+
} else {
|
|
273
|
+
api.logger.warn(`Test task ${task.id} completed but no reply found`);
|
|
274
|
+
await client.testReply(task.id, "(no reply extracted)");
|
|
175
275
|
}
|
|
276
|
+
} else {
|
|
277
|
+
api.logger.error(
|
|
278
|
+
`Agent run failed for test task ${task.id}: ${waitResult.error}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
176
282
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
283
|
+
async function handleTask(
|
|
284
|
+
task: TaskPayload,
|
|
285
|
+
client: StageWhisperClient,
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
api.logger.info(`Received task: ${task.title} (${task.id})`);
|
|
181
288
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (isTestTask(task)) {
|
|
186
|
-
await client.testReply(task.id, reply);
|
|
187
|
-
} else {
|
|
188
|
-
await client.postReply(task.id, reply);
|
|
189
|
-
}
|
|
190
|
-
api.logger.info(`Task ${task.id} completed with reply`);
|
|
191
|
-
} else {
|
|
192
|
-
api.logger.warn(`Task ${task.id} completed but no assistant reply found`);
|
|
193
|
-
if (isTestTask(task)) {
|
|
194
|
-
await client.testReply(task.id, "(no reply extracted)");
|
|
195
|
-
} else {
|
|
196
|
-
await updateStatus(client, task, "completed").catch(() => {});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
289
|
+
try {
|
|
290
|
+
if (isTestTask(task)) {
|
|
291
|
+
await handleTestTask(task, client);
|
|
199
292
|
} else {
|
|
200
|
-
|
|
201
|
-
`Agent run failed for task ${task.id}: ${waitResult.error}`,
|
|
202
|
-
);
|
|
203
|
-
await updateStatus(client, task, "failed").catch(() => {});
|
|
293
|
+
await handleNormalTask(task, client);
|
|
204
294
|
}
|
|
205
295
|
} catch (err) {
|
|
206
296
|
api.logger.error(`Failed to process task ${task.id}: ${err}`);
|
|
@@ -256,11 +346,9 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
256
346
|
|
|
257
347
|
try {
|
|
258
348
|
const task = JSON.parse(jsonStr) as TaskPayload;
|
|
259
|
-
handleTask(task, client)
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
} catch (parseErr) {
|
|
263
|
-
api.logger.warn(`Failed to parse stream event: ${parseErr}`);
|
|
349
|
+
await handleTask(task, client);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
api.logger.error(`Error processing task from stream: ${err}`);
|
|
264
352
|
}
|
|
265
353
|
}
|
|
266
354
|
}
|