diff --git a/.runtime/portal/scripts/runtime-slo-report.mjs b/.runtime/portal/scripts/runtime-slo-report.mjs
index 88ec96f..77309cf 100755
--- a/.runtime/portal/scripts/runtime-slo-report.mjs
+++ b/.runtime/portal/scripts/runtime-slo-report.mjs
@@ -87,6 +87,30 @@ function uniquePaths(paths) {
return result;
}
+function parseArgs(argv) {
+ const args = {
+ writeReport: false,
+ reportDir: process.env.MEMIND_RUNTIME_REPORT_DIR || '',
+ };
+ for (let i = 0; i < argv.length; i += 1) {
+ const item = argv[i];
+ if (item === '--write-report') args.writeReport = true;
+ else if (item === '--report-dir') args.reportDir = String(argv[++i] ?? args.reportDir);
+ else if (item === '--help' || item === '-h') args.help = true;
+ }
+ return args;
+}
+
+function printHelp() {
+ console.log([
+ 'Usage:',
+ ' node scripts/runtime-slo-report.mjs [--write-report] [--report-dir
]',
+ '',
+ 'Default behavior is read-only and writes nothing.',
+ '--write-report writes JSON and Markdown snapshots to an operations report directory.',
+ ].join('\n'));
+}
+
function tableCountQueries() {
return [
['h5_users', 'users'],
@@ -178,6 +202,11 @@ function summarizeRuntime(runtimeJson) {
const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env');
const appRoot = process.env.MEMIND_APP_ROOT || path.dirname(path.resolve(envFile));
loadEnvFile(envFile);
+const args = parseArgs(process.argv.slice(2));
+if (args.help) {
+ printHelp();
+ process.exit(0);
+}
const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, '');
const redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL || 'redis://127.0.0.1:6379/0';
@@ -244,8 +273,67 @@ const report = {
database: false,
mindSpace: false,
redis: false,
+ report: Boolean(args.writeReport),
},
};
+function markdownReport(payload) {
+ const workers = payload.runtime?.workers ?? [];
+ const lines = [
+ `# Memind Runtime SLO Report`,
+ '',
+ `- checkedAt: ${payload.checkedAt}`,
+ `- ok: ${payload.ok}`,
+ `- failures: ${payload.failures.length ? payload.failures.join(', ') : 'none'}`,
+ `- publicBase: ${payload.publicBase}`,
+ `- appRoot: ${payload.appRoot}`,
+ '',
+ '## Workers',
+ '',
+ '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | metricsFresh | score |',
+ '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |',
+ ];
+ for (const worker of workers) {
+ lines.push([
+ worker.id,
+ worker.healthy,
+ worker.activeStreams,
+ worker.streamErrorCount,
+ `${worker.firstToken5m?.p50Ms ?? 0}/${worker.firstToken5m?.p95Ms ?? 0}`,
+ `${worker.firstToken1h?.p50Ms ?? 0}/${worker.firstToken1h?.p95Ms ?? 0}`,
+ worker.metricsFresh,
+ worker.score,
+ ].join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
+ }
+ lines.push(
+ '',
+ '## Tool Queue',
+ '',
+ '```json',
+ JSON.stringify(payload.runtime?.toolQueue ?? null, null, 2),
+ '```',
+ '',
+ '## Writes',
+ '',
+ '```json',
+ JSON.stringify(payload.writes, null, 2),
+ '```',
+ '',
+ );
+ return lines.join('\n');
+}
+
+if (args.writeReport) {
+ const reportDir = path.resolve(args.reportDir || path.join(appRoot, 'reports', 'runtime-slo'));
+ fs.mkdirSync(reportDir, { recursive: true });
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+ report.reportFiles = {
+ json: path.join(reportDir, `${stamp}.json`),
+ markdown: path.join(reportDir, `${stamp}.md`),
+ };
+ fs.writeFileSync(report.reportFiles.json, `${JSON.stringify(report, null, 2)}\n`);
+ fs.writeFileSync(report.reportFiles.markdown, markdownReport(report));
+}
+
console.log(JSON.stringify(report, null, 2));
process.exit(report.ok ? 0 : 1);
diff --git a/scripts/runtime-slo-report.mjs b/scripts/runtime-slo-report.mjs
index 88ec96f..77309cf 100755
--- a/scripts/runtime-slo-report.mjs
+++ b/scripts/runtime-slo-report.mjs
@@ -87,6 +87,30 @@ function uniquePaths(paths) {
return result;
}
+function parseArgs(argv) {
+ const args = {
+ writeReport: false,
+ reportDir: process.env.MEMIND_RUNTIME_REPORT_DIR || '',
+ };
+ for (let i = 0; i < argv.length; i += 1) {
+ const item = argv[i];
+ if (item === '--write-report') args.writeReport = true;
+ else if (item === '--report-dir') args.reportDir = String(argv[++i] ?? args.reportDir);
+ else if (item === '--help' || item === '-h') args.help = true;
+ }
+ return args;
+}
+
+function printHelp() {
+ console.log([
+ 'Usage:',
+ ' node scripts/runtime-slo-report.mjs [--write-report] [--report-dir ]',
+ '',
+ 'Default behavior is read-only and writes nothing.',
+ '--write-report writes JSON and Markdown snapshots to an operations report directory.',
+ ].join('\n'));
+}
+
function tableCountQueries() {
return [
['h5_users', 'users'],
@@ -178,6 +202,11 @@ function summarizeRuntime(runtimeJson) {
const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env');
const appRoot = process.env.MEMIND_APP_ROOT || path.dirname(path.resolve(envFile));
loadEnvFile(envFile);
+const args = parseArgs(process.argv.slice(2));
+if (args.help) {
+ printHelp();
+ process.exit(0);
+}
const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, '');
const redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL || 'redis://127.0.0.1:6379/0';
@@ -244,8 +273,67 @@ const report = {
database: false,
mindSpace: false,
redis: false,
+ report: Boolean(args.writeReport),
},
};
+function markdownReport(payload) {
+ const workers = payload.runtime?.workers ?? [];
+ const lines = [
+ `# Memind Runtime SLO Report`,
+ '',
+ `- checkedAt: ${payload.checkedAt}`,
+ `- ok: ${payload.ok}`,
+ `- failures: ${payload.failures.length ? payload.failures.join(', ') : 'none'}`,
+ `- publicBase: ${payload.publicBase}`,
+ `- appRoot: ${payload.appRoot}`,
+ '',
+ '## Workers',
+ '',
+ '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | metricsFresh | score |',
+ '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |',
+ ];
+ for (const worker of workers) {
+ lines.push([
+ worker.id,
+ worker.healthy,
+ worker.activeStreams,
+ worker.streamErrorCount,
+ `${worker.firstToken5m?.p50Ms ?? 0}/${worker.firstToken5m?.p95Ms ?? 0}`,
+ `${worker.firstToken1h?.p50Ms ?? 0}/${worker.firstToken1h?.p95Ms ?? 0}`,
+ worker.metricsFresh,
+ worker.score,
+ ].join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
+ }
+ lines.push(
+ '',
+ '## Tool Queue',
+ '',
+ '```json',
+ JSON.stringify(payload.runtime?.toolQueue ?? null, null, 2),
+ '```',
+ '',
+ '## Writes',
+ '',
+ '```json',
+ JSON.stringify(payload.writes, null, 2),
+ '```',
+ '',
+ );
+ return lines.join('\n');
+}
+
+if (args.writeReport) {
+ const reportDir = path.resolve(args.reportDir || path.join(appRoot, 'reports', 'runtime-slo'));
+ fs.mkdirSync(reportDir, { recursive: true });
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+ report.reportFiles = {
+ json: path.join(reportDir, `${stamp}.json`),
+ markdown: path.join(reportDir, `${stamp}.md`),
+ };
+ fs.writeFileSync(report.reportFiles.json, `${JSON.stringify(report, null, 2)}\n`);
+ fs.writeFileSync(report.reportFiles.markdown, markdownReport(report));
+}
+
console.log(JSON.stringify(report, null, 2));
process.exit(report.ok ? 0 : 1);