|
1 | 1 | import { basename, delimiter, dirname, join } from 'path' |
2 | 2 | import { existsSync, readdirSync, statSync } from 'fs' |
| 3 | +import * as https from 'https' |
3 | 4 | import { createRequire } from 'module' |
4 | 5 |
|
5 | 6 | const require = createRequire(import.meta.url) |
@@ -55,6 +56,10 @@ export class WcdbCore { |
55 | 56 | private wcdbStopMonitorPipe: any = null |
56 | 57 | private wcdbGetMonitorPipeName: any = null |
57 | 58 | private wcdbSetMyWxid: any = null |
| 59 | + private wcdbSetTrustedTime: any = null |
| 60 | + |
| 61 | + // 可信时间同步定时器(防本地改时钟绕过到期) |
| 62 | + private trustedTimeTimer: any = null |
58 | 63 |
|
59 | 64 | // 管道监控状态 |
60 | 65 | private monitorPipeClient: any = null |
@@ -132,19 +137,105 @@ export class WcdbCore { |
132 | 137 | this.wcdbStopMonitorPipe = tryBind('int32 wcdb_stop_monitor_pipe()') |
133 | 138 | this.wcdbGetMonitorPipeName = tryBind('int32 wcdb_get_monitor_pipe_name(_Out_ void** outName)') |
134 | 139 | this.wcdbSetMyWxid = tryBind('int32 wcdb_set_my_wxid(int64 handle, const char* wxid)') |
| 140 | + this.wcdbSetTrustedTime = tryBind('int32 wcdb_set_trusted_time(int64 epochSeconds)') |
135 | 141 |
|
136 | | - const initResult = this.wcdbInit() |
| 142 | + let initResult = this.wcdbInit() |
| 143 | + if (initResult === -8 && this.wcdbSetTrustedTime) { |
| 144 | + await this.syncTrustedTime() |
| 145 | + initResult = this.wcdbInit() |
| 146 | + } |
137 | 147 | if (initResult !== 0) { |
138 | 148 | return { success: false, error: `wcdb_init() 返回错误码: ${initResult}` } |
139 | 149 | } |
140 | 150 |
|
141 | 151 | this.initialized = true |
| 152 | + this.startTrustedTimeSync(false) |
142 | 153 | return { success: true } |
143 | 154 | } catch (e: any) { |
144 | 155 | return { success: false, error: `WCDB 初始化异常: ${e.message || String(e)}` } |
145 | 156 | } |
146 | 157 | } |
147 | 158 |
|
| 159 | + // ============== 可信时间同步(防本地改时钟绕过到期)============== |
| 160 | + // native 维护"高水位 + 联网投影"的有效时间;这里负责从公共时间 API 取可信时间喂进 DLL。 |
| 161 | + // 取不到(离线)就静默,DLL 退化为本地时钟 + 历史高水位,离线照常可用。 |
| 162 | + private startTrustedTimeSync(syncNow = true): void { |
| 163 | + if (!this.wcdbSetTrustedTime) return |
| 164 | + if (syncNow) void this.syncTrustedTime() |
| 165 | + if (this.trustedTimeTimer) clearInterval(this.trustedTimeTimer) |
| 166 | + this.trustedTimeTimer = setInterval(() => { void this.syncTrustedTime() }, 6 * 60 * 60 * 1000) |
| 167 | + this.trustedTimeTimer?.unref?.() |
| 168 | + } |
| 169 | + |
| 170 | + private stopTrustedTimeSync(): void { |
| 171 | + if (this.trustedTimeTimer) { |
| 172 | + clearInterval(this.trustedTimeTimer) |
| 173 | + this.trustedTimeTimer = null |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + private async syncTrustedTime(): Promise<void> { |
| 178 | + try { |
| 179 | + if (!this.wcdbSetTrustedTime) return |
| 180 | + const epoch = await this.fetchNetworkEpochSeconds() |
| 181 | + if (epoch && this.isPlausibleEpoch(epoch)) { |
| 182 | + this.wcdbSetTrustedTime(epoch) |
| 183 | + } |
| 184 | + } catch { |
| 185 | + // 离线/失败静默,靠 native 本地高水位兜底 |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + private isPlausibleEpoch(sec: number): boolean { |
| 190 | + return Number.isFinite(sec) && sec > 1700000000 && sec < 4102444800 |
| 191 | + } |
| 192 | + |
| 193 | + private async fetchNetworkEpochSeconds(): Promise<number | null> { |
| 194 | + const sources: Array<{ url: string; parse: (body: string) => number | null }> = [ |
| 195 | + { |
| 196 | + url: 'https://worldtimeapi.org/api/timezone/Etc/UTC', |
| 197 | + parse: (b) => { try { const j = JSON.parse(b); return typeof j.unixtime === 'number' ? j.unixtime : null } catch { return null } }, |
| 198 | + }, |
| 199 | + { |
| 200 | + url: 'https://timeapi.io/api/time/current/zone?timeZone=Etc%2FUTC', |
| 201 | + parse: (b) => { try { const j = JSON.parse(b); const t = Date.parse(String(j.dateTime).replace(/Z?$/, 'Z')); return Number.isFinite(t) ? Math.floor(t / 1000) : null } catch { return null } }, |
| 202 | + }, |
| 203 | + { |
| 204 | + url: 'https://www.cloudflare.com/cdn-cgi/trace', |
| 205 | + parse: (b) => { const m = /(?:^|\n)ts=([0-9.]+)/.exec(b); return m ? Math.floor(parseFloat(m[1])) : null }, |
| 206 | + }, |
| 207 | + ] |
| 208 | + for (const s of sources) { |
| 209 | + try { |
| 210 | + const { body, dateHeader } = await this.httpGetText(s.url, 4000) |
| 211 | + let epoch = s.parse(body) |
| 212 | + if ((!epoch || !this.isPlausibleEpoch(epoch)) && dateHeader) { |
| 213 | + const d = Date.parse(dateHeader) |
| 214 | + if (Number.isFinite(d)) epoch = Math.floor(d / 1000) |
| 215 | + } |
| 216 | + if (epoch && this.isPlausibleEpoch(epoch)) return epoch |
| 217 | + } catch { |
| 218 | + // 试下一个源 |
| 219 | + } |
| 220 | + } |
| 221 | + return null |
| 222 | + } |
| 223 | + |
| 224 | + private httpGetText(url: string, timeoutMs: number): Promise<{ body: string; dateHeader?: string }> { |
| 225 | + return new Promise((resolve, reject) => { |
| 226 | + const req = https.get(url, { timeout: timeoutMs, headers: { 'User-Agent': 'CipherTalk' } }, (res) => { |
| 227 | + const rawDateHeader = res.headers?.date |
| 228 | + const dateHeader = Array.isArray(rawDateHeader) ? rawDateHeader[0] : rawDateHeader |
| 229 | + let body = '' |
| 230 | + res.setEncoding('utf8') |
| 231 | + res.on('data', (c: string) => { if (body.length < 8192) body += c }) |
| 232 | + res.on('end', () => resolve({ body, dateHeader })) |
| 233 | + }) |
| 234 | + req.on('timeout', () => req.destroy(new Error('timeout'))) |
| 235 | + req.on('error', reject) |
| 236 | + }) |
| 237 | + } |
| 238 | + |
148 | 239 | // ============== 路径解析 ============== |
149 | 240 | private findSessionDbs(dir: string, depth = 0, results: string[] = []): string[] { |
150 | 241 | if (depth > 5) return results |
@@ -291,6 +382,7 @@ export class WcdbCore { |
291 | 382 | } |
292 | 383 |
|
293 | 384 | close(): void { |
| 385 | + this.stopTrustedTimeSync() |
294 | 386 | if (this.handle !== null && this.wcdbCloseAccount) { |
295 | 387 | try { this.wcdbCloseAccount(this.handle) } catch (e) { console.error('关闭 WCDB 句柄失败:', e) } |
296 | 388 | } |
@@ -784,12 +876,19 @@ export class WcdbCore { |
784 | 876 | case -4: return '数据库打开失败' |
785 | 877 | case -5: return '查询执行失败' |
786 | 878 | case -6: return 'WCDB 尚未初始化' |
| 879 | + case -7: return 'WCDB 表结构不匹配' |
| 880 | + case -8: return '软件偷来的吧!' |
| 881 | + case -9: return '快提醒作者更新软件了!' |
| 882 | + case -10: return '靠,你从哪搞得软件?' |
787 | 883 | default: return `WCDB 错误码: ${code}` |
788 | 884 | } |
789 | 885 | } |
790 | 886 |
|
791 | 887 | private mapCursorStatusCode(code: number, prefix: string): string { |
792 | 888 | if (code === -7) return 'message schema mismatch:当前账号消息表结构与程序要求不一致' |
| 889 | + if (code === -8) return `${prefix}: ${code}(软件偷来的吧!)` |
| 890 | + if (code === -9) return `${prefix}: ${code}(快提醒作者更新软件了!)` |
| 891 | + if (code === -10) return `${prefix}: ${code}(靠,你从哪搞得软件?)` |
793 | 892 | if (code === -3) return `${prefix}: ${code}(消息数据库未找到)` |
794 | 893 | return `${prefix}: ${code}` |
795 | 894 | } |
|
0 commit comments