weifuwu 0.7.0 → 0.8.1
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.md +44 -0
- package/dist/deploy/config.d.ts +2 -0
- package/dist/deploy/gateway.d.ts +2 -0
- package/dist/deploy/index.d.ts +4 -0
- package/dist/deploy/manager.d.ts +16 -0
- package/dist/deploy/process.d.ts +14 -0
- package/dist/deploy/types.d.ts +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +681 -105
- package/dist/serve.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
|
|
|
28
28
|
- **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
|
|
29
29
|
- **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` — immutable
|
|
30
30
|
- **Error handling** — global `onError()`
|
|
31
|
+
- **Deploy** — `deploy()` — self-hosted PaaS: multi-app reverse proxy, subdomain routing, zero-downtime updates, auto SSL, Git-based deployment
|
|
31
32
|
- **Zero build** — native TypeScript in Node.js v24+
|
|
32
33
|
- **Zero deps** (core) — only `node:http` and `node:stream`
|
|
33
34
|
|
|
@@ -998,6 +999,42 @@ const app = new Router()
|
|
|
998
999
|
.get('/crash', () => { throw new Error('boom') })
|
|
999
1000
|
```
|
|
1000
1001
|
|
|
1002
|
+
## Deploy
|
|
1003
|
+
|
|
1004
|
+
See [deploy.md](./deploy.md) for complete documentation — VPS setup, subdomain routing, blue-green zero-downtime, WebSocket bridge, Git webhook, auto SSL, and management API.
|
|
1005
|
+
|
|
1006
|
+
Quick start on a fresh VPS:
|
|
1007
|
+
|
|
1008
|
+
```bash
|
|
1009
|
+
# 1. Install Node.js
|
|
1010
|
+
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
|
1011
|
+
apt-get install -y nodejs git
|
|
1012
|
+
|
|
1013
|
+
# 2. Create deploy project
|
|
1014
|
+
mkdir -p /opt/deploy && cd /opt/deploy
|
|
1015
|
+
npm init -y && npm install weifuwu
|
|
1016
|
+
|
|
1017
|
+
# 3. Write deploy.ts
|
|
1018
|
+
cat > deploy.ts << 'EOF'
|
|
1019
|
+
import { deploy, defineConfig } from 'weifuwu'
|
|
1020
|
+
await deploy(defineConfig({
|
|
1021
|
+
domain: 'example.com',
|
|
1022
|
+
deployToken: process.env.DEPLOY_TOKEN,
|
|
1023
|
+
apps: {
|
|
1024
|
+
blog: {
|
|
1025
|
+
repo: 'https://github.com/me/my-blog.git',
|
|
1026
|
+
subdomain: 'blog',
|
|
1027
|
+
entry: 'app.ts',
|
|
1028
|
+
port: 3001,
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
}))
|
|
1032
|
+
EOF
|
|
1033
|
+
|
|
1034
|
+
# 4. Run
|
|
1035
|
+
DEPLOY_TOKEN='my-secret' node deploy.ts
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1001
1038
|
## API
|
|
1002
1039
|
|
|
1003
1040
|
### `serve(handler, options?)`
|
|
@@ -1099,6 +1136,13 @@ Returns `Promise<Router>`.
|
|
|
1099
1136
|
| `ai(handler)` | AI streaming endpoint (POST) |
|
|
1100
1137
|
| `workflow(handler)` | Workflow engine (POST + SSE) |
|
|
1101
1138
|
|
|
1139
|
+
### Deploy
|
|
1140
|
+
|
|
1141
|
+
| Import | Description |
|
|
1142
|
+
|--------|-------------|
|
|
1143
|
+
| `deploy(config)` | Start the deployment platform — see [deploy.md](./deploy.md) |
|
|
1144
|
+
| `defineConfig(config)` | Type-safe config helper with validation — see [deploy.md](./deploy.md) |
|
|
1145
|
+
|
|
1102
1146
|
### Utilities
|
|
1103
1147
|
|
|
1104
1148
|
| Function | Description |
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DeployConfig, DeployServer } from './types.ts';
|
|
2
|
+
export { defineConfig } from './config.ts';
|
|
3
|
+
export type { DeployConfig, AppConfig, DeployServer, AppStatus, GatewayResult } from './types.ts';
|
|
4
|
+
export declare function deploy(config: DeployConfig): Promise<DeployServer>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Router } from '../router.ts';
|
|
2
|
+
import type { DeployConfig, AppStatus } from './types.ts';
|
|
3
|
+
export interface AppRuntime {
|
|
4
|
+
config: import('./types.ts').AppConfig;
|
|
5
|
+
status: AppStatus;
|
|
6
|
+
logs: string[];
|
|
7
|
+
process: import('node:child_process').ChildProcess | null;
|
|
8
|
+
currentPort: number;
|
|
9
|
+
startedAt: number | null;
|
|
10
|
+
restartCount: number;
|
|
11
|
+
restartTimer: ReturnType<typeof setTimeout> | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function createManager(config: DeployConfig, apps: Map<string, AppRuntime>, manager: {
|
|
14
|
+
deployApp(name: string): Promise<void>;
|
|
15
|
+
reloadConfig(): Promise<void>;
|
|
16
|
+
}): Router;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export interface ManagedProcess {
|
|
3
|
+
child: ChildProcess;
|
|
4
|
+
port: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function forkApp(opts: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
entry: string;
|
|
9
|
+
port: number;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
onLog?: (line: string) => void;
|
|
12
|
+
}): ManagedProcess;
|
|
13
|
+
export declare function stopProcess(mp: ManagedProcess, timeout?: number): Promise<void>;
|
|
14
|
+
export declare function healthCheck(port: number, path?: string): Promise<boolean>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
import type { Handler } from '../types.ts';
|
|
4
|
+
export interface DeployConfig {
|
|
5
|
+
domain: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
ssl?: {
|
|
8
|
+
email: string;
|
|
9
|
+
staging?: boolean;
|
|
10
|
+
};
|
|
11
|
+
deployToken?: string;
|
|
12
|
+
webhookSecret?: string;
|
|
13
|
+
appsDir?: string;
|
|
14
|
+
defaultApp?: string;
|
|
15
|
+
apps: Record<string, AppConfig>;
|
|
16
|
+
}
|
|
17
|
+
export interface AppConfig {
|
|
18
|
+
repo: string;
|
|
19
|
+
branch?: string;
|
|
20
|
+
subdomain?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
port: number;
|
|
23
|
+
ports?: [number, number];
|
|
24
|
+
entry: string;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
healthEndpoint?: string;
|
|
27
|
+
buildCommand?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface AppStatus {
|
|
30
|
+
name: string;
|
|
31
|
+
status: 'starting' | 'running' | 'stopped' | 'error';
|
|
32
|
+
port: number;
|
|
33
|
+
subdomain?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
pid?: number;
|
|
36
|
+
uptime?: number;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface DeployServer {
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
ready: Promise<void>;
|
|
42
|
+
url: string;
|
|
43
|
+
apps: {
|
|
44
|
+
list(): AppStatus[];
|
|
45
|
+
status(name: string): AppStatus | undefined;
|
|
46
|
+
deploy(name: string): Promise<void>;
|
|
47
|
+
restart(name: string): Promise<void>;
|
|
48
|
+
stop(name: string): Promise<void>;
|
|
49
|
+
start(name: string): Promise<void>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface GatewayResult {
|
|
53
|
+
handler: Handler;
|
|
54
|
+
wsHandler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
55
|
+
}
|
|
56
|
+
declare module '../types.ts' {
|
|
57
|
+
interface Context {
|
|
58
|
+
deploy?: {
|
|
59
|
+
appName?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -39,3 +39,5 @@ export { agent } from './agent/index.ts';
|
|
|
39
39
|
export type { AgentOptions, AgentModule, AgentConfig, RunParams, RunResult, KnowledgeDoc } from './agent/types.ts';
|
|
40
40
|
export { messager } from './messager/index.ts';
|
|
41
41
|
export type { MessagerOptions, MessagerModule, Channel, ChannelMember, Message } from './messager/types.ts';
|
|
42
|
+
export { deploy, defineConfig } from './deploy/index.ts';
|
|
43
|
+
export type { DeployConfig, AppConfig, DeployServer, AppStatus } from './deploy/types.ts';
|
package/dist/index.js
CHANGED
|
@@ -10718,9 +10718,24 @@ var require_built3 = __commonJS({
|
|
|
10718
10718
|
|
|
10719
10719
|
// serve.ts
|
|
10720
10720
|
import http from "node:http";
|
|
10721
|
-
async function readBody(req) {
|
|
10721
|
+
async function readBody(req, maxSize) {
|
|
10722
|
+
if (maxSize) {
|
|
10723
|
+
const cl = parseInt(req.headers["content-length"] ?? "0", 10);
|
|
10724
|
+
if (cl > maxSize) {
|
|
10725
|
+
const err = new Error("Request body too large");
|
|
10726
|
+
err.status = 413;
|
|
10727
|
+
throw err;
|
|
10728
|
+
}
|
|
10729
|
+
}
|
|
10722
10730
|
const chunks = [];
|
|
10731
|
+
let total = 0;
|
|
10723
10732
|
for await (const chunk of req) {
|
|
10733
|
+
total += chunk.byteLength;
|
|
10734
|
+
if (maxSize && total > maxSize) {
|
|
10735
|
+
const err = new Error("Request body too large");
|
|
10736
|
+
err.status = 413;
|
|
10737
|
+
throw err;
|
|
10738
|
+
}
|
|
10724
10739
|
chunks.push(chunk);
|
|
10725
10740
|
}
|
|
10726
10741
|
return Buffer.concat(chunks);
|
|
@@ -10766,11 +10781,16 @@ function serve(handler, options) {
|
|
|
10766
10781
|
const hostname = options?.hostname ?? "0.0.0.0";
|
|
10767
10782
|
const server = http.createServer(async (req, res) => {
|
|
10768
10783
|
try {
|
|
10769
|
-
const body = await readBody(req);
|
|
10784
|
+
const body = await readBody(req, options?.maxBodySize);
|
|
10770
10785
|
const [request, query] = createRequest(req, body);
|
|
10771
10786
|
const response = await handler(request, { params: {}, query });
|
|
10772
10787
|
await sendResponse(res, response);
|
|
10773
|
-
} catch {
|
|
10788
|
+
} catch (err) {
|
|
10789
|
+
if (err?.status === 413) {
|
|
10790
|
+
res.writeHead(413, { "Content-Type": "text/plain" });
|
|
10791
|
+
res.end("Request Body Too Large");
|
|
10792
|
+
return;
|
|
10793
|
+
}
|
|
10774
10794
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
10775
10795
|
res.end("Internal Server Error");
|
|
10776
10796
|
}
|
|
@@ -10918,38 +10938,38 @@ var Router = class _Router {
|
|
|
10918
10938
|
}
|
|
10919
10939
|
return this;
|
|
10920
10940
|
}
|
|
10921
|
-
get(
|
|
10922
|
-
return this.route("GET",
|
|
10941
|
+
get(path2, ...args) {
|
|
10942
|
+
return this.route("GET", path2, ...args);
|
|
10923
10943
|
}
|
|
10924
|
-
post(
|
|
10925
|
-
return this.route("POST",
|
|
10944
|
+
post(path2, ...args) {
|
|
10945
|
+
return this.route("POST", path2, ...args);
|
|
10926
10946
|
}
|
|
10927
|
-
put(
|
|
10928
|
-
return this.route("PUT",
|
|
10947
|
+
put(path2, ...args) {
|
|
10948
|
+
return this.route("PUT", path2, ...args);
|
|
10929
10949
|
}
|
|
10930
|
-
delete(
|
|
10931
|
-
return this.route("DELETE",
|
|
10950
|
+
delete(path2, ...args) {
|
|
10951
|
+
return this.route("DELETE", path2, ...args);
|
|
10932
10952
|
}
|
|
10933
|
-
patch(
|
|
10934
|
-
return this.route("PATCH",
|
|
10953
|
+
patch(path2, ...args) {
|
|
10954
|
+
return this.route("PATCH", path2, ...args);
|
|
10935
10955
|
}
|
|
10936
|
-
head(
|
|
10937
|
-
return this.route("HEAD",
|
|
10956
|
+
head(path2, ...args) {
|
|
10957
|
+
return this.route("HEAD", path2, ...args);
|
|
10938
10958
|
}
|
|
10939
|
-
options(
|
|
10940
|
-
return this.route("OPTIONS",
|
|
10959
|
+
options(path2, ...args) {
|
|
10960
|
+
return this.route("OPTIONS", path2, ...args);
|
|
10941
10961
|
}
|
|
10942
|
-
all(
|
|
10943
|
-
return this.route("*",
|
|
10962
|
+
all(path2, ...args) {
|
|
10963
|
+
return this.route("*", path2, ...args);
|
|
10944
10964
|
}
|
|
10945
10965
|
onError(handler) {
|
|
10946
10966
|
this.errorHandler = handler;
|
|
10947
10967
|
return this;
|
|
10948
10968
|
}
|
|
10949
|
-
route(method,
|
|
10969
|
+
route(method, path2, ...args) {
|
|
10950
10970
|
const handler = args.pop();
|
|
10951
10971
|
const middlewares = args;
|
|
10952
|
-
const segments = this.splitPath(
|
|
10972
|
+
const segments = this.splitPath(path2);
|
|
10953
10973
|
let node = this.root;
|
|
10954
10974
|
for (const segment of segments) {
|
|
10955
10975
|
if (segment === "*") {
|
|
@@ -10964,10 +10984,10 @@ var Router = class _Router {
|
|
|
10964
10984
|
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
10965
10985
|
return this;
|
|
10966
10986
|
}
|
|
10967
|
-
ws(
|
|
10987
|
+
ws(path2, ...args) {
|
|
10968
10988
|
const handler = args.pop();
|
|
10969
10989
|
const middlewares = args;
|
|
10970
|
-
const segments = this.splitPath(
|
|
10990
|
+
const segments = this.splitPath(path2);
|
|
10971
10991
|
let node = this.wsRoot;
|
|
10972
10992
|
for (const segment of segments) {
|
|
10973
10993
|
node = getWsNode(node, segment);
|
|
@@ -11013,8 +11033,13 @@ var Router = class _Router {
|
|
|
11013
11033
|
return mw(innerReq, ctx2, dispatch);
|
|
11014
11034
|
}
|
|
11015
11035
|
return await new Promise((resolve3) => {
|
|
11016
|
-
|
|
11017
|
-
|
|
11036
|
+
try {
|
|
11037
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
11038
|
+
resolve3(new Response(null, { status: 101 }));
|
|
11039
|
+
} catch {
|
|
11040
|
+
socket.destroy();
|
|
11041
|
+
resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
11042
|
+
}
|
|
11018
11043
|
});
|
|
11019
11044
|
};
|
|
11020
11045
|
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
@@ -11026,8 +11051,8 @@ var Router = class _Router {
|
|
|
11026
11051
|
});
|
|
11027
11052
|
};
|
|
11028
11053
|
}
|
|
11029
|
-
splitPath(
|
|
11030
|
-
return
|
|
11054
|
+
splitPath(path2) {
|
|
11055
|
+
return path2.split("/").filter(Boolean);
|
|
11031
11056
|
}
|
|
11032
11057
|
matchTrie(method, segments) {
|
|
11033
11058
|
let node = this.root;
|
|
@@ -11815,7 +11840,7 @@ function validate(schemas) {
|
|
|
11815
11840
|
if (issues.length > 0) {
|
|
11816
11841
|
return Response.json({ error: "Validation failed", issues }, { status: 400 });
|
|
11817
11842
|
}
|
|
11818
|
-
ctx.parsed = parsed;
|
|
11843
|
+
ctx.parsed = { ...ctx.parsed, ...parsed };
|
|
11819
11844
|
return next(req, ctx);
|
|
11820
11845
|
};
|
|
11821
11846
|
}
|
|
@@ -11831,7 +11856,11 @@ function getCookies(req) {
|
|
|
11831
11856
|
const name15 = pair.slice(0, idx).trim();
|
|
11832
11857
|
const value = pair.slice(idx + 1).trim();
|
|
11833
11858
|
if (name15) {
|
|
11834
|
-
|
|
11859
|
+
try {
|
|
11860
|
+
cookies[name15] = decodeURIComponent(value);
|
|
11861
|
+
} catch {
|
|
11862
|
+
cookies[name15] = value;
|
|
11863
|
+
}
|
|
11835
11864
|
}
|
|
11836
11865
|
}
|
|
11837
11866
|
return cookies;
|
|
@@ -11874,67 +11903,45 @@ function upload(options) {
|
|
|
11874
11903
|
const saveDir = options?.dir;
|
|
11875
11904
|
return async (req, ctx, next) => {
|
|
11876
11905
|
const ct = req.headers.get("content-type") ?? "";
|
|
11877
|
-
if (!ct.includes("multipart/form-data"))
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
return Response.json({ error: "
|
|
11906
|
+
if (!ct.includes("multipart/form-data")) return next(req, ctx);
|
|
11907
|
+
let formData;
|
|
11908
|
+
try {
|
|
11909
|
+
formData = await req.formData();
|
|
11910
|
+
} catch {
|
|
11911
|
+
return Response.json({ error: "Invalid multipart data" }, { status: 400 });
|
|
11883
11912
|
}
|
|
11884
|
-
const boundary = match[1] ?? match[2];
|
|
11885
|
-
const body = await req.text();
|
|
11886
|
-
const rawParts = body.split(`--${boundary}`).filter((p) => p && !p.startsWith("--") && !p.startsWith("\r\n--"));
|
|
11887
11913
|
const files = {};
|
|
11888
11914
|
const fields = {};
|
|
11889
|
-
for (const
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
const headers = {};
|
|
11894
|
-
while (i < lines.length && lines[i].length > 0) {
|
|
11895
|
-
const sep3 = lines[i].indexOf(": ");
|
|
11896
|
-
if (sep3 !== -1) headers[lines[i].slice(0, sep3).toLowerCase()] = lines[i].slice(sep3 + 2);
|
|
11897
|
-
i++;
|
|
11898
|
-
}
|
|
11899
|
-
i++;
|
|
11900
|
-
const bodyValue = lines.slice(i).join("\r\n");
|
|
11901
|
-
const disposition = headers["content-disposition"] ?? "";
|
|
11902
|
-
const nameMatch = disposition.match(/name="([^"]*)"/);
|
|
11903
|
-
if (!nameMatch) continue;
|
|
11904
|
-
const name15 = nameMatch[1];
|
|
11905
|
-
const filenameMatch = disposition.match(/filename="([^"]*)"/);
|
|
11906
|
-
const filename = filenameMatch?.[1];
|
|
11907
|
-
if (filename) {
|
|
11908
|
-
const buf = Buffer.from(bodyValue.replace(/\r?\n$/, ""), "binary");
|
|
11909
|
-
if (options?.allowedTypes) {
|
|
11910
|
-
const mime = headers["content-type"] ?? "application/octet-stream";
|
|
11911
|
-
if (!options.allowedTypes.includes(mime)) {
|
|
11912
|
-
return Response.json({ error: `File type not allowed: ${mime}` }, { status: 415 });
|
|
11913
|
-
}
|
|
11915
|
+
for (const [key, value] of formData) {
|
|
11916
|
+
if (value instanceof File) {
|
|
11917
|
+
if (options?.allowedTypes && !options.allowedTypes.includes(value.type)) {
|
|
11918
|
+
return Response.json({ error: `File type not allowed: ${value.type}` }, { status: 415 });
|
|
11914
11919
|
}
|
|
11915
|
-
if (options?.maxFileSize &&
|
|
11916
|
-
return Response.json({ error: `File too large: ${
|
|
11920
|
+
if (options?.maxFileSize && value.size > options.maxFileSize) {
|
|
11921
|
+
return Response.json({ error: `File too large: ${value.name}` }, { status: 413 });
|
|
11917
11922
|
}
|
|
11923
|
+
const buf = Buffer.from(await value.arrayBuffer());
|
|
11918
11924
|
const uf = {
|
|
11919
|
-
name:
|
|
11920
|
-
type:
|
|
11925
|
+
name: value.name,
|
|
11926
|
+
type: value.type,
|
|
11921
11927
|
size: buf.byteLength,
|
|
11922
11928
|
buffer: saveDir ? void 0 : buf
|
|
11923
11929
|
};
|
|
11924
11930
|
if (saveDir) {
|
|
11925
|
-
const
|
|
11931
|
+
const safeName = value.name.replace(/[/\\]/g, "");
|
|
11932
|
+
const filePath = join2(saveDir, `${randomUUID()}-${safeName}`);
|
|
11926
11933
|
await mkdir(saveDir, { recursive: true });
|
|
11927
11934
|
await writeFile(filePath, buf);
|
|
11928
11935
|
uf.path = filePath;
|
|
11929
11936
|
}
|
|
11930
|
-
if (files[
|
|
11931
|
-
const existing = files[
|
|
11932
|
-
files[
|
|
11937
|
+
if (files[key]) {
|
|
11938
|
+
const existing = files[key];
|
|
11939
|
+
files[key] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
|
|
11933
11940
|
} else {
|
|
11934
|
-
files[
|
|
11941
|
+
files[key] = uf;
|
|
11935
11942
|
}
|
|
11936
11943
|
} else {
|
|
11937
|
-
fields[
|
|
11944
|
+
fields[key] = value;
|
|
11938
11945
|
}
|
|
11939
11946
|
}
|
|
11940
11947
|
ctx.parsed = { ...ctx.parsed, files, fields };
|
|
@@ -12192,9 +12199,9 @@ function tool(def) {
|
|
|
12192
12199
|
}
|
|
12193
12200
|
|
|
12194
12201
|
// workflow/reference.ts
|
|
12195
|
-
function getByPath(obj,
|
|
12202
|
+
function getByPath(obj, path2) {
|
|
12196
12203
|
let current = obj;
|
|
12197
|
-
for (const key of
|
|
12204
|
+
for (const key of path2) {
|
|
12198
12205
|
if (current === null || current === void 0) return void 0;
|
|
12199
12206
|
if (typeof current === "object" && key in current) {
|
|
12200
12207
|
current = current[key];
|
|
@@ -12204,9 +12211,9 @@ function getByPath(obj, path) {
|
|
|
12204
12211
|
}
|
|
12205
12212
|
return current;
|
|
12206
12213
|
}
|
|
12207
|
-
function resolveRef(
|
|
12208
|
-
if (
|
|
12209
|
-
const afterNodes =
|
|
12214
|
+
function resolveRef(path2, ctx) {
|
|
12215
|
+
if (path2.startsWith("$nodes.")) {
|
|
12216
|
+
const afterNodes = path2.slice(7);
|
|
12210
12217
|
const dotIdx = afterNodes.indexOf(".");
|
|
12211
12218
|
if (dotIdx === -1) {
|
|
12212
12219
|
return ctx.nodeOutputs.get(afterNodes);
|
|
@@ -12222,23 +12229,23 @@ function resolveRef(path, ctx) {
|
|
|
12222
12229
|
}
|
|
12223
12230
|
return getByPath(output, propPath.split("."));
|
|
12224
12231
|
}
|
|
12225
|
-
if (
|
|
12226
|
-
const name15 =
|
|
12232
|
+
if (path2.startsWith("$var.")) {
|
|
12233
|
+
const name15 = path2.slice(5);
|
|
12227
12234
|
if (!ctx.variables.has(name15)) {
|
|
12228
12235
|
throw new Error(`Variable "${name15}" is not defined`);
|
|
12229
12236
|
}
|
|
12230
12237
|
return ctx.variables.get(name15);
|
|
12231
12238
|
}
|
|
12232
|
-
if (
|
|
12233
|
-
const key =
|
|
12239
|
+
if (path2.startsWith("$input.")) {
|
|
12240
|
+
const key = path2.slice(7);
|
|
12234
12241
|
return ctx.input[key];
|
|
12235
12242
|
}
|
|
12236
|
-
if (
|
|
12237
|
-
if (
|
|
12238
|
-
if (
|
|
12239
|
-
const num = Number(
|
|
12240
|
-
if (!isNaN(num) &&
|
|
12241
|
-
return
|
|
12243
|
+
if (path2 === "true") return true;
|
|
12244
|
+
if (path2 === "false") return false;
|
|
12245
|
+
if (path2 === "null") return null;
|
|
12246
|
+
const num = Number(path2);
|
|
12247
|
+
if (!isNaN(num) && path2.trim() !== "") return num;
|
|
12248
|
+
return path2;
|
|
12242
12249
|
}
|
|
12243
12250
|
function resolveValue(v, ctx) {
|
|
12244
12251
|
if (typeof v === "string" && v.startsWith("$")) {
|
|
@@ -13542,12 +13549,21 @@ function queue(opts) {
|
|
|
13542
13549
|
if (!running) return;
|
|
13543
13550
|
try {
|
|
13544
13551
|
const now = Date.now();
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13552
|
+
while (true) {
|
|
13553
|
+
const result = await redis2.zpopmin(jobKey);
|
|
13554
|
+
if (result.length < 2) break;
|
|
13555
|
+
const raw = result[0];
|
|
13556
|
+
const score = parseInt(result[1], 10);
|
|
13557
|
+
if (score > now) {
|
|
13558
|
+
await redis2.zadd(jobKey, score, raw);
|
|
13559
|
+
break;
|
|
13560
|
+
}
|
|
13561
|
+
let job;
|
|
13562
|
+
try {
|
|
13563
|
+
job = JSON.parse(raw);
|
|
13564
|
+
} catch {
|
|
13565
|
+
continue;
|
|
13566
|
+
}
|
|
13551
13567
|
const handler = handlers.get(job.type);
|
|
13552
13568
|
if (handler) {
|
|
13553
13569
|
handler(job).then(() => {
|
|
@@ -13555,11 +13571,13 @@ function queue(opts) {
|
|
|
13555
13571
|
try {
|
|
13556
13572
|
const nextRun = cronNext(job.schedule);
|
|
13557
13573
|
const nextJob = { ...job, id: crypto3.randomUUID(), runAt: nextRun, createdAt: Date.now() };
|
|
13558
|
-
redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob))
|
|
13574
|
+
redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob)).catch(() => {
|
|
13575
|
+
});
|
|
13559
13576
|
} catch {
|
|
13560
13577
|
}
|
|
13561
13578
|
}
|
|
13562
|
-
}).catch(() => {
|
|
13579
|
+
}).catch((e) => {
|
|
13580
|
+
console.error("[queue] handler error:", e);
|
|
13563
13581
|
});
|
|
13564
13582
|
}
|
|
13565
13583
|
}
|
|
@@ -13844,6 +13862,10 @@ async function getUserTable(sql, tenantId, slug) {
|
|
|
13844
13862
|
`;
|
|
13845
13863
|
return row ?? null;
|
|
13846
13864
|
}
|
|
13865
|
+
function requireAdmin(ctx) {
|
|
13866
|
+
if (ctx.tenant?.role !== "admin") return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
13867
|
+
return null;
|
|
13868
|
+
}
|
|
13847
13869
|
function buildRouter(sql, usersTable) {
|
|
13848
13870
|
const r = new Router();
|
|
13849
13871
|
r.post("/sys/tenants", async (req, ctx) => {
|
|
@@ -13869,6 +13891,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13869
13891
|
return Response.json(rows);
|
|
13870
13892
|
});
|
|
13871
13893
|
r.post("/sys/tenants/invite", async (req, ctx) => {
|
|
13894
|
+
const err = requireAdmin(ctx);
|
|
13895
|
+
if (err) return err;
|
|
13872
13896
|
const { email, role = "member" } = await req.json();
|
|
13873
13897
|
const [user2] = await sql`
|
|
13874
13898
|
SELECT id FROM ${sql(usersTable)} WHERE "email" = ${email} LIMIT 1
|
|
@@ -13886,6 +13910,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13886
13910
|
return Response.json({ ok: true }, { status: 201 });
|
|
13887
13911
|
});
|
|
13888
13912
|
r.delete("/sys/tenants/members/:userId", async (req, ctx) => {
|
|
13913
|
+
const err = requireAdmin(ctx);
|
|
13914
|
+
if (err) return err;
|
|
13889
13915
|
const userId = parseInt(ctx.params.userId, 10);
|
|
13890
13916
|
await sql`
|
|
13891
13917
|
DELETE FROM "_tenant_members"
|
|
@@ -13894,6 +13920,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13894
13920
|
return Response.json({ ok: true });
|
|
13895
13921
|
});
|
|
13896
13922
|
r.post("/sys/tables", async (req, ctx) => {
|
|
13923
|
+
const err = requireAdmin(ctx);
|
|
13924
|
+
if (err) return err;
|
|
13897
13925
|
const body = await req.json();
|
|
13898
13926
|
const slugErr = validateSlug(body.slug);
|
|
13899
13927
|
if (slugErr) return Response.json({ error: slugErr }, { status: 400 });
|
|
@@ -13932,6 +13960,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13932
13960
|
return Response.json(table);
|
|
13933
13961
|
});
|
|
13934
13962
|
r.patch("/sys/tables/:slug", async (req, ctx) => {
|
|
13963
|
+
const err = requireAdmin(ctx);
|
|
13964
|
+
if (err) return err;
|
|
13935
13965
|
const body = await req.json();
|
|
13936
13966
|
if (!body.fields || !Array.isArray(body.fields)) {
|
|
13937
13967
|
return Response.json({ error: "fields array required" }, { status: 400 });
|
|
@@ -13952,6 +13982,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13952
13982
|
return Response.json({ ...table, fields: merged });
|
|
13953
13983
|
});
|
|
13954
13984
|
r.delete("/sys/tables/:slug", async (_req, ctx) => {
|
|
13985
|
+
const err = requireAdmin(ctx);
|
|
13986
|
+
if (err) return err;
|
|
13955
13987
|
await sql.unsafe(dropTableSQL(ctx.tenant.id, ctx.params.slug));
|
|
13956
13988
|
await sql`
|
|
13957
13989
|
DELETE FROM "_user_tables"
|
|
@@ -23884,37 +23916,37 @@ function createOpenAI(options = {}) {
|
|
|
23884
23916
|
);
|
|
23885
23917
|
const createChatModel = (modelId) => new OpenAIChatLanguageModel(modelId, {
|
|
23886
23918
|
provider: `${providerName}.chat`,
|
|
23887
|
-
url: ({ path }) => `${baseURL}${
|
|
23919
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23888
23920
|
headers: getHeaders,
|
|
23889
23921
|
fetch: options.fetch
|
|
23890
23922
|
});
|
|
23891
23923
|
const createCompletionModel = (modelId) => new OpenAICompletionLanguageModel(modelId, {
|
|
23892
23924
|
provider: `${providerName}.completion`,
|
|
23893
|
-
url: ({ path }) => `${baseURL}${
|
|
23925
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23894
23926
|
headers: getHeaders,
|
|
23895
23927
|
fetch: options.fetch
|
|
23896
23928
|
});
|
|
23897
23929
|
const createEmbeddingModel = (modelId) => new OpenAIEmbeddingModel(modelId, {
|
|
23898
23930
|
provider: `${providerName}.embedding`,
|
|
23899
|
-
url: ({ path }) => `${baseURL}${
|
|
23931
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23900
23932
|
headers: getHeaders,
|
|
23901
23933
|
fetch: options.fetch
|
|
23902
23934
|
});
|
|
23903
23935
|
const createImageModel = (modelId) => new OpenAIImageModel(modelId, {
|
|
23904
23936
|
provider: `${providerName}.image`,
|
|
23905
|
-
url: ({ path }) => `${baseURL}${
|
|
23937
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23906
23938
|
headers: getHeaders,
|
|
23907
23939
|
fetch: options.fetch
|
|
23908
23940
|
});
|
|
23909
23941
|
const createTranscriptionModel = (modelId) => new OpenAITranscriptionModel(modelId, {
|
|
23910
23942
|
provider: `${providerName}.transcription`,
|
|
23911
|
-
url: ({ path }) => `${baseURL}${
|
|
23943
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23912
23944
|
headers: getHeaders,
|
|
23913
23945
|
fetch: options.fetch
|
|
23914
23946
|
});
|
|
23915
23947
|
const createSpeechModel = (modelId) => new OpenAISpeechModel(modelId, {
|
|
23916
23948
|
provider: `${providerName}.speech`,
|
|
23917
|
-
url: ({ path }) => `${baseURL}${
|
|
23949
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23918
23950
|
headers: getHeaders,
|
|
23919
23951
|
fetch: options.fetch
|
|
23920
23952
|
});
|
|
@@ -23929,7 +23961,7 @@ function createOpenAI(options = {}) {
|
|
|
23929
23961
|
const createResponsesModel = (modelId) => {
|
|
23930
23962
|
return new OpenAIResponsesLanguageModel(modelId, {
|
|
23931
23963
|
provider: `${providerName}.responses`,
|
|
23932
|
-
url: ({ path }) => `${baseURL}${
|
|
23964
|
+
url: ({ path: path2 }) => `${baseURL}${path2}`,
|
|
23933
23965
|
headers: getHeaders,
|
|
23934
23966
|
fetch: options.fetch,
|
|
23935
23967
|
fileIdPrefixes: ["file-"]
|
|
@@ -24407,15 +24439,19 @@ function createWSHandler(deps) {
|
|
|
24407
24439
|
VALUES (${channel_id}, ${am.member_id}, 'agent', ${result.output})
|
|
24408
24440
|
`.then(([r]) => {
|
|
24409
24441
|
broadcastToChannel(channel_id, { type: "message", data: r });
|
|
24442
|
+
}).catch((e) => {
|
|
24443
|
+
console.error("[messager] agent reply insert failed:", e);
|
|
24410
24444
|
});
|
|
24411
24445
|
}
|
|
24412
|
-
}).catch(() => {
|
|
24446
|
+
}).catch((e) => {
|
|
24447
|
+
console.error("[messager] agent run failed:", e);
|
|
24413
24448
|
});
|
|
24414
24449
|
}
|
|
24415
24450
|
}
|
|
24416
24451
|
break;
|
|
24417
24452
|
}
|
|
24418
24453
|
case "typing": {
|
|
24454
|
+
if (channel_id) subscribe(ws, userId, channel_id);
|
|
24419
24455
|
broadcastToChannel(channel_id, {
|
|
24420
24456
|
type: "typing",
|
|
24421
24457
|
channel_id,
|
|
@@ -24426,6 +24462,7 @@ function createWSHandler(deps) {
|
|
|
24426
24462
|
}
|
|
24427
24463
|
case "read": {
|
|
24428
24464
|
if (!channel_id || !last_message_id) return;
|
|
24465
|
+
subscribe(ws, userId, channel_id);
|
|
24429
24466
|
await sql`
|
|
24430
24467
|
UPDATE "_channel_members"
|
|
24431
24468
|
SET last_read_id = ${last_message_id}, last_read_at = NOW()
|
|
@@ -24570,9 +24607,12 @@ function buildRouter3(deps) {
|
|
|
24570
24607
|
VALUES (${channelId}, ${am.member_id}, 'agent', ${result.output})
|
|
24571
24608
|
`.then(([r2]) => {
|
|
24572
24609
|
broadcastToChannel(channelId, { type: "message", data: r2 });
|
|
24610
|
+
}).catch((e) => {
|
|
24611
|
+
console.error("[messager] agent reply insert failed:", e);
|
|
24573
24612
|
});
|
|
24574
24613
|
}
|
|
24575
|
-
}).catch(() => {
|
|
24614
|
+
}).catch((e) => {
|
|
24615
|
+
console.error("[messager] agent run failed:", e);
|
|
24576
24616
|
});
|
|
24577
24617
|
}
|
|
24578
24618
|
}
|
|
@@ -24628,6 +24668,540 @@ function messager(options) {
|
|
|
24628
24668
|
}
|
|
24629
24669
|
};
|
|
24630
24670
|
}
|
|
24671
|
+
|
|
24672
|
+
// deploy/index.ts
|
|
24673
|
+
import { execSync } from "node:child_process";
|
|
24674
|
+
import fs from "node:fs";
|
|
24675
|
+
import path from "node:path";
|
|
24676
|
+
|
|
24677
|
+
// deploy/gateway.ts
|
|
24678
|
+
import WebSocket, { WebSocketServer as WebSocketServer2 } from "ws";
|
|
24679
|
+
function isBareDomain(host, domain) {
|
|
24680
|
+
return host === domain || host === `www.${domain}`;
|
|
24681
|
+
}
|
|
24682
|
+
function matchApp(config, getPort, host, pathname) {
|
|
24683
|
+
for (const [name15, ac] of Object.entries(config.apps)) {
|
|
24684
|
+
if (ac.subdomain && host === `${ac.subdomain}.${config.domain}`) {
|
|
24685
|
+
const port = getPort(name15);
|
|
24686
|
+
if (port) return { name: name15, port };
|
|
24687
|
+
}
|
|
24688
|
+
}
|
|
24689
|
+
const pathApps = Object.entries(config.apps).filter(([, ac]) => ac.path).sort(([, a], [, b]) => (b.path?.length ?? 0) - (a.path?.length ?? 0));
|
|
24690
|
+
for (const [name15, ac] of pathApps) {
|
|
24691
|
+
if (ac.path && pathname.startsWith(ac.path)) {
|
|
24692
|
+
const port = getPort(name15);
|
|
24693
|
+
if (port) return { name: name15, port, stripPath: ac.path };
|
|
24694
|
+
}
|
|
24695
|
+
}
|
|
24696
|
+
if (config.defaultApp && isBareDomain(host, config.domain)) {
|
|
24697
|
+
const port = getPort(config.defaultApp);
|
|
24698
|
+
if (port) return { name: config.defaultApp, port };
|
|
24699
|
+
}
|
|
24700
|
+
return void 0;
|
|
24701
|
+
}
|
|
24702
|
+
function createGateway(config, getPort) {
|
|
24703
|
+
const handler = async (req) => {
|
|
24704
|
+
const url = new URL(req.url);
|
|
24705
|
+
const match = matchApp(config, getPort, url.hostname, url.pathname);
|
|
24706
|
+
if (!match) return new Response("Not Found", { status: 404 });
|
|
24707
|
+
let targetPath = url.pathname;
|
|
24708
|
+
if (match.stripPath && targetPath.startsWith(match.stripPath)) {
|
|
24709
|
+
targetPath = targetPath.slice(match.stripPath.length) || "/";
|
|
24710
|
+
}
|
|
24711
|
+
const target = `http://127.0.0.1:${match.port}${targetPath}${url.search}`;
|
|
24712
|
+
try {
|
|
24713
|
+
const proxyReq = new Request(target, {
|
|
24714
|
+
method: req.method,
|
|
24715
|
+
headers: req.headers,
|
|
24716
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? req.body : null
|
|
24717
|
+
});
|
|
24718
|
+
return await fetch(proxyReq);
|
|
24719
|
+
} catch {
|
|
24720
|
+
return new Response("Bad Gateway", { status: 502 });
|
|
24721
|
+
}
|
|
24722
|
+
};
|
|
24723
|
+
const wss = new WebSocketServer2({ noServer: true });
|
|
24724
|
+
const wsHandler = (req, socket, head) => {
|
|
24725
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
24726
|
+
const host = req.headers.host?.split(":")[0] ?? "";
|
|
24727
|
+
const match = matchApp(config, getPort, host, url.pathname);
|
|
24728
|
+
if (!match) {
|
|
24729
|
+
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
24730
|
+
socket.destroy();
|
|
24731
|
+
return;
|
|
24732
|
+
}
|
|
24733
|
+
let targetPath = url.pathname;
|
|
24734
|
+
if (match.stripPath && targetPath.startsWith(match.stripPath)) {
|
|
24735
|
+
targetPath = targetPath.slice(match.stripPath.length) || "/";
|
|
24736
|
+
}
|
|
24737
|
+
const wsUrl = `ws://127.0.0.1:${match.port}${targetPath}${url.search}`;
|
|
24738
|
+
const backendWS = new WebSocket(wsUrl);
|
|
24739
|
+
backendWS.on("open", () => {
|
|
24740
|
+
wss.handleUpgrade(req, socket, head, (clientWS) => {
|
|
24741
|
+
const clientSend = (data) => {
|
|
24742
|
+
clientWS.send(data);
|
|
24743
|
+
};
|
|
24744
|
+
const backendSend = (data) => {
|
|
24745
|
+
backendWS.send(data);
|
|
24746
|
+
};
|
|
24747
|
+
clientWS.on("message", backendSend);
|
|
24748
|
+
backendWS.on("message", clientSend);
|
|
24749
|
+
clientWS.on("close", () => backendWS.close());
|
|
24750
|
+
backendWS.on("close", () => clientWS.close());
|
|
24751
|
+
clientWS.on("error", () => backendWS.close());
|
|
24752
|
+
backendWS.on("error", () => clientWS.close());
|
|
24753
|
+
});
|
|
24754
|
+
});
|
|
24755
|
+
backendWS.on("error", () => {
|
|
24756
|
+
socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
24757
|
+
socket.destroy();
|
|
24758
|
+
});
|
|
24759
|
+
};
|
|
24760
|
+
return { handler, wsHandler };
|
|
24761
|
+
}
|
|
24762
|
+
|
|
24763
|
+
// deploy/manager.ts
|
|
24764
|
+
import crypto4 from "node:crypto";
|
|
24765
|
+
function createManager(config, apps, manager) {
|
|
24766
|
+
const router = new Router();
|
|
24767
|
+
const auth2 = (req, ctx, next) => {
|
|
24768
|
+
if (!config.deployToken) return next(req, ctx);
|
|
24769
|
+
const header = req.headers.get("authorization") ?? "";
|
|
24770
|
+
const token = header.replace("Bearer ", "");
|
|
24771
|
+
if (token !== config.deployToken) {
|
|
24772
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
24773
|
+
}
|
|
24774
|
+
return next(req, ctx);
|
|
24775
|
+
};
|
|
24776
|
+
router.get("/apps", auth2, () => {
|
|
24777
|
+
const list = Array.from(apps.values()).map((a) => a.status);
|
|
24778
|
+
return Response.json(list);
|
|
24779
|
+
});
|
|
24780
|
+
router.get("/apps/:name", auth2, (req, ctx) => {
|
|
24781
|
+
const app = apps.get(ctx.params.name);
|
|
24782
|
+
if (!app) return new Response("Not Found", { status: 404 });
|
|
24783
|
+
return Response.json(app.status);
|
|
24784
|
+
});
|
|
24785
|
+
router.post("/apps/:name/deploy", auth2, async (req, ctx) => {
|
|
24786
|
+
const app = apps.get(ctx.params.name);
|
|
24787
|
+
if (!app) return new Response("Not Found", { status: 404 });
|
|
24788
|
+
try {
|
|
24789
|
+
await manager.deployApp(ctx.params.name);
|
|
24790
|
+
return Response.json({ success: true });
|
|
24791
|
+
} catch (err) {
|
|
24792
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24793
|
+
return Response.json({ error: msg }, { status: 500 });
|
|
24794
|
+
}
|
|
24795
|
+
});
|
|
24796
|
+
router.post("/apps/:name/restart", auth2, async (req, ctx) => {
|
|
24797
|
+
const app = apps.get(ctx.params.name);
|
|
24798
|
+
if (!app) return new Response("Not Found", { status: 404 });
|
|
24799
|
+
try {
|
|
24800
|
+
await manager.deployApp(ctx.params.name);
|
|
24801
|
+
return Response.json({ success: true });
|
|
24802
|
+
} catch (err) {
|
|
24803
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24804
|
+
return Response.json({ error: msg }, { status: 500 });
|
|
24805
|
+
}
|
|
24806
|
+
});
|
|
24807
|
+
router.post("/apps/:name/stop", auth2, async (req, ctx) => {
|
|
24808
|
+
const app = apps.get(ctx.params.name);
|
|
24809
|
+
if (!app) return new Response("Not Found", { status: 404 });
|
|
24810
|
+
if (app.process) {
|
|
24811
|
+
app.process.kill("SIGTERM");
|
|
24812
|
+
app.process = null;
|
|
24813
|
+
}
|
|
24814
|
+
app.status = { ...app.status, status: "stopped", pid: void 0 };
|
|
24815
|
+
return Response.json({ success: true });
|
|
24816
|
+
});
|
|
24817
|
+
router.post("/apps/:name/start", auth2, async (req, ctx) => {
|
|
24818
|
+
const app = apps.get(ctx.params.name);
|
|
24819
|
+
if (!app) return new Response("Not Found", { status: 404 });
|
|
24820
|
+
try {
|
|
24821
|
+
await manager.deployApp(ctx.params.name);
|
|
24822
|
+
return Response.json({ success: true });
|
|
24823
|
+
} catch (err) {
|
|
24824
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24825
|
+
return Response.json({ error: msg }, { status: 500 });
|
|
24826
|
+
}
|
|
24827
|
+
});
|
|
24828
|
+
router.get("/apps/:name/logs", auth2, (req, ctx) => {
|
|
24829
|
+
const app = apps.get(ctx.params.name);
|
|
24830
|
+
if (!app) return new Response("Not Found", { status: 404 });
|
|
24831
|
+
let index = app.logs.length;
|
|
24832
|
+
let interval;
|
|
24833
|
+
const stream = new ReadableStream({
|
|
24834
|
+
start(controller) {
|
|
24835
|
+
for (const line of app.logs) {
|
|
24836
|
+
controller.enqueue(`data: ${JSON.stringify({ line })}
|
|
24837
|
+
|
|
24838
|
+
`);
|
|
24839
|
+
}
|
|
24840
|
+
interval = setInterval(() => {
|
|
24841
|
+
while (index < app.logs.length) {
|
|
24842
|
+
controller.enqueue(`data: ${JSON.stringify({ line: app.logs[index] })}
|
|
24843
|
+
|
|
24844
|
+
`);
|
|
24845
|
+
index++;
|
|
24846
|
+
}
|
|
24847
|
+
}, 500);
|
|
24848
|
+
},
|
|
24849
|
+
cancel() {
|
|
24850
|
+
if (interval) clearInterval(interval);
|
|
24851
|
+
}
|
|
24852
|
+
});
|
|
24853
|
+
return new Response(stream, {
|
|
24854
|
+
headers: {
|
|
24855
|
+
"Content-Type": "text/event-stream",
|
|
24856
|
+
"Cache-Control": "no-cache"
|
|
24857
|
+
}
|
|
24858
|
+
});
|
|
24859
|
+
});
|
|
24860
|
+
router.post("/reload", auth2, async () => {
|
|
24861
|
+
try {
|
|
24862
|
+
await manager.reloadConfig();
|
|
24863
|
+
return Response.json({ success: true });
|
|
24864
|
+
} catch (err) {
|
|
24865
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24866
|
+
return Response.json({ error: msg }, { status: 400 });
|
|
24867
|
+
}
|
|
24868
|
+
});
|
|
24869
|
+
router.post("/webhook", async (req) => {
|
|
24870
|
+
if (config.webhookSecret) {
|
|
24871
|
+
const sig = req.headers.get("x-hub-signature-256") ?? "";
|
|
24872
|
+
const body = await req.clone().text();
|
|
24873
|
+
const hmac = crypto4.createHmac("sha256", config.webhookSecret).update(body).digest("hex");
|
|
24874
|
+
if (sig !== `sha256=${hmac}`) {
|
|
24875
|
+
return Response.json({ error: "invalid signature" }, { status: 401 });
|
|
24876
|
+
}
|
|
24877
|
+
}
|
|
24878
|
+
const payload = await req.json();
|
|
24879
|
+
const repoUrl = payload?.repository?.clone_url ?? payload?.repository?.html_url ?? "";
|
|
24880
|
+
if (!repoUrl) return Response.json({ deployed: [] });
|
|
24881
|
+
const deployed = [];
|
|
24882
|
+
for (const [name15] of apps) {
|
|
24883
|
+
const ac = apps.get(name15)?.config;
|
|
24884
|
+
if (!ac) continue;
|
|
24885
|
+
const repoNorm = ac.repo.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
|
|
24886
|
+
const payloadNorm = repoUrl.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
|
|
24887
|
+
if (payloadNorm.includes(repoNorm)) {
|
|
24888
|
+
await manager.deployApp(name15);
|
|
24889
|
+
deployed.push(name15);
|
|
24890
|
+
}
|
|
24891
|
+
}
|
|
24892
|
+
return Response.json({ deployed });
|
|
24893
|
+
});
|
|
24894
|
+
return router;
|
|
24895
|
+
}
|
|
24896
|
+
|
|
24897
|
+
// deploy/process.ts
|
|
24898
|
+
import { fork } from "node:child_process";
|
|
24899
|
+
function forkApp(opts) {
|
|
24900
|
+
const child = fork(opts.entry, [], {
|
|
24901
|
+
cwd: opts.cwd,
|
|
24902
|
+
env: {
|
|
24903
|
+
...process.env,
|
|
24904
|
+
...opts.env,
|
|
24905
|
+
PORT: String(opts.port)
|
|
24906
|
+
},
|
|
24907
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"]
|
|
24908
|
+
});
|
|
24909
|
+
child.stdout?.on("data", (chunk) => {
|
|
24910
|
+
for (const line of chunk.toString().split("\n").filter(Boolean)) {
|
|
24911
|
+
opts.onLog?.(line);
|
|
24912
|
+
}
|
|
24913
|
+
});
|
|
24914
|
+
child.stderr?.on("data", (chunk) => {
|
|
24915
|
+
for (const line of chunk.toString().split("\n").filter(Boolean)) {
|
|
24916
|
+
opts.onLog?.(`[error] ${line}`);
|
|
24917
|
+
}
|
|
24918
|
+
});
|
|
24919
|
+
return { child, port: opts.port };
|
|
24920
|
+
}
|
|
24921
|
+
function stopProcess(mp, timeout = 1e4) {
|
|
24922
|
+
return new Promise((resolve3) => {
|
|
24923
|
+
const timer = setTimeout(() => {
|
|
24924
|
+
mp.child.kill("SIGKILL");
|
|
24925
|
+
resolve3();
|
|
24926
|
+
}, timeout);
|
|
24927
|
+
mp.child.on("exit", () => {
|
|
24928
|
+
clearTimeout(timer);
|
|
24929
|
+
resolve3();
|
|
24930
|
+
});
|
|
24931
|
+
mp.child.kill("SIGTERM");
|
|
24932
|
+
});
|
|
24933
|
+
}
|
|
24934
|
+
async function healthCheck(port, path2 = "/") {
|
|
24935
|
+
try {
|
|
24936
|
+
const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
|
|
24937
|
+
signal: AbortSignal.timeout(5e3)
|
|
24938
|
+
});
|
|
24939
|
+
return res.ok;
|
|
24940
|
+
} catch {
|
|
24941
|
+
return false;
|
|
24942
|
+
}
|
|
24943
|
+
}
|
|
24944
|
+
|
|
24945
|
+
// deploy/config.ts
|
|
24946
|
+
function defineConfig(config) {
|
|
24947
|
+
if (!config.domain) throw new Error("deploy: domain is required");
|
|
24948
|
+
if (!config.apps || Object.keys(config.apps).length === 0) {
|
|
24949
|
+
throw new Error("deploy: at least one app is required");
|
|
24950
|
+
}
|
|
24951
|
+
for (const [name15, app] of Object.entries(config.apps)) {
|
|
24952
|
+
if (!app.repo) throw new Error(`deploy: app "${name15}" has no repo`);
|
|
24953
|
+
if (!app.entry) throw new Error(`deploy: app "${name15}" has no entry`);
|
|
24954
|
+
if (!app.port) throw new Error(`deploy: app "${name15}" has no port`);
|
|
24955
|
+
}
|
|
24956
|
+
return {
|
|
24957
|
+
port: config.port ?? 80,
|
|
24958
|
+
appsDir: config.appsDir ?? "/opt/weifuwu/apps",
|
|
24959
|
+
...config
|
|
24960
|
+
};
|
|
24961
|
+
}
|
|
24962
|
+
|
|
24963
|
+
// deploy/index.ts
|
|
24964
|
+
async function deploy(config) {
|
|
24965
|
+
const appsDir = config.appsDir ?? "/opt/weifuwu/apps";
|
|
24966
|
+
const apps = /* @__PURE__ */ new Map();
|
|
24967
|
+
let httpServer;
|
|
24968
|
+
if (!fs.existsSync(appsDir)) {
|
|
24969
|
+
fs.mkdirSync(appsDir, { recursive: true });
|
|
24970
|
+
}
|
|
24971
|
+
async function forkAndCheck(cwd, entry, port, env, onLog, healthEndpoint) {
|
|
24972
|
+
try {
|
|
24973
|
+
const mp = forkApp({ cwd, entry, port, env, onLog });
|
|
24974
|
+
onLog(`[deploy] forked pid ${mp.child.pid} on port ${mp.port}`);
|
|
24975
|
+
const healthy = await healthCheck(port, healthEndpoint ?? "/");
|
|
24976
|
+
if (healthy) onLog("[deploy] health check passed");
|
|
24977
|
+
else onLog("[deploy] health check failed");
|
|
24978
|
+
return mp;
|
|
24979
|
+
} catch (err) {
|
|
24980
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24981
|
+
onLog(`[deploy] fork error: ${msg}`);
|
|
24982
|
+
return null;
|
|
24983
|
+
}
|
|
24984
|
+
}
|
|
24985
|
+
function scheduleRestart(name15, runtime) {
|
|
24986
|
+
const delay = Math.min(1e3 * Math.pow(2, runtime.restartCount), 3e4);
|
|
24987
|
+
runtime.restartCount++;
|
|
24988
|
+
runtime.logs.push(`[deploy] auto-restart in ${delay}ms (attempt ${runtime.restartCount})`);
|
|
24989
|
+
runtime.restartTimer = setTimeout(() => initApp(name15), delay);
|
|
24990
|
+
}
|
|
24991
|
+
async function initApp(name15) {
|
|
24992
|
+
const ac = config.apps[name15];
|
|
24993
|
+
if (!ac) return;
|
|
24994
|
+
const old = apps.get(name15);
|
|
24995
|
+
if (old?.restartTimer) {
|
|
24996
|
+
clearTimeout(old.restartTimer);
|
|
24997
|
+
old.restartTimer = void 0;
|
|
24998
|
+
}
|
|
24999
|
+
const appDir = path.join(appsDir, name15);
|
|
25000
|
+
const logs = [];
|
|
25001
|
+
const log = (line) => {
|
|
25002
|
+
logs.push(line);
|
|
25003
|
+
if (logs.length > 1e3) logs.splice(0, logs.length - 1e3);
|
|
25004
|
+
};
|
|
25005
|
+
try {
|
|
25006
|
+
if (fs.existsSync(path.join(appDir, ".git"))) {
|
|
25007
|
+
execSync("git pull", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
|
|
25008
|
+
log("[deploy] git pull done");
|
|
25009
|
+
} else {
|
|
25010
|
+
if (fs.existsSync(appDir)) {
|
|
25011
|
+
fs.rmSync(appDir, { recursive: true });
|
|
25012
|
+
}
|
|
25013
|
+
execSync(`git clone ${ac.repo} ${appDir}`, { stdio: "pipe", timeout: 12e4 });
|
|
25014
|
+
log("[deploy] git clone done");
|
|
25015
|
+
if (ac.branch) {
|
|
25016
|
+
execSync(`git checkout ${ac.branch}`, { cwd: appDir, stdio: "pipe", timeout: 3e4 });
|
|
25017
|
+
log(`[deploy] switched to branch ${ac.branch}`);
|
|
25018
|
+
}
|
|
25019
|
+
}
|
|
25020
|
+
} catch (err) {
|
|
25021
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25022
|
+
setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
|
|
25023
|
+
log(`[deploy] git error: ${msg}`);
|
|
25024
|
+
if (old?.process) {
|
|
25025
|
+
apps.set(name15, old);
|
|
25026
|
+
}
|
|
25027
|
+
return;
|
|
25028
|
+
}
|
|
25029
|
+
try {
|
|
25030
|
+
execSync("npm install", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
|
|
25031
|
+
log("[deploy] npm install done");
|
|
25032
|
+
} catch (err) {
|
|
25033
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25034
|
+
setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
|
|
25035
|
+
log(`[deploy] npm install error: ${msg}`);
|
|
25036
|
+
if (old?.process) apps.set(name15, old);
|
|
25037
|
+
return;
|
|
25038
|
+
}
|
|
25039
|
+
if (ac.buildCommand) {
|
|
25040
|
+
try {
|
|
25041
|
+
execSync(ac.buildCommand, { cwd: appDir, stdio: "pipe", timeout: 12e4 });
|
|
25042
|
+
log("[deploy] build done");
|
|
25043
|
+
} catch (err) {
|
|
25044
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25045
|
+
setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
|
|
25046
|
+
log(`[deploy] build error: ${msg}`);
|
|
25047
|
+
if (old?.process) apps.set(name15, old);
|
|
25048
|
+
return;
|
|
25049
|
+
}
|
|
25050
|
+
}
|
|
25051
|
+
let targetPort = ac.port;
|
|
25052
|
+
if (ac.ports && old?.process) {
|
|
25053
|
+
targetPort = old.currentPort === ac.ports[0] ? ac.ports[1] : ac.ports[0];
|
|
25054
|
+
}
|
|
25055
|
+
const mp = await forkAndCheck(appDir, ac.entry, targetPort, ac.env, log, ac.healthEndpoint);
|
|
25056
|
+
if (!mp) {
|
|
25057
|
+
log("[deploy] new process failed to start, keeping old running");
|
|
25058
|
+
if (old?.process) apps.set(name15, old);
|
|
25059
|
+
else {
|
|
25060
|
+
setAppRuntime(name15, ac, logs, { status: "error", port: targetPort, error: "failed to start" });
|
|
25061
|
+
}
|
|
25062
|
+
return;
|
|
25063
|
+
}
|
|
25064
|
+
const runtime = {
|
|
25065
|
+
config: ac,
|
|
25066
|
+
status: { name: name15, status: "running", port: targetPort, subdomain: ac.subdomain, path: ac.path, pid: mp.child.pid ?? void 0 },
|
|
25067
|
+
logs,
|
|
25068
|
+
process: mp.child,
|
|
25069
|
+
currentPort: targetPort,
|
|
25070
|
+
startedAt: Date.now(),
|
|
25071
|
+
restartCount: 0,
|
|
25072
|
+
restartTimer: void 0
|
|
25073
|
+
};
|
|
25074
|
+
apps.set(name15, runtime);
|
|
25075
|
+
mp.child.on("exit", (code, signal) => {
|
|
25076
|
+
runtime.process = null;
|
|
25077
|
+
runtime.status = {
|
|
25078
|
+
...runtime.status,
|
|
25079
|
+
status: "error",
|
|
25080
|
+
error: `exited (code=${code} signal=${signal})`,
|
|
25081
|
+
pid: void 0
|
|
25082
|
+
};
|
|
25083
|
+
log(`[deploy] process exited code=${code} signal=${signal}`);
|
|
25084
|
+
if (code !== 0 && signal !== "SIGTERM") {
|
|
25085
|
+
scheduleRestart(name15, runtime);
|
|
25086
|
+
}
|
|
25087
|
+
});
|
|
25088
|
+
if (old?.process && old.currentPort !== targetPort) {
|
|
25089
|
+
if (old.restartTimer) clearTimeout(old.restartTimer);
|
|
25090
|
+
log(`[deploy] stopping old process on port ${old.currentPort}`);
|
|
25091
|
+
await stopProcess({ child: old.process, port: old.currentPort });
|
|
25092
|
+
}
|
|
25093
|
+
}
|
|
25094
|
+
function setAppRuntime(name15, ac, logs, overrides) {
|
|
25095
|
+
apps.set(name15, {
|
|
25096
|
+
config: ac,
|
|
25097
|
+
status: { name: name15, ...overrides },
|
|
25098
|
+
logs,
|
|
25099
|
+
process: null,
|
|
25100
|
+
currentPort: overrides.port ?? ac.port,
|
|
25101
|
+
startedAt: null,
|
|
25102
|
+
restartCount: 0,
|
|
25103
|
+
restartTimer: void 0
|
|
25104
|
+
});
|
|
25105
|
+
}
|
|
25106
|
+
for (const name15 of Object.keys(config.apps)) {
|
|
25107
|
+
await initApp(name15);
|
|
25108
|
+
}
|
|
25109
|
+
const getPort = (name15) => apps.get(name15)?.currentPort;
|
|
25110
|
+
const gw = createGateway(config, getPort);
|
|
25111
|
+
const managerRouter = createManager(config, apps, {
|
|
25112
|
+
deployApp: async (name15) => {
|
|
25113
|
+
await initApp(name15);
|
|
25114
|
+
},
|
|
25115
|
+
reloadConfig: async () => {
|
|
25116
|
+
throw new Error("reload not supported, restart the deploy process");
|
|
25117
|
+
}
|
|
25118
|
+
});
|
|
25119
|
+
const fullHandler = async (req, ctx) => {
|
|
25120
|
+
const url = new URL(req.url);
|
|
25121
|
+
if (url.pathname.startsWith("/_deploy")) {
|
|
25122
|
+
const stripped = url.pathname.replace("/_deploy", "") || "/";
|
|
25123
|
+
const rewritten = new URL(stripped + url.search, "http://deploy.local");
|
|
25124
|
+
const rewrittenReq = new Request(rewritten, req);
|
|
25125
|
+
return managerRouter.handler()(rewrittenReq, ctx);
|
|
25126
|
+
}
|
|
25127
|
+
return gw.handler(req, ctx);
|
|
25128
|
+
};
|
|
25129
|
+
if (config.ssl) {
|
|
25130
|
+
ensureCertificates(config);
|
|
25131
|
+
}
|
|
25132
|
+
httpServer = serve(fullHandler, {
|
|
25133
|
+
port: config.port,
|
|
25134
|
+
websocket: gw.wsHandler
|
|
25135
|
+
});
|
|
25136
|
+
const portSuffix = config.port !== 80 ? `:${config.port}` : "";
|
|
25137
|
+
return {
|
|
25138
|
+
stop: async () => {
|
|
25139
|
+
for (const [, app] of apps) {
|
|
25140
|
+
if (app.restartTimer) clearTimeout(app.restartTimer);
|
|
25141
|
+
if (app.process) {
|
|
25142
|
+
await stopProcess({ child: app.process, port: app.currentPort });
|
|
25143
|
+
}
|
|
25144
|
+
}
|
|
25145
|
+
httpServer?.stop();
|
|
25146
|
+
},
|
|
25147
|
+
ready: httpServer.ready,
|
|
25148
|
+
url: `http://${config.domain}${portSuffix}`,
|
|
25149
|
+
apps: {
|
|
25150
|
+
list: () => Array.from(apps.values()).map((a) => a.status),
|
|
25151
|
+
status: (name15) => apps.get(name15)?.status,
|
|
25152
|
+
deploy: async (name15) => {
|
|
25153
|
+
await initApp(name15);
|
|
25154
|
+
},
|
|
25155
|
+
restart: async (name15) => {
|
|
25156
|
+
await initApp(name15);
|
|
25157
|
+
},
|
|
25158
|
+
stop: async (name15) => {
|
|
25159
|
+
const app = apps.get(name15);
|
|
25160
|
+
if (app?.restartTimer) clearTimeout(app.restartTimer);
|
|
25161
|
+
if (app?.process) {
|
|
25162
|
+
await stopProcess({ child: app.process, port: app.currentPort });
|
|
25163
|
+
app.process = null;
|
|
25164
|
+
app.status = { ...app.status, status: "stopped", pid: void 0 };
|
|
25165
|
+
}
|
|
25166
|
+
},
|
|
25167
|
+
start: async (name15) => {
|
|
25168
|
+
await initApp(name15);
|
|
25169
|
+
}
|
|
25170
|
+
}
|
|
25171
|
+
};
|
|
25172
|
+
}
|
|
25173
|
+
function ensureCertificates(config) {
|
|
25174
|
+
const { domain, ssl } = config;
|
|
25175
|
+
if (!ssl) return;
|
|
25176
|
+
const certDir = "/etc/weifuwu/ssl";
|
|
25177
|
+
const certPath = path.join(certDir, `${domain}.pem`);
|
|
25178
|
+
const keyPath = path.join(certDir, `${domain}-key.pem`);
|
|
25179
|
+
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) return;
|
|
25180
|
+
if (!fs.existsSync(certDir)) {
|
|
25181
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
25182
|
+
}
|
|
25183
|
+
const acmeHome = path.join(certDir, ".acme.sh");
|
|
25184
|
+
try {
|
|
25185
|
+
execSync("which acme.sh", { stdio: "pipe" });
|
|
25186
|
+
} catch {
|
|
25187
|
+
execSync(
|
|
25188
|
+
`curl -s https://get.acme.sh | sh -s email=${ssl.email}`,
|
|
25189
|
+
{ stdio: "pipe", timeout: 6e4 }
|
|
25190
|
+
);
|
|
25191
|
+
}
|
|
25192
|
+
const subdomains = Object.values(config.apps).filter((a) => a.subdomain).map((a) => `${a.subdomain}.${domain}`).join(",");
|
|
25193
|
+
const allDomains = subdomains ? `${domain},${subdomains}` : domain;
|
|
25194
|
+
const acmeSh = path.join(acmeHome, "acme.sh");
|
|
25195
|
+
const staging = ssl.staging ? " --staging" : "";
|
|
25196
|
+
execSync(
|
|
25197
|
+
`${acmeSh} --issue -d ${allDomains} --standalone${staging} --cert-file ${certPath} --key-file ${keyPath}`,
|
|
25198
|
+
{ stdio: "pipe", timeout: 12e4 }
|
|
25199
|
+
);
|
|
25200
|
+
execSync(
|
|
25201
|
+
`${acmeSh} --install-cronjob`,
|
|
25202
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
25203
|
+
);
|
|
25204
|
+
}
|
|
24631
25205
|
export {
|
|
24632
25206
|
Router,
|
|
24633
25207
|
TsxContext,
|
|
@@ -24638,7 +25212,9 @@ export {
|
|
|
24638
25212
|
cors,
|
|
24639
25213
|
createSSEManager,
|
|
24640
25214
|
createWorkflowEngine,
|
|
25215
|
+
defineConfig,
|
|
24641
25216
|
deleteCookie,
|
|
25217
|
+
deploy,
|
|
24642
25218
|
generateWorkflow,
|
|
24643
25219
|
getCookies,
|
|
24644
25220
|
graphql,
|
package/dist/serve.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface ServeOptions {
|
|
|
6
6
|
hostname?: string;
|
|
7
7
|
signal?: AbortSignal;
|
|
8
8
|
websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
9
|
+
maxBodySize?: number;
|
|
9
10
|
}
|
|
10
11
|
export interface Server {
|
|
11
12
|
stop: () => void;
|
|
@@ -13,7 +14,7 @@ export interface Server {
|
|
|
13
14
|
readonly hostname: string;
|
|
14
15
|
ready: Promise<void>;
|
|
15
16
|
}
|
|
16
|
-
export declare function readBody(req: IncomingMessage): Promise<Buffer>;
|
|
17
|
+
export declare function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer>;
|
|
17
18
|
export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
|
|
18
19
|
export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
19
20
|
export declare function serve(handler: Handler, options?: ServeOptions): Server;
|