@musnows/scriverse 1.0.12 → 1.1.0
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 -0
- package/README.md +3 -0
- package/dist/ai-stream-steer.js +96 -0
- package/dist/ai-stream-steer.js.map +1 -0
- package/dist/ai-write-plans.js +44 -38
- package/dist/ai-write-plans.js.map +1 -1
- package/dist/ai.js +241 -128
- package/dist/ai.js.map +1 -1
- package/dist/app.js +29 -5
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +3 -0
- package/dist/attachment-storage.js.map +1 -1
- package/dist/chapter-annotation-note.js +14 -0
- package/dist/chapter-annotation-note.js.map +1 -0
- package/dist/character-structured-fields.js +7 -0
- package/dist/character-structured-fields.js.map +1 -0
- package/dist/cli-contract.js +1 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/im.js +2 -1
- package/dist/im.js.map +1 -1
- package/dist/public/ai-prompt-keyboard.js +5 -1
- package/dist/public/ai-prompt-queue.js +226 -0
- package/dist/public/ai-send-mode.js +31 -0
- package/dist/public/app.js +564 -37
- package/dist/public/character-profile.d.ts +7 -0
- package/dist/public/character-profile.js +123 -5
- package/dist/public/index.html +11 -4
- package/dist/public/styles.css +28 -1
- package/dist/server-runtime.js +2 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +7 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
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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"}
|
package/dist/ai-write-plans.js
CHANGED
|
@@ -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:
|
|
121
|
+
attributes: characterAttributesInputSchema.optional().describe("身份与扩展属性。只使用 identity 字符串和 details[{label,value}];其他键会在入库时折入 details。"),
|
|
120
122
|
profile: jsonObjectSchema.optional(),
|
|
121
|
-
currentState:
|
|
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:
|
|
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
|
-
|
|
357
|
-
z.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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 =
|
|
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
|
// 先统一做逐操作校验,收集标题与详情。
|