guanwei 1.2.3 → 1.2.4

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guanwei",
3
3
  "private": false,
4
- "version": "1.2.3",
4
+ "version": "1.2.4",
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": ">=22.5.0"
94
+ "node": ">=22.13.0"
95
95
  }
96
96
  }
@@ -4,7 +4,7 @@
4
4
  # 用法:./scripts/release.sh 1.1.2 仅 git 发版(tag 触发 CI/Release/镜像)
5
5
  # ./scripts/release.sh 1.1.2 --npm 额外发布 npm 包(需先配置 npm 登录)
6
6
  # 要求:工作区无未提交改动;CHANGELOG.md 已写好本次版本条目
7
- # 详见(本地文档):docs/RELEASE-SOP.md
7
+ # 详见(本地文档,不入库):docs/RELEASE-SOP.md(发版 SOP 详情/踩坑记录)
8
8
  # ============================================================
9
9
  set -euo pipefail
10
10
  cd "$(dirname "$0")/.."
@@ -6278,57 +6278,87 @@
6278
6278
  "records": []
6279
6279
  },
6280
6280
  {
6281
- "username": "victim_alice",
6282
- "passHash": "scrypt$dab8303d062c903d08876f726a8ed5ae$fc0a04e8b0ed6f4a91198ab3ac41b8036310ce7efd2a9a233d4035e2c3413e237b3d2f831596953c08b3571048d99b5d09511c40d951a70c85e9e9fe3649a4d4",
6283
- "createdAt": 1787972917849,
6284
- "profile": {},
6281
+ "username": "联调2179",
6282
+ "passHash": "scrypt$a51ae5b7ef7ef2aefc5dd7c51adfaa2d$fa9362e9b80497e5955137670334e733bc9c13509e10bde8ac259f5f786ef558d4ebf14c7406a1cb1022a613d87d36f66171b6e1e8c80c4c0cb0e1e7ec0a45c2",
6283
+ "createdAt": 1787974772299,
6284
+ "profile": {
6285
+ "birthDate": "1996-09-09",
6286
+ "birthHourIndex": 5,
6287
+ "gender": "男",
6288
+ "location": null
6289
+ },
6285
6290
  "samples": [],
6286
6291
  "records": [
6287
6292
  {
6288
- "pwned": true
6293
+ "artId": "bazi",
6294
+ "createdAt": 1,
6295
+ "result": {
6296
+ "dayGZ": "壬申"
6297
+ }
6289
6298
  }
6290
- ]
6299
+ ],
6300
+ "token": "55efbbaeaa550c9966f04dda52f998095e463738eb645048c2cdd49701a2627e"
6301
+ },
6302
+ {
6303
+ "username": "鉴权2490",
6304
+ "passHash": "scrypt$bee1f62b05efb97d0af677b90f3569e0$641c60c1aed8e610ce18365ca7d894ca9570e34736695c041bf53be1f7216e7ddadce34fc47291512d73106ca4c8ce1fccfb02be4389cbcbfaa43226efeab9a8",
6305
+ "createdAt": 1787974772532,
6306
+ "profile": {
6307
+ "birthDate": "1999-01-01"
6308
+ },
6309
+ "samples": [],
6310
+ "records": [],
6311
+ "token": "a8ad6a5ebb99a49abc167b0473daf56293ec89b0071a24ed58b37651ab864bf4"
6312
+ },
6313
+ {
6314
+ "username": "他人2562",
6315
+ "passHash": "scrypt$c277c3c2d4cc8a620bba42fc8798bc31$47edb65dae757c4b46d42b18fa3c444fc83f430e69b76dcb5981bf940107ced5652853f401106830a224615cf1250514e57413b41cabf9435af116c1c66d7d29",
6316
+ "createdAt": 1787974772603,
6317
+ "profile": {},
6318
+ "samples": [],
6319
+ "records": [],
6320
+ "token": "4cc7aa02a8fc745eefc12a6027e73348874b5aba79e68137b807108c86198b41"
6291
6321
  },
