@lynn123411/dsh-a6api 1.0.0 → 1.1.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/lib/index.js CHANGED
@@ -892,6 +892,66 @@ async function fetchChannelDetails(channelId, userId, sessionCookie, targetModel
892
892
  }
893
893
  return null;
894
894
  }
895
+ async function fetchPriceFluctuation(userId, sessionCookie, accessToken) {
896
+ const token = (accessToken || sessionCookie || "").trim();
897
+ const uid = (userId || "").trim();
898
+ if (!uid && !token) {
899
+ return { pendingCount: 0, unseenCount: 0, totalCount: 0, authError: false };
900
+ }
901
+ const headers = buildWebHeaders(uid || void 0, token || void 0);
902
+ const url = "https://a6api.com/api/marketplace/price-notices";
903
+ try {
904
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(8e3) });
905
+ if (res.status === 401 || res.status === 403) {
906
+ console.warn("[dsh-a6api] fetchPriceFluctuation auth failed", res.status);
907
+ return { pendingCount: 0, unseenCount: 0, totalCount: 0, authError: true };
908
+ }
909
+ if (!res.ok) {
910
+ console.warn("[dsh-a6api] fetchPriceFluctuation HTTP", res.status);
911
+ return { pendingCount: 0, unseenCount: 0, totalCount: 0 };
912
+ }
913
+ const json = await res.json().catch(() => null);
914
+ if (!json) return { pendingCount: 0, unseenCount: 0, totalCount: 0 };
915
+ if (json.success === false) return { pendingCount: 0, unseenCount: 0, totalCount: 0 };
916
+ let arr = [];
917
+ if (Array.isArray(json)) arr = json;
918
+ else if (Array.isArray(json.data)) arr = json.data;
919
+ else if (Array.isArray(json.data?.notices)) arr = json.data.notices;
920
+ else if (Array.isArray(json.data?.items)) arr = json.data.items;
921
+ else if (Array.isArray(json.notices)) arr = json.notices;
922
+ else if (Array.isArray(json.items)) arr = json.items;
923
+ const pickWithPresent = (keys) => {
924
+ for (const k of keys) {
925
+ const v = json?.data?.[k] ?? json?.[k];
926
+ if (v !== void 0 && v !== null) {
927
+ const n = Number(v);
928
+ if (!Number.isNaN(n)) return { value: n, present: true };
929
+ }
930
+ }
931
+ return { value: 0, present: false };
932
+ };
933
+ const pendingPick = pickWithPresent(["pendingCount", "pending_count", "pending", "openCount"]);
934
+ const unseenPick = pickWithPresent(["unseenCount", "unseen_count", "unseen", "has_unseen_count"]);
935
+ let pending = pendingPick.value;
936
+ let unseen = unseenPick.value;
937
+ const total = arr.length;
938
+ if (!pendingPick.present && arr.length > 0) {
939
+ const counted = arr.filter((n) => {
940
+ const s = String(n.state || n.status || "").toLowerCase();
941
+ return s === "open" || s === "pending" || n.pending === true;
942
+ }).length;
943
+ const hasState = arr.some((n) => n.state !== void 0 || n.status !== void 0);
944
+ if (hasState) pending = counted;
945
+ }
946
+ if (!unseenPick.present && arr.length > 0) {
947
+ unseen = arr.filter((n) => n.has_unseen === true || n.hasUnseen === true || n.unseen === true || n.is_unread === true).length;
948
+ }
949
+ return { pendingCount: pending, unseenCount: unseen, totalCount: total, notices: arr };
950
+ } catch (err) {
951
+ console.warn("[dsh-a6api] fetchPriceFluctuation error", err);
952
+ return { pendingCount: 0, unseenCount: 0, totalCount: 0 };
953
+ }
954
+ }
895
955
 
896
956
  // src/server/probe.ts
897
957
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -916,7 +976,8 @@ async function probeSingleModel(baseURL, apiKey, userId, accessToken, modelName)
916
976
  messages: [{ role: "user", content: "1" }],
917
977
  max_tokens: 1
918
978
  }),
919
- signal: AbortSignal.timeout(15e3)
979
+ // 推理模型(如 grok-4.6)实测单次响应可达 40-90s+,阈值过短会被频繁掐断导致探测失败
980
+ signal: AbortSignal.timeout(18e4)
920
981
  });
