llm-simple-router 0.5.2 → 0.5.4

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 (149) hide show
  1. package/config/recommended-providers.json +234 -19
  2. package/dist/admin/api-response.d.ts +0 -1
  3. package/dist/admin/api-response.js +8 -4
  4. package/dist/admin/groups.js +35 -0
  5. package/dist/admin/monitor.js +2 -0
  6. package/dist/admin/providers.js +188 -22
  7. package/dist/admin/proxy-enhancement.js +9 -9
  8. package/dist/config/model-context.d.ts +10 -0
  9. package/dist/config/model-context.js +105 -0
  10. package/dist/db/index.d.ts +3 -1
  11. package/dist/db/index.js +2 -1
  12. package/dist/db/logs.d.ts +4 -0
  13. package/dist/db/logs.js +7 -3
  14. package/dist/db/mappings.d.ts +2 -1
  15. package/dist/db/mappings.js +2 -2
  16. package/dist/db/migrations/023_create_provider_model_info.sql +8 -0
  17. package/dist/db/migrations/024_add_mapping_groups_is_active.sql +1 -0
  18. package/dist/db/migrations/025_add_client_status_code.sql +3 -0
  19. package/dist/db/model-info.d.ts +14 -0
  20. package/dist/db/model-info.js +27 -0
  21. package/dist/db/providers.d.ts +1 -0
  22. package/dist/db/providers.js +1 -1
  23. package/dist/index.js +15 -3
  24. package/dist/middleware/auth.js +1 -1
  25. package/dist/monitor/request-tracker.d.ts +2 -0
  26. package/dist/monitor/request-tracker.js +18 -0
  27. package/dist/proxy/anthropic.js +13 -0
  28. package/dist/proxy/enhancement/directive-parser.d.ts +8 -2
  29. package/dist/proxy/enhancement/directive-parser.js +44 -17
  30. package/dist/proxy/enhancement/enhancement-handler.js +184 -54
  31. package/dist/proxy/enhancement/index.d.ts +1 -1
  32. package/dist/proxy/enhancement/index.js +1 -1
  33. package/dist/proxy/enhancement-config.d.ts +6 -0
  34. package/dist/proxy/enhancement-config.js +19 -0
  35. package/dist/proxy/openai.js +40 -3
  36. package/dist/proxy/overflow.d.ts +18 -0
  37. package/dist/proxy/overflow.js +128 -0
  38. package/dist/proxy/patch/deepseek/index.d.ts +6 -0
  39. package/dist/proxy/patch/deepseek/index.js +11 -0
  40. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +12 -0
  41. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +90 -0
  42. package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +6 -0
  43. package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +24 -0
  44. package/dist/proxy/patch/index.d.ts +9 -0
  45. package/dist/proxy/patch/index.js +17 -0
  46. package/dist/proxy/proxy-core.d.ts +9 -2
  47. package/dist/proxy/proxy-core.js +24 -2
  48. package/dist/proxy/proxy-handler.js +34 -9
  49. package/dist/proxy/proxy-logging.js +23 -2
  50. package/dist/proxy/resilience.d.ts +4 -0
  51. package/dist/proxy/resilience.js +8 -1
  52. package/dist/proxy/strategy/types.d.ts +2 -0
  53. package/dist/proxy/stream-proxy.js +2 -1
  54. package/dist/proxy/transport-fn.js +3 -2
  55. package/dist/proxy/transport.js +3 -2
  56. package/dist/proxy/types.d.ts +3 -1
  57. package/dist/proxy/types.js +5 -1
  58. package/dist/upgrade/checker.js +5 -2
  59. package/dist/utils/time-range.js +28 -13
  60. package/frontend-dist/assets/CardContent-GNY_j_L3.js +1 -0
  61. package/frontend-dist/assets/CardTitle-BhXJbSoh.js +1 -0
  62. package/frontend-dist/assets/Checkbox-n_sh6Lvx.js +1 -0
  63. package/frontend-dist/assets/CollapsibleTrigger-DDCUOXDR.js +1 -0
  64. package/frontend-dist/assets/Collection-DbtqQ1jF.js +1 -0
  65. package/frontend-dist/assets/Dashboard-Dy9frcgO.js +3 -0
  66. package/frontend-dist/assets/DialogTitle-BEWUnuJQ.js +1 -0
  67. package/frontend-dist/assets/{Input-O0ebU-Va.js → Input-CmibY9Fx.js} +1 -1
  68. package/frontend-dist/assets/Label-Cs__wFH0.js +1 -0
  69. package/frontend-dist/assets/Login-BciEc1TW.js +1 -0
  70. package/frontend-dist/assets/Logs-BkqwWW0-.js +1 -0
  71. package/frontend-dist/assets/ModelMappings-DrCJ_TCf.js +1 -0
  72. package/frontend-dist/assets/Monitor-C-b4qyuI.js +1 -0
  73. package/frontend-dist/assets/PopoverTrigger-DaKOMSVs.js +1 -0
  74. package/frontend-dist/assets/PopperContent-DZ6plcjf.js +1 -0
  75. package/frontend-dist/assets/Providers-u8utX74M.js +1 -0
  76. package/frontend-dist/assets/ProxyEnhancement-8_xhndGt.js +5 -0
  77. package/frontend-dist/assets/RetryRules-D1psYDEP.js +1 -0
  78. package/frontend-dist/assets/RouterKeys-ovPFGhjy.js +1 -0
  79. package/frontend-dist/assets/RovingFocusItem-Dsv9AkP7.js +1 -0
  80. package/frontend-dist/assets/SelectValue-BoUWfZAg.js +1 -0
  81. package/frontend-dist/assets/Settings-DXF-6A8C.js +6 -0
  82. package/frontend-dist/assets/Setup-rVLqiz0d.js +1 -0
  83. package/frontend-dist/assets/Switch-po5ZVBE3.js +1 -0
  84. package/frontend-dist/assets/TableHeader-Zyvq_0p2.js +1 -0
  85. package/frontend-dist/assets/{TabsTrigger-CPCi2HIa.js → TabsTrigger-CgDhZGkT.js} +1 -1
  86. package/frontend-dist/assets/Teleport-CgTHarey.js +3 -0
  87. package/frontend-dist/assets/TooltipTrigger-C2qO21dQ.js +1 -0
  88. package/frontend-dist/assets/UnifiedRequestDialog-Dksad8eN.js +3 -0
  89. package/frontend-dist/assets/{VisuallyHidden-Cyk-jWwh.js → VisuallyHidden-fbPmoMwi.js} +1 -1
  90. package/frontend-dist/assets/VisuallyHiddenInput-7j8wkPrW.js +1 -0
  91. package/frontend-dist/assets/alert-dialog-DbT3PzoF.js +1 -0
  92. package/frontend-dist/assets/badge-BVxnlnsH.js +1 -0
  93. package/frontend-dist/assets/{button-BQ3s7yNh.js → button-BCrIpNwA.js} +2 -2
  94. package/frontend-dist/assets/chevron-down-CWBwGxSp.js +1 -0
  95. package/frontend-dist/assets/circle-question-mark-DRkkqjgG.js +1 -0
  96. package/frontend-dist/assets/dialog-BNlCZpHK.js +1 -0
  97. package/frontend-dist/assets/file-text-BavS6SrF.js +1 -0
  98. package/frontend-dist/assets/format-K3VR67cG.js +1 -0
  99. package/frontend-dist/assets/index-BP4imfye.css +1 -0
  100. package/frontend-dist/assets/index-DrBJPq6d.js +1 -0
  101. package/frontend-dist/assets/lib-CGpNhf06.js +1 -0
  102. package/frontend-dist/assets/loader-circle-Cpd89XQ7.js +1 -0
  103. package/frontend-dist/assets/ohash.D__AXeF1-DkJnWU8a.js +1 -0
  104. package/frontend-dist/assets/{useClipboard-Cnnz6AAN.js → useClipboard-Bq8yZunx.js} +1 -1
  105. package/frontend-dist/assets/useLogRetention-BWPm3G_A.js +1 -0
  106. package/frontend-dist/assets/useNonce-D5lpSPNk.js +1 -0
  107. package/frontend-dist/assets/x-BFIp7DLt.js +1 -0
  108. package/frontend-dist/index.html +20 -17
  109. package/package.json +2 -1
  110. package/frontend-dist/assets/CardContent-WrBnGhTg.js +0 -1
  111. package/frontend-dist/assets/CardTitle-BcDYk7cq.js +0 -1
  112. package/frontend-dist/assets/Checkbox-MZf0YsDG.js +0 -1
  113. package/frontend-dist/assets/CollapsibleTrigger-CrOH9HlW.js +0 -1
  114. package/frontend-dist/assets/Collection-DcTx_Y54.js +0 -1
  115. package/frontend-dist/assets/Dashboard-D0oDrSLr.js +0 -3
  116. package/frontend-dist/assets/DialogTitle-Cl5Cd7QH.js +0 -1
  117. package/frontend-dist/assets/Label-C_S0y7Um.js +0 -1
  118. package/frontend-dist/assets/Login-DGY7uF8P.js +0 -1
  119. package/frontend-dist/assets/Logs-ls8pv89b.js +0 -1
  120. package/frontend-dist/assets/ModelMappings-DGlf0S4s.js +0 -1
  121. package/frontend-dist/assets/Monitor-BSI87grz.js +0 -1
  122. package/frontend-dist/assets/PopperContent-C6Q7hDmf.js +0 -1
  123. package/frontend-dist/assets/Providers-ZkRpj8_m.js +0 -1
  124. package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +0 -5
  125. package/frontend-dist/assets/RetryRules-DtM31qsl.js +0 -1
  126. package/frontend-dist/assets/RouterKeys-D63tRFKm.js +0 -1
  127. package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +0 -1
  128. package/frontend-dist/assets/SelectValue-CLp5z6_I.js +0 -1
  129. package/frontend-dist/assets/Settings-DSgRKbTQ.js +0 -6
  130. package/frontend-dist/assets/Setup-BDmj6CRk.js +0 -1
  131. package/frontend-dist/assets/Switch-Wz-t_zkv.js +0 -1
  132. package/frontend-dist/assets/TableHeader-DGtcqGkw.js +0 -1
  133. package/frontend-dist/assets/Teleport-DdjYHlNK.js +0 -3
  134. package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +0 -1
  135. package/frontend-dist/assets/UnifiedRequestDialog-BAAfMJJl.js +0 -3
  136. package/frontend-dist/assets/VisuallyHiddenInput-CYjNe_H8.js +0 -1
  137. package/frontend-dist/assets/alert-dialog-Bi3dliLl.js +0 -1
  138. package/frontend-dist/assets/badge-Kkta3e9W.js +0 -1
  139. package/frontend-dist/assets/createLucideIcon-D1tkPDOQ.js +0 -1
  140. package/frontend-dist/assets/dialog-DoIATUYw.js +0 -1
  141. package/frontend-dist/assets/file-text-Dt6QP1bZ.js +0 -1
  142. package/frontend-dist/assets/format-DOVIVsQC.js +0 -1
  143. package/frontend-dist/assets/index-BY0E7CHR.js +0 -1
  144. package/frontend-dist/assets/index-Bnrh1mFY.css +0 -1
  145. package/frontend-dist/assets/lib-CxwxnlwW.js +0 -1
  146. package/frontend-dist/assets/ohash.D__AXeF1-b0PiKZB_.js +0 -1
  147. package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +0 -1
  148. package/frontend-dist/assets/useNonce-DKbOCfgM.js +0 -1
  149. package/frontend-dist/assets/x-CAoitXRt.js +0 -1
