@musnows/scriverse 0.6.0 → 0.6.2
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/README.en.md +3 -2
- package/README.md +3 -2
- package/dist/ai-tool-results.js +177 -0
- package/dist/ai-tool-results.js.map +1 -0
- package/dist/ai.js +516 -138
- package/dist/ai.js.map +1 -1
- package/dist/app.js +186 -55
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +9 -1
- package/dist/attachment-storage.js.map +1 -1
- package/dist/cli-contract.js +9 -5
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +14 -2
- package/dist/cli-core.js.map +1 -1
- package/dist/collaboration-presence.js +1 -0
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/credential-vault.js +7 -5
- package/dist/credential-vault.js.map +1 -1
- package/dist/database.js +121 -2
- package/dist/database.js.map +1 -1
- package/dist/domain.js +9 -0
- package/dist/domain.js.map +1 -1
- package/dist/public/ai-context-meter.js +7 -0
- package/dist/public/app.js +556 -223
- package/dist/public/entity-version.js +2 -2
- package/dist/public/index.html +19 -11
- package/dist/public/markdown.js +1 -2
- package/dist/public/model-config.d.ts +3 -0
- package/dist/public/model-config.js +14 -0
- package/dist/public/page-route.js +1 -0
- package/dist/public/styles.css +40 -20
- package/dist/public/work-permissions.d.ts +1 -1
- package/dist/public/work-permissions.js +1 -1
- package/dist/security.js +45 -10
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +56 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +236 -25
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +19 -71
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +1 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -14,14 +14,14 @@ import { AiManager } from "./ai.js";
|
|
|
14
14
|
import { CredentialVault } from "./credential-vault.js";
|
|
15
15
|
import { Database } from "./database.js";
|
|
16
16
|
import { assertSafeDocxArchive } from "./docx-security.js";
|
|
17
|
-
import { TASK_TYPES } from "./domain.js";
|
|
17
|
+
import { DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
|
|
18
18
|
import { AppError } from "./errors.js";
|
|
19
19
|
import { HYBRID_SEARCH_TYPES } from "./hybrid-search.js";
|
|
20
20
|
import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
21
|
-
import { Store, versionedEntityTypes } from "./store.js";
|
|
22
|
-
import { parsePagination } from "./pagination.js";
|
|
21
|
+
import { attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
|
|
22
|
+
import { paginated, parsePagination } from "./pagination.js";
|
|
23
23
|
import { normalizeUploadFileName } from "./utils.js";
|
|
24
|
-
import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware } from "./security.js";
|
|
24
|
+
import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, 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";
|
|
@@ -29,7 +29,7 @@ import { createRequestLoggingMiddleware, sanitizeRequestPath } from "./http-logg
|
|
|
29
29
|
import { accountReference, logger, sanitizeError } from "./logger.js";
|
|
30
30
|
import { currentRequestActor, runWithRequestActor } from "./request-context.js";
|
|
31
31
|
import { APP_VERSION } from "./version.js";
|
|
32
|
-
import { fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
|
|
32
|
+
import { canReadWorkModule, canWriteWorkModule, fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
|
|
33
33
|
import { CollaborationPresence, entityEditorPageKey, presencePageKinds } from "./collaboration-presence.js";
|
|
34
34
|
import { analysisTaskReadModules, clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
|
|
35
35
|
const nonEmpty = z.string().trim().min(1);
|
|
@@ -38,6 +38,7 @@ const optionalStrings = z.array(z.string()).optional();
|
|
|
38
38
|
const jsonObject = z.record(z.string(), z.unknown());
|
|
39
39
|
const chapterTypeSchema = z.enum(["正文", "设定", "作者的话", "其他"]);
|
|
40
40
|
const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
|
|
41
|
+
const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
|
|
41
42
|
const maximumImportedTextLength = 20_000_000;
|
|
42
43
|
const maximumKnowledgeSectionsLength = 4_000_000;
|
|
43
44
|
const captchaFields = {
|
|
@@ -50,6 +51,7 @@ const registrationSchema = z.object({
|
|
|
50
51
|
username: usernameSchema,
|
|
51
52
|
password: passwordSchema,
|
|
52
53
|
passwordConfirmation: passwordSchema,
|
|
54
|
+
setupToken: z.string().max(500).optional(),
|
|
53
55
|
...captchaFields
|
|
54
56
|
}).strict().refine((input) => input.password === input.passwordConfirmation, {
|
|
55
57
|
path: ["passwordConfirmation"],
|
|
@@ -103,7 +105,7 @@ const presenceHeartbeatSchema = z.object({
|
|
|
103
105
|
page: z.discriminatedUnion("kind", [
|
|
104
106
|
z.object({ kind: z.literal(presencePageKinds[0]) }).strict(),
|
|
105
107
|
z.object({ kind: z.literal(presencePageKinds[1]), resourceId: identifier }).strict(),
|
|
106
|
-
z.object({ kind: z.literal(presencePageKinds[2]), module: z.enum(["drafts", "settings", "characters", "races", "organizations", "timeline", "relationships", "outlines", "reviews", "tasks", "ai-settings"]) }).strict(),
|
|
108
|
+
z.object({ kind: z.literal(presencePageKinds[2]), module: z.enum(["drafts", "settings", "characters", "races", "organizations", "timeline", "comments", "relationships", "outlines", "reviews", "tasks", "ai-settings"]) }).strict(),
|
|
107
109
|
z.object({ kind: z.literal(presencePageKinds[3]), module: z.enum(["setting", "character", "race", "organization", "relationship"]), resourceId: identifier.optional() }).strict(),
|
|
108
110
|
z.object({ kind: z.literal(presencePageKinds[4]) }).strict()
|
|
109
111
|
])
|
|
@@ -144,6 +146,8 @@ const settingSchema = z.object({
|
|
|
144
146
|
});
|
|
145
147
|
const draftSchema = z.object({
|
|
146
148
|
draftType: z.enum(["prose", "setting"]),
|
|
149
|
+
volumeId: identifier.nullable().optional(),
|
|
150
|
+
settingModule: z.enum(DRAFT_SETTING_MODULES).nullable().optional(),
|
|
147
151
|
title: nonEmpty.max(200),
|
|
148
152
|
content: z.string().max(200_000)
|
|
149
153
|
}).strict();
|
|
@@ -300,7 +304,7 @@ const modelSchema = z.object({
|
|
|
300
304
|
modelId: nonEmpty.max(300),
|
|
301
305
|
purposes: optionalStrings,
|
|
302
306
|
contextNote: z.string().max(10_000).optional(),
|
|
303
|
-
contextWindow: z.number().int().min(
|
|
307
|
+
contextWindow: z.number().int().min(32_768, "模型上下文不能低于 32768 Token").max(2_000_000).optional(),
|
|
304
308
|
outputNote: z.string().max(10_000).optional(),
|
|
305
309
|
preset: jsonObject.optional(),
|
|
306
310
|
thinkingEnabled: z.boolean().optional(),
|
|
@@ -322,6 +326,7 @@ const platformPageSizesSchema = z.object({
|
|
|
322
326
|
timeline: z.number().int().min(10).max(100).optional(),
|
|
323
327
|
outlines: z.number().int().min(10).max(100).optional(),
|
|
324
328
|
relationships: z.number().int().min(10).max(100).optional(),
|
|
329
|
+
comments: z.number().int().min(10).max(100).optional(),
|
|
325
330
|
reviews: z.number().int().min(10).max(100).optional(),
|
|
326
331
|
analysisTasks: z.number().int().min(10).max(100).optional(),
|
|
327
332
|
fileVersions: z.number().int().min(10).max(100).optional()
|
|
@@ -361,6 +366,15 @@ const aiProcessStepSchema = z.discriminatedUnion("type", [
|
|
|
361
366
|
round: z.number().int().min(1).max(20),
|
|
362
367
|
toolCall: aiToolCallResultSchema,
|
|
363
368
|
createdAt: z.string().datetime({ offset: true })
|
|
369
|
+
}).strict(),
|
|
370
|
+
z.object({
|
|
371
|
+
id: z.string().min(1).max(300),
|
|
372
|
+
type: z.literal("context_compaction"),
|
|
373
|
+
round: z.number().int().min(1).max(20),
|
|
374
|
+
sourceMessageCount: z.number().int().min(1).max(100),
|
|
375
|
+
sourceChars: z.number().int().min(1).max(10_000_000),
|
|
376
|
+
summaryChars: z.number().int().min(1).max(1_000_000),
|
|
377
|
+
createdAt: z.string().datetime({ offset: true })
|
|
364
378
|
}).strict()
|
|
365
379
|
]);
|
|
366
380
|
const workAiSettingsSchema = z.object({
|
|
@@ -586,6 +600,32 @@ function redactTaskCharacterNames(record, permissions) {
|
|
|
586
600
|
}
|
|
587
601
|
return result;
|
|
588
602
|
}
|
|
603
|
+
function redactAiCallContext(record, permissions) {
|
|
604
|
+
const result = { ...record };
|
|
605
|
+
const scope = recordValue(result.contextScope);
|
|
606
|
+
if (!scope)
|
|
607
|
+
return result;
|
|
608
|
+
const redactedScope = { ...scope };
|
|
609
|
+
let restricted = false;
|
|
610
|
+
if (permissions.prose === "none") {
|
|
611
|
+
for (const field of ["selection", "chapterId", "volumeId", "chapterIds", "includeBookSummary"]) {
|
|
612
|
+
if (field in redactedScope) {
|
|
613
|
+
delete redactedScope[field];
|
|
614
|
+
restricted = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (permissions.characters === "none" && "characterIds" in redactedScope) {
|
|
619
|
+
delete redactedScope.characterIds;
|
|
620
|
+
restricted = true;
|
|
621
|
+
}
|
|
622
|
+
if (permissions.settings === "none" && "settingIds" in redactedScope) {
|
|
623
|
+
delete redactedScope.settingIds;
|
|
624
|
+
restricted = true;
|
|
625
|
+
}
|
|
626
|
+
result.contextScope = restricted ? { ...redactedScope, restricted: true } : redactedScope;
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
589
629
|
function redactMergeRecords(value, mapper) {
|
|
590
630
|
const record = recordValue(value);
|
|
591
631
|
if (!record)
|
|
@@ -632,6 +672,32 @@ export function createRuntime(options) {
|
|
|
632
672
|
? auth.listUsers().find((user) => user.status === "active") ?? null
|
|
633
673
|
: null;
|
|
634
674
|
const store = new Store(database);
|
|
675
|
+
let attachmentCleanupChain = Promise.resolve();
|
|
676
|
+
const cleanupAttachments = () => {
|
|
677
|
+
const cleanup = attachmentCleanupChain.then(async () => {
|
|
678
|
+
store.queueUnreferencedAttachments();
|
|
679
|
+
for (const queued of store.listAttachmentCleanupQueue()) {
|
|
680
|
+
if (!store.attachmentCleanupStillRequired(queued.storageKey)) {
|
|
681
|
+
store.completeAttachmentCleanup(queued.storageKey);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
await attachmentStorage.remove(queued.storageKey);
|
|
686
|
+
store.completeAttachmentCleanup(queued.storageKey);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
store.failAttachmentCleanup(queued.storageKey, error instanceof Error ? error.message : "Attachment cleanup failed");
|
|
690
|
+
logger.warn("attachment.cleanup.failed", {
|
|
691
|
+
storageKey: queued.storageKey,
|
|
692
|
+
attempts: queued.attempts + 1,
|
|
693
|
+
error: sanitizeError(error)
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
attachmentCleanupChain = cleanup.catch(() => undefined);
|
|
699
|
+
return cleanup;
|
|
700
|
+
};
|
|
635
701
|
const requestPermissions = (request, workId) => {
|
|
636
702
|
if (!request.authUser)
|
|
637
703
|
return fullWorkModulePermissions();
|
|
@@ -699,14 +765,16 @@ export function createRuntime(options) {
|
|
|
699
765
|
app.get("/api/auth/session", (request, response) => {
|
|
700
766
|
const session = auth.authenticate(request);
|
|
701
767
|
const registrationOpen = options.security?.allowRegistration === true;
|
|
768
|
+
const setupRequired = !auth.hasUsers();
|
|
769
|
+
const setupTokenRequired = setupRequired && Boolean(options.security?.setupToken);
|
|
702
770
|
const developmentUser = getDevelopmentUser();
|
|
703
771
|
if (!session && developmentUser) {
|
|
704
|
-
data(response, { authenticated: true, user: developmentUser, csrfToken: null, setupRequired: false, registrationOpen });
|
|
772
|
+
data(response, { authenticated: true, user: developmentUser, csrfToken: null, setupRequired: false, setupTokenRequired: false, registrationOpen });
|
|
705
773
|
return;
|
|
706
774
|
}
|
|
707
775
|
data(response, session
|
|
708
|
-
? { authenticated: true, user: session.user, csrfToken: session.csrfToken, setupRequired: false, registrationOpen }
|
|
709
|
-
: { authenticated: false, user: null, csrfToken: null, setupRequired
|
|
776
|
+
? { authenticated: true, user: session.user, csrfToken: session.csrfToken, setupRequired: false, setupTokenRequired: false, registrationOpen }
|
|
777
|
+
: { authenticated: false, user: null, csrfToken: null, setupRequired, setupTokenRequired, registrationOpen });
|
|
710
778
|
});
|
|
711
779
|
app.get("/api/auth/captcha", (_request, response) => {
|
|
712
780
|
data(response, captcha.create());
|
|
@@ -717,6 +785,9 @@ export function createRuntime(options) {
|
|
|
717
785
|
}
|
|
718
786
|
const input = parse(registrationSchema, request.body);
|
|
719
787
|
captcha.consume(input.captchaId, input.captchaAnswer);
|
|
788
|
+
if (!auth.hasUsers() && !verifySetupToken(options.security?.setupToken, input.setupToken)) {
|
|
789
|
+
throw new AppError(403, "SETUP_TOKEN_INVALID", "初始化令牌无效或未配置");
|
|
790
|
+
}
|
|
720
791
|
const result = auth.register({ username: input.username, password: input.password });
|
|
721
792
|
setSessionCookie(response, result.token, request.secure);
|
|
722
793
|
runWithRequestActor(result.session.user, () => store.audit(null, "user.registered", "user", result.session.user.userId, { role: result.session.user.role }));
|
|
@@ -736,6 +807,7 @@ export function createRuntime(options) {
|
|
|
736
807
|
disabled: options.disableUserAuth === true,
|
|
737
808
|
resolveBypassUser: getDevelopmentUser
|
|
738
809
|
}));
|
|
810
|
+
app.use(createUploadRateLimitMiddleware());
|
|
739
811
|
app.use(createCliApiScopeMiddleware(options.disableUserAuth));
|
|
740
812
|
app.use(createWorkAuthorizationMiddleware(auth, options.disableUserAuth));
|
|
741
813
|
app.get("/api/cli/session", (request, response) => {
|
|
@@ -935,8 +1007,8 @@ export function createRuntime(options) {
|
|
|
935
1007
|
});
|
|
936
1008
|
app.delete("/api/works/:workId", async (request, response) => {
|
|
937
1009
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
938
|
-
|
|
939
|
-
await
|
|
1010
|
+
store.deleteWork(request.params.workId, input.expectedVersionNo);
|
|
1011
|
+
await cleanupAttachments();
|
|
940
1012
|
noContent(response);
|
|
941
1013
|
});
|
|
942
1014
|
app.get("/api/works/:workId/cover", (request, response) => {
|
|
@@ -951,14 +1023,16 @@ export function createRuntime(options) {
|
|
|
951
1023
|
if (!request.file)
|
|
952
1024
|
throw new AppError(400, "FILE_REQUIRED", "请选择 PNG、JPEG 或 WebP 封面");
|
|
953
1025
|
const bytes = request.file.buffer;
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1026
|
+
try {
|
|
1027
|
+
const metadata = readRasterImageMetadata(bytes);
|
|
1028
|
+
const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
|
|
1029
|
+
data(response, store.setWorkCover(String(request.params.workId), metadata.mimeType, bytes, expectedVersionNo));
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
if (error instanceof InvalidRasterImageError)
|
|
1033
|
+
throw new AppError(415, "INVALID_COVER", error.message);
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
962
1036
|
});
|
|
963
1037
|
app.delete("/api/works/:workId/cover", (request, response) => {
|
|
964
1038
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
@@ -1049,6 +1123,12 @@ export function createRuntime(options) {
|
|
|
1049
1123
|
data(response, pagination ? store.listChapterInsightsPage(request.params.chapterId, pagination) : store.listChapterInsights(request.params.chapterId));
|
|
1050
1124
|
});
|
|
1051
1125
|
app.get("/api/chapters/:chapterId/annotations", (request, response) => data(response, store.listChapterAnnotations(request.params.chapterId)));
|
|
1126
|
+
app.get("/api/works/:workId/chapter-annotations", (request, response) => {
|
|
1127
|
+
const pagination = parsePagination(request.query);
|
|
1128
|
+
data(response, pagination
|
|
1129
|
+
? store.listWorkChapterAnnotationsPage(request.params.workId, pagination)
|
|
1130
|
+
: store.listWorkChapterAnnotations(request.params.workId));
|
|
1131
|
+
});
|
|
1052
1132
|
app.post("/api/chapters/:chapterId/annotations", (request, response) => {
|
|
1053
1133
|
const input = parse(z.object({
|
|
1054
1134
|
kind: z.enum(["note", "todo"]),
|
|
@@ -1285,11 +1365,16 @@ export function createRuntime(options) {
|
|
|
1285
1365
|
});
|
|
1286
1366
|
app.get("/api/works/:workId/attachments", (request, response) => {
|
|
1287
1367
|
const pagination = parsePagination(request.query);
|
|
1288
|
-
|
|
1368
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
1369
|
+
const readable = store.listAttachments(request.params.workId).filter((attachment) => (store.attachmentModules(String(attachment.id)).some((module) => canReadWorkModule(permissions, module))));
|
|
1370
|
+
data(response, pagination
|
|
1371
|
+
? paginated(readable.slice(pagination.offset, pagination.offset + pagination.limit + 1), pagination, readable.length)
|
|
1372
|
+
: readable);
|
|
1289
1373
|
});
|
|
1290
1374
|
app.post("/api/works/:workId/attachments", attachmentUpload.single("file"), async (request, response) => {
|
|
1291
1375
|
if (!request.file)
|
|
1292
1376
|
throw new AppError(400, "FILE_REQUIRED", "请选择要上传的图片附件");
|
|
1377
|
+
const accessModule = parse(attachmentPermissionModuleSchema, request.query.module ?? "settings");
|
|
1293
1378
|
let storageKey = null;
|
|
1294
1379
|
try {
|
|
1295
1380
|
const stored = await attachmentStorage.ingest(request.file.path);
|
|
@@ -1297,7 +1382,7 @@ export function createRuntime(options) {
|
|
|
1297
1382
|
const result = store.createAttachment(String(request.params.workId), {
|
|
1298
1383
|
originalName: normalizeUploadFileName(request.file.originalname),
|
|
1299
1384
|
...stored
|
|
1300
|
-
});
|
|
1385
|
+
}, accessModule);
|
|
1301
1386
|
data(response, { ...result.attachment, deduplicated: !result.created }, result.created ? 201 : 200);
|
|
1302
1387
|
}
|
|
1303
1388
|
catch (error) {
|
|
@@ -1314,6 +1399,10 @@ export function createRuntime(options) {
|
|
|
1314
1399
|
});
|
|
1315
1400
|
app.get("/api/attachments/:attachmentId/content", async (request, response) => {
|
|
1316
1401
|
const attachment = store.getAttachment(request.params.attachmentId);
|
|
1402
|
+
const permissions = requestPermissions(request, String(attachment.workId));
|
|
1403
|
+
if (!store.attachmentModules(request.params.attachmentId).some((module) => canReadWorkModule(permissions, module))) {
|
|
1404
|
+
throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取该附件所属资料模块的权限");
|
|
1405
|
+
}
|
|
1317
1406
|
const content = await attachmentStorage.read(String(attachment.storageKey));
|
|
1318
1407
|
response.setHeader("Content-Type", String(attachment.storedMimeType));
|
|
1319
1408
|
response.setHeader("Content-Length", String(attachment.storedByteLength));
|
|
@@ -1323,9 +1412,13 @@ export function createRuntime(options) {
|
|
|
1323
1412
|
response.send(content);
|
|
1324
1413
|
});
|
|
1325
1414
|
app.delete("/api/attachments/:attachmentId", async (request, response) => {
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1415
|
+
const attachment = store.getAttachment(request.params.attachmentId);
|
|
1416
|
+
const permissions = requestPermissions(request, String(attachment.workId));
|
|
1417
|
+
if (!store.attachmentModules(request.params.attachmentId).some((module) => canWriteWorkModule(permissions, module))) {
|
|
1418
|
+
throw new AppError(403, "WORK_MODULE_WRITE_DENIED", "你没有编辑该附件所属资料模块的权限");
|
|
1419
|
+
}
|
|
1420
|
+
store.deleteAttachment(request.params.attachmentId);
|
|
1421
|
+
await cleanupAttachments();
|
|
1329
1422
|
noContent(response);
|
|
1330
1423
|
});
|
|
1331
1424
|
app.get("/api/works/:workId/races", (request, response) => {
|
|
@@ -1683,8 +1776,11 @@ export function createRuntime(options) {
|
|
|
1683
1776
|
data(response, updated);
|
|
1684
1777
|
});
|
|
1685
1778
|
app.get("/api/works/:workId/ai-conversations", (request, response) => {
|
|
1686
|
-
const pagination = parsePagination(
|
|
1687
|
-
|
|
1779
|
+
const pagination = parsePagination({
|
|
1780
|
+
page: request.query.page ?? "1",
|
|
1781
|
+
limit: request.query.limit ?? "20"
|
|
1782
|
+
}) ?? { page: 1, limit: 20, offset: 0 };
|
|
1783
|
+
data(response, store.listAiConversationsPage(request.params.workId, pagination));
|
|
1688
1784
|
});
|
|
1689
1785
|
app.post("/api/works/:workId/ai-conversations", (request, response) => {
|
|
1690
1786
|
const input = parse(z.object({ title: z.string().max(200).optional() }), request.body ?? {});
|
|
@@ -1734,32 +1830,23 @@ export function createRuntime(options) {
|
|
|
1734
1830
|
app.post("/api/ai-conversations/:conversationId/compact", async (request, response) => {
|
|
1735
1831
|
const input = parse(z.object({ modelId: identifier.optional(), scope: contextSchema }), request.body);
|
|
1736
1832
|
const conversation = store.getAiConversation(request.params.conversationId);
|
|
1737
|
-
|
|
1833
|
+
const compacted = await ai.compactConversation({
|
|
1738
1834
|
conversationId: request.params.conversationId,
|
|
1739
1835
|
workId: String(conversation.workId),
|
|
1740
1836
|
modelId: input.modelId,
|
|
1741
1837
|
scope: input.scope
|
|
1742
|
-
})
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
})
|
|
1754
|
-
data(response, ai.getContextUsage({
|
|
1755
|
-
workId: request.params.workId,
|
|
1756
|
-
modelId: input.modelId,
|
|
1757
|
-
taskType: input.taskType,
|
|
1758
|
-
scope: input.scope,
|
|
1759
|
-
instruction: instructionWithCitations(input.instruction, input.citations ?? []),
|
|
1760
|
-
conversationId: input.conversationId,
|
|
1761
|
-
excludeConversationMessageId: input.currentMessageId
|
|
1762
|
-
}));
|
|
1838
|
+
});
|
|
1839
|
+
data(response, {
|
|
1840
|
+
...compacted,
|
|
1841
|
+
usage: ai.getContextUsage({
|
|
1842
|
+
workId: String(conversation.workId),
|
|
1843
|
+
taskType: "chat",
|
|
1844
|
+
instruction: "",
|
|
1845
|
+
conversationId: request.params.conversationId,
|
|
1846
|
+
modelId: input.modelId,
|
|
1847
|
+
scope: input.scope
|
|
1848
|
+
})
|
|
1849
|
+
});
|
|
1763
1850
|
});
|
|
1764
1851
|
app.get("/api/works/:workId/providers", (request, response) => {
|
|
1765
1852
|
store.getWork(request.params.workId);
|
|
@@ -1861,6 +1948,40 @@ export function createRuntime(options) {
|
|
|
1861
1948
|
};
|
|
1862
1949
|
sendEvent("ready", { streaming: true });
|
|
1863
1950
|
try {
|
|
1951
|
+
const conversation = input.conversationId
|
|
1952
|
+
? store.getAiConversationSummary(input.conversationId)
|
|
1953
|
+
: store.createAiConversation(request.params.workId);
|
|
1954
|
+
if (String(conversation.workId) !== request.params.workId) {
|
|
1955
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
1956
|
+
}
|
|
1957
|
+
const conversationId = String(conversation.id);
|
|
1958
|
+
const prepared = await ai.prepareConversationContext({
|
|
1959
|
+
conversationId,
|
|
1960
|
+
workId: request.params.workId,
|
|
1961
|
+
modelId: input.modelId,
|
|
1962
|
+
scope: input.scope,
|
|
1963
|
+
instruction: instructionWithCitations(input.instruction, citations),
|
|
1964
|
+
excludeConversationMessageId: input.currentMessageId
|
|
1965
|
+
});
|
|
1966
|
+
sendEvent("context", {
|
|
1967
|
+
...prepared,
|
|
1968
|
+
conversation: {
|
|
1969
|
+
...store.getAiConversationSummary(conversationId),
|
|
1970
|
+
contextWarningPending: prepared.action === "warn"
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
if (prepared.action === "warn")
|
|
1974
|
+
return;
|
|
1975
|
+
const userMessage = input.currentMessageId
|
|
1976
|
+
? null
|
|
1977
|
+
: store.addAiConversationMessage(conversationId, {
|
|
1978
|
+
role: "user",
|
|
1979
|
+
content: input.instruction,
|
|
1980
|
+
citations
|
|
1981
|
+
});
|
|
1982
|
+
const currentMessageId = input.currentMessageId ?? String(userMessage?.id ?? "");
|
|
1983
|
+
if (userMessage)
|
|
1984
|
+
sendEvent("user_message", { message: userMessage });
|
|
1864
1985
|
const suggestion = await ai.createStreamingChat({
|
|
1865
1986
|
workId: request.params.workId,
|
|
1866
1987
|
instruction: instructionWithCitations(input.instruction, citations),
|
|
@@ -1868,9 +1989,10 @@ export function createRuntime(options) {
|
|
|
1868
1989
|
signal: controller.signal,
|
|
1869
1990
|
onToolCall: (toolCall, round) => sendEvent("tool_call", { ...toolCall, round }),
|
|
1870
1991
|
onProcessStep: (step) => sendEvent("process_step", step),
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1992
|
+
onContextCompacted: (event) => sendEvent("context_compacted", event),
|
|
1993
|
+
conversationId,
|
|
1994
|
+
excludeConversationMessageId: currentMessageId,
|
|
1995
|
+
...(currentMessageId ? { assistantMessageRequestId: `assistant:${currentMessageId}` } : {}),
|
|
1874
1996
|
...(input.modelId ? { modelId: input.modelId } : {}),
|
|
1875
1997
|
...(input.parameters ? { parameters: input.parameters } : {})
|
|
1876
1998
|
}, (delta) => sendEvent("delta", { delta }));
|
|
@@ -1884,6 +2006,8 @@ export function createRuntime(options) {
|
|
|
1884
2006
|
chapterVersion: suggestion.chapterVersion,
|
|
1885
2007
|
toolCalls: suggestion.toolCalls,
|
|
1886
2008
|
processSteps: suggestion.processSteps,
|
|
2009
|
+
contextUsage: suggestion.contextUsage,
|
|
2010
|
+
conversationId,
|
|
1887
2011
|
conversationTitle: suggestion.conversationTitle,
|
|
1888
2012
|
messageId: typeof suggestion.conversationMessage === "object" && suggestion.conversationMessage !== null
|
|
1889
2013
|
? suggestion.conversationMessage.id
|
|
@@ -1903,7 +2027,11 @@ export function createRuntime(options) {
|
|
|
1903
2027
|
message: error instanceof Error ? error.message : "AI 流式调用失败",
|
|
1904
2028
|
...(error instanceof AppError ? { status: error.status } : {}),
|
|
1905
2029
|
...(typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
1906
|
-
...(typeof details?.callId === "string" ? { callId: details.callId } : {})
|
|
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 } : {})
|
|
1907
2035
|
});
|
|
1908
2036
|
}
|
|
1909
2037
|
}
|
|
@@ -1930,7 +2058,10 @@ export function createRuntime(options) {
|
|
|
1930
2058
|
app.post("/api/suggestions/:suggestionId/reject", (request, response) => data(response, ai.rejectSuggestion(request.params.suggestionId)));
|
|
1931
2059
|
app.get("/api/works/:workId/ai-calls", (request, response) => {
|
|
1932
2060
|
const pagination = parsePagination(request.query);
|
|
1933
|
-
|
|
2061
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
2062
|
+
data(response, pagination
|
|
2063
|
+
? mapRecords(ai.listCallsPage(request.params.workId, pagination), (call) => redactAiCallContext(call, permissions))
|
|
2064
|
+
: ai.listCalls(request.params.workId).map((call) => redactAiCallContext(call, permissions)));
|
|
1934
2065
|
});
|
|
1935
2066
|
app.get("/api/works/:workId/search", async (request, response) => {
|
|
1936
2067
|
const query = parse(z.object({
|
|
@@ -2084,7 +2215,7 @@ export function createRuntime(options) {
|
|
|
2084
2215
|
response.status(500).json({ error: { code: "INTERNAL_ERROR", message: "服务器内部错误" } });
|
|
2085
2216
|
});
|
|
2086
2217
|
logger.info("runtime.ready", { serveUi: options.serveUi ?? true });
|
|
2087
|
-
return { app, database, store, ai, auth, attachmentStorage, close: () => {
|
|
2218
|
+
return { app, database, store, ai, auth, attachmentStorage, cleanupAttachments, close: () => {
|
|
2088
2219
|
logger.info("runtime.closing");
|
|
2089
2220
|
ai.dispose();
|
|
2090
2221
|
database.close();
|