Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions bds-tools/src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
* log.ts — bds-tools 统一日志实例
*
* stdout bare + 落盘 LOG_PATH (<ROOT>/.sfmc/logs/bds-update.log)。
* check-update.ts 是独立入口,用 source = "updater" 单独创建 logger,
* 见该文件内的 createUpdaterLogger()。
* check-update.ts 是独立入口,用 source = "updater" 单独创建 logger。
*/

import { createNodeServiceLogger } from "@sfmc-bds/sdk/logs";
Expand All @@ -14,9 +13,7 @@ export const log = createNodeServiceLogger({
logPath: LOG_PATH,
});

/** 关闭文件流 (进程退出前调用,确保缓冲落盘) */
/** 兼容钩子:FileSink 为 sync append,close 为空操作 */
export function closeLog(): void {
log.close();
}

process.on("exit", () => closeLog());
9 changes: 2 additions & 7 deletions db-server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ import {
ensureCoreConfigs,
loadEnsuredConfig,
modulePath,
resolveRuntimeRoot,
DEFAULT_DB_CONFIG,
DEFAULT_QQ_CONFIG,
} from "@sfmc-bds/sdk/node/config";
import { isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { log } from "./lib/log.js";
import { PROJECT_ROOT as RESOLVED_ROOT } from "./project-root.js";

import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export interface EnvConfig {
PROJECT_ROOT: string;
Expand All @@ -41,7 +36,7 @@ export interface EnvConfig {
}

export function loadEnv(): EnvConfig {
const PROJECT_ROOT = resolveRuntimeRoot(resolve(__dirname, "..", ".."));
const PROJECT_ROOT = RESOLVED_ROOT;
/* 启动时确认 db/qq/permissions 存在;不存在就写带默认值的骨架。
* 不依赖 wizard:wizard 只填字段,骨架由服务自己 ensure。 */
ensureCoreConfigs(PROJECT_ROOT, ["db_config", "qq_config", "permissions"]);
Expand Down
12 changes: 3 additions & 9 deletions db-server/src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,10 @@
*/

import { createNodeServiceLogger } from "@sfmc-bds/sdk/logs";
import { logFile, resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

/** src/lib → 仓根(与 env.ts 的 src 上溯差一层) */
const ROOT = resolveRuntimeRoot(resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."));
import { logFile } from "@sfmc-bds/sdk/node/config";
import { PROJECT_ROOT } from "../project-root.js";

export const log = createNodeServiceLogger({
source: "db",
logPath: logFile(ROOT, "db"),
logPath: logFile(PROJECT_ROOT, "db"),
});

process.on("exit", () => log.close());
12 changes: 12 additions & 0 deletions db-server/src/project-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* project-root.ts — 仓根唯一解析点(DRY/DIP:env / log 勿各自上溯)
*/

import { resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));

/** SFMC_ROOT > 相对 db-server/src 上溯两级 */
export const PROJECT_ROOT: string = resolveRuntimeRoot(resolve(HERE, "..", ".."));
4 changes: 3 additions & 1 deletion modules/sdk/@sfmc-sdk/src/logs/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export interface Logger {

/** Node 仓顶服务标准 logger:stdout(可 bare) + 文件落盘 */
export interface NodeServiceLogger extends Logger {
/** 关闭文件 sink */
/**
* 关闭文件 sink(FileSink 为 sync append 时为空操作;保留统一调用面)。
*/
close(): void;
readonly fileSink: FileSink;
}
Expand Down
9 changes: 5 additions & 4 deletions modules/sdk/@sfmc-sdk/src/logs/sink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* sink.ts — 日志输出目标实现
*
* StdoutSink: 输出到 stdout (可选颜色,可选 stderr 路由 error)
* FileSink: 追加写入文件 (纯文本,无 ANSI 码,单例 FD)
* FileSink: 同步追加写入文件 (纯文本,无 ANSI 码;进程退出无需 flush)
*/

import fs from "node:fs";
Expand Down Expand Up @@ -49,12 +49,13 @@ export function createStdoutSink(opts: StdoutSinkOptions = {}): Sink {
export interface FileSinkOptions {
/** 是否自动创建父目录 (默认 true) */
mkdir?: boolean;
/** 文件打开模式 (默认 "a" 追加) */
flags?: string;
}

export interface FileSink extends Sink {
/** 关闭底层文件流,释放 FD */
/**
* 兼容钩子:当前实现为 sync appendFile,无持有 FD,调用为空操作。
* 保留以便 NodeServiceLogger / 进程 exit 统一调用而不破坏 LSP。
*/
close(): void;
}

Expand Down
11 changes: 3 additions & 8 deletions qq-bridge/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,14 @@ import {
configPath,
DEFAULT_QQ_CONFIG,
loadEnsuredConfig,
resolveRuntimeRoot,
stripConfigMeta,
} from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { log } from "./log.js";
import { PROJECT_ROOT } from "./project-root.js";
import type { QQBridgeConfig } from "./types.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

/** 统一通过 SDK 解析项目根:env SFMC_ROOT > __dirname 上溯。 */
export const ROOT_DIR: string = resolveRuntimeRoot(resolve(__dirname, "..", ".."));
/** 统一通过 SDK 解析项目根:env SFMC_ROOT > project-root 上溯。 */
export const ROOT_DIR: string = PROJECT_ROOT;
export const CFG_PATH: string = configPath(ROOT_DIR, "qq_config.json");

function applyDefaults(raw: Partial<QQBridgeConfig>): QQBridgeConfig {
Expand Down
11 changes: 3 additions & 8 deletions qq-bridge/src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@
*/

import { createNodeServiceLogger } from "@sfmc-bds/sdk/logs";
import { logFile, resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = resolveRuntimeRoot(resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."));
import { logFile } from "@sfmc-bds/sdk/node/config";
import { PROJECT_ROOT } from "./project-root.js";

export const log = createNodeServiceLogger({
source: "qq",
logPath: logFile(ROOT, "qq"),
logPath: logFile(PROJECT_ROOT, "qq"),
});

process.on("exit", () => log.close());
12 changes: 12 additions & 0 deletions qq-bridge/src/project-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* project-root.ts — 仓根唯一解析点(DRY/DIP:config / log 勿各自上溯)
*/

import { resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));

/** SFMC_ROOT > 相对 qq-bridge/src 上溯两级 */
export const PROJECT_ROOT: string = resolveRuntimeRoot(resolve(HERE, "..", ".."));
2 changes: 1 addition & 1 deletion sfmc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"start": "node ./dist/main.js",
"build": "node ../scripts/esbuild-transpile.mjs --dts",
"typecheck": "tsc7 --noEmit -p tsconfig.json",
"test": "npm run build && node --test pack-update-policy.test.mjs pack-update-config.test.mjs terminal-progress.test.mjs wrap-log-line.test.mjs",
"test": "npm run build && node --test pack-update-policy.test.mjs pack-update-config.test.mjs terminal-progress.test.mjs wrap-log-line.test.mjs world-packs-uninstall.test.mjs",
"prepublishOnly": "npm run build"
},
"dependencies": {
Expand Down
120 changes: 59 additions & 61 deletions sfmc/src/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,25 +114,39 @@ export const SOURCE_META: SourceMeta[] = [
{ value: "bds-tools", name: "BDSTools", paint: (s) => c.red(s) },
];

/** 源标签无色文本(formatSourceTag / logPrefixWidth 共用) */
function sourceTagPlain(source: string): string {
const meta = SOURCE_META.find((m) => m.value === source);
if (meta) return `[${meta.name}]`;
return `[${source.padEnd(7).slice(0, 8)}]`;
}

export function formatSourceTag(source: string): string {
const meta = SOURCE_META.find((m) => m.value === source);
if (meta) return meta.paint(`[${meta.name}]`);
return c.bold(`[${source.padEnd(7).slice(0, 8)}]`);
const plain = sourceTagPlain(source);
if (meta) return meta.paint(plain);
return c.bold(plain);
}

/** 简化BDS日志 */
function stripLogPrefix(line: string): string {
// 匹配格式: [2026-07-18 23:56:06:778 INFO]
const prefixRegex = /^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\d{3} (INFO|WARNING|ERROR|FATAL|DEBUG)\]\s*/;
return line.replace(prefixRegex, "");
return line.replace(BDS_TS_PREFIX_RE, "");
}

/** BDS 行首时间戳+级别前缀(strip / 解析级别共用,DRY) */
const BDS_TS_PREFIX_RE =
/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\d{3} (INFO|WARNING|ERROR|FATAL|DEBUG)\]\s*/i;

/**
* 从一行 BDS 日志中提取日志等级
* @param line 日志行字符串
* @returns 日志等级(大写),若无法识别则返回 'UNKNOWN'
*/
function getLogLevel(line: string): string {
const fromTs = BDS_TS_PREFIX_RE.exec(line);
if (fromTs?.[1]) return fromTs[1].toUpperCase();

const levelNames = ["INFO", "WARNING", "ERROR", "FATAL", "DEBUG", "TRACE", "WARN"];
const levelPattern = levelNames.join("|");

Expand All @@ -151,44 +165,56 @@ function getLogLevel(line: string): string {
return "UNKNOWN";
}

/** 格式化日志用于 REPL 展示 (用 theme.ts chalk 配色) */
export function formatLog(l: UnifiedLog): string {
const ts = c.dim(l.time.toLocaleTimeString());
let lvl = levelTag(l.level);
let txt = highlightLogLine(l.text);
const src = formatSourceTag(l.source);
if (l.source === "bds") {
const parsed = getLogLevel(l.text);
const mapped: LogLevel =
parsed === "WARNING" || parsed === "WARN"
? "warn"
: parsed === "ERROR" || parsed === "FATAL"
? "error"
: parsed === "DEBUG" || parsed === "TRACE"
? "debug"
: "info";
/* 去掉 BDS 自带时间戳前缀后再高亮正文 */
txt = highlightLogLine(stripLogPrefix(l.text));
lvl = levelTag(mapped);
}
return `${ts} ${src} ${lvl} ${txt}`;
/** 展示用级别:BDS 行从正文解析,其余用 entry.level(DRY:formatLog / logPrefixWidth 共用) */
export function resolveDisplayLevel(l: UnifiedLog): LogLevel {
if (l.source !== "bds") return l.level;
const parsed = getLogLevel(l.text);
if (parsed === "WARNING" || parsed === "WARN") return "warn";
if (parsed === "ERROR" || parsed === "FATAL") return "error";
if (parsed === "DEBUG" || parsed === "TRACE") return "debug";
return "info";
}

/** 无色级别标签文本(可见宽度权威源) */
const LEVEL_TAG_TEXT: Record<LogLevel, string> = {
error: "[ERR]",
warn: "[WRN]",
success: "[OK]",
debug: "[DBG]",
info: "[INF]",
};

function levelTagPlain(lvl: LogLevel): string {
return LEVEL_TAG_TEXT[lvl] ?? LEVEL_TAG_TEXT.info;
}

function levelTag(lvl: LogLevel): string {
const text = levelTagPlain(lvl);
switch (lvl) {
case "error":
return c.red("[ERR]");
return c.red(text);
case "warn":
return c.yellow("[WRN]");
return c.yellow(text);
case "success":
return c.green(c.bold("[OK]"));
return c.green(c.bold(text));
case "debug":
return c.dim("[DBG]");
return c.dim(text);
default:
return c.blue("[INF]");
return c.blue(text);
}
}

/** 格式化日志用于 REPL 展示 (用 theme.ts chalk 配色) */
export function formatLog(l: UnifiedLog): string {
const ts = c.dim(l.time.toLocaleTimeString());
const level = resolveDisplayLevel(l);
const lvl = levelTag(level);
const src = formatSourceTag(l.source);
/* BDS:去掉自带时间戳前缀后再高亮正文 */
const txt = highlightLogLine(l.source === "bds" ? stripLogPrefix(l.text) : l.text);
return `${ts} ${src} ${lvl} ${txt}`;
}

/* ==================================================================
* 软换行: 超终端宽度时换行,后续行缩进对齐
* ================================================================== */
Expand Down Expand Up @@ -219,42 +245,14 @@ export function visibleWidth(s: string): number {
return w;
}

/** 无色级别标签,与 levelTag 可见宽度一致 */
function levelTagPlain(lvl: LogLevel): string {
switch (lvl) {
case "error":
return "[ERR]";
case "warn":
return "[WRN]";
case "success":
return "[OK]";
case "debug":
return "[DBG]";
default:
return "[INF]";
}
}

/**
* 日志前缀可见宽度(时间 + 源 + 级别 + 尾空格),供悬挂缩进对齐正文起点。
* 与 formatLog 拼接顺序保持一致。
* 与 formatLog 拼接顺序保持一致(共用 resolveDisplayLevel / sourceTagPlain / levelTagPlain)
*/
export function logPrefixWidth(l: UnifiedLog): number {
const ts = l.time.toLocaleTimeString();
const meta = SOURCE_META.find((m) => m.value === l.source);
const src = meta ? `[${meta.name}]` : `[${l.source.padEnd(7).slice(0, 8)}]`;
let level: LogLevel = l.level;
if (l.source === "bds") {
const parsed = getLogLevel(l.text);
level =
parsed === "WARNING" || parsed === "WARN"
? "warn"
: parsed === "ERROR" || parsed === "FATAL"
? "error"
: parsed === "DEBUG" || parsed === "TRACE"
? "debug"
: "info";
}
const src = sourceTagPlain(l.source);
const level = resolveDisplayLevel(l);
return visibleWidth(`${ts} ${src} ${levelTagPlain(level)} `);
}

Expand Down
Loading