@stagewhisper/stagewhisper 0.60.0 → 0.61.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 +1032 -89
- package/openclaw.plugin.json +29 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -252,8 +252,8 @@ function resolveGatewayConfig(api) {
|
|
|
252
252
|
function isResponsesEndpointEnabled(api) {
|
|
253
253
|
const cfg = api.config;
|
|
254
254
|
const gw = cfg?.gateway ?? {};
|
|
255
|
-
const
|
|
256
|
-
const endpoints =
|
|
255
|
+
const http2 = gw?.http ?? {};
|
|
256
|
+
const endpoints = http2?.endpoints ?? {};
|
|
257
257
|
const responses = endpoints?.responses ?? {};
|
|
258
258
|
return responses?.enabled === true;
|
|
259
259
|
}
|
|
@@ -431,7 +431,7 @@ function createHasher(hashCons) {
|
|
|
431
431
|
hashC.create = () => hashCons();
|
|
432
432
|
return hashC;
|
|
433
433
|
}
|
|
434
|
-
function
|
|
434
|
+
function randomBytes2(bytesLength = 32) {
|
|
435
435
|
if (crypto2 && typeof crypto2.getRandomValues === "function") {
|
|
436
436
|
return crypto2.getRandomValues(new Uint8Array(bytesLength));
|
|
437
437
|
}
|
|
@@ -2003,7 +2003,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
2003
2003
|
});
|
|
2004
2004
|
const { prehash } = eddsaOpts;
|
|
2005
2005
|
const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
|
|
2006
|
-
const
|
|
2006
|
+
const randomBytes4 = eddsaOpts.randomBytes || randomBytes2;
|
|
2007
2007
|
const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
|
|
2008
2008
|
const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
|
|
2009
2009
|
_abool2(phflag, "phflag");
|
|
@@ -2085,7 +2085,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
2085
2085
|
signature: 2 * _size,
|
|
2086
2086
|
seed: _size
|
|
2087
2087
|
};
|
|
2088
|
-
function randomSecretKey(seed =
|
|
2088
|
+
function randomSecretKey(seed = randomBytes4(lengths.seed)) {
|
|
2089
2089
|
return _abytes2(seed, lengths.seed, "seed");
|
|
2090
2090
|
}
|
|
2091
2091
|
function keygen(seed) {
|
|
@@ -2280,7 +2280,7 @@ function montgomery(curveDef) {
|
|
|
2280
2280
|
const is25519 = type === "x25519";
|
|
2281
2281
|
if (!is25519 && type !== "x448")
|
|
2282
2282
|
throw new Error("invalid type");
|
|
2283
|
-
const randomBytes_ = rand ||
|
|
2283
|
+
const randomBytes_ = rand || randomBytes2;
|
|
2284
2284
|
const montgomeryBits = is25519 ? 255 : 448;
|
|
2285
2285
|
const fieldLen = is25519 ? 32 : 56;
|
|
2286
2286
|
const Gu = is25519 ? BigInt(9) : BigInt(5);
|
|
@@ -3547,7 +3547,7 @@ var init_cryptoNode2 = __esm({
|
|
|
3547
3547
|
});
|
|
3548
3548
|
|
|
3549
3549
|
// node_modules/.pnpm/@noble+ciphers@1.3.0/node_modules/@noble/ciphers/esm/webcrypto.js
|
|
3550
|
-
function
|
|
3550
|
+
function randomBytes3(bytesLength = 32) {
|
|
3551
3551
|
if (crypto3 && typeof crypto3.getRandomValues === "function") {
|
|
3552
3552
|
return crypto3.getRandomValues(new Uint8Array(bytesLength));
|
|
3553
3553
|
}
|
|
@@ -3591,7 +3591,7 @@ function generateUUID() {
|
|
|
3591
3591
|
}
|
|
3592
3592
|
function seal(key, senderRole, sessionId, correlationId, contentType, plaintext) {
|
|
3593
3593
|
const messageId = generateUUID();
|
|
3594
|
-
const nonce =
|
|
3594
|
+
const nonce = randomBytes3(24);
|
|
3595
3595
|
const metadata = {
|
|
3596
3596
|
version: ENVELOPE_VERSION,
|
|
3597
3597
|
sender_role: senderRole,
|
|
@@ -3666,7 +3666,7 @@ var init_crypto = __esm({
|
|
|
3666
3666
|
this.publicKey = x25519.getPublicKey(secretKey);
|
|
3667
3667
|
}
|
|
3668
3668
|
static generate() {
|
|
3669
|
-
const secretKey =
|
|
3669
|
+
const secretKey = randomBytes3(32);
|
|
3670
3670
|
return new _IdentityKeypair(secretKey);
|
|
3671
3671
|
}
|
|
3672
3672
|
static fromSecretBytes(bytes) {
|
|
@@ -3987,6 +3987,858 @@ var stagewhisperPlugin = {
|
|
|
3987
3987
|
}
|
|
3988
3988
|
};
|
|
3989
3989
|
|
|
3990
|
+
// src/http-transport.ts
|
|
3991
|
+
import http from "node:http";
|
|
3992
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
3993
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3994
|
+
|
|
3995
|
+
// src/core.ts
|
|
3996
|
+
var TASK_ID_REGEX = /^[0-9a-f-]{36}$/;
|
|
3997
|
+
var LOOPBACK_CALLBACK_URL_REGEX = /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/?$/;
|
|
3998
|
+
var ALLOWED_REASONS = /* @__PURE__ */ new Set([
|
|
3999
|
+
"transcript_chunk",
|
|
4000
|
+
"chat_message",
|
|
4001
|
+
"system_prelude"
|
|
4002
|
+
]);
|
|
4003
|
+
var MAX_BODY_BYTES = 262144;
|
|
4004
|
+
var MAX_PAYLOAD_TEXT_CHARS = 8e3;
|
|
4005
|
+
var MAX_SESSION_ID_CHARS = 128;
|
|
4006
|
+
var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
|
|
4007
|
+
function isTestTask(task) {
|
|
4008
|
+
return task.action_type === "connectivity_test";
|
|
4009
|
+
}
|
|
4010
|
+
function buildTaskMessage(task) {
|
|
4011
|
+
const lines = [];
|
|
4012
|
+
lines.push(`**${task.title}**`);
|
|
4013
|
+
lines.push("");
|
|
4014
|
+
lines.push(task.request_text);
|
|
4015
|
+
if (task.evidence_payload) {
|
|
4016
|
+
const evidence = task.evidence_payload;
|
|
4017
|
+
if (evidence["transcript_excerpt"]) {
|
|
4018
|
+
lines.push("");
|
|
4019
|
+
lines.push(`Context: ${evidence["transcript_excerpt"]}`);
|
|
4020
|
+
}
|
|
4021
|
+
if (evidence["signal_summary"]) {
|
|
4022
|
+
lines.push(`Signal: ${evidence["signal_summary"]}`);
|
|
4023
|
+
}
|
|
4024
|
+
if (evidence["tone_summary"]) {
|
|
4025
|
+
lines.push(`Tone: ${evidence["tone_summary"]}`);
|
|
4026
|
+
}
|
|
4027
|
+
if (evidence["playbook_label"]) {
|
|
4028
|
+
lines.push(`Playbook: ${evidence["playbook_label"]}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
lines.push("");
|
|
4032
|
+
lines.push(`Action type: ${task.action_type}`);
|
|
4033
|
+
lines.push(`StageWhisper task: ${task.id}`);
|
|
4034
|
+
lines.push(`Session: ${task.session_id}`);
|
|
4035
|
+
return lines.join("\n");
|
|
4036
|
+
}
|
|
4037
|
+
async function dispatchTaskToAgent(options) {
|
|
4038
|
+
const { api, task } = options;
|
|
4039
|
+
const peerId = options.sessionPeerId ?? (isTestTask(task) ? `sw-test-${task.id}` : `sw-session-${task.session_id}`);
|
|
4040
|
+
const sessionKey = buildAgentSessionKey({
|
|
4041
|
+
agentId: "default",
|
|
4042
|
+
channel: "stagewhisper",
|
|
4043
|
+
peer: { kind: "direct", id: peerId },
|
|
4044
|
+
dmScope: "per-channel-peer"
|
|
4045
|
+
});
|
|
4046
|
+
const messageContent = buildTaskMessage(task);
|
|
4047
|
+
const result = await api.runtime.subagent.run({
|
|
4048
|
+
sessionKey,
|
|
4049
|
+
message: messageContent,
|
|
4050
|
+
deliver: options.deliver ?? !isTestTask(task),
|
|
4051
|
+
idempotencyKey: options.idempotencyKey ?? `sw-task-${task.id}`
|
|
4052
|
+
});
|
|
4053
|
+
return { runId: result.runId, sessionKey };
|
|
4054
|
+
}
|
|
4055
|
+
function isLoopbackCallbackUrl(url) {
|
|
4056
|
+
return LOOPBACK_CALLBACK_URL_REGEX.test(url);
|
|
4057
|
+
}
|
|
4058
|
+
function isAllowedHostHeader(host) {
|
|
4059
|
+
if (!host) return false;
|
|
4060
|
+
const trimmed = host.trim().toLowerCase();
|
|
4061
|
+
if (!trimmed) return false;
|
|
4062
|
+
const colonIdx = trimmed.lastIndexOf(":");
|
|
4063
|
+
const hostname = colonIdx === -1 ? trimmed : trimmed.slice(0, colonIdx);
|
|
4064
|
+
const port = colonIdx === -1 ? "" : trimmed.slice(colonIdx + 1);
|
|
4065
|
+
if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
|
|
4066
|
+
if (port && !/^\d+$/.test(port)) return false;
|
|
4067
|
+
return true;
|
|
4068
|
+
}
|
|
4069
|
+
function buildChatId(sessionId, reason) {
|
|
4070
|
+
if (reason === "transcript_chunk") return `sw:${sessionId}:reasoning`;
|
|
4071
|
+
if (reason === "chat_message") return `sw:${sessionId}:chat`;
|
|
4072
|
+
return null;
|
|
4073
|
+
}
|
|
4074
|
+
function buildCallbackUrl(baseUrl, taskId) {
|
|
4075
|
+
return `${baseUrl.replace(/\/$/, "")}/tasks/${taskId}`;
|
|
4076
|
+
}
|
|
4077
|
+
function applyPrelude(text, prelude) {
|
|
4078
|
+
if (!prelude) return text;
|
|
4079
|
+
return `[Context: ${prelude}]
|
|
4080
|
+
|
|
4081
|
+
${text}`;
|
|
4082
|
+
}
|
|
4083
|
+
function validateHttpTaskRequest(body) {
|
|
4084
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4085
|
+
return { ok: false, error: "body must be a JSON object" };
|
|
4086
|
+
}
|
|
4087
|
+
const obj = body;
|
|
4088
|
+
const task_id = obj["task_id"];
|
|
4089
|
+
if (typeof task_id !== "string" || task_id.length === 0) {
|
|
4090
|
+
return { ok: false, error: "task_id must be a non-empty string" };
|
|
4091
|
+
}
|
|
4092
|
+
if (!TASK_ID_REGEX.test(task_id)) {
|
|
4093
|
+
return { ok: false, error: "task_id must match ^[0-9a-f-]{36}$" };
|
|
4094
|
+
}
|
|
4095
|
+
const session_id = obj["session_id"];
|
|
4096
|
+
if (typeof session_id !== "string" || session_id.length === 0) {
|
|
4097
|
+
return { ok: false, error: "session_id must be a non-empty string" };
|
|
4098
|
+
}
|
|
4099
|
+
if (session_id.length > MAX_SESSION_ID_CHARS) {
|
|
4100
|
+
return {
|
|
4101
|
+
ok: false,
|
|
4102
|
+
error: `session_id must be <= ${MAX_SESSION_ID_CHARS} chars`
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
const reason = obj["reason"];
|
|
4106
|
+
if (typeof reason !== "string" || reason.length === 0) {
|
|
4107
|
+
return { ok: false, error: "reason must be a non-empty string" };
|
|
4108
|
+
}
|
|
4109
|
+
if (!ALLOWED_REASONS.has(reason)) {
|
|
4110
|
+
return {
|
|
4111
|
+
ok: false,
|
|
4112
|
+
error: `reason must be one of ${Array.from(ALLOWED_REASONS).join(", ")}`
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
const payload = obj["payload"];
|
|
4116
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
4117
|
+
return { ok: false, error: "payload must be an object" };
|
|
4118
|
+
}
|
|
4119
|
+
const payloadObj = payload;
|
|
4120
|
+
const text = payloadObj["text"];
|
|
4121
|
+
if (typeof text !== "string") {
|
|
4122
|
+
return { ok: false, error: "payload.text must be a string" };
|
|
4123
|
+
}
|
|
4124
|
+
if (text.length > MAX_PAYLOAD_TEXT_CHARS) {
|
|
4125
|
+
return {
|
|
4126
|
+
ok: false,
|
|
4127
|
+
error: `payload.text must be <= ${MAX_PAYLOAD_TEXT_CHARS} chars`
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
let callback;
|
|
4131
|
+
const callbackRaw = obj["callback"];
|
|
4132
|
+
if (callbackRaw !== void 0 && callbackRaw !== null) {
|
|
4133
|
+
if (typeof callbackRaw !== "object" || Array.isArray(callbackRaw)) {
|
|
4134
|
+
return { ok: false, error: "callback must be an object" };
|
|
4135
|
+
}
|
|
4136
|
+
const cbObj = callbackRaw;
|
|
4137
|
+
const cbUrl = cbObj["url"];
|
|
4138
|
+
const cbToken = cbObj["token"];
|
|
4139
|
+
if (typeof cbUrl !== "string" || cbUrl.length === 0) {
|
|
4140
|
+
return { ok: false, error: "callback.url must be a non-empty string" };
|
|
4141
|
+
}
|
|
4142
|
+
if (!isLoopbackCallbackUrl(cbUrl)) {
|
|
4143
|
+
return {
|
|
4144
|
+
ok: false,
|
|
4145
|
+
error: "callback.url must be a loopback base URL (http://127.0.0.1:PORT or http://localhost:PORT) with no path"
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
if (typeof cbToken !== "string" || cbToken.length < 16) {
|
|
4149
|
+
return {
|
|
4150
|
+
ok: false,
|
|
4151
|
+
error: "callback.token must be a string of length >= 16"
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
callback = { url: cbUrl, token: cbToken };
|
|
4155
|
+
}
|
|
4156
|
+
const chatIdRaw = obj["chat_id"];
|
|
4157
|
+
const chat_id = typeof chatIdRaw === "string" && chatIdRaw.length > 0 ? chatIdRaw : buildChatId(session_id, reason) ?? void 0;
|
|
4158
|
+
const req = {
|
|
4159
|
+
task_id,
|
|
4160
|
+
session_id,
|
|
4161
|
+
reason,
|
|
4162
|
+
...chat_id ? { chat_id } : {},
|
|
4163
|
+
occurred_at: typeof obj["occurred_at"] === "string" ? obj["occurred_at"] : void 0,
|
|
4164
|
+
payload: {
|
|
4165
|
+
text,
|
|
4166
|
+
ts_start_ms: typeof payloadObj["ts_start_ms"] === "number" ? payloadObj["ts_start_ms"] : void 0,
|
|
4167
|
+
ts_end_ms: typeof payloadObj["ts_end_ms"] === "number" ? payloadObj["ts_end_ms"] : void 0,
|
|
4168
|
+
is_final: typeof payloadObj["is_final"] === "boolean" ? payloadObj["is_final"] : void 0,
|
|
4169
|
+
user_message_id: typeof payloadObj["user_message_id"] === "string" ? payloadObj["user_message_id"] : void 0
|
|
4170
|
+
},
|
|
4171
|
+
...callback ? { callback } : {}
|
|
4172
|
+
};
|
|
4173
|
+
return { ok: true, req };
|
|
4174
|
+
}
|
|
4175
|
+
function httpTaskRequestToTaskPayload(req, overrides) {
|
|
4176
|
+
if (req.reason === "system_prelude") return null;
|
|
4177
|
+
const text = overrides?.text ?? req.payload.text;
|
|
4178
|
+
const evidence = {
|
|
4179
|
+
transcript_excerpt: text
|
|
4180
|
+
};
|
|
4181
|
+
if (typeof req.payload.ts_start_ms === "number") {
|
|
4182
|
+
evidence["ts_start_ms"] = req.payload.ts_start_ms;
|
|
4183
|
+
}
|
|
4184
|
+
if (typeof req.payload.ts_end_ms === "number") {
|
|
4185
|
+
evidence["ts_end_ms"] = req.payload.ts_end_ms;
|
|
4186
|
+
}
|
|
4187
|
+
if (typeof req.payload.is_final === "boolean") {
|
|
4188
|
+
evidence["is_final"] = req.payload.is_final;
|
|
4189
|
+
}
|
|
4190
|
+
if (typeof req.payload.user_message_id === "string") {
|
|
4191
|
+
evidence["user_message_id"] = req.payload.user_message_id;
|
|
4192
|
+
}
|
|
4193
|
+
if (req.chat_id) {
|
|
4194
|
+
evidence["chat_id"] = req.chat_id;
|
|
4195
|
+
}
|
|
4196
|
+
const isChat = req.reason === "chat_message";
|
|
4197
|
+
return {
|
|
4198
|
+
id: req.task_id,
|
|
4199
|
+
session_id: req.session_id,
|
|
4200
|
+
title: isChat ? `Chat message in session ${req.session_id}` : `Transcript chunk from session ${req.session_id}`,
|
|
4201
|
+
request_text: text,
|
|
4202
|
+
action_type: req.reason,
|
|
4203
|
+
status: "delivered",
|
|
4204
|
+
evidence_payload: evidence,
|
|
4205
|
+
created_at: req.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// src/http-transport.ts
|
|
4210
|
+
var IDEMPOTENCY_MAX_SIZE = 1024;
|
|
4211
|
+
var IDEMPOTENCY_TTL_MS = 5 * 60 * 1e3;
|
|
4212
|
+
var CALLBACK_TIMEOUT_MS = 5e3;
|
|
4213
|
+
var CALLBACK_MAX_ATTEMPTS = 4;
|
|
4214
|
+
var CALLBACK_RETRY_BASE_MS = 250;
|
|
4215
|
+
var SUBAGENT_WAIT_TIMEOUT_MS = 12e4;
|
|
4216
|
+
var INCOMING_PATH = "/v1/incoming";
|
|
4217
|
+
var PING_PATH = "/v1/ping";
|
|
4218
|
+
function constantTimeTokenEqual(provided, expected) {
|
|
4219
|
+
const a = Buffer2.from(provided);
|
|
4220
|
+
const b = Buffer2.from(expected);
|
|
4221
|
+
if (a.length !== b.length) {
|
|
4222
|
+
const sink = Buffer2.alloc(Math.max(a.length, b.length));
|
|
4223
|
+
timingSafeEqual(sink, sink);
|
|
4224
|
+
return false;
|
|
4225
|
+
}
|
|
4226
|
+
return timingSafeEqual(a, b);
|
|
4227
|
+
}
|
|
4228
|
+
function extractBearerToken(header) {
|
|
4229
|
+
if (!header) return null;
|
|
4230
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
4231
|
+
if (!match) return null;
|
|
4232
|
+
return match[1]?.trim() ?? null;
|
|
4233
|
+
}
|
|
4234
|
+
function isLoopbackAddress(addr) {
|
|
4235
|
+
if (!addr) return false;
|
|
4236
|
+
if (addr === "127.0.0.1" || addr === "::1") return true;
|
|
4237
|
+
if (addr === "::ffff:127.0.0.1") return true;
|
|
4238
|
+
return false;
|
|
4239
|
+
}
|
|
4240
|
+
function readBody(req, maxBytes) {
|
|
4241
|
+
return new Promise((resolve, reject) => {
|
|
4242
|
+
const chunks = [];
|
|
4243
|
+
let total = 0;
|
|
4244
|
+
req.on("data", (chunk) => {
|
|
4245
|
+
total += chunk.length;
|
|
4246
|
+
if (total > maxBytes) {
|
|
4247
|
+
reject(new Error("body_too_large"));
|
|
4248
|
+
req.destroy();
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
chunks.push(chunk);
|
|
4252
|
+
});
|
|
4253
|
+
req.on("end", () => {
|
|
4254
|
+
resolve(Buffer2.concat(chunks).toString("utf8"));
|
|
4255
|
+
});
|
|
4256
|
+
req.on("error", (err) => reject(err));
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
4259
|
+
function jsonResponse(status, body) {
|
|
4260
|
+
const serialized = JSON.stringify(body);
|
|
4261
|
+
return {
|
|
4262
|
+
status,
|
|
4263
|
+
headers: {
|
|
4264
|
+
"Content-Type": "application/json",
|
|
4265
|
+
"Content-Length": String(Buffer2.byteLength(serialized))
|
|
4266
|
+
},
|
|
4267
|
+
body: serialized
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
function extractContentFromMessage(msg) {
|
|
4271
|
+
const content = msg["content"];
|
|
4272
|
+
if (typeof content === "string") return content;
|
|
4273
|
+
if (Array.isArray(content)) {
|
|
4274
|
+
for (const part of content) {
|
|
4275
|
+
if (typeof part === "object" && part !== null && part["type"] === "text" && typeof part["text"] === "string") {
|
|
4276
|
+
return part["text"];
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
return null;
|
|
4281
|
+
}
|
|
4282
|
+
function createHttpTransport(options) {
|
|
4283
|
+
const { api } = options;
|
|
4284
|
+
const host = options.host ?? "127.0.0.1";
|
|
4285
|
+
const port = options.port ?? 8765;
|
|
4286
|
+
const token = options.token;
|
|
4287
|
+
const callbackFetch = options.callbackFetch ?? fetch;
|
|
4288
|
+
if (!token || token.length < 16) {
|
|
4289
|
+
throw new Error("http-transport: token must be at least 16 characters");
|
|
4290
|
+
}
|
|
4291
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
4292
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
4293
|
+
const chatTasks = /* @__PURE__ */ new Map();
|
|
4294
|
+
const pendingPreludes = /* @__PURE__ */ new Map();
|
|
4295
|
+
function rememberResult(taskId, status, body) {
|
|
4296
|
+
idempotency.set(taskId, {
|
|
4297
|
+
status,
|
|
4298
|
+
body,
|
|
4299
|
+
expiresAt: Date.now() + IDEMPOTENCY_TTL_MS
|
|
4300
|
+
});
|
|
4301
|
+
while (idempotency.size > IDEMPOTENCY_MAX_SIZE) {
|
|
4302
|
+
const oldestKey = idempotency.keys().next().value;
|
|
4303
|
+
if (oldestKey === void 0) break;
|
|
4304
|
+
idempotency.delete(oldestKey);
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
function evictExpired() {
|
|
4308
|
+
const now = Date.now();
|
|
4309
|
+
for (const [key, entry] of idempotency) {
|
|
4310
|
+
if (entry.expiresAt <= now) idempotency.delete(key);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
function recordTerminalCallback(taskId, callback, body) {
|
|
4314
|
+
const entry = idempotency.get(taskId);
|
|
4315
|
+
if (!entry) return;
|
|
4316
|
+
entry.terminal = { callback, body };
|
|
4317
|
+
}
|
|
4318
|
+
function consumePrelude(sessionId) {
|
|
4319
|
+
const prelude = pendingPreludes.get(sessionId);
|
|
4320
|
+
if (prelude !== void 0) pendingPreludes.delete(sessionId);
|
|
4321
|
+
return prelude;
|
|
4322
|
+
}
|
|
4323
|
+
async function extractReplyForChatTask(sessionKey, taskId, userMessageId, maxAttempts = 3) {
|
|
4324
|
+
const markers = [`StageWhisper task: ${taskId}`];
|
|
4325
|
+
if (userMessageId) markers.push(`[StageWhisper chat: ${userMessageId}]`);
|
|
4326
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4327
|
+
if (attempt > 0) {
|
|
4328
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
4329
|
+
}
|
|
4330
|
+
const session = await api.runtime.subagent.getSessionMessages({
|
|
4331
|
+
sessionKey,
|
|
4332
|
+
limit: 50
|
|
4333
|
+
});
|
|
4334
|
+
const messages = session.messages;
|
|
4335
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4336
|
+
const msg = messages[i];
|
|
4337
|
+
if (msg["role"] !== "user") continue;
|
|
4338
|
+
const text = extractContentFromMessage(msg) ?? "";
|
|
4339
|
+
if (!markers.some((m) => text.includes(m))) continue;
|
|
4340
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
4341
|
+
const reply = messages[j];
|
|
4342
|
+
const role = reply["role"];
|
|
4343
|
+
if (role === "assistant" || role === "model") {
|
|
4344
|
+
return extractContentFromMessage(reply);
|
|
4345
|
+
}
|
|
4346
|
+
if (role === "user") break;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
return null;
|
|
4351
|
+
}
|
|
4352
|
+
async function postCallback(callback, taskId, body) {
|
|
4353
|
+
const url = buildCallbackUrl(callback.url, taskId);
|
|
4354
|
+
const serialized = JSON.stringify(body);
|
|
4355
|
+
let lastError = null;
|
|
4356
|
+
for (let attempt = 0; attempt < CALLBACK_MAX_ATTEMPTS; attempt++) {
|
|
4357
|
+
const controller = new AbortController();
|
|
4358
|
+
const timeoutId = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT_MS);
|
|
4359
|
+
try {
|
|
4360
|
+
const res = await callbackFetch(url, {
|
|
4361
|
+
method: "POST",
|
|
4362
|
+
headers: {
|
|
4363
|
+
"Content-Type": "application/json",
|
|
4364
|
+
Authorization: `Bearer ${callback.token}`
|
|
4365
|
+
},
|
|
4366
|
+
body: serialized,
|
|
4367
|
+
signal: controller.signal
|
|
4368
|
+
});
|
|
4369
|
+
clearTimeout(timeoutId);
|
|
4370
|
+
if (res.ok) return;
|
|
4371
|
+
lastError = new Error(`callback returned status ${res.status}`);
|
|
4372
|
+
if (res.status >= 400 && res.status < 500) {
|
|
4373
|
+
throw lastError;
|
|
4374
|
+
}
|
|
4375
|
+
} catch (err) {
|
|
4376
|
+
clearTimeout(timeoutId);
|
|
4377
|
+
lastError = err;
|
|
4378
|
+
}
|
|
4379
|
+
if (attempt + 1 < CALLBACK_MAX_ATTEMPTS) {
|
|
4380
|
+
await new Promise(
|
|
4381
|
+
(r) => setTimeout(r, CALLBACK_RETRY_BASE_MS * 2 ** attempt)
|
|
4382
|
+
);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "callback_failed"));
|
|
4386
|
+
}
|
|
4387
|
+
async function redeliverTerminalCallback(record, taskId) {
|
|
4388
|
+
try {
|
|
4389
|
+
await postCallback(record.callback, taskId, record.body);
|
|
4390
|
+
api.logger.info(
|
|
4391
|
+
`[stagewhisper-http] redelivered terminal callback for ${taskId}`
|
|
4392
|
+
);
|
|
4393
|
+
} catch (err) {
|
|
4394
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4395
|
+
api.logger.error(
|
|
4396
|
+
`[stagewhisper-http] terminal callback redelivery failed for ${taskId}: ${errMsg}`
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
async function runReplyTaskAsync(taskRequest, kind) {
|
|
4401
|
+
const callback = taskRequest.callback;
|
|
4402
|
+
if (!callback) {
|
|
4403
|
+
api.logger.error(
|
|
4404
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} missing callback after validation`
|
|
4405
|
+
);
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
const userMessageId = taskRequest.payload.user_message_id;
|
|
4409
|
+
const effectiveText = applyPrelude(
|
|
4410
|
+
taskRequest.payload.text,
|
|
4411
|
+
consumePrelude(taskRequest.session_id)
|
|
4412
|
+
);
|
|
4413
|
+
const taskPayload = httpTaskRequestToTaskPayload(taskRequest, {
|
|
4414
|
+
text: effectiveText
|
|
4415
|
+
});
|
|
4416
|
+
if (!taskPayload) {
|
|
4417
|
+
api.logger.error(
|
|
4418
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no payload`
|
|
4419
|
+
);
|
|
4420
|
+
return;
|
|
4421
|
+
}
|
|
4422
|
+
const peerId = `sw:${taskRequest.session_id}:${kind}`;
|
|
4423
|
+
const sessionKey = buildAgentSessionKey({
|
|
4424
|
+
agentId: "default",
|
|
4425
|
+
channel: "stagewhisper",
|
|
4426
|
+
peer: { kind: "direct", id: peerId },
|
|
4427
|
+
dmScope: "per-channel-peer"
|
|
4428
|
+
});
|
|
4429
|
+
const chatIdReason = kind === "chat" ? "chat_message" : "transcript_chunk";
|
|
4430
|
+
const chatId = taskRequest.chat_id ?? buildChatId(taskRequest.session_id, chatIdReason);
|
|
4431
|
+
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4432
|
+
const sendCallback = async (body) => {
|
|
4433
|
+
recordTerminalCallback(taskRequest.task_id, callback, body);
|
|
4434
|
+
try {
|
|
4435
|
+
await postCallback(callback, taskRequest.task_id, body);
|
|
4436
|
+
} catch (err) {
|
|
4437
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4438
|
+
api.logger.error(
|
|
4439
|
+
`[stagewhisper-http] callback POST failed for ${taskRequest.task_id}: ${errMsg}`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
};
|
|
4443
|
+
try {
|
|
4444
|
+
const dispatch = await dispatchTaskToAgent({
|
|
4445
|
+
api,
|
|
4446
|
+
task: taskPayload,
|
|
4447
|
+
deliver: true,
|
|
4448
|
+
idempotencyKey: `sw-http-task-${taskRequest.task_id}`,
|
|
4449
|
+
sessionPeerId: peerId
|
|
4450
|
+
});
|
|
4451
|
+
api.logger.info(
|
|
4452
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} dispatched (runId: ${dispatch.runId})`
|
|
4453
|
+
);
|
|
4454
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
4455
|
+
runId: dispatch.runId,
|
|
4456
|
+
timeoutMs: SUBAGENT_WAIT_TIMEOUT_MS
|
|
4457
|
+
});
|
|
4458
|
+
if (waitResult.status === "ok") {
|
|
4459
|
+
const reply = await extractReplyForChatTask(
|
|
4460
|
+
sessionKey,
|
|
4461
|
+
taskRequest.task_id,
|
|
4462
|
+
userMessageId
|
|
4463
|
+
);
|
|
4464
|
+
if (reply) {
|
|
4465
|
+
await sendCallback({
|
|
4466
|
+
task_id: taskRequest.task_id,
|
|
4467
|
+
session_id: taskRequest.session_id,
|
|
4468
|
+
chat_id: chatId,
|
|
4469
|
+
user_message_id: userMessageId ?? null,
|
|
4470
|
+
status: "completed",
|
|
4471
|
+
reply_text: reply,
|
|
4472
|
+
occurred_at: occurredAt
|
|
4473
|
+
});
|
|
4474
|
+
api.logger.info(
|
|
4475
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} callback delivered`
|
|
4476
|
+
);
|
|
4477
|
+
} else if (kind === "reasoning") {
|
|
4478
|
+
await sendCallback({
|
|
4479
|
+
task_id: taskRequest.task_id,
|
|
4480
|
+
session_id: taskRequest.session_id,
|
|
4481
|
+
chat_id: chatId,
|
|
4482
|
+
user_message_id: userMessageId ?? null,
|
|
4483
|
+
status: "silent",
|
|
4484
|
+
occurred_at: occurredAt
|
|
4485
|
+
});
|
|
4486
|
+
api.logger.info(
|
|
4487
|
+
`[stagewhisper-http] reasoning task ${taskRequest.task_id} produced no signal (silent)`
|
|
4488
|
+
);
|
|
4489
|
+
} else {
|
|
4490
|
+
await sendCallback({
|
|
4491
|
+
task_id: taskRequest.task_id,
|
|
4492
|
+
session_id: taskRequest.session_id,
|
|
4493
|
+
chat_id: chatId,
|
|
4494
|
+
user_message_id: userMessageId ?? null,
|
|
4495
|
+
status: "errored",
|
|
4496
|
+
error_code: "no_reply",
|
|
4497
|
+
error_message: "Agent run produced no reply",
|
|
4498
|
+
occurred_at: occurredAt
|
|
4499
|
+
});
|
|
4500
|
+
api.logger.warn(
|
|
4501
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no reply`
|
|
4502
|
+
);
|
|
4503
|
+
}
|
|
4504
|
+
} else {
|
|
4505
|
+
await sendCallback({
|
|
4506
|
+
task_id: taskRequest.task_id,
|
|
4507
|
+
session_id: taskRequest.session_id,
|
|
4508
|
+
chat_id: chatId,
|
|
4509
|
+
user_message_id: userMessageId ?? null,
|
|
4510
|
+
status: "errored",
|
|
4511
|
+
error_code: "agent_error",
|
|
4512
|
+
error_message: waitResult.error ?? `agent run ${waitResult.status}`,
|
|
4513
|
+
occurred_at: occurredAt
|
|
4514
|
+
});
|
|
4515
|
+
api.logger.error(
|
|
4516
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} agent failed: ${waitResult.error}`
|
|
4517
|
+
);
|
|
4518
|
+
}
|
|
4519
|
+
} catch (err) {
|
|
4520
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4521
|
+
api.logger.error(
|
|
4522
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} threw: ${errMsg}`
|
|
4523
|
+
);
|
|
4524
|
+
await sendCallback({
|
|
4525
|
+
task_id: taskRequest.task_id,
|
|
4526
|
+
session_id: taskRequest.session_id,
|
|
4527
|
+
chat_id: chatId,
|
|
4528
|
+
user_message_id: userMessageId ?? null,
|
|
4529
|
+
status: "errored",
|
|
4530
|
+
error_code: "execution_error",
|
|
4531
|
+
error_message: errMsg,
|
|
4532
|
+
occurred_at: occurredAt
|
|
4533
|
+
});
|
|
4534
|
+
} finally {
|
|
4535
|
+
inflight.delete(taskRequest.task_id);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
async function handleChatMessageRequest(taskReq) {
|
|
4539
|
+
if (!taskReq.callback) {
|
|
4540
|
+
return jsonResponse(400, {
|
|
4541
|
+
error: "chat_message requires callback {url, token}"
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
|
|
4545
|
+
const response = {
|
|
4546
|
+
status: 202,
|
|
4547
|
+
headers: {
|
|
4548
|
+
"Content-Type": "application/json",
|
|
4549
|
+
"Content-Length": String(Buffer2.byteLength(ackBody))
|
|
4550
|
+
},
|
|
4551
|
+
body: ackBody
|
|
4552
|
+
};
|
|
4553
|
+
rememberResult(taskReq.task_id, response.status, response.body);
|
|
4554
|
+
const task = runReplyTaskAsync(taskReq, "chat");
|
|
4555
|
+
chatTasks.set(taskReq.task_id, task);
|
|
4556
|
+
void task.finally(() => {
|
|
4557
|
+
if (chatTasks.get(taskReq.task_id) === task) {
|
|
4558
|
+
chatTasks.delete(taskReq.task_id);
|
|
4559
|
+
}
|
|
4560
|
+
});
|
|
4561
|
+
return response;
|
|
4562
|
+
}
|
|
4563
|
+
async function handleTranscriptChunkRequest(taskReq) {
|
|
4564
|
+
if (taskReq.payload.is_final !== true) {
|
|
4565
|
+
const response2 = jsonResponse(202, {
|
|
4566
|
+
status: "accepted",
|
|
4567
|
+
task_id: taskReq.task_id,
|
|
4568
|
+
dispatched: false
|
|
4569
|
+
});
|
|
4570
|
+
rememberResult(taskReq.task_id, response2.status, response2.body);
|
|
4571
|
+
inflight.delete(taskReq.task_id);
|
|
4572
|
+
return response2;
|
|
4573
|
+
}
|
|
4574
|
+
if (!taskReq.callback) {
|
|
4575
|
+
try {
|
|
4576
|
+
const effectiveText = applyPrelude(
|
|
4577
|
+
taskReq.payload.text,
|
|
4578
|
+
consumePrelude(taskReq.session_id)
|
|
4579
|
+
);
|
|
4580
|
+
const taskPayload = httpTaskRequestToTaskPayload(taskReq, {
|
|
4581
|
+
text: effectiveText
|
|
4582
|
+
});
|
|
4583
|
+
if (!taskPayload) {
|
|
4584
|
+
return jsonResponse(400, { error: "invalid_payload_for_reason" });
|
|
4585
|
+
}
|
|
4586
|
+
const reasoningPeerId = `sw:${taskReq.session_id}:reasoning`;
|
|
4587
|
+
const dispatch = await dispatchTaskToAgent({
|
|
4588
|
+
api,
|
|
4589
|
+
task: taskPayload,
|
|
4590
|
+
deliver: true,
|
|
4591
|
+
idempotencyKey: `sw-http-task-${taskReq.task_id}`,
|
|
4592
|
+
sessionPeerId: reasoningPeerId
|
|
4593
|
+
});
|
|
4594
|
+
api.logger.info(
|
|
4595
|
+
`[stagewhisper-http] dispatched one-way task ${taskReq.task_id} (runId: ${dispatch.runId})`
|
|
4596
|
+
);
|
|
4597
|
+
const response2 = jsonResponse(200, {
|
|
4598
|
+
status: "accepted",
|
|
4599
|
+
task_id: taskReq.task_id,
|
|
4600
|
+
run_id: dispatch.runId,
|
|
4601
|
+
dispatched: true
|
|
4602
|
+
});
|
|
4603
|
+
rememberResult(taskReq.task_id, response2.status, response2.body);
|
|
4604
|
+
return response2;
|
|
4605
|
+
} catch (err) {
|
|
4606
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4607
|
+
api.logger.error(
|
|
4608
|
+
`[stagewhisper-http] dispatch failed for ${taskReq.task_id}: ${errMsg}`
|
|
4609
|
+
);
|
|
4610
|
+
return jsonResponse(503, { error: "dispatch_failed", detail: errMsg });
|
|
4611
|
+
} finally {
|
|
4612
|
+
inflight.delete(taskReq.task_id);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
|
|
4616
|
+
const response = {
|
|
4617
|
+
status: 202,
|
|
4618
|
+
headers: {
|
|
4619
|
+
"Content-Type": "application/json",
|
|
4620
|
+
"Content-Length": String(Buffer2.byteLength(ackBody))
|
|
4621
|
+
},
|
|
4622
|
+
body: ackBody
|
|
4623
|
+
};
|
|
4624
|
+
rememberResult(taskReq.task_id, response.status, response.body);
|
|
4625
|
+
const task = runReplyTaskAsync(taskReq, "reasoning");
|
|
4626
|
+
chatTasks.set(taskReq.task_id, task);
|
|
4627
|
+
void task.finally(() => {
|
|
4628
|
+
if (chatTasks.get(taskReq.task_id) === task) {
|
|
4629
|
+
chatTasks.delete(taskReq.task_id);
|
|
4630
|
+
}
|
|
4631
|
+
});
|
|
4632
|
+
return response;
|
|
4633
|
+
}
|
|
4634
|
+
function handleSystemPreludeRequest(taskReq) {
|
|
4635
|
+
pendingPreludes.set(taskReq.session_id, taskReq.payload.text);
|
|
4636
|
+
const response = jsonResponse(202, {
|
|
4637
|
+
status: "accepted",
|
|
4638
|
+
task_id: taskReq.task_id
|
|
4639
|
+
});
|
|
4640
|
+
rememberResult(taskReq.task_id, response.status, response.body);
|
|
4641
|
+
inflight.delete(taskReq.task_id);
|
|
4642
|
+
return response;
|
|
4643
|
+
}
|
|
4644
|
+
async function dispatchRoute(req) {
|
|
4645
|
+
if (!isLoopbackAddress(req.remoteAddress)) {
|
|
4646
|
+
api.logger.warn(
|
|
4647
|
+
`[stagewhisper-http] rejecting non-loopback connection from ${req.remoteAddress}`
|
|
4648
|
+
);
|
|
4649
|
+
return jsonResponse(403, { error: "non_loopback_rejected" });
|
|
4650
|
+
}
|
|
4651
|
+
if (!isAllowedHostHeader(req.host)) {
|
|
4652
|
+
api.logger.warn(
|
|
4653
|
+
`[stagewhisper-http] rejecting disallowed Host header: ${req.host ?? "<missing>"}`
|
|
4654
|
+
);
|
|
4655
|
+
return jsonResponse(403, { error: "invalid_host" });
|
|
4656
|
+
}
|
|
4657
|
+
const providedToken = extractBearerToken(req.authorization);
|
|
4658
|
+
if (!providedToken || !constantTimeTokenEqual(providedToken, token)) {
|
|
4659
|
+
return jsonResponse(401, { error: "invalid_token" });
|
|
4660
|
+
}
|
|
4661
|
+
if (req.method === "POST" && req.url === PING_PATH) {
|
|
4662
|
+
return jsonResponse(200, { ok: true });
|
|
4663
|
+
}
|
|
4664
|
+
if (req.method === "POST" && req.url === INCOMING_PATH) {
|
|
4665
|
+
let parsed;
|
|
4666
|
+
try {
|
|
4667
|
+
parsed = req.body.length === 0 ? {} : JSON.parse(req.body);
|
|
4668
|
+
} catch (err) {
|
|
4669
|
+
return jsonResponse(400, {
|
|
4670
|
+
error: `invalid_json: ${err instanceof Error ? err.message : String(err)}`
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
const validation = validateHttpTaskRequest(parsed);
|
|
4674
|
+
if (!validation.ok) {
|
|
4675
|
+
return jsonResponse(400, { error: validation.error });
|
|
4676
|
+
}
|
|
4677
|
+
const taskReq = validation.req;
|
|
4678
|
+
evictExpired();
|
|
4679
|
+
const cached = idempotency.get(taskReq.task_id);
|
|
4680
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
4681
|
+
if (cached.terminal) {
|
|
4682
|
+
const terminal = cached.terminal;
|
|
4683
|
+
const redelivery = redeliverTerminalCallback(terminal, taskReq.task_id);
|
|
4684
|
+
chatTasks.set(taskReq.task_id, redelivery);
|
|
4685
|
+
void redelivery.finally(() => {
|
|
4686
|
+
if (chatTasks.get(taskReq.task_id) === redelivery) {
|
|
4687
|
+
chatTasks.delete(taskReq.task_id);
|
|
4688
|
+
}
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4691
|
+
return {
|
|
4692
|
+
status: cached.status,
|
|
4693
|
+
headers: {
|
|
4694
|
+
"Content-Type": "application/json",
|
|
4695
|
+
"Content-Length": String(Buffer2.byteLength(cached.body)),
|
|
4696
|
+
"X-Idempotent-Replay": "true"
|
|
4697
|
+
},
|
|
4698
|
+
body: cached.body
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4701
|
+
if (inflight.has(taskReq.task_id)) {
|
|
4702
|
+
return jsonResponse(503, { error: "task_in_flight" });
|
|
4703
|
+
}
|
|
4704
|
+
inflight.add(taskReq.task_id);
|
|
4705
|
+
if (taskReq.reason === "chat_message") {
|
|
4706
|
+
return handleChatMessageRequest(taskReq);
|
|
4707
|
+
}
|
|
4708
|
+
if (taskReq.reason === "system_prelude") {
|
|
4709
|
+
return handleSystemPreludeRequest(taskReq);
|
|
4710
|
+
}
|
|
4711
|
+
return handleTranscriptChunkRequest(taskReq);
|
|
4712
|
+
}
|
|
4713
|
+
return jsonResponse(404, { error: "not_found" });
|
|
4714
|
+
}
|
|
4715
|
+
async function handleRealRequest(req, res) {
|
|
4716
|
+
const method = req.method ?? "";
|
|
4717
|
+
const url = req.url ?? "";
|
|
4718
|
+
const authHeader = req.headers["authorization"];
|
|
4719
|
+
const authorization = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
4720
|
+
const hostHeader = req.headers["host"];
|
|
4721
|
+
const hostValue = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
4722
|
+
const remoteAddress = req.socket.remoteAddress;
|
|
4723
|
+
let body = "";
|
|
4724
|
+
if (method === "POST") {
|
|
4725
|
+
try {
|
|
4726
|
+
body = await readBody(req, MAX_BODY_BYTES);
|
|
4727
|
+
} catch (err) {
|
|
4728
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4729
|
+
const response = msg === "body_too_large" ? jsonResponse(413, { error: "body_too_large" }) : jsonResponse(400, { error: `invalid_body: ${msg}` });
|
|
4730
|
+
writeResponse(res, response);
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
const result = await dispatchRoute({
|
|
4735
|
+
method,
|
|
4736
|
+
url,
|
|
4737
|
+
authorization,
|
|
4738
|
+
host: hostValue,
|
|
4739
|
+
remoteAddress,
|
|
4740
|
+
body
|
|
4741
|
+
});
|
|
4742
|
+
writeResponse(res, result);
|
|
4743
|
+
}
|
|
4744
|
+
function writeResponse(res, response) {
|
|
4745
|
+
res.statusCode = response.status;
|
|
4746
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
4747
|
+
res.setHeader(key, value);
|
|
4748
|
+
}
|
|
4749
|
+
res.end(response.body);
|
|
4750
|
+
}
|
|
4751
|
+
let server = null;
|
|
4752
|
+
return {
|
|
4753
|
+
async start() {
|
|
4754
|
+
if (server) return;
|
|
4755
|
+
const srv = http.createServer((req, res) => {
|
|
4756
|
+
handleRealRequest(req, res).catch((err) => {
|
|
4757
|
+
api.logger.error(`[stagewhisper-http] unhandled error: ${err}`);
|
|
4758
|
+
if (!res.headersSent) {
|
|
4759
|
+
try {
|
|
4760
|
+
writeResponse(res, jsonResponse(500, { error: "internal_error" }));
|
|
4761
|
+
} catch {
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
});
|
|
4765
|
+
});
|
|
4766
|
+
server = srv;
|
|
4767
|
+
await new Promise((resolve, reject) => {
|
|
4768
|
+
const onError = (err) => {
|
|
4769
|
+
srv.off("listening", onListening);
|
|
4770
|
+
reject(err);
|
|
4771
|
+
};
|
|
4772
|
+
const onListening = () => {
|
|
4773
|
+
srv.off("error", onError);
|
|
4774
|
+
resolve();
|
|
4775
|
+
};
|
|
4776
|
+
srv.once("error", onError);
|
|
4777
|
+
srv.once("listening", onListening);
|
|
4778
|
+
srv.listen(port, host);
|
|
4779
|
+
});
|
|
4780
|
+
const addr = srv.address();
|
|
4781
|
+
api.logger.info(
|
|
4782
|
+
`[stagewhisper-http] listening on http://${host}:${typeof addr === "object" && addr ? addr.port : port}`
|
|
4783
|
+
);
|
|
4784
|
+
},
|
|
4785
|
+
async stop() {
|
|
4786
|
+
const srv = server;
|
|
4787
|
+
if (!srv) return;
|
|
4788
|
+
server = null;
|
|
4789
|
+
await new Promise((resolve) => {
|
|
4790
|
+
srv.close(() => resolve());
|
|
4791
|
+
srv.closeAllConnections?.();
|
|
4792
|
+
});
|
|
4793
|
+
const pending = Array.from(chatTasks.values());
|
|
4794
|
+
if (pending.length > 0) {
|
|
4795
|
+
await Promise.allSettled(pending);
|
|
4796
|
+
}
|
|
4797
|
+
idempotency.clear();
|
|
4798
|
+
inflight.clear();
|
|
4799
|
+
chatTasks.clear();
|
|
4800
|
+
pendingPreludes.clear();
|
|
4801
|
+
api.logger.info("[stagewhisper-http] stopped");
|
|
4802
|
+
},
|
|
4803
|
+
address() {
|
|
4804
|
+
const addr = server?.address();
|
|
4805
|
+
if (typeof addr === "object" && addr !== null) return addr;
|
|
4806
|
+
return null;
|
|
4807
|
+
},
|
|
4808
|
+
async handleSyntheticRequest(input) {
|
|
4809
|
+
const headers = input.headers ?? {};
|
|
4810
|
+
const authorization = headers["Authorization"] ?? headers["authorization"] ?? void 0;
|
|
4811
|
+
const hostValue = headers["Host"] ?? headers["host"] ?? "127.0.0.1";
|
|
4812
|
+
const remoteAddress = input.remoteAddress ?? "127.0.0.1";
|
|
4813
|
+
const body = input.body ?? "";
|
|
4814
|
+
return dispatchRoute({
|
|
4815
|
+
method: input.method,
|
|
4816
|
+
url: input.url,
|
|
4817
|
+
authorization,
|
|
4818
|
+
host: hostValue,
|
|
4819
|
+
remoteAddress,
|
|
4820
|
+
body
|
|
4821
|
+
});
|
|
4822
|
+
},
|
|
4823
|
+
whenChatTaskSettled(taskId) {
|
|
4824
|
+
return chatTasks.get(taskId);
|
|
4825
|
+
}
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
// src/pairing.ts
|
|
4830
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
4831
|
+
import { randomBytes } from "node:crypto";
|
|
4832
|
+
var PAIRING_CODE_PREFIX = "stagewhisper-pair:v1:";
|
|
4833
|
+
function encodePairingCode(url, token, label) {
|
|
4834
|
+
const payload = JSON.stringify({ url, token, label });
|
|
4835
|
+
const encoded = Buffer3.from(payload, "utf8").toString("base64url");
|
|
4836
|
+
return `${PAIRING_CODE_PREFIX}${encoded}`;
|
|
4837
|
+
}
|
|
4838
|
+
function generateRelayToken() {
|
|
4839
|
+
return randomBytes(32).toString("base64url");
|
|
4840
|
+
}
|
|
4841
|
+
|
|
3990
4842
|
// src/runtime.ts
|
|
3991
4843
|
var runtimeStore = createPluginRuntimeStore(
|
|
3992
4844
|
"StageWhisper plugin runtime not initialized"
|
|
@@ -4278,41 +5130,13 @@ function createRelayService(api) {
|
|
|
4278
5130
|
};
|
|
4279
5131
|
}
|
|
4280
5132
|
}
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
lines.push(`**${task.title}**`);
|
|
4284
|
-
lines.push("");
|
|
4285
|
-
lines.push(task.request_text);
|
|
4286
|
-
if (task.evidence_payload) {
|
|
4287
|
-
const evidence = task.evidence_payload;
|
|
4288
|
-
if (evidence["transcript_excerpt"]) {
|
|
4289
|
-
lines.push("");
|
|
4290
|
-
lines.push(`Context: ${evidence["transcript_excerpt"]}`);
|
|
4291
|
-
}
|
|
4292
|
-
if (evidence["signal_summary"]) {
|
|
4293
|
-
lines.push(`Signal: ${evidence["signal_summary"]}`);
|
|
4294
|
-
}
|
|
4295
|
-
if (evidence["tone_summary"]) {
|
|
4296
|
-
lines.push(`Tone: ${evidence["tone_summary"]}`);
|
|
4297
|
-
}
|
|
4298
|
-
if (evidence["playbook_label"]) {
|
|
4299
|
-
lines.push(`Playbook: ${evidence["playbook_label"]}`);
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4302
|
-
lines.push("");
|
|
4303
|
-
lines.push(`Action type: ${task.action_type}`);
|
|
4304
|
-
lines.push(`StageWhisper task: ${task.id}`);
|
|
4305
|
-
lines.push(`Session: ${task.session_id}`);
|
|
4306
|
-
return lines.join("\n");
|
|
4307
|
-
}
|
|
4308
|
-
function isTestTask(task) {
|
|
4309
|
-
return task.action_type === "connectivity_test";
|
|
4310
|
-
}
|
|
5133
|
+
const buildTaskMessage2 = buildTaskMessage;
|
|
5134
|
+
const isTestTask2 = isTestTask;
|
|
4311
5135
|
async function updateStatus(client, task, status) {
|
|
4312
|
-
if (
|
|
5136
|
+
if (isTestTask2(task)) return;
|
|
4313
5137
|
await client.updateTaskStatus(task.id, status);
|
|
4314
5138
|
}
|
|
4315
|
-
function
|
|
5139
|
+
function extractContentFromMessage2(msg) {
|
|
4316
5140
|
const content = msg["content"];
|
|
4317
5141
|
if (typeof content === "string") return content;
|
|
4318
5142
|
if (Array.isArray(content)) {
|
|
@@ -4330,7 +5154,7 @@ function createRelayService(api) {
|
|
|
4330
5154
|
if (!msg) continue;
|
|
4331
5155
|
const role = msg["role"];
|
|
4332
5156
|
if (role !== "assistant" && role !== "model") continue;
|
|
4333
|
-
return
|
|
5157
|
+
return extractContentFromMessage2(msg);
|
|
4334
5158
|
}
|
|
4335
5159
|
return null;
|
|
4336
5160
|
}
|
|
@@ -4381,13 +5205,13 @@ function createRelayService(api) {
|
|
|
4381
5205
|
for (let i = 0; i < messages.length; i++) {
|
|
4382
5206
|
const msg = messages[i];
|
|
4383
5207
|
if (msg["role"] !== "user") continue;
|
|
4384
|
-
const text =
|
|
5208
|
+
const text = extractContentFromMessage2(msg) ?? "";
|
|
4385
5209
|
if (!text.includes(`StageWhisper task: ${taskId}`)) continue;
|
|
4386
5210
|
for (let j = i + 1; j < messages.length; j++) {
|
|
4387
5211
|
const reply = messages[j];
|
|
4388
5212
|
const role = reply["role"];
|
|
4389
5213
|
if (role === "assistant" || role === "model") {
|
|
4390
|
-
return
|
|
5214
|
+
return extractContentFromMessage2(reply);
|
|
4391
5215
|
}
|
|
4392
5216
|
if (role === "user") break;
|
|
4393
5217
|
}
|
|
@@ -4423,13 +5247,13 @@ function createRelayService(api) {
|
|
|
4423
5247
|
for (let i = 0; i < messages.length; i++) {
|
|
4424
5248
|
const msg = messages[i];
|
|
4425
5249
|
if (msg["role"] !== "user") continue;
|
|
4426
|
-
const text =
|
|
5250
|
+
const text = extractContentFromMessage2(msg) ?? "";
|
|
4427
5251
|
if (!text.includes(marker)) continue;
|
|
4428
5252
|
for (let j = i + 1; j < messages.length; j++) {
|
|
4429
5253
|
const next = messages[j];
|
|
4430
5254
|
const role = next["role"];
|
|
4431
5255
|
if (role === "assistant" || role === "model") {
|
|
4432
|
-
return
|
|
5256
|
+
return extractContentFromMessage2(next);
|
|
4433
5257
|
}
|
|
4434
5258
|
if (role === "user") break;
|
|
4435
5259
|
}
|
|
@@ -4505,7 +5329,7 @@ function createRelayService(api) {
|
|
|
4505
5329
|
}
|
|
4506
5330
|
const isByo = !!task.is_byo_encrypted;
|
|
4507
5331
|
const effectiveTask = isByo ? decryptTaskFields(task, client) : task;
|
|
4508
|
-
const messageContent =
|
|
5332
|
+
const messageContent = buildTaskMessage2(effectiveTask);
|
|
4509
5333
|
const peerId = `sw-session-${task.session_id}`;
|
|
4510
5334
|
const sessionKey = buildAgentSessionKey({
|
|
4511
5335
|
agentId: "default",
|
|
@@ -4548,7 +5372,7 @@ function createRelayService(api) {
|
|
|
4548
5372
|
});
|
|
4549
5373
|
}
|
|
4550
5374
|
async function handleTestTask(task, client) {
|
|
4551
|
-
const messageContent =
|
|
5375
|
+
const messageContent = buildTaskMessage2(task);
|
|
4552
5376
|
const sessionKey = buildAgentSessionKey({
|
|
4553
5377
|
agentId: "default",
|
|
4554
5378
|
channel: "stagewhisper",
|
|
@@ -4581,7 +5405,7 @@ function createRelayService(api) {
|
|
|
4581
5405
|
async function handleTask(task, client) {
|
|
4582
5406
|
api.logger.info(`Received task: ${task.title} (${task.id})`);
|
|
4583
5407
|
try {
|
|
4584
|
-
if (
|
|
5408
|
+
if (isTestTask2(task)) {
|
|
4585
5409
|
await handleTestTask(task, client);
|
|
4586
5410
|
} else {
|
|
4587
5411
|
await handleNormalTask(task, client);
|
|
@@ -5411,20 +6235,73 @@ function createRelayService(api) {
|
|
|
5411
6235
|
}
|
|
5412
6236
|
|
|
5413
6237
|
// plugin-main.ts
|
|
6238
|
+
function resolveTransportMode(api) {
|
|
6239
|
+
const pluginCfg = api.pluginConfig ?? {};
|
|
6240
|
+
const fromConfig = pluginCfg["transport"];
|
|
6241
|
+
if (fromConfig === "http" || fromConfig === "sse") return fromConfig;
|
|
6242
|
+
const envOverride = process.env["STAGEWHISPER_TRANSPORT"];
|
|
6243
|
+
if (envOverride === "http" || envOverride === "sse") return envOverride;
|
|
6244
|
+
return "sse";
|
|
6245
|
+
}
|
|
6246
|
+
function resolveHttpTransportToken(pluginCfg, env = process.env) {
|
|
6247
|
+
if (typeof pluginCfg["httpToken"] === "string" && pluginCfg["httpToken"]) {
|
|
6248
|
+
return pluginCfg["httpToken"];
|
|
6249
|
+
}
|
|
6250
|
+
if (env["STAGEWHISPER_HTTP_TOKEN"]) {
|
|
6251
|
+
return env["STAGEWHISPER_HTTP_TOKEN"];
|
|
6252
|
+
}
|
|
6253
|
+
if (typeof pluginCfg["relayToken"] === "string") {
|
|
6254
|
+
return pluginCfg["relayToken"];
|
|
6255
|
+
}
|
|
6256
|
+
return "";
|
|
6257
|
+
}
|
|
6258
|
+
function createHttpTransportService(api) {
|
|
6259
|
+
const pluginCfg = api.pluginConfig ?? {};
|
|
6260
|
+
const host = typeof pluginCfg["httpHost"] === "string" ? pluginCfg["httpHost"] : process.env["STAGEWHISPER_HTTP_HOST"] ?? "127.0.0.1";
|
|
6261
|
+
const portRaw = pluginCfg["httpPort"];
|
|
6262
|
+
const port = typeof portRaw === "number" ? portRaw : typeof portRaw === "string" ? Number(portRaw) : Number(process.env["STAGEWHISPER_HTTP_PORT"]) || 8765;
|
|
6263
|
+
const token = resolveHttpTransportToken(pluginCfg);
|
|
6264
|
+
let transport = null;
|
|
6265
|
+
return {
|
|
6266
|
+
id: "stagewhisper-http-transport",
|
|
6267
|
+
async start(_ctx) {
|
|
6268
|
+
if (!token || token.length < 16) {
|
|
6269
|
+
api.logger.warn(
|
|
6270
|
+
"StageWhisper HTTP transport requires `httpToken` (>=16 chars) in plugin config or STAGEWHISPER_HTTP_TOKEN env var \u2014 listener not started."
|
|
6271
|
+
);
|
|
6272
|
+
return;
|
|
6273
|
+
}
|
|
6274
|
+
transport = createHttpTransport({ api, host, port, token });
|
|
6275
|
+
try {
|
|
6276
|
+
await transport.start();
|
|
6277
|
+
api.logger.info(`StageWhisper HTTP transport started on ${host}:${port}`);
|
|
6278
|
+
} catch (err) {
|
|
6279
|
+
api.logger.error(`StageWhisper HTTP transport failed to start: ${err}`);
|
|
6280
|
+
transport = null;
|
|
6281
|
+
}
|
|
6282
|
+
},
|
|
6283
|
+
async stop(_ctx) {
|
|
6284
|
+
if (transport) {
|
|
6285
|
+
await transport.stop();
|
|
6286
|
+
transport = null;
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
};
|
|
6290
|
+
}
|
|
5414
6291
|
async function ensureResponsesEndpoint(api) {
|
|
5415
6292
|
try {
|
|
5416
6293
|
const cfg = await api.runtime.config.loadConfig();
|
|
5417
6294
|
const gw = cfg["gateway"] ?? {};
|
|
5418
|
-
const
|
|
5419
|
-
const endpoints =
|
|
6295
|
+
const http2 = gw["http"] ?? {};
|
|
6296
|
+
const endpoints = http2["endpoints"] ?? {};
|
|
5420
6297
|
const responses = endpoints["responses"] ?? {};
|
|
5421
6298
|
if (responses["enabled"] === true) return;
|
|
5422
6299
|
const auth = gw["auth"] ?? {};
|
|
5423
6300
|
if (auth["mode"] === "none" && !auth["token"] && !auth["password"]) return;
|
|
5424
6301
|
responses["enabled"] = true;
|
|
5425
6302
|
endpoints["responses"] = responses;
|
|
5426
|
-
|
|
5427
|
-
gw["http"] =
|
|
6303
|
+
http2["endpoints"] = endpoints;
|
|
6304
|
+
gw["http"] = http2;
|
|
5428
6305
|
cfg["gateway"] = gw;
|
|
5429
6306
|
await api.runtime.config.writeConfigFile(cfg);
|
|
5430
6307
|
api.logger.info(
|
|
@@ -5442,13 +6319,7 @@ var plugin_main_default = definePluginEntry({
|
|
|
5442
6319
|
api.registerCli(
|
|
5443
6320
|
({ program }) => {
|
|
5444
6321
|
const sw = program.command("stagewhisper").description("StageWhisper integration");
|
|
5445
|
-
sw.command("pair").description(
|
|
5446
|
-
"Pair with StageWhisper using a pairing code from the desktop app"
|
|
5447
|
-
).requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option(
|
|
5448
|
-
"--api-url <url>",
|
|
5449
|
-
"StageWhisper backend URL",
|
|
5450
|
-
"https://api.stagewhisper.io"
|
|
5451
|
-
).option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
|
|
6322
|
+
sw.command("pair").description("Pair with StageWhisper using a pairing code from the desktop app").requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option("--api-url <url>", "StageWhisper backend URL", "https://api.stagewhisper.io").option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
|
|
5452
6323
|
async (opts) => {
|
|
5453
6324
|
const { StageWhisperClient: StageWhisperClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
5454
6325
|
const { IdentityKeypair: IdentityKeypair2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
@@ -5493,26 +6364,33 @@ var plugin_main_default = definePluginEntry({
|
|
|
5493
6364
|
const authMode = gwAuth["mode"];
|
|
5494
6365
|
const hasToken = typeof gwAuth["token"] === "string" && gwAuth["token"].length > 0;
|
|
5495
6366
|
if (authMode === "none" && !hasToken) {
|
|
5496
|
-
console.warn(
|
|
6367
|
+
console.warn(
|
|
6368
|
+
" \u26A0 gateway.auth.mode is 'none' with no token \u2014 skipping HTTP API enablement."
|
|
6369
|
+
);
|
|
5497
6370
|
console.warn(" Set a gateway auth token first for reasoning to work.\n");
|
|
5498
6371
|
} else {
|
|
5499
|
-
const
|
|
5500
|
-
const endpoints =
|
|
6372
|
+
const http2 = gw["http"] ?? {};
|
|
6373
|
+
const endpoints = http2["endpoints"] ?? {};
|
|
5501
6374
|
const responses = endpoints["responses"] ?? {};
|
|
5502
6375
|
responses["enabled"] = true;
|
|
5503
6376
|
endpoints["responses"] = responses;
|
|
5504
|
-
|
|
5505
|
-
gw["http"] =
|
|
6377
|
+
http2["endpoints"] = endpoints;
|
|
6378
|
+
gw["http"] = http2;
|
|
5506
6379
|
cfg["gateway"] = gw;
|
|
5507
6380
|
}
|
|
5508
6381
|
}
|
|
5509
6382
|
await api.runtime.config.writeConfigFile(cfg);
|
|
5510
|
-
console.log(
|
|
5511
|
-
|
|
5512
|
-
\u2713 Paired with StageWhisper (${result.label})`
|
|
5513
|
-
);
|
|
6383
|
+
console.log(`
|
|
6384
|
+
\u2713 Paired with StageWhisper (${result.label})`);
|
|
5514
6385
|
console.log(" Config saved. Restart the gateway to activate:\n");
|
|
5515
6386
|
console.log(" openclaw gateway restart\n");
|
|
6387
|
+
console.log(
|
|
6388
|
+
" For local HTTP transport (transport: http), the desktop authenticates"
|
|
6389
|
+
);
|
|
6390
|
+
console.log(
|
|
6391
|
+
" against the loopback listener with this same relay token \u2014 no separate"
|
|
6392
|
+
);
|
|
6393
|
+
console.log(" httpToken is required unless you set one explicitly.\n");
|
|
5516
6394
|
} catch (err) {
|
|
5517
6395
|
console.error(`
|
|
5518
6396
|
\u2717 Pairing failed: ${err}
|
|
@@ -5521,9 +6399,62 @@ var plugin_main_default = definePluginEntry({
|
|
|
5521
6399
|
}
|
|
5522
6400
|
}
|
|
5523
6401
|
);
|
|
5524
|
-
sw.command("
|
|
5525
|
-
"
|
|
5526
|
-
).option("--
|
|
6402
|
+
sw.command("pair-code").description(
|
|
6403
|
+
"Generate a StageWhisper Free pairing code for the local HTTP transport (no backend)"
|
|
6404
|
+
).option("--url <url>", "Relay URL StageWhisper Free should reach this gateway at").option("--port <port>", "Loopback port for the HTTP transport listener").option("--label <label>", "Label shown in StageWhisper Free", "OpenClaw").action(async (opts) => {
|
|
6405
|
+
try {
|
|
6406
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
6407
|
+
const plugins = cfg["plugins"] ?? {};
|
|
6408
|
+
const entries = plugins["entries"] ?? {};
|
|
6409
|
+
const swEntry = entries["stagewhisper"] ?? {};
|
|
6410
|
+
const swConfig = swEntry["config"] ?? {};
|
|
6411
|
+
const host = typeof swConfig["httpHost"] === "string" && swConfig["httpHost"] ? swConfig["httpHost"] : "127.0.0.1";
|
|
6412
|
+
const port = opts.port ? Number(opts.port) : typeof swConfig["httpPort"] === "number" ? swConfig["httpPort"] : 8765;
|
|
6413
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
6414
|
+
console.error(`
|
|
6415
|
+
\u2717 Invalid port ${port} (must be 1024-65535)
|
|
6416
|
+
`);
|
|
6417
|
+
process.exit(1);
|
|
6418
|
+
}
|
|
6419
|
+
let token = typeof swConfig["httpToken"] === "string" ? swConfig["httpToken"] : "";
|
|
6420
|
+
if (token.length < 16) {
|
|
6421
|
+
token = generateRelayToken();
|
|
6422
|
+
}
|
|
6423
|
+
const label = (opts.label ?? swConfig["label"] ?? "OpenClaw").trim() || "OpenClaw";
|
|
6424
|
+
swConfig["transport"] = "http";
|
|
6425
|
+
swConfig["httpHost"] = host;
|
|
6426
|
+
swConfig["httpPort"] = port;
|
|
6427
|
+
swConfig["httpToken"] = token;
|
|
6428
|
+
swConfig["label"] = label;
|
|
6429
|
+
swEntry["config"] = swConfig;
|
|
6430
|
+
entries["stagewhisper"] = swEntry;
|
|
6431
|
+
plugins["entries"] = entries;
|
|
6432
|
+
cfg["plugins"] = plugins;
|
|
6433
|
+
await api.runtime.config.writeConfigFile(cfg);
|
|
6434
|
+
const relayUrl = (opts.url ?? "").trim() || `http://${host}:${port}`;
|
|
6435
|
+
const code = encodePairingCode(relayUrl, token, label);
|
|
6436
|
+
console.log("\nStageWhisper Free pairing code:\n");
|
|
6437
|
+
console.log(` ${code}
|
|
6438
|
+
`);
|
|
6439
|
+
console.log("Paste it into StageWhisper Free under Settings \u2192 Connection.");
|
|
6440
|
+
console.log(
|
|
6441
|
+
"Restart the gateway so the HTTP transport listens: openclaw gateway restart"
|
|
6442
|
+
);
|
|
6443
|
+
if (relayUrl.startsWith("http://127.0.0.1")) {
|
|
6444
|
+
console.log(
|
|
6445
|
+
"\nRunning on a remote host? Tunnel the port from the machine running Free:"
|
|
6446
|
+
);
|
|
6447
|
+
console.log(` ssh -L ${port}:127.0.0.1:${port} <this-host>
|
|
6448
|
+
`);
|
|
6449
|
+
}
|
|
6450
|
+
} catch (err) {
|
|
6451
|
+
console.error(`
|
|
6452
|
+
\u2717 Failed to generate pairing code: ${err}
|
|
6453
|
+
`);
|
|
6454
|
+
process.exit(1);
|
|
6455
|
+
}
|
|
6456
|
+
});
|
|
6457
|
+
sw.command("unpair").description("Remove StageWhisper pairing (run before `openclaw plugins uninstall`)").option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair").action(async (opts) => {
|
|
5527
6458
|
try {
|
|
5528
6459
|
const cfg = await api.runtime.config.loadConfig();
|
|
5529
6460
|
const plugins = cfg["plugins"] ?? {};
|
|
@@ -5541,15 +6472,19 @@ var plugin_main_default = definePluginEntry({
|
|
|
5541
6472
|
}
|
|
5542
6473
|
if (!opts.keepResponses) {
|
|
5543
6474
|
const gw = cfg["gateway"];
|
|
5544
|
-
const
|
|
5545
|
-
const endpoints =
|
|
6475
|
+
const http2 = gw?.["http"];
|
|
6476
|
+
const endpoints = http2?.["endpoints"];
|
|
5546
6477
|
const responses = endpoints?.["responses"];
|
|
5547
6478
|
if (responses?.["enabled"] === true) {
|
|
5548
6479
|
delete responses["enabled"];
|
|
5549
|
-
if (Object.keys(responses).length === 0 && endpoints)
|
|
5550
|
-
|
|
5551
|
-
if (
|
|
5552
|
-
|
|
6480
|
+
if (Object.keys(responses).length === 0 && endpoints)
|
|
6481
|
+
delete endpoints["responses"];
|
|
6482
|
+
if (endpoints && Object.keys(endpoints).length === 0 && http2)
|
|
6483
|
+
delete http2["endpoints"];
|
|
6484
|
+
if (http2 && Object.keys(http2).length === 0 && gw) delete gw["http"];
|
|
6485
|
+
console.log(
|
|
6486
|
+
" \u2139 Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it."
|
|
6487
|
+
);
|
|
5553
6488
|
}
|
|
5554
6489
|
}
|
|
5555
6490
|
await api.runtime.config.writeConfigFile(cfg);
|
|
@@ -5563,7 +6498,11 @@ var plugin_main_default = definePluginEntry({
|
|
|
5563
6498
|
process.exit(1);
|
|
5564
6499
|
}
|
|
5565
6500
|
});
|
|
5566
|
-
sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option(
|
|
6501
|
+
sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option(
|
|
6502
|
+
"--model <model>",
|
|
6503
|
+
"Model to use (omit to use your configured default)",
|
|
6504
|
+
"openclaw/default"
|
|
6505
|
+
).action(async (opts) => {
|
|
5567
6506
|
const { callOpenResponses: callOpenResponses2, isResponsesEndpointEnabled: isResponsesEndpointEnabled2 } = await Promise.resolve().then(() => (init_openresponses(), openresponses_exports));
|
|
5568
6507
|
const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
|
|
5569
6508
|
const cfg = api.config;
|
|
@@ -5578,7 +6517,9 @@ var plugin_main_default = definePluginEntry({
|
|
|
5578
6517
|
console.log(` responses.enabled: ${responsesEnabled ? "\u2713 true" : "\u2717 false"}`);
|
|
5579
6518
|
if (!responsesEnabled) {
|
|
5580
6519
|
console.warn("\n\u26A0 responses.enabled is false in the running config.");
|
|
5581
|
-
console.warn(
|
|
6520
|
+
console.warn(
|
|
6521
|
+
" The plugin auto-enables it on startup \u2014 restart the gateway if you haven't:"
|
|
6522
|
+
);
|
|
5582
6523
|
console.warn(" openclaw gateway restart\n");
|
|
5583
6524
|
}
|
|
5584
6525
|
if (!hasToken) {
|
|
@@ -5652,9 +6593,7 @@ Testing reasoning with model: ${modelLabel}`);
|
|
|
5652
6593
|
} catch (err) {
|
|
5653
6594
|
const elapsed = Date.now() - start;
|
|
5654
6595
|
console.error(`\u2717 Reasoning check failed after ${elapsed}ms`);
|
|
5655
|
-
console.error(
|
|
5656
|
-
` Error: ${err instanceof Error ? err.message : String(err)}`
|
|
5657
|
-
);
|
|
6596
|
+
console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
5658
6597
|
process.exitCode = 1;
|
|
5659
6598
|
}
|
|
5660
6599
|
});
|
|
@@ -5663,9 +6602,7 @@ Testing reasoning with model: ${modelLabel}`);
|
|
|
5663
6602
|
const configured = !!(cfg?.["integrationId"] && cfg?.["relayToken"]);
|
|
5664
6603
|
if (!configured) {
|
|
5665
6604
|
console.log("\nStageWhisper: not paired\n");
|
|
5666
|
-
console.log(
|
|
5667
|
-
" Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n"
|
|
5668
|
-
);
|
|
6605
|
+
console.log(" Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n");
|
|
5669
6606
|
console.log(
|
|
5670
6607
|
" Get the pairing code from StageWhisper desktop: Settings \u2192 Assistant \u2192 Generate Pairing Code\n"
|
|
5671
6608
|
);
|
|
@@ -5699,10 +6636,16 @@ StageWhisper:`);
|
|
|
5699
6636
|
if (api.registrationMode !== "full") return;
|
|
5700
6637
|
ensureResponsesEndpoint(api);
|
|
5701
6638
|
setRuntime(api.runtime);
|
|
5702
|
-
const
|
|
5703
|
-
api.
|
|
6639
|
+
const transportMode = resolveTransportMode(api);
|
|
6640
|
+
api.logger.info(`StageWhisper transport: ${transportMode}`);
|
|
6641
|
+
if (transportMode === "http") {
|
|
6642
|
+
api.registerService(createHttpTransportService(api));
|
|
6643
|
+
} else {
|
|
6644
|
+
api.registerService(createRelayService(api));
|
|
6645
|
+
}
|
|
5704
6646
|
}
|
|
5705
6647
|
});
|
|
5706
6648
|
export {
|
|
5707
|
-
plugin_main_default as default
|
|
6649
|
+
plugin_main_default as default,
|
|
6650
|
+
resolveHttpTransportToken
|
|
5708
6651
|
};
|