miladyai 2.0.0-alpha.27
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/dist/_virtual/_rolldown/runtime.js +7 -0
- package/dist/actions/emote.js +64 -0
- package/dist/actions/restart.js +81 -0
- package/dist/actions/send-message.js +152 -0
- package/dist/agent-admin-routes.js +82 -0
- package/dist/agent-lifecycle-routes.js +79 -0
- package/dist/agent-transfer-routes.js +102 -0
- package/dist/api/agent-admin-routes.js +82 -0
- package/dist/api/agent-lifecycle-routes.js +79 -0
- package/dist/api/agent-transfer-routes.js +102 -0
- package/dist/api/apps-hyperscape-routes.js +58 -0
- package/dist/api/apps-routes.js +114 -0
- package/dist/api/auth-routes.js +56 -0
- package/dist/api/autonomy-routes.js +44 -0
- package/dist/api/bug-report-routes.js +111 -0
- package/dist/api/character-routes.js +195 -0
- package/dist/api/cloud-routes.js +330 -0
- package/dist/api/cloud-status-routes.js +155 -0
- package/dist/api/compat-utils.js +111 -0
- package/dist/api/database.js +735 -0
- package/dist/api/diagnostics-routes.js +205 -0
- package/dist/api/drop-service.js +134 -0
- package/dist/api/early-logs.js +86 -0
- package/dist/api/http-helpers.js +131 -0
- package/dist/api/knowledge-routes.js +534 -0
- package/dist/api/memory-bounds.js +71 -0
- package/dist/api/models-routes.js +28 -0
- package/dist/api/og-tracker.js +36 -0
- package/dist/api/permissions-routes.js +109 -0
- package/dist/api/plugin-validation.js +198 -0
- package/dist/api/provider-switch-config.js +41 -0
- package/dist/api/registry-routes.js +86 -0
- package/dist/api/registry-service.js +164 -0
- package/dist/api/sandbox-routes.js +1112 -0
- package/dist/api/server.js +7949 -0
- package/dist/api/subscription-routes.js +172 -0
- package/dist/api/terminal-run-limits.js +24 -0
- package/dist/api/training-routes.js +158 -0
- package/dist/api/trajectory-routes.js +300 -0
- package/dist/api/trigger-routes.js +246 -0
- package/dist/api/twitter-verify.js +134 -0
- package/dist/api/tx-service.js +108 -0
- package/dist/api/wallet-routes.js +266 -0
- package/dist/api/wallet.js +568 -0
- package/dist/api/whatsapp-routes.js +182 -0
- package/dist/api/zip-utils.js +109 -0
- package/dist/apps-hyperscape-routes.js +58 -0
- package/dist/apps-routes.js +114 -0
- package/dist/ascii.js +20 -0
- package/dist/auth/anthropic.js +44 -0
- package/dist/auth/apply-stealth.js +41 -0
- package/dist/auth/claude-code-stealth.js +78 -0
- package/dist/auth/credentials.js +156 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/openai-codex.js +66 -0
- package/dist/auth/types.js +9 -0
- package/dist/auth-routes.js +56 -0
- package/dist/autonomy-routes.js +44 -0
- package/dist/bug-report-routes.js +111 -0
- package/dist/build-info.json +6 -0
- package/dist/character-routes.js +195 -0
- package/dist/cli/argv.js +63 -0
- package/dist/cli/banner.js +34 -0
- package/dist/cli/cli-name.js +21 -0
- package/dist/cli/cli-utils.js +16 -0
- package/dist/cli/git-commit.js +78 -0
- package/dist/cli/parse-duration.js +15 -0
- package/dist/cli/plugins-cli.js +590 -0
- package/dist/cli/profile-utils.js +9 -0
- package/dist/cli/profile.js +95 -0
- package/dist/cli/program/build-program.js +17 -0
- package/dist/cli/program/command-registry.js +23 -0
- package/dist/cli/program/help.js +47 -0
- package/dist/cli/program/preaction.js +33 -0
- package/dist/cli/program/register.config.js +106 -0
- package/dist/cli/program/register.configure.js +20 -0
- package/dist/cli/program/register.dashboard.js +124 -0
- package/dist/cli/program/register.models.js +23 -0
- package/dist/cli/program/register.setup.js +36 -0
- package/dist/cli/program/register.start.js +22 -0
- package/dist/cli/program/register.subclis.js +70 -0
- package/dist/cli/program/register.tui.js +163 -0
- package/dist/cli/program/register.update.js +154 -0
- package/dist/cli/program.js +3 -0
- package/dist/cli/run-main.js +37 -0
- package/dist/cli/version.js +7 -0
- package/dist/cloud/validate-url.js +93 -0
- package/dist/cloud-routes.js +330 -0
- package/dist/cloud-status-routes.js +155 -0
- package/dist/compat-utils.js +111 -0
- package/dist/config/config.js +69 -0
- package/dist/config/env-vars.js +19 -0
- package/dist/config/includes.js +121 -0
- package/dist/config/object-utils.js +7 -0
- package/dist/config/paths.js +38 -0
- package/dist/config/plugin-auto-enable.js +231 -0
- package/dist/config/schema.js +864 -0
- package/dist/config/telegram-custom-commands.js +76 -0
- package/dist/config/zod-schema.agent-runtime.js +519 -0
- package/dist/config/zod-schema.core.js +538 -0
- package/dist/config/zod-schema.hooks.js +103 -0
- package/dist/config/zod-schema.js +488 -0
- package/dist/config/zod-schema.providers-core.js +785 -0
- package/dist/config/zod-schema.session.js +73 -0
- package/dist/core-plugins.js +37 -0
- package/dist/custom-actions.js +250 -0
- package/dist/database.js +735 -0
- package/dist/diagnostics/integration-observability.js +57 -0
- package/dist/diagnostics-routes.js +205 -0
- package/dist/drop-service.js +134 -0
- package/dist/early-logs.js +24 -0
- package/dist/eliza.js +2061 -0
- package/dist/emotes/catalog.js +271 -0
- package/dist/entry.js +40 -0
- package/dist/hooks/discovery.js +167 -0
- package/dist/hooks/eligibility.js +64 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/loader.js +147 -0
- package/dist/hooks/registry.js +55 -0
- package/dist/http-helpers.js +131 -0
- package/dist/index.js +49 -0
- package/dist/knowledge-routes.js +534 -0
- package/dist/memory-bounds.js +71 -0
- package/dist/milady-plugin.js +90 -0
- package/dist/models-routes.js +28 -0
- package/dist/onboarding-names.js +78 -0
- package/dist/onboarding-presets.js +922 -0
- package/dist/package.json +1 -0
- package/dist/permissions-routes.js +109 -0
- package/dist/plugin-validation.js +107 -0
- package/dist/plugins/whatsapp/actions.js +91 -0
- package/dist/plugins/whatsapp/index.js +16 -0
- package/dist/plugins/whatsapp/service.js +270 -0
- package/dist/provider-switch-config.js +41 -0
- package/dist/providers/admin-trust.js +46 -0
- package/dist/providers/autonomous-state.js +101 -0
- package/dist/providers/session-bridge.js +86 -0
- package/dist/providers/session-utils.js +36 -0
- package/dist/providers/simple-mode.js +50 -0
- package/dist/providers/ui-catalog.js +15 -0
- package/dist/providers/workspace-provider.js +93 -0
- package/dist/providers/workspace.js +348 -0
- package/dist/registry-routes.js +86 -0
- package/dist/registry-service.js +164 -0
- package/dist/restart.js +40 -0
- package/dist/runtime/core-plugins.js +37 -0
- package/dist/runtime/custom-actions.js +250 -0
- package/dist/runtime/eliza.js +2061 -0
- package/dist/runtime/embedding-manager-support.js +185 -0
- package/dist/runtime/embedding-manager.js +193 -0
- package/dist/runtime/embedding-presets.js +54 -0
- package/dist/runtime/embedding-state.js +8 -0
- package/dist/runtime/milady-plugin.js +90 -0
- package/dist/runtime/onboarding-names.js +78 -0
- package/dist/runtime/restart.js +40 -0
- package/dist/runtime/version.js +7 -0
- package/dist/sandbox-routes.js +1112 -0
- package/dist/security/audit-log.js +149 -0
- package/dist/security/network-policy.js +70 -0
- package/dist/server.js +7949 -0
- package/dist/services/agent-export.js +559 -0
- package/dist/services/app-manager.js +389 -0
- package/dist/services/browser-capture.js +86 -0
- package/dist/services/fallback-training-service.js +128 -0
- package/dist/services/mcp-marketplace.js +134 -0
- package/dist/services/plugin-installer.js +396 -0
- package/dist/services/plugin-manager-types.js +15 -0
- package/dist/services/registry-client-app-meta.js +144 -0
- package/dist/services/registry-client-endpoints.js +166 -0
- package/dist/services/registry-client-local.js +271 -0
- package/dist/services/registry-client-network.js +93 -0
- package/dist/services/registry-client-queries.js +70 -0
- package/dist/services/registry-client.js +157 -0
- package/dist/services/sandbox-engine.js +511 -0
- package/dist/services/sandbox-manager.js +297 -0
- package/dist/services/self-updater.js +175 -0
- package/dist/services/skill-catalog-client.js +119 -0
- package/dist/services/skill-marketplace.js +521 -0
- package/dist/services/stream-manager.js +236 -0
- package/dist/services/update-checker.js +121 -0
- package/dist/services/update-notifier.js +29 -0
- package/dist/services/version-compat.js +78 -0
- package/dist/services/whatsapp-pairing.js +196 -0
- package/dist/shared/ui-catalog-prompt.js +728 -0
- package/dist/subscription-routes.js +172 -0
- package/dist/terminal/links.js +19 -0
- package/dist/terminal/palette.js +14 -0
- package/dist/terminal/theme.js +25 -0
- package/dist/terminal-run-limits.js +24 -0
- package/dist/training-routes.js +158 -0
- package/dist/trajectory-routes.js +300 -0
- package/dist/trigger-routes.js +246 -0
- package/dist/triggers/action.js +218 -0
- package/dist/triggers/runtime.js +281 -0
- package/dist/triggers/scheduling.js +295 -0
- package/dist/triggers/types.js +5 -0
- package/dist/tui/components/assistant-message.js +76 -0
- package/dist/tui/components/chat-editor.js +34 -0
- package/dist/tui/components/embeddings-overlay.js +46 -0
- package/dist/tui/components/footer.js +60 -0
- package/dist/tui/components/index.js +15 -0
- package/dist/tui/components/modal-frame.js +45 -0
- package/dist/tui/components/modal-style.js +15 -0
- package/dist/tui/components/model-selector.js +70 -0
- package/dist/tui/components/pinned-chat-layout.js +46 -0
- package/dist/tui/components/plugins-endpoints-tab.js +196 -0
- package/dist/tui/components/plugins-installed-tab-view.js +69 -0
- package/dist/tui/components/plugins-installed-tab.js +319 -0
- package/dist/tui/components/plugins-overlay-catalog.js +81 -0
- package/dist/tui/components/plugins-overlay-data-api.js +21 -0
- package/dist/tui/components/plugins-overlay-data-shared.js +20 -0
- package/dist/tui/components/plugins-overlay-data.js +323 -0
- package/dist/tui/components/plugins-overlay.js +117 -0
- package/dist/tui/components/plugins-store-tab.js +148 -0
- package/dist/tui/components/settings-overlay.js +61 -0
- package/dist/tui/components/status-bar.js +64 -0
- package/dist/tui/components/tool-execution.js +68 -0
- package/dist/tui/components/user-message.js +22 -0
- package/dist/tui/eliza-tui-bridge.js +606 -0
- package/dist/tui/index.js +370 -0
- package/dist/tui/modal-presets.js +33 -0
- package/dist/tui/model-spec.js +46 -0
- package/dist/tui/sse-parser.js +78 -0
- package/dist/tui/theme.js +110 -0
- package/dist/tui/titlebar-spinner.js +62 -0
- package/dist/tui/tui-app.js +311 -0
- package/dist/tui/ws-client.js +215 -0
- package/dist/twitter-verify.js +134 -0
- package/dist/tx-service.js +108 -0
- package/dist/utils/exec-safety.js +17 -0
- package/dist/utils/globals.js +20 -0
- package/dist/utils/milady-root.js +61 -0
- package/dist/utils/number-parsing.js +37 -0
- package/dist/version-resolver.js +37 -0
- package/dist/version.js +7 -0
- package/dist/wallet-routes.js +266 -0
- package/dist/wallet.js +568 -0
- package/dist/whatsapp-routes.js +182 -0
- package/dist/zip-utils.js +109 -0
- package/milady.mjs +14 -0
- package/package.json +111 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { isBlockedPrivateOrLinkLocalIp, normalizeHostLike } from "../security/network-policy.js";
|
|
2
|
+
import { parseClampedFloat, parsePositiveInteger } from "../utils/number-parsing.js";
|
|
3
|
+
import { lookup } from "node:dns/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
|
|
6
|
+
//#region src/api/knowledge-routes.ts
|
|
7
|
+
const FRAGMENT_COUNT_BATCH_SIZE = 500;
|
|
8
|
+
const MAX_URL_IMPORT_BYTES = 10 * 1024 * 1024;
|
|
9
|
+
const MAX_YOUTUBE_WATCH_PAGE_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
const MAX_YOUTUBE_TRANSCRIPT_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const URL_FETCH_TIMEOUT_MS = 15e3;
|
|
12
|
+
const BLOCKED_HOST_LITERALS = new Set(["localhost", "metadata.google.internal"]);
|
|
13
|
+
function toSafeNumber(value, fallback) {
|
|
14
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
18
|
+
}
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
function hasUuidId(memory) {
|
|
22
|
+
return typeof memory.id === "string" && memory.id.length > 0;
|
|
23
|
+
}
|
|
24
|
+
function hasUuidIdAndCreatedAt(memory) {
|
|
25
|
+
return hasUuidId(memory) && typeof memory.createdAt === "number";
|
|
26
|
+
}
|
|
27
|
+
async function countKnowledgeFragmentsForDocument(knowledgeService, roomId, documentId) {
|
|
28
|
+
let offset = 0;
|
|
29
|
+
let fragmentCount = 0;
|
|
30
|
+
while (true) {
|
|
31
|
+
const knowledgeBatch = await knowledgeService.getMemories({
|
|
32
|
+
tableName: "knowledge",
|
|
33
|
+
roomId,
|
|
34
|
+
count: FRAGMENT_COUNT_BATCH_SIZE,
|
|
35
|
+
offset
|
|
36
|
+
});
|
|
37
|
+
if (knowledgeBatch.length === 0) break;
|
|
38
|
+
fragmentCount += knowledgeBatch.filter((memory) => {
|
|
39
|
+
return memory.metadata?.documentId === documentId;
|
|
40
|
+
}).length;
|
|
41
|
+
if (knowledgeBatch.length < FRAGMENT_COUNT_BATCH_SIZE) break;
|
|
42
|
+
offset += FRAGMENT_COUNT_BATCH_SIZE;
|
|
43
|
+
}
|
|
44
|
+
return fragmentCount;
|
|
45
|
+
}
|
|
46
|
+
async function mapKnowledgeFragmentsByDocumentId(knowledgeService, roomId, documentIds) {
|
|
47
|
+
const fragmentCounts = /* @__PURE__ */ new Map();
|
|
48
|
+
const trackedDocumentIds = new Set(documentIds);
|
|
49
|
+
for (const documentId of trackedDocumentIds) fragmentCounts.set(documentId, 0);
|
|
50
|
+
if (trackedDocumentIds.size === 0) return fragmentCounts;
|
|
51
|
+
let offset = 0;
|
|
52
|
+
while (true) {
|
|
53
|
+
const knowledgeBatch = await knowledgeService.getMemories({
|
|
54
|
+
tableName: "knowledge",
|
|
55
|
+
roomId,
|
|
56
|
+
count: FRAGMENT_COUNT_BATCH_SIZE,
|
|
57
|
+
offset
|
|
58
|
+
});
|
|
59
|
+
if (knowledgeBatch.length === 0) break;
|
|
60
|
+
for (const memory of knowledgeBatch) {
|
|
61
|
+
const documentId = memory.metadata?.documentId;
|
|
62
|
+
if (typeof documentId === "string" && trackedDocumentIds.has(documentId)) {
|
|
63
|
+
const currentCount = fragmentCounts.get(documentId) ?? 0;
|
|
64
|
+
fragmentCounts.set(documentId, currentCount + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (knowledgeBatch.length < FRAGMENT_COUNT_BATCH_SIZE) break;
|
|
68
|
+
offset += FRAGMENT_COUNT_BATCH_SIZE;
|
|
69
|
+
}
|
|
70
|
+
return fragmentCounts;
|
|
71
|
+
}
|
|
72
|
+
async function listKnowledgeFragmentsForDocument(knowledgeService, roomId, documentId) {
|
|
73
|
+
let offset = 0;
|
|
74
|
+
const fragmentIds = [];
|
|
75
|
+
while (true) {
|
|
76
|
+
const knowledgeBatch = await knowledgeService.getMemories({
|
|
77
|
+
tableName: "knowledge",
|
|
78
|
+
roomId,
|
|
79
|
+
count: FRAGMENT_COUNT_BATCH_SIZE,
|
|
80
|
+
offset
|
|
81
|
+
});
|
|
82
|
+
for (const memory of knowledgeBatch) if (memory.metadata?.documentId === documentId && hasUuidId(memory)) fragmentIds.push(memory.id);
|
|
83
|
+
if (knowledgeBatch.length < FRAGMENT_COUNT_BATCH_SIZE) break;
|
|
84
|
+
offset += FRAGMENT_COUNT_BATCH_SIZE;
|
|
85
|
+
}
|
|
86
|
+
return fragmentIds;
|
|
87
|
+
}
|
|
88
|
+
async function getKnowledgeService(runtime) {
|
|
89
|
+
if (!runtime) return null;
|
|
90
|
+
let service = runtime.getService("knowledge");
|
|
91
|
+
if (service) return service;
|
|
92
|
+
try {
|
|
93
|
+
const servicePromise = runtime.getServiceLoadPromise("knowledge");
|
|
94
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
reject(/* @__PURE__ */ new Error("Knowledge service load timeout (10s)"));
|
|
97
|
+
}, 1e4);
|
|
98
|
+
});
|
|
99
|
+
await Promise.race([servicePromise, timeout]);
|
|
100
|
+
service = runtime.getService("knowledge");
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return service;
|
|
105
|
+
}
|
|
106
|
+
function isBlockedIp(ip) {
|
|
107
|
+
return isBlockedPrivateOrLinkLocalIp(ip);
|
|
108
|
+
}
|
|
109
|
+
async function resolveUrlSafetyRejection(url) {
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = new URL(url);
|
|
113
|
+
} catch {
|
|
114
|
+
return "Invalid URL format";
|
|
115
|
+
}
|
|
116
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "Only http:// and https:// URLs are allowed";
|
|
117
|
+
const hostname = normalizeHostLike(parsed.hostname);
|
|
118
|
+
if (!hostname) return "URL hostname is required";
|
|
119
|
+
if (BLOCKED_HOST_LITERALS.has(hostname)) return `URL host "${hostname}" is blocked for security reasons`;
|
|
120
|
+
if (net.isIP(hostname)) {
|
|
121
|
+
if (isBlockedIp(hostname)) return `URL host "${hostname}" is blocked for security reasons`;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
let addresses;
|
|
125
|
+
try {
|
|
126
|
+
const resolved = await lookup(hostname, { all: true });
|
|
127
|
+
addresses = Array.isArray(resolved) ? resolved : [resolved];
|
|
128
|
+
} catch {
|
|
129
|
+
return `Could not resolve URL host "${hostname}"`;
|
|
130
|
+
}
|
|
131
|
+
if (addresses.length === 0) return `Could not resolve URL host "${hostname}"`;
|
|
132
|
+
for (const entry of addresses) if (isBlockedIp(entry.address)) return `URL host "${hostname}" resolves to blocked address ${entry.address}`;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function isYouTubeUrl(url) {
|
|
136
|
+
return /^https?:\/\/(www\.)?(youtube\.com|youtu\.be)/.test(url);
|
|
137
|
+
}
|
|
138
|
+
function extractYouTubeVideoId(url) {
|
|
139
|
+
const shortMatch = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
|
|
140
|
+
if (shortMatch) return shortMatch[1];
|
|
141
|
+
const watchMatch = url.match(/[?&]v=([a-zA-Z0-9_-]{11})/);
|
|
142
|
+
if (watchMatch) return watchMatch[1];
|
|
143
|
+
const embedMatch = url.match(/\/embed\/([a-zA-Z0-9_-]{11})/);
|
|
144
|
+
if (embedMatch) return embedMatch[1];
|
|
145
|
+
const vMatch = url.match(/\/v\/([a-zA-Z0-9_-]{11})/);
|
|
146
|
+
if (vMatch) return vMatch[1];
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
async function fetchYouTubeTranscript(videoId) {
|
|
150
|
+
const response = await fetchWithTimeout(`https://www.youtube.com/watch?v=${videoId}`, { headers: {
|
|
151
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
152
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
153
|
+
} });
|
|
154
|
+
if (!response.ok) return null;
|
|
155
|
+
const html = new TextDecoder().decode(await readResponseBodyWithLimit(response, MAX_YOUTUBE_WATCH_PAGE_BYTES));
|
|
156
|
+
if (!html.match(/"captions":\s*\{[^}]*"playerCaptionsTracklistRenderer":\s*\{[^}]*"captionTracks":\s*\[([^\]]+)\]/)) {
|
|
157
|
+
if (!html.match(/"captionTracks":\s*\[([^\]]+)\]/)) return null;
|
|
158
|
+
}
|
|
159
|
+
const baseUrlMatch = html.match(/"baseUrl":\s*"(https:\/\/www\.youtube\.com\/api\/timedtext[^"]+)"/);
|
|
160
|
+
if (!baseUrlMatch) return null;
|
|
161
|
+
const transcriptResponse = await fetchWithTimeout(baseUrlMatch[1].replace(/\\u0026/g, "&").replace(/\\\//g, "/"), {});
|
|
162
|
+
if (!transcriptResponse.ok) return null;
|
|
163
|
+
const textMatches = new TextDecoder().decode(await readResponseBodyWithLimit(transcriptResponse, MAX_YOUTUBE_TRANSCRIPT_BYTES)).matchAll(/<text[^>]*>([^<]*)<\/text>/g);
|
|
164
|
+
const segments = [];
|
|
165
|
+
for (const match of textMatches) {
|
|
166
|
+
const text = match[1].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/ /g, " ").trim();
|
|
167
|
+
if (text) segments.push(text);
|
|
168
|
+
}
|
|
169
|
+
if (segments.length === 0) return null;
|
|
170
|
+
return segments.join(" ");
|
|
171
|
+
}
|
|
172
|
+
function readContentLengthHeader(response) {
|
|
173
|
+
const raw = response.headers.get("content-length");
|
|
174
|
+
if (!raw) return null;
|
|
175
|
+
const parsed = Number.parseInt(raw, 10);
|
|
176
|
+
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
function isAbortError(error) {
|
|
180
|
+
return error instanceof DOMException ? error.name === "AbortError" : error instanceof Error && error.name === "AbortError";
|
|
181
|
+
}
|
|
182
|
+
async function fetchWithTimeout(input, init, timeoutMs = URL_FETCH_TIMEOUT_MS) {
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const upstreamSignal = init.signal;
|
|
185
|
+
const onAbort = () => controller.abort();
|
|
186
|
+
if (upstreamSignal) if (upstreamSignal.aborted) controller.abort();
|
|
187
|
+
else upstreamSignal.addEventListener("abort", onAbort, { once: true });
|
|
188
|
+
const timeoutHandle = setTimeout(() => {
|
|
189
|
+
controller.abort();
|
|
190
|
+
}, timeoutMs);
|
|
191
|
+
try {
|
|
192
|
+
return await fetch(input, {
|
|
193
|
+
...init,
|
|
194
|
+
signal: controller.signal
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (isAbortError(error)) throw new Error(`URL fetch timed out after ${timeoutMs}ms`);
|
|
198
|
+
throw error;
|
|
199
|
+
} finally {
|
|
200
|
+
clearTimeout(timeoutHandle);
|
|
201
|
+
if (upstreamSignal) upstreamSignal.removeEventListener("abort", onAbort);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function readResponseBodyWithLimit(response, maxBytes) {
|
|
205
|
+
const declaredLength = readContentLengthHeader(response);
|
|
206
|
+
if (declaredLength !== null && declaredLength > maxBytes) throw new Error(`URL content exceeds maximum size of ${maxBytes} bytes`);
|
|
207
|
+
if (!response.body) {
|
|
208
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
209
|
+
if (bytes.byteLength > maxBytes) throw new Error(`URL content exceeds maximum size of ${maxBytes} bytes`);
|
|
210
|
+
return bytes;
|
|
211
|
+
}
|
|
212
|
+
const reader = response.body.getReader();
|
|
213
|
+
const chunks = [];
|
|
214
|
+
let totalBytes = 0;
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
const { done, value } = await reader.read();
|
|
218
|
+
if (done) break;
|
|
219
|
+
if (!value) continue;
|
|
220
|
+
totalBytes += value.byteLength;
|
|
221
|
+
if (totalBytes > maxBytes) throw new Error(`URL content exceeds maximum size of ${maxBytes} bytes`);
|
|
222
|
+
chunks.push(value);
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
try {
|
|
226
|
+
await reader.cancel(err);
|
|
227
|
+
} catch {}
|
|
228
|
+
throw err;
|
|
229
|
+
} finally {
|
|
230
|
+
reader.releaseLock();
|
|
231
|
+
}
|
|
232
|
+
const output = new Uint8Array(totalBytes);
|
|
233
|
+
let offset = 0;
|
|
234
|
+
for (const chunk of chunks) {
|
|
235
|
+
output.set(chunk, offset);
|
|
236
|
+
offset += chunk.byteLength;
|
|
237
|
+
}
|
|
238
|
+
return output;
|
|
239
|
+
}
|
|
240
|
+
async function fetchUrlContent(url) {
|
|
241
|
+
if (isYouTubeUrl(url)) {
|
|
242
|
+
const videoId = extractYouTubeVideoId(url);
|
|
243
|
+
if (!videoId) throw new Error("Invalid YouTube URL: could not extract video ID");
|
|
244
|
+
const transcript = await fetchYouTubeTranscript(videoId);
|
|
245
|
+
if (!transcript) throw new Error("Could not fetch YouTube transcript. The video may not have captions available.");
|
|
246
|
+
return {
|
|
247
|
+
content: transcript,
|
|
248
|
+
contentType: "text/plain",
|
|
249
|
+
filename: `youtube-${videoId}-transcript.txt`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const response = await fetchWithTimeout(url, {
|
|
253
|
+
redirect: "manual",
|
|
254
|
+
headers: { "User-Agent": "Mozilla/5.0 (compatible; Milaidy/1.0; +https://milaidy.ai)" }
|
|
255
|
+
});
|
|
256
|
+
if (response.status >= 300 && response.status < 400) throw new Error("URL redirects are not allowed");
|
|
257
|
+
if (!response.ok) throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
|
|
258
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
259
|
+
const pathSegments = new URL(url).pathname.split("/");
|
|
260
|
+
const encodedFilename = pathSegments[pathSegments.length - 1] || "document";
|
|
261
|
+
const filename = decodeURIComponent(encodedFilename);
|
|
262
|
+
const buffer = await readResponseBodyWithLimit(response, MAX_URL_IMPORT_BYTES);
|
|
263
|
+
if (contentType.startsWith("application/pdf") || contentType.startsWith("application/msword") || contentType.startsWith("application/vnd.openxmlformats-officedocument") || contentType.startsWith("image/")) return {
|
|
264
|
+
content: Buffer.from(buffer).toString("base64"),
|
|
265
|
+
contentType,
|
|
266
|
+
filename
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
content: new TextDecoder().decode(buffer),
|
|
270
|
+
contentType,
|
|
271
|
+
filename
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async function handleKnowledgeRoutes(ctx) {
|
|
275
|
+
const { req, res, method, pathname, url, runtime, json, error, readJsonBody } = ctx;
|
|
276
|
+
if (!pathname.startsWith("/api/knowledge")) return false;
|
|
277
|
+
const knowledgeService = await getKnowledgeService(runtime);
|
|
278
|
+
if (!knowledgeService) {
|
|
279
|
+
error(res, "Knowledge service is not available. Agent may not be running.", 503);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (!runtime?.agentId) {
|
|
283
|
+
error(res, "Agent runtime is not available", 503);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const agentId = runtime.agentId;
|
|
287
|
+
if (method === "GET" && pathname === "/api/knowledge/stats") {
|
|
288
|
+
json(res, {
|
|
289
|
+
documentCount: await knowledgeService.countMemories({
|
|
290
|
+
tableName: "documents",
|
|
291
|
+
roomId: agentId,
|
|
292
|
+
unique: false
|
|
293
|
+
}),
|
|
294
|
+
fragmentCount: await knowledgeService.countMemories({
|
|
295
|
+
tableName: "knowledge",
|
|
296
|
+
roomId: agentId,
|
|
297
|
+
unique: false
|
|
298
|
+
}),
|
|
299
|
+
agentId
|
|
300
|
+
});
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (method === "GET" && pathname === "/api/knowledge/documents") {
|
|
304
|
+
const limit = parsePositiveInteger(url.searchParams.get("limit"), 100);
|
|
305
|
+
const offset = parsePositiveInteger(url.searchParams.get("offset"), 0);
|
|
306
|
+
const documents = await knowledgeService.getMemories({
|
|
307
|
+
tableName: "documents",
|
|
308
|
+
roomId: agentId,
|
|
309
|
+
count: limit,
|
|
310
|
+
offset: offset > 0 ? offset : void 0
|
|
311
|
+
});
|
|
312
|
+
const fragmentCounts = await mapKnowledgeFragmentsByDocumentId(knowledgeService, agentId, documents.filter(hasUuidId).map((doc) => doc.id));
|
|
313
|
+
const cleanedDocuments = documents.map((doc) => {
|
|
314
|
+
const metadata = doc.metadata;
|
|
315
|
+
const documentId = hasUuidId(doc) ? doc.id : null;
|
|
316
|
+
return {
|
|
317
|
+
id: doc.id,
|
|
318
|
+
filename: metadata?.filename || metadata?.title || "Untitled",
|
|
319
|
+
contentType: metadata?.fileType || metadata?.contentType || "unknown",
|
|
320
|
+
fileSize: toSafeNumber(metadata?.fileSize, 0),
|
|
321
|
+
createdAt: toSafeNumber(doc.createdAt, 0),
|
|
322
|
+
fragmentCount: documentId !== null && fragmentCounts.has(documentId) ? fragmentCounts.get(documentId) ?? 0 : 0,
|
|
323
|
+
source: metadata?.source || "upload",
|
|
324
|
+
url: metadata?.url
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
json(res, {
|
|
328
|
+
documents: cleanedDocuments,
|
|
329
|
+
total: cleanedDocuments.length,
|
|
330
|
+
limit,
|
|
331
|
+
offset: offset > 0 ? offset : 0
|
|
332
|
+
});
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
const docIdMatch = /^\/api\/knowledge\/documents\/([^/]+)$/.exec(pathname);
|
|
336
|
+
if (method === "GET" && docIdMatch) {
|
|
337
|
+
const documentId = decodeURIComponent(docIdMatch[1]);
|
|
338
|
+
const document = (await knowledgeService.getMemories({
|
|
339
|
+
tableName: "documents",
|
|
340
|
+
roomId: agentId,
|
|
341
|
+
count: 1e4
|
|
342
|
+
})).find((d) => d.id === documentId);
|
|
343
|
+
if (!document) {
|
|
344
|
+
error(res, "Document not found", 404);
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
const fragmentCount = await countKnowledgeFragmentsForDocument(knowledgeService, agentId, documentId);
|
|
348
|
+
const metadata = document.metadata;
|
|
349
|
+
json(res, { document: {
|
|
350
|
+
id: document.id,
|
|
351
|
+
filename: metadata?.filename || metadata?.title || "Untitled",
|
|
352
|
+
contentType: metadata?.fileType || metadata?.contentType || "unknown",
|
|
353
|
+
fileSize: toSafeNumber(metadata?.fileSize, 0),
|
|
354
|
+
createdAt: toSafeNumber(document.createdAt, 0),
|
|
355
|
+
fragmentCount,
|
|
356
|
+
source: metadata?.source || "upload",
|
|
357
|
+
url: metadata?.url,
|
|
358
|
+
content: document.content
|
|
359
|
+
} });
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
if (method === "DELETE" && docIdMatch) {
|
|
363
|
+
const documentId = decodeURIComponent(docIdMatch[1]);
|
|
364
|
+
const fragmentIds = await listKnowledgeFragmentsForDocument(knowledgeService, agentId, documentId);
|
|
365
|
+
for (const fragmentId of fragmentIds) await knowledgeService.deleteMemory(fragmentId);
|
|
366
|
+
await knowledgeService.deleteMemory(documentId);
|
|
367
|
+
json(res, {
|
|
368
|
+
ok: true,
|
|
369
|
+
deletedFragments: fragmentIds.length
|
|
370
|
+
});
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
if (method === "POST" && pathname === "/api/knowledge/documents") {
|
|
374
|
+
const body = await readJsonBody(req, res);
|
|
375
|
+
if (!body) return true;
|
|
376
|
+
if (!body.content || !body.filename) {
|
|
377
|
+
error(res, "content and filename are required");
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
const result = await knowledgeService.addKnowledge({
|
|
381
|
+
agentId,
|
|
382
|
+
worldId: agentId,
|
|
383
|
+
roomId: agentId,
|
|
384
|
+
entityId: agentId,
|
|
385
|
+
clientDocumentId: "",
|
|
386
|
+
contentType: body.contentType || "text/plain",
|
|
387
|
+
originalFilename: body.filename,
|
|
388
|
+
content: body.content,
|
|
389
|
+
metadata: body.metadata
|
|
390
|
+
});
|
|
391
|
+
json(res, {
|
|
392
|
+
ok: true,
|
|
393
|
+
documentId: result.clientDocumentId,
|
|
394
|
+
fragmentCount: result.fragmentCount
|
|
395
|
+
});
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (method === "POST" && pathname === "/api/knowledge/documents/url") {
|
|
399
|
+
const body = await readJsonBody(req, res);
|
|
400
|
+
if (!body) return true;
|
|
401
|
+
if (!body.url?.trim()) {
|
|
402
|
+
error(res, "url is required");
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
const urlToFetch = body.url.trim();
|
|
406
|
+
const safetyRejection = await resolveUrlSafetyRejection(urlToFetch);
|
|
407
|
+
if (safetyRejection) {
|
|
408
|
+
error(res, safetyRejection);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
let content;
|
|
412
|
+
let contentType;
|
|
413
|
+
let filename;
|
|
414
|
+
try {
|
|
415
|
+
({content, contentType, filename} = await fetchUrlContent(urlToFetch));
|
|
416
|
+
} catch (err) {
|
|
417
|
+
error(res, err instanceof Error ? err.message : "Failed to fetch URL content");
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
const result = await knowledgeService.addKnowledge({
|
|
421
|
+
agentId,
|
|
422
|
+
worldId: agentId,
|
|
423
|
+
roomId: agentId,
|
|
424
|
+
entityId: agentId,
|
|
425
|
+
clientDocumentId: "",
|
|
426
|
+
contentType,
|
|
427
|
+
originalFilename: filename,
|
|
428
|
+
content,
|
|
429
|
+
metadata: {
|
|
430
|
+
...body.metadata,
|
|
431
|
+
url: urlToFetch,
|
|
432
|
+
source: isYouTubeUrl(urlToFetch) ? "youtube" : "url"
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
json(res, {
|
|
436
|
+
ok: true,
|
|
437
|
+
documentId: result.clientDocumentId,
|
|
438
|
+
fragmentCount: result.fragmentCount,
|
|
439
|
+
filename,
|
|
440
|
+
contentType,
|
|
441
|
+
isYouTubeTranscript: isYouTubeUrl(urlToFetch)
|
|
442
|
+
});
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
if (method === "GET" && pathname === "/api/knowledge/search") {
|
|
446
|
+
const query = url.searchParams.get("q");
|
|
447
|
+
if (!query?.trim()) {
|
|
448
|
+
error(res, "Search query (q) is required");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
const threshold = parseClampedFloat(url.searchParams.get("threshold"), {
|
|
452
|
+
fallback: .3,
|
|
453
|
+
min: 0,
|
|
454
|
+
max: 1
|
|
455
|
+
});
|
|
456
|
+
const limit = parsePositiveInteger(url.searchParams.get("limit"), 20);
|
|
457
|
+
const searchMessage = {
|
|
458
|
+
id: crypto.randomUUID(),
|
|
459
|
+
entityId: agentId,
|
|
460
|
+
agentId,
|
|
461
|
+
roomId: agentId,
|
|
462
|
+
content: { text: query.trim() },
|
|
463
|
+
createdAt: Date.now()
|
|
464
|
+
};
|
|
465
|
+
const filteredResults = (await knowledgeService.getKnowledge(searchMessage, { roomId: agentId })).filter((r) => (r.similarity ?? 0) >= threshold).slice(0, limit).map((r) => {
|
|
466
|
+
const meta = r.metadata;
|
|
467
|
+
return {
|
|
468
|
+
id: r.id,
|
|
469
|
+
text: r.content?.text || "",
|
|
470
|
+
similarity: r.similarity,
|
|
471
|
+
documentId: meta?.documentId,
|
|
472
|
+
documentTitle: meta?.filename || meta?.title || "",
|
|
473
|
+
position: meta?.position
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
json(res, {
|
|
477
|
+
query: query.trim(),
|
|
478
|
+
threshold,
|
|
479
|
+
results: filteredResults,
|
|
480
|
+
count: filteredResults.length
|
|
481
|
+
});
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
const fragmentsMatch = /^\/api\/knowledge\/fragments\/([^/]+)$/.exec(pathname);
|
|
485
|
+
if (method === "GET" && fragmentsMatch) {
|
|
486
|
+
const documentId = decodeURIComponent(fragmentsMatch[1]);
|
|
487
|
+
const allFragments = [];
|
|
488
|
+
let fragmentOffset = 0;
|
|
489
|
+
while (true) {
|
|
490
|
+
const fragmentBatch = await knowledgeService.getMemories({
|
|
491
|
+
tableName: "knowledge",
|
|
492
|
+
roomId: agentId,
|
|
493
|
+
count: FRAGMENT_COUNT_BATCH_SIZE,
|
|
494
|
+
offset: fragmentOffset
|
|
495
|
+
});
|
|
496
|
+
if (fragmentBatch.length === 0) break;
|
|
497
|
+
const matchingFragments = fragmentBatch.filter((fragment) => {
|
|
498
|
+
return fragment.metadata?.documentId === documentId;
|
|
499
|
+
});
|
|
500
|
+
for (const fragment of matchingFragments) {
|
|
501
|
+
if (!hasUuidIdAndCreatedAt(fragment)) continue;
|
|
502
|
+
const meta = fragment.metadata;
|
|
503
|
+
allFragments.push({
|
|
504
|
+
id: fragment.id,
|
|
505
|
+
text: fragment.content?.text || "",
|
|
506
|
+
position: meta?.position,
|
|
507
|
+
createdAt: fragment.createdAt
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (fragmentBatch.length < FRAGMENT_COUNT_BATCH_SIZE) break;
|
|
511
|
+
fragmentOffset += FRAGMENT_COUNT_BATCH_SIZE;
|
|
512
|
+
}
|
|
513
|
+
const documentFragments = allFragments.sort((a, b) => {
|
|
514
|
+
return (typeof a.position === "number" ? a.position : 0) - (typeof b.position === "number" ? b.position : 0);
|
|
515
|
+
}).map((f) => {
|
|
516
|
+
return {
|
|
517
|
+
id: f.id,
|
|
518
|
+
text: f.text,
|
|
519
|
+
position: f.position,
|
|
520
|
+
createdAt: f.createdAt
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
json(res, {
|
|
524
|
+
documentId,
|
|
525
|
+
fragments: documentFragments,
|
|
526
|
+
count: documentFragments.length
|
|
527
|
+
});
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
//#endregion
|
|
534
|
+
export { handleKnowledgeRoutes };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/api/memory-bounds.ts
|
|
2
|
+
/**
|
|
3
|
+
* Evict expired entries from a rate-limit map when it exceeds `threshold`.
|
|
4
|
+
* Safe to call during iteration (Map spec permits deletion during for-of).
|
|
5
|
+
*/
|
|
6
|
+
function sweepExpiredEntries(map, now, threshold) {
|
|
7
|
+
if (map.size <= threshold) return;
|
|
8
|
+
for (const [k, v] of map) if (now > v.resetAt) map.delete(k);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Evict the oldest conversation (by `updatedAt`) when the map exceeds `cap`.
|
|
12
|
+
* Returns the evicted key, or null if no eviction was needed.
|
|
13
|
+
*/
|
|
14
|
+
function evictOldestConversation(map, cap) {
|
|
15
|
+
if (map.size <= cap) return null;
|
|
16
|
+
let oldestKey = null;
|
|
17
|
+
let oldestTime = Infinity;
|
|
18
|
+
for (const [k, v] of map) {
|
|
19
|
+
const t = new Date(v.updatedAt).getTime();
|
|
20
|
+
if (t < oldestTime) {
|
|
21
|
+
oldestTime = t;
|
|
22
|
+
oldestKey = k;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (oldestKey) map.delete(oldestKey);
|
|
26
|
+
return oldestKey;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Push an entry to a bounded buffer and batch-evict when the high-water
|
|
30
|
+
* mark is reached. Returns the current buffer length.
|
|
31
|
+
*
|
|
32
|
+
* @param buffer - The array to push into.
|
|
33
|
+
* @param entry - The item to append.
|
|
34
|
+
* @param highWater - Trigger eviction when `buffer.length` exceeds this.
|
|
35
|
+
* @param evictCount - Number of oldest entries to remove on eviction.
|
|
36
|
+
*/
|
|
37
|
+
function pushWithBatchEvict(buffer, entry, highWater, evictCount) {
|
|
38
|
+
buffer.push(entry);
|
|
39
|
+
if (buffer.length > highWater) buffer.splice(0, evictCount);
|
|
40
|
+
return buffer.length;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Retrieve a file from a bounded cache, reading from disk on miss.
|
|
44
|
+
* Files larger than `fileSizeLimit` are never cached.
|
|
45
|
+
*
|
|
46
|
+
* @param cache - The Map serving as the LRU-ish cache.
|
|
47
|
+
* @param filePath - Absolute path to the file.
|
|
48
|
+
* @param mtimeMs - File's last-modified time (for invalidation).
|
|
49
|
+
* @param readFile - Callback that reads the file (injected for testing).
|
|
50
|
+
* @param maxEntries - Maximum number of cached files.
|
|
51
|
+
* @param fileSizeLimit - Maximum file size (bytes) eligible for caching.
|
|
52
|
+
*/
|
|
53
|
+
function getOrReadCachedFile(cache, filePath, mtimeMs, readFile, maxEntries, fileSizeLimit) {
|
|
54
|
+
const cached = cache.get(filePath);
|
|
55
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.body;
|
|
56
|
+
const body = readFile(filePath);
|
|
57
|
+
if (body.length <= fileSizeLimit) {
|
|
58
|
+
if (cache.size >= maxEntries) {
|
|
59
|
+
const firstKey = cache.keys().next().value;
|
|
60
|
+
if (firstKey !== void 0) cache.delete(firstKey);
|
|
61
|
+
}
|
|
62
|
+
cache.set(filePath, {
|
|
63
|
+
body,
|
|
64
|
+
mtimeMs
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { evictOldestConversation, getOrReadCachedFile, pushWithBatchEvict, sweepExpiredEntries };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/api/models-routes.ts
|
|
2
|
+
async function handleModelsRoutes(ctx) {
|
|
3
|
+
const { res, method, pathname, url, json, providerCachePath, getOrFetchProvider, getOrFetchAllProviders, resolveModelsCacheDir, pathExists, readDir, unlinkFile, joinPath } = ctx;
|
|
4
|
+
if (method !== "GET" || pathname !== "/api/models") return false;
|
|
5
|
+
const force = url.searchParams.get("refresh") === "true";
|
|
6
|
+
const specificProvider = url.searchParams.get("provider");
|
|
7
|
+
if (specificProvider) {
|
|
8
|
+
if (force) try {
|
|
9
|
+
unlinkFile(providerCachePath(specificProvider));
|
|
10
|
+
} catch {}
|
|
11
|
+
json(res, {
|
|
12
|
+
provider: specificProvider,
|
|
13
|
+
models: await getOrFetchProvider(specificProvider, force)
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (force) try {
|
|
18
|
+
const dir = resolveModelsCacheDir();
|
|
19
|
+
if (pathExists(dir)) {
|
|
20
|
+
for (const file of readDir(dir)) if (file.endsWith(".json")) unlinkFile(joinPath(dir, file));
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
json(res, { providers: await getOrFetchAllProviders(force) });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { handleModelsRoutes };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveStateDir } from "../config/paths.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/api/og-tracker.ts
|
|
7
|
+
/**
|
|
8
|
+
* OG tracking code system.
|
|
9
|
+
*
|
|
10
|
+
* Silently writes a unique identifier to ~/.milady/.og on first run.
|
|
11
|
+
* The code is a random UUID. A set of 100 "winning" codes can be
|
|
12
|
+
* generated deterministically from a secret seed (in MiladyMaker scripts).
|
|
13
|
+
* This file is planted now; whitelist eligibility is revealed in a future update.
|
|
14
|
+
*/
|
|
15
|
+
const OG_FILENAME = ".og";
|
|
16
|
+
/**
|
|
17
|
+
* Write a random tracking UUID to ~/.milady/.og if it does not already exist.
|
|
18
|
+
* Called once during startup. Silent on failure.
|
|
19
|
+
*/
|
|
20
|
+
function initializeOGCode() {
|
|
21
|
+
const dir = resolveStateDir();
|
|
22
|
+
const filePath = path.join(dir, OG_FILENAME);
|
|
23
|
+
if (fs.existsSync(filePath)) return;
|
|
24
|
+
const code = crypto.randomUUID();
|
|
25
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, {
|
|
26
|
+
recursive: true,
|
|
27
|
+
mode: 448
|
|
28
|
+
});
|
|
29
|
+
fs.writeFileSync(filePath, code, {
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
mode: 384
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { initializeOGCode };
|