@musnows/scriverse 1.0.12 → 1.1.1

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 CHANGED
@@ -109,6 +109,7 @@ Run `scriverse --help` for all local server, default server, authentication, wor
109
109
  | `SCRIVERSE_AI_RETRY_COUNT` | `3` | Retry count for AI upstream HTTP errors other than `403`, `429`, and `502`; valid integers are clamped to `1`–`20` |
110
110
  | `SCRIVERSE_AI_BACKOFF_RETRY_COUNT` | `10` | Backoff retry count when an AI upstream returns `429` or `502`; valid integers are clamped to `1`–`20` |
111
111
  | `SCRIVERSE_AI_STREAM_IDLE_TIMEOUT_SECONDS` | `30` | Maximum idle time while an interactive AI stream waits for its first or next valid event; valid integers are clamped to `10`–`120`, and invalid values fall back to `30` |
112
+ | `SCRIVERSE_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH` | `6000` | Maximum character length for chapter comments and todos; valid integers are clamped to `2000`–`20000`, values below `2000` are treated as `2000`, and invalid values fall back to `6000` |
112
113
  | `APP_AUTH_USERNAME` | Empty | Optional deployment gateway username; the in-app user system is always enabled |
113
114
  | `APP_AUTH_PASSWORD` | Empty | Optional deployment gateway password, at least 12 characters; must be transported over HTTPS |
114
115
  | `APP_TRUST_PROXY` | `false` | Set to the trusted proxy hop count (usually `1`) or `true` behind a trusted reverse proxy |
@@ -120,6 +121,8 @@ Run `scriverse --help` for all local server, default server, authentication, wor
120
121
 
121
122
  `SCRIVERSE_AI_STREAM_IDLE_TIMEOUT_SECONDS` is read when the service starts and only controls how long an interactive AI stream may remain without a new event. Every valid stream event restarts the timer, so generation may continue beyond 60 seconds; this setting does not impose a total-duration limit or change timeout behavior for analysis tasks and other AI requests. Restart the service after changing it.
122
123
 
124
+ `SCRIVERSE_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH` is read when the service starts and applies to API validation, the comment input, and AI write plans. Restart the service after changing it.
125
+
123
126
  The AI upstream HTTP retry settings are read when the service starts. `403` is never retried. `429` and `502` use exponential backoff starting at 500 milliseconds and capped at 5 seconds, honoring a numeric `Retry-After` within the same cap; other HTTP errors use a linear delay. Invalid values fall back to their defaults. Restart the service after changing either setting.
124
127
 
125
128
  Custom configuration example:
package/README.md CHANGED
@@ -118,6 +118,7 @@ CLI 会按服务器保存登录凭据。所有连接服务的数据命令都可
118
118
  | `SCRIVERSE_AI_BACKOFF_RETRY_COUNT` | `10` | AI 上游返回 `429` 或 `502` 时的退避重试次数;有效整数按 `1`–`20` 钳制 |
119
119
  | `SCRIVERSE_AI_STREAM_IDLE_TIMEOUT_SECONDS` | `30` | 交互式 AI 流等待首个或下一个有效事件的最长空闲秒数;有效整数按 `10`–`120` 钳制,非法值回退为 `30` |
120
120
  | `APP_AI_CHAT_TAB_LIMIT` | `5` | 浏览器中可同时打开的 Agent 对话数;有效整数按 `1`–`20` 钳制,设为 `1` 时关闭多会话切换和工作台 |
121
+ | `SCRIVERSE_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH` | `6000` | 正文评论和待办内容的最大字符数;有效整数按 `2000`–`20000` 钳制,低于 `2000` 按 `2000` 处理,非法值回退为 `6000` |
121
122
  | `APP_AUTH_USERNAME` | 空 | 可选的部署网关账号;应用内用户系统始终启用 |
122
123
  | `APP_AUTH_PASSWORD` | 空 | 可选的部署网关密码,至少 12 个字符;必须通过 HTTPS 传输 |
123
124
  | `APP_TRUST_PROXY` | `false` | 位于可信反向代理后时设为代理跳数(通常为 `1`)或 `true` |
@@ -137,6 +138,8 @@ CLI 会按服务器保存登录凭据。所有连接服务的数据命令都可
137
138
 
138
139
  `SCRIVERSE_AI_STREAM_IDLE_TIMEOUT_SECONDS` 在服务启动时读取,只控制交互式 AI 流连续没有新事件的等待时间。每收到一个有效流事件都会重新计时,持续生成超过 60 秒不会因此中断;该配置不设置总时长上限,也不改变分析任务等其他 AI 请求的超时策略。修改后需重启服务生效。
139
140
 
