feat: bootstrap personal stock agent

This commit is contained in:
2026-05-11 21:10:55 +08:00
commit c87f37974e
21 changed files with 5014 additions and 0 deletions

98
web/src/server/index.ts Normal file
View File

@@ -0,0 +1,98 @@
import "dotenv/config";
import cors from "cors";
import express from "express";
import { join } from "node:path";
import { generateDailyReport } from "../agent/report";
import { createProvider } from "../agent/provider";
import { WatchlistStore } from "../agent/watchlistStore";
import { loadIpadsConfig } from "./ipadsConfig";
const port = Number(process.env.PORT ?? 8787);
const dataDir = process.env.STOCK_AGENT_DATA_DIR ?? join(process.cwd(), "data");
const watchlistStore = new WatchlistStore(join(dataDir, "watchlist.json"));
const app = express();
let cachedReportUpdatedAt = 0;
let cachedReport: Awaited<ReturnType<typeof generateDailyReport>> | undefined;
app.use(cors());
app.use(express.json());
app.get("/api/health", async (_request, response) => {
const config = loadIpadsConfig();
response.json({
ok: true,
providerConfigured: config.apiKeyConfigured,
baseUrl: config.baseUrl,
wireApi: config.wireApi,
scheduler: "24h"
});
});
app.get("/api/watchlist", async (_request, response) => {
response.json(await watchlistStore.list());
});
app.post("/api/watchlist", async (request, response, next) => {
try {
response.status(201).json(await watchlistStore.add(String(request.body?.symbol ?? ""), request.body?.name));
cachedReport = undefined;
} catch (error) {
next(error);
}
});
app.delete("/api/watchlist/:symbol", async (request, response, next) => {
try {
await watchlistStore.remove(request.params.symbol);
cachedReport = undefined;
response.status(204).end();
} catch (error) {
next(error);
}
});
app.get("/api/report/daily", async (_request, response, next) => {
try {
response.json(await getDailyReport());
} catch (error) {
next(error);
}
});
app.post("/api/report/daily/refresh", async (_request, response, next) => {
try {
cachedReport = undefined;
response.json(await getDailyReport());
} catch (error) {
next(error);
}
});
app.use((error: unknown, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
const message = error instanceof Error ? error.message : "Unknown server error";
response.status(400).json({ error: message });
});
setInterval(() => {
void getDailyReport().catch((error) => {
console.error("daily report refresh failed", error);
});
}, 24 * 60 * 60 * 1000).unref();
app.listen(port, "127.0.0.1", () => {
console.log(`stock agent api listening at http://127.0.0.1:${port}`);
});
async function getDailyReport() {
const now = Date.now();
if (cachedReport && now - cachedReportUpdatedAt < 24 * 60 * 60 * 1000) {
return cachedReport;
}
const watchlist = await watchlistStore.list();
cachedReport = await generateDailyReport(watchlist, createProvider(loadIpadsConfig().apiKey), new Date());
cachedReportUpdatedAt = now;
return cachedReport;
}