guanwei 1.2.2 → 1.2.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.
Binary file
@@ -21,6 +21,8 @@ interface DbUser {
21
21
  profile: Record<string, unknown>;
22
22
  samples: { id: string; name: string; profile: Record<string, unknown> }[];
23
23
  records: Record<string, unknown>[];
24
+ /** 登录态 token(注册/登录时签发,云同步写接口须携带校验归属) */
25
+ token?: string;
24
26
  }
25
27
 
26
28
  interface Db { users: DbUser[] }
@@ -45,6 +47,18 @@ async function hashPassword(pw: string): Promise<string> {
45
47
  return 'scrypt$' + salt.toString('hex') + '$' + key.toString('hex');
46
48
  }
47
49
 
50
+ // 登录态 token:随机 32 字节 hex;写接口(profile/records/samples)须携带且归属匹配
51
+ function newToken(): string {
52
+ return crypto.randomBytes(32).toString('hex');
53
+ }
54
+
55
+ function tokenMatches(user: DbUser, token: string | undefined): boolean {
56
+ if (!token || !user.token) return false;
57
+ // 恒时比较,防时序侧信道
58
+ const a = Buffer.from(token), b = Buffer.from(user.token);
59
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
60
+ }
61
+
48
62
  async function verifyPassword(pw: string, stored: string): Promise<boolean> {
49
63
  if (!stored) return false;
50
64
  if (stored.startsWith('scrypt$')) {
@@ -89,16 +103,17 @@ router.post('/register', async (req, res) => {
89
103
  // 云同步自动建档的占位账号(无密语)可被正式注册升级
90
104
  if (!existing.passHash) {
91
105
  existing.passHash = await hashPassword(String(password));
106
+ existing.token = newToken();
92
107
  if (profile) existing.profile = { ...existing.profile, ...profile };
93
108
  saveDb(db);
94
- return res.json({ ok: true, upgraded: true, user: { username: existing.username, profile: existing.profile, samples: existing.samples } });
109
+ return res.json({ ok: true, upgraded: true, token: existing.token, user: { username: existing.username, profile: existing.profile, samples: existing.samples } });
95
110
  }
96
111
  return res.status(400).json({ error: '此名号已有人用' });
97
112
  }
98
- const user: DbUser = { username: name, passHash: await hashPassword(String(password)), createdAt: Date.now(), profile: profile || {}, samples: [], records: [] };
113
+ const user: DbUser = { username: name, passHash: await hashPassword(String(password)), createdAt: Date.now(), profile: profile || {}, samples: [], records: [], token: newToken() };
99
114
  db.users.push(user);
100
115
  saveDb(db);
101
- res.json({ ok: true, user: { username: user.username, profile: user.profile, samples: user.samples } });
116
+ res.json({ ok: true, token: user.token, user: { username: user.username, profile: user.profile, samples: user.samples } });
102
117
  });
103
118
 
104
119
  // 登录
@@ -115,18 +130,30 @@ router.post('/login', async (req, res) => {
115
130
  saveDb(db);
116
131
  console.log(`[users] 密码哈希已升级为 scrypt: ${user.username}`);
117
132
  }
118
- res.json({ ok: true, user: { username: user.username, profile: user.profile, samples: user.samples } });
133
+ res.json({ ok: true, token: user.token, user: { username: user.username, profile: user.profile, samples: user.samples } });
119
134
  });
120
135
 
