@musnows/scriverse 1.1.2 → 1.1.4
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-response-limit.js +12 -0
- package/dist/ai-response-limit.js.map +1 -0
- package/dist/ai.js +18 -12
- package/dist/ai.js.map +1 -1
- package/dist/app.js +47 -4
- package/dist/app.js.map +1 -1
- package/dist/chapter-title-numbering.js +210 -0
- package/dist/chapter-title-numbering.js.map +1 -1
- package/dist/cli-contract.js +15 -6
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +37 -1
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +21 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +374 -15
- package/dist/public/index.html +12 -2
- package/dist/public/styles.css +3 -2
- package/dist/store.js +190 -10
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +5 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +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_AI_RESPONSE_MAX_BYTES` | `0` (unlimited) | Byte limit for AI provider and Desktop local AI responses; a positive safe integer enables the limit, while unset, `0`, and invalid values mean unlimited |
|
|
112
113
|
| `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` |
|
|
113
114
|
| `APP_AUTH_USERNAME` | Empty | Optional deployment gateway username; the in-app user system is always enabled |
|
|
114
115
|
| `APP_AUTH_PASSWORD` | Empty | Optional deployment gateway password, at least 12 characters; must be transported over HTTPS |
|
|
@@ -121,6 +122,8 @@ Run `scriverse --help` for all local server, default server, authentication, wor
|
|
|
121
122
|
|
|
122
123
|
`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.
|
|
123
124
|
|
|
125
|
+
`SCRIVERSE_AI_RESPONSE_MAX_BYTES` controls the byte limit for streamed and regular AI provider responses, as well as Desktop local AI responses. The limit is disabled by default; unset, `0`, and invalid values mean unlimited. Set a positive safe integer to enable a byte limit. Restart the service after changing it.
|
|
126
|
+
|
|
124
127
|
`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
128
|
|
|
126
129
|
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.
|
package/README.md
CHANGED
|
@@ -117,6 +117,7 @@ CLI 会按服务器保存登录凭据。所有连接服务的数据命令都可
|
|
|
117
117
|
| `SCRIVERSE_AI_RETRY_COUNT` | `3` | AI 上游返回除 `403`、`429`、`502` 外的 HTTP 错误时的重试次数;有效整数按 `1`–`20` 钳制 |
|
|
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
|
+
| `SCRIVERSE_AI_RESPONSE_MAX_BYTES` | `0`(默认不限) | AI 供应商和 Desktop 本地 AI 响应的字节上限;正安全整数启用,未设置、`0` 或非法值表示不限 |
|
|
120
121
|
| `APP_AI_CHAT_TAB_LIMIT` | `5` | 浏览器中可同时打开的 Agent 对话数;有效整数按 `1`–`20` 钳制,设为 `1` 时关闭多会话切换和工作台 |
|
|
121
122
|
| `SCRIVERSE_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH` | `6000` | 正文评论和待办内容的最大字符数;有效整数按 `2000`–`20000` 钳制,低于 `2000` 按 `2000` 处理,非法值回退为 `6000` |
|
|
122
123
|
| `APP_AUTH_USERNAME` | 空 | 可选的部署网关账号;应用内用户系统始终启用 |
|
|
@@ -138,6 +139,8 @@ CLI 会按服务器保存登录凭据。所有连接服务的数据命令都可
|
|
|
138
139
|
|
|
139
140
|
`SCRIVERSE_AI_STREAM_IDLE_TIMEOUT_SECONDS` 在服务启动时读取,只控制交互式 AI 流连续没有新事件的等待时间。每收到一个有效流事件都会重新计时,持续生成超过 60 秒不会因此中断;该配置不设置总时长上限,也不改变分析任务等其他 AI 请求的超时策略。修改后需重启服务生效。
|
|
140
141
|
|
|
142
|
+
`SCRIVERSE_AI_RESPONSE_MAX_BYTES` 控制 AI 供应商流式响应、普通响应和 Desktop 本地 AI 响应的字节上限。默认关闭上限;未设置、设为 `0` 或非法值时不限,设为正安全整数后按该字节数限制。修改后需重启服务生效。
|
|
143
|
+
|
|
141
144
|
`SCRIVERSE_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH` 在服务启动时读取,同时约束 API、界面输入和 AI 写计划中的正文评论与待办。修改后需重启服务生效。
|
|
142
145
|
|
|
143
146
|
AI 上游 HTTP 重试配置在服务启动时读取。`403` 始终不重试;`429` 和 `502` 使用指数退避,等待从 500 毫秒开始并在 5 秒封顶,同时遵循秒数格式的 `Retry-After`(同样最多等待 5 秒);其余 HTTP 错误使用线性等待。非法配置回退到对应默认值,修改后需重启服务生效。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const AI_RESPONSE_MAX_BYTES_ENV = "SCRIVERSE_AI_RESPONSE_MAX_BYTES";
|
|
2
|
+
export function resolveAiResponseMaxBytes(environment = process.env) {
|
|
3
|
+
const raw = environment[AI_RESPONSE_MAX_BYTES_ENV]?.trim() ?? "";
|
|
4
|
+
if (!/^\d+$/u.test(raw))
|
|
5
|
+
return null;
|
|
6
|
+
const configured = Number(raw);
|
|
7
|
+
return Number.isSafeInteger(configured) && configured > 0 ? configured : null;
|
|
8
|
+
}
|
|
9
|
+
export function isAiResponseByteLimitExceeded(receivedBytes, maximumBytes) {
|
|
10
|
+
return maximumBytes !== null && receivedBytes > maximumBytes;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=ai-response-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-response-limit.js","sourceRoot":"","sources":["../src/ai-response-limit.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,yBAAyB,GAAG,iCAAiC,CAAC;AAE3E,MAAM,UAAU,yBAAyB,CAAC,cAAiC,OAAO,CAAC,GAAG;IACpF,MAAM,GAAG,GAAG,WAAW,CAAC,yBAAyB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACjE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,aAAqB,EAAE,YAA2B;IAC9F,OAAO,YAAY,KAAK,IAAI,IAAI,aAAa,GAAG,YAAY,CAAC;AAC/D,CAAC"}
|
package/dist/ai.js
CHANGED
|
@@ -8,6 +8,7 @@ import { AiConnectivityTestGate, hashAiConnectivityConfiguration } from "./ai-co
|
|
|
8
8
|
import { aiHttpRetryCount, aiHttpRetryDelayMs, normalizeAiRetryPolicy } from "./ai-retry.js";
|
|
9
9
|
import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_MS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
|
|
10
10
|
import { DEFAULT_AI_CHAT_IMAGE_MAX_BYTES, formatUploadLimit } from "./upload-limits.js";
|
|
11
|
+
import { isAiResponseByteLimitExceeded, resolveAiResponseMaxBytes } from "./ai-response-limit.js";
|
|
11
12
|
import { characterExtractionHash, characterExtractionSelectionFingerprint, editableCharacterExtractionCandidate, normalizeCharacterExtractionCandidate, parseStoredCharacterExtractionCandidates } from "./character-extraction.js";
|
|
12
13
|
import { AI_WRITE_TOOL_IDS, aiWritePlanOperationToolSchemas, askAiUserQuestionInputSchema } from "./ai-write-plans.js";
|
|
13
14
|
import { DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH } from "./chapter-annotation-note.js";
|
|
@@ -169,15 +170,18 @@ function completionSkillsTokens(messages) {
|
|
|
169
170
|
// A small but non-transparent 128x128 PNG. The model test must exercise an actual image_url
|
|
170
171
|
// payload, while keeping the request cheap and avoiding any user data in the probe.
|
|
171
172
|
const MULTIMODAL_TEST_IMAGE_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAPoAAAD6AG1e1JrAAACfklEQVR4nO2cwY3EQBACJ8LOglRJyw4DJOpR/xOUuF17Zp91H9xsBi/9B8AhABIcC4AEx78AJDg+AyDB8SEQCY5vAUhwfA1EguM5ABIcD4KQ4HgSiATHo2AkON4FIMHxMggJjreBSHC8DkaC4zwAEhwHQpDgOBGEBMeRMCQ4zgQiwXEoFAmOU8FIcBwLR4LjXgASHBdDkOC4GYQEx9UwJDjuBiLBcTkUCY7bwUhwXA8319P5fQCPS8APRChfAgIUBOFRWADlS0CAgiA8CgugfAkIUBCER2EBlC8BAQqC8CgsgPIlIEBBEB6FBVC+BAQoCMKjsADKl4AABUF4FBZA+RIQoCAIj8ICKF8CAhQE4VFYAOVLQICCIDwKC6B8CQhQEIRHYQGULwEBCoLwKCyA8iUgQEEQHoUFUL4EBCgIwqOwAMqXgAAFQXgUFkD5EhCgIAiPwgIoXwICFAThUVgA5UtAgIIgPAoLoHwJCFAQhEdhAZQvAQEKgvAoLIDyJSBAQRAehQVQvgQEKAjCo7AAypeAAAVBeBQWQPkSEKAgCI/CAihfAgIUBOFRWADlS0CAgiA8CgugfAkIUBCER2EBlC8BAQqC8CgsgPIlIEBBEB6FBVC+BAQoCMKjsADKl4AABUF4FBZA+RIQoCAIj8ICKF8CAhQE4VFYAOVLQICCIDwKC6B8CQhQEIRHYQGULwEBCoLwKCyA8iUgQEEQHoUFUL4EBCgIwqOwAMqXgAAFQXgUFkD5EhCgIAiPwgIoXwICFAThUVgA5UtAgIIgPAoLoHwJCFAQhEdhAZQvAQEKgvAoLIDyJSBAQRAehQVQvgQEKAjCo7AAypeAAAVBeJQfFY4JQ620WGEAAAAASUVORK5CYII=";
|
|
172
|
-
|
|
173
|
-
export const AI_RESPONSE_MAX_BYTES = 20 * 1024 * 1024;
|
|
174
|
-
export async function readResponseTextLimited(response, maximumBytes = AI_RESPONSE_MAX_BYTES) {
|
|
173
|
+
export async function readResponseTextLimited(response, maximumBytes = resolveAiResponseMaxBytes()) {
|
|
175
174
|
const declared = response.headers.get("content-length");
|
|
176
|
-
if (declared && /^\d+$/u.test(declared) && Number(declared) > maximumBytes) {
|
|
175
|
+
if (maximumBytes !== null && declared && /^\d+$/u.test(declared) && Number(declared) > maximumBytes) {
|
|
177
176
|
throw new AppError(502, "AI_RESPONSE_TOO_LARGE", `AI 供应商响应超过 ${maximumBytes} 字节上限`);
|
|
178
177
|
}
|
|
179
|
-
if (!response.body)
|
|
180
|
-
|
|
178
|
+
if (!response.body) {
|
|
179
|
+
const text = await response.text();
|
|
180
|
+
if (isAiResponseByteLimitExceeded(Buffer.byteLength(text, "utf8"), maximumBytes)) {
|
|
181
|
+
throw new AppError(502, "AI_RESPONSE_TOO_LARGE", `AI 供应商响应超过 ${maximumBytes} 字节上限`);
|
|
182
|
+
}
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
181
185
|
const reader = response.body.getReader();
|
|
182
186
|
const chunks = [];
|
|
183
187
|
let total = 0;
|
|
@@ -188,7 +192,7 @@ export async function readResponseTextLimited(response, maximumBytes = AI_RESPON
|
|
|
188
192
|
if (!value?.byteLength)
|
|
189
193
|
continue;
|
|
190
194
|
total += value.byteLength;
|
|
191
|
-
if (total
|
|
195
|
+
if (isAiResponseByteLimitExceeded(total, maximumBytes)) {
|
|
192
196
|
await reader.cancel().catch(() => undefined);
|
|
193
197
|
throw new AppError(502, "AI_RESPONSE_TOO_LARGE", `AI 供应商响应超过 ${maximumBytes} 字节上限`);
|
|
194
198
|
}
|
|
@@ -236,7 +240,6 @@ export function autoRunFailureDisposition(error, attemptCount) {
|
|
|
236
240
|
}
|
|
237
241
|
const DESKTOP_LOCAL_AI_RUN_LIMIT = 20;
|
|
238
242
|
const DESKTOP_LOCAL_AI_RUN_RETENTION_MS = 10 * 60_000;
|
|
239
|
-
const DESKTOP_LOCAL_AI_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
|
|
240
243
|
const allowedParameters = new Set(["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "seed"]);
|
|
241
244
|
const DEFAULT_MAX_TOKENS = 32_000;
|
|
242
245
|
const MAX_MODEL_OUTPUT_TOKENS = 2_000_000;
|
|
@@ -5961,8 +5964,10 @@ export class AiManager {
|
|
|
5961
5964
|
if (pending.request.requestId !== input.requestId) {
|
|
5962
5965
|
throw new AppError(409, "DESKTOP_LOCAL_AI_REQUEST_MISMATCH", "Desktop 本地 AI 响应与当前请求不匹配");
|
|
5963
5966
|
}
|
|
5964
|
-
|
|
5965
|
-
|
|
5967
|
+
const maximumResponseBytes = resolveAiResponseMaxBytes();
|
|
5968
|
+
const responseBytes = Buffer.byteLength(input.body, "utf8");
|
|
5969
|
+
if (isAiResponseByteLimitExceeded(responseBytes, maximumResponseBytes)) {
|
|
5970
|
+
throw new AppError(413, "DESKTOP_LOCAL_AI_RESPONSE_TOO_LARGE", `Desktop 本地 AI 响应超过 ${maximumResponseBytes} 字节上限`);
|
|
5966
5971
|
}
|
|
5967
5972
|
run.pending = null;
|
|
5968
5973
|
run.status = "running";
|
|
@@ -9973,6 +9978,7 @@ export class AiManager {
|
|
|
9973
9978
|
onThinkingDelta(finalReasoning);
|
|
9974
9979
|
}
|
|
9975
9980
|
};
|
|
9981
|
+
const maximumResponseBytes = resolveAiResponseMaxBytes();
|
|
9976
9982
|
let receivedBytes = 0;
|
|
9977
9983
|
let readerEnded = false;
|
|
9978
9984
|
try {
|
|
@@ -9988,9 +9994,9 @@ export class AiManager {
|
|
|
9988
9994
|
}
|
|
9989
9995
|
if (chunk.value?.byteLength) {
|
|
9990
9996
|
receivedBytes += chunk.value.byteLength;
|
|
9991
|
-
if (receivedBytes
|
|
9997
|
+
if (isAiResponseByteLimitExceeded(receivedBytes, maximumResponseBytes)) {
|
|
9992
9998
|
await reader.cancel().catch(() => undefined);
|
|
9993
|
-
throw new AppError(502, "AI_RESPONSE_TOO_LARGE", `AI 供应商响应超过 ${
|
|
9999
|
+
throw new AppError(502, "AI_RESPONSE_TOO_LARGE", `AI 供应商响应超过 ${maximumResponseBytes} 字节上限`);
|
|
9994
10000
|
}
|
|
9995
10001
|
}
|
|
9996
10002
|
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|