@@ -67,6 +67,10 @@ export class ResilienceLayer {
67
67
  if (result.kind === "stream_abort") {
68
68
  return { action: "abort", reason: "stream_abort" };
69
69
  }
70
+ // stream_error + statusCode < failoverThreshold -> 上游返回 200 但 body 包含错误内容(early error),不可恢复
71
+ if (result.kind === "stream_error" && result.statusCode < config.failoverThreshold) {
72
+ return { action: "abort", reason: "stream_error" };
73
+ }
70
74
  // success + statusCode < failoverThreshold -> done
71
75
  if ((result.kind === "success" || result.kind === "stream_success") &&
72
76
  result.statusCode < config.failoverThreshold) {
@@ -158,6 +162,7 @@ export class ResilienceLayer {
158
162
  target: currentTarget, attemptIndex: globalAttemptIndex,
159
163
  statusCode: null, error: throwErr instanceof Error ? throwErr.message : String(throwErr),
160
164
  latencyMs: Date.now() - start, responseBody: null,
165
+ responseHeaders: null, resultKind: transportResult.kind,
161
166
  });
162
167
  }
163
168
  else {
@@ -165,6 +170,8 @@ export class ResilienceLayer {
165
170
  target: currentTarget, attemptIndex: globalAttemptIndex,
166
171
  statusCode: transportResult.statusCode, error: null,
167
172
  latencyMs: Date.now() - start, responseBody: extractBody(transportResult),
173
+ responseHeaders: extractHeaders(transportResult) ?? null,
174
+ resultKind: transportResult.kind,
168
175
  });