6292
6322
  {
6293
- "username": "他人1016",
6294
- "passHash": "scrypt$bdcebc1fe749ca94b9a569faabdf0b8e$1c608d2369e5e23e7f240f7a370eedb9eca6c3a2f2d46c082ee8d8b43037ddb5b1c01bac186dd5cffa9b76e4691a4d967305e640402bfd5e4d4d6e6cb8811179",
6295
- "createdAt": 1787974051050,
6323
+ "username": "登鉴2612",
6324
+ "passHash": "scrypt$33289372a690e4aff8ce7de07c568905$32b21c770d232278481f71fbade740c225e69598f7d9a945e630c5b2d1aee6d06fe1d04b1d8b7aee51d17e102df0053d4083fc844a4b231b31e540524d37d4cc",
6325
+ "createdAt": 1787974772654,
6296
6326
  "profile": {},
6297
6327
  "samples": [],
6298
6328
  "records": [],
6299
- "token": "ac8919427c76fda2a4678c0fce3dd875b9a4ef6fc4fd5d80527241aff64d220f"
6329
+ "token": "3c6f698637cd4f3f40c7e9d4e44768b1f11127c9efda7358abed5f154ea66ef1"
6300
6330
  },
6301
6331
  {
6302
- "username": "他人5794",
6303
- "passHash": "scrypt$b557d9b6bfe78486f90ac1586dc5a654$14d844f0fc1869dff4be6d1ef0fd1e898f26eb99bbfef9c63adec794b2ef41a6eaef20043089293c8470d721c676722cce88ca285318bf04f5967b4f239114aa",
6304
- "createdAt": 1787974055827,
6332
+ "username": "占位2705",
6333
+ "passHash": "scrypt$15ad8f4b87fec28b3eee34257285fc84$c96d3c49363c3ada5a17ed8060e25c7adcc8f4123d38e44677cc3e0a7fa6a5501fa214367a31b96ddfa462c6ef9af1b6d4bd096add513db153d73bd06fdee43f",
6334
+ "createdAt": 1787974772709,
6305
6335
  "profile": {},
6306
6336
  "samples": [],
6307
6337
  "records": [],
6308
- "token": "692a6dfbd88d65eb17b4d18f985e162547b1effed94795e789c6201b2ae2b474"
6338
+ "token": "65a178f8a72ce1c416c2b438f1a8e43cbf92414384a50fe455fb2e2598c06a8e"
6309
6339
  },
6310
6340
  {
6311
- "username": "他人0715",
6312
- "passHash": "scrypt$f546c55c324a335d629b767f4c11031a$f654778159efbed2f38a6c78d6d64698f306057725c4ae1b5ce7536e7f7597dcdedf8c0a5381b86fb1a37fcea3260cfc3096f9984fd971b6105f3f541b08c20a",
6313
- "createdAt": 1787974060746,
6341
+ "username": "甲2793",
6342
+ "passHash": "scrypt$d7292baa21ca8284048bd112f3f22ed2$488c76756d9d27ca895fe122076d2dfb63421542f7b4c801b6d7c1880758bee7ea2f167e576bbb1d4791735c1fe8fbffb35bad503c2d062a3ae942ce508ef356",
6343
+ "createdAt": 1787974772837,
6314
6344
  "profile": {},
6315
6345
  "samples": [],
6316
6346
  "records": [],
6317
- "token": "b9492097127117d3609eda7d1d77d342f9381abbeb765310b10ae51f87ef4686"
6347
+ "token": "fd1624569025003dd839884a49d8422adf8c37ce14bcfef385873d8c94e9de12"
6318
6348
  },
6319
6349
  {
6320
- "username": "他人4341",
6321
- "passHash": "scrypt$117b14dacf3d50ebcb1a4ab8b26faadd$5bc33e7ecd69351c9379850a1ea25b2a8d77e588e4b75b32763cb21f7f5365dc9cd5f19b6bcd40850fa4fc691e6ed05bdd32723e8f55956047a8cb351b7daec0",
6322
- "createdAt": 1787974074375,
6350
+ "username": "乙2793",
6351
+ "passHash": "scrypt$241e46222657b2758766400c314c273d$5ee351f2e508dcfb2865b8cc641d063ae04bbb10e94b5d86a958a7ccbbc51d8960d6f5825b327f941f6f0d691ba43bc705a0042afe44fe83e50bc37563373739",
6352
+ "createdAt": 1787974772889,
6323
6353
  "profile": {},
6324
6354
  "samples": [],
6325
6355
  "records": [],
6326
- "token": "9a2a792a3c0d786340aff3ef485398ffcc495e5d08cd8b80e8fe47bf0385bd6e"
6356
+ "token": "58ce37d6f2e75f23d7307663b26afa7f459cfdf80744d9f43c4a5f83fefb0a85"
6327
6357
  },
