@kitecd/cli 1.2.0 → 1.3.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 +12 -0
- package/dist/server/index.js +2827 -137
- package/dist/upload.js +3 -1
- package/dist/web/assets/AuditLog-BO9BJdsk.js +1 -0
- package/dist/web/assets/ConfirmDialog-CikXU818.js +1 -0
- package/dist/web/assets/Dashboard-ho157_Gr.js +1 -0
- package/dist/web/assets/DefaultLayout-Y_852d55.js +1 -0
- package/dist/web/assets/DefaultLayout-lj5NNciV.css +1 -0
- package/dist/web/assets/FileExplorer-oxT6ZdHt.js +1 -0
- package/dist/web/assets/FolderPickerDialog-CDO-yrvE.css +1 -0
- package/dist/web/assets/FolderPickerDialog-CKohwBIP.js +1 -0
- package/dist/web/assets/LogBoard-DkvHTXcb.js +6 -0
- package/dist/web/assets/LogBoard-Dx8yNofc.css +1 -0
- package/dist/web/assets/LogTail-CuaBDDKf.js +9 -0
- package/dist/web/assets/Login-kcvq2T9U.js +1 -0
- package/dist/web/assets/Migration-C_M_Exzf.js +1 -0
- package/dist/web/assets/ProjectDetail-BTOfo71A.js +1 -0
- package/dist/web/assets/ProjectList-D_PoTDT9.js +1 -0
- package/dist/web/assets/ProjectList-YJlvRRNh.css +1 -0
- package/dist/web/assets/ProjectTagsEditor-zXN_5rwP.js +1 -0
- package/dist/web/assets/Settings-BL4hNZpU.js +1 -0
- package/dist/web/assets/Storage-B-pZjj08.js +1 -0
- package/dist/web/assets/Storage-CFbfZtA5.css +1 -0
- package/dist/web/assets/Terminal-Bh7bM6Bb.css +1 -0
- package/dist/web/assets/Terminal-fTZlKf2Y.js +7 -0
- package/dist/web/assets/{activity-Ba-YPfmn.js → activity-DZCOq6kQ.js} +1 -1
- package/dist/web/assets/{archive-CJ5gDBKY.js → archive-MB1JcYug.js} +1 -1
- package/dist/web/assets/arrow-left-CsZYc3XK.js +1 -0
- package/dist/web/assets/{circle-alert-DQzM-U4P.js → circle-alert-kmcvMchB.js} +1 -1
- package/dist/web/assets/clock-DIBzfDoY.js +1 -0
- package/dist/web/assets/constants-m1eFfRMw.js +1 -0
- package/dist/web/assets/{copy-C1rADgcQ.js → copy-CNXWZJbC.js} +1 -1
- package/dist/web/assets/createLucideIcon-BmsTm7z-.js +1 -0
- package/dist/web/assets/{database-CI6acTSY.js → database-D6rC_24l.js} +1 -1
- package/dist/web/assets/{eye-off-4PD31MPV.js → eye-off-BNUfzp8P.js} +1 -1
- package/dist/web/assets/{eye-Cas8HsmK.js → eye-vRDqRzR9.js} +1 -1
- package/dist/web/assets/file-text-BxWt6is5.js +1 -0
- package/dist/web/assets/{folder-D9pvA6oI.js → folder-CYPJgLMr.js} +1 -1
- package/dist/web/assets/{folder-open-CGeIri0U.js → folder-open-DQz0XviI.js} +1 -1
- package/dist/web/assets/{hard-drive-CGWwV4Ei.js → hard-drive-DBiQxkNS.js} +1 -1
- package/dist/web/assets/history-dkZbN_TB.js +1 -0
- package/dist/web/assets/{house-8ZvvlaEh.js → house-D9JIOKIs.js} +1 -1
- package/dist/web/assets/index-BZU6i5nw.js +2 -0
- package/dist/web/assets/index-BrlC5Hdt.css +1 -0
- package/dist/web/assets/loader-circle-OwtRa1dp.js +1 -0
- package/dist/web/assets/pencil-BTFuj0gA.js +1 -0
- package/dist/web/assets/plus-CcefsCv_.js +1 -0
- package/dist/web/assets/{refresh-cw-0yb8DZDc.js → refresh-cw-OqaxwQhF.js} +1 -1
- package/dist/web/assets/{rotate-ccw-YevaWXw9.js → rotate-ccw-D8C0iiAw.js} +1 -1
- package/dist/web/assets/{save-BuzCP3T1.js → save-D0NTjP8Q.js} +1 -1
- package/dist/web/assets/{scroll-text-BIYMFNN5.js → scroll-text-6UwJwqap.js} +1 -1
- package/dist/web/assets/{server-B31hj0g7.js → server-DFHpqwVn.js} +1 -1
- package/dist/web/assets/square-terminal-C1oJyHEG.js +1 -0
- package/dist/web/assets/{sun-BaEbKNDV.js → sun-Cg6PdQms.js} +1 -1
- package/dist/web/assets/terminal-CUz5b_ol.js +1 -0
- package/dist/web/assets/{trash-2-BWqtqkeW.js → trash-2-20ikd6fk.js} +1 -1
- package/dist/web/assets/useIntervalRaf-CXWU2lqg.js +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +10 -6
- package/scripts/postinstall.js +53 -0
- package/dist/web/assets/AuditLog-BFmJfgzL.js +0 -1
- package/dist/web/assets/ConfirmDialog-CJ8lJeUc.js +0 -1
- package/dist/web/assets/Dashboard-BTliTkq1.js +0 -1
- package/dist/web/assets/DefaultLayout-BG_y85yG.js +0 -1
- package/dist/web/assets/DefaultLayout-CZENO67n.css +0 -1
- package/dist/web/assets/FileExplorer-Bf32_MMS.js +0 -1
- package/dist/web/assets/LogBoard-0so-XiW3.css +0 -1
- package/dist/web/assets/LogBoard-CDz4n0DY.js +0 -6
- package/dist/web/assets/Login-C3zfObzP.js +0 -1
- package/dist/web/assets/Migration-BaKlcCkB.js +0 -1
- package/dist/web/assets/ProjectDetail-BL_D0OJY.js +0 -1
- package/dist/web/assets/ProjectList-7AnhOS7h.css +0 -1
- package/dist/web/assets/ProjectList-C4KuZEq4.js +0 -1
- package/dist/web/assets/Settings-CzWG9312.js +0 -1
- package/dist/web/assets/Storage-BCao_e7Y.js +0 -1
- package/dist/web/assets/Storage-DNadqpUy.css +0 -1
- package/dist/web/assets/arrow-left-WEOMLXIi.js +0 -1
- package/dist/web/assets/chevron-right-DLiDJVJl.js +0 -1
- package/dist/web/assets/clock-IwxBKgP4.js +0 -1
- package/dist/web/assets/constants-Ch47JPs3.js +0 -1
- package/dist/web/assets/createLucideIcon-CE-ry2oA.js +0 -1
- package/dist/web/assets/file-text-C6nzo1us.js +0 -1
- package/dist/web/assets/index-BFE6PEIL.js +0 -2
- package/dist/web/assets/index-XrzJwjrk.css +0 -1
- package/dist/web/assets/loader-circle-uiC7GaCG.js +0 -1
- package/dist/web/assets/plus-CfMi1Jv1.js +0 -1
- package/dist/web/assets/square-terminal-DT6aoXm0.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -32076,7 +32076,7 @@ var init_node2 = __esm(() => {
|
|
|
32076
32076
|
NodeRequestURL = class extends FastURL {
|
|
32077
32077
|
#req;
|
|
32078
32078
|
constructor({ req }) {
|
|
32079
|
-
const
|
|
32079
|
+
const path17 = req.url || "/";
|
|
32080
32080
|
let host = req.headers.host || req.headers[":authority"];
|
|
32081
32081
|
if (host && !HOST_RE.test(host))
|
|
32082
32082
|
host = "_invalid_";
|
|
@@ -32086,15 +32086,15 @@ var init_node2 = __esm(() => {
|
|
|
32086
32086
|
else
|
|
32087
32087
|
host = "localhost";
|
|
32088
32088
|
const protocol = req.socket?.encrypted || req.headers["x-forwarded-proto"] === "https" || req.headers[":scheme"] === "https" ? "https:" : "http:";
|
|
32089
|
-
if (
|
|
32090
|
-
const qIndex =
|
|
32089
|
+
if (path17[0] === "/") {
|
|
32090
|
+
const qIndex = path17.indexOf("?");
|
|
32091
32091
|
super({
|
|
32092
32092
|
protocol,
|
|
32093
32093
|
host,
|
|
32094
|
-
pathname: qIndex === -1 ?
|
|
32095
|
-
search: qIndex === -1 ? "" :
|
|
32094
|
+
pathname: qIndex === -1 ? path17 : path17.slice(0, qIndex) || "/",
|
|
32095
|
+
search: qIndex === -1 ? "" : path17.slice(qIndex) || ""
|
|
32096
32096
|
});
|
|
32097
|
-
} else if (
|
|
32097
|
+
} else if (path17 === "*")
|
|
32098
32098
|
super({
|
|
32099
32099
|
protocol,
|
|
32100
32100
|
host,
|
|
@@ -32102,7 +32102,7 @@ var init_node2 = __esm(() => {
|
|
|
32102
32102
|
search: ""
|
|
32103
32103
|
});
|
|
32104
32104
|
else
|
|
32105
|
-
super(
|
|
32105
|
+
super(path17);
|
|
32106
32106
|
this.#req = req;
|
|
32107
32107
|
}
|
|
32108
32108
|
get pathname() {
|
|
@@ -32643,7 +32643,7 @@ __export(exports_dist, {
|
|
|
32643
32643
|
});
|
|
32644
32644
|
function createWebSocketAdapter() {
|
|
32645
32645
|
const store = {};
|
|
32646
|
-
function handler(app,
|
|
32646
|
+
function handler(app, path17, options) {
|
|
32647
32647
|
const { parse: parse5, body, response, ...rest3 } = options;
|
|
32648
32648
|
const validateMessage = getSchemaValidator(body, {
|
|
32649
32649
|
modules: app.definitions.typebox,
|
|
@@ -32668,7 +32668,7 @@ function createWebSocketAdapter() {
|
|
|
32668
32668
|
};
|
|
32669
32669
|
return ws;
|
|
32670
32670
|
};
|
|
32671
|
-
app.route("WS",
|
|
32671
|
+
app.route("WS", path17, async (context) => {
|
|
32672
32672
|
const { set: set22, path: path22, qi, headers, query, params } = context;
|
|
32673
32673
|
const id = context.request.wsId;
|
|
32674
32674
|
context.validator = validateResponse;
|
|
@@ -32885,8 +32885,8 @@ var handleFile2 = (response, set22) => {
|
|
|
32885
32885
|
}, handleElysiaFile2 = (file2, set22 = {
|
|
32886
32886
|
headers: {}
|
|
32887
32887
|
}) => {
|
|
32888
|
-
const
|
|
32889
|
-
const contentType = mime[
|
|
32888
|
+
const path17 = file2.path;
|
|
32889
|
+
const contentType = mime[path17.slice(path17.lastIndexOf(".") + 1)];
|
|
32890
32890
|
if (contentType)
|
|
32891
32891
|
set22.headers["content-type"] = contentType;
|
|
32892
32892
|
if (file2.stats && set22 && set22.status !== 206 && set22.status !== 304 && set22.status !== 412 && set22.status !== 416)
|
|
@@ -33411,8 +33411,11 @@ import { drizzle } from "drizzle-orm/libsql";
|
|
|
33411
33411
|
// src/db/schema.ts
|
|
33412
33412
|
var exports_schema = {};
|
|
33413
33413
|
__export(exports_schema, {
|
|
33414
|
+
tags: () => tags,
|
|
33414
33415
|
settings: () => settings,
|
|
33415
33416
|
projects: () => projects,
|
|
33417
|
+
projectTags: () => projectTags,
|
|
33418
|
+
projectLogSources: () => projectLogSources,
|
|
33416
33419
|
deployments: () => deployments,
|
|
33417
33420
|
categories: () => categories,
|
|
33418
33421
|
auditLogs: () => auditLogs
|
|
@@ -33426,11 +33429,13 @@ var projects = sqliteTable("projects", {
|
|
|
33426
33429
|
token: text("token").notNull().unique(),
|
|
33427
33430
|
preDeployScript: text("pre_deploy_script"),
|
|
33428
33431
|
postDeployScript: text("post_deploy_script"),
|
|
33432
|
+
postDeployAsync: integer3("post_deploy_async", { mode: "boolean" }).default(false),
|
|
33429
33433
|
env: text("env"),
|
|
33430
33434
|
status: text("status").default("idle"),
|
|
33431
33435
|
cleanMode: text("clean_mode"),
|
|
33432
33436
|
protectPaths: text("protect_paths"),
|
|
33433
33437
|
categoryId: text("category_id"),
|
|
33438
|
+
pm2AppName: text("pm2_app_name"),
|
|
33434
33439
|
createdAt: text("created_at").notNull(),
|
|
33435
33440
|
updatedAt: text("updated_at").notNull()
|
|
33436
33441
|
});
|
|
@@ -33460,6 +33465,29 @@ var deployments = sqliteTable("deployments", {
|
|
|
33460
33465
|
artifactSize: integer3("artifact_size"),
|
|
33461
33466
|
rollbackOf: text("rollback_of")
|
|
33462
33467
|
});
|
|
33468
|
+
var projectLogSources = sqliteTable("project_log_sources", {
|
|
33469
|
+
id: text("id").primaryKey(),
|
|
33470
|
+
projectId: text("project_id").references(() => projects.id).notNull(),
|
|
33471
|
+
label: text("label").notNull(),
|
|
33472
|
+
filePath: text("file_path").notNull(),
|
|
33473
|
+
kind: text("kind").default("plain"),
|
|
33474
|
+
sortOrder: integer3("sort_order").default(0),
|
|
33475
|
+
createdAt: text("created_at").notNull(),
|
|
33476
|
+
updatedAt: text("updated_at").notNull()
|
|
33477
|
+
});
|
|
33478
|
+
var tags = sqliteTable("tags", {
|
|
33479
|
+
id: text("id").primaryKey(),
|
|
33480
|
+
name: text("name").notNull().unique(),
|
|
33481
|
+
color: text("color"),
|
|
33482
|
+
sortOrder: integer3("sort_order").default(0),
|
|
33483
|
+
createdAt: text("created_at").notNull(),
|
|
33484
|
+
updatedAt: text("updated_at").notNull()
|
|
33485
|
+
});
|
|
33486
|
+
var projectTags = sqliteTable("project_tags", {
|
|
33487
|
+
projectId: text("project_id").references(() => projects.id).notNull(),
|
|
33488
|
+
tagId: text("tag_id").references(() => tags.id).notNull(),
|
|
33489
|
+
createdAt: text("created_at").notNull()
|
|
33490
|
+
});
|
|
33463
33491
|
var auditLogs = sqliteTable("audit_logs", {
|
|
33464
33492
|
id: text("id").primaryKey(),
|
|
33465
33493
|
createdAt: text("created_at").notNull(),
|
|
@@ -33492,6 +33520,7 @@ var initDb = async () => {
|
|
|
33492
33520
|
token TEXT NOT NULL UNIQUE,
|
|
33493
33521
|
pre_deploy_script TEXT,
|
|
33494
33522
|
post_deploy_script TEXT,
|
|
33523
|
+
post_deploy_async INTEGER DEFAULT 0,
|
|
33495
33524
|
env TEXT,
|
|
33496
33525
|
status TEXT DEFAULT 'idle',
|
|
33497
33526
|
created_at TEXT NOT NULL,
|
|
@@ -33511,6 +33540,12 @@ var initDb = async () => {
|
|
|
33511
33540
|
await client.execute(`ALTER TABLE projects ADD COLUMN category_id TEXT`);
|
|
33512
33541
|
} catch {}
|
|
33513
33542
|
await client.execute(`CREATE INDEX IF NOT EXISTS idx_projects_category_id ON projects(category_id);`);
|
|
33543
|
+
try {
|
|
33544
|
+
await client.execute(`ALTER TABLE projects ADD COLUMN post_deploy_async INTEGER DEFAULT 0`);
|
|
33545
|
+
} catch {}
|
|
33546
|
+
try {
|
|
33547
|
+
await client.execute(`ALTER TABLE projects ADD COLUMN pm2_app_name TEXT`);
|
|
33548
|
+
} catch {}
|
|
33514
33549
|
await client.execute(`
|
|
33515
33550
|
CREATE TABLE IF NOT EXISTS categories (
|
|
33516
33551
|
id TEXT PRIMARY KEY,
|
|
@@ -33522,6 +33557,27 @@ var initDb = async () => {
|
|
|
33522
33557
|
);
|
|
33523
33558
|
`);
|
|
33524
33559
|
await client.execute(`CREATE INDEX IF NOT EXISTS idx_categories_sort_order ON categories(sort_order);`);
|
|
33560
|
+
await client.execute(`
|
|
33561
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
33562
|
+
id TEXT PRIMARY KEY,
|
|
33563
|
+
name TEXT NOT NULL UNIQUE,
|
|
33564
|
+
color TEXT,
|
|
33565
|
+
sort_order INTEGER DEFAULT 0,
|
|
33566
|
+
created_at TEXT NOT NULL,
|
|
33567
|
+
updated_at TEXT NOT NULL
|
|
33568
|
+
);
|
|
33569
|
+
`);
|
|
33570
|
+
await client.execute(`CREATE INDEX IF NOT EXISTS idx_tags_sort_order ON tags(sort_order);`);
|
|
33571
|
+
await client.execute(`
|
|
33572
|
+
CREATE TABLE IF NOT EXISTS project_tags (
|
|
33573
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
33574
|
+
tag_id TEXT NOT NULL REFERENCES tags(id),
|
|
33575
|
+
created_at TEXT NOT NULL,
|
|
33576
|
+
PRIMARY KEY (project_id, tag_id)
|
|
33577
|
+
);
|
|
33578
|
+
`);
|
|
33579
|
+
await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_tags_project_id ON project_tags(project_id);`);
|
|
33580
|
+
await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_tags_tag_id ON project_tags(tag_id);`);
|
|
33525
33581
|
await client.execute(`
|
|
33526
33582
|
CREATE TABLE IF NOT EXISTS settings (
|
|
33527
33583
|
key TEXT PRIMARY KEY,
|
|
@@ -33552,6 +33608,36 @@ var initDb = async () => {
|
|
|
33552
33608
|
sql: `INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`,
|
|
33553
33609
|
args: ["artifact_keep_n", "10"]
|
|
33554
33610
|
});
|
|
33611
|
+
const seededRow = await client.execute({
|
|
33612
|
+
sql: "SELECT value FROM settings WHERE key = ?",
|
|
33613
|
+
args: ["tags.seeded"]
|
|
33614
|
+
});
|
|
33615
|
+
if (!seededRow.rows[0]) {
|
|
33616
|
+
const now = new Date().toISOString();
|
|
33617
|
+
const seedTags = [
|
|
33618
|
+
{ name: "前端", color: "blue" },
|
|
33619
|
+
{ name: "后端", color: "green" },
|
|
33620
|
+
{ name: "Node", color: "green" },
|
|
33621
|
+
{ name: "Java", color: "yellow" },
|
|
33622
|
+
{ name: "Go", color: "cyan" },
|
|
33623
|
+
{ name: "Python", color: "purple" },
|
|
33624
|
+
{ name: "PM2", color: "pink" },
|
|
33625
|
+
{ name: "Docker", color: "cyan" },
|
|
33626
|
+
{ name: "SSR", color: "purple" },
|
|
33627
|
+
{ name: "静态站点", color: "gray" }
|
|
33628
|
+
];
|
|
33629
|
+
for (let i = 0;i < seedTags.length; i++) {
|
|
33630
|
+
const t2 = seedTags[i];
|
|
33631
|
+
await client.execute({
|
|
33632
|
+
sql: `INSERT OR IGNORE INTO tags (id, name, color, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
33633
|
+
args: ["tag_" + randomUUID().replace(/-/g, "").substring(0, 12), t2.name, t2.color, i, now, now]
|
|
33634
|
+
});
|
|
33635
|
+
}
|
|
33636
|
+
await client.execute({
|
|
33637
|
+
sql: `INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`,
|
|
33638
|
+
args: ["tags.seeded", "1"]
|
|
33639
|
+
});
|
|
33640
|
+
}
|
|
33555
33641
|
await client.execute(`
|
|
33556
33642
|
CREATE TABLE IF NOT EXISTS deployments (
|
|
33557
33643
|
id TEXT PRIMARY KEY,
|
|
@@ -33597,6 +33683,20 @@ var initDb = async () => {
|
|
|
33597
33683
|
await client.execute(`CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);`);
|
|
33598
33684
|
await client.execute(`CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);`);
|
|
33599
33685
|
await client.execute(`CREATE INDEX IF NOT EXISTS idx_audit_logs_target_id ON audit_logs(target_id);`);
|
|
33686
|
+
await client.execute(`
|
|
33687
|
+
CREATE TABLE IF NOT EXISTS project_log_sources (
|
|
33688
|
+
id TEXT PRIMARY KEY,
|
|
33689
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
33690
|
+
label TEXT NOT NULL,
|
|
33691
|
+
file_path TEXT NOT NULL,
|
|
33692
|
+
kind TEXT DEFAULT 'plain',
|
|
33693
|
+
sort_order INTEGER DEFAULT 0,
|
|
33694
|
+
created_at TEXT NOT NULL,
|
|
33695
|
+
updated_at TEXT NOT NULL
|
|
33696
|
+
);
|
|
33697
|
+
`);
|
|
33698
|
+
await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_log_sources_project_id ON project_log_sources(project_id);`);
|
|
33699
|
+
await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_log_sources_sort_order ON project_log_sources(sort_order);`);
|
|
33600
33700
|
if (process.env.KITE_SEED_DEMO_PROJECT !== "false") {
|
|
33601
33701
|
const existing = await client.execute("SELECT COUNT(*) as count FROM projects");
|
|
33602
33702
|
const count = existing.rows[0]?.count ?? 0;
|
|
@@ -33717,10 +33817,59 @@ var db = {
|
|
|
33717
33817
|
if (!project)
|
|
33718
33818
|
return false;
|
|
33719
33819
|
await ormDb.delete(deployments).where(eq(deployments.projectId, id));
|
|
33820
|
+
await ormDb.delete(projectLogSources).where(eq(projectLogSources.projectId, id));
|
|
33821
|
+
await ormDb.delete(projectTags).where(eq(projectTags.projectId, id));
|
|
33720
33822
|
await ormDb.delete(projects).where(eq(projects.id, id));
|
|
33721
33823
|
return true;
|
|
33722
33824
|
}
|
|
33723
33825
|
},
|
|
33826
|
+
logSources: {
|
|
33827
|
+
async findByProject(projectId) {
|
|
33828
|
+
await ensureDbReady();
|
|
33829
|
+
return await ormDb.select().from(projectLogSources).where(eq(projectLogSources.projectId, projectId)).orderBy(asc(projectLogSources.sortOrder), asc(projectLogSources.label));
|
|
33830
|
+
},
|
|
33831
|
+
async findById(id) {
|
|
33832
|
+
await ensureDbReady();
|
|
33833
|
+
const result = await ormDb.select().from(projectLogSources).where(eq(projectLogSources.id, id)).limit(1);
|
|
33834
|
+
return result[0] || null;
|
|
33835
|
+
},
|
|
33836
|
+
async create(data) {
|
|
33837
|
+
await ensureDbReady();
|
|
33838
|
+
const now = new Date().toISOString();
|
|
33839
|
+
const row = {
|
|
33840
|
+
id: "lsrc_" + randomUUID().replace(/-/g, "").substring(0, 12),
|
|
33841
|
+
projectId: data.projectId,
|
|
33842
|
+
label: data.label,
|
|
33843
|
+
filePath: data.filePath,
|
|
33844
|
+
kind: data.kind ?? "plain",
|
|
33845
|
+
sortOrder: data.sortOrder ?? 0,
|
|
33846
|
+
createdAt: now,
|
|
33847
|
+
updatedAt: now
|
|
33848
|
+
};
|
|
33849
|
+
await ormDb.insert(projectLogSources).values(row);
|
|
33850
|
+
return row;
|
|
33851
|
+
},
|
|
33852
|
+
async update(id, data) {
|
|
33853
|
+
await ensureDbReady();
|
|
33854
|
+
const patch = { updatedAt: new Date().toISOString() };
|
|
33855
|
+
if (data.label !== undefined)
|
|
33856
|
+
patch.label = data.label;
|
|
33857
|
+
if (data.kind !== undefined)
|
|
33858
|
+
patch.kind = data.kind;
|
|
33859
|
+
if (data.sortOrder !== undefined)
|
|
33860
|
+
patch.sortOrder = data.sortOrder;
|
|
33861
|
+
await ormDb.update(projectLogSources).set(patch).where(eq(projectLogSources.id, id));
|
|
33862
|
+
return this.findById(id);
|
|
33863
|
+
},
|
|
33864
|
+
async remove(id) {
|
|
33865
|
+
await ensureDbReady();
|
|
33866
|
+
const existing = await this.findById(id);
|
|
33867
|
+
if (!existing)
|
|
33868
|
+
return false;
|
|
33869
|
+
await ormDb.delete(projectLogSources).where(eq(projectLogSources.id, id));
|
|
33870
|
+
return true;
|
|
33871
|
+
}
|
|
33872
|
+
},
|
|
33724
33873
|
categories: {
|
|
33725
33874
|
async findAll() {
|
|
33726
33875
|
await ensureDbReady();
|
|
@@ -33780,6 +33929,107 @@ var db = {
|
|
|
33780
33929
|
return Number(result.rows[0]?.count ?? 0);
|
|
33781
33930
|
}
|
|
33782
33931
|
},
|
|
33932
|
+
tags: {
|
|
33933
|
+
async findAll() {
|
|
33934
|
+
await ensureDbReady();
|
|
33935
|
+
return await ormDb.select().from(tags).orderBy(asc(tags.sortOrder), asc(tags.name));
|
|
33936
|
+
},
|
|
33937
|
+
async findById(id) {
|
|
33938
|
+
await ensureDbReady();
|
|
33939
|
+
const result = await ormDb.select().from(tags).where(eq(tags.id, id)).limit(1);
|
|
33940
|
+
return result[0] || null;
|
|
33941
|
+
},
|
|
33942
|
+
async findByName(name) {
|
|
33943
|
+
await ensureDbReady();
|
|
33944
|
+
const result = await ormDb.select().from(tags).where(eq(tags.name, name)).limit(1);
|
|
33945
|
+
return result[0] || null;
|
|
33946
|
+
},
|
|
33947
|
+
async create(data) {
|
|
33948
|
+
await ensureDbReady();
|
|
33949
|
+
const now = new Date().toISOString();
|
|
33950
|
+
const row = {
|
|
33951
|
+
id: data.id || "tag_" + randomUUID().replace(/-/g, "").substring(0, 12),
|
|
33952
|
+
name: data.name,
|
|
33953
|
+
color: data.color ?? null,
|
|
33954
|
+
sortOrder: data.sortOrder ?? 0,
|
|
33955
|
+
createdAt: now,
|
|
33956
|
+
updatedAt: now
|
|
33957
|
+
};
|
|
33958
|
+
await ormDb.insert(tags).values(row);
|
|
33959
|
+
return row;
|
|
33960
|
+
},
|
|
33961
|
+
async update(id, data) {
|
|
33962
|
+
await ensureDbReady();
|
|
33963
|
+
const patch = { updatedAt: new Date().toISOString() };
|
|
33964
|
+
if (data.name !== undefined)
|
|
33965
|
+
patch.name = data.name;
|
|
33966
|
+
if (data.color !== undefined)
|
|
33967
|
+
patch.color = data.color;
|
|
33968
|
+
if (data.sortOrder !== undefined)
|
|
33969
|
+
patch.sortOrder = data.sortOrder;
|
|
33970
|
+
await ormDb.update(tags).set(patch).where(eq(tags.id, id));
|
|
33971
|
+
return this.findById(id);
|
|
33972
|
+
},
|
|
33973
|
+
async remove(id) {
|
|
33974
|
+
await ensureDbReady();
|
|
33975
|
+
const tag = await this.findById(id);
|
|
33976
|
+
if (!tag)
|
|
33977
|
+
return false;
|
|
33978
|
+
await ormDb.delete(projectTags).where(eq(projectTags.tagId, id));
|
|
33979
|
+
await ormDb.delete(tags).where(eq(tags.id, id));
|
|
33980
|
+
return true;
|
|
33981
|
+
},
|
|
33982
|
+
async countProjects(id) {
|
|
33983
|
+
await ensureDbReady();
|
|
33984
|
+
const result = await client.execute({
|
|
33985
|
+
sql: "SELECT COUNT(*) as count FROM project_tags WHERE tag_id = ?",
|
|
33986
|
+
args: [id]
|
|
33987
|
+
});
|
|
33988
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
33989
|
+
}
|
|
33990
|
+
},
|
|
33991
|
+
projectTags: {
|
|
33992
|
+
async listByProject(projectId) {
|
|
33993
|
+
await ensureDbReady();
|
|
33994
|
+
const rows = await ormDb.select().from(projectTags).where(eq(projectTags.projectId, projectId));
|
|
33995
|
+
return rows.map((r) => r.tagId);
|
|
33996
|
+
},
|
|
33997
|
+
async listAllPairs() {
|
|
33998
|
+
await ensureDbReady();
|
|
33999
|
+
const rows = await ormDb.select().from(projectTags);
|
|
34000
|
+
return rows.map((r) => ({ projectId: r.projectId, tagId: r.tagId }));
|
|
34001
|
+
},
|
|
34002
|
+
async setForProject(projectId, tagIds) {
|
|
34003
|
+
await ensureDbReady();
|
|
34004
|
+
const now = new Date().toISOString();
|
|
34005
|
+
await ormDb.delete(projectTags).where(eq(projectTags.projectId, projectId));
|
|
34006
|
+
const unique = Array.from(new Set(tagIds.filter(Boolean)));
|
|
34007
|
+
for (const tagId of unique) {
|
|
34008
|
+
try {
|
|
34009
|
+
await ormDb.insert(projectTags).values({ projectId, tagId, createdAt: now });
|
|
34010
|
+
} catch {}
|
|
34011
|
+
}
|
|
34012
|
+
},
|
|
34013
|
+
async projectIdsHavingAll(tagIds) {
|
|
34014
|
+
await ensureDbReady();
|
|
34015
|
+
const ids = Array.from(new Set(tagIds.filter(Boolean)));
|
|
34016
|
+
if (ids.length === 0)
|
|
34017
|
+
return [];
|
|
34018
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
34019
|
+
const res = await client.execute({
|
|
34020
|
+
sql: `SELECT project_id AS pid FROM project_tags
|
|
34021
|
+
WHERE tag_id IN (${placeholders})
|
|
34022
|
+
GROUP BY project_id
|
|
34023
|
+
HAVING COUNT(DISTINCT tag_id) = ?`,
|
|
34024
|
+
args: [...ids, ids.length]
|
|
34025
|
+
});
|
|
34026
|
+
return res.rows.map((r) => String(r.pid));
|
|
34027
|
+
},
|
|
34028
|
+
async clearProject(projectId) {
|
|
34029
|
+
await ensureDbReady();
|
|
34030
|
+
await ormDb.delete(projectTags).where(eq(projectTags.projectId, projectId));
|
|
34031
|
+
}
|
|
34032
|
+
},
|
|
33783
34033
|
deployments: {
|
|
33784
34034
|
async insert(data) {
|
|
33785
34035
|
await ensureDbReady();
|
|
@@ -36373,7 +36623,7 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
|
|
|
36373
36623
|
if (guard4.locked) {
|
|
36374
36624
|
const retrySec = Math.ceil(guard4.retryAfterMs / 1000);
|
|
36375
36625
|
set3.status = 429;
|
|
36376
|
-
set3.headers
|
|
36626
|
+
set3.headers["Retry-After"] = String(retrySec);
|
|
36377
36627
|
await writeAudit({ headers }, {
|
|
36378
36628
|
action: "auth.login_failed",
|
|
36379
36629
|
targetType: "auth",
|
|
@@ -36400,12 +36650,28 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
|
|
|
36400
36650
|
return { success: false, message: "Invalid token" };
|
|
36401
36651
|
}, {
|
|
36402
36652
|
body: t.Object({ token: t.String() })
|
|
36403
|
-
}).get("/api/projects", async ({ headers, set: set3 }) => {
|
|
36653
|
+
}).get("/api/projects", async ({ headers, query, set: set3 }) => {
|
|
36404
36654
|
if (!verifyAdminToken(headers)) {
|
|
36405
36655
|
set3.status = 401;
|
|
36406
36656
|
return { error: "Unauthorized" };
|
|
36407
36657
|
}
|
|
36408
|
-
|
|
36658
|
+
const items = await db.projects.findAllWithMeta();
|
|
36659
|
+
const pairs = await db.projectTags.listAllPairs();
|
|
36660
|
+
const tagMap = new Map;
|
|
36661
|
+
for (const p of pairs) {
|
|
36662
|
+
if (!tagMap.has(p.projectId))
|
|
36663
|
+
tagMap.set(p.projectId, []);
|
|
36664
|
+
tagMap.get(p.projectId).push(p.tagId);
|
|
36665
|
+
}
|
|
36666
|
+
const withTags = items.map((p) => ({ ...p, tagIds: tagMap.get(p.id) ?? [] }));
|
|
36667
|
+
const tagIdsParam = typeof query.tagIds === "string" ? query.tagIds.trim() : "";
|
|
36668
|
+
if (!tagIdsParam)
|
|
36669
|
+
return withTags;
|
|
36670
|
+
const wantedIds = tagIdsParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
36671
|
+
if (wantedIds.length === 0)
|
|
36672
|
+
return withTags;
|
|
36673
|
+
const matchIds = new Set(await db.projectTags.projectIdsHavingAll(wantedIds));
|
|
36674
|
+
return withTags.filter((p) => matchIds.has(p.id));
|
|
36409
36675
|
}).post("/api/projects", async ({ headers, body, set: set3 }) => {
|
|
36410
36676
|
if (!verifyAdminToken(headers)) {
|
|
36411
36677
|
set3.status = 401;
|
|
@@ -36426,29 +36692,53 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
|
|
|
36426
36692
|
}
|
|
36427
36693
|
categoryId = cat.id;
|
|
36428
36694
|
}
|
|
36695
|
+
const pm2AppName = typeof body.pm2AppName === "string" && body.pm2AppName.trim() !== "" ? body.pm2AppName.trim() : null;
|
|
36696
|
+
const tagIds = [];
|
|
36697
|
+
if (Array.isArray(body.tagIds)) {
|
|
36698
|
+
for (const tid of body.tagIds) {
|
|
36699
|
+
if (typeof tid !== "string" || !tid)
|
|
36700
|
+
continue;
|
|
36701
|
+
const tag = await db.tags.findById(tid);
|
|
36702
|
+
if (tag)
|
|
36703
|
+
tagIds.push(tag.id);
|
|
36704
|
+
}
|
|
36705
|
+
}
|
|
36706
|
+
if (pm2AppName) {
|
|
36707
|
+
const pm2Tag = await db.tags.findByName("PM2");
|
|
36708
|
+
if (pm2Tag && !tagIds.includes(pm2Tag.id)) {
|
|
36709
|
+
tagIds.push(pm2Tag.id);
|
|
36710
|
+
}
|
|
36711
|
+
}
|
|
36712
|
+
const { tagIds: _omitTagIds, ...rest3 } = body;
|
|
36429
36713
|
const project = await db.projects.create({
|
|
36430
|
-
...
|
|
36714
|
+
...rest3,
|
|
36431
36715
|
categoryId,
|
|
36716
|
+
pm2AppName,
|
|
36432
36717
|
id: "proj_" + randomUUID2().replace(/-/g, "").substring(0, 12),
|
|
36433
36718
|
token: "kt_" + randomUUID2().replace(/-/g, "")
|
|
36434
36719
|
});
|
|
36720
|
+
if (tagIds.length > 0) {
|
|
36721
|
+
await db.projectTags.setForProject(project.id, tagIds);
|
|
36722
|
+
}
|
|
36435
36723
|
await writeAudit({ headers }, {
|
|
36436
36724
|
action: "project.create",
|
|
36437
36725
|
targetType: "project",
|
|
36438
36726
|
targetId: project.id,
|
|
36439
36727
|
targetName: project.name,
|
|
36440
36728
|
before: null,
|
|
36441
|
-
after: sanitize2(project),
|
|
36729
|
+
after: { ...sanitize2(project), tagIds },
|
|
36442
36730
|
summary: `创建项目 ${project.name}`
|
|
36443
36731
|
});
|
|
36444
|
-
return { success: true, project };
|
|
36732
|
+
return { success: true, project: { ...project, tagIds } };
|
|
36445
36733
|
}, {
|
|
36446
36734
|
body: t.Object({
|
|
36447
36735
|
name: t.String(),
|
|
36448
36736
|
description: t.Optional(t.String()),
|
|
36449
36737
|
deployPath: t.String(),
|
|
36450
36738
|
env: t.Optional(t.String()),
|
|
36451
|
-
categoryId: t.Optional(t.Union([t.String(), t.Null()]))
|
|
36739
|
+
categoryId: t.Optional(t.Union([t.String(), t.Null()])),
|
|
36740
|
+
pm2AppName: t.Optional(t.Union([t.String(), t.Null()])),
|
|
36741
|
+
tagIds: t.Optional(t.Array(t.String()))
|
|
36452
36742
|
})
|
|
36453
36743
|
}).get("/api/projects/:id", async ({ headers, params, set: set3 }) => {
|
|
36454
36744
|
if (!verifyAdminToken(headers)) {
|
|
@@ -36460,7 +36750,8 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
|
|
|
36460
36750
|
set3.status = 404;
|
|
36461
36751
|
return { error: "Project not found" };
|
|
36462
36752
|
}
|
|
36463
|
-
|
|
36753
|
+
const tagIds = await db.projectTags.listByProject(params.id);
|
|
36754
|
+
return { ...project, tagIds };
|
|
36464
36755
|
}).put("/api/projects/:id", async ({ headers, params, body, set: set3 }) => {
|
|
36465
36756
|
if (!verifyAdminToken(headers)) {
|
|
36466
36757
|
set3.status = 401;
|
|
@@ -36526,12 +36817,62 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
|
|
|
36526
36817
|
return { error: "categoryId must be string or null" };
|
|
36527
36818
|
}
|
|
36528
36819
|
}
|
|
36820
|
+
if (typeof patch.pm2AppName !== "undefined") {
|
|
36821
|
+
if (patch.pm2AppName === null) {} else if (typeof patch.pm2AppName === "string") {
|
|
36822
|
+
const v = patch.pm2AppName.trim();
|
|
36823
|
+
patch.pm2AppName = v === "" ? null : v;
|
|
36824
|
+
} else {
|
|
36825
|
+
set3.status = 400;
|
|
36826
|
+
return { error: "pm2AppName must be string or null" };
|
|
36827
|
+
}
|
|
36828
|
+
}
|
|
36829
|
+
let nextTagIds;
|
|
36830
|
+
if (typeof patch.tagIds !== "undefined") {
|
|
36831
|
+
if (!Array.isArray(patch.tagIds)) {
|
|
36832
|
+
set3.status = 400;
|
|
36833
|
+
return { error: "tagIds must be string[]" };
|
|
36834
|
+
}
|
|
36835
|
+
const validated = [];
|
|
36836
|
+
for (const tid of patch.tagIds) {
|
|
36837
|
+
if (typeof tid !== "string" || !tid)
|
|
36838
|
+
continue;
|
|
36839
|
+
const tag = await db.tags.findById(tid);
|
|
36840
|
+
if (tag)
|
|
36841
|
+
validated.push(tag.id);
|
|
36842
|
+
}
|
|
36843
|
+
nextTagIds = validated;
|
|
36844
|
+
delete patch.tagIds;
|
|
36845
|
+
}
|
|
36529
36846
|
const after = await db.projects.update(params.id, patch);
|
|
36530
36847
|
if (!after) {
|
|
36531
36848
|
set3.status = 404;
|
|
36532
36849
|
return { error: "Project not found" };
|
|
36533
36850
|
}
|
|
36851
|
+
const beforeTagIds = await db.projectTags.listByProject(params.id);
|
|
36852
|
+
const beforePm2 = (before.pm2AppName || "").trim();
|
|
36853
|
+
const afterPm2 = (after.pm2AppName || "").trim();
|
|
36854
|
+
if (afterPm2 && afterPm2 !== beforePm2) {
|
|
36855
|
+
const pm2Tag = await db.tags.findByName("PM2");
|
|
36856
|
+
if (pm2Tag) {
|
|
36857
|
+
const base = nextTagIds !== undefined ? nextTagIds : [...beforeTagIds];
|
|
36858
|
+
if (!base.includes(pm2Tag.id)) {
|
|
36859
|
+
base.push(pm2Tag.id);
|
|
36860
|
+
nextTagIds = base;
|
|
36861
|
+
}
|
|
36862
|
+
}
|
|
36863
|
+
}
|
|
36864
|
+
if (nextTagIds !== undefined) {
|
|
36865
|
+
await db.projectTags.setForProject(params.id, nextTagIds);
|
|
36866
|
+
}
|
|
36534
36867
|
const diff = diffFields(before, after, Object.keys(patch));
|
|
36868
|
+
if (nextTagIds !== undefined) {
|
|
36869
|
+
const sortedBefore = [...beforeTagIds].sort();
|
|
36870
|
+
const sortedAfter = [...nextTagIds].sort();
|
|
36871
|
+
if (sortedBefore.join(",") !== sortedAfter.join(",")) {
|
|
36872
|
+
diff.before.tagIds = sortedBefore;
|
|
36873
|
+
diff.after.tagIds = sortedAfter;
|
|
36874
|
+
}
|
|
36875
|
+
}
|
|
36535
36876
|
if (Object.keys(diff.after).length > 0) {
|
|
36536
36877
|
await writeAudit({ headers }, {
|
|
36537
36878
|
action: "project.update",
|
|
@@ -36543,18 +36884,22 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
|
|
|
36543
36884
|
summary: `更新项目配置:${Object.keys(diff.after).join(", ")}`
|
|
36544
36885
|
});
|
|
36545
36886
|
}
|
|
36546
|
-
|
|
36887
|
+
const respTagIds = nextTagIds !== undefined ? nextTagIds : beforeTagIds;
|
|
36888
|
+
return { success: true, project: { ...after, tagIds: respTagIds } };
|
|
36547
36889
|
}, {
|
|
36548
36890
|
body: t.Object({
|
|
36549
36891
|
name: t.Optional(t.String()),
|
|
36550
36892
|
preDeployScript: t.Optional(t.String()),
|
|
36551
36893
|
postDeployScript: t.Optional(t.String()),
|
|
36894
|
+
postDeployAsync: t.Optional(t.Boolean()),
|
|
36552
36895
|
deployPath: t.Optional(t.String()),
|
|
36553
36896
|
description: t.Optional(t.String()),
|
|
36554
36897
|
env: t.Optional(t.String()),
|
|
36555
36898
|
cleanMode: t.Optional(t.Union([t.Literal("merge"), t.Literal("clean"), t.Literal("clean-all"), t.Null()])),
|
|
36556
36899
|
protectPaths: t.Optional(t.Union([t.Array(t.String()), t.Null()])),
|
|
36557
|
-
categoryId: t.Optional(t.Union([t.String(), t.Null()]))
|
|
36900
|
+
categoryId: t.Optional(t.Union([t.String(), t.Null()])),
|
|
36901
|
+
pm2AppName: t.Optional(t.Union([t.String(), t.Null()])),
|
|
36902
|
+
tagIds: t.Optional(t.Array(t.String()))
|
|
36558
36903
|
})
|
|
36559
36904
|
}).delete("/api/projects/:id", async ({ headers, params, set: set3 }) => {
|
|
36560
36905
|
if (!verifyAdminToken(headers)) {
|
|
@@ -36690,6 +37035,19 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
36690
37035
|
}
|
|
36691
37036
|
const preDeployCmd = body.preDeploy || project.preDeployScript;
|
|
36692
37037
|
const postDeployCmd = body.postDeploy || project.postDeployScript;
|
|
37038
|
+
const parseBool = (v) => {
|
|
37039
|
+
if (typeof v === "boolean")
|
|
37040
|
+
return v;
|
|
37041
|
+
if (typeof v === "string") {
|
|
37042
|
+
if (v === "true" || v === "1")
|
|
37043
|
+
return true;
|
|
37044
|
+
if (v === "false" || v === "0" || v === "")
|
|
37045
|
+
return false;
|
|
37046
|
+
}
|
|
37047
|
+
return;
|
|
37048
|
+
};
|
|
37049
|
+
const overrideAsync = parseBool(body.postDeployAsync);
|
|
37050
|
+
const postDeployAsync = overrideAsync !== undefined ? overrideAsync : Boolean(project.postDeployAsync);
|
|
36693
37051
|
let deployEnv;
|
|
36694
37052
|
if (body.env) {
|
|
36695
37053
|
try {
|
|
@@ -36796,22 +37154,56 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
36796
37154
|
await appendLog(`[Kite Deploy] Extracting files...`);
|
|
36797
37155
|
new import_adm_zip.default(tempZipPath).extractAllTo(destPath, true);
|
|
36798
37156
|
if (postDeployCmd) {
|
|
36799
|
-
|
|
36800
|
-
|
|
36801
|
-
|
|
36802
|
-
|
|
36803
|
-
|
|
36804
|
-
|
|
36805
|
-
|
|
36806
|
-
|
|
37157
|
+
if (postDeployAsync) {
|
|
37158
|
+
const dispatchMsg = `[Kite Deploy] Dispatching Post-deploy asynchronously (not waiting): ${postDeployCmd}`;
|
|
37159
|
+
sendEvent(controller, "log", { data: dispatchMsg });
|
|
37160
|
+
await appendLog(dispatchMsg);
|
|
37161
|
+
(async () => {
|
|
37162
|
+
try {
|
|
37163
|
+
for await (const line of runShellCommand(postDeployCmd, destPath, deployEnv)) {
|
|
37164
|
+
if (line.startsWith("\x00EXIT:")) {
|
|
37165
|
+
const exitCode = parseInt(line.slice(6));
|
|
37166
|
+
const exitMsg = exitCode === 0 ? `[Kite Deploy] (async) Post-deploy exited with code 0` : `[Kite Deploy] (async) Post-deploy exited with code ${exitCode}`;
|
|
37167
|
+
await appendLog(exitMsg);
|
|
37168
|
+
if (exitCode !== 0) {
|
|
37169
|
+
try {
|
|
37170
|
+
await writeAudit({ headers }, {
|
|
37171
|
+
action: "deploy.post_deploy_failed",
|
|
37172
|
+
targetType: "project",
|
|
37173
|
+
targetId: project.id,
|
|
37174
|
+
targetName: project.name,
|
|
37175
|
+
summary: `异步 postDeploy 退出码 ${exitCode}(部署 ${deploymentRow.id})`,
|
|
37176
|
+
status: "failed",
|
|
37177
|
+
errorMessage: `Post-deploy exited with code ${exitCode}`
|
|
37178
|
+
});
|
|
37179
|
+
} catch {}
|
|
37180
|
+
}
|
|
37181
|
+
} else {
|
|
37182
|
+
await appendLog(line);
|
|
37183
|
+
}
|
|
37184
|
+
}
|
|
37185
|
+
} catch (asyncErr) {
|
|
37186
|
+
await appendLog(`[Kite Deploy] (async) Post-deploy error: ${asyncErr?.message || asyncErr}`);
|
|
37187
|
+
}
|
|
37188
|
+
})();
|
|
37189
|
+
} else {
|
|
37190
|
+
sendEvent(controller, "log", { data: `[Kite Deploy] Running Post-deploy: ${postDeployCmd}` });
|
|
37191
|
+
await appendLog(`[Kite Deploy] Running Post-deploy: ${postDeployCmd}`);
|
|
37192
|
+
let failed = false;
|
|
37193
|
+
for await (const line of runShellCommand(postDeployCmd, destPath, deployEnv)) {
|
|
37194
|
+
if (line.startsWith("\x00EXIT:")) {
|
|
37195
|
+
const exitCode = parseInt(line.slice(6));
|
|
37196
|
+
if (exitCode !== 0) {
|
|
37197
|
+
failed = true;
|
|
37198
|
+
}
|
|
37199
|
+
} else {
|
|
37200
|
+
sendEvent(controller, "log", { data: line });
|
|
37201
|
+
await appendLog(line);
|
|
36807
37202
|
}
|
|
36808
|
-
} else {
|
|
36809
|
-
sendEvent(controller, "log", { data: line });
|
|
36810
|
-
await appendLog(line);
|
|
36811
37203
|
}
|
|
37204
|
+
if (failed)
|
|
37205
|
+
throw new Error("Post-deploy failed");
|
|
36812
37206
|
}
|
|
36813
|
-
if (failed)
|
|
36814
|
-
throw new Error("Post-deploy failed");
|
|
36815
37207
|
}
|
|
36816
37208
|
await fs3.unlink(tempZipPath);
|
|
36817
37209
|
const durationStr = ((Date.now() - startTime) / 1000).toFixed(1) + "s";
|
|
@@ -36866,6 +37258,7 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
36866
37258
|
projectId: t.String(),
|
|
36867
37259
|
preDeploy: t.Optional(t.String()),
|
|
36868
37260
|
postDeploy: t.Optional(t.String()),
|
|
37261
|
+
postDeployAsync: t.Optional(t.Union([t.Boolean(), t.String()])),
|
|
36869
37262
|
env: t.Optional(t.Any()),
|
|
36870
37263
|
startedAt: t.Optional(t.String())
|
|
36871
37264
|
})
|
|
@@ -36888,7 +37281,7 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
36888
37281
|
}
|
|
36889
37282
|
try {
|
|
36890
37283
|
const entries = await fs3.readdir(targetPath, { withFileTypes: true });
|
|
36891
|
-
const items = await Promise.all(entries.
|
|
37284
|
+
const items = await Promise.all(entries.map(async (entry) => {
|
|
36892
37285
|
const fullPath = path5.join(targetPath, entry.name);
|
|
36893
37286
|
const relativePath = subPath ? `${subPath}/${entry.name}` : entry.name;
|
|
36894
37287
|
const stat2 = await fs3.stat(fullPath);
|
|
@@ -36896,6 +37289,7 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
36896
37289
|
name: entry.name,
|
|
36897
37290
|
path: relativePath,
|
|
36898
37291
|
isDir: entry.isDirectory(),
|
|
37292
|
+
isHidden: entry.name.startsWith("."),
|
|
36899
37293
|
size: stat2.size,
|
|
36900
37294
|
mtime: stat2.mtime.toISOString()
|
|
36901
37295
|
};
|
|
@@ -36903,6 +37297,8 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
36903
37297
|
items.sort((a, b) => {
|
|
36904
37298
|
if (a.isDir !== b.isDir)
|
|
36905
37299
|
return a.isDir ? -1 : 1;
|
|
37300
|
+
if (a.isHidden !== b.isHidden)
|
|
37301
|
+
return a.isHidden ? 1 : -1;
|
|
36906
37302
|
return a.name.localeCompare(b.name);
|
|
36907
37303
|
});
|
|
36908
37304
|
return items;
|
|
@@ -37051,6 +37447,67 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
|
|
|
37051
37447
|
cleanMode: t.Optional(t.Union([t.Literal("merge"), t.Literal("clean"), t.Literal("clean-all"), t.Null()])),
|
|
37052
37448
|
protectPaths: t.Optional(t.Array(t.String()))
|
|
37053
37449
|
})
|
|
37450
|
+
}).patch("/api/deployments/:id/status", async ({ headers, params, body, set: set3 }) => {
|
|
37451
|
+
if (!verifyAdminToken(headers)) {
|
|
37452
|
+
set3.status = 401;
|
|
37453
|
+
return { error: "Unauthorized" };
|
|
37454
|
+
}
|
|
37455
|
+
const nextStatus = body.status;
|
|
37456
|
+
if (nextStatus !== "success" && nextStatus !== "failed") {
|
|
37457
|
+
set3.status = 400;
|
|
37458
|
+
return { error: 'status must be "success" or "failed"' };
|
|
37459
|
+
}
|
|
37460
|
+
const deployment = await db.deployments.findById(params.id);
|
|
37461
|
+
if (!deployment) {
|
|
37462
|
+
set3.status = 404;
|
|
37463
|
+
return { error: "Deployment not found" };
|
|
37464
|
+
}
|
|
37465
|
+
if (deployment.status !== "running") {
|
|
37466
|
+
set3.status = 409;
|
|
37467
|
+
return { error: `Deployment is already ${deployment.status}, cannot mark`, code: "NOT_RUNNING" };
|
|
37468
|
+
}
|
|
37469
|
+
const endTimeIso = new Date().toISOString();
|
|
37470
|
+
const startMs = new Date(deployment.startTime).getTime();
|
|
37471
|
+
const endMs = new Date(endTimeIso).getTime();
|
|
37472
|
+
const durationStr = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs ? ((endMs - startMs) / 1000).toFixed(1) + "s" : deployment.duration || "0s";
|
|
37473
|
+
const markLine = `[Kite] Manually marked as ${nextStatus} by admin at ${endTimeIso}`;
|
|
37474
|
+
const nextOutput = deployment.output ? `${deployment.output}
|
|
37475
|
+
${markLine}` : markLine;
|
|
37476
|
+
await db.deployments.update(deployment.id, {
|
|
37477
|
+
status: nextStatus,
|
|
37478
|
+
endTime: endTimeIso,
|
|
37479
|
+
duration: durationStr,
|
|
37480
|
+
output: nextOutput
|
|
37481
|
+
});
|
|
37482
|
+
const project = await db.projects.findById(deployment.projectId);
|
|
37483
|
+
if (project && project.status === "running") {
|
|
37484
|
+
await db.projects.update(project.id, { status: nextStatus });
|
|
37485
|
+
}
|
|
37486
|
+
broadcastToSubscribers(deployment.id, "log", markLine);
|
|
37487
|
+
broadcastToSubscribers(deployment.id, "status", JSON.stringify({ status: nextStatus, duration: durationStr }));
|
|
37488
|
+
await writeAudit({ headers }, {
|
|
37489
|
+
action: "deployment.mark_status",
|
|
37490
|
+
targetType: "deployment",
|
|
37491
|
+
targetId: deployment.id,
|
|
37492
|
+
targetName: deployment.projectName,
|
|
37493
|
+
before: { status: "running", endTime: deployment.endTime ?? null, duration: deployment.duration ?? null },
|
|
37494
|
+
after: { status: nextStatus, endTime: endTimeIso, duration: durationStr },
|
|
37495
|
+
summary: `手动将部署 ${deployment.id.slice(0, 8)} 标记为 ${nextStatus}`
|
|
37496
|
+
});
|
|
37497
|
+
return {
|
|
37498
|
+
success: true,
|
|
37499
|
+
deployment: {
|
|
37500
|
+
...deployment,
|
|
37501
|
+
status: nextStatus,
|
|
37502
|
+
endTime: endTimeIso,
|
|
37503
|
+
duration: durationStr,
|
|
37504
|
+
output: nextOutput
|
|
37505
|
+
}
|
|
37506
|
+
};
|
|
37507
|
+
}, {
|
|
37508
|
+
body: t.Object({
|
|
37509
|
+
status: t.Union([t.Literal("success"), t.Literal("failed")])
|
|
37510
|
+
})
|
|
37054
37511
|
}).post("/api/deployments/:id/rollback", async ({ headers, params, set: set3 }) => {
|
|
37055
37512
|
if (!verifyAdminToken(headers)) {
|
|
37056
37513
|
set3.status = 401;
|
|
@@ -37235,7 +37692,7 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
|
|
|
37235
37692
|
set3.status = 401;
|
|
37236
37693
|
return { error: "Unauthorized" };
|
|
37237
37694
|
}
|
|
37238
|
-
const allowed = ["webhook_url", "webhook_events", "default_deploy_path", "max_upload_size", "global_deploy_token", "artifact_keep_n"];
|
|
37695
|
+
const allowed = ["webhook_url", "webhook_events", "default_deploy_path", "max_upload_size", "global_deploy_token", "artifact_keep_n", "deployment_stuck_threshold_min"];
|
|
37239
37696
|
const beforeAll = await db.settings.getAll();
|
|
37240
37697
|
const entries = {};
|
|
37241
37698
|
for (const [key, value2] of Object.entries(body)) {
|
|
@@ -37250,6 +37707,14 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
|
|
|
37250
37707
|
return { error: `global_deploy_token 强度不足:${result.reason}` };
|
|
37251
37708
|
}
|
|
37252
37709
|
}
|
|
37710
|
+
if (Object.prototype.hasOwnProperty.call(entries, "deployment_stuck_threshold_min")) {
|
|
37711
|
+
const n = Number(entries.deployment_stuck_threshold_min);
|
|
37712
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 1440) {
|
|
37713
|
+
set3.status = 400;
|
|
37714
|
+
return { error: "deployment_stuck_threshold_min 必须为 1~1440 之间的整数(分钟)" };
|
|
37715
|
+
}
|
|
37716
|
+
entries.deployment_stuck_threshold_min = String(n);
|
|
37717
|
+
}
|
|
37253
37718
|
await db.settings.setMany(entries);
|
|
37254
37719
|
const afterAll = await db.settings.getAll();
|
|
37255
37720
|
const changedKeys = Object.keys(entries);
|
|
@@ -37271,7 +37736,8 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
|
|
|
37271
37736
|
default_deploy_path: t.Optional(t.String()),
|
|
37272
37737
|
max_upload_size: t.Optional(t.String()),
|
|
37273
37738
|
global_deploy_token: t.Optional(t.String()),
|
|
37274
|
-
artifact_keep_n: t.Optional(t.String())
|
|
37739
|
+
artifact_keep_n: t.Optional(t.String()),
|
|
37740
|
+
deployment_stuck_threshold_min: t.Optional(t.String())
|
|
37275
37741
|
})
|
|
37276
37742
|
}).post("/api/settings/token", async ({ headers, body, set: set3 }) => {
|
|
37277
37743
|
if (!verifyAdminToken(headers)) {
|
|
@@ -37283,7 +37749,9 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
|
|
|
37283
37749
|
if (guard4.locked) {
|
|
37284
37750
|
const retrySec = Math.ceil(guard4.retryAfterMs / 1000);
|
|
37285
37751
|
set3.status = 429;
|
|
37286
|
-
|
|
37752
|
+
if (!set3.headers)
|
|
37753
|
+
set3.headers = {};
|
|
37754
|
+
set3.headers["Retry-After"] = String(retrySec);
|
|
37287
37755
|
return { error: `Too many attempts, please retry after ${retrySec}s` };
|
|
37288
37756
|
}
|
|
37289
37757
|
const { oldToken, newToken } = body;
|
|
@@ -38641,33 +39109,47 @@ var fsRoutes = new Elysia().get("/api/fs/home", ({ headers, set: set3 }) => {
|
|
|
38641
39109
|
set3.status = 500;
|
|
38642
39110
|
return { error: err?.message || "readdir failed", path: normalized };
|
|
38643
39111
|
}
|
|
38644
|
-
const
|
|
38645
|
-
const
|
|
38646
|
-
const
|
|
39112
|
+
const includeRaw = typeof query.include === "string" ? query.include : "dirs";
|
|
39113
|
+
const include = includeRaw === "files" || includeRaw === "both" ? includeRaw : "dirs";
|
|
39114
|
+
const wantedEntries = raw.filter((e) => e.isDirectory() || e.isFile() || e.isSymbolicLink());
|
|
39115
|
+
const truncated = wantedEntries.length > MAX_ENTRIES;
|
|
39116
|
+
const sliced = truncated ? wantedEntries.slice(0, MAX_ENTRIES) : wantedEntries;
|
|
38647
39117
|
const entries = await Promise.all(sliced.map(async (e) => {
|
|
38648
39118
|
const full = path11.join(normalized, e.name);
|
|
38649
39119
|
let isDir = e.isDirectory();
|
|
39120
|
+
let isFile = e.isFile();
|
|
38650
39121
|
const isSymlink = e.isSymbolicLink();
|
|
38651
|
-
if (!isDir &&
|
|
39122
|
+
if (isSymlink && !isDir && !isFile) {
|
|
38652
39123
|
try {
|
|
38653
39124
|
const s = await fs9.stat(full);
|
|
38654
39125
|
isDir = s.isDirectory();
|
|
39126
|
+
isFile = s.isFile();
|
|
38655
39127
|
} catch {
|
|
38656
39128
|
isDir = false;
|
|
39129
|
+
isFile = false;
|
|
38657
39130
|
}
|
|
38658
39131
|
}
|
|
38659
39132
|
return {
|
|
38660
39133
|
name: e.name,
|
|
38661
39134
|
path: full,
|
|
38662
39135
|
isDir,
|
|
39136
|
+
isFile,
|
|
38663
39137
|
isHidden: e.name.startsWith("."),
|
|
38664
39138
|
isSymlink
|
|
38665
39139
|
};
|
|
38666
39140
|
}));
|
|
38667
|
-
const
|
|
38668
|
-
|
|
39141
|
+
const filtered = entries.filter((e) => {
|
|
39142
|
+
if (include === "dirs")
|
|
39143
|
+
return e.isDir;
|
|
39144
|
+
if (include === "files")
|
|
39145
|
+
return e.isFile;
|
|
39146
|
+
return e.isDir || e.isFile;
|
|
39147
|
+
});
|
|
39148
|
+
filtered.sort((a, b) => {
|
|
38669
39149
|
if (a.isHidden !== b.isHidden)
|
|
38670
39150
|
return a.isHidden ? 1 : -1;
|
|
39151
|
+
if (a.isDir !== b.isDir)
|
|
39152
|
+
return a.isDir ? -1 : 1;
|
|
38671
39153
|
return a.name.localeCompare(b.name);
|
|
38672
39154
|
});
|
|
38673
39155
|
return {
|
|
@@ -38676,7 +39158,7 @@ var fsRoutes = new Elysia().get("/api/fs/home", ({ headers, set: set3 }) => {
|
|
|
38676
39158
|
exists: true,
|
|
38677
39159
|
isDir: true,
|
|
38678
39160
|
truncated,
|
|
38679
|
-
entries:
|
|
39161
|
+
entries: filtered
|
|
38680
39162
|
};
|
|
38681
39163
|
});
|
|
38682
39164
|
|
|
@@ -38842,58 +39324,2197 @@ var categoryRoutes = new Elysia().get("/api/categories", async ({ headers, set:
|
|
|
38842
39324
|
return { success: true, detachedProjects: projectCount };
|
|
38843
39325
|
});
|
|
38844
39326
|
|
|
38845
|
-
// src/
|
|
39327
|
+
// src/routes/log-sources.ts
|
|
38846
39328
|
init_dist3();
|
|
39329
|
+
import path13 from "node:path";
|
|
39330
|
+
|
|
39331
|
+
// src/lib/log-tail.ts
|
|
39332
|
+
import fs10 from "node:fs";
|
|
39333
|
+
import fsp from "node:fs/promises";
|
|
38847
39334
|
import path12 from "node:path";
|
|
38848
|
-
|
|
38849
|
-
|
|
38850
|
-
|
|
38851
|
-
|
|
38852
|
-
|
|
38853
|
-
|
|
38854
|
-
|
|
38855
|
-
|
|
38856
|
-
|
|
38857
|
-
|
|
38858
|
-
|
|
38859
|
-
|
|
38860
|
-
|
|
38861
|
-
|
|
38862
|
-
|
|
38863
|
-
|
|
38864
|
-
|
|
38865
|
-
|
|
38866
|
-
|
|
38867
|
-
|
|
38868
|
-
|
|
38869
|
-
|
|
39335
|
+
import os5 from "node:os";
|
|
39336
|
+
import readline from "node:readline";
|
|
39337
|
+
var log6 = moduleLogger("log-tail");
|
|
39338
|
+
var MAX_RANGE_SIZE = 1024 * 1024;
|
|
39339
|
+
var DEFAULT_RANGE_SIZE = 64 * 1024;
|
|
39340
|
+
var MAX_TAIL_LINES = 5000;
|
|
39341
|
+
var DEFAULT_TAIL_LINES = 200;
|
|
39342
|
+
var MAX_SEARCH_HITS = 5000;
|
|
39343
|
+
var DEFAULT_SEARCH_HITS = 500;
|
|
39344
|
+
var DEFAULT_SEARCH_CONTEXT = 2;
|
|
39345
|
+
var STREAM_HEARTBEAT_MS = 15000;
|
|
39346
|
+
var STREAM_POLL_MS = 1500;
|
|
39347
|
+
var STREAM_FLUSH_MS = 500;
|
|
39348
|
+
function getBlacklist() {
|
|
39349
|
+
const home = process.env.KITE_HOME || path12.join(os5.homedir(), ".kite");
|
|
39350
|
+
return [
|
|
39351
|
+
path12.join(home, "config.json"),
|
|
39352
|
+
path12.join(home, "kite.db"),
|
|
39353
|
+
"/etc/shadow",
|
|
39354
|
+
"/etc/passwd",
|
|
39355
|
+
"/etc/master.passwd"
|
|
39356
|
+
];
|
|
38870
39357
|
}
|
|
38871
|
-
|
|
38872
|
-
|
|
38873
|
-
|
|
38874
|
-
let pathname = url.pathname;
|
|
38875
|
-
if (!webDir) {
|
|
38876
|
-
if (pathname === "/")
|
|
38877
|
-
return "Deploy Server is running!";
|
|
38878
|
-
set3.status = 404;
|
|
38879
|
-
return { error: "Web console not available" };
|
|
39358
|
+
async function pathGuard(rawPath, opts = {}) {
|
|
39359
|
+
if (typeof rawPath !== "string" || !rawPath) {
|
|
39360
|
+
return { ok: false, status: 400, error: "path is required" };
|
|
38880
39361
|
}
|
|
38881
|
-
|
|
38882
|
-
|
|
38883
|
-
set3.status = 403;
|
|
38884
|
-
return { error: "Access denied" };
|
|
39362
|
+
if (!path12.isAbsolute(rawPath)) {
|
|
39363
|
+
return { ok: false, status: 400, error: "path must be absolute" };
|
|
38885
39364
|
}
|
|
38886
|
-
|
|
39365
|
+
let resolved = path12.resolve(rawPath);
|
|
38887
39366
|
try {
|
|
38888
|
-
|
|
38889
|
-
|
|
38890
|
-
|
|
38891
|
-
|
|
38892
|
-
|
|
38893
|
-
return
|
|
38894
|
-
}
|
|
39367
|
+
resolved = await fsp.realpath(resolved);
|
|
39368
|
+
} catch (err) {
|
|
39369
|
+
if (err?.code === "ENOENT") {
|
|
39370
|
+
if (opts.allowMissing)
|
|
39371
|
+
return { ok: true, resolved };
|
|
39372
|
+
return { ok: false, status: 404, error: "path not found", resolved };
|
|
39373
|
+
}
|
|
39374
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
39375
|
+
return { ok: false, status: 403, error: "permission denied", resolved };
|
|
39376
|
+
}
|
|
39377
|
+
return { ok: false, status: 500, error: err?.message || "realpath failed", resolved };
|
|
39378
|
+
}
|
|
39379
|
+
const blacklist = getBlacklist();
|
|
39380
|
+
for (const blocked of blacklist) {
|
|
39381
|
+
if (resolved === blocked) {
|
|
39382
|
+
return { ok: false, status: 403, error: "path is blacklisted", resolved };
|
|
39383
|
+
}
|
|
39384
|
+
}
|
|
39385
|
+
let stat2;
|
|
39386
|
+
try {
|
|
39387
|
+
stat2 = await fsp.stat(resolved);
|
|
39388
|
+
} catch (err) {
|
|
39389
|
+
if (err?.code === "ENOENT")
|
|
39390
|
+
return { ok: false, status: 404, error: "path not found", resolved };
|
|
39391
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
39392
|
+
return { ok: false, status: 403, error: "permission denied", resolved };
|
|
39393
|
+
}
|
|
39394
|
+
return { ok: false, status: 500, error: err?.message || "stat failed", resolved };
|
|
39395
|
+
}
|
|
39396
|
+
if (!stat2.isFile()) {
|
|
39397
|
+
return { ok: false, status: 400, error: "path is not a regular file", resolved };
|
|
39398
|
+
}
|
|
39399
|
+
return { ok: true, resolved, size: stat2.size };
|
|
39400
|
+
}
|
|
39401
|
+
function isBinarySample(buf) {
|
|
39402
|
+
if (buf.length === 0)
|
|
39403
|
+
return false;
|
|
39404
|
+
let nul = 0;
|
|
39405
|
+
let ctrl = 0;
|
|
39406
|
+
const sample = buf.subarray(0, Math.min(buf.length, 4096));
|
|
39407
|
+
for (let i = 0;i < sample.length; i++) {
|
|
39408
|
+
const b = sample[i];
|
|
39409
|
+
if (b === 0)
|
|
39410
|
+
nul++;
|
|
39411
|
+
else if (b < 9 || b > 13 && b < 32)
|
|
39412
|
+
ctrl++;
|
|
39413
|
+
}
|
|
39414
|
+
if (nul / sample.length > 0.01)
|
|
39415
|
+
return true;
|
|
39416
|
+
if (ctrl / sample.length > 0.3)
|
|
39417
|
+
return true;
|
|
39418
|
+
return false;
|
|
39419
|
+
}
|
|
39420
|
+
async function readTail(filePath, maxLines) {
|
|
39421
|
+
const fh = await fsp.open(filePath, "r");
|
|
39422
|
+
try {
|
|
39423
|
+
const stat2 = await fh.stat();
|
|
39424
|
+
const size = stat2.size;
|
|
39425
|
+
if (size === 0)
|
|
39426
|
+
return { lines: [], binary: false, size, startOffset: 0 };
|
|
39427
|
+
const chunkSize = 64 * 1024;
|
|
39428
|
+
let position = size;
|
|
39429
|
+
const chunks = [];
|
|
39430
|
+
let lineCount = 0;
|
|
39431
|
+
let binary = false;
|
|
39432
|
+
let sampled = false;
|
|
39433
|
+
while (position > 0 && lineCount <= maxLines) {
|
|
39434
|
+
const readSize = Math.min(chunkSize, position);
|
|
39435
|
+
position -= readSize;
|
|
39436
|
+
const buf = Buffer.alloc(readSize);
|
|
39437
|
+
await fh.read(buf, 0, readSize, position);
|
|
39438
|
+
if (!sampled) {
|
|
39439
|
+
binary = isBinarySample(buf);
|
|
39440
|
+
sampled = true;
|
|
39441
|
+
if (binary)
|
|
39442
|
+
return { lines: [], binary: true, size, startOffset: size };
|
|
39443
|
+
}
|
|
39444
|
+
chunks.unshift(buf);
|
|
39445
|
+
for (let i = 0;i < buf.length; i++) {
|
|
39446
|
+
if (buf[i] === 10)
|
|
39447
|
+
lineCount++;
|
|
39448
|
+
}
|
|
39449
|
+
}
|
|
39450
|
+
const all = Buffer.concat(chunks).toString("utf8");
|
|
39451
|
+
const allLines = all.split(`
|
|
39452
|
+
`);
|
|
39453
|
+
if (allLines.length > 0 && allLines[allLines.length - 1] === "")
|
|
39454
|
+
allLines.pop();
|
|
39455
|
+
const sliced = allLines.length > maxLines ? allLines.slice(allLines.length - maxLines) : allLines;
|
|
39456
|
+
return { lines: sliced, binary: false, size, startOffset: position };
|
|
39457
|
+
} finally {
|
|
39458
|
+
await fh.close();
|
|
39459
|
+
}
|
|
39460
|
+
}
|
|
39461
|
+
async function readRange(filePath, opts = {}) {
|
|
39462
|
+
const fh = await fsp.open(filePath, "r");
|
|
39463
|
+
try {
|
|
39464
|
+
const stat2 = await fh.stat();
|
|
39465
|
+
const fileSize = stat2.size;
|
|
39466
|
+
const winSize = Math.max(1024, Math.min(opts.size ?? DEFAULT_RANGE_SIZE, MAX_RANGE_SIZE));
|
|
39467
|
+
const direction = opts.direction ?? (opts.offset === undefined ? "tail" : "forward");
|
|
39468
|
+
if (fileSize === 0) {
|
|
39469
|
+
return { startOffset: 0, endOffset: 0, fileSize, lines: [], truncatedHead: false, truncatedTail: false, binary: false };
|
|
39470
|
+
}
|
|
39471
|
+
let start;
|
|
39472
|
+
if (direction === "tail") {
|
|
39473
|
+
start = Math.max(0, fileSize - winSize);
|
|
39474
|
+
} else if (direction === "backward") {
|
|
39475
|
+
const o = opts.offset ?? fileSize;
|
|
39476
|
+
start = Math.max(0, o - winSize);
|
|
39477
|
+
} else {
|
|
39478
|
+
start = Math.max(0, Math.min(opts.offset ?? 0, fileSize));
|
|
39479
|
+
}
|
|
39480
|
+
let end = Math.min(start + winSize, fileSize);
|
|
39481
|
+
const len = end - start;
|
|
39482
|
+
if (len <= 0) {
|
|
39483
|
+
return { startOffset: start, endOffset: end, fileSize, lines: [], truncatedHead: false, truncatedTail: false, binary: false };
|
|
39484
|
+
}
|
|
39485
|
+
const buf = Buffer.alloc(len);
|
|
39486
|
+
await fh.read(buf, 0, len, start);
|
|
39487
|
+
if (isBinarySample(buf)) {
|
|
39488
|
+
return { startOffset: start, endOffset: end, fileSize, lines: [], truncatedHead: false, truncatedTail: false, binary: true };
|
|
39489
|
+
}
|
|
39490
|
+
let truncatedHead = false;
|
|
39491
|
+
let truncatedTail = false;
|
|
39492
|
+
let viewStart = 0;
|
|
39493
|
+
let viewEnd = buf.length;
|
|
39494
|
+
if (start > 0) {
|
|
39495
|
+
const nl = buf.indexOf(10);
|
|
39496
|
+
if (nl >= 0) {
|
|
39497
|
+
viewStart = nl + 1;
|
|
39498
|
+
truncatedHead = true;
|
|
39499
|
+
} else {
|
|
39500
|
+
return {
|
|
39501
|
+
startOffset: start,
|
|
39502
|
+
endOffset: end,
|
|
39503
|
+
fileSize,
|
|
39504
|
+
lines: [],
|
|
39505
|
+
truncatedHead: true,
|
|
39506
|
+
truncatedTail: end < fileSize,
|
|
39507
|
+
binary: false
|
|
39508
|
+
};
|
|
39509
|
+
}
|
|
39510
|
+
}
|
|
39511
|
+
if (end < fileSize) {
|
|
39512
|
+
const nl = buf.lastIndexOf(10);
|
|
39513
|
+
if (nl >= viewStart) {
|
|
39514
|
+
viewEnd = nl + 1;
|
|
39515
|
+
truncatedTail = true;
|
|
39516
|
+
} else {
|
|
39517
|
+
return {
|
|
39518
|
+
startOffset: start,
|
|
39519
|
+
endOffset: end,
|
|
39520
|
+
fileSize,
|
|
39521
|
+
lines: [],
|
|
39522
|
+
truncatedHead,
|
|
39523
|
+
truncatedTail: true,
|
|
39524
|
+
binary: false
|
|
39525
|
+
};
|
|
39526
|
+
}
|
|
39527
|
+
}
|
|
39528
|
+
const text2 = buf.subarray(viewStart, viewEnd).toString("utf8");
|
|
39529
|
+
const lines = text2.split(`
|
|
39530
|
+
`);
|
|
39531
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
39532
|
+
lines.pop();
|
|
39533
|
+
return {
|
|
39534
|
+
startOffset: start + viewStart,
|
|
39535
|
+
endOffset: start + viewEnd,
|
|
39536
|
+
fileSize,
|
|
39537
|
+
lines,
|
|
39538
|
+
truncatedHead,
|
|
39539
|
+
truncatedTail,
|
|
39540
|
+
binary: false
|
|
39541
|
+
};
|
|
39542
|
+
} finally {
|
|
39543
|
+
await fh.close();
|
|
39544
|
+
}
|
|
39545
|
+
}
|
|
39546
|
+
function watchTail(filePath, initialOffset, cb) {
|
|
39547
|
+
let lastSize = initialOffset;
|
|
39548
|
+
let closed = false;
|
|
39549
|
+
let reading = false;
|
|
39550
|
+
let pending = false;
|
|
39551
|
+
const handleChange = async () => {
|
|
39552
|
+
if (closed)
|
|
39553
|
+
return;
|
|
39554
|
+
if (reading) {
|
|
39555
|
+
pending = true;
|
|
39556
|
+
return;
|
|
39557
|
+
}
|
|
39558
|
+
reading = true;
|
|
39559
|
+
try {
|
|
39560
|
+
let stat2;
|
|
39561
|
+
try {
|
|
39562
|
+
stat2 = await fsp.stat(filePath);
|
|
39563
|
+
} catch (err) {
|
|
39564
|
+
if (err?.code === "ENOENT") {
|
|
39565
|
+
if (lastSize !== 0) {
|
|
39566
|
+
lastSize = 0;
|
|
39567
|
+
cb.onRotate();
|
|
39568
|
+
}
|
|
39569
|
+
return;
|
|
39570
|
+
}
|
|
39571
|
+
throw err;
|
|
39572
|
+
}
|
|
39573
|
+
const newSize = stat2.size;
|
|
39574
|
+
if (newSize < lastSize) {
|
|
39575
|
+
lastSize = 0;
|
|
39576
|
+
cb.onRotate();
|
|
39577
|
+
return;
|
|
39578
|
+
}
|
|
39579
|
+
if (newSize === lastSize)
|
|
39580
|
+
return;
|
|
39581
|
+
const fh = await fsp.open(filePath, "r");
|
|
39582
|
+
try {
|
|
39583
|
+
const len = newSize - lastSize;
|
|
39584
|
+
const buf = Buffer.alloc(len);
|
|
39585
|
+
await fh.read(buf, 0, len, lastSize);
|
|
39586
|
+
lastSize = newSize;
|
|
39587
|
+
cb.onAppend(buf.toString("utf8"), newSize);
|
|
39588
|
+
} finally {
|
|
39589
|
+
await fh.close();
|
|
39590
|
+
}
|
|
39591
|
+
} catch (err) {
|
|
39592
|
+
cb.onError?.(err);
|
|
39593
|
+
} finally {
|
|
39594
|
+
reading = false;
|
|
39595
|
+
if (pending) {
|
|
39596
|
+
pending = false;
|
|
39597
|
+
setImmediate(handleChange);
|
|
39598
|
+
}
|
|
39599
|
+
}
|
|
39600
|
+
};
|
|
39601
|
+
let watcher = null;
|
|
39602
|
+
try {
|
|
39603
|
+
watcher = fs10.watch(filePath, { persistent: false }, () => {
|
|
39604
|
+
handleChange();
|
|
39605
|
+
});
|
|
39606
|
+
watcher.on("error", (err) => {
|
|
39607
|
+
cb.onError?.(err);
|
|
39608
|
+
});
|
|
39609
|
+
} catch (err) {
|
|
39610
|
+
log6.warn({ filePath, err: err.message }, "fs.watch failed; relying on polling");
|
|
39611
|
+
}
|
|
39612
|
+
const timer = setInterval(handleChange, STREAM_POLL_MS);
|
|
39613
|
+
if (typeof timer.unref === "function")
|
|
39614
|
+
timer.unref();
|
|
39615
|
+
return {
|
|
39616
|
+
close() {
|
|
39617
|
+
if (closed)
|
|
39618
|
+
return;
|
|
39619
|
+
closed = true;
|
|
39620
|
+
try {
|
|
39621
|
+
watcher?.close();
|
|
39622
|
+
} catch {}
|
|
39623
|
+
clearInterval(timer);
|
|
39624
|
+
}
|
|
39625
|
+
};
|
|
39626
|
+
}
|
|
39627
|
+
function grepStream(filePath, opts, cb) {
|
|
39628
|
+
const maxHits = Math.max(1, Math.min(opts.maxHits ?? DEFAULT_SEARCH_HITS, MAX_SEARCH_HITS));
|
|
39629
|
+
const context = Math.max(0, Math.min(opts.context ?? DEFAULT_SEARCH_CONTEXT, 20));
|
|
39630
|
+
let matcher;
|
|
39631
|
+
if (opts.regex) {
|
|
39632
|
+
let re;
|
|
39633
|
+
try {
|
|
39634
|
+
re = new RegExp(opts.q, opts.caseInsensitive ? "i" : "");
|
|
39635
|
+
} catch (err) {
|
|
39636
|
+
cb.onError(new Error(`invalid regex: ${err?.message || err}`));
|
|
39637
|
+
return { abort() {} };
|
|
39638
|
+
}
|
|
39639
|
+
matcher = (line) => re.test(line);
|
|
39640
|
+
} else if (opts.caseInsensitive) {
|
|
39641
|
+
const needle = opts.q.toLowerCase();
|
|
39642
|
+
matcher = (line) => line.toLowerCase().includes(needle);
|
|
39643
|
+
} else {
|
|
39644
|
+
const needle = opts.q;
|
|
39645
|
+
matcher = (line) => line.includes(needle);
|
|
39646
|
+
}
|
|
39647
|
+
const start = Math.max(0, opts.fromOffset ?? 0);
|
|
39648
|
+
const end = opts.toOffset !== undefined ? Math.max(start, opts.toOffset) : undefined;
|
|
39649
|
+
const readStream = fs10.createReadStream(filePath, end === undefined ? { start } : { start, end });
|
|
39650
|
+
const rl = readline.createInterface({ input: readStream, crlfDelay: Infinity });
|
|
39651
|
+
let bytesSeen = start;
|
|
39652
|
+
let hits = 0;
|
|
39653
|
+
let aborted = false;
|
|
39654
|
+
const beforeBuf = [];
|
|
39655
|
+
const pendingAfter = [];
|
|
39656
|
+
function flushPending(currentLine) {
|
|
39657
|
+
if (!currentLine) {
|
|
39658
|
+
for (const p of pendingAfter)
|
|
39659
|
+
cb.onHit(p.hit);
|
|
39660
|
+
pendingAfter.length = 0;
|
|
39661
|
+
return;
|
|
39662
|
+
}
|
|
39663
|
+
const stillPending = [];
|
|
39664
|
+
for (const p of pendingAfter) {
|
|
39665
|
+
p.hit.after.push(currentLine.text);
|
|
39666
|
+
p.need -= 1;
|
|
39667
|
+
if (p.need <= 0)
|
|
39668
|
+
cb.onHit(p.hit);
|
|
39669
|
+
else
|
|
39670
|
+
stillPending.push(p);
|
|
39671
|
+
}
|
|
39672
|
+
pendingAfter.length = 0;
|
|
39673
|
+
pendingAfter.push(...stillPending);
|
|
39674
|
+
}
|
|
39675
|
+
function pushBefore(entry) {
|
|
39676
|
+
beforeBuf.push(entry);
|
|
39677
|
+
if (beforeBuf.length > context)
|
|
39678
|
+
beforeBuf.shift();
|
|
39679
|
+
}
|
|
39680
|
+
rl.on("line", (line) => {
|
|
39681
|
+
if (aborted)
|
|
39682
|
+
return;
|
|
39683
|
+
if (opts.signal?.aborted) {
|
|
39684
|
+
abort();
|
|
39685
|
+
return;
|
|
39686
|
+
}
|
|
39687
|
+
const lineOffset = bytesSeen;
|
|
39688
|
+
const byteLen = Buffer.byteLength(line, "utf8") + 1;
|
|
39689
|
+
bytesSeen += byteLen;
|
|
39690
|
+
flushPending({ offset: lineOffset, text: line });
|
|
39691
|
+
if (matcher(line)) {
|
|
39692
|
+
const hit = {
|
|
39693
|
+
offset: lineOffset,
|
|
39694
|
+
text: line,
|
|
39695
|
+
before: beforeBuf.map((b) => b.text),
|
|
39696
|
+
after: []
|
|
39697
|
+
};
|
|
39698
|
+
if (context > 0) {
|
|
39699
|
+
pendingAfter.push({ hit, need: context });
|
|
39700
|
+
} else {
|
|
39701
|
+
cb.onHit(hit);
|
|
39702
|
+
}
|
|
39703
|
+
hits += 1;
|
|
39704
|
+
if (hits >= maxHits) {
|
|
39705
|
+
cb.onTruncated();
|
|
39706
|
+
abort();
|
|
39707
|
+
return;
|
|
39708
|
+
}
|
|
39709
|
+
}
|
|
39710
|
+
pushBefore({ offset: lineOffset, text: line });
|
|
39711
|
+
});
|
|
39712
|
+
rl.on("close", () => {
|
|
39713
|
+
if (aborted)
|
|
39714
|
+
return;
|
|
39715
|
+
flushPending(null);
|
|
39716
|
+
cb.onDone(bytesSeen - start);
|
|
39717
|
+
});
|
|
39718
|
+
readStream.on("error", (err) => {
|
|
39719
|
+
if (aborted)
|
|
39720
|
+
return;
|
|
39721
|
+
aborted = true;
|
|
39722
|
+
cb.onError(err);
|
|
39723
|
+
});
|
|
39724
|
+
function abort() {
|
|
39725
|
+
if (aborted)
|
|
39726
|
+
return;
|
|
39727
|
+
aborted = true;
|
|
39728
|
+
try {
|
|
39729
|
+
rl.close();
|
|
39730
|
+
} catch {}
|
|
39731
|
+
try {
|
|
39732
|
+
readStream.destroy();
|
|
39733
|
+
} catch {}
|
|
39734
|
+
flushPending(null);
|
|
39735
|
+
cb.onDone(bytesSeen - start);
|
|
39736
|
+
}
|
|
39737
|
+
if (opts.signal) {
|
|
39738
|
+
if (opts.signal.aborted)
|
|
39739
|
+
abort();
|
|
39740
|
+
else
|
|
39741
|
+
opts.signal.addEventListener("abort", abort, { once: true });
|
|
39742
|
+
}
|
|
39743
|
+
return { abort };
|
|
39744
|
+
}
|
|
39745
|
+
|
|
39746
|
+
// src/routes/log-sources.ts
|
|
39747
|
+
var log7 = moduleLogger("log-sources");
|
|
39748
|
+
var VALID_KINDS = new Set(["pm2", "nginx", "plain"]);
|
|
39749
|
+
function normalizeKind(input) {
|
|
39750
|
+
if (typeof input !== "string")
|
|
39751
|
+
return "plain";
|
|
39752
|
+
return VALID_KINDS.has(input) ? input : "plain";
|
|
39753
|
+
}
|
|
39754
|
+
function basename(p) {
|
|
39755
|
+
return path13.basename(p) || p;
|
|
39756
|
+
}
|
|
39757
|
+
function toInt(v, fallback) {
|
|
39758
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
39759
|
+
return Math.trunc(v);
|
|
39760
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
39761
|
+
const n = Number(v);
|
|
39762
|
+
if (Number.isFinite(n))
|
|
39763
|
+
return Math.trunc(n);
|
|
39764
|
+
}
|
|
39765
|
+
return fallback;
|
|
39766
|
+
}
|
|
39767
|
+
function sseLine(event, data) {
|
|
39768
|
+
return new TextEncoder().encode(`event: ${event}
|
|
39769
|
+
data: ${JSON.stringify(data)}
|
|
39770
|
+
|
|
39771
|
+
`);
|
|
39772
|
+
}
|
|
39773
|
+
function sseComment(text2) {
|
|
39774
|
+
return new TextEncoder().encode(`: ${text2}
|
|
39775
|
+
|
|
39776
|
+
`);
|
|
39777
|
+
}
|
|
39778
|
+
var logSourceRoutes = new Elysia().get("/api/projects/:id/log-sources", async ({ headers, params, set: set3 }) => {
|
|
39779
|
+
if (!verifyAdminToken(headers)) {
|
|
39780
|
+
set3.status = 401;
|
|
39781
|
+
return { error: "Unauthorized" };
|
|
39782
|
+
}
|
|
39783
|
+
const project = await db.projects.findById(params.id);
|
|
39784
|
+
if (!project) {
|
|
39785
|
+
set3.status = 404;
|
|
39786
|
+
return { error: "Project not found" };
|
|
39787
|
+
}
|
|
39788
|
+
const sources = await db.logSources.findByProject(params.id);
|
|
39789
|
+
return { items: sources };
|
|
39790
|
+
}).post("/api/projects/:id/log-sources", async ({ headers, params, body, set: set3 }) => {
|
|
39791
|
+
if (!verifyAdminToken(headers)) {
|
|
39792
|
+
set3.status = 401;
|
|
39793
|
+
return { error: "Unauthorized" };
|
|
39794
|
+
}
|
|
39795
|
+
const project = await db.projects.findById(params.id);
|
|
39796
|
+
if (!project) {
|
|
39797
|
+
set3.status = 404;
|
|
39798
|
+
return { error: "Project not found" };
|
|
39799
|
+
}
|
|
39800
|
+
const rawItems = Array.isArray(body?.items) ? body.items : null;
|
|
39801
|
+
if (!rawItems || rawItems.length === 0) {
|
|
39802
|
+
set3.status = 400;
|
|
39803
|
+
return { error: "items is required (array of {label, filePath, kind?})" };
|
|
39804
|
+
}
|
|
39805
|
+
if (rawItems.length > 50) {
|
|
39806
|
+
set3.status = 400;
|
|
39807
|
+
return { error: "too many items in one request (max 50)" };
|
|
39808
|
+
}
|
|
39809
|
+
const created = [];
|
|
39810
|
+
const errors2 = [];
|
|
39811
|
+
for (let i = 0;i < rawItems.length; i++) {
|
|
39812
|
+
const item = rawItems[i] || {};
|
|
39813
|
+
const filePath = typeof item.filePath === "string" ? item.filePath : "";
|
|
39814
|
+
if (!filePath) {
|
|
39815
|
+
errors2.push({ index: i, error: "filePath is required" });
|
|
39816
|
+
continue;
|
|
39817
|
+
}
|
|
39818
|
+
const guard4 = await pathGuard(filePath, { allowMissing: true });
|
|
39819
|
+
if (!guard4.ok) {
|
|
39820
|
+
errors2.push({ index: i, error: guard4.error || "invalid path" });
|
|
39821
|
+
continue;
|
|
39822
|
+
}
|
|
39823
|
+
const label = typeof item.label === "string" && item.label.trim() ? item.label.trim() : basename(filePath);
|
|
39824
|
+
const kind = normalizeKind(item.kind);
|
|
39825
|
+
const row = await db.logSources.create({
|
|
39826
|
+
projectId: params.id,
|
|
39827
|
+
label,
|
|
39828
|
+
filePath: guard4.resolved || filePath,
|
|
39829
|
+
kind
|
|
39830
|
+
});
|
|
39831
|
+
created.push(row);
|
|
39832
|
+
}
|
|
39833
|
+
if (created.length === 0) {
|
|
39834
|
+
set3.status = 400;
|
|
39835
|
+
return { error: "no item created", details: errors2 };
|
|
39836
|
+
}
|
|
39837
|
+
return { created, errors: errors2 };
|
|
39838
|
+
}).patch("/api/log-sources/:sourceId", async ({ headers, params, body, set: set3 }) => {
|
|
39839
|
+
if (!verifyAdminToken(headers)) {
|
|
39840
|
+
set3.status = 401;
|
|
39841
|
+
return { error: "Unauthorized" };
|
|
39842
|
+
}
|
|
39843
|
+
const existing = await db.logSources.findById(params.sourceId);
|
|
39844
|
+
if (!existing) {
|
|
39845
|
+
set3.status = 404;
|
|
39846
|
+
return { error: "Log source not found" };
|
|
39847
|
+
}
|
|
39848
|
+
const patch = {};
|
|
39849
|
+
if (typeof body?.label === "string") {
|
|
39850
|
+
const trimmed = body.label.trim();
|
|
39851
|
+
if (!trimmed) {
|
|
39852
|
+
set3.status = 400;
|
|
39853
|
+
return { error: "label cannot be empty" };
|
|
39854
|
+
}
|
|
39855
|
+
patch.label = trimmed;
|
|
39856
|
+
}
|
|
39857
|
+
if (body?.kind !== undefined) {
|
|
39858
|
+
patch.kind = normalizeKind(body.kind);
|
|
39859
|
+
}
|
|
39860
|
+
if (body?.sortOrder !== undefined) {
|
|
39861
|
+
patch.sortOrder = toInt(body.sortOrder, existing.sortOrder ?? 0);
|
|
39862
|
+
}
|
|
39863
|
+
const updated = await db.logSources.update(params.sourceId, patch);
|
|
39864
|
+
return updated;
|
|
39865
|
+
}).delete("/api/log-sources/:sourceId", async ({ headers, params, set: set3 }) => {
|
|
39866
|
+
if (!verifyAdminToken(headers)) {
|
|
39867
|
+
set3.status = 401;
|
|
39868
|
+
return { error: "Unauthorized" };
|
|
39869
|
+
}
|
|
39870
|
+
const ok = await db.logSources.remove(params.sourceId);
|
|
39871
|
+
if (!ok) {
|
|
39872
|
+
set3.status = 404;
|
|
39873
|
+
return { error: "Log source not found" };
|
|
39874
|
+
}
|
|
39875
|
+
return { ok: true };
|
|
39876
|
+
}).get("/api/log-sources/:sourceId/meta", async ({ headers, params, set: set3 }) => {
|
|
39877
|
+
if (!verifyAdminToken(headers)) {
|
|
39878
|
+
set3.status = 401;
|
|
39879
|
+
return { error: "Unauthorized" };
|
|
39880
|
+
}
|
|
39881
|
+
const src = await db.logSources.findById(params.sourceId);
|
|
39882
|
+
if (!src) {
|
|
39883
|
+
set3.status = 404;
|
|
39884
|
+
return { error: "Log source not found" };
|
|
39885
|
+
}
|
|
39886
|
+
const guard4 = await pathGuard(src.filePath);
|
|
39887
|
+
if (!guard4.ok) {
|
|
39888
|
+
set3.status = guard4.status || 500;
|
|
39889
|
+
return { error: guard4.error || "path error" };
|
|
39890
|
+
}
|
|
39891
|
+
return {
|
|
39892
|
+
id: src.id,
|
|
39893
|
+
label: src.label,
|
|
39894
|
+
filePath: src.filePath,
|
|
39895
|
+
resolvedPath: guard4.resolved,
|
|
39896
|
+
kind: src.kind ?? "plain",
|
|
39897
|
+
size: guard4.size ?? 0
|
|
39898
|
+
};
|
|
39899
|
+
}).get("/api/log-sources/:sourceId/range", async ({ headers, params, query, set: set3 }) => {
|
|
39900
|
+
if (!verifyAdminToken(headers)) {
|
|
39901
|
+
set3.status = 401;
|
|
39902
|
+
return { error: "Unauthorized" };
|
|
39903
|
+
}
|
|
39904
|
+
const src = await db.logSources.findById(params.sourceId);
|
|
39905
|
+
if (!src) {
|
|
39906
|
+
set3.status = 404;
|
|
39907
|
+
return { error: "Log source not found" };
|
|
39908
|
+
}
|
|
39909
|
+
const guard4 = await pathGuard(src.filePath);
|
|
39910
|
+
if (!guard4.ok) {
|
|
39911
|
+
set3.status = guard4.status || 500;
|
|
39912
|
+
return { error: guard4.error || "path error" };
|
|
39913
|
+
}
|
|
39914
|
+
const rawOffset = query.offset;
|
|
39915
|
+
const rawDirection = query.direction;
|
|
39916
|
+
const size = Math.max(1024, Math.min(toInt(query.size, DEFAULT_RANGE_SIZE), MAX_RANGE_SIZE));
|
|
39917
|
+
const direction = rawDirection === "backward" || rawDirection === "forward" || rawDirection === "tail" ? rawDirection : rawOffset === undefined ? "tail" : "forward";
|
|
39918
|
+
const offset = rawOffset === undefined ? undefined : Math.max(0, toInt(rawOffset, 0));
|
|
39919
|
+
try {
|
|
39920
|
+
const result = await readRange(guard4.resolved, { offset, size, direction });
|
|
39921
|
+
return result;
|
|
39922
|
+
} catch (err) {
|
|
39923
|
+
log7.warn({ err: err?.message, sourceId: src.id }, "readRange failed");
|
|
39924
|
+
set3.status = 500;
|
|
39925
|
+
return { error: err?.message || "read failed" };
|
|
39926
|
+
}
|
|
39927
|
+
}).get("/api/log-sources/:sourceId/stream", async ({ headers, params, query, set: set3, request }) => {
|
|
39928
|
+
if (!verifyAdminToken(headers)) {
|
|
39929
|
+
set3.status = 401;
|
|
39930
|
+
return new Response("Unauthorized", { status: 401 });
|
|
39931
|
+
}
|
|
39932
|
+
const src = await db.logSources.findById(params.sourceId);
|
|
39933
|
+
if (!src) {
|
|
39934
|
+
set3.status = 404;
|
|
39935
|
+
return new Response("Not found", { status: 404 });
|
|
39936
|
+
}
|
|
39937
|
+
const guard4 = await pathGuard(src.filePath);
|
|
39938
|
+
if (!guard4.ok) {
|
|
39939
|
+
set3.status = guard4.status || 500;
|
|
39940
|
+
return new Response(guard4.error || "path error", { status: guard4.status || 500 });
|
|
39941
|
+
}
|
|
39942
|
+
const tailLines = Math.max(1, Math.min(toInt(query.tailLines, DEFAULT_TAIL_LINES), MAX_TAIL_LINES));
|
|
39943
|
+
const filePath = guard4.resolved;
|
|
39944
|
+
let watcher = null;
|
|
39945
|
+
let heartbeat = null;
|
|
39946
|
+
let flushTimer = null;
|
|
39947
|
+
let pendingLines = [];
|
|
39948
|
+
let closed = false;
|
|
39949
|
+
let controllerRef = null;
|
|
39950
|
+
const safeEnqueue = (chunk) => {
|
|
39951
|
+
if (closed || !controllerRef)
|
|
39952
|
+
return;
|
|
39953
|
+
try {
|
|
39954
|
+
controllerRef.enqueue(chunk);
|
|
39955
|
+
} catch {}
|
|
39956
|
+
};
|
|
39957
|
+
const flush = () => {
|
|
39958
|
+
if (flushTimer) {
|
|
39959
|
+
clearTimeout(flushTimer);
|
|
39960
|
+
flushTimer = null;
|
|
39961
|
+
}
|
|
39962
|
+
if (pendingLines.length === 0)
|
|
39963
|
+
return;
|
|
39964
|
+
const batch = pendingLines;
|
|
39965
|
+
pendingLines = [];
|
|
39966
|
+
safeEnqueue(sseLine("lines", { lines: batch }));
|
|
39967
|
+
};
|
|
39968
|
+
const scheduleFlush = () => {
|
|
39969
|
+
if (flushTimer)
|
|
39970
|
+
return;
|
|
39971
|
+
flushTimer = setTimeout(flush, STREAM_FLUSH_MS);
|
|
39972
|
+
};
|
|
39973
|
+
const cleanup = () => {
|
|
39974
|
+
if (closed)
|
|
39975
|
+
return;
|
|
39976
|
+
closed = true;
|
|
39977
|
+
if (heartbeat)
|
|
39978
|
+
clearInterval(heartbeat);
|
|
39979
|
+
if (flushTimer)
|
|
39980
|
+
clearTimeout(flushTimer);
|
|
39981
|
+
if (watcher) {
|
|
39982
|
+
try {
|
|
39983
|
+
watcher.close();
|
|
39984
|
+
} catch {}
|
|
39985
|
+
}
|
|
39986
|
+
try {
|
|
39987
|
+
controllerRef?.close();
|
|
39988
|
+
} catch {}
|
|
39989
|
+
};
|
|
39990
|
+
const stream = new ReadableStream({
|
|
39991
|
+
start(controller) {
|
|
39992
|
+
controllerRef = controller;
|
|
39993
|
+
(async () => {
|
|
39994
|
+
try {
|
|
39995
|
+
const initial = await readTail(filePath, tailLines);
|
|
39996
|
+
safeEnqueue(sseLine("snapshot", {
|
|
39997
|
+
lines: initial.lines,
|
|
39998
|
+
size: initial.size,
|
|
39999
|
+
binary: initial.binary
|
|
40000
|
+
}));
|
|
40001
|
+
if (initial.binary) {
|
|
40002
|
+
cleanup();
|
|
40003
|
+
return;
|
|
40004
|
+
}
|
|
40005
|
+
watcher = watchTail(filePath, initial.size, {
|
|
40006
|
+
onAppend: (chunk) => {
|
|
40007
|
+
if (closed)
|
|
40008
|
+
return;
|
|
40009
|
+
const parts = chunk.split(`
|
|
40010
|
+
`);
|
|
40011
|
+
if (parts.length > 0 && parts[parts.length - 1] === "")
|
|
40012
|
+
parts.pop();
|
|
40013
|
+
if (parts.length === 0)
|
|
40014
|
+
return;
|
|
40015
|
+
if (pendingLines.length + parts.length > 2000) {
|
|
40016
|
+
pendingLines = pendingLines.concat(parts).slice(-2000);
|
|
40017
|
+
} else {
|
|
40018
|
+
pendingLines.push(...parts);
|
|
40019
|
+
}
|
|
40020
|
+
scheduleFlush();
|
|
40021
|
+
},
|
|
40022
|
+
onRotate: () => {
|
|
40023
|
+
if (closed)
|
|
40024
|
+
return;
|
|
40025
|
+
flush();
|
|
40026
|
+
safeEnqueue(sseLine("rotated", { at: new Date().toISOString() }));
|
|
40027
|
+
readTail(filePath, tailLines).then((again) => {
|
|
40028
|
+
if (closed)
|
|
40029
|
+
return;
|
|
40030
|
+
safeEnqueue(sseLine("snapshot", {
|
|
40031
|
+
lines: again.lines,
|
|
40032
|
+
size: again.size,
|
|
40033
|
+
binary: again.binary
|
|
40034
|
+
}));
|
|
40035
|
+
}).catch((err) => {
|
|
40036
|
+
safeEnqueue(sseLine("error", { message: err?.message || "rotate read failed" }));
|
|
40037
|
+
});
|
|
40038
|
+
},
|
|
40039
|
+
onError: (err) => {
|
|
40040
|
+
if (closed)
|
|
40041
|
+
return;
|
|
40042
|
+
safeEnqueue(sseLine("error", { message: err?.message || "watch error" }));
|
|
40043
|
+
}
|
|
40044
|
+
});
|
|
40045
|
+
heartbeat = setInterval(() => safeEnqueue(sseComment("keep-alive")), STREAM_HEARTBEAT_MS);
|
|
40046
|
+
} catch (err) {
|
|
40047
|
+
safeEnqueue(sseLine("error", { message: err?.message || "init failed" }));
|
|
40048
|
+
cleanup();
|
|
40049
|
+
}
|
|
40050
|
+
})();
|
|
40051
|
+
},
|
|
40052
|
+
cancel() {
|
|
40053
|
+
cleanup();
|
|
40054
|
+
}
|
|
40055
|
+
});
|
|
40056
|
+
request.signal?.addEventListener("abort", cleanup, { once: true });
|
|
40057
|
+
return new Response(stream, {
|
|
40058
|
+
headers: {
|
|
40059
|
+
"Content-Type": "text/event-stream",
|
|
40060
|
+
"Cache-Control": "no-cache",
|
|
40061
|
+
Connection: "keep-alive",
|
|
40062
|
+
"X-Accel-Buffering": "no"
|
|
40063
|
+
}
|
|
40064
|
+
});
|
|
40065
|
+
}).get("/api/log-sources/:sourceId/search", async ({ headers, params, query, set: set3, request }) => {
|
|
40066
|
+
if (!verifyAdminToken(headers)) {
|
|
40067
|
+
set3.status = 401;
|
|
40068
|
+
return new Response("Unauthorized", { status: 401 });
|
|
40069
|
+
}
|
|
40070
|
+
const src = await db.logSources.findById(params.sourceId);
|
|
40071
|
+
if (!src) {
|
|
40072
|
+
set3.status = 404;
|
|
40073
|
+
return new Response("Not found", { status: 404 });
|
|
40074
|
+
}
|
|
40075
|
+
const guard4 = await pathGuard(src.filePath);
|
|
40076
|
+
if (!guard4.ok) {
|
|
40077
|
+
set3.status = guard4.status || 500;
|
|
40078
|
+
return new Response(guard4.error || "path error", { status: guard4.status || 500 });
|
|
40079
|
+
}
|
|
40080
|
+
const q = typeof query.q === "string" ? query.q : "";
|
|
40081
|
+
if (!q) {
|
|
40082
|
+
set3.status = 400;
|
|
40083
|
+
return new Response("q is required", { status: 400 });
|
|
40084
|
+
}
|
|
40085
|
+
const regex2 = query.regex === "true" || query.regex === "1";
|
|
40086
|
+
const caseInsensitive = query.caseInsensitive === "true" || query.caseInsensitive === "1";
|
|
40087
|
+
const maxHits = Math.max(1, Math.min(toInt(query.maxHits, DEFAULT_SEARCH_HITS), MAX_SEARCH_HITS));
|
|
40088
|
+
const context = Math.max(0, Math.min(toInt(query.context, DEFAULT_SEARCH_CONTEXT), 20));
|
|
40089
|
+
const fromOffset = query.fromOffset !== undefined ? Math.max(0, toInt(query.fromOffset, 0)) : undefined;
|
|
40090
|
+
const toOffset = query.toOffset !== undefined ? Math.max(0, toInt(query.toOffset, 0)) : undefined;
|
|
40091
|
+
let controllerRef = null;
|
|
40092
|
+
let heartbeat = null;
|
|
40093
|
+
let stopper = null;
|
|
40094
|
+
let closed = false;
|
|
40095
|
+
const safeEnqueue = (chunk) => {
|
|
40096
|
+
if (closed || !controllerRef)
|
|
40097
|
+
return;
|
|
40098
|
+
try {
|
|
40099
|
+
controllerRef.enqueue(chunk);
|
|
40100
|
+
} catch {}
|
|
40101
|
+
};
|
|
40102
|
+
const cleanup = () => {
|
|
40103
|
+
if (closed)
|
|
40104
|
+
return;
|
|
40105
|
+
closed = true;
|
|
40106
|
+
if (heartbeat)
|
|
40107
|
+
clearInterval(heartbeat);
|
|
40108
|
+
if (stopper) {
|
|
40109
|
+
try {
|
|
40110
|
+
stopper.abort();
|
|
40111
|
+
} catch {}
|
|
40112
|
+
}
|
|
40113
|
+
try {
|
|
40114
|
+
controllerRef?.close();
|
|
40115
|
+
} catch {}
|
|
40116
|
+
};
|
|
40117
|
+
const stream = new ReadableStream({
|
|
40118
|
+
start(controller) {
|
|
40119
|
+
controllerRef = controller;
|
|
40120
|
+
heartbeat = setInterval(() => safeEnqueue(sseComment("keep-alive")), STREAM_HEARTBEAT_MS);
|
|
40121
|
+
stopper = grepStream(guard4.resolved, {
|
|
40122
|
+
q,
|
|
40123
|
+
regex: regex2,
|
|
40124
|
+
caseInsensitive,
|
|
40125
|
+
maxHits,
|
|
40126
|
+
context,
|
|
40127
|
+
fromOffset,
|
|
40128
|
+
toOffset,
|
|
40129
|
+
signal: request.signal
|
|
40130
|
+
}, {
|
|
40131
|
+
onHit: (hit) => safeEnqueue(sseLine("hit", hit)),
|
|
40132
|
+
onTruncated: () => safeEnqueue(sseLine("truncated", { maxHits })),
|
|
40133
|
+
onDone: (scannedBytes) => {
|
|
40134
|
+
safeEnqueue(sseLine("done", { scannedBytes }));
|
|
40135
|
+
cleanup();
|
|
40136
|
+
},
|
|
40137
|
+
onError: (err) => {
|
|
40138
|
+
safeEnqueue(sseLine("error", { message: err?.message || "search error" }));
|
|
40139
|
+
cleanup();
|
|
40140
|
+
}
|
|
40141
|
+
});
|
|
40142
|
+
},
|
|
40143
|
+
cancel() {
|
|
40144
|
+
cleanup();
|
|
40145
|
+
}
|
|
40146
|
+
});
|
|
40147
|
+
request.signal?.addEventListener("abort", cleanup, { once: true });
|
|
40148
|
+
return new Response(stream, {
|
|
40149
|
+
headers: {
|
|
40150
|
+
"Content-Type": "text/event-stream",
|
|
40151
|
+
"Cache-Control": "no-cache",
|
|
40152
|
+
Connection: "keep-alive",
|
|
40153
|
+
"X-Accel-Buffering": "no"
|
|
40154
|
+
}
|
|
40155
|
+
});
|
|
40156
|
+
});
|
|
40157
|
+
|
|
40158
|
+
// src/routes/system.ts
|
|
40159
|
+
init_dist3();
|
|
40160
|
+
|
|
40161
|
+
// src/lib/system-metrics.ts
|
|
40162
|
+
import os6 from "node:os";
|
|
40163
|
+
import fs11 from "node:fs/promises";
|
|
40164
|
+
function readCpuSnapshot() {
|
|
40165
|
+
const cpus = os6.cpus();
|
|
40166
|
+
let idle = 0;
|
|
40167
|
+
let total = 0;
|
|
40168
|
+
for (const c of cpus) {
|
|
40169
|
+
const t2 = c.times;
|
|
40170
|
+
idle += t2.idle;
|
|
40171
|
+
total += t2.user + t2.nice + t2.sys + t2.idle + t2.irq;
|
|
40172
|
+
}
|
|
40173
|
+
return { idle, total };
|
|
40174
|
+
}
|
|
40175
|
+
var lastSnapshot = null;
|
|
40176
|
+
var lastCpuPercent = null;
|
|
40177
|
+
function sampleCpuPercent() {
|
|
40178
|
+
const cur = readCpuSnapshot();
|
|
40179
|
+
if (!lastSnapshot) {
|
|
40180
|
+
lastSnapshot = cur;
|
|
40181
|
+
return lastCpuPercent;
|
|
40182
|
+
}
|
|
40183
|
+
const idleDiff = cur.idle - lastSnapshot.idle;
|
|
40184
|
+
const totalDiff = cur.total - lastSnapshot.total;
|
|
40185
|
+
lastSnapshot = cur;
|
|
40186
|
+
if (totalDiff <= 0)
|
|
40187
|
+
return lastCpuPercent;
|
|
40188
|
+
const pct = Math.max(0, Math.min(100, Math.round((1 - idleDiff / totalDiff) * 1e4) / 100));
|
|
40189
|
+
lastCpuPercent = pct;
|
|
40190
|
+
return pct;
|
|
40191
|
+
}
|
|
40192
|
+
readCpuSnapshot();
|
|
40193
|
+
lastSnapshot = readCpuSnapshot();
|
|
40194
|
+
async function readAvailableMemoryBytes(totalBytes) {
|
|
40195
|
+
try {
|
|
40196
|
+
if (process.platform === "darwin") {
|
|
40197
|
+
const { stdout, code } = await runCmd("vm_stat", []);
|
|
40198
|
+
if (code !== 0)
|
|
40199
|
+
return os6.freemem();
|
|
40200
|
+
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
|
|
40201
|
+
const pageSize = pageSizeMatch ? Number(pageSizeMatch[1]) : 4096;
|
|
40202
|
+
const pick3 = (key) => {
|
|
40203
|
+
const m = stdout.match(new RegExp(`Pages ${key}:\\s+(\\d+)`));
|
|
40204
|
+
return m ? Number(m[1]) : 0;
|
|
40205
|
+
};
|
|
40206
|
+
const free = pick3("free");
|
|
40207
|
+
const inactive = pick3("inactive");
|
|
40208
|
+
const speculative = pick3("speculative");
|
|
40209
|
+
const purgeable = pick3("purgeable");
|
|
40210
|
+
const available = (free + inactive + speculative + purgeable) * pageSize;
|
|
40211
|
+
return available > 0 ? Math.min(available, totalBytes) : os6.freemem();
|
|
40212
|
+
}
|
|
40213
|
+
if (process.platform === "linux") {
|
|
40214
|
+
const content = await fs11.readFile("/proc/meminfo", "utf8");
|
|
40215
|
+
const m = content.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
40216
|
+
if (m) {
|
|
40217
|
+
const bytes = Number(m[1]) * 1024;
|
|
40218
|
+
return Number.isFinite(bytes) ? bytes : os6.freemem();
|
|
40219
|
+
}
|
|
40220
|
+
return os6.freemem();
|
|
40221
|
+
}
|
|
40222
|
+
} catch {
|
|
40223
|
+
return os6.freemem();
|
|
40224
|
+
}
|
|
40225
|
+
return os6.freemem();
|
|
40226
|
+
}
|
|
40227
|
+
var lastProcCpuUsage = process.cpuUsage();
|
|
40228
|
+
var lastProcSampleAt = Date.now();
|
|
40229
|
+
var lastProcCpuPercent = null;
|
|
40230
|
+
function sampleProcessCpuPercent() {
|
|
40231
|
+
const usage = process.cpuUsage(lastProcCpuUsage);
|
|
40232
|
+
const now = Date.now();
|
|
40233
|
+
const elapsedMs = now - lastProcSampleAt;
|
|
40234
|
+
lastProcCpuUsage = process.cpuUsage();
|
|
40235
|
+
lastProcSampleAt = now;
|
|
40236
|
+
if (elapsedMs <= 0)
|
|
40237
|
+
return lastProcCpuPercent;
|
|
40238
|
+
const cpuMs = (usage.user + usage.system) / 1000;
|
|
40239
|
+
const cores = Math.max(1, os6.cpus().length);
|
|
40240
|
+
const pct = Math.max(0, Math.min(100, Math.round(cpuMs / (elapsedMs * cores) * 1e4) / 100));
|
|
40241
|
+
lastProcCpuPercent = pct;
|
|
40242
|
+
return pct;
|
|
40243
|
+
}
|
|
40244
|
+
async function collectSystemResources() {
|
|
40245
|
+
const cpus = os6.cpus();
|
|
40246
|
+
const cpuPercent = sampleCpuPercent();
|
|
40247
|
+
const totalMem = os6.totalmem();
|
|
40248
|
+
const freeMem = os6.freemem();
|
|
40249
|
+
const [availMem, disk] = await Promise.all([
|
|
40250
|
+
readAvailableMemoryBytes(totalMem),
|
|
40251
|
+
fsFree()
|
|
40252
|
+
]);
|
|
40253
|
+
const usedMem = Math.max(0, totalMem - availMem);
|
|
40254
|
+
const mem = process.memoryUsage();
|
|
40255
|
+
const runtimeName = typeof globalThis.Bun !== "undefined" ? "bun" : "node";
|
|
40256
|
+
const runtimeVersion = runtimeName === "bun" ? `v${globalThis.Bun.version}` : process.version;
|
|
40257
|
+
return {
|
|
40258
|
+
collectedAt: new Date().toISOString(),
|
|
40259
|
+
host: {
|
|
40260
|
+
hostname: os6.hostname(),
|
|
40261
|
+
platform: process.platform,
|
|
40262
|
+
arch: process.arch,
|
|
40263
|
+
cpuModel: cpus[0]?.model ?? null,
|
|
40264
|
+
cpuCount: cpus.length,
|
|
40265
|
+
loadAvg: os6.loadavg(),
|
|
40266
|
+
uptimeSec: Math.floor(os6.uptime())
|
|
40267
|
+
},
|
|
40268
|
+
cpu: { percent: cpuPercent },
|
|
40269
|
+
memory: {
|
|
40270
|
+
totalBytes: totalMem,
|
|
40271
|
+
freeBytes: freeMem,
|
|
40272
|
+
availableBytes: availMem,
|
|
40273
|
+
usedBytes: usedMem,
|
|
40274
|
+
percentUsed: totalMem > 0 ? Math.round(usedMem / totalMem * 1e4) / 100 : 0
|
|
40275
|
+
},
|
|
40276
|
+
disk,
|
|
40277
|
+
process: {
|
|
40278
|
+
pid: process.pid,
|
|
40279
|
+
runtime: runtimeName,
|
|
40280
|
+
runtimeVersion,
|
|
40281
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
40282
|
+
cpuPercent: sampleProcessCpuPercent(),
|
|
40283
|
+
memoryRssBytes: mem.rss,
|
|
40284
|
+
memoryHeapUsedBytes: mem.heapUsed
|
|
40285
|
+
}
|
|
40286
|
+
};
|
|
40287
|
+
}
|
|
40288
|
+
|
|
40289
|
+
// src/routes/system.ts
|
|
40290
|
+
var systemRoutes = new Elysia().get("/api/system/resources", async ({ headers, set: set3 }) => {
|
|
40291
|
+
if (!verifyAdminToken(headers)) {
|
|
40292
|
+
set3.status = 401;
|
|
40293
|
+
return { error: "Unauthorized" };
|
|
40294
|
+
}
|
|
40295
|
+
return await collectSystemResources();
|
|
40296
|
+
});
|
|
40297
|
+
|
|
40298
|
+
// src/routes/pm2.ts
|
|
40299
|
+
init_dist3();
|
|
40300
|
+
|
|
40301
|
+
// src/lib/pm2.ts
|
|
40302
|
+
import os7 from "node:os";
|
|
40303
|
+
import path14 from "node:path";
|
|
40304
|
+
var cachedPm2Path = undefined;
|
|
40305
|
+
async function resolvePm2Path() {
|
|
40306
|
+
if (cachedPm2Path !== undefined)
|
|
40307
|
+
return cachedPm2Path;
|
|
40308
|
+
const candidates = ["pm2"];
|
|
40309
|
+
const home = os7.homedir();
|
|
40310
|
+
candidates.push(path14.join(home, ".local/bin/pm2"), "/usr/local/bin/pm2", "/opt/homebrew/bin/pm2");
|
|
40311
|
+
for (const c of candidates) {
|
|
40312
|
+
const probe = await runCmd(c, ["--version"], 1500);
|
|
40313
|
+
if (probe.code === 0) {
|
|
40314
|
+
cachedPm2Path = c;
|
|
40315
|
+
return c;
|
|
40316
|
+
}
|
|
40317
|
+
}
|
|
40318
|
+
cachedPm2Path = null;
|
|
40319
|
+
return null;
|
|
40320
|
+
}
|
|
40321
|
+
async function isPm2Available() {
|
|
40322
|
+
return await resolvePm2Path() !== null;
|
|
40323
|
+
}
|
|
40324
|
+
var cache3 = null;
|
|
40325
|
+
var CACHE_TTL_MS3 = 1500;
|
|
40326
|
+
async function pm2Jlist() {
|
|
40327
|
+
if (cache3 && Date.now() - cache3.at < CACHE_TTL_MS3)
|
|
40328
|
+
return cache3.data;
|
|
40329
|
+
const bin = await resolvePm2Path();
|
|
40330
|
+
if (!bin)
|
|
40331
|
+
return [];
|
|
40332
|
+
const { stdout, code } = await runCmd(bin, ["jlist"], 4000);
|
|
40333
|
+
if (code !== 0)
|
|
40334
|
+
return [];
|
|
40335
|
+
try {
|
|
40336
|
+
const start = stdout.indexOf("[");
|
|
40337
|
+
if (start < 0)
|
|
40338
|
+
return [];
|
|
40339
|
+
const arr = JSON.parse(stdout.slice(start));
|
|
40340
|
+
if (!Array.isArray(arr))
|
|
40341
|
+
return [];
|
|
40342
|
+
cache3 = { at: Date.now(), data: arr };
|
|
40343
|
+
return arr;
|
|
40344
|
+
} catch {
|
|
40345
|
+
return [];
|
|
40346
|
+
}
|
|
40347
|
+
}
|
|
40348
|
+
async function listPm2Apps() {
|
|
40349
|
+
const list = await pm2Jlist();
|
|
40350
|
+
return list.map((item) => ({
|
|
40351
|
+
name: String(item?.name ?? ""),
|
|
40352
|
+
pmId: Number(item?.pm_id ?? -1),
|
|
40353
|
+
status: String(item?.pm2_env?.status ?? "unknown")
|
|
40354
|
+
})).filter((x) => x.name);
|
|
40355
|
+
}
|
|
40356
|
+
async function getPm2AppStatus(name) {
|
|
40357
|
+
const available = await isPm2Available();
|
|
40358
|
+
if (!available) {
|
|
40359
|
+
return { found: false, name, message: "pm2 binary not found in PATH" };
|
|
40360
|
+
}
|
|
40361
|
+
const list = await pm2Jlist();
|
|
40362
|
+
const matched = list.filter((item) => String(item?.name) === name);
|
|
40363
|
+
if (matched.length === 0) {
|
|
40364
|
+
return { found: false, name, message: `no pm2 app named "${name}"` };
|
|
40365
|
+
}
|
|
40366
|
+
const first = matched[0];
|
|
40367
|
+
let cpuSum = 0;
|
|
40368
|
+
let memSum = 0;
|
|
40369
|
+
let restarts = 0;
|
|
40370
|
+
let unstable = 0;
|
|
40371
|
+
let uptimeMax = 0;
|
|
40372
|
+
let status2 = "unknown";
|
|
40373
|
+
for (const m of matched) {
|
|
40374
|
+
const monit = m?.monit || {};
|
|
40375
|
+
cpuSum += Number(monit.cpu ?? 0);
|
|
40376
|
+
memSum += Number(monit.memory ?? 0);
|
|
40377
|
+
const env4 = m?.pm2_env || {};
|
|
40378
|
+
restarts += Number(env4.restart_time ?? 0);
|
|
40379
|
+
unstable += Number(env4.unstable_restarts ?? 0);
|
|
40380
|
+
if (typeof env4.pm_uptime === "number") {
|
|
40381
|
+
const up = Date.now() - env4.pm_uptime;
|
|
40382
|
+
if (up > uptimeMax)
|
|
40383
|
+
uptimeMax = up;
|
|
40384
|
+
}
|
|
40385
|
+
if (env4.status)
|
|
40386
|
+
status2 = String(env4.status);
|
|
40387
|
+
}
|
|
40388
|
+
const env3 = first?.pm2_env || {};
|
|
40389
|
+
return {
|
|
40390
|
+
found: true,
|
|
40391
|
+
name,
|
|
40392
|
+
pmId: Number(first?.pm_id ?? -1),
|
|
40393
|
+
pid: Number(first?.pid ?? 0),
|
|
40394
|
+
status: status2,
|
|
40395
|
+
uptimeMs: uptimeMax || undefined,
|
|
40396
|
+
restarts,
|
|
40397
|
+
unstableRestarts: unstable,
|
|
40398
|
+
cpuPercent: Math.round(cpuSum * 100) / 100,
|
|
40399
|
+
memoryBytes: memSum,
|
|
40400
|
+
execMode: env3.exec_mode ? String(env3.exec_mode) : undefined,
|
|
40401
|
+
instances: matched.length,
|
|
40402
|
+
pm2_env_status: env3.status ? String(env3.status) : undefined,
|
|
40403
|
+
errorLogPath: env3.pm_err_log_path ? String(env3.pm_err_log_path) : undefined,
|
|
40404
|
+
outLogPath: env3.pm_out_log_path ? String(env3.pm_out_log_path) : undefined,
|
|
40405
|
+
createdAt: typeof env3.created_at === "number" ? env3.created_at : undefined
|
|
40406
|
+
};
|
|
40407
|
+
}
|
|
40408
|
+
|
|
40409
|
+
// src/routes/pm2.ts
|
|
40410
|
+
var pm2Routes = new Elysia().get("/api/pm2/available", async ({ headers, set: set3 }) => {
|
|
40411
|
+
if (!verifyAdminToken(headers)) {
|
|
40412
|
+
set3.status = 401;
|
|
40413
|
+
return { error: "Unauthorized" };
|
|
40414
|
+
}
|
|
40415
|
+
const available = await isPm2Available();
|
|
40416
|
+
return { available };
|
|
40417
|
+
}).get("/api/pm2/apps", async ({ headers, set: set3 }) => {
|
|
40418
|
+
if (!verifyAdminToken(headers)) {
|
|
40419
|
+
set3.status = 401;
|
|
40420
|
+
return { error: "Unauthorized" };
|
|
40421
|
+
}
|
|
40422
|
+
const apps = await listPm2Apps();
|
|
40423
|
+
return { apps };
|
|
40424
|
+
}).get("/api/projects/:id/pm2", async ({ headers, params, set: set3 }) => {
|
|
40425
|
+
if (!verifyAdminToken(headers)) {
|
|
40426
|
+
set3.status = 401;
|
|
40427
|
+
return { error: "Unauthorized" };
|
|
40428
|
+
}
|
|
40429
|
+
const project = await db.projects.findById(params.id);
|
|
40430
|
+
if (!project) {
|
|
40431
|
+
set3.status = 404;
|
|
40432
|
+
return { error: "Project not found" };
|
|
40433
|
+
}
|
|
40434
|
+
if (!project.pm2AppName) {
|
|
40435
|
+
return { bound: false, message: "project has no pm2 app bound" };
|
|
40436
|
+
}
|
|
40437
|
+
const status2 = await getPm2AppStatus(project.pm2AppName);
|
|
40438
|
+
return { bound: true, ...status2 };
|
|
40439
|
+
});
|
|
40440
|
+
|
|
40441
|
+
// src/routes/tags.ts
|
|
40442
|
+
init_dist3();
|
|
40443
|
+
var ALLOWED_COLORS2 = new Set(["blue", "green", "yellow", "purple", "pink", "cyan", "gray"]);
|
|
40444
|
+
var COLOR_PALETTE2 = ["blue", "green", "yellow", "purple", "pink", "cyan", "gray"];
|
|
40445
|
+
function normalizeColor2(input) {
|
|
40446
|
+
if (input === null || input === undefined || input === "")
|
|
40447
|
+
return null;
|
|
40448
|
+
if (typeof input !== "string")
|
|
40449
|
+
return null;
|
|
40450
|
+
return ALLOWED_COLORS2.has(input) ? input : null;
|
|
40451
|
+
}
|
|
40452
|
+
async function pickFreeColor2() {
|
|
40453
|
+
const list = await db.tags.findAll();
|
|
40454
|
+
const used = new Set;
|
|
40455
|
+
for (const t2 of list) {
|
|
40456
|
+
if (t2.color)
|
|
40457
|
+
used.add(t2.color);
|
|
40458
|
+
}
|
|
40459
|
+
for (const color of COLOR_PALETTE2) {
|
|
40460
|
+
if (!used.has(color))
|
|
40461
|
+
return color;
|
|
40462
|
+
}
|
|
40463
|
+
return COLOR_PALETTE2[list.length % COLOR_PALETTE2.length];
|
|
40464
|
+
}
|
|
40465
|
+
function normalizeName2(input) {
|
|
40466
|
+
if (typeof input !== "string")
|
|
40467
|
+
return "";
|
|
40468
|
+
return input.trim();
|
|
40469
|
+
}
|
|
40470
|
+
var tagRoutes = new Elysia().get("/api/tags", async ({ headers, set: set3 }) => {
|
|
40471
|
+
if (!verifyAdminToken(headers)) {
|
|
40472
|
+
set3.status = 401;
|
|
40473
|
+
return { error: "Unauthorized" };
|
|
40474
|
+
}
|
|
40475
|
+
const tags2 = await db.tags.findAll();
|
|
40476
|
+
const pairs = await db.projectTags.listAllPairs();
|
|
40477
|
+
const counts = new Map;
|
|
40478
|
+
for (const p of pairs)
|
|
40479
|
+
counts.set(p.tagId, (counts.get(p.tagId) ?? 0) + 1);
|
|
40480
|
+
return tags2.map((t2) => ({ ...t2, projectCount: counts.get(t2.id) ?? 0 }));
|
|
40481
|
+
}).post("/api/tags", async ({ headers, body, set: set3 }) => {
|
|
40482
|
+
if (!verifyAdminToken(headers)) {
|
|
40483
|
+
set3.status = 401;
|
|
40484
|
+
return { error: "Unauthorized" };
|
|
40485
|
+
}
|
|
40486
|
+
const name = normalizeName2(body.name);
|
|
40487
|
+
if (!name) {
|
|
40488
|
+
set3.status = 400;
|
|
40489
|
+
return { error: "标签名不能为空" };
|
|
40490
|
+
}
|
|
40491
|
+
if (name.length > 30) {
|
|
40492
|
+
set3.status = 400;
|
|
40493
|
+
return { error: "标签名过长(最多 30 字符)" };
|
|
40494
|
+
}
|
|
40495
|
+
const exist = await db.tags.findByName(name);
|
|
40496
|
+
if (exist) {
|
|
40497
|
+
set3.status = 409;
|
|
40498
|
+
return { error: "标签名已存在", conflictTag: exist.name };
|
|
40499
|
+
}
|
|
40500
|
+
let color = normalizeColor2(body.color);
|
|
40501
|
+
if (!color)
|
|
40502
|
+
color = await pickFreeColor2();
|
|
40503
|
+
const sortOrder = typeof body.sortOrder === "number" ? body.sortOrder : 0;
|
|
40504
|
+
const created = await db.tags.create({ name, color, sortOrder });
|
|
40505
|
+
await writeAudit({ headers }, {
|
|
40506
|
+
action: "tag.create",
|
|
40507
|
+
targetType: "tag",
|
|
40508
|
+
targetId: created.id,
|
|
40509
|
+
targetName: created.name,
|
|
40510
|
+
before: null,
|
|
40511
|
+
after: sanitize2(created),
|
|
40512
|
+
summary: `创建标签 ${created.name}`
|
|
40513
|
+
});
|
|
40514
|
+
return { success: true, tag: created };
|
|
40515
|
+
}, {
|
|
40516
|
+
body: t.Object({
|
|
40517
|
+
name: t.String(),
|
|
40518
|
+
color: t.Optional(t.Union([t.String(), t.Null()])),
|
|
40519
|
+
sortOrder: t.Optional(t.Number())
|
|
40520
|
+
})
|
|
40521
|
+
}).put("/api/tags/:id", async ({ headers, params, body, set: set3 }) => {
|
|
40522
|
+
if (!verifyAdminToken(headers)) {
|
|
40523
|
+
set3.status = 401;
|
|
40524
|
+
return { error: "Unauthorized" };
|
|
40525
|
+
}
|
|
40526
|
+
const before = await db.tags.findById(params.id);
|
|
40527
|
+
if (!before) {
|
|
40528
|
+
set3.status = 404;
|
|
40529
|
+
return { error: "Tag not found" };
|
|
40530
|
+
}
|
|
40531
|
+
const patch = {};
|
|
40532
|
+
if (body.name !== undefined) {
|
|
40533
|
+
const name = normalizeName2(body.name);
|
|
40534
|
+
if (!name) {
|
|
40535
|
+
set3.status = 400;
|
|
40536
|
+
return { error: "标签名不能为空" };
|
|
40537
|
+
}
|
|
40538
|
+
if (name.length > 30) {
|
|
40539
|
+
set3.status = 400;
|
|
40540
|
+
return { error: "标签名过长(最多 30 字符)" };
|
|
40541
|
+
}
|
|
40542
|
+
if (name !== before.name) {
|
|
40543
|
+
const conflict = await db.tags.findByName(name);
|
|
40544
|
+
if (conflict && conflict.id !== params.id) {
|
|
40545
|
+
set3.status = 409;
|
|
40546
|
+
return { error: "标签名已存在", conflictTag: conflict.name };
|
|
40547
|
+
}
|
|
40548
|
+
}
|
|
40549
|
+
patch.name = name;
|
|
40550
|
+
}
|
|
40551
|
+
if (body.color !== undefined)
|
|
40552
|
+
patch.color = normalizeColor2(body.color);
|
|
40553
|
+
if (body.sortOrder !== undefined && typeof body.sortOrder === "number") {
|
|
40554
|
+
patch.sortOrder = body.sortOrder;
|
|
40555
|
+
}
|
|
40556
|
+
const after = await db.tags.update(params.id, patch);
|
|
40557
|
+
if (!after) {
|
|
40558
|
+
set3.status = 404;
|
|
40559
|
+
return { error: "Tag not found" };
|
|
40560
|
+
}
|
|
40561
|
+
const diff = diffFields(before, after, Object.keys(patch));
|
|
40562
|
+
if (Object.keys(diff.after).length > 0) {
|
|
40563
|
+
await writeAudit({ headers }, {
|
|
40564
|
+
action: "tag.update",
|
|
40565
|
+
targetType: "tag",
|
|
40566
|
+
targetId: params.id,
|
|
40567
|
+
targetName: after.name,
|
|
40568
|
+
before: diff.before,
|
|
40569
|
+
after: diff.after,
|
|
40570
|
+
summary: `更新标签配置:${Object.keys(diff.after).join(", ")}`
|
|
40571
|
+
});
|
|
40572
|
+
}
|
|
40573
|
+
return { success: true, tag: after };
|
|
40574
|
+
}, {
|
|
40575
|
+
body: t.Object({
|
|
40576
|
+
name: t.Optional(t.String()),
|
|
40577
|
+
color: t.Optional(t.Union([t.String(), t.Null()])),
|
|
40578
|
+
sortOrder: t.Optional(t.Number())
|
|
40579
|
+
})
|
|
40580
|
+
}).delete("/api/tags/:id", async ({ headers, params, set: set3 }) => {
|
|
40581
|
+
if (!verifyAdminToken(headers)) {
|
|
40582
|
+
set3.status = 401;
|
|
40583
|
+
return { error: "Unauthorized" };
|
|
40584
|
+
}
|
|
40585
|
+
const before = await db.tags.findById(params.id);
|
|
40586
|
+
if (!before) {
|
|
40587
|
+
set3.status = 404;
|
|
40588
|
+
return { error: "Tag not found" };
|
|
40589
|
+
}
|
|
40590
|
+
const projectCount = await db.tags.countProjects(params.id);
|
|
40591
|
+
const success = await db.tags.remove(params.id);
|
|
40592
|
+
if (!success) {
|
|
40593
|
+
set3.status = 404;
|
|
40594
|
+
return { error: "Tag not found" };
|
|
40595
|
+
}
|
|
40596
|
+
await writeAudit({ headers }, {
|
|
40597
|
+
action: "tag.delete",
|
|
40598
|
+
targetType: "tag",
|
|
40599
|
+
targetId: before.id,
|
|
40600
|
+
targetName: before.name,
|
|
40601
|
+
before: { ...sanitize2(before), projectCount },
|
|
40602
|
+
after: null,
|
|
40603
|
+
summary: `删除标签 ${before.name}(解除 ${projectCount} 个项目关联)`
|
|
40604
|
+
});
|
|
40605
|
+
return { success: true, detachedProjects: projectCount };
|
|
40606
|
+
});
|
|
40607
|
+
|
|
40608
|
+
// src/routes/terminal.ts
|
|
40609
|
+
init_dist3();
|
|
40610
|
+
|
|
40611
|
+
// src/lib/ip-allowlist.ts
|
|
40612
|
+
function normalizeIp(ip) {
|
|
40613
|
+
if (!ip)
|
|
40614
|
+
return "";
|
|
40615
|
+
let s = ip.trim();
|
|
40616
|
+
if (s.startsWith("[") && s.endsWith("]"))
|
|
40617
|
+
s = s.slice(1, -1);
|
|
40618
|
+
const pct = s.indexOf("%");
|
|
40619
|
+
if (pct >= 0)
|
|
40620
|
+
s = s.slice(0, pct);
|
|
40621
|
+
const v4mapped = s.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
40622
|
+
if (v4mapped)
|
|
40623
|
+
return v4mapped[1];
|
|
40624
|
+
if (s === "::1")
|
|
40625
|
+
return "::1";
|
|
40626
|
+
return s.toLowerCase();
|
|
40627
|
+
}
|
|
40628
|
+
function isIPv4(s) {
|
|
40629
|
+
const m = s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
40630
|
+
if (!m)
|
|
40631
|
+
return false;
|
|
40632
|
+
for (let i = 1;i <= 4; i++) {
|
|
40633
|
+
const n = Number(m[i]);
|
|
40634
|
+
if (!Number.isInteger(n) || n < 0 || n > 255)
|
|
40635
|
+
return false;
|
|
40636
|
+
}
|
|
40637
|
+
return true;
|
|
40638
|
+
}
|
|
40639
|
+
function ipv4ToInt(s) {
|
|
40640
|
+
const parts = s.split(".").map((p) => Number(p));
|
|
40641
|
+
return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
40642
|
+
}
|
|
40643
|
+
function isIPv6(s) {
|
|
40644
|
+
return s.includes(":") && /^[0-9a-fA-F:]+$/.test(s);
|
|
40645
|
+
}
|
|
40646
|
+
function expandIPv6(s) {
|
|
40647
|
+
if (s === "::")
|
|
40648
|
+
return 0n;
|
|
40649
|
+
const dq = s.indexOf("::");
|
|
40650
|
+
let head;
|
|
40651
|
+
let tail;
|
|
40652
|
+
if (dq >= 0) {
|
|
40653
|
+
head = s.slice(0, dq) ? s.slice(0, dq).split(":") : [];
|
|
40654
|
+
tail = s.slice(dq + 2) ? s.slice(dq + 2).split(":") : [];
|
|
40655
|
+
} else {
|
|
40656
|
+
head = s.split(":");
|
|
40657
|
+
tail = [];
|
|
40658
|
+
}
|
|
40659
|
+
const last = tail.length ? tail[tail.length - 1] : head.length ? head[head.length - 1] : "";
|
|
40660
|
+
if (last && last.includes(".")) {
|
|
40661
|
+
if (!isIPv4(last))
|
|
40662
|
+
return null;
|
|
40663
|
+
const v4 = ipv4ToInt(last);
|
|
40664
|
+
const hi = v4 >>> 16 & 65535;
|
|
40665
|
+
const lo = v4 & 65535;
|
|
40666
|
+
if (tail.length) {
|
|
40667
|
+
tail = tail.slice(0, -1).concat(hi.toString(16), lo.toString(16));
|
|
40668
|
+
} else {
|
|
40669
|
+
head = head.slice(0, -1).concat(hi.toString(16), lo.toString(16));
|
|
40670
|
+
}
|
|
40671
|
+
}
|
|
40672
|
+
const totalGroups = head.length + tail.length;
|
|
40673
|
+
if (totalGroups > 8)
|
|
40674
|
+
return null;
|
|
40675
|
+
if (dq < 0 && totalGroups !== 8)
|
|
40676
|
+
return null;
|
|
40677
|
+
const fill = 8 - totalGroups;
|
|
40678
|
+
const groups = [...head, ...Array.from({ length: fill }, () => "0"), ...tail];
|
|
40679
|
+
if (groups.length !== 8)
|
|
40680
|
+
return null;
|
|
40681
|
+
let result = 0n;
|
|
40682
|
+
for (const g of groups) {
|
|
40683
|
+
if (g.length === 0 || g.length > 4)
|
|
40684
|
+
return null;
|
|
40685
|
+
if (!/^[0-9a-fA-F]+$/.test(g))
|
|
40686
|
+
return null;
|
|
40687
|
+
result = result << 16n | BigInt(parseInt(g, 16));
|
|
40688
|
+
}
|
|
40689
|
+
return result;
|
|
40690
|
+
}
|
|
40691
|
+
function parseAllowlistEntry(raw) {
|
|
40692
|
+
const trimmed = raw.trim();
|
|
40693
|
+
if (!trimmed)
|
|
40694
|
+
return null;
|
|
40695
|
+
const slash = trimmed.indexOf("/");
|
|
40696
|
+
let host = trimmed;
|
|
40697
|
+
let prefix = null;
|
|
40698
|
+
if (slash >= 0) {
|
|
40699
|
+
host = trimmed.slice(0, slash);
|
|
40700
|
+
const p = Number(trimmed.slice(slash + 1));
|
|
40701
|
+
if (!Number.isInteger(p) || p < 0)
|
|
40702
|
+
return null;
|
|
40703
|
+
prefix = p;
|
|
40704
|
+
}
|
|
40705
|
+
const norm = normalizeIp(host);
|
|
40706
|
+
if (isIPv4(norm)) {
|
|
40707
|
+
if (prefix === null)
|
|
40708
|
+
prefix = 32;
|
|
40709
|
+
if (prefix > 32)
|
|
40710
|
+
return null;
|
|
40711
|
+
const ipInt = BigInt(ipv4ToInt(norm));
|
|
40712
|
+
const mask = prefix === 0 ? 0n : 0xffffffffn << BigInt(32 - prefix) & 0xffffffffn;
|
|
40713
|
+
return { kind: "v4", base: ipInt & mask, mask, prefixLen: prefix, raw: trimmed };
|
|
40714
|
+
}
|
|
40715
|
+
if (isIPv6(norm)) {
|
|
40716
|
+
if (prefix === null)
|
|
40717
|
+
prefix = 128;
|
|
40718
|
+
if (prefix > 128)
|
|
40719
|
+
return null;
|
|
40720
|
+
const ipInt = expandIPv6(norm);
|
|
40721
|
+
if (ipInt === null)
|
|
40722
|
+
return null;
|
|
40723
|
+
const full = (1n << 128n) - 1n;
|
|
40724
|
+
const mask = prefix === 0 ? 0n : full << BigInt(128 - prefix) & full;
|
|
40725
|
+
return { kind: "v6", base: ipInt & mask, mask, prefixLen: prefix, raw: trimmed };
|
|
40726
|
+
}
|
|
40727
|
+
return null;
|
|
40728
|
+
}
|
|
40729
|
+
function parseAllowlist(entries) {
|
|
40730
|
+
const parsed = [];
|
|
40731
|
+
const invalid = [];
|
|
40732
|
+
if (!Array.isArray(entries))
|
|
40733
|
+
return { parsed, invalid };
|
|
40734
|
+
for (const raw of entries) {
|
|
40735
|
+
if (typeof raw !== "string")
|
|
40736
|
+
continue;
|
|
40737
|
+
const trimmed = raw.trim();
|
|
40738
|
+
if (!trimmed)
|
|
40739
|
+
continue;
|
|
40740
|
+
const p = parseAllowlistEntry(trimmed);
|
|
40741
|
+
if (p)
|
|
40742
|
+
parsed.push(p);
|
|
40743
|
+
else
|
|
40744
|
+
invalid.push(trimmed);
|
|
40745
|
+
}
|
|
40746
|
+
return { parsed, invalid };
|
|
40747
|
+
}
|
|
40748
|
+
function ipMatchesEntry(ip, entry) {
|
|
40749
|
+
const norm = normalizeIp(ip);
|
|
40750
|
+
if (entry.kind === "v4") {
|
|
40751
|
+
if (!isIPv4(norm))
|
|
40752
|
+
return false;
|
|
40753
|
+
const ipInt2 = BigInt(ipv4ToInt(norm));
|
|
40754
|
+
return (ipInt2 & entry.mask) === entry.base;
|
|
40755
|
+
}
|
|
40756
|
+
if (!isIPv6(norm))
|
|
40757
|
+
return false;
|
|
40758
|
+
const ipInt = expandIPv6(norm);
|
|
40759
|
+
if (ipInt === null)
|
|
40760
|
+
return false;
|
|
40761
|
+
return (ipInt & entry.mask) === entry.base;
|
|
40762
|
+
}
|
|
40763
|
+
function ipAllowed(ip, allowlist) {
|
|
40764
|
+
if (!allowlist || allowlist.length === 0)
|
|
40765
|
+
return true;
|
|
40766
|
+
const { parsed } = parseAllowlist(allowlist);
|
|
40767
|
+
if (parsed.length === 0)
|
|
40768
|
+
return true;
|
|
40769
|
+
for (const entry of parsed) {
|
|
40770
|
+
if (ipMatchesEntry(ip, entry))
|
|
40771
|
+
return true;
|
|
40772
|
+
}
|
|
40773
|
+
return false;
|
|
40774
|
+
}
|
|
40775
|
+
|
|
40776
|
+
// src/lib/client-ip.ts
|
|
40777
|
+
function headerString(value2) {
|
|
40778
|
+
if (Array.isArray(value2))
|
|
40779
|
+
return value2[0];
|
|
40780
|
+
return value2;
|
|
40781
|
+
}
|
|
40782
|
+
function resolveClientIp(input) {
|
|
40783
|
+
const socketIp = normalizeIp(input.socketRemoteAddress || "");
|
|
40784
|
+
const headers = input.headers || {};
|
|
40785
|
+
const xff = headerString(headers["x-forwarded-for"]) || headerString(headers["X-Forwarded-For"]);
|
|
40786
|
+
const realIp = headerString(headers["x-real-ip"]) || headerString(headers["X-Real-IP"]);
|
|
40787
|
+
let forwardedIp = null;
|
|
40788
|
+
if (typeof xff === "string" && xff.length > 0) {
|
|
40789
|
+
forwardedIp = normalizeIp(xff.split(",")[0].trim());
|
|
40790
|
+
} else if (typeof realIp === "string" && realIp.length > 0) {
|
|
40791
|
+
forwardedIp = normalizeIp(realIp.trim());
|
|
40792
|
+
}
|
|
40793
|
+
return {
|
|
40794
|
+
socketIp,
|
|
40795
|
+
forwardedIp,
|
|
40796
|
+
trusted: socketIp || forwardedIp || ""
|
|
40797
|
+
};
|
|
40798
|
+
}
|
|
40799
|
+
|
|
40800
|
+
// src/lib/terminal.ts
|
|
40801
|
+
import os8 from "node:os";
|
|
40802
|
+
import fs12 from "node:fs";
|
|
40803
|
+
import path15 from "node:path";
|
|
40804
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
40805
|
+
var termLog = moduleLogger("terminal");
|
|
40806
|
+
var TERMINAL_LIMITS = {
|
|
40807
|
+
maxSessionsPerIp: 4,
|
|
40808
|
+
maxTotalSessions: 16,
|
|
40809
|
+
maxLifetimeMs: 2 * 60 * 60 * 1000,
|
|
40810
|
+
idleTimeoutMs: 15 * 60 * 1000
|
|
40811
|
+
};
|
|
40812
|
+
var loadPromise = null;
|
|
40813
|
+
function isPlatformSupported() {
|
|
40814
|
+
return process.platform === "darwin" || process.platform === "linux";
|
|
40815
|
+
}
|
|
40816
|
+
function isBunRuntime() {
|
|
40817
|
+
return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun?.spawn === "function";
|
|
40818
|
+
}
|
|
40819
|
+
async function loadPty() {
|
|
40820
|
+
if (!isPlatformSupported()) {
|
|
40821
|
+
return { available: false, error: `当前平台不支持终端能力:${process.platform}` };
|
|
40822
|
+
}
|
|
40823
|
+
if (loadPromise)
|
|
40824
|
+
return loadPromise;
|
|
40825
|
+
loadPromise = (async () => {
|
|
40826
|
+
if (isBunRuntime()) {
|
|
40827
|
+
return { available: true, driver: "bun" };
|
|
40828
|
+
}
|
|
40829
|
+
try {
|
|
40830
|
+
const mod = await import("node-pty");
|
|
40831
|
+
const spawn3 = mod?.spawn || mod?.default?.spawn;
|
|
40832
|
+
if (typeof spawn3 !== "function") {
|
|
40833
|
+
return { available: false, error: "node-pty 加载成功但未导出 spawn 函数" };
|
|
40834
|
+
}
|
|
40835
|
+
return { available: true, driver: "node-pty", ptyMod: mod };
|
|
40836
|
+
} catch (err) {
|
|
40837
|
+
const msg = err?.message || String(err);
|
|
40838
|
+
termLog.warn({ err: msg }, "加载 node-pty 失败,终端能力将不可用");
|
|
40839
|
+
return { available: false, error: msg };
|
|
40840
|
+
}
|
|
40841
|
+
})();
|
|
40842
|
+
return loadPromise;
|
|
40843
|
+
}
|
|
40844
|
+
var sessions = new Map;
|
|
40845
|
+
var sessionsByIp = new Map;
|
|
40846
|
+
function listSessionsCounts() {
|
|
40847
|
+
const perIp = {};
|
|
40848
|
+
for (const [ip, ids] of sessionsByIp)
|
|
40849
|
+
perIp[ip] = ids.size;
|
|
40850
|
+
return { total: sessions.size, perIp };
|
|
40851
|
+
}
|
|
40852
|
+
function defaultShell() {
|
|
40853
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
40854
|
+
return process.env.SHELL || "/bin/bash";
|
|
40855
|
+
}
|
|
40856
|
+
return process.env.SHELL || "/bin/sh";
|
|
40857
|
+
}
|
|
40858
|
+
function resolveCwd(candidate) {
|
|
40859
|
+
const home = os8.homedir();
|
|
40860
|
+
if (!candidate || typeof candidate !== "string")
|
|
40861
|
+
return home;
|
|
40862
|
+
try {
|
|
40863
|
+
const resolved = path15.resolve(candidate);
|
|
40864
|
+
const stat2 = fs12.statSync(resolved);
|
|
40865
|
+
if (stat2.isDirectory())
|
|
40866
|
+
return resolved;
|
|
40867
|
+
} catch {}
|
|
40868
|
+
return home;
|
|
40869
|
+
}
|
|
40870
|
+
function clampCols(c) {
|
|
40871
|
+
return Math.max(2, Math.min(500, Math.floor(c) || 80));
|
|
40872
|
+
}
|
|
40873
|
+
function clampRows(r) {
|
|
40874
|
+
return Math.max(2, Math.min(200, Math.floor(r) || 24));
|
|
40875
|
+
}
|
|
40876
|
+
function attachToBookkeeping(session, ip) {
|
|
40877
|
+
sessions.set(session.id, session);
|
|
40878
|
+
const bucket = sessionsByIp.get(ip);
|
|
40879
|
+
if (!bucket)
|
|
40880
|
+
sessionsByIp.set(ip, new Set([session.id]));
|
|
40881
|
+
else
|
|
40882
|
+
bucket.add(session.id);
|
|
40883
|
+
}
|
|
40884
|
+
function detachFromBookkeeping(session, ip) {
|
|
40885
|
+
sessions.delete(session.id);
|
|
40886
|
+
const bucket = sessionsByIp.get(ip);
|
|
40887
|
+
if (bucket) {
|
|
40888
|
+
bucket.delete(session.id);
|
|
40889
|
+
if (bucket.size === 0)
|
|
40890
|
+
sessionsByIp.delete(ip);
|
|
40891
|
+
}
|
|
40892
|
+
}
|
|
40893
|
+
function makeSessionShell(params) {
|
|
40894
|
+
const dataListeners = new Set;
|
|
40895
|
+
const exitListeners = new Set;
|
|
40896
|
+
const session = {
|
|
40897
|
+
id: params.id,
|
|
40898
|
+
pid: params.pid,
|
|
40899
|
+
cwd: params.cwd,
|
|
40900
|
+
shell: params.shell,
|
|
40901
|
+
createdAt: Date.now(),
|
|
40902
|
+
ip: params.ip,
|
|
40903
|
+
dataListeners,
|
|
40904
|
+
exitListeners,
|
|
40905
|
+
lastActiveAt: Date.now(),
|
|
40906
|
+
pendingChunks: [],
|
|
40907
|
+
pendingExit: null,
|
|
40908
|
+
lifetimeTimer: undefined,
|
|
40909
|
+
idleTimer: undefined,
|
|
40910
|
+
_killImpl: () => {},
|
|
40911
|
+
_writeImpl: () => {},
|
|
40912
|
+
_resizeImpl: () => {},
|
|
40913
|
+
write(data) {
|
|
40914
|
+
session.touch();
|
|
40915
|
+
try {
|
|
40916
|
+
session._writeImpl(data);
|
|
40917
|
+
} catch {}
|
|
40918
|
+
},
|
|
40919
|
+
resize(c, r) {
|
|
40920
|
+
session.touch();
|
|
40921
|
+
try {
|
|
40922
|
+
session._resizeImpl(clampCols(c), clampRows(r));
|
|
40923
|
+
} catch {}
|
|
40924
|
+
},
|
|
40925
|
+
kill(signal) {
|
|
40926
|
+
try {
|
|
40927
|
+
session._killImpl(signal || "SIGTERM");
|
|
40928
|
+
} catch {}
|
|
40929
|
+
},
|
|
40930
|
+
onData(listener) {
|
|
40931
|
+
dataListeners.add(listener);
|
|
40932
|
+
if (session.pendingChunks.length > 0) {
|
|
40933
|
+
const buffered = session.pendingChunks.splice(0);
|
|
40934
|
+
for (const chunk of buffered) {
|
|
40935
|
+
try {
|
|
40936
|
+
listener(chunk);
|
|
40937
|
+
} catch {}
|
|
40938
|
+
}
|
|
40939
|
+
}
|
|
40940
|
+
},
|
|
40941
|
+
onExit(listener) {
|
|
40942
|
+
exitListeners.add(listener);
|
|
40943
|
+
if (session.pendingExit) {
|
|
40944
|
+
const evt = session.pendingExit;
|
|
40945
|
+
try {
|
|
40946
|
+
listener(evt);
|
|
40947
|
+
} catch {}
|
|
40948
|
+
}
|
|
40949
|
+
},
|
|
40950
|
+
touch() {
|
|
40951
|
+
session.lastActiveAt = Date.now();
|
|
40952
|
+
if (session.idleTimer)
|
|
40953
|
+
clearTimeout(session.idleTimer);
|
|
40954
|
+
session.idleTimer = setTimeout(() => {
|
|
40955
|
+
termLog.warn({ id: session.id, pid: session.pid }, "pty session idle timeout, killing");
|
|
40956
|
+
try {
|
|
40957
|
+
session.kill("SIGTERM");
|
|
40958
|
+
} catch {}
|
|
40959
|
+
}, TERMINAL_LIMITS.idleTimeoutMs);
|
|
40960
|
+
}
|
|
40961
|
+
};
|
|
40962
|
+
return session;
|
|
40963
|
+
}
|
|
40964
|
+
function emitData(session, chunk) {
|
|
40965
|
+
session.touch();
|
|
40966
|
+
if (session.dataListeners.size === 0) {
|
|
40967
|
+
session.pendingChunks.push(chunk);
|
|
40968
|
+
if (session.pendingChunks.length > 256) {
|
|
40969
|
+
session.pendingChunks.splice(0, session.pendingChunks.length - 256);
|
|
40970
|
+
}
|
|
40971
|
+
return;
|
|
40972
|
+
}
|
|
40973
|
+
for (const l of session.dataListeners) {
|
|
40974
|
+
try {
|
|
40975
|
+
l(chunk);
|
|
40976
|
+
} catch {}
|
|
40977
|
+
}
|
|
40978
|
+
}
|
|
40979
|
+
function emitExit(session, ip, evt) {
|
|
40980
|
+
if (session.lifetimeTimer)
|
|
40981
|
+
clearTimeout(session.lifetimeTimer);
|
|
40982
|
+
if (session.idleTimer)
|
|
40983
|
+
clearTimeout(session.idleTimer);
|
|
40984
|
+
detachFromBookkeeping(session, ip);
|
|
40985
|
+
if (session.exitListeners.size === 0) {
|
|
40986
|
+
session.pendingExit = evt;
|
|
40987
|
+
return;
|
|
40988
|
+
}
|
|
40989
|
+
for (const l of session.exitListeners) {
|
|
40990
|
+
try {
|
|
40991
|
+
l(evt);
|
|
40992
|
+
} catch {}
|
|
40993
|
+
}
|
|
40994
|
+
session.dataListeners.clear();
|
|
40995
|
+
session.exitListeners.clear();
|
|
40996
|
+
}
|
|
40997
|
+
async function spawnTerminalSession(params) {
|
|
40998
|
+
const load = await loadPty();
|
|
40999
|
+
if (!load.available) {
|
|
41000
|
+
return { ok: false, reason: "unavailable", message: load.error || "终端能力不可用" };
|
|
41001
|
+
}
|
|
41002
|
+
if (sessions.size >= TERMINAL_LIMITS.maxTotalSessions) {
|
|
41003
|
+
return { ok: false, reason: "limit-total", message: `已达到全局并发上限 (${TERMINAL_LIMITS.maxTotalSessions})` };
|
|
41004
|
+
}
|
|
41005
|
+
const ipBucket = sessionsByIp.get(params.ip);
|
|
41006
|
+
if (ipBucket && ipBucket.size >= TERMINAL_LIMITS.maxSessionsPerIp) {
|
|
41007
|
+
return { ok: false, reason: "limit-per-ip", message: `单 IP 并发上限 (${TERMINAL_LIMITS.maxSessionsPerIp})` };
|
|
41008
|
+
}
|
|
41009
|
+
const shell = defaultShell();
|
|
41010
|
+
const cwd = resolveCwd(params.cwd);
|
|
41011
|
+
const cols = clampCols(params.cols);
|
|
41012
|
+
const rows = clampRows(params.rows);
|
|
41013
|
+
const env3 = {
|
|
41014
|
+
...process.env,
|
|
41015
|
+
TERM: process.env.TERM || "xterm-256color",
|
|
41016
|
+
LANG: process.env.LANG || "en_US.UTF-8",
|
|
41017
|
+
KITE_TERMINAL: "1"
|
|
41018
|
+
};
|
|
41019
|
+
delete env3.ADMIN_TOKEN;
|
|
41020
|
+
const id = "term_" + randomUUID3().replace(/-/g, "").slice(0, 16);
|
|
41021
|
+
if (load.driver === "bun") {
|
|
41022
|
+
return spawnViaBun({ id, shell, cwd, cols, rows, env: env3, ip: params.ip });
|
|
41023
|
+
}
|
|
41024
|
+
return spawnViaNodePty({ id, shell, cwd, cols, rows, env: env3, ip: params.ip, ptyMod: load.ptyMod });
|
|
41025
|
+
}
|
|
41026
|
+
function spawnViaNodePty(opts) {
|
|
41027
|
+
let pty;
|
|
41028
|
+
try {
|
|
41029
|
+
pty = opts.ptyMod.spawn(opts.shell, [], {
|
|
41030
|
+
name: opts.env.TERM,
|
|
41031
|
+
cols: opts.cols,
|
|
41032
|
+
rows: opts.rows,
|
|
41033
|
+
cwd: opts.cwd,
|
|
41034
|
+
env: opts.env
|
|
41035
|
+
});
|
|
41036
|
+
} catch (err) {
|
|
41037
|
+
termLog.error({ err: err?.message || String(err) }, "spawn pty failed (node-pty)");
|
|
41038
|
+
return { ok: false, reason: "spawn-error", message: err?.message || "spawn pty failed" };
|
|
41039
|
+
}
|
|
41040
|
+
const session = makeSessionShell({
|
|
41041
|
+
id: opts.id,
|
|
41042
|
+
pid: pty.pid,
|
|
41043
|
+
cwd: opts.cwd,
|
|
41044
|
+
shell: opts.shell,
|
|
41045
|
+
ip: opts.ip
|
|
41046
|
+
});
|
|
41047
|
+
session._writeImpl = (data) => pty.write(data);
|
|
41048
|
+
session._resizeImpl = (c, r) => pty.resize(c, r);
|
|
41049
|
+
session._killImpl = (sig) => pty.kill(sig || "SIGTERM");
|
|
41050
|
+
session.lifetimeTimer = setTimeout(() => {
|
|
41051
|
+
termLog.warn({ id: opts.id, pid: pty.pid }, "pty session exceeded max lifetime, killing");
|
|
41052
|
+
try {
|
|
41053
|
+
pty.kill("SIGTERM");
|
|
41054
|
+
} catch {}
|
|
41055
|
+
}, TERMINAL_LIMITS.maxLifetimeMs);
|
|
41056
|
+
session.idleTimer = setTimeout(() => {
|
|
41057
|
+
termLog.warn({ id: opts.id, pid: pty.pid }, "pty session idle timeout, killing");
|
|
41058
|
+
try {
|
|
41059
|
+
pty.kill("SIGTERM");
|
|
41060
|
+
} catch {}
|
|
41061
|
+
}, TERMINAL_LIMITS.idleTimeoutMs);
|
|
41062
|
+
attachToBookkeeping(session, opts.ip);
|
|
41063
|
+
pty.onData((chunk) => emitData(session, chunk));
|
|
41064
|
+
pty.onExit((evt) => emitExit(session, opts.ip, { exitCode: evt.exitCode ?? 0, signal: evt.signal }));
|
|
41065
|
+
termLog.info({ id: opts.id, pid: pty.pid, cwd: opts.cwd, shell: opts.shell, ip: opts.ip, driver: "node-pty" }, "pty session started");
|
|
41066
|
+
return { ok: true, handle: session };
|
|
41067
|
+
}
|
|
41068
|
+
function spawnViaBun(opts) {
|
|
41069
|
+
const Bun2 = globalThis.Bun;
|
|
41070
|
+
if (!Bun2 || typeof Bun2.spawn !== "function") {
|
|
41071
|
+
return { ok: false, reason: "spawn-error", message: "Bun.spawn 不可用" };
|
|
41072
|
+
}
|
|
41073
|
+
const session = makeSessionShell({
|
|
41074
|
+
id: opts.id,
|
|
41075
|
+
pid: 0,
|
|
41076
|
+
cwd: opts.cwd,
|
|
41077
|
+
shell: opts.shell,
|
|
41078
|
+
ip: opts.ip
|
|
41079
|
+
});
|
|
41080
|
+
let proc;
|
|
41081
|
+
try {
|
|
41082
|
+
proc = Bun2.spawn([opts.shell], {
|
|
41083
|
+
cwd: opts.cwd,
|
|
41084
|
+
env: opts.env,
|
|
41085
|
+
terminal: {
|
|
41086
|
+
cols: opts.cols,
|
|
41087
|
+
rows: opts.rows,
|
|
41088
|
+
data(_terminal, data) {
|
|
41089
|
+
let str;
|
|
41090
|
+
if (typeof data === "string")
|
|
41091
|
+
str = data;
|
|
41092
|
+
else if (data instanceof Uint8Array)
|
|
41093
|
+
str = new TextDecoder("utf-8").decode(data);
|
|
41094
|
+
else if (data?.toString)
|
|
41095
|
+
str = data.toString();
|
|
41096
|
+
else
|
|
41097
|
+
str = "";
|
|
41098
|
+
if (str)
|
|
41099
|
+
emitData(session, str);
|
|
41100
|
+
},
|
|
41101
|
+
exit(_terminal, exitCode) {
|
|
41102
|
+
emitExit(session, opts.ip, { exitCode: exitCode ?? 0 });
|
|
41103
|
+
}
|
|
41104
|
+
}
|
|
41105
|
+
});
|
|
41106
|
+
} catch (err) {
|
|
41107
|
+
termLog.error({ err: err?.message || String(err) }, "spawn pty failed (bun)");
|
|
41108
|
+
return { ok: false, reason: "spawn-error", message: err?.message || "spawn pty failed" };
|
|
41109
|
+
}
|
|
41110
|
+
const terminal = proc?.terminal;
|
|
41111
|
+
if (!terminal) {
|
|
41112
|
+
try {
|
|
41113
|
+
proc?.kill?.("SIGTERM");
|
|
41114
|
+
} catch {}
|
|
41115
|
+
return { ok: false, reason: "spawn-error", message: "Bun.spawn 未返回 terminal 句柄(请升级到 Bun ≥ 1.3)" };
|
|
41116
|
+
}
|
|
41117
|
+
session.pid = proc.pid || 0;
|
|
41118
|
+
session._writeImpl = (data) => terminal.write(data);
|
|
41119
|
+
session._resizeImpl = (c, r) => {
|
|
41120
|
+
try {
|
|
41121
|
+
terminal.resize(c, r);
|
|
41122
|
+
} catch {}
|
|
41123
|
+
};
|
|
41124
|
+
session._killImpl = (sig) => {
|
|
41125
|
+
try {
|
|
41126
|
+
proc.kill?.(sig || "SIGTERM");
|
|
41127
|
+
} catch {}
|
|
41128
|
+
try {
|
|
41129
|
+
terminal.close?.();
|
|
41130
|
+
} catch {}
|
|
41131
|
+
};
|
|
41132
|
+
session.lifetimeTimer = setTimeout(() => {
|
|
41133
|
+
termLog.warn({ id: opts.id, pid: session.pid }, "pty session exceeded max lifetime, killing");
|
|
41134
|
+
try {
|
|
41135
|
+
proc.kill?.("SIGTERM");
|
|
41136
|
+
} catch {}
|
|
41137
|
+
}, TERMINAL_LIMITS.maxLifetimeMs);
|
|
41138
|
+
session.idleTimer = setTimeout(() => {
|
|
41139
|
+
termLog.warn({ id: opts.id, pid: session.pid }, "pty session idle timeout, killing");
|
|
41140
|
+
try {
|
|
41141
|
+
proc.kill?.("SIGTERM");
|
|
41142
|
+
} catch {}
|
|
41143
|
+
}, TERMINAL_LIMITS.idleTimeoutMs);
|
|
41144
|
+
attachToBookkeeping(session, opts.ip);
|
|
41145
|
+
if (proc.exited && typeof proc.exited.then === "function") {
|
|
41146
|
+
proc.exited.then((code) => {
|
|
41147
|
+
if (!sessions.has(session.id))
|
|
41148
|
+
return;
|
|
41149
|
+
emitExit(session, opts.ip, { exitCode: typeof code === "number" ? code : 0 });
|
|
41150
|
+
}).catch(() => {
|
|
41151
|
+
if (!sessions.has(session.id))
|
|
41152
|
+
return;
|
|
41153
|
+
emitExit(session, opts.ip, { exitCode: 1 });
|
|
41154
|
+
});
|
|
41155
|
+
}
|
|
41156
|
+
termLog.info({ id: opts.id, pid: session.pid, cwd: opts.cwd, shell: opts.shell, ip: opts.ip, driver: "bun" }, "pty session started");
|
|
41157
|
+
return { ok: true, handle: session };
|
|
41158
|
+
}
|
|
41159
|
+
function shutdownAllSessions(reason = "shutdown") {
|
|
41160
|
+
for (const s of sessions.values()) {
|
|
41161
|
+
try {
|
|
41162
|
+
s.kill("SIGTERM");
|
|
41163
|
+
} catch {}
|
|
41164
|
+
}
|
|
41165
|
+
termLog.info({ count: sessions.size, reason }, "killed all pty sessions");
|
|
41166
|
+
}
|
|
41167
|
+
|
|
41168
|
+
// src/routes/terminal.ts
|
|
41169
|
+
import os9 from "node:os";
|
|
41170
|
+
var termLog2 = moduleLogger("terminal-route");
|
|
41171
|
+
var TERMINAL_ALLOWLIST_KEY = "terminal.ipAllowlist";
|
|
41172
|
+
var TERMINAL_SUBPROTOCOL = "kite-admin-token";
|
|
41173
|
+
async function loadTerminalAllowlist() {
|
|
41174
|
+
try {
|
|
41175
|
+
const raw = await db.settings.get(TERMINAL_ALLOWLIST_KEY);
|
|
41176
|
+
if (!raw)
|
|
41177
|
+
return [];
|
|
41178
|
+
const v = JSON.parse(raw);
|
|
41179
|
+
if (Array.isArray(v))
|
|
41180
|
+
return v.map((x) => String(x)).filter(Boolean);
|
|
41181
|
+
return [];
|
|
41182
|
+
} catch {
|
|
41183
|
+
return [];
|
|
41184
|
+
}
|
|
41185
|
+
}
|
|
41186
|
+
async function saveTerminalAllowlist(entries) {
|
|
41187
|
+
await db.settings.set(TERMINAL_ALLOWLIST_KEY, JSON.stringify(entries));
|
|
41188
|
+
}
|
|
41189
|
+
var terminalRoutes = new Elysia().get("/api/terminal/whoami", ({ request, headers, set: set3 }) => {
|
|
41190
|
+
if (!verifyAdminToken(headers)) {
|
|
41191
|
+
set3.status = 401;
|
|
41192
|
+
return { error: "Unauthorized" };
|
|
41193
|
+
}
|
|
41194
|
+
const sockRemote = request?.socket?.remoteAddress || headers["x-kite-socket-ip"] || null;
|
|
41195
|
+
const info = resolveClientIp({
|
|
41196
|
+
socketRemoteAddress: sockRemote || undefined,
|
|
41197
|
+
headers
|
|
41198
|
+
});
|
|
41199
|
+
return {
|
|
41200
|
+
socketIp: info.socketIp || null,
|
|
41201
|
+
forwardedIp: info.forwardedIp,
|
|
41202
|
+
trustedIp: info.trusted || null
|
|
41203
|
+
};
|
|
41204
|
+
}).get("/api/terminal/info", async ({ headers, set: set3 }) => {
|
|
41205
|
+
if (!verifyAdminToken(headers)) {
|
|
41206
|
+
set3.status = 401;
|
|
41207
|
+
return { error: "Unauthorized" };
|
|
41208
|
+
}
|
|
41209
|
+
const platformOk = isPlatformSupported();
|
|
41210
|
+
const load = platformOk ? await loadPty() : { available: false, error: `当前平台不支持终端能力:${process.platform}` };
|
|
41211
|
+
const allowlist = await loadTerminalAllowlist();
|
|
41212
|
+
const counts = listSessionsCounts();
|
|
41213
|
+
return {
|
|
41214
|
+
available: load.available,
|
|
41215
|
+
reason: load.available ? null : load.error || "unknown",
|
|
41216
|
+
shell: defaultShell(),
|
|
41217
|
+
platform: process.platform,
|
|
41218
|
+
defaultCwd: os9.homedir(),
|
|
41219
|
+
limits: TERMINAL_LIMITS,
|
|
41220
|
+
sessions: counts,
|
|
41221
|
+
allowlist,
|
|
41222
|
+
allowlistEnabled: allowlist.length > 0
|
|
41223
|
+
};
|
|
41224
|
+
}).get("/api/terminal/allowlist", async ({ headers, set: set3 }) => {
|
|
41225
|
+
if (!verifyAdminToken(headers)) {
|
|
41226
|
+
set3.status = 401;
|
|
41227
|
+
return { error: "Unauthorized" };
|
|
41228
|
+
}
|
|
41229
|
+
const allowlist = await loadTerminalAllowlist();
|
|
41230
|
+
const { invalid } = parseAllowlist(allowlist);
|
|
41231
|
+
return { entries: allowlist, invalid };
|
|
41232
|
+
}).put("/api/terminal/allowlist", async ({ headers, body, set: set3 }) => {
|
|
41233
|
+
if (!verifyAdminToken(headers)) {
|
|
41234
|
+
set3.status = 401;
|
|
41235
|
+
return { error: "Unauthorized" };
|
|
41236
|
+
}
|
|
41237
|
+
const input = body?.entries;
|
|
41238
|
+
if (!Array.isArray(input)) {
|
|
41239
|
+
set3.status = 400;
|
|
41240
|
+
return { error: "entries 必须是字符串数组" };
|
|
41241
|
+
}
|
|
41242
|
+
const normalized = [];
|
|
41243
|
+
const invalid = [];
|
|
41244
|
+
for (const raw of input) {
|
|
41245
|
+
if (typeof raw !== "string")
|
|
41246
|
+
continue;
|
|
41247
|
+
const trimmed = raw.trim();
|
|
41248
|
+
if (!trimmed)
|
|
41249
|
+
continue;
|
|
41250
|
+
const { parsed } = parseAllowlist([trimmed]);
|
|
41251
|
+
if (parsed.length === 1)
|
|
41252
|
+
normalized.push(trimmed);
|
|
41253
|
+
else
|
|
41254
|
+
invalid.push(trimmed);
|
|
41255
|
+
}
|
|
41256
|
+
if (invalid.length > 0) {
|
|
41257
|
+
set3.status = 400;
|
|
41258
|
+
return { error: `存在无效的 IP / CIDR:${invalid.join(", ")}` };
|
|
41259
|
+
}
|
|
41260
|
+
const before = await loadTerminalAllowlist();
|
|
41261
|
+
await saveTerminalAllowlist(normalized);
|
|
41262
|
+
await writeAudit({ headers }, {
|
|
41263
|
+
action: "terminal.allowlist.update",
|
|
41264
|
+
targetType: "settings",
|
|
41265
|
+
before: { entries: before },
|
|
41266
|
+
after: { entries: normalized },
|
|
41267
|
+
summary: `更新终端 IP 白名单(${normalized.length} 条)`
|
|
41268
|
+
});
|
|
41269
|
+
return { success: true, entries: normalized };
|
|
41270
|
+
});
|
|
41271
|
+
function parseSubprotocols(headerValue) {
|
|
41272
|
+
if (!headerValue)
|
|
41273
|
+
return [];
|
|
41274
|
+
const raw = Array.isArray(headerValue) ? headerValue.join(",") : headerValue;
|
|
41275
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
41276
|
+
}
|
|
41277
|
+
async function decideTerminalUpgrade(ctx) {
|
|
41278
|
+
if (!ctx.url.pathname.startsWith("/api/terminal/ws")) {
|
|
41279
|
+
return { ok: false, status: 404, reason: "not-terminal-route" };
|
|
41280
|
+
}
|
|
41281
|
+
const allowlist = await loadTerminalAllowlist();
|
|
41282
|
+
if (allowlist.length > 0 && ctx.origin && ctx.expectedOrigin) {
|
|
41283
|
+
try {
|
|
41284
|
+
const a = new URL(ctx.origin);
|
|
41285
|
+
const b = new URL(ctx.expectedOrigin);
|
|
41286
|
+
if (a.host !== b.host) {
|
|
41287
|
+
const isLocalDev = /(^|:)(localhost|127\.0\.0\.1)(:|$)/.test(a.host) && /(^|:)(localhost|127\.0\.0\.1)(:|$)/.test(b.host);
|
|
41288
|
+
if (!isLocalDev)
|
|
41289
|
+
return { ok: false, status: 403, reason: "origin-mismatch" };
|
|
41290
|
+
}
|
|
41291
|
+
} catch {
|
|
41292
|
+
return { ok: false, status: 400, reason: "invalid-origin" };
|
|
41293
|
+
}
|
|
41294
|
+
}
|
|
41295
|
+
const subs = parseSubprotocols(ctx.subprotocols.length ? ctx.subprotocols.join(", ") : ctx.headers["sec-websocket-protocol"]);
|
|
41296
|
+
const protoIdx = subs.findIndex((s) => s === TERMINAL_SUBPROTOCOL);
|
|
41297
|
+
if (protoIdx < 0 || protoIdx + 1 >= subs.length) {
|
|
41298
|
+
return { ok: false, status: 401, reason: "missing-token-protocol" };
|
|
41299
|
+
}
|
|
41300
|
+
const token = subs[protoIdx + 1];
|
|
41301
|
+
const ipInfo = resolveClientIp({
|
|
41302
|
+
socketRemoteAddress: ctx.socketRemoteAddress || undefined,
|
|
41303
|
+
headers: ctx.headers
|
|
41304
|
+
});
|
|
41305
|
+
const ip = ipInfo.socketIp || ipInfo.forwardedIp || "unknown";
|
|
41306
|
+
const guard4 = await loginGuard(ip);
|
|
41307
|
+
if (guard4.locked) {
|
|
41308
|
+
return { ok: false, status: 429, reason: "rate-limited" };
|
|
41309
|
+
}
|
|
41310
|
+
const tokenValid = verifyAdminTokenValue(token) && safeEqual(token, process.env.ADMIN_TOKEN || "");
|
|
41311
|
+
if (!tokenValid) {
|
|
41312
|
+
loginFailure(ip);
|
|
41313
|
+
return { ok: false, status: 401, reason: "invalid-token" };
|
|
41314
|
+
}
|
|
41315
|
+
loginSuccess(ip);
|
|
41316
|
+
if (allowlist.length > 0 && !ipAllowed(ip, allowlist)) {
|
|
41317
|
+
await writeAudit({ headers: ctx.headers }, {
|
|
41318
|
+
action: "terminal.denied",
|
|
41319
|
+
targetType: "terminal",
|
|
41320
|
+
summary: `终端访问被 IP 白名单拒绝:${ip}`,
|
|
41321
|
+
status: "failed",
|
|
41322
|
+
errorMessage: `IP ${ip} 不在白名单中`
|
|
41323
|
+
});
|
|
41324
|
+
termLog2.warn({ ip }, "terminal connection rejected by allowlist");
|
|
41325
|
+
return { ok: false, status: 403, reason: "ip-not-allowed" };
|
|
41326
|
+
}
|
|
41327
|
+
const projectId = ctx.url.searchParams.get("projectId");
|
|
41328
|
+
let cwd = os9.homedir();
|
|
41329
|
+
if (projectId) {
|
|
41330
|
+
const proj = await db.projects.findById(projectId);
|
|
41331
|
+
if (proj && proj.deployPath) {
|
|
41332
|
+
cwd = resolveCwd(proj.deployPath);
|
|
41333
|
+
}
|
|
41334
|
+
} else {
|
|
41335
|
+
const cwdParam = ctx.url.searchParams.get("cwd");
|
|
41336
|
+
if (cwdParam)
|
|
41337
|
+
cwd = resolveCwd(cwdParam);
|
|
41338
|
+
}
|
|
41339
|
+
const cols = Number(ctx.url.searchParams.get("cols")) || 80;
|
|
41340
|
+
const rows = Number(ctx.url.searchParams.get("rows")) || 24;
|
|
41341
|
+
return {
|
|
41342
|
+
ok: true,
|
|
41343
|
+
status: 101,
|
|
41344
|
+
selectedProtocol: TERMINAL_SUBPROTOCOL,
|
|
41345
|
+
token,
|
|
41346
|
+
ip,
|
|
41347
|
+
cwd,
|
|
41348
|
+
projectId: projectId || null,
|
|
41349
|
+
cols,
|
|
41350
|
+
rows
|
|
41351
|
+
};
|
|
41352
|
+
}
|
|
41353
|
+
var OPEN = 1;
|
|
41354
|
+
function safeJsonParse(value2) {
|
|
41355
|
+
try {
|
|
41356
|
+
return JSON.parse(value2);
|
|
41357
|
+
} catch {
|
|
41358
|
+
return null;
|
|
41359
|
+
}
|
|
41360
|
+
}
|
|
41361
|
+
async function attachTerminalSocket(params) {
|
|
41362
|
+
const startedAt = Date.now();
|
|
41363
|
+
let handle = null;
|
|
41364
|
+
let exitedCode = null;
|
|
41365
|
+
const sendJson = (obj) => {
|
|
41366
|
+
try {
|
|
41367
|
+
if (params.socket.readyState === OPEN)
|
|
41368
|
+
params.socket.send(JSON.stringify(obj));
|
|
41369
|
+
} catch {}
|
|
41370
|
+
};
|
|
41371
|
+
const spawnResult = await spawnTerminalSession({
|
|
41372
|
+
cwd: params.cwd,
|
|
41373
|
+
cols: params.cols,
|
|
41374
|
+
rows: params.rows,
|
|
41375
|
+
ip: params.ip
|
|
41376
|
+
});
|
|
41377
|
+
if (!spawnResult.ok || !spawnResult.handle) {
|
|
41378
|
+
sendJson({ type: "error", reason: spawnResult.reason, message: spawnResult.message });
|
|
41379
|
+
try {
|
|
41380
|
+
params.socket.close(4000, spawnResult.message || "spawn failed");
|
|
41381
|
+
} catch {}
|
|
41382
|
+
return;
|
|
41383
|
+
}
|
|
41384
|
+
handle = spawnResult.handle;
|
|
41385
|
+
handle.onData((chunk) => {
|
|
41386
|
+
if (params.socket.readyState !== OPEN)
|
|
41387
|
+
return;
|
|
41388
|
+
try {
|
|
41389
|
+
params.socket.send(JSON.stringify({ type: "data", data: chunk }));
|
|
41390
|
+
} catch {}
|
|
41391
|
+
});
|
|
41392
|
+
handle.onExit((info) => {
|
|
41393
|
+
exitedCode = info.exitCode ?? 0;
|
|
41394
|
+
sendJson({ type: "exit", exitCode: info.exitCode, signal: info.signal ?? null });
|
|
41395
|
+
try {
|
|
41396
|
+
params.socket.close(1000, "pty exited");
|
|
41397
|
+
} catch {}
|
|
41398
|
+
});
|
|
41399
|
+
sendJson({
|
|
41400
|
+
type: "ready",
|
|
41401
|
+
sessionId: handle.id,
|
|
41402
|
+
pid: handle.pid,
|
|
41403
|
+
shell: handle.shell,
|
|
41404
|
+
cwd: handle.cwd,
|
|
41405
|
+
projectId: params.projectId
|
|
41406
|
+
});
|
|
41407
|
+
writeAudit({ headers: params.headers }, {
|
|
41408
|
+
action: "terminal.open",
|
|
41409
|
+
targetType: "terminal",
|
|
41410
|
+
targetId: handle.id,
|
|
41411
|
+
targetName: params.projectId ? `project:${params.projectId}` : "global",
|
|
41412
|
+
summary: `打开终端 cwd=${handle.cwd} pid=${handle.pid}`
|
|
41413
|
+
});
|
|
41414
|
+
params.socket.on("message", (data, isBinary) => {
|
|
41415
|
+
if (!handle)
|
|
41416
|
+
return;
|
|
41417
|
+
let raw;
|
|
41418
|
+
if (typeof data === "string")
|
|
41419
|
+
raw = data;
|
|
41420
|
+
else if (data instanceof Buffer)
|
|
41421
|
+
raw = data.toString("utf8");
|
|
41422
|
+
else if (data?.toString)
|
|
41423
|
+
raw = data.toString();
|
|
41424
|
+
else
|
|
41425
|
+
raw = "";
|
|
41426
|
+
if (isBinary === false || typeof data === "string") {
|
|
41427
|
+
const msg = safeJsonParse(raw);
|
|
41428
|
+
if (!msg || typeof msg !== "object")
|
|
41429
|
+
return;
|
|
41430
|
+
if (msg.type === "input" && typeof msg.data === "string") {
|
|
41431
|
+
handle.write(msg.data);
|
|
41432
|
+
} else if (msg.type === "resize") {
|
|
41433
|
+
const c = Number(msg.cols) || 80;
|
|
41434
|
+
const r = Number(msg.rows) || 24;
|
|
41435
|
+
handle.resize(c, r);
|
|
41436
|
+
} else if (msg.type === "ping") {
|
|
41437
|
+
sendJson({ type: "pong", ts: Date.now() });
|
|
41438
|
+
}
|
|
41439
|
+
}
|
|
41440
|
+
});
|
|
41441
|
+
const cleanup = async () => {
|
|
41442
|
+
if (handle) {
|
|
41443
|
+
const id = handle.id;
|
|
41444
|
+
const pid = handle.pid;
|
|
41445
|
+
try {
|
|
41446
|
+
handle.kill("SIGHUP");
|
|
41447
|
+
} catch {}
|
|
41448
|
+
handle = null;
|
|
41449
|
+
await writeAudit({ headers: params.headers }, {
|
|
41450
|
+
action: "terminal.close",
|
|
41451
|
+
targetType: "terminal",
|
|
41452
|
+
targetId: id,
|
|
41453
|
+
summary: `关闭终端 pid=${pid} durationMs=${Date.now() - startedAt} exitCode=${exitedCode ?? "n/a"}`
|
|
41454
|
+
});
|
|
41455
|
+
}
|
|
41456
|
+
};
|
|
41457
|
+
params.socket.on("close", () => {
|
|
41458
|
+
cleanup();
|
|
41459
|
+
});
|
|
41460
|
+
params.socket.on("error", (err) => {
|
|
41461
|
+
termLog2.warn({ err: err?.message }, "terminal socket error");
|
|
41462
|
+
cleanup();
|
|
41463
|
+
});
|
|
41464
|
+
}
|
|
41465
|
+
|
|
41466
|
+
// src/static.ts
|
|
41467
|
+
init_dist3();
|
|
41468
|
+
import path16 from "node:path";
|
|
41469
|
+
var MIME_TYPES = {
|
|
41470
|
+
".html": "text/html; charset=utf-8",
|
|
41471
|
+
".css": "text/css; charset=utf-8",
|
|
41472
|
+
".js": "application/javascript; charset=utf-8",
|
|
41473
|
+
".json": "application/json; charset=utf-8",
|
|
41474
|
+
".svg": "image/svg+xml",
|
|
41475
|
+
".png": "image/png",
|
|
41476
|
+
".jpg": "image/jpeg",
|
|
41477
|
+
".jpeg": "image/jpeg",
|
|
41478
|
+
".gif": "image/gif",
|
|
41479
|
+
".ico": "image/x-icon",
|
|
41480
|
+
".woff": "font/woff",
|
|
41481
|
+
".woff2": "font/woff2",
|
|
41482
|
+
".ttf": "font/ttf",
|
|
41483
|
+
".eot": "application/vnd.ms-fontobject",
|
|
41484
|
+
".map": "application/json",
|
|
41485
|
+
".txt": "text/plain; charset=utf-8",
|
|
41486
|
+
".xml": "application/xml"
|
|
41487
|
+
};
|
|
41488
|
+
function getMimeType(filePath) {
|
|
41489
|
+
const ext2 = path16.extname(filePath).toLowerCase();
|
|
41490
|
+
return MIME_TYPES[ext2] || "application/octet-stream";
|
|
41491
|
+
}
|
|
41492
|
+
var staticPlugin = new Elysia().get("/*", async ({ request, set: set3 }) => {
|
|
41493
|
+
const webDir = process.env.KITE_WEB_DIR;
|
|
41494
|
+
const url = new URL(request.url);
|
|
41495
|
+
let pathname = url.pathname;
|
|
41496
|
+
if (!webDir) {
|
|
41497
|
+
if (pathname === "/")
|
|
41498
|
+
return "Deploy Server is running!";
|
|
41499
|
+
set3.status = 404;
|
|
41500
|
+
return { error: "Web console not available" };
|
|
41501
|
+
}
|
|
41502
|
+
const filePath = path16.resolve(webDir, "." + pathname);
|
|
41503
|
+
if (!filePath.startsWith(path16.resolve(webDir))) {
|
|
41504
|
+
set3.status = 403;
|
|
41505
|
+
return { error: "Access denied" };
|
|
41506
|
+
}
|
|
41507
|
+
const { stat: stat2, access } = await import("node:fs/promises");
|
|
41508
|
+
try {
|
|
41509
|
+
const fileStat = await stat2(filePath);
|
|
41510
|
+
if (fileStat.isFile()) {
|
|
41511
|
+
const buffer = await readFileBuffer(filePath);
|
|
41512
|
+
set3.headers["Content-Type"] = getMimeType(filePath);
|
|
41513
|
+
set3.headers["Cache-Control"] = pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
|
|
41514
|
+
return new Response(buffer);
|
|
41515
|
+
}
|
|
38895
41516
|
} catch {}
|
|
38896
|
-
const indexPath =
|
|
41517
|
+
const indexPath = path16.resolve(webDir, "index.html");
|
|
38897
41518
|
try {
|
|
38898
41519
|
await access(indexPath);
|
|
38899
41520
|
const buffer = await readFileBuffer(indexPath);
|
|
@@ -38907,7 +41528,7 @@ var staticPlugin = new Elysia().get("/*", async ({ request, set: set3 }) => {
|
|
|
38907
41528
|
});
|
|
38908
41529
|
|
|
38909
41530
|
// src/index.ts
|
|
38910
|
-
import { randomUUID as
|
|
41531
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
38911
41532
|
import http from "node:http";
|
|
38912
41533
|
await ensureDbReady();
|
|
38913
41534
|
var port = Number(process.env.PORT) || 5430;
|
|
@@ -38921,8 +41542,60 @@ if (!isBun3) {
|
|
|
38921
41542
|
adapter = node2();
|
|
38922
41543
|
}
|
|
38923
41544
|
var httpLog = moduleLogger("http");
|
|
38924
|
-
var
|
|
38925
|
-
|
|
41545
|
+
var wsLog = moduleLogger("ws");
|
|
41546
|
+
async function buildTerminalWss() {
|
|
41547
|
+
try {
|
|
41548
|
+
const { WebSocketServer } = await import("ws");
|
|
41549
|
+
return new WebSocketServer({ noServer: true });
|
|
41550
|
+
} catch (err) {
|
|
41551
|
+
wsLog.warn({ err: err?.message }, "加载 ws 包失败,终端 WebSocket 将不可用");
|
|
41552
|
+
return null;
|
|
41553
|
+
}
|
|
41554
|
+
}
|
|
41555
|
+
var terminalWss = await buildTerminalWss();
|
|
41556
|
+
function headersFromIncoming(req) {
|
|
41557
|
+
return req.headers;
|
|
41558
|
+
}
|
|
41559
|
+
async function handleTerminalUpgrade(req, socket, head, expectedOrigin) {
|
|
41560
|
+
if (!terminalWss) {
|
|
41561
|
+
socket.destroy();
|
|
41562
|
+
return;
|
|
41563
|
+
}
|
|
41564
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || expectedOrigin}`);
|
|
41565
|
+
const subs = req.headers["sec-websocket-protocol"]?.split(",").map((s) => s.trim()).filter(Boolean) || [];
|
|
41566
|
+
const decision = await decideTerminalUpgrade({
|
|
41567
|
+
url,
|
|
41568
|
+
headers: headersFromIncoming(req),
|
|
41569
|
+
socketRemoteAddress: req.socket?.remoteAddress || null,
|
|
41570
|
+
origin: req.headers.origin || null,
|
|
41571
|
+
expectedOrigin,
|
|
41572
|
+
subprotocols: subs
|
|
41573
|
+
});
|
|
41574
|
+
if (!decision.ok) {
|
|
41575
|
+
const status2 = decision.status || 400;
|
|
41576
|
+
const reason = decision.reason || "rejected";
|
|
41577
|
+
wsLog.warn({ status: status2, reason, path: url.pathname }, "terminal upgrade rejected");
|
|
41578
|
+
socket.write(`HTTP/1.1 ${status2} ${reason}\r
|
|
41579
|
+
Content-Length: 0\r
|
|
41580
|
+
\r
|
|
41581
|
+
`);
|
|
41582
|
+
socket.destroy();
|
|
41583
|
+
return;
|
|
41584
|
+
}
|
|
41585
|
+
terminalWss.handleUpgrade(req, socket, head, (ws) => {
|
|
41586
|
+
attachTerminalSocket({
|
|
41587
|
+
socket: ws,
|
|
41588
|
+
ip: decision.ip,
|
|
41589
|
+
cwd: decision.cwd,
|
|
41590
|
+
projectId: decision.projectId ?? null,
|
|
41591
|
+
cols: decision.cols,
|
|
41592
|
+
rows: decision.rows,
|
|
41593
|
+
headers: headersFromIncoming(req)
|
|
41594
|
+
});
|
|
41595
|
+
});
|
|
41596
|
+
}
|
|
41597
|
+
var app = new Elysia({ adapter }).derive({ as: "global" }, ({ request: _request, headers, set: set3 }) => {
|
|
41598
|
+
const traceId = pickTraceId(headers) || randomUUID4();
|
|
38926
41599
|
set3.headers = { ...set3.headers || {}, "x-kite-trace-id": traceId };
|
|
38927
41600
|
return { _start: performance.now(), traceId };
|
|
38928
41601
|
}).onAfterHandle({ as: "global" }, ({ request, set: set3, _start, traceId }) => {
|
|
@@ -38931,54 +41604,71 @@ var app = new Elysia({ adapter }).derive({ as: "global" }, ({ request, headers,
|
|
|
38931
41604
|
httpLog.info({ traceId, method: request.method, path: new URL(request.url).pathname, status: status2, ms }, `${request.method} ${new URL(request.url).pathname} ${status2} ${ms}ms`);
|
|
38932
41605
|
}).onError({ as: "global" }, ({ request, error: error3, traceId }) => {
|
|
38933
41606
|
httpLog.error({ traceId, method: request?.method, path: request ? new URL(request.url).pathname : undefined, err: { name: error3?.name, message: error3?.message, stack: error3?.stack } }, "request error");
|
|
38934
|
-
}).use(deployRoutes).use(settingsRoutes).use(migrationRoutes).use(auditRoutes).use(healthRoutes).use(diskRoutes).use(statsRoutes).use(fsRoutes).use(categoryRoutes).use(staticPlugin);
|
|
38935
|
-
|
|
38936
|
-
|
|
38937
|
-
|
|
38938
|
-
|
|
38939
|
-
const
|
|
38940
|
-
|
|
38941
|
-
|
|
38942
|
-
const headers = new Headers;
|
|
38943
|
-
for (const [key, value2] of Object.entries(req.headers)) {
|
|
38944
|
-
if (value2) {
|
|
38945
|
-
headers.set(key, Array.isArray(value2) ? value2.join(", ") : value2);
|
|
38946
|
-
}
|
|
41607
|
+
}).use(deployRoutes).use(settingsRoutes).use(migrationRoutes).use(auditRoutes).use(healthRoutes).use(diskRoutes).use(statsRoutes).use(fsRoutes).use(categoryRoutes).use(logSourceRoutes).use(systemRoutes).use(pm2Routes).use(tagRoutes).use(terminalRoutes).use(staticPlugin);
|
|
41608
|
+
var fetchHandler = app.fetch;
|
|
41609
|
+
var server = http.createServer(async (req, res) => {
|
|
41610
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
41611
|
+
const headers = new Headers;
|
|
41612
|
+
for (const [key, value2] of Object.entries(req.headers)) {
|
|
41613
|
+
if (value2) {
|
|
41614
|
+
headers.set(key, Array.isArray(value2) ? value2.join(", ") : value2);
|
|
38947
41615
|
}
|
|
38948
|
-
|
|
38949
|
-
|
|
38950
|
-
|
|
38951
|
-
|
|
38952
|
-
|
|
38953
|
-
|
|
38954
|
-
|
|
38955
|
-
|
|
38956
|
-
|
|
38957
|
-
|
|
38958
|
-
}
|
|
38959
|
-
|
|
38960
|
-
|
|
38961
|
-
|
|
38962
|
-
|
|
38963
|
-
|
|
38964
|
-
|
|
38965
|
-
|
|
38966
|
-
|
|
38967
|
-
|
|
38968
|
-
|
|
38969
|
-
|
|
38970
|
-
|
|
38971
|
-
|
|
38972
|
-
|
|
38973
|
-
};
|
|
41616
|
+
}
|
|
41617
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
41618
|
+
const request = new Request(url.toString(), {
|
|
41619
|
+
method: req.method,
|
|
41620
|
+
headers,
|
|
41621
|
+
body: hasBody ? new ReadableStream({
|
|
41622
|
+
start(controller) {
|
|
41623
|
+
req.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
|
|
41624
|
+
req.on("end", () => controller.close());
|
|
41625
|
+
req.on("error", (err) => controller.error(err));
|
|
41626
|
+
}
|
|
41627
|
+
}) : undefined,
|
|
41628
|
+
duplex: hasBody ? "half" : undefined
|
|
41629
|
+
});
|
|
41630
|
+
const response = await fetchHandler(request);
|
|
41631
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
41632
|
+
if (response.body) {
|
|
41633
|
+
const reader = response.body.getReader();
|
|
41634
|
+
const pump = async () => {
|
|
41635
|
+
const { done, value: value2 } = await reader.read();
|
|
41636
|
+
if (done) {
|
|
41637
|
+
res.end();
|
|
41638
|
+
return;
|
|
41639
|
+
}
|
|
41640
|
+
res.write(value2);
|
|
38974
41641
|
await pump();
|
|
38975
|
-
}
|
|
38976
|
-
|
|
41642
|
+
};
|
|
41643
|
+
await pump();
|
|
41644
|
+
} else {
|
|
41645
|
+
res.end();
|
|
41646
|
+
}
|
|
41647
|
+
});
|
|
41648
|
+
if (terminalWss) {
|
|
41649
|
+
server.on("upgrade", (req, socket, head) => {
|
|
41650
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
|
|
41651
|
+
if (!url.pathname.startsWith("/api/terminal/ws")) {
|
|
41652
|
+
socket.destroy();
|
|
41653
|
+
return;
|
|
38977
41654
|
}
|
|
41655
|
+
const reqHost = req.headers.host || `${host}:${port}`;
|
|
41656
|
+
const expectedOrigin = `http://${reqHost}`;
|
|
41657
|
+
handleTerminalUpgrade(req, socket, head, expectedOrigin);
|
|
38978
41658
|
});
|
|
38979
|
-
|
|
38980
|
-
|
|
38981
|
-
});
|
|
41659
|
+
} else {
|
|
41660
|
+
rootLogger.warn({ module: "terminal" }, "ws 包不可用,终端 WebSocket 已关闭");
|
|
38982
41661
|
}
|
|
41662
|
+
server.listen(port, host, () => {
|
|
41663
|
+
rootLogger.info({ module: "boot", runtime: runtimeName, version: serverVersion2, host, port }, `Kite server listening on http://${host}:${port}`);
|
|
41664
|
+
});
|
|
38983
41665
|
var adminTokenPreview = process.env.ADMIN_TOKEN ? `${process.env.ADMIN_TOKEN.slice(0, 4)}****${process.env.ADMIN_TOKEN.slice(-4)}` : "未设置 (请通过 .env.local 配置)";
|
|
38984
41666
|
rootLogger.info({ module: "boot" }, `Admin token: ${adminTokenPreview}`);
|
|
41667
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
41668
|
+
process.on(sig, () => {
|
|
41669
|
+
try {
|
|
41670
|
+
shutdownAllSessions(sig);
|
|
41671
|
+
} catch {}
|
|
41672
|
+
process.exit(0);
|
|
41673
|
+
});
|
|
41674
|
+
}
|