@musnows/scriverse 0.6.2 → 0.6.3
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/ai-tool-results.js +71 -0
- package/dist/ai-tool-results.js.map +1 -1
- package/dist/ai.js +163 -38
- package/dist/ai.js.map +1 -1
- package/dist/app.js +155 -36
- package/dist/app.js.map +1 -1
- package/dist/cli-core.js +23 -13
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +123 -1
- package/dist/database.js.map +1 -1
- package/dist/docx-export.js +89 -0
- package/dist/docx-export.js.map +1 -0
- package/dist/image-captcha.js +8 -5
- package/dist/image-captcha.js.map +1 -1
- package/dist/public/ai-context-meter.js +4 -0
- package/dist/public/ai-message-time.js +3 -9
- package/dist/public/ai-tool-call.js +4 -0
- package/dist/public/app.js +659 -89
- package/dist/public/index.html +47 -16
- package/dist/public/page-route.js +2 -2
- package/dist/public/styles.css +77 -19
- package/dist/public/system-status.d.ts +12 -0
- package/dist/public/system-status.js +16 -0
- package/dist/public/theme-init.js +2 -2
- package/dist/security.js +101 -9
- package/dist/security.js.map +1 -1
- package/dist/store.js +111 -20
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +41 -22
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +15 -0
- package/dist/writing-progress-time.js.map +1 -1
- package/package.json +2 -1
package/dist/app.js
CHANGED
|
@@ -21,7 +21,7 @@ import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
|
21
21
|
import { attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
|
|
22
22
|
import { paginated, parsePagination } from "./pagination.js";
|
|
23
23
|
import { normalizeUploadFileName } from "./utils.js";
|
|
24
|
-
import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, verifySetupToken } from "./security.js";
|
|
24
|
+
import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
|
|
25
25
|
import { ImageCaptchaService } from "./image-captcha.js";
|
|
26
26
|
import { assertSafeImportedPlainText, decodeUtf8ImportedText } from "./import-security.js";
|
|
27
27
|
import { InvalidRasterImageError, readRasterImageMetadata } from "./image-metadata.js";
|
|
@@ -379,6 +379,7 @@ const aiProcessStepSchema = z.discriminatedUnion("type", [
|
|
|
379
379
|
]);
|
|
380
380
|
const workAiSettingsSchema = z.object({
|
|
381
381
|
systemPrompt: z.string().max(100_000).optional(),
|
|
382
|
+
dailyTokenQuota: z.number().int().min(10_000).max(2_000_000_000).nullable().optional(),
|
|
382
383
|
autoRunEnabled: z.boolean().optional(),
|
|
383
384
|
autoRunConcurrency: z.number().int().min(1).max(8).optional(),
|
|
384
385
|
autoRunBatchLimit: z.number().int().min(1).max(200).optional(),
|
|
@@ -386,6 +387,8 @@ const workAiSettingsSchema = z.object({
|
|
|
386
387
|
autoRunFailureThreshold: z.number().int().min(1).max(10).optional(),
|
|
387
388
|
bookSummaryContextPercent: z.number().int().min(1).max(90).optional(),
|
|
388
389
|
contextCompactThreshold: z.number().int().min(50).max(90).optional(),
|
|
390
|
+
agentToolCallLimit: z.number().int().min(5).max(48).optional(),
|
|
391
|
+
agentToolCallGlobalMultiplier: z.number().int().min(1).max(6).optional(),
|
|
389
392
|
agentTools: z.array(z.enum(["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"])).max(6).optional(),
|
|
390
393
|
titleGenerationModelId: z.string().trim().max(200).optional()
|
|
391
394
|
}).strict();
|
|
@@ -626,6 +629,88 @@ function redactAiCallContext(record, permissions) {
|
|
|
626
629
|
result.contextScope = restricted ? { ...redactedScope, restricted: true } : redactedScope;
|
|
627
630
|
return result;
|
|
628
631
|
}
|
|
632
|
+
const proseRestrictedPlaceholder = "(正文读取权限受限)";
|
|
633
|
+
function redactContinuationGuard(record, permissions) {
|
|
634
|
+
if (permissions.prose !== "none")
|
|
635
|
+
return record;
|
|
636
|
+
return {
|
|
637
|
+
...record,
|
|
638
|
+
issues: [],
|
|
639
|
+
contextRefs: {},
|
|
640
|
+
failure: null,
|
|
641
|
+
restricted: true
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/** 无正文读取权限时移除建议中的原文、指令和检查证据,避免通过 AI 接口绕过 prose=none。 */
|
|
645
|
+
function redactSuggestion(record, permissions) {
|
|
646
|
+
if (permissions.prose !== "none")
|
|
647
|
+
return record;
|
|
648
|
+
const guard = recordValue(record.guard);
|
|
649
|
+
return {
|
|
650
|
+
...record,
|
|
651
|
+
instruction: proseRestrictedPlaceholder,
|
|
652
|
+
sourceText: "",
|
|
653
|
+
...(guard ? { guard: redactContinuationGuard(guard, permissions) } : {}),
|
|
654
|
+
restricted: true
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function redactAiConversationMessage(item, permissions) {
|
|
658
|
+
if (permissions.prose !== "none")
|
|
659
|
+
return item;
|
|
660
|
+
const message = recordValue(item);
|
|
661
|
+
if (!message)
|
|
662
|
+
return item;
|
|
663
|
+
return {
|
|
664
|
+
...message,
|
|
665
|
+
content: proseRestrictedPlaceholder,
|
|
666
|
+
citations: [],
|
|
667
|
+
metadata: { restricted: true },
|
|
668
|
+
restricted: true
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/** 无正文读取权限时隐藏对话预览与消息正文,避免历史对话泄露章节原文。 */
|
|
672
|
+
function redactAiConversation(record, permissions) {
|
|
673
|
+
if (permissions.prose !== "none")
|
|
674
|
+
return record;
|
|
675
|
+
const result = {
|
|
676
|
+
...record,
|
|
677
|
+
title: proseRestrictedPlaceholder
|
|
678
|
+
};
|
|
679
|
+
if (typeof result.preview === "string" && result.preview.length > 0) {
|
|
680
|
+
result.preview = proseRestrictedPlaceholder;
|
|
681
|
+
}
|
|
682
|
+
if (Array.isArray(result.messages)) {
|
|
683
|
+
result.messages = result.messages.map((item) => redactAiConversationMessage(item, permissions));
|
|
684
|
+
}
|
|
685
|
+
const messagesPage = recordValue(result.messagesPage);
|
|
686
|
+
if (messagesPage && Array.isArray(messagesPage.items)) {
|
|
687
|
+
result.messagesPage = {
|
|
688
|
+
...messagesPage,
|
|
689
|
+
items: messagesPage.items.map((item) => redactAiConversationMessage(item, permissions))
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return { ...result, restricted: true };
|
|
693
|
+
}
|
|
694
|
+
/** SSE 错误事件只暴露 AppError 的公开信息,避免透传内部异常 message。 */
|
|
695
|
+
export function publicAiStreamError(error) {
|
|
696
|
+
if (error instanceof AppError) {
|
|
697
|
+
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
|
|
698
|
+
? error.details
|
|
699
|
+
: null;
|
|
700
|
+
return {
|
|
701
|
+
code: error.code,
|
|
702
|
+
message: error.message,
|
|
703
|
+
status: error.status,
|
|
704
|
+
...(error.status < 500 && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
705
|
+
...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
|
|
706
|
+
...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
|
|
707
|
+
...(typeof details?.providerId === "string" ? { providerId: details.providerId } : {}),
|
|
708
|
+
...(typeof details?.modelId === "string" ? { modelId: details.modelId } : {}),
|
|
709
|
+
...(typeof details?.modelRecordId === "string" ? { modelRecordId: details.modelRecordId } : {})
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return { code: "AI_STREAM_FAILED", message: "AI 流式调用失败" };
|
|
713
|
+
}
|
|
629
714
|
function redactMergeRecords(value, mapper) {
|
|
630
715
|
const record = recordValue(value);
|
|
631
716
|
if (!record)
|
|
@@ -652,6 +737,7 @@ export function createRuntime(options) {
|
|
|
652
737
|
sameOriginEnforced: options.security?.enforceSameOrigin ?? true
|
|
653
738
|
});
|
|
654
739
|
const database = new Database(options.databasePath);
|
|
740
|
+
const bootId = randomUUID();
|
|
655
741
|
const temporaryAttachmentRoot = options.databasePath === ":memory:" && !options.attachmentDirectory
|
|
656
742
|
? mkdtempSync(join(tmpdir(), "scriverse-attachments-"))
|
|
657
743
|
: null;
|
|
@@ -722,6 +808,7 @@ export function createRuntime(options) {
|
|
|
722
808
|
}, false, actor?.allowAdminAccess ?? false);
|
|
723
809
|
});
|
|
724
810
|
const app = express();
|
|
811
|
+
enforceCaseInsensitiveRouting(app);
|
|
725
812
|
const upload = multer({
|
|
726
813
|
storage: multer.memoryStorage(),
|
|
727
814
|
limits: { fileSize: 30 * 1024 * 1024, files: 1, fields: 10, fieldSize: 64 * 1024, parts: 11, headerPairs: 100 }
|
|
@@ -742,13 +829,18 @@ export function createRuntime(options) {
|
|
|
742
829
|
limits: { fileSize: 30 * 1024 * 1024, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
743
830
|
});
|
|
744
831
|
app.disable("x-powered-by");
|
|
745
|
-
|
|
746
|
-
|
|
832
|
+
const trustProxy = resolveTrustProxySetting(options.security?.trustProxy);
|
|
833
|
+
if (options.security?.trustProxy === true) {
|
|
834
|
+
logger.warn("security.trust_proxy.coerced", { from: true, to: 1 });
|
|
835
|
+
}
|
|
836
|
+
if (trustProxy !== undefined)
|
|
837
|
+
app.set("trust proxy", trustProxy);
|
|
747
838
|
app.use(createRequestLoggingMiddleware());
|
|
748
839
|
app.use(createSecurityHeadersMiddleware());
|
|
749
840
|
app.get("/api/health", (_request, response) => {
|
|
750
841
|
data(response, {
|
|
751
842
|
status: "ok",
|
|
843
|
+
bootId,
|
|
752
844
|
version: APP_VERSION,
|
|
753
845
|
protocol: "openai-chat-completions",
|
|
754
846
|
protocols: ["openai-chat-completions", "anthropic-messages"],
|
|
@@ -758,6 +850,7 @@ export function createRuntime(options) {
|
|
|
758
850
|
if (options.security?.auth)
|
|
759
851
|
app.use(createBasicAuthMiddleware(options.security.auth));
|
|
760
852
|
app.use(createAuthenticationRateLimitMiddleware());
|
|
853
|
+
app.use(createCaptchaRateLimitMiddleware());
|
|
761
854
|
app.use(createApiRateLimitMiddleware(options.security?.apiRateLimit, options.security?.apiRateWindowMs));
|
|
762
855
|
if (options.security?.enforceSameOrigin ?? true)
|
|
763
856
|
app.use(createSameOriginMiddleware());
|
|
@@ -769,12 +862,12 @@ export function createRuntime(options) {
|
|
|
769
862
|
const setupTokenRequired = setupRequired && Boolean(options.security?.setupToken);
|
|
770
863
|
const developmentUser = getDevelopmentUser();
|
|
771
864
|
if (!session && developmentUser) {
|
|
772
|
-
data(response, { authenticated: true, user: developmentUser, csrfToken: null, setupRequired: false, setupTokenRequired: false, registrationOpen });
|
|
865
|
+
data(response, { authenticated: true, user: developmentUser, csrfToken: null, bootId, setupRequired: false, setupTokenRequired: false, registrationOpen });
|
|
773
866
|
return;
|
|
774
867
|
}
|
|
775
868
|
data(response, session
|
|
776
|
-
? { authenticated: true, user: session.user, csrfToken: session.csrfToken, setupRequired: false, setupTokenRequired: false, registrationOpen }
|
|
777
|
-
: { authenticated: false, user: null, csrfToken: null, setupRequired, setupTokenRequired, registrationOpen });
|
|
869
|
+
? { authenticated: true, user: session.user, csrfToken: session.csrfToken, bootId, setupRequired: false, setupTokenRequired: false, registrationOpen }
|
|
870
|
+
: { authenticated: false, user: null, csrfToken: null, bootId, setupRequired, setupTokenRequired, registrationOpen });
|
|
778
871
|
});
|
|
779
872
|
app.get("/api/auth/captcha", (_request, response) => {
|
|
780
873
|
data(response, captcha.create());
|
|
@@ -808,6 +901,7 @@ export function createRuntime(options) {
|
|
|
808
901
|
resolveBypassUser: getDevelopmentUser
|
|
809
902
|
}));
|
|
810
903
|
app.use(createUploadRateLimitMiddleware());
|
|
904
|
+
app.use(createExpensiveApiRateLimitMiddleware());
|
|
811
905
|
app.use(createCliApiScopeMiddleware(options.disableUserAuth));
|
|
812
906
|
app.use(createWorkAuthorizationMiddleware(auth, options.disableUserAuth));
|
|
813
907
|
app.get("/api/cli/session", (request, response) => {
|
|
@@ -916,6 +1010,8 @@ export function createRuntime(options) {
|
|
|
916
1010
|
app.patch("/api/users/:userId", (request, response) => {
|
|
917
1011
|
if (!request.authUser)
|
|
918
1012
|
throw new AppError(401, "AUTH_REQUIRED", "请先登录");
|
|
1013
|
+
if (request.authUser.role !== "admin")
|
|
1014
|
+
throw new AppError(403, "ADMIN_REQUIRED", "该操作仅限系统管理员");
|
|
919
1015
|
const updated = auth.updateUser(request.authUser, request.params.userId, parse(userUpdateSchema, request.body));
|
|
920
1016
|
store.audit(null, "user.updated", "user", updated.userId, { role: updated.role, status: updated.status });
|
|
921
1017
|
data(response, updated);
|
|
@@ -1780,7 +1876,8 @@ export function createRuntime(options) {
|
|
|
1780
1876
|
page: request.query.page ?? "1",
|
|
1781
1877
|
limit: request.query.limit ?? "20"
|
|
1782
1878
|
}) ?? { page: 1, limit: 20, offset: 0 };
|
|
1783
|
-
|
|
1879
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
1880
|
+
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
1784
1881
|
});
|
|
1785
1882
|
app.post("/api/works/:workId/ai-conversations", (request, response) => {
|
|
1786
1883
|
const input = parse(z.object({ title: z.string().max(200).optional() }), request.body ?? {});
|
|
@@ -1788,11 +1885,17 @@ export function createRuntime(options) {
|
|
|
1788
1885
|
});
|
|
1789
1886
|
app.get("/api/ai-conversations/:conversationId", (request, response) => {
|
|
1790
1887
|
const pagination = parsePagination(request.query);
|
|
1791
|
-
|
|
1888
|
+
const conversation = pagination
|
|
1889
|
+
? store.getAiConversationPage(request.params.conversationId, pagination)
|
|
1890
|
+
: store.getAiConversation(request.params.conversationId);
|
|
1891
|
+
const permissions = requestPermissions(request, String(conversation.workId));
|
|
1892
|
+
data(response, redactAiConversation(conversation, permissions));
|
|
1792
1893
|
});
|
|
1793
1894
|
app.post("/api/ai-conversations/:conversationId/fork", (request, response) => {
|
|
1794
1895
|
const input = parse(z.object({ messageId: identifier, title: z.string().max(200).optional() }), request.body);
|
|
1795
|
-
|
|
1896
|
+
const forked = store.forkAiConversation(request.params.conversationId, input.messageId, input.title);
|
|
1897
|
+
const permissions = requestPermissions(request, String(forked.workId));
|
|
1898
|
+
data(response, redactAiConversation(forked, permissions), 201);
|
|
1796
1899
|
});
|
|
1797
1900
|
app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
|
|
1798
1901
|
const input = parse(z.object({
|
|
@@ -1809,7 +1912,10 @@ export function createRuntime(options) {
|
|
|
1809
1912
|
processSteps: z.array(aiProcessStepSchema).max(50).optional()
|
|
1810
1913
|
}).optional()
|
|
1811
1914
|
}), request.body);
|
|
1812
|
-
|
|
1915
|
+
const message = store.addAiConversationMessage(request.params.conversationId, input);
|
|
1916
|
+
const conversation = store.getAiConversationSummary(request.params.conversationId);
|
|
1917
|
+
const permissions = requestPermissions(request, String(conversation.workId));
|
|
1918
|
+
data(response, redactAiConversationMessage(message, permissions), 201);
|
|
1813
1919
|
});
|
|
1814
1920
|
app.post("/api/ai-conversations/:conversationId/context/prepare", async (request, response) => {
|
|
1815
1921
|
const input = parse(z.object({
|
|
@@ -1891,7 +1997,10 @@ export function createRuntime(options) {
|
|
|
1891
1997
|
app.get("/api/works/:workId/suggestions", (request, response) => {
|
|
1892
1998
|
const status = typeof request.query.status === "string" ? request.query.status : undefined;
|
|
1893
1999
|
const pagination = parsePagination(request.query);
|
|
1894
|
-
|
|
2000
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
2001
|
+
data(response, pagination
|
|
2002
|
+
? mapRecords(ai.listSuggestionsPage(request.params.workId, pagination, status), (suggestion) => redactSuggestion(suggestion, permissions))
|
|
2003
|
+
: ai.listSuggestions(request.params.workId, status).map((suggestion) => redactSuggestion(suggestion, permissions)));
|
|
1895
2004
|
});
|
|
1896
2005
|
app.post("/api/works/:workId/suggestions", async (request, response) => {
|
|
1897
2006
|
const input = parse(z.object({
|
|
@@ -1907,14 +2016,14 @@ export function createRuntime(options) {
|
|
|
1907
2016
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
1908
2017
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
1909
2018
|
}
|
|
1910
|
-
data(response, await ai.createSuggestion({
|
|
2019
|
+
data(response, redactSuggestion(await ai.createSuggestion({
|
|
1911
2020
|
workId: request.params.workId,
|
|
1912
2021
|
taskType: input.taskType,
|
|
1913
2022
|
instruction: instructionWithCitations(input.instruction, citations),
|
|
1914
2023
|
scope: input.scope,
|
|
1915
2024
|
...(input.modelId ? { modelId: input.modelId } : {}),
|
|
1916
2025
|
...(input.parameters ? { parameters: input.parameters } : {})
|
|
1917
|
-
}), 201);
|
|
2026
|
+
}), requestPermissions(request, request.params.workId)), 201);
|
|
1918
2027
|
});
|
|
1919
2028
|
app.post("/api/works/:workId/chat/stream", async (request, response) => {
|
|
1920
2029
|
const input = parse(z.object({
|
|
@@ -1955,6 +2064,7 @@ export function createRuntime(options) {
|
|
|
1955
2064
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
1956
2065
|
}
|
|
1957
2066
|
const conversationId = String(conversation.id);
|
|
2067
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
1958
2068
|
const prepared = await ai.prepareConversationContext({
|
|
1959
2069
|
conversationId,
|
|
1960
2070
|
workId: request.params.workId,
|
|
@@ -1965,10 +2075,10 @@ export function createRuntime(options) {
|
|
|
1965
2075
|
});
|
|
1966
2076
|
sendEvent("context", {
|
|
1967
2077
|
...prepared,
|
|
1968
|
-
conversation: {
|
|
2078
|
+
conversation: redactAiConversation({
|
|
1969
2079
|
...store.getAiConversationSummary(conversationId),
|
|
1970
2080
|
contextWarningPending: prepared.action === "warn"
|
|
1971
|
-
}
|
|
2081
|
+
}, permissions)
|
|
1972
2082
|
});
|
|
1973
2083
|
if (prepared.action === "warn")
|
|
1974
2084
|
return;
|
|
@@ -1981,7 +2091,7 @@ export function createRuntime(options) {
|
|
|
1981
2091
|
});
|
|
1982
2092
|
const currentMessageId = input.currentMessageId ?? String(userMessage?.id ?? "");
|
|
1983
2093
|
if (userMessage)
|
|
1984
|
-
sendEvent("user_message", { message: userMessage });
|
|
2094
|
+
sendEvent("user_message", { message: redactAiConversationMessage(userMessage, permissions) });
|
|
1985
2095
|
const suggestion = await ai.createStreamingChat({
|
|
1986
2096
|
workId: request.params.workId,
|
|
1987
2097
|
instruction: instructionWithCitations(input.instruction, citations),
|
|
@@ -2019,20 +2129,11 @@ export function createRuntime(options) {
|
|
|
2019
2129
|
}
|
|
2020
2130
|
catch (error) {
|
|
2021
2131
|
if (!controller.signal.aborted) {
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
:
|
|
2025
|
-
sendEvent("error", {
|
|
2026
|
-
code: error instanceof AppError ? error.code : "AI_STREAM_FAILED",
|
|
2027
|
-
message: error instanceof Error ? error.message : "AI 流式调用失败",
|
|
2028
|
-
...(error instanceof AppError ? { status: error.status } : {}),
|
|
2029
|
-
...(typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
2030
|
-
...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
|
|
2031
|
-
...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
|
|
2032
|
-
...(typeof details?.providerId === "string" ? { providerId: details.providerId } : {}),
|
|
2033
|
-
...(typeof details?.modelId === "string" ? { modelId: details.modelId } : {}),
|
|
2034
|
-
...(typeof details?.modelRecordId === "string" ? { modelRecordId: details.modelRecordId } : {})
|
|
2132
|
+
logger.error("ai.stream.failed", {
|
|
2133
|
+
workId: request.params.workId,
|
|
2134
|
+
error: sanitizeError(error)
|
|
2035
2135
|
});
|
|
2136
|
+
sendEvent("error", publicAiStreamError(error));
|
|
2036
2137
|
}
|
|
2037
2138
|
}
|
|
2038
2139
|
finally {
|
|
@@ -2040,22 +2141,34 @@ export function createRuntime(options) {
|
|
|
2040
2141
|
response.end();
|
|
2041
2142
|
}
|
|
2042
2143
|
});
|
|
2043
|
-
app.get("/api/suggestions/:suggestionId", (request, response) =>
|
|
2144
|
+
app.get("/api/suggestions/:suggestionId", (request, response) => {
|
|
2145
|
+
const suggestion = ai.getSuggestion(request.params.suggestionId);
|
|
2146
|
+
const permissions = requestPermissions(request, String(suggestion.workId));
|
|
2147
|
+
data(response, redactSuggestion(suggestion, permissions));
|
|
2148
|
+
});
|
|
2044
2149
|
app.get("/api/suggestions/:suggestionId/guards", (request, response) => {
|
|
2045
2150
|
const pagination = parsePagination(request.query);
|
|
2046
|
-
|
|
2151
|
+
const suggestion = ai.getSuggestion(request.params.suggestionId);
|
|
2152
|
+
const permissions = requestPermissions(request, String(suggestion.workId));
|
|
2153
|
+
data(response, mapRecords(pagination
|
|
2047
2154
|
? store.listContinuationGuardsPage(request.params.suggestionId, pagination)
|
|
2048
|
-
: store.listContinuationGuards(request.params.suggestionId));
|
|
2155
|
+
: store.listContinuationGuards(request.params.suggestionId), (guard) => redactContinuationGuard(guard, permissions)));
|
|
2049
2156
|
});
|
|
2050
2157
|
app.post("/api/suggestions/:suggestionId/guard", async (request, response) => {
|
|
2051
2158
|
const input = parse(z.object({ content: z.string().max(2_000_000).optional() }), request.body ?? {});
|
|
2052
|
-
|
|
2159
|
+
const suggestion = ai.getSuggestion(request.params.suggestionId);
|
|
2160
|
+
const permissions = requestPermissions(request, String(suggestion.workId));
|
|
2161
|
+
data(response, redactContinuationGuard(await ai.runSuggestionGuard(request.params.suggestionId, input.content), permissions), 201);
|
|
2053
2162
|
});
|
|
2054
2163
|
app.post("/api/suggestions/:suggestionId/accept", (request, response) => {
|
|
2055
2164
|
const input = parse(z.object({ content: z.string().max(2_000_000).optional() }), request.body ?? {});
|
|
2056
2165
|
data(response, ai.acceptSuggestion(request.params.suggestionId, input.content));
|
|
2057
2166
|
});
|
|
2058
|
-
app.post("/api/suggestions/:suggestionId/reject", (request, response) =>
|
|
2167
|
+
app.post("/api/suggestions/:suggestionId/reject", (request, response) => {
|
|
2168
|
+
const suggestion = ai.rejectSuggestion(request.params.suggestionId);
|
|
2169
|
+
const permissions = requestPermissions(request, String(suggestion.workId));
|
|
2170
|
+
data(response, redactSuggestion(suggestion, permissions));
|
|
2171
|
+
});
|
|
2059
2172
|
app.get("/api/works/:workId/ai-calls", (request, response) => {
|
|
2060
2173
|
const pagination = parsePagination(request.query);
|
|
2061
2174
|
const permissions = requestPermissions(request, request.params.workId);
|
|
@@ -2072,7 +2185,7 @@ export function createRuntime(options) {
|
|
|
2072
2185
|
data(response, await ai.searchWork(request.params.workId, query.q, { type: query.type, limit: query.limit }));
|
|
2073
2186
|
});
|
|
2074
2187
|
app.get("/api/works/:workId/export", async (request, response) => {
|
|
2075
|
-
const format = parse(z.enum(["json", "txt", "markdown"]), request.query.format ?? "json");
|
|
2188
|
+
const format = parse(z.enum(["json", "txt", "markdown", "docx"]), request.query.format ?? "json");
|
|
2076
2189
|
if (format === "json") {
|
|
2077
2190
|
response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.json`);
|
|
2078
2191
|
data(response, store.exportWork(request.params.workId));
|
|
@@ -2092,6 +2205,12 @@ export function createRuntime(options) {
|
|
|
2092
2205
|
}), response);
|
|
2093
2206
|
return;
|
|
2094
2207
|
}
|
|
2208
|
+
if (format === "docx") {
|
|
2209
|
+
response.type("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
|
2210
|
+
response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.docx`);
|
|
2211
|
+
response.send(await store.exportDocx(request.params.workId));
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2095
2214
|
response.type("text/plain");
|
|
2096
2215
|
response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.txt`);
|
|
2097
2216
|
response.send(store.exportText(request.params.workId, format));
|
|
@@ -2151,7 +2270,7 @@ export function createRuntime(options) {
|
|
|
2151
2270
|
setHeaders: setStaticCacheControl
|
|
2152
2271
|
}));
|
|
2153
2272
|
app.get("/{*path}", (request, response, next) => {
|
|
2154
|
-
if (request.path.startsWith("/api/"))
|
|
2273
|
+
if (normalizeApiPath(request.path).startsWith("/api/"))
|
|
2155
2274
|
return next();
|
|
2156
2275
|
sendIndexHtml(request, response);
|
|
2157
2276
|
});
|