chainlesschain 0.49.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent-network.js +785 -0
- package/src/commands/automation.js +654 -0
- package/src/commands/dao.js +565 -0
- package/src/commands/did-v2.js +620 -0
- package/src/commands/economy.js +578 -0
- package/src/commands/evolution.js +391 -0
- package/src/commands/hmemory.js +442 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/multimodal.js +404 -0
- package/src/commands/perf.js +433 -0
- package/src/commands/pipeline.js +449 -0
- package/src/commands/plugin-ecosystem.js +517 -0
- package/src/commands/sandbox.js +401 -0
- package/src/commands/social.js +311 -0
- package/src/commands/sso.js +798 -0
- package/src/commands/workflow.js +320 -0
- package/src/commands/zkp.js +227 -1
- package/src/index.js +27 -0
- package/src/lib/agent-economy.js +479 -0
- package/src/lib/agent-network.js +1121 -0
- package/src/lib/automation-engine.js +948 -0
- package/src/lib/dao-governance.js +569 -0
- package/src/lib/did-v2-manager.js +1127 -0
- package/src/lib/evolution-system.js +453 -0
- package/src/lib/hierarchical-memory.js +481 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/multimodal.js +39 -12
- package/src/lib/perf-tuning.js +734 -0
- package/src/lib/pipeline-orchestrator.js +928 -0
- package/src/lib/plugin-ecosystem.js +1109 -0
- package/src/lib/sandbox-v2.js +306 -0
- package/src/lib/social-graph-analytics.js +707 -0
- package/src/lib/sso-manager.js +841 -0
- package/src/lib/workflow-engine.js +454 -1
- package/src/lib/zkp-engine.js +249 -20
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Ecosystem v2.0 — CLI port of Phase 64 智能插件生态2.0
|
|
3
|
+
* (docs/design/modules/64_智能插件生态2.0.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop ships 8 IPC handlers for an AI-driven plugin ecosystem:
|
|
6
|
+
* recommendation (user profile + collaborative filtering + context),
|
|
7
|
+
* dependency resolver (version negotiation + cycle detection),
|
|
8
|
+
* sandbox tester (resource-limited isolated testing), AI code reviewer
|
|
9
|
+
* (security + quality + API compliance), publisher (package + sign +
|
|
10
|
+
* upload), and revenue manager (download tracking + dev share).
|
|
11
|
+
*
|
|
12
|
+
* CLI port is headless and heuristic-only:
|
|
13
|
+
* - Recommendation uses category overlap + download count + rating
|
|
14
|
+
* weighting against user's installed plugins (NO collaborative
|
|
15
|
+
* filtering / ML model / embedding match)
|
|
16
|
+
* - Dependency resolver does real flatten + cycle detection + exact
|
|
17
|
+
* version conflict detection (NO full semver range negotiation)
|
|
18
|
+
* - Sandbox tester is caller-pushed: you give the test result, CLI
|
|
19
|
+
* just records it (NO real vm / process isolation)
|
|
20
|
+
* - AI review is regex-based security + quality rules with weighted
|
|
21
|
+
* severity score (NO LLM call)
|
|
22
|
+
* - Publish flips a status flag + computes a content hash (NO real
|
|
23
|
+
* packaging / signing / upload)
|
|
24
|
+
* - Revenue tracking is double-entry event log with calculated
|
|
25
|
+
* developer share (NO payment gateway)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import crypto from "crypto";
|
|
29
|
+
|
|
30
|
+
/* ── Constants ───────────────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
export const REVIEW_SEVERITY = Object.freeze({
|
|
33
|
+
INFO: "info",
|
|
34
|
+
WARNING: "warning",
|
|
35
|
+
CRITICAL: "critical",
|
|
36
|
+
BLOCKER: "blocker",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const PUBLISH_STATUS = Object.freeze({
|
|
40
|
+
DRAFT: "draft",
|
|
41
|
+
REVIEWING: "reviewing",
|
|
42
|
+
APPROVED: "approved",
|
|
43
|
+
REJECTED: "rejected",
|
|
44
|
+
PUBLISHED: "published",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const REVENUE_TYPE = Object.freeze({
|
|
48
|
+
DOWNLOAD: "download",
|
|
49
|
+
SUBSCRIPTION: "subscription",
|
|
50
|
+
DONATION: "donation",
|
|
51
|
+
PREMIUM: "premium",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const INSTALL_STATUS = Object.freeze({
|
|
55
|
+
PENDING: "pending",
|
|
56
|
+
RESOLVING: "resolving",
|
|
57
|
+
INSTALLED: "installed",
|
|
58
|
+
FAILED: "failed",
|
|
59
|
+
UNINSTALLED: "uninstalled",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const DEP_KIND = Object.freeze({
|
|
63
|
+
REQUIRED: "required",
|
|
64
|
+
OPTIONAL: "optional",
|
|
65
|
+
PEER: "peer",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const SANDBOX_RESULT = Object.freeze({
|
|
69
|
+
PASSED: "passed",
|
|
70
|
+
FAILED: "failed",
|
|
71
|
+
TIMEOUT: "timeout",
|
|
72
|
+
RESOURCE_EXCEEDED: "resource-exceeded",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const DEFAULT_DEVELOPER_SHARE = 0.7;
|
|
76
|
+
export const DEFAULT_SANDBOX_MEMORY_MB = 256;
|
|
77
|
+
export const DEFAULT_SANDBOX_TIMEOUT_MS = 30_000;
|
|
78
|
+
export const DEFAULT_MAX_RECOMMENDATIONS = 20;
|
|
79
|
+
|
|
80
|
+
/* ── Heuristic review rules ──────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
const REVIEW_RULES = Object.freeze([
|
|
83
|
+
{
|
|
84
|
+
id: "code-injection-eval",
|
|
85
|
+
pattern: /\beval\s*\(/g,
|
|
86
|
+
severity: REVIEW_SEVERITY.BLOCKER,
|
|
87
|
+
message: "Use of eval() — code injection risk",
|
|
88
|
+
penalty: 40,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "code-injection-new-function",
|
|
92
|
+
pattern: /new\s+Function\s*\(/g,
|
|
93
|
+
severity: REVIEW_SEVERITY.CRITICAL,
|
|
94
|
+
message: "Dynamic Function constructor — code injection risk",
|
|
95
|
+
penalty: 30,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "shell-exec",
|
|
99
|
+
pattern: /\bexec(?:Sync)?\s*\(|spawnSync\s*\(/g,
|
|
100
|
+
severity: REVIEW_SEVERITY.CRITICAL,
|
|
101
|
+
message: "Shell command execution — command injection risk",
|
|
102
|
+
penalty: 20,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "fs-write",
|
|
106
|
+
pattern: /fs\.(?:writeFile|unlink|rm|rmdir|mkdir)/g,
|
|
107
|
+
severity: REVIEW_SEVERITY.WARNING,
|
|
108
|
+
message: "Filesystem write/delete — review scope carefully",
|
|
109
|
+
penalty: 10,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "child-process",
|
|
113
|
+
pattern:
|
|
114
|
+
/require\(\s*['"]child_process['"]\s*\)|from\s+['"]child_process['"]/g,
|
|
115
|
+
severity: REVIEW_SEVERITY.WARNING,
|
|
116
|
+
message: "child_process import — review command usage",
|
|
117
|
+
penalty: 10,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "network-request",
|
|
121
|
+
pattern: /\bfetch\s*\(|\bXMLHttpRequest\b|require\(\s*['"]http['"]\s*\)/g,
|
|
122
|
+
severity: REVIEW_SEVERITY.INFO,
|
|
123
|
+
message: "Network request — ensure URLs are allow-listed",
|
|
124
|
+
penalty: 3,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "env-access",
|
|
128
|
+
pattern: /process\.env\b/g,
|
|
129
|
+
severity: REVIEW_SEVERITY.INFO,
|
|
130
|
+
message: "Environment variable access — review secrets exposure",
|
|
131
|
+
penalty: 2,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "hardcoded-secret",
|
|
135
|
+
pattern:
|
|
136
|
+
/(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"][A-Za-z0-9_\-]{16,}['"]/gi,
|
|
137
|
+
severity: REVIEW_SEVERITY.BLOCKER,
|
|
138
|
+
message: "Hardcoded credential — remove before publishing",
|
|
139
|
+
penalty: 40,
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/* ── Schema ──────────────────────────────────────────────── */
|
|
144
|
+
|
|
145
|
+
export function ensurePluginEcosystemTables(db) {
|
|
146
|
+
db.exec(`
|
|
147
|
+
CREATE TABLE IF NOT EXISTS plugin_eco_entries (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
name TEXT NOT NULL,
|
|
150
|
+
version TEXT NOT NULL,
|
|
151
|
+
developer_id TEXT NOT NULL,
|
|
152
|
+
category TEXT,
|
|
153
|
+
description TEXT,
|
|
154
|
+
manifest TEXT,
|
|
155
|
+
source_hash TEXT,
|
|
156
|
+
status TEXT NOT NULL,
|
|
157
|
+
download_count INTEGER DEFAULT 0,
|
|
158
|
+
avg_rating REAL DEFAULT 0.0,
|
|
159
|
+
revenue_total REAL DEFAULT 0.0,
|
|
160
|
+
created_at INTEGER NOT NULL,
|
|
161
|
+
updated_at INTEGER NOT NULL,
|
|
162
|
+
published_at INTEGER
|
|
163
|
+
)
|
|
164
|
+
`);
|
|
165
|
+
|
|
166
|
+
db.exec(`
|
|
167
|
+
CREATE TABLE IF NOT EXISTS plugin_eco_dependencies (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
plugin_id TEXT NOT NULL,
|
|
170
|
+
dep_plugin_id TEXT NOT NULL,
|
|
171
|
+
dep_version TEXT NOT NULL,
|
|
172
|
+
kind TEXT NOT NULL,
|
|
173
|
+
created_at INTEGER NOT NULL
|
|
174
|
+
)
|
|
175
|
+
`);
|
|
176
|
+
|
|
177
|
+
db.exec(`
|
|
178
|
+
CREATE TABLE IF NOT EXISTS plugin_eco_installs (
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
user_id TEXT NOT NULL,
|
|
181
|
+
plugin_id TEXT NOT NULL,
|
|
182
|
+
version TEXT NOT NULL,
|
|
183
|
+
status TEXT NOT NULL,
|
|
184
|
+
error_message TEXT,
|
|
185
|
+
installed_at INTEGER NOT NULL,
|
|
186
|
+
updated_at INTEGER NOT NULL
|
|
187
|
+
)
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
db.exec(`
|
|
191
|
+
CREATE TABLE IF NOT EXISTS plugin_eco_reviews (
|
|
192
|
+
id TEXT PRIMARY KEY,
|
|
193
|
+
plugin_id TEXT NOT NULL,
|
|
194
|
+
source_hash TEXT,
|
|
195
|
+
issues TEXT,
|
|
196
|
+
score REAL NOT NULL,
|
|
197
|
+
severity TEXT NOT NULL,
|
|
198
|
+
strictness TEXT NOT NULL,
|
|
199
|
+
created_at INTEGER NOT NULL
|
|
200
|
+
)
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
db.exec(`
|
|
204
|
+
CREATE TABLE IF NOT EXISTS plugin_eco_sandbox_tests (
|
|
205
|
+
id TEXT PRIMARY KEY,
|
|
206
|
+
plugin_id TEXT NOT NULL,
|
|
207
|
+
test_suite TEXT,
|
|
208
|
+
result TEXT NOT NULL,
|
|
209
|
+
metrics TEXT,
|
|
210
|
+
logs TEXT,
|
|
211
|
+
duration_ms INTEGER,
|
|
212
|
+
created_at INTEGER NOT NULL
|
|
213
|
+
)
|
|
214
|
+
`);
|
|
215
|
+
|
|
216
|
+
db.exec(`
|
|
217
|
+
CREATE TABLE IF NOT EXISTS plugin_eco_revenue (
|
|
218
|
+
id TEXT PRIMARY KEY,
|
|
219
|
+
developer_id TEXT NOT NULL,
|
|
220
|
+
plugin_id TEXT NOT NULL,
|
|
221
|
+
user_id TEXT,
|
|
222
|
+
type TEXT NOT NULL,
|
|
223
|
+
amount REAL NOT NULL,
|
|
224
|
+
developer_share REAL NOT NULL,
|
|
225
|
+
platform_share REAL NOT NULL,
|
|
226
|
+
created_at INTEGER NOT NULL
|
|
227
|
+
)
|
|
228
|
+
`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── Internals ───────────────────────────────────────────── */
|
|
232
|
+
|
|
233
|
+
const _now = () => Date.now();
|
|
234
|
+
const _uid = (prefix) => `${prefix}-${crypto.randomBytes(6).toString("hex")}`;
|
|
235
|
+
|
|
236
|
+
function _parseJSON(value, fallback) {
|
|
237
|
+
if (!value) return fallback;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(value);
|
|
240
|
+
} catch {
|
|
241
|
+
return fallback;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _sha256Hex(input) {
|
|
246
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function _rowToEntry(row) {
|
|
250
|
+
if (!row) return null;
|
|
251
|
+
return {
|
|
252
|
+
id: row.id,
|
|
253
|
+
name: row.name,
|
|
254
|
+
version: row.version,
|
|
255
|
+
developerId: row.developer_id,
|
|
256
|
+
category: row.category,
|
|
257
|
+
description: row.description,
|
|
258
|
+
manifest: _parseJSON(row.manifest, {}),
|
|
259
|
+
sourceHash: row.source_hash,
|
|
260
|
+
status: row.status,
|
|
261
|
+
downloadCount: row.download_count,
|
|
262
|
+
avgRating: row.avg_rating,
|
|
263
|
+
revenueTotal: row.revenue_total,
|
|
264
|
+
createdAt: row.created_at,
|
|
265
|
+
updatedAt: row.updated_at,
|
|
266
|
+
publishedAt: row.published_at,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function _rowToDep(row) {
|
|
271
|
+
if (!row) return null;
|
|
272
|
+
return {
|
|
273
|
+
id: row.id,
|
|
274
|
+
pluginId: row.plugin_id,
|
|
275
|
+
depPluginId: row.dep_plugin_id,
|
|
276
|
+
depVersion: row.dep_version,
|
|
277
|
+
kind: row.kind,
|
|
278
|
+
createdAt: row.created_at,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function _rowToInstall(row) {
|
|
283
|
+
if (!row) return null;
|
|
284
|
+
return {
|
|
285
|
+
id: row.id,
|
|
286
|
+
userId: row.user_id,
|
|
287
|
+
pluginId: row.plugin_id,
|
|
288
|
+
version: row.version,
|
|
289
|
+
status: row.status,
|
|
290
|
+
errorMessage: row.error_message,
|
|
291
|
+
installedAt: row.installed_at,
|
|
292
|
+
updatedAt: row.updated_at,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _rowToReview(row) {
|
|
297
|
+
if (!row) return null;
|
|
298
|
+
return {
|
|
299
|
+
id: row.id,
|
|
300
|
+
pluginId: row.plugin_id,
|
|
301
|
+
sourceHash: row.source_hash,
|
|
302
|
+
issues: _parseJSON(row.issues, []),
|
|
303
|
+
score: row.score,
|
|
304
|
+
severity: row.severity,
|
|
305
|
+
strictness: row.strictness,
|
|
306
|
+
createdAt: row.created_at,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function _rowToSandbox(row) {
|
|
311
|
+
if (!row) return null;
|
|
312
|
+
return {
|
|
313
|
+
id: row.id,
|
|
314
|
+
pluginId: row.plugin_id,
|
|
315
|
+
testSuite: row.test_suite,
|
|
316
|
+
result: row.result,
|
|
317
|
+
metrics: _parseJSON(row.metrics, {}),
|
|
318
|
+
logs: _parseJSON(row.logs, []),
|
|
319
|
+
durationMs: row.duration_ms,
|
|
320
|
+
createdAt: row.created_at,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _rowToRevenue(row) {
|
|
325
|
+
if (!row) return null;
|
|
326
|
+
return {
|
|
327
|
+
id: row.id,
|
|
328
|
+
developerId: row.developer_id,
|
|
329
|
+
pluginId: row.plugin_id,
|
|
330
|
+
userId: row.user_id,
|
|
331
|
+
type: row.type,
|
|
332
|
+
amount: row.amount,
|
|
333
|
+
developerShare: row.developer_share,
|
|
334
|
+
platformShare: row.platform_share,
|
|
335
|
+
createdAt: row.created_at,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function _getEntryRow(db, pluginId) {
|
|
340
|
+
return db
|
|
341
|
+
.prepare("SELECT * FROM plugin_eco_entries WHERE id = ?")
|
|
342
|
+
.get(pluginId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ── Plugin registry ─────────────────────────────────────── */
|
|
346
|
+
|
|
347
|
+
export function registerPlugin(
|
|
348
|
+
db,
|
|
349
|
+
{
|
|
350
|
+
name,
|
|
351
|
+
version,
|
|
352
|
+
developerId,
|
|
353
|
+
category = null,
|
|
354
|
+
description = null,
|
|
355
|
+
manifest = {},
|
|
356
|
+
} = {},
|
|
357
|
+
) {
|
|
358
|
+
if (!name) throw new Error("name is required");
|
|
359
|
+
if (!version) throw new Error("version is required");
|
|
360
|
+
if (!developerId) throw new Error("developerId is required");
|
|
361
|
+
|
|
362
|
+
const id = _uid("plg");
|
|
363
|
+
const now = _now();
|
|
364
|
+
db.prepare(
|
|
365
|
+
`INSERT INTO plugin_eco_entries (id, name, version, developer_id, category, description, manifest, status, download_count, avg_rating, revenue_total, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
366
|
+
).run(
|
|
367
|
+
id,
|
|
368
|
+
name,
|
|
369
|
+
version,
|
|
370
|
+
developerId,
|
|
371
|
+
category,
|
|
372
|
+
description,
|
|
373
|
+
JSON.stringify(manifest || {}),
|
|
374
|
+
PUBLISH_STATUS.DRAFT,
|
|
375
|
+
0,
|
|
376
|
+
0,
|
|
377
|
+
0,
|
|
378
|
+
now,
|
|
379
|
+
now,
|
|
380
|
+
);
|
|
381
|
+
return getPlugin(db, id);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function getPlugin(db, pluginId) {
|
|
385
|
+
return _rowToEntry(_getEntryRow(db, pluginId));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function listPlugins(
|
|
389
|
+
db,
|
|
390
|
+
{ category, status, developerId, limit = 100 } = {},
|
|
391
|
+
) {
|
|
392
|
+
const wheres = [];
|
|
393
|
+
const params = [];
|
|
394
|
+
if (category) {
|
|
395
|
+
wheres.push("category = ?");
|
|
396
|
+
params.push(category);
|
|
397
|
+
}
|
|
398
|
+
if (status) {
|
|
399
|
+
wheres.push("status = ?");
|
|
400
|
+
params.push(status);
|
|
401
|
+
}
|
|
402
|
+
if (developerId) {
|
|
403
|
+
wheres.push("developer_id = ?");
|
|
404
|
+
params.push(developerId);
|
|
405
|
+
}
|
|
406
|
+
const where = wheres.length ? `WHERE ${wheres.join(" AND ")}` : "";
|
|
407
|
+
params.push(limit);
|
|
408
|
+
const rows = db
|
|
409
|
+
.prepare(
|
|
410
|
+
`SELECT * FROM plugin_eco_entries ${where} ORDER BY download_count DESC LIMIT ?`,
|
|
411
|
+
)
|
|
412
|
+
.all(...params);
|
|
413
|
+
return rows.map(_rowToEntry);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function updatePluginStats(
|
|
417
|
+
db,
|
|
418
|
+
pluginId,
|
|
419
|
+
{ downloadCount = null, avgRating = null } = {},
|
|
420
|
+
) {
|
|
421
|
+
const entry = _getEntryRow(db, pluginId);
|
|
422
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
423
|
+
|
|
424
|
+
const sets = [];
|
|
425
|
+
const params = [];
|
|
426
|
+
if (downloadCount != null) {
|
|
427
|
+
sets.push("download_count = ?");
|
|
428
|
+
params.push(downloadCount);
|
|
429
|
+
}
|
|
430
|
+
if (avgRating != null) {
|
|
431
|
+
sets.push("avg_rating = ?");
|
|
432
|
+
params.push(avgRating);
|
|
433
|
+
}
|
|
434
|
+
if (!sets.length) return getPlugin(db, pluginId);
|
|
435
|
+
|
|
436
|
+
sets.push("updated_at = ?");
|
|
437
|
+
params.push(_now());
|
|
438
|
+
params.push(pluginId);
|
|
439
|
+
db.prepare(
|
|
440
|
+
`UPDATE plugin_eco_entries SET ${sets.join(", ")} WHERE id = ?`,
|
|
441
|
+
).run(...params);
|
|
442
|
+
return getPlugin(db, pluginId);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ── Dependencies ────────────────────────────────────────── */
|
|
446
|
+
|
|
447
|
+
export function addDependency(
|
|
448
|
+
db,
|
|
449
|
+
pluginId,
|
|
450
|
+
{ depPluginId, depVersion, kind = DEP_KIND.REQUIRED } = {},
|
|
451
|
+
) {
|
|
452
|
+
if (!depPluginId) throw new Error("depPluginId is required");
|
|
453
|
+
if (!depVersion) throw new Error("depVersion is required");
|
|
454
|
+
if (!Object.values(DEP_KIND).includes(kind)) {
|
|
455
|
+
throw new Error(`Unknown dep kind: ${kind}`);
|
|
456
|
+
}
|
|
457
|
+
const id = _uid("dep");
|
|
458
|
+
const now = _now();
|
|
459
|
+
db.prepare(
|
|
460
|
+
`INSERT INTO plugin_eco_dependencies (id, plugin_id, dep_plugin_id, dep_version, kind, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
461
|
+
).run(id, pluginId, depPluginId, depVersion, kind, now);
|
|
462
|
+
return _rowToDep(
|
|
463
|
+
db.prepare("SELECT * FROM plugin_eco_dependencies WHERE id = ?").get(id),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function listDependencies(db, pluginId) {
|
|
468
|
+
const rows = db
|
|
469
|
+
.prepare(
|
|
470
|
+
"SELECT * FROM plugin_eco_dependencies WHERE plugin_id = ? ORDER BY created_at ASC",
|
|
471
|
+
)
|
|
472
|
+
.all(pluginId);
|
|
473
|
+
return rows.map(_rowToDep);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function resolveDependencies(db, pluginId) {
|
|
477
|
+
const entry = _getEntryRow(db, pluginId);
|
|
478
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
479
|
+
|
|
480
|
+
const flat = new Map(); // depPluginId → { depVersion, kind, path[] }
|
|
481
|
+
const conflicts = [];
|
|
482
|
+
const circular = [];
|
|
483
|
+
const visited = new Set();
|
|
484
|
+
const stack = new Set();
|
|
485
|
+
|
|
486
|
+
function walk(currentId, path) {
|
|
487
|
+
if (stack.has(currentId)) {
|
|
488
|
+
circular.push([...path, currentId]);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (visited.has(currentId)) return;
|
|
492
|
+
visited.add(currentId);
|
|
493
|
+
stack.add(currentId);
|
|
494
|
+
|
|
495
|
+
const deps = listDependencies(db, currentId);
|
|
496
|
+
for (const dep of deps) {
|
|
497
|
+
if (dep.kind === DEP_KIND.OPTIONAL) continue;
|
|
498
|
+
const existing = flat.get(dep.depPluginId);
|
|
499
|
+
if (existing && existing.depVersion !== dep.depVersion) {
|
|
500
|
+
conflicts.push({
|
|
501
|
+
depPluginId: dep.depPluginId,
|
|
502
|
+
requiredByA: existing.path[existing.path.length - 1],
|
|
503
|
+
requiredByB: currentId,
|
|
504
|
+
versionA: existing.depVersion,
|
|
505
|
+
versionB: dep.depVersion,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (!existing) {
|
|
509
|
+
flat.set(dep.depPluginId, {
|
|
510
|
+
depVersion: dep.depVersion,
|
|
511
|
+
kind: dep.kind,
|
|
512
|
+
path: [...path, currentId],
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
walk(dep.depPluginId, [...path, currentId]);
|
|
516
|
+
}
|
|
517
|
+
stack.delete(currentId);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
walk(pluginId, []);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
pluginId,
|
|
524
|
+
dependencies: Array.from(flat.entries()).map(([id, info]) => ({
|
|
525
|
+
depPluginId: id,
|
|
526
|
+
depVersion: info.depVersion,
|
|
527
|
+
kind: info.kind,
|
|
528
|
+
path: info.path,
|
|
529
|
+
})),
|
|
530
|
+
conflicts,
|
|
531
|
+
circular,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* ── Installation ────────────────────────────────────────── */
|
|
536
|
+
|
|
537
|
+
export function installPlugin(
|
|
538
|
+
db,
|
|
539
|
+
{ userId, pluginId, version = null, autoResolveDeps = true } = {},
|
|
540
|
+
) {
|
|
541
|
+
if (!userId) throw new Error("userId is required");
|
|
542
|
+
const entry = _getEntryRow(db, pluginId);
|
|
543
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
544
|
+
|
|
545
|
+
const targetVersion = version || entry.version;
|
|
546
|
+
const resolved = autoResolveDeps ? resolveDependencies(db, pluginId) : null;
|
|
547
|
+
|
|
548
|
+
const id = _uid("ins");
|
|
549
|
+
const now = _now();
|
|
550
|
+
let status = INSTALL_STATUS.INSTALLED;
|
|
551
|
+
let errorMessage = null;
|
|
552
|
+
|
|
553
|
+
if (resolved && resolved.circular.length > 0) {
|
|
554
|
+
status = INSTALL_STATUS.FAILED;
|
|
555
|
+
errorMessage = `circular dependency: ${resolved.circular[0].join(" → ")}`;
|
|
556
|
+
} else if (resolved && resolved.conflicts.length > 0) {
|
|
557
|
+
status = INSTALL_STATUS.FAILED;
|
|
558
|
+
errorMessage = `version conflict: ${resolved.conflicts[0].depPluginId} (${resolved.conflicts[0].versionA} vs ${resolved.conflicts[0].versionB})`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
db.prepare(
|
|
562
|
+
`INSERT INTO plugin_eco_installs (id, user_id, plugin_id, version, status, error_message, installed_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
563
|
+
).run(id, userId, pluginId, targetVersion, status, errorMessage, now, now);
|
|
564
|
+
|
|
565
|
+
if (status === INSTALL_STATUS.INSTALLED) {
|
|
566
|
+
const newCount = (entry.download_count || 0) + 1;
|
|
567
|
+
db.prepare(
|
|
568
|
+
"UPDATE plugin_eco_entries SET download_count = ?, updated_at = ? WHERE id = ?",
|
|
569
|
+
).run(newCount, now, pluginId);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
install: _rowToInstall(
|
|
574
|
+
db.prepare("SELECT * FROM plugin_eco_installs WHERE id = ?").get(id),
|
|
575
|
+
),
|
|
576
|
+
resolved,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function listInstalls(
|
|
581
|
+
db,
|
|
582
|
+
{ userId, pluginId, status, limit = 100 } = {},
|
|
583
|
+
) {
|
|
584
|
+
const wheres = [];
|
|
585
|
+
const params = [];
|
|
586
|
+
if (userId) {
|
|
587
|
+
wheres.push("user_id = ?");
|
|
588
|
+
params.push(userId);
|
|
589
|
+
}
|
|
590
|
+
if (pluginId) {
|
|
591
|
+
wheres.push("plugin_id = ?");
|
|
592
|
+
params.push(pluginId);
|
|
593
|
+
}
|
|
594
|
+
if (status) {
|
|
595
|
+
wheres.push("status = ?");
|
|
596
|
+
params.push(status);
|
|
597
|
+
}
|
|
598
|
+
const where = wheres.length ? `WHERE ${wheres.join(" AND ")}` : "";
|
|
599
|
+
params.push(limit);
|
|
600
|
+
const rows = db
|
|
601
|
+
.prepare(
|
|
602
|
+
`SELECT * FROM plugin_eco_installs ${where} ORDER BY installed_at DESC LIMIT ?`,
|
|
603
|
+
)
|
|
604
|
+
.all(...params);
|
|
605
|
+
return rows.map(_rowToInstall);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function uninstallPlugin(db, installId) {
|
|
609
|
+
const row = db
|
|
610
|
+
.prepare("SELECT * FROM plugin_eco_installs WHERE id = ?")
|
|
611
|
+
.get(installId);
|
|
612
|
+
if (!row) throw new Error(`Install not found: ${installId}`);
|
|
613
|
+
if (row.status === INSTALL_STATUS.UNINSTALLED) {
|
|
614
|
+
throw new Error("Install already uninstalled");
|
|
615
|
+
}
|
|
616
|
+
const now = _now();
|
|
617
|
+
db.prepare(
|
|
618
|
+
"UPDATE plugin_eco_installs SET status = ?, updated_at = ? WHERE id = ?",
|
|
619
|
+
).run(INSTALL_STATUS.UNINSTALLED, now, installId);
|
|
620
|
+
return _rowToInstall(
|
|
621
|
+
db.prepare("SELECT * FROM plugin_eco_installs WHERE id = ?").get(installId),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* ── AI Code Review (heuristic) ──────────────────────────── */
|
|
626
|
+
|
|
627
|
+
export function aiReviewCode(
|
|
628
|
+
db,
|
|
629
|
+
pluginId,
|
|
630
|
+
{ sourceCode = "", strictness = "standard" } = {},
|
|
631
|
+
) {
|
|
632
|
+
const entry = _getEntryRow(db, pluginId);
|
|
633
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
634
|
+
|
|
635
|
+
const code = String(sourceCode || "");
|
|
636
|
+
const sourceHash = code ? _sha256Hex(code) : null;
|
|
637
|
+
|
|
638
|
+
const strictMultiplier =
|
|
639
|
+
strictness === "strict" ? 1.5 : strictness === "lenient" ? 0.6 : 1.0;
|
|
640
|
+
|
|
641
|
+
const issues = [];
|
|
642
|
+
let maxSeverity = REVIEW_SEVERITY.INFO;
|
|
643
|
+
let totalPenalty = 0;
|
|
644
|
+
|
|
645
|
+
for (const rule of REVIEW_RULES) {
|
|
646
|
+
const matches = [...code.matchAll(rule.pattern)];
|
|
647
|
+
if (!matches.length) continue;
|
|
648
|
+
for (const m of matches) {
|
|
649
|
+
const idx = m.index ?? 0;
|
|
650
|
+
const line = code.slice(0, idx).split("\n").length;
|
|
651
|
+
issues.push({
|
|
652
|
+
ruleId: rule.id,
|
|
653
|
+
severity: rule.severity,
|
|
654
|
+
message: rule.message,
|
|
655
|
+
line,
|
|
656
|
+
snippet: (m[0] || "").slice(0, 80),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
totalPenalty += rule.penalty * matches.length * strictMultiplier;
|
|
660
|
+
if (_severityRank(rule.severity) > _severityRank(maxSeverity)) {
|
|
661
|
+
maxSeverity = rule.severity;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const score = Math.max(0, Math.round(100 - totalPenalty));
|
|
666
|
+
|
|
667
|
+
const id = _uid("rev");
|
|
668
|
+
const now = _now();
|
|
669
|
+
db.prepare(
|
|
670
|
+
`INSERT INTO plugin_eco_reviews (id, plugin_id, source_hash, issues, score, severity, strictness, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
671
|
+
).run(
|
|
672
|
+
id,
|
|
673
|
+
pluginId,
|
|
674
|
+
sourceHash,
|
|
675
|
+
JSON.stringify(issues),
|
|
676
|
+
score,
|
|
677
|
+
maxSeverity,
|
|
678
|
+
strictness,
|
|
679
|
+
now,
|
|
680
|
+
);
|
|
681
|
+
return _rowToReview(
|
|
682
|
+
db.prepare("SELECT * FROM plugin_eco_reviews WHERE id = ?").get(id),
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function _severityRank(s) {
|
|
687
|
+
return { info: 0, warning: 1, critical: 2, blocker: 3 }[s] ?? 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function getReview(db, reviewId) {
|
|
691
|
+
return _rowToReview(
|
|
692
|
+
db.prepare("SELECT * FROM plugin_eco_reviews WHERE id = ?").get(reviewId),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function listReviews(db, { pluginId, severity, limit = 100 } = {}) {
|
|
697
|
+
const wheres = [];
|
|
698
|
+
const params = [];
|
|
699
|
+
if (pluginId) {
|
|
700
|
+
wheres.push("plugin_id = ?");
|
|
701
|
+
params.push(pluginId);
|
|
702
|
+
}
|
|
703
|
+
if (severity) {
|
|
704
|
+
wheres.push("severity = ?");
|
|
705
|
+
params.push(severity);
|
|
706
|
+
}
|
|
707
|
+
const where = wheres.length ? `WHERE ${wheres.join(" AND ")}` : "";
|
|
708
|
+
params.push(limit);
|
|
709
|
+
const rows = db
|
|
710
|
+
.prepare(
|
|
711
|
+
`SELECT * FROM plugin_eco_reviews ${where} ORDER BY created_at DESC LIMIT ?`,
|
|
712
|
+
)
|
|
713
|
+
.all(...params);
|
|
714
|
+
return rows.map(_rowToReview);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* ── Sandbox testing ─────────────────────────────────────── */
|
|
718
|
+
|
|
719
|
+
export function recordSandboxTest(
|
|
720
|
+
db,
|
|
721
|
+
pluginId,
|
|
722
|
+
{
|
|
723
|
+
testSuite = "default",
|
|
724
|
+
result = SANDBOX_RESULT.PASSED,
|
|
725
|
+
metrics = {},
|
|
726
|
+
logs = [],
|
|
727
|
+
durationMs = null,
|
|
728
|
+
} = {},
|
|
729
|
+
) {
|
|
730
|
+
const entry = _getEntryRow(db, pluginId);
|
|
731
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
732
|
+
|
|
733
|
+
if (!Object.values(SANDBOX_RESULT).includes(result)) {
|
|
734
|
+
throw new Error(`Unknown sandbox result: ${result}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const id = _uid("sbx");
|
|
738
|
+
const now = _now();
|
|
739
|
+
db.prepare(
|
|
740
|
+
`INSERT INTO plugin_eco_sandbox_tests (id, plugin_id, test_suite, result, metrics, logs, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
741
|
+
).run(
|
|
742
|
+
id,
|
|
743
|
+
pluginId,
|
|
744
|
+
testSuite,
|
|
745
|
+
result,
|
|
746
|
+
JSON.stringify(metrics || {}),
|
|
747
|
+
JSON.stringify(logs || []),
|
|
748
|
+
durationMs,
|
|
749
|
+
now,
|
|
750
|
+
);
|
|
751
|
+
return _rowToSandbox(
|
|
752
|
+
db.prepare("SELECT * FROM plugin_eco_sandbox_tests WHERE id = ?").get(id),
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function getSandboxTest(db, testId) {
|
|
757
|
+
return _rowToSandbox(
|
|
758
|
+
db
|
|
759
|
+
.prepare("SELECT * FROM plugin_eco_sandbox_tests WHERE id = ?")
|
|
760
|
+
.get(testId),
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export function listSandboxTests(db, { pluginId, result, limit = 100 } = {}) {
|
|
765
|
+
const wheres = [];
|
|
766
|
+
const params = [];
|
|
767
|
+
if (pluginId) {
|
|
768
|
+
wheres.push("plugin_id = ?");
|
|
769
|
+
params.push(pluginId);
|
|
770
|
+
}
|
|
771
|
+
if (result) {
|
|
772
|
+
wheres.push("result = ?");
|
|
773
|
+
params.push(result);
|
|
774
|
+
}
|
|
775
|
+
const where = wheres.length ? `WHERE ${wheres.join(" AND ")}` : "";
|
|
776
|
+
params.push(limit);
|
|
777
|
+
const rows = db
|
|
778
|
+
.prepare(
|
|
779
|
+
`SELECT * FROM plugin_eco_sandbox_tests ${where} ORDER BY created_at DESC LIMIT ?`,
|
|
780
|
+
)
|
|
781
|
+
.all(...params);
|
|
782
|
+
return rows.map(_rowToSandbox);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* ── Publish flow ────────────────────────────────────────── */
|
|
786
|
+
|
|
787
|
+
export function submitForReview(db, pluginId) {
|
|
788
|
+
const entry = _getEntryRow(db, pluginId);
|
|
789
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
790
|
+
if (
|
|
791
|
+
entry.status !== PUBLISH_STATUS.DRAFT &&
|
|
792
|
+
entry.status !== PUBLISH_STATUS.REJECTED
|
|
793
|
+
) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`Cannot submit for review from status ${entry.status} (must be draft or rejected)`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const now = _now();
|
|
799
|
+
db.prepare(
|
|
800
|
+
"UPDATE plugin_eco_entries SET status = ?, updated_at = ? WHERE id = ?",
|
|
801
|
+
).run(PUBLISH_STATUS.REVIEWING, now, pluginId);
|
|
802
|
+
return getPlugin(db, pluginId);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function approvePlugin(db, pluginId) {
|
|
806
|
+
const entry = _getEntryRow(db, pluginId);
|
|
807
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
808
|
+
if (entry.status !== PUBLISH_STATUS.REVIEWING) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`Cannot approve plugin in status ${entry.status} (must be reviewing)`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const now = _now();
|
|
814
|
+
db.prepare(
|
|
815
|
+
"UPDATE plugin_eco_entries SET status = ?, updated_at = ? WHERE id = ?",
|
|
816
|
+
).run(PUBLISH_STATUS.APPROVED, now, pluginId);
|
|
817
|
+
return getPlugin(db, pluginId);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export function rejectPlugin(db, pluginId, reason = "review rejected") {
|
|
821
|
+
const entry = _getEntryRow(db, pluginId);
|
|
822
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
823
|
+
if (entry.status !== PUBLISH_STATUS.REVIEWING) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Cannot reject plugin in status ${entry.status} (must be reviewing)`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const now = _now();
|
|
829
|
+
db.prepare(
|
|
830
|
+
"UPDATE plugin_eco_entries SET status = ?, updated_at = ? WHERE id = ?",
|
|
831
|
+
).run(PUBLISH_STATUS.REJECTED, now, pluginId);
|
|
832
|
+
// Store reason as a "rejection" review record so it shows up in listReviews
|
|
833
|
+
db.prepare(
|
|
834
|
+
`INSERT INTO plugin_eco_reviews (id, plugin_id, source_hash, issues, score, severity, strictness, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
835
|
+
).run(
|
|
836
|
+
_uid("rev"),
|
|
837
|
+
pluginId,
|
|
838
|
+
null,
|
|
839
|
+
JSON.stringify([
|
|
840
|
+
{
|
|
841
|
+
ruleId: "review-rejected",
|
|
842
|
+
severity: REVIEW_SEVERITY.BLOCKER,
|
|
843
|
+
message: reason,
|
|
844
|
+
},
|
|
845
|
+
]),
|
|
846
|
+
0,
|
|
847
|
+
REVIEW_SEVERITY.BLOCKER,
|
|
848
|
+
"rejection",
|
|
849
|
+
now,
|
|
850
|
+
);
|
|
851
|
+
return getPlugin(db, pluginId);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export function publishPlugin(
|
|
855
|
+
db,
|
|
856
|
+
pluginId,
|
|
857
|
+
{ sourceCode = "", changelog = "" } = {},
|
|
858
|
+
) {
|
|
859
|
+
const entry = _getEntryRow(db, pluginId);
|
|
860
|
+
if (!entry) throw new Error(`Plugin not found: ${pluginId}`);
|
|
861
|
+
if (entry.status !== PUBLISH_STATUS.APPROVED) {
|
|
862
|
+
throw new Error(
|
|
863
|
+
`Cannot publish plugin in status ${entry.status} (must be approved)`,
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
const now = _now();
|
|
867
|
+
const sourceHash = sourceCode ? _sha256Hex(sourceCode) : null;
|
|
868
|
+
const manifest = _parseJSON(entry.manifest, {});
|
|
869
|
+
if (changelog) manifest.lastChangelog = changelog;
|
|
870
|
+
db.prepare(
|
|
871
|
+
`UPDATE plugin_eco_entries SET status = ?, source_hash = ?, manifest = ?, published_at = ?, updated_at = ? WHERE id = ?`,
|
|
872
|
+
).run(
|
|
873
|
+
PUBLISH_STATUS.PUBLISHED,
|
|
874
|
+
sourceHash,
|
|
875
|
+
JSON.stringify(manifest),
|
|
876
|
+
now,
|
|
877
|
+
now,
|
|
878
|
+
pluginId,
|
|
879
|
+
);
|
|
880
|
+
return getPlugin(db, pluginId);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/* ── Revenue ─────────────────────────────────────────────── */
|
|
884
|
+
|
|
885
|
+
export function recordRevenue(
|
|
886
|
+
db,
|
|
887
|
+
{
|
|
888
|
+
developerId,
|
|
889
|
+
pluginId,
|
|
890
|
+
userId = null,
|
|
891
|
+
type,
|
|
892
|
+
amount,
|
|
893
|
+
developerShareRatio = DEFAULT_DEVELOPER_SHARE,
|
|
894
|
+
} = {},
|
|
895
|
+
) {
|
|
896
|
+
if (!developerId) throw new Error("developerId is required");
|
|
897
|
+
if (!pluginId) throw new Error("pluginId is required");
|
|
898
|
+
if (!Object.values(REVENUE_TYPE).includes(type)) {
|
|
899
|
+
throw new Error(`Unknown revenue type: ${type}`);
|
|
900
|
+
}
|
|
901
|
+
if (typeof amount !== "number" || amount < 0) {
|
|
902
|
+
throw new Error("amount must be a non-negative number");
|
|
903
|
+
}
|
|
904
|
+
if (developerShareRatio < 0 || developerShareRatio > 1) {
|
|
905
|
+
throw new Error("developerShareRatio must be in [0, 1]");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const developerShare = amount * developerShareRatio;
|
|
909
|
+
const platformShare = amount - developerShare;
|
|
910
|
+
const id = _uid("rev");
|
|
911
|
+
const now = _now();
|
|
912
|
+
db.prepare(
|
|
913
|
+
`INSERT INTO plugin_eco_revenue (id, developer_id, plugin_id, user_id, type, amount, developer_share, platform_share, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
914
|
+
).run(
|
|
915
|
+
id,
|
|
916
|
+
developerId,
|
|
917
|
+
pluginId,
|
|
918
|
+
userId,
|
|
919
|
+
type,
|
|
920
|
+
amount,
|
|
921
|
+
developerShare,
|
|
922
|
+
platformShare,
|
|
923
|
+
now,
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
const currentEntry = _getEntryRow(db, pluginId);
|
|
927
|
+
const newRevenueTotal = (currentEntry?.revenue_total || 0) + amount;
|
|
928
|
+
db.prepare(
|
|
929
|
+
"UPDATE plugin_eco_entries SET revenue_total = ?, updated_at = ? WHERE id = ?",
|
|
930
|
+
).run(newRevenueTotal, now, pluginId);
|
|
931
|
+
|
|
932
|
+
return _rowToRevenue(
|
|
933
|
+
db.prepare("SELECT * FROM plugin_eco_revenue WHERE id = ?").get(id),
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export function getDeveloperRevenue(
|
|
938
|
+
db,
|
|
939
|
+
developerId,
|
|
940
|
+
{ from = null, to = null, pluginId = null } = {},
|
|
941
|
+
) {
|
|
942
|
+
const wheres = ["developer_id = ?"];
|
|
943
|
+
const params = [developerId];
|
|
944
|
+
if (from != null) {
|
|
945
|
+
wheres.push("created_at >= ?");
|
|
946
|
+
params.push(from);
|
|
947
|
+
}
|
|
948
|
+
if (to != null) {
|
|
949
|
+
wheres.push("created_at <= ?");
|
|
950
|
+
params.push(to);
|
|
951
|
+
}
|
|
952
|
+
if (pluginId) {
|
|
953
|
+
wheres.push("plugin_id = ?");
|
|
954
|
+
params.push(pluginId);
|
|
955
|
+
}
|
|
956
|
+
const rows = db
|
|
957
|
+
.prepare(
|
|
958
|
+
`SELECT * FROM plugin_eco_revenue WHERE ${wheres.join(" AND ")} ORDER BY created_at DESC`,
|
|
959
|
+
)
|
|
960
|
+
.all(...params);
|
|
961
|
+
|
|
962
|
+
const events = rows.map(_rowToRevenue);
|
|
963
|
+
const totalGross = events.reduce((s, e) => s + e.amount, 0);
|
|
964
|
+
const totalDeveloper = events.reduce((s, e) => s + e.developerShare, 0);
|
|
965
|
+
const totalPlatform = events.reduce((s, e) => s + e.platformShare, 0);
|
|
966
|
+
const byType = {};
|
|
967
|
+
const byPlugin = {};
|
|
968
|
+
for (const e of events) {
|
|
969
|
+
byType[e.type] = (byType[e.type] || 0) + e.amount;
|
|
970
|
+
byPlugin[e.pluginId] = (byPlugin[e.pluginId] || 0) + e.amount;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
developerId,
|
|
975
|
+
totalGross,
|
|
976
|
+
totalDeveloperShare: totalDeveloper,
|
|
977
|
+
totalPlatformShare: totalPlatform,
|
|
978
|
+
eventCount: events.length,
|
|
979
|
+
byType,
|
|
980
|
+
byPlugin,
|
|
981
|
+
events,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* ── Recommendation (heuristic) ──────────────────────────── */
|
|
986
|
+
|
|
987
|
+
export function recommend(
|
|
988
|
+
db,
|
|
989
|
+
{ userId, category = null, limit = DEFAULT_MAX_RECOMMENDATIONS } = {},
|
|
990
|
+
) {
|
|
991
|
+
if (!userId) throw new Error("userId is required");
|
|
992
|
+
|
|
993
|
+
const installed = listInstalls(db, { userId }).filter(
|
|
994
|
+
(i) => i.status === INSTALL_STATUS.INSTALLED,
|
|
995
|
+
);
|
|
996
|
+
const installedIds = new Set(installed.map((i) => i.pluginId));
|
|
997
|
+
|
|
998
|
+
// Discover user's favored categories
|
|
999
|
+
const categoryScores = {};
|
|
1000
|
+
for (const inst of installed) {
|
|
1001
|
+
const entry = getPlugin(db, inst.pluginId);
|
|
1002
|
+
if (entry && entry.category) {
|
|
1003
|
+
categoryScores[entry.category] =
|
|
1004
|
+
(categoryScores[entry.category] || 0) + 1;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const candidates = listPlugins(db, {
|
|
1009
|
+
category: category || undefined,
|
|
1010
|
+
status: PUBLISH_STATUS.PUBLISHED,
|
|
1011
|
+
limit: 500,
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const scored = [];
|
|
1015
|
+
for (const plugin of candidates) {
|
|
1016
|
+
if (installedIds.has(plugin.id)) continue;
|
|
1017
|
+
const categoryAffinity = plugin.category
|
|
1018
|
+
? categoryScores[plugin.category] || 0
|
|
1019
|
+
: 0;
|
|
1020
|
+
const downloadScore = Math.log10((plugin.downloadCount || 0) + 1);
|
|
1021
|
+
const ratingScore = (plugin.avgRating || 0) * 2;
|
|
1022
|
+
const score = categoryAffinity * 10 + downloadScore * 3 + ratingScore + 1;
|
|
1023
|
+
scored.push({
|
|
1024
|
+
plugin,
|
|
1025
|
+
score,
|
|
1026
|
+
reasons: {
|
|
1027
|
+
categoryAffinity,
|
|
1028
|
+
downloadScore,
|
|
1029
|
+
ratingScore,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1035
|
+
return {
|
|
1036
|
+
userId,
|
|
1037
|
+
context: category,
|
|
1038
|
+
userCategories: Object.entries(categoryScores)
|
|
1039
|
+
.sort((a, b) => b[1] - a[1])
|
|
1040
|
+
.map(([cat, count]) => ({ category: cat, weight: count })),
|
|
1041
|
+
recommendations: scored.slice(0, limit),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/* ── Stats / Config ──────────────────────────────────────── */
|
|
1046
|
+
|
|
1047
|
+
export function getConfig() {
|
|
1048
|
+
return {
|
|
1049
|
+
reviewSeverities: Object.values(REVIEW_SEVERITY),
|
|
1050
|
+
publishStatuses: Object.values(PUBLISH_STATUS),
|
|
1051
|
+
revenueTypes: Object.values(REVENUE_TYPE),
|
|
1052
|
+
installStatuses: Object.values(INSTALL_STATUS),
|
|
1053
|
+
depKinds: Object.values(DEP_KIND),
|
|
1054
|
+
sandboxResults: Object.values(SANDBOX_RESULT),
|
|
1055
|
+
defaults: {
|
|
1056
|
+
developerShare: DEFAULT_DEVELOPER_SHARE,
|
|
1057
|
+
sandboxMemoryMb: DEFAULT_SANDBOX_MEMORY_MB,
|
|
1058
|
+
sandboxTimeoutMs: DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
1059
|
+
maxRecommendations: DEFAULT_MAX_RECOMMENDATIONS,
|
|
1060
|
+
},
|
|
1061
|
+
reviewRules: REVIEW_RULES.map((r) => ({
|
|
1062
|
+
id: r.id,
|
|
1063
|
+
severity: r.severity,
|
|
1064
|
+
message: r.message,
|
|
1065
|
+
penalty: r.penalty,
|
|
1066
|
+
})),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
export function getStats(db) {
|
|
1071
|
+
const plugins = db.prepare("SELECT * FROM plugin_eco_entries").all();
|
|
1072
|
+
const installs = db.prepare("SELECT * FROM plugin_eco_installs").all();
|
|
1073
|
+
const reviews = db.prepare("SELECT * FROM plugin_eco_reviews").all();
|
|
1074
|
+
const sandboxTests = db
|
|
1075
|
+
.prepare("SELECT * FROM plugin_eco_sandbox_tests")
|
|
1076
|
+
.all();
|
|
1077
|
+
const revenue = db.prepare("SELECT * FROM plugin_eco_revenue").all();
|
|
1078
|
+
|
|
1079
|
+
const byStatus = {};
|
|
1080
|
+
for (const p of plugins) byStatus[p.status] = (byStatus[p.status] || 0) + 1;
|
|
1081
|
+
|
|
1082
|
+
const byCategory = {};
|
|
1083
|
+
for (const p of plugins) {
|
|
1084
|
+
if (p.category) byCategory[p.category] = (byCategory[p.category] || 0) + 1;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const bySeverity = {};
|
|
1088
|
+
for (const r of reviews)
|
|
1089
|
+
bySeverity[r.severity] = (bySeverity[r.severity] || 0) + 1;
|
|
1090
|
+
|
|
1091
|
+
const totalRevenue = revenue.reduce((s, r) => s + (r.amount || 0), 0);
|
|
1092
|
+
const totalDeveloperShare = revenue.reduce(
|
|
1093
|
+
(s, r) => s + (r.developer_share || 0),
|
|
1094
|
+
0,
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
return {
|
|
1098
|
+
totalPlugins: plugins.length,
|
|
1099
|
+
pluginsByStatus: byStatus,
|
|
1100
|
+
pluginsByCategory: byCategory,
|
|
1101
|
+
totalInstalls: installs.length,
|
|
1102
|
+
totalReviews: reviews.length,
|
|
1103
|
+
reviewsBySeverity: bySeverity,
|
|
1104
|
+
totalSandboxTests: sandboxTests.length,
|
|
1105
|
+
totalRevenueEvents: revenue.length,
|
|
1106
|
+
totalRevenueGross: totalRevenue,
|
|
1107
|
+
totalDeveloperShare,
|
|
1108
|
+
};
|
|
1109
|
+
}
|