@lark-apaas/miaoda-cli 0.1.2-alpha.45db14c → 0.1.2-alpha.4e370b6

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 (55) hide show
  1. package/dist/api/db/api.js +250 -6
  2. package/dist/api/db/client.js +36 -0
  3. package/dist/api/db/index.js +9 -1
  4. package/dist/api/file/api.js +15 -0
  5. package/dist/api/file/index.js +2 -1
  6. package/dist/api/index.js +1 -7
  7. package/dist/cli/commands/db/index.js +137 -0
  8. package/dist/cli/commands/file/index.js +7 -0
  9. package/dist/cli/commands/index.js +0 -6
  10. package/dist/cli/commands/shared.js +3 -61
  11. package/dist/cli/handlers/db/audit.js +285 -0
  12. package/dist/cli/handlers/db/changelog.js +117 -0
  13. package/dist/cli/handlers/db/data.js +1 -1
  14. package/dist/cli/handlers/db/index.js +17 -1
  15. package/dist/cli/handlers/db/migration.js +213 -0
  16. package/dist/cli/handlers/{app/update.js → db/quota.js} +29 -20
  17. package/dist/cli/handlers/db/recovery.js +188 -0
  18. package/dist/cli/handlers/file/index.js +3 -1
  19. package/dist/cli/handlers/{deploy/error-log.js → file/quota.js} +27 -22
  20. package/dist/utils/http.js +0 -86
  21. package/dist/utils/index.js +1 -13
  22. package/dist/utils/output.js +29 -338
  23. package/package.json +5 -7
  24. package/dist/api/app/api.js +0 -25
  25. package/dist/api/app/index.js +0 -15
  26. package/dist/api/app/schemas.js +0 -79
  27. package/dist/api/app/types.js +0 -58
  28. package/dist/api/deploy/api.js +0 -60
  29. package/dist/api/deploy/index.js +0 -16
  30. package/dist/api/deploy/schemas.js +0 -103
  31. package/dist/api/deploy/types.js +0 -22
  32. package/dist/api/observability/api.js +0 -52
  33. package/dist/api/observability/index.js +0 -16
  34. package/dist/api/observability/schemas.js +0 -39
  35. package/dist/api/observability/types.js +0 -27
  36. package/dist/cli/commands/app/index.js +0 -62
  37. package/dist/cli/commands/deploy/index.js +0 -140
  38. package/dist/cli/commands/observability/index.js +0 -229
  39. package/dist/cli/handlers/app/get.js +0 -48
  40. package/dist/cli/handlers/app/index.js +0 -7
  41. package/dist/cli/handlers/deploy/deploy.js +0 -83
  42. package/dist/cli/handlers/deploy/get.js +0 -70
  43. package/dist/cli/handlers/deploy/helpers.js +0 -41
  44. package/dist/cli/handlers/deploy/history.js +0 -70
  45. package/dist/cli/handlers/deploy/index.js +0 -14
  46. package/dist/cli/handlers/deploy/polling.js +0 -139
  47. package/dist/cli/handlers/observability/analytics.js +0 -190
  48. package/dist/cli/handlers/observability/helpers.js +0 -66
  49. package/dist/cli/handlers/observability/index.js +0 -12
  50. package/dist/cli/handlers/observability/log.js +0 -94
  51. package/dist/cli/handlers/observability/metric.js +0 -208
  52. package/dist/cli/handlers/observability/trace.js +0 -102
  53. package/dist/utils/devops-error.js +0 -28
  54. package/dist/utils/git.js +0 -29
  55. package/dist/utils/time.js +0 -132
