llm-simple-router 0.9.16 → 0.9.18
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/dist/admin/quick-setup.js +1 -1
- package/dist/admin/schedules.js +4 -2
- package/dist/admin/settings.js +1 -1
- package/dist/config/model-context.js +5 -1
- package/dist/db/metrics.js +4 -2
- package/dist/db/transform-rules.js +1 -1
- package/dist/metrics/sse-metrics-transform.js +1 -1
- package/dist/proxy/handler/proxy-handler.js +1 -1
- package/dist/proxy/routing/mapping-resolver.js +1 -1
- package/dist/proxy/transform/plugin-registry.js +5 -5
- package/dist/proxy/transform/request-transform-responses.js +2 -1
- package/dist/proxy/transform/response-transform-responses.js +2 -1
- package/dist/proxy/transform/stream-ant2resp.js +11 -8
- package/dist/proxy/transform/stream-bridge-chat2resp.js +6 -5
- package/dist/proxy/transport/stream.js +3 -3
- package/dist/upgrade/checker.js +0 -2
- package/dist/utils/time-range.js +8 -3
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ const QuickSetupSchema = Type.Object({
|
|
|
52
52
|
transform_rules: Type.Optional(QuickSetupTransformSchema),
|
|
53
53
|
});
|
|
54
54
|
export const adminQuickSetupRoutes = (app, options, done) => {
|
|
55
|
-
const { db,
|
|
55
|
+
const { db, tracker, adaptiveController } = options;
|
|
56
56
|
app.post("/admin/api/quick-setup", { schema: { body: QuickSetupSchema } }, async (request, reply) => {
|
|
57
57
|
const body = request.body;
|
|
58
58
|
// 1. Validate provider name
|
package/dist/admin/schedules.js
CHANGED
|
@@ -60,10 +60,11 @@ function validateMappingRule(db, ruleJson) {
|
|
|
60
60
|
return undefined;
|
|
61
61
|
}
|
|
62
62
|
/** 解析 week JSON 为数字数组,失败返回 null */
|
|
63
|
+
const MAX_WEEK_DAY = 6;
|
|
63
64
|
function parseWeekSafe(weekJson) {
|
|
64
65
|
try {
|
|
65
66
|
const arr = JSON.parse(weekJson);
|
|
66
|
-
if (!Array.isArray(arr) || !arr.every((d) => typeof d === "number" && d >= 0 && d <=
|
|
67
|
+
if (!Array.isArray(arr) || !arr.every((d) => typeof d === "number" && d >= 0 && d <= MAX_WEEK_DAY))
|
|
67
68
|
return null;
|
|
68
69
|
return arr;
|
|
69
70
|
}
|
|
@@ -90,8 +91,9 @@ function checkOverlap(db, groupId, excludeId, weekDays, startHour, endHour) {
|
|
|
90
91
|
}
|
|
91
92
|
return undefined;
|
|
92
93
|
}
|
|
94
|
+
const HOUR_PAD_WIDTH = 2;
|
|
93
95
|
function formatHour(h) {
|
|
94
|
-
return String(h).padStart(
|
|
96
|
+
return String(h).padStart(HOUR_PAD_WIDTH, "0") + ":00";
|
|
95
97
|
}
|
|
96
98
|
export const adminScheduleRoutes = (app, options, done) => {
|
|
97
99
|
const { db } = options;
|
package/dist/admin/settings.js
CHANGED
|
@@ -84,6 +84,10 @@ export const OVERFLOW_THRESHOLD = 1000000;
|
|
|
84
84
|
export function lookupContextWindow(modelName) {
|
|
85
85
|
return MODEL_CONTEXT_WINDOWS[modelName] ?? DEFAULT_CONTEXT_WINDOW;
|
|
86
86
|
}
|
|
87
|
+
/** 标准化 patch 名称:连字符 → 下划线 */
|
|
88
|
+
function normalizePatchName(name) {
|
|
89
|
+
return name.replace(/-/g, "_");
|
|
90
|
+
}
|
|
87
91
|
export function parseModels(raw) {
|
|
88
92
|
if (!raw)
|
|
89
93
|
return [];
|
|
@@ -100,7 +104,7 @@ export function parseModels(raw) {
|
|
|
100
104
|
return null;
|
|
101
105
|
return {
|
|
102
106
|
name: obj.name,
|
|
103
|
-
patches: obj.patches ?? [],
|
|
107
|
+
patches: (obj.patches ?? []).map(normalizePatchName),
|
|
104
108
|
};
|
|
105
109
|
}).filter((e) => e !== null);
|
|
106
110
|
}
|
package/dist/db/metrics.js
CHANGED
|
@@ -17,9 +17,11 @@ const PERIOD_OFFSET = {
|
|
|
17
17
|
"7d": "-7 days",
|
|
18
18
|
"30d": "-30 days",
|
|
19
19
|
};
|
|
20
|
-
// 精确
|
|
20
|
+
// 精确 DATA_POINT_COUNT 个数据点:总秒数 / DATA_POINT_COUNT,最小 MIN_BUCKET_SEC 秒避免过细
|
|
21
|
+
const MIN_BUCKET_SEC = 60;
|
|
22
|
+
const DATA_POINT_COUNT = 10;
|
|
21
23
|
function calcBucketSec(totalSec) {
|
|
22
|
-
return Math.max(
|
|
24
|
+
return Math.max(MIN_BUCKET_SEC, Math.round(totalSec / DATA_POINT_COUNT));
|
|
23
25
|
}
|
|
24
26
|
// 预设周期总秒数(与 PERIOD_OFFSET 对应)
|
|
25
27
|
const PERIOD_TOTAL_SEC = {
|
|
@@ -105,7 +105,7 @@ export class SSEMetricsTransform extends Transform {
|
|
|
105
105
|
return delta.content;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
catch { /* 非 JSON 数据行,跳过 */ }
|
|
108
|
+
catch { /* 非 JSON 数据行,跳过 */ } // eslint-disable-line taste/no-silent-catch
|
|
109
109
|
return undefined;
|
|
110
110
|
}
|
|
111
111
|
/** 节流逻辑:首次或距上次回调超过 throttleMs 时触发 */
|
|
@@ -267,7 +267,7 @@ async function executeFailoverLoop(ctx) {
|
|
|
267
267
|
pluginRegistry.applyAfterResponse(respCtx);
|
|
268
268
|
transformed = JSON.stringify(respCtx.response);
|
|
269
269
|
}
|
|
270
|
-
catch { /* response hooks best-effort */ }
|
|
270
|
+
catch { /* response hooks best-effort */ } // eslint-disable-line taste/no-silent-catch
|
|
271
271
|
}
|
|
272
272
|
return transformed;
|
|
273
273
|
}
|
|
@@ -67,7 +67,7 @@ function filterExcluded(targets, excludeTargets) {
|
|
|
67
67
|
return targets.filter(t => !excludeTargets.some(e => e.backend_model === t.backend_model && e.provider_id === t.provider_id));
|
|
68
68
|
}
|
|
69
69
|
// ---------- Schedule matching ----------
|
|
70
|
-
const ALL_WEEK_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
70
|
+
const ALL_WEEK_DAYS = [0, 1, 2, 3, 4, 5, 6]; // eslint-disable-line no-magic-numbers
|
|
71
71
|
/** 将 week JSON 字符串解析为 dayOfWeek 数字集合 (0=Sun ~ 6=Sat) */
|
|
72
72
|
function parseWeekDays(weekJson) {
|
|
73
73
|
try {
|
|
@@ -52,7 +52,7 @@ export class PluginRegistry {
|
|
|
52
52
|
try {
|
|
53
53
|
p.beforeRequestTransform?.(ctx);
|
|
54
54
|
}
|
|
55
|
-
catch (err) {
|
|
55
|
+
catch (err) { // eslint-disable-line taste/no-silent-catch
|
|
56
56
|
console.error(`[plugin-registry] Plugin "${p.name}" beforeRequestTransform error:`, err);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -62,7 +62,7 @@ export class PluginRegistry {
|
|
|
62
62
|
try {
|
|
63
63
|
p.afterRequestTransform?.(ctx);
|
|
64
64
|
}
|
|
65
|
-
catch (err) {
|
|
65
|
+
catch (err) { // eslint-disable-line taste/no-silent-catch
|
|
66
66
|
console.error(`[plugin-registry] Plugin "${p.name}" afterRequestTransform error:`, err);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -72,7 +72,7 @@ export class PluginRegistry {
|
|
|
72
72
|
try {
|
|
73
73
|
p.beforeResponseTransform?.(ctx);
|
|
74
74
|
}
|
|
75
|
-
catch (err) {
|
|
75
|
+
catch (err) { // eslint-disable-line taste/no-silent-catch
|
|
76
76
|
console.error(`[plugin-registry] Plugin "${p.name}" beforeResponseTransform error:`, err);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -82,7 +82,7 @@ export class PluginRegistry {
|
|
|
82
82
|
try {
|
|
83
83
|
p.afterResponseTransform?.(ctx);
|
|
84
84
|
}
|
|
85
|
-
catch (err) {
|
|
85
|
+
catch (err) { // eslint-disable-line taste/no-silent-catch
|
|
86
86
|
console.error(`[plugin-registry] Plugin "${p.name}" afterResponseTransform error:`, err);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -121,7 +121,7 @@ export class PluginRegistry {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
|
-
afterResponseTransform(
|
|
124
|
+
afterResponseTransform() {
|
|
125
125
|
// field_overrides only applies to request direction;
|
|
126
126
|
// response should reflect actual upstream data, not override rules
|
|
127
127
|
},
|
|
@@ -4,8 +4,9 @@ const EFFORT_BUDGET = { low: 1024, medium: 8192, high: 32768 };
|
|
|
4
4
|
const DEFAULT_BUDGET = 8192;
|
|
5
5
|
// ---------- Helpers ----------
|
|
6
6
|
/** Strip "toolu_" prefix from a tool_use_id to recover the original call_id. */
|
|
7
|
+
const TOOLU_PREFIX_LEN = "toolu_".length;
|
|
7
8
|
function stripTooluPrefix(id) {
|
|
8
|
-
return id.startsWith("toolu_") ? id.slice(
|
|
9
|
+
return id.startsWith("toolu_") ? id.slice(TOOLU_PREFIX_LEN) : id;
|
|
9
10
|
}
|
|
10
11
|
/** Merge consecutive same-role messages to satisfy Anthropic strict alternation. */
|
|
11
12
|
function mergeConsecutiveMessages(msgs) {
|
|
@@ -76,9 +76,10 @@ export function responsesToAnthropicResponse(bodyStr) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
// ---------- Anthropic → Responses ----------
|
|
79
|
+
const TOOLU_PREFIX_LEN = "toolu_".length;
|
|
79
80
|
/** Strip "toolu_" prefix from a tool_use_id to recover the original call_id. */
|
|
80
81
|
function stripTooluPrefix(id) {
|
|
81
|
-
return id.startsWith("toolu_") ? id.slice(
|
|
82
|
+
return id.startsWith("toolu_") ? id.slice(TOOLU_PREFIX_LEN) : id;
|
|
82
83
|
}
|
|
83
84
|
export function anthropicToResponsesResponse(bodyStr) {
|
|
84
85
|
const ant = JSON.parse(bodyStr);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
2
|
import { BaseSSETransform } from "./stream-transform-base.js";
|
|
3
|
-
import { generateRespId } from "./id-utils.js";
|
|
3
|
+
import { generateRespId, MS_PER_SECOND } from "./id-utils.js";
|
|
4
4
|
import { RESPONSES_SSE_EVENTS } from "./types-responses.js";
|
|
5
5
|
function randomHex(bytes) {
|
|
6
6
|
return randomBytes(bytes).toString("hex");
|
|
7
7
|
}
|
|
8
|
+
const ID_HEX_LENGTH = 12;
|
|
9
|
+
const SHORT_HEX_LENGTH = 8;
|
|
10
|
+
const TOOLU_PREFIX_LEN = "toolu_".length;
|
|
8
11
|
export class AnthropicToResponsesTransform extends BaseSSETransform {
|
|
9
12
|
state = "init";
|
|
10
13
|
responseId = generateRespId();
|
|
@@ -19,7 +22,7 @@ export class AnthropicToResponsesTransform extends BaseSSETransform {
|
|
|
19
22
|
currentItemId = "";
|
|
20
23
|
currentSummaryPartId = "";
|
|
21
24
|
currentContentPartIndex = 0;
|
|
22
|
-
createdAt = Math.floor(Date.now() /
|
|
25
|
+
createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
23
26
|
nextSeq() {
|
|
24
27
|
return this.sequenceNumber++;
|
|
25
28
|
}
|
|
@@ -68,8 +71,8 @@ export class AnthropicToResponsesTransform extends BaseSSETransform {
|
|
|
68
71
|
const blockType = block?.type;
|
|
69
72
|
if (blockType === "thinking") {
|
|
70
73
|
this.state = "thinking";
|
|
71
|
-
this.currentItemId = `rs_${randomHex(
|
|
72
|
-
this.currentSummaryPartId = `sp_${randomHex(
|
|
74
|
+
this.currentItemId = `rs_${randomHex(ID_HEX_LENGTH)}`;
|
|
75
|
+
this.currentSummaryPartId = `sp_${randomHex(SHORT_HEX_LENGTH)}`;
|
|
73
76
|
this.pushResponsesSSE(RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED, {
|
|
74
77
|
type: RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED,
|
|
75
78
|
output_index: this.outputIndex,
|
|
@@ -86,7 +89,7 @@ export class AnthropicToResponsesTransform extends BaseSSETransform {
|
|
|
86
89
|
}
|
|
87
90
|
else if (blockType === "text") {
|
|
88
91
|
this.state = "text";
|
|
89
|
-
this.currentItemId = `msg_${randomHex(
|
|
92
|
+
this.currentItemId = `msg_${randomHex(ID_HEX_LENGTH)}`;
|
|
90
93
|
this.currentContentPartIndex = 0;
|
|
91
94
|
this.pushResponsesSSE(RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED, {
|
|
92
95
|
type: RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED,
|
|
@@ -107,8 +110,8 @@ export class AnthropicToResponsesTransform extends BaseSSETransform {
|
|
|
107
110
|
const toolId = block.id;
|
|
108
111
|
// Convert toolu_ prefix to fc_ prefix
|
|
109
112
|
this.activeToolCallId = toolId.startsWith("toolu_")
|
|
110
|
-
? `fc_${toolId.slice(
|
|
111
|
-
: `fc_${randomHex(
|
|
113
|
+
? `fc_${toolId.slice(TOOLU_PREFIX_LEN)}`
|
|
114
|
+
: `fc_${randomHex(ID_HEX_LENGTH)}`;
|
|
112
115
|
const callId = this.activeToolCallId;
|
|
113
116
|
this.currentItemId = callId;
|
|
114
117
|
this.pushResponsesSSE(RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED, {
|
|
@@ -288,7 +291,7 @@ export class AnthropicToResponsesTransform extends BaseSSETransform {
|
|
|
288
291
|
}
|
|
289
292
|
emitCompleted() {
|
|
290
293
|
const status = this.pendingStatus ?? "completed";
|
|
291
|
-
const completedAt = Math.floor(Date.now() /
|
|
294
|
+
const completedAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
292
295
|
const response = {
|
|
293
296
|
id: this.responseId,
|
|
294
297
|
object: "response",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
2
|
import { BaseSSETransform } from "./stream-transform-base.js";
|
|
3
|
-
import { generateRespId } from "./id-utils.js";
|
|
3
|
+
import { generateRespId, MS_PER_SECOND } from "./id-utils.js";
|
|
4
4
|
import { RESPONSES_SSE_EVENTS } from "./types-responses.js";
|
|
5
5
|
function randomHex(bytes) {
|
|
6
6
|
return randomBytes(bytes).toString("hex");
|
|
7
7
|
}
|
|
8
|
+
const ID_HEX_LENGTH = 12;
|
|
8
9
|
/**
|
|
9
10
|
* Bridge transform: Chat Completions SSE → Responses API SSE.
|
|
10
11
|
*
|
|
@@ -29,7 +30,7 @@ export class ChatToResponsesBridgeTransform extends BaseSSETransform {
|
|
|
29
30
|
currentFunctionCallId = "";
|
|
30
31
|
currentFunctionCallName = "";
|
|
31
32
|
currentReasoningItemId = "";
|
|
32
|
-
createdAt = Math.floor(Date.now() /
|
|
33
|
+
createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
33
34
|
nextSeq() {
|
|
34
35
|
return this.sequenceNumber++;
|
|
35
36
|
}
|
|
@@ -173,7 +174,7 @@ export class ChatToResponsesBridgeTransform extends BaseSSETransform {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
emitCompleted() {
|
|
176
|
-
const completedAt = Math.floor(Date.now() /
|
|
177
|
+
const completedAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
177
178
|
const response = {
|
|
178
179
|
id: this.responseId,
|
|
179
180
|
object: "response",
|
|
@@ -235,7 +236,7 @@ export class ChatToResponsesBridgeTransform extends BaseSSETransform {
|
|
|
235
236
|
this.closeCurrentMessageItem();
|
|
236
237
|
if (!this.hasReasoningItemStarted) {
|
|
237
238
|
this.hasReasoningItemStarted = true;
|
|
238
|
-
this.currentReasoningItemId = `rs_${randomHex(
|
|
239
|
+
this.currentReasoningItemId = `rs_${randomHex(ID_HEX_LENGTH)}`;
|
|
239
240
|
this.pushResponsesSSE(RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED, {
|
|
240
241
|
type: RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED,
|
|
241
242
|
output_index: this.outputIndex,
|
|
@@ -266,7 +267,7 @@ export class ChatToResponsesBridgeTransform extends BaseSSETransform {
|
|
|
266
267
|
if (!this.hasMessageItemStarted) {
|
|
267
268
|
this.hasMessageItemStarted = true;
|
|
268
269
|
this.contentIndex = 0;
|
|
269
|
-
this.currentMessageItemId = `msg_${randomHex(
|
|
270
|
+
this.currentMessageItemId = `msg_${randomHex(ID_HEX_LENGTH)}`;
|
|
270
271
|
this.pushResponsesSSE(RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED, {
|
|
271
272
|
type: RESPONSES_SSE_EVENTS.OUTPUT_ITEM_ADDED,
|
|
272
273
|
output_index: this.outputIndex,
|
|
@@ -162,7 +162,7 @@ class StreamProxy {
|
|
|
162
162
|
try {
|
|
163
163
|
this.reply.raw.write(chunk);
|
|
164
164
|
}
|
|
165
|
-
catch {
|
|
165
|
+
catch { // eslint-disable-line taste/no-silent-catch
|
|
166
166
|
// 客户端已断开,写已销毁的 socket 会抛出异常,可安全忽略
|
|
167
167
|
}
|
|
168
168
|
});
|
|
@@ -224,7 +224,7 @@ class StreamProxy {
|
|
|
224
224
|
try {
|
|
225
225
|
this.reply.raw.end();
|
|
226
226
|
}
|
|
227
|
-
catch {
|
|
227
|
+
catch { // eslint-disable-line taste/no-silent-catch
|
|
228
228
|
// reply 可能已 destroyed,安全忽略
|
|
229
229
|
}
|
|
230
230
|
});
|
|
@@ -270,7 +270,7 @@ class StreamProxy {
|
|
|
270
270
|
try {
|
|
271
271
|
this.reply.raw.end();
|
|
272
272
|
}
|
|
273
|
-
catch {
|
|
273
|
+
catch { // eslint-disable-line taste/no-silent-catch
|
|
274
274
|
// reply 可能已 destroyed,安全忽略
|
|
275
275
|
}
|
|
276
276
|
}
|
package/dist/upgrade/checker.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import https from 'node:https';
|
|
3
|
-
import path from 'node:path';
|
|
4
3
|
import { getInstalledVersion } from './version.js';
|
|
5
4
|
import { getConfigVersions } from '../config/recommended.js';
|
|
6
5
|
const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/llm-simple-router';
|
|
@@ -49,7 +48,6 @@ export async function fetchJson(url, redirects = 0) {
|
|
|
49
48
|
export function createUpgradeChecker(options) {
|
|
50
49
|
const npmRegistryUrl = options?.npmRegistryUrl ?? DEFAULT_NPM_REGISTRY;
|
|
51
50
|
const configBaseUrl = options?.configBaseUrl ?? DEFAULT_GITHUB_CONFIG_BASE;
|
|
52
|
-
const configDir = options?.configDir ?? path.resolve(process.cwd(), 'config');
|
|
53
51
|
let npmStatus = {
|
|
54
52
|
hasUpdate: false,
|
|
55
53
|
currentVersion: getInstalledVersion(),
|
package/dist/utils/time-range.js
CHANGED
|
@@ -5,6 +5,11 @@ const WINDOW_HOURS = 5;
|
|
|
5
5
|
const MS_PER_HOUR = 3600_000;
|
|
6
6
|
// 与 usage-windows 的默认窗口时长对齐
|
|
7
7
|
const WINDOW_DURATION_MS = WINDOW_HOURS * MS_PER_HOUR;
|
|
8
|
+
const DAYS_TO_SUNDAY = 6;
|
|
9
|
+
const END_OF_DAY_HOUR = 23;
|
|
10
|
+
const END_OF_DAY_MINUTE = 59;
|
|
11
|
+
const END_OF_DAY_SECOND = 59;
|
|
12
|
+
const END_OF_DAY_MS = 999;
|
|
8
13
|
export function resolveTimeRange(period, db, routerKeyId, providerId) {
|
|
9
14
|
const now = new Date();
|
|
10
15
|
switch (period) {
|
|
@@ -23,14 +28,14 @@ export function resolveTimeRange(period, db, routerKeyId, providerId) {
|
|
|
23
28
|
const monday = getMonday(now);
|
|
24
29
|
monday.setHours(0, 0, 0, 0);
|
|
25
30
|
const sunday = new Date(monday);
|
|
26
|
-
sunday.setDate(sunday.getDate() +
|
|
27
|
-
sunday.setHours(
|
|
31
|
+
sunday.setDate(sunday.getDate() + DAYS_TO_SUNDAY);
|
|
32
|
+
sunday.setHours(END_OF_DAY_HOUR, END_OF_DAY_MINUTE, END_OF_DAY_SECOND, END_OF_DAY_MS);
|
|
28
33
|
return { startTime: toSqliteDatetime(monday), endTime: toSqliteDatetime(sunday) };
|
|
29
34
|
}
|
|
30
35
|
case "monthly": {
|
|
31
36
|
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
32
37
|
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
33
|
-
last.setHours(
|
|
38
|
+
last.setHours(END_OF_DAY_HOUR, END_OF_DAY_MINUTE, END_OF_DAY_SECOND, END_OF_DAY_MS);
|
|
34
39
|
return { startTime: toSqliteDatetime(first), endTime: toSqliteDatetime(last) };
|
|
35
40
|
}
|
|
36
41
|
}
|
package/package.json
CHANGED