@seasonkoh/webaz 0.1.7 → 0.1.9

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.
Files changed (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31947 -0
  22. package/dist/pwa/public/i18n.js +5751 -0
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +21 -0
  25. package/dist/pwa/public/manifest.json +48 -0
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +535 -0
  28. package/dist/pwa/public/sw.js +63 -0
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9679 -698
  153. package/package.json +11 -4
@@ -0,0 +1,358 @@
1
+ /**
2
+ * L4-4 · 技能市场(知识技能 / Knowledge Skill Marketplace)
3
+ *
4
+ * 与同目录 skill-engine.ts(卖家自动化插件)是两套独立产品:
5
+ * - skill-engine.ts = 卖家行为自动化(auto_accept / catalog_sync…),config 驱动、免费、无审计
6
+ * - 本文件 = 内容型可购买技能(模板/提示词/指南/清单),人人可发、经 WebAZ 审计后上架、他人付费
7
+ *
8
+ * 核心循环:
9
+ * 作者发布(submitted) → WebAZ 人工审计(approved/rejected) → 上架 →
10
+ * 买家购买/按次使用 → 解锁正文 → 作者收入(净额入钱包,协议费入 sys_protocol)
11
+ *
12
+ * 计费模式:
13
+ * free 免费解锁
14
+ * one_time 一次性买断,永久解锁
15
+ * per_use 按次付费,每次读取正文都扣 price
16
+ *
17
+ * 收益隔离(重要):技能销售收入是独立资金流,
18
+ * 绝不进入 PV 二元匹配 / 推土机三级佣金 / fund_base —— 保持双引擎解耦、规避 PV 合规问题。
19
+ */
20
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
21
+ export const SKILL_KINDS = ['template', 'prompt', 'guide', 'checklist'];
22
+ export const SKILL_BILLING_MODES = ['free', 'one_time', 'per_use'];
23
+ export const SKILL_KIND_META = {
24
+ template: { label: '模板', label_en: 'Template', icon: '📋' },
25
+ prompt: { label: '提示词', label_en: 'Prompt', icon: '💬' },
26
+ guide: { label: '指南', label_en: 'Guide', icon: '📖' },
27
+ checklist: { label: '清单', label_en: 'Checklist', icon: '✅' },
28
+ };
29
+ const PRICE_CAP = 100000; // 单个技能价格上限(WAZ),防误填
30
+ const round2 = (n) => Math.round(Number(n) * 100) / 100; // 金额统一 2 位精度,防结算端凑整造钱
31
+ // 公开字段(不含 content)
32
+ const PUBLIC_COLS = `
33
+ l.id, l.author_id, l.title, l.summary, l.preview, l.category, l.skill_kind,
34
+ l.billing_mode, l.price, l.status, l.total_sales, l.rating, l.rating_count,
35
+ l.active, l.created_at, l.updated_at
36
+ `;
37
+ // ─── Schema ───────────────────────────────────────────────────
38
+ export function initSkillMarketSchema(db) {
39
+ db.exec(`
40
+ CREATE TABLE IF NOT EXISTS skill_listings (
41
+ id TEXT PRIMARY KEY,
42
+ author_id TEXT NOT NULL REFERENCES users(id),
43
+ title TEXT NOT NULL,
44
+ summary TEXT NOT NULL DEFAULT '',
45
+ preview TEXT NOT NULL DEFAULT '',
46
+ content TEXT NOT NULL DEFAULT '',
47
+ category TEXT NOT NULL DEFAULT 'general',
48
+ skill_kind TEXT NOT NULL DEFAULT 'template',
49
+ billing_mode TEXT NOT NULL DEFAULT 'free',
50
+ price REAL NOT NULL DEFAULT 0,
51
+ status TEXT NOT NULL DEFAULT 'submitted',
52
+ audit_note TEXT,
53
+ reviewed_by TEXT,
54
+ reviewed_at TEXT,
55
+ total_sales INTEGER NOT NULL DEFAULT 0,
56
+ total_revenue REAL NOT NULL DEFAULT 0,
57
+ rating REAL NOT NULL DEFAULT 5.0,
58
+ rating_count INTEGER NOT NULL DEFAULT 0,
59
+ active INTEGER NOT NULL DEFAULT 0,
60
+ created_at TEXT DEFAULT (datetime('now')),
61
+ updated_at TEXT DEFAULT (datetime('now'))
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS skill_orders (
65
+ id TEXT PRIMARY KEY,
66
+ listing_id TEXT NOT NULL REFERENCES skill_listings(id),
67
+ buyer_id TEXT NOT NULL REFERENCES users(id),
68
+ billing_mode TEXT NOT NULL,
69
+ amount_paid REAL NOT NULL DEFAULT 0,
70
+ protocol_fee REAL NOT NULL DEFAULT 0,
71
+ author_net REAL NOT NULL DEFAULT 0,
72
+ created_at TEXT DEFAULT (datetime('now'))
73
+ );
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_sklm_status ON skill_listings(status, active);
76
+ CREATE INDEX IF NOT EXISTS idx_sklm_author ON skill_listings(author_id);
77
+ CREATE INDEX IF NOT EXISTS idx_sklm_category ON skill_listings(category, status);
78
+ CREATE INDEX IF NOT EXISTS idx_sko_buyer ON skill_orders(buyer_id);
79
+ CREATE INDEX IF NOT EXISTS idx_sko_listing ON skill_orders(listing_id);
80
+ `);
81
+ }
82
+ function validateListingInput(input) {
83
+ if (!input.title || !input.title.trim())
84
+ throw new Error('请填写技能标题');
85
+ if (!input.content || !input.content.trim())
86
+ throw new Error('请填写技能正文内容');
87
+ if (!SKILL_BILLING_MODES.includes(input.billingMode))
88
+ throw new Error('计费模式无效');
89
+ if (input.skillKind && !SKILL_KINDS.includes(input.skillKind))
90
+ throw new Error('技能类型无效');
91
+ const price = Number(input.price ?? 0);
92
+ if (!Number.isFinite(price) || price < 0 || price > PRICE_CAP)
93
+ throw new Error(`价格必须 0–${PRICE_CAP} WAZ`);
94
+ if (input.billingMode === 'free' && price !== 0)
95
+ throw new Error('免费技能价格必须为 0');
96
+ if (input.billingMode !== 'free' && price <= 0)
97
+ throw new Error('付费技能价格必须大于 0');
98
+ }
99
+ /** 任何登录用户都可发布;发布即进入审核队列(status=submitted, active=0) */
100
+ export function publishListing(db, input) {
101
+ const author = db.prepare('SELECT id FROM users WHERE id = ?').get(input.authorId);
102
+ if (!author)
103
+ throw new Error('用户不存在');
104
+ const price = round2(Number(input.price ?? 0));
105
+ validateListingInput({ ...input, price });
106
+ const id = generateId('skm');
107
+ db.prepare(`
108
+ INSERT INTO skill_listings (id, author_id, title, summary, preview, content, category, skill_kind, billing_mode, price, status, active)
109
+ VALUES (?,?,?,?,?,?,?,?,?,?, 'submitted', 0)
110
+ `).run(id, input.authorId, input.title.trim(), (input.summary ?? '').trim(), (input.preview ?? '').trim(), input.content, (input.category ?? 'general').trim(), input.skillKind ?? 'template', input.billingMode, price);
111
+ return getListingRaw(db, id);
112
+ }
113
+ /**
114
+ * 作者修改自己的技能。已上架(approved)技能修改后需重新审核(status→submitted, active=0),
115
+ * 防止"审通过后偷换正文"。已下架/被拒/待审的可自由修改。
116
+ */
117
+ export function updateListing(db, id, authorId, patch) {
118
+ const cur = getListingRaw(db, id);
119
+ if (!cur)
120
+ throw new Error('技能不存在');
121
+ if (cur.author_id !== authorId)
122
+ throw new Error('仅作者本人可修改');
123
+ const next = {
124
+ title: patch.title !== undefined ? patch.title.trim() : cur.title,
125
+ summary: patch.summary !== undefined ? patch.summary.trim() : cur.summary,
126
+ preview: patch.preview !== undefined ? patch.preview.trim() : cur.preview,
127
+ content: patch.content !== undefined ? patch.content : cur.content,
128
+ category: patch.category !== undefined ? patch.category.trim() : cur.category,
129
+ skill_kind: (patch.skillKind ?? cur.skill_kind),
130
+ billing_mode: (patch.billingMode ?? cur.billing_mode),
131
+ price: round2(patch.price !== undefined ? Number(patch.price) : cur.price),
132
+ };
133
+ validateListingInput({ title: next.title, content: next.content, billingMode: next.billing_mode, skillKind: next.skill_kind, price: next.price });
134
+ // approved 技能被修改 → 回到审核队列
135
+ const reaudit = cur.status === 'approved';
136
+ db.prepare(`
137
+ UPDATE skill_listings SET
138
+ title=?, summary=?, preview=?, content=?, category=?, skill_kind=?, billing_mode=?, price=?,
139
+ status = CASE WHEN ?=1 THEN 'submitted' ELSE status END,
140
+ active = CASE WHEN ?=1 THEN 0 ELSE active END,
141
+ updated_at = datetime('now')
142
+ WHERE id=?
143
+ `).run(next.title, next.summary, next.preview, next.content, next.category, next.skill_kind, next.billing_mode, next.price, reaudit ? 1 : 0, reaudit ? 1 : 0, id);
144
+ return getListingRaw(db, id);
145
+ }
146
+ /** 作者下架(不删除,可重新提交审核) */
147
+ export function delistListing(db, id, authorId) {
148
+ const cur = getListingRaw(db, id);
149
+ if (!cur)
150
+ throw new Error('技能不存在');
151
+ if (cur.author_id !== authorId)
152
+ throw new Error('仅作者本人可下架');
153
+ db.prepare("UPDATE skill_listings SET status='delisted', active=0, updated_at=datetime('now') WHERE id=?").run(id);
154
+ }
155
+ /** 作者把被拒/下架的技能重新提交审核 */
156
+ export function resubmitListing(db, id, authorId) {
157
+ const cur = getListingRaw(db, id);
158
+ if (!cur)
159
+ throw new Error('技能不存在');
160
+ if (cur.author_id !== authorId)
161
+ throw new Error('仅作者本人可提交');
162
+ if (cur.status === 'approved')
163
+ throw new Error('技能已上架,无需重新提交');
164
+ db.prepare("UPDATE skill_listings SET status='submitted', active=0, audit_note=NULL, updated_at=datetime('now') WHERE id=?").run(id);
165
+ }
166
+ // ─── 查询 ─────────────────────────────────────────────────────
167
+ function getListingRaw(db, id) {
168
+ return db.prepare('SELECT * FROM skill_listings WHERE id = ?').get(id);
169
+ }
170
+ /** 公开列表:仅 approved + active 的技能,不含 content */
171
+ export function listMarket(db, filter = {}) {
172
+ const params = [];
173
+ let ownedSelect = '';
174
+ if (filter.viewerId) {
175
+ ownedSelect = `, (SELECT COUNT(*) FROM skill_orders o WHERE o.listing_id = l.id AND o.buyer_id = ?) as owned`;
176
+ params.push(filter.viewerId);
177
+ }
178
+ let sql = `SELECT ${PUBLIC_COLS}, u.name as author_name ${ownedSelect}
179
+ FROM skill_listings l JOIN users u ON l.author_id = u.id
180
+ WHERE l.status = 'approved' AND l.active = 1`;
181
+ if (filter.category) {
182
+ sql += ' AND l.category = ?';
183
+ params.push(filter.category);
184
+ }
185
+ if (filter.skillKind) {
186
+ sql += ' AND l.skill_kind = ?';
187
+ params.push(filter.skillKind);
188
+ }
189
+ if (filter.billingMode) {
190
+ sql += ' AND l.billing_mode = ?';
191
+ params.push(filter.billingMode);
192
+ }
193
+ if (filter.query) {
194
+ sql += ' AND (l.title LIKE ? OR l.summary LIKE ?)';
195
+ params.push(`%${filter.query}%`, `%${filter.query}%`);
196
+ }
197
+ sql += ' ORDER BY l.total_sales DESC, l.rating DESC, l.created_at DESC LIMIT ?';
198
+ params.push(filter.limit ?? 30);
199
+ return db.prepare(sql).all(...params);
200
+ }
201
+ /** 公开详情(不含 content);附加 owned 标记。未上架的仅作者本人可见,防 metadata 泄露 */
202
+ export function getMarketDetail(db, id, viewerId) {
203
+ const row = db.prepare(`
204
+ SELECT ${PUBLIC_COLS}, u.name as author_name
205
+ FROM skill_listings l JOIN users u ON l.author_id = u.id
206
+ WHERE l.id = ?
207
+ `).get(id);
208
+ if (!row)
209
+ return null;
210
+ if (row.status !== 'approved' && row.author_id !== viewerId)
211
+ return null;
212
+ if (viewerId)
213
+ row.owned = hasUnlock(db, viewerId, id) ? 1 : 0;
214
+ return row;
215
+ }
216
+ /** 作者视角:自己的全部技能(含各状态、含 content 由调用方决定是否回传) */
217
+ export function getMyListings(db, authorId) {
218
+ return db.prepare(`
219
+ SELECT ${PUBLIC_COLS}, l.audit_note, l.reviewed_at, l.total_revenue
220
+ FROM skill_listings l WHERE l.author_id = ? ORDER BY l.created_at DESC
221
+ `).all(authorId);
222
+ }
223
+ // ─── 访问权 / 解锁 ────────────────────────────────────────────
224
+ /** one_time/free 是否已永久解锁(per_use 不算永久解锁) */
225
+ function hasUnlock(db, userId, listingId) {
226
+ const r = db.prepare(`
227
+ SELECT 1 FROM skill_orders o
228
+ WHERE o.listing_id = ? AND o.buyer_id = ? AND o.billing_mode IN ('free','one_time') LIMIT 1
229
+ `).get(listingId, userId);
230
+ return !!r;
231
+ }
232
+ /** 扣买家、加作者净额、协议费入 sys_protocol;记 skill_orders。feeRate 由路由层从协议参数传入。 */
233
+ function settlePayment(db, listing, buyerId, feeRate) {
234
+ const price = Number(listing.price);
235
+ const fee = Math.round(price * feeRate * 100) / 100;
236
+ const net = Math.round((price - fee) * 100) / 100;
237
+ const tx = db.transaction(() => {
238
+ const w = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(buyerId);
239
+ if (!w || w.balance < price)
240
+ throw new Error('余额不足,请先充值');
241
+ db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(price, buyerId);
242
+ db.prepare('UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?').run(net, net, listing.author_id);
243
+ db.prepare("INSERT OR IGNORE INTO wallets (user_id, balance) VALUES ('sys_protocol', 0)").run();
244
+ if (fee > 0)
245
+ db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = 'sys_protocol'").run(fee);
246
+ db.prepare('UPDATE skill_listings SET total_sales = total_sales + 1, total_revenue = total_revenue + ? WHERE id = ?').run(price, listing.id);
247
+ db.prepare('INSERT INTO skill_orders (id, listing_id, buyer_id, billing_mode, amount_paid, protocol_fee, author_net) VALUES (?,?,?,?,?,?,?)')
248
+ .run(generateId('sko'), listing.id, buyerId, listing.billing_mode, price, fee, net);
249
+ });
250
+ tx();
251
+ return { fee, net, amount: price };
252
+ }
253
+ /**
254
+ * 购买/解锁(用于 free 与 one_time)。per_use 不走此路径——按次扣费在 readContent 内完成。
255
+ * 自己不能买自己的技能。
256
+ */
257
+ export function purchaseListing(db, buyerId, listingId, feeRate) {
258
+ const listing = getListingRaw(db, listingId);
259
+ if (!listing || listing.status !== 'approved' || !listing.active)
260
+ throw new Error('技能不存在或未上架');
261
+ if (listing.author_id === buyerId)
262
+ throw new Error('不能购买自己发布的技能');
263
+ if (listing.billing_mode === 'per_use')
264
+ throw new Error('按次付费技能无需购买,直接使用即可');
265
+ if (hasUnlock(db, buyerId, listingId))
266
+ return { success: true, already_owned: true, message: '你已拥有此技能' };
267
+ if (listing.billing_mode === 'free') {
268
+ db.prepare('INSERT INTO skill_orders (id, listing_id, buyer_id, billing_mode, amount_paid, protocol_fee, author_net) VALUES (?,?,?,?,0,0,0)')
269
+ .run(generateId('sko'), listingId, buyerId, 'free');
270
+ db.prepare('UPDATE skill_listings SET total_sales = total_sales + 1 WHERE id = ?').run(listingId);
271
+ return { success: true, amount_paid: 0, message: '已解锁(免费)' };
272
+ }
273
+ // one_time
274
+ const r = settlePayment(db, listing, buyerId, feeRate);
275
+ return { success: true, amount_paid: r.amount, message: '购买成功' };
276
+ }
277
+ /**
278
+ * 读取正文(解锁后调用)。
279
+ * - 作者本人:直接返回。
280
+ * - free / one_time:必须已解锁(hasUnlock)。
281
+ * - per_use:每次读取扣费一次,然后返回。
282
+ */
283
+ export function readContent(db, userId, listingId, feeRate) {
284
+ const listing = getListingRaw(db, listingId);
285
+ if (!listing)
286
+ throw new Error('技能不存在');
287
+ // 作者本人随时可读
288
+ if (listing.author_id === userId) {
289
+ return { content: listing.content, billing_mode: listing.billing_mode };
290
+ }
291
+ if (listing.status !== 'approved' || !listing.active)
292
+ throw new Error('技能未上架');
293
+ if (listing.billing_mode === 'per_use') {
294
+ const r = settlePayment(db, listing, userId, feeRate);
295
+ return { content: listing.content, charged: r.amount, billing_mode: 'per_use' };
296
+ }
297
+ if (!hasUnlock(db, userId, listingId))
298
+ throw new Error('请先购买/解锁此技能');
299
+ return { content: listing.content, billing_mode: listing.billing_mode };
300
+ }
301
+ /** 我的技能库:已解锁(free/one_time)的技能 + per_use 使用过的技能 */
302
+ export function getMyLibrary(db, userId) {
303
+ return db.prepare(`
304
+ SELECT ${PUBLIC_COLS}, u.name as author_name, 1 as owned,
305
+ MAX(o.created_at) as last_used
306
+ FROM skill_orders o
307
+ JOIN skill_listings l ON o.listing_id = l.id
308
+ JOIN users u ON l.author_id = u.id
309
+ WHERE o.buyer_id = ?
310
+ GROUP BY l.id
311
+ ORDER BY last_used DESC
312
+ `).all(userId);
313
+ }
314
+ // ─── 审计(WebAZ content admin)─────────────────────────────────
315
+ export function listPendingAudit(db, limit = 100) {
316
+ return db.prepare(`
317
+ SELECT l.*, u.name as author_name
318
+ FROM skill_listings l JOIN users u ON l.author_id = u.id
319
+ WHERE l.status = 'submitted' ORDER BY l.created_at ASC LIMIT ?
320
+ `).all(limit);
321
+ }
322
+ export function auditListing(db, listingId, reviewerId, decision, note) {
323
+ const cur = getListingRaw(db, listingId);
324
+ if (!cur)
325
+ throw new Error('技能不存在');
326
+ if (cur.status !== 'submitted')
327
+ throw new Error('该技能不在待审状态');
328
+ if (decision === 'reject' && (!note || !note.trim()))
329
+ throw new Error('拒绝需填写原因');
330
+ if (decision === 'approve') {
331
+ db.prepare("UPDATE skill_listings SET status='approved', active=1, audit_note=?, reviewed_by=?, reviewed_at=datetime('now'), updated_at=datetime('now') WHERE id=?")
332
+ .run(note?.trim() ?? null, reviewerId, listingId);
333
+ }
334
+ else {
335
+ db.prepare("UPDATE skill_listings SET status='rejected', active=0, audit_note=?, reviewed_by=?, reviewed_at=datetime('now'), updated_at=datetime('now') WHERE id=?")
336
+ .run(note.trim(), reviewerId, listingId);
337
+ }
338
+ return getListingRaw(db, listingId);
339
+ }
340
+ // ─── 给 Agent / MCP 用的格式化 ─────────────────────────────────
341
+ export function formatListingForAgent(l) {
342
+ const meta = SKILL_KIND_META[l.skill_kind];
343
+ return {
344
+ id: l.id,
345
+ title: l.title,
346
+ summary: l.summary,
347
+ kind: l.skill_kind,
348
+ kind_label: meta?.label ?? l.skill_kind,
349
+ kind_icon: meta?.icon ?? '⚙️',
350
+ category: l.category,
351
+ author: l.author_name,
352
+ billing_mode: l.billing_mode,
353
+ price: l.price,
354
+ sales: l.total_sales,
355
+ rating: l.rating,
356
+ owned: Boolean(l.owned),
357
+ };
358
+ }