chainlesschain 0.47.9 → 0.51.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/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/mcp.js +97 -18
- package/src/commands/multimodal.js +404 -0
- package/src/commands/nlprog.js +329 -0
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +118 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/multimodal.js +725 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPFS Storage — CLI port of Phase 17
|
|
3
|
+
* (docs/design/modules/17_IPFS去中心化存储.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop uses IPFSManager with dual-engine mode
|
|
6
|
+
* (embedded Helia + external Kubo RPC) and 18 IPC handlers.
|
|
7
|
+
*
|
|
8
|
+
* CLI port ships:
|
|
9
|
+
*
|
|
10
|
+
* - Content-addressed store backed by SQLite (CID = sha256(content))
|
|
11
|
+
* - AES-256-GCM encryption (same scheme as desktop: iv + tag + ct)
|
|
12
|
+
* - Pin / unpin / list-pins lifecycle
|
|
13
|
+
* - Storage stats + quota enforcement + garbage collection
|
|
14
|
+
* - Knowledge attachment linkage (knowledge_id → CIDs)
|
|
15
|
+
* - Mode toggle (embedded/external) stored as metadata only
|
|
16
|
+
*
|
|
17
|
+
* What does NOT port: real Helia node, libp2p peer discovery,
|
|
18
|
+
* Kubo HTTP RPC, real DAG sharding, bitswap protocol.
|
|
19
|
+
* CIDs are deterministic sha256 hashes (not real CIDv1),
|
|
20
|
+
* so content is local-only.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import crypto from "crypto";
|
|
24
|
+
|
|
25
|
+
/* ── Constants ──────────────────────────────────────────── */
|
|
26
|
+
|
|
27
|
+
export const NODE_MODE = Object.freeze({
|
|
28
|
+
EMBEDDED: "embedded",
|
|
29
|
+
EXTERNAL: "external",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const NODE_STATUS = Object.freeze({
|
|
33
|
+
STOPPED: "stopped",
|
|
34
|
+
STARTING: "starting",
|
|
35
|
+
RUNNING: "running",
|
|
36
|
+
ERROR: "error",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_QUOTA_BYTES = 1024 * 1024 * 1024; // 1 GB
|
|
40
|
+
|
|
41
|
+
/* ── State ──────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
let _content = new Map(); // cid → row
|
|
44
|
+
let _knowledgeLinks = new Map(); // knowledge_id → Set(cid)
|
|
45
|
+
let _node = {
|
|
46
|
+
status: NODE_STATUS.STOPPED,
|
|
47
|
+
mode: NODE_MODE.EMBEDDED,
|
|
48
|
+
startedAt: null,
|
|
49
|
+
peerId: null,
|
|
50
|
+
};
|
|
51
|
+
let _quotaBytes = DEFAULT_QUOTA_BYTES;
|
|
52
|
+
|
|
53
|
+
/* ── Helpers ────────────────────────────────────────────── */
|
|
54
|
+
|
|
55
|
+
function _now() {
|
|
56
|
+
return Date.now();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _strip(row) {
|
|
60
|
+
if (!row) return null;
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(row)) {
|
|
63
|
+
if (k !== "_rowid_" && k !== "rowid") out[k] = v;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _computeCid(buffer) {
|
|
69
|
+
// Deterministic content addressing using sha256.
|
|
70
|
+
// Real IPFS uses CIDv1 with multihash, but for CLI port we
|
|
71
|
+
// use a base58-like hex prefix to keep CIDs identifiable.
|
|
72
|
+
const hash = crypto.createHash("sha256").update(buffer).digest();
|
|
73
|
+
return `bafy${hash.toString("hex").slice(0, 48)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _toBuffer(content) {
|
|
77
|
+
if (Buffer.isBuffer(content)) return content;
|
|
78
|
+
if (typeof content === "string") return Buffer.from(content, "utf-8");
|
|
79
|
+
if (content instanceof Uint8Array) return Buffer.from(content);
|
|
80
|
+
return Buffer.from(JSON.stringify(content), "utf-8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ── Schema ─────────────────────────────────────────────── */
|
|
84
|
+
|
|
85
|
+
export function ensureIpfsTables(db) {
|
|
86
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ipfs_content (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
cid TEXT NOT NULL,
|
|
89
|
+
filename TEXT,
|
|
90
|
+
size INTEGER NOT NULL,
|
|
91
|
+
mime_type TEXT,
|
|
92
|
+
pinned INTEGER DEFAULT 0,
|
|
93
|
+
encrypted INTEGER DEFAULT 0,
|
|
94
|
+
encryption_key TEXT,
|
|
95
|
+
knowledge_id TEXT,
|
|
96
|
+
metadata TEXT,
|
|
97
|
+
payload TEXT,
|
|
98
|
+
created_at INTEGER NOT NULL,
|
|
99
|
+
updated_at INTEGER NOT NULL
|
|
100
|
+
)`);
|
|
101
|
+
|
|
102
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ipfs_node_config (
|
|
103
|
+
config_key TEXT PRIMARY KEY,
|
|
104
|
+
config_value TEXT,
|
|
105
|
+
updated_at INTEGER NOT NULL
|
|
106
|
+
)`);
|
|
107
|
+
|
|
108
|
+
_loadAll(db);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _loadAll(db) {
|
|
112
|
+
_content.clear();
|
|
113
|
+
_knowledgeLinks.clear();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
for (const row of db.prepare("SELECT * FROM ipfs_content").all()) {
|
|
117
|
+
const r = _strip(row);
|
|
118
|
+
_content.set(r.cid, r);
|
|
119
|
+
if (r.knowledge_id) {
|
|
120
|
+
if (!_knowledgeLinks.has(r.knowledge_id))
|
|
121
|
+
_knowledgeLinks.set(r.knowledge_id, new Set());
|
|
122
|
+
_knowledgeLinks.get(r.knowledge_id).add(r.cid);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (_e) {
|
|
126
|
+
/* table may not exist */
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
for (const row of db.prepare("SELECT * FROM ipfs_node_config").all()) {
|
|
131
|
+
const r = _strip(row);
|
|
132
|
+
if (r.config_key === "quota")
|
|
133
|
+
_quotaBytes = Number(r.config_value) || DEFAULT_QUOTA_BYTES;
|
|
134
|
+
else if (r.config_key === "mode") _node.mode = r.config_value;
|
|
135
|
+
}
|
|
136
|
+
} catch (_e) {
|
|
137
|
+
/* empty */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ── Encryption ─────────────────────────────────────────── */
|
|
142
|
+
|
|
143
|
+
function _encrypt(data) {
|
|
144
|
+
const key = crypto.randomBytes(32);
|
|
145
|
+
const iv = crypto.randomBytes(16);
|
|
146
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
147
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
148
|
+
const tag = cipher.getAuthTag();
|
|
149
|
+
return {
|
|
150
|
+
encrypted: Buffer.concat([iv, tag, encrypted]),
|
|
151
|
+
key: key.toString("hex"),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _decrypt(data, keyHex) {
|
|
156
|
+
const key = Buffer.from(keyHex, "hex");
|
|
157
|
+
const iv = data.subarray(0, 16);
|
|
158
|
+
const tag = data.subarray(16, 32);
|
|
159
|
+
const ciphertext = data.subarray(32);
|
|
160
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
161
|
+
decipher.setAuthTag(tag);
|
|
162
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ── Node lifecycle ─────────────────────────────────────── */
|
|
166
|
+
|
|
167
|
+
export function startNode(db, { mode } = {}) {
|
|
168
|
+
if (_node.status === NODE_STATUS.RUNNING)
|
|
169
|
+
return { started: false, reason: "already_running" };
|
|
170
|
+
|
|
171
|
+
const resolvedMode = mode || _node.mode || NODE_MODE.EMBEDDED;
|
|
172
|
+
if (!Object.values(NODE_MODE).includes(resolvedMode))
|
|
173
|
+
return { started: false, reason: "invalid_mode" };
|
|
174
|
+
|
|
175
|
+
_node.status = NODE_STATUS.RUNNING;
|
|
176
|
+
_node.mode = resolvedMode;
|
|
177
|
+
_node.startedAt = _now();
|
|
178
|
+
_node.peerId = `sim-${crypto.randomUUID()}`;
|
|
179
|
+
|
|
180
|
+
_persistConfig(db, "mode", resolvedMode);
|
|
181
|
+
return {
|
|
182
|
+
started: true,
|
|
183
|
+
status: _node.status,
|
|
184
|
+
mode: _node.mode,
|
|
185
|
+
peerId: _node.peerId,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function stopNode(db) {
|
|
190
|
+
if (_node.status !== NODE_STATUS.RUNNING)
|
|
191
|
+
return { stopped: false, reason: "not_running" };
|
|
192
|
+
_node.status = NODE_STATUS.STOPPED;
|
|
193
|
+
_node.startedAt = null;
|
|
194
|
+
_node.peerId = null;
|
|
195
|
+
return { stopped: true };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function getNodeStatus() {
|
|
199
|
+
return {
|
|
200
|
+
status: _node.status,
|
|
201
|
+
mode: _node.mode,
|
|
202
|
+
startedAt: _node.startedAt,
|
|
203
|
+
peerId: _node.peerId,
|
|
204
|
+
uptimeMs: _node.startedAt ? _now() - _node.startedAt : 0,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function setMode(db, mode) {
|
|
209
|
+
if (!Object.values(NODE_MODE).includes(mode))
|
|
210
|
+
return { set: false, reason: "invalid_mode" };
|
|
211
|
+
if (_node.status === NODE_STATUS.RUNNING)
|
|
212
|
+
return { set: false, reason: "stop_node_first" };
|
|
213
|
+
_node.mode = mode;
|
|
214
|
+
_persistConfig(db, "mode", mode);
|
|
215
|
+
return { set: true, mode };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ── Config persistence ─────────────────────────────────── */
|
|
219
|
+
|
|
220
|
+
function _persistConfig(db, key, value) {
|
|
221
|
+
const now = _now();
|
|
222
|
+
const valueStr = String(value);
|
|
223
|
+
const existing = db
|
|
224
|
+
.prepare("SELECT config_key FROM ipfs_node_config WHERE config_key = ?")
|
|
225
|
+
.get(key);
|
|
226
|
+
if (existing) {
|
|
227
|
+
db.prepare(
|
|
228
|
+
"UPDATE ipfs_node_config SET config_value = ?, updated_at = ? WHERE config_key = ?",
|
|
229
|
+
).run(valueStr, now, key);
|
|
230
|
+
} else {
|
|
231
|
+
db.prepare(
|
|
232
|
+
"INSERT INTO ipfs_node_config (config_key, config_value, updated_at) VALUES (?, ?, ?)",
|
|
233
|
+
).run(key, valueStr, now);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ── Content operations ─────────────────────────────────── */
|
|
238
|
+
|
|
239
|
+
export function addContent(
|
|
240
|
+
db,
|
|
241
|
+
content,
|
|
242
|
+
{ filename, mimeType, encrypt, pin, knowledgeId, metadata } = {},
|
|
243
|
+
) {
|
|
244
|
+
if (_node.status !== NODE_STATUS.RUNNING)
|
|
245
|
+
return { added: false, reason: "node_not_running" };
|
|
246
|
+
if (content == null || content === "")
|
|
247
|
+
return { added: false, reason: "empty_content" };
|
|
248
|
+
|
|
249
|
+
const buffer = _toBuffer(content);
|
|
250
|
+
const size = buffer.length;
|
|
251
|
+
|
|
252
|
+
// Quota check (sum pinned only — desktop semantics)
|
|
253
|
+
const pinnedSize = _pinnedBytes();
|
|
254
|
+
if (pin && pinnedSize + size > _quotaBytes)
|
|
255
|
+
return { added: false, reason: "quota_exceeded" };
|
|
256
|
+
|
|
257
|
+
let payload = buffer;
|
|
258
|
+
let encryptionKey = null;
|
|
259
|
+
if (encrypt) {
|
|
260
|
+
const enc = _encrypt(buffer);
|
|
261
|
+
payload = enc.encrypted;
|
|
262
|
+
encryptionKey = enc.key;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const cid = _computeCid(payload);
|
|
266
|
+
|
|
267
|
+
// Idempotent: if CID exists, just update flags
|
|
268
|
+
const existing = _content.get(cid);
|
|
269
|
+
if (existing) {
|
|
270
|
+
const now = _now();
|
|
271
|
+
if (pin && !existing.pinned) {
|
|
272
|
+
existing.pinned = 1;
|
|
273
|
+
existing.updated_at = now;
|
|
274
|
+
db.prepare(
|
|
275
|
+
"UPDATE ipfs_content SET pinned = 1, updated_at = ? WHERE cid = ?",
|
|
276
|
+
).run(now, cid);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
added: true,
|
|
280
|
+
cid,
|
|
281
|
+
size: existing.size,
|
|
282
|
+
pinned: Boolean(existing.pinned),
|
|
283
|
+
duplicate: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const id = crypto.randomUUID();
|
|
288
|
+
const now = _now();
|
|
289
|
+
const metadataJson = metadata
|
|
290
|
+
? typeof metadata === "string"
|
|
291
|
+
? metadata
|
|
292
|
+
: JSON.stringify(metadata)
|
|
293
|
+
: null;
|
|
294
|
+
const payloadBase64 = payload.toString("base64");
|
|
295
|
+
|
|
296
|
+
const entry = {
|
|
297
|
+
id,
|
|
298
|
+
cid,
|
|
299
|
+
filename: filename || null,
|
|
300
|
+
size,
|
|
301
|
+
mime_type: mimeType || null,
|
|
302
|
+
pinned: pin ? 1 : 0,
|
|
303
|
+
encrypted: encrypt ? 1 : 0,
|
|
304
|
+
encryption_key: encryptionKey,
|
|
305
|
+
knowledge_id: knowledgeId || null,
|
|
306
|
+
metadata: metadataJson,
|
|
307
|
+
payload: payloadBase64,
|
|
308
|
+
created_at: now,
|
|
309
|
+
updated_at: now,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
db.prepare(
|
|
313
|
+
`INSERT INTO ipfs_content (id, cid, filename, size, mime_type, pinned, encrypted, encryption_key, knowledge_id, metadata, payload, created_at, updated_at)
|
|
314
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
315
|
+
).run(
|
|
316
|
+
id,
|
|
317
|
+
cid,
|
|
318
|
+
entry.filename,
|
|
319
|
+
size,
|
|
320
|
+
entry.mime_type,
|
|
321
|
+
entry.pinned,
|
|
322
|
+
entry.encrypted,
|
|
323
|
+
entry.encryption_key,
|
|
324
|
+
entry.knowledge_id,
|
|
325
|
+
entry.metadata,
|
|
326
|
+
entry.payload,
|
|
327
|
+
now,
|
|
328
|
+
now,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
_content.set(cid, entry);
|
|
332
|
+
if (knowledgeId) {
|
|
333
|
+
if (!_knowledgeLinks.has(knowledgeId))
|
|
334
|
+
_knowledgeLinks.set(knowledgeId, new Set());
|
|
335
|
+
_knowledgeLinks.get(knowledgeId).add(cid);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
added: true,
|
|
340
|
+
cid,
|
|
341
|
+
size,
|
|
342
|
+
pinned: Boolean(entry.pinned),
|
|
343
|
+
encrypted: Boolean(entry.encrypted),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function getContent(db, cid, { asString } = {}) {
|
|
348
|
+
if (_node.status !== NODE_STATUS.RUNNING) return null;
|
|
349
|
+
const entry = _content.get(cid);
|
|
350
|
+
if (!entry) return null;
|
|
351
|
+
|
|
352
|
+
let buffer = Buffer.from(entry.payload, "base64");
|
|
353
|
+
if (entry.encrypted && entry.encryption_key) {
|
|
354
|
+
try {
|
|
355
|
+
buffer = _decrypt(buffer, entry.encryption_key);
|
|
356
|
+
} catch (_e) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
cid: entry.cid,
|
|
363
|
+
size: entry.size,
|
|
364
|
+
filename: entry.filename,
|
|
365
|
+
mimeType: entry.mime_type,
|
|
366
|
+
encrypted: Boolean(entry.encrypted),
|
|
367
|
+
pinned: Boolean(entry.pinned),
|
|
368
|
+
content: asString ? buffer.toString("utf-8") : buffer,
|
|
369
|
+
base64: buffer.toString("base64"),
|
|
370
|
+
metadata: entry.metadata ? _parseMaybe(entry.metadata) : null,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function _parseMaybe(raw) {
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse(raw);
|
|
377
|
+
} catch (_e) {
|
|
378
|
+
return raw;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function hasContent(db, cid) {
|
|
383
|
+
return _content.has(cid);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function listContent(db, { pinned, knowledgeId, limit = 50 } = {}) {
|
|
387
|
+
let results = [..._content.values()];
|
|
388
|
+
if (pinned != null) {
|
|
389
|
+
const v = pinned ? 1 : 0;
|
|
390
|
+
results = results.filter((c) => c.pinned === v);
|
|
391
|
+
}
|
|
392
|
+
if (knowledgeId)
|
|
393
|
+
results = results.filter((c) => c.knowledge_id === knowledgeId);
|
|
394
|
+
return results
|
|
395
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
396
|
+
.slice(0, limit)
|
|
397
|
+
.map((c) => {
|
|
398
|
+
const { payload, encryption_key, ...rest } = c;
|
|
399
|
+
return rest;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* ── Pin management ─────────────────────────────────────── */
|
|
404
|
+
|
|
405
|
+
export function pin(db, cid) {
|
|
406
|
+
const entry = _content.get(cid);
|
|
407
|
+
if (!entry) return { pinned: false, reason: "not_found" };
|
|
408
|
+
if (entry.pinned) return { pinned: false, reason: "already_pinned" };
|
|
409
|
+
|
|
410
|
+
const pinnedSize = _pinnedBytes();
|
|
411
|
+
if (pinnedSize + entry.size > _quotaBytes)
|
|
412
|
+
return { pinned: false, reason: "quota_exceeded" };
|
|
413
|
+
|
|
414
|
+
const now = _now();
|
|
415
|
+
entry.pinned = 1;
|
|
416
|
+
entry.updated_at = now;
|
|
417
|
+
db.prepare(
|
|
418
|
+
"UPDATE ipfs_content SET pinned = 1, updated_at = ? WHERE cid = ?",
|
|
419
|
+
).run(now, cid);
|
|
420
|
+
return { pinned: true, cid };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function unpin(db, cid) {
|
|
424
|
+
const entry = _content.get(cid);
|
|
425
|
+
if (!entry) return { unpinned: false, reason: "not_found" };
|
|
426
|
+
if (!entry.pinned) return { unpinned: false, reason: "not_pinned" };
|
|
427
|
+
|
|
428
|
+
const now = _now();
|
|
429
|
+
entry.pinned = 0;
|
|
430
|
+
entry.updated_at = now;
|
|
431
|
+
db.prepare(
|
|
432
|
+
"UPDATE ipfs_content SET pinned = 0, updated_at = ? WHERE cid = ?",
|
|
433
|
+
).run(now, cid);
|
|
434
|
+
return { unpinned: true, cid };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function listPins(db, { limit = 50, sortBy = "created_at" } = {}) {
|
|
438
|
+
const pinned = [..._content.values()].filter((c) => c.pinned === 1);
|
|
439
|
+
const sorter =
|
|
440
|
+
sortBy === "size"
|
|
441
|
+
? (a, b) => b.size - a.size
|
|
442
|
+
: sortBy === "filename"
|
|
443
|
+
? (a, b) => (a.filename || "").localeCompare(b.filename || "")
|
|
444
|
+
: (a, b) => b.created_at - a.created_at;
|
|
445
|
+
return pinned
|
|
446
|
+
.sort(sorter)
|
|
447
|
+
.slice(0, limit)
|
|
448
|
+
.map((c) => {
|
|
449
|
+
const { payload, encryption_key, ...rest } = c;
|
|
450
|
+
return rest;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* ── Storage stats + GC + quota ─────────────────────────── */
|
|
455
|
+
|
|
456
|
+
function _pinnedBytes() {
|
|
457
|
+
let total = 0;
|
|
458
|
+
for (const c of _content.values()) if (c.pinned) total += c.size;
|
|
459
|
+
return total;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function _totalBytes() {
|
|
463
|
+
let total = 0;
|
|
464
|
+
for (const c of _content.values()) total += c.size;
|
|
465
|
+
return total;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function getStorageStats() {
|
|
469
|
+
const pinnedBytes = _pinnedBytes();
|
|
470
|
+
const totalBytes = _totalBytes();
|
|
471
|
+
let pinnedCount = 0;
|
|
472
|
+
let encryptedCount = 0;
|
|
473
|
+
for (const c of _content.values()) {
|
|
474
|
+
if (c.pinned) pinnedCount++;
|
|
475
|
+
if (c.encrypted) encryptedCount++;
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
totalContent: _content.size,
|
|
479
|
+
pinnedCount,
|
|
480
|
+
encryptedCount,
|
|
481
|
+
totalBytes,
|
|
482
|
+
pinnedBytes,
|
|
483
|
+
quotaBytes: _quotaBytes,
|
|
484
|
+
usagePercent:
|
|
485
|
+
_quotaBytes > 0
|
|
486
|
+
? Number(((pinnedBytes / _quotaBytes) * 100).toFixed(2))
|
|
487
|
+
: 0,
|
|
488
|
+
peerCount: _node.status === NODE_STATUS.RUNNING ? 0 : 0,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function garbageCollect(db) {
|
|
493
|
+
// Remove all unpinned content (desktop semantics)
|
|
494
|
+
const before = _content.size;
|
|
495
|
+
let freedBytes = 0;
|
|
496
|
+
const toRemove = [];
|
|
497
|
+
for (const [cid, entry] of _content) {
|
|
498
|
+
if (!entry.pinned) {
|
|
499
|
+
toRemove.push(cid);
|
|
500
|
+
freedBytes += entry.size;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (const cid of toRemove) {
|
|
504
|
+
_content.delete(cid);
|
|
505
|
+
db.prepare("DELETE FROM ipfs_content WHERE cid = ?").run(cid);
|
|
506
|
+
}
|
|
507
|
+
// Clean up knowledge links pointing to removed CIDs
|
|
508
|
+
for (const [kid, set] of _knowledgeLinks) {
|
|
509
|
+
for (const cid of [...set]) {
|
|
510
|
+
if (!_content.has(cid)) set.delete(cid);
|
|
511
|
+
}
|
|
512
|
+
if (set.size === 0) _knowledgeLinks.delete(kid);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
removed: toRemove.length,
|
|
516
|
+
freedBytes,
|
|
517
|
+
before,
|
|
518
|
+
after: _content.size,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function setQuota(db, quotaBytes) {
|
|
523
|
+
const n = Number(quotaBytes);
|
|
524
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
525
|
+
return { set: false, reason: "invalid_quota" };
|
|
526
|
+
_quotaBytes = Math.floor(n);
|
|
527
|
+
_persistConfig(db, "quota", _quotaBytes);
|
|
528
|
+
return { set: true, quotaBytes: _quotaBytes };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* ── Knowledge attachment linkage ───────────────────────── */
|
|
532
|
+
|
|
533
|
+
export function addKnowledgeAttachment(
|
|
534
|
+
db,
|
|
535
|
+
knowledgeId,
|
|
536
|
+
content,
|
|
537
|
+
metadata = {},
|
|
538
|
+
) {
|
|
539
|
+
if (!knowledgeId) return { added: false, reason: "missing_knowledge_id" };
|
|
540
|
+
const result = addContent(db, content, {
|
|
541
|
+
knowledgeId,
|
|
542
|
+
metadata,
|
|
543
|
+
pin: true,
|
|
544
|
+
filename: metadata?.filename,
|
|
545
|
+
mimeType: metadata?.mimeType,
|
|
546
|
+
});
|
|
547
|
+
if (!result.added) return result;
|
|
548
|
+
return { ...result, knowledgeId };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function getKnowledgeAttachments(db, knowledgeId) {
|
|
552
|
+
const set = _knowledgeLinks.get(knowledgeId);
|
|
553
|
+
if (!set) return [];
|
|
554
|
+
return [...set]
|
|
555
|
+
.map((cid) => _content.get(cid))
|
|
556
|
+
.filter(Boolean)
|
|
557
|
+
.map((c) => {
|
|
558
|
+
const { payload, encryption_key, ...rest } = c;
|
|
559
|
+
return rest;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ── Reset (tests) ──────────────────────────────────────── */
|
|
564
|
+
|
|
565
|
+
export function _resetState() {
|
|
566
|
+
_content.clear();
|
|
567
|
+
_knowledgeLinks.clear();
|
|
568
|
+
_node = {
|
|
569
|
+
status: NODE_STATUS.STOPPED,
|
|
570
|
+
mode: NODE_MODE.EMBEDDED,
|
|
571
|
+
startedAt: null,
|
|
572
|
+
peerId: null,
|
|
573
|
+
};
|
|
574
|
+
_quotaBytes = DEFAULT_QUOTA_BYTES;
|
|
575
|
+
}
|