td-web-cli 0.1.4 → 0.1.6

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.
@@ -95,6 +95,45 @@ async function batchCheckTexts(texts, language) {
95
95
  }
96
96
  return results;
97
97
  }
98
+ /**
99
+ * 将语言检测结果转换成每条词条对应的错误描述数组
100
+ * @param checkResult 语言检测结果
101
+ * @param texts 词条数组(对应检测文本)
102
+ * @returns 按词条分割的检测错误描述数组,顺序对应输入texts
103
+ */
104
+ function parseCheckResultPerEntry(checkResult, texts) {
105
+ // 初始化每条词条对应的错误信息数组
106
+ const entryErrors = new Array(texts.length).fill('').map(() => '');
107
+ // 语言检测返回的matches是针对整个拼接文本的,需要拆分到对应词条
108
+ // 计算每条词条在拼接文本中的起始位置和结束位置
109
+ const positions = [];
110
+ let pos = 0;
111
+ for (const text of texts) {
112
+ const len = text.length;
113
+ positions.push({ start: pos, end: pos + len });
114
+ pos += len + 1; // +1是换行符长度
115
+ }
116
+ // 遍历所有错误匹配项,将错误信息分配到对应词条
117
+ for (const match of checkResult.matches) {
118
+ const errorOffset = match.offset;
119
+ // 找出错误所在的词条索引
120
+ const idx = positions.findIndex((range) => errorOffset >= range.start && errorOffset < range.end);
121
+ if (idx === -1)
122
+ continue;
123
+ // 生成错误信息字符串
124
+ const errMsg = `错误: ${match.message}\n出错句子: ${match.sentence}\n建议替换: ${match.replacements
125
+ .map((r) => r.value)
126
+ .join(', ')}`;
127
+ // 多条错误用换行分隔
128
+ if (entryErrors[idx]) {
129
+ entryErrors[idx] += '\n' + errMsg;
130
+ }
131
+ else {
132
+ entryErrors[idx] = errMsg;
133
+ }
134
+ }
135
+ return entryErrors;
136
+ }
98
137
  /**
99
138
  * excel转json功能主函数
100
139
  * 读取用户输入的excel路径,解析内容,根据配置生成多语言json文件
@@ -104,13 +143,13 @@ async function batchCheckTexts(texts, language) {
104
143
  export async function excel2json(program) {
105
144
  var _a;
106
145
  // 配置文件默认路径
107
- const configPath = path.join(__dirname, '../setting.json');
146
+ const configPath = path.join(__dirname, '../../../../setting.json');
108
147
  let i18nConfig;
109
148
  // 加载配置文件
110
149
  try {
111
- logger.info(`开始加载配置文件:${configPath}`);
150
+ logger.info(`开始加载配置文件:${configPath}`, true);
112
151
  i18nConfig = loadConfig(configPath);
113
- logger.info('配置文件加载成功');
152
+ logger.info('配置文件加载成功', true);
114
153
  }
115
154
  catch (error) {
116
155
  const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
@@ -120,9 +159,9 @@ export async function excel2json(program) {
120
159
  }
121
160
  // 尝试调用接口获取支持的语言列表,更新 longCodes
122
161
  try {
123
- logger.info('尝试获取在线支持的语言列表...');
162
+ logger.info('尝试获取在线支持的语言列表...', true);
124
163
  const languageTools = await getLanguageTool();
125
- logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`);
164
+ logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`, true);
126
165
  // 构建新的 longCodes 映射
127
166
  const newLongCodes = {};
128
167
  // 语言标识对应语言名称列表,方便匹配
@@ -146,6 +185,7 @@ export async function excel2json(program) {
146
185
  }
147
186
  catch (error) {
148
187
  logger.warn(`获取在线语言列表失败,使用本地配置 longCodes,错误:${normalizeError(error).stack}`);
188
+ console.warn('获取在线语言列表失败,使用本地配置 longCodes');
149
189
  }
150
190
  // 交互式输入excel文件路径并校验
151
191
  const answer = await input({
@@ -164,7 +204,7 @@ export async function excel2json(program) {
164
204
  // 规范化路径,支持相对路径转绝对路径,去除首尾引号
165
205
  const excelPath = path.resolve(process.cwd(), answer.trim().replace(/^['"]|['"]$/g, ''));
166
206
  try {
167
- logger.info(`开始读取excel文件:${excelPath}`);
207
+ logger.info(`开始读取excel文件:${excelPath}`, true);
168
208
  // 读取excel文件
169
209
  const workbook = XLSX.readFile(excelPath);
170
210
  const firstSheetName = workbook.SheetNames[0];
@@ -181,7 +221,7 @@ export async function excel2json(program) {
181
221
  console.error('程序执行时发生异常,已记录日志,程序已退出');
182
222
  process.exit(1);
183
223
  }
184
- logger.info('开始解析表头');
224
+ logger.info('开始解析表头', true);
185
225
  // 处理表头行,去除空格,转成字符串
186
226
  const headerRow = rows[0].map((cell) => (cell ? String(cell).trim() : ''));
187
227
  // 根据表头匹配语言列,建立列索引到语言key的映射
@@ -206,7 +246,7 @@ export async function excel2json(program) {
206
246
  Object.values(colIndexToLangKey).forEach((langKey) => {
207
247
  langTranslations[langKey] = {};
208
248
  });
209
- logger.info('开始解析数据行');
249
+ logger.info('开始解析数据行', true);
210
250
  // 遍历数据行,提取所有语言词条
211
251
  // key统一用默认语言列的值,其他语言对应的列为翻译内容
212
252
  const langKeysMap = {}; // 语言key => 词条数组
@@ -237,31 +277,49 @@ export async function excel2json(program) {
237
277
  langTranslations[langKey][key] = valStr;
238
278
  langKeysMap[langKey].push(valStr);
239
279
  }
280
+ else {
281
+ // 如果单元格为空,也要保证检测结果数组长度一致,填空字符串
282
+ langKeysMap[langKey].push('');
283
+ }
240
284
  }
241
285
  }
286
+ // 语言检测结果映射,语言key => 每条词条的错误描述数组
287
+ const langCheckErrorsMap = {};
242
288
  // 对所有语言词条批量进行语言检测(包括默认语言)
243
- for (const [langKey, texts] of Object.entries(langKeysMap)) {
289
+ const langKeysEntries = Object.entries(langKeysMap);
290
+ for (let idx = 0; idx < langKeysEntries.length; idx++) {
291
+ const [langKey, texts] = langKeysEntries[idx];
244
292
  const longCode = i18nConfig.longCodes[langKey];
245
293
  if (!longCode) {
246
- logger.warn(`语言(${langKey})未配置 longCode,跳过检测`);
294
+ logger.warn(`语言(${langKey})未配置 longCode,跳过检测`, true);
295
+ langCheckErrorsMap[langKey] =
296
+ texts.length > 0
297
+ ? texts.map(() => '未配置 longCode,未进行检测')
298
+ : ['无词条'];
247
299
  continue;
248
300
  }
249
301
  if (texts.length === 0) {
250
- logger.info(`语言(${langKey})无词条,跳过检测`);
302
+ logger.info(`语言(${langKey})无词条,跳过检测`, true);
303
+ langCheckErrorsMap[langKey] = ['无词条'];
251
304
  continue;
252
305
  }
253
- logger.info(`开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length}`);
306
+ logger.info(`开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length} (${idx + 1}/${langKeysEntries.length})`, true);
254
307
  const checkResults = await batchCheckTexts(texts, longCode);
255
308
  if (!checkResults || checkResults.length === 0 || !checkResults[0]) {
256
- logger.error(`语言(${langKey})词条检测失败`);
309
+ logger.error(`语言(${langKey})词条检测失败`, true);
310
+ langCheckErrorsMap[langKey] = texts.map(() => '检测失败,未知错误');
257
311
  continue;
258
312
  }
259
313
  const result = checkResults[0];
260
314
  if (result.matches.length === 0) {
261
- logger.info(`语言(${langKey})词条检测无错误`);
315
+ logger.info(`语言(${langKey})词条检测无错误`, true);
316
+ langCheckErrorsMap[langKey] = texts.map(() => '无错误');
262
317
  }
263
318
  else {
264
- logger.info(`语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`);
319
+ logger.info(`语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`, true);
320
+ // 解析检测结果,拆分到每条词条
321
+ langCheckErrorsMap[langKey] = parseCheckResultPerEntry(result, texts);
322
+ // 详细日志输出
265
323
  for (const match of result.matches) {
266
324
  logger.info(`- 错误: ${match.message}\n 出错句子: ${match.sentence}\n 建议替换: ${match.replacements
267
325
  .map((r) => r.value)
@@ -276,13 +334,13 @@ export async function excel2json(program) {
276
334
  if (!fs.existsSync(outputRoot)) {
277
335
  fs.mkdirSync(outputRoot, { recursive: true });
278
336
  }
279
- logger.info(`开始生成语言文件,输出目录:${outputRoot}`);
337
+ logger.info(`开始生成语言文件,输出目录:${outputRoot}`, true);
280
338
  // 按语言生成对应的json文件,默认语言的key=value不生成文件
281
339
  for (const [langKey, translations] of Object.entries(langTranslations)) {
282
340
  if (Object.keys(translations).length === 0)
283
341
  continue;
284
342
  if (langKey === defaultLang) {
285
- logger.info(`跳过默认语言(${langKey})的json文件生成`);
343
+ logger.info(`跳过默认语言(${langKey})的json文件生成`, true);
286
344
  continue; // 跳过默认语言文件生成
287
345
  }
288
346
  const langDir = path.join(outputRoot, langKey);
@@ -293,9 +351,53 @@ export async function excel2json(program) {
293
351
  fs.writeFileSync(filePath, JSON.stringify(translations, null, 2), {
294
352
  encoding: 'utf-8',
295
353
  });
296
- logger.info(`已生成语言文件:${filePath}`);
354
+ logger.info(`已生成语言文件:${filePath}`, true);
355
+ }
356
+ // 生成语言检测结果excel文件
357
+ logger.info('开始生成语言检测结果excel文件', true);
358
+ // 构造检测结果excel的表头:默认语言列 + 其他语言列(对应原文列名)
359
+ // 这里表头用原excel的表头中对应语言列的值
360
+ const errorSheetHeader = [];
361
+ // 按列索引顺序遍历,匹配语言key,构造表头
362
+ Object.entries(colIndexToLangKey)
363
+ .sort((a, b) => Number(a[0]) - Number(b[0]))
364
+ .forEach(([colIdxStr, langKey]) => {
365
+ const colIdx = Number(colIdxStr);
366
+ // 表头为原excel表头中对应列的文字
367
+ errorSheetHeader.push(headerRow[colIdx]);
368
+ });
369
+ // 构造检测结果excel的内容,每一列对应语言检测错误描述
370
+ // 每行对应原excel中一条数据行
371
+ const errorSheetData = [errorSheetHeader];
372
+ // 数据行数(不包括表头)
373
+ const dataRowCount = rows.length - 1;
374
+ for (let i = 0; i < dataRowCount; i++) {
375
+ const rowErrors = [];
376
+ // 按列索引顺序遍历,填充对应语言的检测错误
377
+ Object.entries(colIndexToLangKey)
378
+ .sort((a, b) => Number(a[0]) - Number(b[0]))
379
+ .forEach(([colIdxStr, langKey]) => {
380
+ const errorsArr = langCheckErrorsMap[langKey];
381
+ if (errorsArr && errorsArr.length > i) {
382
+ rowErrors.push(errorsArr[i] || '');
383
+ }
384
+ else {
385
+ // 可能某些语言词条数量不足时,填空
386
+ rowErrors.push('');
387
+ }
388
+ });
389
+ errorSheetData.push(rowErrors);
297
390
  }
298
- logger.info('全部转换完成', true);
391
+ // 生成excel工作簿和工作表
392
+ const errorWorkbook = XLSX.utils.book_new();
393
+ const errorSheet = XLSX.utils.aoa_to_sheet(errorSheetData);
394
+ XLSX.utils.book_append_sheet(errorWorkbook, errorSheet, 'LanguageCheckResults');
395
+ // 写入检测结果excel文件,固定文件名 lang_check_results.xlsx
396
+ const errorExcelPath = path.join(outputRoot, `lang_check_results.xlsx`);
397
+ XLSX.writeFile(errorWorkbook, errorExcelPath);
398
+ logger.info(`语言检测结果excel文件已生成:${errorExcelPath}`, true);
399
+ // 最终完成提示,包含输出目录
400
+ logger.info(`全部转换完成,语言文件输出目录:${outputRoot}`, true);
299
401
  }
300
402
  catch (error) {
301
403
  // 记录错误日志,方便排查
@@ -27,32 +27,32 @@ export function getTimestamp() {
27
27
  return `${year}${month}${day}${hour}${minute}${second}`;
28
28
  }
29
29
  /**
30
- * 获取程序入口文件路径(兼容 ES Module)
31
- * @returns 程序入口文件的绝对路径
30
+ * 获取项目根目录路径,兼容 ESM
31
+ * @returns 根目录绝对路径
32
32
  */
