@musnows/scriverse 0.5.1 → 0.5.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/app.js CHANGED
@@ -28,7 +28,7 @@ import { currentRequestActor, runWithRequestActor } from "./request-context.js";
28
28
  import { APP_VERSION } from "./version.js";
29
29
  import { fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
30
30
  import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, pageLabelForKey, presencePageKinds } from "./collaboration-presence.js";
31
- import { clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, setSessionCookie, UserAuthService } from "./user-auth.js";
31
+ import { clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
32
32
  const nonEmpty = z.string().trim().min(1);
33
33
  const identifier = z.string().trim().min(1).max(200);
34
34
  const optionalStrings = z.array(z.string()).optional();
@@ -358,7 +358,7 @@ const contextSchema = z.object({
358
358
  });
359
359
  const analysisTaskTypeSchema = z.enum(["structure", "chapter-analysis", "character-extraction", "character-summary", "character-identity-audit", "timeline-analysis", "worldview-analysis", "setting-extraction", "consistency-check", "report-update", "book-analysis"]);
360
360
  const relationshipAnalysisScopeSchema = z.object({
361
- type: z.enum(["chapter", "book"]),
361
+ type: z.enum(["chapter", "book", "settings"]),
362
362
  chapterId: identifier.optional(),
363
363
  includeAllSettings: z.boolean().optional(),
364
364
  additionalPrompt: z.string().trim().max(10_000).optional(),
@@ -379,8 +379,8 @@ const relationshipAnalysisScopeSchema = z.object({
379
379
  }
380
380
  });
381
381
  const analysisTaskSchema = z.union([
382
- z.object({ taskType: z.literal("relationship-analysis"), scope: relationshipAnalysisScopeSchema.optional() }).strict(),
383
- z.object({ taskType: analysisTaskTypeSchema, scope: jsonObject.optional() }).strict().superRefine((input, context) => {
382
+ z.object({ taskType: z.literal("relationship-analysis"), scope: relationshipAnalysisScopeSchema.optional(), modelId: identifier.optional() }).strict(),
383
+ z.object({ taskType: analysisTaskTypeSchema, scope: jsonObject.optional(), modelId: identifier.optional() }).strict().superRefine((input, context) => {
384
384
  if (input.scope?.includeAllSettings !== undefined) {
385
385
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "includeAllSettings"], message: "包含所有设定仅支持人物关系分析" });
386
386
  }
@@ -434,16 +434,84 @@ function redactRaceMembers(record, permissions) {
434
434
  function redactOrganizationMembers(record, permissions) {
435
435
  return permissions.characters === "none" ? { ...record, memberIds: [], members: [] } : record;
436
436
  }
437
+ const taskResultPermissionModules = {
438
+ "chapter-analysis": "prose",
439
+ "character-extraction": "characters",
440
+ "character-summary": "characters",
441
+ "character-identity-audit": "reviews",
442
+ "timeline-analysis": "timeline",
443
+ "relationship-analysis": "relationships",
444
+ "worldview-analysis": "settings",
445
+ "setting-extraction": "settings",
446
+ "consistency-check": "reviews",
447
+ "book-analysis": "prose",
448
+ structure: "outlines",
449
+ "report-update": "prose"
450
+ };
451
+ function requiredTaskResultModule(taskType) {
452
+ return taskResultPermissionModules[String(taskType)];
453
+ }
437
454
  function redactTaskCharacterNames(record, permissions) {
438
455
  const { scopeSummaryWithoutCharacterNames, ...result } = record;
439
- if (permissions.characters !== "none")
440
- return result;
441
- if (typeof scopeSummaryWithoutCharacterNames === "string")
442
- result.scopeSummary = scopeSummaryWithoutCharacterNames;
443
- const scope = recordValue(result.scope);
444
- if (scope) {
445
- const { targetCharacters: _targetCharacters, ...redactedScope } = scope;
446
- result.scope = redactedScope;
456
+ const proseRestricted = permissions.prose === "none";
457
+ const proseScope = recordValue(result.scope);
458
+ const selectionScopeRestricted = proseRestricted
459
+ && (proseScope?.type === "selection" || String(result.scopeSummary ?? "").startsWith("选定内容:"));
460
+ if (selectionScopeRestricted) {
461
+ if (proseScope) {
462
+ const { selection: _selection, ...redactedScope } = proseScope;
463
+ result.scope = redactedScope;
464
+ }
465
+ result.scopeSummary = "选定内容(正文读取权限受限)";
466
+ if (Array.isArray(result.scopeDetails)) {
467
+ result.scopeDetails = result.scopeDetails.map((item) => {
468
+ const detail = recordValue(item);
469
+ return detail?.type === "selection" ? { type: "selection", restricted: true } : item;
470
+ });
471
+ }
472
+ }
473
+ const characterRestricted = permissions.characters === "none";
474
+ if (characterRestricted) {
475
+ if (!selectionScopeRestricted && typeof scopeSummaryWithoutCharacterNames === "string")
476
+ result.scopeSummary = scopeSummaryWithoutCharacterNames;
477
+ const scope = recordValue(result.scope);
478
+ if (scope) {
479
+ const { targetCharacters: _targetCharacters, ...redactedScope } = scope;
480
+ result.scope = redactedScope;
481
+ }
482
+ const taskResult = recordValue(result.result);
483
+ if (taskResult) {
484
+ const redactedTaskResult = { ...taskResult };
485
+ if (Array.isArray(taskResult.relationshipResults)) {
486
+ redactedTaskResult.relationshipResults = taskResult.relationshipResults.map((value) => {
487
+ const relationship = recordValue(value);
488
+ if (!relationship)
489
+ return value;
490
+ const { fromCharacterName: _fromCharacterName, toCharacterName: _toCharacterName, ...redactedRelationship } = relationship;
491
+ return redactedRelationship;
492
+ });
493
+ }
494
+ const analysisTarget = recordValue(taskResult.analysisTarget);
495
+ if (analysisTarget) {
496
+ const { characterNames: _characterNames, ...redactedAnalysisTarget } = analysisTarget;
497
+ redactedTaskResult.analysisTarget = redactedAnalysisTarget;
498
+ }
499
+ result.result = redactedTaskResult;
500
+ }
501
+ }
502
+ const resultSummary = recordValue(result.resultSummary);
503
+ const characterSensitiveTask = ["character-extraction", "character-summary", "character-identity-audit", "relationship-analysis"]
504
+ .includes(String(result.taskType));
505
+ const requiredResultModule = requiredTaskResultModule(result.taskType);
506
+ const resultRestricted = requiredResultModule !== undefined && permissions[requiredResultModule] === "none";
507
+ if (resultSummary && (resultRestricted || (characterRestricted && characterSensitiveTask))) {
508
+ result.resultSummary = {
509
+ ...resultSummary,
510
+ analysisContent: `${String(resultSummary.title ?? "AI 分析")};范围:${String(result.scopeSummary ?? "未指定")}`,
511
+ summary: "当前账号缺少相关资料的读取权限,无法展示对应的可读结论。",
512
+ sections: [],
513
+ restricted: true
514
+ };
447
515
  }
448
516
  return result;
449
517
  }
@@ -502,7 +570,24 @@ export function createRuntime(options) {
502
570
  return auth.workModulePermissions(request.authUser, resolvedWorkId, request.authMethod !== "api-key") ?? fullWorkModulePermissions();
503
571
  };
504
572
  const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
505
- const ai = new AiManager(store, new CredentialVault(options.masterSecret), options.fetchImpl ?? fetch, options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined);
573
+ const ai = new AiManager(store, new CredentialVault(options.masterSecret), options.fetchImpl ?? fetch, options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined, (task, actor) => {
574
+ if (task.taskType !== "relationship-analysis")
575
+ return;
576
+ const requiredModules = relationshipAnalysisReadModules(task.scope);
577
+ if (requiredModules.length === 0)
578
+ return;
579
+ const creator = actor ? null : database.get("SELECT created_by_user_id FROM analysis_tasks WHERE id = ?", String(task.id));
580
+ const userId = actor?.userId ?? (typeof creator?.created_by_user_id === "string" ? creator.created_by_user_id : null);
581
+ if (!userId)
582
+ return;
583
+ const user = auth.getUser(userId);
584
+ if (user.status !== "active")
585
+ throw new AppError(403, "WORK_ACCESS_DENIED", "任务创建者已无法访问这部作品");
586
+ auth.assertWorkAccess(user, String(task.workId), {
587
+ read: requiredModules,
588
+ write: ["ai-analysis"]
589
+ }, false, actor?.allowAdminAccess ?? false);
590
+ });
506
591
  const app = express();
507
592
  const upload = multer({
508
593
  storage: multer.memoryStorage(),
@@ -1349,20 +1434,54 @@ export function createRuntime(options) {
1349
1434
  });
1350
1435
  app.post("/api/works/:workId/tasks", (request, response) => {
1351
1436
  const input = parse(analysisTaskSchema, request.body);
1352
- data(response, redactTaskCharacterNames(store.createTask(request.params.workId, input), requestPermissions(request, request.params.workId)), 201);
1437
+ data(response, redactTaskCharacterNames(ai.createTask(request.params.workId, input), requestPermissions(request, request.params.workId)), 201);
1353
1438
  });
1354
1439
  app.post("/api/works/:workId/tasks/auto-run", (request, response) => {
1440
+ const permissions = requestPermissions(request, request.params.workId);
1441
+ for (const taskId of store.listOldestPendingTaskIds(request.params.workId, store.countPendingTasks(request.params.workId))) {
1442
+ const task = store.getTask(taskId);
1443
+ if (task.taskType !== "relationship-analysis")
1444
+ continue;
1445
+ const deniedModules = relationshipAnalysisReadModules(task.scope).filter((module) => permissions[module] === "none");
1446
+ if (deniedModules.length > 0) {
1447
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取待运行定向人物关系分析所需资料模块的权限", {
1448
+ taskId,
1449
+ modules: deniedModules
1450
+ });
1451
+ }
1452
+ }
1355
1453
  data(response, ai.startAutoRunBatch(request.params.workId));
1356
1454
  });
1455
+ app.get("/api/tasks/:taskId/detail", (request, response) => data(response, redactTaskCharacterNames(store.getTaskDetail(request.params.taskId), requestPermissions(request))));
1456
+ app.get("/api/tasks/:taskId/result", (request, response) => {
1457
+ const task = store.getTaskResultPayload(request.params.taskId);
1458
+ const permissions = requestPermissions(request);
1459
+ const requiredModule = requiredTaskResultModule(task.taskType);
1460
+ if (requiredModule && permissions[requiredModule] === "none") {
1461
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取该分析结果对应资料模块的权限");
1462
+ }
1463
+ const redacted = redactTaskCharacterNames(task, permissions);
1464
+ data(response, { taskId: task.id, result: redacted.result });
1465
+ });
1357
1466
  app.get("/api/tasks/:taskId", (request, response) => data(response, redactTaskCharacterNames(store.getTask(request.params.taskId), requestPermissions(request))));
1358
1467
  app.get("/api/tasks/:taskId/trace", (request, response) => data(response, ai.getTaskTrace(request.params.taskId)));
1359
- app.get("/api/tasks/:taskId/trace/calls/:callId", (request, response) => {
1360
- const full = parse(z.enum(["true", "false"]).default("false"), request.query.full ?? "false") === "true";
1361
- data(response, ai.getTaskTraceCall(request.params.taskId, request.params.callId, full));
1362
- });
1468
+ app.get("/api/tasks/:taskId/trace/calls/:callId", (request, response) => data(response, ai.getTaskTraceCall(request.params.taskId, request.params.callId)));
1363
1469
  app.post("/api/tasks/:taskId/run", async (request, response) => {
1364
1470
  const input = parse(z.object({ modelId: identifier.optional() }), request.body ?? {});
1365
- data(response, redactTaskCharacterNames(await ai.runTask(request.params.taskId, input.modelId), requestPermissions(request)));
1471
+ const task = store.getTask(request.params.taskId);
1472
+ if (task.taskType === "relationship-analysis") {
1473
+ const permissions = requestPermissions(request, String(task.workId));
1474
+ const deniedModules = relationshipAnalysisReadModules(task.scope).filter((module) => permissions[module] === "none");
1475
+ if (deniedModules.length > 0) {
1476
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取本次定向人物关系分析所需资料模块的权限", {
1477
+ modules: deniedModules
1478
+ });
1479
+ }
1480
+ }
1481
+ data(response, redactTaskCharacterNames(await ai.runTask(request.params.taskId, input.modelId, request.authUser ? {
1482
+ userId: request.authUser.userId,
1483
+ allowAdminAccess: request.authMethod !== "api-key"
1484
+ } : undefined), requestPermissions(request)));
1366
1485
  });
1367
1486
  app.post("/api/tasks/:taskId/cancel", (request, response) => data(response, redactTaskCharacterNames(ai.cancelTask(request.params.taskId), requestPermissions(request))));
1368
1487
  app.get("/api/platform/ai/providers", (request, response) => {