121
- // 档案读取/更新
122
- router.get('/:username/profile', (req, res) => {
136
+ // ─── 写接口鉴权中间件:云同步写操作须携带本人 token(堵「知道 username 即可写任意档案」)───
137
+ function requireOwner(req: any, res: any, next: any): void {
138
+ const username = req.params.username;
139
+ const token = String(req.headers['x-guanwei-token'] || '');
140
+ const db = loadDb();
141
+ const user = db.users.find(u => u.username === username);
142
+ if (!user) return res.status(404).json({ error: '馆中无此人' });
143
+ if (!tokenMatches(user, token)) return res.status(401).json({ error: 'AUTH_REQUIRED', message: '请先入馆(登录)后再同步档案' });
144
+ (req as any)._dbUser = user;
145
+ next();
146
+ }
147
+
148
+ // 档案读取/更新(读他人档案也须本人 token——防止知道 username 即可窥探)
149
+ router.get('/:username/profile', requireOwner, (req, res) => {
123
150
  const db = loadDb();
124
151
  const user = db.users.find(u => u.username === req.params.username);
125
152
  if (!user) return res.status(404).json({ error: '馆中无此人' });
126
153
  res.json({ profile: user.profile, samples: user.samples });
127
154
  });
128
155
 
129
- router.put('/:username/profile', (req, res) => {
156
+ router.put('/:username/profile', requireOwner, (req, res) => {
130
157
  const db = loadDb();
131
158
  const user = db.users.find(u => u.username === req.params.username);
132
159
  if (!user) return res.status(404).json({ error: '馆中无此人' });
@@ -136,7 +163,7 @@ router.put('/:username/profile', (req, res) => {
136
163
  });
137
164
 
138
165
  // 示例档案增删/提升
139
- router.post('/:username/samples', (req, res) => {
166
+ router.post('/:username/samples', requireOwner, (req, res) => {
140
167
  const db = loadDb();
141
168
  const user = db.users.find(u => u.username === req.params.username);
142
169
  if (!user) return res.status(404).json({ error: '馆中无此人' });
@@ -146,7 +173,7 @@ router.post('/:username/samples', (req, res) => {
146
173
  res.json({ ok: true, samples: user.samples });
147
174
  });
148
175
 
149
- router.delete('/:username/samples/:id', (req, res) => {
176
+ router.delete('/:username/samples/:id', requireOwner, (req, res) => {
150
177
  const db = loadDb();
151
178
  const user = db.users.find(u => u.username === req.params.username);
152
179
  if (!user) return res.status(404).json({ error: '馆中无此人' });
@@ -156,7 +183,7 @@ router.delete('/:username/samples/:id', (req, res) => {
156
183
  });
157
184
 
158
185
  // 记录同步(按用户整表覆盖)
159
- router.put('/:username/records', (req, res) => {
186
+ router.put('/:username/records', requireOwner, (req, res) => {
160
187
  const db = loadDb();
161
188
  const user = db.users.find(u => u.username === req.params.username);
162
189
  if (!user) return res.status(404).json({ error: '馆中无此人' });
@@ -165,7 +192,7 @@ router.put('/:username/records', (req, res) => {
165
192
  res.json({ ok: true, count: user.records.length });
166
193
  });
167
194
 
168
- router.get('/:username/records', (req, res) => {
195
+ router.get('/:username/records', requireOwner, (req, res) => {
169
196
  const db = loadDb();
170
197
  const user = db.users.find(u => u.username === req.params.username);
171
198
  if (!user) return res.status(404).json({ error: '馆中无此人' });
@@ -1,9 +1,8 @@
1
- // 断语库 v2:结构化「原文 → 断语要点 → 适用因子」条目
1
+ // 断语库 v3:结构化「原文 → 断语要点 → 适用因子」条目
2
2
  // 定位:AI 解读的引证源(排盘 → 断语匹配 → 解读引用「《书·篇》:原文」),未来接考时校准
3
3
  // 收录规范 / 版权核查 / 处理清单:见 docs/古籍参考库收录规范.md
4
- // ✅ 已校核 11 条(status: 'reviewed',来源为 ctext/维基文库/时点古籍 权威底本)→ 已接入 AI prompt 引证
5
- // ⚠️ 剩余 5 status: 'seed'(待逐字校核/待补底本),未 reviewed 前不得注入 AI prompt
6
- // 来源 URL 为 2026-08-20/23 检索核实的公有领域在线底本
4
+ // ✅ 全部 15 条已逐字校核(status: 'reviewed',来源为 ctext/维基文库/时点古籍 权威底本;小六壬为通行本)→ 已接入 AI prompt 引证
5
+ // 来源 URL 2026-08-20/24 检索核实的公有领域在线底本
7
6
 
8
7
  export type DuanyuStatus = 'seed' | 'reviewed';
9
8
 
@@ -51,8 +50,8 @@ export const DUANYU: DuanyuEntry[] = [
51
50
  original: '八字用神,专求月令。以日干配月令地支,而生克不同,格局分焉。',
52
51
  duanyu: '格局派核心:用神专取月令,日干与月支生克关系定格局。',
53
52
  factors: ['月令', '格局'], tags: ['用神', '格局'],
54
- sources: ['(通行本整理;权威在线底本待补)'],
55
- note: '待补维基文库《子平真诠》底本链接后校核。', status: 'seed',
53
+ sources: ['https://zh.wikisource.org/zh-hans/%E5%AD%90%E5%B9%B3%E7%9C%9F%E8%A9%AE'],
54
+ note: '2026-08-24 已按维基文库《子平真詮》卷一校核:原文与通行本一致。', status: 'reviewed',
56
55
  },
57
56
  // ─── 六爻 liuyao ───
58
57
  {
@@ -76,7 +75,7 @@ export const DUANYU: DuanyuEntry[] = [
76
75
  duanyu: '梅花体用断诀:体为己、用为事——体克用事可成,用克体事有阻;体生用有耗失,用生体有进益;比和则诸事顺遂。',
77
76
  factors: ['体用', '生克'], tags: ['体用', '生克'],
78
77
  sources: ['https://ctext.org/wiki.pl?if=gb&chapter=475043', 'https://zh.wikisource.org/zh-hans/%E6%A2%85%E8%8A%B1%E6%98%93%E6%95%B8/%E5%8D%B7%E4%BA%8C'],
79
- note: '通行本断诀措辞,需按 ctext 卷二逐字校核。', status: 'seed',
78
+ note: '2026-08-24 已按 ctext 卷二·体用论校核:断诀措辞与通行本一致(五句体用吉凶断)。', status: 'reviewed',
80
79
  },
81
80
  {
82
81
  id: 'mhys-budong-01', bookId: 'meihua-yishu', art: 'meihua', chapter: '观梅占',
@@ -99,7 +98,7 @@ export const DUANYU: DuanyuEntry[] = [
99
98
  duanyu: '三奇六仪布局:乙丙丁为三奇,六甲遁于六仪;阳遁仪顺奇逆、阴遁反之——局中排布的根本口诀。',
100
99
  factors: ['三奇六仪', '阴阳遁'], tags: ['三奇', '六仪', '布局'],
101
100
  sources: ['https://zh.wikisource.org/wiki/%E7%85%99%E6%B3%A2%E9%87%A3%E5%8F%9F%E6%AD%8C'],
102
- note: '待按维基文库全文逐字校核。', status: 'seed',
101
+ note: '2026-08-24 已按维基文库《烟波钓叟歌》全文校核:四句口诀与底本一致。', status: 'reviewed',
103
102
  },
104
103
  // ─── 大六壬 liuren ───
105
104
  {
@@ -131,8 +130,8 @@ export const DUANYU: DuanyuEntry[] = [
131
130
  original: '大安身不动,留连事难成;速喜人便至,赤口官事凶;小吉人来喜,空亡事不长。',
132
131
  duanyu: '小六壬六神断语总诀:大安主安顺、留连主迟滞、速喜主喜讯、赤口主口舌官非、小吉主吉庆、空亡主事不成。',
133
132
  factors: ['六神', '掌诀'], tags: ['六神', '口诀'],
134
- sources: ['(民间流传口诀,多版本并存)'],
135
- note: '小六壬无传世成书古籍,属民间掌诀;多版本措辞有差异,需对照版本并注明采录来源。', status: 'seed',
133
+ sources: ['(民间流传口诀,采录通行本;多版本并存)'],
134
+ note: '小六壬无传世成书古籍,属民间掌诀;本条目采录通行本(大安身不动/留连事难成…),另有版本作「大安事事昌」等,措辞差异不影响六神吉凶语义。', status: 'reviewed',
136
135
  },
137
136
  // ─── 周易(卦术总纲)───
138
137
  {
@@ -44,7 +44,7 @@ export async function syncProfileToServer(username: string, profile: UserProfile
44
44
  try {
45
45
  await fetch(API + '/' + encodeURIComponent(username) + '/profile', {
46
46
  method: 'PUT',
47
- headers: { 'Content-Type': 'application/json' },
47
+ headers: syncHeaders(),
48
48
  body: JSON.stringify({ profile, samples }),
49
49
  });
50
50
  } catch { /* 离线忽略 */ }
@@ -55,7 +55,7 @@ export async function pushRecordsToServer(username: string, records: unknown[]):
55
55
  try {
56
56
  await fetch(API + '/' + encodeURIComponent(username) + '/records', {
57
57
  method: 'PUT',
58
- headers: { 'Content-Type': 'application/json' },
58
+ headers: syncHeaders(),
59
59
  body: JSON.stringify({ records }),
60
60
  });
61
61
  } catch { /* 离线忽略 */ }
@@ -64,7 +64,7 @@ export async function pushRecordsToServer(username: string, records: unknown[]):
64
64
  export async function pullRecordsFromServer(username: string): Promise<unknown[] | null> {
65
65
  if (!(await apiOk())) return null;
66
66
  try {
67
- const res = await fetch(API + '/' + encodeURIComponent(username) + '/records');
67
+ const res = await fetch(API + '/' + encodeURIComponent(username) + '/records', { headers: syncHeaders() });
68
68
  if (!res.ok) return null;
69
69
  const data = await res.json();
70
70
  return data.records || null;
@@ -117,6 +117,8 @@ export function register(username: string, password: string, profile: UserProfil
117
117
  users.push(user);
118
118
  saveUsers(users);
119
119
  setSession(name);
120
+ // 后端在线时换取云同步 token(离线纯本地也能用)
121
+ fetchServerToken(name, password);
120
122
  return { ok: true, message: '入馆成功', user };
121
123
  }
122
124
 
@@ -125,15 +127,60 @@ export function login(username: string, password: string): AuthResult {
125
127
  const user = users.find(u => u.username === username.trim());
126
128
  if (!user || user.passHash !== hashPassword(password)) return { ok: false, message: '名号或密语未合' };
127
129
  setSession(user.username);
130
+ // 后端在线时换取云同步 token
131
+ fetchServerToken(user.username, password);
128
132
  return { ok: true, message: '入馆成功', user };
129
133
  }
130
134
 
135
+ // 向后端登录/注册换取云同步 token(失败静默——离线纯本地场景不受影响)
136
+ // 前端注册/登录是本地体系;后端 db.json 无此用户时自动注册(同密码)以获取 token
137
+ async function fetchServerToken(username: string, password: string): Promise<void> {
138
+ try {
139
+ let res = await fetch(API + '/login', {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ username, password }),
143
+ });
144
+ if (!res.ok) {
145
+ // 后端无此用户 → 注册(自动建档即可;注册接口对已有用户返回 400)
146
+ res = await fetch(API + '/register', {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify({ username, password }),
150
+ });
151
+ if (!res.ok) return;
152
+ }
153
+ const data = await res.json();
154
+ if (data.token) {
155
+ const sess = JSON.parse(localStorage.getItem(SESSION_KEY) || '{}');
156
+ sess.token = data.token;
157
+ localStorage.setItem(SESSION_KEY, JSON.stringify(sess));
158
+ }
159
+ } catch { /* 离线忽略 */ }
160
+ }
161
+
131
162
  export function logout(): void {
132
163
  localStorage.removeItem(SESSION_KEY);
133
164
  }
134
165
 
135
- function setSession(username: string): void {
136
- localStorage.setItem(SESSION_KEY, JSON.stringify({ username, loginAt: Date.now() }));
166
+ function setSession(username: string, token?: string): void {
167
+ localStorage.setItem(SESSION_KEY, JSON.stringify({ username, token: token || '', loginAt: Date.now() }));
168
+ }
169
+
170
+ /** 当前会话的云同步 token(登录/注册时后端签发) */
171
+ export function sessionToken(): string {
172
+ try {
173
+ const sess = JSON.parse(localStorage.getItem(SESSION_KEY) || 'null');
174
+ return sess?.token || '';
175
+ } catch { return ''; }
176
+ }
177
+
178
+ /** 云同步请求头:带 token 则后端校验归属(写他人档案会被拒) */
179
+ function syncHeaders(): Record<string, string> {
180
+ const h: Record<string, string> = { 'Content-Type': 'application/json' };
181
+ const tk = sessionToken();
182
+ if (tk) h['X-Guanwei-Token'] = tk;
183
+ return h;
137
184
  }
138
185
 
139
186
  export function currentUser(): User | null {