6328
6358
  {
6329
- "username": "联调0222",
6330
- "passHash": "scrypt$1429fe608a08f39536064900c6ef7dab$850dec8986bd9695a60d86d08f737d3064342c18a210b7c8e1daae155de02e2bd0309c7f1c09c290da62bbd0017121b5f39ae17e9285fb28a4ff544e1ca9b4ef",
6331
- "createdAt": 1787974080284,
6359
+ "username": "联调1815",
6360
+ "passHash": "scrypt$7c8e52d78fc5c72c15dd5fd1376cad2f$302ee9e4004dc6bc4de15cc38ca5cc091b9133e7fd0880c2b6aa5a7f2d63bbf2846be2b5ac268f3e76f36fde5aa31e3418be5a3101d6d8d9499b99aae62e3889",
6361
+ "createdAt": 1787974791902,
6332
6362
  "profile": {
6333
6363
  "birthDate": "1996-09-09",
6334
6364
  "birthHourIndex": 5,
@@ -6345,87 +6375,63 @@
6345
6375
  }
6346
6376
  }
6347
6377
  ],
6348
- "token": "9be12d7b521c3af14f862e2fb6b1b6947a6ab5a078b1f826e8d4dee134f769e9"
6378
+ "token": "873d8e6fb689c0a759eb24489302d902d9c582cf63b6eed253efcb7dd289c7eb"
6349
6379
  },
6350
6380
  {
6351
- "username": "鉴权0445",
6352
- "passHash": "scrypt$097d662378cfaa40363d658c29f5993f$b954141aeb4405e3964a78e29451050bc322cde400de352bb40eb784a1102638d7f8ccbf6b7bfd10373ec7497b0a30d182c80cc4da1e6715d90454908924772f",
6353
- "createdAt": 1787974080493,
6381
+ "username": "鉴权2044",
6382
+ "passHash": "scrypt$a7d28897ecebf00429e38530a1be3ddb$1d05f31b71b52ed3233ebbb1d9c17ea189faf66cdcbed57969a79131efe75f744fd48dde8c807740e200e7f6738aacd0fc142c3377de02cafde91381b6c99a2a",
6383
+ "createdAt": 1787974792091,
6354
6384
  "profile": {
6355
6385
  "birthDate": "1999-01-01"
6356
6386
  },
6357
6387
  "samples": [],
6358
6388
  "records": [],
6359
- "token": "3c98d1825580f48484f76f6260fad2f3153bd945a2b7928e6ae989eded00c0cb"
6389
+ "token": "2d9fe3339c4a239c03ec747c5b6a2eb526bec03d4204c692421ede018dc92757"
6360
6390
  },
6361
6391
  {
6362
- "username": "他人0518",
6363
- "passHash": "scrypt$27918731e28d947f40646a69659738d1$78dbf7ea56f0f476bfeb3d0acb74746add05f46221e1416bce4c026b8f39cfc98738ca821908e6741801616c0aba6caf83574f1339253d0b49ed0d7e1d497794",
6364
- "createdAt": 1787974080561,
6392
+ "username": "他人2114",
6393
+ "passHash": "scrypt$ab67c3c80a2c62c559cbe399d0643c63$fa0f39676d8e12ac0e613c00b100564c55b3153bc074cef3e665773b62f23bc6d6d67873543563fac68d59b490055b309e1d371a33c74e52ea13a0d150928b84",
6394
+ "createdAt": 1787974792156,
6365
6395
  "profile": {},
6366
6396
  "samples": [],
6367
6397
  "records": [],
6368
- "token": "b2acd2d401dd6684e9b39592b6e8c0504da1da48bd17abd2a0b72c90fc4c2cc4"
6398
+ "token": "cd08fe94f891fe49b43106684b342cfba169ffd920bf9f72558842a91bb86b67"
6369
6399
  },
