guanwei 1.3.4 → 1.3.5

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.
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- // 发布内容守卫(硬门槛):断言 npm 包内不含任何运行时数据与密钥
3
- //
4
- // 用法:node scripts/check-package.mjs (CI / release / 本地发布前均调用)
5
- // 退出码:0=干净;1=命中敏感内容(拒绝发布)
6
- //
7
- // 背景(2026-09 事故):`files` 白名单曾含 `server/src`,而运行时数据默认写在
8
- // `server/src/data/`,导致 1.1.1–1.3.2 全部 npm 版本把用户档案(含 token/passHash/
9
- // 出生信息)与占卜记录打包公开。此守卫对**文件名与文件内容**双重检查。
10
- import { execFileSync } from 'node:child_process';
11
- import fs from 'node:fs';
12
- import os from 'node:os';
13
- import path from 'node:path';
14
-
15
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gw-pack-'));
16
- let tgz;
17
- try {
18
- // --cache 指向临时目录:避免依赖本机 ~/.npm 缓存状态(权限异常时 npm 会直接失败)
19
- const out = execFileSync('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', tmp, '--cache', path.join(tmp, 'npmcache')],
20
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
21
- tgz = path.join(tmp, JSON.parse(out)[0].filename);
22
- } catch (e) {
23
- console.error('✗ npm pack 失败:', e.message);
24
- process.exit(1);
25
- }
26
-
27
- // 1) 文件名黑名单
28
- const BAD_NAME = [
29
- /(^|\/)\.env(\.|$)/, /(^|\/)db\.json$/, /(^|\/)guanwei\.db$/, /\.(sqlite|sqlite3|db)$/,
30
- /(^|\/)\.npmrc$/, /(^|\/)id_rsa/, /\.pem$/, /(^|\/)secret/i,
31
- ];
32
- const names = execFileSync('tar', ['-tzf', tgz], { encoding: 'utf8' }).split('\n').filter(Boolean);
33
- const badNames = names.filter(n => BAD_NAME.some(re => re.test(n)));
34
-
35
- // 2) 内容级扫描(解包后逐文件,跳过二进制大文件)
36
- const dir = path.join(tmp, 'x');
37
- fs.mkdirSync(dir);
38
- execFileSync('tar', ['-xzf', tgz, '-C', dir]);
39
- const BAD_CONTENT = [
40
- [/sk-[A-Za-z0-9_-]{20,}/, '疑似 OpenAI/DeepSeek 风格 key'],
41
- [/AIza[0-9A-Za-z_-]{30,}/, '疑似 Google API key'],
42
- [/gsk_[A-Za-z0-9]{40,}/, '疑似 Groq key'],
43
- [/sk-[a-f0-9]{32}/, '疑似 DashScope key'],
44
- [/xox[baprs]-[A-Za-z0-9-]{10,}/, '疑似 Slack token'],
45
- [/gh[pousr]_[A-Za-z0-9]{30,}/, '疑似 GitHub token'],
46
- [/_authToken\s*=\s*(?!\$\{)[A-Za-z0-9_.\-]{16,}/, 'npm 认证 token(真实值)'],
47
- [/"(passHash|tokenExpires)"\s*:/, '用户档案库特征字段'],
48
- ];
49
- const hits = [];
50
- const walk = (d) => {
51
- for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
52
- const p = path.join(d, ent.name);
53
- if (ent.isDirectory()) { walk(p); continue; }
54
- const rel = path.relative(dir, p);
55
- if (/\.(png|jpg|jpeg|gif|ico|woff2?|ttf|db|zip|pdf)$/i.test(ent.name)) continue; // 二进制跳过
56
- if (fs.statSync(p).size > 8 * 1024 * 1024) continue;
57
- const text = fs.readFileSync(p, 'utf8');
58
- for (const [re, label] of BAD_CONTENT) if (re.test(text)) hits.push(rel + ' ← ' + label);
59
- }
60
- };
61
- walk(dir);
62
-
63
- fs.rmSync(tmp, { recursive: true, force: true });
64
-
65
- if (badNames.length || hits.length) {
66
- console.error('✗ 包内容守卫未通过——拒绝发布');
67
- if (badNames.length) console.error(' 敏感文件:', badNames.slice(0, 10));
68
- if (hits.length) console.error(' 敏感内容:', hits.slice(0, 10));
69
- process.exit(1);
70
- }
71
- console.log(`✅ 包内容干净(${names.length} 个文件,无运行时数据/密钥)`);
@@ -1,52 +0,0 @@
1
- // 本地凭据加载(统一入口,供各脚本复用)
2
- //
3
- // 设计说明(为什么不是直接让 npm 读 .env):
4
- // - npm CLI **不读** .env;它只读 .npmrc,并支持 `${VAR}` 环境变量插值
5
- // - 因此约定:token 值集中放 internal/.env(人可读、整目录 gitignore),
6
- // 由脚本读取后注入环境/临时 .npmrc,再调用 npm
7
- //
8
- // 读取优先级:环境变量 > internal/.env > internal/<name> 独立文件
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
-
12
- const ROOT = process.cwd();
13
- const ENV_FILE = path.join(ROOT, 'internal', '.env');
14
-
15
- /** 极简 dotenv 解析(仅 KEY=VALUE,支持 # 整行注释与引号) */
16
- function parseEnvFile(file) {
17
- const out = {};
18
- if (!fs.existsSync(file)) return out;
19
- for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
20
- const line = raw.trim();
21
- if (!line || line.startsWith('#')) continue;
22
- const eq = line.indexOf('=');
23
- if (eq <= 0) continue;
24
- const k = line.slice(0, eq).trim();
25
- let v = line.slice(eq + 1).trim();
26
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
27
- if (v) out[k] = v;
28
- }
29
- return out;
30
- }
31
-
32
- function readFileIfExists(rel) {
33
- const p = path.join(ROOT, rel);
34
- try { return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').trim() : ''; } catch { return ''; }
35
- }
36
-
37
- /** 返回 { GITHUB_TOKEN, NPM_TOKEN }(缺失则为空字符串) */
38
- export function loadCredentials() {
39
- const fromEnvFile = parseEnvFile(ENV_FILE);
40
- const pick = (key, file) =>
41
- process.env[key]?.trim() || fromEnvFile[key] || readFileIfExists(file) || '';
42
- return {
43
- GITHUB_TOKEN: pick('GITHUB_TOKEN', 'internal/github-token'),
44
- NPM_TOKEN: pick('NPM_TOKEN', 'internal/npm-token'),
45
- };
46
- }
47
-
48
- export const CRED_FILES = {
49
- env: 'internal/.env',
50
- github: 'internal/github-token',
51
- npm: 'internal/npm-token',
52
- };
@@ -1,140 +0,0 @@
1
- #!/usr/bin/env node
2
- // GitHub 仓库历史清理工具(可复用于任何仓库)
3
- //
4
- // 场景:用 git filter-repo 重写历史后,远端仍残留指向旧历史的 tag / release / 分支
5
- // (尤其 immutable release 的 tag 会被 GitHub 永久绑定,无法用 git push 更新)。
6
- //
7
- // 用法:
8
- // GITHUB_TOKEN=xxx node scripts/github-history-cleanup.mjs list --repo owner/name
9
- // GITHUB_TOKEN=xxx node scripts/github-history-cleanup.mjs purge --repo owner/name --keep v1.3.4 [--apply]
10
- // GITHUB_TOKEN=xxx node scripts/github-history-cleanup.mjs branches --repo owner/name [--apply]
11
- //
12
- // token 读取顺序:--token-file > $GITHUB_TOKEN > internal/github-token(本地私有,git 忽略)
13
- // 默认 **dry-run**:不加 --apply 只打印将要执行的操作。
14
- import fs from 'node:fs';
15
- import path from 'node:path';
16
-
17
- const argv = process.argv.slice(2);
18
- const cmd = argv[0];
19
- const arg = (name, def) => {
20
- const i = argv.indexOf(name);
21
- return i >= 0 && argv[i + 1] ? argv[i + 1] : def;
22
- };
23
- const has = (name) => argv.includes(name);
24
- const REPO = arg('--repo');
25
- const KEEP = (arg('--keep', '') || '').split(',').map(s => s.trim()).filter(Boolean);
26
- const APPLY = has('--apply');
27
- if (!REPO || !REPO.includes('/')) {
28
- console.error('用法: node scripts/github-history-cleanup.mjs <list|purge|branches|releases> --repo owner/name [--keep tag,...] [--apply]');
29
- process.exit(1);
30
- }
31
- const TOKEN = (() => {
32
- const f = arg('--token-file');
33
- const candidates = [f, process.env.GITHUB_TOKEN && '(env)', path.resolve('internal/github-token')].filter(Boolean);
34
- if (f && fs.existsSync(f)) return fs.readFileSync(f, 'utf8').trim();
35
- if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN.trim();
36
- const local = path.resolve('internal/github-token');
37
- if (fs.existsSync(local)) return fs.readFileSync(local, 'utf8').trim();
38
- console.error('缺少 token(--token-file / $GITHUB_TOKEN / internal/github-token 均不可用)', candidates);
39
- process.exit(1);
40
- })();
41
-
42
- async function api(method, p, body) {
43
- const res = await fetch('https://api.github.com' + p, {
44
- method,
45
- headers: {
46
- Authorization: 'Bearer ' + TOKEN,
47
- Accept: 'application/vnd.github+json',
48
- 'X-GitHub-Api-Version': '2022-11-28',
49
- ...(body ? { 'Content-Type': 'application/json' } : {}),
50
- },
51
- body: body ? JSON.stringify(body) : undefined,
52
- });
53
- const text = await res.text();
54
- return { status: res.status, body: text ? JSON.parse(text) : null };
55
- }
56
-
57
- async function listAll(p) {
58
- const out = [];
59
- for (let page = 1; page <= 10; page++) {
60
- const { status, body } = await api('GET', `${p}${p.includes('?') ? '&' : '?'}per_page=100&page=${page}`);
61
- if (status !== 200 || !Array.isArray(body) || body.length === 0) break;
62
- out.push(...body);
63
- if (body.length < 100) break;
64
- }
65
- return out;
66
- }
67
-
68
- const info = async () => {
69
- const { status, body } = await api('GET', `/repos/${REPO}`);
70
- if (status !== 200) { console.error('仓库不可读:', status, body?.message); process.exit(1); }
71
- return body;
72
- };
73
-
74
- const run = async () => {
75
- const repo = await info();
76
- const releases = await listAll(`/repos/${REPO}/releases`);
77
- const tags = await listAll(`/repos/${REPO}/git/refs/tags`);
78
- const branches = await listAll(`/repos/${REPO}/branches`);
79
- const tagNames = tags.map(t => t.ref.replace('refs/tags/', ''));
80
-
81
- if (cmd === 'list') {
82
- console.log(`仓库 ${REPO}(默认分支 ${repo.default_branch})`);
83
- console.log(` releases (${releases.length}):`, releases.map(r => `${r.tag_name}${r.immutable ? '(immutable)' : ''}`).join(', ') || '无');
84
- console.log(` tags (${tagNames.length}):`, tagNames.join(', ') || '无');
85
- console.log(` branches (${branches.length}):`, branches.map(b => b.name).join(', '));
86
- const stale = tagNames.filter(t => !KEEP.includes(t));
87
- console.log(`\n--keep 之外的 tag(purge 将删除其 release + tag):`, stale.join(', ') || '无');
88
- return;
89
- }
90
-
91
- if (cmd === 'branches') {
92
- const stale = branches.map(b => b.name).filter(n => n !== repo.default_branch);
93
- if (!stale.length) return console.log('无非默认分支');
94
- for (const n of stale) {
95
- if (!APPLY) { console.log(`[dry-run] 将删除分支 ${n}`); continue; }
96
- const { status } = await api('DELETE', `/repos/${REPO}/git/refs/heads/${n}`);
97
- console.log(`删除分支 ${n} → ${status}`);
98
- }
99
- return;
100
- }
101
-
102
- if (cmd === 'purge' || cmd === 'releases') {
103
- // 防误删:未显式 --keep 时,默认保留「最新版本 tag」(latest release → semver 最大者)
104
- let keep = [...KEEP];
105
- if (keep.length === 0) {
106
- const { body: latest } = await api('GET', `/repos/${REPO}/releases/latest`);
107
- const cand = latest?.tag_name || tagNames
108
- .slice()
109
- .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
110
- .pop();
111
- if (cand) { keep = [cand]; console.log(`(未指定 --keep,默认保留最新版本 ${cand})`); }
112
- }
113
- const staleTags = tagNames.filter(t => !keep.includes(t));
114
- const byTag = new Map(releases.map(r => [r.tag_name, r]));
115
- for (const t of staleTags) {
116
- const rel = byTag.get(t);
117
- if (rel) {
118
- if (!APPLY) console.log(`[dry-run] 将删除 release ${t}${rel.immutable ? '(immutable)' : ''} + tag ${t}`);
119
- else {
120
- const d1 = await api('DELETE', `/repos/${REPO}/releases/${rel.id}`);
121
- const d2 = await api('DELETE', `/repos/${REPO}/git/refs/tags/${t}`);
122
- console.log(`删除 ${t}: release=${d1.status} tag=${d2.status}`);
123
- }
124
- } else if (cmd === 'purge') {
125
- if (!APPLY) console.log(`[dry-run] 将删除 tag ${t}(无 release)`);
126
- else {
127
- const d = await api('DELETE', `/repos/${REPO}/git/refs/tags/${t}`);
128
- console.log(`删除 tag ${t} → ${d.status}`);
129
- }
130
- }
131
- }
132
- if (!APPLY) console.log(`\n共 ${staleTags.length} 个待处理;确认后加 --apply 执行`);
133
- return;
134
- }
135
-
136
- console.error('未知子命令:', cmd);
137
- process.exit(1);
138
- };
139
-
140
- run().catch(e => { console.error('执行失败:', e.message); process.exit(1); });
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env node
2
- // npm 发布工具(读本地凭据 → 校验 → 包内容守卫 → 发布)
3
- //
4
- // 用法:
5
- // node scripts/npm-release.mjs --check # 只校验凭据与登录态(不发布)
6
- // node scripts/npm-release.mjs --dry-run # 走一遍打包与守卫,不真正发布
7
- // node scripts/npm-release.mjs # 正式发布(用 internal/.env 的 NPM_TOKEN)
8
- // node scripts/npm-release.mjs --otp 123456 # 用动态码(无 Automation token 时)
9
- //
10
- // 为什么需要它:npm 不读 .env;本脚本把 internal/.env 的 NPM_TOKEN 注入临时 .npmrc 后调用 npm。
11
- import { execFileSync, spawnSync } from 'node:child_process';
12
- import fs from 'node:fs';
13
- import path from 'node:path';
14
- import { loadCredentials, CRED_FILES } from './credentials.mjs';
15
-
16
- const argv = process.argv.slice(2);
17
- const has = (f) => argv.includes(f);
18
- const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : ''; };
19
- const REGISTRY = 'https://registry.npmjs.org';
20
- const ROOT = process.cwd();
21
-
22
- const { NPM_TOKEN } = loadCredentials();
23
- const otp = val('--otp');
24
-
25
- async function whoami(token) {
26
- const res = await fetch(`${REGISTRY}/-/whoami`, { headers: token ? { Authorization: 'Bearer ' + token } : {} });
27
- if (!res.ok) return null;
28
- return (await res.json()).username;
29
- }
30
-
31
- const run = async () => {
32
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
33
- console.log(`包 ${pkg.name}@${pkg.version} → ${REGISTRY}`);
34
-
35
- // 1) 凭据检查
36
- let user = null;
37
- if (NPM_TOKEN) {
38
- user = await whoami(NPM_TOKEN);
39
- console.log(`NPM_TOKEN 校验: ${user ? '✅ 有效(账号 ' + user + ')' : '❌ 无效/已撤销'}`);
40
- if (!user) process.exit(1);
41
- } else {
42
- console.log(`未找到 NPM_TOKEN(可写入 ${CRED_FILES.env} 或 ${CRED_FILES.npm})`);
43
- user = await whoami(''); // 回退系统 ~/.npmrc
44
- console.log(`系统 npm 登录态: ${user ? '✅ ' + user : '❌ 未登录/已失效'}`);
45
- if (!user && !otp) { console.error('既无 NPM_TOKEN 也无有效登录态 → 无法发布'); process.exit(1); }
46
- }
47
- if (has('--check')) return;
48
-
49
- // 2) 包内容守卫(硬门槛)
50
- if (!has('--skip-guard')) {
51
- console.log('包内容守卫...');
52
- execFileSync('node', ['scripts/check-package.mjs'], { stdio: 'inherit' });
53
- }
54
-
55
- // 3) 临时 userconfig(仅当使用 NPM_TOKEN)
56
- let cfgPath = '';
57
- if (NPM_TOKEN) {
58
- cfgPath = path.join(ROOT, 'internal', '.npmrc');
59
- fs.writeFileSync(cfgPath, `registry=${REGISTRY}\n//registry.npmjs.org/:_authToken=\${NPM_TOKEN}\n`);
60
- fs.chmodSync(cfgPath, 0o600);
61
- }
62
-
63
- // 4) 发布
64
- const args = ['publish', '--registry', REGISTRY];
65
- if (cfgPath) args.push('--userconfig', cfgPath);
66
- if (otp) args.push('--otp', String(otp));
67
- if (has('--dry-run')) args.push('--dry-run');
68
- console.log('执行: npm ' + args.join(' '));
69
- const r = spawnSync('npm', args, {
70
- stdio: 'inherit',
71
- env: { ...process.env, ...(NPM_TOKEN ? { NPM_TOKEN } : {}), npm_config_cache: path.join(ROOT, '.npm-cache') },
72
- });
73
- if (cfgPath) { try { fs.unlinkSync(cfgPath); } catch { /* ignore */ } }
74
- try { fs.rmSync(path.join(ROOT, '.npm-cache'), { recursive: true, force: true }); } catch { /* ignore */ }
75
- if (r.status !== 0) { console.error('❌ 发布失败(退出码 ' + r.status + ')'); process.exit(r.status || 1); }
76
- console.log('✅ 发布完成');
77
- };
78
-
79
- run().catch(e => { console.error('执行失败:', e.message); process.exit(1); });
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env bash
2
- # 发版前自检(本地硬门槛):内容干净 + 测试通过 + 版本一致
3
- # 用法:./scripts/preflight-release.sh [期望版本号]
4
- set -euo pipefail
5
- cd "$(dirname "$0")/.."
6
-
7
- echo "1/5 仓库边界守卫(公开区不得含内部内容;打包区须为公开区子集)..."
8
- node scripts/check-boundary.mjs
9
-
10
- echo "2/5 包内容守卫(文件名 + 内容级扫描,拒绝任何运行时数据/密钥)..."
11
- node scripts/check-package.mjs
12
-
13
- echo "3/5 类型检查..."
14
- npx tsc --noEmit
15
- (cd server && npx tsc --noEmit)
16
-
17
- echo "4/5 全量测试..."
18
- npx vitest run --reporter=dot >/tmp/gw-preflight-test.log 2>&1 || { echo "✗ 测试未通过,见 /tmp/gw-preflight-test.log"; tail -20 /tmp/gw-preflight-test.log; exit 1; }
19
- grep -E "Test Files|Tests " /tmp/gw-preflight-test.log | tail -2
20
-
21
- echo "5/5 版本一致性..."
22
- V=$(node -e "console.log(require('./package.json').version)")
23
- for f in server/package.json packages/guanwei-api/package.json package-lock.json server/package-lock.json; do
24
- Vf=$(node -e "const d=require('./$f');console.log(d.version||d.packages?.['']?.version||'?')")
25
- [ "$Vf" = "$V" ] || { echo "✗ 版本漂移:$f = $Vf,根 = $V"; exit 1; }
26
- done
27
- grep -q "version: '$V'" packages/guanwei-api/src/mcp.ts || echo "⚠️ MCP serverInfo.version 未同步到 $V"
28
- if [ -n "${1:-}" ] && [ "$1" != "$V" ]; then echo "✗ 参数版本 $1 ≠ package.json $V"; exit 1; fi
29
- echo "✅ 自检通过:v$V 可发布"
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ============================================================
3
- # 观微发版脚本:升级版本号 → 同步 lockfile → 打 tag → 推送 git → (可选)发布 npm
4
- # 用法:./scripts/release.sh 1.1.2 仅 git 发版(tag 触发 CI/Release/镜像)
5
- # ./scripts/release.sh 1.1.2 --npm 额外发布 npm 包(需先配置 npm 登录)
6
- # 要求:工作区无未提交改动;CHANGELOG.md 已写好本次版本条目
7
- # 详见(本地文档,不入库):internal/docs/RELEASE-SOP.md(发版 SOP 详情/踩坑记录)
8
- # ============================================================
9
- set -euo pipefail
10
- cd "$(dirname "$0")/.."
11
-
12
- NEW_VER="${1:-}"
13
- DO_NPM=0
14
- if [[ "${2:-}" == "--npm" ]]; then DO_NPM=1; fi
15
- if [[ -z "$NEW_VER" ]] || ! [[ "$NEW_VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
16
- echo "用法: ./scripts/release.sh <版本号> [--npm] 例如: ./scripts/release.sh 1.1.2 --npm"; exit 1
17
- fi
18
-
19
- if [[ -n "$(git status --porcelain)" ]]; then
20
- echo "⚠️ 工作区有未提交改动,先提交或 stash 再发版"; exit 1
21
- fi
22
-
23
- # 0. 发版前必须全量测试通过(后端集成测试会起服务)
24
- echo "0️⃣ 全量测试(npm test)..."
25
- if ! npm test >/tmp/guanwei-release-test.log 2>&1; then
26
- echo " ❌ 测试未通过,见 /tmp/guanwei-release-test.log"; exit 1
27
- fi
28
- echo " ✅ 测试通过"
29
-
30
- echo "1️⃣ 更新 package.json / server/package.json -> ${NEW_VER}"
31
- node -e "
32
- const fs = require('fs');
33
- for (const p of ['package.json', 'server/package.json', 'package-lock.json', 'server/package-lock.json']) {
34
- const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
35
- d.version = '${NEW_VER}';
36
- fs.writeFileSync(p, JSON.stringify(d, null, 2) + '\n');
37
- }
38
- "
39
- echo " ✅ 版本号与 lockfile 已同步"
40
-
41
- echo "2️⃣ 检查 CHANGELOG.md 是否已有 [${NEW_VER}] 条目..."
42
- if ! grep -q "^## \[${NEW_VER}\]" CHANGELOG.md; then
43
- echo " ⚠️ CHANGELOG.md 缺少 [${NEW_VER}] 条目——请补上后再继续(Ctrl+C 中断)"
44
- read -rp " 已补好?回车继续: " _
45
- fi
46
-
47
- # 2.5 防 tag 错位:提交后 HEAD 的 package.json 版本号必须等于目标版本(否则 tag 会落到错误 commit)
48
- echo "2.5️⃣ 校验 HEAD 版本号 === ${NEW_VER}(防 v1.1.0 式 tag 错位)..."
49
- HEAD_VER="$(node -e "console.log(require('./package.json').version)")"
50
- if [[ "$HEAD_VER" != "$NEW_VER" ]]; then
51
- echo " ⚠️ 当前工作区 package.json 版本为 ${HEAD_VER},目标为 ${NEW_VER}"
52
- echo " (若刚跑过步骤 1 的版本升级但尚未提交,继续即可;提交后 tag 前将再次校验)"
53
- fi
54
-
55
- echo "3️⃣ 提交 + 打 tag + 推送 git"
56
- git add package.json package-lock.json server/package.json server/package-lock.json CHANGELOG.md
57
- # 版本号/CHANGELOG 已在之前提交中就位时,此处无暂存差异——跳过提交,避免 set -e 中断发版
58
- if git diff --cached --quiet; then
59
- echo " ℹ️ 版本文件无变化(已在历史提交中就位),跳过版本提交"
60
- else
61
- git commit -m "chore: 版本升级 ${NEW_VER}"
62
- fi
63
- # 提交后再次校验:确保 tag 一定落在版本号已落地的 commit 上
64
- HEAD_VER="$(node -e "console.log(require('./package.json').version)")"
65
- if [[ "$HEAD_VER" != "$NEW_VER" ]]; then
66
- echo " ❌ 提交后 HEAD 版本为 ${HEAD_VER},目标 ${NEW_VER}——版本号未落地,拒绝打 tag"
67
- exit 1
68
- fi
69
- echo " ✅ 版本号一致,打 tag v${NEW_VER}"
70
- git tag "v${NEW_VER}"
71
- git push origin main
72
- git push origin "v${NEW_VER}"
73
- echo " ✅ 已推送——CI / Release / 镜像 / Pages 将自动执行"
74
-
75
- if [[ "$DO_NPM" == "1" ]]; then
76
- echo "4️⃣ 发布 npm(registry 必须是官方源,本机默认 npmmirror 需显式指定)..."
77
- # 用本地缓存避免 ~/.npm 写入受限;发布失败不中断后续提示
78
- npm publish --registry https://registry.npmjs.org --cache ./.npm-cache || echo " ⚠️ npm publish 失败——按 internal/docs/RELEASE-SOP.md 第四节排查(2FA/权限/registry)"
79
- rm -rf .npm-cache
80
- echo "5️⃣ 验证线上版本..."
81
- npm view guanwei version --registry https://registry.npmjs.org --cache ./.npm-cache 2>/dev/null || true
82
- rm -rf .npm-cache
83
- else
84
- echo "(未加 --npm:跳过 npm 发布。如需发布:./scripts/release.sh ${NEW_VER} --npm,或手动 npm publish)"
85
- fi
86
- echo "🎉 v${NEW_VER} 发版完成"
@@ -1,45 +0,0 @@
1
- // 用户库(JSON)读写:进程内串行锁 + 原子写
2
- //
3
- // 背景(2026-09 并发写缺陷):原 users.ts/divine.ts 各自 read-modify-write 整份 db.json,
4
- // 且 load→改→save 之间跨 await(scrypt 哈希),并发请求互相覆盖——实测 30 个并发注册仅 1 个落库。
5
- // 现统一走本模块:单进程内以 Promise 链串行化写操作,落盘用 临时文件 + rename 原子替换。
6
- import fs from 'fs';
7
- import path from 'path';
8
- import { USERS_DB } from './dataDir.js';
9
-
10
- export interface UsersDb { users: any[] }
11
-
12
- /** 读用户库(文件缺失/损坏 → 空库,交由调用方建档) */
13
- export function readUsersDb(): UsersDb {
14
- try { return JSON.parse(fs.readFileSync(USERS_DB, 'utf-8')); }
15
- catch { return { users: [] }; }
16
- }
17
-
18
- /** 原子写:先写同目录临时文件再 rename,避免半截文件与并发撕裂 */
19
- export function writeUsersDb(db: UsersDb): void {
20
- fs.mkdirSync(path.dirname(USERS_DB), { recursive: true });
21
- const tmp = USERS_DB + '.tmp-' + process.pid + '-' + Date.now();
22
- fs.writeFileSync(tmp, JSON.stringify(db, null, 2));
23
- fs.renameSync(tmp, USERS_DB);
24
- }
25
-
26
- // 写串行队列(单进程内生效;多进程部署需外部锁,见 README 部署说明)
27
- let chain: Promise<unknown> = Promise.resolve();
28
-
29
- /** 串行执行一次「读-改-写」事务;fn 内可安全 await(如 scrypt) */
30
- export function withUsersDb<T>(fn: (db: UsersDb) => T | Promise<T>): Promise<T> {
31
- const run = chain.then(() => {
32
- const db = readUsersDb();
33
- return Promise.resolve(fn(db)).then((result) => {
34
- writeUsersDb(db); // fn 内直接改 db 对象即可;由本函数统一落盘
35
- return result;
36
- });
37
- });
38
- chain = run.catch(() => { /* 防止链断 */ });
39
- return run as Promise<T>;
40
- }
41
-
42
- /** 只读事务(不落盘) */
43
- export function readUsers<T>(fn: (db: UsersDb) => T): T {
44
- return fn(readUsersDb());
45
- }