141
+ `SCRIVERSE_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH` 在服务启动时读取,同时约束 API、界面输入和 AI 写计划中的正文评论与待办。修改后需重启服务生效。
142
+
140
143
  AI 上游 HTTP 重试配置在服务启动时读取。`403` 始终不重试;`429` 和 `502` 使用指数退避,等待从 500 毫秒开始并在 5 秒封顶,同时遵循秒数格式的 `Retry-After`(同样最多等待 5 秒);其余 HTTP 错误使用线性等待。非法配置回退到对应默认值,修改后需重启服务生效。
141
144
 
142
145
  自定义示例:
@@ -0,0 +1,96 @@
1
+ import { AppError } from "./errors.js";
2
+ import { id, now } from "./utils.js";
3
+ export const AI_STEER_MAX_CHARS = 100_000;
4
+ export const AI_STEER_INSTRUCTION_HEADING = "【执行流引导】";
5
+ export function createAiSteerRefreshError() {
6
+ return new AppError(409, "AI_STEER_REFRESH", "收到执行流引导,正在把当前回复交给引导继续");
7
+ }
8
+ export function isAiSteerRefreshError(error) {
9
+ return error instanceof AppError && error.code === "AI_STEER_REFRESH";
10
+ }
11
+ export function normalizeAiSteerContent(value) {
12
+ return String(value ?? "").trim();
13
+ }
14
+ export function formatAiSteerInstruction(content) {
15
+ const normalized = normalizeAiSteerContent(content);
16
+ return `${AI_STEER_INSTRUCTION_HEADING}\n作者在当前回答尚未结束时补充了以下引导,请立即按此调整后续执行,不要重新开始整个任务:\n\n${normalized}`;
17
+ }
18
+ export class AiStreamSteerMailbox {
19
+ active = new Set();
20
+ pending = new Map();
21
+ waiters = new Map();
22
+ begin(conversationId) {
23
+ const key = String(conversationId ?? "").trim();
24
+ if (!key)
25
+ throw new AppError(400, "AI_STEER_CONVERSATION_REQUIRED", "执行流引导必须绑定对话");
26
+ this.active.add(key);
27
+ if (!this.pending.has(key))
28
+ this.pending.set(key, []);
29
+ return () => this.end(key);
30
+ }
31
+ isActive(conversationId) {
32
+ return this.active.has(String(conversationId ?? "").trim());
33
+ }
34
+ enqueue(conversationId, content) {
35
+ const key = String(conversationId ?? "").trim();
36
+ const normalized = normalizeAiSteerContent(content);
37
+ if (!key)
38
+ throw new AppError(400, "AI_STEER_CONVERSATION_REQUIRED", "执行流引导必须绑定对话");
39
+ if (!normalized)
40
+ throw new AppError(400, "AI_STEER_EMPTY", "请输入引导内容");
41
+ if (normalized.length > AI_STEER_MAX_CHARS) {
42
+ throw new AppError(400, "AI_STEER_TOO_LONG", `引导内容不能超过 ${AI_STEER_MAX_CHARS} 个字符`);
43
+ }
44
+ if (!this.active.has(key)) {
45
+ throw new AppError(409, "AI_STEER_NO_ACTIVE_STREAM", "当前对话没有正在执行的回复,无法发送引导");
46
+ }
47
+ const entry = {
48
+ id: id("steer"),
49
+ conversationId: key,
50
+ content: normalized,
51
+ createdAt: now()
52
+ };
53
+ const queue = this.pending.get(key) ?? [];
54
+ queue.push(entry);
55
+ this.pending.set(key, queue);
56
+ this.signal(key);
57
+ return entry;
58
+ }
59
+ drain(conversationId) {
60
+ const key = String(conversationId ?? "").trim();
61
+ const items = this.pending.get(key) ?? [];
62
+ this.pending.set(key, []);
63
+ return items;
64
+ }
65
+ subscribe(conversationId, controller) {
66
+ const key = String(conversationId ?? "").trim();
67
+ if (!key)
68
+ return () => undefined;
69
+ let waiters = this.waiters.get(key);
70
+ if (!waiters) {
71
+ waiters = new Set();
72
+ this.waiters.set(key, waiters);
73
+ }
74
+ waiters.add(controller);
75
+ return () => {
76
+ waiters?.delete(controller);
77
+ if (waiters && waiters.size === 0)
78
+ this.waiters.delete(key);
79
+ };
80
+ }
81
+ signal(conversationId) {
82
+ const waiters = this.waiters.get(conversationId);
83
+ if (!waiters)
84
+ return;
85
+ for (const controller of waiters) {
86
+ if (!controller.signal.aborted)
87
+ controller.abort(createAiSteerRefreshError());
88
+ }
89
+ }
90
+ end(conversationId) {
91
+ this.active.delete(conversationId);
92
+ this.pending.delete(conversationId);
93
+ this.waiters.delete(conversationId);
94
+ }
95
+ }
96
+ //# sourceMappingURL=ai-stream-steer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-stream-steer.js","sourceRoot":"","sources":["../src/ai-stream-steer.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,8 @@ import { analysisTaskReadModules } from "./user-auth.js";
3
3
  import { AppError } from "./errors.js";
