waypoi 0.0.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/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- package/ui/vite.config.ts +37 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import {
|
|
3
|
+
listSessions,
|
|
4
|
+
getSession,
|
|
5
|
+
createSession,
|
|
6
|
+
updateSession,
|
|
7
|
+
deleteSession,
|
|
8
|
+
addMessage,
|
|
9
|
+
appendMessageContent,
|
|
10
|
+
} from "../storage/sessionRepository";
|
|
11
|
+
import { storeMedia, getMediaPath, getMediaEntry, getCacheStats, clearCache, ensureMediaCacheReady } from "../storage/imageCache";
|
|
12
|
+
import { resolveStoragePaths } from "../storage/files";
|
|
13
|
+
import { ChatMessage } from "../types";
|
|
14
|
+
import { promises as fs } from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { routeRequest } from "../routing/router";
|
|
17
|
+
import { pickBestModelByCapabilities } from "../storage/repositories";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sessions Routes
|
|
21
|
+
*
|
|
22
|
+
* REST API for managing chat sessions in the playground.
|
|
23
|
+
*
|
|
24
|
+
* Endpoints:
|
|
25
|
+
* GET /admin/sessions - List all sessions
|
|
26
|
+
* POST /admin/sessions - Create a new session
|
|
27
|
+
* GET /admin/sessions/:id - Get session by ID
|
|
28
|
+
* PUT /admin/sessions/:id - Update session metadata
|
|
29
|
+
* DELETE /admin/sessions/:id - Delete a session
|
|
30
|
+
* POST /admin/sessions/:id/messages - Add a message to session
|
|
31
|
+
* PATCH /admin/sessions/:id/messages/:msgIndex - Append to message (streaming)
|
|
32
|
+
*
|
|
33
|
+
* GET /admin/images/:hash - Get cached image by hash
|
|
34
|
+
* POST /admin/images - Store image in cache
|
|
35
|
+
* GET /admin/images/stats - Get image cache stats
|
|
36
|
+
* DELETE /admin/images - Clear image cache
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export async function registerSessionRoutes(app: FastifyInstance): Promise<void> {
|
|
40
|
+
const paths = resolveStoragePaths();
|
|
41
|
+
await ensureMediaCacheReady(paths);
|
|
42
|
+
app.log.info({ mediaRoot: path.join(paths.baseDir, "media") }, "Media cache initialized");
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Session CRUD
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
app.get("/admin/sessions", async (_req: FastifyRequest, reply: FastifyReply) => {
|
|
49
|
+
try {
|
|
50
|
+
const sessions = await listSessions(paths);
|
|
51
|
+
return reply.send({
|
|
52
|
+
object: "list",
|
|
53
|
+
data: sessions.map((s) => ({
|
|
54
|
+
id: s.id,
|
|
55
|
+
name: s.name,
|
|
56
|
+
model: s.model,
|
|
57
|
+
storageVersion: s.storageVersion,
|
|
58
|
+
titleStatus: s.titleStatus,
|
|
59
|
+
titleUpdatedAt: s.titleUpdatedAt,
|
|
60
|
+
messageCount: s.messages.length,
|
|
61
|
+
createdAt: s.createdAt,
|
|
62
|
+
updatedAt: s.updatedAt,
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
app.log.error(error, "Failed to list sessions");
|
|
67
|
+
return reply.status(500).send({
|
|
68
|
+
error: { message: "Failed to list sessions", type: "internal_error" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post(
|
|
74
|
+
"/admin/sessions",
|
|
75
|
+
async (
|
|
76
|
+
req: FastifyRequest<{ Body: { name?: string; model?: string } }>,
|
|
77
|
+
reply: FastifyReply
|
|
78
|
+
) => {
|
|
79
|
+
try {
|
|
80
|
+
const { name, model } = req.body || {};
|
|
81
|
+
const session = await createSession(paths, { name, model });
|
|
82
|
+
return reply.status(201).send(toApiSession(session));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
app.log.error(error, "Failed to create session");
|
|
85
|
+
return reply.status(500).send({
|
|
86
|
+
error: { message: "Failed to create session", type: "internal_error" },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
app.post(
|
|
93
|
+
"/admin/sessions/:id/auto-title",
|
|
94
|
+
async (
|
|
95
|
+
req: FastifyRequest<{ Params: { id: string }; Body: { model?: string; seedText?: string } }>,
|
|
96
|
+
reply: FastifyReply
|
|
97
|
+
) => {
|
|
98
|
+
try {
|
|
99
|
+
const session = await getSession(paths, req.params.id);
|
|
100
|
+
if (!session) {
|
|
101
|
+
return reply.status(404).send({
|
|
102
|
+
error: { message: "Session not found", type: "not_found" },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const seedText =
|
|
107
|
+
req.body?.seedText?.trim() || extractSeedTextFromSession(session.messages);
|
|
108
|
+
if (!seedText) {
|
|
109
|
+
return reply.status(400).send({
|
|
110
|
+
error: { message: "seedText is required for auto-title", type: "invalid_request" },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const model =
|
|
115
|
+
req.body?.model ||
|
|
116
|
+
session.model ||
|
|
117
|
+
(await pickBestModelByCapabilities(
|
|
118
|
+
paths,
|
|
119
|
+
{ requiredInput: ["text"], requiredOutput: ["text"] },
|
|
120
|
+
"llm"
|
|
121
|
+
));
|
|
122
|
+
|
|
123
|
+
let generated = false;
|
|
124
|
+
let title = fallbackTitleFromSeed(seedText);
|
|
125
|
+
|
|
126
|
+
if (model) {
|
|
127
|
+
try {
|
|
128
|
+
title = await generateTitleFromModel(paths, model, seedText);
|
|
129
|
+
generated = true;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
app.log.warn(error, "Auto-title generation failed, using fallback title");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const updated = await updateSession(paths, req.params.id, {
|
|
136
|
+
name: title,
|
|
137
|
+
titleStatus: generated ? "generated" : "failed",
|
|
138
|
+
titleUpdatedAt: new Date(),
|
|
139
|
+
});
|
|
140
|
+
if (!updated) {
|
|
141
|
+
return reply.status(404).send({
|
|
142
|
+
error: { message: "Session not found", type: "not_found" },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return reply.send({
|
|
146
|
+
id: updated.id,
|
|
147
|
+
name: updated.name,
|
|
148
|
+
titleStatus: updated.titleStatus,
|
|
149
|
+
titleUpdatedAt: updated.titleUpdatedAt,
|
|
150
|
+
generated,
|
|
151
|
+
model,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
app.log.error(error, "Failed to auto-title session");
|
|
155
|
+
return reply.status(500).send({
|
|
156
|
+
error: { message: "Failed to auto-title session", type: "internal_error" },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
app.get(
|
|
163
|
+
"/admin/sessions/:id",
|
|
164
|
+
async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
|
165
|
+
try {
|
|
166
|
+
const session = await getSession(paths, req.params.id);
|
|
167
|
+
if (!session) {
|
|
168
|
+
return reply.status(404).send({
|
|
169
|
+
error: { message: "Session not found", type: "not_found" },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return reply.send(toApiSession(session));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
app.log.error(error, "Failed to get session");
|
|
175
|
+
return reply.status(500).send({
|
|
176
|
+
error: { message: "Failed to get session", type: "internal_error" },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
app.put(
|
|
183
|
+
"/admin/sessions/:id",
|
|
184
|
+
async (
|
|
185
|
+
req: FastifyRequest<{ Params: { id: string }; Body: { name?: string; model?: string } }>,
|
|
186
|
+
reply: FastifyReply
|
|
187
|
+
) => {
|
|
188
|
+
try {
|
|
189
|
+
const updates: Partial<{ name: string; model: string }> = {};
|
|
190
|
+
if (req.body?.name !== undefined) updates.name = req.body.name;
|
|
191
|
+
if (req.body?.model !== undefined) updates.model = req.body.model;
|
|
192
|
+
|
|
193
|
+
const session = await updateSession(paths, req.params.id, updates);
|
|
194
|
+
if (!session) {
|
|
195
|
+
return reply.status(404).send({
|
|
196
|
+
error: { message: "Session not found", type: "not_found" },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return reply.send(toApiSession(session));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
app.log.error(error, "Failed to update session");
|
|
202
|
+
return reply.status(500).send({
|
|
203
|
+
error: { message: "Failed to update session", type: "internal_error" },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
app.delete(
|
|
210
|
+
"/admin/sessions/:id",
|
|
211
|
+
async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
|
212
|
+
try {
|
|
213
|
+
const deleted = await deleteSession(paths, req.params.id);
|
|
214
|
+
if (!deleted) {
|
|
215
|
+
return reply.status(404).send({
|
|
216
|
+
error: { message: "Session not found", type: "not_found" },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return reply.status(204).send();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
app.log.error(error, "Failed to delete session");
|
|
222
|
+
return reply.status(500).send({
|
|
223
|
+
error: { message: "Failed to delete session", type: "internal_error" },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
// Message management
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
app.post(
|
|
234
|
+
"/admin/sessions/:id/messages",
|
|
235
|
+
async (
|
|
236
|
+
req: FastifyRequest<{ Params: { id: string }; Body: IncomingChatMessage }>,
|
|
237
|
+
reply: FastifyReply
|
|
238
|
+
) => {
|
|
239
|
+
try {
|
|
240
|
+
const message = await addMessage(paths, req.params.id, normalizeIncomingMessage(req.body));
|
|
241
|
+
|
|
242
|
+
if (!message) {
|
|
243
|
+
return reply.status(404).send({
|
|
244
|
+
error: { message: "Session not found", type: "not_found" },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return reply.status(201).send({
|
|
248
|
+
messageId: message.id,
|
|
249
|
+
createdAt: message.createdAt,
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
app.log.error(error, "Failed to add message");
|
|
253
|
+
return reply.status(500).send({
|
|
254
|
+
error: { message: "Failed to add message", type: "internal_error" },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
app.patch(
|
|
261
|
+
"/admin/sessions/:id/messages/:messageId",
|
|
262
|
+
async (
|
|
263
|
+
req: FastifyRequest<{
|
|
264
|
+
Params: { id: string; messageId: string };
|
|
265
|
+
Body: { content: string };
|
|
266
|
+
}>,
|
|
267
|
+
reply: FastifyReply
|
|
268
|
+
) => {
|
|
269
|
+
try {
|
|
270
|
+
const success = await appendMessageContent(
|
|
271
|
+
paths,
|
|
272
|
+
req.params.id,
|
|
273
|
+
req.params.messageId,
|
|
274
|
+
req.body?.content || ""
|
|
275
|
+
);
|
|
276
|
+
if (!success) {
|
|
277
|
+
return reply.status(404).send({
|
|
278
|
+
error: { message: "Session or message not found", type: "not_found" },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return reply.send({ success: true });
|
|
282
|
+
} catch (error) {
|
|
283
|
+
app.log.error(error, "Failed to append to message");
|
|
284
|
+
return reply.status(500).send({
|
|
285
|
+
error: { message: "Failed to append to message", type: "internal_error" },
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
292
|
+
// Media cache (/admin/media) with image alias compatibility (/admin/images)
|
|
293
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
294
|
+
registerMediaCacheRoutes("/admin/media");
|
|
295
|
+
registerMediaCacheRoutes("/admin/images");
|
|
296
|
+
|
|
297
|
+
function registerMediaCacheRoutes(prefix: "/admin/media" | "/admin/images"): void {
|
|
298
|
+
app.get(`${prefix}/stats`, async (_req: FastifyRequest, reply: FastifyReply) => {
|
|
299
|
+
try {
|
|
300
|
+
const stats = await getCacheStats(paths);
|
|
301
|
+
return reply.send(stats);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
app.log.error(error, "Failed to get media cache stats");
|
|
304
|
+
return reply.status(500).send({
|
|
305
|
+
error: { message: "Failed to get cache stats", type: "internal_error" },
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
app.get(
|
|
311
|
+
`${prefix}/:hash`,
|
|
312
|
+
async (req: FastifyRequest<{ Params: { hash: string } }>, reply: FastifyReply) => {
|
|
313
|
+
try {
|
|
314
|
+
const filePath = await getMediaPath(paths, req.params.hash);
|
|
315
|
+
const entry = await getMediaEntry(paths, req.params.hash);
|
|
316
|
+
if (!filePath || !entry) {
|
|
317
|
+
return reply.status(404).send({
|
|
318
|
+
error: { message: "Media not found", type: "not_found" },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const buffer = await fs.readFile(filePath);
|
|
323
|
+
return reply
|
|
324
|
+
.header("Content-Type", entry.mimeType || guessMimeType(filePath))
|
|
325
|
+
.header("Cache-Control", "public, max-age=31536000, immutable")
|
|
326
|
+
.send(buffer);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
app.log.error(error, "Failed to get media");
|
|
329
|
+
return reply.status(500).send({
|
|
330
|
+
error: { message: "Failed to get media", type: "internal_error" },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
app.post(
|
|
337
|
+
`${prefix}`,
|
|
338
|
+
async (
|
|
339
|
+
req: FastifyRequest<{ Body: { data: string; model?: string; mimeType?: string } }>,
|
|
340
|
+
reply: FastifyReply
|
|
341
|
+
) => {
|
|
342
|
+
try {
|
|
343
|
+
if (!req.body?.data) {
|
|
344
|
+
return reply.status(400).send({
|
|
345
|
+
error: { message: "Missing media data", type: "invalid_request" },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = await storeMedia(paths, req.body.data, {
|
|
350
|
+
model: req.body.model,
|
|
351
|
+
mimeType: req.body.mimeType,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return reply.status(201).send({
|
|
355
|
+
hash: result.hash,
|
|
356
|
+
mimeType: result.mimeType,
|
|
357
|
+
url: `${prefix}/${result.hash}`,
|
|
358
|
+
evicted: result.evicted,
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
app.log.error(error, "Failed to store media");
|
|
362
|
+
return reply.status(500).send({
|
|
363
|
+
error: { message: "Failed to store media", type: "internal_error" },
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
app.delete(`${prefix}`, async (_req: FastifyRequest, reply: FastifyReply) => {
|
|
370
|
+
try {
|
|
371
|
+
const deleted = await clearCache(paths);
|
|
372
|
+
return reply.send({ deleted });
|
|
373
|
+
} catch (error) {
|
|
374
|
+
app.log.error(error, "Failed to clear media cache");
|
|
375
|
+
return reply.status(500).send({
|
|
376
|
+
error: { message: "Failed to clear cache", type: "internal_error" },
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
type IncomingChatMessage = Partial<ChatMessage> & { timestamp?: string };
|
|
384
|
+
|
|
385
|
+
function normalizeIncomingMessage(body: IncomingChatMessage): Omit<ChatMessage, "id" | "createdAt"> {
|
|
386
|
+
return {
|
|
387
|
+
role: (body.role as ChatMessage["role"]) ?? "user",
|
|
388
|
+
content: body.content ?? "",
|
|
389
|
+
name: body.name,
|
|
390
|
+
tool_calls: body.tool_calls,
|
|
391
|
+
tool_call_id: body.tool_call_id,
|
|
392
|
+
images: body.images,
|
|
393
|
+
model: body.model,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function toApiSession(session: {
|
|
398
|
+
id: string;
|
|
399
|
+
name: string;
|
|
400
|
+
model?: string;
|
|
401
|
+
titleStatus?: "pending" | "generated" | "manual" | "failed";
|
|
402
|
+
titleUpdatedAt?: Date;
|
|
403
|
+
storageVersion: number;
|
|
404
|
+
messages: ChatMessage[];
|
|
405
|
+
createdAt: Date;
|
|
406
|
+
updatedAt: Date;
|
|
407
|
+
}) {
|
|
408
|
+
return {
|
|
409
|
+
...session,
|
|
410
|
+
messages: session.messages.map((message) => ({
|
|
411
|
+
...message,
|
|
412
|
+
timestamp: message.createdAt,
|
|
413
|
+
})),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function guessMimeType(filePath: string): string {
|
|
418
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
419
|
+
const mimeTypes: Record<string, string> = {
|
|
420
|
+
png: "image/png",
|
|
421
|
+
jpg: "image/jpeg",
|
|
422
|
+
jpeg: "image/jpeg",
|
|
423
|
+
gif: "image/gif",
|
|
424
|
+
webp: "image/webp",
|
|
425
|
+
wav: "audio/wav",
|
|
426
|
+
mp3: "audio/mpeg",
|
|
427
|
+
ogg: "audio/ogg",
|
|
428
|
+
webm: "audio/webm",
|
|
429
|
+
m4a: "audio/mp4",
|
|
430
|
+
};
|
|
431
|
+
return mimeTypes[ext] ?? "application/octet-stream";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function extractSeedTextFromSession(messages: ChatMessage[]): string {
|
|
435
|
+
const firstUserMessage = messages.find((message) => message.role === "user");
|
|
436
|
+
if (!firstUserMessage) {
|
|
437
|
+
return "";
|
|
438
|
+
}
|
|
439
|
+
return textFromContent(firstUserMessage.content);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function textFromContent(content: ChatMessage["content"]): string {
|
|
443
|
+
if (typeof content === "string") {
|
|
444
|
+
return content.trim();
|
|
445
|
+
}
|
|
446
|
+
if (!Array.isArray(content)) {
|
|
447
|
+
return "";
|
|
448
|
+
}
|
|
449
|
+
const parts: string[] = [];
|
|
450
|
+
for (const part of content) {
|
|
451
|
+
if (part.type === "text") {
|
|
452
|
+
parts.push(part.text);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return parts.join(" ").trim();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function fallbackTitleFromSeed(seed: string): string {
|
|
459
|
+
const cleaned = sanitizeTitle(seed);
|
|
460
|
+
if (!cleaned) {
|
|
461
|
+
return "New Session";
|
|
462
|
+
}
|
|
463
|
+
const words = cleaned.split(/\s+/).slice(0, 7).join(" ");
|
|
464
|
+
return words.length > 60 ? words.slice(0, 60).trim() : words;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function generateTitleFromModel(paths: ReturnType<typeof resolveStoragePaths>, model: string, seedText: string): Promise<string> {
|
|
468
|
+
const prompt = [
|
|
469
|
+
"Generate a short title for this chat.",
|
|
470
|
+
"Rules: 3-7 words, sentence case, no quotes, no punctuation at end.",
|
|
471
|
+
"Return only the title text.",
|
|
472
|
+
`Chat seed: ${seedText}`,
|
|
473
|
+
].join("\n");
|
|
474
|
+
|
|
475
|
+
const payload: Record<string, unknown> = {
|
|
476
|
+
model,
|
|
477
|
+
stream: false,
|
|
478
|
+
temperature: 0,
|
|
479
|
+
max_tokens: 32,
|
|
480
|
+
messages: [
|
|
481
|
+
{
|
|
482
|
+
role: "system",
|
|
483
|
+
content: "You write concise conversation titles.",
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
role: "user",
|
|
487
|
+
content: prompt,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const outcome = await routeRequest(
|
|
493
|
+
paths,
|
|
494
|
+
model,
|
|
495
|
+
"/v1/chat/completions",
|
|
496
|
+
payload,
|
|
497
|
+
{},
|
|
498
|
+
AbortSignal.timeout(15_000),
|
|
499
|
+
{
|
|
500
|
+
requiredInput: ["text"],
|
|
501
|
+
requiredOutput: ["text"],
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const payloadJson = await readJsonBody(outcome.attempt.response.body);
|
|
506
|
+
const text = extractAssistantText(payloadJson);
|
|
507
|
+
const sanitized = sanitizeTitle(text);
|
|
508
|
+
if (!sanitized) {
|
|
509
|
+
throw new Error("Model did not return a valid title");
|
|
510
|
+
}
|
|
511
|
+
return sanitized;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function readJsonBody(stream: NodeJS.ReadableStream): Promise<unknown> {
|
|
515
|
+
const chunks: Buffer[] = [];
|
|
516
|
+
for await (const chunk of stream) {
|
|
517
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
518
|
+
}
|
|
519
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
520
|
+
return JSON.parse(text);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function extractAssistantText(payload: unknown): string {
|
|
524
|
+
if (!payload || typeof payload !== "object") {
|
|
525
|
+
return "";
|
|
526
|
+
}
|
|
527
|
+
const choices = (payload as { choices?: Array<{ message?: { content?: unknown } }> }).choices;
|
|
528
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
529
|
+
return "";
|
|
530
|
+
}
|
|
531
|
+
const content = choices[0]?.message?.content;
|
|
532
|
+
if (typeof content === "string") {
|
|
533
|
+
return content;
|
|
534
|
+
}
|
|
535
|
+
if (!Array.isArray(content)) {
|
|
536
|
+
return "";
|
|
537
|
+
}
|
|
538
|
+
const parts: string[] = [];
|
|
539
|
+
for (const part of content) {
|
|
540
|
+
if (part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string") {
|
|
541
|
+
parts.push((part as { text: string }).text);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return parts.join(" ");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function sanitizeTitle(input: string): string {
|
|
548
|
+
const normalized = input
|
|
549
|
+
.replace(/[\r\n]+/g, " ")
|
|
550
|
+
.replace(/["'`]+/g, "")
|
|
551
|
+
.replace(/\s+/g, " ")
|
|
552
|
+
.trim();
|
|
553
|
+
const trimmed = normalized.replace(/[.,;:!?]+$/g, "").trim();
|
|
554
|
+
if (!trimmed) {
|
|
555
|
+
return "";
|
|
556
|
+
}
|
|
557
|
+
return trimmed.length > 60 ? trimmed.slice(0, 60).trim() : trimmed;
|
|
558
|
+
}
|