molta 1.0.2 → 1.0.3

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/README.en.md CHANGED
@@ -113,6 +113,15 @@ yarn build
113
113
  yarn start
114
114
  ```
115
115
 
116
+ ## SEA build (single executable)
117
+ > Requires Node.js 20+ (22+ recommended)
118
+
119
+ ```bash
120
+ yarn build:sea
121
+ ```
122
+
123
+ Output: `dist/molta-sea` (append `.exe` on Windows). Intermediate files in `dist-sea/`.
124
+
116
125
  ## Project Layout
117
126
  - `src/router/chat/completions.ts`: main API logic
118
127
  - `src/services/gateway.ts`: Clawd gateway WebSocket client
package/README.md CHANGED
@@ -113,6 +113,15 @@ yarn build
113
113
  yarn start
114
114
  ```
115
115
 
116
+ ## SEA 打包(单文件可执行)
117
+ > 需要 Node.js 20+(推荐 22+)
118
+
119
+ ```bash
120
+ yarn build:sea
121
+ ```
122
+
123
+ 产物位于 `dist/molta-sea`(Windows 可自行加 `.exe`),中间产物在 `dist-sea/`。
124
+
116
125
  ## 目录结构
117
126
  - `src/router/chat/completions.ts`:主接口逻辑
118
127
  - `src/services/gateway.ts`:Clawd 网关 WebSocket 客户端
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ elysia
13
13
  .use(openapi({
14
14
  references: fromTypes()
15
15
  }));
16
- elysia.listen(configObject.port, () => {
16
+ elysia.listen({ hostname: configObject.host, port: configObject.port }, () => {
17
17
  logger.info(`Server started on http://${configObject.host}:${configObject.port}`);
18
18
  });
19
19
  logger.info("Initialized server successfully");
@@ -2,6 +2,7 @@ import { Elysia } from "elysia";
2
2
  import { GatewayClient } from "../../services/gateway.js";
3
3
  import { config } from "../../services/config.js";
4
4
  import { expandRandomString, getOrCreateHttpChatId, renewHttpChatId } from "../../utils/random.js";
5
+ import { Readable } from "node:stream";
5
6
  function extractLastUserMessage(messages) {
6
7
  if (!Array.isArray(messages))
7
8
  return null;
@@ -79,6 +80,46 @@ function createSseResponse(replyText, created) {
79
80
  },
80
81
  });
81
82
  }
