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.
- package/dist/assets/{index-CQjUFsty.js → index-CjLUvkh9.js} +22 -22
- package/dist/assets/index-CjLUvkh9.js.map +1 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/src/data/db.json +5325 -7813
- package/server/src/data/guanwei.db +0 -0
- package/server/src/routes/users.ts +38 -11
- package/shared/core/data/duanyu.ts +9 -10
- package/src/utils/userStore.ts +52 -5
- package/dist/assets/index-CQjUFsty.js.map +0 -1
|
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
|
-
|
|
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
|
-
// 断语库
|
|
1
|
+
// 断语库 v3:结构化「原文 → 断语要点 → 适用因子」条目
|
|
2
2
|
// 定位:AI 解读的引证源(排盘 → 断语匹配 → 解读引用「《书·篇》:原文」),未来接考时校准
|
|
3
3
|
// 收录规范 / 版权核查 / 处理清单:见 docs/古籍参考库收录规范.md
|
|
4
|
-
// ✅
|
|
5
|
-
//
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
133
|
+
sources: ['(民间流传口诀,采录通行本;多版本并存)'],
|
|
134
|
+
note: '小六壬无传世成书古籍,属民间掌诀;本条目采录通行本(大安身不动/留连事难成…),另有版本作「大安事事昌」等,措辞差异不影响六神吉凶语义。', status: 'reviewed',
|
|
136
135
|
},
|
|
137
136
|
// ─── 周易(卦术总纲)───
|
|
138
137
|
{
|
package/src/utils/userStore.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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 {
|