@seasonkoh/webaz 0.1.27 → 0.1.28
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/README.md +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +234 -54
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +9956 -18841
- package/dist/pwa/public/i18n.js +16 -0
- package/dist/pwa/public/index.html +10 -0
- package/dist/pwa/public/openapi.json +85 -1
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/webauthn.js +6 -1
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +157 -1559
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1848 -0
- package/package.json +11 -2
|
@@ -0,0 +1,1848 @@
|
|
|
1
|
+
// ─── 验证员白名单表 ───────────────────────────────────────────────
|
|
2
|
+
export function initVerifierWhitelistSchema(db) {
|
|
3
|
+
db.exec(`
|
|
4
|
+
CREATE TABLE IF NOT EXISTS verifier_whitelist (
|
|
5
|
+
user_id TEXT PRIMARY KEY,
|
|
6
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
7
|
+
note TEXT
|
|
8
|
+
)
|
|
9
|
+
`);
|
|
10
|
+
}
|
|
11
|
+
// ─── MCP 工具调用埋点表(远程上报)─────────────────────────────────
|
|
12
|
+
export function initMcpToolCallsSchema(db) {
|
|
13
|
+
db.exec(`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS mcp_tool_calls (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
tool_name TEXT NOT NULL,
|
|
17
|
+
user_id_hash TEXT,
|
|
18
|
+
server_version TEXT,
|
|
19
|
+
outcome TEXT NOT NULL,
|
|
20
|
+
latency_ms INTEGER NOT NULL,
|
|
21
|
+
ts TEXT NOT NULL DEFAULT (datetime('now'))
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tc_ts ON mcp_tool_calls(ts)`);
|
|
25
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tc_tool ON mcp_tool_calls(tool_name, ts)`);
|
|
26
|
+
}
|
|
27
|
+
// ─── 笔记图片 hash 索引表(审计修 C-1)─────────────────────────────
|
|
28
|
+
// hash PRIMARY KEY 天然唯一约束;删笔记时同步删对应行
|
|
29
|
+
export function initNotePhotoIndexSchema(db) {
|
|
30
|
+
db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS note_photo_index (
|
|
32
|
+
hash TEXT PRIMARY KEY,
|
|
33
|
+
shareable_id TEXT NOT NULL
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
try {
|
|
37
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_npi_shareable ON note_photo_index(shareable_id)");
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
// ─── Wave A-1: 个人心愿单(独立于慈善 wishes)──────────────────────
|
|
42
|
+
export function initUserWishlistSchema(db) {
|
|
43
|
+
db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS user_wishlist (
|
|
45
|
+
user_id TEXT NOT NULL,
|
|
46
|
+
product_id TEXT NOT NULL,
|
|
47
|
+
note TEXT,
|
|
48
|
+
notify_price_drop INTEGER DEFAULT 1,
|
|
49
|
+
notify_back_in_stock INTEGER DEFAULT 1,
|
|
50
|
+
price_at_add REAL,
|
|
51
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
52
|
+
PRIMARY KEY(user_id, product_id)
|
|
53
|
+
)
|
|
54
|
+
`);
|
|
55
|
+
try {
|
|
56
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_wl_product ON user_wishlist(product_id)');
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
60
|
+
// ─── Wave A-2: 商品 Q&A(公开问答 — 自动 FAQ + 防虚假承诺)─────────
|
|
61
|
+
export function initProductQaSchema(db) {
|
|
62
|
+
db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS product_qa (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
product_id TEXT NOT NULL,
|
|
66
|
+
asker_id TEXT NOT NULL,
|
|
67
|
+
seller_id TEXT NOT NULL,
|
|
68
|
+
question TEXT NOT NULL,
|
|
69
|
+
answer TEXT,
|
|
70
|
+
answered_at TEXT,
|
|
71
|
+
is_public INTEGER DEFAULT 1,
|
|
72
|
+
helpful_count INTEGER DEFAULT 0,
|
|
73
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
74
|
+
)
|
|
75
|
+
`);
|
|
76
|
+
try {
|
|
77
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_qa_product ON product_qa(product_id, created_at DESC)');
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
try {
|
|
81
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_qa_seller ON product_qa(seller_id, answered_at)');
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
try {
|
|
85
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_qa_asker ON product_qa(asker_id)');
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
// 防重复 +1 的 votes 表
|
|
89
|
+
db.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS product_qa_helpful_voters (
|
|
91
|
+
qa_id TEXT NOT NULL,
|
|
92
|
+
user_id TEXT NOT NULL,
|
|
93
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
94
|
+
PRIMARY KEY (qa_id, user_id)
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
// ─── Wave A-3: 优惠券 / 限时折扣(卖家发券 · 全店满减 · 单品限时)──
|
|
99
|
+
export function initCouponsSchema(db) {
|
|
100
|
+
db.exec(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS coupons (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
seller_id TEXT NOT NULL,
|
|
104
|
+
code TEXT NOT NULL,
|
|
105
|
+
scope TEXT NOT NULL, -- 'product' | 'shop' | 'all'
|
|
106
|
+
scope_id TEXT, -- product_id when scope='product'
|
|
107
|
+
discount_type TEXT NOT NULL, -- 'percentage' | 'fixed'
|
|
108
|
+
discount_value REAL NOT NULL,
|
|
109
|
+
min_order_amount REAL DEFAULT 0,
|
|
110
|
+
max_uses INTEGER DEFAULT 0, -- 0 = unlimited
|
|
111
|
+
uses_count INTEGER DEFAULT 0,
|
|
112
|
+
starts_at TEXT,
|
|
113
|
+
expires_at TEXT,
|
|
114
|
+
is_active INTEGER DEFAULT 1,
|
|
115
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
116
|
+
UNIQUE(seller_id, code)
|
|
117
|
+
)
|
|
118
|
+
`);
|
|
119
|
+
try {
|
|
120
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_coupons_seller ON coupons(seller_id, is_active)');
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
try {
|
|
124
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_coupons_scope ON coupons(scope, scope_id) WHERE is_active = 1');
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
}
|
|
128
|
+
// ─── Wave A-4: 平台公告(admin 发布 → 角色 / 区域定向)+ 阅读记录 ──
|
|
129
|
+
export function initAnnouncementsSchema(db) {
|
|
130
|
+
db.exec(`
|
|
131
|
+
CREATE TABLE IF NOT EXISTS announcements (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
author_id TEXT NOT NULL,
|
|
134
|
+
title TEXT NOT NULL,
|
|
135
|
+
body TEXT NOT NULL,
|
|
136
|
+
target_roles TEXT, -- JSON array: ['buyer','seller'] or null=all
|
|
137
|
+
target_regions TEXT, -- JSON array: ['china','us'] or null=all
|
|
138
|
+
severity TEXT DEFAULT 'info', -- 'info' | 'warning' | 'critical'
|
|
139
|
+
is_active INTEGER DEFAULT 1,
|
|
140
|
+
starts_at TEXT,
|
|
141
|
+
expires_at TEXT,
|
|
142
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
143
|
+
)
|
|
144
|
+
`);
|
|
145
|
+
try {
|
|
146
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_ann_active ON announcements(is_active, created_at DESC)');
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
// 用户阅读记录(PK 防重复 dismiss)
|
|
150
|
+
db.exec(`
|
|
151
|
+
CREATE TABLE IF NOT EXISTS announcement_reads (
|
|
152
|
+
user_id TEXT NOT NULL,
|
|
153
|
+
announcement_id TEXT NOT NULL,
|
|
154
|
+
read_at TEXT DEFAULT (datetime('now')),
|
|
155
|
+
PRIMARY KEY (user_id, announcement_id)
|
|
156
|
+
)
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
// ─── Wave B-2: 预售 / waitlist(缺货商品允许买家排队 → 回货时通知)──
|
|
160
|
+
export function initProductWaitlistSchema(db) {
|
|
161
|
+
db.exec(`
|
|
162
|
+
CREATE TABLE IF NOT EXISTS product_waitlist (
|
|
163
|
+
user_id TEXT NOT NULL,
|
|
164
|
+
product_id TEXT NOT NULL,
|
|
165
|
+
desired_qty INTEGER DEFAULT 1,
|
|
166
|
+
note TEXT,
|
|
167
|
+
notified_at TEXT, -- 回货时填,表示已发通知
|
|
168
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
169
|
+
PRIMARY KEY (user_id, product_id)
|
|
170
|
+
)
|
|
171
|
+
`);
|
|
172
|
+
try {
|
|
173
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_waitlist_product ON product_waitlist(product_id) WHERE notified_at IS NULL');
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
// ─── Wave D-4: 限时促销 / Flash Sale ───────────────────────────────
|
|
178
|
+
export function initFlashSalesSchema(db) {
|
|
179
|
+
db.exec(`
|
|
180
|
+
CREATE TABLE IF NOT EXISTS flash_sales (
|
|
181
|
+
id TEXT PRIMARY KEY,
|
|
182
|
+
seller_id TEXT NOT NULL,
|
|
183
|
+
product_id TEXT NOT NULL,
|
|
184
|
+
variant_id TEXT, -- 可选,绑定具体规格
|
|
185
|
+
sale_price REAL NOT NULL,
|
|
186
|
+
original_price REAL NOT NULL, -- 创建时快照,用于显示「省 X」
|
|
187
|
+
max_qty INTEGER DEFAULT 0, -- 0 = 不限
|
|
188
|
+
sold_count INTEGER DEFAULT 0,
|
|
189
|
+
starts_at TEXT NOT NULL,
|
|
190
|
+
ends_at TEXT NOT NULL,
|
|
191
|
+
is_active INTEGER DEFAULT 1,
|
|
192
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
193
|
+
)
|
|
194
|
+
`);
|
|
195
|
+
try {
|
|
196
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_flash_product ON flash_sales(product_id, is_active)');
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
try {
|
|
200
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_flash_seller ON flash_sales(seller_id, ends_at DESC)');
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
}
|
|
204
|
+
// ─── 首屏「我有建议」公开收集(匿名可投,登录态自动绑 user_id)──────
|
|
205
|
+
export function initPublicIdeasSchema(db) {
|
|
206
|
+
db.exec(`
|
|
207
|
+
CREATE TABLE IF NOT EXISTS public_ideas (
|
|
208
|
+
id TEXT PRIMARY KEY,
|
|
209
|
+
user_id TEXT, -- 可空(匿名提交)
|
|
210
|
+
contact TEXT, -- 可选 email / handle / 任何联系方式
|
|
211
|
+
content TEXT NOT NULL,
|
|
212
|
+
ip_hash TEXT,
|
|
213
|
+
ua_hash TEXT,
|
|
214
|
+
status TEXT NOT NULL DEFAULT 'new', -- new / triaged / resolved / spam
|
|
215
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
216
|
+
)
|
|
217
|
+
`);
|
|
218
|
+
try {
|
|
219
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pi_status ON public_ideas(status, created_at DESC)");
|
|
220
|
+
}
|
|
221
|
+
catch { }
|
|
222
|
+
try {
|
|
223
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pi_rate ON public_ideas(ip_hash, created_at)");
|
|
224
|
+
}
|
|
225
|
+
catch { }
|
|
226
|
+
}
|
|
227
|
+
// ─── #959: 拍卖「⏰ 提醒我」(1 订阅 = 多行,每个 lead 时间一行)────
|
|
228
|
+
export function initAuctionRemindersSchema(db) {
|
|
229
|
+
db.exec(`
|
|
230
|
+
CREATE TABLE IF NOT EXISTS auction_reminders (
|
|
231
|
+
id TEXT PRIMARY KEY, -- arm_xxxx
|
|
232
|
+
auction_id TEXT NOT NULL REFERENCES auctions(id),
|
|
233
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
234
|
+
lead_minutes INTEGER NOT NULL, -- 提前多少分钟提醒
|
|
235
|
+
fire_at TEXT NOT NULL, -- deadline - lead_minutes(创建时算好)
|
|
236
|
+
sent_at TEXT,
|
|
237
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
238
|
+
UNIQUE(auction_id, user_id, lead_minutes)
|
|
239
|
+
)
|
|
240
|
+
`);
|
|
241
|
+
try {
|
|
242
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_arm_due ON auction_reminders(sent_at, fire_at) WHERE sent_at IS NULL");
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
try {
|
|
246
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_arm_user ON auction_reminders(user_id, auction_id)");
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
}
|
|
250
|
+
// ─── 邮箱订阅独立表(GDPR-ready)— 与 ideas 解耦 ───────────────────
|
|
251
|
+
// consent 显式存;unsubscribe_token 让用户主动退订;source 区分来源
|
|
252
|
+
// 注:后续 ALTER 列扩展刻意保留在 server.ts 原位(紧跟本 init 调用之后)
|
|
253
|
+
export function initEmailSubscriptionsSchema(db) {
|
|
254
|
+
db.exec(`
|
|
255
|
+
CREATE TABLE IF NOT EXISTS email_subscriptions (
|
|
256
|
+
id TEXT PRIMARY KEY,
|
|
257
|
+
email TEXT NOT NULL UNIQUE,
|
|
258
|
+
source TEXT NOT NULL DEFAULT 'welcome',
|
|
259
|
+
consent_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
260
|
+
unsubscribe_token TEXT NOT NULL UNIQUE,
|
|
261
|
+
unsubscribed_at TEXT,
|
|
262
|
+
ip_hash TEXT,
|
|
263
|
+
user_id TEXT,
|
|
264
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
265
|
+
)
|
|
266
|
+
`);
|
|
267
|
+
try {
|
|
268
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_es_status ON email_subscriptions(unsubscribed_at, created_at DESC)");
|
|
269
|
+
}
|
|
270
|
+
catch { }
|
|
271
|
+
try {
|
|
272
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_es_source ON email_subscriptions(source, created_at DESC)");
|
|
273
|
+
}
|
|
274
|
+
catch { }
|
|
275
|
+
}
|
|
276
|
+
// ─── Wave D-3: 用户反馈 / 客服工单(buyer-to-platform,独立于 disputes)──
|
|
277
|
+
// 注:后续 ALTER 列扩展刻意保留在 server.ts 原位(紧跟本 init 调用之后)
|
|
278
|
+
export function initFeedbackTicketsSchema(db) {
|
|
279
|
+
db.exec(`
|
|
280
|
+
CREATE TABLE IF NOT EXISTS feedback_tickets (
|
|
281
|
+
id TEXT PRIMARY KEY,
|
|
282
|
+
user_id TEXT NOT NULL,
|
|
283
|
+
category TEXT NOT NULL, -- 'bug' | 'abuse' | 'feature' | 'account' | 'other'
|
|
284
|
+
severity TEXT DEFAULT 'medium', -- 'low' | 'medium' | 'high'
|
|
285
|
+
subject TEXT NOT NULL,
|
|
286
|
+
body TEXT NOT NULL,
|
|
287
|
+
status TEXT NOT NULL DEFAULT 'open', -- open | in_progress | resolved | closed
|
|
288
|
+
admin_reply TEXT,
|
|
289
|
+
replied_by TEXT,
|
|
290
|
+
replied_at TEXT,
|
|
291
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
292
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
293
|
+
)
|
|
294
|
+
`);
|
|
295
|
+
try {
|
|
296
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_feedback_user ON feedback_tickets(user_id, created_at DESC)');
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
}
|
|
300
|
+
// ─── W7 客服 ticket-thread — 多轮消息(user ↔ admin)─────────────────
|
|
301
|
+
// 注:后续 ALTER 列扩展刻意保留在 server.ts 原位(紧跟本 init 调用之后)
|
|
302
|
+
export function initFeedbackMessagesSchema(db) {
|
|
303
|
+
db.exec(`
|
|
304
|
+
CREATE TABLE IF NOT EXISTS feedback_messages (
|
|
305
|
+
id TEXT PRIMARY KEY, -- fmsg_xxx
|
|
306
|
+
ticket_id TEXT NOT NULL,
|
|
307
|
+
sender_id TEXT NOT NULL,
|
|
308
|
+
sender_role TEXT NOT NULL, -- 'user' | 'admin'
|
|
309
|
+
body TEXT NOT NULL,
|
|
310
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
311
|
+
)
|
|
312
|
+
`);
|
|
313
|
+
try {
|
|
314
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_fmsg_ticket ON feedback_messages(ticket_id, created_at)');
|
|
315
|
+
}
|
|
316
|
+
catch { }
|
|
317
|
+
}
|
|
318
|
+
// ─── 公开判例(裁决后脱敏版本,disputes 是当事人/仲裁员私域)────────
|
|
319
|
+
export function initDisputeCasesSchema(db) {
|
|
320
|
+
db.exec(`
|
|
321
|
+
CREATE TABLE IF NOT EXISTS dispute_cases (
|
|
322
|
+
id TEXT PRIMARY KEY, -- dcase_xxx
|
|
323
|
+
dispute_id TEXT, -- 原始 disputes.id (内部追溯)
|
|
324
|
+
order_id TEXT,
|
|
325
|
+
product_id TEXT, -- 关键索引:按商品查公开判例
|
|
326
|
+
seller_id TEXT,
|
|
327
|
+
buyer_id TEXT, -- 仅内部使用,不外露
|
|
328
|
+
category_tag TEXT, -- 物流 / 质量 / 描述不符 / 售后 / 拒收 / 其他
|
|
329
|
+
winner TEXT, -- buyer / seller / split / dismissed
|
|
330
|
+
resolution TEXT, -- 简短人读判决 (如 '全额退款')
|
|
331
|
+
amount_bucket TEXT, -- '0-100' / '100-500' / '500-2000' / '2000+' WAZ
|
|
332
|
+
buyer_argument TEXT, -- 脱敏后买家陈述
|
|
333
|
+
seller_argument TEXT, -- 脱敏后卖家陈述
|
|
334
|
+
ruling_text TEXT, -- 仲裁员判决书
|
|
335
|
+
arbitrator_id TEXT,
|
|
336
|
+
fairness_yes INTEGER DEFAULT 0,
|
|
337
|
+
fairness_no INTEGER DEFAULT 0,
|
|
338
|
+
comment_count INTEGER DEFAULT 0,
|
|
339
|
+
published_at TEXT DEFAULT (datetime('now')),
|
|
340
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
341
|
+
)
|
|
342
|
+
`);
|
|
343
|
+
try {
|
|
344
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_dcase_product ON dispute_cases(product_id, published_at DESC)');
|
|
345
|
+
}
|
|
346
|
+
catch { }
|
|
347
|
+
try {
|
|
348
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_dcase_seller ON dispute_cases(seller_id, published_at DESC)');
|
|
349
|
+
}
|
|
350
|
+
catch { }
|
|
351
|
+
}
|
|
352
|
+
// ─── 公开判例评论(一案一人一次;anonymous ALTER + 索引留 server.ts 原位)──
|
|
353
|
+
export function initDisputeCommentsSchema(db) {
|
|
354
|
+
db.exec(`
|
|
355
|
+
CREATE TABLE IF NOT EXISTS dispute_comments (
|
|
356
|
+
id TEXT PRIMARY KEY, -- dcom_xxx
|
|
357
|
+
case_id TEXT NOT NULL,
|
|
358
|
+
commenter_id TEXT NOT NULL,
|
|
359
|
+
body TEXT NOT NULL,
|
|
360
|
+
flagged INTEGER DEFAULT 0,
|
|
361
|
+
likes INTEGER DEFAULT 0,
|
|
362
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
363
|
+
UNIQUE(case_id, commenter_id) -- 一案一人一次(防刷)
|
|
364
|
+
)
|
|
365
|
+
`);
|
|
366
|
+
}
|
|
367
|
+
// ─── W5 仲裁公开评论楼中楼 — 单层子回复 ────────────────────────────
|
|
368
|
+
export function initDisputeCommentRepliesSchema(db) {
|
|
369
|
+
db.exec(`
|
|
370
|
+
CREATE TABLE IF NOT EXISTS dispute_comment_replies (
|
|
371
|
+
id TEXT PRIMARY KEY, -- drep_xxx
|
|
372
|
+
parent_comment_id TEXT NOT NULL, -- 指向 dispute_comments.id
|
|
373
|
+
case_id TEXT NOT NULL,
|
|
374
|
+
replier_id TEXT NOT NULL,
|
|
375
|
+
body TEXT NOT NULL,
|
|
376
|
+
anonymous INTEGER DEFAULT 0,
|
|
377
|
+
likes INTEGER DEFAULT 0,
|
|
378
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
379
|
+
)
|
|
380
|
+
`);
|
|
381
|
+
try {
|
|
382
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_drep_parent ON dispute_comment_replies(parent_comment_id, created_at)');
|
|
383
|
+
}
|
|
384
|
+
catch { }
|
|
385
|
+
try {
|
|
386
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_drep_case ON dispute_comment_replies(case_id, created_at DESC)');
|
|
387
|
+
}
|
|
388
|
+
catch { }
|
|
389
|
+
}
|
|
390
|
+
// ─── W6 笔记评论 — 原生 parent_id 楼中楼(仅 1 层)─────────────────
|
|
391
|
+
export function initShareableCommentsSchema(db) {
|
|
392
|
+
db.exec(`
|
|
393
|
+
CREATE TABLE IF NOT EXISTS shareable_comments (
|
|
394
|
+
id TEXT PRIMARY KEY, -- scom_xxx
|
|
395
|
+
shareable_id TEXT NOT NULL, -- shareables.id
|
|
396
|
+
commenter_id TEXT NOT NULL,
|
|
397
|
+
parent_id TEXT, -- 子评论指向父评论;root = NULL
|
|
398
|
+
body TEXT NOT NULL,
|
|
399
|
+
flagged INTEGER DEFAULT 0,
|
|
400
|
+
likes INTEGER DEFAULT 0,
|
|
401
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
402
|
+
)
|
|
403
|
+
`);
|
|
404
|
+
try {
|
|
405
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_scom_shareable ON shareable_comments(shareable_id, parent_id, created_at DESC)');
|
|
406
|
+
}
|
|
407
|
+
catch { }
|
|
408
|
+
}
|
|
409
|
+
// ─── 公开判例公平性投票(一案一人一票)──────────────────────────────
|
|
410
|
+
export function initDisputeFairnessVotesSchema(db) {
|
|
411
|
+
db.exec(`
|
|
412
|
+
CREATE TABLE IF NOT EXISTS dispute_fairness_votes (
|
|
413
|
+
case_id TEXT NOT NULL,
|
|
414
|
+
voter_id TEXT NOT NULL,
|
|
415
|
+
vote TEXT NOT NULL, -- 'yes' / 'no'
|
|
416
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
417
|
+
PRIMARY KEY (case_id, voter_id)
|
|
418
|
+
)
|
|
419
|
+
`);
|
|
420
|
+
}
|
|
421
|
+
// ─── Wave C-3: 买家评价 / 评分(完成订单后给卖家 1-5 星 + 文字)──────
|
|
422
|
+
// 注:后续结构化维度 ALTER + 跨表 orders 索引刻意保留在 server.ts 原位
|
|
423
|
+
export function initOrderRatingsSchema(db) {
|
|
424
|
+
db.exec(`
|
|
425
|
+
CREATE TABLE IF NOT EXISTS order_ratings (
|
|
426
|
+
order_id TEXT PRIMARY KEY,
|
|
427
|
+
buyer_id TEXT NOT NULL,
|
|
428
|
+
seller_id TEXT NOT NULL,
|
|
429
|
+
product_id TEXT NOT NULL,
|
|
430
|
+
stars INTEGER NOT NULL, -- 1-5
|
|
431
|
+
comment TEXT,
|
|
432
|
+
reply TEXT, -- seller 可回复
|
|
433
|
+
replied_at TEXT,
|
|
434
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
435
|
+
)
|
|
436
|
+
`);
|
|
437
|
+
try {
|
|
438
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_rating_seller ON order_ratings(seller_id, created_at DESC)');
|
|
439
|
+
}
|
|
440
|
+
catch { }
|
|
441
|
+
try {
|
|
442
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_rating_product ON order_ratings(product_id, created_at DESC)');
|
|
443
|
+
}
|
|
444
|
+
catch { }
|
|
445
|
+
// P2 hot-path:覆盖 recommend_count 子查询(COUNT DISTINCT buyer_id WHERE product_id=? AND stars>=4)
|
|
446
|
+
try {
|
|
447
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_rating_recommend ON order_ratings(product_id, stars, buyer_id)');
|
|
448
|
+
}
|
|
449
|
+
catch { }
|
|
450
|
+
}
|
|
451
|
+
// ─── 反向评价:卖家给买家评分(双盲)──────────────────────────────
|
|
452
|
+
export function initBuyerRatingsSchema(db) {
|
|
453
|
+
db.exec(`
|
|
454
|
+
CREATE TABLE IF NOT EXISTS buyer_ratings (
|
|
455
|
+
order_id TEXT PRIMARY KEY,
|
|
456
|
+
seller_id TEXT NOT NULL,
|
|
457
|
+
buyer_id TEXT NOT NULL,
|
|
458
|
+
stars INTEGER NOT NULL,
|
|
459
|
+
comment TEXT,
|
|
460
|
+
dim_payment_speed INTEGER,
|
|
461
|
+
dim_communication INTEGER,
|
|
462
|
+
dim_responsiveness INTEGER,
|
|
463
|
+
hidden_until TEXT,
|
|
464
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
465
|
+
)
|
|
466
|
+
`);
|
|
467
|
+
try {
|
|
468
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_buyer_ratings_buyer ON buyer_ratings(buyer_id, created_at DESC)');
|
|
469
|
+
}
|
|
470
|
+
catch { }
|
|
471
|
+
}
|
|
472
|
+
// ─── Wave C-2: 多收货地址簿(buyer 保存常用地址,下单时选默认)──────
|
|
473
|
+
export function initUserAddressesSchema(db) {
|
|
474
|
+
db.exec(`
|
|
475
|
+
CREATE TABLE IF NOT EXISTS user_addresses (
|
|
476
|
+
id TEXT PRIMARY KEY,
|
|
477
|
+
user_id TEXT NOT NULL,
|
|
478
|
+
label TEXT NOT NULL, -- 标签(家 / 公司 / 父母家)
|
|
479
|
+
recipient TEXT NOT NULL,
|
|
480
|
+
phone TEXT,
|
|
481
|
+
region TEXT, -- 省/市/区
|
|
482
|
+
detail TEXT NOT NULL, -- 详细地址
|
|
483
|
+
is_default INTEGER DEFAULT 0,
|
|
484
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
485
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
486
|
+
)
|
|
487
|
+
`);
|
|
488
|
+
try {
|
|
489
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_addr_user ON user_addresses(user_id, is_default DESC)');
|
|
490
|
+
}
|
|
491
|
+
catch { }
|
|
492
|
+
}
|
|
493
|
+
// ─── P2P 店铺 ──────────────────────────────────────────────────────
|
|
494
|
+
export function initP2pShopsSchema(db) {
|
|
495
|
+
db.exec(`
|
|
496
|
+
CREATE TABLE IF NOT EXISTS p2p_shops (
|
|
497
|
+
id TEXT PRIMARY KEY,
|
|
498
|
+
owner_id TEXT NOT NULL,
|
|
499
|
+
name TEXT NOT NULL,
|
|
500
|
+
description TEXT,
|
|
501
|
+
thumbnail_uri TEXT,
|
|
502
|
+
peer_endpoint TEXT,
|
|
503
|
+
peer_pubkey TEXT,
|
|
504
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
505
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
506
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
507
|
+
)
|
|
508
|
+
`);
|
|
509
|
+
try {
|
|
510
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_p2p_shops_owner ON p2p_shops(owner_id, status)');
|
|
511
|
+
}
|
|
512
|
+
catch { }
|
|
513
|
+
}
|
|
514
|
+
// ─── 笔记点赞 ──────────────────────────────────────────────────────
|
|
515
|
+
export function initShareableLikesSchema(db) {
|
|
516
|
+
db.exec(`
|
|
517
|
+
CREATE TABLE IF NOT EXISTS shareable_likes (
|
|
518
|
+
id TEXT PRIMARY KEY,
|
|
519
|
+
shareable_id TEXT NOT NULL,
|
|
520
|
+
user_id TEXT NOT NULL,
|
|
521
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
522
|
+
UNIQUE(shareable_id, user_id)
|
|
523
|
+
)
|
|
524
|
+
`);
|
|
525
|
+
try {
|
|
526
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_shr_likes_shr ON shareable_likes(shareable_id)');
|
|
527
|
+
}
|
|
528
|
+
catch { }
|
|
529
|
+
try {
|
|
530
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_shr_likes_user ON shareable_likes(user_id, created_at DESC)');
|
|
531
|
+
}
|
|
532
|
+
catch { }
|
|
533
|
+
}
|
|
534
|
+
// ─── audit P2:收藏功能(小红书风格"收藏" tab)───────────────────────
|
|
535
|
+
export function initShareableBookmarksSchema(db) {
|
|
536
|
+
db.exec(`
|
|
537
|
+
CREATE TABLE IF NOT EXISTS shareable_bookmarks (
|
|
538
|
+
id TEXT PRIMARY KEY,
|
|
539
|
+
shareable_id TEXT NOT NULL,
|
|
540
|
+
user_id TEXT NOT NULL,
|
|
541
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
542
|
+
UNIQUE(shareable_id, user_id)
|
|
543
|
+
)
|
|
544
|
+
`);
|
|
545
|
+
try {
|
|
546
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_shr_bm_user ON shareable_bookmarks(user_id, created_at DESC)');
|
|
547
|
+
}
|
|
548
|
+
catch { }
|
|
549
|
+
try {
|
|
550
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_shr_bm_shr ON shareable_bookmarks(shareable_id)');
|
|
551
|
+
}
|
|
552
|
+
catch { }
|
|
553
|
+
}
|
|
554
|
+
// ─── audit P1 backlog:# 话题/标签系统(小红书风格内容分发)──────────
|
|
555
|
+
export function initShareableTagsSchema(db) {
|
|
556
|
+
db.exec(`
|
|
557
|
+
CREATE TABLE IF NOT EXISTS shareable_tags (
|
|
558
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
559
|
+
shareable_id TEXT NOT NULL,
|
|
560
|
+
tag TEXT NOT NULL, -- 已 lowercase + trim,最长 30 字符
|
|
561
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
562
|
+
UNIQUE(shareable_id, tag)
|
|
563
|
+
)
|
|
564
|
+
`);
|
|
565
|
+
try {
|
|
566
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_shr_tags_tag ON shareable_tags(tag, created_at DESC)');
|
|
567
|
+
}
|
|
568
|
+
catch { }
|
|
569
|
+
try {
|
|
570
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_shr_tags_shr ON shareable_tags(shareable_id)');
|
|
571
|
+
}
|
|
572
|
+
catch { }
|
|
573
|
+
}
|
|
574
|
+
// ─── manifest_registry = 原生 P2P 内容索引(仅 hash + 签名 + 元数据)──
|
|
575
|
+
export function initManifestRegistrySchema(db) {
|
|
576
|
+
db.exec(`
|
|
577
|
+
CREATE TABLE IF NOT EXISTS manifest_registry (
|
|
578
|
+
hash TEXT PRIMARY KEY,
|
|
579
|
+
owner_id TEXT NOT NULL,
|
|
580
|
+
content_type TEXT NOT NULL,
|
|
581
|
+
byte_size INTEGER NOT NULL,
|
|
582
|
+
title TEXT,
|
|
583
|
+
description TEXT,
|
|
584
|
+
thumbnail_data_uri TEXT,
|
|
585
|
+
signature TEXT NOT NULL,
|
|
586
|
+
signed_at TEXT NOT NULL,
|
|
587
|
+
related_product_id TEXT,
|
|
588
|
+
related_anchor TEXT,
|
|
589
|
+
status TEXT DEFAULT 'active',
|
|
590
|
+
takedown_reason TEXT,
|
|
591
|
+
takedown_at TEXT,
|
|
592
|
+
takedown_by TEXT,
|
|
593
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
594
|
+
)
|
|
595
|
+
`);
|
|
596
|
+
try {
|
|
597
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_owner ON manifest_registry(owner_id, status)");
|
|
598
|
+
}
|
|
599
|
+
catch { }
|
|
600
|
+
try {
|
|
601
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_product ON manifest_registry(related_product_id, status)");
|
|
602
|
+
}
|
|
603
|
+
catch { }
|
|
604
|
+
try {
|
|
605
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_anchor ON manifest_registry(related_anchor, status)");
|
|
606
|
+
}
|
|
607
|
+
catch { }
|
|
608
|
+
}
|
|
609
|
+
// ─── peer_directory = 在线 peer 注册(hash cache 持有者,heartbeat 5min 失效)──
|
|
610
|
+
export function initPeerDirectorySchema(db) {
|
|
611
|
+
db.exec(`
|
|
612
|
+
CREATE TABLE IF NOT EXISTS peer_directory (
|
|
613
|
+
peer_id TEXT NOT NULL,
|
|
614
|
+
manifest_hash TEXT NOT NULL,
|
|
615
|
+
is_owner INTEGER DEFAULT 0,
|
|
616
|
+
pin_intent INTEGER DEFAULT 0,
|
|
617
|
+
last_heartbeat TEXT NOT NULL,
|
|
618
|
+
bytes_served_total INTEGER DEFAULT 0,
|
|
619
|
+
PRIMARY KEY (peer_id, manifest_hash)
|
|
620
|
+
)
|
|
621
|
+
`);
|
|
622
|
+
try {
|
|
623
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_peer_hash ON peer_directory(manifest_hash, last_heartbeat DESC)");
|
|
624
|
+
}
|
|
625
|
+
catch { }
|
|
626
|
+
try {
|
|
627
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_peer_heartbeat ON peer_directory(last_heartbeat)");
|
|
628
|
+
}
|
|
629
|
+
catch { }
|
|
630
|
+
}
|
|
631
|
+
// ─── signaling_queue = WebRTC SDP/ICE 中继(TTL 2min,cron 清理)─────
|
|
632
|
+
export function initSignalingQueueSchema(db) {
|
|
633
|
+
db.exec(`
|
|
634
|
+
CREATE TABLE IF NOT EXISTS signaling_queue (
|
|
635
|
+
id TEXT PRIMARY KEY,
|
|
636
|
+
to_peer_id TEXT NOT NULL,
|
|
637
|
+
from_peer_id TEXT NOT NULL,
|
|
638
|
+
signal_type TEXT NOT NULL,
|
|
639
|
+
signal_data TEXT NOT NULL,
|
|
640
|
+
created_at TEXT NOT NULL,
|
|
641
|
+
delivered_at TEXT
|
|
642
|
+
)
|
|
643
|
+
`);
|
|
644
|
+
try {
|
|
645
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_sig_to ON signaling_queue(to_peer_id, delivered_at)");
|
|
646
|
+
}
|
|
647
|
+
catch { }
|
|
648
|
+
}
|
|
649
|
+
// ─── CHAT — 上下文绑定聊天(order / rfq / listing_qa)────────────────
|
|
650
|
+
export function initConversationsSchema(db) {
|
|
651
|
+
db.exec(`
|
|
652
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
653
|
+
id TEXT PRIMARY KEY,
|
|
654
|
+
kind TEXT NOT NULL,
|
|
655
|
+
context_id TEXT NOT NULL,
|
|
656
|
+
user_a TEXT NOT NULL,
|
|
657
|
+
user_b TEXT NOT NULL,
|
|
658
|
+
last_message_at TEXT,
|
|
659
|
+
last_preview TEXT,
|
|
660
|
+
unread_a INTEGER NOT NULL DEFAULT 0,
|
|
661
|
+
unread_b INTEGER NOT NULL DEFAULT 0,
|
|
662
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
663
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
664
|
+
UNIQUE(kind, context_id, user_a, user_b)
|
|
665
|
+
)
|
|
666
|
+
`);
|
|
667
|
+
try {
|
|
668
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_conv_a ON conversations(user_a, last_message_at DESC)');
|
|
669
|
+
}
|
|
670
|
+
catch { }
|
|
671
|
+
try {
|
|
672
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_conv_b ON conversations(user_b, last_message_at DESC)');
|
|
673
|
+
}
|
|
674
|
+
catch { }
|
|
675
|
+
try {
|
|
676
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_conv_ctx ON conversations(kind, context_id)');
|
|
677
|
+
}
|
|
678
|
+
catch { }
|
|
679
|
+
}
|
|
680
|
+
// ─── 聊天消息(后续 kind/meta ALTER 刻意保留在 server.ts 原位)────────
|
|
681
|
+
export function initMessagesSchema(db) {
|
|
682
|
+
db.exec(`
|
|
683
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
684
|
+
id TEXT PRIMARY KEY,
|
|
685
|
+
conversation_id TEXT NOT NULL,
|
|
686
|
+
sender_id TEXT NOT NULL,
|
|
687
|
+
body TEXT NOT NULL DEFAULT '',
|
|
688
|
+
attachments TEXT,
|
|
689
|
+
flagged INTEGER NOT NULL DEFAULT 0,
|
|
690
|
+
flag_reasons TEXT,
|
|
691
|
+
read_at TEXT,
|
|
692
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
693
|
+
)
|
|
694
|
+
`);
|
|
695
|
+
try {
|
|
696
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversation_id, created_at)');
|
|
697
|
+
}
|
|
698
|
+
catch { }
|
|
699
|
+
try {
|
|
700
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_msg_sender ON messages(sender_id, created_at DESC)');
|
|
701
|
+
}
|
|
702
|
+
catch { }
|
|
703
|
+
}
|
|
704
|
+
// ─── 反诈举报表(chat report → 人工审核)──────────────────────────────
|
|
705
|
+
export function initChatReportsSchema(db) {
|
|
706
|
+
db.exec(`
|
|
707
|
+
CREATE TABLE IF NOT EXISTS chat_reports (
|
|
708
|
+
id TEXT PRIMARY KEY,
|
|
709
|
+
conversation_id TEXT NOT NULL,
|
|
710
|
+
message_id TEXT,
|
|
711
|
+
reporter_id TEXT NOT NULL,
|
|
712
|
+
reported_id TEXT NOT NULL,
|
|
713
|
+
reason TEXT NOT NULL,
|
|
714
|
+
note TEXT,
|
|
715
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
716
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
717
|
+
)
|
|
718
|
+
`);
|
|
719
|
+
try {
|
|
720
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_chatrpt_status ON chat_reports(status, created_at)');
|
|
721
|
+
}
|
|
722
|
+
catch { }
|
|
723
|
+
}
|
|
724
|
+
// ─── 配额提升申请 ──────────────────────────────────────────────────
|
|
725
|
+
export function initQuotaIncreaseApplicationsSchema(db) {
|
|
726
|
+
db.exec(`
|
|
727
|
+
CREATE TABLE IF NOT EXISTS quota_increase_applications (
|
|
728
|
+
id TEXT PRIMARY KEY,
|
|
729
|
+
user_id TEXT NOT NULL,
|
|
730
|
+
current_quota INTEGER,
|
|
731
|
+
requested_quota INTEGER,
|
|
732
|
+
reason TEXT,
|
|
733
|
+
status TEXT DEFAULT 'pending',
|
|
734
|
+
applied_at TEXT DEFAULT (datetime('now')),
|
|
735
|
+
reviewed_at TEXT,
|
|
736
|
+
reviewed_by TEXT,
|
|
737
|
+
decision_note TEXT
|
|
738
|
+
)
|
|
739
|
+
`);
|
|
740
|
+
try {
|
|
741
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_quota_apps_status ON quota_increase_applications(status)');
|
|
742
|
+
}
|
|
743
|
+
catch { }
|
|
744
|
+
}
|
|
745
|
+
// ─── Verifier 申请记录 ─────────────────────────────────────────────
|
|
746
|
+
export function initVerifierApplicationsSchema(db) {
|
|
747
|
+
db.exec(`
|
|
748
|
+
CREATE TABLE IF NOT EXISTS verifier_applications (
|
|
749
|
+
id TEXT PRIMARY KEY,
|
|
750
|
+
user_id TEXT NOT NULL,
|
|
751
|
+
status TEXT DEFAULT 'pending',
|
|
752
|
+
applied_at TEXT DEFAULT (datetime('now')),
|
|
753
|
+
reviewed_at TEXT,
|
|
754
|
+
reviewed_by TEXT,
|
|
755
|
+
decision_note TEXT,
|
|
756
|
+
snapshot TEXT
|
|
757
|
+
)
|
|
758
|
+
`);
|
|
759
|
+
try {
|
|
760
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_verifier_apps_status ON verifier_applications(status)');
|
|
761
|
+
}
|
|
762
|
+
catch { }
|
|
763
|
+
}
|
|
764
|
+
// ─── Arbitrator 申请 + 白名单(外部仲裁员路径 — 与 verifier 平行)────
|
|
765
|
+
// 注:legacy 内部仲裁员 → 白名单的 migration INSERT 刻意保留在 server.ts 原位
|
|
766
|
+
export function initArbitratorReviewSchema(db) {
|
|
767
|
+
db.exec(`
|
|
768
|
+
CREATE TABLE IF NOT EXISTS arbitrator_applications (
|
|
769
|
+
id TEXT PRIMARY KEY,
|
|
770
|
+
user_id TEXT NOT NULL,
|
|
771
|
+
status TEXT DEFAULT 'pending',
|
|
772
|
+
applied_at TEXT DEFAULT (datetime('now')),
|
|
773
|
+
reviewed_at TEXT,
|
|
774
|
+
reviewed_by TEXT,
|
|
775
|
+
decision_note TEXT,
|
|
776
|
+
snapshot TEXT
|
|
777
|
+
);
|
|
778
|
+
CREATE TABLE IF NOT EXISTS arbitrator_whitelist (
|
|
779
|
+
user_id TEXT PRIMARY KEY,
|
|
780
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
781
|
+
note TEXT,
|
|
782
|
+
is_system INTEGER DEFAULT 0,
|
|
783
|
+
granted_by TEXT,
|
|
784
|
+
stake_amount INTEGER DEFAULT 0
|
|
785
|
+
)
|
|
786
|
+
`);
|
|
787
|
+
try {
|
|
788
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_arb_apps_status ON arbitrator_applications(status)');
|
|
789
|
+
}
|
|
790
|
+
catch { }
|
|
791
|
+
}
|
|
792
|
+
// ─── Verifier 申诉记录 ─────────────────────────────────────────────
|
|
793
|
+
export function initVerifierAppealsSchema(db) {
|
|
794
|
+
db.exec(`
|
|
795
|
+
CREATE TABLE IF NOT EXISTS verifier_appeals (
|
|
796
|
+
id TEXT PRIMARY KEY,
|
|
797
|
+
user_id TEXT NOT NULL,
|
|
798
|
+
task_id TEXT,
|
|
799
|
+
submission_id TEXT,
|
|
800
|
+
reason TEXT NOT NULL,
|
|
801
|
+
evidence_urls TEXT DEFAULT '[]',
|
|
802
|
+
status TEXT DEFAULT 'pending',
|
|
803
|
+
admin_note TEXT,
|
|
804
|
+
reviewed_by TEXT,
|
|
805
|
+
reviewed_at TEXT,
|
|
806
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
807
|
+
)
|
|
808
|
+
`);
|
|
809
|
+
try {
|
|
810
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_verifier_appeals_status ON verifier_appeals(status)');
|
|
811
|
+
}
|
|
812
|
+
catch { }
|
|
813
|
+
}
|
|
814
|
+
// ─── 用户暂停状态(admin 管理)────────────────────────────────────
|
|
815
|
+
export function initUserModerationSchema(db) {
|
|
816
|
+
db.exec(`
|
|
817
|
+
CREATE TABLE IF NOT EXISTS user_moderation (
|
|
818
|
+
user_id TEXT PRIMARY KEY,
|
|
819
|
+
suspended INTEGER DEFAULT 0,
|
|
820
|
+
reason TEXT,
|
|
821
|
+
suspended_by TEXT,
|
|
822
|
+
suspended_at TEXT
|
|
823
|
+
)
|
|
824
|
+
`);
|
|
825
|
+
}
|
|
826
|
+
// ─── admin 操作审计日志(initAdminCoordinationSchema FK 依赖本表,须先建)──
|
|
827
|
+
export function initAdminAuditLogSchema(db) {
|
|
828
|
+
db.exec(`
|
|
829
|
+
CREATE TABLE IF NOT EXISTS admin_audit_log (
|
|
830
|
+
id TEXT PRIMARY KEY,
|
|
831
|
+
admin_id TEXT NOT NULL,
|
|
832
|
+
action TEXT NOT NULL,
|
|
833
|
+
target_type TEXT,
|
|
834
|
+
target_id TEXT,
|
|
835
|
+
detail TEXT,
|
|
836
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
837
|
+
)
|
|
838
|
+
`);
|
|
839
|
+
try {
|
|
840
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created ON admin_audit_log(created_at)');
|
|
841
|
+
}
|
|
842
|
+
catch { }
|
|
843
|
+
}
|
|
844
|
+
// ─── 验证码表(邮箱绑定 / 找回密钥 / 改密码 等共用)────────────────
|
|
845
|
+
export function initVerificationCodesSchema(db) {
|
|
846
|
+
db.exec(`
|
|
847
|
+
CREATE TABLE IF NOT EXISTS verification_codes (
|
|
848
|
+
id TEXT PRIMARY KEY,
|
|
849
|
+
user_id TEXT NOT NULL,
|
|
850
|
+
channel TEXT NOT NULL, -- 'email' / 'phone'
|
|
851
|
+
target TEXT NOT NULL, -- 邮箱地址 / 手机号
|
|
852
|
+
code TEXT NOT NULL, -- 6 位数字
|
|
853
|
+
purpose TEXT NOT NULL, -- 'bind_email' / 'recover_key' / ...
|
|
854
|
+
attempts INTEGER DEFAULT 0,
|
|
855
|
+
used_at TEXT,
|
|
856
|
+
expires_at TEXT NOT NULL,
|
|
857
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
858
|
+
)
|
|
859
|
+
`);
|
|
860
|
+
try {
|
|
861
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_verification_codes_lookup ON verification_codes(channel, target, purpose)');
|
|
862
|
+
}
|
|
863
|
+
catch { }
|
|
864
|
+
}
|
|
865
|
+
// ─── 里程碑 4:Agent observability/reputation schema ─────────────────
|
|
866
|
+
// 注:调用方 server.ts 保留原 try/catch 边界与 console.error label;
|
|
867
|
+
// 这些 DDL 原本无逐句 try/catch(靠外层大 try 兜底),此处照搬不加。
|
|
868
|
+
export function initAgentCallLogSchema(db) {
|
|
869
|
+
db.exec(`CREATE TABLE IF NOT EXISTS agent_call_log (
|
|
870
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
871
|
+
api_key TEXT,
|
|
872
|
+
user_id TEXT,
|
|
873
|
+
endpoint TEXT NOT NULL,
|
|
874
|
+
method TEXT,
|
|
875
|
+
status_code INTEGER,
|
|
876
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
877
|
+
)`);
|
|
878
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_acl_apikey_ts ON agent_call_log(api_key, created_at)`);
|
|
879
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_acl_user_ts ON agent_call_log(user_id, created_at)`);
|
|
880
|
+
}
|
|
881
|
+
export function initAgentReputationSchema(db) {
|
|
882
|
+
db.exec(`CREATE TABLE IF NOT EXISTS agent_reputation (
|
|
883
|
+
api_key TEXT PRIMARY KEY,
|
|
884
|
+
user_id TEXT NOT NULL,
|
|
885
|
+
trust_score REAL DEFAULT 0,
|
|
886
|
+
level TEXT DEFAULT 'new',
|
|
887
|
+
signals TEXT, -- JSON
|
|
888
|
+
last_calculated_at TEXT,
|
|
889
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
890
|
+
)`);
|
|
891
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ar_user ON agent_reputation(user_id)`);
|
|
892
|
+
}
|
|
893
|
+
// ─── Agent 治理(spec: docs/AGENT-GOVERNANCE.md)────────────────────
|
|
894
|
+
// agent_declarations:agent 自声明(trust > new 必须先登记)
|
|
895
|
+
export function initAgentDeclarationsSchema(db) {
|
|
896
|
+
db.exec(`CREATE TABLE IF NOT EXISTS agent_declarations (
|
|
897
|
+
api_key TEXT PRIMARY KEY,
|
|
898
|
+
user_id TEXT NOT NULL,
|
|
899
|
+
operator_name TEXT NOT NULL, -- 公司/开发者名
|
|
900
|
+
operator_contact TEXT NOT NULL, -- email/handle/DID
|
|
901
|
+
purpose TEXT NOT NULL, -- ≤200 字
|
|
902
|
+
declared_scope TEXT NOT NULL, -- JSON: {roles, actions, regions}
|
|
903
|
+
attestations TEXT, -- JSON: {gdpr, kids_safe, no_pii_export, ...}
|
|
904
|
+
repo_url TEXT,
|
|
905
|
+
homepage TEXT,
|
|
906
|
+
revoked_at TEXT, -- 撤销时间(用户主动 revoke)
|
|
907
|
+
revoked_reason TEXT,
|
|
908
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
909
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
910
|
+
)`);
|
|
911
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agd_operator ON agent_declarations(operator_name)`);
|
|
912
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agd_revoked ON agent_declarations(revoked_at) WHERE revoked_at IS NOT NULL`);
|
|
913
|
+
}
|
|
914
|
+
// agent_attestations:bilateral consent(用户主动同意某 agent 的 scope)
|
|
915
|
+
export function initAgentAttestationsSchema(db) {
|
|
916
|
+
db.exec(`CREATE TABLE IF NOT EXISTS agent_attestations (
|
|
917
|
+
id TEXT PRIMARY KEY,
|
|
918
|
+
api_key TEXT NOT NULL, -- agent 的 api_key
|
|
919
|
+
user_id TEXT NOT NULL, -- 同意此 agent 行动的用户
|
|
920
|
+
approved_scope TEXT NOT NULL, -- JSON:用户实际批准的子集
|
|
921
|
+
spend_cap_per_order REAL, -- 该用户给此 agent 的单笔下单上限(可空 = 沿用 declared_scope)
|
|
922
|
+
spend_cap_daily REAL, -- 24h 累计上限
|
|
923
|
+
granted_at TEXT DEFAULT (datetime('now')),
|
|
924
|
+
revoked_at TEXT,
|
|
925
|
+
UNIQUE(api_key, user_id)
|
|
926
|
+
)`);
|
|
927
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_aat_user ON agent_attestations(user_id, revoked_at)`);
|
|
928
|
+
}
|
|
929
|
+
// agent_strikes:违规累积(3-strike state machine)
|
|
930
|
+
export function initAgentStrikesSchema(db) {
|
|
931
|
+
db.exec(`CREATE TABLE IF NOT EXISTS agent_strikes (
|
|
932
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
933
|
+
api_key TEXT NOT NULL,
|
|
934
|
+
user_id TEXT NOT NULL,
|
|
935
|
+
severity TEXT NOT NULL, -- 'warning' | 'suspend_7d' | 'permanent'
|
|
936
|
+
reason_code TEXT NOT NULL, -- 'fake_shipment' | 'mass_spam' | 'overlimit_order' | 'fraud_claim' | ...
|
|
937
|
+
reason_detail TEXT,
|
|
938
|
+
reported_by TEXT, -- user_id(举报人 / system / admin)
|
|
939
|
+
related_ref TEXT, -- 关联 order/dispute/claim_task id
|
|
940
|
+
issued_at TEXT DEFAULT (datetime('now')),
|
|
941
|
+
expires_at TEXT, -- warning=24h; suspend_7d=7d; permanent=null
|
|
942
|
+
appeal_status TEXT DEFAULT 'none', -- 'none' | 'pending' | 'approved' | 'denied'
|
|
943
|
+
appeal_reason TEXT,
|
|
944
|
+
appeal_decided_by TEXT,
|
|
945
|
+
appeal_decided_at TEXT
|
|
946
|
+
)`);
|
|
947
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_apikey ON agent_strikes(api_key, issued_at DESC)`);
|
|
948
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_user ON agent_strikes(user_id, issued_at DESC)`);
|
|
949
|
+
// 注:SQLite 不允许 partial index 用非确定性函数 (datetime('now'));用 expires_at 普通索引代替
|
|
950
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_active ON agent_strikes(api_key, expires_at)`);
|
|
951
|
+
}
|
|
952
|
+
// agent_revocations:operator-级撤销(封禁同 operator 名下所有 agent)
|
|
953
|
+
export function initAgentRevocationsSchema(db) {
|
|
954
|
+
db.exec(`CREATE TABLE IF NOT EXISTS agent_revocations (
|
|
955
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
956
|
+
target_kind TEXT NOT NULL, -- 'api_key' | 'operator_name'
|
|
957
|
+
target_value TEXT NOT NULL,
|
|
958
|
+
revoked_by TEXT NOT NULL, -- user_id(用户自己 OR root admin)
|
|
959
|
+
revoked_by_role TEXT, -- 'self' | 'admin'
|
|
960
|
+
reason TEXT,
|
|
961
|
+
revoked_at TEXT DEFAULT (datetime('now')),
|
|
962
|
+
UNIQUE(target_kind, target_value, revoked_by)
|
|
963
|
+
)`);
|
|
964
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_arev_target ON agent_revocations(target_kind, target_value)`);
|
|
965
|
+
}
|
|
966
|
+
// ─── 里程碑 7.2:商品 alias 系统 schema ─────────────────────────────
|
|
967
|
+
// 注:调用方 server.ts 保留原 try/catch 边界与 console.error label;
|
|
968
|
+
// 这些 DDL 原本无逐句 try/catch(靠外层 try 兜底),此处照搬不加。
|
|
969
|
+
export function initProductAliasesSchema(db) {
|
|
970
|
+
db.exec(`CREATE TABLE IF NOT EXISTS product_aliases (
|
|
971
|
+
id TEXT PRIMARY KEY,
|
|
972
|
+
product_id TEXT NOT NULL,
|
|
973
|
+
alias_type TEXT NOT NULL, -- 'external_id' | 'external_title' | 'short_url' | 'kouling_token' | 'title_substring'
|
|
974
|
+
alias_value TEXT NOT NULL,
|
|
975
|
+
min_match_chars INTEGER DEFAULT 6,
|
|
976
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
977
|
+
challenged_at TEXT, -- M7.4 verifier 挑战时间
|
|
978
|
+
status TEXT DEFAULT 'active', -- 'active' | 'revoked' | 'challenged'
|
|
979
|
+
UNIQUE(alias_type, alias_value, product_id)
|
|
980
|
+
)`);
|
|
981
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_value ON product_aliases(alias_value)`);
|
|
982
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_product ON product_aliases(product_id)`);
|
|
983
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_type ON product_aliases(alias_type)`);
|
|
984
|
+
}
|
|
985
|
+
// ─── M-5:region 切换 audit log + 24h 限流 ──────────────────────────
|
|
986
|
+
export function initRegionChangeLogSchema(db) {
|
|
987
|
+
db.exec(`CREATE TABLE IF NOT EXISTS region_change_log (
|
|
988
|
+
id TEXT PRIMARY KEY,
|
|
989
|
+
user_id TEXT NOT NULL,
|
|
990
|
+
from_region TEXT,
|
|
991
|
+
to_region TEXT NOT NULL,
|
|
992
|
+
ip TEXT,
|
|
993
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
994
|
+
)`);
|
|
995
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_region_change_user_ts ON region_change_log(user_id, created_at DESC)`);
|
|
996
|
+
}
|
|
997
|
+
// ─── P13: 购物车 ───────────────────────────────────────────────────
|
|
998
|
+
export function initCartItemsSchema(db) {
|
|
999
|
+
db.exec(`
|
|
1000
|
+
CREATE TABLE IF NOT EXISTS cart_items (
|
|
1001
|
+
user_id TEXT NOT NULL,
|
|
1002
|
+
product_id TEXT NOT NULL,
|
|
1003
|
+
qty INTEGER NOT NULL DEFAULT 1,
|
|
1004
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
1005
|
+
PRIMARY KEY (user_id, product_id)
|
|
1006
|
+
)
|
|
1007
|
+
`);
|
|
1008
|
+
}
|
|
1009
|
+
// ─── P14: 关注关系(社交电商)──────────────────────────────────────
|
|
1010
|
+
export function initFollowsSchema(db) {
|
|
1011
|
+
db.exec(`
|
|
1012
|
+
CREATE TABLE IF NOT EXISTS follows (
|
|
1013
|
+
follower_id TEXT NOT NULL,
|
|
1014
|
+
followee_id TEXT NOT NULL,
|
|
1015
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1016
|
+
PRIMARY KEY (follower_id, followee_id)
|
|
1017
|
+
)
|
|
1018
|
+
`);
|
|
1019
|
+
try {
|
|
1020
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_follows_followee ON follows(followee_id)');
|
|
1021
|
+
}
|
|
1022
|
+
catch { }
|
|
1023
|
+
}
|
|
1024
|
+
// ─── Web Push 订阅 ─────────────────────────────────────────────────
|
|
1025
|
+
export function initPushSubscriptionsSchema(db) {
|
|
1026
|
+
db.exec(`
|
|
1027
|
+
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
1028
|
+
id TEXT PRIMARY KEY,
|
|
1029
|
+
user_id TEXT NOT NULL,
|
|
1030
|
+
endpoint TEXT NOT NULL,
|
|
1031
|
+
p256dh TEXT NOT NULL,
|
|
1032
|
+
auth TEXT NOT NULL,
|
|
1033
|
+
user_agent TEXT,
|
|
1034
|
+
enabled INTEGER DEFAULT 1,
|
|
1035
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1036
|
+
UNIQUE(user_id, endpoint)
|
|
1037
|
+
)
|
|
1038
|
+
`);
|
|
1039
|
+
try {
|
|
1040
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_push_user ON push_subscriptions(user_id, enabled)');
|
|
1041
|
+
}
|
|
1042
|
+
catch { }
|
|
1043
|
+
}
|
|
1044
|
+
// ─── 用户会话(一键全登出 = rotate users.api_key)──────────────────
|
|
1045
|
+
export function initUserSessionsSchema(db) {
|
|
1046
|
+
db.exec(`
|
|
1047
|
+
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
1048
|
+
id TEXT PRIMARY KEY,
|
|
1049
|
+
user_id TEXT NOT NULL,
|
|
1050
|
+
api_key TEXT NOT NULL,
|
|
1051
|
+
ip TEXT,
|
|
1052
|
+
user_agent TEXT,
|
|
1053
|
+
fingerprint_hash TEXT,
|
|
1054
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1055
|
+
last_seen_at TEXT DEFAULT (datetime('now')),
|
|
1056
|
+
revoked_at TEXT
|
|
1057
|
+
)
|
|
1058
|
+
`);
|
|
1059
|
+
try {
|
|
1060
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_user ON user_sessions(user_id, revoked_at)');
|
|
1061
|
+
}
|
|
1062
|
+
catch { }
|
|
1063
|
+
try {
|
|
1064
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_key ON user_sessions(api_key)');
|
|
1065
|
+
}
|
|
1066
|
+
catch { }
|
|
1067
|
+
}
|
|
1068
|
+
// ─── A2 黑名单(精准匹配护栏)──────────────────────────────────────
|
|
1069
|
+
export function initUserBlocklistSchema(db) {
|
|
1070
|
+
db.exec(`
|
|
1071
|
+
CREATE TABLE IF NOT EXISTS user_blocklist (
|
|
1072
|
+
blocker_id TEXT NOT NULL,
|
|
1073
|
+
blocked_id TEXT NOT NULL,
|
|
1074
|
+
reason TEXT,
|
|
1075
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1076
|
+
PRIMARY KEY (blocker_id, blocked_id)
|
|
1077
|
+
)
|
|
1078
|
+
`);
|
|
1079
|
+
try {
|
|
1080
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_blocklist_blocker ON user_blocklist(blocker_id)");
|
|
1081
|
+
}
|
|
1082
|
+
catch { }
|
|
1083
|
+
}
|
|
1084
|
+
// ─── 导入次数追踪表 ────────────────────────────────────────────────
|
|
1085
|
+
export function initImportLogsSchema(db) {
|
|
1086
|
+
db.exec(`
|
|
1087
|
+
CREATE TABLE IF NOT EXISTS import_logs (
|
|
1088
|
+
id TEXT PRIMARY KEY,
|
|
1089
|
+
user_id TEXT NOT NULL,
|
|
1090
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1091
|
+
)
|
|
1092
|
+
`);
|
|
1093
|
+
}
|
|
1094
|
+
// ─── 错误日志(server uncaught/rejection + client onerror)──────────
|
|
1095
|
+
export function initErrorLogSchema(db) {
|
|
1096
|
+
db.exec(`
|
|
1097
|
+
CREATE TABLE IF NOT EXISTS error_log (
|
|
1098
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1099
|
+
source TEXT NOT NULL, -- 'server-uncaught' | 'server-rejection' | 'client'
|
|
1100
|
+
message TEXT NOT NULL,
|
|
1101
|
+
stack TEXT,
|
|
1102
|
+
url TEXT, -- 客户端 location.href
|
|
1103
|
+
user_agent TEXT, -- 客户端 UA
|
|
1104
|
+
user_id TEXT, -- 已登录用户(可空)
|
|
1105
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1106
|
+
)
|
|
1107
|
+
`);
|
|
1108
|
+
try {
|
|
1109
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_error_log_created ON error_log(created_at)');
|
|
1110
|
+
}
|
|
1111
|
+
catch { }
|
|
1112
|
+
}
|
|
1113
|
+
// ─── 二手市场(1 件即 1 件,个人卖家,协议费 1%)───────────────────
|
|
1114
|
+
// 注:调用方 server.ts 保留原 try/catch + [secondhand schema] label;
|
|
1115
|
+
// 这些 DDL 原本无逐句 try/catch(靠外层 try 兜底),此处照搬不加。
|
|
1116
|
+
export function initSecondhandItemsSchema(db) {
|
|
1117
|
+
db.exec(`CREATE TABLE IF NOT EXISTS secondhand_items (
|
|
1118
|
+
id TEXT PRIMARY KEY,
|
|
1119
|
+
seller_id TEXT NOT NULL,
|
|
1120
|
+
title TEXT NOT NULL,
|
|
1121
|
+
description TEXT,
|
|
1122
|
+
category TEXT NOT NULL, -- phone/computer/appliance/furniture/clothing/book/toy/sports/other
|
|
1123
|
+
condition_grade TEXT NOT NULL, -- brand_new/like_new/lightly_used/well_used/heavily_used
|
|
1124
|
+
price REAL NOT NULL,
|
|
1125
|
+
negotiable INTEGER DEFAULT 0,
|
|
1126
|
+
images TEXT, -- JSON 数组:dataURL 字符串 (最多 9 张)
|
|
1127
|
+
region TEXT,
|
|
1128
|
+
fulfillment TEXT DEFAULT 'both', -- shipping / in_person / both
|
|
1129
|
+
status TEXT DEFAULT 'available', -- available / reserved / sold / closed
|
|
1130
|
+
view_count INTEGER DEFAULT 0,
|
|
1131
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1132
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
1133
|
+
sold_at TEXT,
|
|
1134
|
+
sold_order_id TEXT
|
|
1135
|
+
)`);
|
|
1136
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_si_status_created ON secondhand_items(status, created_at DESC)`);
|
|
1137
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_si_seller ON secondhand_items(seller_id, status)`);
|
|
1138
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_si_cat ON secondhand_items(category, status)`);
|
|
1139
|
+
}
|
|
1140
|
+
// ─── 测评免单计划(1 product 1 row;后续无 ALTER)──────────────────
|
|
1141
|
+
export function initProductTrialCampaignsSchema(db) {
|
|
1142
|
+
db.exec(`
|
|
1143
|
+
CREATE TABLE IF NOT EXISTS product_trial_campaigns (
|
|
1144
|
+
id TEXT PRIMARY KEY, -- ptc_xxxx
|
|
1145
|
+
-- 1 product 1 row (复用同一行:关闭后再开 = UPDATE status='active',避免 UNIQUE 阻断 reopen)
|
|
1146
|
+
product_id TEXT NOT NULL UNIQUE REFERENCES products(id),
|
|
1147
|
+
seller_id TEXT NOT NULL REFERENCES users(id),
|
|
1148
|
+
quota_total INTEGER NOT NULL, -- 总名额 1-200
|
|
1149
|
+
quota_claimed INTEGER NOT NULL DEFAULT 0,
|
|
1150
|
+
reach_threshold INTEGER NOT NULL, -- 综合 reach 阈值 (默认 50)
|
|
1151
|
+
min_chars INTEGER NOT NULL DEFAULT 50, -- 笔记最少字数
|
|
1152
|
+
min_days_live INTEGER NOT NULL DEFAULT 7, -- 笔记需 live 至少 N 天才评估
|
|
1153
|
+
status TEXT NOT NULL DEFAULT 'active', -- active / paused / closed
|
|
1154
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1155
|
+
closed_at TEXT
|
|
1156
|
+
)
|
|
1157
|
+
`);
|
|
1158
|
+
try {
|
|
1159
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_ptc_seller ON product_trial_campaigns(seller_id, status)");
|
|
1160
|
+
}
|
|
1161
|
+
catch { }
|
|
1162
|
+
try {
|
|
1163
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_ptc_product ON product_trial_campaigns(product_id, status)");
|
|
1164
|
+
}
|
|
1165
|
+
catch { }
|
|
1166
|
+
}
|
|
1167
|
+
// ─── 测评免单认领(snap/audit ALTER 刻意留 server.ts 原位)──────────
|
|
1168
|
+
export function initProductTrialClaimsSchema(db) {
|
|
1169
|
+
db.exec(`
|
|
1170
|
+
CREATE TABLE IF NOT EXISTS product_trial_claims (
|
|
1171
|
+
id TEXT PRIMARY KEY, -- pcl_xxxx
|
|
1172
|
+
campaign_id TEXT NOT NULL REFERENCES product_trial_campaigns(id),
|
|
1173
|
+
product_id TEXT NOT NULL REFERENCES products(id),
|
|
1174
|
+
seller_id TEXT NOT NULL REFERENCES users(id),
|
|
1175
|
+
buyer_id TEXT NOT NULL REFERENCES users(id),
|
|
1176
|
+
order_id TEXT NOT NULL REFERENCES orders(id),
|
|
1177
|
+
note_id TEXT, -- shareables.id with type='note'
|
|
1178
|
+
status TEXT NOT NULL DEFAULT 'pending_note', -- pending_note / pending_threshold / refunded / expired / cancelled
|
|
1179
|
+
reach_score REAL DEFAULT 0,
|
|
1180
|
+
metrics_json TEXT, -- 最新评估的 {views, shares, conversions} 快照
|
|
1181
|
+
refund_amount REAL,
|
|
1182
|
+
refunded_at TEXT,
|
|
1183
|
+
expired_at TEXT,
|
|
1184
|
+
last_eval_at TEXT,
|
|
1185
|
+
claimed_at TEXT DEFAULT (datetime('now')),
|
|
1186
|
+
note_linked_at TEXT,
|
|
1187
|
+
UNIQUE(buyer_id, product_id) -- 一买家一商品仅 1 个名额
|
|
1188
|
+
)
|
|
1189
|
+
`);
|
|
1190
|
+
try {
|
|
1191
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_campaign ON product_trial_claims(campaign_id, status)");
|
|
1192
|
+
}
|
|
1193
|
+
catch { }
|
|
1194
|
+
try {
|
|
1195
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_buyer ON product_trial_claims(buyer_id, status)");
|
|
1196
|
+
}
|
|
1197
|
+
catch { }
|
|
1198
|
+
try {
|
|
1199
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_seller ON product_trial_claims(seller_id, status)");
|
|
1200
|
+
}
|
|
1201
|
+
catch { }
|
|
1202
|
+
try {
|
|
1203
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_eval ON product_trial_claims(status, last_eval_at)");
|
|
1204
|
+
}
|
|
1205
|
+
catch { }
|
|
1206
|
+
try {
|
|
1207
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_note ON product_trial_claims(note_id) WHERE note_id IS NOT NULL");
|
|
1208
|
+
}
|
|
1209
|
+
catch { }
|
|
1210
|
+
}
|
|
1211
|
+
// ─── Wave B-3: 退货请求(pickup ALTER 刻意留 server.ts 原位)────────
|
|
1212
|
+
export function initReturnRequestsSchema(db) {
|
|
1213
|
+
db.exec(`
|
|
1214
|
+
CREATE TABLE IF NOT EXISTS return_requests (
|
|
1215
|
+
id TEXT PRIMARY KEY,
|
|
1216
|
+
order_id TEXT NOT NULL,
|
|
1217
|
+
buyer_id TEXT NOT NULL,
|
|
1218
|
+
seller_id TEXT NOT NULL,
|
|
1219
|
+
product_id TEXT NOT NULL,
|
|
1220
|
+
reason TEXT NOT NULL, -- 'quality' | 'wrong_item' | 'damaged' | 'no_longer_needed' | 'other'
|
|
1221
|
+
reason_text TEXT,
|
|
1222
|
+
refund_amount DECIMAL(18,2), -- 默认 = order.total_amount
|
|
1223
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | rejected | refunded | escalated | cancelled
|
|
1224
|
+
seller_response TEXT,
|
|
1225
|
+
escalated_dispute_id TEXT,
|
|
1226
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1227
|
+
resolved_at TEXT
|
|
1228
|
+
)
|
|
1229
|
+
`);
|
|
1230
|
+
try {
|
|
1231
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_returns_seller_pending ON return_requests(seller_id, status) WHERE status = \'pending\'');
|
|
1232
|
+
}
|
|
1233
|
+
catch { }
|
|
1234
|
+
try {
|
|
1235
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_returns_buyer ON return_requests(buyer_id, created_at)');
|
|
1236
|
+
}
|
|
1237
|
+
catch { }
|
|
1238
|
+
try {
|
|
1239
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_returns_order ON return_requests(order_id)');
|
|
1240
|
+
}
|
|
1241
|
+
catch { }
|
|
1242
|
+
}
|
|
1243
|
+
// ─── W2 售后协商时间线(flagged/flag_reasons ALTER 刻意留 server.ts 原位)──
|
|
1244
|
+
export function initReturnMessagesSchema(db) {
|
|
1245
|
+
db.exec(`
|
|
1246
|
+
CREATE TABLE IF NOT EXISTS return_messages (
|
|
1247
|
+
id TEXT PRIMARY KEY, -- rmsg_xxx
|
|
1248
|
+
return_id TEXT NOT NULL,
|
|
1249
|
+
sender_id TEXT NOT NULL,
|
|
1250
|
+
sender_role TEXT NOT NULL, -- 'buyer' | 'seller' | 'system'
|
|
1251
|
+
body TEXT NOT NULL,
|
|
1252
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1253
|
+
)
|
|
1254
|
+
`);
|
|
1255
|
+
try {
|
|
1256
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_rmsg_return ON return_messages(return_id, created_at)');
|
|
1257
|
+
}
|
|
1258
|
+
catch { }
|
|
1259
|
+
}
|
|
1260
|
+
// ─── Wave B-1: 商品 variants(has_variants/options_key ALTER + 回填 + uniq 索引刻意留 server.ts 原位)──
|
|
1261
|
+
export function initProductVariantsSchema(db) {
|
|
1262
|
+
db.exec(`
|
|
1263
|
+
CREATE TABLE IF NOT EXISTS product_variants (
|
|
1264
|
+
id TEXT PRIMARY KEY,
|
|
1265
|
+
product_id TEXT NOT NULL,
|
|
1266
|
+
sku TEXT, -- 卖家内部 SKU 编号(可选)
|
|
1267
|
+
options_json TEXT NOT NULL, -- {"颜色":"红","尺寸":"L"} 必填
|
|
1268
|
+
price_override REAL, -- null = 用 product.price
|
|
1269
|
+
stock INTEGER DEFAULT 0,
|
|
1270
|
+
images_json TEXT, -- variant 专属图(可选,null = 用 product.images)
|
|
1271
|
+
is_active INTEGER DEFAULT 1,
|
|
1272
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1273
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1274
|
+
)
|
|
1275
|
+
`);
|
|
1276
|
+
try {
|
|
1277
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_pv_product ON product_variants(product_id, is_active)');
|
|
1278
|
+
}
|
|
1279
|
+
catch { }
|
|
1280
|
+
}
|
|
1281
|
+
// ─── B-4: 编辑精选 / 每周推荐 ──────────────────────────────────────
|
|
1282
|
+
export function initEditorPicksSchema(db) {
|
|
1283
|
+
db.exec(`
|
|
1284
|
+
CREATE TABLE IF NOT EXISTS editor_picks (
|
|
1285
|
+
id TEXT PRIMARY KEY,
|
|
1286
|
+
kind TEXT NOT NULL, -- 'product' | 'seller'
|
|
1287
|
+
target_id TEXT NOT NULL,
|
|
1288
|
+
title TEXT, -- 编辑推荐语
|
|
1289
|
+
note TEXT,
|
|
1290
|
+
starts_at TEXT NOT NULL,
|
|
1291
|
+
ends_at TEXT NOT NULL,
|
|
1292
|
+
sort_order INTEGER DEFAULT 0,
|
|
1293
|
+
created_by TEXT,
|
|
1294
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1295
|
+
)
|
|
1296
|
+
`);
|
|
1297
|
+
try {
|
|
1298
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_ep_active ON editor_picks(kind, ends_at, sort_order)');
|
|
1299
|
+
}
|
|
1300
|
+
catch { }
|
|
1301
|
+
}
|
|
1302
|
+
// ─── D-3: KYC light — 实名认证(轻度,不存原始证件号)──────────────
|
|
1303
|
+
export function initKycRecordsSchema(db) {
|
|
1304
|
+
db.exec(`
|
|
1305
|
+
CREATE TABLE IF NOT EXISTS kyc_records (
|
|
1306
|
+
user_id TEXT PRIMARY KEY,
|
|
1307
|
+
real_name TEXT NOT NULL,
|
|
1308
|
+
id_type TEXT NOT NULL, -- 'passport' | 'national_id' | 'driver_license'
|
|
1309
|
+
id_number_hash TEXT NOT NULL, -- sha256(id_number + MASTER_SEED)
|
|
1310
|
+
id_number_last4 TEXT, -- 末 4 位明文(便于核对)
|
|
1311
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / rejected
|
|
1312
|
+
reject_reason TEXT,
|
|
1313
|
+
reviewed_by TEXT,
|
|
1314
|
+
reviewed_at TEXT,
|
|
1315
|
+
submitted_at TEXT DEFAULT (datetime('now'))
|
|
1316
|
+
)
|
|
1317
|
+
`);
|
|
1318
|
+
try {
|
|
1319
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_kyc_status ON kyc_records(status, submitted_at)');
|
|
1320
|
+
}
|
|
1321
|
+
catch { }
|
|
1322
|
+
}
|
|
1323
|
+
// ─── WebAuthn / Passkey — 敏感操作二次确认 ─────────────────────────
|
|
1324
|
+
// 注:调用方 server.ts 保留原外层 try/catch + [webauthn schema] label,并在本
|
|
1325
|
+
// init 调用之后、同一 try 内保留 users.webauthn_required_for_withdraw ALTER。
|
|
1326
|
+
// 这些 DDL 原本无逐句 try/catch(靠外层 try 兜底),此处照搬不加。
|
|
1327
|
+
export function initWebauthnSchema(db) {
|
|
1328
|
+
db.exec(`CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
|
1329
|
+
id TEXT PRIMARY KEY, -- credential.id (base64url)
|
|
1330
|
+
user_id TEXT NOT NULL,
|
|
1331
|
+
public_key BLOB NOT NULL, -- COSE public key
|
|
1332
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
1333
|
+
transports TEXT, -- JSON array
|
|
1334
|
+
device_label TEXT, -- user-friendly label
|
|
1335
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1336
|
+
last_used_at TEXT
|
|
1337
|
+
)`);
|
|
1338
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_user ON webauthn_credentials(user_id)`);
|
|
1339
|
+
db.exec(`CREATE TABLE IF NOT EXISTS webauthn_challenges (
|
|
1340
|
+
id TEXT PRIMARY KEY,
|
|
1341
|
+
user_id TEXT NOT NULL,
|
|
1342
|
+
challenge TEXT NOT NULL,
|
|
1343
|
+
purpose TEXT NOT NULL, -- 'register' | 'withdraw' | 'change-password' | 'reveal-key' | 'region'
|
|
1344
|
+
purpose_data TEXT, -- JSON:例如 {amount: 1000, to_address: '0x...'}
|
|
1345
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1346
|
+
expires_at TEXT NOT NULL,
|
|
1347
|
+
consumed_at TEXT
|
|
1348
|
+
)`);
|
|
1349
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_chall_user ON webauthn_challenges(user_id, expires_at)`);
|
|
1350
|
+
// gate token:auth/finish 成功后颁发,绑定 user + purpose + 业务参数(防重放)
|
|
1351
|
+
db.exec(`CREATE TABLE IF NOT EXISTS webauthn_gate_tokens (
|
|
1352
|
+
id TEXT PRIMARY KEY, -- token
|
|
1353
|
+
user_id TEXT NOT NULL,
|
|
1354
|
+
purpose TEXT NOT NULL,
|
|
1355
|
+
purpose_data TEXT, -- JSON
|
|
1356
|
+
expires_at TEXT NOT NULL, -- now + 60s
|
|
1357
|
+
consumed_at TEXT
|
|
1358
|
+
)`);
|
|
1359
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_gate_user ON webauthn_gate_tokens(user_id, expires_at)`);
|
|
1360
|
+
}
|
|
1361
|
+
// ─── M7.3:claim 验证任务系统(base) ──────────────────────────────
|
|
1362
|
+
// 注:调用方 server.ts 保留原外层 try/catch + [M7.3 schema claim_verification];
|
|
1363
|
+
// 本函数只含 claim_verification_tasks/votes + indexes,结算扩展 ALTER
|
|
1364
|
+
// (majority_vote / was_majority) 刻意留在 server.ts 本函数调用之后。
|
|
1365
|
+
export function initClaimVerificationBaseSchema(db) {
|
|
1366
|
+
db.exec(`CREATE TABLE IF NOT EXISTS claim_verification_tasks (
|
|
1367
|
+
id TEXT PRIMARY KEY,
|
|
1368
|
+
order_id TEXT NOT NULL,
|
|
1369
|
+
buyer_id TEXT NOT NULL,
|
|
1370
|
+
seller_id TEXT NOT NULL,
|
|
1371
|
+
product_id TEXT NOT NULL,
|
|
1372
|
+
claim_target TEXT NOT NULL, -- 'price' | 'commission' | 'protection' | 'return' | 'warranty' | 'handling' | 'other'
|
|
1373
|
+
claim_text TEXT NOT NULL, -- 买家陈述(≤ 500 字)
|
|
1374
|
+
evidence_uri TEXT, -- 买家证据(URL / hash)
|
|
1375
|
+
stake_buyer REAL NOT NULL, -- 买家锁定的质押金
|
|
1376
|
+
seller_evidence_uri TEXT, -- 卖家提交的证据
|
|
1377
|
+
seller_evidence_at TEXT,
|
|
1378
|
+
deadline_at TEXT NOT NULL, -- 默认 48h;卖家提交证据后 +24h
|
|
1379
|
+
status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'sealed' | 'resolved_pass' | 'resolved_fail' | 'resolved_no_fault' | 'timeout_pass' | 'timeout_fail'
|
|
1380
|
+
resolved_at TEXT,
|
|
1381
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1382
|
+
UNIQUE(order_id)
|
|
1383
|
+
)`);
|
|
1384
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_status ON claim_verification_tasks(status)`);
|
|
1385
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_buyer ON claim_verification_tasks(buyer_id)`);
|
|
1386
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_seller ON claim_verification_tasks(seller_id)`);
|
|
1387
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_deadline ON claim_verification_tasks(deadline_at) WHERE status = 'open'`);
|
|
1388
|
+
db.exec(`CREATE TABLE IF NOT EXISTS claim_verification_votes (
|
|
1389
|
+
id TEXT PRIMARY KEY,
|
|
1390
|
+
task_id TEXT NOT NULL,
|
|
1391
|
+
verifier_id TEXT NOT NULL,
|
|
1392
|
+
vote TEXT NOT NULL, -- 'pass' | 'fail' | 'no_fault'
|
|
1393
|
+
evidence_uri TEXT,
|
|
1394
|
+
note TEXT,
|
|
1395
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
1396
|
+
UNIQUE(task_id, verifier_id)
|
|
1397
|
+
)`);
|
|
1398
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvv_task ON claim_verification_votes(task_id)`);
|
|
1399
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvv_verifier ON claim_verification_votes(verifier_id)`);
|
|
1400
|
+
}
|
|
1401
|
+
// ─── verifier 禁言 / 永封记录(outlier 累计触发)────────────────────
|
|
1402
|
+
export function initClaimVerifierSuspensionsSchema(db) {
|
|
1403
|
+
db.exec(`CREATE TABLE IF NOT EXISTS claim_verifier_suspensions (
|
|
1404
|
+
id TEXT PRIMARY KEY,
|
|
1405
|
+
user_id TEXT NOT NULL,
|
|
1406
|
+
type TEXT NOT NULL, -- 'suspended' | 'revoked'
|
|
1407
|
+
until_at TEXT, -- NULL = permanent (revoked)
|
|
1408
|
+
reason TEXT,
|
|
1409
|
+
outlier_count INTEGER,
|
|
1410
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1411
|
+
)`);
|
|
1412
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cvs_user ON claim_verifier_suspensions(user_id, created_at DESC)`);
|
|
1413
|
+
}
|
|
1414
|
+
// ─── Sprint 1: 商品声明验证(product 层,与 order claim 平行)────────
|
|
1415
|
+
export function initProductClaimSchema(db) {
|
|
1416
|
+
db.exec(`CREATE TABLE IF NOT EXISTS product_claim_tasks (
|
|
1417
|
+
id TEXT PRIMARY KEY,
|
|
1418
|
+
product_id TEXT NOT NULL,
|
|
1419
|
+
claimant_id TEXT NOT NULL,
|
|
1420
|
+
seller_id TEXT NOT NULL,
|
|
1421
|
+
claim_target TEXT NOT NULL, -- 'title' | 'description' | 'condition' | 'return_days' | 'handling_hours' | 'warranty_days' | 'shipping_regions' | 'origin' | 'other'
|
|
1422
|
+
claim_text TEXT NOT NULL, -- 发起人陈述 6-500 字
|
|
1423
|
+
evidence_uri TEXT, -- 发起人证据 URL
|
|
1424
|
+
stake_claimant REAL NOT NULL, -- 发起人锁定质押
|
|
1425
|
+
seller_evidence_uri TEXT, -- 卖家反驳证据
|
|
1426
|
+
seller_evidence_at TEXT,
|
|
1427
|
+
deadline_at TEXT NOT NULL, -- 默认 72h;卖家提交证据后 +24h
|
|
1428
|
+
status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'sealed' | 'resolved_upheld' | 'resolved_dismissed' | 'expired'
|
|
1429
|
+
ruling TEXT, -- 'upheld' | 'dismissed' | 'insufficient'
|
|
1430
|
+
majority_vote TEXT,
|
|
1431
|
+
resolved_at TEXT,
|
|
1432
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1433
|
+
)`);
|
|
1434
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_status ON product_claim_tasks(status)`);
|
|
1435
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_product ON product_claim_tasks(product_id)`);
|
|
1436
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_claimant ON product_claim_tasks(claimant_id)`);
|
|
1437
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_seller ON product_claim_tasks(seller_id)`);
|
|
1438
|
+
db.exec(`CREATE TABLE IF NOT EXISTS product_claim_votes (
|
|
1439
|
+
id TEXT PRIMARY KEY,
|
|
1440
|
+
claim_id TEXT NOT NULL,
|
|
1441
|
+
verifier_id TEXT NOT NULL,
|
|
1442
|
+
vote TEXT NOT NULL, -- 'upheld' | 'dismissed' | 'insufficient'
|
|
1443
|
+
evidence_uri TEXT,
|
|
1444
|
+
note TEXT,
|
|
1445
|
+
was_majority INTEGER,
|
|
1446
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
1447
|
+
UNIQUE(claim_id, verifier_id)
|
|
1448
|
+
)`);
|
|
1449
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pcv_claim ON product_claim_votes(claim_id)`);
|
|
1450
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pcv_verifier ON product_claim_votes(verifier_id)`);
|
|
1451
|
+
}
|
|
1452
|
+
// ─── Sprint 2-A: 测评真实性验证(shareables / manifests)────────────
|
|
1453
|
+
export function initReviewClaimSchema(db) {
|
|
1454
|
+
db.exec(`CREATE TABLE IF NOT EXISTS review_claim_tasks (
|
|
1455
|
+
id TEXT PRIMARY KEY,
|
|
1456
|
+
review_type TEXT NOT NULL, -- 'shareable' | 'manifest'
|
|
1457
|
+
review_id TEXT NOT NULL, -- shareable.id 或 manifest.hash
|
|
1458
|
+
product_id TEXT, -- 关联商品(用于显示)
|
|
1459
|
+
reviewer_id TEXT NOT NULL, -- 被诉评测作者
|
|
1460
|
+
claimant_id TEXT NOT NULL,
|
|
1461
|
+
claim_target TEXT NOT NULL, -- 'not_real_purchase' | 'paid_promo' | 'incentivized' | 'misleading' | 'fake' | 'other'
|
|
1462
|
+
claim_text TEXT NOT NULL,
|
|
1463
|
+
evidence_uri TEXT,
|
|
1464
|
+
stake_claimant REAL NOT NULL,
|
|
1465
|
+
deadline_at TEXT NOT NULL,
|
|
1466
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
1467
|
+
ruling TEXT,
|
|
1468
|
+
majority_vote TEXT,
|
|
1469
|
+
resolved_at TEXT,
|
|
1470
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1471
|
+
)`);
|
|
1472
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_status ON review_claim_tasks(status)`);
|
|
1473
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_review ON review_claim_tasks(review_type, review_id)`);
|
|
1474
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_reviewer ON review_claim_tasks(reviewer_id)`);
|
|
1475
|
+
db.exec(`CREATE TABLE IF NOT EXISTS review_claim_votes (
|
|
1476
|
+
id TEXT PRIMARY KEY,
|
|
1477
|
+
claim_id TEXT NOT NULL,
|
|
1478
|
+
verifier_id TEXT NOT NULL,
|
|
1479
|
+
vote TEXT NOT NULL, -- 'upheld' | 'dismissed' | 'insufficient'
|
|
1480
|
+
evidence_uri TEXT,
|
|
1481
|
+
note TEXT,
|
|
1482
|
+
was_majority INTEGER,
|
|
1483
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
1484
|
+
UNIQUE(claim_id, verifier_id)
|
|
1485
|
+
)`);
|
|
1486
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_rcv_claim ON review_claim_votes(claim_id)`);
|
|
1487
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_rcv_verifier ON review_claim_votes(verifier_id)`);
|
|
1488
|
+
}
|
|
1489
|
+
// ─── Sprint 2-B: 二手成色验证(secondhand_items)───────────────────
|
|
1490
|
+
export function initSecondhandClaimSchema(db) {
|
|
1491
|
+
db.exec(`CREATE TABLE IF NOT EXISTS secondhand_claim_tasks (
|
|
1492
|
+
id TEXT PRIMARY KEY,
|
|
1493
|
+
sh_item_id TEXT NOT NULL,
|
|
1494
|
+
seller_id TEXT NOT NULL, -- 二手卖家
|
|
1495
|
+
claimant_id TEXT NOT NULL,
|
|
1496
|
+
claim_target TEXT NOT NULL, -- 'condition' | 'images' | 'description' | 'title' | 'price' | 'other'
|
|
1497
|
+
claim_text TEXT NOT NULL,
|
|
1498
|
+
evidence_uri TEXT,
|
|
1499
|
+
stake_claimant REAL NOT NULL,
|
|
1500
|
+
deadline_at TEXT NOT NULL,
|
|
1501
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
1502
|
+
ruling TEXT,
|
|
1503
|
+
majority_vote TEXT,
|
|
1504
|
+
resolved_at TEXT,
|
|
1505
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1506
|
+
)`);
|
|
1507
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_status ON secondhand_claim_tasks(status)`);
|
|
1508
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_item ON secondhand_claim_tasks(sh_item_id)`);
|
|
1509
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_seller ON secondhand_claim_tasks(seller_id)`);
|
|
1510
|
+
db.exec(`CREATE TABLE IF NOT EXISTS secondhand_claim_votes (
|
|
1511
|
+
id TEXT PRIMARY KEY,
|
|
1512
|
+
claim_id TEXT NOT NULL,
|
|
1513
|
+
verifier_id TEXT NOT NULL,
|
|
1514
|
+
vote TEXT NOT NULL,
|
|
1515
|
+
evidence_uri TEXT,
|
|
1516
|
+
note TEXT,
|
|
1517
|
+
was_majority INTEGER,
|
|
1518
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
1519
|
+
UNIQUE(claim_id, verifier_id)
|
|
1520
|
+
)`);
|
|
1521
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_scv_claim ON secondhand_claim_votes(claim_id)`);
|
|
1522
|
+
}
|
|
1523
|
+
// ─── Sprint 3-A: 拍卖声明(auctions)───────────────────────────────
|
|
1524
|
+
export function initAuctionClaimSchema(db) {
|
|
1525
|
+
db.exec(`CREATE TABLE IF NOT EXISTS auction_claim_tasks (
|
|
1526
|
+
id TEXT PRIMARY KEY,
|
|
1527
|
+
auction_id TEXT NOT NULL,
|
|
1528
|
+
seller_id TEXT NOT NULL,
|
|
1529
|
+
claimant_id TEXT NOT NULL,
|
|
1530
|
+
claim_target TEXT NOT NULL, -- 'unreasonable_reserve' | 'shill_bidding' | 'collusion' | 'fake_listing' | 'other'
|
|
1531
|
+
claim_text TEXT NOT NULL,
|
|
1532
|
+
evidence_uri TEXT,
|
|
1533
|
+
stake_claimant REAL NOT NULL,
|
|
1534
|
+
deadline_at TEXT NOT NULL,
|
|
1535
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
1536
|
+
ruling TEXT,
|
|
1537
|
+
majority_vote TEXT,
|
|
1538
|
+
resolved_at TEXT,
|
|
1539
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1540
|
+
)`);
|
|
1541
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_act_status ON auction_claim_tasks(status)`);
|
|
1542
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_act_auction ON auction_claim_tasks(auction_id)`);
|
|
1543
|
+
db.exec(`CREATE TABLE IF NOT EXISTS auction_claim_votes (
|
|
1544
|
+
id TEXT PRIMARY KEY,
|
|
1545
|
+
claim_id TEXT NOT NULL,
|
|
1546
|
+
verifier_id TEXT NOT NULL,
|
|
1547
|
+
vote TEXT NOT NULL,
|
|
1548
|
+
evidence_uri TEXT,
|
|
1549
|
+
note TEXT,
|
|
1550
|
+
was_majority INTEGER,
|
|
1551
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
1552
|
+
UNIQUE(claim_id, verifier_id)
|
|
1553
|
+
)`);
|
|
1554
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_acv_claim ON auction_claim_votes(claim_id)`);
|
|
1555
|
+
}
|
|
1556
|
+
// ─── Sprint 3-B: 慈善许愿声明(wishes)─────────────────────────────
|
|
1557
|
+
export function initWishClaimSchema(db) {
|
|
1558
|
+
db.exec(`CREATE TABLE IF NOT EXISTS wish_claim_tasks (
|
|
1559
|
+
id TEXT PRIMARY KEY,
|
|
1560
|
+
wish_id TEXT NOT NULL,
|
|
1561
|
+
wisher_id TEXT NOT NULL,
|
|
1562
|
+
claimant_id TEXT NOT NULL,
|
|
1563
|
+
claim_target TEXT NOT NULL, -- 'fake_identity' | 'fake_story' | 'already_fulfilled' | 'duplicate' | 'inappropriate' | 'other'
|
|
1564
|
+
claim_text TEXT NOT NULL,
|
|
1565
|
+
evidence_uri TEXT,
|
|
1566
|
+
stake_claimant REAL NOT NULL,
|
|
1567
|
+
deadline_at TEXT NOT NULL,
|
|
1568
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
1569
|
+
ruling TEXT,
|
|
1570
|
+
majority_vote TEXT,
|
|
1571
|
+
resolved_at TEXT,
|
|
1572
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1573
|
+
)`);
|
|
1574
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_wct_status ON wish_claim_tasks(status)`);
|
|
1575
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_wct_wish ON wish_claim_tasks(wish_id)`);
|
|
1576
|
+
db.exec(`CREATE TABLE IF NOT EXISTS wish_claim_votes (
|
|
1577
|
+
id TEXT PRIMARY KEY,
|
|
1578
|
+
claim_id TEXT NOT NULL,
|
|
1579
|
+
verifier_id TEXT NOT NULL,
|
|
1580
|
+
vote TEXT NOT NULL,
|
|
1581
|
+
evidence_uri TEXT,
|
|
1582
|
+
note TEXT,
|
|
1583
|
+
was_majority INTEGER,
|
|
1584
|
+
voted_at TEXT DEFAULT (datetime('now')),
|
|
1585
|
+
UNIQUE(claim_id, verifier_id)
|
|
1586
|
+
)`);
|
|
1587
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_wcv_claim ON wish_claim_votes(claim_id)`);
|
|
1588
|
+
}
|
|
1589
|
+
// ─── 里程碑 3:反操纵层 schema ──────────────────────────────────────
|
|
1590
|
+
// 注:调用方 server.ts 保留原外层 try/catch + label([M3 schema scl/cal/ral]);
|
|
1591
|
+
// 这些 DDL 原本无逐句 try/catch(靠外层 try 兜底),此处照搬不加。
|
|
1592
|
+
// shareables 的 unique_click_count / flag_new_account ALTER 刻意留在 server.ts
|
|
1593
|
+
// 原位(scl init 之后、cal init 之前)。
|
|
1594
|
+
export function initShareableClickLogSchema(db) {
|
|
1595
|
+
db.exec(`CREATE TABLE IF NOT EXISTS shareable_click_log (
|
|
1596
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1597
|
+
shareable_id TEXT NOT NULL,
|
|
1598
|
+
ip_hash TEXT NOT NULL,
|
|
1599
|
+
ua_hash TEXT NOT NULL,
|
|
1600
|
+
ref_path TEXT,
|
|
1601
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1602
|
+
)`);
|
|
1603
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_scl_share_ts ON shareable_click_log(shareable_id, created_at)`);
|
|
1604
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_scl_share_ipua ON shareable_click_log(shareable_id, ip_hash, ua_hash, created_at)`);
|
|
1605
|
+
}
|
|
1606
|
+
export function initCommissionAuditLogSchema(db) {
|
|
1607
|
+
db.exec(`CREATE TABLE IF NOT EXISTS commission_audit_log (
|
|
1608
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1609
|
+
order_id TEXT,
|
|
1610
|
+
buyer_id TEXT NOT NULL,
|
|
1611
|
+
seller_id TEXT NOT NULL,
|
|
1612
|
+
flag TEXT NOT NULL, -- 'sponsor_chain_cross' / 'self_in_chain'
|
|
1613
|
+
detail TEXT, -- JSON: { relation: 'buyer_ancestor_of_seller' | ..., path: '...' }
|
|
1614
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1615
|
+
)`);
|
|
1616
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_cal_buyer ON commission_audit_log(buyer_id, created_at)`);
|
|
1617
|
+
}
|
|
1618
|
+
export function initRegistrationAuditLogSchema(db) {
|
|
1619
|
+
db.exec(`CREATE TABLE IF NOT EXISTS registration_audit_log (
|
|
1620
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1621
|
+
user_id TEXT NOT NULL,
|
|
1622
|
+
ip_hash TEXT NOT NULL,
|
|
1623
|
+
ua_hash TEXT NOT NULL,
|
|
1624
|
+
sponsor_id TEXT,
|
|
1625
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1626
|
+
)`);
|
|
1627
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ral_ip_ts ON registration_audit_log(ip_hash, created_at)`);
|
|
1628
|
+
}
|
|
1629
|
+
// ─── 外部链接验证 schema ────────────────────────────────────────────
|
|
1630
|
+
// 注:product_external_links 的 revoked/platform/external_id/external_title ALTER、
|
|
1631
|
+
// idx_pel_platform_ext / idx_pel_ext_title 索引、以及回填 IIFE 刻意留 server.ts 原位。
|
|
1632
|
+
// 这些 DDL 原本是 top-level db.exec 无外层 catch,helper 不新增 catch。
|
|
1633
|
+
export function initProductExternalLinksBaseSchema(db) {
|
|
1634
|
+
db.exec(`
|
|
1635
|
+
CREATE TABLE IF NOT EXISTS product_external_links (
|
|
1636
|
+
id TEXT PRIMARY KEY,
|
|
1637
|
+
product_id TEXT NOT NULL,
|
|
1638
|
+
url TEXT NOT NULL,
|
|
1639
|
+
source TEXT DEFAULT 'manual',
|
|
1640
|
+
verified INTEGER DEFAULT 0,
|
|
1641
|
+
verify_note TEXT,
|
|
1642
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
1643
|
+
verified_at TEXT,
|
|
1644
|
+
UNIQUE(product_id, url)
|
|
1645
|
+
)
|
|
1646
|
+
`);
|
|
1647
|
+
}
|
|
1648
|
+
export function initLinkChallengesSchema(db) {
|
|
1649
|
+
db.exec(`
|
|
1650
|
+
CREATE TABLE IF NOT EXISTS link_challenges (
|
|
1651
|
+
id TEXT PRIMARY KEY,
|
|
1652
|
+
product_id TEXT NOT NULL,
|
|
1653
|
+
url TEXT NOT NULL,
|
|
1654
|
+
code TEXT NOT NULL,
|
|
1655
|
+
status TEXT DEFAULT 'pending',
|
|
1656
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1657
|
+
expires_at TEXT NOT NULL,
|
|
1658
|
+
verified_at TEXT
|
|
1659
|
+
)
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1662
|
+
export function initVerifyTasksSchema(db) {
|
|
1663
|
+
db.exec(`
|
|
1664
|
+
CREATE TABLE IF NOT EXISTS verify_tasks (
|
|
1665
|
+
id TEXT PRIMARY KEY,
|
|
1666
|
+
type TEXT NOT NULL DEFAULT 'code_check',
|
|
1667
|
+
product_id TEXT NOT NULL,
|
|
1668
|
+
url TEXT NOT NULL,
|
|
1669
|
+
code TEXT,
|
|
1670
|
+
verifiers_needed INTEGER NOT NULL DEFAULT 3,
|
|
1671
|
+
reward_per_verifier REAL NOT NULL DEFAULT 0.1,
|
|
1672
|
+
fee_locked REAL NOT NULL DEFAULT 0,
|
|
1673
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
1674
|
+
result TEXT,
|
|
1675
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1676
|
+
expires_at TEXT NOT NULL,
|
|
1677
|
+
settled_at TEXT
|
|
1678
|
+
)
|
|
1679
|
+
`);
|
|
1680
|
+
}
|
|
1681
|
+
export function initVerifySubmissionsSchema(db) {
|
|
1682
|
+
db.exec(`
|
|
1683
|
+
CREATE TABLE IF NOT EXISTS verify_submissions (
|
|
1684
|
+
id TEXT PRIMARY KEY,
|
|
1685
|
+
task_id TEXT NOT NULL,
|
|
1686
|
+
verifier_id TEXT NOT NULL,
|
|
1687
|
+
submission TEXT,
|
|
1688
|
+
verdict TEXT,
|
|
1689
|
+
claimed_at TEXT DEFAULT (datetime('now')),
|
|
1690
|
+
submitted_at TEXT,
|
|
1691
|
+
UNIQUE(task_id, verifier_id)
|
|
1692
|
+
)
|
|
1693
|
+
`);
|
|
1694
|
+
}
|
|
1695
|
+
export function initVerifierStatsSchema(db) {
|
|
1696
|
+
db.exec(`
|
|
1697
|
+
CREATE TABLE IF NOT EXISTS verifier_stats (
|
|
1698
|
+
user_id TEXT PRIMARY KEY,
|
|
1699
|
+
verify_rights INTEGER NOT NULL DEFAULT 3,
|
|
1700
|
+
tasks_done INTEGER NOT NULL DEFAULT 0,
|
|
1701
|
+
tasks_correct INTEGER NOT NULL DEFAULT 0,
|
|
1702
|
+
tasks_wrong INTEGER NOT NULL DEFAULT 0,
|
|
1703
|
+
suspended_until TEXT
|
|
1704
|
+
)
|
|
1705
|
+
`);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Non-money columns the MCP sandbox register → list_product → search path needs
|
|
1709
|
+
* that previously lived ONLY as inline `ALTER TABLE` statements in
|
|
1710
|
+
* src/pwa/server.ts boot (so an MCP-initialized fresh DB never got them, and a
|
|
1711
|
+
* sandbox `webaz_register` / `webaz_list_product` failed with "no such column").
|
|
1712
|
+
*
|
|
1713
|
+
* Single source shared by the PWA boot path (called from the same boot position
|
|
1714
|
+
* the inline `handle` pre-warm ran, before the anchor migration) and the MCP
|
|
1715
|
+
* runtime schema composition root. All guarded + idempotent; the `users` /
|
|
1716
|
+
* `products` CREATE TABLEs are in L0 initDatabase(), so call this AFTER it
|
|
1717
|
+
* (CREATE-before-ALTER preserved).
|
|
1718
|
+
*
|
|
1719
|
+
* SCOPE GUARD: exactly the 14 non-money identity/locale/product-attribute
|
|
1720
|
+
* columns the register/list/search regression requires — NO wallet / order /
|
|
1721
|
+
* status / escrow / commission / fund / tokenomics columns. DDL text is
|
|
1722
|
+
* byte-identical to the former inline statements, so schema:verify is zero-diff.
|
|
1723
|
+
*/
|
|
1724
|
+
export function initRegisterListSearchColumns(db) {
|
|
1725
|
+
for (const stmt of [
|
|
1726
|
+
// users — 4-layer identity model short code + handle + sales/commission region
|
|
1727
|
+
'ALTER TABLE users ADD COLUMN permanent_code TEXT',
|
|
1728
|
+
'ALTER TABLE users ADD COLUMN handle TEXT',
|
|
1729
|
+
"ALTER TABLE users ADD COLUMN region TEXT DEFAULT 'global'",
|
|
1730
|
+
// products — structured listing attributes (specs / sourcing ref / shipping / returns / warranty)
|
|
1731
|
+
'ALTER TABLE products ADD COLUMN specs TEXT',
|
|
1732
|
+
'ALTER TABLE products ADD COLUMN brand TEXT',
|
|
1733
|
+
'ALTER TABLE products ADD COLUMN model TEXT',
|
|
1734
|
+
'ALTER TABLE products ADD COLUMN source_price REAL',
|
|
1735
|
+
'ALTER TABLE products ADD COLUMN ship_regions TEXT DEFAULT "全国"',
|
|
1736
|
+
'ALTER TABLE products ADD COLUMN handling_hours INTEGER DEFAULT 24',
|
|
1737
|
+
'ALTER TABLE products ADD COLUMN estimated_days TEXT',
|
|
1738
|
+
'ALTER TABLE products ADD COLUMN fragile INTEGER DEFAULT 0',
|
|
1739
|
+
'ALTER TABLE products ADD COLUMN return_days INTEGER DEFAULT 7',
|
|
1740
|
+
'ALTER TABLE products ADD COLUMN return_condition TEXT',
|
|
1741
|
+
'ALTER TABLE products ADD COLUMN warranty_days INTEGER DEFAULT 0',
|
|
1742
|
+
]) {
|
|
1743
|
+
try {
|
|
1744
|
+
db.exec(stmt);
|
|
1745
|
+
}
|
|
1746
|
+
catch { /* column already exists */ }
|
|
1747
|
+
}
|
|
1748
|
+
// unique short-code / handle indexes (partial — only non-NULL)
|
|
1749
|
+
try {
|
|
1750
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_permanent_code ON users(permanent_code) WHERE permanent_code IS NOT NULL");
|
|
1751
|
+
}
|
|
1752
|
+
catch { /* exists */ }
|
|
1753
|
+
try {
|
|
1754
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(handle) WHERE handle IS NOT NULL");
|
|
1755
|
+
}
|
|
1756
|
+
catch { /* exists */ }
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* RFC-020 PR-B — agent delegation grants (Passkey-approved, scoped, short-lived,
|
|
1760
|
+
* revocable agent credentials; NOT a permanent api_key). A NEW table — deliberately
|
|
1761
|
+
* separate from `agent_attestations` (RFC-020 decision: do not overload it). Pure
|
|
1762
|
+
* idempotent DDL; the composition root (applyWebazRuntimeSchema) auto-runs this so
|
|
1763
|
+
* MCP also has the table for the future `webaz_pair` consumer.
|
|
1764
|
+
*
|
|
1765
|
+
* Bearer-first: `token_hash` stores a SHA-256 of the bearer (raw bearer is shown
|
|
1766
|
+
* once, never stored). `agent_pubkey` / `pkce_challenge` are RESERVED for the PoP /
|
|
1767
|
+
* device-flow phase (required before any risk scope or longer-lived delegation) —
|
|
1768
|
+
* unused/NULL in PR-B. `human_confirm_required` is a design field only; its
|
|
1769
|
+
* enforcement reuses the existing `webauthn_gate_tokens` / requireHumanPresence
|
|
1770
|
+
* gate (no second confirmation mechanism). NO money/order/status columns.
|
|
1771
|
+
*/
|
|
1772
|
+
export function initAgentDelegationGrantsSchema(db) {
|
|
1773
|
+
db.exec(`
|
|
1774
|
+
CREATE TABLE IF NOT EXISTS agent_delegation_grants (
|
|
1775
|
+
grant_id TEXT PRIMARY KEY, -- grt_xxx
|
|
1776
|
+
human_id TEXT NOT NULL, -- delegating human (users.id)
|
|
1777
|
+
agent_label TEXT, -- human-friendly agent name
|
|
1778
|
+
capabilities TEXT NOT NULL DEFAULT '[]', -- JSON [{capability, constraints}] — SAFE scopes only in PR-B
|
|
1779
|
+
token_hash TEXT, -- SHA-256 of bearer (bearer-first); raw never stored
|
|
1780
|
+
agent_pubkey TEXT, -- RESERVED (PoP, RFC-020 §3.3); NULL in PR-B
|
|
1781
|
+
pkce_challenge TEXT, -- RESERVED (device-flow pairing); NULL in PR-B
|
|
1782
|
+
human_confirm_required INTEGER NOT NULL DEFAULT 0,-- design field; enforcement reuses webauthn_gate_tokens
|
|
1783
|
+
status TEXT NOT NULL DEFAULT 'active', -- active | revoked | expired
|
|
1784
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1785
|
+
expires_at TEXT NOT NULL, -- short-lived (clamped, RFC-020 bearer-first)
|
|
1786
|
+
revoked_at TEXT,
|
|
1787
|
+
revoked_reason TEXT
|
|
1788
|
+
)
|
|
1789
|
+
`);
|
|
1790
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_adg_human ON agent_delegation_grants(human_id, status)`);
|
|
1791
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_adg_token ON agent_delegation_grants(token_hash)`);
|
|
1792
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_adg_expiry ON agent_delegation_grants(status, expires_at)`);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* RFC-020 PR-C1 — agent pairing sessions (OAuth device-flow + PKCE shape).
|
|
1796
|
+
*
|
|
1797
|
+
* One short-lived, one-time pairing per attempt: the agent starts a pairing (sends a
|
|
1798
|
+
* PKCE code_challenge), a logged-in human approves it (server-generated consent), and
|
|
1799
|
+
* the agent retrieves the credential ONCE using its PKCE verifier. NO raw bearer is
|
|
1800
|
+
* ever stored here — the bearer is generated at retrieval and only its SHA-256 hash is
|
|
1801
|
+
* persisted on the grant (agent_delegation_grants). `agent_pubkey` is reserved for the
|
|
1802
|
+
* PoP phase (stored if sent, NOT verified in C1). Pure idempotent DDL; the composition
|
|
1803
|
+
* root auto-runs it so MCP also has the table. NO money/order/status columns.
|
|
1804
|
+
*/
|
|
1805
|
+
export function initAgentPairingSchema(db) {
|
|
1806
|
+
db.exec(`
|
|
1807
|
+
CREATE TABLE IF NOT EXISTS agent_pairing_sessions (
|
|
1808
|
+
pairing_id TEXT PRIMARY KEY, -- par_xxx (agent holds this)
|
|
1809
|
+
user_code TEXT NOT NULL, -- short one-time code the human approves
|
|
1810
|
+
code_challenge TEXT NOT NULL, -- PKCE S256 = base64url(sha256(verifier))
|
|
1811
|
+
agent_label TEXT,
|
|
1812
|
+
agent_pubkey TEXT, -- RESERVED (PoP); stored if sent, NOT verified in C1
|
|
1813
|
+
reason TEXT, -- agent free-text reason (shown in consent)
|
|
1814
|
+
capabilities TEXT NOT NULL DEFAULT '[]', -- requested SAFE scopes (validated safe-only at start)
|
|
1815
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | consumed | expired | revoked
|
|
1816
|
+
human_id TEXT, -- set on approve
|
|
1817
|
+
grant_id TEXT, -- set on approve (issued grant; token_hash filled at retrieve)
|
|
1818
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1819
|
+
expires_at TEXT NOT NULL, -- short TTL
|
|
1820
|
+
approved_at TEXT,
|
|
1821
|
+
consumed_at TEXT -- one-time retrieval marker
|
|
1822
|
+
)
|
|
1823
|
+
`);
|
|
1824
|
+
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_aps_user_code ON agent_pairing_sessions(user_code)`);
|
|
1825
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_aps_status ON agent_pairing_sessions(status, expires_at)`);
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* RFC-020 PR-C2a — per-request audit of delegation-grant authorizations (RFC-020 §3.7).
|
|
1829
|
+
*
|
|
1830
|
+
* Records each grant-scoped access attempt (allow/deny + reason) so "every agent action
|
|
1831
|
+
* is backed by an accountable human" is checkable. Append-only log; pure idempotent DDL;
|
|
1832
|
+
* composition root auto-runs it for MCP. NO money/order/status columns.
|
|
1833
|
+
*/
|
|
1834
|
+
export function initAgentGrantAuthLogSchema(db) {
|
|
1835
|
+
db.exec(`
|
|
1836
|
+
CREATE TABLE IF NOT EXISTS agent_grant_auth_log (
|
|
1837
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1838
|
+
grant_id TEXT, -- null when token missing / grant not found
|
|
1839
|
+
human_id TEXT, -- the accountable human (when resolved)
|
|
1840
|
+
capability TEXT NOT NULL, -- the required safe scope checked
|
|
1841
|
+
outcome TEXT NOT NULL, -- 'allow' | 'deny'
|
|
1842
|
+
error_code TEXT, -- typed denial reason (null on allow)
|
|
1843
|
+
ts TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1844
|
+
)
|
|
1845
|
+
`);
|
|
1846
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agal_grant ON agent_grant_auth_log(grant_id, ts)`);
|
|
1847
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agal_ts ON agent_grant_auth_log(ts)`);
|
|
1848
|
+
}
|