@stagewhisper/stagewhisper 0.57.0 → 0.60.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/dist/index.js +397 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98,6 +98,75 @@ var init_client = __esm({
|
|
|
98
98
|
throw new Error(`Reply failed (${res.status}): ${text}`);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
async postChatReply(userMessageId, content, options) {
|
|
102
|
+
const body = {
|
|
103
|
+
content,
|
|
104
|
+
status: options?.status ?? "completed"
|
|
105
|
+
};
|
|
106
|
+
if (options?.errorCode) body.error_code = options.errorCode;
|
|
107
|
+
if (options?.errorMessage) body.error_message = options.errorMessage;
|
|
108
|
+
if (options?.metadata) body.metadata_ = options.metadata;
|
|
109
|
+
const res = await fetch(
|
|
110
|
+
`${this.baseUrl}/api/v1/openclaw/chat/messages/${userMessageId}/reply`,
|
|
111
|
+
{
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: this.headers(),
|
|
114
|
+
body: JSON.stringify(body)
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const text = await res.text();
|
|
119
|
+
throw new Error(`Chat reply failed (${res.status}): ${text}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async startChatReply(userMessageId) {
|
|
123
|
+
const res = await fetch(
|
|
124
|
+
`${this.baseUrl}/api/v1/openclaw/chat/messages/${userMessageId}/reply/start`,
|
|
125
|
+
{
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: this.headers()
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const text = await res.text();
|
|
132
|
+
throw new Error(`Chat reply start failed (${res.status}): ${text}`);
|
|
133
|
+
}
|
|
134
|
+
return await res.json();
|
|
135
|
+
}
|
|
136
|
+
async postChatReplyDelta(assistantMessageId, delta) {
|
|
137
|
+
const res = await fetch(
|
|
138
|
+
`${this.baseUrl}/api/v1/openclaw/chat/messages/${assistantMessageId}/reply/delta`,
|
|
139
|
+
{
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: this.headers(),
|
|
142
|
+
body: JSON.stringify({ delta })
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const text = await res.text();
|
|
147
|
+
throw new Error(`Chat reply delta failed (${res.status}): ${text}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async completeChatReply(assistantMessageId, options) {
|
|
151
|
+
const body = {
|
|
152
|
+
status: options?.status ?? "completed"
|
|
153
|
+
};
|
|
154
|
+
if (options?.content !== void 0) body.content = options.content;
|
|
155
|
+
if (options?.errorCode) body.error_code = options.errorCode;
|
|
156
|
+
if (options?.errorMessage) body.error_message = options.errorMessage;
|
|
157
|
+
const res = await fetch(
|
|
158
|
+
`${this.baseUrl}/api/v1/openclaw/chat/messages/${assistantMessageId}/reply/complete`,
|
|
159
|
+
{
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: this.headers(),
|
|
162
|
+
body: JSON.stringify(body)
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
const text = await res.text();
|
|
167
|
+
throw new Error(`Chat reply complete failed (${res.status}): ${text}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
101
170
|
async heartbeat(capabilities) {
|
|
102
171
|
const res = await fetch(
|
|
103
172
|
`${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
|
|
@@ -4166,6 +4235,24 @@ function createRelayService(api) {
|
|
|
4166
4235
|
const COMPLETED_JOB_TTL_MS = 5 * 60 * 1e3;
|
|
4167
4236
|
const COMPLETED_JOB_MAX_SIZE = 5e3;
|
|
4168
4237
|
const processingReasoningJobs = /* @__PURE__ */ new Set();
|
|
4238
|
+
const chatSessionQueues = /* @__PURE__ */ new Map();
|
|
4239
|
+
function enqueueChat(sessionId, work) {
|
|
4240
|
+
const prev = chatSessionQueues.get(sessionId) ?? Promise.resolve();
|
|
4241
|
+
const next = prev.catch(() => {
|
|
4242
|
+
}).then(async () => {
|
|
4243
|
+
try {
|
|
4244
|
+
await work();
|
|
4245
|
+
} catch (err) {
|
|
4246
|
+
api.logger.error(`Chat queue work failed: ${err}`);
|
|
4247
|
+
}
|
|
4248
|
+
});
|
|
4249
|
+
chatSessionQueues.set(sessionId, next);
|
|
4250
|
+
void next.then(() => {
|
|
4251
|
+
if (chatSessionQueues.get(sessionId) === next) {
|
|
4252
|
+
chatSessionQueues.delete(sessionId);
|
|
4253
|
+
}
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4169
4256
|
function evictStaleCompletedJobs() {
|
|
4170
4257
|
const cutoff = Date.now() - COMPLETED_JOB_TTL_MS;
|
|
4171
4258
|
for (const [jobId, completedAt] of completedReasoningJobs) {
|
|
@@ -4322,6 +4409,34 @@ function createRelayService(api) {
|
|
|
4322
4409
|
}
|
|
4323
4410
|
return null;
|
|
4324
4411
|
}
|
|
4412
|
+
async function extractReplyForChatMessage(sessionKey, userMessageId, maxAttempts = 3) {
|
|
4413
|
+
const marker = `[StageWhisper chat: ${userMessageId}]`;
|
|
4414
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4415
|
+
if (attempt > 0) {
|
|
4416
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
4417
|
+
}
|
|
4418
|
+
const session = await api.runtime.subagent.getSessionMessages({
|
|
4419
|
+
sessionKey,
|
|
4420
|
+
limit: 100
|
|
4421
|
+
});
|
|
4422
|
+
const messages = session.messages;
|
|
4423
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4424
|
+
const msg = messages[i];
|
|
4425
|
+
if (msg["role"] !== "user") continue;
|
|
4426
|
+
const text = extractContentFromMessage(msg) ?? "";
|
|
4427
|
+
if (!text.includes(marker)) continue;
|
|
4428
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
4429
|
+
const next = messages[j];
|
|
4430
|
+
const role = next["role"];
|
|
4431
|
+
if (role === "assistant" || role === "model") {
|
|
4432
|
+
return extractContentFromMessage(next);
|
|
4433
|
+
}
|
|
4434
|
+
if (role === "user") break;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
return null;
|
|
4439
|
+
}
|
|
4325
4440
|
function decryptTaskFields(task, client) {
|
|
4326
4441
|
const keypair = client.pluginKeypair;
|
|
4327
4442
|
const desktopPub = client.desktopPublicKey;
|
|
@@ -4746,6 +4861,278 @@ function createRelayService(api) {
|
|
|
4746
4861
|
api.logger.error(`Failed to post BYO reasoning result for ${job.job_id}: ${postErr}`);
|
|
4747
4862
|
}
|
|
4748
4863
|
}
|
|
4864
|
+
function looksLikeBYOEnvelope(s) {
|
|
4865
|
+
const trimmed = s.trimStart();
|
|
4866
|
+
if (!trimmed.startsWith("{")) return false;
|
|
4867
|
+
return trimmed.includes('"version"') && trimmed.includes('"sender_role"') && trimmed.includes('"ciphertext"');
|
|
4868
|
+
}
|
|
4869
|
+
function decryptChatPrompt(rawContent, client) {
|
|
4870
|
+
const keypair = client.pluginKeypair;
|
|
4871
|
+
const desktopPub = client.desktopPublicKey;
|
|
4872
|
+
if (!keypair || !desktopPub) {
|
|
4873
|
+
return { error: "missing_byo_keys" };
|
|
4874
|
+
}
|
|
4875
|
+
try {
|
|
4876
|
+
const envelope = JSON.parse(rawContent);
|
|
4877
|
+
const envelopeKey = keypair.deriveEnvelopeKey(desktopPub);
|
|
4878
|
+
const plaintextBytes = open(envelopeKey, envelope);
|
|
4879
|
+
return {
|
|
4880
|
+
plaintext: new TextDecoder().decode(plaintextBytes),
|
|
4881
|
+
envelopeKey
|
|
4882
|
+
};
|
|
4883
|
+
} catch (err) {
|
|
4884
|
+
return { error: `decryption_error: ${err}` };
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
function sealChatReply(envelopeKey, sessionId, userMessageId, replyText) {
|
|
4888
|
+
const envelope = seal(
|
|
4889
|
+
envelopeKey,
|
|
4890
|
+
"plugin",
|
|
4891
|
+
sessionId,
|
|
4892
|
+
`chat-reply:${userMessageId}`,
|
|
4893
|
+
"task_content",
|
|
4894
|
+
new TextEncoder().encode(replyText)
|
|
4895
|
+
);
|
|
4896
|
+
return JSON.stringify(envelope);
|
|
4897
|
+
}
|
|
4898
|
+
async function handleChatMessage(envelope, client) {
|
|
4899
|
+
const userMessageId = envelope.user_message_id;
|
|
4900
|
+
const sessionId = envelope.session_id;
|
|
4901
|
+
const rawContent = envelope.content;
|
|
4902
|
+
if (!userMessageId || !sessionId || !rawContent) {
|
|
4903
|
+
api.logger.warn(
|
|
4904
|
+
`Skipping chat message dispatch \u2014 missing fields (user_message_id=${userMessageId}, session_id=${sessionId})`
|
|
4905
|
+
);
|
|
4906
|
+
return;
|
|
4907
|
+
}
|
|
4908
|
+
api.logger.info(
|
|
4909
|
+
`Received chat message: ${userMessageId} (session: ${sessionId})`
|
|
4910
|
+
);
|
|
4911
|
+
let plaintextContent = rawContent;
|
|
4912
|
+
let envelopeKey = null;
|
|
4913
|
+
if (looksLikeBYOEnvelope(rawContent)) {
|
|
4914
|
+
const decryptResult = decryptChatPrompt(rawContent, client);
|
|
4915
|
+
if ("error" in decryptResult) {
|
|
4916
|
+
api.logger.error(
|
|
4917
|
+
`Chat ${userMessageId} BYO decrypt failed: ${decryptResult.error}`
|
|
4918
|
+
);
|
|
4919
|
+
try {
|
|
4920
|
+
await client.postChatReply(
|
|
4921
|
+
userMessageId,
|
|
4922
|
+
"(BYO decrypt failed)",
|
|
4923
|
+
{
|
|
4924
|
+
status: "errored",
|
|
4925
|
+
errorCode: decryptResult.error.startsWith("missing_") ? "missing_byo_keys" : "decryption_error",
|
|
4926
|
+
errorMessage: decryptResult.error
|
|
4927
|
+
}
|
|
4928
|
+
);
|
|
4929
|
+
} catch (postErr) {
|
|
4930
|
+
api.logger.error(`Failed to report BYO error: ${postErr}`);
|
|
4931
|
+
}
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
plaintextContent = decryptResult.plaintext;
|
|
4935
|
+
envelopeKey = decryptResult.envelopeKey;
|
|
4936
|
+
}
|
|
4937
|
+
const sessionKey = buildAgentSessionKey({
|
|
4938
|
+
agentId: "default",
|
|
4939
|
+
channel: "stagewhisper",
|
|
4940
|
+
peer: { kind: "direct", id: `sw-chat-${sessionId}` }
|
|
4941
|
+
});
|
|
4942
|
+
const finalizeReply = async (replyText, options) => {
|
|
4943
|
+
const outbound = envelopeKey ? sealChatReply(envelopeKey, sessionId, userMessageId, replyText) : replyText;
|
|
4944
|
+
await client.postChatReply(userMessageId, outbound, options);
|
|
4945
|
+
};
|
|
4946
|
+
const decoratedPrompt = `${plaintextContent}
|
|
4947
|
+
|
|
4948
|
+
[StageWhisper chat: ${userMessageId}]`;
|
|
4949
|
+
if (envelopeKey === null) {
|
|
4950
|
+
await handleChatMessageStreaming(
|
|
4951
|
+
client,
|
|
4952
|
+
sessionKey,
|
|
4953
|
+
userMessageId,
|
|
4954
|
+
decoratedPrompt
|
|
4955
|
+
);
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
try {
|
|
4959
|
+
const result = await api.runtime.subagent.run({
|
|
4960
|
+
sessionKey,
|
|
4961
|
+
message: decoratedPrompt,
|
|
4962
|
+
deliver: true,
|
|
4963
|
+
idempotencyKey: `sw-chat-${userMessageId}`
|
|
4964
|
+
});
|
|
4965
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
4966
|
+
runId: result.runId,
|
|
4967
|
+
timeoutMs: 12e4
|
|
4968
|
+
});
|
|
4969
|
+
if (waitResult.status === "ok") {
|
|
4970
|
+
const reply = await extractReplyForChatMessage(sessionKey, userMessageId);
|
|
4971
|
+
if (reply) {
|
|
4972
|
+
await finalizeReply(reply);
|
|
4973
|
+
api.logger.info(`Chat message ${userMessageId} replied`);
|
|
4974
|
+
} else {
|
|
4975
|
+
api.logger.warn(
|
|
4976
|
+
`Chat message ${userMessageId} completed but no reply found`
|
|
4977
|
+
);
|
|
4978
|
+
await finalizeReply("(no reply produced)", {
|
|
4979
|
+
status: "errored",
|
|
4980
|
+
errorCode: "no_reply",
|
|
4981
|
+
errorMessage: "Agent run produced no reply"
|
|
4982
|
+
});
|
|
4983
|
+
}
|
|
4984
|
+
} else {
|
|
4985
|
+
api.logger.error(
|
|
4986
|
+
`Agent run failed for chat message ${userMessageId}: ${waitResult.error}`
|
|
4987
|
+
);
|
|
4988
|
+
await finalizeReply("(agent run failed)", {
|
|
4989
|
+
status: "errored",
|
|
4990
|
+
errorCode: "agent_error",
|
|
4991
|
+
errorMessage: waitResult.error ?? "Agent run failed"
|
|
4992
|
+
});
|
|
4993
|
+
}
|
|
4994
|
+
} catch (err) {
|
|
4995
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4996
|
+
api.logger.error(`Failed to process chat message ${userMessageId}: ${errMsg}`);
|
|
4997
|
+
try {
|
|
4998
|
+
await finalizeReply("(execution error)", {
|
|
4999
|
+
status: "errored",
|
|
5000
|
+
errorCode: "execution_error",
|
|
5001
|
+
errorMessage: errMsg
|
|
5002
|
+
});
|
|
5003
|
+
} catch (postErr) {
|
|
5004
|
+
api.logger.error(`Failed to report chat failure: ${postErr}`);
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
async function handleChatMessageStreaming(client, sessionKey, userMessageId, decoratedPrompt) {
|
|
5009
|
+
let assistantId;
|
|
5010
|
+
try {
|
|
5011
|
+
const started = await client.startChatReply(userMessageId);
|
|
5012
|
+
assistantId = started.id;
|
|
5013
|
+
} catch (err) {
|
|
5014
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5015
|
+
api.logger.error(
|
|
5016
|
+
`Chat ${userMessageId} failed to start streaming reply: ${errMsg}`
|
|
5017
|
+
);
|
|
5018
|
+
try {
|
|
5019
|
+
await client.postChatReply(userMessageId, "(streaming start failed)", {
|
|
5020
|
+
status: "errored",
|
|
5021
|
+
errorCode: "stream_start_failed",
|
|
5022
|
+
errorMessage: errMsg
|
|
5023
|
+
});
|
|
5024
|
+
} catch (postErr) {
|
|
5025
|
+
api.logger.error(
|
|
5026
|
+
`Chat ${userMessageId} fallback errored post failed: ${postErr}`
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
5031
|
+
let ackedText = "";
|
|
5032
|
+
const sendDelta = async (full) => {
|
|
5033
|
+
if (!full || full.length <= ackedText.length) return;
|
|
5034
|
+
if (!full.startsWith(ackedText)) return;
|
|
5035
|
+
const diff = full.slice(ackedText.length);
|
|
5036
|
+
if (!diff) return;
|
|
5037
|
+
try {
|
|
5038
|
+
await client.postChatReplyDelta(assistantId, diff);
|
|
5039
|
+
ackedText = full;
|
|
5040
|
+
} catch (err) {
|
|
5041
|
+
api.logger.warn(`Chat ${userMessageId} delta post failed: ${err}`);
|
|
5042
|
+
}
|
|
5043
|
+
};
|
|
5044
|
+
const finalizeErrored = async (content, errorCode, errorMessage) => {
|
|
5045
|
+
try {
|
|
5046
|
+
await client.completeChatReply(assistantId, {
|
|
5047
|
+
status: "errored",
|
|
5048
|
+
content: content || void 0,
|
|
5049
|
+
errorCode,
|
|
5050
|
+
errorMessage
|
|
5051
|
+
});
|
|
5052
|
+
} catch (postErr) {
|
|
5053
|
+
api.logger.error(
|
|
5054
|
+
`Chat ${userMessageId} finalize errored failed: ${postErr}`
|
|
5055
|
+
);
|
|
5056
|
+
}
|
|
5057
|
+
};
|
|
5058
|
+
let pollDone = false;
|
|
5059
|
+
const pollInterval = 400;
|
|
5060
|
+
const pollLoop = (async () => {
|
|
5061
|
+
while (!pollDone) {
|
|
5062
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5063
|
+
if (pollDone) break;
|
|
5064
|
+
try {
|
|
5065
|
+
const partial = await extractReplyForChatMessage(
|
|
5066
|
+
sessionKey,
|
|
5067
|
+
userMessageId,
|
|
5068
|
+
1
|
|
5069
|
+
);
|
|
5070
|
+
if (partial) await sendDelta(partial);
|
|
5071
|
+
} catch (err) {
|
|
5072
|
+
api.logger.warn(`Chat ${userMessageId} poll failed: ${err}`);
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
})();
|
|
5076
|
+
try {
|
|
5077
|
+
const result = await api.runtime.subagent.run({
|
|
5078
|
+
sessionKey,
|
|
5079
|
+
message: decoratedPrompt,
|
|
5080
|
+
deliver: true,
|
|
5081
|
+
idempotencyKey: `sw-chat-${userMessageId}`
|
|
5082
|
+
});
|
|
5083
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
5084
|
+
runId: result.runId,
|
|
5085
|
+
timeoutMs: 12e4
|
|
5086
|
+
});
|
|
5087
|
+
pollDone = true;
|
|
5088
|
+
await pollLoop;
|
|
5089
|
+
const finalReply = await extractReplyForChatMessage(
|
|
5090
|
+
sessionKey,
|
|
5091
|
+
userMessageId
|
|
5092
|
+
);
|
|
5093
|
+
if (waitResult.status !== "ok") {
|
|
5094
|
+
api.logger.error(
|
|
5095
|
+
`Agent run failed for chat ${userMessageId}: ${waitResult.error}`
|
|
5096
|
+
);
|
|
5097
|
+
await finalizeErrored(
|
|
5098
|
+
finalReply ?? ackedText,
|
|
5099
|
+
"agent_error",
|
|
5100
|
+
waitResult.error ?? "Agent run failed"
|
|
5101
|
+
);
|
|
5102
|
+
return;
|
|
5103
|
+
}
|
|
5104
|
+
if (!finalReply) {
|
|
5105
|
+
api.logger.warn(
|
|
5106
|
+
`Chat message ${userMessageId} completed but no reply found`
|
|
5107
|
+
);
|
|
5108
|
+
await finalizeErrored(
|
|
5109
|
+
ackedText,
|
|
5110
|
+
"no_reply",
|
|
5111
|
+
"Agent run produced no reply"
|
|
5112
|
+
);
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
try {
|
|
5116
|
+
await client.completeChatReply(assistantId, { content: finalReply });
|
|
5117
|
+
api.logger.info(`Chat message ${userMessageId} streamed`);
|
|
5118
|
+
} catch (completeErr) {
|
|
5119
|
+
const errMsg = completeErr instanceof Error ? completeErr.message : String(completeErr);
|
|
5120
|
+
api.logger.error(
|
|
5121
|
+
`Chat ${userMessageId} complete failed: ${errMsg}`
|
|
5122
|
+
);
|
|
5123
|
+
await finalizeErrored(finalReply, "complete_failed", errMsg);
|
|
5124
|
+
}
|
|
5125
|
+
} catch (err) {
|
|
5126
|
+
pollDone = true;
|
|
5127
|
+
try {
|
|
5128
|
+
await pollLoop;
|
|
5129
|
+
} catch {
|
|
5130
|
+
}
|
|
5131
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5132
|
+
api.logger.error(`Failed to stream chat ${userMessageId}: ${errMsg}`);
|
|
5133
|
+
await finalizeErrored(ackedText, "execution_error", errMsg);
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
4749
5136
|
async function handleReasoningJob(job, client) {
|
|
4750
5137
|
const hydrated = await hydrateReasoningJobEnvelope(
|
|
4751
5138
|
job,
|
|
@@ -4914,6 +5301,16 @@ function createRelayService(api) {
|
|
|
4914
5301
|
handleReasoningJob(envelope, client).catch(
|
|
4915
5302
|
(err) => api.logger.error(`Error handling reasoning job: ${err}`)
|
|
4916
5303
|
);
|
|
5304
|
+
} else if (envelope.event_type === "chat_message_dispatched") {
|
|
5305
|
+
const chatEnv = envelope;
|
|
5306
|
+
enqueueChat(
|
|
5307
|
+
chatEnv.session_id,
|
|
5308
|
+
() => handleChatMessage(chatEnv, client)
|
|
5309
|
+
);
|
|
5310
|
+
} else if (envelope.event_type !== void 0 && envelope.id === void 0) {
|
|
5311
|
+
api.logger.warn(
|
|
5312
|
+
`Ignoring unknown relay envelope: ${JSON.stringify(envelope.event_type)}`
|
|
5313
|
+
);
|
|
4917
5314
|
} else {
|
|
4918
5315
|
const task = envelope;
|
|
4919
5316
|
await handleTask(task, client);
|
package/openclaw.plugin.json
CHANGED