6370
6400
  {
6371
- "username": "登鉴0569",
6372
- "passHash": "scrypt$0ce756c9179b4704763ef5fca492f58e$1a2086c2956f89ab356b6817ac4591eccc05da5ac153df5f41292918ff58abcab7c60ae3e63d67ad4a29bb2503f474675d1efa06781e4abd6cc523b10f205cdf",
6373
- "createdAt": 1787974080616,
6401
+ "username": "登鉴2165",
6402
+ "passHash": "scrypt$8fd7b88e6f63f16bd355a799c1fdb507$4d82632c56bf3657bfb1395726d3014272a8dbd61206010f809533b4561a9727e5e4041b6a076c7ae0f70eda21ff4b63bc415a92bbad29538a974e0dc9aa029d",
6403
+ "createdAt": 1787974792209,
6374
6404
  "profile": {},
6375
6405
  "samples": [],
6376
6406
  "records": [],
6377
- "token": "2b9c18f9e9e58999d0196144fe2e26d3ff829db747036fa6dc0f5186292d8d65"
6378
- },
6379
- {
6380
- "username": "联调0274",
6381
- "passHash": "scrypt$1532e76bac74844794f8ca1b035a403b$d905f04f65150570ede4fc09dd51744ec0d342588409f5a7f16c368363ef82c0f03c9223c1f9455aa1edc4d452b1495a0d00769be9177a6ac99133f3aa4cb733",
6382
- "createdAt": 1787974330369,
6383
- "profile": {
6384
- "birthDate": "1996-09-09",
6385
- "birthHourIndex": 5,
6386
- "gender": "男",
6387
- "location": null
6388
- },
6389
- "samples": [],
6390
- "records": [
6391
- {
6392
- "artId": "bazi",
6393
- "createdAt": 1,
6394
- "result": {
6395
- "dayGZ": "壬申"
6396
- }
6397
- }
6398
- ],
6399
- "token": "d58ee529556e21e862126df09d6e9865acbd328eb2acfb2bfcb9d9abb1d01e4e"
6407
+ "token": "4e69bfef54a7c816bf22357687720d112062d7cda9e0a3e483668a5d0ffae04b"
6400
6408
  },
6401
6409
  {
6402
- "username": "鉴权0536",
6403
- "passHash": "scrypt$c1943511dd7934aab8dbfeaa782a23b4$9a496aaa0356dbd11b03b1154251d897b0fffa29996ad8e08c3af43abdf4a59259ec840263bc8e119c8aaa7d91c55dc03ac2ae89d50726e57ccde82b014d2c51",
6404
- "createdAt": 1787974330586,
6405
- "profile": {
6406
- "birthDate": "1999-01-01"
6407
- },
6410
+ "username": "占位2262",
6411
+ "passHash": "scrypt$9b030e5b8d1c475f3700534c0cea8bcc$5dcff5dd3abe675650bc5c8d7a0bb7402893cb6a7b20582a399dbbb39b5afea61c12525ce6beb080acf26dccb801f707d6c48434689cad680fcc2c37932cecfc",
6412
+ "createdAt": 1787974792265,
6413
+ "profile": {},
6408
6414
  "samples": [],
6409
6415
  "records": [],
6410
- "token": "6f6150f1383558c8e1e411dd5367830aa3a4bb62366f81629c3b5773627cc3dd"
6416
+ "token": "06b5d6f063dc265a4ab0181dbeeabfaedd815bcf44a481c5817a84e3403162bd"
6411
6417
  },
6412
6418
  {
6413
- "username": "他人0611",
6414
- "passHash": "scrypt$1629a44ccf691673b8402b08a76af528$d6d3ee3d5f734617a29fb1e455280a4c1126b6dc7e606b7250230a6aeecab145e483e108f8d426fa75be69015907b06fb5603a1ed1754b454a9ca2df36d480ee",
6415
- "createdAt": 1787974330660,
6419
+ "username": "甲2362",
6420
+ "passHash": "scrypt$e0a1334c2e39079fe996d50550b296ac$22b689d05605618743ed4de49781e4920c813fe9773c66b1ffcfd841202e5eea8654bca1cb0722fdd4121abe997fe0d3b2ce6ea52b1b72f616ae4373ca92be42",
6421
+ "createdAt": 1787974792397,
6416
6422
  "profile": {},
6417
6423
  "samples": [],
6418
6424
  "records": [],
6419
- "token": "d605ca3807bc4bae9ba7b5db0e384a61523a4f820ee41ce793bc6f68641f1197"
6425
+ "token": "dcb5ab3832984ddb12f1e4c0934a6ec109677cac4c744ee0f43877c6265ff0ce"
6420
6426
  },
