guanwei 1.1.1 → 1.1.3

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 (55) hide show
  1. package/README.en.md +5 -2
  2. package/README.md +6 -3
  3. package/dist/assets/index-C3wrtpPY.css +1 -0
  4. package/dist/assets/{index-CsOD5rSo.js → index-CAyAIgw8.js} +25 -25
  5. package/dist/assets/index-CAyAIgw8.js.map +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/scripts/release.sh +29 -5
  9. package/server/src/data/db.json +168 -0
  10. package/server/src/index.ts +27 -4
  11. package/server/src/routes/ai.ts +37 -0
  12. package/server/src/routes/tarot.ts +4 -4
  13. package/server/src/routes/users.ts +42 -7
  14. package/server/src/services/divineStore.ts +1 -5
  15. package/server/src/services/promptBuilder.ts +2 -2
  16. package/server/src/types/index.ts +2 -104
  17. package/{src → shared/core}/data/lifeContext.ts +2 -8
  18. package/{src → shared/core}/data/spreads.ts +1 -1
  19. package/{src → shared/core}/data/tarotCards.ts +1 -1
  20. package/shared/core/engine/bazi.ts +0 -1
  21. package/shared/core/engine/calendar.ts +1 -1
  22. package/{server/src/utils → shared/core/engine}/semanticAnalyzer.ts +9 -46
  23. package/{server/src/utils → shared/core/engine}/tarotEngine.ts +6 -6
  24. package/shared/core/engine/ziwei.ts +3 -2
  25. package/shared/core/engine/ziweiInterpret.ts +0 -3
  26. package/shared/core/types.ts +157 -0
  27. package/src/arts/registry.ts +1 -1
  28. package/src/components/QuestionFields.tsx +0 -1
  29. package/src/components/ReportView.tsx +0 -1
  30. package/src/components/TimeShichenInput.tsx +1 -0
  31. package/src/components/arts/BaziArt.tsx +0 -10
  32. package/src/components/arts/LiurenArt.tsx +1 -1
  33. package/src/components/arts/LiuyaoArt.tsx +0 -1
  34. package/src/components/arts/MeihuaArt.tsx +0 -1
  35. package/src/components/arts/QimenArt.tsx +0 -1
  36. package/src/components/arts/TarotArt.tsx +2 -2
  37. package/src/components/arts/XiaoliurenArt.tsx +1 -2
  38. package/src/components/arts/ZiweiArt.tsx +2 -15
  39. package/src/pages/AuthPage.tsx +4 -6
  40. package/src/pages/DemoPage.tsx +1 -2
  41. package/src/pages/HistoryPage.tsx +10 -9
  42. package/src/pages/ModulePage.tsx +25 -32
  43. package/src/pages/SpreadLibrary.tsx +1 -1
  44. package/src/pages/TarotPage.tsx +7 -9
  45. package/src/services/api.ts +4 -4
  46. package/src/stores/useDivinationStore.ts +1 -1
  47. package/src/types/index.ts +2 -104
  48. package/tsconfig.json +5 -5
  49. package/dist/assets/index-CsOD5rSo.js.map +0 -1
  50. package/dist/assets/index-DQx62TUr.css +0 -1
  51. package/server/src/data/lifeContext.ts +0 -423
  52. package/server/src/data/spreads.ts +0 -255
  53. package/server/src/data/tarotCards.ts +0 -2353
  54. package/src/utils/semanticAnalyzer.ts +0 -555
  55. package/src/utils/tarotEngine.ts +0 -849
package/dist/index.html CHANGED
@@ -6,8 +6,8 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>观微 · 玄学问道 —— 九术参详,观微知著</title>
8
8
  <meta name="description" content="以纸墨留白为骨,融四柱八字、紫微斗数、奇门遁甲、梅花易数、六爻、大六壬、小六壬、星盘、塔罗九术于一体。观微知著,以术问道。" />
9
- <script type="module" crossorigin src="/assets/index-CsOD5rSo.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DQx62TUr.css">
9
+ <script type="module" crossorigin src="/assets/index-CAyAIgw8.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-C3wrtpPY.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guanwei",
3
3
  "private": false,
4
- "version": "1.1.1",
4
+ "version": "1.1.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -91,6 +91,6 @@
91
91
  "url": "git+https://github.com/RubyCcll/guanwei.git"
