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,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multimodal Collaboration — CLI port of Phase 27
|
|
3
|
+
* (docs/design/modules/27_多模态协作.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop exposes 12 IPC handlers for ModalityFusion /
|
|
6
|
+
* DocumentParser / MultimodalContext / MultimodalOutput.
|
|
7
|
+
*
|
|
8
|
+
* CLI port ships:
|
|
9
|
+
*
|
|
10
|
+
* - 5 input modalities (text / document / image / audio / screen)
|
|
11
|
+
* with weighted fusion (same weights as desktop)
|
|
12
|
+
* - Session + artifact persistence in SQLite
|
|
13
|
+
* - Native parse for txt / md / csv / json; report-only for
|
|
14
|
+
* pdf / docx / xlsx (no heavy deps in CLI)
|
|
15
|
+
* - Context builder with 4000-token cap and heuristic token count
|
|
16
|
+
* (~chars/4). No cache TTL (CLI is one-shot).
|
|
17
|
+
* - 6 output formats: markdown / html / json / csv / slides
|
|
18
|
+
* (Reveal.js skeleton) / chart (ECharts option JSON)
|
|
19
|
+
*
|
|
20
|
+
* What does NOT port: OCR / ASR, real PDF/DOCX/XLSX parsing,
|
|
21
|
+
* 5-minute cache, EventEmitter events, IPC channels.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from "fs";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import crypto from "crypto";
|
|
27
|
+
|
|
28
|
+
/* ── Constants ──────────────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
export const MODALITIES = Object.freeze([
|
|
31
|
+
"text",
|
|
32
|
+
"document",
|
|
33
|
+
"image",
|
|
34
|
+
"audio",
|
|
35
|
+
"screen",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export const MODALITY_WEIGHTS = Object.freeze({
|
|
39
|
+
text: 1.0,
|
|
40
|
+
document: 0.9,
|
|
41
|
+
image: 0.8,
|
|
42
|
+
audio: 0.7,
|
|
43
|
+
screen: 0.6,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const INPUT_FORMATS = Object.freeze([
|
|
47
|
+
"pdf",
|
|
48
|
+
"docx",
|
|
49
|
+
"xlsx",
|
|
50
|
+
"txt",
|
|
51
|
+
"md",
|
|
52
|
+
"csv",
|
|
53
|
+
"json",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export const NATIVE_FORMATS = Object.freeze(["txt", "md", "csv", "json"]);
|
|
57
|
+
|
|
58
|
+
export const OUTPUT_FORMATS = Object.freeze([
|
|
59
|
+
"markdown",
|
|
60
|
+
"html",
|
|
61
|
+
"chart",
|
|
62
|
+
"slides",
|
|
63
|
+
"json",
|
|
64
|
+
"csv",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const SESSION_STATUS = Object.freeze({
|
|
68
|
+
ACTIVE: "active",
|
|
69
|
+
COMPLETED: "completed",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const DEFAULT_MAX_TOKENS = 4000;
|
|
73
|
+
|
|
74
|
+
const PRIORITY_ORDER = ["text", "document", "image", "audio", "screen"];
|
|
75
|
+
|
|
76
|
+
/* ── State ──────────────────────────────────────────────── */
|
|
77
|
+
|
|
78
|
+
let _contextCache = new Map(); // sessionId → { tokens, content, items }
|
|
79
|
+
|
|
80
|
+
/* ── Helpers ────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
function _now() {
|
|
83
|
+
return Date.now();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _strip(row) {
|
|
87
|
+
if (!row) return null;
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const [k, v] of Object.entries(row)) {
|
|
90
|
+
if (k !== "_rowid_" && k !== "rowid") out[k] = v;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _parseMaybe(raw) {
|
|
96
|
+
if (raw == null) return null;
|
|
97
|
+
if (typeof raw !== "string") return raw;
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(raw);
|
|
100
|
+
} catch (_e) {
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _estimateTokens(s) {
|
|
106
|
+
if (!s) return 0;
|
|
107
|
+
const str = typeof s === "string" ? s : JSON.stringify(s);
|
|
108
|
+
return Math.ceil(str.length / 4);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── Schema ─────────────────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
export function ensureMultimodalTables(db) {
|
|
114
|
+
db.exec(`CREATE TABLE IF NOT EXISTS multimodal_sessions (
|
|
115
|
+
id TEXT PRIMARY KEY,
|
|
116
|
+
modalities TEXT,
|
|
117
|
+
context TEXT,
|
|
118
|
+
status TEXT DEFAULT 'active',
|
|
119
|
+
token_count INTEGER DEFAULT 0,
|
|
120
|
+
created_at INTEGER NOT NULL,
|
|
121
|
+
updated_at INTEGER NOT NULL
|
|
122
|
+
)`);
|
|
123
|
+
|
|
124
|
+
db.exec(`CREATE TABLE IF NOT EXISTS multimodal_artifacts (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
session_id TEXT NOT NULL,
|
|
127
|
+
type TEXT NOT NULL,
|
|
128
|
+
modality TEXT,
|
|
129
|
+
format TEXT,
|
|
130
|
+
content TEXT,
|
|
131
|
+
metadata TEXT,
|
|
132
|
+
created_at INTEGER NOT NULL
|
|
133
|
+
)`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── Session management ─────────────────────────────────── */
|
|
137
|
+
|
|
138
|
+
export function createSession(db, { title, metadata } = {}) {
|
|
139
|
+
const id = crypto.randomUUID();
|
|
140
|
+
const now = _now();
|
|
141
|
+
const meta = metadata
|
|
142
|
+
? typeof metadata === "string"
|
|
143
|
+
? metadata
|
|
144
|
+
: JSON.stringify(metadata)
|
|
145
|
+
: null;
|
|
146
|
+
db.prepare(
|
|
147
|
+
`INSERT INTO multimodal_sessions (id, modalities, context, status, token_count, created_at, updated_at)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
149
|
+
).run(id, JSON.stringify([]), meta, SESSION_STATUS.ACTIVE, 0, now, now);
|
|
150
|
+
return { sessionId: id, createdAt: now };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getSession(db, sessionId) {
|
|
154
|
+
const row = _strip(
|
|
155
|
+
db.prepare("SELECT * FROM multimodal_sessions WHERE id = ?").get(sessionId),
|
|
156
|
+
);
|
|
157
|
+
if (!row) return null;
|
|
158
|
+
return {
|
|
159
|
+
id: row.id,
|
|
160
|
+
modalities: _parseMaybe(row.modalities) || [],
|
|
161
|
+
context: _parseMaybe(row.context),
|
|
162
|
+
status: row.status,
|
|
163
|
+
tokenCount: row.token_count,
|
|
164
|
+
createdAt: row.created_at,
|
|
165
|
+
updatedAt: row.updated_at,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function listSessions(db, { status, limit = 50 } = {}) {
|
|
170
|
+
let rows = db.prepare("SELECT * FROM multimodal_sessions").all();
|
|
171
|
+
rows = rows.map(_strip);
|
|
172
|
+
if (status) rows = rows.filter((r) => r.status === status);
|
|
173
|
+
return rows
|
|
174
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
175
|
+
.slice(0, limit)
|
|
176
|
+
.map((r) => ({
|
|
177
|
+
id: r.id,
|
|
178
|
+
status: r.status,
|
|
179
|
+
tokenCount: r.token_count,
|
|
180
|
+
modalities: _parseMaybe(r.modalities) || [],
|
|
181
|
+
createdAt: r.created_at,
|
|
182
|
+
updatedAt: r.updated_at,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function completeSession(db, sessionId) {
|
|
187
|
+
const s = getSession(db, sessionId);
|
|
188
|
+
if (!s) return { completed: false, reason: "not_found" };
|
|
189
|
+
db.prepare(
|
|
190
|
+
"UPDATE multimodal_sessions SET status = ?, updated_at = ? WHERE id = ?",
|
|
191
|
+
).run(SESSION_STATUS.COMPLETED, _now(), sessionId);
|
|
192
|
+
return { completed: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function deleteSession(db, sessionId) {
|
|
196
|
+
const s = getSession(db, sessionId);
|
|
197
|
+
if (!s) return { deleted: false, reason: "not_found" };
|
|
198
|
+
db.prepare("DELETE FROM multimodal_artifacts WHERE session_id = ?").run(
|
|
199
|
+
sessionId,
|
|
200
|
+
);
|
|
201
|
+
db.prepare("DELETE FROM multimodal_sessions WHERE id = ?").run(sessionId);
|
|
202
|
+
_contextCache.delete(sessionId);
|
|
203
|
+
return { deleted: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ── Artifact storage ───────────────────────────────────── */
|
|
207
|
+
|
|
208
|
+
function _addArtifact(db, sessionId, kind) {
|
|
209
|
+
const id = crypto.randomUUID();
|
|
210
|
+
const now = _now();
|
|
211
|
+
db.prepare(
|
|
212
|
+
`INSERT INTO multimodal_artifacts (id, session_id, type, modality, format, content, metadata, created_at)
|
|
213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
214
|
+
).run(
|
|
215
|
+
id,
|
|
216
|
+
sessionId,
|
|
217
|
+
kind.type,
|
|
218
|
+
kind.modality || null,
|
|
219
|
+
kind.format || null,
|
|
220
|
+
typeof kind.content === "string"
|
|
221
|
+
? kind.content
|
|
222
|
+
: JSON.stringify(kind.content ?? null),
|
|
223
|
+
kind.metadata ? JSON.stringify(kind.metadata) : null,
|
|
224
|
+
now,
|
|
225
|
+
);
|
|
226
|
+
return { artifactId: id, createdAt: now };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function listArtifacts(
|
|
230
|
+
db,
|
|
231
|
+
sessionId,
|
|
232
|
+
{ type, modality, limit = 100 } = {},
|
|
233
|
+
) {
|
|
234
|
+
let rows = db
|
|
235
|
+
.prepare("SELECT * FROM multimodal_artifacts WHERE session_id = ?")
|
|
236
|
+
.all(sessionId);
|
|
237
|
+
rows = rows.map(_strip);
|
|
238
|
+
if (type) rows = rows.filter((r) => r.type === type);
|
|
239
|
+
if (modality) rows = rows.filter((r) => r.modality === modality);
|
|
240
|
+
return rows
|
|
241
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
242
|
+
.slice(0, limit)
|
|
243
|
+
.map((r) => ({
|
|
244
|
+
id: r.id,
|
|
245
|
+
type: r.type,
|
|
246
|
+
modality: r.modality,
|
|
247
|
+
format: r.format,
|
|
248
|
+
content: _parseMaybe(r.content),
|
|
249
|
+
metadata: _parseMaybe(r.metadata),
|
|
250
|
+
createdAt: r.created_at,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── Modality operations ────────────────────────────────── */
|
|
255
|
+
|
|
256
|
+
export function addModality(db, sessionId, modality, data, { metadata } = {}) {
|
|
257
|
+
if (!MODALITIES.includes(modality))
|
|
258
|
+
return { added: false, reason: "unknown_modality" };
|
|
259
|
+
const s = getSession(db, sessionId);
|
|
260
|
+
if (!s) return { added: false, reason: "session_not_found" };
|
|
261
|
+
|
|
262
|
+
const artifact = _addArtifact(db, sessionId, {
|
|
263
|
+
type: "input",
|
|
264
|
+
modality,
|
|
265
|
+
format: metadata?.format || null,
|
|
266
|
+
content: data,
|
|
267
|
+
metadata,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const modalities = new Set(s.modalities);
|
|
271
|
+
modalities.add(modality);
|
|
272
|
+
db.prepare(
|
|
273
|
+
"UPDATE multimodal_sessions SET modalities = ?, updated_at = ? WHERE id = ?",
|
|
274
|
+
).run(JSON.stringify([...modalities]), _now(), sessionId);
|
|
275
|
+
_contextCache.delete(sessionId);
|
|
276
|
+
|
|
277
|
+
return { added: true, modality, artifactId: artifact.artifactId };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function getSessionModalities(db, sessionId) {
|
|
281
|
+
const s = getSession(db, sessionId);
|
|
282
|
+
if (!s) return [];
|
|
283
|
+
const arts = listArtifacts(db, sessionId, { type: "input" });
|
|
284
|
+
const byModality = {};
|
|
285
|
+
for (const m of s.modalities) byModality[m] = [];
|
|
286
|
+
for (const a of arts) {
|
|
287
|
+
if (!byModality[a.modality]) byModality[a.modality] = [];
|
|
288
|
+
byModality[a.modality].push({
|
|
289
|
+
artifactId: a.id,
|
|
290
|
+
content: a.content,
|
|
291
|
+
format: a.format,
|
|
292
|
+
metadata: a.metadata,
|
|
293
|
+
createdAt: a.createdAt,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return byModality;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function fuse(db, sessionId) {
|
|
300
|
+
const s = getSession(db, sessionId);
|
|
301
|
+
if (!s) return { fused: false, reason: "session_not_found" };
|
|
302
|
+
const arts = listArtifacts(db, sessionId, { type: "input" });
|
|
303
|
+
if (arts.length === 0) return { fused: false, reason: "no_input" };
|
|
304
|
+
|
|
305
|
+
// Weighted aggregation: concatenate contents ordered by priority,
|
|
306
|
+
// compute sum(weight) and per-modality summary.
|
|
307
|
+
const byModality = {};
|
|
308
|
+
for (const a of arts) {
|
|
309
|
+
if (!byModality[a.modality]) byModality[a.modality] = [];
|
|
310
|
+
byModality[a.modality].push(a);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const parts = [];
|
|
314
|
+
const summary = [];
|
|
315
|
+
let totalWeight = 0;
|
|
316
|
+
for (const m of PRIORITY_ORDER) {
|
|
317
|
+
if (!byModality[m]) continue;
|
|
318
|
+
const weight = MODALITY_WEIGHTS[m] || 0.5;
|
|
319
|
+
totalWeight += weight;
|
|
320
|
+
for (const a of byModality[m]) {
|
|
321
|
+
const contentStr =
|
|
322
|
+
typeof a.content === "string"
|
|
323
|
+
? a.content
|
|
324
|
+
: JSON.stringify(a.content ?? "");
|
|
325
|
+
parts.push(`[${m} w=${weight}] ${contentStr}`);
|
|
326
|
+
}
|
|
327
|
+
summary.push({
|
|
328
|
+
modality: m,
|
|
329
|
+
count: byModality[m].length,
|
|
330
|
+
weight,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const fused = parts.join("\n\n");
|
|
335
|
+
return {
|
|
336
|
+
fused: true,
|
|
337
|
+
content: fused,
|
|
338
|
+
summary,
|
|
339
|
+
totalWeight: Number(totalWeight.toFixed(2)),
|
|
340
|
+
tokenEstimate: _estimateTokens(fused),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ── Document parsing ───────────────────────────────────── */
|
|
345
|
+
|
|
346
|
+
export function getSupportedFormats() {
|
|
347
|
+
return { formats: [...INPUT_FORMATS], native: [...NATIVE_FORMATS] };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function parseDocument(filePath, { content } = {}) {
|
|
351
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
352
|
+
if (!INPUT_FORMATS.includes(ext))
|
|
353
|
+
return { parsed: false, reason: "unsupported_format", ext };
|
|
354
|
+
|
|
355
|
+
let text;
|
|
356
|
+
if (content != null) {
|
|
357
|
+
text = typeof content === "string" ? content : content.toString("utf-8");
|
|
358
|
+
} else {
|
|
359
|
+
try {
|
|
360
|
+
text = fs.readFileSync(filePath, "utf-8");
|
|
361
|
+
} catch (_e) {
|
|
362
|
+
return { parsed: false, reason: "read_failed", ext };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (ext === "txt" || ext === "md") {
|
|
367
|
+
return {
|
|
368
|
+
parsed: true,
|
|
369
|
+
format: ext,
|
|
370
|
+
text,
|
|
371
|
+
length: text.length,
|
|
372
|
+
tokenEstimate: _estimateTokens(text),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (ext === "json") {
|
|
377
|
+
let json;
|
|
378
|
+
try {
|
|
379
|
+
json = JSON.parse(text);
|
|
380
|
+
} catch (_e) {
|
|
381
|
+
return { parsed: false, reason: "invalid_json", ext };
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
parsed: true,
|
|
385
|
+
format: "json",
|
|
386
|
+
text,
|
|
387
|
+
json,
|
|
388
|
+
length: text.length,
|
|
389
|
+
tokenEstimate: _estimateTokens(text),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (ext === "csv") {
|
|
394
|
+
const rows = _parseCsv(text);
|
|
395
|
+
return {
|
|
396
|
+
parsed: true,
|
|
397
|
+
format: "csv",
|
|
398
|
+
text,
|
|
399
|
+
rows,
|
|
400
|
+
rowCount: rows.length,
|
|
401
|
+
tokenEstimate: _estimateTokens(text),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// pdf / docx / xlsx — not implemented in CLI port
|
|
406
|
+
return {
|
|
407
|
+
parsed: false,
|
|
408
|
+
reason: "parser_not_available",
|
|
409
|
+
ext,
|
|
410
|
+
hint: `Install dedicated parser (pdf-parse / mammoth / xlsx) and use desktop for ${ext} parsing`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function _parseCsv(text) {
|
|
415
|
+
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
416
|
+
if (lines.length === 0) return [];
|
|
417
|
+
const header = lines[0].split(",").map((c) => c.trim());
|
|
418
|
+
const out = [];
|
|
419
|
+
for (let i = 1; i < lines.length; i++) {
|
|
420
|
+
const cells = lines[i].split(",").map((c) => c.trim());
|
|
421
|
+
const row = {};
|
|
422
|
+
header.forEach((h, idx) => {
|
|
423
|
+
row[h] = cells[idx] ?? "";
|
|
424
|
+
});
|
|
425
|
+
out.push(row);
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* ── Context management ─────────────────────────────────── */
|
|
431
|
+
|
|
432
|
+
export function buildContext(
|
|
433
|
+
db,
|
|
434
|
+
sessionId,
|
|
435
|
+
{ maxTokens = DEFAULT_MAX_TOKENS } = {},
|
|
436
|
+
) {
|
|
437
|
+
const s = getSession(db, sessionId);
|
|
438
|
+
if (!s) return { built: false, reason: "session_not_found" };
|
|
439
|
+
const arts = listArtifacts(db, sessionId, { type: "input" });
|
|
440
|
+
if (arts.length === 0) return { built: false, reason: "no_input" };
|
|
441
|
+
|
|
442
|
+
// Order artifacts by modality priority, then by createdAt
|
|
443
|
+
const sorted = [...arts].sort((a, b) => {
|
|
444
|
+
const pa = PRIORITY_ORDER.indexOf(a.modality);
|
|
445
|
+
const pb = PRIORITY_ORDER.indexOf(b.modality);
|
|
446
|
+
if (pa !== pb) return pa - pb;
|
|
447
|
+
return a.createdAt - b.createdAt;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const items = [];
|
|
451
|
+
let tokens = 0;
|
|
452
|
+
for (const a of sorted) {
|
|
453
|
+
const contentStr =
|
|
454
|
+
typeof a.content === "string"
|
|
455
|
+
? a.content
|
|
456
|
+
: JSON.stringify(a.content ?? "");
|
|
457
|
+
const t = _estimateTokens(contentStr);
|
|
458
|
+
if (tokens + t > maxTokens) {
|
|
459
|
+
const remaining = maxTokens - tokens;
|
|
460
|
+
if (remaining <= 0) break;
|
|
461
|
+
const clipped = contentStr.slice(0, remaining * 4);
|
|
462
|
+
items.push({
|
|
463
|
+
modality: a.modality,
|
|
464
|
+
content: clipped,
|
|
465
|
+
tokens: _estimateTokens(clipped),
|
|
466
|
+
truncated: true,
|
|
467
|
+
});
|
|
468
|
+
tokens += _estimateTokens(clipped);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
items.push({
|
|
472
|
+
modality: a.modality,
|
|
473
|
+
content: contentStr,
|
|
474
|
+
tokens: t,
|
|
475
|
+
truncated: false,
|
|
476
|
+
});
|
|
477
|
+
tokens += t;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const content = items
|
|
481
|
+
.map(
|
|
482
|
+
(i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`,
|
|
483
|
+
)
|
|
484
|
+
.join("\n\n");
|
|
485
|
+
|
|
486
|
+
const contextData = { items, tokens, maxTokens, content };
|
|
487
|
+
_contextCache.set(sessionId, contextData);
|
|
488
|
+
|
|
489
|
+
db.prepare(
|
|
490
|
+
"UPDATE multimodal_sessions SET context = ?, token_count = ?, updated_at = ? WHERE id = ?",
|
|
491
|
+
).run(JSON.stringify(contextData), tokens, _now(), sessionId);
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
built: true,
|
|
495
|
+
tokens,
|
|
496
|
+
maxTokens,
|
|
497
|
+
itemCount: items.length,
|
|
498
|
+
content,
|
|
499
|
+
items,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function getContext(db, sessionId) {
|
|
504
|
+
const cached = _contextCache.get(sessionId);
|
|
505
|
+
if (cached) return cached;
|
|
506
|
+
const s = getSession(db, sessionId);
|
|
507
|
+
if (!s || !s.context) return null;
|
|
508
|
+
return s.context;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function clearContext(db, sessionId) {
|
|
512
|
+
const s = getSession(db, sessionId);
|
|
513
|
+
if (!s) return { cleared: false, reason: "session_not_found" };
|
|
514
|
+
_contextCache.delete(sessionId);
|
|
515
|
+
db.prepare(
|
|
516
|
+
"UPDATE multimodal_sessions SET context = ?, token_count = ?, updated_at = ? WHERE id = ?",
|
|
517
|
+
).run(null, 0, _now(), sessionId);
|
|
518
|
+
return { cleared: true };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function trimContext(context, maxTokens) {
|
|
522
|
+
if (!context || !Array.isArray(context.items))
|
|
523
|
+
return { trimmed: false, reason: "invalid_context" };
|
|
524
|
+
let tokens = 0;
|
|
525
|
+
const items = [];
|
|
526
|
+
for (const i of context.items) {
|
|
527
|
+
const t = i.tokens ?? _estimateTokens(i.content);
|
|
528
|
+
if (tokens + t > maxTokens) {
|
|
529
|
+
const remaining = maxTokens - tokens;
|
|
530
|
+
if (remaining <= 0) break;
|
|
531
|
+
const clipped = (i.content || "").slice(0, remaining * 4);
|
|
532
|
+
items.push({
|
|
533
|
+
...i,
|
|
534
|
+
content: clipped,
|
|
535
|
+
tokens: _estimateTokens(clipped),
|
|
536
|
+
truncated: true,
|
|
537
|
+
});
|
|
538
|
+
tokens += _estimateTokens(clipped);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
items.push(i);
|
|
542
|
+
tokens += t;
|
|
543
|
+
}
|
|
544
|
+
const content = items
|
|
545
|
+
.map(
|
|
546
|
+
(i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`,
|
|
547
|
+
)
|
|
548
|
+
.join("\n\n");
|
|
549
|
+
return { trimmed: true, items, tokens, maxTokens, content };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* ── Output generation ──────────────────────────────────── */
|
|
553
|
+
|
|
554
|
+
export function getOutputFormats() {
|
|
555
|
+
return [...OUTPUT_FORMATS];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function generateOutput(db, sessionId, content, format, options = {}) {
|
|
559
|
+
if (!OUTPUT_FORMATS.includes(format))
|
|
560
|
+
return { generated: false, reason: "unsupported_format", format };
|
|
561
|
+
|
|
562
|
+
let produced;
|
|
563
|
+
switch (format) {
|
|
564
|
+
case "markdown":
|
|
565
|
+
produced =
|
|
566
|
+
typeof content === "string"
|
|
567
|
+
? content
|
|
568
|
+
: JSON.stringify(content, null, 2);
|
|
569
|
+
break;
|
|
570
|
+
case "html":
|
|
571
|
+
produced = _renderHtml(content, options);
|
|
572
|
+
break;
|
|
573
|
+
case "json":
|
|
574
|
+
produced =
|
|
575
|
+
typeof content === "string"
|
|
576
|
+
? JSON.stringify({ content }, null, 2)
|
|
577
|
+
: JSON.stringify(content, null, 2);
|
|
578
|
+
break;
|
|
579
|
+
case "csv":
|
|
580
|
+
produced = _renderCsv(content);
|
|
581
|
+
break;
|
|
582
|
+
case "slides":
|
|
583
|
+
produced = _renderSlides(content, options);
|
|
584
|
+
break;
|
|
585
|
+
case "chart":
|
|
586
|
+
produced = _renderChart(content, options);
|
|
587
|
+
break;
|
|
588
|
+
default:
|
|
589
|
+
return { generated: false, reason: "unsupported_format", format };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (sessionId) {
|
|
593
|
+
const s = getSession(db, sessionId);
|
|
594
|
+
if (s) {
|
|
595
|
+
_addArtifact(db, sessionId, {
|
|
596
|
+
type: "output",
|
|
597
|
+
format,
|
|
598
|
+
content: produced,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
generated: true,
|
|
605
|
+
format,
|
|
606
|
+
content: produced,
|
|
607
|
+
size: typeof produced === "string" ? produced.length : 0,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function _renderHtml(content, options) {
|
|
612
|
+
const title = options.title || "Multimodal Output";
|
|
613
|
+
const body =
|
|
614
|
+
typeof content === "string"
|
|
615
|
+
? content
|
|
616
|
+
: `<pre>${JSON.stringify(content, null, 2)}</pre>`;
|
|
617
|
+
return `<!DOCTYPE html>
|
|
618
|
+
<html><head><meta charset="UTF-8"><title>${title}</title></head>
|
|
619
|
+
<body>
|
|
620
|
+
${body}
|
|
621
|
+
</body></html>`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function _renderCsv(content) {
|
|
625
|
+
if (typeof content === "string") return content;
|
|
626
|
+
if (!Array.isArray(content)) return "";
|
|
627
|
+
if (content.length === 0) return "";
|
|
628
|
+
const header = Object.keys(content[0]);
|
|
629
|
+
const lines = [header.join(",")];
|
|
630
|
+
for (const row of content) {
|
|
631
|
+
lines.push(header.map((h) => String(row[h] ?? "")).join(","));
|
|
632
|
+
}
|
|
633
|
+
return lines.join("\n");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function _renderSlides(content, options) {
|
|
637
|
+
const title = options.title || "Multimodal Slides";
|
|
638
|
+
const slides = Array.isArray(content)
|
|
639
|
+
? content
|
|
640
|
+
: typeof content === "string"
|
|
641
|
+
? content.split(/\n---\n/)
|
|
642
|
+
: [String(content)];
|
|
643
|
+
const sections = slides
|
|
644
|
+
.map((s) => `<section>${_escapeHtml(s)}</section>`)
|
|
645
|
+
.join("\n");
|
|
646
|
+
return `<!DOCTYPE html>
|
|
647
|
+
<html><head><meta charset="UTF-8"><title>${title}</title>
|
|
648
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.0.0/dist/reveal.min.css">
|
|
649
|
+
</head><body>
|
|
650
|
+
<div class="reveal"><div class="slides">
|
|
651
|
+
${sections}
|
|
652
|
+
</div></div>
|
|
653
|
+
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.0.0/dist/reveal.min.js"></script>
|
|
654
|
+
<script>Reveal.initialize();</script>
|
|
655
|
+
</body></html>`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function _renderChart(data, options) {
|
|
659
|
+
const chartType = options.chartType || "line";
|
|
660
|
+
const option = {
|
|
661
|
+
title: { text: options.title || "Chart" },
|
|
662
|
+
tooltip: {},
|
|
663
|
+
xAxis: { data: data?.categories || [] },
|
|
664
|
+
yAxis: {},
|
|
665
|
+
series: Array.isArray(data?.series)
|
|
666
|
+
? data.series.map((s) => ({ ...s, type: s.type || chartType }))
|
|
667
|
+
: [
|
|
668
|
+
{
|
|
669
|
+
name: options.title || "series",
|
|
670
|
+
type: chartType,
|
|
671
|
+
data: data?.values || [],
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
};
|
|
675
|
+
return JSON.stringify(option, null, 2);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function _escapeHtml(s) {
|
|
679
|
+
return String(s)
|
|
680
|
+
.replace(/&/g, "&")
|
|
681
|
+
.replace(/</g, "<")
|
|
682
|
+
.replace(/>/g, ">");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/* ── Stats ──────────────────────────────────────────────── */
|
|
686
|
+
|
|
687
|
+
export function getMultimodalStats(db) {
|
|
688
|
+
const rows = db
|
|
689
|
+
.prepare("SELECT * FROM multimodal_sessions")
|
|
690
|
+
.all()
|
|
691
|
+
.map(_strip);
|
|
692
|
+
const arts = db
|
|
693
|
+
.prepare("SELECT * FROM multimodal_artifacts")
|
|
694
|
+
.all()
|
|
695
|
+
.map(_strip);
|
|
696
|
+
const byStatus = {};
|
|
697
|
+
let totalTokens = 0;
|
|
698
|
+
for (const r of rows) {
|
|
699
|
+
byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
700
|
+
totalTokens += r.token_count || 0;
|
|
701
|
+
}
|
|
702
|
+
const byModality = {};
|
|
703
|
+
let inputs = 0;
|
|
704
|
+
let outputs = 0;
|
|
705
|
+
for (const a of arts) {
|
|
706
|
+
if (a.type === "input") inputs++;
|
|
707
|
+
else if (a.type === "output") outputs++;
|
|
708
|
+
if (a.modality) byModality[a.modality] = (byModality[a.modality] || 0) + 1;
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
sessions: rows.length,
|
|
712
|
+
byStatus,
|
|
713
|
+
artifacts: arts.length,
|
|
714
|
+
inputs,
|
|
715
|
+
outputs,
|
|
716
|
+
byModality,
|
|
717
|
+
totalTokens,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* ── Reset (tests) ──────────────────────────────────────── */
|
|
722
|
+
|
|
723
|
+
export function _resetState() {
|
|
724
|
+
_contextCache.clear();
|
|
725
|
+
}
|