83
+ function createSseStreamResponse(streamSource, created) {
84
+ const encoder = new TextEncoder();
85
+ const nodeStream = Readable.from((async function* () {
86
+ let sentRole = false;
87
+ for await (const chunkText of streamSource) {
88
+ if (!chunkText)
89
+ continue;
90
+ const delta = { content: chunkText };
91
+ if (!sentRole) {
92
+ delta.role = "assistant";
93
+ sentRole = true;
94
+ }
95
+ const chunk = {
96
+ id: `chatcmpl-clawd-${created}`,
97
+ object: "chat.completion.chunk",
98
+ created,
99
+ model: "clawd",
100
+ choices: [{ index: 0, delta, finish_reason: null }],
101
+ };
102
+ yield encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`);
103
+ }
104
+ const done = {
105
+ id: `chatcmpl-clawd-${created}`,
106
+ object: "chat.completion.chunk",
107
+ created,
108
+ model: "clawd",
109
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
110
+ };
111
+ yield encoder.encode(`data: ${JSON.stringify(done)}\n\n`);
112
+ yield encoder.encode("data: [DONE]\n\n");
113
+ })());
114
+ const webStream = Readable.toWeb(nodeStream);
115
+ return new Response(webStream, {
116
+ headers: {
117
+ "Content-Type": "text/event-stream; charset=utf-8",
118
+ "Cache-Control": "no-cache",
119
+ Connection: "keep-alive",
120
+ },
121
+ });
122
+ }
82
123
  export const completions = new Elysia()
83
124
  .use(config)
84
125
  .post("/v1/chat/completions", async ({ request, body, set, store }) => {
@@ -111,6 +152,11 @@ export const completions = new Elysia()
111
152
  const token = store.config?.clawdToken;
112
153
  const gateway = new GatewayClient(host, port, token);
113
154
  try {
155
+ if (payload.stream) {
156
+ const created = Math.floor(Date.now() / 1000);
157
+ const source = gateway.askStream(lastUser, sessionKey);
158
+ return createSseStreamResponse(source, created);
159
+ }
114
160
  replyText = await gateway.ask(lastUser, sessionKey);
115
161
  }
116
162
  catch (error) {
@@ -139,4 +139,190 @@ export class GatewayClient {
139
139
  };
140
140
  }));
141
141
  }
142
+ async *askStream(prompt, session) {
143
+ const queue = [];
144
+ let done = false;
145
+ let error = null;
146
+ let notify = null;
147
+ const push = (value) => {
148
+ if (!value)
149
+ return;
150
+ queue.push(value);
151
+ if (notify) {
152
+ notify();
153
+ notify = null;
154
+ }
155
+ };
156
+ const finish = () => {
157
+ done = true;
158
+ if (notify) {
159
+ notify();
160
+ notify = null;
161
+ }
162
+ };
163
+ const fail = (err) => {
164
+ error = err;
165
+ done = true;
166
+ if (notify) {
167
+ notify();
168
+ notify = null;
169
+ }
170
+ };
171
+ const ws = new WebSocket(`ws://${this.host}:${this.port}`);
172
+ let run_id = null;
173
+ let settled = false;
174
+ let lastAssistant = "";
175
+ const timeout = setTimeout(() => {
176
+ if (settled)
177
+ return;
178
+ settled = true;
179
+ ws.close();
180
+ fail(new Error("gateway timeout"));
181
+ }, 60_000);
182
+ const finalize = () => {
183
+ if (settled)
184
+ return;
185
+ settled = true;
186
+ clearTimeout(timeout);
187
+ ws.close();
188
+ finish();
189
+ };
190
+ const finalizeReject = (err) => {
191
+ if (settled)
192
+ return;
193
+ settled = true;
194
+ clearTimeout(timeout);
195
+ ws.close();
196
+ fail(err);
197
+ };
198
+ ws.onmessage = ({ data }) => {
199
+ let obj;
200
+ try {
201
+ obj = JSON.parse(data);
202
+ }
203
+ catch (err) {
204
+ finalizeReject(err instanceof Error ? err : new Error("invalid gateway payload"));
205
+ return;
206
+ }
207
+ if (obj.type === "event" && obj.event === "connect.challenge") {
208
+ ws.send(JSON.stringify({
209
+ "type": "req",
210
+ "id": "connect",
211
+ "method": "connect",
212
+ "params": {
213
+ "minProtocol": 3,
214
+ "maxProtocol": 3,
215
+ "client": {
216
+ "id": "gateway-client",
217
+ "version": "0.1.0",
218
+ "platform": this.platform,
219
+ "mode": "backend",
220
+ },
221
+ "role": "operator",
222
+ "scopes": ["operator.read", "operator.write"],
223
+ "auth": { "token": this.token },
224
+ "locale": "zh-CN",
225
+ "userAgent": "openai-clawdbot-bridge",
226
+ },
227
+ }));
228
+ return;
229
+ }
230
+ if (obj["type"] === "res" && obj["id"] === "connect") {
231
+ if (!(obj["ok"])) {
232
+ finalizeReject(new Error((obj["error"] || {})["message"] || "connect failed"));
233
+ return;
234
+ }
235
+ ws.send(JSON.stringify({
236
+ "type": "req",
237
+ "id": "agent",
238
+ "method": "agent",
239
+ "params": {
240
+ "message": prompt,
241
+ "agentId": "main",
242
+ "sessionKey": session,
243
+ "deliver": false,
244
+ "idempotencyKey": crypto.randomUUID(),
245
+ },
246
+ }));
247
+ return;
248
+ }
249
+ if (obj["type"] === "res" && obj["id"] === "agent") {
250
+ if (!(obj["ok"])) {
251
+ finalizeReject(new Error((obj["error"] ?? {})["message"] || "agent failed"));
252
+ return;
253
+ }
254
+ const payload = obj["payload"] || {};
255
+ if (payload["runId"]) {
256
+ run_id = payload["runId"];
257
+ }
258
+ return;
259
+ }
260
+ if (obj["type"] === "event" && obj["event"] === "agent") {
261
+ const payload = obj["payload"] || {};
262
+ if (payload["runId"] !== run_id)
263
+ return;
264
+ if (payload["stream"] === "assistant") {
265
+ const data = payload["data"] || {};
266
+ if (typeof data["text"] === "string") {
267
+ const text = data["text"];
268
+ let delta = text;
269
+ if (text.startsWith(lastAssistant)) {
270
+ delta = text.slice(lastAssistant.length);
271
+ }
272
+ lastAssistant = text;
273
+ push(delta);
274
+ }
275
+ }
276
+ else if (typeof payload["delta"] === "string") {
277
+ push(payload["delta"]);
278
+ }
279
+ if (payload["stream"] === "lifecycle") {
280
+ const phase = (payload["data"] || {})["phase"];
281
+ if (phase === "end") {
282
+ finalize();
283
+ }
284
+ else if (phase === "error") {
285
+ finalizeReject(new Error((payload["data"] || {})["message"] || "agent failed"));
286
+ }
287
+ }
288
+ }
289
+ };
290
+ ws.onerror = () => {
291
+ const detail = ws.readyState === WebSocket.CLOSED ? "closed" : `state:${ws.readyState}`;
292
+ finalizeReject(new Error(`gateway websocket error (${detail}) url=${ws.url}`));
293
+ };
294
+ ws.onclose = (event) => {
295
+ if (settled)
296
+ return;
297
+ if (event.wasClean) {
298
+ finalize();
299
+ return;
300
+ }
301
+ const reason = event.reason ? ` reason=${event.reason}` : "";
302
+ finalizeReject(new Error(`gateway websocket closed code=${event.code}${reason} url=${ws.url}`));
303
+ };
304
+ try {
305
+ while (true) {
306
+ if (queue.length) {
307
+ yield queue.shift();
308
+ continue;
309
+ }
310
+ if (done) {
311
+ if (error)
312
+ throw error;
313
+ return;
314
+ }
315
+ await new Promise((resolve) => {
316
+ notify = resolve;
317
+ });
318
+ }
319
+ }
320
+ finally {
321
+ if (!settled) {
322
+ settled = true;
323
+ clearTimeout(timeout);
324
+ ws.close();
325
+ }
326
+ }
327
+ }
142
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molta",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Use MoltBot anywhere",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.12.0",
@@ -11,6 +11,7 @@
11
11
  "license": "ISC",
12
12
  "scripts": {
13
13
  "build": "tsc",
14
+ "build:sea": "tsc -p tsconfig.sea.json && node -e \"const fs=require('fs');fs.renameSync('dist-sea/index.js','dist-sea/index.cjs')\" && node --build-sea sea-config.json",
14
15
  "dev": "tsx src/index.ts",
15
16
  "prepublishOnly": "yarn build && node scripts/add-shebang.cjs",
16
17
  "start": "node dist/index.js"