92
92
  },
93
93
  "engines": {
94
- "node": ">=18"
94
+ "node": ">=22.5.0"
95
95
  }
96
96
  }
@@ -1,21 +1,32 @@
1
1
  #!/usr/bin/env bash
2
2
  # ============================================================
3
- # 观微发版脚本:升级版本号 → 同步 lockfile → 打 tag → 推送
4
- # 用法:./scripts/release.sh 1.1.2 (新版本号,不带 v 前缀)
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 登录)
5
6
  # 要求:工作区无未提交改动;CHANGELOG.md 已写好本次版本条目
7
+ # 详见(本地文档):docs/RELEASE-SOP.md
6
8
  # ============================================================
7
9
  set -euo pipefail
8
10
  cd "$(dirname "$0")/.."
9
11
 
10
12
  NEW_VER="${1:-}"
13
+ DO_NPM=0
14
+ if [[ "${2:-}" == "--npm" ]]; then DO_NPM=1; fi
11
15
  if [[ -z "$NEW_VER" ]] || ! [[ "$NEW_VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
12
- echo "用法: ./scripts/release.sh <版本号> 例如: ./scripts/release.sh 1.1.2"; exit 1
16
+ echo "用法: ./scripts/release.sh <版本号> [--npm] 例如: ./scripts/release.sh 1.1.2 --npm"; exit 1
13
17
  fi
14
18
 
15
19
  if [[ -n "$(git status --porcelain)" ]]; then
16
20
  echo "⚠️ 工作区有未提交改动,先提交或 stash 再发版"; exit 1
17
21
  fi
18
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
+
19
30
  echo "1️⃣ 更新 package.json / server/package.json -> ${NEW_VER}"
20
31
  node -e "
21
32
  const fs = require('fs');
@@ -33,10 +44,23 @@ if ! grep -q "^## \[${NEW_VER}\]" CHANGELOG.md; then
33
44
  read -rp " 已补好?回车继续: " _
34
45
  fi
35
46
 
36
- echo "3️⃣ 提交 + 打 tag + 推送"
47
+ echo "3️⃣ 提交 + 打 tag + 推送 git"
37
48
  git add package.json package-lock.json server/package.json server/package-lock.json CHANGELOG.md
38
49
  git commit -m "chore: 版本升级 ${NEW_VER}"
39
50
  git tag "v${NEW_VER}"
40
51
  git push origin main
41
52
  git push origin "v${NEW_VER}"
42
- echo "🎉 v${NEW_VER} 已推送——CI / Release / 镜像 / Pages 将自动执行"
53
+ echo " ✅ 已推送——CI / Release / 镜像 / Pages 将自动执行"
54
+
55
+ if [[ "$DO_NPM" == "1" ]]; then
56
+ echo "4️⃣ 发布 npm(registry 必须是官方源,本机默认 npmmirror 需显式指定)..."
57
+ # 用本地缓存避免 ~/.npm 写入受限;发布失败不中断后续提示
58
+ npm publish --registry https://registry.npmjs.org --cache ./.npm-cache || echo " ⚠️ npm publish 失败——按 docs/RELEASE-SOP.md 第四节排查(2FA/权限/registry)"
59
+ rm -rf .npm-cache
60
+ echo "5️⃣ 验证线上版本..."
61
+ npm view guanwei version --registry https://registry.npmjs.org --cache ./.npm-cache 2>/dev/null || true
62
+ rm -rf .npm-cache
63
+ else
64
+ echo "(未加 --npm:跳过 npm 发布。如需发布:./scripts/release.sh ${NEW_VER} --npm,或手动 npm publish)"
65
+ fi
66
+ echo "🎉 v${NEW_VER} 发版完成"
@@ -12054,6 +12054,174 @@
12054
12054
  }
12055
12055
  }
12056
12056
  ]
