koishi-plugin-chat-patch 3.0.2 → 5.0.0
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/client/vue/index.vue +60 -634
- package/client/web/env.d.ts +56 -0
- package/client/web/index.html +21 -0
- package/client/web/package-lock.json +3419 -0
- package/client/web/package.json +62 -0
- package/client/web/public/bcui/css/color-dark.css +35 -0
- package/client/web/public/bcui/css/color-light.css +35 -0
- package/client/web/public/bcui/css/style.css +468 -0
- package/client/web/public/bcui/js/main.js +193 -0
- package/client/web/public/css/append-dark.css +13 -0
- package/client/web/public/css/append-light.css +13 -0
- package/client/web/public/css/append-napcat-dark.css +14 -0
- package/client/web/public/css/append-napcat-light.css +14 -0
- package/client/web/public/favicon.ico +0 -0
- package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/public/img/icons/icon-maskable.png +0 -0
- package/client/web/public/img/icons/icon.png +0 -0
- package/client/web/public/img/icons/icon.svg +10 -0
- package/client/web/public/robots.txt +2 -0
- package/client/web/public/sw.js +58 -0
- package/client/web/scripts/copy-bcui.mjs +17 -0
- package/client/web/src/App.vue +1127 -0
- package/client/web/src/assets/css/append/append_darwin.css +68 -0
- package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
- package/client/web/src/assets/css/append/append_linux.css +125 -0
- package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
- package/client/web/src/assets/css/append/append_napcat.css +35 -0
- package/client/web/src/assets/css/append/append_new.css +843 -0
- package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
- package/client/web/src/assets/css/append/append_win32.css +109 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
- package/client/web/src/assets/css/chat.css +2104 -0
- package/client/web/src/assets/css/msg.css +941 -0
- package/client/web/src/assets/css/options.css +811 -0
- package/client/web/src/assets/css/sys_notice.css +62 -0
- package/client/web/src/assets/css/view.css +2105 -0
- package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
- package/client/web/src/assets/img/Vue.png +0 -0
- package/client/web/src/assets/img/cakes.svg +189 -0
- package/client/web/src/assets/img/icons/AppIcon.png +0 -0
- package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/exit.png +0 -0
- package/client/web/src/assets/img/icons/pen.png +0 -0
- package/client/web/src/assets/img/stars.svg +89 -0
- package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
- package/client/web/src/assets/l10n/_About I10n.md +35 -0
- package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
- package/client/web/src/assets/l10n/zh-CN.po +1941 -0
- package/client/web/src/assets/logo.png +0 -0
- package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
- package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
- package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
- package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
- package/client/web/src/assets/qed.txt +1 -0
- package/client/web/src/components/AboutPan.vue +197 -0
- package/client/web/src/components/BulletinBody.vue +129 -0
- package/client/web/src/components/DepPan.vue +310 -0
- package/client/web/src/components/EmojiFace.vue +32 -0
- package/client/web/src/components/FacePan.vue +461 -0
- package/client/web/src/components/FileBody.vue +129 -0
- package/client/web/src/components/FileManager.vue +532 -0
- package/client/web/src/components/FriendBody.vue +54 -0
- package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
- package/client/web/src/components/History.vue +49 -0
- package/client/web/src/components/LazyLottie.vue +82 -0
- package/client/web/src/components/MergePan.vue +127 -0
- package/client/web/src/components/MsgBody.vue +1478 -0
- package/client/web/src/components/MusicPlayer.vue +549 -0
- package/client/web/src/components/NoticeBody.vue +168 -0
- package/client/web/src/components/QzonePermissionPan.vue +150 -0
- package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
- package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
- package/client/web/src/components/TinySessionBody.vue +51 -0
- package/client/web/src/components/UmamiInfoPan.vue +1549 -0
- package/client/web/src/components/UpdatePan.vue +263 -0
- package/client/web/src/components/ViewerCom.vue +1360 -0
- package/client/web/src/components/VoiceMsg.vue +626 -0
- package/client/web/src/components/WelPan.vue +894 -0
- package/client/web/src/components/msg-body/.gitkeep +0 -0
- package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
- package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
- package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
- package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
- package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
- package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
- package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
- package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
- package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
- package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
- package/client/web/src/components/notice-component/.gitkeep +0 -0
- package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
- package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
- package/client/web/src/components/tooltip/Tooltip.vue +50 -0
- package/client/web/src/components/tooltip/Tooltips.vue +14 -0
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
- package/client/web/src/function/base.ts +238 -0
- package/client/web/src/function/connect.ts +2042 -0
- package/client/web/src/function/debug.ts +10 -0
- package/client/web/src/function/elements/information.ts +141 -0
- package/client/web/src/function/elements/system.ts +101 -0
- package/client/web/src/function/elements/vueComp.ts +62 -0
- package/client/web/src/function/favicon.ts +76 -0
- package/client/web/src/function/input.ts +69 -0
- package/client/web/src/function/model/emoji.ts +127 -0
- package/client/web/src/function/model/img.ts +150 -0
- package/client/web/src/function/model/msg-body.ts +51 -0
- package/client/web/src/function/msg.ts +2674 -0
- package/client/web/src/function/notify.ts +225 -0
- package/client/web/src/function/option.ts +749 -0
- package/client/web/src/function/satori-model.ts +818 -0
- package/client/web/src/function/satori.ts +325 -0
- package/client/web/src/function/sender.ts +178 -0
- package/client/web/src/function/tooltip.ts +49 -0
- package/client/web/src/function/utils/appUtil.ts +2288 -0
- package/client/web/src/function/utils/avatarUtil.ts +31 -0
- package/client/web/src/function/utils/backgroundUtil.ts +110 -0
- package/client/web/src/function/utils/linkViewUtil.ts +101 -0
- package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
- package/client/web/src/function/utils/msgUtil.ts +1242 -0
- package/client/web/src/function/utils/pinyin.ts +141 -0
- package/client/web/src/function/utils/recordUtil.ts +187 -0
- package/client/web/src/function/utils/sessionUtil.ts +114 -0
- package/client/web/src/function/utils/systemUtil.ts +664 -0
- package/client/web/src/main.ts +91 -0
- package/client/web/src/pages/Chat.vue +3453 -0
- package/client/web/src/pages/Friends.vue +636 -0
- package/client/web/src/pages/Info.vue +717 -0
- package/client/web/src/pages/Messages.vue +875 -0
- package/client/web/src/pages/Options.vue +82 -0
- package/client/web/src/pages/Qzone.vue +1178 -0
- package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
- package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
- package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
- package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
- package/client/web/src/pages/options/OptAccount.vue +107 -0
- package/client/web/src/pages/options/OptAddon.vue +185 -0
- package/client/web/src/pages/options/OptDev.vue +704 -0
- package/client/web/src/pages/options/OptFunction.vue +403 -0
- package/client/web/src/pages/options/OptInfo.vue +165 -0
- package/client/web/src/pages/options/OptView.vue +846 -0
- package/client/web/src/runtime/backend.ts +310 -0
- package/client/web/src/state/auth.ts +14 -0
- package/client/web/src/state/chat.ts +36 -0
- package/client/web/src/state/connection.ts +20 -0
- package/client/web/src/state/contact.ts +37 -0
- package/client/web/src/state/qzone.ts +18 -0
- package/client/web/src/state/sessionHistory.ts +25 -0
- package/client/web/src/state/settings.ts +18 -0
- package/client/web/src/state/sticker.ts +10 -0
- package/client/web/src/state/ui.ts +56 -0
- package/client/web/tsconfig.json +27 -0
- package/client/web/tsconfig.tsbuildinfo +1 -0
- package/client/web/vite.config.ts +33 -0
- package/dist/index.js +1 -12
- package/dist/style.css +1 -1
- package/lib/index.js +2070 -2077
- package/package.json +15 -2
- package/readme.md +0 -22
- package/src/bootstrap.ts +71 -0
- package/src/cache.ts +319 -0
- package/src/config.ts +23 -35
- package/src/database.ts +650 -0
- package/src/index.ts +27 -59
- package/src/media.ts +302 -0
- package/src/recorder.ts +175 -0
- package/src/self-message.ts +456 -0
- package/src/server.d.ts +26 -0
- package/src/types.ts +108 -48
- package/src/web.ts +712 -0
- package/client/vue/chat-logic.ts +0 -683
- package/client/vue/composables/useChatActions.ts +0 -61
- package/client/vue/composables/useChatData.ts +0 -185
- package/client/vue/composables/useImageCache.ts +0 -281
- package/client/vue/composables/useVideoCache.ts +0 -125
- package/client/vue/types.ts +0 -72
- package/lib/api-handlers.d.ts +0 -25
- package/lib/config.d.ts +0 -14
- package/lib/file-manager.d.ts +0 -124
- package/lib/index.d.ts +0 -17
- package/lib/logger.d.ts +0 -9
- package/lib/message-handler.d.ts +0 -35
- package/lib/types.d.ts +0 -57
- package/lib/utils.d.ts +0 -18
- package/src/api-handlers.ts +0 -646
- package/src/file-manager.ts +0 -1436
- package/src/message-handler.ts +0 -507
- package/src/utils.ts +0 -189
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @FileDescription: 本地历史消息工具(Tauri 平台)
|
|
3
|
+
* @Description:
|
|
4
|
+
* 封装对 Tauri 后端 db_* 命令的调用,提供类型安全的本地 SQLite 历史消息读写接口。
|
|
5
|
+
* 非 Tauri 平台调用时会静默 no-op / 返回空数组,不影响其他平台逻辑。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { backend } from '../../runtime/backend'
|
|
9
|
+
import { getMsgRawTxt } from './msgUtil'
|
|
10
|
+
import { Logger } from '../base'
|
|
11
|
+
import { useSettingsStore } from '../../state/settings'
|
|
12
|
+
import { useAuthStore } from '../../state/auth'
|
|
13
|
+
|
|
14
|
+
const logger = new Logger()
|
|
15
|
+
|
|
16
|
+
// ── 类型定义(与 Rust MsgRecord 对应) ───────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface LocalMsgRecord {
|
|
19
|
+
/** Bot 侧的消息 ID */
|
|
20
|
+
message_id: string
|
|
21
|
+
/** 会话 ID(group_id 或 user_id) */
|
|
22
|
+
chat_id: number | string
|
|
23
|
+
/** 会话类型:"group" | "private" */
|
|
24
|
+
chat_type: string
|
|
25
|
+
/** 发送者 user_id */
|
|
26
|
+
sender_id: number | string
|
|
27
|
+
/** 发送者昵称(card 优先,fallback nickname) */
|
|
28
|
+
sender_name: string | null
|
|
29
|
+
/** 消息序号(并非所有 Bot 都提供,可能为 null) */
|
|
30
|
+
seq: number | null
|
|
31
|
+
/** 消息时间戳(秒,Bot 原始值) */
|
|
32
|
+
time: number
|
|
33
|
+
/** JSON 序列化的 MsgItemElem[] 消息段数组 */
|
|
34
|
+
message: string
|
|
35
|
+
/** 纯文本摘要 */
|
|
36
|
+
raw_message: string | null
|
|
37
|
+
/** 是否已撤回 */
|
|
38
|
+
revoked: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isTauriHistoryAvailable(): boolean {
|
|
42
|
+
const settingsStore = useSettingsStore()
|
|
43
|
+
return backend.type === 'tauri' && settingsStore.sysConfig.enable_local_history === true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function callDbRecordList(
|
|
47
|
+
selfId: string | number,
|
|
48
|
+
command: string,
|
|
49
|
+
payload: Record<string, any>,
|
|
50
|
+
errorTag: string,
|
|
51
|
+
): Promise<any[]> {
|
|
52
|
+
if (!isTauriHistoryAvailable()) return []
|
|
53
|
+
try {
|
|
54
|
+
const records: LocalMsgRecord[] = await backend.call(
|
|
55
|
+
undefined,
|
|
56
|
+
command,
|
|
57
|
+
true,
|
|
58
|
+
{ selfId: String(selfId), ...payload },
|
|
59
|
+
)
|
|
60
|
+
return (records ?? []).map(deserializeRecord)
|
|
61
|
+
} catch (e) {
|
|
62
|
+
logger.error(e as unknown as Error, errorTag)
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function callDb(
|
|
68
|
+
selfId: string | number,
|
|
69
|
+
command: string,
|
|
70
|
+
payload: Record<string, any>,
|
|
71
|
+
fallback: any,
|
|
72
|
+
errorTag: string,
|
|
73
|
+
): Promise<any> {
|
|
74
|
+
if (!isTauriHistoryAvailable()) return fallback
|
|
75
|
+
try {
|
|
76
|
+
return await backend.call(
|
|
77
|
+
undefined,
|
|
78
|
+
command,
|
|
79
|
+
true,
|
|
80
|
+
{ selfId: String(selfId), ...payload },
|
|
81
|
+
)
|
|
82
|
+
} catch (e) {
|
|
83
|
+
logger.error(e as unknown as Error, errorTag)
|
|
84
|
+
return fallback
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const META_SEGMENT_TYPE = 'chat-patch-meta'
|
|
89
|
+
|
|
90
|
+
function serializeMsgSegments(segments: any[] | undefined, timeMs?: number): string {
|
|
91
|
+
try {
|
|
92
|
+
const next = [...(segments ?? [])]
|
|
93
|
+
if (Number.isFinite(timeMs) && timeMs && timeMs > 0) {
|
|
94
|
+
next.push({ type: META_SEGMENT_TYPE, time: timeMs })
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify(next)
|
|
97
|
+
} catch {
|
|
98
|
+
return '[]'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function deserializeMsgSegments(serialized: string): any[] {
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(serialized)
|
|
105
|
+
} catch {
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function computeRawMessage(msg: any): string | null {
|
|
111
|
+
try {
|
|
112
|
+
return getMsgRawTxt(msg) || msg.raw_message || null
|
|
113
|
+
} catch {
|
|
114
|
+
return msg.raw_message ?? null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function deriveChatId(selfId: string | number, msgs: any[]): number | string | undefined {
|
|
119
|
+
const firstMsg = msgs[0]
|
|
120
|
+
let chatId: number | string | undefined = firstMsg?.infoList?.group_id ?? firstMsg?.infoList?.target_id
|
|
121
|
+
if (chatId != null) return chatId
|
|
122
|
+
|
|
123
|
+
for (const item of msgs) {
|
|
124
|
+
if (item?.infoList?.sender != null && String(item.infoList.sender) !== String(selfId)) {
|
|
125
|
+
chatId = item.infoList.sender
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return chatId
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function ensureChatIdOnMsgs(selfId: string | number, msgs: any[]): any[] {
|
|
133
|
+
const chatId = deriveChatId(selfId, msgs)
|
|
134
|
+
if (chatId == null) return msgs
|
|
135
|
+
|
|
136
|
+
return msgs.map((item: any) => {
|
|
137
|
+
if (!item?.infoList) return item
|
|
138
|
+
if (item.infoList.group_id != null || item.infoList.target_id != null) {
|
|
139
|
+
return item
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...item,
|
|
143
|
+
infoList: {
|
|
144
|
+
...item.infoList,
|
|
145
|
+
target_id: chatId,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── 辅助:将运行时消息对象转换为 LocalMsgRecord ──────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 将已经过 msgPreprocess 处理的消息对象转成可存入 DB 的 LocalMsgRecord。
|
|
155
|
+
* 若必要字段缺失则返回 null,调用方需过滤掉 null。
|
|
156
|
+
*/
|
|
157
|
+
export function msgToRecord(msg: any): LocalMsgRecord | null {
|
|
158
|
+
const messageId = msg.message_id
|
|
159
|
+
if (!messageId) return null
|
|
160
|
+
|
|
161
|
+
const chatId: number | string = msg.infoList.group_id ?? msg.infoList.target_id
|
|
162
|
+
if (chatId == null) return null
|
|
163
|
+
|
|
164
|
+
const chatType: string =
|
|
165
|
+
msg.message_type ?? (msg.group_id != null ? 'group' : 'private')
|
|
166
|
+
const senderId: number | string = msg.infoList.sender
|
|
167
|
+
if (senderId == null) return null
|
|
168
|
+
|
|
169
|
+
const senderName: string | null =
|
|
170
|
+
(msg.sender?.card && msg.sender.card !== '') ? msg.sender.card : (msg.sender?.nickname ?? null)
|
|
171
|
+
|
|
172
|
+
const rawMessage = computeRawMessage(msg)
|
|
173
|
+
const timeMs = Number(msg.local_time ?? msg.timestamp_ms ?? msg.time_ms ?? 0)
|
|
174
|
+
const messageSerialized = serializeMsgSegments(
|
|
175
|
+
msg.message,
|
|
176
|
+
Number.isFinite(timeMs) && timeMs > 0 ? timeMs : undefined,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
message_id: String(messageId),
|
|
181
|
+
chat_id: chatId,
|
|
182
|
+
chat_type: chatType,
|
|
183
|
+
sender_id: senderId,
|
|
184
|
+
sender_name: senderName,
|
|
185
|
+
seq: msg.seq_id != null ? Number(msg.seq_id) : null,
|
|
186
|
+
time: Number(msg.time),
|
|
187
|
+
message: messageSerialized,
|
|
188
|
+
raw_message: rawMessage,
|
|
189
|
+
revoked: false,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── 读写接口 ──────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 批量将消息保存到本地 SQLite(已有的 message_id 自动忽略,不覆盖)。
|
|
197
|
+
*
|
|
198
|
+
* @param selfId 当前登录账号 uin
|
|
199
|
+
* @param msgs 已完成预处理的消息对象数组(来自 chatStore.messageList 或 newMsg)
|
|
200
|
+
*/
|
|
201
|
+
export async function dbSaveMessages(selfId: string | number, msgs: any[]): Promise<void> {
|
|
202
|
+
if (!isTauriHistoryAvailable()) return
|
|
203
|
+
|
|
204
|
+
const persistableMsgs = ensureChatIdOnMsgs(selfId, msgs)
|
|
205
|
+
const records: LocalMsgRecord[] = persistableMsgs
|
|
206
|
+
.map(msgToRecord)
|
|
207
|
+
.filter((r): r is LocalMsgRecord => r !== null)
|
|
208
|
+
|
|
209
|
+
if (records.length === 0) {
|
|
210
|
+
logger.error(null, '[LocalHistory] dbSaveMessages: 没有有效消息可保存')
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await backend.call(undefined, 'db:saveMessages', true, {
|
|
216
|
+
selfId: String(selfId),
|
|
217
|
+
messages: records,
|
|
218
|
+
})
|
|
219
|
+
} catch (e) {
|
|
220
|
+
logger.error(e as unknown as Error, '[LocalHistory] dbSaveMessages 失败')
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function saveMessagesWithSideEffects(selfId: string | number, msgs: any[]): Promise<void> {
|
|
225
|
+
const settingsStore = useSettingsStore()
|
|
226
|
+
const persistableMsgs = ensureChatIdOnMsgs(selfId, msgs)
|
|
227
|
+
await dbSaveMessages(selfId, persistableMsgs)
|
|
228
|
+
if (settingsStore.sysConfig.disable_local_history_image_cache === true) return
|
|
229
|
+
cacheImagesFromMsgs(selfId, persistableMsgs).catch(() => {})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 获取某会话最新 n 条本地消息(正序,revoked 消息不包含)。
|
|
234
|
+
*
|
|
235
|
+
* @returns 消息段数组已反序列化的消息对象数组,出错或非 Tauri 返回空数组
|
|
236
|
+
*/
|
|
237
|
+
export async function dbGetLatest(
|
|
238
|
+
selfId: string | number,
|
|
239
|
+
chatId: number | string,
|
|
240
|
+
n: number,
|
|
241
|
+
): Promise<any[]> {
|
|
242
|
+
return callDbRecordList(selfId, 'db:getLatest', { chatId, n }, '[LocalHistory] dbGetLatest 失败')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 获取锚点消息之前(更旧)的 n 条,不含锚点本身,正序返回。
|
|
247
|
+
*
|
|
248
|
+
* 典型用途:上拉加载更多历史。
|
|
249
|
+
*/
|
|
250
|
+
export async function dbGetBefore(
|
|
251
|
+
selfId: string | number,
|
|
252
|
+
chatId: number | string,
|
|
253
|
+
messageId: string,
|
|
254
|
+
n: number,
|
|
255
|
+
): Promise<any[]> {
|
|
256
|
+
return callDbRecordList(selfId, 'db:getBefore', { chatId, messageId, n }, '[LocalHistory] dbGetBefore 失败')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 获取某个时间戳之前(更旧)的 n 条消息,正序返回。
|
|
261
|
+
*/
|
|
262
|
+
export async function dbGetBeforeByTime(
|
|
263
|
+
selfId: string | number,
|
|
264
|
+
chatId: number | string,
|
|
265
|
+
beforeTime: number,
|
|
266
|
+
n: number,
|
|
267
|
+
): Promise<any[]> {
|
|
268
|
+
return callDbRecordList(selfId, 'db:getBeforeByTime', { chatId, beforeTime, n }, '[LocalHistory] dbGetBeforeByTime 失败')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 获取锚点消息之后(更新)的 n 条,不含锚点本身,正序返回。
|
|
273
|
+
*
|
|
274
|
+
* 典型用途:从指定消息位置向后展开查看。
|
|
275
|
+
*/
|
|
276
|
+
export async function dbGetAfter(
|
|
277
|
+
selfId: string | number,
|
|
278
|
+
chatId: number | string,
|
|
279
|
+
messageId: string,
|
|
280
|
+
n: number,
|
|
281
|
+
): Promise<any[]> {
|
|
282
|
+
return callDbRecordList(selfId, 'db:getAfter', { chatId, messageId, n }, '[LocalHistory] dbGetAfter 失败')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 将指定 message_id 在本地 DB 中标记为已撤回。
|
|
287
|
+
*
|
|
288
|
+
* @returns 是否命中(true = DB 中存在该消息并更新成功)
|
|
289
|
+
*/
|
|
290
|
+
export async function dbRevokeMessage(
|
|
291
|
+
selfId: string | number,
|
|
292
|
+
messageId: string,
|
|
293
|
+
): Promise<boolean> {
|
|
294
|
+
return callDb(selfId, 'db:revokeMessage', { messageId }, false, '[LocalHistory] dbRevokeMessage 失败')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 在指定会话的本地 DB 中按关键词搜索消息(对 raw_message 做 LIKE 匹配)。
|
|
299
|
+
*
|
|
300
|
+
* @returns 匹配消息列表(正序),出错或非 Tauri 返回空数组
|
|
301
|
+
*/
|
|
302
|
+
export async function dbSearchMessages(
|
|
303
|
+
selfId: string | number,
|
|
304
|
+
chatId: number | string,
|
|
305
|
+
query: string,
|
|
306
|
+
): Promise<any[]> {
|
|
307
|
+
if (!isTauriHistoryAvailable() || !query) return []
|
|
308
|
+
return callDbRecordList(selfId, 'db:searchMessages', { chatId, query }, '[LocalHistory] dbSearchMessages 失败')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function dbGetStats(
|
|
312
|
+
selfId: string | number,
|
|
313
|
+
): Promise<{ totalMessages: number; imageCount: number; imageCacheBytes: number; dbSizeBytes: number } | null> {
|
|
314
|
+
return callDb(selfId, 'db:getStats', {}, null, '[LocalHistory] dbGetStats 失败')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 计算 URL 的 SHA-256 十六进制摘要,用作图片缓存的唯一键。
|
|
319
|
+
*/
|
|
320
|
+
export async function hashUrl(url: string): Promise<string> {
|
|
321
|
+
const data = new TextEncoder().encode(url)
|
|
322
|
+
const buf = await crypto.subtle.digest('SHA-256', data)
|
|
323
|
+
return Array.from(new Uint8Array(buf))
|
|
324
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
325
|
+
.join('')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 将图片缓存到本地加密数据库。
|
|
330
|
+
* `data` 为 base64 编码的原始图片字节。
|
|
331
|
+
*/
|
|
332
|
+
export async function dbCacheImage(
|
|
333
|
+
selfId: string | number,
|
|
334
|
+
urlHash: string,
|
|
335
|
+
mimeType: string,
|
|
336
|
+
data: string,
|
|
337
|
+
): Promise<void> {
|
|
338
|
+
if (!isTauriHistoryAvailable()) return
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
await backend.call(undefined, 'db:cacheImage', true, {
|
|
342
|
+
selfId: String(selfId),
|
|
343
|
+
urlHash,
|
|
344
|
+
mimeType,
|
|
345
|
+
data,
|
|
346
|
+
})
|
|
347
|
+
} catch (e) {
|
|
348
|
+
logger.error(e as unknown as Error, '[LocalHistory] dbCacheImage 失败')
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 从本地数据库读取已缓存的图片。
|
|
354
|
+
* 返回 `{ mimeType, data }` (data 为 base64),未缓存则返回 `null`。
|
|
355
|
+
*/
|
|
356
|
+
export async function dbGetImage(
|
|
357
|
+
selfId: string | number,
|
|
358
|
+
urlHash: string,
|
|
359
|
+
): Promise<{ mimeType: string; data: string } | null> {
|
|
360
|
+
return callDb(selfId, 'db:getImage', { urlHash }, null, '[LocalHistory] dbGetImage 失败')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface DbClearImagesProgress {
|
|
364
|
+
selfId: string
|
|
365
|
+
total: number
|
|
366
|
+
deleted: number
|
|
367
|
+
batchDeleted: number
|
|
368
|
+
progress: number
|
|
369
|
+
done: boolean
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface DbClearImagesResult {
|
|
373
|
+
total: number
|
|
374
|
+
deleted: number
|
|
375
|
+
batches: number
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function dbClearImages(
|
|
379
|
+
selfId: string | number,
|
|
380
|
+
onProgress?: (progress: DbClearImagesProgress) => void,
|
|
381
|
+
): Promise<DbClearImagesResult> {
|
|
382
|
+
if (!isTauriHistoryAvailable()) {
|
|
383
|
+
return { total: 0, deleted: 0, batches: 0 }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let unlisten: undefined | (() => void | Promise<void>)
|
|
387
|
+
if (onProgress && backend.type === 'tauri') {
|
|
388
|
+
const { listen } = await import('@tauri-apps/api/event')
|
|
389
|
+
unlisten = await listen<DbClearImagesProgress>('db:clearImagesProgress', (event) => {
|
|
390
|
+
const payload = event.payload
|
|
391
|
+
if (!payload) return
|
|
392
|
+
if (String(payload.selfId) !== String(selfId)) return
|
|
393
|
+
onProgress(payload)
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const result = await callDb(
|
|
399
|
+
selfId,
|
|
400
|
+
'db:clearImages',
|
|
401
|
+
{},
|
|
402
|
+
{ total: 0, deleted: 0, batches: 0 },
|
|
403
|
+
'[LocalHistory] dbClearImages 失败',
|
|
404
|
+
)
|
|
405
|
+
return {
|
|
406
|
+
total: Number(result?.total ?? 0),
|
|
407
|
+
deleted: Number(result?.deleted ?? 0),
|
|
408
|
+
batches: Number(result?.batches ?? 0),
|
|
409
|
+
}
|
|
410
|
+
} finally {
|
|
411
|
+
if (unlisten) await unlisten()
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── 内部工具 ──────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 遍历消息列表,将所有图片段下载并缓存到本地数据库。
|
|
419
|
+
* 已缓存的图片(url_hash 命中)不会重复下载。
|
|
420
|
+
*/
|
|
421
|
+
async function cacheImagesFromMsgs(selfId: string | number, msgs: any[]): Promise<void> {
|
|
422
|
+
if (!isTauriHistoryAvailable()) return
|
|
423
|
+
const urls = extractImageUrlsFromMsgs(msgs)
|
|
424
|
+
for (const url of urls) {
|
|
425
|
+
try {
|
|
426
|
+
await cacheSingleImage(selfId, url)
|
|
427
|
+
} catch {
|
|
428
|
+
// 单张图片失败不影响其余图片
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function extractImageUrlsFromMsgs(msgs: any[]): string[] {
|
|
434
|
+
const urls: string[] = []
|
|
435
|
+
for (const msg of msgs) {
|
|
436
|
+
if (!Array.isArray(msg.message)) continue
|
|
437
|
+
for (const seg of msg.message) {
|
|
438
|
+
if (seg.type === 'image' && seg.url && seg.url.startsWith('http')) {
|
|
439
|
+
urls.push(seg.url)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return urls
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function downloadImageViaProxy(url: string): Promise<{ mimeType: string; base64: string } | null> {
|
|
447
|
+
const fetchUrl = backend.proxy? `http://localhost:${backend.proxy}/proxy?url=${encodeURIComponent(url)}`: url
|
|
448
|
+
|
|
449
|
+
const resp = await fetch(fetchUrl)
|
|
450
|
+
if (!resp.ok) return null
|
|
451
|
+
|
|
452
|
+
const mimeType = resp.headers.get('Content-Type')?.split(';')[0]?.trim() ?? 'image/jpeg'
|
|
453
|
+
const buffer = await resp.arrayBuffer()
|
|
454
|
+
const bytes = new Uint8Array(buffer)
|
|
455
|
+
let binary = ''
|
|
456
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i])
|
|
457
|
+
const base64 = btoa(binary)
|
|
458
|
+
return { mimeType, base64 }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function cacheSingleImage(selfId: string | number, url: string): Promise<void> {
|
|
462
|
+
const urlHash = await hashUrl(url)
|
|
463
|
+
const existing = await dbGetImage(selfId, urlHash)
|
|
464
|
+
if (existing) return
|
|
465
|
+
|
|
466
|
+
const downloaded = await downloadImageViaProxy(url)
|
|
467
|
+
if (!downloaded) return
|
|
468
|
+
|
|
469
|
+
await dbCacheImage(selfId, urlHash, downloaded.mimeType, downloaded.base64)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 将 DB 返回的 LocalMsgRecord 还原为与 chatStore.messageList 兼容的消息对象。
|
|
474
|
+
*/
|
|
475
|
+
function deserializeRecord(record: LocalMsgRecord): any {
|
|
476
|
+
const authStore = useAuthStore()
|
|
477
|
+
const parsed = deserializeMsgSegments(record.message)
|
|
478
|
+
const meta = parsed.find((item) => item?.type === META_SEGMENT_TYPE)
|
|
479
|
+
const timeMs = Number(meta?.time ?? 0)
|
|
480
|
+
const message = parsed.filter((item) => item?.type !== META_SEGMENT_TYPE)
|
|
481
|
+
|
|
482
|
+
// 判断是否为自己发送的消息,还原 post_type
|
|
483
|
+
const isSelf = record.sender_id === Number(authStore.loginInfo.uin)
|
|
484
|
+
const postType = isSelf ? 'message_sent' : 'message'
|
|
485
|
+
|
|
486
|
+
// 群消息:sender_name 来自 card,私聊来自 nickname
|
|
487
|
+
const isGroup = record.chat_type === 'group'
|
|
488
|
+
const sender = isGroup ? { user_id: record.sender_id, card: record.sender_name ?? '', nickname: record.sender_name ?? '' } : { user_id: record.sender_id, card: '', nickname: record.sender_name ?? '' }
|
|
489
|
+
const infoList = {
|
|
490
|
+
message_id: record.message_id,
|
|
491
|
+
private_id: isGroup ? undefined : record.chat_id,
|
|
492
|
+
group_id: isGroup ? record.chat_id : undefined,
|
|
493
|
+
target_id: isGroup ? undefined : record.chat_id,
|
|
494
|
+
sender: record.sender_id,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
post_type: postType,
|
|
499
|
+
message_id: record.message_id,
|
|
500
|
+
message_type: record.chat_type,
|
|
501
|
+
// 根据 chat_type 恢复对应的 id 字段
|
|
502
|
+
...(isGroup ? { group_id: record.chat_id } : { user_id: record.chat_id }),
|
|
503
|
+
sender,
|
|
504
|
+
time: record.time,
|
|
505
|
+
timestamp_ms: timeMs || undefined,
|
|
506
|
+
local_time: timeMs || undefined,
|
|
507
|
+
time_ms: timeMs || undefined,
|
|
508
|
+
message,
|
|
509
|
+
infoList,
|
|
510
|
+
raw_message: record.raw_message ?? '',
|
|
511
|
+
revoked: record.revoked,
|
|
512
|
+
// 消息序列号(并非所有 Bot 都提供,可为 null)
|
|
513
|
+
...(record.seq != null ? { message_seq: record.seq, seq_id: record.seq } : {}),
|
|
514
|
+
// 标记来源为本地缓存,业务层可按需用此字段区分
|
|
515
|
+
_from_local_db: true,
|
|
516
|
+
}
|
|
517
|
+
}
|