4
4
  import { logger } from "./logger.js";
5
5
  import { id as randomId, json, now } from "./utils.js";
6
+ import { characterAttributesInputSchema, characterStateInputSchema } from "./character-structured-fields.js";
7
+ import { DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH } from "./chapter-annotation-note.js";
6
8
  import { canReadWorkModule, canWriteWorkModule, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions, workPermissionModuleLabels } from "./work-permissions.js";
7
9
  /**
8
10
  * AI 可写工具与持久化审批工作流。
@@ -116,9 +118,9 @@ const characterInputSchema = z.object({
116
118
  aliases: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
117
119
  raceId: identifierSchema.nullable().optional(),
118
120
  organizationIds: z.array(identifierSchema).max(100).optional(),
119
- attributes: jsonObjectSchema.optional(),
121
+ attributes: characterAttributesInputSchema.optional().describe("身份与扩展属性。只使用 identity 字符串和 details[{label,value}];其他键会在入库时折入 details。"),
120
122
  profile: jsonObjectSchema.optional(),
121
- currentState: jsonObjectSchema.optional()
123
+ currentState: characterStateInputSchema.optional()
122
124
  }).strict();
123
125
  /** 种族可写字段:成员归属与分节设定结构保留给人工。 */
124
126
  const raceInputSchema = z.object({
@@ -247,7 +249,7 @@ function toolInputJsonSchema(schema) {
247
249
  * 每个操作类型和实体类型都使用独立分支,避免通用对象把 entityId、scope 等
248
250
  * 仅属于其他操作的字段错误地暴露给 create_entry。
249
251
  */
250
- export function aiWritePlanOperationToolSchemas(toggles) {
252
+ export function aiWritePlanOperationToolSchemas(toggles, chapterAnnotationNoteMaxLength = DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH) {
251
253
  const entityTypes = [
252
254
  ...(toggles.settings ? ["setting"] : []),
253
255
  ...(toggles.characters ? ["character"] : []),
@@ -300,7 +302,7 @@ export function aiWritePlanOperationToolSchemas(toggles) {
300
302
  kind: { type: "string", enum: ["note", "todo"] },
301
303
  startLine: { type: "integer", minimum: 1 },
302
304
  endLine: { type: "integer", minimum: 1 },
303
- note: { type: "string", minLength: 1, maxLength: 2000 }
305
+ note: { type: "string", minLength: 1, maxLength: chapterAnnotationNoteMaxLength }
304
306
  },
305
307
  required: ["opType", "chapterId", "kind", "startLine", "endLine", "note"],
306
308
  additionalProperties: false
@@ -353,37 +355,39 @@ export const aiAnalysisTaskTypeLabels = {
353
355
  "book-analysis": "整书分析",
354
356
  "relationship-analysis": "人物关系分析"
355
357
  };
356
- const createOperationSchema = z.discriminatedUnion("opType", [
357
- z.object({
358
- opType: z.literal("create_entry"),
359
- entityType: entryEntityTypeSchema,
360
- /** 章节大纲按章节定位。 */
361
- chapterId: identifierSchema.optional(),
362
- input: z.unknown()
363
- }).strict(),
364
- z.object({
365
- opType: z.literal("update_entry"),
366
- entityType: entryEntityTypeSchema,
367
- entityId: identifierSchema.optional(),
368
- chapterId: identifierSchema.optional(),
369
- input: z.unknown()
370
- }).strict(),
371
- z.object({
372
- opType: z.literal("create_annotation"),
373
- chapterId: identifierSchema,
374
- kind: z.enum(["note", "todo"]),
375
- startLine: z.number().int().positive(),
376
- endLine: z.number().int().positive(),
377
- note: z.string().trim().min(1).max(2000)
378
- }).strict(),
379
- z.object({
380
- opType: z.literal("create_task"),
381
- taskType: z.enum([...AI_ANALYSIS_TASK_TYPES, "relationship-analysis"]),
382
- scope: jsonObjectSchema.optional(),
383
- modelId: identifierSchema.optional()
384
- }).strict()
385
- ]);
386
- const planOperationsSchema = z.array(createOperationSchema).min(1);
358
+ function createPlanOperationSchema(chapterAnnotationNoteMaxLength) {
359
+ return z.discriminatedUnion("opType", [
360
+ z.object({
361
+ opType: z.literal("create_entry"),
362
+ entityType: entryEntityTypeSchema,
363
+ /** 章节大纲按章节定位。 */
364
+ chapterId: identifierSchema.optional(),
365
+ input: z.unknown()
366
+ }).strict(),
367
+ z.object({
368
+ opType: z.literal("update_entry"),
369
+ entityType: entryEntityTypeSchema,
370
+ entityId: identifierSchema.optional(),
371
+ chapterId: identifierSchema.optional(),
372
+ input: z.unknown()
373
+ }).strict(),
374
+ z.object({
375
+ opType: z.literal("create_annotation"),
376
+ chapterId: identifierSchema,
377
+ kind: z.enum(["note", "todo"]),
378
+ startLine: z.number().int().positive(),
379
+ endLine: z.number().int().positive(),
380
+ note: z.string().trim().min(1).max(chapterAnnotationNoteMaxLength)
381
+ }).strict(),
382
+ z.object({
383
+ opType: z.literal("create_task"),
384
+ taskType: z.enum([...AI_ANALYSIS_TASK_TYPES, "relationship-analysis"]),
385
+ scope: jsonObjectSchema.optional(),
386
+ modelId: identifierSchema.optional()
387
+ }).strict()
388
+ ]);
389
+ }
390
+ const createOperationSchema = createPlanOperationSchema(DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH);
387
391
  export const createAiWritePlanInputSchema = z.object({
388
392
  aiSummary: z.string().trim().min(1).max(2000),
389
393
  operations: z.unknown()
@@ -851,8 +855,8 @@ export function planOperationRequirements(operation) {
851
855
  }
852
856
  return { toolId: toolId, writeModules: [...writeModules], readModules: [...readModules] };
853
857
  }
854
- export function normalizePlanOperations(rawOperations, maxOperations) {
855
- const rawList = planOperationsSchema.max(maxOperations, `单次计划最多包含 ${maxOperations} 个操作`).safeParse(rawOperations);
858
+ export function normalizePlanOperations(rawOperations, maxOperations, chapterAnnotationNoteMaxLength = DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH) {
859
+ const rawList = z.array(createPlanOperationSchema(chapterAnnotationNoteMaxLength)).min(1).max(maxOperations, `单次计划最多包含 ${maxOperations} 个操作`).safeParse(rawOperations);
856
860
  if (!rawList.success) {
857
861
  const firstIssue = rawList.error.issues[0];
858
862
  throw new AppError(400, "AI_PLAN_OPERATION_INVALID", `${firstIssue?.path.join(".") || "operation"}:${firstIssue?.message ?? "操作格式无效"}`);
@@ -997,6 +1001,7 @@ export class AiWritePlanManager {
997
1001
  startAnalysisTask;
998
1002
  planTtlMs;
999
1003
  questionTtlMs;
1004
+ chapterAnnotationNoteMaxLength;
1000
1005
  constructor(deps, options = {}) {
1001
1006
  this.database = deps.database;
1002
1007
  this.store = deps.store;
@@ -1005,6 +1010,7 @@ export class AiWritePlanManager {
1005
1010
  this.startAnalysisTask = deps.startAnalysisTask;
1006
1011
  this.planTtlMs = options.planTtlMs ?? AI_WRITE_PLAN_TTL_MS;
1007
1012
  this.questionTtlMs = options.questionTtlMs ?? AI_USER_QUESTION_TTL_MS;
1013
+ this.chapterAnnotationNoteMaxLength = options.chapterAnnotationNoteMaxLength ?? DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH;
1008
1014
  this.recoverStaleExecutingPlans();
1009
1015
  }
1010
1016
  // --------------------------------------------------------------- 工具开关
@@ -1237,7 +1243,7 @@ export class AiWritePlanManager {
1237
1243
  if (!summaryParse.success)
1238
1244
  throw new AppError(400, "AI_PLAN_SUMMARY_REQUIRED", "AI 必须提供一段简要说明");
1239
1245
  const maxOperations = resolveAiWritePlanMaxOperations(process.env.AI_WRITE_PLAN_MAX_OPERATIONS);
1240
- const operations = normalizePlanOperations(input.operations, maxOperations);
1246
+ const operations = normalizePlanOperations(input.operations, maxOperations, this.chapterAnnotationNoteMaxLength);
1241
1247
  const toggles = this.getEnabledTools(workId);
1242
1248
  const effective = this.effectiveWritePermissions(input.initiator, input.conversationOwnerUserId, workId);
1243
1249
  // 先统一做逐操作校验,收集标题与详情。