@wdyy/skills 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.
Files changed (22) hide show
  1. package/.well-known/skills/index.json +4 -4
  2. package/.well-known/skills/wdyy-api-standard/SKILL.md +3 -2
  3. package/.well-known/skills/wdyy-api-standard/reference/{api-/346/240/271/346/215/256/345/260/261/350/257/212/345/217/267/350/216/267/345/217/226/346/243/200/351/252/214/346/225/260/346/215/256.md → api-demo.md} +5 -3
  4. package/.well-known/skills/wdyy-api-standard/reference/api-sms.md +67 -0
  5. package/.well-known/skills/wdyy-deployment-standard/SKILL.md +34 -21
  6. package/.well-known/skills/wdyy-deployment-standard/agents/openai.yaml +2 -2
  7. package/.well-known/skills/wdyy-deployment-standard/reference/linux-deployment-rules.md +129 -7
  8. package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.mjs +159 -11
  9. package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs +576 -38
  10. package/.well-known/skills/wdyy-deployment-standard/templates/Dockerfile.template +6 -1
  11. package/.well-known/skills/wdyy-deployment-standard/templates/deploy.sh.template +588 -77
  12. package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.blue-green.yml +10 -8
  13. package/.well-known/skills/wdyy-deployment-standard/templates/dockerignore.template +15 -0
  14. package/.well-known/skills/wdyy-deployment-standard/templates/nginx-upstream.template.conf +5 -4
  15. package/.well-known/skills/wdyy-logging-standard/SKILL.md +18 -21
  16. package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +2 -2
  17. package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +8 -7
  18. package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.mjs +0 -19
  19. package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +71 -25
  20. package/.well-known/skills/wdyy-logging-standard/templates/frontend-error-report.template.ts +1 -6
  21. package/.well-known/skills/wdyy-logging-standard/templates/logger.template.ts +26 -25
  22. package/package.json +1 -1
@@ -4,14 +4,15 @@ services:
4
4
  env_file: ../../.env.production
5
5
  environment:
6
6
  INSTANCE_ID: blue
7
- LOG_DIR: /var/log/application
7
+ HOST: ${BACKEND_LISTEN_HOST}
8
+ PORT: ${BACKEND_CONTAINER_PORT}
8
9
  ports:
9
- - "127.0.0.1:${BLUE_PORT:-3001}:3000"
10
+ - "${BACKEND_BIND_IP}:${BLUE_PORT}:${BACKEND_CONTAINER_PORT}"
10
11
  volumes:
11
- - ${HOST_LOG_DIR:?set HOST_LOG_DIR}/backend/blue:/var/log/application
12
+ - ../../logs:/app/logs
12
13
  restart: unless-stopped
13
14
  healthcheck:
14
- test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
15
+ test: ["CMD", "wget", "-qO-", "${BACKEND_CONTAINER_HEALTH_URL}"]
15
16
  interval: 10s
16
17
  timeout: 3s
17
18
  retries: 6
@@ -21,14 +22,15 @@ services:
21
22
  env_file: ../../.env.production
22
23
  environment:
23
24
  INSTANCE_ID: green
24
- LOG_DIR: /var/log/application
25
+ HOST: ${BACKEND_LISTEN_HOST}
26
+ PORT: ${BACKEND_CONTAINER_PORT}
25
27
  ports:
26
- - "127.0.0.1:${GREEN_PORT:-3002}:3000"
28
+ - "${BACKEND_BIND_IP}:${GREEN_PORT}:${BACKEND_CONTAINER_PORT}"
27
29
  volumes:
28
- - ${HOST_LOG_DIR:?set HOST_LOG_DIR}/backend/green:/var/log/application
30
+ - ../../logs:/app/logs
29
31
  restart: unless-stopped
30
32
  healthcheck:
31
- test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
33
+ test: ["CMD", "wget", "-qO-", "${BACKEND_CONTAINER_HEALTH_URL}"]
32
34
  interval: 10s
33
35
  timeout: 3s
34
36
  retries: 6
@@ -0,0 +1,15 @@
1
+ .env
2
+ .env.*
3
+ !.env.example
4
+ *.pem
5
+ *.key
6
+ id_rsa
7
+ id_ed25519
8
+ credentials
9
+ credentials.*
10
+ deploy/
11
+ logs/
12
+ .git/
13
+ node_modules/
14
+ venv/
15
+ .pnpm-store/
@@ -1,22 +1,23 @@
1
- include /srv/<project>/deploy/nginx/backend-active.conf;
1
+ include "__DEPLOY_ROOT__/nginx/backend-active.conf";
2
2
 