@@ -1,208 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.handleObservabilityMetric = handleObservabilityMetric;
37
- const api = __importStar(require("../../../api/index"));
38
- const output_1 = require("../../../utils/output");
39
- const shared_1 = require("../../../cli/commands/shared");
40
- const helpers_1 = require("./helpers");
41
- /**
42
- * PRD <metric-name> + --series → 服务端 metric name → 表头列名(=对应 --series 取值)。
43
- *
44
- * - 缺省 --series 时返回该 cliName 下所有相关线。
45
- * - latency / requests 的 error 线 v1.0.122 起由独立 metric 提供(不再 is_error filter)。
46
- *
47
- * 表头标签直接用 --series 名("total"/"error"/"p50"/"p99");单一指标则用 cliName。
48
- */
49
- const METRIC_LABELS = {
50
- cpu: { cpu_usage: "cpu" },
51
- memory: { mem_usage: "memory" },
52
- requests: {
53
- client_api_request_count: "total",
54
- client_api_request_error_count: "error",
55
- },
56
- latency: {
57
- client_api_request_latency_p50: "p50",
58
- client_api_request_latency_p99: "p99",
59
- },
60
- };
61
- /** miaoda observability metric <metric-name> */
62
- async function handleObservabilityMetric(opts) {
63
- if (!opts.metricName)
64
- (0, shared_1.failArgs)("<metric-name> 必填");
65
- const appID = (0, shared_1.resolveAppId)({ appId: opts.appId });
66
- const { metricNames, labelByMetric, extraFilters } = resolveMetricSelection(opts.metricName, opts.series);
67
- const filters = (0, helpers_1.buildFieldFilters)([
68
- { key: "referer_path", value: opts.page ? (0, helpers_1.eqFilter)(opts.page) : undefined },
69
- { key: "api", value: opts.api ? (0, helpers_1.eqFilter)(opts.api) : undefined },
70
- ...extraFilters,
71
- ]);
72
- // since/until 直接以秒透传到 BAM;当前未做桶对齐(如需开启把 until
73
- // 用 ceilMsToBucket(untilMs, downSample as GranularityBucket) 包一层即可)。
74
- const downSample = opts.downSample ?? "1h";
75
- const nowMs = Date.now();
76
- const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
77
- const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
78
- const req = {
79
- appID,
80
- appEnv: "runtime",
81
- metricNames,
82
- startTimestamp: (0, helpers_1.msToSec)(sinceMs),
83
- endTimestamp: (0, helpers_1.msToSec)(untilMs),
84
- filters,
85
- downSample,
86
- needPackLackPoint: false,
87
- };
88
- const resp = await api.observability.queryMetricsData(req);
89
- // requests 的 count / error_count:
90
- // - BAM 在该时间点无数据时会传 null,或者 values 里干脆缺 key
91
- // - CLI 按"请求过的 metricNames"逐一兜底为 0,让"没有请求"和"没有数据"
92
- // 在表格 / JSON 里都显式呈现成 0,避免空格 / 缺字段被误解为缺失
93
- // latency / cpu / memory 不做这个兜底——p50 没数据 ≠ 延迟为 0。
94
- if (opts.metricName === "requests") {
95
- fillRequestedMetricsWithZero(resp, metricNames);
96
- }
97
- emitMetricsResponse(resp, labelByMetric);
98
- }
99
- /**
100
- * 按"客户端请求过的 metricNames"兜底缺失/为 null 的值为 0;不去动 BAM 多返回的
101
- * 其他字段(防御性,避免误改不属于本次查询的数据)。
102
- */
103
- function fillRequestedMetricsWithZero(resp, requestedMetricNames) {
104
- for (const point of resp.points ?? []) {
105
- for (const name of requestedMetricNames) {
106
- point.values[name] ??= 0;
107
- }
108
- }
109
- }
110
- // ── 渲染 ─────────
111
- //
112
- // BAM v1.0.123 起返回 points: [{timestamp, dimensions, values: {metricName: v}}]——
113
- // 服务端已把多 metric 合并到同 timestamp 的 row,下游直接平铺即可。
114
- // JSON 模式:原样透传 points(保留 BAM 原 metricName 作为 values 的 key)。
115
- // Pretty 模式:把 values 里的 metricName 重命名成 CLI label(p50/p99/total 等),
116
- // 再走 buildPivotSchema 渲染。
117
- function emitMetricsResponse(resp, labelByMetric) {
118
- const points = resp.points ?? [];
119
- // metric 不分页,但仍补齐 has_more / next_cursor 以对齐 log/trace 的信封形状
120
- if ((0, output_1.isJsonMode)()) {
121
- (0, output_1.emit)({ data: points, next_cursor: null, has_more: false });
122
- return;
123
- }
124
- const { rows, seriesLabels } = renameAndSort(points, labelByMetric);
125
- if (rows.length === 0) {
126
- (0, output_1.emit)({ data: [], next_cursor: null, has_more: false });
127
- return;
128
- }
129
- const hasLatency = Object.keys(labelByMetric).some((n) => n.includes("latency"));
130
- (0, output_1.emit)({ data: rows, next_cursor: null, has_more: false }, buildPivotSchema(seriesLabels, hasLatency));
131
- }
132
- function renameAndSort(points, labelByMetric) {
133
- // 列顺序按 labelByMetric 声明顺序,避免 BAM 返回顺序波动影响表头
134
- const seriesLabels = Object.values(labelByMetric);
135
- // TODO: 当 dimensions 非空(groupBy 多 series)时,把 dimensions 也并入 row
136
- const rows = points.map((point) => {
137
- const row = { timestamp: point.timestamp };
138
- for (const [metricName, value] of Object.entries(point.values)) {
139
- const label = labelByMetric[metricName] ?? metricName;
140
- row[label] = value;
141
- }
142
- return row;
143
- });
144
- // 按时间倒序,与 log/trace 的"最新在前"风格一致
145
- rows.sort((a, b) => b.timestamp - a.timestamp);
146
- return { rows, seriesLabels };
147
- }
148
- function buildPivotSchema(seriesLabels, hasLatency) {
149
- const valueFormat = hasLatency ? output_1.fmt.durationMs() : undefined;
150
- return {
151
- columns: [
152
- { key: "timestamp", label: "time", format: output_1.fmt.sec() },
153
- ...seriesLabels.map((label) => ({
154
- key: label,
155
- label,
156
- ...(valueFormat ? { format: valueFormat } : {}),
157
- })),
158
- ],
159
- strict: true,
160
- };
161
- }
162
- /**
163
- * 解析 <metric-name> + --series 的组合:决定最终 metricNames、表头 label 映射、附加过滤。
164
- *
165
- * label 直接用 --series 取值("total"/"error"/"p50"/"p99");单一指标 cliName 也作为 label。
166
- */
167
- function resolveMetricSelection(cliName, series) {
168
- const extras = [];
169
- if (cliName === "latency") {
170
- if (series === "p99") {
171
- return single("client_api_request_latency_p99", "p99", extras);
172
- }
173
- if (series === "p50") {
174
- return single("client_api_request_latency_p50", "p50", extras);
175
- }
176
- return all(METRIC_LABELS.latency, extras);
177
- }
178
- if (cliName === "requests") {
179
- // v1.0.122 起 error 线由独立 metric 提供,不再走 is_error filter
180
- if (series === "total") {
181
- return single("client_api_request_count", "total", extras);
182
- }
183
- if (series === "error") {
184
- return single("client_api_request_error_count", "error", extras);
185
- }
186
- return all(METRIC_LABELS.requests, extras);
187
- }
188
- if (cliName === "cpu")
189
- return all(METRIC_LABELS.cpu, extras);
190
- if (cliName === "memory")
191
- return all(METRIC_LABELS.memory, extras);
192
- // 兜底:CLI 名直接作为 metric name + label
193
- return single(cliName, cliName, extras);
194
- }
195
- function single(metricName, label, extras) {
196
- return {
197
- metricNames: [metricName],
198
- labelByMetric: { [metricName]: label },
199
- extraFilters: extras,
200
- };
201
- }
202
- function all(labelByMetric, extras) {
203
- return {
204
- metricNames: Object.keys(labelByMetric),
205
- labelByMetric,
206
- extraFilters: extras,
207
- };
208
- }
@@ -1,102 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.handleObservabilityTraceList = handleObservabilityTraceList;
37
- exports.handleObservabilityTraceGet = handleObservabilityTraceGet;
38
- const api = __importStar(require("../../../api/index"));
39
- const output_1 = require("../../../utils/output");
40
- const shared_1 = require("../../../cli/commands/shared");
41
- const index_1 = require("../../../api/observability/index");
42
- const helpers_1 = require("./helpers");
43
- /** miaoda observability trace list */
44
- async function handleObservabilityTraceList(opts) {
45
- const appID = (0, shared_1.resolveAppId)({ appId: opts.appId });
46
- const appEnv = "runtime";
47
- const limit = (0, helpers_1.validateLimit)(opts.limit ?? 50);
48
- // 过滤 key 对齐 BAM Span 字段名:
49
- // - 顶层字段:traceID(注意大小写)
50
- // - attributes 业务字段:user_id(snake_case)
51
- // - 入口 span:BAM 没有 entrySpanName fieldFilter,统一走 fuzzyFilter(trace endpoint
52
- // 默认作用于 span name),所以 --root-span 不进 fieldFilters
53
- const fieldFilters = (0, helpers_1.buildFieldFilters)([
54
- { key: "trace_id", value: opts.traceId ? (0, helpers_1.eqFilter)(opts.traceId) : undefined },
55
- { key: "user_id", value: opts.userId ? (0, helpers_1.eqFilter)(opts.userId, "i64") : undefined },
56
- ]);
57
- const req = {
58
- appID,
59
- appEnv,
60
- startTimestampNs: (0, helpers_1.parseToNs)(opts.since),
61
- endTimestampNs: (0, helpers_1.parseToNs)(opts.until),
62
- limit,
63
- pageToken: opts.cursor,
64
- fieldFilters,
65
- fuzzyFilter: opts.rootSpan ? (0, helpers_1.fuzzyFilter)(opts.rootSpan) : undefined,
66
- };
67
- const resp = await api.observability.searchTraces(req);
68
- emitSearchTracesResponse(resp);
69
- }
70
- function emitSearchTracesResponse(resp) {
71
- (0, output_1.emit)({
72
- data: resp.spans ?? [],
73
- next_cursor: resp.hasMore ? resp.nextPageToken : null,
74
- has_more: resp.hasMore,
75
- }, index_1.spanSchema);
76
- }
77
- /** miaoda observability trace get <trace-id> */
78
- async function handleObservabilityTraceGet(opts) {
79
- if (!opts.traceId)
80
- (0, shared_1.failArgs)("<trace-id> 必填");
81
- const appID = (0, shared_1.resolveAppId)({ appId: opts.appId });
82
- const appEnv = "runtime";
83
- const resp = await api.observability.getTraces({
84
- appID,
85
- appEnv,
86
- traceID: opts.traceId,
87
- withLogSeverityCount: opts.withLogSeverityCount,
88
- });
89
- emitGetTraceResponse(resp);
90
- }
91
- function emitGetTraceResponse(resp) {
92
- // JSON 模式:保持单条信封语义,便于下游一次解析
93
- if ((0, output_1.isJsonMode)()) {
94
- (0, output_1.emit)({
95
- data: { spans: resp.spans ?? [], isBreak: resp.isBreak },
96
- });
97
- return;
98
- }
99
- // Pretty 模式:先打头部 key-value(断链状态等元信息),再按 spanSchema 渲染主表
100
- (0, output_1.emit)({ data: { isBreak: resp.isBreak } });
101
- (0, output_1.emit)({ data: resp.spans ?? [] }, index_1.spanSchema);
102
- }
@@ -1,28 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mapDevopsError = mapDevopsError;
4
- const error_1 = require("./error");
5
- /**
6
- * 把 BAM lark.apaas.devops 的业务码(数字字符串)映射成 CLI 错误码。
7
- * 来源:服务端技术方案 wiki L7P8wlNQ5iyShLkxzlOcCo8LnTg「业务错误码映射表」。
8
- *
9
- * 放在 utils 下而不是某个具体 api 域里:app / deploy 都跑在 lark.apaas.devops
10
- * 这同一 PSM,业务码命名空间也共享;boundaries 规则又禁止 api 域间互引。
11
- */
12
- function mapDevopsError(bizCode, message) {
13
- switch (bizCode) {
14
- case "400002577":
15
- case "400002569":
16
- return new error_1.AppError("APP_NOT_FOUND", message, {
17
- next_actions: ["Run `miaoda app list` to see available apps"],
18
- });
19
- case "400002579":
20
- return new error_1.AppError("APP_PERMISSION_DENIED", message);
21
- case "400002575":
22
- return new error_1.AppError("INVALID_ARG_VALUE", message, {
23
- next_actions: ["缩短 --name / --description 后重试(后端有长度限制)"],
24
- });
25
- default:
26
- return undefined;
27
- }
28
- }
package/dist/utils/git.js DELETED
@@ -1,29 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getCurrentGitBranch = getCurrentGitBranch;
4
- const node_child_process_1 = require("node:child_process");
5
- /**
6
- * 读取 cwd 所在仓库的当前分支名。
7
- *
8
- * 不在 git 仓库、git 不可用、或处于 detached HEAD 时返回 undefined,
9
- * 让调用方决定回退策略(如交给后端使用应用默认分支)。
10
- */
11
- function getCurrentGitBranch(cwd = process.cwd()) {
12
- let result;
13
- try {
14
- result = (0, node_child_process_1.spawnSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
15
- cwd,
16
- encoding: "utf8",
17
- stdio: ["ignore", "pipe", "ignore"],
18
- });
19
- }
20
- catch {
21
- return undefined;
22
- }
23
- if (result.status !== 0)
24
- return undefined;
25
- const branch = result.stdout.trim();
26
- if (!branch || branch === "HEAD")
27
- return undefined;
28
- return branch;
29
- }
@@ -1,132 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseTimeToMs = parseTimeToMs;
4
- exports.msToNs = msToNs;
5
- exports.msToSec = msToSec;
6
- exports.parseToMs = parseToMs;
7
- exports.parseToNs = parseToNs;
8
- exports.parseToSec = parseToSec;
9
- exports.floorMsToBucket = floorMsToBucket;
10
- exports.ceilMsToBucket = ceilMsToBucket;
11
- const error_1 = require("./error");
12
- const TIMESTAMP_HELP = "支持格式:相对时间 1h/2d/1w;日期 2026-04-01;ISO 2026-04-01T10:00:00Z";
13
- /**
14
- * 解析 PRD 时间格式。返回毫秒时间戳。
15
- * - 相对时间:`1h`、`2d`、`1w`、`30m`(h=小时, d=天, w=周, m=分钟)
16
- * - 日期:`2026-04-01`(按当日 00:00:00 UTC)
17
- * - ISO 8601:`2026-04-01T10:00:00Z`
18
- *
19
- * 失败抛 AppError("ARGS_INVALID", ...);CLI 层用 withHelp 自动转 exit 2 + help。
20
- */
21
- function parseTimeToMs(input, now = new Date()) {
22
- const relative = /^(\d+)([mhdw])$/.exec(input);
23
- if (relative) {
24
- const n = Number(relative[1]);
25
- const unit = relative[2];
26
- const factor = unit === "m"
27
- ? 60_000
28
- : unit === "h"
29
- ? 3_600_000
30
- : unit === "d"
31
- ? 86_400_000
32
- : /* w */ 604_800_000;
33
- return now.getTime() - n * factor;
34
- }
35
- if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
36
- const ms = Date.parse(input + "T00:00:00Z");
37
- if (Number.isNaN(ms))
38
- failInvalidTimestamp(input);
39
- return ms;
40
- }
41
- const ms = Date.parse(input);
42
- if (Number.isNaN(ms))
43
- failInvalidTimestamp(input);
44
- return ms;
45
- }
46
- function failInvalidTimestamp(input) {
47
- throw new error_1.AppError("ARGS_INVALID", `无法解析时间 '${input}'。${TIMESTAMP_HELP}`);
48
- }
49
- /** 毫秒 → 纳秒(字符串,避免 JS Number 精度丢失) */
50
- function msToNs(ms) {
51
- return `${String(ms)}000000`;
52
- }
53
- /** 毫秒 → 秒(字符串,向下取整) */
54
- function msToSec(ms) {
55
- return String(Math.floor(ms / 1000));
56
- }
57
- /** 解析到毫秒数;input 为空返回 undefined */
58
- function parseToMs(input, now) {
59
- if (!input)
60
- return undefined;
61
- return parseTimeToMs(input, now);
62
- }
63
- /** 解析到纳秒字符串;input 为空返回 undefined */
64
- function parseToNs(input, now) {
65
- if (!input)
66
- return undefined;
67
- return msToNs(parseTimeToMs(input, now));
68
- }
69
- /** 解析到秒字符串;input 为空返回 undefined */
70
- function parseToSec(input, now) {
71
- if (!input)
72
- return undefined;
73
- return msToSec(parseTimeToMs(input, now));
74
- }
75
- // ── 桶边界对齐 ──
76
- //
77
- // metric 的 down-sample 与 analytics 的 timeAggregationUnit 都按 UTC 桶切片。
78
- // 当用户传的 since/until 落在桶中间,服务端可能返回不到该桶(边界数据丢失)。
79
- // 约定:since 向下取整、until 向上取整,把 since/until 各自所在桶完整纳入。
80
- //
81
- // 周界以 ISO 周(周一为周首,UTC);月按 UTC 月初对齐。
82
- const MS_MIN = 60_000;
83
- const MS_HOUR = 3_600_000;
84
- const MS_DAY = 86_400_000;
85
- /** 把 ms 向下对齐到桶起点(UTC)。未识别的 bucket 原样返回。 */
86
- function floorMsToBucket(ms, bucket) {
87
- switch (bucket) {
88
- case "1m":
89
- return Math.floor(ms / MS_MIN) * MS_MIN;
90
- case "1h":
91
- return Math.floor(ms / MS_HOUR) * MS_HOUR;
92
- case "1d":
93
- case "DAY": {
94
- const d = new Date(ms);
95
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
96
- }
97
- case "WEEK": {
98
- const d = new Date(ms);
99
- const offsetDays = (d.getUTCDay() + 6) % 7; // 距上一个周一的天数
100
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offsetDays);
101
- }
102
- case "MONTH": {
103
- const d = new Date(ms);
104
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
105
- }
106
- default:
107
- return ms;
108
- }
109
- }
110
- /** 把 ms 向上对齐到下一个桶起点(UTC);恰在边界则不变。 */
111
- function ceilMsToBucket(ms, bucket) {
112
- const floor = floorMsToBucket(ms, bucket);
113
- if (floor === ms)
114
- return ms;
115
- switch (bucket) {
116
- case "1m":
117
- return floor + MS_MIN;
118
- case "1h":
119
- return floor + MS_HOUR;
120
- case "1d":
121
- case "DAY":
122
- return floor + MS_DAY;
123
- case "WEEK":
124
- return floor + 7 * MS_DAY;
125
- case "MONTH": {
126
- const d = new Date(floor);
127
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
128
- }
129
- default:
130
- return ms;
131
- }
132
- }