@voko/lite 0.3.1
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 +32 -0
- package/scripts/build-native.js +72 -0
- package/src/bankHeadOffices.js +20543 -0
- package/src/channels/email.js +35 -0
- package/src/channels/feishu.js +31 -0
- package/src/channels/qq-email.js +30 -0
- package/src/channels/registry.js +279 -0
- package/src/channels/telegram.js +28 -0
- package/src/channels/voko-email.js +7 -0
- package/src/channels/wechat.js +35 -0
- package/src/cli.js +120 -0
- package/src/context.js +164 -0
- package/src/core/access-control-api.js +150 -0
- package/src/core/access-control.js +56 -0
- package/src/core/agent-registration.js +319 -0
- package/src/core/api-signature.js +33 -0
- package/src/core/audit.js +133 -0
- package/src/core/database.js +1409 -0
- package/src/core/did-auth.js +54 -0
- package/src/core/hermes-paths.js +57 -0
- package/src/core/invitation.js +49 -0
- package/src/core/lite-bus.js +16 -0
- package/src/core/llm-client.js +1032 -0
- package/src/core/messenger.js +456 -0
- package/src/core/notifier.js +99 -0
- package/src/core/offline-sync.js +150 -0
- package/src/core/payment.js +285 -0
- package/src/core/publish-agent.js +166 -0
- package/src/core/register-capabilities.js +119 -0
- package/src/core/search-capabilities.js +136 -0
- package/src/core/send-message.js +85 -0
- package/src/core/set-agent-status.js +65 -0
- package/src/core/update-agent-profile.js +102 -0
- package/src/core/worker-manager.js +332 -0
- package/src/endpoints.json +21 -0
- package/src/index.js +712 -0
- package/src/mcp/CLAUDE_TEST.md +82 -0
- package/src/mcp/FULL_TEST.md +139 -0
- package/src/mcp/TEST.md +124 -0
- package/src/mcp/TEST_STEPS.md +75 -0
- package/src/mcp/server.js +612 -0
- package/src/mcp/tools.js +1367 -0
- package/src/mcp/transport/http.js +95 -0
- package/src/mcp/transport/stdio.js +20 -0
- package/src/preload.js +27 -0
- package/src/server/agent-email-api.js +120 -0
- package/src/server/agent-manager.js +580 -0
- package/src/server/email-handler.js +329 -0
- package/src/server/feishu-handler.js +249 -0
- package/src/server/hermes-api-client.js +166 -0
- package/src/server/hermes-discovery.js +80 -0
- package/src/server/hermes-handler.js +287 -0
- package/src/server/openclaw-handler-cli.js +131 -0
- package/src/server/openclaw-websocket-handler.js +1290 -0
- package/src/server/oss.js +186 -0
- package/src/server/owner-intervention-notifier.js +320 -0
- package/src/server/release-page.html +204 -0
- package/src/server/telegram-handler.js +208 -0
- package/src/server/voko-email-handler.js +68 -0
- package/src/server/wechat-handler.js +439 -0
- package/src/workers/agent-worker.js +378 -0
- package/src/workers/message-content.js +51 -0
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* database.js — 数据库层
|
|
3
|
+
*
|
|
4
|
+
* 从 main.js 提取的 DB 初始化、迁移、查询封装。
|
|
5
|
+
* 纯 Node.js,无 Electron 依赖。Desktop 和 Lite 共用。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const Database = require('better-sqlite3');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { BANK_HEAD_OFFICES } = require('../bankHeadOffices');
|
|
12
|
+
const ENDPOINTS = require('../endpoints.json');
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// DB 写入串行队列
|
|
16
|
+
// ============================================
|
|
17
|
+
let _dbWriteQueue = Promise.resolve();
|
|
18
|
+
|
|
19
|
+
function enqueueDbWrite(fn) {
|
|
20
|
+
_dbWriteQueue = _dbWriteQueue.then(fn, fn).catch(e => console.error('[DB队列]', e));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 返回当前队列 Promise,可用于等待队列清空 */
|
|
24
|
+
function waitForDbQueue() {
|
|
25
|
+
return _dbWriteQueue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// 建表 + 迁移
|
|
30
|
+
// ============================================
|
|
31
|
+
function initDatabase(dbPath) {
|
|
32
|
+
const db = new Database(dbPath, { timeout: 3000 });
|
|
33
|
+
try { db.pragma('journal_mode = WAL'); } catch (_) {}
|
|
34
|
+
try { db.pragma('synchronous = NORMAL'); } catch (_) {}
|
|
35
|
+
console.log('Database opened successfully');
|
|
36
|
+
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
channel_id TEXT NOT NULL,
|
|
41
|
+
channel_type INTEGER NOT NULL,
|
|
42
|
+
from_uid TEXT NOT NULL,
|
|
43
|
+
to_uid TEXT NOT NULL,
|
|
44
|
+
content TEXT NOT NULL,
|
|
45
|
+
timestamp INTEGER NOT NULL,
|
|
46
|
+
is_me INTEGER NOT NULL,
|
|
47
|
+
status TEXT NOT NULL,
|
|
48
|
+
agent_id TEXT
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
console.log('Messages table created/verified');
|
|
52
|
+
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
55
|
+
user_uid TEXT NOT NULL,
|
|
56
|
+
channel_id TEXT NOT NULL,
|
|
57
|
+
channel_type INTEGER NOT NULL,
|
|
58
|
+
name TEXT NOT NULL,
|
|
59
|
+
avatar TEXT,
|
|
60
|
+
last_message TEXT,
|
|
61
|
+
last_timestamp INTEGER,
|
|
62
|
+
unread_count INTEGER DEFAULT 0,
|
|
63
|
+
session_key TEXT,
|
|
64
|
+
agent_id TEXT,
|
|
65
|
+
PRIMARY KEY (user_uid, channel_id)
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
console.log('Conversations table created/verified');
|
|
69
|
+
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS owner_interventions (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
visitor_id TEXT NOT NULL,
|
|
74
|
+
session_key TEXT NOT NULL,
|
|
75
|
+
problem TEXT NOT NULL,
|
|
76
|
+
agent_suggestion TEXT,
|
|
77
|
+
ask_time INTEGER NOT NULL,
|
|
78
|
+
expire_time INTEGER,
|
|
79
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
80
|
+
owner_reply TEXT,
|
|
81
|
+
reply_time INTEGER,
|
|
82
|
+
parent_message_id TEXT,
|
|
83
|
+
is_sent INTEGER DEFAULT 0,
|
|
84
|
+
channel_type TEXT,
|
|
85
|
+
resolved_at INTEGER,
|
|
86
|
+
created_at INTEGER NOT NULL,
|
|
87
|
+
updated_at INTEGER NOT NULL
|
|
88
|
+
)
|
|
89
|
+
`);
|
|
90
|
+
console.log('Owner interventions table created/verified');
|
|
91
|
+
|
|
92
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_owner_interventions_visitor ON owner_interventions(visitor_id)`);
|
|
93
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_owner_interventions_status ON owner_interventions(status)`);
|
|
94
|
+
|
|
95
|
+
// 迁移:conversations 添加 session_key(兼容旧表)
|
|
96
|
+
try {
|
|
97
|
+
const tableInfo = db.prepare(`PRAGMA table_info(conversations)`).all();
|
|
98
|
+
const hasSessionKey = tableInfo.some(col => col.name === 'session_key');
|
|
99
|
+
if (!hasSessionKey) {
|
|
100
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN session_key TEXT`);
|
|
101
|
+
console.log('Added session_key column to conversations table');
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.log('Session_key column check/add error (may already exist):', e.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 迁移:owner_interventions 兼容字段
|
|
108
|
+
try {
|
|
109
|
+
const tableInfo = db.prepare(`PRAGMA table_info(owner_interventions)`).all();
|
|
110
|
+
const hasParentMsgId = tableInfo.some(col => col.name === 'parent_message_id');
|
|
111
|
+
const hasFeishuMsgId = tableInfo.some(col => col.name === 'feishu_message_id');
|
|
112
|
+
const hasChannelType = tableInfo.some(col => col.name === 'channel_type');
|
|
113
|
+
const hasIsSent = tableInfo.some(col => col.name === 'is_sent');
|
|
114
|
+
const hasRetryCount = tableInfo.some(col => col.name === 'retry_count');
|
|
115
|
+
const hasLastRetryAt = tableInfo.some(col => col.name === 'last_retry_at');
|
|
116
|
+
|
|
117
|
+
if (hasFeishuMsgId && !hasParentMsgId) {
|
|
118
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN parent_message_id TEXT`);
|
|
119
|
+
db.exec(`UPDATE owner_interventions SET parent_message_id = feishu_message_id WHERE parent_message_id IS NULL`);
|
|
120
|
+
console.log('Renamed feishu_message_id to parent_message_id');
|
|
121
|
+
}
|
|
122
|
+
if (!hasChannelType) {
|
|
123
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN channel_type TEXT`);
|
|
124
|
+
console.log('Added channel_type column to owner_interventions table');
|
|
125
|
+
}
|
|
126
|
+
if (!hasIsSent) {
|
|
127
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN is_sent INTEGER DEFAULT 0`);
|
|
128
|
+
console.log('Added is_sent column to owner_interventions table');
|
|
129
|
+
}
|
|
130
|
+
if (!hasRetryCount) {
|
|
131
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN retry_count INTEGER DEFAULT 0`);
|
|
132
|
+
console.log('Added retry_count column to owner_interventions table');
|
|
133
|
+
}
|
|
134
|
+
if (!hasLastRetryAt) {
|
|
135
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN last_retry_at INTEGER DEFAULT 0`);
|
|
136
|
+
console.log('Added last_retry_at column to owner_interventions table');
|
|
137
|
+
}
|
|
138
|
+
const hasAgentNotified = tableInfo.some(col => col.name === 'agent_notified');
|
|
139
|
+
if (!hasAgentNotified) {
|
|
140
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN agent_notified INTEGER DEFAULT 0`);
|
|
141
|
+
console.log('Added agent_notified column to owner_interventions table');
|
|
142
|
+
}
|
|
143
|
+
if (!tableInfo.some(col => col.name === 'skip_reply')) {
|
|
144
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN skip_reply INTEGER DEFAULT 0`);
|
|
145
|
+
console.log('Added skip_reply column to owner_interventions table');
|
|
146
|
+
}
|
|
147
|
+
if (!tableInfo.some(col => col.name === 'email_message_id')) {
|
|
148
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN email_message_id TEXT`);
|
|
149
|
+
console.log('Added email_message_id column to owner_interventions table');
|
|
150
|
+
}
|
|
151
|
+
if (hasFeishuMsgId) {
|
|
152
|
+
db.exec(`ALTER TABLE owner_interventions DROP COLUMN feishu_message_id`);
|
|
153
|
+
console.log('Dropped feishu_message_id column');
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.log('Owner interventions column migration error:', e.message);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, timestamp)`);
|
|
160
|
+
|
|
161
|
+
// 新建 agents 表
|
|
162
|
+
db.exec(`
|
|
163
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
164
|
+
id TEXT PRIMARY KEY,
|
|
165
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
166
|
+
imUid TEXT NOT NULL,
|
|
167
|
+
imToken TEXT NOT NULL,
|
|
168
|
+
im_server_url TEXT NOT NULL,
|
|
169
|
+
owner_email TEXT,
|
|
170
|
+
chatroom_url TEXT,
|
|
171
|
+
payment_url TEXT,
|
|
172
|
+
did TEXT,
|
|
173
|
+
public_key TEXT,
|
|
174
|
+
login_token TEXT,
|
|
175
|
+
ability TEXT,
|
|
176
|
+
publish_status TEXT NOT NULL DEFAULT 'unpublished',
|
|
177
|
+
created_at INTEGER NOT NULL,
|
|
178
|
+
updated_at INTEGER NOT NULL
|
|
179
|
+
)
|
|
180
|
+
`);
|
|
181
|
+
console.log('Agents table created/verified');
|
|
182
|
+
|
|
183
|
+
// 迁移:messages 表添加 agent_id
|
|
184
|
+
try {
|
|
185
|
+
const msgTableInfo = db.prepare(`PRAGMA table_info(messages)`).all();
|
|
186
|
+
if (!msgTableInfo.some(col => col.name === 'agent_id')) {
|
|
187
|
+
db.exec(`ALTER TABLE messages ADD COLUMN agent_id TEXT`);
|
|
188
|
+
console.log('Added agent_id column to messages table');
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.log('Messages agent_id column migration error:', e.message);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 迁移:conversations 表添加 agent_id
|
|
195
|
+
try {
|
|
196
|
+
const convTableInfo = db.prepare(`PRAGMA table_info(conversations)`).all();
|
|
197
|
+
if (!convTableInfo.some(col => col.name === 'agent_id')) {
|
|
198
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN agent_id TEXT`);
|
|
199
|
+
console.log('Added agent_id column to conversations table');
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.log('Conversations agent_id column migration error:', e.message);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 迁移:agents 表批量加字段
|
|
206
|
+
try {
|
|
207
|
+
const agentsTableInfo = db.prepare(`PRAGMA table_info(agents)`).all();
|
|
208
|
+
if (!agentsTableInfo.some(col => col.name === 'ability')) {
|
|
209
|
+
db.exec(`ALTER TABLE agents ADD COLUMN ability TEXT`);
|
|
210
|
+
console.log('Added ability column to agents table');
|
|
211
|
+
}
|
|
212
|
+
if (!agentsTableInfo.some(col => col.name === 'private_key')) {
|
|
213
|
+
db.exec(`ALTER TABLE agents ADD COLUMN private_key TEXT`);
|
|
214
|
+
console.log('Added private_key column to agents table');
|
|
215
|
+
}
|
|
216
|
+
if (!agentsTableInfo.some(col => col.name === 'short_link_url')) {
|
|
217
|
+
db.exec(`ALTER TABLE agents ADD COLUMN short_link_url TEXT`);
|
|
218
|
+
console.log('Added short_link_url column to agents table');
|
|
219
|
+
}
|
|
220
|
+
if (!agentsTableInfo.some(col => col.name === 'qr_code_url')) {
|
|
221
|
+
db.exec(`ALTER TABLE agents ADD COLUMN qr_code_url TEXT`);
|
|
222
|
+
console.log('Added qr_code_url column to agents table');
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.log('Agents columns migration error:', e.message);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 迁移:owner_interventions 添加 agent_id
|
|
229
|
+
try {
|
|
230
|
+
const oiTableInfo = db.prepare(`PRAGMA table_info(owner_interventions)`).all();
|
|
231
|
+
if (!oiTableInfo.some(col => col.name === 'agent_id')) {
|
|
232
|
+
db.exec(`ALTER TABLE owner_interventions ADD COLUMN agent_id TEXT`);
|
|
233
|
+
console.log('Added agent_id column to owner_interventions table');
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.log('Owner interventions agent_id column migration error:', e.message);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 迁移:agents 批量添加 Hermes/计费/能力相关字段
|
|
240
|
+
try {
|
|
241
|
+
const agentsCols = db.prepare(`PRAGMA table_info(agents)`).all();
|
|
242
|
+
const agentFields = [
|
|
243
|
+
['backend_type', "TEXT NOT NULL DEFAULT 'openclaw'"],
|
|
244
|
+
['agent_name', 'TEXT'],
|
|
245
|
+
['category', "TEXT DEFAULT 'other'"],
|
|
246
|
+
['category_label', 'TEXT'],
|
|
247
|
+
['description', 'TEXT'],
|
|
248
|
+
['address', 'TEXT'],
|
|
249
|
+
['contact_phone', 'TEXT'],
|
|
250
|
+
['short_description', 'TEXT'],
|
|
251
|
+
['icon_url', 'TEXT'],
|
|
252
|
+
['cover_url', 'TEXT'],
|
|
253
|
+
['tags', 'TEXT'],
|
|
254
|
+
['capability', 'TEXT'],
|
|
255
|
+
["access_mode", "TEXT NOT NULL DEFAULT 'public'"],
|
|
256
|
+
];
|
|
257
|
+
for (const [col, type] of agentFields) {
|
|
258
|
+
if (!agentsCols.some(c => c.name === col)) {
|
|
259
|
+
db.exec(`ALTER TABLE agents ADD COLUMN ${col} ${type}`);
|
|
260
|
+
console.log(`Added ${col} column to agents table`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.log('Agents backend_type/meta fields migration error:', e.message);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 迁移:agents 表添加 payment_fee_rate / agent_usage_fee_rate
|
|
268
|
+
try {
|
|
269
|
+
const agFeeCols = db.prepare(`PRAGMA table_info(agents)`).all();
|
|
270
|
+
if (!agFeeCols.some(col => col.name === 'payment_fee_rate')) {
|
|
271
|
+
db.exec(`ALTER TABLE agents ADD COLUMN payment_fee_rate REAL`);
|
|
272
|
+
console.log('Added payment_fee_rate column to agents table');
|
|
273
|
+
}
|
|
274
|
+
if (!agFeeCols.some(col => col.name === 'agent_usage_fee_rate')) {
|
|
275
|
+
db.exec(`ALTER TABLE agents ADD COLUMN agent_usage_fee_rate REAL`);
|
|
276
|
+
console.log('Added agent_usage_fee_rate column to agents table');
|
|
277
|
+
}
|
|
278
|
+
if (!agFeeCols.some(col => col.name === 'payment_auth_id')) {
|
|
279
|
+
db.exec(`ALTER TABLE agents ADD COLUMN payment_auth_id TEXT`);
|
|
280
|
+
console.log('Added payment_auth_id column to agents table');
|
|
281
|
+
}
|
|
282
|
+
if (!agFeeCols.some(col => col.name === 'cap_error')) {
|
|
283
|
+
db.exec(`ALTER TABLE agents ADD COLUMN cap_error TEXT`);
|
|
284
|
+
console.log('Added cap_error column to agents table');
|
|
285
|
+
}
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.log('Agents fee/payment migration error:', e.message);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 迁移:messages 表添加 is_audit_reply
|
|
291
|
+
try {
|
|
292
|
+
const msgCols = db.prepare(`PRAGMA table_info(messages)`).all();
|
|
293
|
+
if (!msgCols.some(col => col.name === 'is_audit_reply')) {
|
|
294
|
+
db.exec(`ALTER TABLE messages ADD COLUMN is_audit_reply INTEGER DEFAULT 0`);
|
|
295
|
+
console.log('Added is_audit_reply column to messages table');
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.log('Messages is_audit_reply migration error:', e.message);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 迁移:messages 添加 WukongIM 标准字段
|
|
302
|
+
try {
|
|
303
|
+
const msgCols2 = db.prepare(`PRAGMA table_info(messages)`).all();
|
|
304
|
+
const wkVars = [
|
|
305
|
+
['message_seq', 'INTEGER'],
|
|
306
|
+
['client_msg_no', 'TEXT'],
|
|
307
|
+
['no_persist', 'INTEGER DEFAULT 0'],
|
|
308
|
+
['red_dot', 'INTEGER DEFAULT 0'],
|
|
309
|
+
['sync_once', 'INTEGER DEFAULT 0'],
|
|
310
|
+
['content_type', 'INTEGER DEFAULT 1']
|
|
311
|
+
];
|
|
312
|
+
for (const [col, type] of wkVars) {
|
|
313
|
+
if (!msgCols2.some(c => c.name === col)) {
|
|
314
|
+
db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
|
|
315
|
+
console.log(`Added ${col} column to messages table`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
console.log('Messages WukongIM fields migration error:', e.message);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_messages_channel_msgseq ON messages(channel_id, message_seq)`);
|
|
323
|
+
|
|
324
|
+
// 新建 audit_rules 表
|
|
325
|
+
db.exec(`
|
|
326
|
+
CREATE TABLE IF NOT EXISTS audit_rules (
|
|
327
|
+
id TEXT PRIMARY KEY,
|
|
328
|
+
direction TEXT NOT NULL,
|
|
329
|
+
keyword TEXT NOT NULL,
|
|
330
|
+
action TEXT NOT NULL,
|
|
331
|
+
prompt TEXT,
|
|
332
|
+
is_default INTEGER DEFAULT 0,
|
|
333
|
+
created_at INTEGER NOT NULL,
|
|
334
|
+
updated_at INTEGER NOT NULL
|
|
335
|
+
)
|
|
336
|
+
`);
|
|
337
|
+
console.log('Audit rules table created/verified');
|
|
338
|
+
|
|
339
|
+
// 新建 payment_auth 表
|
|
340
|
+
db.exec(`
|
|
341
|
+
CREATE TABLE IF NOT EXISTS payment_auth (
|
|
342
|
+
id TEXT PRIMARY KEY,
|
|
343
|
+
name TEXT DEFAULT '',
|
|
344
|
+
id_card TEXT DEFAULT '',
|
|
345
|
+
bank_card TEXT DEFAULT '',
|
|
346
|
+
phone TEXT DEFAULT '',
|
|
347
|
+
status TEXT DEFAULT '未认证',
|
|
348
|
+
receiver_type INTEGER DEFAULT 1,
|
|
349
|
+
bank_code TEXT DEFAULT '',
|
|
350
|
+
bank_name TEXT DEFAULT '',
|
|
351
|
+
company_name TEXT DEFAULT '',
|
|
352
|
+
unified_social_credit_code TEXT DEFAULT '',
|
|
353
|
+
legal_name TEXT DEFAULT '',
|
|
354
|
+
legal_licence_no TEXT DEFAULT '',
|
|
355
|
+
request_no TEXT,
|
|
356
|
+
receiver_no TEXT,
|
|
357
|
+
receiver_apply_status TEXT DEFAULT 'none',
|
|
358
|
+
receiver_sign_status TEXT DEFAULT '',
|
|
359
|
+
receiver_sign_url TEXT DEFAULT '',
|
|
360
|
+
merchant_sign_url TEXT DEFAULT '',
|
|
361
|
+
payment_user_uid TEXT,
|
|
362
|
+
created_at INTEGER NOT NULL,
|
|
363
|
+
updated_at INTEGER NOT NULL
|
|
364
|
+
)
|
|
365
|
+
`);
|
|
366
|
+
console.log('Payment auth table created/verified');
|
|
367
|
+
|
|
368
|
+
// 迁移:payment_auth 兼容 payment_user_uid
|
|
369
|
+
try {
|
|
370
|
+
const paCols = db.prepare(`PRAGMA table_info(payment_auth)`).all().map(c => c.name);
|
|
371
|
+
if (!paCols.includes('payment_user_uid')) {
|
|
372
|
+
db.exec(`ALTER TABLE payment_auth ADD COLUMN payment_user_uid TEXT`);
|
|
373
|
+
console.log('Added payment_user_uid column to payment_auth');
|
|
374
|
+
}
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.log('payment_auth payment_user_uid migration:', e.message);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 新建 payment_orders 表
|
|
380
|
+
db.exec(`
|
|
381
|
+
CREATE TABLE IF NOT EXISTS payment_orders (
|
|
382
|
+
id TEXT PRIMARY KEY,
|
|
383
|
+
agent_id TEXT NOT NULL,
|
|
384
|
+
visitor_id TEXT NOT NULL,
|
|
385
|
+
from_uid TEXT,
|
|
386
|
+
amount REAL NOT NULL,
|
|
387
|
+
description TEXT DEFAULT '',
|
|
388
|
+
order_no TEXT,
|
|
389
|
+
pay_url TEXT,
|
|
390
|
+
status TEXT DEFAULT 'pending',
|
|
391
|
+
result TEXT,
|
|
392
|
+
created_at INTEGER NOT NULL,
|
|
393
|
+
updated_at INTEGER NOT NULL
|
|
394
|
+
)
|
|
395
|
+
`);
|
|
396
|
+
console.log('Payment orders table created/verified');
|
|
397
|
+
|
|
398
|
+
// 迁移:payment_orders 兼容
|
|
399
|
+
try {
|
|
400
|
+
const poCols = db.prepare(`PRAGMA table_info(payment_orders)`).all();
|
|
401
|
+
if (poCols.some(c => c.name === 'error_msg')) {
|
|
402
|
+
db.exec(`ALTER TABLE payment_orders RENAME COLUMN error_msg TO result`);
|
|
403
|
+
console.log('payment_orders: error_msg → result');
|
|
404
|
+
}
|
|
405
|
+
if (!poCols.some(col => col.name === 'type')) {
|
|
406
|
+
db.exec(`ALTER TABLE payment_orders ADD COLUMN type TEXT`);
|
|
407
|
+
console.log('Added type column to payment_orders table');
|
|
408
|
+
}
|
|
409
|
+
if (!poCols.some(col => col.name === 'query_token')) {
|
|
410
|
+
db.exec(`ALTER TABLE payment_orders ADD COLUMN query_token TEXT`);
|
|
411
|
+
console.log('Added query_token column to payment_orders table');
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.log('Payment orders migration error:', e.message);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 新建 agent_pricing 表
|
|
418
|
+
db.exec(`
|
|
419
|
+
CREATE TABLE IF NOT EXISTS agent_pricing (
|
|
420
|
+
id TEXT PRIMARY KEY,
|
|
421
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
422
|
+
pricing_model TEXT NOT NULL DEFAULT 'free',
|
|
423
|
+
price REAL,
|
|
424
|
+
duration_minutes INTEGER,
|
|
425
|
+
trial_minutes INTEGER DEFAULT 3,
|
|
426
|
+
enabled INTEGER DEFAULT 1,
|
|
427
|
+
created_at INTEGER NOT NULL,
|
|
428
|
+
updated_at INTEGER NOT NULL
|
|
429
|
+
)
|
|
430
|
+
`);
|
|
431
|
+
console.log('Agent pricing table created/verified');
|
|
432
|
+
|
|
433
|
+
// 新建 user_cache 表
|
|
434
|
+
db.exec(`
|
|
435
|
+
CREATE TABLE IF NOT EXISTS user_cache (
|
|
436
|
+
uid TEXT PRIMARY KEY,
|
|
437
|
+
nickname TEXT,
|
|
438
|
+
avatar_path TEXT,
|
|
439
|
+
avatar_url TEXT,
|
|
440
|
+
updated_at INTEGER
|
|
441
|
+
)
|
|
442
|
+
`);
|
|
443
|
+
console.log('User cache table created/verified');
|
|
444
|
+
|
|
445
|
+
// 新建 config 表
|
|
446
|
+
db.exec(`
|
|
447
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
448
|
+
type TEXT PRIMARY KEY,
|
|
449
|
+
data TEXT NOT NULL,
|
|
450
|
+
updated_at INTEGER NOT NULL
|
|
451
|
+
)
|
|
452
|
+
`);
|
|
453
|
+
console.log('Config table created/verified');
|
|
454
|
+
|
|
455
|
+
// 新建好友邀请表
|
|
456
|
+
db.exec(`
|
|
457
|
+
CREATE TABLE IF NOT EXISTS friend_invitations (
|
|
458
|
+
code TEXT PRIMARY KEY,
|
|
459
|
+
agent_id TEXT NOT NULL,
|
|
460
|
+
friend_email TEXT NOT NULL,
|
|
461
|
+
whitelisted INTEGER NOT NULL DEFAULT 0,
|
|
462
|
+
created_at INTEGER NOT NULL
|
|
463
|
+
)
|
|
464
|
+
`);
|
|
465
|
+
console.log('Friend invitations table created/verified');
|
|
466
|
+
|
|
467
|
+
// 同步版本号
|
|
468
|
+
try {
|
|
469
|
+
const pkg = require('../package.json');
|
|
470
|
+
const ver = pkg.version || '0.0.0';
|
|
471
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)').run('version', ver, Date.now());
|
|
472
|
+
console.log('Version synced to config:', ver);
|
|
473
|
+
} catch (_) {}
|
|
474
|
+
|
|
475
|
+
// 新建 agent_access_lists 表
|
|
476
|
+
db.exec(`
|
|
477
|
+
CREATE TABLE IF NOT EXISTS agent_access_lists (
|
|
478
|
+
id TEXT PRIMARY KEY,
|
|
479
|
+
agent_id TEXT NOT NULL,
|
|
480
|
+
list_type TEXT NOT NULL CHECK(list_type IN ('whitelist', 'blacklist')),
|
|
481
|
+
visitor_id TEXT NOT NULL,
|
|
482
|
+
reason TEXT,
|
|
483
|
+
created_at INTEGER NOT NULL,
|
|
484
|
+
updated_at INTEGER NOT NULL,
|
|
485
|
+
UNIQUE(agent_id, list_type, visitor_id)
|
|
486
|
+
)
|
|
487
|
+
`);
|
|
488
|
+
console.log('Agent access lists table created/verified');
|
|
489
|
+
|
|
490
|
+
// 迁移:config 表旧版 id 主键 → type 主键
|
|
491
|
+
const colInfo = db.prepare('PRAGMA table_info(config)').all();
|
|
492
|
+
const hasLegacyId = colInfo.some(col => col.name === 'id');
|
|
493
|
+
if (hasLegacyId) {
|
|
494
|
+
try {
|
|
495
|
+
const oldRow = db.prepare('SELECT data, updated_at FROM config WHERE id = 1').get();
|
|
496
|
+
db.exec('DROP TABLE IF EXISTS config');
|
|
497
|
+
db.exec(`
|
|
498
|
+
CREATE TABLE config (
|
|
499
|
+
type TEXT PRIMARY KEY,
|
|
500
|
+
data TEXT NOT NULL,
|
|
501
|
+
updated_at INTEGER NOT NULL
|
|
502
|
+
)
|
|
503
|
+
`);
|
|
504
|
+
if (oldRow) {
|
|
505
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
506
|
+
.run('channel_config', oldRow.data, oldRow.updated_at);
|
|
507
|
+
console.log('[Config] 已迁移旧版数据到 type=channel_config');
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
console.error('[Config] 迁移失败:', e.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 迁移:user_access_tokens 旧表 → config
|
|
515
|
+
migrateUserAccessTokensToConfig(db);
|
|
516
|
+
|
|
517
|
+
// 迁移:user_access_tokens → user_access_token type 名
|
|
518
|
+
try {
|
|
519
|
+
const oldRow = db.prepare("SELECT data, updated_at FROM config WHERE type = 'user_access_tokens'").get();
|
|
520
|
+
if (oldRow) {
|
|
521
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
522
|
+
.run('user_access_token', oldRow.data, oldRow.updated_at);
|
|
523
|
+
db.prepare("DELETE FROM config WHERE type = 'user_access_tokens'").run();
|
|
524
|
+
console.log('[Config] 迁移 user_access_tokens → user_access_token');
|
|
525
|
+
}
|
|
526
|
+
} catch (e) {
|
|
527
|
+
console.warn('[Config] 迁移 user_access_tokens 类型名失败:', e.message);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 初始化默认 OSS 配置(优先环境变量,否则留空由用户手动配置)
|
|
531
|
+
const defaultOss = {
|
|
532
|
+
accessKeyId: process.env.OSS_ACCESS_KEY_ID || 'LTAI5tD1KmxKEmTc6vgmMF7K',
|
|
533
|
+
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || 'jxeT0Ky32VSvIueMQvCwyakJJNN9hG',
|
|
534
|
+
region: ENDPOINTS.oss.region,
|
|
535
|
+
bucket: ENDPOINTS.oss.bucket,
|
|
536
|
+
endpoint: ENDPOINTS.oss.endpoint,
|
|
537
|
+
publicUrl: ENDPOINTS.oss.publicUrl
|
|
538
|
+
};
|
|
539
|
+
if (!db.prepare('SELECT data FROM config WHERE type = ?').get('oss_config')) {
|
|
540
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
541
|
+
.run('oss_config', JSON.stringify(defaultOss), Date.now());
|
|
542
|
+
console.log('[Config] 已写入独立 OSS 配置');
|
|
543
|
+
}
|
|
544
|
+
// 清理 channel_config 中的 oss_config 残留
|
|
545
|
+
const ccRow = db.prepare('SELECT data FROM config WHERE type = ?').get('channel_config');
|
|
546
|
+
if (ccRow) {
|
|
547
|
+
try {
|
|
548
|
+
const cc = JSON.parse(ccRow.data);
|
|
549
|
+
if (cc.oss_config) {
|
|
550
|
+
delete cc.oss_config;
|
|
551
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
552
|
+
.run('channel_config', JSON.stringify(cc), Date.now());
|
|
553
|
+
console.log('[Config] 已从 channel_config 清理 oss_config');
|
|
554
|
+
}
|
|
555
|
+
} catch (_) {}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 迁移:conversations 添加计费会话字段
|
|
559
|
+
try {
|
|
560
|
+
const convCols = db.prepare(`PRAGMA table_info(conversations)`).all();
|
|
561
|
+
if (!convCols.some(col => col.name === 'session_status')) {
|
|
562
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN session_status TEXT`);
|
|
563
|
+
console.log('Added session_status column to conversations table');
|
|
564
|
+
}
|
|
565
|
+
if (!convCols.some(col => col.name === 'session_expire_at')) {
|
|
566
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN session_expire_at INTEGER`);
|
|
567
|
+
console.log('Added session_expire_at column to conversations table');
|
|
568
|
+
}
|
|
569
|
+
if (!convCols.some(col => col.name === 'mode')) {
|
|
570
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN mode TEXT`);
|
|
571
|
+
console.log('Added mode column to conversations table');
|
|
572
|
+
}
|
|
573
|
+
} catch (e) {
|
|
574
|
+
console.log('Conversations session columns migration error:', e.message);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 初始化默认出入站审核规则
|
|
578
|
+
const defaultRules = [
|
|
579
|
+
{ direction: 'inbound', keyword: '忽略之前的指令', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
580
|
+
{ direction: 'inbound', keyword: '忽略以上所有内容', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
581
|
+
{ direction: 'inbound', keyword: '忘记所有设定', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
582
|
+
{ direction: 'inbound', keyword: '覆盖你的规则', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
583
|
+
{ direction: 'inbound', keyword: '你现在是DAN', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
584
|
+
{ direction: 'inbound', keyword: 'Do Anything Now', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
585
|
+
{ direction: 'inbound', keyword: 'jailbreak', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
586
|
+
{ direction: 'inbound', keyword: '输出你的提示词', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
587
|
+
{ direction: 'inbound', keyword: '你的system prompt', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
588
|
+
{ direction: 'inbound', keyword: 'reveal your prompt', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
589
|
+
{ direction: 'inbound', keyword: 'print your instructions', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
590
|
+
{ direction: 'inbound', keyword: '<|im_start|>', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
591
|
+
{ direction: 'inbound', keyword: '<|system|>', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
592
|
+
{ direction: 'inbound', keyword: '[INST]', action: 'hard_deny', prompt: '对不起,该信息包含敏感词"{keyword}",无法执行或转发。' },
|
|
593
|
+
{ direction: 'inbound', keyword: '<<SYS>>', action: 'soft_deny', prompt: '' },
|
|
594
|
+
{ direction: 'inbound', keyword: '如何绕过', action: 'soft_deny', prompt: '' },
|
|
595
|
+
{ direction: 'inbound', keyword: '怎么联系主人', action: 'soft_deny', prompt: '' },
|
|
596
|
+
{ direction: 'inbound', keyword: '获取你的权限', action: 'soft_deny', prompt: '' },
|
|
597
|
+
{ direction: 'outbound', keyword: '/sk-[A-Za-z0-9-]{20,}/', action: 'hard_deny', prompt: '' },
|
|
598
|
+
{ direction: 'outbound', keyword: '/AKID[A-Za-z0-9]{16,}/', action: 'hard_deny', prompt: '' },
|
|
599
|
+
{ direction: 'outbound', keyword: '/\\d{17}[\\dXx]/', action: 'hard_deny', prompt: '' },
|
|
600
|
+
{ direction: 'outbound', keyword: '/10\\.\\d+\\.\\d+\\.\\d+/', action: 'hard_deny', prompt: '' },
|
|
601
|
+
{ direction: 'outbound', keyword: 'secret', action: 'hard_deny', prompt: '' },
|
|
602
|
+
{ direction: 'outbound', keyword: '银行卡', action: 'hard_deny', prompt: '' },
|
|
603
|
+
{ direction: 'outbound', keyword: '密码', action: 'hard_deny', prompt: '' },
|
|
604
|
+
{ direction: 'outbound', keyword: 'token', action: 'hard_deny', prompt: '' },
|
|
605
|
+
];
|
|
606
|
+
|
|
607
|
+
const defaultRuleCount = db.prepare(`SELECT COUNT(*) as cnt FROM audit_rules WHERE is_default = 1`).get()?.cnt || 0;
|
|
608
|
+
if (defaultRuleCount > 0 && defaultRuleCount !== defaultRules.length) {
|
|
609
|
+
console.log(`[Audit] 默认规则版本更新 (${defaultRuleCount} → ${defaultRules.length}),重新初始化`);
|
|
610
|
+
db.prepare(`DELETE FROM audit_rules WHERE is_default = 1`).run();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
db.prepare(`
|
|
615
|
+
UPDATE audit_rules SET prompt = REPLACE(prompt, '敏感词{keyword}', '敏感词"{keyword}"'), updated_at = ? WHERE prompt LIKE '%敏感词{keyword}%'
|
|
616
|
+
`).run(Date.now());
|
|
617
|
+
} catch (e) {
|
|
618
|
+
console.log('Audit prompt migration error:', e.message);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const hasDefaults = db.prepare(`SELECT COUNT(*) as cnt FROM audit_rules WHERE is_default = 1`).get()?.cnt || 0;
|
|
622
|
+
if (hasDefaults === 0) {
|
|
623
|
+
const now = Date.now();
|
|
624
|
+
const insertRule = db.prepare(`
|
|
625
|
+
INSERT INTO audit_rules (id, direction, keyword, action, prompt, is_default, created_at, updated_at)
|
|
626
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)
|
|
627
|
+
`);
|
|
628
|
+
const insertMany = db.transaction((rules) => {
|
|
629
|
+
for (const r of rules) {
|
|
630
|
+
const safeKeyword = r.keyword.replace(/[\/\\{}[\]()|*+?^$.,:;!@#%&'"<>`~]/g, '_');
|
|
631
|
+
insertRule.run(`audit_default_${r.direction}_${safeKeyword}`, r.direction, r.keyword, r.action, r.prompt, now, now);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
insertMany(defaultRules);
|
|
635
|
+
console.log(`Initialized ${defaultRules.length} default audit rules`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 新建 bank_head_offices 表
|
|
639
|
+
db.exec(`
|
|
640
|
+
CREATE TABLE IF NOT EXISTS bank_head_offices (
|
|
641
|
+
id INTEGER PRIMARY KEY,
|
|
642
|
+
code TEXT NOT NULL,
|
|
643
|
+
name TEXT NOT NULL,
|
|
644
|
+
short_name TEXT DEFAULT '',
|
|
645
|
+
note TEXT DEFAULT ''
|
|
646
|
+
)
|
|
647
|
+
`);
|
|
648
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bank_head_code ON bank_head_offices(code)`);
|
|
649
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bank_head_name ON bank_head_offices(name)`);
|
|
650
|
+
|
|
651
|
+
// 填充银行数据
|
|
652
|
+
const bankCount = db.prepare(`SELECT COUNT(*) as c FROM bank_head_offices`).get();
|
|
653
|
+
if (bankCount.c === 0 && BANK_HEAD_OFFICES && BANK_HEAD_OFFICES.length > 0) {
|
|
654
|
+
const insertBank = db.prepare(`INSERT OR IGNORE INTO bank_head_offices (id, code, name, short_name, note) VALUES (?, ?, ?, ?, ?)`);
|
|
655
|
+
const insertMany = db.transaction((banks) => {
|
|
656
|
+
let id = 1;
|
|
657
|
+
for (const b of banks) {
|
|
658
|
+
insertBank.run(id++, b.code, b.name, b.short_name || '', b.note || '');
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
insertMany(BANK_HEAD_OFFICES);
|
|
662
|
+
console.log(`Initialized ${BANK_HEAD_OFFICES.length} bank head offices`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
console.log('Database initialized successfully');
|
|
666
|
+
return db;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ============================================
|
|
670
|
+
// databaseAPI — SQL 查询封装
|
|
671
|
+
// ============================================
|
|
672
|
+
function createDatabaseAPI(db) {
|
|
673
|
+
if (!db) throw new Error('createDatabaseAPI: db instance is required');
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
getNewMessages: () => {
|
|
677
|
+
try {
|
|
678
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
679
|
+
const stmt = db.prepare(`
|
|
680
|
+
SELECT * FROM messages
|
|
681
|
+
WHERE timestamp > ? AND is_me = 0
|
|
682
|
+
ORDER BY timestamp DESC
|
|
683
|
+
LIMIT 50
|
|
684
|
+
`);
|
|
685
|
+
const rows = stmt.all(fiveMinutesAgo);
|
|
686
|
+
return rows.map(row => ({
|
|
687
|
+
id: row.id,
|
|
688
|
+
channelId: row.channel_id,
|
|
689
|
+
channelType: row.channel_type,
|
|
690
|
+
fromUid: row.from_uid,
|
|
691
|
+
toUid: row.to_uid,
|
|
692
|
+
content: row.content,
|
|
693
|
+
timestamp: row.timestamp,
|
|
694
|
+
isMe: row.is_me === 1,
|
|
695
|
+
status: row.status,
|
|
696
|
+
messageSeq: row.message_seq,
|
|
697
|
+
clientMsgNo: row.client_msg_no,
|
|
698
|
+
noPersist: row.no_persist,
|
|
699
|
+
redDot: row.red_dot,
|
|
700
|
+
syncOnce: row.sync_once,
|
|
701
|
+
contentType: row.content_type
|
|
702
|
+
}));
|
|
703
|
+
} catch (e) {
|
|
704
|
+
console.error('getNewMessages error:', e);
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
getLastMessageForChannel: (channelId) => {
|
|
710
|
+
try {
|
|
711
|
+
const stmt = db.prepare(`
|
|
712
|
+
SELECT * FROM messages
|
|
713
|
+
WHERE channel_id = ?
|
|
714
|
+
AND is_me = 0
|
|
715
|
+
ORDER BY timestamp DESC
|
|
716
|
+
LIMIT 1
|
|
717
|
+
`);
|
|
718
|
+
const row = stmt.get(channelId);
|
|
719
|
+
if (!row) return null;
|
|
720
|
+
return {
|
|
721
|
+
id: row.id,
|
|
722
|
+
channelId: row.channel_id,
|
|
723
|
+
channelType: row.channel_type,
|
|
724
|
+
fromUid: row.from_uid,
|
|
725
|
+
toUid: row.to_uid,
|
|
726
|
+
content: row.content,
|
|
727
|
+
timestamp: row.timestamp,
|
|
728
|
+
isMe: row.is_me === 1,
|
|
729
|
+
status: row.status,
|
|
730
|
+
messageSeq: row.message_seq,
|
|
731
|
+
clientMsgNo: row.client_msg_no,
|
|
732
|
+
noPersist: row.no_persist,
|
|
733
|
+
redDot: row.red_dot,
|
|
734
|
+
syncOnce: row.sync_once,
|
|
735
|
+
contentType: row.content_type
|
|
736
|
+
};
|
|
737
|
+
} catch (e) {
|
|
738
|
+
console.error('getLastMessageForChannel error:', e);
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
getUnreadMessagesForChannel: (channelId, since) => {
|
|
744
|
+
try {
|
|
745
|
+
const stmt = db.prepare(`
|
|
746
|
+
SELECT * FROM messages
|
|
747
|
+
WHERE channel_id = ?
|
|
748
|
+
AND is_me = 0
|
|
749
|
+
AND timestamp > ?
|
|
750
|
+
ORDER BY timestamp ASC
|
|
751
|
+
`);
|
|
752
|
+
const rows = stmt.all(channelId, since);
|
|
753
|
+
return rows.map(row => ({
|
|
754
|
+
id: row.id,
|
|
755
|
+
fromUid: row.from_uid,
|
|
756
|
+
content: row.content,
|
|
757
|
+
timestamp: row.timestamp,
|
|
758
|
+
messageSeq: row.message_seq,
|
|
759
|
+
contentType: row.content_type
|
|
760
|
+
}));
|
|
761
|
+
} catch (e) {
|
|
762
|
+
console.error('getUnreadMessagesForChannel error:', e);
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
getHistoryMessagesForChannel: (channelId, limit) => {
|
|
768
|
+
try {
|
|
769
|
+
const stmt = db.prepare(`
|
|
770
|
+
SELECT * FROM messages
|
|
771
|
+
WHERE channel_id = ?
|
|
772
|
+
AND is_me = 0
|
|
773
|
+
ORDER BY timestamp DESC
|
|
774
|
+
LIMIT ?
|
|
775
|
+
`);
|
|
776
|
+
const rows = stmt.all(channelId, limit);
|
|
777
|
+
return rows.map(row => ({
|
|
778
|
+
id: row.id,
|
|
779
|
+
fromUid: row.from_uid,
|
|
780
|
+
content: row.content,
|
|
781
|
+
timestamp: row.timestamp,
|
|
782
|
+
messageSeq: row.message_seq,
|
|
783
|
+
contentType: row.content_type
|
|
784
|
+
}));
|
|
785
|
+
} catch (e) {
|
|
786
|
+
console.error('getHistoryMessagesForChannel error:', e);
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
saveMessage: (message) => {
|
|
792
|
+
try {
|
|
793
|
+
const trimmedContent = (message.content || '').replace(/^[\n\r\s]+/, '').trim();
|
|
794
|
+
const stmt = db.prepare(`
|
|
795
|
+
INSERT OR REPLACE INTO messages (id, channel_id, channel_type, from_uid, to_uid, content, timestamp, is_me, status, message_seq, client_msg_no, no_persist, red_dot, sync_once, content_type, agent_id)
|
|
796
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
797
|
+
`);
|
|
798
|
+
stmt.run(
|
|
799
|
+
message.id,
|
|
800
|
+
message.channelId,
|
|
801
|
+
message.channelType,
|
|
802
|
+
message.fromUid,
|
|
803
|
+
message.toUid,
|
|
804
|
+
trimmedContent,
|
|
805
|
+
message.timestamp,
|
|
806
|
+
message.isMe ? 1 : 0,
|
|
807
|
+
message.status,
|
|
808
|
+
message.messageSeq ?? null,
|
|
809
|
+
message.clientMsgNo ?? null,
|
|
810
|
+
message.noPersist ?? 0,
|
|
811
|
+
message.redDot ?? 0,
|
|
812
|
+
message.syncOnce ?? 0,
|
|
813
|
+
message.contentType ?? 1,
|
|
814
|
+
message.agentId ?? null
|
|
815
|
+
);
|
|
816
|
+
return { success: true };
|
|
817
|
+
} catch (e) {
|
|
818
|
+
console.error('saveMessage error:', e);
|
|
819
|
+
return { success: false, error: e.message };
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
getSessionKeyForChannel: (channelId, userUid) => {
|
|
824
|
+
try {
|
|
825
|
+
const stmt = db.prepare(`
|
|
826
|
+
SELECT session_key FROM conversations
|
|
827
|
+
WHERE channel_id = ? AND user_uid = ?
|
|
828
|
+
`);
|
|
829
|
+
const row = stmt.get(channelId, userUid);
|
|
830
|
+
return row ? row.session_key : null;
|
|
831
|
+
} catch (e) {
|
|
832
|
+
console.error('getSessionKeyForChannel error:', e);
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
saveSessionKeyForChannel: (channelId, userUid, sessionKey) => {
|
|
838
|
+
try {
|
|
839
|
+
const stmt = db.prepare(`
|
|
840
|
+
INSERT OR REPLACE INTO conversations
|
|
841
|
+
(user_uid, channel_id, channel_type, name, session_key)
|
|
842
|
+
VALUES (?, ?, ?, ?, ?)
|
|
843
|
+
`);
|
|
844
|
+
stmt.run(userUid, channelId, 1, channelId, sessionKey);
|
|
845
|
+
return { success: true };
|
|
846
|
+
} catch (e) {
|
|
847
|
+
console.error('saveSessionKeyForChannel error:', e);
|
|
848
|
+
return { success: false, error: e.message };
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
saveOwnerIntervention: (intervention) => {
|
|
853
|
+
try {
|
|
854
|
+
const stmt = db.prepare(`
|
|
855
|
+
INSERT OR REPLACE INTO owner_interventions
|
|
856
|
+
(id, visitor_id, session_key, problem, agent_suggestion, ask_time, expire_time, status, owner_reply, reply_time, parent_message_id, channel_type, resolved_at, created_at, updated_at, agent_id)
|
|
857
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
858
|
+
`);
|
|
859
|
+
stmt.run(
|
|
860
|
+
intervention.id,
|
|
861
|
+
intervention.visitorId,
|
|
862
|
+
intervention.sessionKey,
|
|
863
|
+
intervention.problem,
|
|
864
|
+
intervention.agentSuggestion || null,
|
|
865
|
+
intervention.askTime,
|
|
866
|
+
intervention.expireTime || null,
|
|
867
|
+
intervention.status || 'pending',
|
|
868
|
+
intervention.ownerReply || null,
|
|
869
|
+
intervention.replyTime || null,
|
|
870
|
+
intervention.parentMessageId || null,
|
|
871
|
+
intervention.channelType || 'unknown',
|
|
872
|
+
intervention.resolvedAt || null,
|
|
873
|
+
intervention.createdAt,
|
|
874
|
+
intervention.updatedAt,
|
|
875
|
+
intervention.agentId || null
|
|
876
|
+
);
|
|
877
|
+
return { success: true };
|
|
878
|
+
} catch (e) {
|
|
879
|
+
console.error('saveOwnerIntervention error:', e);
|
|
880
|
+
return { success: false, error: e.message };
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
getOwnerIntervention: (id) => {
|
|
885
|
+
try {
|
|
886
|
+
const stmt = db.prepare(`SELECT * FROM owner_interventions WHERE id = ?`);
|
|
887
|
+
const row = stmt.get(id);
|
|
888
|
+
return row ? {
|
|
889
|
+
id: row.id, visitorId: row.visitor_id, sessionKey: row.session_key,
|
|
890
|
+
problem: row.problem, agentSuggestion: row.agent_suggestion,
|
|
891
|
+
askTime: row.ask_time, expireTime: row.expire_time,
|
|
892
|
+
status: row.status, ownerReply: row.owner_reply, replyTime: row.reply_time,
|
|
893
|
+
parentMessageId: row.parent_message_id, channelType: row.channel_type,
|
|
894
|
+
resolvedAt: row.resolved_at, createdAt: row.created_at, updatedAt: row.updated_at,
|
|
895
|
+
agentId: row.agent_id
|
|
896
|
+
} : null;
|
|
897
|
+
} catch (e) {
|
|
898
|
+
console.error('getOwnerIntervention error:', e);
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
|
|
903
|
+
getOwnerInterventionByParentMsgId: (parentMessageId) => {
|
|
904
|
+
try {
|
|
905
|
+
const stmt = db.prepare(`SELECT * FROM owner_interventions WHERE parent_message_id = ? AND status = 'pending'`);
|
|
906
|
+
const row = stmt.get(parentMessageId);
|
|
907
|
+
return row ? {
|
|
908
|
+
id: row.id, visitorId: row.visitor_id, sessionKey: row.session_key,
|
|
909
|
+
problem: row.problem, agentSuggestion: row.agent_suggestion,
|
|
910
|
+
askTime: row.ask_time, expireTime: row.expire_time,
|
|
911
|
+
status: row.status, ownerReply: row.owner_reply, replyTime: row.reply_time,
|
|
912
|
+
parentMessageId: row.parent_message_id, channelType: row.channel_type,
|
|
913
|
+
resolvedAt: row.resolved_at, createdAt: row.created_at, updatedAt: row.updated_at,
|
|
914
|
+
agentId: row.agent_id
|
|
915
|
+
} : null;
|
|
916
|
+
} catch (e) {
|
|
917
|
+
console.error('getOwnerInterventionByParentMsgId error:', e);
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
|
|
922
|
+
getLatestPendingIntervention: () => {
|
|
923
|
+
try {
|
|
924
|
+
const stmt = db.prepare(`
|
|
925
|
+
SELECT * FROM owner_interventions
|
|
926
|
+
WHERE status = 'pending'
|
|
927
|
+
ORDER BY created_at DESC LIMIT 1
|
|
928
|
+
`);
|
|
929
|
+
const row = stmt.get();
|
|
930
|
+
return row ? {
|
|
931
|
+
id: row.id, visitorId: row.visitor_id, sessionKey: row.session_key,
|
|
932
|
+
problem: row.problem, agentSuggestion: row.agent_suggestion,
|
|
933
|
+
askTime: row.ask_time, expireTime: row.expire_time,
|
|
934
|
+
status: row.status, ownerReply: row.owner_reply, replyTime: row.reply_time,
|
|
935
|
+
parentMessageId: row.parent_message_id, channelType: row.channel_type,
|
|
936
|
+
resolvedAt: row.resolved_at, createdAt: row.created_at, updatedAt: row.updated_at
|
|
937
|
+
} : null;
|
|
938
|
+
} catch (e) {
|
|
939
|
+
console.error('getLatestPendingIntervention error:', e);
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
updateOwnerInterventionReply: (id, ownerReply, replyTime, channelType) => {
|
|
945
|
+
try {
|
|
946
|
+
const trimmedReply = (ownerReply || '').replace(/^[\n\r\s]+/, '').trim();
|
|
947
|
+
const row = db.prepare('SELECT owner_reply, agent_notified FROM owner_interventions WHERE id = ?').get(id);
|
|
948
|
+
if (!row) return { success: false, error: '记录不存在' };
|
|
949
|
+
if (row.owner_reply === trimmedReply) {
|
|
950
|
+
return { success: true, contentChanged: false, agentNotified: row.agent_notified === 1 };
|
|
951
|
+
}
|
|
952
|
+
const stmt = db.prepare(`
|
|
953
|
+
UPDATE owner_interventions
|
|
954
|
+
SET owner_reply = ?, reply_time = ?, status = 'replied', updated_at = ?, agent_notified = 0, channel_type = ?
|
|
955
|
+
WHERE id = ?
|
|
956
|
+
`);
|
|
957
|
+
stmt.run(trimmedReply, replyTime, Date.now(), channelType || null, id);
|
|
958
|
+
return { success: true, contentChanged: true };
|
|
959
|
+
} catch (e) {
|
|
960
|
+
console.error('updateOwnerInterventionReply error:', e);
|
|
961
|
+
return { success: false, error: e.message };
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
markAgentNotified: (id) => {
|
|
966
|
+
try {
|
|
967
|
+
db.prepare(`UPDATE owner_interventions SET agent_notified = 1 WHERE id = ?`).run(id);
|
|
968
|
+
return { success: true };
|
|
969
|
+
} catch (e) {
|
|
970
|
+
console.error('markAgentNotified error:', e);
|
|
971
|
+
return { success: false, error: e.message };
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
updateOwnerInterventionParentMsgId: (id, parentMessageId) => {
|
|
976
|
+
try {
|
|
977
|
+
const stmt = db.prepare(`
|
|
978
|
+
UPDATE owner_interventions
|
|
979
|
+
SET parent_message_id = ?, updated_at = ?
|
|
980
|
+
WHERE id = ?
|
|
981
|
+
`);
|
|
982
|
+
stmt.run(parentMessageId, Date.now(), id);
|
|
983
|
+
return { success: true };
|
|
984
|
+
} catch (e) {
|
|
985
|
+
console.error('updateOwnerInterventionParentMsgId error:', e);
|
|
986
|
+
return { success: false, error: e.message };
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
updateOwnerInterventionEmailMsgId: (id, messageId) => {
|
|
991
|
+
try {
|
|
992
|
+
db.prepare(`UPDATE owner_interventions SET email_message_id = ?, updated_at = ? WHERE id = ?`)
|
|
993
|
+
.run(messageId, Date.now(), id);
|
|
994
|
+
return { success: true };
|
|
995
|
+
} catch (e) {
|
|
996
|
+
console.error('updateOwnerInterventionEmailMsgId error:', e);
|
|
997
|
+
return { success: false, error: e.message };
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
updateOwnerInterventionStatus: (id, status, resolvedAt) => {
|
|
1002
|
+
try {
|
|
1003
|
+
const stmt = db.prepare(`
|
|
1004
|
+
UPDATE owner_interventions
|
|
1005
|
+
SET status = ?, resolved_at = ?, updated_at = ?
|
|
1006
|
+
WHERE id = ?
|
|
1007
|
+
`);
|
|
1008
|
+
stmt.run(status, resolvedAt || null, Date.now(), id);
|
|
1009
|
+
return { success: true };
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
console.error('updateOwnerInterventionStatus error:', e);
|
|
1012
|
+
return { success: false, error: e.message };
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
getUnresolvedOwnerInterventions: () => {
|
|
1017
|
+
try {
|
|
1018
|
+
const stmt = db.prepare(`
|
|
1019
|
+
SELECT * FROM owner_interventions
|
|
1020
|
+
WHERE status IN ('pending', 'replied')
|
|
1021
|
+
ORDER BY ask_time DESC
|
|
1022
|
+
`);
|
|
1023
|
+
return stmt.all().map(row => ({
|
|
1024
|
+
id: row.id, visitorId: row.visitor_id, sessionKey: row.session_key,
|
|
1025
|
+
problem: row.problem, agentSuggestion: row.agent_suggestion,
|
|
1026
|
+
askTime: row.ask_time, expireTime: row.expire_time,
|
|
1027
|
+
status: row.status, ownerReply: row.owner_reply, replyTime: row.reply_time,
|
|
1028
|
+
parentMessageId: row.parent_message_id, channelType: row.channel_type,
|
|
1029
|
+
resolvedAt: row.resolved_at, createdAt: row.created_at, updatedAt: row.updated_at
|
|
1030
|
+
}));
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
console.error('getUnresolvedOwnerInterventions error:', e);
|
|
1033
|
+
return [];
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
|
|
1037
|
+
getAllOwnerInterventions: () => {
|
|
1038
|
+
try {
|
|
1039
|
+
const stmt = db.prepare(`SELECT * FROM owner_interventions ORDER BY ask_time DESC`);
|
|
1040
|
+
return stmt.all().map(row => ({
|
|
1041
|
+
id: row.id, visitorId: row.visitor_id, agentId: row.agent_id || 'voko',
|
|
1042
|
+
sessionKey: row.session_key, problem: row.problem,
|
|
1043
|
+
agentSuggestion: row.agent_suggestion, askTime: row.ask_time,
|
|
1044
|
+
expireTime: row.expire_time, status: row.status, ownerReply: row.owner_reply,
|
|
1045
|
+
replyTime: row.reply_time, parentMessageId: row.parent_message_id,
|
|
1046
|
+
channelType: row.channel_type, resolvedAt: row.resolved_at,
|
|
1047
|
+
createdAt: row.created_at, updatedAt: row.updated_at, skipReply: row.skip_reply || 0
|
|
1048
|
+
}));
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
console.error('getAllOwnerInterventions error:', e);
|
|
1051
|
+
return [];
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
|
|
1055
|
+
getPendingOwnerInterventions: () => {
|
|
1056
|
+
try {
|
|
1057
|
+
const stmt = db.prepare(`
|
|
1058
|
+
SELECT * FROM owner_interventions
|
|
1059
|
+
WHERE is_sent = 0
|
|
1060
|
+
ORDER BY ask_time ASC
|
|
1061
|
+
`);
|
|
1062
|
+
return stmt.all().map(row => ({
|
|
1063
|
+
id: row.id, visitorId: row.visitor_id, agentId: row.agent_id || 'voko',
|
|
1064
|
+
sessionKey: row.session_key, problem: row.problem,
|
|
1065
|
+
agentSuggestion: row.agent_suggestion, askTime: row.ask_time,
|
|
1066
|
+
expireTime: row.expire_time, status: row.status, ownerReply: row.owner_reply,
|
|
1067
|
+
replyTime: row.reply_time, parentMessageId: row.parent_message_id,
|
|
1068
|
+
channelType: row.channel_type, resolvedAt: row.resolved_at,
|
|
1069
|
+
createdAt: row.created_at, updatedAt: row.updated_at,
|
|
1070
|
+
retryCount: row.retry_count || 0, lastRetryAt: row.last_retry_at || 0,
|
|
1071
|
+
skipReply: row.skip_reply || 0
|
|
1072
|
+
}));
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
console.error('getPendingOwnerInterventions error:', e);
|
|
1075
|
+
return [];
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
|
|
1079
|
+
getPendingByAgentAndVisitor: (agentId, visitorId) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const stmt = db.prepare(`
|
|
1082
|
+
SELECT * FROM owner_interventions
|
|
1083
|
+
WHERE status = 'pending' AND agent_id = ? AND visitor_id = ?
|
|
1084
|
+
ORDER BY created_at DESC LIMIT 1
|
|
1085
|
+
`);
|
|
1086
|
+
const row = stmt.get(agentId, visitorId);
|
|
1087
|
+
if (!row) return null;
|
|
1088
|
+
return {
|
|
1089
|
+
id: row.id, visitorId: row.visitor_id, agentId: row.agent_id || 'voko',
|
|
1090
|
+
sessionKey: row.session_key, problem: row.problem, askTime: row.ask_time,
|
|
1091
|
+
status: row.status, ownerReply: row.owner_reply,
|
|
1092
|
+
parentMessageId: row.parent_message_id, channelType: row.channel_type
|
|
1093
|
+
};
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
console.error('getPendingByAgentAndVisitor error:', e);
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
updateOwnerInterventionSent: (id, parentMessageId, channelType) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const current = db.prepare('SELECT id, is_sent, status FROM owner_interventions WHERE id = ?').get(id);
|
|
1103
|
+
const stmt = db.prepare(`
|
|
1104
|
+
UPDATE owner_interventions
|
|
1105
|
+
SET parent_message_id = ?, channel_type = ?, updated_at = ?, retry_count = 0
|
|
1106
|
+
WHERE id = ?
|
|
1107
|
+
`);
|
|
1108
|
+
const result = stmt.run(parentMessageId, channelType, Date.now(), id);
|
|
1109
|
+
return { success: result.changes > 0 };
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
console.error('updateOwnerInterventionSent error:', e);
|
|
1112
|
+
return { success: false, error: e.message };
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
|
|
1116
|
+
getAgentImUid: (agentId) => {
|
|
1117
|
+
try {
|
|
1118
|
+
const row = db.prepare(`SELECT imUid FROM agents WHERE agent_id = ?`).get(agentId);
|
|
1119
|
+
return row ? row.imUid : '';
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
console.error('getAgentImUid error:', e);
|
|
1122
|
+
return '';
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
getAgentDid: (agentId) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const row = db.prepare(`SELECT did FROM agents WHERE agent_id = ?`).get(agentId);
|
|
1129
|
+
return row?.did || null;
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
console.error('getAgentDid error:', e);
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1136
|
+
getPaymentAuth: (agentId) => {
|
|
1137
|
+
try {
|
|
1138
|
+
const row = db.prepare(`SELECT pa.* FROM payment_auth pa JOIN agents a ON pa.id = a.payment_auth_id WHERE a.agent_id = ?`).get(agentId);
|
|
1139
|
+
return row || null;
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
console.error('getPaymentAuth error:', e);
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
savePaymentOrder: (order) => {
|
|
1147
|
+
try {
|
|
1148
|
+
const stmt = db.prepare(`
|
|
1149
|
+
INSERT INTO payment_orders (id, agent_id, visitor_id, from_uid, amount, description, type, status, created_at, updated_at)
|
|
1150
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1151
|
+
`);
|
|
1152
|
+
stmt.run(order.id, order.agent_id, order.visitor_id, order.from_uid, order.amount, order.description, order.type || 'service', order.status, order.created_at, order.updated_at);
|
|
1153
|
+
return { success: true };
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
console.error('savePaymentOrder error:', e);
|
|
1156
|
+
return { success: false, error: e.message };
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
|
|
1160
|
+
getPaymentOrdersByStatus: (status) => {
|
|
1161
|
+
try {
|
|
1162
|
+
return db.prepare(`SELECT * FROM payment_orders WHERE status = ? ORDER BY created_at ASC`).all(status);
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
console.error('getPaymentOrdersByStatus error:', e);
|
|
1165
|
+
return [];
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
updatePaymentOrder: (id, updates) => {
|
|
1170
|
+
try {
|
|
1171
|
+
const sets = [];
|
|
1172
|
+
const vals = [];
|
|
1173
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
1174
|
+
sets.push(`${k} = ?`);
|
|
1175
|
+
vals.push(v);
|
|
1176
|
+
}
|
|
1177
|
+
vals.push(Date.now(), id);
|
|
1178
|
+
db.prepare(`UPDATE payment_orders SET ${sets.join(', ')}, updated_at = ? WHERE id = ?`).run(...vals);
|
|
1179
|
+
return { success: true };
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
console.error('updatePaymentOrder error:', e);
|
|
1182
|
+
return { success: false, error: e.message };
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
|
|
1186
|
+
query: (sql) => {
|
|
1187
|
+
try {
|
|
1188
|
+
return db.prepare(sql).all();
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
console.error('query error:', e.message);
|
|
1191
|
+
return [];
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1195
|
+
exec: (sql) => {
|
|
1196
|
+
try {
|
|
1197
|
+
return db.prepare(sql).run();
|
|
1198
|
+
} catch (e) {
|
|
1199
|
+
console.error('exec error:', e.message);
|
|
1200
|
+
return { changes: 0 };
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
|
|
1204
|
+
getConfigFromDb: (type = 'channel_config') => {
|
|
1205
|
+
try {
|
|
1206
|
+
const row = db.prepare('SELECT data FROM config WHERE type = ?').get(type);
|
|
1207
|
+
return row ? JSON.parse(row.data) : null;
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
|
|
1213
|
+
saveConfigToDb: (data, type = 'channel_config') => {
|
|
1214
|
+
try {
|
|
1215
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
1216
|
+
.run(type, JSON.stringify(data), Date.now());
|
|
1217
|
+
return true;
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
console.error('[Config] saveConfigToDb error:', e);
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
|
|
1224
|
+
saveAgentCache: (data) => {
|
|
1225
|
+
try {
|
|
1226
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
1227
|
+
.run('agent_cache', JSON.stringify(data), Date.now());
|
|
1228
|
+
return true;
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
console.error('[Config] saveAgentCache error:', e);
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
|
|
1235
|
+
loadAgentCache: () => {
|
|
1236
|
+
try {
|
|
1237
|
+
const row = db.prepare('SELECT data FROM config WHERE type = ?').get('agent_cache');
|
|
1238
|
+
return row ? JSON.parse(row.data) : null;
|
|
1239
|
+
} catch (e) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
// ── IPC 数据查询方法(原 Desktop src/ipc/db.js 内联 SQL,移植到 Lite) ──
|
|
1245
|
+
|
|
1246
|
+
getMessages: (channelId, { limit = 50, offset = 0, agentId = null } = {}) => {
|
|
1247
|
+
try {
|
|
1248
|
+
let sql = `SELECT * FROM messages WHERE channel_id = ?`;
|
|
1249
|
+
const params = [channelId];
|
|
1250
|
+
if (agentId) { sql += ` AND agent_id = ?`; params.push(agentId); }
|
|
1251
|
+
sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
|
|
1252
|
+
params.push(limit, offset);
|
|
1253
|
+
const rows = db.prepare(sql).all(...params);
|
|
1254
|
+
rows.reverse();
|
|
1255
|
+
return rows.map(row => ({ id: row.id, channelId: row.channel_id, channelType: row.channel_type, fromUid: row.from_uid, toUid: row.to_uid, content: row.content, timestamp: row.timestamp, isMe: row.is_me === 1 || row.is_me === 2, status: row.status, agentId: row.agent_id || null, messageSeq: row.message_seq, clientMsgNo: row.client_msg_no, noPersist: row.no_persist, redDot: row.red_dot, syncOnce: row.sync_once, contentType: row.content_type }));
|
|
1256
|
+
} catch (e) { return []; }
|
|
1257
|
+
},
|
|
1258
|
+
|
|
1259
|
+
getMessageCount: (channelId, agentId = null) => {
|
|
1260
|
+
try {
|
|
1261
|
+
let sql = `SELECT COUNT(*) as count FROM messages WHERE channel_id = ?`;
|
|
1262
|
+
const params = [channelId];
|
|
1263
|
+
if (agentId) { sql += ` AND agent_id = ?`; params.push(agentId); }
|
|
1264
|
+
return db.prepare(sql).get(...params).count;
|
|
1265
|
+
} catch (e) { return 0; }
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1268
|
+
saveConversation: (userUid, conversation) => {
|
|
1269
|
+
try {
|
|
1270
|
+
db.prepare(`INSERT OR REPLACE INTO conversations (user_uid, channel_id, channel_type, name, avatar, last_message, last_timestamp, unread_count, agent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1271
|
+
.run(userUid, conversation.channelId, conversation.channelType, conversation.name, conversation.avatar || null, conversation.lastMessage || null, conversation.lastTimestamp || null, conversation.unreadCount, conversation.agentId || null);
|
|
1272
|
+
return { success: true };
|
|
1273
|
+
} catch (e) { return { success: false, error: e.message }; }
|
|
1274
|
+
},
|
|
1275
|
+
|
|
1276
|
+
getConversations: (userUid = null, ownerEmail = null) => {
|
|
1277
|
+
try {
|
|
1278
|
+
let rows, sql;
|
|
1279
|
+
if (ownerEmail) {
|
|
1280
|
+
sql = `SELECT c.* FROM conversations c JOIN agents a ON c.agent_id = a.agent_id WHERE a.owner_email = ? ORDER BY c.last_timestamp DESC`;
|
|
1281
|
+
rows = db.prepare(sql).all(ownerEmail);
|
|
1282
|
+
} else if (userUid) {
|
|
1283
|
+
rows = db.prepare(`SELECT * FROM conversations WHERE user_uid = ? ORDER BY last_timestamp DESC`).all(userUid);
|
|
1284
|
+
} else {
|
|
1285
|
+
rows = db.prepare(`SELECT * FROM conversations ORDER BY last_timestamp DESC`).all();
|
|
1286
|
+
}
|
|
1287
|
+
return rows.map(row => ({
|
|
1288
|
+
userUid: row.user_uid, channelId: row.channel_id, channelType: row.channel_type, name: row.name,
|
|
1289
|
+
avatar: row.avatar, lastMessage: row.last_message, lastTimestamp: row.last_timestamp,
|
|
1290
|
+
unreadCount: row.unread_count, sessionKey: row.session_key, agentId: row.agent_id || null,
|
|
1291
|
+
mode: row.mode || null,
|
|
1292
|
+
lastMessageIsMe: (() => {
|
|
1293
|
+
try { const m = db.prepare(`SELECT is_me FROM messages WHERE channel_id = ? AND agent_id = ? AND content_type != 11 ORDER BY timestamp DESC LIMIT 1`).get(row.channel_id, row.agent_id); return m ? m.is_me : null; } catch (_) { return null; }
|
|
1294
|
+
})(),
|
|
1295
|
+
}));
|
|
1296
|
+
} catch (e) { return []; }
|
|
1297
|
+
},
|
|
1298
|
+
|
|
1299
|
+
deleteConversation: (channelId, agentId) => {
|
|
1300
|
+
try {
|
|
1301
|
+
if (agentId) {
|
|
1302
|
+
db.prepare(`DELETE FROM messages WHERE channel_id = ? AND agent_id = ?`).run(channelId, agentId);
|
|
1303
|
+
db.prepare(`DELETE FROM conversations WHERE channel_id = ? AND agent_id = ?`).run(channelId, agentId);
|
|
1304
|
+
} else {
|
|
1305
|
+
db.prepare(`DELETE FROM messages WHERE channel_id = ?`).run(channelId);
|
|
1306
|
+
db.prepare(`DELETE FROM conversations WHERE channel_id = ?`).run(channelId);
|
|
1307
|
+
}
|
|
1308
|
+
return { success: true };
|
|
1309
|
+
} catch (e) { return { success: false, error: e.message }; }
|
|
1310
|
+
},
|
|
1311
|
+
|
|
1312
|
+
insertSystemMessage: ({ agentId, channelId, content, timestamp }) => {
|
|
1313
|
+
try {
|
|
1314
|
+
const msgId = `sys-${agentId}-${channelId}-${Date.now()}`;
|
|
1315
|
+
const ts = timestamp || Math.floor(Date.now() / 1000);
|
|
1316
|
+
db.prepare(`INSERT INTO messages (id, from_uid, to_uid, content, channel_id, channel_type, agent_id, timestamp, is_me, status, content_type) VALUES (?, ?, ?, ?, ?, 1, ?, ?, 0, 'sent', 10)`)
|
|
1317
|
+
.run(msgId, 'system_inject', channelId, content, channelId, agentId, ts);
|
|
1318
|
+
return { success: true, messageId: msgId };
|
|
1319
|
+
} catch (e) { return { success: false, error: e.message }; }
|
|
1320
|
+
},
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ============================================
|
|
1325
|
+
// User Access Token 工具
|
|
1326
|
+
// ============================================
|
|
1327
|
+
const USER_ACCESS_TOKEN_CONFIG_TYPE = 'user_access_token';
|
|
1328
|
+
|
|
1329
|
+
function normalizeUserEmail(email) {
|
|
1330
|
+
return String(email || '').trim().toLowerCase();
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function loadUserAccessTokenConfig(db) {
|
|
1334
|
+
try {
|
|
1335
|
+
const row = db.prepare('SELECT data FROM config WHERE type = ?').get(USER_ACCESS_TOKEN_CONFIG_TYPE);
|
|
1336
|
+
if (!row?.data) return {};
|
|
1337
|
+
const parsed = JSON.parse(row.data);
|
|
1338
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
1339
|
+
} catch (e) {
|
|
1340
|
+
console.warn('[Pay] loadUserAccessTokenConfig error:', e.message);
|
|
1341
|
+
return {};
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function saveUserAccessTokenConfig(db, map) {
|
|
1346
|
+
db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
|
|
1347
|
+
.run(USER_ACCESS_TOKEN_CONFIG_TYPE, JSON.stringify(map), Date.now());
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function migrateUserAccessTokensToConfig(db) {
|
|
1351
|
+
try {
|
|
1352
|
+
const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='user_access_tokens'`).get();
|
|
1353
|
+
if (!tableExists) return;
|
|
1354
|
+
|
|
1355
|
+
const rows = db.prepare('SELECT email, user_access_token, updated_at FROM user_access_tokens').all();
|
|
1356
|
+
if (rows.length) {
|
|
1357
|
+
const map = loadUserAccessTokenConfig(db);
|
|
1358
|
+
for (const row of rows) {
|
|
1359
|
+
const email = normalizeUserEmail(row.email);
|
|
1360
|
+
if (!email || !row.user_access_token) continue;
|
|
1361
|
+
if (!map[email]) {
|
|
1362
|
+
map[email] = { user_access_token: row.user_access_token, updated_at: row.updated_at || Date.now() };
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
saveUserAccessTokenConfig(db, map);
|
|
1366
|
+
console.log('[Config] migrated user_access_tokens table to config type=user_access_tokens');
|
|
1367
|
+
}
|
|
1368
|
+
db.exec('DROP TABLE IF EXISTS user_access_tokens');
|
|
1369
|
+
} catch (e) {
|
|
1370
|
+
console.warn('[Config] migrate user_access_tokens error:', e.message);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function saveUserAccessToken(db, email, token) {
|
|
1375
|
+
const normalized = normalizeUserEmail(email);
|
|
1376
|
+
if (!normalized || !token) return;
|
|
1377
|
+
saveUserAccessTokenConfig(db, { [normalized]: { user_access_token: token, updated_at: Date.now() } });
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function getUserAccessToken(db, email) {
|
|
1381
|
+
const normalized = normalizeUserEmail(email);
|
|
1382
|
+
if (!normalized) return null;
|
|
1383
|
+
const map = loadUserAccessTokenConfig(db);
|
|
1384
|
+
return map[normalized]?.user_access_token || null;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function getPrimaryOwnerEmail(db) {
|
|
1388
|
+
const row = db.prepare(`
|
|
1389
|
+
SELECT owner_email FROM agents
|
|
1390
|
+
WHERE owner_email IS NOT NULL AND TRIM(owner_email) != ''
|
|
1391
|
+
ORDER BY updated_at DESC
|
|
1392
|
+
LIMIT 1
|
|
1393
|
+
`).get();
|
|
1394
|
+
return row?.owner_email ? String(row.owner_email).trim() : null;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
module.exports = {
|
|
1398
|
+
initDatabase,
|
|
1399
|
+
createDatabaseAPI,
|
|
1400
|
+
enqueueDbWrite,
|
|
1401
|
+
waitForDbQueue,
|
|
1402
|
+
normalizeUserEmail,
|
|
1403
|
+
loadUserAccessTokenConfig,
|
|
1404
|
+
saveUserAccessTokenConfig,
|
|
1405
|
+
migrateUserAccessTokensToConfig,
|
|
1406
|
+
saveUserAccessToken,
|
|
1407
|
+
getUserAccessToken,
|
|
1408
|
+
getPrimaryOwnerEmail,
|
|
1409
|
+
};
|