33
- function getEntryFilePath() {
34
- try {
35
- // 当前模块文件路径
36
- return fileURLToPath(import.meta.url);
37
- }
38
- catch {
39
- // 兜底:使用 process.argv[1]
40
- if (process.argv.length > 1) {
41
- return path.resolve(process.cwd(), process.argv[1]);
42
- }
43
- // 最终兜底
44
- return '';
45
- }
33
+ function getRootDir() {
34
+ // 当前模块文件路径
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ // 当前模块目录
37
+ const __dirname = path.dirname(__filename);
38
+ // 根目录为当前模块目录的上两级,视项目结构调整
39
+ return path.resolve(__dirname, '../../');
46
40
  }
47
41
  /**
48
42
  * 默认日志配置
49
- * logsDir 默认设置为程序入口文件所在目录的上级目录的 logs 文件夹
43
+ * logsDir 默认设置为项目根目录的 logs 文件夹
50
44
  */
51
45
  const defaultOptions = {
52
- logsDir: '',
53
- filenameFormatter: (date) => date.toISOString().slice(0, 10).replace(/-/g, '') + '.txt',
46
+ logsDir: path.resolve(getRootDir(), 'logs'),
47
+ filenameFormatter: (date) => {
48
+ // 使用本地时间格式化,格式 YYYYMMDD.txt
49
+ const pad = (n) => n.toString().padStart(2, '0');
50
+ const year = date.getFullYear();
51
+ const month = pad(date.getMonth() + 1);
52
+ const day = pad(date.getDate());
53
+ return `${year}${month}${day}.txt`;
54
+ },
54
55
  env: process.env.NODE_ENV || 'production',
55
- entryFilePath: getEntryFilePath(),
56
56
  };
57
57
  /**
58
58
  * 格式化日志内容,支持字符串或对象
@@ -62,7 +62,24 @@ const defaultOptions = {
62
62
  * @returns 格式化后的日志字符串
63
63
  */
64
64
  function formatLogLine(level, message, date) {
65
- const timeStr = date.toISOString();
65
+ // 使用本地时间格式化为 ISO-like 字符串(带时区偏移)
66
+ // 格式示例:2026-01-23T13:55:00+08:00
67
+ function formatLocalISO(date) {
68
+ const pad = (n) => n.toString().padStart(2, '0');
69
+ const year = date.getFullYear();
70
+ const month = pad(date.getMonth() + 1);
71
+ const day = pad(date.getDate());
72
+ const hour = pad(date.getHours());
73
+ const minute = pad(date.getMinutes());
74
+ const second = pad(date.getSeconds());
75
+ // 计算时区偏移,单位分钟
76
+ const tzOffset = -date.getTimezoneOffset();
77
+ const sign = tzOffset >= 0 ? '+' : '-';
78
+ const tzHour = pad(Math.floor(Math.abs(tzOffset) / 60));
79
+ const tzMinute = pad(Math.abs(tzOffset) % 60);
80
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}${sign}${tzHour}:${tzMinute}`;
81
+ }
82
+ const timeStr = formatLocalISO(date);
66
83
  let msgStr;
67
84
  if (typeof message === 'string') {
68
85
  msgStr = message;
@@ -89,10 +106,8 @@ export class Logger {
89
106
  constructor(options) {
90
107
  var _a, _b;
91
108
  const opts = { ...defaultOptions, ...options };
92
- // 如果未传 logsDir,则默认设置为入口文件所在目录的上级目录的 logs 文件夹
93
- this.logsDir =
94
- opts.logsDir ||
95
- path.resolve(path.dirname(opts.entryFilePath), '..', 'logs');
109
+ // 如果未传 logsDir,则默认设置为根目录的 logs 文件夹
110
+ this.logsDir = opts.logsDir;
96
111
  this.filenameFormatter =
97
112
  (_a = opts.filenameFormatter) !== null && _a !== void 0 ? _a : defaultOptions.filenameFormatter;
98
113
  this.env = (_b = opts.env) !== null && _b !== void 0 ? _b : defaultOptions.env;
@@ -0,0 +1,150 @@
1
+ [2026-01-27T15:35:46+08:00] [INFO] td-web-cli程序启动
2
+ [2026-01-27T15:35:46+08:00] [INFO] 命令行参数解析完成:
3
+ [2026-01-27T15:35:59+08:00] [INFO] 用户选择模块:国际化
4
+ [2026-01-27T15:35:59+08:00] [INFO] 国际化模块开始执行
5
+ [2026-01-27T15:35:59+08:00] [INFO] 国际化模块启动,等待用户选择功能
6
+ [2026-01-27T15:36:01+08:00] [INFO] 用户选择功能:excel转json
7
+ [2026-01-27T15:36:01+08:00] [INFO] excel转json功能开始执行
8
+ [2026-01-27T15:36:01+08:00] [INFO] 开始加载配置文件:D:\person\code\td-web-cli\setting.json
9
+ [2026-01-27T15:36:01+08:00] [INFO] 配置文件加载成功
10
+ [2026-01-27T15:36:01+08:00] [INFO] 尝试获取在线支持的语言列表...
11
+ [2026-01-27T15:36:03+08:00] [INFO] 成功获取语言列表,覆盖配置文件中的 longCodes
12
+ [2026-01-27T15:36:11+08:00] [INFO] 开始读取excel文件:d:\test\词条\A9-V4-CTC01翻译词条-20251215 - 副本.xlsx
13
+ [2026-01-27T15:36:11+08:00] [INFO] 开始解析表头
14
+ [2026-01-27T15:36:11+08:00] [INFO] 开始解析数据行
15
+ [2026-01-27T15:36:11+08:00] [INFO] 开始对语言(cn)词条进行语言检测,词条数量:12
16
+ [2026-01-27T15:36:12+08:00] [INFO] 语言(cn)词条检测无错误
17
+ [2026-01-27T15:36:12+08:00] [INFO] 开始对语言(en)词条进行语言检测,词条数量:12
18
+ [2026-01-27T15:36:13+08:00] [INFO] 语言(en)词条检测发现问题,词条数量: 4
19
+ [2026-01-27T15:36:13+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
20
+ 出错句子: Enjoy your new WiFi network!
21
+ 建议替换: Wi-Fi
22
+ [2026-01-27T15:36:13+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
23
+ 出错句子: The old WiFi network has been disconnected.
24
+ 建议替换: Wi-Fi
25
+ [2026-01-27T15:36:13+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
26
+ 出错句子: To access the setup page again, please connect to your new WiFi network.
27
+ 建议替换: Wi-Fi
28
+ [2026-01-27T15:36:13+08:00] [INFO] - 错误: Possible spelling mistake found.
29
+ 出错句子: Return to Login
30
+ BrosTrend N300 Wi-Fi to Ethernet Adapter
31
+ Applying settings...
32
+ 建议替换: Bros Trend
33
+ [2026-01-27T15:36:13+08:00] [INFO] 开始对语言(zh)词条进行语言检测,词条数量:12
34
+ [2026-01-27T15:36:14+08:00] [ERROR] 程序执行时发生错误:Error: https://api.languagetool.org/v2/check接口报错:AxiosError: Request failed with status code 400
35
+ at settle (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/settle.js:19:12)
36
+ at IncomingMessage.handleStreamEnd (file:///D:/person/code/td-web-cli/node_modules/axios/lib/adapters/http.js:798:11)
37
+ at IncomingMessage.emit (node:events:530:35)
38
+ at endReadableNT (node:internal/streams/readable:1698:12)
39
+ at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
40
+ at Axios.request (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/Axios.js:45:41)
41
+ at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
42
+ at async postData (file:///D:/person/code/td-web-cli/dist/api/index.js:10:17)
43
+ at async languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:246:21)
44
+ at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
45
+ at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:304:34)
46
+ at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
47
+ at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
48
+ at languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:253:15)
49
+ at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
50
+ at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
51
+ at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:304:34)
52
+ at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
53
+ at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
54
+ [2026-01-27T15:36:14+08:00] [ERROR] 语言(zh)词条检测失败
55
+ [2026-01-27T15:36:14+08:00] [INFO] 开始对语言(uk)词条进行语言检测,词条数量:12
56
+ [2026-01-27T15:36:16+08:00] [INFO] 语言(uk)词条检测发现问题,词条数量: 3
57
+ [2026-01-27T15:36:16+08:00] [INFO] - 错误: «Підключіть» — нерекомендоване слово, кращий варіант: увімкнути, під'єднати, приєднати.
58
+ 出错句子: Підключіть точку доступу до вашого роутера або активного порту Ethernet за допомогою кабелю Ethernet.
59
+ 建议替换: Увімкнути, Під'єднати, Приєднати
60
+ [2026-01-27T15:36:16+08:00] [INFO] - 错误: «підключіться» — нерекомендоване слово, кращий варіант: увімкнутися, під'єднатися, приєднатися.
61
+ 出错句子: Щоб знову отримати доступ до сторінки налаштувань, підключіться до своєї нової мережі WiFi.
62
+ 建议替换: увімкнутися, під'єднатися, приєднатися
63
+ [2026-01-27T15:36:16+08:00] [INFO] - 错误: «веб-інтерфейс» — написання не відповідає чинній версії правопису, виправлення: вебінтерфейс.
64
+ 出错句子: Після скидання пристрою відвідайте http://brostrendwifi.com для входу в веб-інтерфейс.
65
+ 建议替换: вебінтерфейс
66
+ [2026-01-27T15:36:16+08:00] [INFO] 开始生成语言文件,输出目录:d:\test\词条\lang_20260127153616
67
+ [2026-01-27T15:36:16+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127153616\cn\translate.json
68
+ [2026-01-27T15:36:16+08:00] [INFO] 跳过默认语言(en)的json文件生成
69
+ [2026-01-27T15:36:16+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127153616\zh\translate.json
70
+ [2026-01-27T15:36:16+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127153616\uk\translate.json
71
+ [2026-01-27T15:36:16+08:00] [INFO] 开始生成语言检测结果excel文件
72
+ [2026-01-27T15:36:16+08:00] [INFO] 语言检测结果excel文件已生成:d:\test\词条\lang_20260127153616\lang_check_results.xlsx
73
+ [2026-01-27T15:36:16+08:00] [INFO] 全部转换完成,语言文件输出目录:d:\test\词条\lang_20260127153616
74
+ [2026-01-27T15:36:16+08:00] [INFO] excel转json功能执行完成
75
+ [2026-01-27T15:36:16+08:00] [INFO] 国际化模块执行完成
76
+ [2026-01-27T16:26:38+08:00] [INFO] td-web-cli程序启动
77
+ [2026-01-27T16:26:38+08:00] [INFO] 命令行参数解析完成:
78
+ [2026-01-27T16:26:41+08:00] [INFO] 用户选择模块:国际化
79
+ [2026-01-27T16:26:41+08:00] [INFO] 国际化模块开始执行
80
+ [2026-01-27T16:26:41+08:00] [INFO] 国际化模块启动,等待用户选择功能
81
+ [2026-01-27T16:26:42+08:00] [INFO] 用户选择功能:excel转json
82
+ [2026-01-27T16:26:42+08:00] [INFO] excel转json功能开始执行
83
+ [2026-01-27T16:26:42+08:00] [INFO] 开始加载配置文件:D:\person\code\td-web-cli\setting.json
84
+ [2026-01-27T16:26:42+08:00] [INFO] 配置文件加载成功
85
+ [2026-01-27T16:26:42+08:00] [INFO] 尝试获取在线支持的语言列表...
86
+ [2026-01-27T16:26:44+08:00] [INFO] 成功获取语言列表,覆盖配置文件中的 longCodes
87
+ [2026-01-27T16:26:49+08:00] [INFO] 开始读取excel文件:d:\test\词条\A9-V4-CTC01翻译词条-20251215 - 副本.xlsx
88
+ [2026-01-27T16:26:49+08:00] [INFO] 开始解析表头
89
+ [2026-01-27T16:26:49+08:00] [INFO] 开始解析数据行
90
+ [2026-01-27T16:26:49+08:00] [INFO] 开始对语言(cn)词条进行语言检测,词条数量:12 (1/4)
91
+ [2026-01-27T16:26:50+08:00] [INFO] 语言(cn)词条检测无错误
92
+ [2026-01-27T16:26:50+08:00] [INFO] 开始对语言(en)词条进行语言检测,词条数量:12 (2/4)
93
+ [2026-01-27T16:26:51+08:00] [INFO] 语言(en)词条检测发现问题,词条数量: 4
94
+ [2026-01-27T16:26:51+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
95
+ 出错句子: Enjoy your new WiFi network!
96
+ 建议替换: Wi-Fi
97
+ [2026-01-27T16:26:51+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
98
+ 出错句子: The old WiFi network has been disconnected.
99
+ 建议替换: Wi-Fi
100
+ [2026-01-27T16:26:51+08:00] [INFO] - 错误: Did you mean “Wi-Fi”? (This is the officially approved term by the Wi-Fi Alliance.)
101
+ 出错句子: To access the setup page again, please connect to your new WiFi network.
102
+ 建议替换: Wi-Fi
103
+ [2026-01-27T16:26:51+08:00] [INFO] - 错误: Possible spelling mistake found.
104
+ 出错句子: Return to Login
105
+ BrosTrend N300 Wi-Fi to Ethernet Adapter
106
+ Applying settings...
107
+ 建议替换: Bros Trend
108
+ [2026-01-27T16:26:51+08:00] [INFO] 开始对语言(zh)词条进行语言检测,词条数量:12 (3/4)
109
+ [2026-01-27T16:26:51+08:00] [ERROR] 程序执行时发生错误:Error: https://api.languagetool.org/v2/check接口报错:AxiosError: Request failed with status code 400
110
+ at settle (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/settle.js:19:12)
111
+ at IncomingMessage.handleStreamEnd (file:///D:/person/code/td-web-cli/node_modules/axios/lib/adapters/http.js:798:11)
112
+ at IncomingMessage.emit (node:events:530:35)
113
+ at endReadableNT (node:internal/streams/readable:1698:12)
114
+ at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
115
+ at Axios.request (file:///D:/person/code/td-web-cli/node_modules/axios/lib/core/Axios.js:45:41)
116
+ at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
117
+ at async postData (file:///D:/person/code/td-web-cli/dist/api/index.js:10:17)
118
+ at async languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:246:21)
119
+ at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
120
+ at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:307:34)
121
+ at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
122
+ at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
123
+ at languageToolCheck (file:///D:/person/code/td-web-cli/dist/utils/index.js:253:15)
124
+ at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
125
+ at async batchCheckTexts (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:89:21)
126
+ at async excel2json (file:///D:/person/code/td-web-cli/dist/modules/i18n/excel2json/index.js:307:34)
127
+ at async i18n (file:///D:/person/code/td-web-cli/dist/modules/i18n/index.js:57:17)
128
+ at async main (file:///D:/person/code/td-web-cli/dist/index.js:51:17)
129
+ [2026-01-27T16:26:51+08:00] [ERROR] 语言(zh)词条检测失败
130
+ [2026-01-27T16:26:51+08:00] [INFO] 开始对语言(uk)词条进行语言检测,词条数量:12 (4/4)
131
+ [2026-01-27T16:26:53+08:00] [INFO] 语言(uk)词条检测发现问题,词条数量: 3
132
+ [2026-01-27T16:26:53+08:00] [INFO] - 错误: «Підключіть» — нерекомендоване слово, кращий варіант: увімкнути, під'єднати, приєднати.
133
+ 出错句子: Підключіть точку доступу до вашого роутера або активного порту Ethernet за допомогою кабелю Ethernet.
134
+ 建议替换: Увімкнути, Під'єднати, Приєднати
135
+ [2026-01-27T16:26:53+08:00] [INFO] - 错误: «підключіться» — нерекомендоване слово, кращий варіант: увімкнутися, під'єднатися, приєднатися.
136
+ 出错句子: Щоб знову отримати доступ до сторінки налаштувань, підключіться до своєї нової мережі WiFi.
137
+ 建议替换: увімкнутися, під'єднатися, приєднатися
138
+ [2026-01-27T16:26:53+08:00] [INFO] - 错误: «веб-інтерфейс» — написання не відповідає чинній версії правопису, виправлення: вебінтерфейс.
139
+ 出错句子: Після скидання пристрою відвідайте http://brostrendwifi.com для входу в веб-інтерфейс.
140
+ 建议替换: вебінтерфейс
141
+ [2026-01-27T16:26:53+08:00] [INFO] 开始生成语言文件,输出目录:d:\test\词条\lang_20260127162653
142
+ [2026-01-27T16:26:53+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127162653\cn\translate.json
143
+ [2026-01-27T16:26:53+08:00] [INFO] 跳过默认语言(en)的json文件生成
144
+ [2026-01-27T16:26:53+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127162653\zh\translate.json
145
+ [2026-01-27T16:26:53+08:00] [INFO] 已生成语言文件:d:\test\词条\lang_20260127162653\uk\translate.json
146
+ [2026-01-27T16:26:53+08:00] [INFO] 开始生成语言检测结果excel文件
147
+ [2026-01-27T16:26:53+08:00] [INFO] 语言检测结果excel文件已生成:d:\test\词条\lang_20260127162653\lang_check_results.xlsx
148
+ [2026-01-27T16:26:53+08:00] [INFO] 全部转换完成,语言文件输出目录:d:\test\词条\lang_20260127162653
149
+ [2026-01-27T16:26:53+08:00] [INFO] excel转json功能执行完成
150
+ [2026-01-27T16:26:53+08:00] [INFO] 国际化模块执行完成
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-web-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A CLI tool for efficiency",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -135,6 +135,54 @@ async function batchCheckTexts(
135
135
  return results;
136
136
  }
137
137
 
138
+ /**
139
+ * 将语言检测结果转换成每条词条对应的错误描述数组
140
+ * @param checkResult 语言检测结果
141
+ * @param texts 词条数组(对应检测文本)
142
+ * @returns 按词条分割的检测错误描述数组,顺序对应输入texts
143
+ */
144
+ function parseCheckResultPerEntry(
145
+ checkResult: CheckResult,
146
+ texts: string[]
147
+ ): string[] {
148
+ // 初始化每条词条对应的错误信息数组
149
+ const entryErrors = new Array(texts.length).fill('').map(() => '');
150
+
151
+ // 语言检测返回的matches是针对整个拼接文本的,需要拆分到对应词条
152
+ // 计算每条词条在拼接文本中的起始位置和结束位置
153
+ const positions: { start: number; end: number }[] = [];
154
+ let pos = 0;
155
+ for (const text of texts) {
156
+ const len = text.length;
157
+ positions.push({ start: pos, end: pos + len });
158
+ pos += len + 1; // +1是换行符长度
159
+ }
160
+
161
+ // 遍历所有错误匹配项,将错误信息分配到对应词条
162
+ for (const match of checkResult.matches) {
163
+ const errorOffset = match.offset;
164
+ // 找出错误所在的词条索引
165
+ const idx = positions.findIndex(
166
+ (range) => errorOffset >= range.start && errorOffset < range.end
167
+ );
168
+ if (idx === -1) continue;
169
+
170
+ // 生成错误信息字符串
171
+ const errMsg = `错误: ${match.message}\n出错句子: ${match.sentence}\n建议替换: ${match.replacements
172
+ .map((r) => r.value)
173
+ .join(', ')}`;
174
+
175
+ // 多条错误用换行分隔
176
+ if (entryErrors[idx]) {
177
+ entryErrors[idx] += '\n' + errMsg;
178
+ } else {
179
+ entryErrors[idx] = errMsg;
180
+ }
181
+ }
182
+
183
+ return entryErrors;
184
+ }
185
+
138
186
  /**
139
187
  * excel转json功能主函数
140
188
  * 读取用户输入的excel路径,解析内容,根据配置生成多语言json文件
@@ -143,14 +191,14 @@ async function batchCheckTexts(
143
191
  */
144
192
  export async function excel2json(program: Command) {
145
193
  // 配置文件默认路径
146
- const configPath = path.join(__dirname, '../setting.json');
194
+ const configPath = path.join(__dirname, '../../../../setting.json');
147
195
  let i18nConfig: I18nConfig;
148
196
 
149
197
  // 加载配置文件
150
198
  try {
151
- logger.info(`开始加载配置文件:${configPath}`);
199
+ logger.info(`开始加载配置文件:${configPath}`, true);
152
200
  i18nConfig = loadConfig(configPath);
153
- logger.info('配置文件加载成功');
201
+ logger.info('配置文件加载成功', true);
154
202
  } catch (error: unknown) {
155
203
  const msg = `读取配置文件失败:${normalizeError(error).stack},程序已退出`;
156
204
  logger.error(msg);
@@ -160,9 +208,9 @@ export async function excel2json(program: Command) {
160
208
 
161
209
  // 尝试调用接口获取支持的语言列表,更新 longCodes
162
210
  try {
163
- logger.info('尝试获取在线支持的语言列表...');
211
+ logger.info('尝试获取在线支持的语言列表...', true);
164
212
  const languageTools = await getLanguageTool();
165
- logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`);
213
+ logger.info(`成功获取语言列表,覆盖配置文件中的 longCodes`, true);
166
214
 
167
215
  // 构建新的 longCodes 映射
168
216
  const newLongCodes: Record<string, string> = {};
@@ -193,6 +241,7 @@ export async function excel2json(program: Command) {
193
241
  logger.warn(
194
242
  `获取在线语言列表失败,使用本地配置 longCodes,错误:${normalizeError(error).stack}`
195
243
  );
244
+ console.warn('获取在线语言列表失败,使用本地配置 longCodes');
196
245
  }
197
246
 
198
247
  // 交互式输入excel文件路径并校验
@@ -215,7 +264,7 @@ export async function excel2json(program: Command) {
215
264
  );
216
265
 
217
266
  try {
218
- logger.info(`开始读取excel文件:${excelPath}`);
267
+ logger.info(`开始读取excel文件:${excelPath}`, true);
219
268
 
220
269
  // 读取excel文件
221
270
  const workbook = XLSX.readFile(excelPath);
@@ -236,7 +285,7 @@ export async function excel2json(program: Command) {
236
285
  process.exit(1);
237
286
  }
238
287
 
239
- logger.info('开始解析表头');
288
+ logger.info('开始解析表头', true);
240
289
  // 处理表头行,去除空格,转成字符串
241
290
  const headerRow = rows[0].map((cell) => (cell ? String(cell).trim() : ''));
242
291
 
@@ -268,7 +317,7 @@ export async function excel2json(program: Command) {
268
317
  langTranslations[langKey] = {};
269
318
  });
270
319
 
271
- logger.info('开始解析数据行');
320
+ logger.info('开始解析数据行', true);
272
321
  // 遍历数据行,提取所有语言词条
273
322
  // key统一用默认语言列的值,其他语言对应的列为翻译内容
274
323
  const langKeysMap: Record<string, string[]> = {}; // 语言key => 词条数组
@@ -300,40 +349,61 @@ export async function excel2json(program: Command) {
300
349
  const valStr = String(valCell);
301
350
  langTranslations[langKey][key] = valStr;
302
351
  langKeysMap[langKey].push(valStr);
352
+ } else {
353
+ // 如果单元格为空,也要保证检测结果数组长度一致,填空字符串
354
+ langKeysMap[langKey].push('');
303
355
  }
304
356
  }