6421
6427
  {
6422
- "username": "登鉴0667",
6423
- "passHash": "scrypt$f076ee11a4603added931cc7046293f0$9eebf8f786d377b4f4912ab4c71be32801eca5cab2ee8b3af5f89f7889418a224d3a87d953fce780cc8ede45461eecfa9288db40d3fdab91ca5456ba4fa98dca",
6424
- "createdAt": 1787974330711,
6428
+ "username": "乙2362",
6429
+ "passHash": "scrypt$65bc82b0a841f543f17cdd4db4f06419$5e7609d3c5c1dbc632489007cab5c5aaf1400652b61cb45aa20876ebb1f8274bca0cde13a11e9670beab60613a5676cc7afc82d0c31043df85700e41efe38335",
6430
+ "createdAt": 1787974792452,
6425
6431
  "profile": {},
6426
6432
  "samples": [],
6427
6433
  "records": [],
6428
- "token": "9e5b4699e7c6a533810aef647764868e24abdff869647b87aff6c8263dbd8711"
6434
+ "token": "a4c67d7d395086f836811508e9199d007487ad24f3f92db0492163bcef07ed44"
6429
6435
  }
6430
6436
  ]
6431
6437
  }
Binary file
@@ -53,6 +53,24 @@ const PORT = process.env.PORT || 3018;
53
53
  app.use(cors());
54
54
  app.use(express.json());
55
55
 
56
+ // ─── AI 接口限流(防 BYOK Key 被刷爆):per-IP 令牌桶 ───
57
+ const AI_RATE_LIMIT = { windowMs: 60_000, max: 30 }; // 每分钟 30 次(本地单用户无感,公网防刷)
58
+ const aiHits = new Map<string, { count: number; resetAt: number }>();
59
+ app.use('/api/ai', (req, res, next) => {
60
+ const ip = req.ip || req.socket.remoteAddress || 'unknown';
61
+ const now = Date.now();
62
+ const rec = aiHits.get(ip);
63
+ if (!rec || now > rec.resetAt) {
64
+ aiHits.set(ip, { count: 1, resetAt: now + AI_RATE_LIMIT.windowMs });
65
+ return next();
66
+ }
67
+ rec.count++;
68
+ if (rec.count > AI_RATE_LIMIT.max) {
69
+ return res.status(429).json({ error: 'AI_RATE_LIMITED', message: '请求过于频繁,请稍后再试' });
70
+ }
71
+ next();
72
+ });
73
+
56
74
  // 请求日志(联调排查用)
