@wdyy/skills 0.1.0

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 (37) hide show
  1. package/.well-known/skills/index.json +24 -0
  2. package/.well-known/skills/wdyy-database-standard/SKILL.md +72 -0
  3. package/.well-known/skills/wdyy-database-standard/agents/openai.yaml +4 -0
  4. package/.well-known/skills/wdyy-database-standard/reference/database-rules.md +65 -0
  5. package/.well-known/skills/wdyy-database-standard/scripts/apply-migrations.sh +68 -0
  6. package/.well-known/skills/wdyy-database-standard/scripts/apply-migrations.test.mjs +47 -0
  7. package/.well-known/skills/wdyy-database-standard/scripts/validate-migration-layout.mjs +35 -0
  8. package/.well-known/skills/wdyy-database-standard/scripts/validate-migration-layout.test.mjs +73 -0
  9. package/.well-known/skills/wdyy-database-standard/scripts/validate-table-design.mjs +49 -0
  10. package/.well-known/skills/wdyy-database-standard/templates/table-design.template.md +44 -0
  11. package/.well-known/skills/wdyy-deployment-standard/SKILL.md +68 -0
  12. package/.well-known/skills/wdyy-deployment-standard/agents/openai.yaml +4 -0
  13. package/.well-known/skills/wdyy-deployment-standard/reference/linux-deployment-rules.md +9 -0
  14. package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.mjs +48 -0
  15. package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs +72 -0
  16. package/.well-known/skills/wdyy-deployment-standard/templates/Dockerfile.template +15 -0
  17. package/.well-known/skills/wdyy-deployment-standard/templates/deploy.sh.template +171 -0
  18. package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.blue-green.yml +34 -0
  19. package/.well-known/skills/wdyy-deployment-standard/templates/nginx-upstream.template.conf +26 -0
  20. package/.well-known/skills/wdyy-internal-api-standard/SKILL.md +60 -0
  21. package/.well-known/skills/wdyy-internal-api-standard/agents/openai.yaml +4 -0
  22. package/.well-known/skills/wdyy-internal-api-standard/reference/internal-api-rules.md +5 -0
  23. package/.well-known/skills/wdyy-internal-api-standard/scripts/check-raw-http-calls.mjs +10 -0
  24. package/.well-known/skills/wdyy-internal-api-standard/templates/api-client.template.ts +20 -0
  25. package/.well-known/skills/wdyy-internal-api-standard/templates/api-error.template.ts +7 -0
  26. package/.well-known/skills/wdyy-internal-api-standard/templates/api-mock.template.ts +8 -0
  27. package/.well-known/skills/wdyy-logging-standard/SKILL.md +65 -0
  28. package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +4 -0
  29. package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +12 -0
  30. package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.mjs +36 -0
  31. package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +133 -0
  32. package/.well-known/skills/wdyy-logging-standard/templates/frontend-error-report.template.ts +22 -0
  33. package/.well-known/skills/wdyy-logging-standard/templates/logger.template.ts +74 -0
  34. package/README.md +65 -0
  35. package/bin/wdyy.js +6 -0
  36. package/lib/wdyy-cli.js +124 -0
  37. package/package.json +24 -0
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { afterEach, test } from 'node:test';
6
+ import { spawnSync } from 'node:child_process';
7
+
8
+ const validator = new URL('./validate-deployment-package.mjs', import.meta.url);
9
+ const temporaryDirectories = [];
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(
13
+ temporaryDirectories.splice(0).map((directory) =>
14
+ rm(directory, { force: true, recursive: true }),
15
+ ),
16
+ );
17
+ });
18
+
19
+ async function createFiles({ includeRollback = true } = {}) {
20
+ const directory = await mkdtemp(join(tmpdir(), 'deployment-package-'));
21
+ temporaryDirectories.push(directory);
22
+ const deploy = join(directory, 'deploy.sh');
23
+ const nginx = join(directory, 'nginx.conf');
24
+ const compose = join(directory, 'compose.yml');
25
+ await writeFile(
26
+ deploy,
27
+ `case "$1" in
28
+ start) version="$2";;
29
+ stop) ;;
30
+ restart) ;;
31
+ status) ;;
32
+ ${includeRollback ? 'rollback) version="$2";;' : ''}
33
+ esac
34
+ HEALTH_URL=/health
35
+ VERSION_URL=/version
36
+ database/migrations
37
+ `,
38
+ );
39
+ await writeFile(
40
+ nginx,
41
+ 'location /releases/ { root /srv/app; }\nroot /srv/app/current;\n',
42
+ );
43
+ await writeFile(
44
+ compose,
45
+ 'backend-blue:\nbackend-green:\nINSTANCE_ID: blue\nINSTANCE_ID: green\nLOG_DIR: /logs\n',
46
+ );
47
+ return { deploy, nginx, compose };
48
+ }
49
+
50
+ test('接受版本化前端、蓝绿后端和完整命令', async () => {
51
+ const files = await createFiles();
52
+ const result = spawnSync(
53
+ process.execPath,
54
+ [validator.pathname, files.deploy, files.nginx, files.compose],
55
+ { encoding: 'utf8' },
56
+ );
57
+
58
+ assert.equal(result.status, 0, result.stderr);
59
+ assert.match(result.stdout, /valid deployment package/);
60
+ });
61
+
62
+ test('缺少 rollback 命令时明确失败', async () => {
63
+ const files = await createFiles({ includeRollback: false });
64
+ const result = spawnSync(
65
+ process.execPath,
66
+ [validator.pathname, files.deploy, files.nginx, files.compose],
67
+ { encoding: 'utf8' },
68
+ );
69
+
70
+ assert.notEqual(result.status, 0);
71
+ assert.match(result.stderr, /rollback/);
72
+ });
@@ -0,0 +1,15 @@
1
+ FROM node:24-bookworm-slim AS build
2
+ WORKDIR /app
3
+ COPY package.json pnpm-lock.yaml ./
4
+ RUN corepack enable && pnpm install --frozen-lockfile
5
+ COPY . .
6
+ RUN pnpm build
7
+
8
+ FROM node:24-bookworm-slim AS runtime
9
+ WORKDIR /app
10
+ ENV NODE_ENV=production
11
+ COPY --from=build /app/package.json ./
12
+ COPY --from=build /app/node_modules ./node_modules
13
+ COPY --from=build /app/dist ./dist
14
+ EXPOSE 3000
15
+ CMD ["node", "dist/main.js"]
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ : "${PROJECT_NAME:?PROJECT_NAME is required}"
5
+ : "${DATABASE_URL:?DATABASE_URL is required}"
6
+
7
+ DEPLOY_ROOT="${DEPLOY_ROOT:-/srv/$PROJECT_NAME}"
8
+ COMPOSE_FILE="${COMPOSE_FILE:-$DEPLOY_ROOT/deploy/docker/docker-compose.blue-green.yml}"
9
+ UPSTREAM_FILE="${UPSTREAM_FILE:-$DEPLOY_ROOT/deploy/nginx/backend-active.conf}"
10
+ MIGRATION_RUNNER="${MIGRATION_RUNNER:-$DEPLOY_ROOT/scripts/apply-migrations.sh}"
11
+ BACKEND_IMAGE_PREFIX="${BACKEND_IMAGE_PREFIX:-$PROJECT_NAME-backend}"
12
+ BLUE_PORT="${BLUE_PORT:-3001}"
13
+ GREEN_PORT="${GREEN_PORT:-3002}"
14
+ STATE_DIR="$DEPLOY_ROOT/state"
15
+ ACTIVE_STATE="$STATE_DIR/active.env"
16
+ PREVIOUS_STATE="$STATE_DIR/previous.env"
17
+ INCOMING_DIR="$DEPLOY_ROOT/incoming"
18
+ RELEASES_DIR="$DEPLOY_ROOT/releases"
19
+
20
+ mkdir -p "$STATE_DIR" "$RELEASES_DIR"
21
+
22
+ assert_version() {
23
+ [[ "${1:-}" =~ ^[0-9]{8}-[0-9]{3}$ ]] || {
24
+ echo "version must match YYYYMMDD-NNN" >&2
25
+ exit 2
26
+ }
27
+ }
28
+
29
+ read_active() {
30
+ ACTIVE_COLOR=green
31
+ ACTIVE_VERSION=
32
+ if [[ -f "$ACTIVE_STATE" ]]; then
33
+ # shellcheck disable=SC1090
34
+ source "$ACTIVE_STATE"
35
+ fi
36
+ }
37
+
38
+ port_for() {
39
+ [[ "$1" == blue ]] && printf '%s' "$BLUE_PORT" || printf '%s' "$GREEN_PORT"
40
+ }
41
+
42
+ switch_traffic() {
43
+ local color="$1" version="$2" port backup
44
+ port="$(port_for "$color")"
45
+ backup="$UPSTREAM_FILE.previous"
46
+ printf 'upstream backend_active {\n server 127.0.0.1:%s;\n keepalive 32;\n}\n' "$port" > "$UPSTREAM_FILE.new"
47
+ [[ -f "$UPSTREAM_FILE" ]] && cp "$UPSTREAM_FILE" "$backup"
48
+ mv -f "$UPSTREAM_FILE.new" "$UPSTREAM_FILE"
49
+ if ! nginx -t || ! nginx -s reload; then
50
+ if [[ -f "$backup" ]]; then
51
+ mv -f "$backup" "$UPSTREAM_FILE"
52
+ nginx -t
53
+ nginx -s reload
54
+ fi
55
+ echo "failed to switch Nginx upstream" >&2
56
+ return 1
57
+ fi
58
+ rm -f "$backup"
59
+ ln -sfn "releases/$version" "$DEPLOY_ROOT/current.new"
60
+ mv -Tf "$DEPLOY_ROOT/current.new" "$DEPLOY_ROOT/current"
61
+ }
62
+
63
+ verify_backend() {
64
+ local color="$1" version="$2" port health_url version_url actual
65
+ port="$(port_for "$color")"
66
+ health_url="http://127.0.0.1:$port/health"
67
+ version_url="http://127.0.0.1:$port/version"
68
+ HEALTH_URL="$health_url"
69
+ VERSION_URL="$version_url"
70
+ curl --fail --silent --show-error --retry 12 --retry-delay 2 "$HEALTH_URL"
71
+ actual="$(curl --fail --silent --show-error "$VERSION_URL")"
72
+ [[ "$actual" == "$version" ]] || {
73
+ echo "backend version mismatch: expected $version, got $actual" >&2
74
+ exit 1
75
+ }
76
+ }
77
+
78
+ start_version() {
79
+ local version="$1" source_dir release_dir target_color image
80
+ assert_version "$version"
81
+ source_dir="$INCOMING_DIR/$version"
82
+ release_dir="$RELEASES_DIR/$version"
83
+ [[ -f "$source_dir/frontend.tar.gz" ]] || { echo "missing frontend.tar.gz" >&2; exit 1; }
84
+ [[ -f "$source_dir/backend-image.tar" ]] || { echo "missing backend-image.tar" >&2; exit 1; }
85
+ [[ -d "$source_dir/database/migrations" ]] || { echo "missing database/migrations" >&2; exit 1; }
86
+
87
+ rm -rf "$release_dir"
88
+ mkdir -p "$release_dir"
89
+ if tar -tzf "$source_dir/frontend.tar.gz" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
90
+ echo "frontend archive contains an unsafe path" >&2
91
+ exit 1
92
+ fi
93
+ tar -xzf "$source_dir/frontend.tar.gz" -C "$release_dir"
94
+ [[ -f "$release_dir/index.html" ]] || { echo "frontend release missing index.html" >&2; exit 1; }
95
+ grep -q "/releases/$version/" "$release_dir/index.html" || {
96
+ echo "frontend assets are not built with the versioned release URL" >&2
97
+ exit 1
98
+ }
99
+ docker load -i "$source_dir/backend-image.tar"
100
+ image="$BACKEND_IMAGE_PREFIX:$version"
101
+ docker image inspect "$image" >/dev/null
102
+ MIGRATIONS_DIR="$source_dir/database/migrations" "$MIGRATION_RUNNER"
103
+
104
+ read_active
105
+ target_color=$([[ "$ACTIVE_COLOR" == blue ]] && echo green || echo blue)
106
+ if [[ "$target_color" == blue ]]; then
107
+ BACKEND_BLUE_IMAGE="$image" docker compose -f "$COMPOSE_FILE" up -d --force-recreate backend-blue
108
+ else
109
+ BACKEND_GREEN_IMAGE="$image" docker compose -f "$COMPOSE_FILE" up -d --force-recreate backend-green
110
+ fi
111
+ verify_backend "$target_color" "$version"
112
+ [[ -f "$ACTIVE_STATE" ]] && cp "$ACTIVE_STATE" "$PREVIOUS_STATE"
113
+ switch_traffic "$target_color" "$version"
114
+ printf 'ACTIVE_COLOR=%q\nACTIVE_VERSION=%q\n' "$target_color" "$version" > "$ACTIVE_STATE"
115
+ }
116
+
117
+ restart_current() {
118
+ read_active
119
+ [[ -n "$ACTIVE_VERSION" ]] || { echo "no active version" >&2; exit 1; }
120
+ local target_color image
121
+ target_color=$([[ "$ACTIVE_COLOR" == blue ]] && echo green || echo blue)
122
+ image="$BACKEND_IMAGE_PREFIX:$ACTIVE_VERSION"
123
+ if [[ "$target_color" == blue ]]; then
124
+ BACKEND_BLUE_IMAGE="$image" docker compose -f "$COMPOSE_FILE" up -d --force-recreate backend-blue
125
+ else
126
+ BACKEND_GREEN_IMAGE="$image" docker compose -f "$COMPOSE_FILE" up -d --force-recreate backend-green
127
+ fi
128
+ verify_backend "$target_color" "$ACTIVE_VERSION"
129
+ switch_traffic "$target_color" "$ACTIVE_VERSION"
130
+ printf 'ACTIVE_COLOR=%q\nACTIVE_VERSION=%q\n' "$target_color" "$ACTIVE_VERSION" > "$ACTIVE_STATE"
131
+ }
132
+
133
+ rollback_version() {
134
+ local version="$1"
135
+ assert_version "$version"
136
+ [[ -f "$PREVIOUS_STATE" ]] || { echo "no previous deployment" >&2; exit 1; }
137
+ # shellcheck disable=SC1090
138
+ source "$PREVIOUS_STATE"
139
+ [[ "$ACTIVE_VERSION" == "$version" ]] || { echo "rollback target is not the retained previous version" >&2; exit 1; }
140
+ verify_backend "$ACTIVE_COLOR" "$ACTIVE_VERSION"
141
+ switch_traffic "$ACTIVE_COLOR" "$ACTIVE_VERSION"
142
+ cp "$ACTIVE_STATE" "$PREVIOUS_STATE.tmp"
143
+ cp "$PREVIOUS_STATE" "$ACTIVE_STATE"
144
+ mv "$PREVIOUS_STATE.tmp" "$PREVIOUS_STATE"
145
+ }
146
+
147
+ case "${1:-}" in
148
+ start)
149
+ version="$2"
150
+ start_version "$version"
151
+ ;;
152
+ stop)
153
+ docker compose -f "$COMPOSE_FILE" down
154
+ ;;
155
+ restart)
156
+ restart_current
157
+ ;;
158
+ status)
159
+ read_active
160
+ printf 'color=%s\nversion=%s\n' "$ACTIVE_COLOR" "$ACTIVE_VERSION"
161
+ docker compose -f "$COMPOSE_FILE" ps
162
+ ;;
163
+ rollback)
164
+ version="$2"
165
+ rollback_version "$version"
166
+ ;;
167
+ *)
168
+ echo "usage: ./deploy.sh start <version>|stop|restart|status|rollback <version>" >&2
169
+ exit 2
170
+ ;;
171
+ esac
@@ -0,0 +1,34 @@
1
+ services:
2
+ backend-blue:
3
+ image: ${BACKEND_BLUE_IMAGE:-backend-blue:not-configured}
4
+ env_file: ../../.env.production
5
+ environment:
6
+ INSTANCE_ID: blue
7
+ LOG_DIR: /var/log/application
8
+ ports:
9
+ - "127.0.0.1:${BLUE_PORT:-3001}:3000"
10
+ volumes:
11
+ - ${HOST_LOG_DIR:?set HOST_LOG_DIR}/backend/blue:/var/log/application
12
+ restart: unless-stopped
13
+ healthcheck:
14
+ test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
15
+ interval: 10s
16
+ timeout: 3s
17
+ retries: 6
18
+
19
+ backend-green:
20
+ image: ${BACKEND_GREEN_IMAGE:-backend-green:not-configured}
21
+ env_file: ../../.env.production
22
+ environment:
23
+ INSTANCE_ID: green
24
+ LOG_DIR: /var/log/application
25
+ ports:
26
+ - "127.0.0.1:${GREEN_PORT:-3002}:3000"
27
+ volumes:
28
+ - ${HOST_LOG_DIR:?set HOST_LOG_DIR}/backend/green:/var/log/application
29
+ restart: unless-stopped
30
+ healthcheck:
31
+ test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
32
+ interval: 10s
33
+ timeout: 3s
34
+ retries: 6
@@ -0,0 +1,26 @@
1
+ include /srv/<project>/deploy/nginx/backend-active.conf;
2
+
3
+ server {
4
+ location /api/ {
5
+ proxy_pass http://backend_active;
6
+ proxy_set_header X-Trace-Id $request_id;
7
+ }
8
+
9
+ location /releases/ {
10
+ root /srv/<project>;
11
+ try_files $uri =404;
12
+ }
13
+
14
+ location / {
15
+ root /srv/<project>/current;
16
+ try_files $uri $uri/ /index.html;
17
+ }
18
+
19
+ location = /index.html {
20
+ add_header Cache-Control "no-cache, no-store, must-revalidate" always;
21
+ }
22
+
23
+ location ~* ^/releases/.+\.(?:js|css|woff2?|png|jpg|svg)$ {
24
+ add_header Cache-Control "public, max-age=31536000, immutable" always;
25
+ }
26
+ }
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: wdyy-internal-api-standard
3
+ description: 规范企业内部 REST API 客户端的认证头、traceId、超时、受控重试、熔断、错误映射、日志和响应校验。Use when DEFINE 阶段识别接口边界、BUILD 阶段调用内部服务或 REVIEW 阶段发现散落 fetch、axios、httpClient 调用时。
4
+ ---
5
+
6
+ # 企业内部接口规范
7
+
8
+ ## Overview
9
+
10
+ 将内部服务调用收敛到可追踪、可验证且有明确失败语义的 REST 客户端边界。
11
+
12
+ ## When to Use
13
+
14
+ - 识别接口边界、实现内部服务调用或审查业务层 HTTP 调用时。
15
+
16
+ ## 协作边界
17
+
18
+ 先确认接口契约、错误语义、实现计划和测试要求。本 skill 独立规定企业客户端实现约束,不能替代实现和验证工作。
19
+
20
+ ## 输入与输出
21
+
22
+ - 输入:接口契约、认证方案、超时预算、错误码、重试安全性和 Mock 场景。
23
+ - 输出:统一客户端、接口定义、错误处理、Mock 与测试证据。
24
+ - 使用 [客户端模板](templates/api-client.template.ts)、[错误模板](templates/api-error.template.ts) 和 [规则](reference/internal-api-rules.md)。
25
+
26
+ ## 执行步骤
27
+
28
+ 1. 确认服务边界、认证头、请求/响应 schema、超时预算及幂等条件。
29
+ 2. 通过唯一封装客户端发起所有内部调用,注入 traceId 和请求日志。
30
+ 3. 仅对明确幂等且可恢复的错误执行有限重试;配置熔断与可观测错误码。
31
+ 4. 校验响应结构,映射外部错误为业务可处理的稳定错误。
32
+ 5. 用 Mock 覆盖超时、认证失败、无效响应、重试和熔断路径。
33
+
34
+ ## 禁止事项
35
+
36
+ - 不得在业务代码中散落 `fetch`、`axios` 或原始 `httpClient` 调用。
37
+ - 不得对非幂等写操作无条件重试。
38
+ - 不得吞掉响应校验、超时、错误码或调用日志。
39
+
40
+ ## Red Flags
41
+
42
+ - 业务模块直接调用 fetch、axios 或原始 httpClient。
43
+ - 未定义幂等性却配置重试,或未校验响应结构。
44
+
45
+ ## Verification
46
+
47
+ - [ ] 调用均通过封装客户端,traceId 与认证头可验证。
48
+ - [ ] 超时、重试上限、熔断与错误码映射有测试。
49
+ - [ ] 无效响应不能进入业务逻辑。
50
+ - [ ] Mock 覆盖成功及主要异常分支。
51
+
52
+ 认证、幂等性或错误语义未定义时,停止调用实现并回到接口契约澄清;不得以无限重试或吞错替代决策。
53
+
54
+ ## Common Rationalizations
55
+
56
+ | 合理化说法 | 事实 |
57
+ |---|---|
58
+ | “一个 fetch 不值得封装” | 分散调用无法统一认证、追踪、超时和错误行为。 |
59
+ | “重试总会提高成功率” | 非幂等重试会造成重复写入和状态污染。 |
60
+ | “内部服务可以信任响应” | 服务边界仍可能发生版本漂移和故障。 |
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Enterprise Internal API Standard"
3
+ short_description: "Standardize internal REST client integrations"
4
+ default_prompt: "Use $wdyy-internal-api-standard to implement an internal REST API client."
@@ -0,0 +1,5 @@
1
+ # 内部 API 规则
2
+
3
+ - 认证头、`x-trace-id`、超时、响应 schema 与错误码映射均由统一客户端处理。
4
+ - 仅明确幂等请求可有限重试;熔断状态必须可观测。
5
+ - 禁止业务层直接调用 fetch、axios 或原始 httpClient。
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ const file = process.argv[2];
5
+ if (!file) throw new Error('Pass a business source file');
6
+ const content = await readFile(file, 'utf8');
7
+ if (/\b(fetch|axios\.|httpClient\.)\s*\(/.test(content)) {
8
+ throw new Error(`Raw HTTP call found in ${file}; use the internal API client`);
9
+ }
10
+ process.stdout.write('no raw HTTP calls\n');
@@ -0,0 +1,20 @@
1
+ type RequestOptions = { method: string; path: string; body?: unknown; traceId: string; idempotent?: boolean };
2
+
3
+ export class InternalApiClient {
4
+ constructor(private readonly baseUrl: string, private readonly token: () => Promise<string>) {}
5
+
6
+ async request<T>(options: RequestOptions): Promise<T> {
7
+ const response = await fetch(`${this.baseUrl}${options.path}`, {
8
+ method: options.method,
9
+ headers: {
10
+ authorization: `Bearer ${await this.token()}`,
11
+ 'content-type': 'application/json',
12
+ 'x-trace-id': options.traceId,
13
+ },
14
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
15
+ signal: AbortSignal.timeout(5_000),
16
+ });
17
+ if (!response.ok) throw new Error(`INTERNAL_API_${response.status}`);
18
+ return await response.json() as T;
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ export class InternalApiError extends Error {
2
+ constructor(
3
+ readonly code: string,
4
+ readonly retryable: boolean,
5
+ message: string,
6
+ ) { super(message); }
7
+ }
@@ -0,0 +1,8 @@
1
+ import { http, HttpResponse } from 'msw';
2
+
3
+ export const handlers = [
4
+ http.get('*/internal/<resource>', ({ request }) => {
5
+ if (!request.headers.get('x-trace-id')) return HttpResponse.json({ code: 'TRACE_ID_REQUIRED' }, { status: 400 });
6
+ return HttpResponse.json({ data: [] });
7
+ }),
8
+ ];
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: wdyy-logging-standard
3
+ description: 为 NestJS 和 Vue 项目实现前端异常上报、后端统一结构化 JSON 日志、蓝绿实例隔离、traceId 传递、敏感信息脱敏与 2MB 文件轮转。Use when 编写日志、错误处理、前端异常上报或审查生产可观测性时。
4
+ ---
5
+
6
+ # 企业日志规范
7
+
8
+ ## Overview
9
+
10
+ 建立可追踪、脱敏、可轮转的结构化日志,以支撑运行时排障而不泄露敏感信息。
11
+
12
+ ## When to Use
13
+
14
+ - 编写业务日志、错误处理、接口中间件或进行可观测性审查时。
15
+
16
+ ## 协作边界
17
+
18
+ 日志实现必须纳入可观测性、实现计划和测试验证。本 skill 独立规定企业字段与隐私边界,不能降低 RED 指标、追踪或告警要求。
19
+
20
+ ## 输入与输出
21
+
22
+ - 输入:服务名、实例标识、环境、请求上下文、敏感字段清单、`LOG_DIR`。
23
+ - 输出:统一 logger、前端异常上报客户端、HTTP 日志中间件、脱敏规则和日志验证结果。
24
+ - 使用 [日志规则](reference/logging-rules.md) 和 [logger 模板](templates/logger.template.ts)。
25
+
26
+ ## 执行步骤
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`,精确到秒。
29
+ 2. 在入口生成或透传 traceId;在内部 REST 调用中继续传递。
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 时创建新文件,并测试轮转。
35
+
36
+ ## 禁止事项
37
+
38
+ - 不得记录秘密、完整敏感身份信息或未裁剪的大请求/响应对象。
39
+ - 不得将 `message` 仅限制为 `success` 或 `error`。
40
+ - 不得用 console 输出替代业务日志。
41
+
42
+ ## Red Flags
43
+
44
+ - 业务代码直接调用 console,或日志包含未脱敏的凭证、身份信息。
45
+ - 请求日志缺失 traceId、状态码或耗时。
46
+ - 蓝绿容器共享同一日志文件,或前端异常未进入统一后端日志。
47
+
48
+ ## Verification
49
+
50
+ - [ ] 正常、异常和 HTTP 请求日志均为可解析 JSON。
51
+ - [ ] traceId 跨入口与内部调用可追踪。
52
+ - [ ] 敏感字段移除、数组截断、HTTP 入参与状态结果映射均有测试覆盖,轮转阈值可验证。
53
+ - [ ] 前端异常上报经过裁剪、脱敏和后端身份校验。
54
+ - [ ] 蓝绿实例使用独立日志目录,容器替换后日志仍保留。
55
+ - [ ] `LOG_DIR` 缺失或不可写时服务以明确错误停止。
56
+
57
+ 无法确定某字段是否敏感时按敏感处理并要求数据负责人确认;不得先原样记录再补救。
58
+
59
+ ## Common Rationalizations
60
+
61
+ | 合理化说法 | 事实 |
62
+ |---|---|
63
+ | “开发日志不用脱敏” | 开发日志同样会被共享和长期保留。 |
64
+ | “console.log 足够定位问题” | 无统一字段、轮转和 traceId 的输出不能支撑运维。 |
65
+ | “完整请求最方便排查” | 便利不构成暴露隐私和大对象的理由。 |
@@ -0,0 +1,4 @@
1
+ interface:
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."
@@ -0,0 +1,12 @@
1
+ # 日志规则
2
+
3
+ - 日志为一行 JSON,最小字段见 `logger.template.ts`。
4
+ - `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]`。
9
+ - HTTP 入口和内部服务调用必须透传 traceId。
10
+ - 每条日志必须包含 `instanceId`;生产蓝绿容器分别写入 `LOG_DIR/<service>/<instanceId>/`,不得并发写同一文件。
11
+ - Vue 未处理异常和 Promise 拒绝必须裁剪、脱敏后上报后端;后端校验请求身份和大小后写入统一日志。
12
+ - 日志目录必须挂载到容器外部持久路径,替换容器不得删除历史日志。
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ const entry = JSON.parse(await readFile(process.argv[2], 'utf8'));
5
+ const required = ['timestamp', 'level', 'service', 'instanceId', 'env', 'message'];
6
+ const missing = required.filter((key) => !(key in entry));
7
+ if (missing.length) throw new Error('Missing log fields: ' + missing.join(', '));
8
+ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(entry.timestamp)) {
9
+ throw new Error('timestamp must use local YYYY-MM-DD HH:mm:ss format');
10
+ }
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
+ if ('statusCode' in entry) {
32
+ const expected = entry.statusCode >= 100 && entry.statusCode <= 399 ? 'success'
33
+ : entry.statusCode >= 400 && entry.statusCode <= 599 ? 'failure' : undefined;
34
+ if (!expected || entry.result !== expected) throw new Error('statusCode and result must match');
35
+ }
36
+ process.stdout.write('valid log entry\n');