12057
+ },
12058
+ {
12059
+ "username": "联调3661",
12060
+ "passHash": "h7a11ca53",
12061
+ "createdAt": 1787394463725,
12062
+ "profile": {
12063
+ "birthDate": "1996-09-09",
12064
+ "birthHourIndex": 5,
12065
+ "gender": "男",
12066
+ "location": null
12067
+ },
12068
+ "samples": [],
12069
+ "records": [
12070
+ {
12071
+ "artId": "bazi",
12072
+ "createdAt": 1,
12073
+ "result": {
12074
+ "dayGZ": "壬申"
12075
+ }
12076
+ }
12077
+ ]
12078
+ },
12079
+ {
12080
+ "username": "联调7035",
12081
+ "passHash": "h7a11ca53",
12082
+ "createdAt": 1787462187089,
12083
+ "profile": {
12084
+ "birthDate": "1996-09-09",
12085
+ "birthHourIndex": 5,
12086
+ "gender": "男",
12087
+ "location": null
12088
+ },
12089
+ "samples": [],
12090
+ "records": [
12091
+ {
12092
+ "artId": "bazi",
12093
+ "createdAt": 1,
12094
+ "result": {
12095
+ "dayGZ": "壬申"
12096
+ }
12097
+ }
12098
+ ]
12099
+ },
12100
+ {
12101
+ "username": "联调6052",
12102
+ "passHash": "h7a11ca53",
12103
+ "createdAt": 1787463476120,
12104
+ "profile": {
12105
+ "birthDate": "1996-09-09",
12106
+ "birthHourIndex": 5,
12107
+ "gender": "男",
12108
+ "location": null
12109
+ },
12110
+ "samples": [],
12111
+ "records": [
12112
+ {
12113
+ "artId": "bazi",
12114
+ "createdAt": 1,
12115
+ "result": {
12116
+ "dayGZ": "壬申"
12117
+ }
12118
+ }
12119
+ ]
12120
+ },
12121
+ {
12122
+ "username": "联调4217",
12123
+ "passHash": "scrypt$5d770ea33c351c28afc8ee563d5712a2$7d00490154d70d549eba3a28d0a94a8cf2b4b23fa2fcde990200e850566993779a3854ed20858039690fb21a59ac814942947d95d4bad8f4ae328317d3e35137",
12124
+ "createdAt": 1787463514296,
12125
+ "profile": {
12126
+ "birthDate": "1996-09-09",
12127
+ "birthHourIndex": 5,
12128
+ "gender": "男",
12129
+ "location": null
12130
+ },
12131
+ "samples": [],
12132
+ "records": [
12133
+ {
12134
+ "artId": "bazi",
12135
+ "createdAt": 1,
12136
+ "result": {
12137
+ "dayGZ": "壬申"
12138
+ }
12139
+ }
12140
+ ]
12141
+ },
12142
+ {
12143
+ "username": "联调3642",
12144
+ "passHash": "scrypt$e68ed2ead08e6da65060e6a7eaa44d4f$494bf83f166632db29cc3d85b334b5a7e64bec70949721cf3ef77c71b3e2b4f2b6a9abf6fe3bf8371ab506df450ead79ba01bda5005beaba47fdc269b9492c64",
12145
+ "createdAt": 1787464003708,
12146
+ "profile": {
12147
+ "birthDate": "1996-09-09",
12148
+ "birthHourIndex": 5,
12149
+ "gender": "男",
12150
+ "location": null
12151
+ },
12152
+ "samples": [],
12153
+ "records": [
12154
+ {
12155
+ "artId": "bazi",
12156
+ "createdAt": 1,
12157
+ "result": {
12158
+ "dayGZ": "壬申"
12159
+ }
12160
+ }
12161
+ ]
12162
+ },
12163
+ {
12164
+ "username": "联调7150",
12165
+ "passHash": "scrypt$a691ebce2d89d81489e7209925cda9ed$4f9aaf62d714d4ce7bc8f1df2566461161bd8ad32bad88d23680534bede52d3a99efd28897ddbd75eedcf5eb08bc7b748020cc3d242acc8966c96d52fbf53c0d",
12166
+ "createdAt": 1787464067260,
12167
+ "profile": {
12168
+ "birthDate": "1996-09-09",
12169
+ "birthHourIndex": 5,
12170
+ "gender": "男",
12171
+ "location": null
12172
+ },
12173
+ "samples": [],
12174
+ "records": [
12175
+ {
12176
+ "artId": "bazi",
12177
+ "createdAt": 1,
12178
+ "result": {
12179
+ "dayGZ": "壬申"
12180
+ }
12181
+ }
12182
+ ]
12183
+ },
12184
+ {
12185
+ "username": "联调7275",
12186
+ "passHash": "scrypt$5f91de795c181fe55ef00de36e94e7b8$5d3bd0f8d7097ea4fac56be03934a4a8638642c784f82d066d7b4b582669024c9d38ae82e19d7ab73781a7ab80130858b03a7490f979e04d7ed72294079571e6",
12187
+ "createdAt": 1787468567400,
12188
+ "profile": {
12189
+ "birthDate": "1996-09-09",
12190
+ "birthHourIndex": 5,
12191
+ "gender": "男",
12192
+ "location": null
12193
+ },
12194
+ "samples": [],
12195
+ "records": [
12196
+ {
12197
+ "artId": "bazi",
12198
+ "createdAt": 1,
12199
+ "result": {
12200
+ "dayGZ": "壬申"
12201
+ }
12202
+ }
12203
+ ]
12204
+ },
12205
+ {
12206
+ "username": "联调9581",
12207
+ "passHash": "scrypt$16272308b8058bf6ccbe9d3a2ed81a4d$52177ab888d594c6da154cf421557b10d8ab4fd170ee975cbfa432309bdd41cd0fb07c1a58f7d239662e7d36d035db3ce62bc9c50aeb36c0355779a8cf38fac5",
12208
+ "createdAt": 1787468589681,
12209
+ "profile": {
12210
+ "birthDate": "1996-09-09",
12211
+ "birthHourIndex": 5,
12212
+ "gender": "男",
12213
+ "location": null
12214
+ },
12215
+ "samples": [],
12216
+ "records": [
12217
+ {
12218
+ "artId": "bazi",
12219
+ "createdAt": 1,
12220
+ "result": {
12221
+ "dayGZ": "壬申"
12222
+ }
12223
+ }
12224
+ ]
12057
12225
  }