921
982
  if (res.ok) {
922
983
  requestOk = true;
@@ -925,11 +986,16 @@ async function probeSingleModel(baseURL, apiKey, userId, accessToken, modelName)
925
986
  requestError = `HTTP ${res.status}: ${errText.slice(0, 150)}`;
926
987
  }
927
988
  } catch (err) {
928
- requestError = err?.message || String(err);
989
+ const raw = err?.message || String(err);
990
+ if (raw.includes("aborted due to timeout") || err?.name === "TimeoutError") {
991
+ requestError = "\u63A2\u6D4B\u8D85\u65F6(\u9608\u503C180\u79D2) \u2014 \u63A8\u7406\u6A21\u578B\u54CD\u5E94\u8F83\u6162,\u5DF2\u4FDD\u7559\u4E0A\u6B21\u5546\u6237\u6570\u636E,\u8BF7\u7A0D\u540E\u91CD\u8BD5";
992
+ } else {
993
+ requestError = raw;
994
+ }
929
995
  }
930
996
  const durationMs = Date.now() - startTime;
931
997
  if (requestOk && (userId || accessToken)) {
932
- await sleep(700);
998
+ await sleep(1200);
933
999
  try {
934
1000
  const logs = await fetchRecentLogs(userId, accessToken, 15);
935
1001
  const minTimestamp = Math.floor(startTime / 1e3) - 10;
@@ -1019,6 +1085,13 @@ import * as fsp from "node:fs/promises";
1019
1085
  import * as path from "node:path";
1020
1086
  import * as os from "node:os";
1021
1087
  var A6API_CRED_REF = "A6API_API_KEY";
1088
+ async function atomicWriteFile(filePath, content, mode = 384) {
1089
+ const dir = path.dirname(filePath);
1090
+ await fsp.mkdir(dir, { recursive: true });
1091
+ const tmpPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
1092
+ await fsp.writeFile(tmpPath, content, { mode });
1093
+ await fsp.rename(tmpPath, filePath);
1094
+ }
1022
1095
  function dshHome() {
1023
1096
  return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
1024
1097
  }
@@ -1070,8 +1143,8 @@ async function readPluginConfig() {
1070
1143
  }
1071
1144
  async function savePluginConfig(config) {
1072
1145
  const filePath = configFile();
1073
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
1074
- await fsp.writeFile(filePath, JSON.stringify(config, null, 2), "utf8");
1146
+ const { apiKey: _apiKey, ...safeConfig } = config;
1147
+ await atomicWriteFile(filePath, JSON.stringify(safeConfig, null, 2));
1075
1148
  if (config.apiKey && config.apiKey.trim()) {
1076
1149
  await writeCredentialKey(A6API_CRED_REF, config.apiKey.trim());
1077
1150
  }
@@ -1144,8 +1217,7 @@ async function writeCredentialKey(refKey, value) {
1144
1217
  lines.push("refs:", ` ${refKey}: ${JSON.stringify(value)}`);
1145
1218
  }
1146
1219
  }
1147
- await fsp.mkdir(path.dirname(cFile), { recursive: true });
1148
- await fsp.writeFile(cFile, lines.join("\n"), "utf8");
1220
+ await atomicWriteFile(cFile, lines.join("\n"), 384);
1149
1221
  }
1150
1222
  async function syncToDshSettings(baseURL, modelIds) {
1151
1223
  const sFile = settingsFile();
@@ -1238,8 +1310,7 @@ async function syncToDshSettings(baseURL, modelIds) {
1238
1310
  } else {
1239
1311
  lines.push(`llm-pi-ai:`, ` providers:`, ...a6apiBlockLines);
1240
1312
  }
1241
- await fsp.mkdir(path.dirname(sFile), { recursive: true });
1242
- await fsp.writeFile(sFile, lines.join("\n"), "utf8");
1313
+ await atomicWriteFile(sFile, lines.join("\n"), 420);
1243
1314
  }
1244
1315
  async function getDshConfiguredModels() {
1245
1316
  try {
@@ -1279,13 +1350,23 @@ async function getDshConfiguredModels() {
1279
1350
  var name = "@lynn123411/dsh-a6api";
1280
1351
  var inject = ["webServer"];
1281
1352
  var PREFIX = "/api/dsh-a6api";
1353
+ var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
1354
+ function maskConfig(c) {
1355
+ return {
1356
+ ...c,
1357
+ apiKey: c.apiKey ? MASK : "",
1358
+ accessToken: c.accessToken || c.sessionCookie ? MASK : "",
1359
+ sessionCookie: "",
1360
+ userId: c.userId ? MASK : "",
1361
+ hasApiKey: Boolean(c.apiKey),
1362
+ hasToken: Boolean(c.accessToken || c.sessionCookie)
1363
+ };
1364
+ }
1282
1365
  function sendJson(res, status, body) {
1283
1366
  res.writeHead(status, {
1284
1367
  "content-type": "application/json; charset=utf-8",
1285
- "cache-control": "no-cache",
1286
- "access-control-allow-origin": "*",
1287
- "access-control-allow-headers": "*",
1288
- "access-control-allow-methods": "GET, POST, OPTIONS"
1368
+ "cache-control": "no-cache"
1369
+ // 不设 access-control-allow-origin:仅允许同源调用,阻断跨站读取与 CSRF 预检
1289
1370
  });
1290
1371
  res.end(JSON.stringify(body));
1291
1372
  }
@@ -1313,6 +1394,7 @@ async function parseJsonBody(req) {
1313
1394
  }
1314
1395
  }
1315
1396
  var merchantCardCache = /* @__PURE__ */ new Map();
1397
+ var MERCHANT_CARD_TTL_MS = 15 * 60 * 1e3;
1316
1398
  function apply(ctx) {
1317
1399
  const webServer = ctx.webServer || (ctx.get ? ctx.get("webServer") : null);
1318
1400
  if (webServer && typeof webServer.register === "function") {
@@ -1324,11 +1406,7 @@ function apply(ctx) {
1324
1406
  const url = new URL(req.url || "/", "http://localhost");
1325
1407
  const pathname = url.pathname.replace(PREFIX, "") || "/";
1326
1408
  if (req.method === "OPTIONS") {
1327
- res.writeHead(204, {
1328
- "access-control-allow-origin": "*",
1329
- "access-control-allow-headers": "*",
1330
- "access-control-allow-methods": "GET, POST, OPTIONS"
1331
- });
1409
+ res.writeHead(204);
1332
1410
  return res.end();
1333
1411
  }
1334
1412
  try {
@@ -1360,18 +1438,22 @@ function apply(ctx) {
1360
1438
  ];
1361
1439
  }
1362
1440
  if (config.userId || token) {
1363
- const missing = modelIds.filter((m) => !merchantCardCache.has(m.toLowerCase()));
1441
+ const missing = modelIds.filter((m) => {
1442
+ const entry = merchantCardCache.get(m.toLowerCase());
1443
+ return !entry || Date.now() - entry.at >= MERCHANT_CARD_TTL_MS;
1444
+ });
1364
1445
  if (missing.length > 0) {
1365
1446
  const found = await getKnownMerchantsFromLogs(config.userId, token, missing);
1366
1447
  for (const [mName, card] of Object.entries(found)) {
1367
- merchantCardCache.set(mName.toLowerCase(), card);
1448
+ merchantCardCache.set(mName.toLowerCase(), { card, at: Date.now() });
1368
1449
  }
1369
1450
  }
1370
1451
  }
1371
1452
  const dshSet = new Set(dshConfiguredModels);
1372
1453
  const models = modelIds.map((mId) => {
1373
1454
  const meta = resolveModelMeta(mId);
1374
- const cachedCard = merchantCardCache.get(mId.toLowerCase());
1455
+ const cacheEntry = merchantCardCache.get(mId.toLowerCase());
1456
+ const cachedCard = cacheEntry && Date.now() - cacheEntry.at < MERCHANT_CARD_TTL_MS ? cacheEntry.card : void 0;
1375
1457
  return {
1376
1458
  model_name: mId,
1377
1459
  brand: meta.brand,
@@ -1386,7 +1468,7 @@ function apply(ctx) {
1386
1468
  });
1387
1469
  const recentLogs = await fetchRecentLogs(config.userId, token, 20);
1388
1470
  const response = {
1389
- config,
1471
+ config: maskConfig(config),
1390
1472
  balance,
1391
1473
  models,
1392
1474
  dshConfiguredModels,
@@ -1397,10 +1479,11 @@ function apply(ctx) {
1397
1479
  if (pathname === "/config" && req.method === "POST") {
1398
1480
  const body = await parseJsonBody(req);
1399
1481
  const current = await readPluginConfig();
1400
- const rawToken = body.accessToken !== void 0 ? body.accessToken : body.sessionCookie !== void 0 ? body.sessionCookie : current.accessToken || current.sessionCookie || "";
1482
+ const rawToken = body.accessToken !== void 0 && body.accessToken !== MASK ? body.accessToken : body.sessionCookie !== void 0 && body.sessionCookie !== MASK ? body.sessionCookie : current.accessToken || current.sessionCookie || "";
1483
+ const newApiKey = body.apiKey !== void 0 && body.apiKey !== MASK ? body.apiKey : current.apiKey;
1401
1484
  const updated = {
1402
1485
  baseURL: body.baseURL !== void 0 ? body.baseURL : current.baseURL,
1403
- apiKey: body.apiKey !== void 0 ? body.apiKey : current.apiKey,
1486
+ apiKey: newApiKey,
1404
1487
  accessToken: rawToken,
1405
1488
  sessionCookie: rawToken,
1406
1489
  userId: body.userId !== void 0 ? body.userId : current.userId,
@@ -1415,7 +1498,7 @@ function apply(ctx) {
1415
1498
  if (updated.activeModels.length > 0) {
1416
1499
  await syncToDshSettings(updated.baseURL, updated.activeModels);
1417
1500
  }
1418
- return sendJson(res, 200, { ok: true, config: updated, balance });
1501
+ return sendJson(res, 200, { ok: true, config: maskConfig(updated), balance });
1419
1502
  }
1420
1503
  if (pathname === "/balance" && (req.method === "GET" || req.method === "HEAD")) {
1421
1504
  const config = await readPluginConfig();
@@ -1438,7 +1521,7 @@ function apply(ctx) {
1438
1521
  if (modelName && modelName !== "all") {
1439
1522
  const result = await probeSingleModel(config.baseURL, config.apiKey, config.userId, token, modelName);
1440
1523
  if (result.merchant) {
1441
- merchantCardCache.set(modelName.toLowerCase(), result.merchant);
1524
+ merchantCardCache.set(modelName.toLowerCase(), { card: result.merchant, at: Date.now() });
1442
1525
  }
1443
1526
  return sendJson(res, 200, { ok: true, result });
1444
1527
  }
@@ -1453,7 +1536,7 @@ function apply(ctx) {
1453
1536
  for (const m of modelIds) {
1454
1537
  const r = await probeSingleModel(config.baseURL, config.apiKey, config.userId, token, m);
1455
1538
  if (r.merchant) {
1456
- merchantCardCache.set(m.toLowerCase(), r.merchant);
1539
+ merchantCardCache.set(m.toLowerCase(), { card: r.merchant, at: Date.now() });
1457
1540
  }
1458
1541
  results.push(r);
1459
1542
  }
@@ -1471,6 +1554,17 @@ function apply(ctx) {
1471
1554
  const dshConfiguredModels = await getDshConfiguredModels();
1472
1555
  return sendJson(res, 200, { ok: true, dshConfiguredModels });
1473
1556
  }
1557
+ if (pathname === "/price-fluctuation" && (req.method === "GET" || req.method === "HEAD")) {
1558
+ const config = await readPluginConfig();
1559
+ const token = config.accessToken || config.sessionCookie || "";
1560
+ if (!token || !config.userId) {
1561
+ return sendJson(res, 200, { ok: true, data: { pendingCount: 0, unseenCount: 0, totalCount: 0, hasAuth: false, authError: false, updatedAt: Date.now() } });
1562
+ }
1563
+ const result = await fetchPriceFluctuation(config.userId, token, token);
1564
+ const { notices, ...counts } = result;
1565
+ const hasAuth = !counts.authError;
1566
+ return sendJson(res, 200, { ok: true, data: { pendingCount: counts.pendingCount, unseenCount: counts.unseenCount, totalCount: counts.totalCount, hasAuth, authError: Boolean(counts.authError), updatedAt: Date.now() } });
1567
+ }
1474
1568
  return sendJson(res, 404, { ok: false, error: "Not found" });
1475
1569
  } catch (err) {
1476
1570
  console.error("[dsh-a6api] API error:", err);