@musnows/scriverse 0.9.3 → 0.9.5
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-write-plans.js +2279 -0
- package/dist/ai-write-plans.js.map +1 -0
- package/dist/ai.js +1236 -91
- package/dist/ai.js.map +1 -1
- package/dist/app.js +190 -3
- package/dist/app.js.map +1 -1
- package/dist/chapter-annotation-anchor.js +327 -0
- package/dist/chapter-annotation-anchor.js.map +1 -0
- package/dist/database.js +338 -3
- package/dist/database.js.map +1 -1
- package/dist/public/ai-context-meter.js +10 -5
- package/dist/public/ai-interactive.js +483 -0
- package/dist/public/app.js +1187 -86
- package/dist/public/chapter-editor-behavior.js +40 -0
- package/dist/public/chapter-line-id-tracker.d.ts +19 -0
- package/dist/public/chapter-line-id-tracker.js +127 -0
- package/dist/public/index.html +59 -10
- package/dist/public/styles.css +237 -54
- package/dist/public/theme-init.js +25 -1
- package/dist/roleplay-memory.js +53 -0
- package/dist/roleplay-memory.js.map +1 -0
- package/dist/store.js +454 -24
- package/dist/store.js.map +1 -1
- package/dist/ui-module-preload.js +27 -0
- package/dist/ui-module-preload.js.map +1 -0
- package/dist/user-auth.js +22 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/utils.js +0 -8
- package/dist/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
package/dist/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import compression from "compression";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import JSZip from "jszip";
|
|
3
4
|
import multer from "multer";
|
|
@@ -20,6 +21,7 @@ import { AiManager } from "./ai.js";
|
|
|
20
21
|
import { LiteLlmPriceCache } from "./ai-model-pricing.js";
|
|
21
22
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
22
23
|
import { CHARACTER_EXTRACTION_MAX_ALIASES, CHARACTER_EXTRACTION_MAX_CANDIDATES, CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH, CHARACTER_EXTRACTION_MAX_NAME_LENGTH, CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH } from "./character-extraction.js";
|
|
24
|
+
import { AiWritePlanManager, AI_USER_QUESTION_STATUSES, AI_WRITE_PLAN_STATUSES, aiWriteToolDescriptions, aiWriteToolLabels, aiWriteToolsUpdateSchema, answerAiUserQuestionSchema, resolveAiWritePlanMaxOperations } from "./ai-write-plans.js";
|
|
23
25
|
import { CredentialVault } from "./credential-vault.js";
|
|
24
26
|
import { Database } from "./database.js";
|
|
25
27
|
import { assertSafeDocxArchive } from "./docx-security.js";
|
|
@@ -29,8 +31,10 @@ import { AppError } from "./errors.js";
|
|
|
29
31
|
import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./google-vertex-auth.js";
|
|
30
32
|
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, readableHybridSearchTypes } from "./hybrid-search.js";
|
|
31
33
|
import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
34
|
+
import { MAX_CHAPTER_LINE_IDS } from "./chapter-annotation-anchor.js";
|
|
32
35
|
import { aiConversationTaskTypes, attachmentPermissionModules, RECYCLE_BIN_RETENTION_DAYS, Store, versionedEntityTypes, WORK_AGENT_TOOL_IDS } from "./store.js";
|
|
33
36
|
import { composeRoleplayStoredUserContent } from "./roleplay-turn.js";
|
|
37
|
+
import { ROLEPLAY_MEMORY_CATEGORIES, ROLEPLAY_MEMORY_CERTAINTY, ROLEPLAY_MEMORY_IMPORTANCE, ROLEPLAY_MEMORY_STATUSES } from "./roleplay-memory.js";
|
|
34
38
|
import { paginated, parsePagination } from "./pagination.js";
|
|
35
39
|
import { normalizeUploadFileName } from "./utils.js";
|
|
36
40
|
import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
|
|
@@ -50,6 +54,7 @@ import { canReadWorkModule, canWriteWorkModule, chapterAnnotationPermissionModul
|
|
|
50
54
|
import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, presencePageKinds } from "./collaboration-presence.js";
|
|
51
55
|
import { PresenceStore } from "./presence-store.js";
|
|
52
56
|
import { analysisTaskReadModules, clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
|
|
57
|
+
import { extractStaticModuleImports, injectModulePreloads } from "./ui-module-preload.js";
|
|
53
58
|
const chapterAnnotationKinds = ["note", "todo"];
|
|
54
59
|
function readableChapterAnnotationKinds(permissions) {
|
|
55
60
|
return chapterAnnotationKinds.filter((kind) => canReadWorkModule(permissions, chapterAnnotationPermissionModule(kind)));
|
|
@@ -62,6 +67,23 @@ const optionalStrings = z.array(z.string()).optional();
|
|
|
62
67
|
const jsonObject = z.record(z.string(), z.unknown());
|
|
63
68
|
const chapterTypeSchema = z.enum(["正文", "设定", "作者的话", "其他"]);
|
|
64
69
|
const aiConversationTaskTypeSchema = z.enum(aiConversationTaskTypes);
|
|
70
|
+
const roleplayMemoryInputSchema = z.object({
|
|
71
|
+
category: z.enum(ROLEPLAY_MEMORY_CATEGORIES),
|
|
72
|
+
content: z.string().trim().min(1).max(2_000),
|
|
73
|
+
importance: z.enum(ROLEPLAY_MEMORY_IMPORTANCE).optional(),
|
|
74
|
+
certainty: z.enum(ROLEPLAY_MEMORY_CERTAINTY).optional(),
|
|
75
|
+
isPinned: z.boolean().optional()
|
|
76
|
+
}).strict();
|
|
77
|
+
const roleplayMemoryListQuerySchema = z.object({
|
|
78
|
+
q: z.string().trim().max(200).optional(),
|
|
79
|
+
categories: z.preprocess((value) => typeof value === "string" ? value.split(",").filter(Boolean) : value, z.array(z.enum(ROLEPLAY_MEMORY_CATEGORIES)).max(ROLEPLAY_MEMORY_CATEGORIES.length).optional()),
|
|
80
|
+
statuses: z.preprocess((value) => typeof value === "string" ? value.split(",").filter(Boolean) : value, z.array(z.enum(ROLEPLAY_MEMORY_STATUSES)).max(ROLEPLAY_MEMORY_STATUSES.length).optional()),
|
|
81
|
+
cursor: z.coerce.number().int().min(0).max(100_000).optional(),
|
|
82
|
+
limit: z.coerce.number().int().min(1).max(100).optional()
|
|
83
|
+
}).strict();
|
|
84
|
+
const roleplayMemoryUpdateSchema = roleplayMemoryInputSchema.partial().extend({
|
|
85
|
+
expectedVersion: z.number().int().min(1)
|
|
86
|
+
}).strict().refine((value) => Object.keys(value).some((key) => key !== "expectedVersion"), "至少提供一个要修改的字段");
|
|
65
87
|
const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
|
|
66
88
|
const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
|
|
67
89
|
const maximumImportedTextLength = 20_000_000;
|
|
@@ -211,7 +233,9 @@ const workSchema = z.object({
|
|
|
211
233
|
description: z.string().max(10_000).optional(),
|
|
212
234
|
language: z.string().max(30).optional(),
|
|
213
235
|
coverUrl: z.string().url().nullable().optional(),
|
|
214
|
-
tags: optionalStrings
|
|
236
|
+
tags: optionalStrings,
|
|
237
|
+
editorAutoIndentEnabled: z.boolean().optional(),
|
|
238
|
+
editorTypewriterModeEnabled: z.boolean().optional()
|
|
215
239
|
});
|
|
216
240
|
const workOfflineAccessSchema = z.object({ enabled: z.boolean() }).strict();
|
|
217
241
|
const settingSchema = z.object({
|
|
@@ -1340,6 +1364,15 @@ export function createRuntime(options) {
|
|
|
1340
1364
|
liteLlmPriceCache,
|
|
1341
1365
|
allowPrivateAiEndpoints: options.security?.allowPrivateAiEndpoints === true
|
|
1342
1366
|
});
|
|
1367
|
+
// AI 可写工具与审批工作流:计划创建只能由侧边栏 AI 发起,确认入口只接收审批 ID。
|
|
1368
|
+
const aiWritePlanManager = new AiWritePlanManager({
|
|
1369
|
+
database,
|
|
1370
|
+
store,
|
|
1371
|
+
auth,
|
|
1372
|
+
resolveAnalysisTask: (workId, input) => ai.resolveTaskInput(workId, input),
|
|
1373
|
+
startAnalysisTask: (workId, input) => store.createTask(workId, input)
|
|
1374
|
+
});
|
|
1375
|
+
ai.attachWritePlanManager(aiWritePlanManager);
|
|
1343
1376
|
const app = express();
|
|
1344
1377
|
enforceCaseInsensitiveRouting(app);
|
|
1345
1378
|
const upload = multer({
|
|
@@ -1905,7 +1938,7 @@ export function createRuntime(options) {
|
|
|
1905
1938
|
});
|
|
1906
1939
|
app.get("/api/chapters/:chapterId", (request, response) => data(response, store.getChapter(request.params.chapterId)));
|
|
1907
1940
|
app.patch("/api/chapters/:chapterId", (request, response) => {
|
|
1908
|
-
const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1941
|
+
const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), lineIds: z.array(z.union([identifier, z.null()])).max(MAX_CHAPTER_LINE_IDS).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1909
1942
|
const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
|
|
1910
1943
|
const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
|
|
1911
1944
|
publishEditorChange(String(chapter.workId), String(chapter.id));
|
|
@@ -2875,6 +2908,16 @@ export function createRuntime(options) {
|
|
|
2875
2908
|
data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
|
|
2876
2909
|
});
|
|
2877
2910
|
app.use("/api/ai-conversations/:conversationId", (request, _response, next) => {
|
|
2911
|
+
const sourceId = typeof request.query.roleplayMemorySourceId === "string"
|
|
2912
|
+
? request.query.roleplayMemorySourceId
|
|
2913
|
+
: null;
|
|
2914
|
+
const messageId = typeof request.query.messageId === "string" ? request.query.messageId : null;
|
|
2915
|
+
if (["GET", "HEAD"].includes(request.method)
|
|
2916
|
+
&& request.authUser?.role === "admin"
|
|
2917
|
+
&& sourceId !== null
|
|
2918
|
+
&& messageId !== null
|
|
2919
|
+
&& store.isRoleplayMemorySourceTarget(sourceId, request.params.conversationId, messageId))
|
|
2920
|
+
return next();
|
|
2878
2921
|
assertRequestAiConversationOwner(request, request.params.conversationId);
|
|
2879
2922
|
next();
|
|
2880
2923
|
});
|
|
@@ -2942,6 +2985,20 @@ export function createRuntime(options) {
|
|
|
2942
2985
|
const permissions = requestPermissions(request, String(updated.workId));
|
|
2943
2986
|
data(response, redactAiConversation(updated, permissions));
|
|
2944
2987
|
});
|
|
2988
|
+
app.get("/api/characters/:characterId/roleplay-memories", (request, response) => {
|
|
2989
|
+
const query = parse(roleplayMemoryListQuerySchema, request.query);
|
|
2990
|
+
data(response, store.listRoleplayMemories(request.params.characterId, {
|
|
2991
|
+
query: query.q,
|
|
2992
|
+
categories: query.categories,
|
|
2993
|
+
statuses: query.statuses,
|
|
2994
|
+
cursor: query.cursor,
|
|
2995
|
+
limit: query.limit
|
|
2996
|
+
}));
|
|
2997
|
+
});
|
|
2998
|
+
app.post("/api/characters/:characterId/roleplay-memories", (request, response) => {
|
|
2999
|
+
const input = parse(roleplayMemoryInputSchema, request.body);
|
|
3000
|
+
data(response, store.createRoleplayMemory(request.params.characterId, input), 201);
|
|
3001
|
+
});
|
|
2945
3002
|
app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
|
|
2946
3003
|
const input = parse(z.object({
|
|
2947
3004
|
role: z.enum(["user", "assistant"]),
|
|
@@ -3004,6 +3061,130 @@ export function createRuntime(options) {
|
|
|
3004
3061
|
})
|
|
3005
3062
|
});
|
|
3006
3063
|
});
|
|
3064
|
+
app.patch("/api/roleplay-memories/:memoryId", (request, response) => {
|
|
3065
|
+
const input = parse(roleplayMemoryUpdateSchema, request.body);
|
|
3066
|
+
data(response, store.updateRoleplayMemory(request.params.memoryId, input));
|
|
3067
|
+
});
|
|
3068
|
+
app.delete("/api/roleplay-memories/:memoryId", (request, response) => {
|
|
3069
|
+
const input = parse(z.object({ expectedVersion: z.number().int().min(1) }).strict(), request.body);
|
|
3070
|
+
data(response, store.setRoleplayMemoryArchived(request.params.memoryId, true, input.expectedVersion));
|
|
3071
|
+
});
|
|
3072
|
+
app.post("/api/roleplay-memories/:memoryId/restore", (request, response) => {
|
|
3073
|
+
const input = parse(z.object({ expectedVersion: z.number().int().min(1) }).strict(), request.body);
|
|
3074
|
+
data(response, store.setRoleplayMemoryArchived(request.params.memoryId, false, input.expectedVersion));
|
|
3075
|
+
});
|
|
3076
|
+
// ---------------------------------------------------------------- AI 可写工具与审批中心
|
|
3077
|
+
const planViewer = () => {
|
|
3078
|
+
const actor = currentRequestActor();
|
|
3079
|
+
return actor ? { userId: actor.userId, role: actor.role } : null;
|
|
3080
|
+
};
|
|
3081
|
+
app.get("/api/works/:workId/ai/tools", (request, response) => {
|
|
3082
|
+
store.getWork(request.params.workId);
|
|
3083
|
+
data(response, {
|
|
3084
|
+
tools: aiWritePlanManager.getEnabledTools(request.params.workId),
|
|
3085
|
+
labels: aiWriteToolLabels,
|
|
3086
|
+
descriptions: aiWriteToolDescriptions,
|
|
3087
|
+
maxOperations: resolveAiWritePlanMaxOperations(process.env.AI_WRITE_PLAN_MAX_OPERATIONS)
|
|
3088
|
+
});
|
|
3089
|
+
});
|
|
3090
|
+
app.put("/api/works/:workId/ai/tools", (request, response) => {
|
|
3091
|
+
const input = parse(aiWriteToolsUpdateSchema, request.body ?? {});
|
|
3092
|
+
data(response, {
|
|
3093
|
+
tools: aiWritePlanManager.updateToolSettings(request.params.workId, input.tools, currentRequestActor()?.userId ?? null)
|
|
3094
|
+
});
|
|
3095
|
+
});
|
|
3096
|
+
app.get("/api/works/:workId/ai/write-plans", (request, response) => {
|
|
3097
|
+
const status = typeof request.query.status === "string"
|
|
3098
|
+
? parse(z.enum(AI_WRITE_PLAN_STATUSES), request.query.status)
|
|
3099
|
+
: undefined;
|
|
3100
|
+
const limit = Number.parseInt(typeof request.query.limit === "string" ? request.query.limit : "", 10);
|
|
3101
|
+
data(response, {
|
|
3102
|
+
plans: aiWritePlanManager.listPlansForWork(request.params.workId, planViewer(), {
|
|
3103
|
+
status,
|
|
3104
|
+
...(Number.isFinite(limit) ? { limit } : {})
|
|
3105
|
+
})
|
|
3106
|
+
});
|
|
3107
|
+
});
|
|
3108
|
+
app.get("/api/works/:workId/ai/write-plans/:planId", (request, response) => {
|
|
3109
|
+
data(response, aiWritePlanManager.getPlanDetail(request.params.planId, request.params.workId, planViewer()));
|
|
3110
|
+
});
|
|
3111
|
+
app.post("/api/works/:workId/ai/write-plans/:planId/confirm", async (request, response) => {
|
|
3112
|
+
data(response, await aiWritePlanManager.confirmPlan(request.params.planId, request.params.workId, planViewer()));
|
|
3113
|
+
});
|
|
3114
|
+
app.post("/api/works/:workId/ai/write-plans/:planId/reject", (request, response) => {
|
|
3115
|
+
data(response, aiWritePlanManager.rejectPlan(request.params.planId, request.params.workId, planViewer()));
|
|
3116
|
+
});
|
|
3117
|
+
app.post("/api/works/:workId/ai/write-plans/:planId/undo", (request, response) => {
|
|
3118
|
+
data(response, aiWritePlanManager.createUndoPlan(request.params.planId, request.params.workId, planViewer()), 201);
|
|
3119
|
+
});
|
|
3120
|
+
app.get("/api/works/:workId/ai/questions", (request, response) => {
|
|
3121
|
+
const conversationId = typeof request.query.conversationId === "string"
|
|
3122
|
+
? parse(identifier, request.query.conversationId)
|
|
3123
|
+
: undefined;
|
|
3124
|
+
const status = typeof request.query.status === "string"
|
|
3125
|
+
? parse(z.enum(AI_USER_QUESTION_STATUSES), request.query.status)
|
|
3126
|
+
: undefined;
|
|
3127
|
+
const limit = Number.parseInt(typeof request.query.limit === "string" ? request.query.limit : "", 10);
|
|
3128
|
+
data(response, {
|
|
3129
|
+
questions: aiWritePlanManager.listQuestions(request.params.workId, planViewer(), {
|
|
3130
|
+
...(conversationId !== undefined ? { conversationId } : {}),
|
|
3131
|
+
...(status !== undefined ? { status } : {}),
|
|
3132
|
+
...(Number.isFinite(limit) ? { limit } : {})
|
|
3133
|
+
})
|
|
3134
|
+
});
|
|
3135
|
+
});
|
|
3136
|
+
app.get("/api/works/:workId/ai/questions/:questionId", (request, response) => {
|
|
3137
|
+
data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, planViewer()));
|
|
3138
|
+
});
|
|
3139
|
+
const resumeQuestionWorkflow = async (questionId, workId, viewer) => {
|
|
3140
|
+
const continuation = aiWritePlanManager.claimQuestionContinuation(questionId, workId, viewer);
|
|
3141
|
+
if (!continuation)
|
|
3142
|
+
return;
|
|
3143
|
+
try {
|
|
3144
|
+
const conversationId = typeof continuation.conversationId === "string" ? continuation.conversationId : "";
|
|
3145
|
+
if (!conversationId)
|
|
3146
|
+
throw new AppError(409, "AI_QUESTION_CONTINUATION_MISSING", "提问缺少可恢复的对话状态");
|
|
3147
|
+
const scope = parse(contextSchema, continuation.scope ?? { type: "none" });
|
|
3148
|
+
const resumed = await ai.resumeUserQuestion({
|
|
3149
|
+
questionId,
|
|
3150
|
+
workId,
|
|
3151
|
+
conversationId,
|
|
3152
|
+
scope,
|
|
3153
|
+
status: String(continuation.status ?? "rejected"),
|
|
3154
|
+
answerText: String(continuation.answerText ?? ""),
|
|
3155
|
+
selectedOptionLabel: typeof continuation.selectedOptionLabel === "string" ? continuation.selectedOptionLabel : null,
|
|
3156
|
+
supplementalAnswer: typeof continuation.customAnswer === "string" ? continuation.customAnswer : "",
|
|
3157
|
+
...(typeof continuation.modelId === "string" && continuation.modelId ? { modelId: continuation.modelId } : {}),
|
|
3158
|
+
...(typeof continuation.toolCallId === "string" && continuation.toolCallId ? { toolCallId: continuation.toolCallId } : {}),
|
|
3159
|
+
...(typeof continuation.assistantMessageRequestId === "string" && continuation.assistantMessageRequestId
|
|
3160
|
+
? { assistantMessageRequestId: continuation.assistantMessageRequestId }
|
|
3161
|
+
: {}),
|
|
3162
|
+
...(continuation.questionView && typeof continuation.questionView === "object" && !Array.isArray(continuation.questionView)
|
|
3163
|
+
? { questionView: continuation.questionView }
|
|
3164
|
+
: {}),
|
|
3165
|
+
...(typeof continuation.round === "number" ? { round: continuation.round } : {}),
|
|
3166
|
+
...(Array.isArray(continuation.toolMessages) ? { toolMessages: continuation.toolMessages } : {})
|
|
3167
|
+
});
|
|
3168
|
+
aiWritePlanManager.finishQuestionContinuation(questionId, { callId: resumed.callId ?? null, completed: true });
|
|
3169
|
+
}
|
|
3170
|
+
catch (error) {
|
|
3171
|
+
aiWritePlanManager.finishQuestionContinuation(questionId, { message: error instanceof Error ? error.message : "恢复失败" }, true);
|
|
3172
|
+
throw error;
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3175
|
+
app.post("/api/works/:workId/ai/questions/:questionId/answer", async (request, response) => {
|
|
3176
|
+
const input = parse(answerAiUserQuestionSchema, request.body ?? {});
|
|
3177
|
+
const viewer = planViewer();
|
|
3178
|
+
aiWritePlanManager.answerQuestion(request.params.questionId, request.params.workId, viewer, { ...(input.selectedOption !== undefined ? { selectedOption: input.selectedOption } : {}), ...(input.customAnswer !== undefined ? { customAnswer: input.customAnswer } : {}) });
|
|
3179
|
+
await resumeQuestionWorkflow(request.params.questionId, request.params.workId, viewer);
|
|
3180
|
+
data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, viewer));
|
|
3181
|
+
});
|
|
3182
|
+
app.post("/api/works/:workId/ai/questions/:questionId/reject", async (request, response) => {
|
|
3183
|
+
const viewer = planViewer();
|
|
3184
|
+
aiWritePlanManager.rejectQuestion(request.params.questionId, request.params.workId, viewer);
|
|
3185
|
+
await resumeQuestionWorkflow(request.params.questionId, request.params.workId, viewer);
|
|
3186
|
+
data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, viewer));
|
|
3187
|
+
});
|
|
3007
3188
|
app.get("/api/works/:workId/providers", (request, response) => {
|
|
3008
3189
|
store.getWork(request.params.workId);
|
|
3009
3190
|
data(response, ai.listProviders());
|
|
@@ -3356,6 +3537,9 @@ export function createRuntime(options) {
|
|
|
3356
3537
|
const currentMessageId = String(begun.userMessage?.id ?? input.currentMessageId ?? "");
|
|
3357
3538
|
if (begun.userMessage)
|
|
3358
3539
|
sendEvent("user_message", { message: redactAiConversationMessage(begun.userMessage, permissions) });
|
|
3540
|
+
if (aiWritePlanManager.latestPendingQuestion(conversationId)) {
|
|
3541
|
+
throw new AppError(409, "AI_QUESTION_PENDING", "当前对话仍有待回答问题,请先回答或拒绝后再继续");
|
|
3542
|
+
}
|
|
3359
3543
|
const suggestion = await ai.createStreamingChat({
|
|
3360
3544
|
workId: request.params.workId,
|
|
3361
3545
|
instruction: resolvedInstruction,
|
|
@@ -3540,11 +3724,14 @@ export function createRuntime(options) {
|
|
|
3540
3724
|
});
|
|
3541
3725
|
if (options.serveUi ?? true) {
|
|
3542
3726
|
const publicPath = options.publicPath ?? join(process.cwd(), "src", "public");
|
|
3727
|
+
const publicApplicationModuleImports = extractStaticModuleImports(readFileSync(join(publicPath, "app.js"), "utf8"));
|
|
3728
|
+
// API 与 SSE 路由已经在此前注册;压缩只作用于页面和静态资源,避免缓冲流式响应。
|
|
3729
|
+
app.use(compression());
|
|
3543
3730
|
// index.html 按登录态动态下发:未登录时注入 login-route 类,首帧直接渲染登录页;
|
|
3544
3731
|
// 已登录时保持骨架屏,由前端恢复会话后进入工作台,避免两种闪烁。
|
|
3545
3732
|
const sendIndexHtml = (request, response) => {
|
|
3546
3733
|
const authenticated = options.disableUserAuth === true || auth.authenticate(request) !== null;
|
|
3547
|
-
let html = readFileSync(join(publicPath, "index.html"), "utf8");
|
|
3734
|
+
let html = injectModulePreloads(readFileSync(join(publicPath, "index.html"), "utf8"), publicApplicationModuleImports);
|
|
3548
3735
|
if (!authenticated)
|
|
3549
3736
|
html = html.replace('<html lang="zh-CN">', '<html lang="zh-CN" class="login-route">');
|
|
3550
3737
|
if (options.disableUserAuth === true) {
|