@stagewhisper/stagewhisper 0.57.0 → 0.59.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 +212 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98,6 +98,27 @@ 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
|
+
}
|
|
101
122
|
async heartbeat(capabilities) {
|
|
102
123
|
const res = await fetch(
|
|
103
124
|
`${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
|
|
@@ -4166,6 +4187,24 @@ function createRelayService(api) {
|
|
|
4166
4187
|
const COMPLETED_JOB_TTL_MS = 5 * 60 * 1e3;
|
|
4167
4188
|
const COMPLETED_JOB_MAX_SIZE = 5e3;
|
|
4168
4189
|
const processingReasoningJobs = /* @__PURE__ */ new Set();
|
|
4190
|
+
const chatSessionQueues = /* @__PURE__ */ new Map();
|
|
4191
|
+
function enqueueChat(sessionId, work) {
|
|
4192
|
+
const prev = chatSessionQueues.get(sessionId) ?? Promise.resolve();
|
|
4193
|
+
const next = prev.catch(() => {
|
|
4194
|
+
}).then(async () => {
|
|
4195
|
+
try {
|
|
4196
|
+
await work();
|
|
4197
|
+
} catch (err) {
|
|
4198
|
+
api.logger.error(`Chat queue work failed: ${err}`);
|
|
4199
|
+
}
|
|
4200
|
+
});
|
|
4201
|
+
chatSessionQueues.set(sessionId, next);
|
|
4202
|
+
void next.then(() => {
|
|
4203
|
+
if (chatSessionQueues.get(sessionId) === next) {
|
|
4204
|
+
chatSessionQueues.delete(sessionId);
|
|
4205
|
+
}
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4169
4208
|
function evictStaleCompletedJobs() {
|
|
4170
4209
|
const cutoff = Date.now() - COMPLETED_JOB_TTL_MS;
|
|
4171
4210
|
for (const [jobId, completedAt] of completedReasoningJobs) {
|
|
@@ -4322,6 +4361,34 @@ function createRelayService(api) {
|
|
|
4322
4361
|
}
|
|
4323
4362
|
return null;
|
|
4324
4363
|
}
|
|
4364
|
+
async function extractReplyForChatMessage(sessionKey, userMessageId, maxAttempts = 3) {
|
|
4365
|
+
const marker = `[StageWhisper chat: ${userMessageId}]`;
|
|
4366
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4367
|
+
if (attempt > 0) {
|
|
4368
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
4369
|
+
}
|
|
4370
|
+
const session = await api.runtime.subagent.getSessionMessages({
|
|
4371
|
+
sessionKey,
|
|
4372
|
+
limit: 100
|
|
4373
|
+
});
|
|
4374
|
+
const messages = session.messages;
|
|
4375
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4376
|
+
const msg = messages[i];
|
|
4377
|
+
if (msg["role"] !== "user") continue;
|
|
4378
|
+
const text = extractContentFromMessage(msg) ?? "";
|
|
4379
|
+
if (!text.includes(marker)) continue;
|
|
4380
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
4381
|
+
const next = messages[j];
|
|
4382
|
+
const role = next["role"];
|
|
4383
|
+
if (role === "assistant" || role === "model") {
|
|
4384
|
+
return extractContentFromMessage(next);
|
|
4385
|
+
}
|
|
4386
|
+
if (role === "user") break;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
return null;
|
|
4391
|
+
}
|
|
4325
4392
|
function decryptTaskFields(task, client) {
|
|
4326
4393
|
const keypair = client.pluginKeypair;
|
|
4327
4394
|
const desktopPub = client.desktopPublicKey;
|
|
@@ -4746,6 +4813,141 @@ function createRelayService(api) {
|
|
|
4746
4813
|
api.logger.error(`Failed to post BYO reasoning result for ${job.job_id}: ${postErr}`);
|
|
4747
4814
|
}
|
|
4748
4815
|
}
|
|
4816
|
+
function looksLikeBYOEnvelope(s) {
|
|
4817
|
+
const trimmed = s.trimStart();
|
|
4818
|
+
if (!trimmed.startsWith("{")) return false;
|
|
4819
|
+
return trimmed.includes('"version"') && trimmed.includes('"sender_role"') && trimmed.includes('"ciphertext"');
|
|
4820
|
+
}
|
|
4821
|
+
function decryptChatPrompt(rawContent, client) {
|
|
4822
|
+
const keypair = client.pluginKeypair;
|
|
4823
|
+
const desktopPub = client.desktopPublicKey;
|
|
4824
|
+
if (!keypair || !desktopPub) {
|
|
4825
|
+
return { error: "missing_byo_keys" };
|
|
4826
|
+
}
|
|
4827
|
+
try {
|
|
4828
|
+
const envelope = JSON.parse(rawContent);
|
|
4829
|
+
const envelopeKey = keypair.deriveEnvelopeKey(desktopPub);
|
|
4830
|
+
const plaintextBytes = open(envelopeKey, envelope);
|
|
4831
|
+
return {
|
|
4832
|
+
plaintext: new TextDecoder().decode(plaintextBytes),
|
|
4833
|
+
envelopeKey
|
|
4834
|
+
};
|
|
4835
|
+
} catch (err) {
|
|
4836
|
+
return { error: `decryption_error: ${err}` };
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
function sealChatReply(envelopeKey, sessionId, userMessageId, replyText) {
|
|
4840
|
+
const envelope = seal(
|
|
4841
|
+
envelopeKey,
|
|
4842
|
+
"plugin",
|
|
4843
|
+
sessionId,
|
|
4844
|
+
`chat-reply:${userMessageId}`,
|
|
4845
|
+
"task_content",
|
|
4846
|
+
new TextEncoder().encode(replyText)
|
|
4847
|
+
);
|
|
4848
|
+
return JSON.stringify(envelope);
|
|
4849
|
+
}
|
|
4850
|
+
async function handleChatMessage(envelope, client) {
|
|
4851
|
+
const userMessageId = envelope.user_message_id;
|
|
4852
|
+
const sessionId = envelope.session_id;
|
|
4853
|
+
const rawContent = envelope.content;
|
|
4854
|
+
if (!userMessageId || !sessionId || !rawContent) {
|
|
4855
|
+
api.logger.warn(
|
|
4856
|
+
`Skipping chat message dispatch \u2014 missing fields (user_message_id=${userMessageId}, session_id=${sessionId})`
|
|
4857
|
+
);
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
api.logger.info(
|
|
4861
|
+
`Received chat message: ${userMessageId} (session: ${sessionId})`
|
|
4862
|
+
);
|
|
4863
|
+
let plaintextContent = rawContent;
|
|
4864
|
+
let envelopeKey = null;
|
|
4865
|
+
if (looksLikeBYOEnvelope(rawContent)) {
|
|
4866
|
+
const decryptResult = decryptChatPrompt(rawContent, client);
|
|
4867
|
+
if ("error" in decryptResult) {
|
|
4868
|
+
api.logger.error(
|
|
4869
|
+
`Chat ${userMessageId} BYO decrypt failed: ${decryptResult.error}`
|
|
4870
|
+
);
|
|
4871
|
+
try {
|
|
4872
|
+
await client.postChatReply(
|
|
4873
|
+
userMessageId,
|
|
4874
|
+
"(BYO decrypt failed)",
|
|
4875
|
+
{
|
|
4876
|
+
status: "errored",
|
|
4877
|
+
errorCode: decryptResult.error.startsWith("missing_") ? "missing_byo_keys" : "decryption_error",
|
|
4878
|
+
errorMessage: decryptResult.error
|
|
4879
|
+
}
|
|
4880
|
+
);
|
|
4881
|
+
} catch (postErr) {
|
|
4882
|
+
api.logger.error(`Failed to report BYO error: ${postErr}`);
|
|
4883
|
+
}
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
plaintextContent = decryptResult.plaintext;
|
|
4887
|
+
envelopeKey = decryptResult.envelopeKey;
|
|
4888
|
+
}
|
|
4889
|
+
const sessionKey = buildAgentSessionKey({
|
|
4890
|
+
agentId: "default",
|
|
4891
|
+
channel: "stagewhisper",
|
|
4892
|
+
peer: { kind: "direct", id: `sw-chat-${sessionId}` }
|
|
4893
|
+
});
|
|
4894
|
+
const finalizeReply = async (replyText, options) => {
|
|
4895
|
+
const outbound = envelopeKey ? sealChatReply(envelopeKey, sessionId, userMessageId, replyText) : replyText;
|
|
4896
|
+
await client.postChatReply(userMessageId, outbound, options);
|
|
4897
|
+
};
|
|
4898
|
+
const decoratedPrompt = `${plaintextContent}
|
|
4899
|
+
|
|
4900
|
+
[StageWhisper chat: ${userMessageId}]`;
|
|
4901
|
+
try {
|
|
4902
|
+
const result = await api.runtime.subagent.run({
|
|
4903
|
+
sessionKey,
|
|
4904
|
+
message: decoratedPrompt,
|
|
4905
|
+
deliver: true,
|
|
4906
|
+
idempotencyKey: `sw-chat-${userMessageId}`
|
|
4907
|
+
});
|
|
4908
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
4909
|
+
runId: result.runId,
|
|
4910
|
+
timeoutMs: 12e4
|
|
4911
|
+
});
|
|
4912
|
+
if (waitResult.status === "ok") {
|
|
4913
|
+
const reply = await extractReplyForChatMessage(sessionKey, userMessageId);
|
|
4914
|
+
if (reply) {
|
|
4915
|
+
await finalizeReply(reply);
|
|
4916
|
+
api.logger.info(`Chat message ${userMessageId} replied`);
|
|
4917
|
+
} else {
|
|
4918
|
+
api.logger.warn(
|
|
4919
|
+
`Chat message ${userMessageId} completed but no reply found`
|
|
4920
|
+
);
|
|
4921
|
+
await finalizeReply("(no reply produced)", {
|
|
4922
|
+
status: "errored",
|
|
4923
|
+
errorCode: "no_reply",
|
|
4924
|
+
errorMessage: "Agent run produced no reply"
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
} else {
|
|
4928
|
+
api.logger.error(
|
|
4929
|
+
`Agent run failed for chat message ${userMessageId}: ${waitResult.error}`
|
|
4930
|
+
);
|
|
4931
|
+
await finalizeReply("(agent run failed)", {
|
|
4932
|
+
status: "errored",
|
|
4933
|
+
errorCode: "agent_error",
|
|
4934
|
+
errorMessage: waitResult.error ?? "Agent run failed"
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
} catch (err) {
|
|
4938
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4939
|
+
api.logger.error(`Failed to process chat message ${userMessageId}: ${errMsg}`);
|
|
4940
|
+
try {
|
|
4941
|
+
await finalizeReply("(execution error)", {
|
|
4942
|
+
status: "errored",
|
|
4943
|
+
errorCode: "execution_error",
|
|
4944
|
+
errorMessage: errMsg
|
|
4945
|
+
});
|
|
4946
|
+
} catch (postErr) {
|
|
4947
|
+
api.logger.error(`Failed to report chat failure: ${postErr}`);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4749
4951
|
async function handleReasoningJob(job, client) {
|
|
4750
4952
|
const hydrated = await hydrateReasoningJobEnvelope(
|
|
4751
4953
|
job,
|
|
@@ -4914,6 +5116,16 @@ function createRelayService(api) {
|
|
|
4914
5116
|
handleReasoningJob(envelope, client).catch(
|
|
4915
5117
|
(err) => api.logger.error(`Error handling reasoning job: ${err}`)
|
|
4916
5118
|
);
|
|
5119
|
+
} else if (envelope.event_type === "chat_message_dispatched") {
|
|
5120
|
+
const chatEnv = envelope;
|
|
5121
|
+
enqueueChat(
|
|
5122
|
+
chatEnv.session_id,
|
|
5123
|
+
() => handleChatMessage(chatEnv, client)
|
|
5124
|
+
);
|
|
5125
|
+
} else if (envelope.event_type !== void 0 && envelope.id === void 0) {
|
|
5126
|
+
api.logger.warn(
|
|
5127
|
+
`Ignoring unknown relay envelope: ${JSON.stringify(envelope.event_type)}`
|
|
5128
|
+
);
|
|
4917
5129
|
} else {
|
|
4918
5130
|
const task = envelope;
|
|
4919
5131
|
await handleTask(task, client);
|
package/openclaw.plugin.json
CHANGED