12058
12226
  ]
12059
12227
  }
@@ -3,15 +3,38 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
 
5
5
  // 轻量 .env 加载(无第三方依赖)
6
+ // 支持:# 注释行、空行、KEY=value(可带双引号/单引号,内嵌转义 \n \t \" \\ 与注释剥离)
6
7
  (() => {
7
8
  try {
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
  const envFile = path.join(__dirname, '..', '.env');
10
11
  const content = fs.readFileSync(envFile, 'utf-8');
11
- content.split('\n').forEach(line => {
12
- const m = /^\s*([A-Z_]+)\s*=\s*(.+)\s*$/.exec(line);
13
- if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
14
- });
12
+ for (const rawLine of content.split(/\r?\n/)) {
13
+ const line = rawLine.trim();
14
+ if (!line || line.startsWith('#')) continue; // 空行 / 注释行
15
+ const eq = line.indexOf('=');
16
+ if (eq <= 0) continue; // 无等号 → 跳过
17
+ const key = line.slice(0, eq).trim();
18
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; // 非法键名 → 跳过
19
+ if (process.env[key] !== undefined) continue; // 环境变量优先,不覆盖
20
+ let val = line.slice(eq + 1).trim();
21
+ // 行内注释剥离(引号外的 # 视为注释起点)
22
+ if (!val.startsWith('"') && !val.startsWith("'")) {
23
+ const hash = val.indexOf(' #');
24
+ if (hash >= 0) val = val.slice(0, hash);
25
+ }
26
+ // 引号包裹 → 整体剥引号 + 处理转义(仅双引号内做转义展开)
27
+ const q = val.startsWith('"') ? '"' : val.startsWith("'") ? "'" : null;
28
+ if (q && val.endsWith(q) && val.length >= 2) {
29
+ val = val.slice(1, -1);
30
+ if (q === '"') {
31
+ val = val.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
32
+ }
33
+ } else {
34
+ val = val.trim();
35
+ }
36
+ process.env[key] = val;
37
+ }
15
38
  console.log('· 已加载 .env(LLM_PROVIDER=' + (process.env.LLM_PROVIDER || '未配置') + ')');
16
39
  } catch { /* 无 .env 时忽略 */ }
17
40
  })();
