hvip-mcp-server 0.5.0 → 0.6.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.
@@ -3723,7 +3723,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
3723
3723
  // src/utils/logger.ts
3724
3724
  var LEVEL_EMOJI = {
3725
3725
  [0 /* DEBUG */]: "\u{1F50D}",
3726
- [1 /* INFO */]: "",
3726
+ [1 /* INFO */]: "\u2139\uFE0F",
3727
3727
  [2 /* WARN */]: "\u26A0\uFE0F",
3728
3728
  [3 /* ERROR */]: "\u274C",
3729
3729
  [4 /* SILENT */]: ""
@@ -3778,7 +3778,11 @@ var AgentHub = class {
3778
3778
  rooms = /* @__PURE__ */ new Map();
3779
3779
  heartbeatTimer = null;
3780
3780
  version = "0.0.0";
3781
+ startTime = 0;
3781
3782
  port = 0;
3783
+ // public: updated after WS negotiate, read by hub for dashboard URL
3784
+ registryCount = 0;
3785
+ // set by hub-server after HubRegistry loads
3782
3786
  db = null;
3783
3787
  token = "";
3784
3788
  // PSK 鉴权令牌
@@ -3820,6 +3824,7 @@ var AgentHub = class {
3820
3824
  // ── 启动 ──
3821
3825
  start(port, host2 = "0.0.0.0", version = "0.0.0", token2 = "") {
3822
3826
  this.version = version;
3827
+ this.startTime = Date.now();
3823
3828
  this.token = token2;
3824
3829
  const startWss = (p) => {
3825
3830
  return new Promise((resolve) => {
@@ -4415,7 +4420,7 @@ var AgentHub = class {
4415
4420
  branch: t.branch
4416
4421
  }));
4417
4422
  const rooms = this.getRooms();
4418
- return { agents, tasks, rooms, agentCount: agents.length, taskCount: tasks.length };
4423
+ return { agents, tasks, rooms, agentCount: agents.length, taskCount: tasks.length, version: this.version, uptime: this.startTime ? Math.floor((Date.now() - this.startTime) / 1e3) : 0, registry: `${this.registryCount || 24} plugins` };
4419
4424
  }
4420
4425
  // ── 关闭 ──
4421
4426
  close() {
@@ -4986,9 +4991,11 @@ var HubCosts = class {
4986
4991
  const entries = all.filter((e) => e.timestamp.startsWith(slot));
4987
4992
  hourly.push({ hour: slot, calls: entries.length, cost: entries.reduce((s, e) => s + e.cost, 0) });
4988
4993
  }
4994
+ const budget = parseFloat(process.env.HUB_DAILY_BUDGET || "5.00");
4989
4995
  return {
4990
4996
  today: sum(todayEntries),
4991
4997
  total: sum(all),
4998
+ budget,
4992
4999
  byModel,
4993
5000
  byAgent,
4994
5001
  hourly
@@ -5096,8 +5103,8 @@ var CircuitBreaker = class {
5096
5103
  var circuitBreaker = new CircuitBreaker({ failureThreshold: 3, cooldownMs: 6e4 });
5097
5104
 
5098
5105
  // src/hub-server.ts
5099
- var import_node_fs2 = require("node:fs");
5100
- var import_node_path2 = require("node:path");
5106
+ var import_node_fs3 = require("node:fs");
5107
+ var import_node_path3 = require("node:path");
5101
5108
 
5102
5109
  // src/adapters/hub-templates.ts
5103
5110
  var TASK_TEMPLATES = [
@@ -5934,10 +5941,150 @@ NEXT: <\u540E\u7EED\u8DDF\u8FDB\u5EFA\u8BAE>
5934
5941
  }
5935
5942
  ];
5936
5943
 
5944
+ // src/adapters/api-credits.ts
5945
+ var import_node_sqlite = require("node:sqlite");
5946
+ var import_node_fs2 = require("node:fs");
5947
+ var import_node_path2 = require("node:path");
5948
+ var log5 = logger("Credits");
5949
+ var CREDIT_COST = {
5950
+ READ: 1,
5951
+ WRITE: 5,
5952
+ FUND_TRANSFER: 10,
5953
+ ADMIN: 20
5954
+ };
5955
+ var ApiCredits = class {
5956
+ db;
5957
+ cache = /* @__PURE__ */ new Map();
5958
+ cacheLoaded = false;
5959
+ constructor(dbPath2 = ".hub/api-credits.db") {
5960
+ const dir = (0, import_node_path2.dirname)(dbPath2);
5961
+ if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
5962
+ this.db = new import_node_sqlite.DatabaseSync(dbPath2);
5963
+ this.db.exec("PRAGMA journal_mode=WAL");
5964
+ this.init();
5965
+ }
5966
+ init() {
5967
+ this.db.exec(`
5968
+ CREATE TABLE IF NOT EXISTS api_keys (
5969
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5970
+ key TEXT UNIQUE NOT NULL,
5971
+ client_name TEXT NOT NULL,
5972
+ credits REAL NOT NULL DEFAULT 0,
5973
+ total_used INTEGER NOT NULL DEFAULT 0,
5974
+ enabled INTEGER NOT NULL DEFAULT 1,
5975
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5976
+ last_used_at TEXT
5977
+ );
5978
+ CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
5979
+ `);
5980
+ this.loadCache();
5981
+ }
5982
+ loadCache() {
5983
+ const rows = this.db.prepare("SELECT * FROM api_keys").all();
5984
+ for (const r of rows) this.cache.set(r.key, r);
5985
+ this.cacheLoaded = true;
5986
+ log5.info(`\u5DF2\u52A0\u8F7D ${rows.length} \u4E2A API Key`);
5987
+ }
5988
+ /** 查找 Key — 缓存命中或直接查 DB */
5989
+ lookup(key) {
5990
+ if (!this.cacheLoaded) this.loadCache();
5991
+ const cached = this.cache.get(key);
5992
+ if (cached) return cached;
5993
+ const row = this.db.prepare("SELECT * FROM api_keys WHERE key = ?").get(key);
5994
+ if (row) {
5995
+ this.cache.set(key, row);
5996
+ return row;
5997
+ }
5998
+ return null;
5999
+ }
6000
+ /** 校验并扣费 */
6001
+ deduct(key, riskLevel) {
6002
+ const record = this.lookup(key);
6003
+ if (!record) return { ok: false, error: "\u65E0\u6548\u7684 API Key" };
6004
+ if (!record.enabled) return { ok: false, error: "API Key \u5DF2\u7981\u7528" };
6005
+ const cost = CREDIT_COST[riskLevel] || 1;
6006
+ if (record.credits < cost) return { ok: false, error: `\u79EF\u5206\u4E0D\u8DB3 (\u9700\u8981 ${cost}\uFF0C\u5269\u4F59 ${record.credits})` };
6007
+ record.credits -= cost;
6008
+ record.totalUsed++;
6009
+ record.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
6010
+ this.db.prepare("UPDATE api_keys SET credits = ?, total_used = ?, last_used_at = ? WHERE id = ?").run(record.credits, record.totalUsed, record.lastUsedAt, record.id);
6011
+ return { ok: true, remaining: record.credits };
6012
+ }
6013
+ /** 管理:创建 Key */
6014
+ createKey(clientName, initialCredits = 1e3) {
6015
+ const key = "hvip-" + Array.from(
6016
+ { length: 32 },
6017
+ () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]
6018
+ ).join("");
6019
+ const result = this.db.prepare(
6020
+ "INSERT INTO api_keys (key, client_name, credits) VALUES (?, ?, ?)"
6021
+ ).run(key, clientName, initialCredits);
6022
+ const record = {
6023
+ id: result.lastInsertRowid,
6024
+ key,
6025
+ clientName,
6026
+ credits: initialCredits,
6027
+ totalUsed: 0,
6028
+ enabled: true,
6029
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6030
+ lastUsedAt: null
6031
+ };
6032
+ this.cache.set(key, record);
6033
+ return record;
6034
+ }
6035
+ /** 管理:充值 */
6036
+ topUp(key, amount) {
6037
+ const record = this.lookup(key);
6038
+ if (!record) return { ok: false, error: "Key \u4E0D\u5B58\u5728" };
6039
+ if (amount <= 0) return { ok: false, error: "\u5145\u503C\u91D1\u989D\u5FC5\u987B > 0" };
6040
+ record.credits += amount;
6041
+ this.db.prepare("UPDATE api_keys SET credits = ? WHERE id = ?").run(record.credits, record.id);
6042
+ return { ok: true, newBalance: record.credits };
6043
+ }
6044
+ /** 管理:启用/禁用 Key */
6045
+ setEnabled(key, enabled) {
6046
+ const record = this.lookup(key);
6047
+ if (!record) return false;
6048
+ record.enabled = enabled;
6049
+ this.db.prepare("UPDATE api_keys SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, record.id);
6050
+ return true;
6051
+ }
6052
+ /** 管理:列出所有 Key (含 Key 前缀,完整 Key 仅创建时返回) */
6053
+ listAll() {
6054
+ return [...this.cache.values()].map((r) => ({
6055
+ id: r.id,
6056
+ clientName: r.clientName,
6057
+ credits: r.credits,
6058
+ totalUsed: r.totalUsed,
6059
+ enabled: r.enabled,
6060
+ createdAt: r.createdAt,
6061
+ lastUsedAt: r.lastUsedAt,
6062
+ keyPrefix: r.key.slice(0, 12) + "..." + r.key.slice(-8)
6063
+ }));
6064
+ }
6065
+ /** 客户端:查询余额 */
6066
+ getBalance(key) {
6067
+ const record = this.lookup(key);
6068
+ if (!record) return null;
6069
+ return { clientName: record.clientName, credits: record.credits, totalUsed: record.totalUsed };
6070
+ }
6071
+ close() {
6072
+ try {
6073
+ this.db.close();
6074
+ } catch {
6075
+ }
6076
+ }
6077
+ };
6078
+ var instance = null;
6079
+ function getApiCredits(dbPath2) {
6080
+ if (!instance) instance = new ApiCredits(dbPath2);
6081
+ return instance;
6082
+ }
6083
+
5937
6084
  // src/hub-server.ts
5938
- var envPath = (0, import_node_path2.join)(process.cwd(), ".env");
5939
- if ((0, import_node_fs2.existsSync)(envPath)) {
5940
- (0, import_node_fs2.readFileSync)(envPath, "utf-8").split(/\r?\n/).forEach((line) => {
6085
+ var envPath = (0, import_node_path3.join)(process.cwd(), ".env");
6086
+ if ((0, import_node_fs3.existsSync)(envPath)) {
6087
+ (0, import_node_fs3.readFileSync)(envPath, "utf-8").split(/\r?\n/).forEach((line) => {
5941
6088
  const hashIdx = line.indexOf("#");
5942
6089
  if (hashIdx >= 0) line = line.substring(0, hashIdx);
5943
6090
  const m = line.match(/^\s*([^#\s=]+)\s*=\s*(.*)$/);
@@ -5951,11 +6098,11 @@ if ((0, import_node_fs2.existsSync)(envPath)) {
5951
6098
  }
5952
6099
  });
5953
6100
  }
5954
- var VERSION = "0.4.3";
6101
+ var VERSION = "0.6.0";
5955
6102
  function getDashboardHtml(host2, port) {
5956
- const paths = [(0, import_node_path2.join)(__dirname, "web", "dashboard.html"), (0, import_node_path2.join)(__dirname, "..", "src", "web", "dashboard.html")];
6103
+ const paths = [(0, import_node_path3.join)(__dirname, "web", "dashboard.html"), (0, import_node_path3.join)(__dirname, "..", "src", "web", "dashboard.html")];
5957
6104
  for (const p of paths) {
5958
- if ((0, import_node_fs2.existsSync)(p)) return (0, import_node_fs2.readFileSync)(p, "utf-8").replace("HUB_HOST", host2).replace("WS_PORT = 0", "WS_PORT = " + port);
6105
+ if ((0, import_node_fs3.existsSync)(p)) return (0, import_node_fs3.readFileSync)(p, "utf-8").replace("HUB_HOST", host2).replace("WS_PORT = 0", "WS_PORT = " + port);
5959
6106
  }
5960
6107
  return "<html><body><h2>dashboard.html not found</h2></body></html>";
5961
6108
  }
@@ -5970,7 +6117,7 @@ var wsPort = parseInt(flag("port") || process.env.HUB_PORT || "9321", 10);
5970
6117
  var host = flag("host") || process.env.HUB_HOST || "127.0.0.1";
5971
6118
  var webPort = parseInt(flag("web-port") || process.env.HUB_WEB_PORT || "3000", 10);
5972
6119
  var dbPath = flag("db") || process.env.HUB_DB_PATH || ".hub/hub.db";
5973
- var log5 = logger("Hub");
6120
+ var log6 = logger("Hub");
5974
6121
  var anthropicKey = process.env.ANTHROPIC_API_KEY || "";
5975
6122
  var openaiKey = process.env.OPENAI_API_KEY || "";
5976
6123
  var openaiBase = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
@@ -5986,7 +6133,7 @@ var MAX_CONCURRENT_WORKERS = 3;
5986
6133
  function spawnWorker(taskId) {
5987
6134
  const activeWorkers = workers.filter((w) => w.exitCode === null && !w.killed).length;
5988
6135
  if (activeWorkers >= MAX_CONCURRENT_WORKERS) {
5989
- log5.warn(`Worker \u5E76\u53D1\u4E0A\u9650 (${MAX_CONCURRENT_WORKERS})\uFF0C${taskId} \u7B49\u5F85\u4E0B\u6B21\u8C03\u5EA6`);
6136
+ log6.warn(`Worker \u5E76\u53D1\u4E0A\u9650 (${MAX_CONCURRENT_WORKERS})\uFF0C${taskId} \u7B49\u5F85\u4E0B\u6B21\u8C03\u5EA6`);
5990
6137
  return { ok: false, error: `\u5E76\u53D1\u4E0A\u9650 ${MAX_CONCURRENT_WORKERS}\uFF0C\u5F53\u524D ${activeWorkers} \u6D3B\u8DC3` };
5991
6138
  }
5992
6139
  const hubUrl = `ws://127.0.0.1:${wsPort}`;
@@ -6000,23 +6147,23 @@ function spawnWorker(taskId) {
6000
6147
  promptB64 = Buffer.from(p, "utf-8").toString("base64");
6001
6148
  }
6002
6149
  }
6003
- const workerArgs = ["dist/hub-worker.js", "--task", taskId, "--hub", hubUrl, "--repo", repoPath, "--web-port", String(webPort)];
6150
+ const workerArgs = ["dist/hub-worker-v2.js", "--task", taskId, "--hub", hubUrl, "--repo", repoPath, "--web-port", String(webPort)];
6004
6151
  if (promptB64) workerArgs.push("--prompt-b64", promptB64);
6005
6152
  try {
6006
6153
  const worker = (0, import_node_child_process.spawn)("node", workerArgs, { cwd: repoPath, stdio: "pipe", detached: true, windowsHide: true });
6007
6154
  worker.stdout?.on("data", (d) => process.stderr.write(`[Worker-${taskId}] ${d}`));
6008
6155
  worker.stderr?.on("data", (d) => process.stderr.write(`[Worker-${taskId}] ${d}`));
6009
- worker.on("error", (e) => log5.error(`Worker \u542F\u52A8\u5931\u8D25: ${e.message}`));
6156
+ worker.on("error", (e) => log6.error(`Worker \u542F\u52A8\u5931\u8D25: ${e.message}`));
6010
6157
  worker.on("close", (code) => {
6011
- log5.info(`Worker-${taskId} \u9000\u51FA (${code})`);
6158
+ log6.info(`Worker-${taskId} \u9000\u51FA (${code})`);
6012
6159
  const idx = workers.indexOf(worker);
6013
6160
  if (idx >= 0) workers.splice(idx, 1);
6014
6161
  });
6015
6162
  workers.push(worker);
6016
- log5.info(`\u6D3B\u8DC3 Worker: ${workers.length}`);
6163
+ log6.info(`\u6D3B\u8DC3 Worker: ${workers.length}`);
6017
6164
  return { ok: true, workerPid: worker.pid };
6018
6165
  } catch (e) {
6019
- log5.error(`Worker \u542F\u52A8\u5F02\u5E38: ${e.message}`);
6166
+ log6.error(`Worker \u542F\u52A8\u5F02\u5E38: ${e.message}`);
6020
6167
  return { ok: false, error: e.message };
6021
6168
  }
6022
6169
  }
@@ -6049,21 +6196,21 @@ function startHttpServer() {
6049
6196
  return;
6050
6197
  }
6051
6198
  if (_req.method === "GET" && _req.url?.startsWith("/v2/")) {
6052
- const v2Dir = (0, import_node_path2.join)(__dirname, "..", "dashboard-v2", "dist");
6199
+ const v2Dir = (0, import_node_path3.join)(__dirname, "..", "dashboard-v2", "dist");
6053
6200
  let filePath;
6054
6201
  if (_req.url === "/v2/") {
6055
- filePath = (0, import_node_path2.join)(v2Dir, "index.html");
6202
+ filePath = (0, import_node_path3.join)(v2Dir, "index.html");
6056
6203
  } else {
6057
- filePath = (0, import_node_path2.join)(v2Dir, _req.url.replace(/^\/v2\//, ""));
6204
+ filePath = (0, import_node_path3.join)(v2Dir, _req.url.replace(/^\/v2\//, ""));
6058
6205
  }
6059
- if (!(0, import_node_fs2.existsSync)(filePath) || !filePath.includes(".")) {
6060
- filePath = (0, import_node_path2.join)(v2Dir, "index.html");
6206
+ if (!(0, import_node_fs3.existsSync)(filePath) || !filePath.includes(".")) {
6207
+ filePath = (0, import_node_path3.join)(v2Dir, "index.html");
6061
6208
  }
6062
- if ((0, import_node_fs2.existsSync)(filePath)) {
6209
+ if ((0, import_node_fs3.existsSync)(filePath)) {
6063
6210
  const ext = filePath.split(".").pop() || "html";
6064
6211
  const mime = { html: "text/html; charset=utf-8", js: "application/javascript", css: "text/css", svg: "image/svg+xml", png: "image/png", ico: "image/x-icon" };
6065
6212
  res.writeHead(200, { "Content-Type": mime[ext] || "application/octet-stream" });
6066
- res.end((0, import_node_fs2.readFileSync)(filePath));
6213
+ res.end((0, import_node_fs3.readFileSync)(filePath));
6067
6214
  } else {
6068
6215
  res.writeHead(404);
6069
6216
  res.end("dashboard-v2 not found");
@@ -6071,11 +6218,11 @@ function startHttpServer() {
6071
6218
  return;
6072
6219
  }
6073
6220
  if (_req.method === "GET" && _req.url === "/chat") {
6074
- const paths = [(0, import_node_path2.join)(__dirname, "web", "index.html"), (0, import_node_path2.join)(__dirname, "..", "src", "web", "index.html")];
6221
+ const paths = [(0, import_node_path3.join)(__dirname, "web", "index.html"), (0, import_node_path3.join)(__dirname, "..", "src", "web", "index.html")];
6075
6222
  for (const p of paths) {
6076
- if ((0, import_node_fs2.existsSync)(p)) {
6223
+ if ((0, import_node_fs3.existsSync)(p)) {
6077
6224
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
6078
- res.end((0, import_node_fs2.readFileSync)(p, "utf-8"));
6225
+ res.end((0, import_node_fs3.readFileSync)(p, "utf-8"));
6079
6226
  return;
6080
6227
  }
6081
6228
  }
@@ -6117,11 +6264,9 @@ function startHttpServer() {
6117
6264
  }
6118
6265
  agentHub.registerTask(taskId, title || taskId, promptB64 || void 0);
6119
6266
  db?.saveTask({ taskId, status: "unassigned", title: title || taskId });
6120
- let spawned = false;
6121
- let workerPid = 0;
6122
- log5.info(`\u65B0\u4EFB\u52A1: ${taskId} "${title}"${spawned ? " \u{1F916} \u5DF2\u62C9\u8D77" : ""}`);
6267
+ log6.info(`\u65B0\u4EFB\u52A1: ${taskId} "${title}"`);
6123
6268
  res.writeHead(201, { "Content-Type": "application/json; charset=utf-8" });
6124
- res.end(JSON.stringify({ ok: true, taskId, title, spawned, workerPid }));
6269
+ res.end(JSON.stringify({ ok: true, taskId, title }));
6125
6270
  } catch {
6126
6271
  res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
6127
6272
  res.end(JSON.stringify({ error: "JSON \u89E3\u6790\u5931\u8D25" }));
@@ -6176,8 +6321,15 @@ function startHttpServer() {
6176
6321
  res.end(JSON.stringify(entries));
6177
6322
  return;
6178
6323
  }
6179
- if (_req.method === "GET" && _req.url === "/api/memory") {
6180
- const entries = memory.recent(30);
6324
+ if (_req.method === "GET" && (_req.url === "/api/memory" || _req.url?.startsWith("/api/memory?"))) {
6325
+ const url = _req.url.startsWith("/api/memory?") ? new import_node_url.URL(_req.url, `http://${host}:${webPort}`) : null;
6326
+ const type = url?.searchParams.get("type") || "";
6327
+ let entries;
6328
+ if (type && type !== "all" && ["memory", "doc", "directive", "skill", "strategy"].includes(type)) {
6329
+ entries = memory.byType(type, 30);
6330
+ } else {
6331
+ entries = memory.recent(30);
6332
+ }
6181
6333
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6182
6334
  res.end(JSON.stringify(entries));
6183
6335
  return;
@@ -6268,10 +6420,10 @@ function startHttpServer() {
6268
6420
  return;
6269
6421
  }
6270
6422
  if (_req.method === "GET" && _req.url === "/api/traders") {
6271
- const stateFile = (0, import_node_path2.join)(process.cwd(), ".hub", "trader-state.json");
6272
- if ((0, import_node_fs2.existsSync)(stateFile)) {
6423
+ const stateFile = (0, import_node_path3.join)(process.cwd(), ".hub", "trader-state.json");
6424
+ if ((0, import_node_fs3.existsSync)(stateFile)) {
6273
6425
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6274
- res.end((0, import_node_fs2.readFileSync)(stateFile, "utf-8"));
6426
+ res.end((0, import_node_fs3.readFileSync)(stateFile, "utf-8"));
6275
6427
  } else {
6276
6428
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6277
6429
  res.end(JSON.stringify({ traders: {}, history: [], round: 0 }));
@@ -6279,22 +6431,39 @@ function startHttpServer() {
6279
6431
  return;
6280
6432
  }
6281
6433
  if (_req.method === "GET" && _req.url === "/api/traders/leaderboard") {
6282
- const stateFile = (0, import_node_path2.join)(process.cwd(), ".hub", "trader-state.json");
6283
- if ((0, import_node_fs2.existsSync)(stateFile)) {
6284
- const state = JSON.parse((0, import_node_fs2.readFileSync)(stateFile, "utf-8"));
6285
- const rankings = Object.values(state.traders || {}).map((t) => ({
6286
- id: t.id,
6287
- name: t.name,
6288
- emoji: t.emoji,
6289
- title: t.title,
6290
- style: t.style,
6291
- capital: t.capital,
6292
- totalPnl: t.totalPnl,
6293
- totalPnlPct: t.totalPnlPct,
6294
- tradeCount: t.tradeCount || 0,
6295
- winCount: t.winCount || 0,
6296
- openPositions: (t.openPositions || []).filter((p) => !p.closed).length
6297
- })).sort((a, b) => b.totalPnlPct - a.totalPnlPct);
6434
+ const stateFile = (0, import_node_path3.join)(process.cwd(), ".hub", "trader-state.json");
6435
+ if ((0, import_node_fs3.existsSync)(stateFile)) {
6436
+ const state = JSON.parse((0, import_node_fs3.readFileSync)(stateFile, "utf-8"));
6437
+ const rankings = Object.values(state.traders || {}).map((t) => {
6438
+ const positions = (t.openPositions || []).filter((p) => !p.closed);
6439
+ const initialCapital = 1e4;
6440
+ return {
6441
+ id: t.id,
6442
+ name: t.name,
6443
+ emoji: t.emoji,
6444
+ capital: t.capital,
6445
+ equity: t.equity || t.capital,
6446
+ totalPnl: t.totalPnl,
6447
+ totalPnlPct: t.totalPnlPct,
6448
+ unrealizedPnl: t.unrealizedPnl || 0,
6449
+ tradeCount: t.tradeCount || 0,
6450
+ winCount: t.winCount || 0,
6451
+ totalFees: t.totalFees || 0,
6452
+ totalFunding: t.totalFunding || 0,
6453
+ openPositions: positions.length,
6454
+ positions: positions.map((p) => ({
6455
+ symbol: p.symbol,
6456
+ direction: p.direction,
6457
+ leverage: p.leverage,
6458
+ entryPrice: p.entryPrice,
6459
+ currentPrice: p.currentPrice,
6460
+ margin: p.margin,
6461
+ liquidationPrice: p.liquidationPrice,
6462
+ unrealizedPnl: p.unrealizedPnl,
6463
+ unrealizedPnlPct: p.unrealizedPnlPct
6464
+ }))
6465
+ };
6466
+ }).sort((a, b) => b.totalPnlPct - a.totalPnlPct);
6298
6467
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6299
6468
  res.end(JSON.stringify(rankings));
6300
6469
  } else {
@@ -6303,11 +6472,48 @@ function startHttpServer() {
6303
6472
  }
6304
6473
  return;
6305
6474
  }
6475
+ if (_req.method === "GET" && _req.url?.startsWith("/api/traders/history")) {
6476
+ const url = new import_node_url.URL(_req.url, `http://${host}:${webPort}`);
6477
+ const traderId = url.searchParams.get("trader") || "";
6478
+ const limit = parseInt(url.searchParams.get("limit") || "50");
6479
+ const histFile = (0, import_node_path3.join)(process.cwd(), ".hub", "trade-history.jsonl");
6480
+ if ((0, import_node_fs3.existsSync)(histFile)) {
6481
+ try {
6482
+ const lines = (0, import_node_fs3.readFileSync)(histFile, "utf-8").trim().split("\n");
6483
+ let entries = lines.map((l) => {
6484
+ try {
6485
+ return JSON.parse(l);
6486
+ } catch {
6487
+ return null;
6488
+ }
6489
+ }).filter(Boolean);
6490
+ if (traderId) entries = entries.filter((e) => e.traderId === traderId);
6491
+ entries = entries.slice(-limit);
6492
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6493
+ res.end(JSON.stringify(entries));
6494
+ } catch {
6495
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6496
+ res.end(JSON.stringify([]));
6497
+ }
6498
+ } else {
6499
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6500
+ res.end(JSON.stringify([]));
6501
+ }
6502
+ return;
6503
+ }
6504
+ if (_req.method === "GET" && _req.url === "/api/traders/risk") {
6505
+ const mode = process.env.AI_TRADER_MODE || "simulate";
6506
+ const maxOrder = parseFloat(process.env.TRADER_MAX_ORDER_USD || "100");
6507
+ const dailyLimit = parseFloat(process.env.TRADER_DAILY_LOSS_LIMIT || "500");
6508
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6509
+ res.end(JSON.stringify({ mode, maxOrderUsd: maxOrder, dailyLossLimit: dailyLimit }));
6510
+ return;
6511
+ }
6306
6512
  if (_req.method === "GET" && _req.url === "/api/signals") {
6307
- const sigFile = (0, import_node_path2.join)(process.cwd(), ".hub", "signals.json");
6308
- if ((0, import_node_fs2.existsSync)(sigFile)) {
6513
+ const sigFile = (0, import_node_path3.join)(process.cwd(), ".hub", "signals.json");
6514
+ if ((0, import_node_fs3.existsSync)(sigFile)) {
6309
6515
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6310
- res.end((0, import_node_fs2.readFileSync)(sigFile, "utf-8"));
6516
+ res.end((0, import_node_fs3.readFileSync)(sigFile, "utf-8"));
6311
6517
  } else {
6312
6518
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6313
6519
  res.end(JSON.stringify({ signals: [], lastRun: null }));
@@ -6392,6 +6598,86 @@ function startHttpServer() {
6392
6598
  res.end(JSON.stringify({ circuits, openCount, healthy: openCount === 0 }));
6393
6599
  return;
6394
6600
  }
6601
+ const apiCredits = getApiCredits();
6602
+ if (_req.method === "GET" && _req.url === "/api/credits/balance") {
6603
+ const key = _req.headers["authorization"]?.replace(/^Bearer\s+/i, "") || "";
6604
+ const balance = apiCredits.getBalance(key);
6605
+ if (!balance) {
6606
+ res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
6607
+ res.end(JSON.stringify({ error: "\u65E0\u6548\u7684 API Key" }));
6608
+ } else {
6609
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6610
+ res.end(JSON.stringify(balance));
6611
+ }
6612
+ return;
6613
+ }
6614
+ if (_req.method === "GET" && _req.url === "/api/admin/keys") {
6615
+ if (token && _req.headers["authorization"]?.replace(/^Bearer\s+/i, "") !== token) {
6616
+ res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
6617
+ res.end(JSON.stringify({ error: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" }));
6618
+ return;
6619
+ }
6620
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6621
+ res.end(JSON.stringify(apiCredits.listAll()));
6622
+ return;
6623
+ }
6624
+ if (_req.method === "POST" && _req.url === "/api/admin/keys") {
6625
+ if (token && _req.headers["authorization"]?.replace(/^Bearer\s+/i, "") !== token) {
6626
+ res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
6627
+ res.end(JSON.stringify({ error: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" }));
6628
+ return;
6629
+ }
6630
+ const chunks = [];
6631
+ _req.on("data", (c) => chunks.push(c));
6632
+ _req.on("end", () => {
6633
+ try {
6634
+ const { clientName, initialCredits } = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
6635
+ if (!clientName) {
6636
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
6637
+ res.end(JSON.stringify({ error: "\u7F3A\u5C11 clientName" }));
6638
+ return;
6639
+ }
6640
+ const record = apiCredits.createKey(clientName, initialCredits || 1e3);
6641
+ res.writeHead(201, { "Content-Type": "application/json; charset=utf-8" });
6642
+ res.end(JSON.stringify(record));
6643
+ } catch {
6644
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
6645
+ res.end(JSON.stringify({ error: "JSON \u89E3\u6790\u5931\u8D25" }));
6646
+ }
6647
+ });
6648
+ return;
6649
+ }
6650
+ if (_req.method === "POST" && _req.url === "/api/admin/keys/topup") {
6651
+ if (token && _req.headers["authorization"]?.replace(/^Bearer\s+/i, "") !== token) {
6652
+ res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
6653
+ res.end(JSON.stringify({ error: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" }));
6654
+ return;
6655
+ }
6656
+ const chunks = [];
6657
+ _req.on("data", (c) => chunks.push(c));
6658
+ _req.on("end", () => {
6659
+ try {
6660
+ const { key, amount } = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
6661
+ if (!key || !amount) {
6662
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
6663
+ res.end(JSON.stringify({ error: "\u7F3A\u5C11 key \u6216 amount" }));
6664
+ return;
6665
+ }
6666
+ const result = apiCredits.topUp(key, amount);
6667
+ if (!result.ok) {
6668
+ res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
6669
+ res.end(JSON.stringify({ error: result.error }));
6670
+ return;
6671
+ }
6672
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
6673
+ res.end(JSON.stringify({ ok: true, newBalance: result.newBalance }));
6674
+ } catch {
6675
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
6676
+ res.end(JSON.stringify({ error: "JSON \u89E3\u6790\u5931\u8D25" }));
6677
+ }
6678
+ });
6679
+ return;
6680
+ }
6395
6681
  if (_req.method === "POST" && _req.url === "/mcp") {
6396
6682
  const chunks = [];
6397
6683
  _req.on("data", (c) => chunks.push(c));
@@ -6422,6 +6708,7 @@ function startHttpServer() {
6422
6708
  }
6423
6709
  result = JSON.stringify(parsed);
6424
6710
  } catch {
6711
+ log6.warn("MCP \u54CD\u5E94\u683C\u5F0F\u89E3\u6790\u5931\u8D25");
6425
6712
  }
6426
6713
  }
6427
6714
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
@@ -6516,8 +6803,15 @@ function startHttpServer() {
6516
6803
  res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
6517
6804
  res.end(JSON.stringify({ error: "Not Found" }));
6518
6805
  });
6806
+ httpServer.on("error", (err) => {
6807
+ if (err.code === "EADDRINUSE") {
6808
+ log6.error(`\u7AEF\u53E3 ${webPort} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5 ${webPort + 1}`);
6809
+ } else {
6810
+ log6.error(`HTTP Server \u9519\u8BEF: ${err.message}`);
6811
+ }
6812
+ });
6519
6813
  httpServer.listen(webPort, host, () => {
6520
- log5.info(`\u{1F310} \u4EEA\u8868\u76D8 \u2192 http://${host}:${webPort}`);
6814
+ log6.info(`\u{1F310} \u4EEA\u8868\u76D8 \u2192 http://${host}:${webPort}`);
6521
6815
  });
6522
6816
  }
6523
6817
  var schedules = [
@@ -6565,6 +6859,17 @@ var schedules = [
6565
6859
  failCount: 0,
6566
6860
  timer: null
6567
6861
  },
6862
+ {
6863
+ id: "sched-signals",
6864
+ name: "\u{1F6A6} VBT\u4FE1\u53F7\u5237\u65B0",
6865
+ interval: 4 * 36e5,
6866
+ template: "vbt-signal-refresh",
6867
+ params: { script: "scripts/refresh-signals.mjs" },
6868
+ lastRun: 0,
6869
+ count: 0,
6870
+ failCount: 0,
6871
+ timer: null
6872
+ },
6568
6873
  // ════════ 24x7 守护岗 ════════
6569
6874
  {
6570
6875
  id: "guard-dashboard",
@@ -6650,7 +6955,7 @@ function runScheduledJob(job) {
6650
6955
  (t) => t.status === "assigned"
6651
6956
  ).filter((t) => t.taskId.startsWith(job.id + "-"));
6652
6957
  if (prevTasks.length > 0) {
6653
- log5.warn(`Scheduler: \u23ED\uFE0F ${job.name} \u4E0A\u6B21\u4EFB\u52A1\u672A\u5B8C\u6210 (${prevTasks.map((t) => t.taskId).join(",")})\uFF0C\u8DF3\u8FC7`);
6958
+ log6.warn(`Scheduler: \u23ED\uFE0F ${job.name} \u4E0A\u6B21\u4EFB\u52A1\u672A\u5B8C\u6210 (${prevTasks.map((t) => t.taskId).join(",")})\uFF0C\u8DF3\u8FC7`);
6654
6959
  scheduleNext(job);
6655
6960
  return;
6656
6961
  }
@@ -6659,17 +6964,33 @@ function runScheduledJob(job) {
6659
6964
  ).filter((t) => t.taskId.startsWith(job.id + "-"));
6660
6965
  for (const o of orphans) {
6661
6966
  db?.saveTask({ taskId: o.taskId, status: "done", result: "[Scheduler] \u81EA\u52A8\u6E05\u7406\u5B64\u513F\u4EFB\u52A1" });
6662
- log5.warn(`Scheduler: \u{1F9F9} \u6E05\u7406\u5B64\u513F: ${o.taskId}`);
6967
+ log6.warn(`Scheduler: \u{1F9F9} \u6E05\u7406\u5B64\u513F: ${o.taskId}`);
6663
6968
  }
6664
6969
  const taskId = `${job.id}-${Date.now().toString(36)}`;
6665
6970
  const title = `[\u5B9A\u65F6] ${job.name} #${job.count + 1}`;
6666
- log5.info(`Scheduler: \u23F0 ${job.name} \u2192 ${taskId}`);
6971
+ if (job.template === "vbt-signal-refresh") {
6972
+ log6.info(`Scheduler: \u{1F504} ${job.name}`);
6973
+ try {
6974
+ const { execSync: execSync2 } = require("node:child_process");
6975
+ const out = execSync2(`node scripts/refresh-signals.mjs`, { cwd: process.cwd(), encoding: "utf-8", timeout: 6e4, windowsHide: true, maxBuffer: 2e5 });
6976
+ log6.info(`Scheduler: ${job.name} \u5B8C\u6210 \u2014 ${out.trim().split("\n").length} \u884C\u8F93\u51FA`);
6977
+ } catch (e) {
6978
+ log6.error(`Scheduler: ${job.name} \u5931\u8D25: ${e.message}`);
6979
+ job.failCount++;
6980
+ }
6981
+ job.count++;
6982
+ job.lastRun = Date.now();
6983
+ scheduleNext(job);
6984
+ return;
6985
+ }
6986
+ log6.info(`Scheduler: \u23F0 ${job.name} \u2192 ${taskId}`);
6667
6987
  const tpl = TASK_TEMPLATES.find((t) => t.id === job.template);
6668
6988
  let promptB64 = "";
6669
6989
  if (tpl) {
6670
6990
  try {
6671
6991
  promptB64 = Buffer.from(tpl.buildPrompt(job.params), "utf-8").toString("base64");
6672
6992
  } catch {
6993
+ log6.warn(`promptB64 \u6784\u5EFA\u5931\u8D25: ${job.taskId}`);
6673
6994
  }
6674
6995
  }
6675
6996
  taskMeta.set(taskId, { templateId: job.template, params: job.params });
@@ -6686,7 +7007,7 @@ function scheduleNext(job) {
6686
7007
  }
6687
7008
  function startScheduler() {
6688
7009
  for (const job of schedules) scheduleNext(job);
6689
- log5.info(`Scheduler: ${schedules.length} \u4E2A\u5B9A\u65F6\u4EFB\u52A1 (\u9996\u6B2130s\u540E\u89E6\u53D1\uFF0CChronos \u2192 V2 Worker \u96F6\u5F39\u7A97)`);
7010
+ log6.info(`Scheduler: ${schedules.length} \u4E2A\u5B9A\u65F6\u4EFB\u52A1 (\u9996\u6B2130s\u540E\u89E6\u53D1\uFF0CChronos \u2192 V2 Worker \u96F6\u5F39\u7A97)`);
6690
7011
  }
6691
7012
  var banner = [
6692
7013
  `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`,
@@ -6701,38 +7022,47 @@ var db = new HubDB(dbPath);
6701
7022
  if (db.open()) {
6702
7023
  agentHub.setDB(db);
6703
7024
  const stats = db.stats();
6704
- log5.info(`DB \u72B6\u6001: ${stats.taskCount} tasks, ${stats.messageCount} messages`);
7025
+ log6.info(`DB \u72B6\u6001: ${stats.taskCount} tasks, ${stats.messageCount} messages`);
6705
7026
  }
6706
7027
  var memoryPath = flag("memory-db") || process.env.HUB_MEMORY_DB || ".hub/memory.db";
6707
7028
  var memory = new HubMemory(memoryPath);
6708
7029
  var memOk = memory.open();
6709
7030
  if (memOk) {
6710
7031
  const ms = memory.stats();
6711
- log5.info(`\u{1F9E0} \u8BB0\u5FC6: ${ms.total} \u6761 (doc:${ms.byType.doc || 0} directive:${ms.byType.directive || 0} memory:${ms.byType.memory || 0} skill:${ms.byType.skill || 0})`);
7032
+ log6.info(`\u{1F9E0} \u8BB0\u5FC6: ${ms.total} \u6761 (doc:${ms.byType.doc || 0} directive:${ms.byType.directive || 0} memory:${ms.byType.memory || 0} skill:${ms.byType.skill || 0})`);
6712
7033
  }
6713
7034
  var registryPath = flag("registry-db") || process.env.HUB_REGISTRY_DB || ".hub/registry.db";
6714
7035
  var registry = new HubRegistry(registryPath);
6715
7036
  var regOk = registry.open();
6716
- if (!regOk) {
6717
- log5.warn(`\u26A0\uFE0F Registry \u6253\u5F00\u5931\u8D25\uFF0C\u5546\u5E97\u529F\u80FD\u4E0D\u53EF\u7528`);
7037
+ if (regOk) {
7038
+ agentHub.registryCount = registry.all().length;
7039
+ } else {
7040
+ log6.warn(`\u26A0\uFE0F Registry \u6253\u5F00\u5931\u8D25\uFF0C\u5546\u5E97\u529F\u80FD\u4E0D\u53EF\u7528`);
6718
7041
  }
6719
7042
  var costsPath = flag("costs-db") || process.env.HUB_COSTS_DB || ".hub/llm-costs.jsonl";
6720
7043
  var costTracker = new HubCosts(costsPath);
6721
7044
  agentHub.setCostTracker(costTracker);
6722
- log5.info(`\u{1F4B0} \u6210\u672C\u8FFD\u8E2A: ${costsPath}`);
7045
+ log6.info(`\u{1F4B0} \u6210\u672C\u8FFD\u8E2A: ${costsPath}`);
6723
7046
  startHttpServer();
6724
7047
  startScheduler();
6725
7048
  var mcpProcess = null;
6726
- try {
6727
- mcpProcess = (0, import_node_child_process.spawn)("node", ["dist/index.js", "start:http"], { cwd: process.cwd(), stdio: "pipe", detached: true, env: { ...process.env, PORT: "9222" } });
6728
- mcpProcess.stderr?.on("data", (d) => process.stderr.write(d));
6729
- log5.info("\u{1F6E0} MCP Server \u542F\u52A8\u4E2D \u2192 http://127.0.0.1:9222");
6730
- } catch {
6731
- log5.warn("\u26A0\uFE0F MCP Server \u542F\u52A8\u5931\u8D25");
7049
+ if (process.env.HUB_SPAWN_MCP === "1") {
7050
+ try {
7051
+ mcpProcess = (0, import_node_child_process.spawn)("node", ["dist/index.js", "start:http"], { cwd: process.cwd(), stdio: "pipe", detached: true, windowsHide: true, env: { ...process.env, PORT: "9222" } });
7052
+ mcpProcess.stderr?.on("data", (d) => process.stderr.write(d));
7053
+ log6.info("\u{1F6E0} MCP Server \u542F\u52A8\u4E2D \u2192 http://127.0.0.1:9222");
7054
+ } catch {
7055
+ log6.warn("\u26A0\uFE0F MCP Server \u542F\u52A8\u5931\u8D25");
7056
+ }
7057
+ } else {
7058
+ log6.info("\u{1F6E0} MCP Server \u7531 PM2 \u72EC\u7ACB\u7BA1\u7406 (hvip-mcp :9222)\uFF0C\u8DF3\u8FC7\u5B50\u8FDB\u7A0B\u6D3E\u751F");
6732
7059
  }
6733
7060
  agentHub.start(wsPort, host, VERSION, token);
7061
+ setTimeout(() => {
7062
+ wsPort = agentHub.port || wsPort;
7063
+ }, 2e3);
6734
7064
  function shutdown() {
6735
- log5.info("\u6B63\u5728\u5173\u95ED...");
7065
+ log6.info("\u6B63\u5728\u5173\u95ED...");
6736
7066
  for (const w of workers) {
6737
7067
  try {
6738
7068
  w.kill();