llm-simple-router 0.5.0 → 0.5.2

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.
Files changed (179) hide show
  1. package/config/recommended-providers.json +76 -0
  2. package/config/recommended-retry-rules.json +10 -0
  3. package/dist/admin/api-response.d.ts +27 -0
  4. package/dist/admin/api-response.js +40 -0
  5. package/dist/admin/constants.d.ts +0 -2
  6. package/dist/admin/constants.js +0 -3
  7. package/dist/admin/groups.js +9 -5
  8. package/dist/admin/logs.js +3 -2
  9. package/dist/admin/mappings.js +7 -6
  10. package/dist/admin/metrics.js +23 -5
  11. package/dist/admin/monitor.js +2 -1
  12. package/dist/admin/providers.js +13 -4
  13. package/dist/admin/proxy-enhancement.js +11 -6
  14. package/dist/admin/recommended.js +1 -9
  15. package/dist/admin/retry-rules.js +8 -4
  16. package/dist/admin/router-keys.js +5 -1
  17. package/dist/admin/routes.js +2 -0
  18. package/dist/admin/settings-import-export.js +3 -2
  19. package/dist/admin/settings.js +7 -5
  20. package/dist/admin/setup.js +3 -2
  21. package/dist/admin/stats.js +20 -3
  22. package/dist/admin/upgrade.d.ts +13 -0
  23. package/dist/admin/upgrade.js +114 -0
  24. package/dist/admin/usage.js +12 -24
  25. package/dist/config.d.ts +1 -1
  26. package/dist/config.js +1 -1
  27. package/dist/constants.d.ts +3 -0
  28. package/dist/constants.js +11 -0
  29. package/dist/db/index.d.ts +3 -3
  30. package/dist/db/index.js +2 -2
  31. package/dist/db/mappings.js +5 -8
  32. package/dist/db/metrics.js +3 -4
  33. package/dist/db/providers.d.ts +8 -0
  34. package/dist/db/providers.js +6 -0
  35. package/dist/db/retry-rules.d.ts +1 -0
  36. package/dist/db/retry-rules.js +3 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +7 -0
  39. package/dist/db/stats.d.ts +1 -2
  40. package/dist/db/stats.js +7 -11
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +55 -34
  43. package/dist/metrics/metrics-extractor.js +1 -1
  44. package/dist/metrics/sse-parser.js +2 -0
  45. package/dist/middleware/admin-auth.js +6 -5
  46. package/dist/middleware/auth.js +1 -10
  47. package/dist/monitor/request-tracker.d.ts +1 -0
  48. package/dist/monitor/request-tracker.js +9 -45
  49. package/dist/monitor/runtime-collector.js +1 -1
  50. package/dist/monitor/stream-content-accumulator.d.ts +14 -0
  51. package/dist/monitor/stream-content-accumulator.js +58 -0
  52. package/dist/proxy/anthropic.d.ts +2 -1
  53. package/dist/proxy/anthropic.js +3 -3
  54. package/dist/proxy/enhancement/directive-parser.d.ts +18 -0
  55. package/dist/proxy/{directive-parser.js → enhancement/directive-parser.js} +44 -0
  56. package/dist/proxy/{enhancement-handler.js → enhancement/enhancement-handler.js} +152 -32
  57. package/dist/proxy/enhancement/index.d.ts +3 -0
  58. package/dist/proxy/enhancement/index.js +3 -0
  59. package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
  60. package/dist/proxy/log-helpers.d.ts +1 -1
  61. package/dist/proxy/mapping-resolver.js +4 -4
  62. package/dist/proxy/openai.d.ts +2 -1
  63. package/dist/proxy/openai.js +4 -4
  64. package/dist/proxy/orchestrator.d.ts +0 -1
  65. package/dist/proxy/orchestrator.js +1 -3
  66. package/dist/proxy/proxy-core.d.ts +0 -4
  67. package/dist/proxy/proxy-core.js +0 -2
  68. package/dist/proxy/proxy-handler.d.ts +1 -1
  69. package/dist/proxy/proxy-handler.js +52 -132
  70. package/dist/proxy/proxy-logging.d.ts +0 -2
  71. package/dist/proxy/proxy-logging.js +1 -3
  72. package/dist/proxy/resilience.d.ts +5 -2
  73. package/dist/proxy/resilience.js +16 -7
  74. package/dist/proxy/strategy/failover.js +2 -7
  75. package/dist/proxy/strategy/random.js +2 -2
  76. package/dist/proxy/strategy/round-robin.js +2 -2
  77. package/dist/proxy/strategy/scheduled.js +1 -8
  78. package/dist/proxy/strategy/targets-rule.d.ts +1 -0
  79. package/dist/proxy/strategy/targets-rule.js +5 -0
  80. package/dist/proxy/transport-fn.d.ts +25 -0
  81. package/dist/proxy/transport-fn.js +55 -0
  82. package/dist/proxy/transport.d.ts +0 -25
  83. package/dist/proxy/transport.js +0 -38
  84. package/dist/upgrade/checker.d.ts +25 -0
  85. package/dist/upgrade/checker.js +120 -0
  86. package/dist/upgrade/deployment.d.ts +2 -0
  87. package/dist/upgrade/deployment.js +20 -0
  88. package/dist/upgrade/version.d.ts +1 -0
  89. package/dist/upgrade/version.js +13 -0
  90. package/dist/utils/password.js +4 -2
  91. package/dist/utils/time-range.d.ts +9 -0
  92. package/dist/utils/time-range.js +40 -0
  93. package/frontend-dist/assets/CardContent-WrBnGhTg.js +1 -0
  94. package/frontend-dist/assets/CardTitle-BcDYk7cq.js +1 -0
  95. package/frontend-dist/assets/Checkbox-MZf0YsDG.js +1 -0
  96. package/frontend-dist/assets/CollapsibleTrigger-CrOH9HlW.js +1 -0
  97. package/frontend-dist/assets/Collection-DcTx_Y54.js +1 -0
  98. package/frontend-dist/assets/Dashboard-D0oDrSLr.js +3 -0
  99. package/frontend-dist/assets/DialogTitle-Cl5Cd7QH.js +1 -0
  100. package/frontend-dist/assets/{Input-l5ZurXX5.js → Input-O0ebU-Va.js} +1 -1
  101. package/frontend-dist/assets/Label-C_S0y7Um.js +1 -0
  102. package/frontend-dist/assets/Login-DGY7uF8P.js +1 -0
  103. package/frontend-dist/assets/Logs-ls8pv89b.js +1 -0
  104. package/frontend-dist/assets/ModelMappings-DGlf0S4s.js +1 -0
  105. package/frontend-dist/assets/Monitor-BSI87grz.js +1 -0
  106. package/frontend-dist/assets/PopperContent-C6Q7hDmf.js +1 -0
  107. package/frontend-dist/assets/Providers-ZkRpj8_m.js +1 -0
  108. package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +5 -0
  109. package/frontend-dist/assets/RetryRules-DtM31qsl.js +1 -0
  110. package/frontend-dist/assets/RouterKeys-D63tRFKm.js +1 -0
  111. package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +1 -0
  112. package/frontend-dist/assets/SelectValue-CLp5z6_I.js +1 -0
  113. package/frontend-dist/assets/Settings-DSgRKbTQ.js +6 -0
  114. package/frontend-dist/assets/Setup-BDmj6CRk.js +1 -0
  115. package/frontend-dist/assets/Switch-Wz-t_zkv.js +1 -0
  116. package/frontend-dist/assets/TableHeader-DGtcqGkw.js +1 -0
  117. package/frontend-dist/assets/TabsTrigger-CPCi2HIa.js +1 -0
  118. package/frontend-dist/assets/Teleport-DdjYHlNK.js +3 -0
  119. package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +1 -0
  120. package/frontend-dist/assets/UnifiedRequestDialog-BAAfMJJl.js +3 -0
  121. package/frontend-dist/assets/{VisuallyHidden-BwwTtzb9.js → VisuallyHidden-Cyk-jWwh.js} +1 -1
  122. package/frontend-dist/assets/VisuallyHiddenInput-CYjNe_H8.js +1 -0
  123. package/frontend-dist/assets/alert-dialog-Bi3dliLl.js +1 -0
  124. package/frontend-dist/assets/badge-Kkta3e9W.js +1 -0
  125. package/frontend-dist/assets/button-BQ3s7yNh.js +12 -0
  126. package/frontend-dist/assets/{createLucideIcon-Biq59l_W.js → createLucideIcon-D1tkPDOQ.js} +1 -1
  127. package/frontend-dist/assets/dialog-DoIATUYw.js +1 -0
  128. package/frontend-dist/assets/{file-text-DoRW0hQW.js → file-text-Dt6QP1bZ.js} +1 -1
  129. package/frontend-dist/assets/index-BY0E7CHR.js +1 -0
  130. package/frontend-dist/assets/index-Bnrh1mFY.css +1 -0
  131. package/frontend-dist/assets/lib-CxwxnlwW.js +1 -0
  132. package/frontend-dist/assets/{ohash.D__AXeF1-BGxYMs6k.js → ohash.D__AXeF1-b0PiKZB_.js} +1 -1
  133. package/frontend-dist/assets/{useClipboard-vaHkvJHw.js → useClipboard-Cnnz6AAN.js} +1 -1
  134. package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +1 -0
  135. package/frontend-dist/assets/useNonce-DKbOCfgM.js +1 -0
  136. package/frontend-dist/assets/x-CAoitXRt.js +1 -0
  137. package/frontend-dist/index.html +18 -9
  138. package/package.json +2 -1
  139. package/dist/proxy/directive-parser.d.ts +0 -7
  140. package/frontend-dist/assets/CardContent-CIO85eT6.js +0 -1
  141. package/frontend-dist/assets/CardTitle-DiqIReMT.js +0 -1
  142. package/frontend-dist/assets/Checkbox-C2u5pIp4.js +0 -1
  143. package/frontend-dist/assets/CollapsibleTrigger-RKFL41om.js +0 -1
  144. package/frontend-dist/assets/Collection-iiNnuTQj.js +0 -1
  145. package/frontend-dist/assets/Dashboard-DOEqP6gF.js +0 -3
  146. package/frontend-dist/assets/DialogTitle-CEqndrf6.js +0 -1
  147. package/frontend-dist/assets/Label-PgGtS8v2.js +0 -1
  148. package/frontend-dist/assets/Login-DaN6ZcCx.js +0 -1
  149. package/frontend-dist/assets/Logs-CleRQ7Xk.js +0 -1
  150. package/frontend-dist/assets/ModelMappings-CacA_ua_.js +0 -1
  151. package/frontend-dist/assets/Monitor-LSMFOBN2.js +0 -1
  152. package/frontend-dist/assets/PopperContent-zLFHqQP0.js +0 -1
  153. package/frontend-dist/assets/Providers-NT5MUDU0.js +0 -1
  154. package/frontend-dist/assets/ProxyEnhancement-DhOy8nNy.js +0 -5
  155. package/frontend-dist/assets/RetryRules-7arWa3jB.js +0 -1
  156. package/frontend-dist/assets/RouterKeys-CdaZunRg.js +0 -1
  157. package/frontend-dist/assets/SelectValue-CSg-MKW_.js +0 -1
  158. package/frontend-dist/assets/Settings-1ntV9XE3.js +0 -6
  159. package/frontend-dist/assets/Setup-CXLTDhYJ.js +0 -1
  160. package/frontend-dist/assets/Switch-DivrIFE3.js +0 -1
  161. package/frontend-dist/assets/TableHeader-Bn0bodWx.js +0 -1
  162. package/frontend-dist/assets/TabsContent-MWvOH_LJ.js +0 -1
  163. package/frontend-dist/assets/TabsTrigger-WKkUfO2M.js +0 -1
  164. package/frontend-dist/assets/Teleport-B0PNXZbP.js +0 -3
  165. package/frontend-dist/assets/UnifiedRequestDialog-Ba2e7YuJ.js +0 -3
  166. package/frontend-dist/assets/VisuallyHiddenInput-EGZSP7s8.js +0 -1
  167. package/frontend-dist/assets/alert-dialog-CS1yFhdV.js +0 -1
  168. package/frontend-dist/assets/badge-C-QcC5n2.js +0 -1
  169. package/frontend-dist/assets/button-Dbz2Be22.js +0 -12
  170. package/frontend-dist/assets/dialog-Cr0YQlLW.js +0 -1
  171. package/frontend-dist/assets/index-0H2uCGbx.js +0 -1
  172. package/frontend-dist/assets/index-D-cdVNCb.css +0 -1
  173. package/frontend-dist/assets/lib-B0lieqgg.js +0 -1
  174. package/frontend-dist/assets/useForwardExpose-C2_ks3sW.js +0 -1
  175. package/frontend-dist/assets/useLogRetention-Cs_fiKql.js +0 -1
  176. package/frontend-dist/assets/useNonce-C9do0jOI.js +0 -1
  177. package/frontend-dist/assets/x-BlTnH_0_.js +0 -1
  178. /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
  179. /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