169
176
  }
170
177
  const state = {
@@ -186,7 +193,7 @@ export class ResilienceLayer {
186
193
  // 跨 provider failover 需要切换信号量,抛出异常让上层处理
187
194
  const nextAvail = targets().filter(t => !excludedTargets.some(e => e.backend_model === t.backend_model && e.provider_id === t.provider_id));
188
195
  if (nextAvail.length > 0 && nextAvail[0].provider_id !== currentTarget.provider_id) {
189
- throw new ProviderSwitchNeeded(nextAvail[0].provider_id);
196
+ throw new ProviderSwitchNeeded(nextAvail[0].provider_id, [...allAttempts], transportResult);
190
197
  }
191
198
  continue;
192
199
  case "abort":
@@ -7,6 +7,8 @@ export declare const STRATEGY_NAMES: {
7
7
  export interface Target {
8
8
  backend_model: string;
9
9
  provider_id: string;
10
+ overflow_provider_id?: string;
11
+ overflow_model?: string;
10
12
  }
11
13
  export interface ResolveContext {
12
14
  now: Date;
@@ -1,5 +1,6 @@
1
1
  import { PassThrough } from "stream";
2
2
  import { UPSTREAM_SUCCESS, filterHeaders } from "./types.js";
3
+ import { buildUpstreamUrl } from "./proxy-core.js";
3
4
  import { _transportInternals, buildRequestOptions, } from "./transport.js";
4
5
  const UPSTREAM_BAD_GATEWAY = 502;
5
6
  const BUFFER_SIZE_LIMIT = 4096;
@@ -224,7 +225,7 @@ class StreamProxy {
224
225
  export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve) {
225
226
  return new Promise((resolve) => {
226
227
  const effectiveResolve = compatResolve ?? resolve;
227
- const url = new URL(`${backend.base_url}${upstreamPath}`);
228
+ const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
228
229
  const payload = JSON.stringify(body);
229
230
  const upstreamHeaders = buildHeaders(clientHeaders, apiKey, Buffer.byteLength(payload));
230
231
  const options = buildRequestOptions(url, upstreamHeaders);
@@ -17,6 +17,7 @@ function toStreamMetrics(m) {
17
17
  };
18
18
  }
19
19
  export function buildTransportFn(p) {
20
+ const buildHeaders = (cliHdrs, key, bytes) => buildUpstreamHeaders(cliHdrs, key, bytes, p.apiType);
20
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
21
22
  return async (_target) => {
22
23
  if (p.isStream) {
@@ -25,14 +26,14 @@ export function buildTransportFn(p) {
25
26
  onChunk: (rawLine) => { p.tracker?.appendStreamChunk(p.logId, rawLine, p.apiType, STREAM_CONTENT_MAX_RAW, STREAM_CONTENT_MAX_TEXT); },
26
27
  });
27
28
  const checkEarlyError = p.matcher ? (data) => p.matcher.test(UPSTREAM_SUCCESS, data) : undefined;
28
- const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildUpstreamHeaders, metricsTransform, checkEarlyError);
29
+ const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError);
29
30
  const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
30
31
  ? streamResult.metrics : undefined;
31
32
  if (m)
32
33
  p.tracker?.update(p.logId, { streamMetrics: toStreamMetrics(m) });
33
34
  return streamResult;
34
35
  }
35
- const result = await callNonStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.upstreamPath, buildUpstreamHeaders);
36
+ const result = await callNonStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.upstreamPath, buildHeaders);
36
37
  if (result.kind === "success") {
37
38
  const mr = MetricsExtractor.fromNonStreamResponse(p.apiType, result.body);
38
39
  if (mr)
@@ -1,6 +1,7 @@
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 { buildUpstreamUrl } from "./proxy-core.js";
4
5
  // Re-export callStream from stream-proxy.ts for external consumers
5
6
  export { callStream } from "./stream-proxy.js";
6
7
  // ---------- Constants ----------
@@ -28,7 +29,7 @@ export function buildRequestOptions(url, headers, method = "POST") {
28
29
  // ---------- callNonStream ----------
29
30
  export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath, buildHeaders) {
30
31
  return new Promise((resolve) => {
31
- const url = new URL(`${backend.base_url}${upstreamPath}`);
32
+ const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
32
33
  const payload = JSON.stringify(body);
33
34
  const upstreamHeaders = buildHeaders(clientHeaders, apiKey, Buffer.byteLength(payload));
34
35
  const options = buildRequestOptions(url, upstreamHeaders);
@@ -69,7 +70,7 @@ export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath
69
70
  }
70
71
  export function callGet(backend, apiKey, clientHeaders, upstreamPath, buildHeaders) {
71
72
  return new Promise((resolve, reject) => {
72
- const url = new URL(`${backend.base_url}${upstreamPath}`);
73
+ const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
73
74
  const headers = buildHeaders(clientHeaders, apiKey);
74
75
  const options = buildRequestOptions(url, headers, "GET");
75
76
  const req = _transportInternals.createUpstreamRequest(url, options);
@@ -52,7 +52,9 @@ export type TransportResult = {
52
52
  */
53
53
  export declare class ProviderSwitchNeeded extends Error {
54
54
  readonly targetProviderId: string;
55
- constructor(targetProviderId: string);
55
+ readonly attempts?: import("./resilience.js").ResilienceAttempt[] | undefined;
56
+ readonly lastResult?: TransportResult | undefined;
57
+ constructor(targetProviderId: string, attempts?: import("./resilience.js").ResilienceAttempt[] | undefined, lastResult?: TransportResult | undefined);
56
58
  }
57
59
  /** 流式传输阶段状态 */
58
60
  export type StreamState = "BUFFERING" | "STREAMING" | "COMPLETED" | "EARLY_ERROR" | "ABORTED";
@@ -22,9 +22,13 @@ export function filterHeaders(raw) {
22
22
  */
23
23
  export class ProviderSwitchNeeded extends Error {
24
24
  targetProviderId;
25
- constructor(targetProviderId) {
25
+ attempts;
26
+ lastResult;
27
+ constructor(targetProviderId, attempts, lastResult) {
26
28
  super(`Provider switch needed: ${targetProviderId}`);
27
29
  this.targetProviderId = targetProviderId;
30
+ this.attempts = attempts;
31
+ this.lastResult = lastResult;
28
32
  this.name = "ProviderSwitchNeeded";
29
33
  }
30
34
  }
@@ -6,6 +6,9 @@ import { getInstalledVersion } from './version.js';
6
6
  const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/llm-simple-router';
7
7
  const DEFAULT_GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
8
8
  const CHECK_TIMEOUT_MS = 5000;
9
+ const HTTP_STATUS_REDIRECT_LOWER = 300;
10
+ const HTTP_STATUS_REDIRECT_UPPER = 400;
11
+ const HTTP_STATUS_OK_LOWER = 200;
9
12
  const MAX_REDIRECTS = 5;
10
13
  export async function fetchJson(url, redirects = 0) {
11
14
  if (redirects > MAX_REDIRECTS)
@@ -14,13 +17,13 @@ export async function fetchJson(url, redirects = 0) {
14
17
  return new Promise((resolve, reject) => {
15
18
  const timer = setTimeout(() => reject(new Error('timeout')), CHECK_TIMEOUT_MS);
16
19
  mod.get(url, (res) => {
17
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
20
+ if (res.statusCode && res.statusCode >= HTTP_STATUS_REDIRECT_LOWER && res.statusCode < HTTP_STATUS_REDIRECT_UPPER && res.headers.location) {
18
21
  clearTimeout(timer);
19
22
  res.resume();
20
23
  fetchJson(res.headers.location, redirects + 1).then(resolve, reject);
21
24
  return;
22
25
  }
23
- if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
26
+ if (res.statusCode && (res.statusCode < HTTP_STATUS_OK_LOWER || res.statusCode >= HTTP_STATUS_REDIRECT_LOWER)) {
24
27
  clearTimeout(timer);
25
28
  res.resume();
26
29
  reject(new Error(`HTTP ${res.statusCode}`));
@@ -1,20 +1,21 @@
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;
1
+ import { randomUUID } from "crypto";
2
+ import { getLatestWindow, insertWindow } from "../db/usage-windows.js";
3
+ import { toSqliteDatetime, parseSqliteDatetime } from "./datetime.js";
4
+ const WINDOW_HOURS = 5;
5
+ const MS_PER_HOUR = 3600_000;
6
+ // 与 usage-windows 的默认窗口时长对齐
7
+ const WINDOW_DURATION_MS = WINDOW_HOURS * MS_PER_HOUR;
5
8
  export function resolveTimeRange(period, db, routerKeyId) {
6
9
  const now = new Date();
7
10
  switch (period) {
8
11
  case "window": {
9
12
  const latest = getLatestWindow(db, routerKeyId);
10
13
  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
- };
14
+ return createAndReturnWindow(db, now, routerKeyId);
15
+ }
16
+ // 最新窗口已过期(无请求触发新窗口创建),主动补齐
17
+ if (now > parseSqliteDatetime(latest.end_time)) {
18
+ return createAndReturnWindow(db, now, routerKeyId);
18
19
  }
19
20
  return { startTime: latest.start_time, endTime: latest.end_time };
20
21
  }
@@ -33,8 +34,22 @@ export function resolveTimeRange(period, db, routerKeyId) {
33
34
  export function getMonday(date) {
34
35
  const d = new Date(date);
35
36
  const day = d.getDay();
36
- // 周日 getDay()=0,需要回退到上周一
37
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
37
+ // 周日 getDay()=0,需要回退到上周一;+1 将周日=0 映射到周一=1 基准
38
+ const SUNDAY_OFFSET = -6;
39
+ const MONDAY_BASE = 1;
40
+ const diff = d.getDate() - day + (day === 0 ? SUNDAY_OFFSET : MONDAY_BASE);
38
41
  d.setDate(diff);
39
42
  return d;
40
43
  }
44
+ function createAndReturnWindow(db, now, routerKeyId) {
45
+ const start = new Date(now);
46
+ start.setMinutes(0, 0, 0);
47
+ const end = new Date(start.getTime() + WINDOW_DURATION_MS);
48
+ insertWindow(db, {
49
+ id: randomUUID(),
50
+ router_key_id: routerKeyId ?? null,
51
+ start_time: toSqliteDatetime(start),
52
+ end_time: toSqliteDatetime(end),
53
+ });
54
+ return { startTime: toSqliteDatetime(start), endTime: toSqliteDatetime(end) };
55
+ }
@@ -0,0 +1 @@
1
+ import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-BCrIpNwA.js";var s=[`data-size`],c=n({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(n){let c=n;return(l,u)=>(a(),e(`div`,{"data-slot":`card`,"data-size":n.size,class:t(r(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=n({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-content`,class:t(r(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(n.$slots,`default`)],2))}});export{c as n,l as t};
@@ -0,0 +1 @@
1
+ import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-BCrIpNwA.js";var s=n({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-header`,class:t(r(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(n.$slots,`default`)],2))}}),c=n({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-title`,class:t(r(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(n.$slots,`default`)],2))}});export{s as n,c as t};
@@ -0,0 +1 @@
1
+ import{J as e,L as t,Lt as n,Pt as r,R as i,U as a,V as o,W as s,X as c,dt as l,i as u,lt as d,m as f,o as p,ot as m,q as h,r as g,tt as _,x as v,yt as y}from"./button-BCrIpNwA.js";import{g as b,o as x,u as S,y as C}from"./Teleport-CgTHarey.js";import{n as w,t as T}from"./ohash.D__AXeF1-DkJnWU8a.js";import{n as E}from"./VisuallyHidden-fbPmoMwi.js";import{t as D}from"./VisuallyHiddenInput-7j8wkPrW.js";import{t as O}from"./RovingFocusItem-Dsv9AkP7.js";function k(e,t){return b(e)?!1:Array.isArray(e)?e.some(e=>T(e,t)):T(e,t)}var[A,j]=C(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=C(`CheckboxRoot`),I=e({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(e,{emit:n}){let c=e,h=n,{forwardRef:g,currentElement:v}=p(),x=A(null),S=f(c,`modelValue`,h,{defaultValue:c.defaultValue??c.falseValue,passive:c.modelValue===void 0}),C=o(()=>x?.disabled.value||c.disabled),w=o(()=>T(S.value,c.trueValue)),j=o(()=>b(x?.modelValue.value)?S.value===`indeterminate`?`indeterminate`:w.value:k(x.modelValue.value,c.value));function P(){if(b(x?.modelValue.value))S.value===`indeterminate`?S.value=c.trueValue:S.value=w.value?c.falseValue:c.trueValue;else{let e=[...x.modelValue.value||[]];if(k(e,c.value)){let t=e.findIndex(e=>T(e,c.value));e.splice(t,1)}else e.push(c.value);x.modelValue.value=e}}let I=E(v),L=o(()=>c.id&&v.value?document.querySelector(`[for="${c.id}"]`)?.innerText:void 0);return F({disabled:C,state:j}),(e,n)=>(m(),a(l(r(x)?.rovingFocus.value?r(O):r(u)),_(e.$attrs,{id:e.id,ref:r(g),role:`checkbox`,"as-child":e.asChild,as:e.as,type:e.as===`button`?`button`:void 0,"aria-checked":r(M)(j.value)?`mixed`:j.value,"aria-required":e.required,"aria-label":e.$attrs[`aria-label`]||L.value,"data-state":r(N)(j.value),"data-disabled":C.value?``:void 0,disabled:C.value,focusable:r(x)?.rovingFocus.value?!C.value:void 0,onKeydown:t(i(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:y(()=>[d(e.$slots,`default`,{modelValue:r(S),state:j.value}),r(I)&&e.name&&!r(x)?(m(),a(r(D),{key:0,type:`checkbox`,checked:!!j.value,name:e.name,value:e.value,disabled:C.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=e({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:t}=p(),n=P();return(e,i)=>(m(),a(r(x),{present:e.forceMount||r(M)(r(n).state.value)||r(n).state.value===!0},{default:y(()=>[h(r(u),_({ref:r(t),"data-state":r(N)(r(n).state.value),"data-disabled":r(n).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:y(()=>[d(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=e({__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:t}){let i=e,o=t,s=S(v(i,`class`),o);return(e,t)=>(m(),a(r(I),_({"data-slot":`checkbox`},r(s),{class:r(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`,i.class)}),{default:y(t=>[h(r(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:y(()=>[d(e.$slots,`default`,n(c(t)),()=>[h(r(w))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{Dt as e,J as t,Lt as n,Mt as r,Pt as i,U as a,V as o,W as s,X as c,d as l,gt as u,i as d,it as f,lt as p,m,nt as h,o as g,ot as _,q as v,tt as y,yt as b}from"./button-BCrIpNwA.js";import{c as x,o as S,u as C,y as w}from"./Teleport-CgTHarey.js";var[T,E]=w(`CollapsibleRoot`),D=t({__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:n}){let o=e,s=m(o,`open`,n,{defaultValue:o.defaultOpen,passive:o.open===void 0}),{disabled:c,unmountOnHide:l}=r(o);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),t({open:s}),g(),(e,t)=>(_(),a(i(d),{as:e.as,"as-child":o.asChild,"data-state":i(s)?`open`:`closed`,"data-disabled":i(c)?``:void 0},{default:b(()=>[p(e.$slots,`default`,{open:i(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=t({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(t,{emit:n}){let r=t,c=n,m=T();m.contentId||=x(void 0,`reka-collapsible-content`);let C=e(),{forwardRef:w,currentElement:E}=g(),D=e(0),O=e(0),k=o(()=>m.open.value),A=e(k.value),j=e();u(()=>[k.value,C.value?.present],async()=>{await h();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=o(()=>A.value&&m.open.value);return f(()=>{requestAnimationFrame(()=>{A.value=!1})}),l(E,`beforematch`,e=>{requestAnimationFrame(()=>{m.onOpenToggle(),c(`contentFound`)})}),(e,t)=>(_(),a(i(S),{ref_key:`presentRef`,ref:C,present:e.forceMount||i(m).open.value,"force-mount":!0},{default:b(({present:t})=>[v(i(d),y(e.$attrs,{id:i(m).contentId,ref:i(w),"as-child":r.asChild,as:e.as,hidden:t?void 0:i(m).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:i(m).open.value?`open`:`closed`,"data-disabled":i(m).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:b(()=>[!i(m).unmountOnHide.value||t?p(e.$slots,`default`,{key:0}):s(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=t({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(e){let t=e;g();let n=T();return(e,r)=>(_(),a(i(d),{type:e.as===`button`?`button`:void 0,as:e.as,"as-child":t.asChild,"aria-controls":i(n).contentId,"aria-expanded":i(n).open.value,"data-state":i(n).open.value?`open`:`closed`,"data-disabled":i(n).disabled?.value?``:void 0,disabled:i(n).disabled?.value,onClick:i(n).onOpenToggle},{default:b(()=>[p(e.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),A=t({__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 r=C(e,t);return(e,t)=>(_(),a(i(D),y({"data-slot":`collapsible`},i(r)),{default:b(t=>[p(e.$slots,`default`,n(c(t)))]),_:3},16))}}),j=t({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,n)=>(_(),a(i(O),y({"data-slot":`collapsible-content`},t),{default:b(()=>[p(e.$slots,`default`)]),_:3},16))}}),M=t({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,n)=>(_(),a(i(k),y({"data-slot":`collapsible-trigger`},t),{default:b(()=>[p(e.$slots,`default`)]),_:3},16))}});export{j as n,A as r,M as t};
@@ -0,0 +1 @@
1
+ import{Dt as e,J as t,Q as n,V as r,Z as i,_t as a,a as o,gt as s,st as c,u as l,wt as u}from"./button-BCrIpNwA.js";import{h as d}from"./Teleport-CgTHarey.js";function f(t){let n=d({dir:e(`ltr`)});return r(()=>t?.value||n.dir?.value||`ltr`)}function p(){let t=e();return{primitiveElement:t,currentElement:r(()=>[`#text`,`#comment`].includes(t.value?.$el.nodeName)?t.value?.$el.nextElementSibling:l(t))}}var m=`data-reka-collection-item`;function h(l={}){let{key:d=``,isProvider:f=!1}=l,h=`${d}CollectionProvider`,g;if(f){let t=e(new Map);g={collectionRef:e(),itemMap:t},c(h,g)}else g=n(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=t({name:`CollectionSlot`,inheritAttrs:!1,setup(e,{slots:t,attrs:n}){let{primitiveElement:r,currentElement:a}=p();return s(a,()=>{g.collectionRef.value=a.value}),()=>i(o,{ref:r,...n},t)}}),y=t({name:`CollectionItem`,inheritAttrs:!1,props:{value:{validator:()=>!0}},setup(e,{slots:t,attrs:n}){let{primitiveElement:r,currentElement:s}=p();return a(t=>{if(s.value){let n=u(s.value);g.itemMap.value.set(n,{ref:s.value,value:e.value}),t(()=>g.itemMap.value.delete(n))}}),()=>i(o,{...n,[m]:``,ref:r},t)}});return{getItems:_,reactiveItems:r(()=>Array.from(g.itemMap.value.values())),itemMapSize:r(()=>g.itemMap.value.size),CollectionSlot:v,CollectionItem:y}}export{p as n,f as r,h as t};