3
3
  server {
4
4
  location /api/ {
5
- proxy_pass http://backend_active;
5
+ proxy_pass __BACKEND_PROXY_URL__;
6
6
  proxy_set_header X-Trace-Id $request_id;
7
7
  }
8
8
 
9
9
  location /releases/ {
10
- root /srv/<project>;
10
+ root "__DEPLOY_ROOT__";
11
11
  try_files $uri =404;
12
12
  }
13
13
 
14
14
  location / {
15
- root /srv/<project>/current;
15
+ root "__DEPLOY_ROOT__/current";
16
16
  try_files $uri $uri/ /index.html;
17
17
  }
18
18
 
19
19
  location = /index.html {
20
+ root "__DEPLOY_ROOT__/current";
20
21
  add_header Cache-Control "no-cache, no-store, must-revalidate" always;
21
22
  }
22
23
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: wdyy-logging-standard
3
- description: 为 NestJS 和 Vue 项目实现前端异常上报、后端统一结构化 JSON 日志、蓝绿实例隔离、traceId 传递、敏感信息脱敏与 2MB 文件轮转。Use when 编写日志、错误处理、前端异常上报或审查生产可观测性时。
3
+ description: 为 NestJS 和 Vue 项目实现仅写入项目根目录 ./logs 的结构化 JSON 日志、前端异常上报、traceId 传递、完整原始入参与 2MB 文件轮转。Use when 编写日志、错误处理、接口日志或审查生产可观测性时。
4
4
  ---
5
5
 
6
6
  # 企业日志规范
7
7
 
8
8
  ## Overview
9
9
 
10
- 建立可追踪、脱敏、可轮转的结构化日志,以支撑运行时排障而不泄露敏感信息。
10
+ 建立可追踪、可轮转的结构化日志,以支撑运行时排障并保留完整原始上下文。
11
11
 
12
12
  ## When to Use
13
13
 
@@ -15,51 +15,48 @@ description: 为 NestJS 和 Vue 项目实现前端异常上报、后端统一结
15
15
 
16
16
  ## 协作边界
17
17
 
18
- 日志实现必须纳入可观测性、实现计划和测试验证。本 skill 独立规定企业字段与隐私边界,不能降低 RED 指标、追踪或告警要求。
18
+ 日志实现必须纳入可观测性、实现计划和测试验证,不能降低 RED 指标、追踪或告警要求。
19
19
 
20
20
  ## 输入与输出
21
21
 
22
- - 输入:服务名、实例标识、环境、请求上下文、敏感字段清单、`LOG_DIR`。
23
- - 输出:统一 logger、前端异常上报客户端、HTTP 日志中间件、脱敏规则和日志验证结果。
22
+ - 输入:服务名、实例标识、环境和请求上下文;应用从项目根目录启动。
23
+ - 输出:统一 logger、前端异常上报客户端、HTTP 日志中间件、日志文件命名规则和日志验证结果。
24
24
  - 使用 [日志规则](reference/logging-rules.md) 和 [logger 模板](templates/logger.template.ts)。
25
25
 
26
26
  ## 执行步骤
27
27
 
28
- 1. 定义 `timestamp`、`level`、`service`、`instanceId`、`env`、`traceId`、`userId`、`method`、`path`、`query`、`body`、`statusCode`、`result`、`durationMs`、`errorCode`、`message`、`params` 的最小 JSON 契约。`timestamp` 必须为服务器本地时间 `YYYY-MM-DD HH:mm:ss`,精确到秒。
28
+ 1. 定义 `timestamp`、`level`、`service`、`instanceId`、`env`、`traceId`、`userId`、`method`、`path`、`query`、`body`、`response`、`statusCode`、`result`、`durationMs`、`errorCode`、`message`、`params` 的最小 JSON 契约。`timestamp` 必须为服务器本地时间 `YYYY-MM-DD HH:mm:ss`,精确到秒。
29
29
  2. 在入口生成或透传 traceId;在内部 REST 调用中继续传递。
30
30
  3. 统一封装 logger;业务代码不得直接使用 `console.log`。
31
- 4. HTTP 请求日志记录 `query`(查询参数)和 `body`(请求体);授权请求可记录实际用户、模式、表和权限等入参。`statusCode` 100–399 `result` `success`,为 400–599 时 `result` 为 `failure`。
32
- 5. password、token、secret、authorization、databaseUrl、idCard、bankCard 为敏感字段,必须连同键和值一起从日志中移除。数组仅保留前 500 项,超出部分追加 `[TRUNCATED N ITEMS]`;拒绝记录未裁剪的大对象。
33
- 6. Vue 捕获未处理异常和 Promise 拒绝,裁剪、脱敏后上报后端;浏览器不得尝试写服务器文件。
34
- 7. 后端按 `LOG_DIR/<service>/<instanceId>/yyyy-mm-dd_hh24-mm-ss.log` 写入,文件名使用服务器本地时间;历史日志不重命名。最新文件超过 2MB 时创建新文件,并测试轮转。
31
+ 4. HTTP 请求日志记录完整的原始 `query`(查询参数)和 `body`(请求体);授权请求可记录实际用户、模式、表和权限等入参。服务端返回成功时记录 `result: success`;返回失败时记录 `result: failure`,并在 `response` 中记录完整失败内容。HTTP `statusCode` 为 100–399 时为成功,400–599 时为失败。
32
+ 5. 日志不执行脱敏、数组截断或大对象裁剪;`body` 必须保留完整原始入参。
33
+ 6. Vue 捕获未处理异常和 Promise 拒绝并上报后端;浏览器不得尝试写服务器文件。
34
+ 7. 后端只在项目根目录创建 `./logs`,并直接按 `./logs/yyyy-mm-dd_hh24-mm-ss.log` 写入;不得读取 `LOG_DIR`、`HOST_LOG_DIR` 或其他目录配置,不得在 `logs` 中创建任何子目录。使用排他创建避免并发覆盖;同一秒发生冲突时依次使用 `_1`、`_2` 等递增后缀。最新文件超过 2MB 时仍在 `./logs` 创建新文件,既有文件不得移动、重命名或覆盖。
35
35
 
36
36
  ## 禁止事项
37
37
 
38
- - 不得记录秘密、完整敏感身份信息或未裁剪的大请求/响应对象。
39
38
  - 不得将 `message` 仅限制为 `success` 或 `error`。
40
39
  - 不得用 console 输出替代业务日志。
40
+ - 不得将日志写入项目根目录 `./logs` 之外,或按服务、实例、日期、blue/green 颜色创建日志子目录。
41
41
 
42
42
  ## Red Flags
43
43
 
44
- - 业务代码直接调用 console,或日志包含未脱敏的凭证、身份信息。
44
+ - 业务代码直接调用 console,或 `body` 未保留完整原始入参。
45
45
  - 请求日志缺失 traceId、状态码或耗时。
46
- - 蓝绿容器共享同一日志文件,或前端异常未进入统一后端日志。
46
+ - 日志路径可被环境变量改变、生成了日志子目录、同秒文件覆盖既有文件,或前端异常未进入统一后端日志。
47
47
 
48
48
  ## Verification
49
49
 
50
50
  - [ ] 正常、异常和 HTTP 请求日志均为可解析 JSON。
51
51
  - [ ] traceId 跨入口与内部调用可追踪。
52
- - [ ] 敏感字段移除、数组截断、HTTP 入参与状态结果映射均有测试覆盖,轮转阈值可验证。
53
- - [ ] 前端异常上报经过裁剪、脱敏和后端身份校验。
54
- - [ ] 蓝绿实例使用独立日志目录,容器替换后日志仍保留。
55
- - [ ] `LOG_DIR` 缺失或不可写时服务以明确错误停止。
56
-
57
- 无法确定某字段是否敏感时按敏感处理并要求数据负责人确认;不得先原样记录再补救。
52
+ - [ ] 完整 HTTP 入参、服务端成功/失败结果映射、失败响应内容、固定 `./logs` 路径、同秒递增命名与轮转阈值均有测试覆盖。
53
+ - [ ] 前端异常上报经过后端身份校验。
54
+ - [ ] 日志直接写入项目根目录 `./logs`,且容器替换后日志仍保留。
55
+ - [ ] 项目根目录或 `./logs` 不可写时服务以明确错误停止。
58
56
 
59
57
  ## Common Rationalizations
60
58
 
61
59
  | 合理化说法 | 事实 |
62
60
  |---|---|
63
- | “开发日志不用脱敏” | 开发日志同样会被共享和长期保留。 |
64
61
  | “console.log 足够定位问题” | 无统一字段、轮转和 traceId 的输出不能支撑运维。 |
65
- | “完整请求最方便排查” | 便利不构成暴露隐私和大对象的理由。 |
62
+ | “失败响应只记录状态码即可” | 必须记录完整失败内容,才能定位服务端返回的业务错误。 |
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Enterprise Logging Standard"
3
- short_description: "Implement instance-safe logs and frontend error reporting"
4
- default_prompt: "Use $wdyy-logging-standard to implement frontend error reporting and instance-isolated structured logs."
3
+ short_description: "Write full-context logs only to project-root ./logs"
4
+ default_prompt: "Use $wdyy-logging-standard to implement full-context structured logs that write only to project-root ./logs."
@@ -1,12 +1,13 @@
1
1
  # 日志规则
2
2
 
3
3
  - 日志为一行 JSON,最小字段见 `logger.template.ts`。
4
+ - `logger.template.ts` 必须可在未安装 `@types/node` 的项目中独立类型检查;读取环境变量时通过 `globalThis` 声明最小运行时形状,不直接依赖 Node.js 全局类型。
4
5
  - `timestamp` 使用服务器本地时间 `YYYY-MM-DD HH:mm:ss`,精确到秒;不得包含毫秒、`T` 或 `Z`。
5
- - 文件名使用服务器本地时间 `yyyy-mm-dd_hh24-mm-ss.log`;最新日志文件大于 2MB 时先创建新文件再写入。既有日志文件不得重命名。
6
- - 日志仅记录关键业务事件、请求摘要和异常;`params`、`query` `body` 必须裁剪。授权请求可记录实际用户、模式、表和权限等入参。
7
- - HTTP 日志必须保留 `statusCode`,且 `result` 在 100–399 时为 `success`、在 400–599 时为 `failure`。
8
- - password、token、secret、authorization、databaseUrl、idCard、bankCard 必须从输出中完全移除,不得以占位符输出。数组保留前 500 项;超出时追加 `[TRUNCATED N ITEMS]`。
6
+ - 应用必须从项目根目录启动,只允许创建项目根目录 `./logs`,并将文件直接写入该目录;不得读取 `LOG_DIR`、`HOST_LOG_DIR` 或其他目录配置,不得创建服务、实例、日期、blue/green 颜色或其他日志子目录。
7
+ - 文件名使用服务器本地时间 `yyyy-mm-dd_hh24-mm-ss.log`。使用排他创建;同一秒内文件名冲突时依次尝试 `yyyy-mm-dd_hh24-mm-ss_1.log`、`yyyy-mm-dd_hh24-mm-ss_2.log`,不得先检查后覆盖写入。最新日志文件大于 2MB 时在同一 `./logs` 中创建新文件,既有日志文件不得移动、重命名或覆盖。
8
+ - 日志仅记录关键业务事件、请求和异常;`params`、`query`、`body` `response` 均保留完整原始值,不执行脱敏、数组截断或大对象裁剪。授权请求可记录实际用户、模式、表和权限等入参。
9
+ - HTTP 日志必须保留 `statusCode`。服务端成功返回时 `result` `success`;失败返回时 `result` 为 `failure`,并在 `response` 中保留完整失败内容。100–399 为成功,400–599 为失败。
9
10
  - HTTP 入口和内部服务调用必须透传 traceId。
10
- - 每条日志必须包含 `instanceId`;生产蓝绿容器分别写入 `LOG_DIR/<service>/<instanceId>/`,不得并发写同一文件。
11
- - Vue 未处理异常和 Promise 拒绝必须裁剪、脱敏后上报后端;后端校验请求身份和大小后写入统一日志。
12
- - 日志目录必须挂载到容器外部持久路径,替换容器不得删除历史日志。
11
+ - 每条日志必须包含 `instanceId`;blue、green 与其他进程共享同一 `./logs`,通过排他创建和同秒递增后缀避免覆盖。
12
+ - Vue 未处理异常和 Promise 拒绝必须上报后端;后端校验请求身份后写入统一日志。
13
+ - 容器必须将宿主机项目根目录 `./logs` 挂载到应用根目录 `/app/logs`,替换容器不得删除历史日志。
@@ -9,25 +9,6 @@ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(entry.timestamp)) {
9
9
  throw new Error('timestamp must use local YYYY-MM-DD HH:mm:ss format');
10
10
  }