57
75
  app.use((req, res, next) => {
58
76
  res.on('finish', () => {
@@ -1,10 +1,28 @@
1
1
  // AI 解读路由:/api/ai/interpret(非流式)+ /api/ai/interpret/stream(SSE)
2
2
  import { Router } from 'express';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import crypto from 'crypto';
6
+ import { fileURLToPath } from 'url';
3
7
  import { buildMessages, buildReportMessages, buildStep1Messages, buildStep2Messages } from '../services/promptBuilder.js';
4
8
  import { chatOnce, chatStream, activeProvider, providerStatus, lastFinishReason } from '../services/llmProvider.js';
5
9
  import { getDivination, attachReport, markAiFailed } from '../services/divineStore.js';
6
10
  import { verifyRelatives, fixFamilyRelatives } from '../services/relativesCheck.js';
7
11
 
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // 请求 token → 用户名(与 divine.ts 同策略:有 token 以 token 为准,防 body 自报他人名)
15
+ function authedUsername(req: any): string | null {
16
+ const tk = String(req.headers['x-guanwei-token'] || '');
17
+ if (!tk) return null;
18
+ try {
19
+ const db = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'db.json'), 'utf-8'));
20
+ const user = db.users.find((u: any) => u.token && u.token.length === tk.length && crypto.timingSafeEqual(Buffer.from(u.token), Buffer.from(tk)));
21
+ return user ? user.username : null;
22
+ } catch { return null; }
23
+ }
24
+
25
+ // 非流式:token 优先归属
8
26
  const router = Router();
9
27
 
10
28
  // Provider 健康状态
@@ -23,7 +41,8 @@ router.post('/interpret', async (req, res) => {
23
41
  if (divineId) {
24
42
  const rec = getDivination(divineId);
25
43
  if (!rec) return res.status(400).json({ error: 'DIVINE_NOT_FOUND', message: '起占记录不存在,请重新起占' });
26
- if (username && rec.username !== username) return res.status(403).json({ error: 'FORBIDDEN' });
44
+ const authed = authedUsername(req);
45
+ if (authed ? authed !== rec.username : (username && rec.username !== username)) return res.status(403).json({ error: 'FORBIDDEN' });
27
46
  resultRaw = rec.resultRaw;
28
47
  recProfile = rec.profile;
29
48
  } else {
@@ -87,7 +106,8 @@ router.post('/interpret/stream', async (req, res) => {
87
106
  res.status(400).json({ error: 'DIVINE_NOT_FOUND', message: '起占记录不存在,请重新起占' });
88
107
  return;
89
108
  }
90
- if (username && rec.username !== username) {
109
+ const authed = authedUsername(req);
110
+ if (authed ? authed !== rec.username : (username && rec.username !== username)) {
91
111
  res.status(403).json({ error: 'FORBIDDEN' });
92
112
  return;
93
113
  }
@@ -13,6 +13,7 @@ import { allTarotSpreads } from '../../../shared/core/data/tarotSpreads.js';
13
13
  import { createDivination, listDivinations, getDivination, deleteDivination } from '../services/divineStore.js';
14
14
  import fs from 'fs';
15
15
  import path from 'path';
16
+ import crypto from 'crypto';
16
17
  import { fileURLToPath } from 'url';
17
18
 
18
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -21,24 +22,42 @@ const DB_FILE = path.join(__dirname, '..', 'data', 'db.json');
21
22
  const router = Router();
22
23
  const MINGPAN_ARTS = ['bazi', 'ziwei', 'astrology'];
23
24
 
24
- // 宽松建档:前端本地注册的用户在此同步建档(与 users.ts upsert 同策略)
25
+ // 宽松建档:前端本地注册的用户在此同步建档(与 users.ts upsert 同策略;建档即发 claimToken 防抢占)
25
26
  function ensureUser(username: string): boolean {
26
27
  if (!username || typeof username !== 'string') return false;
27
28
  try {
28
29
  const db = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8'));
29
- if (db.users.some((u: any) => u.username === username)) return true;
30
- db.users.push({ username, passHash: '', createdAt: Date.now(), profile: {}, samples: [], records: [] });
30
+ const exist = db.users.find((u: any) => u.username === username);
31
+ if (exist) return true;
32
+ const token = crypto.randomBytes(32).toString('hex');
33
+ db.users.push({ username, passHash: '', createdAt: Date.now(), profile: {}, samples: [], records: [], token });
31
34
  fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));
32
35
  console.log('[divine] 自动建档:', username);
33
36
  return true;
34
- } catch { return false; }
37
+ } catch (e: any) {
38
+ console.error('[divine] ensureUser 异常:', e?.message || e);
39
+ return false;
40
+ }
41
+ }
42
+
43
+ // 请求携带的 token → 对应用户名(有 token 则以其为准,堵「query/body 自报 username」越权)
44
+ function authedUsername(req: any): string | null {
45
+ const tk = String(req.headers['x-guanwei-token'] || '');
46
+ if (!tk) return null;
47
+ try {
48
+ const db = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8'));
49
+ const user = db.users.find((u: any) => u.token && u.token.length === tk.length && crypto.timingSafeEqual(Buffer.from(u.token), Buffer.from(tk)));
50
+ return user ? user.username : null;
51
+ } catch { return null; }
35
52
  }
36
53
 
37
54
  // POST /api/divine —— 起占入库
38
55
  router.post('/', (req, res) => {
39
56
  const { artId, inputs, profile, question, username, profileId } = req.body || {};
57
+ const authed = authedUsername(req);
58
+ const owner = authed || String(username || ''); // 有 token 以 token 为准,防自报他人名
40
59
  // 游客不允许
41
- if (!ensureUser(username)) {
60
+ if (!ensureUser(owner)) {
42
61
  return res.status(401).json({ error: 'UNAUTHORIZED', message: '请先入馆(登录)再起占' });
43
62
  }
44
63
  if (!artId || !inputs) return res.status(400).json({ error: '缺少必要参数' });
@@ -124,8 +143,11 @@ router.get('/', (req, res) => {
124
143
  router.get('/:id', (req, res) => {
125
144
  const rec = getDivination(req.params.id);
126
145
  if (!rec) return res.status(404).json({ error: 'DIVINE_NOT_FOUND' });
127
- // 归属校验:query username 必须与记录一致
128
- if (req.query.username !== rec.username) return res.status(403).json({ error: 'FORBIDDEN' });
146
+ // 归属校验:token 优先;无 token 时 query username 必须与记录一致(向后兼容本地旧前端)
147
+ const authed = authedUsername(req);
148
+ if (authed ? authed !== rec.username : String(req.query.username || '') !== rec.username) {
149
+ return res.status(403).json({ error: 'FORBIDDEN' });
150
+ }
129
151
  res.json({ ...rec, resultRaw: rec.resultRaw, display: rec.display, report: rec.report || null });
130
152
  });
131
153
 
@@ -84,7 +84,7 @@ function upsertUser(username: string): DbUser {
84
84
  const db = loadDb();
85
85
  let user = db.users.find((x: any) => x.username === username);
86
86
  if (!user) {
87
- user = { username, passHash: '', createdAt: Date.now(), profile: {}, samples: [], records: [] };
87
+ user = { username, passHash: '', createdAt: Date.now(), profile: {}, samples: [], records: [], token: newToken() };
88
88
  db.users.push(user);
89
89
  saveDb(db);
90
90
  console.log(`[users] 自动建档: ${username}`);
@@ -100,8 +100,12 @@ router.post('/register', async (req, res) => {
100
100
  const db = loadDb();
101
101
  const existing = db.users.find(u => u.username === name);
102
102
  if (existing) {
103
- // 云同步自动建档的占位账号(无密语)可被正式注册升级
103
+ // 占位账号(自动建档、无密语):升级须持有建档时发放的 claimToken,防任意抢占
104
104
  if (!existing.passHash) {
105
+ const claimToken = String(req.body.claimToken || '');
106
+ if (!tokenMatches(existing, claimToken)) {
107
+ return res.status(409).json({ error: 'ACCOUNT_CLAIMED', message: '此名号已被自动建档占用,需持有建档凭据方可注册(或更换名号)' });
108
+ }
105
109
  existing.passHash = await hashPassword(String(password));
106
110
  existing.token = newToken();
107
111
  if (profile) existing.profile = { ...existing.profile, ...profile };
@@ -49,10 +49,9 @@ export function matchDuanyu(artId: string, resultRaw: unknown, limit = 3): Duany
49
49
  const hit = d.tags.filter(t => kws.includes(t)).length + (d.factors || []).filter(f => kws.includes(f)).length;
50
50
  return { d, hit };
51
51
  }).sort((a, b) => b.hit - a.hit);
52
- // 至少命中 1 个关键词才注入(避免无关引证);全部不命中则按术别取第一条最通用的
52
+ // 至少命中 1 个关键词才注入(避免无关引证);零命中不注入
53
53
  const matched = scored.filter(s => s.hit > 0);
54
- const picked = matched.length > 0 ? matched : [scored[0]];
55
- return picked.slice(0, limit).map(s => s.d);
54
+ return matched.slice(0, limit).map(s => s.d);
56
55
  }
57
56
 
58
57
  // 生成 prompt 引证段(仅 reviewed 条目;未校核 seed 一律不注入)
package/tsconfig.json CHANGED
@@ -35,6 +35,6 @@
35
35
  },
36
36
  "include": [
37
37
  "src",
38
- "api"
38
+ "shared"
39
39
  ]
40
40
  }