305
357
  }
306
358
 
359
+ // 语言检测结果映射,语言key => 每条词条的错误描述数组
360
+ const langCheckErrorsMap: Record<string, string[]> = {};
361
+
307
362
  // 对所有语言词条批量进行语言检测(包括默认语言)
308
- for (const [langKey, texts] of Object.entries(langKeysMap)) {
363
+ const langKeysEntries = Object.entries(langKeysMap);
364
+ for (let idx = 0; idx < langKeysEntries.length; idx++) {
365
+ const [langKey, texts] = langKeysEntries[idx];
309
366
  const longCode = i18nConfig.longCodes[langKey];
310
367
  if (!longCode) {
311
- logger.warn(`语言(${langKey})未配置 longCode,跳过检测`);
368
+ logger.warn(`语言(${langKey})未配置 longCode,跳过检测`, true);
369
+ langCheckErrorsMap[langKey] =
370
+ texts.length > 0
371
+ ? texts.map(() => '未配置 longCode,未进行检测')
372
+ : ['无词条'];
312
373
  continue;
313
374
  }
314
375
  if (texts.length === 0) {
315
- logger.info(`语言(${langKey})无词条,跳过检测`);
376
+ logger.info(`语言(${langKey})无词条,跳过检测`, true);
377
+ langCheckErrorsMap[langKey] = ['无词条'];
316
378
  continue;
317
379
  }
318
380
 
319
381
  logger.info(
320
- `开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length}`
382
+ `开始对语言(${langKey})词条进行语言检测,词条数量:${texts.length} (${idx + 1}/${langKeysEntries.length})`,
383
+ true
321
384
  );
322
385
 
323
386
  const checkResults = await batchCheckTexts(texts, longCode);
324
387
 
325
388
  if (!checkResults || checkResults.length === 0 || !checkResults[0]) {
326
- logger.error(`语言(${langKey})词条检测失败`);
389
+ logger.error(`语言(${langKey})词条检测失败`, true);
390
+ langCheckErrorsMap[langKey] = texts.map(() => '检测失败,未知错误');
327
391
  continue;
328
392
  }
329
393
 
330
394
  const result = checkResults[0];
331
395
  if (result.matches.length === 0) {
332
- logger.info(`语言(${langKey})词条检测无错误`);
396
+ logger.info(`语言(${langKey})词条检测无错误`, true);
397
+ langCheckErrorsMap[langKey] = texts.map(() => '无错误');
333
398
  } else {
334
399
  logger.info(
335
- `语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`
400
+ `语言(${langKey})词条检测发现问题,词条数量: ${result.matches.length}`,
401
+ true
336
402
  );
403
+ // 解析检测结果,拆分到每条词条
404
+ langCheckErrorsMap[langKey] = parseCheckResultPerEntry(result, texts);
405
+
406
+ // 详细日志输出
337
407
  for (const match of result.matches) {
338
408
  logger.info(
339
409
  `- 错误: ${match.message}\n 出错句子: ${match.sentence}\n 建议替换: ${match.replacements
@@ -352,14 +422,14 @@ export async function excel2json(program: Command) {
352
422
  fs.mkdirSync(outputRoot, { recursive: true });
353
423
  }
354
424
 
355
- logger.info(`开始生成语言文件,输出目录:${outputRoot}`);
425
+ logger.info(`开始生成语言文件,输出目录:${outputRoot}`, true);
356
426
 
357
427
  // 按语言生成对应的json文件,默认语言的key=value不生成文件
358
428
  for (const [langKey, translations] of Object.entries(langTranslations)) {
359
429
  if (Object.keys(translations).length === 0) continue;
360
430
 
361
431
  if (langKey === defaultLang) {
362
- logger.info(`跳过默认语言(${langKey})的json文件生成`);
432
+ logger.info(`跳过默认语言(${langKey})的json文件生成`, true);
363
433
  continue; // 跳过默认语言文件生成
364
434
  }
365
435
 
@@ -372,10 +442,68 @@ export async function excel2json(program: Command) {
372
442
  fs.writeFileSync(filePath, JSON.stringify(translations, null, 2), {
373
443
  encoding: 'utf-8',
374
444
  });
375
- logger.info(`已生成语言文件:${filePath}`);
445
+ logger.info(`已生成语言文件:${filePath}`, true);
376
446
  }
377
447
 
378
- logger.info('全部转换完成', true);
448
+ // 生成语言检测结果excel文件
449
+ logger.info('开始生成语言检测结果excel文件', true);
450
+
451
+ // 构造检测结果excel的表头:默认语言列 + 其他语言列(对应原文列名)
452
+ // 这里表头用原excel的表头中对应语言列的值
453
+ const errorSheetHeader: string[] = [];
454
+
455
+ // 按列索引顺序遍历,匹配语言key,构造表头
456
+ Object.entries(colIndexToLangKey)
457
+ .sort((a, b) => Number(a[0]) - Number(b[0]))
458
+ .forEach(([colIdxStr, langKey]) => {
459
+ const colIdx = Number(colIdxStr);
460
+ // 表头为原excel表头中对应列的文字
461
+ errorSheetHeader.push(headerRow[colIdx]);
462
+ });
463
+
464
+ // 构造检测结果excel的内容,每一列对应语言检测错误描述
465
+ // 每行对应原excel中一条数据行
466
+ const errorSheetData: (string | null)[][] = [errorSheetHeader];
467
+
468
+ // 数据行数(不包括表头)
469
+ const dataRowCount = rows.length - 1;
470
+
471
+ for (let i = 0; i < dataRowCount; i++) {
472
+ const rowErrors: (string | null)[] = [];
473
+
474
+ // 按列索引顺序遍历,填充对应语言的检测错误
475
+ Object.entries(colIndexToLangKey)
476
+ .sort((a, b) => Number(a[0]) - Number(b[0]))
477
+ .forEach(([colIdxStr, langKey]) => {
478
+ const errorsArr = langCheckErrorsMap[langKey];
479
+ if (errorsArr && errorsArr.length > i) {
480
+ rowErrors.push(errorsArr[i] || '');
481
+ } else {
482
+ // 可能某些语言词条数量不足时,填空
483
+ rowErrors.push('');
484
+ }
485
+ });
486
+
487
+ errorSheetData.push(rowErrors);
488
+ }
489
+
490
+ // 生成excel工作簿和工作表
491
+ const errorWorkbook = XLSX.utils.book_new();
492
+ const errorSheet = XLSX.utils.aoa_to_sheet(errorSheetData);
493
+ XLSX.utils.book_append_sheet(
494
+ errorWorkbook,
495
+ errorSheet,
496
+ 'LanguageCheckResults'
497
+ );
498
+
499
+ // 写入检测结果excel文件,固定文件名 lang_check_results.xlsx
500
+ const errorExcelPath = path.join(outputRoot, `lang_check_results.xlsx`);
501
+ XLSX.writeFile(errorWorkbook, errorExcelPath);
502
+
503
+ logger.info(`语言检测结果excel文件已生成:${errorExcelPath}`, true);
504
+
505
+ // 最终完成提示,包含输出目录
506
+ logger.info(`全部转换完成,语言文件输出目录:${outputRoot}`, true);
379
507
  } catch (error: unknown) {
380
508
  // 记录错误日志,方便排查
381
509
  loggerError(error, logger);
@@ -41,7 +41,7 @@ type LogLevel = 'INFO' | 'WARN' | 'ERROR';
41
41
  */
42
42
  interface LoggerOptions {
43
43
  /**
44
- * 日志目录,默认程序入口文件所在目录的上级目录的 logs 文件夹
44
+ * 日志目录,默认根目录下的 logs 文件夹
45
45
  */
46
46
  logsDir?: string;
47
47
 
@@ -55,41 +55,36 @@ interface LoggerOptions {
55
55
  * 当前环境,默认 process.env.NODE_ENV
56
56
  */
57
57
  env?: string;
58
-
59
- /**
60
- * 程序入口文件绝对路径,用于确定日志目录位置
61
- */
62
- entryFilePath?: string;
63
58
  }
64
59
 
65
60
  /**
66
- * 获取程序入口文件路径(兼容 ES Module)
67
- * @returns 程序入口文件的绝对路径
61
+ * 获取项目根目录路径,兼容 ESM
62
+ * @returns 根目录绝对路径
68
63
  */
69
- function getEntryFilePath(): string {
70
- try {
71
- // 当前模块文件路径
72
- return fileURLToPath(import.meta.url);
73
- } catch {
74
- // 兜底:使用 process.argv[1]
75
- if (process.argv.length > 1) {
76
- return path.resolve(process.cwd(), process.argv[1]);
77
- }
78
- // 最终兜底
79
- return '';
80
- }
64
+ function getRootDir(): string {
65
+ // 当前模块文件路径
66
+ const __filename = fileURLToPath(import.meta.url);
67
+ // 当前模块目录
68
+ const __dirname = path.dirname(__filename);
69
+ // 根目录为当前模块目录的上两级,视项目结构调整
70
+ return path.resolve(__dirname, '../../');
81
71
  }
82
72
 
83
73
  /**
84
74
  * 默认日志配置
85
- * logsDir 默认设置为程序入口文件所在目录的上级目录的 logs 文件夹
75
+ * logsDir 默认设置为项目根目录的 logs 文件夹
86
76
  */
87
77
  const defaultOptions: LoggerOptions = {
88
- logsDir: '',
89
- filenameFormatter: (date: Date) =>
90
- date.toISOString().slice(0, 10).replace(/-/g, '') + '.txt',
78
+ logsDir: path.resolve(getRootDir(), 'logs'),
79
+ filenameFormatter: (date: Date) => {
80
+ // 使用本地时间格式化,格式 YYYYMMDD.txt
81
+ const pad = (n: number) => n.toString().padStart(2, '0');
82
+ const year = date.getFullYear();
83
+ const month = pad(date.getMonth() + 1);
84
+ const day = pad(date.getDate());
85
+ return `${year}${month}${day}.txt`;
86
+ },
91
87
  env: process.env.NODE_ENV || 'production',
92
- entryFilePath: getEntryFilePath(),
93
88
  };
94
89
 
95
90
  /**
@@ -100,7 +95,29 @@ const defaultOptions: LoggerOptions = {
100
95
  * @returns 格式化后的日志字符串
101
96
  */
102
97
  function formatLogLine(level: LogLevel, message: unknown, date: Date): string {
103
- const timeStr = date.toISOString();
98
+ // 使用本地时间格式化为 ISO-like 字符串(带时区偏移)
99
+ // 格式示例:2026-01-23T13:55:00+08:00
100
+ function formatLocalISO(date: Date): string {
101
+ const pad = (n: number) => n.toString().padStart(2, '0');
102
+
103
+ const year = date.getFullYear();
104
+ const month = pad(date.getMonth() + 1);
105
+ const day = pad(date.getDate());
106
+ const hour = pad(date.getHours());
107
+ const minute = pad(date.getMinutes());
108
+ const second = pad(date.getSeconds());
109
+
110
+ // 计算时区偏移,单位分钟
111
+ const tzOffset = -date.getTimezoneOffset();
112
+ const sign = tzOffset >= 0 ? '+' : '-';
113
+ const tzHour = pad(Math.floor(Math.abs(tzOffset) / 60));
114
+ const tzMinute = pad(Math.abs(tzOffset) % 60);
115
+
116
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}${sign}${tzHour}:${tzMinute}`;
117
+ }
118
+
119
+ const timeStr = formatLocalISO(date);
120
+
104
121
  let msgStr: string;
105
122
 
106
123
  if (typeof message === 'string') {
@@ -132,10 +149,8 @@ export class Logger {
132
149
  constructor(options?: LoggerOptions) {
133
150
  const opts = { ...defaultOptions, ...options };
134
151
 
135
- // 如果未传 logsDir,则默认设置为入口文件所在目录的上级目录的 logs 文件夹
136
- this.logsDir =
137
- opts.logsDir ||
138
- path.resolve(path.dirname(opts.entryFilePath!), '..', 'logs');
152
+ // 如果未传 logsDir,则默认设置为根目录的 logs 文件夹
153
+ this.logsDir = opts.logsDir!;
139
154
  this.filenameFormatter =
140
155
  opts.filenameFormatter ?? defaultOptions.filenameFormatter!;
141
156
  this.env = opts.env ?? defaultOptions.env!;