11
11
 
12
- const sensitiveKeys = new Set([
13
- 'password', 'token', 'secret', 'authorization', 'databaseurl', 'idcard', 'bankcard',
14
- ]);
15
- const validateValue = (value) => {
16
- if (Array.isArray(value)) {
17
- if (value.length > 500 && (value.length !== 501 || !/^\[TRUNCATED \d+ ITEMS\]$/.test(value.at(-1)))) {
18
- throw new Error('Arrays over 500 items must end with a truncation marker');
19
- }
20
- value.forEach(validateValue);
21
- return;
22
- }
23
- if (!value || typeof value !== 'object') return;
24
- for (const [key, item] of Object.entries(value)) {
25
- if (sensitiveKeys.has(key.toLowerCase())) throw new Error('Sensitive fields must be omitted');
26
- validateValue(item);
27
- }
28
- };
29
- validateValue(entry);
30
-
31
12
  if ('statusCode' in entry) {
32
13
  const expected = entry.statusCode >= 100 && entry.statusCode <= 399 ? 'success'
33
14
  : entry.statusCode >= 400 && entry.statusCode <= 599 ? 'failure' : undefined;
@@ -4,6 +4,12 @@ import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { afterEach, test } from 'node:test';
6
6
  import { spawnSync } from 'node:child_process';
7
+ import {
8
+ createLogFileName,
9
+ createLogFilePath,
10
+ LOG_DIRECTORY,
11
+ MAX_LOG_FILE_SIZE_BYTES,
12
+ } from '../templates/logger.template.ts';
7
13
 
8
14
  const validator = new URL('./validate-log-entry.mjs', import.meta.url);
9
15
  const temporaryDirectories = [];
@@ -24,6 +30,59 @@ async function validate(entry) {
24
30
  return spawnSync(process.execPath, [validator.pathname, input], { encoding: 'utf8' });
25
31
  }
26
32
 
33
+ test('同一秒日志文件使用递增后缀', () => {
34
+ const date = new Date(2026, 6, 28, 9, 5, 3);
35
+
36
+ assert.deepEqual(
37
+ [createLogFileName(date), createLogFileName(date, 1), createLogFileName(date, 2)],
38
+ ['2026-07-28_09-05-03.log', '2026-07-28_09-05-03_1.log', '2026-07-28_09-05-03_2.log'],
39
+ );
40
+ });
41
+
42
+ test('日志路径固定在项目根目录的扁平 logs 目录', () => {
43
+ const date = new Date(2026, 6, 28, 9, 5, 3);
44
+
45
+ assert.equal(LOG_DIRECTORY, './logs');
46
+ assert.deepEqual(
47
+ [createLogFilePath(date), createLogFilePath(date, 1), createLogFilePath(date, 2)],
48
+ [
49
+ './logs/2026-07-28_09-05-03.log',
50
+ './logs/2026-07-28_09-05-03_1.log',
51
+ './logs/2026-07-28_09-05-03_2.log',
52
+ ],
53
+ );
54
+ assert.ok(
55
+ [createLogFilePath(date), createLogFilePath(date, 1), createLogFilePath(date, 2)]
56
+ .every((path) => path.split('/').length === 3),
57
+ );
58
+ });
59
+
60
+ test('旧日志目录环境变量不能改变固定路径', () => {
61
+ const previousLogDir = process.env.LOG_DIR;
62
+ const previousHostLogDir = process.env.HOST_LOG_DIR;
63
+ process.env.LOG_DIR = '/tmp/legacy-logs';
64
+ process.env.HOST_LOG_DIR = '/srv/legacy-logs';
65
+
66
+ try {
67
+ assert.equal(
68
+ createLogFilePath(new Date(2026, 6, 28, 9, 5, 3)),
69
+ './logs/2026-07-28_09-05-03.log',
70
+ );
71
+ } finally {
72
+ if (previousLogDir === undefined) delete process.env.LOG_DIR;
73
+ else process.env.LOG_DIR = previousLogDir;
74
+ if (previousHostLogDir === undefined) delete process.env.HOST_LOG_DIR;
75
+ else process.env.HOST_LOG_DIR = previousHostLogDir;
76
+ }
77
+ });
78
+
79
+ test('2MB 轮转阈值使用同一 logs 目录的下一个候选路径', () => {
80
+ const date = new Date(2026, 6, 28, 9, 5, 3);
81
+
82
+ assert.equal(MAX_LOG_FILE_SIZE_BYTES, 2 * 1024 * 1024);
83
+ assert.equal(createLogFilePath(date, 3), './logs/2026-07-28_09-05-03_3.log');
84
+ });
85
+
27
86
  test('蓝绿实例日志必须包含 instanceId', async () => {
28
87
  const result = await validate({
29
88
  timestamp: '2026-07-09 08:00:00',
@@ -66,24 +125,7 @@ test('HTTP 状态码与 result 不一致时明确失败', async () => {
66
125
  assert.match(result.stderr, /result/);
67
126
  });
68
127
 
69
- test('每个敏感字段出现在日志中时都明确失败', async () => {
70
- for (const key of ['password', 'token', 'secret', 'authorization', 'databaseUrl', 'idCard', 'bankCard']) {
71
- const result = await validate({
72
- timestamp: '2026-07-09 08:00:00',
73
- level: 'info',
74
- service: 'backend',
75
- instanceId: 'blue',
76
- env: 'production',
77
- message: 'request completed',
78
- body: { [key]: 'not-allowed' },
79
- });
80
-
81
- assert.notEqual(result.status, 0, key);
82
- assert.match(result.stderr, /Sensitive/, key);
83
- }
84
- });
85
-
86
- test('HTTP 成功结果与截断数组可通过验证', async () => {
128
+ test('完整原始入参和成功响应可通过验证', async () => {
87
129
  const result = await validate({
88
130
  timestamp: '2026-07-09 08:00:00',
89
131
  level: 'info',
@@ -93,9 +135,11 @@ test('HTTP 成功结果与截断数组可通过验证', async () => {
93
135
  message: 'request completed',
94
136
  query: { page: '1' },
95
137
  body: {
96
- user: 'operator',
97
- permissions: [...Array.from({ length: 500 }, (_, index) => index), '[TRUNCATED 1 ITEMS]'],
138
+ password: 'unredacted',
139
+ idCard: '110101199001011234',
140
+ permissions: Array.from({ length: 501 }, (_, index) => index),
98
141
  },
142
+ response: { code: 'OK', message: '处理成功' },
99
143
  statusCode: 201,
100
144
  result: 'success',
101
145
  });
@@ -103,19 +147,21 @@ test('HTTP 成功结果与截断数组可通过验证', async () => {
103
147
  assert.equal(result.status, 0, result.stderr);
104
148
  });
105
149
 
106
- test('超过 500 项且没有截断标记时明确失败', async () => {
150
+ test('失败响应内容和 failure 结果可通过验证', async () => {
107
151
  const result = await validate({
108
152
  timestamp: '2026-07-09 08:00:00',
109
153
  level: 'info',
110
154
  service: 'backend',
111
155
  instanceId: 'blue',
112
156
  env: 'production',
113
- message: 'request completed',
114
- body: { items: Array.from({ length: 501 }, (_, index) => index) },
157
+ message: 'request failed',
158
+ body: { data: 'original-request' },
159
+ response: { code: 'INVALID_DATA', message: '参数不合法' },
160
+ statusCode: 422,
161
+ result: 'failure',
115
162
  });
116
163
 
117
- assert.notEqual(result.status, 0);
118
- assert.match(result.stderr, /truncation/);
164
+ assert.equal(result.status, 0, result.stderr);
119
165
  });
120
166
 
121
167
  test('UTC ISO timestamp 时明确失败', async () => {
@@ -7,16 +7,11 @@ type FrontendErrorReport = {
7
7
  };
8
8
 
9
9
  export async function reportFrontendError(report: FrontendErrorReport): Promise<void> {
10
- const payload = {
11
- ...report,
12
- message: report.message.slice(0, 500),
13
- stack: report.stack?.slice(0, 2000),
14
- };
15
10
  const response = await fetch('/api/client-errors', {
16
11
  method: 'POST',
17
12
  headers: { 'Content-Type': 'application/json' },
18
13
  credentials: 'same-origin',
19
- body: JSON.stringify(payload),
14
+ body: JSON.stringify(report),
20
15
  });
21
16
  if (!response.ok) throw new Error(`Frontend error report failed: ${response.status}`);
22
17
  }
@@ -6,6 +6,7 @@ export type LogContext = {
6
6
  statusCode?: number;
7
7
  query?: Record<string, unknown>;
8
8
  body?: unknown;
9
+ response?: unknown;
9
10
  durationMs?: number;
10
11
  errorCode?: string;
11
12
  params?: Record<string, unknown>;
@@ -21,24 +22,19 @@ export type LogEntry = LogContext & {
21
22
  result?: 'success' | 'failure';
22
23
  };
23
24
 
24
- const sensitiveKeys = new Set([
25
- 'password', 'token', 'secret', 'authorization', 'databaseurl', 'idcard', 'bankcard',
26
- ]);
25
+ const pad = (value: number) => String(value).padStart(2, '0');
27
26
 
28
- export const redact = (value: unknown): unknown => {
29
- if (Array.isArray(value)) {
30
- const items = value.slice(0, 500).map(redact);
31
- return value.length > 500
32
- ? [...items, '[TRUNCATED ' + (value.length - 500) + ' ITEMS]']
33
- : items;
34
- }
35
- if (!value || typeof value !== 'object') return value;
36
- return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => (
37
- sensitiveKeys.has(key.toLowerCase()) ? [] : [[key, redact(item)]]
38
- )));
39
- };
27
+ export const LOG_DIRECTORY = './logs' as const;
28
+ export const MAX_LOG_FILE_SIZE_BYTES = 2 * 1024 * 1024;
40
29
 
41
- const pad = (value: number) => String(value).padStart(2, '0');
30
+ type RuntimeEnvironment = Record<string, string | undefined>;
31
+
32
+ const getRuntimeEnvironment = (): RuntimeEnvironment => {
33
+ const runtime = globalThis as typeof globalThis & {
34
+ process?: { env?: RuntimeEnvironment };
35
+ };
36
+ return runtime.process?.env ?? {};
37
+ };
42
38
 
43
39
  export const formatLocalTimestamp = (date = new Date()): string => (
44
40
  [date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join('-')
@@ -46,8 +42,13 @@ export const formatLocalTimestamp = (date = new Date()): string => (
46
42
  + [pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds())].join(':')
47
43
  );
48
44
 
49
- export const createLogFileName = (date = new Date()): string => (
50
- formatLocalTimestamp(date).replace(' ', '_').replaceAll(':', '-') + '.log'
45
+ export const createLogFileName = (date = new Date(), collisionIndex = 0): string => {
46
+ const baseName = formatLocalTimestamp(date).replace(' ', '_').replaceAll(':', '-');
47
+ return `${baseName}${collisionIndex > 0 ? `_${collisionIndex}` : ''}.log`;
48
+ };
49
+
50
+ export const createLogFilePath = (date = new Date(), collisionIndex = 0): string => (
51
+ `${LOG_DIRECTORY}/${createLogFileName(date, collisionIndex)}`
51
52
  );
52
53
 
53
54
  const resultForStatusCode = (statusCode: number): LogEntry['result'] => {
@@ -57,18 +58,18 @@ const resultForStatusCode = (statusCode: number): LogEntry['result'] => {
57
58
  };
58
59
 
59
60
  export const toLogEntry = (level: LogEntry['level'], message: string, context: LogContext): LogEntry => {
60
- const sanitizedContext = redact(context) as LogContext;
61
- const result = sanitizedContext.statusCode === undefined
61
+ const environment = getRuntimeEnvironment();
62
+ const result = context.statusCode === undefined
62
63
  ? undefined
63
- : resultForStatusCode(sanitizedContext.statusCode);
64
+ : resultForStatusCode(context.statusCode);
64
65
  return {
65
66
  timestamp: formatLocalTimestamp(),
66
67
  level,
67
- service: process.env.SERVICE_NAME ?? 'backend',
68
- instanceId: process.env.INSTANCE_ID ?? 'local',
69
- env: process.env.NODE_ENV ?? 'development',
68
+ service: environment.SERVICE_NAME ?? 'backend',
69
+ instanceId: environment.INSTANCE_ID ?? 'local',
70
+ env: environment.NODE_ENV ?? 'development',
70
71
  message,
71
- ...sanitizedContext,
72
+ ...context,
72
73
  ...(result ? { result } : {}),
73
74
  };
74
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdyy/skills",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "企业内部开发 Skill 与项目初始化命令",
5
5
  "type": "module",
6
6
  "bin": {