@meyicloud/meyi-cost-server 1.7.0 → 1.7.1
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/package.json +1 -1
- package/src/controllers/cost-analysis.controller.js +5 -0
- package/src/repositories/cost-analysis-report.repository.js +16 -2
- package/src/routes/index.js +1 -0
- package/src/services/cost-analyser.service.js +7 -1
- package/src/services/cost-analysis-report.service.js +19 -0
- package/src/services/cost-report-artifact.service.js +18 -2
- package/src/workers/cost-analysis-report.worker.js +1 -1
package/package.json
CHANGED
|
@@ -36,6 +36,11 @@ export class CostAnalysisController {
|
|
|
36
36
|
catch (error) { return next(error); }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
async removeReport(req, res, next) {
|
|
40
|
+
try { return res.json(await this.reportService.remove(req, req.params.id)); }
|
|
41
|
+
catch (error) { return next(error); }
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
async downloadPdf(req, res, next) {
|
|
40
45
|
try {
|
|
41
46
|
const report = await this.reportService.pdf(req, req.params.id);
|
|
@@ -71,8 +71,14 @@ export class CostAnalysisReportRepository {
|
|
|
71
71
|
return rows(await this.db.execute(sql`UPDATE ${sql.raw(this.reports)} SET status = 'RUNNING', attempt_count = attempt_count + 1, started_at = now(), error = NULL, updated_at = now() WHERE id = ${id} AND status = 'PENDING' RETURNING *`))[0] || null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
async
|
|
75
|
-
await this.db.execute(sql`
|
|
74
|
+
async setTaskStarted(id, { taskArn, provider, modelId, source, artifactBucket, markdownKey, pdfKey }) {
|
|
75
|
+
await this.db.execute(sql`
|
|
76
|
+
UPDATE ${sql.raw(this.reports)}
|
|
77
|
+
SET ecs_task_arn = ${taskArn}, provider_name = ${provider}, model_id = ${modelId},
|
|
78
|
+
provider_source = ${source}, artifact_bucket = ${artifactBucket},
|
|
79
|
+
markdown_key = ${markdownKey}, pdf_key = ${pdfKey}, updated_at = now()
|
|
80
|
+
WHERE id = ${id}
|
|
81
|
+
`);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
async completeExternal(id, result) {
|
|
@@ -125,4 +131,12 @@ export class CostAnalysisReportRepository {
|
|
|
125
131
|
FROM ${sql.raw(this.reports)} WHERE tenant_id = ${tenantId} AND id = ${id} LIMIT 1
|
|
126
132
|
`))[0] || null;
|
|
127
133
|
}
|
|
134
|
+
|
|
135
|
+
async remove(tenantId, id) {
|
|
136
|
+
return rows(await this.db.execute(sql`
|
|
137
|
+
DELETE FROM ${sql.raw(this.reports)}
|
|
138
|
+
WHERE tenant_id = ${tenantId} AND id = ${id}
|
|
139
|
+
RETURNING id
|
|
140
|
+
`))[0] || null;
|
|
141
|
+
}
|
|
128
142
|
}
|
package/src/routes/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export function createCostRouter({ costController, budgetController, costAnalysi
|
|
|
15
15
|
router.put("/analysis/schedule", costAnalysisController.updateSchedule.bind(costAnalysisController));
|
|
16
16
|
router.get("/analysis/reports", costAnalysisController.reports.bind(costAnalysisController));
|
|
17
17
|
router.get("/analysis/reports/:id", costAnalysisController.report.bind(costAnalysisController));
|
|
18
|
+
router.delete("/analysis/reports/:id", costAnalysisController.removeReport.bind(costAnalysisController));
|
|
18
19
|
router.get("/analysis/reports/:id/pdf", costAnalysisController.downloadPdf.bind(costAnalysisController));
|
|
19
20
|
router.get("/analysis/reports/:id/markdown", costAnalysisController.downloadMarkdown.bind(costAnalysisController));
|
|
20
21
|
router.get("/budgets", budgetController.list.bind(budgetController));
|
|
@@ -218,7 +218,13 @@ export class CostAnalyserService {
|
|
|
218
218
|
throw Object.assign(new Error(`Unable to start cost analyser: ${reason}`), { name: "CostAnalyserLaunchError", statusCode: 502 });
|
|
219
219
|
}
|
|
220
220
|
const taskArn = launched.tasks[0].taskArn;
|
|
221
|
-
await onStarted?.(
|
|
221
|
+
await onStarted?.({
|
|
222
|
+
taskArn,
|
|
223
|
+
...this.statusSummary(provider),
|
|
224
|
+
artifactBucket: this.reportBucket,
|
|
225
|
+
markdownKey: `${prefix}/report.md`,
|
|
226
|
+
pdfKey: `${prefix}/report.pdf`,
|
|
227
|
+
});
|
|
222
228
|
const deadline = Date.now() + this.timeoutMs;
|
|
223
229
|
while (Date.now() < deadline) {
|
|
224
230
|
const response = await this.ecs.send(new DescribeTasksCommand({ cluster: this.cluster, tasks: [taskArn] }));
|
|
@@ -49,6 +49,25 @@ export class CostAnalysisReportService {
|
|
|
49
49
|
return reportDto(row);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
async remove(req, id) {
|
|
53
|
+
if (String(req.user?.role || "").toLowerCase() !== "admin") {
|
|
54
|
+
throw Object.assign(new Error("Administrator access is required to remove AI report history"), { statusCode: 403 });
|
|
55
|
+
}
|
|
56
|
+
const tenantId = this.tenant(req);
|
|
57
|
+
const row = await this.repository.find(tenantId, id, true);
|
|
58
|
+
if (!row) throw Object.assign(new Error("AI cost report was not found"), { statusCode: 404 });
|
|
59
|
+
if (row.status === "PENDING" || row.status === "RUNNING") {
|
|
60
|
+
throw Object.assign(new Error("A running AI cost report cannot be removed"), { statusCode: 409 });
|
|
61
|
+
}
|
|
62
|
+
const artifactKey = row.markdown_key || row.pdf_key;
|
|
63
|
+
if (row.artifact_bucket && artifactKey) {
|
|
64
|
+
const slash = artifactKey.lastIndexOf("/");
|
|
65
|
+
await this.artifactService.removePrefix(row.artifact_bucket, slash >= 0 ? artifactKey.slice(0, slash + 1) : artifactKey);
|
|
66
|
+
}
|
|
67
|
+
await this.repository.remove(tenantId, id);
|
|
68
|
+
return { success: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
async pdf(req, id) {
|
|
53
72
|
const row = await this.repository.find(this.tenant(req), id, true);
|
|
54
73
|
if (!row) throw Object.assign(new Error("AI cost report was not found"), { statusCode: 404 });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
1
|
+
import { DeleteObjectsCommand, GetObjectCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
|
|
2
2
|
|
|
3
3
|
async function bytes(body) {
|
|
4
4
|
if (!body) return Buffer.alloc(0);
|
|
@@ -18,5 +18,21 @@ export class CostReportArtifactService {
|
|
|
18
18
|
const response = await this.client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
19
19
|
return bytes(response.Body);
|
|
20
20
|
}
|
|
21
|
-
}
|
|
22
21
|
|
|
22
|
+
async removePrefix(bucket, prefix) {
|
|
23
|
+
if (!bucket || !prefix) return;
|
|
24
|
+
let continuationToken;
|
|
25
|
+
do {
|
|
26
|
+
const page = await this.client.send(new ListObjectsV2Command({
|
|
27
|
+
Bucket: bucket,
|
|
28
|
+
Prefix: prefix,
|
|
29
|
+
ContinuationToken: continuationToken,
|
|
30
|
+
}));
|
|
31
|
+
const objects = (page.Contents || []).map(({ Key }) => ({ Key })).filter(({ Key }) => Boolean(Key));
|
|
32
|
+
if (objects.length) {
|
|
33
|
+
await this.client.send(new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: objects, Quiet: true } }));
|
|
34
|
+
}
|
|
35
|
+
continuationToken = page.IsTruncated ? page.NextContinuationToken : undefined;
|
|
36
|
+
} while (continuationToken);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -27,7 +27,7 @@ export class CostAnalysisReportWorker {
|
|
|
27
27
|
reportId: report.id,
|
|
28
28
|
range,
|
|
29
29
|
frequency: schedule.frequency,
|
|
30
|
-
onStarted: (
|
|
30
|
+
onStarted: (metadata) => this.repository.setTaskStarted(report.id, metadata),
|
|
31
31
|
});
|
|
32
32
|
await this.repository.completeExternal(report.id, result);
|
|
33
33
|
await this.repository.setNextRun(schedule.tenant_id, nextRunAt, true, schedule.next_run_at);
|