-
Notifications
You must be signed in to change notification settings - Fork 363
Expand file tree
/
Copy pathapp.ts
More file actions
217 lines (200 loc) · 8.12 KB
/
Copy pathapp.ts
File metadata and controls
217 lines (200 loc) · 8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import "dotenv/config";
import os from "node:os";
import cors from "cors";
import express from "express";
import helmet from "helmet";
import morgan from "morgan";
import type { ApiResponse } from "@ai-novel/shared/types/api";
import { errorHandler } from "./middleware/errorHandler";
import { loadProviderApiKeys } from "./llm/factory";
import astrologyRouter from "./routes/astrology";
import agentCatalogRouter from "./routes/agentCatalog";
import agentRunsRouter from "./routes/agentRuns";
import bookAnalysisRouter from "./routes/bookAnalysis";
import characterRouter from "./routes/character";
import chatRouter from "./routes/chat";
import creativeHubRouter from "./routes/creativeHub";
import genreRouter from "./routes/genre";
import healthRouter from "./routes/health";
import imagesRouter from "./routes/images";
import knowledgeRouter from "./routes/knowledge";
import llmRouter from "./routes/llm";
import novelRouter from "./routes/novel";
import novelDirectorRouter from "./routes/novelDirector";
import novelDecisionsRouter from "./routes/novelDecisions";
import novelChapterSummaryRouter from "./routes/novelChapterSummary";
import novelExportRouter from "./routes/novelExport";
import novelWorkflowsRouter from "./routes/novelWorkflows";
import ragRouter from "./routes/rag";
import settingsRouter from "./routes/settings";
import styleEngineRouter from "./routes/styleEngine";
import styleEngineExtractionRouter from "./routes/styleEngineExtraction";
import storyModeRouter from "./routes/storyMode";
import tasksRouter from "./routes/tasks";
import titleLibraryRouter from "./routes/titleLibrary";
import worldRouter from "./routes/world";
import writingFormulaRouter from "./routes/writingFormula";
import { novelEventBus, registerNovelEventHandlers } from "./events";
import { bookAnalysisService } from "./services/bookAnalysis/BookAnalysisService";
import { ragServices } from "./services/rag";
import { NovelPipelineRuntimeService } from "./services/novel/NovelPipelineRuntimeService";
import { recoveryTaskService } from "./services/task/RecoveryTaskService";
import {
ensureSystemResourceStarterData,
hasSystemResourceBootstrapChanges,
} from "./services/bootstrap/SystemResourceBootstrapService";
import { initializeRagSettingsCompatibility } from "./services/settings/RagCompatibilityBootstrapService";
registerNovelEventHandlers(novelEventBus);
const novelPipelineRuntimeService = new NovelPipelineRuntimeService();
morgan.token("error-message", (_req, res) => {
const response = res as typeof res & {
locals?: {
requestErrorMessage?: unknown;
};
};
const errorMessage = response.locals?.requestErrorMessage;
return typeof errorMessage === "string" ? errorMessage.trim() : "";
});
function parseEnvFlag(value: string | undefined, defaultValue: boolean): boolean {
if (value === undefined) return defaultValue;
return value === "true" || value === "1";
}
export function createApp() {
const app = express();
const jsonBodyLimit = process.env.API_JSON_LIMIT ?? "20mb";
const corsOriginEnv = process.env.CORS_ORIGIN;
const corsAllowList = corsOriginEnv
? corsOriginEnv
.split(",")
.map((item) => item.trim())
.filter(Boolean)
: [];
const allowLan = parseEnvFlag(process.env.ALLOW_LAN, process.env.NODE_ENV !== "production");
app.use(
cors({
origin: (origin, callback) => {
if (!origin) {
callback(null, true);
return;
}
const isListedOrigin = corsAllowList.includes(origin);
const isLocalhostDevOrigin = /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin);
const isLanOrigin = allowLan && /^https?:\/\/(?:\d{1,3}\.){3}\d{1,3}:\d+$/.test(origin);
callback(null, isListedOrigin || isLocalhostDevOrigin || isLanOrigin);
},
credentials: true,
}),
);
app.use(helmet());
app.use(morgan((tokens, req, res) => {
const method = tokens.method(req, res) ?? "-";
const url = tokens.url(req, res) ?? "-";
const status = tokens.status(req, res) ?? "-";
const responseTime = tokens["response-time"](req, res) ?? "0";
const contentLength = tokens.res(req, res, "content-length") ?? "0";
const errorMessage = tokens["error-message"](req, res);
const errorSuffix = errorMessage ? ` | error: ${errorMessage}` : "";
return `${method} ${url} ${status} ${responseTime} ms - ${contentLength}${errorSuffix}`;
}));
app.use(express.json({ limit: jsonBodyLimit }));
app.use("/api/health", healthRouter);
app.use("/api/agent-catalog", agentCatalogRouter);
app.use("/api/agent-runs", agentRunsRouter);
app.use("/api/book-analysis", bookAnalysisRouter);
app.use("/api/genres", genreRouter);
app.use("/api/story-modes", storyModeRouter);
app.use("/api/knowledge", knowledgeRouter);
app.use("/api/llm", llmRouter);
app.use("/api/title-library", titleLibraryRouter);
app.use("/api", styleEngineRouter);
app.use("/api", styleEngineExtractionRouter);
app.use("/api/novels", novelRouter);
app.use("/api/novels/director", novelDirectorRouter);
app.use("/api/novel-workflows", novelWorkflowsRouter);
app.use("/api/novels", novelDecisionsRouter);
app.use("/api/novels", novelChapterSummaryRouter);
app.use("/api/novels", novelExportRouter);
app.use("/api/worlds", worldRouter);
app.use("/api/rag", ragRouter);
app.use("/api/base-characters", characterRouter);
app.use("/api/writing-formula", writingFormulaRouter);
app.use("/api/chat", chatRouter);
app.use("/api/creative-hub", creativeHubRouter);
app.use("/api/images", imagesRouter);
app.use("/api/tasks", tasksRouter);
app.use("/api/settings", settingsRouter);
app.use("/api/astrology", astrologyRouter);
app.use((_req, res) => {
const response: ApiResponse<null> = {
success: false,
error: "接口不存在。",
};
res.status(404).json(response);
});
app.use(errorHandler);
return app;
}
function getLanIp(): string | null {
const ifaces = os.networkInterfaces();
for (const list of Object.values(ifaces)) {
if (!list) continue;
for (const info of list) {
if (info.family === "IPv4" && !info.internal) {
return info.address;
}
}
}
return null;
}
async function bootstrap(): Promise<void> {
const ragCompatibilityReport = await initializeRagSettingsCompatibility();
const app = createApp();
const port = Number(process.env.PORT ?? 3001);
const allowLan = parseEnvFlag(process.env.ALLOW_LAN, process.env.NODE_ENV !== "production");
const host = process.env.HOST ?? (allowLan ? "0.0.0.0" : "localhost");
ragServices.ragWorker.start();
const recoveryInitialization = recoveryTaskService.initializePendingRecoveries();
app.listen(port, host, () => {
console.log(`[server] listening on http://localhost:${port}`);
if (host === "0.0.0.0" || host === "::") {
const lanIp = getLanIp();
if (lanIp) {
console.log(`[server] LAN: http://${lanIp}:${port}`);
}
}
void loadProviderApiKeys().catch((error) => {
console.warn("数据库中的模型密钥加载失败,已回退到环境变量。", error);
});
if (
ragCompatibilityReport.importedSettingKeys.length > 0
|| ragCompatibilityReport.importedProviderRecords.length > 0
) {
console.log("[server] imported legacy RAG env settings.", ragCompatibilityReport);
}
void ensureSystemResourceStarterData()
.then((systemResourceReport) => {
if (hasSystemResourceBootstrapChanges(systemResourceReport)) {
console.log("[server] built-in creative resources bootstrapped.", systemResourceReport);
}
})
.catch((error) => {
console.warn("Failed to bootstrap built-in creative resources.", error);
});
void recoveryInitialization
.then(() => {
bookAnalysisService.startWatchdog();
novelPipelineRuntimeService.startWatchdog();
})
.catch((error) => {
console.warn("Failed to prepare pending recovery candidates.", error);
bookAnalysisService.startWatchdog();
novelPipelineRuntimeService.startWatchdog();
});
});
}
if (require.main === module) {
void bootstrap().catch((error) => {
console.error("[server] bootstrap failed.", error);
process.exit(1);
});
}