timarai-dashboard-mcp 1.0.3

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/client.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * 平台驾驶舱 OpenAPI 客户端。
3
+ *
4
+ * 这里集中处理三件在真实调用中最容易出错的事:
5
+ * 1. **rawBody 一致性**:签名串必须与实际发送的字节原文逐字节一致,
6
+ * 所以本文件自行 JSON.stringify 并直接作为 fetch body,绝不让请求库二次序列化。
7
+ * 2. **±30 秒时钟偏斜**:Sandbox/Production 强制校验。本地时钟漂移时,
8
+ * 用响应的 `Date` 头估算服务端时间并做一次性偏斜校正后重试。
9
+ * 3. **错误归一化**:把 `CryptoApiResponse` 的数字码翻译成 Agent 能据此行动的建议。
10
+ */
11
+ import { randomUUID } from "node:crypto";
12
+ import { redactSecret } from "./config.js";
13
+ import { buildSignedHeaders, timestampMillis } from "./signature.js";
14
+ import { DASHBOARD_ROUTE_PREFIX } from "./registry.js";
15
+ /** 接口契约约定的业务码(成功码对外为字符串 "0")。 */
16
+ export const API_CODE = {
17
+ SUCCESS: "0",
18
+ PARAMETER_INVALID: "400",
19
+ SIGNATURES_NOT_MATCH: "401",
20
+ UNAUTHORIZED: "403",
21
+ SERVER_ERROR: "500",
22
+ };
23
+ export class ApiError extends Error {
24
+ detail;
25
+ constructor(message, detail) {
26
+ super(message);
27
+ this.name = "ApiError";
28
+ this.detail = detail;
29
+ }
30
+ }
31
+ /** 响应外壳在部分网关包装下可能多一层 result,这里做宽容解析。 */
32
+ function unwrapEnvelope(payload) {
33
+ if (payload && typeof payload === "object") {
34
+ const obj = payload;
35
+ if (obj.code !== undefined || obj.data !== undefined)
36
+ return obj;
37
+ if (obj.result !== undefined && typeof obj.result === "object") {
38
+ const inner = obj.result;
39
+ return { code: inner.code, msg: inner.msg, data: inner.data };
40
+ }
41
+ }
42
+ return {};
43
+ }
44
+ function describeFailure(code, msg) {
45
+ const text = `${msg}`;
46
+ switch (code) {
47
+ case API_CODE.SIGNATURES_NOT_MATCH:
48
+ return {
49
+ hint: "签名不匹配。检查:Secret 是否为数据字典 PlatformDashboardOpenApi 中该 clientId 的明文密钥;" +
50
+ "时间戳是否为 Unix 毫秒;签名串是否为 timestamp+requestId+rawBody。",
51
+ };
52
+ case API_CODE.UNAUTHORIZED:
53
+ return {
54
+ hint: "无权限。检查:clientId 是否存在、enabled 是否为 true、scopes 是否包含 dashboard.read;" +
55
+ "注意平台凭证与商户 X-Api-Key 不可混用。",
56
+ };
57
+ case API_CODE.PARAMETER_INVALID:
58
+ if (/timestamp/i.test(text)) {
59
+ return { hint: "时间戳超出 ±30 秒窗口。请校准 MCP 宿主机时钟(建议开启 NTP 自动同步)。" };
60
+ }
61
+ return { hint: "参数不合法或缺少鉴权 Header。请检查筛选值与必填字段。" };
62
+ case API_CODE.SERVER_ERROR:
63
+ if (/dictionary|unavailable/i.test(text)) {
64
+ return {
65
+ hint: "服务端数据字典未配置(DictionaryMissing)。请让管理员在数据字典中写入 " +
66
+ "Code=PlatformDashboardOpenApi 的平台 OpenAPI 配置项。",
67
+ };
68
+ }
69
+ return { hint: "服务端错误。请携带 requestId 联系平台侧排查。" };
70
+ default:
71
+ return { hint: "调用失败,请结合 code 与 msg 排查。" };
72
+ }
73
+ }
74
+ /** 幂等的简单 TTL 缓存,仅用于筛选选项这类低频变动数据。 */
75
+ class TtlCache {
76
+ ttlMs;
77
+ store = new Map();
78
+ constructor(ttlMs) {
79
+ this.ttlMs = ttlMs;
80
+ }
81
+ get(key) {
82
+ const hit = this.store.get(key);
83
+ if (!hit)
84
+ return undefined;
85
+ if (hit.expiresAt < Date.now()) {
86
+ this.store.delete(key);
87
+ return undefined;
88
+ }
89
+ return hit.value;
90
+ }
91
+ set(key, value) {
92
+ if (this.ttlMs <= 0)
93
+ return;
94
+ this.store.set(key, { expiresAt: Date.now() + this.ttlMs, value });
95
+ }
96
+ }
97
+ export class DashboardClient {
98
+ cfg;
99
+ /** 由服务端 Date 头估算的「服务端时间 - 本地时间」偏移(毫秒)。 */
100
+ skewMs = 0;
101
+ cache = new TtlCache(0);
102
+ constructor(cfg) {
103
+ this.cfg = cfg;
104
+ this.cache = new TtlCache(cfg.TIMARAI_FILTER_CACHE_TTL_MS);
105
+ }
106
+ absoluteUrl(path) {
107
+ return new URL(DASHBOARD_ROUTE_PREFIX + path.replace(/^\/+/, ""), `${this.cfg.TIMARAI_API_BASE_URL}/`).toString();
108
+ }
109
+ nowWithSkew() {
110
+ return timestampMillis(new Date(Date.now() + this.skewMs));
111
+ }
112
+ audit(entry) {
113
+ const safe = JSON.stringify(entry, (_k, v) => typeof v === "string" ? redactSecret(v, this.cfg.TIMARAI_PLATFORM_SECRET) : v);
114
+ // STDIO 传输下 stdout 被协议占用,日志必须走 stderr。
115
+ process.stderr.write(`[TimarAI-Dashboard-MCP] ${safe}\n`);
116
+ }
117
+ async dispatch(spec, requestId, rawBody, signBody) {
118
+ const { headers } = buildSignedHeaders({
119
+ clientId: this.cfg.TIMARAI_PLATFORM_CLIENT_ID,
120
+ secret: this.cfg.TIMARAI_PLATFORM_SECRET,
121
+ requestId,
122
+ timestamp: this.nowWithSkew(),
123
+ rawBody: signBody,
124
+ });
125
+ const startedAt = Date.now();
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), this.cfg.TIMARAI_HTTP_TIMEOUT_MS);
128
+ let response;
129
+ try {
130
+ response = await fetch(this.absoluteUrl(spec.path), {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json", ...headers },
133
+ // rawBody 与签名串同源,禁止替换为对象让 fetch 重新序列化。
134
+ body: rawBody,
135
+ signal: controller.signal,
136
+ });
137
+ }
138
+ catch (error) {
139
+ const elapsedMs = Date.now() - startedAt;
140
+ const reason = controller.signal.aborted
141
+ ? `请求超时(${this.cfg.TIMARAI_HTTP_TIMEOUT_MS}ms)`
142
+ : `网络错误:${error.message}`;
143
+ this.audit({ event: "http_error", requestId, endpointId: spec.id, elapsedMs, reason });
144
+ throw new ApiError(reason, {
145
+ endpointId: spec.id,
146
+ requestId,
147
+ httpStatus: null,
148
+ code: null,
149
+ hint: "检查 TIMARAI_API_BASE_URL 连通性与防火墙策略。",
150
+ });
151
+ }
152
+ finally {
153
+ clearTimeout(timer);
154
+ }
155
+ const elapsedMs = Date.now() - startedAt;
156
+ const text = await response.text();
157
+ // 借用响应 Date 头持续校正时钟偏斜,避免每次都靠失败来纠偏。
158
+ const serverDate = response.headers.get("date");
159
+ if (serverDate) {
160
+ const parsed = Date.parse(serverDate);
161
+ if (!Number.isNaN(parsed))
162
+ this.skewMs = parsed - Date.now();
163
+ }
164
+ let payload = null;
165
+ if (text) {
166
+ try {
167
+ payload = JSON.parse(text);
168
+ }
169
+ catch {
170
+ this.audit({ event: "bad_json", requestId, endpointId: spec.id, httpStatus: response.status });
171
+ throw new ApiError(`响应不是合法 JSON(HTTP ${response.status})`, {
172
+ endpointId: spec.id,
173
+ requestId,
174
+ httpStatus: response.status,
175
+ code: null,
176
+ hint: "确认请求是否命中了正确的 API Host(可能被网关返回了 HTML 错误页)。",
177
+ });
178
+ }
179
+ }
180
+ const env = unwrapEnvelope(payload);
181
+ const code = env.code === undefined || env.code === null ? null : String(env.code);
182
+ const msg = typeof env.msg === "string" ? env.msg : "";
183
+ if (code !== API_CODE.SUCCESS) {
184
+ const { hint } = describeFailure(code ?? "", msg);
185
+ this.audit({
186
+ event: "api_error",
187
+ requestId,
188
+ endpointId: spec.id,
189
+ httpStatus: response.status,
190
+ code,
191
+ msg,
192
+ });
193
+ throw new ApiError(`[${code ?? "unknown"}] ${msg || "调用失败"}`, {
194
+ endpointId: spec.id,
195
+ requestId,
196
+ httpStatus: response.status,
197
+ code,
198
+ hint,
199
+ });
200
+ }
201
+ this.audit({
202
+ event: "ok",
203
+ requestId,
204
+ endpointId: spec.id,
205
+ httpStatus: response.status,
206
+ elapsedMs,
207
+ });
208
+ return {
209
+ meta: {
210
+ endpointId: spec.id,
211
+ path: `${DASHBOARD_ROUTE_PREFIX}${spec.path}`,
212
+ requestId,
213
+ httpStatus: response.status,
214
+ elapsedMs,
215
+ code: API_CODE.SUCCESS,
216
+ },
217
+ data: env.data,
218
+ };
219
+ }
220
+ /**
221
+ * 调用一个驾驶舱路由。
222
+ *
223
+ * 失败策略:
224
+ * - `401 SignaturesNotMatch`:换用「空 body」签名串重试一次。
225
+ * 这是针对部分网关/中间件重读 body 失败、导致对端收到空串的兜底;
226
+ * 一旦命中会在 stderr 打出 WARN,便于定位这类链路问题。
227
+ * - `5xx`:按指数退避重试 `TIMARAI_MAX_RETRIES` 次。
228
+ */
229
+ async call(spec, body) {
230
+ const rawBody = JSON.stringify(body);
231
+ const requestId = randomUUID();
232
+ let attempt = 0;
233
+ let lastError = null;
234
+ const variants = [
235
+ { name: "sent-body", signBody: rawBody },
236
+ { name: "empty-body", signBody: undefined },
237
+ ];
238
+ for (const variant of variants) {
239
+ attempt += 1;
240
+ try {
241
+ return await this.dispatch(spec, requestId, rawBody, variant.signBody);
242
+ }
243
+ catch (error) {
244
+ if (!(error instanceof ApiError))
245
+ throw error;
246
+ lastError = error;
247
+ if (error.detail.code === API_CODE.SIGNATURES_NOT_MATCH && variant.name === "sent-body") {
248
+ this.audit({
249
+ event: "warn",
250
+ requestId,
251
+ endpointId: spec.id,
252
+ reason: "sent-body signature rejected, retry with empty-body variant",
253
+ });
254
+ continue;
255
+ }
256
+ const retryable = error.detail.httpStatus !== null &&
257
+ error.detail.httpStatus >= 500 &&
258
+ attempt <= this.cfg.TIMARAI_MAX_RETRIES + variants.length - 1;
259
+ if (retryable) {
260
+ await new Promise((r) => setTimeout(r, 200 * 2 ** (attempt - 1)));
261
+ continue;
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+ throw lastError ?? new ApiError("未知调用失败", {
267
+ endpointId: spec.id,
268
+ requestId,
269
+ httpStatus: null,
270
+ code: null,
271
+ hint: "请结合 requestId 排查。",
272
+ });
273
+ }
274
+ /** 带 TTL 缓存的调用(仅用于 filters)。 */
275
+ async callCached(spec, body, cacheKey) {
276
+ const cached = this.cache.get(cacheKey);
277
+ if (cached)
278
+ return cached;
279
+ const result = await this.call(spec, body);
280
+ this.cache.set(cacheKey, result);
281
+ return result;
282
+ }
283
+ }
284
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/config.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 运行时配置。
3
+ *
4
+ * 安全约束:
5
+ * 1. Secret 仅从环境变量读取,绝不写入日志、错误信息或工具返回结果。
6
+ * 2. 缺失必填项时快速失败,并给出「可行动」的修复指引,而不是抛出原始堆栈。
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { z } from "zod";
10
+ const Booleanish = z
11
+ .enum(["true", "false"])
12
+ .default("true")
13
+ .transform((v) => v === "true");
14
+ const EnvSchema = z.object({
15
+ TIMARAI_API_BASE_URL: z.string().url(),
16
+ TIMARAI_PLATFORM_CLIENT_ID: z.string().min(1),
17
+ TIMARAI_PLATFORM_SECRET: z.string().min(8),
18
+ TIMARAI_HTTP_TIMEOUT_MS: z.coerce.number().int().positive().max(120_000).default(15_000),
19
+ /** 传输层重试次数(仅网络错误 / 5xx)。签名类失败由客户端单独做策略重试。 */
20
+ TIMARAI_MAX_RETRIES: z.coerce.number().int().min(0).max(3).default(1),
21
+ /** 服务端硬限制为 90 天;工具层前置校验以给出更友好的提示。 */
22
+ TIMARAI_MAX_DATE_SPAN_DAYS: z.coerce.number().int().positive().max(90).default(90),
23
+ /** 是否注册 call_endpoint 逃生阀工具。 */
24
+ TIMARAI_ALLOW_RAW_ENDPOINT: Booleanish,
25
+ /** 筛选项缓存时长(该数据变动极低频)。 */
26
+ TIMARAI_FILTER_CACHE_TTL_MS: z.coerce.number().int().nonnegative().default(300_000),
27
+ MCP_TRANSPORT: z.enum(["stdio", "http"]).default("stdio"),
28
+ /** HTTP 模式监听地址。默认只听回环;要给别人连就显式指定,并自行加反代鉴权。 */
29
+ MCP_HTTP_HOST: z.string().min(1).default("127.0.0.1"),
30
+ MCP_HTTP_PORT: z.coerce.number().int().positive().max(65_535).default(8899),
31
+ /** 图表 / 报表的落盘目录;相对路径按仓库根目录解析。 */
32
+ TIMARAI_OUTPUT_DIR: z.string().min(1).default("out"),
33
+ });
34
+ /**
35
+ * 极简 .env 加载器(零依赖)。生产环境建议由宿主进程直接注入环境变量。
36
+ *
37
+ * 对外导出是为了让 CLI 脚本与 Server 进程共用同一份加载逻辑,
38
+ * 避免脚本各自实现一套、导致两边读到的配置不一致。
39
+ * 已存在的环境变量优先(宿主注入 > .env)。
40
+ */
41
+ export function loadDotEnvIfPresent() {
42
+ try {
43
+ const raw = readFileSync(new URL("../.env", import.meta.url), "utf8");
44
+ for (const line of raw.split(/\r?\n/)) {
45
+ const trimmed = line.trim();
46
+ if (!trimmed || trimmed.startsWith("#"))
47
+ continue;
48
+ const eq = trimmed.indexOf("=");
49
+ if (eq <= 0)
50
+ continue;
51
+ const key = trimmed.slice(0, eq).trim();
52
+ const value = trimmed.slice(eq + 1).trim();
53
+ if (process.env[key] === undefined)
54
+ process.env[key] = value;
55
+ }
56
+ }
57
+ catch {
58
+ // .env 不存在属正常情况
59
+ }
60
+ }
61
+ /** 命令行参数 → 环境变量。少数客户端不传 env,用 args 也能配。 */
62
+ const CLI_FLAGS = {
63
+ url: "TIMARAI_API_BASE_URL",
64
+ key: "TIMARAI_PLATFORM_KEY",
65
+ "client-id": "TIMARAI_PLATFORM_CLIENT_ID",
66
+ secret: "TIMARAI_PLATFORM_SECRET",
67
+ out: "TIMARAI_OUTPUT_DIR",
68
+ };
69
+ export function applyCliOverrides(argv = process.argv.slice(2)) {
70
+ for (const arg of argv) {
71
+ const m = /^--([a-z-]+)=(.*)$/.exec(arg);
72
+ if (!m)
73
+ continue;
74
+ const flag = m[1];
75
+ const value = m[2];
76
+ if (!flag || value === undefined)
77
+ continue;
78
+ const envName = CLI_FLAGS[flag];
79
+ if (envName && value.trim())
80
+ process.env[envName] = value.trim();
81
+ }
82
+ }
83
+ /**
84
+ * 凭证归一。
85
+ *
86
+ * 主写法是分开的两项(ClientId + Secret);也接受合并成一个
87
+ * `TIMARAI_PLATFORM_KEY=clientId:secret`——在客户端配置里一次粘完更省事。
88
+ * 分开写优先,两者都填时以分开写的为准。
89
+ */
90
+ export function normalizeCredentials(env) {
91
+ const key = env.TIMARAI_PLATFORM_KEY?.trim();
92
+ if (!key)
93
+ return env;
94
+ const i = key.indexOf(":");
95
+ if (i <= 0 || i === key.length - 1) {
96
+ throw new ConfigError([
97
+ "TIMARAI_PLATFORM_KEY: 应为 clientId:secret(英文冒号分隔),例如 <你的 clientId>:<明文密钥>",
98
+ ]);
99
+ }
100
+ return {
101
+ ...env,
102
+ TIMARAI_PLATFORM_CLIENT_ID: env.TIMARAI_PLATFORM_CLIENT_ID || key.slice(0, i).trim(),
103
+ TIMARAI_PLATFORM_SECRET: env.TIMARAI_PLATFORM_SECRET || key.slice(i + 1).trim(),
104
+ };
105
+ }
106
+ /**
107
+ * 首次使用引导。
108
+ *
109
+ * STDIO 下 stdout 归协议所有、stdin 是 Agent 的 JSON-RPC,Server 不可能交互式提问。
110
+ * 所以这里只做一件事:**把要填什么、去哪拿说清楚**——
111
+ * 只列缺失项的话,第一次用的人连 ClientId 是什么都不知道。
112
+ */
113
+ export function firstRunGuidance(issues) {
114
+ const missingKeys = issues.map((i) => i.split(":")[0]?.trim() ?? i);
115
+ const need = (key) => missingKeys.some((k) => k === key);
116
+ const lines = [
117
+ "尚未完成配置:需要「接口地址 + 平台凭证」才能工作。",
118
+ "",
119
+ "最快的方式:在 Agent 客户端的 MCP 配置里填这三个环境变量(**都没有默认值**,",
120
+ "地址和凭证按环境由平台侧给出,找平台管理员索取):",
121
+ "",
122
+ ];
123
+ if (need("TIMARAI_API_BASE_URL")) {
124
+ lines.push(" TIMARAI_API_BASE_URL —— 驾驶舱 OpenAPI 根地址,形如 https://<你的接口域名>");
125
+ }
126
+ if (need("TIMARAI_PLATFORM_CLIENT_ID")) {
127
+ lines.push(" TIMARAI_PLATFORM_CLIENT_ID —— 平台数据字典里为该项 OpenAPI 登记的客户端标识");
128
+ }
129
+ if (need("TIMARAI_PLATFORM_SECRET")) {
130
+ lines.push(" TIMARAI_PLATFORM_SECRET —— 与之配对的明文密钥(找平台管理员登记)");
131
+ }
132
+ lines.push("", "也可以合并成一个:TIMARAI_PLATFORM_KEY=<你的 clientId>:<明文密钥>", "(或写成命令行参数:--url=https://<你的接口域名> --key=<你的 clientId>:<明文密钥>)", "", "源码安装的用户另可:cp .env.example .env 手工填写,或运行 npm run setup 走向导。", "注意:.env 不要提交进版本库(.gitignore 已排除)。");
133
+ return lines.join("\n");
134
+ }
135
+ export class ConfigError extends Error {
136
+ issues;
137
+ constructor(issues) {
138
+ super([
139
+ "配置校验失败,请检查 MCP 服务器环境变量:",
140
+ ...issues.map((i) => ` - ${i}`),
141
+ "",
142
+ "必填三项:TIMARAI_API_BASE_URL / TIMARAI_PLATFORM_CLIENT_ID / TIMARAI_PLATFORM_SECRET",
143
+ "(也可合并成一个 TIMARAI_PLATFORM_KEY=clientId:secret)",
144
+ "源码安装可运行:npm run setup;参考仓库根目录的 .env.example。",
145
+ ].join("\n"));
146
+ this.issues = issues;
147
+ this.name = "ConfigError";
148
+ }
149
+ }
150
+ export function loadConfig() {
151
+ loadDotEnvIfPresent();
152
+ const parsed = EnvSchema.safeParse(normalizeCredentials(process.env));
153
+ if (!parsed.success) {
154
+ throw new ConfigError(parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`));
155
+ }
156
+ return parsed.data;
157
+ }
158
+ /** 抹掉文本中可能出现的 Secret,用于安全输出到 stderr。 */
159
+ export function redactSecret(input, secret) {
160
+ if (!secret)
161
+ return input;
162
+ return input.split(secret).join("***REDACTED***");
163
+ }
164
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * 日期范围解析。
3
+ *
4
+ * 重要:报表时区为固定的 \`Asia/Shanghai\`(服务端以此封板日统计),
5
+ * 因此「今天 / 昨天」必须按报表时区折算,而不是按 MCP 宿主机的本地时区。
6
+ * 这是驾驶舱口径一致性的常见踩坑点。
7
+ */
8
+ export const REPORT_TIME_ZONE = "Asia/Shanghai";
9
+ /** 与需求文档「全局筛选区-时间」下拉框一致:今日/昨日/本周/本月/近7天/近30天/近90天。 */
10
+ export const DATE_PRESETS = [
11
+ "today",
12
+ "yesterday",
13
+ "last_7d",
14
+ "last_30d",
15
+ "last_90d",
16
+ "this_week",
17
+ "last_week",
18
+ "this_month",
19
+ "last_month",
20
+ ];
21
+ export class DateRangeError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "DateRangeError";
25
+ }
26
+ }
27
+ /** 取报表时区下的今天(YYYY-MM-DD)。 */
28
+ export function todayInReportTz(now = new Date()) {
29
+ const fmt = new Intl.DateTimeFormat("en-CA", {
30
+ timeZone: REPORT_TIME_ZONE,
31
+ year: "numeric",
32
+ month: "2-digit",
33
+ day: "2-digit",
34
+ });
35
+ return fmt.format(now);
36
+ }
37
+ const STAT_DATE_FMT = new Intl.DateTimeFormat("en-CA", {
38
+ timeZone: REPORT_TIME_ZONE,
39
+ year: "numeric",
40
+ month: "2-digit",
41
+ day: "2-digit",
42
+ });
43
+ /**
44
+ * 归一化「统计日」标签。
45
+ *
46
+ * 序列里的日期字段给过不止一种写法:`"2026-09-12"` 字符串、Unix 秒、Unix 毫秒。
47
+ * 只认字符串的话,遇到时间戳就会得到一串空标签——图还在,但读不出是哪天,
48
+ * 而且这种错不会报错,只会产出一张「看不懂但看着正常」的图。
49
+ * 这里三种写法都认,统一输出 YYYY-MM-DD(按报表时区,与封板口径一致)。
50
+ */
51
+ export function statDateLabel(value) {
52
+ if (typeof value === "string") {
53
+ if (/^\d{10,13}$/.test(value))
54
+ return fromTimestamp(Number(value));
55
+ return value.slice(0, 10);
56
+ }
57
+ if (typeof value === "number" && Number.isFinite(value))
58
+ return fromTimestamp(value);
59
+ return "";
60
+ }
61
+ /** 阈值取 1e11:它两边分别落在「秒」与「毫秒」的合理区间内,足以区分两种单位。 */
62
+ function fromTimestamp(value) {
63
+ const ms = value < 1e11 ? value * 1000 : value;
64
+ const date = new Date(ms);
65
+ if (Number.isNaN(date.getTime()))
66
+ return "";
67
+ return STAT_DATE_FMT.format(date).replace(/\//g, "-").slice(0, 10);
68
+ }
69
+ /**
70
+ * 与当前区间**等长且紧邻其前**的对照区间,用于算环比。
71
+ * 例:2026-09-01~09-07(7 天)→ 2026-08-25~08-31。
72
+ * 报表口径按天聚合,用 UTC 日界即可,不必再折算时区。
73
+ */
74
+ export function previousPeriod(startDate, endDate) {
75
+ const start = new Date(`${startDate}T00:00:00Z`);
76
+ const end = new Date(`${endDate}T00:00:00Z`);
77
+ const spanDays = Math.max(1, Math.round((end.getTime() - start.getTime()) / 86_400_000) + 1);
78
+ const prevEnd = new Date(start.getTime() - 86_400_000);
79
+ const prevStart = new Date(prevEnd.getTime() - (spanDays - 1) * 86_400_000);
80
+ return {
81
+ startDate: prevStart.toISOString().slice(0, 10),
82
+ endDate: prevEnd.toISOString().slice(0, 10),
83
+ };
84
+ }
85
+ function parseIsoDate(value, field) {
86
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
87
+ throw new DateRangeError(`${field} 必须为 YYYY-MM-DD 格式,收到:${value}`);
88
+ }
89
+ const date = new Date(`${value}T00:00:00Z`);
90
+ if (Number.isNaN(date.getTime())) {
91
+ throw new DateRangeError(`${field} 不是合法日期:${value}`);
92
+ }
93
+ return value;
94
+ }
95
+ function addDays(isoDate, days) {
96
+ const base = new Date(`${isoDate}T00:00:00Z`);
97
+ base.setUTCDate(base.getUTCDate() + days);
98
+ return base.toISOString().slice(0, 10);
99
+ }
100
+ function startOfMonth(isoDate) {
101
+ return `${isoDate.slice(0, 7)}-01`;
102
+ }
103
+ function endOfPreviousMonth(isoDate) {
104
+ const firstOfMonth = new Date(`${startOfMonth(isoDate)}T00:00:00Z`);
105
+ firstOfMonth.setUTCDate(0);
106
+ return firstOfMonth.toISOString().slice(0, 10);
107
+ }
108
+ /** 周一为一周起始,与国内报表习惯一致。 */
109
+ function startOfWeek(isoDate) {
110
+ const base = new Date(`${isoDate}T00:00:00Z`);
111
+ const dayOfWeek = base.getUTCDay(); // 0=周日
112
+ const offset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
113
+ base.setUTCDate(base.getUTCDate() + offset);
114
+ return base.toISOString().slice(0, 10);
115
+ }
116
+ function inclusiveSpan(startDate, endDate) {
117
+ const start = Date.parse(`${startDate}T00:00:00Z`);
118
+ const end = Date.parse(`${endDate}T00:00:00Z`);
119
+ return Math.floor((end - start) / 86_400_000) + 1;
120
+ }
121
+ export function resolveDateRange(options) {
122
+ const now = options.now ?? new Date();
123
+ const today = todayInReportTz(now);
124
+ let startDate;
125
+ let endDate;
126
+ let resolvedFrom;
127
+ if (options.startDate || options.endDate) {
128
+ startDate = parseIsoDate(options.startDate ?? options.endDate, "startDate");
129
+ endDate = parseIsoDate(options.endDate ?? options.startDate, "endDate");
130
+ resolvedFrom = "explicit";
131
+ }
132
+ else {
133
+ const preset = options.preset ?? "last_7d";
134
+ resolvedFrom = `preset:${preset}`;
135
+ switch (preset) {
136
+ case "today":
137
+ startDate = today;
138
+ endDate = today;
139
+ break;
140
+ case "yesterday":
141
+ startDate = addDays(today, -1);
142
+ endDate = startDate;
143
+ break;
144
+ case "last_7d":
145
+ endDate = today;
146
+ startDate = addDays(today, -6);
147
+ break;
148
+ case "last_30d":
149
+ endDate = today;
150
+ startDate = addDays(today, -29);
151
+ break;
152
+ case "last_90d":
153
+ endDate = today;
154
+ startDate = addDays(today, -89);
155
+ break;
156
+ case "this_week":
157
+ startDate = startOfWeek(today);
158
+ endDate = today;
159
+ break;
160
+ case "last_week": {
161
+ const thisWeekStart = startOfWeek(today);
162
+ endDate = addDays(thisWeekStart, -1);
163
+ startDate = addDays(endDate, -6);
164
+ break;
165
+ }
166
+ case "this_month":
167
+ startDate = startOfMonth(today);
168
+ endDate = today;
169
+ break;
170
+ case "last_month": {
171
+ endDate = endOfPreviousMonth(today);
172
+ startDate = startOfMonth(endDate);
173
+ break;
174
+ }
175
+ default: {
176
+ const exhaustive = preset;
177
+ throw new DateRangeError(`不支持的日期预设:${String(exhaustive)}`);
178
+ }
179
+ }
180
+ }
181
+ if (startDate > endDate) {
182
+ throw new DateRangeError(`开始日期 ${startDate} 不能晚于结束日期 ${endDate}。`);
183
+ }
184
+ const spanDays = inclusiveSpan(startDate, endDate);
185
+ if (spanDays > options.maxSpanDays) {
186
+ throw new DateRangeError(`查询跨度 ${spanDays} 天超过上限 ${options.maxSpanDays} 天(接口最大支持 90 天)。请缩短日期范围,或按周分段查询后自行汇总。`);
187
+ }
188
+ return { startDate, endDate, spanDays, resolvedFrom };
189
+ }
190
+ //# sourceMappingURL=date-range.js.map