@openspecui/server 3.10.0 → 3.11.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/dist/index.mjs +228 -6
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BatchTranslateInputSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, LocalModelAssetStateSchema, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, ServiceTranslationEngineIdSchema, TRANSLATION_ENGINE_MANIFESTS, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, TranslationEngineIdSchema, TranslationLocalSettingsSchema, TranslationOpenAISettingsSchema, buildBackendHealthPayload, buildLocalDownloadPlanFromRepositoryFiles, clearCache, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, selectLocalDownloadGroup, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
2
|
-
import { basename, dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { BatchTranslateInputSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, LocalModelAssetStateSchema, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, ServiceTranslationEngineIdSchema, TRANSLATION_ENGINE_MANIFESTS, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, TranslationEngineIdSchema, TranslationLocalSettingsSchema, TranslationOpenAISettingsSchema, buildBackendHealthPayload, buildLocalDownloadPlanFromRepositoryFiles, clearCache, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getOpsxEntityRootRelativePath, getToolInitStates, getWatcherRuntimeStatus, inferFileMime, inferFilePreviewKind, initWatcherPool, isWatcherPoolInitialized, normalizeOpsxEntityPath, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, selectLocalDownloadGroup, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
2
|
+
import { basename, dirname, extname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { access, copyFile, lstat, mkdir, open, readFile, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createServer as createServer$1 } from "node:net";
|
|
@@ -8,7 +8,7 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
|
8
8
|
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import { cors } from "hono/cors";
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { WebSocketServer } from "ws";
|
|
13
13
|
import { CustomSoundHashSchema as CustomSoundHashSchema$1, CustomSoundIdSchema, CustomSoundMetadataFileSchema, customHashFromSoundId, soundIdFromCustomHash } from "@openspecui/core/sounds";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
@@ -1450,6 +1450,160 @@ async function buildEntityReadOptions(ctx, stage, id) {
|
|
|
1450
1450
|
}
|
|
1451
1451
|
}
|
|
1452
1452
|
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/entity-file-paths.ts
|
|
1455
|
+
function ensureInsideRoot(rootPath, candidatePath) {
|
|
1456
|
+
if (candidatePath === rootPath) return;
|
|
1457
|
+
if (!candidatePath.startsWith(rootPath + "/")) throw new Error("Resolved path escaped entity root.");
|
|
1458
|
+
}
|
|
1459
|
+
function getEntityRootPath(projectDir, stage, changeId) {
|
|
1460
|
+
return resolve(projectDir, getOpsxEntityRootRelativePath(stage, changeId));
|
|
1461
|
+
}
|
|
1462
|
+
function resolveEntityEntryPath(input) {
|
|
1463
|
+
const relativePath$1 = normalizeOpsxEntityPath(input.path);
|
|
1464
|
+
if (!relativePath$1) throw new Error("path is required");
|
|
1465
|
+
const entityRoot = getEntityRootPath(input.projectDir, input.stage, input.changeId);
|
|
1466
|
+
const absolutePath = resolve(entityRoot, relativePath$1);
|
|
1467
|
+
ensureInsideRoot(entityRoot, absolutePath);
|
|
1468
|
+
return {
|
|
1469
|
+
entityRoot,
|
|
1470
|
+
relativePath: relativePath$1,
|
|
1471
|
+
absolutePath
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/file-preview-service.ts
|
|
1477
|
+
const PREVIEW_ENTRY_FILE_BY_KIND = {
|
|
1478
|
+
image: "image-preview.html",
|
|
1479
|
+
audio: "audio-preview.html",
|
|
1480
|
+
video: "video-preview.html",
|
|
1481
|
+
pdf: "pdf-preview.html"
|
|
1482
|
+
};
|
|
1483
|
+
const SESSION_PREVIEW_KINDS = new Set([
|
|
1484
|
+
"html",
|
|
1485
|
+
"image",
|
|
1486
|
+
"audio",
|
|
1487
|
+
"video",
|
|
1488
|
+
"pdf"
|
|
1489
|
+
]);
|
|
1490
|
+
function isSessionPreviewKind(previewKind) {
|
|
1491
|
+
return SESSION_PREVIEW_KINDS.has(previewKind);
|
|
1492
|
+
}
|
|
1493
|
+
function toHash(input) {
|
|
1494
|
+
return createHash("sha256").update(input).digest("hex");
|
|
1495
|
+
}
|
|
1496
|
+
function stripLeadingSlash(path) {
|
|
1497
|
+
return path.replace(/^\/+/, "");
|
|
1498
|
+
}
|
|
1499
|
+
function inferPreviewAssetContentType(path) {
|
|
1500
|
+
switch (extname(path).toLowerCase()) {
|
|
1501
|
+
case ".html": return "text/html";
|
|
1502
|
+
case ".js":
|
|
1503
|
+
case ".mjs": return "application/javascript";
|
|
1504
|
+
case ".css": return "text/css";
|
|
1505
|
+
case ".json": return "application/json";
|
|
1506
|
+
case ".svg": return "image/svg+xml";
|
|
1507
|
+
case ".png": return "image/png";
|
|
1508
|
+
case ".jpg":
|
|
1509
|
+
case ".jpeg": return "image/jpeg";
|
|
1510
|
+
case ".woff": return "font/woff";
|
|
1511
|
+
case ".woff2": return "font/woff2";
|
|
1512
|
+
default: return inferFileMime(path) ?? "application/octet-stream";
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function isRewritablePreviewAsset(path) {
|
|
1516
|
+
const extension = extname(path).toLowerCase();
|
|
1517
|
+
return extension === ".html" || extension === ".js" || extension === ".mjs" || extension === ".css";
|
|
1518
|
+
}
|
|
1519
|
+
function rewritePreviewAssetPaths(content, hash) {
|
|
1520
|
+
const sessionAssetPrefix = `/api/file-preview/${hash}/assets/`;
|
|
1521
|
+
return content.replaceAll("/assets/", sessionAssetPrefix);
|
|
1522
|
+
}
|
|
1523
|
+
var FilePreviewService = class {
|
|
1524
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1525
|
+
constructor(projectDir, previewAssetsDir) {
|
|
1526
|
+
this.projectDir = projectDir;
|
|
1527
|
+
this.previewAssetsDir = previewAssetsDir;
|
|
1528
|
+
}
|
|
1529
|
+
prepareEntityFilePreview(input) {
|
|
1530
|
+
const resolved = resolveEntityEntryPath({
|
|
1531
|
+
projectDir: this.projectDir,
|
|
1532
|
+
stage: input.stage,
|
|
1533
|
+
changeId: input.changeId,
|
|
1534
|
+
path: input.path
|
|
1535
|
+
});
|
|
1536
|
+
if (!statSync(resolved.absolutePath, { throwIfNoEntry: false })?.isFile()) throw new Error("Preview target file not found.");
|
|
1537
|
+
const mime = inferFileMime(resolved.relativePath);
|
|
1538
|
+
if (!mime) throw new Error("Preview target mime is unknown.");
|
|
1539
|
+
const previewKind = inferFilePreviewKind(resolved.relativePath, mime);
|
|
1540
|
+
if (!isSessionPreviewKind(previewKind)) throw new Error("Preview route is not supported for this file type.");
|
|
1541
|
+
const directoryPath = resolve(resolved.absolutePath, "..");
|
|
1542
|
+
const hash = toHash(`${directoryPath}:${mime}`);
|
|
1543
|
+
const entryFileName = previewKind === "html" ? null : PREVIEW_ENTRY_FILE_BY_KIND[previewKind];
|
|
1544
|
+
const fileName = basename(resolved.absolutePath);
|
|
1545
|
+
this.sessions.set(hash, {
|
|
1546
|
+
hash,
|
|
1547
|
+
directoryPath,
|
|
1548
|
+
mime,
|
|
1549
|
+
previewKind,
|
|
1550
|
+
entryFileName
|
|
1551
|
+
});
|
|
1552
|
+
const htmlPathname = `/api/file-preview/${hash}/${fileName}`;
|
|
1553
|
+
const resourcePathname = previewKind === "html" ? null : `/api/file-preview/${hash}/resource/${fileName}`;
|
|
1554
|
+
const entryPathname = previewKind === "html" ? htmlPathname : `/api/file-preview/${hash}/${entryFileName}`;
|
|
1555
|
+
return {
|
|
1556
|
+
hash,
|
|
1557
|
+
mime,
|
|
1558
|
+
previewKind,
|
|
1559
|
+
relativePath: resolved.relativePath,
|
|
1560
|
+
resourcePathname,
|
|
1561
|
+
entryPathname,
|
|
1562
|
+
urlPath: previewKind === "html" ? htmlPathname : `${entryPathname}?file=${encodeURIComponent(fileName)}`
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
readPreviewRequest(hash, requestPath) {
|
|
1566
|
+
const session = this.sessions.get(hash);
|
|
1567
|
+
if (!session) return null;
|
|
1568
|
+
const normalized = stripLeadingSlash(requestPath);
|
|
1569
|
+
if (session.previewKind === "html") {
|
|
1570
|
+
const absolutePath$1 = resolve(session.directoryPath, normalized);
|
|
1571
|
+
if (!absolutePath$1.startsWith(session.directoryPath + "/")) return null;
|
|
1572
|
+
if (!existsSync(absolutePath$1) || !statSync(absolutePath$1).isFile()) return null;
|
|
1573
|
+
return {
|
|
1574
|
+
content: readFileSync(absolutePath$1),
|
|
1575
|
+
contentType: inferFileMime(absolutePath$1) ?? "application/octet-stream"
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
if (normalized.startsWith("resource/")) {
|
|
1579
|
+
const resourcePath = normalized.slice(9);
|
|
1580
|
+
const absolutePath$1 = resolve(session.directoryPath, resourcePath);
|
|
1581
|
+
if (!absolutePath$1.startsWith(session.directoryPath + "/")) return null;
|
|
1582
|
+
if (!existsSync(absolutePath$1) || !statSync(absolutePath$1).isFile()) return null;
|
|
1583
|
+
return {
|
|
1584
|
+
content: readFileSync(absolutePath$1),
|
|
1585
|
+
contentType: inferFileMime(absolutePath$1) ?? "application/octet-stream"
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
const assetName = normalized || session.entryFileName;
|
|
1589
|
+
if (!assetName) return null;
|
|
1590
|
+
const absolutePath = resolve(this.previewAssetsDir, assetName);
|
|
1591
|
+
if (!absolutePath.startsWith(resolve(this.previewAssetsDir) + "/")) return null;
|
|
1592
|
+
if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) return null;
|
|
1593
|
+
if (isRewritablePreviewAsset(assetName)) {
|
|
1594
|
+
const rewritten = rewritePreviewAssetPaths(readFileSync(absolutePath, "utf8"), hash);
|
|
1595
|
+
return {
|
|
1596
|
+
content: Buffer.from(rewritten, "utf8"),
|
|
1597
|
+
contentType: inferPreviewAssetContentType(absolutePath)
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
content: readFileSync(absolutePath),
|
|
1602
|
+
contentType: inferPreviewAssetContentType(absolutePath)
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1453
1607
|
//#endregion
|
|
1454
1608
|
//#region src/huggingface-endpoint.ts
|
|
1455
1609
|
const DEFAULT_HUGGING_FACE_ENDPOINT = "https://huggingface.co";
|
|
@@ -5290,6 +5444,31 @@ const changeRouter = router({
|
|
|
5290
5444
|
}),
|
|
5291
5445
|
subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
5292
5446
|
return createReactiveSubscriptionWithInput((id) => ctx.adapter.readChangeFiles(id))(input.id);
|
|
5447
|
+
}),
|
|
5448
|
+
writeFile: publicProcedure.input(z.object({
|
|
5449
|
+
id: z.string(),
|
|
5450
|
+
path: z.string(),
|
|
5451
|
+
content: z.string()
|
|
5452
|
+
})).mutation(async ({ ctx, input }) => {
|
|
5453
|
+
const info = resolveEntityEntryPath({
|
|
5454
|
+
projectDir: ctx.projectDir,
|
|
5455
|
+
stage: "change",
|
|
5456
|
+
changeId: input.id,
|
|
5457
|
+
path: input.path
|
|
5458
|
+
});
|
|
5459
|
+
await mkdir(dirname(info.absolutePath), { recursive: true });
|
|
5460
|
+
await writeFile(info.absolutePath, input.content, "utf-8");
|
|
5461
|
+
return { success: true };
|
|
5462
|
+
}),
|
|
5463
|
+
prepareFilePreview: publicProcedure.input(z.object({
|
|
5464
|
+
id: z.string(),
|
|
5465
|
+
path: z.string()
|
|
5466
|
+
})).query(({ ctx, input }) => {
|
|
5467
|
+
return ctx.filePreviewService.prepareEntityFilePreview({
|
|
5468
|
+
stage: "change",
|
|
5469
|
+
changeId: input.id,
|
|
5470
|
+
path: input.path
|
|
5471
|
+
});
|
|
5293
5472
|
})
|
|
5294
5473
|
});
|
|
5295
5474
|
/**
|
|
@@ -5322,9 +5501,32 @@ const archiveRouter = router({
|
|
|
5322
5501
|
return createReactiveSubscriptionWithInput(async (id) => ctx.documentService.readEntityDetail("archive", id, "view", "processed", await buildEntityReadOptions(ctx, "archive", id)))(input.id);
|
|
5323
5502
|
}),
|
|
5324
5503
|
subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
5325
|
-
return createReactiveSubscriptionWithInput(
|
|
5326
|
-
|
|
5327
|
-
|
|
5504
|
+
return createReactiveSubscriptionWithInput((id) => ctx.documentService.readArchivedChangeFiles(id, "view", "source"))(input.id);
|
|
5505
|
+
}),
|
|
5506
|
+
writeFile: publicProcedure.input(z.object({
|
|
5507
|
+
id: z.string(),
|
|
5508
|
+
path: z.string(),
|
|
5509
|
+
content: z.string()
|
|
5510
|
+
})).mutation(async ({ ctx, input }) => {
|
|
5511
|
+
const info = resolveEntityEntryPath({
|
|
5512
|
+
projectDir: ctx.projectDir,
|
|
5513
|
+
stage: "archive",
|
|
5514
|
+
changeId: input.id,
|
|
5515
|
+
path: input.path
|
|
5516
|
+
});
|
|
5517
|
+
await mkdir(dirname(info.absolutePath), { recursive: true });
|
|
5518
|
+
await writeFile(info.absolutePath, input.content, "utf-8");
|
|
5519
|
+
return { success: true };
|
|
5520
|
+
}),
|
|
5521
|
+
prepareFilePreview: publicProcedure.input(z.object({
|
|
5522
|
+
id: z.string(),
|
|
5523
|
+
path: z.string()
|
|
5524
|
+
})).query(({ ctx, input }) => {
|
|
5525
|
+
return ctx.filePreviewService.prepareEntityFilePreview({
|
|
5526
|
+
stage: "archive",
|
|
5527
|
+
changeId: input.id,
|
|
5528
|
+
path: input.path
|
|
5529
|
+
});
|
|
5328
5530
|
})
|
|
5329
5531
|
});
|
|
5330
5532
|
z.object({
|
|
@@ -6940,6 +7142,7 @@ function createServer(config) {
|
|
|
6940
7142
|
const kernel = config.kernel;
|
|
6941
7143
|
const hookRuntime = createHookRuntime(config.projectDir);
|
|
6942
7144
|
const documentService = new DocumentService(config.projectDir, adapter, hookRuntime);
|
|
7145
|
+
const filePreviewService = new FilePreviewService(config.projectDir, config.previewAssetsDir ?? join(__dirname, "..", "..", "web", "dist"));
|
|
6943
7146
|
const workflowInvocationService = new WorkflowInvocationService({
|
|
6944
7147
|
projectDir: config.projectDir,
|
|
6945
7148
|
hookRuntime,
|
|
@@ -7054,6 +7257,21 @@ function createServer(config) {
|
|
|
7054
7257
|
"Cache-Control": "private, max-age=31536000, immutable"
|
|
7055
7258
|
} });
|
|
7056
7259
|
});
|
|
7260
|
+
app.get("/api/file-preview/:hash/*", async (c) => {
|
|
7261
|
+
const hash = c.req.param("hash");
|
|
7262
|
+
const prefix = `/api/file-preview/${hash}/`;
|
|
7263
|
+
const requestPath = c.req.path.startsWith(prefix) ? c.req.path.slice(prefix.length) : "";
|
|
7264
|
+
const asset = filePreviewService.readPreviewRequest(hash, requestPath);
|
|
7265
|
+
if (!asset) return c.notFound();
|
|
7266
|
+
const stream = new ReadableStream({ start(controller) {
|
|
7267
|
+
controller.enqueue(new Uint8Array(asset.content));
|
|
7268
|
+
controller.close();
|
|
7269
|
+
} });
|
|
7270
|
+
return new Response(stream, {
|
|
7271
|
+
status: 200,
|
|
7272
|
+
headers: { "Content-Type": asset.contentType }
|
|
7273
|
+
});
|
|
7274
|
+
});
|
|
7057
7275
|
app.use("/trpc/*", async (c) => {
|
|
7058
7276
|
return await fetchRequestHandler({
|
|
7059
7277
|
endpoint: "/trpc",
|
|
@@ -7073,6 +7291,7 @@ function createServer(config) {
|
|
|
7073
7291
|
customSoundService,
|
|
7074
7292
|
globalSettingsManager,
|
|
7075
7293
|
translationCacheService,
|
|
7294
|
+
filePreviewService,
|
|
7076
7295
|
translationEngineService,
|
|
7077
7296
|
localModelAssetService,
|
|
7078
7297
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
@@ -7095,6 +7314,7 @@ function createServer(config) {
|
|
|
7095
7314
|
customSoundService,
|
|
7096
7315
|
globalSettingsManager,
|
|
7097
7316
|
translationCacheService,
|
|
7317
|
+
filePreviewService,
|
|
7098
7318
|
translationEngineService,
|
|
7099
7319
|
localModelAssetService,
|
|
7100
7320
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
@@ -7116,7 +7336,9 @@ function createServer(config) {
|
|
|
7116
7336
|
customSoundService,
|
|
7117
7337
|
globalSettingsManager,
|
|
7118
7338
|
translationCacheService,
|
|
7339
|
+
filePreviewService,
|
|
7119
7340
|
translationEngineService,
|
|
7341
|
+
localModelAssetService,
|
|
7120
7342
|
hookRuntime,
|
|
7121
7343
|
watcher,
|
|
7122
7344
|
createContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@huggingface/hub": "^2.12.0",
|
|
26
26
|
"@huggingface/transformers": "^4.2.0",
|
|
27
27
|
"@lydell/node-pty": "^1.1.0",
|
|
28
|
-
"@openspecui/core": "3.
|
|
29
|
-
"@openspecui/search": "3.
|
|
28
|
+
"@openspecui/core": "3.11.0",
|
|
29
|
+
"@openspecui/search": "3.11.0",
|
|
30
30
|
"@trpc/server": "^11.0.0",
|
|
31
31
|
"@types/better-sqlite3": "^7.6.13",
|
|
32
32
|
"better-sqlite3": "^12.5.0",
|