@@ -1,7 +1,6 @@
1
1
  import { request as httpRequestFn } from "http";
2
2
  import { request as httpsRequestFn } from "https";
3
3
  import { UPSTREAM_SUCCESS, filterHeaders } from "./types.js";
4
- import { callStream } from "./stream-proxy.js";
5
4
  // Re-export callStream from stream-proxy.ts for external consumers
6
5
  export { callStream } from "./stream-proxy.js";
7
6
  // ---------- Constants ----------
@@ -16,9 +15,6 @@ export const _transportInternals = {
16
15
  : httpRequestFn(options);
17
16
  },
18
17
  };
19
- export function createUpstreamRequest(url, options) {
20
- return _transportInternals.createUpstreamRequest(url, options);
21
- }
22
18
  export function buildRequestOptions(url, headers, method = "POST") {
23
19
  return {
24
20
  hostname: url.hostname,
@@ -92,37 +88,3 @@ export function callGet(backend, apiKey, clientHeaders, upstreamPath, buildHeade
92
88
  req.end();
93
89
  });
94
90
  }
95
- export function proxyNonStreamCompat(backend, apiKey, body, clientHeaders, upstreamPath, buildHeaders) {
96
- return callNonStream(backend, apiKey, body, clientHeaders, upstreamPath, buildHeaders)
97
- .then((r) => {
98
- if (r.kind === "throw")
99
- throw r.error;
100
- return {
101
- statusCode: r.statusCode,
102
- body: "body" in r ? r.body : "",
103
- headers: "headers" in r ? r.headers : {},
104
- sentHeaders: r.sentHeaders,
105
- sentBody: "sentBody" in r ? r.sentBody : "",
106
- };
107
- });
108
- }
109
- export function proxyStreamCompat(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError) {
110
- return new Promise((resolve, reject) => {
111
- function onResult(r) {
112
- if (r.kind === "throw") {
113
- reject(r.error);
114
- return;
115
- }
116
- const metrics = (r.kind === "stream_success" || r.kind === "stream_abort") ? r.metrics : undefined;
117
- resolve({
118
- statusCode: r.statusCode,
119
- responseBody: r.kind === "stream_success" ? undefined : ("body" in r ? r.body : undefined),
120
- upstreamResponseHeaders: ("upstreamResponseHeaders" in r ? r.upstreamResponseHeaders : undefined) ?? ("headers" in r ? r.headers : {}) ?? {},
121
- sentHeaders: r.sentHeaders,
122
- metricsResult: metrics ?? undefined,
123
- abnormalClose: r.kind === "stream_abort",
124
- });
125
- }
126
- callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, onResult);
127
- });
128
- }
@@ -0,0 +1,25 @@
1
+ export interface NpmStatus {
2
+ hasUpdate: boolean;
3
+ currentVersion: string;
4
+ latestVersion: string | null;
5
+ }
6
+ export interface ConfigStatus {
7
+ hasUpdate: boolean;
8
+ providerChanges: number;
9
+ retryRuleChanges: number;
10
+ }
11
+ export interface UpgradeStatus {
12
+ npm: NpmStatus;
13
+ config: ConfigStatus;
14
+ lastCheckedAt: string | null;
15
+ }
16
+ export interface CheckerOptions {
17
+ npmRegistryUrl?: string;
18
+ configBaseUrl?: string;
19
+ configDir?: string;
20
+ }
21
+ export declare function fetchJson(url: string, redirects?: number): Promise<unknown>;
22
+ export declare function createUpgradeChecker(options?: CheckerOptions): {
23
+ check: (sourceOverride?: string) => Promise<void>;
24
+ getStatus: () => UpgradeStatus;
25
+ };
@@ -0,0 +1,120 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { getInstalledVersion } from './version.js';
6
+ const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/llm-simple-router';
7
+ const DEFAULT_GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
8
+ const CHECK_TIMEOUT_MS = 5000;
9
+ const MAX_REDIRECTS = 5;
10
+ export async function fetchJson(url, redirects = 0) {
11
+ if (redirects > MAX_REDIRECTS)
12
+ throw new Error('too many redirects');
13
+ const mod = url.startsWith('https') ? https : http;
14
+ return new Promise((resolve, reject) => {
15
+ const timer = setTimeout(() => reject(new Error('timeout')), CHECK_TIMEOUT_MS);
16
+ mod.get(url, (res) => {
17
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
18
+ clearTimeout(timer);
19
+ res.resume();
20
+ fetchJson(res.headers.location, redirects + 1).then(resolve, reject);
21
+ return;
22
+ }
23
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
24
+ clearTimeout(timer);
25
+ res.resume();
26
+ reject(new Error(`HTTP ${res.statusCode}`));
27
+ return;
28
+ }
29
+ let data = '';
30
+ res.on('data', (chunk) => { data += chunk; });
31
+ res.on('end', () => {
32
+ clearTimeout(timer);
33
+ try {
34
+ resolve(JSON.parse(data));
35
+ }
36
+ catch {
37
+ reject(new Error('invalid json'));
38
+ }
39
+ });
40
+ }).on('error', (err) => {
41
+ clearTimeout(timer);
42
+ reject(err);
43
+ });
44
+ });
45
+ }
46
+ export function createUpgradeChecker(options) {
47
+ const npmRegistryUrl = options?.npmRegistryUrl ?? DEFAULT_NPM_REGISTRY;
48
+ const configBaseUrl = options?.configBaseUrl ?? DEFAULT_GITHUB_CONFIG_BASE;
49
+ const configDir = options?.configDir ?? path.resolve(process.cwd(), 'config');
50
+ let npmStatus = {
51
+ hasUpdate: false,
52
+ currentVersion: getInstalledVersion(),
53
+ latestVersion: null,
54
+ };
55
+ let configStatus = {
56
+ hasUpdate: false,
57
+ providerChanges: 0,
58
+ retryRuleChanges: 0,
59
+ };
60
+ let lastCheckedAt = null;
61
+ function loadLocalJson(filename) {
62
+ const filePath = path.join(configDir, filename);
63
+ try {
64
+ if (!fs.existsSync(filePath))
65
+ return null;
66
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ async function checkNpm() {
73
+ try {
74
+ const data = await fetchJson(npmRegistryUrl);
75
+ const latest = data?.['dist-tags']?.latest ?? null;
76
+ npmStatus = {
77
+ hasUpdate: latest !== null && latest !== npmStatus.currentVersion,
78
+ currentVersion: npmStatus.currentVersion,
79
+ latestVersion: latest,
80
+ };
81
+ }
82
+ catch {
83
+ process.stderr.write('[upgrade] failed to check npm version\n');
84
+ }
85
+ }
86
+ async function checkConfig(sourceOverride) {
87
+ try {
88
+ const base = sourceOverride ?? configBaseUrl;
89
+ const [providersResult, rulesResult] = await Promise.allSettled([
90
+ fetchJson(`${base}/recommended-providers.json`),
91
+ fetchJson(`${base}/recommended-retry-rules.json`),
92
+ ]);
93
+ const remoteProviders = providersResult.status === 'fulfilled' ? providersResult.value : null;
94
+ const remoteRules = rulesResult.status === 'fulfilled' ? rulesResult.value : null;
95
+ if (!remoteProviders && !remoteRules) {
96
+ throw new Error('both providers and rules fetch failed');
97
+ }
98
+ const localProviders = loadLocalJson('recommended-providers.json');
99
+ const localRules = loadLocalJson('recommended-retry-rules.json');
100
+ const providersChanged = remoteProviders !== null && JSON.stringify(remoteProviders) !== JSON.stringify(localProviders);
101
+ const rulesChanged = remoteRules !== null && JSON.stringify(remoteRules) !== JSON.stringify(localRules);
102
+ configStatus = {
103
+ hasUpdate: providersChanged || rulesChanged,
104
+ providerChanges: providersChanged ? 1 : 0,
105
+ retryRuleChanges: rulesChanged ? 1 : 0,
106
+ };
107
+ }
108
+ catch {
109
+ process.stderr.write('[upgrade] failed to check config update\n');
110
+ }
111
+ }
112
+ async function check(sourceOverride) {
113
+ await Promise.allSettled([checkNpm(), checkConfig(sourceOverride)]);
114
+ lastCheckedAt = new Date().toISOString();
115
+ }
116
+ function getStatus() {
117
+ return { npm: { ...npmStatus }, config: { ...configStatus }, lastCheckedAt };
118
+ }
119
+ return { check, getStatus };
120
+ }
@@ -0,0 +1,2 @@
1
+ export type DeploymentType = 'npm' | 'docker' | 'unknown';
2
+ export declare function detectDeployment(): DeploymentType;
@@ -0,0 +1,20 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ let cachedDeployment = null;
4
+ export function detectDeployment() {
5
+ if (cachedDeployment)
6
+ return cachedDeployment;
7
+ if (existsSync('/.dockerenv') || existsSync('/run/.containerenv')) {
8
+ cachedDeployment = 'docker';
9
+ return cachedDeployment;
10
+ }
11
+ try {
12
+ execSync('npm --version', { stdio: 'pipe', timeout: 3000 });
13
+ cachedDeployment = 'npm';
14
+ return cachedDeployment;
15
+ }
16
+ catch {
17
+ // 不缓存 unknown,下次调用时重试检测
18
+ return 'unknown';
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export declare function getInstalledVersion(): string;
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+ let cachedVersion = null;
5
+ export function getInstalledVersion() {
6
+ if (cachedVersion)
7
+ return cachedVersion;
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const pkgPath = path.resolve(__dirname, '../../package.json');
10
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
11
+ cachedVersion = pkg.version;
12
+ return cachedVersion;
13
+ }
@@ -1,4 +1,4 @@
1
- import { randomBytes, scryptSync } from "node:crypto";
1
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
2
2
  const SCRYPT_KEYLEN = 64;
3
3
  const SALT_BYTES = 16;
4
4
  export function hashPassword(password) {
@@ -11,5 +11,7 @@ export function verifyPassword(password, stored) {
11
11
  if (!salt || !hash)
12
12
  return false;
13
13
  const derived = scryptSync(password, salt, SCRYPT_KEYLEN).toString("hex");
14
- return derived === hash;
14
+ if (derived.length !== hash.length)
15
+ return false;
16
+ return timingSafeEqual(Buffer.from(derived), Buffer.from(hash));
15
17
  }
@@ -0,0 +1,9 @@
1
+ import Database from "better-sqlite3";
2
+ export type DashboardPeriod = "window" | "weekly" | "monthly";
3
+ export interface TimeRange {
4
+ startTime: string;
5
+ endTime: string;
6
+ }
7
+ export declare function resolveTimeRange(period: DashboardPeriod, db: Database.Database, routerKeyId?: string): TimeRange;
8
+ /** 从 date 对象中计算出当周的周一 */
9
+ export declare function getMonday(date: Date): Date;
@@ -0,0 +1,40 @@
1
+ import { getLatestWindow } from "../db/usage-windows.js";
2
+ import { toSqliteDatetime } from "./datetime.js";
3
+ // 5 小时窗口,与 usage-windows 的默认窗口时长对齐
4
+ const WINDOW_DURATION_MS = 5 * 3600_000;
5
+ export function resolveTimeRange(period, db, routerKeyId) {
6
+ const now = new Date();
7
+ switch (period) {
8
+ case "window": {
9
+ const latest = getLatestWindow(db, routerKeyId);
10
+ if (!latest) {
11
+ // 无窗口数据时回退到当前小时的 5h 区间
12
+ const start = new Date(now);
13
+ start.setMinutes(0, 0, 0);
14
+ return {
15
+ startTime: toSqliteDatetime(start),
16
+ endTime: toSqliteDatetime(new Date(start.getTime() + WINDOW_DURATION_MS)),
17
+ };
18
+ }
19
+ return { startTime: latest.start_time, endTime: latest.end_time };
20
+ }
21
+ case "weekly": {
22
+ const monday = getMonday(now);
23
+ monday.setHours(0, 0, 0, 0);
24
+ return { startTime: toSqliteDatetime(monday), endTime: toSqliteDatetime(now) };
25
+ }
26
+ case "monthly": {
27
+ const first = new Date(now.getFullYear(), now.getMonth(), 1);
28
+ return { startTime: toSqliteDatetime(first), endTime: toSqliteDatetime(now) };
29
+ }
30
+ }
31
+ }
32
+ /** 从 date 对象中计算出当周的周一 */
33
+ export function getMonday(date) {
34
+ const d = new Date(date);
35
+ const day = d.getDay();
36
+ // 周日 getDay()=0,需要回退到上周一
37
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
38
+ d.setDate(diff);
39
+ return d;
40
+ }
@@ -0,0 +1 @@
1
+ import{Ft as e,Nt as t,W as n,at as r,ct as i,q as a,r as o}from"./button-BQ3s7yNh.js";var s=[`data-size`],c=a({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(a){let c=a;return(l,u)=>(r(),n(`div`,{"data-slot":`card`,"data-size":a.size,class:e(t(o)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[i(l.$slots,`default`)],10,s))}}),l=a({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(r(),n(`div`,{"data-slot":`card-content`,class:e(t(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(a.$slots,`default`)],2))}});export{c as n,l as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,Nt as t,W as n,at as r,ct as i,q as a,r as o}from"./button-BQ3s7yNh.js";var s=a({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(r(),n(`div`,{"data-slot":`card-header`,class:e(t(o)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[i(a.$slots,`default`)],2))}}),c=a({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(r(),n(`div`,{"data-slot":`card-title`,class:e(t(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(a.$slots,`default`)],2))}});export{s as n,c as t};
@@ -0,0 +1 @@
1
+ import{B as e,H as t,I as n,It as r,K as i,L as a,Nt as o,U as s,Y as c,at as l,ct as u,et as d,i as f,m as p,o as m,q as h,r as g,ut as _,vt as v,x as y}from"./button-BQ3s7yNh.js";import{n as b,t as x}from"./ohash.D__AXeF1-b0PiKZB_.js";import{g as S,o as C,u as w,y as T}from"./Teleport-DdjYHlNK.js";import{n as E}from"./VisuallyHidden-Cyk-jWwh.js";import{t as D}from"./VisuallyHiddenInput-CYjNe_H8.js";import{t as O}from"./RovingFocusItem-BJoylAKU.js";function k(e,t){return S(e)?!1:Array.isArray(e)?e.some(e=>x(e,t)):x(e,t)}var[A,j]=T(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=T(`CheckboxRoot`),I=h({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(r,{emit:i}){let c=r,h=i,{forwardRef:g,currentElement:y}=m(),b=A(null),C=p(c,`modelValue`,h,{defaultValue:c.defaultValue??c.falseValue,passive:c.modelValue===void 0}),w=e(()=>b?.disabled.value||c.disabled),T=e(()=>x(C.value,c.trueValue)),j=e(()=>S(b?.modelValue.value)?C.value===`indeterminate`?`indeterminate`:T.value:k(b.modelValue.value,c.value));function P(){if(S(b?.modelValue.value))C.value===`indeterminate`?C.value=c.trueValue:C.value=T.value?c.falseValue:c.trueValue;else{let e=[...b.modelValue.value||[]];if(k(e,c.value)){let t=e.findIndex(e=>x(e,c.value));e.splice(t,1)}else e.push(c.value);b.modelValue.value=e}}let I=E(y),L=e(()=>c.id&&y.value?document.querySelector(`[for="${c.id}"]`)?.innerText:void 0);return F({disabled:w,state:j}),(e,r)=>(l(),t(_(o(b)?.rovingFocus.value?o(O):o(f)),d(e.$attrs,{id:e.id,ref:o(g),role:`checkbox`,"as-child":e.asChild,as:e.as,type:e.as===`button`?`button`:void 0,"aria-checked":o(M)(j.value)?`mixed`:j.value,"aria-required":e.required,"aria-label":e.$attrs[`aria-label`]||L.value,"data-state":o(N)(j.value),"data-disabled":w.value?``:void 0,disabled:w.value,focusable:o(b)?.rovingFocus.value?!w.value:void 0,onKeydown:n(a(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:v(()=>[u(e.$slots,`default`,{modelValue:o(C),state:j.value}),o(I)&&e.name&&!o(b)?(l(),t(o(D),{key:0,type:`checkbox`,checked:!!j.value,name:e.name,value:e.value,disabled:w.value,required:e.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):s(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=h({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:n}=m(),r=P();return(e,a)=>(l(),t(o(C),{present:e.forceMount||o(M)(o(r).state.value)||o(r).state.value===!0},{default:v(()=>[i(o(f),d({ref:o(n),"data-state":o(N)(o(r).state.value),"data-disabled":o(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:v(()=>[u(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=h({__name:`Checkbox`,props:{defaultValue:{},modelValue:{},disabled:{type:Boolean},value:{},id:{},trueValue:{},falseValue:{},asChild:{type:Boolean},as:{},name:{},required:{type:Boolean},class:{type:[Boolean,null,String,Object,Array]}},emits:[`update:modelValue`],setup(e,{emit:n}){let a=e,s=n,f=w(y(a,`class`),s);return(e,n)=>(l(),t(o(I),d({"data-slot":`checkbox`},o(f),{class:o(g)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,a.class)}),{default:v(t=>[i(o(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:v(()=>[u(e.$slots,`default`,r(c(t)),()=>[i(o(b))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{B as e,Et as t,H as n,It as r,K as i,Nt as a,U as o,Y as s,at as c,ct as l,d as u,et as d,ht as f,i as p,jt as m,m as h,o as g,q as _,rt as v,tt as y,vt as b}from"./button-BQ3s7yNh.js";import{c as x,o as S,u as C,y as w}from"./Teleport-DdjYHlNK.js";var[T,E]=w(`CollapsibleRoot`),D=_({__name:`CollapsibleRoot`,props:{defaultOpen:{type:Boolean,required:!1,default:!1},open:{type:Boolean,required:!1,default:void 0},disabled:{type:Boolean,required:!1},unmountOnHide:{type:Boolean,required:!1,default:!0},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`update:open`],setup(e,{expose:t,emit:r}){let i=e,o=h(i,`open`,r,{defaultValue:i.defaultOpen,passive:i.open===void 0}),{disabled:s,unmountOnHide:u}=m(i);return E({contentId:``,disabled:s,open:o,unmountOnHide:u,onOpenToggle:()=>{s.value||(o.value=!o.value)}}),t({open:o}),g(),(e,t)=>(c(),n(a(p),{as:e.as,"as-child":i.asChild,"data-state":a(o)?`open`:`closed`,"data-disabled":a(s)?``:void 0},{default:b(()=>[l(e.$slots,`default`,{open:a(o)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=_({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(r,{emit:s}){let m=r,h=s,_=T();_.contentId||=x(void 0,`reka-collapsible-content`);let C=t(),{forwardRef:w,currentElement:E}=g(),D=t(0),O=t(0),k=e(()=>_.open.value),A=t(k.value),j=t();f(()=>[k.value,C.value?.present],async()=>{await y();let e=E.value;if(!e)return;j.value=j.value||{transitionDuration:e.style.transitionDuration,animationName:e.style.animationName},e.style.transitionDuration=`0s`,e.style.animationName=`none`;let t=e.getBoundingClientRect();O.value=t.height,D.value=t.width,A.value||(e.style.transitionDuration=j.value.transitionDuration,e.style.animationName=j.value.animationName)},{immediate:!0});let M=e(()=>A.value&&_.open.value);return v(()=>{requestAnimationFrame(()=>{A.value=!1})}),u(E,`beforematch`,e=>{requestAnimationFrame(()=>{_.onOpenToggle(),h(`contentFound`)})}),(e,t)=>(c(),n(a(S),{ref_key:`presentRef`,ref:C,present:e.forceMount||a(_).open.value,"force-mount":!0},{default:b(({present:t})=>[i(a(p),d(e.$attrs,{id:a(_).contentId,ref:a(w),"as-child":m.asChild,as:e.as,hidden:t?void 0:a(_).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:a(_).open.value?`open`:`closed`,"data-disabled":a(_).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:b(()=>[!a(_).unmountOnHide.value||t?l(e.$slots,`default`,{key:0}):o(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=_({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(e){let t=e;g();let r=T();return(e,i)=>(c(),n(a(p),{type:e.as===`button`?`button`:void 0,as:e.as,"as-child":t.asChild,"aria-controls":a(r).contentId,"aria-expanded":a(r).open.value,"data-state":a(r).open.value?`open`:`closed`,"data-disabled":a(r).disabled?.value?``:void 0,disabled:a(r).disabled?.value,onClick:a(r).onOpenToggle},{default:b(()=>[l(e.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),A=_({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let i=C(e,t);return(e,t)=>(c(),n(a(D),d({"data-slot":`collapsible`},a(i)),{default:b(t=>[l(e.$slots,`default`,r(s(t)))]),_:3},16))}}),j=_({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(c(),n(a(O),d({"data-slot":`collapsible-content`},t),{default:b(()=>[l(e.$slots,`default`)]),_:3},16))}}),M=_({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(c(),n(a(k),d({"data-slot":`collapsible-trigger`},t),{default:b(()=>[l(e.$slots,`default`)]),_:3},16))}});export{j as n,A as r,M as t};
@@ -0,0 +1 @@
1
+ import{B as e,Ct as t,Et as n,X as r,Z as i,a,gt as o,ht as s,ot as c,q as l,u}from"./button-BQ3s7yNh.js";import{h as d}from"./Teleport-DdjYHlNK.js";function f(t){let r=d({dir:n(`ltr`)});return e(()=>t?.value||r.dir?.value||`ltr`)}function p(){let t=n();return{primitiveElement:t,currentElement:e(()=>[`#text`,`#comment`].includes(t.value?.$el.nodeName)?t.value?.$el.nextElementSibling:u(t))}}var m=`data-reka-collection-item`;function h(u={}){let{key:d=``,isProvider:f=!1}=u,h=`${d}CollectionProvider`,g;if(f){let e=n(new Map);g={collectionRef:n(),itemMap:e},c(h,g)}else g=i(h);let _=(e=!1)=>{let t=g.collectionRef.value;if(!t)return[];let n=Array.from(t.querySelectorAll(`[${m}]`)),r=Array.from(g.itemMap.value.values()).sort((e,t)=>n.indexOf(e.ref)-n.indexOf(t.ref));return e?r:r.filter(e=>e.ref.dataset.disabled!==``)},v=l({name:`CollectionSlot`,inheritAttrs:!1,setup(e,{slots:t,attrs:n}){let{primitiveElement:i,currentElement:o}=p();return s(o,()=>{g.collectionRef.value=o.value}),()=>r(a,{ref:i,...n},t)}}),y=l({name:`CollectionItem`,inheritAttrs:!1,props:{value:{validator:()=>!0}},setup(e,{slots:n,attrs:i}){let{primitiveElement:s,currentElement:c}=p();return o(n=>{if(c.value){let r=t(c.value);g.itemMap.value.set(r,{ref:c.value,value:e.value}),n(()=>g.itemMap.value.delete(r))}}),()=>r(a,{...i,[m]:``,ref:s},n)}});return{getItems:_,reactiveItems:e(()=>Array.from(g.itemMap.value.values())),itemMapSize:e(()=>g.itemMap.value.size),CollectionSlot:v,CollectionItem:y}}export{p as n,f as r,h as t};