@@ -63,6 +63,12 @@ router.post('/interpret', async (req, res) => {
63
63
  if (e.message === 'AI_UNCONFIGURED') {
64
64
  return res.status(503).json({ error: 'AI_UNCONFIGURED', message: '未配置 AI 服务,请设置 LLM_PROVIDER 与对应 Key' });
65
65
  }
66
+ // 透传服务商 HTTP 错误码(401 Key 无效 / 404 模型不存在 / 429 额度限流 / 5xx 故障等),前端给专门引导
67
+ const mapped = mapLlmError(e);
68
+ if (mapped) {
69
+ console.error('[ai/interpret]', e.message);
70
+ return res.status(502).json({ error: mapped.code, message: mapped.message });
71
+ }
66
72
  console.error('[ai/interpret]', e);
67
73
  res.status(502).json({ error: 'AI_FAILED', message: 'AI 解读暂未应机' });
68
74
  }
@@ -141,6 +147,18 @@ router.post('/interpret/stream', async (req, res) => {
141
147
  }
142
148
  return;
143
149
  }
150
+ // 透传服务商 HTTP 错误码,前端给专门引导
151
+ const mapped = mapLlmError(e);
152
+ if (mapped) {
153
+ console.error('[ai/stream]', e.message);
154
+ if (res.headersSent) {
155
+ res.write(`data: ${JSON.stringify({ type: 'error', code: mapped.code, message: mapped.message })}\n\n`);
156
+ res.end();
157
+ } else {
158
+ res.status(502).json({ error: mapped.code, message: mapped.message });
159
+ }
160
+ return;
161
+ }
144
162
  console.error('[ai/stream]', e);
145
163
  if (res.headersSent) {
146
164
  res.write(`data: ${JSON.stringify({ type: 'error', code: 'AI_FAILED', message: 'AI 解读暂未应机' })}\n\n`);
@@ -373,4 +391,23 @@ export function parseSections(text: string): { title: string; content: string }[
373
391
  return [{ title: 'AI 参详', content: cleaned || 'AI 未返回内容' }];
374
392
  }
375
393
 
394
+ // LLM 服务商 HTTP 错误 → 用户可行动的引导码(AI_HTTP_401 等透传给前端)
395
+ export function mapLlmError(e: any): { code: string; message: string } | null {
396
+ const m = /^AI_HTTP_(\d{3})$/.exec(String(e?.message || ''));
397
+ if (!m) return null;
398
+ const status = Number(m[1]);
399
+ switch (status) {
400
+ case 401: return { code: 'AI_HTTP_401', message: 'API Key 无效或已过期——请检查 server/.env 中的 Key 是否正确(guanwei doctor 可自检)' };
401
+ case 403: return { code: 'AI_HTTP_403', message: 'API Key 无权限访问该模型——请检查账号权限或更换模型' };
402
+ case 404: return { code: 'AI_HTTP_404', message: '模型不存在或模型名有误——请检查 .env 中 LLM_*_MODEL 配置' };
403
+ case 402:
404
+ case 429: return { code: 'AI_HTTP_429', message: '请求过于频繁或额度不足——请稍后重试,或检查服务商账户余额' };
405
+ case 500:
406
+ case 502:
407
+ case 503:
408
+ case 504: return { code: 'AI_HTTP_' + status, message: 'AI 服务商暂时不可用——请稍后重试' };
409
+ default: return { code: 'AI_HTTP_' + status, message: 'AI 服务响应异常(HTTP ' + status + ')——请稍后重试' };
410
+ }
411
+ }
412
+
376
413
  export default router;
@@ -1,8 +1,8 @@
1
1
  import { Router } from 'express';
2
- import { tarotCards } from '../data/tarotCards.js';
3
- import { defaultSpreads } from '../data/spreads.js';
4
- import { drawCards, generateInterpretation } from '../utils/tarotEngine.js';
5
- import { analyzeQuestion } from '../utils/semanticAnalyzer.js';
2
+ import { tarotCards } from '../../../shared/core/data/tarotCards.js';
3
+ import { defaultSpreads } from '../../../shared/core/data/spreads.js';
4
+ import { drawCards, generateInterpretation } from '../../../shared/core/engine/tarotEngine.js';
5
+ import { analyzeQuestion } from '../../../shared/core/engine/semanticAnalyzer.js';
6
6
  import type { QuestionCategory, Spread } from '../types/index.js';
7
7
 
8
8
  const router = Router();
@@ -2,11 +2,18 @@
2
2
  import { Router } from 'express';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
+ import crypto from 'crypto';
5
6
  import { fileURLToPath } from 'url';
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const DB_FILE = path.join(__dirname, '..', 'data', 'db.json');
9
10
 
11
+ function scryptAsync(pw: string, salt: Buffer, keylen: number): Promise<Buffer> {
12
+ return new Promise((resolve, reject) => {
13
+ crypto.scrypt(pw, salt, keylen, (err, key) => err ? reject(err) : resolve(key));
14
+ });
15
+ }
16
+
10
17
  interface DbUser {
11
18
  username: string;
12
19
  passHash: string;
@@ -28,11 +35,31 @@ function saveDb(db: Db): void {
28
35
  fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));
29
36
  }
30
37
 
31
- function hash(pw: string): string {
38
+ // 密码哈希:scrypt(带随机盐,防彩虹表/暴力破解)。
39
+ // 格式:scrypt$<saltHex>$<hashHex>;登录时对存量旧哈希(djb2 前缀 'h')做兼容校验,命中即升级为 scrypt。
40
+ const SCRYPT_N = 16384, SCRYPT_R = 8, SCRYPT_P = 1, SCRYPT_KEYLEN = 64;
41
+
42
+ async function hashPassword(pw: string): Promise<string> {
43
+ const salt = crypto.randomBytes(16);
44
+ const key = await scryptAsync(pw, salt, SCRYPT_KEYLEN);
45
+ return 'scrypt$' + salt.toString('hex') + '$' + key.toString('hex');
46
+ }
47
+
48
+ async function verifyPassword(pw: string, stored: string): Promise<boolean> {
49
+ if (!stored) return false;
50
+ if (stored.startsWith('scrypt$')) {
51
+ const parts = stored.split('$');
52
+ if (parts.length !== 3) return false;
53
+ const salt = Buffer.from(parts[1], 'hex');
54
+ const expected = Buffer.from(parts[2], 'hex');
55
+ const key = await scryptAsync(pw, salt, expected.length);
56
+ return key.length === expected.length && crypto.timingSafeEqual(key, expected);
57
+ }
58
+ // 旧格式(djb2 变体):仅做兼容校验,命中后由调用方升级
32
59
  let h = 5381;
33
60
  const s = 'guanwei::' + pw;
34
61
  for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
35
- return 'h' + h.toString(16);
62
+ return 'h' + h.toString(16) === stored;
36
63
  }
37
64
 
38
65
  const router = Router();
@@ -51,7 +78,7 @@ function upsertUser(username: string): DbUser {
51
78
  return user;
52
79
  }
53
80
 
54
- router.post('/register', (req, res) => {
81
+ router.post('/register', async (req, res) => {
55
82
  const { username, password, profile } = req.body;
56
83
  const name = String(username || '').trim();
57
84
  if (name.length < 2) return res.status(400).json({ error: '名号至少二字' });
@@ -61,25 +88,33 @@ router.post('/register', (req, res) => {
61
88
  if (existing) {
62
89
  // 云同步自动建档的占位账号(无密语)可被正式注册升级
63
90
  if (!existing.passHash) {
64
- existing.passHash = hash(String(password));
91
+ existing.passHash = await hashPassword(String(password));
65
92
  if (profile) existing.profile = { ...existing.profile, ...profile };
66
93
  saveDb(db);
67
94
  return res.json({ ok: true, upgraded: true, user: { username: existing.username, profile: existing.profile, samples: existing.samples } });
68
95
  }
69
96
  return res.status(400).json({ error: '此名号已有人用' });
70
97
  }
71
- const user: DbUser = { username: name, passHash: hash(String(password)), createdAt: Date.now(), profile: profile || {}, samples: [], records: [] };
98
+ const user: DbUser = { username: name, passHash: await hashPassword(String(password)), createdAt: Date.now(), profile: profile || {}, samples: [], records: [] };
72
99
  db.users.push(user);
73
100
  saveDb(db);
74
101
  res.json({ ok: true, user: { username: user.username, profile: user.profile, samples: user.samples } });
75
102
  });
76
103
 
77
104
  // 登录
78
- router.post('/login', (req, res) => {
105
+ router.post('/login', async (req, res) => {
79
106
  const { username, password } = req.body;
80
107
  const db = loadDb();
81
108
  const user = db.users.find(u => u.username === String(username || '').trim());
82
- if (!user || user.passHash !== hash(String(password || ''))) return res.status(401).json({ error: '名号或密语未合' });
109
+ if (!user) return res.status(401).json({ error: '名号或密语未合' });
110
+ const ok = await verifyPassword(String(password || ''), user.passHash);
111
+ if (!ok) return res.status(401).json({ error: '名号或密语未合' });
112
+ // 旧哈希命中 → 升级为 scrypt 存储
113
+ if (user.passHash && !user.passHash.startsWith('scrypt$')) {
114
+ user.passHash = await hashPassword(String(password));
115
+ saveDb(db);
116
+ console.log(`[users] 密码哈希已升级为 scrypt: ${user.username}`);
117
+ }
83
118
  res.json({ ok: true, user: { username: user.username, profile: user.profile, samples: user.samples } });
84
119
  });
85
120
 
@@ -30,11 +30,7 @@ export function getDb(): DatabaseSync {
30
30
  report_quality TEXT,
31
31
  status TEXT NOT NULL DEFAULT 'divined',
32
32
  created_at INTEGER NOT NULL,
33
- updated_at INTEGER NOT NULL,
34
- plan_tier TEXT NOT NULL DEFAULT 'free',
35
- quota_key TEXT,
36
- quota_consumed INTEGER NOT NULL DEFAULT 1,
37
- billing_meta TEXT
33
+ updated_at INTEGER NOT NULL
38
34
  );
39
35
  CREATE INDEX IF NOT EXISTS idx_div_user_time ON divinations(username, created_at DESC);
40
36
 
@@ -1,4 +1,4 @@
1
- // 提示词构建:9 术 agent + Skills 编排 + 两套报告 Schema 模板 + 档案注入
1
+ // 提示词构建:9 术角色 persona(单轮注入,多 Agent 编排规划中)+ 两套报告 Schema 模板 + 档案注入
2
2
  import type { ChatMessage } from './llmProvider';
3
3
  import { agentOf } from './skills';
4
4
  import { chartBrief } from './chartBrief';
@@ -89,7 +89,7 @@ function lifeEventsNote(profile?: unknown): string {
89
89
  return '【命主已知人生经历 · 解读校准】以下为命主本人确认的人生事件,解读必须与此相符:\n' + clean.join('\n') + '\n要求:① 凡解读涉及这些年份(或与之相邻的大运流年)时,必须呼应对应事件(如该年患病手术,则论健康/流年时须呼应);② 严禁写出与已知经历相矛盾的内容(如已知 2024 年重大健康事件,就不得断言该年平安无事);③ 已知经历未覆盖的年份正常论命,不得为了呼应而编造具体事件细节。';
90
90
  }
91
91
 
92
- // Skills 编排版:9 术 agent + 档案 + 排盘结果 → 结构化解读报告
92
+ // 角色化解读版:9 术 persona(单轮注入)+ 档案 + 排盘结果 → 结构化解读报告
93
93
  export function buildReportMessages(
94
94
  artId: string,
95
95
  question: string,
@@ -1,104 +1,2 @@
1
- export interface TarotCard {
2
- id: number;
3
- name: string;
4
- nameEn: string;
5
- arcana: 'major' | 'minor';
6
- suit?: 'wands' | 'cups' | 'swords' | 'pentacles';
7
- number?: number;
8
- keywords: string[];
9
- meanings: {
10
- upright: string;
11
- reversed: string;
12
- };
13
- deepMeaning: {
14
- coreTheme: string;
15
- psychological: string;
16
- spiritual: string;
17
- shadow: string;
18
- lifeAreas: {
19
- love: string;
20
- career: string;
21
- wealth: string;
22
- health: string;
23
- growth: string;
24
- };
25
- advice: string;
26
- warning: string;
27
- };
28
- imageSymbols: string[];
29
- story: string;
30
- astrology: {
31
- zodiac?: string;
32
- planet?: string;
33
- element?: string;
34
- };
35
- kabbalah: {
36
- path?: number;
37
- sephirah?: string;
38
- };
39
- alchemy: {
40
- stage?: string;
41
- element?: string;
42
- };
43
- numerology: {
44
- number: number;
45
- meaning: string;
46
- };
47
- image: string;
48
- }
49
-
50
- export interface SpreadPosition {
51
- id: number;
52
- name: string;
53
- description: string;
54
- x: number;
55
- y: number;
56
- }
57
-
58
- export interface Spread {
59
- id: string;
60
- name: string;
61
- description: string;
62
- positions: SpreadPosition[];
63
- category: 'basic' | 'advanced' | 'custom';
64
- scene?: string[];
65
- isCustom?: boolean;
66
- }
67
-
68
- export interface DrawnCard {
69
- cardId: number;
70
- isReversed: boolean;
71
- positionId: number;
72
- }
73
-
74
- export interface DivinationRecord {
75
- id: string;
76
- type: 'tarot' | 'combo' | 'ziwei' | 'bazi' | 'astrology';
77
- question: string;
78
- category: string;
79
- spreadId?: string;
80
- cards?: DrawnCard[];
81
- comboMethod?: string;
82
- comboResult?: string;
83
- interpretation: string;
84
- timestamp: number;
85
- }
86
-
87
- export interface ComboResult {
88
- method: 'xiaoliuren' | 'daliuren' | 'liuyao' | 'meihua' | 'qimen';
89
- methodName: string;
90
- result: string;
91
- detail: string;
92
- relationToTarot: string;
93
- }
94
-
95
- export type QuestionCategory = 'love' | 'career' | 'wealth' | 'health' | 'study' | 'family' | 'friendship' | 'spiritual' | 'travel' | 'general';
96
-
97
- export interface TarotReading {
98
- question: string;
99
- category: QuestionCategory;
100
- spreadId: string;
101
- cards: DrawnCard[];
102
- interpretation: string;
103
- comboResult?: ComboResult;
104
- }
1
+ // 类型桥(单一副本):全部类型收敛于 shared/core/types.ts,此处仅 re-export 保持旧导入路径兼容
2
+ export * from '../../../shared/core/types.js';
@@ -1,15 +1,9 @@
1
1
  // 生活化场景语料库
2
2
  // 根据场景提供具体的日常落地建议
3
3
 
4
- import type { Scene, Emotion } from '@/utils/semanticAnalyzer';
4
+ import type { Scene, LifeContext } from '../types.js';
5
5
 
6
- export interface LifeContext {
7
- scenes: string[];
8
- actions: string[];
9
- warnings: string[];
10
- timeline: { short: string; mid: string; long: string };
11
- emotionAdvice: Record<Emotion, string>;
12
- }
6
+ export type { LifeContext };
13
7
 
14
8
  export const LIFE_CONTEXTS: Record<Scene, LifeContext> = {
15
9
  love: {
@@ -1,4 +1,4 @@
1
- import type { Spread } from '@/types';
1
+ import type { Spread } from '../types.js';
2
2
 
3
3
  export const defaultSpreads: Spread[] = [
4
4
  // ===== 极简牌阵 =====
@@ -1,4 +1,4 @@
1
- import type { TarotCard } from '@/types';
1
+ import type { TarotCard } from '../types.js';
2
2
 
3
3
  export const tarotCards: TarotCard[] = [
4
4
  // ===== 大阿卡纳 Major Arcana =====
@@ -104,7 +104,6 @@ export function baziCalc(input: BaziInput): BaziResult {
104
104
  const nayin = NAYIN[Math.floor(jiaziIndex(y) / 2) % 30];
105
105
  const support = wxCount[dayGanWx] + wxCount[WX_ORDER[(WX_ORDER.indexOf(dayGanWx) + 4) % 5]];
106
106
  const drain = gzList.length * 2 - support;
107
- const strength = support > drain ? '身强' : (support < drain ? '身弱' : '中和');
108
107
 
109
108
  // ─── 7. 地支藏干 + 藏干十神 + 加权五行 ───
110
109
  const canggan = gzList.map(gz => ({
@@ -16,7 +16,7 @@ export function daysSince(y: number, m: number, d: number): number {
16
16
 
17
17
  /* ---------- 节气 ---------- */
18
18
  // 近似表取法(奇门定遁 / 展示用):
19
- export function getJieqiApproxName(y: number, m: number, d: number): string {
19
+ export function getJieqiApproxName(_y: number, m: number, d: number): string {
20
20
  const v = m * 100 + d;
21
21
  let name = '大雪';
22
22
  for (let i = JIEQI_APPROX.length - 1; i >= 0; i--) {